2026-04-22 · architecture / observability
【可观测性工程】可观测性全景:Metrics、Logs、Traces、Profiles、Events 五大支柱
从控制论到云原生:拆解可观测性的五大信号支柱,对比监控与可观测性的本质区别,梳理开源/商业/SaaS 分类,以及国内互联网公司三大支柱落地现状与典型工程坑点。






















在可观测性的语境里,指标(Metrics)、日志(Logs)、追踪(Traces)被反复讨论了十年,而事件(Events)这一支柱常常被工程师忽略,或者被混入日志里一并处理。但凡做过线上事故响应的人都知道,事故复盘的第一句话几乎永远是”刚才谁发布了什么”。变更事件、基础设施事件、业务事件,才是把 MTTR(Mean Time To Recovery,平均恢复时间)从小时级压到分钟级的关键数据。
本文把 Events 作为独立的可观测性支柱来讨论:它与日志的本质差异,CloudEvents(CNCF 的事件规范)如何统一事件模型,Kubernetes Events API 的内部机制,Argo Events、Keptn 等事件流平台,以及如何把发布打点、K8s 异常事件、业务事件统一到 Grafana 的 Annotations 轨道上,与指标曲线、追踪 span 进行时间线对齐,最终形成”变更即根因”(Change First)的事故响应方法论。
传统的可观测性三支柱(Three Pillars)由 Cindy Sridharan 在 2017 年的《Distributed Systems Observability》里确立:Metrics、Logs、Traces。这个划分在工具生态里根深蒂固——Prometheus 负责 Metrics,Loki/ELK 负责 Logs,Jaeger/Tempo 负责 Traces。
但过去几年,业界对三支柱模型的反思越来越多:
本文采用一个折中立场:Events 是一个独立的支柱,有自己的数据模型、存储和查询需求,与日志有本质差异;至于是”第四”还是”第五”,并不重要,重要的是它应该被当作一等公民来设计。
很多工程师会说”事件不就是一条特殊日志吗”,这种认知会导致事件被塞进 ELK,然后在事故里捞不出来。Events 与 Logs 的差异至少有四点:
第一,语义层级不同。Log 是程序执行过程的副产物,一条 log 描述了”某个函数某一行执行到了”。Event 是业务或系统状态的转移,一个 event 描述了”系统从状态 A 进入了状态 B”。一次部署启动、一个 Pod 被 OOMKill(Out Of Memory Kill,内存耗尽被杀)、一次支付成功,都是状态转移。
第二,粒度不同。Log 是高频、低信息密度的;一个忙碌的服务一秒钟可以输出上万行 log。Event 是低频、高信息密度的;一天可能只有几十个真正关键的 event。
第三,消费方式不同。Log 大部分情况下是”事后捞”,你知道出事了再去关键字搜索。Event 天然适合”事前订阅”,发布系统要通知监控系统、通知 ChatOps、通知告警静默。
第四,Schema 稳定性不同。Log 的文本格式随意,结构化 log 也只是在应用内约定。Event 应该有严格的、跨系统的 schema,这样才能被机器消费。
这四点差异决定了 Events 不能复用 Logs 的存储、索引、消费模型。
Google SRE Book 第 15 章《Postmortem Culture》里列出了一个反复出现的结论:生产事故有超过 70% 可以直接追溯到一次近期变更。这个经验数据在国内各大厂也反复被验证——阿里巴巴的安全生产团队曾公开披露,线上 P1/P2 事故中约 65% 与变更强相关。
这意味着,如果观测系统能回答”过去 1 小时内发生了什么变更”,你就已经能覆盖大半事故的根因定位需求。遗憾的是,这个能力在很多团队里是缺失的——变更散落在 Jenkins、ArgoCD、Ansible、Helm、数据库迁移脚本、配置中心、特性开关系统里,没有统一的时间线。
Google SRE 把故障响应的第一动作固化为”Change First”:一旦报警触发,值班人(On-Caller)的第一件事是打开 Change Feed,看最近 30 分钟有没有变更。如果有,立即联系变更负责人,优先评估回滚。这个原则有三个推论:
本文后面会详细展开这三个能力如何在开源栈里搭建。
从可靠性工程的角度看,这不是巧合,而是数学必然。假设系统在”无变更、无外部扰动”的稳态下故障率极低(这是工程师追求的目标),那么故障发生的时刻必然与某个扰动时刻高度相关。扰动的来源无非三类:
前两类本身就是事件;第三类也可以建模为”阈值跨越事件”。于是,一套完善的事件系统应该能覆盖这三类扰动的打点。
为了让事件系统不至于一开始就失控,我们需要一个简单的分类体系。借鉴
CloudEvents 的 type
命名空间约定,可以把事件划为三大类。
变更事件描述了”工程师主动改变了系统”。这是最重要、也最容易统一打点的一类:
com.example.deploy.started 与
com.example.deploy.finished,标识一次 rollout
的开始与结束。典型载荷包含服务名、版本号、环境、发布者、commit
SHA。这些事件的共性:发起人明确、时间点明确、影响范围有边界。工程上只要在发起工具里埋点一次,就可以长期受益。
基础设施事件是系统自己产生的、非工程师主动发起的状态变化:
EC2 Spot Instance Interruption Warning)、Azure
VM 计划维护、GCP 预抢占式实例(Preemptible)终止、阿里云 ECS
实例系统事件。这类事件的特点是高频、机器产生、容易淹没人。因此必须有分类、降噪、聚合机制,否则事件系统会被刷屏。
业务事件描述了业务流程的状态转移:
业务事件并不是每一条都需要流入可观测性系统;真正需要的是”观测相关”的业务事件——那些会影响系统负载或 SLO 的业务状态转移。比如大促活动开始,运维团队需要知道这个时间点,才能正确解读流量曲线的突变。
讨论事件工程时,CloudEvents 是绕不过去的一个规范。它由 CNCF(Cloud Native Computing Foundation,云原生计算基金会)Serverless Working Group 在 2018 年启动,2019 年底达到 1.0,2022 年成为 CNCF 毕业项目级别的规范。
在 CloudEvents 之前,每个事件生产者都自己定义 schema:AWS
有自己的 CloudWatch Events、Azure 有
Event Grid、GitHub 有自己的 webhook
payload、Jenkins 有自己的 build event
格式。消费者要对接多个系统,就要写多套解析器。CloudEvents
的核心目标就是给事件加一个统一的”信封”(envelope),载荷(payload)内部可以继续差异化,但信封层必须标准化。
CloudEvents v1.0 定义了四个必选属性:
id:事件唯一标识。生产者自己保证同一
source 内不重复。消费者可以用
(source, id) 做去重。source:事件产生的来源 URI(Uniform
Resource Identifier,统一资源标识符)。比如
https://github.com/myorg/myrepo、/namespaces/prod/deployments/payment-svc。specversion:目前是
"1.0"。type:事件类型,建议用反向域名命名,如
com.github.push、io.kubernetes.pod.oom_killed。可选但高频的属性:
datacontenttype:载荷 MIME 类型,常见
application/json。dataschema:指向该 type 的
schema 文件 URI,方便消费者校验。subject:事件主体在 source
下的进一步定位,如 Pod 名。time:事件发生时间,RFC 3339
格式(2026-04-22T10:30:00Z)。data:事件载荷本体。规范允许自定义扩展属性(Extension Attributes),命名必须全小写字母数字,不能与核心属性冲突。常见扩展:
traceparent:W3C Trace Context 的
traceparent header,用于把事件与追踪 span 关联。traceid /
spanid:显式携带。partitionkey:在 Kafka
等分区消息系统里用的分区键。ratelimit:事件速率限制标记。CloudEvents 支持两种编码模式:
data 字段直接内嵌对象或放 base64
字符串。CloudEvents 针对常见传输协议给出了标准绑定规则:
二进制模式下,属性以 ce- 前缀的 HTTP header
携带:
POST /events HTTP/1.1
Host: event-router.example.com
Content-Type: application/json
ce-specversion: 1.0
ce-type: com.example.deploy.finished
ce-id: 5f6d7e8a-1234-4abc-9def-0123456789ab
ce-source: https://argocd.example.com/applications/payment-svc
ce-time: 2026-04-22T10:30:00Z
ce-traceparent: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01
{"service":"payment-svc","version":"v1.2.3","env":"prod","actor":"alice"}
结构化模式下,整个事件作为一个 JSON body 发送:
POST /events HTTP/1.1
Content-Type: application/cloudevents+json
{
"specversion": "1.0",
"type": "com.example.deploy.finished",
"id": "5f6d7e8a-1234-4abc-9def-0123456789ab",
"source": "https://argocd.example.com/applications/payment-svc",
"time": "2026-04-22T10:30:00Z",
"datacontenttype": "application/json",
"data": {
"service": "payment-svc",
"version": "v1.2.3",
"env": "prod",
"actor": "alice"
}
}
Kafka 绑定把信封属性放在 Kafka record header 里,key 以
ce_ 前缀(Kafka header 通常小写加下划线):
Kafka Record:
topic: events
key: payment-svc
headers:
ce_specversion: 1.0
ce_type: com.example.deploy.finished
ce_id: 5f6d7e8a-...
ce_source: https://argocd.example.com/applications/payment-svc
ce_time: 2026-04-22T10:30:00Z
content-type: application/json
value: {"service":"payment-svc","version":"v1.2.3","env":"prod"}
结构化模式直接把 CloudEvents JSON 作为
value,content-type 为
application/cloudevents+json。
AMQP(Advanced Message Queuing Protocol,高级消息队列协议)1.0 绑定把信封放在 message application-properties 里:
AMQP 1.0 Message:
application-properties:
cloudEvents:specversion: 1.0
cloudEvents:type: com.example.deploy.finished
cloudEvents:id: 5f6d7e8a-...
cloudEvents:source: https://argocd.example.com/applications/payment-svc
content-type: application/json
body: {"service":"payment-svc","version":"v1.2.3"}
此外还有 NATS、MQTT、gRPC 等绑定,规则大同小异,这里不再逐一展开。
下面是一个生产级部署完成事件的 CloudEvent,带追踪上下文与完整业务元数据:
{
"specversion": "1.0",
"type": "com.example.deploy.finished",
"id": "ev-2026-04-22-1030-payment-svc-v1.2.3",
"source": "https://argocd.example.com/applications/payment-svc",
"subject": "prod/payment-svc/v1.2.3",
"time": "2026-04-22T10:30:15.123Z",
"datacontenttype": "application/json",
"dataschema": "https://schemas.example.com/deploy.finished/v1.json",
"traceparent": "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01",
"data": {
"service": "payment-svc",
"version": "v1.2.3",
"previous_version": "v1.2.2",
"environment": "prod",
"cluster": "prod-cn-north-1",
"namespace": "payments",
"strategy": "rolling",
"replicas": 12,
"commit_sha": "a1b2c3d4e5f6789",
"commit_author": "alice@example.com",
"commit_message": "fix(order): handle nil shipping address",
"pull_request": "https://github.com/example/payment-svc/pull/842",
"actor": "alice@example.com",
"trigger": "auto-sync",
"duration_seconds": 127,
"health_status": "Healthy",
"rollback_url": "https://argocd.example.com/applications/payment-svc?rollback=v1.2.2"
}
}值得注意的几点:
subject 用 env/service/version
组合,方便消费端按维度聚合。previous_version 和
rollback_url,让事故响应工具可以一键回滚。traceparent 允许把这次部署作为一个 trace
span,后续服务启动阶段的 span 可以挂在它下面。dataschema 指向外部 JSON
Schema,消费者可以自动校验。把 K8s 原生 Event 转换成 CloudEvent 时,典型映射如下:
{
"specversion": "1.0",
"type": "io.kubernetes.pod.oom_killed",
"id": "payment-svc-7f8d9c-xk2lm.OOMKilled.1713782400",
"source": "k8s://prod-cn-north-1/payments/payment-svc-7f8d9c-xk2lm",
"subject": "payments/payment-svc-7f8d9c-xk2lm/container/app",
"time": "2026-04-22T10:33:20Z",
"datacontenttype": "application/json",
"data": {
"reason": "OOMKilled",
"message": "Container app exceeded its memory limit of 512Mi",
"severity": "Warning",
"cluster": "prod-cn-north-1",
"namespace": "payments",
"pod": "payment-svc-7f8d9c-xk2lm",
"container": "app",
"node": "ip-10-0-12-34.cn-north-1.compute.internal",
"image": "registry.example.com/payment-svc:v1.2.3",
"limits": {"memory": "512Mi"},
"exit_code": 137,
"owner_ref": {"kind": "Deployment", "name": "payment-svc"},
"count": 4,
"first_timestamp": "2026-04-22T10:30:45Z",
"last_timestamp": "2026-04-22T10:33:20Z"
}
}关键字段
count、first_timestamp、last_timestamp
来自 K8s Event
原生字段,事件路由器应当保留它们,以便后端做去重与聚合。
很多公司内部有自己的事件
Schema,常见的问题是:每个系统格式不一样,字段命名五花八门(有的用
svc,有的用 service_name,有的用
application),没有统一的 time
字段(有的是 Unix
秒、有的是毫秒、有的是本地时区字符串)。CloudEvents
的价值不是要求你把所有事件都重写,而是给出一套信封约束,核心属性统一,载荷保留内部自由。
对比表:
| 维度 | 自定义 Schema | CloudEvents |
|---|---|---|
| 生态工具 | 自己写 | SDK 覆盖 Go/Java/Python/Node/.NET/Rust |
| 跨系统集成 | 两两对接 | 统一规范 |
| 协议绑定 | 需要自己设计 | HTTP/Kafka/AMQP/NATS/MQTT/gRPC |
| 扩展性 | 随意但混乱 | 扩展属性命名约束 |
| 与 OTel 集成 | 无 | 通过 traceparent 天然关联 |
| 学习成本 | 低 | 中 |
推荐策略:存量系统不强制迁移,新系统一律按 CloudEvents 设计,事件路由层做格式归一。
K8s 自带一个完整的事件系统,它是集群内最稳定、最高频的事件源,也是最容易被忽视的金矿。
K8s 事件 API 经历了一次重构,目前两套并存:
v1.Event(旧):位于
core/v1,最早的实现,字段冗余、去重逻辑散落在
kubelet。events.k8s.io/v1(新):1.19 起
GA,结构更清晰,引入 EventSeries 和
ReportingController,去重由 API server
侧聚合。两者在 etcd 里是同一份数据,API server
会做双向转换。生产环境推荐读
events.k8s.io/v1,但要意识到老组件可能还在写
v1.Event。
一个典型的 Event 对象字段:
apiVersion: events.k8s.io/v1
kind: Event
metadata:
name: payment-svc-7f8d9c-xk2lm.17a0b1c2d3e4f567
namespace: payments
eventTime: "2026-04-22T10:33:20.123456Z"
reportingController: kubelet
reportingInstance: ip-10-0-12-34.cn-north-1.compute.internal
action: Killing
reason: OOMKilling
note: "Container app exceeded memory limit"
type: Warning
regarding:
apiVersion: v1
kind: Pod
namespace: payments
name: payment-svc-7f8d9c-xk2lm
uid: 5f6a7b8c-...
fieldPath: spec.containers{app}
related: null
series:
count: 4
lastObservedTime: "2026-04-22T10:33:20.123456Z"核心字段解释:
reason:机器可读的原因代码,如
OOMKilling、FailedScheduling、BackOff、Unhealthy。note:人类可读的消息(旧 API 里叫
message)。type:Normal 或
Warning,是最粗粒度的严重性标签。regarding:事件针对的对象(旧 API 里叫
involvedObject)。series.count:同类事件在一个聚合窗口内的次数。eventTime:事件发生时间(旧 API 里是
firstTimestamp 与
lastTimestamp)。最常用的命令:
kubectl get events -n payments --sort-by=.lastTimestamp
kubectl get events -n payments --field-selector type=Warning
kubectl get events -n payments --field-selector involvedObject.name=payment-svc-7f8d9c-xk2lm
kubectl get events -A --field-selector reason=OOMKilling --sort-by=.lastTimestamp
kubectl describe pod payment-svc-7f8d9c-xk2lm -n paymentskubectl describe
会把事件追加在对象描述底部,是排查 Pod 启动失败的首选。
K8s 并不会为每一次 OOM 都写一个新的 Event,而是采用聚合去重:
EventCorrelator,同一
(involvedObject, reason, message) 组合会累加
count 字段。EventSeries
表达连续重复事件,只写一条 Event,更新
series.count 与
series.lastObservedTime。聚合窗口默认是 10 分钟,超过 10 分钟的间隔会产生一条新
Event。这个机制极大降低了事件量,但也意味着你在短时间内看到的
count=1
可能是实际发生了几十次事件被聚合后的首次出现。
K8s Event 的默认 TTL 是 1 小时(由
--event-ttl 控制 kube-apiserver 的配置)。etcd
里只保留最近 1
小时的事件,超过就被清理。这个设计的原因是:
但对可观测性工程师来说,1 小时远远不够。典型事故复盘需要回溯至少 24 小时、灾难级事故需要回溯一周以上。这就要求必须有独立的 Event Exporter 把 K8s Events 导出到外部存储。
真实集群的事件量会让人吃惊。笔者观测过的一个 2000 节点的 K8s 集群,稳态事件速率 200/秒、突发到 2000/秒。这意味着:
常见的放大器:CronJob 失败(每分钟一次)、HPA(Horizontal Pod Autoscaler,水平 Pod 自动伸缩器)频繁调整、Pod 循环重启、镜像拉取失败。遇到这些要优先解决根因,不要通过加存储来逃避。
开源社区最成熟的事件导出工具是 opsgenie/kubernetes-event-exporter(现在 fork 到 resmoio)。它的能力:
Helm 安装:
helm repo add resmoio https://resmoio.github.io/kubernetes-event-exporter
helm install event-exporter resmoio/kubernetes-event-exporter \
--namespace observability --create-namespace \
-f values.yamlvalues.yaml 示例:
config:
logLevel: info
logFormat: json
route:
routes:
- match:
- receiver: "warning-loki"
type: "Warning"
- receiver: "all-elasticsearch"
drop:
- namespace: "kube-system"
reason: "Scheduled"
- match:
- receiver: "oom-slack"
reason: "OOMKilling"
receivers:
- name: "warning-loki"
loki:
url: "http://loki.observability:3100/loki/api/v1/push"
streamLabels:
source: k8s-events
severity: warning
- name: "all-elasticsearch"
elasticsearch:
hosts:
- "https://es.observability:9200"
index: k8s-events
indexFormat: "k8s-events-{2006-01-02}"
username: event-exporter
password: "${ES_PASSWORD}"
- name: "oom-slack"
slack:
token: "${SLACK_TOKEN}"
channel: "#k8s-alerts"
message: ":fire: OOMKilled in {{ .InvolvedObject.Namespace }}/{{ .InvolvedObject.Name }}"配置 receivers.*.webhook 发送
CloudEvents:
receivers:
- name: "ce-router"
webhook:
endpoint: "http://event-router:8080/events"
headers:
Content-Type: "application/cloudevents+json"
layout:
specversion: "1.0"
type: "io.kubernetes.{{ .Reason | lower }}"
id: "{{ .UID }}"
source: "k8s://{{ .ClusterName }}/{{ .InvolvedObject.Namespace }}"
subject: "{{ .InvolvedObject.Namespace }}/{{ .InvolvedObject.Name }}"
time: "{{ .GetTimestampISO8601 }}"
data:
reason: "{{ .Reason }}"
message: "{{ .Message }}"
type: "{{ .Type }}"
count: "{{ .Count }}"
involvedObject:
kind: "{{ .InvolvedObject.Kind }}"
name: "{{ .InvolvedObject.Name }}"
namespace: "{{ .InvolvedObject.Namespace }}"这样所有 K8s Event 在离开集群之前就被规范化为 CloudEvents,下游路由器统一消费。
事件过滤要解决两类问题:降噪和合规。
降噪层面:
drop:
- namespace: "kube-system"
- reason: "Scheduled"
- reason: "Pulled"
- reason: "Created"
- reason: "Started"
type: "Normal"Scheduled / Pulled /
Created / Started 是 Pod
正常生命周期事件,90% 的情况下不需要进入事件流。
合规层面:剥离可能包含敏感信息的字段,如
data.involvedObject.annotations
里可能有配置内容。
这两个工具经常被混淆。简单区分:
它们是互补的:KSM 告诉你”当前集群有多少 Pod 在 Pending”,Event Exporter 告诉你”哪些 Pod 因为什么原因进入 Pending”。事故排查时两者都要看。
当事件需要触发动作(比如部署完成后自动打 Grafana Annotation、自动运行集成测试、自动通知下游系统)时,光有路由不够,需要事件流平台。
Argo Events 是 CNCF 孵化项目,属于 Argo 家族(Argo Workflows、Argo CD、Argo Rollouts、Argo Events)。它的定位是 Kubernetes 原生的事件驱动自动化引擎。
核心三组件:
场景:ArgoCD 完成部署后,发送 webhook 到 Argo Events,Argo Events 做三件事:创建 Grafana Annotation、启动 e2e 测试 Workflow、发 Slack 通知。
EventSource:
apiVersion: argoproj.io/v1alpha1
kind: EventSource
metadata:
name: deploy-webhook
namespace: argo-events
spec:
service:
ports:
- port: 12000
targetPort: 12000
webhook:
deploy-finished:
port: "12000"
endpoint: /deploy-finished
method: POSTSensor:
apiVersion: argoproj.io/v1alpha1
kind: Sensor
metadata:
name: deploy-sensor
namespace: argo-events
spec:
dependencies:
- name: deploy-event
eventSourceName: deploy-webhook
eventName: deploy-finished
filters:
data:
- path: body.data.environment
type: string
value:
- "prod"
- path: body.data.health_status
type: string
value:
- "Healthy"
triggers:
- template:
name: grafana-annotation
http:
url: https://grafana.example.com/api/annotations
method: POST
headers:
Authorization: "Bearer ${GRAFANA_TOKEN}"
Content-Type: application/json
payload:
- src:
dependencyName: deploy-event
dataTemplate: |
{"service":"{{ .Input.body.data.service }}","version":"{{ .Input.body.data.version }}"}
dest: payload
- template:
name: start-e2e
argoWorkflow:
operation: submit
source:
resource:
apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
generateName: e2e-
spec:
entrypoint: run
templates:
- name: run
container:
image: registry.example.com/e2e-runner:latest
env:
- name: SERVICE
value: "{{ .Input.body.data.service }}"
- name: VERSION
value: "{{ .Input.body.data.version }}"
- template:
name: slack-notify
slack:
slackToken:
name: slack-token
key: token
channel: "#deploy-notice"
message: ":rocket: {{ .Input.body.data.service }} deployed to prod ({{ .Input.body.data.version }})"这段配置展示了 Argo Events 的核心能力:基于 CloudEvents 载荷字段做过滤,并行触发多个下游动作,每个动作有自己的失败处理策略。
除了 webhook,EventSource 可以直接监听 K8s 资源变化:
apiVersion: argoproj.io/v1alpha1
kind: EventSource
metadata:
name: k8s-resource-watch
spec:
resource:
configmap-change:
namespace: payments
group: ""
version: v1
resource: configmaps
eventTypes:
- UPDATE
filter:
labels:
- key: observability/track
operation: "=="
value: "true"这把打了 observability/track=true 标签的
ConfigMap
更新事件统一接入事件流,完美覆盖”配置变更”这个高频事故源。
Keptn 是另一个 CNCF 项目,最早由 Dynatrace 开源,2023 年新版本(Keptn v2,基于 Keptn Lifecycle Toolkit)聚焦云原生应用的生命周期管理。
Keptn 完全以 CloudEvents 为事件模型。它定义了一组领域事件(Deployment Events):
sh.keptn.event.deployment.triggeredsh.keptn.event.deployment.startedsh.keptn.event.test.triggeredsh.keptn.event.evaluation.triggeredsh.keptn.event.evaluation.finishedsh.keptn.event.release.triggered每个事件有 triggered / started
/ status.changed / finished
四个子类型,表达一个动作的生命周期。这种”四态模型”是 Keptn
的核心抽象,它让每个环节都是可观察、可中断、可回滚的。
Keptn 的招牌能力是质量门禁:部署前后自动评估 SLO。配置示例:
apiVersion: lifecycle.keptn.sh/v1beta1
kind: KeptnEvaluationDefinition
metadata:
name: payment-svc-slo
spec:
objectives:
- keptnMetricRef:
name: error-rate-prod
evaluationTarget: "<1"
- keptnMetricRef:
name: p99-latency-prod
evaluationTarget: "<500"配合 KeptnMetricsProvider(对接
Prometheus、Dynatrace、Datadog 等),Keptn 在
deployment.finished
后自动发起评估,评估未通过则触发
release.aborted 事件,进而触发回滚。
Keptn 默认输出 OpenTelemetry trace、metrics 和 log。每个 deployment 是一个 trace,阶段(pre-deploy、deploy、post-deploy evaluation)是子 span。这样你可以在 Jaeger/Tempo 里看到一次发布的完整时间线,并与业务 trace 关联。
发布打点指把”我刚刚发布了什么”这个事实写入监控系统,让曲线上出现一条标记。这是事件工程里投入产出比最高的一件事。
不打点的情况:值班人看到 p99 延迟从 120ms 涨到 350ms,开始怀疑数据库、缓存、下游依赖,花 15 分钟排除,最后被别的同事告知”我 10 分钟前发布了”。
打点的情况:p99 曲线上直接有一条竖线,标明了
payment-svc v1.2.3 10:30,值班人看一眼就知道第一嫌疑人是谁。
这个差异的本质是:曲线上的尖峰(Spike)只告诉你”什么时候有问题”,但不告诉你”什么时候做了什么”。打点补齐了后者。
Grafana 内置 Annotations 数据模型:
curl -X POST https://grafana.example.com/api/annotations \
-H "Authorization: Bearer $GRAFANA_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"time": 1713782415000,
"timeEnd": 1713782415000,
"tags": ["deploy","prod","payment-svc","v1.2.3"],
"text": "payment-svc deployed v1.2.3 by alice"
}'区间标注:
curl -X POST https://grafana.example.com/api/annotations \
-H "Authorization: Bearer $GRAFANA_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"time": 1713782400000,
"timeEnd": 1713786000000,
"tags": ["canary","prod","payment-svc"],
"text": "Canary rollout in progress"
}'Dashboard JSON 里配置 annotation query:
{
"annotations": {
"list": [
{
"name": "Deployments",
"datasource": "Grafana",
"enable": true,
"iconColor": "rgb(44, 162, 252)",
"matchAny": false,
"tags": ["deploy","prod"],
"type": "tags"
},
{
"name": "Feature Flags",
"datasource": "Grafana",
"enable": true,
"iconColor": "rgb(174, 98, 255)",
"tags": ["feature-flag"],
"type": "tags"
}
]
}
}这样面板上会同时展示所有匹配 tag 的标注。
Alertmanager 可以通过 webhook 把触发的告警自动打成 Grafana Annotation,这样”某次告警”在仪表盘上也有痕迹:
receivers:
- name: grafana-annotation
webhook_configs:
- url: http://annotation-bridge:8080/alert-to-annotation
send_resolved: true桥接服务是一段很简单的代码:
from flask import Flask, request
import requests, os, time
app = Flask(__name__)
GRAFANA_URL = os.environ["GRAFANA_URL"]
TOKEN = os.environ["GRAFANA_TOKEN"]
@app.post("/alert-to-annotation")
def alert_to_annotation():
payload = request.json
for alert in payload.get("alerts", []):
start = int(time.mktime(time.strptime(
alert["startsAt"][:19], "%Y-%m-%dT%H:%M:%S")) * 1000)
end = 0
if alert["status"] == "resolved":
end = int(time.mktime(time.strptime(
alert["endsAt"][:19], "%Y-%m-%dT%H:%M:%S")) * 1000)
requests.post(
f"{GRAFANA_URL}/api/annotations",
headers={"Authorization": f"Bearer {TOKEN}"},
json={
"time": start,
"timeEnd": end or start,
"tags": ["alert", alert["labels"]["alertname"],
alert["labels"].get("severity","unknown")],
"text": alert["annotations"].get("summary","")
}
)
return "ok", 200
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8080)Datadog 的事件模型更丰富,天然支持聚合、相关性:
curl -X POST "https://api.datadoghq.com/api/v1/events" \
-H "DD-API-KEY: $DD_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"title": "payment-svc deployed v1.2.3",
"text": "Deployed by alice, commit a1b2c3d",
"priority": "normal",
"tags": ["env:prod","service:payment-svc","version:v1.2.3","deploy"],
"alert_type": "info",
"aggregation_key": "deploy-payment-svc",
"source_type_name": "ci"
}'aggregation_key 让同一服务的连续部署在
Datadog 事件流里自动聚合,避免刷屏。
Pushgateway 设计上是为批处理作业指标服务的,但也可以借用来做发布打点:
cat <<EOF | curl --data-binary @- \
http://pushgateway.observability:9091/metrics/job/deploy/service/payment-svc
# TYPE deploy_info gauge
deploy_info{version="v1.2.3",actor="alice",env="prod"} $(date +%s)
EOF然后在 Prometheus 查询:
changes(deploy_info{service="payment-svc",env="prod"}[5m]) > 0
这个值大于 0 的时间点就是部署时刻。Grafana 的
state timeline
面板可以直接把它叠加在主面板下方。注意 Pushgateway
不适合高频事件,只适合低频发布打点。
name: Deploy
on:
push:
tags: ['v*']
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy to prod
run: ./scripts/deploy.sh ${{ github.ref_name }}
- name: Annotate Grafana
run: |
curl -X POST ${{ secrets.GRAFANA_URL }}/api/annotations \
-H "Authorization: Bearer ${{ secrets.GRAFANA_TOKEN }}" \
-H "Content-Type: application/json" \
-d @- <<JSON
{
"time": $(date +%s%3N),
"tags": ["deploy","prod","${{ github.event.repository.name }}","${{ github.ref_name }}"],
"text": "${{ github.event.repository.name }} deployed ${{ github.ref_name }} by ${{ github.actor }}"
}
JSON
- name: Publish CloudEvent
run: |
curl -X POST ${{ secrets.EVENT_ROUTER_URL }}/events \
-H "Content-Type: application/cloudevents+json" \
-d @- <<JSON
{
"specversion": "1.0",
"type": "com.example.deploy.finished",
"id": "$GITHUB_RUN_ID",
"source": "https://github.com/${{ github.repository }}",
"time": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"data": {
"service": "${{ github.event.repository.name }}",
"version": "${{ github.ref_name }}",
"env": "prod",
"actor": "${{ github.actor }}",
"commit_sha": "${{ github.sha }}"
}
}
JSONpipeline {
agent any
stages {
stage('Deploy') {
steps {
sh './scripts/deploy.sh ${VERSION}'
}
post {
success {
script {
def payload = [
time: System.currentTimeMillis(),
tags: ["deploy","prod","${env.JOB_NAME}","${env.VERSION}"],
text: "${env.JOB_NAME} deployed ${env.VERSION} by ${env.BUILD_USER}"
]
httpRequest(
url: "${env.GRAFANA_URL}/api/annotations",
httpMode: 'POST',
contentType: 'APPLICATION_JSON',
customHeaders: [[name:'Authorization', value:"Bearer ${env.GRAFANA_TOKEN}"]],
requestBody: groovy.json.JsonOutput.toJson(payload)
)
}
}
}
}
}
}ArgoCD 可以通过 PostSync Hook
在应用同步完成后运行任意 Job。这个 Job 就负责打点:
apiVersion: batch/v1
kind: Job
metadata:
name: annotate-deploy
annotations:
argocd.argoproj.io/hook: PostSync
argocd.argoproj.io/hook-delete-policy: HookSucceeded
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: annotate
image: curlimages/curl:8.5.0
env:
- name: GRAFANA_TOKEN
valueFrom:
secretKeyRef: {name: grafana, key: token}
command: ["/bin/sh","-c"]
args:
- |
curl -X POST https://grafana.example.com/api/annotations \
-H "Authorization: Bearer $GRAFANA_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"time": '"$(date +%s%3N)"',
"tags": ["deploy","prod","payment-svc"],
"text": "payment-svc deployed"
}'统一封装一个 bash 脚本,所有 CI/CD 复用:
#!/usr/bin/env bash
# annotate-release.sh
set -euo pipefail
: "${SERVICE:?}"
: "${VERSION:?}"
: "${ENV:?}"
: "${ACTOR:=${USER:-unknown}}"
: "${GRAFANA_URL:?}"
: "${GRAFANA_TOKEN:?}"
: "${EVENT_ROUTER_URL:=}"
NOW_MS=$(date +%s%3N)
NOW_ISO=$(date -u +%Y-%m-%dT%H:%M:%SZ)
COMMIT=${COMMIT:-$(git rev-parse --short HEAD 2>/dev/null || echo unknown)}
# 1. Grafana Annotation
curl -sfSL -X POST "$GRAFANA_URL/api/annotations" \
-H "Authorization: Bearer $GRAFANA_TOKEN" \
-H "Content-Type: application/json" \
-d "$(jq -nc \
--arg text "$SERVICE $VERSION deployed by $ACTOR ($COMMIT)" \
--argjson time "$NOW_MS" \
--arg service "$SERVICE" \
--arg version "$VERSION" \
--arg env "$ENV" \
'{time:$time, tags:["deploy",$env,$service,$version], text:$text}')" \
> /dev/null
# 2. CloudEvent to router
if [[ -n "$EVENT_ROUTER_URL" ]]; then
curl -sfSL -X POST "$EVENT_ROUTER_URL/events" \
-H "Content-Type: application/cloudevents+json" \
-d "$(jq -nc \
--arg id "deploy-$SERVICE-$VERSION-$NOW_MS" \
--arg source "https://ci.example.com/$SERVICE" \
--arg time "$NOW_ISO" \
--arg service "$SERVICE" \
--arg version "$VERSION" \
--arg env "$ENV" \
--arg actor "$ACTOR" \
--arg commit "$COMMIT" \
'{specversion:"1.0",
type:"com.example.deploy.finished",
id:$id,
source:$source,
time:$time,
datacontenttype:"application/json",
data:{service:$service, version:$version, env:$env, actor:$actor, commit_sha:$commit}}')" \
> /dev/null
fi
echo "[annotate-release] $SERVICE $VERSION ($ENV) recorded at $NOW_ISO"用法:
SERVICE=payment-svc VERSION=v1.2.3 ENV=prod \
GRAFANA_URL=https://grafana.example.com \
GRAFANA_TOKEN=$TOKEN \
./annotate-release.sh适合 Django/Flask 项目在发布钩子里直接调用:
# annotate_release.py
import os, time, uuid, json
from datetime import datetime, timezone
import requests
def publish_release(service, version, env, actor,
commit_sha=None, previous_version=None,
extra=None):
now = datetime.now(timezone.utc)
now_ms = int(now.timestamp() * 1000)
now_iso = now.strftime("%Y-%m-%dT%H:%M:%SZ")
grafana_url = os.environ["GRAFANA_URL"]
grafana_token = os.environ["GRAFANA_TOKEN"]
event_router = os.environ.get("EVENT_ROUTER_URL")
requests.post(
f"{grafana_url}/api/annotations",
headers={"Authorization": f"Bearer {grafana_token}"},
json={
"time": now_ms,
"tags": ["deploy", env, service, version],
"text": f"{service} {version} deployed by {actor}",
},
timeout=10,
).raise_for_status()
if event_router:
data = {
"service": service,
"version": version,
"environment": env,
"actor": actor,
}
if commit_sha: data["commit_sha"] = commit_sha
if previous_version: data["previous_version"] = previous_version
if extra: data.update(extra)
ce = {
"specversion": "1.0",
"type": "com.example.deploy.finished",
"id": f"deploy-{service}-{version}-{uuid.uuid4().hex[:8]}",
"source": f"https://ci.example.com/{service}",
"subject": f"{env}/{service}/{version}",
"time": now_iso,
"datacontenttype": "application/json",
"data": data,
}
requests.post(
f"{event_router}/events",
headers={"Content-Type": "application/cloudevents+json"},
data=json.dumps(ce),
timeout=10,
).raise_for_status()
return now_iso
if __name__ == "__main__":
import argparse
p = argparse.ArgumentParser()
p.add_argument("--service", required=True)
p.add_argument("--version", required=True)
p.add_argument("--env", required=True)
p.add_argument("--actor", default=os.environ.get("USER","unknown"))
p.add_argument("--commit", default=None)
args = p.parse_args()
ts = publish_release(args.service, args.version, args.env,
args.actor, commit_sha=args.commit)
print(f"released at {ts}")Grafana Annotations 看起来简单,但用好需要一些经验。
点标注与区间标注的选择原则:
| 事件类型 | 建议类型 | 原因 |
|---|---|---|
| 部署完成 | 点 | 瞬时事件 |
| 灰度进行中 | 区间 | 持续一段时间 |
| 配置变更 | 点 | 瞬时事件 |
| 维护窗口 | 区间 | 明确起止 |
| 告警触发 | 区间(开始→resolved) | 持续影响 |
| 大促活动 | 区间 | 明确起止 |
| 特性开关切换 | 点 | 瞬时事件 |
| SLO 违约 | 区间 | 违约持续 |
Grafana 有两级标注:
api/annotations
创建的默认是 global,可被所有 dashboard 查询。实践建议:部署、配置变更、基础设施变更一律 global;业务流程里程碑(如大促活动)按需 dashboard 级。
强烈建议统一 tag 命名,否则后期查询会乱:
deploy、config-change、feature-flag、incident、maintenance、business。prod、staging、dev。p0、p1、p2、p3。示例:
tags: ["deploy","prod","payment-svc","v1.2.3"]
tags: ["config-change","prod","feature-flags","enable_new_checkout"]
tags: ["incident","prod","payment-svc","p1"]
tags: ["maintenance","prod","database","postgres-primary"]
查询 API:
curl -H "Authorization: Bearer $TOKEN" \
"https://grafana.example.com/api/annotations?from=$FROM_MS&to=$TO_MS&tags=deploy&tags=prod&limit=100"筛选某个服务的最近部署:
curl -H "Authorization: Bearer $TOKEN" \
"https://grafana.example.com/api/annotations?tags=deploy&tags=payment-svc&limit=10" | jq '.[].text'Dashboard 定义变量 $service,annotation
query 可以写:
tags: ["deploy", "$env", "$service"]
切换变量时,面板上的标注自动过滤成当前服务的变更。这个小技巧能让”根因定位”的操作路径极短。
Annotations 会在 Grafana 数据库里无限累积。建议:
annotations.cleanup
任务自动删除超过 N 天的记录。事件与追踪的关联是根因分析最高级的能力,也是近年来 Grafana Labs、Honeycomb、Datadog 都在加码的方向。
time 字段与 trace
startTime 在相近窗口内。实现简单,精度低。data.service 与 trace
service.name 相同。消除跨服务噪声。traceparent
属性,直接关联到具体 trace/span。精度最高。生产实践里,三种方式叠加使用:先用时间戳+服务过滤出候选事件,再用 traceparent 精确穿透。
OpenTelemetry 规范允许 span 上附加 events(注意这是 OTel 内部概念,不等于本文主题的 Events,但可以互通):
from opentelemetry import trace
tracer = trace.get_tracer(__name__)
with tracer.start_as_current_span("handle_order") as span:
span.add_event("order.validated", {
"order.id": order_id,
"order.amount": amount,
})
# ... business logic
span.add_event("payment.initiated", {
"payment.method": "alipay",
})这些 span events 在 Jaeger/Tempo 里会作为时间点标注附在 span 上。它们在语义上与全局 Events 层不同,但可以通过事件路由器把关键 span events 提升为 CloudEvents,反向也可以把 CloudEvents 作为 span event 打到当前活跃的 span 上。
设想场景:部署 payment-svc 的过程是一个长 trace,阶段
pre-sync / sync /
post-sync 是子 span。部署中间某个 ConfigMap
变更产生了 CloudEvent,我们希望这个事件在 trace
时间线上可见。
实现方式:事件路由器收到 traceparent
扩展属性后,用 OTLP 协议反向把这个事件发送为 span event 到
OTel Collector,Collector 再写入 Tempo。伪代码:
func (r *Router) handleEvent(ce cloudevents.Event) {
tp := ce.Extensions()["traceparent"]
if tp == nil { /* 正常路由 */ return }
ctx := propagation.ExtractTraceparent(tp.(string))
tracer := otel.Tracer("event-router")
_, span := tracer.Start(ctx, "event:"+ce.Type(),
trace.WithSpanKind(trace.SpanKindInternal))
span.AddEvent(ce.Type(), trace.WithAttributes(
attribute.String("event.id", ce.ID()),
attribute.String("event.source", ce.Source()),
attribute.String("event.subject", ce.Subject()),
))
span.End()
}Grafana 的 Explore 界面有一个 Correlations 功能(2023 起 GA),允许把 annotation 直接叠加到 log/trace/metric 查询结果上。
配置示例(数据源 JSON):
{
"correlations": [
{
"label": "Related deployments",
"sourceUID": "prometheus-prod",
"targetUID": "grafana",
"config": {
"type": "query",
"target": {
"tags": ["deploy","${__data.fields.service}"]
},
"field": "service"
}
}
]
}这样在 Prometheus panel 上选中一个服务,会自动出现”相关部署”按钮,点击跳到 annotations 视图。
自动相关分析的基础思路:给定一个指标异常点 t,查询
t - 30min 到 t + 5min
窗口内发生的所有事件,按服务、namespace
维度打分。典型打分规则:
得分最高的事件就是首要嫌疑人。Netflix 的 Atlas、Uber 的 Observability 平台都有类似实现。开源栈里可以用 Grafana + 自研 scoring 服务实现一个简化版。
事件数据的存储选型要考虑写入吞吐、查询模式、保留周期三个维度。
优点:全文检索强、聚合查询丰富、生态成熟。 缺点:写入开销大、冷热分层成本高、事件 schema 变化频繁时 mapping 维护成本高。 适合:事件量中等(百万/天)、查询需求复杂的场景。
索引策略:按日期切分 events-YYYY-MM-DD,用
ILM(Index Lifecycle
Management,索引生命周期管理)自动归档。
优点:廉价、与 Grafana 无缝、按标签索引 缺点:全文检索能力弱、不适合复杂聚合 适合:K8s Events 导出、CloudEvents 作为日志流处理。
Loki 把事件作为 log line 存,用 labels 做维度划分:
streamLabels:
source: cloudevents
type: com.example.deploy.finished
environment: prod
service: payment-svc注意:Loki 标签基数不能过高,每个服务+版本组合就是一个流,上百万个流会拖垮 ingester。
优点:列存、写入极快、聚合查询秒级、压缩比好。 缺点:schema 变更麻烦、全文检索弱。 适合:大规模事件(亿级/天)、结构化分析。
典型表结构:
CREATE TABLE events (
event_time DateTime64(3) CODEC(DoubleDelta),
event_type LowCardinality(String),
event_source LowCardinality(String),
event_id String,
subject String,
service LowCardinality(String),
environment LowCardinality(String),
data String CODEC(ZSTD(3)),
traceparent String,
INDEX idx_traceparent traceparent TYPE bloom_filter GRANULARITY 4
) ENGINE = MergeTree
ORDER BY (event_type, service, event_time)
PARTITION BY toYYYYMMDD(event_time)
TTL event_time + INTERVAL 90 DAY;查询示例:
SELECT event_time, service, subject, data
FROM events
WHERE event_type = 'com.example.deploy.finished'
AND environment = 'prod'
AND event_time BETWEEN '2026-04-22 10:00:00' AND '2026-04-22 11:00:00'
ORDER BY event_time DESC;事件系统几乎永远按时间查询,时间字段必须是主排序键。ClickHouse
的 ORDER BY (..., event_time)
天然满足;Elasticsearch 则要显式设置 date
类型并优化 _source 存储。
分布式系统里事件重复投递是常态,去重策略:
(source, id)
组合作为主键,下游基于此去重。(source, id)
查存储,已存在则跳过。ReplacingMergeTree
引擎,后台合并时自动去重;ES 用 _id 固定为
source+id 的 hash。建议分层保留:
Google SRE 对故障响应的首要原则是 “If you don’t know what you changed, you don’t know what broke”。把变更视为嫌疑人,回滚视为最快的止血手段,这是整个方法论的灵魂。
标准流程:
故障期间必须能一键冻结全公司变更,避免新变更加剧问题。实现方式:
company.change-freeze.active,为 true
时拒绝所有非 emergency 的发布。样例 CloudEvent:
{
"specversion": "1.0",
"type": "com.example.change-freeze.activated",
"id": "freeze-20260422-1045",
"source": "https://freeze.example.com",
"time": "2026-04-22T10:45:00Z",
"data": {
"scope": "global",
"reason": "P1 incident: payment-svc latency spike",
"incident_id": "INC-12345",
"activated_by": "on-call@example.com",
"expected_duration_minutes": 60
}
}自动化的变更影响关联是成熟 SRE 团队的标配:
简化 SQL:
WITH anomaly AS (
SELECT 'payment-svc' AS service,
toDateTime('2026-04-22 10:32:00') AS t
)
SELECT e.event_time,
e.event_type,
e.service,
JSONExtractString(e.data, 'version') AS version,
JSONExtractString(e.data, 'actor') AS actor,
CASE
WHEN e.service = a.service THEN 10
WHEN e.environment = 'prod' THEN 5
ELSE 1
END AS score
FROM events e, anomaly a
WHERE e.event_time BETWEEN a.t - INTERVAL 30 MINUTE AND a.t
AND e.event_type LIKE 'com.example.deploy.%'
ORDER BY score DESC, e.event_time DESC
LIMIT 10;真实生产里变更不只来自一个系统。一个典型的 30 分钟窗口可能涉及:
单点查某个系统都看不到全貌。Change Feed 的价值就是把这些异构源汇聚到一个时间线上。
一个真实案例(脱敏):某电商公司在大促前 10 分钟,运营小组通过配置中心关闭了一个”营销活动标识”。这个 key 被订单服务用于判断是否启用额外的积分计算逻辑。关闭后,积分服务接收到无效的请求参数,开始返回 500 错误,订单服务的调用延迟飙升,进而触发上游支付服务超时,最终级联到用户下单失败。
在没有事件关联的情况下,值班人从支付服务开始排查,花了 40 分钟才追溯到配置变更。事后引入统一 Change Feed 后,类似故障的定位时间从 40 分钟压到 5 分钟内:运营小组按下保存按钮的那一刻,CloudEvent 已经出现在 Grafana Annotation 上,值班人一眼就能看到。
好的 Postmortem 模板必须包含完整时间线,每条都要有 UTC 时间与事件类型:
10:30:00 com.example.deploy.started payment-svc v1.2.3 prod
10:30:15 com.example.deploy.finished payment-svc v1.2.3 prod
10:32:07 p99 latency alert fires payment-svc p99 > 500ms
10:32:30 io.kubernetes.pod.oom_killed payment-svc-7f8d9c-xk2lm
10:33:45 on-call paged
10:34:12 Change Feed opened: 1 deployment found within window
10:35:00 rollback initiated payment-svc → v1.2.2
10:36:40 com.example.deploy.finished payment-svc v1.2.2 prod
10:37:50 p99 latency recovered
这种时间线的生成只要事件系统搭建好,就是一条 SQL 的事。
美团的变更管控系统 Radar 是国内相对公开讨论较多的方案。核心设计点:
美团技术博客里多次提到这个系统把 MTTR 从平均 40 分钟降到 10 分钟以内,核心贡献就是快速识别”变更嫌疑人”。
字节跳动的 Canary 灰度系统与监控系统深度集成:
字节 ByteTrace 平台把这些发布事件打成 trace span,与业务 trace 关联,形成”发布-调用链”一体视图。
阿里 AHAS(Application High Availability Service,应用高可用服务)与 MSE(Microservice Engine,微服务引擎)的限流、降级、熔断规则变更,都会产生规范化事件:
此外阿里的 EDAS(Enterprise Distributed Application Service,企业级分布式应用服务)发布也会通过 CloudEvents 协议流入 EventBridge,EventBridge 再路由到各观测后端。这种 EventBridge 架构在公有云产品里是事实标准。
多家互联网公司在内部建设了”K8s 事件中心”,共同特征:
笔者了解的一家头部电商公司,K8s 事件中心每天处理约 8 亿条事件,存储 30 天,查询 p95 在 500ms 以内。核心经验:
Normal 且 reason
为生命周期类的事件,能砍掉 60% 流量。(cluster, namespace, kind, name, reason)
五元组,窗口 5 分钟。TTL
自动清理老数据,配合 S3 归档。当某个集群出问题时,事件量会指数放大。典型场景:一个 1000
Pod 的 Deployment 因为镜像仓库故障全部
ImagePullBackOff,每 30 秒重试一次,10 分钟产生
20 万条事件。如果事件管道不做反压与降级,会拖垮下游。
应对:
(service, reason) 5
分钟内合并。分布式追踪上下文穿透里,同一个业务动作可能在多处产生事件。比如”下单”可能在网关、订单服务、支付服务都打点。如果都用
com.example.order.created,下游会看到多份重复。
解决方案:
com.example.payment.initiated。source
字段必须被严格约束,避免两个系统自认为权威源。很多公司同时存在多套发布系统:老的 Ansible + 脚本、中生代 Jenkins、云原生 ArgoCD、数据库 DBA 手工运维。每套系统各自打点,格式五花八门。
解决方案:
事件源时钟与中心时钟不一致时,事件时间戳会乱。典型后果:两个事件的因果顺序在时间线上反了,关联分析出错。
对策:
time。time
容忍一定误差窗口。“看到一个变更事件恰好在异常前发生”并不一定代表因果。自动相关分析容易给出 false positive:
对策:
前面提过默认 1 小时 TTL。更隐蔽的问题是:如果 event-exporter 重启恰好跨越 TTL 边界,会丢事件。
对策:
事件 schema 一定会变:加字段、改字段类型、拆分 type。如果没有版本管理,消费者会大规模 break。
对策:
dataschema 字段指向版本化的
JSON Schema URL。type(如
com.example.deploy.finished.v2),老消费者继续读老
type。异步事件管道必然有延迟。极端情况下,事件在故障现场滞后数分钟才到达 Grafana,值班人看到的时间线是错位的。
对策:
| 场景 | 推荐栈 |
|---|---|
| 初创团队,< 20 服务 | Grafana Annotations + K8s event-exporter → Loki |
| 中型团队,几十到上百服务 | CloudEvents + event-exporter → Kafka → Loki/ClickHouse |
| 大型团队,跨集群 | 完整 Event Bus + Argo Events/Keptn + ClickHouse |
| 云厂商重度用户 | EventBridge / Event Grid 原生 |
| 合规强相关(金融等) | 事件双写 Kafka + 不可变审计存储 |
第一阶段:最小可用
第二阶段:事件规范化
第三阶段:高级能力
第四阶段:方法论落地
警告:以下做法是常见的反模式,应当避免。
上一篇:持续性能分析(Profiling):pprof、Pyroscope、Parca、async-profiler、JFR
下一篇:eBPF 可观测性全景:bcc、bpftrace、libbpf 的工程路径
把当前热点继续串成多页阅读,而不是停在单篇消费。
2026-04-22 · architecture / observability
从控制论到云原生:拆解可观测性的五大信号支柱,对比监控与可观测性的本质区别,梳理开源/商业/SaaS 分类,以及国内互联网公司三大支柱落地现状与典型工程坑点。
2026-04-22 · architecture / observability
监控与可观测性不是新旧迭代,而是认知模型的根本转换。本文梳理从 1999 年 Nagios 到 2019 年 OpenTelemetry 的二十年演进时间线,对比 push/pull 模型、数据模型差异,以及国内从 Zabbix 到 Prometheus 再到 OTel 的典型迁移路径与工程坑点。
2026-04-22 · architecture / observability
从 Prometheus 架构与数据模型出发,系统梳理 Remote Write、PromQL 进阶、Thanos 全局聚合、Mimir 多租户、VictoriaMetrics 性能、M3DB 原理,以及五者在大规模生产场景下的对比矩阵与迁移实践。
2026-04-22 · architecture / observability
从日志场景分类出发,深入对比 Elasticsearch/OpenSearch、Grafana Loki、ClickHouse、OpenObserve 四大方案在全文检索、写入吞吐、存储成本、多租户和运维复杂度上的本质差异,结合 B 站、知乎 ClickHouse 日志平台实践,给出选型决策矩阵与工程坑点。
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。