MySQL 高可用浅析
对于多数应用来说,MySQL 都是作为最关键的数据存储中心的,所以,如何让 MySQL 提供 HA 服务,是我们不得不面对的一个问题。当 master 当机的时候,我们如何保证数据尽可能的不丢失,如何保证快速的获知 master 当机并进行相应的故障转移处理,都是需要我们好好思考的。这里,笔者将结合这段时间做的 MySQL proxy 以及 toolsets 相关工作,说说我们现阶段以及后续会在项目中采用的 MySQL HA 方案。
Replication
要保证 MySQL 数据不丢失,replication 是一个很好的解决方案,而 MySQL 也提供了一套强大的 replication 机制。只是我们需要知道,为了性能考量,replication 是采用的 asynchronous 模式,也就是写入的数据并不会同步更新到 slave 上面,如果这时候 master 当机,我们仍然可能会面临数据丢失的风险。
为了解决这个问题,我们可以使用 semi-synchronous replication,semi-synchronous replication 的原理很简单,当 master 处理完一个事务,它会等待至少一个支持 semi-synchronous 的 slave 确认收到了该事件并将其写入 relay-log 之后,才会返回。这样即使 master 当机,最少也有一个 slave 获取到了完整的数据。
但是,semi-synchronous 并不是 100%的保证数据不会丢失,如果 master 在完成事务并将其发送给 slave 的时候崩溃,仍然可能造成数据丢失。只是相比于传统的异步复制,semi-synchronous replication 能极大地提升数据安全。更为重要的是,它并不慢,MHA 的作者都说他们在 facebook 的生产环境中使用了 semi-synchronous( 这里 ),所以我觉得真心没必要担心它的性能问题,除非你的业务量级已经完全超越了 facebook 或者 google。在 这篇 文章里面已经提到,MySQL 5.7 之后已经使用了 Loss-Less Semi-Synchronous replication,所以丢数据的概率已经很小了。
如果真的想完全保证数据不会丢失,现阶段一个比较好的办法就是使用 gelera ,一个 MySQL 集群解决方案,它通过同时写三份的策略来保证数据不会丢失。笔者没有任何使用 gelera 的经验,只是知道业界已经有公司将其用于生产环境中,性能应该也不是问题。但 gelera 对 MySQL 代码侵入性较强,可能对某些有代码洁癖的同学来说不合适了:-)
我们还可以使用 drbd 来实现 MySQL 数据复制,MySQL 官方文档有一篇文档有详细 介绍 ,但笔者并未采用这套方案,MHA 的作者写了一些采用 drdb 的问题, 在这里 ,仅供参考。
在后续的项目中,笔者会优先使用 semi-synchronous replication 的解决方案,如果数据真的非常重要,则会考虑使用 gelera。
Monitor
前面我们说了使用 replication 机制来保证 master 当机之后尽可能的数据不丢失,但是我们不能等到 master 当了几分钟才知道出现问题了。所以一套好的监控工具是必不可少的。
当 master 当掉之后,monitor 能快速的检测到并做后续处理,譬如邮件通知管理员,或者通知守护程序快速进行 failover。
通常,对于一个服务的监控,我们采用 keepalived 或者 heartbeat 的方式,这样当 master 当机之后,我们能很方便的切换到备机上面。但他们仍然不能很即时的检测到服务不可用。笔者的公司现阶段使用的是 keepalived 的方式,但后续笔者更倾向于使用 zookeeper 来解决整个 MySQL 集群的 monitor 以及 failover。
对于任何一个 MySQL 实例,我们都有一个对应的 agent 程序,agent 跟该 MySQL 实例放到同一台机器上面,并且定时的对 MySQL 实例发送 ping 命令检测其可用性,同时该 agent 通过 ephemeral 的方式挂载到 zookeeper 上面。这样,我们可以就能知道 MySQL 是否当机,主要有以下几种情况:
- 机器当机,这样 MySQL 以及 agent 都会当掉,agent 与 zookeeper 连接自然断开
- MySQL 当掉,agent 发现 ping 不通,主动断开与 zookeeper 的连接
- Agent 当掉,但 MySQL 未当
上面三种情况,我们都可以认为 MySQL 机器出现了问题,并且 zookeeper 能够立即感知。agent 与 zookeeper 断开了连接,zookeeper 触发相应的 children changed 事件,监控到该事件的管控服务就可以做相应的处理。譬如如果是上面前两种情况,管控服务就能自动进行 failover,但如果是第三种,则可能不做处理,等待机器上面 crontab 或者 supersivord 等相关服务自动重启 agent。
使用 zookeeper 的好处在于它能很方便的对整个集群进行监控,并能即时的获取整个集群的变化信息并触发相应的事件通知感兴趣的服务,同时协调多个服务进行相关处理。而这些是 keepalived 或者 heartbeat 做不到或者做起来太麻烦的。
使用 zookeeper 的问题在于部署起来较为复杂,同时如果进行了 failover,如何让应用程序获取到最新的数据库地址也是一个比较麻烦的问题。
对于部署问题,我们要保证一个 MySQL 搭配一个 agent,幸好这年头有了 docker,所以真心很简单。而对于第二个数据库地址更改的问题,其实并不是使用了 zookeeper 才会有的,我们可以通知应用动态更新配置信息,VIP,或者使用 proxy 来解决。
虽然 zookeeper 的好处很多,但如果你的业务不复杂,譬如只有一个 master,一个 slave,zookeeper 可能并不是最好的选择,没准 keepalived 就够了。
Failover
通过 monitor,我们可以很方便的进行 MySQL 监控,同时在 MySQL 当机之后通知相应的服务做 failover 处理,假设现在有这样的一个 MySQL 集群,a 为 master,b,c 为其 slave,当 a 当掉之后,我们需要做 failover,那么我们选择 b,c 中的哪一个作为新的 master 呢?
原则很简单,哪一个 slave 拥有最近最多的原 master 数据,就选哪一个作为新的 master。我们可以通过 show slave status
这个命令来获知哪一个 slave 拥有最新的数据。我们只需要比较两个关键字段 Master_Log_File
以及 Read_Master_Log_Pos
,这两个值代表了 slave 读取到 master 哪一个 binlog 文件的哪一个位置,binlog 的索引值越大,同时 pos 越大,则那一个 slave 就是能被提升为 master。这里我们不讨论多个 slave 可能会被提升为 master 的情况。
在前面的例子中,假设 b 被提升为 master 了,我们需要将 c 重新指向新的 master b 来开始复制。我们通过 CHANGE MASTER TO
来重新设置 c 的 master,但是我们怎么知道要从 b 的 binlog 的哪一个文件,哪一个 position 开始复制呢?
GTID
为了解决这一个问题,MySQL 5.6 之后引入了 GTID 的概念,即 uuid:gid,uuid 为 MySQL server 的 uuid,是全局唯一的,而 gid 则是一个递增的事务 id,通过这两个东西,我们就能唯一标示一个记录到 binlog 中的事务。使用 GTID,我们就能非常方便的进行 failover 的处理。
仍然是前面的例子,假设 b 此时读取到的 a 最后一个 GTID 为 3E11FA47-71CA-11E1-9E33-C80AA9429562:23
,而 c 的为 3E11FA47-71CA-11E1-9E33-C80AA9429562:15
,当 c 指向新的 master b 的时候,我们通过 GTID 就可以知道,只要在 b 中的 binlog 中找到 GTID 为 3E11FA47-71CA-11E1-9E33-C80AA9429562:15
这个 event,那么 c 就可以从它的下一个 event 的位置开始复制了。虽然查找 binlog 的方式仍然是顺序查找,稍显低效暴力,但比起我们自己去猜测哪一个 filename 和 position,要方便太多了。
google 很早也有了一个 Global Transaction ID 的补丁,不过只是使用的一个递增的整形, LedisDB 就借鉴了它的思路来实现 failover,只不过 google 貌似现在也开始逐步迁移到 MariaDB 上面去了。
MariaDB 的 GTID 实现跟 MySQL 5.6 是不一样的,这点其实比较麻烦,对于我的 MySQL 工具集 go-mysql 来说,意味着要写两套不同的代码来处理 GTID 的情况了。后续是否支持 MariaDB 再看情况吧。
Pseudo GTID
GTID 虽然是一个好东西,但是仅限于 MySQL 5.6+,当前仍然有大部分的业务使用的是 5.6 之前的版本,笔者的公司就是 5.5 的,而这些数据库至少长时间也不会升级到 5.6 的。所以我们仍然需要一套好的机制来选择 master binlog 的 filename 以及 position。
最初,笔者打算研究 MHA 的实现,它采用的是首先复制 relay log 来补足缺失的 event 的方式,但笔者不怎么信任 relay log,同时加之 MHA 采用的是 perl,一个让我完全看不懂的语言,所以放弃了继续研究。
幸运的是,笔者遇到了 orchestrator 这个项目,这真的是一个非常神奇的项目,它采用了一种 Pseudo GTID 的方式,核心代码就是这个
create database if not exists meta;
drop event if exists meta.create_pseudo_gtid_view_event;
delimiter ;;
create event if not exists
meta.create_pseudo_gtid_view_event
on schedule every 10 second starts current_timestamp
on completion preserve
enable
do
begin
set @pseudo_gtid := uuid();
set @_create_statement := concat('create or replace view meta.pseudo_gtid_view as select \'', @pseudo_gtid, '\' as pseudo_gtid_unique_val from dual');
PREPARE st FROM @_create_statement;
EXECUTE st;
DEALLOCATE PREPARE st;
end
;;
delimiter ;
set global event_scheduler := 1;
它在 MySQL 上面创建了一个事件,每隔 10s,就将一个 uuid 写入到一个 view 里面,而这个是会记录到 binlog 中的,虽然我们仍然不能像 GTID 那样直接定位到一个 event,但也能定位到一个 10s 的区间了,这样我们就能在很小的一个区间里面对比两个 MySQL 的 binlog 了。
继续上面的例子,假设 c 最后一次出现 uuid 的位置为 s1,我们在 b 里面找到该 uuid,位置为 s2,然后依次对比后续的 event,如果不一致,则可能出现了问题,停止复制。当遍历到 c 最后一个 binlog event 之后,我们就能得到此时 b 下一个 event 对应的 filename 以及 position 了,然后让 c 指向这个位置开始复制。
使用 Pseudo GTID 需要 slave 打开 log-slave-update
的选项,考虑到 GTID 也必须打开该选项,所以个人感觉完全可以接受。
后续,笔者自己实现的 failover 工具,将会采用这种 Pseudo GTID 的方式实现。
在《MySQL High Availability》这本书中,作者使用了另一种 GTID 的做法,每次 commit 的时候,需要在一个表里面记录 gtid,然后就通过这个 gtid 来找到对应的位置信息,只是这种方式需要业务 MySQL 客户端的支持,笔者不很喜欢,就不采用了。
后记
MySQL HA 一直是一个水比较深的领域,笔者仅仅列出了一些最近研究的东西,有些相关工具会尽量在 go-mysql 中实现。
更新
经过一段时间的思考与研究,笔者又有了很多心得与收获,设计的 MySQL HA 跟先前有了很多不一样的地方。后来发现,自己设计的这套 HA 方案,跟 facebook 这篇 文章 几乎一样,加之最近跟 facebook 的人聊天听到他们也正在大力实施,所以感觉自己方向是对了。
新的 HA,我会完全拥抱 GTID,比较这玩意的出现就是为了解决原先 replication 那一堆问题的,所以我不会考虑非 GTID 的低版本 MySQL 了。幸运的是,我们项目已经将 MySQL 全部升级到 5.6,完全支持 GTID 了。
不同于 fb 那篇文章将 mysqlbinlog 改造支持 semi-sync replication 协议,我是将 go-mysql 的 replication 库支持 semi-sync replication 协议,这样就能实时的将 MySQL 的 binlog 同步到一台机器上面。这可能就是我和 fb 方案的唯一区别了。
只同步 binlog 速度铁定比原生 slave 要快,毕竟少了执行 binlog 里面 event 的过程了,而另外真正的 slaves,我们仍然使用最原始的同步方式,不使用 semi-sync replication。然后我们通过 MHA 监控整个集群以及进行故障转移处理。
以前我总认为 MHA 不好理解,但其实这是一个非常强大的工具,而且真正看 perl,发现也还是看的懂得。MHA 已经被很多公司用于生产环境,经受了检验,直接使用绝对比自己写一个要划算。所以后续我也不会考虑 zookeeper,考虑自己写 agent 了。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论