← 返回模块
3.6.6.2beta 可读 · 未来付费校验通过内容版本 2026-05-29

Prometheus、Grafana 与交易栈监控

3.6.6 · 可观测性与系统设计 · 编程

沪深300 期权做市的私募,盘中 14:23 出现单边行情,CFFEX IF 主力合约成交骤增。值班 quant developer 在飞书智能助手里收到一条钉钉机器人推送:『KafkaConsumerLagHigh:feedhandler-warehouse 消费组 lag > 10000,持续 5m』。打开 Grafana,dashboard 名字叫 feed-handler,最上方一行是 Throughput、Latency、Errors、Saturation、Up 五个面板。点开 Saturation 那一行的 lag 面板,看到 lag 在过去 3 分钟里冲到 18000,然后稳在了这个数。L1 的结构化日志让你能 grep 出这一笔 SSE 50ETF 报价具体落在哪条 offset,但回答不了『过去 5 分钟全集群所有报价的速率与 p99 延迟』。指标这一支柱专门回答这个问题,本课要做的就是把这套问答机制装到 3.6.5 的 capstone 上。

为什么是 Prometheus 与 Grafana

Prometheus + Grafana 是国内私募量化自建监控的模态选择,约占新建项目 80% 的份额(兼容 polyglot 服务、原生支持 Kubernetes service discovery、PromQL 已成 SRE 工程师共同语言)。VictoriaMetrics 是 Prometheus 的高性能替代,在中型私募内增长迅速(远程写入兼容、资源占用更低)。公有云 SaaS 选项是阿里云 ARMS(Application Real-time Monitoring Service,含 Prometheus 托管服务 + APM + 链路追踪一体),腾讯云 APM / 腾讯云 Observability Platform,华为云 AOM(Application Operations Management)。本课全部示例对 OSS Prometheus + Grafana 与阿里云 ARMS 托管 Prometheus 都直接适用,因为它们都以原生 PromQL 与 Prometheus exposition format 为契约。

Prometheus 的拉取模型(scrape / pull)是反直觉但正确的:服务暴露 /metrics HTTP endpoint,Prometheus server 周期性来抓。服务自身不需要知道监控后端是谁,只需要按规定格式输出。这与日志层 push 模型(应用写 stdout、orchestrator 推到聚合器)形成互补。

四个规范指标的埋点

Fenced Python 代码块,落在 feed_handler/_metrics.py 模块里。这是 3.6.6.2 的指标埋点契约:

from prometheus_client import Counter, Gauge, Histogram, start_http_server

kafka_messages_consumed = Counter(
    'kafka_messages_consumed_total',
    'Kafka messages consumed',
    ['topic', 'partition'],
)

kafka_consumer_lag = Gauge(
    'kafka_consumer_lag_messages',
    'Kafka consumer-group lag in messages',
    ['group', 'topic', 'partition'],
)

order_latency = Histogram(
    'order_latency_seconds',
    'Order placement to fill latency',
    ['venue'],
    buckets=(0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0),
)

logs_emitted = Counter(
    'logs_emitted_total',
    'Structured log lines emitted',
    ['level', 'service'],
)


def start_metrics_server(port: int = 9090) -> None:
    start_http_server(port)

四个指标命名遵循 Prometheus 官方 best practice:_total 后缀给 Counter,_seconds 给延迟单位,_messages 给计数单位。Histogram 的桶集 (0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0) 覆盖从 1ms 到 5s 的对数尺度,是适合订单延迟的通用模板。logs_emitted_total 与 L1 的 structlog 处理器钩子配合——每行日志一次 .labels(level=..., service=...).inc()——把日志层的事件量级转成可查询指标。

Kubernetes Deployment 的抓取注解

Fenced YAML 代码块,对 3.6.5 capstone 的 feed-handler-consumer Deployment 做 patch:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: feed-handler-consumer
  namespace: feed-dev
  annotations:
    prometheus.io/scrape: "true"
    prometheus.io/port: "9090"
    prometheus.io/path: "/metrics"
spec:
  template:
    spec:
      containers:
        - name: feed-handler
          image: feed-handler:1.2.0
          ports:
            - name: metrics
              containerPort: 9090
              protocol: TCP
          env:
            - name: METRICS_PORT
              value: "9090"

三条注解告诉 Prometheus 的 Kubernetes service discovery 来抓 9090 端口的 /metrics 路径;container port 用 metrics 作为名字是行业惯例,便于在 ServiceMonitor / PodMonitor CRD 中按端口名而非端口号引用。

PromQL 的五条规范惯例

Fenced PromQL 代码块,每条注释一行:

# Counter rate over 5m
rate(kafka_messages_consumed_total[5m])
# Gauge smoothed average over 5m
avg_over_time(kafka_consumer_lag_messages[5m])
# Histogram p99 with le-aware aggregation
histogram_quantile(0.99, sum by (le, venue) (rate(order_latency_seconds_bucket[5m])))
# Per-symbol throughput
sum by (symbol) (rate(orders_filled_total[1m]))
# Service down detection
up{service="feed-handler"} == 0

histogram_quantile + sum by (le) 的顺序是不可逆的:必须先按 le(histogram 桶上界标签)聚合 rate 才能传给 histogram_quantile,否则得到的是每个时间序列各自的分位再聚合,统计意义错误。up{service="feed-handler"} == 0 是服务存活检测的官方惯例,Prometheus 自动给每个 scrape 目标维护 up 时间序列。

告警规则文件

Fenced YAML 代码块,alerts/feed-handler.yml,包含一条 recording rule 与两条告警:

groups:
  - name: feed-handler.rules
    rules:
      - record: feed_handler:order_latency_p99:5m
        expr: histogram_quantile(0.99, sum by (le, service) (rate(order_latency_seconds_bucket[5m])))
  - name: feed-handler.alerts
    rules:
      - alert: KafkaConsumerLagHigh
        expr: max by (group, topic) (kafka_consumer_lag_messages) > 10000
        for: 5m
        labels:
          severity: page
          team: feed-handler
        annotations:
          summary: |-
            Kafka consumer lag above 10000 for 5m on
            group {{ $labels.group }}
            topic {{ $labels.topic }}
          runbook_url: "https://runbooks.example.com/feed-handler/lag"
      - alert: DeploymentRolloutStuck
        expr: kube_deployment_status_observed_generation != kube_deployment_metadata_generation
        for: 10m
        labels:
          severity: warn
          team: feed-handler
        annotations:
          summary: "Deployment {{ $labels.deployment }} rollout stuck"

feed_handler:order_latency_p99:5m 这条 recording rule 的命名遵循 Prometheus 官方惯例(<namespace>:<metric>:<aggregation>)。它把昂贵的 histogram_quantile 预先物化成一条时间序列,dashboard 与告警直接读,比每次重算便宜得多。两条告警的 for: 时长不同:KafkaConsumerLagHigh 容忍 5 分钟(避免行情瞬时尖峰误报),DeploymentRolloutStuck 容忍 10 分钟(rollout 慢但通常能恢复)。

Alertmanager 路由配置

Fenced YAML 代码块,给出路由与接收人配置:

route:
  receiver: 'team-feed-handler-warn'
  group_by: ['alertname', 'cluster', 'service']
  group_wait: 30s
  group_interval: 5m
  repeat_interval: 4h
  routes:
    - match:
        severity: 'page'
      receiver: 'team-feed-handler-page'

receivers:
  - name: 'team-feed-handler-warn'
    webhook_configs:
      - url: 'https://oapi.dingtalk.com/robot/send?access_token=...'
  - name: 'team-feed-handler-page'
    webhook_configs:
      - url: 'https://open.feishu.cn/open-apis/bot/v2/hook/...'

group_wait: 30s / group_interval: 5m / repeat_interval: 4h 是国内私募的常见时长设置:等 30 秒以便相关告警合并、合并后每 5 分钟可重新通知、整段重复不超过 4 小时。severity=page 路由到钉钉 / 飞书的电话呼叫机器人(搭配阿里云 SMS / 腾讯云 SMS 短信网关触达备班);severity=warn 路由到飞书 / 钉钉的 channel 即可。PagerDuty 在国内几乎没有部署。

选指标类型的四条规则

Inline-code 列举(顺序固定):

  1. Counter——单调递增的事件计数。查询用 rate(...[5m]);永远不要直接画原始值。
  2. Gauge——可上可下的瞬时量。查询用 avg_over_time(...[5m]) 做平滑。
  3. Histogram——延迟分布。查询用 histogram_quantile(0.99, sum by (le, ...) (rate(..._bucket[5m])))sum by (le) 必须在 histogram_quantile 之前。
  4. Summary——客户端分位(桶未知时)。基本上避免使用,因为分位无法跨实例聚合。

Grafana dashboard 的五条黄金信号

Inline-code 列举(顺序固定,dashboard 行的组织原则):

  1. Throughputrate(..._total[1m]))。
  2. Latencyhistogram_quantile(0.99, ...) 取 p50 / p95 / p99)。
  3. Errorsrate(logs_emitted_total{level="error"}[5m]))。
  4. Saturation(消费 lag、池占用、CPU / 内存)。
  5. Upup == 0 红色 stat 面板)。

规则:一个服务一个 dashboard,dashboard JSON 提交到 git,使用 $service / $namespace / $pod 模板变量,一份 dashboard 覆盖所有环境。

练习

Exercise

扩展 L1 已加结构化日志的 feed-handler,加入 Prometheus 指标 + Grafana dashboard + 两条告警。(a) 在 pyproject.toml 加入 prometheus-client==0.20.0 并重新锁定。(b) 撰写 feed_handler/_metrics.py,定义本课四个规范指标(kafka_messages_consumed_total Counter、kafka_consumer_lag_messages Gauge、order_latency_seconds Histogram 含 11 档桶、logs_emitted_total Counter)与 start_metrics_server(port=9090) 函数。(c) 在 feed_handler/__main__.pyconfigure_logging 之后调用一次 start_metrics_server(9090)。(d) 在 feed_handler/consumer.py 里,每条消息调用 kafka_messages_consumed.labels(topic=msg.topic(), partition=str(msg.partition())).inc();每次轮询消费组状态时 kafka_consumer_lag.labels(group=..., topic=..., partition=...).set(lag);任何带延迟的操作用 with order_latency.labels(venue=...).time(): ... 包起来。(e) 在 L1 的 logging 库里挂钩 logs_emitted.labels(level=event_dict["level"], service=event_dict["service"]).inc() 进入 structlog 处理器链,使每行日志同步打点。(f) 给 K8s Deployment 加上 prometheus.io/scrape: "true" / prometheus.io/port: "9090" / prometheus.io/path: "/metrics" 注解并暴露 metrics 容器端口。(g) 应用 manifests 后跑 kubectl port-forward deployment/feed-handler-consumer 9090:9090 -n feed-dev && curl localhost:9090/metrics | head -50 验证指标可见。(h) 在 Grafana 创建名为 feed-handler 的 dashboard,按本课五行(Throughput / Latency / Errors / Saturation / Up)布局并用规范 PromQL 惯例,设 $namespace / $service / $pod 模板变量,导出 JSON 到 dashboards/feed-handler.json。(i) 撰写 alerts/feed-handler.yml,含 recording rule feed_handler:order_latency_p99:5m 与两条告警 KafkaConsumerLagHighfor: 5m,severity=page)+ DeploymentRolloutStuckfor: 10m,severity=warn)。(j) 配置 Alertmanager:severity: page 走电话 / 钉钉 / 飞书电话呼叫机器人;severity: warn 走飞书 / 钉钉 channel;group_by: [alertname, cluster, service]group_wait: 30srepeat_interval: 4h。(k) 通过停消费 6 分钟人为触发 lag 告警;确认告警在 Alertmanager 与你的 channel 同时触发;重启消费后告警自动 resolve。

提示
curl localhost:9090/metrics 看不到 kafka_messages_consumed_total,多半是 start_metrics_server 在子进程或线程外被调用;检查 __main__.py 顺序,configure_logging 之后立即调用,不要 import 之前。
提示
histogram_quantile 在 dashboard 上画出来全是 0 或 NaN?多半 sum by (le) 漏写。完整写法 histogram_quantile(0.99, sum by (le, venue) (rate(order_latency_seconds_bucket[5m]))),缺一不可。

跨区域阅读清单

Prometheus 官方文档(prometheus.io/docs/,部分章节有社区中文译本)的 Best Practices 节是指标命名与标签设计的权威;《Prometheus 监控实战》(郑松林,电子工业出版社)是国内最流行的 Prometheus 入门;极客时间《Prometheus 监控实战》(白白)是 video 形式补充;阿里云 ARMS Prometheus 文档(help.aliyun.com/product/34364.html)覆盖托管服务的差异点;Grafana 中文文档(grafana.com/docs/,部分中文化)覆盖 dashboard JSON 与模板变量;腾讯云监控文档(cloud.tencent.com/document/product/248)给出腾讯云的对应路径;CNCF Landscape 中可观测性板块的中文解读(jimmysong.io)每年更新。一条额外注释:国内量化 firm 的 Prometheus + Grafana 通常由平台 / DevOps 团队部署与维护,quant developer 自行在 Python 服务里加 prometheus_client 埋点 + 写 PromQL + 提交 dashboard JSON;告警规则与 Alertmanager 路由配置由平台团队审核。

标签基数纪律

指标埋点最容易翻车的一处是高基数标签。Prometheus 把每一组唯一的标签值视作独立时间序列存储;如果把 order_idcorrelation_id 作为 label(而 order_id 是每条订单都不同的 UUID),那么一天下来你就会有几百万条时间序列,Prometheus 的 TSDB 会因为索引爆炸直接 OOM。规则:label 只用低基数维度(venuetopicpartitionlevelservice);高基数信息(order_idcorrelation_idtrace_id)放进日志层或链路追踪层,不要放在指标的 label 里。如果某条指标的某个 label 的取值数已经超过几十个,就要重新审视——通常意味着设计错了。

一个服务一个 dashboard 的工程纪律

Dashboard 作为 JSON 文件提交到 git,路径约定 dashboards/<service>.json,与 alerts/<service>.yml 并列。改动经 PR review,CI 自动应用到 Grafana(通过 Grafana provisioning 机制或 grafana-dashboard-operator Kubernetes CRD)。这意味着 dashboard 是代码、是有 review 痕迹的,不是一个开发者在 UI 上点出来的私房菜。$service / $namespace / $pod 三个模板变量保证同一份 JSON 既能在 feed-dev 也能在 feed-prod 渲染。一个服务一个 dashboard 的反面是『一个团队一个超级 dashboard』——后者面板数过多、加载缓慢、信息密度过高、值班看不过来;遵循前者,把跨服务的对比留给 ad-hoc PromQL 与 Explore 视图。

跨服务的 SLI 草图

虽然 SLI / SLO / 错误预算的完整论述在 L3,本课已经能勾勒出三条 SLI 草图供后续展开:可用性 sum(rate(orders_filled_total{status="ok"}[30d])) / sum(rate(orders_filled_total[30d]))、p99 延迟 feed_handler:order_latency_p99:5m、消费 lag max(kafka_consumer_lag_messages)。这三条都直接由本课的四个规范指标算出,再加上 recording rule 把昂贵聚合预先物化。L3 会把这三条 SLI 配上目标值(如 99.9% / 500ms / 10000)变成 SLO,再用『1 - SLO』算出错误预算并把烧速率(burn rate)做成 dashboard 面板。

衔接到 L3

下一课在 L1 日志 + L2 指标之上加入第三支柱——分布式追踪。你将给同一套 feed-handler 接入 OpenTelemetry SDK 与 auto-instrumentation 包,部署 OTel Collector DaemonSet,trace exporter 指向 Grafana Tempo,log exporter 指向 Loki,并在 structlog 处理器里注入 trace_idspan_id 字段,使 L1 的每条 JSON 日志、L2 的每条 metric 与 L3 的每条 span 都能通过同一个 trace_id 双向跳转。