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

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

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小さくなってしまう場合が!)