如何构建“正确的”云平台存储

做者:ZStack 王为算法

从 2015 年到如今,ZStack 有一条宗旨一直没有变过,就是向客户交付稳定、可靠、高性能的云平台,这条宗旨在前几年让咱们一直聚焦云平台自己,包括虚拟化、云网络、云编排、存储管理等等这些功能。数据库

在这里面最让咱们头痛的,即便不是第一也能进前三的存在,就是存储管理编程

考虑到存储对业务的无比的重要性,以及咱们做为一家创业公司的支持能力,咱们一开始一直是基于一些开源的存储方案对客户提供服务:缓存

  1. XFS,做为 RHEL 默认的本地文件系统,咱们本来一直对 XFS 是比较信任的,但实际上 XFS 在使用过程当中问题多多,咱们帮客户绕过了不少坑,也在考虑别的替代方案;
  2. NFS,NFS 是一个对云平台很简单的方案,由于它屏蔽了不少存储的复杂性,用文件系统的方式提供了共享存储,使得咱们能够用相似本地文件系统的管理方式管理共享存储,既简单又支持热迁移等高级功能,看似完美,但实际上 NFS 几乎是咱们最不推荐的生产用存储方案之一,细节将在后面讨论;
  3. OCFS2,当用户只有 SAN 存储,也没法提供 NFS 接口时,咱们的选择并很少,此时 Oracle 的 OCFS2 成为一个值得青睐的方案,其优势是在小规模使用时基本上很稳定,部署后也可使用文件系统的方式使用,但在性能、大规模的扩展性和部分功能(例如文件锁)上支持也并不完美;
  4. Ceph,基于 Ceph 能够提供很棒的存储方案,但 Ceph 相对复杂的部署运维对部分客户仍是比较难接受,特别是在私有云中,不少客户习惯了 SAN 存储带来的性能和安全感,对他们来讲也没有超大容量的需求或者随时须要灵活扩容,反而大厂商带来的安全感,或者可以将以前用在VMware 上的 SAN 存储继续用起来才是最重要的。

综合考虑前面的各类存储,NFS、OCFS2 的不完美促使咱们提供一个可以管理共享存储的存储方案,这个方案要能达到下面的要求:安全

  1. 部署速度要足够快,ZStack 的部署速度一贯是业界前列,咱们的标准一直是对于 Linux 有基本理解的人可以在 30 分钟内完成部署,这个时间是包括部署主存储、镜像仓库的时间的。
  2. 可以扩展到足够大的规模,根据 SAN 存储的性能,单个集群应该能够接管几十到上百的服务器(由于通常来讲单个 SAN 存储能支撑的服务器数量有限)。
  3. 性能可以完整发挥 SAN 存储的性能,IO 模式可以发挥 SAN 存储的 cache 性能,对于 OCFS2 咱们能够经过调整 block size 来优化 OCFS2 性能,但若是在分层 SAN 存储上测试就会发现因为大 block size 带来的 IO pattern 变化,若是测试 4k 小文件随机写,性能并不稳定,没法像直接在物理机上对 LUN 测试前期所有写到高速盘上,带来了测试数据的不理想。
  4. 高稳定性,与互联网、公有云业务不一样,私有云均部署在客户机房,甚至是一些隔离、保密机房,这意味着咱们没法像互联网环境同样执行“反复试错”的策略,咱们没法控制用户的升级节奏,没法时刻监控运维存储状态,也没法再客户环境进行灰度测试、镜像验证。

最终,在2018 年咱们决定本身开发一个面向共享块存储的存储方法,命名很直接就叫 SharedBlock。整个方案是这样的:服务器

  1. 基于块设备,直接基于块设备向虚拟机提供虚拟云盘,经过避免文件系统开销能够明显提高性能和稳定性;
  2. 在块设备上基于 Paxos 实现分布式锁来管理块设备的分配和节点的加入、心跳、IO 状态检查;
  3. 经过 Qemu 的接口实现对用户磁盘读写情况进行监控;

SharedBlock 在推出后,应用在了不少的生产客户上,特别是能够利旧 SAN 存储特色让 SharedBlock 快速部署在大量以往使用虚拟化的客户上。网络

后来随着 5G 和物联网、云端互联的发展,让市场迫切须要一个价格不高、能够简便部署、软硬一体的超融合产品,所以咱们就在考虑一个两节点一体机的产品,经过和硬件厂商合做设计,能够实现 2U 的一体机包含足够用户使用的硬盘、独立的模块和双电冗余,咱们但愿能经过这个产品将客户的本来单节点运行的应用平滑升级到两节点备份,让客户的运行在轨道站点、制造业工厂这些“端”应用既享受到云的便利,又不须要复杂的运维和部署。这就是咱们的 Mini Storagesession

在开发这些存储产品的过程当中,咱们踩了无数的坑,也收获了不少经验。架构

下面先说说将存储作正确有多难,在今年说这个话题有一个热点事件是避不开的,就是今年的 FOSDEM 19' 上 PostgreSQL 的开发者在会上介绍了 PostgreSQL 开发者发现本身使用 fsync() 调用存在一个十年的 bug——并发

  1. PG 使用 writeback 机制,特别是在过去使用机械硬盘的时代,这样能够大大提升速度,但这就须要定时 fsync 来确保把数据刷到磁盘;
  2. PG 使用了一个单独线程来执行 fsync(),指望当写入错误时可以返回错误;
  3. 但其实操做系统可能本身会将脏页同步到磁盘,或者可能别的程序调用 fsync();
  4. 不管上面的哪一种状况,PG 本身的同步线程在 fsync 时都没法收到错误信息;

这样 PG 可能误觉得数据已经同步而移动了 journal 的指针,实际上数据并无同步到磁盘,若是磁盘持续没有修复且忽然丢失内存数据就会存在数据丢失的状况。

在这场 session 上 PG 的开发者吐槽了 kernel 开发以及存储开发里的不少问题,不少时候 PG 只是想更好地实现数据库,但却发现常常要为 SAN/NFS 这些存储操心,还要为内核的未文档的行为买单。

这里说到 NFS,不得很少提两句,在 Google 上搜索 "nfs bug" 能够看到五百万个结果,其中不乏 Gitlab 之类的知名厂商踩坑,也不乏 Redhat 之类的操做系统尝试提供遇到 NFS 问题的建议:

从咱们一个云厂商的角度看来,虚拟机存储使用 NFS 遇到的问题包括但不限于这几个:

  1. 部分客户的存储不支持 NFS 4.0 带来一系列性能问题和并发问题,并且 4.0 以前不支持 locking;
  2. nfs 服务自己会带来安全漏洞;
  3. 对于在 server 上作一些操做(例如 unshare)带来的神秘行为;
  4. 使用 async 挂载可能会带来一些不一致问题,在虚拟化这种 IO 栈嵌套多层的环境可能会放大这一问题,而使用 sync 挂载会有明显的性能损失;
  5. NFS 自己的 bug

最终咱们的建议就是生产环境、较大的集群的状况下,最起码,少用 NFS 4.0 之前的版本……

另外一个出名的文章是发表在 14 年 OSDI 的这篇 All File Systems Are Not Created Equal,做者测试了数个文件系统和文件应用,在大量系统中找到了不乏丢数据的 Bug, 在此以后诸如 FSE'16 的 Crash consistency validation made easy 又找到了 gmake、atom 等软件的各类丢数据或致使结果不正确的问题:

上面咱们举了不少软件、文件系统的例子,这些都是一些单点问题或者局部问题,若是放在云平台的存储系统上的话,复杂度就会更高:

1. 首先,私有云面临的是一个离散碎片的环境,咱们都知道 Android 开发者每每有比 iOS 开发者有更高的适配成本,这个和私有云是相似的,由于客户有:

1)不一样厂商的设备

2)不一样的多路径软件

3)不一样的服务器硬件、HBA 卡;

虽然 SCSI 指令是通用的,但实际上对 IO 出错、路径切换、缓存使用这些问题上,不一样的存储+多路径+HBA 能够组成不一样的行为,是最容易出现难以调试的问题地方,例若有的存储配合特定 HBA 就会产生下面的 IO 曲线:

2. 因为咱们是产品化的私有云,产品化就意味着整套系统不多是托管运维,也不会提供驻场运维,这样就会明显受客户良莠不齐的运维环境和运维水平限制:

1)升级条件不一样,有的用户但愿一旦部署完就不再要升级不要动了,这就要求咱们发布的版本必定要是稳定可靠的,由于发出去可能就没有升级的机会了,这点和互联网场景有明显的区别;

2)联网条件不一样,通常来讲,来自生产环境的数据和日志是相当重要的,但对产品化的厂商来讲,这些数据倒是弥足珍贵,由于有的客户机房不只不容许链接外网,甚至咱们的客户工程师进机房的时候手机也不容许携带;

3)运维水平不一样,对于一个平台系统,若是运维水平不一样,那么能发挥的做用也是不一样的,好比一样是硬件故障,对于运维水平高的客户团队可能很快可以确认问题并找硬件厂商解决,而有的客户就须要咱们先帮忙定位分析问题甚至帮助和硬件厂商交涉,就须要消耗咱们不少精力。

3. 漫长的存储路径,对于平台来讲,咱们不只要操心 IO 路径——Device Mapper、多路径、SCSI、HBA 这些,还要操心虚拟化的部分——virtio 驱动、virtio-scsi、qcow2…… 还要操心存储的控制平面——快照、热迁移、存储迁移、备份…… 不少存储的正确性验证只涉及选举、IO 这部分,而对存储管理并无作足够的关注,而根据咱们的经验,控制面板一旦有 Bug,破坏力可能比数据面更大。

说了这么多难处,咱们来讲说怎么解决。提到存储的正确性,接触过度布式系统的同窗可能会说 TLA+,咱们先对不熟悉 TLA+ 的同窗简单介绍下 TLA+。

2002 Lamport 写了一本书《Specifying Systems》基本上算是 TLA+ 比较正式的第一本书,了解的朋友可能知道在此以前 Lamport 在分布式系统和计算结科学就很出名了——LaTex、Lamport clock、PAXOS 等等,TLA+ 刚开始的时候没有特别受重视,他的出名是来自 AWS 15 年发表在 ACM 会刊的《How Amazon Web Services Uses Formal Methods》。

从本质上讲,形式化验证并非新东西,大概在上世纪就有了相关的概念,TLA+ 的优点在于它特别适合验证分布式系统的算法设计。由于对于一个可验证的算法来讲,核心是将系统时刻的状态肯定化,并肯定状态变化的条件和结果,这样 TLA+ 能够经过穷举+剪枝检查当有并发操做时会不会有违反要求(TLA+ 称之为 invariant)的地方——例如帐户余额小于 0,系统中存在了多个 leader 等等。

看最近的几场 TLA Community Meeting,能够看到 Elasticserach、MongoDB 都有应用。

那么既然这个东西这么好,为何在国内开发界彷佛并无特别流行呢?咱们在内部也尝试应用了一段时间,在 Mini Storage 上作了一些验证,感受若是 TLA+ 想应用更普遍的话,可能仍是有几个问题须要优化:

  1. 状态爆炸,由于 TLA+ 的验证方式决定了状态数量要通过精心的抽象和仔细的检查,若是一味地增长状态就可能遇到状态爆炸的问题;
  2. TLA+ Spec 是没法直接转换成代码的,反过来,代码也没法直接转换成 Spec。那么换句话说,不管是从代码到 Spec 仍是从 Spec 到代码都有出错的可能,轻则有 Bug,重则可能致使你信心满满的算法其实与你的实现根本不一样;
  3. 外部依赖的正确性,这一点可能有点要求太高,但却也是可靠系统的重要部分,由于用户是无论产品里是否用到了开源组件,不管是 qemu 的问题仍是 Linux 内核的问题,客户只会认为是你的问题,而咱们不太可能分析验证每一个依赖。

固然了,涉及到算法的正确性证实,形式化证实依然是不可替代的,但不得不说目前阶段在云平台存储上应用,还没作到所有覆盖,固然了咱们也看到 TLA+ 也在不断进步——

  1. 可视化
  2. 加强可读性
  3. Spec 的可执行

这里特别是第三点,若是咱们的 Spec 可以被转换成代码,那么咱们就能够将核心代码的算法部分抽象出来,作成一个单独的库,直接使用被 Spec 证实过的代码。

分布式系统的测试和验证,这几年还有一个很热门的词汇,就是混沌工程

混沌工程对大多数人来讲并非一个新鲜词汇,能够说它是在单机应用转向集群应用,面向系统编程转向到面向服务编程的必然结果,咱们已经看到不少互联网应用声称在混沌工程的帮助下提升了系统的稳定性如何如何,那么对于基础架构软件呢?

在必定程度上能够说 ZStack 很早就开始在用混沌工程的思想测试系统的稳定性,首先咱们有三个关键性的外部总体测试

  1. MTBF,这个概念通常见于硬件设备,指的是系统的正常运行的时间,对咱们来讲会在系统上根据用户场景反复操做存储(建立、删除虚拟机,建立、删除快照,写入、删除数据等)在此之上引入故障检查正确性;
  2. DPMO,这个是一个测试界很老的概念,偏向于单个操做的反复操做,例如重启 1000 次物理机,添加删除 10000 次镜像等等,在这之上再考虑同时引入故障来考察功能的正确性;
  3. Woodpecker,这是 ZStack 从最开始就实现的测试框架,代码和原理都是开源的,它会智能的组合 ZStack 的上千个 API自动找到能够持续下去的一条路径,根据资源当前的状态判断资源能够执行的 API,这样一天下来能够组合执行数万次乃至上百万次,与此同时再考虑引入错误。

上面这些方法,在大量调用 API、测试 IO 以外,很重要的一点就是注入错误,例如强制关闭虚拟机、物理机,经过可编程 PDU 模拟断电等等,可是这些方法有一些缺陷:

  1. 复杂场景的模拟能力有限,例若有些客户存储并非一直 IO 很慢,而是呈现波峰波谷的波浪型,这种状况和 IO 始终有明显 delay 是有比较大的区别的;
  2. 不够灵活,例若有的客户存储随机 IO 不好但顺序 IO 性能却还能够,也不是简单的下降 IO 性能就能够模拟的。

总之大部分混沌工程所提供的手段(随机关闭节点、随机杀进程、经过 tc 增长延时和 iproute二、iptables 改变网络等等)并不能知足 ZStack 的彻底模拟用户场景的需求。

在这种状况下,咱们将扩展手段放在了几个方向上:

  1. libfiu,libfiu 能够经过 LD_PRELOAD 来控制应用调用 POSIX API 的结果,可让应用申请内存失败、打开文件失败,或者执行 open 失败。

     使用 fiurun + fiuctl 能够对某个应用在须要的时刻控制系统调用。

fiu 对注入 libaio 没有直接提供支持,但好在 fio 扩展和编译都极为简单,所以咱们能够轻松的根据本身的需求增长 module。

2. systemtap,systemtap 是系统界的经典利器了,能够对内核函数的返回值根据需求进行修改,对内核理解很清晰的话,systemtap 会很好用,若是是对存储进行错误注入,能够重点搜 scsi 相关的函数,以及参考这里:Kernel Fault injection framework using SystemTap

3. device-mapper,device-mapper 提供了 dm-flakey、dm-dust、dm-delay,固然你也能够写本身的 target,而后能够搭配 lio 等工具就能够模拟一个 faulty 的共享存储,得益于 device-mapper 的动态加载,咱们能够动态的修改 target 和参数,从而更真实的模拟用户场景下的状态;

4. nbd,nbd 的 plugin 机制很是便捷,咱们能够利用这一点来修改每一个 IO 的行为,从而实现出一些特殊的 IO pattern,举例来讲,咱们就用 nbd 模拟过用户的顺序写很快但随机写异常慢的存储设备;

5. 此外,还有 scsi_debug 等 debug 工具,但这些比较面向特定问题,就不细说了。

上面两张图对这些错误注入手段作了一些总结,从系统角度来看,若是咱们在设计阶段可以验证算法的正确性,在开发时注意开发可测试的代码,经过海量测试和错误注入将路径完整覆盖,对遇到的各类 IO 异常经过测试 case 固化下来,咱们的存储系统必定会是愈来愈稳定,持续的走在“正确”的道路上的。

相关文章
相关标签/搜索