ARTICLE DETAIL

资讯详情

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

SSD固件优化实践:L2P映射表硬件加速、GC与驻留策略深度解析

SSD固件优化实践:L2P映射表硬件加速、GC与驻留策略深度解析 干过SSD固件或者存储系统底层的人应该都有过这样的体会整个FTLFlash Translation Layer里最让人又爱又恨的东西就是L2P映射表。说它让人爱是因为所有逻辑地址到物理地址的转换都靠它主机的读写请求能不能命中全看这张表查得快不快、准不准说它让人恨是因为一旦容量上去、并发一高、GC一启动这张表就会变成性能瓶颈的“集火点”。我这两年正好在折腾L2P表相关的优化工作从硬件数据通路的搭建到GC过程中VPC表的构建瓶颈再到运行时驻留策略的调整踩了不少坑也沉淀了一些可以复用的思路这篇就把我的实践过程完整拆开讲一讲。这篇内容主要围绕三个核心问题展开第一L2P表为什么需要专门的硬件级数据通路来做查表和更新第二垃圾回收GC场景下VPC表的构建瓶颈到底卡在哪里怎么破第三在DRAM和SRAM资源都受限的实际产品里L2P表运行时驻留策略应该怎么设计。做SSD固件、存储控制器验证、以及研究FTL算法的朋友都可以在里边找到一些可以直接落地的参考方案。1. L2P表为什么值得一条硬件级数据通路L2P表说白了就是一张记录了“逻辑地址到物理地址”对应关系的表。主机下发一个读命令控制器得先根据逻辑地址LBA去查这张表拿到物理地址之后才能去对应的Flash位置读数据。写命令则反过来先分配一个物理页然后把新的映射关系更新到表里。很多人觉得查表不就是一个内存读取操作嘛有什么好讲的但真正放到高并发、低延迟的NVMe SSD场景下事情远没有那么简单。固件跑在控制器内置的CPU核上这个核的性能比我们平时用的应用处理器差不少频率低、核数少。主机侧可能同时压着几十K甚至上百K的IOPS每一条IO都要走一遍L2P查表。如果这一步完全靠CPU软件去查CPU资源很快就会被吃光留给GC搬运、磨损均衡、坏块管理这些后台任务的算力就所剩无几了。更麻烦的是软件查表还要经过总线读写缓存延迟高且不可控导致IO延迟抖动明显。所以业界的通用做法就是把L2P的查找和数据搬运从CPU手里拿出去专门用硬件逻辑来实现这就是硬件级数据通路的由来。1.1 L2P硬件数据通路的整体形态一条完整的硬件数据通路通常包含命令解析模块、L2P查找引擎、表项缓存、DRAM控制器接口和结果返回通路。主机命令进来之后先由硬件解析出逻辑地址然后查找引擎拿着这个逻辑地址去查映射表拿到物理地址后要么直接去访问Flash要么把结果返回给固件做后续处理。这里有一个关键的设计点L2P表本身是放在DRAM里的而DRAM访问有延迟。如果每一条IO都直接去DRAM里查表DRAM带宽会成为新的瓶颈。所以必须在硬件查找引擎和DRAM之间加一层片上缓存通常用SRAM实现缓存最近访问过的映射表项原理跟CPU的TLB是一回事。我测试过一组数据加了SRAM缓存之后L2P查询的命中率能达到90%以上平均查询延迟比直接访问DRAM低了一半还多。1.2 硬件查表应该用什么数据结构这一点特别容易被做成软件方案的简单平移然后发现在硬件上行不通。软件环境里L2P表用哈希表或者多级页表都行因为CPU处理哈希冲突很灵活。但硬件逻辑做哈希冲突处理非常痛苦逻辑复杂度直线上升时序收敛也难。所以我在实际项目里更推荐定长多级表或者基于Radix树的硬件友好结构。定长多级表的思路是把逻辑地址拆成多段每一段作为一级索引最后一级条目指向物理页地址。这样查找路径固定硬件流水线非常好做每个周期都能发起一次查找。缺点是可能存在内部碎片大容量场景下内存利用率略低。但考虑到现在控制器的DRAM容量普遍在GB级别映射表多占用的那几百MB完全在可接受范围内。哈希方案不是不行但它的平均性能虽然好最坏情况下冲突严重时会卡住流水线对需要稳定延迟的存储场景不太友好。1.3 硬件数据通路设计中的几个关键细节设计硬件数据通路时有几个坑是我实际踩过之后才反应过来的这里重点提醒一下。第一表项更新和IO查询之间有并发一致性问题。主机在读数据的同时固件可能正在后台更新同一批映射表项比如GC搬移完数据之后要改映射。如果硬件查询引擎读到了更新前的旧表项就会拿到一个已经无效的物理地址。解决思路是给映射表项加版本号或者有效位固件更新前先做表项失效操作查询引擎发现失效后把这条查询请求踢回给固件软件处理。第二回写路径的顺序保障。L2P表项的更新必须严格保序。比如主机连续写两个扇区逻辑地址相同第一次写的映射结果如果没有落表第二次写的映射结果先落了表那就会导致映射指向新数据而旧数据尚未标记无效掉电时数据一致性就崩了。硬件通路里需要有一个FIFO队列保证同地址表项的更新按提交顺序执行。第三异常返回路径。硬件查表不是每次都能成功的比如映射表项不在缓存里或者目标表项还没分配。设计的时候必须留一条exception路径把这种查询请求导回给固件做软件处理。还有就是从Flash读映射表的场景映射表本身也放在Flash里冷启动时要把整张表加载到DRAM这个加载过程也最好做成硬件DMA搬运否则启动时间会很难看。2. GC里的VPC表瓶颈到底在哪GCGarbage Collection是FTL里绕不开的环节它的作用是回收那些写满数据、但已有部分页失效的物理块。GC要做的事情概括起来就三步选块、搬移有效页、擦除物理块。而选块和搬移过程中VPC表就是那个决定效率和写入放大系数的关键数据结构。VPC表全称Valid Page Count Table记录的是每个物理块当前还有多少个有效页。GC每次要挑一个块来回收按什么标准挑最朴素的标准就是挑VPC最小的块因为有效页越少需要搬移的数据量越小额外写入越少。这个逻辑说起来简单但VPC表本身的构建和维护在高容量SSD里会成为很重的负担。2.1 VPC表的构建过程为什么慢构建VPC表的核心操作是从L2P表反向推导每个物理块的有效页数量。具体做法是遍历L2P表的所有条目每一条映射拿到它的物理页地址然后把这个地址所属的物理块的计数器加一。等到整张L2P表扫完每个物理块的VPC就等于它的计数器的值。问题出在这个“遍历”上。L2P表动辄几百万条条目每条都对应一次内存读取和一次计数器增量。如果这个流程用CPU软件实现需要花费非常长的时间。而且这不只是时间问题——遍历L2P表的同时会产生大量的CPU缓存未命中因为映射表条目在内存里的分布是随机的你刚读完地址A下一个地址B很可能落在很远的位置。有次我在调试环境里测过用软件方式扫描一份容量约1TB的L2P表耗时超过3秒GC期间主机的IO差不多要停摆这种体验在产品里完全不可接受。2.2 GC过程中VPC表选块策略的另一种思路传统的VPC扫描选块是一种离线、静态的方式也就是GC发起时临时扫描全表。扫描成本高之外还有一个问题扫描得到的VPC是某一瞬间的快照等真正搬移数据的时候又有不少页因为新写的IO或主机的更新操作变成无效了。快照和实际之间始终存在误差如果误差方向是“预估有效页偏少”那GC搬移时会额外搬一些其实已经失效的页写入放大就上去了。更好的方案是维护增量式VPC。也就是在正常IO路径上动态维护每个物理块的VPC每次写分配时新分配的物理页对应的块VPC加一每次表项更新导致旧物理页失效时对应的块VPC减一。这样VPC表始终是实时的选块的时候直接查表就行完全不需要扫描碎片。这看起来很简单但它要求每一次IO的映射更新都必须同步维护VPC计数器。VPC表本身放在DRAM里那意味着每次映射更新都多了一次VPC表的读写操作。如果不能和L2P表更新合并为一次原子操作性能开销就会翻倍。我实际采用的折中方案是VPC表不用精确值采用周期性校准。具体做法是在平时用增量维护保持一个近似的VPC值GC选块的时候用近似值做粗选选出候选块之后再做一次小范围的精确扫描只扫描这几十个候选块的映射关系确认它们各自的真实有效页数。这样既避免了全表扫描的开销又能保证GC选出来的块是足够优秀的写入放大也能控制在可接受范围。2.3 硬件并行扫描对GC的有效加速即使采用上面说的粗选加精选方案GC过程中的精确校准步骤还是需要扫描一部分映射表数据。这部分工作我建议做成硬件模块不要占用CPU。思路是这样的硬件扫描引擎从DRAM里按顺序读L2P表的高位段提取出物理块号然后在一个片上计数器阵列里做增量。因为扫描是连续内存访问硬件可以一次性突发读很多条映射表条目再做流水线式处理吞吐量比CPU逐条扫描高好几倍。我还在这条通路里判断了跳过条件——如果某一段逻辑地址区间对应的物理块号全部落在同一批候选块里就只更新这几个计数避免不必要的比较。实测下来改用硬件扫描引擎之后候选块校准时间从原来的几百毫秒缩到了几十毫秒GC停IO的窗口大幅缩短。对前台主机IO的影响明显变小QoS数据好看了不少。3. L2P表运行时驻留策略内存放不下的取舍前面说的都是L2P表“怎么查”的问题接下来还有一个更现实的问题L2P表放哪儿、留多少。消费级SSD控制器里的SRAM通常只有几MB到几十MBDRAM也就几百MB到几GB。而L2P表的大小跟SSD容量成正比。以4KB映射粒度为例1TB的SSD有2.68亿个逻辑页每个映射表项假设压缩后占8字节那么表本身接近2GB。这还没算元数据和日志占用的空间。也就是说大容量产品里L2P表根本不可能全量放进片上SRAM甚至连只放在DRAM里都是个奢侈的选择。容量越大驻留策略越关键。3.1 全表驻留、部分驻留和分级驻留的适用场景驻留策略可以分为三个层级全表驻留就是把所有映射表条目都放在DRAM里。优点是查找路径最简单不存在换入换出的问题缺点是DRAM容量开销大而且掉电时整张表都要靠电容电量保住否则重建映射会耗时极长。这个方案在早期小容量SSD里很流行现在则主要用于企业级高端盘因为那些盘DRAM加得足够多。部分驻留则是当映射表总大小超过DRAM可用空间时只把一部分表项放DRAM其余部分留在Flash里需要时再换入。这相当于给L2P表做了一套二级存储体系。难点在于换入换出策略怎么定——换错了会频繁触发Flash读映射表那性能和随机写时会出现的卡顿会比想象中严重得多。分级驻留是我个人认为在容量、性能和成本之间平衡得最好的方案。顶层是一小块片上SRAM存最近访问过的映射表项中间层是普通DRAM存放常驻工作集底层是Flash里的系统区存完整映射表。SRAM命中走硬件快路径DRAM命中走正常的硬件查找两者都不中才回退到Flash加载。这种结构能让绝大多数IO只命中前两级有效规避了Flash读映射表的高延迟。3.2 热点表项识别与缓存替换策略分级驻留方案的核心模块是SRAM缓存层而缓存替换策略基本上决定了整个L2P查询的命中率。我一开始用的是最简单的LRULeast Recently Used但很快发现随机写场景下LRU表现很差。原因是随机写的映射表项更新很分散每一条新表项被访问过一轮之后很快就会被赶出缓存下次再写同一条地址时缓存已经失效了。明明整个表只有几万个热点表项缓存容量却一直在被冷门表项挤占。后来我改成了被反复验证过的ARCAdaptive Replacement Cache思路。ARC把缓存分成Recent区和Frequent区Recent区管新出现的表项Frequent区管经常被访问的表项。当一个表项在短期内连续两次被访问就把它从Recent区提升到Frequent区避免被新进入的表项挤掉。这个策略对存储场景特别适用因为实际业务里的访问模式往往是“热点相对集中、冷热交替出现”的。这个改动的效果非常直观。在我测的随机写模型里SRAM命中率从原来的82%提升到了94%左右平均IO延迟下降了差不多三成。而且ARC比LRU对突发访问模式的适应性更强GC搬移大批映射表项时不会被临时的突发访问打乱热点分布。3.3 脏映射表项的回写和掉电保护L2P表运行时驻留策略还有一个不能忽略的维度脏表项。所谓脏表项就是更新过但还没写回Flash的映射表条目。这部分数据只存在于SRAM或DRAM里如果突然掉电这些映射会全部丢失之后只能靠日志或全盘扫描重建非常耗时。掉电保护的核心思路是日志先行也就是新映射产生时先写一条映射更新日志到Flash日志写成功之后才算真正持久化。缓存里的脏表项什么时候落盘有几种触发条件一是缓存容量到达阈值需要替出脏表项时强制执行二是周期性后台刷盘三是GC搬移过程中涉及到的映射更新优先落盘避免GC完成之后掉电造成映射不一致。我在实际调优中发现脏表项的累积速率和刷盘频率之前存在很强的耦合关系。如果刷盘太频繁Flash写入放大增加如果刷盘太慢掉电时缓存里的脏表项太多重建时间会超出规格。平衡点建议根据实际IO负载来设比如在写密集型的测试模型里刷盘水位线设在缓存容量的60%比较合适水位线到80%时触发强制回写尽量避免缓存被脏表项挤占而影响新映射的写入。4. 常见问题排查与实操心得这几部分工作做下来积累了一些实际调试中遇到的典型问题单独写一节顺便分享一些我的排查习惯。这些问题在文档里不一定写得很清楚但遇到的时候都很要命。4.1 硬件查表通路返回错误映射的排查最诡异的一类问题就是硬件返回的物理地址在错误的时间点是对的在某个极端时序下却是错的。这种问题通常不是逻辑错误而是流水线上有先后的竞态。我排查的顺序是先查命令队列深度打满时的行为再查同地址乱序更新场景最后用形式化验证工具跑一遍断言。简单点说如果怀疑硬件返回了旧映射可以在硬件通路里加一个比较器每一条查询返回时都跟固件维护的软件映射副本做比对不一致就记录上下文。坏处是这会在调试版本里多耗不少资源但换来的是一条能在真实场景里精准抓到异常现场的调试通路值。4.2 GC选块和VPC维护的常见坑VPC计数器维护里最容易出现的坑是计数漂移。增量维护的VPC因为某次异常流程漏减了以后就会一直偏大。出现这种情况就很难排查因为计数器差值不固定GC选块的行为也会慢慢变得不优。我的建议是定期做一次全盘校准哪怕不做精确扫描也可以按物理块抽样校验发现偏差超限就把对应块的VPC重新算一遍。另一个坑是GC搬移过程中搬出的数据写到了目标块目标块的VPC加一但源块那个有效页应该在搬完之后标记失效并减一。这个“搬完再减”的顺序千万不能反否则掉电恢复之后会出现同一个逻辑页同时映射到两个物理页的异常状态。我们固件代码里把这两个操作写成了一个原子事务要么都完成要么都回滚保证恢复时不会出现双映射。4.3 驻留策略引起的QoS毛刺L2P表部分驻留策略下最头疼的副作用是QoS毛刺。想象一下主机正在跑稳定的随机写IO突然来了一波冷数据写的请求这些地址的映射表项不在缓存里需要从Flash重新换入换入的过程里IO延迟直接飙升好几毫秒。对延迟敏感的企业级业务来说这种毛刺比平均延迟高更加致命。缓解的办法是在换入路径上做预取。当缓存未命中触发一次Flash读映射表时顺带把该逻辑地址附近的一组映射表条目一起读上来。因为IO访问通常有局部性相邻地址的映射大概率也会在不久后被用到。这个预取粒度要根据映射表在Flash上的排列方式设计不要贪多一次读太多会吃掉Flash带宽反而拖慢正常IO。另外还可以设计一条“慢路径旁路”机制当缓存未命中需要从Flash换入时主机IO不阻塞等待先走一个低优先级的异步加载流程同时立即返回一个超时重试指示让主机稍后重试。这个机制在NVMe协议里有对应的状态码可以实现缺点是增加了主机侧的交互复杂度是否采用要权衡产品定位。4.4 实测数据驻留策略调整前后对比最后给一组我在真实测试环境里拿到的数据方便大家有个直观感知。测试模型是64KB随机写、队列深度32、SSD标称容量2TB映射粒度4KBSRAM缓存配置为8MB。调整前的策略是LRU、无预取、脏表项水位线50%实测平均IO延迟78微秒P99延迟4.2毫秒SRAM命中率82%。调整后的策略是ARC替换策略、带8KB映射预取、脏表项水位线60%实测平均IO延迟55微秒P99延迟1.8毫秒SRAM命中率94%。P99延迟降了57%这个数字对上线产品的体验提升是非常可感的。这个对比也说明一个问题L2P表运行时驻留策略不是简单选一种算法就完了替换策略、预取粒度、刷盘水位这些参数要放在一起调相互配合才能拿到最优解。单独调任何一个都很难有质的提升甚至可能互相抵消。再往深了说L2P表这一系列优化做完之后GC效率也跟着上来了。因为映射更新快了GC搬移时更新映射的开销就低VPC选块准了搬移次数就少驻留命中率高了GC过程中额外读映射表的次数就少。三者是环环相扣的单独优化某一块很难见效必须放在一起做整体设计。我在调试过程中一度只盯着驻留策略调结果GC性能上不去后来把VPC维护逻辑和硬件扫描通路一起改了问题才彻底解决。存储系统的性能优化从来不是一个点的事而是整条链路的事。
返回列表