数据库作为IT系统中最关键的服务之一,其可用性一直是系统设计中的重点考虑因素。同时,由于数据库有数据有状态的天性,数据库高可用有其天然的复杂性和难点,云原生架构下尤其如此,是一个值得深入探讨的课题。本系列文章将基于网易杭州研究院的研究与实践,解析数据库高可用技术要点,梳理主流数据库方案,为数据库技术建设规划提供参考。
本文由作者授权网易云发布,未经许可,请勿转载!
作者:倪山三,网易杭州研究院运维工程师
Catalog0. Prologue
1. 数据库高可用技术要点1.1. 冗余设计 — 数据冗余方案
1.2. 冗余设计 — 冗余数据间的一致性
1.3. 冗余设计 — 实例冗余
1.4. 故障切换 — 服务角色
1.5. 故障切换 — 故障感知与应对
1.6. 故障切换 — 业务连接切换
2. 主流数据库方案概览2.1. 主流数据库高可用功能概览
2.2. MySQL数据库高可用功能概览
接上文:
1.6. 故障切换 — — 业务连接切换与过渡
关于高可用最后一个技术要点, 也经常容易被忽视. 业务使用数据库通常是长连接, 这意味着在后台数据库由于可用性问题需要发生服务切换的时候, 应用客户端的长连接首先会(也必须)断开, 然后能够尝试不断重建连接, 这之后有两个具体要求.一旦确定切换, 尝试重建的连接不应该再成功连上切换前的老服务.
不断重试一段时间后, 需要能够正确定位并连接上切换后替换上来的新服务.
要实现这一点其实并不简单. 由于业务对数据库访问地址无法轻易或者快速改变, 那么就要求数据库服务层来通过各种方案实现业务连接从失效地址转移到新服务地址的目的.
1.6.1. 可漂移资源
较为常见的一种方案, 在访问门牌号不能变的情况下, 将门牌号从故障地址转移到新地址去. 业务应用访问数据库一般需要通过一个类似URL的连接字符串来定位具体服务的网络位置.
jdbc:mysql://192.168.1.1:3306/mydb
其中最重要的定位手段是"地址 + 端口"部分, 这个地址可以是虚拟IP, 也可以是DNS域名, 因此在业务不改变连接字符串配置的前提下, 如果能够操作目标IP或域名的归属, 就能够起到操作业务具体连接目标的目的. 这一方法被称为可漂移资源, 通过高可用切换时漂移虚拟IP或域名的方式完成业务连接目标的切换.
显而易见, 可漂移资源主要是网络资源, 并不是数据库服务的固有资源, 因此该方案多用于自身缺乏高可用保障机制, 需要外部框架来完成监控+切换流程的数据库服务, 是一种较原始的连接切换方案. 说白了, VIP或域名切换应用如此广泛的主要原因就是MySQL应用广, 而MySQL通过我们前面的分析, 相信大家都理解了传统上MySQL是一种缺乏高可用设计的数据库.图38, 任何具备后端服务健康监控+VIP管理能力的服务都可以作为MySQL高可用切换框架使用, 例如keepalive.图39, MHA作为MySQL切换框架严格来说其连接切换能力是开放式的, 交由DBA实现的, 但是由于原生MHA其受限的脚本调用和执行能力, 最常用的实现手段还是VIP漂移.
而在两种最主要的漂移资源 —— VIP和域名 —— 之中, 我认为域名切换比VIP切换更可靠更安全. 原因在于VIP作为需要保证全局唯一的资源, 其切换操作需要两个步骤, 首先在故障节点删除VIP, 然后才能在切换目标节点添加VIP. 而前者是无法保证执行成功率的. 如果故障节点是显而易见的断电问题那还好, 即使不用删除VIP, 该VIP也相当于下线, 但是如果VIP所在的故障节点是某些复杂的hang死问题, 非常容易出现数据库被判定不可用, 但是VIP未离线且删除VIP操作失败. 这个情况下, VIP切换操作无法保证原子性, 第二步操作做不做都有很大问题, 如果不做, 那么切换实际上失败; 如果做, IP出现冲突, 导致VIP无法访问, 切换依然失败. 因此我始终强调关于高可用切换方案, 如果依赖故障节点上任何必须成功的操作, 那么就是不可靠的. 这是VIP切换方案难以根治的问题.
对比而言域名方案要好多了, 因为域名作为漂移资源是由DNS服务集中管理控制, 切换本身不依赖也不需要对故障节点进行任何操作, 而DNS服务的高可用和冗余机制是另外一套较为完善的体系, 因此基于DNS的域名切换是一种更可靠, 成功率明显更高的漂移资源切换方案. 但是域名切换依然有着一些痛点, 这部分会在本文下篇, 网易应用痛点中再展开介绍.图40, AWS的RDS服务中, 跨AZ高可用方案的漂移资源就是域名.
1.6.2. 代理层实现
相较于通过和具体底层服务绑定的漂移资源, 增加一个代理层来管理应用数据库连接切换的方案是一种高级的多, 同时也复杂的多的方案. 类似于用户通过Nginx层接入后端应用服务层, 由Nginx层来决定对后端应用的健康检查, 故障隔离, 负载均衡等问题, 这里的Nginx相当于代理层.
在数据库领域, 引入接入代理层是一种并不罕见的现象. 拿MySQL接入方案举例, 这个代理层可以是通用的haproxy或者加载了四层模块的Nginx, 也可以是MySQL那一堆专用代理, 比如Oracle官方的MySQL Router, MariaDB套件中的MaxScale, 得到percona推荐的ProxySQL…等等有很多种可选的代理. 再比如, Redis方面, twemproxy就是一种使用proxy, 另外还有codis, 是兼具水平分布式和高可用能力的代理架构.图41, MySQL Router是MySQL官方推荐的代理层方案. MySQL Router功能上也能够适配最新的MGR集群架构.图42, Twemproxy代理方案是redis cluster出现前, 运用最广泛的redis访问架构. 配合sentinel监控服务, 可以实现对应用透明的底层节点高可用切换能力.
数据库服务引入代理层有以下显著优势:由于应用访问的endpoint由漂移资源改变为固定资源, VIP的脑裂问题, 域名的应用端缓存问题等痛点不复存在.
得益于长连接, 数据库代理是不需要考虑连接sticky问题的, 因此代理层是真正的无状态, 本身的高可用和负载均衡保障可以通过LVS, OSPF等通用成熟4层LB方案, 也可以使用任意无状态服务LB方案.
代理层可以内建适合与数据库的更多请求路由逻辑, 除了高可用, SQL请求负载均衡, 读写分离等能力通常也会具备.
由于具体处理底层数据库服务主备切换过程中连接漂移问题的是代理层, 内部逻辑可以适配其他任意连接切换策略, 比如代理层+底层VIP, 代理层+底层驱动内部协议, 代理层+配置中心, 等等, 技术实现上很灵活. 举个例子, 我们将底层数据库从传统MySQL主从切换到了MGR集群架构, 如果引入代理层, 就有办法做到底层架构改动对业务无感知.
代理层内部可以设置大量高可用切换时连接管理策略, 比如连接超时, socket超时, 连接池清理, 备库可用性探测, 业务请求快速熔断等等… 能够非常好地适配数据库服务层切换逻辑, 并且有效防止切换失败或脑裂等风险发生. 接入一个好的代理层对高可用切换的可靠性提升帮助非常大.
进一步地, 由于底层服务连接需要漂移的时候, 应用到代理层的连接不会受影响, 只要代理层做的足够好, 甚至可以让业务体验完全几乎无感知的底层数据库高可用切换.
但是引入数据库代理层也意味着增加一些需要考虑的点:代理层服务会有额外的资源开销, 服务吐量较大的时候开销也不算小.
代理层会引入额外的网络路径, 对业务响应时间有一定影响.
进一步地, 代理层本身的性能瓶颈会成为数据库服务全局性能瓶颈, 各种原因引起的阻塞也会成为业务服务阻塞, 引入了一个新的性能考察点.
显著地提高了架构复杂度, 提高了服务治理难度.
最关键的问题, 各种数据库代理服务方案虽然多, 但是在开源方案中, 各方面做的都很优秀的服务几乎没有.
由于代理服务本身较好理解, 实际种类又很多, 具体的例子这里不再一一展开. 理想的代理服务对数据库服务易用性和可靠性帮助非常大, 我们来看一个正面的例子.
国内阿里云的RDS服务, 同AWS的RDS故障转移通过DNS更改域名实现不同, 阿里云的RDS在访问的时候是经过一个proxy层的, 相比较就有一定优势, 双方关于高可用切换的描述对比如下.
AWS的产品说明:Failover Process for Amazon RDS
In the event of a planned or unplanned outage of your DB instance, Amazon RDS automatically switches to a standby replica in another Availability Zone if you have enabled Multi-AZ. The time it takes for the failover to complete depends on the database activity and other conditions at the time the primary DB instance became unavailable. Failover times are typically 60-120 seconds. However, large transactions or a lengthy recovery process can increase failover time. When the failover is complete, it can take additional time for the RDS console UI to reflect the new Availability Zone.
The failover mechanism automatically changes the DNS record of the DB instance to point to the standby DB instance. As a result, you need to re-establish any existing connections to your DB instance. Due to how the Java DNS caching mechanism works, you may need to reconfigure your JVM environment. For more information on how to manage a Java application that caches DNS values in the case of a failover, see the AWS SDK for Java.
我们看到AWS RDS基于DNS域名切换, 一个是生效时间较长, 这里面也包括域名TTL, 第二个是业务需要自己重建数据库连接, 最后是由于域名缓存问题, 使用AWS RDS(以及其他一些服务) 的应用开发需要关注这方面的配置.
阿里云的产品说明:采用一主一备的双机热备架构,适合80%以上的用户场景。主节点故障时,主备节点秒级完成切换,整个切换过程对应用透明;备节点故障时,RDS会自动新建备节点以保障高可用。
单可用区实例:主备节点位于同一个可用区。主备节点位于两台不同的物理服务器上,可用区内的机柜、空调、电路、网络都有冗余,保障高可用性。
多可用区实例(也称为同城双机房或者同城容灾实例):主备节点位于同一地域的不同可用区,提供跨可用区的容灾能力,且不额外收费。
阿里云RDS由于引入了专门的数据库代理层, 一个是切换时间短( 仅切换, 探测确定失效的时间无法缩短 ), 且过程对业务透明, 不需要业务适配. 但是相应的, 做一个可靠好用的数据库代理服务的技术门槛实际上并不低, 这也是阿里云RDS的核心优势之一.
1.6.3. 驱动与连接协议内嵌
可靠的开源数据库代理方案并不多见. 而一些脱离实际需求的架构设计也使得数据库代理方案没有能够成为高可用架构的主流.
举个反面例子, 我们引入代理还是希望能够为业务屏蔽底层细节, 彻底解决数据库故障时连接切换管理的难题, 不是想加重业务架构负担. 上面我们举过MySQL官方的MySQL Router代理服务, 能够兼容最新的MGR集群架构的高可用机制, 但是对于MySQL Router层部署和使用方面, 官方给出了请将应用和Router部署在一起的方案. 也就是每个应用服务器上运行一个MySQL Router. 这个思路不能说不好, 但是现实中比如应用部署架构, 多数据源访问, 应用服务器性能规格等等, 显得过去激进.图43, MySQL官方所谓的innodb cluster架构, 要求能够兼容MGR集群自动切换的MySQL Router代理服务和应用服务部一一绑定, 思路上有Service Mesh的影子, 但是过于激进了.
而在数据库业界, 底层数据库服务具备了集群化能力, 能够自我完成故障感知, 角色选举, 服务切换一系列流程的情况下 —— 就像我们之前介绍的, Oracle, Redis, MongoDB, HBase 等等大部分可靠的数据库产品一样, 我们有个应用更广泛的高可用接入方案 —— 驱动层适配方案.
如果熟悉zookeeper的同学应该都知道, 我们连接zookeeper服务的时候可以一次性在地址项里填入一堆host+port地址, 然后当后端zookeeper集群内部发生主节点漂移, 节点宕机等可用性问题的时候, 业务是不用操心的, 这就是连接驱动层能够兼容底层集群架构, 屏蔽集群内部复杂度的实例. 我喜欢将这样一次性填写多个地址, 然后连接驱动层能自动兼容底层架构变动的字符串称为高可用连接字符串. 我们再通过一些数据库的实际的例子来说明:
MongoDB的连接字符串:
mongodb://127.0.0.1:8017,127.0.0.1:8016,127.0.0.1:8015/books?replicaSet=rstest
相关特性:前面提过ReplicaSet是一种内部之间能够互相监控, 并自动选主的高可用集群.
业务一次性连接整个ReplicaSet的地址, 这样驱动能够在创建连接的时候自动发现当前主节点, 下发写请求. 如果用户需要, 也可以轮询地向各个从节点下发只读请求, 实现读写分离.
底层从节点如果发生问题, MongoDB驱动会自动剔除问题节点, 并且通过保证足够的屏蔽时间来确保服务无抖动.
底层主节点如果发生问题, 集群触发高可用切换, 驱动在尝试重建连接的过程中主动同步集群元数据, 寻找新主节点, 自动切换并恢复可写连接.
Oracle的连接字符串:
jdbc:oracle:thin:@(DESCRIPTION =(ADDRESS_LIST =(FAILOVER = ON)(LOAD_BALANCE = OFF)
(ADDRESS = (PROTOCOL = TCP)(HOST = xxx-ora-db1.db.yyy)(PORT = 9539))
(ADDRESS = (PROTOCOL = TCP)(HOST = xxx-ora-db2.db.yyy)(PORT = 9539))
(ADDRESS = (PROTOCOL = TCP)(HOST = xxx-ora-db3.db.yyy)(PORT = 9539))
)(CONNECT_DATA =(SERVICE_NAME = db_pri)))
相关特性:Oracle dataguard架构下常见的高可用字符串. dataguard也是区分主从角色的高可用集群, 但是并不是选举产生主库, 而是由DBA人工或者observer仲裁服务产生.
驱动连接多个地址并不是通过协议来返回当前主库, 而是通过指定的SERVICE_NAME字段来寻找当前主库. 也就是说, 光切换主备角色是不够的, 还需要将SERVICE_NAME切换到新主库去, 这个切换是个较轻量级操作, 可以在角色切换的同时做掉, 也可以由系统触发器自动来做.
虽然并不能自动识别主库, 但是通过SERVICE_NAME识别主角色的方案, 也能够实现切换后驱动重建连接的过程自动向新主库建立连接的功能.
Redis Cluster的连接字符串:
10.200.128.172:6339,10.200.128.183:6369,10.200.128.184:6365,10.200.128.171:6315,10.200.128.170:6322,10.200.128.171:6301,10.200.128.171:6316,10.200.128.170:6302,10.200.128.183:6352,10.200.128.184:6364,10.200.128.183:6355,10.200.128.172:6340,10.200.128.170:6323,10.200.128.182:6348,10.200.128.182:6344,10.200.128.182:6339,10.200.128.184:6353,10.200.128.183:6372,10.200.128.184:6366,10.200.128.170:6320,10.200.128.184:6359,10.200.128.171:6306,10.200.128.184:6371 ......
相关特性:Redis由于单线程, 通常会有非常多借点的大规模集群. 连接字符串允许将集群所有节点地址罗列进去. 但是实际上有选择性地挑选5~10个左右地址就足够了.
Redis Cluster集群通过gossip通信, 每个正常的节点都能够学习到整个集群的元数据与路由信息. 因此理论上连通任意一个正常实例地址, 都能够获取当前集群的全部主节点地址与slot分配情况, 用于创建具体业务请求连接.
Redis的驱动就相当于加入这组集群, 不断校验本地缓存的集群信息是否正确, 一旦后端集群发生了高可用切换, 集群主节点列表和slot路由发生变化, 驱动感知后主动适应变化, 放弃故障节点连接, 尝试向新主创建连接, 完成主从切换的业务端连接转移工作.
HBase的连接字符串:
hbase.zookeeper.quorum
db-hbase19.hz.123.321:2181,db-hbase20.hz.123.321:2181,db-hbase21.hz.123.321:2181
zookeeper.znode.parent
/hbase-test
相关特性:HBase的连接字符串就更奇葩了, 其中根本没有出现集群中任何一个服务节点的地址, 无论集群有多大, 只要填写作为HBase集群协调器的Zookeeper地址就可以了.
HBase的客户端首先连接zk, 获取集群第一份元数据, 主要包括集群META表所载RS地址, 然后向该RS地址发送读请求, 获取集群完整的服务和路由信息. 这个路由表可能很大, 所以客户端会缓存下来.
一旦底层RS服务发送故障, region发生move, 也就是对HBase来说的服务高可用切换, 集群路由信息会发送变化, 客户端本地的路由缓存也就失效. 如果客户端发现由于路由变化造成的请求错误, HBase客户端就会重新尝试获取最新的路由表.
也就是说, 只要zk集群可用, HBase客户端就能够寻找到当前META所在服务, 而集群只要还有一个RS活着, 客户端就始终能获取最新服务路由, 所以RS不需要像其他数据库一样, 保留多数存活的要求.
我相信这几个例子足够说明问题了. 所谓高可用的数据库连接就是一次性填写足够获取集群当前信息的连接地址, 然后由驱动和服务之间来协调当前谁是主库, 应该向哪些节点创建连接, 切换后应该向哪个节点重建连接等内部细节, 不需要引入外部资源或额外的代理层, 也不需要业务做任何配置改动, 就能够完成故障切换后连接转移恢复的全流程. 作为分布式服务非常通行的业务连接接入方案, 我个人很中意.
不过遗憾的是, MySQL的驱动或客户端并不支持该方案. 即使服务层提供了MGR这样的高可用集群功能, 但是MySQL的客户端驱动层虽然允许填写多个地址, 但是不兼容MGR架构, 也起不到自动兼容底层切换的能力, 业务接入还是得在地址资源漂移或者引入代理中选择.
该方案硬要说缺点的话, 如果配置中的高可用字符串里的所有地址都因为某些极端原因需要全部更换的情况下, 业务需要修改静态配置, 无法做到平滑. 除此之外我没遇到过其他痛点了.
1.6.4. 动态通知方案
关于业务连接在数据库高可用切换后自动漂移的实现, 最后一个常见的方案需要业务层进行较多配合, 但是如果数据库和应用层能够配合的好, 实际上也是很不错的一种解决方式. 我们说前面三种解决方案都是基于应用不想要修改数据库访问地址前提下设计的, 但是如果应用愿意也能够通过在数据库故障时修改访问地址, 来实现恢复连接到新主库的情况下, 那么就又出现了一种思路.
目前基于服务治理和快速运维的需求, 业务普遍会使用配置中心, 并且大部分配置中心服务都具备配置变更的动态通知功能. 数据库连接地址也是一种配置.图44, 起高可用管理作用的外部监控服务不再操作漂移资源, 而是修改配置中心内数据库地址, 业务主动来切换连接.
假设这样一种流程:数据库访问地址作为一种有业务属性的配置项存放在配置中心中, key是服务namespace, value是数据库连接字符串地址
业务改造, 通过配置中心的服务namespace获取特定数据库访问地址, 交给DAO层, DAO根据配置创建到底层的实际连接
数据库依然通过日志复制保证主备冗余架构, 由一种监控服务来监听主库可用性
一旦发现主库发生故障, 监控服务便修改配置中心中对应数据库的value, 也就是直接修改访问地址配置值
配置中心将配置变更的消息主动通知接入的应用, 应用将修改的数据地址交给DAO层
DAO层具备能力, 当如果获取某个地址配置项修改, 则中断修改前地址连接上命令下发(很可能已经断开了), 清空针对老地址的连接池, 并根据给予的新地址重建连接
这种处理形式对业务DAO层的连接和请求管理有较高要求, 但是对数据库方面的高可用实现有很大的简化作用, 有很多显而易见的好处, 是很不错的一种客户端连接切换实现形式.不需要引入漂移资源, 回避了VIP的可靠性问题和动态DNS的实现难度.
应用层如果做好了连接资源管理, 那么失效库的故障隔离其实并不算困难.
不依赖针对故障节点的任何操作, 可靠性高.
实时性好, 相比DNS方案来说.
不需要proxy方案的额外资源投入, 并将proxy部分优势整合到了应用层.
该方案有许多种变体, 基本流程和思路一样, 但是连接管理可以做在应用逻辑层, 也可以做到proxy层, 也可以做到专门的数据库高可用胖客户端层, 在网易内部有着大量的运用. 但是也有一些难以根治的痛点, 例如分布式配置下发可能失败等等, 这个我们在后面的章节结合生产实践再具体讨论.
相关阅读推荐: