H13

I/O 与中断

点击正文段落选定收藏或划线位置;未选定时使用屏幕内第一段。

课内目录

1 为什么程序会「卡住等设备」

订单 API 收到请求后,要检查库存、写入订单、提交事务,再把成功响应发给客户。计算价格可能很快,持久化日志却要等待存储设备完成,响应也要经过网卡。程序执行到这些位置时,后续动作需要的条件还没有成立。即使 CPU 可以继续计算,也不能凭空宣布日志已经可靠落盘。这种等待来自依赖关系,不能只靠更快的处理器消除。

I/O 是输入与输出,指计算机与外部设备交换数据。这里的“外部”不一定在机箱外:本地 SSD、网卡都是设备。应用通常通过系统调用请求内核服务,再由文件系统、网络协议栈与驱动协同完成。驱动负责把通用请求转成设备能够理解的操作,并处理设备的状态变化。实际路径可能绕过部分层次,但分层图能帮助我们定位等待发生在哪里。

订单请求经过应用、内核与设备
订单请求经过应用、内核与设备

把请求总时间拆开看,比盯住 CPU 利用率更有帮助。请求可能在应用队列里排队,在连接池里等连接,在内核里等 I/O,在设备队列里等服务,还可能在条件满足后等待再次获得 CPU。可以写成:响应时间约等于各阶段排队时间、执行时间与必要等待时间之和。这里不能把并行发生的阶段重复相加;这张账单用于解释关键路径,实际分析仍需沿调用链核对。

线程等待设备时,操作系统通常把它置为等待状态,让 CPU 执行其他可运行线程。于是会出现“接口很慢,CPU 却不高”。CPU 利用率描述处理器正在执行多少工作,并不描述业务请求距离完成还有多久。反过来,CPU 很忙也未必都在运行订单逻辑:网络中断、协议处理、内存复制都可能消耗处理器时间。业务耗时与资源利用率需要放在相同时间窗口观察。

从 H12 的存储视角继续向上看,设备完成请求只是链条中的事件。完成状态还要被内核发现、处理,并通知等待者。即使介质服务时间稳定,完成通知不及时也能让请求变慢。本课要回答的是:设备怎样引起 CPU 的注意,CPU 为此付出多少成本,以及这些成本如何进入订单 API 的 p99。p99 表示约 99% 请求耗时不超过该值,必须同时注明统计窗口和请求范围。

2 轮询 vs 中断:谁在付 CPU 成本

设想程序不断问设备:“完成了吗?”这就是轮询。CPU 反复读取状态,发现完成后立即推进处理。若设备尚未完成,每次检查都没有带来有效进展,却仍占用处理器。忙轮询可以缩短发现事件的时间,代价是持续消耗 CPU;定时轮询让出处理器后再检查,节省部分成本,却引入检查间隔。事件恰好发生在检查之后,就要等到下次检查才被发现。

中断换了通知方向:设备或中断控制机制在事件出现时通知处理器,处理器按规则进入相应处理路径。设备并不任意执行应用代码,也不是直接调用等待线程。内核先识别来源、处理状态,再决定后续工作。中断避免了无事件时反复检查,但通知、切换执行上下文以及处理设备事件本身都有成本。事件密集时,每件小事都发出通知,累计开销可能很大。

比较时应区分“每秒检查多少次”和“每秒发生多少次事件”。假设轮询每秒检查 100000 次,每次花费 0.2 微秒,仅检查就消耗约 20 毫秒 CPU 时间,即 1 个核心每秒时间的 2%。假设每秒产生 5000 次中断,每次通知与基础处理平均花费 2 微秒,对应约 10 毫秒 CPU 时间。这些是帮助理解量级的假设值,真实成本随硬件、内核、缓存状态和处理内容变化,不能当作选型基准。

当事件频率升到每秒 200000 次,仍按每次 2 微秒估计,成本就达到每秒 400 毫秒,还没有算完整业务处理。此时批量处理可能更划算。反过来,事件很少时,持续忙轮询容易浪费大部分检查。也不能简单推断“轮询必然更快”:轮询线程若没有及时运行、设备状态访问很慢或批量过大,完成发现照样会延迟。决定效果的是完整路径与运行条件。

现实系统经常混合使用两者。例如 Linux 的许多网络驱动利用 NAPI:先由中断提示有工作,再在有预算的轮询处理中批量收取数据,达到合适条件后恢复通知。这里的轮询是内核处理机制,并不等于应用线程永远空转。理解混合方式后,看到中断次数下降就不会立刻认定工作量下降;同样的数据量可能被更少的通知触发,并在后续批次里完成。

3 中断处理路径:硬中断到软中断的直觉

以网卡收到数据为例。设备准备好数据和完成状态后,经中断控制机制向某个 CPU 发出 IRQ,也就是中断请求。处理器在允许响应时进入内核注册的处理程序。我们把直接响应硬件中断的部分称为硬中断处理。它需要尽快完成必要的确认、状态检查与后续工作安排,避免长时间占住这条处理路径。具体驱动如何确认、屏蔽或恢复通知,取决于设备和实现。

需要更多时间的工作可以推迟执行。Linux 网络接收常见的后续机制是软中断,例如 NET_RX 相关处理。软中断并非设备又发出 1 次物理中断,而是内核标记有待处理工作,在合适的执行点运行。硬中断与软中断是理解 Linux 的重要术语,但不要把它们理解成所有操作系统、所有设备都完全相同的固定流水线。存储完成和其他驱动也可能采用不同的延后机制。

从 IRQ 到后续处理与线程唤醒
从 IRQ 到后续处理与线程唤醒

软中断通常会在相关 CPU 上执行;当工作过多或达到处理约束时,部分待办可能交给该 CPU 的 ksoftirqd 内核线程继续处理。因此,观察到 ksoftirqd 使用 CPU,常提示延后处理有压力,但不能单凭线程名判断发生了中断风暴。软中断处理上下文也不能随意睡眠,不能把它当成普通应用线程,执行任何可能阻塞的操作。需要睡眠的工作通常要采用适当的线程化机制。

通知链末端,内核可能更新套接字可读状态或 I/O 完成状态,并唤醒等待线程。“唤醒”表示线程具备继续运行的条件,进入可运行状态;它不保证立即在 CPU 上执行。如果刚学过 S3,可以把这里接到运行队列等待的结论:从事件发生到应用继续运行,既有设备与内核处理耗时,也有唤醒后的调度等待,无需重新讨论完整调度算法。

于是,网卡中断过密可能通过多个环节拉长尾延迟。硬中断与软中断消耗核心时间,待处理数据形成积压,可运行的业务线程也更难及时获得 CPU。整机平均 CPU 只有 35%,仍可能有某个承担大量 IRQ 的核心接近饱和。应同时看每核利用率、IRQ 分布、软中断活动与应用运行等待,才能区分“总体容量不足”和“局部处理路径拥塞”。

4 阻塞 I/O 与异步:应用线程在等什么

阻塞 I/O 的典型表现是:线程调用读取操作,在当前条件不足以完成时进入等待,直到有结果、发生错误、超时或被允许的信号打断。线程没有返回,不代表它持续占着 CPU。以读取套接字为例,线程可能睡眠在等待数据的条件上;内核收到足够的数据后更新状态并唤醒它。应用看到的调用耗时包含等待,因此不能把整个调用时长当作 CPU 执行时间。

也不是每次读写都会碰到设备。读取可能命中页缓存,写入也可能先进入缓存后返回。写调用返回成功、数据进入内核缓存、设备确认写入、满足持久化要求,是不同的完成边界。数据库提交通常有自己的日志持久化与复制语义,必须结合配置确认。订单接口宣称“支付后订单可靠保存”,就不能只凭某次 write 返回很快来证明持久性已经达成。

非阻塞接口在暂时无法继续时通常立即返回相应状态,例如 EAGAIN,让调用方决定接下来做什么。应用若收到 EAGAIN 后立刻无限重试,就自己制造了忙轮询。常见做法是通过就绪通知等待可读或可写,再尝试操作。就绪表示操作有机会推进,并不保证后续任意大小的读写都一次完成,也不保证业务消息已经完整到达,仍须处理部分结果与状态变化。

异步 I/O 强调提交操作后,应用继续做其他工作,随后接收完成结果。具体接口、文件类型与内核支持决定实际执行方式,不能看到 async 名字就断言底层完全没有线程等待。事件驱动框架也可能把某些文件操作交给工作线程池。设计时要问清楚:谁保存未完成请求,谁处理完成通知,结果如何关联原请求,超时后已提交的操作是否仍可能完成。

阻塞等待、就绪通知与异步完成的边界
阻塞等待、就绪通知与异步完成的边界

异步可以提高等待期间的并发利用率,却不会自动提升磁盘每秒可处理的工作量。若提交速度持续超过完成速度,未完成请求会占用内存、连接和队列位置,尾延迟仍会增长。因此需要限制在途请求数、设置超时并实施背压。取消等待与撤销设备操作也不是同义词;订单重试必须结合幂等机制,避免前次提交已成功、响应却晚到时创建重复订单。

5 DMA 入门:数据为何不必每字节都经 CPU

如果设备每传送 1 字节,都要求 CPU 执行指令搬运,处理器会把大量时间花在运输数据上。DMA,即直接内存访问,让具备相应能力的设备在系统安排下直接与内存交换数据。CPU 仍负责建立请求、准备可访问的缓冲区并处理完成状态,但不必为每字节都执行搬运指令。这里的“直接”描述数据传输路径,并不表示设备绕过操作系统权限任意访问内存。

以读取存储为例,内核和驱动准备目标内存区域与设备请求,设备将读出的数据传入约定的内存区域,随后报告完成。CPU 读取完成状态后,内核才能按相应协议使用结果、通知等待者。DMA 与中断解决的是不同问题:DMA 负责数据怎样移动,中断负责事件怎样被发现。DMA 操作的完成也可以通过轮询发现,因此不能把 DMA 当作中断的替代品。

CPU 安排传输,设备经 DMA 访问约定内存
CPU 安排传输,设备经 DMA 访问约定内存

驱动需要遵循平台的地址映射、内存可见性与同步规则。设备使用的地址不应直接等同于应用拿到的虚拟地址;某些平台还通过 IOMMU 管理设备可访问的内存范围。CPU 缓存与设备访问之间也可能需要适当同步。这些工作由内核和驱动按平台规范处理,本课只建立边界认识:设备把数据写进内存,并不意味着任何线程可以跳过完成检查立刻读取。

网卡也能利用 DMA 把接收数据放进缓冲区,驱动会管理缓冲区和描述传输任务的元数据。可以先把描述符理解为“告诉设备数据在哪里、要做什么”的记录,不必深入描述符环的细节。无论元数据怎样组织,可用缓冲区、处理预算与消费速度都有限。应用处理不及时,仍可能出现队列积压或丢包;DMA 不能替代容量规划与流量控制。

减少 CPU 搬运不等于整条路径没有复制。数据在内核缓冲区与用户缓冲区之间可能仍要复制,协议处理、校验与内存分配也仍有成本。有些接口能减少复制,但需要另外分析语义与生命周期。判断 DMA 的价值时,应看 CPU 时间、内存带宽和端到端吞吐的变化;如果主要瓶颈是存储服务时间,减少某段复制未必显著改善订单提交的 p99。

6 观测:iostat、softirq、/proc/interrupts 看什么

观测先建立时间对齐。记录订单请求量、p50、p95、p99、错误率和数据库提交耗时,同时采集设备、CPU 与中断指标。平均值会掩盖短毛刺,过短窗口的 p99 又可能样本不足,应明确采样粒度和请求数量。命令输出只是不同角度的证据,既不能用主机磁盘指标直接代替数据库调用链,也不能把应用机器上的空闲磁盘当作远端数据库存储健康的证明。

Linux 上可用 iostat -xz 1 连续观察扩展设备指标。部分版本默认首份报告是启动以来的平均值,可用其支持的选项跳过,或从后续间隔报告分析。r/s、w/s 反映完成操作的速率;吞吐字段反映数据量,注意工具显示的单位。await 通常反映被统计请求的平均排队与服务耗时,r_await、w_await 可帮助区分读写。它们不是订单接口的 p99,也不覆盖所有应用层等待。

aqu-sz 或相应版本的队列长度字段描述平均未完成 I/O 数量,包含排队和服务中的请求,不能一律解释为“尚未进入设备的队列”。%util 表示采样期内设备有 I/O 活动的时间比例。对于能够并行处理请求的现代设备,接近 100% 不等于已达到全部吞吐上限;很低也不能排除短时尖峰。应把请求大小、吞吐、延迟和队列变化放在一起,并确认观察的是哪层逻辑设备。

设备、CPU、中断与业务指标如何对照
设备、CPU、中断与业务指标如何对照

读取 /proc/interrupts 可以看到按 CPU 展开的累计中断计数及来源标签。比较相邻时刻的差值,才得到观察窗口内的增量;累计值很大可能只是运行时间很长。关注网卡相关 IRQ 是否集中在少数核心,事件增加是否对应流量增加。标签与硬件、驱动有关,向量也未必和整块设备一一对应。不要把某行计数直接当成网卡包数,因为合并通知和批处理会改变两者关系。

读取 /proc/softirqs 可以看到各 CPU 上各类软中断的累计执行计数,常见类别有 NET_RX、NET_TX、BLOCK 等。相邻快照差值可展示活动分布,但次数不等于处理时长,也不等于处理的数据包或 I/O 数量。结合 mpstat -P ALL 1 等工具的每核 %irq、%soft 和业务 CPU 使用,才能判断成本。某类计数少,并不足以说明该子系统没有工作,实际完成路径可能采用其他机制。

还要谨慎解释 %iowait。它是 CPU 时间记账中的一种状态,通常与 CPU 空闲且存在待完成 I/O 有关,不是“业务线程等待 I/O 的总时长占比”。多核、任务迁移以及远端服务等待都会使直觉失效。高 iowait 可以提供线索,低 iowait 也不能排除数据库或网络等待。必要时补充 pidstat -d、数据库等待事件、网络丢包与重传指标,并沿调用链确认等待对象。

7 常见坑:中断风暴、错误关 offload、把存储延迟当 CPU 忙

中断风暴描述通知过于密集,以至于大量 CPU 时间用于响应与处理,正常工作受到挤压的现象。来源可能是大量小包、异常流量或设备与驱动问题,不能只凭中断增量高就下结论。若流量、吞吐与业务量同步增长,可能是合理负载;若中断陡增而有效吞吐停滞、少数核心 %soft 很高、丢包和尾延迟上升,就更值得调查事件来源与处理分布。

中断合并允许设备积攒若干事件,或等待一定时间后集中通知,批处理也能摊薄固定开销。收益是减少每单位数据的通知成本,代价是某些事件要多等一段时间。低流量时,等待合并定时器的成本可能更明显;高流量时,合适批量又可能缓解 CPU 拥塞并改善尾延迟。因此“批量越小延迟越低”只覆盖了没有拥塞的部分条件,调优应比较完整负载曲线。

offload 泛指把某些工作交给硬件等机制承担,例如部分校验与分段工作。本课不展开高级网卡卸载机制,只强调操作原则:不要因“更直接、延迟应当更低”的猜测而成批关闭功能。关闭后,CPU 可能承担更多工作,包处理次数与工具看到的包形态也可能变化。应先记录现状,明确要验证的假设,在可控环境逐项改变,并用相同吞吐、包大小和业务请求分布比较结果。

另一个常见错误是把高存储等待归为“CPU 不够”。订单提交耗时增加时,若核心仍有余量、数据库日志同步等待上升、存储写延迟同步升高,优先调查持久化路径更合理。增加应用核心未必有用,增加并发反而可能把更多请求推入设备队列。也不要立即认定磁盘损坏:后台备份、共享存储争用、请求大小改变、同步写增加,都可能改变排队与服务时间。

调整 IRQ 亲和性也不能只追求均匀。合理分布可能缓解热点,但频繁迁移会影响缓存与局部性,系统中的自动管理机制还可能覆盖手工设置。应先确认哪个队列、哪个核心承压,再做有限实验并保留回退方法。监控中断与软中断的目的,是解释业务延迟的来源,不是把所有计数都压到最低。最终要检查吞吐、丢包、CPU 成本和 p99 是否共同满足目标。

8 案例:订单写库偶发毛刺与 I/O 等待

某零售订单接口平时 p99 为 80 毫秒,促销期间每隔几分钟升到 900 毫秒,应用主机 CPU 约 30%。团队最初怀疑线程不够,于是扩大工作线程池。结果同时在途的数据库提交更多,毛刺延续时间变长。这个现象说明排队可能被放大,却还不能直接证明存储是根因:锁竞争、连接池等待和远端网络都可能产生类似表现,需要进一步拆分关键路径。

调用链显示,价格计算和库存检查稳定,慢请求主要停留在事务提交。应用连接池等待随后上升,表示较慢的提交延长了连接占用,连接池成为后续请求的等待点。数据库侧的等待事件指向日志同步完成;在数据库所在主机采样,w_await 与平均未完成 I/O 同时升高,而应用机器本地磁盘正常。这里需要分清最先变慢的依赖和随后被占满的资源,避免只扩大连接池掩盖上游压力。

存储毛刺如何传导成订单接口排队
存储毛刺如何传导成订单接口排队

进一步对齐后台任务,发现毛刺窗口与备份读取及集中写入重叠。团队在受控环境保持订单请求分布不变,暂停相关后台任务后,日志同步耗时明显回落;恢复竞争负载又重现延迟。这比单次曲线相关更有说服力。还需核对日志、数据与备份实际落在哪些设备或共享存储路径上,目录不同并不能证明底层资源已经隔离,逻辑卷名字不同也不构成隔离证据。

处置分成即时缓解与容量修正。即时限制后台任务带宽,压低订单在途提交数,防止设备队列持续增长;随后按持久化负载规划存储余量,验证日志路径与后台任务的资源隔离。对于事务批量与提交策略,必须保留业务认可的持久性要求,不能为降低延迟随意关闭必要同步。每项改动都要在目标吞吐下检验错误率、超时和延迟分布,避免只改善空载表现。

本例中的 IRQ 没有异常增加,软中断热点也不明显。这条证据帮助排除部分方向,但不能说“没有中断问题,所以一定是磁盘”。最终结论来自调用链、数据库等待、设备指标和受控复现的相互支持。处理完成后,团队还保留提交延迟和后台任务重叠告警。这样下次 p99 抬升时,可以先识别等待发生的位置,再判断是否属于同类问题。

9 排查案例:CPU 不高但 p99 飙升

另一次故障表面相似:订单 API 的 p99 从 100 毫秒升到 1.2 秒,整机 CPU 只有 40%。这次事务提交时间没有明显变化,慢请求主要发生在读取下游响应与返回客户端的阶段。排查先标注客户端、应用、数据库和网络的时间边界,确认不是客户端统计口径变化,也不是采样时钟误差。随后对比请求量、响应大小和重试量,避免把放大的流量误认为独立原因。

按核心展开后,发现某个核心的 %soft 接近饱和,其他核心较空闲。/proc/interrupts 的窗口增量显示多个繁忙接收相关向量集中在少数核心,/proc/softirqs 的 NET_RX 活动也集中。与此同时,接收丢包与 TCP 重传增加。计数提供的是处理压力分布,结合每核 CPU 与网络结果才形成较完整的判断:局部网络处理能力不足,整机平均值把热点稀释了。

继续追踪流量,发现新接入的探测程序产生了大量小请求,有效业务字节数增加不多,包速率却大幅增加。对网卡与 CPU 来说,小包也需要状态管理和协议处理,所以带宽未接近上限并不表示网络路径有余量。团队先限制异常探测频率,再检查队列与 IRQ 分布是否符合主机配置;没有直接套用另一台机器的亲和性参数,因为核心拓扑与队列设置可能不同。

受控验证中,合理分散处理负载后,热点核心的软中断占用下降,重传减少,订单 p99 恢复。随后比较不同合并参数:过小的合并值在高包速率下增加通知成本,过大的值又抬高低负载请求延迟。最终采用满足目标负载与延迟约束的设置,并记录驱动版本、队列数和原参数,以便回退。这个结论只适用于验证过的环境,不能变成“所有机器都加大批量”的规则。

若同样的初始症状下,每核 CPU、中断、网络都正常,下一步仍应回到等待链:线程是否堵在连接池,数据库是否等锁,外部接口是否变慢,超时与重试是否形成额外排队。设备完成到线程实际运行之间,还可能存在运行队列等待。排查的顺序是缩小等待范围,再用对应证据验证;“CPU 不高”只说明平均计算使用情况,不能替代对依赖完成时间的分析。

放进方案里

订单系统方案应先写清完成边界:接口在事务提交并满足约定的持久化条件后返回成功,超时重试使用幂等键处理结果不确定的情况。把目标请求量、请求与响应大小、读写比例、日志同步频率和突发持续时间列为容量输入,同时规定 p99、错误率与恢复时间。网络容量除带宽外还要考虑包速率,存储容量除空间外还要考虑延迟与并发处理能力。

应用侧设置连接池、工作队列和在途 I/O 上限,规定队列满时的背压或拒绝方式。让请求超时与下游调用预算相互匹配,限制重试次数,避免设备变慢后不断叠加工作。持久化策略由业务可靠性要求决定;如果考虑批量提交,应说明吞吐收益、等待上限和故障时的语义,不能把降低可靠性藏在性能参数里。

观测方案同时覆盖业务、内核和设备。保存订单调用链与数据库等待信息,采集每核 CPU、IRQ 与软中断活动增量、设备延迟和平均未完成 I/O、网络丢包及重传,并使用一致时间窗口关联。告警以业务延迟与错误为入口,再用资源指标辅助定位;不要单凭 %util 或 %iowait 的固定阈值宣布容量耗尽。

验证计划至少包含正常负载、短时突发、存储后台竞争及大量小包场景,检查目标吞吐下的尾延迟与排队是否有界。中断合并、IRQ 分布和卸载设置逐项比较,保留原值、实验条件与回退步骤。上线后核对预期与实际负载差异,把局部核心热点和持久化等待纳入容量复盘,使 I/O 与中断约束成为可验证的非功能需求,而非上线后的临时猜测。

10 小结:I/O 与中断清单接到容量与 NFR

I/O 等待来自尚未完成的依赖。轮询用主动检查换取事件发现,中断用通知与处理成本换取无事件时的 CPU 空间;混合批处理需要同时考虑通知成本和等待时间。DMA 减少逐字节搬运,但完成处理、协议工作与线程唤醒仍需要 CPU,设备完成也不代表应用立即运行。

审阅订单系统的容量与非功能需求时,逐项确认:完成与持久化边界是否明确,队列与在途请求是否有上限,存储与网络压力是否覆盖真实业务分布,观测是否能看到每核热点,调优是否经过目标负载验证。面对“CPU 不高但 p99 很高”,先问请求在等哪个条件,再追踪条件何时满足、线程何时继续执行。

可以用 3 个问题自测:为什么增加异步并发可能让磁盘排队更长?为什么中断计数下降不代表网络数据量下降?为什么日志已经完成写入,订单线程仍可能稍后才运行?若能分别用容量边界、批量通知和唤醒后的运行等待解释,就能把设备、内核与业务延迟连成可用于排查的因果链。

下一课转向 H15 服务器形态:把本章验收过的结论带进后续主题,写成可检查的方案约束。

同阶段相关课

AI 答疑

默认关闭。启用后,只发送本课标题、最近阅读的屏幕内正文摘录(最多 6000 字)与本次问题。

配置自己的服务