ZOZOMETRYにおけるインフラ構成変更による計測値計算処理のパフォーマンス改善

ZOZOMETRYにおけるインフラ構成変更による計測値計算処理のパフォーマンス改善

はじめに

こんにちは。計測プラットフォーム開発本部SREの纐纈です。最近はZOZOMETRYという法人向け計測業務効率化サービスの開発・運用に携わっています。今回ZOZOMETRYが正式に公開されることとなったので、合わせてこの記事を書くことになりました。

biz.zozometry.com

現在はパフォーマンスも良好なZOZOMETRYですが、ローンチ当初はパフォーマンスが良くなく、改善のために様々な検証をしました。今回はその経緯と検証内容を中心に、ZOZOMETRYのパフォーマンス改善についてお話ししたいと思います。

一般的にも、Lambdaを使ったサーバーレスアーキテクチャの構築やSQSを使った非同期処理の設計など、参考になる部分があるかもしれません。ぜひ最後までお付き合い頂ければ幸いです。

ZOZOMETRYの機能と構成紹介

まずは、ZOZOMETRYの機能と構成について簡単に紹介します。

ZOZOMETRYは、アプリでスキャンした体型データを元に、様々な体の部位の計測を可能にしています。計測部位に関しては、ユーザーの希望によって任意の組み合わせが可能です。部位の計測は、ZOZOグループの海外子会社が開発するSDKによって行われます。

これまでの計測サービスでは、このSDKをアプリに組み込んで計測していました。しかし、計測したい部位が企業によって異なるため、計測箇所を柔軟に変更できるようにする必要がありました。そのため、アプリで計測して、計測データをサーバーにアップロードし、サーバー側で計算する仕組みを導入しました。以降、このSDKを内包したサーバーのことをSDKサーバーと呼びます。以下、構成の簡略図です。

計測計算フロー

この図でのAPIサーバーは組織に登録されている計測部位のリストを取得し、計測値計算リクエスト後データベースに保存しています。これを計測値保存APIと呼びます。また、初期段階ではこれらの計測値保存APIやSDKサーバーは、K8s上のPodとして動作していました。SDKサーバーには計測値計算APIからのPod間通信でリクエストが送られる想定でした。

データの流れも補足すると、アプリで計測したデータはS3にアップロードされます。この計測データと前もってS3に格納されている計測箇所ごとのデータを元に、SDKサーバーによってそれぞれの計測箇所ごとの計測値を計算します。その後、その計測値データはデータベースに保存され、APIを通じて取得可能となります。

この構成を取ったことで、選択対象となる計測箇所の追加や更新が容易になり、複数の組織に対して異なる計測部位の提供がしやすくなりました。しかしながら、この構成によって計測値を計算するというサービスのコア機能にパフォーマンスの問題が発生してしまいました。次に、その経緯についてお話しします。

開発段階でのパフォーマンス

ZOZOMETRYは正式に公開する以前に、クローズドローンチ期間を設け、いくつかの企業に提供していました。しかし、その限定的ローンチの直前に、パフォーマンスの問題が発覚しました。具体的には、SDKサーバーにかかる負荷がそこまで高くなくても(~10rps)、タイムアウトやクラッシュが発生してしまう状況でした。

なお、SDKサーバーは1つの計測箇所に対して1つのAPIを叩く形になっており、計測された際には、計測箇所数のリクエストが走ることになります。このため、数十箇所の計測箇所を計測したい組織の場合、SDKサーバーに対して一度に数十リクエストが走ることになります。

この仕様によって、少ない計測数でもリクエストが集中しやすくなっており、負荷によるクラッシュが発生しやすい状況でした。この問題を解決するために、SDKサーバーに負荷をかけるリクエストを直列にすることで、一時的な対応をしました。結果として、計測値結果の算出完了までのリードタイムは伸びてしまいましたが、幸いなことにローンチ時点では提供していた企業が少数だったため、これらの企業でのユースケースでは許容される範囲でした。というのも、仕様上スキャンアプリで計測直後に計測値を確認するわけではなく、スキャン後に別途、企業担当者がWebから計測結果を確認するという流れでした。そのため、計測値の保存に時間がかかってもそこまで支障がありませんでした。

しかしながら、今後の展開やクライアント数の増加を考えると、このままでは問題の発生する可能性が高いと判断し、ローンチ後に本格的なパフォーマンス改善をすることにしました。

ローンチ前の緩和策

計測値保存APIのLambda化とSQSの導入

ローンチ前の緩和策について、まずは紹介します。SDKサーバーが負荷に対して不安定かつ遅延も大きいという問題から、ひとまず同期的にリクエストを処理するのではなく、呼び出し元の計測値保存APIをLambdaに切り替えました。これには、SDKサーバーとの通信で起こるスレッドの占有によって、APIサーバーの他APIに影響を出さないという意図もありました。また、S3のイベントトリガーを使って、S3にアップロードされた計測データをトリガーにしてLambdaを起動し、SDKサーバーにリクエストを送るようにしました。さらに、失敗時の再処理をかけやすくできるように、SQSを挟みました。

この変更によって、スキャン後の計測値計算が非同期で行われることになったので、待ち時間がなくなり、計測体験も向上しました。また、Lambdaの同時実行数を設定することでSDKサーバーの最大負荷も調整できるようになりました。さらに、SQSを挟むことでリトライやDLQの設定が容易になり、計測処理が失敗した場合でも、再処理が可能になりました。

これによってSDKサーバーが安定稼働はするものの、計測箇所の数に応じて直列にSDKサーバーへリクエストを送るので、計測箇所の処理が終わるまでに2〜3分はかかるようになってしまいました。また、スループットに関しても30分の間に30件の計測を処理するのがやっとという状態でした。

スループットが低い理由としては、コストを抑えるためSDKサーバーのPodの台数が少なかったこともあります。HPAを設定していたものの、Podの起動に時間がかかるため、リクエストが集中するとPodの起動が追いつかず、再実行が必要になることがありました。

計測計算フロー Lambda + SQS

項目
計測値保存Lambdaのバッチ処理時間 120~180s
30分で処理可能な計測数 30

LambdaのSnapStart有効化

さらに、LambdaのSnapStartを有効化することで、Lambdaの起動時間を短縮しました。SnapStartは、Lambdaのコンテナを再利用することで、Lambdaの起動時間を短縮する機能です。SnapStartを有効化することで、Lambdaの起動時間が若干ではありますが短縮され、SDKサーバーへのリクエストが早く処理されるようになりました。

項目  
計測値保存Lambdaのバッチ処理時間 90~180s
30分で処理可能な計測数 50~60

この時点で、1件の計測では1.5〜3分、50件の計測を処理するのに25分要する状態でした。ただし、失敗した場合でも自動でLambdaによって計測の再実行が可能になりました。それだけではなく、一定数リトライが失敗した場合でもSQSのDLQにメッセージが移動するように設定していたため、その失敗した計測を検知して再実行できるようになりました。

立て直し

ここからは、このSDKサーバーの安定性とパフォーマンス改善のために行ったことを紹介します。

ローンチ後の改善

SDKサーバーのLambda化

ローンチ後の改善施策として、まずはSDKサーバーのLambda化を行いました。SDKサーバーの処理をLambdaに切り出すことで、SDKに負荷がかかってクラッシュすることを防ぎ、計測値計算APIの安定稼働を実現しました。なお、この時点ではまだ計測値の並列計算はしていない状態です。

計測計算フロー SDK Lambda

この時点でのパフォーマンスは以下のとおりです。

項目
計測値保存Lambdaのバッチ処理時間 90~180s
30分で処理可能な計測数 165

設定値の調整

また、各種インフラの設定値も調整しました。例えば、Lambdaの同時実行数やタイムアウト時間、SQSのバッチサイズやリトライ回数など、パフォーマンスに影響を与える設定値を調整し、最適な値を探りました。

Lambda

同時実行数

Lambdaの同時実行数は、ReservedConcurrentExecutionsという設定値で制御されます。この値を調整することで、Lambdaの同時実行数をクォータ内で確保また制限できます。ただし、この数はアカウント全体で共有されているため、他のLambdaに影響する可能性があります。

また、Lambda化したSDKサーバーを並列で呼び出すため、計測値保存Lambdaの同時実行数と組織で登録可能な計測箇所数の乗数までは、Lambdaの起動数が増えます。そのため、この最大値がクォータに引っかからないように設定する、もしくは事前にクォータ(デフォルトでは1000)の引き上げ申請をする必要があります。

こちらに関しては、現段階では、同時に計測されるようなケースは少ないため、計測値保存Lambdaの同時実行数は小さい値で十分でした。ただし、このLambdaの起動数がクォータに引っかからないように、クォータの割合に応じた監視を入れており、引き上げのタイミングがわかるようにしています。

docs.aws.amazon.com

タイムアウト時間

Lambdaのタイムアウト時間は、1リクエストの処理時間x SQSのバッチサイズに余裕を持たせた値を設定する必要があります。これは、最大SQSのバッチサイズ分のメッセージ数が1つのLambdaで処理されるためです。

また、SQSのメッセージ再送信までの時間もこのタイムアウト時間よりも長い値に設定する必要があります。これによって、Lambdaの処理がタイムアウトすることなく、正常に処理を終えることができます。

docs.aws.amazon.com

SQS

バッチサイズ

SQSのバッチサイズは、1つのLambdaで処理させるメッセージの量を制御する設定値です。この値を大きくすると、Lambdaのタイムアウトに引っかかりやすくなります。逆に、小さくすると、Lambdaの並列数が上がり、キューの中で待ちになるメッセージが増えます。

docs.aws.amazon.com

リトライ回数

SQSのリトライ回数は、Lambdaへ再送信する回数を制御する設定値です。この値を大きくすると、再実行の試行回数が増えるため、DLQでの検知が遅れます。逆に、小さくすると、リトライによって成功した可能性のあるメッセージもDLQに入れられてしまう可能性が高くなります。

こちらは今回の検証項目には含まれていませんが、参考までに記載しておきます。

docs.aws.amazon.com

これらの設定値を調整した際のパフォーマンスは以下のようになりました。

検証した組み合わせは以下の通りです。

  • パターン1
    • Lambdaタイムアウト:180s
    • SQSバッチサイズ:10
    • Lambda同時実行数:10
  • パターン2
    • Lambdaタイムアウト:300s
    • SQSバッチサイズ:10
    • Lambda同時実行数:10
  • パターン3
    • Lambdaタイムアウト:300s
    • SQSバッチサイズ:4
    • Lambda同時実行数:10
  • パターン4
    • Lambdaタイムアウト:300s
    • SQSバッチサイズ:4
    • Lambda同時実行数:20
パターン 1 2 3 4
計測値保存Lambdaのバッチ処理時間 60~180s 60~300s 60~300s 60~300s
30分で処理可能な計測数 165 200 200 500
SQSメッセージの再送信数 3 3 2 2

計測値の並列計算の導入

最後に、計測値計算の並列処理を導入しました。これによって、複数の計測箇所を同時に計算できるようになり、計測値の計算処理が高速化されました。具体的には、計測値保存Lambdaのコードを改修し、複数の計測箇所を並行してリクエストするようにしました。これによって、すべてのメッセージを再送信することなく捌き切ることができるようになりました。

項目
計測値保存Lambdaのバッチ処理時間 ~60s
30分で処理可能な計測数 850
SQSメッセージの再送信数 0

結果

これらの構成変更や設定の調整によって、現在は計測値の計算処理は5秒程度、3分以内に最低でも160件は捌けるようになりました。SDKサーバーのクラッシュの懸念もなくなり、計測値の計算処理も安定しています。また、スケールアウトも容易になり、今後のクライアント数の増加にも対応しやすくなりました。

項目 改善前 改善後
計測1件あたりの処理時間 120~180s 5~10s
30分で処理可能な計測数 30 850

余談

さて、これらのパフォーマンス改善ですが、実はこれらを改善する前までビジネス的な優先度が低いとされていました。なぜなら、ローンチ直後には、クライアントからのフィードバックも特になく、負荷試験の結果から見ても先1年はビジネスチームからも問題がないとされていたからです。

とはいえ、以前のままでは将来的に問題の発生する可能性が高く、SREとしては喫緊のタスクがなかったこともあり、機能開発の裏でパフォーマンス検証を進めていきました。その後、検証結果を提示することで改善の効果とその工数を確認してもらいました。その結果、ビジネスチームからの期待値が上がり、本格的なパフォーマンス改善を進めることができました。コストに関しても、イベント駆動型の設計に切り替えたことで、インフラコストを抑えることができました。

学んだこと

最後に、今回のパフォーマンス改善を通じて学んだことをまとめます。

もちろん、パフォーマンスがあまり優れないAPIをLambdaに載せ替え、並列化することでレイテンシや安定性を向上させることができたのは、あまり新鮮ではなかったかもしれません。しかし、今回の改善を通じて、以下のようなことを学びました。

不確定要素が多い機能の実装は早めに設計を済ませる

今回、この計測値計算の機能はローンチ間際まで設計が進んでいませんでした。そのため、ローンチ後にパフォーマンスの問題が発生し、改善に時間がかかってしまいました。SDKの仕様や計測値計算の仕組みを早い段階で把握していれば、より適切なインフラ設計をできたと思います。

PoC段階でパフォーマンス試験を実施する

今回の計測値を計算する機能ですが、PoC段階では複数企業に対応したものではなく、単一企業を想定した実装となっていました。そのため、複数の企業に対応した際のパフォーマンスの問題がローンチ間際に発生しました。PoC段階で複数企業に対応したパフォーマンス試験を取り入れていれば、この問題を事前に発見し、改善できたかもしれません。

コミュニケーションコストが高い開発では、早い段階で認識を合わせる

今回のSDKの改善に取り組みづらかった理由として、SDKの開発をZOZOグループの海外子会社であるZOZO NEW ZEALANDが担っていたことが挙げられます。そのため、SDKの仕組みやコードの詳細を把握しきれていませんでしたが、SDKの仕組みを理解していれば、改善のアプローチも取りやすかったかもしれません。実装する上でボトルネックになりそうな箇所がある場合、早い段階で認識を合わせることが重要であると感じました。

終わりに

今回は、ZOZOMETRYのパフォーマンス改善についてお話ししました。ローンチ直後は問題があったものの、今では安定して計測値の処理も行えるようになりました。今後も引き続き、ZOZOMETRYの改善に取り組んでいきます。

また、この記事以外にもZOZOMETRYに関する記事を連載しておりますので、興味のある方はぜひご覧ください。

techblog.zozo.com

techblog.zozo.com

techblog.zozo.com

ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。

corp.zozo.com

カテゴリー