こんにちは、成田(@mirakui)です。今日はみんな大好き ImageMagick チューニングのお話です。
2016/5/13 に公開された、いわゆる ImageTragickと呼ばれる脆弱性では、 policy.xml
というファイルを更新するという workaround が紹介されていたのは記憶に新しいと思います。
この policy.xml
は、今回の workaround のようにファイルタイプを制限するだけではなく、画像の縦横ピクセル数、利用するメモリやディスクのサイズなどを制限することができます。
Web サービスなどでユーザのアップロードした画像を ImageMagick で変換する場合、このようなリソース制限を適切に行うべきでしょう。
そこで今回は policy.xml
によるリソース制限方法を紹介します。
前提
特に明記しない限り、2016/05/14 現在の 6 系における最新開発版である ImageMagick 6.9.4-2 の仕様を基準にしています。
基本の書式
policy.xml
の基本の書式は以下のとおりです。
<policymap><policy domain="resource"name="temporary-path"value="/tmp"/><policy domain="resource"name="memory"value="256MiB"/><policy domain="resource"name="map"value="512MiB"/><policy domain="resource"name="width"value="8KP"/><policy domain="resource"name="height"value="8KP"/><policy domain="resource"name="area"value="128MB"/><policy domain="resource"name="disk"value="1GiB"/><policy domain="resource"name="file"value="768"/><policy domain="resource"name="thread"value="2"/><policy domain="resource"name="throttle"value="0"/><policy domain="resource"name="time"value="120"/><policy domain="system"name="precision"value="6"/><policy domain="cache"name="shared-secret"value="replace with your secret phrase"/><policy domain="coder"rights="none"pattern="EPHEMERAL" /><policy domain="coder"rights="none"pattern="HTTPS" /><policy domain="coder"rights="none"pattern="MVG" /><policy domain="coder"rights="none"pattern="MSL" /><policy domain="coder"rights="none"pattern="TEXT" /><policy domain="path"rights="none"pattern="@*" /></policymap>
ImageTragick 脆弱性の workaround では domain="coder"
の設定だけを書いたと思いますが、それ以外にも上記のような設定項目があります。
なお、「基本の」と書きましたが、設定できる項目はこれで全てです。
以下のように、コマンドラインで現在の設定が確認できます。
$ identify -list resource Resource limits: Width: 100MP Height: 100MP Area: 25.181GB Memory: 11.726GiB Map: 23.452GiB Disk: unlimited File: 768 Thread: 12 Throttle: 0 Time: unlimited
本記事では、domain="resource"
で指定できるリソース制限について紹介します。
ピクセルキャッシュが消費するリソース
policy.xml
の domain=resource
で示されている「リソース」というのは、具体的にはピクセルキャッシュの記憶領域を指します。
The Pixel Cache - ImageMagick: Architecture
ピクセルキャッシュは、1ピクセルを表現する PixelPacket
構造体を、画素数の分だけ並べた配列です。 ImageMagick は内部的にこのピクセルキャッシュで画像を表現しています。
画像を処理する場合には、このピクセルキャッシュの領域を確保するためにメモリやディスクといったリソースを消費することになります。
typedefstruct _PixelPacket { Quantum blue, green, red, opacity; } PixelPacket;
Quantum
は Q16
でビルドした場合(デフォルト)は 2 バイト、Q8
の場合は 1 バイトです。
つまり、Q16
でビルドした ImageMagick において横 400 px、縦 300 px のピクセルキャッシュのサイズは以下のように求めることができます。
ピクセルキャッシュサイズ = width * height * sizeof(PixelPacket) = 400 * 300 * (4 * 2) = 960,000 [bytes]
リサイズ処理におけるピクセルキャッシュの利用例
ImageMagick の画像処理においてどのようなサイズのピクセルキャッシュが作られるかを説明します。
例として、以下のように convert
コマンドで横 6,000、縦 4,000 ピクセルの JPEG 画像を 300x200 に縮小する場合のピクセルキャッシュ領域について考えます。
$ convert -debug All src.jpg -resize 300x200 dst.jpg
この場合、内部的には 3 サイズのピクセルキャッシュが作られます。
- 6000x4000 (183.1 MiB: 入力画像の展開用ピクセルキャッシュ)
- 6000x200 (9.155 MiB: リサイズ処理のためのピクセルキャッシュ)
- 300x200 (469 KiB: 出力画像用ピクセルキャッシュ)
メモリリソース上でのリサイズ処理では、この3つが同時にメモリ上に作られるため、合計 192.7 MiB のメモリリソースが消費されます。 つまり、6000x4000 ピクセルの画像をメモリ内で 300x200 にリサイズする場合には、メモリリミットを最低でも 192.7 MiB より大きく設定する必要があります。これがメモリリソースリミットです。
ちなみに、以下のブログ記事で紹介されているように JPEG の size ヒントを与えることによって、上記の例の場合は、リソース消費を 192.7 MiB を 4.463 MiB まで抑えることができました。
本当は速いImageMagick: サムネイル画像生成を10倍速くする方法 - 昼メシ物語
$ convert -debug All -define jpeg:size=300x200 src.jpg -resize 300x200 dst.jpg
policy.xml によるリソース制限
ピクセルキャッシュはメモリリソースを消費すると書きましたが、正確には、リソースは以下の3種類があります。
memory
: メモリmap
: メモリマップドファイルdisk
: ディスク
この記事の本題である policy.xml
でのリソース制限というのは、ピクセルキャッシュが消費するこれらのリソースを制限する、という意味です。
これらのリソースの挙動と制限について、policy.xml
に沿って説明します。
なお各リソースには、対応する環境変数が存在します。もし対応する環境変数が定義されている場合は、policy.xml
の値よりも環境変数の値が優先されます。
また、コマンドラインツールで -limit memory 256MiB -limit map 512MiB
のようにリソースリミットを指定することもできます。この場合、環境変数よりもコマンドラインオプションの値が優先されます。
memory, map, disk
ピクセルキャッシュを作ることができるリソースには以下の3種類があり、それぞれ容量のリミットが設定されています。
リソース名 | 対応する環境変数 | デフォルト値 |
---|---|---|
memory | MAGICK_MEMORY_LIMIT | システムのメモリサイズ [bytes] |
map | MAGICK_MAP_LIMIT | システムのメモリサイズ * 2 [bytes] |
disk | MAGICK_DISK_LIMIT | unlimited [bytes] |
ピクセルキャッシュは通常、メモリ上に作られます。
もしメモリのリソースリミット以上のサイズのピクセルキャッシュを作ろうとした場合、メモリマップドファイルが使われます。
さらにメモリマップドファイルのリソースが不足している場合は、ディスクに作られます。
ImageMagick のユーザなら、/tmp/magick-xxxxx
というような名前の一時ファイルを見たことがあるかもしれません。これがディスクリソースに作られたピクセルキャッシュです。
以上の 3 リソースの制限値は、policy.xml
では以下のように記述します。
<policy domain="resource"name="memory"value="256MiB"/><policy domain="resource"name="map"value="512MiB"/><policy domain="resource"name="disk"value="1GiB"/>
area
メモリ利用の制限には、memory
の他にも area
という値があります。
<policy domain="resource"name="area"value="128MB"/>
リソース名 | 対応する環境変数 | デフォルト値 |
---|---|---|
area | MAGICK_AREA_LIMIT | システムのメモリサイズ * 2 [bytes] |
area
リミットは、メモリに作ることを許す最大のピクセルキャッシュサイズです。
area
は memory
とよく似ていますが、意味はやや異なります。
memory
リソースは複数回ピクセルキャッシュが作られると、都度消費されるものです。そしてピクセルキャッシュが不要になったときに解放されます。
例えば一連の処理で 100 KiB のピクセルキャッシュが 3 つ作成される場合、memory
リミットは 300 KiB より大きい必要があります。
それに対して area
は消費されるリソースではなく、メモリに作ることを許すピクセルキャッシュのサイズに対するリミットです。同様の例の場合は、area
リミットは 100 KiB より大きければ十分です。
ピクセルキャッシュ作成時における area
、memory
、map
、disk
の関係を擬似コードで表すと以下のようになります。
if作りたいピクセルキャッシュのサイズ < areaリミット && 作りたいピクセルキャッシュのサイズ < memoryリソース残量 memoryリソース残量を消費してピクセルキャッシュを作成 elsif作りたいピクセルキャッシュのサイズ < mapリソース残量 mapリソース残量を消費してピクセルキャッシュを作成 elsif作りたいピクセルキャッシュのサイズ < diskリソース残量 diskリソース残量を消費してピクセルキャッシュを作成 elsif分散ピクセルキャッシュサーバ※が有効 分散ピクセルキャッシュサーバ上でピクセルキャッシュを作成 elseエラー end
この擬似コードからも分かるように、リソースのリミットが設定されていれば、リミットを超えた変換が走る前に失敗させることができ、リソースは消費されずに済みます。
※なお、分散ピクセルキャッシュサーバ(distribute-cache)については本題から外れるので詳しい説明を省きます。公式ドキュメントの "Distributed Pixel Cache"の項を御覧ください。
width, height
作成されるピクセルキャッシュの横、縦の長さに対して制限をかけることができます。
<policy domain="resource"name="width"value="8KP"/><policy domain="resource"name="height"value="8KP"/>
リソース名 | 対応する環境変数 | デフォルト値 |
---|---|---|
width | MAGICK_WIDTH_LIMIT | 214.7 MP (Q16の場合。Q8なら429.5MP) |
height | MAGICK_HEIGHT_LIMIT | 214.7 MP (Q16の場合。Q8なら429.5MP) |
なお、ImageMagick 6.9.4-1 までは、width
リミットで指定した値が height
リミットとしても使われてしまうというバグがあります。
この記事を書くためにソースコードを読んでいたらそのバグを発見したので、下記のプルリクエストを送ったところ、すぐにマージしていただくことができました。6.9.4-2 では直っていると思われます。
その他
<policy domain="resource"name="temporary-path"value="/tmp"/><policy domain="resource"name="file"value="768"/><policy domain="resource"name="thread"value="2"/><policy domain="resource"name="throttle"value="0"/><policy domain="resource"name="time"value="120"/>
リソース名 | 対応する環境変数 | デフォルト値 |
---|---|---|
temporary-path | MAGICK_TEMPORARY_PATH , MAGICK_TMPDIR | $TMPDIR の値( /tmp など) |
file | MAGICK_FILE_LIMIT | ulimit -n の 3/4 |
thread | MAGICK_THREAD_LIMIT | OpenMPの最大スレッド数。OpenMP無効時は 1 |
throttle | MAGICK_THROTTLE_LIMIT | 0 [microseconds] |
time | MAGICK_TIME_LIMIT | unlimited [seconds] |
それぞれの値の意味は以下です。
temporary-path
/tmp/magick-xxxxxx
のように、ピクセルキャッシュがファイルとして展開される際のディレクトリ
file
- ピクセルキャッシュをディスク上で同時に展開できる最大個数。
thread
- OpenMP で並列処理を行う最大スレッド数。一般的に、画像のリサイズ程度の処理では並列処理をしないほうが高速であることが多いです。並列処理を無効化する方法はいくつかありますが、この値を 1 にすることでも実現できます。
throttle
- 並列処理を行う際、CPU 負荷を下げるための設定です。単位はマイクロ秒で、これが大きいほどピクセルキャッシュの走査処理を遅くし、負荷を下げることができるようです。ただし、私たちは並列処理を使ってないため詳細な性能は確認していません。
time
- ピクセルキャッシュの走査処理におけるタイムアウト時間を指定します。単位は秒です。
リソース制限のチューニング例
ユーザからアップロードされた画像をオンラインでリサイズするというユースケースについて考えます。
この場合、もしかしたらユーザは巨大な画像をアップロードするかもしれません。 ファイルサイズが小さくても縦横のサイズが大きい画像というものを作ることは可能です。しかし ImageMagick でそれを愚直にピクセルキャッシュとして展開してしまうと、メモリやディスクが埋め尽くされる事になりかねません。
このようなユースケースの場合、私のおすすめは以下のとおりです。
memory
を、そのプロセスが使っていい最大の容量にするarea
の指定でもいいと思います。前述の通り似たような役割なので、area
かmemory
どちらかが書いてあれば事足りると思います。- アップロードされうる画像の最大の width, height が分かっている場合は事前に
convert -debug All
オプションで表示されるデバッグログを見て、必要なリソース容量を見積もるのをおすすめします。 - ピクセルキャッシュの容量を減らしたい場合は、JPEG 画像なら JPEG size hintを利用したり、ImageMagick を Q8 (
--with-quantum-depth=8
)でビルドしたりすると良いでしょう。
disk
とmap
は0B
にする- ユースケースによりますが、普通のスマホやデジカメで撮ったような写真であれば、オンメモリで処理できずにディスク I/O が走るような巨大リサイズは何らかの異常である可能性が高いと思います。こういった場合はそもそもディスクにピクセルキャッシュを書かせず、即エラーにしてしまう方が可用性にとって良いでしょう。
width
,height
リミットはデフォルトのまま- 画像面積に対する制限がしたければ、
memory
もしくはarea
だけで十分に役割が果たされるためです。そもそも前述の通り、width
のリミットがheight
としても使われてしまうというバグが 6.9.4-1 まであるので、それを理解したうえで使う必要があります。width, height の制限をかけたかったら、policy.xml
ではなく、別途identify
コマンドなどで調べたうえでアプリケーションから制限するのが現状では良さそうです。
- 画像面積に対する制限がしたければ、
thread
は1
に- 前述の通り、並列処理を無効にしたほうが画像リサイズは速いからです。
- なお
--disable-openmp
や--without-threads
オプションをつけてビルドされた ImageMagick の場合は、そもそも並列処理は無効になっているのでここを変更する必要はありません。
まとめると、下記のような設定があれば十分でしょう。
<policymap><policy domain="resource"name="memory"value="4GiB"/><!-- 容量は環境に合わせて調整 --><policy domain="resource"name="map"value="0B"/><policy domain="resource"name="disk"value="0B"/><policy domain="resource"name="thread"value="1"/><!-- 並列処理が有効なビルドの場合 --></policymap>
まとめ
本記事では ImageMagick におけるピクセルキャッシュの仕組みと、そのリソース制限について紹介しました。
今回紹介した内容の中には公式ドキュメントを読んだだけでは分からない仕様が含まれているので、皆様のチューニングのお役に立てれば幸いです。