はじめに
こんにちは。DMM.com プラットフォーム開発本部 第三開発部 基盤開発グループの登石と野田です。
DMMプラットフォーム開発本部では、会員機能や決済機能などの様々な機能をAPIとして事業部側へ提供しています。我々のチームでは、これらAPIの入口となるAPIゲートウェイ"Gen2-GW"を保守・運用しています。
数カ月の間、Gen2-GWの負荷試験を実施したので、本記事ではその時に実施したこと、工夫したこと、実施により発見できた問題について紹介していきます。
負荷試験実施の背景
Gen2-GWでは日々キャンペーンの実施や新たなエンドポイントの追加が続いており、それに伴いGen2-GWに要求される性能が更新されていきます。また、Gen2-GW自体も継続的な改修が行われており、その限界性能が刻々と変化しています。
しかしながら限界性能の具体的な数値を常に把握することはできておらず、現状では要求される性能が満たされているか否かを本番環境のパフォーマンスデータから判断する状況であり、未知の負荷シナリオに直面した際に適切な評価を下すことは難しいという問題に直面していました。また、Gen2-GWは複数のコンポーネントから構成されており(メインとなるバックエンドサービス、認可を行うバックエンドサービス、オンプレミス環境専用のプロキシサーバなど)、個別のコンポーネントに対する負荷試験だけでは全体像を把握するには不充分でした。
これらの課題を解決するために、Gen2-GW全体に対して本番環境と同等の負荷をかける試験を実施することを決定しました。この負荷試験を通じて、Gen2-GWの真の限界性能を定量的に把握し、将来に向けたインフラのスケーリング計画やリスクマネジメント戦略を策定するための実証データを得ることができると期待しました。
環境
負荷試験クライアントには、PFの多くで利用されている負荷試験環境を利用しました。負荷試験環境についての詳細は、 https://inside.dmm.com/articles/dmm-go-4-load-testing/#content5 で紹介されています。詳しくはそちらを参照ください。
今回のGen2-GW負荷試験においては、Go言語でシナリオが記述可能なこと、k8sベースによるスケーラビリティなどがうまく機能したと思います。素早く負荷試験の実施まで辿り着くことができ、高RPSの負荷試験利用にも耐えてくれました。
シナリオ
負荷試験では、目的に従って負荷をかけるためのシナリオが必要になります。
今回の負荷試験の目的は、Gen2-GWが本番環境でどの程度の負荷に耐えられるか検証することです。今回のシナリオでは本番のリクエスト傾向を再現することが要求されます。
シナリオの作成方法として以下の二つの方法が考えられます。
- 本番環境のトラフィックを記録して負荷試験用に利用
- ログ・メトリクスなどのデータや知識をもとにシナリオを定義する
今回はプロジェクトのスケジュールの都合もあったので、実現性や検証の手間が少なそうな後者を選択しました。
シナリオの作り方
Gen2-GWのシナリオの設計について具体的に説明します。
通常のAPIサーバーでは、一般的にはAPIごとにRPSを算出して負荷シナリオに反映すると思います。しかしGen2-GWではこの方法は困難です。Gen2-GWにはPFの多様なAPIが登録されており、現状で約1000個のAPIがあります。そのため通常のAPIサーバーのように、エンドポイントごとに割合を決めたりユーザーストーリをベースにシナリオを作成したりすることは難しいです。
Gen2-GWにはエンドポイントごとに設定できるいくつかの機能があり、パフォーマンスに影響を与えます。さらに、リクエストやレスポンスの一部のフィールドがパフォーマンスに大きな影響を与えることが過去の事例から分かっています。
Gen2-GWは比較的シンプルで機能数が多くないため、考慮すべき機能やフィールドは洗い出すことが可能です。よって今回は、APIではなく機能と、リクエスト・レスポンスの性質を軸にシナリオを作成することにしました。
機能を軸にシナリオを作成する
具体的には、洗い出した機能やリクエスト・レスポンスの性質ごとに割合を算出し、それをシナリオとすることにしました。
割合の算出にはアクセスログを活用しました。Gen2-GWはアクセスログをBigQuqeryに保存しており、一定期間のデータが分析可能になっています。
例えば、Gen2-GWにはレスポンスの一部フィールドを変換する機能があります。APIごとに使う・使わないが決まっているので、この割合をアクセスログから算出できます。これまでの運用経験から、この機能が時間のかかる重い処理であることがわかっていたため、シナリオで考慮することにしました。
その他機能についても、経験則からコストのかかるものを割り出し、分析して数値化することでシナリオを作成しました。
バックエンドのレイテンシを再現する
Gen2-GWにとって、バックエンドのレイテンシやレスポンスのサイズなどもパフォーマンスに影響を与える重要な要素です。ここではレイテンシを具体例に、再現方法を紹介します。
負荷試験用にバックエンドサーバーを用意していたので、そこにsleep処理を入れることでレイテンシを再現しました。
sleepする時間に関しては、本番のレイテンシの分布を参考にしました。いくつかのパーセンタイルごとにsleep時間を設定し、本番の分布に近くなるようにしました。
下記のようなコードを実装していて、リクエストごとに実行されるようになっています。
func generateBackendLatency() int { // 小数点切り上げ // 分布の区切りは適当 const ( p10 = 10 p50 = 33 p75 = 44 p90 = 72 p95 = 96 p99 = 166 p99_9 = 2470 p99_99 = 6050 max = 123000 ) // 比較をわかりやすくするため100倍 switch i := rand.Float64() * float64(100); { case i < 50: return p50 case 50 <= i && i < 75: return p75 case 75 <= i && i < 90: return p90 case 90 <= i && i < 99: return p99 case 99 <= i && i < 99.9: return p99_9 case 99.9 <= i && i < 99.99: return p99_99 default: return max } }
Gen2-GWから見たアップストリームのレイテンシもアクセスログに記録しているので、レイテンシの統計もアクセスログから算出しています。
以下は特定期間のアップストリームのレイテンシをDatadogで可視化したものです。
単位はmsで、例えば99パーセンタイルで166msであることがわかります。
負荷試験中に遭遇した問題
負荷試験を実施する中で、いくつかのパフォーマンスに関する問題に直面しました。特に興味深かった「CPUスロットリングの問題」と「ネットワーク帯域の問題」について紹介します。
CPUスロットリングによるレイテンシ悪化
負荷試験の過程で、CPUスロットリングの問題に当たりました。
まずはどのような現象が発生していたかについて見ていきます。高RPSの負荷試験の際に、レイテンシの悪化を観測しました。 (横軸が時間、縦軸がレイテンシです。また、オレンジの線が比較対象の低RPS時のもの、 青の線が高RPS時のものです。)
グラフから、周期的にレイテンシが悪化する傾向にあることがわかりました。
いくつかのメトリクスを調査した結果、CPUスロットリングのメトリクスとレイテンシの変動について、周期の一致が明らかになりました。 さらに調査を進めると、CPUスロットリングが悪化するタイミングとGCのタイミングが一致することもわかりました。 GCのタイミングでCPUリソースが足りないため、CPUスロットリングが起きていると考えました。
HPAによる対応
CPUスロットリングを抑えレイテンシの悪化を改善するために、Gen2-GWのリソース量を調整する必要があります。
まずはHPAを調整することで対策できないか検討しました。Gen2-GWはk8sクラスター上で動作しており、HPAによるオートスケーリングの仕組みを導入しています。平均CPU使用率を閾値に設定しており、普段はキャンペーンなどでRPSが増えた際に機能し、Gen2-GWのpodがスケールします。
しかし今回の場合は、常にCPUリソースが不足しているわけではありません。HPAに評価される際には均されてしまうので、HPAで今回の問題に対応するのは難しいと判断しました。
また、GCによるCPU使用率のスパイクは数分間隔で上下するので、podのスケール速度を考えてもHPAは適しませんでした。
Resource Limitsによる対応
結果的に今回はCPUのResourece Limitsを増やすことによって、問題に対処することにしました。コンテナはLimitsの設定によって、使用できるリソースが制限されています。コンテナがLimitsを超えてリソースを消費しようとすると、CPUスロットリングが発生します。Limitsを増やすことでCPUスロットリングを抑制できます。
以下は今回実施した設定変更の一部です。
最終的な設定値については、設定値を変更しながら負荷試験を複数回実施し、レイテンシが許容範囲になる設定値を測定しました。
Limitsの調整によって、CPUスロットリングを抑制し、パフォーマンスの最適化を行うことができました。
ネットワーク帯域制限
試験における負荷がある程度大きくなったあたりで、直接原因のわからない問題が出てくるようになりました。結論としてはネットワークの帯域の制限が原因でした。あたかもそのネットワークを利用するアプリケーションのパフォーマンスが悪化したように見えるので、直接の原因であるネットワークの帯域制限に辿り着くまでに時間がかかりました。
この問題はGen2-GWを構成するオンプレミス環境で動作しているコンポーネントでの通信部分で発生しました。負荷としては25000RPSから30000RPSへ増加させて試験を行った際にこの事象が発生しました。
25000RPS時と30000RPS時のそれぞれで負荷試験を行った際のGen2-GW後段のバックエンドのエラー数を示すグラフは以下となっています。25000RPS時と比較して30000RPS時はバックエンドのエラー数が増加していることが確認できます。
同時にコンポーネントに関連するGen2-GWの指標である認可チェックのOK/NGの数を示すグラフも以下となっています。25000RPS時と比較して30000RPS時では認可NGとなるリクエストの数が増えていることが、グラフのある時間帯に対する赤い系列の割合が多いことから確認できます。
このときコンポーネント側で何かが起きていることを察知し、メトリクスを確認したところ、レイテンシが増加していることを以下のグラフから確認できました。
この段階ではコンポーネント側アプリケーションやDB自体にボトルネックがあると考えて、それらのメトリクスを調査していましたが、最終的にネットワークに何かあるのではないかと考えるようになりました。そこでコンポーネントのアプリケーションからDBへの送信トラフィック量を以下のグラフから確認したところ、100Mbpsで頭打ちになっているのを見つけました。 14:20 - 14:35と15:41 - 15:56の2つの期間で30000RPSの負荷試験を行っており、時間としても同時刻になっています。ただこのグラフからだけではたまたま100Mbpsの通信が行われているだけの可能性があるので、実際にネットワークに帯域制限がかけられているかなどを調査する必要はあります。実際に調査したところ、100Mpbsの制限がかかっていたので、制限を緩和することにより解決しました。
この問題の発見が遅れた理由はネットワークスループットのメトリクスと負荷試験でみていたメトリクスの監視ツールが異なることです。ネットワークスループットのメトリクスは普段から見る場所に含まれておらず、普段使いすることもないため、わざわざ調べにいく必要がありました。このことからわかることとして、普段からメトリクスに触れること、触れられるような状況を作っておくことが重要であると教訓を得ました。
まとめ
本稿では、DMM内部で利用されているGen2-GWについての負荷試験について解説しました。具体的には、負荷試験の準備段階でのシナリオ作成方法や実施時に遭遇した問題と、その解決策について紹介しました。あくまで一例ではありますが、負荷試験を実施する際の参考になれば幸いです。
この負荷試験は、Gen2-GWの現状の性能を評価し、とあるプロジェクトへの対応能力を確認する目的で行われました。したがって、この試験は一回限りのプロジェクトとなっています。背景でも触れたように、Gen2-GWの要求性能はエンドポイントの追加やGen2-GW自体の継続的な改修により日々変化しています。そのため、これらの性能を維持するためにも、定期的な負荷試験を行い、性能を担保できているかを確認することが今後の重要な課題となります。
プラットフォーム開発本部では一緒に働く仲間を募集しています!
speakerdeck.com