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的管理数据。
通过上面的操作就会形成这样的结构(如下图)
图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中。
原本的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是如何接到请求,并且怎么来处理这个请求的。请期待下一节。