はじめに
どうも、GMOペパボ インフラエンジニア 正規雇用となりました久米です。
以下のエントリでngx_mrubyを用いたリバースプロキシを構築しました。 CentOS6で ngx_mruby + mruby-memcached + mruby-mysql をインストール ngx_mrubyで転送先を外部参照するリバースプロキシを構築する
本エントリでは、ここまで作ってきたリバースプロキシの性能向上をするために 以下のことを行います。
- Workerが起動したときに、mysqldやmemcachedと接続しコネクションを保持する。
- リクエスト毎に行っていた接続処理を保持したコネクションを使い回すことで効率化を図る。
上記を実現するために以下の機能を使います。
これをngx_mrubyに組み込むことで(デフォルトのbuild_config.rbに組み込まれている。) Worker単位でデータを保持し他のmrubyスクリプトからデータの出し入れをすることができるようになります。
参考までにmruby-userdataを利用するためにはngx_mrubyをビルドする時にbuild_config.rbに以下が記載されておく必要があります。
conf.gem :github => 'matsumoto-r/mruby-userdata'
Let’s development!!
nginx.conf (抜粋)
: (略)
http {
: (略)
mruby_init_worker /usr/local/nginx/lib/mruby_init_worker.rb;
mruby_exit_worker /usr/local/nginx/lib/mruby_exit_worker.rb;
server {
listen 80;
location / {
resolver 127.0.0.1;
mruby_set $proxy_pass /usr/local/nginx/lib/mruby_get_proxypass.rb cache;
proxy_pass http://$proxy_pass;
proxy_http_version 1.1;
proxy_set_header host $host;
proxy_set_header Connection "";
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Server $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
}
mruby_init_worker /usr/local/nginx/lib/mruby_init_worker.rb;
これはngx_mrubyのWorkerが起動した時に実行されるスクリプトです。 本検証では、mysqldとmemcachedへの接続と、コネクションをmruby-userdataで内部に格納する役割を持ちます。
mruby_exit_worker /usr/local/nginx/lib/mruby_exit_worker.rb;
これはngx_mrubyのWorkerが修了時に実行されるスクリプトです。 本検証では、mysqldとmemcachedのコネクションをクローズする役割を持ちます。
mruby_set $proxy_pass /usr/local/nginx/lib/mruby_get_proxypass.rb cache;
proxy_pass http://$proxy_pass;
これは、リクエスト毎に実行されproxy_passとなるドメインをmemcachedやmysqldから取得する役割を持ちます。 cacheというオプションをつけることで、mrubyスクリプトのコンパイルコストを削減します。 (しかし、mrubyスクリプトを修正した場合にnginx restartが必要になります。)
mruby_init_worker.rb
#!mruby
# memcachedの接続設定
memcached_server = "192.168.100.31:11211"
# mysqldの接続設定
mysqld_server = "192.168.100.21"
mysqld_user = "root"
mysqld_password = "root123"
mysqld_database = "domains"
#
# メイン処理
#
# memcachedに接続
memcached = Memcached.new memcached_server
if memcached != nil then
Userdata.new("memcached_#{Process.pid}").memcached_connection = memcached
end
# mysqldに接続
mysqld = MySQL::Database.new(mysqld_server, mysqld_user, mysqld_password, mysqld_database)
if mysqld != nil then
Userdata.new("mysqld_#{Process.pid}").mysqld_connection = mysqld
end
Userdata.new("memcached_#{Process.pid}").memcached_connection
上記のようにコネクションをmruby-userdataを利用して格納する。 これで、コネクションをWorker内の他のmrubyスクリプトから取り出すことができる。
mruby_exit_worker.rb
#!mruby
#
# メイン処理
#
# mruby-userdataのmemcachedのオブジェクト
memcached = Userdata.new("memcached_#{Process.pid}").memcached_connection
# mruby-userdataのmysqldのオブジェクト
mysqld = Userdata.new("mysqld_#{Process.pid}").mysqld_connection
# Close memcached connection
if memcached != nil then
Userdata.new("memcached_#{Process.pid}").memcached_connection.close
end
# Close mysqld connection
if mysqld != nil then
Userdata.new("mysqld_#{Process.pid}").mysqld_connection.close
end
mruby_get_proxypass.rb
#!mruby
# memcachedの接続設定
memecached_server = "192.168.100.31:11211"
# mysqldの接続設定
mysqld_server = "192.168.100.21"
mysqld_user = "root"
mysqld_password = "root123"
mysqld_database = "domains"
# mruby_set用の変数初期化
proxy_pass = ''
#
# メイン処理
#
# mruby-userdataのmemcachedのオブジェクト
memcached = Userdata.new("memcached_#{Process.pid}").memcached_connection
# mruby-userdataのmysqldのオブジェクト
mysqld = Userdata.new("mysqld_#{Process.pid}").mysqld_connection
# Nginx variables
v = Nginx::Var.new
# request domainからホストされているサーバを照会する。
Nginx.return -> do
# memcachedのコネクションが切れている場合は接続する。(mruby-userdataに格納する。)
if memcached == nil then
Nginx.errlogger Nginx::LOG_INFO, "mruby_get_proxypass: trying to connect to memcached. host=" + memecached_server
memcached = Memcached.new memecached_server
Userdata.new("memcached_#{Process.pid}").memcached_connection = memecached
end
# memcachedからproxy_passを取得するし格納する。
proxy_pass = memcached.get v.http_host
# memcachedからproxy_passが取得できた場合は処理を抜ける。
if proxy_pass != nil then
Nginx.errlogger Nginx::LOG_INFO, "mruby_get_proxypass: memcached: req=" + v.http_host + " res=" + proxy_pass
return Nginx::DECLINED
end
# mysqldのコネクションが切れている場合は接続する。(mruby-userdataに格納する。)
Nginx.errlogger Nginx::LOG_INFO, "mruby_get_proxypass: trying to connect to mysqld. host=" + mysqld_server
mysqld = MySQL::Database.new(mysqld_server, mysqld_user, mysqld_password, mysqld_database)
Userdata.new("mysqld_#{Process.pid}").mysqld_connection = mysqld
# mysqldからproxy_passを取得する。
begin
mysql_result = mysqld.execute("SELECT host FROM domain WHERE domain = ? LIMIT 1", "#{v.http_host}")
rescue
# mysqldのコネクションが切れている場合は接続する。(mruby-userdataに格納する。
Nginx.errlogger Nginx::LOG_INFO, "mruby_get_proxypass: trying to connect to mysqld. host=" + mysqld_server
Userdata.new("mysqld_#{Process.pid}").mysqld_connection = mysqld
retry
end
# mysqldから結果が帰ってくればproxy_passに格納する。
if mysql_result != nil then
proxy_pass = mysql_result.next[0]
mysql_result.close
end
# mysqldからproxy_passが取得できれば処理を抜ける。
if proxy_pass != nil then
Nginx.errlogger Nginx::LOG_INFO, "mruby_get_proxypass: mysqld: req=" + v.http_host + " res=" + proxy_pass
# memcachedにproxy_passを格納する。
memcached.set v.http_host, proxy_pass
Nginx.errlogger Nginx::LOG_INFO, "mruby_get_proxypass: memcached: create cache key=" + v.http_host + " val=" + proxy_pass
return Nginx::DECLINED
end
# 存在しないドメインをリクエストされているためログを出力して503エラーを返す。
Nginx.errlogger Nginx::LOG_INFO, "mruby_get_proxypass: request host is not found. req=" + v.http_host
return Nginx::HTTP_SERVICE_UNAVAILABLE
end.call
proxy_pass
Nginx.return -> do
(処理)
end.call
このようにラムダ的な書き方になっているのは、mrubyはreturnのように途中でプログラムから抜けることができないため最後まで処理が実行されてしまうからである。 上記のような書き方をすることで一般的な関数のように、条件によって処理を抜けるような振る舞いをすることができる。
これには弊社の@matsumotoryさんの紹介により以下のブログを参考にして実装した。
ngx_mrubyを使って特定ホスト以外からのアクセスをメンテナンス画面にする
ここの実装は例えばmysqldやmemcachedのサーバがrestartされるなどしてコネクションを失ってしまった時のために、接続の有効性を確認→無効であれば接続してmruby-userdataで格納。ということをしている。
# mysqldからproxy_passを取得する。
begin
mysql_result = mysqld.execute("SELECT host FROM domain WHERE domain = ? LIMIT 1", "#{v.http_host}")
rescue
# mysqldのコネクションが切れている場合は接続する。(mruby-userdataに格納する。
Nginx.errlogger Nginx::LOG_INFO, "mruby_get_proxypass: trying to connect to mysqld. host=" + mysqld_server
Userdata.new("mysqld_#{Process.pid}").mysqld_connection = mysqld
retry
end
上記のように、mysqldのクエリ実行に失敗した場合にmysqldに再接続してリトライするようにしている。 単純に"mysql_connection != nil …" のように評価するだけでは意図した動作をしなかった。
動作検証
以下のようなコマンドをmysqldサーバ、memcachedサーバで実行してコネクションをモニタリングする。
watch -d "netstat -tanp | grep ngx_mrubyサーバのIPアドレス"
nginxの error_log も tail -f などでモニタリングする。
nginxを起動する。
# nginx
/etc/init.d/nginx start
# mysqld
tcp 0 0 192.168.100.21:3306 192.168.100.10:51528 ESTABLISHED 6029/mysqld
# memcached
こちらは最初のgetやsetメソッドが実行されないとコネクションが張られない模様。
接続時にmysqldのコネクションが張られたことが分かった。 次はこのコネクションを使い回す。
ngx_mruby経由でWEBサイトにアクセスしてみる。
1回目のアクセス
# クライアント
curl http://site-a.local/
# nginx
2016/07/13 15:16:09 [info] 26391#0: *1 mruby_get_proxypass: mysqld: req=site-a.local res=web1.local, client: 192.168.100.10, server: , request: "GET / HTTP/1.1", host: "site-a.local"
2016/07/13 15:16:09 [info] 26391#0: *1 mruby_get_proxypass: memcached: create cache key=site-a.local val=web1.local, client: 192.168.100.10, server: , request: "GET / HTTP/1.1", host: "site-a.local"
# memcached
tcp 0 0 192.168.100.31:11211 192.168.100.10:47983 ESTABLISHED 28027/memcached
# mysqld
tcp 0 0 192.168.100.21:3306 192.168.100.10:51532 ESTABLISHED 6029/mysqld
- memcachedのコネクションが接続された。
- mysqldのコネクションが使いまわされている。
- mysqldから"site-a.local"のホスト先である"web1.local"が取得されていて、それをmemachedに書き込んでいる。
2回目のアクセス
# クライアント
curl http://site-a.local/
# nginx
2016/07/13 15:28:44 [info] 26391#0: *4 mruby_get_proxypass: memcached: req=site-a.local res=web1.local, client: 192.168.100.10, server: , request: "GET / HTTP/1.1", host: "site-a.local"
# memcached
tcp 0 0 192.168.100.31:11211 192.168.100.10:47983 ESTABLISHED 28027/memcached
# mysqld
tcp 0 0 192.168.100.21:3306 192.168.100.10:51532 ESTABLISHED 6029/mysqld
- memcachedのコネクションが使いまわされている。
- mysqldのコネクションが使いまわされている。
- mysqldから"site-a.local"のホスト先である"web1.local"が1回目で書き込んだmemcachedのキャッシュから取得している。
mysqld memcached を restart する。
# mysqld
/etc/init.d/mysqld restart
# memcached
/etc/init.d/memcached restart
再度アクセスする。
2016/07/13 16:06:49 [info] 26684#0: *1 mruby_get_proxypass: trying to connect to mysqld. host=192.168.100.21, client: 192.168.100.10, server: , request: "GET / HTTP/1.1", host: "site-a.local"
2016/07/13 16:06:49 [info] 26684#0: *1 mruby_get_proxypass: mysqld: req=site-a.local res=web1.local, client: 192.168.100.10, server: , request: "GET / HTTP/1.1", host: "site-a.local"
2016/07/13 16:06:49 [info] 26684#0: *1 mruby_get_proxypass: memcached: create cache key=site-a.local val=web1.local, client: 192.168.100.10, server: , request: "GET / HTTP/1.1", host: "site-a.local"
- mysqldと再接続している。
今後の課題
- コネクションを使いまわせることが確認できたので、それをしない場合とでどの程度パフォーマンスが向上したのかを測定する。
- 現在の実装だと複数のmrubyスクリプトで同じmemcachedのIPアドレスなどの設定を持っている。これをどうにか設定ファイルなどに切り出せないか?もしくはこの設定すらもmruby_init_workerでmruby-userdataを使って設定情報を保持するでもいい気もする。