白云喝了人间酒——从零跑通一个云原生应用的可观测性链路
“白云喝了人间酒”,第一次看到这句话,我脑子里冒出来的并不是诗,而是一个很具体的工程画面:一朵云上的服务,终于接到了人间真实的业务流量。
这句诗落到技术世界里,其实就是三件事——服务能跑,流量能接,问题能看见。前两件事大多数团队上云之后都能做到,但第三件事往往被严重低估。很多项目容器化做完、Kubernetes 集群也搭好了,接口正常返回,流量也进来了,结果线上一告警,所有人对着 Grafana 看半天也说不清到底哪一环出了问题。白云是飘上去了,酒也倒进去了,但没人品得出味道。
这篇文章不聊云原生的高大上概念,而是用一套最小可运行的示例项目,完整走一遍容器化 -> 部署到 Kubernetes -> 接入 Prometheus 指标采集 -> 接入 Loki 日志收集 -> 接入 Tempo 链路追踪 -> 在 Grafana 里把三类数据串起来的全过程。文章里所有的代码和配置都可以直接复制到自己的环境验证。读完你能回答三个问题:一朵云上的服务怎么接住真实流量;出问题的时候,怎么在三分钟内定位到具体环节;以及这套可观测性体系在生产环境里要避开哪些坑。
1. 这篇文章真正要解决的问题
先说一个很多团队都经历过的场景。
应用上了 Kubernetes,Pod 也起来了,业务同学反馈“下单失败”。后端同学登录跳板机,先看 Pod 状态,发现一直在重启;看 Events,说是探针失败;看日志,发现日志打到 stdout 了但采集不到;想查一下这次失败到底走了哪些服务,发现链路追踪根本没接;最后只能靠猜,猜不出来就回滚。整个过程持续半小时以上,业务投诉、Leader 追问,压力全在值班的人身上。
这不是某一家公司的特例。从材料来看,云原生落地过程中最常见的三个痛点分别是:
- 部署靠手工:镜像标签乱打,配置改完不知道有没有生效,回滚靠运气。
- 故障定位慢:没有统一的指标、日志、链路追踪平台,出了问题只能逐台机器查。
- 资源成本失控:Node 数量涨上去了,但不知道每个应用真正的资源水位,扩容靠拍脑袋。
这篇文章的核心判断是:可观测性不是可选项,而是云原生应用的基础设施。只有把指标、日志、链路追踪三类数据全部打通,云上的服务才算真正“喝到了人间的酒”——每一笔业务请求发生什么、慢在哪、错在哪,都能被看见、被分析、被复盘。
适合读这篇文章的读者有三类:
- 后端开发:想搞清楚自己写的服务部署到 K8s 之后,怎么被监控、日志怎么采、链路怎么串。
- 运维 / SRE:想把传统的“监控告警”升级为完整的可观测性体系。
- 云原生学习者:对 Docker、Kubernetes 有基础了解,想通过一个最小项目把知识串成闭环。
2. 核心概念:云原生、容器与可观测性三支柱
2.1 从“酿酒”到“品酒”的比喻
理解云原生,可以借用一句比喻:传统部署方式是你在本地酿好一坛酒,搬到一台固定的服务器上供人品尝;云原生则是把酿酒过程标准化成一条流水线,酒装进标准容器里,由调度系统自动分配到最合适的酒桌(Node)上,客人来了自动开坛,客人走了自动撤台。
这套体系里四个核心角色分工明确:
- 镜像:把应用和它的运行环境打包成不可变的标准产物。
- 容器运行时:负责让镜像跑起来,隔离进程和资源。
- Kubernetes:负责调度、伸缩、自愈,管理容器的生命周期。
- 可观测性平台:负责回答“现在发生什么、之前发生了什么、为什么会这样”。
2.2 可观测性三支柱:Metrics、Logs、Traces
可观测性不是单一产品,而是三类数据能力的组合:
| 支柱 | 英文 | 回答的问题 | 典型工具 | 类比 |
|---|---|---|---|---|
| 指标 | Metrics | 系统整体健康吗?请求量、错误率、延迟如何? | Prometheus | 体温、血压 |
| 日志 | Logs | 某个具体事件里发生了什么细节? | Loki、ELK | 病历记录 |
| 链路追踪 | Traces | 一次请求经过了哪些服务,每段耗时多少? | Tempo、Jaeger | 工厂流水线追溯单 |
三类数据解决的问题不同,但需要联动才能高效定位问题。比如指标告诉你订单失败率突增,日志告诉你某条错误码大量出现,链路追踪告诉你请求卡在了下游支付服务。单独看任何一类,都只能看到部分真相。
2.3 为什么传统监控不够用
传统监控模式下,Zabbix 告诉你“这台机器 CPU 100%”,但你不知道哪个进程导致的;日志平台能搜到异常堆栈,但你不知道这次异常影响了多少用户;APM 工具能看调用链路,但覆盖不全。根本原因是三类数据独立建设、没有关联维度。
云原生可观测性强调的“可观测”,不是多装几个工具,而是让三类数据拥有统一的标签体系,比如命名空间、Pod 名称、服务名、Trace ID。这样指标异常时可以一键跳转到关联日志和链路,定位路径被大幅缩短。
3. 环境准备与前置条件
既然是实操文章,环境先说明清楚。
本文示例基于 Linux 或 macOS 开发机,建议 8GB 以上内存。为了保证过程可复现,我们需要准备以下工具:
| 工具 | 作用 | 说明 |
|---|---|---|
| Docker | 构建镜像、本地运行容器 | 版本以你实际环境为准 |
| kubectl | 与 Kubernetes 集群交互 | 需要和集群版本兼容 |
| Kubernetes 集群 | 运行示例应用 | 推荐 minikube 或 k3s,也可以使用云厂商托管集群 |
| Helm | 部署 Prometheus 等组件 | 可选,但建议安装 |
| Prometheus + Grafana | 指标采集与可视化 | 本文用 Helm Chart 演示 |
| Loki + Promtail | 日志收集与存储 | 与 Grafana 集成 |
| Tempo | 链路追踪存储和查询 | 支持 OpenTelemetry 协议 |
版本这里不做死板指定,因为工具链更新很快。建议实践时选择当前官方稳定版本即可。
创建示例项目的目录结构如下:
demo-ordersvc/ ├── src/main/java/com/example/ordersvc/ │ ├── OrdersvcApplication.java │ └── controller/OrderController.java ├── src/main/resources/application.yml ├── Dockerfile ├── k8s/ │ ├── deployment.yaml │ ├── service.yaml │ ├── configmap.yaml │ └── prometheusrule.yaml └── pom.xml这里用 Spring Boot 写一个最小订单服务,暴露两个接口:GET /api/order/{id}查询订单,POST /api/order创建订单。关键点是服务内要引入 Micrometer 暴露/actuator/prometheus指标端点,同时用 OpenTelemetry SDK 把链路数据上报给 Tempo。
4. 核心流程拆解
整个链路分成五个步骤,每一步完成之后都有明确的验证方式。
第一步:容器化应用。把 Spring Boot 服务打包成镜像。这一步决定后续所有环节能否顺利推进。镜像里要有健康检查能力,比如暴露/actuator/health。
第二步:部署到 Kubernetes。通过 Deployment 管理应用副本,通过 ConfigMap 管理环境配置,通过 Service 暴露访问入口。部署时使用资源配置限制和存活探针、就绪探针。
第三步:接入指标采集。在应用里暴露 Prometheus 格式的指标端点,通过 Prometheus 抓取指标,由 Grafana 展示。核心指标包括 QPS、错误率、响应延迟 P50/P95/P99。
第四步:接入日志收集。应用日志统一以 JSON 格式输出到 stdout,Promtail 自动采集并打上 Pod 标签,Loki 负责存储,Grafana 负责检索。
第五步:接入链路追踪。通过 OpenTelemetry Agent 自动注入,请求上下文通过 HTTP Header 在服务间传递,上报到 Tempo。
每一步做错都会引起不同故障现象,下一章我会给出排查清单。
5. 完整示例代码实现
5.1 Spring Boot 应用的 Dockerfile
# 文件路径:demo-ordersvc/Dockerfile FROM eclipse-temurin:17-jre AS runtime LABEL maintainer="devops@example.com" RUN useradd --system --create-home --shell /sbin/nologin appuser WORKDIR /app COPY target/ordersvc.jar app.jar RUN chown -R appuser:appuser /app USER appuser EXPOSE 8080 ENV JAVA_OPTS="-Xms256m -Xmx512m" ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]这里有几个容易被新手忽略的设计细节。
第一,使用非 root 用户运行应用。容器里默认是 root,一旦应用被攻破,攻击者就拿到了宿主机的 root 权限。创建独立用户是安全底线。
第二,JAVA_OPTS用环境变量抽取出来,方便在 K8s 里按环境覆盖内存参数。
第三,基础镜像选择了eclipse-temurin,如果网络环境拉取困难,可以换成阿里云镜像仓库地址。
5.2 构建镜像
mvn clean package -DskipTests docker build -t demo-ordersvc:1.0.0 .如果本地要推送到私有仓库,需要先打标签再推送:
docker tag demo-ordersvc:1.0.0 registry.example.com/demo/demo-ordersvc:1.0.0 docker push registry.example.com/demo/demo-ordersvc:1.0.0在实际项目中,镜像仓库地址通常是云厂商的镜像仓库地址,这里用registry.example.com占位,替换成自己仓库即可。
5.3 Kubernetes 部署配置
先写一个 Deployment。关键点是定义资源配额和健康检查:
# 文件路径:demo-ordersvc/k8s/deployment.yaml apiVersion: apps/v1 kind: Deployment metadata: name: demo-ordersvc namespace: demo labels: app: demo-ordersvc spec: replicas: 2 selector: matchLabels: app: demo-ordersvc template: metadata: labels: app: demo-ordersvc version: v1 spec: containers: - name: ordersvc image: registry.example.com/demo/demo-ordersvc:1.0.0 imagePullPolicy: IfNotPresent ports: - name: http containerPort: 8080 - name: metrics containerPort: 8080 envFrom: - configMapRef: name: ordersvc-config resources: requests: cpu: 100m memory: 256Mi limits: cpu: 500m memory: 512Mi readinessProbe: httpGet: path: /actuator/health port: 8080 initialDelaySeconds: 10 periodSeconds: 5 livenessProbe: httpGet: path: /actuator/health port: 8080 initialDelaySeconds: 15 periodSeconds: 10 securityContext: runAsNonRoot: true readOnlyRootFilesystem: true解释几个关键配置:
resources.requests是给调度器的“最低需求声明”,limits是容器不能超过的上限。生产环境必须配置,否则一个内存泄漏的应用可能拖垮整台 Node。readinessProbe判断应用是否能够接收流量,失败时从 Service 端点摘除;livenessProbe判断进程是否存活,失败时重启容器。注意两个探针的路径、端口和延迟参数不要照抄,要结合应用启动时间设置。readOnlyRootFilesystem开启后,如果应用有写本地文件的需求,会报权限错误,需要挂载临时目录或调整应用配置。它安全,但有成本。
接着创建 Service:
# 文件路径:demo-ordersvc/k8s/service.yaml apiVersion: v1 kind: Service metadata: name: ordersvc-svc namespace: demo labels: app: demo-ordersvc spec: type: ClusterIP selector: app: demo-ordersvc ports: - name: http port: 80 targetPort: 80805.4 ConfigMap 配置管理
# 文件路径:demo-ordersvc/k8s/configmap.yaml apiVersion: v1 kind: ConfigMap metadata: name: ordersvc-config namespace: demo data: APP_NAME: "demo-ordersvc" LOG_LEVEL: "INFO" LOG_FORMAT: "json" SPRING_PROFILES_ACTIVE: "prod" SERVER_PORT: "8080"这里把 Spring Boot 的配置抽成环境变量,动态注入到 Deployment。这样做的好处是:环境差异不污染镜像,镜像只构建一次,测试、预发、生产通过不同 ConfigMap 切换配置。
5.5 应用侧接入 Prometheus 指标
Spring Boot 项目引入 Micrometer:
<!-- 文件路径:demo-ordersvc/pom.xml --> <dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-registry-prometheus</artifactId> <version>1.13.6</version> </dependency>然后在application.yml中开启暴露端点:
# 文件路径:demo-ordersvc/src/main/resources/application.yml management: endpoints: web: exposure: include: health,prometheus metrics: tags: application: ${APP_NAME:unknown}重启应用后,通过端口转发验证指标端点:
kubectl port-forward -n demo svc/ordersvc-svc 8080:80 curl http://localhost:8080/actuator/prometheus能看到http_server_requests_seconds_count、jvm_memory_used_bytes等指标,说明指标端点已经正常工作。
5.6 部署 Prometheus 和 Grafana
这里采用 Helm 部署。如果你的环境里没有 Helm,需要先安装。
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts helm repo update创建命名空间:
kubectl create namespace monitoring安装 kube-prometheus-stack,它包含 Prometheus Operator、Prometheus、Grafana 和 Alertmanager:
helm install kube-prometheus-stack prometheus-community/kube-prometheus-stack -n monitoring看到 Pod 状态为Running后,通过端口转发访问 Grafana:
kubectl port-forward -n monitoring svc/kube-prometheus-stack-grafana 3000:80默认账号为admin,密码默认是prom-operator,部署在集群内通过 secret 获取:
kubectl get secret -n monitoring kube-prometheus-stack-grafana -o jsonpath="{.data.admin-password}" | base64 -d echo5.7 接入 Loki 日志
日志是整个可观测性链路里最容易踩坑的一环。很多团队日志没采集到,不是因为工具问题,而是应用日志格式不规范。
推荐做法是让应用直接输出 JSON 结构化日志。Spring Boot 里配置logstash-logback-encoder或者直接调整 pattern,使每行日志都包含时间、级别、服务名、Trace ID、消息体。
示例 JSON 日志:
{"timestamp":"2024-06-01T10:00:01.123Z","level":"INFO","service":"demo-ordersvc","traceId":"abc123","message":"create order success","orderId":"20240601001"}安装 Loki 相关组件:
helm repo add grafana https://grafana.github.io/helm-charts helm install loki grafana/loki-stack -n monitoringLoki 默认会安装 Promtail,Promtail 会采集每个 Pod 的 stdout 日志,并自动附加 Pod 标签。部署完成后,在 Grafana 中增加 Loki 数据源,地址为http://loki:3100。
验证日志接入成功的标志:在 Grafana 的 Explore 页面选择 Loki 数据源,输入{app="demo-ordersvc"},能看到实时日志流。
5.8 接入 Tempo 链路追踪
链路追踪选择 OpenTelemetry + Tempo 方案,因为 OpenTelemetry 已成为可观测性数据标准,语言 Agent 也很成熟。
安装 Tempo:
helm install tempo grafana/tempo -n monitoring --set tempo.receivers.otlp.protocols.grpc=true --set tempo.receivers.otlp.protocols.http=true安装 OpenTelemetry Collector(可选),或者在应用里直接配置上报地址。最轻量的方式是使用 OpenTelemetry Java Agent,直接挂载到启动命令上:
java -javaagent:/path/to/opentelemetry-javaagent.jar \ -Dotel.service.name=demo-ordersvc \ -Dotel.traces.exporter=otlp \ -Dotel.exporter.otlp.endpoint=http://tempo:4317 \ -jar app.jar这样不需要修改业务代码,Java Agent 会自动埋点,HTTP 请求的链路信息就会上报到 Tempo。
在实际项目中,建议把 Java Agent 打进镜像,而不是在容器启动时临时下载:
COPY --from=otel-agent:latest /opentelemetry-javaagent.jar /opt/opentelemetry-javaagent.jar然后在 K8s 的 Deployment 里通过环境变量传入上报地址。
5.9 Grafana 数据关联
在 Grafana 中配置三个数据源:
- Prometheus:地址
http://kube-prometheus-stack-prometheus:9090 - Loki:地址
http://loki:3100 - Tempo:地址
http://tempo:3200
配置完成后,可以在一个 Dashboard 里打通三类数据。做法是给 Prometheus 的指标面板增加链接,从指标异常跳转到对应 Pod 的日志查询;链路 ID 可以嵌入到日志字段中,从日志直接跳转 Tempo Trace 详情页。
6. 运行结果与效果验证
部署完成后,按下面的顺序验证整条链路。
第一步,检查 Pod 状态:
kubectl get pods -n demo预期输出:
NAME READY STATUS RESTARTS AGE demo-ordersvc-7d48f4d5b6-abc12 1/1 Running 0 2m demo-ordersvc-7d48f4d5b6-def34 1/1 Running 0 2m如果READY不是 1/1,说明探针失败,需要看 Pod 日志和 Events。
第二步,模拟业务流量,让服务产生指标、日志和链路数据:
kubectl port-forward -n demo svc/ordersvc-svc 8080:80 # 新开一个终端 curl -X POST http://localhost:8080/api/order \ -H "Content-Type: application/json" \ -d '{"productId":1001,"quantity":2}' curl http://localhost:8080/api/order/1第三步,验证指标。在 Grafana Explore 中选择 Prometheus,执行查询:
sum(rate(http_server_requests_seconds_count{application="demo-ordersvc"}[5m]))能画出请求量曲线,说明指标链路正常。
第四步,验证日志。在 Grafana Explore 中选择 Loki,查询:
{app="demo-ordersvc"} |= "create order success"能看到刚创建订单的日志,说明日志采集链路正常。
第五步,验证链路追踪。在 Tempo 数据源里选择最近 15 分钟,搜索demo-ordersvc,应该能看到 Traces 列表,点击后能看到 span 的耗时和标签。
如果整个过程都跑通,那么“白云喝了人间酒”这个比喻就成立了:云端服务已经完整接入了流量,而且每一笔流量的来龙去脉都能看见。
7. 常见问题与排查思路
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
Pod 一直处于ImagePullBackOff | 镜像名称不正确或镜像仓库需要认证 | kubectl describe pod <pod-name>查看 Events | 检查镜像名、标签、仓库地址;需要认证则创建imagePullSecrets |
Pod 反复重启,RESTARTS持续增长 | livenessProbe 失败,或应用启动崩溃 | kubectl logs <pod-name> --previous查看上一次日志 | 优化启动参数;调整探针initialDelaySeconds;修复启动异常 |
READY为 0/1 | readinessProbe 失败,服务没有被加入 Endpoint | kubectl describe pod <pod-name>查看探针事件 | 确认健康检查路径是否正确;确认端口是否对应 |
| Prometheus 抓不到指标 | 应用未暴露/actuator/prometheus或端口不匹配 | kubectl port-forward后手动 curl 指标端点 | 检查management.endpoints.web.exposure.include是否包含 prometheus |
| Loki 查询不到日志 | Promtail 未采集到该命名空间 | 查看 Promtail 日志;确认日志是否输出到 stdout | 统一日志输出到 stdout;检查 Promtail 配置的标签选择器 |
| 链路在 Tempo 中查不到 | OpenTelemetry Agent 未挂载或上报地址错误 | 查看应用启动日志是否有 Agent 加载 | 确认 OTLP endpoint 可达;检查端口 4317/4318 |
| Grafana 数据源无法连通 | 服务名解析失败或 svc 端口写错 | kubectl exec到 Pod 内 ping 数据源服务名 | 检查 Service 名称和端口;确认与数据源在同一集群 |
| 应用启动后日志报权限错误 | readOnlyRootFilesystem: true | 看容器启动日志中的Read-only file system | 给需要写文件的目录挂载 emptyDir 卷 |
| 节点内存持续上涨 | 应用没有配置资源 limits | 查看kubectl top node | 在 Deployment 中配置 requests/limits,并设置 JVM 最大堆 |
8. 最佳实践与工程建议
8.1 命名与标签规范是整个可观测性的地基
可观测性系统依赖标签来关联数据。如果应用名、命名空间、版本标签混乱,后续所有查询都会失效。建议从第一天就执行以下规范:
- 每个应用必须有唯一
app标签。 - Deployment、Service、ConfigMap、日志标签、指标标签使用同一个应用名。
- 镜像 tag 使用语义化版本或 Git commit 哈希,不用
latest。 - 所有容器统一端口命名,如
http、grpc、metrics。
8.2 日志格式统一为结构化日志
非结构化文本日志难以检索、难以统计、难以和链路关联。团队应该制定日志规范:JSON 格式、必须包含timestamp、level、service、traceId、message。业务关键操作必须打印入参订单号或用户 ID,这样排障时才能从日志跳到具体业务实体。
8.3 探针参数要按应用特性设置
探针是 K8s 自愈能力的核心,但配置不当会引发严重事故。有一个典型的反面案例:应用启动需要 40 秒,但initialDelaySeconds只设了 10 秒,导致 Pod 一直被 livenessProbe 杀掉重启,陷入 CrashLoopBackOff。建议:
- readinessProbe 的
initialDelaySeconds应大于应用最慢启动时间。 - livenessProbe 的判定条件要选对,不要依赖外部数据库,否则数据库抖动会导致整个应用被重启。
- 探针的
periodSeconds不宜过短,避免给应用带来额外压力。
8.4 配置管理遵循环境隔离
镜像构建一次,不同环境通过 ConfigMap 或外部配置中心差异化配置。预发环境配置了调试日志,生产环境没有;测试环境连测试库,生产连生产库。配置变更走 Git 仓库管理,使用 ArgoCD 或 GitOps 流程,避免有人手动修改线上 ConfigMap。
8.5 安全与最小权限原则
Kubernetes 是强大的平台,也是需要安全约束的系统。建议严格执行这些措施:
- 容器以非 root 用户运行。
- 只读根文件系统,必要时挂载临时目录。
- 禁止使用
privileged模式。 - RBAC 权限按命名空间最小化分配。
- 生产环境变更前先在测试集群验证,保留回滚方案。
8.6 资源成本治理
可观测性本身也会产生成本。指标数量爆炸、日志存储无限增长、链路采样率过高,都会造成成本失控。实践建议:
- 指标只保留高价值的时间序列,删除低基数的调试指标。
- 日志设置保留周期,比如热数据 7 天,冷数据 30 天。
- 链路追踪采用采样策略,核心交易接口 100% 采样,普通查询接口 10% 采样。
- 为命名空间设置 ResourceQuota,为 Node 设置自动扩缩容和 HPA 上限。
8.7 变更与回滚流程
任何改动都要有回滚能力。以 Deployment 为例,回滚到上一个版本:
kubectl rollout undo deployment/demo-ordersvc -n demo发布前建议打开滚动更新参数:
spec: strategy: type: RollingUpdate rollingUpdate: maxUnavailable: 0 maxSurge: 1maxUnavailable: 0表示发布期间不允许服务不可用,maxSurge: 1表示允许先多启动一个新副本,再摘旧副本。这是生产环境最常见的安全发布策略。
9. 总结与后续学习方向
这篇文章从一句诗出发,落到了一个具体工程问题:如何让云上的服务真正“喝到”人间真实的流量,并且让每一笔流量从进入到出现问题都能被看见。我们从容器化一个 Spring Boot 应用讲起,完成了 Dockerfile 编写、Kubernetes Deployment 和 Service 配置、Prometheus 指标采集、Loki 日志收集、Tempo 链路追踪,最后在 Grafana 中把三类数据串成一条完整的排障路径。
整条链路跑通之后,你会明显感觉到排障方式的变化——过去是登服务器翻日志碰运气,现在是先在 Dashboard 看指标趋势,缩小时间窗口,再顺着关联标签跳到具体日志和单条链路。这就是“可观测性”四个字真正落地后带来的效率提升。
下一步建议按自己的实际项目继续深入这几个方向:
- 服务网格:Istio 或 Linkerd 能提供更细粒度的流量指标,无需修改业务代码。
- 告警规则和抑制策略:把 Prometheus 的原始指标变成有业务含义的告警,配合 Alertmanager 做通知降噪。
- AIOps:在指标异常时自动关联日志和链路,缩短平均故障恢复时间。
- 混沌工程:主动注入故障,验证可观测性体系是否真的能发现问题。
最后提醒一句:可观测性体系不要等项目上线后再补,而是要在第一个接口设计时就开始考虑。日志规范、标签规范、指标命名这些基础工作,越早定下,后面的排障成本越低。建议把这篇文章的示例项目克隆到本地跑一遍,然后把你负责的服务按同样的方式接入,收藏备用。