ImageMagick ARM libvips

ARMアーキテクチャとlibvipsへの変更で画像変換のコストが40%ダウン

ImageMagick ARM libvips

 こんにちは、最近は旅行しているか、コードを書いているかの2極化が進みつつあります、P山です。直近の業務において、私が支援している国内最大級のハンドメイドマーケットサービス minne において画像変換サーバの実装を変更し、大幅にコストダウンできたので、その事例を紹介します。

minneについて

 minneはハンドメイド作家が創作したハンドメイド作品を販売することができるハンドメイド作家支援サービスです。技術スタックとしてはRuby on Railsを軸に、実行環境はOpenStackとAWSを用いたデュアルスタックのKubernetesを利用しており、スマートフォンアプリもiOS、Androidともに提供しています。  幅広い技術を、モダンな構成で扱うことができるので、もし採用にご興味があれば採用ページ をご確認ください。ペパボ社内を見渡しても若いメンバーが比較的多く、日々活気のある開発がされています。なにか個人的に相談したいことがあれば DM で質問もお待ちしております。

画像変換サーバについて

 minneの画像配信はオリジナルのデータをS3に保存して、その前段にLambdaで実行される画像変換サーバがあり、変換した画像をCloud Frontで配信しています。

architecture

ImageMagickからlibvipsへ

 画像変換サーバについては、Go言語を用いて実装されており、変更前の画像変換についてはImageMagickを利用して実行されていました。今回、ImageMagickからlibvipsのGo言語のバインディング実装であるh2non/bimgを利用した実行に変更しました。なお、ImageMagickが利用されていた経緯としては、Go言語のimageパッケージを利用したところ、我々の環境ではうまくカラープロファイルが引き継がれないという問題があったため、ImageMagickをexecして利用するような実装になっていました。

 下記にImageMagickとlibvipsのjpgイメージを用いたベンチマークの結果を示します。

# サイズ変換処理
BenchmarkConvertImageUseImageMagick-11                        22          56439328 ns/op
BenchmarkConvertImageUseLibVips-11                            129           9790194 ns/op
# サイズ変換+jpgからpngへの変換
BenchmarkConvertImageWithPNGUseImageMagick-11                 10         110441008 ns/op
BenchmarkConvertImageWithPNGUseLibVips-11                      46          24438304 ns/op

 上記の通り、サイズ変換処理では6倍程度、サイズ変換+jpgからpngへの変換においては5倍程度の高速が見られます。我々がこの処理を実行しているLambdaにおいては処理時間は支払い料金に直結するため、大きなコストダウンが期待できます。

ARMアーキテクチャへの変更

 LambdaではGo言語の実行環境をDockerイメージで準備することが可能です。libvipsもARMのパッケージが提供されていたため、旧来のlinux/amd64の環境からlinux/arm64の環境へと変更しました。Dockerfileの一部を紹介します。

FROM golang:latest as build
WORKDIR /our_product_name
COPY go.mod go.sum ./
COPY . /our_product_name
ARG TARGETARCH
RUN apt-get update && apt-get install -y libvips-dev
RUN GOOS=linux GOARCH=$TARGETARCH make build

 Dockerはビルド時のCPUアーキテクチャをTARGETARCH に格納しているので、この値をGo言語のビルドにも渡しています。Go言語自体がもともとクロスコンパイルをサポートしているので、すんなり移行することが出来ました。

効果測定

 下記のグラフが、リリース後のAWSの利用料金のグラフです。具体的な金額の記載は控えますが、概ねリリース前と比較して4割程度の削減をすることが出来ました。なおグラフではもっと減っているように見えますが、下記のグラフはLambda単体なので、その他の料金も含めると4割という話です。

コスト削減

 また処理速度についてのグラフも下記に示します。

レイテンシの比較

 少々分かりづらいのですが、グラフの左の1が、amd64 + ImageMagickです。グラフの真ん中の2の低いラインが arm64 + libvipsで、グラフの右の3がarm64 + ImageMagickです。グラフから分かる通り、arm64 + ImageMagickでももともとの処理時間の3分の2程度になっており、アーキテクチャの変更だけでもそれなりの高速化が見込めることがわかります。arm64 + libvipsとなると、もともとの処理時間の半分程度になるので、さらに高速に処理することが出来ました。

発生した課題

 ImageMagickからlibvipsに移行するに当たり、画像変換の処理が一部異なり、サイズ変更したときに画像の上下が黒塗りになってしまう事象が発生しました。先のグラフが、謎の順序になっていたのは、実はこの事象による切り戻しによって生成された結果であるからです。

黒塗り

 もともとImageMagickでは下記のようにサイズ変換していました。

convert := exec.Command("convert", "-resize", fmt.Sprintf("%dx%d", p.Width, p.Height))

それを下記のように実装すると、不具合が発生します。

r, err := bimage.Resize(
        p.Width,
        p.Height,
})

 この事象を防ぐには、下記のようにEnlargeメソッドを利用する必要があります。

r, err := bimage.Enlarge(
  p.Width,
  p.Height,
)

 このように実装することで画像のアスペクト比を保存したまま、拡大することが出来ます。このあたりの実装の微妙な差を目視で確認するために、画像をそれぞれの実装で変換後、HTMLで確認できるようなユーティリティコマンドも今回追加しており、他職種と相互に確認するときに非常に便利でした。この手のユーティリティも最近はAIがピッと作ってくれるので便利な時代になったなと思います。

最後に

 ImageMagickは非常に多機能で便利なのですが、我々の用途のようなシンプルな画像変換や変更であればlibvipsのほうが高速です。またbimgが様々なメソッドを提供してくれているので、移行も非常に容易でした。minneでは今回紹介したような仕組みだけでなく、内部広告や、検索など幅広いチャレンジを行っているので、ぜひ一緒に働きましょう!