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

标签: General | 发表时间:2015-01-22 02:00 | 作者:ajaxj
出处:http://www.geek521.com

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#7304, WL#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:

 --source include/master-slave.inc
 --source include/have_binlog_format_row.inc
 connection slave;
 set global slave_rows_search_algorithms=’TABLE_SCAN’;
 connection master;
 create table t1(id int, name varchar(20);
 insert into t1 values(1,’a’);
 insert into t2 values(2, ’b’);
 ......
 insert into t3 values(1000, ’xxx’);
 delete from t1;
 ---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 如下:

#0 Rows_log_event::do_table_scan_and_update
#1 0x0000000000a3d7f7 in Rows_log_event::do_apply_event 
#2 0x0000000000a28e3a in Log_event::apply_event
#3 0x0000000000a8365f in apply_event_and_update_pos
#4 0x0000000000a84764 in exec_relay_log_event 
#5 0x0000000000a89e97 in handle_slave_sql (arg=0x1b3e030) 
#6 0x0000000000e341c3 in pfs_spawn_thread (arg=0x2b7f48004b20) 
#7 0x0000003a00a07851 in start_thread () from /lib64/libpthread.so.0
#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

 Rows_log_event::do_apply_event
    Rows_log_event::do_hash_scan_and_update 
      Rows_log_event::do_hash_row  (add entry info of changed records)
        if (m_key_index < MAX_KEY) (index used instead of table scan)
          Rows_log_event::add_key_to_distinct_keyset ()

当一个event 中包含多个行的更改时,会首先扫描所有的更改,将结果缓存到m_hash中,如果该表有索引,则将索引的值缓存至m_distinct_key_list List 中,如果没有,则不使用这个缓存结构,而直接进行全表扫描;

执行 stack 如下:

#0 handler::ha_delete_row 
#1 0x0000000000a4192b in Delete_rows_log_event::do_exec_row 
#2 0x0000000000a3a9c8 in Rows_log_event::do_apply_row
#3 0x0000000000a3c1f4 in Rows_log_event::do_scan_and_update 
#4 0x0000000000a3c5ef in Rows_log_event::do_hash_scan_and_update 
#5 0x0000000000a3d7f7 in Rows_log_event::do_apply_event 
#6 0x0000000000a28e3a in Log_event::apply_event
#7 0x0000000000a8365f in apply_event_and_update_pos
#8 0x0000000000a84764 in exec_relay_log_event 
#9 0x0000000000a89e97 in handle_slave_sql
#10 0x0000000000e341c3 in pfs_spawn_thread
#11 0x0000003a00a07851 in start_thread () 
#12 0x0000003a006e767d in clone ()

执行过程说明:

Rows_log_event::do_scan_and_update

  open_record_scan()
    do
      next_record_scan()
        if (m_key_index > MAX_KEY)
           ha_rnd_next();
        else
           ha_index_read_map(m_key from m_distinct_key_list)       
        entry= m_hash->get()
        m_hash->del(entry);
        do_apply_row()
     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 不起作用;

TokuDB·版本优化·7.5.0

TokuDB 7.5.0大版本已发布,是一个里程碑的版本,这里谈几点优化,以飨存储引擎爱好者们。

a) shutdown加速

有用户反馈TokuDB在shutdown的时候,半个小时还没完事,非常不可接受。在shutdown的时候,TokuDB在干什么呢?在做checkpoint,把内存中的节点数据序列化并压缩到磁盘。

那为什么如此耗时呢?如果tokudb_cache_size开的比较大,内存中的节点会非常多,在shutdown的时候,大家都排队等着被压缩到磁盘(串行的)。

在7.5.0版本,TokuDB官方针对此问题进行了优化,使多个节点并行压缩来缩短时间。

BTW: TokuDB在早期设计的时候已保留并行接口,只是一直未开启。

b) 内节点读取加速

在内存中,TokuDB内节点(internal node)的每个message buffer都有2个重要数据结构:

 1) FIFO结构,保存{key, value}
2) OMT结构,保存{key, FIFO-offset}

由于FIFO不具备快速查找特性,就利用OMT来做快速查找(根据key查到value)。这样,当内节点发生cache miss的时候,索引层需要做:

 1) 从磁盘读取节点内容到内存
2) 构造FIFO结构
3) 根据FIFO构造OMT结构(做排序)

由于TokuDB内部有不少性能探(ji)针(shu),他们发现步骤3)是个不小的性能消耗点,因为每次都要把message buffer做下排序构造出OMT,于是在7.5.0版本,把OMT的FIFO-offset(已排序)也持久化到磁盘,这样排序的损耗就没了。

c) 顺序写加速

当写发生的时候,会根据当前的key在pivots里查找(二分)当前写要落入哪个mesage buffer,如果写是顺序(或局部顺序,数据走向为最右边路径)的,就可以避免由"查找"带来的额外开销。

如何判断是顺序写呢?TokuDB使用了一种简单的启发式方法(heurstic):seqinsert_score积分式。如果:

1) 当前写入落入最右节点,对seqinsert_score加一分(原子)
2) 当前写入落入非最右节点,对seqinsert_score清零(原子)

当seqinsert_score大于100的时候,就可以认为是顺序写,当下次写操作发生时,首先与最右的节点pivot进行对比判断,如果确实为顺序写,则会被写到该节点,省去不少compare开销。方法简单而有效。

MariaDB· 性能优化·filesort with small LIMIT optimization

从MySQL 5.6.2/MariaDB 10.0.0版本开始,MySQL/MariaDB针对”ORDER BY …LIMIT n”语句实现了一种新的优化策略。当n足够小的时候,优化器会采用一个容积为n的优先队列来进行排序,而不是排序所有数据然后取出前n条。 这个新算法可以这么描述:(假设是ASC排序)

  1. 建立一个只有n个元素的优先队列(堆),根节点为堆中最大元素
  2. 根据其他条件,依次从表中取出一行数据
  3. 如果当前行的排序关键字小于堆头,则把当前元素替换堆头,重新Shift保持堆的特性
  4. 再取一条数据重复2步骤,如果没有下一条数据则执行5
  5. 依次取出堆中的元素(从大到小排序),逆序输出(从小到大排序),即可得ASC的排序结果

这样的算法,时间复杂度为m*log(n),m为索引过滤后的行数,n为LIMIT的行数。而原始的全排序算法,时间复杂度为m*log(m)。只要n远小于m,这个算法就会很有效。

不过在MySQL 5.6中,除了optimizer_trace,没有好的方法来看到这个新的执行计划到底起了多少作用。MariaDB 10.013开始,提供一个系统状态,可以查看新执行计划调用的次数:

 Sort_priority_queue_sorts
描述: 通过优先队列实现排序的次数。(总排序次数=Sort_range+Sort_scan)
范围: Global, Session
数据类型: numeric
引入版本: MariaDB 10.0.13

此外,MariaDB还将此信息打入了Slow Log中。只要指定 log_slow_verbosity=query_plan,就可以在Slow Log中看到这样的记录:

# Time: 140714 18:30:39
 # User@Host: root[root] @ localhost []
 # Thread_id: 3  Schema: test  QC_hit: No
 # Query_time: 0.053857  Lock_time: 0.000188  Rows_sent: 11  Rows_examined: 100011
 # Full_scan: Yes  Full_join: No  Tmp_table: No  Tmp_table_on_disk: No
 # Filesort: Yes  Filesort_on_disk: No  Merge_passes: 0  Priority_queue: Yes
 SET timestamp=1405348239;SET timestamp=1405348239;
 select * from t1 where col1 between 10 and 20 order by col2 limit 100;

“Priority_queue: Yes” 就表示这个Query利用了优先队列的执行计划(pt-query-digest 目前已经可以解析 Priority_queue 这个列)。更多精彩内容,敬请期待!

相关 [淘宝 内部 分享] 推荐:

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

- - 极客521 | 极客521
编者按:淘宝自从2010开始规模使用MySQL,替换了之前商品、交易、用户等原基于IOE方案的核心数据库,目前已部署数千台规模. 同时和Oracle, Percona, Mariadb等上游厂商有良好合作,共向上游提交20多个Patch. 目前淘宝核心系统研发部数据库组,根据淘宝的业务需求,改进数据库和提升性能,提供高性能、可扩展的、稳定可靠的数据库(存储)解决方案.

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

- - 极客521 | 极客521
MySQL是目前使用最多的开源数据库,但是MySQL数据库的默认设置性能非常的差,必须进行不断的优化,而优化是一个复杂的任务,本文描述淘宝数据库团队针对MySQL数据库Metadata Lock子系统的优化,hash_scan 算法的实现解析的性能优化,TokuDB·版本优化,以及MariaDB·的性能优化.

利用淘宝 API 在新浪微博上分享淘宝商品,赚取淘宝客返利

- Ken - python.cn(jobs, news)
前段时间做了个新浪应用,在新浪微博上分享淘宝商品. 淘宝api申请好几次都没能通过审核. 于是把代码共享,为学习flask的朋友多一个参考项目. 项目地址:https://github.com/laoqiu/sinaapp. 项目实例:http://tuibei.viimii.li (网站功能已无法正常使用).

正祥的淘宝海量数据库思考分享

- alex zhao - 淘宝核心系统团队博客
正祥(阳振坤老师)最近在个人新浪博客上发表了一系列针对淘宝分布式数据服务系统思考性文章,特此分享. 淘宝海量数据库之一:来自业务的挑战. 淘宝海量数据库之二:一致性选择. 淘宝海量数据库之三:事务的ACID. 淘宝海量数据库之四:系统架构与跨表事务. 淘宝海量数据库之五:数据结构.

"Java在淘宝AD系统中的应用"分享汇总

- - ITeye博客
条幅AD 搜索AD 定向AD. 高性能Web服务器 关系数据库,内存KV数据库, NoSQL (BigTable) 搜索引擎 排序算法(Ranking) 实时计算 海量日志收集和处理(MapReduce, 报表和结算) RPC,消息中间件 客户端技术:浏览器,Javascript. 多个子系统都采用Java实现:.

淘宝网架构分享总结 - 架构,分布式,淘宝,虚拟化,水平伸缩

- - 互联网 - ITeye博客
关键字:淘宝网架构分享总结 - 架构,分布式,淘宝,虚拟化,水平伸缩. 一场由淘宝的架构师,曾宪杰先生主讲的淘宝网架构分享. 一、为什么stateless比较有利于实现水平伸缩. 关于什么是stateless的扫盲,见这个贴: http://kyfxbl.iteye.com/blog/1831869.

2018微博内部技术分享春晚专场

- - 鸟窝
这是公司内部分享的各部门春晚保障的技术分享. 我将其中的和公司隐私相关的数据删除了,只保留了技术的介绍,总结了一些知识点. 双十一、微信红包和微博的区别(无法预期). 孟兆飞 混合云架构下微博春晚保障. 15分钟1000台全公司随时随地. 双仓库(公司内网、阿里云). 传统扩容,由于资源限制会失败.

如何打造高质量的SSP广告引擎(内部干货分享)

- -
当今互联网有几种主流的商业模式:广告、游戏、增值服务等. 毫无疑问“广告推送”带给互联网公司的收入绝对是相当可观. 今天小主就为大家分享一篇来自360手机卫士团队分享的SSP广告引擎,这么高质量的内部干货不拿出来分享实在对不起大家. PS:丰富的一线技术、多元化的表现形式,尽在“HULK一线技术杂谈”,点关注哦.

淘宝“伤”城

- 品味视界 - FT中文网_英国《金融时报》(Financial Times)
秦苏为英国《金融时报》中文网撰稿. 中国互联网的野蛮生长,再次震惊了电子商务市场. 10月11日晚间,为抗议淘宝商城大幅提高技术服务年费和保证金,约7000家中小卖家通过YY网络语音等组织方式,对韩都衣舍、欧莎、七格格、优衣库等大卖家进行攻击,包括利用规则进行购物、给差评、到货付款或申请退款等. 通过集中拍下某商品,导致这些商家的大部分商品下架“被拍死”.

如何开发一款堪比APP的微信小程序(腾讯内部团队分享)

- - ITeye博客
一夜之间,微信小程序刷爆了行业网站和朋友圈,小程序真的能如张小龙所说让用户“即用即走”吗. 其功能能和动辄几十兆安装文件的APP相比吗. 开发小程序,是不是意味着移动应用开发的一次推倒重来,企业的运营成本会不会因此增加. 近日,笔者参加了一次腾讯自选股介绍小程序开发过程的内部沙龙,自选股小程序产品经理Poppy和首席开发工程师小K的一番海聊,从开发技术和行业走势两个方面为我们揭开了小程序的秘密.