💻テック💼仕事

ワーカーからLambdaへ:360°キューブマップ生成を約4倍高速化した話

Tony Duong

Tony Duong

6月 15, 20264

他の言語:🇫🇷🇬🇧
#aws#lambda#sidekiq#rails#performance#spacely#py360convert#krpano#360#s3
ワーカーからLambdaへ:360°キューブマップ生成を約4倍高速化した話

この記事はもともとSpacely技術ブログで公開したものです:360°キューブマップ生成をワーカーからLambdaへ移して約4倍高速化した話

こんにちは、SpacelyでRailsバックエンドエンジニアをしているTony Duongです。日々Spacelyのプラットフォーム開発に携わっています。この記事では、spacely_web Railsアプリケーションの中でもっとも稼働が多くCPUを消費するジョブのひとつ——360°写真をキューブマップに変換する処理——をSidekiqワーカーから切り離してAWS Lambdaへ移し、その過程でエンドツーエンドの体験を約4倍高速にした話を紹介します。

キューブマップとは何か、そしてなぜ生成するのか

ユーザーが360°の部屋写真をアップロードすると、それは1枚の**正距円筒図法(equirectangular)**のJPEGとして届きます。横が縦の2倍ある、あの「展開された球体」画像です。この投影は保存には向いていますが、リアルタイムで描画するにはコストがかかります。

正距円筒パノラマのサンプル:横が縦の2倍ある1枚の画像で、天井と床が引き伸ばされ湾曲している

正距円筒パノラマ。天井と床が引き伸ばされ歪んでいるのが分かります——360°の部屋全体が1枚の長方形画像に押し込められています。

そこで写真を表示する前に、これをキューブマップに変換します。同じシーンを立方体の6面——右・左・上・下・前・後(pano_r / pano_l / pano_u / pano_d / pano_f / pano_b)——に再投影したものです。上のパノラマから生成された6つの面はこちらです:

front face前面 (pano_f) right face右面 (pano_r) back face後面 (pano_b)
left face左面 (pano_l) up face上面 (pano_u) down face下面 (pano_d)

同じ部屋を、歪みのない6枚の正方形の面として表したもの。引き伸ばされた正距円筒画像とは違い、各面は普通のフラットな写真です——まさにGPUが立方体にテクスチャとして貼り付けたい形そのものです。

この6つの面こそが、パノラマプレイヤーが実際に描画するものです。ビューアはこれらをカメラを囲む立方体の内側にテクスチャとして貼り付けます。部屋を見回そうとドラッグしているとき、あなたが見ているのはそのフラットな面で、GPUは歪んだ正距円筒画像よりはるかに低コストでサンプリングできます。Spacelyで見回すすべての360°ツアーは、このジョブが生成したキューブマップから描画されています。

SpacelyのVR360°プレイヤーはこちらで体験できます:**https://info.spacely.co.jp/realestate-vr/**。

この変換は1日に数千回実行され、しかもアップロードの経路上に位置しています——そのためスピードはユーザーが直接体感するものなのです。

問題:Sidekiqプールを共有するCPUバウンドなジョブ

旧来のジョブCreateCubeMapJobは、CPU負荷の高いkrpano 1.1xの変換をすべてSidekiqワーカー上で実行し、その結果をS3にアップロードしていました。

これには3つの厄介な点がありました:

  1. 他のすべてを枯渇させていた。 この変換はCPU負荷が高く、他のジョブと同じSidekiqワーカー上で実行されていました。Spacelyの1つのプロジェクトは最大50枚のパノラマを保持できるため、ユーザーがプロジェクトを一括でアップロードすると、これらのジョブが何十個も並列で走り、CPUを占有し、キューが詰まる間に無関係なジョブまで遅くなりました。Sidekiqには現実的なスケーリングの限界があるため、バーストアップロードはレイテンシのスパイクに変わりました。

  2. 実行時間が予測できなかった。 このジョブはワーカーをすべてのジョブと共有していたため、1回の変換にかかる時間は、同じ瞬間にたまたま実行されている他のキューブマップジョブ——そして他のCPUバウンドなジョブ——の数に完全に左右されました。ワーカーに余裕があるときはすぐに終わり(約5〜10秒)、混雑時にはまったく同じ変換が最大で約2分かかることもありました。

  3. krpano 1.1xはアップデートの時期をとうに過ぎていた。 これは2019年のリリースです。新しいライブラリが役立つことを期待しましたが、現実的な並行負荷下(後述)では効果はわずかでした。

Before architecture: an ECS Sidekiq service whose tasks are saturated by CPU-heavy krpano cubemap jobs, with unrelated jobs waiting behind them

バースト時には、CPUバウンドなkrpanoジョブ(オレンジ)がすべてのSidekiqタスクを飽和させ、無関係なジョブ(グレー)が順番待ちになります——これがキューブマップ処理も非キューブマップ処理も両方遅くなった理由です。

魅力的だが本質ではない解決策:ただひたすらスケールする

最初に思いつく当たり前の反応は、ハードウェアを投入することです。Sidekiqワーカーにより大きなCPUを与え(垂直スケーリング)、その数を増やす(水平スケーリング)。これは確かに余裕を生み、ある程度まではやる価値があります——しかし問題を実際に解決するわけではありません。変換は依然として他のすべてのジョブと同じワーカーを奪い合っているので、新しく確保した容量を超えて負荷が増えた瞬間、私たちは同じ競合と同じ予測不能な実行時間に逆戻りします。スケーリングは壁を遠ざけるだけで、壁から離れさせてはくれません。

より良い解決策はアーキテクチャ的なもので、しかも大袈裟なものではありません。ロジックを書き直す代わりに、重い部分をそのために作られた環境へ委譲するのです。コストの高いCPUバウンドな変換は、レイテンシに敏感なジョブと共有されるワーカープールではなく、隔離されたオンデマンドのコンピュート(Lambda)に属すべきものです。重要なのは、私たちはこれをインターフェースを同一に保ったまま行ったことです。新しいジョブは古いものと同じ入力と同じ副作用を持ちます——同じソースを読み、同じ6つの面を同じS3の場所に書き込みます。コントラクトを変えずに保ったことで、影響範囲が最小限に留まるという確信を持って実装を差し替えることができました。下流のどこも、どちらのジョブがキューブマップを生成したかを区別できないのです。

この記事の残りは、その変更の2つの半分——変換エンジンの評価と、処理をワーカーから切り離すこと——についてです。

変換エンジンの評価

3つの変換エンジンをベンチマークしました:現行のkrpano 1.1x(2019)、新しいkrpano 1.2x(2025)、そしてオープンソースのPythonライブラリpy360convertです。テストは現実的なバーストを模擬しました:50枚を一括アップロード(短い時間枠で50個のSidekiqジョブをエンキュー)。インフラは変更なし——Sidekiqをホストする同じECSサーバー、同じCPUとメモリで、変換ライブラリだけが異なります。画像1枚あたりの結果(p50 = 中央値):

エンジン p50 p95
krpano 1.1x 19.6s 39.4s
krpano 1.2x 17.6s 55.5s
py360convert 21.4s 38.3s

ライブラリを差し替えただけでは、目立った改善はありませんでした。 並行負荷下では3つとも同程度の結果に収まりました——共有Sidekiqワーカー上で何十もの変換を同時に走らせるCPU競合が、エンジンごとの速度差をかすませてしまったのです。これではまだ勝者を決めるには不十分です。基盤となるアーキテクチャを変えたときにパフォーマンスがどう変わるかを見てみましょう。

新しい設計:Lambdaへのオフロード

変換をAWS Lambdaへ移しました。50個の並行変換が1個とまったく同じ性能を発揮する、隔離され水平方向にスケールする環境です。

新しいジョブCreateCubeMapV2Jobは処理をオーケストレーションします——各変換がそれぞれ隔離されたLambdaで走る一方で、Sidekiqは軽量なままです。画像のバイトデータはS3とLambdaの間を直接流れ、ワーカーが触れることは一切ありません。

After architecture: a light Sidekiq orchestrator with spare CPU and other jobs running, fanning out to isolated per-image Lambdas, with image bytes flowing directly between S3 and Lambda

ワーカーは調整役に徹し、各変換はそれぞれ専用のLambdaを得ます。画像データはSidekiqを通りません:

バイトデータはワーカーを通らない。 ジョブはサーバーサイドのS3コピーを行い、それからLambdaに**署名付きURL(presigned URL)**を渡します。ソースを読むためのGETと、各出力を書き込むためのPUTです。画像データはS3 → Lambda → S3へ直接流れます。

Lambda上でのエンジンのベンチマーク

CPU競合がなくなった状態で、同じ50枚バーストテストをLambda上で再実行しました——今回はkrpano 1.2xpy360convertを比較します(krpano 1.1xはスキップし、代わりに新しいライブラリでテストしました)。各変換はそれぞれ隔離されたLambda(2048 MB)で走り、変換ライブラリだけが異なります。画像1枚あたりの結果(p50 = 中央値):

5K (5376×2688)

エンジン p50 p90
krpano 1.2x 6.99s 8.33s
py360convert 5.71s 10.95s

11K (11008×5504)

エンジン p50 p90
krpano 1.2x 21.26s 25.17s
py360convert 10.24s 16.44s

2048 MBで計測。11Kの両エンジンとも、LambdaのメモリとvCPUをそれ以上増やしてもパフォーマンスは改善しませんでした——2048 MBがスイートスポットでした。

これを上のSidekiqの表と比べてみてください——そこではどのエンジンもライブラリに関係なく約18〜21秒の範囲に収まっていました。2つの結論が得られます:

  1. アーキテクチャの変更が決定的な勝因だった。 共有ワーカーから隔離されたLambdaへ移したことで、1枚あたりの時間は約18〜21秒から、5Kでは1桁秒台まで短縮されました。Sidekiq上でのライブラリ選択だけではほとんど針が動きませんでした。効いたのは隔離です。

  2. Lambda上ではpy360convertが明確な勝者であり、解像度が上がるほど差が広がる。 5Kではkrpano 1.2xよりわずかに速く(p50で5.71s対6.99s)、11Kではその優位がおよそ倍になります(p50で10.24s対21.26s)。

私たちは本番に**py360convert**を選びました。最も重要なところ——大きなパノラマ——で最速の選択肢であり、外部バイナリやライセンスを管理する必要のない純粋なPythonライブラリだからです。出力画像を同じ(キューブ面の)解像度で生成した場合、2つのエンジンの品質は見た目では区別がつきませんでした。

変換そのもの——Lambdaハンドラ内のpy360convert呼び出し:

def _convert_faces(image: Image.Image, face_width: int = None) -> Dict[str, Image.Image]:
    np_img = np.array(image)

    ...

    faces = py360convert.e2c(np_img, face_w=face_width, cube_format="dict")

    ...

py360convert向けのLambdaメモリ調整

Lambdaでは、メモリ設定は単なる容量ではなく実はパフォーマンスのつまみです——メモリが増えるほど比例してCPUも増え(1,769 MBで関数はフルの1 vCPU相当を得ます)。そのため唯一正しい値というものはありません。少なすぎれば大きなパノラマでメモリ不足になり、多すぎれば速度向上を生まない余剰分にお金を払うことになります。各画像サイズを複数のメモリ設定にわたってベンチマークし、それぞれのスイートスポットを見つけました。

5376×2688のパノラマでは、1024 MBから4096 MBまでテストしました:

Lambdaメモリ p50 p90
1024 MB 9.10s 15.48s
2048 MB 5.71s 10.95s
4096 MB 5.97s 10.69s

パフォーマンスは1024→2048 MBで急激に改善し、その後横ばいになります——4096 MBは2048より速くありません。同じ形状はより大きな11008×5504(約60 MP)のパノラマでも当てはまります。ただしそこでは1024 MBはそもそも動作しません:

Lambdaメモリ p50 p90
1024 MB メモリ不足
2048 MB 10.24s 16.44s
4096 MB 10.12s 16.94s

Line chart of py360convert conversion time (p50) versus Lambda memory. For 5376×2688, time drops from 9.10s at 1024 MB to 5.71s at 2048 MB, then stays flat at 5.97s at 4096 MB. For 11008×5504, 1024 MB runs out of memory, then 10.24s at 2048 MB and 10.12s at 4096 MB. A dashed line marks 2048 MB as the sweet spot.

両方の曲線は同じ場所で折れ曲がります:時間は2048 MBまで急激に下がり、その後横ばいになります——余分なメモリ(とそれに伴うCPU)はもはやボトルネックではありません。

結論:2048 MBが私たちの典型的なパノラマにとってのスイートスポットであり、より大きなティアは規格外の入力のために予備として確保しています。本番では、CreateCubeMapV2Jobがピクセル数に応じて3つのLambdaエンドポイントのいずれかへルーティングします——同じハンドラコードで、メモリサイズだけが異なります:

ルーティングルール:≤ 11K → 2048 MB・≤ 16K → 3072 MB・> 16K → 4096 MB。すべての画像が動作するのに十分なメモリを得て、どれも使い切れない余剰分にお金を払うことはありません。

コスト

ピーク時でも請求額は控えめなままです。混雑する日には約19,000件の変換(月あたり約570,000件)を処理します。2048 MBで平均約17秒の実行時間、東京リージョンの場合(Lambdaの料金):

  • コンピュート: 570,000 × 2 GB × 17s = 19,380,000 GB秒 × $0.0000166667 ≈ 月$323
  • リクエスト: 570,000 × $0.20 / 1M ≈ 月$0.11
  • 加えてAPI Gatewayが数ドル。

つまり全体で最悪のケースでも月およそ$350に収まります——重くバースト性の高いワークロードを共有ワーカープールから切り離し、その代わりに安定して予測可能なレイテンシを得る対価として妥当な価格です。

結果

現実的な最悪ケース——大きな(11008×5504)パノラマ50枚からなるプロジェクト全体を一括アップロード(50はプロジェクトが保持できる最大数)——を本番仕様のインフラでテストし、アップロードから使用可能なプレイヤーまでを計測しました:

パイプライン エンドツーエンド
Sidekiq + krpano 1.1x(before) 約8分50秒
Lambda + krpano 1.2x
Lambda + py360convert(after) 約1分50秒

エンドツーエンドで約4倍の高速化——そして同じくらい重要なのは、Lambdaの数値が安定していることです。Sidekiqでは、ジョブ時間はワーカーの混雑具合によって大きく揺れ動きました。Lambdaでは、隔離された実行のおかげで50個の並行ジョブが1個とほぼ同じ時間で終わります。

これを会社単位のフィーチャーフラグの背後で段階的にロールアウトしています——トラフィックを分割するカナリアではなく、段階的なロールアウトです。リクエスト単位ではなく、会社単位で新しい経路を有効化していきます。

  1. 限定リリース — 一部の会社についてフラグを切り替え、残りはSidekiq + krpano 1.1xのままにしながらパフォーマンスとコストを監視する。
  2. 全社 — Lambda + py360convertをどこでも有効化する。
  3. クリーンアップ — 新しい経路が実証されたら、古いkrpanoジョブとそのツール一式を完全に削除する。

まとめ

  • CPUバウンドでバースト性のある処理を共有ワーカープールから切り離す。 決定的な勝因はアーキテクチャ的な隔離でした——Sidekiq上でライブラリを差し替えることではありません。Lambdaに移ると、**py360convert**が、特に大きな(11K)パノラマで頭ひとつ抜け出しました。
  • リモートワーカーは薄く保ち、署名付きURLを渡す。 LambdaがS3を直接読み書きします——画像のバイトデータがアプリケーションサーバーを通ることは一切ありません。
  • リモートのコンピュートを入力に合わせて適正化する。 ソースのピクセル数でLambdaのメモリティアを選択することで、巨大なパノラマでのメモリ不足失敗と、小さなパノラマでの過剰支払いの両方を回避しました。

Spacelyではエンジニアを募集しています。この種のバックエンドの仕事に興味があれば、ぜひ採用ページをご覧ください。


🌐 Claudeによる翻訳

Tony Duong

著者: Tony Duong

デジタル日記。思考、経験、そして人生についての考え。