Skip to content

Repository files navigation

Matching Engine 低延遲交易撮合引擎(Rust)

限價撮合引擎(price-time priority),含下單閘道、行情推播、主備高可用與確定性重放。 走低延遲 / 強一致 / 確定性路線 —— 與姊妹專案 ../seckill(.NET 高吞吐) 刻意採相反的併發哲學,對照見 設計決策 FAQ

實作規格見 ROADMAP.md,Agent 工作守則見 CLAUDE.md。 協議 docs/protocol.md · 架構與 ack 語意 docs/architecture.md · 效能數據 docs/perf.md · 問題與決策紀錄 docs/issues.md

進度

Milestone 狀態 完成日 備註
M0 骨架 ✅ 完成 2026-07-26 8 crates、Rust 1.96.1 釘版、workspace lints、Kafka+etcd compose
M1 撮合核心 ✅ 完成 2026-07-26 四種訂單型別 + STP、proptest 3 不變量 ×10,000、criterion baseline
M2 WAL/重放 ✅ 完成 2026-07-26 framing+CRC32C、snapshot、replay hash 一致、kill -9 ×3 輪
M3 Gateway/組裝 ✅ 完成 2026-07-27 WS 二進位協議 + JWT + 風控(etcd 熱更新)、佇列背壓、Kafka producer
M4 行情/落庫 ✅ 完成 2026-07-27 L2 snapshot+增量(md_seq)、冪等落庫、me_reconcile_diff==0
M5 主備 HA ✅ 完成 2026-07-27 etcd 選主 + journal 串流複製、kill -9 接管 2.53–2.59s、零遺失
M6 微秒級攻堅 🟡 2/4 量級達成 吞吐 1M/s ✅、p99 3.98–6.39µs ✅、零配置 ✅;p99.9 與 LAN p99 未達(見下)
M7 部署/報告 ✅ 完成 2026-07-27 RKE2 全部 Ready、k8s failover 4.04s、混沌後 me_reconcile_diff==0

M6 兩項量級目標未達,依 CLAUDE.md 不自行放寬、也未打 m6-done tag; 原因與所需條件見 docs/perf.md。M7 為部署與報告階段,獨立驗收。

部署實測(RKE2,M7)

項目 數字
kubectl apply -k deploy/k8s/ 11 個 pod 全部 Ready
engine primary 永久移除 → 接管 4,041ms(修 SIGTERM 前是 34,602ms)
gateway pod kill 新 pod 2s Running,客戶端重連
Kafka 中斷 20s engine 存活,恢復後 downstream 追上
混沌後對帳 me_reconcile_diff{count}=0{notional}=0,13,672 rows,0 duplicates
端到端 ack p50(生產設定) 533ms —— 其中 fsync 約 4×、主備複製約 20×(歸因見 perf.md)

部署階段抓到、本機永遠不會發現的四個 bug(細節見 docs/issues.md):

  1. journal writer 的 yield_now() 空轉 —— 每個 engine 常駐燒掉一整顆 CPU, 在 limits.cpu:1 下把吞吐壓到 431/s(修正後 2,097/s)
  2. 容器 PID 1 忽略未安裝 handler 的 SIGTERM —— failover 慢了 30 秒
  3. Kafka 的 PVC 掛在 log dir 的上一層,entrypoint 每次重建該目錄 → topic 全失
  4. me-marketdata 不跟隨選主,會安靜地黏在熱備上(看起來健康、深度永遠不動)

架構

                    ┌──────────── etcd ────────────┐
                    │  /matching/leader/{group}     │  lease TTL 5s
                    │  /matching/risk/{symbol}      │  風控熱更新
                    └───▲──────────────────▲────────┘
             watch      │                  │ campaign
                        │                  │
  clients ──WS(binary)──▶ me-gateway ──TCP:9000──▶ me-engine (primary)
   ▲   JWT + 風控前置    │                         │  單寫者:
   │                    └── order 路由/合成 ◀──────┤  sequencer(WAL)
   │                                               │    → core 撮合
   └──────── ACCEPTED / EXECUTION / CANCELED ──────┤    → 事件分發
                                                   │
        me-marketdata ◀── EventStream(L2 深度)────┤
        (WS: snapshot + md_seq 增量)                │
                                                   │ TCP:9001 journal 串流
        me-downstream ◀── Kafka me.trades ──────────┤
        (冪等落庫 → PostgreSQL + 對帳)              ▼
                                          me-engine (backup, 熱備)

單寫者模型:每個 symbol 一條撮合執行緒,journal → 撮合 → 事件分發全序化。 撮合執行緒上沒有鎖、沒有 syscall、穩態零記憶體配置。

延遲分布報告

量測環境

開發機 專用機 k6 RKE2 節點(agent)
CPU AMD Ryzen 5 9600X 6C/12T(Zen 5,2024) Intel Xeon E5-2695 v2 8 vCPU(Ivy Bridge-EP,2013) Xeon E5-2695 v2 4 vCPU
時脈 基頻 3.9GHz / boost 5.4GHz 2.40GHz 固定 2.40GHz 固定
RAM / OS 31GB / Windows 11 Pro 15GB / Debian 13,kernel 6.12 7GB / Debian 13,kernel 6.12
型態 實體機 PVE VM PVE VM
干擾 桌面背景程序;timer 精度 15.6ms 無其他負載 131 個容器任務(Longhorn/Prometheus/calico/kubelet)
建置 Rust 1.96.1 release,lto="thin", codegen-units=1 同左(容器交叉編譯) 同左

下面最好的數字都來自開發機,而它本來就是這三台裡最快的機器 —— 兩顆 CPU 相隔約 11 年,單核性能約 2 倍(同一份 binary:開發機 1,000,000/s、k6 713,579/s)。 這不是調校的功勞。反過來說,唯一能開 isolcpus 的兩台都是老 Xeon 的 VM, 所以「快 CPU + 核心隔離」這個組合從來沒被量到 —— 詳見 docs/perf.md「這批數據沒能分離的兩個變因」。

in-process 四段延遲(me-engine harness,mixed profile、fsync=off)

延遲從排定到達時刻起算(constant-arrival-rate,無 coordinated omission), quanta 打點,HdrHistogram 至 p99.99。

p50 p90 p99 p99.9 p99.99
純撮合 sequenced→matched 0.01µs 0.02µs 0.02µs 0.02–0.03µs 0.04µs
ingress→matched(含跨執行緒交棒) 0.22µs 0.34µs 3.98–6.39µs 159µs 355µs
matched→ack(含落盤水位) 0.21µs 0.28µs 43.6µs 163µs 646µs

(開發機、1M orders/s、spin 模式 + RealTime 優先級;RKE2 節點的對照數據見 docs/perf.md。)

吞吐

情境 結果
in-process、fsync=off 1,000,000 orders/s(持續 5s)
in-process、batch(64,5) 148,661/s —— 天花板是磁碟:2,323 fsync/s × 64,即本機 NVMe 的 0.43ms fsync
端到端 LAN(loadgen 另機) 1,000/s 時 p50 607µs / p90 1,037µs / p99 4,731µs

量級目標對照(誠實結論)

目標 實測 判定
in-process ≥ 1M orders/s 1,000,000/s
in-process p99 ≤ 10µs 3.98–6.39µs(5 次跑 4 次)
in-process p99.9 ≤ 50µs 85–159µs
LAN 端到端 p99 ≤ 1ms 4,731µs(p50 607µs、p90 1,037µs)
確定性重放 hash 一致 11,211 筆 kill -9 後兩次 replay hash 相同
kill -9 接管 ≤ 3s、已 ack 零遺失 2.53–2.59s(4 次)、503 筆全在新 primary journal
穩態零配置 0 alloc / 0 dealloc / 0 realloc(100k applies)

兩項未達的根因是環境不是程式碼:純撮合段的 p99.9 在所有平台都 ≤0.71µs (比 50µs 目標好 70–2,500×);超標的部分全在「撮合執行緒沒被 OS 排到」的 跨執行緒交棒段。目前沒有任何一台安靜的機器可用(開發機有桌面負載、 RKE2 節點各有 131 個容器)。達成需要:專用主機 + isolcpus/nohz_full + 只給撮合執行緒(而非整個 process)RT 優先級。詳見 docs/perf.md

設計決策 FAQ

為什麼單寫者(single-writer)?

撮合的正確性取決於全序。多執行緒撮合同一個 symbol 需要鎖或 CAS 迴圈, 在爭用下尾延遲遠差於單執行緒;而單執行緒的代價(單核吞吐上限)實測是 每筆 20–90ns,遠高於任何真實市場的單 symbol 訊息率。擴展方式是 按 symbol 分片(每片一條執行緒 / 一個 StatefulSet),不是加副本。

確定性重放為什麼是地基,不是附加功能?

core 不讀時鐘、不用隨機、不做 I/O,輸入只有 InboundMsg 序列 —— 因此 「同一 journal ⇒ 同一事件流」。這一條同時買到:崩潰恢復(重放到崩潰點)、 主備熱備(backup 重放同一串流)、除錯(重現生產事故)、以及 M6 換掉整個 資料結構後的等價性驗證(ladder 與 BTreeMap 逐 bit 相同,10,000 proptest cases)。

ack 到底代表什麼?

ack = min(journal fsync 落盤 seq, backup 已複製 seq)。 即**「已 ack ⇒ 紀錄同時在 primary 磁碟與 backup 上」** —— 這就是 kill -9 零遺失的數學基礎。batch(64,5) 的代價是 ack 延遲(≤5ms 窗), 不是丟失窗口(ack 在 fsync 之後才發)。完整取捨見 docs/architecture.md §2

busy-spin 值得嗎?

實測 park → spin 把 p50 從 7,262µs 打到 0.25µs(29,000×),代價是 多吃約 2 顆核。原因不美好但很真實:Windows 預設 timer 精度 15.6ms, recv_timeout(1ms) 實際睡 ~15ms。在專用核心上值得,在共享 k8s 上不值得 —— 所以 ME_ENGINE_SPIN 預設 false。

core pinning 呢?

在沒有核心隔離的環境是負收益:實測 p99 差 111×(2,757µs vs 24.8µs)。 硬綁單一邏輯核後,該核被系統執行緒/DPC 佔用時只能等;不綁時排程器立刻 把執行緒挪到空閒核。ME_ENGINE_PIN 預設 false,只有 isolcpus 環境才該開。 (還踩過一次 SMT 坑:pin 到 ids[0]/ids[1] 正好是同一物理核的兩條超執行緒。)

與 seckill 專案的哲學對比

matching-engine(本專案) seckill(.NET)
目標 微秒級尾延遲、強一致 高吞吐、最終一致
併發 單寫者,每 symbol 一執行緒 無狀態水平擴展 + HPA
狀態 記憶體 order book + WAL journal Redis + DB
擴展 symbol 分片(垂直切) 加副本(水平擴展)
一致性 確定性重放、ack 後不丟 冪等 + 補償
熱路徑 禁 async/鎖/syscall/配置 async 全鏈路

本機開發

podman compose up -d          # Kafka 3.8.1 (KRaft) + etcd 3.5.17
cargo test --workspace        # 31 suites
cargo bench -p me-core        # 撮合微基準
me-engine harness --rate 1000000 --duration 5 --spin   # 四段延遲 harness

k8s 部署見 deploy/k8s/README.md, 節點壓測腳本見 deploy/bench/README.md

About

Low-latency matching engine in Rust: single-writer per symbol, ack only after fsync + replication, deterministic WAL replay, primary/backup failover on Kubernetes.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages