Edited at

nginxのupstreamコンテキストで有償のresolveオプションを使わずに動的にDNS解決する

More than 3 years have passed since last update.


TL;DR


  • nginxからリバプロ先のバックエンドがELBの場合は、IPアドレスがキャッシュされないようにDNS名を動的に解決しないといけない。

  • serverコンテキストならset変数使うハックで回避可能だけど、この技はupstreamコンテキストで使えない。

  • nginxのupstreamコンテキストでresolveオプション使おうと思ったら有償版の商用機能だった罠。

  • upstreamコンテキストで必要な負荷分散しつつ、Unixドメインソケットを1段挟んで、serverコンテキストでset変数するハックと組み合わせると動的にDNS解決できた。


経緯

nginxをリバースプロキシとして使っていて、バックエンドにELBみたいな構成の場合、DNS名を起動時に名前解決してIPアドレスをキャッシュしてしまうので、IPアドレスが変わると繋がらなくなるというのはよくあるハマりポイントみたいです。

で、調べると色んな方法が出てくるわけですが、このへんの記事がよくまとまっていました。

Nginxでproxy_passにホスト名を書いた時の名前解決のタイミング


  1. setディレクティブで変数にホスト名をセットし、proxy_passではその変数を参照=>upstreamコンテキストで使えない

  2. serverディレクティブにresolveパラメータをつける=>商用機能

  3. OSSで公開されている拡張モジュールGUI/nginx-upstream-dyanmic-serversを使う

単純なproxy_passであれば1番の方法で解決しますが、諸般の事情で、以下のような構成になっていて、upstreamコンテキストを使って、2系統に重み付けで振り分けたりfailoverさせたりみたいな構成で使いたかったので、なんとしてもupstreamコンテキストで使いたいという事情がありました。

client -> ELB -> nginx -> ELB -> appサーバ(backend1系)

-> ELB -> appサーバ(backend2系)

で、2番の方法をnginxのマニュアルで見つけてresolveオプションでいけるやんと喜んでいたら商用機能であることが判明し、絶望の淵に叩き落とされ一喜一憂。nginxは必要な機能がちょいちょい有償版でしか使えないので油断ならないです(´・ω・`)

で。希望を託して3番の方法をやってみたのですが、私の環境ではうまく動きませんでした。うまく動かないとは、簡単な動作確認ではDNS解決できており、機能的には問題なかったのですが、しばらくテスト環境で検証してたら、nginxのCPUが100%に張り付いてハングする事象が結構な頻度で発生しました。

問題の事象を確認したのはnginx 1.9.10で、再現条件や詳細な原因が不明なのですが、ハングしている状態でstrace取ってみると sched_yield() が全力で無限ループしており、疑わしいのが追加の拡張モジュールで入れたnginx-upstream-dyanmic-serversしかなく、このモジュール自体既にメンテされていないようなので、証拠不十分ですがnginx-upstream-dyanmic-serversを使わない方針で、第4の方法を思いついたので共有しておきます。


解決策

状況を改めて整理すると以下のとおりです。


  • nginxはデフォルトだとプロセス起動時にDNS解決してIPをキャッシュしてしまう

  • proxy_passの連携先がELBの場合は動的にIPアドレスが変わる可能性がある

  • server/locationコンテキストでresolverの設定をいれた状態でproxy先をset変数で定義するのを組み合わせると動的に名前解決されるハックがある(上記の1番目の方法)

  • このハックはupstreamコンテキストでは使えない

  • upstreamコンテキストのresolveオプションは商用版でしか使えない

  • 負荷分散やフェールオーバの制御でupstreamコンテキストは使いたい

ということはupstreamで名前解決しないようにすればよいわけです。つまり、役割分担して


  • upstreamコンテキストで負荷分散やフェールオーバの制御しつつローカルのUnixドメインソケットに振り分ける

  • 振り分け先のserverコンテキストでset変数のハックを使って動的に名前解決させる

という合わせ技でいけました。設定ファイルはこんなかんじ。


nginx.conf

http {

...
resolver 10.9.0.2 valid=5s;

upstream backend {
server unix:/var/run/nginx_backend1.sock weight=9 max_fails=1 fail_timeout=20s;
server unix:/var/run/nginx_backend2.sock weight=1;
}

server {
listen 8080;
server_name example.com;
proxy_next_upstream error timeout http_502 http_503 http_504;;
...

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

server {
listen unix:/var/run/nginx_backend1.sock;
server_name example.com;
...

set $lb_backend1 "internal-lb-backend1-XXXXXXXXX.ap-northeast-1.elb.amazonaws.com";

location / {
proxy_pass http://$lb_backend1:80;
}
}

server {
listen unix:/var/run/nginx_backend2.sock;
server_name example.com;
...

set $lb_backend2 "internal-lb-backend2-XXXXXXXXX.ap-northeast-1.elb.amazonaws.com";

location / {
proxy_pass http://$lb_backend2:80;
}
}
}