本文へスキップ
Starone

FinTech

高性能なFinTechシステムの構築

金融システムにおいて速度が意味を持つのは、すべての結果が正しく、順序が保たれ、後から説明できる場合だけです。私たちはまず予測可能なテールレイテンシと完全な監査証跡を前提に設計し、スループットそのものはその結果として捉えます。

· 5分で読めます · Starone エンジニアリング

多くのソフトウェアにおけるパフォーマンス改善は、正確性との交渉です。ここでは少し古いデータを許し、あちらでは概算の件数で済ませます。金融システムではその選択肢がありません。一時的に誤った残高、二重に適用された注文、半年後に説明がつかない元帳エントリは、どれほど速く生成されたものであっても欠陥です。

この領域における高性能とは、目を引くスループットの数字ではなく、負荷がかかった状態での予測可能な振る舞いを指すと私たちは考えています。エンジニアリングの労力は、レイテンシ分布のテール、資金の状態が変わる箇所での競合の排除、そしてホットパスにほとんど負担をかけない監査証跡の維持に注ぎます。

レイテンシバジェットはホップごとに設定する

エンドツーエンドの目標は、分割して初めて行動に移せるものになります。たとえば決済のオーソリに99パーセンタイルで300 msのバジェットがあるとすれば、各ホップに取り分を割り当てます。エッジと認証、リスクチェック、元帳への書き込み、外部の決済レールへの呼び出しです。取り分を超過したコンポーネントは、議論ではなく計測によって特定されます。

バジェットを高パーセンタイルで定めるのは、平均値では重要なリクエストが隠れてしまうからです。複数のサービスにファンアウトするユーザー向けの操作は、その中で最も遅いものの影響を受けるため、テールレイテンシは依存先が増えるたびに積み重なります。負荷テストはオープンループで、つまりレスポンスにかかわらず一定のレートでリクエストを送る方式で行う必要があります。クローズドループのテストはシステムが遅くなると送信も遅くなり、明らかにするはずだったキューイングをまさに覆い隠してしまいます。

本当のボトルネックは競合

金融ワークロードでは、書き込みが少数の行に集中します。加盟店の精算口座、トレジャリーウォレット、人気の高い銘柄は全トランザクションの大きな割合から参照され、そこにかかる行レベルロックは、アプリケーションのインスタンスがいくつあってもシステムを直列化します。制約が1つのロックである以上、ハードウェアを追加しても効果はありません。

私たちが選ぶ設計は、ロックを保持している間に行う処理を減らし、ロックが必要になる頻度を減らすものです。

  • トランザクションを短く保つ:データベーストランザクションの中では、ネットワーク呼び出し、リスクスコアリング、大きなペイロードのシリアライズを行いません。
  • ロックは一貫した順序で取得する:たとえば口座識別子の昇順にすることで、逆方向の2つの振替がデッドロックしないようにします。
  • 残高の行をその場で更新するのではなく元帳エントリを追記し、残高はエントリから、または定期的にマテリアライズしたスナップショットから導出します。
  • 処理を口座単位でパーティション分割し、単一のライターが各口座のコマンドを順に処理するようにします。これにより、その経路ではロックが完全に不要になります。

楽観か悲観かは競合率で決める

楽観的同時実行制御、つまり書き込み時にバージョン列を確認する方式は、競合がまれな場合に優れた性能を示します。リーダーもライターも待たされないからです。ホットな口座では、これがリトライの繰り返しに陥り、処理を無駄にしてテールを伸ばします。SELECT FOR UPDATEによる悲観的ロックはライターを順番に待たせ、競合が多い状況でも安定したレイテンシをもたらしますが、待機中にコネクションを保持し続けるという代償があります。

どちらを選ぶかは、エンティティごとに計測した競合率に従います。1つのシステムで両方を使うのも妥当です。顧客口座には楽観的制御を、あらゆる処理が触れるごく少数の口座には直列化した単一ライターの経路を使います。

シーケンス、順序、ギャップ

ここでは順序は正確性に関わる特性です。すべての状態変更は、そのスコープ内で単調増加するシーケンスを持つべきです。そうすればコンシューマーはギャップからメッセージの欠落を検知し、気づかないまま乖離していく代わりにリプレイを要求できます。実時間のタイムスタンプは代わりになりません。ホスト間で時計はずれますし、2つのイベントが同じミリ秒を共有することもあります。

データベースのシーケンスはよくある落とし穴です。これは意図的に非トランザクショナルになっているため、ロールバックされたトランザクションも値を消費して欠番を残し、値が数値順とは異なる順でコミットされることもあります。サロゲートキーであればそれで構いません。しかし、一部の請求書や取引明細の採番のように欠番が許されない場合や、リーダーが既読の最大識別子でポーリングしていて遅れてコミットされた行を読み飛ばす場合には許容できません。欠番のない採番には同一トランザクション内で直列化されたアロケーターが必要です。これは設計上遅くなるため、必要な箇所だけに限定すべきです。

崩壊ではなくバックプレッシャー

制限のないシステムは悪い壊れ方をします。キューが伸び、レイテンシが上がり、クライアントがタイムアウトしてリトライし、そのリトライがすでに遅れているシステムにさらに負荷をかけます。すべてのキューとコネクションプールには上限を設け、上限に達したときの振る舞いは意思決定として定めるべきです。ある操作ではリトライ可能なエラーで素早く拒否するのが正しく、別の操作ではオーソリを守るためにレポート生成のような優先度の低い処理を切り捨てるのが正しい対応です。

デッドラインはリクエストとともに伝播させるべきです。そうすれば、呼び出し元がすでに諦めた処理に下流のサービスが労力を費やすことはありません。リトライには、ジッターを加えた上限付きの指数バックオフとリトライバジェットが必要で、冪等性キーを必ず伴わなければなりません。そうでなければ、インシデントからの復旧が資金移動の重複を生みます。

ホットパス上の監査可能性

監査は、ログテーブルへの2つ目の書き込みとして後付けされることがよくあります。これでは処理が倍になるうえ、主たる記録と食い違う可能性も残ります。私たちは、監査証跡そのものが主たる記録となる設計を選びます。追記専用の複式簿記の元帳で、すべてのエントリは不変であり、その原因となったコマンドを参照し、訂正は更新ではなく逆仕訳で行います。金額は最小通貨単位の整数または固定精度の小数で保持し、2進浮動小数点は決して使いません。

追記専用のログは高速でもあります。シーケンシャルな書き込みはストレージエンジンが提供する最も安価な操作であり、同じログがリプレイ、照合、調査を支えます。代償は、リードモデルを導出して最新に保つ必要があること、そしてスナップショットとアーカイブを計画しない限りストレージが際限なく増えることです。

トレードオフの所在

単一ライターによるパーティション分割とインメモリの状態は、最良のレイテンシと最も難しい運用モデルをもたらします。フェイルオーバー、リプレイにかかる時間、パーティションのリバランスのすべてに作り込みが必要です。中程度の取引量のシステムであれば、適切にインデックスを張り、トランザクションを短くし、ロック順序に注意を払ったPostgreSQLデータベースのほうがシンプルで理解しやすく、たいていはそれで十分です。私たちが特化したアーキテクチャに手を伸ばすのは、リレーショナルな設計に余地がなくなったことを計測が示したときであり、それより前ではありません。

  • 元帳には厳密な直列化可能性を選び、レポーティング、検索、通知には結果整合性を受け入れます。
  • 確認応答済みの書き込みを失うことが許されない箇所では同期レプリケーションを選び、それによって増えるレイテンシをバジェットに織り込みます。
  • 取引量が許すならシンプルさを選びます。オンコールのエンジニアが説明できるアーキテクチャは、インシデント時には性能上の強みになります。

同様の課題に取り組んでいる方へ

ぜひ意見を交換させてください。この記事を書いたエンジニアが直接お話しします。