本文へスキップ
Orleansで金融取引基盤を建て直した話 ある証券系プロジェクトの現場からのアイキャッチ画像
Architecture

Orleansで金融取引基盤を建て直した話 ある証券系プロジェクトの現場から

公開: 更新: 約8分で読めます

金融のリアルタイム基盤は、平常時の姿だけを見ていると意外なほど静かに動いている。負荷が本当の意味で問われるのは、相場が動いた一瞬だ。ここで紹介するのは、私たちがご一緒したある金融系のお客様(社名は伏せる)の取引基盤を、.NET Orleans で組み直したプロジェクトの記録である。守秘の都合で具体的な数値や固有名は出せないが、どんな課題があり、なぜ Orleans を選び、どこでつまずき、最終的に何が変わったのか。設計判断の流れを、なるべくそのまま残しておきたい。

相談は「落ちた朝」の翌週に来た

最初のお声がけは、相場が急変した週明けの直後だった。海外発の金利観測をきっかけに寄り付きから売り注文が殺到し、平常時とは桁の違う流量がシステムに押し寄せた。既存の取引システムはその負荷を捌ききれず、約定の遅延と一部機能の停止が発生したという。担当の方の言葉を借りれば、「動いていないわけではないのに、肝心なときにだけ動かない」状態だった。

ヒアリングを進めると、痛点は性能だけではないことが見えてきた。注文・ポートフォリオ・市場データ・リスク評価といった本来別々に扱えるはずの関心事が、単一プロセスと単一データベースに強く結び付いていた。どこか一箇所の遅延が全体に波及し、片方をスケールさせようとするともう片方が足を引っ張る。運用チームは日々の対処に追われ、根本の作り替えには手が回らない。よくある構図だった。

興味深かったのは、社内でも問題の所在はおおむね把握されていた点だ。ただ、止められないシステムをどう作り替えるかという道筋が描けず、着手できないまま時間だけが過ぎていた。私たちがまず求められたのは新しい技術の提案そのものより、動かし続けながら移れる現実的な段取りを一緒に描くことだった。

旧システムの中核は、乱暴に単純化すればこういう構造だった。ひとつのロックの下で、同期的なデータベース呼び出しと外部リスク判定の呼び出しが直列に並ぶ。負荷が上がると、CPU は余っているのにスレッドが待ち行列で埋まっていく。

// 旧構造のボトルネックを単純化して再現したもの
public class LegacyTradingSystem
{
    private static readonly object _lock = new();

    public OrderResult PlaceOrder(Order order)
    {
        // 1件の注文が、実質すべての処理を止めてしまう
        lock (_lock)
        {
            using var cmd = new SqlCommand("sp_PlaceOrder", _connection);
            cmd.ExecuteScalar();              // 同期DB呼び出し

            var price = CalculatePriceSync(order);            // 同期
            var validation = CallExternalValidationApi(order); // ブロッキング

            return new OrderResult { Success = true };
        }
    }
}

問題はコードの巧拙というより構造にあった。全注文が同じロックへ列を作り、外部 I/O をブロッキングで待つ。しかも中核のデータベースが単一障害点で、そこが詰まればサービス全体が連鎖して止まる。ピーク時にだけ牙をむくこの設計を、私たちは真っ先に解体対象に据えた。

証券会社のエンジニアと外部コンサルタントが、モダンなオフィスで大型ディスプレイのリアルタイム市場データを見ながら落ち着いた表情で議論している情景
急変する相場でも安定して動く取引基盤を、金融のお客様と協働で組み直していった

フルリプレイスを避けたかった、という制約

お客様側の要望ははっきりしていた。作り直したいが、全面刷新に何年もかける余裕はない。その間にまた同じ朝が来たら困る。つまり「現行の限界は越えたいが、一度に全部は替えられない」という制約が、技術選定の前提だった。

この制約が、私たちが Orleans を有力候補に置いた理由でもある。Orleans のバーチャルアクター(グレイン)モデルは、注文・ポートフォリオ・市場データ・リスクといったドメインを、それぞれ独立した状態を持つ小さな単位へ自然に切り出させてくれる。アクターの生成・配置・活性化はランタイムが受け持つので、こちらは「どの単位で状態を分けるか」に集中できる。加えて、旧システムを生かしたまま新基盤へ少しずつ流量を移す Strangler Fig 的な進め方とも相性がよい。全面刷新のリスクを取らずに段階移行できる点が、この案件では決め手になった。

グレインで取引コアを組み直す

設計の出発点は、責務をグレイン単位に切り直すことだった。クライアント単位の OrderGrain、利用者単位の PortfolioGrain、銘柄単位の MarketDataGrain、リスク評価を専任する RiskGrain。それぞれが自分の状態だけを持ち、グローバルなロックは前提から外した。

OrderGrain では、検証・市場データ取得・ポートフォリオ照会を並列で走らせ、約定が確定する前の待ち時間を削っていく。即答が要らないリスクチェックや通知は、待たずに別グレインへ投げる。おおよそ次のような形だ。

[Reentrant]
public class OrderGrain : Grain, IOrderGrain
{
    private readonly IPersistentState<OrderState> _state;

    public async Task<OrderResult> PlaceOrderAsync(OrderRequest request)
    {
        _state.State = new OrderState(this.GetPrimaryKey(), request);

        // 独立した処理は並列で発火させる
        var validation = ValidateOrderAsync(request);
        var market     = GetMarketDataAsync(request.Symbol);
        var portfolio  = CheckPortfolioAsync(request.UserId);
        await Task.WhenAll(validation, market, portfolio);

        if (!validation.Result.IsValid)
        {
            _state.State.Reject(validation.Result.Reason);
            await _state.WriteStateAsync();
            return OrderResult.Rejected(validation.Result.Reason);
        }

        var price = await CalculatePriceAsync(request, market.Result, portfolio.Result);
        _state.State.SetPrice(price);

        // 即答が不要なリスク判定は待たずに逃がす
        _ = PerformRiskCheckAsync();

        var exec = await SendToMarketAsync();
        _state.State.MarkExecuted(exec);
        await _state.WriteStateAsync();

        return OrderResult.Executed(_state.State.OrderId, exec.Price);
    }
}

市場データは銘柄ごとにグレインを立て、外部から受け取った価格更新をキャッシュしつつ、購読中のクライアントグレインへストリームで配信する。指値の合致もそのストリームの流れの中で評価されるので、約定までの経路が短くなる。状態の永続化は Orleans の Persistence に任せ、シロ(ホスト)が落ちても別のシロでグレインが復活する。旧来なら自前で書いていた活性化と復旧の面倒を、ランタイム側へ寄せられたのは大きかった。

段階移行という、いちばん神経を使った工程

技術的な設計より緊張したのは、切り替えの進め方だった。一気に全量を新基盤へ流して何かが崩れれば、それこそ相談のきっかけになった朝の再演になる。そこで機能フラグで対象利用者を絞り、社内での検証から始めて、限られた利用者、アクティブな利用者、そして全体へと、週単位で割合を上げていった。

public async Task<OrderResult> RouteOrderAsync(OrderRequest request)
{
    if (await _toggle.IsEnabledAsync("UseOrleansTrading", request.UserId))
    {
        var grain = _grainFactory.GetGrain<IOrderGrain>(Guid.NewGuid());
        return await grain.PlaceOrderAsync(request);
    }
    return await _legacy.PlaceOrderAsync(request);  // 旧系へフォールバック
}

移行期間は新旧両系の応答時間と約定の成否を並べて観測し続けた。割合を上げるかどうかは印象ではなく計測で決める、というのがお客様側の一貫した方針で、私たちもそれに合わせて可観測性の整備を先に手当てした。旧系へいつでも戻せる経路を残しておいたことが、意思決定の心理的な負担をかなり軽くしてくれたと思う。

金融ならではの壁をどう吸収したか

金融システムには、性能とは別軸の要件がついて回る。監査ログは後から改ざんできてはならないので、各エントリを直前のハッシュに連結するチェーン構造をグレイン側に持たせ、連鎖が切れない設計にした。一定の条件に該当する取引は、規制報告を担うグレインへ即座に引き渡し、別系統の処理が所定の届け出につなげる。

public class AuditLogGrain : Grain, IAuditLogGrain
{
    private readonly IPersistentState<AuditLog> _state;

    public async Task LogTradeAsync(TradeAuditEntry entry)
    {
        // 直前のハッシュに連結して改ざんを検知可能にする
        entry.PreviousHash = _state.State.LatestHash;
        entry.Hash = ComputeHash(entry);

        _state.State.Entries.Add(entry);
        _state.State.LatestHash = entry.Hash;

        if (RequiresRegulatoryReport(entry))
        {
            await ReportToRegulatoryBodyAsync(entry);
        }
        await _state.WriteStateAsync();
    }
}

可用性のための冗長化は、Orleans のクラスタリングが多くを引き受けてくれた。一方で、拠点をまたぐ配置での遅延や、どこまでを強い整合で扱いどこからを緩めてよいかという線引きは、ランタイム任せにはできない。ここはドメインの意味を踏まえてアプリ側で設計するしかなく、金融の担当者と何度も机を囲んで詰めた。分散システムの難所は結局のところ、技術ではなく業務の判断が残る部分に集まる、という当たり前を改めて確認した工程でもあった。

変わったこと、学んだこと

移行を終えて何より大きかったのは、相場が急に動く場面での挙動が安定したことだ。以前は負荷の山でだけ壊れていた処理が、ピークでも過度に遅れずに流れるようになり、片方の関心事の詰まりが全体を巻き込む連鎖も起きにくくなった。運用チームは急変時の火消しから解放され、その分の労力を機能改善へ回せるようになったという声をいただいた。具体的な数値は共有できないが、定性的には「肝心なときにだけ動かない」状態から抜け出せた、というのが率直な総括だ。相場の急変は今後も起きるが、そのたびに現場が身構える必要はなくなった、という安心感がいちばんの成果かもしれない。

副次的だが効いたのが、変化への追従が速くなったことだ。外部フォーマットの変更にも、インターフェースを担うグレインを差し替える範囲で対応できる場面が増え、以前なら身構えていた案件が普通の作業に収まるようになった。振り返って学びを整理すると、次のあたりに集約される。

  • バーチャルアクターの抽象は、クリティカルなリアルタイム処理でも実用に足る — 状態の分割と活性化・復旧をランタイムへ寄せられる意味は大きい
  • 段階移行を前提に置けたことが成否を分けた — 旧系へ戻せる経路を残すと、意思決定の心理的なコストが下がる
  • 監査・規制報告・リスク管理といった金融固有の要件も、責務をグレイン単位に切ればコードの見通しを保ったまま吸収しやすい
  • 分散配置の遅延や整合性の線引きはランタイム任せにできない — ここはドメインの判断として設計に組み込む必要がある

金融に限らず、リアルタイム性とスケールを同時に求められ、かつ全面刷新のリスクを取りにくいドメインでは、Orleans は十分に検討に値する選択肢だと考えている。

エンハンスド株式会社では、Orleans や .NET Aspire を用いた高性能な分散システムの設計と、金融をはじめとするミッションクリティカルな領域でのシステム開発を支援しています。現行構成の評価から、グレイン設計・状態管理・クラスタリング、そして無停止での段階移行の計画まで、要件に応じて伴走します。まずは現状の課題整理からでもお気軽にご相談ください。

この記事をシェア

コピーしました

関連記事