1.整體流程
圖中展示了流程中的關鍵路徑及涉及到的線程與隊列。下面詳細闡述工作流程。
重點關注:狀態切換;kv存儲數據的GC時機
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的作用。