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 读写分离策略:
- 普通 select 才走读写分离
- 普通 select 提供 hint 强制走主
- proxy 可以将事务设置为 readonly,这样也能走读库,设置由 proxy 来实现
1.2 读写分离的业务要求
读库和写库不强一致,可能存在主从延迟,此时如果 update 后 select 要求查询最新数据,有以下几个办法:
- 强制走主(主库压力大)
- 客户端等待一定时间后查询
- 客户端提交后弹窗让用户确认,一般确认完后刚才语句主从就同步了
- 客户端提交后主动生成一条记录,比如发微博,发完后微博 app 主动将刚发的微博在客户端自行生成,这样用户能立刻看到发布的微博,其他用户延迟些刷新到没问题
- 检查从库同步情况,比如从库收到的 gtid 集合和应用的 gtid 集合是否一致,如果一致,则认为无延迟,该方法有问题,可能存在主库执行一个大事务后传输时,从库还没收完,此时收到的和应用的是一致的,但其实主从是有延迟的,而且还存这种情况,从库一致落后主库,主从一直不一致,那么从库没法提供服务了
- 上面方法可以用 semi-sync 解决,但是不完美
- 等位点,MySQL 提供语法
select master_pos_wait(file, pos, 2),主库更新完后查询下当前位点,然后去读库执行语句,2 秒内该位点执行成功,则执行查询 - 等 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 数据访问模式
第一类分表查询后不用做复杂聚合,直接整合结果返回即可,后面几类聚合需要先在各个分表查出数据后进行聚合处理,然后才能返回,这种涉及 sql 改写
2.3 几种分表方案对比
3. 分库
3.1 为什么要分库
-
资源不足
- 磁盘空间
- CPU
- I/O
- 内存(命中率)
- 网络带宽
-
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
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 直接更新数据,少了脏页刷新的带宽
问答
-
按照业务字段分表后,还要按时间分表吗?
看情况,订单和日志这种建议分,涉及后续清理和迁移
-
大的分区表执行 DDL 有没有推荐方案?
推荐使用 gh-ost,如果有主备,建议使用主备,不过从规范性角度来看,建议 DDL 统一用 gh-ost
-
执行查询某些列,引擎层会给 server 层返回哪些数据?
如果 page 没在内存,innodb 去磁盘读, 需要读整个 page,然后只需要把 shop_id, row_id 拷贝给 server 层。