4. 水平扩展

156 阅读6分钟

4. 水平扩展.png

1. 读写分离

1.1 读写分离基本架构

1.1.1 业务分表:直连 connector

早期方案,每个客户端持有一个 connector,该 connector 保存主从库信息(ip 或者域名或者 VIP),并且持有读写逻辑,读语句走从库,更新语句走主库

难点在于如何同步,当 master 故障时需要主动切换,不同 connector 可能认为的 master 不同

1.1.2 proxy

切换过程:

CHANGE MASTER TO
MASTER_HOST=$host_name
MASTER_PORT=$port
MASTER_USER=$user_name
MASTER_PASSWORD=$password
master_auto_position=1

思考,下面语句应该发给主库还是读库

begin;
select ……

答案是应该发给主库,因为一个事务要求在一个 session 中处理,如果发给读库,万一后续有更新操作,就没法更新了

常见的 proxy 读写分离策略:

  1. 普通 select 才走读写分离
  2. 普通 select 提供 hint 强制走主
  3. proxy 可以将事务设置为 readonly,这样也能走读库,设置由 proxy 来实现

1.2 读写分离的业务要求

读库和写库不强一致,可能存在主从延迟,此时如果 update 后 select 要求查询最新数据,有以下几个办法:

  1. 强制走主(主库压力大)
  2. 客户端等待一定时间后查询
    • 客户端提交后弹窗让用户确认,一般确认完后刚才语句主从就同步了
    • 客户端提交后主动生成一条记录,比如发微博,发完后微博 app 主动将刚发的微博在客户端自行生成,这样用户能立刻看到发布的微博,其他用户延迟些刷新到没问题
  3. 检查从库同步情况,比如从库收到的 gtid 集合和应用的 gtid 集合是否一致,如果一致,则认为无延迟,该方法有问题,可能存在主库执行一个大事务后传输时,从库还没收完,此时收到的和应用的是一致的,但其实主从是有延迟的,而且还存这种情况,从库一致落后主库,主从一直不一致,那么从库没法提供服务了
  4. 上面方法可以用 semi-sync 解决,但是不完美
  5. 等位点,MySQL 提供语法 select master_pos_wait(file, pos, 2),主库更新完后查询下当前位点,然后去读库执行语句,2 秒内该位点执行成功,则执行查询
  6. 等 gtid,MySQL 提供语法 select wait_for_executed_gtid_set(gtid_no, 1),与等位点类似,且 MySQL 有配置 set session_track_gtids=on,打开后每次 sql 执行完成,会返回该 sql 对应的 gtid

2. 分表

2.1 如何选择分表字段

2.1.1 基于时间字段

选择时间字段优点是便于清理历史数据,比如按照月分表,每个月一张,提前申请好当前需要的表,下一年时,也需要提前申请表以及清理旧表

按照分表方式,如果数据还是存不下,怎么办?

考虑冷热分离存储,冷数据保存到其他系统,如下

2.1.2 基于业务字段

可以选择业务字段作为分表键,常见的有 tenant_id、store_id 等

2.2 数据访问模式

image.png 第一类分表查询后不用做复杂聚合,直接整合结果返回即可,后面几类聚合需要先在各个分表查出数据后进行聚合处理,然后才能返回,这种涉及 sql 改写

2.3 几种分表方案对比

3. 分库

3.1 为什么要分库

  1. 资源不足

    • 磁盘空间
    • CPU
    • I/O
    • 内存(命中率)
    • 网络带宽
  2. SLA

    • 故障影响面
    • 故障恢复速度

3.2 不分库的方案和场景

3.2.1 冷热分离/历史库

热数据放在 MySQL,冷数据放在其他系统,比如历史库、AP 系统、存储系统

3.2.2 Redis/应用层缓存

如果是内存命中率不够,可以考虑用缓存提升命中率,减少数据库压力,但是要注意一致性问题

3.3 水平分库技术方向

3.3.1 应用直连

connector 需要维护分库信息,改写 sql 语句,路由 sql 语句,但是跨库的事务怎么做?

这种跨库事务可以使用 MySQL 提供的 XA 事务来做,比如:

s1.prepare(DMLs) 
s2.prepare(DMLs) 
s1.XA_COMMIT 
s2.XA_COMMIT

s1、s2 分别表示两个库的事务,存在问题是 s1 commit 后,s2 commit 失败,比如 s2 操作的库磁盘空间满等,XA 有一个能力,将这种事务强行补回去,但是可能存在一致性问题

s1 提交,s2 还未提交时,此时其他线程来查询可能出现查询到新老数据的情况

除此外,备份也是一个问题,因为很难拿到一个一致性视图

3.3.2 proxy

image.png

proxy通过全局事务管理器能解决直连模式一致性问题,解决方法为:GTMS 生成事务 id,然后按照 MySQL MVCC 的方式得到一致性视图

4. 其他水平扩展方案

4.1 节点间协议

事务一致性由节点间通过一致性协议来实现,proxy 仅作转发,至少需要三个节点,因为一致性协议最少就要求 3 节点

一般只有一个更新节点,该节点分发给其他节点更新操作

可以看到这种方案中,proxy 功能已经非常单薄了,因此有些数据库也会将 proxy 去掉,如下:

该方案跟 MGR 区别为 MGR 每个库都是全数据,只是在库之间利用一致性协议确保数据一致性,MGR 本质是主备,而分布式数据库是指数据只有一份,分散存储在不同节点,MGR 原理如下:

4.2 共享存储方案——存储计算分离

以亚马逊的欧若拉(Amazon Aurora)为代表只有一个写 MySQL 服务器,其余都是读 MySQL 服务器,这样如果需要水平扩展读,很简单,拉一个容器启动一个 mysqld 进程即可,算云原生的分布式数据库

难点:

  • 共享存储:MySQL 本身不允许一份数据多个进程对其操作
  • MySQL 存储和 MySQL 进程不在同一服务内,之间是网络,MySQL 的设计是是引擎层和 server 交互非常频繁,修改成存储计算分离后,如果还这样频繁,那由于网络传输耗时,性能可能下降较多,因此需要尽量做计算下推,整块的返回数据,写数据也需要下推,利用 redo 直接更新数据,少了脏页刷新的带宽

问答

  1. 按照业务字段分表后,还要按时间分表吗?

    看情况,订单和日志这种建议分,涉及后续清理和迁移

  2. 大的分区表执行 DDL 有没有推荐方案?

    推荐使用 gh-ost,如果有主备,建议使用主备,不过从规范性角度来看,建议 DDL 统一用 gh-ost

  3. 执行查询某些列,引擎层会给 server 层返回哪些数据?

    如果 page 没在内存,innodb 去磁盘读, 需要读整个 page,然后只需要把 shop_id, row_id 拷贝给 server 层。