关于golang:如何利用事件溯源思想实现分布式任务编排的容错

7次阅读

共计 4530 个字符,预计需要花费 12 分钟才能阅读完成。

在做分布式系统集成的时候,当一个性能波及到多个平台的时候,通常面对的问题都是如果失败了怎么办?明天就给大家分享一个新思路 - 基于事件溯源实现分布式协调

咱们的挑战

在进行正式开始之前咱们须要先介绍下咱们的场景是什么,要解决的问题是什么。

场景

在利用治理平台建设中须要整合外部的多个平台,比方容器、虚机、监控、公布、cmdb、负载等多个平台,每个平台都只负责某一部分性能,然而比方咱们要做一个虚机扩容、灰度公布等通常就须要操作多个平台;如果是全部都是基于 k8s 的可能还好一点,然而对于一些公司这种平台建设早于容器平台,这时候就得由利用治理平台来进行协调了

问题

在做一些业务开发时,比方订单领取通常会为了实现这个性能,多个服务会针对业务进行革新,比方应用 tcc、saga 等分布式事务模型来进行业务的一致性保障,其外围参考 ACID 的事务模型。而在利用平台建设中,首先对应的业务方不太会配合你进行革新,其次很多业务场景也不可能实现事务。比方你扩容创立了一台虚机,如果后续流程失败了,你总不能把机器给干掉吧?

思考

既然不能像业务一样通过传统的事务模型进行业务完整性保障,那咱们何不换一种思路呢?于是基于稳定性的思考,笔者将设计思路转换成进步零碎的容错能力,并尽可能的减小爆炸半径,同时尽可能的晋升零碎的可扩展性,保障高可用。

扩大

提到容错能力比拟典型的场景就是数据处理场景了,这里先给大家介绍一下在分布式数据场景中是如何进行容错的。在分布式数据中,通常由 source、process、sink 三局部组成,而在很多场景中又要实现精确的 exactly once,咱们看看再 flink 外面是如何进行设计的,这里先给大家介绍相干概念

checkpoint

checkpoint 通常用于保留某些记录的地位信息用于不便系统故障后疾速复原,在 flink 中也利用了 checkpoint 机制来实现 exactly once 语义,其会依照配置周期性的计算状态生成检查点快照,而后将 checkpoint 长久化存储下来,这样后续如果解体则就能够通过 checkpoint 来进行复原

barrier

checkpoint 只作用于 flink 外部,那如果要实现从 source 到 sink 整个链路的 exactly once,则就会波及到多个组件同时做 checkpoint 的同步,这时候就要让多个组件的 checkpoint 达到一致性,为了实现这个性能 flink 外面引入了 Barrier 用于切分数据流;就相似编程语言中的内存屏障,通过 Barrier 让多个组件同时进行对于 checkpoint 的长久化。每个 Barrier 都会携带一个 checkpoint ID,这样整个数据流的多个组件就会同时进行同一个 checkpoint 的长久化了

checkpointCoordinator

有了 Barrier 机制之后则就须要一个触发和治理组件,利用 barrier 和 checkpoit 让 source、process、sink 三者同时进行 checkpoint 保留,在 flink 中就引入 checkpointCoordinator 来协调多个组件,有了这三个外围的概念,就能够让在 flink 中的多个分布式组件中实现 checkpoint 机制了

两阶段提交

后面的设计都是位于 flink 外部,然而在数据处理中 source、sink 组件则通常是第三方平台,这个时候如果还要保障 exactly once 则除了幂等性就须要用到咱们这里说的两阶段提交了;要实现两阶段提交,则就须要对应的平台提供事务机制,在 preCommit 阶段做数据的生产和写入,同时在 commit 阶段实现事务的提交,因为事务未提交则对应的平台读取不到对应的数据,只有最终都提交胜利后,才能够读取到写入的数据

总结

通过下面的咱们理解了如何基于利用两阶段提交、checkpoint、barrier 联合事务机制实现分布式环境中的 exactly once 实现机制,后续在数据处理的场景中,咱们就能够利用这套机制结合实际业务场景进行落地了

在下一节咱们将开始介绍分布式工作编排中的另外一种实现机制,用于实现分布式系统的容错解决上述场景中遇到的问题

基于 event sourcing 的分布式工作编排

事件溯源

事件溯源保障利用状态的所有扭转都保留在事件流中. 这样咱们不仅能查问这些事件, 咱们也能够通过这个事件的日志来从新构建以前的状态, 以些为根底实现主动扭转状态来应答追溯过的变动.

其外围关键点:事件、程序、长久化,通过对长久化存储中的事件依照程序进行回放,咱们就能够失去以后的状态,同理在工作编排的场景下,也能够借鉴相似的思维。

工作编排容错

工作编排的外围是通过编排对应的工作序列实现某个业务性能,在分布式环境中,通常会波及到 workflow 工作的编排、task 任务分配、运行时数据的存储等。在大多数的工作编排框架中,关注点都是任务调度。而咱们明天接下来要介绍的 temporal 其关键点则是容错,即当对应的 workflow、task 如果执行失败,零碎该如何进行复原。也是事件溯源利用的次要场景。

工作执行容错语义

在后面的介绍 exactly once 场景中咱们介绍过两阶段对事物机制的依赖,同理在工作编排中的状态,咱们这里容错机制实现的语义是 at-lease-once,即工作至多被执行一次,并尽可能保障业务不会反复被执行

溯源工作状态


联合事件溯源介绍下 temporal 里是如何基于事件溯源来实现容错语义的。在 temporal 一个 workflow 的以后状态,是由对应的 workflow 的事件 reply 来决定的,即通过回放 workflow 的所有事件来决定接下来该执行那个工作,在 temporal 外面的工作事件数据都由 history 服务对立存储,即事件数据的存储都是 transaction 的,这样就能够保障即便产生网络分区的状况,一个工作的执行后果也会只有一份, 那当咱们要复原工作状态的时候,就只须要通过事件回放,就晓得接下来要执行那个工作,以及以后的状态数据

不变性

后面提到通过事件序列来进行事件回放能够失去以后状态,其实在工作编排场景中还有第二个序列 - 执行序列,即咱们要执行的工作列表肯定要是程序的。只有这样能力顺着正确的路线持续复原。

例如在 go 外面对 slice 的 for range 遍历是固定的,这里蕴含两局部:复原 slice 和遍历 slice, 即我再不同的机器上通过历史数据我能够构建出 slice,而后遍历这个 slice 这两个操作的后果都是一样的。

然而对 map 则不肯定,咱们并不能保障在不同机器上复原和遍历这两个操作的后果都是一样的。所以 workflow 外面的逻辑和状态数据肯定要是不变的

为什么是 temporal

除了下面提到的容错,其实抉择 temporal 更多的是就是易于学习和了解,大家能够看下咱们创立虚机的 workflow。

  • 如果出现异常则 temporal 会依据咱们的重试策略主动进行重试,代码外面只有失常的业务逻辑
  • 如果咱们须要期待工作的执行后果,就像写本地代码一样通过 future.Get 去获取后果
  • 如果执行能力有余,则就只须要加 worker 节点即可进步零碎的分布式能力
  • 如果对于同一个资源申请单想要保障只有一个 workflow,只须要在创立 workflow 的时候传入配置即可
// 创立虚机工作流
func CreateVMWorkflow(ctx workflow.Context, clientToken string, vmRequest cloud.CreateVMRequest, vmGroup ServerGroupt) (*CreateVMWorkflowResponse, error) {
    var (
        tvmTask     TVM
        response    CreateVMWorkflowResponse
        workflowCtx = workflow.WithActivityOptions(ctx, defaultTaskOptions)
    )

    // 创立虚机
    var createResponse *cloud.CreateVMResponse
    if err := workflow.ExecuteActivity(workflowCtx, tvmTask.CreateVMActivity, clientToken, vmRequest).Get(workflowCtx, &createResponse); err != nil {return nil, err}

    if !createResponse.Success() {return nil, errorx.StringError("create vm response error: %v", createResponse)
    }

    // 虚机初始化流程
    var futures []workflow.ChildWorkflowFuture
    for _, host := range createResponse.Data.Instance {future := workflow.ExecuteChildWorkflow(workflowCtx, WaitAndBindWorkflow, host, vmRequest.IDC, vmGroup)
        futures = append(futures, future)
    }

    // 期待虚机后果
    for _, future := range futures {
        var resp *AddServerLoadResponse
        if err := future.Get(workflowCtx, &resp); err != nil {response.Messages = append(response.Messages, err.Error())
            continue
        }
        if resp.Success {response.Success = append(response.Success, resp.IP)
            continue
        }
        response.Failure = append(response.Failure, resp.IP)
        response.Messages = append(response.Messages, resp.Message)
    }

    return response, nil
}

总结

temporal 当然也有有余的中央,例如

  • 不反对 dsl
  • 兼容性:同时针对 Mysql 分支版本比方 BaikalDB 就不太反对 (致命 bug),
  • 基于一致性 hash 的分片机制可能会存在工作散布不均
  • 材料太少,生产配置没有能够参考的优化 (官网社区比拟 nice,反馈比拟及时)

不过想想基于 temporal 能够疾速实现一个分布式、可扩大、高容错、无状态的工作编排零碎,其余都是小事件哈哈。前面有工夫在给大家从源码上梳理下 temporal 的是如何实现上述性能的。包含工作分片、ringpop、信号、状态保留等

参考地址

什么是事件溯源:https://www.oschina.net/translate/event-sourcing?print

云原生学习笔记地址: https://www.yuque.com/baxiaoshi/tyado3)
微信号:baxiaoshi2020 公共号: 图解源码

微信号:baxiaoshi2020
关注布告号浏览更多源码剖析文章
本文由博客一文多发平台 OpenWrite 公布!

正文完
 0