价格源(price feed)在交易系统、清算协议、资产管理平台里通常被当作基础数据依赖。几乎每一个下游模块都会默认“拿到的价格是正确的”,但这个假设在真实环境里非常脆弱。真正的问题是:谁会在价格源出错时第一时间发现?最先感知到的往往是清算机器人、套利交易者,或者刚好在错误窗口内成交的用户,而不是平台自己的监控告警系统。等到人工介入,错误窗口可能已经持续了几分钟甚至更久。这篇文章要解决的不是“如何让价格源永不犯错”,而是如何设计一套从指标采集、异常识别到告警通知、故障排查的完整机制,让错误窗口尽量缩短。
下面的内容会围绕价格源常见故障形态、观测指标、最小可运行监控实现、Prometheus 与 Alertmanager 配置、故障注入验证、排查链路和上线清单展开。读者可以把它当作一套可复用的价格源监控方案骨架,再结合自己的业务场景去补充具体数据源和告警通道。
1. 价格源故障不只是“数字不对”,先建立问题清单
1.1 从数据到业务的链路,价格源会在哪个环节失效
价格源通常不是一个独立的“价格数字”,而是一条完整的数据链路。一个典型的消费场景里,价格源至少会经过以下几个环节:
- 原始行情来源,例如交易所撮合数据、做市商报价、指数计算器。
- 采集节点或预言机节点,负责定时拉取、清洗、格式化原始数据。
- 聚合或定价模块,负责对多个来源做去重、加权、取中位数或平均数。
- 对外提供读取能力的接口、缓存、合约或数据库。
- 下游交易系统、清算系统、风控系统、报表系统读取价格并执行业务逻辑。
价格源故障可能发生在任意一层。节点宕机、行情接口限流、采集进程被 OOM、数据库连接数打满、缓存过期、聚合逻辑引入了错误权重,都会让下游拿到的价格不可用。更重要的是,即使最终输出的价格数值看起来合理,时间戳也可能已经非常陈旧。数据正确有两层含义:数值接近真实市场,时间足够新鲜。两者缺一不可。
在监控体系里,不能只对“接口是否返回 HTTP 200”做判断,还要对数值、时间戳、多个来源之间的一致性做综合判断。
1.2 必须能识别的典型故障模式
在搭建任何监控脚本之前,先列一份故障模式清单,可以避免监控指标设计遗漏。
| 故障形态 | 典型原因 | 风险信号 | 如果没有监控会发生什么 |
|---|---|---|---|
| 单源节点长时间不可达 | 节点宕机、网络分区、进程崩溃 | HTTP 超时、连接被拒绝、连续拉取失败 | 单点故障被隐藏,聚合结果逐渐失真 |
| 价格长时间不更新 | 上游行情停止、节点定时任务卡死 | 最新时间戳与当前时间差持续增大 | 下游用陈旧价格成交,风险敞口扩大 |
| 单个来源价格偏离其他来源 | 数据流错位、单位错误、交易对混淆 | 与中位数偏差超过阈值 | 单源异常权重被带入聚合结果 |
| 多个来源同时受到极端行情影响 | 某个交易所插针、市场剧烈波动 | 所有来源价格同步大幅变化 | 多源聚合也难以消除错价,阈值可能误报 |
| 接口限流或鉴权失效 | API Key 过期、配额耗尽、频率超限 | 返回 401、429,或数据字段变为错误提示 | 采集端静默失败,告警缺失 |
| 下游读取到悬空值 | 服务缓存未更新、合约读取到默认值 | 业务日志出现 0 价格或默认价格 | 清算、下单逻辑基于错误价格执行 |
上面这张表并不完整,但它已经说明了一个重要事实:价格源监控不是“请求一次看有没有返回”这么简单。它需要同时覆盖可用性、新鲜度、数值偏离和业务语义几个维度。
1.3 监控目标:不追求永不犯错,而是缩短错误暴露时间
任何外部行情源都可能出错,任何聚合逻辑都可能被极端行情击穿。生产环境里更现实的目标是控制错误的暴露时间。
可以定义一个衡量指标:
错误窗口 = 错误开始时间 → 第一次有效告警时间 → 完成响应时间监控告警体系的作用是压缩“错误开始时间”到“第一次有效告警时间”的间隔,同时通过 runbook 和清晰告警内容压缩第二个间隔。
所以价格源监控的设计原则不是“测出所有问题”,而是“在问题造成不可逆损失之前,让值班人员或自动化系统有足够的反应时间”。不要把监控做成全知全能的系统,而是做成有明确优先级和响应动作的哨兵。
2. 谁在第一时间发现价格源故障:角色分析与设计启示
2.1 真实世界里,往往是套利者先于平台发现
如果价格源出现偏差,最快感知到异常的通常不是维护监控的大多数开发人员,而是套利者。套利者的工作就是同时观察多个市场的价格,并在价差超过交易成本后执行反向操作。价格源一旦失真,他们会认为这是无风险价差,于是不断买入低估资产、卖出高估资产。
从这个现象能读出监控设计的第一条经验:不能依赖“用户反馈”或“客服工单”来发现问题。用户看到的价格可能来自缓存或展示层,真正在交易逻辑里生效的价格错误往往没有直接的 UI 反馈。等用户察觉到价格不对并提交投诉时,套利者可能已经完成多轮交易。
因此,监控系统必须模拟“最敏感交易者”的视角。套利者看的是什么?看的是同一资产在不同来源之间的价格差异。对应到技术指标上,就是要做多源偏差监控。
2.2 风控和清算系统会发现问题,但那时通常已经触发了强逻辑
在 DeFi 清算协议或杠杆交易系统里,风控模块会实时检查用户抵押率和清算线。如果价格源提供的是错误价格,风控模块可能会基于错误价格提前清算用户,或者错过本来应该执行的清算。这种影响往往是双向的:价格偏高会让部分用户被过早清算,价格偏低又会掩盖真实风险。
风控系统感知到问题时,通常已经在业务层触发了保护动作。例如暂停借款、暂停交易、拒绝使用该价格源。这类保护动作本身是对的,但它们属于“止损”,而不是“预警”。好的价格源监控应该比风控动作更早、更柔和地发出信号,让运维和开发人员可以在业务中断之前定位问题。
2.3 把“人的怀疑”转成“机器可计算指标”
设计一套监控规则时,一个有效的方法是回顾真实业务中“谁会最先怀疑价格出了问题”,并把他的判断依据写成规则。
比如:
- 做市商或套利者会怀疑“为什么这个源和其他源差这么多”,对应指标是多源偏差率。
- 清算机器人会怀疑“为什么这个价格几分钟都没动”,对应指标是价格更新时间差。
- 数据分析师会怀疑“为什么这个价格突然波动 20%”,对应指标是单次抽样变动幅度。
- 运维工程师会怀疑“为什么这个源总是超时”,对应指标是请求耗时和错误码比例。
把这些怀疑规则化之后,监控系统的设计会更有依据。而不是简单地用一条探活脚本去检查“端口通不通”。
3. 监控系统的架构和指标选型,先于代码动手
3.1 要监控的是一条“价格生命周期”,而不是一个价格点
价格源监控需要分成多层,每一层回答不同的问题。
| 监控层 | 监控对象 | 要回答的问题 |
|---|---|---|
| 行情源层 | 交易所 API、指数服务、对手方报价 | 外部数据源本身是否正常,网络是否可达 |
| 采集与转换节点 | 拉取程序、消息队列、数据清洗任务 | 内部程序是否成功拿到数据,转换逻辑是否稳定 |
| 聚合与定价层 | 聚合服务、链上合约、缓存 | 多个来源是否形成一致价格,聚合结果是否新鲜 |
| 下游消费层 | 交易、清算、风控模块日志 | 下游拿到的价格是否符合预期,是否出现错误分支 |
实际项目中,很多团队只监控了采集与转换节点,看到“脚本没有崩溃”就认为价格源正常。但一个更隐蔽的问题是:脚本正常执行,但上游返回的数据内容已经损坏。比如某个行情源因为参数错误返回了上一交易日的收盘价,脚本依然把它当成当前价格写入缓存。
所以,采集层的“任务成功”不能代表“数据质量正确”。只有上游源、聚合结果和下游消费三层都覆盖到,闭环才算完整。
3.2 核心指标与推荐阈值设计
以下指标可以覆盖绝大多数价格源监控场景。阈值不是固定的,需要根据交易对流动性、业务容忍度和数据源质量调整。
| 指标名称 | 含义 | 参考阈值 | 说明 |
|---|---|---|---|
| price_feed_up | 价格源是否可成功请求并返回合法结构 | 0 或 1 | 不能只看 HTTP 状态码,需要校验字段类型和价格大于 0 |
| price_update_age_seconds | 最新价格时间戳与当前时间的差值 | 稳定计价对建议 30-60 秒 | 低流动性交易对可以放宽,但不能超过数分钟 |
| price_deviation_ratio | 单一来源价格与多源中位数的偏差率 | 0.5% 到 2% 之间,看业务 | 如果业务触发强清算,建议阈值低于清算条件 |
| price_source_latency_seconds | 单次请求延迟 | 500ms 到 2s 之间 | 更长延迟可能影响下游实时决策 |
| price_feed_active_sources | 有效来源数量 | 大于等于 2 | 小于 2 时说明聚合失真风险高 |
| cross_source_spread | 多个来源中最高价与最低价的相对差 | 与 deviation 阈值一致 | 用于观察整体一致性 |
这里的核心思想是:不要只监控可用性,还要监控新鲜度和一致性。
3.3 指标统计口径要提前定好
统计口径不一致会让监控数据很难解释。通常建议先统一以下几个口径:
- 中位数作为参考价。平均值容易被单一极端值带偏,中位数对单源异常更稳定。
- 时间差使用“当前监控节点时间”与“价格事件自带时间戳”的差值。不同机器之间需要配置 NTP,否则可能出现负延迟。
- 偏差率分母使用中位数,而不是单一来源价格。这样在多源之间比较时口径一致。
- 超时时间要小于源的真实响应能力。如果源正常响应是 1 秒,监控超时设 10 秒会导致故障发现过慢。
注意:在配置阈值前,先花时间把历史正常数据收集一段时间,观察“正常情况下偏差和新鲜度的波动范围”。直接套用网上的阈值可能会让告警发得过多或过少。
4. 从最小脚本到指标导出:一套可以落地的参考实现
4.1 环境准备与项目结构
以下参考实现使用 Python 3.10+,依赖较少,主要用来演示完整链路。实际项目落地前需要替换为真实价格源地址,并根据自己的监控平台调整。
先创建项目目录:
mkdir -p price-feed-monitor/{src,prometheus,alertmanager} cd price-feed-monitor创建requirements.txt:
requests==2.31.0 prometheus_client==0.19.0安装依赖:
pip install -r requirements.txt为了避免直接依赖真实第三方行情接口,这里先实现两个小工具:一个是本地 mock 行情源,另一个是本地 alert 接收端。这样可以在完全可控的局域网内验证监控逻辑。
4.2 用本地 Mock 数据源模拟三个独立报价源
在src/mock_price_sources.py中写入以下内容:
import json import time from http.server import BaseHTTPRequestHandler, HTTPServer from multiprocessing import Process def make_server(port, price): class Handler(BaseHTTPRequestHandler): def do_GET(self): if self.path == "/price/btc_usd": payload = json.dumps({ "symbol": "BTC/USD", "price": price, "timestamp": time.time() }).encode("utf-8") self.send_response(200) self.send_header("Content-Type", "application/json") self.send_header("Content-Length", str(len(payload))) self.end_headers() self.wfile.write(payload) else: self.send_response(404) self.end_headers() def log_message(self, format, *args): pass server = HTTPServer(("127.0.0.1", port), Handler) print(f"mock source running at http://127.0.0.1:{port}") server.serve_forever() if __name__ == "__main__": processes = [ Process(target=make_server, args=(8001, 100000)), Process(target=make_server, args=(8002, 100100)), Process(target=make_server, args=(8003, 100000)), ] for p in processes: p.start() for p in processes: p.join()运行后本地会出现三个价格源:
http://127.0.0.1:8001/price/btc_usd,返回 100000http://127.0.0.1:8002/price/btc_usd,返回 100100http://127.0.0.1:8003/price/btc_usd,返回 100000
这个 mock 的作用不是模拟真实市场的复杂行为,而是为后续验证“多源偏差”和“源故障”提供可控输入。
启动 mock:
python src/mock_price_sources.py另开一个终端,验证接口可以返回数据:
curl http://127.0.0.1:8001/price/btc_usd预期返回 JSON 中包含价格和时间戳。
4.3 写一个不依赖中间件的价格偏差监控脚本
在src/monitor_script.py中写入以下核心逻辑:
import json import sys import time import requests SOURCES = [ {"name": "source-a", "url": "http://127.0.0.1:8001/price/btc_usd"}, {"name": "source-b", "url": "http://127.0.0.1:8002/price/btc_usd"}, {"name": "source-c", "url": "http://127.0.0.1:8003/price/btc_usd"}, ] FETCH_TIMEOUT = 2 MAX_DEVIATION = 0.01 MAX_STALE_SECONDS = 15 ALERT_WEBHOOK = "http://127.0.0.1:9000/alert" def fetch_price(source): resp = requests.get(source["url"], timeout=FETCH_TIMEOUT) resp.raise_for_status() data = resp.json() price = float(data["price"]) timestamp = float(data["timestamp"]) if price <= 0: raise ValueError("price must be positive") return price, timestamp def median(values): ordered = sorted(values) n = len(ordered) mid = n // 2 if n % 2 == 0: return (ordered[mid - 1] + ordered[mid]) / 2.0 return ordered[mid] def send_alert(message): try: requests.post(ALERT_WEBHOOK, json={"text": message}, timeout=2) except Exception as exc: print(f"send alert failed: {exc}", file=sys.stderr) def check_once(): samples = [] for source in SOURCES: try: price, timestamp = fetch_price(source) age = time.time() - timestamp samples.append({"source": source["name"], "price": price, "age": age}) print(f"[{source['name']}] price={price:.2f} age={age:.2f}s") except Exception as exc: message = f"[{source['name']}] fetch failed: {exc}" print(message, file=sys.stderr) send_alert(f"price feed {source['name']} fetch failed: {exc}") continue if len(samples) < 2: print("too few healthy sources to calculate deviation") return reference = median([item["price"] for item in samples]) for item in samples: deviation = abs(item["price"] - reference) / reference if deviation > MAX_DEVIATION: message = ( f"{item['source']} price {item['price']:.2f} " f"deviates {deviation:.4%} from median {reference:.2f}" ) print("ALERT:", message) send_alert(message) if item["age"] > MAX_STALE_SECONDS: message = ( f"{item['source']} price is stale, age={item['age']:.2f}s" ) print("ALERT:", message) send_alert(message) if __name__ == "__main__": interval = float(sys.argv[1]) if len(sys.argv) > 1 else 5 print(f"monitor loop started, interval={interval}s") while True: check_once() time.sleep(interval)这个脚本一次循环做了三件事:
- 依次请求三个价格源。
- 以多源中位数为基准计算每个来源的偏差。
- 对请求失败、价格偏差过大、时间戳过旧分别发送告警。
其中每个步骤的顺序非常重要。如果某个源已经失败,应该把它从偏差计算集合中剔除,而不是用失败前缓存的旧价格继续参与计算。这里没有引入缓存,直接把失败源排除在本次计算之外,是更保守的做法。
运行监控脚本:
python src/monitor_script.py 5此时所有源价格偏差较小,不会触发告警。
4.4 用 Prometheus 指标暴露代替打印和单机告警
脚本打印适合验证思路,但生产环境需要把监控结果变成可查询指标。下面给出一个使用prometheus_client暴露指标的例子。
创建src/exporter.py:
import time import requests from prometheus_client import start_http_server, Gauge from monitor_script import SOURCES, FETCH_TIMEOUT, median source_up = Gauge("price_feed_up", "price source up", ["source"]) source_age = Gauge("price_feed_update_age_seconds", "price source update age", ["source"]) source_price = Gauge("price_feed_price", "latest price from source", ["source"]) source_deviation = Gauge("price_feed_deviation_ratio", "deviation from median", ["source"]) def refresh(): samples = {} for source in SOURCES: try: resp = requests.get(source["url"], timeout=FETCH_TIMEOUT) resp.raise_for_status() data = resp.json() price = float(data["price"]) timestamp = float(data["timestamp"]) age = time.time() - timestamp source_up.labels(source["name"]).set(1) source_age.labels(source["name"]).set(age) source_price.labels(source["name"]).set(price) samples[source["name"]] = price except Exception: source_up.labels(source["name"]).set(0) source_age.labels(source["name"]).set(-1) if len(samples) >= 2: reference = median(list(samples.values())) for name, price in samples.items(): deviation = abs(price - reference) / reference source_deviation.labels(name).set(deviation) if __name__ == "__main__": start_http_server(9100) print("prometheus exporter listening on 9100") while True: refresh() time.sleep(5)运行 exporter:
python src/exporter.py访问http://127.0.0.1:9100/metrics,可以看到price_feed_up、price_feed_update_age_seconds、price_feed_deviation_ratio等指标。
Prometheus 每 15 秒或 30 秒抓取一次该端口,即可在 Prometheus 中通过 PromQL 做规则判断。
5. 告警策略和告警疲劳管理
5.1 告警不是“报错”,是“建议有人行动的事件”
很多告警系统最终被值班人员忽略,是因为告警数量太多,且绝大多数不需要行动。价格源监控尤其容易出现这种情况,因为行情天然会波动。
要避免这个问题,需要为告警设计等级和响应动作。
| 告警等级 | 触发场景示例 | 建议响应动作 |
|---|---|---|
| critical | 全部源不可用,或可用源少于 2 个 | 停止依赖价格源的下游业务,联系数据源负责人 |
| warning | 单源偏差超过阈值但仍有可用源 | 查看异常源日志,确认是否为限流或数据源切换 |
| info | 某个源偶尔超时,恢复后正常 | 记录日志,观察频率,不打扰值班人员 |
在 Prometheus Rule 中,可以通过labels和annotations表达这个等级。
5.2 Prometheus 与 Alertmanager 配置示例
Prometheus 抓取配置prometheus/prometheus.yml保持最小化:
global: scrape_interval: 15s evaluation_interval: 15s rule_files: - "rules.yml" scrape_configs: - job_name: "price_feed_monitor" static_configs: - targets: ["127.0.0.1:9100"]告警规则prometheus/rules.yml:
groups: - name: price_feed rules: - alert: PriceFeedSourceDown expr: price_feed_up == 0 for: 1m labels: severity: critical annotations: summary: "price source {{ $labels.source }} is down" description: "source has been unavailable for more than 1 minute" - alert: PriceFeedDeviationHigh expr: price_feed_deviation_ratio > 0.01 for: 2m labels: severity: warning annotations: summary: "price source {{ $labels.source }} deviation high" description: "source price deviates more than 1% from median" - alert: PriceFeedStale expr: price_feed_update_age_seconds > 30 for: 1m labels: severity: warning annotations: summary: "price source {{ $labels.source }} is stale" description: "last price update is older than 30 seconds"Alertmanager 配置alertmanager/alertmanager.yml:
route: group_by: ["alertname", "source"] group_wait: 30s group_interval: 5m repeat_interval: 4h receiver: "default-webhook" routes: - matchers: - severity = "critical" receiver: "critical-webhook" receivers: - name: "default-webhook" webhook_configs: - url: "http://127.0.0.1:9000/alert" - name: "critical-webhook" webhook_configs: - url: "http://127.0.0.1:9000/alert-critical"这里的repeat_interval: 4h决定相同告警在未恢复前多久重复提醒一次。设置太短会告警疲劳,设置太长则可能漏掉长时间未处理的问题。
注意:生产环境应该使用真实的 Webhook 地址,并配置请求超时和重试策略。同时要确认告警接收端对重复告警有去重逻辑,否则每一条 Prometheus 告警都会产生一条通知。
5.3 恢复通知与告警去重
告警恢复本身也是重要事件。如果某个价格源故障在 1 分钟内自动恢复,但没有恢复通知,值班人员会不确定问题是否已经消失。在 Prometheus 中,当表达式恢复为正常时,会生成一条resolved告警。Alertmanager 会把它发送到接收端,内容中带有status: resolved。
在设计 Webhook 接收端时,可以按status字段区分告警触发和恢复,避免把恢复通知也当成新告警处理。同时要留意for子句的持续时间。expr中的条件满足后,需要持续一段时间才进入 firing 状态,这可以有效过滤瞬时的网络抖动。
6. 通过故障注入验证监控是否真的会告警
6.1 模拟价格偏差
验证多源偏差告警时,可以保持source-a和source-c不变,把source-b的价格调高 10%。
做法是先停掉一个端口对应的 mock 进程,再单独启动一个价格异常的 mock。
假设source-b原端口为 8002。先停掉整个 mock 进程,然后只启动一个异常源:
python -c "from mock_price_sources import make_server; make_server(8002, 110000)"这里需要先进入src目录,或者设置PYTHONPATH。为了演示方便,也可以直接修改mock_price_sources.py,增加环境变量支持。实际项目里,故障注入工具通常会放在专门的测试环境脚本中。
重启监控脚本后,预期输出:
[source-a] price=100000.00 age=0.02s [source-b] price=110000.00 age=0.01s [source-c] price=100000.00 age=0.02s ALERT: source-b price 110000.00 deviates 9.0910% from median 100000.00这个偏差已经明显超过MAX_DEVIATION = 0.01,因此会触发告警。
6.2 模拟价格源宕机
如果想验证“源不可用”场景,可以让一个 mock 源停止响应。最简单的方式是直接 kill 掉一个进程,或者在一个端口上监听但永远不返回数据。
实际中也可以用一个简单脚本模拟超时:
import time from http.server import HTTPServer, BaseHTTPRequestHandler class DelayedHandler(BaseHTTPRequestHandler): def do_GET(self): time.sleep(30) self.send_response(200) self.end_headers() def log_message(self, format, *args): pass HTTPServer(("127.0.0.1", 8002), DelayedHandler).serve_forever()把超时时间拉长到 30 秒,监控脚本的FETCH_TIMEOUT只有 2 秒,因此会抛异常并触发 fetch failed 告警。
这种验证方式尤其适合检查“告警发送”链路是否真正可用。不要等到线上出了问题,才发现 Webhook 地址配置错误或接收端已经停止服务。
6.3 验证告警接收端是否收到信息
本地 alert receiver 可以简单实现如下:
from http.server import HTTPServer, BaseHTTPRequestHandler import sys class AlertHandler(BaseHTTPRequestHandler): def do_POST(self): length = int(self.headers.get("Content-Length", 0)) body = self.rfile.read(length) print("ALERT RECEIVED:", body.decode(), file=sys.stdout) self.send_response(200) self.end_headers() def log_message(self, format, *args): pass if __name__ == "__main__": server = HTTPServer(("127.0.0.1", 9000), AlertHandler) print("alert receiver listening on 9000") server.serve_forever()先启动 alert receiver,再启动监控脚本。触发价格偏差后,终端会打印:
ALERT RECEIVED: {"text": "source-b price 110000.00 deviates 9.0910% from median 100000.00"}到这里,从“行情源返回异常数据”到“监控脚本发现偏差”再到“告警接收端收到消息”的完整链路就验证通了。
6.4 故障注入后的清理步骤
每次故障注入结束后,建议恢复原始 mock 数据源,并等待下一次循环输出恢复。不要留着异常源继续运行,否则监控指标会一直处于告警状态,影响后续验证。
清理时可以按以下顺序操作:
- 停止所有手工启动的 mock 源、监控脚本和 alert receiver。
- 删除或注释掉临时的延迟 handler。
- 重新启动标准三个 mock 源,确认价格恢复为 100000 左右。
- 查看 Prometheus 或脚本日志,确认 no alert 或恢复通知已经产生。
7. 告警触发后的排查链路
7.1 不要急着怀疑价格源本身,先看告警类型
价格源告警触发后,第一反应不应该是“上游坏了”。先根据告警特征缩小范围,通常按下面的顺序排查:
- 是什么类型的告警?是单源不可用、全部源不可用、偏差过高,还是时间戳陈旧。
- 影响范围是什么?单个交易对、单个源,还是所有交易对、所有源。
- 有没有外部事件?例如重大行情波动、交易所维护、网络割接。
- 监控系统自身是否正常?是不是因为 Prometheus 抓取超时或 Alertmanager 配置变更引起了误报。
- 查看源节点自身的日志和负载,确认源进程是崩溃了还是被限流。
这个顺序的核心是“先确认监控输入是否可信,再进入业务链路分析”。
7.2 常见问题速查表
下表列出了价格源监控上线后比较常见的告警现象及排查路径。
| 问题现象 | 常见原因 | 检查方式 | 处理建议 |
|---|---|---|---|
| 单个源 fetch failed | 源进程崩溃、端口被占用、网络不同 | 查看源节点进程、端口监听、防火墙 | 重启源进程,必要时切换备用源 |
| 所有源都 updater 超时 | 监控节点与上游网络中断,或上游行情服务故障 | 用 curl 手动请求上游,检查监控节点 DNS | 联系网络负责人,查看核心交换机状态 |
| 价格偏差率持续高于 1% | 某源返回了错误单位或错误交易对 | 对比异常源最近一次原始报文 | 修正采集映射关系,增加 schema 校验 |
| 时间戳 age 很高但请求正常 | 源程序内部任务卡死,缓存没有刷新 | 查看源节点日志中的最后一次成功更新时间 | 修复定时任务,增加执行状态监控 |
| 告警没有发到接收端 | Webhook 地址错误、接收端服务不可用 | 查看 Alertmanager 日志和接收端访问日志 | 修复 Webhook 配置,测试真实通知 |
| 告警只在价格剧烈波动时触发 | 阈值设置过窄 | 回看历史数据计算正常波动范围 | 提高阈值或增加 for 时间过滤瞬时抖动 |
7.3 监控系统自身也可能成为问题来源
监控系统虽然是为了发现问题,但它本身也会引入问题。常见情况有:
- 监控节点与价格源时间不同步,导致时间戳 age 计算成负值或异常大值。
- Prometheus 抓取间隔长于价格源更新周期,导致偏差告警延迟。
- Alertmanager 的
repeat_interval设置过短,同一个源不可用问题每小时提醒一次,造成值班人员疲劳。 - Webhook 接收端没有做鉴权,任何人都可以伪造告警消息。
解决这些问题的办法是把监控系统也纳入发布管理和变更管理。规则文件、阈值、告警接收人发生变化时,要走审批流程,并在测试环境验证。
8. 生产环境落地价格源监控的最佳实践清单
8.1 数据质量检查不能只停留在“能拿到数据”
在采集层之后、写入缓存或聚合之前,至少需要做以下数据质量检查:
| 检查项 | 具体规则 |
|---|---|
| 字段类型 | price 必须是数字,能转成 float 或 decimal |
| 数值范围 | price 必须大于 0,且不能超过最近一段时间均值的数倍 |
| 时间戳范围 | timestamp 不能晚于当前时间超过 1 分钟,也不能早于配置的 stale 阈值 |
| 交易所对 | 请求的交易对必须与返回数据中的 symbol 一致 |
| 返回包结构 | 必须包含预期字段,不能只判断 200 状态码 |
这些规则可以放在采集函数内部,也可以在聚合层做统一清洗。建议在采集层先做一次基础检查,在聚合层再做一次偏差检查。
8.2 分层监控:源层、节点层、下游层
开发环境可以只用一个脚本验证逻辑,但生产环境至少要有三层:
- 源层:外部数据源的健康状态,例如请求耗时、错误率、认证状态。
- 节点与聚合层:采集任务执行时间、聚合结果新鲜度、可用源数量。
- 下游消费层:下游模块实际读取价格时的日志,以及是否出现“0 价格”“负价格”“默认价格”等异常值。
如果只在源层监控,很难发现采集程序本身有 bug。如果只在下游消费层监控,发现问题时业务往往已经受到影响。
注意:学习环境可以用最小脚本快速跑通,生产环境必须考虑 Prometheus 高可用、告警接收端备份、运行手册和值班轮转制度。
8.3 发布前的可复用检查清单
价格源监控模块上线前,建议按下面的清单逐项确认。
- 是否已经收集足够长的历史正常数据,用于校准阈值?
- 是否覆盖了单源不可用、全部源不可用、偏差过高、时间戳陈旧四类核心故障?
- 监控脚本是否支持多源参考价计算?当源数量减少时是否仍能工作?
- 告警消息是否包含源名称、交易对、当前值、参考值、时间戳和可能的处理建议?
- 告警接收端是否配置了鉴权、超时和去重?
- 是否在测试环境通过故障注入验证过告警链路?
- 是否配置了恢复通知?
- 是否已经为值班人员准备好了排查 runbook?
这个清单可以直接用作文档中的发布检查步骤,也可以放到 CI/CD 流水线里,在配置变更后自动提示人工确认。
8.4 从主动监控走向自动化响应
价格源监控的最终目标不只是发告警,而是缩短错误窗口。当监控体系稳定后,可以考虑把部分响应动作自动化。例如:
- 当某个源连续多次请求失败,自动把该源从聚合池中摘除。
- 当全部源价格都出现异常高波动,自动暂停依赖价格源的新交易,避免在插针行情中继续成交。
- 当价格源恢复后,自动执行一次数据一致性校验,再重新开放下游业务。
自动化响应需要非常谨慎。摘除源之前要确认剩余源数量仍然足够,暂停交易之前要确认这个操作不会引发连锁清算。建议先用“自动建议+人工确认”模式运行一段时间,再逐步放开为自动执行。每一次自动化动作都要保留完整审计日志,否则出现误操作时会很难回溯。
价格源监控并不是一个“上线后就不用管”的系统。它更像是一套持续演进的业务保障机制,随着交易对增加、数据源切换和业务规则变化,监控阈值和告警路由都需要被重新审视。对于刚开始做价格源监控的团队,最有效的启动方式是先跑通一个交易对的完整监控链路,确认告警能被真实收到,再逐步扩展。这样投入最小,也能尽早发现监控系统本身的盲点。