【CPUは余っているのに処理が詰まる…】スループットを約2倍にした地道なパフォーマンスチューニングの話
はじめに
はじめまして。バクラクビジネスカードの開発を担当している、Douglasこと武内です。
バクラクビジネスカードは法人向けのクレジットカードサービスで、外部システムからのリクエストを短い時間内にさばくオンライン処理をいくつも抱えています。
この記事では、そうしたオンライン処理のひとつを題材に、負荷が高まったときにスループットが頭打ちになる状況に向き合い、計測でボトルネックを見極め、処理の並列化とロック取得のリトライ調整によって、スループットを約2倍に改善した際の試行錯誤を書いていきます。
経緯
きっかけは、ある機能でリクエストが集中する時間帯にアラートが増えていたことでした。この処理は外部システムからのリクエストを受けて、短い時間の内に応答を返す必要がありますが、いくつかの検証処理を順に行っています。
しかし、機能追加を重ねるうちに直列でいくつもの処理が続くようになり、平常時は問題なくても、リクエストが集中したタイミングで後続のリクエストが詰まりやすくなっていました。
今後、より多くのユーザー様にご利用いただくことを見越すと、この集中時のパフォーマンスが将来のボトルネックになりかねないと考え、改善に取り組みました。
計測してわかったこと
まず、内部処理を分解して計測しました。
ここで面白かったのは、ボトルネックが特定の1ステップに固定されなかったことです。負荷のかかり方によって、実行時間の主役になる検証処理が入れ替わりました。
そのため、ここだけ直せば終わり、という唯一のボトルネックは存在していなかったのです。
さらに意外だったのが、リクエストが集中している時間帯であってもCPU使用率には余裕があったことでした。つまり、CPUは主因ではありませんでした。
この処理では、整合性を正しく保つために一時的な排他制御(排他制御)を行っています。
検証を直列に実行していたことで1件あたりの処理時間、ひいてはロックを保持する時間が延び、リクエストが集中すると後続のリクエストが制限時間内にロックを取得できずにタイムアウトするようになっていました。
つまり、ボトルネックは直列処理が生むIO待ち(I/O待ち)とロック競合にありました。
打ち手を決める
打ち手の候補は、大きく3つありました。
- (a) 外部サービス側の根本的な最適化
- (b) 互いに依存しない検証処理の並列化
- (c) インフラのスペック増強
この処理では外部サービスとの通信が発生する箇所があるのですが、(a)は外部サービス側の対応が必要でスコープが大きいため今回は見送りました。
(c)については、主要なボトルネックがCPUではないと判断した以上、c単体では効果が薄いと整理しました。
最終的には、今回は(b)をベースとしつつ、処理の並列化に伴うCPU使用率の増加を見越して最小限のスペック増加も実施し、(c)も副次的に採択することになりました。
施策1:独立した検証をまとめて並列化する
前述の通り、この処理はリクエストを受けてから応答するまでに、複数の検証を順に行っています。
これらの検証には、互いに依存関係のないものが多くありました。依存関係がないのなら、順番に実行する必要はありません。
そこで今回は、互いに依存しない検証処理群を分解し、まとめて並列に実行するようにしました。Goであれば、goroutineを使ってこうした並列化を比較的手軽に実装できます。
直列だった処理が並列になったことで、1件あたりの処理時間とロックの保持時間が縮み、スループットを約2倍に引き上げることができました。本番環境でも、リクエストが集中したときに起きていた処理詰まりを大きく減らせています。
もう一つ効いたのが、ロック取得のリトライ間隔の調整です。
従来は、リトライ間隔にExponential Backoffを選択していました。
競合を避けるには有効な一方、最初の数回のロック取得の試行に失敗してしまった場合に、後続のリトライの待ち時間が急速に伸びるため、制限時間内に試行できる回数が限られます。
ロックが解放されても次の試行までの待ちが必要以上に長くなってしまい、取得のチャンスを逃してしまう状態となっていました。
事前の計測結果より、ほとんどの処理はロックを短い時間で解放しているとわかっていたので、その事実に合わせてリトライ戦略を見直し、制限時間内に十分な回数を試行できるように調整しました。
リトライが増える分、ロック管理に使っているデータストアへの負荷が高まる恐れがありましたが、メトリクスで負荷に余裕があることを確認しながら、段階的にリトライ間隔の調整を進めていきました。
結果として、月末月初にリクエストが集中するようなケースのアラート件数を、0件(!)にまで減らすことができました。
おわりに
リクエストが集中すると処理が詰まる、という課題に向き合った話を紹介しました。
派手な特効薬はなく、やったことは「内部処理を分解して計測する」「ボトルネックを突き止める」「候補を並べて、やらないことを決める」「選んだ打ち手を計測で裏取りしながら段階的に入れる」というような地道な積み重ねでした。
このような「計測とファクトに基づいた、粘り強いエンジニアリング」に少しでもワクワクした方、決済領域の裏側に興味がある方は、ぜひ一度カジュアルにお話ししましょう!
求人はこちらです。
原文を表示
はじめに
はじめまして。バクラクビジネスカードの開発を担当している、Douglasこと武内です。
バクラクビジネスカードは法人向けのクレジットカードサービスで、外部システムからのリクエストを短い時間内にさばくオンライン処理をいくつも抱えています。
この記事では、そうしたオンライン処理のひとつを題材に、負荷が高まったときにスループットが頭打ちになる状況に向き合い、計測でボトルネックを見極め、処理の並列化とロック取得のリトライ調整によって、スループットを約2倍に改善した際の試行錯誤を書いていきます。
経緯
きっかけは、ある機能でリクエストが集中する時間帯にアラートが増えていたことでした。この処理は外部システムからのリクエストを受けて、短い時間の内に応答を返す必要がありますが、いくつかの検証処理を順に行っています。
しかし、機能追加を重ねるうちに直列でいくつもの処理が続くようになり、平常時は問題なくても、リクエストが集中したタイミングで後続のリクエストが詰まりやすくなっていました。
今後、より多くのユーザー様にご利用いただくことを見越すと、この集中時のパフォーマンスが将来のボトルネックになりかねないと考え、改善に取り組みました。
計測してわかったこと
まず、内部処理を分解して計測しました。
ここで面白かったのは、ボトルネックが特定の1ステップに固定されなかったことです。負荷のかかり方によって、実行時間の主役になる検証処理が入れ替わりました。
そのため、ここだけ直せば終わり、という唯一のボトルネックは存在していなかったのです。
さらに意外だったのが、リクエストが集中している時間帯であってもCPU使用率には余裕があったことでした。つまり、CPUは主因ではありませんでした。
この処理では、整合性を正しく保つために一時的な排他制御を行っています。
検証を直列に実行していたことで1件あたりの処理時間、ひいてはロックを保持する時間が延び、リクエストが集中すると後続のリクエストが制限時間内にロックを取得できずにタイムアウトするようになっていました。
つまり、ボトルネックは直列処理が生むIO待ちとロック競合にありました。
打ち手を決める
打ち手の候補は、大きく3つありました。
- (a) 外部サービス側の根本的な最適化
- (b) 互いに依存しない検証処理の並列化
- (c) インフラのスペック増強
この処理では外部サービスとの通信が発生する箇所があるのですが、(a)は外部サービス側の対応が必要でスコープが大きいため今回は見送りました。
(c)については、主要なボトルネックがCPUではないと判断した以上、c単体では効果が薄いと整理しました。
最終的には、今回は(b)をベースとしつつ、処理の並列化に伴うCPU使用率の増加を見越して最小限のスペック増加も実施し、(c)も副次的に採択することになりました。
施策1:独立した検証をまとめて並列化する
前述の通り、この処理はリクエストを受けてから応答するまでに、複数の検証を順に行っています。
これらの検証には、互いに依存関係のないものが多くありました。依存関係がないのなら、順番に実行する必要はありません。
そこで今回は、互いに依存しない検証処理群を分解し、まとめて並列に実行するようにしました。Goであれば、goroutineを使ってこうした並列化を比較的手軽に実装できます。
直列だった処理が並列になったことで、1件あたりの処理時間とロックの保持時間が縮み、スループットを約2倍に引き上げることができました。本番環境でも、リクエストが集中したときに起きていた処理詰まりを大きく減らせています。
もう一つ効いたのが、ロック取得のリトライ間隔の調整です。
従来は、リトライ間隔にExponential Backoffを選択していました。
競合を避けるには有効な一方、最初の数回のロック取得の試行に失敗してしまった場合に、後続のリトライの待ち時間が急速に伸びるため、制限時間内に試行できる回数が限られます。
ロックが解放されても次の試行までの待ちが必要以上に長くなってしまい、取得のチャンスを逃してしまう状態となっていました。
事前の計測結果より、ほとんどの処理はロックを短い時間で解放しているとわかっていたので、その事実に合わせてリトライ戦略を見直し、制限時間内に十分な回数を試行できるように調整しました。
リトライが増える分、ロック管理に使っているデータストアへの負荷が高まる恐れがありましたが、メトリクスで負荷に余裕があることを確認しながら、段階的にリトライ間隔の調整を進めていきました。
結果として、月末月初にリクエストが集中するようなケースのアラート件数を、0件(!)にまで減らすことができました。
おわりに
リクエストが集中すると処理が詰まる、という課題に向き合った話を紹介しました。
派手な特効薬はなく、やったことは「内部処理を分解して計測する」「ボトルネックを突き止める」「候補を並べて、やらないことを決める」「選んだ打ち手を計測で裏取りしながら段階的に入れる」というような地道な積み重ねでした。
このような「計測とファクトに基づいた、粘り強いエンジニアリング」に少しでもワクワクした方、決済領域の裏側に興味がある方は、ぜひ一度カジュアルにお話ししましょう!
求人はこちらです。
関連記事
今日のまとめ
AI日報で今日の重要ニュースをまとめ読み