nginxのデフォルトの動作ではクライアントから受け取ったリクエストボディをメモリにバッファリングするようになっています。
このメモリバッファのサイズはclient_body_buffer_sizeで変更することができ、リクエストボディのサイズがこのバッファのサイズを越えた場合はclient_body_temp_path
にファイルとして書き出されます。
ログレベルがwarn
以上の場合はエラーログにa client request body is buffered ...
という警告が出ます。
2015/03/29 14:02:20 [warn] 6965#0: *1 a client request body is buffered to a
temporary file /etc/nginx/client_body_temp/0000000001, client: x.x.x.x,
server: example.com, request: "POST /upload HTTP/1.1", host: "example.com"
ワークロードの高い環境ではディスクI/Oは極力抑えたいので、この警告が出ないようにしたいところです。
とりあえずclient_body_buffer_sizeを大きくするという方法がありますが、あまり大きくし過ぎてもメモリを圧迫するので悩ましいところです。なので、大容量のファイルアップロードを受け付けるようなアプリケーションではむしろバッファリングしない方がうれしかったりします。(Unbuffered Uploadという用語がよく使われています)
この問題はnginx界隈では広く認識されていて、(もう長いことメンテされてないけど)nginx-upload-moduleや、nginxの有名なフォークであるTengineで先立ってUnbuffered Upload機能が実装されたりしました。
proxy_request_buffering
そんな感じでnginx本体では長らくサポートされていなかったUnbuffered Upload機能ですが、nginx-1.7.11からproxy_request_bufferingというディレクティブが追加されました。このディレクティブを利用するとリクエストボディのバッファリングのON/OFFが切り替えられるようになります。(デフォルトはもちろんONです)
例えば/upload
のロケーションにファイルアップロードのリクエストがあった際にバックエンドのアプリケーションにプロキシする設定を考えてみましょう。
client_max_body_size 50m;
server {
listen 80;
server_name example.com;
location /upload {
proxy_request_buffering off;
proxy_pass http://upload_backend;
}
}
これで/upload
にアクセスがあった際はリクエストボディがバッファリングされなくなります。めでたしめでたし。
なお、nginxが受け付けれるリクエストボディの最大サイズはclient_max_body_size
で決まります。このサイズのデフォルト値は1MBと非常に小さいのでnginxからファイルアップロード機能を持つアプリケーションサーバにプロキシする際は注意しましょう。リクエストボディのサイズがこの値を越えるとnginxはHTTPステータス413(Request entity too large)を返します。
proxy_request_bufferingが常にONになるケース
さて、proxy_request_buffering off;
と書くことでUnbuffered Uploadが実現できることがわかりました。本来ならこれでめでたしめでたしなのですが、私の場合はそうはなりませんでした。
というのも私のnginx.conf
は実際には以下のような形になっていたためです。
client_max_body_size 50m;
server {
listen 443 ssl spdy;
server_name example.com;
location /upload {
proxy_request_buffering off;
proxy_pass http://upload_backend;
}
}
さきほどのnginx.conf
との差分は以下になります。
--- nginx.conf 2015-04-01 21:30:15.000000000 +0900
+++ nginx_ssl_spdy.conf 2015-04-01 21:29:57.000000000 +0900
@@ -1,6 +1,6 @@
client_max_body_size 50m;
server {
- listen 80;
+ listen 443 ssl spdy;
server_name example.com;
location /upload {
そう、私の環境ではSPDYが有効になっていました。ソースコードを読んでみると、
...
#if (NGX_HTTP_SPDY)
if (r->spdy_stream && r == r->main) {
r->request_body_no_buffering = 0;
rc = ngx_http_spdy_read_request_body(r, post_handler);
goto done;
}
#endif
...
という感じでSPDYを利用しているときにrequest_body_no_buffering
が0にセットされています。どうもSPDYを利用しているときはリクエストボディはproxy_request_buffering
の設定に関わらず常にバッファリングされてしまうようです。
追記:また、chunked transfer encodingが使われている場合も常にバッファリングされるようです -> http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_request_buffering
MLで聞いてみる
というわけで聞いてみました。それに対するnginxの中の人の回答が↓。
Yes, it is not currently possible to switch off proxy_request_buffering
when using SPDY.
なお、この問題はTengineでも起こります。
なので、今のところSPDYを利用している場合リクエストボディは常にバッファリングされる、ということでFAのようです。
追記(2016/04/08): nginx-1.9.14からHTTP/2でproxy_request_bufferingでバッファリングをoffにできるようになったようです
proxy_request_bufferingをOFFにせずにディスクI/Oのコストを減らす
リクエストボディをバッファリングしつつ、ディスクI/Oを極力抑えるにはclient_body_temp_pathをtmpfsにするという方法が有効です。
client_body_temp_path /dev/shm/client_body_temp 1 2;
非常に大きいファイル(GB単位とか)のアップロードが発生するような環境だとtmpfsが溢れてデータが書き込めなくなってしまうケースがあるので注意しましょう。
また、client_body_in_file_onlyを利用することでリクエストボディをバッファリングしつつ「a client request body is buffered..」の警告メッセージをエラーログに出さないようにすることもできます。
client_body_temp_path /dev/shm/client_body_temp 1 2;
location /upload {
client_body_in_file_only clean;
proxy_pass http://upload_backend;
}
もちろんclient_body_in_file_onlyを利用した場合、リクエストボディは常にファイルに書き出されてしまいますが、その場合はclient_body_temp_path
をtmpfsに設定することでディスクI/Oのコストを大幅に抑えることができます。