Nginxのupstreamに安易にlocalhostと書くと嵌まる罠、またはドメイン名解決の挙動

Nginxで、アクセスしたらちゃんと返ってきているのにログを見ると connect() failed (111:Connection refused) が頻発に発生している謎を調査したら、
いままで知らなかったNginxの挙動にたどり着いたのでメモ。

(ただしよく見るとドキュメントにちゃんと書いてあったというオチ)

問題

nginx_default.conf
upstream rails-unicorn {
    server localhost:3000;
}

こんな風に書いていたら

[error] 11#11: *21735 connect() failed (111: Connection refused) while connecting to upstream, client: xx.xx.x.x, server: example.com, request: "GET /hogehoge HTTP/2.0", upstream: "http://[::1]:3000/hogehoge", host: "xx.xx.x.x", referrer: "http://xx.xx.x.x:80/hogehoge"

[warn] 11#11: *21735 upstream server temporarily disabled while connecting to upstream, client: xx.xx.x.x, server: example.com, request: "GET /hogehoge HTTP/2.0", upstream: "http://[::1]:3000/hogehoge", host: "xx.xx.x.x", referrer: "http://xx.xx.x.x:80/hogehoge"

ログに↑のメッセージがしょっちゅうでていた。

解決方法

localhost ではなく 127.0.0.1 とすると解消する

nginx_default.conf
upstream rails-unicorn {
    server 127.0.0.1:3000;
}

[Sy] nginx(リバースプロキシ)+node.jsでconnect() failedが頻発する場合の対処 | Syntax Error.

何故なのか

nginxのupstream serverは、複数のIPを持つドメイン名があると、それぞれのIPごとに複数のserverを定義したことになるから

ドキュメント : http://nginx.org/en/docs/http/ngx_http_upstream_module.html#server

A domain name that resolves to several IP addresses defines multiple servers at once.

で、localhost127.0.0.1 (IPv4), ::1 (IPv6) どちらともに名前解決される設定になっていた

/etc/hosts
127.0.0.1   localhost
::1 localhost

つまり

nginx_default.conf
upstream rails-unicorn {
    server localhost:3000;
}

nginx_default.conf
upstream rails-unicorn {
    server 127.0.0.1:3000;
    server ::1:3000;
}

と書いてることになっていた。

そのためこんな動作になっていた模様。

  1. upstreamにserverを複数書くとラウンドロビンでロードバランシングされるので、IPv4とIPv6で交互にバックエンド(rails-unicorn)を呼ぼうとする。

  2. バックエンドのunicornではIPv4しかlistenしていなかったため、 ::1:3000(IPv6で)は繋がらず、
    Nginxは

    connect() failed (111: Connection refused) while connecting to upstream
    

    とログを残し、

  3. 繋がらなかった::1:3000をしばらくバランシング対象から外す

    upstream server temporarily disabled while connecting to upstream
    
  4. ここで、まだ他のバランシング先(127.0.0.1:3000)があるので、アクセス元にエラーはまだ帰らず、Ipv4の方に接続を試みにいく

  5. Ipv4ではちゃんとつながるので無事レスポンスが返される

  6. バランシングから外された::1:3000しばらくするとNginxは::1:3000をバランシングに復帰させてみる

  7. もちろんつながらないので、以下これが繰り返されてエラーログが吐かれ続ける

おわり

バックエンドがIPv6でもlistenしていれば server localhost:3000 と書いててもエラーは出ないが、同じサーバをIPv4とIpv6でラウンドロビンさせることに意味はないので localhost とは書かず 127.0.0.1::1 かで書くようにしたほうが良さそう。

複数のIPを持つドメイン名があると、それぞれのIPごとに複数のserverを定義したことになるのは localhost に限らず(むしろ特殊)どんなドメインに対してもなので、気をつけておかないと他でも嵌りそう。