Lec 16 高性能网络与调度
本节讨论操作系统在支持高性能网络时面临的挑战。阅读论文:Shenango: achieving high CPU efficiency for latency-sensitive datacenter workloads(NSDI'19)。
思考题(论文 Figure 2):Shenango 的整体设计和 xv6 网络实验有何不同?例如,在 xv6 里应用能直接把包写进网卡(NIC)队列吗?xv6 net 实验里有几个 NIC 队列?(见下文「与 xv6 网络实验的关系」一节。)
总览
- 问题背景:数据中心延迟敏感负载、尾延迟(tail latency)、低延迟 vs 高利用率的矛盾
- 传统 OS 网络为什么慢:中断、系统调用、上下文切换、每包 CPU 预算
- 内核旁路(kernel bypass):DPDK、RSS 队列 steering、轮询、per-core 隔离、ZygOS 基线
- Shenango 设计:IOKernel + 每应用 runtime + 不改的 Linux;5µs 拥塞检测;核分配算法
- 与 xv6 网络实验的关系(思考题)
- 论文重点图(Figure 2、拥塞检测、性能结果)
一、问题背景:数据中心的延迟敏感负载
典型负载:memcached 这类内存 KV 存储,单次 get/set 只要 ~1µs——应用本身不是瓶颈,系统开销才是。
尾延迟(tail latency)为什么是关键?关注的不是平均延迟,而是 99 分位。Google 的经验:典型响应 10ms,但每 100 个请求就有 1 个慢到 1 秒。放大效应:一个用户请求可能扇出到 100+ 台机器,只要每台有 1% 概率慢 1 秒,大量终端用户就会感到卡顿。
延迟的来源:多应用争抢机器、后台守护进程、突发时包排队、核间负载不均。
核心矛盾:低延迟 vs 高利用率为压低尾延迟,运营商只能让数据中心跑在 20–30% 的低利用率,预留的核大量闲置、浪费资源。目标:既要低尾延迟、又要高利用率——靠动态共享核来达成。
二、为什么传统 OS 网络慢
Linux 收包路径很长:
NIC → DMA 进 rx ring → 中断 → TCP/UDP 处理 → 拷到 socket 队列
→ 唤醒应用 → 系统调用开销 → 应用 read → 发包 → tx ring → NIC 发出瓶颈:
- 中断开销:约束在 ~100 万次/秒;
- 系统调用:上限几百万次/秒;
- 上下文切换:睡眠/唤醒线程带来延迟;
- 数据搬运:核间缓存一致性流量。
每包 CPU 预算算一笔账10 Gbps 以太网 ≈ 每秒 1000 万个 100 字节小包。一颗 2.4GHz 核每包只有 ~240 个周期,8 核也只有 ~1920 周期/包——而中断、系统调用、上下文切换轻易就吃光这点预算。所以「每包都进内核」根本来不及。
三、内核旁路(kernel bypass)
核心思路:把 NIC 队列和专属 CPU 核直接交给应用,绕过 Linux 内核,在用户态实现一个最小 OS(libOS)。
- 用户态库设计:直接访问 NIC 的 rx/tx ring;在用户态实现完整 TCP/IP 栈;在专属核上做多线程调度。
- RSS 队列 steering:网卡按
{srcIP, dstIP, srcPort, dstPort}五元组做一致性哈希,把同一条连接固定 steer 到同一个队列/核——理想情况下均匀分布、负载均衡。 - 轮询代替中断:专属核不停轮询 NIC ring,消除中断开销(前提是核被独占、不共享)。
- per-core 隔离的好处:每个核管自己的空闲包链表和 TCP 结构 → 无锁争用、无核间通信、单线程事件循环,同步极简。
基线系统 ZygOS(纯内核旁路)低负载下中位延迟 ~35µs、尾延迟 300–400µs,全程压得住。缺点:所有核都被预留独占,低负载时核大量闲置、无法像 Linux 那样把核共享给批处理任务——利用率低。
四、Shenango 的设计
目标:保留内核旁路的低延迟,同时把闲置的核动态回收给别的应用;当网络应用需要更多核时,及时抢占批处理任务、避免「计算拥塞」。
三层架构
| 层 | 角色 |
|---|---|
| IOKernel | 一个有 root 权限的专属 Linux 进程,独占一个核;坐在 NIC 与各应用 runtime 之间,每 5µs 扫描所有输入/输出队列,并把剩余的核动态分配给各 Shenango 应用;用软件把包 steer 到合适的核(比重编程网卡便宜)。 |
| 应用 runtime(每应用一个用户态 OS) | 用户态线程(green threads)+ 用户态 TCP/IP 栈 + per-core 包队列;保留最少核数,空闲核多时可「burst」借用。 |
| Linux 内核 | 不改动,继续跑后台任务;用 pthread affinity 把线程钉在核上;IOKernel 与 runtime 之间用共享内存 ring 通信。 |
拥塞检测(Algorithm 1,每 5µs 一次)
怎么判断「某应用核不够用了」?IOKernel 每 5µs 检查两类队列:① 某个包连续两次扫描都还在输入队列里 → 包在堆积;② 某个用户线程(uthread)一直留在 runtime 的 runqueue → 本地核处理不过来。任一成立就说明该应用核不够,需要加核。
核分配(Algorithm 2)——优先级
- 优先选「和该应用已有超线程同核」的核(缓存局部性最好);
- 其次选该应用最近刚让出的核;
- 再次随便挑一个空闲核;
- 实在没有,就抢占某个正在 burst 的(非预留)应用的核。
本质:尽量复用缓存、减小新核的冷启动延迟。Shenango 不是生产系统,而是「在 Linux 之上巧妙实现的一个新 OS」的研究成果——说明延迟敏感网络需要 OS 级的重新设计,而非仅靠库优化。
五、与 xv6 网络实验的关系(思考题解答)
Q:在 xv6 里应用能直接把包写进 NIC 队列吗?xv6 net 实验有几个 NIC 队列?
- 应用不能直接碰 NIC。xv6 走的是传统内核中介模型:用户进程通过系统调用(socket/read/write)进内核,由内核里的 E1000 驱动(
e1000.c的e1000_transmit/e1000_recv)去操作网卡的发送/接收环。应用碰不到 NIC 的 DMA 环——这正是 Figure 2 里「传统系统应用无法访问 NIC 硬件队列」的对照。 - xv6 net 实验只有一对队列:一个 TX 环 + 一个 RX 环(各 16 个描述符),单核处理、靠中断收包。
- Shenango 则相反:每个核一个队列(实验里 16 个 NIC 队列),应用 runtime 在用户态直接轮询属于自己的共享内存包队列,IOKernel 用软件把包按核分发——是 kernel-bypass + 多队列 + 轮询的路子。
| xv6 net 实验 | Shenango | |
|---|---|---|
| 应用能否直接访问 NIC 队列 | ❌ 必须经内核 E1000 驱动 | ✅ 用户态 runtime 直接轮询 |
| NIC 队列数 | 1 个 RX + 1 个 TX | 多个(每核一个,实验 16 个) |
| 收包方式 | 中断 | 轮询(专属核) |
| 协议栈 | 内核里 | 用户态 runtime |
六、论文重点图
按整理惯例,论文部分只记录图中最重点的内容。
Figure 2:整体架构
- Shenango 把数据面拆成特权的 IOKernel(独占核)和非特权的每应用 runtime。
- 包从 NIC 进来 → IOKernel 解复用 → 分发到共享内存队列 → 应用 runtime 直接轮询取走,全程绕过传统内核网络栈。
- 与常规系统形成鲜明对比:常规系统里应用根本访问不到 NIC 硬件队列。
拥塞检测机制图/算法
- IOKernel 约每 5µs 检查一次系统状态,同时盯包队列(NIC 积压)和应用 runqueue(待处理工作)。
- 队列深度上升 = CPU 不够 → 把核从批处理任务收回给网络应用。这个基于轮询的反馈环实现了亚毫秒级的负载响应。
核 steering / RSS
- IOKernel 用 RSS 把流量导向特定核——不是均匀摊开,而是按流 steer 到应用核,减少缓存失效与协调开销。
性能结果(Pareto 前沿与利用率)
- 延迟:请求-响应负载下尾延迟低至个位数微秒(Linux/IX 是毫秒级)。
- CPU 效率:网络低负载时能腾出 10–15 个核给批处理计算。
- 延迟-吞吐折中:比 Arachne、IX、ZygOS 好约 10× 的 Pareto 前沿;在 memcached、RocksDB 等真实负载上全面优于 Linux/IX/ZygOS。
- 注意 Shenango 的延迟曲线比 ZygOS 更早开始上升——因为它在动态共享核:负载升高、预留核占满后需要抢占批处理、重分配核,这一步引入了额外延迟(但换来了高利用率)。
自测清单
- [ ] 为什么数据中心关注 99 分位尾延迟而非平均?扇出如何放大尾延迟?
- [ ] 传统 OS 网络慢在哪几处?「每包 CPU 预算」为什么撑不住 10Gbps 小包?
- [ ] 内核旁路靠哪几招提速(队列直通、RSS、轮询、per-core 隔离)?ZygOS 的优缺点?
- [ ] Shenango 三层各管什么?IOKernel 每 5µs 在检测什么、据此做什么?
- [ ] 低延迟 vs 高利用率的矛盾,Shenango 如何同时拿下?
- [ ] xv6 net 实验里应用能直接写 NIC 队列吗?有几个队列?和 Shenango 差在哪?