目录

性能之巅

目录

性能之巅一书的笔记。


参考:




绪论



系统性能

系统性能是对整个计算机系统的性能的研究,包括主要硬件组件和软件组件。你应该有一张所有系统组件的架构图,帮助你理解所有组件的关系。

系统性能的目标是通过减少延时和降低计算成本来改善用户的体验。



延时

延时测量的是用于等待的时间。



可观测性

可观测性是指通过观测来理解一个系统,并对完成这一任务的工具进行分析。这包括使用计数器、剖析和跟踪。

应用程序和内核通常提供了关于其状态和活动的数据,这些数据通常被称为计数器的整型变量实现。例如,工具 vmstat 根据 /proc 文件系统中的内核计数器,打印出系统级别的虚拟内存统计和汇总信息。

指标是为评估或检测一个目标而选择的统计数据。

术语剖析通常指使用工具来进行采样。

跟踪是基于事件的记录,捕获事件数据并保存起来供以后使用,或即时用于自定义总结和其他操作。如用于系统调用的 strace 和网络数据包的 tcpdump 的追踪工具。

静态检测描述的是添加到源代码中的硬编码的软件检测点。

动态检测实在软件运行起来后,通过修改内存指令插入检测程序来创建检测点。

BPF(Berkeley Packet Filter)可以为 Linux 最新的动态跟踪工具赋能。自 2013 年以来,BPF 已经被扩展(eBPF)成为一个通用的内核执行环境,一个能安全的快速访问资源的环境。



Linux性能分析60秒

这是基于 Linux 工具的一个检查表,可在调查性能问题的头 60 秒执行。

工具 检查
uptime 平均负载可识别负载的增加或减少
dmesg -T | tail 包括 OOM 事件的内核错误
vmstat -SM 1 系统级统计:运行队列长度、交换、CPU 使用情况
mpstat -P ALL 1 CPU 平衡情况:单个 CPU 很繁忙,意味着线程扩展性糟糕
pidstat 1 每个进程的 CPU 使用情况:识别意外的 CPU 消费者,以及每个进程的用户/系统的 CPU 时间
iostat -sxz 1 磁盘 IO 统计:IOPS 和吞吐量、平均等待时间、忙碌百分比
free -h 内存使用情况,包括文件系统的缓冲
sar -n DEV 1 网络设备 IO:数据包和吞吐量
sar -n TCP,ETCP 1 TCP 统计:连接率、重传
top 检查概览


方法



术语

  • IOPS:每秒发生的输入/输出操作的次数,是数据传输率的一种度量方法。
  • 吞吐量:评价工作执行的速率,尤其是在数据传输方面。
  • 响应时间:完成一次操作的时间。
  • 延时:描述操作中用来等待服务的时间。
  • 使用率:对于服务所请求的资源,描绘在给定时间区间内资源的繁忙程度。
  • 饱和度:指某一资源无法提供服务的工作的排队程度。
  • 瓶颈:指限制系统性能的那个资源。
  • 工作负载:系统的输入或是对系统所施加的负载。
  • 缓存:用于复制或者缓冲一定数量的高速存储区域,目的是为了避免对较慢的存储层级的直接访问,从而提高性能。


模型

  • 受测系统
  • 排队系统


系统性能的概念

系统性能的一些重要概念。

  • 延时:网络和 IO 等。
  • 时间量级:1 个 CPU 周期的时间是很短暂的。
  • 权衡:好/快/便宜的选择。
  • 调优的影响:性能调优在越靠近工作执行的地方效果最显著。
  • 合适的层级
  • 合适停止分析:当已解释了大部分性能问题时;但回报率低于分析的成本是;当其他地方有更大的回报率时。
  • 性能推荐的时间点
  • 负载与架构
  • 扩展性
  • 指标:吞吐量、IOPS、使用率和延时。
  • 使用率:基于时间或容量的。
  • 饱和度
  • 剖析
  • 缓存:被频繁使用来提高性能。如把磁盘内容缓存到内存中。CPU 的多级缓存。
  • 算法
  • 已知未知

层级 调优对象
应用程序 应用程序逻辑、请求队列大小、执行的数据库请求等
数据库 库表的布局、索引、缓冲等
系统调用 内存映射或读写、同步或异步 IO 标志等
文件系统 记录尺寸、缓冲尺寸、文件系统可调参数、日志等
存储 RAID 级别、磁盘类型和数目、存储可调参数

缓存的命中率是所需数据在缓存中被找到的次数与总访问次数的比例。缓存的失效率是每秒缓存失效的次数。两者对性能都有影响,工作负载的总运行时间可有此攻击计算:运行时间 = (命中率 x 命中延时)+ (失效率 x 失效延时)

缓存的热、冷和温状态:

  • 冷:冷缓存是空的,或填充的无用数据。命中率为 0。
  • 热:热缓存填充的是常用的数据,并有很高的命中率。
  • 温:温缓存指填充了有用的数据,但命中率还没达到预想的高度。
  • 热度:指缓存的冷或热,提高缓存热度的目的就是提高缓存的命中率。

  • 已知的已知:有些东西你知道。
  • 已知的未知:有些东西你知道你不知道。
  • 未知的未知:有些东西你不知道你不知道。


视角

性能分析有两个常用的视角:

  • 工作负载分析
  • 资源分析

https://raw.githubusercontent.com/zhang21/images/master/cs/operatingsystem/systems-performance/2-10.png



资源分析

资源分析以系统资源为起点,涉及的资源有:CPU、内存、磁盘、网卡、总线以及它们之间的互联。

操作如下:

  • 性能问题研究
  • 容量规划

资源分析的指标:

  • IOPS
  • 吞吐量
  • 使用率
  • 饱和度


工作负载分析

工作负载分析检查应用程序的性能:所施加的工作负载和应用程序时如何响应的。

分析对象:

  • 请求:所施加的工作负载
  • 延时:应用程序的响应时间
  • 完成度:查找错误

分析指标:

  • 吞吐量
  • 延时


性能分析和调优的方法

要知道从哪里开始分析和如何进行分析。



街灯讹方法

此方法用一类观测偏差来命令,这类偏差叫做街灯效应。

这相当于查看 top,不是因为这么做有道理,而是用户不知道怎么使用其他工具。



随机变动讹方法

用户随机猜测问题可能存在的位置,然后做改动,直到问题消失。



责怪他人讹方法

此方法包含以下步骤:

  1. 找到一个不是你负责的系统或组件
  2. 假定问题与那个组件相关
  3. 把问题扔给那个组件的团队
  4. 如果证明错了,返回第一步


AdHoc核对清单法

当需要检查和调试系统时,技术人员通常会花一些时间过一遍核对清单,一份核对清单包含很多检查项目。



问题陈述法

明确如何陈述问题:

  • 是什么让你认为存在性能问题?
  • 系统之前运行得好吗?
  • 最近有什么改动?软件、硬件、负载?
  • 问题能用延时或运行时间来表述吗?
  • 问题影响其他的人和应用程序吗?
  • 环境是什么样的?用了哪些软件?是什么版本?是怎样配置的?


科学法

通过假设和测试来研究位置的问题:

  • 问题
  • 假设
  • 预测
  • 测试
  • 分析


诊断循环

诊断周期:假设-仪器检验-数据-假设。



工具法

以工具为导向的方法:

  • 列出可用到的性能工具。
  • 对于每一种工具,列出它提供的有用的指标。
  • 对于每一个指标,列出阐释该指标可能的规则。


USE方法

USE(utilization, saturation, errors)使用率、饱和度和错误,用来识别系统瓶颈。

对于所有的资源,查看它的使用率、饱和度和错误。

一种遍历所有资源的方法是画一张系统的原理框图。

https://raw.githubusercontent.com/zhang21/images/master/cs/operatingsystem/systems-performance/2-13.png


资源列表:

  • CPU:插槽、核心、硬件线程(虚拟 CPU)
  • 内存
  • 网络接口
  • 存储设备
  • 加速器
  • 控制器:存储、网络
  • 互联:CPU、内存、IO

一些指标

资源 类型 指标
CPU 使用率 CPU 利用率
CPU 饱和度 运行队列长度、调度器延时、CPU 压力
内存 使用率 可用空闲内存
内存 饱和度 交换、页面扫描、内存缺失事件、内存压力
网络接口 使用率 接收/发送的吞吐量或最大带宽
存储设备 IO 使用率 设备繁忙百分比
存储设备 IO 饱和度 等待队列长度、IO 压力
存储设备 IO 错误 设备错误

软件资源:

  • 互斥锁:锁被持有的时间是使用率,饱和度指有线程排队在等待锁。
  • 线程池:线程忙于处理工作的时间是使用率,饱和度指等待线程池服务的请求数目。
  • 进程/线程容量:系统的进程或线程的总数是有上限的,当前的使用数目是使用率,等待分配的数据是饱和度,错误是分配失败。
  • 文件描述符容量:同进程/线程容量一样,只不过针对的是文件描述符。

使用建议:

  • 使用率:100% 的使用率通常是瓶颈的信号(检查饱和度并确认其影响)。
  • 饱和度:任何程序的饱和都是问题。饱和度可以用排队长度或排队所花的时间来度量。
  • 错误:错误都是值得研究的,尤其是随着错误增加性能会变差的那些错误。


RED方法

RED(request, error, duration)请求、错误、持续时间。对于每个服务,检查请求率、错误和持续时间。

此方法的重点是服务,通常是微服务架构中的云服务。

  • 请求率:每秒的服务请求数。
  • 错误:失败的请求数。
  • 持续时间:请求完成的时间。

画出微服务架构图,并确保每个服务的三个指标都进行监测。

RED 方法是 Tom Wilkie 创建,他还为 Prometheus 开发了 USE 和 RED 方法指标的实现,并使用了 Grafana 仪表盘。



工作负载特征归纳

此方法用于辨别这一类问题——由施加负载导致的问题。此方法关注系统的输入,而不是所产生的性能。系统的负载是否超出了它所能承受的合理范围。

工作负载可以通过回答下列问题来进行归纳:

  • 负载是谁产生的?
  • 负载为什么会被调用?
  • 负载的特征是什么?
  • 负载时怎样随着时间变化的?


向下钻取分析

此方法开始于检查高层次的问题,然后依据之前的发现缩小关注的范围,忽视那些无关的部分,更深入发掘那些相关部分。



延时分析

延时分析检查完成一项操作所用的时间,然后把时间再分成小的时间段,接着对有着最大延时的时间段再次做划分,最后定位并量化问题的根本原因。



事件跟踪

系统的操作就是处理离散的事件,包括 CPU 指令、磁盘 IO,以及磁盘命令、网络包、系统调用函数库调用、应用程序事务、数据库查询等。



基础线统计

环境通常使用监测解决方案来记录服务器性能指标,并将指标可视化为线图,时间在 X 轴上。



静态性能调优

静态性能调优着重处理的是架构配置的问题。静态性能分析实在系统空闲没有施加负载的时候执行的。

例如:

  • 网络带宽不足
  • 软硬件版本太低
  • 文件系统已满
  • 权限不正确
  • 使用的资源不对
  • 等等


缓存调优

从应用程序到磁盘,应用程序和操作系统会部署多层的缓存来提高 IO 的性能。

介绍各级缓存的通用调优策略:

  • 尽量将缓存放在栈的顶端,靠近工作开展的地方,以降低缓存命中的操作开销。
  • 确认缓存开启并确实在工作
  • 确认缓存的命中率和失效率
  • 如果缓存的大小是动态的,确认它的当前尺寸。
  • 针对工作负载调整缓存。
  • 针对缓存调整工作负载。

要小心二次缓存——比如,消耗内存的两个不同的缓存块,或把相同的数据缓存了两次。

要考虑每一层缓存调优的整体性能收益。如 CPU 的 L1, L2, L3 缓存。



微基准测试

微基准测试测量的是简单的人造工作负载的性能。而宏观基准测试是通过运行工作负载仿真来进行的,执行和理解的复杂度高。



性能箴言

如何最好地提高性能的调优方法,可操作的项目从最有效到无效的顺序如下:

  1. 不要做:消除不必要的工作。
  2. 做吧,但不要再做:缓存。
  3. 做少点:将刷新、轮询或更新的频率调低。
  4. 稍后再做:回写缓存。
  5. 在不注意的时候做:安排工作或在非工作时间进行。
  6. 同时做:从单线程切换到多线程。
  7. 做的更便宜:购买更快的硬件。


建模

建立系统的分析模型有很多用途,特别是对于可扩展性分析:研究当负载或资源扩展时性能会如何变化。



容量规划

容量规划可以检查系统处理负载的情况,以及系统如何随着负载的增加而扩展。



统计

用统计(指标)的方法量化性能问题,统计的类型包括平均值、标准方差,以及百分位数。

标准方差度量的是数据的离散程度,更大的数值表示数据偏离均值的程度越大。

第 99 百分位数显示的是该点在分布上包含了 99% 的数值。

第 50 百分位数,又叫做中位数,用以显示数据的大部分如何分布。

诸如第 99, 95, 90 和 99.9 百分位数都会在请求延时的性能监测时使用,对请求分布的最慢部分做量化。



监测

系统性能检测记录一段时间内的性能统计数据。



可视化

可视化能让人们对更多的数据做检查,而且比文字更容易理解。

可视化工具如 Grafana。




操作系统

了解操作系统和它的内核对于系统性能的分析至关重要。

本章目标:

  • 学习内核术语:上下文切换、交换、分页、抢占等。
  • 理解内核和系统调用的作用。
  • 了解内核内部的工作机制:中断、调度器、虚拟内存和 IO 栈。
  • 对 eBPF 有一个基本的了解。


术语

  • 操作系统:指安装在系统中的软件和文件。包括:内核、管理工具,以及系统库。
  • 内核:指管理系统的程序,包括硬件设备、内存和 CPU 调度。它运行在 CPU 的特权模式,允许直接访问硬件,被称为内核态。
  • 进程:是操作系统的一个抽象概念,用来执行程序的环境。程序通常运行在用户态,通过系统调用或陷入来进入内核态。
  • 线程:可被调度的运行在 CPU 上的可执行上下文。内核有多个线程,一个进程有一个或多个线程。
  • 任务:一个 Linux 的可运行实体,可以指一个线程,或内核线程。
  • BPF程序:在 BPF 执行环境中运行的内核态的程序。
  • 主存储器:系统的物理内存。
  • 虚拟内存:主存的一个抽象。
  • 内核空间:内核的虚拟内存地址空间。
  • 用户空间:进程的虚拟内存地址空间。
  • 用户环境:用户级别的程序和库。
  • 上下文切换:从运行一个线程或进程切换到运行另一个线程或进程。这是内核 CPU 调度器的功能,此过程将运行中的 CPU 寄存器集(线程上下文)切换到一个新的寄存器集。
  • 模式切换:内核态和用户态之间的切换。
  • 系统调用:一套定义明确的协议,为用户程序请求内核执行特权操作,包括设备 IO。
  • 处理器:包含一颗或多颗 CPU 的物理芯片。
  • 自陷:信号发送到内核,请求执行一段系统例程(特权操作)。自陷类型包括:系统调用、处理器异常,以及中断。
  • 硬件中断:由物理设备发送给内核的信号,通常是请求 IO 服务。


背景



内核

内核是操作系统的核心软件,管理着 CPU 调度、内存、文件系统、网络协议和系统设备。

https://raw.githubusercontent.com/zhang21/images/master/cs/operatingsystem/systems-performance/3-1.png


还存在其他的内核模型:

  • 微内核采用一个小的内核,其功能被转移到用户态的程序中。
  • 单内核(宏内核)把内核和应用程序的代码作为一个单一的程序编译在一起。如 Linux, BSD, Unix-Like。
  • 还有一些混合内核,如 Windeows NT 内核,它同时使用了单内核和微内核的方法。

Linux 最近改变了自己的模式,允许一种新的软件类型:eBPF,这让安全的内核态的应用程序与它自己的内核 API 一起使用成为可能。通过 BPF 帮助器,可用 BPF 重写一些应用程序和系统功能,以提供更高水平的安全性和性能。


内核的执行。当用户程序进行系统调用时,或设备发出中断时,内核首先要按需执行。执行频繁的 IO 工作负载,主要在内核上下文中运行。计算密集型的工作负载通常在用户态运行,不受内核的干扰。但在很多情况下是影响的,最明显的场景是 CPU 的争夺,当其他线程争夺 CPU 资源时,内核调度器需要决定哪些运行,哪些等待。内核还会选择哪个 CPU 来运行线程等。



内核态与用户态

内核是运行在特殊 CPU 模式下的程序,这一特殊模式叫做 内核态。在此状态下,设备的一切访问及特权指令的执行都是被允许的。由内核来控制设备的访问,用以支持多任务处理,除非明确允许,否则进程之间和用户之间的数据是无法彼此访问的。

用户程序运行在 用户态 下,对于内核特权操作的请求是通过系统调用传递的。


在传统内核里,系统调用会做上下文切换,从用户态到内核态,然后执行系统调用的代码。

https://raw.githubusercontent.com/zhang21/images/master/cs/operatingsystem/systems-performance/3-3.png


内核态和用户态都有自己的软件执行的上下文,包括栈和注册表。

在用户态和内核态之间的切换是模式转换。所有的系统调用都会进行模式切换。某些系统调用也会进行上下文切换:阻塞的系统调用(IO, 网络)

模式转换和上下文切换都会增加一小部分的时间开销(CPU 周期),有多种方法来避免开销:

  • 用户态的系统调用:可以单独在用户态中实现一些系统调用。
  • 内存映射:用于按需换页。
  • 内核旁路:允许用户态的程序直接访问设备,绕过系统调用和典型的内核代码路径。
  • 内核态的应用程序:如 eBPF 技术。


系统调用

系统调用请求内核执行特权的系统例程。应该努力确保这一数目尽可能地小,以保持内核简单(UNIX 的理念)。更为复杂的接口应该作为库存储在用户空间中,在那里开发和维护更为容易。

系统调用都有很好的文档,都有一个 man 手册。一些关键的系统调用:

系统调用 描述
read() 读取字节
write() 写入字节
open() 打开文件
close() 关闭文件
fork() 创建新进程
clone() 克隆新进程或线程
exec() 执行新程序
connect() 连接到主机
accept() 接受网络连接
stat() 获取文件统计信息
ioctl() 设置 IO 属性
mmap() 把文件映射到内存地址空间
brk() 扩展堆指针
futex() 快速用户空间互斥锁


中断

中断是向处理器发出的信号,即发生了一些需要处理的事件,要中断处理器当前的执行来实施处理。如果处理器还没有进入内核模式的话,中断通常会使处理器进入内核态,并保存当前线程状态。

  • 由外部硬件产生的异步中断
  • 由软件指令产生的同步中断

https://raw.githubusercontent.com/zhang21/images/master/cs/operatingsystem/systems-performance/3-4.png



异步中断,硬件设备可以向处理器发送中断服务请求(IRQ),这些请求以异步方式到达当前运行的软件。硬件中断示例:

  • 磁盘设备发出磁盘 IO 完成的信号。
  • 硬件显示有故障情况。
  • 网络接口发出数据包到达的信号。
  • 输入设备:键盘和鼠标的输入。

同步中断,由软件产生。

  • 自陷:故意调用内核,如通过 int 中断指令。
  • 异常:一个特殊指令,如执行除以零。
  • 故障:一个通常用于内存事件的术语,如缺页故障。

对于这些中断,相对应的软件和指令仍在 CPU 上。


中断线程,中断服务例程(ISR)被设计为尽可能快地运行,以减少中断活动线程的影响。如果一个中断需要执行更过的工作,尤其是还可能被锁阻塞,那么最好用中断线程来处理,这个线程可以由内核来安排。


中断屏蔽,内核中的某些代码是不能被安全中断的。如中断自旋锁可能导致死锁。为了防止这种情况,内核可以通过设置 CPU 的中断屏蔽寄存器来暂时屏蔽中断。中断禁用的时间应该尽可能地短,因为它可能干扰被其他中断唤醒的应用程序的执行。

一些高优先级的事件不应该被忽略,因此被实现为不可屏蔽的中断(NMI)。



时钟和空闲

早期 UNIX 内核的一个核心组件是 clock() 例程,有一个计时器中断执行。它每秒的执行次数通常以 Hz 表示,每次执行称为一个 tick。

时钟曾有一些性能问题:如 tick 延时和开销。不过都在之后的内核中得到了改进。

现代内核已经把许多功能移出了时钟例程,放到了按需中断中。这是为了努力创造无 tick 的内核。

Linux 的时钟例程是 scheduler_tick(),在没有任何 CPU 负载的情况下,Linux 有办法不调用时钟。

空闲线程,当 CPU 没有工作可做时,内核会安排一个等待工作的占位线程,称为空闲线程。



进程

进程是执行用户级别程序的环境。包括:

  • 内存地址空间
  • 文件描述符
  • 线程栈
  • 寄存器

进程可以让内核进行多任务处理,使得在一个系统中可以执行上千个进程。每个进程有一个唯一的 PID 来标识。

一个进程中包含一个或多个线程,其在进程的地址空间内操作并且共享着一样的文件描述符。

线程是一个可执行的上下文,包括栈、寄存器,以及指令指针(程序计数器)。多线程让单进程可在多个 CPU 上并发地执行。在 Linux 中,线程和进程都是任务。

内核启动的第一个进程叫做 init,来自 /sbin/init,PID 为 1,用于启动用户空间服务。在 UNIX 中会涉及从 /etc 中运行启动脚本,此方法被称为 SysV(来自 Unix System V)。现在,Linux 通常使用 systemd 软件来启动服务并跟踪其依赖关系。



进程的创建,在 Unix 中,进程的创建通常使用 fork() 系统调用。在 Linux 中,C 语言库通常包裹多功能的 clone() 系统调用来实现 fork() 功能。

https://raw.githubusercontent.com/zhang21/images/master/cs/operatingsystem/systems-performance/3-7.png



进程生命周期 简化示意图。

https://raw.githubusercontent.com/zhang21/images/master/cs/operatingsystem/systems-performance/3-8.png



进程环境,包括进程空间内的数据和内核里的元数据。

https://raw.githubusercontent.com/zhang21/images/master/cs/operatingsystem/systems-performance/3-9.png



栈是一个用于存储临时数据的内存区域,以后进先出(LIFO)为组织方式。

栈被用来存储比适合 CPU 寄存器集的数据更不重要的数据。当函数被调用时,返回地址被保存到栈中。寄存器也可以保存在栈里面。栈也可以用于向函数传递参数。栈中与函数的执行有关的数据集被称为栈帧。

通过检查线程栈中所有栈帧中保存的返回地址,可看到当前执行的函数的调用路径(栈遍历)。这个调用路径被称为栈回溯或栈踪迹。栈是调试和性能分析的宝贵工具。



如何读栈,栈通常按从叶到根的顺序打印。第一行是当前执行的函数,在它下面是它的父函数,祖父函数,以此类推。

通过向下阅读栈,可以看到完整的调用链:函数、父函数、祖父函数,等等。



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

https://raw.githubusercontent.com/zhang21/images/master/cs/operatingsystem/systems-performance/3-10.png



虚拟内存

虚拟内存是主存的抽象,为进程和内核提供近乎是无限的和私有的主存视图。虚拟内存支持多任务处理,允许进程和内核在它们自己的私有地址空间执行而不用担心任何竞争。

https://raw.githubusercontent.com/zhang21/images/master/cs/operatingsystem/systems-performance/3-11.png



内存管理,当虚拟内存用二级存储作为主存的扩展时,内核会尽力保持最活跃的数据在主存中。有两个内核例程做这件事:

  • 进程交换:让整个进程在主存和二级存储之间移动。
  • 换页:移动被称为页的小的内存单元(如 4KB)。

进程交换会引起严重的性能损耗(内存速度和磁盘速度)。换页是更高效的方法。这两种方法都是将最近最少使用(或未使用)的内存移出主存,需要时在搬回主存。



调度器

Unix 机器衍生系统都是分时系统,通过划分执行时间,让多个进程同时运行。进程在处理器上和 CPU 间的调度是由调度器完成的,调度器操作线程,并将它们映射到 CPU 上。

https://raw.githubusercontent.com/zhang21/images/master/cs/operatingsystem/systems-performance/3-12.png


调度器基本的作用是将 CPU 时间划分为活跃的进程和线程,而且维护一套优先级机制。多数内核线程运行的优先级要比用户级别的优先级高。

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

  • CPU 密集型:应用程序执行繁重的计算。如科学计算、数学分析等。
  • IO 密集型:应用程序执行 IO,计算不多。如 Web 服务,文件服务等。


文件系统

文件系统是文件和目录的数据组织。有一个基于文件的接口用于访问文件系统,通常是基于 POSIX 标准的。

操作系统提供了全局的文件命名空间,其被组织为以根目录为起点,自上而下的拓扑结构。通过挂载可以添加文件系统的树,把自己的树挂在一个目录上。

多数文件系统类型使用存储设备(磁盘)来存放内容。某些文件系统类型是由内核动态生成,如 /proc/dev



VFS(虚拟文件系统),是一个对文件系统类型进行抽象的内核接口。最初的目的是让 Unix 文件系统和 NFS 能更容易地共存。

VFS 结构让内核添加新的文件系统时更加简单。



IO栈,基于存储设备的文件系统,从用户级软件到存储设备的路径被称为 IO 栈。



缓存

由于磁盘 IO 的延时比较长,软件栈中很多层级通过缓存读取和缓存写入来试图避免这一点。

常见的缓存层级

缓存 实例
客户端缓存 网络浏览器缓存
应用程序缓存 -
Web 服务器缓存 Nginx 缓存
缓存服务器 Redis
数据库缓存 MySQL 缓冲区高速缓存
目录缓存 dcache
文件元数据缓存 inode 缓存
操作系统缓冲区高速缓存 缓冲区高速缓存
文件系统主缓存 换页缓存
文件系统次缓存 -
设备缓存 -
块缓存 缓冲区高速缓存
磁盘控制器缓存 RAID 卡缓存
存储阵列缓存 -
磁盘内置缓存 -

缓冲区高速缓存是主存的一块区域,用于存放最近使用的磁盘块。如果请求的块存在,磁盘读取能立即完成,避免了高延时的磁盘 IO。



网络

现代内核提供了一套内置的网络协议栈(TCP/IP),能够让系统通过网络进行通信,成为分布式系统环境的一部分。用户级别的应用程序通过套接字的编程端点跨网络通信。



设备驱动

内核必须和各种各样的物理设备通信,这样的通信可以通过使用设备驱动达成,设备驱动适用于设备管理和设备 IO 的内核软件。

设备驱动给设备提供的接口由字符接口和块接口。

  • 字符设备,也称为原始设备,提供无缓冲的设备顺序访问。如键盘和串口。
  • 块设备所执行的 IO 以块为单位(512B)。基于块的偏移值可被随机访问,偏移值在块设备的头部以 0 开始计数。


多处理器

支持多处理器使得操作系统可以用多个 CPU 实例来并行地执行工作。



抢占

支持内核抢占让高优先级的用户线程可以中断内核并开始被执行。支持抢占的内核被称为完全可抢占的内核。

Linux 支持的是资源内核抢占,在内核代码中的逻辑停止点可以做就检查并执行抢占。这避免了完全抢占式内核的某些复杂性,为常见工作负载提供低延时的抢占。



资源管理

Unix 和 BSD 有基于每个进程的资源控制,包括用 nice() 调整优先级,用 ulimit() 对某些资源做出限制。

Linux 则是开发了控制组(cgroups, control groups)。



可观测性



内核

讨论 Unix-Like 内核的实现细节,重点是性能。



UNIX

Unix 在 1969 年及之后的岁月里开发的。



BSD

BSD 基于 Unix 第六版的增强,并在 1978 年首次发布。



Solaris

Solaris 是在 1982 年开发的一个 Unix 和 BSD 衍生的内核和操作系统。



Linux

Linux 诞生于 1991 年,由 Linus Torvalds 开发。



Linux内核开发

Linux 内核开发,尤其是与性能相关的开发,涉及内容如下:

  • CPU 调度类型:各种 CPU 调度算法
  • IO 调度类型: 块 IO 调度算法
  • TCP 拥塞算法:TCP 拥塞控制算法
  • Overcommit:有 OOM killer,该策略支持用较少内存做更多的事情。
  • Futex(fast user-space mutex):用于提供高性能的用户级别的同步原语。
  • 巨型页:由内存和内核管理单元支持大型内存的预分配。
  • OProfile:研究 CPU 使用和其他活动的系统剖析工具。
  • RCU:内核所提供的只读更新同步机制,支持伴随更新实现多个读取的并发,提升了读取频繁数据的性能和扩展性。
  • epoll:对多个打开的文件描述符,可以高效地针对 IO 等待进行系统调用,提升服务器应用程序的性能。
  • 模块 IO 调度:对调度块设备 IO 提供可插拔的调度算法。
  • DebugFS:内核使用该接口可实现数据在用户级别的暴露。
  • Cpusets:进程独占的 CPU 分组。
  • 自愿内核抢占:提供了低延时的调度,并且避免了完全式抢占的复杂性。
  • inotify:文件系统事件的监测框架。
  • blkstrace:跟踪块 IO 事件的框架和工具。
  • splice:将数据在文件描述符和管道之间快速移动,而不经过用户空间。
  • 延时审计:跟踪每个任务的延时状态。
  • IO 审计:测量每个任务的延时状态
  • DynTicks:动态的 tick,当不需要时,内核定时中断不会触发,可以节省 CPU 的资源和电力。
  • SLUB:新的 slab 内存分配器的简化版本。
  • CFS:完全公平调度算法。
  • cgroups:控制组可以测量并限制进程组的资源使用。
  • TCP LRO(large receive offload):TCP 大型接受卸载允许网络驱动和硬件在将数据包发送到网络栈之前将其聚合成较大的体积。
  • latencytop:观测操作系统的延时来源的仪器和工具。
  • tracepoints:静态内核跟踪点可以组织内核里的逻辑执行点,用于跟踪工具。
  • perf:一套性能观测工具。
  • 没有 BKL:最终消除了大内核锁的性能瓶颈。
  • 透明巨型页
  • KVM(kernel-based virtual machine):基于内核的虚拟机技术。
  • BPF JIT:通过将 BPF 字节码编译为本地指令来提高包过滤性能。
  • CFS 带宽控制:一种 CPU 调度算法,支持 CPU 配额和节流。
  • TCP 防缓冲器:解决缓冲区膨胀问题。
  • uprobes:用于动态跟踪用户级软件的基础设施。
  • TCP 早期重传:用于减少触发快速重传所需的重复确认。
  • TFO:TCP 快速打开可将三次握手减少到一个带有 TFO cookie 的 SYN 包,从而提高性能。
  • NUMA 平衡:增加了内核在多 NUMA 系统上自动平衡内存位置的方法,减少了 CPU 互联流量并提高性能。
  • SO_REUSEPORT:一个套接字选项,允许多个监听器套接字绑定到同一个端口,提高了多线程的可扩展性。
  • SSD 缓存设备:设备映射器支持固态硬盘设备被用作较慢旋转磁盘的缓存。
  • bcache:一种用于块接口的 SSD 缓存技术。
  • TCP TLP:TCP 尾部丢失探测,在较短时间探测到超时后会发送新数据或最后一个未确认的端,避免了高成本的基于定时重传,从而触发更快的恢复。
  • NO_HZ_FULL:无计时器的多任务或无时钟的内核,允许非空闲线程在没有时钟跳动的情况下运行,避免了工作负载的扰动。
  • 多队列块 IO:提供了每个 CPU 的 IO 提交队列,而不是单一的请求队列,提高了可扩展性。
  • SCHED_DEADLINE:一个可选的调度策略。
  • TCP autocorking:允许内核凝聚小的写操作,减少发送的数据包。
  • MCS 锁和 qspinlock:搞笑的内核锁。
  • eBPF:一个用于运行安全内核态程序的内核内执行环境。
  • Overlayfs:一个联合装载文件系统。
  • DCTCP:数据中心 TCP 拥塞控制算法,其目的是提高突发容忍度,低延时和高吞吐量。
  • DAX:直接访问允许用户空间直接从持久性内存中读取,没有缓冲区的开销。
  • TCP 无锁监听器
  • 队列自旋锁
  • cgroup v2
  • epoll 可扩展性
  • KCM:内核连接复用器
  • TCP NV:TCP 拥塞控制算法
  • XDP:基于 BPF 的可编程快速路径
  • TCP BBR:TCP 拥塞控制算法
  • 硬件延时跟踪器
  • 多队列 IO 调度器
  • 内核 TLS
  • 等等


systemd

systemd 是 Linux 服务管理器,它是作为原始 UNIX init 系统的替代品而开发的。

它的功能包括依赖感知服务启动和服务时间统计。

1
2
3
4
5
# 报告总体启动时间
systemd-analyze

# 更多信息
systemd-analyze critical-chain


KPTI

内核页表隔离(KPTI)补丁是对被称为 meltdown 的因特尔处理器漏洞的一种缓解。



eBPF

BPF(Berkeley Packet Filter),是一种灵活且高效的技术,由指令集、存储对象和 helper 函数组成。鉴于 BPF 的虚拟指令集规范,它可被认为是一个虚拟机。

BPF 程序在内核态下运行,并被配置为运行在 socket event, tracepoint, USDT probe, kprobes, uprobes 和 perf_events 等事件上。

BPF 字节码必须首先通过一个检查安全的验证器,以确保 BPF 程序不会崩溃或破坏内核。它还可以使用一个 BPF 类型格式系统来理解数据类型和结构。

因为 BPF 正在为新一代高效、安全和先进的跟踪工具提供动力,所以 BPF 对系统性能分析很重要。



其他主题



PGO内核

剖析引导的优化(PGO),使用 CPU 剖析信息来改善编辑器的决策。



unikernel

unikernel 是一个单一应用的机器镜像,它将内核、库和应用软件结合在一起,通常可以在硬件虚拟机或裸机的单一地址空间中运行。



微内核和混合内核

Unix-Like 的内核,也称为单内核,其中所有管理设备的代码都作为一个大的内核程序仪器运行。

微内核,内核软件被保持在最小的程度。一个微内核支持的基本要素包括诸如:内存管理、线程管理、进程间通信。文件系统、网络栈和驱动程序是通过用户态的软件实现的,这使得这些用户态的组件更容易被修改和替换。

微内核的一个缺点是,执行 IO 和其他功能会需要额外的 IPC 步骤,从而降低了性能。一个解决方案是混合内核。它结合了单内核和微内核的优点。



分布式操作系统

分布式操作系统在一组独立的计算机节点上运行一个操作系统实例,并将其连接成网络。



内核比较



观测工具

本章目标:

  • 认识静态性能工具和危机排查工具。
  • 了解工具的类型和工具运行的开销。
  • 观测数据的来源,包括:/proc, /sys, tracepoint, kprobes, uprobes, USDTPMC
  • 了解如何配置 sar 来做归档统计。


工具范围

https://raw.githubusercontent.com/zhang21/images/master/cs/operatingsystem/systems-performance/4-1.png



静态性能工具

静态性能工具检查系统在静止状态下的特性,而不是在主动工作负载下的特性。

https://raw.githubusercontent.com/zhang21/images/master/cs/operatingsystem/systems-performance/4-2.png



危机处理工具

这些工具可能需要额外安装。一下软件包基于 Ubuntu/Debian,其他 Linux 发行版软件名称可能有所不同。

软件包 提供的工具
procps ps vmstat uptime top
util-linux dmesg lsblk lscpu
sysstat iostat mpstat pidstat sar
iproute2 ip ss nstat tc
numactl numstat
linut-tools-common perf turbostat
bcc-tools opensnoop execsnoop runqlat softirqs ext4... bios... tcp...
bpftrace bpftrace
trace-cmd trace-cmd
nicstat nicstat
ethtool ethtool
tiptop tiptop
msr-tools rdmsr wrmsr

在容器环境下,可以考虑创建一个特权调试容器,拥有系统的完全访问权限和所有安装工具。



工具类型

性能观测工具可按系统级别和进程级别来分类,多数工具要么基于计数器,要么基于事件。



固定计数器

内核维护了各种提供系统统计信息的计数器。通常,计数器被实现为无符号的整型,发生事件时递增。


系统级别的:

  • vmstat:统计虚拟内存和物理内存。
  • mpstat:检测每个 CPU 的使用情况。
  • iostat:检测每个磁盘 IO 的使用情况,由块设备接口报告。
  • nstat:TCP/IP 栈的统计。
  • sar:各种统计,能归档历史数据。

进程级别的:

  • ps:进程状态,显示进程的各种统计信息。
  • top:按一个统计数据排序进程
  • pmap:将进程的内存段和使用统计一起列出


剖析

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


系统级别的:

  • perf:Linux 中的标准剖析器
  • profile:一个来自 BCC 代码库的基于 BPF 的 CPU 剖析器,在内核上下文中对栈进行频率统计。

进程级别的:

  • grof:GNU 剖析工具,分析由编译器添加的剖析信息。
  • cachegrind:可对硬件缓存的使用情况进行剖析。


跟踪

跟踪(tracing)每一次发生的记录事件,并可以存储事件的细节信息,供以后分析或生成摘要。

请注意开销。


系统级别的:

  • tcpdump:网络包跟踪(使用 libpcap 库)
  • biosnoop:块 IO 跟踪
  • execsnoop:新的进程跟踪
  • perf:Linux 标准剖析器
  • perf trace:子命令,跟踪系统级别的系统调用
  • ftrace:Linux 内置的跟踪器
  • bcc:基于 BPF 的跟踪库和工具集
  • bpftrace:基于 BPF 的跟踪器和工具集

进程级别的:

  • strace:系统调用跟踪
  • gdb:代码级别的调试器


监测

监测持续记录数据,以备日后需要。


sar(system activity report)命令,监测单个操作系统主机,基于计数器,在预定的时间(通过 cron)执行以记录系统级别计数器的状态。

sar 可提供对 CPU、内存、磁盘、网络工作、中断、电源使用等的洞察力。


网络监测的传统技术是简单网络管理协议(snmp)。但现在大多数环境已改用基于自定义代理的监测来代替。


现代监测软件在每个系统上运行代理,以记录内核和应用程序的指标。如 Prometheus 等软件。



监测来源

Linux 监测来源:

类型 来源
进程级计数器 /proc
系统级计数器 /proc /sys
设备配置与计数器 /sys
cgroup 统计 /sys/fs/cgroup
进程级跟踪 ptrace
硬件计数器 perf_event
网络统计 newlink
捕获网络数据包 libpcap
线程级延时指标 延时审计
系统级跟踪 函数剖析、软件事件、tracepoint kprobes uprobes


/proc

/proc 提供内核统计信息的文件系统接口,它由内核动态创建,不需要任何存储设备(在内存中运行)。它包含很多目录,其中以 PID 命名的目录代表那个进程的信息。



进程级别的统计,信息如下。

1
ls -F /proc/${PID}

与进程性能观测相关的文件描述如下:

  • limits:实际的资源限制
  • maps:映射的内存区域
  • sched:CPU 调度器的各种统计
  • schedstat:CPU 运行时、延时和时间分片
  • smaps:映射内存区域的使用统计。
  • stat:进程状态和统计信息。
  • statm:以页为单位的内存使用总结。
  • status:标记过的 stat 和 statm 的信息。
  • fd/:文件描述符符号链接的目录(也见 fdinfo)。
  • cgroup:cgroup 成员信息。
  • task/:每个任务的统计目录。


系统级别的统计,信息如下。

/proc 目录中,除去 PID 信息,其他的就是系统级别的统计数据。

1
cd /proc; ls -Fd [a-z]*

与系统性能观测相关的文件描述如下:

  • cpuinfo:物理处理器信息。
  • diskstats:所有磁盘设备信息。
  • interrupts:每个 CPU 的中断计数器。
  • loadavg:平均负载。
  • meminfo:内存明细。
  • net/dev:网络接口信息。
  • net/netstat:网络统计。
  • net/tcp:活跃的 TCP 套接字信息。
  • presure:压力滞留信息。
  • schedstat:CPU 调度统计。
  • self:关联当前 PID 路径的符号链接。
  • slabinfo:内核 slab 分配器缓存统计。
  • stat:内核和系统资源的信息。
  • zoneinfo:内存区信息。


/proc/stat 提供了系统级别的 CPU 使用率统计信息,这些数据被许多工具所使用。

这些统计数据的准确性取决于内核的配置,默认配置(CONFIG_TICK_CPU_ACCOUNTING)测量 CPU 使用率的粒度为时钟刻度,可能是 4ms(取决于 CONFIG_HZ)。

/proc 文件通常是文本内容,很容易读取和处理。



/sys

Linux 提供了一个 sysfs 文件系统,挂载在 /sys。这在内核 2.6 版本中引入,为内核统计提供了一个基于目录的结构。

/proc 不同的是,/sys 经过一段时间的发展,把各种系统信息放在了顶层目录。

/sys 文件系统有上万行统计信息放在只读文件里,还有很多可写的文件用于调整内核状态。例如用 0 和 1 或其它文本控制状态,然后在命令行直接通过 echo 设置状态,而不通过二进制的接口。

1
2
3
4
# redis 禁用透明大页
echo 'never' > /sys/kernel/mm/transparent_hugepage/enabled
echo 'never' > /sys/kernel/mm/transparent_hugepage/defrag



延时核算

开启 CONFIG_TASK_DELAY_ACCT 选项的 Linux 系统按一下状态跟踪每个任务的时间。

  • 调度器延时:等待轮到上 CPU。
  • 块 IO:等待块 IO 完成。
  • 交换:等待换页。
  • 内存回收:内地内存回收例程。

统计器延时源自 schedstat,用户级工具通过 taskstats 可读取这些数据。



netlink 是一个特殊的套接字地址族(AF_NETLINK),用于获取内核信息。

使用 netlink 的命令包括:ip, ss, route, ifconfignetstat

1
2
3
4
5
# 检查套接字统计工具 ss 的输出
strace ss
...
socket(AF_NETLINK, SOCK_RAW|SOCK_CLOEXEC, NETLINK_SOCK_DIAG) = 3
...

netlink 组包含如下内容:

  • NETLINT_ROUTE:路由信息。
  • NETLINK_SOCK_DIAG:套接字信息。
  • NETLINK_SELINUX:SELinux 事件通知。
  • NETLINK_AUDIT:审计。
  • NETLINK_SCSITRANSPORT:SCSI 传输。
  • NETLINK_CRYPTO:内核加密信息。


tracepoint

tracepoint 是一个基于 静态检测 的 Linux 内核事件源。它是硬编码的检测点,放置在内核代码的逻辑位置。例如,在系统调用、调度器事件、文件系统操作和磁盘 IO 的开始和结束处都有 tracepoint。

tracepoint 在技术上是指放在内核源代码中的跟踪函数(钩子)

当 tracepoint 被激活时,它会给每个事件增加少量的 CPU 开销。但每秒少于 10 000 个事件的开销可以忽略不计。

1
2
3
# 列出可用的 tracepoint
perf list tracepoint



kprobes

kprobes(内核探针)是一个 Linux 内核事件源,用于基于 动态检测 的跟踪器。

kprobes 标准的方法是修改运行中的内核代码的指令文本,在需要的地方插入逻辑检测。

kprobes 很重要,因为这是对于生产环境中内核行为的最后信息来源,可提供近乎无限的信息。

1
2
bpftrace -e 'kprobe:do_nanosleep { printf("sleep by: %s\n", comm); }'



uprobes

uprobes(用户空间探针)可动态地检测应用程序和库中的函数。

1
2
3
# 列出 bash 中可能的 uprobes 函数入口
bpftrace -l 'uprobe:/bin/bash:*'



用户级静态定义跟踪

用户级静态定义跟踪(USDT),是用户空间版本的 tracepoint。

usdt 探针必须被编译到所观测的可执行文件中。usdt 探针在 linux 中是使用 uprobes 实现的。

1
2
3
# openjdk 的 usdt 探针
bpftrace -lv 'usdt:/usr/lib/jvm/openjdk/libjvm.so:*'



硬件计数器

处理器和其他设备通常有硬件计数器用于观测活动。这类硬件计数器通常被称为 性能监测计数器(PMC)。

Intel 架构中的 PMC 事件:

  • UnHalted Core cycles
  • Instruction Retired
  • UnHalted Reference Cycles
  • LLC References
  • LLC Misses
  • Branch Instruction Retired
  • Branch Misses Retired
1
2
3
# 对 PMC 进行检测
perf stat



其他观测源

其他的观测源:

  • MSR:PMC 是使用特定的寄存器(MSR)来实现的。
  • ptrace:此系统调用能控制进程跟踪。
  • 函数剖析
  • 网络嗅探(libpcap)
  • netfilter 连接跟踪
  • 进程核算
  • 软件事件
  • 系统调用


sar

sar(system activity reporter) 是一个必不可少的系统性能工具,它是通过 sysstat 包提供的。



sar覆盖范围

https://raw.githubusercontent.com/zhang21/images/master/cs/operatingsystem/systems-performance/4-6.png



sar监测

Linux 系统是已启用 sar 数据监测。如果没有,你需要启用它。

sadf 命令可查看不同格式的 sar 统计数据。

1
2
3
4
sar

# 1s 间隔显示 5次
sar -n TCP 1 5


跟踪工具

主要跟踪工具:

  • perf
  • Ftrace
  • BPF(BCC, bpftrace)
  • SystemTap
  • LTTng



应用程序

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

本章目标如下:

  • 描述性能优化的目标。
  • 熟悉提高性能的技术,包括多线程编程、哈希表和非阻塞 IO。
  • 理解常见的锁和同步原语。
  • 了解不同编程语言所带来的挑战。
  • 遵顼线程状态分析方法。
  • 进行 on-CPU 和 off-CPU 的剖析。
  • 执行系统调用分析,包括跟踪进程的执行。
  • 了解栈踪迹的弊端:符号和栈的缺失。


应用程序基础

试着回答以下关于应用程序的问题:

  • 功能:应用程序的角色是什么?
  • 操作:应用程序服务哪些请求?
  • 性能要求:有没有服务水平目标(SLO)?
  • CPU 模式:用户级还是内核级的软件实现?
  • 配置:应用程序如何配置?
  • 主机:承载应用程序的是什么?
  • 指标:有没有什么可用的应用程序指标?
  • 日志:创建的操作日志有哪些?
  • 版本
  • Bug
  • 源代码
  • 社区
  • 图书
  • 专家


性能目标

设立性能目标能为你的性能分析工作指明方向。没有清晰的目标,性能分析容易沦为随机的钓鱼探险。

性能目标可能:

  • 延时
  • 吞吐量
  • 资源使用率
  • 价格/成本


常见情况的优化

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



可观测性

选择成熟的、有很多观测工具的语言和框架。



时间复杂度

大 O 标记法,用于分析算法的复杂度。选择更高效率和性能的算法。

标记法 示例
$O(1)$ 布尔判断
$O(log^n)$ 顺序数组的二分搜索
$O(n)$ 链表的线性搜索
$O(nlog^n)$ 快速排序
$O(n^2)$ 冒泡排序
$O(2^n)$ 分解质因素、指数增长
$O(n!)$ 旅行商人问题的穷举法

https://raw.githubusercontent.com/zhang21/images/master/cs/operatingsystem/systems-performance/5-1.png



应用程序性能技术

提高应用程序性能的常用技术:IO 大小、缓存、缓冲区、轮询、并发、并行、非阻塞 IO 和处理器绑定。



选择IO尺寸

执行 IO 的开销包括:

  • 初始换缓冲区
  • 系统调用
  • 上下文切换
  • 分配内核元素据
  • 检查进程权限和限制
  • 将地址映射到设备
  • 执行内核和驱动代码来执行 IO
  • 释放元数据和缓冲区

从效率上讲,每次 IO 传输的数据越多,效率越高。

增加 IO 尺寸是提高应用吞吐量的常用策略。考虑到每次 IO 的固定开销,一次 IO 传输 128KB 要比 128 次传输 1KB 高效得多。

如果应用不需要,较大的 IO 尺寸也会有负面效果。一个执行 8KB 随机读取的数据按 128KB 尺寸运行会慢得多,因为 120KB 数据的传输能力都被浪费了。

选择更贴近应用所需 IO 尺寸,能降低引起的 IO 延时。不必要的大尺寸 IO 还会浪费缓存的空间。



缓存

应用使用缓存提高文件系统的读性能和内存的分配性能。将常用的数据保存在缓冲中以备后用,而非总是执行较大的开销。数据库缓冲区的高速缓存就是一例,该缓存会保存经常执行的数据库查询的数据。

缓存(cache)提高了读性能,存储常用缓冲区(buffer)来提高写性能。



缓冲区

为了提高写性能,数据在被送入下一层级之前会被合并放在缓冲区中。这增加了 IO 大小,提升了操作的效率。取决于写操作的类型,这样做可能会增加写延时,因为第一次写入缓冲区后,在发送前,还要等待后续的写入。



轮询

轮询是等待某一事件发生的技术,该技术在循环中检查事件状态,两次检查之间有停顿。它有一些潜在的性能问题:

  • 重复检查的 CPU 开销高昂。
  • 事件发生和下一次检查之间的延时较高。

应用应能改变自身行为来监听事件发生,当事件发生时立即通知应用程序并执行相应的例程。


epoll() 系统调用,用来检查文件描述符的状态,提供与轮询相似的功能。不过它是基于事件的,因此没有轮询那样的性能负担。



并行和并发

分时系统支持程序的并发:装载和开始执行多个程序的能力。虽然它们的运行时间是重叠的,但并不一定在同一瞬间同一 CPU 上执行。

为了利用多处理器,程序需要在同一时间运行在多个 CPU 上。这称为 并行,通过 多进程多线程 实现。多线程(多任务)更为高效,因此是首选。

使用多进程/多线程架构,意味着允许内核通过 CPU 调度器来决定谁运行,这是以上下文切换开销为代价的。另一种方法是让用户态的程序实现自己的调度机制和程序模型。机制包括:

  • 纤程(轻量级线程):一种用户态的线程,每个代表一个可调度的程序。
  • 协程:比纤程更轻,协程是一个子程序,由用户态的应用程序进行调度,提供一种并发机制。
  • 基于事件的并发:程序被分解成一系列的事件处理程序,可运行的事件从队列中做计划和执行。

一些运行时使用协程来实现轻量级并发,又使用多个操作系统线程来实现并行。一个典型的例子是 golang 的 goroutine 。


多线程编程的三种常见模式:

  • 服务线程池:一个线程池为网络请求提供服务,每个线程一次为一个客户连接提供服务。
  • CPU 线程池:每个 CPU 创建一个线程。常用于长周期的批处理,如视频编码。
  • 分阶段事件驱动架构:应用请求被分解为多个阶段,每个阶段有一个或多个线程的线程池来处理。

由于多线程编程与进程共享相同的地址空间,所以线程可以直接读写相同的内存,而不需要更高开销的接口。为了保证完整性,会使用同步原语,这样数据就不会因为多个线程同时读写而被破坏。


同步原语 管理对内存的访问,以确保完整性。三种常用同步原语类型:

  • 互斥锁:只有锁的持有者可以操作。其他线程则在 CPU 之外阻塞和等待。
  • 自旋锁:自旋锁允许持有者操作,而其他线程在紧密的循环中在 CPU 上自旋,检查锁是否被释放。被封锁的线程从未离开 CPU,一旦锁可用,就可在几个周期内运行。但这些线程也在自旋等待上浪费了 CPU 资源。
  • 读写锁:读写锁通过要么允许多个读者,要么只允许一个写者而不允许有读者来确保完整性。
  • 信号量:一个变量类型,可通过计数来允许一定数量的并行操作,也可以是一个允许单个操作的二进制结果。

根据锁的状态不同,有三种路径:

  • 快速路径:试图用 cmpxchg 指令来获取锁,并将其发送给所有者。只有在锁没有被持有时才会成功。
  • 中速路径:也称为乐观自旋,在锁持有者也在运行时的自旋,希望锁很快被释放并不阻塞。
  • 慢速路径:对线程的阻塞和调度,当锁可用时会被唤醒。

可用一张锁的 哈希表 来对大量数据结构的锁做数目优化。



非阻塞IO

进程在 IO 期间会阻塞并进入 sleep 状态。阻塞 IO 模型存在两个性能问题:

  • 对于多路并发的 IO,当阻塞时,每一个阻塞的 IO 都会消耗一个线程。这样做开销很大。
  • 对于频繁发生的短时 IO,频繁切换上下文的开销会消耗 CPU 资源并增加程序的延时。

非阻塞 IO 模型是异步地发起 IO,不阻塞当前的线程,线程可以执行其他工作。这个 Node.js 的一个关键特性。

执行非阻塞(异步)IO 的机制如下:

  • open():通过 O_ASYNC 标志。当文件描述符的 IO 是可能时,会用信号通知进程。
  • io_submit():Linux 异步 IO。
  • sendfile():将数据从一个文件描述符复制到另一个文件描述符,将 IO 推迟到内核,而不是用户级。
  • io_uring_enter():Linux 的 io_uring 允许在用户和内核空间之间使用一个共享的环形缓冲区来提交异步 IO。


处理器绑定



编程语言

编程语言可能是编译型或解释型,也可能是通过虚拟机执行的。



编译型语言

编译 是在运行之前将程序生成机器指令,这些指令存储在被称为二进制的可执行文件中。编译型语言包括 C, C++ 和汇编语言等。某些语言可能同时具有解释器和编译器。

可以使用编译器优化来提升性能,它能对 CPU 指令的选择和部署做优化。



解释型语言

解释型语言程序的执行是将语言在运行时翻译成行为,这一过程会增加执行的开销。解释型语言并不期望能表现出很高的性能,而是易于编程和调试。shell 脚本是一个例子。



虚拟机

语言虚拟机(或进程虚拟机),是模拟计算机的软件。如 Java 和 Erlang 都是用虚拟机执行的,它提供了平台独立的编程环境。先将应用程序编译成虚拟机指令集(字节码),再由虚拟机执行。这样编译的对象就具有了可移植性,只要有虚拟机就能运行。



垃圾回收

一些语言使用自动内存管理,分配的内存不需要被显式地释放,留给异步的垃圾回收(GC)来处理。

虽然这让程序更易编写,但也有缺点:

  • 内存增长
  • CPU 成本
  • 延时异常值

GC 是常见的性能调整对象,用以降低 CPU 成本和减少延时异常值的发生。如 Java 虚拟机提供了许多可调参数来设置 GC 类型、GC 的线程数、堆大小和空闲率等。



应用程序调优方法

应用程序分析和调优的方法。

方法 类型
CPU 剖析 观测分析
off-CPU 分析 观测分析
系统调用分析 观测分析
USE 方法 观测分析
线程状态分析 观测分析
锁分析 观测分析
静态性能调优 观测分析、调优
分布式跟踪 观测分析


应用程序性能观测工具

应用程序性能观测工具。

工具 说明
perf CPU 剖析、CPU 火焰图、系统调用跟踪
profile 使用基于时间的采样做 CPU 剖析
offcputime 用调度器跟踪做 off-CPU 剖析
strace 系统调用跟踪
execsnoop 新进程跟踪
syscount 系统调用审计
bpftrace 信号跟踪、IO 剖析、锁分析


perf

perf(performance analysis tools)的优点是使用每个 CPU 的缓冲区来减少开销,使 perfstrace 实现更安全。它还可以跟踪整个系统,而不仅限于进程。

perf trace 命令最终可以成为 strace 的换入的替代。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# cpu 剖析
# 在所有 cpu(-a)上以 49HZ (-F 49)对栈踪迹(-g)采样 10s
perf record -F 49 -a -g -- sleep 10


# cpu 火焰图
perf record -F 49 -a -g -- sleep 10; perf script --header > out.stacks
# 然后通过开源工具脚本 FlameGrap 通过上面文件生成 svg 文件,在浏览器中打开。


# 系统调用跟踪
# 跟踪一个 mysql 服务器进程
perf trace -p $(pgrep mysqld)


# 内核时间分析
# -s 输出系统调用的汇总
perf trace -s -p $(pgrep mysqld)


# IO 剖析
# 使用过滤器(-e)跟踪某个调用
perf trace -e sendto -p $(pgrep mysqld)


profile

profile 使用 BPF 来减少开销,在内核上下文中聚合栈踪迹,并且一次性地将栈和它们的计数传递给用户空间。

1
2
3
4
5
6
7
# 安装 bcc
yum install bcc bcc-tools
# bcc 工具目录 /usr/share/bcc/tools/

# 在所有 CPU 上以 49HZ 的频率采样,持续 10s。
profile -F 49 10



offcputime

offcputime 是一个 bcc 和 bpftrace 工具,用于汇总线程被阻塞和离开 CPU 的时间,通过显示栈踪迹来解释原因。

offcputimeprofile 的互补工具,它们显示了线程在系统中花费的全部时间。

1
2
3
# 跟踪 5s
offcputime 5



strace

strace 命令是 Linux 中系统调用的跟踪器。

警告:strace 通过 linux 的 ptrace 接口采用了基于端点的跟踪,这是一种侵入性的做法,可能会影响程序的性能。

未来 strace 可能通过成为 perf trace 子命令的别名来解决其开销问题。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
strace -ttt -T -p ${PID}

# -c 调用汇总
# 注意 strace 对 dd 程序性能的严重影响。
strace -c dd if=/dev/zero of=/dev/null bs=1k count=500k
524288000 bytes (524 MB) copied, 123.663 s, 4.2 MB/s

dd if=/dev/zero of=/dev/null bs=1k count=500k
524288000 bytes (524 MB) copied, 0.191729 s, 2.7 GB/s

perf trace -s dd if=/dev/zero of=/dev/null bs=1k count=500k
524288000 bytes (524 MB) copied, 4.50812 s, 116 MB/s


execsnoop

execsnoop 是一个 bcc 和 bpftrace 工具,它可以跟踪系统级别的新进程的执行。它可以发现消耗 CPU 资源的短命进程的问题,也可用来对软件做测试。

execsnoop 的工作是跟踪 execve 系统调用,并为每个调用打印出一行摘要。

1
2
execsnoop



syscount

syscount 是系统级别的统计系统调用的 bcc 和 bpftrace 工具。

1
2
syscount



bpftrace

bpftrace 是一个基于 bpf 的跟踪器,它提供了一种高级编程语言,允许创建强大的单行命令和短脚本,非常适用于根据其他工具的线索进行定制的应用程序分析。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# Files opened by thread name
bpftrace -e 'tracepoint:syscalls:sys_enter_open { printf("%s %s\n", comm, str(args->filename)); }'

# Syscall count by thread name
bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @[comm] = count(); }'

# Read bytes by thread name:
bpftrace -e 'tracepoint:syscalls:sys_exit_read /args->ret/ { @[comm] = sum(args->ret); }'

# Read size distribution by thread name:
bpftrace -e 'tracepoint:syscalls:sys_exit_read { @[comm] = hist(args->ret); }'

# Show per-second syscall rates:
bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @ = count(); } interval:s:1 { print(@); clear(@); }'

# Trace disk size by PID and thread name
bpftrace -e 'tracepoint:block:block_rq_issue { printf("%d %s %d\n", pid, comm, args->bytes); }'

# Count page faults by thread name
bpftrace -e 'software:faults:1 { @[comm] = count(); }'

# Count LLC cache misses by thread name and PID (uses PMCs):
bpftrace -e 'hardware:cache-misses:1000000 { @[comm, pid] = count(); }'

# Profile user-level stacks at 99 Hertz for PID 189:
bpftrace -e 'profile:hz:99 /pid == 189/ { @[ustack] = count(); }'

# Files opened in the root cgroup-v2
bpftrace -e 'tracepoint:syscalls:sys_enter_openat /cgroup == cgroupid("/sys/fs/cgroup/unified/mycg")/ { printf("%s\n", str(args->filename)); }'



CPU

本章目标:

  • 理解 CPU 模型和概念
  • 熟悉 CPU 硬件机制
  • 熟悉 CPU 调度器机制
  • 了解分析 CPU 的不同方法
  • 解释平均负载和 PSI
  • 归纳系统级别和每个 CPU 的使用率
  • 识别和量化调度器延时问题
  • 通过 CPU 周期分析找出效率瓶颈
  • 通过剖析器和 CPU 火焰图调查 CPU 用量
  • 找出 CPU 软硬中断的消费者
  • 解释 CPU 火焰图和其他 CPU 可视化方案
  • 了解 CPU 的可调参数


术语

  • 处理器:物理芯片,以核或硬件线程的方式包含多块 CPU。
  • 核:一个独立 CPU 实例。
  • 硬件线程:一种支持在一个核上同时执行多个线程的 CPU 架构,每个线程是一个独立的 CPU 实例。
  • CPU 指令:单个 CPU 操作,来源于它的指令集。指令用于算术操作、内存 IO,以及逻辑控制。
  • 逻辑 CPU:又被称为虚拟处理器。
  • 调度器:把 CPU 分配个线程运行的内核子系统。
  • 运行队列:一个等待 CPU 服务的可运行线程队列。


模型



CPU架构

一个 4核 8线程 的 CPU 架构示例。

每个硬件线程都可以按逻辑 CPU 寻址,因此看上去有 8块 CPU。

https://raw.githubusercontent.com/zhang21/images/master/cs/operatingsystem/systems-performance/6-1.png



CPU内存缓存

为了提高内存 IO 性能,处理器提供了多种硬件缓存。越贵越小越快,并越靠近 CPU。

https://raw.githubusercontent.com/zhang21/images/master/cs/operatingsystem/systems-performance/6-2.png



CPU运行队列

一个由内核调度器管理的 CPU 运行队列。

https://raw.githubusercontent.com/zhang21/images/master/cs/operatingsystem/systems-performance/6-3.png



概念

CPU 的时钟频率和如何执行指令。



时钟频率

时钟是一个驱动所有处理器逻辑的数字信号。每个 CPU 指令都会花费一个或多个时钟周期(CPU 周期)来执行。CPU 以一个特定的时钟频率执行,如一个 4GHZ 的 CPU 每秒运行 40 亿个时钟周期。

有些处理器可以改变时钟频率,升频或降频以改变性能。

时钟频率经常被当作处理器的主要指标,但这有可能让人误解。即使系统的 CPU 看上去已被完全利用,但更快的时钟频率并不一定会提高性能。它取决于在快速 CPU 周期里处理器到底在做些什么。如果大部分时间是 CPU 停止等待内存访问,那更快的执行速度实际上并不能提高 CPU 指令的执行效能或负载吞吐量。



指令

CPU 执行指令集中的指令。一个指令包括以下步骤,每个步骤都由 CPU 的一个叫作功能单元的组件处理:

  • 指令预取
  • 指令解码
  • 执行
  • 内存访问
  • 寄存器回写

这里的每一步都至少需要一个时钟周期来执行。内存访问经常是最慢的,它通常需要几十个时钟周期来读写主存。在此期间指令执行陷入停滞(这些周期称为停滞周期)。这就是 CPU 缓存如此重要的原因:它可以极大地降低内存访问需要的周期数。



指令流水线

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

指令流水线可以把一条指令分解为多个简单步骤使其可以并发执行。这取决于处理器,这些步骤可能成为处理器后端执行的位操作,这种处理器的前端负责取指令和分支预测。



指令宽度

同一类型的功能单元可以有好几个,这样在每个时钟周期里就可以处理更多的指令。这种 CPU 架构被称为超标量,通常和流水线一起使用以达到高指令吞吐量。

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



指令尺寸

x86 这样的复杂指令集计算机(CISC),指令尺寸最大可以达到 15 字节。

ARM 作为精简指令集计算机(RISC)的代表,AArch32 架构指令为 4 字节,而 ARM Thumb 的指令为 2字节或4字节。



同步多线程

同步多线程(SMT)通过超标量架构和硬件多线程技术来提高并发度。它允许一个 CPU 核心运行一个以上的线程,在指令执行期间有效地在线程之间进行调度。

例如,当一个指令在内存 IO 上停滞时,内核把这些硬件线程表示为虚拟 CPU,并把它们当作正常的 CPU 调度进程和线程。

Intel 的超线程技术是 SMT 的一个实现例子,每个核有两个硬件线程。

每个硬件线程的性能与单独的 CPU 核不同,并且取决于负载。为了避免性能问题,内核把 CPU 的负载分配到不同的和上,这样每个核上都只有一个繁忙的硬件线程,不会有硬件线程争用核。停滞周期密集(低 IPC)的负载比那些运算周期密集(高 IPC)的负载往往有更好的性能,这是因为停滞周期降低了对核的争用。



每周期指令数

每周期指令数(IPC)是一个重要的高级指标,描述 CPU 如何使用它的时钟周期,同时也可以用来理解 CPU 使用率的本质。这个指标也可以被表示为 每指令周期数(CPI)。

IPC 较低表示 CPU 经常陷入停滞,通常都是在访问内存。IPC 较高则代表 CPU 基本没有停滞,指令吞吐量较高。这些指标指明了性能调优的主要方向。

内存访问密集的负载,可通过以下方法提高性能:

  • 使用更快的内存。
  • 提高内存本地性。
  • 减少内存 IO 数量。
  • 使用更高时钟频率的 CPU 并不能达到预取的性能目标。因为 CPU 还是需要为等待内存 IO 完成而花费同样的时间。

需要注意,IPC 代表了指令处理的效率,并不代表指令本身的效率。



使用率

CPU 使用率高并不一定代表有问题,仅仅表示系统正在工作。和其他资源不同,CPU 在高使用率的情况下,性能并不会出现显著下降,因为内核支持优先级、抢占和分时共享。这些概念组合起来让内核决定了什么线程的优先级更高,并保证它优先运行。

CPU 使用率的测量包括所有符合条件活动的时钟周期,还包括内存停滞周期。如经常停滞等待 IO而导致高使用率,而不仅是在执行指令。

CPU 使用率通常被分为内核时间和用户时间。



内核时间和用户时间

  • 内核时间:CPU 花在执行内核级软件的时间。包括系统调用、内核线程和中断的时间。
  • 用户时间:CPU 花在执行用户级软件上的时间。

在整个系统范围内进行测量时,用户时间和内核时间之比解释了运行的负载类型。

计算密集型的应用几乎会把大量的时间用户用户级代码上,如图像处理、机器学习和数据分析等。

IO 密集的应用的系统调用频率较高,通过执行代码进行 IO 操作。



饱和度

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

一个饱和运行的 CPU 不像其他类型的资源那样问题重重,因为更高优先级的工作可以抢占当前线程。



抢占

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



优先级反转

优先级反转指的是一个低优先级的线程拥有了一项资源,从而阻塞了高优先级线程的运行的情况。这降低了高优先级工作的性能,因为它被阻塞只能等待。

此问题可以通过优先级继承机制解决。



多进程和多线程

应用可以根据 CPU 数目进行有效放大的额能力称为扩展性。

应用在多 CPU 上扩展的技术分为多进程和多线程。

https://raw.githubusercontent.com/zhang21/images/master/cs/operatingsystem/systems-performance/6-4.png


在 Linux 上可使用多进程和多线程模型,而这两种技术都是由任务实现的。下面是多进程和多线程的属性。

属性 多进程 多线程
开发 较简单。使用 fork 或 clone 使用线程 API
内存开销 每个进程不同的地址空间消耗了一些内存资源 小。只需要额外的栈和寄存器空间,以及存放线程局部数据的空间
CPU 开销 fork/clone/exit 的开销,其中包括 MMU 管理地址空间的开销 小。API 调用
通信 通过 IPC。导致了 CPU 开销,包括为了在不同地址空间之间移动数据而导致的上下文切换,除非使用共享内存区域。 最快。直接访问共享内存。通过同步原语保证数据一致性
崩溃弹性 高,进程间互相独立 低,任何 bug 都会导致整个引用程序崩溃
内存使用 虽然有一些冗余的内存使用,但不同的进程可以调用 exit,并向系统返还所有的内存 通过系统分配器。这可能导致多个线程之间的 CPU 竞争,而在内存被重新使用之前会有些碎片化

多线程一般被认为优于多进程,尽管对开发者而言更难实现。

不管使用何种技术,重要的是要创建足够的进程或线程,以占据预期数量的 CPU。如果要最大化性能,那就要用到所有的 CPU。有些应用可能在更少的 CPU 上跑的更快,这是因为线程同步和内存本地性下降反而吞噬了更多 CPU 资源。



字长

处理器是围绕最大字长设计的(32位或64位),这是整数大小和寄存器宽度。字长也被普遍使用,表示地址空间大小和数据通路宽度(位宽),取决于不同的处理器实现。



编译器优化

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



架构

CPU 的架构和实现。



硬件

CPU 硬件包括了处理器和它的子系统,以及多处理器之间的 CPU 互联。



处理器

控制单元是 CPU 的心脏,运行指令预取、解码、管理执行以及存储结果。

一颗通用的双核处理器组成示例图。

https://raw.githubusercontent.com/zhang21/images/master/cs/operatingsystem/systems-performance/6-5.png


其他与性能相关的组件:

  • P-cache:预取缓存(每个核一个)
  • W-cache:写缓存(每个核一个)
  • 时钟:CPU 时钟信号生成器
  • 时间戳计数器:通过时钟递增,可获取高精度时间。
  • 微代码 ROM:快速把指令转化为电路信号。
  • 温度传感器:用于温度监测
  • 网络接口:如果集成在芯片里

处理器性能状态(P状态)与处理器电源状态(C状态)。

P状态通过在正常执行中变换 CPU 频率以提供不同级别的性能。如低频、睿频。状态可通过硬件和软件控制。

C状态通过提供不同的空闲状态,在执行期间节约能耗。



CPU缓存

多种硬件缓存往往被包含在处理器内(包括芯片上、晶粒内置、嵌入或集成),或与处理器放在一起(外置)。这样通过更快类型的内存缓存了读并缓冲了写,提高了内存性能。

它们包括:

  • 一级指令缓存(I$
  • 一级数据缓存(D$
  • 转义后备缓冲器(TLB)
  • 二级缓存(E$
  • 三级缓存,或最后一级高速缓存(可选)

https://raw.githubusercontent.com/zhang21/images/master/cs/operatingsystem/systems-performance/6-6.png



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



相联性 是定位缓存新条目范围的一种缓存特性。

  • 全相联:缓存可以在任何地方防止新条目。
  • 直接映射:每个条目在缓存里只有一个有效的地方。
  • 组相联:首先通过映射定位出缓存中的一组位置,然后再对这些使用另一个算法。

CPU 缓存经常使用组相联方法,这是在全相联(开销过大)与直接相联(命中过低)中间找一个平衡点。



CPU 缓存的另一个特征是 缓存行大小,这是一个存储和传输字节数量的单位,可提高内存的吞吐量。

x86 处理器典型的缓存行大小是 64 字节。



内存可能会同时被缓存在不同处理器的多个 CPU 里。当一个 CPU 修改了内存时,所有的缓存都需要知道它们的缓存拷贝已经失效,应该被丢弃。这样后续所有的读才会取到新修改的拷贝。这个过程叫做 缓存一致性,确保了 CPU 永远访问正确的内存状态。

缓存一致性的一个副作用是 LLC(Last Level Cache)访问延时。

  • LLC 命中,缓存行未共享:约 40 个 CPU 周期。
  • LLC 命中,缓存行与另一个核共享:约 65 个 CPU 周期。
  • LLC 命中,缓存行被另一个核修改:约 75 个 CPU 周期。

缓存一致性是设计可扩展的多处理器系统时最大的挑战之一,因为内存会被频繁修改。



内存管理单元

内存管理单元(MMU) 负责虚拟地址到物理地址的转换。

https://raw.githubusercontent.com/zhang21/images/master/cs/operatingsystem/systems-performance/6-8.png



对于多处理器架构,处理器通过共享系统总线或专用 互联 连接起来。

https://raw.githubusercontent.com/zhang21/images/master/cs/operatingsystem/systems-performance/6-9.png

互联可以连接除处理器之外的组件,如 IO 控制器。

https://raw.githubusercontent.com/zhang21/images/master/cs/operatingsystem/systems-performance/6-10.png

处理器之间的私有连接提供了无须竞争的访问以及比共享系统总线更高的带宽。

除了外部的互联,处理器还有核间通信用的内部互联。

互联通常被设计为高带宽,这样它们就不会成为系统的瓶颈。一旦成为瓶颈,性能会下降。因为牵涉互联的 CPU 指令会陷入停滞。



硬件计数器

性能监测计数器(PMC,Performance Monitoring Counter),可以是统计底层 CPU 活动的处理器寄存器,其通过硬件实现。它们通过包括下列计数器:

  • CPU 周期:包括停滞周期和停滞周期类型。
  • CPU 指令:引退的。
  • 一级、二级、三级缓存访问:命中,未命中。
  • 浮点单元:操作。
  • 内存 IO:读、写、停滞周期。
  • 资源 IO:读、写、停滞周期。

每个 CPU 都有少量的可编程的记录类似事件的寄存器,通常是 2-8 个。处理器类型和型号决定了哪些寄存器可用,在处理器手册中有记录。



GPU

图形处理单元(GPU)用来支持图形显示,其他领域(人工智能、机器学习、加密货币挖矿等)。它特别适用于高度并行的数据处理工作(如矩阵变换)。

一颗 CPU 可能包含十几个核心,而一颗 GPU 则可能包含成百上千个名为流处理器(SP)的小核心,每个小核心可执行一个线程。由于 GPU 的负载高度并行,所以可并发执行的线程组成线程块,它们之间相互协作。这些线程块由几组称为流式多处理器(SM)的 SP 执行,SM 也提供了包括内存缓存在内的其他资源。

属性 CPU GPU
封装 直接插入系统主板的插槽里,直接连接系统总线 通常作为扩展卡提供,通过扩展总线(PCIe)接入系统
封装伸缩性 多插槽的配置通过 CPU 互联相互连接 也可以由多 GPU 的配置,通过 GPU 对 GPU 的互联
一般几带几十个 有多少 SM 一般就有多少核
线程 一个典型的核可以执行两个硬件线程 一个 SM 可能包含几十到几百个 SP,每个 SP 只能执行一个线程
缓存 每个核有 L1, L2 缓存,并且可能共享一个 L3 缓存 每个 SM 有一个缓存,并且 SM 之间可能会共享一个 L2 缓存
时钟 高(如 3.5GHz) 相对低(如 1.0GHz)


软件

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



调度器

内核 CPU 调度器的主要功能:

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

https://raw.githubusercontent.com/zhang21/images/master/cs/operatingsystem/systems-performance/6-12.png



调度类

调度类管理了可运行线程的行为,特别是优先级,还有 CPU 时间是否分片,以及这些时间片的长度。通过调度策略还可以施加其他的控制,在一个调度器内进行选择,控制同一优先级线程间的调度。

https://raw.githubusercontent.com/zhang21/images/master/cs/operatingsystem/systems-performance/6-13.png


调度策略:

  • RR:轮转调度。
  • FIFO:先进先出调度。
  • NORMAL:是一种分时调度,用户进程的默认策略。
  • BATCH
  • IDLE
  • DEADLINE

调度类参数:

  • RT:为是实时类负载提供固定的高优先级。
  • O(1)
  • CFS:完全公平调度。
  • Idle:运行优先级最低的线程。
  • Deadline


NUMA分组

NUMA 系统上的性能可通过内核感知 NUMA 而得到极大提高,因为这样它可以做出更好的调度核内存分配决定。



方法

CPU 分析和调优的多种方法。



工具法

使用工具检查关键指标。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
uptime
top         # 查看哪个进程或用户是 CPU 消耗大户。
vmstat
mpstat
pidstat     # 把 CPU 消耗大户分解成用户时间和系统时间。
perf
profile
showboosts/thurboboost      # 检查当前 CPU 的时钟频率
dmesg



使用率饱和度和错误

  • 使用率
  • 饱和度:可运行线程排队等待 CPU 的程度。
  • 错误


负载特征

负载特征:

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


观测工具

CPU 性能观测工具。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
uptime      # 平均负载
cat /proc/pressure/cpu  # 查看压力滞留信息
vmstat      # 包括系统级的平均负载
mpstat      # 单个 CPU 统计信息
sar         # 历史统计信息
ps          # 进程状态
top         # 监测每个进程/线程的  CPU 用量,top 最消耗 CPU,它读取 /proc 的系统调用。
pidstat     # 每个进程/线程的 CPU 用量分解
time;ptime  # 给一个命令计时,含 CPU 用量分解
turbotstat  # CPU 时钟频率和其他状态
showboost   # CPU 时钟频率和睿频加速
pmcarch     # 显示高级 CPU 周期用量
tlbstat     # TLB 周期
perf        # CPU 剖析和 PMC 分析
profile     # CPU 栈踪迹采样
cpudist     # CPU 上运行的时间
runqlat     # CPU 运行队列延迟
runqlen     # CPU 运行队列长度
softirqs    # 软中断时间
hardirqs    # 硬中断时间
bpftrace    # CPU 分析跟踪程序


CPU 分析工具。

1
2
3
4
nvidia-smi; nvperf
inter_gpu_top
radeontop



调优

  • 编译器选项
  • 调度优先级和调度类:nice 命令调整进程的优先级,chrt 命令设置优先级和调度策略。
  • 调度器选型
  • 调节调速器
  • 能耗状态
  • CPU 绑定
  • 独占 CPU 组
  • 资源控制:cgroups 控制资源用量,CFS 调度器施加固定限制。
  • 安全启动选型
  • 处理器选型



内存

系统主存存储应用程序和内核指令,包括它们的数据,以及文件系统缓存。存放这些数据的二级存储通常是磁盘,但磁盘的处理速度比内存低很多。

主存和存储设备之间交换数据,常常成为系统瓶颈,严重影响性能。系统也有可能终止内存用量最多的进程,导致应用故障。

其他因素包括:分配内存、释放内存、复制内存,以及管理内存地址空间映射的 CPU 开销。

本章学习目标:

  • 了解内存的概念。
  • 熟悉内存的物理结构。
  • 熟悉内核和用户态内存的分配机制。
  • 对 MMU 和 TLB 有一定的了解。
  • 通过不同的方法进行内存分析。
  • 描述整个系统和每个进程的内存使用情况。
  • 识别由于内存不足引起的问题。
  • 定位地址空间和内核分配器的内存使用情况。
  • 掌握内存的可调参数。


术语

  • 主存:物理内存,描述了计算机的高速数据存储区域,通常是 DRAM。
  • 虚拟内存:一个抽象的资源。
  • 常驻内存:当前处于主存中的内存。
  • 匿名内存:无文件系统位置或路径名的内存。包括进程地址空间(称为堆)。
  • 地址空间:内存上下文。每个进程和内核都有对应的虚拟地址空间。
  • 段:标记为特殊用途的一块虚拟内存区域,如用来存储可执行或可写的页。
  • 指令文本:内存中的 CPU 指令,通常在一个段中。
  • OOM:内存耗尽。
  • 页:操作系统和 CPU 使用的内存单元。它一直以来是 4KB 或 8KB。现代的处理器允许多种页大小以支持更大的页面尺寸。
  • 缺页:无效的内存访问。使用按需虚拟内存时,这是正常事件。
  • 换页:在主存与存储设备间交换页。
  • 交换:将匿名页面转移到交换设备。
  • 交换空间:存放换页的匿名数据的磁盘区域。


概念

内存性能的重要概念。



虚拟内存

虚拟内存是一个抽象的资源,它向每个进程和内核提供巨大的、线性的并且私有的地址空间。它简化了软件开发,把物理内存的分配交给操作系统。

进程的地址空间由虚拟内存子系统映射到主存和物理交换设备。内核会按需在它们之间移动内存页,此过程在 Linux 中称为 交换



换页

换页是将页面换入和调出主存,它们分别被称为页面换入和页面换出。

  • 文件系统换页:由读写位于内存中的映射文件页引发。
  • 匿名换页:牵涉进程的私有数据:进程的堆和栈。被称为匿名是由于它在操作系统中缺乏有名字的地址。匿名页面换出要求将数据迁移到物理交换设备或交换文件,因此有损性能。


按需换页

支持按需换页的操作系统将虚拟内存映射到物理内存。

虚拟内存模型和按需换页的结果会导致任何虚拟内存也可能处于如下的某种状态:

  • 未分配。

  • 已分配,未映射(未填充并且缺页)。

  • 已分配,已映射到主存。

  • 已分配,已映射到物理交换空间。

  • 常驻内存大小(RSS):已分配的主存(C)大小。

  • 虚拟内存大小:所有已分配区域(B+C+D)。



过度提交

Linux 支持过度提交,允许分配超过系统可以存储的内存——超过物理内存和交换设备的总和。

有了过度提交,应用程序提交的内存请求就会成功,否则会失败。应用开发人员能够慷慨地分配内存并按需使用,而不是谨慎地分配内存以控制再虚拟内存的限制内。



进程交换

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

交换出一个进程,要求进程的所有私有数据必须被写入交换设备,包括进程堆(匿名数据)、打开文件表和其他仅在进程运行时需要的原数组。来自文件系统但还未修改的数据可被丢弃,需要的时候再从原来的位置读取。

进程交换严重影响性能,因为已交换出的进程需要许多磁盘 IO 才能重新运行。对于早期运行在当时的硬件上的 UNIX,最大进程只有 64KB 是合理的,但现在的系统可以运行 GB 大小的进程。



文件系统缓存用量

操作系统会将可用的内存用于文件系统缓存以提高性能。原则是:如果有可用的内存,就有效地使用它。你可能会看到启动后可用内存(free)减少的接近零,但这不会对应用程序造成影响。

文件系统缓存使用的内存可被当作未使用,因为它可以被应用程序重用。因为在需要的时候,内核能够很快从文件系统缓存中释放内存。



使用率饱和度

对内存的需求超过了主存的情况被称为主存饱和。这时操作系统会使用换页、进程交换或 OOM来释放内存。



分配器

当虚拟内存处理多任务物理内存时,在虚拟地址空间中实际分配和放置内存时通常由分配器来处理。

分配器对性能有显著的影响。



共享内存

内存可在进程之间共享。这通常用于系统库,通过与所有使用它的进程共享其只读指令集的同一个副本来节省内存。



工作集大小

工作集大小(WSS)是一个进程运行时频繁使用的主存大小。如果 WSS 可以放入 CPU 缓存,而不是主存,那么性能可以得到巨大的提升。如果 WSS 超过了主存的大小,应用必须通过交换来执行,那么性能将大大降低。



字长

地址空间大小受限于字长的寻址空间,32 位的地址空间放不下 4GB 以上的应用程序,必须用 64 位或更大的字长来编译。



架构

内存架构,包括软件和硬件,以及处理器和操作系统的细节。



硬件

  • 主存
  • 总线
  • CPU 缓存
  • MMU(内存管理单元)


主存

常见主存类型是动态随机存取内存(DRAM)。这是一种易失性的内存,它存储的内容在断电时会丢失。

由于每个比特仅由两个逻辑零件组成(一个电容和一个晶体管),所以能提供高容量的存储。其中的电容需要定期更新以保持其电荷。



延时

主存的访问时间可用 CAS(列地址控制器)延时衡量。



主存架构

通过共享系统总线,每个 CPU 访问所有内存都有均匀的访存延时。



总线

访问方式:

  • 共享系统总线
  • 直连
  • 互联

一个通用的内存接口标准是,双倍数据速率同步动态随机访问内存(DDR SDRAM),如 DDR4-3200。

系统架构可能支持并行使用多个内存总线来增加带宽,常见的倍数为双、三或四通道。



CPU缓存

处理器通常会在芯片中包含硬件缓存以提高内存访问性能。

  • L1:通常为指令缓存和数据缓存
  • L2:同时缓存指令和数据
  • L3:更大一级的缓存

一级缓存通常按虚拟内存地址空间寻址,二级及以上缓存按物理内存地址寻址,具体取决于处理器。



内存管理单元

内存管理单元(MMU)负责虚拟地址到物理地址的转换。它按页做转换,而页内的偏移量则直接被映射。

现代处理器支持多个页大小,因此操作系统和内存管理单元也可使用不同的页大小(如 4KB, 2MB, 2GB)。Linux 的巨型页支持更大的页大小,如 2MB 甚至 1GB。

https://raw.githubusercontent.com/zhang21/images/master/cs/operatingsystem/systems-performance/7-5.png



地址转换后备缓冲器

地址转换后备缓冲器(TLB),MMU 使用 TLB 作为第一级地址转换缓存,其后是主存中的页表。TLB 可以被进一步分为指令缓存和数据页缓存。

由于 TLB 映射记录的数量有限制,所以使用更大的页可以增加从其缓存转换的内存范围,从而减少 TLB 未命中而提高系统性能。TLB 能进一步按每个不同页大小分设单独的缓存,以提高在缓存中保留更大范围映射的可能性。



软件



内存释放

当系统中的可用内存过低时,内核有多种方法释放内存,并将释放的内存添加到页的空闲链表中。

  • 空闲链表:一个未使用的页列表(空闲内存),能立即分配。
  • 页缓存:文件系统缓存。
  • 交换:页面换出。
  • 回收:当内存低于某个阈值时,内核模块和内核分配器会立即释放任何可以轻易释放的内存。
  • OOM killer


空闲链表

空闲链表能立刻定位可用内存。

释放的内存被添加到表头以便将来分配。通过页面换出守护进程释放的内存——它可能包含有价值的文件系统页缓存——被追加到表尾。如果在未被重用前有对任一页的请求,他能被回收并从空闲链表中移除。

https://raw.githubusercontent.com/zhang21/images/master/cs/operatingsystem/systems-performance/7-7.png


回收大多是从内核的 slab 分配器缓存释放内存。这些缓存包含 slab 大小的未使用的内存块,以供重用。回收将这些内存交还给系统进行分配。


内核页面换出守护进程管理利用换页释放内存。当主存中可用的空闲链表低于阈值时,页面换出守护进程(kswapd)会开始页扫描。此守护进程扫描非活动和活动内存的 LRU 页列表以释放页面。



进程虚拟地址空间

进程虚拟地址空间是一段范围的虚拟页,由硬件和软件同时管理,按需映射到物理页。这些地址被划分为段以存放线程栈、进程可执行的文本等、库和堆。

https://raw.githubusercontent.com/zhang21/images/master/cs/operatingsystem/systems-performance/7-10.png


  • 可执行文本:包括进程可执行的 CPU 指令。
  • 可执行数据:包括已初始化的变量。
  • 堆:应用程序的工作内存并且是匿名内存,按需增长并用 malloc() 分配。
  • 栈:运行中的线程栈,映射为读写。


分配器

多种用户级和内核级的分配器可用于内存分配。

https://raw.githubusercontent.com/zhang21/images/master/cs/operatingsystem/systems-performance/7-11.png



方法



工具法

对于内存而言,在 Linux 中应用工具法可检查以下指标。

  • 页扫描:它是内存压力的预兆。使用 sar -B 命令检查 pgscan 列。
  • 压力滞留信息:查看 /proc/pressure/memory 可检查内存饱和度统计。
  • 交换:内存页的交换是系统内存不足的一个迹象。
  • vmstat
  • OOM killer:可在 /var/log/messagesdmesg 中找到。
  • top
  • perf/bcc/bpgtrace:通过栈踪迹跟踪内存分配,以确定内存消耗的原因。请注意,这回产生大量的开销。


观测工具

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
# 虚拟内存和物理内存统计信息
vmstat

# 内存压力滞留信息
cat /proc/pressure/memory

# 交换设备
swapon; swapoff

# 历史统计信息
sar

# 内核 slab 分配器统计信息
# slab 信息取自 /proc/slabinfo,也可用 vmstat -m 输出
slabtop

# numa 分析
numastat

ps
top
free
oomkill
dmesg
iostat

# 进程地址空间统计信息
pmap

perf

# 直接回收跟踪
drsnoop

# 工作集大小估算
wss

bpftrace

# 包括 LLC 缺失在内的 CPU 周期使用情况
pmcarch

# 汇总 TLB 周期
tlbstat

# 页缓存统计
cachestat

# 可能的内存泄露代码路径
memleak

# 跟踪系统级的 mmap 调用
brkstack

# 跟踪内存调用的细节
shmsnoop

# 按用户栈显示缺页故障
faults

# 按文件名显示缺页故障
ffaults

# 衡量 VM 扫描器的收缩和回收时间
vmscan

# 按进程显示进入量
swapin; swapout

# 按进程显示巨型页故障
hfaults

# 显示内存库的 BIOS 信息
dmidecode

# 内存区域信息
cat /proc/zoninfo
# 内核页面伙伴分配器信息
cat /proc/buddyinfo
# 内核空闲内存页信息
cat /proc/pagetypeinfo


调优

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

  • 内核参数
  • 配置巨型页
  • 分配器
  • 资源控制


内核可调参数

Linux 内存可调参数

选项 默认值 描述
vm.dirty_background_bytes 0 触发 pdflush 后台回写的脏内存量
vm.dirty_backgroud_ratio 10 触发 pdflush 后台回写的脏系统内存百分比
vm.dirty_bytes 0 触发一个写入进程开始回写的脏内存量
vm.dirty_ratio 20 触发一个写入进程开始回写的脏系统内存
vm.dirty_expire_centisecs 3000 脏内存复合 pdflush 条件的最短时间
vm.dirty_writeback_centisecs 500 pdflush 唤醒时间间隔
vm.min_free_kbytes 动态的 设置期望的空闲内存量
vm.watermark_scale_factor 10 控制唤醒和睡眠 kswapd 水位之间的距离(单位万分之一)
vm.watermark_boost_factor 5000 kswapd 在内存碎裂时扫描多大范围的高水位(单位万分之一)
vm.percpu_pagelist_fraction 0 可分配个每个 CPU 页列表的最大页面比例(10 表示十分之一)
vm.overcommit_memory 0 0 表示利用探索法允许合理的过度分配;1 表示一直过度分配;2 表示禁止过度分配
vm.swappiness 60 相对于页面缓存回收更倾向于交换释放内存的程度
vm.vfs_cache_pressure 100 回收缓存的目录和 inode 对象的程度。越小回收越小,0 意味着从不回收,容易导致内存耗尽
kernel.numa_balancing 1 启用自动 NUMA 页平衡
kernel.numa_calancing_scan_size_mb 256 NUMA 平衡扫描时要扫描多少 MB 的页面


多种页面大小

更大的页面能通过提高 TLB 缓存命中率(增加它的覆盖范围)来提升内存 IO 性能。现代处理器支持多种页面大小,如默认的 4KB 级 2MB 的大页面。

1
2
3
echo 50 > /proc/sys/vm/nr_hugepages
grep Huge /proc/meminfo

透明巨型页(THP)是另一种机制,它通过自动将普通页提升或降级为巨型页来使用巨型页,而无须应用程序指定巨型页。

过去,透明巨型页存在性能问题,因此阻碍了它的使用。此问题有望得到解决。



分配器

有多种为多线程应用程序提升性能的用户级分配器可用。可在编译阶段选择,也可在执行时通过 LD_PRELOAD 环境变量设置。

1
export LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libtcmalloc_minimal.so.4


NUMA绑定

在 NUMA 系统中,numactl 命令可用来将进程与 NUMA 节点绑定。这可提高那些不需要超过一个 NUMA 节点的主存的应用程序的性能。

1
numactl --membind=0 3161


资源控制

基础的资源控制包括主存限制和虚拟内存限制,可用 ulimit 实现,也可在 /etc/security/limits.conf 中进行系统级别的配置。

在 Linux 中,控制组(cgroups)的内存子系统可提供多种附加控制,包括:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# 允许的最大用户内存,包括文件缓存
memory.limit_in_bytes

# 允许的最大内存和交换空间
memory.memsw.limit_in_bytes

# 允许的内核最大内存
memory.kmem.kimit_in_bytes

# 允许的最大 tcp 缓冲区内存
memory.tcp.limit_in_bytes: 

memory.swappiness

# 0 允许 oom killer 运用于这个 cgroup,1 禁用。
memory.oom_control



文件系统

对应用程序来说,文件系统性能比磁盘性能更为重要,因为应用交互和等待的就是文件系统。文件系统通过缓存、缓冲以及异步 IO 等手段来缓解磁盘的延时对引用的影响。

本章学习目标:

  • 理解文件系统模型和概念
  • 理解文件系统负载如何影响性能
  • 熟悉文件系统缓存
  • 熟悉文件系统的内部机制和性能特性
  • 学会文件系统分析的集中方法
  • 测量文件系统延时以识别模式和异常
  • 使用跟踪工具调查文件系统用量
  • 使用微基准测试文件系统性能
  • 了解文件系统可调参数


术语

  • 文件系统:一种把数据组织成文件和目录的存储方式,提供了基于文件的访问接口,并通过文件权限控制访问。
  • 文件系统缓存:主存的一块区域,用来缓存文件系统的内容。
  • 操作:对文件系统的请求,包括 read, write, open, close, stat, mkdir 等操作。
  • IO(输入输出):这里仅指直接读写的操作,包括 read, write, stat, mkdir。
  • 逻辑IO:由应用程序发送给文件系统的 IO。
  • 物理IO:由文件系统直接发送给磁盘的 IO。
  • 块大小(记录大小):磁盘上的文件系统数据组的大小。
  • 吞吐量:当前应用程序和文件系统之间的数据传输率,单位 KB/s。
  • inode:一个索引节点是一种含有文件系统对象元数据的数据结构,其中由访问权限、时间戳和数据指针。
  • VFS(虚拟文件系统):一个为了抽象与支持不同文件系统的内核接口。
  • 卷:一个存储的实例。
  • 卷管理器:灵活管理物理存储设备的软件。


模型



文件系统接口

https://raw.githubusercontent.com/zhang21/images/master/cs/operatingsystem/systems-performance/8-1.png



文件系统缓存

读操作从缓存返回数据(缓存命中),或从磁盘返回数据(缓存未命中)。未命中的操作被存储在缓存中,并填充缓存(热身)。

文件系统缓存也可用来缓冲写操作,使之延时写入(刷新)。

https://raw.githubusercontent.com/zhang21/images/master/cs/operatingsystem/systems-performance/8-2.png



二级缓存

二级缓存可能是各种存储类型。



概念



文件系统延时

文件系统延时,测量一个文件系统逻辑请求从开始到结束的时间。应用程序的线程通常在请求时阻塞,等待文件系统请求的结束。在此情况下,文件系统的延时与应用程序的性能有着直接的成正比的关系。



缓存

文件系统通常会使用主存作为缓存提高性能。

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



随机IO与顺序IO

一连串的文件系统逻辑 IO,按照每个 IO 的文件偏移量,可分为随机 IO 和顺序 IO。

由于存储设备的某些性能特性,文件系统一直以来在磁盘上顺序和连续地存放数据,以努力减少随机 IO 的数目。当文件系统未能达成这个目标时,文件的摆放变得杂乱无章,顺序的逻辑 IO 被分解为随机的物理 IO,这种情况称之为碎片化。

https://raw.githubusercontent.com/zhang21/images/master/cs/operatingsystem/systems-performance/8-4.png



预取

常见的文件系统负载包括顺序地读大量的文件系统(如文件系统备份)。这种数据量可能太大了,放不进缓存,或只读依次而不能被保留在缓存里。在这样的负载下,由于缓存命中率偏低,所以系统性能较差。

预取是文件系统解决这个问题的通常做法。通过检查当前和上一个 IO 的文件偏移量,可检测出当前是否顺序读负载,并作出预测,在应用请求前向磁盘发出读命令,以填充文件系统缓存。

预取的预测一旦准确,应用的顺序读性能将会有显著提升。而一旦预测不准,文件系统会发起应用不需要的 IO,不仅污染了缓存,也消耗了磁盘和 IO 传输的资源。文件系统一般语序对预取参数进行调优。

https://raw.githubusercontent.com/zhang21/images/master/cs/operatingsystem/systems-performance/8-5.png



预读

预取一直也被认为是预读。最近,Linux 采用了预读这个词作为一个系统调用(readahead),允许应用显式地预热文件系统缓存。



回写缓存

回写缓存被广泛地应用于文件系统,用来提高性能。它的原理是,当数据写入主存后,就认为写入已结束并返回,之后再异步地把数据刷入磁盘。

文件系统将脏数据写入磁盘的过程被称为刷新(flushing)。

这期间牺牲了可靠性,内存中的脏数据会在断电情况下丢失,而应用认为已完成。数据可能被非完整写入,这样磁盘中的数据就处在一种被破坏的状态。

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



同步写

同步写完成的标志是,所有的数据以及必要的文件系统元数据被完整地写入到永久存储介质中。但这会比异步写慢很多。

有些应用,如数据库写日志,因完全不能承担异步写带来的数据损坏风险,而使用了同步写。

  • 单次同步写
  • 同步提交一写内容


裸IO与直接IO

  • 裸 IO:绕过了整个文件系统,直接发给磁盘地址。缺点是难以管理。
  • 直接 IO:允许应用绕过文件系统缓存使用文件系统。


非阻塞IO

一般而言,文件系统 IO 要么立刻结束(如从缓存返回),要么需要等待(如等待磁盘设备 IO)。如果需要等待,应用线程会被阻塞并让出 CPU,在等待期间给其他线程执行的机会。

在某些情况下,非阻塞 IO 正合适,因为可避免创建线程带来的额外性能和资源开销。



内存映射文件

对于某些应用和负载,可通过把文件映射到进程地址空间,并直接存取内存地址的方法来提高文件系统 IO 性能。这样可避免调用 read 和 write 系统调用和上下文开销。

内存映射通过系统调用 mmap 创建,nunmap 销毁。



元数据

元数据有关数据文件的信息。

  • 逻辑元数据:通过文件系统接口(POSIX)读出的信息。
    • 显式:读取文件信息,创建和删除文件和目录,设置文件属性。
    • 隐式:文件系统存取时间戳更新、目录修改时间更新、空闲空间信息。
  • 物理元数据:文件系统实现磁盘布局所需的信息。可能包括超级块、inode、数据块指针和空闲链表。


逻辑IO和物理IO

文件系统的工作不仅仅是在永久存储介质上提供一个基于文件的接口那么简单。它们缓存读、缓冲写,将文件映射到地址空间,发起额外的 IO 以维护磁盘上与物理布局相关的元数据,这些元数据记录了数据存储的位置。



特殊的文件系统

  • 临时文件 /tmp
  • 内核设备路径 /dev
  • 系统统计信息 /proc
  • 系统配置 /sys


访问时间戳

许多文件系统支持访问时间戳,可记录下文件和目录的访问时间,这会造成读取文件时需要更新元数据,消耗磁盘 IO 资源。

有些文件系统对访问时间戳做了优化,合并及推迟这些写操作,以减少有效负载的干扰。



容量

当文件系统装满时,性能会因为多种原因而下降。当写入数据时,需要花更多时间和磁盘 IO 来寻找磁盘上的空闲块。更小更分散也就更随机。



架构



文件系统IO栈

https://raw.githubusercontent.com/zhang21/images/master/cs/operatingsystem/systems-performance/8-6.png



VFS

(VFS)虚拟文件系统接口,给不同类型的文件系统提供了一个通用的接口。



文件系统缓存

  • 缓冲区高速缓存
  • 页缓存
  • 目录项缓存
  • inode 缓存


文件系统特性

  • 块:基于块的文件系统把数据存储在固定大小的块里。另一种是变长的块大小。
  • 区域:基于区域的文件系统余弦给文件(区域)分配了连续的空间,并按需增长。
  • 日志:文件系统日志记录了文件系统的更改,这样在系统宕机时,能原子地回放更改。
  • 写时复制
  • 擦洗:在后台读出文件系统里所有的数据块,验证校验和。
  • 其他特性:快照、压缩、内置冗余、消重、擦除等


文件系统种类

文件系统及其特性。

  • ffs(fast file system)
    • 通过把磁盘分区划分为多个柱面组以提高性能。
    • 块大小增加到 4KB,提高了吞吐量。
    • 块交叉,连续地摆放磁盘上的文件块,但中间留几个块的间隔。
  • ext3(extended file system):发布于 1999 年
    • 日志
    • 日志设备
    • Orlov 块分配器:减少随机 IO。
    • 目录索引:在文件中引入哈希 B-tree,提高目录查找速度。
  • ext4:发布于 2008 年
    • 区段:提高了数据的连续性,减少了随机 IO,提高了连续 IO 的大小。
    • 预分配:让应用程序预分配一些可能连续的空间,以提高之后的写性能。
    • 延时分配:块分配被推迟到写入磁盘,方便合并写请求。
    • 更快的 fsck:标记未分配的块和 inode 项,减少 fsck 时间。
  • xfs
    • 分配组:分区被分配为可被同时访问的相同大小的分配组。
    • 区段
    • 日志:日志改善了系统崩溃后的启动性能。
    • 日志设备:可使用外部日志设备,这样日志的负载不会和数据负载相互竞争。
    • 条带分配
    • 延时分配
    • 在线碎片整理
  • zfs
    • 池化存储:将所有被分配的存储设备放到一个池里,文件系统则从池里创建。
    • COW(写时复制)
    • 日志
    • ARC:自适应替换缓存通过使用多种缓存算法达到缓存的高命中。
    • 智能预取
    • 多预取流
    • 快照
    • ZIO 流水线
    • 压缩
    • SLOG
    • L2ARC
    • 数据消重
  • btrfs(B 树文件系统),是基于写时复制的 B 树。
    • 池存储
    • COW
    • 在线平衡
    • 区段
    • 快照
    • 压缩
    • 日志


卷和池

文件系统一直以来都是建立在一块磁盘上或一个磁盘分区上。卷和池使文件系统可以建立在多块磁盘,并可以使用不同的 RAID 策略。

卷把多块磁盘合成一块虚拟磁盘,在此之上建立文件系统。Linux 系统卷管理软件是逻辑卷管理器(LVM)。

池存储把多块磁盘放到一个存储池里,池中可以建立多个文件系统。池存储比卷存储更灵活,文件系统可以增长或缩小而不牵涉下面的设备。

https://raw.githubusercontent.com/zhang21/images/master/cs/operatingsystem/systems-performance/8-11.png



方法



磁盘分析



延时分析

时间可从以下四个层面来测量:

  • 应用程序
  • 系统调用接口
  • VFS
  • 文件系统上


负载特征归纳

一些特征:

  • 操作频率和操作类型
  • 文件 IO 吞吐量
  • 文件 IO 大小
  • 读写比例
  • 同步写比例
  • 文件的随机和连续访问比例


静态性能调优

静态配置情况:

  • 当前挂载并正在使用多少个文件系统?
  • 文件系统记录的大小是多少?
  • 启用了访问时间戳吗?
  • 还启用了那些文件系统选项?
  • 文件系统缓存时怎么配置的?最大缓存大小是多少?
  • 其他缓存(目录、inode、高速缓存区)是怎么配置的?
  • 有二级缓存吗?
  • 有多少个存储设备?
  • 存储设备是怎么配置的?用 RAID 了吗?
  • 用了哪种文件系统?
  • 用的是哪个文件系统的版本?
  • 有什么需要考虑的文件系统 BUG 吗?
  • 启用文件系统 IO 的资源控制了吗?


缓存调优

内核系统和文件系统会使用多种缓存,包括缓冲区高速缓存、目录缓存、inode 缓存和文件系统页缓存。



负载分离



微基准测试

典型的测试参数:

  • 操作类型:读、写和其他文件系统操作的频率。
  • IO 大小:从 1 字节到 1MB 甚至更大。
  • 文件偏移量模式:随机或连续。
  • 随机访问模式:统一的、随机的或帕累托分布。
  • 写类型:异步或同步。
  • 工作集大小:文件系统缓存是否放得下。
  • 并发:并行执行的 IO 数,或执行 IO 的线程数。
  • 内存映射
  • 缓存状态:冷还是热?
  • 文件系统可调参数:压缩、数据消重等。


观测工具

Linux 系统的文件系统分析工具。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
mount
free 
top
vmstat
sar
df
inotiry

# 内核 slab 分配器统计信息
slabtop

# 系统调用追踪
strace
perf trace    # 开销更小

# 使用 fanotify 跟踪文件系统统计
fatrace

# 跟踪打开的文件
opensnoop

# 使用中的最高 IOPS 和字节数的文件
filetop

# 页缓存统计信息
cachestat

# 显示 ext4 操作延时分布
ex4dist
# 显示慢的 ext4 操作
ex4slower

# 统计包括与文件系统相关的系统调用
syscount

# 跟踪对 stat 调用
statsnoop; syncsnoop

# 统计 mmap 文件数
mmapfiles

# 统计 read 文件数
scread

# 统计文件映射错误
fmapfault

# 跟踪短命文件
filelife

# 一般 VFS 操作统计信息
vfsstat
# 统计所有 VFS 操作
vfscount
# 显示 VFS 读写大小
vfssize
# 按文件系统类型显示 VFS 读写数
fsrwstat
# 显示慢的文件读写
fileslower
# 按照文件类型和进程显示 VFS 读写
filetype

# 统计 IO 上的栈,显示代码路径
ioprofile

# 按照同步标志显示普通文件写
writesync

# 显示回写事件和延时
writeback

# 目录缓存命中统计信息
dcstat
# 跟踪目录缓存查找
dcsnoop

# 系统范围内跟踪挂载和卸载
mountsnoop

# inode 缓存命中统计信息
icstat

# 按照进程和字节数显示缓存高速缓冲区增长
bufgrow

# 显示预取命中和效率
readahead


实验



特定性能测试

1
2
3
4
5
6
7
# dd 命令构造数据测试磁盘性能
dd if=/dev/zero of=/dev/null bs=1024k count=1k

# 等命令检测性能
iostat
iotop



缓存刷新

Linux 提供了一个刷新文件系统缓存的方法。

1
2
3
4
5
6
7
8
9
# free pagecache
echo 1 > /proc/sys/vm/drop_caches

# free reclaimable slab objects(indludes dentires and inodes)
echo 2 > /proc/sys/vm/drop_caches

# free slab objects and pagecache
echo 3 > /proc/sys/vm/drop_caches




磁盘

磁盘 IO 可能会造成严重的应用程序延时,因此是系统性能分析的一个重要目标。在高负载下,磁盘成为瓶颈,CPU 持续空闲以等待磁盘 IO 结束。发现并消除这些瓶颈能让性能和应用吞吐量提升几个数量级。



术语

  • 虚拟磁盘:存储设备的模拟。
  • 传输总线:用来通信的物理总线,包括数据传输以及其他磁盘命令。
  • 扇区:磁盘上的一个存储块,过去是 512B,如今通常为 4KB。
  • IO:磁盘读写至少由方向(读或写)、磁盘地址(位置)和大小(字节数)组成。
  • 磁盘命令:除了读写,还有其他非数据传输的命令(如,缓存回写)。
  • 吞吐量:通常是指当前数据传输速率(Byte/s)。
  • 带宽:存储传输总线或控制器能够达到的最大数据传输速率,它受限于硬件。
  • IO 延时:一个 IO 操作的执行时间。
  • 延时离群点:非同寻常的高延时磁盘 IO。


模型



简单磁盘

https://raw.githubusercontent.com/zhang21/images/master/cs/operatingsystem/systems-performance/9-1.png

虽然看起来好像是一个先来后到的队列,但磁盘管理器可以为了优化性能而采用其他算法(如,电梯寻道和读写分别准备队列等)。



缓存磁盘

https://raw.githubusercontent.com/zhang21/images/master/cs/operatingsystem/systems-performance/9-2.png



控制器

磁盘控制器(又称为主机总线适配器(HBA host bus adaptor)),把 CPU 的 IO 传输总线、存储总线以及相连的磁盘设备连接起来。

https://raw.githubusercontent.com/zhang21/images/master/cs/operatingsystem/systems-performance/9-3.png



概念



测量时间

IO 时间:

  • IO 请求时间:从发出一条 IO 到完成的完整时间。
  • IO 等待时间:IO 在队列中等待服务的时间。
  • IO 服务时间:IO 得到处理的事件。

https://raw.githubusercontent.com/zhang21/images/master/cs/operatingsystem/systems-performance/9-4.png


从内核的角度:

  • 块 IO 等待时间(操作系统等待时间):从一个新 IO 被创建并插入内核 IO 队列到它最后离开内核队列并被发送给设备的时间。
  • 块 IO 服务时间:发出请求到达设备和设备完成中断之间的时间。
  • 块 IO 请求时间:块 IO 等待时间和块 IO 服务时间之和。

从磁盘角度:

  • 磁盘等待时间(DWT):花在磁盘队列上的时间。
  • 磁盘服务时间(DST):进入磁盘的队列之后 IO 被主动处理需要的时间。
  • 磁盘请求时间

https://raw.githubusercontent.com/zhang21/images/master/cs/operatingsystem/systems-performance/9-5.png


1
2
磁盘服务时间 =  使用率 / IOPS
# 60% 使用率,300 IOPS,得出平均服务时间为 2ms


时间尺度

磁盘 IO 延时时间的一个大致范围。

事件 延时 比例
磁盘缓存命中 小于 100us 1s
读闪存 100 - 1000us 1-10s
旋转磁盘连续读 1ms 10s
旋转磁盘随机读(7200r/min) 8ms 1.3m
旋转磁盘随机读(慢,排队) 10ms 1.7m
旋转磁盘随机读(队列较长) 100ms 17m
最差情况的虚拟磁盘 IO 1000ms+ 2.8h


缓存

最好的磁盘 IO 性能就是没有 IO。许多软件栈的层尝试通过缓存读和缓冲写来避免磁盘 IO 抵达磁盘。

磁盘 IO 缓存。

缓存 示例
设备缓存 ZFS vdev
块缓存 缓冲区高速缓存
磁盘控制器缓存 RAID 卡缓存
存储阵列缓存 阵列缓存
磁盘缓存 磁盘数据控制器附带 DRAM


随机IO与连续IO

根据磁盘上 IO 的相对位置(磁盘偏移量),可用术语随机和连续描述磁盘 IO 负载。

连续负载也被称为流负载。

在磁性旋转磁盘时代,随机与连续磁盘 IO 模式的对比是研究重点。

其他类型的磁盘(包括基于闪存的 SSD),在执行随机和连续时通常没什么区别。不过还是有一些细微差别。

需要注意的是,从操作系统角度看到的磁盘偏移量并不一定是物理磁盘的偏移量。

https://raw.githubusercontent.com/zhang21/images/master/cs/operatingsystem/systems-performance/9-6.png



读写比

一个读频率高的系统,可通过增加缓存来获得性能提升。而一个写频率较高的系统则可以通过增加磁盘来提高最大吞吐量和 IOPS。

读可能是随机的,但写可能是连续的。



IO大小

IO 的平均大小,或 IO 大小的分布。更大的 IO 一般提供了更大的吞吐量,尽管单位 IO 的延时有所上升。

磁盘设备子系统可能会改变 IO 大小。



IOPS并不平等

有意义的 IOPS 需要包含其他细节:随机还是连续、IO大小、读写比例、缓存、直接读写比例,以及并行 IO数量。



非数据传输磁盘命令

除了读写 IO,磁盘还可以接受其他命令。如,命令带有缓存的磁盘(RAM)把缓存回写到磁盘。这种命令不是数据传输。或丢弃数据的命令。

这些命令会影响性能,造成磁盘运转而让其他 IO 等待。



使用率

使用率可以通过某段时间内磁盘运行工作的忙时间比例计算得出。

任意数值的磁盘使用率都可能导致糟糕的性能,毕竟磁盘 IO 是一个相对缓慢的活动。

对于虚拟硬件的虚拟磁盘,操作系统可能只知道虚拟磁盘的忙时间,却不清楚底下磁盘的性能。

一旦一块物理磁盘达到 100% 的使用率,向他发更多的 IO 会让磁盘饱和。



饱和度

饱和度衡量了因超出资源服务能力而排队的工作。



IO等待

IO 等待是针对单个 CPU 的性能指标,表示当 CPU 分发队列(在睡眠状态)里有线程被阻塞在磁盘 IO 上消耗的空闲时间。较高的每 CPU IO 等待时间表示磁盘可能是瓶颈所在,导致 CPU 等待而空闲。

一个更可靠的指标可能是应用程序被阻塞在磁盘 IO 上的时间。



同步与异步

如果应用程序 IO 和磁盘 IO 是异步的,那磁盘 IO 延时可能不直接影响应用程序的性能。



磁盘IO与应用程序IO

磁盘 IO 是多个内核组件的终点,包括文件系统和设备驱动。



架构



磁盘类型

常用的两种类型:

  • 磁性旋转磁盘
  • 基于闪存的 SSD

磁性旋转磁盘(硬盘驱动器(hdd, hard disk drive))。有一片或多个盘片构成,称为磁碟,上面涂满了氧化铁颗粒。这些颗粒中的一小块区域可以被磁化成两个方向之一,用来存储一位。当磁碟旋转时,一条带有电路的机械手臂对表面上的数据进行读写。这个电路包括磁头,一条手臂上可能不止拥有一个磁头,这样它就可以同时读写多位。数据在磁碟上按照环状磁道存储,每个磁道被划分为多个扇区。

机械设备速度相对较慢,特别是对于随机 IO。

影响磁性旋转磁盘性能的因素:

  • 寻道和旋转
  • 理论最大吞吐量
  • 短行程
  • 扇区分区
  • 扇区大小
  • 磁盘缓存
  • 电梯寻道
  • 数据一致性
  • 振动
  • 怠工磁盘
  • 磁盘数据控制器

固态磁盘(ssd, solid state disk),使用固态电子元器件。比 hdd 性能高很多。没有了移动部件,可使用更长的时间,不会因振动问题而影响性能。

这类磁盘的性能通常在不同的偏移量下保持一致(没有旋转或寻道的延时)。

有些 SSD 使用非易失性 DRAM,大部分使用闪存。



接口

接口是驱动器支持的与系统通信的协议,一般通过一个磁盘控制器实现。

  • SCSI(小型计算机系统接口)
  • SAS(串行连接 SCSI)
  • ATA(IDE)
  • SATA(串行 ATA)
  • FC(光纤通道)
  • NVMe(非易失性内存标志)是一种用于存储设备的 PCIe 总线规范。


存储类型

四种通用架构:

  • 磁盘设备
  • RAID(链家磁盘冗余阵列)
  • 存储阵列
  • 网络连接存储(NAS)

最简单的是服务器里有几块内置磁盘,每一块都由擦做系统分别控制。磁盘连接到磁盘控制器。


级别 描述 性能
0(拼接) 一次填充一个驱动器 由于多个驱动器的参与,还是可以提高随机读的性能
0(条带) 并发使用驱动器,把 IO 分割发给多个驱动器 最好的随机和连续 IO 性能
1(镜像) 多个驱动器(通常是两个)为一组,存放相同的内容,互为备份 不错的随机和连续读性能。写受限于镜像中最慢的驱动器,吞吐量的开销加倍
10 0 条带化的组合建立在 1 组合上,提供容量和冗余 性能和 1 差不多,但让更多组驱动器参与,类似 0,增加了带宽
5 数据被存储在条带上,横跨多个驱动器,还有额外的奇偶校验信息提供冗余 由于读-改-写周期和奇偶校验计算造成写性能交叉
6 每个条带有两块校验盘的 5 类似 5,不过更差

存储阵列可以把许多磁盘接入系统。它们使用高级磁盘控制器,这样 RAID 可以被配置,它们通常也提供一个更大的缓存以提高读写性能。


网络连接存储通过现有网络暴露给系统,其支持的网络协议有 NFS, SMB/CIFS 或 iSCSI。网络的性能会严重影响。



操作系统磁盘IO栈

https://raw.githubusercontent.com/zhang21/images/master/cs/operatingsystem/systems-performance/9-7.png



观测工具

一些常用工具。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 单个磁盘的各种统计信息
# -d 磁盘报告;-x 扩展输出
iostat

# 
iotop

# 磁盘历史统计信息
# -d 磁盘信息
sar

# 磁盘压力滞留信息
cat /proc/pressure/io

# 按进程列出磁盘 IO 使用情况
# -d 磁盘信息
pidstat

# 记录块 IO 跟踪点
perf

# bcc 和 bpftrace 工具
biolatency
biosnoop
biotop
biostacks
blktrace
bpftrace

# 驱动器统计数据
smartctl



调优



操作系统可调参数

ionice 设置进程的 IO 调度级别和优先级。

  • 0:无级别。
  • 1:实时,对磁盘的最高访问,如果误用会导致其他进程饿死。
  • 2:尽力,默认调度级别。包括优先级 0-7,0 为最高。
  • 3:空闲,在磁盘空闲一定时间后才允许进行 IO。(如一些长时间备份任务)

现代操作系统提供了资源控制方式,以管理磁盘或文件系统 IO 的使用。

Linux 中的控制组(cgroups)块 IO 子系统为进程和进程组提供了存储资源控制机制。


Linux 中的可调参数:

  • /sys/block/*/queue/scheduler:选择 IO 调度策略,是空操作、最后期限还是 cfq。
  • /sys/block/*/queue/nr_requests:块层可分配的读写请求的数量。
  • /sys/block/*/queue/read_ahead_kb:文件系统请求的最大预读 KB 数。


磁盘设备可调参数



磁盘控制器可调参数




网络

网络在性能方面越来越重要。常见的任务有改进网络延时和吞吐量、消除可能由丢包引起的延时异常。

网络分析是跨硬件和软件的。硬件(物理网络)包括:网卡、交换机、路由器和网关。软件(内核网络栈)包括:网络设备驱动、数据包队列、数据包调度器及网络协议的实现。较低级别的协议是典型的内核软件(IP, TCP, UDP等),高级协议是典型的库或应用程序软件(如 HTTP)。


学习目标:

  • 理解网络模型和概念
  • 理解网络延时的不同衡量标准
  • 掌握常见的网络协议的工作原理
  • 熟悉网络硬件的内部结构
  • 熟悉套接字和设备的内核路径
  • 遵循网络分析的不同方法
  • 描述整个系统和每个进程的网络 IO
  • 识别由 TCP 重传引起的问题
  • 使用跟踪工具调查网络内部情况
  • 了解网络可调参数


术语

  • 接口(interface port):指物理连接器。
  • 数据包(packet):交换网络中的信息,如 IP 数据包。
  • 帧(frame):物理网络级信息,如以太网帧。
  • 套接字(socket):源于 BSD 的一个 API,用于网络接口。
  • 带宽:网络类型的最大数据传输速率,通常以 b/s 为单位。
  • 吞吐量:当前两个网络端点之间的数据传输速率。
  • 延时:指信息在端点之间往返所需的时间。


模型



网络接口

网络接口是网络连接的操作系统端点,它是系统管理员可以配置和管理的抽象层。

https://raw.githubusercontent.com/zhang21/images/master/cs/operatingsystem/systems-performance/10-1.png



控制器

网络接口卡(NIC)给系统提供一个或多个网络端口并且设有一个网络控制器。

https://raw.githubusercontent.com/zhang21/images/master/cs/operatingsystem/systems-performance/10-2.png



协议栈

网络是由一组协议栈组成的,其中的每一层服务一个特定的目标。

https://raw.githubusercontent.com/zhang21/images/master/cs/operatingsystem/systems-performance/10-3.png



概念



网络和路由

网络是一组由网络协议联系在一起的主机。

路由管理被称为包的报文的跨网络传递。

https://raw.githubusercontent.com/zhang21/images/master/cs/operatingsystem/systems-performance/10-4.png



协议

网络协议标准是系统与设备之间通信的必要条件。通信由称为包的报文来实现,它通常包含封装的负载数据。

协议通常使用封装来传输数据。



封装

封装会将元数据添加到包头或包尾,它会增加报文总长度,但不会修改负载数据,增加了一些传输的系统开销。

https://raw.githubusercontent.com/zhang21/images/master/cs/operatingsystem/systems-performance/10-5.png



包的大小

数据包的大小和它们的有效载荷会影响性能,较大的数据包会提高吞吐量并减少数据包的开销。

对于 TCP/IP 和以太网,数据包的大小可以在 54 至 9054 字节之间。包括 54 字节(或更多)的协议头。

包的大小通常受限于网络接口的最大传输单元(MTU)的大小,在许多以太网中将它设置为 1500 Byte。

以太网支持接近 9000B 的特大帧,称为巨型帧。这能够提高网络吞吐性能,同时因为需要更少的包而降低数据传输延时。



延时

延时是一个重要的网络性能指标,并能用不同的方法测量。包括:

  • 主机名解析延时
  • ping 延时
  • 连接延时
  • 首字节延时:第一字节到达时间
  • 往返时间(RTT)
  • 连接生命周期


缓冲

尽管存在多种网络延时,利用发送端和接收端的缓冲,网络吞吐量也仍能保持高速率。较大的缓冲可通过在阻塞和等待确认前持续传输数据以缓解高往返延时带来的影响。

TCP 利用缓冲及可变的发送窗口提升吞吐量。网络套接字也有缓冲。

外部的网络组件,如交换器和路由器,也会利用缓冲提高它们的吞吐量。遗憾但是,如果包长时间在队列中,在这些组件中使用的高缓冲,会导致被称为缓冲膨胀的问题。这会引发主机中的 TCP 阻塞避免,它会限制性能。

遵循端到端的原则,缓冲的功能最好由端点(主机)来发挥作用,而不是中间的网络节点。



连接积压队列

另一种类型的缓冲用于最初的连接请求。TCP 的积压队列实现会把 SYN 请求在用户级进程接收前队列于内核中。过多的 TCP 连接请求超过进程当前的处理能力,积压队列会达到极限而丢弃客户机可以迟些时候再重新传输 SYN 包。这些包的重新传输会增加客户端连接的时间。

积压队列丢包和 SYN 重传都说明了主机过载。



接口协商

通过与对端自动协商,网络接口能够工作与不同的模式。

  • 带宽:10Mb/s, 100Mb/s, 1000Mb/s…
  • 双工(模式):半双工、全双工。

必要时,网络接口可以自动协商使用较低的速率。

全双工模式允许双向同时传输,半双工模式仅允许单向传输。



避免阻塞

网络是共享资源,当流量负荷高时,会出现阻塞。这可能导致性能问题,如丢包,造成诱发延时的 TCP 重传。



使用率

网络接口的使用率可以用当前的吞吐量除以最大带宽来计算。

一旦一个网络接口方向的使用率达到 100%,会影响性能。



本地连接

本地连接会使用一个虚拟的网络接口:环回接口。

通过 IP 与本机通信时 IP 套接字的进程间通信技巧(IPC)。另一个技巧是 UNIX 域套接字(UDS),它在文件系统中建立一个用于通信的文件。由于绕过了内核 TCP/IP 栈,省略了内核代码以及协议包封装的系统开销,UDS 的性能会更好。

对于 TCP/IP 套接字,内核能够在握手后检测到本地连接,然后对 TCP/IP 栈的数据传输使用快捷通道处理,以提高性能。



架构

网络架构:协议、硬件和软件。



协议



IP

网际协议(IP):IPv4 和 IPv6。



TCP

传输控制协议(TCP),用于建立可靠网络连接的互联网标准。

就性能而言,即使在高延时的网络中,利用缓冲和可变窗口,TCP 也能提供高吞吐量。TCP 还会利用拥塞控制以及由发送发设置的拥塞窗口,因而不仅能保持高速而且在跨越不同并变化的网络时仍能保持合适的传输速率。拥塞控制能避免会导致网络阻塞进而损害性能的过度发送。

TCP 特性:

  • 可变窗口:允许在收到确认前在网络上发送总和小于窗口大小的多个包,以在高延时的网络中提供高吞吐量。窗口的大小由接收方通知以表明当前它愿意接收的包的数量。
  • 阻塞避免:阻止发送过多的数据进而导致饱和,饱和会导致丢包而损害性能。
  • 缓启动:TCP 拥塞控制的一部分,它会以较小的拥塞窗口开始,之后按一定时间内接收到的确认 ACK 逐渐增加。如果没有收到确认,拥塞窗口的大小会减少。
  • 选择性确认(SACK):允许 TCP 确认非连续的包,以减少需要重传输的数量。
  • 快速重传输:TCP 能基于重复收到的确认重传输被丢弃的包,而不是等待计时器超时
  • 快速恢复:通过重设连接开始慢启动,以在检查到重复确认后恢复 TCP 性能。
  • TCP快速开放:允许客户端在 SYN 数据包中包含数据,这样服务器的请求处理过程就可以提前开始,而不用等待 SYN 握手。这可以使用一个加密的 cookie 来验证客户端的身份。
  • TCP时间戳:包括一个在 ACK 中返回的已发送的数据包的时间戳,以便测量往返时间。
  • TCP SYN cookies:在可能的 SYN 洪泛攻击中向客户端提供加密的 cookie,以便合法的客户端可以继续连接,而服务器不需要为这些连接尝试存储额外的数据。

关于 TCP 性能的重要内容包括:三次握手、重复确认检测、拥塞控制算法、Nagle 算法、延时确认、SACK 和 FACK。


三次握手

连接建立需要主机间的三次握手。一台主机被动地等待连接,另一方主动地发起连接。

一旦三次握手完成,TCP 会话就被置于 ESTABLISHED 状态。

https://raw.githubusercontent.com/zhang21/images/master/cs/operatingsystem/systems-performance/10-6.png


状态和定时器

TCP 会话会根据数据包和套接字时间切换 TCP 状态。

  • LISTEN
  • SYN-SENT
  • SYN-RECEIVED
  • ESTABLISHED:性能分析通常关注活动连接。
  • FIN-WAIT-1
  • FIN-WAIT-2
  • CLOSE-WAIT
  • CLOSING
  • LAST-ACK
  • TIME-WAIT:一个已经关闭的会话会进入 TIME-WAIT 状态,这样迟来的数据包就不会与同一端口的新连接错误地联系起来。这可能会导致端口耗尽的性能问题。TIME-WAIT 通常是两分钟(内核可能允许调整)。
  • CLOSED

重复确认检测

重复确认检测被快速重传和快速恢复算法用于快速检测已发送的数据包(或确认包)何时丢失,工作原理如下:

  1. 发送方发送一个序号为 10 的包。
  2. 接收方返回一个序号为 11 的确认包。
  3. 发送方发送包 11, 12 和 13。
  4. 包 11 被丢弃。
  5. 接收方发送序号为 11 的确认包以响应包 12 和 13,表明它仍在等待(包 11)。
  6. 发送方收到重复的序号为 11 的确认包。

多种阻塞避免算法也会利用重复确认检测。


重传

有两个常用的机制用于发现和重传 TCP 丢失的包:

  • 基于定时器的重传:当一段时间过后还没有收到数据包的确认时,就会发生此情况。这个时间是 TCP 重传超时时间,根据连接的往返时间动态计算。在 Linux 中,第一次重传的超时时间至少为 200ms,随后重传将慢得多,遵循指数退避算法,超时时间加倍。
  • 快速重传:当重复的 ACK 到达时,TCP 可以认为一个数据包被丢弃了,并立即重传。

拥塞控制算法

拥塞控制算法已经被开发出来维持拥堵网络的性能。一些操作系统允许选择算法作为系统调整的一部分,算法如下:

  • Reno:三次重复确认触发器,即拥塞窗口减半、慢启动阈值减半、快速重传和快速恢复。
  • Tahoe:三次重复确认触发器,即快速重传、慢启动阈值减半、拥塞窗口设置为最大报文段长度和慢启动状态。
  • CUBIC:使用一个立方函数来缩放窗口,并使用一个混合启动函数来推出慢速启动。
  • BBR:不是基于窗口,而是利用探测阶段建立的网络路径特性的明确模型。
  • DCTCP:DataCenter TCP 依赖于交换机被配置为在很低的队列占用时发出显式拥塞通知标记,以迅速提升到可用带宽。

Nagle 算法

该算法通过推迟小尺寸包的传输以减少网络中这些包的数量,从而使更多的数据能到达并被合并。仅当有数据进入数据通道并且已经发生延时时,才会推迟数据包。


延时确认

该算法最多推迟 500ms 发送确认,从而能合并多个确认。其他 TCP 控制报文也能被合并,进而减少网络中包的数量。


SACK, FACK 和 RACK

TCP 选择性确认(SACK)算法允许接收方通知发送方收到非连续的数据块。

Linux 默认支持由 SACK 扩展而来的向前确认(FACK),它跟踪更多的状态并且能更好地控制网络中未完成的数据传输,并提高整体性能。

SACK 和 FACK 都被用来改善丢包恢复。最近的 ACK(RACK)使用来自 ACK 的时间信息,实现了更好的丢失检测和恢复。


初始窗口

初始窗口(IW)是指 TCP 发送方在等待对方确认之前,在连接开始时发送的数据包数量。

Linux 的默认值(10 个数据包,IW10)在慢速链路上或许多连接启动时可能太高了;其他操作系统默认为 2个或 4个数据包。



UDP

用户数据报协议(UDP)提供如下特性:

  • 简单:简单而短小的协议头降低了计算与大小带来的系统开销。
  • 无状态:降低连接与传输控制带来的系统开销。
  • 无重传:这个 TCP 连接增加了大量的延时。

由于简单,UDP 并不可靠,数据可能丢失或被乱序发送。它还缺乏拥塞避免,因而会引起网络阻塞。

UDP 的一个主要用途是 DNS。



QUIC

QUIC 是一个性能更高、延时更低的 TCP 替代品。为 HTTP 和 TLS 进行了优化。QUIC 是建立在 UDP 之上的,并在其基础之上提供了一些功能。

  • 在同一个连接上,能够多路复用几个应用程序定义的数据流。
  • 类似于 TCP 的可靠的顺序流传输,可选择关闭单个子流的传输。
  • 当客户端改变其网络地址时,基于加密的连接 ID 认证,可恢复连接。
  • 对载荷数据进行完全加密,包括 QUIC 头文件。
  • 包含加密信息的 0-RTT 连接握手。


硬件

网络硬件包括接口、控制器、交换机、路由器和防火墙。



接口

物理网络接口,在连接的网络上发送并接收称为帧的报文。它们管理电气、光学或无线信号,包括对传输错误的处理。

接口类型基于第 2 层网络标准。



控制器

物理网络接口由控制器提供给系统,它集成于系统主板上或扩展卡。



交换机和路由器

请注意,交换机和路由器通常也是发生速率转换的地方。



防火墙

防火墙可通过物理网络设备和内核软件来体现。

防火墙可能成为性能瓶颈,特别是当配置为有状态时。防火墙在处理许多连接时可能会遇到过度的内存负载。



软件

网络通信软件包括网络栈、TCP 和设备驱动程序。



网络栈

现代内核中,网络栈时多线程的,并且传入的包能被多个 CPU 处理。

https://raw.githubusercontent.com/zhang21/images/master/cs/operatingsystem/systems-performance/10-7.png


在 Linux 系统中,网络栈是核心内核组件,而设备驱动程序是附加模块。数据包以 struct sk_buff 数据类型穿过这些内核组件。

https://raw.githubusercontent.com/zhang21/images/master/cs/operatingsystem/systems-performance/10-8.png



TCP连接队列

突发的连接由积压队列处理。有两个此类队列:

  • 一个在 TCP 握手完成前处理未完成的连接(SYN 积压队列)
  • 一个处理等待应用接收已建立的会话(侦听积压队列)

早期的内核仅使用一个队列,并且易受 SYN 洪泛攻击,这会在 TCP 等待完成握手时填满积压队列,进而阻止真实的客户连接。

在有两个队列的情况下,第一个可作为潜在的伪造连接的集结地,仅在连接建立后才迁移到第二个队列。第一个队列可被设置得很长以吸收海量 SYN 并且被优化为仅存放最少的必要元数据。

使用 SYN cookie 可绕过第一个队列,因为它们显示客户端已经被授权。

https://raw.githubusercontent.com/zhang21/images/master/cs/operatingsystem/systems-performance/10-9.png



TCP缓冲区

利用套接字的发送和接收缓冲区能够提升数据吞吐量。

https://raw.githubusercontent.com/zhang21/images/master/cs/operatingsystem/systems-performance/10-10.png



分段卸载

网络设备和网络接受的数据包大小到最大段大小(MSS),可能小到 1500 字节。为了避免发送许多小数据包的网络栈开销,Linux 使用通用分段卸载(GSO)来发送超级数据包(64KB),这些数据包在被传送到网络设备之前被分割成 MSS 大小的片段。

如果网卡和驱动程序支持 TCP 分段卸载(TSO),那么 GSO 就会将分割工作留给设备,从而提高网络栈的吞吐量。

GSO 和 GRO(通用接收卸载)由内核软件实现,TSO 由硬件网卡实现。



排队规则

这是一个可选的层,用于管理流量分类(tc)、调度、操作、过滤和网络数据包的整形。

1
2
3
4
5
6
7
8
9
man -k tc-
tc-basic (8)         - basic traffic control filter
tc-bfifo (8)         - Packet limited First In, First Out queue
tc-bpf (8)           - BPF programmable classifier and actions for ingress/egress queueing disciplines
tc-cbq (8)           - Class Based Queueing
tc-cbq-details (8)   - Class Based Queueing
tc-cgroup (8)        - control group based traffic control filter
...



网络设备驱动

网络设备驱动程序通常还带有一个附加的缓冲区——环形缓冲区——用于在内核内存与网卡间发送和接收数据包。

一个在高速网络中变得越来越普遍的性能特征是利用中断结合模式。一个中断仅在计时器激活或到达一定数量的包时才被发送,而不是每当有数据包到达就中断内核。这降低了内核与网卡通信的频率,允许缓冲更多的发送,从而达到更高的吞吐量,尽管会有一些延时。



网卡的发送和接收

对于发送的数据包,网卡收到通知,并通常使用直接内存访问(DMA)从内核内存中读取数据包,以提高帧率。网卡提供发送描述符来管理 DMA 数据包。如果网卡没有空闲的描述符,网络栈将暂停传输以使网卡能够赶上。

对于收到的数据包,网卡可以使用 DMA 将数据包放入内核环形缓冲区内存,然后使用中断通知内核。中断触发一个softirq,将数据包送到网络栈进行进一步处理。



CPU扩展

通过使用多个 CPU 来处理数据包和 TCP/IP 协议栈,可以实现高数据包率。Linux 支持各种多 CPU 数据包处理的方法。

  • RSS:接收侧缩放。
  • RPS:接收侧包控制。
  • RFS:接收端流控制。
  • 加速的接收端流控制。
  • XPS:发送端包控制。


内核旁路



其他优化

还有其他一些算法用于提高性能。

  • 控速
  • TCP 小队列(TSQ)
  • 字节队列限制(BQL)
  • 最早触发事件(EDT)

https://raw.githubusercontent.com/zhang21/images/master/cs/operatingsystem/systems-performance/10-11.png



方法



延时分析

网络延时:

  • 主机名解析延时
  • Ping 延时
  • TCP 连接初始化延时
  • TCP 首字节延时
  • TCP 重传
  • TCP TIME_WAIT 延时
  • 连接/会话 寿命
  • 系统调用发送/接收延时
  • 系统调用连接延时
  • 网络往返时间
  • 中断延时
  • 栈间的延时


性能监测

关键的网络监测指标:

  • 吞吐量
  • 连接数
  • 错误
  • TCP 重传数
  • TCP 乱序数据包


TCP分析

  • TCP(套接字)发送/接收缓冲的使用。
  • TCP 积压队列的使用。
  • 由于积压队列已满导致的内核丢包。
  • 拥塞窗口大小,包括零长度通知。
  • TIME_WAIT 间隔中接收到的 SYN。


静态性能调优

静态配置:

  • 有多少网络接口可供使用?当前使用中的有哪些?
  • 网络接口的最大速度是多少?
  • 当前协商的网络接口的速度是多少?
  • 网络接口协商为半双工还是全双工?
  • 网络接口配置的 MTU 是多少?
  • 网络接口是否使用了中继模式?
  • 有哪些适用于设备驱动的可调参数?
  • 有哪些可调参数已不再是默认值?
  • 路由是如何配置的?默认路由是什么?
  • 数据路径中网络组件的最大吞吐量是多少?
  • 数据路径的最大 MTU 是多少,是否会发生分片?
  • 数据路径中是否有无线连接?它们是否受到干扰?
  • 是否启用了数据转发?该系统是否作为路由器使用?
  • DNS 是如何设置的?它距离服务器有多远?
  • 该版本的网络接口固件是否有已知的性能问题?
  • 该网络设备驱动是否有已知的性能问题?
  • 是否存在防火墙?
  • 是否存在软件施加的网络吞吐量限制(资源控制)?


观测工具

网络观测工具。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# 套接字统计信息
# ss 从 netlink 接口读取这些扩展的细节,该接口通过 AF_NETLINK 系列的套接字操作,从内核获取信息。
# -t(tcp), -i(内部信息), -e(扩展信息), -p(进程信息), -m(内存信息)
ss

# 网络接口和路由统计信息
ip

# 网络接口统计信息
ifconfig

# 网络栈统计信息
nstat

# 多种网络栈和接口统计信息
# -a(所有套接字), -s(网络栈), -i(网络接口), -r(路由表)
netstat

# 历史统计信息
# -n DEV/EDEV/IP/EIP/TCP/ETCP/SOCK
sar

# 网络接口吞吐量和使用率
nicstat

# 网络接口驱动程序统计信息
# -S eth0 打印驱动程序信息
ethtool

# 用连接细节跟踪 tcp 会话的寿命
tcplife

# 按主机和进程显示 tcp 吞吐量
tcptop

# 用地址和 tcp 状态跟踪 tcp 重传的情况
tcpretrans

# tcp/ip 栈踪迹:连接、数据包、掉线、延时
bpftrace

# 抓包/分析 wireshark
tcpdump


网络性能测试

网络性能测试工具。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# ping 发送 icmp echo 请求数据包测试网络连通性
ping www.google.com

# traceroute 发出一系列数据包探测到一台主机的当前的路由。
# 显示 * 表示没有返回 icmp 超时消息
# 三个 * 可能由于某一条根本没返回 icmp,或 icmp 被防火墙拦截。
# 可使用 -T 切换到 tcp 而不是 icmp。
traceroute www.google.com

# pathchar 类似 traceroute,并包含了每一跳间的带宽。
pathchar 192.168.1.11

# mtr 类似 traceroute 并包含 ping 统计的工具
mtr

# iperf 是一款测试最大 TCP 和 UDP 吞吐量的开源工具。必须同时在 c/s 上运行。
iperf -s -l 128k
# -c(主机), -l(套接字缓冲), -P(并行), -i(时间间隔), -t(测试时间)
iperf -c 10.1.1.11 -l 128k -P 2 -i l -t 60

# netperf 是一个新进的微基准测试工具,可测试请求/响应 的性能。
netserver -D -p 7001
netperf -v 100 -H 10.1.1.11 -t TCP_RP -p 7001

# tc 流量控制工具,允许选择各种排队规则来改善性能。
tc



调优

在试图调优之前,最好能先理解网络的使用情况。



系统级可调参数

在 Linux 中,系统级可调参数可用 sysctl 命令查看和设置,并写入 /etc/sysctl.conf 文件中。也可以在 /proc/sys/net 下读取到信息。

1
2
sysctl -a | grep tcp



套接字选型

应用程序可通过 setsockopt() 系统调用对套接字进行单独调优。




云计算



硬件虚拟化

硬件虚拟化可以创建一台能运行包括自己内核的完整操作系统的虚拟机(VM)。

  • 直接在处理器上执行。一个例子是 Xen。
  • 在宿主机操作系统上执行。一个例子是 KVM 管理程序。

最初的硬件管理程序是由 VMware 在 1998 年开创的,使用二进制翻译来执行完全的硬件虚拟化。后来的一些改进:

  • 处理器虚拟化支持:2006 年引入了 AMD-V 和因特尔 VT-x 扩展,可为处理器的虚拟操作提供更快的硬件支持。
  • 半虚拟化:提供一个虚拟系统,它提供一个接口,供客户机操作系统有效地使用宿主机资源(通过 hypercall),而不需要对所有组件进行完全虚拟化。
  • 设备硬件支持:设备硬件已经在增加对虚拟机的支持。


实现

  • VMware ESX
  • Xen
  • Hyper-V
  • KVM
  • Nitro


系统开销

虚拟化的资源访问可能需要由管理程序进行代理和翻译,这会增加开销。可以使用基于硬件的技术来避免这些开销。

CPU:

  • 二进制翻译
  • 半虚拟化
  • 硬件辅助

内存映射和内存消耗。


IO 是硬件虚拟化开销的最大来源。每个设备的 IO 都必须由管理程序进行翻译。

提高 IO 性能的一种方法是使用半虚拟化驱动程序,它通过合并 IO 产生更少的设备中断,来减少管理程序的开销。

另一种技术叫 PCI 直通,它将 PCI 设备直接分配给客户机。


多租户竞争。



资源控制

CPU 资源通常以虚拟 CPU 的方式分配给客户机。然后由管理程序来调度它们。

CPU 缓存。

内存限制。

文件系统容量。

设备 IO。



操作系统虚拟化

操作系统虚拟化间操作系统分为很多实例,这些实例在 Linux 中被称为容器。

这种实现方法源自 UNIX 的 chroot 命令,它将进程隔离在 UNIX 的全局文件系统的一个子目录内。后续的 Linux 添加了 namespace 和 cgroup 来创建和管理容器。

相比硬件虚拟化技术,容器技术仅有一个内核在运行。



实现方式

在 Linux 内核中没有容器的概念。但用户空间的软件(如 Docker)使用 namespace 和 cgroup 来创建所谓的容器。每个容器内都有一个 PID 为 1 的进程。

许多容器部署使用 Kubernetes。

https://raw.githubusercontent.com/zhang21/images/master/cs/operatingsystem/systems-performance/11-10.png

https://raw.githubusercontent.com/zhang21/images/master/cs/operatingsystem/systems-performance/11-11.png



命名空间

命名空间(namespace)对系统的试图进行过滤,使容器只能看到和管理自己的进程、挂载点以及其他资源。这是提供容器于系统中其他容器隔离的主要机制。

命名空间 说明
cgroup 控制组的可见性
ipc 进程间通信的可见性
mnt 文件系统挂载点
net 网络栈隔离、过滤接口、套接字、路由等
pid 进程可见性,过滤 /proc
time 不同容器单独的系统时钟
user 用户 ID
uts 宿主机信息和 uname 系统调用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# 查看系统当前的命名空间
lsns
        NS TYPE  NPROCS   PID USER   COMMAND
4026531836 pid      185     1 root   /usr/lib/systemd/systemd --switched-root --system --deserialize 22
4026531837 user     185     1 root   /usr/lib/systemd/systemd --switched-root --system --deserialize 22
4026531838 uts      185     1 root   /usr/lib/systemd/systemd --switched-root --system --deserialize 22
4026531839 ipc      185     1 root   /usr/lib/systemd/systemd --switched-root --system --deserialize 22
4026531840 mnt      180     1 root   /usr/lib/systemd/systemd --switched-root --system --deserialize 22
4026531856 mnt        1    18 root   kdevtmpfs
4026531956 net      185     1 root   /usr/lib/systemd/systemd --switched-root --system --deserialize 22
4026532502 mnt        1   739 root   /usr/libexec/bluetooth/bluetoothd
4026532567 mnt        1   773 chrony /usr/sbin/chronyd
4026532568 mnt        2   797 root   /usr/sbin/NetworkManager --no-daemon


控制组

控制组(cgroups)限制资源的使用。

控制组 说明
blkio 限制块 IO 和字节数和 IOPS
cpu 限制基于共享的 CPU 使用
cpuacct 统计进程组的 CPU 使用量
cpuset 为容器分配 CPU 和内存节点
devices 控制设备管理
hugetlb 限制巨型页的使用
memory 限制进程内存、内核内存和交换空间的使用
net_cls 为防火墙使用的数据包设置分类
net_prio 设置网络接口的优先级
perf_event 允许 perf 监视控制组中的进程
pids 限制可以创建的进程数量
rdma 限制 RDMA 和 InfiniBand 的资源量


系统开销

容器执行的开销应该是轻量级的。最大的性能问题是由多租户竞争引起的,因为容器促进了内核和物理资源的更多共享。

当容器线程以用户态运行时,没有直接的 CPU 开销。在 Linux 中,在命名空间和控制组中运行的进程也没有额外的 CPU 开销,无论容器是否在使用,所有进程已经在默认的命名空间和控制组中运行了。

内存映射、加载和存储执行时应该没有额外开销。

应用程序可以使用为容器分配的全部内存。

常见的容器配置(使用 overlayfs)允许共享访问同一文件的容器之间的页缓存。这减少了内存消耗。

IO 开销取决于容器的配置,因为可能包括额外的用于隔离的层。文件系统 IO(如 overlayfs),网络 IO(如桥接网络)。

其他正在运行的租户可能会导致资源竞争和中断,从而损害性能。包括:

  • CPU 缓存的命中率可能较低,因为其他租户正在消耗和驱逐缓存条目。
  • 由于其他租户的使用,TLB 缓存的命中率也可能较低,而且上下文切换时也会被刷新。
  • CPU 的执行可能会因为其他租户设备执行中断服务而被短期打断。
  • 内核执行可能会遇到对缓冲区、缓存、队列和锁的额外竞争。
  • 由于使用 iptables 来实现容器网络,所以网络 IO 会遇到 CPU 开销。


资源控制

资源控制限制了对资源的访问,以便更公平地共享资源。在 Linux 中主要通过控制组提供。

独立的资源控制的方式可分为 优先级权限。优先级引导资源消耗,根据权重值来平衡邻居之间的使用。极限是资源消耗的最高值。

资源 优先级 极限
CPU CFS 共享 cpusets (整个 CPU)
CFS 带宽(部分 CPU)
内存容量 内存软件限制 内存限制
交换容量 - 交换限制
文件系统容量 - 文件系统限制
文件系统缓存 - 内核内存限制
磁盘 IO blkio 权重 blkio IOPS 限制
blkio 吞吐量限制
网络 IO net_prio 优先级
qdiscs
自定义 BPF
-

可使用 cpusets cgroup 跨容器分配 CPU,并从 CFS 调度器中获得共享和带宽。

cpusets cgroup 允许将整个 CPU 分配给特定容器,已分配的 CPU(即使空闲) 不能被其他容器使用。

由 CFS 调度器提供的 CPU 共享是一种不同的 CPU 分配方法,它允许容器共享它们的空闲 CPU 容量。CPU 共享通过被称为份额的分配单元分配给容器,份额用于计算繁忙的容器在给定时间内将得到的 CPU 数量。


memory cgroup 提供了 4种机制来管理内存的使用。

名称 描述
memory.limit_in_bytes 大小限制
memory.soft_limit_in_bytes 软大小限制(尽力而为)
memory.kmem.limit_in_bytes 内核内存的大小限制
memory.kmem.tcp.limit_in_bytes TCP 缓冲区的大小限制
memory.pressure_level 低内存通知器
memsw.limit_in_bytes 内存叫上交换的大小

注意,一个容器未使用的内存可被内核页缓存中的其他容器使用。


文件系统的容量通常可由文件系统来限制。


blkio cgroup 提供了管理磁盘 IO 的机制。

名称 描述
blkio.weight 权重
blkio.weight_device 特定设备的权重
blkio.throttle.read_bps_device 读操作限速
blkio.throttle.write_bps_device 写操作限速
blkio.throttle.read_iops_device 读 IOPS 限制
blkio.throttle.write_iops_device 写 IOPS 限制

net_prio cgroup 允许为网络 IO 设置优先级。这与 SO_PRIORITY 套接字选项相同,用于控制网络栈中数据包处理的优先级。

net_cls cgroup 可以给数据包打上类 ID 的标签,以便 qdiscs 管理(也适用于 k8s pod,每个 pod 可使用一个 net_cls)。

排队规则(qdiscs)可对类 ID 进行操作,也可分配给容器虚拟网络接口,对网络流量进行优先级控制和节流。

BPF 程序也可被附加到 cgroup 上,用于自定义可编程资源控制和防火墙。



轻量虚拟化



其他类型

其他云计算原语和技术:

  • 功能/函数即服务(FaaS):开发者将应用功能提交到云端按需运行。此模式构建的应用程序是一种无服务器架构(Serverless)。
  • 软件即服务(SaaS):提供了高级软件,用户不需要自己配置服务器或应用程序。
  • Unikernels:此技术将一个应用程序于最小的内核部分一起编译成一个单一的二进制文件,可有硬件虚拟机管理程序直接执行,不需要操作系统。


比较

属性 硬件虚拟化 操作系统虚拟化 轻量虚拟化
例子 KVM 容器 Firecracker
CPU 性能 高(需要 CPU 支持) 同硬件
CPU 分配 固定在 vCPU 极限 灵活(资源共享 + 带宽限制) 同硬件
IO 吞吐量 高(需要 SR-IOV) 高(无内在的开销) 同硬件)
IO 延时 低(需要 SR-IOV 且没有 QEMU) 低(无内在的开销) 低(需要 SR-IOV)
内存访问开销 一些(EPT/NPT 或影子页表) 同硬件
内存损失 一些(额外的内核、页表) 同硬件
内存分配 固定(有可能需要双重缓存) 灵活(未使用的客户机内存用于文件系统高速缓存) 同硬件
资源控制 最多(内核加上虚拟机管理程序控制) 许多(依内核而不同) 同硬件
宿主机上的可观测性 中(资源使用,管理程序统计信息,从操作系统审查管理程序,但无法查看客户机内部) 高(一切都可见) 同硬件
客户机上的可观测性 高(完整的内核及虚拟设备审查) 中(仅用户态,内核计数器、完整的内核可见性) 同硬件
观测喜好 终端用户 宿主机运营商 同硬件
虚拟机管理程序复杂性 高(需要复杂的管理程序) 中(操作系统) 高(需要轻量化虚拟机管理程序)
不同操作系统的客户机 通常不支持



基准测试

在可控状态下做性能的基准测试,可对不同的选择做比较,发现回归问题,并在生产环境将要达到性能极限的时候了解性能极限。

本章学习目标:

  • 理解微基准测试和宏基准测试。
  • 了解多种基准测试的失败,从而避免它们。
  • 遵顼一个有效基准测试的方法论。
  • 使用基准测试清单区检查结果。
  • 改进执行和解读基准测试的准确性。


基准测试的类型

https://raw.githubusercontent.com/zhang21/images/master/cs/operatingsystem/systems-performance/12-1.png



微基准测试

微基准测试利用人造的工作负载对某类特定的操作做测试。

文件系统基准测试实例:测试 512B, 1MB, 512MB, 1GB 的读写效果。



模拟

模拟客户应用程序的工作负载(有时称为宏基准测试)。

模拟所生成的结果于客户在现实世界所执行的工作负载是相似的。

模拟的问题是它忽略了变化。



回放

回放目标的跟踪日志,用真实捕捉到的客户端的操作来测试性能。



行业标准

  • TPS
  • TPC
  • SPEC


基准测试检查清单

  • 为什么不是双倍?
  • 是否突破了限制?
  • 它有错误吗?
  • 它重现了吗?
  • 它重要吗?
  • 它真的发生了吗?


基准测试问题




perf

perf 是 Linux 的官方剖析器,在内核源码 tools/perf 下。它是一个集剖析、跟踪和脚本功能于一身的工具。

它可用来回答以下问题:

  • 哪些代码路径在消耗 CPU 资源?
  • CPU 是否被滞留在内存的负载/存储上?
  • 线程因为什么原因离开 CPU?
  • 磁盘 IO 的模式是什么?


子命令

部分 perf 子命令。

1
2
3
4
# perf.data
perf record -F 99 -a -- sleep 30

perf report --stdio
命令 描述
annotate 读取 perf.data 数据并显示注释过的代码
archive 创建一个含有调试和符号信息的便携式 perf.data 文件
bench 系统微基准测试
buildid-cache 管理 build-id 缓存
c2c 缓存行分析工具
diff 读取两个 perf.data 文件并展示剖析的区别
evlist 列出 perf.data 文件中的事件名称
ftrace 接入 Ftrace 跟踪器的接口
inject 注入附加信息以增强事件流的过滤器
kmem 跟踪/度量内核内存属性
kvm 跟踪/度量 KVM 虚拟机实例
list 列出事件类型
lock 分析锁事件
mem 剖析内存访问
probe 定义新的动态 tracepoint
record 运行一个命令并把它的剖析记录到 perf.data 中
report 读取 perf.data 并显示剖析
sched 跟踪/度量调度器属性
script 读取 perf.data 并显示跟踪输出
stat 运行一个命令并收集性能计数器统计信息
timechart 在施加负载时可视化整个系统的行为
top 实时更新的系统剖析工具
trace 实时跟踪系统调用


单行命令



列出事件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 列出所有当前已知的事件
perf list

# 列出 sched tracepoint
perf list 'sched:*'

# 列出所有带有 block 的事件
perf list block

# 列出所有当前可用的动态探查器
perf probe -l


计数事件

1
2
3
4
5
6
7
8
9
# 为特定事件显示 PMC 统计信息
perf stat 命令

# 为特定进程显示 PMC 统计信息,直到按下 ctrl + c
perf stat -p PID

# 为整个系统显示 PMC 统计信息,5s
perf stat -a sleep 5



静态跟踪



动态跟踪



报告




Ftrace

Ftrace 是 Linux 官方的跟踪器,是一个由不同的跟踪工具组成的多功能工具。

它可以用来回答一些问题:

  • 某些内核函数被调用的频率如何?
  • 什么代码路径会导致这个函数被调用?
  • 这个内核函数调用了哪些子函数?
  • 禁用抢占的代码路径造成的最高延时是多少?



BPF

eBPF 用 BCC 和 bpftrace 跟踪前端。这些前端提供了性能分析的工具集合。

BPF 与其他跟踪器的不同之处在于它是可编程的。它可以执行用户自己编写的基于事件的程序。

eBPF 工具可用来回答以下问题:

  • 磁盘 IO 的延时输出为直方图是什么样子的?
  • CPU 调度器的延时是否会高到引起问题?
  • 应用程序是否会受到文件系统延时的影响?
  • 哪些 TCP 会话正在进行中,持续时间是多少?
  • 哪些代码路径被阻塞,阻塞的时间有多长?



Linux检查清单