nginxのリクエストボディのバッファリングに関する問題とその改善策

  • 257
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

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のロケーションにファイルアップロードのリクエストがあった際にバックエンドのアプリケーションにプロキシする設定を考えてみましょう。

nginx.conf
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は実際には以下のような形になっていたためです。

nginx_ssl_spdy.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が有効になっていました。ソースコードを読んでみると、

nginx/src/http/ngx_http_request_body.c
...
#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で聞いてみる

http://mailman.nginx.org/pipermail/nginx/2015-March/047066.html

というわけで聞いてみました。それに対する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のコストを大幅に抑えることができます。