☰
行情协议深度解析:STEP与FAST的编码设计与恢复机制
2026/10/22 7:49:59 网站建设 项目流程

做行情接入的同行,大多经历过这种尴尬:协议文档翻了几十页,对接测试也全绿,一到仿真环境压测,带宽先报警。明明单条行情只有几百字节,乘以每秒几万笔、几十个订阅方以后,流量立刻变得可观。这时候才会回头研究 STEP、FAST 这类协议到底省了哪里、又贵在哪里。

这篇文章想把行情协议这块讲透一点。它不是规范文档的搬运,而是我这些年对接多个境内交易场所、改造行情网关时对 STEP 与 FAST 的实际理解。内容更适合自己做协议解析、做行情转发网关、或者做量化系统接入层的研发工程师。你能看到两类协议在结构设计上的核心差异、压缩与恢复的取舍,以及真正落地时的工程细节。

顺带说一句,行情接入和普通业务接口不一样,单个报文的时效和价值都极高,协议设计往往把"可恢复性"排在"可读性"前面。理解了这一点,再去看 STEP 和 FAST 的种种设计,思路会顺很多。

1. 行情协议选型背后站着的,不只是带宽

1.1 从一次链路压测说起

之前在某公司的行情网关改造中,我们遇到过一件很典型的事。业务侧要求把原来仅支持文本快照的接口换成更完整的两档行情加逐笔成交,逻辑上其实不难,但压测结果一出来,带宽先炸了。订阅端数量一多,文本格式的消息体里光字段标签就占掉一大块,还没算分隔符和没有意义的空格。后来换掉编码方式,同样一批业务字段,线上流量直接降到原来的四分之一左右。

这不是什么新奇发现,但说明了一个道理:行情协议的价值,绝大部分体现在编码设计和恢复机制上,而不是业务字段本身。STEP 和 FAST 的差异,归根结底也是围绕这两点展开的。

1.2 行情数据链路真正要解决的三件事

我把行情协议的核心问题归纳成三个:延迟稳定性、吞吐上限、断点恢复能力。

延迟稳定意味着每个包的处理时间不能忽高忽低。吞吐上限决定了一台网关能撑多少路订阅。断点恢复则决定了网络抖动、进程重启之后,下游能不能快速回到正确状态。任何行情协议,无论叫什么名字,本质都是在回答这三个问题。STEP 比较偏向"逻辑清晰、恢复简单、实现成本低",FAST 则把重点放在"流式压缩、状态依赖、带宽效率最大化"上。

后面所有细节,都可以回到这三件事上去对照着看。

2. STEP:境内市场常见的本地化接入协议

STEP 这个名字在不同语境下可能对应略不一样的规范,但它的技术路线是统一的:从经典文本 FIX 体系脱胎而来,做了二进制紧凑化改造,以适应行情报文的高频、高吞吐场景。我在对接多个境内容易接入平台时,见过不同版本的 STEP 变体,字段编号、消息类型有出入,但整体结构非常相似。

对实现者来说,STEP 的门槛在于二进制字段表不够"自描述",必须对照规范逐字段解析。好处是,一旦解析器写对了,性能很稳定,不涉及复杂的状态推导。

2.1 STEP 的报文结构:头部、消息体、重复条目

绝大部分 STEP 报文可以拆成三块:固定长度的头部、变长的消息体、消息体内的重复条目。头部通常包含消息类型、消息序号、消息体长度、标志位。解析时第一步永远是先读头部,校验长度,再决定后续怎么走。

消息体里的字段不是文本形式的"标签=值",而是按规范排好的二进制字段,每个字段要么有长度前缀,要么有固定宽度。比如价格、数量这类数值型字段,多数实现会采用定长或者短前缀编码;交易时间、交易所代码这类字段,则按规范截位存。

最体现 STEP 风格的是重复条目区。拿行情快照举例,一条消息里可能要带十档买卖报价,这十档价格、数量作为一组一组的小结构在消息体里重复排列。解析时先在固定偏移读到一个条目计数器,再按计数器循环解析每一条,每条内部再按各自的字段表读取。

我还见过一些 STEP 变体在重复条目前不加计数器,而是用某一位的终止标志,读到标志位就停。这给解析器带来一点判断成本,但好处是消息长度可以更灵活。实现的时候,最好两种都兼容,因为实际线上很可能混着不同版本。

2.2 快照、增量和按序恢复

行情接入最核心的一组消息是:全量快照和增量更新。

快照解决"新订阅方如何快速进入状态",增量解决"老订阅方如何紧跟变化"。STEP 对这两类消息做了明确区分,快照负责把当前的订单簿完整地发一遍,增量则只发个别价位的变动。增量的条目通常会带一个更新动作标志,标识是"新增""修改"还是"删除"。比如某个价位被吃掉,增量里就会出现一条删除该价位的记录;如果只是价格变了,就会带一条修改记录。

序列号在 STEP 里承担了关键角色。每条消息都带一个单调递增的序号,网关收到后按序应用。一旦发现序号之间出现跳跃,就别硬着头皮继续处理,因为可能会漏掉某个价位的删除或修改,导致订单簿脏掉。正确的做法是:记录最后一个连续序号,然后主动或被动触发补增量流程,从断裂点之前的序号开始重传。

快照和增量之间也有配合。订阅方首次连接时,通常是先拿快照,再连续接收增量;如果增量序号和快照里的最后序号对不上,说明中间丢了一段,必须重新请求快照。这个逻辑听上去简单,但很多系统恰恰是"能收到就算成功",没有严格校验序号衔接,最后行情越跑越偏。

2.3 一个可落地的 STEP 解析流程

写解析器的时候,我习惯先把固定头部解析做成一个独立的小函数,保证所有消息类型共用同一段逻辑。下面是一段简单的 C 风格解析示意,重点看长度校验和消息类型分支的方式:

#pragma pack(push, 1) typedef struct { uint8_t msg_type; uint32_t seq_no; uint16_t body_len; uint8_t flags; } step_msg_head; #pragma pack(pop) static int step_parse(const uint8_t *buf, size_t len) { if (len < sizeof(step_msg_head)) { return -1; } const step_msg_head *head = (const step_msg_head *)buf; if (head->body_len != len - sizeof(step_msg_head)) { return -2; } const uint8_t *body = buf + sizeof(step_msg_head); switch (head->msg_type) { case MSG_TYPE_SNAPSHOT: return parse_snapshot(body, head->body_len, head->seq_no); case MSG_TYPE_INCREMENT: return parse_increment(body, head->body_len, head->seq_no); default: return 0; } }

这里你只需要抓住三个要点:头部长度正确、消息体长度和实际接收长度一致、消息类型进入对应解析分支。线上最常见的解析失败原因,往往是长度字段没有校验就直接读了消息体,导致越界或者错位。

实际工程里,我建议把解析函数的输入设计成缓冲区加长度的形式,不要接收"已经复制好的字符串",这样能省掉大量无意义的内存复制。

3. FAST:以模板驱动的状态压缩协议

FAST 全称里的关键词是 Adaptive Streaming Technology,核心目的就是压缩。它不是为了做到让报文结构足够严谨,而是为了在有限的带宽里塞下更多行情。我在处理跨地域、多方订阅的行情分发时,FAST 的压缩收益非常明显。

代价是,实现复杂度高了不少。FAST 的解码器不是简单的字段循环,而是一个有状态、有模板、需要维护前值字典的解析引擎。

3.1 模板与存在性位

FAST 最大的特点,是编码器和解码器必须共享一份模板定义。模板决定了每条消息里包含哪些字段、字段按什么顺序排列、每个字段使用什么操作符。模板不是每包都传的,而是在连接建立之初或版本变更时一次性同步。

有了模板之后,FAST 还引入了一个叫"存在性位"的机制。你可以把它理解成一张位表:模板里每个可选字段,在报文流里都对应一个存在性位。位为 1,说明这个字段本轮实际传了值;位为 0,说明可以用前值代替,或者按模板里的默认值处理。

这就带来了一个非常实际的效果:连续两笔行情里,如果某个字段值没变,FAST 可以一整轮都不传这个字段。下游解码时一看存在性位为 0,就自动把上一轮的值填回来。在订单簿这种"大部分价格不变、个别价格微动"的场景里,这种机制能省掉非常多字节。

3.2 变长整数与操作符状态机

FAST 里处理数字不使用固定长度的 UInt32 或 UInt64,而是用变长整数。原理很简单:每个字节的最高位用来表示"后面还有没有更多字节",剩下的 7 位是有效数据。

def decode_uint(buf, pos): value = 0 while True: if pos >= len(buf): raise FastDecodeError("premature end") b = buf[pos] pos += 1 if b & 0x80: value = (value << 7) | (b & 0x7F) else: value = (value << 7) | b break return value, pos

举个例子:字节序列 0x81 0x57,第一个字节最高位是 1,表示继续,低 7 位是 1;第二个字节最高位是 0,表示结束,低 7 位是 0x57。把两段有效数据拼起来,得到 1 左移 7 位再加 87,也就是 215。小数字只用一个字节就能表达,大数字才多占几个字节,比固定四字节表达一个"1"要划算得多。

模板里的字段可以配操作符,常见的有常量、默认值、增量、复制和尾部。增量操作符的意思是:流里传的不是本轮的绝对值,而是与上一轮之差。还是拿价格举例,上一轮是 100.01,本轮变成 100.02,如果不带操作符,FAST 要传一个 10002 之类的绝对值;如果带增量操作符,就可能只传一个很小的差值。这个设计在价格缓慢变化的市场里效果极好。

但要注意,增量意味着状态依赖。如果中间丢了一条消息,编码器和解码器的"上一条值"就对不上了,后面所有增量都无法正确还原。这也是 FAST 必须配合快照和重置机制的根本原因。

3.3 解码器状态与同步点

写 FAST 解码器,本质上是在维护一张字段值状态表。模板里每个字段都可能在解码器侧保存一份"最近一次的值",下次读取时,如果存在性位为 0,就继续用旧值;如果操作符是增量,就在旧值基础上加上新传来的差值。

这种机制对连接恢复提出了更高要求。网络断开之后重新连上,解码器状态已不可信,必须等一个带重置标志的快照或者重置消息,把状态表清干净,才能继续增量处理。常见做法是:检测到序号不连续,立刻丢弃后续所有增量,并向服务端请求最新的快照,快照应用完成后再恢复增量流的处理。

所以,在采取 FAST 的线路上,快照不只是给新人使用的,更是给"摔过跤"的现有订阅方用来重新对齐状态的。快照发送频率、重置标志的位置,都会直接影响恢复速度和带宽成本。

4. STEP 与 FAST 的同场竞技:从压缩率到容错恢复

4.1 带宽压缩率的实测对比

在我们某次内部基准测试里,用同一批订单簿消息分别跑 STEP 编码和 FAST 编码。场景设定为五档行情加逐笔成交混合流,消息量约每秒六万笔。原始业务数据转成文本 FIX 风格大概需要 1000 字节一条,STEP 二进制约在 450 字节上下,FAST 用模板加变长整数压缩后,单条平均能压到 200 字节左右。

这不是一个严谨到可以到处引用的数字,毕竟不同字段分布、不同模板设计对结果影响很大,但量级关系是稳定的:FAST 能比 STEP 再省一半甚至更多带宽。如果你的场景是跨地域传输、公网带宽有限、订阅方特别多,这个差距足以决定方案是否可行。

4.2 解码开销与恢复能力的差异

省带宽不是免费的。FAST 的解码需要查模板、读存在性位、维护字段状态、执行操作符运算,CPU 开销明显比 STEP 高。STEP 因为无状态,可以在解析时走非常紧凑的偏移计算,很少需要回头看历史字段。对延迟极度敏感的场内系统来说,STEP 的处理路径更短,也更容易在未来用向量化等手段做进一步优化。

FAST 另一个麻烦在于,它无法从某条消息中间"掐进去"直接开解,必须要从同步点开始。这意味着在网关做灰度发布、多副本切换、或者日志回放时,FAST 需要先经过一套完整的初始化流程,而 STEP 只要第一包长度没问题,就能立刻开始解析。

下面这张表可以比较直观地看出两类协议在我眼里的定位:

对比项STEP 本地化接入协议FAST 压缩协议
编码思路二进制紧凑加长度前缀模板驱动加存在性位加变长整数
单条消息独立性较高,按序号即可解析较低,依赖前文状态
典型压缩效果中等更高
解码复杂度较低,基本无状态较高,需要维护字段状态表
断线恢复请求补增量即可需快照加重置上下文
适合场景内部局域网、直连接入、低延迟跨地域、窄带宽、多订阅方

4.3 什么情况下选 STEP,什么情况下引入 FAST

从我自己的经验看,选型并不存在谁绝对替代谁的结论。如果你的行情源就在同一个机房,下游订阅系统也都在同一片局域网里,带宽压力不大,STEP 的简单和无状态会省掉大量运维麻烦。相反,如果行情要过公网、要发给多个异地节点、或者订阅方数量多到带宽预算吃紧,那 FAST 的压缩收益就非常值得去换那点 CPU 开销。

还有一个容易忽略的因素是团队对协议的理解深度。FAST 上线不是只写个解码器就完事,还要维护模板版本、重置策略、快照节奏、异常恢复脚本。团队里如果没有经历过完整 FAST 工程的人,刚开始很容易在状态恢复上踩坑。这种时候,我建议先上 STEP 把业务跑通,再看带宽瓶颈是否真实存在。做技术选型,不要为了用新技术而引入不必要的风险。

5. 从协议解析到行情网关落地的实操细节

5.1 网关的整体架构与线程模型

行情网关看起来简单,实际做起来很考验工程基本功。我比较推荐的结构是:接收线程 + 无锁队列 + 解码线程组 + 下游分发器。接收线程只管把网络包塞进队列,解码线程按通道独立消费,下游分发器负责把解码后的统一事件对象发给各订阅方。

多路行情接入时,尽量让每路行情独占一组解码线程,避免一路行情波动拖累其他路。队列要设水位上限,超过水位就丢弃或者触发快照重订阅,绝对不能无界增长。很多线上故障都是内存逐步膨胀到最后被操作系统杀掉,根源就是队列没有背压机制。

5.2 协议适配层与字段映射

为了让两类协议共存,我会在网关里抽象出一个统一的市场数据事件结构,STEP 和 FAST 各写一个适配器。解码器输出统一结构,上层的存储、转发、监控逻辑完全不感知底层协议差异。

字段映射要注意单位统一。STEP 里有些价格是整数,有些是定点数,FAST 里还有可能按增量存储,适配层应在进入统一结构之前把它转成统一的数值精度。我见过因为"价格忘了除以一万"导致的脏数据问题,行情对不上,排查半天,最后只是单位没对齐。

另外,模板类协议的解码器一定要把模板文件单独管理,不要硬编码在代码里。模板一旦更新,只要不是不兼容的破坏性变更,网关应该能在运行时重新加载模板,而不是重启进程。

5.3 高频场景容易踩的问题排查表

从生产环境里捞了几个高频问题,列成一张排查表,大部分问题在开发阶段就能规避:

问题现象可能原因排查方向
网关 CPU 占用异常高解码逻辑没有走快速路径,或者存在无锁队列误用先做热点分析,看是模板解析还是字段复制耗时
重连后行情长时间不出数状态恢复逻辑没有触发快照请求检查序号连续性检测和快照请求时机
偶发消息缺失但序号连续存在性位或者长度字段解析错位回放抓包比对原始报文,逐个字节核对
多路行情时间戳不一致源端时钟不一致,或网关未做时钟对齐在网关统一处理时间偏移,不做透传
内存缓慢增长下游订阅方消费慢,队列堆积增加背压策略,拒绝过慢订阅方

这些问题的共性,是它们都不会在第一天爆发,往往要跑一段时间才暴露。协议解析这种工作,最怕的就是"能通就行",一定要把异常路径当成主线来实现。

6. 几条来自生产环境的实践体会

6.1 先把状态恢复模型画清楚

无论是 STEP 还是 FAST,代码动手前,我建议先别急着写解析器。拿一张纸,把正常消息流、断线重连流、快照重置流这几条路径画出来,确认每一个环节的序号校验和状态切换条件。这个过程比写代码重要得多。我之前接一个 FAST 项目,开发人员一开始就把解码器写完了,结果遇到消息丢失后恢复异常,最后不得不在原有代码上打了个大补丁,比从设计阶段就理顺状态机要痛苦得多。

6.2 边界测试必须覆盖的场景

协议解析的单元测试,只测正常报文远远不够。我建议至少覆盖:字段缺失、字段值等于默认值、空重复组、序号重复或者回退、模板版本不一致、报文中途截断、包含中文字符的长字段、以及超大条目组。这些边界在真实线上一定会遇到,早测早安心。

6.3 给协议演进留好扩展口

行情协议不会一成不变。STEP 的规范可能在字段编号上做调整,FAST 的模板可能会增删字段。网关设计时,协议版本号、模板版本号都应该成为处理和监控的显式对象,不要默默忽视。版本不一致时,要么自动兼容,要么明确报错,绝对不能静默解析出错误数据。

根据我的实际经验,行情系统对错误数据的容忍度远低于对延迟的容忍度。宁可多花几毫秒判断版本和状态,也不要为了省时间把一个不确定的报文硬塞进业务链路。毕竟,坏一个快照,可能后面等着的是一整条交易链路的错误决策。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询