一、一次排查让我崩溃的经历
那年双十一,凌晨两点,系统突然报警:订单成功率从99%掉到了85%。
我开始排查:
- 看监控:订单服务的RT升高了
- 看日志:没有明显的错误
- 看调用:不确定是哪个环节慢
一个接口涉及5个服务,每个服务都有日志。我翻遍了所有日志,翻了整整3个小时,才定位到是Redis连接池被打满了。
那一刻,我崩溃了。
后来接触到SkyWalking,我才知道:原来请求链路追踪可以这么简单。
二、为什么需要链路追踪?
2.1 微服务架构的问题
一个用户下单请求的背后:
用户 → 网关 → 订单服务 → 用户服务
↓
库存服务 → Redis
↓
支付服务 → 第三方支付
↓
物流服务 → 消息队列 → 物流系统
问题:
1. 一次请求涉及10+个服务
2. 日志分散在各个服务
3. 出现问题时,不知道是哪个环节的锅
4. 调用关系复杂,无法直观看到
2.2 链路追踪的核心概念
java
// Trace:一次完整的请求链路
// Span:链路中的一个操作节点
// Context:传递上下文(TraceId、SpanId等)
/*
┌──────────── Trace ────────────────────────────────────────────┐
│ │
│ Span1: 网关处理 │
│ ├─ Span2: 订单服务 │
│ │ ├─ Span3: 用户服务(RPC) │
│ │ ├─ Span4: 库存服务(RPC) │
│ │ │ └─ Span5: Redis查询 │
│ │ └─ Span6: 支付服务(RPC) │
│ │ └─ Span7: 第三方支付(HTTP) │
│ └─ Span8: 记录日志 │
│ │
└──────────────────────────────────────────────────────────────┘
每个Span包含:
- operationName: 操作名称
- startTime: 开始时间
- duration: 耗时
- tags: 标签(如http.status_code=200)
- logs: 日志(异常信息等)
*/
三、SkyWalking架构
3.1 整体架构
┌──────────────────────────────────────────────────────────────────┐
│ SkyWalking │
│ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ SkyWalking UI(可视化界面) │ │
│ │ - 链路拓扑图 │ │
│ │ - 调用链路详情 │ │
│ │ - 性能指标 │ │
│ │ - 告警配置 │ │
│ └────────────────────────────────────────────────────────────┘ │
│ ↑ │
│ │ REST/gRPC │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ OAP Server(分析服务) │ │
│ │ - 接收Trace数据 │ │
│ │ - 聚合分析 │ │
│ │ - 指标计算 │ │
│ │ - 告警触发 │ │
│ └────────────────────────────────────────────────────────────┘ │
│ ↑ │
│ │ Kafka/GRPC │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ SkyWalking Agent(探针) │ │
│ │ Java Agent: javaagent:skywalking-agent.jar │ │
│ │ 自动埋点:Spring Cloud、Dubbo、gRPC、OkHttp、Redis... │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────┘
Agent → OAP Server → UI
3.2 数据流转
┌─────────┐ gRPC/RabbitMQ/Kafka ┌──────────┐ gRPC/REST ┌─────────┐
│ Service │ ─────────────────────────▶ │ OAP │ ──────────────▶ │ UI │
│ + Agent │ │ Server │ │ │
└─────────┘ └──────────┘ └─────────┘
↑
│ H2/ES/MySQL/TiDB
┌──────────────┐
│ Storage │
│ (时序存储) │
└──────────────┘
四、SkyWalking快速安装
4.1 下载安装
bash
# 下载SkyWalking(选择带ES7的版本)
wget https://archive.apache.org/dist/skywalking/9.5.0/apache-skywalking-apm-9.5.0.tar.gz
# 解压
tar -xzf apache-skywalking-apm-9.5.0.tar.gz
cd apache-skywalking-apm-bin
# 目录结构
ls -la
# bin/ 启动脚本
# agent/ Java Agent探针
# config/ 配置文件
# oap-libs/ OAP服务依赖
# webapp/ UI前端
# 配置存储(使用H2内置数据库,生产建议用ES)
# 修改 config/application.yml 中的 storage 配置
4.2 启动OAP服务
bash
# 启动OAP服务(默认端口11800 gRPC,12800 REST)
./bin/startup.sh
# 或者分开启动
./bin/oapService.sh # 启动OAP
./bin/webappService.sh # 启动UI
# 验证
curl http://localhost:12800/health
# 返回 {"status":"UP"} 表示正常
4.3 访问UI
UI地址:http://localhost:12800
默认功能:
- Dashboard:总览仪表盘
- Topology:服务拓扑图
- Trace:链路追踪
- Alarm:告警记录
- Service:服务列表
五、Java应用接入SkyWalking
5.1 引入依赖
xml
<!-- Spring Boot项目无需额外引入依赖,只需加载Agent -->
<!-- 但如果有特殊需求,可以引入以下依赖 -->
<dependency>
<groupId>org.apache.skywalking</groupId>
<artifactId>apm-toolkit-trace</artifactId>
<version>9.5.0</version>
</dependency>
<!-- 链路上下文传递 -->
<dependency>
<groupId>org.apache.skywalking</groupId>
<artifactId>apm-toolkit-logback-1.x</artifactId>
<version>9.5.0</version>
</dependency>
<!-- OpenTelemetry支持 -->
<dependency>
<groupId>org.apache.skywalking</groupId>
<artifactId>apm-toolkit-opentelemetry</artifactId>
<version>9.5.0</version>
</dependency>
5.2 Agent配置
bash
# 启动参数
java -javaagent:skywalking-agent.jar \
-Dskywalking.agent.service_name=order-service \
-Dskywalking.collector.backend_service=localhost:11800 \
-jar order-service.jar
yaml
# skywalking-agent.conf 配置
agent:
# 服务名
service_name=${SW_AGENT_NAME:order-service}
# 实例名,默认是主机名+IP
instance_name=${SW_AGENT_INSTANCE_NAME:order-instance-1}
# 每3秒采样一个请求,0表示全采样
sample_n_per_3_secs=0
# 最长追踪链路深度
max_depth=500
# 队列最大缓冲数
buffer_size=30000
collector:
# OAP服务地址
backend_service=${SW_AGENT_COLLECTOR_BACKEND_SERVICES:localhost:11800}
# gRPC通信方式
grpc_channel_check_interval=5000
grpc_channel_reconnect_period=30
plugin:
# 追踪的段最大跨度
trace.ignore_path=/health,/info,/metrics
# Spring Bean异步方法追踪
spring.bean.invoke.enabled=true
# Kotlin协程追踪
kotlin.coroutine.enabled=true
5.3 Docker部署配置
dockerfile
# Dockerfile
FROM openjdk:8-jdk-alpine
WORKDIR /app
COPY target/order-service.jar app.jar
COPY skywalking-agent agent/
ENV JAVA_OPTS="-javaagent:agent/skywalking-agent.jar \
-Dskywalking.agent.service_name=order-service \
-Dskywalking.collector.backend_service=oap:11800"
EXPOSE 8080
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
yaml
# docker-compose.yml
version: '3.8'
services:
oap:
image: apache/skywalking-oap-server:9.5.0-es7
ports:
- "11800:11800"
- "12800:12800"
order-service:
build: ./order-service
environment:
SW_AGENT_NAME: order-service
SW_AGENT_COLLECTOR_BACKEND_SERVICES: oap:11800
六、手动埋点:让追踪更精确
6.1 获取当前Trace上下文
java
import org.apache.skywalking.apm.toolkit.trace.TraceContext;
@Service
@Slf4j
public class OrderService {
public void createOrder(OrderDTO order) {
// 获取当前TraceID
String traceId = TraceContext.traceId();
log.info("当前TraceID: {}", traceId);
// 获取当前SpanID
int spanId = TraceContext.spanId();
log.info("当前SpanID: {}", spanId);
}
}
6.2 自定义埋点
java
import org.apache.skywalking.apm.toolkit.trace.Traced;
@Service
@Slf4j
public class PaymentService {
/**
* @Traced 标记此方法为一个独立的Span
*/
@Traced(operationName = "PaymentService.processPayment")
public PaymentResult processPayment(String orderId, BigDecimal amount) {
long start = System.currentTimeMillis();
try {
// 调用支付渠道
PaymentResponse response = callPaymentGateway(orderId, amount);
// 记录结果标签
ActiveSpan.tag("payment.status", response.getStatus());
ActiveSpan.tag("payment.channel", response.getChannel());
return PaymentResult.success(response);
} catch (Exception e) {
// 记录错误日志到Span
ActiveSpan.error();
ActiveSpan.log(e);
return PaymentResult.fail(e.getMessage());
} finally {
log.info("支付耗时: {}ms", System.currentTimeMillis() - start);
}
}
/**
* 带参数追踪
*/
@Traced(operationName = "PaymentService.callChannel",
argsWith = {"orderId", "amount"})
private PaymentResponse callPaymentGateway(String orderId, BigDecimal amount) {
// ...
return null;
}
}
6.3 异步任务追踪
java
@Service
@Slf4j
public class AsyncTaskService {
@Autowired
private Executor asyncExecutor;
/**
* 异步方法追踪
* 需要使用 @TraceChain 配合
*/
@Async
public void sendNotificationAsync(String userId, String message) {
// SkyWalking Agent会自动处理@Async的追踪
notificationClient.send(userId, message);
}
/**
* 手动传递TraceContext
*/
public void executeWithContext(String taskId) {
// 在主线程获取TraceContext
String traceId = TraceContext.traceId();
String segmentId = TraceContext.segmentId();
int spanId = TraceContext.spanId();
// 在新线程中继续追踪
asyncExecutor.execute(() -> {
// 继续之前的链路
TraceContext continued = TraceContext.continued(
new TraceSegment.Builder(traceId, segmentId, spanId)
);
try (ActiveSpan scope = continued.start()) {
// 执行异步任务
processTask(taskId);
}
});
}
}
6.4 跨进程传递
java
// Feign调用时自动传递TraceId
@Configuration
public class FeignConfig {
@Bean
public RequestInterceptor traceInterceptor() {
return (Template template, feign.RequestTemplate requestTemplate) -> {
// 添加TraceId到请求头
requestTemplate.header("X-Trace-Id", TraceContext.traceId());
requestTemplate.header("X-Span-Id", String.valueOf(TraceContext.spanId()));
};
}
}
// 或者使用SkyWalking提供的Header传递
@Configuration
public class SkyWalkingConfig {
@Bean
public RequestInterceptor skyWalkingInterceptor() {
return new RequestInterceptor() {
@Override
public void apply(RequestTemplate template) {
// SkyWalking会自动在header中添加sw8相关header
// 只需要确保下游服务也接入了Agent即可
}
};
}
}
七、实战:订单服务链路追踪
7.1 场景描述
用户下单请求链路:
用户 → 网关 → 订单服务 → 用户服务(RPC)
↓
库存服务(RPC)→ Redis缓存
↓
支付服务(RPC)→ 第三方支付(HTTP)
↓
消息队列 → 物流服务
7.2 完整代码
java
@RestController
@RequestMapping("/order")
@Slf4j
public class OrderController {
@Autowired
private OrderService orderService;
/**
* 创建订单
*/
@PostMapping("/create")
@Traced(operationName = "OrderController.createOrder")
public Result<Order> createOrder(@RequestBody @Validated CreateOrderRequest request) {
// 获取TraceID
String traceId = TraceContext.traceId();
log.info("创建订单开始, traceId={}, userId={}", traceId, request.getUserId());
// 记录请求标签
ActiveSpan.tag("user.id", request.getUserId());
ActiveSpan.tag("order.type", request.getOrderType());
try {
Order order = orderService.createOrder(request);
log.info("创建订单成功, traceId={}, orderId={}", traceId, order.getId());
return Result.success(order);
} catch (Exception e) {
ActiveSpan.error();
ActiveSpan.log(e);
log.error("创建订单失败, traceId={}", traceId, e);
throw e;
}
}
}
@Service
@Slf4j
public class OrderService {
@Autowired
private UserClient userClient;
@Autowired
private InventoryClient inventoryClient;
@Autowired
private PaymentClient paymentClient;
@Autowired
private RocketMQTemplate mqTemplate;
@Transactional
public Order createOrder(CreateOrderRequest request) {
// 1. 验证用户
@Traced(operationName = "OrderService.validateUser")
User user = userClient.getUser(request.getUserId());
if (user == null) {
throw new BusinessException("用户不存在");
}
ActiveSpan.tag("user.status", user.getStatus());
// 2. 检查库存
@Traced(operationName = "OrderService.checkInventory")
Inventory inventory = inventoryClient.check(request.getSkuId(), request.getQuantity());
if (!inventory.isAvailable()) {
throw new BusinessException("库存不足");
}
ActiveSpan.tag("inventory.available", String.valueOf(inventory.getAvailableQuantity()));
// 3. 计算价格
BigDecimal totalAmount = calculatePrice(request);
ActiveSpan.tag("order.amount", totalAmount.toString());
// 4. 创建订单
Order order = new Order();
order.setUserId(request.getUserId());
order.setTotalAmount(totalAmount);
order.setStatus(OrderStatus.PENDING);
order = orderRepository.save(order);
// 5. 扣减库存
inventoryClient.deduct(request.getSkuId(), request.getQuantity());
// 6. 发送订单创建消息
mqTemplate.convertAndSend("order-topic", "order-create", order);
return order;
}
private BigDecimal calculatePrice(CreateOrderRequest request) {
// 实际计算逻辑
return request.getQuantity().multiply(new BigDecimal("100"));
}
}
// Feign客户端
@FeignClient(name = "user-service", fallback = UserClientFallback.class)
public interface UserClient {
@GetMapping("/user/{userId}")
User getUser(@PathVariable String userId);
}
@Component
@Slf4j
public class UserClientFallback implements UserClient {
@Override
public User getUser(String userId) {
log.warn("User服务调用失败,降级返回, userId={}", userId);
User fallback = new User();
fallback.setId(userId);
fallback.setName("Unknown");
fallback.setStatus("UNKNOWN");
return fallback;
}
}
八、告警配置
8.1 告警规则
yaml
# alarm-settings.yml
rules:
# 服务响应时间告警
service_resp_time_rule:
metrics-name: service_resp_time
op: ">"
threshold: 1000 # 超过1000ms告警
period: 10 # 10分钟内
count: 3 # 出现3次触发告警
silence-period: 5 # 沉默5分钟
message: "服务 {name} 响应时间超过 {threshold}ms"
# 服务成功率告警
service_success_rate_rule:
metrics-name: service_success_rate
op: "<"
threshold: 95 # 成功率低于95%
period: 10
count: 3
message: "服务 {name} 成功率低于 {threshold}%"
# 慢SQL告警
database_access_resp_time_rule:
metrics-name: database_access_resp_time
op: ">"
threshold: 500
period: 10
count: 2
message: "数据库访问超过 {threshold}ms"
# 熔断告警
circuit_breaker_error_rate:
metrics-name: service_circuit_breaker_error_rate
op: ">"
threshold: 0.5 # 熔断错误率超过50%
period: 5
count: 1
message: "服务 {name} 触发熔断"
webhooks:
# 告警回调地址
- http://192.168.1.100:8080/alert/webhook
8.2 自定义告警
java
@Component
@Slf4j
public class CustomAlertHandler {
/**
* 接收SkyWalking告警
*/
@PostMapping("/alert/webhook")
public void receiveAlert(@RequestBody SkyWalkingAlert alert) {
log.info("收到SkyWalking告警: id={}, name={}, severity={}",
alert.getId(), alert.getName(), alert.getScope());
for (AlertItem item : alert.getAlerts()) {
// 发送通知
sendNotification(item);
}
}
private void sendNotification(AlertItem item) {
// 1. 发送企业微信通知
wechatClient.send(item);
// 2. 发送钉钉通知
dingTalkClient.send(item);
// 3. 发送邮件
emailClient.send(item);
}
}
九、常见问题排查
9.1 服务没有出现在拓扑图
bash
# 排查步骤:
# 1. 检查Agent是否启动成功
# 查看Agent日志
tail -f logs/skywalking-api.log
# 2. 检查Agent配置
# 确认 service_name 和 backend_service 配置正确
# 3. 检查网络连通性
telnet oap-server 11800
# 4. 确认应用启动参数
jps -l | grep order-service
ps aux | grep skywalking-agent
9.2 链路中断
yaml
# 如果链路在某个服务后中断,可能是以下原因:
# 1. 服务没有接入Agent
# 解决方案:添加 -javaagent 参数
# 2. 服务使用了异步线程池
# 解决方案:配置异步追踪
spring:
cloud:
gateway:
filter:
# 配置异步传递
request-trace-enabled: true
# 3. 服务使用了消息队列
# 解决方案:配置MQ追踪插件
# SkyWalking已支持 Kafka、RocketMQ、RabbitMQ 等
9.3 数据丢失
bash
# 如果Trace数据丢失,检查:
# 1. OAP服务的存储是否正常
# 2. Kafka队列是否积压
# 3. Agent的buffer_size是否足够
# 增加buffer配置
agent:
buffer_size: 30000 # 增加缓冲区大小
十、踩坑实录
坑1:Agent版本与OAP版本不匹配
新手上路时,OAP用9.5.0,Agent用了9.3.0,结果数据对不上。
教训:SkyWalking要求Agent版本和OAP版本必须匹配,版本差异太大会导致不兼容。
坑2:生产环境性能问题
生产环境接了SkyWalking后,CPU飙升了15%。
原因:采样率设置太高,链路数据太多。
解决:调整采样率,不追踪静态资源。
yaml
# 生产环境配置
agent:
# 每3秒采样一个请求,减少开销
sample_n_per_3_secs=1
# 不追踪健康检查接口
exclude_activities=/actuator/**
# 不追踪静态资源
plugin:
trace:
ignore_path=/health,/metrics,/static/**
坑3:异步任务链路丢失
使用线程池的异步任务,链路总是中断。
解决 :使用SkyWalking提供的
TracingRunnable和TracingCallable。
java
@Service
public class AsyncService {
private Executor executor = Executors.newFixedThreadPool(10);
public void executeAsync() {
// 错误方式:链路会丢失
executor.execute(() -> doSomething());
// 正确方式:传递Trace上下文
Runnable wrapped = TracingRunnable.of(new Runnable() {
@Override
public void run() {
doSomething();
}
}, "async-task");
executor.execute(wrapped);
}
}
坑4:日志中没有TraceID
日志满天飞,但不知道哪条对应哪个请求。
解决:使用SkyWalking的Logback插件,自动在日志中添加TraceID。
xml
<!-- pom.xml -->
<dependency>
<groupId>org.apache.skywalking</groupId>
<artifactId>apm-toolkit-logback-1.x</artifactId>
<version>9.5.0</version>
</dependency>
<!-- logback.xml -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{tid}] %-5level %logger{36} - %msg%n</pattern>
<!-- %X{tid} 会自动输出TraceID -->
</encoder>
</appender>
十一、总结
SkyWalking是微服务链路追踪的利器:
- 无侵入接入:Java Agent方式,无需修改代码
- 自动埋点:支持主流框架(Spring Cloud、Dubbo等)
- 链路追踪:清晰看到每个请求的完整链路
- 拓扑图:直观展示服务调用关系
- 告警:可配置多种告警规则
最佳实践:
- Agent和OAP版本必须一致
- 生产环境要调整采样率
- 日志中要包含TraceID
- 异步任务要手动传递Trace上下文
- 定期清理历史数据,避免存储爆炸
血的教训:
不要以为接入了SkyWalking就高枕无忧了。如果采样率配置不对,数据量会非常大,影响性能。另外,生产环境的告警阈值要设置合理,太敏感会收到很多无用告警,太迟钝又可能漏掉真正的问题。
思考题: 你的项目有用链路追踪吗?如果链路在某处中断了,通常是什么原因?
个人观点,仅供参考