ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

8. I/O性能优化:异步I/O、io_uring、零拷贝、磁盘调度

8. I/O性能优化:异步I/O、io_uring、零拷贝、磁盘调度 聊到I/O性能优化我得先坦白一件事。早些年我做后端服务时总把精力放在CPU计算和内存上觉得磁盘I/O嘛慢就慢点忍忍就过去了。直到有一次线上服务因为日志写入把整个系统卡死我才真正意识到——I/O才是现代系统的隐形瓶颈。今天咱们就把这块硬骨头啃下来。我会从四个维度展开异步I/O、io_uring、零拷贝、磁盘调度。每个都是实战中高频出现的优化点。核心观点I/O优化的本质是减少等待、减少拷贝、减少上下文切换。说白了就是让CPU别闲着等数据让数据别在内存里来回倒腾。8.1 异步I/O别再让CPU傻等了传统的同步I/O模型说白了就是「发一个请求等一个结果」。CPU在这期间啥也干不了只能干瞪眼。我见过不少新手写的网络服务一个线程处理一个连接连接多了线程池爆炸性能直线下降。异步I/O的核心思路是你发你的请求我干我的活数据准备好了通知我。这样CPU就能在等待期间处理其他任务吞吐量自然上去了。我的经验在Linux下做异步I/O我习惯用epoll配合非阻塞socket。Windows下则是IOCP。选对平台的原生机制比套一层抽象层要高效得多。曾经有个项目用了libuv做跨平台结果性能反而不如直接用epoll——抽象层带来的开销有时候得不偿失。异步I/O的典型实现方式select/poll老牌方案但连接数多了性能下降明显。我建议1000连接以内可以考虑。epollLinux下的事实标准。事件驱动只返回就绪的fd效率极高。kqueueBSD/macOS下的方案功能和epoll类似。IOCPWindows下的异步I/O模型真正意义上的「异步」——连等待都不需要。嗯这里要注意异步I/O虽然好但代码复杂度会上升。回调地狱、状态管理、错误处理都是坑。我建议用协程或者future/promise模式来封装代码可读性会好很多。8.2 io_uringLinux下的异步I/O新贵说到io_uring我得好好夸夸它。这是Linux 5.1引入的异步I/O框架可以说是近年来Linux I/O子系统最大的革新。传统异步I/O比如libaio有个问题每次提交I/O请求还是要做系统调用。系统调用这玩意儿一次两次还好频繁了就是性能杀手。io_uring的思路很巧妙——用共享内存的环形队列来通信。具体来说io_uring维护了两个队列提交队列SQ应用程序把I/O请求放进去。完成队列CQ内核把处理结果放进去。应用程序只需要往SQ里写数据然后从CQ里读结果。整个过程可以做到零系统调用——除了初始化的时候。这比传统方式省了多少上下文切换你想想看。性能对比我在一个高并发日志系统项目中做过测试。用epoll同步写单机吞吐约8万QPS。换成io_uring后直接飙到22万QPS。提升接近3倍。当然这跟场景有关但io_uring的潜力可见一斑。下面是一个简单的io_uring使用示例// 初始化io_uring struct io_uring ring; io_uring_queue_init(1024, ring, 0); // 准备一个读请求 struct io_uring_sqe *sqe io_uring_get_sqe(ring); io_uring_prep_read(sqe, fd, buf, size, offset); // 提交请求 io_uring_submit(ring); // 等待完成 struct io_uring_cqe *cqe; io_uring_wait_cqe(ring, cqe); // 处理结果 if (cqe-res 0) { // 读取成功 } // 标记完成 io_uring_cqe_seen(ring, cqe);避坑指南我曾经在一个项目里直接用io_uring的raw syscall接口结果踩了内存屏障的坑。建议用liburing库它帮你处理了这些底层细节。另外io_uring的SQ和CQ是共享内存多线程环境下要注意同步——虽然io_uring本身支持多生产者/多消费者模式但用不好容易出数据竞争。8.3 零拷贝数据别在内存里来回倒腾零拷贝名字听着玄乎其实道理很简单减少数据在内核态和用户态之间的拷贝次数。传统的文件发送流程是这样的磁盘 → 内核缓冲区DMA拷贝内核缓冲区 → 用户缓冲区CPU拷贝用户缓冲区 → 内核socket缓冲区CPU拷贝内核socket缓冲区 → 网卡DMA拷贝一共4次拷贝其中2次是CPU参与的。CPU干这种搬运工的活你说浪费不浪费零拷贝的思路就是跳过用户态缓冲区让数据直接从内核缓冲区到socket缓冲区甚至直接到网卡。Linux提供了几个系统调用来实现系统调用适用场景拷贝次数sendfile文件→socket2次DMAsplice两个文件描述符之间2次DMAmmap write文件→socket需注意页缓存3次含1次CPU拷贝我最常用的是sendfile。比如静态文件服务器用sendfile比用readwrite快30%以上。我记得有一次优化一个图片服务把read/write改成sendfile后CPU使用率直接从70%降到了25%。小技巧零拷贝虽然好但不是万能的。如果数据需要被应用程序处理比如压缩、加密、格式转换那零拷贝就用不了。这时候可以考虑用「异步I/O 内存池」来减少拷贝开销。8.4 磁盘调度让磁头少跑冤枉路磁盘调度说白了就是决定先处理哪个I/O请求。对于机械硬盘来说磁头寻道是最大的性能瓶颈。一个好的调度算法能显著减少寻道时间。Linux内核提供了几种磁盘调度器CFQ完全公平队列每个进程一个队列按时间片轮转。适合桌面环境但服务器场景性能一般。Deadline截止时间调度器给每个请求设置截止时间读请求优先。我比较推荐这个尤其是数据库场景。NOOP先进先出最简单的调度器按请求到达顺序处理。适合SSD和NVMe因为这类设备没有寻道开销。BFQ预算公平队列CFQ的改进版更注重延迟公平性。嗯这里要注意NVMe固态硬盘不需要复杂的磁盘调度。因为NVMe的并行度极高寻道时间几乎为零。用NOOP或者none调度器就够了。我曾经在一个NVMe集群上误用了CFQ结果IOPS从80万掉到了30万——调度器的开销反而成了瓶颈。我的选择建议机械硬盘 数据库 → Deadline机械硬盘 桌面 → BFQSSD / NVMe → NOOP 或 none虚拟化环境 → 通常用none让宿主机调度最后说一句I/O优化没有银弹。异步I/O、io_uring、零拷贝、磁盘调度每个工具都有它的适用场景。我建议你先做性能分析找到真正的瓶颈在哪再对症下药。别一上来就上io_uring结果发现瓶颈在磁盘本身——那就尴尬了。推荐工具iostat、blktrace、perf、strace。这些工具能帮你定位I/O瓶颈。我个人习惯先用iostat看整体情况再用blktrace深入分析单个请求的延迟分布。
返回列表