沪深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 列举(顺序固定):
Counter——单调递增的事件计数。查询用rate(...[5m]);永远不要直接画原始值。Gauge——可上可下的瞬时量。查询用avg_over_time(...[5m])做平滑。Histogram——延迟分布。查询用histogram_quantile(0.99, sum by (le, ...) (rate(..._bucket[5m])));sum by (le)必须在histogram_quantile之前。Summary——客户端分位(桶未知时)。基本上避免使用,因为分位无法跨实例聚合。
Grafana dashboard 的五条黄金信号
Inline-code 列举(顺序固定,dashboard 行的组织原则):
Throughput(rate(..._total[1m]))。Latency(histogram_quantile(0.99, ...)取 p50 / p95 / p99)。Errors(rate(logs_emitted_total{level="error"}[5m]))。Saturation(消费 lag、池占用、CPU / 内存)。Up(up == 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__.py 的 configure_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 与两条告警 KafkaConsumerLagHigh(for: 5m,severity=page)+ DeploymentRolloutStuck(for: 10m,severity=warn)。(j) 配置 Alertmanager:severity: page 走电话 / 钉钉 / 飞书电话呼叫机器人;severity: warn 走飞书 / 钉钉 channel;group_by: [alertname, cluster, service]、group_wait: 30s、repeat_interval: 4h。(k) 通过停消费 6 分钟人为触发 lag 告警;确认告警在 Alertmanager 与你的 channel 同时触发;重启消费后告警自动 resolve。
提示
curl localhost:9090/metrics 看不到 kafka_messages_consumed_total,多半是 start_metrics_server 在子进程或线程外被调用;检查 __main__.py 顺序,configure_logging 之后立即调用,不要 import 之前。提示
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_id 或 correlation_id 作为 label(而 order_id 是每条订单都不同的 UUID),那么一天下来你就会有几百万条时间序列,Prometheus 的 TSDB 会因为索引爆炸直接 OOM。规则:label 只用低基数维度(venue、topic、partition、level、service);高基数信息(order_id、correlation_id、trace_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_id 与 span_id 字段,使 L1 的每条 JSON 日志、L2 的每条 metric 与 L3 的每条 span 都能通过同一个 trace_id 双向跳转。