
1. 项目概述一套可观测性组合拳背后的真实需求做后端和运维时间久了大家基本都会遇到这么个场景线上某个接口突然变慢用户投诉已经进来一轮你打开监控大盘看到CPU、内存全部正常登录服务器翻日志发现报错信息散落在十几个文件里想要把一次请求从网关到数据库的完整路径拼出来得挨个系统对时间戳手动串半天——运气好十分钟定位运气差折腾一整个下午最后发现是某个下游服务连接池被打满了。这套全链路可观测性方案就是用来解决这个串不起来、查不透彻的痛点。它用 OpenTelemetry 统一生成Metrics、Logs、Traces三类数据Prometheus负责指标存储与告警Loki做日志聚合Tempo做链路追踪最后全部接入Grafana做统一可视化。我最早接触这套组合是在一个日活百万的电商中台项目里当时系统拆分出几十个微服务排查问题越来越吃力从调研选型到完整落地用了大半个月现在沉淀下来这套从零到可以上线运行的整体方案真心建议所有被排查慢、观测散折磨过的团队都来参考。这套方案的适用对象非常明确微服务数量多但还没建立规范观测体系的团队、正在从监控往可观测性转型的技术团队、希望用一套开源技术栈避免厂商锁定的小组。它解决的核心问题有三个让一次请求的前世今生有迹可循让指标异常和日志报错、Trace断点能互相印证让新同学也能借助统一面板迅速定位故障而不是靠老师傅的经验。我评估过不少同类方案比如单独用ELK做日志用Jaeger做追踪用SkyWalking做APM各有各的好处但要同时覆盖指标、日志、追踪三类数据且这几类数据之间还要能互相串联跳转目前开源社区里最顺滑的组合就是OTel加PLGT这套。理由后面详细说先记住这句话可观测性的目标不是堆工具而是让三类数据在同一个时间轴上互相对齐快速还原故障现场。2. 核心设计与技术选型拆解为什么是OTel加PLGT这一套2.1 可观测性的三个支柱到底在说什么刚开始接触可观测性时很容易把Metrics、Logs、Traces理解为三种孤立的数据。其实它们是从三个不同维度描述同一个系统状态互相补充。打个比方系统是一辆车。Metrics指标是仪表盘告诉你车速多少、油量剩多少、水温是否正常——它是聚合后的数值优点是可以长期低成本存储、设置阈值告警缺点是它只告诉你哪里不正常不告诉你为什么不正常。Logs日志是行车记录仪记录了每一个具体事件比如13:05:22 这条SQL执行了3秒——它是离散的信息量最大但如果没有关联字段很难把同一段旅程的事件按顺序串起来。Traces链路追踪是GPS轨迹记录了一次请求从出发到结束经过的每一个节点和耗时——它是结构化的调用链能还原一次请求的完整路径但单条Trace日常存储成本不小没人会为所有请求永久保留完整轨迹。这套方案的核心思路就是用OpenTelemetry把三类数据的产生端统一起来通过trace_id、service.name这些公共标签在存储端把它们关联起来。凡是打点埋过OTel的服务Metrics里能看到服务健康度Logs里能找到具体报错Traces里能还原完整调用链三者在Grafana里通过一个trace_id或者标签就能互相跳转。这套组合拳打通之后排查效率提升是肉眼可见的。2.2 技术选型对比这套组合的生态协同优势我在做选型时拿这四款工具分别和主流替代品对比过结论很清晰。Prometheus和InfluxDB、VictoriaMetrics这类时序数据库相比最大优势是生态标准。Prometheus的Exporter体系几乎覆盖了所有中间件MySQL、Redis、Kafka、Nginx全有现成的采集器告警规则通过PromQL表达也足够灵活。虽然VictoriaMetrics在超大规模存储上性能更强但对于绝大多数团队单机Prometheus加上合理的数据保留周期已经非常够用。Loki对标的自然是ELK。Elasticsearch全文检索能力强悍但要为高可用部署多个节点还要伺候索引生命周期。Loki的设计思路完全不同——它不为每条日志建立全文索引而是像Prometheus一样为日志打标签用标签过滤再对查询范围内的日志做内容检索。这个设计让它对存储和内存的消耗大幅下降和Grafana无缝集成特别适合以Kubernetes为核心运维场景的中小团队。Tempo和Jaeger对比也很有意思。Jaeger自带界面功能完整但需要单独部署一套UI数据模型和OTel的交互也没有Tempo那么原生。Tempo的设计初衷就是做Grafana生态里的Trace后端本身不带界面全部通过Grafana的Tempo数据源查询。这样用户访问入口只有一个——Grafana学习成本和运维成本都低。OpenTelemetry是这套组合的连接器。它是CNCF的标准化项目目标是统一Metrics、Logs、Traces三类数据的产生和传输协议。选择它而不是直接用各家SDK就等于告诉业务团队只需要学一套埋点规范不用关心后端是Prometheus还是Jaeger以后即使存储层要换业务代码几乎不用动。2.3 数据模型的串联指标、日志、链路如何互相咬合三类数据不是各自孤零零存着而是通过统一资源模型串联起来的。OTel规范里定义了Resource这个核心概念每个服务在产生数据时都会带上service.name、service.namespace、host.name、k8s.pod.name这类公共属性。Prometheus指标里的标签可以记录job和instanceLoki日志里通过label提取出trace_id字段Tempo链路里天然就是trace_id和span_id的树形结构。真正让它们互相咬合的钥匙就是trace_id。日志里打印了trace_id就能在Grafana日志面板一键跳转到Tempo查看完整链路指标异常时也能通过service.name下钻到对应的日志和最近的新Trace。这套联动机制需要在落地时刻意设计而不是默认就有的。我在下面实操部分会详细教大家怎么把trace_id从OTel SDK一路传递到日志系统里这一步做通整套系统的价值立刻翻倍。3. 落地实操从部署到接入埋点的完整过程3.1 环境准备docker-compose搭起一套全家桶我建议第一阶段追求的是快速跑通全流程不需要Kubernetes环境一台服务器或者开发机用Docker Compose就能把整套系统搭起来。需要准备的服务有Prometheus、Grafana、Loki、Tempo、OpenTelemetry Collector以及一个用来演示埋点的Demo应用。写一个docker-compose.ymlPrometheus挂载配置文件Loki挂载配置目录Tempo暴露端口接收OTLP数据Grafana的 provisioning目录里自动配置好数据源。这个初始版全部使用默认配置先把通路跑通。几个关键配置值得说明一下。Prometheus的配置文件里除了抓取自身指标最重要的是创建两个独立的job。一个job用来抓取OpenTelemetry Collector暴露的指标端口端口一般是9464路径是/metrics另一个job用于抓取业务服务通过Prometheus exporter暴露的指标。如果你是首次使用Prometheus建议先不用配太多告警规则等数据源稳定了再慢慢加。Tempo需要启用几个关键配置才能配合Grafana工作。最主要的是开启metrics_generator功能它可以从Trace数据里自动生成服务拓扑和RED指标Rate、Errors、Duration这样即便服务没有单独暴露指标Grafana的Tempo数据源也能展示服务之间的调用关系和错误率。接收OTLP数据的端口默认是4317gRPC和4318HTTP需要在Compose中映射出来。Loki的配置在初期其实可以非常简洁只需要指定存储路径和一个本地文件系统不需要启用多租户模式把需要对外的端口映射好。Grafana界面里创建Loki和Tempo数据源时URL直接填服务名和端口即可。3.2 应用接入埋点优先推零代码的Agent方案对于Java技术栈的团队我强烈建议第一阶段优先使用OpenTelemetry Java Agent方式真正零代码改动。直接在JVM启动参数里加javaagent服务不用改任何业务代码就可以自动完成HTTP调用、JDBC、Redis客户端、消息队列等常用框架的埋点采集。启动命令大概是这样的java -javaagent:/opt/otel/opentelemetry-javaagent.jar \ -Dotel.service.nameorder-service \ -Dotel.metrics.exporterotlp \ -Dotel.logs.exporterotlp \ -Dotel.traces.exporterotlp \ -Dotel.exporter.otlp.endpointhttp://otel-collector:4318 \ -jar order-service.jar这里有一个很关键的点日志的exporter也发往OTLP很多人会忽略。OTel支持把日志往Collector发送Collector可以再转发给Loki。但这里有个限制Java Agent自动捕获日志时日志必须通过SLF4J这类框架输出Agent才能把日志和当前Trace关联起来。如果你的项目用的是Log4j2需要确保采用SLF4J桥接否则trace_id不会自动出现在日志上下文里。对于非Java语言项目比如Go或者Python就没有Agent这种零侵入的方案了需要用OTel SDK手动埋点。以Python为例FastAPI服务通过OpenTelemetry Instrumentor可以自动创建Span但手动埋点可以控制更细粒度。原理上都一样——在请求入口创建根Span在调用下游的时候创建子Span将父Span的context传递过去。无论哪种语言都要确保一个原则在服务入口处生成的trace_id要能够被日志采集组件提取出来。Java Agent会默认把trace_id和span_id注入到日志的MDC中如果日志格式里有对应占位符但也需要你的日志pattern里包含%X{trace_id}和%X{span_id}并且日志必须输出了这些字段Loki侧才能提取。3.3 打通数据链路Collector的配置是重中之重在整套体系里OTel Collector是一个数据中转站也是最容易被低估的组件。它的作用可以理解为连接SDK和后端的交通枢纽接收SDK发来的OTLP数据经过批处理、压缩、资源修改等步骤再分发到Prometheus、Loki、Tempo不同的后端。Collector的核心配置文件是config.yaml分为receivers、processors、exporters、service四个大段。开发阶段可以简单配置但生产环境有几个处理器值得优先安排。第一个是batch处理器。没有它SDK产生的每个Span和每个Metric都会单独发往后端造成大量小请求Collector和后端的CPU、内存都会浪费在协议解析上。batch处理器会积攒一段时间内的数据合并成一个大批次发送默认配置可以设置timeout为5秒、send_batch_size为10000条效果立竿见影。第二个是resource处理器。它可以给数据统一添加公共标签比如从环境变量读取的环境名、机房、团队信息。这样在Grafana里可以按团队或者环境去筛选数据不会有遗漏。第三个是memory_limiter处理器用于保护Collector自身。当下游后端比如Loki或者Tempo响应变慢或暂时不可用时Collector内存会迅速攀升如果不限制内存使用Collector自己就会被打挂。配置内存上限为总内存的一半左右并设置检查间隔积累的数据持续达到上限时会触发降级丢弃数据。Exporters这块Tempo作为Trace后端直接配置endpoint指向Tempo的接收端口Prometheus作为Metrics后端需要配置单独的exporter端点让Prometheus定期来抓取Loki作为日志后端需要配置认证信息和无证书模式。这里有件事需要反复强调发送给Loki的数据最好在日志中保留原始级别。Loki目前对Trace的关联是通过提取日志内容中的trace_id字段实现的。所以日志exporter必须保留Trace上下文中的trace_id和span_id字段并在Loki的采集流水线里用正则提取成标签这样Grafana中Loki日志和Tempo Trace才能实现自动跳转。3.4 为日志和链路加上关联标签在Loki中配置提取trace_id是在PromeLoki的采集配置里完成的。在docker-compose中部署Loki时需要在Loki配置文件的scrape_configs或pipeline_stages中做正则提取。以我常用的Loki配置为例scrape_configs: - job_name: otel-logs pipeline_stages: - regex: expression: trace_id(?Ptrace_id[a-f0-9]{32}) - labels: trace_id: relabel_configs: - source_labels: [__meta_kubernetes_pod_label_app] target_label: service_name这里的regex表达式是专门匹配OTel生成的trace_id格式默认是32位十六进制字符。如果你的SDK生成的trace_id格式不同比如带横线正则表达式要对应调整。标签设计有一条铁律用来筛选的字段才做成标签用来检索的内容保留在日志正文里。Loki的索引就是标签标签太多会拖慢查询标签值变动频繁更是大忌。比如pod名称这种每秒都在变的字段做成标签只会导致索引膨胀查询变慢。标准的做法是把pod名称存储在日志正文里或者做成低基数的service.name标签。Tempo侧也一样Tempo按trace_id索引数据所以Grafana的Tempo数据源配置时不需要额外做关联只要Loki日志中能提取出trace_idUI面板会提供从日志行到Trace详情的跳转链接。3.5 指标链路的打通与核心参数设定Metrics这部分是相对最成熟的流程也最顺理成章。OTel SDK需要把指标导出到CollectorCollector通过prometheus exporter暴露一个HTTP端点然后Prometheus去拉取。配置Prometheus抓取Collector指标时需要注意抓取间隔。我习惯把抓取间隔设为30秒或60秒太长会丢细节太短会对Collector和存储造成压力。Prometheus还有两个非常重要的参数scrape_timeout必须小于scrape_interval否则会报错evaluation_interval决定告警规则的评估频率一般设成scrape_interval的倍数。对于业务指标的埋点几个最常见的类型要区分清楚。Counter是只增不减的累计值适合记录请求总数、错误总数Gauge是可以上下浮动的瞬时值适合记录当前连接数、队列长度Histogram是值分布的累计统计适合记录接口耗时分布比如p50、p95、p99。一个合格的请求指标集至少要包含这三类数据总请求数和错误数的Counter处理耗时的Histogram以及当前在途请求数的Gauge。在SDK侧我通常建议命名遵循Prometheus命名规范以命名空间和单位结尾如http.server.request.duration。这套规范在OpenTelemetry语义约定里写得很清楚照着做就不会乱。3.6 Grafana面板搭建让三类数据出现在同一个界面Grafana数据源配置好之后最有价值的是创建几块典型面板。推荐至少搭建四个面板。第一个是服务总览面板。上方选时间范围下面的核心指标图展示每秒请求数、错误率、p95耗时。这个面板基于Prometheus数据源适合放在团队墙上的大屏里一眼看清所有服务健康状态。第二个是日志检索面板。使用Loki数据源上方是LogQL查询输入框可以输入{service_nameorder-service}过滤出某个服务的所有日志再按时间实时刷新。配合Grafana的Trace to Logs特性从日志行可以直接打开对应的Trace。第三个是链路追踪面板。使用Tempo数据源支持直接输入trace_id查询单条Trace也支持按服务名和耗时列表从中筛选慢Trace。这个面板是排查慢请求的利器。第四个是服务依赖拓扑图。通过Tempo的metrics_generator生成的服务图数据支持在Grafana界面直接看到服务与服务的调用关系。谁调用了谁、哪个环节延迟高在图上一目了然非常直观。4. 从0到可上线推进路径与数据量预估4.1 分阶段推进先跑通再覆盖先核心后边缘我建议落地这套体系千万别想着一步到位分三条路走最稳。第一阶段是可见花费一到两天实现上面说的一整套Docker化部署接入两个核心服务做Demo在Grafana看到三类数据。这一阶段的验证目标是技术跑通发现坑及时解决。第二阶段是可查把Agent都接入到所有Java服务让trace_id贯穿所有日志完善日志标签和指标面板沉淀出一套服务部署和配置标准。这一阶段的目标是让一线研发在排障时主动使用Grafana而不是遇到问题直接翻服务器日志。第三阶段是可告警基于采集到的历史指标逐步添加Prometheus告警规则、告警分组和通知渠道。告警做在前面使系统能够在用户发现问题之前就主动通知值班同学。每阶段之间的间隔建议在一到两周。一套观测系统先跑通比什么都强。4.2 容量评估一套小账本决定你的存储策略开始大规模接入前最常被忽视的是容量评估。有一件事业务同学不会关心但你必须提前想明白Trace数据比Metrics大一个数量级越细的链路越占空间。以某服务日均100万请求为例做估算。假设平均一个请求产生15个Span每个Span序列化后约800字节到1KB一天产生的Trace原始数据约为100万乘以15再乘以1KB就是15GB。Loki日志存储的膨胀比例大约是1比1.3Prometheus指标存储与抓取频率和标签数量强相关一套规范的采集方案一天大概也只有几百MB。这意味着如果不做采样Trace存储每天增加至少15GB一个月接近450GB一年的成本相当可观。而且Trace数据是排障数据保留太久并不划算。因此我的建议是Metrics保留30到90天Loki日志保留15到30天Trace只保留7到15天且必须做采样。Trace采样配置推荐使用尾部采样在Collector端根据已经完成的链路决定哪些Trace送入后端存储比如把耗时超过某阈值的慢请求100%保留普通请求保留10%。这样既能覆盖排障高发场景又能控制成本。4.3 上线前必做的三项检查清单上线前有几件事很容易被遗漏专门列出来提醒一下。第一项是检查所有服务的时区。OpenTelemetry默认用UTC时间很多团队的日志系统用的是本地时区如果时区不统一会导致日志与Trace在时间轴上对不上需要额外花很多时间对齐。第二项是检查trace_id是否真的在日志里。用一个测试请求打一个小接口到Loki里查对应日志看trace_id字段是否存在、格式是否匹配。很多人推进到这一步就卡住实际原因是日志框架的MDC功能没打开或pattern没配置。第三项是检查Grafana的告警渠道通知是否走通。Prometheus告警默认会推送到AlertmanagerAlertmanager再通过webhook或者邮件发送到钉钉、企微这类渠道。大家很多时候配好了告警规则结果消息发不出来只能等到线上出状况才发现这是最让人抓狂的情况。5. 常见问题与排查技巧实录5.1 数据不显示按这三个位置逐层排查接入完成后面板为空这是第一周最常见的求助问题。排查顺序一定要遵循数据流向。先看数据源状态。在Grafana的Configuration里检查Prometheus、Loki、Tempo三个数据源是否标为Healthy。如果数据源本身就是红色的大概率是地址没填对或者端口没映射出去。Compose环境下可以用docker exec -it grafana curl http://loki:3100/ready验证网络连通性。再看采集端。Prometheus界面Targets页面检查job的up状态如果显示down就看endpoint是否可连通。Loki这里查看接受到的日志数可以看loki_ingester_received_chunks_total指标。最后看查询语句和日志字段。如果面板为空但Targets是绿的多半是查询的标签不匹配。Loki的标签是精确匹配{service_nameorder-service}和{service_nameorder-service }多了一个空格完全是两个结果。在排查时我习惯先用{__name__~.}全量看有没有数据再逐步缩小标签范围。5.2 Trace断链链路总是在某一步中断链路断掉是接入过程中仅次于数据不显示的帐篷级问题。常见的断链原因有三个。第一个是异步线程没有传播Context。使用了线程池或者异步调用后新线程里不会自动带上前一个线程的Trace上下文。这时需要显式调用Span.current().makeCurrent()或者使用io.opentelemetry.context.Context.current()传递。线上排查时凡是看到多线程到异步之间的Span就断了基本都是这个原因。第二个是消息队列消费时没有看Consumer端是否接收了消息头里的Trace Context。SDK确实会自动解析消息队列的Header但前提是消息生产者写入的消息Header中包含W3C Traceparent。如果业务代码在包装消息时手工修改了Header可能会把Trace上下文丢失。第三个是网关层没有透传HTTP Header。有的网关框架默认只透传业务自定义Header不会自动透传traceparent。此处的解决办法是在网关层开启透传或者用SDK的Propagator注入否则下游拿不到原始trace_id链路上每个服务都自成一条链。5.3 日志与Trace关联不上先从格式与大小写查起日志里有trace_id但Grafana无法跳到Tempo这个是Loki侧正则匹配失败造成的。最常见的坑是SDK输出了32位小写十六进制但日志格式里带上了双引号或者中括号正则匹配没找到。用Loki的text查询先确认日志原貌再根据原貌修正正则表达式。建议在正则表达式中预留灵活的空白符号比如trace_id[ ]*([a-f0-9]{32})。另一个坑是大小写混用。W3C标准对trace-id字段大小写不敏感但OTel Java Agent默认输出小写如果用Python SDK生成了大写Loki的正则表达式就得加上(?i)或者写成匹配大小写不敏感的模式。模板里统一全部转小写最保险。5.4 告警噪音怎么控制告警不打扰告警规则上线后铺天盖地的告警会让所有人麻木。核心要领是给告警分级和加持续时间。比如接口成功率低于90%这个条件如果只设了阈值没有任何持续时间网络抖动一下就会触发告警。Prometheus里通过for字段设置至少持续5分钟才报警就可以过滤掉大量瞬时抖动。持续1小时才报警的属于低级别需要当天处理的属于中级持续5分钟就需要立即响应的属于高级。告警内容也可以做得更可操作。Alertmanager的通知模板里建议把跳转到对应Grafana面板的URL拼接好比如{{ .ExternalURL }}或者自定义grafana_host变量加上实际面板ID。这样值班同学点开链接就能看到可视化图表而不是看到一个干巴巴的表达式。5.5 验证一次全链路排查慢接口问题实战还原拿一个真实的故障场景讲一下三支柱是怎么协同工作的。某次线上反馈订单查询接口变慢用户页面等待转圈。你打开服务总览面板发现order-service的p95耗时从200ms飙到3秒错误率没有明显上升初步判断是下游响应慢。顺着面板点到日志面板输入{service_nameorder-service} | query_order看到若干条日志拖尾出现redis timeout字样而且日志行里有trace_id。点击那条日志的Trace链接跳转到Tempo完整链路展示出来发现Redis查询的Span异常直接显示Redis命令超时。再回到Prometheus查Redis的Exporter指标发现Redis慢查询数和连接数暴涨定位到某个热点keyRedis集群锁争用严重。整个排查过程从发现异常到定位根因大概十来分钟三类数据多次互相跳转基本没有来回翻系统的操作。6. 落地过程中沉淀的经验与新探索前面把全链路可观测性从前到后走了一遍最后再补充一段我个人在落地过程中最深的一些体会。第一点是这套体系技术难点并不是部署和配置而是规范和协作。部署、接入等等操作一天就能做完但是什么样的指标需要埋点、每个服务需要保留多少日志、Trace采样的比例怎么统一、告警分级的规则谁来定这些问题如果不在项目启动前和所有业务团队达成一致推进过程中就会不断返工。我的建议是找一个核心团队先立标准做一个简单的接入文档里面写清楚Trace命名规范、日志字段约定、指标命名规则、数据保留周期这些硬性约束让其他业务线照着执行。第二点是监控不是让你看到所有数据而是让你看到需要知道的数据。任何时候指标500个不如50个设计得合理、能指导决策的指标。特别是告警规则少而准永远比多而杂强。我们的情况是团队最先设计了几百条告警规则上线头三天告警几乎刷屏经过两轮精简现在保留的告警不足最初的五分之一但几乎每一条报警都能对应一个实际问题。第三点是可观测性建设是一个逐渐演进的过程不必一次追求完美。这套PLGT组合的好处在于基础设施搭好之后后续接入新服务只是一个标准操作按既有规范配置Agent和日志。随着接入的服务越来越多面板和告警规则也会慢慢优化这套体系会自动沉淀出团队的运维经验。如果你所在团队正准备搞微服务可观测性别犹豫了从今天开始搭一套跑通它接上你自己的服务用一段时间你就会理解排障体验可以如此顺畅。