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

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

写在前面

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年,我们都能写出更好的代码,解决更有趣的问题。

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

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

相关推荐
zopple6 小时前
常见的 Spring 项目目录结构
java·后端·spring
cjy0001118 小时前
springboot的 nacos 配置获取不到导致启动失败及日志不输出问题
java·spring boot·后端
小江的记录本9 小时前
【事务】Spring Framework核心——事务管理:ACID特性、隔离级别、传播行为、@Transactional底层原理、失效场景
java·数据库·分布式·后端·sql·spring·面试
sheji34169 小时前
【开题答辩全过程】以 基于springboot的校园失物招领系统为例,包含答辩的问题和答案
java·spring boot·后端
程序员cxuan9 小时前
人麻了,谁把我 ssh 干没了
人工智能·后端·程序员
wuyikeer10 小时前
Spring Framework 中文官方文档
java·后端·spring
Victor35611 小时前
MongoDB(61)如何避免大文档带来的性能问题?
后端
Victor35611 小时前
MongoDB(62)如何避免锁定问题?
后端
wuyikeer11 小时前
Spring BOOT 启动参数
java·spring boot·后端
子木HAPPY阳VIP12 小时前
Ubuntu 22.04 VMware 设置固定IP配置
人工智能·后端·目标检测·机器学习·目标跟踪