在国内,很多朋友习惯用 Discord 收发消息。每一天,都有成百上千万用户通过 Discord 平台传递多达 40 亿条消息,而文本聊天其实只占其中一部分。服务器角色、自定义表情符号、视频通话等也都在一刻不停地往来流转,共同构成了全球用户高达数百 TB 的交互数据总量。
为了支持如此庞大的数据规模,我们运行有一组 NoSQL 数据库集群(采用 ScyllaDB),其中每个集群都作为相应数据集的真实来源。作为实时聊天平台,我们希望 Discord 的数据库能够尽快响应来自用户的高频率查询。
从截屏中可见,我们的数据库每秒处理约 200 万条请求
超越硬件极限
我们这套数据库面临的最大性能挑战,就是单一磁盘操作带来的延迟——也就是面向物理硬件执行数据读取/写入所耗费的时间。在数据库查询率低于一定水平时,磁盘延迟其实并不明显,毕竟我们的数据库在并行处理请求方面颇有心得(不会阻塞单个磁盘操作)。
但这种并行能力是有极限的,所以一旦查询率达到某个阈值,数据库就需要先等待上一条磁盘操作完成,之后才会发出下一条。再加上本身就需要一、两毫秒才能完成的磁盘操作,就导致数据库无法立即向传入的查询提供数据结果。于是乎,磁盘操作和查询就得排队等待,拖慢对查询客户端的响应速度,进而导致应用程序性能不佳。
在最极端的情况下,整个事态可能会级联出一个不断扩大的磁盘操作队列,最终使得磁盘查询超时。我们在自己的服务器上就曾经见到过这样的状况,数据库中的磁盘读取队列越来越长,查询操作也开始发生超时。
看到这里,细心的朋友可能发现了:磁盘操作要一、两毫秒才能完成?磁盘延迟不是应该以微秒为单位吗,这怎么直接提升了一个数量级?
Discord 的大部分硬件运行在谷歌云中,所以能够获得对“本地 SSD”的直接访问。所谓本地 SSD,就是基于 NVMe 的实例存储,确实可以提供令人印象深刻的超低延迟性能指标。
但遗憾的是,我们在测试中发现了一大堆可靠性问题,所以没法安心将关键数据存储在这套解决方案当中。于是我们重新开始思考,既然无法依赖超高速存储设备,那要怎么获得极低的延迟水平?
谷歌云还提供另外一种实例存储选项,也就是持久磁盘。这些磁盘可以一边运行,一边从服务器上附加/拆解,可以无需停机就调整容量大小,可以随时生成当前时间点快照,也可以按设计进行复制(防止单块磁盘故障导致数据丢失)。优点确实很多,但持久磁盘的短板就是并非直接接入服务器,而是通过网络就近(可能与服务器处于同一处数据中心内)连接。
虽然本地网络连接的延迟并不算高,但还是没法跟跨度在一米以内的 PCI 或 SATA 连接相比肩。所以,磁盘操作的平均延迟(从操作系统的角度来看)一般在几毫秒左右,而直连磁盘则可以控制在约 0.5 毫秒。
本地 SSD 还有其他问题。跟传统磁盘驱动器一样,一旦某款硬件(可能是 SSD 本身,也可能是 SSD 控制器)发生故障,则盘上的所有数据都会立即丢失。而且比传统磁盘更糟的是,如果本地 SSD 所接入的主机发生严重故障,那么盘上的数据将永久消失。因为无法为全盘创建时间点快照,所以 Discord 的某些工作流程(例如数据备份)在 SSD 上根本就不可行。正是由于这些功能缺失,所以 Discord 的几乎所有服务器配备的都是持久磁盘,而非本地 SSD。
问题评估
在理想状态下,我们当然应该用持久磁盘搭配本地 SSD,让它们各自发挥最佳属性,共同为数据库提供完美支持。但这样的完美搭配根本不存在,至少在常规云服务商的生态系统中完全找不到。选择延迟更低的直连 SSD,就意味着必须放弃持久磁盘的灵活性优势。
但如果我们愿意放弃其中一部分灵活性呢?比方说,写入延迟对我们的工作负载并不重要,因为读取延迟才是对应用程序性能影响最大的部分(消息发布平台上的大部分工作负载都属于读取密集型)。另外,在不停机的前提下,调整磁盘大小也没那么重要,我们完全可以认真预估容量需求,并提前配置更大的存储空间。
所以在考量了数据库运营中最关键的几项指标之后,我们缩小了对数据库解决方案的要求:
继续使用谷歌云(继续使用 Google Cloud Platform 的存储产品)
继续使用时间点快照进行数据备份
将低延迟 SSD 读取,设为高于其他一切指标的首要原则
不影响现有数据库的正常运行时间谷歌云提供多种存储选项,能够以各自不同的方式满足上述要求。所以如果能把两种存储方案组合成超级选项,那可就太方便了。因为我们对存储性能的核心诉求是低延迟读取,所以最好是能把读取操作交给谷歌云的本地 SSD(保证低延迟),而写入则继续指向持久磁盘(发挥快照、复制冗余等优势)。那么,有没有办法能在纯软件层面创建起这样的超级存储方案呢?
创建“超级盘”
我们前文描述的这套存储方案在本质上其实是个直写缓存,其中以本地 SSD 作为缓存、持久磁盘则作为存储层。我们的数据库服务器运行的是 Ubuntu,所以我们发现 Linux 内核提供 dm-cache、lvm-cache 和 bcache 等模块,能够以多种方式在盘这个层次上实现数据缓存。
但很遗憾,我们在缓存实验中发现了几个问题。首先就是如何处理缓存盘中的故障:一旦缓存中出现坏扇区,就会直接导致整个读取操作失败。本地 SSD 是 NVMe SSD 硬件之上的一个“薄”层,所以跟常规物理盘一样会受到坏扇区的影响。
我们当然可以用来自存储层的数据覆盖掉缓存上的扇区,借此实现坏扇区修复,但目前可选的缓存选项要么不提供这种功能、要么就是复杂度超出了当前阶段的承受范围。
总之,如果无法修复缓存中的坏扇区,这些扇区将直接暴露在执行调用的应用程序面前,我们的数据库则会出于安全原因而关闭:
storage_service - 由于 I/O 错误导致通信关闭,需要操作员干预
storage_service - 盘错误:std::system_error (error system:61, 无可用数据)
到这里,我们的需求被更新为:能够容忍本地 SSD 上存在坏扇区。为此,我们尝试了另外一种不同类型的 Linux 内核系统:md。
其实 md 的作用很简单,就是允许 Linux 创建软件 RAID 阵列,将多个盘转化成单一“阵列”(虚拟盘)。但我们无法简单将本地 SSD 和持久磁盘构建成简单的镜像(RAID1)阵列,因为这样会导致约一半的读取操作错误命中持久磁盘。这时候 md 的优势就来了,它提供一项传统 RAID 控制器所不具备的附加功能,即“write-mostly”。内核手册对这项功能做出了准确描述:
RAID1 中的单个设备可被标记为“write-mostly”。这些驱动器将被排除在正常的读取均衡之外,仅在没有其他选项时才会被读取。这项功能适用于通过慢速链路接入的设备。
就是它了,“通过慢速链路接入的设备”,这说的不就是持久磁盘吗?看起来 md 应该能帮我们完成打造“超级盘”的计划。只要构建一个包含本地 SSD 和 “write-mostly”持久磁盘的 RAID1 阵列,我们的所有要求就都能得到满足。
但现在还有最后一个问题:谷歌云中的本地 SSD 大小正好是 375 GB。对于某些应用程序,Discord 需要让单一数据库实例具备 1 TB 甚至更大的存储空间,所以 375 GB 明显有点不够看。我们当然可以把多个本地 SSD 接入服务器,但还需要找到办法把这堆小盘整合成统一的大盘。
好在 md 提供多种 RAID 配置,可以跨多个盘实现数据条带化。最简单的方式就是用 RAID0 将原始数据拆分到所有盘上,但这样如果某个盘损坏,整个阵列都会出现故障、导致全部数据丢失。更复杂的方法是用 RAID5、RAID6 保持奇偶校验,这样至少在单盘损坏时仍能保持数据完好,但代价就是性能下降。这是一种常见的正常运行时间保障方法,把损坏盘拆下、换块新盘即可。
但在谷歌云中,压根不存在替换本地 SSD 这个概念——毕竟这些设备都隐藏在谷歌数据中心的未知角落。另外,谷歌云还为本地 SSD 提供了一项神奇的“保障”服务:一旦有本地 SSD 发生故障,整个服务器将被迁移至另外一套数据集上,就相当于擦除了该服务器之前的所有本地 SSD 数据。因为没办法更换本地 SSD,也不想承担 RAID 条带造成的性能影响,所以我们最终选择用 RAID0 把多个本地 SSD 转化为单一低延迟虚拟磁盘。
我们用本地 SSD 建立 RAID0 阵列,再把持久磁盘跟 RAID0 阵列共同构建成 RAID1 阵列,这样就可以为数据库提供低延迟读取盘,同时仍然享受持久磁盘提供的种种灵活性优势。
数据库性能
从测试来看,这套新方案确实表现不错。那在实际生产中,它又能不能满足我们的需求呢?
结果没有令我们失望 — 在峰值负载时,我们的数据库再未出现过磁盘操作队列,而且查询延迟也不会波动。再结合实践指标,可以看到绝大部分数据库读取都发生在这套“超级盘”上,极少会跑去读持久磁盘,所以 I/O 操作的整体耗时得到显著降低。
凭借出色的性能提升,我们能够在同一批服务器上处理更多查询。这对我们这些数据库服务器维护人员和公司财务部门来说,都是个好消息。
总结
回想起来,我们真该在数据库部署之初就认真处理磁盘延迟问题。云计算世界中包含太多系统,所以同样的硬件在其中的运行方式往往跟我们熟悉的本地数据中心截然不同。
通过这次“超级盘”解决方案的研发和测试经历,我们总结出一系列实用的性能监控指标,让团队理解了存储设备的内部工作原理(包括在 Linux 和谷歌云环境下),同时也改善了我们的测试与验证架构变更文化。随着“超级盘”的生产实践,我们的数据库终于能随 Discord 用户规模的扩大而稳定拓展了。
但拥有 RAID 集群维护经验的朋友可能还抱有疑问:这样一套配置能稳定运行吗?毕竟云环境下充斥着无数系统,往往会以难以理解的方式引发故障。所以单靠 md 配置,恐怕不足以支撑起一套稳定的存储组合方案。说得没错,所以请大家期待本系列博文的第二部分,届时我们将具体介绍“超级盘”在云端遇到的极端案例,并分享我们的解决思路。
原文链接:
https://discord.com/blog/how-discord-supercharges-network-disks-for-extreme-low-latency
评论