<b>提要</b>
mysql 5.5.39 release版本正式从源码里删除了全局参数timed_mutexes。timed_mutexes原本用来控制是否对innodb引擎的mutex wait进行计时统计,以方便进行性能诊断。为什么要删除这个参数呢? 下面介绍下相关背景:
<b>innodb的同步锁机制</b>
innodb封装了mutex和rw_lock结构来保护内存的变量和结构,进行多线程同步,考虑可移植性, mutex使用lock_word或者os mutex来保证原子操作,并使用event条件变量进行阻塞和唤醒操作。
<b>innodb同步锁引入的数据结构和开销</b>
<b>1. 全局mutex链表</b>
innodb引入了一个全局的链表ut_list_base_node_t mutex_list,并使用一个单独的mutex来保护链表。 所有的mutex在create或者free的时候来修改链表,有了全局链表,也使统计汇总有了可能性,参考命令“show engine innodb mutex”. 虽然需要维护一个全局的链表,但这并不会影响太多的性能,因为大部分的mutex的生命周期都是从innodb启动一直到shutdown。
<b>2. 统计信息</b>
mutex的结构中,有几个统计信息:
lock mutex的主要步骤:
在mysql5.5的版本里,非univ_debug模式下,innodb仅仅保留了count_os_wait的次数,这也是为了性能的考虑。所以5.5的版本后, timed_mutexes在release下,其实已经不再起作用,所以5.5.39,以及5.6以后,源码里都不再保留timed_mutexes。 要么在debug模式下,启用这些统计,但上线版本又不可能使用debug模式,所以对于mutex的统计,mysql在后面的版本中使用了performance_schema的等待事件来代替,即:
<b>3. 全局等待队列</b>
innodb为所有的等待状态的线程准备了一个队列,如果获取mutex失败,那么就申请一个cell,进入阻塞状态,等待signal。 sync_primary_wait_array,有了这个全局的队列,innodb就可以对这些wait的线程进行统计,比如long semaphore waits就是根据这个队列进行的查询。
<b>4. signal丢失</b>
这里再讨论下signal丢失的情况,我们重新再看下lock mutex的步骤:
如果按照这个时序,在线程2 signal event后,线程1才进入队列,那么线程1就永远处在阻塞状态,无法唤醒。为了解决signal丢失的情况, innodb启动了一个后台线程:sync_arr_wake_threads_if_sema_free,每隔1s就轮询wait数组,如果可以lock,就signal这个event来唤醒线程。
从上面来看,innodb为了mutex和rwlock的移植性,以及为了监控和诊断,添加了多个全局的数据结构,这样实时的统计才有可能,但也带来了维护数据结构的开销。 而timed_mutexes控制的mutex wait时间统计,因为只在debug模式下进行编译,而且5.6以后使用performance schema的等待事件进行替代,所以参数做了删除处理。
<b>背景</b>
简单说来,可选值的安全性从0->2->1递增,分别对应于mysqld 进程crash可能丢失 -> os crash可能丢失 -> 事务安全。
以上是路人皆知的故事,并且似乎板上钉钉,无可八卦。
<b>innodb_use_global_flush_log_at_trx_commit</b>
直到2010年的某一天,percona的cto vadim同学觉得这种一刀切的风格不够灵活,最好把这个变量设置成session级别,每个session自己控制。
但同时为了保持super权限对提交行为的控制,同时增加了innodb_use_global_flush_log_at_trx_commit参数。 这两个参数的配合逻辑为:
1、若innodb_use_global_flush_log_at_trx_commit为off,则使用session.innodb_flush_log_at_trx_commit;
2、若innodb_use_global_flush_log_at_trx_commit为on,则使用global .innodb_flush_log_at_trx_commit(此时session中仍能设置,但无效)
3、每个session新建时,以当前的global.innodb_flush_log_at_trx_commit 为默认值。
<b>业务应用</b>
这个特性可以用在一些对表的重要性做等级定义的场景。比如同一个实例下,某些表数据有外部数据备份,或允许丢失部分事务的情况,对这些表的更新,可以设置 session.innodb_flush_log_at_trx_commit为非1值。
在阿里云rds服务中,我们对数据可靠性和可用性要求更高,将 innodb_use_global_flush_log_at_trx_commit设置为on,因此修改session.innodb_flush_log_at_trx_commit也没有作用,统一使用 global.innodb_flush_log_at_trx_commit = 1。
mysql现行版本中存在一个count(distinct)语句返回结果错误的bug,表现为,实际结果存在值,但是用count(distinct)统计后返回的是0。
<b>原因分析</b>
count(distinct f)的语义就是计算字段f的去重总数,计算流程大致如下:
流程一:
1、 构造一个unique集合a1(用tree实现) 2、 对每个值都试图插入集合a1中 3、 若和a1中现有item重复则直接跳过,不重复则插入并+1 4、 完成后计算集合中元素个数。
细心的同学会看到上面的语句中有一个set tmp_table_size的过程,集合a1并不能无限扩大,大小上限为tmp_table_size。若超过则上述流程变为
流程二:
1、 构造一个unique 集合a1 2、 插入item过程中若大小超过tmp_table_size,则将a1暂时写到文件中,再构造集合a2 3、 重复步骤2直到所有的item插入完成 因此若item很多则可能重复生成多个集合a1~an。 4、 对a1~an作合并操作。由于只是每个集合a保证unique,因此需要做类似归并排序的操作(实际上不需要排序,只是扫一遍) 5、 因此合并操作需要一个临时内存,长度为n,单元大小为key_length (key大小)。这个临时内存,用的也是tmp_table_size定义的大小。实际上在合并过程中还需要长为key_length的预留空间作临时内存保存。因此需要的空间为 (n+1)*key_length。 6、 在进行合并前会判断tmp_table_size >=(n+1)*key_length, 不满足则直接放弃合并。其结果就是返回为0。
<b>案例分析</b>
以上面这个case为例。字段v的单key大小为65 (65 = 32*2+1) 加上tree节点字占空间24字节共89字节。单个集合只能放11个item (1024/89), 因此n为 24 (24>=256/11), 在合并时需要 (24+1)*65= 1625字节的临时空间,大于1024,放弃合并。
<b>sql_big_tables</b>
实际上在最初处理这个问题时,dba同学发现社区也有人讨论这个bug,并且指出在set sql_big_tables=on的时候,执行count(distinct)就能正确返回结果。原因就是在sql_big_tables=on的情况下,构造集合的方式是直接生成一个临时表,全部插入后直接计算临时表的大小作为结果,整个过程与tmp_table_size无关。
<b>解决方法</b>
运维上,set sql_big_tables是一个方法,不过会影响性能。调高tmp_table_size算是正招。当然本质上这是一个bug。 代码上,对于已经走到合并操作的这个逻辑,如果tmp_table_size不够,应该直接申请新的临时空间用于合并,完成后释放。虽然会造成临时征用内存,不过以现有的逻辑来看,临时征用的内存已经不少了.
另外一种时间换空间的方法,就是作多次合并。
相比之下第一种改造比较简单安全。该bug在rds mysql 5.5 中已经修复。
<b>bug背景</b>
在上个月发布的新版本中,官方修复了一个mysqldump输入库名或表明长度越界的bug。
在mysql的当前约束中,库名和表名字符串最大长度为name_len=192字节。在myqldump实现中,需要对输入的表名做处理,比如增加``防止表名中的特殊字符。这些临时处理的内存,声明为类似name_buff[name_len+3],这样在用户输入的库名或表名长度过长时,会造成数组越界读写,导致不可预期的错误。
这个修复的逻辑也比较简单,就是在开始dump前作参数检查,若发现长度超过name_len的库/表名,直接抛错返回“argument too long”。
<b>细节说明</b>
需要注意的是,该修复改变了mysqldump的行为。由于名字长度超过name_len的库/表肯定不存在,因此修复之前的逻辑,是报告该表不存在。“table not exists”这个逻辑是可以通过--force 跳过的。而“argument too long”则无视force参数,直接抛错返回。
<b>现象描述:</b>
innodb引擎,父表和子表通过foreign constraint进行关联,因为在更新数据时需要check外键constraint,当父表被大量的子表referenced时候,那么在open innodb数据字典的时候,需要open所有的child table和所有的foreign constraint,导致持有dict_sys->mutex时间过长,产生long semaphore wait, 然后innodb crash了。
<b>case复现</b>
<b>分析过程</b>
<b>1. 数据字典</b>
innodb使用系统表空间保存表相关的数据字典,系统的数据字典包括:
在load某个表的时候,分别从这些表中把表相关的index,column, index_field, foreign, foreign_col数据保存到dictionary cache中。 对应的内存对象分别是:dict_col_struct,dict_field_struct,dict_index_struct,dict_table_struct,dict_foreign_struct。
<b>2. open过程</b>
dict_load_table:
<b>3. load foreign的详细过程</b>
3.1 根据表名t1 查找sys_foreign.
而sys_foreign表上一共有三个索引:
所以,根据for_name='t1', ref_name='t1'检索出来所有相关的foreign_id.
3.2 加入cache
因为没有专门的cache,foreign分别加入到for_name->foreign_list, ref_name->referenced_list。 问题的关键:因为foreign是全局唯一的,但foreign又与两个表关联,所以,有可能在open 其它表的时候已经打开过,所以,create foreign对象后,需要判断以下四个list,是否已经存在,如果存在就直接使用。
dict_foreign_find:分别查询这四个list,如果已经存在,则free新建的foreign对象,引用已经存在的。
如果不存在,把新建的foreign加入到for_name->foreign_list,ref_name->referenced_list链表中。
<b>4. 问题的原因:</b>
因为第一次load,所以find都没有找到,但这四个都是list,随着open的越来越多,检索的代价越来越大。 而整个过程中,都一直持有trx_sys->mutex,最终导致了long semaphore wait。
<b>5. 问题改进方法:</b>
在mysql 5.5.39版本中,进行了修复,修复的方法就是,除了foreign_list,referenced_list。 另外又增加了两个red_black tree,如下源码所示:
这样dict_foreign_find的过程中,通过red_black tree进行检索,时间复杂度降到o(log n).