ceph本地对象存储(2)
本地对象存储模块完成了数据如何原子地写入磁盘,这就涉及事务和日志的概念。对象如何在本地文件系统中组织的代码实现在src/os中。本章将介绍在单个OSD上数据如何写入磁盘中。
目前有4种本地对象存储实现:
-
FileStore: 这是目前比较稳定,生产环境上使用的主流对象存储引擎,也是本章重点介绍的对象存储引擎
-
BlueStore: 这是目前社区在实现的一个新版本,社区丢弃了本地文件系统,自己写了一个简单的,专门支持RADOS用户态的文件系统
-
KStore: 这是以本地KV存储系统实现的对象存储,它基于RADOS的框架用来实现一个分布式的KV存储系统
-
Memstore: 它把数据和元数据都保存在内存中,用来测试和验证使用
KStore和Memstore两种存储引擎比较简单,这里就不介绍了(会在后面适当的章节进行详细分析)。BlueStore社区还正在开发之中,这里也暂时不介绍。本章将详细介绍目前在生产环境中使用的FileStore存储的实现。
在了解ceph filestore本地对象对象存储的具体实现之前,建议先参看本章附录
相关内容,以对object的attr及omap操作有一个直观上的认识。
我们可以通过ceph_test_objectstore来对本地对象存储有一个更深的理解,执行如下命令进行编译:
# cd src # make ceph_test_objectstore
1. 基本概念介绍
RADOS本地对象存储系统(也称为对象存储引擎)基于本地文件系统实现,目前默认的文件系统为XFS。一个对象包含数据
和元数据
两种数据。对应本地文件系统里,一个对象就是一个固定大小(默认4MB)的文件,其元数据保存在文件的扩展属性或者本地独立的KV存储系统中。
1.1 对象的元数据
对象的元数据就是用于对象描述信息的数据,它以简单的key-value(键值对)形式存在,在RADOS存储系统中有两种实现:xattrs和omap:
-
xattrs保存在对象对应文件的扩展属性中,这要求支持对象存储的本地文件系统支持扩展属性。
-
omap就是object map的简称,是一些键值对,保存在本地文件系统之外的独立的key-value存储系统中,例如leveldb、rockdb等。
有些文件系统可能不支持扩展属性,有些虽然也支持扩展属性但对key或者value占用空间的大小有限制,或者扩展属性占的总的空间大小有限制。对于leveldb等本地键值存储系统基本没有这样的限制,但是它的写性能优于读性能。所以一般情况下,xattrs保存一些比较小而经常访问的信息。omap保存一些大而不是经常访问的数据。
1.2 事务和日志的基本概念
假设磁盘正在执行一个操作,此时由于发生磁盘错误,或者系统宕机,或者断电等其他原因,导致只有部分数据写入成功。这种情况就会出现磁盘上的数据有一部分是旧数据,部分是新写入的数据,使得磁盘数据不一致。
当一个操作要么全部成功,要么全部失败,不允许只有部分操作成功,就称这个操作具有原子性。引入事务和日志,就是为了实现操作的原子性,解决数据的不一致性问题。
引入日志后,数据写入变为两步: 1) 先把要写入的数据全部封装成一个事务,其整体作为一条日志,先写入日志文件并持久化到磁盘,这个过程称为日志提交(journal submit)。2)然后再把数据写入对象文件中,这称为日志的应用(journal apply)。
当系统在日志提交的过程中出错,系统重启后,直接丢弃不完整的日志条目即可,该条日志对应的实际对象数据并没有修改,数据可以保持一致。当在日志应用的过程中出错,由于数据已经写入并回刷到日志盘中,系统重启后,重放(replay)日志,就可以保证新数据重新完整写入,保证了数据的一致性。
这个机制需要确保所有的更新操作都是幂等操作。所谓幂等操作,就是数据的更新可以多次写入,不会产生任何副作用。对象存储的操作一般都具有幂等性。
在事务的提交过程中,一条日志记录可以对应一个事务。为了提高日志提交的性能,一般都允许多条事务并发提交,一个事务可以对应多条日志,批量提交。所以事务的提交过程,一般和日志的提交过程是一个概念。
日志有三个处理阶段,对应过程分别为:
-
日志提交(journal submit): 日志写入日志磁盘
-
日志的应用(journal apply): 日志对应的修改更新到对象的磁盘文件中。这个修改不一定写入磁盘,可能缓存在本地文件系统的页缓存(page cache)中
-
日志的同步(Journal sync或者journal commit): 当确定日志对应的修改操作已经刷回到磁盘中,就可以把相应的日志记录删除,释放所占用的日志空间。
1.3 事务的封装
ObjectStore的内部类Transaction用来实现相关的事务。它有两种封装形式,一种是use_tbl
(transaction bufferlist),事务把操作的元数据和数据都封装在bufferlist类型的tbl中:
另一种是不使用tbl,把元数据操作以struct Op的结构体,封装在op_bl中,把操作相关的数据封装在data_bl中:
由于struct Op里保存的是coll_id和object_id,所以需要保存coll_t到coll_id的映射关系,以及ghobject_t与object_id的映射关系。从这种存储方式就可以看出,这种方式和前一种的区别,是在数据封装上实现了一种压缩。当事务中多个操作有相同的对象时,只保存一次ghobject_t结构体,其他情况只保存index来索引。
数据结构TransactionData记录了一个事务中有关操作的统计信息:
比如我们对一个Transaction t进行了两次write操作,那么t.data.ops的值就是2,后面我们在构造FileStore::Op时可能就会需要用到该值。
一个事务中,对应如下三类回调函数,分别在事务不同的处理阶段调用。当事务完成相应阶段工作后,就调用相应的回调函数来通知事件完成。注意每一类都可以注册多个回调函数:
on_commit是事务提交完成之后调用的回调函数;on_applied_sync和on_applied都是事务应用完成之后的回调函数。前者是被同步调用执行,后者是在Finisher线程里异步调用执行。
注: on_commit是在事务提交完成之后会通过Finisher线程进行异步回调。这里对事务提交完成
进行一下说明,对于journal writeahead模式,由于首先是进行写日志操作,然后才是应用日志操作,那么此种情况下,写日志完成即认为事务提交完成
;而对于journal parallel模式,由于写日志与应用日志是并行进行的,因此这两个操作任意一个完成即认为事务提交完成
。
2. ObjectStore对象存储接口
下面我们先对ObjectStore中的一些概念及术语做一个简单的介绍:
1) ObjectStore中Object的内容及语义
ObjectStore所存储的每一个object都是由处于collection(coll_t)中ghobject_t或hobject_t所标识(注: 这里我们可以将collection理解为一个PG,一个object对应一个PG,但是一个PG可以对应多个object,一个coll_t中的objects可以认为都属于同一个PG)。ObjectStore支持在一个collection内创建(creation)、修改(mutation)、删除(deletion)、枚举(enumeration)对象的操作。这里枚举对象时,是按hash后的对象名的字典序进行输出的。object名称在全局是唯一的。
每一个object都有4个不同的部分: byte data、xattrs、omap_header以及omap entries。
-
data: 从概念上来讲,一个object的data部分就相当于文件系统上的一个文件。能够支持对data数据的随机读写操作。对一个object的data部分的轻量级的实现有助于降低系统的工作负载。在系统层面上,我们通常会限制每个对象的最大大小为100MB。
-
xattrs: 等价于文件系统的扩展属性。Xattrs是一系列的键值对,支持对xattrs进行枚举操作。在实现层面,xattr并不属于ceph,而是依赖于所采用的文件系统,因此所支持的xattrs通常小于64KB。通常来说,ceph假设在访问对象时一并访问该对象相应的xattr是很方便的,并不会付出过高的代价。
-
omap_header: 是一个单独的数据块,可以作为一个整体来进行读写操作
-
omap entries: 从概念上来讲,omap entries类似于xattrs,但保存于不同的位置。换句话说,在xattrs中所保存的key/value,在omap entries中可以有相同的key/value。在枚举xattrs时,并不会包括omap entries,反之亦然。通常xattrs最多只能有64KB,而omap entries大小可以达到MB级别,另外两者之间的访问方式也有很大的不同。ceph必须要提供高效
2) Collections
一个collection就是一个简单的对象分组。collections具有名称(coll_t),并且可以被枚举。与一个单独的object类似,一个collection也有一系列的xattrs。
3) transaction
一个Transaction代表一系列的原子修改操作。在transaction的生命周期中会产生3个事件回调,每个回调可以包含任意数量的callback上下文(Context)。3个事件回调分别为:
-
on_applied_sync
-
on_applied
-
on_commit
当Transaction所对应的修改请求对ObjectStore的其他子操作已经可见(比如数据已经可读),就会回调on_applied_sync或on_applied。on_applied_sync与on_applied之间的唯一区别就是回调线程、调用环境的不同。对于on_applied_sync是直接由ObjectStore执行线程所回调,通常回调可以快速完成,且在回调时并不需要执行获取environment锁的操作;相反,对于on_applied则会通过一个单独的Finisher线程来完成,这就意味着其可能需要竞争environment锁。
注: on_applied有时候也会被称为on_readable,on_applied_sync有时候会被称为on_readable_sync
on_commit回调也是通过Finisher线程来进行,表明所有的修改操作都已经被提交,并完成了持久化操作。
在实现层面,每一个修改原语(也包括其所关联的数据)都可以被序列化到一个单独的buffer中。在序列化时,并不会拷贝任何的数据,而是通过bufferlist直接引用原始的缓冲块。这就意味着在提交过程中,必须保持整个buffer数据的稳定,直到on_commit回调完成。实际上,bufferlist会帮你完成这个过程,只有在你通过buffer::raw_static来引用一块已存在的内存时才需要自己注意在提交期间保证buffer的数据不会被修改。
有一些ObjectStore的实现会选择根据Transaction的序列化格式来实现自己的日志(journaling)形式。这就要求相应的encode/decode逻辑能够正确的识别version信息,并且在Transaction的编码格式改变时也能够正确的进行升级。这种情况其实已经发生了,Transaction object也包含一些辅助的变量来协助进行解码:
对于bobtail版本之前的ceph, 可通过sobject_encoding来识别older/simpler类型的oid; 可以通过use_pool_override来侦测pool oid被覆盖的场景。
4) 事务的隔离
除非另行说明,否则事务的隔离都是由调用者来负责的。换句话说,假如任何存储元素(即上文说的object的4个部分)在事务中被修改,那么在事务处于pending期间(从一个事务产生,到收到on_applied_sync回调这一时间段都处于pending状态),调用者都不应该尝试去读取该元素。ObjectStore并不会侦测隔离性是否被破坏,也没有相应的机制来报告隔离性遭到了破坏。
如果一个transaction本身包含创建(create)或删除(delete)存储元素的操作,那么执行enumeration操作也有可能会破坏事务的隔离性。在这种情况下如果执行enumeration操作,那么允许ObjectStore自行决定是返回事务元素被破坏前还是破坏后的结果。换句话说,ObjectStore返回的枚举结果集对于是否包含该事务所修改的元素是不确定的。例如,假设一个事务(transaction)含有”Create A”与”delete B”两个修改元素的操作,那么在transaction处于pending期间,如果执行enumeration操作,则ObjectStore可能会返回A/B存在性的4种组合.
下面通过对象存储的接口说明和代码示例,可以了解对象存储的基本功能及如何使用这些功能,从而对对象存储有一个概要了解:
由上面可看到,基本上ObjectStore就是对一个文件系统相关功能的一个抽象。ObjectStore可以将Transaction打包进一个队列,然后由相关线程负责相应事务的提交。
下面我们通过对象存储的接口说明和代码示例,可以了解对象存储的基本功能及如何使用这些功能,从而对对象存储有一个概要了解。
2.1 对外接口说明
类ObjectStore是对象存储系统抽象操作接口。所有的对象存储引擎都有继承并实现它定义的接口。下面列出一些代表性的函数接口:
- objectstore初始化相关的函数
mkfs 创建objectstore相关的系统信息 mount 加载objectstore相关的系统信息 statfs 获取objectstore系统信息
- 获取属性相关信息
getattr 获取对象的扩展属性xattr omap_get 获取对象的omap信息
- queue_transactions是所有ObjectStore更新操作的接口。更新相关的操作(例如创建一个对象,修改属性,写数据等)都是以事务的方式提交给ObjectStore,该函数被重载成各种不同的接口。其参数为:
2.2 ObjectStore代码示例
下面通过一段ObjectStore的测试代码,来展示ObjectStore的基本功能和接口使用:
1) 首先调用create方法创建一个ObjectStore实例。参数分别为配置选项CephContext,对象存储的类型: filestore,对象存储的目录名,日志文件名。如下
2) 创建并加载ObjectStore本地对象存储
3) 创建了一个collection,并写数据到一个对象中。对象的任何写操作,都先调用事务的相关写操作。事务把相关操作的元数据和数据封装,然后调用store的apply_transaction来真正实现数据的更改。
8. 附录–ceph存储object的attr和omap操作
在ceph中,所有的存储不管是块存储、对象存储、还是文件存储最后都转化成了底层的对象object,这个object包含3个元素data、xattr、omap。data是保存对象的数据。xattr是保存对象的扩展属性,每个对象文件都可以设置文件的属性,这个属性是一个key/value对,但是受到文件系统的限制,key/value对的个数和每个value的大小都进行了限制。如果要设置的对象的key/value不能存储在文件的扩展属性中,还存在另外一种方式保存:omap,omap实际上是保存到了key/value对的数据库levelDB中,在这里value的值限制要比xattr中好的多。
一开始不太明白key/value是做什么的?在ceph中起到什么作用?这里要说明的是这些key/value是保存对象的元数据相关信息,这些元数据相关信息是可以单独创建和设置的,等于对象存储的扩展,支持属性的键值对存储。这个作用就是提供给ceph内部使用,暂时用处不大。
8.1 块存储设备元数据管理
对于块存储,其元数据主要是块的相关信息。我们在创建一个块设备后,会创建一个默认的rbd(rados block device)元数据文件,用其来存放相应的元数据。
1) 创建块设备
首先执行如下命令查看当前已有的存储池:
# rados lspools .rgw.root default.rgw.control default.rgw.data.root default.rgw.gc default.rgw.log default.rgw.users.uid default.rgw.users.keys default.rgw.buckets.index default.rgw.usage default.rgw.buckets.data default.rgw.buckets.non-ec default.rgw.users.swift benchmark default.rgw.users.email
创建pool的语法为:
如下我们创建名为rbd-01
的pool(其pg-num为128):
# ceph osd pool create rbd-01 128 128 pool 'rbd-01' created # ceph osd pool set rbd-01 size 3 set pool 42 size to 3 # ceph osd pool set rbd-01 crush_ruleset 5 set pool 42 crush_ruleset to 5 # ceph osd pool ls .rgw.root default.rgw.control default.rgw.data.root default.rgw.gc default.rgw.log default.rgw.users.uid default.rgw.users.keys default.rgw.buckets.index default.rgw.usage default.rgw.buckets.data default.rgw.buckets.non-ec default.rgw.users.swift benchmark default.rgw.users.email rbd-01
之后,通过如下命令在rbd-01上面创建一个大小为100GB的块设备:
# rbd create rbd-01/rbd-image01 --size=102400 # rbd info rbd-01/rbd-image01 rbd image 'rbd-image01': size 102400 MB in 25600 objects order 22 (4096 kB objects) block_name_prefix: rbd_data.2f7502ae8944a format: 2 features: layering, exclusive-lock, object-map, fast-diff, deep-flatten flags: # rados -p rbd-01 ls rbd_id.rbd-image01 rbd_object_map.2f7502ae8944a rbd_directory rbd_header.2f7502ae8944a
2) 查看块设备存被映射到了哪些OSD上
采用如下的命令查看rbd-image01
这个块设备映射到了哪些OSD上:
3) 设置xattr元数据信息
# rados -p rbd-01 setxattr rbd-image01 creator ivanzz1001 # rados -p rbd-01 setxattr rbd-image01 description 'just for test' # rados -p rbd-01 listxattr rbd-image01 creator description # rados -p rbd-01 getxattr rbd-image01 creator ivanzz1001
4) 查看rbd-image01对象
通过上面我们发现rbd-image01被存储在pg 42.23内,因此我们可以进入osd.5相应的目录下查看rbd-image01
的相关信息:
# pwd /var/lib/ceph/osd/ceph-5/current # cd 42.23_head # ls __head_00000023__2a rbd-image01__head_F11B8EA3__2a
获取对象的扩展属性信息:
# getfattr ./rbd-image01__head_F11B8EA3__2a # file: rbd-image01__head_F11B8EA3__2a user.ceph._ user.ceph._@1 user.ceph._creator user.ceph._description user.ceph.snapset user.cephos.spill_out # getfattr -n user.ceph._creator ./rbd-image01__head_F11B8EA3__2a # file: rbd-image01__head_F11B8EA3__2a user.ceph._creator="ivanzz1001" # getfattr -n user.ceph._description ./rbd-image01__head_F11B8EA3__2a # file: rbd-image01__head_F11B8EA3__2a user.ceph._description="just for test"
通过上面我们可以直观的看到刚才我们所设置的扩展属性信息。
8.2 对象存储元数据管理(xattr)
1) 上传文件到benchmark存储池
# rados lspools .rgw.root default.rgw.control default.rgw.data.root default.rgw.gc default.rgw.log default.rgw.users.uid default.rgw.users.keys default.rgw.buckets.index default.rgw.usage default.rgw.buckets.data default.rgw.buckets.non-ec default.rgw.users.swift benchmark default.rgw.users.email # pwd /root/ceph_cluster/test # cat hello.txt hello,world # rados -p benchmark put hello.txt /root/ceph_cluster/test/hello.txt # rados -p benchmark ls | grep hello hello.txt
2) 查看上传的hello.txt对象
3) 设置对象的扩展属性
# rados -p benchmark setxattr hello.txt creator ivanzz1001 # rados -p benchmark setxattr hello.txt description 'just for test' # rados -p benchmark listxattr hello.txt creator description # rados -p benchmark getxattr hello.txt creator ivanzz1001
4) 通过linux相关命令查看设置的扩展属性
# getfattr ./hello.txt__head_D92FD82B__27 # file: hello.txt__head_D92FD82B__27 user.ceph._ user.ceph._@1 user.ceph._creator user.ceph._description user.ceph.snapset user.cephos.spill_out # getfattr -n user.ceph._creator ./hello.txt__head_D92FD82B__27 # file: hello.txt__head_D92FD82B__27 user.ceph._creator="ivanzz1001"
8.3 对象存储元数据管理(omap)
1) 设置omap
对上面所创建的benchmark这个pool中的hello.txt
这个对象设置omap:
# rados -p benchmark setomapval hello.txt hcreator 'ivanzz1001' # rados -p benchmark setomapval hello.txt hdescription 'just for test' # rados -p benchmark listomapkeys hello.txt hcreator hdescription
2) 查看omap
使用如下命令查看omap值:
# rados -p benchmark getomapval hello.txt hcreator value (10 bytes) : 00000000 69 76 61 6e 7a 7a 31 30 30 31 |ivanzz1001| 0000000a # rados -p benchmark getomapval hello.txt hdescription value (13 bytes) : 00000000 6a 75 73 74 20 66 6f 72 20 74 65 73 74 |just for test| 0000000d
目前对于omap具体存放在哪个文件还未知晓,猜测是在对应OSD的omap
文件夹下。例如:/var/lib/ceph/osd/ceph-7/current/omap
[参看]