共计 14505 个字符,预计需要花费 37 分钟才能阅读完成。
一、OSD 模块简介
1.1 消息封装 :在 OSD 上发送和接收信息。
cluster_messenger - 与其它 OSDs 和 monitors 沟通
client_messenger -与客户端沟通
1.2 消息调度 :
Dispatcher 类,主要负责消息分类
1.3 工作队列:
1.3.1 OpWQ: 处理 ops(从客户端)和 sub ops(从其他的 OSD)。运行在 op_tp 线程池。
1.3.2 PeeringWQ: 处理 peering 任务,运行在 op_tp 线程池。
1.3.3 CommandWQ: 处理 cmd 命令,运行在 command_tp。
1.3.4 RecoveryWQ: 数据修复,运行在 recovery_tp。
1.3.5 SnapTrimWQ: 快照相关,运行在 disk_tp。
1.3.6 ScrubWQ: scrub,运行在 disk_tp。
1.3.7 ScrubFinalizeWQ: scrub,运行在 disk_tp。
1.3.8 RepScrubWQ: scrub,运行在 disk_tp。
1.3.9 RemoveWQ: 删除旧的 pg 目录。运行在 disk_tp。
1.4 线程池:
有 4 种 OSD 线程池:
1.4.1 op_tp: 处理 ops 和 sub ops
1.4.2 recovery_tp: 处理修复任务
1.4.3 disk_tp: 处理磁盘密集型任务
1.4.4 command_tp: 处理命令
1.5 主要对象:
ObjectStore *store;
OSDSuperblock superblock; 主要是版本号等信息
OSDMapRef osdmap;
1.6 主要操作流程:
1.6.1 客户端发起请求过程
1.6.2 op_tp 线程处理数据读取
1.6.3 对象操作的处理过程
1.6.4 修改操作的处理
1.6.5 日志的写入
1.6.6 写操作处理
1.6.7 事务的 sync 过程
1.6.8 日志恢复
1.7 整体处理过程图
二、客户端写入数据大致流程及保存形式
2.1 读写框架
2.2 客户端写入流程
在客户端使用 rbd 时一般有两种方法:
- 第一种 是 Kernel rbd。就是创建了 rbd 设备后,把 rbd 设备 map 到内核中,形成一个虚拟的块设备,这时这个块设备同其他通用块设备一样,一般的设备文件为 /dev/rbd0,后续直接使用这个块设备文件就可以了,可以把 /dev/rbd0 格式化后 mount 到某个目录,也可以直接作为裸设备使用。这时对 rbd 设备的操作都通过 kernel rbd 操作方法进行的。
- 第二种是 librbd 方式。就是创建了 rbd 设备后,这时可以使用 librbd、librados 库进行访问管理块设备。这种方式不会 map 到内核,直接调用 librbd 提供的接口,可以实现对 rbd 设备的访问和管理,但是不会在客户端产生块设备文件。
应用写入 rbd 块设备的过程:
- 应用调用 librbd 接口或者对 linux 内核虚拟块设备写入二进制块。下面以 librbd 为例。
- librbd 对二进制块进行分块,默认块大小为 4M,每一块都有名字,成为一个对象
- librbd 调用 librados 将对象写入 Ceph 集群
- librados 向主 OSD 写入分好块的二进制数据块 (先建立 TCP/IP 连接,然后发送消息给 OSD,OSD 接收后写入其磁盘)
- 主 OSD 负责同时向一个或者多个次 OSD 写入副本。注意这里是写到日志(Journal)就返回,因此,使用 SSD 作为 Journal 的话,可以提高响应速度,做到服务器端对客户端的快速同步返回写结果(ack)。
- 当主次 OSD 都写入完成后,主 OSD 向客户端返回写入成功。
- 当一段时间(也许得几秒钟)后 Journal 中的数据向磁盘写入成功后,Ceph 通过事件通知客户端数据写入磁盘成功(commit),此时,客户端可以将写缓存中的数据彻底清除掉了。
- 默认地,Ceph 客户端会缓存写入的数据直到收到集群的 commit 通知。如果此阶段内(在写方法返回到收到 commit 通知之间)OSD 出故障导致数据写入文件系统失败,Ceph 将会允许客户端重做尚未提交的操作(replay)。因此,PG 有个状态叫 replay:“The placement group is waiting for clients to replay operations after an OSD crashed.”。
也就是,文件系统负责文件处理,librbd 负责块处理,librados 负责对象处理,OSD 负责将数据写入在 Journal 和磁盘中。
2.3 RBD 保存形式
如下图所示,Ceph 系统中不同层次的组件 / 用户所看到的数据的形式是不一样的:
- Ceph 客户端所见的是一个完整的连续的二进制数据块,其大小为创建 RBD image 是设置的大小或者 resize 的大小,客户端可以从头或者从某个位置开始写入二进制数据。
- librados 负责在 RADOS 中创建对象(object),其大小为 pool 的 order 决定,默认情况下 order = 22 此时 object 大小为 4MB;以及负责将客户端传入的二进制块条带化为若干个条带(stripe)。
- librados 控制哪个条带由哪个 OSD 写入(条带 — 写入哪个 —-> object —- 位于哪个 —-> OSD)
- OSD 负责创建在文件系统中创建文件,并将 librados 传入的数据写入数据。
Ceph client 向一个 RBD image 写入二进制数据(假设 pool 的拷贝份数为 3):
(1)Ceph client 调用 librados 创建一个 RBD image,这时候不会做存储空间分配,而是创建若干元数据对象来保存元数据信息。
(2)Ceph client 调用 librados 开始写数据。librados 计算条带、object 等,然后开始写第一个 stripe 到特定的目标 object。
(3)librados 根据 CRUSH 算法,计算出 object 所对应的主 OSD ID,并将二进制数据发给它。
(4)主 OSD 负责调用文件系统接口将二进制数据写入磁盘上的文件(每个 object 对应一个 file,file 的内容是一个或者多个 stripe)。
(5)主 ODS 完成数据写入后,它使用 CRUSH 算啊计算出第二个 OSD(secondary OSD)和第三个 OSD(tertiary OSD)的位置,然后向这两个 OSD 拷贝对象。都完成后,它向 ceph client 反馈该 object 保存完毕。
(6)然后写第二个条带,直到全部写入完成。全部完成后,librados 还应该会做元数据更新,比如写入新的 size 等。
完整的过程(来源):
该过程具有强一致性的特点:
- Ceph 的读写操作采用 Primary-Replica 模型,Client 只向 Object 所对应 OSD set 的 Primary 发起读写请求,这保证了数据的强一致性。
- 由于每个 Object 都只有一个 Primary OSD,因此对 Object 的更新都是顺序的,不存在同步问题。
- 当 Primary 收到 Object 的写请求时,它负责把数据发送给其他 Replicas,只要这个数据被保存在所有的 OSD 上时,Primary 才应答 Object 的写请求,这保证了副本的一致性。这也带来一些副作用。相比那些只实现了最终一致性的存储系统比如 Swift,Ceph 只有三份拷贝都写入完成后才算写入完成,这在出现磁盘损坏时会出现写延迟增加。
- 在 OSD 上,在收到数据存放指令后,它会产生 2~3 个磁盘 seek 操作:
- 把写操作记录到 OSD 的 Journal 文件上 (Journal 是为了保证写操作的原子性)。
- 把写操作更新到 Object 对应的文件上。
- 把写操作记录到 PG Log 文件上。
三、客户端请求流程 (转的一只小江的博文,写的挺好的)
RADOS 读对象流程
RADOS 写对象操作流程
例子:
#!/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. 首先 cluster = rados.Rados(conffile = ‘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. 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 将数据消息处理完成之后,就是进行回调,反馈执行结果,然后一步步的将结果告知调用者。
四、Ceph 读流程
OSD 端读消息分发流程
OSD 端读操作处理流程
总体流程图:
int read(inodeno_t ino,
file_layout_t *layout,
snapid_t snap,
uint64_t offset,
uint64_t len,
bufferlist *bl, // ptr to data
int flags,
Context *onfinish,
int op_flags = 0) ——————————–Filer.h
Striper::file_to_extents(cct, ino, layout, offset, len, truncate_size, extents);// 将要读取数据的长度和偏移转化为要访问的对象,extents 沿用了 brtfs 文件系统的概念
objecter->sg_read_trunc(extents, snap, bl, flags, truncate_size, truncate_seq, onfinish, op_flags);// 向 osd 发起请求
对于读操作而言:
1. 客户端直接计算出存储数据所属于的主 osd,直接给主 osd 上发送消息。
2. 主 osd 收到消息后,可以调用 Filestore 直接读取处在底层文件系统中的主 pg 里面的内容然后返回给客户端。具体调用函数在 ReplicatedPG::do_osd_ops 中实现。
CEPH_OSD_OP_MAPEXT||CEPH_OSD_OP_SPARSE_READ
r = osd->store->fiemap(coll, soid, op.extent.offset, op.extent.length, bl);
CEPH_OSD_OP_READ
r = pgbackend->objects_read_sync(soid, miter->first, miter->second, &tmpbl);
五、Ceph 写流程
OSD 端写操作处理流程
而对于写操作而言,由于要保证数据写入的同步性就会复杂很多:
1. 首先客户端会将数据发送给主 osd,
2. 主 osd 同样要先进行写操作预处理,完成后它要发送写消息给其他的从 osd,让他们对副本 pg 进行更改,
3. 从 osd 通过 FileJournal 完成写操作到 Journal 中后发送消息告诉主 osd 说完成,进入 5
4. 当主 osd 收到所有的从 osd 完成写操作的消息后,会通过 FileJournal 完成自身的写操作到 Journal 中。完成后会通知客户端,已经完成了写操作。
5. 主 osd,从 osd 的线程开始工作调用 Filestore 将 Journal 中的数据写入到底层文件系统中。
写的逻辑流程图如图:
从图中我们可以看到写操作分为以下几步:
1.OSD::op_tp 线程从 OSD::op_wq 中拿出来操作如本文开始的图上描述,具体代码流是
ReplicatePG::apply_repop 中创建回调类 C_OSD_OpCommit 和 C_OSD_OpApplied
FileStore::queue_transactions 中创建了回调类 C_JournaledAhead
2.FileJournal::write_thread 线程从 FileJournal::writeq 中拿出来操作,主要就是写数据到具体的 journal 中,具体代码流:
3.Journal::Finisher.finisher_thread 线程从 Journal::Finisher.finish_queue 中拿出来操作,通过调用 C_JournalAhead 留下的回调函数 FileStore:_journaled_ahead,该线程开始工作两件事:首先入底层 FileStore::op_wq 通知开始写,再入 FileStore::ondisk_finisher.finisher_queue 通知可以返回。具体代码流:
4.FileStore::ondisk_finisher.finisher_thread 线程从 FileStore::ondisk_finisher.finisher_queue 中拿出来操作,通过调用 C_OSD_OpCommit 留下来的回调函数 ReplicatePG::op_commit,通知客户端写操作成功
5.FileStore::op_tp 线程池从 FileStore::op_wq 中拿出操作(此处的 OP_WQ 继承了父类 ThreadPool::WorkQueue 重写了_process 和_process_finish 等函数,所以不同于 OSD::op_wq,它有自己的工作流程),首先调用 FileStore::_do_op, 完成后调用 FileStore::_finish_op。
6. FileStore::op_finisher.finisher_thread 线程从 FileStore::op_finisher.finisher_queue 中拿出来操作,通过调用 C_OSD_OpApplied 留下来的回调函数 ReplicatePG::op_applied, 通知数据可读。
此文主要整理了 ceph 客户端读写流程,OSD 端读写流程等。
在 CentOS 7.1 上安装分布式存储系统 Ceph http://www.linuxidc.com/Linux/2015-08/120990.htm
Ceph 环境配置文档 PDF http://www.linuxidc.com/Linux/2013-05/85212.htm
CentOS7 下部署 Ceph 集群(版本 10.2.2)http://www.linuxidc.com/Linux/2017-02/140728.htm
Ceph 的安装过程 http://www.linuxidc.com/Linux/2013-05/85210.htm
如何升级 Ceph 版本及注意事项 http://www.linuxidc.com/Linux/2017-02/140631.htm
HOWTO Install Ceph On FC12, FC 上安装 Ceph 分布式文件系统 http://www.linuxidc.com/Linux/2013-05/85209.htm
实验环境 Ceph 9.2.1 部署笔记 http://www.linuxidc.com/Linux/2016-11/137094.htm
Ubuntu 16.04 快速安装 Ceph 集群 http://www.linuxidc.com/Linux/2016-09/135261.htm
Ceph 的详细介绍 :请点这里
Ceph 的下载地址 :请点这里
本文永久更新链接地址 :http://www.linuxidc.com/Linux/2017-03/141872.htm