【SkyWalking从入门到精通】第21篇:用SkyWalking排查慢服务——从蛛丝马迹到真相大白
上一篇【第20篇】用SkyWalking查找失败服务——从告警到根因的完整追踪术
下一篇【第22篇】SkyWalking告警配置实战——让你的系统学会"喊救命"
1. 引言:一只慢服务引发的血案
凌晨两点,你的手机疯狂震动。运维群里炸开了锅——“用户端打开页面要转10秒!”“订单接口超时了!”"老板在群里@你了!"你睡眼惺忪地打开电脑,日志密密麻麻像天书,几十个微服务互相调用,到底是谁拖了后腿?
如果你经历过这种场景,那你一定明白"慢"是最难排查的问题。因为慢不是错误,它不会在日志里留下醒目的红色ERROR;慢是温水煮青蛙,用户感知越来越差,直到某天集中爆发。
SkyWalking就像一位经验丰富的老中医,望闻问切,把脉问诊——它不只看症状,还能追溯到病因。今天我们就来聊聊,怎么用SkyWalking这把"手术刀",精准切除系统中的"慢肿瘤"。
2. 慢服务的定义与阈值设置
2.1 什么样的服务算"慢"?
在程序员的世界里,"快"和"慢"是相对的。一个批处理任务跑5秒理所当然,一个登录接口超过500ms用户就开始不耐烦。所以第一步,我们要给"慢"下一个可量化的定义。
+------------------------------------------------------------------+ | 慢服务定义层级 | +------------------------------------------------------------------+ | 层级 | 典型阈值 | 说明 | +------------------------------------------------------------------+ | 用户感知层 | > 2s | 页面白屏时间,用户失去耐心的临界点 | | HTTP接口层 | > 500ms | 单个API响应时间,前端体验的底线 | | 数据库操作 | > 100ms | 单条SQL执行时间,超出需要关注 | | 缓存操作 | > 10ms | Redis/Memcached单次操作 | | RPC调用 | > 200ms | 服务间调用,累积效应明显 | | 消息队列 | > 1000ms | 生产/消费延迟,异步链路的关键指标 | +------------------------------------------------------------------+2.2 在SkyWalking中设置响应时间阈值
SkyWalking OAP Server的application.yml中,可以配置慢端点阈值:
# application.ymlcore:default:# 慢数据库语句阈值(毫秒)slowDBAccessThreshold:${SW_SLOW_DB_THRESHOLD:50}# 支持的响应时间范围metrics:# P99响应时间桶分布percentile:-100# 100ms-200-500-1000# 1s-2000-3000-5000# 5s而在SkyWalking UI的拓扑图、端点列表、追踪详情中,超过阈值的调用会被高亮标记(通常用橙色或红色),一眼就能揪出问题。
3. 从SkyWalking识别慢Endpoint
3.1 端点列表——全局视野
登录SkyWalking UI,进入端点(Endpoint)面板,你会看到所有被监控的API端点。按平均响应时间降序排列,慢端点立马现行。
+------------------------------------------------------------------+ | SkyWalking UI - 端点面板 | +------------------------------------------------------------------+ | 端点名称 | 平均RT | P99 | 调用量 | +------------------------------------------------------------------+ | /api/order/submit {POST} | 3.2s | 8.7s | 12,345 | | /api/user/profile {GET} | 1.8s | 4.2s | 56,789 | | /api/product/search {GET} | 890ms | 2.1s | 34,210 | | /api/cart/add {POST} | 120ms | 350ms | 98,765 | +------------------------------------------------------------------+ | 🔴 红色 = 慢端点 🟡 橙色 = 需要关注 🟢 绿色 = 正常 | +------------------------------------------------------------------+/api/order/submit平均响应时间3.2秒,P99高达8.7秒——这就是今晚的"罪魁祸首"。
3.2 热力图——发现时间规律
SkyWalking的端点详情页提供了响应时间热力图,它以时间为X轴、响应时间为Y轴,颜色深浅代表请求密度。一张好的热力图长这样:
慢 ↑ 响应时间 | 5s + | ██ | ████ 2s + | ██ ██████ | ████ ████████ 1s + | ██ ██████████████ | ████ ██████████████ 0.5s+ | ████████████████████████ | ████████████████████████████ 快 +--+--+--+--+--+--+--+--+--+--+--> 时间 00:00 04:00 08:00 12:00 16:00 20:00高峰期(12:00-14:00)响应时间飙到5秒,说明要么是并发压力太大,要么是某些依赖在这个时段也不行了。
4. 在Trace中定位慢Span
识别出慢端点后,下一步就是"拆解"——这个请求到底在哪一步花了最多时间?
4.1 Trace树结构
点击慢端点中的任意一次调用,进入Trace详情页。你会看到一颗调用树,每个节点就是一个Span:
+------------------------------------------------------------------+ | Trace: /api/order/submit (总耗时: 3.2s) | +------------------------------------------------------------------+ | ├── [EntrySpan] /api/order/submit (3.2s) | | │ ├── [LocalSpan] 参数校验 (5ms) | | │ ├── [ExitSpan] POST /user-service/verify (200ms) | | │ ├── [ExitSpan] POST /inventory-service/deduct (150ms) | | │ ├── [ExitSpan] MySQL SELECT * FROM products (2.5s) ⚠️ | | │ │ └── DB慢查询确认: SQL耗时占比 78% (2.5s) | | │ ├── [ExitSpan] Redis SET order:lock (8ms) | | │ ├── [ExitSpan] MySQL INSERT INTO orders (120ms) | | │ └── [ExitSpan] Kafka produce order-created (15ms) | +------------------------------------------------------------------+ | ⚠️ 慢Span: MySQL SELECT * FROM products 耗时2.5秒(占78%) | +------------------------------------------------------------------+一目了然——MySQL的SELECT * FROM products这条查询占了78%的时间,慢的根源在数据库。
4.2 各类型Span的定位思路
不同类型的慢Span,排查思路截然不同:
| Span类型 | 慢场景示例 | 排查方向 |
|---|---|---|
| HTTP ExitSpan | 调用下游服务超时 | 检查下游服务健康状况、网络延迟、是否触发了限流熔断 |
| Database ExitSpan | SQL执行慢 | 查看SQL语句、检查索引、分析执行计划 |
| Redis ExitSpan | 缓存操作超时 | 检查key是否是大Key、Redis内存是否打满、网络是否抖动 |
| MQ ExitSpan | 消息发送/消费慢 | 检查队列积压、消费者处理逻辑、批量大小 |
| LocalSpan | 本地方法执行慢 | 检查业务逻辑复杂度、循环/递归、大对象创建 |
5. 数据库SQL慢查询的Trace展示
5.1 SkyWalking的SQL详情
SkyWalking Agent会自动拦截JDBC驱动,记录每一条SQL的完整信息。在Trace详情中,点击Database类型的Span,展开后你会看到:
-- SkyWalking Trace中捕获的SQL详情Span: MySQL/JDBI/PreparedStatement/executeQueryDatabase: order_db Endpoint:192.168.1.100:3306SQL:SELECTp.*,i.*,c.*,s.*FROMproducts pLEFTJOINinventory iONp.id=i.product_idLEFTJOINcategories cONp.category_id=c.idLEFTJOINsuppliers sONp.supplier_id=s.idWHEREp.status='online'ANDp.created_at>'2025-01-01'ORDERBYp.sales_volumeDESCLIMIT100耗时:2456ms5.2 慢SQL的典型问题
从上面的SQL可以立刻看出几个问题:
SELECT p.*, i.*, c.*, s.*:四表联查返回所有字段,数据量爆炸LEFT JOIN连了4张表:笛卡尔积膨胀WHERE p.status = 'online':status字段是否有索引?ORDER BY p.sales_volume DESC:filesort?
// 对应的Java代码(常见问题代码示例)@RepositorypublicclassProductRepository{// ❌ 错误示范:全字段联表查询@Query("SELECT p.*, i.*, c.*, s.* FROM products p "+"LEFT JOIN inventory i ON p.id = i.product_id "+"LEFT JOIN categories c ON p.category_id = c.id "+"LEFT JOIN suppliers s ON p.supplier_id = s.id "+"WHERE p.status = :status ORDER BY p.sales_volume DESC")List<Object[]>findProductsRaw(@Param("status")Stringstatus);// ✅ 优化方案1:只查需要的字段@Query("SELECT p.id, p.name, p.price, p.imageUrl, "+"i.stock, c.name as categoryName "+"FROM Product p "+"LEFT JOIN Inventory i ON p.id = i.productId "+"LEFT JOIN Category c ON p.categoryId = c.id "+"WHERE p.status = :status "+"ORDER BY p.salesVolume DESC")List<ProductDTO>findProductsOptimized(@Param("status")Stringstatus);// ✅ 优化方案2:加索引// ALTER TABLE products ADD INDEX idx_status_sales (status, sales_volume DESC);}5.3 数据库慢查询定位流程图
+------------------------------------------------------------------+ | 数据库慢查询排查决策树 | +------------------------------------------------------------------+ 慢SQL检测到 | +--------+--------+ | | SELECT慢 UPDATE/INSERT慢 | | +-------+-------+ 检查锁等待 | | | (行锁/表锁/死锁) 索引缺失 返回过多 联表过多 | | | 加索引 只查需要的 拆分查询 EXPLAIN 字段+分页 或使用缓存 | +----+----+ | | 覆盖索引 降级查询 生效了 逻辑 | 慢SQL解决!6. 外部服务依赖超时的识别
6.1 拓扑图中的慢依赖
SkyWalking的拓扑图会实时展示服务间的调用关系。边的粗细代表调用量,颜色代表响应时间:
+------------------------------------------------------------------+ | 服务拓扑图(部分) | +------------------------------------------------------------------+ | | | [订单服务] ──────────── [用户服务] | | | 200ms 🔴 | | | | | |──────────────── [库存服务] | | | 150ms 🟢 | | | | | |════════════════ [支付网关] | | | 3.2s ⚠️ 🔴 粗线=慢依赖 | | | | | |──────────────── [消息队列] | | 15ms 🟢 | +------------------------------------------------------------------+6.2 HTTP请求超时的Trace分析
当你怀疑是第三方依赖慢时,SkyWalking会在ExitSpan中记录完整信息:
Span: POST http://payment-gateway/api/pay Type: ExitSpan Component: SpringRestTemplate (HTTP) URL: http://10.0.1.50:8080/api/pay StatusCode: 200 RequestBody: {"orderId":"ORD123", "amount":99.00} 响应大小: 2KB 时间拆解: DNS解析: 2ms TCP连接: 3ms SSL握手: 15ms 请求发送: 1ms 等待响应: 3150ms ⚠️ 大头在这里 响应读取: 5ms 总耗时: 3176ms请求只花了20ms就建立连接并发送出去,但等待响应花了3.15秒——问题不在网络,而在支付网关本身处理慢。
6.3 Redis超时排查
// 场景:Redis缓存命中但查询极慢@Cacheable(value="userSessions",key="#userId")publicUserSessiongetSession(StringuserId){// 原本很快的缓存,突然变慢// SkyWalking Trace显示 Redis GET 耗时 800msreturnsessionRepository.findByUserId(userId);}在SkyWalking Trace中,Redis操作的Span会显示:
Span: Redis/Jedis/get Key: userSession:10086 Value大小: 1.5MB ⚠️ 大Key! 耗时: 823ms1.5MB的Value意味着这是一个大Key——Redis是单线程的,大Key操作会阻塞其他请求。解决方案是拆分或压缩。
6.4 消息队列延迟排查
+------------------------------------------------------------------+ | 消息队列慢场景分析 | +------------------------------------------------------------------+ | | | 生产者慢: 消费者慢: | | ┌──────────────┐ ┌──────────────┐ | | │ 发送耗时 2s │ │ 消费耗时 5s │ | | └──────┬───────┘ └──────┬───────┘ | | | | | | 可能原因: 可能原因: | | - Broker负载高 - 消费逻辑重 | | - 消息体过大 - 下游依赖慢 | | - 网络抖动 - 批量大小不合理 | | - 同步发送等待ACK - 消费线程池不够 | | | +------------------------------------------------------------------+7. 实战:一次完整的慢服务排查
7.1 场景描述
用户反馈"商品搜索接口"响应超慢(>5秒),而之前一直是200ms以内。
7.2 排查过程
Step 1:全局视角
进入SkyWalking → 端点面板 → 按响应时间排序 → 发现/api/product/search耗时突增。
Step 2:锁定异常时间段
查看热力图,发现14:30开始响应时间飙升——正好是运维发布了新版本的ES集群。
Step 3:进入Trace详情
Trace: /api/product/search?keyword=手机&page=1 (总耗时: 5.2s) ├── [EntrySpan] /api/product/search (5.2s) │ ├── [ExitSpan] Redis GET keyword:hot:手机 (8ms) ✅ │ ├── [ExitSpan] ElasticSearch search (4.8s) ⚠️ │ │ └── ES查询: {"query":{"match":{"name":"手机"}}} │ │ ES响应状态: 200, 耗时: 4780ms │ ├── [ExitSpan] Redis GET product:detail:* (15ms) ✅ │ └── [LocalSpan] 组装结果 (12ms) ✅Step 4:定位原因
ES查询耗时4.8秒——对比版本发布前,同样的查询只需50ms。检查ES集群发现:运维在升级时误删了索引的mapping配置,导致全文检索退化成全表扫描。
Step 5:修复验证
恢复ES索引mapping → SkyWalking中观察/api/product/search的响应时间 → 降回200ms以内 → 问题解决。
7.3 实战总结
+------------------------------------------------------------------+ | 慢服务排查五步法 | +------------------------------------------------------------------+ | | | ① 全局扫描 ② 锁定时间 ③ 深入Trace | | ┌─────────┐ ┌─────────┐ ┌─────────┐ | | │端点面板 │────→│ 热力图 │──────→│Trace树 │ | | │排序找慢 │ │找异常时段│ │找慢Span │ | | └─────────┘ └─────────┘ └────┬────┘ | | │ | | ⑤ 回溯验证 │ ④ 定位根因 | | ┌─────────┐ ┌─────────┐ | | │持续观察 │←──────│展开详情 │ | | │确认恢复 │ │找瓶颈点 │ | | └─────────┘ └─────────┘ | +------------------------------------------------------------------+8. 慢服务排查的进阶技巧
8.1 告警先行
与其被动排查,不如主动告警。在alarm-settings.yml中配置:
rules:# 端点响应时间告警endpoint_avg_rule:metrics-name:endpoint_avgop:">"threshold:1000# 1秒period:10count:3silence-period:5message:端点{name}平均响应时间超过 1s,当前值:{value}ms8.2 慢端点TopN仪表盘
SkyWalking支持自定义仪表盘。创建一个"慢服务监控大盘",包含:
- 响应时间Top 10端点(实时刷新)
- 各端点的P99/P95/P90曲线
- 慢SQL语句列表
- 慢依赖服务排行榜
8.3 与日志平台联动
SkyWalking的traceId可以注入日志系统:
// logback-spring.xml<property name="LOG_PATTERN"value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId}] %-5level %logger{36} - %msg%n"/>配置SkyWalking的Toolkit:
<dependency><groupId>org.apache.skywalking</groupId><artifactId>apm-toolkit-logback-1.x</artifactId><version>${skywalking.version}</version></dependency>这样在ELK中搜索traceId,就能把日志和Trace串联起来,慢服务排查效率翻倍。
9. 总结
慢服务排查是一场侦探游戏:
- 全局扫描:端点面板找出最慢的API
- 锁定时间:热力图定位异常时段
- 深入Trace:Trace树找到慢Span
- 定位根因:展开Span详情直击瓶颈
- 回溯验证:观察指标确认问题修复
SkyWalking的价值在于,它把"猜"变成了"看"——你不需要靠经验猜测哪里慢,Trace会原原本本地告诉你每一毫秒去了哪里。
下面一篇文章,我们将把"被动排查"升级为"主动告警",详细讲解SkyWalking的告警配置实战。
上一篇【第20篇】用SkyWalking查找失败服务——从告警到根因的完整追踪术
下一篇【第22篇】SkyWalking告警配置实战——让你的系统学会"喊救命"
