2016-11-01更新 start:--------------------------------------------------------------------------------------python
最近比较多的人私下问我,改了ceph的源码,从新编译了,可是在使用本节提供的python脚本测试librbd的时候出现了错误,怎么解决出现的这个错误。这应该是个好现象,不少人都深刻到代码层级了,这个也是开始代码之旅的重要环节,今天在这里更新说明下。以前在第二节https://my.oschina.net/u/2460844/blog/515353中讲述了怎么编译源码,可是在使用本节提供的脚本时会出现一些问题,这些问题来自于使用了源码编译后,在python脚本中调用失败。问题的缘由是我忘记添加上了一个环节,从新编译后的代码要替换一些库文件,首先来看下提示的错误有哪些,错误以下:c++
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 directorysession
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/python2.7
针对问题2.拷贝最新编译出来的的librados到/usr/lib/ 目录下便可, 这个最新编译出来的librados在目录../ceph-0.94.2/src/.lib/目录中,该目录是一个隐藏目录,容易被忽略。在该目录下找到librados.so.2 和librbd.so.1 拷贝到 /usr/lib/ 下。socket
解决了问题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:--------------------------------------------------------------------------------------ui
讲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()
1、写操做数据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的管理数据。
经过上面的操做就会造成这样的结构(以下图)
图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的具体实例。
2、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中。
本来的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处理完成返回结果了。
总结客户端的全部流程和数据结构,下面来看下客户端的全部结构图。
经过这个所有的结构图来总结客户端的处理过程。
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是如何接到请求,而且怎么来处理这个请求的。请期待下一节。