分布式调度系统(3)——任务分配

Python015

分布式调度系统(3)——任务分配,第1张

将任务分配到具体执行节点

以上配置信息保存在 /config/job/{jobName} 节点。

任务分配的目标是:让当前工作尽可能均衡地在各执行节点上执行。

触发任务分配的事件,可能是:

任务分配单元 : 将任务拆分为最小可执行的粒度,如:一个父任务、一个shard子任务,一个子任务,作为一个分配单元。每个分配单元包括:(父任务名称,子任务shardItem序号/childIndex流水号,load)。

待分配任务列表 :按任务分类,每个任务形成一个待分配列表,按load倒叙、sharingItem/childIndex正序 排序。

节点下线 :在节点下线的场景,需要将故障节点下的任务摘下,按任务放入多个待分配任务列表。

任务启动 :对于任务启动的场景,任务下所有的任务分配单元均需要重新分配,都放入到待分配任务列表中。

节点上线 :在节点上线的场景中,需读取 /config/node/{nodeName} 数据, 确定受影响的任务。读取 /runtime/job/{jobName}/availableNodes/{nodeName} 确认受影响的节点。 计算新节点上线后平均每节点load,从负载最多的节点摘下。摘取时,每次摘取不同任务,直至节点node在平均值以下。

构建节点列表,构建 Map<String /*node name*/ , Long /*node load*/ >。

逐个任务分配,优先分配总load高的任务。

每个任务按load倒叙,每次分配分配到不同节点,优先分配总load低的node。

如果节点load已经高出平均值,则退出待分配node列表。

集群启动过程中, 节点不断加进来, 不断触发任务重新分配, 这是没有必要的。

冷冻时间 :集群初始启动过程中,第1个启动的节点确认没有其他节点后,会创建 /runtime/frozonTime 节点,节点数据为解冻的时间点(一般为5分钟后)。

在解冻时间到达之前,不触发任务分配。

创建冷冻节点的同时,清理availableNodes,和 /runtime/node/{nodeName}/job 。

集群收到 shutdown 调用后,开始关闭集群,关闭顺序:

1. 介绍

最近在研究一些消息中间件,常用的MQ如RabbitMQ,ActiveMQ,Kafka等。NSQ是一个基于Go语言的分布式实时消息平台,它基于MIT开源协议发布,由bitly公司开源出来的一款简单易用的消息中间件。

官方和第三方还为NSQ开发了众多客户端功能库,如官方提供的基于HTTP的nsqd、Go客户端go-nsq、Python客户端pynsq、基于Node.js的JavaScript客户端nsqjs、异步C客户端libnsq、Java客户端nsq-java以及基于各种语言的众多第三方客户端功能库。

1.1 Features

1). Distributed

NSQ提供了分布式的,去中心化,且没有单点故障的拓扑结构,稳定的消息传输发布保障,能够具有高容错和HA(高可用)特性。

2). Scalable易于扩展

NSQ支持水平扩展,没有中心化的brokers。内置的发现服务简化了在集群中增加节点。同时支持pub-sub和load-balanced 的消息分发。

3). Ops Friendly

NSQ非常容易配置和部署,生来就绑定了一个管理界面。二进制包没有运行时依赖。官方有Docker image。

4.Integrated高度集成

官方的 Go 和 Python库都有提供。而且为大多数语言提供了库。

1.2 组件

1.3 拓扑结构

NSQ推荐通过他们相应的nsqd实例使用协同定位发布者,这意味着即使面对网络分区,消息也会被保存在本地,直到它们被一个消费者读取。更重要的是,发布者不必去发现其他的nsqd节点,他们总是可以向本地实例发布消息。

NSQ

首先,一个发布者向它的本地nsqd发送消息,要做到这点,首先要先打开一个连接,然后发送一个包含topic和消息主体的发布命令,在这种情况下,我们将消息发布到事件topic上以分散到我们不同的worker中。

事件topic会复制这些消息并且在每一个连接topic的channel上进行排队,在我们的案例中,有三个channel,它们其中之一作为档案channel。消费者会获取这些消息并且上传到S3。

nsqd

每个channel的消息都会进行排队,直到一个worker把他们消费,如果此队列超出了内存限制,消息将会被写入到磁盘中。Nsqd节点首先会向nsqlookup广播他们的位置信息,一旦它们注册成功,worker将会从nsqlookup服务器节点上发现所有包含事件topic的nsqd节点。

nsqlookupd

2. Internals

2.1 消息传递担保

1)客户表示已经准备好接收消息

2)NSQ 发送一条消息,并暂时将数据存储在本地(在 re-queue 或 timeout)

3)客户端回复 FIN(结束)或 REQ(重新排队)分别指示成功或失败。如果客户端没有回复, NSQ 会在设定的时间超时,自动重新排队消息

这确保了消息丢失唯一可能的情况是不正常结束 nsqd 进程。在这种情况下,这是在内存中的任何信息(或任何缓冲未刷新到磁盘)都将丢失。

如何防止消息丢失是最重要的,即使是这个意外情况可以得到缓解。一种解决方案是构成冗余 nsqd对(在不同的主机上)接收消息的相同部分的副本。因为你实现的消费者是幂等的,以两倍时间处理这些消息不会对下游造成影响,并使得系统能够承受任何单一节点故障而不会丢失信息。

2.2 简化配置和管理

单个 nsqd 实例被设计成可以同时处理多个数据流。流被称为“话题”和话题有 1 个或多个“通道”。每个通道都接收到一个话题中所有消息的拷贝。在实践中,一个通道映射到下行服务消费一个话题。

在更底的层面,每个 nsqd 有一个与 nsqlookupd 的长期 TCP 连接,定期推动其状态。这个数据被 nsqlookupd 用于给消费者通知 nsqd 地址。对于消费者来说,一个暴露的 HTTP /lookup 接口用于轮询。为话题引入一个新的消费者,只需启动一个配置了 nsqlookup 实例地址的 NSQ 客户端。无需为添加任何新的消费者或生产者更改配置,大大降低了开销和复杂性。

2.3 消除单点故障

NSQ被设计以分布的方式被使用。nsqd 客户端(通过 TCP )连接到指定话题的所有生产者实例。没有中间人,没有消息代理,也没有单点故障。

这种拓扑结构消除单链,聚合,反馈。相反,你的消费者直接访问所有生产者。从技术上讲,哪个客户端连接到哪个 NSQ 不重要,只要有足够的消费者连接到所有生产者,以满足大量的消息,保证所有东西最终将被处理。对于 nsqlookupd,高可用性是通过运行多个实例来实现。他们不直接相互通信和数据被认为是最终一致。消费者轮询所有的配置的 nsqlookupd 实例和合并 response。失败的,无法访问的,或以其他方式故障的节点不会让系统陷于停顿。

2.4 效率

对于数据的协议,通过推送数据到客户端最大限度地提高性能和吞吐量的,而不是等待客户端拉数据。这个概念,称之为 RDY 状态,基本上是客户端流量控制的一种形式。

efficiency

2.5 心跳和超时

组合应用级别的心跳和 RDY 状态,避免头阻塞现象,也可能使心跳无用(即,如果消费者是在后面的处理消息流的接收缓冲区中,操作系统将被填满,堵心跳)为了保证进度,所有的网络 IO 时间上限势必与配置的心跳间隔相关联。这意味着,你可以从字面上拔掉之间的网络连接 nsqd 和消费者,它会检测并正确处理错误。当检测到一个致命错误,客户端连接被强制关闭。在传输中的消息会超时而重新排队等待传递到另一个消费者。最后,错误会被记录并累计到各种内部指标。

2.6 分布式

因为NSQ没有在守护程序之间共享信息,所以它从一开始就是为了分布式操作而生。个别的机器可以随便宕机随便启动而不会影响到系统的其余部分,消息发布者可以在本地发布,即使面对网络分区。

这种“分布式优先”的设计理念意味着NSQ基本上可以永远不断地扩展,需要更高的吞吐量?那就添加更多的nsqd吧。唯一的共享状态就是保存在lookup节点上,甚至它们不需要全局视图,配置某些nsqd注册到某些lookup节点上这是很简单的配置,唯一关键的地方就是消费者可以通过lookup节点获取所有完整的节点集。清晰的故障事件——NSQ在组件内建立了一套明确关于可能导致故障的的故障权衡机制,这对消息传递和恢复都有意义。虽然它们可能不像Kafka系统那样提供严格的保证级别,但NSQ简单的操作使故障情况非常明显。

2.7 no replication

不像其他的队列组件,NSQ并没有提供任何形式的复制和集群,也正是这点让它能够如此简单地运行,但它确实对于一些高保证性高可靠性的消息发布没有足够的保证。我们可以通过降低文件同步的时间来部分避免,只需通过一个标志配置,通过EBS支持我们的队列。但是这样仍然存在一个消息被发布后马上死亡,丢失了有效的写入的情况。

2.8 没有严格的顺序

虽然Kafka由一个有序的日志构成,但NSQ不是。消息可以在任何时间以任何顺序进入队列。在我们使用的案例中,这通常没有关系,因为所有的数据都被加上了时间戳,但它并不适合需要严格顺序的情况。

2.9 无数据重复删除功能

NSQ对于超时系统,它使用了心跳检测机制去测试消费者是否存活还是死亡。很多原因会导致我们的consumer无法完成心跳检测,所以在consumer中必须有一个单独的步骤确保幂等性。

3. 实践安装过程

本文将nsq集群具体的安装过程略去,大家可以自行参考官网,比较简单。这部分介绍下笔者实验的拓扑,以及nsqadmin的相关信息。

3.1 拓扑结构

topology

实验采用3台NSQD服务,2台LOOKUPD服务。

采用官方推荐的拓扑,消息发布的服务和NSQD在一台主机。一共5台机器。

NSQ基本没有配置文件,配置通过命令行指定参数。

主要命令如下:

LOOKUPD命令

NSQD命令

工具类,消费后存储到本地文件。

发布一条消息

3.2 nsqadmin

对Streams的详细信息进行查看,包括NSQD节点,具体的channel,队列中的消息数,连接数等信息。

nsqadmin

channel

列出所有的NSQD节点:

nodes

消息的统计:

msgs

lookup主机的列表:

hosts

4. 总结

NSQ基本核心就是简单性,是一个简单的队列,这意味着它很容易进行故障推理和很容易发现bug。消费者可以自行处理故障事件而不会影响系统剩下的其余部分。

事实上,简单性是我们决定使用NSQ的首要因素,这方便与我们的许多其他软件一起维护,通过引入队列使我们得到了堪称完美的表现,通过队列甚至让我们增加了几个数量级的吞吐量。越来越多的consumer需要一套严格可靠性和顺序性保障,这已经超过了NSQ提供的简单功能。

结合我们的业务系统来看,对于我们所需要传输的发票消息,相对比较敏感,无法容忍某个nsqd宕机,或者磁盘无法使用的情况,该节点堆积的消息无法找回。这是我们没有选择该消息中间件的主要原因。简单性和可靠性似乎并不能完全满足。相比Kafka,ops肩负起更多负责的运营。另一方面,它拥有一个可复制的、有序的日志可以提供给我们更好的服务。但对于其他适合NSQ的consumer,它为我们服务的相当好,我们期待着继续巩固它的坚实的基础。

本文基于对redis、zookpeer、rocketmq、elasticsearch学习总结,对于分布式系统学习,一定绕不开一个点,那就是CAP定理。什么是CAP定理,我这里简单的复制摘抄一下百度上的文案。

CAP原则又称CAP定理,指的是在一个分布式系统中,一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance)。CAP 原则指的是,这三个要素最多只能同时实现两点,不可能三者兼顾。

说明一下上面的三个要素各代表的含义:

CAP定理说明上述的三个要素不能兼顾,最多只能满足其中的两个要素,在分布式系统中,一般都是保证分区容错性,而在一致性和可用性之间做取舍。因此存在CP、AP两种分布式集群的实现。

CP集群,即满足一致性和分区容错性,如zookpeer

AP集群,即满足可用性和分区容错性,如redis-cluster

下面,针对与上述的CP和AP问题,我们展开话题。

对于分布式系统,学习了解多了之后,发现其内在的解决方案基本上都是一样的,所谓万变不离其中。总结一下大体在于以下几步:

数据分片,很多分布式系统尤其是中间件服务,一般都会涉及高并发,数据量大的问题,如redis-cluster、recketmq,以及被大家熟知的Elasticsearch。针对于大数据量高并发的问题,若不做处理,服务器的性能将会成为服务的瓶颈,解决的方案之一便是数据分片,将大数据量在集群中按照一定的规则分片,使数据按照一定的规则分布集群的不同服务器上,以减轻单个服务器的压力,保证服务集群的可用性。

redis-cluster的数据分片是通过redis-cluster的哈希槽来实现的,redis-cluster有16384个哈希槽,这个数量是固定的,根据集群中服务器的数量可以手动的调配每个服务上存放的hash槽的数量,哈希槽之间是相互独立的,因此对集群的扩展提供了便利。

rocketmq的分片和topic紧密相关,在使用rocketmq中,无论是消息的生产者还是消费者都需要注册订阅一个topic。在rocketmq集群中,集群中的broker保存这个topic下数据的一部分,也就是topic的其中一个数据分片。当然,rocketmq不仅将一个topic下的数据分片到多个broker上,而且,一个broker上的topic数据还可以被分为多个queue,这是因为rocketmq中,一个queue只能被一个consumer消费,若是consumer的数量多于queue的数量,没有绑定queue的consumer将不能消费数据。

elasticsearch的数据分片在我看来和mysql的分库分表原理是一样的,elasticsearch中,每一个索引都相当于mysql的一个表,将一个索引分成多个shard放在不同的节点上,每个shard存储一部分数据。elasticsearch将数据进行分片,这样可以支持集群的横向扩展,同时,多个节点提供服务可以提高系统的效率和吞吐量。

综上所述,数据分片的一般都有两个好处,一个是支持集群的横向扩展,而是提升服务的吞吐量和性能。数据分片解决了以上两个问题,但是若是集群中一个节点发生宕机,或者因为网络原因和集群断开链接,那么这部分的数据分片甚至整个集群都会不可用,如何解决这个问题,就需要用到数据备份和主备切换。

数据分片的策略 了解了数据分片之后,需要了解以下数据分片的策略,根据集群提供服务的性质不同,可以采用的数据分片策略也各有不同,下面是我学习后的总结:

说到这里,会发现其实这种分片策略和负载均衡的策略还是挺相似的。

数据备份,举个例子来说,我有两台电脑A、电脑B,A用于工作,B用于游戏,我写了一篇文章,保存在电脑上电脑上,若是某一天我的电脑A磁盘坏了,那我这篇文章就找不到了,即便我现在还有电脑B,我也没有办法在对文章进行编辑。但是若是我在之前,就将文章拷贝了一份放在电脑B上,那么现在,我用电脑B就可以对文件进行编辑修改。

举这个例子,我的目的就是为了说明数据备份对于集群可用性的意义,例子中,我的两台电脑可以认为是集群中两台服务器,两台服务器一开始提供的服务可能不相同,A电脑提供的就是编辑文章的服务,数据备份的意义就在于,当原本提供服务的服务器宕机损坏,集群中另外的服务器仍然可以根据已经备份的数据提供相同的服务,而不会影响到用户的工作。

数据备份的目的就是不发生单点问题的措施之一,但是若是数据备份的策略不合适,备份的时机不对,那么备份的数据时效性也是问题。还是从例子出发,这里的文章每次都是我手动从A电脑拷贝到B电脑,这是我的备份策略,若是我选择每天晚上才拷贝一次,那么若是A电脑在我拷贝之前坏了,当天的文章编辑数据就丢失了,采用手动的方式备份,这种备份方式耗时耗力且不可控,而在分布式集群中,不同的系统采用了不同的备份策略,下面一一来说明。

首先明确一点,在分布式集群中,不可能采用人工手动备份,一定是系统程序按照一定的规则自动备份,就好像我将AB连在一起,写个程序,让A电脑自动把文章同步到B电脑。数据备份的方式分为两种:

这里以redis-cluster和zookeeper举例。

在redis-cluster中,当一台新的slave节点加入时,会出发数据同步,需要将主节点的数据同步到从节点。这时根据从节点的状态有两种同步方案:完整重同步 和 部分重同步

完整重同步既是将主节点的全部数据都复制给新的slave节点。大致流程为,当一个新的节点加入进来时,发送PSYNC命令给主节点并携带slave节点自身的信息(重点是复制偏移量),主节点会根据slave传过来的信息判断是完整重同步还是部分重同步,如何判断与数据同步时的复制缓冲区有关,更细节不展开介绍。

相对于redis-cluster,zookeeper中的数据同步有四种方式,和redis-cluster完整重同步和部分重同步相似的SNAP(全量同步)和DIFF(增量同步),以及zk事务处理相关的TRUNC(仅回滚同步)、TRUNC+DIFF(回滚+增量同步)

当节点已经加入集群,成为集群中的从节点,只要不断开连接,一般都只需要进行增量同步,不过系统同步的范围和方式有所差异,大致分为下面六种:

下面还是以具体服务来举例: redis-cluster中,主从复制采用的是异步复制的方式,master节点在做数据变更之后,会由一个异步线程将数据变更同步给slave节点,这是通过push的方式。当redis2.8之后,slave会周期的获取最新的数据,加入了pull方式。无论是master还是slave,在进行数据同步时,不会阻塞正常的应用请求。所以redis-cluster的主从复制,是异步备份+最终一致性的备份。

elasticsearch的主从复制可以手动设置同步备份或者异步备份,数据备份时不要求强一致性,而是主分片(primary shard)会维护一份需要同步的(replica shard)分片列表,这个分片列表同步完成,则认为数据备份完成,需要注意的是,这里的主从复制不是节点的更新数据,而是分片的更新数据。

rocketmq的主从复制和elasticsearch类似,也可以分为同步备份和异步备份,不同的是rocketetmq的数据备份采用的是pull的方式,从节点会通过HAConnection链接主动向主节点发送待拉取数据偏移量,待主节点返回节点更新数据信息,更新从节点数据偏移量,如此重复。

zookeeper的数据备份则是通过ZAB协议,通过消息广播的方式同步数据到从节点。

当数据备份后,主从节点上就有了相同的数据,为了提升服务的性能,那么可以采用读写分离的方式。主节点提供数据写服务,从节点提供读服务,可以有效的分担主节点的服务器压力。可以进行数据分片的系统,如:redis、rocketmq、elasticsearch,一般都可以配置一主多从、多主多从的集群架构。

读写分离之后,主节点提供写服务,从节点只提供读服务,因此若是主节点发生宕机,从节点依然可以提供读服务,但是服务无法更新数据,这时候就要进行主从切换。早起,主从切换可以由人工手动完成,不过随着技术发展,主从切换已经成为集群的必备功能。想要实现主从切换,必须要解决两个问题:

解决这个问题,需要额外再引入一个角色,相当于是一个监视者的角色,能够长期的对主节点进行监视,若是只有一个监视者,可能会发生误判,所以还需要一套机制去保证当监视者说主节点宕机,那么主节点是真的宕机,否则集群会出现脑裂问题。

以redis为例,在redis的哨兵模式中,这个监视者的角色是一个个哨兵实例,而在redis-cluster架构中,这个监视者的角色是redis实例自己。

在redis哨兵模式中,哨兵集群中的哨兵实例会定期和redis实例进行通信(ping),监视redis实例的在线情况,若是其中一台哨兵发现redis实例master故障,那么该哨兵会将该master状态改为主观下线,并通知其他哨兵,当哨兵集群中达到配置数量的哨兵实例认为该master都为主观下线状态,这时会将master修改为客观下线状态,并开始触发后续的故障转移。

在redis-cluster模式中,集群中的每一个节点都可以和其他节点通讯(ping),当某一个节点A发现主节点B下线了,A会将该主节点B设为疑似下线状态。集群中的节点会通过互发消息维护信息,当另一个节点C收到A的消息时,会将A对B节点的判断记录在C节点的维护信息下,这个信息可以理解为A说C疑似下线了。若是有其他节点发送C的状态信息,A同样也会记录。当某一个节点如C发现记录的B节点信息中,超过半数的主节点都认为B下线了,那么C就会将B节点状态修改为已下线状态,并广播消息给集群的其他节点,开始后续的故障转移。

上面就是redis的两种分布式模式故障检测的方案。大致可以归结为,监视节点会和被监视节点进行通讯,感知被监视节点的状态;监视节点之间也会进行通讯,同步信息。为了防止集群出现脑裂,对于某个主节点的故障判断会十分的谨慎,需要达到一定数量的监视节点都认为主节点故障时,才会认为主节点真的故障,从而触发故障转移。

在rocketmq集群模式中,nameserver扮演着监视者的角色(不同于其他系统,nameserver并不负责集群的主从切换,rocketmq 4.5之前不支持自动主从切换,4.5之后,通过dledger实现自动的故障转移)。在elasticsearch集群中,elasticsearch实例本身在扮演监视者角色。zookeeper也是实例本身扮演监视者的角色。

故障转移就是当集群发现集群中的主节点/从节点发生故障之后的处理,从节点比较简单,直接将从节点下线即可,主节点的故障转移流程比较复杂,各个系统根据系统的功能和架构有不同的实现方式,共同点是选举出的主节点一定是集群中数据最新的最完善的节点。

选举过程大致如下:

首先选举成功的条件时集群中具有投票权限的超过半数的节点投票一致,通过某一个节点成为主节点。

开始一轮选举时,定义为一个纪元,用一个自增的id表示。

候选节点将带着纪元id,以及自身信息作为投票申请广播给集群给可投票的节点。

具有投票权限的节点投票只要满足两个条件:1.自身在最新纪元没有给投过票 2.节点发送过来的投票申请时最新纪元的(如何判断时最新纪元,则是判断一下节点之前通过申请的纪元id是否小于当前申请的纪元id)。

半数以上的投票节点通过某一个候选节点成为leader节点,则leader产生。

若是一个纪元没有产生主节点,则候选节点进入随机的休眠,并且开启下一个纪元,知道产生leader节点。

在zk集群经过崩溃恢复模式之后,需要保证:1.已经提交的事务不能丢失 2.未被提交的事务不能出现。如何保证以上两点,zk服务集群中维护了zxid,zxid也可以看作是一个自增的id,集群中每产生一个新事物,zxid就会增加。zxid有64位,前32位维护了集群主节点变更情况,每重新选举出一个新的主节点则增加,后32位维护在新的主节点集群下事务的id,产生一个新事物则增加。

ZAB的选举模式有很多种,我主要了解了默认,也是推荐的FastLeaderElection模式,在这个模式下,我会以集群中一台参与选举的服务器的视角来模拟选主的过程;

我是一台zk服务器,我现在很慌,因为我的leader服务器不见了,作为一个有梦想的follower,我也要参加leader的选举,为了这次选举我要准备:myid(在集群中标识是这台服务器的id),zxid(本台服务器保存的最新事务id),logicClock(本台服务器发起的第几轮投票)

首先我会自己选自己,这得自信。于是我将自身的选举信息[myid, zxid]放到自己的收票箱,然后将我的选举信息还有我的选举轮次logicClock广播给其他服务器进行PK

作为一个有原则的服务器,我们的选举也是有原则的,当我收到别人的选举信息时,我也会将他和我自己的选举信息进行PK,PK的原则如下:

经过这一系列的PK,终于选出了我心中的leader服务器,要广播给其他服务器。

超过半数的服务器都同意某一台服务器成为leader,选举结束了。