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

スパイスな人生

素敵な人生をおくるためのスパイスを届けていきたい、そんな想いで仕事をするspice lifeメンバーブログ

がんばらない動的画像変換サーバーのつくりかた

tmixエンジニアのid:ksssです。

今年の夏も暑いですね。 僕は最近tmixで作ったドライTシャツを着て、朝のランニングで体力づくりに取り組んでいます。

http://tmix.jp/designs/2052395

このTシャツには、プログラマー界での有名な言葉をもじって「ぐだぐだ言ってないで、走れ!」という意味を込めていて、なまけがちな自分をとっとと走りに行かせようと意識をたかめています。

今回はここに表示されている画像の裏側についてお話したいと思います。

動的画像変換

このTシャツ画像はリクエストを受けたときに指定のサイズの画像を生成して返しています。

動的画像変換を使うと、管理するファイルの削減や、事前処理の単純化、デザインへの柔軟対応などのメリットがあり、tmixでも様々なページで使われています。

動的画像変換には様々な実装がありますが、tmixではあまりがんばらない動的画像変換アプリを自前で実装し、運用しています。

画像変換については様々な記事や実装を参考にさせていただきました。

pixivのサムネイル事情 - pixiv inside

料理を楽しくする画像配信システム

画像変換Nightでngx_small_lightの話をしました - 考える人、コードを書く人

kanoko

こうして実装した動的画像変換アプリはだれでも使えるように、オープンソースとして公開しています。

github.com

動的画像変換アプリでは長いので、「kanoko」と名づけました。*1

kanokoはふつうのRubysinatraで書いたRackアプリケーションになっていて、system(2)からImageMagickのconvert(1)を呼ぶだけのシンプルな実装になってます。

webサーバーでがんばらない

webサーバーとして有名なnginxはwebサーバーです。 epoll等でのI/O複数管理を基本とした大量のリクエストを瞬時に振り分ける、いわば司令塔的な役割です。

自らも静的ファイル配信などでプレイヤーとしても活躍しますが、プレイヤーとしての処理に時間を取られるとだいじな個性であるマネージャー的な能力が無駄になってしまいます。

今回はnginxのプロセスでがんばるのはやめて、専任のRackアプリケーションにリクエストを渡して画像処理することにしました。

画像処理をがんばらない

動的画像変換ではある程度の速度が求められます。

しかしどうしても画像処理は処理時間は長くなってしまいがち。

画像処理の分野だけでも恐ろしく奥が深いのですが、ここはがんばらずにImageMagickのconvert(1)を使うことにしました。

ImageMagickをそのまま使っているおかげで「ちょっと中央寄りにズームした感じの画像」や「セピア色に加工した画像」なんかも簡単に作ることができるようになりました。

処理速度をがんばらない

CのAPIを使うとfork/execによるオーバーヘッドがなくなり処理速度の向上が望めます。 しかし依存関係や実装が複雑になってしまいがちです。ここもがんばらずにsystem(2)によってconvert(1)を実行するだけというおてがるな実装にしました。

並列処理もRackサーバーも頑張らない

処理速度をがんばらないとはいえ、そこそこの速度は欲しいので、マシンリソースは有効に使いたいです。

マシンリソースを有効活用するためには並列動作ができるかどうかがとても重要です。

ImageMagickのconvert(1)を使う以上、並列化するには複数プロセスが有効そうです。

そのためsinatraアプリケーションを動かすRackサーバーは複数プロセスで動作するunicornを選択しました。

unicornのプロセスからそのままsystem(2)を呼んでconvert(1)用のプロセスにしてしまおうという魂胆です。

unicornは他のアプリケーションでも運用経験があるので、運用もがんばらずに済みそうです。

入力値チェックをがんばらない

動的画像処理でよくあるのは「1000000x1000000の画像ファイルをつくる命令が来るとすごく困る」というもの。 対策として最大サイズを制限して……取得リソース先も決められたとこだけに制限して……。と漏れのないバリデーションをかける方法も考えられます。

kanokoでは画像変換サーバーとアプリケーション、双方で秘密の言葉を共有して、入力値にdigestをかけることにしました。

これにより、URLから予測してパラメータを作られてもdigestが一致しないリクエストは全て弾くことができます。

全てのリクエストは信用したアプリケーションからのものなので入力値のチェックもがんばらなくてよくなりました。

URL生成方法もgemライブラリ化して、どのアプリケーションからでもかんたんに使えるようにしました。

CPUでがんばらない

全ての画像リクエストに対して毎回画像を生成していては、突然のスパイクアクセスにCPUが耐えられません。

ここはマシンを増やして対応したりはせず、CDN *2 を前段に置くことによって一度生成した画像の再生成をしなくてもいいようにしました。

動的画像生成とCDNは相性がいいのでよく使われる方法だと思います。

画像を更新した時にURLが変わるように気をつけてさえいれば、*3二度目以降のアクセスに対して高速に変換後の画像を配信することができます。

また、取得リソースの情報を活かせば表示しているブラウザ側でもうまくキャッシュしてくれるかもしれません。

ここはがんばらずに、取得リソースのレスポンスヘッダも引き継いでkanokoのレスポンスヘッダーとして使いまわすことによって、kanokoではなにもがんばらずにS3などがつけてくれたキャッシュヘッダーを流用することができます。etagヘッダーもそのまま返していますが、画像サイズが変わればURLが変わるので同じetagヘッダーがついていても気になりません。

運用をがんばらない

アプリ運用にはパフォーマンス監視・死活監視・ロギングなどが欠かせません。 これまでさまざまなことをがんばらずにいた結果、これまで運用してきたアプリケーションと同じ構成になったので、 運用も楽できそうです。

でもちょっとだけがんばる

指定されたリソースから画像データを落としてきて、いったん一時的なファイルとして書き出してからconvert(1)コマンドを発行するようにしていたのですが、容量が非常に少ない画像でのみ画像変換が失敗する現象に遭遇しました。

たまたまその時読んでいた書籍「UNIXプログラミングインターフェース」によると、 I/Oにはバッファがつきもので、読み書き回数を減らすためにある程度のデータ量が貯まるまで、たとえwrite(2)したとしても実際にHDDのファイルに書き込まない。 という動作をするようでした。

一時的なファイルにwrite(2)したはいいものの、情報はI/Oバッファにとどまりファイルに書き込まれていないまま、convert(1)していたのが問題だということがわかりました。

そこで、ファイルへの書き込みを確定させるfdatasync(2)を用いることによって問題を解決することができました。

解決した問題を公開して、一般化する

kanokoはしばらくprivateリポジトリに、ただの社内アプリとして動いていました。

privateにしている理由はcapistrano用のファイルやunicorn用のファイル、監視用のコードなどが含まれていて公開して誰でも使えるというようにはできていなかったためでした。

そんなときに、「問題解決を一般化する」ことの重要性を説いた記事*4に感化され、

「2つのリポジトリに分ければいいんじゃん」とおもいつき、アプリケーションとしての設定ファイルになるcapistranoファイルやunicornファイルを含んだprivateリポジトリと、 アプリの核になるsinatraアプリケーション部分を含んだpublicリポジトリとに分けることで、オープンソースとして公開することができました。

そんなわけで誰でも使えるように意識して作っているので、皆様の問題解決の助けになれれば嬉しいです。*5

*1:鹿の子編みが名前の由来です。

*2:今回はAWSのCloudFrontを採用

*3:レコードの更新時間をdigestしてパラメータにくっつけています。

*4:エンジニアとしていかに成長するかについて、GMOグループの新卒エンジニア・クリエータの皆さんにお話した - delirious thoughts

*5:僕も趣味アプリで使っています。実装例にどうぞ。 https://github.com/ksss/kanoko-app