实时行情系统的坑,我踩了不止一次。前几年给一套交易辅助平台做行情模块的时候,团队里每个人对“实时”两个字都有自己的理解:做后台的觉得 500 毫秒也叫实时,做策略的恨不得每笔成交都在 1 毫秒内落地,做运维的又担心链路一抖就雪崩。那段时间开会讨论最多的就是三个问题:接入用什么协议,架构怎么做成高可用,数据源到底听谁的。这篇不打算写成教科书式的架构说明,主要分享我们在协议选择、高可用链路、数据源选型这三件事上的真实取舍、踩过的坑,以及一些事后复盘出来的设计原则。准备搭建行情系统或者正在改造行情链路的团队,可以参考一下。
1. 动手前先把“实时”变成一组可验证的数字
很多人一上来就开始聊 FIX、聊 UDP 组播、聊 Kafka,但我发现真正的问题往往不是技术选型,而是压根没定义清楚“实时”的验收标准。没有量化目标,所有方案都只是在打嘴仗。
1.1 延迟预算:从哪里开始计时,到哪一步截止
我当时的第一个动作,是让所有人把“实时”落到一条时间轴上:从行情源发出数据、到接入网关收到、再到内部处理后推给下游应用,到底哪一段算延迟?如果只说“全链路要快”,这句话是没有办法指导设计的。
我们最终把延迟预算拆成了三段:上游网络延迟、网关和中间件处理延迟、下游应用消费延迟。其中上游网络延迟通常不受自己控制,只能通过拉专线、把服务部署到靠近行情源机房的区域来优化;自己真正能设计的是后面两段。
举个例子,如果业务容忍的端到端延迟是 100 毫秒,而行情源机房到自己服务器的公网链路正常情况下就要 30 毫秒,那留给内部的预算其实只有 70 毫秒。如果网关解析要 20 毫秒、消息总线排队要 20 毫秒、下游业务处理还要 20 毫秒,看起来每段都不多,但合在一起已经逼近上限。一旦出现 GC 停顿或者网络重传,某一段稍微劣化,P99 立刻就会冲出指标。
所以我会建议先把这条链路每一跳的具体耗时上限写出来,能压缩到多少毫秒、每段由哪个团队负责,然后再去讨论用什么协议和架构。否则最后系统上线了,业务方投诉慢,你根本说不清是源头慢、解析慢还是分发慢。
1.2 容量规划:按峰值设计,别按平均值设计
行情流量有个典型特征:峰值瞬间能把平均曲线甩开很多倍。开盘、收盘、重大消息发布的瞬间,事件频率会突然拉高,而且这种突发可能没有任何预兆。
我们当时统计过一段行情回放数据,全天平均每秒可能只有几千个 tick,但某些瞬间能冲到每秒数万笔,最高峰和均值之间的差距经常是几十倍。这里面真正的瓶颈往往不是带宽,而是包率。一条 tick 消息即使只有几百字节,当每秒几万个包砸过来时,服务器的软中断、内核协议栈解析、用户态拷贝都会成为瓶颈。
做容量规划时我习惯按峰值流量再乘上 3 到 5 倍余量来设计。原因很简单:行情系统不能靠“临时扩容”扛住突发,很多情况下流量是在几秒钟内瞬间上去的,弹性扩容根本来不及。如果买机器时按 QPS 均值去算,高峰期大概率会出现网卡丢包或者处理线程堆积。
1.3 故障恢复窗口:高可用不是“永远在线”,而是“多久恢复”
再好的架构也没法保证不出现断连。真正需要定义清楚的是:如果链路断了,你允许业务在多少秒内感知不到异常,链路恢复后能不能补齐缺失的数据。
我们当时给团队定的目标是:正常情况下从行情源收到 tick 到下游业务可见,P99 不超过 30 毫秒;高峰期不允许丢弃 tick;单条链路断线后自动切换到备用链路的窗口控制在 10 秒以内;断线期间累积的增量如果补齐不了,必须在 30 秒内完成一次全量快照重建。
这几个数字看起来不难,但真正落地的时候会发现,很多协议和架构方案在这些条件下是不过关的。有的协议不支持从断点续传,断线后只能重新拉全量;有的消息队列在重连恢复时需要几十分钟的追赶,根本无法满足 30 秒重建的要求。所以,在写第一行代码之前先把这些硬指标定下来,后面每一步的取舍都会有判断依据。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 接入协议选型:别把“看起来很专业”当成最重要的标准
协议选择是实时行情系统里最容易引发争论的环节。做量化的人大概率会告诉你必须用 FASTER 的二进制协议,做 Web 的人会告诉你 WebSocket 够用了,做传统集成的可能坚持要用 FIX。这些说法在特定语境下都成立,但脱离场景谈协议就是耍流氓。
2.1 主流行情协议的真实差异和适用边界
先整理一下我实际接触过的几类行情协议各自的定位。
| 协议类型 | 典型使用场景 | 优点 | 需要注意的地方 |
|---|---|---|---|
| FIX / FAST | 机构间交易、传统券商、部分行情源的标准接入 | 字段语义标准化,生态成熟,兼容性好 | 文本协议解析开销大;FAST 虽然压缩率高,但编解码依赖复杂上下文,恢复逻辑容易踩坑 |
| WebSocket + JSON | 面向互联网、移动端、内部运营系统 | 浏览器友好,接入成本低,调试方便 | JSON 文本膨胀明显,解析消耗 CPU,消息频繁时吞吐容易被压垮 |
| 自定义二进制协议 | 交易所直连、专线场景、高频自营系统 | 包体小、解析快、编解码逻辑可控 | 没有通用生态,所有边界都要自己处理,设计不好会埋很多雷 |
| UDP 组播 | 局域网内一对多高速分发 | 延迟最低,适合大规模水平扩展 | 需要自行设计确认、重传、排序机制,否则丢包就是灾难 |
有段时间团队里不少人觉得,既然做实时行情,肯定应该用能想到的最“硬核”的协议,比如定制的二进制私有协议。但后来发现,你的协议再高效,如果上游行情源根本不支持,最后还是得在网关层做一个转换。协议选型的本质不是选一个最快的,而是要选一个和你的数据源、下游消费方都匹配的。
比如 WebSocket 最大的问题不是解析速度,而是业务层普遍缺乏可靠恢复机制。浏览器前端或者说多数移动端场景下,连接断开很常见,但断开后你是重新订阅所有标的还是一股脑拉全量?很多实现偷懒,直接断开后重新发送订阅列表,再从当前时间开始接收增量。这样在断开期间发生的行情就永远丢掉了,如果拿这种数据去做盘中统计,结果一定是错的。
UDP 组播则是另一个极端。它在局域网里的延迟表现确实好,但组播本身无法保证不丢包,也不保证包的到达顺序。你去设计重传逻辑时就会发现:“哪一段丢了、丢了多久、需要向谁补”这些问题远比 TCP 复杂。如果团队没有很强的网络底子,我不建议第一天就直接让所有下游都接入裸组播。
2.2 网关适配层:把协议隔离在系统边缘
我自己比较推崇的做法是:无论上游是 FIX、WebSocket 还是某种私有协议,都先在接入网关层把它们统一转换成内部消息模型。下游业务模块永远只感知一种内部协议,而不是让不同协议的差异渗透到业务代码里。
这样做的好处,在切换数据源或接入新行情源时特别明显。我们曾经遇到一个情况,某个交易所源只能提供 FIX/FAST 格式,另一个服务商则提供 WebSocket JSON,如果业务层直接对接这两种上游,遇到字段理解不一致的问题就会改到怀疑人生。加了一个网关适配层之后,接入新源只意味着新增一个适配器,把上游字段映射为内部字段,核心服务和下游应用完全不用动。
内部消息模型也不是随便抽一层就完事,要保留三类基础信息:原始源的会话标识、源序列号、源事件时间。这些信息是后面做去重、对账、排查问题的基础。如果你在网关转换的时候把这些原始上下文丢掉了,等出了问题再想追溯,基本无从下手。
2.3 增量、快照和补数接口必须一起设计
协议里最容易被忽略的,是把行情分为全量快照和增量序列之后,增量链路断开怎么办。很多协议设计只定义了“连接建立后先发快照,然后持续发增量”这种理想流程,一旦中间出现几分钟断线,客户端手里的快照早就过期了,增量也接不上,最后只能重新拉全量。
正确的做法是必须同时配套一个“补数”机制。当客户端发现序列号不连续时,知道自己缺了从哪一条到哪一条的数据,向服务端请求补发缺失区间;如果缺失区间太大,超出了服务端缓存窗口,则触发一次全量快照重建。
当时我们给这个补数逻辑设计了一个超时阈值:如果当前缓存里找不到请求的起始序列号,就直接返回快照请求,让客户端降级为全量重新订阅。这种降级看起来很粗暴,但总比让客户端一直挂着、默默丢数据要好。
服务端行情缓存窗口要开多大,取决于你的业务恢复目标。如果要求断线 30 秒内完成重建而不是从头拉全量,那服务端至少要缓存稍大于 30 秒的增量数据。对全市场行情来说,这需要的内存不小,所以需要配合实际事件频率算一算。
3. 高可用链路的关键点:不是“两台机器”,而是数据链路够不够抗造
很多团队一提高可用就是用 Keepalived 搞两台机器、配一个虚拟 IP,主挂了备顶上。这个做法本身没错,但用在实时行情上远远不够。你真正要解决的是:数据链路断开的瞬间,上游和下游是否能在不丢行情的前提下平稳切换。
3.1 接入层双链路互备:主备切换和双活选优的区别
我们最初做的也是简单的主备模式:一条链路正常接收,另一条空闲,主链路挂了才把流量切到备用链路。但实测下来发现,切换过程中 TCP 建连、重新认证、重新订阅快照,这些步骤加起来的耗时经常比我们预期的 10 秒指标还要长。而且主备模式下备用链路长时间空闲,很难感知自己是否真的能正常工作,等主链路真正出问题时才发现备用链路也早就断了,这种情况最尴尬。
后来我们把主备切换改成了双活选优:两条链路同时连接、同时接收行情。接收端维护两个通道,正常情况下谁的数据先到先用谁的数据,但通过来源序列号做去重,绝不会把两条链路传过来的同一条行情重复投递给下游。
双活选优在实现上会比主备复杂,有一个很现实的问题:两条通道收到同一笔行情的时间可能相差很大,一条链路先到的包可能实际上是旧包,另一条链路后到的才是较新的边界。所以不能只比“谁先到”,要比“谁的序列号更高、事件时间更新”,还要在序列号相同的情况下用源优先级作为仲裁。
这样设计有个额外好处:当某条链路持续异常时,仲裁器可以自动降低它的优先级,甚至暂时屏蔽它,让另一条链路继续对外输出,整个过程不会中断数据流。行情源切换对下游来说基本无感。
3.2 去重和排序要用源序列号,不要用业务字段
去重的规则看似简单,实际上很容易做错。有时候业务上同一个事件可以从两个不同数据源取到,但它们的消息格式、内部编号甚至价格字段都可能存在细微差异。如果用业务字段去判断重复,比如“同一时间同一价格同一成交量”,很容易出现误判,把本来不重复的行情当成重复给过滤掉。
我们踩过的版本就是通过自定义业务哈希做去重,结果在行情剧烈波动时出现几条其实应该保留的“瞬间回抽”被误删了,后来全部改成依赖源会话和源序列号。
序列号也不是永远可信,它可能因为数据源重启而回退。所以代码里判断“gap”的时候,还需要结合源会话版本号。如果数据源实例重启,我们就会把它视为一个新会话,检查行情源是否重新发快照;如果它直接发增量,就要特别小心,因为下游缓存里还是旧会话数据,新旧会话混着用极易产生脏数据。
3.3 断线检测和切源策略:别只看 TCP 通不通
TCP 连接还在,不代表这条链路是健康的。我们监控过一个现象:交易所侧行情源程序发生死锁,TCP 层心跳看起来还在,但实际上数据已经停滞了十几秒。如果只看连接状态,永远不会触发切换。
健康的链路检测需要有两层:第一层是物理或应用层心跳,确认对端进程还活着;第二层是数据新鲜度检测,记录最近一条消息到达本机的本地时间,如果超过一个阈值没有新数据到达,就要认为通道处于半死状态。
新鲜度检测的阈值不能设得太小,否则行情本身平静的时候容易误报。正常情况下行情确实可能几秒钟没有新 tick,如果阈值小于这个自然静默窗口,就会反复切换,反而制造抖动。我们一般把阈值设成正常最大静默间隔的两倍以上,同时把这个时间窗口做成可配置,方便根据不同市场调整。
真正要触发切换的另一个信号,是序列号持续发生异常,比如间隔频繁跳变、重复包比例过高。链路数据质量比连接可用性更重要,我觉得可以把稳定性监控做得比连接监控更重。
3.4 内部下发链路同样不能有单点
接入层的双活只是解决了“上游到网关”这一段,如果网关把行情单线程地推给一个消息中间件,中间件崩了,下游照样全部瘫痪。我们要把实时链路想象成一条水管,中间任何一个阀门坏了都会断流。
实践上,我把内部实时分发拆成两条路径:一条是非常轻量的内存/组播分发总线,给延迟敏感的核心业务用;另一条是偏持久化的异步通道,用来做落库、回放、监控统计。实时路径不经过磁盘,避免持久化拖慢主链路;持久化路径丢几条实时性要求不高的消息没关系,但绝不能阻塞实时路径。
在具体的部署上,接入网关至少两个实例,彼此独立;下行分发的多个消费者也采用订阅组模式,任何一个消费者处理不过来,不应该拖垮其他消费者。这里需要给每个消费者设置独立的缓冲队列,并配合丢弃策略或者降级策略,而不是让一个慢消费者占满整个队列,影响其他业务。
4. 数据源选型:这是最容易被低估的环节
协议选定了,高可用架构有了雏形,最后卡住项目进度的往往是数据源。数据源选得不好,再好的系统也等于建在沙地上。而且数据源不像协议或架构那样可以比较直观地测试,很多质量问题要跑一段时间才能暴露。
4.1 不同等级数据源的核心差异
我把行情数据源从质量和成本两个维度上分成几类,功能上也有明显区别。
| 数据源类型 | 优势 | 劣势 | 适用场景 |
|---|---|---|---|
| 交易所等第一方直连源 | 延迟最低,字段最全,最接近真实撮合结果 | 接入门槛高、成本高、运维复杂,可能需要专线或特定机房部署 | 高频交易、自营策略、对真实性和深度要求极高的场景 |
| 第三方整合行情源 | 接入方便,通常提供多种访问方式,一条链路能覆盖多市场 | 中间环节多,延迟更高,部分字段可能被处理过,不具备完整深度 | 绝大多数常规业务系统,对延迟要求不是极致的场景 |
| 免费或延迟行情源 | 零成本,开发调试友好 | 延迟、完整性、稳定性都无法保证,还可能存在授权限制 | 开发联调、离线回放、演示环境,不建议直接用于生产核心链路 |
这里要特别提醒,别把第三方整合源当成第一方源来设计系统。部分第三方源会对原始行情做加工,比如为了降低带宽丢弃部分逐笔成交、延迟几分钟统一推送、或者改变字段语义。如果业务逻辑对这些加工不知情,很容易出现策略问题。最稳妥的方法是在接入前问清楚:能不能提供字段说明、能不能提供原始序列号、有没有历史回放接口。
4.2 评估数据源质量的四个指标
我收到一个新的数据源接入申请,会重点看四件事。
第一是事件完整率。选择某段已知的高波动时段,统计期望应该收到的行情事件数和实际接收到的数量。如果完整率长期低于某个阈值,说明数据源侧在源头就有丢弃,而不是我们自己链路的锅。
第二是序列连续性。持续观察源序列号,统计正常时段和峰值时段的跳变次数。一个成熟的行情源,在正常网络下序列号应该是连续的,偶尔的间隙要能从补数接口补齐。
第三是时间戳稳定性。比较每个事件携带的原始事件时间和本地接收时间之间的差值分布,重点关注 P99 和最大抖动。如果原始时间戳和接收时间戳之间的差值频繁大幅波动,这也许说明数据源侧在集中发送缓存,也许是自己网络的带宽被占满,总之需要追查。
第四是字段正确性。要重点验证快照字段是否与底层的成交回报逻辑自洽,比如最新价是否总落在当日涨跌停范围内,成交量是否为单调非递减。如果有任何逻辑不成立,说明上游数据源内部可能已经做了不正确的拼接或清洗,这种源直接用会非常危险。
4.3 多源合并时的仲裁和脏源隔离
很多系统会接入两个甚至更多的行情源来做交叉验证。双源的好处是当主源故障时还能切换到备源,但多源用起来也有大坑:当两个源给出的价格不一样时,该信谁?
我见过一些比较激进的实现,会实时比较两个源的数据,哪个源和另一个源偏差大了就自动切换。这在个别场景下能工作,但在波动极端剧烈时,两个正常的数据源也可能因为各自内部处理链路不同而产生短暂的差异。如果自动切换逻辑太灵敏,会造成系统在两个源之间反复横跳,这种抖动可能比单一源故障更影响下游。
我当时更偏向的策略是:正常情况固定使用一个主源,另一个辅源只用于一致性校验和告警。当主源和辅源在某个标的上的差异超过阈值时,不自动切换,而是先把主源的数据状态放到“观察”阶段,同时检查主源通道的序列号、时间戳和字段完整性。只有确认主源确实存在持续异常,才在冷静时间过后切换到辅源。
脏源隔离也很重要。一个数据源可能在大多数标的上都正常,唯独某一类标的数据错乱。你不能因为某一个标的出问题就整个切换,而应该支持按标的维度做降级或隔离。比如某个标的的源异常,可以针对它单独切换到备用源,其它标的不受影响。
5. 压测和排障中的真实经验:这几个问题会让你上线当夜睡不着
无论前面文档写得有多完善,最后真正让你成长的永远是压测阶段和上线后的排障。我觉得有几类问题是经常会出现的,提前了解能少踩很多坑。
5.1 序列号回退:可能意味着上游重启了,不一定只是乱序
我们在联调时遇到过这个怪异场景:某路行情源一直正常,突然之间序列号从一万多直接跳回了一百多,而且后面的时间戳和行情数据都是新的。最初我们的接收模块把它当作乱序包处理,准备缓存等待后续大序列号,结果等了好几分钟也没有等到,反而把下游的全部数据卡住了。
后来仔细翻日志才发现,序列号回退是因为数据源侧做了一次主备切换或进程重启,新的会话是从一个小序列号重新开始的。如果继续拿旧会话的“期望序列号”去校验新会话的数据,就会永久性误判。
为了应对这种情况,我们给网关增加了一个“源会话标识”概念。每个数据源连接在建立时会分配一个会话 ID,当接收到的会话 ID 变了,网关就知道这是一个新的数据源会话,会立刻丢弃之前缓存的乱序窗口,并且主动向下游通知“当前数据处于新旧会话切换状态”。如果是增量数据无法对齐,就需要马上申请新的全量快照。
这也是为什么我特别强调,协议转换层必须保留原始源的会话标识和序列号。没有源会话标识,这种序列号回退的问题会变得无法排查。
5.2 时间戳比较的时钟源不确定,导致延迟监控失真
实时行情系统的监控指标里,延迟统计是核心中的核心。大部分延迟统计要依赖事件携带的时间戳和本机接收时间作差,但这里面有一个特别容易被忽略的前提:上游事件时间戳和本机时间必须来自同一个可信时钟。
我们最初接入两个不同行情源时,直接比较“行情源事件时间”和“本机收到时间”的差值来评估链路延迟。正常情况还挺准,但到了某个时间段,发现几乎每一条消息的延迟都在飙升,可是业务数据分析起来又好像没有明显延迟。排查了很久才发现,是其中一台行情源服务器的时间出现了漂移,NTP 同步不到位,导致它发出的时间戳比真实时间慢了将近 20 秒。
后来我们统一调整了策略:所有延迟指标一律使用“本机入口时间戳”作为基准,也就是消息进入自己接入网关的瞬间,立即打一个本地高精度时间戳,后续所有延迟计算都基于这个入口时间。行情源携带的原始事件时间,只用于业务上的时序判断,不用于链路延迟的即时告警。
5.3 压测要回放真实行情,不要用均匀流量模拟
压测中最容易犯的错误是用一个固定的速率去压,比如每秒 5000 条、10000 条,数据均匀地发过去。这种流量结构太“听话”了,根本无法触发行情系统在真实环境下的突发队列、线程调度竞争、GC 和网络拥塞等问题。
更可靠的方案是用历史行情做回放。把某个高波动时段录下来的真实 tick 流原样重放,甚至可以故意把某些突发的流量再放大几倍,看看系统的队列积压情况。压测期间重点观察的不是平均延迟,而是 P99 甚至 P99.9 延迟有没有突然抬高,以及队列堆积到多少时开始触发背压或丢弃策略。
压测过程中我们还会在网络层注入各种故障:随机丢包、乱序重排、延迟抖动、TCP 重连、源服务器重启。每个故障注入后,都要检查系统是否触发了正确的切换逻辑,切换过程花了多长时间,下游有没有出现明显的数据空洞。这种事前演练,比上线出问题后再手忙脚乱地排查要有效得多。
5.4 监控看板应该盯住哪些信号
最后聊聊监控。实时行情系统的监控不能只停留在“进程是否活着、CPU 是否超过 80%”这种层面,至少要把下面几类信号放在同一个看板上:
- 每秒事件数和每秒字节数:能让你快速判断当前流量状态是否处于峰值,也能用来对齐上游故障时间点。
- 最近处理序列号与数据源最新序列号的差值:这个值一旦持续增长,说明消费速度跟不上生产速度。
- 链路延迟 P50 和 P99:能反映整体链路是否存在劣化。
- 队列长度和处理线程调度延迟:定位是否出现处理线程被卡住或循环等待。
- 全量重建次数和补数请求次数:如果次数异常增加,说明链路稳定性或者网络质量出了问题,需要引起警觉。
- 心跳超时误报次数:可以判断健康检查阈值是否合理,避免不正常的反复切源。
这些指标如果分散在不同系统里,排查问题时会非常痛苦。最好是统一采集到一个时序数据库,用同一个时间轴展示,才能把不同层级的异常关联起来。
实时行情系统的难点,其实不只是某一个技术点,而是协议、架构、数据源这三层必须一起考虑。我们经常只在协议上较真,却忘了看看数据源本身可不可靠;只把双机热备做得花团锦簇,却忽略了切换后序列号是否能连续。我个人的体会是,再花哨的设计都不如先把链路里每一个环节的故障模式列清楚,然后一条一条去压测和演练。如果能够做到断线自动恢复、序列号不重不漏、源切换不打断下游,这个系统就已经能扛住绝大多数真实环境里的麻烦了。
