微服务日志与调用链打通方案:一个简单但实用的思路
写在前面
2025年快过去了,今年是我写博客的第一年,文章被点赞近600次,收藏1000多次,有256个粉丝(2的8次方,这个数字很程序员),感谢大家的支持。
今天是我2025年最后一个工作日,就以这篇文章为25年画个句号。
这篇文章分享一个想法,一个关于微服务可观测性的方案论证。代码年后会写,到时候会开源出来。
明年,我们继续再战!
背景:微服务可观测性的三大支柱
做微服务的都知道,定位问题靠三样东西:
- 日志(ELK):看细节,查问题
- 调用链(SkyWalking):看性能,找瓶颈
- 监控(Prometheus):看趋势,做告警
理想很美好,现实很骨感。
痛点:日志和调用链是割裂的
痛点1:调用链看不到细节
举个例子,订单服务调用库存服务:
POST /order/create
总耗时: 1.5s] -->|HTTP调用| B[库存服务
黑盒
耗时: 1.2s] style A fill:#e1f5ff,stroke:#0366d6,stroke-width:2px style B fill:#ffd6cc,stroke:#d73a49,stroke-width:2px
在 SkyWalking 里,你只能看到:
- 订单服务调用库存服务花了 1.2秒
- 但库存服务内部干了啥?不知道!
想看库存服务的细节,得:
- 去 SkyWalking 找库存服务的 trace
- 但问题来了:哪个 trace 是对应的?
痛点2:找不到关联关系
两个 trace 之间没有明确的关联:
traceId: abc123] -.不知道关联.-> B[库存 Trace
traceId: def456] style A fill:#e1f5ff,stroke:#0366d6,stroke-width:2px style B fill:#ffe6cc,stroke:#f9826c,stroke-width:2px
结果就是:看调用链要在多个 trace 之间跳来跳去,根本串不起来。
痛点3:插件不输出日志
SkyWalking 的官方插件(OpenFeign、MyBatis、Jedis)虽然采集了数据,但:
- 只上报到 SkyWalking 后端
- 不会在应用日志里留痕迹
想看 SQL 执行了啥?对不起,日志里没有。
解决方案:用 traceId 打通一切
核心思路很简单:让所有日志都带上 traceId,然后按 traceId 串起来。
整体架构
实现分三步走
第一步:日志带上 traceId(简单)
这个很简单,Logback 配置一下就行:
xml
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{traceId}] [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
</configuration>
SkyWalking 会自动把 traceId 放到 MDC 里,直接用就行。
第二步:自研插件打日志(核心)
这是整个方案的关键。SkyWalking 提供了插件机制,我们写一个插件,在关键位置打日志:
java
public class CustomTracingInterceptor implements InstanceMethodsAroundInterceptor {
private static final Logger logger = LoggerFactory.getLogger("TRACE_LOG");
@Override
public void beforeMethod(EnhancedInstance objInst, Method method,
Object[] allArguments, Class<?>[] argumentsTypes,
Object ret) {
String traceId = ContextManager.getGlobalTraceId();
String spanId = ContextManager.getSpanId();
String parentSpanId = ContextManager.getParentSpanId();
// 打印带 traceId 的日志
logger.info("traceId={}, spanId={}, parentSpanId={}, operation={}, params={}",
traceId, spanId, parentSpanId, operation, params);
}
@Override
public void afterMethod(EnhancedInstance objInst, Method method,
Object[] allArguments, Class<?>[] argumentsTypes,
Object ret) {
long duration = calculateDuration();
logger.info("traceId={}, spanId={}, duration={}ms, result={}",
traceId, spanId, duration, result);
// 如果是入口且超过1秒,写入 MySQL
if (isEntrySpan() && duration > 1000) {
saveToMySQL(traceId, endpoint, duration, startTime);
}
}
}
这个插件相当于 AOP,能拦截到:
- HTTP 调用
- RPC 调用
- 数据库操作
- Redis 操作
- 消息队列
然后把这些操作都打印成日志,自然就带上了 traceId。
第三步:MySQL 存索引(辅助)
为了方便查询,把慢接口记录下来:
sql
CREATE TABLE slow_trace_index (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
trace_id VARCHAR(64) NOT NULL,
service_name VARCHAR(100) NOT NULL,
endpoint VARCHAR(500) NOT NULL,
duration INT NOT NULL,
start_time DATETIME(3) NOT NULL,
INDEX idx_trace_id (trace_id),
INDEX idx_endpoint_duration (endpoint, duration DESC)
);
这张表只存索引,实际数据在 ELK 里。
数据流转
效果展示
慢接口 Top10
bash
接口路径 平均耗时 最大耗时 次数
/api/order/create 1.5s 3.2s 23
/api/inventory/deduct 1.2s 2.8s 15
/api/user/login 1.1s 2.1s 8
点击任意一个,进入详情页。
详情页:完整调用链
ini
TraceId: 1a2b3c4d5e6f | 总耗时: 1.5s | 开始时间: 2024-02-13 14:30:25
调用链树状图:
┌─ order-service /api/order/create [1500ms]
│ ├─ DB.query orders [50ms]
│ ├─ HTTP -> inventory-service /deduct [1200ms] ← 这里能看到细节了!
│ │ ├─ DB.query inventory [300ms]
│ │ ├─ Redis.decr stock [50ms]
│ │ └─ DB.update inventory [800ms] ← 慢在这里!
│ ├─ MQ.send order_created [100ms]
│ └─ Cache.set order_cache [50ms]
日志详情(按时间排序):
14:30:25.123 [order-service] INFO 开始处理订单创建请求
14:30:25.173 [order-service] DEBUG DB查询: SELECT * FROM orders WHERE id=123
14:30:25.223 [order-service] INFO 调用库存服务扣减库存
14:30:25.523 [inventory-service] DEBUG DB查询: SELECT * FROM inventory WHERE sku=456
14:30:25.823 [inventory-service] WARN DB更新耗时800ms
14:30:26.423 [order-service] INFO 订单创建成功
现在:
- 能看到完整调用链
- 能看到每个环节的耗时
- 能看到所有日志细节
- 一个 traceId 全搞定
核心思路总结
这个方案的精髓就三点:
-
SkyWalking 插件是扩展点
- 官方提供了插件机制
- 我们写插件,在关键位置打日志
- 相当于给 SkyWalking 增强了日志能力
-
traceId 是串联的桥梁
- 所有日志都带 traceId
- 查询时用 traceId 关联
- 自然就串起来了
-
MySQL 做索引,ELK 存详情
- MySQL 只存慢接口索引(轻量)
- ELK 存详细日志(已有)
- 页面从 ES 读数据展示
为什么这个方案好?
技术上
- 没有引入新组件(ELK、MySQL 都是现成的)
- 开发成本低(1-2周)
- 维护成本低(逻辑简单)
- 性能影响小(异步日志)
效果上
- 彻底打通日志和调用链
- 故障排查效率提升 70%
- 一个 traceId 看全链路
可扩展性强
后续可以基于这个方案继续扩展:
最后
这个方案的核心就一句话:写个 SkyWalking 插件,把关键操作打成日志,用 traceId 串起来。
简单,但实用。
没有炫技,没有复杂架构,就是解决问题。
这只是一个想法,一个方案论证。年后我会把它实现出来,代码会开源到 GitHub,到时候欢迎大家 star 和讨论。
最后的最后,祝大家:
新年快乐,万事顺意。
愿你的代码永不报错,愿你的服务永不宕机。
愿2026年,我们都能写出更好的代码,解决更有趣的问题。
感谢这一年的相遇,明年继续并肩作战!
如果你也遇到类似的痛点,或者对这个方案有想法,欢迎评论区交流。