GitHub 的 MySQL 高可用性实践分享 - 技术翻译 - 开源中国社区

GitHub 的 MySQL 高可用性实践分享

GitHub 使用 MySQL 作为所有非 git 仓库数据的主要存储, 它的可用性对 GitHub 的访问操作至关重要。GitHub 站点本身、GitHub 的 API、身份验证等等都需要进行数据库访问。我们运行着多个 MySQL 集群来为不同的服务和任务提供支持。我们的集群使用经典的主从配置, 主集群中的某个节点能够接受写入。其余的从集群节点异步同步来自主服务器的更改, 并提供数据的读取服务。

主节点的可用性尤为重要。没有主服务器, 集群无法接受写入:任何需要保留的写入数据都不能持久化保存,任何传入的更改(如提交、问题、用户创建、审阅、新存储库等)都将失败。

为了支持写操作,我们显然需要有一个可用的数据写入节点,一个主集群。但同样重要的是,我们需要能够识别或找到该节点。

在一个写入失败,提示说主节点崩溃的场景中,我们必须确保能启用一个新的主节点,并快速表明其身份。检测故障所需的时间、进行故障转移并公布新的主节点所花费的时间,构成了总的停机时间。

本文将介绍 GitHub 的 MySQL 高可用性和主服务发现解决方案,它使我们能够可靠地运行跨数据中心操作,容忍数据中心隔离,并使得出现故障时耗费的停机时间变得更短。

高可用目标

本文描述的解决方案,迭代并改进了之前在 GitHub 实现的高可用(HA)解决方案。随着规模的扩大,MySQL 的高可用策略必须适应变化。我们希望为 GitHub 中的 MySQL 和其他服务,提供类似的高可用策略。

在考虑高可用和服务发现时,有些问题可以引导你找到合适的解决方案。包含但不限于:

为了说明上面的一些情况,首先让我们讨论一下之前的高可用方案,并说说我们为什么要修改它。

移除基于 VIP 和 DNS 的服务发现

在之前的迭代版本中,我们:

在这个迭代版本中,客户端使用名字服务(比如 mysql-writer-1.github.net)来发现写节点。名字可以解析为一个虚拟 IP(VIP),这个 VIP 指向主节点。

因此,在正常情况下,客户端只需要解析名称,连接到解析后的 IP上,然后发现主节点也正在另一边监听链接(也就是客户端连上了主节点)。

考虑这个跨越三个不同数据中心的复制拓扑:

当主节点发生故障时,必须在副本集中选出一个服务器,提升为新的主节点。

orchestrator 将会检测到故障,选举出一个新的主节点,然后重新分配 name(名称)和 VIP(虚拟 IP)。客户端实际上并不知道主节点的真实身份:它们只知道 name(名字),而这个名字现在必须解析给新的主节点。不过,需要考虑:

VIP 是需要协作的:它们由数据库服务器自己声明和拥有。为了获得或释放 VIP,服务器必须发送 ARP 请求。拥有 VIP 的服务器必须在新提升的主节点获得 VIP 之前先释放掉。这还有一些额外的影响:

VIP 受限于物理位置。它们属于交换机或者路由器。所以,我们只能将 VIP 重新分配到位于同一位置的服务器上。特别是,当新提升的服务器位于不同的数据中心时,我们无法分配 VIP,只能修改 DNS。

仅这些限制,就足以促使我们寻求新的解决方案,但考虑更多的是:

这些额外的步骤是导致中断总时间的一个因素,并且引入了它们自己的故障和摩擦。

该解决方案生效了,GitHub 已经成功完成 MySQL 的故障迁移,但我们希望我们的 HA 在以下方面有所改进:

GitHub 的高可用解决方案:orchestrator, Consul, GLB

我们的新策略,除了附带的改进外,还解决或减轻了上面的许多问题。在今天的高可用设置中,我们有:

新的设置将完全删除 VIP 和 DNS 的修改。在我们引入更多组件的同时,我们能够将组件解耦并简化任务,并且能够使用可靠、稳定的解决方案。下面逐一分析。

正常流程

正常情况下,应用程序通过 GLB/HAProxy 连接到写节点。

应用程序永远不知道主节点的身份。和之前一样,它们使用名字。例如,cluster1 的主节点命名为 mysql-writer-1.github.net。在我们当前的设置中,名字被解析为一个选播(anycastIP。

使用选播时,名字在任何地方都被解析为相同的 IP,但流量会根据客户端位置的不同进行路由。需要指出的是,在我们的每个数据中心,都有 GLB(我们的高可用负载均衡)被部署在不同的容器中。指向 mysql-writer-1.github.net 的流量总是路由到本地数据中心的 GLB 集群。因此,所有客户端都由本地代理提供服务。

我们在 HAProxy 上运行 GLB。我们的 HAProxy 维护了一个写连接池:每个 MySQL 集群一个连接池,其中每个连接池只有一个后端服务器:集群的主节点。DC 中的所有 GLB/HAProxy 容器都具有相同的连接池,并且它们都指向相同的后端服务器。这样,如果一个应用程序想要写入 mysql-writer-1.github.net,它连接到哪个 GLB 服务器并不重要。它总会被路由到实际的 cluster1 主节点上。

对于应用程序而言,服务发现结束于 GLB,并且不再需要重新发现。就这样,通过 GLB 将流量路由到正确地址。

GLB 如何知道哪些服务器可以作为后端服务器,以及如何将更改传播到 GBL 呢?

Consul 的服务发现

Consul 是著名的服务发现解决方案,它也提供 DNS 服务。然而,在我们的解决方案中,我们用它作为高效的键值存储系统。

在 Consul 的键值存储中,我们写入了集群主控的标识。对于每一个集群,都有一个键值对记录标识集群的主 FQDN,端口,IPV4,IPV6。

每一个 GLB/HAProxy 节点都运行 consul 模板:每一个服务都在监听 consul 数据的变更(这里主要是对集群主控的数据变更)。consul 模板会生成一个有效的配置文件并且当配置变更时,能够自动重载 HAProxy。

因此,Consul 中主控标识的变更会被每一个 GLB/HAProxy 观察到,然后它们立即重新配置它们自己,在集群后端池中设置新的主控作为单一对象,并且进行重载以反映这些变更。

在 GitHub 中,每一个数据中心都有一个 Consul 设置,并且每一个设置都具有高可用性。然而,这些设置又互相独立,它们之间不进行互相复制或数据共享。

那么 Consul 是如何获得变更通知,在交叉数据中心中,信息又是如何分布的呢?

orchestrator/raft

运行一个 orchestrator/raft 设置:orchestrator 节点之间通过 raft 一致性算法进行通信。每一个数据中心有 1~2 个 orchestrator 节点。

orchestrator 负责失败检测,MySQL 故障转移,以及 Consul 主控的变更通知。故障转移通过单个 orchestrator/raft 主导节点进行操作,但是对于主控变更,产生新主控的消息会通过 raft 机制被传播到所有 orchestrator 节点。

一旦 orchestrator 节点接收到主控变更的消息,它们会与自己对应的本地 Consul 设置通信:它们都执行 KV 写操作。具有多个 orchestrator 节点的数据中心会有多个完全相同的 Consul 写操作。

整体流程

在主节点故障的场景中:

每个组件都有明确的责任归属,而且整个设计简单并且解耦。orchestrator 不需要知道负载均衡。Consul 不需要知道这些信息是从哪里来的。代理只关心 Consul,客户端只关心代理。

而且:

其他细节

为了进一步确保流程的安全,我们还提供了以下内容:

我们会在以下章节进一步完成备受关注和期望的高可用目标。

orchestrator/raft 失败检测

orchestrator使用全面方法来检测失败,因此这种方法非常可靠。我们不会观察到误报 —— 因为我们没有进行过早的故障转移,所以也不会产生不必要的中断时间。

通过完全的 DC 网络隔离(又称 DC 栅栏),orchestrator/raft 进一步处理这个问题。一个 DC 网络隔离会引起一些混淆:这个 DC 中的服务器是可以互相通信的。他们是与其他 DC 网络隔离,还是其他 DC 被网络隔离?

在一个 orchestrator/raft 设置中,raft 的 leader 节点就是运行故障转移的那个节点。leader 是取得了大多数节点支持的节点(特定数量)。我们的 orchestrator 节点部署就是这样,没有单一数据中心可以占大多数,任何 n-1 的 DC 也是如此。

在一个完全 DC 网络隔离的事件中,这个 DC 的 orchestrator 节点与其它 DC 中的对应节点失去连接。最终,隔离 DC 中的 orchestrator 节点不能作为 raft 集群的 leader 节点。如果任何这种节点碰巧成为了 leader 节点,它就会退出。一个新的 leader 节点可以从任何一个其他 DC 分配。leader 节点会得到其他所有 DC 的支持,这些 DC 彼此之间可以进行通信。

因此,调用 shots 的 orchestrator 节点将位于网络隔离数据中心之外。一个隔离 DC 应该有一个主服务器,orchestrator 会使用可用 DC 中的其中一个服务器将它替换来初始化故障转移。我们委托非隔离 DC 中的那些节点来做这个决定,以此来缓解 DC 隔离。

更快的公告

通过发出公告说主分支即将修改,可以进一步减少运行停机的总时间。如何实现这个想法?

当 orchestrator 开始进行故障迁移的时候,它会观察可用于升级的服务器队列。在了解自我复制的规则,以及接受提示和限制的情况下,在最好的行动方针中,它能做出基于一定训练的决策。

它可能意识到一个可以升级的服务器也是一个理想的候选策略,例如:

在这个例子中 orchestrator 首先将服务器设置为可写,然后立即公告服务器的升级(我们的例子中是写到了 Consul KV),即使异步开始修复复制树,这种操作通畅会花费更多几秒的运算。

有可能当我们的 GLB 服务器完全重载时,复制树已经完好无损,但是这不是严格要求的。服务器可以接收到写操作!

半同步复制

在 MySQL 的半同步复制中,在获知更改已发送到一个或多个副本之前,主服务器不会确认事务已提交。它提供了一种实现无损故障转移的方法:应用于主服务器的任何更改都将应用于或等待应用于其中一个副本。

一致性带来的成本是:可用性风险。如果没有副本确认收到更改,主服务器将被阻塞并且写入操作将停止。幸运的是,这里有一个超时设置,在这之后主服务器可以恢复到异步复制模式,使写入操作再次可用。

我们已经把我们的超时设置在一个合理的低值:500ms。将更改从主服务器发送到本地 DC 副本,通常也发送到远程 DC,这个阈值是绰绰有余的。设置这个超时时间之后,我们可以观察到完美的半同步行为(无需回退到异步复制),并且在确认失败的情况下,可以在非常短的阻塞周期内获得让人满意的表现。

我们在本地 DC 副本上启用半同步,并且在主服务器宕机的情况下,我们期望(尽管不严格地执行)无损故障转移。对完整的 DC 故障进行无损故障转移的代价很高昂,我们并不期待这么做。

在试验半同步超时的同时,我们还观察到一种对我们有利的行为:主服务器在发生故障时,我们能够影响最佳候选人的标识。通过在指定的服务器上启用半同步,并将它们标记为候选服务器,我们可以通过影响故障的结果来减少总的停机时间。在我们的试验中,我们观察到,我们通常最终会得到最佳候选服务器,并因此发布快速公告。

心跳注入

我们没有在已提升/已降级的主设备上管理 pt-heartbeat 服务的启动/关闭,作为替代,我们选择随时随地运行它。这需要进行打一些补丁,以便使 pt-heartbeat 可以支持服务器端来回更改它们的 read_only 状态或其完全崩溃。

在我们目前的设置中,在主服务器及其副本上运行 pt-heartbeat 服务。在主服务器上,他们产生心跳事件。在副本服务器上,他们识别到服务器是只读的,并定期重新检查其状态。只要服务器升级为主服务器,该服务器上的 pt-heartbeat 会将服务器标识为可写,并开始注入心跳事件。

orchestrator 所有权委托

我们进一步委托到 orchestrator:

对于新主控,以上所有这些操作减少了冲突的可能性。一个刚刚被提升的主控应该是在线的并且可接入,否则我们就不应该提升它。然后让 orchestrator 直接应用变更到被晋升的主控上也应该是合理的。

限制和缺点

代理层使得应用程序不知道主服务器的身份,但是对于主服务器它也掩盖了应用程序的身份。所有主服务器看到的连接都来自代理层,我们丢失了关于连接实际来源的信息。

随着分布式系统的发展,我们仍然遗留了未处理的场景。

值得注意的是,在数据中心隔离场景中,假设主服务器位于 DC 中,DC 中的应用程序仍然能写入主服务器。一旦网络恢复正常,可能会导致状态不一致。我们正努力在非常独立的 DC 中,通过实现一个可靠的 STONITH 来缓解这种脑裂。和以前一样,在将主服务器之前需要花费一些时间,可能出现短暂的脑裂。而避免脑裂的操作成本非常高。

更多的场景存在:故障转移时 Consul 的终端;部分 DC 隔离;其他的。我们知道,使用这种性质的分布式系统不可能消除所有的漏洞,因此,我们将焦点放在最重要的案例上。

结果

orchestrator/GLB/Consul 设置给我们提供以下功能:

结语

编排/代理/服务发现范例在解耦架构中使用众所周知的可信组件,这使得部署、运维和观察变得更加容易,并且每个组件可以独立扩展或缩减。我们将不断测试我们的设置,以继续寻求改进。