ITPub博客

首页 > 数据库 > 数据库开发技术 > mysql每天一点点笔记(十一)

mysql每天一点点笔记(十一)

原创 数据库开发技术 作者:DBA修行之路 时间:2022-09-02 22:12:06 0 删除 编辑

文字表述

文字表述

文字表述

左右滑动查看更多



点击上方“蓝字”,发现更多精彩。





mysql每天一点点笔记(十一)


今日内容

前一段时间遇到一个很有趣的case,现场的实例规格是2c16g,现象为实例频繁发生ha切换,ha切换的类型为实例hang住,实际登录实例发现实例没有问题,然后去看了下错误日志,在错误日志中发现了下面的信息: [Note] InnoDB: page_cleaner: 1000ms intended loop took 70389ms. The settings might not be optimal. (flushed=26 and evicted=0, during the time.),根据这个信息可以得知page cleaner 的循环刷脏周期是1s,如果不足1s则sleep,超过1s可能是刷脏太慢,而这里已经超过70s。很明显是不正常的。查看了下现场内存使用率不高,iops值也不高。另外,查看了innodb_log_waits值是大于0的,另外,查看了innodb_buffer_pool_instances的值为1,innodb_buffer_page_cleaners值为1,到此,innodb_lru_scan_depth值为默认值1024,innodb_log_buffer_size值为8388608(8m),结合上面参数结果,初步分析导致实例hang住的原因为innodb_log_buffer_size值较小,同时,刷脏页线程数较少,导致刷脏页较慢,从而导致写redo出现阻塞,继而实例响应慢,出现假死状况,从而触发ha切换机制发生ha切换。为了验证猜想,先调整了innodb_lru_scan_depth参数,从1024调整为256,但是效果不大,还是会有ha情况发生,然后调整了innodb_buffer_page_cleaners和innodb_buffer_pool_instances为4。对innodb_log_buffer_size暂未做调整,后续观察实例运行稳定,便未进行调整了,到此,问题解决。问题虽然解决了,但是这个case感觉还是挺值得思考的,于是有了该篇学习笔记。

针对ha问题进行扩展问题,归根到底是实例出现卡顿了,但是为什么会出现卡顿呢,出现卡顿时又如何进行解决,如何避免呢,这中间又涉及哪些知识点呢,如上图,我做了一个归纳。



sql问题

sql问题是一个复杂,庞大的概念体系,我们日常接触并了解的只是冰山一角,但是可以总结出那些常用的知识并转化自己的知识储备,如此未来便可少踩坑。从个人角度,我将sql问题大致分为以下六类:join优化问题,索引优化问题,limit优化问题。count优化问题,子查询问题,分库分表问题,详细如下:

join优化问题:

join在mysql中扮演着不可或缺的角色。当两张表需要连接时,通常便会使用到join,根据连接方式的不同,可以分为内连接和外连接,其中外连接又可细分为左外连接和右外连接。在连接中,分为驱动表(外表)和被驱动表(内表,亦或匹配表),内连接中,驱动表中的记录在被驱动表中找不到匹配的记录则不会计入到最终结果集中,因此驱动表和被驱动表可以互换而不影响最终查询结果。而左外连接和右外连接驱动表中的记录即使在被驱动表中没有匹配的记录,也需要加入到最终结果集,因此驱动表和驱动表交换则会影响查询结果。左外连接选取左侧的表为驱动表,右外连接则会选取右侧的表为驱动表。进行join时,会就驱动表的每一条记录去被驱动表中进行匹配。为了获取更好的性能,建议的原则是小表驱动大表,然后在被驱动表上建立关联字段索引。对于内连接mysql优化器会自动优化。而对于外连接则需要在写sql时自己注意。在8.0.18之前,驱动表不走索引,可能会造成查询扫描数据量大。8.0.18引入了hash 索引,使得驱动表也可以走索引,实现了一个性能的提升。另外,mysql针对join做了一些优化,这个后面进行细说。还有,即使不走索引,也可以通过调整join_buffer大小减少磁盘交互。


索引优化问题:

用一句话来说,成也索引,败也索引。合适的索引能够有效提高查询效率,事半功倍。不恰当的索引不仅不能提高查询性能,还会产生额外的维护成本。一般来说,表的数据量较大时索引效果更好,另外,如果表的数据量频繁变动,维护索引的成本很高,此时是否创建索引便需要进行斟酌了。此外,建立索引的列的数据应该有良好的区分度,同时,如果只需要查询部分列,则覆盖索引是一个很好的选择。如果需要针对多个列建立组合索引,需要考虑最左前缀原则,最后,单表的索引个数不建议太多,建议不超过3-5个。


limit优化问题:

在mysql中,查询关键字查询是按照一定先后顺序规则执行的,同时还会涉及server层和引擎层的交互。而limit关键字是在server层从引擎层获取数据后向客户端发送记录的时候进行处理。对于limit N方式,只需要选择符合条件的前N条即可,而对于limit M,N方式,则需要查询出符合条件的M+N条数据,会增加查询成本。可优化的方法有两种:第一种,先查询需要的N条数据的主键值,然后根据主键值进行回表查询。示例:select * from (select pk_no from test limit M,N) b, test a where a.pk_no= b.pk_no;第二种,通过业务侧优化,记录上次查询的值,然后进行条件查询。例如电影网站使用more获取更多数据。


count优化问题:

count(表达式)的用途是用于统计在满足搜索条件的记录中,指定表达式不为null的行数。在进行统计时候,server层接收从innodb层返回的记录并进行判断,如果不为null则count变量加1,直至innodb层没有记录向server层返回结束。常见的有count(*),count(常数),count(主键),count(普通索引),对于count(*),count(常数),mysql自动对其做了优化,因为server层只关心存储引擎是否读取到记录而不需要判断记录是否为null,因此优化器只会指定占用存储空间最小的索引进行查询,对于count(主键),虽然优化器只会指定占用存储空间最小的索引进行查询,但是会根据索引字段进行判断,而对于count(普通索引),server层需要从innodb中读取包含非主键列的记录。因此通常可以认为性能方面count(*)=count(常数)>count(主键)>=count(普通索引)。但是建议使用count(*)标准方式而不是count(常数)。


子查询:

子查询这一块涉及的比较多,我自身也存在一定欠缺,后续还需强化,总之,概括起来就是:mysql对子查询的处理不是很友好,虽然mysql自身有一些优化机制,但是如果可以的话,还是我们自己动手将子查询改写为连接的方式,另外,对于子查询delete操作,可以利用delete语句支持from后跟多张表的特性将子查询改写为关联查询。


分库分表:

之前业界的通用说法是超过500万行的表就是大表,需要考虑分表,实际上对于最新的8.0而言,千万级别的表也不会有太大压力。当然,需不需要分库分表具体还得结合业务来决断,例如某张表数据很大,但是基本上写多读少,此种情况也可以考虑不分表。分库分表详细来说可分为水平拆分和垂直拆分,或者两者结合使用,除此,还可以使用mysql自身的分区表功能,亦或者使用一些开源中间件,我自己之前接触过mycat,类似的还有shardingjdbc等工具。



规格问题

数据库服务是运行在机器的,机器常见的配置有cpu,内存,iops等,同时,数据库侧还会有连接数等限制。根据日常遇到的问题及平时学习积累,总结如下:

cpu规格:

cpu高是mysql中一个很高频的问题,也是一个需要重视的问题,cpu高可能会导致响应慢,机器负载高,严重的甚至导致服务hang住,影响业务,常见的导致cpu高的原因有几下几点:1.慢sql,这种一般也是最常见的,解决方法即找到对应的慢sql并进行优化,可以通过mysqldumpslow工具或者pt-query-digest工具协助进行分析。2.业务量,这种可以通过监控曲线进行判断,如果之前cpu都处于一个正常的值,随着qps,tps等指标进行增长,则大概率是因为业务量上升导致cpu高,此种情况升级规格即可。3.短时连接,这种情况一般较少,也比较难排查,可以借助execsnoop工具进行抓取。


内存规格:

mysql与主机侧关联唯二重要的便是内存和磁盘了。在进行操作时,一般都是先和内存交互,然后再持久化到磁盘上,毕竟内存比磁盘要快的多。内存设置的合理,事半功倍,设置不好,轻则导致性能慢,重则引发oom,影响业务。内存设置中占据大头的是buffer pool,一般根据需要设置为示例规格的65%-75%。


连接数:

连接数是限制连接到mysql的会话数,如果连接数满,则会无法连接。这种也会导致业务受损。造成连接数满的原因一方面可能是当前连接数确实设置不合理,无法满足业务需求。另外一方面原因可能是业务侧代码逻辑问题,请求后未及时关闭连接。解决连接数满的方法可以从规避,预防,救火三个时间段解决。规避是从源头解决,合理预估好业务量,并设计好代码逻辑。预防指的是通过监控,提前设置水位,到达水位高点则进行提前处理。救火指的是当连接数已经满了的情况下,此时可能无法登录示例,如果重启,可能对业务影响较大,则可以通过gdb命令临时调整连接数参数。


iops设置:

之前说过,除了内存,mysql与物理机交互最多的便是磁盘了,因为内存断电数据便会丢失,而生产上的数据,尤其涉及到重要业务,是不允许丢失的,因此便涉及到数据的持久化了,这便是落盘机制。相对内存而言,磁盘写入较慢,即使现在大部分都是ssd盘比原来的普通机械盘快了很多。iops便是衡量磁盘读写的一个关键指标,mysql中与之对应的参数便是innodb_io_capacity、innodb_io_capacity_max相关参数,这些参数控制mysql刷脏页的能力。过小会导致刷脏页能力不足,影响性能,甚至因刷脏页耗时过长而导致实例hang住,过大会导致主机io过高,需要合理设置,对于主机的iops能力可以通过fio工具来测试。


其他设置:

对于数据库方面,还可以从主机侧进行一些优化,部分如下:

1.优先使用flash设备,例如NVMe和PCIe接口的flash设备。

2.将binlog,redo等日志文件和数据文件分开存储,日志文件一般是顺序写,此时普通HDD和SSD盘即可满足需求,而数据是异步I/O,需要磁盘满足随机读写IOPS高,并发I/O处理能力强,在资源充足的情况下建议使用PCIe等接口的flash存储或者不带缓冲的SSD盘。

3.设置较小的vm.swappiness值。

4.可以开启大页。

5.采购机器时优先选择频率较高,核数较多的cpu。

6.关闭iptables和SELinux,修改ssh登录端口。




配置问题

结合本case,我总结了跟这类场景相关的参数,再遇到这类问题时候,可以从这些参数进行入手排查并进行优化。总体分为五个部分:redo相关,bp_ins相关,LRU相关,thread相关,cleaner相关,具体详细参数见下图:

redo相关:

redo日志作为mysql中innodb引擎特有的一类用于crash-safe的日志,其重要性不言而喻。同时,利用xtrabackup进行物理备份时也离不开他。另外,我们需要知道的redo日志是循环写的,在落盘前是先写到redo buffer中。因此,redo日志大小及redo buffer大小的设置就很关键了。设置过小,可能会导致被覆盖备份失败或者写等待,设置过大会导致崩溃恢复时耗时较长。其中,redo日志单个文件大小由innodb_log_file_size控制,redo日志组文件个数由innodb_log_files_in_group控制,redo日志总大小为这两个参数的乘积。在mysql8.0.30之前,修改redo日志大小需要重启服务,这个对业务不太友好,幸运的是mysql8.0.30已经支持在线修改。redo buffer的大小由innodb_log_buffer_size参数控制。


bp_ins:

bp在mysql中无疑是王者一般的地位,90%的操作都是在bp中完成的,如果资源充足的情况下,可以设置bp足够大到能够容纳所有的数据,此时的性能绝对是飞上天的。但是往往理想是美好的,现实是残酷的。一般的话设置bp为实例规格的65%-75%,bp大小对应的参数为innodb_buffer_pool_size。同时,如果bp较大并且业务量并发高,为了提高性能可以设置innodb buffer pool的实例个数大于1,控制bp实例个数的参数为innodb_buffer_pool_instances,需要注意的是,当innodb_buffer_pool_size不大于1G时,设置innodb_buffer_pool_instances大于1不生效。


LRU:

mysql中数据在内存和磁盘之间进行交互,而LRU链表便是内存和磁盘进行交互的纽扣。当查询的数据不在bp中,便会将数据从磁盘加载到bp中,而具体位置便是LRU链表中。同理,当bp的内存不足容纳新的数据时,便会从LRU链表或者FLUSH链表中刷新脏页到磁盘以释放内存空间。早期大部分磁盘是普通机械盘,为了减少磁盘寻道开销,mysql设计了预读机制以提高语句执行效率,但是预读机制可能会驱除有效数据,导致缓存命中率降低,随着ssd盘的普及,磁盘读取效率已经大大提高,可以关闭预读机制,控制预读机制的参数是innodb_random_read_ahead。另外,在从LRU链表的冷数据中刷新脏页到磁盘,后台线程会定时从lru链表尾部开始扫描缓存页,扫描的页面数由innodb_lru_scan_depth指定,默认为1024,当刷脏页耗时较长时,可以考虑减小该值。


thread:

mysql中对读操作,写操作以及并发度都有相关的限制,对应的参数如下:innodb_write_io_threads,innodb_read_io_threads,innodb_thread_concurrency。其中,innodb_write_io_threads表示写线程数量。innodb_read_io_threads表示预读线程数量。对于小规格实例,无需额外调整,对于规格较大的实例,例如cpu规格16C以上,可以适当调大该值,例如和cpu个数一致,此时能够提升读操作和写操作的能力。而innodb_thread_concurrency参数则是控制并发的最大线程数。默认为0,即不限制,不建议调整该值,之前遇到过一次案例,由于现场人员设置该值大于0,导致在资源充足的情况下并发较低,严重影响了业务。


cleaner:

mysql中,与刷脏页相关的常见参数有如下这些:innodb_max_dirty_pages_pct,innodb_page_cleaners,innodb_flush_neighbors,innodb_io_capacity。这些参数分别从不同的维度影响刷脏操作。innodb_max_dirty_pages_pct表示高水位标记值,默认值为75,如果缓冲池中脏页百分比达到该值,则会全力刷新缓冲池中脏页,此时可能会导致外服务受影响。innodb_page_cleaners参数控制从缓冲池实例中刷新脏页的页面清理线程数。如果页面清理线程的数量超过缓冲池实例的数量, 则innodb_page_cleaners值会被自动设置为与innodb_buffer_pool_instances值一致。innodb_flush_neighbors该参数是innodb用来控制buffer pool刷脏页时候是否将脏页附近的其他脏页一起刷入磁盘的,在传统普通机械盘时代能提升随机写效率,但是现在ssd盘时代,通常关闭该参数,即设置为0。innodb_io_capacity变量定义每秒可用于InnoDB后台任务的 I/O 操作数(IOPS),例如从缓冲池刷新 页面和合并来自更改缓冲区的数据。合并插入缓冲时,每秒合并插入缓冲的数量为innodb_io_capacity值的5%,在刷新脏页时,每秒刷新脏页的数量等于innodb_io_capacity的值。对于磁盘的iops,可以通过fio命令进行测试,参考值为sata/sas盘值为200左右,sas raid10为2000左右,ssd硬盘为8000左右,fusion-io为25000-50000。





设计问题

与oracle不同,oracle中支持RBO(Rule-Based Optimization)和CBO(Cost-Based Optimization),mysql只支持CBO。在mysql中,为了提高节能减排,设计者可谓煞费苦心,无论是ICP,还是BNL,亦或是MRR,都能发现这些算法的终极目标就是为了使得sql执行成本更低,了解这些设计思想和优化原理,对于我们日常工作裨益良多。如下,是常见的一些优化算法:

回表:

当执行sql时,如果实际使用的索引是辅助索引,此时如果索引字段不能满足全部查询字段,则需要进行回表操作,即通过辅助索引叶子节点的主键值到主键索引所在的B+树去查找完整记录,这会增加查询成本。


MRR:

MRR全称为multi-range Read,二级索引进行回表时,由于主键索引和二级索引的顺序不一致会导致随机io,随机io会比较慢,MRR会在回表前将索引扫描的顺序根据主键进行排序,然后按照主键进行顺序查找。将随机I/O转化为顺序I/O。可以通过设置SET  @@optimizer_switch='mrr=on,mrr_cost_based=on';开启MRR,该过程涉及的buffer参数为read_rnd_buffer_size。


ICP:

ICP全称为Index Condition Pushdown,当where条件查询使用索引时,mysql会将过滤操作从server层下推到引擎层,在引擎层过滤不符合条件的行记录,以此减少引擎层访问基表的次数和server层访问引擎层的次数,降低I/O操作,提高性能。可以通过设置SET  @@optimizer_switch='index_condition_pushdown=on';开启ICP,使用ICP优化时,使用explain查看执行计划会发现Extra列显示Using indexcondition提示。


BNL:

BNL全称Block Nested-Loop Join,当join中内循环是全表扫描时,将外循环的行缓存到join buffer中,然后内层循环的每一行与buffer中的记录进行对比,从而减少内层循环的次数。join buffer大小由join_buffer_size参数控制,每次连接使用一个join buffer,多表连接使用多个join buffer,且join buffer只存储与查询相关的字段数据。此时性能较差。


BKA:

BKA全称Batched Key Aceess,是对BNL算法的优化,如果被join的表上有索引,则在join buffer中利用MRR特性对索引字段进行排序,以此减少随机I/O。


index dive:

mysql优化器在进行是基于成本计算的,实际执行查询前,优化器会计算索引查询的成本和全表扫描的成本然后选取更低的方式执行。在范围查询中,当计算索引查询成本时,mysql有两种优化算法,index dive和index statistic,index dive方式中,在查询前优化器会先访问索引来计算需要扫描的索引记录数量。在index statistic方式中,在查询前优化器直接使用索引的统计数值。前者速度慢,但是值更准确,后者速度快,但是值未必准确。另外,mysql中使用eq_range_index_dive_limit参数来控制使用哪种方式。在mysql5.7.3之后,该值默认为200,即当范围区间小于200时,使用index dive方式,否则使用index statistic方式。


刷脏页:

个人角度,mysql中刷脏页的意义一方面是为了解决磁盘和内存容量级别的问题,另一方面是为了保证数据的安全性,持久性,毕竟内存中断电数据就会丢失。


自问自答

Q:
redo log buffer如何调整?

对于调整redo log buffer参数,可以通过实际测试后进行合理调整,innodb_log_buffer_size和innodb_os_log_written状态和

Com_commit状态相关。其中,innodb_os_log_written表示innodb引擎中redo log有多少个字节被写入,Com_commit表示在autocommit=0时提交的事务数量。在业务高峰期,通过如下方式计算每个事务缓冲的平均数据量大小,然后以此为依据设置redo log buffer,单位为bytes。

set global autocommit = 0; 

SHOW GLOBAL STATUS LIKE 'Innodb_os_log_written';

select SLEEP(时间);

SHOW GLOBAL STATUS LIKE 'Com_commit';

select Innodb_os_log_written / Com_commit;


另外,如果Innodb_log_waits值一直增大,说明innodb_log_buffer_size过小,需要调大,可通过如下方式测试:

set @old_value := 0;

set @new_value := 0;

select VARIABLE_VALUE into @old_value from information_schema.GLOBAL_STATUS where VARIABLE_NAME = 'Innodb_log_waits';

select SLEEP(时间);

select VARIABLE_VALUE into @new_value from information_schema.GLOBAL_STATUS where VARIABLE_NAME = 'Innodb_log_waits';

select @old_value;

select @new_value;

Q:
redo log大小设置?

在mysql中,如何合理设置redo日志的大小呢,我们知道,redo日志是循环写的,如果写满了就会从头覆盖,因此,正常来说,redo日志的大小应该至少设置为满足业务高峰期一个小时的业务日志写入量。可以通过下面方式进行估算:

在业务高峰期执行:

pager grep sequence;

show engine innodb status\G select sleep(60);

show engine innodb status\G

然后将两次lsn的值相减估算出单位时间的写入量,再以此为基数计算一个小时的写入量,最后即可得出估算结果。

Q:
服务未异常为啥会ha?

首先,我们需要了解ha的机制和原理,ha服务会定时向数据库发送心跳检测数据库是否正常,通常都是连续三次未响应便判定服务异常。其次,关联数据库的四种刷脏页场景。当redo满和脏页过多时虽然mysql本身服务正常,但是此时由于需要全力落盘而无法正常对外提供服务,进入假死状态,类似于jvm发生full gc时的朱丽叶暂停现象。该案例即是如此。


结尾小结

以上便是由之前遇到的一个案例引发的思考了,学而不思则罔,思而不学则怠。虽然是一个比较平常的case,但是如果细细挖掘,便能从中学到很多,结合这个case,我们可以将平时学习的一些知识点进行串联,再消化。下次遇到这类问题思路就更加清晰了,是不是也是一件很有趣很有意义的事情呢。如果能够一直坚持,难道还怕学不好嘛?好了,今天就到这里吧,周五了,祝大家有个美好的周末。




END



扫码关注我
每天进步一点点,未来才可期。如果你也渴望成长,可以添加微信xuwl_1234,我们一起学习,一起探讨。
赞一下你最美!


原文作者:DBA修行之路;如有侵权,请联系管理员删除。

请登录后发表评论 登录
全部评论

注册时间:2021-05-25

  • 博文量
    64
  • 访问量
    23943