分布式服务架构-第五章 基于调用链的服务治理系统的设计与实现

1.APM系统简介

1.1优秀的开源APM系统

开源的APM系统的实现

  • Pinpoint:在互联网公司得到广泛应用
  • Zipkin:是Twitter的一个开源项目,原本用于收集Twitter各个服务上的监控数据,并提供查询接口
  • CAT:一款国产开源的APM系统
1.1.1 Pinpoint

基于Java语言的APM工具,用于大规模分布式服务化系统或实施了微服务架构的系统。

特性

  • 安装的采集端代理组件对原有的服务代码无侵入
  • 对性能的影响较小,只增加约3%的资源利用率
  • 根据请求的流量自动生成微服务调用的拓扑结构
  • 通过可视化结构显示网络微服务调用的关系,下钻(指点击进入子页面)可显示该服务的详细信息页面
  • 实时监控活动线程,并通过图形的形式展示
  • 可视化地显示请求超时发生的位置,帮助快速定位问题
  • 可以收集和显示CPU,内存,垃圾收集,请求吞吐量和JVM运行情况等
  • 使用异步线程推送和UDP协议减少对程序处理性能的影响
1.1.2 Zipkin

一个分布式服务的调用链跟踪系统,能够收集服务调用的时序数据,解决在微服务架构中定位超时等性能问题。

通过在应用程序中挂载字节码增强库来将实时数据汇报给Zipkin,Zipkin UI可以通过图形的方式显示调用链中有多少请求经过系统的某个节点,并构造和显示系统的拓扑结构。

如果在定位微服务系统间交互的超时问题,则可以根据服务节点,调用链长度,时间戳等信息过滤和查找想找到的调用链,一点找到一个调用链,则能清晰地看到调用链是否有问题及问题在哪里。

1.1.3 CAT

美团点评开源的一款实时的应用和性能监控系统。系统原型和理念来源于eBay的CAL系统,并增强了CAL系统的核心模型,添加了丰富的报表功能。

聚焦于对Java应用的全链路监控方面,开源实时处理有时间价值的日志数据,并全量采集应用产生的数据,具有高可用,容错性,高吞吐量和可横向扩展等特性。

CAT不保证消息的可靠收集,允许发送的消息丢失,是在设计过程中对准确性与稳定性的一个权衡,CAT服务端号称可以做到4个9可靠性。

1.2国内商业APM产品的介绍

  1. 听云
  2. 博睿
  3. OneAPM
  4. 云智慧

2.调用链跟踪的原理

2.1分布式系统的远程调用过程

用户请求调前端服务,前端服务分发请求到内部各个服务,每次调用涉及跨系统一次请求和一次响应。

分布式服务架构-第五章 基于调用链的服务治理系统的设计与实现_第1张图片

树形结构:

  • 树节点是整个架构的基本单元,每个节点是一个独立的服务节点,
  • 每个节点对应一个Span,节点之间连线表示Span和它的父节点Span之间的关系,具体表现为一次调用请求和响应的调用关系(组合数据叫调用信息)

重点关注两个服务之间的通信

  • 通过增加应用层的标记来对服务化中的请求和响应建立联系
  • 例如:通过http协议头携带标记信息,标记信息包括标识调用链的唯一流水ID-TrackID,调用层次SpanId,顺序ParentSpanId

分布式服务架构-第五章 基于调用链的服务治理系统的设计与实现_第2张图片

一次调用需要保存的调用信息:

一次远程调用过程4个阶段,每种类型的远程调用信息包含:调用端或者被调用端的IP,系统ID,本次请求的TraceID,SpanID,ParentSpanID,时间戳,调用的方法名称及远程调用信息的类型等

  • 调用端发送请求的调用信息
  • 被调用端接收请求的调用信息
  • 被调用端发送响应的调用信息
  • 调用端接收响应的

第3 4阶段进一步分为成功响应和异常响应,为主子线程间调用增加了一种调用信息类型

分布式服务架构-第五章 基于调用链的服务治理系统的设计与实现_第3张图片

2.2TraceID

Vesta是一款原创的多场景的互联网发号器,此发号器可以作为全局唯一的流水号,即TrackID

  • 前端接收用户的请求后,为用户请求分配一个TraceID
  • 内部调用时,会通过应用层的协议将TraceID传递到下层服务,直到整个调用链的每个节点都拥有了TraceID
  • 可以使用这个唯一的TraceID迅速找到系统间发生过的所有交互请求和响应并定位问题

分布式服务架构-第五章 基于调用链的服务治理系统的设计与实现_第4张图片

2.3SpanID

  • SpanID包含SpanID(当前为一个调用节点)和ParentSpanID(这个调用节点的父节点)
  • 组合在一起表示一个树形的调用关系
  • 记录了一次调用的节点信息,

分布式服务架构-第五章 基于调用链的服务治理系统的设计与实现_第5张图片

当系统故障:

  • 通过TraceID把一整条调用链所有调用信息收集到一个集合,包括请求响应
  • 通过SpanID 和ParentSpanID恢复树形的调用树,ParentSpanID为-1的节点为调用树的根节点,请求源头
  • 识别调用链中出错或超时的节点,做出标记
  • 把恢复的调用树和出错的节点信息通过某种图形显示到UI界面上

SpanID是一个64位的整型值,有多种策略产生SpanID

  • 使用随机数产生SpanID,理论上可重复,但可能性微乎其微,本地生成随机数的效率会高于其他方法
  • 使用分布式的全局唯一的流水号生成方式,可参考互联网发号器Vesta
  • 每个SpanID包含所有父亲及前辈节点的SpanID,使用 远点符合作为分隔符,缺点:当一个请求调用链太多节点层次时,包含冗余信息,导致服务间性能下降。

分布式服务架构-第五章 基于调用链的服务治理系统的设计与实现_第6张图片

2.4业务链

一个业务流程的完成由用户的多次请求组成,这些请求之间是有关联的,在串联调用链之后,会根据业务的属性,将不同的调用链聚合在一起形成业务链。

需要在多次请求之间建立联系,可以通过业务系统的订单号来串联业务链,调用链是一个简单的树形结构,而业务链式一个森林结构。

分布式服务架构-第五章 基于调用链的服务治理系统的设计与实现_第7张图片

3.调用链跟踪系统的设计与实现

3.1整体架构

  • 调用链跟踪系统由采集器,处理器和分布式存储系统组成
  • 经过处理后的调用数据会在调用链展示系统中对外提供查看和查询功能
    • 采集器:负责把业务系统的远程服务调用信息从业务系统中传递给处理器
    • 处理器:负责从业务系统的采集器中接收服务调用信息并聚合调用链,将其存储在分布式数据存储中,以及对调用链进行分析,并输出给监控和报警系统
    • 分布式存储系统:存储海量的调用链数据,并支持灵活的查询和搜索功能
    • 调用链展示系统:支持查询调用链,业务链功能

3.2TraceID 和SpanID在服务间的传递

主要解决Java应用内传递、服务间传递、多线程传递、应用与消息队列传递、缓存和数据库间传递TraceID和SpanID的问题。

分布式服务架构-第五章 基于调用链的服务治理系统的设计与实现_第8张图片

1.Java进程内传递

在Java应用系统内部通过ThreadLocal传递TraceID 和SpanID,这样在需要调用外部系统时,TraceID和SpanID是可以得到的

2.服务间传递

需要在应用层的网络通信协议中传递TraceID和SpanID,

  • 使用RESTful风格的API服务,HTTP头是传递TraceID 和 SpanID的最佳位置
  • 使用RPC远程调用,需要在RPC的序列化协议上增加定制化字段,将TraceID和SpanID从调用方传递给被调用方

3.主子线程间传递

服务化架构里,非核心链路上的逻辑抽象成异步处理,通常会在异步线程中执行,跟踪这部分调用链,在创建新的线程或子线程时,将TraceID和SpanID一并传递过去,并放在子线程的TreadLocal中,可以把这个过程封装在一个可以独立使用的线程池的AsyncThreadPoolExecutor子类中。

伪代码

public class AsyncThreadPoolExecutor extends ThreadPoolExecutor
{
    public AsyncThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
    }

    public AsyncThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory);
    }

    public AsyncThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler);
    }

    public AsyncThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
    }

    @Override
    public void execute(Runnable runnable){
        AsyncTaskHolder asyncTaskHolder = new AsyncTaskHolder(runnable);
        try{
            //首先处理TraceID 和 SpanID
            asyncTaskHolder.beforeInvoke();
        } catch (Exception e) {
            //打印异常日志,吃掉异常,不能让新增功能影响原有功能
        }
        super.execute(asyncTaskHolder);
    }
}


public abstract class AsyncTaskHolder {

    //用于发送调用信息的Kakfa客户端
    private CallInfoSender callInfoSender = new CallInfoKafkaSender(...);
    //当前线程的TraceID SpanID 和parentSpanID
    private long traceID = -1;
    private long spanID = -1;
    private long parentSpanID=-1;
    //临时保存前面线程TraceID 和 SpanID
    private long prevTraceID = -1;
    private long preSpanID=-1;
    private long prevParentSpanID = -1;
    //原生异步线程任务
    private volatile Runnable task;
    public AsyncTaskHolder(Runnable runnable){
        this.task = runnable;
    }
    @Override
    final public void run(){
        try{
            //进入异步线程处理,将SpanID进一步传递下去,并保存原来的SpanID
            beforeRun();
        } catch (Exception e) {
            //打印异常日志
        }
        try{
            task.run();
        }finally {
            try{
                //退出异常线程处理,恢复原来的SpanID
                afterRun()
            } catch (Exception e) {
                //打印日志
            }
        }
    }
    public void beforeSubmit(){
        //将当前线程的TraceID SpanID 和parentSpanID传递给即将要执行的任务对象
        traceID = getTraceID();
        spanID = getSpanID();
        parentSpanID = getParentSpanID();
    }
    public void beforeRun(){
        //在子线程执行任务前,在任务对象中保存父线程的TraceID SpanID和parentSpanID
        prevTraceID = traceID;
        preSpanID = spanID;
        prevParentSpanID = parentSpanID;
        //在子线程执行任务前,生成新的TraceID,spanID 和parentSpanID 其中traceID不变,parentSpanID为父线程的spanID,生成新的spanID
        setTraceID(traceID);
        setSpanID(getNewSpanID(spanID));
        setParentSpanID(spanID);
        long ip = getHostIp();
        callInfoSender.send(Thread.currentThread().getId(), RPCPhase.SIB, traceID,spanID,parentSpanID,ip);
    }
    public void afterRun(){
        traceID = prevTraceID;
        spanID = preSpanID;
        parentSpanID = prevParentSpanID;
    }
    public Runnable getTask(){
        return task;
    }
    public void setTask(Runnable task){
        this.task = task;
    }
}

4.消息队列的传递

  • 通过更改消息队列实现的底层协议,将TraceID 和SpanID在底层透明地传递,这样不需要应用层有感知,但是更改底层消息队列协议实现复杂,消息队列产品多样化
  • 在应用层报文上增加附加字段,应用层在发送消息时,手工将TraceID和SpanID通过报文传递,这种方案侵入了系统,但实现起来比较简单,快捷
  • 在第二种方案基础上,在消息队列客户端的库上做定制化,每次发送消息时将TraceID 和SpanID 增加到消息报文中,在消息队列的处理机的库中先对报文进行解析,再将业务报文传递给应用层处理,即不用更改底层协议,也不用开发者在写业务逻辑代码时,手工赋值这两个字段,避免由于人为疏忽造成TraceID 和SpanID的缺失。

5缓存、数据库访问

  • 对缓存和数据库服务二次开发,通常改造缓存和数据库服务于客户端库的网络通信协议,将TraceID 和SpanID通过网络通信协议进行透明地传输,技术难度大,风险高
  • 封装缓存、数据库的客户端,将TraceID 和 SpanID 与访问的数据进行管理,实现简单。

3.3采集器的设计和实现

职责:解决采集TraceID 和SpanID 数据及推送数据的问题

3.3.1采集器的实现方法

4种方法:应用层主动推送,AOP推送,JavaAgent字节码增强,大数据日志推送

1.应用层主动推送

应用层通过编写代码,把相关数据推送到调用链处理器

  • 优点:实现简单,快速
  • 缺点:侵入了业务代码

2. AOP推送

在应用的业务层代码中使用AOP拦截目标服务调用,把请求和响应的调用信息收集后,推送到调用链处理器。

3. JavaAgent 字节码增强

构建一个独立应用程序的代理程序,用来检测和协助运行在JVM上的程序,甚至能替换和修改某些类的定义,增加定制化的监控代码,实现更为灵活的运行时虚拟机监控和Java字节码操作。

4. 代理推送

与大数据日志系统的建设方式类似,日志文件通过应用程序打印相关调用日志,随着日志一起推送到日志中心,再从日志中心提取相关的调用日志,组成调用链。

将日志推送到大数据日志中心一般采用Fluentd,Flume,Scrib,Logstash等框架来实现。

3.3.2推送的实现方法

1.Kafka消息队列

在采集器和处理器中间增加了Kafka消息队列,对于高峰时的日志处理起到了有效的削峰作用,可以把产生的大量日志缓冲到Kafka消息队列的Broker服务器上,处理器可以后续慢慢处理,不影响业务。

分布式服务架构-第五章 基于调用链的服务治理系统的设计与实现_第9张图片

2.UDP推送

UDP传输数据效率比TCP或者任何上层协议的效率都要高得多

Kafka 和UDP 都是用异步线程池发送调用信息,异步线程池与业务线程之间必须采用有界队列,防止收集器出现问题,出现数据积压后导致应用内存耗光而产生OutOfMemoryError问题。

分布式服务架构-第五章 基于调用链的服务治理系统的设计与实现_第10张图片

3.4处理器的设计与实现

调用链处理器对调用信息进行组合和聚合,并进行一定的附加处理,然后存储到大数据存储系统中,或者通过Spark等流式处理后发送给监控和报警系统。

3.4.1处理器的处理逻辑

对收集的信息进行处理,使用Java开发一个处理器,对得到的日志聚合,存入调用链的大数据存储系统中。

生产实践中,在处理器中除了对调用链进行聚合,还需要从调用链中发现调用的问题,例如异常,超时等。需使用类似Storm,Spark等流式计算系统对恢复的调用链进行分析,然后发送给报警和监控系统。
分布式服务架构-第五章 基于调用链的服务治理系统的设计与实现_第11张图片

3.4.2调用链的大数据存储系统

HBase比较适合存储系统,基于HBase的TSDB也适合存储基于时序的数据。

设计存储的数据结构为宽表,宽表设计:

分布式服务架构-第五章 基于调用链的服务治理系统的设计与实现_第12张图片

列簇:列簇中的每一列是一次远程调用的每个阶段的信息,每个阶段做一个编号

  • 基本信息列簇:用来存储调用链的基本信息,包括调用链的类型和状态等
  • 调用信息列簇:调用链中每次调用的消息数据

分布式服务架构-第五章 基于调用链的服务治理系统的设计与实现_第13张图片

两个调用链实例

分布式服务架构-第五章 基于调用链的服务治理系统的设计与实现_第14张图片

调用链宽表设计

分布式服务架构-第五章 基于调用链的服务治理系统的设计与实现_第15张图片

高表用于通过业务系统的ID确定在某个业务系统中调用链的TraceID

分布式服务架构-第五章 基于调用链的服务治理系统的设计与实现_第16张图片

假设交易系统出现问题,业务系统的运营只能提供订单号,则需要使用订单号查询到相应的TraceID,通过调用链的恢复找到问题的所在

分布式服务架构-第五章 基于调用链的服务治理系统的设计与实现_第17张图片

3.5调用链系统的展示

问题调用超时

分布式服务架构-第五章 基于调用链的服务治理系统的设计与实现_第18张图片

你可能感兴趣的:(分布式,#,分布式服务架构原理设计与实践,架构,java,网络)