ITPub博客

首页 > 数据库 > 数据库开发技术 > OceanBase V3.2 分布式死锁测试分析

OceanBase V3.2 分布式死锁测试分析

原创 数据库开发技术 作者:plvision 时间:2023-09-12 11:09:04 0 删除 编辑


本文主要是测试 OceanBase 企业版 3.2.4  里死锁现象和分析方法。OceanBase 的锁和死锁特征跟传统数据库 ORACLE/MySQL 还是有很大区别。

1. 测试准备
很多开发口中的“死锁”并不是死锁,而是锁阻塞,或锁等待。本文说的“死锁”通常就是两到多个会话事务在资源的锁定和等待上出现环路。传统集中式数据库很容易就能通过算法检测到死锁并立即解决。但是在分布式数据库里,这个环路的检测还是有一定难度的。
要模拟测试分布式数据库上的死锁,需要考虑两种场景。一个是发生在单节点内部的死锁;二是发生在多个节点上的死锁,后者可以称之为分布式死锁。
要测试后者,OB 集群和租户必须是多节点架构。这里我选用的是 1-1-1 的三副本架构。
1.1 租户
本集群是一个三节点三副本集群,租户也是三副本。其中 `primary_zone` 设置为 `zone1,zone2` 。即在 `zone1` 和 `zone2` 做负载均衡。
租户环境资源分布信息。
select t.tenant_id, t.tenant_name, t.zone_list, t.primary_zone, t.locality, u.resource_pool_name, u.zone, u.svr_ip from gv$tenant t join gv$unit u on (t.tenant_id=u.tenant_id)where t.tenant_id=effective_tenant_id()order by t.tenant_id, u.zone, u.svr_ip;
查询结果如下,为节省空间,删除了部分列(后同)。
tenant
_id
tenan
t_name
primary
_zone
resource_pool_name zone svr_ip
1,002
obmysql
zone1,zone2
pool_obmysql_zone1_ltr
zone1
10.0.0.61
1,002
obmysql
zone1,zone2
pool_obmysql_zone2_fwj
zone2
10.0.0.62
1,002
obmysql
zone1,zone2
pool_obmysql_zone3_vug
zone3
10.0.0.63

1.2 表信息
这里用分区表做测试。因为 3.2 的分区表做负载均衡的时候,不同分区可以分布在不同节点上。
建表语句:
create table bmsql_oorder (  o_w_id       integer      not null,  o_d_id       integer      not null,  o_id         integer      not null,  o_c_id       integer,  o_carrier_id integer,  o_ol_cnt     integer,  o_all_local  integer,  o_entry_d    timestamp,  primary key (o_w_id, o_d_id, o_id)) partition by hash (o_w_id) partitions 12 ;

确认一下实际表分区主副本分布在多个节点上。
SELECT  t5.tenant_name, t5.primary_zone tnt_primary_zone,  t4.database_name, t4.primary_zone db_primary_zone,  t3.tablegroup_name, t3.primary_zone tg_primary_zone,  conv(t1.table_id,10,16) table_id, t1.primary_zone ,  t1.table_name,   t2.partition_id,  t2.role,  t2.zone,  concat(t2.svr_ip, ':', t2.svr_port) observer,  round(t2.data_size / 1024 / 1024) data_size_mb,  t2. row_count FROM  gv$table t1  JOIN gv$partition t2 ON  (t1.tenant_id = t2.tenant_id AND t1.table_id = t2.table_id)   LEFT JOIN __all_virtual_tablegroup t3 ON  (t1.tenant_id = t3.tenant_id  AND t1.tablegroup_id = t3.tablegroup_id)    JOIN gv$database t4 ON  (t1.tenant_Id = t4.tenant_id  AND t1.database_id = t4.database_id)    JOIN gv$tenant t5 ON  (t1.tenant_id = t5.tenant_id)WHERE  t5.tenant_id = 1002  AND t2.role = 1 AND t4.database_name = 'TPCCDB' AND t1.table_name = 'bmsql_oorder'ORDER BY  t5.tenant_name,  t4.database_name,  t3.tablegroup_name,  t2.partition_id;
查询结果如下,为节省空间,删除了部分列。
primary_zone table_name partition_id zone observer
zone1,zone2
bmsql_oorder
0
zone2
10.0.0.62:2882
zone1,zone2
bmsql_oorder
1
zone2
10.0.0.62:2882
zone1,zone2
bmsql_oorder
2
zone2
10.0.0.62:2882
zone1,zone2
bmsql_oorder
3
zone2
10.0.0.62:2882
zone1,zone2
bmsql_oorder
4
zone2
10.0.0.62:2882
zone1,zone2
bmsql_oorder
5
zone2
10.0.0.62:2882
zone1,zone2
bmsql_oorder
6
zone1
10.0.0.61:2882
zone1,zone2
bmsql_oorder
7
zone1
10.0.0.61:2882
zone1,zone2
bmsql_oorder
8
zone1
10.0.0.61:2882
zone1,zone2
bmsql_oorder
9
zone1
10.0.0.61:2882
zone1,zone2
bmsql_oorder
10
zone1
10.0.0.61:2882
zone1,zone2
bmsql_oorder
11
zone1
10.0.0.61:2882
一共 12 个分区。有 2 个分区没有数据。其中 分区 0-5 在 节点 62 ,分区 6-11 在节点 61 上。如果实际结果不是这样,就看看从租户到数据库到表分组到表的 primary_zone 是否设置正确。一般是后者继承自前者,只需要设置租户的即可。
1.3 测试数据
从表 `bmsql_oorder` 找出仓库 ID 跟分区的对应信息。
select '0' as "partition id", o_w_id "warehouse id" , count(*) cnt from tpccdb.bmsql_oorder partition (p0) group by o_w_id  union select '1' as "partition id", o_w_id "warehouse id" , count(*) cnt from tpccdb.bmsql_oorder partition (p1) group by o_w_id  unionselect '2' as "partition id", o_w_id "warehouse id" , count(*) cnt from tpccdb.bmsql_oorder partition (p2) group by o_w_id  union select '3' as "partition id", o_w_id "warehouse id" , count(*) cnt from tpccdb.bmsql_oorder partition (p3) group by o_w_id  union select '4' as "partition id", o_w_id "warehouse id" , count(*) cnt from tpccdb.bmsql_oorder partition (p4) group by o_w_id  union select '5' as "partition id", o_w_id "warehouse id" , count(*) cnt from tpccdb.bmsql_oorder partition (p5) group by o_w_id  union select '6' as "partition id", o_w_id "warehouse id" , count(*) cnt from tpccdb.bmsql_oorder partition (p6) group by o_w_id  union select '7' as "partition id", o_w_id "warehouse id" , count(*) cnt from tpccdb.bmsql_oorder partition (p7) group by o_w_id  union select '8' as "partition id", o_w_id "warehouse id" , count(*) cnt from tpccdb.bmsql_oorder partition (p8) group by o_w_id  union select '9' as "partition id", o_w_id "warehouse id" , count(*) cnt from tpccdb.bmsql_oorder partition (p9) group by o_w_id  union select '10' as "partition id", o_w_id "warehouse id" , count(*) cnt from tpccdb.bmsql_oorder partition (p10) group by o_w_id  union select '11' as "partition id", o_w_id "warehouse id" , count(*) cnt from tpccdb.bmsql_oorder partition (p11) group by o_w_id  union select '12' as "partition id", o_w_id "warehouse id" , count(*) cnt from tpccdb.bmsql_oorder partition (p0) group by o_w_id ;

查询结果如下:

partition id warehouse id cnt
1 1 30,028
2 2 30,026
3 3 30,033
4 4 30,029
5 5 30,032
6 6 30,041
7 7 30,027
8 8 30,033
9 9 30,027
10 10 30,040
所以挑选的测试记录如下。
select * from tpccdb.bmsql_oorder where o_w_id in (4,5,6) and o_d_id = 1 and o_id in (1,2)order by o_w_id ,o_d_id ,o_id ;
查询结果如下(第一列为我添加的,用于说明记录的主副本所在节点位置)。下面记录将用于后面测试场景。
svr_ip o_w_id o_d_id o_id o_c_id o_ol_cnt o_entry_d
10.0.0.62 4 1 1 2,520 7 2023-07-04 18:40:51.000
10.0.0.62 4 1 2 2,678 14 2023-07-04 18:40:51.000
10.0.0.62 5 1 1 1,681 12 2023-07-04 18:41:58.000
10.0.0.62 5 1 2 2 14 2023-07-04 18:41:58.000
10.0.0.61 6 1 1 1 8 2023-07-04 18:43:10.000
10.0.0.61 6 1 2 2 7 2023-07-04 18:43:10.000



2. 测试场景
常见的死锁场景就是 两个会话事务,彼此分别锁定了对方需要的资源,却又在等待对方释放锁。
首先构建单节点内的死锁场景。
2.1 单节点内的死锁
  • 会话 1 更新记录不提交。
mysql> begin;select connection_id(); update bmsql_oorder set o_ol_cnt = o_ol_cnt+10, o_entry_d = now() where o_w_id = 4 and o_d_id = 1 and o_id = 1 ;Query OK, 0 rows affected (0.00 sec)
+-----------------+| connection_id() |+-----------------+| 3221870232 |+-----------------+1 row in set (0.00 sec)
Query OK, 1 row affected (0.10 sec)Rows matched: 1 Changed: 1 Warnings: 0
  • 会话 2 更新另外一笔记录不提交。
mysql> begin;select connection_id(); update bmsql_oorder set o_ol_cnt = o_ol_cnt+10, o_entry_d = now() where o_w_id = 5 and o_d_id = 1 and o_id = 1 ;Query OK, 0 rows affected (0.03 sec)
+-----------------+| connection_id() |+-----------------+| 3221742394 |+-----------------+1 row in set (0.01 sec)
Query OK, 1 row affected (0.30 sec)Rows matched: 1 Changed: 1 Warnings: 0
会话 2 更新的记录跟会话 1 更新的记录是在同一个节点内部。
  • 会话 1 更新被会话2 锁住的记录。

update bmsql_oorder set o_ol_cnt = o_ol_cnt+10, o_entry_d = now() where o_w_id = 5 and o_d_id = 1 and o_id = 1 ;
会话 1 发生阻塞。

  • 会话 2 也更新会话 1 锁住的记录。

update bmsql_oorder set o_ol_cnt = o_ol_cnt+10, o_entry_d = now() where o_w_id = 4 and o_d_id = 1 and o_id = 1 ;
此时也发生阻塞。
按常理,已经形成死锁环路,但是没有报错,两个会话依然继续阻塞/等待。
最终两个会话都分别因为语句超时报错。
  • 语句超时报错 
mysql> update bmsql_oorder set o_ol_cnt = o_ol_cnt+10, o_entry_d = now() where o_w_id = 4 and o_d_id = 1 and o_id = 1 ;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> show global variables where variable_name in ('ob_query_timeout','ob_trx_idle_timeout','ob_trx_lock_timeout','ob_trx_timeout');+---------------------+------------+| Variable_name | Value |+---------------------+------------+| ob_query_timeout | 10000000 || ob_trx_idle_timeout | 1000000000 || ob_trx_lock_timeout | -1 || ob_trx_timeout | 1000000000 |+---------------------+------------+4 rows in set (0.01 sec)
在官网 OCP 里点击 【租户】-【会话管理】-【死锁分析】里的 死锁诊断,也没有输出。
这个跟官网文档([# 死锁检测](https://www.oceanbase.com/docs/enterprise-oceanbase-database-cn-10000000000945520)) 里说的不一致。

2.2 开启死锁检测

后咨询 OB 技术,了解到是没有在 OCP 里开启“死锁检测”。

+ 前提条件- 当前登录账号具备集群的管理权限。- 当前集群版本为 V3.2.0 及以上,且集群非只读集群。- 集群未开启死锁自动检测功能。
+ 操作步骤1. 登录 OCP 。默认进入 **集群** 页面。    2. 在 **集群** 页面的 **集群列表** 区域,选择待操作的集群并单击其集群名。    3. 单击 **总览** 页面右上角的 **...** 图标,在菜单栏中单击 **开启死锁自动检测** 。    4. 在对话框中单击 **确定** ,即可开启死锁自动检测功能。

开启死锁自动检测后,OceanBase 会自动发现死锁,并对死锁进行回滚,同时可保存近 7 天的死锁记录。

注意: 开启死锁需要消耗 2% 左右的性能,请谨慎操作。

按照提示操作后。果然这次能检测到死锁了。

文档对于原理没有多说,应该是开启了什么参数。通过 OB 的事件日志表 `__all_rootservice_event_history` 找到了相应的线索。

select gmt_create , module, event, name1, value1, name2, value2,name3, value3  from `__all_rootservice_event_history` e where module in ('root_service') and event='admin_set_config'order by gmt_create desc limit 5;


gmt_create module event name1 value1 name2 value2
2023-07-11 15:51:53.826 root_service admin_set_config ret 0 arg {items:[{name:"_lcl_op_interval", value:"0ms", comment:"", zone:"", server:"0.0.0.0", tenant_name:"", exec_tenant_id:1, tenant_ids:[]}], is_inner:false}
2023-07-11 15:50:18.221 root_service admin_set_config ret 0 arg {items:[{name:"_lcl_op_interval", value:"20ms", comment:"", zone:"", server:"0.0.0.0", tenant_name:"", exec_tenant_id:1, tenant_ids:[]}], is_inner:false}

从事件日志里我开启死锁检测和关闭死锁检测都有记录,是通过隐含参数 `_lcl_op_interval` 实现死锁检测,为`0ms` 表示关闭检测。

 查看隐含参数值。

alter system set `_lcl_op_interval` = '25ms';select gmt_modified , svr_type, svr_ip , name, data_type , value ,`section` ,`scope` ,edit_level  from `__all_sys_parameter` p where p.name='_lcl_op_interval' ;
gmt_modified name data_type value scope edit_level
2023-07-11 16:42:06.410 _lcl_op_interval varchar 25ms CLUSTER DYNAMIC_EFFECTIVE

开启死锁检测后,可以通过内部表 `__all_virtual_deadlock_event_history` 查看相关信息。

select tenant_id, event_id, svr_ip, detector_id, usec_to_time(report_time) report_time, cycle_idx, cycle_size, role, priority, usec_to_time(create_time) create_time, start_delay,visitor, resource, extra_name1,extra_value1from __all_virtual_deadlock_event_historyorder by create_time desc limit 10;

OCP 提供了一个页面诊断死锁,可读性更好一些。

分布式数据库死锁检测是个难点,官网关于“死锁检测”的原理说明:[OceanBase 主动死锁检测](https://www.oceanbase.com/docs/enterprise-oceanbase-database-cn-10000000000946538) ,这里不深入讨论。

这里需要注意,跟传统数据不一样的地方是,OB 检测到死锁后只是中断了发生死锁的语句,并不会回滚整个事务。另外一个事务依然处于阻塞/等待状态,直到语句超时。所以,需要应用能捕获数据库异常,在异常处理里及时回滚事务。

目前这个死锁还属于单个节点内部的死锁,更大的挑战是跨多个节点的死锁检测,即分布式死锁检测。

2.3 多节点内的死锁

更换两笔记录 (`o_w_id` 为 5 和 6)的更新。

  • 会话 1  更新并锁住一笔记录。

set session ob_query_timeout=800000000;begin;select now(),connection_id(); update bmsql_oorder set o_ol_cnt = o_ol_cnt+10, o_entry_d = now() where o_w_id = 5  and o_d_id = 1 and o_id = 1 ;


  • 会话 2 更新并锁住另外一笔记录。

set session ob_query_timeout=800000000;begin;select now(),connection_id(); update bmsql_oorder set o_ol_cnt = o_ol_cnt+10, o_entry_d = now() where o_w_id = 6  and o_d_id = 1 and o_id = 1 ;

  • 会话 1 继续更新会话 2 锁住的记录。

update bmsql_oorder set o_ol_cnt = o_ol_cnt+10, o_entry_d = now() where o_w_id = 6 and o_d_id = 1 and o_id = 1 ;

此时发生阻塞。

  • 会话 2 继续

mysql> update bmsql_oorder set o_ol_cnt = o_ol_cnt+10, o_entry_d = now() where o_w_id = 5 and o_d_id = 1 and o_id = 1 ;ERROR 1213 (HY000): Deadlock

会话 2 报错,发生死锁。

此时,会话 1 依然是阻塞等待状态,直到会话 2 提交或者回滚事务,或者会话 1 语句超时或者事务超时。

死锁内部表数据:

再查看 OCP 中的死锁检测报告。

不同之处在于,OCP 指出了其中一个请求的资源是`remote row`  ,说明这是一个分布式死锁。



3. 测试结论
OB 自 1.0 版本到 3.2 版本就有个特点, SQL 语句执行默认 10 秒超时,兼容MySQL 有个 锁等待超时时间(默认为 0,不启用)。事务未提交默认超时时间 100 秒,事务空闲默认超时时间 120 秒。3.2 版本之前没有死锁检测功能,3.2 版本开始解决死锁检测问题,相关原理 OB 也发表了论文在国际数据库顶级会议 ICDE 2023上。论文题为《LCL: A Lock Chain Length-based Distributed Algorithm for Deadlock Detection and Resolution》。该论文获得高度评价。该论文提出的创新算法可以在分布式环境下准确识别并消除数据库系统中所有真正的死锁,这对于提高分布式数据库系统的可靠性和性能具有重要的意。这里不展开了。
实际测试发现,目前分布式死锁检测需要打开一个隐含参数,性能会有 2% 的下降。默认是关闭。打开分布式死锁检测后,能降低应用阻塞的概率。应用操作 OB 数据库需要捕获所有异常(不限于这里提到的超时异常、死锁异常等),在事务中发生异常需要显示的回滚事务。传统数据库的应用开发也要遵守这个规范。所以对应用来说这个要求也不过分。
不过 OB 的死锁检测功能应该还会进一步改进。据论坛(http://open.oceanbase.com)里透露的信息会考虑租户的类型。如果是 ORACLE 租户就跟ORACLE保持一致,只回滚当前语句,不回滚事务;如果是MySQL租户,就跟MySQL保持一致,默认回滚整个事务。


4. 其他参考
  • [OceanBase 企业版3.2 正式发布](https://ask.oceanbase.com/t/topic/13700131)
  • [OceanBase 死锁检测](https://www.oceanbase.com/docs/enterprise-oceanbase-database-cn-10000000000945520)
  • [从ORACLE/MySQL到OceanBase:数据库超时机制](https://mp.weixin.qq.com/s/zQcuvFbyhsPjSS2s6_2F1w)
  • [国际顶会 ICDE入选 | OceanBase 死锁检测实现了哪些技术突破?- 知乎](https://zhuanlan.zhihu.com/p/624847601)


原文作者:数据库技术闲谈;如有侵权,请联系管理员删除。

请登录后发表评论 登录
全部评论

注册时间:2018-11-02

  • 博文量
    156
  • 访问量
    96850