跳至主要內容

Klustron 的全局死锁检测

Klustron大约 8 分钟

Klustron 的全局死锁检测

注意

如无特别说明,文中的版本号可以使用任何已发布版本的版本号代替。所有已发布版本详见:http://doc.klustron.com/zh/Release_notes.html

本文目标:

事务死锁是基于事务锁的数据库系统中的常见现象,在MySQL、Oracle等数据库中都有相应的死锁处理机制。在Klustron分布式数据库系统中,可能发生全局死锁的情况,单独的存储节点的死锁处理机制无法解除全局死锁,需要分布式数据库集群层面的全局死锁处理机制才能检测和解除。本文首先介绍死锁和分布式系统的全局死锁,以及在Klustron中如何解决全局死锁问题,最后有相应的示例。

01 什么是死锁

死锁是指两个或多个事务相互等待对方释放所持有的资源,导致所有事务都

无法继续执行。这种情况下,每个事务都需要等待一个或多个资源,而这些资源却又被其他事务所持有,没有人能够释放掉这些资源,因此整个系统陷入了僵局。

02 死锁对数据库的影响

由于死锁会导致所有事务都无法继续执行,因此它非常严重,并且可以造成堆积等问题。最明显的影响是降低数据库处理事务的并发性。如果是高并发服务器,死锁很可能会导致数据库无法响应请求,影响整个系统的稳定性和可用性。

03 死锁的例子

下面是两个session产生死锁环路等待的例子。

Session 1Session 2
begin; update bank_accounts set balance=balance-10 where id=100;
begin; update bank_accounts set balance=balance-100 where id=200;
update bank_accounts set balance=balance+10 where id=200;
update bank_accounts set balance=balance+100 where id=100;

Session 1和2的两个事务各自获得了对方需要的事务锁,从而形成了环路等待,产生了死锁。

04 死锁的解除

目前常见的事务存储引擎都支持事务死锁的检测和解除,方法是在无法获取事务锁时以及定期由死锁处理器完成一轮死锁检测:扫描锁系统的锁等待关系,找到锁等待环路,然后选择环路中的一个事务作为所谓的victim来拒绝其锁请求。于是这个victim事务正在执行的DML语句就会返回错误,客户端需要处理这个错误,比如回滚这个事务或者重新执行这个语句。

MySQL的InnoDB存储引擎有自己的死锁检测机制,就按照这个方法来做死锁检测和解除。

05 Klustron 全局死锁的形成机制

全局死锁是一种新型的数据库事务死锁,它发生在分布式数据库集群的全局层面,是单一存储节点的死锁处理器无法发现或者解除的。不过其发生条件与单机数据库情况下完全相同。

下面是一个全局死锁的例子,bank_accounts id为100的记录位于shard1, id为600的记录位于shard2

全局事务1(GT1)全局事务2(GT2)
GT1.T11begin; update bank_accounts set balance=balance-10 where id=100;
GT2.T21begin; update bank_accounts set balance=balance-100 where id=600;
GT1.T12Update bank_accounts set balance=balance+10 where id=600;
GT2.T22update bank_accounts set balance=balance+100 where id=100;

其中GT2.T21阻塞了GT1.T12,GT2阻塞了GT1;GT1.T11阻塞了GT2.T22,GT1阻塞了GT2。于是GT1和GT2形成了环路等待。

全局死锁有这几个特点:

  1. 存储节点事务分支的局部等待导致全局事务的等待,全局事务构成了环路等待关系。
  2. 在每个存储节点内部没有发生死锁,存储节点无法自动发现或解除全局死锁。
  3. 全局死锁环路中的全局事务可以是来自从多个计算节点启动的事务,也可以都来自同一个计算节点。

06 Klustron 全局死锁的处理方式

Klustron发现和处理全局死锁分为以下几步:首先要从集群所有的存储集群主节点获取当前每个存储节点内的本地事务锁等待关系,从而构建全局事务锁等待关系图。然后遍历这个图,寻找等待环路,找到一个环路就从环路中选择一个Victim来拒绝其锁请求,这样Victim客户端就会收到语句执行错误。

07 Klustron 死锁检测的触发和客户端处理

Klustron全局死锁检测机制是kunlun-server计算节点的一个模块,在计算节点启动后作为后台进程运行,称为全局死锁检测后台进程(gdd)。如果某个增删改DML语句执行过程中,发给存储节点一个语句后超过一定时间(start_global_deadlock_detection_wait_timeout)没有返回,那么计算节点就会通知global_deadlock_detector进程触发其做一轮全局死锁检测和解除。同时,gdd也会在后台定期运行,时间间隔设置global_deadlock_detector.naptime=3s。

被选为victim的全局事务执行语句会返回错误 ER_QUERY_CANCLED 给客户端,默认情况下该事务在计算节点内部自动被回滚,用户在本事务后续语句全部忽略直到事务结束。

Klustron也支持MySQL的事务处理模式,如果在事务启动之前预先设置enable_stmt_subxact = true,那么语句执行错误就不会自动回滚事务,而是由客户端代码决定如何处理错误,这一机制适用于所有错误,不仅仅是死锁错误;也同时适用于MySQL连接和PostgreSQL连接。客户端收到任何语句执行错误,可以忽略该错误继续运行后续SQL语句,最后提交事务;或重新执行这个SQL语句,以及其后面的语句,最后提交事务;或直接回滚该事务,然后可以重新执行该事务。

Klustron计算节点会在其运行日志中记录每一次全局死锁检测所回滚的全局事务,供用户追溯和检查

08 Klustron 死锁示例

数据准备如下:

psql -h 10.37.129.6 -p 47001 postgres
create user kunlun_test with password 'kunlun';
create database test_db with owner kunlun_test encoding utf8 template template0;
\q
psql -h 10.37.129.6 -p 47001 -U kunlun_test test_db
create table bank_accounts
(
   id         INT NOT NULL AUTO_INCREMENT,
   balance    DECIMAL(18,2) NOT NULL,
   primary key(id)
) partition by range(id);
create table bank_accounts_p0 partition of bank_accounts 
for values from (1) to (501) with (shard=1);
create table bank_accounts_p1 partition of bank_accounts
for values from (501) to (1001) with (shard=2);

create or replace procedure generate_account_data()
AS $$
DECLARE
  v_balance double;
  i integer = 1;
BEGIN
    while i<=1000 loop
        v_balance = ROUND(1000+RANDOM()*9000,2);
        INSERT INTO bank_accounts VALUES (i,v_balance);
        commit;
        i = i+1;
    end loop;
END; $$
LANGUAGE plpgsql;

call generate_account_data();
analyze bank_accounts;

8.1 Shard 数据死锁

由于Klustron的存储节点使用的是MySQL,所以同Shard的死锁处理是使用MySQL的死锁检测机制。具体执行顺序如下:

Session 1Session 2
begin; update bank_accounts set balance=balance-10 where id=100;
begin; update bank_accounts set balance=balance-100 where id=200;
update bank_accounts set balance=balance+10 where id=200;
update bank_accounts set balance=balance+100 where id=100;

当Session 2执行最后一个update语句时,报错如下:

Session 1的第二个update语句会停止阻塞,更新成功。

8.2 不同 Shard 数据死锁

具体执行顺序如下图:

全局事务1(GT1)全局事务2(GT2)
GT1.T11begin; update bank_accounts set balance=balance-10 where id=100;
GT2.T21begin; update bank_accounts set balance=balance-100 where id=600;
GT1.T12Update bank_accounts set balance=balance+10 where id=600;
GT2.T22update bank_accounts set balance=balance+100 where id=100;

当GT2.T22执行后,Klustron全局死锁检测机制会进行处理,抛出1317的错误。

查看后台日志有下面的输出:

2024-04-05 10:48:18.258 CST [26130]  kunlun_test@test_db/psql: [00000]STATEMENT:  update
bank_accounts set balance=balance+100 where id=100;
2024-04-05 10:48:18.761 CST [25104] LOG:  GDD waken up by backends.
2024-04-05 10:48:18.761 CST [25104] LOG:  Performing one round of global deadlock detection on 1 requests.
2024-04-05 10:48:18.766 CST [25104] LOG:  Global deadlock detector: Global deadlock detector found a deadlock and killed the victim(gtxnid: 1-1712285272-1736). Killed txn branches (shardid, connection-id): (1, 112) (2, 110)
2024-04-05 10:48:18.768 CST [26130]  kunlun_test@test_db/psql: [57014]ERROR:  Kunlun-db: MySQL storage node (1, 1) returned error: 1317, Query execution was interrupted.
2024-04-05 10:48:18.768 CST [26130]  kunlun_test@test_db/psql: [57014]STATEMENT:  update bank_accounts set balance=balance+100 where id=100;
2024-04-05 10:48:18.768 CST [25100] LOG:  Start processing 2  sharding topology checks.
2024-04-05 10:48:18.768 CST [25104] LOG:  Performing one round of global deadlock detection on 1 requests.

8.3 Rocksdb 存储引擎表死锁检测

重新创建测试表为Rocksdb存储引擎,再进行相关测试

drop table bank_accounts;
create table bank_accounts
(
   id         INT NOT NULL AUTO_INCREMENT,
   balance    DECIMAL(18,2) NOT NULL,
   primary key(id)
) partition by range(id);
create table bank_accounts_p0 partition of bank_accounts
for values from (1) to (501) with (shard=1,engine=rocksdb);
create table bank_accounts_p1 partition of bank_accounts
for values from (501) to (1001) with (shard=2,engine=rocksdb);
call generate_account_data();
analyze bank_accounts;

然后重复8.2中的测试过程,也出现了预期的全局死锁处理结果。

8.4 Rocksdb InnoDB 混合存储引擎表死锁检测

重新创建测试表为Rocksdb和InnoDB两种存储引擎,再进行相关测试

drop table bank_accounts;
create table bank_accounts
(
   id         INT NOT NULL AUTO_INCREMENT,
   balance    DECIMAL(18,2) NOT NULL,
   primary key(id)
) partition by range(id);
create table bank_accounts_p0 partition of bank_accounts
for values from (1) to (501) with (shard=1,engine=rocksdb);
create table bank_accounts_p1 partition of bank_accounts
for values from (501) to (1001) with (shard=2);
call generate_account_data();
analyze bank_accounts;

然后重复8.2中的测试过程,同样也出现了预期的全局死锁处理结果。

Klustron全局死锁检测机制能够快速有效的发现和检测全局死锁,应用开发者需要根据本文第7节内容正确地处理死锁错误,保证应用系统高效流畅运行。

END