在上一节,我带你了解了链路追踪的基本概念,它是可观测性中必不可少的一环。这一课时,我会带你了解链路追踪中的短板是什么,又该如何去处理。
在上一课时我提到,链路追踪中最小的单位是 Span,每一个 Span 代表一个操作,但这样存在一个问题,粒度还是太粗了。如何解决粒度太粗的问题呢?在链路追踪的实现中,我们一般会有 2 种方式。
这两点的区别在于,是否是侵入式的。埋点的方式虽然灵活,但是依赖性较高;字节码增强的形式不需要代码层介入,但仅能支持一部分应用框架。这两种方式能够实现链路追踪,并且可以大面积的使用 ,因为框架都是通用的。
但通用的链路监控方案的实现方式都逃不过面向切面编程,因为它们只能做到框架级别,而这又会导致我们可能会遇到程序执行缓慢或者不稳定的情况,却无法查询到原因。我之前在链路监控时,发现一个接口内部有很长的一段时间内没有 Span 信息。虽然我知道了这个情况,但还是无能为力。
这时候我们一般只能通过在业务中手动增加埋点的方式来进行更细粒度的 Span 开发,这种方法也有几个缺点:
对于一些 JDK 中的底层工具类,如果我们处理不当,还会出现很多不可预料的错误,那时候就不光是影响程序效率这么简单的问题。
那么对于以上的 3 个问题,要怎样解决呢?其实你可以换个角度去思考,不要被链路追踪中 Span 的思想框架给限制住。我在这里给出 2 点建议:
既然都是基于线程的,而线程中基本会伴随着方法栈,即每进入一个方法都会通过压入一个方法栈帧的情况来保存。那我们是不是可以定期查看方法栈的情况来确认问题呢?答案是肯定的。比如我们经常使用到的 jstack,其实就是实时地对所有线程的堆栈进行快照操作,来查看当前线程的执行情况。
利用我上面提到的 2 点,再结合链路中的上下文信息,我们可以通过周期性地对执行中的线程进行快照操作,并聚合所有的快照,来获得应用线程在生命周期中的执行情况,从而估算代码的执行速度,查看出具体的原因。
这样的处理方式,我们就叫作性能剖析(Profile),原理可以参照下图:
在这张图中,第一行代表线程进行快照的周期情况,每一个周期都可以认为是一段时间,比如 10ms、100ms。周期的时间长短,决定了对程序性能影响的大小。因为在进行线程快照时程序会暂停,当快照完成后才会继续进行操作。
第二行则代表我们需要进行观测的方法的执行时间,线程快照只能做到周期性的快照获取。虽然可能并不能完全匹配,但通过这种方式,相对来说已经很精准了。
性能剖析与埋点相比,有以下几个优势:
假如我们已经利用性能剖析工具获取到了链路中的线程快照列表了,那么我们该怎么去展现它们呢?
这个时候我们一般可以通过 2 种方式查看线程聚合的结果信息,它们分别是火焰图和树形图。
火焰图,顾名思义,是和火焰一样的图片。火焰图是在 Linux 环境中比较常见的一种性能剖析展现方式。火焰图有很多种展现形式,这里我就以我们会用到的 CPU 火焰图为例:
CPU 火焰图
在 CPU 火焰图中,每一个方格代表一个方法栈帧,方格的长度则代表它的执行时间,所以方格越长就说明该栈帧执行的时间越长。火焰图中在某一个方格中增高一层,就说明是这个方法栈帧中,又调用了某个方法的栈帧。最顶层的函数,是叶子函数。叶子函数的方格越宽,说明这个方法在这里的执行耗时越长。
如果觉得上面的火焰图太复杂的话,我们可以看一张简化的图,如下:
图中,a 方法是执行的方法,可以看出来,其中 g 方法是执行时间相对较长的。
无论是火焰图,还是这张简化的图,它们都通过图形的方式,让我们能够快速定位到执行缓慢的原因。但是这种的方式也存在一些问题:
为了解决这 2 个问题,就有了另外一种展现方式,那就是树形图。树形图就是将方法的调用堆栈通过树形图的形式展现出来。这对于开发人员来说相对直观,因为你可以通过树形图的形式快速查看整体的调用情况,并且针对火焰图中的问题,树形图也有很好的解决方法:
但树形图也有一些自己的问题:
所以在树形图中一般会配合前端的展示,比如只显示 topN 中耗时较高的内容,或者支持搜索功能。
火焰图和树形图没有绝对的好坏之分,只是相对应的侧重点不同:
相信通过这篇文章的讲解,你应该对链路追踪的短板,以及如何通过性能剖析去解决这些短板有了一个整体的思路。那么,你认为怎么样将这两者结合才是最方便的呢?你在工作中又遇到过什么和链路追踪相关的问题呢?欢迎你在留言区分享你的思考。
© 2019 - 2023 Liangliang Lee. Powered by gin and hexo-theme-book.