首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

GitHub负载均衡器深度解析

本文是一篇演讲整理。Joe Williams在演讲中介绍了GitHub负载均衡器(GLB)的架构。

GitHub在HAProxy之上构建了一个弹性的自定义解决方案,以智能地路由来自各种客户端(包括Git、SSH和MySQL)的请求。GLB分为两大部分:GLB导向器和GLB代理,后者是基于HAProxy构建的。HAProxy有许多优点,包括负载平衡、高级运行状况检查和可观察性等。有了与Consul的紧密集成,可以进行实时配置更改。部署使用一个GitHub流,并包含一个扩展的CI流程,并通过Slack上的ChatOps管理所有金丝雀部署。

演讲内容

我是Joe,在GitHub工作,今天我要讲的是GLB。我们在2015年前后开始构建GLB,并于2016年投入生产。它是由我本人和GitHub的另一位工程师Theo Julienne共同构建的。我们构建GLB是为了替代一组不可扩展的HAProxy主机,后者是单体架构,难以测试,非常令人困惑而且有些可怕。这套基础设施是在我加入GitHub之前很久就建立的,但我的工作就是要替换掉它。

在那个时候,我们在构建GLB时要遵循一些设计原则,以缓解之前系统存在的许多问题。其中一条原则是:我们想要的系统应该运行在货架硬件上。之前的讨论中我们谈到了F5的故事,我们不想重蹈F5的复辙。

我们想要的是能够水平扩展,支持高可用性,并且不会在任何时候中断TCP会话的系统。我们想要支持连接清空(connection draining),可以在生产环境中轻松加入或撤出主机,而又不会破坏TCP连接的系统。我们想要的系统应该是以服务为单位的。GitHub上有很多不一样的服务。GitHub.com是一项重要的服务,但我们还有很多内部服务,希望将它们隔离到各自的HAProxy实例中。

我们需要的系统应该可以像在GitHub上随处可见的代码一样迭代,并且存储在Git仓库中。我们需要在每一层上都可以做测试,这样就能确保每个组件都在正常工作。我们已经扩展到了全球范围内的多个数据中心上,所以希望构建出为多个PoP和数据中心设计的产品。最后,我们想要一种能够抵御DoS攻击的系统,因为不幸的是这种攻击对于GitHub来说是非常普遍的。

首先,我要深入谈一谈GitHub的请求路径,并介绍一些我们用来管理它的开源工具;然后,我将深入探讨GLB的两大组成部分,分别是基于DPDK的导向器(Director)和基于HAProxy的代理。

上图是GitHub上请求路径的高级概览。由于我们对内部和外部请求都使用GLB,因此这套流程基本上适用于进出GitHub的所有请求和服务(包括MySQL之类的服务)。接下来介绍两大组件,也就是GLB导向器和GLB代理。

先从客户端开始。GitHub上有很多不同种类的客户端和协议。如果你看一下自己的Git仓库配置,就会发现它是GitHub.com。所以这意味着我必须弄清楚哪些客户端基于HTTP,哪些客户端基于Git,哪些是SSH,之类的各种事情。如果我能回到过去并与GitHub的创建者交流,让他们把所有Git流量都放在git.github.com上,我肯定会这么干的;但我必须弄清楚端口443上都在跑什么,诸如此类。因此,我们必须在HAProxy配置中做很多疯狂的事情来解决所有问题。

我们有诸如Git、SSH和MySQL之类的客户端,还有GLB的内部和外部版本,用来路由所有流量。另外Git有一点很特别,那就是它不支持重定向或重试之类的功能。如果你在拉取Git的过程中曾经按过Control+C,你肯定知道它会从头重新开始;因此GLB最重要的任务之一,就是我们要避免任何种类的连接重置或类似的事情,以免干掉Git的拉取和推送。

有一样功能我们是没法支持的,那就是TCP Anycast。我们的设计完全是围绕在GeoDNS之上构建事物,并使用DNS而非Anycast来管理我们的流量。我们跨多个提供商和机构来管理DNS,并使用开源项目OctoDNS来完成所有这些工作。这是我与GitHub的另一位工程师Ross McFarland一起开发的项目。

在网络的边缘,我们使用ECMP和客户端发出请求,而ECMP则将这些客户端分片到我们的GLB导向器层上。GLB导向器是一个基于DPDK的L4代理。它使用直接代理返回(应用通用UDP封装),对客户端和L7代理完全透明。

从导向器这里,请求被转发到HAProxy上,所有这些操作都主要使用Consul和Consul-Template以及称为Kube Service Exporter的工具来管理,后者是我们在Kubernetes和HAProxy之间的一种桥梁。我们完全不使用任何入口控制器。我们直接与kube节点上的Nodeports对话,而Kube Service Exporter简化了整个过程,而且它也是完全开源的。

下面我们来深入研究GLB本身,首先关注导向器,然后是代理的具体内容。

首先,我们使用等价多路径(Equal-Cost Multi-Path,ECMP)对跨多个服务器的单个IP进行负载均衡,就像前面提到的一样,它基本上就是为跨多个机器的特定IP哈希一个客户端。我们将其称为跨多个机器“拉伸IP”。

之前我们将GLB的设计分为L4代理/L7代理。这种设计的一个不错的特性是,一般来说,L4代理不会经常更改,而L7代理则每小时或每天都会更改一次;因此我们在更新这些配置时可以减轻一些风险。

我们在导向器中采取了很多谨慎措施,以尽可能减少状态。GLB导向器完全没有像IPVS那样的状态共享。我们不会在节点之间进行任何类型的TCP连接多播共享或类似操作。我们要做的基本上就是建立一个转发表,该转发表在每台主机上都是一样的,并且和ECMP有些相似;我们将客户端哈希到这个转发表上,然后将它们的请求路由到后端中的特定代理。

每个客户端都使用zip哈希对它们的源IP和源端口进行哈希处理,然后把这些哈希基于称为集合哈希(rendezvous hash)的工具分配给L4代理。集合哈希有一个很好的属性,它可以在更改转发表时仅重置1/n个连接。它们被分配给特定的主机。这样,为了尽可能不断开TCP连接,我们只断开了与发生故障的主机连接的客户端的连接。作出这些更改的另一种方式是,在每个导向器主机上都有一个运行状况健康检查程序。它们不断检查代理主机,进行更改并更新这个转发表。

这个哈希表也是协调代理主机维护的一种方式。我们在导向器内部有一个状态机,我们可以转发它,也可以通过它处理每个代理主机,图上就是一个示例。这让我们可以将代理主机拉入和退出生产环境,还能清空主机在将它们填充回去,而不会破坏这些连接。这种设计的唯一缺点是,我们必须使状态表在所有导向器之间保持同步,这只在我们要更新状态表以做维护等情况下才会成为问题。这种情况下代理主机就会失败,因为我们处于一种怪异的状态,其中流量以一种奇怪的方式被路由。所幸这种事情很少见,我们没见过那么多故障。

为了在发生故障时减少连接重置的状况,我们引入了一种称为“故障转移第二次机会”的操作,我认为这也是GLB导向器的一种简洁且新颖的设计:为了允许最近失败的主机完成TCP流,我们有一个名为glb-redirect的IP表模块;在代理主机上,该模块检查我们在每个数据包中发送给代理主机的额外元数据,从而将这些数据包从活动主机重定向到最近发生故障的主机上。这样,如果发生故障的主机还能处理请求,它将继续处理下去。这种工作机制基本上是这样的,如果主服务器因为数据包不是SYN包,或者因为包与已经建立的流对应而无法理解这个包,则glb-redirect将接收该包,并将其转发到转发表中现有的“失败主机”上。这里的元数据由GLB导向器注入,接下来会介绍细节。

为了提供这种元数据,我们使用通用UDP封装(Generic UDP Encapsulation)。我们在GLB设计阶段的早期就决定使用直接服务器或直接代理返回。这种设计有很多影响,这里讲一些重点的部分。首先,它简化了导向器的设计,因为我们只处理一个方向的数据包流。这也让导向器对客户和L7代理都是透明的。因此我们不必为X-Forwarded-For之类的事情烦恼。我们还可以向代理层的每个数据包添加额外的元数据,以实现"second-chance flow"。

我们使用称为通用UDP封装的相对较新的内核功能来执行这种操作。我们将元数据添加到这个GLB私有数据域中。在引入通用UDP封装之前,我们使用了内核中称为Foo-over-UDP的某种功能,我认为这两种事物都是来自谷歌的。基本上,结果就是我们将所有目的地为L7代理的TCP会话都包装在一个特殊的UDP包中,然后在代理端解包,就好像HAProxy直接从客户端看到它一样。那么导向器的内容就讲的差不多了,下面开始谈代理的事情。

我们很好地构建了我们的代理,或者一般来说是GLB,但更具体地说,我们的代理层基本上是在服务集群中。上面的树形图提供了高层次的配置概览。

这种做法导致的结果是,我们通常会在其作业或服务上拆分HA配置,但是我们有许多配置恰好是多租户的。“服务”一词有很多定义,但是我们在GLB中给服务下的定义是,服务是单个HAProxy配置文件,它可以侦听任意数量的端口或IP之类的内容。像我之前提到的那样,其中一些是特定于协议的。我们有Git配置;我们有SSH配置;我们有HTTP、SMTP和MySQL。

然后,在我们看来“集群”基本上就是这些HAProxy服务或配置的集合,它们被分配给特定区域或数据中心,然后我们为它们命名。在树图中可以看到,我们在一个集群中有一个区域和一个数据中心,而该集群由一堆服务组成。

这种设计落地到现实世界,结果就是我们最终在CI中构建了所有HAProxy配置。这些是预构建的配置文件打包工件。部署的所有内容最后都成为了一种甚至还没见到服务器时就已经预先构建好的配置。

我们曾经使用Puppet来部署这些配置,然后我们决定搞一些打包的配置工件出来。这些配置包与HAProxy本身分开部署,而HAProxy实际上是由Puppet管理的。如你在GitHub的屏幕截图中所见,我们为GLB本身提供了48个配置包,你看过其中某个的话就会知道,它几乎就是用Jenkins构建的矩阵。也就是说,这里的48个构建代表了我们拥有的每个数据中心、集群和站点组合配置。

接下来,就像在做CI作业的时候那样,我们会运行大量测试。我在GitHub的前六个月工作,就是为GitHub的上一代负载均衡器编写500个集成测试。因为我们对它的工作机制不了解,也不了解配置文件的生成方式,所以为了迁移到新系统,我们必须采用某种方式编写测试,然后确保新配置能够继续工作。在CI作业期间,我们有一个测试套件,其在我们的测试环境中配置一个具有真实IP和实时后端的完整HAProxy GLB堆栈。然后我们使用数百个针对性的集成测试,这些测试用上了curl、Git、OpenSSL和我们可以想到的所有客户端,以对所有这些HAProxy实例运行测试,并让被正确终止和路由的请求失效。

通过这种开发流程,我们就可以有效地使用HAProxy配置进行测试驱动的开发工作,我认为这是非常简洁的。在截图中你可以看到一些测试示例;这些测试完成度达到了100%,过去的五年一直表现出色。

现在,我们希望在CI中部署我们的配置。CI通过后,我们可以将其部署到GLB集群中HAProxy…的一台或多台主机中。部署都是特定的Git分支。就像开源项目一样,我们使用了GitHub Flow、PR等等功能。我们通常从“无操作”部署开始,这样就能预览将要做出的更改,并对比当前配置和新配置之间的区别。如果结果看起来是正确的,则我们会继续进行金丝雀部署。金丝雀部署通常是在单个节点上进行的,然后我们可以观察这个节点的客户流量,并确保一切都能顺利工作。接下来,我们可以部署到整个集群,并将更改合并到主集群中;这些工作都是通过Slack完成的。

一旦有一台代理主机部署了配置,基本上实时更新就开始了。而这一切都是由Consul-Template、Consul和Kube Service Exporter编排的。对于Kubernetes,我们使用自己的开源项目Kube Service Exporter来管理服务和Consul。基本上,Kube Exporter是在Kubernetes中运行并导出kube服务数据的服务。Kube服务不同于GLB服务,前者与Kubernetes API对话,拉回Kubernetes服务信息元数据并将其转储到Consul中。

然后,我们从Consul中获取这份数据,并使用Consul-Template根据该数据构建HAProxy配置。就像我提过的那样,我们绝不使用Kubernetes入口控制器。我们直接与每个kube节点上的Nodeport对话。在最初部署Kubernetes的过程中我们发现,至少对于我们来说,Kubernetes入口控制器是一种实际上并不需要的间接访问机制,因为我们已经拥有了如此强大的负载平衡基础架构,并且已经解决了DNS之类的问题。

一旦部署了一个配置,我们可能就需要进行维护工作了。上图是服务器后端线路的示例。为了进行维护工作,我们在每台GLB代理主机上都使用了一个小型服务,称为Agent Checker。Agent Checker知道如何对话,知道如何处理HAProxy中的agent-check和agent-send。Agent Checker将有关后端及其当前状态的元数据存储在本地配置文件中,然后由Consul和Consul-Template管理。因此,我们可以在整个集群中作出更改。下图是我们使用ChatOps拉入服务器,和拉出服务器以备维护的具体方式。

现在客户流量上来了,我们显然会想要监控这些流量。我们几乎记录了能想到的所有内容,并将它们全部转储到Splunk中,以便对数据切片和切块。我们还做了一些事情,例如使用HAProxy映射将IP地址映射到国家和自治系统编号上,以便我们追踪性能并提交报告。

我们还为每个请求设置了唯一的请求ID,然后确保GLB后面的服务在整个流程中都包含该标头,以便我们跟踪整个堆栈中的请求。这里有一些Splunk的屏幕截图。最上面的是按国家/地区划分的客户端连接时间,最下面的是按自治系统划分的客户端连接时间。我必须加进去自Splunk的服务器指标,因为它确实很大。同样,我们在导向器和HAProxy里也会跟踪许多指标。

目前我们将所有这些数据都转储到DataDog中。我们可以将这些数据切片和切块,我们还有一个自定义的DataDog插件,该插件为我们要切片和切块的元数据添加了一堆标签。因此在这里,我们可以按数据中心、群集、服务主机之类的标准来对GLB群集分类。GitHub上的所有团队都使用这些仪表板来监视GLB,及其背后服务的运行状况和性能,并对任何类型的问题发出警报。

GLB就讲到这里吧。正如我所提到的,它自2016年以来一直处于生产环境,并且几乎处理了GitHub内部和外部每个服务的所有请求。

原文链接:

https://www.haproxy.com/user-spotlight-series/inside-the-github-load-balancer/

  • 发表于:
  • 本文为 InfoQ 中文站特供稿件
  • 首发地址https://www.infoq.cn/article/OMoyUzaGflOVddzdqvmz
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券