MySQL5_1高可用集群

主从同步

MySQL的主从同步是基于bin log实现的。

bin log 同步流程

MySQL主从复制
备库 B 和主库 A 之间维持了一个长连接,主库 A 内部有一个线程专门服务于与 B 的 bin log 同步,一个事务日志同步的过程如下:

  1. 在备库 B 上通过 change master 命令,设置主库 A 的 IP、端口、用户名、密码,以及要从哪个位置开始请求 binlog,这个位置包含文件名和日志偏移量;
  2. 在备库 B 上执行 start slave 命令,这时候备库会启动两个线程,就是图中的 io_threadsql_thread。其中 io_thread 负责与主库建立连接。
  3. 主库 A 校验完用户名、密码后,开始按照备库 B 传过来的位置,从本地读取 bin log,发给 B。
  4. 备库 B 拿到 bin log 后,写到本地文件,称为中转日志(relay log)。
  5. sql_thread 读取中转日志,解析出日志里的命令,并执行。

binlog格式

MySQL的bin log主要支持三种格式,分别是statement、row以及mixed。MySQL是在5.1.5版本开始支持row的、在5.1.8版本中开始支持mixed。

  • statement:binlog中记录的是SQL语句的原文
    容易引起主从不一致,因为binlog是按事务提交顺序记录的,但是两个并发执行的事务中update语句可能是乱序的。
  • row:记录原数据

主备延迟

产生主备延迟的可能情况:

  1. 备库所在的机器性能较主库差;
  2. 备库的压力较大,比如因为备库不跑业务,所以很多人会随意执行一些特别耗时的操作,这些查询耗费大量的 CPU 资源,影响了同步速度,造成主备延迟。
  3. 出现了大事务,比如,一次性用 delete 语句删除大量数据,或者大表的 DDL。
  4. 一个服务器开放N个链接给客户端来连接,但是Slave里读取binlog的线程只有一个,当某个SQL在Slave上执行的时间稍长或由于某个SQL要进行锁表就会导致Master的SQL大量积压,未被同步到Slave,这就导致了主从不一致,也就是主从延迟。

并行复制策略

为了避免备库追不上主库的情况,MySQL 利用并行复制策略提高复制的效率,从上面的主备同步流程图可知,并行化可以加到客户端连接和写入数据两个过程中。

  1. 按表分发策略
    如果两个事务更新不同的表,它们就可以并行。因为数据是存储在表里的,所以按表分发,可以保证两个 worker 不会更新同一行。
    当然,如果有跨表的事务,还是要把两张表放在一起考虑的。
  2. 按行分发策略
    按表复制存在热点表的并行复制问题,即热点表会被分配给一个 worker 执行复制,这样就会退化成单线程复制。
    按行复制的核心思路是:如果两个事务没有更新相同的行,则它们在备库上可以并行执行,为了知道具体修改了哪些行,这种模式需要设置 binlog 的格式为 row(因为 statement 格式直接记录更新语句,row 记录的是受影响的具体数据的 ID)。

半同步

在 MySQL 5.5 版本之前一直采用的是上述的异步复制方案,主库的事务执行不会管备库的同步进度,如果备库落后,主库不幸 crash,那么就会导致数据丢失。
于是在 MySQL 在 5.5 中就顺其自然地引入了半同步复制,主库在应答客户端提交的事务前需要保证至少一个从库接收并写到 relay log 中。

异步 & 半同步 & 全同步

  • 对于异步复制,主库将事务 Binlog 事件写入到 bin log 文件中,此时主库只会通知一下 Dump 线程发送这些新的 Binlog,然后主库就会继续处理提交操作,而此时不会保证这些 Binlog 传到任何一个从库节点上。
  • 对于全同步复制,当主库提交事务之后,所有的从库节点必须收到,APPLY 并且提交这些事务,然后主库线程才能继续做后续操作。这里面有一个很明显的缺点就是,主库完成一个事务的时间被拉长,性能降低。
  • 对于半同步复制,是介于全同步复制和异步复制之间的一种,主库只需要等待至少一个从库节点收到并且 Flush Binlog 到 Relay Log 文件即可,主库不需要等待所有从库给主库反馈。同时,这里只是一个收到的反馈,而不是已经完全执行并且提交的反馈,这样就节省了很多时间。

主备切换

主备功能主要是通过 bin log 实现的。

主备切换流程

MySQL-主备切换流程
如图示,客户端的读写都是直接访问的节点 A,而节点 B 是 A 的备库,通常是只读的,只是将 A 的更新同步过来到本地执行,节点 A 的 update 同步到节点 B 的流程图如下所示:
MySQL-主备同步流程图

  1. 主库接收到客户端的更新请求后,执行内部事务的更新逻辑,同时写 binlog;
  2. 备库 B 跟主库 A 之间维持了一个长连接,专门用于服务备库 B 的事务日志同步;

当需要切换时,切换到状态 2,这时候客户端读写访问的都是节点 B,而节点 A 是 B 的备库;

双 M 架构

实际生产中更多采用的是双 M 架构:
MySQL-双M架构主备切换流程
与原先的方案相比,只是节点 A 和 B 之间多了一条线,这样,节点 A 和 B 之间总是互为主备关系,在切换的时候就不用再修改主备关系。

可靠性优先策略

双 M 结构的可靠性优先主备切换流程如下:

  1. 判断备库 B 现在的 seconds_behind_master,如果小于某个值(比如 5 秒)继续下一步,否则持续重试这一步;
  2. 把主库 A 改成只读状态,即把 readonly 设置为 true;
  3. 判断备库 B 的 seconds_behind_master 的值,直到这个值变成 0 为止;
  4. 把备库 B 改成可读写状态,也就是把 readonly 设置为 false;
  5. 把业务请求切到备库 B。

这个切换流程一般由专门的 HA 系统来完成,称为可靠性优先流程
注意:

  1. 这个过程中,比较耗时的是第 3 步,可能会耗费好几秒的时间,因此一般会先在第 1 步中做判断,确保 seconds_behind_master 足够小后才执行。

可用性优先策略

可靠性优先策略中,同步流程中存在一段系统不可用的时间,如果强行把步骤 4、5 调整到最开始执行,也就是说不等主备数据同步,直接把连接切到备库 B,并且让备库 B 可以读写,那么系统几乎就没有不可用时间了,这个流程称为可用性优先策略。

一主多从

平时使用数据库一般都是读多写少,在发展过程中很可能会先遇到读性能问题,为了解决读性能问题,在架构上的解决方式是一主多从
MySQL-一主多从基本结构
其中:

  • A 和 A’互为主备;
  • 从库 B、C、D 指向主库 A,主库负责所有写入和一部分读,其他的读请求由从库分担。

MySQL-一主多从的主备切换

  • 主备切换后,A’将成为新的主库;
  • 从库 B、C、D 改成连接到 A’。

读写分离

上述的主从结构其实形成了一种读写分离的架构,连接信息一般保存到客户端,由客户端执行负载均衡。
另一种读写分离架构在客户端和服务器之间架设了一个代理层 proxy,客户端全部连接到这个 proxy,由 proxy 根据请求类型和上下文执行请求的路由分发。
MySQL-带proxy的读写分离架构

  • 直连的架构,少了一层 proxy,因此性能稍微更好一点,排查问题也更方便,但是主备切换库迁移时客户端会感知到,所以客户端需要一个后端管理组件,比如 Zookeeper。
  • 带 proxy 架构,对客户端友好,但是同时 proxy 架构也更加复杂。

“过期读”问题

当客户端先写入再读取时可能会读到修改前的值,因为写入是对主库写入,读取是对从库读,而主从同步存在延迟,刚写入主库的数据可能还没有同步到所有的从库。
解决过期读问题的方案:

  • 强制走主库方案;
    一些必须拿到最新结果的请求,可以强制将其发到主库上,比如用户支付后需要马上看到商品是否已经购买成功,这个请求需要马上拿到最新的结果,因此最好走主库;
    一些请求没有必要立刻拿到最新的结果,比如商户发布商品后,用户即使没有马上看到商品也是可以的,因此用户读取商品列表的请求完全可以路由到从库上去。
  • sleep 方案;
    不大靠谱,但是一定程度上还是可以解决问题的。
  • 判断主备无延迟方案;
    判断 show slave status 结果里的 seconds_behind_master 参数的值,等于 0 才执行查询请求,这个参数可以表明从库是否已经完全同步。
  • 配合 semi-sync 方案;
  • 等主库位点方案;
  • 等 GTID 方案。

探活

在一主一备的双 M 架构里,主备切换只需要把客户端流量切到备库;而在一主多从架构里,主备切换除了要把客户端流量切到备库外,还需要把从库接到新主库上。
主备切换有两种场景:主动切换和被动切换,其中被动切换往往是因为主库出问题而由 HA 系统发起的。

select 1

select 1只能用于判断该数据库进程仍能执行,但是不能说明主库没有问题,比如,数据库线程池(由参数 innodb_thread_concurrency 控制)被打满的情况下,虽然select 1能执行,但是线程池还是会被堵住。

innodb_thread_concurrency 控制的是并发查询,而不是并发连接,因为并发连接多只是多占用一些内存空间,并不会占用 CPU 资源。

查表判断

为了知道线程池是否被打满,我们可以创建一张health_check表,里面只放一条数据,然后定时执行:

1
select * from mysql.health_check;

这种方法的缺点是,不能用于判断磁盘空间是否满了,因为如果磁盘空间满了,所有的更新语句和事务提交语句都会被堵塞,但是查询语句仍能执行。

更新判断

更新一行数据,一般会放一个 timestamp 字段,用来表示最后一次执行检测的时间。
但是要注意如果主备都要检测,就不能只有一行数据了,因为会产生行冲突,导致主备同步的停止。一般会采用数据库实例的 server_id 作为主键,因为 MySQL 规定了主备服务器的 server_id 必须不同,这样就能保证主备的检测命令不会冲突了。
这种方式仍然存在一种问题:这种更新语句占用的 IO 资源很少,即使当时 IO 已经 100%,检测语句仍可以获得 IO 资源来执行,但系统可能已经出问题了,也就是说,这种检测存在随机性。

内部统计

前面几种方法都是通过外部调用来发现问题的,更好的方式是利用 MySQL 本身的统计数据:performance_schema库的file_summary_by_event_name表。

QA

主从同步的流程

主备服务器之间维持了一个长连接,备库上回启动两个线程,一个 io_thread 负责与主库建立连接并读取 bin log,另一个 sql_thread 负责解析命令并执行。

MySQL 是怎么保证数据不丢失的

MySQL 是怎么保证高可用的

MySQL如何优化千万级的大表?

  1. 优化SQL和索引;
  2. 加缓存,比如Memcached或Redis;
  3. 主从复制或主主复制,实现读写分离
    可以在应用层做,效率高
    也可以用三方工具,如360的atlas
  4. 使用MySQL自带的分区表
    优点是对应用透明,但是SQL需要针对分区表做一些优化,sql条件中要带上分区条件的列,从而使查询定位到少量的分区上,否则就会扫描全部分区。
  5. 垂直拆分,根据模块耦合情况将一个大系统分为多个小系统
  6. 水平切分,选择合适的sharding key将大表数据拆分到多个小表上

参考

  1. MySQL 5.7 半同步复制技术