锁以及排查锁

锁是一种数据库的信号量管理机制,作用是保证SQL执行上不同事务的隔离性。本文为您介绍Hologres中的锁及如何排查锁。

背景信息

当一个Query发起时,在Hologres中其经过的链路如下图所示。Query链路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 UPDATESELECT FOR SHARE这两个命令需要获取目标表的这个锁,非目标表(比如JOIN关联的其他表)仅获取ACCESS SHARE锁。

        Hologres不支持SELECT FOR UPDATESELECT FOR SHARE命令,因此无需关注。

        ROW EXCLUSIVE

        UPDATEDELETEINSERT 修改数据的命令需要获取此锁。

        需要结合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 TABLEset_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命令包括CREATEDROPALTER等。

操作/锁范围

表锁

表数据锁

行数据锁

表Schema锁

CREATE

已开通

不涉及

DROP

已开通

说明

一旦DROP命令获取锁,无法执行其他命令,其他命令会等待锁,直到发现表被删除了,则其他命令失败。

说明:

不涉及

不涉及

已开通

说明

与其他操作都冲突。

ALTER

已开通

说明

与DROP命令一致。

不涉及

不涉及

已开通

说明

在获取表Schema锁的同时可以对表执行SELECT命令。

SELECT

已开通

说明

在获取表锁的同时可以对表执行INSERTUPDATEDELETE命令,只与DDL锁冲突。

不涉及

不涉及

已开通

说明

执行SELECT命令时除了不能执行DROP命令,其他操作都可以做。

INSERT (包含INSERT ON CONFLICT)

已开通

说明

INSERT命令与 CREATE INDEX、DDL冲突。

  • Hologres V2.0及以前版本的Bulkload方式会获取表数据锁。

  • Hologres V2.1版本起,仅有主键表的Bulkload会获取表数据锁。

说明

Bulkload与Fixed Plan相互冲突。

如果是通过Fixed Plan方式则获取行数据锁。

Hologres V2.1版本起,无主键表的Bulkload也改为仅获取行数据锁。

说明
  • Hologres V2.0及以前版本不支持离线写入和Fixed Plan实时写入同时进行。

  • Hologres V2.1版本起支持无主键表的离线写入和Fixed Plan实时写入同时进行。

已开通

说明

与DDL、DML冲突。

UPDATE

已开通

说明

CREATE INDEX、DDL锁冲突。

已开通

说明

与DDL、DML冲突。

DELETE

已开通

说明

CREATE INDEX、DDL锁冲突。

已开通

说明

与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命令失败时,前面createdrop操作会被回滚。

    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的锁,以及出现锁之后如何解决:

  1. 如果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
  2. 查看锁的持有者。

    使用如下命令查看锁的持有者:

    select * from pg_locks where pid = <pid>;

    pid为步骤1中返回结果中的pid

  3. 排查哪个进程在拿锁。

    通过步骤2判断出当前SQL有锁,根据其oid(即表关系)通过以下命令查看是否有拿锁,t代表true,代表正在拿锁。

    -- 查看拿到了表锁的进程信息
    select pid from pg_locks where relation = <OID> and granted = 't';
  4. 排查拿锁的Query。

    通过步骤3返回结果的PID,使用如下命令查询真正拿锁的Query。

    select * from pg_stat_activity where pid = <PID>;
  5. 释放锁。

    找到拿锁的Query之后,使用如下命令可以直接将Query结束,以释放锁。

    select pg_cancel_backend(<pid>);

BE锁排查

如果在hg_stat_activity视图的be_lock_waitersbe_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)].

    • 可能原因:执行Query的表有被其他Query获取了BE Locks(如报错Lock Mode = SchS|SchE|X,则为Schema稳定锁、存在锁、排他锁),导致Query等待BE Locks超时(5min)从而报错。

    • 解决方法:报错中的transactionTransaction =302xxxx对应Query ID,可以通过Query ID在慢Query日志或者活跃Query中查看对应获取锁的Query。

  • 报错: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需要等锁,直到发现表被删除了从而报错。

    • 解决方法:执行DROPTRUNCATE命令时,不对表执行其他Query。