
价格数据源price feed出错时往往是系统里最安静的一类故障。一个 HTTP 200 的响应依然会被正常解析字段也都在只是价格是两小时前的、是 0、是买卖价倒挂后的负数或者两个独立源之间的差值突然拉大。只要没有人在屏幕上盯住它它会顺着数据管道一路跑到下游成为某个计算、某个展示、某个决策里的坏输入。这个问题真正麻烦的地方在于数据源本身并不是总能先报警。很多行情接口只会告诉你“我现在没有数据”或“请求超时”却不会主动告诉你“我这次返回的值可能错了”。于是谁能在错误刚出现时把它拦住就成了整个数据链路里最关键却又最容易被低估的工程问题。我做了几年行情数据采集、缓存分发和异常监控相关的工作一个比较强烈的体感是不要指望某个监控工具、某个外部健康检查服务或某个指标能一劳永逸地解决“价格源出错”这件事。更可行的做法是把“发现价格源出错”从一次人工盯盘变成一个分层监控能力让它在采集层、规则层、统计层、多源交叉层和下游消费层同时生效。1. 先别急着找原因价格源出错通常分四种坏味道很多人一听说“价格源出错”第一反应是接口挂掉、返回 5xx、连接超时。这类问题当然算但只占真实故障里比较容易处理的一部分。更难的是那些没有报错、看起来一切正常、但数据已经不可信的情况。我一般会把行情数据异常先分成四类因为它们的发现手段、告警级别和处理策略完全不一样。异常类型典型表现真正难点可用性异常超时、断连、HTTP 5xx、空响应容易被基础监控覆盖但恢复判断不准确静默异常HTTP 200、字段齐全但价格是 0、重复、停更、明显偏离没有显式报错规则不细就漏过去语义异常字段错位、单位错误、买卖价倒挂、精度截断需要懂业务规则不能只靠通用数学检测跨源不一致两个源分别都对但同一时刻差异过大或时间戳错位需要给不同源定义“谁更可信”的仲裁逻辑1.1 可用性故障最响也最好处理可用性故障是四类里最容易被系统发现的。请求超时、连接被重置、返回状态码不是 2xx、响应体为空、WebSocket 长时间没有心跳这类问题几乎任何一个基础监控都能在几十秒内发现。但我后来发现很多人把“接口返回 200”当成了“数据可用”这是一个隐蔽的盲区。HTTP 200 只能说明网关或服务端接受了请求不能说明返回内容里的行情是新鲜的、完整的、可用的。有些行情服务在内部数据源异常时会返回一个带默认值或缓存值的响应状态码依然是 200。如果监控只盯着状态码这类问题就会被直接放过去。所以即使是处理可用性问题也应该把检查口径从“请求是否成功”改成“这次响应是否满足业务所需的最基本条件”。最基本的条件包括响应时间是否在容忍范围内、返回体是不是 JSON 且能解析、关键字段是否存在、时间戳离当前时间是不是太远。这些条件不满足时即使状态码是 200也应该按数据源不可用来对待。1.2 静默错误最危险因为它没有报错静默错误是行情数据领域最需要警惕的情况。表现通常是调用方拿到一个结构完整的响应解析正常没有抛异常字段都是数字但数字本身没有意义。常见的静默错误有几类价格字段返回 0 或负数。成交量为 0但价格还在更新。最新的 ticker 时间戳比当前时间慢了十几分钟甚至数小时。响应体里包含的是默认值比如某个交易品种停牌或没有成交服务端就用最后一次价格无限续命。盘口价格、最新价、结算价等字段发生了错位比如把上一个周期的收盘价当成当前最新价。不同字段来自不同时间点比如最新价是一秒前的但买一价是半小时前的组合到一起后直接误导下游。这类错误的麻烦在于它们不会触发“连接失败”这类常见异常自然也不会被基础设施监控捕获。等到下游计算发现收益曲线或价格曲线出现一个离谱毛刺时坏数据往往已经污染了多个系统。我做行情采集时一直坚持一个原则任何外部行情源都不能默认它是可信的。每一条进入内部系统的数据都要过一层显式校验哪怕只是最基础的“价格是否大于 0”“时间戳是否新鲜”。这两条规则看着简单但能挡住真实环境里很大一部分静默错误。1.3 内容语义异常和跨源不一致需要业务知识兜底比静默错误更麻烦的是“单看一个源很难判断它错了”的情况。比如某个价格源返回的报价本身没有超出历史波动范围但和另一个独立源在同一时刻差了 2%。再比如因为交易所临时维护某个交易对的价格源停更了 10 分钟恢复后第一批数据的累计成交额被重置导致单位或精度发生变化。这些问题要求监控规则里必须带有业务知识。比如知道这个交易品种的涨跌幅限制、知道交易时间、知道最小价格精度、知道正常买卖价差应该是什么范围。否则通用规则只能判断“这个数有没有超出历史极值”却判断不了“这个数在这个时间点合不合理”。所以我说价格源监控不是简单的技术问题它的一半是数据工程另一半是业务建模。你越了解你说监控的资产、市场和交易制度就越能在数据真正出错之前设计出有效的规则。2. 没有参照系“监控到异常”就是一句空话很多团队在建设行情监控时第一个动作是“加告警”但真正应该先做的是定义清楚对当前业务来说什么价格才算“对”这个问题的答案比想象中模糊。价格不是一个静止的真理它随着成交和时间不断变化。所谓“价格错了”通常不是说某个绝对数字错了而是它不符合某个时间、某个市场、某个业务定义下的预期。2.1 价格对错和时间上下文强绑定判断一条价格数据是否可用的第一个前提是它是否足够新鲜。很多数据管道里的坏价格不是数值本身离谱而是时间戳太旧。旧价格并不天然错误它只是无法代表当前市场状态。一次我在排查一个“价格偶尔跳变”的问题时发现根因不是监控规则阈值太紧而是上游有两个线程在同时写入缓存一个线程写入最新价另一个线程在延迟重试时把一条旧价格写回了同一个 key。由于旧价格本身是一个合理的历史值数值层面的规则完全检测不出来。最后是靠比较每条数据的“数据时间戳”和“写入时间戳”的差值才发现有大量旧数据在延迟到达。从这段经历往后我设计校验规则时都会把时间戳列成头等字段而不是只看数值。你甚至可以接受一个没有最新成交价的行情源但不能接受一个时间戳混乱、新旧数据乱序写入的行情源。2.2 单源校验只能防低级错误更可靠的参照系是“另一个视角”单看一个源的价格我们能做的只有边界检查、变化率检查和历史分位检查。这些规则能防住明显错误但防不住“源 A 整体偏移”这类系统性问题。这时候需要建立参照系。参照系不一定是另一个同等精度的实时源也可以是另一个独立行情源的同类报价。自建的低频采样比如每隔 30 秒从另一个公开渠道拿一次价格做慢校验。多个下游成交记录聚合出的成交均价。同一个源在不同地域节点上的返回结果。理论上参照源越独立交叉验证越有价值。但实际落地上独立源往往意味着更高的成本和更复杂的维护。我的建议是不要一上来就追求三个实时源可以先给核心交易品种增加一个低频慢速校验源用来周期性地回答“源 A 最近这段时间是不是整体偏了”。这样成本可控也能建立最基本的参照系。2.3 光看最终价格不够要把监控扩展到整条链路价格数据从外部源进入内部系统通常要经过采集、解析、清洗、缓存、推送/订阅、落库多个环节。也就是说即使外部源完全正常内部任何一个环节写错字段、改错单位、推错主题都会造成下游看到的价格出问题。这也是为什么不要把监控只放在“外部源返回是否正常”这一层。更合理的做法是在关键节点都设置观测点采集端记录原始响应保留最近 N 条原始 payload。解析端记录解析成功率和字段缺失率。清洗端记录每次规则命中的数量。缓存端记录缓存更新时间和 key 的有效期。推送端记录消费者收到的最后一条消息时间戳。消费端记录“最近一次拿到数据的新鲜度”指标。当异常出现时你不仅需要知道“价格不对”还需要知道它具体是从哪一段开始不对的。没有链路上的观测点这个问题只能靠人肉翻日志效率很低而且容易漏掉偶发问题。3. 真正管用的是一套从源端到消费端的分层监控基于前面的经验我把“发现行情源出错”拆成一个五层结构。不同层解决不同种类的问题每一层就像一个过滤器。优先级从低到高成本也从低到高。3.1 第一层可用性检查活着不等于可用第一层要做的是回答“源是否还活着”但不是简单检查 HTTP 状态码而是检查业务意义上的可用性。具体信号包括最近一次成功请求的时间。连续失败次数。最近一次响应中的最新数据时间戳。请求延迟的移动平均值。WebSocket 心跳间隔是否超过阈值。在实践里我会把“可用”定义为“过去 2 分钟内至少有一次成功的、解析通过的关键数据”。如果只是 30 秒没有新数据可能只是行情不活跃如果 2 分钟都没有任何新数据大概率是源或网络出了问题。这个时间窗口要根据不同交易品种的成交活跃度调整比如成交稀疏的品种窗口要放宽。这一层最容易踩的坑是把“源能连上”当成“源的数据可用”。建议把可用性检查做成一个复合条件而不是单一条件。3.2 第二层静态规则用最小成本抓住大部分低级错误第二层是性价比最高的校验。它不依赖机器学习、不依赖历史数据只需要几条业务规则能挡住大部分可预见的坏数据。我通常会先建这样一组静态规则价格必须大于 0且不能超过该品种的合理上限。最新价、买价、卖价必须是有限数字。卖价必须大于等于买价价差不能为负。成交量不能为负数。价格精度不能超过交易所规定的最大小数位。时间戳不能晚于当前系统时间加上一个较小容忍值。时间戳不能早于当前系统时间减去最大新鲜度阈值。代码逻辑不复杂一个简单的示例结构是这样的def validate_ticker(item, now_ts, max_stale_sec120): errors [] price item.get(price) bid item.get(bid) ask item.get(ask) ts item.get(timestamp) if not isinstance(price, (int, float)) or price 0: errors.append(non_positive_price) if bid is not None and ask is not None and ask bid: errors.append(negative_spread) if ts is None or now_ts - ts max_stale_sec or ts now_ts 30: errors.append(stale_or_future_timestamp) return errors现实里的规则会比这个复杂但思路是一样的先把确定性错误用显式规则拦截掉而不是直接丢给统计模型去判断。静态规则的问题是它只能发现单条数据内部的错误发现不了“整体偏离”的问题所以需要继续向上做统计和多源交叉验证。3.3 第三层时序统计在正常波动中识别形态异常第三层处理的是“单看每一条数据都合法但放在时间序列里很不合理”的情况。常见做法是维护一个短期移动窗口计算当前值和窗口均值、标准差之间的关系。比如用 z-score 做粗略判断z_score (current_price - rolling_mean) / rolling_std如果z_score超过预设阈值就认为当前价格偏离短期正常状态。这种方法能发现突然的跳变、清零和异常拉大。但这里有一个重要的前提行情越剧烈统计模型越容易误报。尤其是在突发新闻导致市场瞬间上涨或下跌时价格出现大偏离是正常现象不能机械地告警。我的处理经验是给统计规则设置“业务场景上下文”。如果当前处于非交易时段、成交量极低、或者已知存在新闻事件就把统计阈值放宽或者只记录不告警。统计方法的作用是给监控一个参考值而不是替人做最终判断。3.4 第四层交叉验证用多个视角制造参照系第四层引入外部参照系。最简单的形式是对同一个交易品种取两个或三个独立源的数据对齐时间戳后计算两两之间的偏离度。比如源 A 和源 B 都返回了同一时刻附近的价格计算deviation abs(price_a - price_b) / max(abs(price_b), 1e-9)如果deviation超过阈值就标记为“源 A 和源 B 发生偏离”。接下来需要判断哪一个更可信而不是简单认为两个都错了。常见仲裁策略包括以成交量更大、更新频率更高的源为主。以历史稳定性更好的源为主。如果三个源中两个一致一个偏离明显视为那个偏离源异常。如果三个源相互都偏离先检查是否是时间对齐问题再检查是否有行情剧烈波动。交叉验证需要重点处理时间对齐问题。不同源的响应时间、延迟、服务器时钟都可能不同直接拿两个误差在几百毫秒内的价格做比较可能产生不符合业务实际的误报。实际落地时我会先按秒甚至分钟级窗口对齐数据再做比较。3.5 第五层下游反馈消费方是最后一个哨兵前四层都是在数据源头和数据管道内部做检测。第五层把视角放到下游真正使用这些价格数据的用户、策略、页面、计算模块是感受最直接的环节。下游反馈有两种实现方式第一种是消费方主动检查。比如一个计算指标的服务在拿到行情价格后先判断时间戳新鲜度再判断数值是否在合理区间。如果发现不对劲就拒绝使用并写日志、上报指标。这种“客户端防御”非常重要因为下游才知道某笔计算对价格有什么特殊要求。第二种是记录消费方的人工介入和后续修复。每一次因为价格异常而触发的告警、工单、人工修正都应该被记录成结构化事件。这些事件不但是问题痕迹更是后续完善规则的输入。如果你发现自己反复在同一个时间点手动修正同一个源的数据说明监控规则还没有覆盖这条路径。我比较推崇的一个小习惯是在下游埋一个“自上次收到有效 ticker 的时间差”指标。这个指标很轻量却能在上游监控全部失效时兜底。数据消费者永远知道它有多久没有收到可用的新数据这是一种天然的感知机制不应该被浪费。4. 告警不能止于“发群里”要把发现变成可执行动作很多人以为监控到位就是把异常事件推到群里。但推送只是第一步真正重要的是收到告警后系统和人能否快速做出正确响应。无差别的告警反而会造成“狼来了”效应让真正严重的故障淹没在大量信息里。4.1 给告警分级同时维护状态机我会把告警分成三类级别触发条件预期响应P0主价格源连续不可用、停更超过阈值、核心数据大面积异常立即通知值班人考虑自动切换备用源或暂停下游任务P1某个价格源出现明显偏离、静默错误、时间戳大规模滞后在几分钟内确认是否影响核心链路下发人工或自动核查P2单个源延迟稍高、偶发抖动、维护窗口事件记录指标不通知或仅汇总到日报P0 和 P1 的关键差异不是错误类型而是它是否已经影响到核心业务。如果只是备用源抖动而主源正常就不应该把所有人半夜叫起来。另一个容易忽略的点告警也应该有状态机。一条告警出现后应该经历触发、确认、持续、恢复四个状态。比如某源连续失败 5 次触发 P1第 6 次恢复成功就应该自动把这条告警关闭而不是让群里的告警消息一条接一条地刷。没有状态机告警系统本身也会变成噪声源。4.2 告警内容要带完整的上下文不要只写“价格异常”一条真正有用的告警应该能让人在 10 秒内判断出问题范围。我见过最无用的告警就是“源 A 价格异常”六个字。看到之后只能开始查查半天才能确认是哪个交易品种、哪个源、什么时间段、偏离了多少。推荐在告警内容里包含这些字段告警规则名和规则版本。资产或交易对名称。异常源名称。异常观测值、参考值、偏离比例。最近一次正常数据的时间戳。连续异常次数。触发时间窗口。关联的任务 ID、请求 ID 或日志路径。是否已经触发自动降级或切换。看上去字段很多但实现起来并不复杂本质是把检测函数里已经计算出来的上下文拼到消息模板里。这样才能让告警真正辅助决策而不是增加排查负担。4.3 自动降级和人工确认的边界要清楚有些团队在主源故障时会把数据自动切换到备用源这个逻辑在架构上是对的但落地时需要谨慎。自动切换不是“无脑切换”它要有明确的前提和退出机制。我的建议是自动切换只适用于主源连续失败、备用源持续通过基础校验且差距在可接受范围的情况。切换后要有“冷却期”和“回切条件”。不能主源刚恢复 30 秒就自动切回来那样容易造成来回抖动。切换动作必须写日志并发出通知。不要让一条数据在无人知情的情况下换了来源。如果主源和备用源同时偏离自动切换就没意义了。这时候应该暂停依赖价格数据的下游计算等人确认后再恢复。自动化的目的是缩短故障恢复时间不是取消人的判断。越是关键场景越要设计一个“可以人工介入的自动切换机制”。5. 已经出现坏数据按这个链路逐层排查无论事前做了多少监控最终还是会遇到“数据已经坏了但刚刚才发现”的情况。这种时候的排查效率取决于你手里有没有清晰的链路意识。下面是我处理行情数据异常时惯用的排查顺序可以看成一条从现象到根因的递减路径。5.1 先缩小范围再决定看哪一层拿到问题后先不要急着打开代码或查数据库先确认现象范围是所有交易品种都异常还是只有一个交易品种异常是所有数据源都异常还是只有某一个源异常是所有服务器都异常还是只有某台机器异常是偶发一次还是持续一段时间是数据完全拿不到还是能拿到但不可信这几个问题能大幅缩小排查范围。如果只有一个源、一个品种异常通常是该源该品种的数据问题如果所有源的所有品种都异常问题大概率出在内部公共链路上而不是外部源。5.2 逐层确认的完整顺序确认范围后我通常按“外部输入 → 采集端 → 解析清洗 → 缓存推送 → 下游消费”的顺序逐段检查先看采集端拿到的原始响应。直接看日志里保存的原始 JSON确认外部源到底返回了什么。这一步可以判断是源错了还是内部解析错了。再看解析和清洗层。确认字段映射是否正确、单位有没有被额外转换、时间戳有没有被错误格式化、有没有对缺失字段做了错误的默认值填充。再看缓存和推送层。确认缓存写入顺序是否可能乱序、旧数据是否被延迟写回、推送消息里是否带有正确的标识。再看下游消费代码。确认消费者是否从正确的 topic、正确的接口取数是否有缓存了过期值而没有刷新。最后检查运维变更。有没有最近发布的代码、修改过的参数、切换过的域名、调整过的权重。如果以上每一层看起来都正常就继续扩大时间窗口看问题是不是从某个特定事件后才开始的。比如交易所升级、公共库版本变化、某个第三方服务调整了字段命名。5.3 几个最容易误判的点根据过往经验有几类问题经常被误判写在这里供排查时参考外部源在剧烈行情下会主动降级。比如返回数据从实时 tick 变成延迟行情但接口没有明显报错。这时监控看到的是“频率下降”或“时间戳变旧”容易被当成链路故障。不同源的时间基准不同。源 A 返回的可能是服务端撮合时间源 B 返回的可能是客户端接收时间直接比较会产生虚假偏离。容错逻辑掩埋了问题。有的代码会在解析失败时返回上一次成功的结果这本意是增加可用性但会让“最近一次成功时间”指标一直更新掩盖数据已经很久没有真正成功解析的事实。监控自身也会出错。比如异常检测的窗口没做好滑动窗口重置导致某一天的异常样本把后续所有数据都拉偏。遇到这些情况最有效的办法不是靠头脑猜而是保留原始数据回放。排查时一定要能快速拿到故障发生前后一段时间内的原始 payload否则很多静默错误都很难复现。6. 把一次故障变成长期资产回放、契约和演练价格源出错的概率不会归零但每一次错误都应该留下一点东西。如果处理完故障后只更新了一行告警阈值那这个团队对故障的响应能力其实没有本质提升。6.1 每次故障都变成一个回归样例我会建议团队在每一个价格源异常事件结束后把原始触发数据、规则输入、告警信息和最终结论整理成一个样例存到独立的目录或测试集里。后续任何一次改动不管是调整解析逻辑、修改告警规则还是升级依赖版本都应该用这些历史故障样例跑一遍回归。跑回归的目的不是证明“代码编译通过”而是证明“那些历史上曾经造成坏数据的场景在改动后仍然能被识别出来”。这个习惯会显著降低同类问题复发概率。很多价格源问题不是没被发现过而是修复后因为某个参数或逻辑改动被悄悄放回了系统。6.2 对上对下建立数据契约行情数据的可用性不能只靠监控端单方面定义它应该在上下游之间有一个显式约定。一个可用的数据项应该满足什么字段结构、什么精度、什么新鲜度这些都应该写清楚。比如上游和下游之间的数据契约可以是价格字段永远是大于 0 的浮点数。时间戳字段统一使用毫秒级 Unix 时间。每次推送必须携带源标识。下游在超过 120 秒未收到新数据时必须进入降级状态。有了这种契约下游就可以在数据到达时做防御性校验而不是依赖上游一定会给“正确的数据”。同时上游也能根据契约去构建更一致的监控规则减少每个消费方各自开发一套判断逻辑的重复成本。6.3 定期做故障演练和弱源依赖测试最后一项长期机制是主动制造故障来测试监控链路的有效性。比如在一个独立的测试环境里把主源返回改成延迟 10 分钟看监控多久能发现、告警是否进入正确的状态机、下游是否按预期拒绝使用旧数据、工单是不是只发给对的人。这种演练不是没事找事它是在验证一个最根本的问题当价格源真的出错时你的系统真的会注意到吗如果演练结果是不能那说明监控规则还停在纸面上。演练也可以覆盖到“主源长期不可用”这类极端场景。系统不能只有一个强依赖源。如果成本不允许接入多个实时商业源至少可以准备一个低频校验源和一个降级策略。宁可数据更新频率低一点也不要让下游默默消费一份没有经过验证的坏数据。回到我开头提到的问题谁注意到了价格源出错最可靠的答案不是一个特别认真的人而是一套从源端到消费端都有感知、有校验、有告警、有响应的分层机制。单点监控永远会漏多点分层才能兜住大多数故障。把每一次故障回放成一个可验证样例把每一次告警收敛成一个可执行动作这个领域真正拼的是在安静故障发生之前你已经为它准备了多久。