【Zipkin】OkHttp集成Zipkin实现链路追踪
目录
概述
背景介绍
在微服务架构中,服务间调用链的复杂性使得问题定位变得困难。Zipkin作为分布式追踪系统,能够帮助我们追踪和分析服务调用链路,快速定位问题。
问题描述
在使用Zuul作为API网关时,如果从Apache HttpClient切换到OkHttp,可能会遇到以下问题:
- 链路追踪信息(traceid、spanid、parentspanid)无法正确传递
- 下游服务会生成新的traceid,导致调用链断裂
- 无法从网关开始追踪完整的服务调用链路
解决方案概览
通过自定义OkHttp Interceptor,我们可以:
- 在请求头中注入追踪信息
- 确保链路追踪的连续性
- 实现从网关到最终服务的完整链路追踪
环境准备
依赖配置
<dependencies>
<!-- Spring Cloud Starter Zipkin -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
<!-- OkHttp -->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.9.3</version> <!-- 推荐使用稳定版本 -->
</dependency>
<!-- Brave OkHttp -->
<dependency>
<groupId>io.zipkin.brave</groupId>
<artifactId>brave-instrumentation-okhttp3</artifactId>
<version>5.13.9</version> <!-- 使用与Spring Cloud版本兼容的版本 -->
</dependency>
</dependencies>
配置文件
spring:
application:
name: your-service-name
zipkin:
base-url: http://your-zipkin-server:9411
sender:
type: web
sleuth:
sampler:
probability: 1.0 # 开发环境可设置为1.0,生产环境建议0.1
web:
client:
enabled: true
async:
enabled: true # 启用异步跟踪
# OkHttp配置
ribbon:
http:
client:
enabled: false
okhttp:
enabled: true
ConnectTimeout: 3000
ReadTimeout: 5000
实现步骤
定义追踪拦截器
创建 TracingInterceptor.java
:
@Component
public class TracingInterceptor implements Interceptor {
private static final Logger LOGGER = LoggerFactory.getLogger(TracingInterceptor.class);
// B3 追踪头
private static final String TRACE_ID_NAME = "X-B3-TraceId";
private static final String SPAN_ID_NAME = "X-B3-SpanId";
private static final String PARENT_SPAN_ID_NAME = "X-B3-ParentSpanId";
private static final String SAMPLED_NAME = "X-B3-Sampled";
private static final String FLAGS_NAME = "X-B3-Flags";
private static final String SPAN_NAME = "X-Span-Name";
@Autowired
private Tracer tracer;
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Span span = tracer.currentSpan();
if (span == null) {
return chain.proceed(request);
}
TraceContext traceContext = span.context();
String spanName = request.method() + " " + request.url().encodedPath();
// 构建追踪头
Headers.Builder headersBuilder = request.headers().newBuilder()
.add(TRACE_ID_NAME, traceContext.traceIdString())
.add(SPAN_ID_NAME, HexCodec.toLowerHex(generateNextId()))
.add(PARENT_SPAN_ID_NAME, getParentId(traceContext))
.add(SAMPLED_NAME, "1")
.add(SPAN_NAME, spanName);
if (traceContext.debug()) {
headersBuilder.add(FLAGS_NAME, "1");
}
// 创建新请求
Request tracedRequest = request.newBuilder()
.headers(headersBuilder.build())
.build();
// 记录请求信息
LOGGER.debug("Sending request: method={}, url={}, traceId={}",
request.method(), request.url(), traceContext.traceIdString());
// 执行请求并记录响应
Response response = null;
try {
response = chain.proceed(tracedRequest);
LOGGER.debug("Received response: code={}, traceId={}",
response.code(), traceContext.traceIdString());
return response;
} catch (Exception e) {
LOGGER.error("Request failed: traceId={}, error={}",
traceContext.traceIdString(), e.getMessage());
throw e;
}
}
private String getParentId(TraceContext context) {
return context.parentIdString() != null ?
context.parentIdString() :
HexCodec.toLowerHex(context.spanId());
}
private long generateNextId() {
long nextId;
do {
nextId = Platform.get().randomLong();
} while (nextId == 0L);
return nextId;
}
}
注册拦截器
创建 HttpClientConfiguration.java
:
@Configuration
public class HttpClientConfiguration {
@Bean
public TracingInterceptor tracingInterceptor() {
return new TracingInterceptor();
}
@Bean
@ConditionalOnMissingBean(OkHttpClient.class)
public OkHttpClient okHttpClient(TracingInterceptor tracingInterceptor) {
return new OkHttpClient.Builder()
.addInterceptor(tracingInterceptor)
.addInterceptor(new HttpLoggingInterceptor() // 添加日志拦截器
.setLevel(HttpLoggingInterceptor.Level.BASIC))
.connectTimeout(2, TimeUnit.SECONDS)
.readTimeout(3, TimeUnit.SECONDS)
.writeTimeout(3, TimeUnit.SECONDS)
.retryOnConnectionFailure(true)
.connectionPool(new ConnectionPool(20, 10L, TimeUnit.MINUTES)) // 增加连接池保持时间
.build();
}
@Bean
public HttpLoggingInterceptor loggingInterceptor() {
return new HttpLoggingInterceptor()
.setLevel(HttpLoggingInterceptor.Level.BASIC);
}
}
验证与测试
测试步骤
-
启动Zipkin服务器
docker run -d -p 9411:9411 openzipkin/zipkin
-
启动包含OkHttp配置的服务
./mvnw spring-boot:run
-
通过网关发送测试请求
curl -v http://your-gateway-url/api/test
-
在Zipkin UI中查看追踪结果
- 访问 http://localhost:9411
- 使用服务名和TraceID进行查询
验证要点
- 检查traceid是否在整个调用链中保持一致
- 确认spanid的父子关系是否正确
- 验证服务调用的时序是否符合预期
- 检查请求和响应的时间戳是否准确
- 确认所有相关的元数据(如HTTP方法、URL)是否正确记录
常见问题及解决方案
-
追踪信息丢失
- 检查拦截器是否正确注册
- 确认追踪头的名称是否正确
- 验证Sleuth配置是否生效
logging.level.org.springframework.cloud.sleuth: DEBUG
-
新traceid生成
- 验证parentId的传递是否正确
- 检查Sleuth配置是否正确
- 确保所有服务使用相同版本的Sleuth
-
链路不完整
- 确保所有服务都配置了Zipkin
- 检查采样率配置
- 验证网络连接是否正常
curl -v http://your-zipkin-server:9411/api/v2/services
-
性能问题
- 检查连接池配置
- 监控请求延迟
- 分析Zipkin UI中的时间分布
最佳实践
性能优化
- 合理配置连接池参数
new ConnectionPool( 20, // 最大空闲连接数 10L, // 保持时间 TimeUnit.MINUTES )
- 设置适当的超时时间
.connectTimeout(2, TimeUnit.SECONDS) .readTimeout(3, TimeUnit.SECONDS) .writeTimeout(3, TimeUnit.SECONDS)
- 启用请求重试
.retryOnConnectionFailure(true)
安全考虑
- 在生产环境中使用合适的采样率(建议0.1-0.3)
- 配置敏感信息过滤
@Bean public SleuthSkipPatternProvider skipPatternProvider() { return () -> Pattern.compile("/api/health|/api/metrics"); }
- 实现追踪信息加密
- 控制日志级别,避免敏感信息泄露
监控与告警
- 配置Zipkin指标监控
- 设置关键指标告警阈值
- 定期检查追踪数据质量
开发建议
- 使用有意义的span名称
- 添加自定义标签记录业务信息
- 保持追踪粒度的合理性
- 定期更新依赖版本