こんにちは!株式会社リーディングマークでSREをしている武藤です。
みなさんは、普段Webサービスを使っていて「パフォーマンス」を意識したことはありますか?
あるという方も、「このサービスのパフォーマンス改善ってどうやって実現しているのだろう?」という視点を持つことはそう多くないかと思います。まして、その裏側を明確に想像することは熟練のエンジニアでも簡単ではないかもしれません。
さて、弊社が展開しているサービスの「ミキワメAI」でも、2026年度の初頭に大規模なパフォーマンス改善を行いました。ミキワメAIは、採用から入社後の組織づくりまでを支援するHR Tech SaaSで、累計200万人以上に利用されています。今回は、数万名規模のエンタープライズ導入に向けて行ったこの取り組みの軌跡についてご紹介します。
特に中盤の「k6のシナリオ設計を間違えると、完全に間違った計測結果が出る」という話は、これから負荷検証をやる方にはきっと役に立つはずです。RailsもRubyもあまり書けなかった新卒1ヶ月目の私が、どうやってこのプロジェクトの一角を担えるようになったのか、当時の思考もセットでお伝えします。
自己紹介
さて、自己紹介ですが、私はリーディングマークに新卒で入社し、サービス開発部SREチームに所属しています。
もともと、学生時代はプログラミングコンテストに2回出場し、フロントエンド開発やバックエンド開発、デスクトップアプリ開発など色々やってきた「開発大好き人間」だったのですが、さすがに運用は未経験で「やってみたいな」と思っていました。
そこで、配属面談のときダメ元で「SREチームに入りたいのですが、希望してOKですか…?」と言ってみたら、「いいよ!」(!?)と返事が来て、新卒なのにSREをやらせていただけることになりました。
1. 背景
遡るは2026年の5月、弊社では全社方針として「エンタープライズ対応」が打ち出されました。というのも、数ヶ月後に全社導入が決まったエンタープライズのお客様が従業員数が数万名を超える大企業だったのです。
そして、リーディングマークのエンジニア組織であるサービス開発部でも「数万名導入のためのパフォーマンス改善」が最優先課題となりました。
ミキワメAIには、従業員のコンディションを定期的に可視化する「ウェルビーイングサーベイ」という機能があるのですが、サーベイ結果は部署単位で閲覧・分析される仕様になっています。また、管理アカウントの閲覧権限も各企業ごとの部署の階層構造から動的に算出されるようになっていました。
ところが、当時のメイン層のお客様は従業員100〜5000名、部署数が数百という規模だったので、数万名+数千部署は完全に未知の領域。パフォーマンス問題が発生することが予想されました。
サービス開発部の方針として、パフォーマンス改善はSREチームが主導して行うことが決定されたのですが、2026年4月入社だった私もSREチームの一員としてそこに携わることになりました。
2. プロジェクトのフェーズ分解
EM(エンジニアリングマネージャー)が立てた計画のもと、プロジェクトは大きく次のフェーズに分解されました。
| フェーズ | やること |
|---|---|
| 準備 | 想定値の確定、APM(アプリケーション性能監視)整備、検証環境・テストデータの用意 |
| 負荷検証・レポート化 | 「数万名でどれくらい詰まるか」を機能別にレポート化 |
| PdMレビュー | レポートを見てPdM(プロダクトマネージャー)が改修判断 |
| 改修 | 特定したボトルネックを各開発チームが改修 |
| 再検証 | 改修後に実際に負荷をかけて効果を測定 |
| 最終調整 | 残課題の対応と本番投入可否の判断 |
ポイントは、「エンジニアが直したい所を直す」のではなく、「開発側が根拠付きの想定値をレポートとして提示し、PdMがビジネス判断として直す/直さないを決め、担当ドメインの開発チームにアサインされる」という判断のフローを最初に設計したことです。
もうひとつポイントがあり、計画を立てる前にClaudeでコードベースを静的解析し、ボトルネック候補をファイル・行番号単位で裏取りしていました。たとえば「CSVエクスポートはN+1で数十万クエリが発行され、数万名規模ではタイムアウト確定」といったレベルまで、実測の前に当たりがついていました。レポートに書く推定値にも「静的解析推定 / APM実測 / ベンチマーク実測」のどれなのか根拠タグを必須にするルールが敷かれ、PdMが判断に使える精度が担保されていました。
こうした計画のもと、私は同時アクセス系の負荷検証全般を担当することになりました。今回は、最も苦戦した「サーベイ回答画面」を例に挙げて説明していきます!
3. k6計測とレポート化
ウェルビーイングサーベイは、社員の心の状態を定期的に把握・可視化するためのプロダクトです。配信メールが届くと社員は回答画面を開き、全設問に回答したあと、「深掘り質問」に答えます。数万名に一斉配信すれば、メール開封直後に数千人が同時に回答しに来るので、その負荷を検証するのが私のミッションでした。
(3-5で後述しますが、この時「同時に」という言葉の定義についてよく考えておくべきでした…)
負荷検証ツールには k6 を採用しました。JavaScriptでシナリオを書ける手軽さと、負荷のかけ方(後述するexecutor)を細かく制御できるのが決め手です。
結論から言うと、私はこの検証で シナリオを4回完全に作り直しました。そして、その過程こそがこの記事で一番伝えたいことです。k6は「とりあえず動くスクリプト」が簡単に書けてしまう一方、シナリオ設計を間違えると、本物っぽく見える完全に間違った計測結果が出ます。
3-1. ドメイン知識のキャッチアップ
そもそも、負荷検証をするには「特定の状態の社員アカウントを数千件用意する」「特定モデルを数千件生成する」「特定画面の処理を追う」といった下準備が必要です。しかし入社1ヶ月目だった私はRubyもRailsもそれほど触ったことがなく、プロダクト自体へのドメイン知識も浅かったため、業務は管理画面の操作手順をメモして勘で進めていました。
当然すぐ限界が来ます。そこで「時間の投資」と割り切り、自作のRailsバッチを書けるようになることを目標に、丸1日かけて主要なActiveRecordモデルを中心にリポジトリ全体をキャッチアップし、あわせてミキワメAIの機能を一通りブラウザから触ってみました。
ぼんやりとでも全体像を掴めたことは、結果的にこの後のあらゆる判断の土台になりました。振り返ると、この丸1日が一番リターンの大きい「時間の投資」になったと感じています。
3-2. 最初に書いたk6スクリプト
さて、満を持して負荷検証に臨んだ私の最初のk6スクリプトはこうでした。仮想ユーザー(VU)を2シナリオ合計300まで増やして回し続けるという、チュートリアルに書いてあるような定番コードです。一見良さげですね。
// ramping-vus で VU を増やしながら回し続ける survey_new: { executor: 'ramping-vus', exec: 'surveyNewFlow', // ログイン → GET 回答画面 → POST 回答 startVUs: 0, stages: [ { duration: '1m', target: 150 }, { duration: '5m', target: 150 }, { duration: '30s', target: 0 }, ], },
実行結果がこちらです(抜粋)。
✗ 'rate<0.01' rate=23.41% ← リクエスト失敗率 23%
✗ 'p(95)<2000' p(95)=1m0s ← p95 が 60秒(タイムアウト)
✗ login status is 200
↳ 46% — ✓ 826 / ✗ 960 ← ログイン成功率 46%!?
✗ surveys/new status is 200
↳ 95% — ✓ 390 / ✗ 17 ← 回答画面自体は 95% 通っている
p95(パーセンタイル95)とは? データを小さい順に並べた際に、全体の95%のデータがある値以下に収まるとき、その値をp95と言います。上記の例だと、リクエスト時間のp95が60秒なので「95%のリクエストは60秒以内に完了したが、残り5%はタイムアウト(60秒)に達した」と読み取れます。
上記結果を見ると、失敗率23%、p95は60秒と出ています。この結果を改修レポートにまとめて終わろうと思ったのですが、よくログを眺めると下記のような行がありました。
WARN[0074] Request Failed error="Post \"https://<検証環境>/<ログインAPI>\": request timeout" ERRO[0074] GoError: login failed for k6_employee_69@example.com: status=0 body=null
どうやらタイムアウトしているのは、測りたかった回答画面ではなく ログインAPI のようです。実行結果を見直すと、回答画面のGET自体は95%通っています。つまりこの数字は「サーベイ回答画面の性能」ではなく、全VUが毎回ログインから始めるシナリオを組んだせいで、ログイン処理と測りたかった画面を同時に計測した数字でした。
k6自体についてのキャッチアップが疎かなまま進めてしまうと、測りたいものと、シナリオが再現しているものがズレてしまうという学びを得ました。
3-3. setup()でログインを計測から分離する
k6の公式ドキュメントを読み直すと、こういう前処理は setup() に書くのが推奨されていました。setup() は本計測が始まる前に1回だけ実行される関数で、ここでログインを済ませておけば、VUが踏む計測経路からログインを外せます。
// 計測前に全アカウントを並列ログインさせ、cookie を回収しておく export function setup() { const sessions = {}; for (const chunk of chunks(userIndices, CONCURRENCY)) { Object.assign(sessions, signInBatch(chunk)); // http.batch で並列ログイン } return { sessions }; // 本計測の各 VU に cookie が配られる }
これで本計測のVUはログイン済みcookieを使い回し、回答画面のGET/POSTだけを叩くようになりました。本計測の経路とログインを分離し、ようやく「サーベイ回答画面の性能」を見ることができます!
setup()についての補足 k6は
setup()内のHTTPリクエストも末尾のhttp_reqs/http_req_durationに含めます。本記事のbefore/afterの数値にも事前ログイン分が含まれています。厳密に分けたい場合はthresholdでタグを絞り込むことができます(例:http_req_duration{scenario:survey_full_flow}。setup()由来のリクエストにはscenarioタグが付きません)。
3-4. その負荷、現実に存在しますか? executorという概念
しかし、まだまだ実態に即した数字は取れていません。
ここで、k6の executor(シナリオの実行モデル)という概念について見ていきましょう。例えば、これまで使っていた ramping-vus は 各VUが期間中ずっと同じ画面をループし続ける executorです。つまり同じ社員が回答画面を何十回も開き続ける負荷になっている。一方で、現実のサーベイ回答は「1人がちょうど1回、回答して離脱する」フローです。
…嫌な予感がしますね。実行結果を見返すと、やはり state_redirects が5万件近く出ていました。
state_redirects................: 56156 ← 全リクエストの1割近くがリダイレクト
…そう、これまで私がやっていた計測では、2周目に入ったVUが「回答済み」画面を叩いていたので、さっき出した計測結果は何の意味もない数値でした😭。
ここで私はようやく、k6のexecutorに正面から向き合いました。k6では「どんな負荷をかけるか」をexecutorで宣言します。そして、executorの選択そのものが「どんな現実を再現するか」の宣言なのです。
| executor | かかる負荷 | 再現する現実の例 |
|---|---|---|
ramping-vus |
N人が期間中ループし続ける | 同じ画面を延々と操作し続けるユーザー群 |
per-vu-iterations |
N人が各1回だけ実行して離脱(iterations: 1 の場合) |
一斉メール直後、全員が1回ずつ回答 |
constant-arrival-rate |
毎秒一定件数を、完了を待たず投入 | 「秒間15リクエスト」という到着レート |
(他のexecutorは k6公式ドキュメント を参照)
「一斉配信メール直後に、ユニークな社員が1回ずつ回答しに来る」を再現するなら per-vu-iterations(iterations: 1)です。これに乗り換えれば良さそうですね!
// 全員が「ちょうど1回」回答して離脱するフローを再現 survey_new: { executor: 'per-vu-iterations', vus: PRELOGIN_COUNT, // 事前ログイン済みの人数 iterations: 1, // 1人1回。「回答済みなのに再回答」が原理的に起きない },
もし私がプロダクトのドメイン知識をキャッチアップしなければ、executorの間違いに気づかずに結果をPdMに報告していたことでしょう。そして、誤った数字で開発部内全体に影響する重要な改修判断が行われるところでした。負荷検証で一番怖いのはこういう部分です。
3-5. 負荷要件は人数とexecutorだけじゃない
さて、流石にこれで正確な数字が取れているはず…と思っていましたが、PdMとの擦り合わせにより検証の前提そのものが間違っていたことが明らかになりました。またも書き直しです。
というのも、それまでは「瞬間的に数百人がアクセスする」要件を暗黙的に想定していましたが、実際のアクセス分布に詳しいPdMと会話をするうちに「ピーク時は秒間10〜15リクエストで、1000人分の分散アクセスがある」という持続負荷が実態に近い、という結論になりました。
executorは constant-arrival-rate(到着レート一定)に乗り換えです。
ここで面白いのは、人数が同じでも、同時アクセスか分散アクセスかで結果は180度変わることです。実例として、1500名の同時リクエストではリクエストの2割が失敗した操作が、2000名の分散リクエストでは全員がエラーなく完走したことがありました。
最終形では、回答フローの実際のリクエスト5本(回答画面GET → 回答POST → 深掘り質問GET → 深掘り回答POST → 完了画面GET)を一人一人が通しで踏む形にしました。
// 最終形。1 iteration = 1ユーザーが全5リクエストを通しで踏む survey_full_flow: { executor: 'constant-arrival-rate', rate: FLOW_RATE, // = 目標req/s ÷ 5 (1フローで5リクエスト消費するため) timeUnit: '1s', duration: `${DURATION_SEC}s`, // 総量 (人数×5リクエスト) から逆算して自然終了 preAllocatedVUs: 50, // 必須。VU が足りないと目標レートに届かず k6 が warning を出す maxVUs: 200, // 同時に動く VU の上限。1,000 セッションとは別物 },
これでようやく、完全に実態に即した負荷検証が完成しました!🙌
3-6. レポート化
こうして取れた実測値は、Notionの実測レポートにまとめてPdMに渡しました。サーベイ回答画面については、最終的に「1000人分の回答フローを秒間15リクエストで流すと、成功率は100%だがp(95)レイテンシが18秒、最悪で約40秒になり、DB負荷も6割を超える」という実測が出ています。全設問に答えたあと、送信ボタンを押してから数十秒待たされるのは体験として致命的です。
レポートを受けたPdMが改修の要否と優先度を判断し、担当ドメインの開発チームにアサインされていく、という2章で作った判断フローに自分の計測結果が使われていくのを見ると達成感がありました。
なお、この検証と並行してリポジトリ・インフラ全体を散々調査したおかげで、他画面の検証や、N+1・インデックス欠損といったボトルネックの当たりを付ける作業もどんどんできるようになっていきました。k6も「トライアンドエラーで詳しくなった」というのが正直なところで、挑戦の機会をくれたサービス開発部の懐の深さに感謝しています。
4. 再計測・改修効果の測定
開発チームに各画面のボトルネックを改修していただき、SREチームでもDB増強を行った後に、改修前と同じk6シナリオで再計測を行い効果を測定しました。サーベイ回答画面は最終形のシナリオを同じ設定値で再計測しました。結果がこちらです。
| 改修前 | 改修後 | |
|---|---|---|
| HTTPエラー率 | 0.00% | 0.00% |
| p(95) | 18.42s | 807ms |
| p(99) | 32.64s | 1.0s |
| Aurora ACU使用率 | 63% | 20% |
HTTPエラー率は変わらず0%のまま、p95は18秒から約0.8秒になっています。持続負荷をかけても、レイテンシは1秒前後に収まるようになりました。シナリオをファイルとして残しているからこそ、改修の効果を同じシナリオのbefore/afterで出せます。
さて、この頃になると、エンタープライズ導入の期日が目前に迫っていました。導入企業では利用開始にあたって説明会が開かれ、そこでは数百名が一斉にログインして画面を触ります。「本当に耐えられるのか」を事前に検証できるのは、知見・データ・シナリオ・AWS権限を持っている自分だけという状況になっていることに気づきました。
PdMとやり取りする回数が増え、様々な画面の再検証が必要になり、1日1日の重みが増していく中で、「自分は今、この巨大プロジェクトの一角をちゃんと担えているんだ」と自覚できました。大変な仕事ではありましたが、それは「信頼されている」ということの裏返しでもあり、自信が付きました。
5. 属人化の解消
一方で、「自分だけ」という状態は組織としては明確なリスクです。負荷検証は、対象画面のアーキテクチャ調査、テストデータの下準備、シナリオ設計、疎通確認、本実行、観測と工程が多く、このまま属人化させ続けたら次に取り組む人は私と同じ間違いで工数を無駄にしてしまうでしょう。
そこで現在、この一連の手順をClaude CodeのSkillとして体系化し、チームに展開する準備を進めています。要件のヒアリングからシナリオ生成、実行、観測までを対話で伴走してくれる形で作っていて、様々な効率化の結果、試運転の感触ではこれまで1〜2日かかっていた負荷検証が1時間〜半日程度で完了できる見込みです。
「自分にしかできない」を「誰でもできる」に変えるところまでが、SREの仕事だと思っています。
おわりに
新卒1ヶ月目でRailsもそれほど書けなかった私が、リポジトリを読み、バッチを書き、k6のシナリオを4回作り直しながら、数万名導入のパフォーマンス改善プロジェクトの一角を担った話をお届けしました。負荷検証をこれから始める方は、ぜひ「そのシナリオはどんな現実を再現しているか?」を自問してみてください。
最後まで読んでいただき、ありがとうございました!