内容简介:SOFATracer 是一个用于分布式系统调用跟踪的组件,通过统一的从RoadMap 和目前还未支持的主要是 Dubbo、MQ 以及 Redis 等。本文将从 SOFATracer 已提供的一个插件源码来分析下 SOFATracer 插件的埋点实现。
SOFATracer 是一个用于分布式系统调用跟踪的组件,通过统一的 traceId
将调用链路中的各种网络调用情况以日志的方式记录下来,以达到透视化网络调用的目的。这些日志可用于故障的快速发现,服务治理等。
从RoadMap 和 PR 来看,目前 SOFATracer 已经支持了丰富的组件插件埋点。
目前还未支持的主要是 Dubbo、MQ 以及 Redis 等。本文将从 SOFATracer 已提供的一个插件源码来分析下 SOFATracer 插件的埋点实现。
1 SOFATracer 插件埋点机制
SOFATracer 插件的作用实际上就是对于不同组件进行埋点,以便于收集这些组件的链路数据。SOFATracer 埋点方式一般是通过 Filter、Interceptor 机制实现的。
另一个是,SOFATracer 的埋点方式并不是基于 OT-api 进行埋点的,而是基于 SOFATracer 自己的 api 进行埋点的,详见 issue#126 。
1.1 Filter or Interceptor
目前已实现的插件中,像 MVC 插件是基于 Filter 进行埋点的,httpclient、resttemplate 等是基于Interceptor进行埋点的。在实现插件时,要根据不同插件的特性来选择具体的埋点方式。
当然除了这两种方式之外还可以通过静态代理的方式来实现埋点。比如 sofa-tracer-datasource-plugin 插件就是将不同的数据源进行统一代理给 SmartDatasource,从而实现埋点的。
1.2 AbstractTracer API
SOFATracer 中所有的插件均需要实现自己的 Tracer 实例,如 Mvc 的 SpringMvcTracer 、HttpClient的 HttpClientTracer 等,这一点与基于 Opentracing-api 接口埋点的实现有所区别。
- 1、基于 SOFATracer api 埋点方式插件扩展
AbstractTracer 是 SOFATracer 用于插件扩展使用的一个抽象类,根据插件类型不同,又可以分为 clientTracer 和 serverTracer,分别对应于:AbstractClientTracer 和 AbstractServerTracer,再通过 AbstractClientTracer 和 AbstractServerTracer 衍生出具体的组件 Tracer 实现。这种方式的好处在于,所有的插件实现均由 SOFATracer 本身来管控,对于不同的组件可以轻松的实现差异化和定制化。缺点也源于此,每增加一个组件都需要做一些重复工作。
- 2、基于 OpenTracing-api 埋点方式插件扩展
这种埋点方式不基于 SOFATracer 自身提供的 API,而是基于 OpenTracing-api 接口。因为均遵循 OpenTracing-api 规范,所以组件和 Tracer 实现可以独立分开来维护。这样就可以对接开源的一些基于 OpenTracing-api 规范实现的组件。例如: OpenTracing API Contributions 。
SOFATracer 在后面将会在 4.0 版本中支持基于 OT-api 的埋点方式,对外部组件接入扩展提供支持。
1.3 AbstractTracer
这里先来看下 AbstractTracer 这个抽象类中具体提供了哪些抽象方法,也就是对于 AbstractClientTracer 和 AbstractServerTracer 需要分别扩展哪些能力。
// 获取client端 摘要日志日志名 protected abstract String getClientDigestReporterLogName(); // 获取client端 摘要日志滚动策略key protected abstract String getClientDigestReporterRollingKey(); // 获取client端 摘要日志日志名key protected abstract String getClientDigestReporterLogNameKey(); // 获取client端 摘要日志编码器 protected abstract SpanEncoder<SofaTracerSpan> getClientDigestEncoder(); // 创建client端 统计日志Reporter类 protected abstract AbstractSofaTracerStatisticReporter generateClientStatReporter(); // 获取server端 摘要日志日志名 protected abstract String getServerDigestReporterLogName(); // 获取server端 摘要日志滚动策略key protected abstract String getServerDigestReporterRollingKey(); // 获取server端 摘要日志日志名key protected abstract String getServerDigestReporterLogNameKey(); // 获取server端 摘要日志编码器 protected abstract SpanEncoder<SofaTracerSpan> getServerDigestEncoder(); // 创建server端 统计日志Reporter类 protected abstract AbstractSofaTracerStatisticReporter generateServerStatReporter(); 复制代码
从 AbstractTracer 类提供的抽象方法来看,不管是 client 还是 server,在具体的 Tracer 组件实现中,都必须提供以下实现:
- DigestReporterLogName :当前组件摘要日志的日志名称
- DigestReporterRollingKey : 当前组件摘要日志的滚动策略
- SpanEncoder:对摘要日志进行编码的编码器实现
- AbstractSofaTracerStatisticReporter : 统计日志 reporter 类的实现类。
2 SpringMVC 插件埋点分析
这里我们以 SpringMVC 插件为例,来分析下如何实现一个埋点插件的。这里是官方给出的案例工程:基于 Spring MVC 示例落地日志 。
2.1 实现 Tracer 实例
SpringMvcTracer 继承了 AbstractServerTracer 类,是对 serverTracer 的扩展。
PS:如何确定一个组件是client端还是server端呢?就是看当前组件是请求的发起方还是请求的接受方,如果是请求发起方则一般是client端,如果是请求接收方则是 server 端。那么对于 MVC 来说,是请求接受方,因此这里实现了 AbstractServerTracer 类。
public class SpringMvcTracer extends AbstractServerTracer 复制代码
2.1.1 构造函数与单例对象
在构造函数中,需要传入当前 Tracer 的 traceType,SpringMvcTracer 的 traceType 为 "springmvc"。这里也可以看到,tracer 实例是一个单例对象,对于其他插件也是一样的。
private volatile static SpringMvcTracer springMvcTracer = null; /*** * Spring MVC Tracer Singleton * @return singleton */ public static SpringMvcTracer getSpringMvcTracerSingleton() { if (springMvcTracer == null) { synchronized (SpringMvcTracer.class) { if (springMvcTracer == null) { springMvcTracer = new SpringMvcTracer(); } } } return springMvcTracer; } private SpringMvcTracer() { super("springmvc"); } 复制代码
2.1.2 AbstractServerTracer 抽象类
在看 SpringMvcTracer 实现之前,先来看下 AbstractServerTracer。
public abstract class AbstractServerTracer extends AbstractTracer { // 构造函数,子类必须提供一个构造函数 public AbstractServerTracer(String tracerType) { super(tracerType, false, true); } // 因为是server端,所以Client先关的提供了默认实现,返回null protected String getClientDigestReporterLogName() { return null; } protected String getClientDigestReporterRollingKey() { return null; } protected String getClientDigestReporterLogNameKey() { return null; } protected SpanEncoder<SofaTracerSpan> getClientDigestEncoder() { return null; } protected AbstractSofaTracerStatisticReporter generateClientStatReporter() { return null; } } 复制代码
结合上面 AbstractTracer 小节中抽象方法分析,这里在 AbstractServerTracer 中将 client 对应的抽象方法提供了默认实现,也就是说如果要继承 AbstractServerTracer 类,那么就必须实现 server 对应的所有抽象方法。
2.1.3 SpringMVCTracer 实现
下面是 SpringMvcTracer 部分对 server 部分抽象方法的实现。
@Override protected String getServerDigestReporterLogName() { return SpringMvcLogEnum.SPRING_MVC_DIGEST.getDefaultLogName(); } @Override protected String getServerDigestReporterRollingKey() { return SpringMvcLogEnum.SPRING_MVC_DIGEST.getRollingKey(); } @Override protected String getServerDigestReporterLogNameKey() { return SpringMvcLogEnum.SPRING_MVC_DIGEST.getLogNameKey(); } @Override protected SpanEncoder<SofaTracerSpan> getServerDigestEncoder() { if (Boolean.TRUE.toString().equalsIgnoreCase( SofaTracerConfiguration.getProperty(SPRING_MVC_JSON_FORMAT_OUTPUT))) { return new SpringMvcDigestJsonEncoder(); } else { return new SpringMvcDigestEncoder(); } } @Override protected AbstractSofaTracerStatisticReporter generateServerStatReporter() { return generateSofaMvcStatReporter(); } 复制代码
目前 SOFATracer 日志名、滚动策略key等都是通过枚举类来定义的,也就是一个组件会对应这样一个枚举类,在枚举类里面定义这些常量。
2.2 SpringMvcLogEnum 类实现
SpringMVC 插件中的枚举类是 SpringMvcLogEnum。
public enum SpringMvcLogEnum { // 摘要日志相关 SPRING_MVC_DIGEST("spring_mvc_digest_log_name", "spring-mvc-digest.log", "spring_mvc_digest_rolling"), // 统计日志相关 SPRING_MVC_STAT("spring_mvc_stat_log_name", "spring-mvc-stat.log", "spring_mvc_stat_rolling"); // 省略部分代码.... } 复制代码
在 XXXLogEnum 枚举类中定义了当前组件对应的摘要日志和统计日志的日志名和滚动策略,因为 SOFATracer 目前还没有服务端的能力,链路数据不是直接上报给 server 的,因此 SOFATracer 提供了落到磁盘的能力。不同插件的链路日志也会通过 XXXLogEnum 指定的名称将链路日志输出到各个组件对应的日志目录下。
2.3 统计日志 Reportor 实现
SOFATracer 中统计日志打印的实现需要各个组件自己来完成,具体就是需要实现一个AbstractSofaTracerStatisticReporter 的子类,然后实现 doReportStat 这个方法。当然对于目前的实现来说,我们也会重写 print 方法。
2.3.1 doReportStat
@Override public void doReportStat(SofaTracerSpan sofaTracerSpan) { Map<String, String> tagsWithStr = sofaTracerSpan.getTagsWithStr(); // 构建StatMapKey对象 StatMapKey statKey = new StatMapKey(); // 增加 key:当前应用名 statKey.addKey(CommonSpanTags.LOCAL_APP, tagsWithStr.get(CommonSpanTags.LOCAL_APP)); // 增加 key:请求 url statKey.addKey(CommonSpanTags.REQUEST_URL, tagsWithStr.get(CommonSpanTags.REQUEST_URL)); // 增加 key:请求方法 statKey.addKey(CommonSpanTags.METHOD, tagsWithStr.get(CommonSpanTags.METHOD)); // 压测标志 statKey.setLoadTest(TracerUtils.isLoadTest(sofaTracerSpan)); // 请求响应码 String resultCode = tagsWithStr.get(CommonSpanTags.RESULT_CODE); // 请求成功标识 boolean success = (resultCode != null && resultCode.length() > 0 && this .isHttpOrMvcSuccess(resultCode)); statKey.setResult(success ? "true" : "false"); //end statKey.setEnd(TracerUtils.getLoadTestMark(sofaTracerSpan)); //value the count and duration long duration = sofaTracerSpan.getEndTime() - sofaTracerSpan.getStartTime(); long values[] = new long[] { 1, duration }; // reserve this.addStat(statKey, values); } 复制代码
这里就是就是将统计日志添加到日志槽里,等待被消费(输出到日志)。具体可以参考:SofaTracerStatisticReporterManager.StatReporterPrinter。
2.3.2 print
print 方法是实际将数据写入到磁盘的方法。
@Override public void print(StatKey statKey, long[] values) { if (this.isClosePrint.get()) { //关闭统计日志输出 return; } if (!(statKey instanceof StatMapKey)) { return; } StatMapKey statMapKey = (StatMapKey) statKey; try { // 构建需要打印的数据串 jsonBuffer.reset(); jsonBuffer.appendBegin(); jsonBuffer.append("time", Timestamp.currentTime()); jsonBuffer.append("stat.key", this.statKeySplit(statMapKey)); jsonBuffer.append("count", values[0]); jsonBuffer.append("total.cost.milliseconds", values[1]); jsonBuffer.append("success", statMapKey.getResult()); //压测 jsonBuffer.appendEnd("load.test", statMapKey.getEnd()); if (appender instanceof LoadTestAwareAppender) { ((LoadTestAwareAppender) appender).append(jsonBuffer.toString(), statMapKey.isLoadTest()); } else { appender.append(jsonBuffer.toString()); } // 这里强制刷一次 appender.flush(); } catch (Throwable t) { SelfLog.error("统计日志<" + statTracerName + ">输出异常", t); } } 复制代码
print 这个方法里面就是将 statMapKey 中,也就是 doReportStat 中塞进来的数据转换成 json 格式,然后刷到磁盘。需要注意的是这里是强制 flush 了一次。如果没有重写 print 这个方法的话,则是在SofaTracerStatisticReporterManager.StatReporterPrinter 里面调用 print 方法刷到磁盘。
2.4 数据传播格式实现
SOFATracer 支持使用 OpenTracing 的内建格式进行上下文传播。
public class SpringMvcHeadersCarrier implements TextMap { private HashMap<String, String> headers; public SpringMvcHeadersCarrier(HashMap<String, String> headers) { this.headers = headers; } @Override public void put(String key, String value) { headers.put(key, value); } @Override public Iterator<Map.Entry<String, String>> iterator() { return headers.entrySet().iterator(); } } 复制代码
2.5 自定义编码格式实现
这个决定了摘要日志打印的格式,和在统计日志里面的实现要有所区分。
public class SpringMvcDigestJsonEncoder extends AbstractDigestSpanEncoder { // 重写encode,对span进行编码处理 @Override public String encode(SofaTracerSpan span) throws IOException { JsonStringBuilder jsonStringBuilder = new JsonStringBuilder(); //日志打印时间 jsonStringBuilder.appendBegin("time", Timestamp.format(span.getEndTime())); appendSlot(jsonStringBuilder, span); return jsonStringBuilder.toString(); } // 具体字段处理 private void appendSlot(JsonStringBuilder jsonStringBuilder, SofaTracerSpan sofaTracerSpan) { SofaTracerSpanContext context = sofaTracerSpan.getSofaTracerSpanContext(); Map<String, String> tagWithStr = sofaTracerSpan.getTagsWithStr(); Map<String, Number> tagWithNumber = sofaTracerSpan.getTagsWithNumber(); //当前应用名 jsonStringBuilder .append(CommonSpanTags.LOCAL_APP, tagWithStr.get(CommonSpanTags.LOCAL_APP)); //TraceId jsonStringBuilder.append("traceId", context.getTraceId()); //RpcId jsonStringBuilder.append("spanId", context.getSpanId()); //请求 URL jsonStringBuilder.append(CommonSpanTags.REQUEST_URL, tagWithStr.get(CommonSpanTags.REQUEST_URL)); //请求方法 jsonStringBuilder.append(CommonSpanTags.METHOD, tagWithStr.get(CommonSpanTags.METHOD)); //Http 状态码 jsonStringBuilder.append(CommonSpanTags.RESULT_CODE, tagWithStr.get(CommonSpanTags.RESULT_CODE)); Number requestSize = tagWithNumber.get(CommonSpanTags.REQ_SIZE); //Request Body 大小 单位为byte jsonStringBuilder.append(CommonSpanTags.REQ_SIZE, (requestSize == null ? 0L : requestSize.longValue())); Number responseSize = tagWithNumber.get(CommonSpanTags.RESP_SIZE); //Response Body 大小,单位为byte jsonStringBuilder.append(CommonSpanTags.RESP_SIZE, (responseSize == null ? 0L : responseSize.longValue())); //请求耗时(MS) jsonStringBuilder.append("time.cost.milliseconds", (sofaTracerSpan.getEndTime() - sofaTracerSpan.getStartTime())); jsonStringBuilder.append(CommonSpanTags.CURRENT_THREAD_NAME, tagWithStr.get(CommonSpanTags.CURRENT_THREAD_NAME)); //穿透数据放在最后 jsonStringBuilder.appendEnd("baggage", baggageSerialized(context)); } } 复制代码
从这里其实也可以看出,统计日志和摘要日志的不同点。统计日志里面核心的数据是 span 里面的 tags 数据,但是其主要作用是统计当前组件的次数。摘要日志里面除了 tags 里面的数据之外还会包括例如 traceId 和 spanId 等信息。
- 统计日志
{"time":"2018-11-28 14:42:25.127","stat.key":{"method":"GET","local.app":"SOFATracerSpringMVC","request.url":"http://localhost:8080/springmvc"},"count":3,"total.cost.milliseconds":86,"success":"true","load.test":"F"} 复制代码
- 摘要日志
{"time":"2018-11-28 14:46:08.216","local.app":"SOFATracerSpringMVC","traceId":"0a0fe91b1543387568214100259231","spanId":"0.1","request.url":"http://localhost:8080/springmvc","method":"GET","result.code":"200","req.size.bytes":-1,"resp.size.bytes":0,"time.cost.milliseconds":2,"current.thread.name":"http-nio-8080-exec-2","baggage":""} 复制代码
2.6 请求拦截埋点
对于基于标准 servlet 实现的组件,要实现对请求的拦截过滤,通常就是 Filter 了。sofa-tracer-springmvc-plugin 插件埋点的实现就是基于 Filter 机制完成的。
SpringMvcSofaTracerFilter 实现了 javax.servlet.Filter 接口,因此遵循标准的 servlet 规范的容器也可以通过此插件进行埋点。参考文档: 对于标准 servlet 容器的支持( tomcat/jetty 等) 。
public class SpringMvcSofaTracerFilter implements Filter 复制代码
2.6.1 基本埋点思路
对于一个组件来说,一次处理过程一般是产生一个 span。这个span的生命周期是从接收到请求到返回响应这段过程。
但是这里需要考虑的问题是如何与上下游链路关联起来呢?在 Opentracing 规范中,可以在 Tracer 中 extract 出一个跨进程传递的 SpanContext 。然后通过这个 SpanContext 所携带的信息将当前节点关联到整个 tracer 链路中去。当然有提取(extract)就会有对应的注入(inject)。
链路的构建一般是 client-server-client-server 这种模式的,那这里就很清楚了,就是会在 client 端进行注入(inject),然后再 server 端进行提取(extract),反复进行,然后一直传递下去。
在拿到 SpanContext 之后,此时当前的 span 就可以关联到这条链路中了,那么剩余的事情就是收集当前组件的一些数据。
整个过程大概分为以下几个阶段:
- 从请求中提取 spanContext
- 构建 span,并将当前 span 存入当前 tracer上下文中(SofaTraceContext.push(span)) 。
- 设置一些信息到span中
- 返回响应
- span结束&上报
下面逐一分析下这几个过程。
2.6.2 从请求中提取 spanContext
这里的提取用到了上面我们提到的#数据传播格式实现#SpringMvcHeadersCarrier 这个类。上面分析到,因为mvc 做作为 server 端存在的,所以在 server 端就是从请求中 extract 出 SpanContext。
public SofaTracerSpanContext getSpanContextFromRequest(HttpServletRequest request) { HashMap<String, String> headers = new HashMap<String, String>(); // 获取请求头信息 Enumeration headerNames = request.getHeaderNames(); while (headerNames.hasMoreElements()) { String key = (String) headerNames.nextElement(); String value = request.getHeader(key); headers.put(key, value); } // 拿到 SofaTracer 实例对象 SofaTracer tracer = springMvcTracer.getSofaTracer(); // 解析出 SofaTracerSpanContext(SpanContext的实现类) SofaTracerSpanContext spanContext = (SofaTracerSpanContext) tracer.extract( ExtendFormat.Builtin.B3_HTTP_HEADERS, new SpringMvcHeadersCarrier(headers)); spanContext.setSpanId(spanContext.nextChildContextId()); return spanContext; } 复制代码
2.6.3 获取 span & 数据获取
serverReceive 这个方法是在 AbstractTracer 类中提供了实现,子类不需要关注这个。在 SOFATracer 中将请求大致分为以下几个过程:
- 客户端发送请求 clientSend cs
- 服务端接受请求 serverReceive sr
- 服务端返回结果 serverSend ss
- 客户端接受结果 clientReceive cr
无论是哪个插件,在请求处理周期内都可以从上述几个阶段中找到对应的处理方法。因此,SOFATracer 对这几个阶段处理进行了封装。这四个阶段实际上会产生两个 span,第一个 span 的起点是 cs,到 cr 结束;第二个 span是从 sr 开始,到 ss 结束。也就是说当执行 clientSend 和 serverReceive 时会返回一个 span 对象。来看下MVC中的实现:
红色框内对应的服务端接受请求,也就是 sr 阶段,产生了一个 span 。红色框下面的这段代码是为当前这个 span 设置一些基本的信息,包括当前应用的应用名、当前请求的url、当前请求的请求方法以及请求大小。
2.6.4 返回响应与结束 span
在 filter 链执行结束之后,在 finally 块中又补充了当前请求响应结果的一些信息到 span 中去。然后调用serverSend 结束当前 span。这里关于 serverSend 里面的逻辑就不展开说了,不过能够想到的是这里肯定是调用span.finish 这个方法( opentracing 规范中,span.finish 的执行标志着一个 span 的结束),当前也会包括对于数据上报的一些逻辑处理等。
3 思路总结与插件编写流程
在第2节中以 SpringMVC 插件为例,分析了下 SOFATracer 插件埋点实现的一些细节。那么本节则从整体思路上来总结下如何编写一个 SOFATracer 的插件。
- 1、确定所要实现的插件,然后确定以哪种方式来埋点
- 2、实现当前插件的 Tracer 实例,这里需要明确当前插件是以 client 存在还是以 server 存在。
- 3、实现一个枚举类,用来描述当前组件的日志名称和滚动策略 key 值等
- 4、实现插件摘要日志的 encoder ,实现当前组件的定制化输出
- 5、实现插件的统计日志 Reporter 实现类,通过继承 AbstractSofaTracerStatisticReporter 类并重写doReportStat。
- 6、定义当前插件的传播格式
当然最重要的还是对于要实现插件的理解,要明确我们需要收集哪些数据。
小结
本文先介绍了SOFATracer的埋点方式与标准OT-api 埋点方式的区别,然后对 SOFATracer 中 SpringMVC 插件的埋点实现进行了分析。希望通过本文能够让更多的同学理解埋点实现这样一个过程以及需要关注的一些点。如果有兴趣或者有什么实际的需求,欢迎来讨论。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- webpack4配置详解之常用插件分享 原 荐
- IDEA 插件:多线程文件下载插件开发
- 从头开发一个Flutter插件(二)高德地图定位插件
- Gradle插件开发系列之gradle插件调试方法
- Gradle插件开发系列之开发第一个gradle插件
- WordPress插件开发 -- 在插件使用数据库存储数据
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。