拆开Ceph看队列和线程

时间:2021-10-08 17:20:43

作者:吴香伟 发表于 2017/01/08
版权声明:可以任意转载,转载时务必以超链接形式标明文章原始出处和作者信息以及版权声明


我上小学时家离学校很远,家在某某山脚,学校在镇里。每周回家一趟,周五放学后回家,周日带着一星期吃的梅干菜回学校。从家到学校有5公里山路,现在觉得5公里不算长,骑个自行车只要一溜烟的功夫,但那时还小觉得很远。从学校到家的路上零零散散有几个路亭,这些路亭有的建在岔路口,有的建在木桥边,还有的建在转弯处。不同位置的亭子有不同好处,岔路口的宜送别,小桥边的方便在亭里歇个脚后再到桥底洗把脸,转弯处的路亭告诉你前面还有人家。除此之外,路亭还有很多其它功能,下雨天可以供人避雨,艳阳天可以供人乘凉,到了过年过节时还可以供人拜祭各路神仙。对我来说印象最深刻的是期末时在路亭里和小伙伴们相互修改成绩单,开学时相互填家长评语,其乐无穷。

从用户发起请求到数据落盘,Ceph也有很长的IO路径。在这条路径上也同样散落着功能各异的亭子(队列),梅干菜从一个路亭到下一个路亭靠的是两只小脚丫,IO从一个队列到另一个队列靠的是线程(池)。没错,本文要讲的就是路亭和脚夫的故事。不对是队列和线程。

队列和线程的关系

队列和线程在一起,通常称为生产者和消费者模式。生产者向队列添加元素,消费者从队列提取元素。一个队列可以同时拥有多个生产者和多个消费者。

图中的Tgt是改造过的与原生版本略有不同。

Ceph客户端队列

Pending队列

一个Tgt进程有多个Pending队列,一个队列负责来自一个Pool的请求。一个Pending队列只有一个生产者,所有队列共享同一个生产者,这个生产者就是Tgt主线程。Tgt主线程负责接收网络报文,并依据iSCSI协议将报文转换为iSCSI请求。因为要负责接收来自所有Lun的请求,所以主线程并不空闲,如果继续负责请求的处理,必然导致后面的请求不能被及时接收,进而降低Initiator发送请求的速度。网络本来就慢,再延迟接收,将导致性能大大的低。

此时Pending队列起到了缓冲的作用,让主线程只负责请求的接收过程,接收到的请求不用立即处理先缓存到队列,缩短主线程的执行路径从而解决网络请求不能被及时接收的问题。从另一方面来说,Pending队列解耦请求的接收过程和处理过程,将这两个过程分别交给队列的生产者和消费者。

Ok,我们Get到了队列的第一个作用,缓冲,第一个附带作用,解耦。一般在设计队列时考虑缓冲,在分析代码时看解耦。因为有100种解耦方法,队列只是其中之一,解耦并不是设计队列的充分条件。

PointerWq队列

开发者调用Librbd API读写块设备时,请求入PointerWq队列后立即返回给调用者。话音未落,我似乎又Get到了队列的第3个作用:实现异步调用。调用者将请求以及请求结果处理函数一并提交给Rbd层,Rbd层完成请求后回调请求结果处理函数,以此实现异步调用。

PointerWq队列隶属于ImageCtx类,代表一个Image存储块,不论读请求还是写请求都入同个队列。Bs-worker线程池,Pending队列的消费者,PointerWq队列的生产者,主要工作是将一个Pool请求队列分裂成若干个Image请求队列。

Out_q队列

从请求的粒度来看,Image请求在Ceph客户端中有下面几种形态:Image请求、Object请求、Op以及MOsdOp消息。Image请求只关注块设备中的一段区域,一段块设备区域可能对应于多个Object,这些Object可能属于不同PG不同Osd节点。一个Out_q队列对应一个Osd节点,隶属于一个连接,一个OSDSession。

Out_q是个优先级队列(第4个功能),不过对优先级的处理比较粗暴,只有最高优先级的请求全部出队列后才允许次高优先级的请求出队列。Ceph社区中曾有人建议通过请求的优先级来实现QoS,这显然是行不通的,因为特别容易导致低优先级用户饿死。

作为Out_q队列的生产者,Rbd_op_threads线程池的压力不小,一方面默认情况下池中只有一个线程(不知道是怎么考虑的)但要处理所有Image请求;另一方面它处理的逻辑最多,包括在Rbd层把Image请求拆分成Object请求(还要考虑Rbd缓存),在Rados层计算每个Object的目标Osd,以及回调函数的层层封装。虽然事情很多,但总体而言还是计算密集型不存在等待问题。

对Out_q队列的消费者Worker,本文关注的是AsyncMessenger的实现,SimpleMessenger会不一样。Messenger隶属于RadosClient,一个RadosClient实例拥有一个Messenger实例。老版本的SimpleMessenger会为每个连接都创建一个读线程和一个写线程。原生Tgt中为每个存储块创建一个RadosClient对象,一个RadosClient将与一个Osd建立一个连接,一个连接将有两个线程。粗略想想线程数目,心里有点发毛。

AsyncMessenger将工作线程Worker和Messenger解耦,不论有多少个RadosClient实例总线程数目都固定不变。不过和给定Osd节点的连接数还是会随着RadosClient实例的增加递增。一个Worker会同时处理多个连接,主要职责是从队列提取消息发送给Osd进程。

Ceph Osd队列

Osd进程从端口接收数据和Ceph客户端发送数据一样都由Worker线程完成。不同的是,Osd进程是服务端要监听端口。对监听Fd的Worker(处理连接打开和关闭)和属于该监听Fd连接(处理请求接收和响应发送)的Worker是同一个线程。

Osd接收到请求后根据不同的请求类型有两种处理方式:一种是Fast dispatch,要么直接处理掉要么入ShardedOp队列。另一种是入Dispatch队列。客户端读写请求走Fast dispatch路径,命令或者更新OsdMap版本的请求入Dispatch队列。

Mqueue分发队列

Mqueue是个优先级队列。

(待续)

参考资料

源码