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

ImageMagickのピクセルキャッシュとリソース制限

こんにちは、成田(@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.xmldomain=resource で示されている「リソース」というのは、具体的にはピクセルキャッシュの記憶領域を指します。

The Pixel Cache - ImageMagick: Architecture

ピクセルキャッシュは、1ピクセルを表現する PixelPacket 構造体を、画素数の分だけ並べた配列です。 ImageMagick は内部的にこのピクセルキャッシュで画像を表現しています。 画像を処理する場合には、このピクセルキャッシュの領域を確保するためにメモリやディスクといったリソースを消費することになります。

typedef struct _PixelPacket
{
  Quantum
    blue,
    green,
    red,
    opacity;
} PixelPacket;

QuantumQ16 でビルドした場合(デフォルト)は 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 サイズのピクセルキャッシュが作られます。

  1. 6000x4000 (183.1 MiB: 入力画像の展開用ピクセルキャッシュ)
  2. 6000x200 (9.155 MiB: リサイズ処理のためのピクセルキャッシュ)
  3. 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 リミットは、メモリに作ることを許す最大のピクセルキャッシュサイズです。

areamemory とよく似ていますが、意味はやや異なります。

memory リソースは複数回ピクセルキャッシュが作られると、都度消費されるものです。そしてピクセルキャッシュが不要になったときに解放されます。 例えば一連の処理で 100 KiB のピクセルキャッシュが 3 つ作成される場合、memory リミットは 300 KiB より大きい必要があります。

それに対して area は消費されるリソースではなく、メモリに作ることを許すピクセルキャッシュのサイズに対するリミットです。同様の例の場合は、area リミットは 100 KiB より大きければ十分です。

ピクセルキャッシュ作成時における areamemorymapdisk の関係を擬似コードで表すと以下のようになります。

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 では直っていると思われます。

Fix typo s/width/height/ in resource.c (ImageMagick-6 branch) by mirakui · Pull Request #199 · ImageMagick/ImageMagick

その他

  <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 の指定でもいいと思います。前述の通り似たような役割なので、areamemory どちらかが書いてあれば事足りると思います。
    • アップロードされうる画像の最大の width, height が分かっている場合は事前に convert -debug All オプションで表示されるデバッグログを見て、必要なリソース容量を見積もるのをおすすめします。
    • ピクセルキャッシュの容量を減らしたい場合は、JPEG 画像なら JPEG size hint を利用したり、ImageMagick を Q8 (--with-quantum-depth=8)でビルドしたりすると良いでしょう。
  • diskmap0B にする
    • ユースケースによりますが、普通のスマホやデジカメで撮ったような写真であれば、オンメモリで処理できずにディスク I/O が走るような巨大リサイズは何らかの異常である可能性が高いと思います。こういった場合はそもそもディスクにピクセルキャッシュを書かせず、即エラーにしてしまう方が可用性にとって良いでしょう。
  • width, height リミットはデフォルトのまま
    • 画像面積に対する制限がしたければ、 memory もしくは area だけで十分に役割が果たされるためです。そもそも前述の通り、width のリミットが height としても使われてしまうというバグが 6.9.4-1 まであるので、それを理解したうえで使う必要があります。width, height の制限をかけたかったら、policy.xml ではなく、別途 identify コマンドなどで調べたうえでアプリケーションから制限するのが現状では良さそうです。
  • thread1
    • 前述の通り、並列処理を無効にしたほうが画像リサイズは速いからです。
    • なお --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 におけるピクセルキャッシュの仕組みと、そのリソース制限について紹介しました。

今回紹介した内容の中には公式ドキュメントを読んだだけでは分からない仕様が含まれているので、皆様のチューニングのお役に立てれば幸いです。

/* */ @import "/css/theme/report/report.css"; /* */ /* */ body{ background-image: url('http://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527163350.png'); background-repeat: repeat-x; background-color:transparent; background-attachment: scroll; background-position: left top;} /* */ body{ border-top: 3px solid orange; color: #3c3c3c; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; line-height: 1.8; font-size: 16px; } a { text-decoration: underline; color: #693e1c; } a:hover { color: #80400e; text-decoration: underline; } .entry-title a{ color: rgb(176, 108, 28); cursor: auto; display: inline; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; font-size: 30px; font-weight: bold; height: auto; line-height: 40.5px; text-decoration: underline solid rgb(176, 108, 28); width: auto; line-height: 1.35; } .date a { color: #9b8b6c; font-size: 14px; text-decoration: none; font-weight: normal; } .urllist-title-link { font-size: 14px; } /* Recent Entries */ .recent-entries a{ color: #693e1c; } .recent-entries a:visited { color: #4d2200; text-decoration: none; } .hatena-module-recent-entries li { padding-bottom: 8px; border-bottom-width: 0px; } /*Widget*/ .hatena-module-body li { list-style-type: circle; } .hatena-module-body a{ text-decoration: none; } .hatena-module-body a:hover{ text-decoration: underline; } /* Widget name */ .hatena-module-title, .hatena-module-title a{ color: #b06c1c; margin-top: 20px; margin-bottom: 7px; } /* work frame*/ #container { width: 970px; text-align: center; margin: 0 auto; background: transparent; padding: 0 30px; } #wrapper { float: left; overflow: hidden; width: 660px; } #box2 { width: 240px; float: right; font-size: 14px; word-wrap: break-word; } /*#blog-title-inner{*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-position: left 0px;*/ /*}*/ /*.header-image-only #blog-title-inner {*/ /*background-repeat: no-repeat;*/ /*position: relative;*/ /*height: 200px;*/ /*display: none;*/ /*}*/ /*#blog-title {*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-image: url('http://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527172848.png');*/ /*background-repeat: no-repeat;*/ /*background-position: left 0px;*/ /*}*/