無印吉澤

ソフトウェア開発、運用管理(俗にいう DevOps)、クラウドコンピューティングなどについて、吉澤が調べたり試したことを書いていくブログです。

Nginx でリバースプロクシを立てるときに気にすべき proxy_next_upstream 設定

f:id:muziyoshiz:20171026000347p:plain:w400

個人的に、Nginx で「これは危険だ」と思っている設定があって、Nginx でなにかあるたびにその設定をつい疑ってしまいます。その設定について他の人に話すたびに、いちいち資料を集めるのが面倒になってきたので、今回はその設定項目についての情報をまとめておきます。

まだ理解に自信がない部分があるので、新しい情報が入ってきたら、この記事を適宜修正します。

リバースプロクシ設定の基本

Nginx をリバースプロクシとして使う時には、ngx_http_upstream_module でサーバのグループを定義します。そして、サーバ名やロケーション(パス)に対して、送信先のグループを指定します。

以下はマニュアルにある例です。その Nginx サーバへのすべてのアクセスを、backend グループに指定されたいずれかのサーバに送信します。

upstream backend {
    server backend1.example.com       weight=5;
    server backend2.example.com:8080;
    server unix:/tmp/backend3;

    server backup1.example.com:8080   backup;
    server backup2.example.com:8080   backup;
}

server {
    location / {
        proxy_pass http://backend;
    }
}

この送信に関わる設定は、proxy_pass を含む ngx_http_proxy_module の方にあります。このモジュールの設定のなかで、(僕が個人的に)よくつまづくのが proxy_next_upstream から始まる設定です。

proxy_next_upstream から始まる設定

これらは、upstream(リクエストの送信先)からエラーが返されたり、リクエストがタイムアウトした場合の動作に関する設定です。

  • proxy_next_upstream
    • 失敗したリクエストを他のサーバに再送する条件(複数指定可)
  • proxy_next_upstream_timeout
    • Nginx 側でリクエストがタイムアウトしたと判断するまでの時間
    • proxy_next_upstream で timeout が指定された場合のみ、この設定が使われる
    • 時間の単位は Configuration file measurement units を参照
  • proxy_next_upstream_tries
    • proxy_next_upstream の条件に合致したリクエストを、最大で何台のサーバに送信するか
    • マニュアルには明示されていないが、この送信回数は最初の1台を含む
      • 1が設定されたら、最初のサーバ1台にしかリクエストを送信しない
      • 3が設定されたら、最初のサーバ1台への送信と、それ以外の2台への再送を行う

これらの設定が明示的に指定されなかった場合のデフォルト値と、その意味は以下の通りです。

  • Default: proxy_next_upstream error timeout;
    • 何らかのエラーが発生した場合、または Nginx 側でリクエストがタイムアウトした場合に、リクエストを再送する
    • ここで言う「エラー」とは、(転送先)サーバへの接続時、リクエストの転送時、またはレスポンスヘッダの読み込み時に発生するエラーのこと
    • 4xx 応答、5xx 応答は、ここで言う「エラー」には含まれない
  • Default: proxy_next_upstream_timeout 0;
    • Nginx 側でのタイムアウトは起こらない(0 は無制限を表す)
  • Default: proxy_next_upstream_tries 0;
    • upstream ディレクトリで定義されたすべてのサーバに対して順番に、エラーが発生したリクエストを再送する(0 は無制限を表す)

不適切な設定が問題になるケース

proxy_next_upstream_tries を指定せずに使っていると、バックエンドのサーバへの接続で何らかのエラーが発生したら、最悪の場合、そのリクエストはすべてのサーバに対して送信 されます。

例えば、以下のような状況になると、無駄なリクエストが Nginx で大量に増幅されて、システム全体の負荷が急増します。

proxy_next_upstream 設定を何も指定していない状態で、
→ アプリケーションサーバが何かのバグで不正なレスポンスを返すようになる
→ そのバグを踏むリクエストが来る
→ そのリクエストがアプリケーションサーバの台数だけ複製される(サーバ10台なら10倍になる)
→ システム全体の負荷が急増

また、proxy_next_upstream_timeout だけ設定していると、こういうこともあり得ます。

proxy_next_upstream_timeout が2秒に設定されていて、proxy_next_upstream_tries は未指定の状態で、
→ 処理時間が2秒を超える重いリクエストが来る
→ その重いリクエストがアプリケーションサーバの台数だけ複製される(サーバ10台なら10倍になる)
→ システム全体の負荷が上がって、普段は2秒未満のリクエストも2秒以上かかるようになる
→ それらのリクエストも10倍に複製される
→ システム全体の負荷が急増

あるべき設定

個人的に考える、あるべき設定は以下の通りです。

proxy_next_upstream_tries は必ず0以外に設定する

この値がデフォルト値の0(無制限)でさえなければ、上記のような問題は起こらないので、まずこれを設定します。

1にすれば再送は起こりませんが、アプリケーションサーバを再起動するような場合にいちいちエラーが出てしまいます。再起動の場合のみを考えるなら、この値が大きすぎても意味はありません。そのため、proxy_next_upstream_tries は2〜3でいいと思います。

proxy_next_upstream_timeout はアプリケーションサーバ側の応答時間より長くする

proxy_next_upstream_timeout がアプリケーションサーバ側の応答時間よりも短いと、せっかくアプリケーションサーバがレスポンスを返しても Nginx で破棄されてしまいます。これではサーバの計算資源の無駄遣いです。

そのため、アプリケーションサーバの応答時間を事前に見積もって、それより長い時間を proxy_next_upstream_timeout に指定しましょう。これは、タイムアウト設計をきちんとしましょう、そして時間がかかる処理(データベース接続)があるならアプリケーション内にきちんとタイムアウト処理を入れましょう、という話ですね。

応答時間の見積もりが難しいなら、proxy_next_upstream_timeout はデフォルト(タイムアウトなし)のままでもいいと思います。

サービスによっては再送処理をオフにする

API 提供時など、クライアント側で再送処理をしてくれるなら、proxy_next_upstream off; を設定し、再送処理をオフにするという手もあります。

関連情報