读《性能之巅》

《Systems Performance》这本书可以说是做性能调优的必读书目了,不仅仅是因为它有很多关于性能调优的理论知识,还因为它有很多关于性能调优的实例和思考。其实这本书我几年前就读过了,这次重新回顾一下,可以记录一些重要的知识点。话不多说,开始读起来。

绪论

系统性能是对整个系统的研究,包括了所有的硬件组件和整个软件栈。术语“全栈”(entire stack)有时一般仅仅指的是应用程序环境,包括数据库、应用程序,以及网站服务器。不过,当论及系统性能时,我们用全栈来表示所有事情,包括系统库和内核。

性能领域包括了一下的事情:

1.设置性能目标和建立性能模型
2.基于软件或硬件原型进行性能特征归纳
3.对开发代码进行性能分析(软件整合之前)
4.执行软件非回归性测试(软件发布前或发布后)
5.针对软件发布版本的基准测试
6.目标环境中的概念验证(Proof-of-concept)测试
7.生产环境部署的配置优化
8.监控生产环境中运行的软件
9.特定问题的性能分析

性能分析的两种视角:负载分析(workload analysis)和资源分析(resource analysis),二者从不同的方向对软件栈做分析。

方法

常见术语:

  • IOPS:每秒发生的输入/输出操作的次数,是数据传输的一个度量方法。对于磁盘的读写,IOPS 指的是每秒读和写的次数。
  • 吞吐量:评价工作执行的速率,尤其是在数据传输方面,这个术语用于描述数据传输速度(字节/秒或比特/秒)。在某些情况下(如数据库),吞吐量指的是操作的速度(每秒操作数或每秒业务数)
  • 响应时间:一次操作完成的时间。包括用于等待和服务的时间,也包括用来返回结果的时间。
  • 延时:延时是描述操作里用来等待服务的时间。在某些情况下,它可以指的是整个操作时间,等同于响应时间。例子参见 2.3 节。
  • 使用率:对于服务所请求的资源,使用率描述在所给定的时间区间内资源的繁忙程度。对于存储资源来说,使用率指的就是所消耗的存储容量(例如,内存使用率)。
  • 饱和度:指的是某一资源无法满足服务的排队工作量。
  • 瓶颈:在系统性能里,瓶颈指的是限制系统性能的那个资源。分辨和移除系统瓶颈是系统性能的一项重要工作。
  • 工作负载:系统的输入或者是对系统所施加的负载叫做工作负载。对于数据库来说,工作负载就是客户端发出的数据库请求和命令。
  • 缓存:用于复制或者缓冲一定量数据的高速存储区域,目的是为了避免对较慢的存储层级的直接访问,从而提高性能。出于经济考虑,缓存区的容量要比更慢一级的存储容量要小。

各种系统延时:

Alt text

在计算机性能领域,profiling通常是按照特定的时间间隔对系统的状态进行采样,然后对这些样本进行研究。

通用的系统性能方法:

  • 问题陈述法:对性能问题的描述,包括对性能目标的定义。
  • 科学法:科学法研究未知的问题是通过假设和试验。总结下来有以下步骤:问题、假设、预测、试验、分析。
  • 诊断循环:假设→仪器检验→数据→假设
  • 工具法:1.列出可用到的性能工具(可选的,安装的或者可购买的)2.对于每一个工具,列出它提供的有用的指标 3.对于每一个指标,列出阐释该指标可能的规则。
  • USE 方法(utilization、saturation、errors)应用于性能研究,用来识别系统瓶颈[Gregg 13]。一言以蔽之,就是:对于所有的资源,查看它的使用率、饱和度和错误。
  • 工作负载特征归纳:工作负载可以通过回答下列的问题来进行特征归纳:负载是谁产生的?进程ID、用户ID、远端的IP 地址?负载为什么会被调用?代码路径、堆栈跟踪?负载的特征是什么?IOPS、吞吐量、方向类型(读取/写入)?包含变动(标准方差),如果有的话。负载是怎样随着时间变化的?有日常模式吗?
  • 向下挖掘分析:1.监测:用于持续记录高层级的统计数据,如果问题出现,予以辨别和报警。2.识别:对于给定问题,缩小研究的范围,找到可能的瓶颈。3.分析:对特定的系统部分做进一步的检查,找到问题根源并量化问题。五个 Why
  • 延时分析:延时分析检查完成一项操作所用的时间,然后把时间再分成小的时间段,接着对有着最大延时的时间段做再次的划分,最后定位并量化问题的根本原因。
  • R 方法:R方法是针对Oracle 数据库开发的性能分析方法,意在找到延时的根源,基于Oracle 的trace events[Millsap 03]。它被描述成“基于时间的响应性能提升方法,可以得到对业务的最大经济收益”,着重于识别和量化查询过程中所消耗的时间。
  • 事件跟踪:系统的操作就是处理离散的事件,包括CPU 指令、磁盘I/O,以及磁盘命令、网络包、系统调用、函数库调用、应用程序事件、数据库查询,等等
  • 基础线统计:基础线统计包括大范围的系统观测并将数据进行保存以备将来参考。在系统或应用程序变化的之前和之后都能做基础线统计,进而分析性能变化。可以不定期地执行基础线统计并把它作为站点记录的一部分,让管理员有一个参照,了解“正常”是什么样的。若是作为性能监测的一部分,可以每天都按固定间隔执行这类任务。
  • 静态性能调整:静态性能分析是在系统空闲没有施加负载的时候执行的。做性能分析和调整,要对系统的所有组件逐一确认下列问题:该组件是需要的吗?配置是针对预期的工作负载设定的吗?组件的自动配置对于预期的工作负载是最优的吗?有组件出现错误吗?是在降级状态(degraded state)吗?
  • 缓存调优:1.缓存的大小尽量和栈的高度一样,靠近工作执行的地方,减少命中缓存的资源开销。2.确认缓存开启并确实在工作。3.确认缓存的命中/失效比例和失效率。4.如果缓存的大小是动态的,确认它的当前尺寸。5.针对工作负载调整缓存。这项工作依赖缓存的可调参数。6.针对缓存调整工作负载。这项工作包括减少对缓存不必要的消耗,这样可以释放更多空间来给目标工作负载使用。
  • 微基准测试:微基准测试测量的是施加了简单的人造工作负载的性能。微基准测试可以用于支持科学方法,将假设和预测放到测试中验证,或者作为容量规划的一部分来执行。可以用微基准测试工具来施加工作负载并度量性能。或者用负载生成器来产生负载,用标准的系统工具来测量性能。两种方法都可以,但最稳妥的办法是使用微基准测试工具并用标准系统工具再次确认性能数据。

操作系统

  • 操作系统:这里指的是安装在系统上的软件和文件,使得系统可以启动和运行程序。操作系统包括内核、管理工具,以及系统库。
  • 内核:内核是管理系统的程序,包括设备(硬件)、内存和CPU 调度。它运行在CPU的特权模式,允许直接访问硬件,称为内核态。
  • 进程:是一个OS 的抽象概念,是用来执行程序的环境。程序通常运行在用户模式,通过系统调用或自陷来进入内核模式(例如,执行设备I/O)。进程是用以执行用户级别程序的环境。它包括内存地址空间、文件描述符、线程栈和寄存器
  • 线程:可被调度的运行在CPU 上的可执行上下文。内核有多个线程,一个进程有一个或多个线程。
  • 任务:一个Linux 的可运行实体,可以指一个进程(含有单个线程),或一个多线程的进程里的一个线程,或者内核线程。
  • 内核空间:内核的内存地址空间。
  • 用户空间:进程的内存地址空间。
  • 用户空间:用户级别的程序和库(/usr/bin、/usr/lib……)。
  • 上下文切换:内核程序切换CPU 让其在不同的地址空间上做操作(上下文)。
  • 系统调用:一套定义明确的协议,为用户程序请求内核执行特权操作,包括设备I/O。
  • 处理器:不要与进程混淆[1],处理器是包含有一颗或多颗CPU 的物理芯片。
  • 自陷:信号发送到内核,请求执行一段系统程序(特权操作)。自陷类型包括系统调用、处理器异常,以及中断。
  • 中断:由物理设备发送给内核的信号,通常是请求I/O 服务。中断是自陷的一种类型。

内核管理着CPU 调度、内存、文件系统、网络协议,以及系统设备(磁盘、网络接口,等等)。

栈用函数和寄存器的方式记录了线程的执行历史。当函数被调用时,CPU 当前的寄存器组(保存CPU 状态)会存放在栈里,在顶部会为线程的当前执行添加一个新的栈帧。函数通过调用CPU 指令“return”终止执行,从而清除当前的栈,执行会返回到之前的栈,并恢复相应的状态。

在执行系统调用时,一个进程的线程有两个栈:一个用户级别的栈和一个内核级别的栈。

中断服务程序(interrupt service routine)需要通过注册来处理设备中断。这类程序的设计要点是需要运行得尽可能快,以减少对活动线程中断的影响。如果中断要做的工作不少,尤其是还可能被锁阻塞,那么最好用中断线程来处理,由内核来调度。

从中断开始到中断被服务之间的时间叫做中断延时(interrupt latency)

中断优先级(interrupt priority level,IPL)表示的是当前活跃的中断服务程序的优先级。

虚拟内存是主存的抽象,提供进程和内核,它们自己的近乎是无穷的和私有的主存视野。

一级存储是主存(RAM),二级存储是存储设备(磁盘)。

当虚拟内存用二级存储作为主存的扩展时,内核会尽力保持最活跃的数据在主存中。有以下两个内核例程做这件事情。
● 交换:让整个进程在主存和二级存储之间做移动。
● 换页:移动称为页的小的内存单元(例如,4KB)。
swapping 是原始的UNIX 方法,会引起严重的性能损耗。paging 是更高效的方法,经由换页虚拟内存的引入而加到了BSD 中。两种方法,最近最少使用(或最近未使用)的内存被移动到二级存储,仅在需要时再次搬回

调度器可以动态地修改进程的优先级以提升特定工作负载的性能。工作负载可以做以下分类:

  • CPU 密集型:应用程序执行繁重的计算,例如,科学和数学分析,通常运行时间较长(秒、分钟、小时)。这些会受到CPU 资源的限制。
  • I/O 密集型:应用程序执行I/O,计算不多,例如,Web 服务器、文件服务器,以及交互的shell,这些需要的是低延时的响应。当负载增加时,会受到存储I/O 或网络资源的限制。

操作系统提供了全局的文件命名空间,组织成为一个以根目录(“/”)为起点,自上而下的拓扑结构。通过挂载(mounting)可以添加文件系统的树,把自己的树挂在一个目录上(挂载点)。这使得遍历文件命名空间对于终端用户是透明的,不用考虑底层的文件系统类型。

观测工具

  1. 计数器

内核维护了各种统计数据,称为计数器,用于对事件计数。计数器的使用可以认为是“零开销”的,因为它们默认就是开启的,而且始终由内核维护。唯一的使用开销是从用户空间读取它们的时候(可以忽略不计)。

系统级别:

  • vmstat:虚拟内存和物理内存的统计,系统级别。
  • mpstat:每个CPU 的使用情况。
  • iostat:每个磁盘I/O 的使用情况,由块设备接口报告。
  • netstat:网络接口的统计,TCP/IP 栈的统计,以及每个连接的一些统计信息。
  • sar:各种各样的统计,能归档历史数据。

进程级别:

  • ps:进程状态,显示进程的各种统计信息,包括内存和CPU 的使用。
  • top:按一个统计数据(如CPU 使用)排序,显示排名高的进程。基于Solaris 的系统对应的工具是prstat(1M)。
  • pmap:将进程的内存段和使用统计一起列出。
  1. tracing

跟踪收集每一个事件的数据以供分析。跟踪框架一般默认是不启用的,因为跟踪捕获数据会有CPU 开销。

系统级别:

  • tcpdump:网络包跟踪(用libpcap 库)。
  • snoop:为基于Solaris 的系统打造的网络包跟踪工具。
  • blktrace:块I/O 跟踪(Linux)。
  • iosnoop:块I/O 跟踪(基于DTrace)。
  • execsnoop:跟踪新进程(基于DTrace)。
  • dtruss:系统级别的系统调用缓冲跟踪(基于DTrace)。
  • DTrace:跟踪内核的内部活动和所有资源的使用情况(不仅仅是网络和块I/O),支持静态和动态的跟踪。
  • SystemTap:跟踪内核的内部活动和所有资源的使用情况,支持静态和动态的跟踪。
  • perf:Linux 性能事件,跟踪静态和动态的探针。

进程级别:

  • strace:基于Linux 系统的系统调用跟踪。
  • truss:基于Solaris 系统的系统调用跟踪。
  • gdb:源代码级别的调试器,广泛应用于Linux 系统。
  • mdb:Solaris 系统的一个具有可扩展性的调试器。
  1. Profiling

剖析(profiling)通过对目标收集采样或快照来归纳目标特征。

系统级别和进程级别:

  • profile:Linux 系统剖析。
  • perf:Linux 性能工具集,包含有剖析的子命令。
  • DTrace:程序化剖析,基于时间的剖析用自身的profile provider,基于硬件事件的剖析用cpc provider。
  • SystemTap:程序化剖析,基于时间的剖析用自身的timer tapset,基于硬件事件的剖析用自身perf tapset。
  • cachegrind:源自valgrind 工具集,能对硬件缓存的使用做剖析,也能用kcachegrind做数据可视化。
  • Intel VTune Amplifier XE:Linux 和Windows 的剖析,拥有包括源代码浏览在内的图形界面。
  • Oracle Solaris Studio:用自带的性能分析器对Solaris 和Linux 做剖析,拥有包括源代码浏览在内的图形界面。

应用程序

性能调整离工作所执行的地方越近越好:最好在应用程序里。

一个能有效提高应用程序性能的方法是找到对应生产环境工作负载的公用代码路径,并开始对其做优化。如果应用程序是CPU 密集型的,那么意味着代码路径会频繁占用CPU。如果应用程序是I/O 密集型的,你应该查看导致频繁I/O 的代码路径。

操作系统最大的性能提升在于消除不必要的工作。

应用程序性能技术:

  • 选择 IO 的大小:执行I/O 的开销包括初始化缓冲区、系统调用、上下文切换、分配内核元数据、检查进程权限和限制、映射地址到设备、执行内核和驱动代码来执行I/O,以及,在最后释放元数据和缓冲区。增加I/O 尺寸是应用程序提高吞吐量的常用策略。考虑到每次I/O 的固定开销,一次I/O 传输128KB 要比128 次传输1KB 高效得多。尤其是磁盘I/O,由于寻道时间,每次I/O 开销都较高。
  • 缓存:缓存提高了读操作性能,存储通常用缓冲区来提高写操作的性能。
  • 缓冲区:环形缓冲区(或循环缓冲区)是一类用于组件之间连续数据传输的大小固定的缓冲区,缓冲区的操作是异步的。该类型缓冲可以用头指针和尾指针来实现,指针随着数据的增加或移出而改变位置。
  • 轮询:轮询是系统等待某一事件发生的技术,该技术在循环中检查事件状态,两次检查之间有停顿。轮询有一些潜在的性能问题:重复检查的CPU 开销高昂;事件发生和下一次检查的延时较高。poll()接口支持多个文件描述符作为一个数组,当事件发生要找到相应的文件描述符时,需要应用程序扫描这个数组。这个扫描是O(n),扩展时可能会变成一个性能问题:在Linux 里是epoll(),epoll()避免了这种扫描,复杂度是O(1)
  • 并发和并行:分时系统(包括所有从UNIX 衍生的系统)支持程序的并发:装载和开始执行多个可运行程序的能力。另外一个方法是基于事件并发(event-based concurrency),应用程序服务于不同的函数并在事件发生时在这些函数之间进行切换。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Note:

同步原语:

- mutex(MUTually EX clusive)锁:只有锁持有者才能操作,其他线程会阻塞并等待CPU。
- 自旋锁:自旋锁允许锁持有者操作,其他的需要自旋锁的线程会在CPU 上循环自旋,检查锁是否被释放。虽然这样可以提供低延时的访问,被阻塞的线程不会离开CPU,时刻准备着运行直到锁可用,但是线程自旋、等待也是对CPU 资源的浪费。
- 读写锁:读/写锁通过允许多个读者或者只允许一个写者而没有读者,来保证数据的完整性。

mutex 锁可以用库或内核实现成为自适应mutex 锁(adaptive mutex lock):这是自旋锁和mutex 锁的混合,如果锁持有者当前正运行在另一个CPU 上,线程会自旋,如果不是,线程会阻塞(或者自旋的时间阈值到了)。自适应的mutex 锁的优化支持低延时访问而又不浪费CPU资源,在基于Solaris 的系统上已经用了很多年。它在2009年应用到了Linux 上,称为自适应自旋mutex(adaptive spinning mutex)

哈希表:

可以用一张锁的哈希表来对大量数据结构的锁做数目优化。下面是两种方法:
- 为所有的数据结构只设定一个全局的mutex 锁。虽然这个方案很简单,不过并发的访问会有锁的竞争,等待时也会有延时。需要该锁的多个线程会串行执行,而不是并发执行。
- 为每个数据结构都设定一个mutex 锁。虽然这个方案将锁的竞争减小到真正需要时才发生——对同一个数据结构的访问也会是并发的——但是锁会有存储开销,为每个数据结构创建和销毁锁也会有CPU 开销。
锁哈希表是一种折衷的方案,当期望锁的竞争能轻一些的时候很适用。创建固定数目的锁,用哈希算法来选择哪个锁用于哪个数据结构。这就避免了随数据结构创建和销毁锁的开销,也避免了只使用单个锁的问题。
  • 非阻塞 I/O:非阻塞I/O 模型是异步地发起I/O,而不阻塞当前的线程,线程可以执行其他的工作。
  • 处理器绑定:NUMA 环境对于进程或线程保持运行在一颗CPU 上是有优势的,线程执行I/O 后,能像执行I/O 之前那样运行在同一CPU 上。这提高了应用程序的内存本地性,减少内存I/O,并提高了应用程序的整体性能。某些应用程序会强制将自身与CPU 绑定。对于某些系统,这样做能显著地提高性能。

方法与分析:

  • 线程状态分析:六种状态。
    • 执行:在CPU 上。
    • 可运行:等待轮到上CPU。
    • 匿名换页:可运行,但是因等待匿名换页而受阻。
    • 睡眠:等待包括网络、块设备和数据/文本页换入在内的I/O。
    • 锁:等待获取同步锁(等待其他线程)。
    • 空闲:等待工作。
  • CPU剖析:剖析的目标是要判断应用程序是如何消耗CPU 资源的。一个有效的技术是对CPU 上的用户栈跟踪做采样并将采样结果联系起来。栈跟踪告诉我们所选择的代码路径,这能够从高层和底层两方面揭示出应用程序消耗CPU 的原因。
  • 系统调用分析:执行:CPU 上(用户模式);系统调用:系统调用的时间(内核模式在运行或者等待)系统调用的时间包括I/O、锁,以及其他系统调用类型。其他的线程状态,如可运行(等待CPU)和匿名换页,都被简化了。即使碰到这些状态(CPU 饱和或内存饱和),也能用USE 方法在系统中识别出来。
  • I/O 剖析:I/O 剖析判断的是I/O 相关的系统调用执行的原因和方式。用DTrace 可以做到这一点,检查用户栈里系统调用的栈跟踪。
  • 工作负载特征归纳:应用程序向系统资源——CPU、内存、文件系统、磁盘和网络,施加负载,也通过系统调用向操作系统施加负载。
  • USE 方法:USE 方法检查使用率、饱和,以及所有硬件资源的错误。通过发现某一成为瓶颈的资源,许多应用程序的性能问题都能用该方法得到解决。USE 方法也适用于软件资源,取决于应用程序。如果你能找到应用程序的内部组件的功能图,对每种软件资源都做使用率、饱和和错误指标上的考量,看看有什么问题。
  • 向下挖掘法:对于应用程序,向下挖掘法可以检查应用程序的服务操作作为开始,然后向下至应用程序内部,看看它是如何执行的。对于I/O,向下挖掘的程度可以进入系统库、系统调用,甚至是内核。
  • 锁分析:对于多线程的应用程序,锁可能会成为阻碍并行化和扩展性的瓶颈。锁的分析可以通过检查竞争或者检查过长的持锁时间。第一个要识别的是当前是否有问题。过长的持锁时间并不一定会是问题,但是在将来随着更多的并行负载的加入,可能会产生问题的是试图识别每一个锁的名字(若存在)和通向使用锁的代码路径。
  • 静态性能调优:重点在于环境配置的问题

CPU

CPU 架构

Alt text

CPU 内存缓存

Alt text

正在排队和就绪运行的软件线程数量是一个很重要的性能指标,表示了CPU 的饱和度

对于多处理器系统,内核通常为每个CPU 提供了一个运行队列,并尽量使得线程每次都被放到同一队列之中。这意味着线程更有可能在同一个CPU 上运行,因为CPU 缓存里保存了它们的数据。

时钟是一个驱动所有处理器逻辑的数字信号。每个CPU 指令都可能会花费一个或者多个时钟周期(称为CPU 周期)来执行。CPU 以一个特定的时钟频率执行,例如,一个5GHz 的CPU每秒运行五十亿个时钟周期。

CPU 执行指令集中的指令。一个指令包括以下步骤,每个都由CPU 的一个叫作功能单元的组件处理:
1.指令预取
2.指令解码
3.执行
4.内存访问
5.寄存器写回

指令流水线是一种CPU 架构,通过同时执行不同指令的不同部分,来达到同时执行多个指令的结果

指令宽度描述了同时处理的目标指令数量。现代处理器一般为宽度3 或者宽度4,意味着它们可以在每个周期里最多完成3~4 个指令。

每指令周期数(CPI)是一个很重要的高级指标,用来描述CPU 如何使用它的时钟周期,同时也可以用来理解CPU 使用率的本质。这个指标也可以被表示为每周期指令数(instructions per cycle,IPC),即CPI 的倒数。CPI 较高代表CPU 经常陷入停滞,通常都是在访问内存。而较低的CPI 则代表CPU 基本没有停滞,指令吞吐量较高。内存访问密集的负载,可以通过下面的方法提高性能,如使用更快的内存(DRAM)、提高内存本地性(软件配置),或者减少内存I/O 数量。

CPU 使用率通过测量一段时间内CPU 实例忙于执行工作的时间比例获得,以百分比表示。CPU 使用率的测量包括了所有符合条件活动的时钟周期,包括内存停滞周期。虽然看上去有些违反直觉,但CPU 有可能像前面描述的那样,会因为经常停滞等待I/O 而导致高使用率,而不仅是在执行指令。

CPU 花在执行用户态应用程序代码的时间称为用户时间,而执行内核态代码的时间称为内核时间。内核时间包括系统调用、内核线程和中断的时间。当在整个系统范围内进行测量时,用户时间和内核时间之比揭示了运行的负载类型。

计算密集的应用程序几乎会把大量的时间用在用户态代码上,用户/内核时间之比接近99/1。这类例子有图像处理、基因组学和数据分析。

I/O 密集的应用程序的系统调用频率较高,通过执行内核代码进行I/O 操作。例如,一个进行网络I/O 的Web 服务器的用户/内核时间比大约为70/30。

一个100%使用率的CPU 被称为是饱和的,线程在这种情况下会碰上调度器延时,因为它们需要等待才能在CPU 上运行,降低了总体性能。

允许更高优先级的线程抢占当前正在运行的线程,并开始执行自己。这样节省了更高优先级工作的运行队列延时时间,提高了性能。

优先级反转指的是一个低优先级线程拥有了一项资源,从而阻塞了高优先级线程运行的情况。

多进程 vs 多线程

Alt text

处理器是围绕最大字长设计的——32 位或者64 位——这是整数大小和寄存器宽度。

应用程序在CPU 上的运行时间可以通过编译器选项(包括字长设置)来大幅改进。编译器也频繁地更新以利用最新的CPU 指令集以及其他优化。有时应用程序性能可以通过使用新的编译器显著地提高。

多级缓存是用来取得大小和延时平衡的最佳配置。一级缓存的访问时间一般是几个CPU 时钟周期,而更大的二级缓存大约是几十个时钟周期。主存大概会花上60ns(对于4GHz 处理器大约是240 个周期),而MMU 的地址转译又会增加延时。

内存可能会同时被缓存在不同处理器的多个CPU 里。当一个CPU 修改了内存,所有的缓存需要知道它们的缓存拷贝已经失效,应该被丢弃,这样后续所有的读才会取到新修改的拷贝。这个过程叫做缓存一致性,确保了CPU 永远访问正确的内存状态。这也是设计可扩展多处理器系统里最大的挑战之一,因为内存会被频繁修改。

MMU 负责虚拟地址到物理地址的转换

支撑CPU 的内核软件包括了调度器、调度器类和空闲线程。

调度器功能如下:

  • 分时:可运行线程之间的多任务,优先执行最高优先级任务。
  • 抢占:一旦有高优先级线程变为可运行状态,调度器能够抢占当前运行的线程,这样较高优先级的线程可以马上开始运行。
  • 负载均衡:把可运行的线程移到空闲或者较不繁忙的CPU 队列中。

CPU 分析和调优的方法:

  • 工具法: 对于CPU,工具法可以检查以下项目。

    • uptime:检查负载平均数以确认CPU 负载是随时间上升还是下降。负载平均数超过了CPU 数量通常代表CPU 饱和。
    • vmstat:每秒运行vmstat,然后检查空闲列,看看还有多少余量。少于10%可能是一个问题。
    • mpstat:检查单个热点(繁忙)CPU,挑出一个可能的线程扩展性问题。
    • top/prstat:看看哪个进程和用户是CPU 消耗大户。
    • pidstat/prstat:把CPU 消耗大户分解成用户和系统时间。
    • perf/dtrace/stap/oprofile:从用户时间或者内核时间的角度剖析CPU 使用的堆栈跟踪,以了解为什么使用这么多CPU。
    • perf/cpustat:测量CPI。
  • USE 方法:对于每个CPU,检查以下内容

    • 使用率:CPU 繁忙的时间(未在空闲线程中)。
    • 饱和度:可运行线程排队等待CPU 的程度。
    • 错误:CPU 错误,包括可改正错误。
  • 负载特征归纳:CPU 负载特征归纳的基本属性有:

    • 平均负载(使用率+饱和度)
    • 用户时间与系统时间之比
    • 系统调用频率
    • 自愿上下文切换频率
    • 中断频率
  • 剖析Profiling

  • 周期分析

  • 性能监控:使用率和饱和度

  • 静态性能调优:配置环境的问题

  • 优先级调优:UNIX 一直都提供nice()系统调用,通过设置nice 值以调整进程优先级。正nice 值代表降低进程优先级(更友好),而负值——只能由超级用户(root)设置——代表提高优先级。

  • 资源控制: 操作系统可能为给进程或者进程组分配CPU 资源提供细粒度控制。

  • CPU 绑定:另一个CPU 性能调优的方法是把进程和线程绑定在单个CPU 或者一组CPU 上。这可以增加进程的CPU 缓存温度,提高它的内存I/O 性能。实现方式有进程绑定和独占 CPU 组两种

  • 微型基准测试:操作可能基于下列元素。

    • CPU 指令:整数运算、浮点操作、内存加载和存储、分支和其他指令。
    • 内存访问:调查不同CPU 缓存的延时和主存吞吐量。
    • 高级语言:类似CPU 指令测试,不过使用高级解释或者编译语言编写。
    • 操作系统操作:测试CPU 消耗型系统库和系统调用函数,例如getpid()和进程创建。
  • 扩展:一个基于资源的容量规划简单的扩展方法:
    1.确定目标用户数或者应用程序请求频率。
    2.转化成每用户或每请求CPU 使用率。对于现有系统,CPU 用量可以通过监控获得,再除以现有用户数或者请求数。对于未投入使用系统,负载生成工具可以模拟用户,以获得CPU 用量。
    3.推算出当CPU 资源达到100%使用率时的用户或者请求数。这就是系统的理论上限。

内存

  • 主存:也称为物理内存,描述了计算机的高速数据存储区域,通常是动态随机访问内存(DRAM)。
  • 虚拟内存:一个抽象的主存概念,它(几乎是)无限的和非竞争性的。虚拟内存不是真实的内存。
  • 常驻内存:当前处于主存中的内存。
  • 匿名内存:无文件系统位置或者路径名的内存。它包括进程地址空间的工作数据,称作堆。
  • 地址空间:内存上下文。每个进程和内核都有对应的虚拟地址空间。
  • 段:标记为特殊用途的一块内存区域,例如用来存储可执行或者可写的页。
  • OOM:内存耗尽,内核检测到可用内存低。
  • 页:操作系统和CPU 使用的内存单位。它一直以来是4KB 或者8KB。现代的处理器允许多种页大小以支持更大的页面尺寸。
  • 缺页:无效的内存访问。使用按需虚拟内存时,这是正常事件。
  • 换页:在主存与存储设备间交换页。
  • 交换:源自UNIX,指将整个进程从主存转移到交换设备。Linux 中交换指页面转移到交换设备(迁移交换页)。本书中使用原来的定义,即转移整个进程。
  • 交换(空间):存放换页的匿名数据和交换进程的磁盘空间。它可以是存储设备的一块空间,也称为物理交换设备,或者是文件系统文件,称作交换文件。部分工具用交换这个术语特指虚拟内存(这是令人误解和不正确的)。

文件系统换页由读写位于内存中的映射文件页引发。对于使用文件内存映射(mmap())的应用程序和使用了页缓存的文件系统,这是正常的行为。这也被称作“好的”换页。如果一个文件系统页在主存中修改过(“脏的”),页面换出要求将该页写回磁盘。相反,如果文件系统页没有修改过(“干净的”),因为磁盘已经存在一份副本,页面换出仅仅释放这些内存以便立即重用。

匿名换页牵涉进程的私有数据:进程堆和栈。被称为匿名是由于它在操作系统中缺乏有名字的地址(例如,没有文件系统路径)。匿名页面换出要求迁移数据到物理交换设备或者交换文件。Linux 用交换(swapping)来命名这种类型的换页。

交换是在主存与物理交换设备或者交换文件之间移动整个进程。

利用更大的字长有可能提升内存性能,具体取决于CPU 架构。当一个数据类型在更长的字长下有未使用的位时,可能会浪费一小部分内存。

内存硬件包括主存、总线、CPU 缓存和MMU(内存管理单元)。连接到每个CPU 的内存组被称为内存节点,或者仅仅是节点。基于处理器提供的信息,操作系统能了解内存节点的拓扑。这使得它可以根据内存本地性分配和调度线程,尽可能倾向于本地内存以提高性能。

空闲链表是一个未使用的页列表(也称为空闲内存),它能立刻用于分配。通常的实现是多个空闲页链表,每个本地组(NUMA)一个。在节点的空闲链表内分配能提高内存的本地性和性能。

页扫描仅按需启动。通常平衡的系统不会经常做页扫描并且仅以短期爆发方式扫描。如前所述,基于Solaris 的系统在页扫描前会利用其他机制释放内存,因此若页扫描多于几秒通常是内存压力问题的预兆。

内存调优方法:

  • 工具法
    • 页扫描:寻找连续的页扫描(超过10 秒),它是内存压力的预兆。Linux 中,可以使用sar -B 并检查pgscan 列。在Solaris 中,可以使用vmstat (1M)并检查sr 列。
    • 换页:换页是系统内存低的进一步征兆。Linux 中,可以使用vmstat (8)并检查si和so 列(这里,交换指匿名换页)。Solaris 中,vmstat -p 按类型显示换页,检查匿名换页。
    • vmstat:每秒运行vmstat 检查free 列的可用内存。
    • OOM 终结者:仅对Linux 有效,这些事件可以在系统日志/var/log/messages,或者从dmesg(1)中找到。搜索“Out of memory”。
    • 交换:仅对Solaris 有效,运行vmstat 并检查w 列,它显示交换出的线程,这往往事后才能注意到。要查看即时的交换,用vmstat -S 并检查si 和so。
    • top/prstat:查看哪些进程和用户是(常驻)物理内存和虚拟内存的最大使用者(列名参考Man 手册,不同版本有所变化)。这些工具也会总结内存使用率。
    • dtrace/stap/perf:内存分配的栈跟踪,确认内存使用的原因。
  • USE 方法:要确认物理使用率,需要了解可用内存(free)大小。不同的工具不一定考虑了未被引用的文件系统缓存页或者非活动页,因此它们的报告可能会不同。
    • 使用特征归纳:对于内存,这包括了要求发现内存用于何处以及使用了多少,如下所示。
    • 系统范围的物理和虚拟内存使用率。
    • 饱和度:换页、交换、OOM 终结者。
    • 内核和文件系统缓存使用情况。
    • 每个进程的物理和虚拟内存使用情况。
    • 是否存在内存资源控制。
  • 周期分析:内存总线负载通过检查CPU 性能计数器(CPC)测定,它能被设置用来计算内存停滞周期。
  • 性能监测:关键的内存指标如下。使用率:使用百分比,由可用内存推断。饱和度:换页、交换、OOM 终结者。
  • 泄露检测:内存泄漏:一种类型的软件bug,忘记分配过的内存而没有释放。通过修改软件代码,或应用补丁及进行升级(进而修改代码)能修复。内存增长:软件在正常地消耗内存,远高于系统允许的速率。通过修改软件配置,或者由软件开发人员修改软件内存的消耗方式来进行修复。
  • 静态性能调优:调整各类配置
  • 资源控制:操作系统可能向进程或进程组内存分配提供细粒度控制。这些控制可能会包括使用主存和虚拟内存的固定极限。
  • 微基准测试:微基准测试可用于确定主存的速度和特征,例如CPU 缓存和缓存线长度。它有助于分析系统间的不同。由于应用程序和负载的不同,内存访问速度可能比CPU 时钟速度对性能影响更大。

最重要的内存调优是保证应用程序保留在主存中,并且避免换页和交换经常发生。

文件系统

  • 文件系统:一种把数据组织成文件和目录的存储方式,提供了基于文件的存取接口,并通过文件权限控制访问。另外,还包括一些表示设备、套接字和管道的特殊文件类型,以及包含文件访问时间戳的元数据。
  • 文件系统缓存:主存(通常是DRAM)的一块区域,用来缓存文件系统的内容,可能包含各种数据和元数据。
  • 操作:文件系统的操作是对文件系统的请求,包括read()、write()、open()、close()、stat()、 mkdir()以及其他操作。
  • I/O:输入/输出。文件系统I/O 有好几种定义,这里仅仅指直接读写(执行I/O)的操作,包括read()、write()、stat()(读的统计信息)和mkdir()(创建一个新的目录项)。I/O 不包括open()和close()。
  • 逻辑I/O:由应用程序发给文件系统的I/O。
  • 物理I/O:由文件系统直接发给磁盘的I/O(或者通过裸I/O)。
  • 吞吐量:当前应用程序和文件系统之间的数据传输率,单位是B/s。
  • inode:一个索引节点(inode)是一种含有文件系统对象元数据的数据结构,其中有访问权限、时间戳以及数据指针。
  • VFS:虚拟文件系统,一个为了抽象与支持不同文件系统类型的内核接口。在Solaris上,一个VFS inode 被称为一个vnode。
  • 卷管理器:灵活管理物理存储设备的软件,在设备上创建虚拟卷供操作系统使用。

文件系统延时是文件系统性能一项主要的指标,指的是一个文件系统逻辑请求从开始到结束的时间。它包括了消耗在文件系统、内核磁盘I/O 子系统以及等待磁盘设备——物理I/O 的时间。应用程序的线程通常在请求时阻塞,等待文件系统请求的结束。这种情况下,文件系统的延时与应用程序的性能有着直接和成正比的关系。

文件系统用缓存(caching)提高读性能,而用缓冲(buffering)(在缓存中)提高写性能。

为了平衡系统对于速度和可靠性的需求,文件系统默认采用写回缓存策略,但同时也提供一个同步写的选项绕过这个机制,把数据直接写在磁盘上。

裸I/O:绕过了整个文件系统,直接发给磁盘地址。有些应用程序使用了裸I/O(特别是数据库),因为它们能比文件系统更好地缓存自己的数据。其缺点在于难以管理,即不能使用常用文件系统工具执行备份/恢复和监控。

直接I/O:允许应用程序绕过缓存使用文件系统。直接I/O 可用于备份文件系统的应用程序,防止只读一次的数据污染文件系统缓存。裸I/O和直接I/O 还可以用于那些在进程堆里自建缓存的应用程序,避免了双重缓存的问题。

内存映射通过系统调用mmap()创建,通过munmap()销毁。如果问题在于磁盘设备的高I/O 延时,用mmap()消除小小的系统调用开销,是无济于事的,这时,磁盘设备的高I/O 问题并没有解决并仍在拖累性能。

如果说数据对应了文件和目录的内容,那元数据则对应了有关它们的信息。元数据可能是通过文件系统接口(POSIX)读出的信息,也可能是文件系统实现磁盘布局所需的信息。前者被称为逻辑元数据,后者被称为物理元数据。

下面这个列举步骤的例子描述了应用程序写入一个字节的背后发生了什么:
1.一个应用程序对一个已有的文件发起了一个一字节的写操作。
2.文件系统定位了这个地址对应的128KB 数据块,发现它未在缓存中(尽管指向数据块的元数据被缓存了)。
3.文件系统请求从磁盘载入那个记录块。
4.磁盘设备层把128KB 字节的读请求分拆成适配设备的较小读请求。
5.磁盘执行了多次较小的读请求,总共128KB。
6.文件系统把要写入的那个字节替换成新的数据。
7.一段时间后,文件系统请求把128KB 的“脏”记录写回到磁盘。
8.磁盘写入128KB 的记录(如果有需要还要分拆请求)。
9.文件系统写入新的元数据,比如引用(为了写时复制)或者访问时间。
10.磁盘执行更多的写入操作。
这样,即便应用程序只执行了一个字节的写操作,磁盘也承担了多次读(共128KB)和更多的写(超过128KB)操作。

一些操作的性能数据:
Alt text

页缓存缓存了虚拟内存的页面,包括文件系统的页面,提升了文件和目录的性能。页缓存大小是动态的,它会不断增长消耗可用的内存,并在应用程序需要的时候释放

基于块的文件系统把数据存储在固定大小的块里,被存储在元数据块里的指针所引用。对于大文件,这种方法需要大量的块指针和元数据块,而且数据块的摆放可能会变得零零碎碎,造成随机I/O。有些基于块的文件系统尝试通过把块连续摆放,来解决这个问题。另一个办法是使用变长的块大小,随着文件的增长采用更大的数据块,也能减小元数据的开销。

磁盘

  • 虚拟磁盘:存储设备的模拟。在系统看来,这是一块物理磁盘,但是,它可能由多块磁盘组成。
  • 传输总线:用来通信的物理总线,包括数据传输(I/O)以及其他磁盘命令。
  • 扇区:磁盘上的一个存储块,通常是512B 大小。
  • I/O:对于磁盘,严格地说仅仅包括读和写,而不包括其他磁盘命令。I/O 至少由方向(读或写)、磁盘地址(位置)和大小(字节数)组成。
  • 磁盘命令:除了读写之外,磁盘还会被指派执行其他非数据传输的命令(例如缓存写回)。
  • 吞吐量:对于磁盘而言,吞吐量通常指当前数据传输速率,单位是B/s。
  • 带宽:这是存储传输或者控制器能够达到的最大数据传输速率。
  • I/O 延时:一个I/O 操作的执行时间,这个词在操作系统领域广泛使用,早已超出了设备层。注意在网络领域,这个词有其他的意思,指发起一个I/O 的延时,后面还跟着数据传输时间。
  • 延时离群点:非同寻常的高延时磁盘I/O。

I/O 等待是针对单个CPU 的性能指标,表示当CPU 分发队列(在睡眠态)里有线程被阻塞在磁盘I/O 上时消耗的空闲时间。

磁盘检查工具:

  • iostat:使用扩展模式寻找繁忙磁盘(超过60%使用率),较高的平均服务时间(超过大概10ms),以及高IOPS(可能)。
  • iotop:发现哪个进程引发了磁盘I/O。
  • dtrace/stap/perf:包含了iosnoop(1)工具,仔细检查磁盘I/O 延时,以发现延时离群点(超过大概100ms)。
  • 磁盘控制器专用工具(厂商提供)。

网络

包的长度通常受限于网络接口的最大传输单元(MTU)长度,许多以太网中它设置为1500B。以太网支持接近9000B 的特大包(帧),也称为巨型帧。

较大的缓冲可以通过在阻塞和等待确认前持续传输数据缓解高往返延时带来的影响。

全双工模式允许双向同时传输,利用分离的通道传输和接收能利用全部带宽。半双工模式仅允许单向的传输。

现代内核中网络栈是多线程的,并且传入的包能被多个CPU 处理。传入的包与CPU 的映射可用多个方法完成:可能基于源IP 地址哈希以平均分布负载,或者基于最近处理的CPU 以有效利用CPU 缓存热度以及内存本地性。

对于网络通信来说,应用工具法可以检查如下内容。

  • netstat -s:查找高流量的重新传输和乱序数据包。哪些是“高”重新传输率依客户机而不同,面向互联网的系统因具有不稳定的远程客户会比仅拥有同数据中心客户的内部系统具有更高的重新传输率。
  • netstat -i:检查接口的错误计数器(特定的计数器依OS 版本而不同)。
  • ifconfig(仅限Linux 版本):检查“错误”、“丢弃”、“超限”。
  • 吞吐量:检查传输和接收的字节率-在Linux 中用ip(8),在Solaris 中用nicstat(1)或者dladm(1M)。高吞吐量可能会因为到达协商的线速率而受到限制,它也可能导致系统中网络用户的竞争及延时。
  • tcpdump/snoop:尽管需要大量的CPU 开销,短期使用可能就足以发现谁在使用网络并且定位可以消除的不必要的操作。
  • dtrace/stap/perf:用来检查包括内核状态在内的应用程序与线路间选中的数据。

云计算

OS 虚拟化将操作系统划分为形同分隔的访客服务器且能独立于宿主管理和重启的实例。相比硬件虚拟化技术,一个关键区别是仅有一个内核在运行。它有如下优势:

  • 由于客户应用程序能直接向宿主内核发起系统调用,客户应用程序I/O 仅有一些甚至没有性能开销。
  • 分配给客户的内存能完全用于客户应用程序——而没有来自OS 虚拟层或者其他访客内核的额外内核负担。
  • 只有一个统一的文件系统缓存——没有宿主和访客的双重缓存。
  • 所有的客户进程都可由宿主观测到,这样可以调试牵涉到它们之间的相互作用(包括资源竞争)的性能问题。
  • CPU 是真实的CPU,自适应互斥锁的假设仍然有效。
    而劣势为:
  • 任何kernel panic 会影响到所有客户。
  • 客户不能运行不同的内核版本。

硬件虚拟化创建系统虚拟机实例,它们能运行包括自己内核在内的整个操作系统。硬件虚拟化包括如下类型。

  • 全虚拟化——二进制翻译:提供一个由虚拟硬件组件组成的完整虚拟系统。在此之上可以安装未修改的操作系统。它由VMware 于1998年在x86 平台上始创,混合利用了直接处理器执行和按需翻译二进制指令。相对于服务器整合带来的节省,它的性能开销通常是可接受的。
  • 全虚拟化——硬件支持:提供一个由虚拟组件组成的完整虚拟系统。在此之上可以安装未修改的操作系统。它利用处理器的支持以更有效地执行虚拟机,特别是2005-2006年引入的AMD-V 和Intel VT-x 扩展。
  • 半虚拟化:提供了包括使得访客机操作系统更有效地使用宿主资源的接口(通过hypercalls)的一个虚拟系统,而不需要包括所有组件的完整虚拟化。例如,设置计时器通常需要多个必须由虚拟层模拟的特权指令。对于一个半虚拟化的客户机,这能被简化为单个超级调用。半虚拟化可能会利用访客机的半虚拟网络设备驱动将数据包更高效地传递给宿主的物理网络接口。尽管性能有所提升,但它依赖于客户OS 对半虚拟化的支持(Windows 一直以来不提供)。

另一类硬件虚拟化,混合虚拟化,同时利用硬件辅助的虚拟化和更高效的半虚拟化调用,以期提供最佳性能。最常见的半虚拟化的目标是如网络板卡和存储控制器等的虚拟设备。

Alt text

图中展示了两种类型的虚拟机管理程序:

  • 1 型 直接运行于处理器上,并且不属于其他宿主的内核或用户软件。虚拟机管理程序的管理可以由一个特权访客机实现(这里描绘为系统中的第一个:0 号)。它能创建和启动新的访客机。1 型也称为本地虚拟机管理程序或者裸机虚拟机管理程序。这类虚拟机管理程序包括了自带的用于客户VM 的CPU 调度器。
  • 2 型 由宿主OS 内核运行并且可能由内核级模块和用户级进程组成。宿主OS 有管理虚拟机管理程序和启动新访客机的特权。这类虚拟机管理程序由宿主内核调度器调度。

虚拟化技术的比较:

Alt text