ceph的数据存储之路(4) ----- rbd client 端的数据请求处理

2016-11-01更新 start:--------------------------------------------------------------------------------------

最近比较多的人私下问我,改了ceph的源码,重新编译了,但是在使用本节提供的python脚本测试librbd的时候出现了错误,怎么解决出现的这个错误。这应该是个好现象,很多人都深入到代码层级了,这个也是开始代码之旅的重要环节,今天在这里更新说明下。之前在第二节https://my.oschina.net/u/2460844/blog/515353中讲述了怎么编译源码,但是在使用本节提供的脚本时会出现一些问题,这些问题来自于使用了源码编译后,在python脚本中调用失败。问题的原因是我忘记添加上了一个环节,重新编译后的代码要替换一些库文件,首先来看下提示的错误有哪些,错误如下:

1.ImportError: No module named rados

root@cephmon:~/ceph/ceph-0.94.2/python# python create_rbd.py 
Traceback (most recent call last):
  File "create_rbd.py", line 2, in <module>
    import sys,rados,rbd
ImportError: No module named rados

2.OSError: librados.so.2: cannot open shared object file: No such file or directory

root@cephmon:~/ceph/ceph-0.94.2/python# python create_rbd.py
Traceback (most recent call last):
  File "create_rbd.py", line 18, in <module>
    connectceph()
  File "create_rbd.py", line 4, in connectceph
    cluster = rados.Rados(conffile = '/root/ceph/ceph-0.94.2/src/ceph.conf')
  File "/usr/lib/python2.7/rados.py", line 215, in __init__
    self.librados = CDLL(library_path if library_path is not None else 'librados.so.2')
  File "/usr/lib/python2.7/ctypes/__init__.py", line 365, in __init__
    self._handle = _dlopen(self._name, mode)
OSError: librados.so.2: cannot open shared object file: No such file or directory

出现这两个问题的原因是 源码编译后,python 脚本无法找到对应的库文件(或者python文件)。

针对问题1.拷贝源码包下面的python脚本 cp  ../ceph-0.94.2/src/pybind/*  /usr/lib/python2.7/

针对问题2.拷贝最新编译出来的的librados到/usr/lib/ 目录下即可, 这个最新编译出来的librados在目录../ceph-0.94.2/src/.lib/目录中,该目录是一个隐藏目录,容易被忽略。在该目录下找到librados.so.2 和librbd.so.1 拷贝到 /usr/lib/ 下。

解决了问题1和问题2,脚本就可以正常的运行了。

针对问题2,OSError: librados.so.2: cannot open shared object file: No such file or directory 网上经常有人在用源码部署ceph的时候都出现了这个问题,好像没人特别准确的回答这个问题。原因就是最新编译出的librados没有拷贝到/usr/lib下,脚本或者程序找不到这个库所以报错。解决办法如上即可。

2016-11-01更新 end:--------------------------------------------------------------------------------------

讲ceph的文章有很多,但是都是从高大尚的理论出发,看了很多这样的文章收获很多,但是总有一种不能实际抓住ceph的命门,不能切脉,很多时候可能看完就忘了。从这篇博客开始抛开高大尚的理论,从最接地气的方式开始,可以帮助那些需要开发ceph童鞋们,或者想深入了解ceph实现的童鞋们。这里用最接地气的方式讲述ceph背后的故事。

首先明白ceph就是用来存储的,这个系列的博客就讲述如何ceph的读写请求的一生,本节讲述数据写操作的生命开始。

首先看一下我们用python调用librbd 写rbd设备的测试代码:

#!/usr/bin/env python
import sys,rados,rbd
def connectceph():
      cluster = rados.Rados(conffile = '/root/xuyanjiangtest/ceph-0.94.3/src/ceph.conf')
      cluster.connect()
      ioctx = cluster.open_ioctx('mypool')
      rbd_inst = rbd.RBD()
      size = 4*1024**3 #4 GiB
      rbd_inst.create(ioctx,'myimage',size)
      image = rbd.Image(ioctx,'myimage')
      data = 'foo'* 200
      image.write(data,0)
      image.close()
      ioctx.close()
        cluster.shutdown()
 
if __name__ == "__main__":
        connectceph()

 

一、写操作数据request的孕育过程

在write request 请求开始之前,它需要准备点旅行的用品,往返的机票等。下面先看看前期准备了什么。

1. 首先cluster = rados.Rados(conffile = 'XXXX/ceph.conf'),用当前的这个ceph的配置文件去创建一个rados,这里主要是解析ceph.conf中写明的参数。然后将这些参数的值保存在rados中。

2. cluster.connect() ,这里将会创建一个radosclient的结构,这里会把这个结构主要包含了几个功能模块:消息管理模块Messager,数据处理模块Objector,finisher线程模块。这些模块具体的工作后面讲述。

3. ioctx = cluster.open_ioctx('mypool'),为一个名字叫做mypool的存储池创建一个ioctx ,ioctx中会指明radosclient与Objector模块,同时也会记录mypool的信息,包括pool的参数等。

4. rbd_inst.create(ioctx,'myimage',size) ,创建一个名字为myimage的rbd设备,之后就是将数据写入这个设备。

5. image = rbd.Image(ioctx,'myimage'),创建image结构,这里该结构将myimage与ioctx 联系起来,后面可以通过image结构直接找到ioctx。这里会将ioctx复制两份,分为为data_ioctx和md_ctx。见明知意,一个用来处理rbd的存储数据,一个用来处理rbd的管理数据。

通过上面的操作就会形成这样的结构(如下图)

143540_OSPk_2460844.jpg

 

图1-1 request孕育阶段

 

过程描述,首先根据配置文件创建一个rados,接下来为这个rados创建一个radosclient,radosclient包含了3个主要模块(finisher,Messager,Objector)。再根据pool创建对应的ioctx,ioctx中能够找到radosclient。再对生成对应rbd的结构image,这个image中复制了两个ioctx,分别成为了md_ioctx与data_ioctx。这时完全可以根据image入口去查找到前期准备的其他数据结构。接下来的数据操作完全从image开始,也是rbd的具体实例。

 

二、request的出生和成长。

1. image.write(data,0),通过image开始了一个写请求的生命的开始。这里指明了request的两个基本要素 buffer=data 和 offset=0。由这里开始进入了ceph的世界,也是c++的世界。

由image.write(data,0)  转化为librbd.cc 文件中的Image::write() 函数,来看看这个函数的主要实现

ssize_t Image::write(uint64_t ofs, size_t len, bufferlist& bl)
  {  
      //…………………
   ImageCtx *ictx = (ImageCtx *)ctx;
    int r = librbd::write(ictx, ofs, len, bl.c_str(), 0);
    return r;     
  }

 

2. 该函数中直接进行分发给了librbd::wrte的函数了。跟随下来看看librbd::write中的实现。该函数的具体实现在internal.cc文件中。

ssize_t write(ImageCtx *ictx, uint64_t off, size_t len, const char *buf, int op_flags)
 {
      ……………
    Context *ctx = new C_SafeCond(&mylock, &cond, &done, &ret);   //---a
    AioCompletion *c = aio_create_completion_internal(ctx, rbd_ctx_cb);//---b
   r = aio_write(ictx, off, mylen, buf, c, op_flags);  //---c
     ……………     
    while (!done)
           cond.Wait(mylock);  // ---d
      ……………
}

 

---a.这句要为这个操作申请一个回调操作,所谓的回调就是一些收尾的工作,信号唤醒处理。

---b。这句是要申请一个io完成时 要进行的操作,当io完成时,会调用rbd_ctx_cb函数,该函数会继续调用ctx->complete()。

---c.该函数aio_write会继续处理这个请求。

---d.当c句将这个io下发到osd的时候,osd还没请求处理完成,则等待在d上,直到底层处理完请求,回调b申请的 AioCompletion, 继续调用a中的ctx->complete(),唤醒这里的等待信号,然后程序继续向下执行。

3.再来看看aio_write 拿到了 请求的offset和buffer会做点什么呢?

int aio_write(ImageCtx *ictx, uint64_t off, size_t len, const char *buf,
           AioCompletion *c, int op_flags)
  {
      ………
      //将请求按着object进行拆分
      vector<ObjectExtent> extents;
     if (len > 0) 
      {
         Striper::file_to_extents(ictx->cct, ictx->format_string,
                        &ictx->layout, off, clip_len, 0, extents);   //---a
      } 
      //处理每一个object上的请求数据
      for (vector<ObjectExtent>::iterator p = extents.begin(); p != extents.end(); ++p) 
      {
               ……..
           C_AioWrite *req_comp = new C_AioWrite(cct, c); //---b
           ……..
           AioWrite *req = new AioWrite(ictx, p->oid.name, p->objectno, p- >offset,bl,….., req_comp);     //---c
           r = req->send();    //---d
           …….
      }
      ……
}

 

根据请求的大小需要将这个请求按着object进行划分,由函数file_to_extents进行处理,处理完成后按着object进行保存在extents中。file_to_extents()存在很多同名函数注意区分。这些函数的主要内容做了一件事儿,那就对原始请求的拆分。

一个rbd设备是有很多的object组成,也就是将rbd设备进行切块,每一个块叫做object,每个object的大小默认为4M,也可以自己指定。file_to_extents函数将这个大的请求分别映射到object上去,拆成了很多小的请求如下图。最后映射的结果保存在ObjectExtent中。

145240_0zUe_2460844.jpg

 

 

原本的offset是指在rbd内的偏移量(写入rbd的位置),经过file_to_extents后,转化成了一个或者多个object的内部的偏移量offset0。这样转化后处理一批这个object内的请求。

4. 再回到 aio_write函数中,需要将拆分后的每一个object请求进行处理。

---b.为写请求申请一个回调处理函数。

---c.根据object内部的请求,创建一个叫做AioWrite的结构。

---d.将这个AioWrite的req进行下发send().

5. 这里AioWrite 是继承自 AbstractWrite ,AbstractWrite 继承自AioRequest类,在AbstractWrite 类中定义了send的方法,看下send的具体内容.

int AbstractWrite::send() 

 {  ………………

    if (send_pre())           //---a

      ……………

}

#进入send_pre()函数中

bool AbstractWrite::send_pre()

{

      m_state = LIBRBD_AIO_WRITE_PRE;   // ----a

      FunctionContext *ctx =    //----b

           new FunctionContext( boost::bind(&AioRequest::complete, this, _1));

      m_ictx->object_map.aio_update(ctx); //-----c

}

 

---a.修改m_state 状态为LIBRBD_AIO_WRITE_PRE。

---b.申请一个回调函数,实际调用AioRequest::complete()

---c.开始下发object_map.aio_update的请求,这是一个状态更新的函数,不是很重要的环节,这里不再多说,当更新的请求完成时会自动回调到b申请的回调函数。

6. 进入到AioRequest::complete() 函数中。

void AioRequest::complete(int r)
 {
    if (should_complete(r))   //---a
        …….
}

 

---a.should_complete函数是一个纯虚函数,需要在继承类AbstractWrite中实现,来7. 看看AbstractWrite:: should_complete()

bool AbstractWrite::should_complete(int r)
{
    switch (m_state) 
  {
          case LIBRBD_AIO_WRITE_PRE:  //----a

      {

                     send_write(); //----b

 

----a.在send_pre中已经设置m_state的状态为LIBRBD_AIO_WRITE_PRE,所以会走这个分支。

----b. send_write()函数中,会继续进行处理,

7.1.下面来看这个send_write函数

void AbstractWrite::send_write()
{
      m_state = LIBRBD_AIO_WRITE_FLAT;   //----a

      add_write_ops(&m_write);    // ----b

      int r = m_ictx->data_ctx.aio_operate(m_oid, rados_completion, &m_write);

}

 

---a.重新设置m_state的状态为 LIBRBD_AIO_WRITE_FLAT。

---b.填充m_write,将请求转化为m_write。

---c.下发m_write  ,使用data_ctx.aio_operate 函数处理。继续调用io_ctx_impl->aio_operate()函数,继续调用objecter->mutate().

8. objecter->mutate()

ceph_tid_t mutate(……..) 
  {
    Op *o = prepare_mutate_op(oid, oloc, op, snapc, mtime, flags, onack, oncommit, objver);   //----d
    return op_submit(o);
  }

 

---d.将请求转化为Op请求,继续使用op_submit下发这个请求。在op_submit中继续调用_op_submit_with_budget处理请求。继续调用_op_submit处理。

8.1 _op_submit 的处理过程。这里值得细看

ceph_tid_t Objecter::_op_submit(Op *op, RWLock::Context& lc)
{
    check_for_latest_map = _calc_target(&op->target, &op->last_force_resend); //---a
    int r = _get_session(op->target.osd, &s, lc);  //---b
    _session_op_assign(s, op); //----c
    _send_op(op, m); //----d
}

 

----a. _calc_target,通过计算当前object的保存的osd,然后将主osd保存在target中,rbd写数据都是先发送到主osd,主osd再将数据发送到其他的副本osd上。这里对于怎么来选取osd集合与主osd的关系就不再多说,在《ceph的数据存储之路(3)》中已经讲述这个过程的原理了,代码部分不难理解。

----b. _get_session,该函数是用来与主osd建立通信的,建立通信后,可以通过该通道发送给主osd。再来看看这个函数是怎么处理的

9. _get_session

int Objecter::_get_session(int osd, OSDSession **session, RWLock::Context& lc)
{
    map<int,OSDSession*>::iterator p = osd_sessions.find(osd);   //----a
    OSDSession *s = new OSDSession(cct, osd); //----b
    osd_sessions[osd] = s;//--c
    s->con = messenger->get_connection(osdmap->get_inst(osd));//-d
    ………
}

 

----a.首先在osd_sessions中查找是否已经存在一个连接可以直接使用,第一次通信是没有的。

----b.重新申请一个OSDSession,并且使用osd等信息进行初始化。

---c. 将新申请的OSDSession添加到osd_sessions中保存,以备下次使用。

----d.调用messager的get_connection方法。在该方法中继续想办法与目标osd建立连接。

10. messager 是由子类simpleMessager实现的,下面来看下SimpleMessager中get_connection的实现方法

ConnectionRef SimpleMessenger::get_connection(const entity_inst_t& dest)
{
    Pipe *pipe = _lookup_pipe(dest.addr);     //-----a
    if (pipe) 
    {
        ……
    } 
    else 
    {
      pipe = connect_rank(dest.addr, dest.name.type(), NULL, NULL); //----b
    }
}

 

----a.首先要查找这个pipe,第一次通信,自然这个pipe是不存在的。

----b. connect_rank 会根据这个目标osd的addr进行创建。看下connect_rank做了什么。

11. SimpleMessenger::connect_rank

Pipe *SimpleMessenger::connect_rank(const entity_addr_t& addr,  int type, PipeConnection *con,    Message *first)

{
      Pipe *pipe = new Pipe(this, Pipe::STATE_CONNECTING, static_cast<PipeConnection*>(con));      //----a
      pipe->set_peer_type(type); //----b
      pipe->set_peer_addr(addr); //----c
      pipe->policy = get_policy(type); //----d
      pipe->start_writer();  //----e
      return pipe; //----f
}

 

----a.首先需要创建这个pipe,并且pipe同pipecon进行关联。

----b,----c,-----d。都是进行一些参数的设置。

----e.开始启动pipe的写线程,这里pipe的写线程的处理函数pipe->writer(),该函数中会尝试连接osd。并且建立socket连接通道。

目前的资源统计一下,写请求可以根据目标主osd,去查找或者建立一个OSDSession,这个OSDSession中会有一个管理数据通道的Pipe结构,然后这个结构中存在一个发送消息的处理线程writer,这个线程会保持与目标osd的socket通信。

12. 建立并且获取到了这些资源,这时再回到_op_submit 函数中

ceph_tid_t Objecter::_op_submit(Op *op, RWLock::Context& lc)
{
    check_for_latest_map = _calc_target(&op->target, &op->last_force_resend); //---a
    int r = _get_session(op->target.osd, &s, lc);  //---b
    _session_op_assign(s, op); //----c
    MOSDOp *m = _prepare_osd_op(op); //-----d
    _send_op(op, m); //----e
}

 

---c,将当前的op请求与这个session进行绑定,在后面发送请求的时候能知道使用哪一个session进行发送。

--d,将op转化为MOSDop,后面会以MOSDOp为对象进行处理的。

---e,_send_op 会根据之前建立的通信通道,将这个MOSDOp发送出去。_send_op 中调用op->session->con->send_message(m),这个方法会调用SimpleMessager-> send_message(m), 再调用_send_message(),再调用submit_message().在submit_message会找到之前的pipe,然后调用pipe->send方法,最后通过pipe->writer的线程发送到目标osd。

 

自此,客户就等待osd处理完成返回结果了。

总结客户端的所有流程和数据结构,下面来看下客户端的所有结构图。

151617_dNP0_2460844.jpg

 

 

通过这个全部的结构图来总结客户端的处理过程。

1.看左上角的rados结构,首先创建io环境,创建rados信息,将配置文件中的数据结构化到rados中。

2.根据rados创建一个radosclient的客户端结构,该结构包括了三个重要的模块,finiser 回调处理线程、Messager消息处理结构、Objector数据处理结构。最后的数据都是要封装成消息 通过Messager发送给目标的osd。

3.根据pool的信息与radosclient进行创建一个ioctx,这里面包好了pool相关的信息,然后获得这些信息后在数据处理时会用到。

4.紧接着会复制这个ioctx到imagectx中,变成data_ioctx与md_ioctx数据处理通道,最后将imagectx封装到image结构当中。之后所有的写操作都会通过这个image进行。顺着image的结构可以找到前面创建并且可以使用的数据结构。

5.通过最右上角的image进行读写操作,当读写操作的对象为image时,这个image会开始处理请求,然后这个请求经过处理拆分成object对象的请求。拆分后会交给objector进行处理查找目标osd,当然这里使用的就是crush算法,找到目标osd的集合与主osd。

6.将请求op封装成MOSDOp消息,然后交给SimpleMessager处理,SimpleMessager会尝试在已有的osd_session中查找,如果没有找到对应的session,则会重新创建一个OSDSession,并且为这个OSDSession创建一个数据通道pipe,把数据通道保存在SimpleMessager中,可以下次使用。

7.pipe 会与目标osd建立Socket通信通道,pipe会有专门的写线程writer来负责socket通信。在线程writer中会先连接目标ip,建立通信。消息从SimpleMessager收到后会保存到pipe的outq队列中,writer线程另外的一个用途就是监视这个outq队列,当队列中存在消息等待发送时,会就将消息写入socket,发送给目标OSD。

8. 等待OSD将数据消息处理完成之后,就是进行回调,反馈执行结果,然后一步步的将结果告知调用者。

上面是就rbd client处理写请求的过程,那么下面会在分析一个OSD是如何接到请求,并且怎么来处理这个请求的。请期待下一节。

转载于:https://my.oschina.net/u/2460844/blog/532755

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值