微服务日志与调用链打通方案:一个简单但实用的思路

微服务日志与调用链打通方案:一个简单但实用的思路

写在前面

2025年快过去了,今年是我写博客的第一年,文章被点赞近600次,收藏1000多次,有256个粉丝(2的8次方,这个数字很程序员),感谢大家的支持。

今天是我2025年最后一个工作日,就以这篇文章为25年画个句号。

这篇文章分享一个想法,一个关于微服务可观测性的方案论证。代码年后会写,到时候会开源出来。

明年,我们继续再战!

背景:微服务可观测性的三大支柱

做微服务的都知道,定位问题靠三样东西:

  • 日志(ELK):看细节,查问题
  • 调用链(SkyWalking):看性能,找瓶颈
  • 监控(Prometheus):看趋势,做告警

理想很美好,现实很骨感。

痛点:日志和调用链是割裂的

痛点1:调用链看不到细节

举个例子,订单服务调用库存服务:

graph LR A[订单服务
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秒
  • 但库存服务内部干了啥?不知道!

想看库存服务的细节,得:

  1. 去 SkyWalking 找库存服务的 trace
  2. 但问题来了:哪个 trace 是对应的?

痛点2:找不到关联关系

两个 trace 之间没有明确的关联:

graph TD A[订单 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 串起来。

整体架构

graph TB subgraph 微服务应用 A[业务代码] --> B[SkyWalking Agent] B --> C[自研插件] end C -->|带 traceId 的日志| D[ELK] C -->|慢接口索引| E[MySQL] subgraph 查询页面 F[前端界面] end D -.读取详细日志.-> F E -.读取索引.-> F style A fill:#e1f5ff,stroke:#0366d6,stroke-width:2px style B fill:#d4edda,stroke:#28a745,stroke-width:2px style C fill:#fff3cd,stroke:#ffc107,stroke-width:2px style D fill:#f8d7da,stroke:#dc3545,stroke-width:2px style E fill:#cfe2ff,stroke:#0d6efd,stroke-width:2px style F fill:#d1ecf1,stroke:#17a2b8,stroke-width:2px

实现分三步走

第一步:日志带上 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 里。

数据流转

sequenceDiagram participant App as 微服务应用 participant Plugin as 自研插件 participant ELK as ELK日志 participant MySQL as MySQL索引 participant Page as 查询页面 App->>Plugin: 调用方法 Plugin->>Plugin: 获取 traceId/spanId Plugin->>ELK: 打印日志(带 traceId) alt 慢接口 Plugin->>MySQL: 存入索引 end Page->>MySQL: 查询慢接口 Top10 Page->>ELK: 根据 traceId 查询日志 Page->>Page: 构建调用链树

效果展示

慢接口 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 全搞定

核心思路总结

这个方案的精髓就三点:

  1. SkyWalking 插件是扩展点

    • 官方提供了插件机制
    • 我们写插件,在关键位置打日志
    • 相当于给 SkyWalking 增强了日志能力
  2. traceId 是串联的桥梁

    • 所有日志都带 traceId
    • 查询时用 traceId 关联
    • 自然就串起来了
  3. MySQL 做索引,ELK 存详情

    • MySQL 只存慢接口索引(轻量)
    • ELK 存详细日志(已有)
    • 页面从 ES 读数据展示

为什么这个方案好?

技术上

  • 没有引入新组件(ELK、MySQL 都是现成的)
  • 开发成本低(1-2周)
  • 维护成本低(逻辑简单)
  • 性能影响小(异步日志)

效果上

  • 彻底打通日志和调用链
  • 故障排查效率提升 70%
  • 一个 traceId 看全链路

可扩展性强

后续可以基于这个方案继续扩展:

graph LR A[基础方案] --> B[增加告警] A --> C[性能对比分析] A --> D[导出报告] A --> E[集成到监控平台] style A fill:#d1ecf1,stroke:#17a2b8,stroke-width:2px style B fill:#d4edda,stroke:#28a745,stroke-width:2px style C fill:#fff3cd,stroke:#ffc107,stroke-width:2px style D fill:#f8d7da,stroke:#dc3545,stroke-width:2px style E fill:#e1f5ff,stroke:#0366d6,stroke-width:2px

最后

这个方案的核心就一句话:写个 SkyWalking 插件,把关键操作打成日志,用 traceId 串起来。

简单,但实用。

没有炫技,没有复杂架构,就是解决问题。

这只是一个想法,一个方案论证。年后我会把它实现出来,代码会开源到 GitHub,到时候欢迎大家 star 和讨论。


最后的最后,祝大家:

新年快乐,万事顺意。

愿你的代码永不报错,愿你的服务永不宕机。

愿2026年,我们都能写出更好的代码,解决更有趣的问题。

感谢这一年的相遇,明年继续并肩作战!

如果你也遇到类似的痛点,或者对这个方案有想法,欢迎评论区交流。

相关推荐
爱叫啥叫啥2 小时前
ESP32Mini打印机:多状态的LED灯驱动
后端
人道领域2 小时前
SSM框架从入门到入土(RESTful风格)
后端·restful
野犬寒鸦2 小时前
WebSocket协同编辑:高性能Disruptor架构揭秘及项目中的实战应用
java·开发语言·数据库·redis·后端
AntBlack2 小时前
上下求索,2025年我用AI写了哪些东西
后端·ai编程·年终总结
消失的旧时光-19432 小时前
第二十一课:系统是怎么一步步拆坏的?——单体到模块化实践(完整工程版)
java·spring boot·后端·架构
wanderful_2 小时前
自定义用户体系下 Django 业务模块开发踩坑与通用解决方案(技术分享版)
后端·python·django
Coder_Boy_3 小时前
Java高级_资深_架构岗 核心面试知识点(AI整合+混合部署)
java·人工智能·spring boot·后端·面试·架构
PD我是你的真爱粉3 小时前
RabbitMQRPC与死信队列
后端·python·中间件