0x00 摘要
SOFA是蚂蚁金服自主研发的金融级分布式中间件,包含了构建金融级云原生架构所需的各个组件,SOFATracer 是其中用于分布式系统调用跟踪的组件。
笔者之前有过zipkin的经验,希望扩展到Opentracing,于是在学习SOFATracer官方博客结合源码的基础上总结出此文与上文,与大家分享。
书接上文,参见[业界方案] 用SOFATracer学习分布式追踪系统Opentracing (1),我们继续讲解。
0x07 SOFATracer 的插件埋点机制
对一个应用的跟踪要关注的无非就是 客户端--->web 层--->rpc 服务--->dao 后端存储、cache 缓存、消息队列 mq 等这些基础组件
。SOFATracer 插件的作用实际上也就是对不同组件进行埋点,以便基于这些组件采集应用的链路数据。
不同组件有不同的应用场景和扩展点,因此对插件的实现也要因地制宜,SOFATracer 埋点方式一般是通过 Filter、Interceptor 机制实现的。
7.1 组件扩展入口之 Filter or Interceptor
SOFATracer 目前已实现的插件中,像 SpringMVC 插件是基于 Filter 进行埋点的,httpclient、resttemplate 等是基于 Interceptor 机制进行埋点的。在实现插件时,要根据不同插件的特性和扩展点来选择具体的埋点方式。正所谓条条大路通罗马,不管怎么实现埋点,都是依赖 SOFATracer 自身 API 的扩展机制来实现。
SOFATracer 中所有的插件均需要实现自己的 Tracer 实例,如 SpringMVC 的 SpringMvcTracer 、HttpClient 的 HttpClientTracer 等。
AbstractTracer 是 SOFATracer 用于插件扩展使用的一个抽象类,根据插件类型不同,又可以分为 clientTracer 和 serverTracer,分别对应于 AbstractClientTracer 和 AbstractServerTracer;再通过 AbstractClientTracer 和 AbstractServerTracer 衍生出具体的组件 Tracer 实现,比如上图中提到的 HttpClientTracer 、RestTemplateTracer 、SpringMvcTracer 等插件 Tracer 实现。
如何确定一个组件是 client 端还是 server 端呢?就是看当前组件是请求的发起方还是请求的接受方,如果是请求发起方则一般是 client 端,如果是请求接收方则是 server 端。那么对于 RPC 来说,即是请求的发起方也是请求的接受方,因此这里实现了 AbstractTracer 类。
7.2 插件扩展基本思路总结
对于一个组件来说,一次处理过程一般是产生一个 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 结束&上报
7.3 标准 Servlet 规范埋点原理
SOFATracer 支持对标准 Servlet 规范的 Web MVC 埋点,包括普通的 Servlet 和 Spring MVC 等,基本原理就是基于 Servelt 规范所提供的 javax.servlet.Filter 过滤器接口扩展实现。
过滤器位于 Client 和 Web 应用程序之间,用于检查和修改两者之间流过的请求和响应信息。在请求到达 Servlet 之前,过滤器截获请求。在响应送给客户端之前,过滤器截获响应。多个过滤器形成一个 FilterChain,FilterChain 中不同过滤器的先后顺序由部署文件 web.xml 中过滤器映射的顺序决定。最先截获客户端请求的过滤器将最后截获 Servlet 的响应信息。
Web 应用程序一般作为请求的接收方,在 SOFATracer 中应用是作为 Server 存在的,其在解析 SpanContext 时所对应的事件为 sr (server receive)。
SOFATracer 在 sofa-tracer-springmvc-plugin 插件中解析及产生 Span 的过程大致如下:
- Servlet Filter 拦截到 request 请求;
- 从请求中解析 SpanContext;
- 通过 SpanContext 构建当前 MVC 的 Span;
- 给当前 Span 设置 tag、log;
- 在 Filter 处理的最后,结束 Span;
7.4 HTTP 客户端埋点原理
HTTP 客户端埋点包括 HttpClient、OkHttp、RestTemplate 等,此类埋点一般都是基于拦截器机制来实现的,如 HttpClient 使用的 HttpRequestInterceptor、HttpResponseInterceptor;OkHttp 使用的 okhttp3.Interceptor;RestTemplate 使用的 ClientHttpRequestInterceptor。
以 OkHttp 为例,简单分析下 HTTP 客户端埋点的实现原理:
@Override
public Response intercept(Chain chain) throws IOException {
// 获取请求
Request request = chain.request();
// 解析出 SpanContext ,然后构建 Span
SofaTracerSpan sofaTracerSpan = okHttpTracer.clientSend(request.method());
// 发起具体的调用
Response response = chain.proceed(appendOkHttpRequestSpanTags(request, sofaTracerSpan));
// 结束 span
okHttpTracer.clientReceive(String.valueOf(response.code()));
return response;
}
0x08 请求总体过程
在 SOFATracer 中将请求大致分为以下几个过程:
- 客户端发送请求 clientSend cs
- 服务端接受请求 serverReceive sr
- 服务端返回结果 serverSend ss
- 客户端接受结果 clientReceive cr
无论是哪个插件,在请求处理周期内都可以从上述几个阶段中找到对应的处理方法。因此,SOFATracer 对这几个阶段处理进行了封装。
在SOFA这里,四个阶段实际上会产生两个 Span,第一个 Span 的起点是 cs,到 cr 结束;第二个 Span 是从 sr 开始,到 ss 结束。
clientSend // 客户端发送请求,也就是 cs 阶段,会产生一个 Span。
serverReceive // 服务端接收请求 sr 阶段,产生了一个 Span 。
...
serverSend
clientReceive
从时间序列上看,如下图所示。
Client Server
+--------------+ Request +--------------+
| Client Send | +----------------> |Server Receive|
+------+-------+ +------+-------+
| |
| v
| +------+--------+
| |Server Business|
| +------+--------+
| |
| |
v v
+------+--------+ Response +------+-------+
|Client Receive | <---------------+ |Server Send |
+------+--------+ +------+-------+
| |
| |
v v
8.1 TraceID
产生trace ID 是在 客户端发送请求 clientSend cs 这个阶段,即,此 ID 一般由集群中第一个处理请求的系统产生,并在分布式调用下通过网络传递到下一个被请求系统。就是 AbstractTracer # clientSend 函数。
-
调用 buildSpan 构建一个 SofaTracerSpan clientSpan,然后调用 start 函数建立一个 Span。
-
如果不存在Parent context,则调用 createRootSpanContext 建立了 new root span context。
-
sofaTracerSpanContext = this.createRootSpanContext();
- 调用 String traceId = TraceIdGenerator.generate(); 来构建 trace ID。
-
-
如果存在 Parent context,则调用 createChildContext 建立 span context。
-
-
对 clientSpan 设置各种 Tag。
-
clientSpan.setTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_CLIENT);
-
-
对 clientSpan 设置 log。
-
clientSpan.log(LogData.CLIENT_SEND_EVENT_VALUE);
-
-
把 clientSpan 设置进入SpanContext.
-
sofaTraceContext.push(clientSpan);
-
具体产生traceId 的代码是在类 TraceIdGenerator 中。可以看到,TraceId 是由 ip,时间戳,递增序列,进程ID等构成,即traceId为服务器 IP + 产生 ID 时候的时间 + 自增序列 + 当前进程号,以此保证全局唯一性。这就回答了我们之前提过的问题:traceId是怎么生成的,有什么规则?
public class TraceIdGenerator {
private static String IP_16 = "ffffffff";
private static AtomicInteger count = new AtomicInteger(1000);
private static String getTraceId(String ip, long timestamp, int nextId) {
StringBuilder appender = new StringBuilder(30);
appender.append(ip).append(timestamp).append(nextId).append(TracerUtils.getPID());
return appender.toString();
}
public static String generate() {
return getTraceId(IP_16, System.currentTimeMillis(), getNextId());
}
private static String getIP_16(String ip) {
String[] ips = ip.split("\\.");
StringBuilder sb = new StringBuilder();
String[] var3 = ips;
int var4 = ips.length;
for(int var5 = 0; var5 < var4; ++var5) {
String column = var3[var5];
String hex = Integer.toHexString(Integer.parseInt(column));
if (hex.length() == 1) {
sb.append('0').append(hex);
} else {
sb.append(hex);
}
}
return sb.toString();
}
private static int getNextId() {
int current;
int next;
do {
current = count.get();
next = current > 9000 ? 1000 : current + 1;
} while(!count.compareAndSet(current, next));
return next;
}
static {
try {
String ipAddress = TracerUtils.getInetAddress();
if (ipAddress != null) {
IP_16 = getIP_16(ipAddress);
}
} catch (Throwable var1) {
}
}
}
8.2 SpanID
有两个地方会生成SpanId : CS, SR。SOFARPC 和 Dapper不同,spanId中已经包含了调用链上下文关系,包含parent spanId 的信息。比如 系统在处理一个请求的过程中依次调用了 B,C,D 三个系统,那么这三次调用的的 SpanId 分别是:0.1,0.2,0.3。如果 C 系统继续调用了 E,F 两个系统,那么这两次调用的 SpanId 分别是:0.2.1,0.2.2。
8.2.1 Client Send
接上面小节,在客户端发送请求 clientSend cs 这个阶段,就会构建Span,从而生成 SpanID。
-
调用 buildSpan 构建一个 SofaTracerSpan clientSpan,然后调用 start 函数建立一个 Span。
-
如果不存在Parent context,则调用 createRootSpanContext 建立了 new root span context。
-
sofaTracerSpanContext = this.createRootSpanContext();
- 调用 SofaTracerSpanContext 生成新的SpanContext,里面就生成了新的Span ID。
-
-
如果存在 Parent context,则调用 createChildContext 建立 span context,这里的 preferredReference.getSpanId() 就生成了Span ID。因为此时已经有了Parent Context,所以新的Span Id是在 Parent Span id基础上构建。
-
SofaTracerSpanContext sofaTracerSpanContext = new SofaTracerSpanContext( preferredReference.getTraceId(), preferredReference.nextChildContextId(), preferredReference.getSpanId(), preferredReference.isSampled());
-
-
8.2.2 Server Receive
我们再以 Server Receive这个动作为例,可以看到在Server端 的 Span构建过程。
-
SpringMvcSofaTracerFilter # doFilter 会从 Header 中提取 SofaTracerSpanContext。
- 利用 SofaTracer # extract 提取SofaTracerSpanContext,这里用到了 SpringMvcHeadersCarrier。
- 利用 RegistryExtractorInjector # extract 从 SpringMvcHeadersCarrier 中提取 SpanContext。
- 利用 AbstractTextB3Formatter # extract 从 SpringMvcHeadersCarrier 中提取 SpanContext。
- 利用 RegistryExtractorInjector # extract 从 SpringMvcHeadersCarrier 中提取 SpanContext。
- 利用 SofaTracer # extract 提取SofaTracerSpanContext,这里用到了 SpringMvcHeadersCarrier。
-
AbstractTracer # serverReceive 会根据 SofaTracerSpanContext 进行后续操作,此时 SofaTracerSpanContext 如下:
-
sofaTracerSpanContext = {SofaTracerSpanContext@6056} "SofaTracerSpanContext{traceId='c0a80103159927161709310013925', spanId='0', parentId='', isSampled=true, bizBaggage={}, sysBaggage={}, childContextIndex=0}" traceId = "c0a80103159927161709310013925" spanId = "0" parentId = "" isSampled = true sysBaggage = {ConcurrentHashMap@6060} size = 0 bizBaggage = {ConcurrentHashMap@6061} size = 0 childContextIndex = {AtomicInteger@6062} "0"
-
从当前线程取出当前的SpanContext,然后提取serverSpan,此 serverSpan 可能为null,也可能有值。
-
SofaTraceContext sofaTraceContext = SofaTraceContextHolder.getSofaTraceContext(); SofaTracerSpan serverSpan = sofaTraceContext.pop();
-
如果serverSpan为null,则生成一个新的 newSpan,然后调用 setSpanId 对传入的 SofaTracerSpanContext 参数进行设置新的 SpanId
-
sofaTracerSpanContext.setSpanId(sofaTracerSpanContext.nextChildContextId()); 此时 sofaTracerSpanContext 内容有变化了,具体就是spanId。 sofaTracerSpanContext = {SofaTracerSpanContext@6056} traceId = "c0a80103159927161709310013925" spanId = "0.1" parentId = "" .....
-
-
如果serverSpan 不为 null,则 newSpan = serverSpan
-
-
设置log
-
设置Tag
-
把 newSpan 设置进入本地上下文。
sofaTraceContext.push(newSpan);
-
需要注意,在链路的后续环节中,traceId 和 spanId 都是存储在本地线程的 sofaTracerSpanContext 之中,不是在 Span 之中。
具体代码如下:
首先,SpringMvcSofaTracerFilter # doFilter 会从 Header 中提取 SofaTracerSpanContext
public class SpringMvcSofaTracerFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
FilterChain filterChain) {
// 从header中提取Context
SofaTracerSpanContext spanContext = getSpanContextFromRequest(request);
// sr
springMvcSpan = springMvcTracer.serverReceive(spanContext);
}
}
其次,AbstractTracer # serverReceive 会根据 SofaTracerSpanContext 进行后续操作
public abstract class AbstractTracer {
public SofaTracerSpan serverReceive(SofaTracerSpanContext sofaTracerSpanContext) {
SofaTracerSpan newSpan = null;
SofaTraceContext sofaTraceContext = SofaTraceContextHolder.getSofaTraceContext();
SofaTracerSpan serverSpan = sofaTraceContext.pop();
try {
if (serverSpan == null) {
if (sofaTracerSpanContext == null) {
sofaTracerSpanContext = SofaTracerSpanContext.rootStart();
isCalculateSampled = true;
} else {
sofaTracerSpanContext.setSpanId(sofaTracerSpanContext.nextChildContextId());
}
newSpan = this.genSeverSpanInstance(System.currentTimeMillis(),
StringUtils.EMPTY_STRING, sofaTracerSpanContext, null);
} else {
newSpan = serverSpan;
}
}
}
}
我们可以看到,SpanID的构建规则相对简单,这就回答了我们之前提过的问题:spanId是怎么生成的,有什么规则? 以及 ParentSpan 从哪儿来?
public class SofaTracerSpanContext implements SpanContext {
private AtomicInteger childContextIndex = new AtomicInteger(0);
public String nextChildContextId() {
return this.spanId + RPC_ID_SEPARATOR + childContextIndex.incrementAndGet();
}
}
0x09 Client 发送
本节我们看看RestTemplate是如何发送请求的。
首先,打印出程序运行时候的Stack如下,这样大家可以先有一个大致的印象:
intercept:56, RestTemplateInterceptor (com.sofa.alipay.tracer.plugins.rest.interceptor)
execute:92, InterceptingClientHttpRequest$InterceptingRequestExecution (org.springframework.http.client)
executeInternal:76, InterceptingClientHttpRequest (org.springframework.http.client)
executeInternal:48, AbstractBufferingClientHttpRequest (org.springframework.http.client)
execute:53, AbstractClientHttpRequest (org.springframework.http.client)
doExecute:734, RestTemplate (org.springframework.web.client)
execute:669, RestTemplate (org.springframework.web.client)
getForEntity:337, RestTemplate (org.springframework.web.client)
main:40, RestTemplateDemoApplication (com.alipay.sofa.tracer.examples.rest)
在 InterceptingClientHttpRequest # execute 此处代码中
class InterceptingClientHttpRequest extends AbstractBufferingClientHttpRequest {
@Override
public ClientHttpResponse execute(HttpRequest request, byte[] body) throws IOException {
if (this.iterator.hasNext()) {
ClientHttpRequestInterceptor nextInterceptor = this.iterator.next();
return nextInterceptor.intercept(request, body, this); // 这里进行拦截处理
}
}
}
最后是来到了 SOFA 的拦截器中,这里会做处理。
9.1 生成Span
具体实现代码是在 RestTemplateInterceptor # intercept函数。
我们可以看到,RestTemplateInterceptor这里有一个成员变量 restTemplateTracer,具体处理就是在 restTemplateTracer 这里实现。可以看到这里包含了 clientSend 和 clientReceive 两个过程。
-
首先生成一个Span。SofaTracerSpan sofaTracerSpan = restTemplateTracer.clientSend(request.getMethod().name());
-
先从 SofaTraceContext 取出 serverSpan。如果本 client 就是 一个服务中间点(即 serverSpan 不为空),那么需要给新span设置父亲Span。
-
调用
clientSpan = (SofaTracerSpan)this.sofaTracer.buildSpan(operationName).asChildOf(serverSpan).start();
得到本身的 client Span。如果有 server Span,则本 Client Span 就是 Sever Span的 child。-
public Tracer.SpanBuilder asChildOf(Span parentSpan) { if (parentSpan == null) { return this; } return addReference(References.CHILD_OF, parentSpan.context()); }
-
-
设置父亲
clientSpan.setParentSofaTracerSpan(serverSpan);
-
-
然后调用 appendRestTemplateRequestSpanTags 来把Span放入Request的Header中。
- 给Span加入各种Tag,比如 app, url, method...
- 进行Carrier处理
,injectCarrier(request, sofaTracerSpan);
- 调用 AbstractTextB3Formatter.inject 设置 traceId, spanId, parentId ....
-
发送请求。
-
收到服务器返回之后进一步处理。
-
从ThreadLocal中获取 sofaTraceContext
-
从 SofaTracerSpan 中获取 currentSpan
-
调用 appendRestTemplateResponseSpanTags 设置各种 Tag
-
调用 restTemplateTracer.clientReceive(resultCode); 处理
-
clientSpan = sofaTraceContext.pop(); 把之前的Span移除
-
调用 clientReceiveTagFinish ,进而调用 clientSpan.finish();
- 调用
SpanTracer.reportSpan
进行 Span 上报,其中Reporter 数据上报 reportSpan 或者链路跨度 SofaTracerSpan 启动调用采样器 sample 方法检查链路是否需要采样,获取采样状态 SamplingStatus 是否采样标识 isSampled。
- 调用
-
如果还有父亲Span,则需要再push 父亲 Span进入Context。
sofaTraceContext.push(clientSpan.getParentSofaTracerSpan());
以备后续处理。
-
-
具体代码如下:
public class RestTemplateInterceptor implements ClientHttpRequestInterceptor {
protected AbstractTracer restTemplateTracer; // Sofa内部逻辑实现
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body,
ClientHttpRequestExecution execution) throws IOException {
SofaTracerSpan sofaTracerSpan = restTemplateTracer.clientSend(request.getMethod().name()); // 生成Span
appendRestTemplateRequestSpanTags(request, sofaTracerSpan); //放入Header
ClientHttpResponse response = null;
Throwable t = null;
try {
return response = execution.execute(request, body); //发送请求
} catch (IOException e) {
t = e;
throw e;
} finally {
SofaTraceContext sofaTraceContext = SofaTraceContextHolder.getSofaTraceContext();
SofaTracerSpan currentSpan = sofaTraceContext.getCurrentSpan();
String resultCode = SofaTracerConstant.RESULT_CODE_ERROR;
// is get error
if (t != null) {
currentSpan.setTag(Tags.ERROR.getKey(), t.getMessage());
// current thread name
sofaTracerSpan.setTag(CommonSpanTags.CURRENT_THREAD_NAME, Thread.currentThread()
.getName());
}
if (response != null) {
//tag append
appendRestTemplateResponseSpanTags(response, currentSpan);
//finish
resultCode = String.valueOf(response.getStatusCode().value());
}
restTemplateTracer.clientReceive(resultCode);
}
}
}
9.2 Fomatter
上文提到了发送时候会调用 AbstractTextB3Formatter.inject 设置 traceId, spanId, parentId。
Fomatter 这个接口负责了具体场景中序列化/反序列化上下文的具体逻辑,例如在HttpCarrier使用中通常就会有一个对应的HttpFormatter。Tracer的注入和提取就是委托给了Formatter。
执行时候堆栈如下:
inject:141, AbstractTextB3Formatter (com.alipay.common.tracer.core.registry)
inject:26, AbstractTextB3Formatter (com.alipay.common.tracer.core.registry)
inject:115, SofaTracer (com.alipay.common.tracer.core)
injectCarrier:146, RestTemplateInterceptor (com.sofa.alipay.tracer.plugins.rest.interceptor)
appendRestTemplateRequestSpanTags:141, RestTemplateInterceptor (com.sofa.alipay.tracer.plugins.rest.interceptor)
intercept:57, RestTemplateInterceptor (com.sofa.alipay.tracer.plugins.rest.interceptor)
execute:92, InterceptingClientHttpRequest$InterceptingRequestExecution (org.springframework.http.client)
executeInternal:76, InterceptingClientHttpRequest (org.springframework.http.client)
executeInternal:48, AbstractBufferingClientHttpRequest (org.springframework.http.client)
execute:53, AbstractClientHttpRequest (org.springframework.http.client)
doExecute:734, RestTemplate (org.springframework.web.client)
execute:669, RestTemplate (org.springframework.web.client)
getForEntity:337, RestTemplate (org.springframework.web.client)
main:40, RestTemplateDemoApplication (com.alipay.sofa.tracer.examples.rest)
OpenTracing提供了两个处理“跟踪上下文(trace context)”的函数:
- “extract(format,carrier)”从媒介(通常是HTTP头)获取跟踪上下文。
- “inject(SpanContext,format,carrier)” 将跟踪上下文放入媒介,来保证跟踪链的连续性。
Inject 和 extract 分别对应了序列化 和 反序列化。
public abstract class AbstractTextB3Formatter implements RegistryExtractorInjector<TextMap> {
public static final String TRACE_ID_KEY_HEAD = "X-B3-TraceId";
public static final String SPAN_ID_KEY_HEAD = "X-B3-SpanId";
public static final String PARENT_SPAN_ID_KEY_HEAD = "X-B3-ParentSpanId";
public static final String SAMPLED_KEY_HEAD = "X-B3-Sampled";
static final String FLAGS_KEY_HEAD = "X-B3-Flags";
static final String BAGGAGE_KEY_PREFIX = "baggage-";
static final String BAGGAGE_SYS_KEY_PREFIX = "baggage-sys-";
public SofaTracerSpanContext extract(TextMap carrier) {
if (carrier == null) {
return SofaTracerSpanContext.rootStart();
} else {
String traceId = null;
String spanId = null;
String parentId = null;
boolean sampled = false;
boolean isGetSampled = false;
Map<String, String> sysBaggage = new ConcurrentHashMap();
Map<String, String> bizBaggage = new ConcurrentHashMap();
Iterator var9 = carrier.iterator();
while(var9.hasNext()) {
Entry<String, String> entry = (Entry)var9.next();
String key = (String)entry.getKey();
if (!StringUtils.isBlank(key)) {
if (traceId == null && "X-B3-TraceId".equalsIgnoreCase(key)) {
traceId = this.decodedValue((String)entry.getValue());
}
if (spanId == null && "X-B3-SpanId".equalsIgnoreCase(key)) {
spanId = this.decodedValue((String)entry.getValue());
}
if (parentId == null && "X-B3-ParentSpanId".equalsIgnoreCase(key)) {
parentId = this.decodedValue((String)entry.getValue());
}
String keyTmp;
if (!isGetSampled && "X-B3-Sampled".equalsIgnoreCase(key)) {
keyTmp = this.decodedValue((String)entry.getValue());
if ("1".equals(keyTmp)) {
sampled = true;
} else if ("0".equals(keyTmp)) {
sampled = false;
} else {
sampled = Boolean.parseBoolean(keyTmp);
}
isGetSampled = true;
}
String valueTmp;
if (key.indexOf("baggage-sys-") == 0) {
keyTmp = StringUtils.unescapeEqualAndPercent(key).substring("baggage-sys-".length());
valueTmp = StringUtils.unescapeEqualAndPercent(this.decodedValue((String)entry.getValue()));
sysBaggage.put(keyTmp, valueTmp);
}
if (key.indexOf("baggage-") == 0) {
keyTmp = StringUtils.unescapeEqualAndPercent(key).substring("baggage-".length());
valueTmp = StringUtils.unescapeEqualAndPercent(this.decodedValue((String)entry.getValue()));
bizBaggage.put(keyTmp, valueTmp);
}
}
}
if (traceId == null) {
return SofaTracerSpanContext.rootStart();
} else {
if (spanId == null) {
spanId = "0";
}
if (parentId == null) {
parentId = "";
}
SofaTracerSpanContext sofaTracerSpanContext = new SofaTracerSpanContext(traceId, spanId, parentId, sampled);
if (sysBaggage.size() > 0) {
sofaTracerSpanContext.addSysBaggage(sysBaggage);
}
if (bizBaggage.size() > 0) {
sofaTracerSpanContext.addBizBaggage(bizBaggage);
}
return sofaTracerSpanContext;
}
}
}
public void inject(SofaTracerSpanContext spanContext, TextMap carrier) {
if (carrier != null && spanContext != null) {
carrier.put("X-B3-TraceId", this.encodedValue(spanContext.getTraceId()));
carrier.put("X-B3-SpanId", this.encodedValue(spanContext.getSpanId()));
carrier.put("X-B3-ParentSpanId", this.encodedValue(spanContext.getParentId()));
carrier.put("X-B3-SpanId", this.encodedValue(spanContext.getSpanId()));
carrier.put("X-B3-Sampled", this.encodedValue(String.valueOf(spanContext.isSampled())));
Iterator var3 = spanContext.getSysBaggage().entrySet().iterator();
Entry entry;
String key;
String value;
while(var3.hasNext()) {
entry = (Entry)var3.next();
key = "baggage-sys-" + StringUtils.escapePercentEqualAnd((String)entry.getKey());
value = this.encodedValue(StringUtils.escapePercentEqualAnd((String)entry.getValue()));
carrier.put(key, value);
}
var3 = spanContext.getBizBaggage().entrySet().iterator();
while(var3.hasNext()) {
entry = (Entry)var3.next();
key = "baggage-" + StringUtils.escapePercentEqualAnd((String)entry.getKey());
value = this.encodedValue(StringUtils.escapePercentEqualAnd((String)entry.getValue()));
carrier.put(key, value);
}
}
}
}
经过序列化之后,最后发送的Header如下,我们需要回忆下 spanContext 的概念。
上下文存储的是一些需要跨越边界的一些信息,例如:
- spanId :当前这个span的id
- traceId :这个span所属的traceId(也就是这次调用链的唯一id)。
trace_id
和span_id
用以区分Trace
中的Span
;任何 OpenTraceing 实现相关的状态(比如 trace 和 span id)都需要被一个跨进程的 Span 所联系。- baggage :其他的能过跨越多个调用单元的信息,即跨进程的 key value 对。
Baggage Items
和Span Tag
结构相同,唯一的区别是:Span Tag
只在当前Span
中存在,并不在整个trace
中传递,而Baggage Items
会随调用链传递。
可以看到,spanContext 已经被分解并且序列化到 Header 之中。
request = {InterceptingClientHttpRequest@5808}
requestFactory = {SimpleClientHttpRequestFactory@5922}
interceptors = {ArrayList@5923} size = 1
method = {HttpMethod@5924} "GET"
uri = {URI@5925} "http://localhost:8801/rest"
bufferedOutput = {ByteArrayOutputStream@5926} ""
headers = {HttpHeaders@5918} size = 6
"Accept" -> {LinkedList@5938} size = 1
"Content-Length" -> {LinkedList@5940} size = 1
"X-B3-TraceId" -> {LinkedList@5942} size = 1
key = "X-B3-TraceId"
value = {LinkedList@5942} size = 1
0 = "c0a800031598690915258100115720"
"X-B3-SpanId" -> {LinkedList@5944} size = 2
key = "X-B3-SpanId"
value = {LinkedList@5944} size = 2
0 = "0"
1 = "0"
"X-B3-ParentSpanId" -> {LinkedList@5946} size = 1
"X-B3-Sampled" -> {LinkedList@5948} size = 1
executed = false
body = {byte[0]@5810}
9.3 Report
发送的最后一步是 clientSpan.finish()。
在 Opentracing 规范中提到,Span#finish 方法是 span 生命周期的最后一个执行方法,也就意味着一个 span 跨度即将结束。那么当一个 span 即将结束时,也是当前 span 具有最完整状态的时候。所以在 SOFATracer 中,数据上报的入口就是 Span#finish 方法,其调用堆栈如下:
doReportStat:43, RestTemplateStatJsonReporter (com.sofa.alipay.tracer.plugins.rest)
reportStat:179, AbstractSofaTracerStatisticReporter (com.alipay.common.tracer.core.reporter.stat)
statisticReport:143, DiskReporterImpl (com.alipay.common.tracer.core.reporter.digest)
doReport:60, AbstractDiskReporter (com.alipay.common.tracer.core.reporter.digest)
report:51, AbstractReporter (com.alipay.common.tracer.core.reporter.facade)
reportSpan:141, SofaTracer (com.alipay.common.tracer.core)
finish:165, SofaTracerSpan (com.alipay.common.tracer.core.span)
finish:158, SofaTracerSpan (com.alipay.common.tracer.core.span)
clientReceiveTagFinish:176, AbstractTracer (com.alipay.common.tracer.core.tracer)
clientReceive:157, AbstractTracer (com.alipay.common.tracer.core.tracer)
intercept:82, RestTemplateInterceptor (com.sofa.alipay.tracer.plugins.rest.interceptor)
execute:92, InterceptingClientHttpRequest$InterceptingRequestExecution (org.springframework.http.client)
executeInternal:76, InterceptingClientHttpRequest (org.springframework.http.client)
executeInternal:48, AbstractBufferingClientHttpRequest (org.springframework.http.client)
execute:53, AbstractClientHttpRequest (org.springframework.http.client)
doExecute:734, RestTemplate (org.springframework.web.client)
execute:669, RestTemplate (org.springframework.web.client)
getForEntity:337, RestTemplate (org.springframework.web.client)
main:40, RestTemplateDemoApplication (com.alipay.sofa.tracer.examples.rest)
SOFATracer 本身提供了两种上报模式,一种是落到磁盘,另外一种是上报到zipkin。在实现细节上,SOFATracer 没有将这两种策略分开以提供独立的功能支持,而是将两种上报方式组合在了一起,并且在执行具体上报的流程中通过参数来调控是否执行具体的上报。
此过程中涉及到了三个上报点,首先是上报到 zipkin
,后面是落盘;在日志记录方面,SOFATracer
中为不同的组件均提供了独立的日志空间,除此之外,SOFATracer
在链路数据采集时提供了两种不同的日志记录模式:摘要日志和统计日志,这对于后续构建一些如故障的快速发现、服务治理等管控端提供了强大的数据支撑。。
比如 zipkin 对应上报是:
public class ZipkinSofaTracerSpanRemoteReporter implements SpanReportListener, Flushable, Closeable {
public void onSpanReport(SofaTracerSpan span) {
//convert
Span zipkinSpan = zipkinV2SpanAdapter.convertToZipkinSpan(span);
this.delegate.report(zipkinSpan);
}
}
其会调用到 zipkin2.reporter.AsyncReporter 进行具体 report。
9.4 采样计算
采样是对于整条链路来说的,也就是说从 RootSpan 被创建开始,就已经决定了当前链路数据是否会被记录了。在 SofaTracer 类中,Sapmler 实例作为成员变量存在,并且被设置为 final,也就是当构建好 SofaTracer 实例之后,采样策略就不会被改变。当 Sampler 采样器绑定到 SofaTracer 实例之后,SofaTracer 对于产生的 Span 数据的落盘行为都会依赖采样器的计算结果(针对某一条链路而言)。
0x10 服务端接收
类 SpringMvcSofaTracerFilter 完成了服务端接收相关工作。主要就是设置 SpanContext 和 Span。
public class SpringMvcSofaTracerFilter implements Filter {
private SpringMvcTracer springMvcTracer;
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
FilterChain filterChain) {
......
}
}
回忆下:在 client 端就是
- 将当前请求线程的产生的 traceId 相关信息 Inject 到 SpanContext。
- 然后通过 Fomatter 将 SpanContext序列化到Header之中。
server 端则是 从请求的 Header 中 extract 出 spanContext,来还原本次请求线程的上下文。因为上下文是和所处理的线程相关,放入 ThreadLocal中。
大致可以用如下图演示总体流程如下:
Client Span Server Span
┌──────────────────┐ ┌──────────────────┐
│ │ │ │
│ TraceContext │ Http Request Headers │ TraceContext │
│ ┌──────────────┐ │ ┌───────────────────┐ │ ┌──────────────┐ │
│ │ TraceId │ │ │ X-B3-TraceId │ │ │ TraceId │ │
│ │ │ │ │ │ │ │ │ │
│ │ ParentSpanId │ │ Inject │ X-B3-ParentSpanId │Extract │ │ ParentSpanId │ │
│ │ ├─┼─────────>│ ├────────┼>│ │ │
│ │ SpanId │ │ │ X-B3-SpanId │ │ │ SpanId │ │
│ │ │ │ │ │ │ │ │ │
│ │ Sampled │ │ │ X-B3-Sampled │ │ │ Sampled │ │
│ └──────────────┘ │ └───────────────────┘ │ └──────────────┘ │
│ │ │ │
└──────────────────┘ └──────────────────┘
这就回答了之前的问题:服务器接收到请求之后做什么?SpanContext在服务器端怎么处理?
SpringMvcSofaTracerFilter 这里有一个成员变量 SpringMvcTracer, 其是 Server Tracer,这里是逻辑所在。
public class SpringMvcTracer extends AbstractServerTracer {
private static volatile SpringMvcTracer springMvcTracer = null;
}
具体 SpringMvcSofaTracerFilter 的 doFilter 的大致逻辑如下:
-
调用 getSpanContextFromRequest 从 request 中获取 SpanContext,其中使用了 tracer.extract函数。
-
SofaTracerSpanContext spanContext = (SofaTracerSpanContext)tracer.extract(Builtin.B3_HTTP_HEADERS, new SpringMvcHeadersCarrier(headers));
-
-
调用 serverReceive 获取 Span
-
springMvcSpan = this.springMvcTracer.serverReceive(spanContext);
-
SofaTracerSpan serverSpan = sofaTraceContext.pop(); // 取出父亲Span,如果不存在,则 sofaTracerSpanContext.setSpanId(sofaTracerSpanContext.nextChildContextId()); // 设定为下一个child id
-
sofaTraceContext.push(newSpan); // 把Span放入 SpanContext
-
-
-
Span 设置各种 setTag
-
调用 this.springMvcTracer.serverSend(String.valueOf(httpStatus)); 来 结束Span。
-
结束 & report
-
this.clientReceiveTagFinish(clientSpan, resultCode);
- 设置log,resultCode,结束Client Span :clientSpan.finish();
- 调用 SofaTracer # reportSpan 来 report。这部分和 Client 代码功能类似。
- 设置log,resultCode,结束Client Span :clientSpan.finish();
-
-
恢复restore parent span
-
sofaTraceContext.push(clientSpan.getParentSofaTracerSpan());
-
-
函数代码具体如下
public class SpringMvcSofaTracerFilter implements Filter {
private SpringMvcTracer springMvcTracer;
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) {
if (this.springMvcTracer == null) {
this.springMvcTracer = SpringMvcTracer.getSpringMvcTracerSingleton();
}
SofaTracerSpan springMvcSpan = null;
long responseSize = -1L;
int httpStatus = -1;
try {
HttpServletRequest request = (HttpServletRequest)servletRequest;
HttpServletResponse response = (HttpServletResponse)servletResponse;
SofaTracerSpanContext spanContext = this.getSpanContextFromRequest(request);
springMvcSpan = this.springMvcTracer.serverReceive(spanContext);
if (StringUtils.isBlank(this.appName)) {
this.appName = SofaTracerConfiguration.getProperty("spring.application.name");
}
springMvcSpan.setOperationName(request.getRequestURL().toString());
springMvcSpan.setTag("local.app", this.appName);
springMvcSpan.setTag("request.url", request.getRequestURL().toString());
springMvcSpan.setTag("method", request.getMethod());
springMvcSpan.setTag("req.size.bytes", request.getContentLength());
SpringMvcSofaTracerFilter.ResponseWrapper responseWrapper = new SpringMvcSofaTracerFilter.ResponseWrapper(response);
filterChain.doFilter(servletRequest, responseWrapper);
httpStatus = responseWrapper.getStatus();
responseSize = (long)responseWrapper.getContentLength();
} catch (Throwable var15) {
httpStatus = 500;
throw new RuntimeException(var15);
} finally {
if (springMvcSpan != null) {
springMvcSpan.setTag("resp.size.bytes", responseSize);
this.springMvcTracer.serverSend(String.valueOf(httpStatus));
}
}
}
}
0x11 问题解答
我们在最初提出的问题,现在都有了解答。
- traceId是怎么生成的,有什么规则?答案如下:
- 在clientSend cs 这个阶段,建立Span时候,如果不存在 Parent context,则调用 createRootSpanContext 建立了 new root span context。此时会生成一个 traceId
- TraceId 是由 ip,时间戳,递增序列,进程ID等构成,具体可以参见 TraceIdGenerator 类。
- spanId是怎么生成的,有什么规则?答案如下:
- 在 Server Receive 这个阶段,如果当前线程SpanContext中没有Span,则生成一个新的 newSpan,然后调用 setSpanId 对传入的 SofaTracerSpanContext 参数进行设置新的 SpanId。
- 规则很简单,就是在之前Span ID基础上单调递增,参见 SofaTracerSpanContext #nextChildContextId。
- 客户端哪里生成的Span?答案如下:
- 在 客户端发送请求 clientSend cs 这个阶段,就是 AbstractTracer # clientSend 函数,调用 buildSpan 构建一个 SofaTracerSpan clientSpan,然后调用 start 函数建立一个 Span。
- ParentSpan 从哪儿来?答案如下:
- 在 clientSend 阶段,先从 SofaTraceContext 取出 serverSpan。如果本 client 就是 一个服务中间点(即 serverSpan 不为空),则 serverSpan 就是 parentSpan,那么需要给新span设置父亲Span。
- ChildSpan由ParentSpan创建,那么什么时候创建?答案如下:
- 接上面回答,如果存在 ParentSpan,则调用
clientSpan = (SofaTracerSpan)this.sofaTracer.buildSpan(operationName).asChildOf(serverSpan).start();
得到本身的 client Span。 - 即如果存在active span ,若存在则生成CHILD_OF关系的上下文, 如果不存在则createNewContext;
- 接上面回答,如果存在 ParentSpan,则调用
- Trace信息怎么传递?答案如下:
- OpenTracing之中是通过SpanContext来传递Trace信息。
- SpanContext存储的是一些需要跨越边界的一些信息,比如trace Id,span id,Baggage。这些信息会不同组件根据自己的特点序列化进行传递,比如序列化到 http header 之中再进行传递。
- 然后通过这个 SpanContext 所携带的信息将当前节点关联到整个 Tracer 链路中去
- 服务器接收到请求之后做什么?答案如下:
- server 端则是 从请求的 Header 中 extract 出 spanContext,来还原本次请求线程的上下文。因为上下文是和所处理的线程相关,放入 ThreadLocal中。
- SpanContext在服务器端怎么处理?答案见上面回答。
- 链路信息如何搜集?答案如下:
- 采样是对于整条链路来说的,也就是说从 RootSpan 被创建开始,就已经决定了当前链路数据是否会被记录了。
- 在 SofaTracer 类中,Sapmler 实例作为成员变量存在,并且被设置为 final,也就是当构建好 SofaTracer 实例之后,采样策略就不会被改变。当 Sampler 采样器绑定到 SofaTracer 实例之后,SofaTracer 对于产生的 Span 数据的落盘行为都会依赖采样器的计算结果(针对某一条链路而言)。
0xEE 个人信息
★★★★★★关于生活和技术的思考★★★★★★
微信公众账号:罗西的思考
如果您想及时得到个人撰写文章的消息推送,或者想看看个人推荐的技术资料,敬请关注。
0xFF 参考
开放分布式追踪(OpenTracing)入门与 Jaeger 实现
OpenTracing Java Library教程(3)——跨服务传递SpanContext
OpenTracing Java Library教程(1)——trace和span入门
蚂蚁金服分布式链路跟踪组件 SOFATracer 总览|剖析
蚂蚁金服开源分布式链路跟踪组件 SOFATracer 链路透传原理与SLF4J MDC 的扩展能力剖析
蚂蚁金服开源分布式链路跟踪组件 SOFATracer 采样策略和源码剖析
The OpenTracing Semantic Specification
蚂蚁金服分布式链路跟踪组件 SOFATracer 数据上报机制和源码剖析