写量化系统的人,十有八九都遇到过这种场景:策略明明在跑,盘中信号也好好的,结果执行链路某一段悄悄断了;要么是行情源突然不再推送,要么是券商API超时连不上,要么是数据库连接池被慢查询拖死。更麻烦的是,订单已经报出去了,但回报一直不来,这一刻你根本不知道这笔单子到底是成交了、挂住了、还是根本没发出去。手忙脚乱查半天,发现行情已经落后一大截,仓位也乱了。前几天看到“Codex重连5次”相关的讨论,很多人在吐槽同一个会话反复掉线的问题。好笑的是,把“Agent会话语义丢失”和“量化执行链路订单状态未知”放在一起看,底层其实是同一个问题:分布式系统里,某个环节失去联系之后,你靠什么判断它的状态,又靠什么把它安全地拉回来。
这篇文章想聊的,就是量化交易执行链路里的熔断与重连设计。我最早接触这套思路,是在研究工业控制系统的时候。工业现场有一整套成熟的容错工程方法——看门狗、联锁保护、自动重合闸、计划性停机——看起来跟金融IT八竿子打不着,实际建模逻辑惊人地一致。把标换掉,把“继电器”换成“订单状态机”,把“DCS控制器”换成“策略进程”,很多东西可以直接平移过来用。这篇文章会从最底层的设计思路讲起,逐步落到具体代码、参数和排查方法,适合正在做量化系统开发的工程师、维护自研策略框架的量化研究员,以及想理解“执行链路稳定性”这件事的个人交易者。
1. 量化执行链路,本质上是一套“控制回路”
1.1 从传感器到执行机构:一套几乎逐点对应的映射
工业控制系统的典型结构是:传感器采集现场状态,控制器读取状态后按算法计算输出,执行机构执行动作,动作引发状态变化,传感器再次把变化反馈给控制器。这套闭环回路听起来很自然,但它有一个隐含前提——所有环节之间的通信必须在规定时间内完成。一旦信号线断了,哪怕算法再优秀,系统也是一台“眼睛被蒙住的机器”。
量化交易系统几乎完全复刻了这套结构。行情源就是“传感器”,持续采集市场数据;策略引擎是“控制器”,根据行情计算信号;交易接口是“执行机构”,把订单报给券商或交易所;账户持仓和订单回报则是“反馈信号”,告诉策略刚才的动作产生了什么效果。每一层之间都有网络通信,都有超时和失败的可能。理解了这层同构关系,就很容易接受一个结论:量化系统的稳定性问题,本质上就是控制回路的通信质量问题。
我见过不少人把行情、交易、数据这些链路当成独立的IO模块来看待,觉得“断了我重连不就行了”。但工业控制领域几十年前的实践早就证明,这种思路过于天真。链条越长,故障传播越快,一个环节失效会迅速导致整个回路的控制失真。所以,工业系统从一开始就把“通信可能中断”当成了设计前提,而不是异常情况来对待。
1.2 量化系统里真实的故障形态,远比想象中频繁
很多人以为生产环境里的故障都是“大事故”,是那种直接把系统干到崩溃的事件。实际上我在几个自营盘团队里看到的故障形态,绝大多数都特别“平民化”。
行情链路最常见的故障,是WebSocket连接半开。TCP连接没有收到FIN包,但从应用层看已经不再推送数据了。客户端以为连接还活着,实际服务端早把会话清了。这种故障在加了负载均衡或者走了代理的网络里尤其常见,一台代理服务器重启,就能让几十个行情连接同时变成半开状态。
交易链路的典型故障是超时之后的状态不一致。API请求发出去了,客户端等不到响应就报错,但券商端可能已经收到并处理了这笔请求,订单可能已经挂在市场上。这种场景下,你如果在超时后立刻重新下单,就有可能重复建仓,造成仓位翻倍。这跟“查询重试”不是一回事,属于必须特殊处理的语义模糊问题。
数据持久化链路则经常栽在连接池上。行情高峰时瞬时写入量大,某个慢查询把连接池占满,所有数据库写入排队,队列越积越长,最终触发客户端超时。数据库服务其实没坏,但业务侧的表现就是“链路全断”。
这些故障形态,放到工业控制里分别对应:传感器断线、执行机构反馈丢失、控制柜内通信拥堵。老工程师的做法不是祈祷它不发生,而是专门设计一套机制去检测、隔离、恢复。这套机制,落到量化系统里就是我们说的“熔断”和“重连”。
1.3 为什么“人盯着”这个方案注定不可行
我早期做策略回测框架的时候,团队也讨论过要不要做熔断,当时有个说法是“系统故障了人看着处理就行”。后来一场实盘夜训直接把这个想法否掉了——凌晨两点多,行情源静默了十几秒,等告警出来,数据已经缺了一段,策略基于不完整数据算出了信号,自动报单,然后持仓对不上。人盯着的速度,根本不可能跟上系统内部毫秒级的衰减过程。
这里要算一笔账:一条量化执行链路上,行情、策略、风控、交易、持仓同步五个环节,每个环节每毫秒都在交互。任何两个环节之间发生一次异常,处理得当可能只需几秒钟,但人从“感知异常”到“定位问题”到“采取动作”,至少需要几分钟。在这几分钟里,系统还在按错误的输入继续运行,错误会被持续放大。所以“自动化容错”不是可选项,而是系统能在无人值守环境中运行的基本条件。
这也是为什么我会一直强调,“熔断”这个词在量化语境里,应该被理解为“工业控制系统里的故障净化”。它的目标不是让系统停下来,而是让系统在局部失灵的时候,通过隔离失能部分,保住整个回路的可控性。
2. 熔断:是在对的时候敢于“停下来”
2.1 不是所有故障都应该触发熔断
聊熔断之前先得明确一点:错误的熔断和没有熔断一样危险。如果把所有错误都当成熔断条件,系统会频繁进入保护状态,策略无法正常交易,反而制造更多问题。所以设计熔断条件之前,先把错误分好类。
我习惯把执行链路里的错误分成四类。第一类是连接/通信类错误,典型如连不上行情源、API超时、socket断开,这类错误属于基础设施不稳定,值得触发熔断。第二类是业务校验类错误,比如下单参数被拒、资金不足、持仓超限,这类错误通常是指令本身有问题,和链路健康度无关,不应该触发链路熔断,而应该触发策略暂停。第三类是数据质量异常,比如行情序号跳变、数据延迟超过阈值、买卖价倒挂,这类问题说明“数据不可信”,需要触发该数据源或者策略级的熔断。第四类是未知异常,属于兜底分类,通常会在连续出现时触发熔断。
我踩过的坑是在早期版本里,把“任何异常都熔断”写成了默认行为,结果某天券商接口返回了一个正常范围内的参数调整提醒,被我方代码当成异常计入了失败次数,直接把整条交易链路熔断了十分钟。盘中十分钟意味着什么,做交易的人都明白。所以现在我会把错误分类做成独立模块,每类错误走不同的处置动作,这个设计优先级很高。
2.2 断路器模式:从一个状态机说起
熔断机制在软件工程里早就有成熟实现,就是断路器模式。很多人第一次听说断路器概念,是在微服务架构的容错设计里,但它的思想源头恰恰是电力工程里的“断路器”——电流超过额定值时自动跳闸,保护后面的电路不被烧毁。把这一点平移到量化系统里,逻辑几乎是一比一的:连续失败次数超过阈值,就打开断路器,后续请求直接快速失败,不再继续尝试击打已经有问题的依赖。过一段时间,断路器进入半开状态,放一两个请求进去探测,如果成功就关闭断路器,恢复链路;如果失败就重新打开,继续等待。
这个状态机的价值在于,它把“要不要继续请求”的决策从业务代码里剥离出来,做成一个集中式的链路健康判断。策略代码只需要去询问“交易链路是否可用”,而不用自己去跟踪最近多少次失败。下面是我在自研框架里常用的一个精简实现,注释写得尽量详细:
import time import threading class CircuitBreaker: def __init__(self, fail_threshold=5, reset_timeout=30, probe_requests=2): self.fail_threshold = fail_threshold # 连续失败多少次后熔断 self.reset_timeout = reset_timeout # 熔断后等待多少秒进入半开 self.probe_requests = probe_requests # 半开状态下放行多少探测请求 self.failure_count = 0 self.state = "CLOSED" # CLOSED / OPEN / HALF_OPEN self.opened_at = 0 self.probe_remaining = 0 self.lock = threading.Lock() def allow_request(self) -> bool: with self.lock: if self.state == "CLOSED": return True if self.state == "OPEN": if time.time() - self.opened_at >= self.reset_timeout: self.state = "HALF_OPEN" self.probe_remaining = self.probe_requests return True return False if self.state == "HALF_OPEN": if self.probe_remaining > 0: self.probe_remaining -= 1 return True return False return False def record_success(self): with self.lock: if self.state == "HALF_OPEN": self._reset() elif self.state == "CLOSED": self.failure_count = 0 def record_failure(self): with self.lock: if self.state == "HALF_OPEN": self._open() elif self.state == "CLOSED": self.failure_count += 1 if self.failure_count >= self.fail_threshold: self._open() def _open(self): self.state = "OPEN" self.opened_at = time.time() self.failure_count = 0 def _reset(self): self.state = "CLOSED" self.failure_count = 0 self.opened_at = 0 self.probe_remaining = 0这段代码虽然简单,但把几个重要决策点都体现出来了:熔断打开后不再接受任何请求;半开状态只放固定数量的探测流量;半开状态下只要失败一次就立刻回到打开状态。实践中我还会再加一个“失败率窗口”统计,用最近100次请求中失败占比判断,而不是只用连续次数,这样可以避免“偶发失败未连续、但整体失效率已经很高”的情况漏网。
2.3 熔断的粒度:不是“整个系统一刀切”
熔断设计里另一个高价值问题是粒度。这里很容易犯“全局熔断”的错误——行情出问题,把整个策略进程的执行也停了,那交易端也跟着受影响,本来能正常交易的路径也被切断了。工业控制系统里,联锁保护设计讲究的是“精准隔离故障回路”,只切断有故障的部分,保留健康部分继续运行。量化系统也应该这样做。
我把熔断粒度分成五层来设计。第一层是数据源级别,对应每个行情通道独立熔断,一个交易所行情断了不影响另一个。第二层是策略级别,某个策略依赖的数据链路被熔断,只暂停这个策略的自动交易,其他策略继续跑。第三层是交易接口级别,比如某个券商API处于熔断状态,所有发往这个接口的订单都会被拦截,但发往其他券商的订单不受影响。第四层是订单级别,针对“状态不确定”的订单本身做熔断,也就是不追单、不重复撤报,等待状态恢复确认。第五层是全局保护,当多个关键链路同时处于异常状态,或账户权益出现超阈值的回撤时,触发全局暂停,这相当于工业系统里的“紧急停车”。
每一层熔断的动作也不一样。拿数据源熔断来说,动作是切换备用行情源,而不是停止策略。拿交易接口熔断来说,动作是暂停新订单,但允许撤单。拿全局保护来说,动作才是平仓和停止所有策略。如果级别切错了,比如行情源出问题直接触发全局平仓,那当天盘口正常恢复的时候,你已经躺在地板上了。这些细节需要在设计阶段就明确下来。
2.4 熔断参数怎么定:经验值加动态适配
参数设计是熔断机制里最容易引发争论的部分。阈值设得太小,系统容易过敏,动不动就进入保护模式;设得太大,熔断形同虚设。下面这组参数是我在多个实盘框架里验证过的初始值,可以作为参考,但一定要根据自身策略频率和链路特点做调整。
- 连接类错误的连续失败阈值:5次,时间窗口1分钟。如果行情API在1分钟内连续5次握手失败或心跳超时,基本可以确认链路有问题。
- 业务类错误的连续失败阈值:3次,时间窗口1分钟。业务类错误通常比连接类错误更严重,说明指令本身可能有缺陷。
- 熔断打开后的最短等待时间:30秒到2分钟之间。低频策略可以设长一些,高频策略要短一些,因为高频策略错过太长时间段,基本等于错过整段行情。
- 半开状态探测请求数:2到3个。太少容易一次成功就误判恢复,太多又失去了试探的意义。
- 半开状态下失败的重新熔断阈值:1次。已经半开了,说明故障刚发生过,只要失败一次就应该足够谨慎。
这些参数我一般会放到配置中心里,支持运行中动态调整。熔断机制是给“异常状态”用的,而异常状态本身可能是未知的,参数如果只能改代码再重启进程,就失去了调试空间。实盘中我会先给一个偏保守的初始值,然后根据两个星期的故障统计重新校准。
3. 重连:重试不是目的,一致性恢复才是
3.1 Codex重连的启示:表面是网络问题,实质是状态同步问题
写这一段之前,先绕回那个热词——“Codex老是重连,重连5次才能恢复”。很多人在讨论里把它当纯粹的客户端网络问题,但其实频繁重连背后往往隐藏着会话状态丢失的问题。重连一次成功,但之前的上下文没了,模型要重新理解你的任务;再重连一次,又丢了一部分上下文。这种“物理连接恢复了,但逻辑状态不同步”的现象,在量化系统里更致命。
行情连接断了,重新连上,如果不做数据补偿,策略看到的就是一段缺失的K线,均线指标会算出一个偏移值。交易连接断了,重新连上,如果不主动查询之前的订单状态,就永远不知道那笔单子到底成交没有。数据库连接断了,重新建连,如果缓存里还有未落库的订单记录,恢复之后不补写,那这笔订单就彻底从系统里消失了。所以重连的核心命题只有一句话:网络链路可以丢,业务状态不能丢。这就是“一致性恢复”要做的事。
3.2 重连状态机:别把“重连”做成“重试”
有了上面这个判断,重连就不应该被实现成一个简单的循环重试,而应该设计成完整的状态迁移过程。我习惯把重连过程拆成五个状态:已连接、已断开、退避等待、重连中、恢复同步。每一步都要有明确的触发条件和超时边界。
- 已连接状态:正常工作,心跳持续更新,收到断开事件后进入已断开。
- 已断开状态:停止向该链路上层业务提供数据,标记链路不可用,开始准备重连。
- 退避等待状态:按指数退避策略等待一段时间,避免在故障未恢复时高频撞击服务端。
- 重连中状态:发起握手、鉴权或者订阅,如果失败返回退避等待,如果成功进入恢复同步。
- 恢复同步状态:执行补数据、状态对齐、幂等校验等逻辑,完成之后回到已连接状态。
这里有一个容易被忽视的要点:重连中失败不应该从头开始计数,而应该保留退避时长。如果每次失败都重置重试间隔,等于在接口尚未恢复的时候开启了猛烈的“重试风暴”,很可能把本来就在恢复边缘的接口直接打垮。我在生产环境里给重连加过一个上限,比如最多连续重连10次,超过后进入“死信”状态,需要人工介入或者依赖备用链路,不在原链路上继续消耗资源。
3.3 三类链路的“恢复同步”动作,各有什么讲究
行情链路的恢复同步,核心是“数据连续性”。断开的几十秒里市场经历了什么,必须想办法补回来。如果行情源提供了历史区间拉取接口,那第一时间按断流时间点拉取区间数据,把缺口填平;如果不提供,就向下游发出一个“数据缺口”告警,并让策略侧的信号计算基于现有数据做保守处理,避免用不完整数据算出激进信号。这里还要检查序列号连续性,行情数据通常带递增序号,如果恢复后发现序号有跳变,即使时间连续也不能直接信任,建议重新对齐一次。
交易链路的恢复同步,核心是“订单状态对齐”。重连成功后的第一件事不是继续执行新任务,而是把之前所有“状态未确认”的订单逐笔向券商发起查询,拿到当前状态,更新到本地账本。这一步绝对不能省,因为券商端和本地端的订单状态可能完全不一样。曾经有一次我们标的券商主动断开了连接,恢复后没有主动查询就把本地缓存里状态为“已提交”的订单当作活单处理,结果那笔单子实际已经成交,导致策略在计算可卖持仓时高估了真实持仓,差点触发超卖。
数据库链路的恢复同步,核心是“未落库数据补偿”。连接池重建之后,检查事件缓冲表里有没有状态为“写入中”或“写入失败”的记录,逐条重新落库,同时做幂等保护,避免重复写入。可以给每条写入记录一个唯一业务ID,数据库层面做唯一索引,这样重试多少遍都是安全的。这个经验在交易记录、策略日志、风控快照这几类数据上特别有用。
3.4 退避策略:用指数退避加抖动,避免重连风暴
重连频率设计不合理的系统,容易出现一种很滑稽的故障:行情服务端还在正常恢复中,客户端已用每100毫秒一次的频率疯狂重连,把服务端的半连接队列打满,反而阻断了正常连接。这和工业控制系统里UPS重启后,一堆设备同时上电导致配电柜过载的逻辑一模一样。
解决这个问题的标准做法是指数退避加随机抖动。首先设置一个基础退避时间,比如1秒;每次失败后等待时间翻倍,直到某个上限;每次实际等待时间再叠加一个随机偏移量,比如10%到30%的随机抖动。为什么要有抖动?因为如果一堆客户端都按精确的指数退避时间重连,它们会在同一时刻集体冲击服务端,加入随机抖动可以把重连峰值摊平。
用伪代码表示大概是这个逻辑:
def reconnect_delay(attempt: int, base_delay: float = 1.0, max_delay: float = 60.0) -> float: delay = min(base_delay * (2 ** attempt), max_delay) jitter = delay * random.uniform(0.1, 0.3) return delay + jitter另一个细节是,重连期间仍然要持续监听“链路是否恢复”的信号,不能干等退避。某些协议支持随时收到推送通知,一旦收到,可以提前结束退避状态,直接发起重连。这在行情链路里很实用,因为行情源的恢复信号往往是第一帧正常推送,比探测请求更早知道链路已经回来了。
4. 工业控制系统留给我们的系统工程方法论
4.1 看门狗:系统“活着”的定义是什么
工业控制系统里有个很基础的概念叫看门狗,本质上是一个独立运行的定时器,主系统需要周期性地去“喂狗”;如果超过时间没有喂,看门狗就判定主系统已经死机,自动触发复位或者切换到备机。这套机制的巧妙之处在于,它定义“系统活着”的方式不是“进程在跑”,而是“关键任务在按预期节奏被完成”。
把这个思路引入量化系统,就有了一件值得做的事:定义清楚“链路健康”的判定标准。连接存在不等于等同于链路可用。一条行情连接,即使TCP状态看起来是ESTABLISHED,如果连续N秒没有新的行情数据到达,那么对于交易系统来说,这个链路就是“死了”。同样,交易接口的鉴权token可能还有效,但如果最近的订单回报延迟超过了阈值,管理端就应该标记这个接口为亚健康状态。
我落地这个设计时,会在每一条关键链路上同时维护两个指标:上次活跃时间和累计无数据时长。看门狗线程每隔固定周期检查一次,两个指标任何一个超限,就触发对应的熔断或重连流程。这里的阈值参考值我会按链路本身的重要程度来定:行情毫秒级延迟的系统,3秒没有数据就必须怀疑断流;下单链路如果5秒没有回报,就已经需要向交易员推送告警了。
4.2 联锁保护:条件不满足,动作就不能执行
工业控制里的联锁保护,简单说是一套相互制约的逻辑:某些操作只有在特定条件满足时才会被允许执行,如果条件不成立,即使操作指令已经发出,执行机构也会拒绝动作。电梯不会在门没关的情况下运行,高压开关不会在检修接地线没拆的情况下合闸,都是联锁保护的典型应用。
映射到量化执行链路,联锁保护就是交易动作的前置校验链。设计一套联锁条件:最新行情时间戳必须晚于某个阈值,否则行情数据系统不可信,不允许产生新信号;订单簿和账户权益必须能对齐,仓位不能被重复累计;风险检查必须全部通过,有任何一个风控指标处于熔断或未知状态,新订单就需要被拦截;策略版本必须与当前运行的配置一致。任意一条联锁条件不满足,下单动作就被强制阻断,而不是由策略代码自行决定要不要跳过检查。我在框架里把联锁逻辑做成独立的代理层,放在策略引擎和交易接口之间。策略引擎想要发单,必须穿过这一层,没有任何绕过通道。这在系统故障时价值极大,因为人在紧张状态下很容易写出各种workaround,而联锁层铁面无私。
4.3 冗余设计与降级策略:故障发生时,系统还有Plan B
工业系统做可靠性的核心手段是冗余。传感器有双重冗余、控制器有热备、电源有双回路。但冗余不是简单的“多一套一样的东西”,而是要在主路断掉之后,备路能立刻无缝接管,并且系统知道该往哪条路切换。
量化系统里,冗余设计最常见的形态是备用行情源。一个交易所同时提供多个行情通道,或者有两家数据供应商的行情流,可以做到主行情断流时自动切换备源。但这里有个坑:两个行情源的数据格式、时间戳基准、订阅协议都可能不一样,切换后下游的数据解析逻辑要能兼容,否则就会出现“链路通了,但数据解析报错”的尴尬状态。所以备用行情源不能只做连接层冗余,还要做格式适配层冗余。
交易链路的冗余相对复杂,因为主流券商通常不允许多个客户端同时登录同一个资金账号。这种情况下,冗余方案只能是降低动作级别,比如保留一个手工下单通道,系统出现交易API异常时,至少可以通过人工操作完成应急交易。还有一个降级策略是在“系统自动交易”和“系统只提供信号、人工执行”之间切换,链路不稳时,让系统变成信号推荐器,人来做最终决策。这个降级动作看起来简单,却需要系统在架构上就支持,而不是临时靠人盯着屏幕手动模拟。
4.4 计划性故障演练:让熔断与重连在真正故障前先“彩排”一次
工业控制领域有一个我特别欣赏的做法:计划性停机检修。他们不会等到设备真正坏了才去修,而是定期主动停机,把所有保护装置、冗余链路、切换逻辑都测试一遍,再恢复生产。这种“主动制造故障”和量化系统里的混沌工程是一个逻辑。
很多量化团队做得最差的一环,恰恰是没有演练过自己的熔断和重连机制。新系统上线三个月,一次故障没发生,就默认机制是好的。实际真出故障时才发现:告警发到了没人看的群里,备用行情源根本没配权限,重连后自动补数脚本报了错,或者半开状态的探测请求被鉴权逻辑挡住了。这些问题不经过演练根本发现不了。
我现在会在每次发版之后,主动做一轮故障演练,方式很直接:手动断开行情源,观察策略是不是按预期进入了保护模式;手动杀掉交易连接,观察系统是否会正确地查询未确认订单;手动连数据库都不让访问,观察链路是不是会在熔断后自动恢复;最后再人工注入一个慢接口,观察超时链路的处理逻辑。每次演练都当成真实事故来跑,记录问题、修复、再演练。这套做法看起来笨,但几次之后系统能扛住的故障类型会显著增加。
5. 落地路线:从第一行代码开始,建立你的执行链路防御
5.1 第一步:盘点链路,画出你的“故障影响面”
做熔断与重连设计的最开始,不该是写代码,而是做一份“链路地图”。把系统里每一个重要连接画出来,标注它依赖什么、失败后会影响哪个环节、影响范围有多大。我会用一张表格维护这个信息,形式类似这样。
| 链路名称 | 依赖服务 | 主要故障模式 | 影响范围 | 处置优先级 |
|---|---|---|---|---|
| 实时行情A | 行情网关 | 断流、数据延迟、序列跳变 | 依赖该数据的策略信号失真 | P0 |
| 交易下单接口 | 券商API | 超时、连接中断、重复回报 | 订单状态未知、无法下单 | P0 |
| 持仓同步服务 | 账户系统 | 同步延迟、数据不一致 | 仓位判断错误 | P0 |
| 策略参数配置中心 | 配置服务 | 连接超时、版本不一致 | 策略加载旧参数 | P1 |
| 日志持久化服务 | 数据库 | 连接池耗尽、写入延迟 | 运行记录丢失、审计缺失 | P1 |
| 告警通知服务 | 消息通道 | 消息丢失、通道被限流 | 告警无法触达 | P1 |
做完这张表,就能分清哪些链路需要重连都字字珠玑,哪些链路只需要做瞬时容错。比如策略参数配置中心断了几分钟,影响有限,完全可以等它重连后重新拉取;但行情链路断几秒就会产生信号失真,必须立即处理。没有这张表,代码怎么写都会出现“一把抓”的问题。
5.2 最小闭环实现顺序:先能感知,再能动作,最后能恢复
熔断与重连不是一上来就要写一整套框架,建议按最小闭环的节奏推进。
先做监控与告警。所有链路先埋点,记录连接状态、最近活跃时间、请求耗时和错误计数。这个过程不改变任何行为,只解决“知不知道出了问题”的问题。我自己踩过教训:如果告警没到,熔断机制设计得再好也没有意义,因为很多时候系统还能自动恢复,你根本不知道它在鬼门关前转了一圈。
再实现熔断。按前文说的五层粒度,先做最关键的行情链路和交易链路,每层都从断路器状态机开始,叠加错误分类。熔断动作先做成“只告警不阻断”,观察一段时间,确认判断本身不会误伤正常交易,再改成“告警加阻断”。
接着实现重连。先做被动的重连,也就是连接断开后按退避策略自动恢复连接;再做主动的一致性恢复,比如重连后拉取数据补缺口、主动查询订单状态、补偿未落库记录。这一步需要把业务恢复逻辑处理好,比单纯重连要复杂得多。
最后做故障演练。每次新增或者修改熔断、重连逻辑,都要同步写一个对应的演练脚本,人工模拟故障,验证行为符合预期。
5.3 设计建议:配置中心、统一健康检查、链路可观测性
有几个系统性设计,能大幅降低熔断与重连的维护成本。
第一,所有熔断和重连参数应该进配置中心,不要硬编码在代码里。参数包括熔断阈值、时间窗口、探测请求数量、退避初始值和上限、最大重连次数。配置中心的作用不只是方便修改,更重要的是能按环境、按品种、按交易时段动态调整。比如夜盘流动性差,行情抖动多,可以放宽熔断阈值;日盘开盘抢单时段,熔断阈值要更灵敏。
第二,统一健康检查接口。每一个关键服务都要暴露一个健康检查接口,返回的不只是“进程活着”,还包括链路状态、数据活跃时间、最近错误摘要。监控系统周期性地调用这些接口,汇总成全链路的健康视图。有了这个视图,做问题定位时就不用一个模块一个模块去翻日志,能直接看到哪一段链路是红色。
第三,把熔断和重连行为本身纳入可观测性。每一次断路器状态变更、每一次重连成功或者失败、每一次补偿数据量的大小,都要作为关键事件记录下来。这些数据不仅用于事后排查,还可以用来优化熔断参数。比如通过历史事件分析发现,某个券商的API在每周三上午会有一个固定的短暂不可用窗口,那就可以在那个时间段预先调高阈值,而不是等故障发生了再被动处理。
这方面的可观测性方案很多,最终形式无所谓,重要的是事件要完整、有统一格式、可以按链路和状态维度检索。还有一个小建议:重连发生时要记录重连前链路已经失效了多久,这个“故障持续时长”指标比“重连次数”更有业务价值。
5.4 组合示例:断路器加重连机制如何一起工作
熔断和重连不是两个独立模块,而是彼此协同的关系。断开之后触发重连流程,重连过程本身也会产生失败,失败次数要反馈给断路器决策。下面给一个组合使用的最小示例。
class LinkHealthManager: def __init__(self, circuit_breaker: CircuitBreaker, reconnect_policy: dict): self.cb = circuit_breaker self.reconnect_config = reconnect_policy self.link_state = "DISCONNECTED" def attempt_operation(self, operation): if not self.cb.allow_request(): return LinkStatus.CIRCUIT_OPEN try: result = operation() self.cb.record_success() return result except LinkError as e: self.cb.record_failure() self._handle_link_disconnect(e) raise def _handle_link_disconnect(self, error): if self.link_state == "DISCONNECTED": return self.link_state = "DISCONNECTED" self._start_reconnect_loop() def _start_reconnect_loop(self): attempt = 0 while attempt < self.reconnect_config["max_attempts"]: delay = reconnect_delay(attempt) time.sleep(delay) try: self._establish_link() self._sync_resume() self.link_state = "CONNECTED" self.cb.record_success() return except LinkError: attempt += 1 self.cb.record_failure() self.link_state = "FAILED"真机上这个逻辑会被拆成异步任务来处理,但骨架思路是一致的:任何业务操作都要先问断路器要“令牌”;操作失败后记录到断路器,并触发链路断开处理;重连循环里执行建立连接和业务同步两个步骤,只有两者都完成才算恢复成功;重连耗尽后回到熔断保护,不再发起无意义的重试。
5.5 容易踩的五个坑,写到最后提醒一遍
熔断和重连的代码写起来不难,难的是细节。这五个坑我基本都在生产环境里遇到过,直接列出来供参考。
第一个坑是重连不设最大次数。没有上限的重连循环会让系统卡在“一直试图连接”的状态里,占用线程资源,同时掩盖更根本的问题。保险的做法是重连超过一定次数后彻底放弃当前链路,进入只读或者待机模式,等人工或上层调度介入。
第二个坑是重连成功后不执行状态同步。很多新手把“连接建立”当成“链路恢复”,实际上连接恢复和业务恢复是两回事。行情要补数据、订单要查状态、缓存要落库,这些步骤少一步,后续就会出现隐性数据问题。
第三个坑是熔断恢复参数太激进。半开状态下只放一个探测请求,成功就立刻完全恢复,这在故障刚发生时是过于乐观的做法。我建议半开期间至少连续成功两三次,再真正关闭断路器,对交易系统来说,宁可多等一秒确认稳定,也不要抢着一瞬恢复然后再次断掉。
第四个坑是熔断和告警不同步。断路器都打开了,告警还没发出去;或者告警发了一堆,让人分不清哪个才是核心问题。建议把告警分级,P0级事件对应全链路故障,必须电话和群消息同时触发;P1级事件发到值班群;P2级事件只记录不打扰。
第五个坑是只做程序内熔断,不做仓位熔断。系统层面的链路熔断只是第一步,真正的保护还体现在账户层面。当策略连续触发止损,或者权益回撤超过预设阈值时,应该有一道独立于交易链路的账户级关卡,强制暂停所有策略,而不是只靠链路层面兜底。
6. 最后说点个人体会
回国头来看,熔断与重连这套设计之所以让我觉得“从工业控制借来的系统工程”这个提法特别准确,是因为它背后有一个非常朴素的态度:承认故障会发生,并且提前为故障做好准备,而不是寄希望于“不出事”。这种思维方式在量化系统里很容易被忽略,因为大部分开发者的注意力都在策略逻辑和信号质量上,执行链路的稳定性反而成了“上线后再处理”的尾巴。
我个人的真实体会是,真正有价值的不是把重连做得多么快,也不是把熔断写得多么优雅,而是“知止”——系统知道自己什么时候应该停下来。这一点,很多技术人其实做不到。我们有追根究底、勇于重试的习惯,但工业控制里的老工程师反而更敬畏不确定性,他们知道在某些状态信息不足的时候,让系统停下来等待确认,是成本最低的选择。
如果你还在做一个新量化系统,我的建议很直白:先把这层执行链路防御做出来,再去优化策略绩效。一个年化比别人高5个点的策略,可能因为一次链路故障全部回吐;但一套可靠的执行链路,是你在真实市场里长期活下来的底座。这个顺序,别弄反了。