本系列文章会深入研究 Ceph 以及 Ceph 和 OpenStack 的集成:
(1)安装和部署
(2)Ceph RBD 接口和工具
(3)Ceph 物理和逻辑结构
(4)Ceph 的基础数据结构
(5)Ceph 与 OpenStack 集成的实现
(6)QEMU-KVM 和 Ceph RBD 的 缓存机制总结
从前一篇文章 我们知道,从物理上来讲,一个 Ceph 集群内部其实有几个子集群存在:
(1)MON(Montior)集群:MON 集群有由少量的、数目为奇数个的 Monitor 守护进程(Daemon)组成,它们负责通过维护 Ceph Cluster map 的一个主拷贝(master copy of Cluster map)来维护整 Ceph 集群的全局状态。理论上来讲,一个 MON 就可以完成这个任务,之所以需要一个多个守护进程组成的集群的原因是保证高可靠性。每个 Ceph node 上最多只能有一个 Monitor Daemon。
root@ceph1:~# ps -ef | grep ceph-mon root 964 1 0 Sep18 ? 00:36:33 /usr/bin/ceph-mon --cluster=ceph -i ceph1 -f
实际上,除了维护 Cluster map 以外,MON 还承担一些别的任务,比如用户校验、日志等。详细的配置可以参考 MON 配置。
(2)OSD (Object Storage Device)集群:OSD 集群由一定数目的(从几十个到几万个) OSD Daemon 组成,负责数据存储和复制,向 Ceph client 提供存储资源。每个 OSD 守护进程监视它自己的状态,以及别的 OSD 的状态,并且报告给 Monitor;而且,OSD 进程负责在数据盘上的文件读写操作;它还负责数据拷贝和恢复。在一个服务器上,一个数据盘有一个 OSD Daemon。
root@ceph1:~# ps -ef | grep ceph-osd root 1204 1 0 Sep18 ? 00:24:39 /usr/bin/ceph-osd --cluster=ceph -i 3 -f root 2254 1 0 Sep18 ? 00:20:52 /usr/bin/ceph-osd --cluster=ceph -i 6 -f
(3)若干个数据盘:一个Ceph 存储节点上可以有一个或者多个数据盘,每个数据盘上部署有特定的文件系统,比如 xfs,ext4 或者 btrfs,由一个 OSD Daemon 负责照顾其状态以及向其读写数据。
Disk /dev/vda: 21.5 GB, 21474836480 bytes /dev/vda1 1 41943039 20971519+ ee GPT Disk /dev/vdb: 32.2 GB, 32212254720 bytes /dev/vdb1 1 62914559 31457279+ ee GPT
(MON 和 OSD 可以共同在一个节点上,也可以分开)
关于Ceph 支持的数据盘上的 xfs、ext4 和 btrfs 文件系统,它们都是日志文件系统(其特点是文件系统将没提交的数据变化保存到日志文件,以便在系统崩溃或者掉电时恢复数据),三者各有优势和劣势:
(4)要使用 CephFS,还需要 MDS 集群,用于保存 CephFS 的元数据
(5)要使用对象存储接口,还需要 RADOS Gateway, 它对外提供REST接口,兼容S3和Swift的API。
Ceph 使用以太网连接内部各存储节点以及连接 client 和集群。Ceph 推荐使用两个网络:
下图(来源)显示了这种网络拓扑结构:
这么做,主要是从性能(OSD 节点之间会有大量的数据拷贝操作)和安全性(两网分离)考虑。你可以在 Ceph 配置文件的 [global] 部分配置两个网络:
public network = {public-network/netmask}
cluster network = {cluster-network/netmask}
具体可以参考:
缓存种类 | 说明 | 优劣势 | 适合场景 |
Write-through(直写) | 这种缓存方式在写 I/O 时把数据放入缓存,同时直接写入底层的持久存储,然后再向主机确认写入操作完成。 | 安全地保存数据,从缓存读,减少了读操作的延迟,但是写操作 的延迟没得到优化 | 适合于写较少,但是频繁度的应用 |
Write-back (回写) | 数据直接写入缓存,然后向主机返回写入完成。 | 对频繁写应用减少了写的延迟,但是有数据丢失风险 | 对读写混合型应用有优势,但是需要考虑数据保护 |
缓存的通常位置分类:
更多资料,可以参考 Cache is vital for application deployment, but which one to choose。
默认情况下,Ceph RBD 是不使用缓存的,读和写直接到 Ceph 集群中的存储,写只有在所有 replica 上写都完成后才给客户端返回写完成。Ceph 在较新的版本上陆续添加了 RBD 缓存支持:
可见 RBD 缓存是在客户端做的,见如下图示:
更多信息,可以参考我的另一篇文章:QEMU-KVM 和 Ceph RBD 的 缓存机制总结
Ceph 还支持在集群段做缓存分层。其原理是,在较快的磁盘比如 SSD 上建立一个 cache pool,在建立存储池(storage pool)和它之间的 cache 关系,设置一定的缓存策略,实现类似于在客户端缓存同样的效果。
更多的信息及详细配置,参见 CACHE TIERING 和 Intel 的文章。
从上面的分析可以看出来,两者的区别在于缓存的位置不同:
Rbd cache是 客户端的缓存,当多个客户端使用同个块设备时,存在客户端数据不一致的问题。举个例子,用户A向块设备写入数据后,数据停留在客户自己的缓存中,没有立即刷新到磁盘,所以其它用户读取不到A写入的数据。但是tier不存在这个问题,因为所有用户的数据都直接写入到 ssd,用户读取数据也是在ssd中读取的,所以不存在客户端数据不一致问题。
一般地,Tier 使用 SSD 做缓存,而 Rbd cache 只能使用内存做缓存。SSD和内存有两个方面的差别,一个是读写速度、另一个是掉电保护。掉电后内存中的数据就丢失了,而ssd中的数据不会丢失。
Ceph 集群的逻辑结构由 Pool 和 PG (Placement Group)来定义。
一个 Pool 是 Ceph 中的一些对象的逻辑分组,它并不表示一个连续的分区,而只是一个逻辑概念,类似于将二进制数据打了tag一样然后根据tag归类一样。它类似于 LVM 中的 Volume Group,类似于一个命名空间。RBD Image 类似于 LVM 中的 Logical Volume。RBD Image 必须且只能在一个 Pool 中。Pool 由若干个PG组成。
Ceph Pool 有两种类型:
Pool 提供如下的能力:
PG 是 pool 中的对象和 OSD 之间的映射关系。一个 object 只存在于一个 PG 中。Ceph 的 replication,是在 Pool 级别指定,但是在 PG 级别执行,也就是说 replication 是以PG 为单位进行的。PG 映射一个 Pool 到它使用的若干个 OSD。如果一个拷贝型 Pool 的size(拷贝份数)为 2,它会包含指定数目的 PG,每个 PG 使用两个 OSD,其中,第一个为主 OSD (primary),其它的为从 OSD (secondary)。不同的 PG 可能会共享一个 OSD,类似于下图:
那如何确定一个 Pool 中有多少 PG?Ceph 不会自己计算,而是给出了一些参考原则,让 Ceph 用户自己计算。
Ceph 要求 ceph 客户端和 OSD 守护进程需要知晓整个集群的拓扑结构,它们可以通过 Monitor 获取 cluster map 来达到这一点。Cluster map 包括:
(1)Monitor Map:MON 集群的状态(包括 the cluster fsid, the position, name address and port of each monitor, 创建时间,最后的更新时间等)。
root@ceph1:/osd/data# ceph mon dump dumped monmap epoch 1 epoch 1 fsid 4387471a-ae2b-47c4-b67e-9004860d0fd0 last_changed 0.000000 created 0.000000 0: 9.115.251.194:6789/0 mon.ceph1 1: 9.115.251.195:6789/0 mon.ceph2 2: 9.115.251.218:6789/0 mon.ceph3
(2)OSD Map:当前所有 Pool 的状态和所有 OSD 的状态 (包括 the cluster fsid, map 创建和最后修改时间, pool 列表, replica sizes, PG numbers, a list of OSDs and their status (e.g., up, in) 等)。通过运行 ceph osd dump 获取。
root@ceph1:~# ceph osd dump epoch 76 fsid 4387471a-ae2b-47c4-b67e-9004860d0fd0 created 2015-09-18 02:16:19.504735 modified 2015-09-21 07:58:55.305221 flags pool 0 'data' replicated size 3 min_size 2 crush_ruleset 0 object_hash rjenkins pg_num 64 pgp_num 64 last_change 1 flags hashpspool crash_replay_interval 45 stripe_width 0 osd.3 up in weight 1 up_from 26 up_thru 64 down_at 25 last_clean_interval [7,23) 9.115.251.194:6801/1204 9.115.251.194:6802/1204 9.115.251.194:6803/1204 9.115.251.194:6804/1204 exists,up d55567da-4e2a-40ca-b7c9-5a30240c895a
......
(3)PG Map:包含PG 版本(version)、时间戳、最新的 OSD map epoch, full ratios, and 每个 PG 的详细信息比如 PG ID, Up Set, Acting Set, 状态 (e.g., active + clean), pool 的空间使用统计。可以使用命令 ceph pg dump 来获取 PG Map。
这里 有段代码可以以表格形式显示这些映射关系:
ceph pg dump | awk '
/^pg_stat/ { col=1; while($col!="up") {col++}; col++ }
/^[0-9a-f]+\.[0-9a-f]+/ { match($0,/^[0-9a-f]+/); pool=substr($0, RSTART, RLENGTH); poollist[pool]=0; up=$col; i=0; RSTART=0; RLENGTH=0; delete osds; while(match(up,/[0-9]+/)>0) { osds[++i]=substr(up,RSTART,RLENGTH); up = substr(up, RSTART+RLENGTH) } for(i in osds) {array[osds[i],pool]++; osdlist[osds[i]];} } END { printf("\n"); printf("pool :\t"); for (i in poollist) printf("%s\t",i); printf("| SUM \n"); for (i in poollist) printf("--------"); printf("----------------\n"); for (i in osdlist) { printf("osd.%i\t", i); sum=0; for (j in poollist) { printf("%i\t", array[i,j]); sum+=array[i,j]; poollist[j]+=array[i,j] }; printf("| %i\n",sum) } for (i in poollist) printf("--------"); printf("----------------\n"); printf("SUM :\t"); for (i in poollist) printf("%s\t",poollist[i]); printf("|\n"); }'
(4)CRUSH (Controlled Replication under Scalable Hashing)Map:包含当前磁盘、服务器、机架等层级结构 (Contains a list of storage devices, the failure domain hierarchy (e.g., device, host, rack, row, room, etc.), and rules for traversing the hierarchy when storing data)。 要查看该 map 的话,先运行 ceph osd getcrushmap -o {filename} 命令,然后运行 crushtool -d {comp-crushmap-filename} -o {decomp-crushmap-filename} 命令,在vi 或者 cat {decomp-crushmap-filename} 即可。
因此,Ceph Admin 可以通过配置 CRUSH rules 来决定数据的存放方式。详细信息,可以参考 The RADOS object store and Ceph filesystem: Part 2
(5)MDS Map:包含当前所有 MSD 的状态 (the current MDS map epoch, when the map was created, and the last time it changed. It also contains the pool for storing metadata, a list of metadata servers, and which metadata servers are up and in)。通过执行 ceph mds dump 获取。
root@ceph1:~# ceph mds dump dumped mdsmap epoch 13 epoch 13 flags 0 created 2015-09-18 02:16:19.504427 modified 2015-09-18 08:05:55.438558 4171: 9.115.251.194:6800/962 'ceph1' mds.0.2 up:active seq 7 5673: 9.115.251.195:6800/959 'ceph2' mds.-1.0 up:standby seq 1
Ceph 架构中,Ceph client 是直接读或者写存放在 RADOS 对象存储中的对象(data object)的,因此,Ceph 需要走完 (Pool, Object) → (Pool, PG) → OSD set → Disk 完整的链路,才能让 ceph client 知道目标数据 object:
(1)Ceph client 通过 CRUSH 算法计算出存放 object 的 PG 的 ID:
也就是:PG-id = hash(pool-id). hash(objet-id) % PG-number
(2)通过 CRUSH 算法计算出将 object 保存到 PG 中哪个 OSD 上。
每个 Pool 包含一定数量的 PG。CRUSH 算法动态地将 PG 映射到 OSD。这种映射在 Ceph client 和 OSD Daemon 之间建立了一个间接层。这是因为,Ceph 集群需要能够膨胀(增加 OSD)或者缩小(减少 OSD),以及再平衡(rebalance)。如果 Ceph client 知道 OSD Daemon 上保存了哪个对象,那么两者之间就建立了一个强绑定关系。因此,Ceph 使用 CRUSH 算法,得以动态地将每个 object 映射到 PG,以及将 PG 映射到 OSD。
对 Ceph client 来说,只要它获得了 Cluster map,就可以使用 CRUSH 算法计算出某个 object 所在的 OSD。
(a)Ceph client 从 MON 获取最新的 cluster map,它包含所有的 monitors, OSDs, 和 metadata servers。
(b)Ceph client 根据上面的第(1)步计算出该 object 所在的 PG 的 ID。
(c)Ceph client 再根据 CRUSH 算法计算出 PG 中目标主和次 OSD 的 ID。
(d)Ceph client 向主和次 OSD 写入二进制数据块。
也就是:OSD-id = CURSH(cursh-rules, cluster-map, PG-id)
以存放一个文件为例,下图(来源)说明了完整的计算过程:
CRUSH 算法是相当复杂,快速看看的话可以参考 官方文章,或者直接读代码和作者的论文。几个简单的结论或原则:
使用 这里 的脚本,可以看出在我的环境中,6 个pool,7 个 OSD,每个 pool 中有 192 个PG,每个 OSD 在128+ PG 中:
pool : 4 5 0 1 2 3 | SUM ---------------------------------------------------------------- osd.4 20 19 21 23 21 24 | 128 osd.5 38 28 30 28 34 44 | 202 osd.6 30 33 33 32 34 36 | 198 osd.7 23 19 22 21 22 21 | 128 osd.8 26 36 34 36 30 20 | 182 osd.9 21 26 21 20 21 19 | 128 osd.3 34 31 31 32 30 28 | 186 ---------------------------------------------------------------- SUM : 192 192 192 192 192 192 |
这张图(来源)也有助于理清其中的关系:
总之,Ceph 采用的是通过计算得出对象的位置,这比通过常见的查询算法获取位置快得多。CRUSH 算法是的 ceph 客户端自己计算对象要被保存在哪里(哪个 OSD),也使得客户端可以从主 OSD 上保存或者读取数据。
如下图所示,Ceph 系统中不同层次的组件/用户所看到的数据的形式是不一样的:
Ceph client 向一个 RBD image 写入二进制数据(假设 pool 的拷贝份数为 3):
(1)Ceph client 调用 librados 创建一个 RBD image,这时候不会做存储空间分配,而是创建若干元数据对象来保存元数据信息。
(2)Ceph client 调用 librados 开始写数据。librados 计算条带、object 等,然后开始写第一个 stripe 到特定的目标 object。
(3)librados 根据 CRUSH 算法,计算出 object 所对应的主 OSD ID,并将二进制数据发给它。
(4)主 OSD 负责调用文件系统接口将二进制数据写入磁盘上的文件(每个 object 对应一个 file,file 的内容是一个或者多个 stripe)。
(5)主 ODS 完成数据写入后,它使用 CRUSH 算啊计算出第二个OSD(secondary OSD)和第三个OSD(tertiary OSD)的位置,然后向这两个 OSD 拷贝对象。都完成后,它向 ceph client 反馈该 object 保存完毕。
(6)然后写第二个条带,直到全部写入完成。全部完成后,librados 还应该会做元数据更新,比如写入新的 size 等。
完整的过程(来源):
该过程具有强一致性的特点:
在 RADOS 层,Ceph 本身没有条带的概念,因为一个object 是作为一个 文件整体性保存的。但是,RBD 可以控制向一个 object 的写入方式,默认是将一个 object 写满再去写下一个object;还可以通过指定 stripe_unit 和 stripe_count,来将 object 分成若干个条带即 strip。
参考链接:
http://www.wzxue.com/ceph-librbd-block-library/
http://docs.ceph.com/docs/master/architecture/
https://www.ustack.com/blog/ceph_infra/
https://hustcat.github.io/rbd-image-internal-in-ceph/
http://www.wzxue.com/category/ceph-2/