锁是一种数据库的信号量管理机制,作用是保证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。