背景
关于性能测试相关的问题,我在互联网上看到过很多有很有价值的文章,但是大部分的文章倾向于针对将某些场景下的case进行针对性的分析,比较缺乏一些自顶而下的一些全局视角。在我视野所限的范围内所看到的这方面做的最好的是Brendan gregg。不太习惯尬吹个人能力,有兴趣的可以从其个人网站上看。 Brendan gregg总结了很多有性能相关的更宏观的方法论,这里尝试总结其基本思想,写一篇类似综述性质的文章,此外由于我本人大部分时间在写java,所以也会补充一些Java场景下的经验总结。文章整体比较赶,后续在逐步完善吧。
自定而下的USE模式
USE (Utilization Saturation and Errors)
当我们面对问题的时候需要有一个checklist,来快速的判断当前系统是否遇到了资源的瓶颈,通过过合理的metric指标和对系统本身的理解来快速定位问题。
虽然资源的种类和使用方式各自不同,但是最终都可以用三种方面的指标来描述:
- 利用率:作为一个时间单位内的使用百分比。例如,“一个磁盘以 90% 的利用率运行”。
- 饱和度:作为队列长度。例如,“CPU 的平均运行队列长度为 4”。
- 错误:标量计数。例如,“这个网络接口有五十次后期冲突”。
当我们面对一个系统的性能问题时:
- 首先可以检测下这个checklist,
- 发现问题,缩小范围继续排查
- 发现了未在列表上的资源类型问题,新增列表
- 未发现问题,可能是其他问题导致:比如缓存?缓存可以提高在高资源利用率下的性能问题?
通常来说对于典型的计算机系统,资源的种类大概有下面几种:
-
物理资源
- CPUs: sockets, cores, hardware threads (virtual CPUs)
- Memory: capacity
- Network interfaces
- Storage devices: I/O, capacity
- Controllers: storage, network cards
- Interconnects: CPUs, memory, I/O
-
软件资源
- thread pools: ****利用率可以定义为线程忙于处理工作的时间;等待线程池服务的请求数量达到饱和。
- mutex locks: 利用率可以定义为持有锁的时间;那些排队等待锁的线程饱和。
- process/thread capacity: 系统可能具有有限数量的进程或线程,其当前使用情况可定义为利用率;等待分配可能已饱和;错误是分配失败时(例如,“can't fork”)。
- file descriptor capacity: 与上述类似,但用于文件描述符。
netflixtechblog.com/linux-perfo…
观测资源
可以看到观测的资源还是非常多的,每一个资源都有很多可以观测的维度和指标,但是对于绝大多数程序来说是有相当大的比例问题是有共性的,针对这些共性的问题其实是有很多很好的观测工具和可视化 方案的。时间和篇幅所限,我们用三小节介绍3种最通用的资源 :CPU、MEM、Thread,其他的可以后续补充。
另外需要说明的时候profiler和上一些小节讨论的USE或者说一些黄金指标的本质不同是什么的?个人理解本质的区别是profiler的关键在于帮助我们理解这些被观测的资源是如何被使用的,
CPU
火焰图生成方式
首先最直观的CPU的观测方式就是火焰图(Flame Graphs),基本概念我就不赘述了,简单说下生成的流程,有3个基本的步骤:
生成 堆栈 -》折叠 堆栈 -》生成 火焰图
目前的系统分析工具里面绝大多数情况下,可以将前2步合并在一起,不同的操作系统其实有不同的profile工具,在Linux的场景下,有2中比较主流的方案:perf和eBPF,关于这2个工具的详细原理,我们在稍后的观测工具章节中在详细描述。总体来说perf的方案成熟度更高,是目前linux下较为主流的方案呢,而eBPF是overhead更低的方案未来一定会提供你更多更为强大的功能,是目前比较有意思的方向。
生成堆栈 | 折叠堆栈 | 生成火焰图 | ||
---|---|---|---|---|
Linux 2.6.X | perf record | stackcollapse-perf.pl | flamegraph.pl | |
Linux 4.5 | perf record | perf report | flamegraph.pl | |
Linux 4.x | eBPF(bcc) | flamegraph.pl |
虽然总体的方案是perf但是涉及到语言层面还是有很多更为细微的区别,一个核心的问题就是如何获取到正确的栈:
-
首先最简单的没有Runtime的场景下,比如C、C++等;由于最贴近底层反而是最容易配置的,唯一的问题是部分编译器,没有把栈指针寄存器(frame pointer register)作为一个编译选项,这会导致生成的火焰图会有一部分数据缺失,可以通过下面的方式修复:有兴趣的可以研究下 perf的--call-graph参数相关内容或者FP寄存器及frame pointer介绍_Skylar-CSDN博客
- 把-fno-omit-frame-pointer作为编译选项
- 使用dwarf的方式提供栈调用信息
-
对于一些包含Runtime的实现,比如Java等,实现起来会更复杂,需要单独的描述
VM体系下 的火焰图生成逻辑:
以JAVA为例,总体来说有2种火焰图 ,一种是利用类似perf工具生成系统栈的火焰图(丢失了java内部的stack信息),另一种是jstack方法生成 的火焰图(丢失了系统信息)eg:Lightweight Java Profiler (LJP),但是我们更希望在一张图中描述CPU的使用情况,形成一个Mix的栈,为了实现这个目标我们有2个问题需要解决。
- JVM的编译器JIT通常来说并不将java的方法栈或者符号表暴露给系统profiler的工具
- JVM也不使用FP寄存器,仅仅将其当成是一个通用寄存器,这样profiler无法读取到正确的栈帧信息 。
Linux体系下,为了解决上述2种方案有大概2种思路:
- JVMTI agent+JVM选项 -XX:+PreserveFramePointer
JVMTI是JVM提供的接口来实现对JVM运行状态的一些native的访问方法,通过这个接口可以实现一些jvm状态监控或者debug等高级功能,(这方面也是很有意思的研究方向,arthas、jinfo、skywalking-java等等),利用JVMTI可以实现一个java-agent(perf-map-agent)把内部的符号表写成一个文件暴露给系统profiler。
在Java8之后,通过设置-XX:+PreserveFramePointer,可以使得JVM规范的使用FP寄存器。
从而解决上述2个。生成一个cpu-mixedmode-vertx.svg
- 通过AsyncGetCallTrace
在OracleJDK/OpenJDK的体系下有一个非JVMTI的方法,也可以提供类似的功能,而且由于没有设置-XX:+PreserveFramePointer整体性能可能更好。考虑到OracleJDK/OpenJDK基本上可以算作是JAVA的事实上的业界标准,或许这才是是绝大多数Java程序的做profile的最佳实践;有2部分文章详细讨论了一些优缺点和适用范围the-pros-and-cons-of-agct,JVM CPU Profiler技术原理及源码深度解析
async-profiler 是利用这一原理实现的javaprofile工具。
arthas-profiler实质上也是async profiler
MEM
基本概念的补充
各种内存概念及分配方式
-
高级语言(C++、JAVA、等等)中的new语法 本质上是调用内存管理库(C库)实现的(malloc、free、realloc、calloc)
-
而C库本质上是调用系统调用实现的(brk/sbrk、mmap/munmap)
- (堆)brk/sbrk:用于拓展程序的数据段地址
- (文件映射)mmap/munmap:用于文件映射等待,
- (栈):编译器行为,大概率会被CPU缓存,适宜小对象的分配。
-
Linux下,内存分配的基本原理
-
VM分配的内存并不是直接分配 ,而是在访问的时候才会继续真实分配
-
VM中驻留在物理内存的是RM,MMU中维护这两者之间的映射
-
在CPU中维护了一个TLB,会维护最常用的内存区域,主要用于该CPU的L1、L2、LLC程序优化:CPU缓存基础知识、TLB缓存是个神马鬼,如何查看TLB miss?
-
如果在RM中没有找到对应的内存,或者无法正常访问就会出现Page fault图解|什么是缺页错误Page Fault-技术圈
- Hard Page Fault:物理内存不存在页帧,需要申请或者swap
- Soft Page Fault:物理内存存在页帧,需要简历MMU或者TLB的映射关系,一般是共享内存区域。
- Invalid Page Fault:空指针,越界等等,很简单(死)
-
所以从CPU的视角来说,可以访问的内存空间大概顺序是
- L1——>L2——>L3/LLC——>MEM——>DISK
-
VM下的内存概念对齐
- 如果是类似JAVA这种带有Runtime的虚拟机情况又有些不一样,要估计一个JVM的内存使用是非常困难的问题。
-
在实际生产场景中java程序占用的内存大小总是大于堆内存的大小的
- 一方面Java程序来自于很多地方,而通常我们只指定堆的大小而已。从大类上来分,除了堆内存,含有一些JVM非堆的部分、直接内存、NativeLib调用占用的内存,或者一些内存分配器自身的问题
- 单纯讨论堆内存来说其实,堆内存可能经历过一些峰值,经过GC之后堆内存的大小可能变小了,但是GC后的内存并不会直接返回给操作系统,从操作系统的角度来看虚拟内存是只增不少的。这一点和go是不同的。
-
所以怎么才算是内存有问题?
- 活跃的内存/需要的驻留内存大小大于实际可用的物理内存,导致SWAP。
- 仅仅针对堆内存,没有频繁的FULL GC。
- 内存利用不代表什么。通常只代表内存峰值:committed内存,不代表used。
-
其他更加细化的场景
-
这里讨论的内存问题主要是基于堆内存的,对于大多数场景是适用的,但是堆并非是出现内存问题的唯一可能,受限于篇幅和自身排查经验,不过多介绍,但是还是列举一些场景和排查的工具:
- 比如一些meta元数据也是会有内存问题的:比如适用了一些动态编译的技术比如Code Gen,像是spark、presto都会用,或者一些规则引擎的库,或者内置的生成一些groove,或者js。
- 堆外的场景也是会出现问题:也需要一些额外的排查工具帮忙Native Memory Tracking、 NMT、pmap
- 即使是堆内的场景,有些实现方式可能会自己去实现一些内存管理的,比如netty、array之类的。很多时候会要求用户自己主动call一些release方法,也会有些内存泄露的问题。这种情况下理论上堆内存也是会打印出来的。理论上应该可以用一些引用分析之类的方法分析。另外实现的比较好的还会提供一些监控数据出来帮助排查问题。
-
内存分析的几种方式
一般来说大概2种方式
- 一种是获取一个获取当前内存的快照,比如core dump,然后通过看具体是什么内存来判断到底地方不合理的使用了当前的内存。
- 另一种走的是插桩或者采样的路线,根据申请内存的调用栈分布从统计意义上观测申请内存的大户是谁?
基于内存的快照(Core Dump)
这部分Brendan gregg没有讲太多,但是我依然认为是个很有效和常用的分析工具,受限于于本人非常有限的linux 和C++经验,只说说JVM的heap dump
-
Linux:C++TODO
-
JAVA场景下的Dump
-
首先dump是很危险的,会暂停服务 ,服务也可能会挂,不到万不得已别搞。
-
先尝试下是不是内存问题?jstat -gcutil pid 1000,看下内存占用率和GC状态,判断下是不是GC问题
-
不看内存概要分布,看看能不能从类名上看出来一些端倪,jmap -histo:live
-
当然很多场景下是看不出来的,排在前面的大部分是些string、char、bytes数组或者一些基础类型、数组或者容器类型。这时候基本上只能用dump了
-
如果程序还在运行则需要使用jmap -dump:live,format=b,file=dump.hprof pid来生成dump。这会会触发FULLGC
-
不幸的时候很多时候如果GC太过频繁JVM可能没办法响应我们的请求,这种情况下大概有三种办法:
- 在启动java程序的时候尽量添加这个-XX:+HeapDumpOnOutOfMemoryError,这样在程序oom的时候就会自动转储一份hprof文件,很好的保留了现场。
- 使用 -F选项,
- 检测下用户权限,进程组可见性之类。
- 可以用linux内存直接coredump,然后相同版本的JVM根据coredump恢复(我从未成功过)
-
dump完成之后,大多场景会用MAT来分析内存,大部分场景下关键在于找到内存占用最多的对象,或者持有其引用的内容。JVM 内存分析工具 MAT 的深度讲解与实践--入门篇 - 掘金
-
基于申请内存的Stack(Instrumentation)
基本原理就是跟踪内存分配的方法,在申请内存的过程中,记录下当前的stack,生成内存火焰图等可视化工具从而可以很方便的判断出具体哪里消耗了了资源。
-
malloc:
- 由于malloc调用的频率太过频繁(高负载的机器上可能高达几万到几十万/Sec),绝大多数情况下性能影响非常大,基本限于DEBUG。
- 对申请内存的函数进行插桩(Valgrind memcheck(20-30倍)、libtcmalloc(5倍+)、bcc(直只计数 or eBPF,4倍+)、agentzh.org/misc/leaks.…
-
brk/sbrk
-
由于大部分情况下,应用程序不会把数据段释放掉,所以基本上只适用于增长的场景,而没办法关联到释放的场景。但是由于整体数量是可控的,<1000/Sec,所以使得跟踪的成本大幅度下降,至少使得可以在生产环境中用。
-
对于brk/sbrk跟踪的数据,可以利用perf或者eBPF进行跟踪,生成的堆栈代表着3种可能:
- 内存快速增长的代码栈
- 内存泄露的代码栈
- 异步的内存分配器:比如有个内置的内存管理器去检测当前可用的内存大小,并且在合适的时候进行分配。
- 碰巧遇到了内存分配被采样到一般性代码路径段
-
-
mmap/munmap
-
首先只能针对文件映射或者对象映射的场景,但是比brk好的地方是可以利用地址把申请和释放关联起来,调用频率不高,可以用于生产环境。
-
可以利用perf或者eBPF进行跟踪,生成的堆栈代表着3种可能:
- 内存快速增长的代码栈
- 内存泄露的代码栈
- 异步的内存分配器
-
-
Page fault
-
成本大概处于malloc和brk/sbrk/mmap/munmap之间,可以用于生产环境。
-
可以利用perf或者eBPF进行跟踪,生成的堆栈代表着3种可能:
- 内存快速增长的代码栈
- 内存泄露的代码栈
-
-
接下来我们看JAVA等存在VM的场景
-
如何监控NEW对象,这件事只能通过JVM的接口来提供能力
-
以JAVA为例,通常情况下堆内存是所有线程共享的,但是如果每个线程new一个对象都需要去堆内存去锁定一段地址则,则有非常大的同步性能损耗,所以为了避免这种不必要的损耗,通常JVM会给每个线程分配一个专属于线程自身的一小段可见内存称为Thread-Local Allocation Buffers,在内存申请的过程中会优先申请TLAB内的内存。实际上由于CPU缓存的存在,即使是单线程场景下TLAB也会加速内存分的过程。
-
TLAB也会存在一些问题:
-
比如说会有一些碎片化的问题,TLAB中如果分离一个不合理大小的对象,则剩下的空间无法分配其他对象则会被浪费掉,简单的说GC这个时候在尝试回收一些其实并没有被使用的对象。
-
TLAB整体的大小是有限的,如果分配的对象比较大则会跳过TLAB直接去堆内存进行分配:
- 由于这个原理jvm在分配多个小对象和一个大对象的场景中其实前者通常速度更快。
- 这时候如果使用跟踪TLAB的方式跟踪对象分配可能会遗漏一些大对象的分片。
-
JVMTI会提供一些TLAB的分配callback接口,可以通过这个接口实现内存分配的监控。
-
async-profiler中利用上述原理对TLAB进行采样的监控,为了降低采样频率,async-profiler设置了一个采样阈值,也就是TLAB每分配了比如500KB才进行一次采样,以降低对生产环境的 影响。
工作内存估计(Working Set Size Estimation)
-
-
- 什么是WSS
首先我们需要界定下什么是WSS?很多程序内存可能很大比如几个G或者几十个G,但是并不代表CPU在一个单位时间(比如1S)内需要将所有内存都遍历掉,而是会集中访问比如几M或者几十M的空间范围内。WSS的定义大概就指这个内存的大小。
- 这个对我们来说以为这什么?
那么定义这个WSS有什么价值呢?如果你的WSS很小集中在L1、L2的范围内的,则你基本上不需要访问主存,效率肯定是高于直接访问主存的。同理如果WSS大于主存大小肯定会非常依赖Swap,同样对指导我们内存参数的指定也是非常有意义的:
- 估计实现的原理
我们已经指导WSS指代的是正式工作场景下需要的内存容量,到目前为止其实没有非常有效的方案去实现WSS的估计,作者也只是提出从操作系统来看的估计思路,具体业务场景可能还需要开发人员自己思考如何实现?
-
为何难以跟踪
- 绝大多数用户会一次性向内核申请一块大的内存进行反复的数据处理,内核其实没办法跟踪用户态的操作。
-
什么情况下我们会调整WSS?
- 直接应用程序的内存占用大小,避免swap
- 在优化cpu缓存行的时候。程序优化:CPU缓存基础知识
- 在优化TLB的时候,TLB缓存是个神马鬼,如何查看TLB miss?
- 几种测试的方案
- 通过观测Paging/Swapping和Scanning指标来观测
基本的判断标准是:
- 持续分页/交换 == WSS 大于主内存。
- 没有分页/交换,但持续扫描 == WSS 接近主内存大小。
- 无分页/交换或扫描 == WSS 小于主内存大小。
如何获取观测对应指标:
-
Paging/Swapping:vmstat 1
-
Scanning:
- /proc/meminfo中的active和inactive内存
- perf stat -e 'vmscan:*' -a 或者 vmscan:mm_vmscan_kswapd_wake
- kswapd:用以维护active和inactive的程序
- 通过不断缩小内存,然后观察什么时候Paging/Swapping开始变频繁了,简单粗暴有效。
- 通过观察PMCs实现。主要基于WSS大概在缓存级别的小内存估计:
通过perf工具观测PMCs的CPU缓存的命中率等一些指标来估计WSS。
一个基本的判断原则是说
单线程的场景下,对于L1、L2、L3(LLC)来说,如果在某一层级有接近100%的命中率,则说明当前的WSS会小于当前缓存大小,并且大于上一级缓存。
但是有几个场景需要单独讨论:
- 对于多线程来说,由于其多核的特性,虽然在某一层级上可能有100%的命中率但是其WSS应该是多个核的缓存大小之和
- 对于内存分配来说,起内存地址的访问不是均匀的。由此带来的命中率和内存大小之间可能不是简单的反比关系,比如L2大小是8M,命中率是80%,不代表其WSS就是10M,因为可能是100M的WSS,但是热点大概在8M内。
- 刷新CPU缓存,刷新CPU缓存然后观察大概需要多久把LLC填充完毕,需要的时间越久,则说明WSS越少。
-
通过page table entry (PTE)的访问标识,清除一个进程的所有的PTE访问位,然后等待一段时间,在观测有多少PTE的访问位来统计WSS
-
重置_PAGE_BIT_ACCESSED,然后观测/proc/PID/clear_refs和/proc/PID/smaps
- 作者提供了一个工具GitHub - brendangregg/wss: Working Set Size tools来测量wss
-
# ./wss.pl 423 0.1
Watching PID 423 page references during 0.1 seconds...
Est(s) RSS(MB) PSS(MB) Ref(MB)
0.107 403.66 400.59 28.02
复制代码
- 10%的lantency
-
通过引用Idle and Young page flags的大小来观测内存大小,优点是不用重置Accessed标识位混淆视听。
- wss提供了2种方案。
Thread
如何理解线程
-
为什么线程很重要?线程的工作状态究竟反应了什么?
- 首先,我们如何衡量一个系统的性能,对于绝大多数系统来说可能是2个指标,throughtput和lantency。当然throughput也是非常重要的指标,但是很多时候是我们关心的可能是在多大的throught的情况下latency依然可以接受?
- 从业务的角度上说:对于一个请求来说,整体的工作内容都会最终分摊到一个或者几个线程分阶段来完成。那如何提高latency,最终都会归结到3种思路,能并发的地方提高并发、尽可能的减少off-cpu的时间、尽可能的减少计算量?对于一个系统层面的分析来说如何减少计算量其实我们是没办法控制的(和业务强相关),所以很多时候如何提高性能就转换成了对线程的管理。
- 从系统的角度说,CPU的调度绝大多数情况下是由操作系统实现的,实际上我们是通对线程的管理来合理的规划CPU资源的。所以对于一个线程的状态来说虽然不同系统有不同的定义,但最终可以被划分为2种on-cpu和off-cpu。
- 还有一点需要关注的是,我们并发所有的工作线程都关注其CPU利用率,我们关注的重点应该局限于最终影响请求的latency的核心workload线程上。
如何观测线程(On-CPU和Off-CPU)
总的来说有2种方法,分布是基于on-cpu和off-cpu,具体实现方式也有很多种,这一小节我们先从方法论的角度上去看如何观测:
线程分组和CPU占用时间统计
- 在一段时间内,系统的整体的线程数量是恒定的,我们可以有一个可视化的方式去定时的搜集线程的状态信息,并且按照线程的名字,或者同类型线程的分组占用的工作状态来统计其CPU的占用时间来分析,和CPU的Sample是不同的,CPU的Sample主要关心的CPU的资源分配情况。比如之前毅总做的这个thread的monitor,其实也可以看出来这部分关注的其实主要是on-cpu的时间。这个是基于java栈的on-cpu。
类似的方案其实还有很多,比如Linux下如何定位Java进程内CPU消耗最多的Java线程
Off-CPU分析
-
另一种观测方式是类似于trace的方式按照请求为对线程的工作状态来进行观测,但是想要在实际场景中利用这种方式观测依然非常困难的。主要受限于下面2个原因:
- 一方面在一个比较复杂的服务中一个请求并不一个线程完成的,有很多异步的场景,只看最上层的请求堆栈并不代表什么。
- 整体CPU的线程唤醒状态是受限于资源的,off-cpu的可能性有非常多
- 另一个方面单条链路的信息不具备统计意义。
虽然有很多问题,但是我们可以从另一个维度上观察:
- 首先虽然异步的场景很多但是大多数场景我们只需要关注我们的workload线程的off-cpu的时间就可以了
- 线程的off-cpu可以通过当前线程的状态和一些持有的监视器等信息作为补充
- 如果我们可以打印所有非executing状态的线程栈,也可以制作成一种类似于火焰图的可视化工具帮助我们排查具体是哪些线程在block。以及block的原因
Off-CPU分析理念与实现
Off-CPU Flame Graph
这一小节我们详细描述下Off-CPU的实现方案和原理,(原文www.brendangregg.com/offcpuanaly…
- 一种方式是拦截所有的线程放弃CPU的时间,并且记录下,当前时间戳、当前堆栈,并且在当前线程恢复的时候比较下当前的时间戳就可以获取到足够多的信息
- 另一种方式是依赖于采样,简单的说就是定期把所有非runtime的线程栈打印出来,但是这一点实现起来比较复杂,通常来说系统的profiler工具并不提供类似的功能。从实现的角度上来说一般利用中断实现类似的功能,比如定时cpu遍历说的线程堆栈和状态,或者在每个线程启动时候给自己设置一个定时器。
-
需要说明的是off-cpu的事件是非常频繁的(几万或者几十万/Sec),在跟踪的过程中需要非常小心,如果是不熟悉的系统需要先用小范围的时间进行试探。
-
从具体实现上来说我们需要跟踪2个检测点
- 一个是上线问切换的过程中,比如linux的finish_task_switch,在上线问的切换过程中我们可以保通过保存一些全局变量来统计总的时间和offcpu的时间。
- 另一个是当线程结束的时候需要统计下总的耗时:
on context switch finish:
sleeptime[prev_thread_id] = timestamp
if !sleeptime[thread_id]
return
delta = timestamp - sleeptime[thread_id]
totaltime[pid, execname, user stack, kernel stack] += delta
sleeptime[thread_id] = 0
on tracer exit:
for each key in totaltime:
print key
print totaltime[key]
复制代码
-
但是在具体实践中还是有很多问题需要注意
- 对于一尝试进入sleep状态的cpu时间可能会更麻烦一些,通常需要在下一次上下文切换的过程中通过比较堆栈信息来确认是否是sleep状态,这个状态需要和因为锁或者因为IO等原因导致的等待区分开来。
- 对于一个多线程的程序,生成的火焰图可能非常奇怪,有2个原因,一个是每多一个线程,整个火焰图的总时间其实是增加的,这一点和CPU的火焰图是不同的;另一个原因是可能一个空闲的线程池占用了很大的比例,但是对于我们排查问题帮助并不大,需要排除掉一些线程。
- 另一个有意思的问题是有些cpu可能是由于无意识的上下文切换导致的,比如常见的抢占式CPu。所以可能需要过滤掉一些切换状态才是有效的。
对于一个Off-CPU我们可以分析出什么东西呢?
www.brendangregg.com/FlameGraphs…
以这个Off-CPU为例我们可以看到很多有意思的信息:
现在,这种类型的分析存在一个明显的问题:此火焰图显示了除磁盘 I/O 之外的大量 CPU 外时间,但该时间主要用于休眠等待工作的线程。这很吸引人,原因有很多:
-
这揭示了 MySQL 用于管理或等待工作的各种代码路径。有许多列代表单个线程,如果您将鼠标悬停在从底部算起的第 4 行上,函数名称将描述线程的任务。例如,io_handler_thread、lock_wait_timeout_thread、pfs_spawn_thread、srv_error_monitor_thread等。这揭示了有关 mysqld 的上层架构的详细信息。
-
其中一些列的宽度在 25 到 30 秒之间。这些很可能代表单线程。一个显示 30 秒,一些显示 29 秒,一个是 25 秒。我猜这些是用于每 1 或 5 唤醒一次的线程,最终唤醒要么被 30 秒跟踪窗口捕获,要么不被捕获。
-
某些列的宽度超过 30 秒,例如io_handler_thread和pfs_spawn_thread。这些很可能代表正在执行相同代码的线程池,并且它们的总等待时间总和高于经过的跟踪时间。
唤醒图(Wake Up)
虽然现在上述的off-CPU确实有很多有价值的地方,但是依然丢失了很多的信息,比如我们可以看到有些线程阻塞在一个锁上,但是我们并不知道到底是谁持有了这个锁?这部分信息,只有在wakeup的时候才能获取到,我们希望知道当一个block的线程被另一个线程wakeup之后,之前被阻塞的线程到底等待了多久?到底是谁持有了这个锁?这个锁是什么?
on context switch start:
sleeptime[thread_id] = timestamp
on wakeup:
if !sleeptime[target_thread_id]
return
delta = timestamp - sleeptime[target_thread_id]
totaltime[pid, execname, user stack, kernel stack, target_pid, target_execname] += delta
复制代码
链图(Chain graph)
更雄心勃勃的工作在于尝试把两者结合起来形成一个链图的概念(Chain Graphs),我觉得这部分非常有意思,但是由于整体工作还在尝试中,不够成熟,我觉得后面有人有兴趣可以单独研究下。
大概的意思是希望完成下面这种的原型图:
- 底层的蓝紫色是被阻塞的栈信息
- 上面的蓝色的栈是被唤醒的栈的倒排
- 因为唤醒可能是多次的,所以我们唤醒栈可能是多层次的。
Waker Stack2唤醒Waker Stack1 ,Waker Stack1 唤醒Blocked Task
Performance tool
常用的性能测试工具
性能分析的工具非常多,每个工具可能都可以展开描述:我们这里只是点到为止的介绍一些如何分类以及如何选取即可。
首先还是要区分出来我们需要的是什么层次的开发工具?从我自己的角度上来看其实虽然都是profiler用的工具但是总体上可以分成2大类,
-
一类是更为底层的由操作系统或者VM透出的工具,这部分严格来说其实是一个观测的入口,通常通过注册一些回调、或者监听一些事件来解决暴露出来数据,典型的比如说
-
另一个类实际上是基于这些接口做出来一些更产品化的工具出来,比如说Brendan Gregg自己做的这些基于perf或者eBPF的工具,或者一些常用的JVM工具等
- perf-tools: perf analysis tools based on Linux perf_events and ftrace.
- bcc: BPF compiler collection, for which I'm a major contributor, especially for performance tools.
- bpftrace: a high-level BPF tracing language, for which I'm a major contributor.
- FlameGraph: a visualization for sampled stack traces, used for performance analysis.
- wss:Working Set Size (WSS) Tools for Linux
- HeatMap: an program for generating interactive SVG heat maps from trace data.
- Specials: "special" tools for system administrators.
每个工具其实都可以展开说下,但是篇幅所限我只能简单介绍一个我自己感兴趣的eBPF,算作一个引子。
eBPF简介
eBPF并入linux,其实带来很多非常有意思的新功能,很多做内核相关开发的人员应该会感觉非常兴奋。
linux常用的观测机制
- Hardware Events: CPU PMC 性能检测计数器
- Software Events: 利用kernel counters低层次的events,例如CPU迁移, minor faults, major faults.
- Kernel Tracepoint Events: 有些内核态的tracepoint被硬件编码到内核中的一些地方。
- User Statically-Defined Tracing (USDT): 用户态程序中的静态tracepoint。
- Dynamic Tracing: 利用kprobe和uprobe在任意位置创建event。
- Timed Profiling: 可以间隔一段时间时间进行快照。
eBPF优势在哪里
首先在对比之前我们需了解下对于linux 的操作系统,我们可以测量的事件来源有哪些?首先其实对比上一代的perf,好像BPF也没有多什么特别的东西,那两者的区别到底在哪里?
网上的解释其实有很多从我自己的理解开看,其实主要的区别在于下面几点:
- eBPF提供了一种映射功能,传统perf等工具需要把观测数据立刻从内核态传送到用户态,由此带来的成本是非常惊人的,这样使得很多观测工具并不能真实应用到生产环境。通过保留一些统计数据,或者异步读取的方式,可以极大的降低观测的overhead
- 另一个重大的变化是BPF其实在内核中提供了一种JIT的编译器,这个东西有点类似于Javascript的V8,或者Javam的VM或Instrument机制,对于开发人员来说可以利用一些编译器工具,把用户语言实现的代码(python、lua)在内核中运行,相当于把内核的生态开发给开发人员自定义,功能增强不言而喻。
使用
对于绝大多数应用程序员来说对接eBPF还是太过复杂了,我自己更关注的其实是怎么利用新机制来帮助我排查问题。从我目前能搜集到的资料来看。绝大多数的eBPF的工具的二次开发都是指向了Brendan Gregg的自己写的2个工具:可以通过下面的链接了解。
- BCC 提供了更高阶的抽象,可以让用户采用 Python、C++ 和 Lua 等高级语言快速开发 BPF 程序;
- BPFTrace 采用类似于 awk 语言快速编写 eBPF 程序;
Visualization
最后我们轻松一点,有趣的可视化小工具:
其中一些已经在上面看过了,比如火焰图,就跳过了:
Latency Heat Map
一般来说描述latency的系统更多是利用一些比如avg、p99等等来描述延迟的,但是这样其实会遗漏掉一些信息
- 有些时候我们可能会由于一些异常值导致avg之类的值很大或者不符合预期。
- avg、p99等更适合描述一些正态分布的场景,比如像一些如下图所示的一些双峰的分布的场景等
因此我们可以利用如下的形势来描述latency,横轴是时间轴,纵轴是耗时。
Utilization Heat Maps
比如在一些集群场景下,我们希望知道500台机器的CPU的使用率?改如何展示呢?
www.brendangregg.com/HeatMaps/ut…
- 量化热图
横轴标识时间,纵轴标识cpu序号,颜色的深浅标识CU的利用率
Frequency Tail
比如我希望统计有100台机器的IO的latency。下图的每一个latency都是单台机器在一段时间内的latenc分布,把他们按机器顺序排列起来就得到了如下的图。图中的黑线是平均值。