← 返回模块
3.6.4.1beta 可读 · 未来免费校验通过内容版本 2026-05-24

行情数据的消息传递基础

3.6.4 · 行情数据的消息与流式处理 · 编程

周二早上,上海一家中等频率股票策略的 私募 量化开发被呼叫:沪深300 ETF 行情入库链路安静了 90 秒,交易室监控屏幕冻在最后一根 K 线上。生产者(producer)——解码 vendor 推送的 normalised 行情的 feed handler——状态正常;消费者(consumer)——把每条 tick 落进数据仓库的 writer——也正常。坏掉的是中间那根管子:一条手写的 UNIX socket,没有缓冲、没有偏移量、不能重放。Writer 因为一次慢盘 flush 暂停了三秒,socket 把背压顶回 feed handler,feed handler 的 receive ring 被压爆,于是 三秒钟 SSE 的 ticks 蒸发在一段无人读取的 kernel buffer 里。整个 3.6 子模块 到这一课为止(3.6.1 到 3.6.3)所有输入都从硬盘上的文件读。等你被要求把策略接到一条「活的」行情上,这套「文件在硬盘上」的心智模型就崩了:生产者与消费者是不同进程、常常是不同机器,任何局部故障都不能允许在十亿条 tick 的一天里漏一条 tick。中间这一层就叫消息传递层(messaging layer),本课教的是后面每一课都要用到的词汇。

消息传递要解决的四个问题

本模块之后所有的选型——Kafka 还是 ZeroMQ 还是 UDP multicastat-least-once 还是 exactly-onceJSON 还是 Protobuf 还是 SBE——都回答 four problems 中的一个。按这个顺序记住、永不打乱:

  • decoupling in time — broker 做缓冲,因此一个慢或正在重启的消费者不会把背压顶到生产者上。
  • fan-out — 一个生产者、N 个互相独立的消费者,每个消费者都看到每一条消息。
  • durable replay — 在 offset N 处崩溃的消费者,重启后从 N+1 接着读,不漏不重。
  • load balancing — 一个消费者组里 N 个消费者共享 topic 的分区,每个分区在任一时刻只属于一个消费者。

各自的典型落地场景:解耦保护了一个因为 chunk-close 慢盘暂停的 TimescaleDB writer,不丢 tick;扇出把一条 normalised 510300 行情同时喂给 20 个策略进程,每个进程都要看到每条 tick;持久重放让回测引擎从 Kafka topic 的 offset 0 读昨天的整个交易日,而不是重新生成;负载均衡让 16 个消费者共享 ticks.sse.510300 的 16 个分区,吞吐量 16 路扩展同时按 symbol 保序。

三种通信模式

三种原语拼出上面的四种方案:

  • point-to-point queue — 一个生产者,N 个互相竞争的消费者,每条消息​​恰好​​送到一个消费者;典型场景是任务队列(跑这个回测、做这次风险重算)。
  • pub-sub topic — 一个生产者,N 个互相独立的订阅者,每条消息送到​​每一个​​订阅者;典型场景就是行情扇出。
  • request-reply — 同步语义的 RPC 走队列(策略问风控『这单能不能下』并等回复);视作 point-to-point 的特例。

规则:要把工作分给唯一负责人,用 point-to-point queue;要让每个消费者都看到每条消息,用 pub-sub topic;要做控制面同步交互,用 request-reply。

三种投递语义:一段话讲清一种

按代价从小到大,broker 能给的三种保证:

  • at-most-once — 发了就不管;生产者发了不重试;便宜;生产者或 broker 一崩就丢;某些 telemetry 可以接受,​​行情或订单状态绝不可​​。
  • at-least-once — broker 反复重试直到消费者 ack;两边都好实现;消费者必须​​幂等​​,因为它可能看到同一条消息两次。本行业的标准幂等姿势:仓库表 ticks_raw(symbol, ts) 做主键,配 INSERT ... ON CONFLICT DO NOTHING(3.6.3 L4)。
  • exactly-once — broker 加事务消费者保证每条消息在事务边界内​​恰好​​处理一次;贵;只有在「靠业务无法做到幂等」时才用(比如一个无法用其他方式幂等化的资金余额变更)。

口号写明:​​默认就是 at-least-once + 幂等消费者​​。把语义升到 exactly-once,仅在「业务上幂等不可达」时才考虑——在量化场景几乎用不到,因为仓库表的 (symbol, ts) 主键已经在帮你做消费端幂等了。理论脚注:跨网络的真正 exactly-once 在一般情形下是不可能的(FLP 不可能性定理、Two Generals' Problem);Kafka 的所谓 EOS 指的是 Kafka 事务边界内的 exactly-once,这通常已经够用。

序列化的六种选择、三个场景

Tick 怎么编码不是语法题——线上的 wire format 决定下游每一项延迟、体积、跨团队契约的成本。六种典型选择:

FormatHuman-readableSize vs JSONEncode/decode costSchema-evolvabilityCanonical use case
JSONyes1.0x baselineslowschema-on-readtwo services on the same team
MessagePackno~0.3xmediumschema-on-readJSON is the bottleneck
Protobufno~0.2xfasttagged schemacross-team contract
FlatBuffersno~0.2xnear-zero (zero-copy)tagged schemalatency-sensitive consumer
SBEno~0.15xnear-zerotemplatelow-latency exchange feed
FIXyes (text tag=value)~0.6xmediumtag-numberedorder-entry session layer

JSON 是默认起手——同一个团队两个服务、对端数据形状还没定下来。MessagePack 一行库替换(msgpack.packb / msgpack.unpackb),大小约 JSON 的 30%;JSON 成为瓶颈但又不想引一份 schema 文件时上 MessagePack。Protobuf 是「生产者与消费者是两个团队、各自有独立发版节奏」的标准答案——.proto 文件就是合约,按 3.6.2 进 git 共享仓库,schema 演化规则内建(新字段用新 tag 号、tag 号不允许重用、删除字段标 reserved)。FlatBuffers 让你做 zero-copy 读取——wire format 就是内存布局——给「一个紧循环每秒读上百万条」的消费者用。SBE(Simple Binary Encoding)是 FIX Trading Community 出的低延迟二进制后继;现代交易所行情就是它(CME MDP 3.0、ICE iMpact、Eurex T7)。FIX 是订单录入的会话协议——tag=value 用 \x01 分隔,FIX 4.4 与 FIX 5.0 SP2 是主用版本——在 CFFEX 中金所 走 CTP 通道之外的境外股票期权订单流里仍随处可见,即便行情侧 SBE 已经取代了它。

一个 Tick 样例。同一条记录在 Protobuf 与 JSON 两种格式下:

message Tick { string symbol = 1; int64 ts_ns = 2; double price = 3; int64 size = 4; string side = 5; }
{"symbol":"510300","ts_ns":1716566400000000000,"price":4.137,"size":10000,"side":"B"}

JSON 约 70 字节;Protobuf 约 16 字节;CME MDP 3.0 上 SBE 模板编码约 10 字节。side 沿用英文 B / S(wire 上不翻译;B=买、S=卖)。Schema registry 脚注:跨团队演化时生产环境答案是 Confluent Schema Registry——.proto subject 注册并设 BACKWARD / FORWARD / FULL 兼容级别。国内 量化 firm 也常自建简化版本;Apicurio 是开源替代。

关键消息传递词汇

整个模块之后每一课——L2 的 Kafka、L3 的低延迟 fabric、L4 的 capstone——都用同一套词汇。现在背下来:

  • topic — 一个具名的 append-only 日志;一条逻辑通道。
  • partition — 一个 topic 被切成 N 个分区;每个分区是磁盘上一条完全有序的日志。
  • consumer group — 一组消费者协同读一个 topic;任一时刻每个分区只属于该组里一个消费者。
  • offset — 分区内单调递增的 64 位下标,指向「我下一条要读的消息」。
  • key — 每条消息可选的字节串;相同 key 经哈希落到相同分区;这是每 key 保序的机制。
  • headers — 与消息同行、但不是 payload 的元数据;用来放 tracing ID、replay marker、源系统标签。
  • retention — broker 保留消息的时长(Kafka 默认 168 小时;行情 topic 经常 30 天或 90 天以满足合规要求)。
  • log compaction — 每个 key 仅保留最新一条、丢弃更早的;典型场景是 instrument-master topic(每个 symbol 只关心最新一条记录)。
  • replication factor — 分区在 broker 间的副本数;生产默认值 3。
  • in-sync replicas (ISR) — 当前与 leader 保持同步的副本集合;acks=all 的写要等 ISR 全部 ack。
  • producer ack — 生产者要求的持久化级别:acks=0(发了不管)、acks=1(仅 leader)、acks=all(整个 ISR)。
  • idempotent producer — broker 按 producer-id + 每分区序列号去重;重试不会产生重复。
  • transactional producer — 多分区原子写入;exactly-once 语义背后的关键机制。

把上面这些词锚到一个例子:topic 叫 ticks.sse.510300,16 个分区,副本因子 3,retention 168 小时,按 symbol 做 key。所有 510300 的 tick 落到同一个分区上;该消费者组内只有一个消费者拥有这个分区,按生产端顺序读到这些 tick;当组里 16 个消费者一起跑时,它们在 symbol 维度上 16 路并行而​​不​​破坏 per-symbol 保序。

背压与有界队列规则

管道里任意两阶段之间的队列都有有限容量。消费者比生产者慢时,队列被填满,必须有人决定接下来怎么办。五种选择,code-formatted:

  • block — 生产者等;当延迟不是目标时(批处理)这是最安全的默认。
  • drop newest — 保留历史、丢失最新;行情场景里基本不是正确答案。
  • drop oldest — 保留最新、丢失历史;适用于「过期 tick 比缺一条 tick 更糟」的实时面板。
  • log-and-skip — 打一条结构化日志,把这条消息丢掉;可观测,能让故障浮上水面。
  • escalate — 拉起更多消费者来排空队列;当慢是结构性而非瞬时性时的正确选项。

口号:​​unbounded queue + slow consumer = OOM crash;每个队列都给上限,自觉选 overflow 策略​​。这是手写 feed handler 最常见的故障形态:默认「够快」、忽略上限,list 一路膨胀直到 OOM-killer 把进程杀掉。consumer lag 是任何消息管道最重要的生产指标;下一课在 Kafka 上具体接起来,3.6.6 把告警接上。

Exercise

Exercise

You are wiring three quant services together — (a) a market-data feed handler that consumes a synthetic exchange feed and publishes normalised ticks to downstream consumers; (b) a TSDB ticker-plant writer that appends each tick to a TimescaleDB hypertable ticks_raw(symbol, ts, price, size, side) with PRIMARY KEY (symbol, ts); (c) a real-time VWAP monitor that prints a 1-minute rolling VWAP per symbol from an in-memory window. For each of (a)->, choose (i) the communication pattern (point-to-point queue vs pub-sub topic vs request-reply) and justify in one sentence, (ii) the delivery semantic (at-most-once vs at-least-once vs exactly-once) and justify in one sentence (note the (symbol, ts) primary key on ticks_raw), (iii) the wire format (JSON vs MessagePack vs Protobuf vs FlatBuffers vs SBE vs FIX) and justify in one sentence, (iv) the overflow policy for the bounded queue on (a)->(b) if the TSDB writer is temporarily slow, choosing one of (block, drop newest, drop oldest, log-and-skip, escalate) and justify in one sentence. State the answers as a four-row table.

提示
(b) 与 (c) 都需要每一条 tick,因此从 (a) 出发的两条箭头模式相同:这是典型的扇出形态。再想:两个消费者各自能否容忍重复落地一次?仓库主键如何改变 (b) 的答案?
提示
第 (iv) 步,仓库 writer 是持久化那一支。TSDB 因 chunk-close 暂停时丢一条 tick 就永远丢了;让 feed handler 阻塞又会把背压顶向上游。正确选项是保留数据并让慢盘可见,让 operator 把 writer 横向扩。

下一课

你现在握住了模块之后每一课都假设你已经会用的词汇——topicpartitionconsumer groupoffset、三种投递语义、六种 wire format、有界队列规则。下一课把每一个词锚定到 Apache Kafka,本模块(以及 3.6 之后所有模块)的具体参照 broker。这里命名的每一个概念在下一课都对应一个具体的 Kafka 旋钮:acks=all 对应耐久性轴、enable.idempotence=true 对应去重轴、key=symbol 对应分区轴、enable.auto.commit=false 对应「先处理再提交」规则。​​先选 broker 再写 producer;先选 schema 再串两个团队;默认 at-least-once + 幂等消费者;每个队列都有界,自觉选 overflow 策略。​

阅读清单

  • Kafka 中文 文档(kafka.apachecn.org)——topic / partition / consumer group 章节。
  • 《Apache Kafka 实战》中文版(胡夕 著)。
  • 《数据密集型应用系统设计》中文版(Martin Kleppmann),关于消息系统与流处理 的章节。
  • ZeroMQ 指南 中文 翻译(社区版,github.com/anjuke/zguide-cn)。
  • Protobuf 中文 文档(developers.google.com 中文镜像)。
  • FIX Trading Community SBE 1.0 规范(fixtrading.org 英文权威)。
  • 《Enterprise Integration Patterns》(Hohpe & Woolf)——经典模式目录(Point-to-Point Channel、Publish-Subscribe Channel、Idempotent Receiver 是本课对应词条)。

一条额外注释:A-股 量化团队 的行情链路 落在 自建 Kafka 上;数据安全 与 跨境带宽 使 Confluent Cloud 与 AWS MSK 在国内 罕见。行情 vendor (TongLianData、WindData、HsiHengSheng) 已经把交易所 原始多播 normalise 成 TCP 推送,firm 把流写进内网 Kafka topic——本课 producer 在实际工作中 就是 这个落点。SSE、SZSE 与 CFFEX 是 ChinaSecuritiesRegulatoryCommission 监管的 ShanghaiStockExchange、ShenzhenStockExchange、ChinaFinancialFuturesExchange 三家场内。词汇先扎稳,L2 与 L3 的选项都建在这一层上。

速查卡

本课反复出现的展示形态:

  1. Inline-code 列表——FourProblems、ThreePatterns、ThreeSemantics、ThirteenVocabulary、FiveOverflowPolicies,code-formatted 项目列表。
  2. Inline-code 表格——SixWireFormats 按(human-readable、size、cost、evolvability、use-case)排列。
  3. Fenced ```protobuf 代码块——Tick schema 含五字段与 tag 1–5。
  4. Fenced ```json 代码块——同一条 Tick 在 wire 上 JSON 形态。
  5. Exercise 与两条渐进 Hint——三服务 (FeedHandlerWarehouseWriterVwapMonitor) 接线决策。