Dubbo RPC调用链追踪接入Jaeger

图片来自pixabay.com的NickRivers会员

1. 介绍

微服务架构中,分布式追踪(distributed tracing)是一个关键的基础功能,通过分布式追踪技术,我们可以深入分析一次请求调用所执行的路径、性能消耗,帮助定位性能瓶颈点,透明化服务之间上下游网络调用关系,帮助优化服务层次依赖问题。

Dubbo RPC是业界常用的一个开源RPC框架,而Jaeger也是业界流行的一个开源分布式追踪组件,本文将介绍如何把Dubbo RPC调用链接入Jaeger追踪,文末将结合分布式追踪技术,对常见的问题案例进行分析和给出优化方案。

2. 追踪数据模型和dubbo埋点实现原理

Jaeger的追踪实现遵循了OpenTracing语义规范,其数据模型是一个trace包含多个span构成的有向无环图,如下是一个典型的trace样例,

        [Span A]  ←←←(the root span)
            |
     +------+------+
     |             |
 [Span B]      [Span C] ←←←(Span C is a `ChildOf` Span A)
     |             |
 [Span D]      +---+-------+
               |           |
           [Span E]    [Span F] >>> [Span G] >>> [Span H]
                                       ↑
                                       ↑
                                       ↑
                         (Span G `FollowsFrom` Span F)

更多OpenTracing术语的定义和详细介绍请见这里,本文将不赘述。

若要将Dubbo RPC接入Jaeger追踪,从consumer到provider一次调用为最基本的追踪单元,整个dubbo的调用追踪可以视为该基本单元的规模扩展,因此该基本单元的追踪埋点实现是Dubbo RPC接入Jaeger追踪的核心。

下图为对这个基本追踪单元的埋点实现描述,

3. 代码实现

下面为简化版的RpcConsumerFilter实现代码,

@Activate(group = {Constants.CONSUMER})
public class RpcConsumerFilter implements Filter {

    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        // 获取context并创建span
        RpcContext rpcContext = RpcContext.getContext();
        Span span = DubboTraceUtil.extractTraceFromLocalCtx(rpcContext);

        Result result = null;    
        try {
            // 将span context加载到dubbo rpc remote context中
            DubboTraceUtil.attachTraceToRemoteCtx(span, rpcContext);

            // 执行dubbo rpc调用
            result = invoker.invoke(invocation);
        } catch (RpcException rpcException) {
            span.setTag("error", "1");
            throw rpcException;
        } finally {
            span.finish();
        }

        return result;
    }
}

下面为简化版的RpcProviderFilter实现代码,

<br>@Activate(group = {Constants.PROVIDER})
public class RpcProviderFilter implements Filter {

    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        // 获取context并创建span
        RpcContext rpcContext = RpcContext.getContext();
        Span span = DubboTraceUtil.extractTraceFromRemoteCtx(rpcContext);

        Result result = null;
        try {
            // 将span context加载到dubbo rpc local context中
            DubboTraceUtil.attachTraceToLocalCtx(span, rpcContext);

            // 执行dubbo rpc调用
            result = invoker.invoke(invocation);
        } catch (RpcException rpcException) {
            span.setTag("error", "1");
            throw rpcException;
        } finally {
            span.finish();
        }

        return result;
    }

完整的代码演示样例请见这里

4. 埋点上报的span数据格式

数据字段 字段类型 是否必要字段 说明
traceId 字符串 Y 当前span所属的一次调用跟踪ID
spanID 字符串 Y 当前span的ID
parentSpanID 字符串 Y 父spanID,串联上下游span
startTime 长整型 Y 当前span的开始时间
duration 长整型 Y 当前span的时长
tags.span.kind 字符串 Y 当前调用方类型:server/client
tags.sampler.type 字符串 抽样器类型
tags.sampler.param 字符串 抽样比例,取值范围:0-1
tags.peer.hostname 字符串 对调方的主机名/IP
tags.peer.port 短整型 对调方的端口
operationName 字符串 Y dubbo接口名
tags.arguments 字符串 Y dubbo接口调用的参数
tags.error 字符串 Y* 当前dubbo调用出现异常或错误,值为“1”,注:该字段只有在有错误异常情况下为必要字段
tags.error.code 字符串 dubbo的异常码
tags.error.message 字符串 dubbo的异常消息

如下分别为来自dubbo consumer/provider的span上报数据样例,

// 消费方span
{
    "traceID": "5eb2e61e850be731",
    "spanID": "5eb2e61e850be731",
    "parentSpanID": "0",
    "startTime": 1592835612981000,
    "duration": 9781,
    "operationName": "com.pphh.demo.common.service.SimpleService:save",
    "tags.span.kind": "client",
    "tags.sampler.type": "probabilistic",
    "tags.peer.service": "com.pphh.demo.common.service.SimpleService",
    "tags.sampler.param": "1.0",
    "tags.arguments": "[{\"userName\":\"michael\"}]",
    "tags.peer.hostname": "192.168.1.105",
    "tags.peer.port": "29001"
}

// 提供方span
{
    "traceID": "5eb2e61e850be731",
    "spanID": "c7aaec2ab0a20823",
    "parentSpanID": "5eb2e61e850be731",
    "startTime": 1592835612986000,
    "duration": 1880,
    "operationName": "com.pphh.demo.common.service.SimpleService:save",
    "tags.span.kind": "server",
    "tags.peer.service": "com.pphh.demo.common.service.SimpleService",
    "tags.arguments": "[{\"userName\":\"michael\"}]",
    "tags.peer.hostname": "192.168.1.105",
    "tags.peer.port": "49707"
}

5. Dubbo RPC分布式追踪大图

将所有dubbo rpc调用的上报span数据按应用聚合,可以看到整个Dubbo RPC分布式追踪大图,

6. 通过分布式追踪发现的常见问题案例

6.1 应用服务依赖:多次重复rpc调用/上下依赖倒置

通过分布式追踪大图可以清晰地看到各个应用的调用关系,应避免不必要的重复rpc调用,禁止底层应用调用上层应用。

6.2 大流量调用

分布式追踪大图中,调用线条的粗细描述了各个调用关系的流量大小,

对于大流量调用,需要评估其流量的合理性,减少不必要的RPC调用开销,可以考虑从如下几个方面进行优化,

  1. 循环多次调用转为单次批量调用。
  2. 若是读操作,接受数据的时延,可以考虑使用local cache,在指定的N秒内,直接读取local cache。
  3. 应用服务拆分,隔离因大流量调用而产生的CPU/内存/网络等资源竞争。

6.3 性能问题 – 重复调用转为单次批量调用

该问题典型场景为在一个循环中重复执行RPC调用,其调用性能取决于循环的次数,比如获取100个用户信息,循环100次获取用户信息,这个问题最好的办法是将循环调用转为单次批量调用。

下图为一个重复调用的追踪图,

6.4 异常调用定位 – 非法参数

对于异常调用,可以查看异常信息,并结合调用的参数,定位问题,比如用户名非法的异常,可以查看调用参数,用户名是否包含非法字符。

6.5 网络抖动

这种问题常见的现象是,整个调用链中,上游span耗时非常长,下游span耗时非常短,见下图,

可以看到,上下游都被执行,但是上下游衔接耗时很长,其问题的原因主要出现在上下游衔接。上图中的问题后来定位到网络抖动导致。

7. 参考资料

  1. Jaeger分布式追踪
  2. Jaeger开源代码
  3. OpenTracing语义规范

中小企业web应用的架构演化

图片来自pixabay.com的hansbenn会员

本文从简单的单体应用架构说起,逐一介绍企业web应用在其演化进程之路上的各种典型架构,并对于各个架构阶段所面临的挑战进行讨论,最后讨论中小企业应用的终极架构目标:同城主备、异地双活。

1. 单体应用架构

在企业的创世阶段,应用开发是一个从0到1的阶段,选择一个简单架构,不仅可以快速搭建起业务,验证业务模式的正确性,还可以方便业务的快速频繁迭代。因此,很多创业企业都是从单体应用架构开始,这个应用架构的典型特点是:单应用+单数据库。

其架构示意图如下,

虽然是简单架构,麻雀虽小但五脏俱全,在这个阶段,选择合适的前后端技术栈和web框架非常重要,尽量为未来发展创造条件。

1.1 面临的挑战

业务的快速变化,应用迭代频繁,这是初期企业应用面临最大的挑战,这个时期,数据模型的设计甚至比技术栈的选择更加困难。很多时候,一旦业务发展起来了,再想对历史数据推倒重来,会是非常困难的一件事情。

为了能够面对业务的快速迭代,数据模型和应用尽量小范围、内聚、低耦合,在应用内尽量实现无状态的数据流转(比如:数据的事务操作),为未来的调整扩展留出空间、做好打算。

2. 高可用、高可靠、高性能的集群架构

当公司的业务发展起来后,对应用高可用、高可靠、高性能的要求随之而来,于是每一个应用服务部署多个实例,将其挂载在SLB下,通过SLB实现流量的均衡负载,这种集群部署方式其架构简单、技术成熟,在中小企业应用中非常流行。

为了保证数据的安全,一般会搭建起数据库的主从架构,主库用于读写,从库用于读。

2.1 架构示意图

一个集群架构的示意图如下,

图中显示了数据库的主从架构,更多的数据库架构设计可以选择,比如主主、一主多从、双主多从等。

2.2 面临的挑战

集群架构简单,部署起来也不复杂,刚开始很容易形成怎么快怎么做的问题,随着部署的应用数增多,机器环境的管理、应用的灰度部署和快速回滚、问题的定位等等,都会让部署问题显得特别突出。

这个时候,Jenkins等持续集成和持续交付工具的使用,配与一套行之有效的上线规范,将有利于上述问题的解决。

3. 微服务化架构

当公司的业务规模发展壮大到一定程度,应用数达到成百上千时,整个系统的复杂度将成倍增长,如何管控系统的复杂度是需要解决的问题。

为了解决这个问题,各种业界基础架构组件涌现登场,这是其发挥神通广大的时候,这些基础技术组件若按其实现目的有如下三种归类,

  1. 组件抽象(即各种中间件):缓存、消息、数据库访问、网关、隔离熔断、日志
  2. 系统的复杂度管理:配置、灰度发布、日志、监控、告警
  3. 架构解耦:RPC服务组件(RPC框架、服务注册和发现等)

拍拍贷技术架构体系

拍拍贷公司成立于2007年6月,其技术体系最早基于微软.Net平台搭建,到了2016年,开始慢慢转向了Java平台,微服务开发也选择了当前业界流行的Spring Boot/Spring Cloud体系。在搭建微服务平台的过程中,拍拍贷基础架构团队并未全盘接纳Spring Cloud技术组件,更多是取其精华,兼并选择业界生产部署验证过成熟的组件。到现在2018年,微服务平台的搭建已过去快2年,整个技术体系已渐渐成型。

本文对当前拍拍贷的逻辑架构体系和微服务架构设计进行简单介绍,以供参考。

1. 逻辑架构体系

拍拍贷的逻辑架构体系如下图所示,

从上到下,分别有,

  • UI用户界面:提供Web应用和前端页面、移动应用、H5页面,以便于用户各个渠道的访问
  • Edge接入层:这一层承接外部所有用户的访问流量,负载均衡,并进行HTTPS的卸载,校验用户请求的合法性。同时,三大网关(API、移动、H5)还承接内部应用服务域名的调用访问流量。
  • Service服务层:主要的业务逻辑应用服务层
  • Platform平台层:包括各大Java服务中间件,微服务开发框架和平台,支撑整个业务服务应用的开发和运营。
  • Data Storage数据存储:数据存储层,主要包括三种:关系型数据库的MySql/MS Sql Server(目前逐渐从MS Sql Server往MySql迁移),缓存Redis,大数据的HBase。

2. 微服务架构设计

拍拍贷的微服务架构基于Spring Cloud,但很多组件选择了业界在生产环境已验证过、比较成熟的技术方案,比如配置中心、调用链监控、监控告警等。

整个微服务架构设计如下,

主要组件模块的功能如下,

  • 网关Zuul 1.0:提供服务路由、负载均衡、访问安全控制、熔断限流
  • 注册中心Eureka:服务注册和发现
  • 配置中心Ctrip Apollo:应用环境配置
  • 微服务Spring Boot:应用服务开发
  • 发布系统:通过网关实现应用服务的上下线、灰度发布、蓝绿发布
  • 调用链监控Dianping CAT:应用服务的监控埋点
  • 数据收集器Kafka:进行ELK的日志分析,收集运营Metrics到KairosDB
  • 运营指标KairosDB:存储相关的运营指标,包括业务、系统和运维指标,比如用户的访问量、消息的消费吞吐量、zabbix的运维监控数据等。
  • 运营看板Grafana:对接ELK/KairosDB/Zabbix,展现数据看板
  • 告警Zalando Zmon:健康和熔断告警

该图可以和Spring Cloud的微服务架构图相比较,可以看到拍拍贷在搭建微服务架构所做的改变,

  1. 添加发布系统,对接注册中心和网关,实现微服务实例的无缝上下线,实现灰度发布要求,为微服务的持续交付打下基础
  2. 添加ELK日志分析,完善监控告警,通过Grafana提供运营看板,确保微服务运营的快速、简单和稳定。在Spring Cloud技术体系中,其运营和监控是短板,ELK和Grafana作为业界流行的日志分析和运营看板组件,具有高可扩展性。
  3. 内部应用服务调用并未选择直连模式,而是通过服务域名走网关,简单可靠,其适合当前拍拍贷的业务运营需求。