我们常常将系统等同于具象的技术实体,例如某种编程语言,OS内核,网络,总觉得系统出问题肯定是我某些技术知识有漏洞没学好。可惜“学海无涯,而吾生有涯,以有涯随无涯,殆矣”,拘泥于各种眼花缭乱的的技术只能让自己迷失造成时间的浪费,技术都是末节,真正要把握的主体其实只是系统本身。
术语
IOPS:每秒发生的输入/输出操作次数,是数据传输的一个度量方法。对于磁盘的读写,IOPS指的是每秒读和写的次数。
吞吐量:评价工作执行的速率。尤其在数据传输方面,这个术语用于描述数据传输速度(字节/秒或比特/秒)。在某些情况下(如数据库),吞吐量指的是操作的速度(每秒操作数或每秒业务数)
响应时间:一次操作完成的时间,包括用于等待和服务的时间,也包括用来返回结果的时间。
延时:延时是描述操作里用来等待服务的时间。在某些情况下,它可以指的是整个操作时间,等同于响应时间。
使用率:对于服务所请求的资源,使用率描述在所给定的时间区间内资源的繁忙程度。对于存储资源来说,使用率指的就是所消耗的存储容量(例如,内存使用率)
饱和度:指的是某一资源无法满足服务的排队工作量。
瓶颈:在系统性能里,瓶颈指的就是限制系统性能的那个资源。分辨和移除系统瓶颈是系统性能的一项重要工作。
工作负载:系统的输入或者是对系统所施加的负载叫做工作负载。对于数据库来说,工作负载就是客户端发出的数据库请求和命令。
缓存:用于复制或者缓冲一定量数据的高速存储区域,目的是为了避免对较慢的存储层级的直接访问,从而提高性能。出于经济考虑,缓存区的容量要比更慢一级的存储容量要小。
操作系统:这里指的是安装在系统上的软件和文件,使得系统可以启动和运行程序。操作系统包括内核、管理工具,以及系统库。
内核:内核是管理系统的程序,包括设备(硬件)、内存和CPU 调度。它运行在CPU的特权模式,允许直接访问硬件,称为内核态。
进程:是一个OS 的抽象概念,是用来执行程序的环境。程序通常运行在用户模式,通过系统调用或自陷来进入内核模式
自陷:信号发送到内核,请求执行一段系统程序(特权操作)。自陷类型包括系统调用、处理器异常,以及中断。
中断:由物理设备发送给内核的信号,通常是请求I/O 服务。中断是自陷的一种类型。
主存:也称为物理内存,描述了计算机的高速数据存储区域,通常是动态随机访问内存(DRAM)
虚拟内存:一个抽象的主存概念,它(几乎是)无限的和非竞争性的。虚拟内存不是真实的内存
常驻内存:当前驻入主存中的内存
地址空间:内存上下文。每个进程和内核都有对应的虚拟地址空间
段:标记为特殊用途的一块内存区域,例如用来存储可执行或者可写的页
OOM:内存耗尽,内核检测到可用内存低
页:操作系统和CPU使用的内存单位,它一直以来是4KB或者16KB,现代的处理器允许多种页大小以支持更大的页面尺寸。
缺页:无效的内存访问,使用按需虚拟内存时,这是正常事件
换页:在主存和存储设备之间交换页
交换:源自UNIX,指将整个进程从主存转移到交换设备。Linux中交换指页面转移到交换设备(迁移交换页)。
交换(空间):存放换页的匿名数据和交换进程的磁盘空间。它可以是存储设备的一块空间,也称为物理交换设备,或者是文件系统文件,称为交换文件。
文件系统:一种把数据组织成文件和目录的存储方式,提供了基于文件的存取接口,并通过文件权限控制访问。另外,还包括一些表示设备,套接字和管道的特殊文件类型,以及包含文件访问时间戳的元数据。
文件系统缓存:主存(通常是DRAM)的一块区域,用来缓存文件系统的内容,可能包含各种数据和元数据。
I/O:输入/输出。
逻辑I/O:由应用程序发给文件系统的I/O
物理I/O:由应用程序直接发给磁盘的I/O(或者通过裸I/O)
innode:一个索引节点(inode)是一种包含文件系统对象元数据的数据结构,其中有访问权限,时间戳以及数据指针。
VFS:虚拟文件系统,一个为了抽象与支持不同文件系统类型的内核接口。
卷管理器:灵活管理物理存储设备的软件,在设备上创建虚拟卷供操作系统使用。
传输总线:用来通信的物理总线,包括数据传输(I/O)以及其他磁盘命令
带宽:这是存储传输或者控制器能够达到的最大数据传输速率。
数据包:术语“数据包”通常指IP 级可路由的报文。
帧:一个物理网络级的报文,例如以太网帧。
带宽:对应网络类型的最大数据传输率,通常以b/s 为单位测量。10GbE 是带宽10Gb/s的以太网。
处理器:插到系统插槽或者处理器板上的物理芯片,以核或者硬件线程的方式包含了一块或者多块CPU。
核:一颗多核处理器上的一个独立CPU 实例。核的使用是处理器扩展的一种方式,又称为芯片级多处理(chip-level multiprocessing,CMP)
硬件线程:一种支持在一个核上同时执行多个线程(包括Intel 的超线程技术)的CPU架构,每个线程是一个独立的CPU 实例。这种扩展的方法又称为多线程。
CPU 指令:单个CPU 操作,来源于它的指令集。指令用于算术操作、内存I/O,以及逻辑控制。
逻辑CPU:又称为虚拟处理器,一个操作系统CPU 的实例(一个可调度的CPU 实体)。处理器可以通过硬件线程(这种情况下又称为虚拟核)、一个核,或者一个单核的处理器实现。
调度器:把CPU 分配给线程运行的内核子系统。
运行队列:一个等待CPU 服务的可运行线程队列。在Solaris 上常被称为分发器队列。
延时
延时测量的是用于等待的时间。广义来说,它可以表示所有操作完成的耗时。例如,一次应用程序请求,一次数据库查询,一次文件系统操作,等等。举个例子,延时可以表示从点击链接到屏幕显示整个网页加载完成的时间。这是一个对于客户和网站提供商来说都非常重要的指标:高延时会令人沮丧,客户可能会选择别处开展业务。
作为一个指标,延时可以估计最大增速。举个例子,下图显示了一次数据库访问需要100ms的时间(这就是延迟),其中80ms的阻塞是等待磁盘读取,通过减少磁盘读取时间(如通过使用缓存)可以达到最好的性能提升,并且可以计算出结果是五倍速(5x)。这就是估计出的增速,而且该计算还可以对性能问题做量化:是磁盘读取让查询慢了5 倍
若以网络作为讨论背景,延时指的是建立一个连接的时间,而不是数据传输的时间。
动态跟踪
动态跟踪技术把所有的软件变得可以监控,而且能用在真实的生产环境中。这项技术利用内存中的CPU 指令并在这些指令之上动态构建监测数据。这样从任何运行的软件中都可以获得定制化的性能统计数据,从而提供了远超系统的自带统计所能给予的观测性。从前因为不易观测而无法处理的问题变得可以解决。从前可以解决而难以解决的问题,现在也往往可以得以简化。
动态跟踪与传统的观测方法相比是如此不同,甚至让人很难一开始就抓住动态跟踪的要领。以操作系统内核举例:分析内核好比闯进了一间黑屋,拿着蜡烛(系统自带统计)去照亮内核工程师他们觉得需要照亮的地方,而动态跟踪则像是手电筒,你可以指哪儿亮哪儿。
DTrace 是第一个适用于生产环境的动态跟踪工具,它提供了许多其他的功能,甚至包括一套自用的编程语言,D 语言。
权衡三角
许多的IT项目都选择了及时和便宜,留下了性能问题在以后的路上解决。当早期的决定阻碍了性能提高的可能性,这样的选择会变得有问题。例如,选择了非最优的存储架构,或者使用的编程语言或操作系统缺乏完善的性能分析工具。
一个常见的性能调整的权衡是在CPU和内存之间,因为内存能用于缓存数据结果,降低CPU的使用。在有着充足CPU资源的现代系统里,交换可以反向进行:CPU可以压缩数据来降低内存的使用。
调整的影响
性能调整发生在越靠近工作执行的地方效果最显著。对于工作负载驱动的应用程序,所执行的工作就是应用程序本身。
应用程序层级的调整,可以通过消去或减少数据库查询获得很大的性能提升(例如,20倍)。在存储设备层级做调整,也可以精简或提高存储I/O,但是性能提升的大头是在更高层级的系统栈代码,所以存储设备层级的调整能够达到的应用程序性能提升有限,是百分比量级(例如,20%)。
性能建议的时间点
环境的性能特性会随着时间改变,更多的用户,新的硬件,升级的软件或固件都是变化的因素。一个环境,受限于1Gb/s网络基础设施,当升级到10Gb/s时,很可能发生磁盘或CPU的性能变得紧张。
性能推荐,尤其是可调整的参数值,仅仅在一段特定时间内有效。一周内从性能专家那里得到的好建议,可能到下一周,经过一次软件或硬件升级,或者用户增多后就无效了。
通过网上搜索找到的可调参数值对于某些情况能快速见效,如果它们对于你的系统或者工作负载并不合适,它们也会损耗性能,可能合适过一次,就不合适了,或者只是作为软件的某个bug修复升级之前暂时的应急措施。这和从别人的医药箱里拿药吃很像,那些药可能不适合你,或者可能已经过期了,或者只适合短期服用。
已知的未知
已知的已知、已知的未知、未知的未知在性能领域是很重要的概念。下面是详细的解释,并有系统性能分析例子。
- 已知的已知:有些东西你知道。你知道你应该检查性能指标,你也知道它的当前值。举个例子,你知道你应该检查CPU 使用率,而且你也知道当前均值是10%。
- 已知的未知:有些东西你知道你不知道。你知道你可以检查一个指标或者判断一个子系统是否存在,但是你还没去做。举个例子,你知道你能用profiling 检查是什么致使CPU 忙碌,但你还没去做这件事。
- 未知的未知:有些东西你不知道你不知道。举个例子,你可能不知道设备中断可以消耗大量CPU资源,因此你对此并不做检查。
性能这块领域是“你知道的越多,你不知道的也就越多”。这和学习系统是一样的原理:你了解的越多,你就能意识到未知的未知就越多,然后这些未知的未知会变成你可以去查看的已知的未知。
饱和度
随着工作量增加而对资源的请求超过资源所能处理的程度叫做饱和度。饱和度发生在100%使用率时(基于容量),这时多出的工作无法处理,开始排队。下图描绘了这种情况。
随着负载的持续上升,图中的饱和度在超过基于容量的使用率100%的标记后线性增长。因为时间花在了等待(延时)上,所以任何程度的饱和度都是性能问题。对于基于时间的使用率(忙碌百分比),排队和饱和度可能不发生在100%使用率时,这取决于资源处理任务的并行能力。
缓存
缓存被频繁使用来提高性能,缓存是将较慢的存储层的结果存放在较快的存储层中。把磁盘的块缓存在主内存(RAM)就是一例。
一个了解缓存的性能的重要指标是每个缓存的命中率—-所需数据在缓存中找到的次数(hits,命中)相比于没有找到的次数(misses,失效)。
命中率 = 命中次数/(命中次数 + 失效次数)
命中率越高越好,更高的命中率意味着更多的数据能成功地从较快的介质中访问获得。
下面我们将介绍几种系统性能分析和性能调整的方法和步骤
1:问题陈述法
明确问题如何陈述是支持人员开始反映问题时的例行工作。通过询问客户以下问题来完成:
- 是什么让你认为存在性能问题?
- 系统之前运行得好吗?
- 最近有什么改动?软件?硬件?负载?
- 问题能用延时或者运行时间来表述吗?
- 问题影响其他的人或应用程序吗(或者仅仅影响的事你)?
- 环境是怎样的?用了什么软件和硬件?是什么版本?是怎样的配置。
询问这些问题并得到相应的回答通常会立即指向一个根源和解决方案。因此问题陈述法作为独立的方法收录在此处,而且当你应对一个新的问题时,首先应该使用的就是这个方法。
2:Ad Hoc 核对清单法
当需要检查和调试系统时,技术支持人员通常会花一点时间一步步地过一遍核对清单。一个典型的场景,在产品环境部署新的服务器或应用时,技术支持人员会花半天的时间来检查一遍系统在真实压力下的常见问题。该类核对清单是Ad Hoc 的,基于对该系统类型的经验和之前所遇到的问题。
举个例子,这是核对清单中的一项:
运行iostat -x 1 检查await 列。如果该列在负载下持续超过10(ms),那么说明磁盘太慢或是磁盘过载。
一份核对清单会包含很多这样的检查项目。
这类清单能在最短的时间内提供最大的价值,是即时建议而且需要频繁更新以保证反映当前状态。这类清单处理的多是修复方法容易记录的问题,例如设置可调参数,而不是针对源代码或环境做定制的修复。
如果你管理一个技术支持的专业团队,Ad Hoc 核对清单能有效保证所有人都知道如何检查最糟糕的问题,能覆盖到所有显而易见的问题。核对清单能够写得清楚而又规范,说明了如何辨别每一个问题和如何做修复。不过当然,这个清单应该时常保持更新。
3:工具法
工具为导向的方向如下:
- 列出可用到的性能工具(可选的,安装的或者可购买的)
- 对于每一个工具,列出它提供的有用的指标
- 对于每一个指标,列出阐释该指标可能的规则
在实践中,工具法确实在一定程度上辨别出了资源的瓶颈、错误,以及其他类型的问题,但通常不太高效。
当大量的工具和指标可被选用时,逐个枚举是很耗时的。当多个工具有相同的功能时,情况更糟。你要花额外的时间来了解每个工具的优缺点。在某些情况下,比如要选择做文件系统微基准的工具的场合,工具相当多,虽然这时你只需要一个。
4:USE 方法
USE 方法(utilization、saturation、errors)应用于性能研究,用来识别系统瓶颈[Gregg 13]。一言以蔽之,就是:
对于所有的资源,查看它的使用率、饱和度和错误。
这些术语定义如下。
- 资源:所有服务器物理元器件(CPU、总线……)。某些软件资源也能算在内,提供有用的指标。
- 使用率:在规定的时间间隔内,资源用于服务工作的时间百分比。虽然资源繁忙,但是资源还有能力接受更多的工作,不能接受更多工作的程度被视为饱和度
- 饱和度:资源不能再服务更多额外工作的程度,通常有等待队列。
- 错误:错误事件的个数。
与工具法相反的是,USE 方法列举的是系统资源而不是工具。这帮助你得到一张完整的问题列表,在你寻找工具的时候做确认。即便对于有些问题现有的工具没有答案,但这些问题所蕴含的知识对于性能分析也是极其有用的。
USE 方法会将分析引导到一定数量的关键指标上,这样可以尽快地核实所有的系统资源。在此之后,如果还没有找到问题,那么可以考虑采用其他的方法。
下图描绘了USE 方法的流程图。错误被置于检查首位,要先于使用率和饱和度。错误通常容易很快被解释,在研究其他指标之前,把它们梳理清楚是省时高效的。
USE 方法的指标通常如下。
- 使用率:一定时间间隔内的百分比值(例如,“单个CPU 运行在90%的使用率上”)。
- 饱和度:等待队列的长度(例如,“CPU 的平均运行队列长度是4”)。
- 错误:报告出的错误数目(例如,“这个网络接口发生了50 次滞后冲突”)。
想象一下高速公路的收费站。使用率就相当于有多少收费站在忙于收费。使用率100%意味着你找不到一个空的收费站,必须排在别人的后面(饱和的情况)。如果我说一整天收费站的使用率是40%,你能判断当天是否有车在某一时间排过队吗?很可能在高峰时候确实排过队,那时的使用率是100%,但是这在一天的均值上是看不出的。
5:延时分析
延时分析检查完成一项操作所用的时间,然后把时间再分成小的时间段,接着对有着最大延时的时间段做再次的划分,最后定位并量化问题的根本原因。
分析可以从所施加的工作负载开始,检查工作负载是如何在应用程序中处理的,然后深入到操作系统的库、系统调用、内核以及设备驱动。
举个例子,MySQL 的请求延时分析可能涉及下列问题的回答:
- 存在请求延时问题吗?(是的)
- 请求时间大量地花在CPU 上吗?(不在CPU 上)
- 不花在CPU 上的时间在等待什么?(文件系统I/O)
- 文件系统的I/O 时间是花在磁盘I/O 上还是锁竞争上?(磁盘I/O)
- 磁盘I/O 时间主要是随机寻址的时间还是数据传输的时间?(数据传输时间)
对于这个问题,每一步所提出的问题都将延时划分成了两个部分,然后继续分析那个较大可能的部分:延时的二分搜索法,你可以这么理解。整个过程见下图:
一旦识别出A 和B 中较慢的那个,就可以对其做进一步的分析和划分,依此类推。
扩展方案
要满足更高的性能需要,常常意味着建立更大的系统,这种策略叫做垂直扩展(vertical scaling)。把负载分散给许许多多的系统,在这些系统前面放置负载均衡器(load balancer),让这些系统看起来像是一个,这种策略叫做水平扩展(horizontal scaling)。
内核态
内核是唯一运行在特殊CPU模式的的程序,这一特殊的CPU模式叫做内核态,在这一状态下,设置的一切访问以及特权指令的指令都是被允许的。由内核来控制设备的访问,用以支持多任务处理,除非明确允许,否则进程之间和用户之间的数据是无法彼此访问的。
用户程序(进程)运行在用户态,对于内核特权操作(例如I/O)的请求是通过系统调用传递的。执行系统操作,执行会做上下文切换从用户态到内核态,然后用更高的特权级别执行,如下图所示
无论是用户态还是内核态,都有自己的软件执行上下文,包括栈和寄存器。在用户态执行特权指令会引起异常,这会由内核来妥善处理。
在这些状态切换上下文是会耗时的(CPU 周期),这对每次I/O 都增加了一小部分的时间开销。有些服务,如NFS,会用内核态的软件来进行实现(而不是用户态的守护进程),这样从设备来回执行I/O 的时候才无须上下文切换到用户态。
上下文切换也会发生在不同进程之间,例如CPU 调度时。
中断和中断线程
除了响应系统调用外,内核也要响应设备的服务请求,这称为中断,它会中断当前的执行,如下图所示。
中断服务程序(interrupt service routine)需要通过注册来处理设备中断。这类程序的设计要点是需要运行得尽可能快,以减少对活动线程中断的影响。如果中断要做的工作不少,尤其是还可能被锁阻塞,那么最好用中断线程来处理,由内核来调度。
进程
进程是用于执行用户级别程序的环境。它包括内存地址空间,文件描述符,线程栈和寄存器。从某种意义上来说,进程像是一台早期电脑的虚拟化,里面只有一个程序在执行,用着自己的寄存器和栈。
进程可以让内核进行多任务处理,使得在一个系统中可以执行着上千个进程。每一个进程用它们的进程ID(process ID,PID)做识别,每一个PID都是唯一的数字标示符。
一个进程中可以包含一个或多个线程,操作在进程的地址空间内并且共享着一样的文件描述符(标识打开文件的状态)。线程是一个可执行的上下文,包括栈,寄存器,以及程序计数器。多线程让单一进程可以在多个CPU上并发地执行。
虚拟内存
虚拟内存是主存的抽象,提供进程和内核,它们自己的近乎是无穷的和私有的主存视野。支持多任务处理,允许进程和内核在它们自己的私有地址空间做操作而不用担心任何竞争。它还支持主存的超额使用,如果需要,操作系统可以将虚拟内存在主存和二级存储(磁盘)之间映射。
下图显示的是虚拟内存的作用。一级存储是主存(RAM),二级存储是存储设备(磁盘)。
是处理器和操作系统的支持使得虚拟内存成为可能,它并不是真实的内存。多数操作系统仅仅在需要的时候将虚拟内存映射到真实内存上,即当内存首次被填充(写入)时。
调度器
UNIX 及其衍生的系统都是分时系统,通过划分执行时间,让多个进程同时运行。进程在处理器上和CPU 间的调度是由调度器完成的,这是操作系统内核的关键组件。
下图 展示了调度器的作用,调度器操作线程(Linux 中是任务(task)),并将它们映射到CPU 上。
调度器基本的意图是将CPU 时间划分给活跃的进程和线程,而且维护一套优先级的机制,这样更重要的工作可以更快地执行。
调度器会跟踪所有处于ready-to-run 状态的线程,传统意义上每一个优先级队列都称为运行队列 [Bach 86]。现代内核会为每个CPU 实现这些队列,也可以用除了队列以外的其他数据结构来跟踪线程。当需要运行的线程多于可用的CPU 数目时,低优先级的线程会等待直到轮到自己。多数的内核线程运行的优先级要比用户级别的优先级高。
调度器可以动态地修改进程的优先级以提升特定工作负载的性能。工作负载可以做以下分类。
- CPU 密集型:应用程序执行繁重的计算,例如,科学和数学分析,通常运行时间较长(秒、分钟、小时)。这些会受到CPU 资源的限制。
- I/O 密集型:应用程序执行I/O,计算不多,例如,Web 服务器、文件服务器,以及交互的shell,这些需要的是低延时的响应。当负载增加时,会受到存储I/O 或网络资源的限制。
调度器能够识别CPU 密集型的进程并降低它们的优先级,可以让I/O 密集型工作负载(需要低延时响应)更快地运行。计算最近的计算时间(在CPU 上执行时间)与真实时间(逝去时间)的比例,通过降低高(计算)比例的进程的优先级就可以达到这一目的[Thompson 78]。这一机制更优先选择那些经常执行I/O 的短时运行进程,包括与人类交互的进程在内。
现代内核支持多类别调度,对优先级和可运行线程的管理实行不同的算法。其中包括实时调度类别,该类别的优先级要高于所有非关键工作的优先级(甚至包括内核线程)。还有抢占的支持(稍后会讲述),实时调度级别对实时系统提供低延时的调度。
VFS
虚拟文件系统(virtual file system,VFS)是一个对文件系统类型做抽象的内核界面,起源于SunMicrosystems 公司,最初的目的是让UNIX 文件系统(UFS)和NFS 能更容易地共存。VFS 的作用见下图
网络
现代内核提供一套内置的网络协议栈,能够让系统用网络进行通信,成为分布式系统环境的一部分。栈指的是TCP/IP栈。这个命名源自最常用的TCP协议和IP协议。用户级别应用程序通过称为套接字的编程端点跨网络通信。
连接网络的物理设备是网络接口,一般使用网络接口卡(network interface card,NIC)。系统管理员的一个常规操作就是把IP地址关联到网络接口上,这样才能用网络进行通信。
计数器
内核维护了各种统计数据,称为计数器,用于对事件计数。通常计数器实现为无符号的整型数,发生事件时递增。例如,有网络包接收的计数器,有磁盘I/O发生的计数器,也有调用执行的计数器。
计数器的使用可以认为是“零开销”的,因为它们默认就是开启的,而且始终由内核维护。唯一的使用开销是从用户空间读取它们的时候(可以忽略不计),下面介绍的两类工具的读取分别是系统级别和进程级别的。
系统级别
- vmstat : 虚拟内存和物理内存的统计,系统级别。
- mpstat:每个CPU的使用情况
- iostat:每个磁盘I/O的使用情况,由块设备接口报告
- netstat:网络接口的统计,TCP/IP栈的统计,以及每个连接的一些统计信息
- sar:各种各样的统计,能归档历史数据
这些工具有一个使用惯例,即可选时间间隔和次数。例如vmstat 1 3 表示用一秒作为时间间隔,输出三次。
进程级别
- ps:进程状态,显示进程各种统计信息,包括内存和CPU的使用
- top:按一个统计数据(如CPU)排序,显示排名高的进程。
- pmap:将进程的内存段和使用统计一起列出
一般来说,上述这些工具是从/proc文件系统里读取统计信息的。
/proc
这是一个提供内核统计信息的文件系统接口。/proc 包含很多的目录,其中以进程ID 命名的目录代表的就是那个进程。
/proc 由内核动态创建,不需要任何存储设备(在内存中运行)。多数文件是只读的,为观测工具提供统计数据。一部分文件是可写的,用于控制进程和内核的行为。
在/proc 下有各种进程统计的文件。下面例子里文件就是你可能看到的:
具体可用的文件列表取决于内核的版本和内核的CONFIG 选项。与进程性能观测相关的文件如下。
- limits:实际的资源限制。
- maps:映射的内存区域。
- sched:CPU 调度器的各种统计。
- schedstat:CPU 运行时间、延时和时间分片。
- smaps:映射内存区域的使用统计。
- stat:进程状态和统计,包括总的CPU 和内存的使用情况。
- statm:以页为单位的内存使用总结。
- status:stat 和statm 的信息,用户可读。
- task:每个任务的统计目录。
设立性能目标
设立性能目标能为你的性能分析工作指明方向,并帮助你选择要做的事情。没有清晰的目的,性能分析容易沦为随机的”钓鱼探险“。
关于应用程序的性能,可以从应用程序执行什么操作和要实现怎样的性能目标入手。目标可能如下:
- 延迟:低应用程序响应时间
- 吞吐量:高应用程序操作率或者数据传输率
- 资源使用率:对于给定应用程序工作负载,高效地使用资源
如果上述目标可量化,就更好了,用从业务或者服务质量需求衍生出的指标做量化,例如:
- 应用程序平均延时5ms
- 95%的请求的延时在100ms或以下
- 消灭延迟异常值,超过1000ms延时的请求数为零
- 最大吞吐量为每台服务器最少10000次应用请求/秒
- 在每秒10000次应用请求的情况下,平均磁盘使用率在50%以下
一旦选中一个目标,你就能着手处理阻碍该目标实现的限制因素了。
应用程序性能技术
1:选择I/O尺寸
增加I/O尺寸是应用程序提高吞吐量的常用策略。考虑到每次I/O的固定开销,一次I/O传输128KB要比128次传输1KB高效得多,尤其是磁盘I/O,由于寻道时间,每次I/O开销都很高。
如果应用程序不需要,更大的I/O尺寸也会带来负面效应。一个执行8KB随机读取的数据库按128KBI/O的尺寸运行会慢得多,因为128KB的数据传输能力被浪费了。选择小一些的I/O尺寸,更贴近应用程序所需,能降低引起的I/O延时,不必要的大尺寸I/O还会浪费缓存的空间。
2:轮询
轮询是系统等到某一事件发生的技术,该技术在循环中检查事件状态,两次检查之间有停顿。轮询有一些潜在的性能问题:
- 重复检查的CPU,开销高昂
- 事件发生和下一次检查的延时较高
这是性能问题,应用程序应能改变自身行为来监听事件发生,当事件发生时立即通知应用程序并执行相应的例程。
poll()系统调用
有系统调用poll()来检查文件描述符的状态,提供与轮询相似的功能,不过它是基于事件的,因此没有轮询那样的性能负担。
poll()接口支持多个文件描述符作为一个数组,当事件发生要找到相应的文件描述符时,需要应用程序扫描这个数组。这个扫描是O(n),扩展时可能会变成一个性能问题:在Linux 里是epoll(),epoll()避免了这种扫描,复杂度是O(1)
3:并行和并发
分时系统(包括所有从UNIX 衍生的系统)支持程序的并发:装载和开始执行多个可运行程序的能力。虽然它们的运行时间是重叠的,但并不一定在同一瞬间都在CPU 上执行。每一个这样的程序都可以是一个应用程序进程。
为了利用多处理器系统的优势,应用程序需要在同一时间运行在多颗CPU 上。这称为并行,应用程序通过多进程或多线程实现。多线程(或多任务)更为高效,因此也是首选的方法
除了增加CPU 工作的吞吐量,多线程(或多进程)让I/O 可以并发执行,当一个线程阻塞在I/O 等待的时候,其他线程还能执行。
同步原语
同步原语(synchronization primitive)监管内存的访问,与交通灯控制十字路口的访问方式相同,正如红绿灯一样,它们停止交通的流动,引起等待时间(延时)。
常见的三种类型如下:
- mutex(MUTually EX clusive)锁:只有锁持有者才能操作,其他线程会阻塞并等待CPU
- 自旋锁:自旋锁允许锁持有者操作,其他的需要自旋锁的线程会在CPU 上循环自旋,检查锁是否被释放。虽然这样可以提供低延时的访问,被阻塞的线程不会离开CPU,时刻准备着运行直到锁可用,但是线程自旋、等待也是对CPU 资源的浪费。
- 读写锁:读/写锁通过允许多个读者或者只允许一个写者而没有读者,来保证数据的完整性。
4:非阻塞I/O
- 对于多路并发的I/O,当阻塞时,每一个阻塞的I/O都会消耗一个线程(或进程)。为了支持多路并发I/O,应用程序必须创建很多的线程(通常一个客户端一个线程),伴随着线程的创建和销毁,这样做的代价也很大。
- 对于频繁发生的短时I/O,频繁切换上下文的开销会消耗CPU资源并增加应用程序的延时。
非阻塞I/O模型是异步地发起I/O,而不阻塞当前的线程,线程可以执行其他的工作。
编译语言和解释语言
编译是在运行之前将程序生成机器指令,保存在二进制可执行文件里。这些文件可以在任何时间运行而无须再度编译。编译语言包括C 和C++。还有些语言既有解释器又有编译器。
编译过的代码总体来说是高性能的,在被CPU 执行之前不需要进一步转换。操作系统内核几乎全部都是用C 写就的,只有一些关键的路径是用汇编写。
解释语言程序的执行是将语言在运行时翻译成行为,这一过程会增加执行的开销。解释语言并不期望能表现出很高的性能,而是用于其他因素更重要的情况下,诸如易于编程和调试。shell 脚本就是解释语言的一个例子。
除非提供了专门的观测工具,否则对解释语言做性能分析是很困难的。CPU 剖析能展示解释器的操作——包括分句、翻译和执行操作——但是不能显示原始程序的函数名,关键程序的上下文仍然是个迷。
strace命令
在Linux 上,用strace(1)命令。例如:
此处用到的选项如下:
- -ttt: 打印第一栏UNIX 时间戳,以秒为单位,精确度可到毫秒级
- -T:输出最后的一栏(),这是系统调用的用时,以秒为单位,精确度到毫秒级。
- -p PID:跟踪这个PID 的进程。strace(1)还可以指定某一命令做跟踪。
strace(1)的一个特性在输出里可以看出来,它将系统调用的内容翻译成了可以阅读的形式。这对于判断ioctl()的使用尤其有用。
上述形式的strac(1)每个系统调用都会输出一行。选项-c 是用于系统调用活动的统计总结:
输出如下。
- time:显示系统CPU 时间花在哪里的百分比。
- seconds:总的系统CPU 时间,单位是秒。
- usecs/call:每次调用的平均系统CPU 时间,单位是毫秒。
- calls:整个strace(1)过程内的系统调用次数。
- syscall:系统调用的名字。
CPU
CPU 推动了所有软件的运行,因而通常是系统性能分析的首要目标。现代系统一般有多颗CPU,通过内核调度器共享给所有运行软件。当需求的CPU 资源超过了系统力所能及的范围时,进程里的线程(或者任务)将会排队,等待轮候自己运行的机会。等待给应用程序的运行带来严重延时,使得性能下降。
我们可以通过仔细检查CPU 的用量,寻找性能改进的空间,还可以去除一些不需要的负载。从上层来说,可以按进程、线程或者任务来检查CPU 的用量。从下层来看,可以剖析并研究应用程序和内核里的代码路径。在底层,可以研究CPU 指令的执行和周期行为。
CPU 架构
下图展示了一个CPU 架构的示例,单个处理器内共有四个核和八个硬件线程。物理架构如图左侧所示,而右侧图则展示了从操作系统角度看到的景象。
每个硬件线程都可以按逻辑CPU 寻址,因此这个处理器看上去有八块CPU。对这种拓扑结构,操作系统可能有一些额外信息,如哪些CPU 在同一个核上,这样可以提高调度的质量。
指令
CPU 执行指令集中的指令。一个指令包括以下步骤,每个都由CPU 的一个叫作功能单元的组件处理:
1:指令预取 2:指令解码 3:执行 4:内存访问 5:寄存器写回
最后两步是可选的,取决于指令本身。许多指令仅仅操作寄存器,并不需要访问内存。这里每一步都至少需要一个时钟周期来执行。内存访问经常是最慢的,因为它通常需要几十个时钟周期读或写主存,在此期间指令执行陷入停滞(停滞期间的这些周期称为停滞周期)。这就是CPU 缓存如此重要的原因:它可以极大地降低内存访问需要的周期数,
使用率
CPU 使用率通过测量一段时间内CPU 实例忙于执行工作的时间比例获得,以百分比表示。它也可以通过测量CPU 未运行内核空闲线程的时间得出,这段时间内CPU 可能在运行一些用户态应用程序线程,或者其他的内核线程,或者在处理中断。
高CPU 使用率并不一定代表着问题,仅仅表示系统正在工作。
多进程,多线程
大多数处理器都以某种形式提供多个CPU。对于想使用这个功能的应用程序来说,需要开启不同的执行线程以并发运行。对于一个64 颗CPU 的系统来说,这意味着一个应用程序如果同时用满所有CPU,可以达到最快64 倍的速度,或者处理64 倍的负载。应用程序可以根据CPU 数目进行有效放大的能力又称为扩展性。
应用程序在多CPU 上扩展的技术分为多进程和多线程
在Linux 上可以使用多进程和多线程模型,而这两种技术都是由任务实现的
多进程和多线程之间的差异如下所示
正如表里多线程的那些优点所示,多线程一般被认为优于多进程,尽管对开发者而言更难实现。
不管使用何种技术,重要的是要创建足够的进程或者线程,以占据预期数量的CPU——如果要最大化性能,即所有的CPU。有些应用程序可能在更少的CPU 上跑得更快,这是因为线程同步和内存本地性下降反而吞噬了更多CPU 资源。
CPU 硬件
CPU 硬件包括了处理器和它的子系统,以及多处理器之间的CPU 互联。
处理器
一颗通用的双核处理器的组件构成如下图 所示。
控制器(标为控制逻辑)是CPU 的心脏,运行指令预取、解码、管理执行以及存储结果。
MMU
MMU负责虚拟地址到物理地址的转换。下图展示了一个普通的MMU,附有CPU缓存类型。这个MMU通过一个芯片上的TLB缓存地址转换。主存(DRAM)里的转换表,又叫做页表,处理缓存未命中情况。页表由MMU(硬件直接读取)。
互联
对于多处理器架构,处理器通过共享系统总线或者专用互联连接起来。这与系统的内存架构有关,统一内存访问(UMA)或者NUMA。
共享的系统总线,称为前端总线,由早期的Intel 处理器使用,下图通过一个四处理器的例子演示
使用系统总线时,在处理器数目增长的情况下,会因为共享系统总线资源而出现扩展性问题。现代服务器通常都是多处理器,NUMA,并使用CPU 互联技术。
互联可以连接除了处理器之外的组件,如I/O 控制器。互联的例子包括Intel 的快速通道互联(QuickPath Interconnect,QPI)和AMD 的HyperTransport(HT)。一个四处理器系统的Intel QPI 架构示例如下图所示
除了外部的互联,处理器还有核间通信用的内部互联。
互联通常设计为高带宽,这样它们不会成为系统的瓶颈。一旦成为瓶颈,性能会下降,因为牵涉互联的CPU 指令会陷入停滞,如远程内存I/O。这种情况的一个关键迹象是CPI 的上升。CPU 指令、周期、CPI、停滞周期和内存I/O 可以通过CPU 性能计数器进行分析。
调度器
内核CPU调度器的主要功能如下图所示
功能如下:
- 分时:可运行线程之间的多任务,优先执行最高优先级任务
- 抢占:一旦有高优先级线程变为可运行状态,调度器能够抢占当前运行的线程,这样较高优先级的线程可以马上开始运行。
- 负载均衡:把可运行的线程移到空闲或者较不繁忙的CPU队列中
在Linux 上,分时通过系统时钟中断调用scheduler_tick()实现。这个函数调用调度器类函数管理优先级和称为时间片的CPU 时间单位的到期事件。当线程状态变成可运行后,就触发了抢占,调度类函数check_preempt_curr()被调用。线程的切换由__schedule()管理,后者通过 pick_next_task()选择最高优先级的线程运行。负载均衡由load_balance()函数负责执行。
空闲线程
内核“空闲”线程(或者空闲任务)只在没有其他可运行线程的时候才在CPU 上运行,并且优先级尽可能地低。它通常被设计为通知处理器CPU 执行停止(停止指令)或者减速以节省资源。CPU 会在下一次硬件中断醒来。
CPU绑定
另一个CPU调优的方法是把进程和线程绑定在单个CPU或者一组CPU上。这可以增加进程的CPU缓存温度,提高它的内存I/O性能。对于NUMA系统这可以提高内存本地性,同样也提高性能。
这个方法有以下两个实现方式。
- 进程绑定:配置一个进程只跑在单个CPU上,或者预定义CPU组中的一个
- 独占CPU组:分出一组CPU,让这些CPU只能运行指定的进程。这可以更大地提升CPU缓存效率,因为当进程空闲时,其他进程不能使用CPU,保证了缓存的温度。
在基于Linux的系统上,独占CPU组可以通过cpuset实现。在基于Solaris的系统上,这称为处理器组。
CPU 分析工具
虚拟内存
虚拟内存是一个抽象概念,它向每个进程和内核提供巨大的,线性的并且私有的地址空间。它简化了软件开发,把物理内存的分配交给操作系统管理。它也支持多任务。因为虚拟地址空间是设计成分离的,并且可以超额订购,即使用中的内存可以超过主内存的容量。
进程的地址空间由虚存内存子系统映射到主内存和物理交换设备。内核会按需在它们之间移动内存页,这个过程称作交换。它允许内核超额订购主内存。
换页
换页是将页面换入到调出主存,它们分别被称为页面换入和页面换出。它允许:
- 运行部分载入的程序
- 运行大于主存的程序
- 高效地在主存和存储设备之间迁移
文件系统换页
文件系统换页由读写位于内存中的映射文件页引发。对于使用文件内存映射(mmap())的应用程序和使用了页缓存的文件系统。这是正常的行为。这也被称作“好的”换页。
有需要时,内核可以调出一些页释放内存。这时说法变得有些复杂:如果一个文件系统页在主存中修改过(“脏的”),页面换出要求将该页写回磁盘。相反,如果文件系统页没有修改过(“干净的”),因为磁盘已经存在一份副本,页面换出仅仅释放这些内存以便立即重用。因术语页面换出指一个页被移出内存——这可能包括或者不包括写入一个存储设备
按需换页
如下图所示,支持按需换页的操作系统(必须支持)将虚拟内存按需映射到物理内存。这会把CPU创建映射的开销推迟到实际需要或访问时,而不是在初次分配这部分内存时。
下图中展示的序列从写入一个新分配的虚拟内存页开始,这导致对物理内存的按需映射。当访问一个尚未从虚拟映射到物理内存的页时,会发生缺页。
如果是一个包含数据但尚未映射到这个进程地址空间的映射文件时,第一步可能是读。
如果这个映射可以由内存中其他的页满足,就这被称作轻微缺页。它可能在进程内存增长过程中发生,从可用内存中映射到一个新的页,它也可能在映射到另一个存在的页时发生,例如从共享库中读一个页。
需要访问存储设备的缺页,例如访问未缓存映射到内存的文件,被称作严重缺页。
虚拟内存和按需换页的结果是任何虚拟内存可能出处于如下的一个状态:
A: 未分配
B:已分配,未映射(未填充并且未缺页)
C:已分配,已映射到主存(RAM)
D:已分配,已映射到物理交换空间(磁盘)
如果因为系统内存压力而换出页就会到达D状态,状态B到C的转变就是缺页。如果需要磁盘读写,就是严重缺页,否则就是轻微缺页。
从这几种状态出发,可以定义另外两种内存使用术语。
- 常驻内存大小(RSS):已分配的主内存(C)大小
- 虚拟内存大小:所有已分配的区域(B+C+D)
交换
交换是在主存和物理交换设备或者交换文件之间移动整个进程,这是UNIX独创的管理主存的技术,并且是交换这个术语的词源。
交换出一个进程,要求进程的所有私有数据必须被写入交换设备,包括线程结构和进程堆(匿名数据)。源于文件系统而且没有修改的数据可以被丢弃,需要的时候再从原来的位置读取。
因为进程的一小部分元数据总是常驻于内核内存中,内核仍能知道已交换出的进程。至于要将哪个进程交换回来,内核会考虑进程优先级,磁盘等待时间以及进程的大小。长期等待和较小的进程享有更高的优先级。
交换严重影响性能,因为已交换出的进程需要许多磁盘I/O才能重新运行。
当人们说“这个系统在交换”,通常指换页。
主存架构
下图展示了一个普通双处理器均匀访存模型(UMA)系统的主存架构。
通过共享系统总线,每个CPU 访问所有内存都有均匀的访存延时。如果上面运行的是单个操作系统实例并可以在所有处理器上统一运行时,又称为对称多处理器架构(SMP)。
作为对照,下图中展示了一个双处理器非均匀访存模型(NUMA)系统,其中使用的一个CPU 互联是内存架构的一部分。在这种架构中,对主存的访问时间随着相对CPU 的位置不同而变化。
CPU 1 可以通过它的内存总线直接对DRAM A 发起I/O 操作,这被称为本地内存。CPU 1通过CPU 2以及CPU 互联(两跳)对DRAM B 发起I/O 操作,这被称为远程内存而访问延时更高。
连接到每个CPU 的内存组被称为内存节点,或者仅仅是节点。基于处理器提供的信息,操作系统能了解内存节点的拓扑。这使得它可以根据内存本地性分配和调度线程,尽可能倾向于本地内存以提高性能。
总线
物理上主存如何连接系统取决于主存架构。实际的实现可能会涉及额外的CPU与内存之间的控制器和总线。可能的访问方式如下。
- 共享系统总线:单个或多个处理器,通过一个共享的系统总线,一个内存桥控制器以及内存总线
- 直连:单个处理器通过内存总线直接连接内存
- 互联:多处理器中的每一个通过一条内存总线与各自的内存直连,并且处理器之间通过一个CPU互联连接起来。
进程地址空间
进程地址空间是一段范围的虚拟页,由硬件和软件同时管理,按需映射到物理页。这些地址被划分为段以存放线程栈、进程可执行(文件)、库和堆。
应用程序可执行段包括分离的文本和数据段。库也由分离的可执行文本和数据段组成。这些不同的段类型如下。
- 可执行文本:包括可执行的进程CPU 指令。由文件系统中的二进制应用程序文本段映射而来。它是只读的并带有执行权限。
- 可执行数据:包括已初始化的变量,由二进制应用程序的数据段映射而来。有读写权限,因此这些变量在应用程序的运行过程中可以被修改。它也带有私有标记,因此这些修改不会被写回磁盘。
- 堆:应用程序的临时工作内存并且是匿名内存(无文件系统位置)。它按需增长并且用mollac()分配。
- 栈:运行中的线程栈,映射为读写。
库的文本段可能与其他使用相同库的进程共享,它们各自有一份库数据段的私有副本。
堆增长
不停增长的堆通常会引起困惑。它是内存泄漏吗?对于大多数分配器,free()不会将内存还给操作系统,相反的,会保留它们以备将来分配。这意味着进程的常驻内存只会增长,并且是正常现象。进程缩减内存的方法如下。
- Re-exec:从空的地址空间调用exec()。
- 内存映射:使用mmap()和munmap(),它们会归还内存到系统
资源控制
基础的资源控制,包括设置主存限制和虚拟内存限制,可以用ulimit(1)实现。
Linux 中,控制组1(cgroup)的内存子系统可提供多种附加控制,如下。
- memory.memsw.limit_in_bytes:允许的最大内存和交换空间,单位是字节。
- memory.limit_in_bytes:允许的最大用户内存,包括文件缓存,单位是字节。
- memory.swappiness:类似之前描述的vm.swappiness,差别是可以设置于cgroup。
- memory.oom_control:设置为0,允许OOM 终结者运用于这个cgroup,或者设置为1,禁用。
文件系统
研究应用程序I/O 性能时经常会发现,文件系统性能比磁盘性能更为重要。文件系统通过缓存、缓冲以及异步I/O 等手段来缓和磁盘(或者远程系统)的延时对应用程序的影响。
文件系统接口
下图从接口的角度展示了文件系统的基本类型。
图中还标出了逻辑与物理操作发生的区域。
文件系统缓存
下图描绘了在响应读操作的时候,存储在主存里的普通文件系统缓存情况。
读操作从缓存返回(缓存命中)或者从磁盘返回(缓存未命中)。未命中的操作被存储在缓存中,并填充缓存(热身)。
文件系统缓存也可用于缓冲写操作,使之延迟写入(刷新)。
文件系统延时
文件系统延时是文件系统性能一项主要的指标,指的是一个文件系统逻辑请求从开始到结束的时间。
它包括了消耗在文件系统、内核磁盘I/O 子系统以及等待磁盘设备——物理I/O 的时间。应用程序的线程通常在请求时阻塞,等待文件系统请求的结束。这种情况下,文件系统的延时与应用程序的性能有着直接和成正比的关系。
缓存
文件系统启动之后会使用主存(RAM)当缓存以提高性能。对应用程序这是透明的:它们的逻辑I/O延时小了很多,因为可以直接从主存返回而不是从慢得多的磁盘设备返回。
文件系统用缓存(caching)提高读性能,而用缓冲(buffering)(在缓存中)提高写性能。
随机和顺序I/O
一连串的文件系统逻辑I/O,按照每个I/O 的文件偏移量,可以分为随机I/O 与顺序I/O。顺序I/O 里每个I/O 都开始于上一个I/O 结束的地址。随机I/O 则找不出I/O 之间的关系,偏移量随机变化。随机的文件系统负载也包括存取随机的文件。下图展示了这些访问模式,给出了一组连续I/O 和文件偏移量的例子。
预取
预取是文件系统解决这个问题的通常做法。通过检查当前和上一个I/O 的文件偏移量,可以检测出当前是否是顺序读负载,并且做出预测,在应用程序请求前向磁盘发出读命令,以填充文件系统缓存。
预取的预测一旦准确,应用程序的读性能将会有显著提升,磁盘在应用程序请求前就把数据读出来了。而一旦预测不准,文件系统会发起应用程序不需要的I/O,不仅污染了缓存,也消耗了磁盘和I/O传输的资源。文件系统一般允许对预取参数进行调优。
写回缓存
写回缓存广泛地应用于文件系统,用来提高写性能。它的原理是,当数据写入主存后,就认为写入已经结束并返回,之后再异步地把数据刷入磁盘。文件系统写入“脏”数据的过程称为刷新(fushing)。例子如下:
- 应用程序发起一个文件的write()请求,把控制权交给内核。
- 数据从应用程序地址空间复制到内核空间。
- write()系统调用被内核视为已经结束,并把控制权交还给应用程序。
- 一段时间后,一个异步的内核任务定位到要写入的数据,并发起磁盘的写请求
如果文件系统的元数据遭到破坏,那可能无法加载。到了这一步,只能从系统备份中还原,造成长时间的宕机。
更糟糕的是,如果损坏蔓延到了应用程序读写的文件内容,那业务就会受到严重冲击。
为了平衡系统对于速度和可靠性的需求,文件系统默认采用写回缓存策略,但同时也提供一个同步写的选项绕过这个机制,把数据直接写在磁盘上。
同步写
同步写完成的标志是,所有的数据以及必要的文件系统元数据被完整地写入到永久存储介质(如磁盘设备)中。由于包含了磁盘I/O 的延时,所以肯定比异步写(写回缓存)慢得多。有些应用程序,例如数据库写日志,因完全不能承担异步写带来的数据损坏风险,而使用了同步写
同步写有两种形式:单次I/O 的同步写,和一组已写I/O 的同步提交。
裸I/O 和直接I/O
裸I/O:绕过了整个文件系统,直接发给磁盘地址。有些应用程序使用了裸I/O(特别是数据库),因为它们能比文件系统更好地缓存自己的数据。其缺点在于难以管理,即不能使用常用文件系统工具执行备份/恢复和监控。
直接I/O:允许应用程序绕过缓存使用文件系统。这有点像同步写(但缺少O_SYNC 选项提供的保证),而且在读取时也能用。它没有裸I/O 那么直接,文件系统仍然会把文件地址映射到磁盘地址,I/O 可能会被文件系统重新调整大小以适应文件系统在磁盘上的块大小(记录尺寸)。不仅仅是读缓存和写缓冲,预取可能也会因此失效,具体取决于文件系统的实现。
非阻塞I/O
一般而言,文件系统I/O 要么立刻结束(如从缓存返回),要么需要等待(比如等待磁盘设备I/O)。如果需要等待,应用程序线程会被阻塞并让出CPU,在等待期间给其他线程执行的机会。虽然被阻塞的线程不能执行其他工作,但问题不大,多线程的应用程序会在有些线程被阻塞的情况下创建额外的线程来执行任务。
某些情况下,非阻塞I/O 正合适,因为可以避免线程创建带来的额外性能和资源开销。在调用open()系统调用时,可以传入O_NONBLOCK 或者O_NDELAY 选项使用非阻塞I/O。这样读写时就会返回错误代码EAGAIN,让应用程序过一会儿再重试,而不是阻塞调用。(有可能仅仅建议的或者强制的文件锁才支持非阻塞功能,具体取决于文件系统实现。)
容量
当文件系统装满时,性能会因为数个原因有所下降。当写入新数据时,需要花更多时间来寻找磁盘上的空闲块,而寻找过程本身也消耗计算和I/O 资源。磁盘上的空闲空间变得更小更分散,而更小或随机的I/O 则影响了文件系统的性能。
文件系统I/O 栈
下图刻画了文件系统I/O 栈的一般模型。具体的模块和层次依赖于使用的操作系统类型、版本以及文件系统。
日志
文件系统日志(或者记录)记录了文件系统的更改,这样在系统宕机时,能原子地回放更改——要么完全成功,要么完全失败。这让文件系统能够迅速恢复到一致的状态。如果与同一个更新相关的数据和元数据没有被完整地写入,没有日志保护的文件系统在系统宕机时可能会损坏。从宕机中恢复需要遍历文件系统所有的结构,大的文件系统(几个TB)可能需要数小时。
日志被同步地写入磁盘,有些文件系统还会把日志写到一个单独的设备上。部分文件系统日志记录了数据和元数据,这样所有的I/O 都会写两次,带来额外的I/O 资源开销。而其他文件系统日志里只有元数据,通过写时复制的技术来保护数据。
有一种文件系统全部由日志构成——日志结构文件系统。在这个系统里所有的数据和元数据更新被写到一个连续的循环日志当中。这非常有利于写操作,因为写总是连续的,还能被一起合并成大I/O。
磁盘
磁盘I/O可能会造成严重的应用程序延时,因此是系统性能分析的一个重要指标。在高负载下,磁盘成为了瓶颈,CPU持续空闲以等待磁盘I/O结束,发现并消除这些瓶颈能让性能和应用程序吞吐量翻好几倍。
名词“磁盘”指系统的主要存储设备。这包括了旋转磁性盘片和基于闪存的固态盘(SSD)。引入后者主要是为了提高磁盘的I/O 性能,而事实上的确做到了。然而,对容量和I/O 速率的需求仍在不断增长,闪存设备并不能完全解决性能问题。
控制器
下图演示了一种简单的磁盘控制器,把CPU I/O 传输总线和存储总线以及相连的磁盘设备桥接起来。这个设备又称为主机总线适配器(host bus adaptor,HBA)。
其性能可能受限于其中任何一个组件,包括总线,磁盘控制器和磁盘。
测量时间
存储设备的响应时间(也叫磁盘I/O 延时)指的是从I/O 请求到结束的时间。它由服务和等待时间组成。
- 服务时间:I/O 得到主动处理(服务)的时间,不包括在队列中等待的时间。
- 等待时间:I/O 在队列中等待服务的时间。
下图展示了测量时间的构成,以及其他一些术语
响应时间、服务时间和等待时间全部取决于测量所处的位置。下面将从操作系统和磁盘上下文角度进行描述
- 从操作系统(块设备接口)角度来说,服务时间可以从I/O 请求发到磁盘设备开始计算,到结束中断发生为止。它并不包括在操作系统队列里等待的时间,仅仅反映了磁盘设备对操作请求的总体性能。
- 而对于磁盘而言,服务时间指从磁盘开始主动服务I/O 开始算起,不包括在磁盘队列里等待的任何时间。
寻道和旋转
磁性旋转磁盘的慢I/O 通常由磁头寻道时间和盘片旋转时间造成,这二者通常需要花费数毫秒。最好的情况是下一个请求的I/O 正好位于当前服务I/O 的结束位置,这样,磁头就不需要寻道或者额外等待盘片旋转。这是连续I/O,而需要磁头寻道或者等待盘片旋转的I/O 被称为随机I/O。
有许多方法可以降低寻道和旋转等待时间,如下。
- 缓存:彻底消除I/O。
- 文件系统布置和行为,包括写时复制。
- 把不同的负载分散到不同的磁盘,避免不同负载之间的寻道。
- 把不同的负载移到不同的系统(有些云计算环境可以通过这个方法降低多租户效应)。
- 电梯寻道,磁盘自身执行。
- 高密度磁盘,可以把负载落盘的位置变得更紧密。
- 分区(或者“切块”)配置,例如短行程。
另外一个降低旋转延时时间的办法就是使用更快的磁盘。
I/O 等待
I/O 等待是针对单个CPU 的性能指标,表示当CPU 分发队列(在睡眠态)里有线程被阻塞在磁盘I/O上时消耗的空闲时间。这就把CPU 空闲时间划分成无所事事的真正空闲时间,以及阻塞在磁盘I/O 上的时间。较高的每CPU I/O 等待时间表示磁盘可能是瓶颈所在,导致CPU 等待而空闲。
I/O 等待可能是一个令人非常困惑的指标。如果另一个CPU 饥饿型的进程也开始执行,I/O等待值可能会下降:CPU 现在有工作要做,而不会闲着。但尽管I/O 等待指标下降了,磁盘I/O还是和原来一样阻塞线程。与此相反,有时候系统管理员升级了应用程序软件,新版本的软件提高了效率,使用较少的CPU,把I/O 等待问题暴露了出来,这会让系统管理员认为软件升级导致了磁盘问题,降低了性能。然而事实上磁盘性能并没有变化,CPU 性能却提高了。
同步 VS 异步
如果应用程序和磁盘I/O 是异步的,那磁盘I/O 延时可能不直接影响应用程序性能,理解这点很重要。通常这发生在写回缓存上,应用程序I/O 早已完成,而磁盘I/O 稍后发出。
应用程序可能用预读执行异步读,在磁盘读取的时候,可能不阻塞应用程序。文件系统也有可能使用类似的手段来预热缓存(预取)。
即使应用程序同步等待I/O,该程序代码路径可能并不在系统的关键路径上,而对客户端应用程序的响应可能也是异步的。
网络
网络分析是跨硬件和软件的。这里的硬件指的是物理网络,包括网络接口卡、交换机,路由器和网关(这通常也含有软件)。这里的系统软件指的是内核协议栈,通常是TCP/IP,以及每个所涉及的协议的行为。
网络接口
网络接口是网络连接的操作系统端点,它是系统管理员可以配置和管理的抽象层。
下图描绘了一个网络接口。作为网络接口配置的一部分,它被映射到物理网络端口。端口连接到网络上并且通常有分离的传输和接收通道。
控制器
网络接口卡(NIC)给系统提供一个或多个网络端口并且设有一个网络控制器:一个在端口与系统I/O传输通道间传输包的微处理器。一个带有四个端口的控制器示例见下图,图中展示了该控制器所含有的物理组件。
控制器可作为独立的板卡提供或者内置于系统主板中。
网络和路由
网络是一组由网络协议地址联系在一起的相连主机。我们有多个网络——而非一个巨型的全球网络——是有许多原因的,其中特别有扩展性的原因。某些网络报文会广播到所有相邻的主机,通过建立更小的子网络,这类广播报文能隔离在本地从而不会引起更大范围的广播泛滥。这也是常规报文隔离的基础,使其仅在源和目标网络间传输,这样能更有效地使用网络基础架构。
路由管理称为包的报文跨网络传递。下图展示了路由的作用。
以主机A 的视角来看,localhost 是它自己,其他所有主机都是远程主机。
主机A 可以通过本地网络连接到主机B,这通常由网络交换机驱动。主机A 可由路由器1 连接到主机C,以及由路由器1、2 和3 连接到主机D。类似路由器这样的网络组件是共享的,源自其他通信的竞争(例如主机C 到主机E)可能会影响性能。
协议
例如IP、TCP 和UDP 这样的网络协议标准是系统与设备间通信的必要条件。通信由传输称为包的报文来实现,它通常包含封装的负载数据。
网络协议具有不同的性能特性,源自初始的协议设计、扩展,或者软硬件的特殊处理。
通常系统的可调参数也能影响协议性能,如修改缓冲区大小、算法以及不同的计时器设置。
包的长度以及它们的负载也会影响性能,更大的长度增加吞吐量并且降低包的系统开销。对于TCP/IP 和以太网,包括封装数据在内的包长度介于54~9054B,其中包括54B(或者更长,取决于选项或者版本)的协议包头。
三次握手
连接的建立需要主机间的三次握手。一个主机被动地等待连接,另一方主动地发起连接。作为术语的澄清:被动和主动源自[RFC 793],然而它们通常按套接字API 分别被称为侦听和连接。按客户端/服务器模型,服务器侦听而客户端发起连接。
下图描绘了三次握手。
硬件
网络硬件包括接口、控制器、交换机和路由器。尽管这些组件由其他工作人员(网络管理员)来管理,理解它们的运行还是必要的。
接口
物理网络接口在连接的网络上发送并接收称为帧的报文。它们处理包括传输错误在内的电器、光学或者无线信号。
接口类型基于第2 层网络标准。每个类型都存在最大带宽。高带宽接口通常延时更低,而成本更高。这也是设计新服务器的一个关键选择,要平衡服务器的售价与期望的网络性能。
交换机、路由器
交换机提供两个连入的主机专用的通信路径,允许多对主机间的多个传输不受影响。此技术取代了集线器(而在此之前,共享物理总线:例如以太网同轴线),它在所有主机间共享所有包。当主机同时传输时,这种共享会导致竞争。接口以“载波侦听多路访问/碰撞检测”(CSMA/CD)算法发现这种冲突,并按指数方式推迟直到重新传输成功。在高负载情况下这种行为会导致性能问题。自使用交换机之后就不再存在这种问题了,不过观测工具仍然提供碰撞计数器——尽管这些通常仅在故障情况(协商或者故障线路)下出现。
路由器在网络间传递数据包,并且用网络协议和路由表来确认最佳的传递路径。在两个城市间发送一个数据包可能涉及十多个甚至更多的路由器,以及其他的网络硬件。路由器和路由经常是设置为动态更新的,因此网络能够自动响应网络和路由器的停机,以及平衡负载。在意味着在任意时点,不可能确认一个数据包的实际路径。由于多个可能的路径,数据包有可能被乱序送达,这会引起TCP 性能问题。
这个网络中的神秘元素时常因糟糕的性能备受指责:可能是大量的网络通信——源自其他不相关的主机——使源与目标网络间的一个路由器饱和。网络管理员团队因此经常需要免除他们基础设施的责任。他们用高级的实时监视工具检查所有的路由器及其他相关的网络组件。
路由器和交换机都包含微处理器,它们本身在高负载情况下会成为性能瓶颈。作为一个极端的例子,我曾经发现因为有限的CPU 处理能力,一个早期的10GbE 以太网交换机只能在所有端口上驱动11Gb/s。
对于网络通信来说,应用工具法可以检查如下内容。
- netstat -s:查找高流量的重新传输和乱序数据包。哪些是“高”重新传输率依客户机而不同,面向互联网的系统因具有不稳定的远程客户会比仅拥有同数据中心客户的内部系统具有更高的重新传输率。
- netstat -i:检查接口的错误计数器(特定的计数器依OS 版本而不同)。
TCP 分析
当一个服务器频繁地用相同的源和目的IP 地址连接另一个服务器的同一个目标端口时,最后一个行为可能成为一个可扩展性问题。每个连接唯一的区别要素是客户端的源端口——一个短暂的端口——对于TCP 它是一个16 位的值并且可能进一步受到操作系统参数的限制(最小和最大值)。综合可能是60s 的TCP TIME-WAIT 间隔,高速率的连接(60s 内多于65 536)会与新连接碰撞。这种情况下,发送一个SYN 包,而那个短暂的端口仍然与前一个处于TIME-WAIT 的TCP 会话关联。如果被误认为是旧连接的一部分(碰撞),这个新的SYN 包可能被拒收。为了避免这种问题,Linux 内核试图快速地重利用或者回收连接(这通常管用)。
云计算
云计算的兴起在性能领域中解决了一些问题,同时也带来了一些新的问题。云通常基于虚拟化技术搭建,它允许多个操作系统实例或者租户共享一个物理服务器。这意味着会存在资源竞争:不仅仅来自常见的其他UNIX 进程,还源自其他的各个操作系统。隔离每个租户的性能影响至关重要,而识别由其他租户所导致的糟糕性能也同样重要。
可扩展的架构
企业环境传统上利用垂直扩展处理负载:组建更大的单一系统(大型机)。这种实现方式有它的极限。组建的计算机在物理大小上存在实际的极限(它可能受制于电梯门或者集装箱的大小),并且随着CPU 数量的增长,保持CPU 缓存一致性的难度也在逐步上升。解决这些极限的方案是跨多个(或许小的)系统扩展负载,这称为水平扩展。在企业中,它被用于服务器群和集群,特别是用于高性能计算(HPC)
云计算也基于水平扩展。下图展示了一个示例环境,它包括负载均衡器、Web 服务器、应用服务器和数据库。
多租户
UNIX 是一个多任务的操作系统。它被设计成能处理多个用户和进程访问同样的资源。之后BSD、Solaris 以及Linux 添加的资源限制和控制使得共享这些资源更加公平,以及当出现资源竞争引起性能问题时,可以提供有助于定位和定量的观测能力。
云计算的不同在于整个操作系统实例共存于同一个物理系统中。每个访客有自己隔离的操作系统:访客不能观察同一宿主的其他访客的用户或进程——信息泄漏——即使它们共享同样的物理资源。
由于资源在租户间共享,性能问题可能由“吵闹的邻居”引起。例如,同一宿主上的另一个客户可能在你的负载峰值时运行数据库全转储,影响到你的磁盘和网络I/O,以及网络吞吐率。更糟糕的是,一个邻居可能在运行微型基准测试以评估云供应商,特意把所有的资源耗光以检测极限。
这个问题有一些解决方案。多租户效应可以通过资源管理控制:设置操作系统资源控制以提供性能隔离(又名资源隔离)。以下是施加单租户限制和优先级的地方:CPU、内存、磁盘或者文件系统I/O,以及网络吞吐率。并非所有的云技术都提供所有这些,特别是磁盘I/O 限制。ZFS I/O 流控是为Joyent 的公有云开发的,特别针对吵闹的磁盘邻居问题。
除了限制资源的使用,观测多租户竞争可以帮助云运营商调优限制并且更好地在可用的宿主上平衡租户。可观测的程度视虚拟化的类型而定:OS 虚拟化或硬件虚拟化。