ブログ・ア・ラ・クレーム

技術的なメモとかライフログとか。

ts_mruby-0.1 をリリースしてみました。さーて今後の課題は?

(English version is here.)

このブログで何度か紹介している、 Apache Traffic Server(ATS) の設定やちょっとした制御ロジックを mruby で記載することを可能にするプラグインts_mruby の 0.1 バージョンをリリースしました。 せっかくなので本記事ではその 0.1 の機能やできることについてつらつらと書いていき、今後の課題についても述べてみます。

ts_mruby-0.1

0.1 ってなんだよ!なにをもって 0.1 とバージョン切ってるんだ!という感じですが、ぶっちゃけ明確な基準が存在しません() ngx_mruby でできるようなことが ATS でもできると良いと考えていた都合もあり、 ngx_mruby の example と似たようなことがある程度出来るようになったら一区切りとしていました。

ts_mruby-0.1 でできること

0.1 段階でなにができるのか、下記でいくつか紹介してみます。 ここに記載する内容は GitHub Wiki にも記載しているので、詳細についてはどうぞそちらをご覧ください。

負荷分散する

原始的な方法と思われるかも知れませんが、下記のように ATS のオリジンサーバのホスト/ポートをランダムに選出し設定することで、簡易的な負荷分散が行えます

backends = [
  "127.0.0.1:8001",
  "127.0.0.1:8002",
  "127.0.0.1:8003",
]
upstream = ATS::Upstream.new
upstream.server = backends[rand(backends.length)]

(ATSの上書き可能な)設定を書き換える

ATS は 上書き可能な設定 を持ちます。(設定と言っても具体的にどんな内容なのよと思われそうですが、多岐にわたっているため実際にドキュメントを参照いただくのが良いかと・・・) なお、ここで上書きした設定はその HTTP トランザクション内でのみ有効になり、別の処理中のトランザクションには影響を及ぼしません。

ts_mruby は ATS::Records クラスによって、その上書き可能な設定に対する読み書き操作をサポートしています。 下記サンプルスクリプトは ATS のメモリ・ディスクキャッシュを有効にするものです。この例は非常にシンプルなものですが、もちろん mruby で条件分岐を書いて条件に一致した場合のみキャッシュを有効にすることもできます。

records = ATS::Records.new
records.set ATS::Records::TS_CONFIG_HTTP_CACHE_HTTP, 1

ATS 公式プラグインconf_remap を使えば、値の上書き自体は行えます。しかし条件分岐などによる柔軟な設定変更はサポートされていません。

Redis と連携して外部から制御可能なメンテナンスモードを設ける

これについては ngx_mruby の example にも同じような例があります。 mruby + mrbgems エコシステムが使えることによりメリットが得られることがわかる例だと思うのですが、下記のように Redis と通信して対象のキーに対する値が設定されていたら別の振る舞いをする、なんてことが簡単にできます。

redis = Redis.new '127.0.0.1', 6789

if redis['ts_maint']
  body = <<'EOS'
<html>
  <head>
    <title>Message from your proxy ...</title>
  </head>
  <body>
    <p>Sorry, maintainance mode now ...</p>
  </body>
</html>
EOS
  ATS::echo body
end

Redis クラスは mruby-redis によって提供されます。例えば下記のような build_config.rb を書いて mruby をビルド、 ts_mruby とリンクすることですぐに上記スクリプトが動かせるようになります。えむるびーの ちからって すげー!

MRuby::Build.new do |conf|
  toolchain :gcc

  conf.gembox 'default'
  conf.gem :mgem => 'mruby-redis'
end

クイックスタート

今回の 0.1 リリースに際して、自分で頑張って ts_mruby をシコシコとビルドしなくても試せる手段を用意してみました。

Docker で試す

ビルド済の Docker Image を Docker Hub に push してあります。下記のようにすれば手元でコンテナを動作させることが可能かと思われます。

$ docker pull syucream/ts_mruby:0.1
$ docker run docker run -it syucream/ts_mruby /bin/bash

この image には ATS::echo するための簡単なサンプルスクリプトを同梱しています。下記のようにして ATS を起動してから HTTP リクエストを投げることで動作確認を行うことも可能です。

root@ad7a82be8a65:/opt# trafficserver start
root@ad7a82be8a65:/opt# curl http://localhost:8080/
ts_mruby test

Homebrew で試す

Mac OS X ユーザの方はおなじみ Homebrew でインストールすることもできます。(この場合はサンプルスクリプトは同梱していません。。。)

$ brew install https://raw.githubusercontent.com/syucream/ts_mruby/master/Formula/trafficserver-atscppapi.rb
$ brew install --HEAD https://raw.githubusercontent.com/syucream/ts_mruby/master/Formula/ts_mruby.rb

ts_mruby-0.1 の設計面の話

今後の話をする前に、現状の 0.1 の設計面のいくつかのトピックに関して記述してみます。

ATS API 参照周り

ts_mruby は C++ で実装しており、 ATS の C++API を叩いてサーバ設定の読み書きなどの処理を行っています。 ただしほんの一部分、 C++API でサポートしていない機能に関しては直接 C API を参照している部分が存在します。

https://github.com/syucream/ts_mruby/wiki/images/stack.png

ATS フック周り

ATS には HTTP トランザクション中の幾つかの時点でプラグインによる制御を挟み込むための いくつかのフック が用意されています。 ts_mruby は現状では、 下記の点に配慮して、任意のフックで実行する処理を追加することはできなくしています。

  • フックを意識し出すとユーザの記述しなければならない ATS の設定ファイル、あるいは mruby スクリプトが複雑になりがちになる
  • どのフックでどの操作が行えるか直感的に分かりにくい ことを鑑みて、ユーザはフックを意識せずシンプルなスクリプトを記述すれば良いようにしたかった

現在の ts_mruby は基本的に最も早い段階のフックである READ_REQUEST_HDR で動作し、スクリプトの内容を評価して必要であれば後続のフックをユーザに隠蔽しつつ差し込むということをしています。(ATS::Headers_out#[]=ATS::Filter#transform! がそれを行っています)

https://github.com/syucream/ts_mruby/wiki/images/hooks.png

排他制御周り

ATS はマルチスレッド、イベント駆動 I/O で HTTP トランザクションを処理するモデルの HTTP サーバです。 その HTTP トランザクション内で動作するプラグインである ts_mruby も、複数スレッドであるスクリプトの処理が走ることを意識しなければなりません。 現状の ts_mruby では下記のような具合でスレッド固有データを用いて排他制御の必要を無くしています。

  • mruby スクリプトは ts_mruby 初期化時に読み込んでおき、スレッド間で共有する
  • mrb_state インスタンスに関してはスレッド固有データとして持っておく
    • スレッド固有データへのアクセスは ATS の API では提供されていないが、 ATS のスレッドの実装は pthread なので、 pthread の specific API を直接叩くようにしている(えっ、いいの?ってカンジですが)

https://github.com/syucream/ts_mruby/wiki/images/threads_and_data.png

今後の課題

ts_mruby の機能を今後拡張していくにあたり、大きく分けて2つの扱いかねている課題があるので、それらについて記述してみます。

フック周りの設計見直し

先述の通り、 ts_mruby では基本的に任意のフックでの処理をサポートしておらず、基本的に READ_REQUEST_HDR 、つまりクライアントからのリクエストヘッダを読み終わった段階でほぼ全ての処理を行うことを想定しています。 しかしながらこの制約はユーザへ提供できる機能は限られてしまいます。例えばオリジンサーバからのレスポンスヘッダの内容に基づいた条件分岐などが行えなくなります。

このデメリットを考慮して、フックの扱いを今後変更して行こうと考えています。現状では具体的には下記のような対応を考えています。

  • ユーザがフック周りの設定等を明示しない限り、現状の通り READ_REQUEST_HDR でほぼ全ての処理を完結する
  • ATS の設定ファイル(plugin.config, remap.config) で ts_mruby をロードする際に渡すパラメータでフックを指定可能にする?
  • mruby スクリプト中で任意の処理を実行するフックを指定できるようにする?
    • かなりラフですが、下記のようなイメージ
# similar to EventMachine?
module MyEventHandler
  def send_request_hdr
    hin = ATS::Headers_in.new
    hin['X-Foo'] = 'Bar'
  end

  def read_response_hdr
    hout = ATS::Headers_out.new

    # rewrite response body if origin server allows
    if (hout['X-Allow-Transform'])
      hout.delete 'X-Allow-Transform'
      f = ATS::Filter.new
      f.transform! do |body|
        body + 'rewritten by ts_mruby :D'
      end
    end
  end
end

# TODO: consider to how to register
ATS::EventSystem.register_handler MyEventHandler

mruby スクリプト上でのイベント駆動 I/O のサポート

上で紹介した Redis を使ったメンテナンスモードの例ですが、パフォーマンス面での問題があります。 前述の通りマルチスレッド、イベント駆動 I/O で処理するため、ワーカスレッドでブロッキング I/O を発行するとワーカスレッドが次のイベント処理に進んでくれず、イベント駆動 I/O による恩恵を殺してしまうのです。 (h2o の話になりますが、同様の問題は この記事あの記事 でも取り上げられています)

後者の記事で kazuho さんが提案されている方式(の ruby バインディング)は個人的に良いとは思うのですが、どう実装したら良いのか、下記の点について悩み中です。 (あまり検討が進んでいない為、的外れな懸念を抱いている可能性があります...)

  • ts_mruby が Fiber の使用を想定していない問題
  • ts_mruby がスレッド固有データを使っている問題
    • resume したくなった際、 fiber の持ち主のスレッドをどう探すのか?
  • イベントループの持ち方問題
    • ATS とは別にイベントループ走らせるのか?
    • もしそうするなら、いつ、どうやって走らせる?
  • ワーカスレッドへの仕事の振り方
    • ATS とそれ以外のイベントシステムでワーカスレッドが取り合いにならないか?

これについては少し長期的にアイデア出しと精査を行いフィジビリティスタディして、目処が立ったらブログエントリに書くなどしてから実装に取り掛かろうかなと思案中です。

つらつらと書いたけど

ATS のユーザが国内にあまり居ない気がするので、 ts_mruby の開発どこまで頑張るかはすごい微妙なところですね。。。うーん。