本文翻译整理自swift官方文档《OpenStack Object Storage Administration Manual》中的“Managing Large Objects (Greater than 5 GB)”一节,并实测,验证文档中的内容。点这里可以看到在线文档。
默认情况中,Swfit上传的最大单个对象的大小为5GB。然而,对于下载对象的大小却是没有限制的,这种“下载无限制”的概念是通过将分段对象组织起来模拟实现的。大对象的每个分段被分别上传,然后再创建一个清单文件(manifest file)指明这个对象所包含的分段,下载时,清单所指示所有分段会被组织成一个单独的对象进行下载。这种实现方式同时页提高了上传的速率,因为我们可以将一个对象的多个分段进行并行的上传。
使用swift命令管理分段对象
尝试这种特性的最快捷的方式,是使用OpenStack对象存储客户端工具。上传对象时,你可以使用-S选项来指明每个分段的大小,例如:
# swift upload test_container -S 1073741824 large_file
这条命令会将large_file分成一组1G大小的分段,然后并行的上传这些分段。一旦所有的分段都上传完毕了,swift客户端工具就会为这些分段创建一个manifest文件,以保证这些分段可以作为一个对象large_file被下载。
以下命令将会下载整个大对象:
swift download test_container large_file
swift命令行工具采用一种严格的转换方式来实现对分段对象的支持。在上面的例子中,swift命令行工具会首先将所有的分段上传到另一个容器(test_container_segments)中,这些分段被命名为类似large_file/1290206778.25/21474836480/00000000, large_file/1290206778.25/21474836480/00000001等 这样的名字。
使用一个分离的容器存储分段的主要的好处是,当我们对主容器进行listings操作时,将不会把所有的分段名称都展示出来。使用 <name>/<timestamp>/<size>/<segment> 这种格式明明对象的分段,是因为如果用户上传了一个拥有相同名称的新对象,将会不覆盖前一个对象,知道清单文件被更新。
swift命令行工具会帮你管理这些分段文件,在删除、覆盖的时候删除这些老的分段。如果需要的话,你可以使用-leave-segments选项来覆盖这些行为。在你希望对大对象进行多版本管理时,s这将会非常有用。
直接使用API管理大对象
你可以直接使用HTTP请求而非swift客户端工具来操作分段和清单。你可以像上传其他文件一样直接上传分段、清单文件,其中清单文件只是一个拥有 X-Object-Manifest 头的大小为0字节的普通文件。
一个对象的所有分段必须存储在同一个容器中,拥有共同的对象名称前缀,并按照他们应有的顺序排序命名。它们可以和清单文件不在同一个容器中,这可以将容器中的对象列表保持的比较干净。
清单文件只是一个大小为0字节、拥有 X-Object-Manifest: <container>/<prefix> 头的普通文件。其中<container>是对象分段所在的容器名称,<prefix>是这些分段的公共前缀。
最好的方式是,先上传所有的分段,再创建/更新清单文件。在这种情况下,在对象完全上传成功前,该对象是不会被下载请求访问到的。你也可以上传一堆新的分段到另外一个位置,然后更新清单文件指向新的位置。在这些新分段上传的过程中,原来的清单文件还是可以被访问的,即在新分段上传的过程中,对该清单文件的下载请求,还是返回原来的分段对象。
这里有一个使用curl执行1个字节的小分段上传的范例:
# 首先,上传所有的分段
$ curl -X PUT -H \'X-Auth-Token: <token>\' http://<storage_url>/container/ myobject/1 --data-binary \'1\' $ curl -X PUT -H \'X-Auth-Token: <token>\' http://<storage_url>/container/ myobject/2 --data-binary \'2\' $ curl -X PUT -H \'X-Auth-Token: <token>\' http://<storage_url>/container/ myobject/3 --data-binary \'3\'
# 然后,创建清单文件
curl -X PUT -H \'X-Auth-Token: <token>\' -H \'X-Object-Manifest: container/myobject/\' http://<storage_url>/container/myobject --data-binary \'\'
# 现在我们可以将这些分段当做一个对象进行下载
curl -H \'X-Auth-Token: <token>\' http://<storage_url>/container/myobject
大对象的额外注意点
对清单文件执行一个GET或HEAD请求,X-Object-Manifest: <container>/<prefix> 头会联通串联后的对象一起返回,因此你可以根据 X-Object-Manifest 判断是从哪里获取的分段。
对清单文件执行GET或HEAD请求,响应头 Content-Length 的值为其指向的所有分段的大小总和,因此这个值是动态的。因此,当你在创建清单文件后,继续上传了额外的分段,将会使串连对象变得更大,但无需重新构造清单文件。
对清单文件执行GET或HEAD请求,响应头 Content-Type 的值与使用PUT请求创建该清单时指定的 Content-Type 相同。因此,你可以通过重新发送PUT请求来改变 Content-Type 的值。
对清单文件执行GET或HEAD请求,响应头 ETag 的值将为其指向的每个分段的ETag串联字符串的MD5值,因此这个值是动态的。通常,swift中对象的ETag值是对象内容的MD5值,这对每个单独的分段也是成立的。但是,清单文件本身不会产生这样的一个ETag值,因此这个方法至少可以用于判断文件的改变。
大对象存储的历史和背景
在当前版本的实现方式之前,大对象的支持已经经历了若干次迭代实现。
在swift中限制单个对象的大小的最主要原因是为了平衡ring上的不同分区。为了保证集群中的磁盘可以被均匀的利用,很显然我们需要将大文件分解成更小的分段,然后在读操作的过程中将小分段合并。
在提出大对象的支持之前,一些应用已经在客户端将它们要上传的对象分解成一些独立的段,然后再对这些分段进行重组。这种设计使得客户端可以支持备份并归档大数据集,然而由于网络中断等问题,却不能很好的提高性能、减少错误。采用这种方法最主要的缺点是,由于需要原始的划分信息来重组对象,因此在一些场景(如CDN源)中这种方法就不太实际。
为了消除客户端对于存储大于5GB对象需求的障碍,我们最初设计了一种对大对象上传完全透明的支持方式。为了实现完全透明的大对象上传,proxy server将会在对象上传的过程中负责将这个大对象分段,而不需要修改任何的API。因此所有的分段都被完全隐藏在了客户端API之后。
然而,这种实现方式将引起一些问题:它不支持客户端并行的上传一个对象,并且没有错误恢复机制。相较于完全透明的分段方式带来的益处,它的复杂性还是显得太高了。
我们当前使用的“user manifest”设计时为了给客户端提供一个透明的大对象下载方式,同时也可以支持分段上传,并保持客户端API的干净、整洁。
另一种“显示的”user manifest实现方式也被讨论过的,它需要预先定义列出已上传分段的格式。虽然这种方式可以提供一些潜在的优点,但是为了实现一个更简单的API,我们应避免将负担推到客户端(实际上,就是决定“X-Object-Manifest”的格式)。
在开发的过程中,我们注意到,这种基于公共路径前缀的“隐式的”user manifest方法将被会被最终一致性窗口的大小影响,理论上会产生这种情况:在上传了一个对象很短的时间内,对清单文件执行GET操作,将会返回一个无效的对象。事实上,除非你的swift运行在一个并发上传度非常高的测试环境中,你将不太可能会遇到这种情况(在并发上传非常高的测试环境中不运行object-updaters和container-replicators)。
像所有的OpenStack对象存储中的特性一样,支持大对象是一个发展中的特性,它将继续改善,并可能随时间而改变。
实测结果1. swift CLI 进行大文件上传、下载
测试文件repost_backup2.sql的大小为12.3G。
1)直接上传,会报错,文件太大。
2)指定分段上传,每段5G。swift CLI工具将文件分成3段,并创建了清单文件。
3)上传成功后,查看容器,发现新增了docs_segments容器。
4)目标容器中只有repost_backup2.sql清单文件,并没有分段对象;docs_segments容器中存储分段对象。
5)下载时,将分段对象作为一个大对象下载。
实测结果2. curl 进行分段上传、合并下载
1)分别上传3个1字节大小的文件,公共前缀为"myobject/"
2)创建清单文件,指定X-Object-Manifest的值为公共前缀"docs/myobject/",并命名为用户知晓的myobject。
3)查看上传结果,可以看到3个分段对象和1个清单文件。
4)下载myobject对象,可以看到返回结果将3个分段文件进行了串联,并只返回一个myobject对象。
此外,通过仔细观察HTTP的头信息,我们也可以应正“大对象的额外注意点”小节中阐述的内容。
一些思考
至此,想到了一些在开发API时需要注意的问题:
- Object的类型定义。由于伪目录的存在,需要为object增加subdir属性,用于区别伪目录和真实的对象;由于清单文件的存在,object的属性应增加对X-Object-Manifest元数据的过滤,用于判断清单文件;
- 容器的listing操作。为了对用户隐藏分段过程,在执行listing操作的时候怎样才能足够“干净”?模仿swift CLI为每个容器创建一个“容器_segments”的分段容器,并限制用户不能创建/访问“_segments”结尾的容器?
- 根据2,想到对象版本的管理,类似的为具有版本的容器创建“容器_versions”的历史版本容器,并限制用户不能创建/访问“_versions”结尾的容器?
- 对象可以并行上传,是否也可以并行下载?
- 大对象的更新操作,可以对其每个分段进行检测,只更新有变动的分段?
这些都是可以做的工作点,前3个属于API层的约束问题,较好实现;后2个应权衡swift的实现机制,做一些测试进行验证。
更新更新
为了给自己和看文的你更多的思考空间,我就不在上面每条问题的下面直接给出目前的想法了,更新在这里=D:
1. 关于Object的类型定义问题。
确实需要增加一个 X-Object-Manifest 属性,虽然分段的过程我们是希望对SDK使用者透明,但也不排除人家也有这个权利知道某个清单文件的真实身份。做了测试,在对Container中的对象进行listing操作时,以JSON格式获取详情为例,以下为返回内容:
[
{"hash": "c4ca4238a0b923820dcc509a6f75849b", "last_modified": "2013-03-21T13:51:18.917260", "bytes": 1, "name": "1", "content_type": "application/x-www-form-urlencoded"}, {"hash": "c81e728d9d4c2f636f067f89cc14862c", "last_modified": "2013-03-21T13:51:41.530770", "bytes": 1, "name": "2", "content_type": "application/x-www-form-urlencoded"}, {"hash": "eccbc87e4b5ce2fe28308fd9f2a7baf3", "last_modified": "2013-03-21T13:51:59.653640", "bytes": 1, "name": "3", "content_type": "application/x-www-form-urlencoded"}, {"hash": "d41d8cd98f00b204e9800998ecf8427e", "last_modified": "2013-03-21T13:55:33.353110", "bytes": 0, "name": "myobject", "content_type": "application/x-www-form-urlencoded"}, {"hash": "c4ca4238a0b923820dcc509a6f75849b", "last_modified": "2013-03-21T13:54:19.956880", "bytes": 1, "name": "myobject/1", "content_type": "application/x-www-form-urlencoded"}, {"hash": "c81e728d9d4c2f636f067f89cc14862c", "last_modified": "2013-03-21T13:54:32.005000", "bytes": 1, "name": "myobject/2", "content_type": "application/x-www-form-urlencoded"}, {"hash": "eccbc87e4b5ce2fe28308fd9f2a7baf3", "last_modified": "2013-03-21T13:54:42.420390", "bytes": 1, "name": "myobject/3", "content_type": "application/x-www-form-urlencoded"}, {"hash": "d41d8cd98f00b204e9800998ecf8427e", "last_modified": "2013-03-21T13:27:49.747290", "bytes": 0, "name": "repost_backup2.sql", "content_type": "application/x-sql"}, {"hash": "b2cfa4183267af678ea06c7407d4d6d8", "last_modified": "2013-03-21T13:16:14.842310", "bytes": 10, "name": "testFile", "content_type": "application/octet-stream"}
]
标红的两个对象实际上都是清单文件,但是在列表中却与其他普通对象没有任何区别:除了长度为0。但需要知道的是,就是一个普通对象也可以是长度为0的呀。所以 X-Object-Manifest 在这里几乎用不到。
然而,对清单文件执行HEAD或GET操作,我们就可以看到 X-Object-Manifest 了,并且对象的长度也不再是0,而是分段对象的总和,即大对象的真正长度。因此,我们为Object对象添加manifest属性的用武之地就是“对清单文件执行HEAD或GET操作”。
2. 容器的listing操作。
Swift CLI 将分段对象单独存在另一个容器中的方式蛮好的,这样可以避免对目标容器listing时出现很多诡异的分段对象(分段对象的属性和普通对象无区别,所以很难做过滤)。
然而,swift CLI 创建的容器也与普通容器没差别,因此在对Account做listing操作的时候会多出一些xxx_segments容器,这样也是不好的。
一种想法是限制用户容器命名,以“_segments”结尾的名称不得用于容器;
另一种想法是保留一个 X-Container-Meta-Segment 属性,类似编程语言里的保留字,属性值为分段对象的清单文件所在的容器名称(不同容器公用一个分段容器时,可以考虑设置其他的值,总之是需要这个属性来做区别)。这样可以在SDK层根据X-Container-Meta-Segment过滤,从而在account listing时隐藏分段对象。
可是,以上两种基于API层的过滤改进还是存在问题,即对account进行HEAD操作时显示的容器数量还是包含分段容器的,因此如果要很完美的做到“透明”,则不可避免的需要动到源码,而容器在ring上的分布也因此受到影响。
感觉上还是没有完美的方案,所以根据需求权衡吧,或者直接放弃“透明”的想法,让SDK使用者自己也做一些业务约束。