前言
云计算架构作为一种基础设施服务几乎已经成为了现代Web开发的标配,它显著地降低了 DevOps(Development and Operations)的门槛,通过云服务商提供的管理平台,Web应用开发人员可以轻松地在云上搭建自己的系统。作为一名云时代的全栈开发工程师,了解云计算架构背后的机制可以让我们更好地利用这个强大的基础设施,在设计、开发、部署和维护应用程序时做出更明智的决策。同时云计算是互联网技术的延伸,通过学习云计算架构,可以更深入地掌握互联网基础知识,这对于个人的技术水平也是一个很大的提升。
为了对云计算有一个全面的了解,最近在读 《图解云计算架构——基础设施和API》 一书,这本书主要以OpenStack和AWS为例,通过API来重点讲解IaaS云服务的本质。下面是边读边整理的学习笔记,一方面可以随时回顾,另一方面希望通过输出来促进自己思考。
正文
对象存储是云环境中常用的存储资源组件,云原生架构正是以分散部署的对象存储为基础数据存储区的系统架构。OpenStack的Swift和AWS中的S3都属于对象存储组件。本章详细介绍对象存储的内部机制。
1. 对象存储
存储类型
存储类型可分为三类:
- 块存储:块存储中的数据是一个个的区块(block),需要借助操作系统中的文件系统才能将其识别成文件。块存储会被服务器识别为设备(device),主要用于本次磁盘和数据库。在联机处理过程中访问的大部分是块存储。
- 网络存储:特点是服务器需要通过TCP/IP网络连接到存储,使用的典型协议是NFS,使用网络存储时需要在操作系统上挂载NFS服务,并借助操作系统上的文件系统才能将数据识别为文件。
- 对象存储:以文件为单位管理数据,使用HTTP(HTTPS)协议访问。它具备与文件系统相当的功能,且大多数情况下不需要挂载到服务器的操作系统上,而是作为独立组件使用。在云计算架构中,对象存储的应用场景非常广泛。
对象存储的内部结构及最佳实践
对象存储的特点:
- 使用HTTP(HTTPS)协议访问文件:内置了HTTP服务器、文件系统、挂载存储设备等功能,因此用户可以直接以网站的形式发布文件
- 可以使用API直接操作文件:不依赖于操作系统命令
- 文件会被复制到多个地方:可以方便地支持备份和灾难恢复
- 不需要测算总容量:适合存放数据量难以预估的大量数据
基于以上特点,对象存储适合于发布静态网站或大容量的视频文件,还可以存放大数据分析专用的大量日志信息。另一方面,对象存储不具备稳定的高性能I/O和带锁的数据一致性,所以在这些场景中人们还是使用块存储。在云环境中需要根据应用场景选用合适的存储方案。
2. 对象存储基本操作相关的API
构成对象存储的资源
对象存储的核心在于“如何管理文件”,它包含的资源只有以下三种:
- 账户:即租户
- 存储桶:相当于文件系统中的顶层目录,在OpenStack中称为容器。存储桶的名称在OpenStack中只需在账户内唯一,在AWS S3中存储桶的名称会以FQDN的形式在网络上公开,必须要是全局唯一。
- 对象:存储桶中的文件。关于路径:我们无法在存储桶中创建存储桶,但是可以在存储对象时添加前缀,从而在存储桶内形成层级结构。
账户、存储桶和对象的关系:对象存储采用账户/存储桶/对象的一对多的树状结构。
获取存储桶列表
- OpenStack Swift:向账户发送GET请求可获取存储桶列表
- AWS S3:使用ListBucket API即可获取账户内所有存储列表
创建存储桶和存储对象
在OpenStack中创建存储桶和存储对象的CLI命令和使用的内部API:
- 创建存储桶:
swift post 存储桶名,这个命令会向账户发送PUT请求,这样可以保证名称的唯一性。(为什么不用POST请求:对象存储资源采用的是以账户为根的树状结构,无论是新建还是更新操作,都一律相当于在根位置的账户下进行的更新操作,从而大幅度简化API) - 上传对象:
swift upload 存储桶名 文件名,这个命令会向目标对象URI发送PUT请求,可以创建或更新对象。命名:默认使用的文件名是本地文件名,也可以为对象另起一个名字。
修改存储桶和对象的配置信息
存储桶和配置信息共享一个URI,修改配置信息时通过HTTP扩展消息头来传递要修改的配置信息(元数据)。
- 新建/修改配置信息:向存储桶或对象URI发送POST请求,在扩展消息头字段中携带新的配置信息
- 删除配置信息:向存储桶或对象URI发送POST请求,并且在扩展消息头字段中携带空白的配置信息以覆盖原有信息,达到删除的效果。(如果调用DELETE请求那么删除的是资源本身)
分段上传
对象存储限定了用户能够操作的文件大小:单个文件越大,上传时的系统开销就越大,为了提升吞吐量,就需要将大文件切割成小文件再并行处理。
分段上传功能的步骤:
- 将对象分割成片段
- 上传片段
- 拼接片段,还原出原始对象
分段上传的API:
- 在OpenStack Swift中只需要在PUT上传文件时指定multipart- manifest的查询参数即可
- 在AWS S3为每个步骤提供了API
3. 变更对象存储的配置与相关API
本节介绍几个典型的对象存储配置功能。
-
启用ACL: ACL(Access Control List,访问控制列表)配置是一种用于控制网络访问权限的技术手段。我们可以将用户放入到对象存储预定义的组中,然后分别为这些组赋予读写权限。
-
版本控制:通过版本控制功能,我们可以存储对象的历史版本。处于历史版本下的对象会带有版本ID,通过指定版本ID就可以获取相应的历史版本对象,使用GET方法时默认获取的是最新版本的对象。
-
生命周期:如果需要隔一段时间就将对象物理删除,那么可以利用生命周期功能来指定删除规则。将生命周期和版本控制结合起来,还可以控制历史版本的存续期间。
-
加密:将重要数据存入对象存储的时候就需要考虑加密。对象存储加密的思路有两种:
- 服务端加密:针对的是整个存储桶配置。当数据写入磁盘时,服务器会在对象级别上进行数据加密,并在用户访问数据时进行数据解密。对于用户来说只要拥有访问权限和密钥信息,就可以直接访问对象。适用于想要在云内部安全地管理对象的场景。
- 客户端加密:用户上传对象时,由客户端对对象本身进行加密。对于用户来说,下载的对象依然处于加密状态。适合于不依赖云环境而自行加密的场景。
-
网站功能:对象存储本身支持HTTP(HTTPS)访问,当存储HTML文件时,只需要为其对象赋予公开权限,就可以通过HTML文件的URL来访问网页。但是此时只能通过带有/index.html的路径来访问网页,而且访问出错时只能显示为对象存储提供的通用错误页面。为了提供更好的网站体验,可以在对象存储中开启网站功能,从而支持默认页面、自定义的错误页面和重定向等其他基本的功能。
-
CORS:将对象存储用作静态网站时,需要考虑资源跨域的问题。比如我们将CSS样式表、脚本、图片文件存储到Amazon S3中,这些资源的URI使用的是存储桶的域名,然后从主站点网页中请求这些文件,就会产生跨域问题。为了解决这个问题,我们可以在存储桶中启用CORS(Cross Origin Resource Sharing,跨域资源共享)的配置。配置内容可以包括:
- 指定允许的请求方法:先导请求(preflight request)会通过OPTIONS方法确认能否进行跨域访问,是否支持先导请求取决于浏览器。
- 指定允许访问的来源域名
- 指定响应的缓存时间
4. 对象和API的关系
最终一致性
对象存储是基于HTTP协议访问文件的,且对象具有持久性,所以API调用后的对象状态具有一致性和最终一致性两种特性。
- 一致性:一致性是指在对象存储系统中,无论何时何地访问对象,都能立即获取到该对象的最新版本,且所有副本的数据完全一致,不存在数据不一致的中间状态。在文件系统和关系型数据库中也有一致性的概念,而在对象存储中一致性只适用于通过PUT请求新增对象的场景。
- 最终一致性:最终一致性是指在对象存储系统中,数据的更新操作可能不会立即在所有节点和副本上生效,但经过一段时间后,所有节点和副本最终都会达到一致的状态。在这个过程中,不同节点和副本之间可能存在短暂的数据不一致情况,但最终会收敛到一致。当通过PUT方法覆盖对象或通过DELETE方法删除对象时,所体现的是最终一致性。
使用PUT操作覆盖对象相当于更新对象,而使用DELETE操作只会在内部临时进行逻辑删除,其实际行为也类似于更新操作。那么更新对象时,为什么表现为最终一致性:
- 分布式架构:对象存储系统通常采用分布式架构,数据会被分散存储在多个节点甚至多个数据中心。当执行对象更新操作时,这些操作首先会在本地节点上进行处理,然后再异步地传播到其他节点。由于不同节点之间的网络延迟、处理速度等存在差异,以及为了性能优化而采取的异步处理和缓存机制,可能导致在一段时间内,不同节点上的数据状态不一致,需要一定时间才能实现所有节点的数据最终一致。
- 并发操作处理:在大规模的对象存储系统中,可能会同时存在多个对象更新操作,这些操作可能会相互影响,导致数据的一致性状态更加复杂。系统需要通过复杂的并发控制机制来处理这些操作,确保数据的最终一致性,但在处理过程中可能会出现短暂的不一致情况。比如从不同客户端对现有对象分别执行PUT覆盖操作,由于网络延迟,有可能后请求的PUT操作反而被先请求的PUT操作覆盖的问题。
对象存储的GET、PUT、DELETE等操作方法是幂等的,即无论重复执行多少次,对象的状态都是满足最终一致性的。
用ETag确认对象
既然对象存储采用了最终一致性的架构,那么怎么确保对象已经存储好了呢?答案就是使用HTTP协议的ETag。
- ETag的基本概念:ETag是实体标签(entity tag)的简称,ETag的常用方式就是对资源内容进行哈希运算,当资源内容发生变化时,其哈希值也会相应改变,从而导致 ETag 值变化。
- ETag在对象存储中的应用:在对象存储中,每个版本的对象都有唯一的 ETag。通过比较不同版本对象的 ETag,可以快速确定对象是否发生了变化以及变化的版本情况。同时,在分布式对象存储环境中,ETag 可以帮助确认不同节点上存储的对象是否一致,当从不同节点获取到的对象 ETag 值相同时,说明对象在这些节点上的数据是一致的,从而确保了对象存储的最终一致性。
5. 对象存储的内部结构
这一节以OpenStack Swift为例介绍对象存储的内部结构。Swift的架构大致可以分为前端和后端:
- 前端:接收HTTP请求的访问层
- 后端:将对象作为数据存储的存储节点
5.1 访问层的架构
对象存储是基于HTTP协议访问文件的,所以在访问层中配置了代理节点,以及用于弹性处理HTTP请求的负载均衡器。此外,对于找不到对象、访问超过上限或认证失败等场景,访问层也起到了控制作用。对后端存储节点进行访问控制的配置就称为RING。
RING配置信息
RING是Swift的核心组件,账户、存储桶和对象都有各自独立的RING。RING为存储节点定义了副本数、分区和磁盘信息这三种信息。
- 副本数:指每个数据对象在存储系统中保存的副本数量。通过设置多个副本,可以提高数据的可靠性和可用性,防止因单个存储节点故障导致数据丢失。
- 分区:RING 将存储系统中的所有存储节点划分为多个分区,每个分区包含一定数量的存储节点。分区的作用主要是为了实现数据的均匀分布和负载均衡,以及实现数据迁移、副本修复等数据管理。
- 磁盘信息:包括存储节点上各个磁盘的容量、性能、使用情况等详细信息。RING 需要了解每个存储节点上磁盘的具体情况,以便在进行数据存储和分配时,合理地将数据分配到不同的磁盘上,实现存储资源的高效利用。
分区映射
- RING通过MD5散列算法来控制分区(partition)与作为存储节点集群的区域(zone)的映射关系,将文件分散部署到对应的区域。分区数越多,散列值就越多,部署就越分散,从而减少分布不均匀的情况。
- 静态散列表:分区的散列值是根据对象的键名计算出来的。只要对象的键名不变,分配的区域就不变,所以分区和区域之间的映射关系是固定的。
- 增减存储节点的时候需要重建RING,通过散列算法进行再平衡(rebalance),使数据更加均匀地分布在整个存储集群中,从而充分利用各个存储节点的存储空间和性能资源,提高存储系统的整体性能和资源利用率。
5.2 存储节点的架构
实际用来存储对象的存储节点的集群称为区域。各个区域在物理上是隔离的,Swift会根据RING配置中的副本配置项决定将文件部署到哪些位置,并分别复制对应于账户、存储桶和对象的文件。
存储节点内部运行的复制器(replicator)是用于同步文件的进程。它的主要功能包括:
- 副本创建:当新的数据写入存储系统时,根据配置的复制策略,复制器会在存储节点内部或不同存储节点之间创建数据的多个副本。例如,若复制因子为3,复制器会确保每个数据块在集群中有3个副本。
- 副本同步:在数据的生命周期内,复制器会持续监测副本之间的一致性。如果发现某个副本与其他副本不一致,例如由于网络故障、存储故障或其他原因导致部分副本更新不及时,复制器会自动发起同步操作,将最新的数据从源副本同步到其他副本上,确保所有副本的数据内容保持一致。
复制的工作包括“确认”和“复制”两个过程。
- 确认过程:
- 副本状态检查:复制器会定期遍历存储系统中的所有数据副本,检查每个副本的状态信息,包括副本是否可用、是否完整、是否存在错误等。
- 一致性校验:采用多种方式对副本进行一致性校验,判断副本之间是否存在差异,确定需要进行同步或修复的副本。检验一致性使用的元数据包括散列值和时间戳,其中散列值是根据对象键名计算的,当键名不变时,就会对比时间戳来确认对象是否发生了变化。使用元数据比较的好处是高效灵活,具有可扩展性,以及减少网络传输。(另外,从一致性比较机制可以看出,删除对象时如果直接采用物理删除会有问题,因此需要采用逻辑删除,即修改元数据的方式来确保DELETE操作的幂等性。)
- 负载与空间检查:确认存储节点的负载情况,包括 CPU 使用率、内存占用、磁盘 I/O 负载和网络带宽占用等。同时,检查存储节点的可用存储空间,确保有足够的空间来进行数据副本的复制和存储。如果发现存储节点负载过高或空间不足,可能会调整复制策略或采取其他措施来优化资源利用。
- 复制过程:
- 源副本选择:首先要选择合适的源副本。通常会选择数据最新、最完整且状态良好的副本作为源副本。如果多个副本都可用,可能会根据一定的策略进行选择,如选择负载较低的存储节点上的副本,以减少对系统性能的影响。
- 目标副本确定:选择目标副本时,会考虑存储节点的可用空间、负载情况、网络连接状况等因素,确保复制操作能够顺利进行,并且不会导致新的性能问题或存储资源不足的情况。
- 数据传输:在源副本和目标副本之间建立可靠的网络连接,采用高效的数据传输协议和优化技术,将数据从源副本传输到目标副本。在传输过程中,会对数据进行加密、校验和验证等操作,确保数据的安全性和完整性。
- 副本更新与同步:在数据传输完成后,对目标副本进行更新操作,确保其数据内容与源副本完全一致。同时,会更新目标副本的元数据信息,如版本号、时间戳、校验和等,使其与源副本保持同步。
5.3 读操作和写操作
对象操作可以分为两类:读操作(GET请求)和写操作(PUT请求)。
- 读操作:读操作可以从RING指定的副本配置项里的所有区域中读对象。执行读操作时,会通过负载均衡从某一个区域获取对象。根据负载均衡的分配机制,多次调用GET请求时,分配到的区域可能会不同。
- 写操作:RING分区配置使用的是静态散列表,只要对象的键名不变,就会映射到固定的分区。写操作会在RING指定的区域写对象,然后再复制同步到其他区域。
小结
本章主要介绍了对象存储资源之间的关系。对象存储的基本资源包括账户、存储桶和对象。OpenStack Swift通过元数据为存储桶和对象赋予配置信息,Amazon S3则将各种配置信息分别定义为与存储桶和对象相同的资源。