【RPC】一步一步实现基于netty+zookeeper的RPC框架(四)

上一篇实现了服务的负载均衡,本篇带来链路追踪。

关于链路追踪,大部分都是参考了谷歌的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拉代码来运行。这里补充几点:

  1. 由于span在服务内部通过本地线程变量传递,会造成服务中起新线程时链路会丢失,这里可以通过其他方式来处理,比如存入第三方缓存等其他方式来解决.
  2. span的采集节点,这里采用了在consumer发起调用前和provider处理完成后的两个节点进行采集,是综合考虑请求成功/超时/异常后的一种方案。各位也可以在别的节点进行采集,比如consumer收到响应、provider收到请求等等节点,或者都收集,然后在trace服务端自行分化处理。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值