ZOZOTOWNカート機能のリプレイスPhase1裏側を大公開

ogp

こんにちは、カート決済部の佐藤です。普段はZOZOTOWNカート決済サービスの新機能開発、既存改修、運用保守を担当しております。

弊社はモノリスからマイクロサービスへのリプレイスを進めており、カート決済サービスも先日リプレイスPhase1の記事を掲載いたしました。

techblog.zozo.com

本記事ではカートリプレイスPhase1全体を振り返りつつ、リプレイスプロジェクトを進める中で苦労した点や得られた知見等の、リプレイスの裏側をご紹介したいと思います。

カート機能はECサイトの中核を担う機能であり、障害のリスクを考えるとドラスティックな改修には中々手を出しにくい機能だと思っています。しかし、弊社ではリプレイスをしたことで確実に前進できたため、同じようなお悩みを抱えている方の参考になれば幸いです。

はじまり

2021年元旦、特定商品へのカート投入リクエストの集中によりデータベースのキャパシティを超えてしまいエラーが多発する事象が発生しました。そのようにアクセスがスパイクするような商品を弊社では過熱商品と呼んでいます。

以前から過熱商品の販売によるアクセス数の増加はありましたが、2021年元旦は圧倒的に群を抜いたアクセス数を記録し、明らかに一般ユーザーの導線ではなくBOTによる過剰アクセスと判断できました。当時は過剰アクセスへの対抗策が少なく、結果的に数時間カート投入・注文がしづらい状況となり損害は大きいものとなってしまいました。

その後、BOTによる過剰アクセスにも耐えうるシステムの整備が急務となりましたが、以下の状況により問題はより深刻になっていきました。

  • インフラ、アプリケーション両軸で対応を進めるものの、改良版BOTとのいたちごっこが続く
  • さらに追い打ちとしてコロナにより外出できない時間が増えたことによるトラフィックの増加

カートリプレイス計画の始動

そんな中、小手先の改修に限界を感じはじめていたのと、弊社はリプレイス過渡期であり、カートシステムについてはオンプレミスで動いているため、抜本的な「スケールできるシステム」の必要性が高まっていました。

「リプレイスを推進するためには組織から変える」という弊社VPoE @sonots の舵取りのもと、カートシステムのリプレイスを推進するために新生カート決済チームが誕生しました。

この組織変更が無かったら通常業務もやりつつ片手間でリプレイスとなっていたため、リリースまで大分時間が掛かっていたはず。まず組織変更するというのは大正義!

カートリプレイス計画の策定

いざリプレイス計画を策定しようとすると、現状の課題解消のため「あれもこれも」となりがちです。弊社もまさにその状況となり、ロードマップを引くのに苦労しました。しかし、やりたいことが増えるほどリリーススパンも長くなりエンジニアのモチベーション低下にも繋がりやすく、ビッグバンリリースによるリスクも高まります。「リリーススパンは半年+α」とし、その中でできることをマイルストーンに落とし込んでロードマップを引いていきました。

そして、Phase1では主に過熱商品販売時のBOTによる過剰アクセスにも耐えうるシステムを目的に、以下ゴールを設定しました。

  • キューによるキャパシティコントロール可能なシステム
  • 目標値として過去のBOTアクセス数からサンプリングしたアクセス数×N倍に耐えうる値

欲張りすぎず、豪華すぎる仕様にしない。

課題を正しく理解する前にどうしても解決を急いでしまいがちなため、そこのバランスは特に意識していました。合わせて技術的に正しくてもサービスへの貢献度が低いなら成果として不十分なため、解決すべき課題を明確にしてスコープを小さくすることも意識しました。また、事前にリリーススパンを決めたことで、そこから逆算した形で落としどころを決めることができました。

アーキテクチャ構成

カートリプレイスPhase1のアーキテクチャ構成の概念図は以下となります。

architecture

Phase1では、Cartデータベースの前段にキューイングシステムを置くことでキャパシティコントロールを実現しています。キューイングサービス選定の際、AWSが提供しているサービスという点より以下2択となりました。

  • Amazon Simple Queue Service
  • Amazon Kinesis Data Streams(以下、KDS)

カート投入リクエスト数に耐えうる性能、及び可用性の観点からKDSを採用しましたが、詳細は以下の記事にまとまっているためご興味のある方はご覧ください。

techblog.zozo.com

いよいよ負荷試験

無事製造も終わり、いよいよ負荷試験です。キュー導入によるチューニング等は発生するだろうとある程度身構えてはいたものの、想定以上のトラブル続きでした。

プロダクション環境を使った負荷試験

今回のアーキテクチャでは、キューイングサービスにKDSを採用しています。もちろん単体での性能検証、負荷試験を実施しておりますが、カート機能はECサイトの中核を担う機能であり、リプレイスによりユーザビリティを損なってしまうのは本末転倒です。可能な限りUXを損なわずに最適なキューの並列度を探るには、やはりプロダクション環境での負荷試験が必須だと考えました。

プロダクション環境を使うにあたりできるだけサービスに影響が出ないような工夫もしており、詳細は以下の記事にまとまっているためご興味のある方はご覧ください。

techblog.zozo.com

負荷試験で見つかった課題

ここでは負荷試験で見つかった課題と、解決に向けたアプローチについていくつかご紹介させていただきます。

課題(1)KDSシャードの一部しか使われておらず想定した性能が出ない

シャード数増減によるStreamの付け替えや、デプロイ・スケールアウト等で新規にWorkerプロセスが起動するタイミングのレスポンス遅延が目立っており、時間経過とともに遅延が解消されていくという事象が発生しました。

概念図

kds_1

  • 原因

Kinesis Client Library(以下、KCL)のCustomMetricsを見ていくと、新規Workerアプリケーション起動時にシャードの一部しか使われていませんでした。さらにそこからブレークダウンしていくと、以下のKCLパラメータが起因していることが分かりました。

KCLパラメータ 概要 デフォルト値
maxLeasesForWorker アプリケーションの単一のインスタンスが受け入れるリースの最大数 INT_MAX
maxLeasesToStealAtOneTime アプリケーションが同時にスティールを試みるリースの最大数 1

①最初に立ち上がったWorkerが「maxLeasesForWorker:INT_MAX」により、INT最大数のシャードを紐づけようとしていた

②その後KCLの負荷分散処理により他Workerよりリースをスティールするが、「maxLeasesToStealAtOneTime:1」によりスティール最大数が1のため均等化にも時間が掛かっていた

kds_2

  • 解消に向けたアプローチ

①maxLeasesForWorkerの見直しにより、最初に起動したWorkerがINT_MAX分のシャード紐づけを行わなくなり、適切なシャード数のみ紐づけを行う

②maxLeasesToStealAtOneTimeの見直しにより、KCL負荷分散処理にて1度にスティールできる数が増えたことで高速な均等化が可能となった

kds_3

初期起動時に限定されていたのと時間経過とともに解消されていたためはまりました。が、Stream、WorkerがN:N構成、かつKCLパラメータがデフォルト値だったことにより発生した事象でした。負荷試験で頻繁にStreamを変更したり、Pod再起動等を行っていたため早期発見に繋がりました。

課題(2)Javaアプリケーション起動時のコールドスタート対応(JVM暖気)

上記により初期起動時のシャード数による遅延は解消されましたが、Javaアプリケーション起動時の重さも課題となり、コールドスタート対策が必要となりました。

  • JVMの暖機運転

JVMはJIT(Just In Time)コンパイラによるコンパイル方式のため、都度プログラム実行時にコンパイルが行われます。そのため、起動直後の初回プログラム実行時が特に重く、予めアプリケーションを実行してコンパイル済みにしておく暖機運転が求められます。

  • 解消に向けたアプローチ

今回作成したWorkerはKubernetes上で稼働しております。Kubernetes上で稼働するアプリケーションの暖機方式としてSidecar方式、postStart方式等がありますが、今回はpostStart方式を採用しています。

jvm_warmup

①KubernetesのpostStartを使ってコンテナ起動時に自エンドポイントへ暖機用パラメータでリクエストする

②後続APIでは暖機用パラメータのリクエストであれば後続処理を行わない

このようなフローにすることでユーザー影響を最小限にしつつ、コールドスタート問題を解消させています。

課題(3)エラーを返しているがカートに商品が入っている

テストの中で負荷を上げていくと、レスポンスでエラーを返しているが、実際にはカートに商品が入っているケースが発生しました。

  • 原因

サービス間でポーリングタイムアウトを検出しているものの、Workerがdequeueして後続のデータベースにリクエストを投げていたことが原因でした。

  • 解消に向けたアプローチ

課題解消のためには、各サービス間のタイムアウト値とKDSシャード並列数の組合せを考慮する必要がありました。タイムアウト値の延長やシャード並列数を下げることで本事象は解消できますが、単純な変更ではカート投入リクエストのレイテンシが高くなりユーザビリティに影響が出てしまいます。

ある程度の仮説を立てた上でテストシナリオを再構築し、しきい値の二分探索を行いながら最適なタイムアウト値やシャード並列数を探りました。そうすることでユーザビリティを担保しつつ、課題を解消できました。

いずれもマイクロサービス単体での負荷試験では検知しづらい課題でした。事前準備や夜間帯というコストを踏まえても、プロダクション環境を使うことで得られた物は大きかったです。エンジニアの理想で言えばプロダクションと同等の検証環境がベストですが、オンプレ環境だとコスト的に難しいのも実情です。

そしてリリースへ

度重なる負荷試験で全ての目標値をクリアし、万が一に備えてフラグによる切り戻し制御も準備した上で、カナリアリリースによるプロダクションリリースを行いました。

成果として、2021年元旦の過熱商品と同等のトラフィックのある商品で、大幅なエラー率削減を達成できました。

error_reduce1

また、同時にAPM(Datadog)によるダッシュボード化も実施しており、各種数値が可視化されたことでモニタリング精度向上、及び障害発生時の原因切り分けまでの時間短縮も実現できました。ダッシュボートはサービス単体のみではなく、APIからWorkerまでの一連のフローをリクエスト単位で見れるような工夫もしております。

以前は過熱商品販売の度にリアルタイム監視をしており現場がピリピリしていましたが、監視コストが下がることでサービスは勿論、組織文化にも良い影響を与えることができました。

良かった良かった・・・と言いたいところだが

リリース後も問題なく安定稼働しており一安心といったところでしたが、ある日リプレイスPhase1の目標値を大きく超える商品の販売により、再度エラーが多発する事態となってしまいました。

エラーの要因

  • 過熱商品の特性の変化(商品展開数の増加など)
  • リプレイスPhase1で想定していた以上のアクセス数
  • 想定以上のアクセス数により参照系処理が増加したことによるデータベースのラッチ競合

上記の対策として、アプリケーション、インフラ両軸で複合的な対策が求められますが、具体例として以下を実施しています。

  • キューのキャパシティコントロール最適化(シャード並列数の再精査)
  • 高負荷時もシステム全体に波及させず影響を最小化するistioを活用したサーキットブレーカーの導入
  • データベースのRead/Write分離による負荷軽減
  • Akamaiなど各種セキュリティソリューションの有効活用

過去のistioを活用したサーキットブレーカーの導入事例は以下の記事にまとまっているためご興味のある方はご覧ください。

techblog.zozo.com

網羅的に想定したつもりではあったものの、その想定を大きく超える事象により障害となってしまいとても悔しい結果となりました。「どこまでやるか」はシステム永遠の課題でありスケジュール・コスト等とのトレードオフです。しかし障害対応によるブラッシュアップによって、より盤石なシステム・サービスになると思っています。

まとめ

【結論】やってよかった

  • 最初に組織変更をしたことで、その後のリプレイスを加速できた。逆に言うとそれぐらいの覚悟は必要
  • 問題を正しく理解する前に解決することを急がない
    • 分かっていてもはまりがちなため、何度も自分に言い聞かせた。超大事!
  • 巨人の肩に乗る
    • 先人達のベストプラクティスには理由がある
  • マイクロサービスは特に負荷試験に時間を割く
    • サービス単体では見つけられなかった課題も、プロダクション環境に組み込んで試験することでリリース前に多くの課題を吸収できた
  • 良いシステムはブラッシュアップによって生まれるもの
    • リリース後も常に問題意識を持つことが大事
  • 将来を見据えた設計は重要だが、先を見過ぎない
    • 先を見過ぎるとあれもこれもとなり結果汎用的になりスピード感も落ちてしまう。大事なのは一点突破できる機能
  • 監視コストが下がることで組織文化にも良い影響を与えることができた
  • 一方、BOTによる過剰アクセス対策については日々ブラッシュアップが求められるため、アプリケーション、インフラ両軸で継続的に取り組んでいく必要がある

最後に

本記事はカートリプレイスPhase1の振り返りとなりますが、カート決済サービスのリプレイスは始まったばかりです。

  • セールやキャンペーンによる突発的な注文フロースパイク
  • 決済サービスのモノリスからの脱却、マイクロサービス化
  • 今後のZOZOTOWN成長にも耐えうる、正確性、信頼性、安全性を重視したスケールできるカート決済サービス
  • モニタリングの洗練化
  • スピード感、モダンなメインストリーム技術の導入、etc

上記はいくつかの例ですが、まだまだやりたいことが山積みの状態です。このような課題の解消に向けて、一緒に進めていく仲間を募集しています。ご興味のある方は、以下のリンクから是非ご応募ください。

hrmos.co

hrmos.co

また、カジュアル面談も随時実施中です。是非ご応募ください。

hrmos.co

カテゴリー