Quantcast
Channel: クックパッド開発者ブログ
Viewing all articles
Browse latest Browse all 726

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

$
0
0

こんにちは、成田(@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 は内部的にこのピクセルキャッシュで画像を表現しています。 画像を処理する場合には、このピクセルキャッシュの領域を確保するためにメモリやディスクといったリソースを消費することになります。

typedefstruct _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種類があり、それぞれ容量のリミットが設定されています。

リソース名 対応する環境変数 デフォルト値
memoryMAGICK_MEMORY_LIMITシステムのメモリサイズ [bytes]
mapMAGICK_MAP_LIMITシステムのメモリサイズ * 2 [bytes]
diskMAGICK_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"/>
リソース名 対応する環境変数 デフォルト値
areaMAGICK_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"/>
リソース名 対応する環境変数 デフォルト値
widthMAGICK_WIDTH_LIMIT 214.7 MP (Q16の場合。Q8なら429.5MP)
heightMAGICK_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-pathMAGICK_TEMPORARY_PATH, MAGICK_TMPDIR$TMPDIRの値( /tmpなど)
fileMAGICK_FILE_LIMITulimit -nの 3/4
threadMAGICK_THREAD_LIMIT OpenMPの最大スレッド数。OpenMP無効時は 1
throttleMAGICK_THROTTLE_LIMIT0 [microseconds]
timeMAGICK_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 におけるピクセルキャッシュの仕組みと、そのリソース制限について紹介しました。

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


Viewing all articles
Browse latest Browse all 726

Trending Articles