首批通过分布式安全可靠测评,为关键业务系统打造
行锁问题排查介绍
更新时间:2024-08-29 02:31
本文介绍行锁相关的问题以及排查的方法。 以行锁为单位,可以提出与行锁有密切关系的两个对象:行锁的持有者和行锁的等待者。如果能够将这两个信息进行监控,那么对排查行锁的相关问题,将带来很大的帮助。本质来讲,行锁冲突都是由事务引起的。上述两个对象,都是隶属于事务,因此对活跃事务的监控也是必不可少的。
schema 详细介绍
活跃事务(__all_virtual_trans_stat)
OceanBase (root@oceanbase)> desc __all_virtual_trans_stat;
+---------------------------+---------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+---------------------------+---------------+------+-----+---------+-------+
| tenant_id | bigint(20) | NO | PRI | NULL | |
| svr_ip | varchar(32) | NO | PRI | NULL | |
| svr_port | bigint(20) | NO | PRI | NULL | |
| inc_num | bigint(20) | NO | PRI | NULL | |
| session_id | bigint(20) | NO | | NULL | |
| proxy_id | varchar(512) | NO | | NULL | |
| trans_type | bigint(20) | NO | | NULL | |
| trans_id | varchar(512) | NO | | NULL | |
| is_exiting | bigint(20) | NO | | NULL | |
| is_readonly | bigint(20) | NO | | NULL | |
| is_decided | bigint(20) | NO | | NULL | |
| active_memstore_version | varchar(64) | NO | | NULL | |
| partition | varchar(64) | NO | | NULL | |
| participants | varchar(1024) | NO | | NULL | |
| autocommit | bigint(20) | NO | | NULL | |
| trans_consistency | bigint(20) | NO | | NULL | |
| ctx_create_time | timestamp(6) | YES | | NULL | |
| expired_time | timestamp(6) | YES | | NULL | |
| refer | bigint(20) | NO | | NULL | |
| sql_no | bigint(20) | NO | | NULL | |
| state | bigint(20) | NO | | NULL | |
| part_trans_action | bigint(20) | NO | | NULL | |
| lock_for_read_retry_count | bigint(20) | NO | | NULL | |
+---------------------------+---------------+------+-----+---------+-------+
23 rows in set (0.01 sec)
该表中展示了该集群中所有参与者上下文的当前状态,关键参数说明如下:
svr_ip: 表示该上下文创建的 server 地址。session_id: 表示该事务所对应 session 的唯一标识。proxy_id: 表示客户端(proxy/java client)所对应的ip:port。trans_id: 表示事务的唯一标识。is_exiting: 当前的事务上下文是否正在退出。partition: 当前的事务上下文在哪个分区上创建。participants: 当前事务的参与者列表。ctx_create_time事务上下文创建的时间。refer: 表示 context 当前的引用计数。sql_no: 表示当前 context 上最后一次执行 SQL 的sql_no。state: 表示 context 当前的状态:INIT/PREPARE/COMMIT/ABORT/CLEAR,值域为 0/1/2/3/4。part_trans_action: 表示当前 context 最后一次操作的动作:START_TASK/END_TASK/COMMIT。lock_for_read_retry_count: 表示当前 context 在 table scan 过程中,是否遇到过锁冲突重试,值越大,表示冲突越严重。
行锁持有者(__all_virtual_trans_lock_stat)
OceanBase (root@oceanbase)> desc __all_virtual_trans_lock_stat;
+-----------------+--------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-----------------+--------------+------+-----+---------+-------+
| tenant_id | bigint(20) | NO | PRI | NULL | |
| trans_id | varchar(512) | NO | PRI | NULL | |
| svr_ip | varchar(32) | NO | PRI | NULL | |
| svr_port | bigint(20) | NO | PRI | NULL | |
| partition | varchar(64) | NO | PRI | NULL | |
| rowkey | varchar(512) | NO | PRI | NULL | |
| session_id | bigint(20) | NO | | NULL | |
| proxy_id | varchar(512) | NO | | NULL | |
| ctx_create_time | timestamp(6) | YES | | NULL | |
| expired_time | timestamp(6) | YES | | NULL | |
+-----------------+--------------+------+-----+---------+-------+
10 rows in set (0.00 sec)
该表记录以行为单位,记录了当前集群所有活跃事务持有行的相关信息,关键参数说明如下:
rowkey:表示内部表示的行消息。session_id、proxy_id、trans_id:等其他参数都是为了跟活跃事务对应起来,不再一一赘述。
行锁等待者(写锁)
OceanBase (root@oceanbase)> desc __all_virtual_lock_wait_stat;
+-----------------+---------------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-----------------+---------------------+------+-----+---------+-------+
| svr_ip | varchar(32) | NO | PRI | NULL | |
| svr_port | bigint(20) | NO | PRI | NULL | |
| table_id | bigint(20) | NO | PRI | NULL | |
| rowkey | varchar(512) | NO | PRI | NULL | |
| addr | bigint(20) unsigned | NO | PRI | NULL | |
| need_wait | tinyint(4) | NO | | NULL | |
| recv_ts | bigint(20) | NO | | NULL | |
| lock_ts | bigint(20) | NO | | NULL | |
| abs_timeout | bigint(20) | NO | | NULL | |
| try_lock_times | bigint(20) | NO | | NULL | |
| time_after_recv | bigint(20) | NO | | NULL | |
| session_id | bigint(20) | NO | | NULL | |
| block_session_id | bigint(20) | NO | | NULL | |
| type | bigint(20) | NO | | NULL | |
| lock_mode | bigint(20) | NO | | NULL | |
+-----------------+---------------------+------+-----+---------+-------+
12 rows in set (0.00 sec)
该表统计了当前集群中,所有正在等待行锁的请求/语句的相关信息,关键参数说明如下:
session_id、proxy_id、trans_id:等其他参数都是为了跟活跃事务对应起来,不再一一赘述。lock_ts: 表示该请求开始等锁的时间点(us)。abs_timeout: 该语句的绝对超时时间(us)。try_lock_times: 表示该语句曾经尝试过加锁的次数,值越大,表示锁冲突越严重。block_session_id: 表示第一个等在这个行上的事务的 session。
注意
在 OceanBase 数据库 V4.2 及之后版本,该表中新增了 holder_trans_id 列,用于记录实际持锁的事务,用户可通过 holder_trans_id 结合 __all_virtual_trans_stat 表查找到实际的 session_id,并尝试通过 kill session 来杀死事务。需要注意的是 holder_trans_id 与 block_session_id 无实际的一一对应关系,block_session_id 的语义仍为上述语义。
等行锁其实分为两种情况,写写冲突,读写冲突,这里的读写之所以会冲突,是因为该语句的 read_snapshot_version > 该事务的 prepare_version,但 commit version 不确定。换言之,需要等该事务形成 commit version 之后,才能决定是否要读到该事务的修改。 上述虚拟表 __all_virtual_lock_wait_stat,展示写写冲突的情况。对于读写冲突的场景,目前我们做的比较简单,用__all_virtual_trans_stat 的后两个字段(part_trans_action,lock_for_read_retry_count)来表示,如果part_trans_action=START_TASK,lock_for_read_retry_count,可以肯定当前的语句在等行锁,很可能是持有行锁的事务异常,导致一直没有提交。
注意
在 OceanBase 数据库 V4.x 版本上 __all_virtual_trans_stat 中移除了 lock_for_read_retry_count 字段,排查因读写冲突造成的异常 hang 需要在日志中检索是否存在大量错误码为 6004 的日志。
具体请详细参见:OceanBase 数据库 V4.2 版本,关于锁冲突问题的排查手册
示例场景
场景 1: 想找到某个事务一直无法成功的原因
场景: 业务由于使用了一个较大的事务超时时间,且存在一个 session 中的未知长事务占有行锁,阻塞别的事务进行,该如何找到这个事务并将其杀死。
方案 1: 通过加不上锁的事务来找到一直占有锁的事务
第一步: 根据加不上锁的事务的 session id,找到等锁的信息,我们就可以通过 rowkey 这一行知道他等在主键为 oceanbase 的行上。
obclient> select * from __all_virtual_lock_wait_stat where session_id = 3221580756\G
*************************** 1. row ***************************
svr_ip: 11.166.xx.xx
svr_port: 4xxxx
table_id: 1101710651081554
rowkey: table_id=1101710651081554 hash=779dd9b202397d7 rowkey_object=[{"VARCHAR":"oceanbase", collation:"utf8mb4_general_ci"}]
addr: 140433355180784
need_wait: 1
recv_ts: 1600440077959302
lock_ts: 1600440077960167
abs_timeout: 1600450077859302
try_lock_times: 1
time_after_recv: 1307610861
session_id: 3221580756
block_session_id: 3221580756
type: 0
lock_mode: 0
1 row in set (0.01 sec)
第二步: 之后我们通过主键 oceanbase,就可以找到对应的持有 oceanbase 行锁的事务 trans id 和其 session id 。
obclient> select * from __all_virtual_trans_lock_stat where rowkey like '%oceanbase%'\G
*************************** 1. row ***************************
tenant_id: 1002
trans_id: {hash:6605492148156030705, inc:3284929, addr:"11.166.80.69:48270", t:1600440036535233}
svr_ip: 11.166.xx.xx
svr_port: 4xxxx
partition: {tid:1101710651081554, partition_id:0, part_cnt:0}
table_id: 1101710651081554
rowkey: table_id=1101710651081554 hash=779dd9b202397d7 rowkey_object=[{"VARCHAR":"oceanbase", collation:"utf8mb4_general_ci"}]
session_id: 3221577520
proxy_id: NULL
ctx_create_time: 2020-09-18 22:41:03.583285
expired_time: 2020-09-19 01:27:16.534919
1 row in set (0.05 sec)
第三步: 杀掉对应 session 的事务:
obclient> kill 3221577520;
Query OK, 0 rows affected (0.00 sec)
方案 2: 通过执行较长时间的事务来找到
第一步: 根据事务的时间,找到执行时间最长且未结束的事务 trans id。
obclient> select * from __all_virtual_trans_lock_stat order by ctx_create_time limit 5\G
*************************** 1. row ***************************
tenant_id: 1002
trans_id: {hash:6605492148156030705, inc:3284929, addr:"11.166.80.69:48270", t:1600440036535233}
svr_ip: 11.166.xx.xx
svr_port: 4xxxx
partition: {tid:1101710651081554, partition_id:0, part_cnt:0}
table_id: 1101710651081554
rowkey: table_id=1101710651081554 hash=779dd9b202397d7 rowkey_object=[{"VARCHAR":"oceanbase", collation:"utf8mb4_general_ci"}]
session_id: 3221577520
proxy_id: NULL
ctx_create_time: 2020-09-18 22:41:03.583285
expired_time: 2020-09-19 01:27:16.534919
1 row in set (0.05 sec)
第二步:通过事务 trans_id 找到其所持有的所有锁,明确是否是所需要杀掉的事务(这里所获取的锁消息可能不全)。执行如下命令就可以查询所持有的所有锁消息。
obclient> select * from __all_virtual_trans_lock_stat where trans_id like '%hash:6605492148156030705, inc:3284929%'\G
*************************** 1. row ***************************
tenant_id: 1002
trans_id: {hash:6605492148156030705, inc:3284929, addr:"11.166.xx.xx:4xxxx", t:1600440036535233}
svr_ip: 11.166.xx.xx
svr_port: 4xxxx
partition: {tid:1101710651081554, partition_id:0, part_cnt:0}
table_id: 1101710651081554
rowkey: table_id=1101710651081554 hash=779dd9b202397d7 rowkey_object=[{"VARCHAR":"oceanbase", collation:"utf8mb4_general_ci"}]
session_id: 3221577520
proxy_id: NULL
ctx_create_time: 2020-09-18 22:41:03.583285
expired_time: 2020-09-19 01:27:16.534919
*************************** 2. row ***************************
tenant_id: 1002
trans_id: {hash:6605492148156030705, inc:3284929, addr:"11.166.xx.xx:4xxxx", t:1600440036535233}
svr_ip: 11.166.80.69
svr_port: 48270
partition: {tid:1101710651081554, partition_id:0, part_cnt:0}
table_id: 1101710651081554
rowkey: table_id=1101710651081554 hash=89413aecf767cd7 rowkey_object=[{"VARCHAR":"ob", collation:"utf8mb4_general_ci"}]
session_id: 3221577520
proxy_id: NULL
ctx_create_time: 2020-09-18 22:41:03.583285
expired_time: 2020-09-19 01:27:16.534919
2 rows in set (0.05 sec)
第三步: 若明确是对应的事务,杀掉对应 session 的事务。
obclient> kill 3221577520;
Query OK, 0 rows affected (0.00 sec)
场景 2: 提前知道某一行加锁失效的场景
场景:为什么某一行(给定 rowkey 部分字段)总超时,是不是有锁冲突? 假设 rowkey 包含字符串 "zhangfei",排查流程如下。 第一步:根据 rowkey 查询 __all_virtual_trans_lock_stat 找到对应的持锁事务。
OceanBase (root@oceanbase)> select * from __all_virtual_trans_lock_stat where memtable_key like '%zhangfei%'\G;
*************************** 1. row ***************************
tenant_id: 1
trans_id: {hash:6124095709354809361, inc:249476, addr:"10.101.xxx.xx:5xxxx", t:1529075759177984}
svr_ip: 10.101.xxx.xx
svr_port: 5xxxx
partition: {tid:1099511677778, partition_id:0, part_cnt:0}
memtable_key: {table_id:1099511677778, hash_val:7893135555906369137, buf:"table_id=1099511677778 hash=3fb183d083d6d9f1 rowkey_object=[{"VARCHAR":"zhangfei", collation:"utf8mb4_general_ci"}] "}
session_id: 2147549190
proxy_id: NULL
ctx_create_time: 2018-06-15 23:16:35.695821
expired_time: 2018-06-15 23:32:39.177533
1 row in set (0.03 sec)
第二步:根据上述结果,查询活跃事务虚拟表 __all_virtual_trans_stat 找到对应的事务和 session 。
OceanBase (root@oceanbase)> select * from __all_virtual_trans_stat where trans_id like '%6124095709354809361%'\G;
*************************** 1. row ***************************
tenant_id: 1
svr_ip: 10.101.xxx.xx
svr_port: 5xxxx
inc_num: 249476
session_id: 2147549190
proxy_id: NULL
trans_type: 0
trans_id: {hash:6124095709354809361, inc:249476, addr:"10.xxx.xxx.xx:59804", t:1529075759177984}
is_exiting: 0
is_readonly: 0
is_decided: 0
active_memstore_version: 0-0-0
partition: {tid:1099511677778, partition_id:0, part_cnt:0}
participants: [{tid:1099511677778, partition_id:0, part_cnt:0}]
autocommit: 0
trans_consistency: 0
ctx_create_time: 2018-06-15 23:16:35.695821
expired_time: 2018-06-15 23:32:39.177533
refer: 1073741826
sql_no: 1
state: 0
part_trans_action: 2
lock_for_read_retry_count: 1
1 row in set (0.01 sec)
可以看到该事务是单分区事务(trans_type=0),在 2018-06-15 23:16:35.695821 时刻完成事务上下文的创建。该 context 最后一步的动作为 END_TASK,一直没有收到 commit 命令,因此怀疑,客户端尚未 commit,导致事务没有结束,从而引起行锁冲突。 当然,也有另外一种可能,其实客户端异常宕机,导致 OBServer 内部残留了部分连接一直没有断开,从而导致事务悬空,此所谓“悬挂事务”,这里可以通过下面的语句,进一步排查 session 的状态。
OceanBase (root@oceanbase)> select * from __all_virtual_processlist where id=2147549190\G;
*************************** 1. row ***************************
id: 2147549190
user: root
tenant: sys
host: 10.101.xxx.xx:60666
db: oceanbase
command: Sleep
time: 466
state: SLEEP
info: NULL
svr_ip: 10.101.xxx.xx
svr_port: 59xxx
sql_port: 59805
proxy_sessid: NULL
1 row in set (0.01 sec)
由此可知,上述 session 是创建者,即客户端 server ip 为 10.101.xxx.xx,此时可以去该 server 上排查客户端日志,进一步定位。