balancer_by_lua_xxxxx
いつの間にやら1 lua-nginx-module に balancer_by_lua_xxx
という新しいディレクティブが増えていました。
以下ドキュメントより抜粋。
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にホスト名を書いた時の名前解決のタイミングの記事が大変詳しいです。ありがたや。
先の記事にもあるのですが、解決方法としては
- Nginx Plusの resolveパラメータを有効にする
- サードパーティーモジュールの nginx-upstream-dyanmic-serversをadd_moduleし、resolveパラメータを有効にする
といった解決方法があります。
今回は札束で殴れなかったので後者のモジュールを利用していたのですが、
「特定の環境にプロキシするとnginxがsegmentation faultで落ちる」
という挙動に悩まされ、今は利用を停止しています2
そこで、第3の解決方法として balancer_by_lua が使えるのでは?と考え検証してみました。
balancer_by_luaバージョンで名前解決する
ドキュメントを確認するとset_current_peer に直接ドメインを指定することは出来ないようです。
推奨方法として lua-resty-dns モジュールで受け取った結果を ngx.ctx で引き回せば良い、とあります。
なお lua-resty-dnsモジュールはOpenRestyにバンドルされているので
インストールなどは特に不要です。
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で取得した結果の属性などはこちらを参照ください。
名前解決の検証内容と結果
プロキシサーバからとあるドメインへプロキシしています。
そのドメインは「サーバA」「サーバB」に対するラウンドロビンの設定がされていると仮定します。
その時、サーバBのwebサーバを停止し、かつドメインからサーバBへの参照を削除したときの挙動を確認しました。
(※なお、切り替えの挙動を確実に確認したいため max_failsとfail_timeoutをoffにしてあえてnext_streamにフォールバックしないように設定しています)
想定通りであれば、生upsteamではサーバBへの参照が残るためエラーが発生し、
balancer_by_luaではサーバBの参照が削除されるためエラーが解消するはずです。
以下、上記作業した直後のスクリーンショットです。
左が生upstream、右がbalancer_by_luaのログで、httpingで定期的にアクセスし続けている時のログとなります。
webサーバを停止したタイミングでは502エラーが発生していたのですが
想定通り、Route53の定義が変更されたタイミングで右側(balancer_by_lua)でのエラーは解消されました。
問題なく名前解決されているようです!
balancer_by_luaの挙動について
単純に振り分けるだけであればサンプル通りに記述するだけで良いのですが、
何もしないと proxy_next_upstream のように自動で次の upstream にアクセスしてくれません(upstreamのホストを自前で管理するので当たり前ですね)。
そのため、同じような挙動にしたい場合は自前で書く必要があります。
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はこちらを参照ください。
- proxy_next_upstream_tries の値をmax値とし、以下を繰り返します
- ngx.ctx.upstream_serversのhostを読み込み
- set_current_peerを実行。エラーが発生するとbalancer_by_lua_block が再度実行される
まずproxy_next_upstream_triesをnginx側の設定で明示的に設定する必要があります。
かつ、ngx.ctx.triesでリトライする数を管理しつつ、DNSで引けた数をmaxとしてnext_upstreamへリトライを繰り返しています。
性能検証
構成
サーバインスタンス
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はこちらになります。
結果は以下のとおりです。
ポート | 説明 | 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 の話とそれを利用した名前解決の検証を行いました。
要件を満たし、かつパフォーマンスも問題ないため
近々プロダクションへ投入してみたいと思います!