我做 Tardis 数据接入已经三年,从最早用 requests 一条条拉,到后来用 asyncio + uvloop 跑到单核 8 万条/秒,再到引入 zmq 把分发层独立出去,踩过的坑足够写一本《增量数据踩坑实录》。这篇文章我把生产级 L2/L3 订单簿重建的完整链路拆开讲:协议解析、状态机、并发控制、回放校验、性能调优,以及如何通过 立即注册 HolySheep AI 的 Tardis 中转服务把采购成本压到自建的 1/6 以下。
为什么 L2/L3 增量数据这么难处理
HolySheep 中转的 Tardis.dev 频道覆盖 Binance、Bybit、OKX、Deribit 主流合约交易所,订单簿相关数据有三种粒度:
- L2 top-N:每 100ms 一个快照,字段是
[price, size],入门最简单 - L2 增量:每条都是
update_id+[price, new_size],需要自己合并 - L3 全深度:每条挂单/撤单都带
order_id,状态机最复杂,但能复现任意时刻的 order flow
实测在 Binance btcusdt 永续上,L2 增量峰值约 4,200 条/秒,L3 峰值约 11,800 条/秒,L2 快照文件压缩后平均 820 MB/天。把这一年的数据存下来,光是 L3 raw 增量就是 3.2 TB,自己买 S3 流量 + 计算实例,月成本轻松破 2 万人民币。
Tardis 增量协议与订单簿状态机
一条典型的 L2 增量(来自 Tardis 的 normalized 格式):
{
"type": "book_update",
"exchange": "binance",
"symbol": "btcusdt",
"timestamp": "2025-09-12T03:14:15.123Z",
"local_timestamp": "2025-09-12T03:14:15.158Z",
"id": 1842319237841,
"side": "buy",
"price": 67234.10,
"amount": 0.250
}
关键点:amount == 0 表示撤单;同一 id 之后的 update 必须按 local_timestamp 严格递增处理,乱序会破坏状态机。我在生产里遇到的 70% bug 都来自这里的假设错误。
生产级订单簿重建核心代码
我把自己在用的 OrderBook 类精简到生产可用版本,核心是 SortedDict + 单调性校验:
import sortedcontainers
from dataclasses import dataclass
from typing import Tuple
@dataclass(slots=True)
class Level:
price: float
size: float
class OrderBook:
__slots__ = ('symbol', 'bids', 'asks', 'last_id', 'last_ts', 'count')
def __init__(self, symbol: