动态代理 MySQL slave 端口

背景介绍

一直以来我们的数据库主从架构都以 vip 作为高可用的基石, 通过 vip + MHA 的方式完成 master 的高可用, 并未对 slave 进行相关的高可用设计. 随着时间的推移, 为了减少一些业务对 master 的繁重的操作, 线上的一小部分业务开始连接 slave 对外提供服务. 这些常见的业务包括以下类型:

1. op 相关的工程, 统计类大查询;
2. 日志分析类的查询操作;
3. 使用 atlas 进行读写的业务;
4. 主从延迟无关, 以读为主的业务;

我们现有的业务中, 并未使用 atlas, proxysql 等中间件, 所以 op 后台类的业务查询都通过 slave 服务. 不过在过往的几次故障案例中, MHA 切换主从后会出现以下情况:

    +--------+                                       +--------+
    |   vip  |                                       |   vip  |
    +--------+                                       +--------+
         |                               MHA              |
         |                             ------>            |
    +--------+           +-------+                   +--------+
    | master |  -------- | slave |                   |  slave |
    +--------+           +-------+                   +--------+

在 MHA 切换主从后, 原有的 slave 变更为新的 master, 而 op 相关的工程则同样连接着新的 master, 而不是其它可用的 slave. 这样业务在做大查询的时候依旧会出现卡慢的情况. 为了解决这种问题, 我们需要提供一种平滑且稳定的方式供应用访问 slave. 这种方式应该包含以下特性:

1. 一直提供可用的 slave 信息;
2. 如果没可用的 slave, 则提供相应的 master 信息;
3. 故障的时候平滑的切换 slave 信息以免影响业务;
4. 访问量骤变的时候应该有连接数等资源相关的限制;

这些需求强制我们必须要持续的检测可用的 slave. 如果业务优先级不高, 则可以使用定时任务计划进行检测.

实现的方式

从上述的需求来看大致可以通过 vip, dns, 自动发现和代理四种方式实现. 这些方式都有各自的缺陷及优点, 下面会单独介绍不同的方式, 其中代理是我们本文要详细介绍的, 如何动态代理也会详加说明.

vip 方式

vip 方式类似上述的 vip + MHA 方式, 只是这里的 vip 只服务 slave, 执行 MHA 后顺便将 slave 的 vip 也切换掉, 相当的简单方便. 不过这种方式有几个弊端:

1. 正执行的 sql 会异常中断;
2. 应用需要有重连机制;
3. vip 切换脚本自己实现或通过服务发现的方式进行任务触发;
4. 主从实例过多的时候会很混乱;
5. 需要监听 0.0.0.0:xxx 地址, 不能只监听单个 ip;

实际上第一个弊端是难以避免的, 只要不是原先的 slave, 正在执行的 sql 都有可能中断或者程序执行 sql 后无响应. 重连机制的特性很重要, 不管是 vip 的后面是 master 还是 slave, 最后都需要有重连机制. 另外主从实例很多的时候, vip 方式就会显得很混乱, 这种情况下使用 dns 方式连接反倒更省事. 监听地址也不能为单个 ip, 否则 ip 切换后端口是不会生效的, 需要重新加载程序.

dns 方式

尽管从整个业务架构上来看, 引入 dns 增加了很多的不稳定性, 但是在服务过多的情况下, dns 的的确确带来了很大的方便. 在做 MHA 切换或者其它故障切换的操作后, 只要更新相关的 dns 条目后即可指向正确的配置. 不过使用 dns 方式同样存在以下弊端:

1. 很多程序(比如 jvm)都存在缓存 dns 的特性, 故障后缓存时间内的 dns 请求都会失效;
2. ttl 时间问题;
3. dns 服务的高可用问题;

缓存问题确实很难解决, 传统的架构下只能在效率和缓存时间之间做个平衡. 新兴的工具, 比如 consul 提供的 dns 接口本身自带了故障转移功能, 如果能够使用 consul 集群对外服务则上述的三个问题都能相对较好的解决.

服务发现

服务发现这种方式其实并不通用, 它是自动化较高的一种方式, 比如国内淘宝和新浪使用较多的 zookeeper, 又或者新兴的 etcdconsul 工具, 都需要应用在代码层面做相应的调整. 整体上的流程类似下面的说明:

1. 应用注册相关服务到 zookeeper/etcd/consul;
2. 获取可用的连接信息;
3. 出现故障的时候, 检测程序更新 zookeeper/etcd/consul 中的相关条目;
4. 将更新后的条目通知到注册的应用;
5. 应用使用新的连接信息重新加载配置;

这种方式一旦实现后基本上就解放了管理员的双手, 虽然过程复杂点, 却值得作为我们努力的方向.

代理

代理的方式则相对复杂, 很多管理员为了方便省事, 会将代理工具(比如 haproxy)结合到一些第三方的自动发现工具中进而自动更新代理的配置, 又或者直接代理一组 ip 地址, 出现问题的时候转发请求即可, 有实力的公司则使用自研的中间件代理工具. 前两种方式现在使用的都比较普遍. 具体的则如下图所示:

代理工具 + 自动发现

                     +-----------------------+    <update>  +-------+
                     | zookeeper/etcd/consul |   <--------- | monit |
                     +-----------------------+              +-------+
                                |                              |
                                | <auto update/reload>         | <check servers> 
                                |                              |
                                |                     +---------------+
       +---------+          +---------+               | real server 1 |
       | request |   ---->  | haproxy |   --------->  | real server 2 |
       +---------+          +---------+               | ......        |
                                                      +---------------+

我们将需要代理的 ip 预先存到服务发现工具中, 在做 MHA 或者其它故障切换的时候, monit 程序检测后端的 server 及时更新自动发现中的条目, 触发程序(比如 confdconsul-template)更新 haproxy 的配置并重新加载haproxy. 在这种流程中, request 到 haproxy 的过程不会改变, 后端的 real server 有任何变更则会通过 monit 程序更新到自动发现条目中. 整个过程基本不需要管理员的干预. 如果觉得单个 haproxy 不够稳健, 大家可以在 haproxy 之前加上 keepalived 实现高可用架构.

实际上整个流程中 monit 程序对后端服务的检测至关重要, 具体的可细分如下:

1. 集群或多节点服务, server 之间没有多大的关联性, monit 仅检测相关服务可用性即可;
2. 主从节点的服务, server 之间关联性很强, 比如 redis/mysql 主从, monit 不仅要做可用性检测, 还要做主从相关的检测;

在写优先级较高的业务中, monit 检测就会显得更为重要, 随便切换则很容易引起数据的不一致. 对于一些特性的工具, 我们总能在开源社区中找到一些有意思的项目, 比如以下链接

redishappy : 程序检测 redis sentinel 的主从变更并更新条目到 haproxy 或 consul, 最后重新加载代理工具; redundis : redundis 则是简易的代理工具, 通过 redis sentinel 检测 master, 再将请求代理到最新可用的 master;

两个工具目前为止还没有 release 版本, 并且资源及连接数限制等在并发较高的情况下估计也没有老牌的 haproxy 更让人放心. 所以我们的工程 confd_haproxy 则使用类似的方式结合 confdhaproxy 实现服务端口的动态代理, checkmysqlslave 一直检测可用的 slave(没有 slave 则提供 master), 有任何变化则更新后端的 consul, confd 则加载 haproxy 配置. 类似的, memcached, redis, http 等服务的检测也可以基于这种方式实现. 不过要谨记上述 monit 程序的重要性, 有主从关联的服务尤为注意.

ip 池代理

ip 池代理有点类似 haproxy 代理一组服务的感觉, 一些开源软件(比如 gorb , gobetween) 以及我们熟知的 shadowsocks 都有类似的功能, 不过故障的转移及剔除则是自动进行, 有些工具可以与自动发信结合实现服务的动态扩展. 如下所示:

                                                          +----------+
     +---------+                  +-----------+           | server 1 |
     | request |     ---------->  |   proxy   |  -------> | server 2 |
     +---------+                  +-----------+           | ...      |
                                                          +----------+

中间件

开源社区为我们提供了众多的可以作为中间件的软件, 这些工具实际上类似上面介绍的 haproxy 方式, 只不过他们增加了更有针对性的特性支持, 比如主从分离请求, 主从变更检测, 协议支持, 自动哈希等, 这些工具都工作在应用层, 上述我们介绍的 haproxy 代理则工作在 tcp/ip 层. 常见的 DB 端中间件工具以下:

mycat : MySQL 代理; proxysql : MySQL 代理; kingshard : MySQL 代理; redis-sentinel : redis sentinel 工具; predixy: redis 代理; codis : redis 代理; mcrouter : memcached 代理; twemproxy : redis/memcached 代理;

不过这些工具和本文讨论的主题关系不太大, 除非加上动态更新服务的功能. 不过从实际的使用情况来看, 小工程更适合使用动态代理的方式, 尤其是有驱动接口更新的业务, 比如 redis 主从切换到 redis sentinel, 几乎所有的应用程序驱动都需要更新 redis 连接方式以支持 sentinel 特性, 如果有些语言不支持 sentinel 还需要开发者额外开发. 这种情况下如果有动态代理(比如上述的 redundis) 就会减少很多开发成本.

总结

服务发现和动态代理能够带给我们很大的灵活性, 也让我们向自动化维护迈进了一步. 实际上结合传统的工具(比如 haproxy) 和新兴的 consul/etcd 等工具已经能够满足我们大多数的需求. 在本文的介绍中我们使用了 MySQL slave 作为介绍, 实际上大家也可以参考 confd_haproxy 完成其它端口(比如 redis, memcached, 其它 tcp 端口等)的动态代理. 我们也会持续更新代码完善检测程序以代理更多的服务.