学习目标
学完本章,你应该能够:
- 讲清主流日志技术栈的演进(ELK/EFK → Loki),以及 Loki 为什么「轻量、存储成本低」,并按「日志流选择器 + 管道操作符」写出基本 LogQL 查询。
- 讲清 Kubernetes 系统指标是从哪几个 endpoint 暴露的、用什么组件采集;用 Prometheus Operator /
ServiceMonitorCRD 声明式地采集业务指标。 - 在 Go 服务里用官方 SDK 暴露 Counter / Gauge / Histogram 三种自定义指标,并接入 Prometheus 抓取,用 PromQL 计算 QPS 和 P95 分位。
- 以 Python + FastAPI 为例完整集成 OpenTelemetry SDK:埋点 trace、日志带
trace_id/span_id、用 exemplar 把指标与 trace 关联。 - 在 Grafana 里配置 Loki / Prometheus / Tempo 三个数据源,打造「日志 ⇄ 追踪 ⇄ 指标」三合一查询面板,实现一键互通跳转。
- 理解零代码(自动注入)集成 OpenTelemetry 的原理、配置方式与局限,并能对比它和 SDK 集成方式的取舍。
前置知识:
- 能打开 K3s / k8s 集群并执行
helm install、kubectl;理解 DaemonSet、Deployment、Pod、Service、命名空间这些基本概念。 - 会用 Docker Compose / Docker 构建镜像。
- 会写简单的 Go
main函数和 Python 脚本,能读懂context.Context传递。 - 上节课已了解 OpenTelemetry 的 Trace / Metric / Log 三种信号与 Collector 的架构。
本章你会动手做的事:
- 用 Helm 在一个 K3s 集群里部署 Loki + Promtail + Grafana,采集全集群日志,写出 LogQL 查询并在 Grafana 里看到日志趋势。
- 用 kube-prometheus-stack 一键部署 Prometheus + Alertmanager + Grafana,抓取集群系统指标;再部署 Blackbox Exporter 做网站可用性监控。
- 用 Go 写一个带三个自定义指标(请求数、状态码计数、耗时直方图)的 HTTP 服务,构建镜像部署到集群,用
ServiceMonitor让 Prometheus 抓取,并建一个多面板 Dashboard。 - 用 Python + FastAPI 写一个模拟微服务调用链的三服务应用,完整集成 OTel SDK,把 trace / log / metric 三样数据分别送到 Tempo / Loki / Prometheus。
- 在 Grafana 里配置派生字段、Prometheus exemplar 和 Tempo 的 trace-to-logs / trace-to-metrics,实现三合一互查。
- 用
opentelemetry-instrument+ 环境变量把同样的服务改造成零代码注入,观察差异。
先建立整体直觉:一门课打通「三支柱 + 三存储 + 一张面板」
这一章我们不再停留在概念,而是把可观测性的三条数据链路真正跑起来。用一句话概括本章的完整闭环:
日志 → Loki、指标 → Prometheus、追踪 → Tempo,最后都汇到 Grafana 一张面板里,并且用 trace_id 把三条链路互相打通。
flowchart LR
App[应用
产生 Log/metric/trace] -->|Promtail / SDK / instrument| COL[(三大后端)]
COL --> G[Grafana
三合一查询面板]
G -->|trace_id 关联合一| R1[日志 ⇄ 追踪]
G -->|exemplar 关联| R2[指标 ⇄ 追踪]整个章节分成五步:先把每一条链路单独打通(第 1、2 讲采集日志和指标),再把 OTel SDK 集成让三条信号能带同一条 trace_id(第 3 讲),接着在 Grafana 里把它们关联成三合一面板(第 4 讲),最后一讲讲一种省代码的零代码注入方式(第 5 讲)。
术语提醒:这一章里你会反复看到两个「pro」开头的东西——在 Loki 场景里 Promtail 是日志采集器,在指标场景里 Prometheus 是指标存储。它们都来自同一个 Grafana/云原生生态,别混。
第一讲:Loki 日志采集实战
日志技术栈:从 ELK / EFK 到 Loki
说到日志采集,业界最常见的选型有三类,其中传统派是 ELK / EFK:
| 缩写 | 组件 | 职责 |
|---|---|---|
| E | Elasticsearch | 存储 + 索引日志(类似 Loki 的职责) |
| F / L | Fluentd / Fluent Bit / Logstash | 日志采集器,增量化地上传日志 |
| K | Kibana | 查询面板(类比 Grafana) |
早期大家普遍用 EFK 里的 Logstash,但 Logstash 性能、效率上偏重,所以很多人把采集器换成 Fluentd / Fluent Bit 来提性能。而今天更主流的选择是走「轻量级」路线——也就是 Loki:
- Loki 是 Grafana 推出的日志查询工具,
Loki对应 Elasticsearch,负责存储 + 索引日志。 - Promtail 是它的采集器。从名字就能记:Linux 里的
tail -F命令就是不断观察文件新增的日志流,Promtail 就是干这个——把日志增量化地推到 Loki 里。 - Grafana 提供日志检索能力,与指标面统一。
相比之下,Loki 的优势是轻量、容易和 Grafana 集成(指标和日志能放同一套面板)、存储成本低(取决于它的索引策略)、处理效率高;而 EFK 的问题是资源占用大、部署和维护复杂度高、长期日志的存储成本高。
安装:用 Helm 快速拉起 Loki + Promtail + Grafana
官方文档提供了两种安装方式:
- 直接用 Helm 装
loki-stack——它包含 Loki、Promtail、Grafana 全套。缺点是内置版本通常比较旧。 - 分别单独安装 Loki、Promtail、Grafana,并在 Grafana 里手动配一个 Loki 数据源。本讲用这种方式,可控性更好。
以我们开通的 K3s 集群为例,大概流程是:
# 1. 加 Grafana 官方 Chart 仓库(Loki / Promtail / Grafana / Tempo 基本都在里面)
helm repo add grafana https://grafana.github.io/helm-charts
helm repo update
# 2. 配置 KUBECONFIG,指向你的 K3s 集群
export KUBECONFIG=$(pwd)/k3s.yaml
kubectl get nodes
# 3. 安装 Loki(单体模式,读取 loki-values.yaml)
helm install loki grafana/loki \
--namespace loki-stack \
--create-namespace \
-f loki-values.yaml
Loki 单体的最简配置(快速上手版,注意这不是生产级):
# loki-values.yaml(示意)
loki:
config:
# 用 TSDB 作为索引存储,速度更快、扩展性更好(生产建议开启)
schema_config:
configs:
- from: 2023-01-01
store: tsdb
object_store: filesystem
schema: v12
storage_config:
filesystem:
directory: /loki/index
singleBinary:
replicas: 1 # 单体二进制模式(target=all)
安装完成后日志会打印一条关键信息:Loki 默认启用了多租户,要求你从 X-Scope-OrgID 这个 header 里拿到租户 ID 传进去。如果不提供,Loki 是采集不到日志的——这个坑在配置 Grafana 数据源时一定要记住。
接着安装作为日志采集器的 Promtail(它以 DaemonSet 方式在每个 Node 上部署):
# promtail-values.yaml(示意)
config:
clients:
- url: http://loki-gateway.loki-stack.svc.cluster.local/loki/api/v1/push
# Loki 开了多租户,必须带上租户 ID,否则采不到日志
headers:
X-Scope-OrgID: "1"
安装后可以用 kubectl get svc -n loki-stack 看到 loki-gateway 这个 Service——它就是 Promtail 的日志上报网关。
安装 Grafana 则很简单,不需要 values 文件,装完取出动态生成的 admin 密码,再做端口转发,本地访问 Grafana:
helm install grafana grafana/grafana --namespace loki-stack
kubectl get secret --namespace loki-stack grafana -o jsonpath="{.data.admin-password}" | base64 --decode
kubectl port-forward service/grafana -n loki-stack 3000:80
在 Grafana 里 Configuration → Data sources 添加 Loki 数据源。重点有两点:
- URL 要填 Promtail 上报的那个 gateway,例如
http://loki-gateway.loki-stack.svc.cluster.local:3100。因为我们是单体部署,读和写走同一个服务。 - 最重要的一项:在 Custom HTTP Headers(或 YAML 里的
httpHeaderName)里,把这个网关接受租户 ID 的X-Scope-OrgID: 1加上。
保存后测试连通,然后可以在 Grafana 里 Explore → Loki 选一个 label。有 label 说明采集正常,然后写 {app="loki"} 查最近 5 分钟,就能看到集群里任意工作负载的日志。
LogQL:日志流选择器 + 管道操作符
LogQL 很像 PromQL,可以拆成两大部分:
第一部分是「日志流选择器」——用来选你想查哪个 Pod / 业务,例如:
{app="loki"}
这些标签是 Promtail 采集时根据工作负载的 label 自动帮你打上的(app、container、service、namespace、pod、instance、job 等)。
第二部分是「管道(Pipeline)」操作符——在选定的日志流里做关键字过滤,有四种匹配方式:
| 管道操作符 | 含义 | 示例 |
|---|---|---|
| ` | =` | 包含关键字 |
!= | 不包含关键字 | != "health" |
| ` | ~` | 正则匹配 |
!~ | 正则不匹配 | !~ "debug" |
组合起来就是一条完整的 LogQL:
{app="loki"} |= "level=error"
在 Grafana 的 Explore 里可以切到 Code 模式手写,也可以回到 Builder 模式,它会把代码反填成表单。
结构化日志才能做运算:logfmt 与 json
日志格式由业务输出决定,一般有两种:logfmt(key=value key2=value2)和 JSON。它们都包含「键值对」,好处是——你可以对某个键的值做运算,而不只是灰蒙蒙地全文匹配。
例如日志里有一个字段叫请求耗时 duration,你就可以查出「耗时超过 500ms 的所有日志」。但做运算之前,必须先用内置函数把日志格式化成结构化字段:
{app="loki"} | logfmt | duration > 5ms
| logfmt:用 logfmt 解释器解析日志行,自动生成里面的标签值(如duration、status)。| json:同理,用 JSON 解释器解析。- 解析后就能直接对
duration、status等字段做运算:duration > 10ms、logfmt | status = 200。
如果你的业务日志特别乱又不方便改应用,Loki 还提供更复杂的正则 / 提取方式(| regexp、| pattern)来做兜底。
重写输出格式:line_format
业务日志往往又杂又吵,可以用 line_format 重写日志输出。前提是先 | logfmt 把字段解析出来,再重新组装:
{app="loki"} | logfmt | line_format "status={{.status}} duration={{.duration}}"
这样每行日志就只输出你要的 status 和 duration,干净很多。
用日志当指标:从日志里算出 P95
日志里既然打了耗时、状态码,这些其实就已经是「指标」了。Loki 提供了一组 ..._over_time 聚合函数,可以从日志里直接算出分位、均值等指标:
quantile_over_time(0.99,
{app="loki"} | logfmt
| unwrap duration [1m]
) by (status)
第二个参数里的 unwrap duration 表示把解析出来的 duration 字段作为采样值取出,[1m] 是一分钟窗口,最后 by (status) 按状态码分组。跑出来就是「按 status 分组的 99% 请求耗时」,在 Grafana 里能看到 200 / 500 两条趋势线。前提是业务日志里得真的有 duration、status 这些字段。
其他 LogQL 技巧
- 删除颜色标记:业务日志常带的 ANSI 颜色编码,可以用
| strip_ansi去掉,方便查看。 - 标签格式化:用
| label_format重命名 / 重组标签。 - 删除 / 保留标签:
| label_drop删掉不要的标签、| label_keep只保留部分标签,避免日志标签太多造成索引膨胀。
原理:Promtail 为什么能采到所有 Pod 日志
Kubernetes 里,所有 Pod 打印到标准输出的日志,都会被 Kubelet(容器运行时)写入宿主机的某个特定目录下。也就是说,只要能从宿主机上采集这个目录的日志,就能间接采到所有 Pod 的日志。
Promtail 正是基于这个原理,以 DaemonSet 方式在每个 Node 上部署一个副本,并通过 hostPath 挂载宿主机的日志目录(如 /var/log/pods 和 /var/lib/docker/containers),read-only 挂载进容器里。它就像 tail -F 一样实时读新增日志行,然后以 HTTP 方式推给 Loki。Promtail 拿到 /var/log/pods/<namespace>/<pod>/<container>/0.log 这类文件,就能按命名空间、Pod、容器解析出标签。
flowchart LR
POD[Pod 打印日志到标准输出] --> NODE["宿主机 /var/log/pods 目录 (Kubelet 写入)"]
NODE -->|hostPath 挂载| PT[Promtail
DaemonSet 每 Node 一个]
PT -->|tail -F 增量化| LOKI[Loki
存储+索引]
LOKI --> GF[Grafana 查询展示]Loki 生产实践
- 启用 TSDB 索引 + 对象存储:生产建议给索引启用 TSDB(更快、扩展性更好),并用
from指定从某个时间点起的新日志才用 TSDB,老日志不用管。对象存储(腾讯云 COS / AWS S3)来存日志——S3 这类存储几乎没有容量上限,成本很低,特别适合长期、海量地存日志。 - 多租户:Loki 原生支持多租户,大规模生产可以配置多租户(每个团队 / 业务线一个租户 ID)。需要单独配 Loki 和 Promtail,Promtail 配置里填租户 ID。在 Grafana 数据源里可以通过配置多个租户值(
1|2)来切换查询,查询日志时带上租户 ID 即可筛选。
Loki 核心组件数据流(谁在干活)
理解 Loki 的架构,你需要记住这几个核心组件和两条链路(写链路 + 读链路):
- Distributor(分发器):日志接收的第一个组件,接收 Promtail 压上来的日志并分发给 Ingester。
- Ingester(摄取器):接收日志,很重要的能力是暂存(内存队列),攒一批再写入云存储,而不是来一条写一条。
- Querier(查询器):从云存储读取日志数据。
- Query Frontend(查询前端):直接跟 Grafana 对接,处理查询请求、管理调度和缓存(短时间重复查询直接走缓存)。
- Ruler(规则引擎):执行查询与告警策略,结合日志数据设置告警。
- Compactor(压缩器):合并压缩日志块,跟对象存储交互,减少存储占用、提高查询性能。
特别注意那个双向箭头:数据从 Distributor 进来后给 Ingester,Ingester 内存在写入云存储前是缓存。所以 Grafana 查询时,Querier 既要读 COS/S3 也要把 Ingester 里还没落盘的数据取出来,合并后交给查询前端,才是完整的日志数据。整个架构跟 Prometheus/Mimir 那套时序引擎很相似。
Loki 的三种部署方式
Loki 支持三种部署方式,区别在于「组件是否拆分 + 是否水平扩展」,镜像其实是同一个,靠启动参数 -target=all / read / write / backend 区分:
| 部署方式 | 原理 | 适用日志量 | 说明 |
|---|---|---|---|
| 单体(Single Binary) | 所有组件跑在单进程二进制里(-target=all) | 每天约 20GB 内 | 快速上手,本讲 demo 用这个;其实也能写副本数水平扩展 |
| 简单可扩展(Simple Scalable) | 读写分离:write(Distributor+Ingester)、read(Query Frontend+Querier)、backend(Compactor 等),各可横向扩容,配套 Gateway 做分发 | 每天几个 TB | Helm 默认安装方式,多数中型公司够用 |
| 微服务(Microservices) | 每个组件单独部署、各自可扩展 | 每天超过 10TB | 维护难度最高,大型公司使用 |
哪怕只有单体模式,也把每天的日志量上限说清楚——这有助于你按日志量挑部署方式。简单可扩展模式里 Grafana 查询走的也是 Gateway,读写组件共用同一个镜像、只是启动方式不同。
第二讲:Prometheus 指标采集实战
先看 K8s 系统指标是怎么暴露的
指标采集前,先把 Kubernetes 系统级指标的来源弄清楚。从「容器运行时」一路往上:
- 容器运行时把指标交给内置在 Kubelet 里的 cAdvisor(收集、处理、导出容器指标)。
- cAdvisor 再在 Kubelet 的几个 metrics endpoint 上暴露指标,并且每个端点提供对应的
/metrics/summary。 - metrics-server(附加组件)负责收集、聚合这些指标,提供 Metrics API。
- 有了 Metrics API,kubectl top 和 HPA(Pod 水平自动扩缩容)才能拿到 Pod 内存 / CPU 用量。
系统级指标主要有三个 endpoint(用 kubectl proxy 转发到本地后可以一个个访问):
/metrics/cadvisor:cAdvisor 采集的容器指标(container_*)。/metrics/resource与/metrics:K8s 资源使用指标(container_cpu_usage_seconds_total、container_memory_usage_bytes等)。/metrics/probes:健康检查指标(readiness / liveness 探针)。
Prometheus 抓这些指标相对简单——把 metricsPath 指到对应的 endpoint,定时去 scrape 就行。但配置很复杂(服务发现、relabel、认证机制等),所以有 Prometheus Operator 帮你做默认配置。
Prometheus Operator:用 CRD 声明式管理采集
原生 Prometheus 配置复杂,Prometheus Operator 是安装 Prometheus 的最佳实践:安装简单、自带大量标准 ServiceMonitor 和告警规则。它的核心思想是用 CRD 管理 Prometheus 的配置,这样就能用声明式方式(kubectl apply YAML)来配置和部署。
Operator 提供的几个关键 CRD 你需要重点掌握:
| CRD | 作用 |
|---|---|
ServiceMonitor | 配置怎么采集指标(按 label 匹配 Service)— 最常用 |
PodMonitor | 类似 ServiceMonitor,但针对没有 Service 的 Pod / job / deployment |
PrometheusRule | 配置告警规则 |
Prometheus / Alertmanager | Prometheus 与告警组件实例 |
Probe(配合 Blackbox Exporter) | 做对外网站的黑盒探活 |
社区方案:kube-prometheus-stack 一键安装
Operator 仍需自己写 CRD,门槛还是有的。社区更简单的方案是 kube-prometheus-stack——它内置了运行所需的全部组件(kube-state-metrics 替代 metrics-server、node-exporter 导出 Linux 内核级指标、Grafana),并且 Kubernetes 常用指标的抓取策略、relabel、告警规则全部预配置好,开箱即用,是入门首选。
典型安装配置大概要做这些事:让 Grafana 给 admin 配好密码而不是走 secret 动态生成;开启 dashboard 编辑能力;把 ServiceMonitor 和 PodMonitor 的 selector 配置为能发现所有命名空间下的对象;给 Prometheus 加额外的 label(多集群场景用集群名区分);配置持久化存储(如 storageClass)。
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm upgrade --install kube-prometheus-stack \
prometheus-community/kube-prometheus-stack -f kube-prometheus-values.yaml
部署后用 NodePort 暴露 Grafana(比如 30080),登录后可以看到 operator 自动帮你添加好了 Prometheus 和 Alertmanager 两个数据源,并且自带了海量 dashboard:
- Resource / Workload:工作负载的 CPU / 内存 usage 与配额。
- Kubernetes 节点:
cluster exporter(node-exporter)的节点状态、IO。 - 网络指标:集群级网络进出字节。
- Prometheus 自身监控:PTSDB 等自身状态。
基本上你想要的系统级监控,这一键装完都有。
黑盒监控:Blackbox Exporter 监控网站可用性
日常最常接到的需求是「7×24 小时监控公司网站可用性」,用 Prometheus 就能解决——这就是黑盒监控(Blackbox),因为无需接入任何业务逻辑。
实现它需要装两个东西:
- Blackbox Exporter:负责真正去请求目标网站并返回指标。
- 一个 Probe CRD(Kube Prober):声明要监控哪些网站、用什么模块、多长时间探一次。
# blackbox-probe.yaml(示意)
apiVersion: monitoring.coreos.com/v1
kind: Probe
metadata:
name: website-availability
namespace: monitoring
spec:
module: http_2xx # 期望 2xx 返回
prober:
url: http://blackbox-exporter.monitoring.svc.cluster.local:9115
targets:
staticConfig:
targets:
- https://www.infoq.cn # 你的目标网站列表
- https://www.geekbang.org
interval: 10s # 每 10 秒探一次
部署后,在 Prometheus 的 Target 里会出现 blackbox job,对应你配置的两个网站。Prometheus 会访问这个黑盒探针、Grafana 端再加载内置的 Blackbox dashboard,就能看到每个网站的状态、返回码、SSL 证书过期时间、探针完成时间、DNS 检查耗时等一整套可用性指标。
flowchart LR
PROM[Prometheus] -->|请求带 mode=target 参数| BB[Blackbox Exporter]
BB -->|真正请求| SITE[目标网站]
SITE -->|返回结果| BB
BB -->|输出指标 /metrics| PROM
PROM --> GF[Grafana Blackbox 面板]这里值得注意一个细节:Prometheus 发起的请求,Object 的 endpoint 其实是同一个 Blackbox Exporter 地址,只是带上了 module 和 target 两个 HTTP 参数——Blackbox 再根据 target 参数真正去请求你的网站。
白盒监控:在 Go 服务里暴露自定义指标
黑盒之外是白盒监控——侵入业务代码,让应用自己输出指标。以 Go 为例,只需集成三个官方包:
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/collectors"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
然后对外提供一个 /metrics 接口,Prometheus 定期来抓取。Go 官方 SDK 输出指标时有四种类型:
| 类型 | 英文 | 特点 | key 方法 |
|---|---|---|---|
| 计数器 | Counter | 只能往上加 | Inc() / Add(float64) |
| 仪表盘 | Gauge | 可升可降的当前值 | Set(float64) |
| 直方图 | Histogram | 分桶统计,算分位 | Observe(float64) |
| 摘要 | Summary | 客户端算分位(更精确但开销大) | Observe(float64) |
我们写一个最简单的 HTTP 服务,集成三个指标:请求总数(Counter)、响应状态码计数(Counter)、请求耗时直方图(Histogram):
// Demo:Prometheus 白盒监控
package main
import (
"math/rand"
"net/http"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
// 1. 请求总数:Counter,带 status 和 method 两个标签
var httpRequestsTotal = promauto.NewCounterVec(prometheus.CounterOpts{
Name: "http_requests_total",
Help: "Total number of HTTP requests",
}, []string{"method"})
// 2. 响应状态码:Counter,带 status 标签
var httpResponseStatus = promauto.NewCounterVec(prometheus.CounterOpts{
Name: "http_response_status",
Help: "Count of HTTP response statuses",
}, []string{"status"})
// 3. 请求耗时:Histogram,带 method 标签,默认桶
var httpRequestDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "HTTP request response time",
}, []string{"method"})
// 中间件:所有请求都会经过
func prometheusMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
rw := &respWriter{ResponseWriter: w, statusCode: http.StatusOK}
next.ServeHTTP(rw, r)
httpRequestDuration.WithLabelValues(r.Method).Observe(time.Since(start).Seconds())
httpResponseStatus.WithLabelValues(fmt.Sprint(rw.statusCode)).Inc()
})
}
关键点:
- 标准计数器写法:
prometheus.NewCounterVec(...)里传Name/Help两种标签;Counter 只能Inc()增加。 - 中间件必须挂在 HTTP 所有请求上,这样每个请求进入/返回时都会更新指标。
- 最后用
promauto.NewXxx自动注册 +promhttp.Handler()暴露/metrics,Prometheus 就能抓到你自定义的指标(它能在/metrics里同时看到 Go 运行时堆栈等指标)。
把服务 go build 成镜像推到仓库,部署成 Deployment,可以加一个 sidecar 容器持续请求接口来制造流量,作为压测/模拟来源(转写里这个 sidecar 反复因为镜像问题 container not found / ImagePullBackOff,最终换官方镜像解决——这类 sidecar 镜像的可用性是常见坑)。
让 Prometheus 抓取业务指标:ServiceMonitor
业务应用能输出指标后,剩下的问题是让 Prometheus 知道去哪抓。原理和抓 K8s 指标一样——告诉 Prometheus 抓指标的接口地址。用 CRD 声明式配置即可,首选 ServiceMonitor(Service 带负载均衡,可通过 label 匹配多个 Pod);PodMonitor 适合没有 Service 的场景(直接抓 Pod / job / deployment)。
ServiceMonitor 这样写:
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: demo2
labels:
# 必须和 Prometheus 的 serviceMonitorSelector 匹配,否则不会被捕获
release: kube-prometheus-stack
spec:
selector:
matchLabels:
app: demo2 # 匹配你的 Service 标签
endpoints:
- port: http # 注意:是 Service 里的端口名,不是端口号
interval: 30s # 多久抓一次
path: /metrics
ServiceMonitor 最大的好处是「责任分离」:工程团队只负责把接口暴露出来,基础设施团队只负责写 ServiceMonitor 这个 CRD 并部署。它还会自动注入一组 relabel 标签(pod、namespace、service、container、instance、job),让你查询时直接用这些标签定位,非常方便。
ServiceMonitor 的 metadata.labels 必须和 Prometheus 实例里配置的 serviceMonitorSelector(或 release 标签)匹配。不写会让你的 ServiceMonitor 静默地不被任何 Prometheus 捕获——排查目标抓取时最容易忽略这一点。部署后在 Prometheus 的 Targets 里能看到这个 target 已被抓(Health=UP),并在 Graph 里查到 http_requests_total、http_response_status、http_request_duration_seconds_bucket 这些指标。
PromQL:从瞬时值到聚合查询
PromQL 和 LogQL 很像——查某个指标,再用标签选择器定位到具体的指标:
http_requests_total{method="GET"}
这种瞬时值频率很高、看意义不大,所以通常用它做聚合查询。下面介绍三种常用的聚合写法。
1) 按接口分组的平均 QPS(Counter 配 rate):
sum(rate(http_requests_total[1m])) by (method)
由内而外读:http_requests_total 是 Counter;[1m] 取过去 1 分钟;rate() 算变化速率;sum ... by (method) 按 method 分组求和——得到每个接口的 QPS。去掉 by (method) 得到的就是系统总 QPS。健康检查和 /metrics 因为是周期性采样、速率恒定,画出来是直线。
2) 计算请求耗时 P95 分位(Histogram 配 quantile):
histogram_quantile(0.95,
sum by (le) (rate(http_request_duration_seconds_bucket[1m])))
_bucket 分桶本质也是 Counter,所以先 rate() 再聚合,最后用 histogram_quantile(0.95, ...) 算出第 95 百分位的请求延迟。sum by (le) 按桶上限分组。
在 Grafana 里做一个指标监控面板
用上面的 PromQL,我们可以在 Grafana 建一个多 panel 的 Dashboard,通常包含:
| 面板 | PromQL / 说明 |
|---|---|
| 实时 QPS | sum(rate(http_requests_total[$__rate_interval])),Gauge 类型 |
| QPS 趋势 | 同上,Time series 类型 |
| 总请求数 | sum(http_response_status) by (status),按状态码分组 |
| 接口 QPS 明细 | sum(rate(http_requests_total[1m])) by (method) |
| 请求延迟分位 | histogram_quantile(0.95, sum by (le) (rate(http_request_duration_seconds_bucket[$__rate_interval]))) |
Prometheus 生产实践的两个坑
坑一:Histogram 默认 bucket 导致分位计算偏差。很多人直接用官方默认 bucket,但采集后算分位会不符合实际。因为默认 bucket 覆盖范围偏 Web 通用(如 01000s),而你的业务延迟如果只在 100200ms,大部分值都落在同一桶里(如 0~250ms),算分位就产生偏差。解决办法是按你的业务自定义 bucket 边界,更贴合现状。如果不喜欢桶的粒度,也可以用 Summary,但它在 SDK 侧(客户端)算分位,开销更大、不支持跨实例聚合。
一句话决策:需要聚合就选 Histogram,需要非常精确的分位数且范围分布未知就选 Summary。
坑二:annotation 方式抓取不推荐。网上很多教程教你用 prometheus.io/scrape: "true" 注解抓指标,这种方式把「抓取配置」和「应用定义」耦合了,维护成本高,且不支持多端口、不支持认证/relabel、不支持标签选择。所以后来 Prometheus 才加入 ServiceMonitor / PodMonitor 增强抓取——生产里用 ServiceMonitor 就能解决大部分问题。
第三讲:集成 OpenTelemetry SDK
目标:让 log / metric / trace 都带上同一个 trace_id
前两讲我们分别打通了日志和指标,但它们是割裂的。这一讲用 Python + FastAPI 完整集成 OpenTelemetry SDK,让三条信号能带上同一条 trace_id,为下一讲的三合一面板做铺垫。
集成前先了解一条核心思路:真正要把三样数据关联起来,靠的就是 trace_id / span_id 这条唯一标识:
- Trace(追踪):通过 SDK 把 span 以 gRPC/OTLP 方式发到 Tempo(Grafana Tempo,分布式追踪后端),Tempo 里能看到完整追踪和自定义 attribute。
- Logs(日志):集成 OTel 后,在输出日志时带上
trace_id和span_id(从 OTel SDK 里取),这样就能用 trace_id 把日志和追踪关联起来。 - Metrics(指标):用 Prometheus exporter 输出指标时,利用 exemplar(示例) 机制把
trace_id注入进指标。需要打开 Prometheus 的 exemplar 存储开关,否则这段注释(exemplar)会被直接丢弃。
搭建 Python + FastAPI 服务
创建 demo3 目录,引用 OTel / FastAPI 相关库:fastapi、uvicorn、opentelemetry-api/sdk、opentelemetry-instrumentation-fastapi、opentelemetry-exporter-otlp 等。为了抽象,我们把指标和一个 setup_open_telemetry 抽到 utils.py 里。
在 utils.py 里定义一组业务指标(用 Prometheus 客户端),包括:
http_requests_total(Counter,按 path + app_name)http_response_total(Counter,按 path + status_code + app_name)http_request_duration_seconds(Histogram,按 path)- 错误请求计数(Counter,按 path + exception_type)
requests_in_progress(Gauge,正在处理的请求数)
setup_open_telemetry(app, app_name, endpoint) 里做的事:
from opentelemetry import trace
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
def setup_open_telemetry(app, app_name, otlp_grpc_endpoint):
# 1. resource:给所有数据打上的资源属性(service.name、自定义 app 标签)
res = Resource.create({
"service.name": app_name,
"service.version": "1.0.0",
"app": app_name, # 之后 Grafana 三合一面板会用到这个标签
})
# 2. TracerProvider + BatchSpanProcessor(批量发送,效率高)
provider = TracerProvider(resource=res)
provider.add_span_processor(BatchSpanProcessor(
OTLPSpanExporter(endpoint=otlp_grpc_endpoint, insecure=True)))
trace.set_tracer_provider(provider)
# 3. 用官方库自动埋点 FastAPI 请求
FastAPIInstrumentor.instrument_app(app)
应用主要逻辑里的关键点:
- 读环境变量
OTEL_EXPORTER_OTLP_ENDPOINT,默认http://tempo:4317(gRPC)。 - 模拟微服务链路:应用 A 的
/check接口会请求应用 B 的/iotask和应用 C 的/cputask(其实都是同一个镜像部署成 A/B/C 三个不同的 Deployment),并带上otel.propagate注入的 headers 传递 trace context。 - 日志格式里显式加上
%(otel_trace_id)s/%(otel_span_id)s,这样日志里就带上了 trace_id、span_id。
# 日志格式:把 OTel 的 trace_id/span_id 打进每行日志
LOG_FORMAT = "%(asctime)s %(levelname)s %(name)s "
"trace_id=%(otel_trace_id)s span_id=%(otel_span_id)s %(message)s"
把三个 Flask/FastAPI 应用(App A / B / C)分别构建成 opentelemetry-python-app 镜像(A 带一个会持续请求 /check 的 sidecar),再配一个 ServiceMonitor 让 Prometheus 抓取这三服务的指标,日志则正常走 Promtail 采到 Loki。
验证:三种数据都带上了 trace_id
部署完可以在 Grafana 里做三项验证:
- 日志:Explore 选 Loki,
{app="app-a"}查最近 5 分钟,能看到日志里正常打印出trace_id=... span_id=...。 - 指标:访问
/metrics,能看到我们定义的那些指标,每个指标带 exemplar 注释(# HELP ...之后的 trace_id 那段)。 - 追踪:Tempo 的 Explore 里能看到 app-a → app-b / app-c 组成的分布式追踪链路。
一句话:只要日志/metric/trace 都带同一条 trace_id,打通三合一面板就有了前提。 这部分的 Prometheus exporter 记得开启 otlp_exemplar / exemplar 存储相关开关,否则 metric 侧的 trace_id 不会进存储。日志那边要小心:日志格式里 trace_id 字段拼错、或采集后没打上标签,后面派生字段就提取不到。
第四讲:打造日志、指标和分布式追踪三合一查询面板
什么是三合一查询面板
上一讲我们把日志、指标、追踪都接进来了,但它们在 Grafana 里还是割裂的。三合一面板要做到:这些数据都能用 trace_id 关联起来,于是可以
- 用日志去查当时的追踪;
- 用追踪去查当时的日志;
- 用指标去查当时的追踪(通过 exemplar)……
并且一键链接跳转到当时的 trace_id。这就是「日志 ⇄ 指标 ⇄ 追踪」三合一查询。
每个数据源要做的打通配置
1) Loki:派生字段(Derived Fields)提取 trace_id
配置 Loki 的目的是给日志创建派生字段,从日志内容里把 trace_id 正则提取出来,并把它用作 Tempo 的查询语法,去把 Tempo 数据一并拉过来。
在 Loki 数据源里 → Derived fields → 添加一个字段,name 为 trace_id,正则表达式从日志里匹配出 trace_id,data source 选 Tempo。
2) Prometheus:开启 exemplar 关联 Tempo
在 Prometheus 数据源里开启 exemplar 相关的 data link,把 trace_id 字段关联到 Tempo。这样在指标里看到 exemplar 标记时,就能一键跳到当时的分布式追踪。
3) Tempo:trace to logs / trace to metrics
Tempo 数据源里需要两项配置:
- trace to logs:选 Loki 为日志源,配置 service name 到 Loki 的 label 映射。例如把 Tempo 里的
service.name=app-a这个属性 relabel 成 Loki 的app标签的值。这样点「查看日志」时查询条件自动填充成{app="app-a"}。 - trace to metrics:选 Prometheus 为指标源,同样做 service name → 指标的 service 标签映射,并可配置一个查询条件(如直接给你写好的 P95 histogram_quantile 查询)。
- 另外在 Tempo → Additional settings 里把 Node graph 开启,能看到完整的调用链关系图。
把这些配置做完后你会发现,Grafana 内部就是不断做「query × data link」的转换,把一条 trace_id 从一套数据跳到另一套数据。
四个互通实验
实验一:Log → Trace(日志查到追踪)
筛选 {app="app-a"} 的日志,找到一条异常日志,里面带着 trace_id。点击这行日志,会出现衍生字段 trace_id 的链接,点进去 → 跳转到 Tempo → 就能看到当时这条日志对应的整条调用链。
sequenceDiagram
participant L as Loki 日志
participant G as Grafana
participant T as Tempo 追踪
L->>G: 日志行含 trace_id(派生字段)
G->>T: data link 用 trace_id 查询
T-->>G: 展示该 trace 调用链实验二:Metric → Trace(指标查到追踪)
在 Prometheus Explore 里查 http_request_duration_seconds_bucket{app_name="app-a"}。要点是先开启 exemplar 开关——开启后图表上会出现少量菱形标记(相关性标记,Scatter/points)。点击菱形,会弹出 Query with Tempo,再点进去就能看到当时该请求的分布式追踪调用链。
实验三:Trace → Log(追踪查到日志)
打开 Tempo,选一个过去几分钟的 trace,点击该 trace 后选择 View logs,Grafana 会自动把 service.name 的值填到 Loki 的 app 标签里,查询出该 trace_id 对应的全部日志。
实验四:Trace → Metric(追踪查到指标)
还在 Tempo 里,点击某个 trace 的 P95 链接,就能跳到 Prometheus 查看该服务当时的指标情况。
两条与 OTel 无关的链路
特别注意:三合一里有两条链路跟 OpenTelemetry 无关:
- Metric → Log:通过日志提取指标(LogQL 的
..._over_time聚合)实现,跟你是否用 OTel 无关。 - Log → Metric:通过与指标的服务标签关联来实现反向查指标,同样不依赖 OTel。
其余四条与 trace_id / exemplar 相关的链路(Log→Trace、Trace→Log、Metric→Trace、Trace→Metric)才是依赖 OTel 埋点把 trace_id 带全的。
第五讲:零代码集成 OpenTelemetry
零代码是指「trace 这块不用写代码」
零代码集成的意思是:trace 部分不需要你手写任何 OTel SDK 的代码。注意——这并不代表你的 metrics 和 logs 完全不用写代码。准确说,零代码主要省掉的是你之前在代码里做的:
setup_open_telemetry(...)那一大段(TracerProvider、SpanProcessor、endpoint 等);- 在日志里手动设置 trace_id / span_id 的代码。
不过业务应用自己的 Prometheus 集成 / 日志那部分代码仍然是需要的。
原理:opentelemetry-instrument 自动注入
以 Python 为例,零代码集成的核心机制是:在启动时用 opentelemetry-instrument 去启动 Python。它会注入埋点能力——这是一种类似「插装」的机制。
# 原来
python main.py
# 零代码:换成(本质是检测并自动插装)
opentelemetry-instrument python main.py
需要安装的库:opentelemetry-distro、opentelemetry-exporter-otlp、opentelemetry-instrumentation-fastapi 等。
改造代码时,把 utils.py 里的 setup_open_telemetry 整个删掉、man.py 里相关的 trace / 日志配置删掉,只保留 metrics 和业务逻辑即可——因为这些不是面向追踪的。
用环境变量代替代码配置
零代码方式下,你要在 Deployment 的 manifest 里通过环境变量把 OTel 配置注入进去(这些是 OpenTelemetry 标准的 OTEL_* 环境变量):
# 零代码集成的关键:环境变量 + 复写启动命令
spec:
containers:
- name: app
image: opentelemetry/python-app:latest
env:
# 资源属性:相当于代码里 Resource.Create 的那几个 attribute
- name: OTEL_RESOURCE_ATTRIBUTES
value: "app=opentelemetry-python"
- name: OTEL_SERVICE_NAME
value: "opentelemetry-python"
# 指标 / 日志 / 追踪 exporter 与 endpoint
- name: OTEL_METRICS_EXPORTER
value: "otlp"
- name: OTEL_LOGS_EXPORTER
value: "otlp"
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: "http://tempo:4317"
# 日志级别
- name: OTEL_EXPORTER_OTLP_LOGS_LEVEL
value: "debug"
# 用自动注入的启动器复写启动命令
command:
- opentelemetry-instrument
- python
- main.py
ports:
- containerPort: 8080
关键环境变量:
| 环境变量 | 作用 |
|---|---|
OTEL_RESOURCE_ATTRIBUTES | 资源属性,相当于代码里的 Resource.create(含自定义 app 标签) |
OTEL_SERVICE_NAME | 服务名 |
OTEL_METRICS_EXPORTER / OTEL_LOGS_EXPORTER | 指定 metrics / logs exporter 为 otlp |
OTEL_EXPORTER_OTLP_ENDPOINT | 发送到 Tempo/Collector 的 gRPC endpoint |
OTEL_EXPORTER_OTLP_LOGS_LEVEL | 日志级别(如 debug) |
同时部署一个 ServiceMonitor 让 Prometheus 抓取它的指标。
零代码的效果与局限
部署后验证:
- 日志:在 Loki 里查
{app="opentelemetry-python"}——你并没有配置任何日志相关的 OTel 代码,但每行日志仍然自动带上了trace_id/span_id(是自动注入进去的)。 - 追踪:Tempo 里能看到该服务自动生成的 trace。
- 指标:exemplar 也会被集成进去,但
trace_id这一项可能有问题——因为零代码没有在指标里配置 exemplar,会导致「用指标查调用链」这条链路不通。
零代码集成的核心局限:它很难同时打通 log / metric / trace 三样,尤其是指标侧无法可靠带 trace_id(exemplar),所以「用指标查到追踪」这条链路经常断。
SDK 集成 vs 自动集成(零代码)
| 维度 | SDK 集成 | 自动集成(零代码) |
|---|---|---|
| 是否修改业务逻辑 | 需要改源代码 | 基本不改源码 |
| 定制性 | 更好 | 一般,只能通过环境变量/复写内置变量 |
| 是否引入额外启动器 | 不需要 | 需要引入 opentelemetry-instrument(动态库/启动器) |
| 实现方式 | 自己控制 | 依赖语言本身的插装机制 |
| 集成难度 | 较大(编码量多) | 较小(编码量少) |
| 三端全打通能力 | 好 | 弱(尤其指标 exemplar) |
总结
这一章我们从零把 OpenTelemetry 的三条可观测链路全部跑通并关联起来了:
- Loki 日志:从 ELK/EFK 到 Loki 的选型演进;用 Helm 部署 Loki + Promtail + Grafana;LogQL = 日志流选择器 + 管道操作符;logfmt/json 结构化后才能做运算;三种部署方式对应不同日志量。
- Prometheus 指标:K8s 系统指标来自 cAdvisor 和 Metrics API;用 kube-prometheus-stack 一键预配置;Blackbox Exporter 做网站可用性黑盒监控;Go 服务集成 Prometheus SDK 暴露 Counter/Gauge/Histogram;
ServiceMonitor声明式抓取 + PromQL 聚合与分位;Histogram bucket 与 annotation 的坑。 - 集成 OTel SDK:Python + FastAPI 全量集成,让 trace/log/metric 都带同一条 trace_id,日志格式注入 trace_id/span_id,指标用 exemplar 关联 trace。
- 三合一面板:Loki 派生字段、Prometheus exemplar data link、Tempo 的 trace-to-logs / trace-to-metrics + Node graph,实现 Log⇄Metric⇄Trace 互通跳转。
- 零代码集成:用
opentelemetry-instrument+ 环境变量自动注入;局限在指标 exemplar 无法可靠打通,追求 All-in-One 建议用 SDK 集成。
把这些带同一 trace_id 的日志、指标、追踪落到存储(Loki / Prometheus / Tempo)后,下一步就可以喂给 AIOps——用大模型 / Agent 做异常检测、根因定位与自动修复了。这正是整个训练营最后要达成的目标。
自测题与动手练习
自测题(合上书能答出来,才算懂):
{app="loki"} 选 Pod/业务)+ 管道操作符(|=/!=/|~/!~ 做关键字或正则匹配)。要先对字段做运算,必须先用 \| logfmt 或 \| json 解析器把字段解析成结构化标签值,才能对 duration、status 这类字段比大小、分组。/var/log/pods 等目录(Kubelet 完成)。Promtail 用 DaemonSet 每 Node 一个副本 + hostPath 只读挂载宿主机目录,像 tail -F 一样实时读新增日志行并推给 Loki,从而间接采到该节点所有 Pod 日志。pod/namespace/service/container/job 等 relabel 标签;而 annotation 方式配置与应用耦合、不支持多端口/认证/relabel/标签选择,不推荐。sum(rate(http_requests_total[1m])) by (method)(Counter 用 rate 算速率再聚合)。P95:histogram_quantile(0.95, sum by (le) (rate(http_request_duration_seconds_bucket[1m])))(直方图先 rate 再按 le 聚合再取分位)。trace_id/span_id(OTel SDK 取);指标通过 exemplar 把 trace_id 注入(需开启存储);追踪天然带 trace_id 存 Tempo。这样每条日志、每个指标、每条 trace 都指向同一请求。opentelemetry-instrument python main.py 自动插装,通过 OTEL_* 环境变量注入配置,无需手写 setup_open_telemetry 和日志里手动设 trace_id 的代码。局限:metrics 侧无法可靠带 exemplar trace_id,导致「用指标查到追踪」链路经常不通;要真正的 All-in-One 三合一建议用 SDK 集成。动手练习(建议真做一遍):
- 在 K3s 集群里用 Helm 分别安装 Loki + Promtail + Grafana,配好
X-Scope-OrgID租户头后,写一条{app="..."} |= "error" | logfmt | duration > 5ms的 LogQL,并顺手用quantile_over_time从日志里算出按状态的 P95 耗时。 - 把 Go 服务的
promauto.NewHistogramVec换成一个自定义 bucket(贴合你 100~200ms 的业务延迟),对比 /metrics 里_bucket的分桶变化,理解默认 bucket 的偏差来源。 - 部署三服务 Python 应用(App A/B/C 模拟链式调用),确认日志/metric/trace 都带同一条 trace_id 后,再按第四讲把 Loki 衍生字段、Prometheus exemplar、Tempo trace-to-logs/metrics 配好,分别做完 Log→Trace、Metric→Trace、Trace→Log、Trace→Metric 四个互通实验。
- 把同一个应用改成
opentelemetry-instrument+ 环境变量启动,改用 env 注入OTEL_*,验证日志仍自动带 trace_id,再验证「用指标查 trace」这条链路是否还通。
本章小结
- Loki:只索引时间戳+labels,故轻量低存储成本;LogQL = 选择器 + 管道;结构化(logfmt/json)后才能算字段;三种部署方式按日志量选(单体→简单可扩展→微服务)。
- Prometheus:K8s 系统指标来自 cAdvisor 与 Metrics API;kube-prometheus-stack 一键预配置;黑盒用 Blackbox Exporter;白盒用 Prometheus SDK 暴露 Counter/Gauge/Histogram;生产用
ServiceMonitor而非 annotation。 - OTel SDK:集成核心是让 log/metric/trace 都带同一 trace_id——日志打 trace_id/span_id、指标走 exemplar、追踪天然带 trace_id。
- 三合一面板:Loki 派生字段 + Prometheus exemplar + Tempo trace-to-logs/metrics + Node graph,四步点穿 Log⇄Metric⇄Trace;两条与 OTel 无关的链路是 metric⇄log(LogQL 提指标 / 服务标签关联)。
- 零代码:
opentelemetry-instrument+OTEL_*环境变量自动注入,省掉 trace 埋点代码;但指标 exemplar 打不通,追求 All-in-One 用 SDK 集成更稳。 - 下一篇我们会把这套带统一 trace_id 的可观测数据接到 LLM / Agent,做自动根因分析与自动修复——把「看到问题」升级成「解决」。