はじめに
お久しぶりです。 ペパボの久米です。 本日はmrubyを利用したWEBサーバのCPU使用率を元にしたアクセス制御例をご紹介します。
具体的には以下のような制御を行いたい。
- 複数ドメイン環境下において、リクエスト毎に使われるCPU使用時間を元にサーバの負荷が高い場合にvhost単位の制限を行う。
- サーバの負荷の判断はLoadAveを元に行う。
- CPU使用時間はリクエスト開始から処理終了までの合計が必要なので、判定に使うCPU時間は前回のリクエストの情報を使う。
実装にあたって使用する主なソフトウェア
- mod_mruby を導入した Apache (本エントリの内容は、おそらく ngx_mruby でも動作する)
- 詳しくは過去のエントリを御覧ください。 (httpd + mod_mrubyでプログラマブルなWEBサーバを構築する)
- mruby-resource
- 今回は
getrusage(2)
の機能を利用した、CPU使用時間の取得を行うために利用します。
- 今回は
実装
本エントリでは httpd2.2 + mod_mrubyを用いて行います。
事前準備
- mod_mrubyのbuildはbuild_config.rbに以下のような追加をしてビルドしておく。
--- /usr/local/src/mod_mruby/build_config.rb.orig 2017-04-04 10:15:31.152370251 +0000
+++ /usr/local/src/mod_mruby/build_config.rb 2017-04-04 10:16:15.034296969 +0000
@@ -43,6 +43,8 @@
# conf.gem :github => 'matsumoto-r/mruby-capability'
# conf.gem :github => 'matsumoto-r/mruby-cgroup'
+ conf.gem :github => 'harasou/mruby-resource'
+
# C compiler settings
conf.cc do |cc|
if ENV['BUILD_TYPE'] == "debug"
メイン実装
httpd.conf
以下の内容を追加する。
LoadModule mruby_module modules/mod_mruby.so
<IfModule mod_mruby.c>
mrubyPostConfigMiddle /etc/httpd/conf.d/mruby/init.rb cache
<FilesMatch ^.*\.php$>
mrubyAccessCheckerMiddle /etc/httpd/conf.d/mruby/begin.rb cache
mrubyLogTransactionMiddle /etc/httpd/conf.d/mruby/end.rb cache
</FilesMatch>
</IfModule>
init.rb
- プロセス起動時に1度だけ実行される。
- 各リクエストで利用するcacheやmutexを作成して、Userdataに格納し各Workerから参照できるようにする。(都度初期化するより大幅にパフォーマンス向上する)
- Userdata.new.shared_cache
- vhost毎に最後のリクエスト終了後のCPU使用時間を保持する。
- Userdata.new.shared_mutex
- 処理の最後に上記Cacheに書き込む際の競合を防止する。
Server = get_server_class
begin
Userdata.new.shared_cache = Cache.new :namespace => "resource-limiter"
rescue => e
raise "localmemcache init failed on #{__LINE__}: #{e}"
end
begin
Userdata.new.shared_mutex = Mutex.new :global => true
rescue => e
raise "mutex init failed on #{__LINE__}: #{e}"
end
begin.rb
- リクエスト毎に最初に実行される。
- LoadAvgが一定以上であれば以下のことをする。
- 最後のリクエスト終了時点のCPU使用時間が一定を超えていれば503エラーを返す。
- それ以外は通常の処理を行う。
LOADAVG_THRESHOLD = 50
RU_UTIME_LIMIT = 20
Server = get_server_class
# get this server loadavg for 3 min
loadavg = IO.read('/proc/loadavg').split(' ')[0].to_f
vhost = Server::Request.new.hostname
if loadavg > LOADAVG_THRESHOLD
Server.errlogger Server::LOG_INFO, "loadavg is over threshold. LOADAVG_THRESHOLD:#{LOADAVG_THRESHOLD} loadavg:#{loadavg} hostname:#{vhost}"
begin
cache = Userdata.new.shared_cache
last_ru_utime = cache[vhost].to_f
if last_ru_utime > RU_UTIME_LIMIT
Server.errlogger Server::LOG_INFO, "ru_utime is over limit (return 503). limit:#{RU_UTIME_LIMIT} last_ru_utime:#{last_ru_utime} hostname:#{vhost}"
Server.return Server::HTTP_SERVICE_UNAVAILABLE
else
Server.return Server::DECLINED
end
rescue => e
Server.errlogger Server::LOG_ERROR "failed on #{__LINE__}: #{e}"
Server.return Server::DECLINED
end
else
Server.errlogger Server::LOG_INFO, "loadavg not over threshold. skip... LOADAVG_THRESHOLD:#{LOADAVG_THRESHOLD} loadavg:#{loadavg} hostname:#{vhost}"
Server.return Server::DECLINED
end
end.rb
- コンテンツ処理、レスポンスが終了した時に実行される。
- CPU使用時間をvhostをキーに記録します。
- getrusage(RUSAGE_SELF)
- mruby-resourceを用いてgetrusageを実行します。
- ru_utime = rusage[:ru_utime]
- 使用されたユーザCPUの時間を取得します。
include Resource
Server = get_server_class
cache = Userdata.new.shared_cache
mutex = Userdata.new.shared_mutex
vhost = Server::Request.new.hostname
rusage = getrusage(RUSAGE_SELF)
ru_utime = rusage[:ru_utime]
timeout = mutex.try_lock_loop(50000) do
begin
cache[vhost] = ru_utime.to_s
Server.errlogger Server::LOG_INFO, "Recorded ru_utime. ru_utime:#{ru_utime} hostname:#{vhost}"
rescue => e
raise "failed on #{__LINE__}: #{e}"
ensure
mutex.unlock
end
end
if timeout
Server.errlogger Server::LOG_WARNING, "Get timeout lock mutex"
end
実行結果
- テストのため閾値を大幅に下げます。(begin.rb)
#LOADAVG_THRESHOLD = 50
LOADAVG_THRESHOLD = 0.001
#RU_UTIME_LIMIT = 20
RU_UTIME_LIMIT = 0.001
- 負荷が低い時
# curl http://hoge.local/index.php => 通常表示
[Tue Apr 04 23:54:11 2017] [info] loadavg not over threshold. skip... LOADAVG_THRESHOLD:0.001 loadavg:0 hostname:hoge.local
[Tue Apr 04 23:54:11 2017] [info] Recorded ru_utime. ru_utime:0.005999 hostname:hoge.local
- 負荷が高い時
# curl http://hoge.local/index.php => 503エラー
[Tue Apr 04 23:54:34 2017] [info] loadavg is over threshold. LOADAVG_THRESHOLD:0.001 loadavg:0.06 hostname:hoge.local
[Tue Apr 04 23:54:34 2017] [info] ru_utime is over limit (return 503). limit:0.001 last_ru_utime:0.010998 hostname:hoge.local
[Tue Apr 04 23:54:34 2017] [info] Recorded ru_utime. ru_utime:0.007998 hostname:hoge.local
このように指定したCPU使用時間を超えた場合にエラーを返すことができました。