画像の動的リサイズ配信システムをサービスインするまで

覚え書きのまとめ記事なので消すかもしれないです。

# 背景

サービスが開始してから１０年が経過したE-commerceを運用しています。
商品画像のデータをアップロード時に複数種類の縦横比率に画像処理してからS3にアップロードしていました。
商品は10億品近くまで作成され、それに紐づく商品画像は23億枚程度も存在します。
23億枚の画像の縦横比率違いが数枚ストレージに保存されているので、データ量はペタバイトにまで達しました。
ペタまでくると、S3の定常コストはえげつないです。

なるべくなら、取り扱いのない商品の画像データを削除してしまいたいのですが、
ビジネス的な事情やデグレの心配から、データの削除を推進することが難しいです。

とある事情でCDNの利用にかなりのディスカウントがありました。

そこで、サイズ違いの画像データは削除してしまって、リクエストされるたびに動的に作成し、それをCDNにキャッシュさせることでストレージのデータ量を削減することが検討されました。

# 複数サイズを管理するサービス構成（AS-IS）

シンプルな構成なので、小さなサービスだとよく考える構成だと思います

![Screenshot 2024-08-20 at 23.56.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315979/b5ecd288-7e25-671a-5d09-89973e82d013.png)


# 実現構成案

動的リサイズシステムは数社で提供されているので、利用を検討しても良いと思います。
運用しているサービスの場合は計算上だと自前の構築の方がコスト削減効果があったので、自前で構築することが決まりました。

1. AWS Lambda
1. AWS AppRunner
1. AWS EKS
1. AWS ECS
1. AWS EC2

## Lambda

![Screenshot 2024-08-21 at 0.01.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315979/b27598b1-e74f-d710-a167-4b0a0b7cd210.png)

AWSの公式がPythonで動的リサイズのスクリプトを公開しているので、それで実現できます。
ただ、Lambdaはの利用だと利用料が安いですが、コールドスタートです。
通常利用だと動的リサイズには適しません。
LambdaにはProvisioned concurrencyという設定があって、コードがプロビジョニングされているコンテナを待機させておくことができます。
Provisioned concurrencyを使えば、実行のオーバーヘッドがかなり減ります。
最大rpsがそこまでないサービスであれば、採用して良さそうです。

### 技術検証

- Java
  - thumbnailator
  - JMagick(ImageMagick)
  - OpenCV
  - bimg(libvips)
- JavaScript
  - Sharp(libvips)
  - Sharp + LLRT
- Python
  - Pillow-SIMD(PIL)
  - OpenCV
- Go
  - 標準ライブラリ
  - GoCV
  - imagick(ImageMagick)
  - bimg(libvips)
  - govips(libvips)

を比較しました。

S3からのレスポンスをバイナリのまま扱ったり、fileでダウンロードしたり、
クライアントへのレスポンスをストリームにしてみたり、
保管フィルタのアルゴリズムを変えてみて
低コンピュートリソース、低レイテンシーを目指しました。

### 知見

- S3からのレスポンスはファイルダウンロード
- 保管フィルタはNearest（縮小リサイズの場合）
- レスポンスをストリーム

で実装すると一番良い結果が得られました。

自分が試した中だと、
libvips > PIL > OpenCV > それ以外
で画像変換ライブラリが早かったです。

Go bimg の組み合わせが一番パフォーマンスが出ました。

コンピュートリソースは
- Memory: 1024MB ~ 2048MB

あたりが一番効率が良さそうでした。

latencyは 
- JPEG: 300 ~ 500ms
- PNG: 1 ~ 1.5s
- GIF(1frame): 300 ~ 500ms

くらいでした。ファイルサイズの大きいPNGのデコードで速度が全然出ませんでした。

ちなみに、うろ覚えですが、
- JavaはLambda用のベースイメージ？ランタイム？を使うことでコールドスタートのオーバーヘッドが抑えられます。
- JavascriptはLLRTというLambda用のベースイメージ？ランタイム？があって、コールドスタートのオーバーヘッド？が抑えられるらしいです。
    - 画像変換ライブラリが使えず、利用を諦めました。

### 採用判断

Lambdaの利用は見送りました。
理由は 最大 rps を捌くために用意する Provisioned concurrency がそれなりに必要。
PNGの処理が遅い。
メトリクスが少なく、監視性が悪い。
AWS Lambdaのインターフェース(API)は扱いにくく、テストコードも書きにくい。アプリケーションコードの管理が大変。

## AWS AppRunner

![Screenshot 2024-08-21 at 0.54.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315979/ec046d27-9df5-7ca4-0baf-3ed0ee548910.png)


サーバレスアプリを簡単にデプロイできるサービスです。
デプロイのトリガーをコード管理のrepogitoryの変更にできたり、コンピュートリソースをコンソール画面からぽちぽち設定できたり、モニタリングダッシュボードが用意されていたり、フルマネージドです。

デフォルトはLambdaと同じようにコールドスタートですが、Provisioned concurrencyが設定できます。

### 知見

Lambdaと同じアルゴリズムのコードで、
Lambdaよりも省リソースでパフォーマンスがよかったです。

### 採用判断

大きな問題があって、Provisioned concurrencyには上限があって、それが低いです。
運用に耐えられなかったです。

## EKS

![Screenshot 2024-08-21 at 0.57.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315979/a9a381bc-1630-6568-c62b-17aa5a5e6609.png)

https://qiita.com/count3/items/44b8e783aff3889dd049

この記事よりも雑な記事ですが、上記の通りです。

### 知見

コンテナのスケーリング管理のためにHPAを利用しているのですが、トリガーとなる閾値の設定に苦労しました。
検証した結果、ExternalMetricを利用することが良さそうでした。
ExternalMetricはコントロールが難しくて、オーバースケールしないようにHPAのbehaviorを設定したりと、いくつかクセがありました。

Kubernetesクラスタの内部の話を色々書きたいけど、書けない。

### 採用判断

この方法を採用することにしました。

## ECS, EC2

ECSもEKSと同じ程度のパフォーマンスが確認できました。
管理も楽なので、EKSよりも良い選択かもしれません。
内部事情で採用しなかったです。

EC2も採用でも良さそうですが、コスト的にECSで良いのかなと思いました。

# 負荷試験

https://aws.amazon.com/jp/builders-flash/202308/distributed-test-on-aws/

上記の資料を大変参考になリました。
負荷試験を行う上で定義すべき内容も項目立ててあるので、注意すべき事項を事前に洗い出すことができます。
ただ、AS-ISのアーキテクチャと同等のSLOを求められていたので、かなりセンシティブにチューニングしました。

### 知見

S3はそこそこSlowDownや500, 503を返すことがあります（Ratelimit）。
このRatelimitのアルゴリズムにクセがあって、エンドポイントの前方からのstringをキーとしてパーティショニングしているみたいで、思ったより、5xx系のレスポンスが発生していました。

# サービスイン戦略

CloudFrontにはContinuousDeploymentPolicyという機能があります。
これは、いわゆるCanary Releaseです。
切り替えの前に、
- リクエストの20%までを新しい設定に流す
- 特定のリクエストヘッダーでアクセスすると新しい設定に流す

検証を行うことができます。

また、CloudFrontにはbehaviorという機能があります。
これはパスベースでupstream server(origin)を切り替えることができます。

bahaviorはパスパターンで `*` を使うことができるので、 パスパラメータの一部ごとにoriginを切り替えることができます。
この方法でRoute53のweighted routingのようなことを実現できます。

# 監視

CloudFrontのメトリクスはVirginiaでのみ管理することができます。
CloudWatchのようなツールで監視するためにはRegion Virginiaでリソースを作成する必要があります。

ちなみに、
Slack連携するためにChatbotというリソースを使ったのですが、これがTerraformに対応していなくて、
特定のmoduleを利用する必要があります。ちょっとクセがあります。

# 最後に

このプロジェクトとっても楽しかった。
会社とチームに感謝。