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

nginxのデフォルトの動作ではクライアントから受け取ったリクエストボディをメモリにバッファリングするようになっています。

このメモリバッファのサイズは[client_body_buffer_size](http://nginx.org/en/docs/http/ngx_http_core_module.html#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](http://nginx.org/en/docs/http/ngx_http_core_module.html#client_body_buffer_size)を大きくするという方法がありますが、あまり大きくし過ぎてもメモリを圧迫するので悩ましいところです。なので、大容量のファイルアップロードを受け付けるようなアプリケーションではむしろバッファリングしない方がうれしかったりします。(Unbuffered Uploadという用語がよく使われています)

この問題はnginx界隈では広く認識されていて、(もう長いことメンテされてないけど)[nginx-upload-module](https://github.com/vkholodkov/nginx-upload-module)や、nginxの有名なフォークである[Tengine](https://github.com/alibaba/tengine)で先立ってUnbuffered Upload機能が実装されたりしました。

## proxy_request_buffering

そんな感じでnginx本体では長らくサポートされていなかったUnbuffered Upload機能ですが、nginx-1.7.11から[proxy_request_buffering](http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_request_buffering)というディレクティブが追加されました。このディレクティブを利用するとリクエストボディのバッファリングのON/OFFが切り替えられるようになります。(デフォルトはもちろんONです)

例えば`/upload`のロケーションにファイルアップロードのリクエストがあった際にバックエンドのアプリケーションにプロキシする設定を考えてみましょう。

```nginx: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: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`との差分は以下になります。

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

```c: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の中の人の[回答](http://mailman.nginx.org/pipermail/nginx/2015-April/047088.html)が↓。

```
Yes, it is not currently possible to switch off proxy_request_buffering 
when using SPDY.
```

なお、この問題はTengineでも起こります。

 * https://github.com/alibaba/tengine/issues/444
 * https://github.com/alibaba/tengine/pull/584

なので、今のところ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にするという方法が有効です。

```nginx
client_body_temp_path /dev/shm/client_body_temp 1 2;
```

非常に大きいファイル(GB単位とか)のアップロードが発生するような環境だとtmpfsが溢れてデータが書き込めなくなってしまうケースがあるので注意しましょう。

また、[client_body_in_file_only](http://nginx.org/en/docs/http/ngx_http_core_module.html#client_body_in_file_only)を利用することでリクエストボディをバッファリングしつつ「a client request body is buffered..」の警告メッセージをエラーログに出さないようにすることもできます。

```nginx
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](http://nginx.org/en/docs/http/ngx_http_core_module.html#client_body_in_file_only)を利用した場合、リクエストボディは常にファイルに書き出されてしまいますが、その場合は`client_body_temp_path`をtmpfsに設定することでディスクI/Oのコストを大幅に抑えることができます。