《性能之巅:洞悉系统、企业与云计算》第一章(绪论)和第二章(方法)的笔记,请参考Part 1,第三章(操作系统)的笔记,请参考Part 2,第四章(观测工具)的笔记,请参考Part 3,本文是第五章——应用程序。
性能调整离工作所执行的地方越近越好:最好在应用程序里。应用程序包括数据库、Web服务器、应用服务器、负载均衡器、文件服务器等。
应用程序能变得极其复杂,尤其是在涉及众多组件的分布式应用程序环境中。研究应用程序的内部通常是应用程序开发人员的领域,会涉及第三方工具自测。对于研究系统性能的人员,包括系统管理员,应用程序性能分析包括配置应用程序实现系统资源最佳利用、归纳应用程序使用系统的方式,以及常见问题的分析。
基础
对应用程序进行性能调优前,请先回答下面这些问题:
- 功能:应用程序的角色是什么?数据库、Web服务器、负载均衡器、文件服务器、对象存储?
- 操作:应用服务器服务哪些请求,或执行什么操作?数据库服务查询(和命令)、Web服务器服务HTTP请求等。这些可以用速率来度量,用以估计负载和做容量规划;
- CPU模式:应用程序是用户级的软件实现还是内核级的软件实现?多数的应用程序是用户级别的,以一个或多个进程的形式执行,但是有些是以内核服务的形式实现的(例如,NFS);
- 配置:应用程序是怎样配置的,为什么这么配?这些信息能在配置文件里找到或用管理工具得到。检查所有与性能相关的可调参数有没有改过,包括缓冲区大小、缓存大小、并发(进程或线程)、其他选项
- 指标:有没有可用的应用程序指标,如操作率?可能是用自带工具或第三方工具,通过API请求,或通过处理操作日志得到
- 日志:应用程序创建的操作日志是哪些?能启用什么样的日志?哪些性能指标,包括延时,能从日志中得到?例如,MySQL支持慢请求日志,对每一个慢于特定阈值的请求提供有价值的性能细节信息;
- 版本:应用程序是最新版本吗?在最近的版本发布说明里有没有提及性能的修复和性能的提升?
- Bugs:应用程序有Bug数据库吗?你用的应用程序版本有什么样的性能Bug?如果你当前有一个性能问题,查找Bug数据库看看以前有没有发生过类似的事情,看看是怎样调查的,以及还有没有涉及其他内容。
- 社区/书/专家:应用程序相关的资源和文档?
目标
设立性能目标能为你的性能分析工作指明方向,并帮助你选择要做的事情。
- 延时:低应用程序响应时间
- 吞吐量:高应用程序操作率或数据传输率
- 资源使用率:对于给定应用程序工作负载,高效地使用资源。
常见情况的优化
一个能有效提高应用程序性能的方法是找到对应生产环境工作负载的公用代码路径,并开始对其优化。
观测性
操作系统最大的性能提升在于消除不必要的工作,应用程序也一样。
大O标记法
大O标记的示例
标记法 | 示例 |
---|---|
O(1) | 布尔判断 |
O(logn) | 顺序队列的二分搜索 |
O(n) | 链表的线性搜索 |
O(nlogn) | 快速排序(一般情况) |
O(n^2) | 冒泡排序(一般情况) |
O(2^n) | 分解质因数、指数增长 |
O(n!) | 旅行商人问题的穷举法 |
应用程序性能技术
提高应用程序性能的常用技术:选择I/O大小、缓存、缓冲区、轮询、并发和并行、非阻塞I/O、处理器绑定。
选择I/O尺寸
执行I/O的开销包括初始化缓冲区、系统调用、上下文切换、分配内核元数据、检查进程权限和限制、映射地址到设备、执行内核和驱动代码来执行I/O,以及,在最后释放元数据和缓冲区。初始化开销对于小型和大型的I/O都是差不多的。从效率上来说,每次I/O传输的数据越多,效率越高。
增加I/O尺寸是应用程序提高吞吐量的常用策略。考虑到每次I/O的固定开销,一次I/O传输128KB要比128次传输1KB高效得多。尤其是磁盘I/O,由于寻道时间,每次I/O开销都较高。
如果应用程序不需要,更大的I/O尺寸也会带来负面效应。一个执行8KB随机读取的数据库按128KB I/O的尺寸运行会慢得多,因为120KB的数据传输能力被浪费。选择小一些的I/O尺寸,更贴近应用程序所需,能降低I/O延时。不必要的大尺寸I/O还会浪费缓存空间。
缓存
缓存一致性(cache coherency):保证完整性,确保查询不会返回过期的数据,且执行的代价不低(理想情况下,不要高于缓存所带来的益处)。
缓存提高读性能,存储通常用缓冲区来提高写性能。
缓冲区
数据在送入下一层级之前会合并放在缓冲区中,这增加I/O 大小,提升操作效率。取决于写操作类型,可能会增加写延时,第一次写入缓冲区后,在发送之前,还要等待后续写入。
环形缓冲区(或循环缓冲区)是一类用于组件之间连续数据传输的大小固定的缓冲区,缓冲区的操作是异步的。该类型缓冲可以用头指针和尾指针来实现,指针随着数据的增加或移出而改变位置。
轮询
轮询有一些潜在的性能问题:
- 重复检查的CPU开销高昂;
- 事件发生和下一次检查的延时较高。
poll()
系统调用
有系统调用poll()
来检查文件描述符的状态,提供与轮询相似的功能,不过它是基于事件的,因此没有轮询那样的性能负担。
poll()
接口支持多个文件描述符作为一个数组,当事件发生要找到相应的文件描述符时,需要应用程序扫描这个数组。这个扫描是
O
(
n
)
O(n)
O(n),扩展时可能会变成一个性能问题。在Linux里的epoll()
可避免这种扫描,复杂度是
O
(
1
)
O(1)
O(1)。Solaris有一个相似的特性叫作事件端口(event ports),用port_get(3C)
代替poll()
。
并发和并行
分时系统(包括所有从UNIX 衍生的系统)支持程序的并发:装载和开始执行多个可运行程序的能力。虽然它们的运行时间是重叠的,但并不一定在同一瞬间都在CPU 上执行。每一个这样的程序都可以是一个应用程序进程。
多进程或多线程也是并发。
基于事件并发:event-based concurrency,应用程序服务于不同的函数并在事件发生时在这些函数之间进行切换。
并行:为了利用多处理器系统的优势,应用程序需要在同一时间运行在多颗CPU 上。
三种类型如下:
- mutex(MUTually EX clusive)锁:只有锁持有者才能操作,其他线程会阻塞并等待CPU。
- 自旋锁:自旋锁允许锁持有者操作,其他的需要自旋锁的线程会在CPU上循环自旋,检查锁是否被释放。虽然这样可以提供低延时的访问,被阻塞的线程不会离开CPU,时刻准备着运行直到锁可用,但是线程自旋、等待也是CPU资源的浪费。
- 读写锁:读/写锁通过允许多个读者或只允许一个写者而没有读者,来保证数据的完整性。
非阻塞I/O
阻塞I/O两个性能问题:
- 对于多路并发的I/O,当阻塞时,每一个阻塞的I/O都会消耗一个线程(或进程)。为支持多路并发I/O,应用程序必须创建很多线程(通常一个客户端一个线程),伴随着线程的创建和销毁,代价也很大。
- 对于频繁发生的短时I/O,频繁切换上下文的开销会消耗CPU资源并增加应用程序的延时。
非阻塞I/O模型是异步地发起I/O,而不阻塞当前线程,线程可执行其他工作。
处理器绑定
NUMA环境对于进程或线程保持运行在一颗CPU上是有优势的,线程执行I/O后,能像执行I/O之前那样运行在同一CPU上。提高应用程序的内存本地性,减少内存I/O。操作系统对此是很清楚的,设计的本意就是让应用程序线程依附在同一颗CPU上(CPU亲和性,CPU affinity)。
某些应用程序会强制将自身与CPU绑定。对于某些系统,这样做能显著地提高性能。不过如果这样的绑定与其他CPU的绑定冲突,如CPU上的设备中断映射,这样的绑定就会损害性能。
云环境下,如果服务器还被其他租户的应用程序所分享并且也做绑定,那么即使其他的CPU是空闲的,也可能因为被绑定的CPU正忙于其他的租户,而发生冲突和调度器延时。
编程语言
编程语言可能是编译的或是解释的,也有可能是通过虚拟机执行的。
编译语言
编译:在运行前将程序生成机器指令,保存在二进制可执行文件里。这些文件可在任何时间运行而无须再度编译。编译语言包括C和C++。
有些语言既有解释器又有编译器。
在编译过程中,会生成一张符号表,列出程序函数和对象名称的映射地址。可使用编译器优化来提升性能,编译器优化能对CPU指令的选择和部署做优化。
gcc(1)
编译器的优化区间是0~3,3 是最大数目的优化,gcc(1)
能显示不同的级别用到哪些优化。命令gcc -Q -O3 --help=optimizers
,完整的输出列表包括将近180项,某些即使是-O0
也会启用。
选项-fomit-frame-pointer
的文档:对于不需要帧指针的函数不记录帧指针。该选项避免保存、设置和恢复帧指针的指令,让函数多一个可用寄存器。还有在某些机器上启用该选项会使得调试不可能进行。
栈剖析有时很重要,一般不建议使用上面这个选项,而使用其对立面选项-fno-omit-frame-pointer
。
当性能问题出现时,很容易会想用更低的优化级别(从-O3
到-O2
)来重新编译应用程序,寄希望于这样能满足所有的调试需求。但并不是那么简单的:编译器输出的变化可能会很大也很重要,以至于影响到你最初想要分析的那个问题的行为。
解释语言
解释语言程序的执行是将语言在运行时翻译成行为,这一过程会增加执行开销。解释语言的目标一般都不是高性能,而是易于编程和方便调试,如Shell脚本,Python。
除非提供专门的观测工具,否则对解释语言做性能分析很困难。
虚拟机
语言虚拟机(language virtual machine),或称为进程虚拟机(process virtual machine)是模拟计算机的软件。如Java和Erlang。应用程序先编译成虚拟机指令集(字节码,bytecode),再由虚拟机解释执行,解释时会把字节码转化为机器码。
虚拟机一般是语言类型里最难观测的,性能分析通常靠的是语言虚拟机提供的工具集(如DTrace探针)和三方工具。
垃圾回收
自动垃圾回收的缺点:
- 内存增长:针对应用程序内存使用的控制不多,当没能自动识别出对象适合被释放时内存的使用会增加。如果应用程序占用内存变得太大,达到程序的极限或引起系统换页,会严重地损害性能;
- CPU成本:GC通常会间歇地运行,还会搜索和扫描内存中的对象。这会消耗CPU资源,短期内能供给应用程序的可用的CPU资源就变少。随着应用程序使用内存增多,GC对CPU的消耗也会增加。在某些情况下,可能会出现GC不断地消耗整个CPU的现象;
- 延时异常值:GC执行期间应用程序的执行可能会中止,偶而出现响应高延时。取决于GC的类型,全停、增量、并发。
方法和分析
应用程序性能方法
方法 | 类型 |
---|---|
线程状态分析 | 观测分析 |
CPU剖析 | 观测分析 |
系统调用分析 | 观测分析 |
IO剖析 | 观测分析 |
工作负载特征归纳 | 观测分析,容量分析 |
USE方法 | 观测分析 |
向下挖掘分析法 | 观测分析 |
锁分析 | 观测分析 |
静态性能调优 | 观测分析,调优 |
建议是按表中所列顺序尝试这些方法。
线程状态分析
线程状态分析的目的是分辨应用程序线程的时间用在什么地方,这能用来很快地解决某些问题,并给其他问题的研究指明方向。通常将应用程序的时间分成几个具有实际意义的状态。
线程两个状态:
- on-CPU:执行
- off-CPU:等待下一轮上CPU,或等待I/O、锁、换页、工作等。
线程的六种状态(将off-CPU情况划分更细致):
- 执行:在CPU上;
- 可运行:等待轮到上CPU;
- 匿名换页:可运行,但是因等待匿名换页而受阻;
- 睡眠:等待包括网络、块设备和数据/文本页换入在内的I/O;
- 锁:等待获取同步锁(等待其他线程);
- 空闲:等待工作。
上面这6个状态的选择保证最小性和有用性。当然可更细分,执行状态可分为用户态和内核态执行;休眠状态可根据休眠的目标来做划分。
通过减少这些状态中的前五项的时间,会得到性能提升,同时也会增加空闲时间。若其他情况不变,则意味着应用程序的请求延时变小,能应对更多负载。一旦确定线程在前五个状态中所花时间,可进一步研究:
- 执行:检查执行的是用户态时间还是内核态时间,用剖析来做CPU资源消耗分析。剖析可确定哪些代码路径消耗CPU和消耗多久,包括花费在自旋锁上的时间;
- 可运行:在这个状态上耗时意味着应用程序需要更多的CPU资源。检查整个系统的CPU负载,以及所有对该应用程序做的CPU限制(例如,资源控制);
- 匿名换页:应用程序缺少可用的主存会引起换页和延时。检查整个系统的内存使用情况,和所有对该应用程序做的内存限制;
- 睡眠:分析阻塞应用程序的资源;
- 锁:识别锁和持有该锁的线程,确定线程持锁这么长时间的原因。原因可能是持锁线程阻塞在另一个锁上,这就需要进一步的梳理。这件事情比较高阶,通常是熟悉应用程序和其锁机制的软件开发人员要做的事。
因为应用程序要等待工作,你常常会发现睡眠状态和锁状态的时间实际上就是空闲的时间。一个应用程序工作线程可能会为了工作等待条件变量(锁状态),或是为了网络I/O(睡眠状态)。当看到大量的睡眠和锁状态时间时,记住要深入检查是否真的空闲。
Linux上:
- 执行时间:
top(1)
将执行时间汇报为%CPU
; - 可运行时间:内核的schedstat将信息显示在
/proc/*/schedstat
中;还有perf sched工具 - 匿名换页:用内核的延时核算(delay accouting)特性来测量。工具包括:
getdelay.c
、DTrace、SystemTap - 睡眠:
pidstat -d
可判断一个进程是在执行磁盘I/O还是睡眠。pstack(1)
会对线程和其用户栈做一个快照,执行pstack(1)
时可能会让目标有短暂停顿,应小心使用。
Solaris上,使用prstat(1M)
,示意图:
线程各状态所用时间:
- 执行:USR+SYS
- 可运行:LAT
- 匿名换页:DFL
- 睡眠:SLP
- 锁:LCK
- 空闲:SLP+LCK
当线程离开CPU时,用DTrace检查栈跟踪,判断该线程在等待什么,这样就可得到空闲时间。如果线程很长时间困于睡眠状态,可使用pstack(1)
会让目标有短暂停顿。
CPU剖析
剖析的目标是要判断应用程序是如何消耗CPU资源的。
对栈跟踪做采样会产生出上千行需要检查的输出,即使打印的仅仅是唯一栈的汇总输出。可用火焰图做可视化。
还可对当前运行的函数单独做采样。
DTrace用ustack helper来审视VM内部,并将栈翻译成原始的程序;用DTrace的jstack()
来采样Java在CPU上的栈。
系统调用分析
有时会基于系统调用syscalls的执行来研究下面这些状态。
- 执行:CPU上,用户模式;
- 系统调用:系统调用的时间,内核模式在运行或等待。
研究系统调用有很多种方法。目标是要找出系统调用的时间花在什么地方、系统调用类型、使用该系统调用的原因。方法:
- 断点跟踪:设置系统调用入口和返回的断点,会在每一个断点中断目标程序的执行;
- strace:Linux上用
strace(1)
,对高频度系统调用和追踪函数调用的开销较高,不适用于多数生产环境场景; - truss:Solaris上用
truss(1)
; - 缓冲跟踪:当目标程序在持续执行时,监测数据可缓冲在内核里。DTrace提供缓冲跟踪和聚合变量两种方式来减少跟踪开销,允许编写定制的程序来进行系统调用分析。
选项:
-ttt
:打印第一栏UNIX时间戳,秒为单位,精确度可到毫秒级;-T
:输出最后的一栏,系统调用的用时,以秒为单位,精确度到毫秒级;-p PID
:跟踪这个PID的进程。strace(1)
还可以指定某一命令做跟踪;
truss选项:
-d
:打印第一栏时间戳,显示命令启动后的秒数;-E
:打印第二栏时间戳,显示系统调用的耗时,单位秒;-c
:统计总结;-u
:对用户级别函数调用执行动态跟踪;-p PID
:跟踪该PID进程。truss(1)
还可指定某一命令做跟踪。
通过识别出一些可做调整或消除的进程活动,更复杂的DTrace脚本:
- dtruss:DTrace版本的
truss(1)
,适用于操作系统级别; - execsnoop:通过系统调用
exec()
来跟踪新进程的执行; - opensnoop:跟踪系统调用
open()
的各种细节; - procsystime:以各种方式对系统调用时间做统计总结。
工作负载就是应用程序的系统调用。
I/O剖析
I/O剖析判断I/O相关的系统调用执行的原因和方式,可用DTrace。利用栈跟踪能得到执行系统调用的原因:
- Who:进程ID,用户名;
- What:I/O系统调用对象(例如文件系统或套接字)、I/O尺寸、IOPS、吞吐量(B/s)、其他属性;
- How:IOPS随时间的变化;
工作负载特征归纳
应用程序向系统资源施加负载,也通过系统调用向操作系统施加负载,可使用特征归纳。
发送给应用程序的工作负载也是可以研究的。这个重点就落于应用程序所提供的服务操作,以及应用程序固有的属性,这很可能就成为性能监视中关键指标,并用于容量规划之中。
USE方法
USE方法也适用于软件资源,取决于应用程序。如果能找到应用程序的内部组件功能图,对每种软件资源都做使用率、饱和和错误指标上的考量,看看有什么问题。
向下挖掘法
向下挖掘法可以检查应用程序的服务操作作为开始。
使用动态跟踪工具。
专门调查库调用的工具,Linux上的ltrace(1)
,Solaris上的apptrace(1)
和DTrace。
锁分析
锁分析可通过:
- 检查竞争
- 检查过长的持锁时间
有用于锁分析的专门工具,但有时用CPU剖析也可解决问题。对于自旋锁来说,竞争出现时,CPU使用率也会变化;对于自适应mutex锁,竞争时常常会有一些自旋:都可用栈跟踪的CPU剖析也能识别出来。不过对于这种情况,CPU剖析只能给出一部分信息,因为线程在等锁的时候可能已经被阻塞或在睡眠之中。
Solaris上锁分析工具:
plockstat(1M)
:用户级别锁lockstat(1M)
:内核级别锁
跟踪内核级别和用户级别的锁肯定会增加开销,解决方法:
- 基于DTrace上的特殊工具,能最大限度地减少这种开销;
- 以固定的频率(如97Hz)做CPU剖析,可确定许多锁问题(但不是全部),而没有跟踪事件的开销。
静态性能调优
静态性能调优的重点在于检查环境配置:
- 在运行的应用程序是什么版本?有更新的版本吗?发布说明有提及性能提高吗?
- 应用程序有哪些已知的性能问题?有可供搜索的Bug数据库吗?
- 应用程序是如何配置的?
- 如果配置或调整的与默认值不同,出于什么原因?(基于测量和分析,还是猜想?)
- 应用程序用到对象缓存吗?缓存大小是多少?
- 应用程序是并发运行的吗?这是如何配置的(如线程池大小)?
- 应用程序运行在特定模式下吗?(调试模式启动会影响性能)
- 应用程序用到哪些系统库?版本是什么?
- 应用程序用什么内存分配器?
- 应用程序配置用大页面做堆吗?
- 应用程序是编译的吗?编译器版本是什么?编译器的选项和优化是哪些?是64位吗?
- 应用程序遇到错误吗?错误之后会运行在降级模式吗?
- 有没有系统设置的限制,以及对CPU、内存、文件系统、磁盘和网络使用的资源控制?(云环境)
练习
概念:
- 什么是缓存?
- 什么是环形缓冲区?
- 什么是自旋锁?
- 什么是自适应mutex锁?
- 并发和并行的区别?
- 什么是CPU亲和性?
- 使用大I/O尺寸的优点和缺点?
- 锁的哈希表的用处?
- 讲一下编译语言、解释语言和虚拟机语言运行时大致的性能特征。
- 解释垃圾回收的作用,如何影响性能。
选择一个应用程序:
- 该应用程序的作用是什么?
- 该应用程序执行些什么样的操作?
- 该应用程序运行在用户模式还是内核模式?
- 该应用程序是如何配置的?关于性能有哪些关键选项?
- 该应用程序提供怎样的性能指标?
- 该应用程序创建的日志是怎样的?有包含性能的信息吗?
- 该应用程序最近的版本有修复的性能问题吗?
- 该应用程序已知的性能Bug有哪些?
- 该应用程序有社区吗(例如,IRC、聚会)?有性能社区吗?
- 有关于该应用程序的书吗?有关于性能的书吗?
- 该应用程序有知名的性能专家吗?他们是谁?
选择一个已施加负载的应用程序,执行下面这些任务:
- 在开始任何测量前,你预计这个应用程序是CPU密集型的还是I/O密集型的?说一下你的道理。
- 如果是CPU密集型的(I/O密集型的),确定要用到的观测工具。
- 归纳该应用程序所执行I/O的尺寸特征(例如,文件系统读取/写入,网络发送/接收)。
- 该应用程序使用缓存吗?确定缓存的大小和命中率。
- 测量应用程序服务的操作延时(响应时间)。得到平均值、最小值、最大值和全局分布。
- 用向下挖掘法调查延时主体的原因。
- 对施加在应用程序上的工作负载做特征归纳(确定who和what)。
- 核对一遍静态性能调整的确认清单。
- 该应用程序运行是并发的吗?调查一下它对同步原语的使用情况。
高阶:为Linux开发一款名为tsastat(1)
工具,按列打印六个线程状态的状态分析结果,每个状态所消耗时间。可与pidstat(1)
的行为相似,以滚屏的方式输出。