pixivのサムネイル事情
この記事はピクシブ株式会社Advent Calendar 12/10の記事です。
こんにちは、インフラチームの@harukasanです。
さて、今日はpixivで使用しているサムネイル変換サーバについて紹介しようと思います。
pixivにはたくさんのサムネイルがある
pixivにはうんざりするほどたくさんの種類のサムネイルがあります。 これは対応しているプラットフォームが多く、また画面毎にもサイズが異なるからです。 PC版であるwww.pixiv.netだけでも10種類以上のサムネイルが使用されています。 また、サムネイルにはアスペクト比を固定したものと、スクエアにクロップした2種類があります。
(Ugoira Tech Talks: Ugoku Backendより)
従来の方法ではこれらのサムネイルをアップロード時に生成していたため、サムネイル生成を非同期化するなどして対応していました。また、新しい種類のサムネイルをつくるためには長時間バッチを回す必要があり、開発の大きな障害になっていました。
動的サムネイル生成
これを解決するのがサムネイルの動的生成です。 動的生成というと曖昧な言葉ですが、ここではマスタ画像から指定したサイズのサムネイル画像をユーザのリクエスト毎に生成して配信することを指します。
これはかなり高コストに思えますが、pixivにはキャッシュヒット率95%以上を維持している画像配信クラスタがあります。 そのため、オリジンサーバへのアクセス自体はそれほど大きくなく、動的にサムネイルを生成してもそれほど問題にならないという予測がありました。
サムネイル生成の要件
動的サムネイル生成が導入された当初、ApacheモジュールであるSMALL LIGHTを使用していました。この構成は安定して使用されていましたが、遅延とCPU負荷が大きい問題があり、pixiv全体で適用するには難しいと思われていました。また、pixivの画像配信クラスタはほとんどnginxで運用しており、Apacheサーバの運用はほんの少し煩雑になっていました。
pixivのメインコンテンツは画像です。これを配信するためにはそれなりのコストをかけても良いんじゃないか、と言うことになり、pixivに必要最低限の機能をもつ高速なサムネイル変換プロキシサーバを開発することになりました。
開発要件には次のものがあります。
高画質な画像生成
pixivはイラストコミュニケーションサイトであるため、サムネイルの画像も非常にクオリティが要求されます。少々劣化しただけでもユーザー体験を大きく損なってしまいます。画質としてはImageMagickが採用しているLanczos Filter(Lanczos2)相当の画質で縮小処理が行えることが条件になりました。
また、対応している処理は縮小だけで、クロップやフィルタ、枠を付ける処理などはありません。
低遅延
前述したとおり、リクエスト数自体はそれほど多くないためスループットは問題になりませんが、とはいえ最初のレスポンスが遅いと、ユーザー体験を損ねてしまいます。20枚画像が並んでいると、1枚でも遅いとそれなりに気になってしまいます。そこでできるだけ高速な変換が必要になります。
JPEG画像のみ対応
pixivでは主にJPEG形式の画像を使用しています。これはpixivに投稿される一般的なイラストの圧縮に適しているためです。また、PNG等の可逆方式では3G回線の帯域制限や、ブラウザのCPU使用率といった問題があり採用していません。とはいえJPEG画像といってもクォリティパラメータは99を設定しており、かなり高品質のJPEG画像を配信しています。
一般的なサムネイル変換サーバはPNG、GIF、JPEGの3種類に対応していることが多いですが、go-thumberではJPEG画像からJPEG画像のサムネイルを生成することだけをサポートしています。これはサムネイルのもととなるサムネイルマスタ画像を予め生成しているからです。
これによりJPEG以外のファイルフォーマットをサポートする必要がないだけでなく、EXIF情報の削除なども事前に行っているため必要なくなります。また、JPEG画像においても一般的ではないサンプリングファクタやCMYKモードの画像には対応しておらず、予めRGBモードでサンプリングファクタを固定したサムネイルマスタ画像を用意しています。
go-thumber
上記の条件を満たすように開発されたのがgo-thumberです。 go-thumberはオープンソースソフトウェアであり、下記Githubリポジトリで公開されています。
開発は当時pixivに在籍していた@marcan42が行いました。
使い方
go-thumberはプロキシサーバとして動作します。パラメータはURLに含めて指定します。次のような感じです。
http://localhost:8000/w=128,h=128,a=0,q=95/upstream.example.com/some-image.jpg
使用できるパラメータには次のようなものがあります。
w: 生成するサムネイルの最大幅 (required) h: 生成するサムネイルの最大高さ (required) q: JPEG quality (default 90) u: 指定サイズより小さかったらアップスケール(引き延ばし)するかどうか (default 1) a: アスペクトをキープするかどうか (default 1) o: JPEGのoptimizeを行う (default 0) p: ダウンサンプリングファクタを指定する(後述) (default 2)
使用されている技術
go-thumberはGo言語で記述されています。といってもサムネイル生成処理をすべてGo言語で全て実装しているわけではなく、次の技術を用いています。
cgo
主な処理であるJPEGファイルのデコード、縮小処理はネイティブライブラリを使用しています。 Go言語ではcgoを用いることでGolangからCのコードを呼び出したり、また、その逆を行うことが出来ます。 go-thumberでは主な部分はこのcgoを用いてCのインターフェースを呼び出しています。
libswscale
画像縮小にはlibswscaleが利用されています。libswscaleはFFmpegで使用されており、非常に高度な最適化が行われています。 また、前述したLanczos Filterを利用した縮小が行えます。
libjpeg-turbo
JPEGのデコード/エンコードにはlibjpeg-turboを使用しています。このため非常に高速なデコード/エンコードを可能にしています。もちろん通常のlibjpegを利用することも可能です。
サムネイル変換のスループット
それではまずサムネイル変換のスループットを比較してみましょう。比較対象としてmod_smallightを用いました。JPEGのデコードにはgo-thumber、SMALL LIGHTともにlibjpeg-turboを使用しています。SMALL LIGHTでは全ての場合でJPEG hintingを有効にしています。
テスト環境は次の通りです。社内のOpenstack上に適当な仮想ホストをたてて行いました。
- CPU: 4コア
- メモリ: 2GB
テストに使用した画像はTecnick's public test imagesに含まれている自然画像です。512〜1200pxの画像125枚が含まれています。go-thumberはJPEGしか変換できないため、予めImageMagickのconvertコマンドを用いてqualityパラメータ100のJPEG画像に変換しました。ベンチマーカとしてLuaスクリプトを利用できるwrkを使用しています。
下のグラフが256x256px、512x512pxのサムネイル画像を生成した場合のベンチマーク結果です。横軸は同時接続数、縦軸は1秒間に応答したリクエスト数(requests/sec)です。グラフをみるとgo-thumberはSMALL LIGHT(imlib2)とほぼ同じスループットを出せていることがわかります。処理でボトルネックになるのはJPEGのデコード/エンコードであるため、これ以上高速化するためにはJPEGのデコード処理に手を入れる必要がありそうです。
サムネイル変換の品質
続いてサムネイル変換の品質を見てみましょう。品質は人間の感性によるため、エンジニアにブラインドテストを行ってもらいました。こちらもSMALL LIGHT(imlib2)、SMALL LIGHT(imagemagick)、go-thumberの3つの場合で比較しています。細かいパラメータは次の通りです。
- 入力画像: Tecnick's public test imagesの1200x1200pxの画像100枚
- 出力サムネイルサイズ: 128x128px
- JPEG hinting: 有効
テスト画面はJavaScriptで適当に実装しており、グレーの画面に2枚の画像が出力されます。ユーザはこの2枚から綺麗だったと思った画像をクリックして選択します。クリックされると次の画像が表示され、100種類すべて選択し終わると終了です。画面上には1枚1秒くらいって書いてましたが、実際には10秒程度で選択した被験者が多かったです。 サムネイルは1つの画像に付き3種類あるため、その中からランダムに2枚選択して表示します。画面は次のようなかんじです。
最終的に業務時間中のエンジニア7名が実験に協力しました。それぞれのサムネイルが"綺麗だと思った"と選ばれた回数と確率を次の表に示しています。選択された割合は、被験者が画像を"綺麗だと思った"と選択した回数を表示回数で割ったものです。表からわかるとおりgo-thumberの生成したサムネイルが最も綺麗な画質であると評価されたことがわかります。
表示回数 | 選択された回数 | 選択された割合 | |
---|---|---|---|
SMALL LIGHT(imlib2) | 467 | 58 | 8.2 % |
SMALL LIGHT(imagemagick) | 466 | 275 | 39.3 % |
go-thumber | 467 | 367 | 52.4 % |
被験者毎にみても、すべての被験者がgo-thumberの生成したサムネイル画像が最も綺麗であると回答しました。また、開発者本人はimlib2のサムネイルを100%判別することができるようになりました。
サムネイルが綺麗なからくり
サムネイルが綺麗であるからくりはJPEG hintingにあります。この機能の正体はJPEGをデコードする際にダウンサンプリングを行いながらデコードを行えるものです。SMALL LIGHT、go-thumberともにこの機能を利用して高速なJPEGデコードを行っています。
JPEGのダウンサンプリング読み込みでは、サムネイルの1/8から8/8サイズで画像をデコードすることが出来ます。SMALL LIGHTやImageMagickは生成するサムネイルサイズに最も近いサイズでデコードを行っていますが、go-thumberのデフォルトでは生成するサムネイルサイズの2倍に最も近いサイズでデコードを行っています。
これにより、デコード時の品質劣化を最低限に抑えつつ高速なデコードを可能にしています(pパラメータを1にすることでImageMagickと同じサムネイル生成が可能です)。
まとめ
さて、長くなりましたがpixivのサムネイル変換について紹介しました。 go-thumberにより高速かつ高品質の動的生成が実現出来るようになりました。 現在、すべてのサムネイル画像をgo-thumberによる動的生成に切り替えるため準備を進めています。 これにより現在以上にデバイス毎に最適化したサムネイル画像が生成できるようになる予定です。
前述したとおりgo-thumberはオープンソースソフトウェアであり、皆様もコントリビュートして頂くことが可能です。 問題を発見した場合、Githubでお知らせください。
pixivではこのように画像配信を高速化したいエンジニアを募集しております。さて、明日のアドベントカレンダーはtantanさんがなにか書いてくれるそうです。ご期待ください。