読者です 読者をやめる 読者になる 読者になる

人間とウェブの未来

「ウェブの歴史は人類の歴史の繰り返し」という観点から色々勉強しています。

Apacheやnginxの同時接続状況における各リクエストの任意の属性をカウントする拡張を作った

先日、ApacheやnginxといったWebサーバにおいてDoS的アクセスを検知して任意の制御を行うhttp-dos-detectorを公開しました。

hb.matsumoto-r.jp

上記の拡張も非常に便利なのですが、もう少しシンプルに、現在のサーバに対するリクエスト状況においてとある属性、例えば、どのファイルにどれだけアクセスが発生しているかを計測したい場合があります。これまでは、そういった情報をApacheのスコアボードの範囲内で情報を得たりしてきました。

しかし、Webサービスの高度化に伴い、もう少し柔軟にアクセス状況を計測し、その計測を元に任意の制御をプログラマブルに行いたい場合があると思います。

そこで、それを実現するための拡張を、mod_mrubyとngx_mrubyを利用して実装しました。つまり、Apacheとnginx両方で動きますし、mrubyの実装コードも同じになっていますので、両方のサーバですぐに同等の処理を使う事ができます。

この拡張をhttp-access-limitterと名づけました。

github.com

h2o_mrubyも実装が進めば、この辺の処理も行えるようになると想像しております。

http-access-limitterの使用例

ということで、簡単な使い方は上記リポジトリのREADMEを見ていただくとして、基本的にはそれぞれのWebサーバのアクセスチェックのタイミングで任意の属性、例えば以下の例ではリクエストファイルのカウンタをインクリメントし、ログ出力のタイミングでカウンタをデクリメントすることができます。

公開しているコードでは、とあるファイルに対する同時アクセス数が2以上になったら503を返すような処理を書いており、以下のようになります。

  • インクリメントのコード
####
threshold = 2
####

Server = get_server_class
r = Server::Request.new
cache = Userdata.new.shared_cache
global_mutex = Userdata.new.shared_mutex

file = r.filename

config = {
  # access limmiter by target
  :target => file,
}

unless r.sub_request?
  limit = AccessLimitter.new r, cache, config
  # process-shared lock
  timeout = global_mutex.try_lock_loop(50000) do
    begin
      limit.increment
      Server.errlogger Server::LOG_NOTICE, "access_limitter: file:#{r.filename} counter:#{limit.current}"
      if limit.current > threshold
        Server.errlogger Server::LOG_NOTICE, "access_limitter: file:#{r.filename} reached threshold: #{threshold}: return #{Server::HTTP_SERVICE_UNAVAILABLE}"
        Server.return Server::HTTP_SERVICE_UNAVAILABLE
      end
    rescue => e
      raise "AccessLimitter failed: #{e}"
    ensure
      global_mutex.unlock
    end
  end
end
  • デクリメントのコード
Server = get_server_class
r = Server::Request.new
cache = Userdata.new.shared_cache
global_mutex = Userdata.new.shared_mutex

file = r.filename

config = {
  # access limmiter by target
  :target => file,
}

unless r.sub_request?
  limit = AccessLimitter.new r, cache, config
  # process-shared lock
  global_mutex.try_lock_loop(50000) do
    begin
      limit.decrement
      Server.errlogger Server::LOG_NOTICE, "access_limitter_end: #{r.filename} #{limit.current}"
    rescue => e
      raise "AccessLimitter failed: #{e}"
    ensure
      global_mutex.unlock
    end
  end
end

また、limit.currentによって現在のカウンタの値を取得し、その後に行う処理は上記例のように503を返すだけではなく、mrubyで何かAPIをつつくとか、mod_mrubyとかngx_mrubyの機能により別のプロキシにリダイレクトするとか、何かリクエストの属性をまとめたログをどこかに吐くとか、様々な制御を行う事ができます。

また、

file = r.filename

config = {
  # access limmiter by target
  :target => file,
}

このカウンターのターゲットをリクエスト処理における別の属性、例えばファイルのパーミッションだったり、アクセス元IPであったり、認証のユーザ名だったり様々な属性を使う事ができます。これによって、任意の属性に対する同時アクセス状況を計測可能となり、それに基づいた任意の制御が実行可能になります。

これらについても、mruby及びmod_mrubyやngx_mrubyで取得・加工可能なターゲットであれば何でも良いです。

AccessLimitter.newなインスタンスを複数生成すれば、2段階の条件だったり、複数条件の組み合わせももちろん可能となります。やりたい事が色々できそうですね。

まとめ

ということで、http-dos-detectorのよりももう少しシンプルでありながら、使い方によっては様々なな応用が可能なhttp-access-limitterを実装・紹介しました。これと、http-dos-detectorを使えば、アクセス状況や同時接続状況などさまざまな状況において適切な制御を実装することができると思うので、是非色々試してみたください。