从源码到部署:基于开源项目构建可观测微服务的实战指南
很多团队做微服务时,第一阶段通常都很顺:服务能跑、接口能通、容器能起。但一到联调、压测、线上灰度,问题就开始集中爆发:
- 某个接口突然变慢,到底慢在哪一层?
- 调用链跨了 3 个服务,异常日志散在不同容器里,根本串不起来
- CPU 不高但用户超时严重,怀疑是数据库连接池或线程池配置有问题
- 服务在本地正常,部署后却没有指标、没有 trace、日志也没结构化
说白了,系统“能跑”不等于“可运营”。而可观测性(Observability)正是把“黑盒运行”变成“可理解运行”的关键能力。
这篇文章我不打算只讲概念,而是按一个更接地气的路径来:从源码启动一个开源微服务,到接入指标、日志、链路追踪,再到容器化部署与排障。你可以把它当成一份可跟着操作的实战教程。
背景与问题
在中级工程实践里,很多人已经会:
- 用 Spring Boot 或类似框架写服务
- 用 Docker 打包部署
- 配 Nginx、MySQL、Redis
- 接一点基础日志
但如果没有完整的可观测性体系,问题仍然很难定位。
这里我们选一个最小但完整的技术组合,避免一上来就把系统堆得过重:
- 微服务框架:Spring Boot
- 指标采集:Micrometer + Prometheus
- 可视化面板:Grafana
- 链路追踪:OpenTelemetry + Jaeger
- 日志:Logback + JSON 输出
- 容器编排:Docker Compose
为什么这么选?
- 全是开源项目
- 社区成熟,资料多
- 既适合本地演练,也能平滑迁移到 Kubernetes
- 足够覆盖“指标 + 日志 + trace”三件套
我们会实现两个微服务:
order-service:订单服务inventory-service:库存服务
下单时,订单服务会调用库存服务扣减库存。这样自然形成一条调用链,方便我们观察 trace、日志与指标。
前置知识
开始前,你最好具备这些基础:
- 会读简单的 Spring Boot 项目结构
- 知道 REST API 是怎么调用的
- 了解 Docker 和 Docker Compose 的基本命令
- 对 Prometheus / Grafana / Jaeger 听说过,不要求很熟
如果你刚接触 OpenTelemetry,也不用担心,本文会直接给可运行配置。
环境准备
建议环境:
- JDK 17
- Maven 3.8+
- Docker 20+
- Docker Compose 2+
- curl 或 Postman
目录结构我建议这样组织:
observable-microservices/
├── order-service/
├── inventory-service/
├── docker-compose.yml
├── prometheus.yml
└── grafana/
核心原理
可观测性最常见的三个支柱是:
- Metrics(指标):告诉你“系统整体是否正常”
- Logs(日志):告诉你“具体发生了什么”
- Traces(链路):告诉你“一个请求经过了哪些服务、每段耗时多少”
这三者各管一部分,单独用都不够,联合起来才真正有效。
1. 指标:从“感觉慢”变成“量化慢”
Prometheus 定期拉取服务暴露的 /actuator/prometheus 指标,比如:
- 请求总数
- 响应时延
- JVM 内存
- 线程池状态
- 数据源连接池
如果一个接口 P95 延迟突增,通常先看指标能快速判断是全局变慢,还是某个依赖变慢。
2. 日志:从“报错了”变成“上下文完整”
日志如果只是普通文本,线上检索成本会很高。比较好的做法是:
- 输出 JSON
- 带上
traceId、spanId - 保留业务关键字段,比如
orderId、skuCode
这样一条异常日志就能和一条 trace 串起来。
3. 链路追踪:从“猜测瓶颈”变成“定位瓶颈”
一个请求从网关进入订单服务,再调用库存服务,最终访问数据库。Trace 能展示:
- 哪些服务参与了调用
- 每一步耗时
- 哪个 span 抛了异常
- 下游调用是快还是慢
一张图看整体架构
flowchart LR
U[User / Client] --> O[order-service]
O --> I[inventory-service]
O --> P[(Prometheus Metrics Endpoint)]
I --> P
P --> G[Grafana]
O --> J[Jaeger]
I --> J
O --> L[Structured Logs]
I --> L
这张图表达的是:两个服务既处理业务,也持续输出“可观测数据”。
调用链时序图
sequenceDiagram
participant C as Client
participant O as order-service
participant I as inventory-service
participant J as Jaeger
participant P as Prometheus
C->>O: POST /orders
O->>I: POST /inventory/deduct
I-->>O: success
O-->>C: order created
O->>J: export trace/span
I->>J: export trace/span
P->>O: scrape /actuator/prometheus
P->>I: scrape /actuator/prometheus
逐步搭建:从源码写出可运行微服务
下面我们从零做两个服务。为了控制篇幅,我用最小实现,但保留可运行性。
第一步:创建 inventory-service
inventory-service/pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>inventory-service</artifactId>
<version>1.0.0</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
</parent>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
<dependency>
<groupId>io.opentelemetry.instrumentation</groupId>
<artifactId>opentelemetry-spring-boot-starter</artifactId>
<version>2.4.0</version>
</dependency>
</dependencies>
</project>
inventory-service/src/main/java/com/example/inventory/InventoryApplication.java
package com.example.inventory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class InventoryApplication {
public static void main(String[] args) {
SpringApplication.run(InventoryApplication.class, args);
}
}
inventory-service/src/main/java/com/example/inventory/InventoryController.java
package com.example.inventory;
import io.micrometer.core.annotation.Timed;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@RequestMapping("/inventory")
public class InventoryController {
private static final Logger log = LoggerFactory.getLogger(InventoryController.class);
@PostMapping("/deduct")
@Timed(value = "inventory_deduct_duration", description = "Inventory deduct duration")
public Map<String, Object> deduct(@RequestParam String skuCode, @RequestParam Integer count) throws InterruptedException {
log.info("deduct inventory, skuCode={}, count={}", skuCode, count);
if (count <= 0) {
throw new IllegalArgumentException("count must be greater than 0");
}
Thread.sleep(100);
return Map.of(
"success", true,
"skuCode", skuCode,
"count", count
);
}
}
inventory-service/src/main/resources/application.yml
server:
port: 8081
spring:
application:
name: inventory-service
management:
endpoints:
web:
exposure:
include: health,info,prometheus
endpoint:
health:
show-details: always
otel:
service:
name: inventory-service
exporter:
otlp:
endpoint: http://jaeger:4318
traces:
exporter: otlp
第二步:创建 order-service
order-service/pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>order-service</artifactId>
<version>1.0.0</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
</parent>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
<dependency>
<groupId>io.opentelemetry.instrumentation</groupId>
<artifactId>opentelemetry-spring-boot-starter</artifactId>
<version>2.4.0</version>
</dependency>
</dependencies>
</project>
order-service/src/main/java/com/example/order/OrderApplication.java
package com.example.order;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@SpringBootApplication
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
order-service/src/main/java/com/example/order/OrderController.java
package com.example.order;
import io.micrometer.core.annotation.Timed;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
import java.util.Map;
import java.util.UUID;
@RestController
@RequestMapping("/orders")
public class OrderController {
private static final Logger log = LoggerFactory.getLogger(OrderController.class);
private final RestTemplate restTemplate;
public OrderController(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
@PostMapping
@Timed(value = "order_create_duration", description = "Order create duration")
public Map<String, Object> createOrder(@RequestParam String skuCode, @RequestParam Integer count) {
String orderId = UUID.randomUUID().toString();
log.info("create order start, orderId={}, skuCode={}, count={}", orderId, skuCode, count);
String url = "http://inventory-service:8081/inventory/deduct?skuCode=" + skuCode + "&count=" + count;
Map inventoryResp = restTemplate.postForObject(url, null, Map.class);
log.info("create order success, orderId={}, inventoryResp={}", orderId, inventoryResp);
return Map.of(
"success", true,
"orderId", orderId,
"inventory", inventoryResp
);
}
}
order-service/src/main/resources/application.yml
server:
port: 8080
spring:
application:
name: order-service
management:
endpoints:
web:
exposure:
include: health,info,prometheus
endpoint:
health:
show-details: always
otel:
service:
name: order-service
exporter:
otlp:
endpoint: http://jaeger:4318
traces:
exporter: otlp
第三步:加入结构化日志
严格说,完整的日志平台通常会接 ELK 或 Loki。不过为了让这篇教程聚焦,我先做最关键的一步:日志里保留 trace 上下文。这样哪怕先用 docker logs 看,也已经比纯文本强很多。
order-service/src/main/resources/logback-spring.xml
<configuration>
<property name="CONSOLE_LOG_PATTERN"
value="%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] traceId=%X{trace_id} spanId=%X{span_id} %logger - %msg%n"/>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
</root>
</configuration>
inventory-service/src/main/resources/logback-spring.xml
<configuration>
<property name="CONSOLE_LOG_PATTERN"
value="%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] traceId=%X{trace_id} spanId=%X{span_id} %logger - %msg%n"/>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
</root>
</configuration>
我个人经验是:traceId 一旦进日志,排障效率会明显提高。哪怕你暂时没上 ELK,这一步也很值。
第四步:编写 Dockerfile
order-service/Dockerfile
FROM maven:3.9.6-eclipse-temurin-17 AS builder
WORKDIR /app
COPY pom.xml .
COPY src ./src
RUN mvn clean package -DskipTests
FROM eclipse-temurin:17-jre
WORKDIR /app
COPY --from=builder /app/target/order-service-1.0.0.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
inventory-service/Dockerfile
FROM maven:3.9.6-eclipse-temurin-17 AS builder
WORKDIR /app
COPY pom.xml .
COPY src ./src
RUN mvn clean package -DskipTests
FROM eclipse-temurin:17-jre
WORKDIR /app
COPY --from=builder /app/target/inventory-service-1.0.0.jar app.jar
EXPOSE 8081
ENTRYPOINT ["java", "-jar", "app.jar"]
第五步:部署 Prometheus、Grafana、Jaeger
prometheus.yml
global:
scrape_interval: 5s
scrape_configs:
- job_name: 'order-service'
metrics_path: '/actuator/prometheus'
static_configs:
- targets: ['order-service:8080']
- job_name: 'inventory-service'
metrics_path: '/actuator/prometheus'
static_configs:
- targets: ['inventory-service:8081']
docker-compose.yml
version: '3.9'
services:
order-service:
build: ./order-service
container_name: order-service
ports:
- "8080:8080"
depends_on:
- inventory-service
- jaeger
inventory-service:
build: ./inventory-service
container_name: inventory-service
ports:
- "8081:8081"
depends_on:
- jaeger
prometheus:
image: prom/prometheus:v2.52.0
container_name: prometheus
ports:
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
grafana:
image: grafana/grafana:10.4.2
container_name: grafana
ports:
- "3000:3000"
jaeger:
image: jaegertracing/all-in-one:1.57
container_name: jaeger
ports:
- "16686:16686"
- "4318:4318"
第六步:启动整个系统
在项目根目录执行:
docker compose up --build
看到服务都正常启动后,访问:
- 订单服务:
http://localhost:8080/actuator/health - 库存服务:
http://localhost:8081/actuator/health - Prometheus:
http://localhost:9090 - Grafana:
http://localhost:3000 - Jaeger:
http://localhost:16686
第七步:发起一次业务请求
curl -X POST "http://localhost:8080/orders?skuCode=SKU-1001&count=2"
如果成功,你会得到类似响应:
{
"success": true,
"orderId": "7c6f1e80-4f6a-4d24-b8e4-0d8f1a8c3456",
"inventory": {
"success": true,
"skuCode": "SKU-1001",
"count": 2
}
}
第八步:验证可观测数据是否完整
这个步骤非常重要。很多人服务启动起来就以为接入成功了,实际上 trace 没上报、指标没抓到、日志没有上下文,最后排障时才发现链条断了。
验证清单
1. 验证指标
浏览器打开:
http://localhost:8080/actuator/prometheushttp://localhost:8081/actuator/prometheus
确认能看到类似:
http_server_requests_seconds_count
jvm_memory_used_bytes
order_create_duration_seconds_count
inventory_deduct_duration_seconds_count
在 Prometheus 页面查询:
http_server_requests_seconds_count
2. 验证链路
打开 Jaeger:
http://localhost:16686
选择服务 order-service,点击 Find Traces。你应该能看到一条请求链路,里面包含:
order-serviceinventory-service
3. 验证日志
查看容器日志:
docker logs order-service
docker logs inventory-service
确认日志中有:
traceId=...spanId=...
可观测组件之间的关系
classDiagram
class Microservice {
+REST API
+Business Logic
+Actuator Endpoint
+Trace Export
+Structured Logs
}
class Prometheus {
+scrape metrics
}
class Grafana {
+dashboards
+visual query
}
class Jaeger {
+trace store
+trace search
}
Microservice --> Prometheus : expose metrics
Prometheus --> Grafana : data source
Microservice --> Jaeger : export spans
实战代码补强:加入自定义业务指标
默认 JVM 和 HTTP 指标已经很有用,但业务指标更直接。比如我们记录“订单创建总数”。
order-service/src/main/java/com/example/order/OrderMetrics.java
package com.example.order;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.stereotype.Component;
@Component
public class OrderMetrics {
private final Counter orderCreatedCounter;
public OrderMetrics(MeterRegistry meterRegistry) {
this.orderCreatedCounter = Counter.builder("business_order_created_total")
.description("Total created orders")
.register(meterRegistry);
}
public void incrementOrderCreated() {
orderCreatedCounter.increment();
}
}
修改 OrderController.java
package com.example.order;
import io.micrometer.core.annotation.Timed;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
import java.util.Map;
import java.util.UUID;
@RestController
@RequestMapping("/orders")
public class OrderController {
private static final Logger log = LoggerFactory.getLogger(OrderController.class);
private final RestTemplate restTemplate;
private final OrderMetrics orderMetrics;
public OrderController(RestTemplate restTemplate, OrderMetrics orderMetrics) {
this.restTemplate = restTemplate;
this.orderMetrics = orderMetrics;
}
@PostMapping
@Timed(value = "order_create_duration", description = "Order create duration")
public Map<String, Object> createOrder(@RequestParam String skuCode, @RequestParam Integer count) {
String orderId = UUID.randomUUID().toString();
log.info("create order start, orderId={}, skuCode={}, count={}", orderId, skuCode, count);
String url = "http://inventory-service:8081/inventory/deduct?skuCode=" + skuCode + "&count=" + count;
Map inventoryResp = restTemplate.postForObject(url, null, Map.class);
orderMetrics.incrementOrderCreated();
log.info("create order success, orderId={}, inventoryResp={}", orderId, inventoryResp);
return Map.of(
"success", true,
"orderId", orderId,
"inventory", inventoryResp
);
}
}
然后在 Prometheus 中查询:
business_order_created_total
这类指标特别适合做业务看板,比如每分钟下单量、成功率、失败率。
Grafana 面板建议
Grafana 里建议先做 4 类基础图表:
-
接口吞吐
rate(http_server_requests_seconds_count[1m])
-
接口延迟
histogram_quantile(0.95, sum(rate(http_server_requests_seconds_bucket[5m])) by (le, uri))
-
JVM 内存
jvm_memory_used_bytes
-
业务订单数
business_order_created_total
如果你是第一次搭面板,不用追求“大而全”,先把这几个关键图跑起来,排障时就已经很有帮助。
常见坑与排查
这一节我尽量写得像真实排障,因为这些问题我确实见过不少。
1. Jaeger 里看不到 trace
现象
请求已经打通,但 Jaeger 页面没有任何链路数据。
排查顺序
- 检查
otel.exporter.otlp.endpoint是否写对 - 确认 Jaeger 是否开放了
4318端口 - 看应用日志中是否有 exporter 报错
- 确认请求确实经过了被自动埋点的组件
常见原因
- 写成了
localhost:4318。在容器里,localhost指向容器自身,不是 Jaeger。 - 用了错误协议端口。
- 依赖版本不兼容。
建议
容器互联时,优先使用服务名:
otel:
exporter:
otlp:
endpoint: http://jaeger:4318
2. Prometheus 抓不到指标
现象
Prometheus 的 Targets 页面显示 DOWN。
排查顺序
- 打开服务自身的
/actuator/prometheus - 检查
management.endpoints.web.exposure.include - 检查
prometheus.yml的目标地址是否是容器名 - 查看服务是否真的监听了对应端口
常见原因
- 忘了引入
micrometer-registry-prometheus - Actuator 端点没暴露
- 写成
localhost:8080,而 Prometheus 容器访问不到宿主机 localhost
3. traceId 没进日志
现象
Jaeger 有链路,但日志里没有 traceId。
排查方向
- 检查日志 pattern 是否包含
%X{trace_id}和%X{span_id} - 检查 OpenTelemetry starter 是否生效
- 确认日志框架是 Logback 而不是别的实现覆盖了
4. 调用链断掉,只看到 order-service
现象
Jaeger 里只有上游服务,看不到下游 inventory-service span。
常见原因
- 下游服务没接 OpenTelemetry
- HTTP 客户端未被正确自动埋点
- 自定义客户端绕过了框架默认拦截
如果你后面换成 WebClient、Feign 或 OkHttp,也要验证上下游 trace 传播是否仍然成立,不要想当然。
5. 指标太多,Grafana 查询很慢
原因
默认指标很多,如果标签维度控制不好,时间序列会迅速膨胀。
典型错误
把高基数字段当成标签,比如:
orderIduserIdtraceId
这些值几乎每次都不同,会让 Prometheus 压力激增。
原则
指标适合聚合,不适合记录每个请求的唯一标识。 唯一标识应放到日志和 trace,不要放到 metrics label。
安全/性能最佳实践
这一节很重要,因为“可观测性本身”也会影响系统。
1. 不要把所有 Actuator 端点都暴露出去
很多教程图省事会写:
management:
endpoints:
web:
exposure:
include: "*"
本地调试还行,线上不建议。更稳妥的是只开放必要端点:
management:
endpoints:
web:
exposure:
include: health,info,prometheus
如果是公网环境,最好再配访问控制,至少不要裸奔。
2. 日志里不要打印敏感信息
尤其避免直接输出:
- 用户手机号
- 身份证号
- token
- 密码
- 完整 SQL 参数中的隐私字段
我见过最离谱的一次,是排查方便把整个请求头都打出来,结果把认证信息带进日志。这个坑真的不值当。
建议做法:
- 对敏感字段脱敏
- 仅记录必要业务上下文
- 错误堆栈只在服务端保留,不回传给前端
3. 控制 trace 采样率
不是每个请求都必须全量追踪,特别是高 QPS 服务。
例如生产环境可以考虑:
- 低流量核心服务:较高采样率
- 高流量接口:按比例采样
- 异常请求:优先保留
如果全量采样,链路系统本身会吃掉不少网络和存储成本。
4. 指标命名遵循统一规范
建议:
- 用业务前缀
- 单位写清楚
- 含义单一
比如:
business_order_created_totalinventory_deduct_duration_seconds
而不要写这种模糊名:
order_counttime_costmetric1
名字一旦乱,后面面板和告警都会越来越难维护。
5. 业务日志、访问日志、错误日志分层
如果项目再大一点,我建议至少分三类:
- 业务事件日志
- HTTP 访问日志
- 错误异常日志
否则所有日志混在一起,关键异常会被大量普通信息淹没。
6. 给超时、重试和线程池加指标
可观测性不是只盯 HTTP 请求。真正影响稳定性的,往往是这些内部资源:
- 线程池活跃线程数
- 队列积压
- 数据库连接池使用率
- 下游调用超时次数
- 重试次数
如果后续你接数据库、消息队列、缓存,这些指标要尽快补上。
从本地到生产:部署边界怎么把握
本文用的是 Docker Compose,这很适合:
- 本地学习
- 小型测试环境
- 单机演示
- 团队内部 PoC
但如果你进入生产环境,通常会演进到:
- Kubernetes 部署服务
- Prometheus Operator 管理监控
- Grafana 做统一看板
- Tempo / Jaeger / SkyWalking 做链路
- Loki / ELK 做日志平台
- Alertmanager 做告警路由
这里的关键不是“立刻上全家桶”,而是先把最小链路打通:
- 服务暴露指标
- 请求带 trace
- 日志能关联 traceId
- 关键看板和告警先跑起来
只要这四点做到,系统的“可维护性”就已经和纯黑盒状态完全不同了。
一份实用的逐步验证清单
如果你打算把本文方案真正落地,我建议照着这份清单做,不容易漏项。
功能验证
-
order-service启动成功 -
inventory-service启动成功 - 下单接口能返回成功
- 错误参数能触发异常路径
指标验证
-
/actuator/prometheus可访问 - Prometheus Targets 全部
UP - 能查到
http_server_requests_seconds_count - 能查到
business_order_created_total
链路验证
- Jaeger 能看到
order-service - trace 中包含
inventory-service - 异常请求也能看到错误 span
日志验证
- 日志带
traceId - 日志带
spanId - 关键业务字段可检索
- 不包含敏感信息
运维验证
- 容器重启后系统可恢复
- Prometheus 配置变更可生效
- Grafana 数据源可正常连接
- 服务依赖异常时有足够观测数据
总结
从源码到部署,构建一个可观测微服务,核心不是多装几个组件,而是把这条链真正连通:
- 指标告诉你系统是否异常
- 日志告诉你具体发生了什么
- 链路告诉你问题在哪一跳发生
本文这套方案的价值在于,它不是纸上谈兵,而是一个能本地跑起来的最小闭环:
- 写两个微服务
- 暴露 Prometheus 指标
- 输出带 traceId 的日志
- 用 OpenTelemetry 上报链路
- 用 Docker Compose 整体部署
- 通过一次真实请求完成验证
如果你准备在自己的项目里落地,我的建议是:
- 先做最小闭环,不要一开始就追求全量平台化
- 先保证 trace、metrics、logs 能互相关联
- 先覆盖核心业务链路,再逐步扩展到数据库、缓存、MQ
- 生产环境重点关注采样率、标签基数、敏感信息和端点暴露
边界条件也要明确:
- 这套 Compose 方案适合学习、测试和 PoC,不是最终生产形态
- 日志这里只做了控制台结构增强,真正生产通常还要配日志采集系统
- 指标和 trace 默认配置偏演示,生产需要结合流量规模做精细化调优
如果你按这篇文章完整走一遍,最大的收获通常不是“我会用了 Prometheus/Jaeger”,而是你开始真正建立起一种工程意识:服务上线,不只是能提供接口,还要能解释自己为什么正常、为什么失败、为什么变慢。 这才是微服务走向可运营、可维护的关键一步。