链路追踪之SkyWalking

1,060 阅读20分钟

1 背景

1.1 过去

过去的用户数量相对较小,而服务的架构也没那么复杂。web服务通常使用两层(WEB服务器和数据库)或三层(WEB服务器,应用服务器和数据库)架构。

1.2 现在

然而在如今,随着互联网的成长,需要支持大量的并发连接,并且需要将功能和服务有机结合,导致更加复杂的软件栈组合。更确切地说,比三层层次更多的N层架构( SOA或者微服务架构)变得更加普遍。

1.3 问题

当采用微服务等架构的时候,某一条链路中的某个请求出现问题怎么办? 1583976058235-e745e8a0-25d8-4bd5-8b34-46e2bcf672d6.png 假设一条调用链如上,出现问题后浏览器响应异常,我们能怎么去排查问题呢?只能采用兜底的方法从上游一直看到下游一个一个方法来看?确实是可以,只是效率比较低,耗费的成本比较高。

2 概念

2.1 链路追踪

链路追踪是分布式系统下的一个概念,它的目的是解决上面的问题也就是将一次分布式请求还原成一条调用链路,将一次分布式请求的调用请求集中展示,比如,各个服务节点上的耗时、请求具体到达哪台机器上、每个服务节点的请求状态等等。 QQ截图20210625105144.png

2.1.1 原理

接口的性能指标有很多,例如:TPS、QPS、RT、错误率、缓存命中率、CPU占用率、内存使用率等等,但是对于链路追踪来说,主要关心的是这一条调用链路中各个分支链路的调用时间调用结果标识日志

2.1.1.1 单体架构

在系统初期,我们的系统架构可能如下图 Inkedbede1ea671d84203ade268f8562d2f94_LI.jpg 对于单体的架构来说,我们可以使用 AOP 来统计这三个指标,如下: Inked73a71704eee24a4e9acfd648e6b355e9_LI.jpg 使用 AOP,对原来的代码逻辑入侵更少,我们只需要在调用具体的业务逻辑前后分别打印一下时间即可计算出整体的调用时间。另外,使用 AOP 来捕获异常也可知道是哪里的调用导致的异常。

2.1.1.2 微服务架构

随着业务的发展,单体结构越来越不满足业务的需求,系统会慢慢朝着微服务架构发展,如下: Inked25043195c5784ec29ad2e1ad8c41845d_LI.jpg 在微服务架构下,当用户反馈某个功能很慢的时候,随后我们知道我们这个功能的调用链是 A ---> B ---> C ---> D ,但是这么多个服务,再加上每个服务都在好几台机子上面,怎么知道问题是出现在哪台机子的哪个服务上呢? QQ截图20210625112100.png

2.1.2 作用

分布式链路追踪就是为了解决上面的问题而生的,它的主要作用如下:

  1. 自动采取数据
  2. 分析数据,产生完整调用链:有了请求的完整调用链,问题有很大概率可复现
  3. 数据可视化:每个组件的性能可视化,能帮助我们很好地定位系统的瓶颈,及时找出问题所在

通过分布式追踪系统,我们能很好地定位请求的每条具体请求链路,从而轻易地实现请求链路追踪,进而定位和分析每个模块的性能瓶颈。

3 分布式调用链路标准(OpenTracing)

OpenTracing 通过提供平台无关、厂商无关的API,使得开发人员能够方便的添加(或更换)追踪系统的实现 不过 OpenTracing 并不是标准。因为 CNCF 不是官方标准机构,但是它的目标是致力为分布式追踪创建更标准的 API 和工具。

OpenTracing 的数据模型主要有下面三个:

  1. Trace:一个完整请求链路
  2. Span:一次调用过程,需要开始时间和结束时间
  3. SpanContext:Trace的全局上下文信息,如 traceId

ecdb5548-dac6-39d9-92bf-c3c9993d75d0.jpg 以上图作为例子:

  1. 一次下单的完整请求就是一个 Trace
  2. TraceId是这个请求的全局标识
  3. 内部的每一次调用就称为一个 Span
  4. 每个 Span 都要带上全局的 TraceId,这样才可把全局 TraceId 与每个调用关联起来
  5. 这个 TraceId 是通过 SpanContext 传输的,既然要传输,显然都要遵循协议来调用

理解完上面的概念,分布式追踪系统在微服务中又是怎么采集调用的链路信息的呢? Inked6e911aad-2774-31ff-8aa0-7dc22d6a7faa_LI.jpg 从上图可以看到,底层的 Collector 每次都会默默的帮我们收集以下的信息: QQ截图20210625115502.png 根据这些图表信息显然可以据此来画出调用链的可视化视图如下: 3e8511f5-c46e-3179-9706-c42b615ad186.jpg

4 SkyWalking

4.1 简介

SkyWalking 的核心是数据分析和度量结果的存储平台,通过 HTTP 或 gRPC 方式向 SkyWalking Collecter 提交分析和度量数据,SkyWalking Collecter 对数据进行分析和聚合,存储到 Elasticsearch、H2、MySQL、TiDB 等其一即可,最后我们可以通过 SkyWalking UI 的可视化界面对最终的结果进行查看。

4.2 架构

frame-v8.jpg

4.3 概念

SkyWalking从三个维度对应用进行监视:service、instance、endpoint。

4.3.1 Service

服务,可理解为广义维度下的程序集合,即多个程序构建成的用户服务集群我们都统称这多个程序为用户服务。

4.3.2 Instance

实例,可理解为程序集群中的某一个程序实例,即运行在某台机器上的某个进程。

4.3.3 Endpoint

端点,可以类比为Spring Boot Actuator暴露的检查端点,即从暴露的端点维度对应用进行监控。

5 使用

下面会提供一些图解,展示对应UI的指标说明,方便大家理解究竟具体某个UI是监控什么的

5.1 模块栏目

QQ截图20210628105957.png

  • 仪表盘:查看被监控服务的运行状态
  • 拓扑图:以拓扑图的方式展现服务直接的关系,并以此为入口查看相关信息
  • 追踪:以接口列表的方式展现,追踪接口内部调用过程
  • 性能剖析:单独端点进行采样分析,并可查看堆栈信息
  • 告警:触发告警的告警列表,包括实例,请求超时等
  • 自动刷新:刷新当前数据内容

5.2 控制栏

QQ截图20210628110125.png

  • 第一栏:不同内容主题的监控面板,应用/数据库/容器等
  • 第二栏:操作,包括编辑/导出当前数据/倒入展示数据/不同服务端点筛选展示
  • 第三栏:不同纬度展示,服务/实例/端点

5.3 Global 全局维度

QQ截图20210628110225.png

  • 第一栏:Global、Server、Instance、Endpoint不同展示面板,可以调整内部内容
  • Services load:服务每分钟请求数
  • Slow Services:慢响应服务,单位ms
  • Un-Health services(Apdex):Apdex性能指标,1为满分
  • Slow Endpoints:慢响应端点,单位ms
  • Global Response Latency:百分比响应延时,不同百分比的延时时间,单位ms
  • Global Heatmap:服务响应时间热力分布图,根据时间段内不同响应时间的数量显示颜色深度

5.4 Service 全局维度

QQ截图20210628110414.png

  • Service Apdex(数字):当前服务的评分
  • Service Avg Response Times:平均响应延时,单位ms
  • Successful Rate(数字):请求成功率
  • Servce Load(数字):每分钟请求数
  • Service Apdex(折线图):不同时间的Apdex评分
  • Service Response Time Percentile(折线图):百分比响应延时,不同百分比的延时时间,单位ms
  • Successful Rate(折线图):不同时间的请求成功率
  • Servce Load(折线图):不同时间的每分钟请求数
  • Service Throughput:服务吞吐量,仅针对TCP
  • Servce Instances Load:每个服务实例的每分钟请求数
  • Slow Service Instance:每个服务实例的最大延时
  • Service Instance Successful Rate:每个服务实例的请求成功率

5.5 Instace 全局维度

QQ截图20210628112053.png

  • Service Instance Load:当前实例的每分钟请求数
  • Service Instance Throughput:当前实例的吞吐量
  • Service Instance Successful Rate:当前实例的请求成功率
  • Service Instance Latency:当前实例的响应延时
  • JVM CPU:jvm占用CPU的百分比
  • JVM Memory:JVM内存占用大小,单位m
  • JVM GC Time:JVM垃圾回收时间,包含YGC和OGC
  • JVM GC Count:JVM垃圾回收次数,包含YGC和OGC
  • JVM Thread Count:JVM线程数

5.6 Endpoint端点(API)维度

QQ截图20210628112416.png

  • Endpoint Load in Current Service:每个端点的每分钟请求数
  • Slow Endpoints in Current Service:每个端点的最慢请求时间,单位ms
  • Successful Rate in Current Service:每个端点的请求成功率
  • Endpoint Load:当前端点每个时间段的请求数据
  • Endpoint Avg Response Time:当前端点每个时间段的请求行响应时间
  • Endpoint Response Time Percentile:当前端点每个时间段的响应时间占比
  • Endpoint Successful Rate:当前端点每个时间段的请求成功率

5.7 DataSource展示栏

QQ截图20210628112625.png

  • 当前数据库:选择查看数据库指标
  • Database Avg Response Time:当前数据库事件平均响应时间,单位ms
  • Database Access Successful Rate:当前数据库访问成功率
  • Database Traffic:CPM,当前数据库每分钟请求数
  • Database Access Latency Percentile:数据库不同比例的响应时间,单位ms
  • Slow Statements:前N个慢查询,单位ms
  • All Database Loads:所有数据库中CPM排名
  • Un-Health Databases:所有数据库健康排名,请求成功率排名

5.8 拓扑图

QQ截图20210628113120.png

5.9 追踪

QQ截图20210628113642.png

  • 左侧:api接口列表,红色-异常请求,蓝色-正常请求
  • 右侧:api追踪列表,api请求连接各端点的先后顺序和时间

5.10 告警

QQ截图20210628113813.png 不同维度告警列表,可分为服务、端点和实例

6. 部署

6.1 下载

SkyWalking下载分为ES6.X及其他,若存储使用ES 6.X则需要下载专用的程序包。 QQ截图20210625135037.png

6.2 安装

将下载后的安装包上传到服务器,解压即可使用,解压后的目录结构如下: QQ截图20210625135232.png

6.3 配置

SkyWalking的配置分为核心配置UI配置告警配置

6.3.1 核心配置

核心配置位于解压后的config/application.yml中,下面仅会对需要注意或常用的配置进行介绍,详细配置请参考官网配置介绍,配置文件中常用的配置模块有:cluster(集群)、core(核心)、storage(存储)。

ModuleProviderSettingsValue(s) and ExplanationSystem Environment VariableDefault
coredefaultrestHostIP绑定,用于GraphQL查询与数据上报SW_CORE_REST_HOST0.0.0.0
--restPort端口绑定SW_CORE_REST_PORT12800
--recordDataTTL记录数据的生命周期,最小值为2,单位:天SW_CORE_RECORD_DATA_TTL3
--metricsDataTTL指标数据的生命周期,最小值为2,单位:天,推荐metricsDataTTL >= recordDataTTLSW_CORE_METRICS_DATA_TTL7
storageelasticsearchclusterNodesES 节点列表SW_STORAGE_ES_CLUSTER_NODESlocalhost
--bulkActions记录数据批量执行的异步批量大小SW_STORAGE_ES_BULK_ACTIONS1000
--flushInterval刷新周期,无论是否达到bulkActions的配置,单位:秒SW_STORAGE_ES_FLUSH_INTERVAL10
--concurrentRequests允许执行的并发请求数SW_STORAGE_ES_CONCURRENT_REQUESTS2

上面简单介绍几个常用的配置,其实SkyWalking可以通过接入配置中心得到动态告警配置的能力,仅需在cluster配置上对应支持配置中心的中间件即可,同时可选择Elasticsearch 6.X、Elasticsearch 7.X、h2、mysql、tidb、influxdb、postgresq等作为数据存储的媒介。

6.3.2 UI配置

UI配置位于解压后的webapp/webapp.yml文件中,如下图: QQ截图20210625141522.png

6.3.3 告警配置

告警的配置位于解压后的config/alarm-settings.yml中,OAP Server会根据配置的配置的告警规则对采集数据进行规则判断符合告警规则的会根据hooks的配置进行指定通道告警,原理下方会进行介绍,这里只介绍告警规则的配置方式。

以下面默认配置的告警规则为例:

  1. 过去3分钟内服务平均响应时间超过1秒
  2. 服务成功率在过去2分钟内低于80%
  3. 服务90%响应时间在过去3分钟内低于1000毫秒
  4. 服务实例在过去2分钟内的平均响应时间超过1秒
rules:
  # Rule unique name, must be ended with `_rule`.
  service_resp_time_rule:
    metrics-name: service_resp_time
    op: ">"
    threshold: 1000
    period: 10
    count: 3
    silence-period: 5
    message: Response time of service {name} is more than 1000ms in 3 minutes of last 10 minutes.
  service_sla_rule:
    # Metrics value need to be long, double or int
    metrics-name: service_sla
    op: "<"
    threshold: 8000
    # The length of time to evaluate the metrics
    period: 10
    # How many times after the metrics match the condition, will trigger alarm
    count: 2
    # How many times of checks, the alarm keeps silence after alarm triggered, default as same as period.
    silence-period: 3
    message: Successful rate of service {name} is lower than 80% in 2 minutes of last 10 minutes
  service_p90_sla_rule:
    # Metrics value need to be long, double or int
    metrics-name: service_p90
    op: ">"
    threshold: 1000
    period: 10
    count: 3
    silence-period: 5
    message: 90% response time of service {name} is more than 1000ms in 3 minutes of last 10 minutes
  service_instance_resp_time_rule:
    metrics-name: service_instance_resp_time
    op: ">"
    threshold: 1000
    period: 10
    count: 2
    silence-period: 5
    message: Response time of service instance {name} is more than 1000ms in 2 minutes of last 10 minutes
  1. rule_name:规则名称,注意请以_rule结尾metrics-name:指标名称,可以根据core.oal里面的指标进行按需配置
  2. op:操作符,目前支持:>、<、=
  3. threshold:阈值
  4. period:时间窗口,表示多久检测一次告警
  5. count:在一个period的时间窗口内,如果values超过threshold值(按op),达到Count值,需要发送警报
  6. silence-period:触发告警后,在silence-period这个时间窗口中不告警,该值默认和period相同
  7. message:告警消息

7 配置

7.1 配置agent

在需要进行服务监控的服务器上,将上面解压出来的agent文件夹上传到服务器上,目录结构如下: QQ截图20210623134637.png 通过config/agent.config可以配置采集服务的配置,详细配置可以参考官网Agent配置,常用配置如下:

property keyDescriptionDefault
agent.service_name服务采集名称,对应service,可以通过启动命令传入Your_ApplicationName
agent.sample_n_per_3_secs采样率,默认3秒采样3次Not set
collector.backend_service采集服务数据上传地址127.0.0.1:11800
logging.file_name采集日志名称skywalking-api.log
logging.level采集日志等级INFO
logging.max_file_size单个日志最大大小300 * 1024 * 1024 = 314572800
logging.max_history_files保留历史日志文件数,默认不控制-1
plugin.mount插件目录plugins,activations
plugin.jdbc.trace_sql_parameters是否采集SQL执行参数false
plugin.springmvc.collect_http_params=true

plugin.tomcat.collect_http_params=true plugin.feign.collect_request_body=true | 开启http参数采集 | false |

7.2 调整启动命令

 java -Xmx512m -Dproject_name=poit-organization -javaagent:/poitech/project/skywalking/agent/skywalking-agent.jar -Dskywalking.agent.service_name=poit-organization -jar poit-organization.jar
  • javaagent:指定对应的探针
  • skywalking.agent.service_name:指定对应的服务名称

8 原理

8.1 设计解说

8.1.1 如何自动采集Span数据

SkyWalking采用的是插件化 + Agent 的形式实现了 Span 数据的自动采集,这样可以做到对代码无侵入且插件化可以实现可扩展、可插拔,对于线上环境比较友好 23ca5044-bbcc-344a-bfd7-8a64dac65ad5.jpg

8.1.2 如何跨进程传递context

**HTTP **对于请求方来说有两个参数header、body,这两个参数是我们很熟悉的东西,如果对于RocketMQ我们也知道它有MessageHeader、MessageBody的概念。body一般就是存放我们业务数据,所以不适宜在body里面传递context,而应该在header里面传递context Inkede984cc98-df95-3d0a-91d9-40160c0ffc36_LI.jpg 其实

8.1.3 怎么保证traceId唯一

要保证全局唯一 ,我们可以采用分布式或者本地生成的 ID。使用分布式的话,需要有一个发号器,每次请求都要先请求一下发号器,会有一次网络调用的开销。而SkyWalking采用的是本地生成ID的方式,就是我们常用的snowflow算法,性能很高,但是会有一个致命问题(时钟回拨),这个问题可能会导致生成的ID重复。 QQ截图20210625170919.png 那SkyWalking是怎么解决这个问题的呢,我们来肝一下源码: QQ截图20210625171346.png SkyWalking当出现时钟回拨的时候,会根据本地变量lastShiftValue响应回去生成ID,这个时候生成的ID会有点特别,跟之前生成的ID长度不一样。

8.1.4 如何保证高并发下的采集损耗

在高并发下,如果我们每个请求都采集,那毫无疑问数据量会非常大,但反过来想一下,是否真的有必要对每个请求都采集呢?其实没有必要,我们可以设置采样频率,只采样部分数据,SkyWalking 默认设置了 3 秒采样 3 次,其余请求不采样,如下图所示: Inkedacaefd444ef64cf2ae9fb4e7281794d0_LI.jpg 其实这个采集的频率和粒度已经足够我们分析组件的性能了,但是如果按照这个方式来采集数据会不会有问题呢? Inkedae5dbab476184d96943e22e15a7c1c53_LI.jpg 这个是正常的网络调用,按顺序一个一个来传递到下游是没问题的,但是如果某几个请求特别慢,堵住了怎么办,刚好请求落到下游没有进入采集区间,难道不采集了? Inked04a453b4cc5845f59632886cd79e20f3_LI.jpg SkyWalking的解决方案是,如果上游接口调用下游接口,如果是携带了Context,那这个请求肯定是会被采集的,保证了链路的完整性。

8.2 源码分析

8.2.1 Agent

从上面的使用中,我们简单通过配置javaagent即可采集相关配置的数据到OAP Server,这个就是JDK 1.5后提供的一个Agent技术(premain,postmain在JDK 1.6后提供),现在我们使用的是JDK 1.8(我全都要),所以我们是可以支持这个黑科技的,下面将采用两个小Demo演示一下:

  1. Pre-Main
public class PreMain {

    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("Pre-Main Do");
        System.out.println("Pre-Main Args: " + (null == agentArgs? "" : agentArgs));
    }

}

public class Test {
    public static void main(String[] args) {
        System.out.println("Main Do");
    }
}

// 输出
// Pre-Main Do
// Pre-Main Args: 
// Main Do
  1. Post-Main
public class PostMain {

    public static void agentmain(String args, Instrumentation inst) {
        System.out.println("Post-Main Do");
    }
}

public class Test {
    public static void main(String[] args) {
        System.out.println("Main Do");
    }
}

// 输出
// Main Do
// Post-Main Do

SkyWalking Agent加载流程:

  1. 初始化参数
  2. 扫描插件包
  3. 加载插件
  4. 过滤不需要用的插件
  5. 创建ByteBuddy
  6. 用ByteBuddy绑定Instrumentation修改字节码
public static void premain(String agentArgs, Instrumentation instrumentation) throws PluginException {
        final PluginFinder pluginFinder;
        try {
            // 初始化一些参数
            SnifferConfigInitializer.initializeCoreConfig(agentArgs);
        } catch (Exception e) {
            // try to resolve a new logger, and use the new logger to write the error log here
            LogManager.getLogger(SkyWalkingAgent.class)
                    .error(e, "SkyWalking agent initialized failure. Shutting down.");
            return;
        } finally {
            // refresh logger again after initialization finishes
            LOGGER = LogManager.getLogger(SkyWalkingAgent.class);
        }

        try {
            // 把插件对象放入 PluginFinder 容器
            pluginFinder = new PluginFinder(new PluginBootstrap().loadPlugins());
        } catch (AgentPackageNotFoundException ape) {
            LOGGER.error(ape, "Locate agent.jar failure. Shutting down.");
            return;
        } catch (Exception e) {
            LOGGER.error(e, "SkyWalking agent initialized failure. Shutting down.");
            return;
        }
		
   		//创建一个 ByteBuddy对象用于修改字节码
        final ByteBuddy byteBuddy = new ByteBuddy().with(TypeValidation.of(Config.Agent.IS_OPEN_DEBUGGING_CLASS));
		
    	//去忽略一些不需要修改字节码的包
        AgentBuilder agentBuilder = new AgentBuilder.Default(byteBuddy).ignore(
                nameStartsWith("net.bytebuddy.")
                        .or(nameStartsWith("org.slf4j."))
                        .or(nameStartsWith("org.groovy."))
                        .or(nameContains("javassist"))
                        .or(nameContains(".asm."))
                        .or(nameContains(".reflectasm."))
                        .or(nameStartsWith("sun.reflect"))
                        .or(allSkyWalkingAgentExcludeToolkit())
                        .or(ElementMatchers.isSynthetic()));

        JDK9ModuleExporter.EdgeClasses edgeClasses = new JDK9ModuleExporter.EdgeClasses();
        try {
            //加载 Bootstrap 相关的插件
            agentBuilder = BootstrapInstrumentBoost.inject(pluginFinder, instrumentation, agentBuilder, edgeClasses);
        } catch (Exception e) {
            LOGGER.error(e, "SkyWalking agent inject bootstrap instrumentation failure. Shutting down.");
            return;
        }

        try {
            agentBuilder = JDK9ModuleExporter.openReadEdge(instrumentation, agentBuilder, edgeClasses);
        } catch (Exception e) {
            LOGGER.error(e, "SkyWalking agent open read edge in JDK 9+ failure. Shutting down.");
            return;
        }

        if (Config.Agent.IS_CACHE_ENHANCED_CLASS) {
            try {
                agentBuilder = agentBuilder.with(new CacheableTransformerDecorator(Config.Agent.CLASS_CACHE_MODE));
                LOGGER.info("SkyWalking agent class cache [{}] activated.", Config.Agent.CLASS_CACHE_MODE);
            } catch (Exception e) {
                LOGGER.error(e, "SkyWalking agent can't active class cache.");
            }
        }
		
    	//使用bytebuddy 去修改字节码
        agentBuilder.type(pluginFinder.buildMatch())
                    .transform(new Transformer(pluginFinder))
                    .with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
                    .with(new RedefinitionListener())
                    .with(new Listener())
            		// 绑定instrumentation对象
                    .installOn(instrumentation);

        try {
            ServiceManager.INSTANCE.boot();
        } catch (Exception e) {
            LOGGER.error(e, "Skywalking agent boot failure.");
        }

        Runtime.getRuntime()
                .addShutdownHook(new Thread(ServiceManager.INSTANCE::shutdown, "skywalking service shutdown thread"));
    }

8.2.2 Instrumentation

Instrumentation底层依赖于JVMTI(JVMTI是Java虚拟机提供的一整套后门。通过这套后门可以对虚拟机方方面面进行监控,分析。甚至干预虚拟机的运行),可以实现在方法插入额外的字节码从而达到收集使用中的数据到指定地方的目的。如果良性使用不会影响程序的正常行为,如果恶性使用就可能产生一些负面的影响(Idea基于License破解等)。

8.2.3 JVMTI

JVMTI 本质上是在JVM内部的许多事件进行了埋点。通过这些埋点可以给外部提供当前上下文的一些信息。在Agent里面注册一些JVM事件的回调。当事件发生时JVMTI调用这些回调方法。Agent可以在回调方法里面实现自己的逻辑。JVMTIAgent是以动态链接库的形式被虚拟机加载的。 a.jpg JVMTI可以用来做什么?

  1. 使用JVMTI对class文件加密
  2. 使用JVMTI实现应用性能监控(APM)
  3. 产品运行时错误监测及调试
  4. JAVA程序的调试(debug)
  5. JAVA程序的诊断(profile)
  6. 热加载(JRebel)

8.2.4 Hook

SkyWalking中告警的回推主要是通过接口AlarmCallback实现,当程序中配置了对应的Hook会调用对应的Callback类进行告警信息的回推,下面以WebhookCallback作为例子说明:

@Slf4j
public class WebhookCallback implements AlarmCallback {
    private static final int HTTP_CONNECT_TIMEOUT = 1000;
    private static final int HTTP_CONNECTION_REQUEST_TIMEOUT = 1000;
    private static final int HTTP_SOCKET_TIMEOUT = 10000;

    private AlarmRulesWatcher alarmRulesWatcher;
    private RequestConfig requestConfig;
    private Gson gson = new Gson();

    public WebhookCallback(AlarmRulesWatcher alarmRulesWatcher) {
        this.alarmRulesWatcher = alarmRulesWatcher;
        requestConfig = RequestConfig.custom()
                                     .setConnectTimeout(HTTP_CONNECT_TIMEOUT)
                                     .setConnectionRequestTimeout(HTTP_CONNECTION_REQUEST_TIMEOUT)
                                     .setSocketTimeout(HTTP_SOCKET_TIMEOUT)
                                     .build();
    }

    @Override
    public void doAlarm(List<AlarmMessage> alarmMessage) {
        if (alarmRulesWatcher.getWebHooks().isEmpty()) {
            return;
        }

        CloseableHttpClient httpClient = HttpClients.custom().build();
        try {
            alarmRulesWatcher.getWebHooks().forEach(url -> {
                HttpPost post = new HttpPost(url);
                post.setConfig(requestConfig);
                post.setHeader(HttpHeaders.ACCEPT, HttpHeaderValues.APPLICATION_JSON.toString());
                post.setHeader(HttpHeaders.CONTENT_TYPE, HttpHeaderValues.APPLICATION_JSON.toString());

                StringEntity entity;
                CloseableHttpResponse httpResponse = null;
                try {
                    entity = new StringEntity(gson.toJson(alarmMessage), StandardCharsets.UTF_8);
                    post.setEntity(entity);
                    httpResponse = httpClient.execute(post);
                    StatusLine statusLine = httpResponse.getStatusLine();
                    if (statusLine != null && statusLine.getStatusCode() != HttpStatus.SC_OK) {
                        log.error("send alarm to " + url + " failure. Response code: " + statusLine.getStatusCode());
                    }
                } catch (UnsupportedEncodingException e) {
                    log.error("Alarm to JSON error, " + e.getMessage(), e);
                } catch (IOException e) {
                    log.error("send alarm to " + url + " failure.", e);
                } finally {
                    if (httpResponse != null) {
                        try {
                            httpResponse.close();
                        } catch (IOException e) {
                            log.error(e.getMessage(), e);
                        }

                    }
                }
            });
        } finally {
            try {
                httpClient.close();
            } catch (IOException e) {
                log.error(e.getMessage(), e);
            }
        }
    }
}

其实这个WebhookCallback做的就是将告警信息转成JSON,然后调用Rest将信息回推到对应的目的地。 QQ截图20210623154920.png

8.2.5 Alarm

SkyWalking告警我们先从告警插件的初始化聊起 QQ截图20210624094438.png QQ截图20210624094621.png 根据上面两张图我们得知,ModuleManager 通过JDK SPI机制将 AlarmModuleProvider 加载进来。从上面的使用介绍中,我们配置了 alarm-settings.yml 文件,那这个文件又是怎么被加载进去程序作为配置类的呢? QQ截图20210624094931.png QQ截图20210624095106.png 我们发现 ModuleManager 的 **init() **方法调用了 AlarmModuleProvider 的 **prepare() **方法,其实这个方法就是加载了 alarm-settings.yml 文件,将它转成相应的rules,然后注册了告警文件的监听,因为关联了配置中心之后,Alarm告警规则其实是可以动态变化的,所以这里需要监听告警规则的变化。 最后 ModuleManager 在 **init() **方法中创建了 **BootstrapFlow **然后调用该对象的 start() 方法,最终其实就是调用了我们SPI注入进来的 AlarmModuleProviderstart() 方法。

上面我们聊了告警规则的加载初始化,下面我们就来聊一下具体告警是怎么做的呢?我们先来整理一下思路,思考一下数据从服务采集上传到OAP Server,它是怎么从采集上来的数据转换成对应的告警数据再存储到对应的媒介的呢,我们先来一波猜想?

  1. 采集数据根据配置的告警规则判断是否为需要告警并存储到容器中
  2. 定时从容器中拉取对应的告警中间信息并重新校验是否需要告警(可能中间告警规则发生改变)
  3. 调用对应的告警处理器进行告警(这里需要选择出对应的存储介质并存储进去)

下面我们就来验证一下我们的猜想,首先告警规则的判断并存储在容器中,从上面的图我们能找到一点蛛丝马迹,看名字就很有意思 **NotifyHanlder **,顾名思义就是通知处理,点进去看到具体的 notify 方法,就很明显了,采集回来的指标信息就会在这里通知了 QQ截图20210624102851.png QQ截图20210624102955.png QQ截图20210624103113.png QQ截图20210624103127.png 上面这4张图就验证我们的第一个猜想了,先通过采集指标的通知回调处理,判断是否需要告警,需要的话就存储到中介媒介中(window 数据时间窗口)

现在我们有了告警数据了,那是通过什么的途径去告警的呢,这里第一个想到的就是定时任务,翻了一下果然还真是,其实就是就是 AlarmCorestart() 方法里面启用了一个定时任务,10秒一次去扫描需要告警的数据并触发告警。 QQ截图20210624110521.png 根据上图可以得知最终肯定是通过一个alarmMessageList去作为数据载体告警的,那它的数据又是从哪里来的呢? QQ截图20210624110753.png 其实就是通过我们的数据窗口拿到数据,然后构建成alarmMessage加入到集合 QQ截图20210624111511.png 最后校验下指标信息知否需要告警,需要的话就可以走到下面的doAlarm方法去告警了。到这里我们就验证了第二个猜想了,下面我们来验证最后一个。

最后其实就是将我们的告警信息创建出来,然后调用对应的存储处理器存储到对应的存储媒介 QQ截图20210624111658.png QQ截图20210624111819.png 上图的 **recordDAO **就是我们选择的存储媒介DAO QQ截图20210624111940.png 具体这里的流程是值将告警信息存储到对应的存储媒介,其实上面的 allCallbacks 循环调用 doAlarm 方法也包含了WebHooks的逻辑在在里面,这个在上面的 Hook 有做对应的说明。到这里我们最后一个猜想也验证完了,End~