返回信息流“码小农”第二期来了!!感谢阿里前辈的支持,这一期将会有三篇文章供大家学习!!我们把原博文贴上来,大家自己认真学习总结吧~希望大家积极思考,尽情讨论!
另:
“码小农”第一期:读《一个递归引发的思考》有感
InnoDB读事务优化
希羽 2014-03-19 10:17:11 发表于:ATA之家
InnoDB的读写事务及暴露的问题
在早期版本的InnoDB存储引擎中,读写事务一直没有进行分离,统一化处理,在这种思路下实现的读写事务模块,埋下了性能问题隐患。随着MySQL被大量引入到互联网公司承担核心的数据在线存储功能时,在高并发的压力下,这些性能问题就开始慢慢暴露出来。最初在MySQL社区官方Buglist中提到相关问题的是Facebook的Mark Callaghan。他抛出的一个关于读视图性能的Bug: Bug49169。这个Bug在高并发的读场景下,非常容易发现,在现在看来算是一个MySQL社区内人人皆知的性能问题。但解决这个问题没有那么容易,从2009年提出被官方确认,到最后关闭已经是2013的事情。
这个Bug为啥持续这么久时间?社区里的开发者和官方开发者是如何推进这个问题的?其优化思路是怎样的?我们在这篇文章来讲述其来龙去脉。
因为这个Bug与读视图有关,我们首先得了解读视图。
何为读视图
简单的讲,读视图就是一个查询语句(SELECT)对当前所有运行的事务做的一个状态标记,将处于活跃状态的事务号记录下来,以便后续查询记录时不应该看到这些活跃事务对数据造成的影响。在MySQL中是通过与记录关联的事务ID来决定多版本中哪个版本对读视图可见,这样读视图创建的事务状态控制信息就决定了应该读取的记录版本。当需要修改某条记录时,记录其UNDO在回滚段中;当没有事务读取此版本的记录时,可以将UNDO从回滚段中移除。读视图决定了读查询如何从这些版本中读取到的合适版本,以及何时将老版本移除。
稍微详细的讲,除活跃事务信息之外,还必须记录当前已经分配的事务ID,以及便于快速过滤的最小事务ID。当判断一条记录是否可以被当前视图可见时,需根据记录所关联的事务来判断:如果关联事务ID在最小/最大事务ID区间内,则需要判断关联事务ID是否在当前视图不可见的事务列表中。因为事务ID是递增分配,所以这两个最小/最大事务ID的获取是O(1)的复杂度。但获取读视图不可见的事务列表,需要从系统维护的事务列表中遍历判断事务是否符合活跃事务的规则,其实现是O(N)的复杂度。其中,对于系统维护的事务列表中单个事务,实际必须记录的数据非常大,但我们所关心就是最大/最小事务ID。
举个例子说明:
假设有10个事务并发运行,事务ID分别为1~10。此时有个查询语句执行,其看到的当前事务列表为:(1, 3, 4, 5, 8, 9),意味着(2,6,7,10)已经运行完成。读视图看的当前事务列表中,事务(1, 8)不符合事务活跃规则而对当前读视图可见,例如已经在内存中提交后清理资源阶段,但尚未从系统事务列表中摘除。这样活跃的事务只剩下事务(3, 4, 5, 9)对读视图不可见。当前读事务可以看到事务小于等于10的事务,但不包括事务(3, 4, 5, 9)。其中,最小事务ID为1,最大事务ID为10。
到这里,我们看到创建读视图时最关键的一个时间复杂度为O(N),N与当前事务列表长度。除此以外,在此O(N)的逻辑中,还必须做一件事情:确定是否可以被删除的事务ID。
由此,我们再谈谈如何确定可以删除的版本。
视图中决定可以删除的事务ID
我们前面谈到,老版本的记录被删除的规则为:当没有事务读取此版本的记录时,可以将此版本移除。而读视图决定了何时将老版本移除。那么读视图如何决定可以删除记录的哪些版本呢?很简单,获取一个最老的读视图:如果系统有读视图,则获取最老的一个读视图;如果没有读视图,则创建一个新的读视图。这个视图里有个特殊的变量,就是可以被删除的最小事务ID,回滚段中记录关联的事务ID低于此事务ID的所有老版本可以被删除掉。这个事务ID就是当前读视图不可见事务列表中最小事务ID。
接着上面的例子讲:
读视图对事务(3, 4, 5, 9)不可见,这个决定记录是否可以删除的事务ID就是事务3。事务ID低于3的事务,对当前读视图可见,也就是3之前的事务对记录的修改而遗留的老版本都可以删除掉。
高并发下O(N)中N与InnoDB并发控制数的关系
前面提到,构造读视图中遍历系统维护的事务列表,其开销是罪魁祸首。那么,MySQL用户可能会有个疑问:InnoDB并发控制不是有个开关控制吗?典型的现实设置不是16或32吗?那么系统事务列表是不是最大也就16或32?答案肯定不是这样的。InnoDB的并发控制是控制同时进入InnoDB干活的线程个数,但要知道,一条语句每扫描一条记录就要进入InnoDB层一次,当一条语句执行完,只是释放占用的InnoDB层并发数,此时其他语句可以进入InnoDB层。而只有事务提交,才会将事务从系统维护的事务列表中删除。
在高并发场景下,即使是设置非常小的InnoDB并发数,在事务周期内的事务可能大量存在,极端情况下,与当前活跃会话数量相当。比如,在4K并发下,系统事务列表长度可能就会有近4K。
有了上面的基础,我们再回到开始提出的那个Bug。
Bug49169的问题是什么
Bug49169中提到创建读事视图过程的两个性能瓶颈:1)遍历事务列表开销大 2)频繁内存分配/释放。这个问题在可重复读(Repeatable Read)以下的事务隔离级别环境中,尤为明显,因为对于事务中每条读语句,都必须创建一个读视图。
其中,1)就是上面提到的遍历事务列表中获取对当前读事务不可见的所有事务及其最小事务ID。2)的修复思路很简单,预先分配内存再复用地址即可解决。这两个问题在MySQL-5.6之前的版本一直存在。早在官方MySQL推出5.6之前,Percona的Alexey Kopytov修复了这两个性能瓶颈。
Alexey对问题1)修复的思路比较简单, 将遍历系统事务列表而拷贝符合活跃规则的事务优化成对预先维护好的活跃事务列表的内存拷贝,也就是将(N)降为O(1)。但这个做法的额外开销是:1)维护活跃事务列表,以事务ID降序排序。2)维护可以清除的事务列表,以事务ID降序排序。之前的逻辑是整个系统事务在事务创建时按ID递增分配,维护的系统事务列表严格有序,开销就是必须遍历来获取构造视图必要的信息。
MySQL-5.6认为其已经修复了Bug49169,但事实上,这个Bug被关闭是有争议的。MySQL InnoDB的Sunny Brains一直在拆分InnoDB层大锁,以解决目前存在的性能瓶颈。他给出的关闭的理由是5.5的后续版本对读写事务分离,以及5.5中一把全局大锁(kernel_mutex)的细粒度拆分(5.6采用trx_sys->mutex),性能得到较好的优化,所以这个问题没有那么严重。显然,这个Bug的严重程度只是缓解,并没有从根本上解决。
我们就着读写事务分离继续谈下,5.5后续版本的优化思路和引入的新问题。
5.6中对只读事务优化及存在的问题
5.6为了解决Bug49169中提到的问题,对事务进行了区分,拆分为三种事务:1)RO事务((Read-Only, 即start transaction read only开启的事务)放在只读事务链表中;2) AC-NL-RO事务(Auto Commit - Non-Locking - Read-Only), 即autocommit=1的SELECT但不带for update的语句,不放在任何链表中; 3)RW事务(Read-Write),即除1)和2)以外的事务,放在读写事务链表中。
事务被拆分后,因为纯读不会对数据有任何影响,所以构造读事务的事务列表时,只涉及RW事务列表。RO/AC-NL-RO事务场景下,RW事务列表为空,从而O(N)的开销就没有了。但对于RO场景,则是有前提的,即事务必须以start transaction read only开启。
在Bug中提及的在打开视图时(read_view_open_now_low),以线性遍历系统事务ID以构造符合活跃规则的不可见事务列表,此性能问题仍然存在。另外,频繁分配释放读视图内存,这点在5.7后续版本中有修复,思路还是预先分配。
5.7的事务中移除对read only语法的依赖
对于5.6中新引入的start transaction read only语法,存在非常大的争议,因为推动线上运行的业务去修改SQL没有那么想像中的那么容易,而在不更改现有SQL的前提下,显式开启的事务中的SELECT语句被当作RW事务处理!即使去修改SQL以绕开性能问题,但额外网络开销对于高并发场景绝对是行不通的。这样Bug49169提到的两个问题都存在。
InnoDB开发团队也意识到这个问题的严重性,在5.7中开始移除这个强加的新语法的要求。在5.7.2版本中,将自动提交的无锁只读事务(AC-NL-RO)由之前的不加入到任何链表改为增加到RO事务列表中,和RO统一化就不再需要显式以read only开启事务。当检测到该事务要更新记录时,将其从RO事务列表移动到RW事务列表中,此过程需要锁来保护(trx_sys->mutex)。而这个设计又引入新的性能瓶颈:事务频繁在RO和RW事务直接迁移导致对事务锁的开销!在5.7.3中的一个重点,就是消除新引入的额外锁开销问题。在此版本中,事务默认不会放在任何链表中,只有当被检测到更新时才将其放到RW事务列表中。理论上讲,其开销和AC-NL-RO相当。
总结
回顾下这个优化过程。回到5.0/5.1版本中,Bug49169指出读视图性能瓶颈。这个问题在官方5.5中一直没有太大进展,而Percona的Alexey最直接的去优化此问题。但很明显,官方想优化更彻底。5.6版本中,读写事务分离,因为无法区分事务中是否会有读写,必须让用户显示指定。但这实在是一个中间方案,5.7.2中开始取消这个限制,事务默认加到RO事务列表中。事务中有更新怎么办?动态迁移到RW事务列表中。好,问题解决了!但后来想一想,这个还是明显有优化空间:频繁的事务迁移需要频繁的加解锁。5.6整出让用户区分RO/AC-NL-RO的初衷不就是为了不想放入任何链表中吗?索性事务开始都不加到任何事务列表中,当检测到事务有更新再加入到RW事务列表中!这样对于纯读场景下,O(N)被优化成O(0)。
到这里,Bug49169可以说是被修复了。虽然在读写混合场景下,可能还是存在些性能隐患,但在MySQL的良好社区生态系统下,一批批杰出的工程师正在致力于进一步优化,相信不久后,会有新的优化思路和实现。同时,我们也会将这些优化引入到集团内部MySQL版本中。
--------------------------------------------------
***“码小农” 第二期之InnoDB读事务优化
***“码小农”第二期之无插件Vim变成技巧
***“码小农”第二期之Java8学习笔记----Lambda表达式
-------------------------------------------------
如果在阅读过程中仍有问题没能弄明白,欢迎加入“阿里高校技术联盟”的【来往-扎堆】,在其中与文章作者做更进一步的交流,扎堆二维码:
这是一条镜像帖。来源:北邮人论坛 / bupt-auta / #57同步于 2014/4/11
该镜像源已超过 30 天没有更新,可能在源站已被删除。
buptAUTA机器人发帖
“码小农” 第二期——之InnoDB读事务优化
sjlee
2014/4/11镜像同步2 回复
订阅后,新回复会通过你的通知中心匿名送达。