摘要
WebSocket 心跳正常、连接状态显示"已连接",但行情数据已经沉默了三分钟------这种"假活"状态比断线更危险,因为它绕过了所有常规监控。本文提出双层 watchdog 模式:连接 watchdog 盯通道交互,数据 watchdog 盯最后一条有效行情的时间戳。两者协作的状态机能在数据静默时触发告警,在连接断开后安全恢复------先校准当前状态,再恢复业务处理。文中以 TickDB 的 REST 快照、WebSocket 推送和 AI 工具入口为例,展示这套架构中每条通道的角色分工,并给出可直接验证的最小 watchdog 结构。
1. 心跳还在,数据呢?
这是一个在行情接入中反复出现的故障模式。
你们的实时行情服务跑了大半年,一直很稳定。某天下午,WebSocket 连接监控面板一片绿色------所有连接都在,心跳正常,没有断线日志。但策略模块的输出开始异常:信号频率骤降,部分品种的报价停留在三个小时前。
排查后发现,数据源在某个时间点之后不再推送 ticker 消息。不是连接断了------TCP 通道完好,协议层 ping/pong 照常往返。只是 data 帧不再到达。你的监控系统盯着连接状态,连接是绿的,于是它什么都没说。
这就是"假活":连接活着,数据死了。 它比断线更隐蔽,因为常规的"连接 watchdog"无法发现它。你需要第二层 watchdog------盯着数据本身的时间戳。
2. 为什么一层 watchdog 不够
传统的 WebSocket 监控只做一件事:检查连接是否存活。实现方式通常是心跳探测------定期发送 ping,等待 pong,超时判定断开。
这个模型假设"连接存活 = 数据正常流动"。在大多数即时通讯场景中,这个假设成立。但在行情推送场景中,有两个因素打破了它:
第一,收盘、休市或服务端静默期。 某些数据源在非交易时段会保持连接但不推送数据。此时连接 watchdog 看到的是一段正常的长连接------只是没有消息而已。
第二,服务端推送逻辑的静默故障。 更危险的情况:交易时段内,服务端因为内部错误、订阅状态丢失或队列阻塞,停止向特定连接推送数据。TCP 通道和协议层心跳完全不受影响。
这两种情况下,连接 watchdog 都会给出"一切正常"的假阳性判断。你的业务逻辑需要另一层 watchdog,直接检查数据的时效性。
3. 双层 watchdog 的状态机
把监控拆成两层,每层有明确的职责和独立的状态。
3.1 连接 watchdog(传输层)
职责:判断 WebSocket 通道本身是否可用。
输入信号:
- 协议层心跳(ping/pong 往返)
- TCP 连接状态变更(on_open / on_close / on_error)
- 连接空闲时间(超过阈值未收到任何帧)
状态 :CONNECTED / DISCONNECTED / RECONNECTING
盲区 :当状态为 CONNECTED 时,无法判断推送的 data 帧是否正在到达。它只监控通道,不监控内容。
3.2 数据 watchdog(业务层)
职责:判断行情数据是否在预期时间窗口内更新。
输入信号:
- 每条 ticker 消息到达时,记录
symbol和本地到达时间 - 周期性扫描:当前时间 - 每个 symbol 的最后一条数据时间
状态 :FRESH(在窗口内)/ STALE(超过窗口)
关键设计决策:数据 watchdog 不依赖连接 watchdog 的状态。 即使连接状态为 CONNECTED,数据也可能 STALE。反之,连接 DISCONNECTED 时,数据必然 STALE------但这个结论应该由数据 watchdog 自己得出,而不是从连接状态推导。
3.3 组合状态与恢复策略
两个 watchdog 的状态组合产生四种场景:
| 连接状态 | 数据状态 | 含义 | 动作 |
|---|---|---|---|
| CONNECTED | FRESH | 正常 | 持续监控 |
| CONNECTED | STALE | 假活:连接正常但数据超时未更新 | 触发业务告警,标记数据不可用 |
| DISCONNECTED | STALE | 连接断开,数据必然过期 | 触发重连,进入恢复流程 |
| RECONNECTING | --- | 正在恢复 | 重连成功后先校准,再恢复业务 |
恢复流程的核心原则:先校准当前状态,再恢复业务处理。 重连成功后,不等 WebSocket 推送第一条数据,立即通过 REST 快照拉取当前状态,作为断线窗口后的第一份可信数据。收到 WebSocket 推送后,对比快照确认数据流恢复,再将状态切回 FRESH。恢复期间,业务层标记为降级状态,不参与需要连续性的计算。
REST 快照在这里的角色是"当前状态的基准",不是"补齐断线窗口内的历史数据"。 断线期间的逐笔变化已经不可恢复。重连后的第一件事是确认"现在是什么状态",而不是假装没有断过。
4. 最小工程结构
下面给出双层 watchdog 的核心结构片段。这不是生产级完整实现,而是展示两层如何协作的状态机骨架。恢复流程中涉及的 REST 快照调用,在实际部署中需要一个持续可用的数据入口------本文第 5 节会以 TickDB 为例说明这个入口应该满足什么条件。
python
"""
双层 watchdog 核心结构(教学性代码片段)
展示连接监控与数据监控的协作状态机,不包含完整网络实现。
"""
import time
import logging
from enum import Enum
from dataclasses import dataclass, field
from typing import Dict, Optional
logger = logging.getLogger(__name__)
# ------------------------------------------------------------
# 状态定义
# ------------------------------------------------------------
class ConnState(Enum):
CONNECTED = "connected"
DISCONNECTED = "disconnected"
RECONNECTING = "reconnecting"
class DataState(Enum):
FRESH = "fresh"
STALE = "stale"
@dataclass
class SymbolWatch:
"""单个 symbol 的数据监控记录"""
symbol: str
last_ts: float = 0.0 # 最后一条数据的本地到达时间
max_age_sec: float = 60.0 # 超过此时间视为 STALE
@property
def state(self) -> DataState:
if self.last_ts == 0.0:
return DataState.STALE
return DataState.FRESH if (time.time() - self.last_ts) < self.max_age_sec else DataState.STALE
# ------------------------------------------------------------
# 双层 watchdog
# ------------------------------------------------------------
class DualWatchdog:
"""
连接 watchdog:外部通过 report_connected / report_disconnected 更新状态。
数据 watchdog:每次收到 ticker 数据时调用 feed(),定期调用 check() 扫描。
"""
def __init__(self, max_data_age: float = 60.0):
self.conn_state: ConnState = ConnState.DISCONNECTED
self.watches: Dict[str, SymbolWatch] = {}
self.max_data_age = max_data_age
self._degraded = True # 业务降级标记,启动时默认降级
# --- 连接 watchdog 接口 ---
def report_connected(self):
self.conn_state = ConnState.CONNECTED
# 不自动清除数据 STALE 状态------数据 freshness 由 feed/check 独立判断
def report_disconnected(self):
self.conn_state = ConnState.DISCONNECTED
self._degraded = True
def report_reconnecting(self):
self.conn_state = ConnState.RECONNECTING
self._degraded = True
# --- 数据 watchdog 接口 ---
def feed(self, symbol: str):
"""收到一条 ticker 数据时调用"""
if symbol not in self.watches:
self.watches[symbol] = SymbolWatch(symbol=symbol, max_age_sec=self.max_data_age)
self.watches[symbol].last_ts = time.time()
def check(self) -> Dict[str, DataState]:
"""周期性扫描所有 symbol,返回各自的数据状态"""
result = {}
for sym, watch in self.watches.items():
result[sym] = watch.state
return result
# --- 恢复流程 ---
def recover_with_snapshot(self, symbols: list):
"""
重连成功后调用。
通过 REST 快照拉取当前状态,校准数据 watchdog。
校准的是"当前状态",不是补齐断线窗口。
"""
logger.info(f"Recovery: snapshot calibration for {len(symbols)} symbols")
self._degraded = False # 快照确认后解除降级
@property
def is_degraded(self) -> bool:
"""业务层可读取此标记,决定是否参与连续性计算"""
return self._degraded
@property
def healthy(self) -> bool:
"""整体健康:连接正常 + 所有监控 symbol 数据新鲜"""
if self.conn_state != ConnState.CONNECTED:
return False
if not self.watches:
return False
return all(w.state == DataState.FRESH for w in self.watches.values())
结构要点:
report_connected()不自动清除数据 STALE 标记------数据 freshness 只由feed()和check()独立判断;recover_with_snapshot()在重连后通过外部快照校准当前状态,而不是等 WebSocket 自然到达;healthy综合两层状态,仅在连接正常且所有监控 symbol 数据新鲜时才返回True。
5. 三层架构的角色分工
双层 watchdog 需要两个数据入口:一个用于持续监控(数据 watchdog 的输入),一个用于断线后的状态校准(恢复流程的基准)。如果再加上 AI Agent 的工具调用入口,就形成了一个三层架构。每一层有明确的职责边界,不能混用。
这个架构需要数据源提供三种不同的接入方式,各自承担独立角色。 下面以 TickDB 为例,说明一个满足此架构的数据源应该具备的能力:
| 入口 | 角色 | 在 watchdog 中的位置 |
|---|---|---|
| REST 快照 | 按需获取当前行情状态 | 恢复流程的校准基准(recover_with_snapshot 调用) |
| WebSocket 推送 | 持续接收实时行情更新 | 数据 watchdog 的输入源(feed() 调用) |
| AI 工具入口 | Agent 按需查询行情的标准化工具调用 | 独立于 watchdog,共享同一数据源 |
为什么恢复流程需要 REST 快照: WebSocket 重连后,第一条推送数据可能延迟到达。如果等推送到达再恢复业务,这段时间窗口内的数据静默可能被误判为持续 STALE。REST 快照的作用是在重连后立即回答"现在是什么状态",让数据 watchdog 有一个明确的基准时间戳,然后 WebSocket 推送接过持续更新的职责。
为什么 AI 工具入口不替代 REST 快照: AI 工具入口(TickDB 提供 MCP 托管的行情查询工具)是请求-响应模式的工具调用,不是持续订阅通道。它适合 Agent 在推理过程中按需查询,但不适合作为 watchdog 的持续监控输入或恢复校准入口------后者需要明确的接口契约和可预期的响应结构。
三条通道各司其职:REST 负责校准,WebSocket 负责持续,AI 工具入口负责按需查询。混用其中任何一个角色,都会在特定故障场景下产生盲区。
6. 适合什么场景
双层 watchdog 不是银弹。它的价值取决于你的业务对数据时效性的敏感程度。
值得投入的场景:
- 实时行情服务,行情停顿直接影响下游策略或前端展示;
- 监控告警系统,需要区分"网络故障"和"数据静默"两种告警;
- 需要处理非交易时段静默与交易时段异常停顿的区分;
- 重连后需要明确的当前状态,而非依赖断线前缓存的场景。
简单脚本或偶尔查询不需要完整状态机: 如果你的使用模式是偶尔发起 REST 查询获取快照,没有持续 WebSocket 订阅,单层错误处理已经足够。双层 watchdog 的复杂度只在"连接可能活着但数据可能停了"的场景中有回报。
7. 你的行情监控,盯的是连接还是数据?
回到开头的事故模式。
如果你的监控面板只显示 WebSocket 连接状态,那"假活"已经埋在你的系统里了------它正在等待一个交易时段、一个静默故障、一个被服务端丢弃的订阅。当它发生时,连接 watchdog 会告诉你一切正常。
给实时数据流加双层 watchdog,本质上是承认一个简单的事实:连接和数据是两层东西,它们的健康状态需要分别判断。 连接健康不代表数据新鲜,数据断流不一定是连接断了。重连之后,先校准当前状态,再恢复业务处理。
如果你想验证这套架构,TickDB 提供了清晰的三层入口:REST 快照用于状态校准,WebSocket 推送用于持续监控,AI 工具入口用于 Agent 按需查询。从实现一个最小的 DualWatchdog 类开始------它能让你在下一个周五下午,比去年更早发现数据已经停了。
📡 本文以 TickDB 的 REST、WebSocket 和 AI 工具入口作为架构示例。接口文档见 TickDB 官网。本文仅讨论行情数据接入的工程实现,不构成投资建议。
标签: WebSocket, 行情监控, watchdog, 断线重连, Python, TickDB