今携わっている案件で、ファイルアップロード時にサイズ超過エラーが発生するとエラーレスポンスが遅延する事象にぶちあたってしまいました。実はかなり前からぶちあたっていたのですが、いよいよ最終的な対応方法を決めないといけなくなってきたので、ちょっとだけ真面目に調べてみました。
事象
事象としては、リクエストしてから約60秒後にエラーレスポンスが返却された・・・。ログを見る限りではアプリ的にはもっと早く検知しているのですが・・・。
この事象はどうやら分割レスポンス(Transfer-Encoding: chunked
)の時に発生するようで、ためしにJSON文字列(Content-Length
がある状態)で返却したら遅延は発生しませんでした。この事象が発生したアプリでは、エラーレスポンスをSpring MVC+Jacksonの仕組み(HttpMessageConverter
)を使ってJavaBean->JSONにしていたため、分割レスポンス扱いになります。
どこで処理が止まっているのかデバッグ実行して調べてみたところ・・・サイズ超過検知後に残りのリクエストデータを読み込む処理のところで処理が止まっていました。どうやら・・・NginxとTomcatがお互いにデータの読み込み待ち状態になってしまい、Nginx側のタイムアウト(デフォルトだと60秒)検知をトリガーにそれぞれ処理が再開した模様・・・
Spring(Spring Boot)アプリ
アップロード用のコントローラはこんな感じ。
@RestController
public class FileUploadRestController {
@RequestMapping("/files")
String upload(MultipartFile multipartFile) {
return multipartFile.getOriginalFilename();
}
@ExceptionHandler
@ResponseStatus(HttpStatus.PAYLOAD_TOO_LARGE)
public ApiError handleMultipartException(MultipartException e) {
ApiError error = new ApiError();
error.setCode("UPLOAD_SIZE_ERROR");
return error;
}
}
エラー時のJSONを表現するJavaBeanはこんな感じ。
public class ApiError implements Serializable {
private String code;
public String getCode() {
return code;
}
}
Spring MVCの例外ハンドラでエラー検知できるようにするために、マルチパートリクエストの解析を遅らせるようにします。
@Configuration
public class WebMvcConfig {
@Bean
StandardServletMultipartResolver multipartResolver() {
StandardServletMultipartResolver resolver = new StandardServletMultipartResolver();
resolver.setResolveLazily(true); // trueを指定すると解析を遅延させることができる
return resolver;
}
}
なお、上記アプリコード(本事象の検証用に使用したアプリ)はGitHubに公開してあります。
原因は?
本質的な原因なのかはかなり怪しいですが・・・・
Nginxがバックサーバーにプロキシする際のHTTPバージョンが1.0(Nginxのデフォルト値のまま)だったことが、この事象を引き起こしていました。分割レスポンスはHTTP 1.1でサポートされた仕組みなので、Tomcat側で行う処理がHTTP 1.0向けになったことで期待通りの動きにならなかったみたいです。(なんとなく、Tomcat側にも何かしらの問題があるような気はするが・・・)
Nginxのプロキシ時のHTTPバージョンを1.1にする
プロキシ時のHTTPバージョンの指定は、proxy_http_version
を使用します。
http {
# ...
upstream spring-boot {
server springboot:8080;
}
# ...
server {
listen 80;
server_name localhost;
# ...
client_max_body_size 100m;
# ...
location / {
proxy_pass http://spring-boot/;
proxy_http_version 1.1; #### これを追加 ###
}
# ...
}
# ...
}
まとめ
たぶん、ぶちあたっていた事象は解決しそうだが・・・・なんかモヤモヤ感がとれないな〜
今のプロジェクト・・・Nginxに詳しい人がいないけど、なぜか使っているw