出現分散式死鎖現象後,如果沒有外部干預,通常是一方等待鎖超時報錯後,事務回滾清理持有鎖資源,另一方可繼續執行。 ...
本文分享自華為雲社區《GaussDB(DWS)中的分散式死鎖問題實踐》,作者: 他強由他強 。
1、什麼是分散式死鎖
分散式死鎖是相對於單機死鎖而言,一個事務塊中的語句,可能會分散在集群里多個節點(CN/DN)執行,在不同節點上可能都會持有鎖,當併發事務進行時可能會導致分散式(全局)死鎖,如下圖所示,會話SESSION1持有了DN1上的lock1資源後再去請求DN2上的lock2,會話SESSION2持有了DN2上的lock2資源後再去請求DN1上的lock1,兩個會話形成互相等待。出現分散式死鎖現象後,如果沒有外部干預,通常是一方等待鎖超時報錯後,事務回滾清理持有鎖資源,另一方可繼續執行。
2、常見的分散式死鎖場景
一般來說,分散式死鎖的產生與在不同節點上的併發時序或持鎖順序有關,所以現網實際發生概率較低,分散式死鎖通常都是RegularLock類型,下麵是幾種常見的分散式死鎖場景,舉例說明兩個併發事務產生的分散式死鎖:
1)鎖升級
# 集群兩個CN,兩個DN create table mytable(a int, b int); insert into mytable values(1,1),(2,2);
其中sessionA與sessionB由不同CN發起(sessionA:CN1,session2:CN2),執行時序如下:
session A(CN1) |
session B(CN2) |
begin; |
begin; |
select * from mytable; // CN1上拿1級表鎖 |
|
select * from mytable; // CN2上拿1級表鎖 |
|
truncate table mytable; // CN1上拿8級表鎖 // CN2上拿8級表鎖,waiting |
|
truncate table mytable; // CN1上拿8級表鎖,waiting |
可以看到sessionA里select會持有本地1級鎖,truncate會持有8級鎖,出現鎖升級現象,導致sessionA在CN2上等鎖,sessionB在CN1上等鎖,形成相互等待。
2)行更新衝突
# 集群兩個CN,兩個DN create table mytable(a int, b int); insert into mytable values(1,1),(2,2);
行存表發生行更新衝突是比較常見的分散式死鎖場景。因為表是round robin分佈,所以行a = 1 與 a = 2數據可以保證分別分佈在DN1和DN2節點。
一個事務在更新數據時需要在對應DN節點持有本xid事務鎖的Exclusive鎖,當發生行更新衝突時(寫寫衝突),一個事務需要阻塞等待另一個事務提交(等待獲取對方事務鎖ShareLock),形成相互等待時造成分散式死鎖。
其中sessionA與sessionB可由相同或者不同CN發起,執行時序如下:
session A(xid1) |
session B(xid2) |
begin; |
begin; |
update mytable set b = 1 where a = 1; // DN1上拿xid1的事務鎖 |
|
update mytable set b = 2 where a = 2; // DN2上拿xid2的事務鎖 |
|
update mytable set b = 1 where a = 2; // DN2上拿xid2的事務鎖,waiting |
|
update mytable set b = 2 where a = 1; // DN1上拿xid1的事務鎖,waiting |
3)CU更新衝突
# 集群兩個CN,兩個DN create table mytable(a int, b int) with (orientation = column); insert into mytable values(1,1),(2,2),(3,3),(4,4);
其中sessionA與sessionB可由相同或者不同CN發起,執行時序如下:
session A(xid1) |
session B(xid2) |
begin; |
begin; |
update mytable set b = 1 where a = 1; // DN1上拿xid1的事務鎖 |
|
update mytable set b = 2 where a = 2; // DN2上拿xid2的事務鎖 |
|
update mytable set b = 1 where a = 3; // DN2上拿xid2的事務鎖,waiting |
|
update mytable set b = 2 where a = 4; // DN1上拿xid1的事務鎖,waiting |
當出現更新衝突時,對於行存表來說是對一行數據加鎖(如場景2所述),但對於列存表來說是對一個CU加鎖。所以一個事務里的更新語句如果涉及到不同的CU,也會拿事務鎖,可能就會產生分散式死鎖。
我們可以通過如下語句觀察ctid信息判斷數據是否分佈在同一個CU上,如下圖:可以看到a = 1 與 a = 3分佈在DN1上,且在同一個CU;a = 2 與 a = 4分佈在DN2上,且在同一個CU。所以這也能解釋為什麼看上去列存表更新不同的“行數據”也會產生鎖阻塞和分散式死鎖現象。
4)單語句出現分散式死鎖
前面幾種場景都是事務塊里涉及到多條SQL語句,可能會到不同節點上去交錯拿鎖導致的分散式死鎖,但有時候某些單語句場景可能也會出現分散式死鎖,如下:
session A(xid1) |
session B(xid2) |
update/delete mytable set b = 1 where a = 1; // waiting |
update/delete mytable set b = 2 where a = 2; // waiting |
此類問題與數據分佈有關,如下場景都可能會導致這個現象:
1)若表是複製表,每個DN節點上都有數據,更新時會去所有DN併發執行
2)表是普通行存表或列存表,但有行數據(如a=1)同時分佈在了多個DN節點上,如round robin分佈下插入兩條相同a=1的數據
insert into mytable values(1,1); insert into mytable values(1,2);
此場景需要具體去排查數據分佈是否會造成此情況。
3、規避分散式死鎖的方法1)控制鎖級別,減少鎖升級
按照各類操作的鎖級別建議規則使用,開發時不要盲目提高鎖級別,造成可能發生的不必要的鎖等待
2)控制鎖粒度
合理控制鎖使用範圍,及時釋放
3)控制拿鎖順序
儘量控制對資源操作的順序,比如對各分區表的操作順序,避免亂序造成的死鎖。但全局各節點的拿鎖情況或順序一般無法提前預測,往往為了考慮提高性能,請求會在節點間併發執行,但我們可以在某個節點上控制併發互斥以規避分散式死鎖問題,如操作某個表時先去FirstCN上請求持鎖,持鎖成功後再對其他CN和DN並行拿鎖。GaussDB(DWS)內核的很多地方的設計中會有這種思想,如DDL語句,autoanalyze等。
4)主動設置較短的鎖超時時間
一般用在非關鍵的用戶路徑操作上,如用戶語句在runtime analyze子事務的流程里會主動設置鎖超時時間為2秒,發生阻塞後可及時放鎖,避免出現長時間鎖等待,也能規避潛在的分散式死鎖場景
4、如何排查系統是否產生了分散式死鎖
本質上是發現集群中是否有全局的死鎖環等待關係,內核中提供有許多視圖可以輔助觀察持鎖等待情況,但需要註意的是,因為查詢到的鎖等待關係可能只是暫時的瞬間狀態,只有持續存在的鎖等待才會造成分散式死鎖,需要判斷鎖是否稍後會主動釋放(事務提交前),還是只能等到事務提交後釋放。如何判斷系統是否產生了分散式死鎖,有以下方法:
1)查詢pgxc_deadlock視圖,會輸出全局死鎖環信息,如果信息為空,則代表無分散式死鎖,但需要註意在某些複雜的場景可能會出現誤報,即輸出有死鎖環信息,但可能並沒有形成分散式死鎖;
當有分散式死鎖時,直到等待鎖超時後,某一方事務會出現“Lock wait timeout...”,列印具體的鎖信息及鎖語句,報錯後釋放鎖,另一方解除阻塞。相關的鎖超時參數是lockwait_timeout或update_lockwait_timeout。
2)在GaussDB(DWS)的8.3.0版本及以後,內核已經支持了自動化地分散式死鎖檢測,當檢測到系統中存在分散式死鎖等待關係後,會自動報錯和挑選事務進行cancel,具體原理下一節中會詳細介紹。
如下圖所示,若用戶出現“cancelled by global deadlock detector”報錯,代表檢測到分散式死鎖並被查殺,此時可以去檢測CN上(FirstCN或者CCN)上去找相關日誌信息,會輸出具體死鎖和session查殺信息,需要註意用戶語句執行CN和檢測CN可能並不是一個,此時檢測CN會向執行CN發起事務cancel。
5、分散式死鎖檢測原理
分散式死鎖檢測的目標原則是做到不誤報,爭取不漏報,儘量及時報。
我們使用了中心化的收集檢測思想,如流程圖所示:首先挑選一個CN作為檢測CN(類似master角色),CN上新增後臺線程啟動GlobalDeadlockDetector模塊,周期性向集群所有節點收集鎖等待關係,計算等待者和持有者信息,然後構造全局有向圖(WFG),依賴定義的規則對圖的頂點和邊進行消除,判斷是否能消除完成。如果無法消除完成,則出現了死鎖環,併進行二次DoubleCheck,如果兩次的死鎖環信息有交集,則報告死鎖信息。當發現死鎖後,按照事務時間戳挑選最年輕的事務(youngest)進行中斷,並會對用戶報錯。
我們在設計上主要參考了Greenplum的思路,由於與GaussDB(DWS)架構和應用場景上的差異性,也針對做了一些改造和優化,主要包括:
1、檢測節點的選擇:在FirstCN或CCN上啟動後臺檢測線程,依賴外部OM模塊做高可用切換;
2、等待關係圖節點的標識:由檢測CN構造全局唯一global_session下發,格式為:timestamp.pid.node_name(timestamp為事務開始的時間戳,pid為執行CN上的線程號,node_name為執行CN名稱);
3、虛實邊關係定義:支持定義線程級別虛實邊,過濾掉不必要的死鎖誤報;- 實邊:鎖等待關係的變化,需要等到持有者事務會話commit或abort
- 虛邊:鎖等待關係的變化,不需要等到持有者事務會話commit或abort
4、死鎖結果的合法性檢查:增加DoubleCheck機制,提高檢測結果準確性,結果以連續兩次檢測到的死鎖環交集為準;
5、死鎖消除:執行CN與檢測CN可能不同,可能存在跨CN發起的事務中斷;
6、與單機死鎖檢測演算法互補:當分散式死鎖檢測演算法如果發現檢測到單機的死鎖環路等待關係後,則忽略,與單機死鎖檢測演算法處理不衝突;
分散式死鎖檢測相關參數:
-
enable_global_deadlock_detector:分散式死鎖檢測功能是否開啟,預設off
-
global_deadlock_detector_period:分散式死鎖檢測周期,預設5秒