ceph bluestore工作流程

前一篇《ceph存储引擎bluestore解析》已经对bluestore整体架构和I/O映射逻辑进行了阐述,本文主要从bluestore的工作处理流程进行解析。

1.整体流程

bluestore-work-flow
图中展示了流程中的关键路径及涉及到的线程与队列。下面详细阐述工作流程。

1.1 queue_transactions

queue_transactions是store统一的入口,各个存储引擎如filestore、kvstore、bluestore都得实现这个入口。在queue_transactions里先初始化transaction(初始化状态为STATE_PREPARE),然后在_txc_add_transaction根据操作码的类型进行不同处理,将其放到transaction里,前一篇文章里的I/O映射逻辑就是根据操作码OP_WRITE在BlueStore::_write里进行处理。BlueStore::_write –> BlueStore::_do_write –> (_do_write_small/_do_write_big) –> _do_alloc_write 分配空间,如果采用aio就会调用bdev->aio_write准备aio的结构,放在pending_aios里,如果不是aio就直接pwrite(如果是非direct的还需要sync)。

1.2 STATE_PREPARE

_txc_state_proc里就是状态机的处理逻辑,根据所处的状态进行不同阶段的处理。起始状态是STATE_PREPARE,在这个状态下回检查是否还有未完成的aio,如果有就将状态置为STATE_AIO_WAIT,并调用_txc_aio_submit进行处理,否则就直接进入到下一个状态STATE_AIO_WAIT的处理。

在_txc_aio_submit里就是就是调用bdev->aio_submit –> KernelDevice::aio_submit –> io_submit将aio提交到内核进行处理(注:目前支持KernelDevice和NVMEDevice,这里以KernelDevice为例)。

使用linux Aio时,将I/O提交后,当内核处理完成后会通知到用户态,一种方式是使用eventfd,将其注册到epoll里,当内核处理I/O完成后会触发eventfd的事件从而进行处理;另外一种方式是用一个单独的线程,轮询调用io_getevents去获取完成的事件。在bluestore里采用的是第二种方式,对应的线程是KernelDevice::_aio_thread,当I/O处理完后会调用回调函数aio_cb进行处理(这个回调函数是在bluestore启动时创建KernelDevice设置的),在aio_cb中调用_txc_aio_finish –> _txc_state_proc,从而进入到下一个状态STATE_AIO_WAIT的处理。

1.3 STATE_AIO_WAIT

在这个状态里是调用_txc_finish_io进行处理,会将状态设置成STATE_IO_DONE。因为aio的完成可能是乱序的,有可能后提交的I/O先完成,但是需要保证kv事务的顺序性。bluestore里通过OpSequencer来保证kv事务的顺序性(在_txc_create里会将新的txc放到osr->q里,即q.push_back。在_osr_reap_done里从osr->q里挨个剔除完成的。),在_txc_finish_io里就是实现通过OpSequencer来保证每个事务在处于某个状态时,这个事务之前的事务也必须在这个状态,即使某个事务的I/O先完成,也得等到它之前的事务的I/O也完成后才能进入到下个状态的处理。
做了这个保证后,再对按序对STATE_IO_DONE状态的事务调用_txc_state_proc进入下一个状态的处理。

1.4 STATE_IO_DONE

进入这个状态后,会设置下个状态为STATE_KV_QUEUED,然后会根据bluestore_sync_transaction和bluestore_sync_submit_transaction这两个配置参数的组合作不同的处理:
1)bluestore_sync_transaction为true:表示同步提交kv到rocksdb并持久化,对应调用_txc_finalize_kv后再调用db->submit_transaction,即rocksdb::Write并设置rocksdb::WriteOptions.sync=true;
2)bluestore_sync_transaction为false,bluestore_sync_submit_transaction为true:表示将kv提交到rocksdb,但是不sync,也就是没有落盘,对应调用_txc_finalize_kv后再调用db->submit_transaction_sync,即rocksdb::Write,但rocksdb::WriteOptions.sync=false;
不管采用何种处理方式,最后都会将事务放到kv_queue里,然后通过kv_cond通知_kv_sync_thread。

1.5 _kv_sync_thread处理kv_queue

_kv_sync_thread线程里就是处理kv_queue和wal_cleanup_queue里的事务。按照流程,这一步走到从kv_queue里取事务进行处理的过程。处理的时候是将kv_queue替换给kv_committing,然后后续对kv_committing进行处理。
当bluestore_sync_transaction和bluestore_sync_submit_transaction都为false时(也就是在前一个状态里没做事务相关处理,直接放到kv_queue里了),会遍历kv_committing里的事务,调用_txc_finalize_kv将空间分配释放相关的元数据在kv事务进行处理(如设置key、删除key等),然后db->submit_transaction将kv提交到rocksdb,但是没有落盘。

然后都会调用db->submit_transaction_sync来进行提交并刷盘的动作,这样即使在1.4那一步已经提交sync了,这里也会多做一次,以确保不管前面是没提交、只提交了没sync、提交并sync了的,到了这一步操作后事务都落盘了。

对于每个处理完的事务,都会调用_txc_state_proc进入到下一个状态STATE_KV_QUEUED的处理。

1.6 STATE_KV_QUEUED

设置状态为STATE_KV_DONE,然后调用_txc_finish_kv处理onreadable、oncommit回调,就是将回调放到finisher queue里,触发finisher thread去进行回调的处理(即如果各个副本都处理完成,就会发送对应的响应给客户端)。然后直接进入到STATE_KV_DONE状态的处理。

1.7 STATE_KV_DONE

如果没有wal事务要处理,就直接设置状态为STATE_FINISHING,然后跳出此处处理,在下一次处理到该事务时判断到状态为STATE_FINISHING,从而进行完成的一些处理。

下面主要介绍有wal的情况,之前一篇文章讲过当有覆盖写的时候就会进行wal,设置状态为STATE_WAL_QUEUED,然后根据配置参数bluestore_sync_wal_apply有两种处理方式:
1)当bluestore_sync_wal_apply为false,就把事务放到wal_wq中(会触发WALWQ threads去处理),结束此次处理;
2)当bluestore_sync_wal_apply为true时,直接调用_wal_apply进行处理;
在_wal_apply中会设置状态为STATE_WAL_APPLYING,然后遍历wal事务中的op,调用_do_wal_op进行处理,在_do_wal_op里调用bdev->aio_write准备aio的结构,放在pending_aios里,如果不是aio就直接pwrite(如果是非direct的还需要sync)。
当wal事务中的op都处理完后,就会调用_txc_state_proc进入到下一个状态的处理。

1.8 WALWQ threads

上面说到在bluestore_sync_wal_apply为false时,wal事务直接放到wal_wq中,交由WALWQ的线程池来处理。在这个线程池里也是调用_wal_apply进行处理,具体在上面已经说过了。处理完调用_txc_state_proc进入到下一个状态STATE_WAL_APPLYING的处理。

1.9 STATE_WAL_APPLYING

在这个状态下回检查是否还有未完成的aio,如果有就将状态置为STATE_WAL_AIO_WAIT,然后调用_txc_aio_submit提交I/O到磁盘,并结束此次处理。否则就判断是否满足后续状态,如果是就进行处理,如果不是就结束。

在KernelDevice::_aio_thread,当I/O处理完后会调用回调函数aio_cb进行处理,在aio_cb中调用_txc_aio_finish –> _txc_state_proc,从而进入到下一个状态STATE_WAL_AIO_WAIT的处理。

1.10 STATE_WAL_AIO_WAIT

在这个状态下,调用_wal_finish,设置状态为STATE_WAL_CLEANUP,然后放到wal_cleanup_queue中,通知_kv_sync_thread去处理。

1.11 _kv_sync_thread处理wal_cleanup_queue

处理的时候是将wal_cleanup_queue替换给wal_cleaning,然后后续对wal_cleaning进行处理。在1.5节里已经介绍过kv_sync_thread对kv_queue的处理,其实_kv_sync_thread在每次处理时都是同时处理kv_queue和wal_cleanup_queue里的事务,只不过本文是按照逻辑处理流程才分开描述的。

对于wal_cleanup_queue里的事务,也是调用_txc_finalize_kv将空间分配释放相关的元数据在kv事务进行处理(如设置key、删除key等),然后在接下去的db->submit_transaction_sync中统一提交kv到rocksdb并sync落盘。这次submit_transaction_sync的调用对于kv_queue和wal_cleanup_queue中事务都是一起生效的。

在处理完成后,对于wal_cleanning中的事务都会调用_txc_state_proc进行下一个状态的处理。

1.12 STATE_WAL_CLEANUP

在这个状态里就是设置状态为STATE_FINISHING,然后直接进入STATE_FINISHING状态的处理。

1.13 STATE_FINISHING

调用_txc_finish进行一些结束状态的处理。

2.总结

从上面的流程分析可以知晓,一个I/O在bluestore里经历了多个线程和队列才最终完成,对于非WAL的写,比如对齐写、写到新的blob里等,I/O先写到块设备上,然后元数据提交到rocksdb并sync了,才返回客户端写完成(在STATE_KV_QUEUED状态的处理);对于WAL(即覆盖写),没有先把数据写块设备,而是将数据和元数据作为wal一起提交到rocksdb并sync后,这样就可以返回客户端写成功了,然后在后面的动作就是将wal里的数据再写到块设备的过程,对这个object的读请求要等到把数据写到块设备完成整个wal写I/O的流程后才行,代码里对应的是_do_read里先o->flush()的操作,所以bluestore里的wal就类似filestore里的journal的作用。