线程剖析 助力定位代码层面高耗时问题

2023年 11月 15日 43.2k 0

在当今的软件开发领域,性能问题是一个永不过时的挑战。为了解决这一挑战,开发人员需要深入了解他们的应用程序运行时的性能,并快速定位高耗时问题。线程剖析是一种强大的工具,通过采集和计算运行时线程栈,可以帮助开发人员更好地理解和解决性能问题。本文将深入探讨线程剖析的基本思想和实现思路,以及客户端和服务端的设计。

一、基本思想

线程剖析的核心思想是在业务线程执行请求时创建一个特定阈值触发的检测任务,用于监测高耗时问题。如果任务未被取消,在达到高耗时阈值时,将有专门的线程去执行剖析任务,采集业务线程的堆栈,并异步发送给剖析服务端进行计算,以估算出栈上的各个方法耗时。这个工具不仅提供了详细的性能数据,还能与开放遥测(OpenTelemetry)结合,从而实现链路特征的关联,主要流程如下:

图片图片

二、实现思路

客户端设计

客户端的架构主要体现在任务的创建、调度、执行和导出四个环节。

创建&调度任务

业务线程执行时,若满足指定要监控的接口或线程名称,将构造一个包含该线程对象的检测任务放入队列,时间轮的工作线程会周期性(默认100ms)在轮盘上移动一格,类似我们平时看到的钟表上的指针那样,每个周期会从任务队列取出所有任务,将各个任务分配添加到时间轮中每个格子中。如下图所示:

图片图片

执行任务

分配完成后,由任务执行线程池的线程去执行当前周期所属格子的所有任务。在执行前,业务线程可能优先结束而取消该任务的执行,例如在达到耗时阈值后,剖析任务已经或准备开始执行,但主线程取消了剖析任务这样一个临界点,此时可通过各语言的同步机制来及时取消剖析任务。

任务执行时,剖析线程将周期性采集线程的堆栈,而为了方便后续的分析工作,也会同时记录当前堆栈产生的时间戳,直到业务线程发出中断通知,或采集样本数达到上限,或任务状态发生改变,然后中断剖析线程的执行。

执行完成后,将采集到的线程栈集 push 到诊断数据队列,等待数据导出线程消费此队列,并发送到服务端。这里需要注意,线程栈数据文本量一般不会太小,比如我们一个专门用于测试的应用,500ms 触发的阈值下的 HTTP 接口,每次请求让线程随机 Sleep 5s 以内,当接口耗时超过 3s,单次剖析产生的栈文本大小在 200KB 以上,因此这里需要有个参数,来控制队列默认长度,避免过多的堆栈快照挤兑内存。

整个任务执行流程如下图所示:

图片图片

数据预聚合&导出

预聚合工作将由独立的工作线程消费诊断数据队列后来做,即将多个线程快照合并为一个,降低网络 IO 开销。具体就是对于快照集中每个快照的栈帧,按照它的开始时间取快照集中相同栈帧的最小值,结束时间取快照集中相同栈帧的最大值这个规则进行聚合,流程如下图所示:

图片图片

而数据发送层就比较简单了,采用高性能无锁队列 Mpsc, 使用 gRPC 协议发送到诊断服务端:

图片图片

当然,为了降低业务系统的压力,也可以将原始数据直接落盘,由外部独立的采集器逐行采集然后发送到消息队列。

服务端设计

图片图片

服务端的架构主要考虑三个点:

数据接收

同 Otel 对 Trace 数据的处理思路类似,诊断数据发送请求需要快速地被响应,来减少客户端因请求延迟导致发送队列数据被丢弃的可能。因此,诊断服务端采用吞吐性能较好的 go 语言编写,而请求涉及到跨语言调用,协议层上,综合高效快速可靠因素,选用较成熟的 gRPC 协议进行通信。

数据接收并成功解析后,需异步将数据放入队列,这里我们选用采用了多副本机制的 Kafka 消息中间件,来满足诊断服务各模块之间的解耦,同时也保证诊断数据不丢失 。

数据解析&加工

诊断剖析数据消费组会去消费队列中的数据,将数据进行进一步解析,并且持久化处理,其中包括:

  • a) 父子栈帧推导

客户端的预聚合会将多个快照合并为一个,因此快照内的每个栈帧将拥有不同的起始和结束时间。由于 Java 的原始线程堆栈是无层级的结构,为了提高数据的可读性,进一步降低高耗时问题定位发现的成本,因此需将已合并的堆栈进一步推导为包含父子栈帧的结构化信息,即从栈顶的第二个栈帧开始遍历调用栈,若当前栈帧的快照开始和结束的时间范围位于上个栈帧的左开右闭或左闭右开区间,则将当前栈帧设置为上个栈帧的子栈:

图片图片

注意:

1. 一个 Java 线程的大部分调用栈形式本身就是个从 "Thread.run" 开始的嵌套,而每次快照时也无从得知层级信息,因此不考虑推导快照开始和结束时间完全一致的栈帧,将这些栈帧置为同级即可。

2. 使用线程的快照时间来推导还原父子栈和耗时仍然是个相对比较粗略的统计行为,其精度受到当前线程调用栈快照导出的耗时,以及每次快照的间隔耗时的影响,因此父子层级结果仅供参考,并不绝对等于实际调用的关系结果。

  • b) 自身耗时计算

当已推导出父子栈帧关系后,可对结果集进行遍历,计算自身耗时,计算规则如下:

  • 从第二个栈帧开始,如果与上一个栈帧的快照开始和结束时间一致,则上个栈帧的自身耗时设为 0,否则会将当前栈帧的父栈帧(若存在)的自身耗时减掉上个栈帧的自身耗时。
  • 如果当前栈帧是最后一个,则将当前栈帧自身耗时设为快照开始与结束的时间差,并且将当前栈帧的父栈帧(若存在)的自身耗时减掉当前栈帧的自身耗时。
  • 如果当前栈帧有子栈帧,处理方式同上。

以上图调用时序为例,根据以上规则得出的自身耗时计算示意图如下:

图片图片

  • c) 数据持久化

当完成父子栈帧推导和自身耗时计算后,数据将持久化存储,例如将数据存储到 ClickHouse,供数据查询端使用。

数据查询

诊断剖析数据将以 HTTP API 形式对外提供查询服务,例如可观测性门户系统,可根据线程名,链路 Trace ID, Span ID 等特征进行剖析数据的查询。

[
    {
        "data": "YXQgc3VuLm5pby5jaC5Vd...",//线程剖析栈
        "thread_name": "XNIO-1 I/O-1",//线程名称
        "thread_state": "RUNNABLE",//线程状态
        "trigger_millisecond": 500,//触发阈值
        "self_millisecond": 38,//自身耗时
        "source_snapshot_count": 153//快照数
    },
    {
        "data": "YXQgaW8udW5kZXJ0b3cuc2Vy...",
        "thread_name": "XNIO-1 task-1",
        "thread_state": "RUNNABLE",
        "trigger_millisecond": 500,
        "self_millisecond": 0,
        "source_snapshot_count": 140
    }
]

调用链关联

线程剖析能结合 OpenTelemetry ,借助 OpenTelemetry Java Instrumentation 上下文的生命周期,从而关联 Trace ID 、接口名等链路特征。

图片图片

自身监控指标

线程剖析功能需要拥有较完善的自身监控,以便观测复杂剖析流程下对业务系统潜在的性能影响。这些监控包括:

  • 任务检测队列大小

检测队列用于给时间轮提供任务,该指标的大小给线程剖析的采样,接口名,线程名称等条件提供了一定参考。

图片图片

  • 任务释放平均耗时

剖析任务的释放将会中断正在执行的剖析任务,其中涉及到剖析、数据状态机的改变,线程的中断。多线程情况下,需保证操作的原子性,如果任务释放的平均耗时变长,则能反映当前业务系统 CPU 线程上下文切换效率下降。

图片图片

  • 正在执行剖析任务的个数

线程剖析是以线程为单位来执行的,通过观测正在进行线程剖析的任务数,可反映出剖析功能繁忙的程度,以及帮助我们决策是否需要对同时剖析的任务数进行限制。

图片图片

  • 线程堆栈导出平均耗时

线程栈导出方法的平均耗时,如果该操作耗时显著升高,且调用栈未有明显变化,则代表性能恶化。

图片图片

  • 数据队列大小

指待发送到服务端的数据队列大小。

  • 数据入队速率

指待发送到服务端的数据入队的速率。

  • 数据合并平均耗时

数据发送前进行预聚合,将多个线程快照合并为一个,这个过程的平均耗时,该值可供剖析条件提供一定参考。

图片图片

  • 线程栈快照导出发送平均字节

线程快照发送的请求包平均大小。

图片图片

  • 数据导出速率

线程快照发送的速率。

图片图片

对以上指标进行监控,也方便对相关参数进行调优,从而更好地在诊断剖析功能的完整性与服务性能之间做相关取舍。

三、结语

线程剖析为解决性能问题提供了有力支持。通过采集和分析线程栈信息,它能够帮助开发人员定位应用程序中的高耗时问题,为性能优化提供关键信息。本文详细介绍了线程剖析的基本思想和实现思路,以及客户端和服务端的设计架构。其核心思想是通过创建特定阈值触发的检测任务,监测高耗时问题,并将采集到的数据异步发送到剖析服务端进行进一步计算和分析。

此外,线程剖析的自身监控指标,这些指标有助于更好地了解剖析功能的性能和繁忙程度,以便进行决策和调优。线程剖析不仅提供了性能数据,还可以与 OpenTelemetry 相结合,实现链路特征的关联,从而更全面地理解性能问题。

总的来说,线程剖析可以帮助开发人员提高应用程序的质量和性能,快速定位性能问题,以确保应用程序的顺畅运行,同时,也可以更有效地应对性能挑战,提高应用程序的可维护性和性能。

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论