上一篇实现了服务的负载均衡,本篇带来链路追踪。
关于链路追踪,大部分都是参考了谷歌的dapper论文:https://bigbully.github.io/Dapper-translation/。
通过论文总结,其中span的核心元素为:traceId,name,spanId,parentSpanId,其他则根据自身业务需要来定义即可。
链路追踪核心原理为通过一个全局的traceId作为依据,在调用服务时为每个服务分配spanId,并记录操作名name,在调用RPC服务时将带有这些属性的span随同请求一起传递,并在关键节点将span数据通过尽量小影响原服务性能的方式传递给我们自己的trace采集服务,采集服务存入数据并通过traceId以及spanId和parentSpanId的关系梳理出调用链并做图形化展示。这里给服务器传递span有很多中模式,其中包括:直接通过RPC服务调用,写入本地磁盘通过另外的进程读取磁盘数据写入远程服务器,写入缓存传输,发送消息等等方式,可以根据自己的需要选择,原则上是尽量少的影响服务本身性能。
本篇只带来客户端采集span的过程,trace服务器采集和分析展示链路的过程这里省略。
这里还是贴出github代码地址,想直接看代码的可以直接下载运行:https://github.com/whiteBX/wrpc
首先来看我这里的span定义:
public class Span {
/**
* 全局唯一id
*/
String traceId;
/**
* 操作名--此处取方法名
*/
String operationName;
/**
* 当前spanId
*/
String spanId;
/**
* 调用方spanId
*/
String parentSpanId;
/**
* appCode
*/
String appCode;
/**
* 当前机器ip
*/
String localIp;
/**
* 目标机器ip
*/
String remoteIp;
/**
* 时间戳,用于记录访问时间
*/
long timestamp;
}
上面是一些我定义的span属性,当然各位可以加一些自己需要用到的,比如exception记录等等,不过原则上这里span要尽量小,如果定义的过大会影响每次请求的传输数据量,对我们的服务性能造成影响。
在我们的comsumer中修改动态代理类,在发起远程调用之前,处理span相关内容:
public <T> T getBean(final Class<T> clazz, final String appCode) {
return (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class[] { clazz }, new InvocationHandler() {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 获取服务器地址
String serverHost = getServer(appCode);
Span span = SpanBuilder.buildNewSpan(SpanHolder.get(), method.getName(), serverHost, appCode);
TODO: 2018/10/25 新启线程发起rpc调用远程链路追踪服务记录追踪日志 此处打日志代替
System.out.println("链路追踪,调用远程服务:" + JSON.toJSONString(span));
BaseRequestBO baseRequestBO = buildBaseBO(span, clazz.getName(), method, JSON.toJSONString(args[0]));
return JSON.parseObject(call(serverHost, JSON.toJSONString(baseRequestBO)), method.getReturnType());
}
});
}
这里注释写的开启新线程调用rpc的方式传输数据,各位可以看需要自行修改,比如写入磁盘通过其他进程读取传输的性能往往会高于这种方式。
这里来看一下上面代码用到的SpanBuilder:
public class SpanBuilder {
/**
* 构造span
* @param parentSpan
* @return
* @throws UnknownHostException
*/
public static Span buildNewSpan(Span parentSpan, String operationName, String serverIp, String appCode) throws UnknownHostException {
Span span = new Span();
span.setLocalIp(InetAddress.getLocalHost().getHostAddress());
if (parentSpan == null) {
span.setTraceId(ShortUUIDUtils.nextId());
span.setParentSpanId("0");
} else {
span.setTraceId(parentSpan.getTraceId());
span.setParentSpanId(parentSpan.getSpanId());
}
span.setTimestamp(System.currentTimeMillis());
span.setOperationName(operationName);
span.setRemoteIp(serverIp);
span.setAppCode(appCode);
span.setSpanId(ShortUUIDUtils.nextId());
return span;
}
/**
* 构建新的appCpde的Span
* @param span
* @param appCode
* @return
*/
public static Span rebuildSpan(Span span, String appCode) {
Span newSpan = copy(span);
newSpan.setAppCode(appCode);
return newSpan;
}
/**
* 拷贝
* @param source
* @return
*/
public static Span copy(Span source) {
if (source == null) {
return null;
}
Span span = new Span();
span.setTraceId(source.getTraceId());
span.setOperationName(source.getOperationName());
span.setSpanId(source.getSpanId());
span.setParentSpanId(source.getParentSpanId());
span.setAppCode(source.getAppCode());
span.setLocalIp(source.getLocalIp());
span.setRemoteIp(source.getRemoteIp());
span.setTimestamp(source.getTimestamp());
return span;
}
}
这里其实就是简单的构造span,其中主要是traceId和spanId的生成,我这里用到了一个短码的生成器,这里就不贴代码了,可以自行去github上拉代码来看,或者直接用uuid也是可以的,这里只是需要保证不重复的基础上尽量短一点。
接下来是改造provider端的接收数据处理方法:
public void channelRead(ChannelHandlerContext ctx, Object msg) {
System.out.println("服务端收到请求:" + msg);
try {
// 解析出 类名+方法名+请求参数类型(方法签名)
BaseRequestBO baseRequestBO = JSON.parseObject(msg.toString(), BaseRequestBO.class);
// 放入span
SpanHolder.put(baseRequestBO.getSpan());
// 获取注册的服务
Object object = ProviderBeanHolder.getBean(baseRequestBO.getClazzName());
if (object == null) {
System.out.println("服务类未注册:" + baseRequestBO.getClazzName());
}
// 通过反射调用服务
Class paramType = Class.forName(baseRequestBO.getParamTypeName());
Method method = object.getClass().getDeclaredMethod(baseRequestBO.getMethodName(), paramType);
Object response = method.invoke(object, JSON.parseObject(baseRequestBO.getData(), paramType));
// 请求响应
ctx.writeAndFlush(JSON.toJSONString(response));
Span span = SpanBuilder.rebuildSpan(baseRequestBO.getSpan(), ProviderConstant.APP_CODE);
TODO: 2018/10/25 新启线程发起rpc调用远程链路追踪服务记录追踪日志 此处打日志代替
System.out.println("链路追踪,远程服务响应:" + JSON.toJSONString(span));
} catch (Exception e) {
System.out.println("服务异常" + e);
}
}
这里获取到传递来的span,之后放入本地线程变量中记录,在这个服务处理中继续调用别的provider时,comsumer代码中可以取到这一个span,再生成新的span时,这个span的traceId会被沿用,spanId则会被设置成为下一个span的parentSpanId,这样一级一级的传递就形成了调用链。
到这里主要的代码其实就完成了,大家可以直接去github拉代码来运行。这里补充几点:
- 由于span在服务内部通过本地线程变量传递,会造成服务中起新线程时链路会丢失,这里可以通过其他方式来处理,比如存入第三方缓存等其他方式来解决.
- span的采集节点,这里采用了在consumer发起调用前和provider处理完成后的两个节点进行采集,是综合考虑请求成功/超时/异常后的一种方案。各位也可以在别的节点进行采集,比如consumer收到响应、provider收到请求等等节点,或者都收集,然后在trace服务端自行分化处理。