# 分布式任务调度

## 知识储备

### 任务执行原理

![image-20200723002006948](/files/-MCrOqLdGkMXy1gGDAvA)

#### 涉及的系统调用

pipe()：创建2个文件描述符，fd\[0]可读，fd\[1]可写

fork()：创建子进程，fd\[1]被继承到子进程

dup2()：重定向子进程stdout/stderr到fd\[1]

exec()：在当前进程内，加载并执行二进制程序

#### 通过Command类执行任务

在golang中通过`os/exec`可以提交执行任务

### Cron表达式

#### Cron表达式语法

#### 开源Cronexpr库

`go get github.com/gorhill/cronexpr`

#### 调度多个Cron

![image-20200723235352790](/files/-MCwdxVwT5WxDyfSWoS_)

### etcd

#### 核心特性

1. 将数据存储在集群中的高可用K-V存储
2. 允许应用实时监听存储中的K-V的变化
3. 能够容忍单点故障，能够应对网络分区

#### 传统存储模型

1. 单点存储：节点宕机，服务将不可用
2. 主从模式：主库宕机，从库暂时还能提供读功能，但是由于同步延迟会有一部分主库信息未能及时同步到从库，导致信息丢失（因为主库宕机了，无法继续同步）。同时从库身份切换成主库需要一定时间，会有段时间写功能异常。

#### 抽屉原理

一个班级有60人，如果将一个秘密告诉其中的31人，那么随便挑选31个人，一定有一个人知道秘密

#### etcd与Raft

raft是强一致的集群日志同步算法，一个分布式协议

etcd是一个分布式KV存储

etcd利用raft算法在集群中同步key-value

#### quorum模型（大多数模型）

集群中有2N+1个节点（N>=1）

![image-20200724001517862](/files/-MCwdxVyYqmC-x0Zksrs)

#### raft日志概念

replication：日志在leader生产，向follower复制，达到各个节点的日志序列最终一致

term：任期，重新选举产生的leader，其term单调递增

log index：日志行在日志序列的下标

#### raft异常场景

![image-20200724002403552](/files/-MCwdxVzCwV2GHrZaTSp)

1. a\~g都可能是第一任leader
2. f是第二任leader
3. f是第三任leader
4. e是第四任leader&#x20;
5. a、c、d、g都可能是第四任leader
6. c是第六任leader
7. d是第七任leader

#### raft异常安全

1. 选举leader需要半数以上节点参与
2. 节点commit日志最多的允许选举为leader
3. commit日志同样多，则term、index越大的允许被选举为leader

#### raft工作示例

#### raft保证

提交成功的请求，一定不会丢

各个节点的数据将最终一致

#### 交互协议

SDK内置gRPC协议，性能高效（一般的服务是基于HTTP+JSON协议实现的，性能低效）

#### 重要特性

1. 底层存储是按key有序排列的，可以顺序遍历
2. 因为key有序，所以etcd天然支持按目录结构高效遍历
3. 支持复杂事务，提供类似if...then...else...的事务能力
4. 基于租约机制实现key的TTL过期

#### MVCC多版本控制

提交版本（revision）在etcd中单调递增。同key维护多个历史版本，用于实现watch机制（可以通过key+version在etcd中访问到旧值）

历史版本过多，可以通过执行compact命令完成删减

#### 监听KV变化

通过watch机制，可以监听某个key，或者某个目录（key前缀）的连续变化

常用于分布式系统的配置分发、状态同步。

#### watch工作原理

![image-20200724004727202](/files/-MCwdxW-ZJ4ADFtP4G3z)

#### lease租约

![image-20200724004900879](/files/-MCwdxW06nTIs1IO83qR)

## 架构

### 传统crontab痛点

1. 机器故障，任务停止调度，甚至crontab配置都找不回来
2. 任务数量多，单机的硬件资源耗尽，需要人工迁移到其他机器
3. 需要人工去机器上配置corn，任务执行状态不方便查看

### 分布式架构 核心要素

1. 调度器：需要高可用，确保不会因为单点故障停止调度
2. 执行器：需要扩展性，提供大量任务的并行处理能力

### 场景开源调度架构

![image-20200801065308951](/files/-MDj4tDQgqnFvwu2UeR9)

### 伪分布式设计

1. 分布式网络环境不可靠，RPC异常属于常态
2. Master下发任务RPC异常，导致Master与Worker状态不一致
3. Worker上报任务RPC异常，导致Master状态信息落后

### 异常CASE举例

1. 状态不一致：Master下发任务给node1异常，实际上node1收到并开始执行
2. 并发执行：Master重试下发任务给node2，结果node1与node2同时执行一个任务
3. 状态丢失：Master更新zookeeper中任务状态异常，此时Master宕机切换Standby，任务仍旧处于旧状态

### 分布式伪命题

1. 但凡需要经过网络的操作，都可能出现异常
2. 将应用状态放在存储中，必然会出现内存与存储状态不一致
3. 应用直接利用raft管理状态，可以确保最终一致性，但是成本太高

### CAP理论（常用于分布式存储）

如：zookeeper、etcd等都应用了该理论

![image-20200801070850218](/files/-MDj4tDRm110Mtkzu5Ho)

对于应用来说必须要具备`P 分区容错性`，然后在C和A之间取舍，也就是说只能实现CP或AP

MySQL实现的就是CP（通过主从复制提供可用性，但是主节点挂掉，只能提供读，写服务异常），对于写入的数据可以立即被读取到。

### BASE理论（常用于应用架构）

![image-20200801071540531](/files/-MDj4tDSsbdJOy5Ugcpj)

### 架构师能做什么？

1. 架构简化，减少有状态服务，秉承最终一致性
2. 架构折衷，确保异常可以被程序自我治愈

## 整体架构（概要设计）

![image-20200801072141264](/files/-MDj4tDT4WzLRpWl75Fi)

### 架构思路

1. 利用etcd同步全量任务列表到所有worker节点
2. 每个worker独立调度全量任务，无需与Master通信（单worker调度全量任务会不会很慢？不会，因为在程序中遍历任务很快）
3. 各个worker利用分布式锁抢占，解决并发调度导致相同任务同时执行的问题（抢到锁的就执行，没有就跳过）

## Master功能点与实现（详细设计）

### Master的功能点

1. 任务管理HTTP接口：新建、修改、查看、删除
2. 任务日志HTTP接口：查看任务执行历史日志
3. 任务控制HTTP接口：提供强制结束任务的接口
4. 实现web管理界面：基于jquery+bootstrap的Web控制台，前后端分离

### 管理后台

![image-20200801073805277](/files/-MDj4tDU0pcjB2pk69Fx)

### Master内部架构

![image-20200801073959270](/files/-MDj4tDVOE6zrytMcEEd)

### 任务管理

#### etcd结构

![image-20200801074220669](/files/-MDj4tDWbi3n_RVn_jHa)

保存到etcd的任务会被实时同步到所以worker

### 任务日志

#### MongoDB结构

![image-20200801074507234](/files/-MDj4tDX01xDzYwAxNdC)

请求MongoDB，按任务名查看最近的执行日志

### 任务控制

#### etcd结构

/cron/killer/任务名 -> "" （key部分是将要杀死的任务名写入到/corn/killer中，value部分可以填写任意值）

worker监听`/corn/killer`目录下put修改操作

master将要结束的任务名put到/cron/killer目录下，触发worker立即结束shell任务

## Worker功能点与实现

### Worker功能点

1. 任务同步：监听etcd中/cron/jobs目录变化
2. 任务调度：基于corn表达式计算，触发过期任务
3. 任务执行：协程池并发执行多任务，基于etcd分布式锁抢占
4. 日志保存：捕获任务执行输出，保存到MongoDB

### Worker内部架构图

![image-20200801075917823](/files/-MDj4tDYiZlT5C64TYlU)

### 监听协程

1. 利用watch API，监听/corn/jobs和/cron/killer目录的变化
2. 将变化事件通过channel推送给调度协程，更新内存中的任务信息

### 调度协程

1. 监听任务变更event，更新内存中维护的任务列表
2. 检查任务cron表达式，扫描到期任务，交给执行协程运行
3. 监听任务控制event，强制中断正在执行中的子进程
4. 监听任务执行result，更新内存中任务状态，投递执行日志

### 执行协程

1. 在etcd中抢占分布式乐观锁：/cron/lock/任务名
2. 抢占成功则通过Command类执行Shell任务
3. 捕获Command输出并等待子进程结束，将任务执行结果投递给调度协程

### 日志协程

1. 监听调度发来的执行日志，放入一个batch中
2. 对新batch启动定时器，超时未满自动提交
3. 若batch被放满，那么立即提交，并取消自动提交定时器


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://1005281342.gitbook.io/code-porter/go-shi-zhan/fen-bu-shi-ren-wu-tiao-du.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
