鎖是一種資料庫的訊號量管理機制,作用是保證SQL執行上不同事務的隔離性。本文為您介紹Hologres中的鎖及如何排查鎖。
背景資訊
當一個Query發起時,在Hologres中其經過的鏈路如下圖所示。Frontend解析Query,Query Engine產生執行計畫,然後Storage Engine讀取資料,整個鏈路的鎖有如下兩種。
Frontend Locks(前端鎖)
Frontend為接入層,相容PostgreSQL協議,因此鎖也會相容PostgreSQL的部分鎖,主要用來管理Frontend(FE)中繼資料資訊。
Backend Locks(後端鎖)
Backend是指Query Engine和Fixed Plan,將會享有Hologres內建的鎖,主要用於管理Storage Engine的Schema和資料。
鎖行為變更說明
自Hologres V2.0版本開始,FE預設啟用了無鎖機制。當表的DDL、DQL發生衝突時(例如,在表A發生DDL變更期間有新的查詢請求),新的請求會直接報錯。如果您希望在衝突發生時不報錯,而是等待鎖釋放後再執行新的請求,您可以執行以下命令來設定GUC參數,關閉無鎖機制。
ALTER database <db_name> SET hg_experimental_disable_pg_locks = off;
從Hologres V2.1版本開始,對於無主鍵表的Bulkload操作進行了最佳化,改為僅擷取行鎖,而不再擷取表鎖。
鎖介紹
Frontend Locks(FE Locks)
Hologres接入層Frontend相容PostgreSQL,因此在接入層的鎖與PostgreSQL相容。PostgreSQL提供了Table-level Lock、Row-level Lock和Advisory Lock三種鎖模式來控制並發的資料訪問。 目前Hologres相容的是Table-level Lock和Advisory Lock。
說明Hologres目前不支援顯式的設定鎖命令以及Advisory Lock相關的UDF。
Table-level Lock
分類
Table-level Lock是指表鎖,表鎖包含如下種類。
鎖名稱
說明
備忘
ACCESS SHARE
一般情況下只有
SELECT
命令會擷取相關表的這個鎖。不涉及。
ROW SHARE
只有
SELECT FOR UPDATE
和SELECT FOR SHARE
這兩個命令需要擷取目標表的這個鎖,非目標表(比如JOIN關聯的其他表)僅擷取ACCESS SHARE鎖。Hologres不支援
SELECT FOR UPDATE
和SELECT FOR SHARE
命令,因此無需關注。ROW EXCLUSIVE
UPDATE
、DELETE
和INSERT
修改資料的命令需要擷取此鎖。需要結合Backend Locks一起關注。
SHARE UPDATE EXCLUSIVE
為了防止Vacuum和並發的Schema變更發生衝突的鎖,如下命令需要擷取此鎖。
lazy VACUUM
即非Vacuum Full
。ANALYZE
。CREATE INDEX CONCURRENTLY
。說明Hologres使用此命令時不會擷取 SHARE UPDATE EXCLUSIVE鎖,而是與非 CONCURRENTLY形式的
CREATE INDEX
一樣擷取SHARE
鎖。CREATE STATISTICS
,此命令Hologres不支援。COMMENT ON
。ALTER TABLE VALIDATE CONSTRAINT
,此命令Hologres不支援。ALTER TABLE SET/RESET (storage_parameter)
,Hologres僅支援使用此命令設定自己擴充的屬性以及PostgreSQL原生的 autovacuum_enabled屬性,不支援設定其他屬性;且設定上述屬性時不會擷取表的任何鎖。修改PostgreSQL內建的其他某些storage parameter參數需要擷取此鎖,詳情請參見ALTER TABLE。ALTER TABLE ALTER COLUMN SET/RESET options
。ALTER TABLE SET STATISTICS
,此命令Hologres不支援。ALTER TABLE CLUSTER ON
,此命令Hologres不支援。ALTER TABLE SET WITHOUT CLUSTER
,此命令Hologres不支援。
重點關注
ANALYZE
命令。SHARE
只有非concurrently的
CREATE INDEX
需要擷取此鎖。說明Hologres中建立JSON索引時會需要拿此鎖。
關注
CREATE INDEX
命令(建立JSON相關索引)。SHARE ROW EXCLUSIVE
為了防止並發的資料修改的鎖,需要擷取此鎖的命令如下。
CREATE COLLATION
,此命令Hologres不支援。CREATE TRIGGER:
,此命令Hologres不支援。部分
ALTER TABLE
命令。DISABLE/ENABLE [ REPLICA | ALWAYS ] TRIGGER
,此命令Hologres不支援。ADD table_constraint
,此命令Hologres不支援。
Hologres不支援擷取此鎖的命令,所以無需關注。
EXCLUSIVE
僅有
REFRESH MATERIALIZED VIEW CONCURRENTLY
命令需要擷取此鎖。Hologres不支援
REFRESH MATERIALIZED VIEW CONCURRENTLY
命令,無需關注。ACCESS EXCLUSIVE
完全獨佔訪問需要的鎖,與其他所有的鎖都衝突,需要擷取此鎖的命令如下。
DROP TABLE
TRUNCATE TABLE
REINDEX
,此命令Hologres不支援。CLUSTER
,此命令Hologres不支援。VACUUM FULL
REFRESH MATERIALIZED VIEW (without CONCURRENTLY)
,此命令Hologres不支援。LOCK
,顯式的LOCK命令,如果不指明具體的鎖類型,預設需要這個鎖;此命令Hologres不支援。ALTER TABLE
,除去上面明確提到擷取特定鎖的ALTER TABLE
形式以外,其他的ALTER TABLE
命令形式都預設擷取這個鎖。
需要重點關注的鎖,在Hologres中執行的DDL操作都會擷取該鎖,與其他鎖衝突。
逾時時間
FE Locks沒有預設逾時時間,需要業務設定逾時時間,防止鎖等待時間過長,詳情請參見Query管理。
衝突關係
鎖之間的衝突關係如下表所示,衝突意味著一個操作擷取鎖,其他動作擷取的鎖都需要等待。
說明表示不衝突,表示衝突。
Requested Lock Mode
ACCESS SHARE
ROW SHARE
ROW EXCLUSIVE
SHARE UPDATE EXCLUSIVE
SHARE
SHARE ROW EXCLUSIVE
EXCLUSIVE
ACCESS EXCLUSIVE
ACCESS SHARE
ROW SHARE
ROW EXCLUSIVE
SHARE UPDATE EXCLUSIVE
SHARE
SHARE ROW EXCLUSIVE
EXCLUSIVE
ACCESS EXCLUSIVE
Advisory Lock
Advisory Lock指諮詢鎖,PostgreSQL提供了一種方法建立由應用定義其含義的鎖。Hologres大部分業務情況無需額外關注。
Backend Locks(BE Locks)
分類
在Hologres中BE Locks鎖分類如下。
鎖分類
鎖介紹
Exclusive(X)
獨佔鎖定(也稱互斥鎖),當事務需要修改一批或一條資料時申請獨佔鎖定,例如DML語句(DELETE、INSERT、UPDATE)。獨佔鎖定申請成功的前提是同一資源上沒有其他的共用鎖定或獨佔鎖定,當獨佔鎖定申請成功後,鎖資源上將不能同時有其他鎖。
Shared(S)
共用鎖定,當事務需要讀取一批或一條資料時申請共用鎖定,以免其他事務對將要讀取的資料做修改。同一個資源上允許存在多個共用鎖定,即允許DQL之間並發執行,因為DQL不會改變資源本身。
Intent(I)
意圖鎖定,這種類型的鎖通常用於表達鎖的階層,同一個資源上允許存在多個意圖鎖定,當申請成功後,該資源上就不能有獨佔鎖定。例如,當事務申請一行的獨佔鎖定時,它會同時申請表的意圖鎖定(表是比行更高層次的資源),以防止其他事務申請表的獨佔鎖定。
逾時時間
BE Locks逾時時間預設為5min,超過該時間則報錯。
衝突關係
BE Locks的衝突關係如下,衝突表示一個操作擷取鎖,其餘操作都需要等待鎖。
說明表示不衝突,表示衝突。
操作
DROP
ALTER
SELECT
UPDATE
DETELE
INSERT(包含INSERT ON CONFLICT)
DROP
ALTER
SELECT
UPDATE
DETELE
INSERT(包含INSERT ON CONFLICT)
鎖的作用範圍
根據鎖的分類,不同的鎖作用範圍不同。
FE Locks
FE Locks只會作用在表對象上,與表資料無關,且鎖只有成功或者卡住兩種狀態,卡住就代表有鎖衝突在等待鎖。
BE Locks
BE Locks會作用在資料或者表結構上,因此在作用範圍上會分為表資料鎖、行資料鎖、表結構(Schema)鎖。
表資料鎖:代表整個表的資料都需要擷取鎖,如果多個任務同時都需要擷取表資料鎖,會造成任務等待鎖現象,從而任務延遲。
行資料鎖:指整行資料擷取鎖,在執行效率上會更高,其中走Fixed Plan加速SQL執行的Query都是行資料鎖或者表Schema鎖。
表Schema鎖:即表結構鎖,當事務需要讀取或修改表結構時申請Schema鎖,大部分的事務都會申請Schema鎖。目前主要有如下類型的Schema鎖。
SchX:Schema獨佔鎖定,用於DDL語句,目前只支援
DROP TABLE
命令。SchU:Schema更新鎖定,用於修改表結構的DDL語句包含
ALTER TABLE
和set_table_property
命令。SchE:Schema存在鎖,使用者DML和DQL語句,確保讀寫資料期間表不會被刪除。
說明SchU是DDL鎖更細粒度的管控,允許ALTER TABLE期間DQL正常運行,無需等待;SchX是最粗粒度的DDL獨佔鎖定,所有的DDL、DML、DQL都會等待。
如果Start Query耗時較長,則可能在等待BE Locks。
在Hologres中,其中常見命令作用的鎖範圍如下。表示操作命令會擷取該鎖。
非Fixed Plan的寫入、更新、刪除都為Bulkload。
CREATE INDEX
命令目前是指建立JSON相關索引。DDL命令包括
CREATE
、DROP
、ALTER
等。
操作/鎖範圍 | 表鎖 | 表資料鎖 | 行資料鎖 | 表Schema鎖 |
CREATE | 不涉及 | |||
DROP | 說明 一旦DROP命令擷取鎖,無法執行其他命令,其他命令會等待鎖,直到發現表被刪除了,則其他命令失敗。 說明: | 不涉及 | 不涉及 | 說明 與其他動作都衝突。 |
ALTER | 說明 與DROP命令一致。 | 不涉及 | 不涉及 | 說明 在擷取表Schema鎖的同時可以對錶執行 |
SELECT | 說明 在擷取表鎖的同時可以對錶執行 | 不涉及 | 不涉及 | 說明 執行 |
INSERT (包含INSERT ON CONFLICT) | 說明
|
說明 Bulkload與Fixed Plan相互衝突。 | 如果是通過Fixed Plan方式則擷取行資料鎖。 Hologres V2.1版本起,無主鍵表的Bulkload也改為僅擷取行資料鎖。 說明
| 說明 與DDL、DML衝突。 |
UPDATE | 說明 與 | 說明 與DDL、DML衝突。 | ||
DELETE | 說明 與 | 說明 與DDL、DML衝突。 |
事務相關的鎖說明
Hologres目前僅支援DDL的明確交易,並不支援單純的DML事務,也不支援DDL和DML混合的事務。
Hologres不支援嵌套子事務。
單純的DML事務雖然文法上可以通過,但是實際不支援原子提交和復原。
如下DML即使
insert
成功,如果之後的update
失敗了,insert
的資料也不會被復原。begin; insert into t1(id, salary) values (1, 0); update t1 set salary = 0.1 where id = 1; commit;
純DDL事務可以按預期的方式工作
其中任何一行DDL命令失敗則會整個事務被復原。例如下面
alter
命令失敗時,前面create
和drop
操作會被復原。begin; create table t1(i int); drop table if exists t2; alter table t1 add column n text; commit;
DDL和DML命令混合的事務會被禁止
如下樣本,當事務中包含DDL和DML命令時,DML命令就會報錯。
begin; create table t1(i int); update t1 set i = 1 where i = 1; -- DML命令報錯 ERROR: UPDATE in ddl transaction is not supported now.
明確交易中任何一個命令獲得的鎖都只會在整個事務結束(提交或復原)時才統一釋放。
如下樣本,當對父表做
alter
操作時,會同時擷取父表(login_history)和子表(login_history_202001)的ACCESS EXCLUSIVE鎖, 但是這個命令執行完不會立即釋放對應的鎖,而是要等待最後的commit
執行完(不管成功還是失敗)才會釋放鎖。若是一直不執行commit
,則會一直保持鎖,這時若有其他對這個表的DDL操作,則會鎖表並報錯。-- suppose we have three tables create table temp1(i int, t text); create table login_history(ds text, user_id bigint, ts timestamptz) partition by list (ds); create table login_history_202001 partition of login_history for values in ('202001'); begin; alter table login_history_s1 add column user_id bigint; drop table temp1; create table tx2(i int); commit;
FE鎖排查
可以通過如下步驟排查是否有FE的鎖,以及出現鎖之後如何解決:
如果Query耗時較長,可以通過
wait_event_type
欄位查詢當前Query是否有鎖。如下命令樣本,如果結果中
wait_event_type
欄位值為lock
,說明該Query有FE的鎖。--2.0及其以上版本 select query,state,query_id,transaction_id,pid,wait_event_type,wait_event,running_info,extend_info FROM hg_stat_activity where query_id = 200640xxxx; --返回結果 ----------------+---------------------------------------------------------------- query | drop table test_order_table1; state | active query_id | 200640xxxx pid | 123xx transaction_id | 200640xxxx wait_event_type | Lock wait_event | relation running_info | {"current_stage":{"stage_duration_ms":47383,"stage_name":"PARSING"},"fe_id":1,"warehouse_id":0}+ | extend_info | {} + --1.3及以下版本 select datname, pid, application_name, wait_event_type, state,query_start, query from pg_stat_activity where backend_type in ('client backend'); --返回結果 ----------------+---------------------------------------------------------------- datname | holo_poc pid | 321xxx application_name | PostgreSQL JDBC Driver wait_event_type | lock state | active query_start |2023-04-20 14:31:46.989+08 query | delete from xxx
查看鎖的持有人。
使用如下命令查看鎖的持有人:
select * from pg_locks where pid = <pid>;
pid為步驟1中返回結果中的
pid
。排查哪個進程在拿鎖。
通過步驟2判斷出當前SQL有鎖,根據其
oid
(即表關係)通過以下命令查看是否有拿鎖,t
代表true
,代表正在拿鎖。-- 查看拿到了表鎖的進程資訊 select pid from pg_locks where relation = <OID> and granted = 't';
排查拿鎖的Query。
通過步驟3返回結果的PID,使用如下命令查詢真正拿鎖的Query。
select * from pg_stat_activity where pid = <PID>;
釋放鎖。
找到拿鎖的Query之後,使用如下命令可以直接將Query結束,以釋放鎖。
select pg_cancel_backend(<pid>);
BE鎖排查
如果在hg_stat_activity視圖的be_lock_waiters
和be_lock_converting_waiters
欄位有資料,說明Query在後端有鎖或者被鎖,可以通過如下步驟排查。
情況1:當前Query產生鎖,其餘Query在等該鎖釋放。
通過以下SQL可以看到當前SQL是否有BE鎖,以及哪些Query被該SQL鎖住,等待其釋放鎖。
select query_id, transaction_id, ((extend_info::json)->'be_lock_waiters'->>0)::text as be_lock_waiters FROM hg_stat_activity as h where h.state = 'active' and ((extend_info::json)->'be_lock_waiters')::text != ''; --返回結果 ----------------+------------------ query_id | 10005xxx transaction_id | 10005xxx be_lock_waiters | 13235xxx
情況2:查看當前Query被哪個Query鎖住,等待它釋放鎖。
通過如下SQL,查詢當前Query被誰拿著鎖,等待這個Query釋放鎖。
select query_id, transaction_id, ((extend_info::json)->'be_lock_waiters')::text as be_lock_waiters FROM hg_stat_activity as h where h.state = 'active' and ((extend_info::jsonb)->'be_lock_waiters')::jsonb ? '10005xxx'; -[ RECORD 1 ]---+------------------------------------------ query_id | 200740017664xxxx transaction_id | 200740017664xxxx be_lock_waiters | ["200640051468xxxx","200540035746xxxx"]
常見問題
報錯:
internal error: Cannot acquire lock in time, current owners: [(Transaction =302xxxx, Lock Mode = SchS|SchE|X)].
。報錯:
ERROR: Operation timed out: Wait schema version timeout.: Server current target schema version:xxx is late from request schema version: yyy
。可能原因:執行DDL後,會先在Frontend(FE)節點執行,再非同步在Storage Engine(SE)執行,當FE執行DDL結束後會對節點版本(Version)進行更新,若SE的DDL還沒執行完成,導致SE的版本比FE的版本低,此時Query會等待SE執行DDL,如果超過5min後SE仍然沒有執行完成,就會報錯等待Schema版本逾時。
解決方案:
先終止(Kill)掉等待鎖的DDL,然後重試Query。
重啟執行個體(極端手段)。
報錯:
The requested table name: xxx (id: 10, version: 26) mismatches the version of the table (id: 10, version: 28) from server
。可能原因:執行DDL後,會先在Frontend(FE)單個節點執行,再非同步在Storage Engine(SE)執行。SE已經完成DDL並更新版本,但是因為FE節點較多,節點間Replay未完成,導致部分FE節點的版本低於SE,而Query正好在版本較低的FE中執行,從而出現報錯。
解決方案:
多重試幾次Query。
若是超過幾分鐘還報錯建議重啟執行個體。
報錯:
internal error: Cannot find index full ID: 86xxx (table id: 20yy, index id: 1) in storages or it is deleting!
。可能原因:執行
DROP Table
或者Truncate Table
時,對這個表進行DML(SELECT、DELETE等)需要擷取DDL鎖,導致Query需要等鎖,直到發現表被刪除了從而報錯。解決方案:執行
DROP
和TRUNCATE
命令時,不對錶執行其他Query。