in 分布式数据库 数据库 ~ read.

分布式数据库之调度器

调度器

调度在计算机中是分配工作所需资源的方法。资源可以指虚拟的计算资源,如线程、进程或数据流;也可以指硬件资源,如处理器、网络连接或扩展卡。
进行调度工作的程序叫做调度器。调度器通常的实现使得所有计算资源都处于忙碌状态(在负载均衡中),允许多位用户有效地同时共享系统资源,或达到指定的服务质量。调度器使得在单处理器上通过多任务处理,从而让执行多个进程成为可能。
于分布式数据库中,调度器使得每条SQL语句在执行过程中的拆分成多个子任务进行并行处理,充分利用整个分布式系统的资源成为可能。

特点

  • 批量任务处理:对于同一批次进入调度器的子任务,统一进行派发执行,并确保所有任务执行完成后才会结束本次任务调度。
  • 隔离性:对于不同批次的任务之间不会产生关联关系。
  • 原子性:对于每一批次的任务会保证要么完全成功,要么将整体失败,不会存在部分任务成功的情况。

起源

调度器的发展,实际上可以参考分布式系统的起源,即:单机模式 --> 数据并行(数据分布式) --> 任务并行(任务分布式)。
以铁路售票系统为例进行说明:
我们将其简单分为三个子系统用户管理,火车票管理,订单管理,外加一个数据仓库。

起初,系统的并发度,访问量没有那么高,单独一个单体数据库足以支持多个子系统的运作,通常采用的是如下单机模式,
单机模式
单机模式下,调度器的主要考虑一个进程内的线程模型,代码和功能集中,便于维护和管理;

随着铁路系统的发展,单点数据库逐渐成为瓶颈,无法同时处理过多的任务,诞生了数据并行的模式,采用应用与数据分离,不同种类的数据分离的方式,它只能让相同的子任务针对不同来源的数据并行,在一定程度上缓解了单点数据库的性能瓶颈,
数据并行
数据并行模式下,调度器的架构模型与单机模式没有明显区别,依然只是进程内级别的线程模型的管理,仅仅是最后进行一次结果的汇总;

到了现在,简单的增删改查已经无法满足铁路售票系统的运作,它所执行的任务越来越复杂,很难产生相同的子任务去并行执行,带来的后果就是执行时间变长,用户体验变差,于是产生了任务并行的模式,
任务并行
任务并行模式下,调度器需要将单个任务拆分成多个子任务,多个子任务并行执行,只要这个复杂任务中的任意一个子任务的执行时间变短了,那这个业务的整体执行时间也会变短,从而提高系统的性能。

任务并行模式的发展,衍生出了分布式调度架构的三种方式:单体调度,双层调度,共享状态调度。关于这三种调度方式在本文不详细展开,后面另开文章详细讲解。

Cirrodata调度器

调度方式

调度器是一个批量任务(单任务也算是一种特殊的批量任务)处理的结构,对于同一批次进入调度器的子任务,统一进行派发执行,并确保所有任务执行完成后才会结束本次任务调度。
从批量任务处理的角度,将划分为三种调度方式:

调度方式 适用场景 描述 支持程度
单点调度 基于主键的增删改查 任务会基于入口节点得到执行
,尽可能降低调度对性能的影响
AllAtOnce调度 即席查询 对延迟敏感的复杂分层任务,
批量进行调度执行,尽可能使
得所有子任务同时得到执行
Phased调度 批量处理业务 对延迟不敏感,可以分阶段进
行执行,会按照任务的依赖层
次进行调度
×

基本架构

Cirrodata的调度器同样是基于任务并行模式,利用DAG规划每个子任务,可以准确描述各个任务的磁盘IO/CPU/内存/网络等资源的使用情况以及任务之间的依赖关系;
下面,简单介绍一下Cirrodata的调度器的基本架构:

  1. DAG:
    DAG并行派发
    我们采用基于DAG的并行任务调度,保证高性能的计算:
  • 首先我们会将任务请求按照DAG的模式拆解为多个子任务,并行派发执行;
  • 以根任务作为起点,向外分发所有子任务,各个子任务与根任务之间形成一个DAG;
  • 各个子任务之间,只存在上下层关系,同层任务之间不会形成关联,从而保证不会形成一个闭环;
    通过这个DAG模式,我们可以保证调度的根任务能够合理的管控所有子任务;
  1. 横向并行
    横向并行
    我们会将相同的任务作为同一层子任务进行派发,以达到类似于数据并行的效果,我们称之为横向并行:
  • 执行相同逻辑的一组同层子任务,是按照数据分布或hash分区进行划分的,可以并行执行;
  • 原则上,同一层执行的子任务工作内容都是相同的,通过并行执行相同的任务达到充分利用整个集群的资源进行计算,避免了将所有的计算或者扫描操作集中在一个Node上来完成,即会造成系统的超负载运行,也会影响整个任务的执行效率;
  1. 纵向并行
    纵向并行
    上下层级的子任务之间,按照PREFETCH模式保持执行并行性,我们称之为纵向并行
    由横向并行可以看到,上下层子任务之间所承担的任务是不同的,同时上层任务的输入依赖下层任务的输出数据,但这不意味着上下层子任务之间是顺序执行;
    首先上下层任务同时展开并且分别执行,N1的任务展开后会立刻向下层N2抽取数据,N2会向N5抽取数据,直到N5将数据读取出来推送给N2,经过N2的计算处理,将数据推送给N1,如果CLIENT没有向N1读取数据,此时数据存储在N1中,此流程为PREFETCH,即预读取模式。
PREFETCH模式:N1,N2,N5在任务展开后,主动进行如下操作
N1 --> N2  抽取数据
N2 --> N5  抽取数据
N2 <-- N5  推送数据
N1 <-- N2  推送数据

数据有两种传送方式,分别是:抽取式 和 推送式。
抽取式:由上层任务主动向下层要数据,优势是可以依据自己对数据的实际需求情况来决定是否要继续读取数据,劣势是无形之中上下层之间就形成了一个线性的关系,每次读取数据都是需要时才会自上而下的抽取一次数据,性能不会很好。
推送式:由下层任务主动向上层推送,优势是上层任务在读取数据时,不需要重新进行一个线性RPC的呼叫,性能有一定的保证,劣势是当数据量过多时,顶层任务会产生大量的数据积压,占用系统内存资源和IO资源,甚至还会需要占用磁盘资源,反而造成性能的下降。

我们的调度器在处理数据时采用半抽取式+半推送相结合的方式来完成,N1自上而下抽取(N1-N2-N5),N5自下而上进行推送(N5-N2-N1),每一个Node上都会缓存有N(N=3)包数据,当N1进行抽取时,首先会从缓存中抽取,如果不存在,则会等待下层N2计算完成后的推送结果;反之,当N5向上层进行推送时,只能推送N包数据至N2,当N2的缓存满时,N5等待N2有空余空间时才会继续推送;

N1 --> N2 --> N5 抽取式
N1 <-- N2 <-- N5 推送式

采用抽取式+推送式相结合的数据调度方式,我们可以结合它们的优点,摒弃它们的缺点,充分利用Node资源,同时不会产生过度的IO和网络开销,充分利用资源的同时保住了性能。

  1. 计算本地化
    传统的分布式调度器,其计算任务与数据的存储节点具有绑定关系,每个计算任务需要向下推送至数据的存储节点,占用磁盘的IO进行计算操作;
    这种方式会导致数据存储节点的资源使用压力过大,整体任务的性能会受限于几个数据存储节点。
    CirroData采用存储与计算分离的架构,底层存储采用基于hdfs分布式文件系统存储,所以我们可以在任何节点安排扫描数据的任务,来完成一次完整的任务调度,而不需要绑定计算任务与数据存储节点;
    同时,为了优化,我们实际上还是会选择尽可能接近存储数据的实际节点进行scan,即直接将扫描任务推送至数据的实际存储节点,减少大量数据的网络传输和磁盘IO所造成的性能影响;

综述

以上简单介绍了调度器的发展,以及CirroData数据库的调度器的基本架构,调度器的实现是一个较大的方面,它还包括调度器的工作流程,调度策略,多种复杂异常场景的处理,以及高并发情况下如何处理线程的并发模型,同时调度器作为一个复杂任务的主导者,还需要能够让任务在出错时,能为开发维护人员提供有效的定位信息,这些问题是一个分布式数据库需要不断探索和解决,不停寻找更优解的过程。

文章发布于公司公众号 CirroData 分布式数据库之调度器