Colossusがアイドル時でもCPUを消費する

Colossuのドキュメントにある一番単純なサンプルを実装しても、アイドル時(何もリクエストを処理していない状態)でCPUを20%くらい消費する(@Thinkpad E450)ので気になって詳細を調べてみた。

ColossusはAkkaを使っているのでまずはAkka関連のパラメータをいじくってみた。

Akkaのアイドル時CPU消費に関わりそうなパラメータ

application.properies(typesafe config)に

akka.log-config-on-start = true

と書くと起動時にAkkaの全パラメータが見れる。

ソースコードを追ったり説明を見た感じ、以下の二つが関連しそう。

# Level of CPU time used, on a scale between 1 and 10, during backoff/idle.
# The tradeoff is that to have low latency more CPU time must be used to be
# able to react quickly on incoming messages or send as fast as possible after
# backoff backpressure.
# Level 1 strongly prefer low CPU consumption over low latency.
# Level 10 strongly prefer low latency over low CPU consumption.
akka.actor.default-dispatcher.affinity-pool-executor.idle-cpu-level = 5
# The LightArrayRevolverScheduler is used as the default scheduler in the
# system. It does not execute the scheduled tasks on exact time, but on every
# tick, it will run everything that is (over)due. You can increase or decrease
# the accuracy of the execution timing by specifying smaller or larger tick
# duration. If you are scheduling a lot of tasks you should consider increasing
# the ticks per wheel.
# Note that it might take up to 1 tick to stop the Timer, so setting the
# tick-duration to a high value will make shutting down the actor system
# take longer.
akka.scheduler.tick-duration : 10ms

このうち、tick-durationは説明書きにあるとおりなのだけど、具体的にはbusy-loopで回してここで指定したtick-duration分経過するたびに処理を行うという実装になっている。ちなみに、この値はWindowsでは10ms以下に設定できないよという悲しいメッセージが埋め込まれている。

akka.actor.LightArrayRevolverScheduler

  val TickDuration =
    config.getMillisDuration("akka.scheduler.tick-duration")
      .requiring(_ >= 10.millis || !Helpers.isWindows, "minimum supported akka.scheduler.tick-duration on Windows is 10ms")
      .requiring(_ >= 1.millis, "minimum supported akka.scheduler.tick-duration is 1ms")

Colossusの実装を追う

Akka関連の前述のパラメータをいじくってみたが特にCPUリソース消費量に変化が見られないのでColossusの実装を調べてみた。すると、actorが毎回自分にメッセージを送ることでbusy-loop的な実装を行っているところがちょっと怪しい気がしてきた。

colossus.core.Worker

def accepting: Receive = {
    case Select => {
      selectLoop()
      self ! Select
    }
    ...
  def selectLoop() {
    val num = selector.select(1) //need short wait times to register new connections
    eventLoops.hit(tags = workerIdTag)
    implicit val TIME = System.currentTimeMillis
    val selectedKeys  = selector.selectedKeys()
    val it            = selectedKeys.iterator()
    while (it.hasNext) {
    ...

ここでselectorは別スレッド(たぶん)で登録されたクライアントからのコネクションを待っている。引数はタイムアウト(msec)。

//need short wait times to register new connections

という意味がちょっとよくわからなかった。別にコネクションが確立するまでやること無いんだから、何ならこのSelectで無限に待ってたって良いと私は思うのだが…。

コネクションを確立させる以外にもactorでやることが色々あって、select操作だけでactorをブロッキングしときたくないとかいう理由なのかな。

このタイムアウトを大きくしてパフォーマンスにどんな影響があるのか、アイドル時のCPU使用率は下がるのかなどを検証してみたいのだが、ウェイト時間がハードコーディングされているので自分でコンパイルするしかない。たぶん、ハードコードされてるという時点で何らかの強い理由があるのだろう。

この記事を書いている時点では私は速度制限がかかったMVNOのテザリングというアナログモデム時代を彷彿とさせる環境におり、ソースをcloneするだけでもしんどいこと、また、ColossusにはHTTP Bodyの基本的なパーサ(application/x-www-form-urlencodedなど)すら無くて使うのが面倒くさそうなこと、などの理由によりColossus以外のサーバでもいいかな…と思っているので気が向いたら検証する。

アイドル時にCPUを消費していたとしてもパフォーマンスが落ちないなら良いじゃんという考えもあるだろう(というか普通はそう考える?)が、例えばさくらインターネットのVPSではあんまり不可の高い処理をしていると警告を食らうなどという話も聞くのですこし神経質になってしまう。