程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 數據庫知識 >> MYSQL數據庫 >> MySQL綜合教程 >> myrocks復制中斷問題排查,myrocks中斷排查

myrocks復制中斷問題排查,myrocks中斷排查

編輯:MySQL綜合教程

myrocks復制中斷問題排查,myrocks中斷排查


背景

      mysql可以支持多種不同的存儲引擎,innodb由於其高效的讀寫性能,並且支持事務特性,使得它成為mysql存儲引擎的代名詞,使用非常廣泛。隨著SSD逐漸普及,硬件存儲成本越來越高,面向寫優化的rocksdb引擎逐漸流行起來,我們也是看中了rocksdb引擎在寫放大和空間放大的優勢,將其引入到mysql體系。兩種引擎的結構B-Tree(innodb引擎)和LSM-Tree(rocksdb引擎)很好地形成互補,我們可以根據業務類型來選擇合適的存儲。一般mysql默認是mysql+innodb引擎,而mysql+rocksdb引擎稱之為myrocks。今天要討論的就是myrocks復制中斷問題,案例來源於真實的業務場景。

問題現象

 

復制過程中,出現了1756錯誤,根據錯誤日志和debug確認是slave-sql線程在更新slave_worker_info表時出錯導致,堆棧如下:

#7  0x0000000000a30104 in Rpl_info_table::do_flush_info (this=0x2b9999c9de00, force=<optimized out>) at /home/admin/95_20161208115448428_15352432_code/rpm_workspace/sql/rpl_info_table.cc:171
#8  0x0000000000a299b1 in flush_info (force=true, this=<optimized out>) at /home/admin/95_20161208115448428_15352432_code/rpm_workspace/sql/rpl_info_handler.h:92
#9  Slave_worker::flush_info (this=0x2b9999f80000, force=<optimized out>) at /home/admin/95_20161208115448428_15352432_code/rpm_workspace/sql/rpl_rli_pdb.cc:318
#10 0x0000000000a29d95 in Slave_worker::commit_positions (this=this@entry=0x2b9999f80000, ev=ev@entry=0x2b9999c9ab00, ptr_g=ptr_g@entry=0x2b9999daca00, force=<optimized out>)
    at /home/admin/95_20161208115448428_15352432_code/rpm_workspace/sql/rpl_rli_pdb.cc:582
#11 0x00000000009d61f0 in Xid_log_event::do_apply_event_worker (this=0x2b9999c9ab00, w=0x2b9999f80000) at /home/admin/95_20161208115448428_15352432_code/rpm_workspace/sql/log_event.cc:74

這裡簡單介紹下復制相關的位點表,在並行復制模式下,參與復制的主要有三個角色,slave_io線程負責將主庫的binlog拉取到本地;slave_sql線程讀取binlog並根據一定的規則分發給各個slave_worker;slave_worker線程回放主庫的操作,達到與主庫同步的目的。slave_io線程和slave_sql線程分別只有一個,而worker線程可以有1個或多個,可以依據參數slave_parallel_workers設置。如果將slave_parallel_workers設置為0,則表示關閉並行復制,slave_sql線程承擔回放的工作。位點表主要有3張,包括slave_worker_info,slave_relay_log_info和slave_master_info表。slave_io線程更新slave_master_info表,更新拉取的最新位置;slave_sql線程更新slave_relay_log_info表,更新同步位點;而slave_worker線程更新slave_worker_info,每個worker線程在該表中都對應一條記錄,每個worker各自更新自己的位點。

Slave_worker的工作流程如下:

1) 讀取event,調用do_apply_event進行回放;
2) 遇到xid event(commit/rollback event),表示事務結束,調用commit_positions更新位點信息;
3) 調用do_commit提交事務。

從我們抓的堆棧來看,是worker線程在執行執行第2步出錯,現在我們得到了初步的信息,更新位點表失敗直接導致了錯誤。

問題定位與分析

接下來,我們要看看最終是什麼導致了更新位點表失敗?根據最後的錯誤碼,我們調試時設置了若干斷點,最終得到了如下堆棧

#0  myrocks::Rdb_transaction::set_status_error (this=0x2b99b6c5b400, thd=0x2b99b6c51000, s=..., kd=std::shared_ptr (count 18, weak 0) 0x2b997fbd7910,
    tbl_def=0x2b9981bc95b0) at /home/admin/95_20161208115448428_15352432_code/rpm_workspace/storage/rocksdb/ha_rocksdb.cc:1460
#1  0x0000000000a6da28 in myrocks::ha_rocksdb::check_and_lock_unique_pk (this=this@entry=0x2b997fbb3010, key_id=key_id@entry=0, row_info=...,
    found=found@entry=0x2b9a58ca77ef, pk_changed=pk_changed@entry=0x2b9a58ca782f)
    at /home/admin/95_20161208115448428_15352432_code/rpm_workspace/storage/rocksdb/ha_rocksdb.cc:7092
#2  0x0000000000a6e8a8 in myrocks::ha_rocksdb::check_uniqueness_and_lock (this=this@entry=0x2b997fbb3010, row_info=..., pk_changed=pk_changed@entry=0x2b9a58ca782f)
    at /home/admin/95_20161208115448428_15352432_code/rpm_workspace/storage/rocksdb/ha_rocksdb.cc:7250
#3  0x0000000000a7386a in myrocks::ha_rocksdb::update_write_row (this=this@entry=0x2b997fbb3010, old_data=old_data@entry=0x0

通過代碼分析得到了如下信息:

更新位點表之所以失敗是因為更新記錄時發現已經存在了一條記錄,這條記錄的sequnceNumber比當前快照獲取的sequnceNumber大,所以報錯。這裡簡單介紹下sequenceNumber,sequenceNumber是全局遞增的,內部存儲為一個7字節的整數,Rocksdb依賴sequenceNumber實現MVCC。每條記錄有一個唯一的sequenceNumber,rocksdb利用sequenceNumber進行可見性判斷。事務在提交時,獲取當前最大的sequenceNumber,並按照先後順序為事務中的每條記錄分配一個sequenceNumber,然後寫入memtable。同一個key的多個不同的sequenceNumber記錄按照逆序存放,即sequenceNumber最大的key放在最前面,最小的放在最後面。表1中key-n,key表示key值,n表示key對應sequenceNumber,假設key1<key2<key3<key4,則存儲順序如下:

Key1-100

Key1-50

Key2-120

Key2-80

Key3-70

Key4-150

ValueA

ValueB

ValueC

ValueD

ValueE

ValueF

進行讀取時,會利用sequenceNumber建立一個快照,讀取快照發生前的已經存在的記錄,系統之後的變更與本快照無關。假設快照的sequenceNumber是150,執行get(key2)時,會找到(key2-120,ValueC);而如果快照sequenceNumber是100,執行get(key2)時,則會找到(key2-80,ValueD)。

回到問題本身,看看與問題相關的更新流程:

1.嘗試對更新key加鎖,如果沒有並發更新,上鎖成功 //TryLock
2.利用當前最大的sequenceNumberA生成快照
3.檢查快照生成後,key是否被修改 // ValidateSnapshot
  1)再次獲取最大sequenceNumberB,構造key進行查找 // GetLatestSequenceForKey
  2)查找是否存在相同的key的記錄
4.若相同key的記錄存在,且key的sequnceNumber大於sequenceNumberA,則認為有寫沖突,報錯。

我們碰到的錯誤正好就是遇到了寫沖突報錯,那麼現在問題來了,明明這個key已經上鎖了,並且獲取了最新的sequencNumberA,為什麼仍然會讀到相同的key,且對應的seqeunceNumber比sequencNumberA大?結合之前分析的slave_worker_info表,我們可以作出以下猜測

大膽猜測:

小心求證:

現象看起來是這麼的不合理,所有的假設感覺都是不攻自破,但是事實如此,唯有通過更多的信息來輔佐我們進一步判斷。結合代碼,我們對於上面懷疑的幾個點,在相關路徑下進行埋點驗證。埋點主要為了得到以下信息:

獲得日志如下:

Resource busy: seq:38818368818, key_seq:38818368817, index number is 0-0-1-2, pk is 0-0-0-10, thread is 46983376549632

映射關系:
worker id is 10, thread is 46983367104256  //寫memtable線程
worker id is 9, thread is 46983376549632   //出錯線程
worker id is 11, thread is 46983359772416  //事務提交線程

寫memtable線程:
2016-12-19 16:42:06 53743 [ERROR] LibRocksDB:In Memtable, index_num is 0-0-1-2, pk is 0-0-0-10, seq is 38818368818, thread is 46983367104256

2016-12-19 16:42:06 53743 [ERROR] LibRocksDB:In Memtable, index_num is 0-0-1-2, pk is 0-0-0-4, seq is 38818368665, thread is 46983367104256

2016-12-19 16:42:06 53743 [ERROR] LibRocksDB:In Memtable, index_num is 0-0-1-2, pk is 0-0-0-3, seq is 38818368675, thread is 46983367104256

提交線程:
type is commit, write_start, thread is 46983359772416,   seq is 38818368817
type is commit, write_end, thread is 46983359772416,   seq is 38818368818, count is 1

報錯的直接原因就是 已存在相同key的sequenceNumber 38818368818大於快照的squenceNumber 38818368817。這個沖突的key由worker 10寫入memtable,但是由worker 11提交,並不是出錯的線程。總結下來, 我們發現幾個奇怪的現象:

    1. 存在多個線程寫一個key的情況,比如worker10曾經寫過key為10,4,和3的記錄
    2. 對於出錯的sequenceNumber(38818368818)的key,為什麼會被worker11提交
    3. 日志中發現SequenceNumber不連續,存在跳躍的現象

前兩個問題很容易讓我們陷入誤區,存在多個worker寫同一個key的情況,而事實上這兩個問題都是源於group-commit機制,其它線程可能會代替你提交,導致你會看到同一個worker寫不同key的現象。這裡的group-commit包括兩個層次,server層group-commit和rocksdb引擎的group-commit。

  

從圖中可以看出,在server層group-commit機制下,流入到rocksdb引擎層commit接口的都是串行的,既然是串行的,為什麼sequenceNumber會存在跳躍呢?這時候我想到了binlog_order_commits參數,之前為了提高性能,關閉了該參數,也就是在提交的最後一階段,多個事務並發在引擎層提交,會不會與並發寫memtable有關,因為在之前復現的過程中,我們發現關閉並發寫memtable特性(rocksdb_allow_concurrent_memtable_write=0),問題不會復現。

但令人失望的是,在並發寫memtable情況下,即使打開了binlog_ordered_commit(server層串行commit),事務串行在引擎層提交仍然會出現同樣的問題。

       到這裡似乎陷入了死胡同,引擎層永遠只有一個事務進來,為啥開啟並發寫memtable會影響正確性呢?因為一個事務沒法並發。不知什麼時候靈光一現,prepare階段和commit階段並發。因為rocksdb最終提交接口WriteImpl是prepare和commit公用的,通過傳入的參數來區別。prepare階段寫wal日志,commit階段寫memtable和commit日志。那我們就重點來看prepare和commit並發進入WriteImpl時SequenceNumber相關的代碼,果然發現了問題。這裡我簡單介紹下開啟並發寫memtable選項時,事務的提交邏輯。

   1.每個事務都對應一個write-batch
   2.第一個進入WriteImpl函數的線程為leader,其它線程為follower
   3.leader和follower根據先後順序串成一個鏈表
   4.對於並發寫memtable的情況,leader根據每個事務write-batch的count,計算每個事務的start-sequenceNumber。
   5.leader寫完wal日志後,follower根據各自start-sequenceNumber,並發寫memtable
   6.所有事務都寫完memtable後,leader更新全局的sequenceNumber。 

問題主要發生在第4個步驟,計算start-sequenceNumber時,忽略了prepare事務的判斷,導致在prepare事務與commit事務成為一個group時,commit事務的sequence出現跳躍,而全局的sequenceNumber只統計了commit事務,最終導致了寫入memtable的sequenceNumber比全局sequenceNumber大的現象,進而發生了後續的錯誤。下面列舉一個錯誤的例子,假設slave_worker1和slave_worker2分別執行完事務trx1和trx2操作,更新位點後開始事務提交,trx1處於prepare階段,trx2處於commit階段,trx1和trx2組成一個commit-group, trx1是leader,trx2是follower,current_sequence 是101。

trx1: prepare phase, batch count is 3
put(user_key1,v1);
put(user_key2,v2);
put(user_key3,v3);

trx2: commit phase, batch count is 2
put(user_keyA,v1);
put(user_keyB,v2);

trx1是leader,因此trx2的start sequence 是101+3=104,寫入memtable中的user_keyA的sequence是104,user_keyB的sequence是105。Current sequence推進到103。這個group結束後,對於新事務trx3, 如果Current sequnce為已經推進到120(全局任何事務提交都會推進sequence),trx3更新user_keyA,就會發現已經存在(user_keyA, 104),也就是我們遇到的錯誤;而另外一種情況,假設current Sequence沒有推進,仍然為103,則會發生更新丟失,因為查不到(user_keyA,104)這條記錄。這正好解釋了,為啥我們在同步過程中,會發生丟失更新的問題。

while (w != pg->last_writer) {
// Writers that won't write don't get sequence allotment
if (!w->CallbackFailed()) {
sequence += WriteBatchInternal::Count(w->batch); # // BUG HERE: not check w-> ShouldWriteToMemtable, sequence out of bound.
}

w = w->link_newer;
w->sequence = sequence;
w->parallel_group = pg;
SetState(w, STATE_PARALLEL_FOLLOWER);
}

小插曲

      到這裡,我們已經回答了之前的所有疑問,問題也最終定位。但萬萬沒想到,修改代碼提交後,復制問題依舊存在,我感歎是不是不只一個bug。於是繼續查,看了下日志,已經不是之前的slave_worker_info表出錯了,而是一張業務表。後來才發現是因為替換mysqld後隔離級別沒有設置,重啟實例後,隔離級別變為Read-Repeatable級別導致。這裡簡單說下RR隔離下,並行復制下,導致上述錯誤的原因。首先明確一點,RR隔離是在事務的第一個語句獲取快照,以後事務中所有語句都使用這個快照,而RC隔離級別則是事務的每個語句會單獨獲取快照。在並行復制模式下,假設這樣一種情況:

時間軸

Trx1

Trx2

1

Begin

Begin

2

Update t1 set v=? where k=1

 

3

 

Update t2 set v=? where k=1

4

 

commit

5

Update t2 set v=? where k=1

 

RR隔離級別下,trx1會在第一個update語句獲取快照,更新t2表時,仍然使用之前的快照,而在這期間,t2表的k=1對應的記錄可能被修改,導致記錄的sequenceNumber大於trx1快照的sequenceNumber,進而導致更新t2失敗。而如果是RC級別,trx1執行更新t2表時則會重新獲取快照,不會存在問題。

問題解決

      處理sequenceNumber邏輯不正確主要會導致兩個問題,備庫丟失更新和備庫復制中斷。定位到問題原因,並且對所有疑問都有合理的解釋後,修改就比較簡單了,在計算start-sequenceNumber函數LaunchParallelFollowers中,添加prepare事務的判斷即可,隨後還需要編寫測試用例穩定復現,並進行回歸測試才算是最終修復這個補丁。我們將問題反饋給官方https://github.com/facebook/mysql-5.6/issues/481,很快得到了官方的確認和回復。

總結

      整個排查過程還是比較曲折,因為這個bug涉及到並發,並且是特定參數組合的並發才會出問題,所以對於這種復雜的場景,通過合理假設與日志埋點能逐步得到一些結論和依據,最後抽絲剝繭獲取與問題相關的信息,才最終解決問題。我們在測試驗證中過程中不斷發現很多看似與預期不符的日志,也正是這些日志讓我們把整個流程弄透徹,離解決問題越來越近,總之不要放過任何一個疑點,因為要堅信日志不會騙人,而代碼邏輯可能因為你忽略了某些分支,導致會有錯誤的推斷。

 

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved