from: http://mysql.taobao.org/index.php/MySQL%E5%86%85%E6%A0%B8%E6%9C%88%E6%8A%A5_2014.08
目录[隐藏]
|
提要
MySQL 5.5.39 Release版本正式从源码里删除了全局参数timed_mutexes。timed_mutexes原本用来控制是否对Innodb引擎的mutex wait进行计时统计,以方便进行性能诊断。为什么要删除这个参数呢? 下面介绍下相关背景:
Innodb的同步锁机制
Innodb封装了mutex和rw_lock结构来保护内存的变量和结构,进行多线程同步,考虑可移植性, mutex使用lock_word或者OS mutex来保证原子操作,并使用event条件变量进行阻塞和唤醒操作。
os_event_t event; volatile lock_word_t lock_word; os_fast_mutex_t os_fast_mutex;
Innodb同步锁引入的数据结构和开销
1. 全局mutex链表
Innodb引入了一个全局的链表ut_list_base_node_t mutex_list,并使用一个单独的mutex来保护链表。 所有的mutex在create或者free的时候来修改链表,有了全局链表,也使统计汇总有了可能性,参考命令“show engine innodb mutex”. 虽然需要维护一个全局的链表,但这并不会影响太多的性能,因为大部分的mutex的生命周期都是从Innodb启动一直到shutdown。
2. 统计信息
mutex的结构中,有几个统计信息:
count_os_wait:请求mutex进入等待的次数 count_using: 请求mutex的次数 count_spin_loop: 请求mutex时spin的轮数 count_spin_rounds: 请求mutex的spin次数 count_os_yield:请求mutex spin失败后os等待次数 lspent_time: 统计等待mutex的时间
lock mutex的主要步骤:
1. 首先trylock mutex,如果没有获取到mutex,并不马上进行wait,而是进行spin。 2. 尝试spin,如果在SYNC_SPIN_ROUNDS次后,仍然没有lock,那么就进入等待队列,等待唤醒。
在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的等待事件来代替,即:
mysql> SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES -> WHERE TABLE_SCHEMA = 'performance_schema' -> AND TABLE_NAME LIKE '%instances'; +------------------+ | TABLE_NAME | +------------------+ | cond_instances | | file_instances | | mutex_instances | | rwlock_instances | +------------------+
3. 全局等待队列
Innodb为所有的等待状态的线程准备了一个队列,如果获取mutex失败,那么就申请一个cell,进入阻塞状态,等待signal。 sync_primary_wait_array,有了这个全局的队列,Innodb就可以对这些wait的线程进行统计,比如long semaphore waits就是根据这个队列进行的查询。
4. signal丢失
这里再讨论下signal丢失的情况,我们重新再看下lock mutex的步骤:
线程1: 1. try lock mutex 2. fail,然后spin 3. fail,然后进入队列,然后wait event 线程2: 1.own mutex 2.free mutex 3.signal event
如果按照这个时序,在线程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的等待事件进行替代,所以参数做了删除处理。
背景
innodb_flush_log_at_trx_commit 这个参数可以说是InnoDB里面最重要的参数之一,它控制了重做日志(redo log)的写盘和落盘策略。 具体的参数意义见手册
简单说来,可选值的安全性从0->2->1递增,分别对应于mysqld 进程crash可能丢失 -> OS crash可能丢失 -> 事务安全。
以上是路人皆知的故事,并且似乎板上钉钉,无可八卦。
innodb_use_global_flush_log_at_trx_commit
直到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 为默认值。
业务应用
这个特性可以用在一些对表的重要性做等级定义的场景。比如同一个实例下,某些表数据有外部数据备份,或允许丢失部分事务的情况,对这些表的更新,可以设置 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。
drop table if exists tb; set tmp_table_size=1024; create table tb(id int auto_increment primary key, v varchar(32)) charset=gbk; insert into tb(v) values("aaa"); insert into tb(v) (select v from tb); insert into tb(v) (select v from tb); insert into tb(v) (select v from tb); insert into tb(v) (select v from tb); insert into tb(v) (select v from tb); insert into tb(v) (select v from tb); insert into tb(v) (select v from tb); insert into tb(v) (select v from tb); update tb set v=concat(v, id); select count(distinct v) from tb; 返回0 上述中update语句的目的是将所有的v值设为各不相同。
原因分析
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。
案例分析
以上面这个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,放弃合并。
Sql_big_tables
实际上在最初处理这个问题时,DBA同学发现社区也有人讨论这个bug,并且指出在set sql_big_tables=on的时候,执行count(distinct)就能正确返回结果。原因就是在sql_big_tables=on的情况下,构造集合的方式是直接生成一个临时表,全部插入后直接计算临时表的大小作为结果,整个过程与tmp_table_size无关。
解决方法
运维上,set sql_big_tables是一个方法,不过会影响性能。调高tmp_table_size算是正招。当然本质上这是一个bug。 代码上,对于已经走到合并操作的这个逻辑,如果tmp_table_size不够,应该直接申请新的临时空间用于合并,完成后释放。虽然会造成临时征用内存,不过以现有的逻辑来看,临时征用的内存已经不少了.
另外一种时间换空间的方法,就是作多次合并。
相比之下第一种改造比较简单安全。该bug在RDS MySQL 5.5 中已经修复。
bug背景
在上个月发布的新版本中,官方修复了一个mysqldump输入库名或表明长度越界的bug。
在MySQL的当前约束中,库名和表名字符串最大长度为NAME_LEN=192字节。在myqldump实现中,需要对输入的表名做处理,比如增加``防止表名中的特殊字符。这些临时处理的内存,声明为类似name_buff[NAME_LEN+3],这样在用户输入的库名或表名长度过长时,会造成数组越界读写,导致不可预期的错误。
这个修复的逻辑也比较简单,就是在开始dump前作参数检查,若发现长度超过NAME_LEN的库/表名,直接抛错返回“argument too long”。
细节说明
需要注意的是,该修复改变了mysqldump的行为。由于名字长度超过NAME_LEN的库/表肯定不存在,因此修复之前的逻辑,是报告该表不存在。“table not exists”这个逻辑是可以通过--force 跳过的。而“argument too long”则无视force参数,直接抛错返回。
现象描述:
Innodb引擎,父表和子表通过foreign constraint进行关联,因为在更新数据时需要check外键constraint,当父表被大量的子表referenced时候,那么在open Innodb数据字典的时候,需要open所有的child table和所有的foreign constraint,导致持有dict_sys->mutex时间过长,产生long semaphore wait, 然后innodb crash了。
case复现
CREATE TABLE `t1` ( `f1` int(11) NOT NULL, PRIMARY KEY (`f1`) ) ENGINE=InnoDB
CREATE TABLE `fk_1` ( `f1` int(11) NOT NULL, PRIMARY KEY (`f1`), CONSTRAINT `pc1` FOREIGN KEY (`f1`) REFERENCES `t1` (`f1`) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINE=InnoDB ...... 这里建了fk_[0-10000]张表。
分析过程
1. 数据字典
innodb使用系统表空间保存表相关的数据字典,系统的数据字典包括:
SYS_TABLES SYS_INDEXES SYS_COLUMNS SYS_FIELDS SYS_FOREIGN SYS_FOREIGN_COLS SYS_STATS
在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。
2. open过程
dict_load_table:
1. 通过sys_tables系统表,load table相关的定义 2. 通过sys_indexes系统表,根据table_id load 所有相关index 3. 通过sys_columns系统表,根据table_id load 所有的columns 4. 通过sys_fields系统表,根据index_id load 所有index的field 5. 通过sys_foreign系统表,load所有关联的表和foreign key
3. load foreign的详细过程
3.1 根据表名t1 查找sys_foreign.
而sys_foreign表上一共有三个索引:
index_1(id): cluster_index index_2(for_name): secondary_index index_3(ref_name): secondary_index
所以,根据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对象,引用已经存在的。
for_name->foreign_list for_name->referenced_list ref_name->foreign_list ref_name->referenced_list
如果不存在,把新建的foreign加入到for_name->foreign_list,ref_name->referenced_list链表中。
4. 问题的原因:
因为第一次load,所以find都没有找到,但这四个都是list,随着open的越来越多,检索的代价越来越大。 而整个过程中,都一直持有trx_sys->mutex,最终导致了long semaphore wait。
5. 问题改进方法:
在MySQL 5.5.39版本中,进行了修复,修复的方法就是,除了foreign_list,referenced_list。 另外又增加了两个red_black tree,如下源码所示:
struct dict_table_struct{ table_id_t id; /*!< id of the table */ mem_heap_t* heap; /*!< memory heap */ char* name; /*!< table name */ UT_LIST_BASE_NODE_T(dict_foreign_t) foreign_list; /*!< list of foreign key constraints in the table; these refer to columns in other tables */ UT_LIST_BASE_NODE_T(dict_foreign_t) referenced_list;/*!< list of foreign key constraints which refer to this table */ ib_rbt_t* foreign_rbt; /*!< a rb-tree of all foreign keys listed in foreign_list, sorted by foreign->id */ ib_rbt_t* referenced_rbt; /*!< a rb-tree of all foreign keys listed in referenced_list, sorted by foreign->id */ }
这样dict_foreign_find的过程中,通过red_black tree进行检索,时间复杂度降到O(log n).
背景
最近发布的MariaDB 10.1 Alpha版本,提交了一个改动,放宽了InnoDB Page<=16K的限制,将上限提高到64K。 从MDEV-6075需求文档中可以看出,目前只支持COMPACT的结构,DYNAMIC结构能否支持还在研究,COMPRESSED结构则确定无法支持。
revno: 3987 committer: Jan Lindström <[email protected]> branch nick: 10.1 timestamp: Tue 2014-05-13 13:28:57 +0300 message: MDEV-6075: Allow > 16K pages on InnoDB This patch allows up to 64K pages for tables with DYNAMIC, COMPACT and REDUNDANT row types. Tables with COMPRESSED row type allows still only <= 16K page size. Note that single row size must be still <= 16K and max key length is not affected.
业务应用
什么情况下需要64K这么大的页面呢? 我们知道一个Page,不是所有的page_size都可以用来存数据,还有一些管理信息要存,例如页头和页尾(InnoDB Page)。 此外,InnoDB Buffer Pool管理页面本身也有代价,Page数越多,那么相同大小下,管理链表就越长。
因此当我们的数据行本身就比较长,尤其是做大块插入的时候,更大的页面更有利于提升如速度,因为一个页面可以放入更多的行,每个IO写下去的大小更大,就可以以更少的IOPS写更多的数据。 而且,当行长超过8K的时候,如果是16K的页面,就会强制转换一些字符串类型为TEXT,把字符串主体转移到扩展页中,会导致读取列需要多一个IO,更大的页面也就支持了更大的行长,64K页面可以支持近似32K的行长而不用使用扩展页。 但是,如果是短小行长的随机读取和写入,则不适合使用这么大的页面,这会导致IO效率下降,大IO只能读取到小部分有效数据,得不偿失。
背景
随着存储设备越来越快,InnoDB许多原有的设计不再适合新的高速硬件,因此MariaDB 10.1 Alpha版本针对FusionIO PCI-E SSD做出了专门的优化,充分利用了Fio的硬件特性。 MDEV-6246这个需求改造了MariaDB,以利用fio的Atomic writes和文件系统压缩特性。
revno: 3988 [merge] committer: Jan Lindström <[email protected]> branch nick: 10.1 timestamp: Thu 2014-05-22 14:24:00 +0300 message: MDEV-6246: Merge 10.0.10-FusionIO to 10.1.
为何Fio会更快呢,因为传统的存储设备读取,是左图的方式,要经过北桥芯片,来回的路径就长了。而Fio才有右图的方式,设备通过PCI槽直接与CPU交互,大大缩短了路径。
Atomic writes
InnoDB一直存在一个叫做Double Write Buffer的东西,目的就是为了防止页面写到一半系统崩溃,导致页面损坏,因为InnoDB的Page是16K,而一般的机械硬盘扇区是512字节,SSD大都是4K的块大小,都不能保证16K的写入是完整的。 而Fio的NVMFS文件系统则提供了原子写的保证,只要对文件句柄增加DFS_IOCTL_ATOMIC_WRITE_SET的ioctl标记位,就可以启用这个文件的原子写支持。
ioctl(file, DFS_IOCTL_ATOMIC_WRITE_SET, &atomic_option)
MariaDB新增了一个参数来启用这个特性,一旦开启,所有文件会用DFS_IOCTL_ATOMIC_WRITE_SET标记打开。
innodb_use_atomic_writes = 1
这样一来Double Write Buffer就没有存在的价值了,因为不会出现部分写,每个write下去都可以保证所写内容全部完成,这可以相当程度上提升InnoDB的性能。
Page compression
InnoDB标准的页面大小是16K,InnoDB也提供1K、2K、4K、8K的压缩页面大小,通过KEY_BLOCK_SIZE来设置压缩大小,使用zlib标准库来进行压缩。 但是Page是频繁被更新的,如果每次修改都重新压缩页面,代价很高,InnoDB就采用了modification log来暂存部分修改信息,而避免了频繁解压缩,待modification log存满时,再重新对整个Page做一次重构压缩。 但是Compressed Page载入InnoDB Buffer Pool时,InnoDB只能处理未压缩的页面,因此还要在内存中存一份解压页面,回写到磁盘时再次压缩。
总而言之,InnoDB的Compressed Page有这些缺点:
内存开销 空间: 压缩和解压缩页面都要存在InnoDB Buffer Pool 访问: 修改需要同时写入到压缩页面和未压缩页面 CPU开销 软件压缩库zlib (从磁盘读取时需要解压缩放入内存, 页面分裂时需要重新压缩) Split & Recompress & Rebalance when mlog overflows 空间收益 固定的页面压缩大小 – 给压缩的效果设置了一个固定的边界 Modification log和Page预留空间弱化了压缩带来的空间缩减 糟糕的实现 代码过于复杂而导致压缩和未压缩的表性能差距非常明显
MariaDB与FusionIO合作利用NVMFS文件系统的特性,修改InnoDB的Page结构来支持文件系统级的压缩。 Page compression要求InnoDB做了如下配置:
innodb_file_per_table = 1 innodb_file_format = Barracuda.
它的实现方法是,只在Page即将写入到文件系统时,才进行压缩,因此最终只有压缩后的容量被写入到磁盘,如果压缩失败,那么就把没有压缩的容量写入磁盘。另外还会对Page内的512字节的倍数的未使用空间清理掉,不占用实际存储:
fallocate(file, FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE, off, trim_len);
当页面被读取时,会在放入Buffer Pool之前进行解压缩,将原始页面载入内存。因此需要在文件头中加入一个新的Page type:FIL_PAGE_PAGE_COMPRESSED
综合起来可以这样定义一张表:
CREATE TABLE t3 (a int KEY, b int) DATA DIRECTORY=’/dev/fioa’ PAGE_COMPRESSED=1 PAGE_COMPRESSION_LEVEL=4 ATOMIC_WRITES=’ON’;
意思是将t3表存到/dev/fioa盘,开启Page compression,采用4级压缩,开启原子写。
经过测试,可以看出,LZ4的压缩比例最好,而且,对性能影响非常小。
Bulk Fetch是为了提升区间操作性能的,聊它之前,先简单唠叨下读取机制,TokuDB由两部分组成: tokuFT和 tokudb-engine 。
tokuFT是个支持事务的key/value存储层,tokudb-engine是MySQL API对接层,调用关系为:tokudb-engine ->tokuFT。
tokuFT里的一个value,在tokudb-engine里就是一条row数据,底层存储与上层调用解耦,是个很棒的设计。
在tokuFT是个key里,索引的每个node都是大块头(4MB),node又细分为多个"小块"(internal node的叫做partition,leaf node的叫做basement)。
从磁盘读取数据到内存的方式有2种:
对于tokudb-engine层的区间操作(比如get_next等),tokuFT这层是无状态的,必须告诉当前的key,然后给你查找next,流程大体是:
tokudb-engine::get_next(current_key) --> tokuFT::search_next(current_key) --> tokuFT::return next
这样,即使tokuFT缓存了整个node数据,tokudb-engine还是遍历着跟tokuFT要一遍:tokuFT每次都要根据当前key,多次调用compare操作最终查出next,路径太长了!
有什么办法优化呢?这就是Bulk Fetch的威力: tokudb-engine向tokuFT一次要回整个node的数据,自己解析出next row数据,tokuFT的调用就省了:
tokudb-engine::get_next(current_key) --> tokudb-engine::parse_next
从Tokutek的测试看,在使用Bulk Fetch后,能有2x-5x的性能提升。
但并不是所有的区间操作都可以Bulk Fetch的(比如涉及update/delete),TokuDB目前实现了:SELECT、CREATE_TABLE、INSERT_SELECT和REPLACE_SELECT的Bulk Fetch功能,预计发布在7.1.8版,更多Bulk Fetch介绍:
https://github.com/Tokutek/tokudb-engine/wiki/Bulk-Fetch
最近,TokuDB的创始人Dr. Bradley Kuzmaul发表了一篇文章: A Comparison of Log-Structured Merge (LSM) and Fractal Tree Indexing,从write amplification(WAMP), read amplification(RAMP), and space amplification三个方面对B-Trees,LSM-Trees(LSM)以及Fractal-Trees(FT)进行了详细的分析和对比。
从结果来看:
在WAMP上,FT跟LSM(leveled)是相同的 在RAMP(range)上,LSM(leveled)的复杂度明显要高不少(FT的O(logN/B)倍)
不过,RAMP这块的分析有个小问题:
LSM(leveled)在实现上(比如LevelDB),可以通过meta-info打"锚点"的方式,把RAMP(range)降低甚至做到跟FT一样,如果是point queries的RAMP,则可以通过Bloom filter来降低。
具体的推导过程请阅读原作,下面简单分析下FT的RAMP为啥比LSM的要低。
FT的读方式比较"特殊",由于每个节点都有个message buffer,当有读请求时,需要把inner node的message buffer数据(部分)推(apply)到leaf node,最后只在leaf node上做二分查找,所以RAMP基本就是树的高度。
另外,在数据流向上(compaction过程中数据走向),LSM强调"level"(横向),从level-L根据规则选取部分数据merge到level-(L+1),如果选取数据的策略不好,会抢占磁盘带宽,容易引起性能抖动,而FT强调"root-to-leaf"(纵向),数据从root有序的逐层merge到leaf节点,每条数据的merge路径是很明确的。
第一期先介绍下TokuDB团队吧。
TokuDB自从开源后(更赞的是开源了所有的commits),逐渐被大家所熟悉,MariaDB 5.5系列和Percona Server 5.6的GA版本中,都以plugin的方式集成。
3位(Tokutek)创始人: Michael A. Bender , Martín Farach-Colton , Bradley C. Kuszmaul
2012年他们合发了一篇208页的pdf[Data Structures and Algorithms for Big Databases],热爱存储引擎算法的朋友们一定要看 :D
TokuDB目前有5名研发:
@prohaska --tokudb-engine研发,版本发布(一个人) @Leif --tokuFT研发(Bender学生) @zkasheff --tokuFT研发(Kuszmaul学生) @esmet --tokuFT研发(Farach学生) @fizzfaldt --算法优化(Bender学生)
可以说是个很"精致"团队,是研发也是测试,tokuFT的测试代码达~18w行(而tokuFT的核心代码才~9w行),在代码把控上也很严格,要求所有代码在valgrind(helgrind和drd)下,没有memory、data race(资源竞争)和lock order(死锁)警告,质量很有保障。(BTW:我们也在借鉴和运用)
同时@Leif和@zkasheff也是tokuMX的研发,不久前,他俩就MongoDB Replication可能"丢数据"的问题,写了篇[Ark: A Real-World Consensus Implementation],对Paxos和Raft感兴趣的同学可以去看下。
更有意思的是,当工程中遇到难题,研发们先讨论出方案,递给三位创始人,得到更权威的指导,如果问题不错,他们就深入研究,出paper,工程与学术相结合。