0%

说到Ceph的通讯一定绕不开Messenger,无论是客户端到OSD,还是OSD到MON,或者OSD到OSD,都需要Messenger来协助完成各个模块间消息的发送、接收。Messenger有三种实现,分别是SimpleMessenger、AsyncMessenger、XioMessenger,本文以AsyncMessenger为例简单介绍一下其工作原理。

原理

asyncmessenger

  • processors线程数量由cct->_conf->ms_async_op_threads决定
  • NetworkStackworkersprocessors一一对应。
  • processors收到请求会调创建AsyncConnection,并存入调用AsyncConnection实例的accept方法,accept通过EventCenter将由NetworkStackworkers调用AsyncConnection实例的process方法。(哈哈哈,绕吧,有点儿晕了吧~~~)
  • processors处理完accept请求后,将AsyncConnection实例存入accepting_conns,等待NetworkStack处理完成。
  • AsyncConnectionprocessNetworkStackworkers线程调用,并构建Message消息通过dispatch_queuems_fast_dispatch发送到fast_dispatchers

Message 格式

CEPH_MSGR_TAG_MSG

message_format

  • tag
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    #define CEPH_MSGR_TAG_READY         1  /* server->client: ready for messages */
    #define CEPH_MSGR_TAG_RESETSESSION 2 /* server->client: reset, try again */
    #define CEPH_MSGR_TAG_WAIT 3 /* server->client: wait for racing incoming connection */
    #define CEPH_MSGR_TAG_RETRY_SESSION 4 /* server->client + cseq: try again with higher cseq */
    #define CEPH_MSGR_TAG_RETRY_GLOBAL 5 /* server->client + gseq: try again with higher gseq */
    #define CEPH_MSGR_TAG_CLOSE 6 /* closing pipe */
    #define CEPH_MSGR_TAG_MSG 7 /* message */
    #define CEPH_MSGR_TAG_ACK 8 /* message ack */
    #define CEPH_MSGR_TAG_KEEPALIVE 9 /* just a keepalive byte! */
    #define CEPH_MSGR_TAG_BADPROTOVER 10 /* bad protocol version */
    #define CEPH_MSGR_TAG_BADAUTHORIZER 11 /* bad authorizer */
    #define CEPH_MSGR_TAG_FEATURES 12 /* insufficient features */
    #define CEPH_MSGR_TAG_SEQ 13 /* 64-bit int follows with seen seq number */
    #define CEPH_MSGR_TAG_KEEPALIVE2 14
    #define CEPH_MSGR_TAG_KEEPALIVE2_ACK 15 /* keepalive reply */
    #define CEPH_MSGR_TAG_CHALLENGE_AUTHORIZER 16 /* ceph v2 doing server challenge */
    ** 我觉得注释处的说明写的很清楚了,此处不做过多说明了。 **
  • header
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    struct ceph_msg_header {
    __le64 seq; /* message seq# for this session */
    __le64 tid; /* transaction id */
    __le16 type; /* message type */
    __le16 priority; /* priority. higher value == higher priority */
    __le16 version; /* version of message encoding */

    __le32 front_len; /* bytes in main payload */
    __le32 middle_len;/* bytes in middle payload */
    __le32 data_len; /* bytes of data payload */
    __le16 data_off; /* sender: include full offset;
    receiver: mask against ~PAGE_MASK */

    struct ceph_entity_name src;

    /* oldest code we think can decode this. unknown if zero. */
    __le16 compat_version;
    __le16 reserved;
    __le32 crc; /* header crc32c */
    } __attribute__ ((packed));
  • payload
    *** 未知 ***
  • middle
    *** 未知 ***
  • data
    具体传递的数据内容,数据大小由header中的data_len决定。
  • footer/old_footer
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    /*
    * follows data payload
    * ceph_msg_footer_old does not support digital signatures on messages PLR
    */
    struct ceph_msg_footer_old {
    __le32 front_crc, middle_crc, data_crc;
    __u8 flags;
    } __attribute__ ((packed));

    struct ceph_msg_footer {
    __le32 front_crc, middle_crc, data_crc;
    // sig holds the 64 bits of the digital signature for the message PLR
    __le64 sig;
    __u8 flags;
    } __attribute__ ((packed));
    footerold_footer之差一个签名sig

分布式系统在极大提高可用性、容错性的同时,带来了一致性问题(CAP理论)。Raft算法能够解决分布式系统环境下的一致性问题。一致性是分布式系统容错的基本问题。一致性涉及多个服务器状态(Values)达成一致。 一旦他们就状态做出决定,该决定就是最终决定。 当大多数服务器可用时,典型的一致性算法会取得进展。

raft是工程上使用较为广泛的强一致性、去中心化、高可用的分布式协议。在这里强调了是在工程上,因为在学术理论界,最耀眼的还是大名鼎鼎的Paxos。但Paxos是:少数真正理解的人觉得简单,尚未理解的人觉得很难,大多数人都是一知半解。本人也花了很多时间、看了很多材料也没有真正理解。直到看到raft的论文,两位研究者也提到,他们也花了很长的时间来理解Paxos,他们也觉得很难理解,于是研究出了raft算法。

Leader选举

raft协议中,一个节点任一时刻处于leader, follower, candidate三个角色之一。

  • leader 接受客户端请求,并向Follower同步请求日志,当日志同步到大多数节点上后告诉Follower提交日志。
  • follower 接受并持久化Leader同步的日志,在Leader告之日志可以提交之后,提交日志。
  • candidate Leader选举过程中的临时角色。

election_state

每个节点以follower角色开始,如果follower超时没有收到leader的消息,它会进入candidate角色,并发起选举投票。如果candidate收到的票数超过半数以上,则切换为leader角色。如果发现其他节点比自己更新,则主动切换到follower。总之,系统中最多只有一个leader,如果在一段时间里发现没有leader,则大家通过选举-投票选出leaderleader会不停的给follower发心跳消息,表明自己的存活状态。如果leader故障,那么follower会转换成candidate,重新选出leader

election_term

leader是大家投票选举出来的,每个leader工作一段时间,然后选出新的leader继续负责。这根民主社会的选举很像,每一届新的履职期称之为一届任期,在raft协议中,也是这样的,对应的术语叫term。term(任期)以选举(election)开始,然后就是一段或长或短的稳定工作期(normal Operation)。从上图可以看到,任期是递增的,这就充当了逻辑时钟的作用;另外,term 3展示了一种情况,就是说没有选举出leader就结束了,然后会发起新的选举。

选举过程

正常情况下选举

5个节点一开始的状态都是Follower
election_flow_normal_1

在一个节点倒计时结束(Timeout) 后,这个节点的状态变成Candidate开始选举,它给其他几个节点发送选举请求(RequestVote)
election_flow_normal_2

其他四个节点都返回成功,这个节点的状态由Candidate变成了Leader,并在每个一小段时间后,就给所有的Follower发送一个 Heartbeat 以保持所有节点的状态,Follower收到Leader的Heartbeat后重设Timeout。
election_flow_normal_3

只要有超过一半的节点投支持票了,Candidate才会被选举为Leader,5个节点的情况下,3个节点 (包括Candidate本身) 投了支持就行。

Leader 出故障情况下的选举

election_flow_error_leader_1

leader出故障挂掉了,其他四个follower将进行重新选主。
election_flow_error_leader_2

4个节点的选主过程和5个节点的类似,在选出一个新的leader后,原来的Leader恢复了又重新加入了,这个时候怎么处理?在Raft里,第几轮选举是有记录的,重新加入的Leader是第一轮选举(Term 1)选出来的,而现在的Leader则是Term 2,所有原来的Leader会自觉降级为Follower
election_flow_error_leader_3
election_flow_error_leader_4
election_flow_error_leader_5
election_flow_error_leader_6

多个Candidate情况下的Leader选举

election_flow_mult_cand_1

有两个Follower同时Timeout,都变成了Candidate开始选举,分别给一个Follower发送了投票请求。
election_flow_mult_cand_2

两个Follower分别返回了ok,这时两个Candidate都只有2票,要3票才能被选成Leader
election_flow_mult_cand_3

两个Candidate会分别给另外一个还没有给自己投票的Follower发送投票请求。
election_flow_mult_cand_4

但是因为Follower在这一轮选举中,都已经投完票了,所以都拒绝了他们的请求。所以在Term 2没有Leader被选出来。
election_flow_mult_cand_5

这时,两个节点的状态是Candidate,两个是Follower,但是他们的倒计时器仍然在运行,最先Timeout的那个节点会进行发起新一轮Term 3的投票。
election_flow_mult_cand_6

两个Follower在Term 3还没投过票,所以返回OK,这时Candidate一共有三票,被选为了Leader
election_flow_mult_cand_7

如果Leader Heartbeat的时间晚于另外一个Candidate timeout的时间,另外一个Candidate仍然会发送选举请求。
election_flow_mult_cand_8

两个Follower已经投完票了,拒绝了这个Candidate的投票请求。
election_flow_mult_cand_9

Leader进行Heartbeat,Candidate收到后状态自动转为Follower,完成选举。
election_flow_mult_cand_10

日志复制

Raft 在实际应用场景中的一致性更多的是体现在不同节点之间的数据一致性,客户端发送请求到任何一个节点都能收到一致的返回,当一个节点出故障后,其他节点仍然能以已有的数据正常进行。在选主之后的复制日志就是为了达到这个目的。

log_replication_normal

正常情况下日志复制过程

log_replication_normal_step_1

客户端发送请求给Leader,储存数据 “sally”,Leader先将数据写在本地日志,这时候数据还是Uncommitted (还没最终确认,红色表示)
log_replication_normal_step_2

Leader给两个Follower发送AppendEntries请求,数据在Follower上没有冲突,则将数据暂时写在本地日志,Follower的数据也还是Uncommitted。
log_replication_normal_step_3

Follower将数据写到本地后,返回OK。Leader收到后成功返回,只要收到的成功的返回数量超过半数(包含Leader),Leader将数据 “sally” 的状态改成Committed。( 这个时候Leader就可以返回给客户端了)
log_replication_normal_step_4

Leader再次给Follower发送AppendEntries请求,收到请求后,Follower将本地日志里Uncommitted数据改成Committed。这样就完成了一整个复制日志的过程,三个节点的数据是一致的
log_replication_normal_step_5

Network Partition情况下日志复制过程

在Network Partition的情况下,部分节点之间没办法互相通信,Raft 也能保证在这种情况下数据的一致性。

log_replication_np_1

Network Partition将节点分成两边,一边有两个节点,一边三个节点。
log_replication_np_2

两个节点这边已经有Leader了,来自客户端的数据“bob”通过Leader同步到Follower
log_replication_np_3

因为只有两个节点,少于3个节点,所以“bob”的状态仍是Uncommitted。所以在这里,服务器会返回错误给客户端
log_replication_np_4

另外一个Partition有三个节点,进行重新选主。客户端数据“tom”发到新的Leader,通过和上节网络状态下相似的过程,同步到另外两个Follower
log_replication_np_5

因为这个Partition有3个节点,超过半数,所以数据“tom”都Commit了。
log_replication_np_6
log_replication_np_7
log_replication_np_8

网络状态恢复,5个节点再次处于同一个网络状态下。但是这里出现了数据冲突“bob”和“tom”
log_replication_np_9

三个节点的Leader广播AppendEntries
log_replication_np_10

两个节点Partition的Leader自动降级为Follower,因为这个Partition的数据 “bob” 没有Commit,返回给客户端的是错误,客户端知道请求没有成功,所以Follower在收到AppendEntries请求时,可以把“bob“删除,然后同步”tom”,通过这么一个过程,就完成了在Network Partition情况下的复制日志,保证了数据的一致性。
log_replication_np_11
log_replication_np_12

安全性

Election safety

选举安全性,即任一任期内最多一个leader被选出。这一点非常重要,在一个复制集中任何时刻只能有一个leader。系统中同时有多余一个leader,被称之为脑裂(brain split),这是非常严重的问题,会导致数据的覆盖丢失。

  • 一个节点某一任期内最多只能投一票
  • 只有获得多数票的节点才会成为leader

log matching

如果两个节点上的某个log entry的log index相同且term相同,那么在该index之前的所有log entry应该都是相同的。在没有异常的情况下,log matching是很容易满足的,但如果出现了node crash,情况就会变得复杂了。

safety_log_matching
*** 上图的a-f不是6个follower,而是某个follower可能存在的六个状态 ***

  • leader日志少: a,b
  • leader日志多:c,d
  • 某些位置比leader多,某些日志比leader少:e,f

当出现了leaderfollower不一致的情况,leader强制follower复制自己的log。

leader completeness

  • 一个日志被复制到多数节点才算committed
  • 一个节点得到多数的投票才能成为leader,而节点A给节点B投票的其中一个前提是,B的日志不能比A的日志旧

State Machine Safety

如果节点将某一位置的log entry应用到了状态机,那么其他节点在同一位置不能应用不同的日志。简单点来说,所有节点在同一位置(index in log entries)应该应用同样的日志。

safety_state_machine

  • (a)时刻, s1是leader,在term2提交的日志只赋值到了s1 s2两个节点就crash了。
  • (b)时刻, s5成为了term 3的leader,日志只赋值到了s5,然后crash。
  • (c)时刻,s1又成为了term 4的leader,开始赋值日志,于是把term2的日志复制到了s3,此刻,可以看出term2对应的日志已经被复制到了多数节点上,因此是committed,可以被状态机应用。
  • (d)时刻,s1又crash了,s5重新当选,然后将term3的日志复制到所有节点,这就出现了一种奇怪的现象:被复制到大多数节点(或者说可能已经应用)的日志被回滚。

因为term4时的leader s1在(c)时刻提交了之前term2任期的日志。某个leader选举成功之后,不会直接提交前任leader时期的日志,而是通过提交当前任期的日志的时候“顺手”把之前的日志也提交了,具体怎么实现了,在log matching部分有详细介绍。那么问题来了,如果leader被选举后没有收到客户端的请求呢,论文中有提到,在任期开始的时候发立即尝试复制、提交一条空的log。

因此,在上图中,不会出现(C)时刻的情况,即term4任期的leader s1不会复制term2的日志到s3。而是如同(e)描述的情况,通过复制-提交term4的日志顺便提交term2的日志。如果term4的日志提交成功,那么term2的日志也一定提交成功,此时即使s1 crash,s5也不会重新当选。

参考&鸣谢

在数据中心领域,远程直接内存访问(英语:remote direct memory access,RDMA)是一种绕过远程主机操作系统内核访问其内存中数据的技术,由于不经过操作系统,不仅节省了大量CPU资源,同样也提高了系统吞吐量、降低了系统的网络通信延迟,尤其适合在大规模并行计算机集群中有广泛应用。在基于NVMe over Fabric的数据中心中,RDMA可以配合高性能的NVMe SSD构建高性能、低延迟的存储网络。

Red Hat和甲骨文公司等软件供应商已经在其最新产品中支持这些API,截至2013年,工程师也已开始开发基于以太网的RDMA网络适配器。Red Hat Enterprise Linux和Red Hat Enterprise MRG已支持RDMA。微软已在Windows Server 2012中通过SMB Direct支持RDMA。

RDMA 原理

传统的基于Socket套接字(TCP/IP协议栈)的网络通信需要经过操作系统协议栈。数据在系统中搬来搬去,因此占用了大量的CPU和内存资源,也加大了网络延时。RDMA解决了传统Socket通信的痛点,采用了Kernel Bypass的工作方式,减少了CPU和内存的占用,也降低了网络延时。

rdma_theory

目前RDMA有三种不同的硬件实现,分别是InfiniBand、iWARP(internet wide area RDMA Protocol)、RoCE(RDMA over Coverged Ethernet)。

rdma_theory_2

  • Infiniband
    支持RDMA的新一代网络协议。 由于这是一种新的网络技术,因此需要支持该技术的NIC和交换机。
  • RoCE
    一个允许在以太网上执行RDMA的网络协议。 其较低的网络标头是以太网标头,其较高的网络标头(包括数据)是InfiniBand标头。 这支持在标准以太网基础设施(交换机)上使用RDMA。 只有网卡应该是特殊的,支持RoCE。
    RoCE v1是一种链路层协议,允许在同一个广播域下的任意两台主机直接访问。
    RoCE v2是一种Internet层协议,即可以实现路由功能。
  • iWARP
    一个允许在TCP上执行RDMA的网络协议。 IB和RoCE中存在的功能在iWARP中不受支持。 这支持在标准以太网基础设施(交换机)上使用RDMA。

关键概念

** QP(Queue Pair) **

每对QP由Send Queue(SQ)和Receive Queue(RQ)构成,这些队列中管理着各种类型的消息。QP会被映射到应用的虚拟地址空间,使得应用直接通过它访问RNIC网卡。

** CQ(Complete Queue) **

  1. 完成队列包含了发送到工作队列(WQ)中已完成的工作请求(WR)。每次完成表示一个特定的 WR执行完毕(包括成功完成的WR和不成功完成的WR)。完成队列是一个用来告知应用程序已结束的工作请求的信息(状态、操作码、大小、来源)的机制。
  2. CQ有n个完成队列实体(CQE)。CQE的数量在CQ创建的时候被指定。
  3. 当一个CQP被轮询到,它就从CQ中被删除。
  4. CQ是一个CQE的先进选出(FIFO)队列。
  5. CQ能服务于发送队列、接收队列或者同时服务于这两种队列。多个不同QP中的工作请求(WQ)可联系到同一个CQ上。

** MR(Memory Region) **

  1. 内存注册机制允许应用程序申请一些连续的虚拟内存空间或者连续的物理内存空间,将这些内存空间提供给网络适配器作为虚拟的连续缓冲区,缓冲区使用虚拟地址。
  2. 内存注册进程锁定了内存页。(为了防止页被替换出去,同时保持物理和虚拟内存的映射)在注册期间,操作系统检查被注册块的许可。注册进程将虚拟地址与物理地址的映射表写入网络适配器。在注册内存时,对应内存区域的权限会被设定。权限包括本地写、远程读、远程写、原子操作、绑定。
  3. 每个内存注册(MR)有一个远程的和一个本地的key(r_key,l_key)。本地key被本地的HCA 用来访问本地内存,例如在接收数据操作的期间。远程key提供给远程HCA用来在RDMA操作期间允许远程进程访问本地的系统内存。同一内存缓冲区可以被多次注册(甚至设置不同的操作权限),并且每次注册都会生成不同的key。

** HCA **

  1. Opening an HCA 打开HCA,准备好HCA供消费者使用。一旦打开了一个HCA设备,只有关闭它以后,才能再次打开。
  2. HCA属性 HCA属性是设备特征,这些属性必须可以被消费者获取。
  3. 修改HCA属性 HCA允许修改一组==受限制的==HCA属性。这些可以修改的属性主要是性能信息和错误计数器管理性息。其他大部分属性或是不可修改的,或是通过General Services Interface / Fabric Management Interface进行操作。
  4. 关闭HCA 将HCA恢复到初始条件下,同时注销打开HCA时分配的资源。

** 寻址 **

  1. 源端地址 CI(Channel Interface)需要存储每个HCA有效的LID和GID。
  2. 目的地址 对于RC服务类型来说,目的地址被保存在本地QP的属性中。
  3. Loopback 由于自寻址的需要,HCA需要支持Loopback。Loopback仅支持于一个HCA中,同一个端口下的QP之间进行。

** Protection Domain **

  1. PD通过在QP/SRQ与MR之间建立联系,获得HCA访问主存的权限。此外,PD还可以用来关联QP和未绑定的内存窗口,用来控制HCA访问主系统内存。
  2. 分配保护域 当创建QP,注册MR,分配MW,创建Address Handle时需要分配PD。
  3. 释放保护域 如果PD仍然与任何队列对、内存区域、内存窗口、SRQ或地址句柄相关联,则不应释放它。如果尝试这样做,则谓词将立即返回一个错误。

RDMA 工作流程

  1. 当一个应用执行RDMA读或写请求时,不执行任何数据复制。在不需要任何内核内存参与的条件下,RDMA请求从运行在用户空间中的应用中发送到本地NIC(网卡)。
  2. NIC读取缓冲的内容,并通过网络传送到远程NIC。
  3. 在网络上传输的RDMA信息包含目标虚拟地址、内存钥匙和数据本身。请求既可以完全在用户空间中处理(通过轮询用户级完成排列) ,又或者在应用一直睡眠到请求完成时的情况下通过系统中断处理。RDMA操作使应用可以从一个远程应用的内存中读数据或向这个内存写数据。
  4. 目标NIC确认内存钥匙,直接将数据写人应用缓存中。用于操作的远程虚拟内存地址包含在RDMA信息中。

RDMA API

RDMA API (Verbs)主要有两种操作方式,One-Sided RDMA。包括RDMA Reads, RDMA Writes, RDMA Atomic。这种模式下的RDMA访问完全不需要远端机的任何确认;Two-Sided RDMA。包括RDMA Send, RDMA Receive。这种模式下的RDMA访问需要远端机CPU的参与。

Two-Side RDMA

  1. 首先,A和B都要创建并初始化好各自的QP,CQ
  2. A和B分别向自己的WQ中注册WQE,对于A,WQ=SQ,WQE描述指向一个等到被发送的数据;对于B,WQ=RQ,WQE描述指向一块用于存储数据的Buffer。
  3. A的RNIC异步调度轮到A的WQE,解析到这是一个SEND消息,从Buffer中直接向B发出数据。数据流到达B的RNIC后,B的WQE被消耗,并把数据直接存储到WQE指向的存储位置。
  4. AB通信完成后,A的CQ中会产生一个完成消息CQE表示发送完成。与此同时,B的CQ中也会产生一个完成消息表示接收完成。每个WQ中WQE的处理完成都会产生一个CQE。

** 双边操作与传统网络的底层Buffer Pool类似,收发双方的参与过程并无差别,区别在零拷贝、Kernel Bypass,实际上对于RDMA,这是一种复杂的消息传输模式,多用于传输短的控制消息。 **

RDMA通信流程

  1. 获取RDMA设备列表(ibv_get_device_list)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    /* 1 获取设备列表 */
    int num_devices;
    struct ibv_device **dev_list = ibv_get_device_list(&num_devices);
    if (!dev_list || !num_devices)
    {
    fprintf(stderr, "failed to get IB devices\n");
    rc = 1;
    goto main_exit;
    }
  2. 打开一个RDMA设备,获取一个上下文(ibv_open_device ibv_context)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    /* 2 打开设备,获取设备上下文 */
    struct ibv_device *ib_dev = dev_list[0];
    res.ib_ctx = ibv_open_device(ib_dev);
    if (!res.ib_ctx)
    {
    fprintf(stderr, "failed to open device \n");
    rc = 1;
    goto main_exit;
    }
  3. 释放RDMA设备列表占用的资源(ibv_free_device_list)
    1
    2
    3
    4
    /* 3 释放设备列表占用的资源 */
    ibv_free_device_list(dev_list);
    dev_list = NULL;
    ib_dev = NULL;
  4. 查询RDMA设备端口信息(ibv_query_port ibv_port_attr)
    1
    2
    3
    4
    5
    6
    7
    /* 4 查询设备端口状态 */
    if (ibv_query_port(res.ib_ctx, 1, &res.port_attr))
    {
    fprintf(stderr, "ibv_query_port on port failed\n");
    rc = 1;
    goto main_exit;
    }
  5. 分配一个Protection Domain (ibv_alloc_pd ibv_pd)
    1
    2
    3
    4
    5
    6
    7
    8
    /* 5 创建PD(Protection Domain) */
    res.pd = ibv_alloc_pd(res.ib_ctx);
    if (!res.pd)
    {
    fprintf(stderr, "ibv_alloc_pd failed\n");
    rc = 1;
    goto main_exit;
    }
  6. 创建一个Complete Queue (ibv_create_cq ibv_cq)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    /* 6 创建CQ(Complete Queue) */
    int cq_size = 10;
    res.cq = ibv_create_cq(res.ib_ctx, cq_size, NULL, NULL, 0);
    if (!res.cq)
    {
    fprintf(stderr, "failed to create CQ with %u entries\n", cq_size);
    rc = 1;
    goto main_exit;
    }
  7. 注册一块Memory Region (ibv_reg_mr ibv_mr)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    /* 7 注册MR(Memory Region) */
    int size = MSG_SIZE;
    res.buf = (char *)malloc(size);
    if (!res.buf)
    {
    fprintf(stderr, "failed to malloc %Zu bytes to memory buffer\n", size);
    rc = 1;
    goto main_exit;
    }
    memset(res.buf, 0, size);

    int mr_flags = IBV_ACCESS_LOCAL_WRITE | IBV_ACCESS_REMOTE_READ | IBV_ACCESS_REMOTE_WRITE;
    res.mr = ibv_reg_mr(res.pd, res.buf, size, mr_flags);
    if (!res.mr)
    {
    fprintf(stderr, "ibv_reg_mr failed with mr_flags=0x%x\n", mr_flags);
    rc = 1;
    goto main_exit;
    }
    fprintf(stdout, "MR was registered with addr=%p, lkey=0x%x, rkey=0x%x, flags=0x%x\n",
    res.buf, res.mr->lkey, res.mr->rkey, mr_flags);
  8. 创建一个Queue Pair (ibv_create_qp ibv_qp)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    /* 8 创建QP(Queue Pair) */
    struct ibv_qp_init_attr qp_init_attr;
    memset(&qp_init_attr, 0, sizeof(qp_init_attr));
    qp_init_attr.qp_type = IBV_QPT_RC;
    qp_init_attr.sq_sig_all = 1;
    qp_init_attr.send_cq = res.cq;
    qp_init_attr.recv_cq = res.cq;
    qp_init_attr.cap.max_send_wr = 1;
    qp_init_attr.cap.max_recv_wr = 1;
    qp_init_attr.cap.max_send_sge = 1;
    qp_init_attr.cap.max_recv_sge = 1;
    res.qp = ibv_create_qp(res.pd, &qp_init_attr);
    if (!res.qp)
    {
    fprintf(stderr, "failed to create QP\n");
    rc = 1;
    goto main_exit;
    }
    fprintf(stdout, "QP was created, QP number=0x%x\n", res.qp->qp_num);
  9. 交换控制信息 (使用Socket)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    /* 9 交换控制信息 */
    struct cm_con_data_t local_con_data; // 发送给远程主机的信息
    struct cm_con_data_t remote_con_data; // 接收远程主机发送过来的信息
    struct cm_con_data_t tmp_con_data;

    local_con_data.addr = htonll((uintptr_t)res.buf);
    local_con_data.rkey = htonl(res.mr->rkey);
    local_con_data.qp_num = htonl(res.qp->qp_num);
    local_con_data.lid = htons(res.port_attr.lid);
    if (sock_sync_data(server_ip, sizeof(struct cm_con_data_t), (char *)&local_con_data, (char *)&tmp_con_data) < 0)
    {
    fprintf(stderr, "failed to exchange connection data between sides\n");
    rc = 1;
    goto main_exit;
    }
    remote_con_data.addr = ntohll(tmp_con_data.addr);
    remote_con_data.rkey = ntohl(tmp_con_data.rkey);
    remote_con_data.qp_num = ntohl(tmp_con_data.qp_num);
    remote_con_data.lid = ntohs(tmp_con_data.lid);
    /* save the remote side attributes, we will need it for the post SR */
    res.remote_props = remote_con_data;
    fprintf(stdout, "Remote address = 0x%" PRIx64 "\n", remote_con_data.addr);
    fprintf(stdout, "Remote rkey = 0x%x\n", remote_con_data.rkey);
    fprintf(stdout, "Remote QP number = 0x%x\n", remote_con_data.qp_num);
    fprintf(stdout, "Remote LID = 0x%x\n", remote_con_data.lid);
  10. 转换QP状态(ibv_modify_qp)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    /* 10 转换QP状态 */
    // RESET -> INIT
    struct ibv_qp_attr attr;
    int flags;
    memset(&attr, 0, sizeof(attr));
    attr.qp_state = IBV_QPS_INIT;
    attr.port_num = 1; // IB 端口号
    attr.pkey_index = 0;
    attr.qp_access_flags = IBV_ACCESS_LOCAL_WRITE | IBV_ACCESS_REMOTE_READ | IBV_ACCESS_REMOTE_WRITE;
    flags = IBV_QP_STATE | IBV_QP_PKEY_INDEX | IBV_QP_PORT | IBV_QP_ACCESS_FLAGS;
    rc = ibv_modify_qp(res.qp, &attr, flags);
    if (rc)
    fprintf(stderr, "failed to modify QP state to INIT\n");

    //INIT -> RTR(Ready To Receive)
    memset(&attr, 0, sizeof(attr));
    attr.qp_state = IBV_QPS_RTR;
    attr.path_mtu = IBV_MTU_256;
    attr.dest_qp_num = res.remote_props.qp_num;
    attr.rq_psn = 0;
    attr.max_dest_rd_atomic = 1;
    attr.min_rnr_timer = 0x12;
    attr.ah_attr.is_global = 0;
    attr.ah_attr.dlid = res.remote_props.lid;
    attr.ah_attr.sl = 0;
    attr.ah_attr.src_path_bits = 0;
    attr.ah_attr.port_num = 1;
    flags = IBV_QP_STATE | IBV_QP_AV | IBV_QP_PATH_MTU | IBV_QP_DEST_QPN | IBV_QP_RQ_PSN | IBV_QP_MAX_DEST_RD_ATOMIC | IBV_QP_MIN_RNR_TIMER;
    rc = ibv_modify_qp(res.qp, &attr, flags);
    if (rc)
    fprintf(stderr, "failed to modify QP state to RTR\n");

    //RTR -> RTS(Ready To Send)
    memset(&attr, 0, sizeof(attr));
    attr.qp_state = IBV_QPS_RTS;
    attr.timeout = 0x12;
    attr.retry_cnt = 6;
    attr.rnr_retry = 0;
    attr.sq_psn = 0;
    attr.max_rd_atomic = 1;
    flags = IBV_QP_STATE | IBV_QP_TIMEOUT | IBV_QP_RETRY_CNT | IBV_QP_RNR_RETRY | IBV_QP_SQ_PSN | IBV_QP_MAX_QP_RD_ATOMIC;
    rc = ibv_modify_qp(res.qp, &attr, flags);
    if (rc)
    fprintf(stderr, "failed to modify QP state to RTS\n");
    • 状态:RESET -> INIT -> RTR -> RTS
    • 要严格按照顺序进行转换
    • QP刚创建时状态为RESET
    • INIT之后就可以调用ibv_post_recv提交一个receive buffer了
    • 当QP进入RTR(ready to receive)状态以后,便开始进行接收处理
    • RTR之后便可以转为RTS(ready to send),RTS状态下可以调用ibv_post_send
  11. 创建发送任务/接收任务(ibv_send_wr/ibv_recv_wr)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    /* 11 创建发送任务ibv_send_wr */
    struct ibv_send_wr sr;
    struct ibv_sge sge;
    struct ibv_send_wr *bad_wr = NULL;
    int rc;
    /* prepare the scatter/gather entry */
    memset(&sge, 0, sizeof(sge));
    sge.addr = (uintptr_t)res->buf;
    sge.length = MSG_SIZE;
    sge.lkey = res->mr->lkey;
    /* prepare the send work request */
    memset(&sr, 0, sizeof(sr));
    sr.next = NULL;
    sr.wr_id = 0;
    sr.sg_list = &sge;
    sr.num_sge = 1;
    sr.opcode = opcode;
    sr.send_flags = IBV_SEND_SIGNALED;
    if (opcode != IBV_WR_SEND)
    {
    sr.wr.rdma.remote_addr = res->remote_props.addr;
    sr.wr.rdma.rkey = res->remote_props.rkey;
    }
    • 该任务会被提交到QP中的SQ(Send Queue)中
    • 发送任务有三种操作:Send,Read,Write。Send操作需要对方执行相应的Receive操作;Read/Write直接操作对方内存,对方无感知。
    • 把要发送的数据的内存地址,大小,密钥告诉HCA
    • Read/Write还需要告诉HCA远程的内存地址和密钥
  12. 提交发送任务/接收任务(ibv_post_send/ibv_post_recv)
    1
    2
    3
    4
    rc = ibv_post_send(res->qp, &sr, &bad_wr);
    if (rc)
    fprintf(stderr, "failed to post SR\n");
    return rc;
  13. 轮询任务完成信息(ibv_poll_cq)
    1
    2
    3
    4
    5
    6
    7
    8
    /* 13 轮询任务结果 */
    struct ibv_wc wc;
    int poll_result;
    int rc = 0;
    do
    {
    poll_result = ibv_poll_cq(res->cq, 1, &wc);
    } while (poll_result == 0);

QP状态转换

rdma_qp_status

Reset State

  1. 该状态为QP新创建时的初始状态
  2. 在不删除QP的情况下,仅能通过Modify Queue Pair Attributes verb跳出该状态
  3. 该状态下向QP提交WR,将会返回错误,远端到来的消息也会直接被忽略
  4. 通过利用verbs修改QP属性,可以将任何状态的QP转换为Reset状态。

Initialized State

  1. 仅能从Reset状态进入该状态
  2. 在不删除QP的情况下,仅能通过Modify Queue Pair Attributes verb跳出该状态
  3. 该状态下,RQ可以接受WR,但不会处理远端到来的消息,并将到来的消息丢弃
  4. 该状态下,向SQ发送WR会返回错误。

Ready To Receive(RTR) State

  1. 在RTR状态下,RQ可以接受WR,从远端到来的消息也会正常处理
  2. 该状态下,向SQ发送WR会返回错误。

Ready To Send(RTS) State

  1. 在RTS状态下,请求端和应答端面向连接的服务类型的通道已经建立
  2. 仅能由RTR和SQD状态进入该状态
  3. 在不删除QP的情况下,仅能通过Modify Queue Pair Attributes verb跳出该状态
  4. 该状态下,向QP发送的WR会被正常处理,发送WR的verb也不会返回错误
  5. 该状态下,从远端到来的消息也正常处理。

Send Queue Drain(SQD) State

  1. 该状态下,向QP发送的WR会被正常处理,发送WR的verb也不会返回错误
  2. 该状态下,从远端到来的消息也正常处理
  3. 仅能由RTS进入该状态
  4. 当转移到该状态时,未处理的消息不能再处理,未处理完的消息必须处理完
  5. 当所有应答都已收到时,如果有事件提醒的请求,则会生成一个附加的异步事件
    a. 消费者可以利用异步事件来确定状态转移的完成
    b. 为确保安全的修改QP的属性,必须在接收到异步事件后在进行属性的更改。
  6. 该状态下,提交到QP的WR会入队,但不会被处理
  7. 在SQ还没有Drained之前,SQD到RTS的状态转换不被允许,在该状况下转移,CI会报告一个立即的错误。

Send Queue Error(SQE) State

  1. 该状态适用于除RC QP之外的所有QP
  2. 该状态仅会由RTS跳入,当处理SQ的WR时,发生完成错误(Completion Error),会造成该转移
  3. 在该状态下,RQ可以接受WR,从远端到来的消息也会正常处理
  4. 发生了完成错误的WR必须通过CQ返回正确的完成错误码
  5. 由于SQ中的WR可能部份或全部执行,因此,接收端的状态是未知的
  6. 在SQ中导致完成错误的WR的下一条WR,必须通过CQ返回刷新错误(Flush Error)的完成状态
  7. 该状态下,可以通过Modify Queue Pair Attributes verb跳到RTS状态、Reset状态、Error状态。

Error State

  1. QP上的所有正常处理全部停止
  2. 由于WR发生完成错误,导致跳入该状态时,必须通过CQ返回完成错误码
  3. 该状态下,从远端收到的数据会被丢弃
  4. 在QP中导致完成错误的WR的下一条WR,必须通过CQ返回刷新错误(Flush Error)的完成状态。

One-Side RDMA

  1. 首先A、B建立连接,QP已经创建并且初始化。
  2. 数据被存档在A的buffer地址VA,注意VA应该提前注册到A的RNIC,并拿到返回的r_key,相当于RDMA操作这块buffer的权限。
  3. A把数据地址VA,key封装到专用的报文传送到B,这相当于A把数据buffer的操作权交给了B。同时A在它的WQ中注册进一个WR,以用于接收数据传输的B返回的状态。
  4. B在收到A的送过来的数据VA和r_key后,RNIC会把它们连同存储地址VB到封装RDMA READ,这个过程A、B两端不需要任何软件参与,就可以将A的数据存储到B的VB虚拟地址。
  5. B在存储完成后,会向A返回整个数据传输的状态信息。

** 单边操作传输方式是RDMA与传统网络传输的最大不同,只需提供直接访问远程的虚拟地址,无须远程应用的参与其中,这种方式适用于批量数据传输。 **

参考&鸣谢

radosgw_rgw_auth.png

** 版本 **
mimic-13.2.6

原理

RGW认证机制中涉及的类分为两类,Strategy类和Engine类。

Strategy(认证策略)

  • 通过Strategy::add_engine向Strategy注册Engine,注册过程中需要指定Control
  • Control 包括REQUISITESUFFICIENTFALLBACK
    REQUISITE 表示这个Engine是一个必要条件,若一个注册的Engine返回失败,则立即终止Strategy的认证过程,并且不再对其它注册的Engine进行认证;
    SUFFICIENT 表示这个Engine是一个充要条件,若一个注册的Engine返回成功,则Strategy完成。然而一个Engine的失败,不会终止整个Strategy,直到所有Engine都返回失败;
    FALLBACKSUFFICIENT类似,所有注册的Engine返回失败,返回result_t::deny(reason = -EACCES);
    具体可见strategy_handle_rejectedstrategy_handle_deniedstrategy_handle_granted三个方法。
  • 一个Strategy可以包含多个Strategy和Engine。当验证过程遇到Strategy时,会进行递归调用,直到Engine返回验证结果。
  • 认证的入口为Strategy::apply,由Strategy::apply调用Strategy的authenticate方法开始逐层递归认证。

Engine(认证引擎)

  • Engine处理具体的认证请求,分为S3AnonymousEngineLDAPEngineLocalEngine
  • Engine的认证状态包括DENIEDGRANTEDREJECTED
    DENIED 没有REJECTED那么强烈的认证失败;
    GRANTED 认证成功;
    REJECTED 认证失败,不需要再尝试其它Engine了;

没钱、没钱、没钱,重要的事情说三遍,因为没钱,所以买不起正版的golang IDE,只能使用免费的轻量级的工具完成golang开发任务。

那么,Coding可以使用vim。debug呢?以前用gdb,据说出了个dlv,据说这个dlv可以调试goroutine。抱着试试看的心态尝试一下。

dlv

安装

1
go get -u github.com/derekparker/delve/cmd/dlv

配置

配置文件在~/.dlv/config.yml,推荐修改其中的max-string-len,此配置为debug时,查看string变量的内容,最大显示多长,对于一些超长的字符串,会显示不下。所以为了看到更为完整的内容,建议将其设置为max-string-len: 640

当然,此配置也可以在debug过程中动态修改,详细请见help中的config命令。

使用

启动dlv

1
dlv debug *.go

进入交互界面后,可以使用help查看命令,b设置断点,llist代码,基本使用与gdb很相似,如此用户体验还是不错的。

gdb

Todo…

参考&鸣谢

使用官方ceph-container工程构建私人定制的ceph/daemon镜像。本文以mimic版本为例,在对mimic进行二次开发后,将生成的rpm包更新到yum源中,再生成新的ceph-release中使用该yum源。

获取官方ceph-container

1
git clone git@github.com:ceph/ceph-container.git

安装私有Ceph.repo

修改获取ceph-releaseRPM包地址,编辑文件./ceph-releases/ALL/centos/daemon-base/__DOCKERFILE_INSTALL__

eg:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
yum install -y epel-release && \
yum install -y jq && \
bash -c ' \
if [ -n "__GANESHA_PACKAGES__" ]; then \
echo "[ganesha]" > /etc/yum.repos.d/ganesha.repo ; \
echo "name=ganesha" >> /etc/yum.repos.d/ganesha.repo ; \
if [[ "${CEPH_VERSION}" =~ master|^wip* ]]; then \
REPO_URL=$(curl -s "https://shaman.ceph.com/api/search/?project=nfs-ganesha&distros=centos/__ENV_[BASEOS_TAG]__&flavor=ceph_master&ref=next&sha1=latest" | jq -a ".[0] | .url"); \
echo "baseurl=$REPO_URL/\$basearch/" >> /etc/yum.repos.d/ganesha.repo ; \
elif [[ "${CEPH_VERSION}" == nautilus ]]; then \
echo "baseurl=http://download.ceph.com/nfs-ganesha/rpm-V2.8-stable/$CEPH_VERSION/\$basearch/" >> /etc/yum.repos.d/ganesha.repo ; \
else \
echo "baseurl=http://download.ceph.com/nfs-ganesha/rpm-V2.7-stable/$CEPH_VERSION/\$basearch/" >> /etc/yum.repos.d/ganesha.repo ; \
fi ; \
echo "gpgcheck=0" >> /etc/yum.repos.d/ganesha.repo ; \
echo "enabled=1" >> /etc/yum.repos.d/ganesha.repo ; \
fi ; \
if [ -n "__ISCSI_PACKAGES__" ]; then \
for repo in tcmu-runner python-rtslib; do \
curl -s -L https://shaman.ceph.com/api/repos/$repo/master/latest/__ENV_[BASEOS_REPO]__/__ENV_[BASEOS_TAG]__/repo > /etc/yum.repos.d/$repo.repo ; \
done ; \
if [[ "${CEPH_VERSION}" =~ master|^wip* ]]; then \
curl -s -L https://shaman.ceph.com/api/repos/ceph-iscsi/master/latest/__ENV_[BASEOS_REPO]__/__ENV_[BASEOS_TAG]__/repo > /etc/yum.repos.d/ceph-iscsi.repo ; \
elif [[ "${CEPH_VERSION}" == nautilus ]]; then \
curl -s -L https://download.ceph.com/ceph-iscsi/3/rpm/el__ENV_[BASEOS_TAG]__/ceph-iscsi.repo -o /etc/yum.repos.d/ceph-iscsi.repo ; \
else \
curl -s -L https://download.ceph.com/ceph-iscsi/2/rpm/el__ENV_[BASEOS_TAG]__/ceph-iscsi.repo -o /etc/yum.repos.d/ceph-iscsi.repo ; \
fi ; \
fi' && \
yum update -y && \
rpm --import 'https://download.ceph.com/keys/release.asc' && \
bash -c ' \
if [[ "${CEPH_VERSION}" =~ master|^wip* ]] || ${CEPH_DEVEL}; then \
REPO_URL=$(curl -s "https://shaman.ceph.com/api/search/?project=ceph&distros=centos/__ENV_[BASEOS_TAG]__&flavor=default&ref=${CEPH_VERSION}&sha1=latest" | jq -a ".[0] | .url"); \
RELEASE_VER=0 ;\
else \
RELEASE_VER=1 ;\
REPO_URL="http://10.100.13.112/rpm-${CEPH_VERSION}/el__ENV_[BASEOS_TAG]__/"; \
fi && \
rpm -Uvh "$REPO_URL/noarch/ceph-release-1-${RELEASE_VER}.el__ENV_[BASEOS_TAG]__.noarch.rpm" ' && \
yum install -y __CEPH_BASE_PACKAGES__ && \
bash -c ' \
if [[ "${CEPH_VERSION}" =~ master|^wip* ]] || ${CEPH_DEVEL}; then \
yum install -y python-pip ; \
pip install -U remoto ; \
yum remove -y python-pip ; \
fi '

REPO_URL使用您指定的yum源。

指定版本构建镜像

1
make FLAVORS=mimic-13.2.6-1.gba13b2d.el7,centos,7 build

参考&鸣谢

ceph-release rpm包用于在/etc/yum.repos.d/目录下安装ceph.repo文件。
本人使用容器进行RPM包构建,镜像为centos:7hub.docker.com下载)

rpmbuild安装

1
yum install -y rpm-build

rpmbuild目录树建立

1
mkdir -pv ~/rpmbuild/{BUILD,RPMS,SOURCES,SPECS,SRPMS,BUILDROOT}
  • SPEC 保存RPM包配置(.spec)文件
  • SOURCES 源代码目录,保存源码包(如.tar 包)和所有patch补丁
  • BUILD 构建目录,源码包被解压至此,并在该目录的子目录完成编译
  • BUILDROOT 最终安装目录,保存 %install 阶段安装的文件,打包好后此目录相关内容会自动删除
  • RPMS 标准RPM包目录,生成/保存二进制RPM包
  • SRPMS 源代码RPM包目录,生成/保存源码RPM包(SRPM)

构建BUILDROOT目录

1
2
mkdir -p ~/rpmbuild/BUILDROOT/ceph-release-1-1.el7.noarch/etc/yum.repos.d/
# 必须建一个你程序名的一个目录

~/rpmbuild/BUILDROOT/ceph-release-1-1.el7.noarch/etc/yum.repos.d/目录下创建ceph.repo文件,并写入如下内容:

1
2
3
4
5
6
7
8
9
10
11
[Ceph]
name=Ceph packages for $basearch
baseurl=http://10.100.13.112/rpm-mimic/el7/$basearch
enabled=1
gpgcheck=0

[Ceph-noarch]
name=Ceph noarch packages
baseurl=http://10.100.13.112/rpm-mimic/el7/noarch
enabled=1
gpgcheck=0

编写SPEC文件

1
touch ~/rpmbuild/SPEC/ceph-release.spec

内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Name: ceph-release
Version: 1
Release: 1.el7
Summary: Ceph Release
License: GPL
Group: Applications/System
Vendor: zhoub
Buildarch: noarch

%description
%prep
%build
%pre
%post
%preun
%postun
%files
/etc/yum.repos.d/ceph.repo
%changelog
  • Name 标签就是软件名,Version标签为版本号,而Release是发布编号。
  • Summary 标签是简要说明,英文的话第一个字母应大写,以避免rpmlint工具(打包检查工具)警告
  • License 标签说明软件包的协议版本,审查软件的License状态是打包者的职责,这可以通过检查源码或LICENSE文件,或与作者沟通来完成。
  • Group 标签过去用于按照/usr/share/doc/rpm-/GROUPS分类软件包。目前该标记已丢弃,vim的模板还有这一条,删掉即可,不过添加该标记也不会有任何影响。
  • %changelog 标签应包含每个Release所做的更改日志,尤其应包含上游的安全/漏洞补丁的说明。Changelog日志可使用rpm --changelog -q <packagename>查询,通过查询可得知已安装的软件是否包含指定漏洞和安全补丁。%changelog条目应包含版本字符串,以避免rpmlint工具警告。
  • 多行的部分,如%changelog%description由指令下一行开始,空行结束。
  • 一些不需要的行(如BuildRequires和Requires)可使用‘#’注释。
  • %prep%build%install%file暂时用默认的,未做任何修改。

构建RPM包

1
rpmbuild -bb ~/rpmbuild/SPEC/ceph-release.spec

参考&鸣谢

Yum源

安装createrepo

1
yum install -y createrepo

创建仓库

使用createrepo工具创建、更新仓库

按Yum源的规则创建目录树rpm-<ceph版本名称>/<os version>/<arch>

1
2
mkdir -p ./ceph_repo/rpm-mimic/el7/x86_64/
mkdir -p ./ceph_repo/rpm-mimic/el7/noarch/

创建完目录树之后,将不同arch的rpm包copy到对应的目录中。然后在创建仓库

创建仓库

1
2
createrepo ./ceph_repo/rpm-mimic/el7/x86_64
createrepo ./ceph_repo/rpm-mimic/el7/noarch

若仓库中RPM有更新、增加、删除,需要更新仓库

1
createrepo --update ./ceph_repo/rpm-mimic/el7/x86_64/

发布仓库

本示例以docker形式发布仓库

1
docker run -d --net=host --name ceph_repo -v $PWD/ceph_repo:/root/repo -w /root/repo/ --restart=always python:3 python -m http.server 80

or

1
docker run -d --net=host --name ceph_repo -v $PWD/ceph_repo:/root/repo -w /root/repo/ --restart=always python:3 python -m SimpleHTTPServer 80

参考&鸣谢

export cephfs

创建用户目录

使用admin用户将根目录挂载,并创建用户使用需要的目录

1
mount -t ceph <monitor ip>:6789:/ /mnt/ -o name=admin,secret=xxxxxxxxxxxxxxxxxxxxxxxx==

/mnt目录下创建kubefs目录

给目录设置配额(Quota)

设置目录配额是linux内核本身提供的功能,cephfs只是支持了该功能。设置前需要先安装attr的rpm包yum install -y attr

设置目录配额

1
setfattr -n ceph.quota.size_bytes -v <size byte> /mnt/kubefs

通过getfattr可以获取该quota值。

cephfs quota 不能精确的限制配额,容量统计有延时,大概10s左右。

创建用户

创建一个名称为client.kubefs的用户,使其在/目录下有只读的权限,在/kubefs目录下有读写权限

1
ceph fs authorize cephfs client.kubefs / r /kubefs rw

cephfsfilesystem name,可以通过ceph fs ls查询。用户创建成功后,可以通过ceph auth list查询用户权限。需要删除用户也可以通过ceph auth rm仅限删除操作。

挂载

1
mount -t ceph <monitor ip>:6789:/kubefs /mnt/ -o name=kubefs,secret=xxxxxxxxxxxxxxxxxxxxxx==

export nfs

cephfs本身不支持nfs协议,可以通过nfs-ganesha将cephfs转成nfs提供出去。

镜像

ananace/nfs-ganesha-ceph

ganesha配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# NFS protocol options
EXPORT
{
# Export Id (mandatory, each EXPORT must have a unique Export_Id)
Export_Id = 77;

# Exported path (mandatory)
Path = /;

# Pseudo Path (for NFS v4)
Pseudo = /;

# Access control options
Access_Type = RW;
Squash = No_Root_Squash;

# NFS protocol options
SecType = "sys";
Transports = TCP;
Protocols = 4;

# Exporting FSAL
FSAL {
Name = CEPH;
}
}

运行ganesha容器

1
docker run -d --net=host  -v /home/xxx/ceph/etc_ceph/:/etc/ceph -v /home/xxx/ceph/ganesha/:/etc/ganesha --name nfs -e GANESHA_BOOTSTRAP_CONFIG=no ananace/nfs-ganesha-ceph

Bucket Policy 开启S3数据分享这扇大门。该配置通过S3接口设置到Bucket上,使bucket可对某些用户开放一些访问权限;或拒绝某些用户的一些访问权限。

环境

  • Ceph集群版本mimic
  • RGW版本nautilus
  • 一个Realm中一个master zonegroup
  • master zonegroup中包含两个zone,exter(master zone)和backup
  • master zongrroup中包含两个placement,default-placement 和 cold-placement
    default-placement 将数据存储于 exter.rgw.buckets.{data, index, non-ec}
    cold-placement 将数据存储于 exter.rgw.cold.{data, index, non-ec}
  • zone exter中创建了4个用户分别属于ours tenant和 默认tenant
    默认tenant包括用户,colder 和 admin
    ours tenant包括用户,ourone 和 ourtwo
  • colder 用户使用的 cold-placement,其它用户均使用default-placement

使用

Policy 配置Json Example:

1
2
3
4
5
6
7
8
9
10
11
12
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"AWS": ["*"]},
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::my-cold-bucket",
"arn:aws:s3:::my-cold-bucket/*"
]
}]
}

** 配置说明:**

  1. Version可以选择2008-10-17或者2012-10-17 AWS就是这样的,没道理讲的。
  2. EffectAllowDeny两个选项
  3. Principal操作主体。eg: “arn:aws:iam:::user/“ 该示例有待验证。
  4. ActionAllowDeny得动作
  5. Resource被操作得对象
  6. Condition使用条件

更多配置得内容参见ceph源码src/rgw/rgw_iam_policy_keywords.gperf

测试

目的

调研同tenant访问配置使用方法和跨tenant访问配置使用方法

测试方法

用户ours$ourone创建一个叫ouronebucket的bucket,并配置其policy

1
2
3
4
5
6
7
8
9
10
11
12
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"AWS": ["arn:aws:iam::ours:user/ourtwo", "arn:aws:iam:::user/admin"]},
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::ouronebucket",
"arn:aws:s3:::ouronebucket/*"
]
}]
}

使用s3cmd工具将policy写入bucket

1
s3cmd setpolicy policy.json s3://ouronebucket

用户colder创建一个叫my-cold-bucket的bucket,并配置其policy

1
2
3
4
5
6
7
8
9
10
11
12
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"AWS": ["*"]},
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::my-cold-bucket",
"arn:aws:s3:::my-cold-bucket/*"
]
}]
}

使用s3cmd工具将policy写入bucket

1
s3cmd setpolicy policy.json s3://my-cold-bucket

** 使用admin用户访问my-cold-bucket **

1
2
3
$ s3cmd -c ./admin.cfg ls s3://my-cold-bucket
2019-10-17 09:06 207 s3://my-cold-bucket/admin.cfg
2019-10-16 08:40 8478720 s3://my-cold-bucket/tgt.tar

同tenant内正常访问

** 使用admin用户访问ouronebucket **

1
2
3
4
5
$ s3cmd -c ./admin.cfg ls s3://ouronebucket
ERROR: Bucket 'ouronebucket' does not exist
ERROR: S3 error: 404 (NoSuchBucket)
$ s3cmd -c ./admin.cfg ls s3://ours:ouronebucket 12 ↵
ERROR: S3 error: 403 (SignatureDoesNotMatch)

** 使用ourtwo用户访问my-cold-bucket **

1
2
3
4
5
$ s3cmd -c ./ours_two.cfg ls s3://my-cold-bucket                                                                                                     77 ↵
ERROR: Bucket 'my-cold-bucket' does not exist
ERROR: S3 error: 404 (NoSuchBucket)
$ s3cmd -c ./ours_two.cfg ls s3://:my-cold-bucket 12 ↵
ERROR: S3 error: 403 (SignatureDoesNotMatch)

** 使用ourtwo用户访问ouronebucket **

1
2
3
4
5
$ s3cmd -c ./ours_two.cfg ls s3://ouronebucket                                                                                                       77 ↵
ERROR: Access to bucket 'ouronebucket' was denied
ERROR: S3 error: 403 (AccessDenied)
$ s3cmd -c ./ours_two.cfg ls s3://ours:ouronebucket 77 ↵
ERROR: S3 error: 403 (SignatureDoesNotMatch)

** 修改 **
ouronebucket的policy中的Principal改为{"AWS": ["*"]},。再试一次

1
2
3
4
5
6
$ s3cmd -c ./ours_two.cfg put ./ours_two.cfg s3://ouronebucket
WARNING: Module python-magic is not available. Guessing MIME types based on file extensions.
upload: './ours_two.cfg' -> 's3://ouronebucket/ours_two.cfg' [1 of 1]
207 of 207 100% in 0s 356.83 B/s done
$ s3cmd -c ./ours_two.cfg ls s3://ouronebucket
2019-10-17 12:01 207 s3://ouronebucket/ours_two.cfg

在网上看到“Rgw bucket policy权限设置”这篇文章,里面提到boto3对tenant不支持,于是猜想是不是s3cmd也不支持tenant,遂自己写一个python验证一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/usr/bin/env python
# encoding: utf-8

import boto
import boto.s3.connection

access_list = ["1CXO01UCDSR1182IUYPL","8I4K2USDV5SK3UFLQUB0"]
secret_list = ["Ww0Io3b6fF7dXHQiO9gLo99DZbZAKqfvNO2N7g48","A4JuvB468tmnDpmkZMfwesb2zmGZeSiCJlzJMALc"]
bucket_list = [":my-cold-bucket","ours:ouronebucket"]

for i in range(0,len(access_list)):
access = access_list[i]
secret = secret_list[i]
bucket = bucket_list[i]
conn = boto.connect_s3(aws_access_key_id=access,
aws_secret_access_key = secret,
host='172.30.12.137',
port=7480,
is_secure=False,
calling_format = boto.s3.connection.OrdinaryCallingFormat())
bkt = conn.get_bucket(bucket)
print(bkt.get_all_keys())

执行下

1
2
3
# python bkt-policy.py
[<Key: :my-cold-bucket,admin.cfg>, <Key: :my-cold-bucket,tgt.tar>]
[<Key: ours:ouronebucket,ours_two.cfg>]

发现跨tenant可以正常访问。

修改ouronebucket policy 中的Principal{"AWS": ["arn:aws:iam::ours:user/two","arn:aws:iam:::user/admin"]},只允许ours$twoadmin这两个用户访问。
再次执行上面的python脚本

1
2
3
# python bkt-policy.py
[<Key: :my-cold-bucket,admin.cfg>, <Key: :my-cold-bucket,tgt.tar>]
[<Key: ours:ouronebucket,ours_two.cfg>]

再在上面的脚本中增加colder的access、secret key。并执行脚本

1
2
3
4
5
6
7
8
9
10
11
# python bkt-policy.py
[<Key: :my-cold-bucket,admin.cfg>, <Key: :my-cold-bucket,tgt.tar>]
[<Key: ours:ouronebucket,ours_two.cfg>]
Traceback (most recent call last):
File "bkt-policy.py", line 22, in <module>
bkt = conn.get_bucket(bucket)
File "/usr/local/lib/python2.7/site-packages/boto/s3/connection.py", line 509, in get_bucket
return self.head_bucket(bucket_name, headers=headers)
File "/usr/local/lib/python2.7/site-packages/boto/s3/connection.py", line 542, in head_bucket
raise err
boto.exception.S3ResponseError: S3ResponseError: 403 Forbidden

由于Principal中没有给colder用户授权,所以colder访问ouronebucket时报403错误。

参考&鸣谢