虎牙应用监控指标设计实践 当前关注
导读 监控的种类有很多种,有反映机器资源情况的基础监控、反映网络传输情况的网络监控、反映进程运行状态的应用监控以及与业务强相关的业务监控。
(资料图片)
本文主要针对应用监控部分,剖析应用监控原理,分享虎牙应用监控的采集方式、指标的设计实践过程。
主要内容包括以下四大部分:
1. 分布式应用监控原理剖析
2. 无侵入的数据采集
3. 可观测指标设计与关联
4. 落地效果展示
分享嘉宾|刘基正 虎牙 SRE开发工程师
编辑整理|张玮
出品社区|DataFun
01
分布式应用监控原理剖析
1. 跨进程监控重点
如图,这是一个常见的服务调用模型。
上半部分是服务之间的调用关系,下半部分是单个服务内部的进程执行逻辑,我们以此来分析应用监控需要关注的重点内容。
在服务之间的调用中,我们需要关注服务之间的调用指标关联,以做到跨服务的请求状态分析,避免服务间的调用指标孤立存在。
以请求数为例,假设服务 A 只监控 req1 入口的请求数,服务 B 只监控 req2 的入口请求数,此时两个指标之间没有关联状态,相互孤立,不形成联系。但是当我们采集的指标能够同时表现 req1 入口的请求次数以及由 req1 进一步调用 req2 的次数,则调用关系就联系了起来,能够挖掘出以 req1 为入口,下游的具体请求分发情况,这是跨进程应用监控需要解决的问题。
2. 单进程监控重点
单个服务进程内,请求被接收后进入到队列,再由工作线程从队列里面取任务去执行。为实现微服务请求逻辑,下游通常会发起新的远程调用,在这个过程需要关注三个应用指标:
① 基础调用指标监控: 如请求量、耗时、请求的成功错误数等;
② 请求处理的负载情况: 反映队列以及工作线程的请求负载情况的指标,如,当前承载的请求量以及请求容量;
③ 上下游请求的关联关系: 请求 req1 进来后下游发起了 req2,需要能够获得两个请求的关联关系,作为 req1 ->req2 的相关监控指标。
--
02
无侵入的数据采集
1. 无侵入数据采集方案选型
上面阐述了应用监控需要关注的重点内容,那么如何进行数据采集,做到业务代码无侵入?且数据采集可拓展?我们调研了以下几种数据采集方案:
(1)日志采集
比较传统的采集方式,通过收集各个业务进程的日志数据,清洗分析出各项应用监控指标,但依赖于企业完善的日志规范以及研发人员进行业务埋点,同时,日志数据量大且无法在同一个地方进行聚合,依赖服务端对大数据的处理,平台维护成本比较高。
(2)端口探测
通过应用进程开放指标获取端口,对端口数据进行拉取达到监控目的。主要用于探测进程存活状态的场景,监控能力比较弱,并且需要依赖业务研发人员开发,且无法监控到业务进程内部的请求关联关系。
(3)网络包监控
通过对网络传输的数据包进行解析达成应用监控的目的,但这个方式也无法探知请求之间的关联关系,同时网络包数据量大,维护成本比较高。
(4)SDK 与插件化
通过 AOP 技术或框架改造,完成不侵入业务代码的埋点监控。此方法只需要引入对应的插件或者部署 agent 就可以达到监控上报的目的,不需要再改动业务代码。在业务进程中的插件可以在应用进程内部进行指标数据聚合,使服务端的数据接收的压力减小。接入 SDK 还可以暴露接口给业务自定义监控指标。
因此,虎牙选择 SDK 与插件化作为无侵入的数据采集方案最终的选型。
2. 无侵入数据采集与请求关联
具体要怎样实现无侵入的数据采集与请求关联?
我们针对不同的请求处理框架、网络库、数据库客户端等等常用的框架做对应的插件,这些插件可以切入到对应的调用点里执行监控逻辑。拿请求处理框架 Spring MVC 来举例,当请求进来时会经由 Handle 处理模型,出发 SpringMVC 插件的监控逻辑,采集信息并设置到线程本地变量中去。当 Handle 模型处理请求后,假设下游会发起相关联远程调用,此时就会有对应的远程调用插件,如 Okhttp、HttpClient 等,来执行对应的信息采集工作,并且从线程本地变量中取出对应的上游入口信息,把他们关联起来。
线程本地变量(ThreadLocal)是 Java 语言的概念,如果是其他语言,也可以理解为把上下文存到一个能够跟随请求生命周期的内存中去。
这里面还涉及到异步请求的上下文传递问题,我们是通过包装修饰类来实现,将上下文从同步线程传递到异步线程从而完成请求的关联。
3. 无侵入数据采集-插件支持情况
目前虎牙已经实现的无侵入采集插件已经覆盖内部 90% 的 Java 平台体系。
以上虎牙以插件实现无侵入数据采集以及请求关联的方式。
--
03
可观测指标设计与关联
1. 应用性能指标
那么具体该如何设计应用监控的指标,以更直观的展示监控情况,帮助快速排查问题和监控服务状态。根据指标作用,可大概分为两种类型:
基础调用指标: 主被调请求成功率、调用请求数 QPS 以及请求耗时区间、P95、P99 等基础的调用指标,反映服务的请求处理状态。
进程负载状态: 线程负载率、线程状态分布等指标,用于观察进程的负载情况。
上图中是上下游指标关联的效果,通过指标关联我们可以看出,入口请求 getVideoList 的接收次数与成功率,而由这个请求引发的下游 Mysql 操作以及调用其他服务等操作的次数与成功率等指标,可以更快速的判断问题以及当前服务的请求处理状态。
2. 应用线程状态模型
基础调用指标比较常见,无外乎次数、耗时、成功率等,而负载状态的指标比较特殊,我在这里展开介绍一下。
进程状态模型的设计是为了观测 Web 服务器内线程池执行情况,以及它能够承载的容量。
在 Reactor 模型中,请求先经由请求队列再到 Web 工作线程池里,这个工作线程池的运行情况则可以反映请求的负载情况。
Java 线程池状态数据来源有两种:一种是操作系统提供的线程池状态模型,包含了执行中、阻塞中、等待中的线程状态数据,一种是 Java 语言中的线程池模型,包含线程池的容量、当前创建线程数,当前活跃线程数、最小空闲连接等数据。
如何把这些数据转换成更具有应用监控价值的指标呢?
我们还需要对原始的线程模型进行抽象,将这些能反映线程状态的数据转换成业务关注且具有监控价值的业务线程状态模型,直观地表达 Web 线程池运行状态,以下是描述这个模型的五个状态指标:
① 线程池容量:Web 容器线程池配置的容量大小,对应 Java 线程池中的最大线程容量;
② 当前线程数:Web 容器当前已创建线程数量,对应 Java 线程池中的当前创建线程数;
③ 任务执行中:正在执行业务逻辑的线程数,对应Java线程池中的当前活跃线程数;
④ 任务等待中:正在等待任务的空闲线程个数,开始采用的是线程状态模型里的 WAITING、TIMED_WAITING 和阻塞中三个状态的线程相加表示。但任务在执行状态中也会出现等待中状态,因此取属于等待中状态的线程并不能准确表达 Web 线程池等待任务这一指标。最终采用当前线程数减去任务执行中的线程,剩下的任务作为等待任务;
⑤ 任务执行阻塞中:执行任务过程中等待资源就绪线程数,采用 TIMED_WATING+WATING - 减去任务等待中数值来表示。
通过对操作系统线程状态数据与 Java 线程池数据进行结合,我们抽象出了 Web 服务中用户更加关注的模型,更直观地反映 Web 服务工作线程的运行状态。
3. 线程负载率
为了更直观地反映应用服务的负载与其容量,我们采用了一个比较特殊的指标来反映负载情况,那就是线程负载率指标。
当线程池执行任务后,会将执行的耗时以及线程池里面的线程池状态,如,线程池活跃线程数、当前线程数等数据通过 SDK 上报给服务端,根据这些数据,可以计算出线程负载率,计算公式为:
线程负载率 = 总实际时间/总容量时间
总容量时间: 已创建的线程占用 CPU 处理的时间的总和。计算方式为,当前的时间周期乘以当前已经创建的线程数,假设有三个线程,60 秒为一个时间周期,那么总的容量时间就为 180 秒。
总实际时间: 执行请求任务实际消耗的时间总和,假设执行请求 url1占用了 150 秒,请求 url2 占用 10 秒钟,总实际时间 = 150s+10s,也就是 160s。
将计算出来的总实际时间除以总容量时间,得到的比率即线程负载率,也就是 160s/180s = 88.88%,当前线程池总的负载率是 88.88%。将执行请求的消耗时间分别除以总容量时间,可以看到总的负载分摊到具体执行请求的占比,如上图,sendGift 是一个入口的 url,占用了 83.33% 的负载率,其中它的下游调用的 payMoney 操作占用了 70%,调用 Mysql 的 insert 操作占用了 13.33%,加起来就是入口总的耗时。通过这种方法,可以具体到请求的维度判断请求占用了多少的 CPU 负载,更细维度地去分析问题。
4. 指标阈值告警与聚合
告警能力是应用监控中必不可少的一部分。在实际的场景下,一个服务出现问题,服务下面的所有实例都会出现问题,如果是从 IP 维度发送告警,就会发送多条告警,事实上这些告警都属于一个服务的 IP,因此把这些告警事件、相同的主被调错误事件聚合起来,得到一个能够反映异常影响面的告警。
5. 可观测指标关联分析
在实际情况下,如果请求耗时均值发生激增,和请求耗时相关联的请求量和它具体的实例 IP 的 CPU 负载通常也是呈增长的趋势。将这些相关联且趋势相似的指标通过算法把搜索出来的一起分析,就可知是请求调用突增,导致了 CPU 使用率的上升,最终导致了请求耗时的增加。把相关联的指标中,有相似趋势呈现的指标通过算法搜索出来集中分析,可以帮助更快速地去定位到出现异常的根因。
--
04
落地效果展示
1.应用指标设置
上图是请求的监控效果,通过指标图表来展示。指标图表中包含了主/被调、请求数 QPS、请求成功率、耗时区间等基础应用指标,并打通进程内关联,可以直观看到请求下游的调用远程服务的情况。如图,入口请求 /control/baseConf 的下游调用了 Mysql 以及 Redis 等服务,达到关联指标效果。
线程分布状态以及线程负载率指标主要反映应用请求负载的情况。通过指标图表里的线程分布状态和线程负载率指标,可以快速看到具体是哪一个请求导致负载率变化。
如图,getUser 请求线程负载率处于高位状态,通过线程的状态分布可以观察到,当前线程池容量 200,线程数 11 个,任务执行中状态线程 8 个,执行中线程已经接近当前线程数。这种情况就可以判断出是一个请求量和并发量不高,但每一个请求的处理都非常耗时的场景。由此应用监控系统会产生一个负载率高的告警事件,在告警事件里展现线程池的快照和负载率比较高的请求给用户。
2.日常监控场景展示
根据告警与监控指标图表,可以发现应用的异常的、不合理的场景,以下是一些不合理场景具体案例:
不合理场景 1 : 应用并发访问量突增导致服务高载。这种情况下,请求耗时和请并发量都有一定突增。从指标图表也可以观察到,当前已经创建的线程数非常接近于线程池的容量,并且已创建的 150 个线程基本全部都处在执行中的状态,只有一个在等待中,已经处在高载的情况。同时,IP 的 CPU 也基本达到了满载的状态。通过应用监控的以上信息可以得知并发访问量突增,导致了服务的高载。
不合理场景 2: 个别请求耗时高占用线程。通过线程负载率算法可以计算出耗时高的请求,如图,getUser 是一个耗时比较高的请求,虽然请求的线程负载率较高,但调用量却并不高,QPS 不到 10,线程状态远远达不到最大容量,且每一个线程都处于执行中的状态,这就是个别请求耗时高占用线程的场景。
不合理场景 3: MySql 连接池过小引起并发瓶颈。对于这类场景也可以通过应用指标图表进行逐项分析。首先看负载率,如图,请求 getUser 下游调用 getConnection 获取 Mysql 连接的负载率占总负载率的比重非常大,比实际的 Mysql 的 CURD 操作负载率高 10 倍不止。具体耗时在图表中也和负载率有相似的表现,getUser 总耗时平均 18 毫秒,但是在获取连接上的耗时平均 12 毫秒左右,而实际调用 SELECT 操作只耗时 1 毫秒不到。最后看线程状态,可以发现大部分的线程处在执行阻塞中的状态,也就是阻塞在获取连接这个等待的过程中。通过这三个图就可以基本验证 Mysql 连接池的性能跟不上,导致了并发瓶颈。
3. 上下游关联排障场景
前面提到,将入口请求以及入口下游调用的关系关联起来作为一个指标进行分析。如图,A 服务告警 hyMobileSendSms 接口成功率突然降到 50%,通过关联查到下游的服务 B 调用的 getSmsCode 接口成功率下降,下游B服务的 getSmsCode 指标在这段时间内耗时激增,它是由什么导致的?
查看 getSmsCode 下游的操作发现,下游调用了 Mysql 的 SELECT 以及另外一个服务的获取明细的 getStringBS 批查询,Mysql 占用以及访问存储层的耗时特别高,因此可以判断是由于下游的数据库异常导致服务B接口成功率下降。进一步观察 B 服务的调用成功率可发现,访问 getStringBS 接口的成功率,以及获取 UID 明细的成功率都明显下降。通过以上的信息可以得出结论:数据库出现异常导致了服务 B 请求接口成功率下降,进一步导致 A 服务的请求成功率下降。
通过跨进程的指标关联分析,可以直接快速地去定位问题。这就是上下游关联排障场景的应用。
--
05
问答环节
Q1:SDK 分别用于前后端吗?
A1:目前虎牙这边只应用与后端,而且主要还是 Java 的应用用这套的应用监控的方案,前端暂时还没有覆盖到。
Q2:有调研过 Skywalking 吗?跨线程目前的串联机制,总实际时间是 CPU 时间还是接口的运行时间?这里是否包含 I/O 等待时间?
A2:有调研过。Skywalking 一套业界比较出名的开源 APM 解决方案,它主要是针对调用链(Trace)的场景。虽然这次的分享我主要是针对应用指标监控,但调用链我们内部也有做,我也可以分享一下,我们是使用另外一个开源的方案叫 Jaeger,基于 Jaeger 上进行二次开发也做到了跨线程的串联。目前的串联机制。原理是对 Java 里面线程的类做了一层修饰操作。在请求还在同步线程中执行时把上下文存在一个可跨线程访问的地方,再等线程切换到异步时从中取出,完成上下文的跨线程传递。这个修饰类我们还做了进一步的业务代码无侵入操作,具体是增强 JDK 的类加载器,切入到各个线程池对象的创建中去,对各个线程池进行这样的修饰,就无须用户主动写代码。
最终采集到的链路耗时是能够覆盖整个请求的执行时间的,当然也包括 IO 的时间,它原理就是在接收到请求或者发起一次调用时开始一次监控,最终处理结束或返回结果时结束。
Q3:线程池的情况获取是调用 tomcat 底层获得的吗?
A3:有一部分是。Java 语言的线程池模型是通过对应的服务端框架获取的,例如 Tomcat 就是。这部分的线程池可以通过框架开放的接口或者 MBean 获取。但是另外的一些操作系统本身的线程状态模型是没办法直接拿到,需要去记录线程池里面的具体的操作系统线程实例,再通过一些 Java 的系统调用来获取这个线程是处在一个执行中还是阻塞中状态。最终获取到元数据后,再对元数据进行一层应用层的一个线程模型的抽象拿到我们应用层的线程模型状态。
今天的分享就到这里,谢谢大家。
▌2023数据智能创新与实践大会
4大体系,专业解构数据智能 16个主题论坛,覆盖当下热点与趋势 70+演讲,兼具创新与最佳实践 1000+专业观众,内行人的技术盛会第四届DataFunCon数据智能创新与实践大会将于⏰ 7月21-22日 在北京召开,会议主题为新基建·新征程,聚焦数据智能四大体系: 数据架构 、 数据效能 、 算法创新 、 智能应用 。在这里, 你将 领略到数据智能技术实践最前沿的景观 。
欢迎大家 点击下方链接 获取大会门票~
标签: