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

咱们的挑战

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

场景

在利用治理平台建设中须要整合外部的多个平台,比方容器、虚机、监控、公布、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 公布!