マイクロ秒を追い求めて: Lighter におけるレイテンシエンジニアリング

@Lighter_xyz
英語1 日前 · 2026年7月08日
190K
552
79
39
266

TL;DR

高性能な Ethereum L2 をスケールさせるために、GC チューニング、CPU アフィニティ、メモリ管理などを駆使してサブミリ秒のレイテンシを達成した Lighter の技術的な取り組みを詳述します。

はじめに

何ヶ月にもわたる改善の末、Lighter のエンドツーエンド p99 レイテンシーは 280 ms からフラットな 55 ms に低下しました。トランザクション処理の p99 は 20–30 ms のスパイクから 1 ms 未満になりました。ホットパスの適用時間は 100–250 µs です。すべてメインネット上で、大規模に実現されています。

どれほどの規模か。2026 年 6 月 5 日、Lighter は 1 日あたりのトランザクション数で最高記録となる 8 億 1100 万を達成し、平均 9,388 TPS、ピーク時には 20,740 TPS を記録しました。

Lighter のシーケンサーは Go で書かれています。トランザクションは Lighter L2 上で実行され、完全な暗号証明書が L1 に送信されます。主要なアーキテクチャ上の洞察は、証明が実行から切り離されていることです。証明の生成は非同期で実行され、取引をブロックすることはありません。つまり、トレーダーが経験するレイテンシーは、Lighter の実行レイヤーと API レイヤーによって完全に決まります。

Lighter の API レイヤーは、トレーダーが直接やり取りするサービスです。メモリ内に完全で最新の取引所状態ビューを維持し、そのローカル状態から REST 接続と WebSocket 接続を提供します。重要なゴルーチンが実行エンジンから状態更新を受け取り、リアルタイムでインメモリキャッシュに適用します。下流のすべて(WebSocket プッシュ、API 読み取り、トランザクションシミュレーション)は、これらのキャッシュから提供されます。

カバーされる主要概念

以下の概念は、この技術文書の残りの部分でカバーされる主要トピックの概要を提供します。

メインネットに出す前に見つける: 同一の負荷テスト環境に合成アカウントと現実的な負荷をかけ、本番よりもはるかに積極的に計装することで、トレーダーが気づく前にボトルネックを発見します。

デュアル環境の可観測性: 負荷テストは本番ではコストが高すぎる指標を持ちます。メインネットは、鮮度、シミュレーションレイテンシー、エンドツーエンドのトランザクションライフサイクルについてリアルタイムで監視されます。これらを組み合わせることで、完全なカバレッジを実現します。

千ものアロケーションによる死: ヒープヘビーな型とディープコピーを、スタック割り当ての代替と不変スナップショットに系統的に置き換えます。GC(ガベージコレクション)のある言語では、アロケーションが少ない = ポーズが少ない = テールレイテンシーが低くなります。

OS レベルの制御: 重要なスレッドを専用の CPU コアに固定し、ほぼリアルタイムの優先度を設定します。

ハードウェアを考慮したデプロイ: NUMA を考慮した配置により、ホットパスが高速なローカルメモリアクセスを持てるようにします。レイテンシースタックの最終レイヤーです。

利便性よりもバイナリ: ホットパスで手書きのバイナリシリアライゼーションを使用することで、リフレクションとアロケーションのオーバーヘッドを排除します。節約された毎マイクロ秒が、1 秒あたり数千の更新にわたって積み重なります。

メインネットに影響が出る前にボトルネックを見つける

見えなければ修正できません。何かを最適化する前に、時間が正確にどこで使われているかを特定するためのツールに大きく投資しました。

負荷テスト環境

何かをテストしたいときは、専用の負荷テスト環境を立ち上げます。これはメインネットインフラの同一コピーで、同じ構成、同じデプロイトポロジーでサービスを実行します。常に稼働しているわけではなく、特定のテストのためにオンデマンドで起動し、その後破棄します。

この環境が立ち上がると、合成アカウントを作成し、現実的な取引負荷を生成して、ストレス下での実際の市場状況をシミュレートします。これにより、以下が可能になります。

  • 超詳細なタイミング: ホットパス内のステップごとのタイミング、操作ごとのキャッシュ構築時間、送信から確認までの各段階を追跡するトランザクションライフサイクルタイムスタンプ。
  • オンデマンドプロファイリング & フライトレコーダー: 負荷中にリクエストに応じて CPU、メモリ、実行トレースプロファイルをキャプチャします。Go のフライトレコーダーにより、常時オンでトレース収集が可能です。遅いイベントが検出されると、実行の最後の数秒間が自動的にキャプチャされ、一過性のレイテンシースパイクを事後に診断できます。
  • 分散トレーシング: ホットパス内のすべての主要関数にトレーススパンを計装し、単一の更新サイクル内で、サービス境界を越えて、正確にどこで時間が使われているかを詳細に可視化できます。

デュアル環境モニタリング

両方の環境(メインネットと負荷テスト)を、異なる粒度で綿密に監視しています。

メインネットはリアルタイムで厳密に監視されています。取引体験に関わるすべてを追跡します。

  • 鮮度追跡: 最もアクティブなマーケットの order_book WebSocket チャンネルを介したレイテンシーを測定します。基本的には、注文書の更新が生成されてからクライアントが受信するまでのずれです。これは、トレーダーが感じる体験を最も直接的に反映する指標です。つまり、見ている注文書がどれほど古いかです。これを 2 つのポイントで追跡します。最も重要なのは、実行エンジン → クライアントです。実行エンジンが状態変更を処理してから、結果の order_book 更新がクライアントに届くまでの全パイプラインです。また、API レイヤー → クライアントも測定します。API レイヤーが各送信更新にタイムスタンプを付け、クライアントが自身の時計と比較することで、ラストワンマイルのレイテンシーを単独で把握できます。
  • ドライラン レイテンシー: API レイヤーは、すべてのトランザクションを実行エンジンに渡す前にドライラン(署名、ナンス、残高の検証)します。すべてのドライランが計時され、報告されます。
  • エンドツーエンドのトランザクションライフサイクル: 送信から確認までのフルサイクルのレイテンシーヒストグラム。
  • キャッシュ効果: メモリから提供される頻度と、低速なストレージにフォールバックする頻度。

負荷テストはさらに詳細に監視されています。実際のトレーダーにサービスを提供していないため、オーバーヘッドを気にせずに計装を強化できます。ホットパス内のステップごとのタイミング、アロケーションプロファイリング、キャッシュ変更ごとの追加のヒストグラムバケットなどです。この細かい粒度により、メインネットの本番セーフな指標では明らかにできないマイクロレベルの劣化を捉えることができます。

メインネットのモニタリングは、システムが実際のトレーダーに対してどのように動作しているかを教えてくれます。負荷テストのモニタリングは、本番環境に到達するに劣化を捉えます。

ディープコピー排除とヒープアロケーション戦争

Go のガベージコレクタはレイテンシーに対する税金です。すべてのヒープアロケーションは、最終的に GC ポーズとなり、トレーディングエンジンにおける GC ポーズはトレーダーにとってのレイテンシースパイクです。そこで、ホットパスをくまなく調べ、可能な限りアロケーションを削減しました。その結果、レイテンシースパイクが減り、パフォーマンスがより予測可能になりました。

ディープコピー排除

ディープコピーはアロケーションファクトリーです。いくつかの方法でこれに取り組みました。

  • 不変スナップショット: 注文書キャッシュを不変にしました。読み取りは現在のスナップショットへのポインタを返し、コピーは不要です。更新は新しいバージョンを作成し、atomic.Pointer を使用してアトミックにスワップインします。
  • 不要なコピーの削除: その後決して変更されないデータをディープコピーしていたコードパスを見つけ、完全に削除しました。

アロケーションを意識した設計

  • スタック割り当ての数値: ヒープヘビーな big.Int および big.Rat を、ホットパス全体(価格変換、注文書深度マップ、サイズ計算)でスタック割り当ての代替品(int128float64int64 除算)に置き換えました。主要なフォーマット関数で最大 8.3 倍高速化され、操作あたりのヒープアロケーションはゼロになりました。
  • 条件付き更新: 変更がない場合はアロケーションをスキップします。
  • 事前サイズ設定されたコレクション: データ構造を事前にサイジングすることで、拡張とコピーのサイクルを排除します。
  • バッファ再利用: サブスクライバーパスは 1 秒あたり数千の更新をデシリアライズします。プールベースの中間バッファ再利用によりアロケーションを削減しました。

これによりテールが平坦になりました。以前は、Lighter のエンドツーエンド p99(実行エンジンからクライアントまで、order_book WebSocket チャンネルで測定)は、アロケーションが多い期間に 200–280 ms にスパイクしていました。ディープコピー排除、不変キャッシュ、ヒープアロケーションの作業後、p99 はフラットな約 50–60 ms の帯域に落ち着き、スパイクは実質的になくなりました。

Lighter - inline image

エンドツーエンド レイテンシー: 実行エンジン → クライアント (p99)

API レイヤーへの GC プレッシャーも測定可能なほど低下しましたが、これは単一の変更ではありません。API サーバーキャッシュの動作方法を根本的に再構築しました。以前は、キャッシュは TTL ベースの有効期限を持ち、更新のたびに新しいオブジェクトを割り当てていました。これらの短命なアロケーションのそれぞれが GC の作業になっていました。再構築後、取引所の完全な状態はスナップショットからホットスタートされ、アトミックにスワップされる長寿命の不変構造としてメモリ内に保持されます。これにより、TTL のチャーンと更新ごとのアロケーションが排除され、それに伴って GC プレッシャーも低下しました。メモリ使用量を綿密に監視しており、ワーキングセットは制限されています。

旧スタイルの API サーバーと再構築された API サーバーを、メインネットのトラフィックで並行して実行しました。再構築されたサーバーの GC ポーズ時間(p75)は約 3 ms で、旧サーバーの約 5–6 ms と比較されました。GC ポーズ時間はほぼ半分です。

Lighter - inline image

GC ポーズ時間: 標準 vs スナップショット API サーバー

頻度はさらに明確なストーリーを示しています。旧 API サーバーは、再構築されたサーバーよりも約 2.2 倍頻繁に GC サイクルをトリガーしており、これは短命なアロケーションが大幅に少なく、GC プレッシャーが全体的に低いことを直接反映しています。

Lighter - inline image

GC サイクル頻度: 標準 API サーバー vs スナップショット API サーバー

実行エンジンでの GOGC チューニング

同様の GC に関する考え方を実行エンジン自体にも適用しました。Go の GOGC パラメータは、ガベージコレクタの実行頻度を制御します。デフォルトでは CPU 時間とメモリ効率をトレードオフしますが、レイテンシーが重要なパスでは、このトレードオフは間違っていました。

GOGC をチューニングした後、実行エンジンの GC 時間は平均約 30 µs(スパイクは 100 µs に達していた)から、安定した約 10 µs の帯域に低下しました。約 3 分の 1 の削減で、スパイクは実質的になくなりました。

Lighter - inline image

GOGC チューニング後の実行エンジン GC 時間

3 ヶ月のメインネットデータが、この改善が持続していることを確認しています。チューニング後の期間はフラットで予測可能です。

トランザクション処理もそれに伴って改善されました。90 日間のメインネットデータ(すべてのトランザクションタイプ(注文作成、キャンセル、清算、デレバレッジ、転送など)をカバー)では、GOGC チューニング前の p99 は頻繁に 20–30 ms のスパイクを示していましたが、チューニング後はほとんどが 1 ms 未満になり、時折 3–4 ms の外れ値が見られる程度です。この改善は 2 ヶ月以上持続しています。

Lighter - inline image

トランザクション処理時間 - p99(過去 90 日間)

スナップショットサービス — データ削減とホットデプロイ

アロケーションのオーバーヘッドを排除した後、次のボトルネックに取り組みました。それは、内部メッセージバスを流れるデータ量です。スナップショットサービスを構築し、完全なインメモリ状態スナップショットを維持するようにしました。これには 2 つの主要な効果がありました。

  • ネットワーク上のデータ量削減: スナップショットサービスが完全な状態を保持するため、実行エンジンは更新のたびに完全な状態をメッセージバスにプッシュする必要がなくなりました。書き込むデータが減るということは、消費するネットワーク帯域が減り、受信側でのデシリアライズ作業も減ることを意味します。
  • ウォームアップ不要のデプロイ: デプロイ時に、API レイヤーはスナップショット(アカウント、注文書、マーケット情報、API 公開鍵)から起動し、すぐにサービスを提供できる状態になります。キャッシュが満たされるのを待つウォームアップ期間はありません。スナップショットをロードした後、API レイヤーは更新ストリームにサブスクライブし、デルタをリアルタイムで適用します。

スナップショットベースに加えて、インメモリキャッシュは継続的に更新されます。

  • アカウント情報: sync.Map によるロックフリー読み取り、アトミックポインタスワップによる更新。
  • 注文書: 不変スナップショットとして保存。読み取りはポインタを取得し、更新は新しいバージョンをスワップイン。読み取りパスにロックはありません。
  • API キーキャッシュ: すべてのキーがメモリに収まります。外部ルックアップを完全に排除しました。

結果として、取引所の状態全体がローカルメモリに存在し、リアルタイムで更新され、すべてのデプロイがホットスタートします。

実行エンジンへの波及効果は大きかったです。以前は、実行エンジンはネットワークを介してキャッシュキーを書き込んでおり、サービスは時々それらを読み取っていました。すべてがメモリ内に存在し、スナップショットサービスが状態分散を処理するようになったため、これらのネットワーク書き込みは不要になりました。それらを削除しました。その結果、ブロック時間の p99 が約 2.6 ms から約 1.2–1.8 ms に低下しました。これは単に、実行エンジンの書き込み量が大幅に減ったためです。

Lighter - inline image

ブロック時間 - p99

CPU 計画

ユーザースペースでの最適化をすべて行った後、次はカーネルに手を伸ばしました。

多数のゴルーチンがあり、それらを実行するために CPU 上でスケジュールする必要があります。スケジューリングのオーバーヘッドを最小化することは、低レイテンシーシステムにとって重要です。デフォルトでは、Go のランタイムはゴルーチンを OS スレッドに多重化し、OS はスレッドを CPU コア間で自由にマイグレーションできます。どちらも予測不可能なレイテンシーを引き起こします。

これを、4 つのメカニズムを積み重ねることで排除しました。

  1. `runtime.LockOSThread()`: ゴルーチンを単一の OS スレッドにロックし、Go のスケジューラがそれをマイグレーションするのを防ぎます。
  2. `sched_setaffinity` による CPU アフィニティ: その OS スレッドを特定の CPU コア(Linux)に固定します。これにより、カーネルがコア間でスレッドをマイグレーションするのを防ぎ、L1/L2 キャッシュの無効化を回避します。
  3. SCHED_FIFO による高優先度スケジューリング: スレッドのスケジューリング優先度を引き上げ、カーネルが他の作業よりもこのスレッドを優先するようにします。
  4. ビジーウェイトスピンループ: ホットパスは空の default ケースを持つ select を実行するため、ゴルーチンは決してパークしません。これがないと、Go はデータがないときにゴルーチンを「実行可能」状態に移行し、再スケジューリングにウェイクアップレイテンシーが加わります。スピンループにより、ゴルーチンは固定されたコア上で実行を続け、スケジューリング遅延ゼロで新しい更新を取得します。

影響を正しく測定することがここでは重要です。適用時間はトラフィック状況によって変動するため、絶対値は負荷によって変化します。ピン留めの効果を分離するために、同じトラフィック下で 2 つのグループの API サーバーを並行して実行しました。一方のグループはピン留めしないコントロールグループとし、もう一方を CPU ピン留めに切り替えました。同じ時間に、同一の負荷の下で測定された、それらの間のパーセンテージ差が、真の姿を示します。

ピン留め前は、両方のグループが一緒に推移しています。同じベースレイテンシー、同じスパイク動作です。

Lighter - inline image

ホットパス適用時間 - CPU 計画前

一方のグループでピン留めを有効にすると、一貫してピン留めしていないベースラインの下に位置します。同じトラフィックで、より低いレイテンシーです。ピン留めされたグループのスパイクも低く抑えられています。これは、スレッドマイグレーションのジッターと L1/L2 キャッシュの無効化が排除されたためです。

Lighter - inline image

ホットパス適用時間 - CPU 計画後

NUMA を考慮したデプロイ

固定されたコアのメモリアクセスが NUMA バウンダリを越える場合、CPU ピン留めだけでは十分ではありません。NUMA ノードは、それぞれ独自のローカルメモリを持つ CPU のグループです。リモート NUMA ノードのメモリにアクセスすると、ローカルアクセスと比較して10 倍のペナルティが発生します。

Lighter の API サーバーは元々、2 つの NUMA ノードを持つより大きなマシンで実行されていました。

text
1$ lscpu | grep NUMA
2NUMA node(s): 2
3NUMA node0 CPU(s): 0-95
4NUMA node1 CPU(s): 96-191
5
6$ cat /sys/devices/system/node/node0/distance
710 100

距離行列が物語っています。ローカルアクセスは 10、クロスノードアクセスは 100 のコストがかかります。10 倍のペナルティです。N 台の API サーバーがマシンを共有している場合、一部のサーバーは必然的に、固定された CPU が一方の NUMA ノードにあり、そのワーキングメモリ(インメモリキャッシュ、更新バッファ)がもう一方の NUMA ノードにあるという状況になります。すべてのホットパスイテレーションでクロスノードの税金を支払っていたのです。

修正方法は直感に反していました。API サーバーを単一の NUMA ノードを持つより小さなマシンに移したのです。スペックは半分ですが、すべてのメモリアクセスがローカルであることが保証されるようになりました。

text
1$ lscpu | grep NUMA
2NUMA node(s): 1
3NUMA node0 CPU(s): 0-95

これにより、コストを削減し、同時にレイテンシーを改善しました。ホットパスの適用時間はさらに低下し、約 100–250 µs の範囲になり、オフピーク時には約 100 µs まで下がります。2-NUMA ノードマシンで CPU ピン留めのみを行った場合の約 200–520 µs の範囲と比較してください。

Lighter - inline image

ホットパス適用時間 - NUMA 最適化

カスタムバイナリシリアライゼーション

Lighter の実行エンジンは、内部メッセージバスを介して API レイヤーに状態更新を公開します。すべてのシステム更新がこのパスを流れます。元々のシリアライゼーションは汎用エンコーディングライブラリを使用していましたが、リフレクション、型スイッチ、フィールドごとのアロケーションがホットパス上で不要なオーバーヘッドを生み出していました。

これを、手書きのバイナリシリアライゼーションに置き換えました。システム内のすべてのエンティティタイプに対して、固定レイアウトで、リフレクションゼロのエンコード/デコードです。各タイプは専用のエンコーダ/デコーダを持ち、既知のバイトオフセットでフィールドを読み書きします。リフレクションや型スイッチはなく、アロケーションも最小限です。すべてのコーデックにはラウンドトリップテストとファズテストがあり、劣化を検出します。

その結果、すべての下流キャッシュにデータを供給するパス上でのシリアライゼーションオーバーヘッドが大幅に削減されました。

独立したベンチマークは、コミュニティメンバー @UngusTrade によって構築・維持されているダッシュボードから入手可能で、永続的な取引所間のライブトランザクションレイテンシーを比較しています。 latency.perps.trading

ワンクリック保存

YouMindでバイラル記事をAI深読み

Save the source, ask focused questions, summarize the argument, and turn a viral article into reusable notes in one AI workspace.

Explore YouMind
クリエイターのために

あなたの Markdown をきれいな 𝕏 記事に

自分の長文を投稿するとき、画像・表・コードブロックを 𝕏 向けに整形するのは手間がかかります。YouMind は Markdown 全体を、そのまま投稿できるきれいな 𝕏 記事に変換します。

Markdown → 𝕏 を試す

解読すべきパターンをもっと

最近のバイラル記事

バイラル記事をもっと見る