一 核心内容
论文分析了传统远端内存存储系统的性能瓶颈, 并依据这些瓶颈提出了 MAGE, 在 Linux 和 LibOS 上实现了对远端内存系统的有效改进.
- 严格限制驱逐线程的数量以避免 IPI 风暴导致的 TLB Flush 延迟过高
- 严格控制应用程序线程和驱逐线程的行为, 明确区分职责
- 利用批处理和流水线来掩盖 TLB Wait 和 RDMA Wait 的延迟
- 减少锁竞争的开销
二 研究动机
论文第三节提出了当前的 Far Memory System 的局限性和性能瓶颈.
Far Memory System
在传统云计算的服务器架构中, 内存(DRAM)直接插在 CPU 旁的插槽中. 然而这样的架构存在一些问题:
- 搁浅(strand): 若一台物理服务器的内存卖完了而 CPU 还有空闲, 则空闲 CPU 处于可用而未用的状态, 称为CPU搁浅; 相对地, 若一台服务器的 CPU 卖完了但是内存仍然存在空闲, 对应内存处于可用而未用的状态, 称为内存搁浅. 核心问题是搁浅的内存无法被其他物理机器上搁浅的 CPU 使用, 搁浅的 CPU 也没有内存用于计算.
- 容量不足: 对于现代较大规模的应用(如数据库系统), 单机的内存是显然不够用的, 通常通过 buffer pool manager 等方式从磁盘载入和换出, 但这样会有 I/O 的开销.
由此产生了远端内存(Far Memory)的概念. 从存储层级来看, Far Memory 处于 Local DRAM 和 本次磁盘之间, Local DRAM 称作 Near Memory, 而与之相对的远端内存则是通过高速网络(RDMA)或专用协议(如 CXL) 被访问的远端服务器内存. (当然会比本地内存慢一点, DRAM 访存在 100ns 左右; 而 CXL 大约在 250ns 左右, RDMA 会更慢一些)
现有远端内存系统存在的问题
论文通过对 DiLOS 和 Hermit 进行了一些实验, 验证了两者在多核场景下性能崩溃(可扩展性较差, 随着线程数增加, 吞吐量在短暂提升后很快持平甚至降低), 并进一步确认了其性能崩溃并非来源于应用层的 Thrashing 而是分页机制的软件同步和全局锁竞争.
宏观测试
通过对多种应用程序内存访问模式下不同内存卸载比例(有多少内存在本地缺页而需要通过 RDMA 从远端访问)的测试. 首先设定了一个理想的基线(所有内存都在本地, 相当于完全没有远端内存参与)用作参照. 基本运行时长 \(T_0\), 加上某一核心 Page Fault 的次数乘以单次 Page Fault 延迟 \(L \times F_{c,x}\) 则为核心 \(c\) 上的该任务运行时间; 任务完成取决于最慢(产生页错误最多)的核心, 则取最小后为单次任务所需时长, 除 \(3600\) 则为一小时的任务数:
\[\text{Thp}_{\text{ideal}}(x) = \underset{c\in C}{\text{min}}(\frac{3600}{T_0 + L\times F_{c,x}}) \text{ jobs/hour} \]
- 在随机访问的访问模式下, 仅仅 10% 的内存卸载就造成了 70% 左右的性能下降
- 在局部性较好的规则/顺序访问模式下, 即使有 prefetch 机制减少了随机访问的 Page Fault, 也仍然因为空闲页耗尽触发同步驱逐而限制吞吐量
- 工作集阶段切换(如 MapReduce): 新老页面交替时造成长达数秒的拥堵
Fault-in 路径和 Eviction 路径都存在无法多核线性扩展的问题. 为确认瓶颈在哪个路径, 论文采用的测试方案是:
每个线程在充足网络带宽下进行页的顺序读取, 区分两种场景:
- 仅处理缺页, 设定 100% 的本地内存数据配额, 但是预先驱逐所有页(本地没有数据, 数据都在远端), 导致每次访问都会 Page Fault, 且只统计 Page Fault 的开销)
- 处理缺页并且主动换出内存. 设定 50% 的数据在本地内存配额, 这导致每从远端拉下来一页, 就会导致本地的一个页被驱逐
情况 2 比情况 1 出现大量的性能损失, 证明驱逐路径的崩溃是主要瓶颈
TLB Shootdown 和 IPI 风暴
当从 local memory 中驱逐一个数据页到 far memory 时, 不仅需要写回远端内存并释放数据页, 还要清理本地页表和 TLB 避免再次通过 TLB 或页表访问到已经被驱逐的数据页.
MMU 一般在 CPU 芯片上, 每个 CPU 核心都有自己的 TLB, 因此 TLB Flush 必须通知其他核心也清理自己 TLB 中对应地址的映射缓存. 这就需要 IPI (Inter-Processor Interrupt) 来通知其他核心. 而 IPI 毕竟是个中断, 对于 CPU 来说还是非常慢的.
在单个 Host 上, Linux kernel 对同一地址空间的 TLB Flush 请求有一个
mmu_gather 的过程进行聚集, 即使有多个 TLB Flush,
也不会朴素地发送多个 IPI. 但是即使存在 mmu_gather,
当大量应用程序线程同时发 IPI, 硬件的中断队列会很快被 \(N \times N\) 的中断风暴填满,
从而导致特定某一个 TLB Flush 的延迟极高(因为还要等待之前的 TLB Flush
完成).
LRU 链表的锁竞争
在支持远端内存的系统中, 需要判断页面热度来决定页面的去留. 被访问次数较多的页称作热页, 按照存储层级的看法应当被放在较快但是容量相对较小的 local DRAM 层; 与之相对的称作冷页, 显然应该放在容量更大(虽然较慢)的远端内存. 而判断页面冷热通常通过 LRU/LRU-K/ARC 等页面置换算法实现, 这依赖(显然应当)共享的数据结构如 LRU 链表. 由于不同 Host 判断页面冷热的需求较频繁, 对 LRU 链表的共享操作造成了较严重的锁竞争.
页面迁移导致的全局分配器的锁竞争
由于不同页面频繁地在本地 DRAM 和远端内存之间迁移, 且这个操作一定需要对全局内存分配器上锁, 因此对 allocator 共享操作的锁竞争同样拖慢了吞吐效率.
三 MAGE 系统设计
针对以上三个问题, 论文分别提出了解决方案.
设计原则
MAGE 有三条设计原则.
- 永远解耦后台驱逐线程和前台应用程序线程, 前台应用程序绝不亲自下场驱逐页面. 当内存不足时, 应用程序就只是等着驱逐线程”开路”, 腾出新的页面后继续进程.
- 用批处理+流水线技巧来掩盖延迟(话说这就是我进组时候面试的代码任务).
既然原先发 TLB Flush 是同步的, 即需要
发 TLB 请求->TLB Wait->接受 ACK, 那干脆就在 TLB Wait 期间去做其他事情而不是干等着(当然, 对于本次驱逐, 必须在 TLB ACK 之后才能去发 RDMA 具体驱逐页面, 否则可能会有线程通过旧的 TLB 访问到已经不在本地内存的页面, 一般是进行下一次驱逐的 TLB Flush Send 或者上一次驱逐的 RDMA Send). RDMA 请求也类似, 在 RDMA Wait 期间可以做其他事情. - 由于统计页面冷热时对共享数据结构的操作造成了锁竞争, 那么干脆设立 Per-CPU 的统计数据结构, 避免冲突. 这样会降低对冷热页的判断精度, 但是这样的 trade-off 是合理的.
其他优化
为了减少 TLB Shootdown 造成的 IPI 风暴, MAGE 严格限制驱逐线程数量, 论文认为 4 个驱逐线程是一个 sweet pot, 甚至能跑满 200Gbps 网卡的上限.
为了避免全局 bitmap 或者 free list 的锁竞争, MAGE 直接采用了线性映射的方式, 即物理地址唯一对应一个偏移的虚拟地址, 避免了锁竞争. 但是坏处是连续虚拟地址无法映射在不连续的物理内存, 远端内存的释放也变得没意义了(因为不会有人再用这一物理页), MAGE 的观点是”反正远端内存又大又便宜, 牺牲一点空间去增加吞吐量降低延迟会更好”
五 阅读总结
验证对比实验比较耳目一新, 这个 profiling 的过程感觉很厉害我要是也能有这个水平就好了
处理方式的话感觉比较经典, 异步, 批处理流水线
不过这篇总结其实写得不太认真, 我还有两篇要读, 感觉最近早睡之后效率实在有点太低了