1. 程式人生 > >ceph儲存 叢集恢復流程詳解

ceph儲存 叢集恢復流程詳解

1. 基本概念

基於版本10.2.1

pg狀態機轉換:

 

1.1 epoch

    epoch是一個單調遞增序列,其序列由monitor負責維護,當叢集中的配置及osd狀態發生變更時,其數值向上加一。這一機制等同於時間軸,每次序列變化是時間軸上的點。

1.2 intervals

    由於epoch反映的是整個叢集的狀態,故從單個pg角度來看,某些epoch序列段變更與本pg是無關的。(例如存在的某一osd掛掉了,但這一osd並不負載本pg的情況),故在epoch的時間軸上,pg的變更實際上是分段的,每個分段稱之為一個intervals。具體如下圖示

 

1.3 pglog

PGLog在程式碼實現中有3個主要的資料結構來維護:pg_info_tpg_log_tpg_log_entry_t。三者的關係示意圖如下。

 

上圖中的eversion列來源於pg_log_entry_t.version欄位,object列來源於pg_log_

entry_t.soid欄位,op列即為pg_log_entry_t.op欄位.

last_complete,last_update欄位即來源於pg_info_t中的相應欄位。

其中:

last_complete:在該指標之前的版本都已經在所有的OSD上完成更新(只表示記憶體更新完成);
last_update:PG內最近一次更新的物件的版本,還沒有在所有OSD上完成更新,在last_update與last_complete之間的操作表示該操作已在部分OSD上完成但是還沒有全部完成;
log_tail:指向pg log最老的那條記錄;
head:最新的pg log記錄;
tail:指向最老的pg log記錄的前一個;
log:存放實際的pglog記錄的list;

1.3.1 主要資料結構分析

l pg_log_t

pg裡儲存的是object,pg自已的一些狀態資訊,儲存在pginfo中,資料結構

struct pg_info_t {

  spg_t pgid;//指出是哪個pg

  eversion_t last_update;   //pg最後一次更新時版本,此資料在寫日誌時更新

  eversion_t last_complete; //此序列之前的在本pg中已確保無丟失,普通情況下,如果last_complete與last_update一樣時,均在寫日誌時更新。

  epoch_t last_epoch_started;  //每到達一次active,就將此值設定為到達active狀態是osdmap表的版本號,此值表明本地的missing集,log均合併完成。

  version_t last_user_version; //使用者自定義的版本,寫日誌時更新成user_version

  eversion_t log_tail;      //最舊的pglog版本。

  hobject_t last_backfill; //指出恢復位置,< objects >= this and < last_complete可以處於缺失。

  bool last_backfill_bitwise;  //一種物件間比對的方法,採用bitwise方式還是nibblewise方式進行比對。

  interval_set<snapid_t> purged_snaps;//於快照相關暫未分析。

  pg_stat_t stats;//狀態資訊,與恢復無關,暫未分析。

  pg_history_t history;//叢集狀態歷史

  pg_hit_set_history_t hit_set;//暫未涉及到,未分析

};

pg_history_t結構體細節如下示:

struct pg_history_t {

  epoch_t epoch_created;    //pg被建立的時間點

   epoch_t last_epoch_started;   //叢集最後一次進入start的時間,不一定和本地相等。

  epoch_t last_epoch_clean;  //叢集中最後一次進入clean狀態的時間

  epoch_t last_epoch_split; // 叢集中最後一次分裂的時間

  epoch_t last_epoch_marked_full;  // 標記為滿,暫未分析

  /**

   * In the event of a map discontinuity, same_*_since may reflect the first

   * map the osd has seen in the new map sequence rather than the actual start

   * of the interval.  This is ok since a discontinuity at epoch e means there

   * must have been a clean interval between e and now and that we cannot be

   * in the active set during the interval containing e.

   */

  epoch_t same_up_since;    //自此時間點開始至今up集無變化

  epoch_t same_interval_since;   //自此時間點開始up與acting集無變化

  epoch_t same_primary_since;  //自此時間點開始primary無變化.

 //清洗相關,暫未分析

  eversion_t last_scrub;

  eversion_t last_deep_scrub;

  utime_t last_scrub_stamp;

  utime_t last_deep_scrub_stamp;

  utime_t last_clean_scrub_stamp;

};

上面的兩個資料結構,比較重要的pg_info_t.last_update,這個資料項在pglog被寫入時由Primary節點產生,由Replicate節點負責寫入。故它代表了整個叢集中的最新狀態。

pg_info_t.last_epoch_started,這個資料項在每次狀態機達到active狀態時,由osd自主寫入,此值寫入時,本地及對端的missing集均已計算完成,故它表示在本osd上,pg_info_t.last_epoch_started之前的物件可能確失,但log,missing集是完全的。

l pg_log_t

pglog在pg層面記錄pg執行過的操作。pglog結構體定義如下

struct pg_log_t {

  eversion_t head; //最新的版本

  eversion_t tail; //最舊的版本

  eversion_t can_rollback_to; //可以rollback的最小版本,執行log_operation時更新,由於這個版本對應的資料已經落盤,故這個版本之後的資料是可以被rollback的。

  eversion_t rollback_info_trimmed_to;//可以移除的版本(可以理解為已固化,小於此的log可以trim掉)

  list<pg_log_entry_t> log;  //儲存log的集合。

};

l pg_log_entry_t

pg_log_entry_t資料結構體定義如下

struct pg_log_entry_t {

  ObjectModDesc mod_desc; //暫未分析

  bufferlist snaps;   // 快照clone時需要,暫未分析

  hobject_t  soid;//pg對應的物件

  osd_reqid_t reqid;  // 唯一標記一個請求

  vector<pair<osd_reqid_t, version_t> > extra_reqids;//目的暫不明確,和客戶端有關

  eversion_t version, prior_version, reverting_to;//本操作版本,本操作的前一版本,回退版本

  version_t user_version; //使用者為此物件安排的版本

  utime_t mtime;  //使用者定義的時間

  int32_t return_code; //針用於失敗時的返回值,此值用於重複請求檢測。

  __s32   op;//操作型別(見類列舉)

  bool invalid_hash; //相容性產物,在編碼時用,目前預設為false

  bool invalid_pool; //相容性產物,在編碼時用,目前預設為false

};

1.3.2 pglog的儲存方式

總體來說,PGLog也是封裝到transaction中,在寫journal的時候一起寫到日誌盤上,最後在寫本地快取的時候遍歷transaction裡的內容,將PGLog相關的東西寫到Leveldb裡,從而完成該OSD上PGLog的更新操作。

1)  PGLog更新到journal

寫I/O序列化到transaction

在ReplicatedPG::do_osd_ops函式里根據型別CEPH_OSD_OP_WRITE就會進行封裝寫I/O到transaction的操作(即將要寫的資料encode到ObjectStore::Transaction::tbl裡,這是個bufferlist,encode時都先將op編碼進去,這樣後續在處理時就可以根據op來操作。注意這裡的encode其實就是序列化操作)。

這個transaction經過的過程如下:ReplicatedPG::OpContext::op_t –> PGBackend::PGTransaction::write(即t->write)–>RPGTransaction::write –> ObjectStore::Transaction::write(encode到ObjectStore::Transacti

on::tbl)後面呼叫ReplicatedBackend::submit_transaction時傳入的PGTransaction _t就是上面這個,通過轉換成RPGTransaction t,然後這個函式裡用到的ObjectStore::Transaction op_t就是對應到RPGTransaction裡的ObjectStore::Transaction t。

PGLog序列化到transaction

在ReplicatedPG::prepare_transaction裡呼叫ReplicatedPG::finish_ctx,然後在finish_ctx函式裡就會呼叫ctx->log.push_back就會構造pg_log_entry_t插入到vector log裡;在ReplicatedBackend::submit_transaction裡呼叫parent->log_operation將PGLog序列化到transaction裡。在PG::append_log裡將PGLog相關資訊序列化到transaction裡。主要序列化到transaction裡的內容包括:pg_info_t,pg_log_entry_t,這兩種資料結構都是以map的形式encode到transaction的bufferlist裡。其中不同的map的value對應的就是pg_info和pg_log_entry的bufferlist。而map的key就是epoch version構成的字串”epoch.version”。另外需要注意的是這些map是附帶上op和oid作為物件的omap(Object的屬性會利用檔案的xattr屬性存取,因為有些檔案系統對xattr的長度有限制,因此超出長度的Metadata會被儲存在DBObjectMap裡。而Object的omap則直接利用DBObjectMap實現。)來序列化到transaction裡;

Transaction裡的內容

從上面的分析得知,寫I/O和PGLog都會序列化到transaction裡的bufferlist裡,這裡就對這個bufferlist裡的主要內容以圖的形式展示出來。transaction的bufflist裡就是按照操作型別op來序列化不同的內容,如OP_WRITE表示寫I/O,而OP_OMAP_SETKEYS就表示設定物件的omap,其中的attrset就是一個kv的map。

 

Trim Log

前面說到PGLog的記錄數是有限制的,正常情況是預設是3000條(由引數osd_min_pg_

log_entries控制),PG降級情況下預設增加到10000條(由引數osd_max_pg_log_entries控制)。當達到限制時,就會trim log進行截斷。

在ReplicatedPG::execute_ctx裡呼叫ReplicatedPG::calc_trim_to來進行計算。計算的時候從log的tail(tail指向最老的記錄)開始,需要trim的條數=log.head-log.tail-max_entries。但是trim的時候需要考慮到min_last_complete_ondisk(這個表示各個副本上last_complete的最小版本,是主osd在收到3副本都完成時再進行計算的,也就是計算last_complete_ondisk和其他副本osd上的last_complete_ondisk–即peer_last_complete_ondisk的最小值得到min_last_complete_ondisk),也就是說trim的時候不能超過min_last_complete_ondisk,因為超過了的也trim掉的話就會導致沒有更新到磁碟上的pg log丟失。所以說可能存在某個時候pglog的記錄數超過max_entries。

 

在ReplicatedPG::log_operation裡的trim_to就是pg_trim_to,trim_rollback_to就是min_last_complete_on_disk。log_operation裡呼叫pg_log.trim(&handler, trim_to, info)進行trim,會將需要trim的key加入到PGLog::trimmed這個set裡。然後在_write_log裡將trimmed裡插入到to_remove裡,最後在呼叫t.omap_rmkeys序列化到transaction的bufferlist裡。

PGLog寫到journal盤PGLog寫到journal盤上就是寫journal一樣的流程,具體如下:在ReplicatedBackend

::submit_transaction呼叫log_operation將PGLog序列化到transaction裡,然後呼叫queue_transaction將這個transaction傳到後續進行處理;呼叫到了FileStore::queue_transactions裡,就將list構造成一個FileStore::Op,對應的list放到FileStore::Op::tls裡;接著在JournalingObjectStore::_op_journal_transactions函式裡遍歷list& tls,將ObjectStore::Transaction encode到一個bufferlist裡(記為tbl);然後FileJournal::submit_entry裡將bufferlist構造成write_item放到writeq;接著在FileJournal::write_thread_entry會從writeq裡取出write_item,放到另外一個bufferlist裡;最後呼叫do_aio_write將bufferlist的內容非同步寫到磁碟上(也就是寫journal);

2) PGLog寫入leveldb在《OSD讀寫流程》裡描述到是在FileStore::_do_op裡進行寫資料到本地快取的操作。將pglog寫入到leveldb裡的操作也是從這裡出發的,會根據不同的op型別來進行不同的操作。比如OP_OMAP_SETKEYS(PGLog寫入leveldb就是根據這個key):

FileStore::_do_op –> FileStore::_do_transactions –> FileStore::_do_transaction根據不同的Transaction型別來進行不同的操作–>case Transaction::OP_OMAP_SETKEYS –> FileS

tore::_omap_setkeys –> object_map->rm_keys,即DBObjectMap::set_keys –> KeyValueDB

::TransactionImpl::set,遍歷map,然後呼叫set(prefix, it->first, it->second),即呼叫到LevelDBStore::LevelDBTransactionImpl::set –>最後呼叫db->submit_transaction提交事務寫到盤上

再比如以OP_OMAP_RMKEYS(trim pglog的時候就是用到了這個key)為例:FileStore

::_do_op –> FileStore::_do_transactions –> FileStore::_do_transaction根據不同的Transaction型別來進行不同的操作–> case Transaction::OP_OMAP_RMKEYS –> FileStore::_omap_rmk

eys –>object_map->rm_keys,後面就是呼叫到LevelDB裡的rm_keys去刪除keys。

PGLog封裝到transaction裡面和journal一起寫到盤上的好處:如果osd異常崩潰時,journal寫完成了,但是資料有可能沒有寫到磁碟上,相應的pg log也沒有寫到leveldb裡,這樣在osd再啟動起來時,就會進行journal replay,這樣從journal裡就能讀出完整的transaction,然後再進行事務的處理,也就是將資料寫到盤上,pglog寫到leveldb裡。

2.恢復流程

2.1 PGLOG如何參與恢復

PGLog參與恢復主要體現在ceph進行peering的時候建立missing列表來標記過時資料,以便於進行對這些資料進行修復。
故障的OSD重新上線後,PG就會標記為peering狀態並暫停處理請求。

· 對於故障OSD所擁有的Primary PG 

o 它作為這部分資料”權責”主體,需要傳送查詢PG元資料請求給所有屬於該PG的Replicate角色節點;

o 該 PG 的 Replicate角色節點實際上在故障OSD下線時期間成為了Primary角色並維護了“權威”的PGLog,該PG在得到故障OSD的Primary PG的查詢請求後會傳送迴應;

o Primary PG 通過對比 Replicate PG 傳送的元資料和PG版本資訊後發現處於落後狀態,因此它會合並得到的PGLog並建立“權威” PGLog,同時會建立missing列表來標記過時資料;

o Primary PG 在完成“權威” PGLog的建立後就可以標誌自己處於Active狀態;

· 對於故障OSD所擁有的Replicate PG 

o 這時上線後故障 OSD 的 Replicate PG會得到Primary PG的查詢請求,傳送自己這份“過時”的元資料和PGLog;

o Primary PG 對比資料後發現該 PG 落後並且過時,比通過PGLog建立了missing列表;

o Primary PG 標記自己處於 Active 狀態;
Peering過程中涉及到PGLog(pg_info和pg_log)的步驟主要包括:

· GetInfo : PG的Primary OSD通過傳送訊息獲取各個Replicate OSD的pg_info資訊。在收到各個Replicate OSD的pg_info後,會呼叫PG::proc_replica_info處理副本OSD的pg_info,在這裡面會呼叫info.history.merge合併Replicate OSD發過來的pg_info資訊,合併的原則就是更新為最新的欄位(比如last_epoch_started和last_epoch_clean都變成最新的);

· GetLog:根據pg_info的比較,選擇一個擁有權威日誌的OSD(auth_log_shard),如果Primary OSD不是擁有權威日誌的OSD,就去該OSD上獲取權威日誌;

o 選取擁有權威日誌的OSD時,遵循3個原則(在find_best_info裡):

§ Prefer newer last_update

§ Prefer longer tail if it brings another info into contiguity

§ Prefer current primary

o 也就是說對比各個OSD的pg_info_t,誰的last_update大,就選誰,如果last_update都一樣,則誰的log_tail小,就選誰,如果log_tail也一樣,就選當前的Primary OSD;

o 如果Primary OSD不是擁有權威日誌的OSD,則需要去擁有權威日誌的osd上去拉取權威日誌,收到權威日誌後,會呼叫proc_master_log將權威日誌合併到本地pg log;

o 在merge權威log到本地pg log的過程中,會將merge的pg_log_entry_t對應的oid和eversion放到missing列表裡,這個missing列表裡的物件就是Primary OSD所缺失的物件,後續在recovery的時候需要從其他osd pull的。

· GetMissing:拉取其它Replicate OSD的pg log(或者部分獲取,或者全部獲取FULL_LOG) ,通過本地的auth log對比,呼叫proc_replica_log處理日誌,會將Replicate OSD裡缺失的物件放到peer_missing列表裡,以用於後續recovery過程的依據;注意:實際上是在PG:activate裡更新peer_missing列表的,在proc_replica_log處理的只是從replica傳過來它本地的missing(就是replica重啟後根據自身的last_update和last_complete構造的missing列表),一般情況下這個missing列表是空

2.2 恢復流程圖