Why do we use "reconcile" in Cloud?
让我们思考下在云上为用户提供一种中间件服务,我们需要做什么?
- 按照顺序编排申请各类云资源 —— 网络,S3,K8S,计算,存储 ……。
- 在 K8S 中自动化部署中间部署
- 完成各种初始化配置
可以想象出看出在 Cloud 上为用户提供服务会有大量流程耗时且步骤复杂,涉及需要几十步操作,其中包含云资源申请、k8s 初始化、脚本执行、接口调用等,且相互存在依赖关系。
Cloud 主要通过 web 跟用户交互,假设我们直接把流程直接放到一个同步 http 接口中实现,会遇到什么问题呢?
- 在请求未成功前,需要一直保持,过于理想,必然会因为网络、用户操作等客观因素难以维系。
- 服务重启无法自恢复。
- ……
自然而然我们会采用异步方式提升用户体验,比如创建 MySQL 这个场景:
- Create MySQL 接口只触发资源申请便立即返回
- Query MySQL 接口不断轮训集群状态可用。
而后端便需要在后台异步执行流程完成 MySQL 创建。
基于此设计,便引出了基于终态设计的的 "reconcile" 模型,在当前目前 Cloud 上有两类 "reconcile" 流派:
- 基于 DB,通过 Timer(Worker) 扫描执行任务
- 基于 Etcd,使用 Operator 编程模式执行任务
两者实现思路都是通过 reconcile 的方式处理异步任务,在设计思路上差距不大。
What are the problems with using "reconcile"?
-
运维难度高
当 "reconcile" 出错时,一般是通过日志去找到报错的点,但是大部分场景下你必须知道上下文才能够解决该问题,这就要求每一位运维同学必须熟悉 worker 的实现逻辑,这是非常困难的。
而且 Cloud 的业务非常复杂,不仅仅使用一个 worker 就实现单一业务,而是由多个 worker 协作完成,这就加大了分析问题的难度。 -
迭代效率低
以 MySQL 的生命周期管理为例,创建、计算扩缩容、磁盘扩容、创建只读等等一系列的独立场景流程,但是 "reconcile" 实现理念是给出预期,下面通过不断的调协达到预期状态,这么多场景耦合在一起实现背景下,在开发过程中很容易出现:
- 可能一个 feature 只涉及某个场景,但是所有的场景都必须考虑,否则可能影响其他场景,爆炸半径过大,测试成本高。
- "reconcile" 中集成过多的能力,导致代码分支必然膨胀,迭代、维护成本高。
- Other
目前都仅能支持单节点部署,可扩展弱
流程无版本概念,容易产生版本不兼容问题。
……
So what is needed?
我们需要的是一种范式,一种最佳实践,一个解决方案。
- 流程复杂且相互存在依赖关系 —— 任务编排
- 优雅的实现任务执行,减少不合理的并发导致的复杂度,降低心智 —— 任务执行调度
- 高可用,可扩展 —— 分布式
- ……
所以我们需要的是一种分布式任务编排调度引擎 —— workflow。
Workflow selection
站在巨人的肩膀上,github 中可以找到一些符合的方案
作为一个业务团队,引入新的解决方案,我们不单单要考虑技术的成熟度,更需要考虑:
- Go 体系
- 开源,不增加额外业务成本
- 由于没有中台团队支持,在出现问题时有能力兜底能够 owner
- 不引入额外的中间件,或者新增的中间件依赖足以 cover 运维
- ……
所以我们选择了性价比最高的方案 Fastflow,可以将其很低成本融入到遗留项目而无需部署、依赖另一个项目。代码精简上手快,在进行一定程度的魔改之后,完全足够支持目前 Cloud 业务。
What is Fastflow?
用一句话来定义它:一个 基于golang协程、支持水平扩容的分布式高性能工作流框架。
Process Define
Fastflow 定义了 Dag、Task、Action 领域模型来描述 DAG 的任务流模型
首先 Task1 节点所定义的任务会被执行,当 Task1 执行完毕后,Task2, Task3 两个节点所定义的任务将同时被触发,而只有 Task2, Task3 两个节点都执行成功后,最后的 Task4 节点才会被触发。
-
Dag
描述了一个完整流程,它的每个节点被称为 Task,它定义了各个 Task 的执行顺序和依赖关系,你可以通过编程 or yaml 来定义它。 -
Task
它定义了这个节点的具体工作,比如是要发起一个 http 请求,或是执行一段脚本等,这些不同动作都通过选择不同的 Action 来实现,同时它也可以定义在何种条件下需要跳过 or 阻塞该节点。 -
Action
Action 是工作流的核心,定义了该节点将执行什么操作,一般你都需要根据具体的业务场景自行编写,它有几个关键属性:
** Name: Required Action 的名称,不可重复,它是与 Task 关联的核心
** Run: Required 需要执行的动作,fastflow 将确保该动作仅会被执行 一次(ExactlyOnce)
** RunBefore: Optional 在执行 Run 之前运行,如果有一些前置动作,可以在这里执行,RunBefore 有可能会被执行多次。
** RunAfter: Optional 在执行 Run 之后运行,一些长时间执行的任务内容建议放在这里,只要 Task 尚未结束,节点发生故障重启时仍然会继续执行这部分内容,
** RetryBefore:Optional 在重试失败的任务节点,可以提前执行一些清理的动作
通过这些领域模型的抽象,我们便拥有了复杂任务的任务编排能力。
Process Execute
当你发起运行 Dag 指令时,会以定义的 Dag 为模版,则会为本次执行生成一个执行记录,它被称为 DagInstance,当分发到一个的节点后,再由其解析、执行。
在这边 Parser、Executor 基于 Goroutine 实现,维护执行协程池,可以按需调优。
Distributed
fastflow 是一个分布式的框架,意味着你可以部署多个实例来分担负载,而实例被分为两类角色:
- Leader:此类实例在运行过程中只会存在一个,从 Worker 中进行选举而得出,它负责给 Worker 实例分发任务,也会监听长时间得不到执行的任务将其调度到其他节点等
- Worker:此类实例会存在复数个,它们负责解析 DAG 工作流并以 协程 执行其中的任务
- Keeper: 每个节点都会运行 负责注册节点到存储中,保持心跳,同时也会周期性尝试竞选 Leader,防止上任 Leader 故障后阻塞系统,我们也可以实现不同存储的 Keeper 来满足特定的需求,比如 Etcd or Zookeepper,目前官方支持的 Keeper 实现只有 Mongo,但是目前个人已经扩展实现了 MySQL。
- Dispatcher:Leader节点才会运行 负责监听等待执行的 DAG,并根据 Worker 的健康状况均匀地分发任务
- WatchDog:Leader节点才会运行 负责监听执行超时的 Task 将其更新为失败,同时也会重新调度那些一直得不到执行的 DagInstance 到其他 Worker
业务实践
- Fastflow 官方仅支持 Mongo,但是其提供了良好的接口抽象,为了避免引入 Mongo 中间件,开发实现了 MySQL 存储支持
- Fastflow 不支持单独部署,魔改添加 watcher 角色,使得其他服务服务可以仅进行 dag 下发而不承担任务执行事务。
Before and after comparison
Some thoughts
Operator 编程模式必然是有其适用场景的,只是当前我们 Cloud 业务场景下 Workflow 更加契合。
从 K8S 来看,其实 Operator 其实要求其 Reconcile 的 CR 是一个明确的领域模型,即单一职责,当其丰富能力时,往往是新增一个 CR,组合其他 CR,实现其增强能力。
这其实要求我们需要小心的设计 Reconcile,不能过度膨胀,精细的设计依赖关系,从而凸显声明式 API 的优势。
其实换个角度来说 Workflow 也是一种 Reconcile 模式,只是 Reconcile 的领域模型是流程,通过不断的 reconcile 直至成功,所以说对于长流程复杂过程的编排,有要求高 SLA 时,workflow 已有的海量的工程落地经验更加切合目前我们的需求。
Future
- 目前 Fastflow 仅实现了基础分布式任务调度引擎框架,还缺乏很多实用的特性,可以业务跑的更快。
- 支持 Task Retry
- 支持多版本
- 回滚
- 任意处跳转
- 父子任务流
- ……