nginx の consistent hash は本当に consistent なのか
nginx の upstream, consistent hash の挙動について
nginx でリバースプロキシを作る際に、同じ URL へのアクセスは同じサーバーに流したい場合があります。 バックエンド側でコンテンツのキャッシュをしている場合等です。
nginx で以下のように設定した場合、サーバーの追加や削除で選ばれる対象がどのように変化するのか調査しました。
upstream myapp1 { hash $host$uri consistent; server 127.0.0.1:10080 max_fails=100 fail_timeout=10; server 127.0.0.2:10080 max_fails=100 fail_timeout=10; server 127.0.0.3:10080 max_fails=100 fail_timeout=10; server 127.0.0.4:10080 max_fails=100 fail_timeout=10; }
Consitent hash 自体については下記の URL が詳しいです。
http://alpha.mixi.co.jp/entry/2008/10691/
期待する挙動について
便宜上、上記設定の 127.0.0.1 はサーバー1、127.0.0.2 はサーバー2 ... 127.0.0.4 はサーバー4 と呼称することにします。
Consistent hash について、インフラ担当の希望としては、以下のようになってくれることを期待します。
- サーバー3 がダウンした(もしくは外された)場合 1, 2, 4 に振られていた URL はそのままでサーバー3 に振っていた URL だけが均等に 1, 2, 4 のサーバーに振られる
- サーバー3 が復活した(もしくは増設された)場合、もともとサーバー3 に振られていた URL のみがサーバー3 へ振られるようになる
- サーバー1~4 のところへ新規でサーバー5 を増設した場合、サーバー1~4 から 1/4 ずつ均等にサーバー5 へ振られるようになる
- サーバー1~4 のところからサーバー3 を外し、新規でサーバー5 を増設した場合、残っているサーバーは URL の一部が再配置されるが、それほど変わらず、サーバー5 にはサーバー3 から多めに振られる
結論から言うと、多少の誤差はあります、このような挙動になっていました。
nginx を使ってのテスト
今回はマシン 1台でテストします。 同じマシンに Apache(ポート10080)、nginx(ポート80) を同時に稼働させ、Apache にはバックエンドの役割をさせます。 Apache が返す結果は 404 でも何でもいいので、デフォルト設定のまま、ポートだけ 10080 に変更しました。
[root @nv003-nginx ~]# netstat -npl | egrep 'apache|nginx' tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN 10014/nginx -g daem tcp 0 0 0.0.0.0:10080 0.0.0.0:* LISTEN 3450/apache2
nginx の設定は下記のとおりです。Ubuntu の sites-available 直下のファイルに以下の設定を入れる感じです。
upstream myapp1 { hash $host$uri consistent; server 127.0.0.1:10080 max_fails=100 fail_timeout=10; server 127.0.0.2:10080 max_fails=100 fail_timeout=10; server 127.0.0.3:10080 max_fails=100 fail_timeout=10; server 127.0.0.4:10080 max_fails=100 fail_timeout=10; } log_format chash-proxy-v2 'uri:$uri\tupstream_addr:$upstream_addr\t'; server { listen 80 default_server; root /var/www/html; index index.html index.htm index.nginx-debian.html; server_name _; access_log /var/log/nginx/chash-access-v2.log chash-proxy-v2; error_log /var/log/nginx/chash-error.log; location / { proxy_pass http://myapp1; # proxy_next_upstream error timeout; proxy_next_upstream off; proxy_next_upstream_tries 0; proxy_next_upstream_timeout 0; proxy_connect_timeout 5s; proxy_send_timeout 5s; proxy_read_timeout 5s; } }
テスト内容について
上記設定の log_format chash-proxy-v2 'uri:$uri\tupstream_addr:$upstream_addr\t';
のログを利用して、アクセスした URL と選ばれたバックエンドの関係を調べることにします。
URL のパターンは
http://127.0.0.1:80/0000 http://127.0.0.1:80/0001 ... http://127.0.0.1:80/9998 http://127.0.0.1:80/9999
という1万パターンの URL を作成し、ウェブベンチマークツール(ここでは siege を使いました)を使い、アクセスすることを考えます。
- アクセスをし、アクセスログを入手
- アクセスログからバックエンド(サーバー1~4)に振られた URL を抽出する -> データA とする
- バックエンドに変化を起こす(サーバー3 を止める等)
- 再度アクセスをし、アクセスログを入手
- アクセスログからバックエンド(サーバー1~4)に振られた URL を抽出する -> データB とする
- データA とデータB を比較して URL とバックエンドの関係の変化を確認する
基準データ(バックエンドはサーバー1~4)
それぞれ下記のような件数となりました。
サーバー | URL パターン数 |
---|---|
1 | 2295 |
2 | 2302 |
3 | 2977 |
4 | 2426 |
これを基準データとします。 全て足すと 10000 となります。 意外にもこの段階から均等とはなっていません。
テスト1 (サーバー3 をダウンさせ、選ばれ方がどうのようになるか)
- サーバー3 がダウンした(もしくは外された)場合 1, 2, 4 に振られていた URL はそのままでサーバー3 に振っていた URL だけが均等に 1, 2, 4 のサーバーに振られる
- サーバー3 が復活した(もしくは増設された)場合、もともとサーバー3 に振られていた URL のみがサーバー3 へ振られるようになる
上記の期待についての検証です。 サーバー3 をダウンさせ、選ばれ方がどうのようになるか確認します。 ダウン前の状態を「サーバー1, 2, 4 の状態にサーバー3 を追加した」と考え、上記2つについての検証をします。
サーバー | 基準データ | 今回のテスト結果 | 基準データとの一致件数 | 残りの件数 | 残りの件数のソース |
---|---|---|---|---|---|
1 | 2295 | 3259 | 2295 | 964 | サーバー3 から 964 件移動 |
2 | 2302 | 3574 | 2302 | 1272 | サーバー3 から 1272 件移動 |
3 | 2977 | 0 | 0 | 0 | 0 |
4 | 2426 | 3167 | 2426 | 741 | サーバー3 から 741 件移動 |
数字は URL パターン件数です
基準データはそのままにサーバー3 に振られていた URL のみほぼ均等に分散しています。 期待通りの動作です。
ちなみに nginx の設定ファイルの書き方としては下記の 2パターンどちらも同じでした。
upstream myapp1 { hash $host$uri consistent; server 127.0.0.1:10080 max_fails=100 fail_timeout=10; server 127.0.0.2:10080 max_fails=100 fail_timeout=10; server 127.0.0.3:10080 max_fails=100 fail_timeout=10 down; server 127.0.0.4:10080 max_fails=100 fail_timeout=10; }
upstream myapp1 { hash $host$uri consistent; server 127.0.0.2:10080 max_fails=100 fail_timeout=10; # server 127.0.0.3:10080 max_fails=100 fail_timeout=10 down; server 127.0.0.4:10080 max_fails=100 fail_timeout=10; server 127.0.0.1:10080 max_fails=100 fail_timeout=10; }
テスト2 (サーバー1~4 のところへ新規でサーバー5 を増設)
- サーバー1~4 のところへ新規でサーバー5 を増設した場合、サーバー1~4 から 1/4 ずつ均等にサーバー5 へ振られるようになる
上記期待についての検証です。 サーバー1~4 のところへ新規でサーバー5 を増設し、変化を確認します。
upstream myapp1 { hash $host$uri consistent; server 127.0.0.1:10080 max_fails=100 fail_timeout=10; server 127.0.0.2:10080 max_fails=100 fail_timeout=10; server 127.0.0.3:10080 max_fails=100 fail_timeout=10; server 127.0.0.4:10080 max_fails=100 fail_timeout=10; server 127.0.0.5:10080 max_fails=100 fail_timeout=10; }
サーバー | 基準データ | 今回のテスト結果 | 基準データとの一致件数 | 残りの件数 | 残りの件数のソース |
---|---|---|---|---|---|
1 | 2295 | 1846 | 1846 | 449 | |
2 | 2302 | 1860 | 1860 | 442 | |
3 | 2977 | 2362 | 2362 | 615 | |
4 | 2426 | 1967 | 1967 | 459 | |
5 | - | 1965 | - | - | 449+442+615+459 = 1965 他のサーバーからほぼ均等に割り振り |
数字は URL パターン件数です
これも期待する結果となりました
テスト3 (サーバー3 を外し、サーバー5 追加して変化を見ます。)
- サーバー1~4 のところからサーバー3 を外し、新規でサーバー5 を増設した場合、残っているサーバーは URL の一部が再配置されるが、それほど変わらず、サーバー5 にはサーバー3 から多めに振られる
上記期待への検証です。
upstream myapp1 { hash $host$uri consistent; server 127.0.0.1:10080 max_fails=100 fail_timeout=10; server 127.0.0.2:10080 max_fails=100 fail_timeout=10; # server 127.0.0.3:10080 max_fails=100 fail_timeout=10 down; server 127.0.0.4:10080 max_fails=100 fail_timeout=10; server 127.0.0.5:10080 max_fails=100 fail_timeout=10; }
サーバー | 基準データ | 今回のテスト結果 | 基準データとの一致件数 | 残りの件数 | 残りの件数のソース |
---|---|---|---|---|---|
1 | 2295 | 2374 | 1846 | 528 | サーバー3 から 528 件移動 |
2 | 2302 | 2584 | 1860 | 724 | サーバー3 から 724 件移動 |
3 | 2977 | 0 | 0 | 0 | 0 |
4 | 2426 | 2411 | 1967 | 444 | サーバー3 から 444 件移動 |
5 | - | 2631 | - | - | サーバー3 から 1281 件移動。残りの 1350 件はサーバー1, 2, 4 から移動(それぞれ 449, 442, 459 件) |
数字は URL パターン件数です
サーバー3 からサーバー5 への移動は半分くらいとなり、残り半分は他のサーバーからの再配置となりました。 もう少し頑張って欲しかったですが、ある程度期待している挙動となりました。
おまけ
server にホスト名を使っている場合は、DNS 名前解決結果のアドレスではなく、ハッシュの決定にはホスト名が使われているようです。 下記の 2パターンのテストをした結果、全く同じ URL の振られ方となりました。 IP が変わるけど、同じ URL を降ってほしい時は最初からホスト名を使うほうがいいかもしれません。
/etc/hosts にこれを追加 127.0.0.4 myhost4 upstream myapp1 { hash $host$uri consistent; server 127.0.0.1:10080 max_fails=100 fail_timeout=10; server 127.0.0.2:10080 max_fails=100 fail_timeout=10; server myhost4:10080 max_fails=100 fail_timeout=10; server 127.0.0.5:10080 max_fails=100 fail_timeout=10; }
/etc/hosts にこれを追加 127.0.0.3 myhost4 upstream myapp1 { hash $host$uri consistent; server 127.0.0.1:10080 max_fails=100 fail_timeout=10; server 127.0.0.2:10080 max_fails=100 fail_timeout=10; server myhost4:10080 max_fails=100 fail_timeout=10; server 127.0.0.5:10080 max_fails=100 fail_timeout=10; }
まとめ
nginx upstream の consistent hash は使えることがわかりました。 ほぼアルゴリズムの理論通りに動作するため、consistent hash が必要なところでは頼ってよさそうです。