ZOZOFIT 認証フローにおけるJVM言語実装のLambda関数のパフォーマンス改善

OGP

はじめに

こんにちは。計測プラットフォーム開発本部バックエンドチームの岡山です。普段はZOZOMATやZOZOGLASSなどの計測技術に関わるシステムの開発、運用に携わっています。去年の夏にZOZOFITというサービスを北米向けにローンチし、そのシステムも同様に開発、運用に携わっています。

本記事では、ZOZOFITの認証フローで実行されるScala実装のAWS Lambda関数が抱えていたパフォーマンス課題と、その課題の解決に至るまでの取り組みについてご紹介します。

目次

ZOZOFITとは

ZOZOFITは2022年に発表した体型管理を目的としたフィットネスアプリです。ZOZOSUITの計測技術を利用したサービスであり、2023年3月時点では、体型計測および身体3Dモデルのデータ・体脂肪率の表示機能を提供しています。

zozofit

ZOZOFITが利用する認証サービス

ZOZOFITでは以下の理由により、認証処理の実装にAmazon Cognitoを利用しています。

  • 認証・認可の機構を時間をかけて実装するよりもコアとなる機能の実装に時間を使いたかった
  • 普段から利用しているAWSの他サービスとの親和性が高かった

また、サービス要件に対応するためAmazon Cognitoが提供しているカスタム認証フローを利用しました。

カスタム認証フローとは

Amazon Cognitoではユーザーを認証するフローが複数用意されています。カスタム認証フローはその中の1つのフローです。カスタム認証フローを利用することで、AWS Lambdaトリガーを利用し認証フローをカスタマイズ可能です。

カスタム認証フローの詳細についてはAmazon Cognitoのドキュメントをご覧ください。

docs.aws.amazon.com

パフォーマンスに関する課題

私たちのチームではDatadogを利用して定期的にレイテンシを振り返っており、その活動の中でサインアップに関連するエンドポイントのパフォーマンスが悪いことを知りました。原因の深掘りのためにAPMを利用して調査しました。結果として、サインアップ時に利用される処理のエンドポイントの大半が特定の処理に偏っていることが分かりました。下記は実際のトレースの一例です。Amazon CognitoのInitiateAuth APIの処理がdurationの大半を占めています。

traceing for signup

問題となっているエンドポイントの詳細について説明します。下図は簡略化したZOZOFITの認証フローの図で、赤色で示す箇所がパフォーマンスに課題のあるエンドポイントの処理です。まず初めにユーザーはZOZOFITを利用するためにEメールアドレスを入力し認証を開始します。そして、APIサーバーはクライアントからの呼び出しを受けて、Amazon CognitoのInitiateAuth APIを呼び出します。Amazon CognitoはInitiateAuth APIへのリクエストを受けて、2つのAWS Lambda関数を同期的に呼び出します。APIサーバーはLambdaの処理が完了したのを受けてクライアントにレスポンスを返します。

このフローの特徴として、Create Auth Challenge Lambdaの実行時に、AWS SDKを使用してAmazon SESで確認コードを含んだEメールをユーザーへ同期的に送信しています。

sequence_issue

上記のエンドポイントは90パーセンタイルでも 4640.8ms のレスポンスタイムでした。私たちのチームでは、ZOZOFITのローンチ前にEメールを同期的に送信する意思決定をしていました。しかし、そのことを加味しても想定外の数値であり、改善する必要があると考えました。

latency of signup

認証フローの図を見ると分かるようにAmazon CognitoのInitiate Auth APIをリクエストすると、2つのAWS Lambda関数が実行されます。この時Amazon CognitoはLambda関数を同期的に呼び出します。よって、これらLambda関数の実行時間がエンドポイントのパフォーマンスに直接影響を与えていると仮説を立てて調査を進めました。

カスタム認証フローにおけるボトルネックの特定

次に、問題となっていたAWS Lambda関数のパフォーマンス調査をしました。

問題となっていたエンドポイントで実行されるAWS Lambda関数は2つあったため、問題の切り分けのためにDatadogを利用して関数の実行時間の確認をしました。

2つのAWS Lambda関数の実行時間の平均を比較すると、Create Auth Challenge Lambdaはもう1つのLambda関数より 2092ms 長いことが判明しました。 lambdas_performance

Lambda関数のボトルネック調査

次に、Create Auth Challenge Lambda関数をトレースし、処理のボトルネックを明らかにすることを考えました。このLambda関数はScalaで実装しており、GraalVM Native Imageで実行ファイルにコンパイルし、Lambdaのカスタムランタイム上で関数を実行しています。今回はそのような状況に適したトレーシングツールを発見できず、処理時間をログ出力する方法でボトルネックを探りました。

Lambda関数をScalaで実装した背景は、チームで2つ以上の言語を維持運用するほどのチーム規模もないため、普段使い慣れているScalaを選択したことにあります。

背景の詳細については、以下記事の計測システム部児島が記載した内容を参考にしてください。

AWS re:Invent 2022 参加レポート(ラスベガスの写真と厳選したセッション情報をお届けします!)

調査した結果、ボトルネックとなっていた処理はAWS SDKを使いAmazon SESで確認コードを含んだEメールをユーザーへ送信する処理でした。この処理に必要な時間はAWS Lambda関数全体の実行時間の 約91.5% を占めていました。このLambda関数のメイン処理は上述のAWS SDKを使ったEメール送信であったため、この調査結果は想定範囲内でした。

percentage for sendEmail

次に、cold startとwarm startでの実行時間の差分を調査しました。一般的にcold start無し(いわゆるwarm start)の場合、Lambdaの実行環境やAWS SDKクライアントなどが再利用されるため実行時間の短縮を期待できます。下記の図は調査結果を表した図となります。

comparison_cold_and_warm_starts

warm startの場合に処理時間が平均1921ms短縮されることを確認しました。

上記の調査結果から、実際のボトルネックはAWS SDKクライアントの初期化処理などのEメールを送信するための準備処理であると仮説を立てることが出来ました。

ここまでの調査結果を踏まえ、以下の解決策が考えられました。

  1. Lambda関数がwarm startで実行される状態を増やす
  2. Lambda関数がcold startで実行される場合でも高速に動作するよう修正

(1)の具体的な解決策としては、Provisioned Concurrencyの設定があります。ZOZOFITはローンチされたばかりのサービスでユーザー数がまだ多くないため事前にLambda関数の実行予約をするのはコストに釣り合わないと判断しました。また、Lambda SnapStart有効化による効果についても実際にパフォーマンスを調査・検討しました。下図はSnapStartの有無とGraalVM Native Imageを利用した場合における関数の処理時間の比較です。今回の調査においては、既存実装であるGraalVM Native Imageを利用した方法の方が良いパフォーマンスであったため、SnapStartの有効化を選択しませんでした。

duration of each optimization.png

最終的に、私たちは(2)を選択し、Lambda関数に設定されているメモリの最適化を実施しました。設定メモリの最適化はLambda関数のパフォーマンス改善策として知られており、コードの修正も発生しないことから小さく始めることができます。

Lambda関数のメモリ設定最適化

AWS Lambdaでは、設定されるメモリの量によって、Lambda関数で使用できる仮想CPUの量が決まります。メモリを追加することで、それに比例して使用可能な全体的な計算能力が向上します。

詳細は、AWSの公式ブログ記事を参考にしてください。

このLambda関数に設定されていたメモリは128MBでした。この値が設定されていた理由は、ZOZOFITローンチ時、このLambda関数のレスポンス性能はミッションクリティカルでなく、実際にパフォーマンス課題等が見られた際に調整する想定があったためです。

当初、どれほどのメモリを設定すれば効率よくLambda関数の実行時間を減らすことができるかが不明でした。ですので、Lambda関数の設定メモリを変更し、実行時間やコストの変化を調査しました。元々設定されていた128MBからスタートし、256, 512, 1024MBと順に設定メモリを変更しLambda関数を実行しました。

下図が設定メモリ別の課金時間と月額の利用料金です。この図を参考にしながらチームで議論し、Lambda関数の設定メモリに1024MBを指定する決定をしました。理由は2点あり、1024MBで課金時間の減少幅が落ち着いていること、月額の利用料金も約$0.02と許容できる額だったためです。

billing_time_and_monthly_cost

パフォーマンス改善結果

最後に、Create Auth Challenge Lambdaと認証開始エンドポイントのパフォーマンスの変化についてまとめました。どちらも90パーセンタイルのレスポンスタイムにて比較をしています。また、Create Auth Challenge Lambdaはcold start有の場合の実行時間を示しています。 performance_by_changing_memory

上図からわかる通り、最終的にCreate Auth Challenge Lambdaの実行時間は 約76.2% 削減され、認証開始エンドポイントのレスポンスタイムは 約44.5% 削減されました。

メモリ設定の最適化後、再度Create Auth Challenge Lambda関数のボトルネック処理を調査しました。最適化前と同様に、AWS SDKを使い、Amazon SESでユーザーにEメールを送信する処理がボトルネックであることを確認しました。これはある意味想定通りで、メモリ設定の最適化によってLambda関数の処理全体のパフォーマンスが向上した結果と考えることができます。

今回はメモリ設定の見直しにより、Lambda関数がcold start時でも高速に動作するよう修正しました。しかし、今後のチーム状況によってはGraalVM Native Imageを使わず、Javaランタイム上でLambda関数を実行したくなる可能性もあると思います。そのような時は、再度Lambda SnapStartの有効化を検討したり、パフォーマンスを保ちながら、チームで運用しやすくなる工夫を取り入れていきたいです。

終わりに

ZOZOFITの認証フローで実行されるScala実装のAWS Lambda関数のパフォーマンス改善についてご紹介しました。ZOZOFITはZOZO New Zealandとの協業のプロジェクトであり言語の壁もあるので簡単なプロジェクトではないですが、チームでADRを残す取り組みやプロジェクト全体の改善も進めています。

計測プラットフォーム部バックエンドチームでは、ZOZOFITのように、日本国内に限らず新しいサービスを開発していくバックエンドエンジニアを募集しています。

ご興味のある方は、以下のリンクからぜひご応募ください!

hrmos.co

カテゴリー