「笔记」MySQL 实战 45 讲 - 实践篇(四)

检测 MySQL 健康状态

  • 每个改进的方案,都会增加额外损耗,需要业务方根据实际情况去做权衡

    • 建议优先考虑 update 系统表,然后再配合增加检测 performance_schema 的信息
  • select 1 判断

    • 使用非常广泛的 MHA(Master High Availability),默认使用的就是这个方法
      • 另一个可选方法是只做连接,就是 “如果连接成功就认为主库没问题”
    • select 1 成功返回,只能说明这个库的进程还在,并不能说明主库没问题
    • innodb_thread_concurrency 参数:控制 InnoDB 的并发线程上限
      • 超过阈值,则进入等待状态,直到有线程退出
      • innodb_thread_concurrency 这个参数的默认值是 0,表示不限制并发线程数量
      • 建议把 innodb_thread_concurrency 设置为 64~128 之间的值
      • 在线程进入锁等待以后,并发线程的计数会减一(也就是说等行级锁的线程不纳入计算
      • 真正地执行查询导致计数加一(select sleep (100) from t
    • 并发连接和并发查询,并不是同一个概念
      • show processlist 的结果里,看到的几千个连接,指的就是并发连接
        • 并发连接数达到几千个影响并不大,就是多占一些内存而已
      • “当前正在执行” 的语句,才是我们所说的并发查询
        • 并发查询太高才是 CPU 杀手(需要设置 innodb_thread_concurrency 参数的原因
  • 查表判断

    • 为了检测 InnoDB 并发线程数过多导致的系统不可用情况,我们需要找一个访问 InnoDB 的场景
    • 一般的做法是,在系统库(mysql 库)里创建一个表,里面只放一行数据并定期执行
      • 栗子:select * from mysql.health_check;
      • 可以检测出由于并发线程过多导致的数据库不可用的情况
      • 缺点:空间满了以后,这种方法又会变得不好使(读不受影响,事务更新 commit 会被堵住
  • 更新判断

    • 为了让主备之间的更新不产生冲突,在 health_check 表上存入多行数据,并用 server_id 做主键
      mysql> CREATE TABLE `health_check` (
        `id` int(11) NOT NULL,
        `t_modified` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
        PRIMARY KEY (`id`)
      ) ENGINE=InnoDB;
      
      /* 检测命令 */
      insert into mysql.health_check(id, t_modified) values (@@server_id, now()) on duplicate key update t_modified=now();
    
    • 更新语句,如果失败或者超时,就可以发起主备切换了,但会存在“判断慢”的问题

      • 栗子(其实就是服务器 IO 资源分配的问题)
        • 假设日志盘的 IO 利用率已经是 100%,整个系统响应非常慢,本应该需要主备切换
        • 但 IO 利用率 100% 表示系统的 IO 是在工作的,仍有机会获得 IO 资源执行该任务
          • 拿到资源后提交成功,并且在超时时间 N 秒未到达之前就返回给了检测系统
      • 根本原因是我们上面说的所有方法,都是基于外部检测(存在天然问题 - 随机性
        • 外部检测都需要定时轮询,可能得多次轮询后才能发现问题,导致切换慢
  • 内部统计

    • MySQL 5.6 版本以后提供了 performance_schema 库,可以借助此库检测健康状态
      • file_summary_by_event_name 表里统计了每次 IO 请求的时间
    • 如果打开所有的 performance_schema 项,性能大概会下降 10% 左右
      • 建议只打开自己需要的项进行统计

删库不跑路

  • 强调的核心:预防远比处理的意义来得大

    • 数据和服务的可靠性不止是运维团队的工作,最终是各个环节一起保障的结果
  • 误删行

    • 可以用 Flashback 工具通过闪回把数据恢复回来
      • 原理:修改 binlog 的内容拿回原库重放(需 binlog_format=row 和 binlog_row_image=FULL
      • 栗子:
        • Delete_rows event 改为 Write_rows event / Update_rows 对调修改前后值的位置
        • 如果误删数据涉及到了多个事务的话,需要将事务的顺序调过来再执行
    • 恢复数据比较安全的做法是找一个从库作为临时库,在从库上执行恢复操作,确认后恢复回主库
      • 数据状态的变更往往是有关联的,若单独恢复了几行数据(未经确认),可能造成二次破坏
    • 如何做好事前预防,避免误删数据
      • 把 sql_safe_updates 参数设置为 on(delete/update 忘记带 where 时,直接报错
      • 代码上线前,必须经过 SQL 审计
  • 误删库 / 表

    • delete 全部很慢,建议优先考虑使用 truncate /drop table 和 drop database 命令删除的数据
      • 即使我们配置了 binlog_format=row,以上三个命令记录的 binlog 均是 statement 格式
      • 导致无法使用 Flashback 工具恢复数据
    • 全量备份,加增量日志的方式才能恢复数据(要求线上有定期的全量备份,并且实时备份 binlog
      • 数据恢复流程 -mysqlbinlog 方法

        img

        • 取最近一次全量备份,假设这个库是一天一备,上次备份是当天 0 点

        • 用备份恢复出一个临时库

        • 从日志备份里面,取出凌晨 0 点之后的日志

        • 把这些日志,除了误删除数据的语句外,全部应用到临时库

        • 恢复流程特别说明

          • 若临时库上有多个数据库,可使用 mysqlbinlog 命令时,加上一个–database 参数
            • 用来指定误删表所在的库,避免了在恢复数据时还要应用其他库日志的情况
          • 在应用日志的时候,需要跳过 12 点误操作的那个语句的 binlog
            • 若未使用 GTID 模式,则通过 –stop-position / –start-position 跳过误操作语句
            • 若使用 GTID 模式,则通过 set gtid_next=gtid1;begin;commit; 跳过误操作语句
        • 此方法恢复不够快的主要两个原因

          • 如果是误删表,最好就只恢复出这张表,但 mysqlbinlog 工具并不能支持表维度解析
          • 用 mysqlbinlog 解析出日志应用,应用日志的过程就只能是单线程(无法并行复制
      • 数据恢复流程 -master-slave 方法

        img

        • 虚线:若备库上已删除需要的 binlog,则从 binlog 备份系统中找到并放回备库

        • 在用备份恢复出临时实例之后,将这个临时实例设置成线上备库的从库

          • 在 start slave 之前执行change replication filter replicate_do_table = (tbl_name) 命令
            • 目的:让临时库只同步误操作的表
          • 这样做也可以用上并行复制技术,来加速整个数据恢复过程
      • 两套方法共同点:误删库表后恢复数据的思路主要就是通过备份,再加上应用 binlog 的方式

        • 均要求要求备份系统定期备份全量日志,确保 binlog 在被从本地删除之前已经做了备份
      • 建议将数据恢复功能做成自动化工具并经常拿出来演练

        • 万一出现了误删事件,能够快速恢复数据,将损失降到最小,也应该不用跑路了
        • 避免出现手忙脚乱的操作,对业务造成二次伤害
  • 延迟复制备库

    • 如果有非常核心的业务,不允许太长的恢复时间,可以考虑搭建延迟复制的备库(MySQL 5.6 起
      • 延迟复制的备库是一种特殊的备库
      • 命令 CHANGE MASTER TO MASTER_DELAY = N
      • 可以指定这个备库持续保持跟主库有 N 秒的延迟(例如 N 设置成3600,则1小时后同步
    • 在N秒内发现误操作命令后,只要在未执行前在备库设置跳过误操作命令,就可以恢复需要的数据
  • 预防误删库 / 表的方法

    • 账号分离(避免写错命令
      • 只给业务开发同学 DML 权限,而不给 truncate/drop 权限(若业务需要则通过管理系统支持
      • 即使是 DBA 团队成员,日常也都规定只使用只读账号,必要的时候才使用有更新权限的账号
    • 制定操作规范(避免写错要删除的表名
      • 在删除数据表之前,必须先对表做改名操作(观察一段时间,确保无影响后再彻底删除
      • 改表名的时候,要求给表名加固定的后缀,删除必须通过管理系统且只能删除固定后缀的表
  • rm 删除数据

    • 只要不是恶意删除整个集群,而只是删除某一个节点数据的话,HA系统会自动选出一个新的主库
      • 高可用机制的 MySQL 集群不会因此造成影响(删除节点数据恢复后可再接入集群
    • SA(系统管理员)的自动化系统若误操作批量下线机器操作,可能导致MySQL集群所有节点挂
      • 建议你的备份跨机房,或者最好是跨城市保存

kill 命令

  • MySQL 中有两个 kill 命令

    • 一个是 kill query + 线程 id,表示终止这个线程中正在执行的语句
    • 一个是 kill connection + 线程 id,这里 connection 可缺省,表示断开这个线程的连接
    • 如果这个线程有语句正在执行,也是要先停止正在执行的语句的
  • kill query/connection 命令有效的场景(大多数情况

    • 执行一个查询的过程中,发现执行时间太久,通过 kill query 命令终止这条查询语句

    • 处于锁等待的时候,直接使用 kill 命令也是有效的

      img

      • kill 并不是马上停止的意思,而是告诉执行线程说,这条语句需要开始 “执行停止的逻辑了”
        • 类似于 Linux kill -N pid(并不是让进程直接停止,而是给进程发一个信号,进入终止逻辑
      • 用户执行 kill query 时,处理 kill 命令的线程做了两件事
        • 把 session B 的运行状态改成 THD::KILL_QUERY (即将变量 killed 赋值
        • 给 session B 的执行线程发一个信号(让线程退出等待,来处理 THD::KILL_QUERY 状态
      • 以上分析包含的三层意思
        • 一个语句执行过程中有多处 “埋点”,在这些 “埋点” 的地方判断线程状态(处理对应逻辑
        • 如果处于等待状态,必须是一个可以被唤醒的等待,否则根本不会执行到 “埋点” 处
        • 语句从开始进入终止逻辑,到终止逻辑完全完成,是有一个过程的
  • kill 不掉的场景

    • 场景一:innodb_thread_concurrency 不够用的例子

      • 首先执行 set global innodb_thread_concurrency=2,将 InnoDB 的并发线程上限数设置为 2

        img

      • 此时执行 show processlist ,则会显示被 kill 线程的 Commnad 列显示的是 Killed

        • 客户端虽然断开了连接,但实际上服务端上这条语句还在执行过程中
          • 只有等到满足进入 InnoDB 的条件后,session C 的查询语句继续执行
        • 显示为Killed的原因(show processlist 时有一个特别的逻辑
          • 如果一个线程的状态是 KILL_CONNECTION,就把 Command 列显示成 Killed
      • 执行 kill query 命令锁等待场景好使,而此处不好使的原因

        • 等行锁时,使用的是 pthread_cond_timedwait 函数,这个等待状态可以被唤醒
        • 此处是每10毫秒判断一下是否可以进入 InnoDB 执行,如果不行就调用 nanosleep 函数
          • 循环过程中并没有去判断线程的状态,因此根本不会进入终止逻辑
      • kill connection 命令执行流程

        • 把 12 号线程状态设置为 KILL_CONNECTION
        • 关掉 12 号线程的网络连接(客户端就能收到断开连接的提示
          • 其实即使是客户端退出了,这个线程的状态仍然是在等待中
          • 只有等到满足进入 InnoDB 的条件后,才有可能判断到线程状态并进入终止逻辑阶段
    • 场景二:由于 IO 压力过大,读写 IO 的函数一直无法返回,导致不能及时判断线程的状态

    • 场景三:终止逻辑耗时较长

      • 常见的场景有以下几种
        • 超大事务执行期间被 kill(需进行大量回滚操作
        • 大查询回滚(清理大量临时文件,可能需要等待IO资源等
        • DDL 命令执行到最后阶段(中间过程的临时文件需清理
  • 三个关于客户端的误解

    • 直接在客户端通过 Ctrl+C 命令,是不可以直接终止线程的
      • 客户端和服务端只能通过网络交互,是不可能直接操作服务端线程的
      • MySQL 是停等协议,在线程执行的语句没有返回时,在往连接里继续发命令是没有用的
      • 命令执行后实际上是另外启动一个连接,然后发送一个 kill query 命令
    • 如果库里面的表特别多,连接就会很慢
      • 我们感知到的连接过程慢,其实并不是连接慢,也不是服务端慢,而是客户端慢
      • 原因:客户端会提供一个本地库名和表名补全的功能(本地构建哈希表的操作极度耗时
      • 如果在连接命令中加上 -A,就可以关掉这个自动补全的功能,客户端就可以快速返回
    • –quick 是一个更容易引起误会的参数,也是关于客户端常见的一个误解
      • MySQL 客户端发送请求后,接收服务端返回结果的方式有两种
        • 一种是本地缓存,也就是在本地开一片内存,先把结果存起来(mysql_store_result 默认
        • 另一种是不缓存,读一个处理一个(mysql_use_result
          • 如果本地处理得慢,就会导致服务端发送结果被阻塞,因此会让服务端变慢
      • 使用这个参数可以达到以下三点效果(–quick 参数的意思,是让客户端变得更快
        • 跳过表名自动补全功能
        • 接收服务端返回结果的方式选择上诉第二种不缓存,读一个处理一个(mysql_use_result
        • 不会把执行命令记录到本地的命令历史文件

全表扫描的影响

  • 对 server 层的影响

    • 采用的是边算边发的逻辑,不会保留完整的结果集,但如果客户端读取结果不及时,会堵住查询过程

    • 栗子:mysql -h$host -P$port -u$user -p$pwd -e "select * from db1.t" > $target_file

      • 查到的每一行都可以直接放到结果集里面,然后返回给客户端(服务端并不需要保存一个完整的结果集
    • 查询结果发送流程

      img

      • 获取一行,写到 net_buffer 中(参数 net_buffer_length 定义这块内存大小

      • 重复获取行,直到 net_buffer 写满,调用网络接口发出去

      • 如果发送成功,就清空 net_buffer,然后继续取下一行,并写入 net_buffer

      • 如果发送函数返回 EAGAIN 或 WSAEWOULDBLOCK,就表示本地网络栈(socket send buffer)写满了,进入等待。直到网络栈重新可写,再继续发送

      • 根据上诉流程可以得出

        • 一个查询在发送过程中,占用的 MySQL 内部的内存最大就是 net_buffer_length 这么大,不会到 200G
        • socket send buffer 也不可能达到 200G(默认定义 /proc/sys/net/core/wmem_default)
          • 如果 socket send buffer 被写满,就会暂停读数据的流程
      • MySQL 是 “边读边发的”(如果客户端接收得慢会导致 MySQL 服务端由于结果发不出去,事务执行时间变长

        • show processlist#State 为 “Sending to client”,就表示服务器端的网络栈写满了
        • 关联知识点:-quick 参数
          • 除非真是大数据查询,否则都建议 mysql_store_result 方法(缓存在客户端本地,不影响服务端响应
        • 如何大量线程处于“Sending to client”,应让业务开发评估返回这么多结果是否合理并进行对应优化
        • 如果要快速减少处于这个状态的线程的话,将 net_buffer_length 参数设置为一个更大值是一个可选方案
    • “Sending data” 并不一定是指 “正在发送数据”,而可能是处于执行器过程中的任意阶段

      • 一个查询语句的状态变化是这样的(已忽略其他无关状态
        • MySQL 查询语句进入执行阶段后,首先把状态设置成 “Sending data”
        • 然后,发送执行结果的列相关的信息(meta data) 给客户端
        • 再继续执行语句的流程
        • 执行完成后,把状态设置成空字符串
      • “Sending data” 与 “Sending to client” 区别概要
        • 仅当一个线程处于 “等待客户端接收结果” 的状态,才会显示 “Sending to client”
        • 如果显示成 “Sending data”,它的意思只是 “正在执行”
  • 对 InnoDB 的影响

    • 由于有淘汰策略,大查询也不会导致内存暴涨,并且利用改进后的 LRU保证对 Buffer Pool 的影响也能做到可控

    • 内存的数据页是在 Buffer Pool (BP) 中管理的,在 WAL 里 Buffer Pool 起到了加速更新的作用

      • 实际上,Buffer Pool 还有一个更重要的作用,就是加速查询(Buffer Pool上的数据总是最新的,可直接读取
    • Buffer Pool 对查询的加速效果,依赖于一个重要的指标,即:内存命中率

      • show engine innodb status 结果中,查看一个系统当前的 BP 命中率(Buffer pool hit rate

        • 一般情况下,一个稳定服务的线上系统,要保证响应时间符合要求的话,内存命中率要在 99% 以上
      • BP 的大小是由参数 innodb_buffer_pool_size 确定的,一般建议设置成可用物理内存的 60%~80%

        • 如果一个 Buffer Pool 满了,而又要从磁盘读入一个数据页,那肯定是要淘汰一个旧数据页的
      • InnoDB 使用改进后的 LRU 算法

        • 是按照 5:3 的比例把整个 LRU 链表分成了 young 区域和 old 区域(靠近链表头部的 5/8 是 young 区域

          img

          • 图中状态 1,要访问数据页 P3,由于 P3 在 young 区域,因此和优化前的 LRU 算法一样,将其移到链表头部,变成状态 2
          • 之后要访问一个新的不存在于当前链表的数据页,这时候依然是淘汰掉数据页 Pm,但是新插入的数据页 Px,是放在 LRU_old 处
          • 处于 old 区域的数据页,每次被访问的时候都要做下面这个判断
            • 若这个数据页在 LRU 链表中存在的时间超过了 1 秒,就把它移动到链表头部
            • 如果这个数据页在 LRU 链表中存在的时间短于 1 秒,位置保持不变
              • 1 秒这个时间,是由参数 innodb_old_blocks_time 控制的(其默认值是 1000,单位毫秒
        • 这个策略最大的收益是在扫描这个大表的过程中,虽然也用到了 BP,但是对 young 区域完全没有影响

          • 从而保证了 Buffer Pool 响应正常业务的查询命中率
发布了98 篇原创文章 · 获赞 197 · 访问量 6万+

猜你喜欢

转载自blog.csdn.net/YangDongChuan1995/article/details/103845518
今日推荐