UPSIDER Tech Blog

仮説駆動で取り組む負荷試験計画

こんにちは。決済/カード発行基盤チームのKinshoです。法人カード基盤の外部提供の挑戦についての技術ブログ バトンリレー5日目の記事です。

本記事では、法人カード基盤の外部提供に向けた負荷試験において、実施前の計画フェーズの設計思想と実践を紹介します。

はじめに — なぜ "計画" の話をするのか

負荷試験の記事はたくさんあります。その多くは「K6 でこう書いた」「このツールが便利だった」というツールの使い方の話です。それはそれで有用なのですが、実際にやってみて痛感したのは、

負荷試験の成否は、実施前の "計画" でほぼ決まる

ということでした。

決済基盤は、止まっても遅くなっても困るシステムです。だからこそ「とりあえず負荷をかけてみて、CPU が張り付いたら考える」では遅いです。何を不安に思っていて、何をどう観測すれば "大丈夫" と言えるのか を先に言語化しておかないと、得られた数字をどう解釈していいか分からなくなります。

この記事では、私たちが負荷試験の前に取り組んだ以下の "計画" を順に紹介します。

1. そもそも、どれだけの負荷を目標にするのか — 前提を定量的に疑う

仮説を立てるより前に、もっと根本的な問いがありました。「そもそも、どれくらいの負荷を目標にすればいいのか?」 です。

きっかけは、上長からの「サービス展開によって、決済頻度が〇〇倍に増えるかもしれない」という見立てでした。有用な情報ではあるものの、同時に本当にその規模まで増えるのかは不明確な状態でした。

ここで「上長が言うならその数字で試験しよう」と進めることもできました。しかし、この数値が間違っていると、その上に積み上げた仮説も、成功条件も、実装も、すべてが間違った前提の上で動くことになります。過大に見積もれば、ありもしない負荷のために過剰なリソースと工数を費やしますし、過小に見積もれば、いざ本番環境に上げた際に足をすくわれます。

そこで、前提そのものを疑い、自分で数字を作ることにしました。具体的には、

  • 毎月の顧客獲得計画(どのくらいのペースで事業者・カードが増えるか)
  • 1顧客あたりの平均的な決済頻度
  • ピーク時の集中度

等といった一次情報を突き合わせ、「将来の決済頻度が現状の何倍になるか」をボトムアップで算出しました。

その結果分かったのは、体感ベースの見立てと、一次情報から積み上げた数字との間には乖離がある ということでした。

この作業を通じて改めて感じたのは、

与えられた前提をそのまま受け取らず、一次情報から定量的に数字を出し直すことが、すべての出発点になる

ということです。負荷試験は「正しい目標値」があって初めて意味を持ちます。

2. 負荷試験を "設計" する — 3 つの仮説を立てる

いきなり負荷をかけない

負荷試験を任されたとき、最初にやりたくなるのは「とにかく目標の負荷を流してみる」ことです。しかしそれをやると、たいてい「なんか動いてるっぽい」か「なんか落ちた」のどちらかしか分かりません。そこで私たちは、負荷をかける前に「このシステムは、どこから壊れそうか?」という仮説を 3 つ立てました。

立てた 3 つの仮説

仮説 懸念していること 観測対象
① リソース仮説 各マイクロサービスの CPU / メモリがボトルネックになるのでは Pod の CPU / メモリ使用率、オートスケールの挙動
② データベース仮説 ロック競合やコネクションプールの枯渇が起きるのでは ロック待ち時間、コネクション数、クエリレイテンシ
③ タイムアウト仮説 サービスメッシュの timeout / retry が不適切でカスケード障害になるのでは サービス間レイテンシ、リトライ回数、エラー率

仮説 2(データベース仮説) について補足すると、決済処理は「残高を引いて、履歴を書いて……」と、1 リクエストの裏で複数のテーブルを触ります。負荷が上がると、同じレコードを取り合うロック競合や、コネクションプールの枯渇が現実的なリスクになります。

仮説 3(タイムアウト仮説) は、決済基盤のように多段のマイクロサービスが連なる構成で特に怖いものです。あるサービスが少し遅くなる → 呼び出し元がタイムアウトしてリトライする → リトライがさらに負荷を増やす、という悪循環が起きえます。

仮説を立てると "見るべきもの" が決まる

この 3 つの仮説を立てたことで、負荷試験中に どのコンポーネント・メトリクスを注視すれば良いか が明確になりました。仮説なので外れても問題ないです。「データベースが詰まると思ったら、実はサービスメッシュのほうが先に悲鳴を上げた」という結果が出れば、それはそれで良い発見になります。

3. 成否を "先に" 決める — 成功条件 とベースライン計測

「成功とは何か」を試験前に定義する

仮説の次に決めたのは 「この負荷試験は、どうなったら成功なのか」 です。これを試験後に決めると、都合のいい解釈が混ざります。なので先に成功条件を決めました。私たちが置いた成功条件はシンプルで、決済入口サービスのエラー率と p99 レイテンシが、ベースラインと同等であることとしました。

ここでポイントが 2 つあります。

ポイント 1: 「絶対値」ではなく「ベースラインとの乖離」で測る

最初は「p99 レイテンシ ◯◯ms 以内」のように絶対値で基準を置こうとしました。しかし、今回負荷試験により実現したいのは決済基盤の改善ではなくて、決済頻度が増えても現在と変わらない決済処理ができる状態かどうかということです。

そこで方針を変え、「通常時と比べて悪化していないこと」 を成功条件にしました。基準を "自分自身の平常時" に置くことで、「負荷をかけたことによる劣化があったか否か」という、本当に知りたい問いに直接答えられるようになります。

ポイント 2: ベースライン計測フェーズを別に設ける

「平常時と比べる」と決めたので、まず平常時を測らないといけません。そこで目標負荷の前に、低負荷のベースライン計測フェーズ を必ず先に回す設計にしました。さらに、ベースライン・目標負荷のそれぞれを Short Run(短時間)と Long Run(長時間)に分けました。

[Phase 0] 低負荷ベースライン / Short Run  ← 平常時を短時間で取る
     ↓
[Phase 1] 低負荷ベースライン / Long Run   ← 平常時を継続負荷でも取る
     ↓
[Phase 2] 目標負荷 / Short Run            ← 短時間で異常がないか
     ↓
[Phase 3] 目標負荷 / Long Run             ← 継続負荷で劣化しないか

この「ベースライン → Short → Long」という段階を踏むことで、各フェーズに役割を持たせられます。

  • Phase 0・1: 比較の基準(短時間・継続時、それぞれの平常値)が手に入る
  • Phase 2: 目標負荷でも短時間で明らかな破綻がないかを低コストで確認できる(Phase 0 と比較)
  • Phase 3: じわじわ来るリーク系の問題(メモリ・コネクション)を炙り出せる(Phase 1 と比較)

4. 実行フェーズの鉄則 — 変更は1つずつ、比較は apples to apples で

実行フェーズでは、立てた仮説を1つずつ検証していきました。このとき徹底したのが、毎回の計測が必ず "apples to apples"(同一条件どうし)の比較になるようにする ことです。

なぜなら、一度に複数の変数を動かすと、得られた差分が「どの変更による効果なのか」を切り分けられなくなる からです。たとえば「DB のコネクションプールを増やす」と「Pod の CPU を増やす」を同時にやって改善したとしても、効いたのがどちらか(あるいは両方か)が分からなければ、その知見は次に活かせません。

そこで、

  • 変更は常に1つだけにし、それ以外の条件(目標 RPS・実行時間・対象データ・実行環境・時間帯)は完全に固定する
  • 比較対象のベースラインは、できるだけ近いタイミングで取り直す

を原則にしました。特に共有環境では、他チームの負荷や時間帯によって素の性能が揺れます。「先週取ったベースライン」と比べると、その揺れを改善効果と取り違えてしまう。だからこそ、比較する2つは可能な限り同じ土俵に乗せる必要がありました。

5. ツール選定と実行設計の勘所

ここまでが「計画」の話です。最後に、その計画をどう実行に落とし込んだか、要点だけ触れておきます。

なぜ K6 / constant-arrival-rate を選んだのか

負荷試験ツールには K6 を選びました。決め手は、今回の目的が 「目標 RPS をきっちり維持して、その状態を観測したい」 ことだったからです。

負荷のかけ方(executor)には、仮想ユーザー(VU)数を固定する方式や、徐々に負荷を上げていく方式など複数あります。そのなかで K6 の constant-arrival-rate(単位時間あたりのリクエスト数を直接指定する方式)を採用しました。「目標の RPS を維持したい」という要件に対して、VU 数ではなく RPS そのものを指定できるこの方式が、いちばん素直に目的と噛み合っていたからです。

設定は以下のとおりです(実際のコードから抜粋・簡略化。具体的な数値は環境変数で外出ししています)。

// config.js — シナリオは環境変数で組み立てる
const scenarios = {};

// ベースライン(低負荷)
if (ENABLE_BASELINE) {
  scenarios.baseline = {
    executor: 'constant-arrival-rate',
    rate: BASELINE_RATE,       // 単位時間あたりのリクエスト数
    timeUnit: '1s',
    duration: BASELINE_DURATION,
    preAllocatedVUs: BASELINE_PRE_ALLOCATED_VUS,
    maxVUs: BASELINE_MAX_VUS,
  };
}

// 目標負荷
if (ENABLE_TARGET) {
  scenarios.target = {
    executor: 'constant-arrival-rate',
    rate: TARGET_RATE,
    timeUnit: '1s',
    duration: TARGET_DURATION,
    preAllocatedVUs: TARGET_PRE_ALLOCATED_VUS,
    maxVUs: TARGET_MAX_VUS,
  };
}

本番相当の経路を、共有環境で安全に叩く

実行環境にもひと工夫しました。負荷試験はローカルからではなく、本番相当の Staging 環境の内部から実行しています。クラスタ内部から決済の入口サービスを直接叩くことで、外形的なネットワークの影響を排除し、サービス本来の性能を測れるようにしました。

最後に

計画駆動でやってよかったこと

計画なしで始めていたら、目標値が曖昧なまま何度も試験をやり直し、出てきた数字の解釈で迷い、「この結果、信じていいんだっけ?」と立ち止まっていたはずです。先に前提・仮説・成功条件を固めたことで、試験そのものは迷いなく一直線に進められました。

その結果、発見した課題に対してひとつずつ向き合うことができました。

法人カード基盤の外部提供の挑戦 技術ブログ バトンリレー

各エンジニア組織が法人カード基盤の外部提供の挑戦の中で得た知見を順次公開していきます!技術、開発設計や思想、Tipsなど、幅広くお伝えします。

▼公開予定表 (各記事公開後、リンク集になります)

タイトル チーム 執筆者
UPSIDERのカード基盤を「外部提供できる形」にするまで——マイクロサービス化とマルチブランドアーキテクチャ 法人カード基盤の外部提供 全体構想 Mitsui
Spanner Change Streams を自前で読む ― 決済データを欠損なく取り込む カード管理基盤チーム(Solaris) Ohsako
1年動き続けるワークフローを壊さずにデプロイする ― Temporal Worker Versioning 導入記 請求基盤チーム(Hermes) nishiwaki
GCP Error Reportingで「最低限の異常検知」を低コストに実現する 法人審査基盤チーム(Hestia) Mitomi
複数の銀行データソースを1つに束ねる ― マルチブランド法人カードを支える銀行連携基盤 銀行連携基盤 terry
仮説駆動で取り組む負荷試験計画 processor Kinsho
AIが実装しレビューする時代に、プロダクトエンジニアは何をするのか お客様向け管理画面チーム(Iris) Akari
法人カード基盤の外部提供にも耐えうる不正利用対策基盤の構築 Anti-fraudチーム Ryutaro
法人カード基盤の外部提供という挑戦 決済基盤を「他社にまるごと使ってもらう」QAの話 QAチーム Naoki

なぜUPSIDERが法人カード基盤の外部提供に取り組むのか、その背景を綴っている記事も合わせてぜひご覧ください。

note.com

We Are Hiring!!

UPSIDERでは現在積極採用をしています。 ぜひお気軽にご応募ください。 herp.careers

herp.careers

UPSIDER Engineering Deckはこちら📣

speakerdeck.com