zoukankan      html  css  js  c++  java
  • ceph之crush算法示例

    root@mon0:~# ceph osd tree

    # id    weight  type name       up/down reweight

    -1      0.05997 root default

    -2      0.02998         host osd0

    1       0.009995                        osd.1   up      1

    2       0.009995                        osd.2   up      1

    3       0.009995                        osd.3   up      1

    -3      0.02998         host osd1

    5       0.009995                        osd.5   up      1

    6       0.009995                        osd.6   up      1

    7       0.009995                        osd.7   up      1

    存储节点

    在进一步之前,先考虑这点:Ceph是分布式存储系统,不管其“分布式逻辑”的细节,数据最终是要存储到设备上。

    此时有两种选择:直接操作设备,或者由本地文件系统代理。前者表示直接面对硬盘,怎么在硬盘上组织数据都是自己做。后者表示不直接面对硬盘,而是使用已有的文件系统。Ceph使用第二种方式,可选的本地文件系统有ext4、btrfs和xfs等,我们使用ext4.

    crush算法

    设想你是用户,你有一部动作片要存放到Ceph集群中。你拿着你的笔记本来到机房,你所看到的Ceph集群是数个机架的服务器。你在想,我的动作片最终是存储在哪里?

    你考虑的实际是数据定位的问题。

    常见有两种数据定位方法:

    1. 记录。将“数据A:位置location(A)”这样的信息记录下来,访问数据时查询记录,获取位置,再进行读取。
    2. 计算。存放和数据A时,其存储位置location(A)是即时计算得到的。能感觉到这种方式更为便利。

    常见的计算方式是一致性哈希(consistent hashing),GlusterFS使用的是这种方式,基本思路是,面对数据A,以数据A的文件名等类似信息为key,通过一致性哈希计算consist_hash(keyA) = location(A)得到存储位置。

    ceph使用的是crush算法:Controlled, Scalable, Decentralized Placement of Replicated Data。crush是ceph核心之一,本文也将重点描述。

    简单来说crush也利用哈希来计算位置,只不过它更多地利用了集群的结构信息。下面通过实例来尝试理解。

    前文提到rbd、cephfs和rgw这三种使用场景,它们都基于RADOS层。RADOS层对外提供有librados接口,据此可以实现自己的工具。ceph默认提供一个程序rados,通过rados可以直接上传一个object到ceph集群。

    mon0# rados put bigfile bigfile.data -p rbd // 将数据bigfile.data上传为object bigfile

    mon0# rados ls -p rbd

    bigfile

    bigfile的存储位置是通过CRUSH计算得来,ceph提供命令可查询一个object的位置。

    mon0# ceph osd map rbd bigfile

    osdmap e67 pool 'rbd' (1) object 'bigfile' -> pg 1.a342bdeb (1.6b) -> up ([6,3], p6) acting ([6,3], p6)

    [6,3]表示bigfile这个object存储在osd.6和osd.3上(ceph集群部署参考前文),并且放置在pg 1.6b下,也即放在目录1.6b下。

    验证这一点:

    osd0# ls /var/lib/ceph/osd/ceph-6/current/1.6b_head/ -lh

    total 61M

    -rw-r--r-- 1 root root 61M 10月 29 08:12 bigfile__head_A342BDEB__1 // 感受下object的命名方式

    pg (placement group)

    上例中提到pg,这是ceph crush数据映射的一个中间层。

    讨论pg需要提及pool,pool是ceph的一个逻辑概念,用户可在ceph集群上创建数个pool,设置不同的属性,然后根据需求,将不同的数据放在不同的pool中。比如我有两种数据,一种只需要存储2个备份,另一种更重要,我需要存储3个备份,从而可以创建两个pool,设置size=2和size=3。

    我的实验环境中只有一个pool,命名为rbd(这个命名是随意的,不要和rbd块设备使用场景混淆),其备份数目是2份:

    mon0# ceph osd dump | grep pool

    pool 1 'rbd' replicated size 2 min_size 2 crush_ruleset 0 object_hash rjenkins pg_num 128 pgp_num 128 last_change 48 flags hashpspool stripe_width 0

    ceph在pool的概念下提供placement group的概念,并通过参数pg_num指定placement group的数目。从上面的输出可知我的rbd pool下有128个pg。

    pg实际对应目录,rbd pool下有128个pg,意即rbd pool设置为有128个目录来存放数据。之所以增加pg层来管理数据,是为了数据管理的便利,也减少了元数据信息量。 

    rbd pool128个pg,每个pg放置在哪些osd上,这也是计算确定的,计算过程是crush算法的一部分。128个pg整体分散存储在所有osd上,因此在某个osd上,我们不会看到所有128个目录。

    1.11_head  1.1f_head  1.27_head  1.33_head  1.39_head  1.42_head  1.4c_head  1.54_head  1.60_head  1.66_head  1.74_head  1.79_head  1.7e_head  1.8_head  1.b_head  1.f_head       nosnap

    1.15_head  1.21_head  1.2e_head  1.36_head  1.3_head   1.44_head  1.4f_head  1.56_head  1.61_head  1.6a_head  1.75_head  1.7b_head  1.7f_head  1.9_head  1.d_head  commit_op_seq  omap

    1.1b_head  1.26_head  1.2f_head  1.37_head  1.41_head  1.45_head  1.50_head  1.5a_head  1.63_head  1.73_head  1.78_head  1.7c_head  1.7_head   1.a_head  1.e_head  meta

    说明:pg的命名方式是${pool_id}.${pg.id}_${snap},pgid是16进制值,如1.a_head表示128个pg中的第10个pg。

    因此crush数据定位算法的大致逻辑是:

    • step 1: 输入object id,计算得到它应该被放置在哪个pg下,得到pgid。
    • step 2: 输入pgid,计算pg位于哪些osd上。
    • step 3: 访问object.

    crush计算示例

    我们想在这个理解基础上,来了解更多代码细节,考虑到ceph貌似没有提供访问crush算法的接口,我们在ceph0.86的代码上做些修改,让librados提供ceph集群的layout信息,同时将crush代码拎出来做成libcrush,然后利用修改过的librados和libcrush写程序验证crush算法的步骤。

     crush-tester.cc main()展示了数据定位的步骤,其逻辑等同于"ceph osd map"的逻辑。需注意:代码是强依赖于我们前文部署的ceph环境的。

    int main(int argc, char **argv) { assert(argc == 2); string objname = argv[argc-1]; // 假设objname=bigfile,这一步是通过objname计算得到一个数值,和pg其实没有关系,但官方代码就是这么命名的,我们这里也做类似命名 // bigfile->some_value,这里利用的算法是ceph_str_hash_rjenkins,算法这里不细究,其实也无必要细究。 // 其作用是将不同的objname映射层不同的数值,相信其特点是得到的数值冲突率很低。 pg_t pg = object_to_pg(object_t(objname), object_locator_t(g_pool_id)); printf("objectr_to_pg: %s -> %x
    ", objname.c_str(), pg.seed); // bigfile得到的值是a342bdeb,作为这一步的输入。 // 这一步的作用是将这个数值,也即将object bigfile映射到具体的pg。 // 用到的方法其实是取模,根据pg_num做取模计算,对应函数是crush_stable_mod(a342bdeb, 128, 127)=6b。 pg_t mpg = raw_pg_to_pg(pg); printf("raw_pg_to_pg: %x
    ", mpg.seed); // 这一步是crush算法的核心步骤, // 输入是pgid=6b,以及ceph集群的layout,也就是crushmap, // 输出是6b的存储位置是哪两个osd(因为备份数设置size=2), // crush算法的过程在libcrush中,注意这样链接本程序"-lcrush -lrados",从而使用libcrush而不是librados中的crush代码,这样我们通过介入libcrush去理解crush算法。 // crush算法计算6b的位置的过程见下文。 printf("pg_to_osds:
    "); vector<int> up; pg_to_up_acting_osds(mpg, &up); } 

    # g++ crush_tester.cc -o test_crush -lcrush -lrados --std=c++11 -g -O0

    # ./test_crush bigfile

    objectr_to_pg: bigfile -> a342bdeb

    raw_pg_to_pg: 6b

    pg_to_osds:

     osd_weight: 0,65536,65536,65536,0,65536,65536,65536,  // 这一输出我觉得奇怪,感觉是错误的,但偏偏最后crush映射的结果是正确的

     ruleno: 0

     placement_ps: 1739805228

    // 这是crush算法计算6b位置的输出,注意计算依据之一是我们实验环境的crushmap,

    // 有数种方法可以从一个live ceph cluster中得到当前的crushmap,我们环境的crushmap见:https://gist.github.com/xanpeng/a41a25b5810cb2c8852c#file-ceph-env-txt

    // 通过crushmap可以知道,从crush算法的视角,我们的环境是这样分层的:

    // 第一层:root

    // id=-1,alg=straw,hash=rjenkins1,包含两个item [osd0, osd1]

    // 第二层:host

    // host osd0: id=-2, alg=straw, hash=rjenkins1,包含三个item [osd.1, osd.2, osd.3]

    // host osd1: id=-3, alg=straw, hash=rjenkins1,包含三个item [osd.5, osd.6, osd.7],之所以不是4,5,6,表示我操作过程中出过点点纰漏,但无碍。

    // 第三层:osd

    // 共6个osd,id是[1,2,3,5,6,7],每个osd有个权重,通过设置osd的权重,影响数据是否存放在当前osd的偏好。

    ---start crush_do_rule---

    CHOOSE_LEAF bucket -1 x 1739805228 outpos 0 numrep 2 tries 51 recurse_tries 1 local_retries 0 local_fallback_retries 0 parent_r 0

    // 算法第一步,从第一层开始,获取第一个数据备份的存放位置,判断我该进入第二层的哪个host。

    // 由于第一层设置的alg=straw,表示用straw算法去选取第二层的host,straw算法又利用到rjenkins1哈希算法

    // 得到第一个备份存放位置是item -3上,即osd1上

    crush_bucket_choose -1 x=1739805228 r=0

     item -3 type 1

    // 算法第二步,在osd1下的第三层中选一个osd,

    // 使用的算法是osd1这个bucket设置的straw+rjenkins1,

    // 这是crush算法过程中的第二次哈希。我们的实验环境简单,layout只有三层,在层次更多的环境中,crush需要做更多次数的哈希。

    // 得到第一个备份存放位置是osd1上的osd.6

    CHOOSE bucket -3 x 1739805228 outpos 0 numrep 1 tries 1 recurse_tries 0 local_retries 0 local_fallback_retries 0 parent_r 0

    crush_bucket_choose -3 x=1739805228 r=0

     item 6 type 0

    CHOOSE got 6

    CHOOSE returns 1

    CHOOSE got -3

    // 算法第三步,计算第二个数据备份的位置,求得位于osd0

    crush_bucket_choose -1 x=1739805228 r=1

     item -2 type 1

    // 算法第四步,同理进而求得第二个备份的位置:osd0上的osd.3

    CHOOSE bucket -2 x 1739805228 outpos 1 numrep 2 tries 1 recurse_tries 0 local_retries 0 local_fallback_retries 0 parent_r 0

    crush_bucket_choose -2 x=1739805228 r=1

     item 3 type 0

    CHOOSE got 3

    CHOOSE returns 2

    CHOOSE got -2

    CHOOSE returns 2

    ---finish crush_do_rule---


     numrep: 2, raw_osds: [6,3,]

    对比执行多次crush-tester和"ceph osd map",证实上述理解应为正确

  • 相关阅读:
    戏说程序猿之荒唐的需求
    戏说程序猿之过年--二叔,我真不会修电脑
    深入理解设计模式(17):迭代器模式
    深入理解设计模式(16):备忘录模式
    我的2018
    Java高级篇(一)——线程
    Java进阶篇(六)——Swing程序设计(下)
    Java进阶篇(六)——Swing程序设计(上)
    Java进阶篇(五)——Java的I/O技术
    Java进阶篇(四)——Java异常处理
  • 原文地址:https://www.cnblogs.com/chris-cp/p/4589161.html
Copyright © 2011-2022 走看看