返回

文章详情

无预读的 io_uring

Hacker News2026年9月1日 13:19

有人在 Turso 中打开了一个 PR 来实现预读。这是一个临时的实现,但这是一个很好的借口来测量 io_uring 并更多地了解它。Turso 有两个后端:syscall 使用 pread(2)。io_uring 使用 io_uring,并使用 O_DIRECT 打开数据库文件,没有使用缓冲 I/O 的选项。O_DIRECT 去掉了内核预读,因此要把它找回来就意味着在应用程序中实现它。PR 的结果令人印象深刻。使用应用程序缓冲区的 io_uring 更快。我想了解原因。没有预读,io_uring 每次只发出一个条目。问题在于缺乏并发:每次读取都要等待上一个读取。应用程序知道“嘿,在这个时间,我需要第 100 页”,这意味着:Turso 为第 100 页提交了一个读取 SQE,然后等待它。扫描继续。现在 Turso 需要第 101 页,因此它为该页提交了一个新的读取 SQE。开启预读后,事情就改变了。Turso 需要第 100 页,它检测到了顺序访问,因此不是只请求一页,而是提交了对第 100 页到第 131 页的读取请求。现在 32 个读取请求同时进行。下面的测量使用 TPC-H,这是一个标准的分析数据库基准,对一个 1.2 GiB 的数据库进行。Q6,我测量的查询,对 lineitem 进行全表扫描,这是基准中最大的表。off 代表 PRAGMA prefetch_pages=0:没有预读的 PR 的代码。on 代表 32 页的窗口。请求合并 我想看看预读在 I/O 路径中改变了什么:Turso 提交了多少请求,以及什么到达了设备。因此我在运行 Q6 的同时,运行了 iostat -dxm 1 sda,并统计了 io_uring:io_uring_submit_req 跟踪点。 off (n=1) on (n=1) SQEs 提交 195,207 218,212 设备请求 ~196,000 ~16,300 rareq-sz 4.37 KiB 56.53 KiB %rrqm ~0 91-93% rareq-sz 是到达设备的读取请求的平均大小。 %rrqm 是在发出之前与另一个请求合并的读取请求的百分比。开启预读后,Turso 发出了 23,005 个额外的 SQE,并且它获取了比所需更多的页面,使得设备读取了更多的字节。但设备接收到的请求更少。如果队列中的两个请求覆盖相邻的扇区,块层将它们合并为一个更大的请求。这只有在两个请求同时在队列中时才能发挥作用。我用 perf 统计了合并跟踪点。关闭预读时,仅有 140 个 195,516 的 bios 被合并。因为队列中只有一个 SQE,没有任何东西可以合并。开启预读时,218,493 个 bios 中有 202,539 个被合并,设备仅接收到 15,951 个请求。由于队列中有多个 SQE,内核将它们合并。轮询线程 Turso 使用带有 sqpoll 的 io_uring。sqpoll 使用一个线程,该线程不断检查工作是否已完成,以及内核是否应该做某事。这是一个跟踪工作的线程,代价就是不断轮询。我用 prefetch_pages=32 计时了 sqpoll 的执行时间,看看时间花在哪里:8.22s 的墙时间,3.70s 的用户时间和 8.46s 的系统时间(七次运行的中位数)。系统时间大于墙时间,而这只有在两个线程同时使用 CPU 时才能实现。我本以为周期在查询代码中,但:使用 SQ 轮询的 io_uring 后端的 Q6。内核轮询线程 io_sq_thread 使用了 65% 的周期。因此我重新构建了 Turso,去掉了 SQ 轮询,将 setup_sqpoll 构建器替换为简单的 IoUring::new,这已经是当 sqpoll 设置失败时 Turso 的后备方案。普通/默认环:8.62s 的墙时间,3.62s 的用户时间和 1.27s 的系统时间。去掉轮询线程使墙时间稍微变差,系统时间大幅减小。系统时间不是零,因为我们仍然每次提交都调用 io_uring_enter(2),而 Turso 每次提交一个 SQE。轮询是否值得取决于机器。Didona 等人在 2022 年 SYSTOR 论文中测量了这一点:使用一个 NVMe 驱动器和一个 CPU 核心的提交轮询仅达到 13 KIOPS(每秒 13,000 个 IO 操作)——这两个线程必须共享一个核心,因此他们交替进行。拥有第二个核心后,性能完全恢复。我正在使用的这台机器有 4 个 vCPU。查询是单线程的,使用一个环和一个轮询线程,并且轮询线程始终有一个空闲核心,因此它从未与查询竞争 CPU。缓存未命中 禁用 SQ 轮询后,我再次在两个后端上计时了 Q6:在 io_uring 上为 8.55s,在 syscall 上为 3.02s。差异显著。我想明白那额外时间去哪了,所以我用 perf 统计了指令和缓存未命中。该表中的计数器有一点说明:它们来自于重新构建的主机(谁想为闲置时间支付 Hetzner?),因此它们的绝对值与上述时间不匹配。后端 周期(中位数,n=7) 指令(中位数,n=7) IPC 缓存未命中 未命中率 io_uring 普通 5.374 B 21.497 B 3.999 10.666 M 13.255% syscall 4.734 B 21.297 B 4.499 5.889 M 7.691% io_uring 执行了 0.2 B 更多的指令,这几乎没有,但它产生了 480 万更多的缓存未命中。

赞助内容

NordVPN Next-gen Antivirus

本站免费、广告极少。如果觉得有帮助,可以请我们喝杯咖啡 —— 任何金额都对持续运营有实际帮助。

请我喝杯咖啡