Client端代码分析
通过之前的研究我们知道IoCtxImpl是client端真正实现写入的方法,在client端会将所有的操作封装成ObjectOperation对象。而对应的异步操作,处理更加的复杂一些,如下图:
同样的一个操作,在异步接口中先构造一个异步的回调函数,然后构建异步的op对象,之后将op对象提交到一个队列中,在rocksdb中主要用到的是同步的接口,所以我们优先分析同步接口。
我们再回到同步接口的分析中,虽然同步与异步都是构建operation对象,但是两个op是完全不同的:
以上的几个图片我们可以看到在同步接口中,主要使用的是ObjectOperation对象,对这个对象进行一些初步的分析:
在这个对象中主要有如上的一些属性,以及对象的工具方法,由于该类也没有注释,从外部来看主要有flags标识符,priority优先级,out_bl是一个bufferlist的集合,这个集合应该是存储数据的主要结构,ops是在osd_type.h中定义的一个结构,这个对象也没有明确的注释,估计是存放了对象的object的操作类型,比如读,写,追加等等,其他两个属性的用处还无法猜测,只能继续来进行学习。
按照接下来的逻辑,将op通过prepare构造好,并使用write_full将对象的数据写入op中后,就调用operate方法来继续进行处理,operate这个方法也是所有的同步接口都需要走的一个方法,所以说这个方法是关键的方法。
在operate方法中,在开始的地方初始化了一些时间,并进行了一些校验工作,然后就是很关键的一个条件变量锁的使用,这个锁是一个很典型的锁,之前用的很少,而且这个锁又很关键,因此对这个锁的一些用法也进行了相关的研究。
首先条件变量锁,对应的linux底层函数为pthread_cond_wait,其实就是上图中的cond.wait()这个方法,这里有一些区别的原因是ceph对linux底层的函数进行了封装,使之更加易用和安全,条件变量为C_SafeCond这个对象,在这个对象中我们可以看到如下代码:
在这个方法中,我们看到了条件变量中的另一个关键的函数,发送信号,即cond->signal(),这个函数对应的linux的pthread_cond_signal与pthread_cond_broadcast这两个函数。
这个锁保证了,方法在提交了op之后,只有op处理结束,调用了finish方法,该方法才会继续执行,正常结束,我的理解是ceph在该方法中虽然也用到了队列,缓存等方法,但是通过这种方式达到了同步的目的,因此这些接口都是同步接口。
那么这段逻辑大致就可以理解为,方法将op提交到队列后立即对互斥锁加锁,而wait方法执行时第一件事就是对互斥锁解锁,然后等待信号量,此时当finish方法获取锁之后发送信号激活主线程,让主线程继续执行。
在理解了这段代码的工作方式后,我们需要继续看另外一个关键的方法,如下图:
即将初始化好的op对象进行提交。
提交之后,ceph进行了很多层的方法调用,这些方法一层一层会记录一些操作的大体轨迹,流程为op_submit->_op_submit_with_budget->_op_submit,其中在_op_submit_with_budget这个方法中有一段代码值得关注:
这段我的理解是,代码中osd_timeout是一个配置项,当该配置项大于0时,代码会添加一个超时的事件,当操作执行超时会报错,而我们目前的默认配置就是0,也就意味着,如果有操作超时,那么这个操作就会一直阻塞,如过是我理解的这样的话,这个配置项可以优化,说不定可以解决我们的丢数据问题。
这里为了弄清此处的问题,需要对ceph中的timer进行一个大概的了解,这个方法中的timer类在ceph_timer.h中,这个类也是一个比较复杂的类,调用机制比较繁琐,但是从类的注释中,我们大概可以知道这个类的功能与我们构想的类似
接下来就是_op_submit这个函数的调用,这个函数中做了client端的主要的工作,
这个方法的开始会根据op的信息计算出这个对象的目标osd节点,当计算出目标节点后,使用get_session方法获取一个链接来和对应的osd通信。
接下来的一大段逻辑判断,我的理解是主要是对osdmap的同步与校验,防止osdmap因某些问题与实际情况不符合,这部分代码分支很多,也没有注释,我认为比较好的定位方法是,当丢数据时将这里的日志打印一下,因为这些异常分支从代码层面上看都是有异常信息打印的。
另外提一下OSDMap这个类,这个类定义在OSDMap.h这个头文件中,如下图:
我们可以看出这个类中记录了集群中osd的全部信息,里面的数据很多,具体的含义我们可以通过字面的意思进行初步的分析,本次分析远源码的重点不在这里,因此先继续向后面查看代码的逻辑。
我们继续之前的逻辑,我们发现在经过前面一系列的判断校验之后主要还是通过send_op这个方法将构建好的op发出去,如下图:
这个方法的内部最关键的逻辑如下:
即通过相关的链接将消息发出去。这里会产生一个问题,在ceph中发送消息都是由Messenger这个对象来进行的,而ceph的Messenger有SimpleMessenger有AsyncMessenger等,虽然说在比较新的版本都使用了AsyncMessenger这个接口,但是在client端到底时如何操作的,我们可以从下图的逻辑代码来分析
我们之前分析过,在客户端使用的时候会初始化Rados这个类中会创建一个RadosClient类,而Rados的connect实际上就是RadosClient的connect()而message就是在这个过程中初始化的。
在这里我有一个疑问,按上面分析的逻辑消息是发出去了,但是发出去后改方法也没有什么返回值,那么发送的消息client端到底是怎么确定消息是否成功的呢?我比较疑惑,但是也不知道代码要从哪里去看。
在这里我尝试从AsyncMessenger这个类去分析发送的源码,因为从这个图中我们可以看出该调用并没有明确的返回值,那么发送成功与失败到底如何保证呢?其中op->session->con其实是一个Messenger类,而从前面的分析与我们对版本12.2.4版本的了解,可以知道这个Messenger其实就是AsyncMessenger,而AsyncMessenger的send_message实现如下
这里的代码逻辑比较长也非常复杂,流程大致为send_messenger--_send_messenger----submit_messenger-------------con->send_message,流程走到这里似乎断了,但是其实这个con就是AsyncConnection,因此我们要去AsyncConnection中去找send_message
在AsyncConnection中也确实存在send_message这个类,在这部分代码中我没有找到类似于网络发送的代码,反而找到的是如下的类似入队的代码,
如上红圈所示,当代码看到这里,我感觉这里的发送没那么简单,要想搞清楚这部分内容,可能需要学习一下AsyncMessenger的相关技术与代码,这里也是ceph代码中很大的一块。
AsyncMessenger引出的通信模块的一些学习
关键的几个概念
Ceph的网络层从上来说是有一套宏观的抽象类的,我们研究的AsyncMessenger只是一种抽象的网络类型,这个类型是近些年ceph开始主要使用的,在之前ceph一直使用simple这种类型,而这种类型因为诸多的弊端,现在已经被Async替代,但是因为ceph良好的代码封装,上层的接口没有变化,因此我们先对上层的一些概念进行理解。
-
Messenger:这个也就是AsyncMessenger的基类,按我的理解这个类有两个作用,对下将消息发送给Connection,由Connection将消息通过网络发走。对上,他会将接到的消息进行分类然后分发给不同的Dispatcher,Dispathcer的作用我们下面介绍,Messenger里面包含了Dispatcher的集合,也包含了一些connection。
每个Messenger中还包含Processor这个关键的对象
当Messenger初始化是执行了bind接口,这个类中包含一个监听的socket会处理收到的请求
在processor的bind方法里面会有如下代码
这段代码中work监听了制定的socket,而这里的worker是个抽象类,具体的实现如下
这里实现了设置网络参数并实现监听。 - Dispathcer:我的理解是这个角色不涉及网络,他其实是将接到的消息发送给具体处理消息的逻辑。
- Connection:这个是AsyncConnection的基类,是ceph的连接实例,负责维护同客户端建立的socket连接以及ceph的协议栈操作接口,向上提供发送消息接口及转发底层消息给DispathcQueue或者消息管理器,向下发送和接收消息。
- Message:这个概念比较好理解,这个是所有消息的基类。
理解了以上几个关键的概念之后,接下来说一下核心的模块
- 发送消息:首先是发送消息,消息发送者按照消息类型将消息封装好,然后使用Messenger接口的send_message方法就可以将消息发送出去,我们在client端看到的方法就是使用了AsyncMessenger的send_message方法。
- 接收消息:Messenger是如何将消息转给下部具体处理消息的逻辑呢?我的理解是这里主要靠分发器,即Dispatcher,Messenger设计了两个分发器管理成员:dispatchers和fast_dispatchers,用来处理不同类型的请求处理,目前来说我也无法很清楚的说出这两个分发器的区别,但是从大体上看,fast_dispather对应的这种分发器应该是忽略了一些底层流程使得分发可以更快一些(比如跳过入队操作)。应用层按需求将不同的分发器注册给Messenger,进而Messenger接收到底层来的消息时,会将消息分发给已经注册的两个dispatchers。Dispatcher类是一个基类,里面设计封装了应用同Messenger交互的接口(每个具体的Dispatcher派生类自行去实现更具体的消息处理,一般都是再根据消息类型来分开处理消息)。Dispatcher并不是所有的接口封装都是为了转发消息,它更深层次的含义是提供一个应用层和底层的通信接口,而这个接口的桥梁是Messenger消息管理器。 当底层有消息到来时,Messenger会将消息转给dispatcher对于的ms_*系列的接口,最常用的是ms_dispatch接口,因此你可以看到像monitor,osd这些应用的核心消息处理都在ms_dispatch接口里面实现。
除了上面这些东西,还需要大致理解一下Dispatcher的使用: - 比较简单的用法是下层逻辑直接作为Dispatcher的派生类,Messenger直接与相关的处理逻辑关联,比如osd,monitor,mgr都是应用组件本身作为Dispatcher的派生类。
-
申请一个Dispatcher的派生类实例,做为应用的模块注册给Messenger,比如RadosClient里面会注册各个Client给Messenger,而这些Client都是Dispatcher的派生类。
除了以上的内容,再对Message进行一些说明,Message里面有一个关键的属性type,这些type定义在Message.h中。
如上图所示,定义很多,我这里只截取一部分。
Async的学习
上面的一些概念,是ceph对网络宏观上的抽象,Async这种类型的Messenger是怎样的工作原理呢?再了解该模块的开始我们先看以下该模块的源码目录,所包含的类如下:
从上面的类中我们可以看到前面所说的AsyncMessenger与AsyncConnection类,还有一些Event*之类的类不明白是干什么的,为了能够更好的学习这部分知识,因此先对这些内容做了一些学习。
首先是五种I/O类型
1.blocking I/O
2.nonblocking I/O
3.I/O multiplexing (select and poll)
4.signal driven I/O (SIGIO)
5.asynchronous I/O (the POSIX aio_functions)
这几种IO类型我们在这里不做详细的分析,我们直入主题,直接分析ceph中使用的IO类型,即类型3。
类型3的模型图如下:
这种模型其实就是select和poll模型,select会先阻塞,当内核中有数据准备好后,select才会返回,这是后通知处理逻辑来对数据进行处理。而我们在源码目录下看到的Epoll与Kqueue其实就是更高级的select,高级在哪呢?主要是这两种调用直接使用callback来代替了轮询,每次select()都要通过遍历FD_SETSIZE个Socket来完成调度,不管哪个Socket是活跃的,都遍历一遍。这会浪费很多CPU时间。如果能给套接字注册某个回调函数,当他们活跃时,自动完成相关操作,那就避免了轮询,这正是epoll与kqueue做的。而epoll与kqueue的区别,以我的理解,主要是看操作系统的支持,像我们使用的linux服务器主要支持的就是epoll,所以为了抓住主要问题,我们最关键的就是要研究Epoll这种机制。
epoll可以理解为event poll,不同于select的忙轮询和无差别轮询,epoll之会把哪个流发生了怎样的I/O事件通知我们。此时我们对这些流的操作都是有意义的。(复杂度降低到了O(k),k为产生I/O事件的流的个数,也有认为O(1))。
对于这个机制有了大概的理解之后我们先结合ceph的源码,看看ceph的源码是如何与epoll这种机制关联上的,在该模块的源码目录下首先我们看到的是Event.h这个类,进入这个类中我们看到了如下的内容:
这个头文件中首先包含了一个事件回调类,这个类与epoll的回调是否能关联起来,还不好确定。
这个类少有的添加了一些注释,从注释来看,这个类封装了不同操作系统之间的事件机制,也就是epoll,kqueue,select,注释中也说明了linux下主要使用的是epoll所以在后面的研究中我们只看epoll。
看到这里我们很容易联想到该模块源码目录下的其他几个类,如下图
这几个类其实就是EventDriver的具体实现
经过查看确实如此。
除了上面的EventDriver之外,还定义了一个EventCenter
这个类的注释也写明了,该类的作用是维护文件描述符然后操作注册的事件,虽然看到这几个类之后感觉从字面上都知道这几个上层类是干什么用的,但其实这几个类之间到底什么原理,一点也不懂,还是需要再继续研究。
此时我们再回到之前分析的AsyncConnection的send_message处
在之前我们已经研究到发送逻辑中有一个入队操作,有一个分发回调句柄的操作。
队列的结构如上图,分发回调句柄的实现如下
这里的逻辑其实是唤醒epoll_wait,那么这个操作到底在哪里呢?因为此处代码是断层的因此只能通篇的找。通过很长事件的概览代码,发现这里大致的流程是这样的
上面这些图就是发送消息时,所走的大概逻辑,看着很容易其实花了很长的时间,而代码看到这里问题又来了cb是一个EventCallBack实例,那么这个东西的具体实现到底是什么?
这时我们在回忆之前AsyncConnection的代码我们注册了一个write_handler这个回调,这个回调在初始化时有,如下图
因此我们可以得知,上面的do_request是C_handle_write的实现,实现代码如下
这个也比较简单,调用了AsyncConnection的handle_write()方法,该方法的实现如下图
在这个try_send中,终于看到了类似于发送的东西,而这个cs的类如下
进入到这个类中发现这个类也是个抽象的父类,查找相关子类的具体实现,如下
到了这层逻辑,已经到了内核网络接口的调用,也就到底了。分析到这一步,我大概理解了一些东西,当ceph将数据发出去后,使用epoll技术可以要么发成功,如果不成功报错,他能够保证发送端健康发送,但是数据发送到server端后,server端在经过处理后,假设处理有问题,client端怎么知道呢?因为我们研究的write_full接口是一个同步的接口,按我们对同步的一般理解,如果发送成功,server端是需要告知是否处理成功的,这部分ceph是怎么做的,还需要接着看。
Client端对于回调的处理
在经过了上面一章的分析,我们基本能够知道client端发送消息的逻辑,但是还是有一个关键问题,没有分析清楚,通过前面的分析我们知道write_full这类接口是一个同步的接口,也就是说,当消息发送之后,client端需要等待消息处理的结果
根据我们之前代码的分析,肯定需要有一个操作,来调用这个finish方法,从而触发条件变量让这个操作结束,但是这个方法到底在哪里调用的不得而知,而c++的代码又不能看调用的关系,很多地方因为使用的是抽象父类的方法,所以搜finish这个关键次根本没用,所以只能继续通篇来阅读源码。
在继续阅读源码过程中我发现在RadosClient类的connet方法中有一些初始化的操作
这里面初始化的这个Objecter是一个Dispatcher,通过前面几章的学习我们知道继承了Dispatcher类的实例,是用来处理网络回应的,如果server端将回应发回来,很有可能就是在这个类中来处理的。
我们找到objector中的ms_dispatch方法,发现在其中有如下的处理逻辑
这个分支就是来处理osd回应的操作的,我们进入到这个方法中去,接下来有一大部分异常处理,类似于接到错误的消息后重发的操作,如下
这些逻辑没有注释,也很琐碎,代码很长,抛开这些逻辑来看,我们从这段逻辑中最终找到了我们想找的逻辑,如下图
也就是回调函数的执行,这个方法就会调用到前面回调类的finish方法,让条件变量触发从而结束整个写入的流程。
到这里,按我的理解,整个client端的代码流程逻辑算是完成了一个闭环,当然client端里面的各种if分支实在是种类繁多,没有注释的情况下,想要一一弄明白,我觉得不现实,因此梳理到这一步,我打算继续来进行server端代码的研究。
另外,按照我的理解,client端虽然是外层逻辑,但是通过分析已经能够得出一些结论,首先正常的流程发送了消息后,通过async接口保证将数据发出去,不如不发处会有错误,而后端接到消息后,一定会给client端一个回应,client端要么接到错的回应重发,要么阻塞,这里我们确实有个配置项配置的不合理,这个在前面有提到,如果我们将那个超时的配置增加上,杜绝一直阻塞的问题,那么按我的理解,client端就能做到,要么发送成功,要么失败,目前有种可能是一直阻塞。
Server端代码学习
在server端源码学习的过程中,因为时间的关系,我们直接来看流程相关的代码,首先osd的server端的启动都在ceph_osd.cc类中,这个类是osd进程的主类,osd相关的初始化工作都是在这个类中进行的。
在ceph_osd这个主进程中处理osd相关请求的逻辑主要封装在OSD这个类中,OSD类的初始化代码如下
这个类构造之前还有一些相关准备工作的代码,我们暂时不详细描述,先看关键的代码,首先是OSD类,这个类同样的继承了Dispatcher这个抽象类
根据之前代码的学习,我们知道继承了这个抽象接口的类一般对相关逻辑的处理都在ms_dispatch或者ms_fast_dispatch之中,研究代码发现在ms_dispatch之中主要处理的是一些PG或者OSD级的消息,没发现对于请求处理方面的逻辑,如下图
所以,我们继续看ms_fast_dispatch,在这个逻辑中首先进行了一些校验,并将消息组装成op结构。
之后下面跟了一个逻辑判断,根据判断条件,我们应该走到下图红圈处的逻辑
在这里我们找到了op的入队操作
看到这个操作,我们大概可以分析出,osd端接到消息之后会将消息入队缓存,在初始化时应该有线程启动,会一直从这个队列中获取op请求来进行处理,这个线程时从哪个地方初始化并一步一步调用到出队这个方法的,这里我们暂时不考虑,我们直接从出队列这个方法来看,因为这个方法就在enqueue_op方法定义的下端。
我们看到入队方法下面紧接着就是出队方法的定义,我们可以直接基于这个方法来进行分析。这个方法中的代码比较少,能够很方便的找到主要的流程,如下图
这里是对op的处理逻辑,此时问题又出现了这个dp_request方法跳不进去,我们需要找到PG的实现类,这里的PG是如下图的一个类
这个类虽说是个父类,但是大概有2000多行的代码,类的dp_request是个虚函数如下图,需要子类去实现
通过全局的搜索,我们找到了Pg的一个子类为PrimaryLogPG
该子类的具体实现类如下:
在方法实现的刚开始,大概有200多行的代码,这部分代码按我的理解是做了一些osdmap于session的准备工作,而关于op的核心处理是在一个标准的switch…case语句中,如下图
这个switch根据op的不同类型对数据进行了不同的处理。在这个流程中有两个关键的逻辑调用,一个是reply_op_error即如果出错了,向client端返回响应,相关的实现逻辑如下
在这个方法中我们看到了明确的网络消息调用。
在上面switch…case中另外一个主要的流程是do_op
这个流程贯彻了ceph的一贯风格,做了很多的正确性与安全行的校验,我觉得向ceph里面写数据,安全归安全,但是这种正确性可靠性的校验势必会影响性能;这个方法做了近500多行op的处理工作,内部包含的逻辑判断在这里不进行详细的分析,我们接下来直接找op的处理流程。在这里op会被封装成一个新的对象来继续处理。
封装成改对象之后,对context的主要处理逻辑封装在execute_ctx函数中,该函数的大致实现逻辑如下图
这个方法中将ctx进行了再一次的封装,变成了transaction即事务这个概念,
在整个execute_ctx的流程中,处理的逻辑也比较多,根据这次的目的,我认为主要还是看,在处理流程中假设有些过程遇到异常,这个处理有没有正确的向client端返回,根据对代码的分析,在这个流程中主要是通过MOSDOpReply构造这个类来向client端进行回应的,如下图
这个是众多异常逻辑中的一个异常逻辑的处理,这个部分并不难理解,在处理流程中如果遇到异常,就构造MOSDOpReply对象,将结果记录到这个对象中去,在合适的时候将这个reply发送出去,上面代码段的发送逻辑就封装在第二个红圈处,如下图
因为这个方法中处理的op有读有写,我们接下来着重来看写相关的流程,与写相关的关键的逻辑调用为如下的代码
这个操作向各个副本发送同步操作的请求,刚看这里时有个令我很疑惑的点,就是这个repop是什么意思,在ceph的源码中有很多有这种单词的变量与方法,从字面意思根本解释不通,经过反复的琢磨,我觉得比较合理的解释应该是这个repop是个缩写,rep是replication的缩写,op就是前面封装的op对象,所以这个方法的字面意思就是将op发布到备份节点上,这个解释比较合理一些。在大概理解了这个方法要干的事情后,我的理解是目前的代码逻辑,与我们之前的相关逻辑的猜想是相符的,即数据通过client端发送过来之后,主osd的server端对数据进行相关的处理之后,将数据同步到备份节点上,这个issue_repop主要就是干这个事情的。在这个方法中ceph一口气构造了3个回调
在构造了这几个回调之后,就进入了后端的事务处理流程,如下图
在这里我们先不进行submit_transation方法的分析,因为这个处理又是一个新的流程,我比较关心的是issue_repop这个方法是如何确定成功的,因为从代码上看这个方法是没有返回值的,那么执行了相关的操作后ceph如何确定相关的操作有没有成功呢?就比如说这个submit_transation方法。在这部分代码中,我们又看到了老面孔,即这个回调,按照预计,ceph应该也是根据这个回调来确定操作确实处理完了,我们跳出这个方法继续往下看,发现了eval_repop这个函数,进入到这个函数中,果然发现了回调相关的判断。
在这个方法中我们看到了对回调的处理,如果向备份节点发送op正确或者异常都会在这里进行处理。
接下来我们掉过头来继续分析写的流程,关于写的流程这里首先要跳回到prepare_transaction这个函数,为什么要回到这个函数呢,因为这个函数看似是个准备,但其实在这个函数中处理了相当多的逻辑,比较简单的read流程,更是直接在这个函数中就做完了,而写的流程因为比较复杂还涉及到要和备份的OSD同步数据,所以这里只做了一部分工作,另一部分工作就在issue_repop这里做的。
这个函数中关键的调用是上图红圈处的逻辑
这个函数非常长,涉及到了很多类型的OP的处理,其中就有读的和写的,读的流程我们暂不关注,但是还是能看出这种按照类型的处理逻辑
我们重点关注写的流程代码,代码大致如下
在写这个过程比较关键的代码就是下图所示的这个write方法
这个方法是向transations中填充数据
这一部分是很容易理解的,就是说把操作码设置为OP_WRITE,记录好要写入的object,将offset 和length设置正确,同时将要写入的data纪录下来,后续ObjectStore部分(更具体地说是filestore),就可以根据上述信息,完成写入底层对象存储的动作。
上述内容仅仅是一个部分,之前也提过,除了data,还有PGLog,这部分内容是为了纪录各个副本之间的写入情况,预防异常发生。prepare_transaction函数的最后,会调用finish_ctx函数,finish_ctx函数里就会调用ctx->log.push_back就会构造pg_log_entry_t插入到vector log里。
梳理完这部分关键的逻辑之后,我们继续来看主OSD与备份OSD通信的相关流程,上面提到过回调,如下图
在看到这几个回调之后,大家估计都会有疑问,什么是commit,什么是applied这两个过程是什么?经过源码分析与一些网上资料的查询,我的理解是这样的,client端只会和Primary OSD交互,而Primary OSD会和Replica OSD交互,当Replica OSD完成写入后,要发消息给Primary OSD,当Primary OSD将各个Replica OSD的消息汇总之后,再给client发送回应。
在这里,需要了解的一个概念是无论Primary还是Replica,他们在写入数据的时候,都分为两部,首先是写Journal日志,然后是数据落磁盘,而这个过程中Replica会给Primary发送两次消息,第一次消息在Journal日志写完之后发送,第二次消息在数据落盘后发送给Primary,在每个阶段Primary收到所有Replica的回应后,都会发相应的消息告知client。
关于这些状态,都在submit_transaction方法中记录
关于submit_transaction方法,我们先粗略的介绍一下,首先PGBackend是一个抽象类,类的说明如下
类的功能在注释上说的比较明确,这个类有两个抽象的实现,如下图
这两个实现在源码的osd目录下,这两个实现类都比较好理解,一个代表的是纠删码的方式,一个代表的是备份的方式,我们基本上不用纠删码这种方式,所以我们以研究备份这种方式为主,下面我们就进入到ReplicatedBackend这种方式的submit_transaction方法中。在这个代码中我们发现了如下的数据结构
这个数据结构就是用来记录前面提到的那些状态的。在这个结构中维护着两个集合,如下图
这两个集合中第一个集合中维护着尚未完成第一阶段工作的OSD的集合,第二个集合存放着尚未完成第二阶段工作的OSD的集合。
有了这两个集合,必然会涉及到这两个集合状态的更新,很明显,当Replica OSD或者Primary OSD完成第一阶段或者第二阶段任务的时候,都必然会通知到Primary OSD,更新这两个集合中的元素: 如何更新?这里就又用到了我们经常看到的回调,我感觉回调这种机制在ceph的代码中贯彻始终,用的很多。
我们先不谈回调,继续来走逻辑,在有了inprogressop这个结构后,我们往下走就会看到如下的逻辑
这个逻辑就是向Replica OSD发送消息的逻辑,我们看一下这个逻辑的主体代码
我们很容易就能看到类似于发送消息的代码逻辑。我们看到了,在循环体中,会遍历所有的Replica OSD,向对应的OSD发送消息,而消息体的组装,是在generate_subop函数中,我们进入该函数。
进入该函数后我们可以看出该函数的主体是这个MOSDRepOp对象,这个函数的构造函数如下
在上图中我们可以看出这个消息的类型是MSG_OSD_REPOP,在Primary发送了这种消息之后,Replica OSD会和Primary前面的逻辑一样,进入到队列,然后从osd.op_wq中取出消息进行处理。当走到do_request函数之后,并没有机会执行do_op,或者do_sub_op之类的函数,而是被handle_message函数拦截了,我们回过头来看一下这里的逻辑
先进入dequeue的do_request,在这个request方法中我们需要注意如下的代码
这个方法会将primary发送给replication的方法拦截住,使得这次的方法不会走到primary之前走到的switch..case语句中,我们看一下这个handle_message的实现
这个父类的handle_message会处理两周类型的消息,外加调用_handle_message这个方法,而实现的类的具体实现的方法如下
在这里我们看到了刚才的那种消息类型,并且使用do_repop方法对这个消息进行了处理,进入这个方法我们发现这个方法比较长,其中有部分代码和之前primary的代码很相似如下图
上面这个图是备份节点的,下面这个图是主节点的,很像,原因是很简单的,即Primary OSD 和Replica OSD 本身要执行的操作,原本是一样的,只不过存在Primary OSD肩负着和Client通信的责任,而Replica 并没有这种责任,但是Replica需要及时向Primary OSD汇报进度。
这部分都有两个回调,一个回调表示数据写入了journal日志,另一个回调表示数据落盘,这两个回调每个完成后都要给主OSD发消息。其中commit表示数据已经写入到了osd的journal,apply表示数据已经写入到磁盘的data partition。我们挑一个回调函数的实现看一下,就会发现这个函数主要工作就是给主OSD发送消息。
在完成了这一系列流程后,整个写入的流程算是完整了,接下来就是OS层对数据的处理了。
关于slow request日志的一些代码学习
rocksdb丢数据时ceph集群一般伴随着slow request的状态,因此在看代码过程中全局搜索了slow request的相关代码,看看有没有什么进展
这个实在PGMap.cc类中找到的一个slow request其中涉及的参数在实际集群中如下
也就是说请求已经阻塞超过了32秒,这个类之前还有这样一段逻辑代码
翻了翻这段代码的上下文,感觉这段代码主要工作时判断一些PG的状态,而发生这些状态的原因骑士并不在这里,比如这个slow request从字面上来看我们已经能够理解这个状态的大致含义,但是后端阻塞的原因我们在这里看不出什么。途中的这个osd_sum是一个osd_stat_t类型的结构体,这个结构体内部记录了一些metric信息