Neinvalli

研究テーマは ”IT 環境でのリソース最適化” です

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. アクセスをし、アクセスログを入手
  2. アクセスログからバックエンド(サーバー1~4)に振られた URL を抽出する -> データA とする
  3. バックエンドに変化を起こす(サーバー3 を止める等)
  4. 再度アクセスをし、アクセスログを入手
  5. アクセスログからバックエンド(サーバー1~4)に振られた URL を抽出する -> データB とする
  6. データ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 が必要なところでは頼ってよさそうです。