一云——Odnoklassniki 的数据中心级操作系统

一云——Odnoklassniki 的数据中心级操作系统

阿罗哈,人们! 我叫 Oleg Anastasyev,在 Odnoklassniki 平台团队工作。 除了我之外,Odnoklassniki 还有很多硬件在工作。 我们有四个数据中心,约有 500 个机架,8 多台服务器。 在某个时刻,我们意识到引入新的管理系统将使我们能够更有效地加载设备,促进访问管理,自动化计算资源(重新)分配,加快新服务的推出并加快响应速度到大规模事故。

结果是什么呢?

除了我和一堆硬件之外,还有使用这些硬件的人:直接位于数据中心的工程师; 设置网络软件的网络人员; 提供基础设施弹性的管理员或 SRE; 和开发团队,每个人负责门户网站的部分功能。 他们创建的软件的工作原理如下:

一云——Odnoklassniki 的数据中心级操作系统

用户请求均在主门户的正面收到 www.ok.ru,以及其他方面,例如音乐 API 方面。 为了处理业务逻辑,他们调用应用程序服务器,应用程序服务器在处理请求时调用必要的专门微服务 - 单图(社交关系图)、用户缓存(用户配置文件缓存)等。

这些服务中的每一个都部署在许多机器上,每个服务都有负责的开发人员负责模块的功能、操作和技术开发。 所有这些服务都在硬件服务器上运行,直到最近我们才为每台服务器启动了一项任务,即它专门用于一项特定任务。

这是为什么? 这种方法有几个优点:

  • 松了口气 大众管理。 假设一项任务需要一些库和一些设置。 然后将服务器准确地分配到一个特定组,描述该组的 cfengine 策略(或已经描述过),并将此配置集中自动推广到该组中的所有服务器。
  • 简化版 诊断。 假设您查看了中央处理器上增加的负载,并意识到该负载只能由在此硬件处理器上运行的任务生成。 寻找罪魁祸首的搜寻很快就结束了。
  • 简化版 监控。 如果服务器出现问题,监视器会报告该问题,并且您可以确切地知道责任所在。

由多个副本组成的服务会分配多个服务器 - 每个服务器分配一个服务器。 那么服务的计算资源分配就非常简单:服务拥有的服务器数量,它可以消耗的最大资源量。 这里的“简单”并不是指易于使用,而是指资源分配是手动完成的。

这种方法还允许我们做 专门的铁配置 对于在此服务器上运行的任务。 如果任务存储大量数据,那么我们使用机箱4盘的38U服务器。 如果任务是纯粹的计算,那么我们可以购买更便宜的1U服务器。 这在计算上是高效的。 除此之外,这种方法使我们可以使用比一个友好的社交网络负载少四倍的机器。

如果我们从最昂贵的东西是服务器的前提出发,这样的计算资源使用效率也应该保证经济效率。 很长一段时间,硬件是最贵的,我们花了很多功夫来降低硬件的价格,提出容错算法来降低硬件的可靠性要求。 今天我们已经到了服务器价格不再具有决定性的阶段。 如果您不考虑最新的外来事物,那么机架中服务器的具体配置并不重要。 现在我们还有一个问题——数据中心内服务器所占用的空间的价格,即机架内的空间。

意识到这种情况后,我们决定计算一下机架的使用效率。
我们从经济上合理的服务器中选择最强大的服务器的价格,计算出我们可以在机架中放置多少台这样的服务器,根据旧模型“一台服务器=一个任务”,我们可以在它们上运行多少个任务,以及多少个这样的服务器。任务可以利用该设备。 他们数着数着,流下了眼泪。 事实证明,我们的机架使用效率约为11%。 结论很明显:我们需要提高数据中心的使用效率。 解决方案似乎很明显:您需要同时在一台服务器上运行多个任务。 但这就是困难的开始。

大规模配置变得更加复杂 - 现在不可能将任何一组分配给服务器。 毕竟,现在可以在一台服务器上启动不同命令的多个任务。 此外,不同应用程序的配置可能会发生冲突。 诊断也变得更加复杂:如果您发现服务器上的 CPU 或磁盘消耗增加,您不知道哪个任务导致了问题。

但最主要的是同一台机器上运行的任务之间不存在隔离。 例如,这里是在同一服务器上启动另一个计算应用程序之前和之后服务器任务的平均响应时间的图表,与第一个计算应用程序没有任何关系 - 主任务的响应时间显着增加。

一云——Odnoklassniki 的数据中心级操作系统

显然,您需要在容器或虚拟机中运行任务。 由于我们几乎所有的任务都在一个操作系统(Linux)下运行或适应它,因此我们不需要支持许多不同的操作系统。 因此,不需要虚拟化;由于额外的开销,它的效率将低于容器化。

作为直接在服务器上运行任务的容器实现,Docker 是一个很好的候选者:文件系统镜像很好地解决了配置冲突的问题。 镜像可以由多个层组成,这一事实使我们能够显着减少在基础设施上部署它们所需的数据量,将公共部分分离到单独的基础层中。 然后,基本(也是最庞大的)层将在整个基础设施中相当快地缓存,并且为了交付许多不同类型的应用程序和版本,只需要传输小层。

另外,Docker 中现成的注册表和图像标记为我们提供了用于版本控制和将代码交付到生产环境的现成原语。

Docker 与任何其他类似技术一样,为我们提供了某种程度的开箱即用的容器隔离。 例如内存隔离——每个容器都被赋予了机器内存的使用限制,超过这个限制就不会消耗。 您还可以根据 CPU 使用情况隔离容器。 然而,对我们来说,标准绝缘材料还不够。 但下面有更多内容。

在服务器上直接运行容器只是问题的一部分。 另一部分与在服务器上托管容器有关。 您需要了解哪个容器可以放置在哪个服务器上。 这并不是一件容易的事,因为容器需要尽可能密集地放置在服务器上,同时又不降低服务器的速度。 从容错的角度来看,这种放置也可能很困难。 通常我们希望将同一服务的副本放置在不同的机架甚至数据中心的不同房间中,这样如果某个机架或房间发生故障,我们不会立即丢失所有服务副本。

当您拥有 8 台服务器和 8-16 个容器时,手动分发容器不是一个选择。

此外,我们希望在资源分配方面给予开发人员更多的独立性,以便他们可以自己在生产中托管其服务,而无需管理员的帮助。 同时,我们希望保持控制,以便一些次要服务不会消耗我们数据中心的所有资源。

显然,我们需要一个能够自动执行此操作的控制层。

所以我们得出了一个所有建筑师都喜欢的简单易懂的图画:三个正方形。

一云——Odnoklassniki 的数据中心级操作系统

one-cloud masters 是一个负责云编排的故障转移集群。 开发人员向主服务器发送清单,其中包含托管服务所需的所有信息。 基于此,master 向选定的 minions(设计用于运行容器的机器)发出命令。 Minion有我们的代理,它接收命令,向Docker发出命令,Docker配置Linux内核以启动相应的容器。 除了执行命令之外,代理还不断向 master 报告 minion 机器及其上运行的容器的状态变化。

资源分配

现在让我们看看许多 Minion 的更复杂的资源分配问题。

单一云中的计算资源是:

  • 特定任务消耗的处理器电量。
  • 任务可用的内存量。
  • 网络流量。 每个 Minion 都有一个带宽有限的特定网络接口,因此不可能在不考虑它们通过网络传输的数据量的情况下分配任务。
  • 磁盘。 此外,显然,对于这些任务的空间,我们还分配磁盘类型:HDD 或 SSD。 磁盘每秒可以处理有限数量的请求 - IOPS。 因此,对于产生的 IOPS 超过单个磁盘可以处理的任务,我们还分配“主轴”,即必须专门为该任务保留的磁盘设备。

那么对于某些服务,例如用户缓存,我们可以这样记录消耗的资源:400 个处理器核心,2,5 TB 内存,双向 50 Gbit/s 流量,位于 6 个主轴上的 100 TB HDD 空间。 或者采用更熟悉的形式,如下所示:

alloc:
    cpu: 400
    mem: 2500
    lan_in: 50g
    lan_out: 50g
    hdd:100x6T

用户缓存服务资源仅消耗生产基础设施中所有可用资源的一部分。 因此,我想确保突然之间,无论是否由于操作员错误,用户缓存消耗的资源不会多于分配给它的资源。 也就是说,我们必须限制资源。 但我们可以将配额与什么挂钩呢?

让我们回到我们大大简化的组件交互图,并用更多细节重新绘制它 - 像这样:

一云——Odnoklassniki 的数据中心级操作系统

吸引眼球的是什么:

  • Web 前端和音乐使用同一应用程序服务器的隔离集群。
  • 我们可以区分这些集群所属的逻辑层:前端、缓存、数据存储和管理层。
  • 前端是异构的;它由不同的功能子系统组成。
  • 缓存还可以分散在它们缓存数据的子系统中。

我们再重新画一下图:

一云——Odnoklassniki 的数据中心级操作系统

呸! 是的,我们看到了层次结构! 这意味着您可以以更大的块分配资源:将负责的开发人员分配到该层次结构中与功能子系统(如图中的“音乐”)相对应的节点,并将配额附加到层次结构的同一级别。 这种层次结构也让我们能够更灵活地组织服务,以便于管理。 例如,我们将所有网络(因为这是一个非常大的服务器分组)分成几个较小的组,如图所示为 group1、group2。

通过删除多余的线,我们可以以更扁平的形式编写图片的每个节点: group1.web.front, api.music.front, 用户缓存.cache.

这就是我们如何得出“分层队列”的概念。 它的名称类似于“group1.web.front”。 为其分配资源配额和用户权限。 我们将授予 DevOps 人员向队列发送服务的权限,这样的员工可以在队列中启动某些内容,OpsDev 人员将拥有管理员权限,现在他可以管理队列,在那里分配人员,授予这些人权限等。在此队列上运行的服务将在队列的配额内运行。 如果队列的计算配额不足以一次性执行所有服务,那么它们将按顺序执行,从而形成队列本身。

让我们仔细看看服务。 服务有一个完全限定的名称,其中始终包含队列的名称。 然后前端网络服务将具有名称 ok-web.group1.web.front。 并且它访问的应用服务器服务会被调用 ok-app.group1.web.front。 每个服务都有一个清单,它指定放置在特定机器上的所有必要信息:该任务消耗多少资源、需要什么配置、应该有多少个副本、用于处理该服务故障的属性。 当服务直接放置在机器上后,它的实例就会出现。 它们也被明确命名 - 作为实例编号和服务名称: 1.ok-web.group1.web.front, 2.ok-web.group1.web.front, …

这非常方便:只需查看正在运行的容器的名称,我们就可以立即了解很多信息。

现在让我们仔细看看这些实例实际执行的内容:任务。

任务隔离类

OK 中的所有任务(可能是所有地方)都可以分为几组:

  • 短延迟任务 - prod。 对于此类任务和服务,响应延迟(延迟)非常重要,即系统处理每个请求的速度。 任务示例:Web 前端、缓存、应用程序服务器、OLTP 存储等。
  • 计算问题 - 批处理。 在这里,每个特定请求的处理速度并不重要。 对于他们来说,重要的是该任务在某个(长)时间段(吞吐量)内将执行多少次计算。 这些将是 MapReduce、Hadoop、机器学习、统计的任何任务。
  • 后台任务 - 空闲。 对于此类任务,延迟和吞吐量都不是很重要。 这包括各种测试、迁移、重新计算以及将数据从一种格式转换为另一种格式。 一方面,它们与计算的相似,另一方面,它们完成的速度对我们来说并不重要。

让我们看看这些任务如何消耗资源,例如中央处理器。

短延迟任务。 这样的任务将具有类似于以下的 CPU 消耗模式:

一云——Odnoklassniki 的数据中心级操作系统

收到用户的请求进行处理,任务开始使用所有可用的 CPU 内核,对其进行处理,返回响应,等待下一个请求并停止。 下一个请求到达 - 我们再次选择了那里的所有内容,计算了它,然后等待下一个请求。

为了保证此类任务的延迟最小,我们必须最大限度地利用其消耗的资源,并在 minion(将执行该任务的机器)上保留所需数量的内核。 那么我们问题的保留公式如下:

alloc: cpu = 4 (max)

如果我们有一台 16 核的 Minion 机器,那么正好可以在上面放置 XNUMX 个这样的任务。 我们特别注意到,此类任务的平均处理器消耗通常非常低 - 这是显而易见的,因为任务的很大一部分时间等待请求而不执行任何操作。

计算任务。 他们的模式会略有不同:

一云——Odnoklassniki 的数据中心级操作系统

此类任务的平均 CPU 资源消耗相当高。 通常我们希望一个计算任务能够在一定的时间内完成,因此我们需要预留它所需要的最少数量的处理器,以便整个计算在可接受的时间内完成。 它的保留公式如下所示:

alloc: cpu = [1,*)

“请把它放在一个至少有一个空闲核心的小兵身上,然后只要有多个核心,它就会吞噬一切。”

这里的使用效率已经比在短延迟任务上要好很多了。 但是,如果您将两种类型的任务组合在一台 Minion 机器上并随时随地分配其资源,那么收益会更大。 当具有短延迟的任务需要处理器时,它立即接收它,并且当不再需要资源时,它们被转移到计算任务,即像这样:

一云——Odnoklassniki 的数据中心级操作系统

但怎么办呢?

首先,让我们看一下 prod 及其分配:cpu = 4。我们需要保留四个核心。 在 Docker run 中,这可以通过两种方式完成:

  • 使用选项 --cpuset=1-4,即将机器上的四个特定核心分配给该任务。
  • 使用 --cpuquota=400_000 --cpuperiod=100_000,为处理器时间分配配额,即表明任务每 100 毫秒实时消耗不超过 400 毫秒的处理器时间。 获得相同的四个核心。

但这些方法中哪一种是合适的呢?

cpuset看起来相当吸引人。 该任务有四个专用核心,这意味着处理器缓存将尽可能高效地工作。 这也有一个缺点:我们必须承担在机器的卸载核心而不是操作系统上分配计算的任务,这是一项相当重要的任务,特别是如果我们尝试将批处理任务放在这样的计算机上机器。 测试表明,带有配额的选项更适合这里:这样操作系统可以更自由地选择当前执行任务的核心,并且处理器时间可以更有效地分配。

我们来看看如何在 Docker 中根据最小核心数进行预留。 批量任务的配额不再适用,因为不需要限制最大值,只保证最小值就足够了。 这里的选项很合适 docker run --cpushares.

我们同意,如果一个批次需要至少一个核心的保证,那么我们表明 --cpushares=1024,如果至少有两个核心,那么我们表明 --cpushares=2048。 只要有足够的 CPU 份额,就不会以任何方式干扰处理器时间的分配。 因此,如果 prod 当前未使用其所有四个核心,则批处理任务不会受到任何限制,并且它们可以使用额外的处理器时间。 但在处理器短缺的情况下,如果 prod 消耗了全部 1024 个核心并达到配额,则剩余处理器时间将按 cpushares 比例分配,即在 2048 个空闲核心的情况下,将分配一个分配给具有 XNUMX 个 cpushares 的任务,其余两个将分配给具有 XNUMX 个 cpushares 的任务。

但仅使用配额和份额还不够。 在分配处理器时间时,我们需要确保具有短延迟的任务比批处理任务具有更高的优先级。 如果没有这样的优先级,批处理任务将在产品需要时占用所有处理器时间。 Docker 运行中没有容器优先级选项,但 Linux CPU 调度程序策略会派上用场。 您可以详细阅读它们 这里,在本文的框架内,我们将简要介绍它们:

  • SCHED_OTHER
    默认情况下,Linux 计算机上的所有正常用户进程都会接收。
  • 计划批次
    专为资源密集型流程而设计。 当将任务放置在处理器上时,会引入所谓的激活惩罚:如果该任务当前正被具有 SCHED_OTHER 的任务使用,则该任务不太可能接收处理器资源
  • 计划闲置
    优先级非常低的后台进程,甚至低于nice -19。 我们使用我们的开源库 一尼奥,以便通过调用启动容器时设置必要的策略

one.nio.os.Proc.sched_setscheduler( pid, Proc.SCHED_IDLE )

但即使您不使用 Java 编程,也可以使用 chrt 命令完成相同的操作:

chrt -i 0 $pid

为了清楚起见,我们将所有隔离级别总结到一张表中:

绝缘级别
分配示例
Docker 运行选项
sched_setscheduler chrt*


中央处理器=4
--cpuquota=400000 --cpuperiod=100000
SCHED_OTHER

批量
CPU = [1, *)
--cpushares=1024
计划批次

空闲
中央处理器= [2, *)
--cpushares=2048
计划闲置

*如果您从容器内部执行 chrt,则可能需要 sys_nice 功能,因为默认情况下 Docker 在启动容器时会删除此功能。

但任务不仅消耗处理器,还消耗流量,这对网络任务延迟的影响甚至比处理器资源的错误分配还要大。 因此,我们自然希望得到一模一样的流量图。 也就是说,当一个 prod 任务向网络发送一些数据包时,我们限制最大速度(公式 分配: lan=[*,500mbps) ),用哪个产品可以做到这一点。 对于批量我们只保证最小吞吐量,但不限制最大吞吐量(公式 分配: lan=[10Mbps,*) )在这种情况下,生产流量应优先于批处理任务。
这里 Docker 没有任何我们可以使用的原语。 但这对我们有帮助 Linux 流量控制。 在纪律的帮助下我们能够达到预期的结果 分层公平服务曲线。 在它的帮助下,我们区分了两类流量:高优先级生产和低优先级批/空闲。 因此,出站流量的配置如下:

一云——Odnoklassniki 的数据中心级操作系统

这里 1:0 是 hsfc 规则的“root qdisc”; 1:1 - hsfc子类,总带宽限制为8 Gbit/s,所有容器的子类都放置在该子类之下; 1:2 - hsfc 子类对于所有具有“动态”限制的批处理和空闲任务都是通用的,这将在下面讨论。 其余的 hsfc 子类是当前运行的 prod 容器的专用类,其限制与其清单相对应 - 450 和 400 Mbit/s。 每个 hsfc 类都会分配一个 qdisc 队列 fq 或 fq_codel,具体取决于 Linux 内核版本,以避免流量突发期间丢包。

通常,tc 规则仅用于对传出流量进行优先级排序。 但我们也希望优先考虑传入流量 - 毕竟,某些批处理任务可以轻松选择整个传入通道,例如接收大量用于 Map&Reduce 的输入数据。 为此,我们使用该模块 ifb,它为每个网络接口创建一个 ifbX 虚拟接口,并将来自该接口的传入流量重定向到 ifbX 上的传出流量。 此外,对于 ifbX,所有相同的规则都用于控制传出流量,其 hsfc 配置将非常相似:

一云——Odnoklassniki 的数据中心级操作系统

在实验过程中,我们发现,当 1:2 类非优先级批量/空闲流量在 Minion 机器上限制为不超过某个空闲通道时,hsfc 会显示出最佳结果。 否则,非优先流量对生产任务的延迟影响太大。 miniond 每秒确定当前的可用带宽量,测量给定 minion 的所有 prod-task 的平均流量消耗 一云——Odnoklassniki 的数据中心级操作系统 并从网络接口带宽中减去它 一云——Odnoklassniki 的数据中心级操作系统 有很小的余量,即

一云——Odnoklassniki 的数据中心级操作系统

频带是为传入和传出流量独立定义的。 并且根据新值,miniond 重新配置非优先级限制 1:2。

因此,我们实现了所有三个隔离类:prod、batch 和idle。 这些类极大地影响任务的性能特征。 因此,我们决定将此属性放置在层次结构的顶部,以便在查看层次结构队列的名称时,可以立即清楚我们正在处理的内容:

一云——Odnoklassniki 的数据中心级操作系统

我们所有的朋友 卷筒纸 и 音乐 然后将前端放置在 prod 下的层次结构中。 例如,在批处理下,让我们将服务放置在 音乐目录,它定期从上传到 Odnoklassniki 的一组 mp3 文件中编译曲目目录。 空闲状态下的服务的一个示例是 音乐变压器,标准化音乐音量级别。

再次删除多余的行后,我们可以通过将任务隔离类添加到完整服务名称的末尾来将服务名称写得更扁平: web.front.prod, 目录.音乐.batch, 变压器.音乐.空闲.

现在,查看服务的名称,我们不仅了解它执行什么功能,还了解它的隔离类,这意味着它的关键性等。

一切都很棒,但有一个痛苦的事实。 完全隔离在一台机器上运行的任务是不可能的。

我们设法实现的目标:如果批量密集消耗 CPU 资源,那么内置的 Linux CPU 调度程序就可以很好地完成其工作,并且对 prod 任务几乎没有影响。 但如果这个批处理任务开始主动与内存合作,那么相互影响就已经出现了。 发生这种情况是因为 prod 任务被“清除”了处理器的内存缓存 - 结果,缓存未命中增加,并且处理器处理 prod 任务的速度变慢。 这样的批处理任务可以使我们典型的 prod 容器的延迟增加 10%。

由于现代网卡具有内部数据包队列,隔离流量变得更加困难。 如果来自批处理任务的数据包首先到达那里,那么它将是第一个通过电缆传输的数据包,对此无能为力。

此外,到目前为止,我们只能解决 TCP 流量优先级的问题:hsfc 方法不适用于 UDP。 而且即使在 TCP 流量的情况下,如果批处理任务产生大量流量,这也会使 prod 任务的延迟增加约 10%。

容错

开发一云的目标之一是提高 Odnoklassniki 的容错能力。 因此,接下来我想更详细地考虑故障和事故可能出现的情况。 让我们从一个简单的场景开始——容器故障。

容器本身可能会以多种方式发生故障。 这可能是清单中的某种实验、错误或错误,因此 prod 任务开始消耗比清单中指示的更多的资源。 我们有一个案例:一名开发人员实现了一个复杂的算法,对其进行了多次修改,自己思考过度,变得如此困惑,最终问题以一种非常不平凡的方式循环。 由于 prod 任务比同一 Minions 上的所有其他任务具有更高的优先级,因此它开始消耗所有可用的处理器资源。 在这种情况下,隔离,或者更确切地说,CPU 时间配额,挽救了局面。 如果为任务分配配额,则该任务不会消耗更多配额。 因此,在同一台机器上运行的批处理和其他生产任务没有注意到任何事情。

第二个可能的问题是容器掉落。 这里重启策略拯救了我们,每个人都知道它们,Docker 本身做得很好。 几乎所有生产任务都有始终重新启动策略。 有时我们使用 on_failure 来执行批处理任务或调试产品容器。

如果整个随从都不可用,您该怎么办?

显然,在另一台机器上运行容器。 这里有趣的部分是分配给容器的 IP 地址会发生什么情况。

我们可以为容器分配与运行这些容器的 Minion 机器相同的 IP 地址。 然后,当容器在另一台机器上启动时,它的IP地址发生变化,所有客户端都必须了解容器已经移动,现在他们需要转到不同的地址,这需要单独的服务发现服务。

服务发现很方便。 市场上有许多具有不同程度容错能力的解决方案用于组织服务注册表。 通常,此类解决方案会实现负载均衡器逻辑,以 KV 存储等形式存储附加配置。
但是,我们希望避免实现单独的注册表的需要,因为这意味着引入一个由生产中的所有服务使用的关键系统。 这意味着这是一个潜在的故障点,您需要选择或开发一个非常容错的解决方案,这显然是非常困难、耗时且昂贵的。

还有一个很大的缺点:为了让我们的旧基础设施能够与新基础设施一起工作,我们必须重写所有任务以使用某种服务发现系统。 有很多工作要做,在某些地方,当涉及到在操作系统内核级别或直接与硬件一起工作的低级设备时,这几乎是不可能的。 使用已建立的解决方案模式实现此功能,例如 边车 在某些地方意味着额外的负载,而在另一些地方则意味着操作的复杂性和额外的故障情况。 我们不想让事情变得复杂,因此我们决定将服务发现的使用作为可选。

在一云中,IP跟随容器,即每个任务实例都有自己的IP地址。 该地址是“静态的”:当服务首次发送到云时,它被分配给每个实例。 如果某个服务在其生命周期内拥有不同数量的实例,那么最终它将被分配与最大实例数一样多的 IP 地址。

随后,这些地址不会更改:它们被分配一次,并在生产服务的整个生命周期中继续存在。 IP 地址在网络中跟随容器。 如果容器被转移到另一个minion,那么地址将跟随它。

因此,服务名称到其 IP 地址列表的映射很少发生变化。 如果您再看一下我们在文章开头提到的服务实例的名称(1.ok-web.group1.web.front.prod, 2.ok-web.group1.web.front.prod, …),我们会注意到它们类似于 DNS 中使用的 FQDN。 没错,为了将服务实例的名称映射到它们的 IP 地址,我们使用 DNS 协议。 此外,此 DNS 返回所有容器的所有保留 IP 地址 - 包括运行的和停止的(假设使用了三个副本,并且我们在那里保留了五个地址 - 所有五个都将返回)。 收到此信息后的客户端将尝试与所有五个副本建立连接 - 从而确定那些正在工作的副本。 这种确定可用性的选项更加可靠;它不涉及 DNS 或服务发现,这意味着在确保这些系统的信息相关性和容错性方面不存在需要解决的难题。 而且,在整个门户运行所依赖的关键服务中,我们根本不能使用DNS,而只需在配置中输入IP地址即可。

在容器后面实现此类 IP 传输可能并不简单 - 我们将通过以下示例了解它是如何工作的:

一云——Odnoklassniki 的数据中心级操作系统

假设一云master向minion M1发出运行命令 1.ok-web.group1.web.front.prod 地址为 1.1.1.1。 适用于小兵 ,它将这个地址通告给特殊的服务器 路由反射器。 后者与网络硬件建立BGP会话,将M1.1.1.1上地址1的路由转换为该会话。 M1 使用 Linux 在容器内路由数据包。 一共有三个路由反射器服务器,因为这是一云基础设施中非常关键的部分 - 没有它们,一云中的网络将无法工作。 我们将它们放置在不同的机架中,如果可能的话,放置在数据中心的不同房间中,以减少所有三个同时发生故障的可能性。

现在我们假设一云Master和M1 Minion之间的连接丢失。 一云主机现在将假设 M1 完全失败而采取行动。 也就是说,它会给M2小兵发出发射命令 web.group1.web.front.prod 具有相同的地址 1.1.1.1。 现在,1.1.1.1 的网络上有两条相互冲突的路由:M1 和 M2。 为了解决此类冲突,我们使用多出口鉴别器,这在 BGP 公告中指定。 这是一个显示所通告路由的权重的数字。 在冲突的路由中,选择MED值较小的路由。 一云Master支持MED作为容器IP地址的组成部分。 第一次,地址写入了足够大的MED=1,在这种紧急集装箱转运的情况下,master减少了MED,M000已经收到了通告地址000的命令,MED=2 1.1.1.1。在这种情况下,在 M999 上运行的实例将保持在没有连接的状态,并且在与主机的连接恢复之前,我们对他的进一步命运没什么兴趣,此时他将像旧镜头一样被停止。

事故

所有数据中心管理系统总是能够以可接受的方式处理小故障。 容器溢出几乎在任何地方都是常态。

让我们看看如何处理紧急情况,例如数据中心的一个或多个房间发生电源故障。

事故对数据中心管理系统意味着什么? 首先,这是很多机器的大规模一次性故障,控制系统需要同时迁移很多容器。 但如果灾难规模非常大,那么可能会出现所有任务无法重新分配给其他minion的情况,因为数据中心的资源容量下降到负载的100%以下。

事故常常伴随着控制层的故障。 这种情况的发生可能是由于其设备的故障,但更多的情况是由于事故没有经过测试,以及控制层本身由于负载增加而掉落。

对于这一切你能做什么?

大规模迁移意味着基础设施中发生大量活动、迁移和部署。 每次迁移都可能需要一些时间来将容器映像交付和解压到 Minions、启动和初始化容器等。因此,最好先启动较重要的任务,然后再启动不太重要的任务。

让我们再次看看我们熟悉的服务层次结构,并尝试确定我们要首先运行哪些任务。

一云——Odnoklassniki 的数据中心级操作系统

当然,这些都是直接参与处理用户请求的进程,即prod。 我们用以下方式表示这一点 放置优先级 — 可以分配给队列的号码。 如果队列具有较高优先级,则其服务被放置在最前面。

在 prod 上,我们分配更高的优先级,0; 批量 - 稍微低一点,100; 空闲时 - 甚至更低,200。优先级按层次应用。 层次结构中较低的所有任务都将具有相应的优先级。 如果我们希望 prod 内的缓存在前端之前启动,那么我们将优先级分配给缓存 = 0 和前端子队列 = 1。例如,如果我们希望首先从前端启动主门户,并且仅从前端启动音乐那么,我们可以为后者分配一个较低的优先级 - 10。

下一个问题是缺乏资源。 因此,大量设备、数据中心的整个大厅都出现故障,我们重新启动了如此多的服务,以至于现在没有足够的资源供每个人使用。 您需要决定牺牲哪些任务以保持主要关键服务的运行。

一云——Odnoklassniki 的数据中心级操作系统

与放置优先级不同,我们不能不加区别地牺牲所有批处理任务;其中一些对于门户的运行很重要。 因此,我们单独强调 抢占优先级 任务。 放置后,如果没有更多的空闲 Minion,则较高优先级的任务可以抢占(即停止)较低优先级的任务。 在这种情况下,低优先级的任务可能会保持未放置状态,即不再有合适的具有足够可用资源的 Minion。

在我们的层次结构中,通过将空闲优先级指定为 200,可以非常简单地指定抢占优先级,以便生产任务和批处理任务抢占或停止空闲任务,但不会互相抢占或停止。就像放置优先级一样,我们可以使用我们的层次结构来描述更复杂的规则。 例如,如果我们没有足够的资源用于主门户网站,我们将牺牲音乐功能,将相应节点的优先级设置为较低:10。

整个 DC 事故

为什么整个数据中心会出现故障? 元素。 是个好帖子 飓风影响了数据中心的工作。 这些元素可以被认为是无家可归的人,他们曾经烧毁了歧管中的光学器件,并且数据中心与其他站点完全失去了联系。 失败的原因也可能是人为因素:操作员会发出这样的命令,整个数据中心就会陷落。 这可能是由于一个大错误而发生的。 一般来说,数据中心崩溃的情况并不少见。 这种情况每隔几个月就会在我们身上发生一次。

这就是我们为防止任何人发推文 #alive 所做的事情。

第一个策略是隔离。 每个一云实例都是隔离的,只能管理一个数据中心的机器。 也就是说,由于错误或不正确的操作员命令而导致的云损失仅相当于一个数据中心的损失。 我们已为此做好准备:我们有一个冗余策略,其中应用程序和数据的副本位于所有数据中心。 我们使用容错数据库并定期测试故障。
从今天开始,我们有四个数据中心,这意味着四个独立、完全隔离的一云实例。

这种方法不仅可以防止物理故障,还可以防止操作员错误。

人为因素还能做什么? 当操作员向云端发出一些奇怪或有潜在危险的命令时,他可能会突然被要求解决一个小问题,看看他的想法如何。 例如,如果这是许多副本的某种大规模停止或只是一个奇怪的命令 - 减少副本数量或更改映像的名称,而不仅仅是新清单中的版本号。

一云——Odnoklassniki 的数据中心级操作系统

结果

一云的显着特点:

  • 服务和容器的分层和可视化命名方案,这使您可以非常快速地找出任务是什么、它与什么相关、它是如何工作的以及谁负责它。
  • 我们应用我们的 产品与批次相结合的技术Minions上的任务,以提高机器共享的效率。 我们使用 CPU 配额、共享、CPU 调度程序策略和 Linux QoS 来代替 cpuset。
  • 同一台机器上运行的容器不可能完全隔离,但它们之间的相互影响保持在20%以内。
  • 将服务组织成层次结构有助于使用以下命令进行自动灾难恢复 放置和抢占优先级.

常问问题

为什么我们不采取现成的解决方案?

  • 不同类别的任务隔离在置于 Minions 上时需要不同的逻辑。 如果简单的预留资源就可以放置prod任务,那么就必须放置batch和idle任务,跟踪minion机器上资源的实际利用率。
  • 需要考虑任务消耗的资源,例如:
    • 网络带宽;
    • 磁盘的类型和“主轴”。
  • 应急响应时需要指明服务的优先级、资源的命令权限和配额,通过一云中的分层队列来解决。
  • 需要对容器进行人工命名,以减少对事故和事件的响应时间
  • 一次性广泛实施服务发现是不可能的; 需要与硬件主机上托管的任务长期共存 - 这可以通过容器后面的“静态”IP 地址来解决,因此需要与大型网络基础设施进行独特的集成。

所有这些功能都需要对现有解决方案进行重大修改才能适合我们,并且在评估工作量后,我们意识到我们可以以大致相同的劳动力成本开发自己的解决方案。 但是您的解决方案将更容易操作和开发 - 它不包含支持我们不需要的功能的不必要的抽象。

对于那些阅读最后几行的人,感谢您的耐心和关注!

来源: habr.com

添加评论