QEMU中librbd相关线程和回调及IO写流程简要介绍




概念介绍

Image

对应于LVM的Logical Volume,是能被attach/detach到VM的载体。在RBD中,Image的数据有多个Object组成。

Snapshot

Image的某一个特定时刻的状态,只能读不能写但是可以将Image回滚到某一个Snapshot状态。Snapshot必定属于某一个Image。

Clone

为Image的某一个Snapshot的状态复制变成一个Image。如ImageA有一个Snapshot-1,clone是根据ImageA的Snapshot-1克隆得到ImageB。ImageB此时的状态与Snapshot-1完全一致,区别在于ImageB此时可写,并且拥有Image的相应能力。

元数据

striping

  • order:22,The size of objects we stripe over is a power of two, specifically 2^[order] bytes. The default is 22, or 4 MB.
  • stripe_unit:4M,Each [stripe_unit] contiguous bytes are stored adjacently in the same object, before we move on to the next object.
  • stripe_count:1,After we write [stripe_unit] bytes to [stripe_count] objects, we loop back to the initial object and write another stripe, until the object reaches its maximum size (as specified by [order]. At that point, we move on to the next [stripe_count] objects.

root@ceph1 ~ $ rados -p rbd ls

  • rbd_header.1bdfd6b8b4567:保存image元数据(rbd info的信息)
  • rbd_directory:保存所有image的id和名称列表
  • rbd_info:“overwrite validated”,EC pool使用?
  • rbd_id.vol1:保存image的id
  • rbd_data.233546b8b4567.0000000000000025:保存image数据的对象,按需分配,233546b8b4567为image id,0000000000000025为stripe_unit id,从0开始增长

参考:

  1. http://hustcat.github.io/rbd-image-internal-in-ceph/
  2. http://tracker.ceph.com/issues/19081

回调

回调类

3个特征:

  1. 类名称以C_开头
  2. 实现了finish成员函数
  3. Context子类

举例:

还有一种回调适配器类,通过模板类实现通用的回调类,可以把各种类转换成回调类:

之后通过回调生成函数create_xxx_callback(create_context_callback、create_async_context_callback)函数创建出回调类,供后续注册使用。

回调适配函数

通过模板函数将任意函数转换为回调函数。

为啥不直接用原始函数作为回调函数注册进去?

回调生成函数

create_context_callback、create_async_context_callback上面已经介绍过,这里主要介绍create_rados_callback:

这个函数只做了一件事,就是创建一个rados操作需要的AioCompletion回调类(与上面),而回调类里的回调函数,则是用上面提到的回调适配函数转换的,把普通函数转换为回调函数。

回调注册

有如下几种方式:

  1. 直接注册:通常在最外层,对外接口中使用,一般需要在librbd内部二次封装
  2. 通过回调生成函数:librbd内部使用较多
  3. 通过回调适配函数:librbd内部使用较多

回调与Finisher线程的关系

回调类为啥必须继承Context?

这是因为所有的回调都由finisher线程处理(执行体为Finisher::finisher_thread_entry),而该线程会调用回调类的complete成员函数,Context类实现了这个函数,专门用来作为回调公共类。只是为了方便、统一,并不是必须的,你可以可以自己实现回调类的complete成员函数,而不继承Context。

参考下面finisher thread的关联队列finisher_queue、finisher_queue_rval的入队过程,可了解回调入队过程。

回调流

在rbd image打开过程中,需要执行很多流程来获取image的各种元数据信息(流程描述参考OpenRequest的注释,主要包括V2_DETECT_HEADER、V2_GET_ID|NAME、V2_GET_IMMUTABLE_METADATA、V2_GET_STRIPE_UNIT_COUNT、V2_GET_CREATE_TIMESTAMP、V2_GET_DATA_POOL等),当然你也可以在一个方法中一次获取全部元数据,但会导致单次操作耗时太长,各元数据的获取函数耦合也比较重,这是我个人的猜测,也可能其他方面的考虑,目前还没有理解。

librbd中用回调流的方式,来依次调用各个元数据请求函数和响应处理函数,入口是rbd_open,第一个执行的元数据请求函数是send_v2_detect_header(发送检查是否为v2版本image header的请求),qemu的具体调用栈如下:

通过直接调用+设置回调再调用形成回调流,最后进入send_v2_apply_metadata,它会注册最后一个回调handle_v2_apply_metadata。

控制流

  • 请求:由RadosClient、MgrClient及其成员函数处理,一般是普通dispatch流程,最终都交给AsyncMessenger发送出去
  • 响应:AsyncMessenger相关方法

数据流

由Objecter类及其成员函数处理,一般是fast dispatch流程,最终都交给AsyncMessenger发送出去

数据结构及IO数据流转

控制流

Context

所有回调的基类

CephContext

所有操作都需要用到,存储了各种全局信息,每个client一个(librbd算一个client)

ImageCtx

存储image的全局信息,每个image一个

ContextWQ

IO控制流的工作队列类(包含队列和处理方法),op_work_queue对象

librados::IoCtx、IoCtxImpl

与rados交互所需的全局信息,一个对外一个内部使用,一个pool一个

Finisher、Finisher::FinisherThread

回调执行类,专门管理回调队列并在线程中调用各种回调

数据流

AsyncConnection

与ceph服务端连接信息,由AsyncMessenger维护,所有请求都由其发送,AsyncConnection::process

librbdioAioCompletion

用户层发起的异步IO完成后的librbd内部回调,主要用来记录perf counter信息,以及IO请求发起用户传入的外部回调函数

librbd::ThreadPoolSingleton

封装ThreadPool,实现tp_librbd单例线程

ThreadPool

所有线程池的基类

ThreadPool::PointerWQ

IO数据流、控制流工作队列的共同基类

librbdioImageRequestWQ

IO数据流的工作队列类(包含队列和处理方法),io_work_queue对象

librbdioImageRequest

IO请求的基类,image级别,对应用户IO请求

librbdioAbstractImageWriteRequest

IO写请求的抽象类,继承自ImageRequest

librbdioImageWriteRequest

IO写请求类,继承自AbstractImageWriteRequest

Thread

所有线程、线程池的基类,子类通过start函数启动各自的entry函数进入thread执行体完成实际工作。

Objecter

上层单次IO操作对象,对应用户IO请求

Objecter::Op

上层IO操作对象可能包含多个object,需要拆分成多个Op,对应到rados对象

Dispatcher

与服务端交互的分发方法基类,MgrClient、Objecter、RadosClient都继承自Dispatcher类

Striper

IO封装、解封,读写操作过程中从IO到object互相转换

librbdioObjectRequest、librbdioObjectReadRequest、librbdioAbstractObjectWriteRequest、librbdioObjectWriteRequest

用户IO请求拆分后的object级别的IO请求

线程池与队列

tp_librbd(librbd::thread_pool)

tp_thread启动(处理io_work_queue及op_work_queue):ThreadPoolstart–ThreadPoolstart_threads–new WorkThread(this)–Threadcreate–Threadtry_create–pthread_create–Thread::_entry_func–Threadentry_wrapper–ThreadPoolWorkThread::entry–线程启动完毕,worker开始工作

关联队列1:io_work_queue

入队过程:见下面主要代码流程部分,从ImageRequestWQ::aio_write()到入队io_work_queue。

关联队列2:op_work_queue

入队过程:搜索op_work_queue->queue()即可找到,主要是执行各种rbd image控制操作时会用到。

两个队列的关系及出队过程

由tp_librbd(ThreadPool)的work_queues成员保存,work_queues[0] == op_work_queue,work_queues[1] == io_work_queue。在ThreadPool::worker里会死循环处理这两个队列,交替处理。

io_work_queue出队过程:ThreadPoolworker–ThreadPoolPointerWQ_void_dequeue/_void_process/_void_process_finish–ThreadPoolPointerWQ<librbdioImageRequestlibrbd::ImageCtx >_void_process–librbdio::ImageRequestWQlibrbd::ImageCtx::process

op_work_queue出队过程类似,只是最终调用的是ContextWQ::process。

finisher thread

执行体

Finisher::finisher_thread_entry

thread1:fn-radosclient

  • 启动及用途:libradosRadosClientconnect里启动的finisher thread,为rados client服务,用来执行相关回调

thread2:fn_anonymous

  • 启动及用途:MonClient::init里启动的finisher thread,为monitor client服务,用来执行相关回调
  • 与fn-radosclient的区别:anonymous不会通过perfcounter记录队列长度(queue_len),处理延时(complete_latency),而fn-radosclient会记录

thread3:taskfin_librbd

  • 启动及用途:主要用来给ImageWatcher对象执行各种任务(基于SafeTimer定时的或者基于finisher_queue的),ImageWatcher主要是在镜像属性变动的发送通知给关注方。
  • 入队过程与其他两个类似,看queue方法调用位置即可。

关联队列:Finisher::finisher_queue、finisher_queue_rval

二者区别见注释:

  • 入队过程:所有调用Finisher::queue函数的地方(一般都是finisher.queue,如c->io->client->finisher.queue),
  • 出队过程:线程执行体Finisher::finisher_thread_entry里面出队

入队过程示例(fn-radosclient线程):

handle_write_object是write_object函数注册的回调,属于tp_librbd线程,也即处理io的线程。

rados_completion回调最终传递给了ObjecterOponfinish(经过一次封装:C_aio_Complete(c)),实现了从tp_librbd线程转到msgr-worker-*线程,再到fn-radosclient线程(也即Finisher线程)的流转,这也是(几乎)所有回调都由Finisher线程调用的缘由。

msgr-worker-*

  • 暂未深入分析
  • 启动及用途:异步消息收发线程,主要与ms_dispatch、ms_local线程交互
  • 关联的队列:用于处理各种事件
  • 执行体:NetworkStackadd_thread里面return的lambda函数,由PosixNetworkStackspawn_worker启动
  • 数量:由配置项cct->_conf->ms_async_op_threads决定,默认值3,代码里写死上限值24个,配置项超出这个会被强制改为24,看代码逻辑应该不能在线修改

admin_socket

  • 用途:用来创建ceph-client.admin.2840389.94310395876384.asok,socket文件位置由ceph.conf配置文件中的[client]admin_socket = /var/run/ceph/qemu/$cluster-$type.$id.$pid.$cctid.asok决定。创建完之后作为UNIX domain socket的server端接收客户端请求,并给出响应,客户端可以用ceph –admin-daemon ceph-client.admin.2840389.94310395876384.asok命令发送请求,支持配置修改、perf dump等命令,具体命令列表可以用help子命令查看。
  • 初始化及启动:在CephContext构造函数中初始化,在CephContext::start_service_thread中启动。

ms_dispatch、ms_local

ms_dispatch

  • 用途:暂未深入分析,接收ms_local线程转发的普通dispatch消息,然后转发给Messager注册的普通dispatcher处理(dispatcher有MgrClient、Objecter、RadosClient,他们都继承自Dispatcher类)
  • 关联队列:优先级队列PrioritizedQueue<QueueItem, uint64_t> mqueue
  • 入队:通过DispatchQueue::enqueue入队
  • 出队:线程执行体DispatchQueue::entry

ms_local

  • 用途:初步理解是接收librbd client端请求,转发给ms_dispatch线程处理(普通dispatch,入队mqueue),或者fast dispatch(直接通过Messenger的fast dispatcher发送,messenger目前为AsyncMessenger,dispatcher有MgrClient、Objecter、RadosClient,他们都继承自Dispatcher类)
  • 关联队列:list<pair<Message *, int> > local_messages
  • 入队:通过DispatchQueue::local_delivery入队
  • 出队:线程执行体DispatchQueue::run_local_delivery

启动

safe_timer

  • 用途:管理及触发定时任务事件,librbd中主要用来跟monitor保持心跳(MonClient::schedule_tick),以及ImageWatcher的定时事件。
  • 初始化及启动:qemu中一共启动了3个线程,其中一处是在libradosRadosClientRadosClient构造函数中初始化,在libradosRadosClientconnect中调用SafeTimerinit启动。通过SafeTimer类进行管理和对外提供接口,SafeTimer类包含一个SafeTimerThread类型的成员thread,SafeTimerThread继承Thread类,safe_timer线程通过SafeTimerinit函数使用thread成员进行创建及启动,线程执行的实体函数是SafeTimertimer_thread(SafeTimerThreadentry里面调用),用来轮询检查是否有新的定时任务事件需要触发。另一处是在ImageWatcher对象初始化时启动,第三处未分析,在构造函数处加断点调试即可知晓。
  • 与cephtimer_detailtimer的关系:二者都有定时器功能,但cephtimer_detailtimer更轻量(参考该类的注释),IO卡顿预警功能使用的是cephtimer_detailtimer。

关联的队列:SafeTimer::schedule

  • 入队过程:SafeTimeradd_event_after、SafeTimeradd_event_at
  • 出队过程:SafeTimercancel_event、SafeTimercancel_all_events,以及SafeTimer::timer_thread中正常的事件触发。

service

  • 用途:CephContextServiceThread::entry是线程执行体,有3个工作,1是检查是否需要重新打开log文件,2是检查心跳,3是更新perfcounter中的记录值,但如果是默认配置情况下,这个线程2、3两个任务是不做的。
  • 初始化及启动:过程与admin_socket的启动过程相同,都在CephContext::start_service_thread中完成

log

  • 初始化及启动:在CephContext构造函数中初始化和启动。
  • 用途:负责文件日志打印和内存日志的存储和dump(通过admin socket)。

主要代码流程分析

块设备IO到rados对象映射过程(Striper)

object到osd的crush计算过程

遗留问题

  • 整体IO流程图
  • IO到object到op的拆分过程,以及op执行完毕后如何判断用户层单次IO全部执行完毕
  • object到osd的crush计算过程
  • IO请求发送过程及响应处理过程

perf counter机制

每个image一个perf counter,初始化过程:

使用过程:

cephtimer_detailtimer机制

类似SafeTimer,一个线程专门检查定时任务是否需要触发,可以取消定时任务,取消时如果发现任务已经触发了就忽略,没触发就取消任务。

线程未命名,仍然叫qemu-system-x86,在Objecter对象构造的时候启动:

参考:Ceph动态更新参数机制浅析 http://t.cn/EPQE1tt