这篇文章来浅谈一下分布式存储系统的大体架构。我也是个刚进入存储领域不久的新人,斗胆根据自己的理解谈谈对分布式存储系统架构的看法。
我觉得目前的分布式存储系统架构大致可以分为三个模块:接入模块、IO 模块、集群管理模块。
接入模块
接入模块负责管理存储系统与上层应用的对接。上层应用对接存储系统的方式有很多,单单存储协议就种类繁多,如常见的 iSCSI/NFS/FC 等,更别说各家存储厂商自己开发的各种对接方案了。所以分布式存储系统的接入模块是很重要的,在整个 IO 路径中起码占用了一跳(在存储的架构里面如果 IO 路径越短则时延会相对越小,带来的收益也是越好的)。
举两个场景:K8s 和 OpenStack。在 K8s 场景中,容器使用存储的方式主要是 PVC/PV 。存储厂商可以通过 k8s 的 CSI 接口对接自身的存储系统,如 AWS 的 Elastic Block Store(EBS)等。容器对接存储主要通过读写容器内部的文件或者块设备,所以第三方厂商只要能暴露出 block device 或者通过 FUSE 接口 mount 成宿主机的路径就可以对接容器。常见的 CSI 对接有块存储的 iSCSI、对象存储的 FUSE,还有一些其他比较小众的,比如 ceph 的 rbd、sheepdog 的 sbd 等,这些的原理就是通过注册内核相关的 block device 类型来做的。
在 OpenStack 的场景中,qemu 模拟出了完整的虚拟机,相对容器来说隔离程度更高,可自由扩展的空间也就越大,关键是它的设备可以热插拔,容器是不可以的(因为容器受到 mnt namespace 的限制,它的所有设备在启动之前都需要在 mnt namespace 进行注册,要让容器支持设备的热插拔也是可以的,这就需要魔改 docker 了)。在虚拟机中可以通过任何方式对接存储,比如 TCP、libvirt 等。在 OpenStack 中管理存储的方式主要是通过 cinder 和 swift 两大组件,cinder 负责管理块存储,swift 负责管理对象存储。
存储的接入模块包括协议层接入、Client 模块。协议层就是上面所说的 iSCSI、NFS 等协议,而 Client 模块则负责将上层请求转换成存储系统所能支持的读写请求发送到存储的核心 IO 模块,这些要做的以及可以做东西很多:故障域管理、IO 转发、IO 超时重试、IO 条带化、集群节点视图等,每一个点都很有搞头。接入模块的改造潜力是比较大的,如利用 SPDK 改造 vhost / iSCSI ,单卷 4k 随机写 IOPS 轻轻松松上 300k,fio 128 队列深度 4k 随机写轻松上 80k。
接入模块性能受限的因素主要有 CPU、网络。CPU 一般能提供给存储用的核数并不多,当然专门的存储服务器除外。网络也比较受限,在计算节点上要跟其他应用分一杯羹,当然也可以拆分成两种网络平面,存储单独走一个网络平面,但是目前主流网卡还只是万兆。
IO 模块
存储的核心 IO 模块是一个比较单纯的模块,它主要负责将传递过来的 IO 写入磁盘。IO 数据有两种:元数据和业务数据。元数据的管理比较多见于集中式存储系统,即在存储系统中节点角色有主从之分。主节点负责管理元数据和 IO 调度,从节点则负责读写数据,乖乖的干活即可。在没有主从之分的 peer 存储系统中,可管理元数据并不多,并且多集中在接入模块的 Client 中,因为它的元数据多是计算出来的,而集中式存储系统多是查出来的,所以存储系统按照这种方式可以分为计算型和查表型。集中式存储系统的元数据多存于类似 LevelDB 或 RocksDB 的单机存储引擎中。
按照数据的修改方式又可以将存储系统分为 append 追加型和覆盖型。append 追加型为当一个写请求进来后,它不会这个新数据在原来的位置进行修改,而是直接写到一个新的地方,原来的数据打个标记后再走其他方式进行清理。直接读写型就相反,它在原来的基础上直接读写,根据请求的偏移量和数据大小将数据直接覆盖。
在 IO 模块里面还需要保证数据的完整一致性,这个可以通过 journal / oplog 搞定。每家存储的方式都不一样,也没有好坏之分,适合系统的就是最好的。分布式系统大多支持多副本和纠删码,这两种方式就是典型的空间换时间和时间换空间,当然两者一起用也是可以的,在计算可用性又可以多几个 9 。
数据恢复是 IO 模块的一个难题。在查表式系统中,这个恢复动作并不难,但是在计算式的系统中就是个麻烦事,特别是节点宕机恢复中又有节点宕机恢复这种场景,如果处理不好数据很有可能在恢复的过程中就丢失了,因为集群的视图在这种恢复中又开始恢复的场景是不太好处理的,这个场景的解决过程有点类似于 raft 的新旧集群选主,需要有个中间态来 cover。
集群管理模块
既然是分布式存储系统,那必定需要有个集群管理模块,它负责管理节点的新增、删除、网络异常等情况,集群的视图主要通过这个模块进行操作。在集中式类型的系统中它还可以用来进行选主。
在这个模块中比较常用的组件有 zookeeper 和 etcd,这两个都有 watch 机制,但 etcd 毕竟是后来者,它的机制就比较成熟。这两个组件分别基于不同的共识协议:zab 和 raft,我个人比较喜欢 raft,清晰易懂,而且也由于在工作中经常碰到 zookeeper 的坑,让我更倾向于 raft 了,当然这不是引战,只是我的个人体会罢了。
好了,才疏学浅斗胆浅谈一下就行。