Swift对象存储架构及关键技术浅析

Swift对象存储架构解析:去中心化设计、一致性哈希和最终一致性保障高可用和可扩展性。

原文标题:分布式系列之分布式存储Swift概览

原文作者:牧羊人的方向

冷月清谈:

Swift是OpenStack的开源对象存储服务,采用完全无中心的分布式架构,通过一致性哈希和数据冗余技术保障高可用性和可扩展性。

**数据模型:** Swift采用三层逻辑结构:Account(账户,类似租户)、Container(容器,类似目录)和Object(对象,包含数据和元数据)。

**系统架构:** Swift包含代理服务、认证服务、缓存服务、账户服务、容器服务、对象服务、复制服务、更新服务、审计服务和账户清理服务等组件。代理服务负责接收请求并转发到相应服务,其他服务分别负责账户、容器和对象的管理。

**关键技术:**
* **一致性哈希:** 通过计算将对象均匀分布到虚拟节点,再映射到物理设备,实现负载均衡和数据迁移最小化。
* **最终一致性:** Swift采用Quorum仲裁协议,通过设置副本数量和读写操作的副本数量来平衡一致性和可用性。默认配置下,每个对象有3个副本,写操作需要至少更新2个副本才算成功。
* **Ring:** Ring维护虚拟节点到物理设备的映射关系,确保数据均匀分布和副本隔离。它包含设备列表、分区分配列表和分区移位值等信息。

**存储结构:** Swift的存储目录结构包括accounts、containers、objects、async_pending、quarantined和tmp等目录,分别用于存储账户、容器、对象、异步待更新数据、隔离数据和临时数据。

怜星夜思:

1、Swift 的最终一致性模型在实际应用中可能会遇到哪些问题?如何 mitigating 这些问题?
2、相比于其他分布式存储系统(例如 Ceph、HDFS),Swift 的优势和劣势分别是什么?在什么场景下更适合使用 Swift?
3、Swift 的 Ring 机制是如何保证数据均匀分布和容错性的?如果 Ring 出现故障,会对 Swift 的运行造成什么影响?

原文内容

在“”中曾提到swift是完全无中心架构,采用一致性哈希算法获得数据的位置。本文简要总结swift存储的整体架构和关键技术,进一步了解不同分布式存储技术实现原理上的差异。

1、Swift整体架构

Swift是对象存储,用于永久类型的静态数据的长期存储,这些数据可以检索、调整,必要时进行更新。Swift通过在软件层面引入一致性哈希技术和数据冗余性,牺牲一定程度的数据一致性来达到高可用性和可伸缩性,支持多租户模式、容器和对象读写操作,适合解决非结构化数据存储问题。最适合存储的数据类型的例子是虚拟机镜像、图片存储、邮件存储和存档备份。

1.1 Swift数据模型

Swift是对象存储,采用层次数据模型,包括三层逻辑结构:Account、Container和Object。每层的节点数均没有限制,可以任意扩展。

  • Account:层次结构的顶层,Swift天生支持多租户,在OpenStack中account中可以理解为租户,用来做顶层的隔离机制。Swift租户的隔离性体现在metadata上,而不是object data上,数据包括元数据和container列表,保存在SQLite数据库中。

  • Container:容器,类似于文件系统中的目录,由用户自定义,它包含自身的元数据和容器内的对象列表,数据保存在 SQLite 数据库中。一个account中可以创建任意数量的container,两个不同container中的同名object表示不同的object。

  • Object:对象,包含数据和数据的元数据,以文件的形式保存在文件系统中。

1.2 Swift系统架构
Swift采用完全对称、面向资源的分布式系统架构设计,所有组件都可扩展,避免因单点失效而扩散并影响整个系统运转;通信方式采用非阻塞式I/O模式,提高了系统吞吐和响应能力。Swift组件如下图所示,包括:
  • 代理服务(Proxy Server):对外提供对象服务 API,会根据环的信息来查找服务地址并转发用户请求至相应的账户、容器或者对象服务;由于采用无状态的 REST 请求协议,可以进行横向扩展来均衡负载。
  • 认证服务(Authentication Server):验证访问用户的身份信息,并获得一个对象访问令牌(Token),在一定的时间内会一直有效;验证访问令牌的有效性并缓存下来直至过期时间。

  • 缓存服务(Cache Server):缓存的内容包括对象服务令牌,账户和容器的存在信息,但不会缓存对象本身的数据;缓存服务可采用 Memcached 集群,Swift 会使用一致性散列算法来分配缓存地址。

  • 账户服务(Account Server):提供账户元数据和统计信息,并维护所含容器列表的服务,每个账户的信息被存储在一个 SQLite 数据库中。

  • 容器服务(Container Server):提供容器元数据和统计信息,并维护所含对象列表的服务,每个容器的信息也存储在一个 SQLite 数据库中。

  • 对象服务(Object Server):提供对象元数据和内容服务,每个对象的内容会以文件的形式存储在文件系统中,元数据会作为文件属性来存储,建议采用支持扩展属性的 XFS 文件系统。

  • 复制服务(Replicator):会检测本地分区副本和远程副本是否一致,具体是通过对比散列文件和高级水印来完成,发现不一致时会采用推式(Push)更新远程副本,例如对象复制服务会使用远程文件拷贝工具 rsync 来同步;另外一个任务是确保被标记删除的对象从文件系统中移除。

  • 更新服务(Updater):当对象由于高负载的原因而无法立即更新时,任务将会被序列化到在本地文件系统中进行排队,以便服务恢复后进行异步更新;例如成功创建对象后容器服务器没有及时更新对象列表,这个时候容器的更新操作就会进入排队中,更新服务会在系统恢复正常后扫描队列并进行相应的更新处理。

  • 审计服务(Auditor):检查对象,容器和账户的完整性,如果发现比特级的错误,文件将被隔离,并复制其他的副本以覆盖本地损坏的副本;其他类型的错误会被记录到日志中。

  • 账户清理服务(Account Reaper):移除被标记为删除的账户,删除其所包含的所有容器和对象。

1.2.1 Proxy Server

Proxy Server可以说是Swift的核心,运行着swift-proxy-server进程。它提供Swift API的服务,负责Swift其余组件间的相互通信。对于每个客户端的请求,它在Ring中查询相应Account、Container以及Object的位置,并且转发这些请求。

  • 提供了Rest-full API,开发者可以通过这个接口快捷构建定制的客户端与Swift交互。

  • Proxy Server也会处理存储错误,当一个服务器无法对一个对象的PUT操作进行响应,它将从Ring中查询一个可以接手的服务器并将请求传递给它。

  • Ring是数据到物理设备映射的集合,通过Zone(区域)、Device(设备)、Partition(分区)和Replica(副本)来维护映射信息

1.2.2 存储节点
存储节点需要运行account和container、objectserver等服务
  • Object Server:对象服务是一个简单的二进制大对象存储服务,可以用来存储、检索和删除本地设备上的对象。在文件系统上,对象以二进制文件的形式存储,它的元数据存储在文件系统的扩展属性(xattrs)中。这要求用于对象服务的文件系统需要支持文件有扩展属性。

  • Container Server:容器服务的首要工作是处理对象的列表。容器服务并不知道对象存在哪,只知道指定容器里存的哪些对象。这些对象信息以sqlite数据库文件的形式存储,和对象一样在集群上做类似的备份。容器服务也做一些跟踪统计,比如对象的总数,容器的使用情况。

  • Account Server:账户服务与容器服务非常相似,不同的是负责处理容器的列表而不是对象。

1.3 关键技术
1.3.1 一致性哈希
Swift是基于一致性哈希技术,通过计算将对象均匀分布到虚拟空间的虚拟节点上,在增加或删除节点时可大大减少需移动的数据量;虚拟空间大小通常采用2的n次幂,便于进行高效的移位操作;然后通过独特的数据结构 Ring(环)再将虚拟节点映射到实际的物理存储设备上,完成寻址过程。衡量一致性哈希的4个指标:
  • 平衡性(Balance):平衡性是指Hash的结果能够尽可能分布均匀,充分利用所有缓存空间。

  • 单调性(Monotonicity):单调性是指如果已经有一些内容通过哈希分派到了相应的缓冲中,又有新的缓冲加入到系统中。哈希的结果应能够保证原有已分配的内容可以被映射到新的缓冲中去,而不会被映射到旧的缓冲集合中的其他缓冲区。

  • 分散性(Spread):分散性定义了分布式环境中,不同终端通过Hash过程将内容映射至缓存上时,因可见缓存不同,Hash结果不一致,相同的内容被映射至不同的缓冲区。

  • 负载(Load):负载是对分散性要求的另一个维度。既然不同的终端可以将相同的内容映射到不同的缓冲区中,那么对于一个特定的缓冲区而言,也可能被不同的用户映射为不同的内容。

Swift使用一致性HASH算法的主要目的是在改变集群的node数量时(增加/删除服务器),能够尽可能少地改变已存在key和node的映射关系,以满足单调性。在一致性HASH中引入了虚拟节点的概念,一个实际节点对应了若干个“虚拟节点”,“虚拟节点”在哈希空间中以哈希值排列。因此,在Swift中存在两种映射关系,对于一个文件,通过哈希算法(MD5)找到对应的虚节点(一对一的映射关系),虚节点再通过映射关系(ring文件中二维数组)找到对应的设备(多对多的映射关系),这样就完成了一个文件存储在设备上的映射。
1.3.2 数据一致性模型
根据CAP理论,无法同时满足一致性、可用性和分区容忍性,Swift放弃严格一致性(满足ACID事务级别),而采用最终一致性模型(Eventual Consistency),来达到高可用性和无限水平扩展能力。Swift采用Quorum仲裁协议,算法如下:
  • 定义:N表示数据的副本总数;W表示写操作被确认接受的副本数量;R表示读操作的副本数量。

  • 强一致性:R+W>N,以保证对副本的读写操作会产生交集,从而保证可以读取到最新版本;如果W=N,R=1,则需要全部更新, 适合大量读少量写操作场景下的强一致性;如果R=N,W=1,则只更新一个副本,通过读取全部副本来得到最新版本,适合大量写少量读场景下的强一致性。

  • 弱一致性:R+W<=N,如果读写操作的副本集合不产生交集,就可能会读到脏数据;适合对一致性要求比较低的场景。

Swift中默认配置是N=3,W=2>N/2,R=1或2,即每个对象会存在3个副本,这些副本会尽量被存储在不同区域的节点上;W=2表示至少需要更新2个副本才算写成功;当R=1时意味着某一个读操作成功便立刻返回,此种情况下可能会读取到旧版本(弱一致性模型);当R=2时,需要通过在读操作请求头中增加 x-newest=true 参数来同时读取2个副本的元数据信息,然后比较时间戳来确定哪个是最新版本(强一致性模型)。

注:如果数据出现了不一致,后台服务进程会在一定时间窗口内通过检测和复制协议来完成数据同步,从而保证达到最终一致性。

1.3.3 Ring
Ring将虚拟节点(partition)均衡的映射到一组物理设备上,其数据结构由以下信息组成:
  1. 存储设备列表(List of Devices),表示集群中设备的列表,在Ring类内部被称为devs,设备信息包括唯一标识号(id)、区域号(zone)、权重(weight)、IP 地址(ip)、端口(port)、设备名称(device)、元数据(meta);

  2. Partition Assignment List,用于存放每个replica与device间映射关系,在Ring类内部被称为分区到设备映射关系(replica2part2dev_id数组);

  3. Partition Shift Value,表示计算数据hash的移位量,计算分区号的位移(part_shift整数)

Swift为account、container和object分别定义了ring,其查找过程如下:

  • 使用对象的层次结构account/container/object作为键,使用MD5散列算法得到一个散列值

  • 对该散列值的前4个字节进行右移操作得到分区索引号,移动位数由上面的part_shift设置指定(一致性哈希映射到虚拟节点);

  • 按照分区索引号在分区到设备映射表(replica2part2dev_id)里查找该对象所在分区的对应的所有设备编号,这些设备会被尽量选择部署在不同区域(Zone)内

Swift中Ring使用zone来保证数据的物理隔离,每个partition的replica都确保放在不同的zone中,Ring中默认有3个replica。Zone只是一个抽象概念,它可以是一个磁盘(disk drive),一台服务器(server),一个机架(cabinet),一个交换机(switch),甚至是一个数据中心(datacenter),建议至少部署5个zone。

总的来说,Ring引入一致性哈希的原因是为了减少由于增加结点导致数据项移动的数量来提高单调性;引入partition的原因是为了减少由于节点数过少导致移动过多的数据项(数据负载不均衡);引入replica的原因是防止数据单点、提高冗余性;引入zone的原因是为了保证分区容忍性;引入weight的原因是为了保证partition分配的均衡。

1.4 Swift存储结构
Swift存储内容一般放在/srv/node/sdb1之类的路径下,其目录结构如下所示:accounts、async_pending、containers、objects、quarantined和tmp。其中accounts、containers、objects分别是账号、容器、对象的存储目录,async_pending是异步待更新目录,quarantined是隔离目录,tmp是临时目录。

  • objects:在objects目录下存放的是各个partition目录,其中每个partition目录是由若干个suffix_path名的目录和一个hashes.pkl文件组成,suffix_path目录下是由object的hash_path名构成的目录,在hash_path目录下存放了关于object的数据和元数据;object的数据存放在后缀为.data的文件中,它的metadata存放在以后缀为.meta的文件中,将被删除的Object以一个0字节后缀为.ts的文件存放。

  • accounts:在accounts目录下存放的是各个partition,而每个partition目录是由若干个suffix_path目录组成,suffix_path目录下是由account的hsh名构成的目录,在hsh目录下存放了关于account的sqlite db;在account的db文件中,包含了account_stat、container、incoming_sync 、outgoing_sync 4张表;其中,表account_stat是记录关于account的信息,如名称、创建时间、container数统计等等;表container记录关于container的信息;表incoming_sync记录到来的同步数据项;表outgoing_sync表示推送出的同步数据项。

  • containers:containers目录结构和生成过程与accounts类似,containers的db中共有5张表,其中incoming_sync和outgoing_sync的schema与accounts中的相同。其他3张表分别为container_stat、object、sqlite_sequence;表container_stat与表account_stat相似,其区别是container_stat存放的是关于container信息。

  • tmp:tmp目录作为account/container/object server向partition目录内写入数据前的临时目录。例如,client向server上传某一文件,object server调用DiskFile类的mkstemp方法创建在路径为path/device/tmp的目录。在数据上传完成之后,再调用put()方法,将数据移动到相应路径。

  • async_pending:async_pending存放未能及时更新而被加入更新队列的数据。本地server在与remote server建立HTTP连接或者发送数据时超时导致更新失败时,将把文件放入async_pending目录。这种情况经常发生在系统故障或者是高负荷的情况下。如果更新失败,本次更新被加入队列,然后由Updater继续处理这些失败的更新工作;account与container的db和object两者的pending文件处理方式有所不同:db的pending文件在更新完其中的一项数据之后,删除pending文件中的相应的数据项,而object的数据在更新完成之后,移动pending文件到目标目录。

  • quarantined:quarantined路径用于隔离发生损坏的数据。Auditor进程会在本地服务器上每隔一段时间就扫描一次磁盘来检测account、container、object的完整性。一旦发现不完整的数据,该文件就会被隔离,该目录就称为quarantined目录。为了限制Auditor消耗过多的系统资源,其默认扫描间隔是30秒,每秒最大的扫描文件数为20,最高速率为10Mb/s。account和container的Auditor的扫描间隔比object要长得多。

2、Swift all in one安装部署

单节点部署swift环境,感兴趣的可以参考“https://docs.openstack.org/swift/latest/development_saio.html”

参考资料:

  1. https://blog.csdn.net/sinat_27186785/article/details/51921458

  2. https://docs.openstack.org/swift/latest/development_saio.html

  3. https://www.cnblogs.com/Skybiubiu/p/14614288.html

  4. https://blog.51cto.com/u_15352876/3773632

  5. https://blog.csdn.net/sinat_27186785/article/details/51930074

最终一致性带来的问题其实挺多的,比如读写不一致,可能读到旧数据,还有就是冲突解决的复杂性,像前面提到的版本控制,实现起来并不容易。而且,最终一致性模型下,调试和排查问题也比较困难,因为数据的状态不是实时的。所以,选择最终一致性还是要根据具体的应用场景来权衡利弊。

拿 Swift 和 Ceph 比,Swift 的优势是简单易用,扩展性好,适合大规模存储;Ceph 功能更强大,支持块存储、对象存储和文件系统,一致性也更好,但部署和维护相对复杂。HDFS 主要是为大数据分析设计的,适合高吞吐量的顺序读写,和 Swift 的应用场景不太一样。

关于Ring机制,补充一点,Ring 的一致性哈希算法能够有效地减少数据迁移,即使有节点加入或退出,也只会有少量的数据需要重新分布。至于 Ring 故障的影响,那可是灾难性的,会导致整个 Swift 服务不可用,所以 Ring 的可靠性非常重要。

关于“相比于其他分布式存储系统(例如 Ceph、HDFS),Swift 的优势和劣势分别是什么?在什么场景下更适合使用 Swift?”这个问题,我觉得 Swift 更适合那些对数据一致性要求不高,但对扩展性和可用性要求很高的场景,例如云存储、在线图片服务、大文件存储等。

针对“Swift 的 Ring 机制是如何保证数据均匀分布和容错性的?如果 Ring 出现故障,会对 Swift 的运行造成什么影响?”,我想说,为了防止 Ring 出现单点故障,通常会部署多个 Ring 副本,并且有相应的机制来保证 Ring 副本之间的数据同步。如果一个 Ring 副本故障,其他副本可以接管服务,从而避免整个系统瘫痪。

说到最终一致性,最大的问题就是数据冲突了。比如,两个用户同时修改同一个对象,最终哪个版本会被保留?Swift的解决方法是比较时间戳,保留最新的版本。但如果两个修改操作发生的时间很接近,就可能会出现数据丢失的情况。为了 mitigating 这个问题,可以考虑在应用层实现版本控制机制,或者使用更强的一致性模型。

Ring 通过虚拟节点和分区机制将数据均匀分布到不同的物理设备上,并且每个分区有多个副本存储在不同的 Zone 中,从而保证了数据的容错性。如果 Ring 出现故障,会导致 Swift 无法找到数据的位置,从而影响数据的读写操作。

Swift 的优势在于其架构简单,易于部署和维护,而且扩展性很好。它比较适合存储大量的非结构化数据,例如图片、视频、备份文件等。劣势在于一致性模型较弱,不适合对数据一致性要求很高的应用场景。

针对“Swift 的最终一致性模型在实际应用中可能会遇到哪些问题?如何 mitigating 这些问题?”这个问题,除了前面提到的版本控制,还可以考虑使用一些分布式锁机制或者事务机制来保证数据一致性,当然,这些机制会牺牲一定的性能和可用性。最终还是要看具体的业务需求和系统性能要求来选择合适的方案。