如何解决热点更新导致的雪崩效应

发布时间:2021-09-13 18:17:13 作者:柒染
来源:亿速云 阅读:164

本篇文章给大家分享的是有关如何解决热点更新导致的雪崩效应,小编觉得挺实用的,因此分享给大家学习,希望大家阅读完这篇文章后可以有所收获,话不多说,跟着小编一起来看看吧。

PartⅠ 案例分析

这个故障的场景比较简单,当时业务出现了大量的请求失败,几乎处于不可用状态。同时对应的MySQL数据库也存在大量的CPU使用率高的告警。

1. 登上数据库,通过show processlist 查看到的现场截图如下:

2. MySQL 版本为5.7,数据库表结构如下:

CREATE TABLE `docid_generator` (`id` int(4) NOT NULL AUTO_INCREMENT,PRIMARY KEY (`id`)) ENGINE=InnoDB AUTO_INCREMENT=2

3. 业务请求Session如下:

connectupdate docid_generator set id=last_insert_id(id+1); select last_insert_id() exit

通过初步排查,了解到:表中只有一个字段、一行记录,该段业务逻辑是通过mysql中last_insert_id(expr)函数特性实现id分配功能;按照processlist执行耗时倒序查看,耗时最长的sql也是该类update请求;innodb status可以看到大量的事务在等待该条记录的X锁;update的X锁使得请求只能串行进行,导致响应很慢,可是最先到来的一批update请求是什么原因卡住了呢?

通过pref分析,显示lock_deadlock_recursive函数占据了cpu recycle事件的近50%时间。该函数是通过深度优先算法进行递归调用,检测是否满足死锁条件,再进行最小代价的事务回滚。

查看information_schema中innodb_trx事务锁等待队列,发现已经有6100+条锁等待信息。

通过查阅文档发现,InnoDB监控器输出的最近死锁检信息中包含“TOO DEEP OR LONG SEARCH IN THE LOCK TABLE WAITS-FOR GRAPH, WE WILL ROLL BACK FOLLOWING TRANSACTION”,表示处于等待的事务列表长度已达到限制200。超过200个事务的等待列表被视为死锁,并且将回滚尝试检查等待列表的事务。如果锁定线程必须查看等待列表上的事务拥有的超过1,000,000个锁,则也可能发生相同的错误。

每个请求维护自己的锁队列,在这个案例中,业务的并发为200个,因为单条记录X锁,只能串行执行,按照先后顺序依次维护自己的锁队列,极限情况记录阻塞的锁队列长度为(1+199)*200/2!所以这一阶段耗时较长。

知道耗时长的原因就好办了。因为业务场景是单一的id分配,只有一条记录,逻辑上不会出现死锁情况,所以完全可以关闭死锁检测功能。很幸运,5.7版本innodb_deadlock_detect可以关闭死锁检测。关闭后,我们再次200并发测试,从原来的10s降低到0.2s,性能提升50倍。

分析到这里,相信大家对这个故障案例也一定有了比较深刻的了解。在之前到的介绍里为了不打断故障分析的连贯性,略过了一些数据库概念的介绍,下面挑选几个给大家详细介绍下。

“死锁”可以理解为两个或两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

在数据库中我们可以形象的理解为:

如上图所示,事务A在等待事务B释放id=2的锁,事务B在等待事务A释放id=1的锁。

这种情况就是死锁,发生死锁有两种方法解决:

1)直接进入等待,直到超时。这个超时时间可以通过参数innodb_lock_wait_timeout来设置

2)发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以执行。将参数innodb_deadlock_detect设置为on,表示开启这个逻辑。

innodb_deadlock_detect=on,该选项使用了禁用MySQL的死锁检测功能的。在高并发系统上,当许多线程等待同一个锁时,死锁检测可能导致速度减慢。当发生死锁时,如果禁用了死锁检测则可能会更有效,这样可以依赖innodb_lock_wait_timeout的设置进行事务回滚。

MySQL默认情况下是开启了死锁检测的,InnoDB自动检测发送死锁的事务,并回滚其中的一个事务或所有导致死锁的事务。InnoDB会在导致死锁的事务中选择一个权重比较小的事务来回滚,这个权重值可能是由该事务insert, updated, deleted的行数决定的。

如果innodb_table_locks = 1(默认值)并且autocommit = 0,则InnoDB能感知到表锁的存在,并且上层的MySQL层知道行级锁。否则,InnoDB无法检测到由MySQL LOCK TABLES语句设置的表锁或由除InnoDB之外的存储引擎设置的锁定的死锁。通过设置innodb_lock_wait_timeout系统变量的值来解决这些情况。

Part Ⅱ 切实有效的应急降级解决方案

如果电商业务在大促和秒杀场景、在线教育业务在报名和签到、游戏业务开服等高并发场景中遇到了类似的热点更新故障,相信大家一定不会有太多时间理性的梳理和挖掘问题的根因,在较短做出最合理优化方案的难度也较大。而此时用户或者业务方对数据库的要求必然是不管用什么方法,先让业务跑起来(恢复)再说。

那么对于热点更新类的故障,DBA常用的应急预案:重启、切换、kill(不论是使用pt-kill还是自己的kill脚本,显然都很难解决,而且会加剧阻塞)、权限控制(极可能误伤一些正常的核心业务逻辑,导致业务依然失败),大概率无法完成业务恢复。即使有损降低如果不依靠业务侧介入都很难完成。

腾讯云数据库智能管家DBbrain,为了防止在热点更新时,用户数据库不被大压力打挂,提供了“SQL限流”和“热点数据防护”这两大功能,帮助用户可以在数据库端实现切实有效的降级和防护,保障用户核心业务能正常运行。

1. SQL限流

DBbrain提供了“SQL限流”功能,能够帮助用户在数据库侧实现优雅的临时降级。通过在SQL进入数据库内核之前拒绝的方式,能解决更多高并发故障中,通过kill无法快速恢复的场景,除了上文介绍的“热点更新引发死锁检测阻塞的场景”之外,还适用于:

用户可以通过在DBbrain控制台中,设置目标SQL的特性。

DBbrain会根据SQL样本的关键字自动拒绝请求,保证业务核心服务的正常运行,并且统计在开启“SQL限流”时间段内被拒绝的SQL请求数量。

2. 热点更新保护

DBbrain针对于秒杀场景,大幅度优化对于单行数据的update操作的性能。当开启热点更新自动探测时,系统会自动探测是否有单行的热点更新(同一数据行上面等待的行锁数量超过32个后续的事务就会开始等待),如果有,则会让大量的并发update排队执行,以减少大量行锁或触发大量死锁检测造成的并发性能下降。

DBbrain提供的“热点更新保护”功能,支持自动结束和手动关闭两种模式,设置自动结束时间可实现灵活控制。

3. 热点更新优化建议

在上面的案例中,5.7.15以上的版本可以通过关闭死锁检测方式提升性能,也可以通过 腾讯云数据库智能管家DBbrain提供的“SQL限流”和“热点更新保护”来缓解大量热点更新对数据库带来的负载压力。那么接下来的章节将从业务实现的角度分享一些启发建议。

3.1)基于MySQL实现

表结构如下:

CREATE TABLE `id_allocate` (`id` bigint NOT NULL AUTO_INCREMENT,business_tag varchar(20) not null,PRIMARY KEY (`id`),UNIQUE KEY `name` (business_tag)) ENGINE=InnoDB AUTO_INCREMENT=2;

3.1.1)类似上文例子,通过mysql last_insert_id(expr)函数方法:

请求逻辑:

connectupdate id_allocate set id=last_insert_id(id+1) where business_tag='test1'; select last_insert_id() exit注意点:5.7以上关闭死锁检测innodb_deadlock_detect;

3.1.2)通过mysql auto_increment字段,去掉business_tag字段,只保留id字段,请求逻辑:

connectinsert into id_allocate value(null); select last_insert_id() exit

注意点:数据量会持续增大,可以定期低峰删除或者创建为分区表,定期删除历史数据

纯依赖MySQL实现,第一种方法更简单易用。高可用上,常见的思路是存在2个MySQL实例中,设置自增的步长和起始值,比如两个数据库,设置auto-increment-increment=2,分别设置auto-increment-offset为1和2,业务请求这两个DB依次获取到1,3,5,7和2,4,6,8。该方法可避免单MySQL故障的影响,但同时系统的严格单调递增也变成了趋势递增(若单机故障,可能还有id变小的情况)。

3.2)基于Redis实现

利用redis的incr和incrby方式,能支撑的qps更高。同样若担心高可用问题,可以设置两个key分别存储在两个redis实例上,通过控制初始值和incrby的offset来保障。这里显著的弊端是 redis数据不能持久化,但目前腾讯云redis支持了主备同步、双机房容灾和备份功能,对于项目开发紧急,性能要求高的场景也可以尝试使用。

3.3)服务化实现

表结构:

CREATE TABLE `id_allocate` (`id` bigint NOT NULL AUTO_INCREMENT,business_tag varchar(20) not null,max_id bigint not null,step int not null,PRIMARY KEY (`id`),UNIQUE KEY `name` (business_tag)) ENGINE=InnoDB AUTO_INCREMENT=2;

business_tag标识业务;

max_id标识目前分配出去的最大id;

step标识每次idallocate-server访问数据库时候一次拉走的id区间大小。

实现思路:第三方通过调用idallocate-server服务获取id 。idallocate-server内存至少包含三个值:当前的mid,最大能发的id1,最大能发的id2;id2和id1相差一个step。初始时候,idallocate-server服务从数据库中更新两次,分别得到初始值mid、id1和id2:

beginselect max_id from id_allocate where business_tag='test1' for update; #得到midupdate id_allocate set max_id=max_id+step  where business_tag='test1';select max_id from id_allocate where business_tag='test1';#得到id1commit
beginupdate id_allocate set max_id=max_id+step  where business_tag='test1';select max_id from id_allocate where business_tag='test1';#得到id2commit

随着第三方请求idallocate-server获取id,mid一直增大,当达到id1的90%时候,需检测id2是否已经存在,若不存在则访问数据库进行获取。若存在则mid达到id1大小后,分配id2部分,当mid达到id2的90%时候,需检测id1是否存在。依次循环保证idallocate-server内存中至少有一个step大小的buffer号段存在。

上述方案中:

1. 可用性:idallocate-server服务可以横向扩展,避免单点;MySQL层面可以通过主备集群半同步或者强一致性同步来保证,且短时间内MySQL故障也不会影响服务。

2. 性能:将更新MySQL的请求降低为纯MySQL id分配 方式的 1/step(没step个id大小 更新一次db),降低数据库的压力;同时通过id2和id1双号段的设计,避免了当单独id1分配完全,需等待idallocate-server实时去db更新获取最新数据 这种延时毛刺

以上就是如何解决热点更新导致的雪崩效应,小编相信有部分知识点可能是我们日常工作会见到或用到的。希望你能通过这篇文章学到更多知识。更多详情敬请关注亿速云行业资讯频道。

推荐阅读:
  1. redis产生雪崩的解决方法
  2. 如何解决redis缓存雪崩

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

mysql

上一篇:PHP中的层次性能分析器的介绍、安装与使用

下一篇:如何使用php foreach修改值

相关阅读

您好,登录后才能下订单哦!

密码登录
登录注册
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》