1、MySQL主备切换
readonly 设置对超级(super)权限是无效的,而用于同步更新的线程,就拥有超级权限。
建议在做主备数据库的时候,将备用数据库设置为只读。(反向用readonly来判断节点的角色)
主备的同步是通过 binlog 日志同步,流程:
1)、备库上通过 change master 命令,设置主库的 IP、端口、用户名、密码,以及从哪个位置开始请求 binlog,这个位置包含了文件名称和日志偏移量;
2)、备库执行 start slave 命令,备库会启动两个线程。其中一个io_thread 负责与主库建立连接;
3)、主库校验完用户名、密码后,开始按照备库传过来的位置,从本地读取 binlog ,发给备库;
4)、备库拿到 binlog 后,写入本地文件(中转日志 relay log);
5)、sql_thread 读取中转日志,解析出日志里面的命令,执行。
binlog 的 format 除了 statement 和 row ,还有第三种格式叫做 mixed。MySQL会自己判断某条SQL语句是否可能引起主备不一致,如果可能有,就用 row 格式,否则就用 statement 格式。
现在越来越多的场景需要将 binlog 的格式设置为 row 格式,便于之后的数据恢复。
binlog 在记录 event 的时候,多记了一条命令:SET TIMESTAMP = xxx。约定了接下来的 now() 函数的返回时间。
如果主备数据库是双M设置,可能会产生循环复制,就得规定两个库的 server id。用 server id 的值来判断,binlog 日志是否应该丢弃。
2、MySQL是怎么保证高可用
主备的"同步延迟",可以在备库上执行 show slave status 命令,返回结果里的 seconds_behind_master,表示当前备库延迟了多少秒。
主备延迟的主要来源是:备库接收完 binlog 和执行完这个事务之间的时间差。(备库消费中转日志 relay log 的速度)
现在比较常见的部署是:对称部署,主备选用相同规格的机器。
但是也可能出现延迟,有可能是备库的压力大。(忽视了备库的压力控制,备库上的查询耗费了大量的CPU资源,影响了同步速度,造成了主备延迟)可以用一主多从的方式解决。
一主多从也可能导致延迟,就涉及到大事务。
可靠性优先策略优先于可用性优先策略,MySQL高可用系统的可用性,依赖于主备延迟。当延迟的时间越小,主库故障的时候,服务恢复需要的时间就越短,可用性越高。
扩展知识
主备同步延迟,工作中常遇到的几种情况:
1)、主库做大量dml的操作,引起延迟;
2)、主库有个大事务在处理,引起延迟;
3)、对myisam存储引起的表做dml操作,从库有延迟;
4)、利用pt工具对主库的大表做字段新增、修改和添加索引等操作,从库会有延迟。
3、主备的并行复制策略
主备的并行复制能力。
主库上影响并发度的原因是各种锁,由于InnoDB支持行锁,所以业务并发的支持度还是很好的。
备库的多线程复制机制,就是把只有一个线程的 sql_thread 拆成多个 worker 线程(从中转日志写数据到备库中,MySQL 5.6版本之前只支持单线程复制)
worker 线程的个数是由 slave_parallel_workers 决定(32核物理机该值设置为8-16最好)。
coordinator 分发任务给 worker 的时候,需要满足2个原则(各个MySQL版本的多线程复制):
1)、不能造成更新覆盖,更新同一行的两个事务必须分发到同一个 worker 中。
2)、同一个事务不能被拆开,必须放到同一个 worker 中。
按表分发策略:每个事务可能和多个 worker 产生冲突关系(一个事务修改多个表,且多个表在多个 worker 中已经等待),就会等待到只和一个 worker 产生冲突,然后分配给该 worker。如果碰到热点表,大部分的更新事务都会涉及到一个表,所有事务都会被分配到同一个 worker 中,就变成单线程复制了。
按行分发策略:如果两个事务没有更新相同的行,它们在备库上可以并行执行。(这个模式要求 binlog 的格式必须是 row ,步骤之类的和按表分发策略相似。
MySQL 5.6 的并行复制策略:按库并行,这个策略的并行效果,取决于压力模型。在主库上有多个DB,并且各个 DB 的压力均衡,使用这个策略的效果会很好。(不要求 binlog 的格式)这个策略用的不多。
MariaDB 的并行复制策略:在同一组里提交的事务,一定不会修改同一行(一组的事务有相同的commit_id ,相同的 commit_id 分发给不同的 worker 执行)。主库上可以并行执行的事务,备库上也一定可以并行执行的。(很容易被大事务拖后腿)
MySQL 5.7 并行复制策略:同时处于 prepare 状态的事务,在备库执行时是可以并行的(经过了锁冲突的检验)。处于 prepare 状态的事务,与处于 commit 状态的事务之间,在备库执行时也是可以并行的。
4、一主多从下的主备切换
在一主多从架构下,主库故障后的主备切换问题。(一主一备互为主备,还有多从)
当主库故障后,备库就会成为新主库,其他从库重新指向备库(新主库)。
基于位点的主备切换:当备库切换为主库,其他从库指向新主库的日志位置和偏移量,就随之改变。去同步位点法得到位置 T,但是通常会遇到主键冲突或者删除行不存在的错误(因为日志位置不精确),可以主动跳过事务,用 set global sql_slave_skip_counter=1; start slave; 命令跳过,或者设置 slave_skip_errors 参数为 1062(插入主键冲突)、1032(删除行不存在)。
但是这种方法很复杂并且很容易出错。
GTID(Global Transaction Identifier):生成一个唯一的全局事务ID,并且是连续递增的。格式为 GTID=server_uuid:gno ,server_uuid 为实例第一次启动时生成,gno 为连续递增整数(事务提交的时候分配的)。启动的方式是在启动一个 MySQL 实例的时候,加上参数 gtid_mode=on 和 enforce_gtid_consistency= on。
在 GTID 模式下,每个事务都会跟一个 GTID 一一对应。每个 MySQL 实例都会维护一个 GTID 集合,用来对应"这个实例执行过的所有事务"。
基于 GTID 的主备切换:master_auto_position = 1 表示这个主备关系使用的是 GTID 协议。备库实例的 GTID 集合记为 set_a ,从库实例的 GTID 集合记为 set_b。
1)、从库指向备库,基于主备关系建立连接;
2)、从库把 set_b 发给备库;
3)、备库算出 set_a 和 set_b 的差集,也就是存在于 set_a,不存在 set_b的GTID 的集合,判断备库是否包含了这个差集需要的所有 binlog 事务。不包含,表示备库已经把从库需要的 binlog 给删掉了,直接返回错误。 包含,备库从自己的 binlog 文件里面,找出第一个不在 set_b 的事务,发给从库。
4)、之后从这个事务开始,往后读文件,按顺序取 binlog 发给从库去执行。
5、读写分离
1)、客户端直连方案,客户端选择后端数据库进行查询。查询性能稍微好些,但是需要了解后端的部署细节,出现主备切换、库迁移等操作,就要相应的调整数据库信息。
2)、带 proxy 的架构,客户端只需要连接 proxy,对后端维护团队的要求比较高,并且需要 proxy 有高可用架构。主要还是选 proxy 这种架构方向。
如果主从存在延迟,客户端马上更新完主库数据,主从延迟,从库查询,查到的刚刚更新事物之前的状态,可以叫做"过期读"。
处理过期读:
1)、强制走主库方案:对于必须拿到最新结果的请求,强制将该请求发送到主库上。对于可以拿到旧数据的请求,才将其发到从库上。
2)、Sleep 方案:主库更新后,读从库之前先 sleep 一下(类似执行一条 select sleep(1) 命令),这样很大概率会在从库拿到最新数据。
3)、判断主备无延迟方案:从库执行查询请求前,先判断 seconds_behind_master 是否等于0,等于0的时候才能执行查询请求。还有对比位点来判断,接收到的日志是否已经同步完成。最后可以对比GTID集合,备库收到的所有日志的GTID集合和备库所有已经执行完的GTID集合相同,则主从无延迟。
4)、配合 semi-sync(半同步复制):事务提交的时候,主库把 binlog 发给从库,从库收到 binlog 以后,发回给主库一个 ack,表示收到了,主库收到这个 ack 以后,才能给客户端返回 "事务完成"的确认。启用了 semi-sync,表示所有给客户端发送过确认的事务,都确保了备库已经收到了这个日志。但是该方法只对 一主一从 的场景成立。
5)、等主库位点方案:
6)、GTID方案:
6、如何判断一个数据库出问题了
select 1 判断
其实 select 1 成功返回,只能说明这个库的进程还在,并不能说明主库没问题。例如一个数据库连接数达到最大值,再接受到新的请求,就会进入等待状态。此时 select 1 是成功的,但是编写的 sql 语句肯定会阻塞。将 innodb_thread_concurrency 设置为 64-128之间,这是并发查询参数。
并发连接:show processlist 看到的几千个连接就是并发连接。
并发查询(并发线程查询):当前正在执行的 sql 语句。并发查询太高才是 CPU 杀手。在线程进入锁等待以后,并发线程的计数会减一。(行锁和间隙锁的线程是不算在 128 参数值里面的,进入锁等待的线程已经不吃 CPU 了,这样设计,避免整个系统锁死)
查表判断
新建一个表,只放一行数据,定期查询。但是空间满了以后这方法又不好使。更新事务要写 binlog,而一旦 binlog 所在的磁盘的空间占用率达到了 100%,那么更新语句和事务提交的 commit 语句就会被堵住。 但是,系统这时候还可以正常读数据。所以就检测不出来是否出问题了。
更新判断
新建一个表,更新时间字段,查看返回结果。但是更新得更新主备库。在主库和备库执行相同的更新命令,肯能出现冲突,导致主备同步停止。 所以表中得多存数据,并且用主备库的 server_id 做主键。
内部统计
performance_schema 库的 file_summary_by_event_name 表里统计了每次 IO 请求的时间。
其实 select 1 是默认使用的判断数据库的方法。因为要从性能和业务实际情况做权衡。
7、误删数据的恢复办法
传统的高可用框架是不能预防误删数据的,主库的一个 drop table 命令,会通过 binlog 传给所有的从库和级联从库,从而导致整个集群的实例都会执行这个命令。
误删行
可以通过 Flashback 工具通过闪回把数据恢复过来。原理是修改 binlog 的内容,拿回原库重放。
前提是 binlog_format=row 和 binlog_row_image=FULL。
恢复举个例子:对于错误的 delete 语句,将 binlog_row 类型是 Delete_rows event 改为 Write_rows event 这种反向恢复。 对于 update 语句,binlog 的 row 记录修改前后的值,对调这两个值的位置就可以。
如果误操作是多个 sql 语句的事务,恢复的时候就将事务的顺序颠倒再执行。
不建议在主库上直接执行这些操作,恢复数据比较安全是,恢复出一个备份,找一个从库做临时库,先恢复临时库,确认数据后,再恢复主库。
事前预防:
1)、设置 sql_safe_updates=0。如果忘了写 where 条件,或者 where 条件里面没有包含索引字段,这条语句的执行就会报错。如果真要删除整个表,那就加 where id >=0 这种条件。
2)、代码上线前,必须经过 SQL 审计。
删除表,优先考虑 truncate table 或者 drop table 命令。
误删库/表
如果想恢复误删库/表的数据,就需要使用全量备份,加增量日志的方式。要求线上有定期的全量备份,并且实时备份 binlog。
流程:
1)、取出最近的一次全量备份,假如是凌晨0点备份的;
2)、用备份恢复出一个临时库;
3)、从日志备份里面,取出0点之后的日志;
4)、把除了误删除语句的这些日志,全部应用到临时库中。
第四步中,如果实例使用了 GTID 模式,假如误操作的 GTID=gtid1,那么只需执行 set gtid_next=gtid1;begin;commit; 先把这个 GTID 加到临时实例的 GTID 集合,之后按顺序执行 binlog 的时候,就会自动跳过误操作的语句。
注意:mysqlbinlog 工具并不能指定只解析一个表的日志,会恢复整个库的所有表。 mysqlbinlog 解析出日志应用,应用日志的过程就只能是单线程。
加速办法:在用备份恢复出临时实例之后,将这个临时实例设置成线上备份库的从库,在 start slave 之前,先执行 change replication filter replicate_do_table=(tbl_name)命令,可以让临时库只同步误操作的那一个表。这样做也可以用上并行复制技术,加速整个数据恢复过程。
延迟复制备库
通过 change master to master_delay=N 命令,可以指定该备库持续保持跟主库有N秒的延迟。
预防误删库/表的方法
1)、账号分离,给业务开发的同学开发库的 DML 权限,不给 truncate/drop 权限。DBA 团队成员日常也用只读账号。
2)、指定操作规范,删除数据表之前,先对表做改名 rename 操作。 观察一段时间确实对业务无影响再删除这张表。 改名最好加固定的后缀格式,删除的时候就不会出错。
扩展知识
修改生产数据库的数据,或者添加索引优化,要写好四个脚本:备份脚本、执行脚本、
验证脚本和回滚脚本。这样4个脚本能很大程度上避免数据误操作。
最主要的思维:备份
MySQL 备份的有效性的验证,可以通过不定期的备份恢复来验证。
8、有哪些 kill 不掉的命令
MySQL有两个 kill 命令:一个 kill query + 线程id ,表示终止这个线程中正在执行的语句;
一个 kill connection + 线程 id ,表示断开这个线程的连接,如果这个线程有语句在执行,就先停止语句再停止连接。
收到 kill 时,线程做的事情
kill 并不是马上停止的意思,而是告诉线程,这句 sql 不需要执行了,可以开始 "执行停止的逻辑了"。当用户执行 kill query thread_id 时,MySQL里处理 kill 命令的线程做了两件事:
将 session 的运行状态修改为 THD:KILL_QUERY;然后给 session 的执行线程发一个信号(如果只是把 session 的线程状态设置为 THD:KILL_QUERY,线程并不知道这个状态变化,还是会继续等待锁释放之类的行为)。
sql 语句执行过程中有多处 "埋点",在这些"埋点"的地方判断线程状态,如果发现线程状态是 THD:KILL_QUERY,才开始进入语句终止逻辑,并且到终止逻辑完全完成,是有一个过程。
如果是 kill connection ,本质上是把客户端的 sql 连接断开,后面的执行流程还是要走 kill query流程。当 show processlist 展示连接列表时, kill conncetion 会显示 "killed"。
例如一个 kill 不掉的例子
innodb_thread_concurrency 不够的情况,线程被堵住了,执行 kill query session 就没什么效果,得执行 kill connection 命令,才断开 session的连接,此时该线程如果 show processlist 的话,该线程的 Commnad 列显示的就是 Killed。(等待锁的等待状态要被唤醒)
上面的 kill connection 命令是把线程状态设置为 KILL_CONNECTIOON。如果一个线程的状态是 KILL_CONNECTION,就把 Command 列显示为 killed。
所以即使是客户端退出了,这个线程的状态仍然是在等待中。然后等到满足进入InnoDB的条件后,语句继续执行,判断线程的状态变成 KILL_QUERY或者KILL_CONNECTION,再进入终止逻辑阶段。
两种情况,一种就是上面的:线程还没有执行到判断线程状态的逻辑。另一种情况就是,终止逻辑耗时较长。
终止逻辑耗时较长常见情况:
1)、超大事务执行期间被 kill;
2)、大查询回滚;
3)、DDL命令执行到最后阶段,被 kill, 需要删除中间过程的临时文件,也可能受 IO 资源影响耗时较久。
库里的表很多,连接很慢的原因
首先并不是表的数目影响了连接性能。当使用默认参数连接的时候,MySQL 客户端会提供一个本地库名和表名补全的功能。客户端在连接成功后,需要多做一些操作:
执行 show databases;切到db库,执行 show tables;把这两个命令的结果用于构建一个本地的哈希表。尤其第三步耗时很长。所以表多,耗时就会变长。
所以其实是客户端慢。
在连接命令上加上 -A,就可以关掉这个自动补全的功能,客户端就可以快速返回了。
全局扩展
根据 explain sql 语句,脑海中脑补 sql 语句的执行流程:
1、连接MySQL;
2、根据 where 条件选择索引(二级索引和主键索引,或者覆盖索引,或者联合索引);
3、根据加锁规则,判断 sql 语句是否处于锁等待,还是直接执行;
4、根据索引,查到主键 id,回表(走二级索引树定位,如果是主键索引不用回表)。或者联合索引(多个条件,只需在一个索引树上就定位到范围内的数据),或者覆盖索引(不用回表,查询的所有字段值也在索引里);
5、根据查询的字段已经分配了一个 sort_buffer,根据符合条件的数据填充 sort_buffer,然后排序(也可以做到用主键 id 排序,然后再回表去拿所有查询字段的值,一般很少用);
6、根据 desc 或者 asc ,和 limit 分页,取出结果集返回给前端。
其中增删改是会写 redo_log 日志和 binlog 日志(二阶段提交。顺序写,减少随机IO。涉及到MySQL的高可用)。
如果删除大量表数据但是表文件大小不变,就是索引树上有很多位置是空洞或者数据页利用率很低,就要重建普通索引,使用命令 alter table t engine=InnoDB。
如果查询很慢,可能是前面的线程在堵塞。MySQL 的 InnoDB 引擎默认事务级别为 RR。
普通索引存在 change buffer,存的是 sql 修改的操作。数据以数据页的方式和磁盘做交互。change buffer 节省的是随机读磁盘的 IO 消耗(修改等操作缓存在 change buffer 中,当查询的时候,从内存拿到数据页,就不会从磁盘直接读,内存拿不到,就从磁盘拿,并且将 change buffer 的操作应用生成新的数据页,返回和刷脏页到磁盘)
InnoDB 显示加锁:sql 语句后加 lock in share mode 或者 sql 语句后加 for update。
该文章只是笔记,记录的是我自己大概的总结。
如果想看完整的知识点,可以去 极客时间app 上找 MySQL实战45讲 --- 林晓斌 老师的课程。