Nginx balancer_by_luaの話とupstream名前解決の話

## balancer_by_lua_xxxxx

いつの間にやら[^1] lua-nginx-module に `balancer_by_lua_xxx` という新しいディレクティブが増えていました。
以下[ドキュメント](https://github.com/openresty/lua-resty-core/blob/master/lib/ngx/balancer.md)より抜粋。

```nginx
http {
    upstream backend {
        server 0.0.0.1;   # just an invalid address as a place holder

        balancer_by_lua_block {
            local balancer = require "ngx.balancer"

            local host = "127.0.0.2"
            local port = 8080

            local ok, err = balancer.set_current_peer(host, port)
            if not ok then
                ngx.log(ngx.ERR, "failed to set the current peer: ", err)
                return ngx.exit(500)
            end
        }

        keepalive 10;  # connection pool
    }
}
```

見た通りなのですが、upstreamディレクティブの中でプロキシ先を動的にゴニョゴニョしたい！
などという変態的な要件を満たしてくれるものです。
前段フェーズでホスト先を生成して ngx.ctxなどに突っ込み、それを balancer_by_lua で受け取ってプロキシする、という使い方が主になりますでしょうか。

## upstreamで名前解決したい問題

話は変わって、Nginxでの名前解決の話題。
Nginxは基本デーモン起動時以外で`DNS情報を更新しません。`
条件によってはresolverを見てくれたりするのですが、特にupstreamディレクティブで名前解決するのは大変難儀なのです。
（みなさんも upstream keepalive したいですよね？？？）
くわしくは hirose31さんの [Nginxでproxy_passにホスト名を書いた時の名前解決のタイミング](http://d.hatena.ne.jp/hirose31/20131112/1384251646)の記事が大変詳しいです。ありがたや。

先の記事にもあるのですが、解決方法としては

1. Nginx Plusの [resolveパラメータ](http://nginx.org/en/docs/http/ngx_http_upstream_module.html#server)を有効にする
2. サードパーティーモジュールの [nginx-upstream-dyanmic-servers](https://github.com/GUI/nginx-upstream-dynamic-servers)をadd_moduleし、resolveパラメータを有効にする

といった解決方法があります。
今回は札束で殴れなかったので後者のモジュールを利用していたのですが、
「特定の環境にプロキシするとnginxがsegmentation faultで落ちる」
という挙動に悩まされ、今は利用を停止しています[^2] 

そこで、第3の解決方法として balancer_by_lua が使えるのでは？と考え検証してみました。

### balancer_by_luaバージョンで名前解決する

[ドキュメント](https://github.com/openresty/lua-resty-core/blob/master/lib/ngx/balancer.md#set_current_peer)を確認するとset_current_peer に直接ドメインを指定することは出来ないようです。
推奨方法として [lua-resty-dns](https://github.com/openresty/lua-resty-dns) モジュールで受け取った結果を ngx.ctx で引き回せば良い、とあります。
なお lua-resty-dnsモジュールはOpenRestyにバンドルされているので
インストールなどは特に不要です。

```lua
rewrite_by_lua_block {
    local resolver = require "resty.dns.resolver"
    local r, err = resolver:new {
      nameservers = {{"127.0.0.1", 53}},
      retrans = 3,    -- 3 retransmissions on receive timeout
      timeout = 1000, -- 1 sec
    }
    if not r then
      ngx.log(ngx.ERR, "failed to instantiate the resolver: ", err)
      ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
    end

    -- dns query
    local answers, err = r:query(ngx.var.proxy_upstream_port)
    if not answers then
      ngx.log(ngx.ERR, "failed to query the DNS server: ", err)
      ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
    end
    if answers.errcode then
      ngx.log(ngx.ERR, "dns server returned error code: ", answers.errcode, ": ", answers.errstr)
      ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
    end

    -- set ctx
    ngx.ctx.upstream_servers = answers
}
```

コード例としてはこんな感じ。
lua-resty-dnsで取得した結果(配列)をngx.ctxに詰め込んでいます。
あとはbalancer_by_lua側でset_current_peerに設定すると問題なく動作するはずです。
ちなみに、lua-resty-dnsで取得した結果の属性などは[こちら](https://github.com/openresty/lua-resty-dns#query)を参照ください。

### 名前解決の検証内容と結果

プロキシサーバからとあるドメインへプロキシしています。
そのドメインは「サーバA」「サーバB」に対するラウンドロビンの設定がされていると仮定します。
その時、サーバBのwebサーバを停止し、かつドメインからサーバBへの参照を削除したときの挙動を確認しました。
（※なお、切り替えの挙動を確実に確認したいため [max_failsとfail_timeoutをoffにして](https://gist.github.com/toritori0318/f9be21fb8df9e4d5768bb5f484567175#file-nginx_for_verification-conf-L103)あえてnext_streamにフォールバックしないように設定しています）

想定通りであれば、生upsteamではサーバBへの参照が残るためエラーが発生し、
balancer_by_luaではサーバBの参照が削除されるためエラーが解消するはずです。 

以下、上記作業した直後のスクリーンショットです。
左が生upstream、右がbalancer_by_luaのログで、httpingで定期的にアクセスし続けている時のログとなります。
![スクリーンショット 2016-08-31 3.00.47_mz.png](https://qiita-image-store.s3.amazonaws.com/0/9099/0e970f1d-f8b5-0d3b-ad37-523abdfecae0.png)


webサーバを停止したタイミングでは502エラーが発生していたのですが
想定通り、Route53の定義が変更されたタイミングで右側(balancer_by_lua)でのエラーは解消されました。
問題なく名前解決されているようです！

## balancer_by_luaの挙動について

単純に振り分けるだけであればサンプル通りに記述するだけで良いのですが、
何もしないと proxy_next_upstream のように自動で次の upstream にアクセスしてくれません（upstreamのホストを自前で管理するので当たり前ですね）。

そのため、同じような挙動にしたい場合は自前で書く必要があります。

```lua
balancer_by_lua_block {
    local balancer = require "ngx.balancer"
    if not ngx.ctx.tries then
        ngx.ctx.tries = 0
    end

    -- dnsで引けたサーバ数まで繰り返し
    if ngx.ctx.tries < #ngx.ctx.upstream_servers then
        b.set_more_tries(1)
    end
    ngx.ctx.tries = ngx.ctx.tries + 1

    -- get upstream server
    local host = ngx.ctx.upstream_servers[ngx.ctx.tries]["address"]
    local port = ngx.var.proxy_upstream_port
    local ok, err = balancer.set_current_peer(host, port)
    if not ok then
        ngx.log(ngx.ERR, "failed to set the peer: ", err)
    end
}
```

フルのexampleは[こちら](https://gist.github.com/toritori0318/f9be21fb8df9e4d5768bb5f484567175#file-nginx_for_production-conf)を参照ください。

- proxy_next_upstream_tries の値をmax値とし、以下を繰り返します
   - ngx.ctx.upstream_serversのhostを読み込み
   - set_current_peerを実行。エラーが発生するとbalancer_by_lua_block が再度実行される

まず[proxy_next_upstream_tries](https://gist.github.com/toritori0318/f9be21fb8df9e4d5768bb5f484567175#file-nginx_for_production-conf-L88)をnginx側の設定で明示的に設定する必要があります。
かつ、ngx.ctx.triesでリトライする数を管理しつつ、DNSで引けた数をmaxとしてnext_upstreamへリトライを繰り返しています。


## 性能検証

### 構成

<img width="302" alt="スクリーンショット 2016-09-01 0.54.26.png" src="https://qiita-image-store.s3.amazonaws.com/0/9099/b8064b61-dc1e-9399-c511-7884b009cd5b.png">

### サーバインスタンス

role | type
-----|-----
proxy | c4.large
upstream_server | t2.micro (x2)

### ベンチマークコマンド

```
wrk -c 10 -t 2 -d 60 http://proxyserverdayo:xx/
```

### 構成詳細

80番ポートは nginxのupstreamを利用してローカルダミーサーバへのプロキシを行っています。
81番ポートは balancer_by_luaを利用してローカルダミーサーバへのプロキシを行っています。
82番ポートは nginxのupstreamを利用して外部サーバへのプロキシを行っています。
83番ポートは balancer_by_luaを利用して外部サーバへのプロキシを行っています。またlua-resty-dnsを利用して名前解決も行います。
84番ポートは83番ポートと同様の機能ですが、名前解決した結果を数秒キャッシュしています。

実際に検証したconfigは[こちら](https://gist.github.com/toritori0318/f9be21fb8df9e4d5768bb5f484567175#file-nginx_for_verification-conf)になります。

結果は以下のとおりです。

ポート | 説明 | req/sec
------|-----|--------
80 | 生upstream > local | 23370
81 | balancer_by_lua > local | 21817
82 | 生upstream > external | 16443
83 | balancer_by_lua > external | 7702
84 | balancer_by_lua > external (dnsキャッシュ) | 15237

80と81の差が 生upsteamとbalancer_by_lua の性能差になると思いますが、
実際に処理できるリクエスト数を考えるとほぼ誤差程度の結果でした。
また83の外部プロキシバージョンではDNSを毎回引き直しているのでその分劣化してしまいましたが、
84のキャッシュバージョンではほぼ同値になりました。


## まとめ

Nginxの新ディレクティブ balancer_by_lua の話とそれを利用した名前解決の検証を行いました。
要件を満たし、かつパフォーマンスも問題ないため
近々プロダクションへ投入してみたいと思います！


## サンプルコード

- https://gist.github.com/toritori0318/f9be21fb8df9e4d5768bb5f484567175

[^1]: どうやら [1.9.7.2](http://lua-users.org/lists/lua-l/2016-01/msg00129.html) から利用できるようです。
[^2]: おそらく [こちら](https://github.com/GUI/nginx-upstream-dynamic-servers/issues/18)のissueと同じ現象かと思われます。まだ未解決…