现如今 CPU 的计算能力和磁盘的访问延迟之间的差距逐渐扩大,使得用户云主机的磁盘 IO 经常成为严重的性能瓶颈,云计算环境下更加明显。针对机械盘 IO 性能低下的问题,我们通过自研的云主机 IO 加速方案,使 4K 随机写的最高性能由原来的 300 IOPS 提升至 4.5W IOPS,提高了 150 倍,即用机械盘的成本获得了 SSD 的性能。13 年上线至今,该方案已历经五年的运营实践,并成功应用于全网 93%的标准型云主机,覆盖 12.7 万台实例,总容量达 26PB。
一.为什么需要 IO 加速
传统的机械磁盘在寻址时需要移动磁头到目标位置,移动磁头的操作是机械磁盘性能低下的主要原因,虽然各种系统软件或者 IO 调度器都致力于减少磁头的移动来提高性能,但大部分场景下只是改善效果。一般,一块 SATA 机械磁盘只有 300 左右的 4K 随机 IOPS,对于大多数云主机来说,300 的随机 IOPS 哪怕独享也是不够的,更何况在云计算的场景中,一台物理宿主机上会有多台云主机。因此,必须要有其他的方法来大幅提升 IO 性能。
早期 SSD 价格昂贵,采用 SSD 必然会带来用户使用成本的提升。于是,我们开始思考能否从技术角度来解决这个问题,通过对磁盘性能特性的分析,我们开始研发第一代 IO 加速方案。即使今天 SSD 越来越普及,机械盘凭借成本低廉以及存储稳定的特点,仍然广泛应用,而 IO 加速技术能让机械盘满足绝大多数应用场景的高 IO 性能需求。
二.IO 加速原理及第一代 IO 加速
我们知道机械磁盘的特性是随机 IO 性能较差,但顺序 IO 性能较好,如前文中提到的 4K 随机 IO 只能有 300 IOPS 的性能,但其顺序 IO 性能可以达到 45000 IOPS。
IO 加速的基本原理就是利用了机械磁盘的这种性能特性,首先系统有两块盘:一块是 cache 盘,它是容量稍小的机械盘,用来暂时保存写入的数据;另一块是目标盘,它是容量较大的机械盘,存放最终的数据。
1. IO 的读写
写入时将上层的 IO 顺序的写入到 cache 盘上,因为是顺序的方式写入所以性能非常好,然后在 cache 盘空闲时由专门的线程将该盘的数据按照写入的先后顺序回刷到目标盘,使得 cache 盘保持有一定的空闲空间来存储新的写入数据。
为了做到上层业务无感知,我们选择在宿主机内核态的 device mapper 层(简称 dm 层)来实现该功能,dm 层结构清晰,模块化较为方便,实现后对上层体现为一个 dm 块设备,上层不用关心这个块设备是如何实现的,只需要知道这是一个块设备可以直接做文件系统使用。
按照上述方式,当新的 IO 写入时,dm 层模块会将该 IO 先写入 cache 盘,然后再回刷到目标盘,这里需要有索引来记录写入的 IO 在 cache 盘上的位置和在目标盘上的位置信息,后续的回刷线程就可以利用该索引来确定 IO 数据源位置和目标位置。我们将索引的大小设计为 512 字节,因为磁盘的扇区是 512 字节,所以每次的写入信息变成了 4K 数据+512 字节索引的模式,为了性能考虑,索引的信息也会在内存中保留一份。
读取过程比较简单,通过内存中的索引判断需要读取位置的数据是在 cache 盘中还是在目标盘中,然后到对应的位置读取即可。
写入的数据一般为 4K 大小,这是由内核 dm 层的特性决定的,当写入 IO 大于 4K 时,dm 层默认会将数据切分,比如写入 IO 是 16K,那么就会切分成 4 个 4K 的 IO ;如果写入数据不是按照 4K 对齐的,比如只有 1024 字节,那么就会先进行特殊处理,首先检查该 IO 所覆盖的数据区,如果所覆盖的内存区在 cache 盘中有数据,那么需要将该数据先写入目标盘,再将该 IO 写入目标盘,这个处理过程相对比较复杂,但在文件系统场景中大部分 IO 都是 4K 对齐的,只有极少数 IO 是非对齐的,所以并不会对业务的性能造成太大影响。
2. 索引的快速恢复与备份
系统在运行过程中无法避免意外掉电或者系统关闭等情况发生,一个健壮的系统必须能够在遇到这些情况时依然能保证数据的可靠性。当系统启动恢复时,需要重建内存中的索引数据,这个数据在 cache 盘中已经和 IO 数据一起写入了,但因为索引是间隔存放的,如果每次都从 cache 盘中读取索引,那么,数据的恢复速度会非常慢。
为此,我们设计了内存索引的定期 dump 机制,每隔大约 1 小时就将内存中的索引数据 dump 到系统盘上,启动时首先读取该 dump 索引,然后再从 cache 盘中读取 dump 索引之后的最新 1 小时内的索引,这样,就大大提升了系统恢复的启动时间。
依据上述原理,UCloud 自研了第一代 IO 加速方案。采用该方案后,系统在加速随机写入方面取得了显著效果,且已在线上稳定运行。
三.第一代 IO 加速方案存在的问题
但随着系统的运行,我们也发现了一些问题。
1 )索引内存占用较大
磁盘索引因为扇区的原因最小为 512 字节,但内存中的索引其实没有必要使用这么多,过大的索引会过度消耗内存。
2 )负载高时 cache 盘中堆积的 IO 数据较多
IO 加速的原理主要是加速随机 IO,对顺序 IO 因为机械盘本身性能较好不需要加速,但该版本中没有区分顺序 IO 和随机 IO,所有 IO 都会统一写入 cache 盘中,使得 cache 堆积 IO 过多。
3 )热升级不友好
初始设计时对在线升级的场景考虑不足,所以第一代 IO 加速方案的热升级并不友好。
4 )无法兼容新的 512e 机械磁盘
传统机械磁盘物理扇区和逻辑扇区都是 512 字节,而新的 512e 磁盘物理扇区是 4K 了,虽然逻辑扇区还可以使用 512 字节,但性能下降严重,所以第一代 IO 加速方案的 4K 数据+512 字节索引的写入方式需要进行调整。
5 )性能无法扩展
系统性能取决于 cache 盘的负载,无法进行扩展。
四.第二代 IO 加速技术
上述问题都是在第一代 IO 加速技术线上运营的过程中发现的。并且在对新的机械磁盘的兼容性方面,因为传统的 512 字节物理扇区和逻辑扇区的 512n 类型磁盘已经逐渐不再生产,如果不对系统做改进,系统可能会无法适应未来需求。因此,我们在第一代方案的基础上,着手进行了第二代 IO 加速技术的研发和优化迭代。
1. 新的索引和索引备份机制
第一代的 IO 加速技术因为盘的原因无法沿用 4K+512Byte 的格式,为了解决这个问题,我们把数据和索引分开,在系统盘上专门创建了一个索引文件,因为系统盘基本处于空闲状态,所以不用担心系统盘负载高影响索引写入,同时我们还优化了索引的大小由 512B 减少到了 64B,而数据部分还是写入 cache 盘,如下图所示:
其中索引文件头部和数据盘头部保留两个 4K 用于存放头数据,头数据中包含了当前 cache 盘数据的开始和结束的偏移,其后是具体的索引数据,索引与 cache 盘中的 4K 数据是一一对应的关系,也就是每个 4K 的数据就会有一个索引。
因为索引放在了系统盘,所以也要考虑,如果系统盘发生了不可恢复的损坏时,如何恢复索引的问题。虽然系统盘发生损坏是非常小概率的事件,但一旦发生,索引文件将会完全丢失,这显然是无法接受的。因此,我们设计了索引的备份机制,每当写入 8 个索引,系统就会将这些索引合并成一个 4K 的索引备份块写入 cache 盘中(具体见上图中的紫色块),不满 4K 的部分就用 0 来填充,这样当系统盘真的发生意外也可以利用备份索引来恢复数据。
在写入时会同时写入索引和数据,为了提高写入效率,我们优化了索引的写入机制,引入了合并写入的方式:
当写入时可以将需要写入的多个索引合并到一个 4K 的 write buffer 中,一次性写入该 write buffer,这样避免了每个索引都会产生一个写入的低效率行为,同时也保证了每次的写入是 4K 对齐的。
2. 顺序 IO 识别能力
在运营第一代 IO 加速技术的过程中,我们发现用户在做数据备份、导入等操作时,会产生大量的写入,这些写入基本都是顺序的,其实不需要加速,但第一代的 IO 加速技术并没有做区分,所以这些 IO 都会被写入在 cache 盘中,导致 cache 盘堆积的 IO 过多。为此,我们添加了顺序 IO 识别算法,通过算法识别出顺序的 IO,这些 IO 不需要通过加速器,会直接写入目标盘。
一个 IO 刚开始写入时是无法预测此 IO 是顺序的还是随机的,一般的处理方式是当一个 IO 流写入的位置是连续的,并且持续到了一定的数量时,才能认为这个 IO 流是顺序的,所以算法的关键在于如何判断一个 IO 流是连续的,这里我们使用了触发器的方式。
当一个 IO 流开始写入时,我们会在这个 IO 流写入位置的下一个 block 设置一个触发器,触发器被触发就意味着该 block 被写入了,那么就将触发器往后移动到再下一个 block,当触发器被触发了一定的次数,我们就可以认为这个 IO 流是顺序的,当然如果触发器被触发后一定时间没有继续被触发,那么我们就可以回收该触发器。
3. 无感知热升级
第一代的 IO 加速技术设计上对热升级支持并不友好,更新存量版本时只能通过热迁移然后重启的方式,整个流程较为繁琐,所以在开发第二代 IO 加速技术时,我们设计了无感知热升级的方案。
由于我们的模块是位于内核态的 dm 层,一旦初始化后就会生成一个虚拟的 dm 块设备,该块设备又被上层文件系统引用,所以这个模块一旦初始化后就不能卸载了。为了解决这个问题,我们设计了父子模块的方式,父模块在子模块和 dm 层之间起到一个桥梁的作用,该父模块只有非常简单的 IO 转发功能,并不包含复杂的逻辑,因此可以确保父模块不需要进行升级,而子模块包含了复杂的业务逻辑,子模块可以从父模块中卸载来实现无感知热升级:
上图中的 binlogdev.ko 就是父模块,cachedev.ko 为子模块,当需要热升级时可以将 cache 盘设置为只读模式,这样 cache 盘只回刷数据不再写入,等 cache 盘回刷完成后,可以认为后续的写入 IO 可直接写入目标盘而不用担心覆盖 cache 盘中的数据,这样子模块就可以顺利拔出替换了。
这样的热升级机制不仅实现了热升级的功能,还提供了故障时的规避机制,在 IO 加速技术的灰度过程中,我们就发现有个偶现的 bug 会导致宿主机重启,我们第一时间将所有 cache 盘设置为只读,以避免故障的再次发生,并争取到了 debug 的时间。
4. 兼容 512e 机械磁盘
新一代的机械磁盘以 512e 为主,该类型的磁盘需要写入 IO 按照 4K 对齐的方式才能发挥最大性能,所以原先的 4K+512B 的索引格式已经无法使用,我们也考虑过把 512B 的索引扩大到 4K,但这样会导致索引占用空间过多,且写入时也会额外占用磁盘的带宽效率太低,所以最终通过将索引放到系统盘并结合上文提到的合并写入技术来解决该问题。
5. 性能扩展和提升
在第一代的 IO 加速技术中只能使用 1 块 cache 盘,当这块 cache 盘负载较高时就会影响系统的性能,在第二代 IO 加速中,我们设计了支持多块 cache 盘,并按照系统负载按需插入的方式,使加速随机 IO 的能力随着盘数量的提升而提升。在本地 cache 盘以及网络 cache 盘都采用 SATA 机械磁盘的条件下,测试发现,随着使用的 cache 盘数量的增多,随机写的性能也得到了大幅度的提升。
在只使用一块本地 cache 盘时,随机写性能可达 4.7W IOPS:
在使用一块本地 cache 盘加一块网络 cache 盘时,随机写性能可达 9W IOPS:
在使用一块本地 cache 盘加两块网络 cache 盘时,随机写性能可达 13.6W IOPS:
目前,我们已经大规模部署应用了第二代 IO 加速技术的云主机。得益于上述设计,之前备受困扰的 cache 盘 IO 堆积过多、性能瓶颈等问题得到了极大的缓解,特别是 IO 堆积的问题,在第一代方案下,负载较高时经常触发并导致性能损失和运维开销,采用第二代 IO 加速技术后,该监控告警只有偶现的几例触发。
五.写在最后
云主机 IO 加速技术极大提升了机械盘随机写的处理能力,使得用户可以利用较低的价格满足业务需求。且该技术的本质并不单单在于对机械盘加速,更是使系统层面具备了一种可以把性能和所处的存储介质进行分离的能力,使得 IO 的性能并不受限于其所存储的介质。此外,一项底层技术在实际生产环境中的大规模应用,其设计非常关键,特别是版本热升级、容错以及性能考虑等都需要仔细斟酌。希望本文可以帮助大家更好的理解底层技术的特点及应用,并在以后的设计中做出更好的改进。
— END —
即将于 12/21 举办的“ UCloud 用户大会暨 TIC 上海站”上,UCloud 将和参会者一起探讨诸如云主机 IO 加速这样的产品设计理念、技术细节及未来发展等话题。欢迎点击下方二维码或者点击阅读原文进行报名,期待您的光临!