淘宝内部分享:MySQL & MariaDB性能优化 - Go语言中文社区

淘宝内部分享:MySQL & MariaDB性能优化


allowtransparency="true" frameborder="0" scrolling="no" src="http://hits.sinajs.cn/A1/weiboshare.html?url=http%3A%2F%2Fwww.csdn.net%2Farticle%2F2015-01-20%2F2823634%3Freload%3D1&type=3&count=&appkey=&title=MySQL%E6%98%AF%E7%9B%AE%E5%89%8D%E4%BD%BF%E7%94%A8%E6%9C%80%E5%A4%9A%E7%9A%84%E5%BC%80%E6%BA%90%E6%95%B0%E6%8D%AE%E5%BA%93%EF%BC%8C%E4%BD%86%E6%98%AFMySQL%E6%95%B0%E6%8D%AE%E5%BA%93%E7%9A%84%E9%BB%98%E8%AE%A4%E8%AE%BE%E7%BD%AE%E6%80%A7%E8%83%BD%E9%9D%9E%E5%B8%B8%E7%9A%84%E5%B7%AE%EF%BC%8C%E5%BF%85%E9%A1%BB%E8%BF%9B%E8%A1%8C%E4%B8%8D%E6%96%AD%E7%9A%84%E4%BC%98%E5%8C%96%EF%BC%8C%E8%80%8C%E4%BC%98%E5%8C%96%E6%98%AF%E4%B8%80%E4%B8%AA%E5%A4%8D%E6%9D%82%E7%9A%84%E4%BB%BB%E5%8A%A1%EF%BC%8C%E6%9C%AC%E6%96%87%E6%8F%8F%E8%BF%B0%E6%B7%98%E5%AE%9D%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9B%A2%E9%98%9F%E9%92%88%E5%AF%B9MySQL%E7%9B%B8%E5%85%B3%E7%9A%84%E6%95%B0%E6%8D%AE%E5%BA%93%E4%BC%98%E5%8C%96%E6%96%B9%E6%A1%88%E3%80%82&pic=&ralateUid=&language=zh_cn&rnd=1421746722134" width="22" height="16"> 摘要:MySQL是目前使用最多的开源数据库,但是MySQL数据库的默认设置性能非常的差,必须进行不断的优化,而优化是一个复杂的任务,本文描述淘宝数据库团队针对MySQL相关的数据库优化方案。

编者按:MySQL是目前使用最多的开源数据库,但是MySQL数据库的默认设置性能非常的差,必须进行不断的优化,而优化是一个复杂的任务,本文描述淘宝数据库团队针对MySQL数据库Metadata Lock子系统的优化,hash_scan 算法的实现解析的性能优化,TokuDB·版本优化,以及MariaDB·的性能优化。本文来自淘宝团队内部经验分享。

往期文章:淘宝内部分享:怎么跳出MySQL的10个大坑


MySQL· 5.7优化·Metadata Lock子系统的优化

背景

引入MDL锁的目的,最初是为了解决著名的bug#989,在MySQL 5.1及之前的版本,事务执行过程中并不维护涉及到的所有表的Metatdata 锁,极易出现复制中断,例如如下执行序列:

Session 1: BEGIN;
Session 1: INSERT INTO t1 VALUES (1);
Session 2: Drop table t1; --------SQL写入BINLOG
Session 1: COMMIT; -----事务写入BINLOG

在备库重放 binlog时,会先执行DROP TABLE,再INSERT数据,从而导致复制中断。

在MySQL 5.5版本里,引入了MDL, 在事务过程中涉及到的所有表的MDL锁,直到事务结束才释放。这意味着上述序列的DROP TABLE 操作将被Session 1阻塞住直到其提交。

不过用过5.5的人都知道,MDL实在是个让人讨厌的东西,相信不少人肯定遇到过在使用mysqldump做逻辑备份时,由于需要执行FLUSH TABLES WITH READ LOCK (以下用FTWRL缩写代替)来获取全局GLOBAL的MDL锁,因此经常可以看到“wait for global read lock”之类的信息。如果备库存在大查询,或者复制线程正在执行比较漫长的DDL,并且FTWRL被block住,那么随后的QUERY都会被block住,导致业务不可用引发故障。

为了解决这个问题,Facebook为MySQL增加新的接口替换掉FTWRL 只创建一个read view ,并返回与read view一致的binlog位点;另外Percona Server也实现了一种类似的办法来绕过FTWRL,具体点击文档连接以及percona的博客,不展开阐述。

MDL解决了bug#989,却引入了一个新的热点,所有的MDL锁对象被维护在一个hash对象中;对于热点,最正常的想法当然是对其进行分区来分散热点,不过这也是Facebook的大神Mark Callaghan在report了bug#66473后才加入的,当时Mark观察到MDL_map::mutex的锁竞争非常高,进而推动官方改变。因此在MySQL 5.6.8及之后的版本中,引入了新参数metadata_locks_hash_instances来控制对mdl hash的分区数(Rev:4350);

不过故事还没结束,后面的测试又发现哈希函数有问题,somedb. someprefix1 … .somedb .someprefix8 的hash key值相同,都被hash到同一个桶下面了,相当于hash分区没生效。这属于hash算法的问题,喜欢考古的同学可以阅读下bug#66473后面Dmitry Lenev的分析。

Mark进一步的测试发现Innodb的hash计算算法比my_hash_sort_bin要更高效, Oracle的开发人员重开了个bug#68487来跟踪该问题,并在MySQL5.6.15对hash key计算函数进行优化,包括fix 上面说的hash计算问题(Rev:5459),使用MurmurHash3算法来计算mdl key的hash值。

MySQL 5.7 对MDL锁的优化

在MySQL 5.7里对MDL子系统做了更为彻底的优化。主要从以下几点出发:

第一,尽管对MDL HASH进行了分区,但由于是以表名+库名的方式作为key值进行分区,如果查询或者DML都集中在同一张表上,就会hash到相同的分区,引起明显的MDL HASH上的锁竞争。

针对这一点,引入了LOCK-FREE的HASH来存储MDL_lock,LF_HASH无锁算法基于论文"Split-Ordered Lists: Lock-Free Extensible Hash Tables",实现还比较复杂。 注:实际上LF_HASH很早就被应用于Performance Schema,算是比较成熟的代码模块。由于引入了LF_HASH,MDL HASH分区特性自然直接被废除了 。对应WL#7305, PATCH(Rev:7249)

第二,从广泛使用的实际场景来看,DML/SELECT相比DDL等高级别MDL锁类型,是更为普遍的,因此可以针对性的降低DML和SELECT操作的MDL开销。

为了实现对DML/SELECT的快速加锁,使用了类似LOCK-WORD的加锁方式,称之为FAST-PATH,如果FAST-PATH加锁失败,则走SLOW-PATH来进行加锁。

每个MDL锁对象(MDL_lock)都维持了一个long long类型的状态值来标示当前的加锁状态,变量名为MDL_lock::m_fast_path_state 举个简单的例子:(初始在sbtest1表上对应MDL_lock::m_fast_path_state值为0)

Session 1: BEGIN;
Session 1: SELECT * FROM sbtest1 WHERE id =1; //m_fast_path_state = 1048576, MDL ticket 不加MDL_lock::m_granted队列
Session 2: BEGIN;
Session 2: SELECT * FROM sbtest1 WHERE id =2; //m_fast_path_state=1048576+1048576=2097152,同上,走FAST PATH
Session 3: ALTER TABLE sbtest1 ENGINE = INNODB; //DDL请求加的MDL_SHARED_UPGRADABLE类型锁被视为unobtrusive lock,可以认为这个是比上述SQL的MDL锁级别更高的锁,并且不相容,因此被强制走slow path。而slow path是需要加MDL_lock::m_rwlock的写锁。m_fast_path_state = m_fast_path_state | MDL_lock::HAS_SLOW_PATH | MDL_lock::HAS_OBTRUSIVE
注:DDL还会获得库级别的意向排他MDL锁或者表级别的共享可升级锁,但为了表述方便,这里直接忽略了,只考虑涉及的同一个MDL_lock锁对象。
Session 4: SELECT * FROM sbtest1 WHERE id =3; // 检查m_fast_path_state &HAS_OBTRUSIVE,如果DDL还没跑完,就会走slow path。

从上面的描述可以看出,MDL子系统显式的对锁类型进行了区分(OBTRUSIVE or UNOBTRUSIVE),存储在数组矩阵m_unobtrusive_lock_increment。 因此对于相容类型的MDL锁类型,例如DML/SELECT,加锁操作几乎没有任何读写锁或MUTEX开销。对应WL#7304WL#7306 , PATCH(Rev:7067,Rev:7129)(Rev:7586)

第三,由于引入了MDL锁,实际上早期版本用于控制Server和引擎层表级并发的THR_LOCK 对于Innodb而言已经有些冗余了,因此Innodb表完全可以忽略这部分的开销。

不过在已有的逻辑中,Innodb依然依赖THR_LOCK来实现LOCK TABLE tbname READ,因此增加了新的MDL锁类型来代替这种实现。实际上代码的大部分修改都是为了处理新的MDL类型,Innodb的改动只有几行代码。对应WL#6671,PATCH(Rev:8232)

第四,Server层的用户锁(通过GET_LOCK函数获取)使用MDL来重新实现。

用户可以通过GET_LOCK()来同时获取多个用户锁,同时由于使用MDL来实现,可以借助MDL子系统实现死锁的检测。注意由于该变化,导致用户锁的命名必须小于64字节,这是受MDL子系统的限制导致。对应WL#1159, PATCH(Rev:8356)


MySQL·性能优化·hash_scan 算法的实现解析

问题描述

首先,我们执行下面的TestCase:

[js]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. --source include/master-slave.inc  
  2. --source include/have_binlog_format_row.inc  
  3. connection slave;  
  4. set global slave_rows_search_algorithms='TABLE_SCAN';  
  5. connection master;  
  6. create table t1(id int, name varchar(20);  
  7. insert into t1 values(1,'a');  
  8. insert into t2 values(2, 'b');  
  9. ......  
  10. insert into t3 values(1000, 'xxx');  
  11. delete from t1;  
  12. ---source include/rpl_end.inc  
随着 t1 数据量的增大,rpl_hash_scan.test 的执行时间会随着 t1 数据量的增大而快速的增长,因为在执行 'delete from t1;' 对于t1的每一行删除操作,备库都要扫描t1,即全表扫描,如果 select count(*) from t1 = N, 则需要扫描N次 t1 表, 则读取记录数为: O(N + (N-1) + (N-2) + .... + 1) = O(N^2),在 replication 没有引入 hash_scan,binlog_format=row时,对于无索引表,是通过 table_scan 实现的,如果一个update_rows_log_event/delete_rows_log_event 包含多行修改时,每个修改都要进行全表扫描来实现,其 stack 如下:

[js]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. #0 Rows_log_event::do_table_scan_and_update  
  2. #1 0x0000000000a3d7f7 in Rows_log_event::do_apply_event   
  3. #2 0x0000000000a28e3a in Log_event::apply_event  
  4. #3 0x0000000000a8365f in apply_event_and_update_pos  
  5. #4 0x0000000000a84764 in exec_relay_log_event   
  6. #5 0x0000000000a89e97 in handle_slave_sql (arg=0x1b3e030)   
  7. #6 0x0000000000e341c3 in pfs_spawn_thread (arg=0x2b7f48004b20)   
  8. #7 0x0000003a00a07851 in start_thread () from /lib64/libpthread.so.0  
  9. #8 0x0000003a006e767d in clone () from /lib64/libc.so.6  
这种情况下,往往会造成备库延迟,这也是无索引表所带来的复制延迟问题。

如何解决问题:

  1. RDS 为了解这个问题,会在每个表创建的时候检查一下表是否包含主建或者唯一建,如果没有包含,则创建一个隐式主建,此主建对用户透明,用户无感,相应的show create, select * 等操作会屏蔽隐式主建,从而可以减少无索引表带来的影响;
  2. 官方为了解决这个问题,在5.6.6 及以后版本引入参数 slave_rows_search_algorithms ,用于指示备库在 apply_binlog_event时使用的算法,有三种算法TABLE_SCAN,INDEX_SCAN,HASH_SCAN,其中table_scan与index_scan是已经存在的,本文主要研究HASH_SCAN的实现方式,关于参数slave_rows_search_algorithms的设置。

hash_scan 的实现方法:

简单的讲,在 apply rows_log_event时,会将 log_event 中对行的更新缓存在两个结构中,分别是:m_hash, m_distinct_key_list。 m_hash:主要用来缓存更新的行记录的起始位置,是一个hash表; m_distinct_key_list:如果有索引,则将索引的值push 到m_distinct_key_list,如果表没有索引,则不使用这个List结构; 其中预扫描整个调用过程如下: Log_event::apply_event

[js]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. Rows_log_event::do_apply_event  
  2.    Rows_log_event::do_hash_scan_and_update   
  3.      Rows_log_event::do_hash_row  (add entry info of changed records)  
  4.        if (m_key_index < MAX_KEY) (index used instead of table scan)  
  5.          Rows_log_event::add_key_to_distinct_keyset ()  
当一个event 中包含多个行的更改时,会首先扫描所有的更改,将结果缓存到m_hash中,如果该表有索引,则将索引的值缓存至m_distinct_key_list List 中,如果没有,则不使用这个缓存结构,而直接进行全表扫描;

执行 stack 如下:

[js]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. #0 handler::ha_delete_row   
  2. #1 0x0000000000a4192b in Delete_rows_log_event::do_exec_row   
  3. #2 0x0000000000a3a9c8 in Rows_log_event::do_apply_row  
  4. #3 0x0000000000a3c1f4 in Rows_log_event::do_scan_and_update   
  5. #4 0x0000000000a3c5ef in Rows_log_event::do_hash_scan_and_update   
  6. #5 0x0000000000a3d7f7 in Rows_log_event::do_apply_event   
  7. #6 0x0000000000a28e3a in Log_event::apply_event  
  8. #7 0x0000000000a8365f in apply_event_and_update_pos  
  9. #8 0x0000000000a84764 in exec_relay_log_event   
  10. #9 0x0000000000a89e97 in handle_slave_sql  
  11. #10 0x0000000000e341c3 in pfs_spawn_thread  
  12. #11 0x0000003a00a07851 in start_thread ()   
  13. #12 0x0000003a006e767d in clone ()   

执行过程说明:

Rows_log_event::do_scan_and_update

[js]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. open_record_scan()  
  2.   do  
  3.     next_record_scan()  
  4.       if (m_key_index > MAX_KEY)  
  5.          ha_rnd_next();  
  6.       else  
  7.          ha_index_read_map(m_key from m_distinct_key_list)         
  8.       entry= m_hash->get()  
  9.       m_hash->del(entry);  
  10.       do_apply_row()  
  11.    while (m_hash->size > 0);  
从执行过程上可以看出,当使用hash_scan时,只会全表扫描一次,虽然会多次遍历m_hash这个hash表,但是这个扫描是O(1),所以,代价很小,因此可以降低扫描次数,提高执行效率。

hash_scan 的一个 bug

bug详情: http://bugs.mysql.com/bug.php?id=72788
bug原因:m_distinct_key_list 中的index key 不是唯一的,所以存在着对已经删除了的记录重复删除的问题。
bug修复: http://bazaar.launchpad.net/~mysql/mysql-server/5.7/revision/8494

问题扩展:

  • 在没有索引的情况下,是不是把 hash_scan 打开就能提高效率,降低延迟呢?不一定,如果每次更新操作只一条记录,此时仍然需要全表扫描,并且由于entry 的开销,应该会有后退的情况;
  • 一个event中能包含多少条记录的更新呢?这个和表结构以及记录的数据大小有关,一个event 的大小不会超过9000 bytes, 没有参数可以控制这个size;
  • hash_scan 有没有限制呢?hash_scan 只会对更新、删除操作有效,对于binlog_format=statement 产生的 Query_log_event 或者binlog_format=row 时产生的 Write_rows_log_event 不起作用;

版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/wang_xya/article/details/42921139
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2021-06-14 04:34:37
  • 阅读 ( 716 )
  • 分类:数据库

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢