
你好,哈布尔! 我是 Artem Karamyshev,系统管理团队负责人 。 在过去的一年里,我们推出了许多新产品。 我们希望确保 API 服务易于扩展、容错,并为用户负载的快速增长做好准备。 我们的平台是在OpenStack上实现的,我想告诉你我们必须解决哪些组件容错问题才能获得容错系统。 我认为这对于那些也在 OpenStack 上开发产品的人来说会很有趣。
平台的整体容错能力由其组件的弹性组成。 因此,我们将逐步完成我们识别风险并关闭风险的所有级别。
本文的视频版本,其主要来源是正常运行时间第 4 天会议的报告,该会议由 ,你可以看到 .
物理架构的弹性
MCS云的公共部分现在基于两个Tier III数据中心,它们之间有自己的暗光纤,通过不同的路由在物理层面预留,吞吐量为200 Gbit/s。 第三层为物理基础设施提供必要的容错级别。
暗光纤在物理层和逻辑层都有预留。 通道预留过程是迭代的,问题也出现了,我们正在不断改进数据中心之间的通信。
例如,不久前,在数据中心附近的一口井中工作时,一台挖掘机打破了一根管道,而该管道内有一条主光缆和一条备用光缆。 事实证明,我们与数据中心的容错通信通道在井中的某一点很容易受到攻击。 因此,我们失去了部分基础设施。 我们得出结论并采取了一系列行动,包括在相邻井中安装额外的光学器件。
在数据中心,有一些通信提供商的存在点,我们通过 BGP 向这些提供商广播我们的前缀。 对于每个网络方向,选择最佳的度量,这允许为不同的客户端提供最佳的连接质量。 如果通过一个提供商的通信出现故障,我们将通过可用的提供商重建路由。
如果某个提供商失败,我们会自动切换到下一个。 如果其中一个数据中心发生故障,我们会在第二个数据中心拥有服务的镜像副本,该副本将承担全部负载。

物理基础设施的弹性
我们使用什么来实现应用程序级容错
我们的服务基于许多开源组件构建。
ExaBGP 是一种使用基于 BGP 的动态路由协议实现多种功能的服务。 我们积极使用它来宣传我们列入白名单的 IP 地址,用户通过这些地址访问 API。
HAProxy的 是一个高负载均衡器,允许您在 OSI 模型的不同级别配置非常灵活的流量均衡规则。 我们用它来平衡所有服务:数据库、消息代理、API 服务、Web 服务、我们的内部项目 - 一切都在 HAProxy 后面。
API申请 — 用 python 编写的 Web 应用程序,用户用它来管理他的基础设施和服务。
工人申请 (以下简称为worker) - 在OpenStack服务中,这是一个基础设施守护进程,允许您将API命令广播到基础设施。 例如,磁盘创建发生在worker中,创建请求发生在应用程序API中。
标准 OpenStack 应用架构
大多数为 OpenStack 开发的服务都尝试遵循单一范例。 一个服务通常由两部分组成:API和workers(后端执行器)。 一般来说,API 是 python 中的 WSGI 应用程序,它可以作为独立进程(守护进程)启动,也可以使用现成的 Nginx 或 Apache Web 服务器启动。 API 处理用户请求并将进一步的指令传递给工作应用程序以供执行。 传输使用消息代理(通常是 RabbitMQ)进行,其他的支持很差。 当消息到达代理时,它们将由工作人员处理,并在必要时返回响应。
此范例涉及孤立的常见故障点:RabbitMQ 和数据库。 但 RabbitMQ 在一个服务内是隔离的,理论上,每个服务都可以是单独的。 因此,在 MCS,我们尽可能地分离这些服务;对于每个单独的项目,我们创建一个单独的数据库,一个单独的 RabbitMQ。 这种方法很好,因为如果某些脆弱点发生事故,并不是整个服务崩溃,而只是部分服务崩溃。
工作应用程序的数量是无限的,因此 API 可以轻松地在平衡器后面水平扩展,以提高性能和容错能力。
当 API 和工作线程之间发生复杂的顺序操作时,某些服务需要在服务内进行协调。 在这种情况下,使用单个协调中心,即 Redis、Memcache、etcd 等集群系统,它允许一个工作人员告诉另一个工作人员该任务已分配给他(“请不要接受”)。 我们使用etcd。 通常,工作人员主动与数据库进行通信,从那里写入和读取信息。 我们使用 mariadb 作为数据库,它位于多主集群中。
这个经典的单一服务以 OpenStack 普遍接受的方式组织。 它可以被认为是一个封闭的系统,其扩展和容错的方法是非常明显的。 例如,为了API容错,在它们前面放一个平衡器就足够了。 扩大工人规模是通过增加工人数量来实现的。
整个方案的弱点是RabbitMQ和MariaDB。 他们的架构值得单独写一篇文章,在这篇文章中我想重点讨论 API 容错。

Openstack 应用架构。 云平台的均衡与容错
使用 ExaBGP 使 HAProxy 平衡器具有容错能力
为了使我们的 API 可扩展、快速且具有容错能力,我们在它们前面放置了一个负载均衡器。 我们选择HAProxy。 在我看来,它具有我们任务所需的所有必要特征:在多个 OSI 级别进行平衡、管理接口、灵活性和可扩展性、大量平衡方法、对会话表的支持。
首先需要解决的问题是平衡器本身的容错问题。 简单地安装平衡器也会产生故障点:平衡器损坏并且服务崩溃。 为了防止这种情况发生,我们将 HAProxy 与 ExaBGP 结合使用。
ExaBGP 允许您实现一种检查服务状态的机制。 我们使用此机制来检查 HAProxy 的功能,并在出现问题时从 BGP 禁用 HAProxy 服务。
ExaBGP+HAProxy方案
- 我们在三台服务器上安装必要的软件 ExaBGP 和 HAProxy。
- 我们在每台服务器上创建一个环回接口。
- 在所有三台服务器上,我们为该接口分配相同的白色 IP 地址。
- 白色 IP 地址通过 ExaBGP 发布到互联网。
容错是通过从所有三台服务器通告相同的 IP 地址来实现的。 从网络的角度来看,可以从三个不同的下一跳访问同一地址。 路由器看到三个相同的路由,根据自己的度量选择其中最高优先级(这通常是相同的选项),并且流量仅流向其中一台服务器。
当HAProxy运行出现问题或服务器故障时,ExaBGP停止通告路由,流量平滑切换到另一台服务器。
这样,我们就实现了平衡器的容错能力。

HAProxy 平衡器的容错能力
事实证明该方案并不完美:我们学会了如何保留 HAProxy,但没有学会如何在服务内分配负载。 因此,我们稍微扩展了这个方案:我们继续在几个白色 IP 地址之间进行平衡。
基于DNS+BGP的均衡
我们的 HAProxy 的负载平衡问题仍未解决。 然而,它可以很简单地解决,就像我们在这里所做的那样。
为了平衡三台服务器,您将需要 3 个白色 IP 地址和良好的旧 DNS。 这些地址中的每一个都在每个 HAProxy 的环回接口上确定并公布到 Internet。
在 OpenStack 中,为了管理资源,使用服务目录,它指定特定服务的端点 API。 在此目录中,我们注册了一个域名 - public.infra.mail.ru,该域名通过三个不同的 IP 地址通过 DNS 进行解析。 结果,我们通过 DNS 获得了三个地址之间的负载分配。
但由于在宣布白色 IP 地址时我们不控制服务器选择优先级,因此这还不是平衡的。 通常,只会根据 IP 地址资历选择一台服务器,另外两台服务器将处于空闲状态,因为 BGP 中未指定任何指标。
我们开始通过 ExaBGP 使用不同的指标发送路由。 每个平衡器都会通告所有三个白色 IP 地址,但其中一个(该平衡器的主要地址)使用最小度量进行通告。 因此,当所有三个平衡器都在运行时,对第一个 IP 地址的调用将转到第一个平衡器,对第二个 IP 地址的调用将转到第二个平衡器,对第三个 IP 地址的调用将转到第三个。
当其中一个平衡器掉落时会发生什么? 如果任何平衡器发生故障,其主地址仍会从其他两个平衡器中通告,并且流量会在它们之间重新分配。 因此,我们通过 DNS 一次为用户提供多个 IP 地址。 通过 DNS 和不同的指标进行平衡,我们可以在所有三个平衡器之间均匀分配负载。 同时我们也不会失去容错能力。

基于DNS+BGP平衡HAProxy
ExaBGP 与 HAProxy 之间的交互
因此,我们通过停止通告路由来实现容错,以防服务器离开。 但 HAProxy 可能会因服务器故障以外的其他原因而关闭:管理错误、服务内故障。 在这些情况下,我们也希望从负载下移除损坏的平衡器,并且我们需要不同的机制。
因此,扩展之前的方案,我们实现了ExaBGP和HAProxy之间的心跳。 这是ExaBGP和HAProxy之间交互的软件实现,ExaBGP使用自定义脚本来检查应用程序的状态。
为此,您需要在 ExaBGP 配置中配置运行状况检查器,该检查器可以检查 HAProxy 的状态。 在我们的示例中,我们在 HAProxy 中配置了运行状况后端,并从 ExaBGP 端使用简单的 GET 请求进行检查。 如果公告停止发生,那么 HAProxy 很可能无法正常工作,并且无需对其进行公告。

HAProxy 健康检查
HAProxy Peers:会话同步
接下来要做的就是同步会话。 当使用分布式平衡器时,很难组织有关客户端会话的信息的存储。 但由于 Peers 功能(能够在不同 HAProxy 进程之间传输会话表的能力),HAProxy 是少数可以做到这一点的平衡器之一。
有不同的平衡方法:简单的方法,例如 ,并扩展,当客户端的会话被记住时,并且每次他都像以前一样结束在同一台服务器上。 我们想实施第二个选项。
HAProxy 使用粘表来保存该机制的客户端会话。 它们保存客户端的原始IP地址、选定的目标地址(后端)和一些服务信息。 通常,棒表用于存储源 IP + 目标 IP 对,这对于在切换到另一个平衡器时无法传输用户会话上下文的应用程序(例如,在 RoundRobin 平衡模式下)特别有用。
如果教导棒表在不同的 HAProxy 进程之间移动(在其之间发生平衡),我们的平衡器将能够使用一个棒表池。 如果其中一个平衡器发生故障,这将使无缝切换客户端网络成为可能;客户端会话的工作将在之前选择的相同后端上继续。
为了正确操作,必须解决建立会话的平衡器的源 IP 地址问题。 在我们的例子中,这是环回接口上的动态地址。
同伴的正确工作只有在一定条件下才能实现。 也就是说,TCP 超时必须足够大或切换必须足够快,以便 TCP 会话没有时间终止。 然而,它允许无缝切换。
在 IaaS 中,我们有一个使用相同技术构建的服务。 这 ,这就是所谓的奥克塔维亚。 它基于两个 HAProxy 进程,最初包括对对等点的支持。 他们在这项服务中证明了自己的出色。
该图示意性地显示了三个 HAProxy 实例之间对等表的移动,提出了如何配置的配置:

HAProxy Peers(会话同步)
如果实施相同的方案,则必须仔细测试其操作。 事实上,它并不是 100% 的时间都以同样的方式工作。 但至少当您需要记住客户端的源 IP 时,您不会丢失粘表。
限制同一客户端同时发出的请求数量
任何公开可用的服务(包括我们的 API)都可能受到大量请求的影响。 其原因可能完全不同,从用户错误到有针对性的攻击。 我们定期受到 IP 地址的 DDoS 攻击。 客户经常在脚本中犯错误,从而给我们带来小型 DDoS 攻击。
无论如何,必须提供额外的保护。 显而易见的解决方案是限制 API 请求的数量,而不是浪费 CPU 时间来处理恶意请求。
为了实现此类限制,我们使用基于 HAProxy 组织的速率限制,并使用相同的棒表。 设置限制非常简单,您可以通过对 API 的请求数量来限制用户。 该算法会记住发出请求的源 IP,并限制一个用户同时发出的请求数量。 当然,我们计算了每个服务的平均 API 负载配置文件,并将限制设置为该值的 ≈ 10 倍。 我们将继续密切关注事态发展并随时掌握动态。
这在实践中是什么样的? 我们的客户一直在使用我们的自动缩放 API。 他们早上创建大约两到三百台虚拟机,晚上删除它们。 对于 OpenStack 来说,创建虚拟机以及 PaaS 服务至少需要 1000 个 API 请求,因为服务之间的交互也是通过 API 进行的。
这样的任务转移会造成相当大的负载。 我们评估了这个负载,收集了每日峰值,将其增加了十倍,这成为了我们的速率限制。 我们密切关注脉搏。 我们经常看到机器人和扫描仪试图查看我们是否有可以运行的 CGA 脚本,我们正在积极削减它们。
如何在用户不注意的情况下更新代码库
我们还在代码部署流程级别实现容错。 推出期间可能会出现故障,但可以最大限度地减少其对服务可用性的影响。
我们不断更新我们的服务,并且必须确保代码库的更新不影响用户。 我们设法使用 HAProxy 的管理功能以及在我们的服务中实现 Graceful Shutdown 来解决这个问题。
为了解决这个问题,需要确保平衡器的控制和服务的“正确”关闭:
- 对于 HAProxy,控制是通过 stats 文件执行的,该文件本质上是一个套接字,并在 HAProxy 配置中定义。 您可以通过 stdio 向其发送命令。 但我们主要的配置控制工具是ansible,因此它内置了一个用于管理HAProxy的模块。 我们积极使用它。
- 我们的大多数 API 和引擎服务都支持优雅关闭技术:关闭时,它们会等待当前任务完成,无论是 http 请求还是某些服务任务。 同样的事情也发生在工人身上。 它知道它正在执行的所有任务,并在成功完成所有任务后结束。
由于这两点,我们部署的安全算法如下所示。
- 开发人员组装一个新的代码包(对我们来说这是 RPM),在开发环境中测试它,在阶段中测试它,并将其保留在阶段存储库中。
- 开发人员使用最详细的“工件”描述来设置部署任务:新包的版本、新功能的描述以及有关部署的其他详细信息(如果需要)。
- 系统管理员开始更新。 启动 Ansible playbook,它依次执行以下操作:
- 从阶段存储库中获取包并使用它来更新产品存储库中的包的版本。
- 编译更新服务的后端列表。
- 关闭 HAProxy 中第一个要更新的服务并等待其进程完成运行。 由于正常关闭,我们有信心所有当前的客户端请求都将成功完成。
- 在 API 和工作线程完全停止并且 HAProxy 关闭后,代码将被更新。
- Ansible 运行服务。
- 对于每项服务,都会拉出某些“句柄”,这些“句柄”对许多预定义的关键测试执行单元测试。 对新代码进行基本检查。
- 如果上一步没有发现错误,则激活后端。
- 让我们继续讨论下一个后端。
- 所有后端更新后,启动功能测试。 如果它们丢失,那么开发人员会查看他创建的任何新功能。
这样就完成了部署。

服务更新周期
如果我们没有一条规则,这个计划就行不通。 我们在战斗中支持新旧版本。 提前在软件开发阶段就规定,即使服务数据库有变化,也不会破坏之前的代码。 因此,代码库逐渐更新。
结论
分享一下我自己对容错WEB架构的看法,我想再次指出它的要点:
- 物理容错;
- 网络容错(平衡器、BGP);
- 使用和开发的软件的容错能力。
大家稳定正常运行!
来源: habr.com
