7 种主流案例,告诉你调度器架构设计通用法则(上)

阅读数:412 2019 年 11 月 25 日 15:48

7种主流案例,告诉你调度器架构设计通用法则(上)

今天小编为大家转载一篇来自 DBAplus 社群的干货文章,希望能够帮助大家对关于调度器的理解。

在开始写这篇文章之前,我先阅读了大量集群资源管理和任务调度的资料和论文,了解了诸如:Hadoop YARN、Mesos、Spark Drizzle、Borg/Kubernetes 以及 Omega 等系统的调度器设计架构。在这篇文章里,我将试图从这些架构案例总结出此类系统一般的设计模式。

一、调度器的定义

无论是在单机系统还是分布式系统当中,调度器其实都是非常核心和普遍的组件,其内涵比较宽广,也比较模糊。

一般来说,下面提到的几种类型的模块都可以认为是调度器:

  • 早期计算机系统当中的批处理调度系统;
  • 现代计算机系统当中的抢占式进程调度系统和内存分配系统;
  • 某些系统或程序提供或实现的,定时激发某些类型操作的工具(如 crontab、Quartz 等);
  • 某些编程语言的 Runtime 提供的线程 / 纤程 / 协程调度器(如 Golang 内置的 Goroutine 调度器);
  • 分布式系统当中的任务关系管理和调度执行系统,(如 Hadoop YARN, Airflow 等);
  • 分布式系统当中的资源管理和调度系统(如 Mesos、Borg、Kubernetes 的调度器等)。

可以被称为调度器的工具涵盖范围非常广,他们有的提供定时激发任务的能力,有的提供资源管理的能力,有的负责维护任务的依赖关系和执行顺序,甚至有的系统还集成了任务监控和各种指标度量的工具。

这篇文章主要涉及的是管理系统资源和调度任务执行相关方面的架构和模型,具体的资源分配策略和任务调度策略不在本文讨论范围内。

二、调度器设计概述

在系统设计领域研究比较多的朋友可以容易地得出一个结论:那就是我们的系统设计——无论是小到一个嵌入式的系统,还是大到好几百个机器的集群——在设计抽象上都是在不同的层次上重复自己。

比如说:

  • 如果我们着眼于一个 CPU,它包括计算单元和一系列用来加速数据访问的缓存 L1、L2、L3 等,每种缓存具有不同的访问速度;
  • 当我们的视野扩大到整个机器,CPU 又可以被当成一个单元,我们又有内存和硬盘两个层次的储存系统用于加速数据载入;
  • 而在分布式系统中,如果我们把 HDFS 或 S3 看作硬盘,也存在像 Alluxio 这样发挥着类似内存作用的系统。

既然系统在设计上的基本原则都是类似的,那为什么大规模分布式系统的设计这么困难呢?

这是因为当问题的规模变化了,原先不显著或者容易解决的问题可能会变得难以解决。

举例来说,当我们谈论起进程间通信或者同一个 CPU 不同内核之间的通信时,我们往往不考虑通讯不稳定所带来的问题:我们无法想象如果一个 CPU 内核无法发送消息到另一个内核的状况。

然而在通过网络通讯的多机机群当中,这是无法回避的问题。Paxos、Raft、Zab 等算法被设计出来的原因也在于此。

我们先看一看单机操作系统调度器的发展路线:

  • 最早的调度器是批处理调度器,这种调度器批量调度、执行任务,通过对计算机资源的分时复用来增加资源的利用率,一般具有较高的吞吐量。
  • 后来,某些与外界交互次数频繁的系统对响应时间具有较强的要求,因此发展出了实时操作系统。实时操作系统的调度器具备低延迟相应外部信号的能力。
  • 我们常用的操作系统基本上以批处理的方式调度任务,又通过中断等机制提供实时性的保证,通过提供灵活的调度策略,在吞吐量和延迟时间当中获得平衡。

在分布式系统中的调度器的设计也是相同的。从单机调度器这里我们首先可以总结出调度器设计的三个最基本的需求:

  • 资源的有效利用
  • 信号的实时响应
  • 调度策略的灵活配置

这三个需求在某种程度上来说是相互矛盾的,在面对不同需求的时候需要做出不一样的取舍。

在上述三个需求的基础上,分布式的调度器设计还需要克服很多其他的困难。这些困难往往在单机系统当中并不显著。比如说:

  • 状态的同步问题
    在单机系统中,我们一般使用常规的同步方法,如:共享内存和锁机制,就可以很好地保证任务协调运行了。这是因为单机系统上的状态同步比较稳定、容易,但在分布式系统中,因为网络通讯的不确定性,使机群中的各个机器对于周围的状态达成一致是非常困难的。现实操作中,甚至无法在分布式系统中通过网络精确同步所有机器的时间!

  • 容错性问题
    由于单机系统的处理能力有限,我们运行任务的规模和同时运行任务的数量都比较有限。出错的概率和成本都比较低。但是在分布式系统中,由于任务规模变大、任务依赖关系变得更加复杂,出错的概率大大增加,错误恢复的成本可能也比较高,因此可能需要调度器快速地识别错误并进行恢复操作。

  • 可扩展性的问题
    当分布式系统的规模到达一定程度,调度器的可扩展性就可能会成为瓶颈。为了提供高可扩展性,调度器不但要可以应对管理上千台机器的挑战,也要能够处理类似动态增减节点这样的问题。

三、分布式调度器的分类

现阶段比较流行的分布式调度器可以归纳为三种类型,接下来会结合具体的案例进行介绍:

1、集中式调度器

集中式(Centralized)调度器也可以被称为宏(Monolithic)调度器,指的是使用中心化的方式管理资源和调度任务。也就是说,调度器本身在系统中只存在单个实例,所有的资源请求和任务调度都通过这一个实例进行。

下图展示了集中式调度器的一般模型。可以看到,在这一模型中,资源的使用状态和任务的执行状态都由中央调度器管理。

7种主流案例,告诉你调度器架构设计通用法则(上)

Centralized Scheduler

按照上面的思路,可以列出集中式调度器在各个方面的表现状况:

  • 适合批处理任务和吞吐量较大、运行时间较长的任务;
  • 调度算法只能全部内置在核心调度器当中,灵活性和策略的可扩展性不高;
  • 状态同步比较容易且稳定,这是因为资源使用和任务执行的状态被统一管理,降低了状态同步和并发控制的难度;
  • 由于存在单点故障的可能性,集中式调度器的容错性一般,有些系统通过热备份 Master 的方式提高可用性;
  • 由于所有的资源和任务请求都要由中央调度器处理,集中式调度器的可扩展性较差,容易成为分布式系统吞吐量的瓶颈;
  • 尽管应用场合比较局限,集中式调度器仍然是普遍使用的调度器,可广泛应用于中小规模数据平台应用。

案例 1: 单机操作系统的调度器

单机操作系统,如 Windows、Linux 和 macOS 的进程调度器是典型的集中式调度器。当用户请求执行应用之后,由操作系统将进程载入内存。计算机硬件的所有资源,包括 CPU、内存和硬盘等都由操作系统集中式管理。当进程需要时,通过系统调用请求操作系统分配资源。如果单机环境中的进程使用了系统级多线程,这些线程的调度也由系统一并控制。

案例 2: Hadoop YARN

对于集中式调度器,我们重点介绍 Hadoop YARN 这个案例。下图是将集中式调度器的一般模型替换成 YARN 当中术语之后的示意图:

7种主流案例,告诉你调度器架构设计通用法则(上)

Hadoop YARN

Hadoop YARN 的特点可以总结为:

  • 集中式的资源管理和调度器被称为 ResourceManager,所有的资源的空闲和使用情况都由 ResourceManager 管理,ResourceManager 也负责监控任务的执行;
  • 集群当中的每一个节点都运行着一个 NodeManager,这个 NodeManager 管理本地的资源占用和任务执行并将这些状态同步给 ResourceManager;
  • 集群中的任务运行在 Container 当中,YARN 使用 Container 作为资源分配的抽象单位,每个 Container 会被分配一些本地资源和运算资源等。

熟悉 YARN 的朋友可能知道 YARN 存在着一个 ApplicationMaster 的概念,也就是当一个应用被启动之后,一个 ApplicationMaster 会先在集群中被启动,随后 ApplicationMaster 会向 ResourceMaster 申请新的资源并调度新的任务。

这一模型好像看起来和后面介绍的双层调度器,特别是 Mesos 的设计有点相似,但一般仍认为 YARN 是 Monolithic 设计的调度器。这主要是因为:

  • ApplicationMaster 其实也是运行在一个 Container 里的 YARN job;
  • ApplicationMaster 虽然决定 Job 如何被激发,但是仍然需要请求 ResourceMaster 申请资源和启动新的 Job;
  • ApplicationMaster 启动的 Job 也会由 ResourceMaster 进行监控,其启动所需的本地资源和运算资源都由 ResourceMaster 负责分配并通知 NodeManager 具体执行。

在 YARN 中,为了防止 ResourceManager 出错退出,可以设计多个 Stand-By Master ,Stand-By Master 一直处于运行状态并和 ResourceManager 注册在同一个 ZooKeeper 集群中。

Active 的 ResourceManager 会定期保存自己的状态到 ZooKeeper,当其失败退出后,一个 Stand-By Master 会被选举出来成为新的 Manager。

7种主流案例,告诉你调度器架构设计通用法则(上)

Hadoop YARN:High Availability

作为一个分布式资源管理和调度器,YARN 与其竞争对手相比,功能其实比较薄弱。譬如默认情况下,YARN 只能对 Memory 资源施加限制(如果一个 Job 使用了超过许可的 Memory,YARN 会直接杀死进程)。

尽管其调度接口提供了对 CPU Cores 的抽象,但 YARN 默认情况下对任务使用 CPU 核数并没有任何限制。不过若运行在 Linux 环境下,在较新版本的 YARN 中可以配置 Cgroup 限制资源使用。

2、双层调度器

前面提到,集中式调度器的主要缺点在于单点模型容错性和可扩展性较差,容易成为性能瓶颈。在一般的数据密集型应用当中,解决这一问题的主要方法是分区。下图是双层调度器的一般模型:

7种主流案例,告诉你调度器架构设计通用法则(上)

2-level Scheduler

在双层调度器当中,资源的使用状态同时由分区调度器和中央调度器管理,但是中央调度器一般只负责宏观的、大规模的资源分配,业务压力较小。

分区调度器负责管理自己分区的所有资源和任务,一般只有当所在分区资源无法满足需求时,才将任务冒泡到中央调度器处理。

相比集中式调度器,双层调度器某一分区内的资源分配和工作安排可以由具体的任务本身进行定制,因此大大增强了使用的灵活性,可以同时对高吞吐和低延迟的两种场景提供良好的支持。

每个分区可以独立运行,降低了单点故障导致系统崩溃的概率,增加了可用性和可扩展性。但是反过来也导致状态同步和维护变得比较困难。

尽管主要思路是一致的,但双层调度器在实现上的变种比较丰富,本文接下来使用案例进行介绍:

案例 1: 协程调度器

单机操作系统的单个进程为了避免系统级多线程上下文切换的成本,可以自行实现进程内的调度器,如 Golang 运行时的 Goroutine 调度器。在这一模型下,一个进程内部的资源就相当于一个分区,分区内的资源由运行时提供的调度器预先申请并自行管理。运行时环境只有当资源耗尽时才会向系统请求新的资源,从而避免频繁的系统调用。

提出这个例子,主要目的在于说明类似的优化思路其实也被应用于分布式系统,再次证明了系统设计分层重复的特点!

评论

发布