全链路分布式跟踪系统与APM

自己刚开始接触APM和分布式跟踪系统还很短,也是慢慢在摸索和理解很多概念,在这里先把自己的想法记录在这,以后慢慢来回味,应该会有不同阶段的进步。

也欢迎大家使用我们组的APM产品 https://www.163yun.com/product/apm,有问题可以一起泡泡交流。

总的来看,一个优秀的分布式跟踪系统要满足3个条件:

1、低消耗:被跟踪的系统为跟踪所付出的系统资源代价要尽量小,现在主流的APM对于系统资源的消耗在2.5%-5%左右,但是这个数值应该越小越好,因为在大规模的分布式系统下,一个单节点的资源是无法把控的,可能是超强配置,也可能是老爷机,只跑几个小服务,但是本身性能已经十分吃紧了,如果这时候跟踪应用再一跑,很可能这个节点就挂掉了,得不偿失。

2、低侵入性,足够透明:作为跟踪系统,侵入性是不可能不存在的,关键这种侵入性要在哪个层面,如何在越底层的层面上侵入,对于开发者的感知和需要配合跟踪系统的工作就越少,如果在代码层面就需要进行侵入,那对于本身业务就比较复杂的应用来说,代码就更加冗余复杂了,也不利于开发者快节奏的开发。

3、灵活的延展性:要能够充分考虑到未来分布式服务的规模,跟踪系统要在未来几年内完全吃得消。

一、跟踪过程与记录

分布式跟踪系统在跟踪过程中总要记录信息,这些信息由时间、行为、数据和信息传递方向等等,一般都记录在一个叫Span的数据结构中。以Google Drapper为例子,它的Span是这样的。


每一个Span大体来看都会有一个自己的ID(Span Id),并且会记录父Sapn的Id来记录调用关系。


Span具体的结构,Annotations中会记录一个Call从客户端发出、服务器接收、服务器处理、服务器返回发送、客户端接收这个几个过程的时间点甚至是中间具体的行为。其中的比如trance_id(用于跟踪一条链路的Id)或者是span_parent_id等信息可以通过线程调用上下文中的ThreadLocal来进行记录。


以Dapper为例,它的的跟踪记录和收集管道的过程分为三个阶段。首先,span数据写入(1)本地日志文件中。然后Dapper的守护进程和收集组件把这些数据从生产环境的主机中拉出来(2),最终写到(3)Dapper的Bigtable仓库中。一次跟踪被设计成Bigtable中的一行,每一列相当于一个span。Bigtable的支持稀疏表格布局正适合这种情况,因为每一次跟踪可以有任意多个span。

以ZIPKIN为例的跟踪过程与数据分析存储流程


在调用的整个过程中,我一直有一个疑惑,每次以节点为单位的跟踪过程中,产生的Span数据是怎么回传的。开始以我单机的思维就认为是由根节点发出跟踪请求开始后,一直等到所有需要跟踪的节点完毕,所有Span一起传回根节点,才会最终由根节点将所有数据发回APM服务端进行数据统计分析。在之后多多学习了分布式,以分布式的思维去思考这个问题时,就会发现问题,如果一个调用链的深度非常深,牵扯上百台机器时,整个跟踪的时间就会非常长,如果最后返回至根节点传回,不仅仅是等待的时间会非常长,而且数据包的量最后也会非常大,对整个网络IO的资源压力也非常大,不利于实时的数据搜集和计算。所以正确优雅的方式应该是每个节点统计完Span信息后就往APM服务端发送,化整为零,既迅速,节约了网络资源,也能够提高实时性。

而且在Span的数据种类上来看,cs代表client send , cr代表clent receive , sr代表server receive , ss代表server send,图中服务器0因为是跟踪的根节点,所以没有sr与ss,同样,服务器2与服务器3作为跟踪的底端,也不会有cs与cr,服务器1因为往下同级跟踪了两台服务器,所以Span中有两组cs与cr。

跟踪过程中具体的Span流转

# Service1的三个span 示例

# Span1:sr ss
{
    "traceId": "daaed0921874ebc3",
    "id": "daaed0921874def7",
    "name": "get",
    "parentId": "daaed0921874ebc3",
    "timestamp": 1476197067623000,
    "duration": 4479000,
    "annotations": [
        {
            "timestamp": 1476197067623000,
            "value": "sr",
            "endpoint": {
                "serviceName": "service1",
                "ipv4": "xxx.xxx.xxx.111"
            }
        },
        {
            "timestamp": 1476197072102000,
            "value": "ss",
            "endpoint": {
                "serviceName": "service1",
                "ipv4": "xxx.xxx.xxx.111"
            }
        }
    ],
    "binaryAnnotations": [
        {
            "key": "http.status_code",
            "value": "200",
            "endpoint": {
                "serviceName": "service1",
                "ipv4": "xxx.xxx.xxx.111"
            }
        },
        {
            "key": "http.url",
            "value": "/service1",
            "endpoint": {
                "serviceName": "service1",
                "ipv4": "xxx.xxx.xxx.111"
            }
        }
    ]
}

#Span2:cs cr
{
    "traceId": "daaed0921874ebc3",
    "id": "411d4c32c102a974",
    "name": "get",
    "parentId": "daaed0921874def7",
    "timestamp": 1476197069680000,
    "duration": 1168000,
    "annotations": [
        {
            "timestamp": 1476197069680000,
            "value": "cs",
            "endpoint": {
                "serviceName": "service1",
                "ipv4": "xxx.xxx.xxx.111"
            }
        },
        {
            "timestamp": 1476197070848000,
            "value": "cr",
            "endpoint": {
                "serviceName": "service1",
                "ipv4": "xxx.xxx.xxx.111"
            }
        }
    ],
    "binaryAnnotations": [
        {
            "key": "http.url",
            "value": "http://localhost:8089/service2",
            "endpoint": {
                "serviceName": "service1",
                "ipv4": "xxx.xxx.xxx.111"
            }
        }
    ]
}

#Span3:cs cr
{
    "traceId": "daaed0921874ebc3",
    "id": "7c0d7d897a858217",
    "name": "get",
    "parentId": "daaed0921874def7",
    "timestamp": 1476197070850000,
    "duration": 1216000,
    "annotations": [
        {
            "timestamp": 1476197070850000,
            "value": "cs",
            "endpoint": {
                "serviceName": "service1",
                "ipv4": "xxx.xxx.xxx.111"
            }
        },
        {
            "timestamp": 1476197072066000,
            "value": "cr",
            "endpoint": {
                "serviceName": "service1",
                "ipv4": "xxx.xxx.xxx.111"
            }
        }
    ],
    "binaryAnnotations": [
        {
            "key": "http.url",
            "value": "http://localhost:8090/service3",
            "endpoint": {
                "serviceName": "service1",
                "ipv4": "xxx.xxx.xxx.111"
            }
        }
    ]
}

二、追踪过程中的性能消耗

        2.1  性能消耗的大头

        创建Span和Annotation是对性能最大的影响,如果某一个Span还被整个跟踪系统采样,也就是被记录,需要写入磁盘以供后续分析的话,那么性能的开销就会更大了,当然很多写入磁盘的操作都会被设计成异步,也就会在一定程度上减小这种对于性能的影响。

        2.2 性能消耗的维度

        性能消耗主要会在这么几块产生影响,CPU负荷、内存消耗、网络带宽消耗,对于一个优秀的跟踪进程,我们希望在CPU的负荷上是尽可能小的,我们应该吧这个跟踪进程的CPU调度优先级调制系统最低,这样在应用高负载的情况下,我们能够最大程度保证原应用的稳定运行。内存消耗和CPU一样的要求,但是网络带宽的消耗很多时候会被忽视,这也是一个需要优化IO的一个点。这三个方面的性能消耗调节可以都通过采样率这个标准来调优。

        2.3 采样率控制性能消耗

        采样最直接的目的有两个:减少计算量和降低描述难度。

        在APM厂商中,普遍采用这样一种采样算法来计算Apdex(Application Performance Index)。

        

Apdex的计算公式是: Apdex = ( 1 x 满意 +0.5 x 容忍 + 0 x 失望 ) / 样本数。

我们套一下上面的公式: 假定样本为:小于2s的请求次数为10次,满意; 大于2s,小于8s的请求次数为20次,容忍;大于等于8s的请求次数为10次,失望。 那么得到 Apdex = ( 1 x 10 + 0.5 x 20 + 0 x 10 ) / 40 = 0.5 ,结果是Unacceptable 不能接受的,说明这次采样的这个系统就在GG 的边缘了。

但是Apdex公式的计算只是在一个宏观上判断一个服务的综合状态,如果细化的话,这个计算公式是一个加权的结果,如果有5个请求都是大于等于8s,但是其他35个都是小于2s的,那么得出的Apdex为0.875,已经是Good的状态了,但是如果这8s的5个请求是服务的主入口或者重要接口,那么这Good的表面价值就会带来巨大隐患,所以对于采样率的计算,需要对于宏观上有一套计算方案,也需要对于细节处进行探查。


应用监控是网易云基础服务提供的端到端应用性能监控服务。提供分布式调用链跟踪功能,让您可以了解服务间的调用关系,一览系统的全貌。同时提供单个服务的出现的错误和慢响应的深入分析,帮助您迅速发掘性能瓶颈。

本文来自网易实践者社区,经作者吴爱天授权发布。