背景
当系统架构变得越来越复杂后,我们一次前端请求,有可能要经历跨多个线程/跨多个协程/跨多个进程处理后,才会最终响应到客户端,如果请求按照预期正确执行还好,万一在某个调用链的某一环节出现了问题,排查起来非常的麻烦,但如果有链路跟踪的话,就会大大降低排查的困难度。
可以通过在关键点设置链路埋点,记录下重要的步骤,例如,请求接收处,逻辑处理处,数据库交互处,调用了外部服务等。
在阅读本文章之前,大家需要有链路跟踪的基础知识,这个可以查看我另一篇文章:我眼中的OpenTracing
前言
Jaeger是一款广受欢迎的开源分布式链路跟踪系统,兼容OpenTracing API,且已加入CNCF开源组织。其主要功能是聚合来自各个异构系统的实时监控数据。对一些常用的框架通过插件可以达到无侵入式跟踪,比如Apache HttpClient,Elasticsearch,JDBC,Kafka,Memcached,Mongo,OkHttp,Redis,Spring Boot,Spring Cloud,要通过Jaeger将Java应用数据上报至链路追踪控制台,首先需要完成埋点工作。您可以手动埋点,也可以利用各种现有插件实现埋点的目的。本文介绍以下三种埋点方法的其中一种手动埋点。
- 手动埋点
- 通过Spring Cloud组件埋点
- 通过gRPC组件埋点
一、Jaeger 是什么?
由于 Uber 的业务增长迅猛,其软件架构也越来越复杂,截止 2015 年下半年,Uber 内部已经有 500 多个微服务在运行,给问题排查和性能分析带来巨大困难。2016 年 4 月,Uber 启动 Jaeger 项目,并逐渐在内部推行分布式追踪系统,一年之后(2017 年 4 月),Uber 宣布正式将 Jaeger 开源。Uber Engineering Blog 有一篇文章介绍了分布式追踪系统在 Uber 的演进过程,建议阅读,《Evolving Distributed Tracing at Uber Engineering》。Uber开源的Jaeger(发音为ˈyā-gər ),因为它对OpenTracing支持的比较好,而且部署使用也非常简单。另外Jaeger的作者就是Yurishkuro。这里就不介绍Jaeger的细节了,有兴趣的可以去官网了解:Jaeger官网。
按照数据流向,整体可以分为四个部分:
jaeger-client:Jaeger 的客户端,实现了 OpenTracing 的 API,支持主流编程语言。客户端直接集成在目标 Application 中,其作用是记录和发送 Span 到 Jaeger Agent。在 Application 中调用 Jaeger Client Library 记录 Span 的过程通常被称为埋点。
jaeger-agent:暂存 Jaeger Client 发来的 Span,并批量向 Jaeger Collector 发送 Span,一般每台机器上都会部署一个 Jaeger Agent。官方的介绍中还强调了 Jaeger Agent 可以将服务发现的功能从 Client 中抽离出来,不过从架构角度讲,如果是部署在 Kubernetes 或者是 Nomad 中,Jaeger Agent 存在的意义并不大。
jaeger-collector:接受 Jaeger Agent 发来的数据,并将其写入存储后端,目前支持采用 Cassandra 和 Elasticsearch 作为存储后端。个人还是比较推荐用 Elasticsearch,既可以和日志服务共用同一个 ES,又可以使用 Kibana 对 Trace 数据进行额外的分析。架构图中的存储后端是 Cassandra,旁边还有一个 Spark,讲的就是可以用 Spark 等其他工具对存储后端中的 Span 进行直接分析。
jaeger-query & jaeger-ui:读取存储后端中的数据,以直观的形式呈现。
Jaeger 的架构非常清晰,部署起来也很轻松,Docker Hub 中有官方打好的 Image,可以拿来直接用,https://hub.docker.com/u/jaegertracing/。如果是本地测试,可以直接用 Jaeger 的 all-in-one Image,
上部分 Agent :负责从应用中,收集链路信息,发送给 SkyWalking OAP 服务器。目前支持 SkyWalking、Zikpin、Jaeger 等提供的 Tracing 数据信息。而我们目前采用的是,SkyWalking Agent 收集 SkyWalking Tracing 数据,传递给服务器。
Jaeger receiver
Jaeger receiver 目前只在跟踪模式下工作,不支持分析模式。Jaeger receiver提供额外的GRPC主机/端口,如果没有,将使用共享服务器主机/端口,还没有就使用核心GRPC主机/端口。Receiver需要激活Jaeger ElasticSearch存储实现。阅读此内容了解如何激活。right now only works in Tracing Mode, and no analysis. Jaeger receiver provides extra gRPC host/port, if absent, sharing-server host/port will be used, then core gRPC host/port. Receiver requires jaeger-elasticsearch storage implementation active. Read this to know how to active.
现在,你需要jaeger agent 来批量发送spans到 SkyWalking的oap服务器。 阅读Jaeger Architecture获取更多详情。
激活这个receiver。
receiver_jaeger:
default:
gRPCHost: ${SW_RECEIVER_JAEGER_HOST:0.0.0.0}
gRPCPort: ${SW_RECEIVER_JAEGER_PORT:14250}
- 下部分 SkyWalking OAP :负责接收 Agent 发送的 Tracing 数据信息,然后进行分析(Analysis Core) ,存储到外部存储器( Storage ),最终提供查询( Query )功能。
- 右部分 Storage :Tracing 数据存储。目前支持 ES、MySQL、Sharding Sphere、TiDB、H2 多种存储器。而我们目前采用的是 ES ,主要考虑是 SkyWalking 开发团队自己的生产环境采用 ES 为主。
- 左部分 SkyWalking UI :负责提供控台,查看链路等等。
二、使用步骤
1.引入库
api group: 'io.jaegertracing', name: 'jaeger-client', version: '1.6.0';
api group: 'io.opentracing', name: 'opentracing-api', version: '0.33.0';
api group: 'io.opentracing', name: 'opentracing-util', version: '0.33.0';
2.配置初始化参数
public void init(String serverName, String agentIp, int agentPort, int ratingLimit) {
logger.info("init tracer client start,serverName={},agentIp={},agentPort={}", serverName, agentIp, agentPort);
Configuration config = new Configuration(serverName);
SenderConfiguration sender = new SenderConfiguration();
sender.withAgentHost(agentIp);
sender.withAgentPort(agentPort);
//一秒钟最多1000次采样
if (ratingLimit == 0) {
ratingLimit = 1000;
}
config.withSampler(new SamplerConfiguration().withType(RateLimitingSampler.TYPE).withParam(ratingLimit));
//全量采集,采样率设置0,1 分别对应打开和关闭
// config.withSampler(new SamplerConfiguration().withType(ConstSampler.TYPE).withParam(1));
config.withReporter(new ReporterConfiguration().withSender(sender).withMaxQueueSize(10000).withFlushInterval(60000));
config.withCodec(new CodecConfiguration().withBinaryCodec(Format.Builtin.BINARY, new BinaryCodec()));
GlobalTracer.registerIfAbsent(config.getTracer());
logger.info("init tracer client end,serverName={},agentIp={},agentPort={}", serverName, agentIp, agentPort);
}
如何设置埋点?
/**
* 开启链路
*/
public Span startTrace(SpanContext context, String methodName) {
if (methodName == null) {
return null;
}
Tracer tracer = GlobalTracer.get();
SpanBuilder spanBuilder = tracer.buildSpan(methodName);
String traceId = ThreadContext.get(SystemConstant.TRACE_ID);
if (traceId != null) {
spanBuilder.withTag("userId", traceId);
}
String threadName = "";
if (Thread.currentThread().isVirtual()) {
spanBuilder.withTag(SystemConstant.VIRTUAL_KEY, true);
threadName = "[" + Thread.currentThread().getName() + "]";
} else {
spanBuilder.withTag(SystemConstant.VIRTUAL_KEY, false);
threadName = Thread.currentThread().getName();
}
spanBuilder.withTag(SystemConstant.THREAD_KEY, threadName);
// Tags.SPAN_KIND_SERVER;
if (context != null) {
spanBuilder.asChildOf(context);
}
Span span = spanBuilder.start();
return span;
}
//埋点
Span span = TraceService.getInstance().startTrace(methodName);
try (Scope scope = GlobalTracer.get().activateSpan(span)) {
span.setTag("message", this.getName());
//handle your business
} catch (Exception e) {
TracingHelper.onError(e, span);
} finally {
span.finish();
}
批量上报的线程如下:
上报的类 :io.jaegertracing.internal.reporters.RemoteReporter
上报的方法在io.jaegertracing.internal.reporters.RemoteReporter.QueueProcessor#run
如果要把上报的日志打开
则要做如下设置:
new ReporterConfiguration().withLogSpans(true)
则会有如下的日志输出
2021-12-31 14:44:01.734 INFO [game-bus-cache-1][worker-0][LoggingReporter.java:43]29a998394fc0478c869ce7aba3623688 - Span reported: 015dcaea33775a88:015dcaea33775a88:0:1 - UserController.handle(GetAccountList_C2S_Msg)
public class LoggingReporter implements Reporter {
private final Logger logger;
public LoggingReporter() {
this(null);
}
public LoggingReporter(Logger logger) {
if (logger == null) {
logger = LoggerFactory.getLogger(this.getClass());
}
this.logger = logger;
}
@Override
public void report(JaegerSpan span) {
logger.info("Span reported: {}", span);
}
@Override
public void close() {
// nothing to do
}
}
2.读入数据
3.对外端口
Port Protocol Component Function
5775 UDP agent accept zipkin.thrift over compact thrift protocol (deprecated, used by legacy clients only)
6831 UDP agent accept jaeger.thrift over compact thrift protocol
6832 UDP agent accept jaeger.thrift over binary thrift protocol
5778 HTTP agent serve configs
16686 HTTP query serve frontend
14268 HTTP collector accept jaeger.thrift directly from clients
14250 HTTP collector accept model.proto
9411 HTTP collector Zipkin compatible endpoint (optional)
查看运行中的容器实例:
doker ps
查看所有容器实例,包括没有运行的:
[root@skywalking ~]# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
5181eab3c173 jaegertracing/all-in-one:1.27 "/go/bin/all-in-one-…" 2 hours ago Up 2 hours 5775/udp, 5778/tcp, 14250/tcp, 6832/udp, 14268/tcp, 0.0.0.0:6831->6831/udp, 0.0.0.0:16686->16686/tcp silly_bardeen```
root 25723 26830 0 15:48 ? 00:00:00 /usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 16686 -container-ip 172.17.0.2 -container-port 16686
root 25737 26830 0 15:48 ? 00:00:00 /usr/bin/docker-proxy -proto udp -host-ip 0.0.0.0 -host-port 6831 -container-ip 172.17.0.2 -container-port 6831
你可以通过访问 http://localhost:16686 展示 Jaeger UI.
采样速率
支持设置采样率是 Jaeger 的一个亮点,生产环境系统性能很重要,所以对于所有的请求都开启 Trace 显然会带来比较大的压力,另外,大量的数据也会带来很大存储压力。为了尽量消除分布式追踪采样对系统带来的影响,设置采样率是一个很好的办法。Jaeger官方 支持四种采样类别,分别是 const、probabilistic、rateLimiting 和 remote
const:全量采集,采样率设置0,1 分别对应打开和关闭
probabilistic:概率采集,默认万份之一,取值可在 0 至 1 之间,例如设置为 0.5 的话意为只对 50% 的请求采样
rateLimiting:限速采集,每秒只能采集一定量的数据,如设置2的话,就是每秒采集2个链路数据
remote :是遵循远程设置,取值的含义和 probabilistic 相同,都意为采样的概率,只不过设置为 remote 后,Client 会从 Jaeger Agent 中动态获取采样率设置。
guaranteedThroughput:复合采样,至少每秒采样lowerBound次(rateLimiting),超过lowerBound次的话,按照samplingRate概率来采样(probabilistic),
看了代码,还有两个复合采样器,GuaranteedThroughputSampler 和 PerOperationSampler
总结
总之,Jaeger链路跟踪有很多值得我们学习的地方。
参考链接:
The OpenTracing Semantic Specification
OpenTracing Java Library教程
阿里云链路追踪 Tracing Analysis通过 Jaeger 上报 Java 应用数据