背景介绍
某保险客户核心系统底层存储使用OceanBase数据库,系统上线后发现简单的主键更新语句,出现sql执行语句耗时波动非常大的异常情况,此时DBA立即上线分析OB审计日志,分析异常sql 的执行情况。
异常主键更新SQL
审计日志分析
从审计日志里面对异常sql进行统计,sql通过主键ID进行更新,正常情况下不到0.5ms 即可返回,但是异常情况下,最大执行时间甚至超过11秒。根据最大执行时间的执行sql的trace_id, 检索对应observer 日志,可以发现有6005错误,failed to lock write memtable相关信息,可确定为行级锁冲突导致的sql变慢。
ERROR 6005 (HY000) : Try lock row conflict
OceanBase 错误码:6005
错误原因:更新操作加锁失败,向上层返回该错误码并重试。
此种异常问题类似热门商品在营销活动中限时秒杀,属于热点更新场景。热点更新的本质是短时间内对数据库中的同一行数据的某些字段值进行高并发的修改(余额,库存等),这其中的瓶颈主要在于关系型数据库为了保持事务一致性,对数据行的更新都需要经过“加锁,更新,写日志提交,释放锁”的过程,而这个过程实质上是串行的。 所以,提高热点行更新能力的关键在于如何尽可能缩短持有锁的时间。OceanBase 在这个问题上通过持续的探索,提出了一种基于分布式架实现的“提前解行锁(Early Lock Release)”的方案(即“ELR”),提升类似业务场景中单行并发更新的能力。
技术原理
事务提交流程
- 优化前
当用户发起commit之后,DB端开始触发日志的持久化操作:序列化内存数据并提交本地『buffer manager』,然后发给所有备机,等多数派备机同步日志成功之后,日志才算持久化成功,最后才会解锁并给客户端应答事务提交成功。显然一个事务持锁的时间,包括了4个方面:数据写入+日志序列化+同步备机网络通信+日志刷盘的耗时。对于三地五中心或者磁盘比较差的场景,热点行的性能影响还是比较大的。
- 优化后
整个提交流程基本不变,仅仅对解锁的时机做了调整。新方案里面,等日志序列化完成,提交到『buffer manager』之后,就开始触发解锁操作,不再等日志多数派刷盘完成,从而降低了整个事务的持锁时间。当前事务解锁之后,允许后续的事务进来操作同一行,到达了多个事务并发更新同一行的效果,从而提高了系统的吞吐能力。
基于上述原理,一个热点行场景的性能,性能的计算公式如下:
TPS=1/一个事务内热点行的持锁耗时,这里的持锁耗时,表示从加锁开始算起,到事务commit的时间间隔;
对于三地五中心场景下,由于整体sql的耗时是30ms,事务跨城的commit rt大约为30ms,因此有了热点行优化之后,性能基本能跟同城部署的性能一致。
正确性保证
(1)两个概念
前驱事务:提前解锁的事务;
后继事务:当前驱事务解锁之后,后面操作同一行的事务会读取到前驱的最新数据,这样后继和前驱产生了『依赖』,我们称当前事务为后继事务。
(2)重要问题解决方案
- 提前解锁的事务客户端应答时机
提前解锁的事务,并不代表日志一定会同步成功。所以解锁之后,不能立即给客户端应答commit成功,需要等日志完成持久化成功之后再决定。
- 前驱和后继并发场景下,提交状态如何决定
前驱事务如果出现了回滚,后继事务必然需要回滚。前驱没有明确commit成功之前,后继事务是不能确定commit成功,需要等前驱的状态确定。
- 级联回滚
如果一行上默认并发的事务很多,一旦最开始的前驱事务回滚,则所有的后继事务都必须回滚,给业务带了在灾难性的问题。为了尽量降低该问题产生的概率,OB限制单行上最大允许并发的事务数量为10,且根据实际情况,可以配置。
应用改造
Mybatis statementType选择
MYbatis支持STATEMENT,PREPARED 或 CALLABLE(存储过程) ,默认是PREPARED,保持默认PREPARED即可;不要使用statementType="CALLABLE",当使用statementType="CALLABLE"时,驱动层会执行 use database,show function like,这些语句比较费性能,因此当前来说不建议使用statementType="CALLABLE",推荐使用PREPARED
注意:PREPARED下也能支持call PL()调用存储过程,可以参考以下写法
delimiter $$
create procedure prc_update_budget (
`pk_id` bigint(18),
`uk_sbid` varchar(64),
`amount` bigint(18)
)
begin
update budget set
CURRENT_AMOUNT = CURRENT_AMOUNT - `amount`,
GMT_MODIFY = now()
where ID = `pk_id` and CURRENT_AMOUNT >= `amount`;
if row_count() <= 0 then
rollback;
signal SQLSTATE 'NOT_ENOUGH';
else
commit;
end if;
end $$
delimiter ;
OBSERVER端参数优化
alter system set enable_early_lock_release=true tenant=all;
alter system set enable_early_lock_release=false tenant=sys;
alter system set syslog_level="ERROR";
alter system set enable_sql_audit=true;
alter system set enable_perf_event=true;
alter system set cpu_quota_concurrency = 4;
alter system set _ob_enable_prepared_statement = true;
注意点:
alter system set enable_early_lock_release=true tenant=all;
alter system set enable_early_lock_release=false tenant=sys;
alter system set syslog_level="ERROR";
alter system set enable_sql_audit=true;
alter system set enable_perf_event=true;
alter system set cpu_quota_concurrency = 4;
alter system set _ob_enable_prepared_statement = true;
走了远程执行计划的热点行sql,走不到热点行优化路径的,热点行能力也会大大下降;由于远程执行计划难以直接发现,因此最好配置巡检,主动发现。