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

ブログのしゅーくりーむ

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

ts_mruby, Apache Traffic Server mruby extension version 0.1 released.

(日本語版は こちら)

I released ts_mruby version 0.1, Apache Traffic Server(ATS) plugin that enables you write simple logic and configuration by mruby. In this entry, I write ts_mruby-0.1's features.

ts_mruby-0.1

ts_mruby-0.1 supports features of large part of ngx_mruby.

What can ts_mruby-0.1 do?

I show some example what ts_mruby-0.1 can do. See GitHub Wiki about full examples.

Load balancing

Below example works load balancing by simply randomizing a target host of ATS origin server.

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)]

Override TSHttpOverridableConfig variables

ATS has various overridable configs.

ts_mruby supports overriding them by ATS::Records class.

Below example enables ATS memory/disk cache in a HTTP transaction. ts_mruby also enables you write more complex overriding logic by mruby control syntax.

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

The conf_remap plugin managed in ATS core repo provides simply overriding, but it doesn't support complex control syntax.

Switch maintenance mode based on Redis value

Its similar to ngx_mruby' Redis maintenance mode example. Below example enables switching logic by 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 class is provided by mruby-redis. You must write below build_config.rb and build mruby to use it simply. (mruby power is awesome!)

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

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

Quickstart

ts_mruby provide 2 methods for starters.

using Docker

Example docker image has already published to Docker Hub.

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

The image contains a test script(simply responds by ATS::echo), so you can test ts_mruby quickly:

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

using Homebrew

If you're a Mac OS X user, you can use Homebrew to install ts_mruby:

$ 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's design

API/Library stacks

ts_mruby calls atscppapi, wrapped TS API by C++ classes basically.

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

ATS server hooks

Apache Traffic Server supports various hooks. But current ts_mruby is supporting a fews because:

  • I think mruby scripts will become complex if various hooks users can use.
  • handling these hooks is difficult. Users can't understand relationship between hooks and ATS's behaviors intuitively.

So ts_mruby uses READ_REQUEST_HDR_HOOK, early hook mainly and response body transform if necessary(ATS::Headers_out#[]= and ATS::Filter#transform! needs this control). Other hooks are unsupported yet.

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

Thread shared/local Data

ATS process model is multi-thread and event-driven I/O. Therefore ts_mruby (and other ATS plugins) must ensure thread-safety. Current ts_mruby resolve the issue by below ways:

  • All of thread share mruby scirpts as string.
  • Each thread has own mruby VM(mrb_state) and RProc's poinging to compiled code.
    • To get/set thread specific data, using pthread(get|set)specific because ATS worker thread's impl is pthread. :D

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

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 の開発どこまで頑張るかはすごい微妙なところですね。。。うーん。

C++ のラムダ式のデフォルトキャプチャの対象となる変数について確認していた

C++初学者なので勉強のため会社の有志の人々と Effective Modern C++ の輪読会をやっているのですが、その際にラムダ式のデフォルトキャプチャの挙動に関する話題になり、個人的に無性に気になってしまったので調べてみました。 もし誤りや補足などございましたら、コメント等でご指摘いただければ幸いです。

ながすぎる; よんでない

疑問

デフォルトコピー・参照キャプチャ([=], [&])を記述した際のラムダ式のデータメンバとしては、ラムダ式を定義した外のスコープの変数すべてが対象になるのか、必要なメンバしか持たないのか、という点が気になりました。 例えば外のスコープで下記のようにクソデカイ容量の変数が存在したとして、必要であろうがなかろうが問答無用でコピーされうるのか・・・。

using namespace std;

int main() {
  vector<int> large_vec;

  ... // 紆余曲折あって large_vec のキャパシティがクソデカくなっていると想定する

  auto lambda = [=]{}; // lambda のメンバにはクソデカイ vector は、例え使ってなくても含まれるのか??

  return 0;
}

輪読会参加メンバーで打ち立てた予測としては、必要な変数のみコピーされるというものでした

回答

言語仕様上、必要な変数のみコピーされるようです。 (・・・但し後述の通り、実際にデバッガ当てて調べていった感じ、そうではない箇所もあり・・・)

調査結果

言語仕様を読んでみた

今回は The Standard : Standard C++ に掲載されている C++14 の仕様のドラフト N4296 を眺めてみました。

ラムダ式に関する記述がある 5.1.2 節の 12,13 番 に、今回扱う疑問点に関するっぽい記述を発見しました。曰く

デフォルトキャプチャを持つラムダ式は ... (中略) ... compound-statement(ラムダ式の {} の中の文のこと) が下記の条件である場合エンティティ(this と変数のこと) を暗黙的にキャプチャする。 エンティティが odr-used である、もしくは ... (略)

... (略) ラムダ式によってキャプチャされるエンティティは、ラムダ式を含むスコープで ord-used である ... (略)

odr-used な変数がキャプチャの対象になりそうです。 ・・・で、 odr-used というのは何だよという話になりますが、大雑把にいうと定義を必要とする変数の使用のことのようです。 つまるところ、上記ドラフトの記述から「定義を必要としない変数の使用しかされていない(not odr-used な)変数はデフォルトキャプチャを書いてもキャプチャされない」と言えそうです。

実際のコンパイル結果

実際どのようにキャプチャされるのか確認してみました。今回は下記のソースコードを手元の Mac OS X 10.11 にて下記の環境でコンパイルし、 lldb でラムダ式のキャプチャ結果を拾ってみます。

$ g++ --version
Configured with: --prefix=/Applications/Xcode.app/Contents/Developer/usr --with-gxx-include-dir=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.11.sdk/usr/include/c++/4.2.1
Apple LLVM version 7.3.0 (clang-703.0.31)
Target: x86_64-apple-darwin15.6.0
Thread model: posix
InstalledDir: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin

デフォルトコピーキャプチャ以外の振る舞いもついでに見てみたいので、幾つかのキャプチャとラムダ式内の文のパターンを用意してみました。

using namespace std;

int main() {
  vector<int> unused_data = {0};
  vector<int> data = {0};

  auto cpcap_ex = [data]{};
  auto cpcap = [=]{ cout << data.size() << endl; };
  auto cpcap_emptyimpl = [=]{};

  auto refcap = [&]{ cout << data.size() << endl; };
  auto refcap_ex = [&data]{};
  auto refcap_emptyimpl = [&]{};

  return 0;
}

デバッガで動作確認したい都合、今回は -g を付けてコンパイルします。

$ g++ -g -std=c++0x test.cpp

コンパイル後に lldb で return あたりの行でブレークポイントを貼って各変数の内容を見てみます。

$ lldb a.out
...
(lldb) b test.cpp:<行数>
...
(lldb) r
...
(lldb) p cpcap_ex
((anonymous class)) $0 = {
  data = size=1 {
    [0] = 0
  }
}
(lldb) p cpcap
((anonymous class)) $1 = {
  data = size=1 {
    [0] = 0
  }
}
(lldb) p cpcap_emptyimpl
((anonymous class)) $2 = {}
(lldb) p refcap
((anonymous class)) $3 = {
  data = size=1
}
(lldb) p refcap_ex
((anonymous class)) $4 = {
  data = size=1
}
(lldb) p refcap_emptyimpl
((anonymous class)) $5 = {}

表題とは外れてしまいますが、まずデフォルトキャプチャではなく明示的にキャプチャした場合は、 odr-used とか関係なくキャプチャされているようですね。 また、デフォルトキャプチャかつラムダ式の文内で data.size() みたいな感じで odr-used すると、対象の変数はキャプチャされているようです。 更にデフォルトキャプチャでラムダ式の文を空にした場合は、何もキャプチャされないという結果になりました。

ちなみに下記のようにラムダ式の文内に odr-used な式を書いてみたのですが・・・

class undef *up;
auto cpcap_undef = [=]{ up; }; // {} 内の式は discarded-value expression であり、 up は odr-used

この場合はキャプチャされているっぽく見えます。自分の仕様の解釈が誤っているのか、あるいは何か読み落としているのか・・・教えてえろいひと!

(lldb) p cpcap_undef
((anonymous class)) $0 = (up = 0x0000000000000000)

ts_mruby + mruby-mrmagick で画像リサイズするリバースプロキシをサクッと実装してみた

今日は夏コミのようですね!僕は今年は特に原稿を描いておらず用事も無いためコミケに縁のない夏を過ごしてしまいました。

ところで最近拙作の ATS(Apache Traffic Server) プラグインである ts_mruby にレスポンスボディをいじくるメソッドを追加し、ふと思い立って画像リサイズのロジックを mruby で書いてみました。

そのレスポンスボディ加工メソッドは ATS::Filter#transform! になります。 このメソッドではブロックまたは Proc オブジェクトを渡すとブロックパラメータに加工前のレスポンスボディが渡され、評価値でレスポンスボディを差し替えます。

f = ATS::Filter.new
f.transform! do |body|
  # append text to the end of response body
  body + "rewritted by ts_mruby :D"
end

ちなみに ngx_mrubyNginx::Filter#body 相当のメソッドは実装できていません。また ATS::Filter#transform! ではブロックの外の変数へのアクセスを許可していません(ブロックの内外で実行時の context を共有していないため)。これらは現状、 ts_mruby を実装する上でフックの挿入をシンプルにして、 mruby スクリプトを書く側に意識させない作りにしているために生じている制限になっています。。。 将来的には 任意のフック時点に挿入可能にして Nginx::Filter#body 相当のメソッドを実装する・・・かもしれません。

話が逸れましたが、 ATS::Filter#transform! を使えばボディの加工はできます。後は画像ファイル取得時のレスポンスボディを ImageMagick でイイカンジに加工してみます。 今回は ImageMagick の mruby バインディング mruby-mrmagick を使ってみます。 要件としては下記を念頭に置きました。

  • ファイルへの読み書きを行わない
    • ATS の主要用途はプロキシだしワーカスレッドに極力 I/O させず処理したい
  • リクエストのクエリ文字列でリサイズの挙動を変更可能にする
  • ATS::Filter#transform! の制約をなんとか突破する

これらの要件を満たす mruby スクリプトを、下記の工夫を織り込んで書いてみました。(もろもろのファイルなどはこちら

  • ImageMagick の blob でボディを加工・取得
  • クエリ文字列を ATS::Request#args を使って取得
  • ATS::Filter#transform! には必要なパラメータを予め Proc#curry で部分適用しておいた Proc オブジェクトを渡す
# parse parameters
params = {}
req = ATS::Request.new
req.args.split('&').each do |arg|
  key, value = arg.split('=')
  params[key] = value
end

# set resizing logic
resizer = nil
if params.has_key?('scale')
  resizer = Proc.new do |scale, body|
    img = Mrmagick::ImageList.new
    new_img = img.scale(scale)
    new_img.from_blob(body)
    new_img.to_blob
  end.curry.(params['scale'].to_f)
end

# set filter if params is valid
if (resizer)
  filter = ATS::Filter.new
  filter.transform! resizer
else
  ATS::rputs 'Parameters are invalid ...'
  ATS::return ATS::HTTP_BAD_REQUEST
end

mruby-mrmagick でラクをしているとは言え、割とシンプルに記述できた気がします!

ちなみに ATS のキャッシュが有効 な場合、 ATS::Filter#transform! で加工した結果がキャッシュに入ります。 これを活用すれば、このリサイズのロジックもキャッシュヒットしている間は都度走らせずに済み、 CPU リソースの削減が可能かと思います。 ・・・ただし、そのためには上記スクリプトにキャッシュヒットしたかどうかのロジックも入れる必要があります(上記ではそれが無いため、キャッシュヒットすると段々とサイズが大きくor小さくなってしまう場合が!)

週報仲間というやつを試してみる

プライベートにおける作業、たとえば英語の勉強だとかちょっとしたプロダクトを実装してみるとか、はたまたプラモデルを組み立てるだとかを実行しモチベーションを維持するのは、どうにも難しいように思えます。 僕の最近の実体験でいうと、去年電子ピアノを購入して趣味で弾いてみていたのですが、うまくモチベーションを管理できず、現在では電子ピアノは触れられることなく部屋を彩るオサレなインテリアのポジションに収まってしまっています(モッタイナイ!) プライベートでやってる作業なんてものは、多くの場合できなかった場合でも人に迷惑を掛けることもなく義務感も生じにくいため、「やめた」と言いやすいですよねー、困ったことに。

さて最近週報仲間なんてエントリを読みまして、これがモチベーション維持に使えそうなんじゃないかなーと思っています。(この話、最近の Rebuild.fm でも紹介されてた気がする!) 今のところ仲間内で毎週日曜に一週間内でやったこと、翌週やること、その他雑感を書いていって、各週の計画や振り返りを共有することによって、そこはかとない作業への義務感を自身に植えつけられ、作業実行やモチベーション維持をスムーズに行える効果が期待できるかなーと。 ......実際のところ、この手法が自分に合っているかはしばらく継続してみないと分からないので、まずは取り組んでみて 1,2 ヶ月後くらいに効果を評価してみようと考え中です。

HTTP/2 のコネクション再利用について確認してみる

はじめに

本記事は http2 Advent Calendar 2015 の 12 日目の記事となります。

本記事では HTTP/2 における TCP コネクション再利用とその周辺仕様を確認してみようと思います。コネクション再利用の挙動を理解することは、実際の Web サイトにおける HTTP/2 のデプロイの助けになると思われます。

HTTP/1.1, HTTP/2 での TCP コネクション管理

よく訓練された方々には既知のことかとは思いますが、 HTTP/2 において HTTP/1.1 の頃にあった性能向上の為のハックであるドメインシャーディングは好ましくないテクニックになってしまいます。 HTTP/1.1 において今日のブラウザは 1 ドメインあたり概ね 6 TCP コネクションほど張って、並列にリクエストを送るような振る舞いをします。このため Web サイトなどで参照するドメインを分割することで HTTP リクエストの並列度を向上する手法がよく取られていました。 しかしながら HTTP/2 において HTTP リクエストの処理は 1 本の TCP コネクションの上で、ストリームという論理単位で HTTP リクエストが並列処理されるようになり、このようなハックは不要になりました。むしろ参照するドメイン名が集約されていた方が、 TCP コネクションを張る回数が削減できて性能の向上に貢献できます。

これは悩ましい問題です。今日までに HTTP/1.1 を前提としたチューニングがされた Web ページの構造を HTTP/2 向けに構築し直すのは骨が折れる作業といえます。また、依然として HTTP/1.1 ユーザも数多くいるわけで、現在 HTTP/2 前提の Web ページ構成にしてしまって良いのかどうかの判断も難しいといえます。 本記事ではこの課題に対する一つの対策となりうる、 HTTP/2 のコネクション再利用呼ばれる振る舞いについて記述します。

HTTP/2 におけるコネクション再利用

RFC7540 Section 9.1.1 Connection Reuse の節には下のような記述があります。

   Connections that are made to an origin server, either directly or
   through a tunnel created using the CONNECT method (Section 8.3), MAY
   be reused for requests with multiple different URI authority
   components.  A connection can be reused as long as the origin server
   is authoritative (Section 10.1).  For TCP connections without TLS,
   this depends on the host having resolved to the same IP address.

   For "https" resources, connection reuse additionally depends on
   having a certificate that is valid for the host in the URI.  The
   certificate presented by the server MUST satisfy any checks that the
   client would perform when forming a new TLS connection for the host
   in the URI.

https://tools.ietf.org/html/rfc7540#section-9.1.1

HTTP/2 において、 オリジンが authoritative であれば既に張られているコネクションを再利用できるのです。 再利用できるかどうかの基準については、再利用しようとしているコネクションにおいて、リクエスト対象のドメイン名がサーバ証明書の subjectAltName にマッチするかどうかで判断します。 例えば、まず https://a.example.com/ にリクエストを投げるために a.example.com にコネクションを張った際にサーバ証明書の subjectAltName に "*.example.com" が含まれている場合、続いて https://b.example.com/ にリクエストを投げる際に以前使ったコネクションを再利用することができます。

f:id:syu_cream:20151211224939p:plain

この仕様により、冒頭で述べた HTTP/1.1 の既存のハックの改修を行わずに HTTP/2 のコネクション集約の恩恵を受けられる可能性が出てきます。 これを利用して、例えば利用ドメインと証明書を集約して持つエッジサーバなどを構築し、 TCP コネクションを束ねて受けてしまうなどの対応が行えます。

Chrome における コネクション再利用

Google Chrome は既にコネクション再利用のロジックが入っているようです。いくつかの Google の Web ページを閲覧した後 HTTP/2 セッションのステータスを chrome://net-internals/#http2 で確認したところ、下記のようになっていました。

f:id:syu_cream:20151211214255p:plain

いくつかのドメイン名の HTTP/2 セッションが集約されていることがわかります。 ただし、 サーバ証明書の subjectAltName としては *.google.com が含まれているのですが、これにマッチして集約されることが期待されるいくつかのセッションが分割されています。

コネクション再利用の落とし穴

このコネクション再利用ですが、サーバとしては望まないコネクションの再利用が行われる可能性があります。 RFC7540 で記述されている通り SNI を元にリクエスト転送先オリジンサーバを決定している HTTP/2 プロキシサーバが存在するかもしれませんし、サーバ証明書としてはワイルドカードの subjectAltName を持っているものの、実際にはレスポンスを返せない場合があります。 このような場合には、サーバはクライアントに対し 421(Misdirected Request) を送って通知することが可能です。

f:id:syu_cream:20151211224943p:plain

ただし、クライアントがこの挙動を考慮して「コネクションを再利用してリクエストを投げてみて 421 が返されてから、再度コネクションを張り直してリクエストを投げ直す」ような挙動をした場合、もちろんレイテンシが向上してしまいます。 この問題を解決するために、 Origin フレーム という HTTP/2 の拡張フレームの Internet-Draft も存在します。 Origin フレームを用いることでサーバ証明書としては集約されてしまうオリジンがあったとしても、サーバが実際に authoritative なオリジンを指定することができ、これにより上記のレイテンシ向上問題を回避することができます。 (Origin フレームは拡張仕様であるためサーバ、クライアント両者が対応していなければならず、また大量のオリジンに対して authoritative であるサーバは Origin フレームにそれらを列挙し送信すると、その送信自体のコストが大きくなってしまいそうな欠点はありますが。。。)

f:id:syu_cream:20151211224947p:plain

おわりに

HTTP/2 のコネクション再利用が期待できると、現在の Web にデプロイした際に HTTP/2 の恩恵が受け易くなると思われます。 ただし現状クライアントの実装と周辺仕様の充実、さらに実際に運用した上で得られた知見が必要である状態だと思われます。

Apache Traffic Server の設定やちょっとした制御ロジックを mruby で書けるようになるかもしれないプラグイン書いてる

mod_mrubyngx_mrubyh2o_mrubylibvmod_mruby など、さまざまな HTTP サーバの設定などを mruby スクリプトで書ける世の中になりつつありますね。 そんな風潮の後押しもあり、近頃、題意の通り mruby で ATS(Apache Traffic Server) の設定やヘッダの加工などのロジックを mruby で記述可能にするプラグインts_mruby をちまちまと開発しています。

ts_mruby によって例えば下記のようなロジックが mruby で書けるようになります。

  • リクエストヘッダを付与したり削除する
if server_name == "NGINX"
  Server = Nginx
elsif server_name == "Apache"
  Server = Apache
elsif server_name == "ApacheTrafficServer"
  Server = ATS
end

# rewrite request headers
r = Server::Request.new
conn = Server::Connection.new
r.headers_in["X-ATS-Plugin"] = "ts_mruby"
r.headers_in["X-Forwarded-For"] = conn.remote_ip
r.headers_in.delete("Cookie")
  • 特定 IP 以外からのリクエストを弾く
if server_name == "NGINX"
  Server = Nginx
elsif server_name == "Apache"
  Server = Apache
elsif server_name == "ApacheTrafficServer"
  Server = ATS
end

whitelist = [
  "127.0.0.1"
]

# deny if client IP isn't listed in whitelist
conn = Server::Connection.new
unless whitelist.include?(conn.remote_ip)
  Server::echo "Your access is not allowed ..."
  Server::return Server::HTTP_FORBIDDEN
end

ATS でプラグインをロードする際、グローバルプラグイン(ATS 全体に作用するプラグイン)や Remap プラグイン(各リクエストをプロキシする際に作用するプラグイン)という形態を採ることになるのですが、 ts_mruby ではそのどちらもサポートしています。 全リクエストで動作させたいのか、特定 VirtualHost や パスだけに対して動作させたいのか、など要件によって使い分けることが可能です。

  • グローバルプラグインとして使う場合、 plugin.config に下のように記述
# write here if you hope to apply all of requests
ts_mruby.so etc/trafficserver/unified_hello.rb
  • Remap プラグインとして使う場合、 remap.config に下のように記述
# write here if you hope to apply only accesses to /test
map /test http://127.0.0.1/ @plugin=ts_mruby.so @pparam=etc/trafficserver/unified_hello.rb

現状サポートしている ts_mruby の mruby 向けクラス、メソッド は mod_mruby, ngx_mruby と共通の名前、機能にするよう気をつけています。 mruby を HTTP サーバ設定の DSL として使うこともできるようになるかもしれません(ただし各サーバ固有の機能の部分はどうしても出てくるわけで、その部分をどう扱うか悩みどころです)

ts_mruby は現状、細々と個人で開発をしている状態です。なにかご意見や Pull-Request などいただけると幸いです。