# 工作流引擎

> 在 IaaS 软件中的任务通常有很长的执行路径，一个错误可能发生在任意一个步骤。为了保持系统的完整性，IaaS 软件必须提供一套机制用于回滚先前执行的步骤。通过工作流引擎，ZStack 的每一个步骤（包裹在独立的工作流中）都可以在出错时回滚。本文介绍了 ZStack 的工作流引擎。

- 发布时间: 2015-04-04
- 更新时间: 2015-04-04
- 作者: [张鑫](https://github.com/zxwing)
- 分类: 技术分享
- 标签: 核心架构, 工作流, 错误处理, 回滚, 工作流引擎, 流程链, 状态一致性, Flow 接口, FlowChain, ZStack, 顺序工作流, 声明式流程, 编程式流程

> *本文原发布于 [ZStack Blog](http://zstack.org/blog/workflow.html)。
> ZSvirt 构建于 ZStack 之上，继承了以下描述的全部架构特性。
> 理解这些基础设计，有助于更深入地认识 ZSvirt 的性能优势与技术原理。*

*在 IaaS 软件中的任务通常有很长的执行路径，一个错误可能发生在任意一个给定的步骤。为了保持系统的完整性，一个 IaaS 软件必须提供一套机制用于回滚先前执行的步骤。通过一个工作流引擎，ZStack 的每一个步骤（包裹在独立的工作流中）都可以在出错的时候回滚。此外，通过在配置文件中组装工作流的方式，关键的执行路径可以被配置，这使得架构的耦合度进一步降低。*

## 动机

数据中心是由大量的、各种各样的资源组成的，包括物理资源（比如：存储、服务器）和虚拟资源（比如：虚拟机）。IaaS 软件本质上就是在管理各种资源的状态。例如，创建一个虚拟机通常会改变存储的状态（在存储上创建了一个新的磁盘卷）、网络的状态（在网络上设置 DHCP/DNS/SNAT 等相关信息）和虚拟机管理程序的状态（在虚拟机管理程序上创建一个新的虚拟机）。不同于普通的应用程序，它们绝大多数时候都在管理存储在内存或数据库中的状态，为了反映出数据中心的整体状态，IaaS 软件必须管理分散在各个设备上的状态，这就导致了很长的执行路径。一个 IaaS 软件任务通常会涉及在多个设备上的状态改变，错误可能在任何步骤发生，然后让系统处在一个中间状态——即一些设备已经改变了状态，而一些没有。例如，创建一个虚拟机时，IaaS 软件配置 VM 网络的常规步骤为 DHCP → DNS → SNAT，如果在创建 SNAT 时发生错误，之前配置好的 DHCP 和 DNS 很有可能还留在系统内，因为它们已经被成功地应用了，即使虚拟机最后无法成功创建。这种状态不一致的问题通常会使云不稳定。

另一方面，硬编码的业务逻辑在传统的 IaaS 软件内对于变更来说是不灵活的；开发人员往往要重写或修改现有的代码来改变一些既定的行为，这影响了软件的稳定性。

这些问题的解决方法是引入工作流的概念：将整块的业务逻辑分解成细粒度的、可回滚的步骤，使软件能够在出错时清理已经生成的状态，并且变得可以配置。

> **注意**：在 ZStack 中，我们可以将工作流中的步骤（step）称为"流程（flow）"，在以下内容中，流程（flow）和步骤（step）是可以互换的。

## 问题

错误处理在软件设计中总是一个很头疼的问题。即使现在每一个软件工程师都知道了错误处理的重要性，但事实上，他们仍然在找借口忽略它。精巧的错误处理是很难的，尤其是在一个任务可能跨越多个组件的系统中。即使富有经验的工程师可以关注自己代码中的错误，如果整个架构中没有强制一种统一的、可以在全局加强错误处理的机制，他们也不可能为不是自己所写的组件付出同样的努力。忽略错误处理在一个 IaaS 软件中是特别有害的。不像消费级程序可以通过重启来恢复所有的状态，一个 IaaS 软件通常没有办法自己恢复状态，它将需要管理员去手动更正在数据库和外部设备中的错误。一个单一的状态不一致可能不会导致任何大问题，甚至可能不会被注意到，但是这种状态不一致性的不断积累，将会在某个时刻最终摧毁整个云系统。

## 工作流引擎

工作流是一种方法：把一些繁琐的方法调用分解为一个个专注于一件事情的细粒度步骤，它由序列或状态机驱动，最终完成一个完整的任务。配置好回滚处理程序后，当错误或未处理的异常在某一步骤发生时，一个工作流可以中止执行并回滚所有之前已经执行的步骤。以创建虚拟机为例，主要工作流看起来像：

[图片可在规范 HTML 页面中查看: 创建虚拟机的工作流](https://zsvirt.io/zh/blog/workflow-engine/)

顺序工作流来源于链式设计模式（[Chain Pattern](http://en.wikipedia.org/wiki/Chain-of-responsibility_pattern)），有着可以预见的执行顺序，这是 ZStack 工作流的基础。一个流程（flow）本质上是一个 Java 接口，可以包含子流程，并且只在前面所有流程完成后才可以执行。

```java
public interface Flow {
    void run(FlowTrigger trigger, Map data);

    void rollback(FlowTrigger trigger, Map data);
}
```

在 `Flow` 接口中，当工作流前进到这个流程（flow）的时候，`run(FlowTrigger trigger, Map data)` 方法会被调用；参数 `Map data` 可以被用于从先前的流程中获取数据，并把数据传递给后续的流程。当自身完成时，这个流程调用 `trigger.next()` 来引导工作流去执行下一个流程；如果一个错误发生了，这个流程应该调用 `trigger.fail(ErrorCode error)` 方法中止执行，并通知工作流回滚已经完成的流程（包括失败的流程自身），即调用它们各自的 `rollback()` 方法。

流程在 `FlowChain` 接口中被组织起来，代表了一个完整的工作流。有两种方法来创建一个 `FlowChain`：

### 1. 声明式

流程可以在一个组件的 Spring 配置文件中被配置，一个 `FlowChain` 可以通过向 `FlowChainBuilder` 填写一个流程类名列表来创建。

```xml
<bean id="VmInstanceManager" class="org.zstack.compute.vm.VmInstanceManagerImpl">
    <property name="createVmWorkFlowElements">
        <list>
            <value>org.zstack.compute.vm.VmAllocateHostFlow</value>
            <value>org.zstack.compute.vm.VmImageSelectBackupStorageFlow</value>
            <value>org.zstack.compute.vm.VmAllocatePrimaryStorageFlow</value>
            <value>org.zstack.compute.vm.VmAllocateVolumeFlow</value>
            <value>org.zstack.compute.vm.VmAllocateNicFlow</value>
            <value>org.zstack.compute.vm.VmInstantiateResourcePreFlow</value>
            <value>org.zstack.compute.vm.VmCreateOnHypervisorFlow</value>
            <value>org.zstack.compute.vm.VmInstantiateResourcePostFlow</value>
        </list>
    </property>
    
    <!--only a part of configuration is showed -->
</bean>
```

```java
FlowChainBuilder createVmFlowBuilder = FlowChainBuilder.newBuilder().setFlowClassNames(createVmWorkFlowElements).construct();
FlowChain chain = createVmFlowBuilder.build();
```

这是创建一个严肃的、可配置的、包含可复用流程的工作流的典型方式。在上面的例子中，那个工作流的目的是创建用户 VM；一个所谓的应用 VM（appliance VM）具有除分配虚拟机网卡之外基本相同的流程，所以 appliance VM 的独立流程配置和用户 VM 的流程配置大多数是可以共享的：

```xml
<bean id="ApplianceVmFacade"
    class="org.zstack.appliancevm.ApplianceVmFacadeImpl">
    <property name="createApplianceVmWorkFlow">
        <list>
            <value>org.zstack.compute.vm.VmAllocateHostFlow</value>
            <value>org.zstack.compute.vm.VmImageSelectBackupStorageFlow</value>
            <value>org.zstack.compute.vm.VmAllocatePrimaryStorageFlow</value>
            <value>org.zstack.compute.vm.VmAllocateVolumeFlow</value>
            <value>org.zstack.appliancevm.ApplianceVmAllocateNicFlow</value>
            <value>org.zstack.compute.vm.VmInstantiateResourcePreFlow</value>
            <value>org.zstack.compute.vm.VmCreateOnHypervisorFlow</value>
            <value>org.zstack.compute.vm.VmInstantiateResourcePostFlow</value>
        </list>
    </property>

    <zstack:plugin>
        <zstack:extension interface="org.zstack.header.Component" />
        <zstack:extension interface="org.zstack.header.Service" />
    </zstack:plugin>
</bean>
```

> **备注**：在之前的图片中，我们把 `ApplianceVmAllocateNicFlow` 流程高亮为绿色，这是创建用户 VM 和应用 VM 的工作流步骤中唯一不同的地方。

### 2. 编程式

一个 `FlowChain` 还可以通过编程的方式创建。通常在要创建的工作流很琐碎、流程不可复用的时候，使用这种方法。

```java
FlowChain chain = FlowChainBuilder.newSimpleFlowChain();
chain.setName("test");
chain.setData(new HashMap());
chain.then(new Flow() {
    String __name__ = "flow1";
    @Override
    public void run(FlowTrigger trigger, Map data) {
        /* do some business */
        trigger.next();
    }

    @Override
    public void rollback(FlowTrigger trigger, Map data) {
        /* rollback something */
        trigger.rollback();
    }
}).then(new Flow() {
    String __name__ = "flow2";
    @Override
    public void run(FlowTrigger trigger, Map data) {
        /* do some business */
        trigger.next();
    }

    @Override
    public void rollback(FlowTrigger trigger, Map data) {
        /* rollback something */
        trigger.rollback();
    }
}).done(new FlowDoneHandler() {
    @Override
    public void handle(Map data) {
        /* the workflow has successfully done */
    }
}).error(new FlowErrorHandler() {
    @Override
    public void handle(ErrorCode errCode, Map data) {
        /* the workflow has failed with error */
    }
}).start();
```

以上形式使用起来不方便，因为在流程中通过一个 map `data` 交换数据，每一个流程必须冗余地调用 `data.get()` 和 `data.put()` 函数。使用一种类似 [DSL](http://en.wikipedia.org/wiki/Domain-specific_language) 的方式，流程可以通过变量共享数据：

```java
FlowChain chain = FlowChainBuilder.newShareFlowChain();
chain.setName("test");
chain.then(new ShareFlow() {
    String data1 = "data can be defined as class variables";

    {
        data1 = "data can be iintialized in object initializer";
    }

    @Override
    public void setup() {
        final String data2 = "data can also be defined in method scope, but it has to be final";

        flow(new Flow() {
            String __name__ = "flow1";

            @Override
            public void run(FlowTrigger trigger, Map data) {
                data1 = "we can change data here";
                String useData2 = data2;

                /* do something */
                trigger.next();
            }

            @Override
            public void rollback(FlowTrigger trigger, Map data) {
                /* do some rollback */
                trigger.rollback();
            }
        });

        flow(new NoRollbackFlow() {
            String __name__ = "flow2";
            
            @Override
            public void run(FlowTrigger trigger, Map data) {
                /* data1 is the value of what we have changed in flow1 */
                String useData1 = data1;

                /* do something */
                trigger.next();
            }
        });

        done(new FlowDoneHandler() {
            @Override
            public void handle(Map data) {
                /* the workflow has successfully done */
            }
        });

        error(new FlowErrorHandler() {
            @Override
            public void handle(ErrorCode errCode, Map data) {
                /*the workflow has failed with error */
            }
        });
    }
}).start();
```

## 总结

在这篇文章中，我们展示了 ZStack 的工作流引擎。通过使用它，在错误发生的时候，ZStack 在 99% 的时间里可以很好地保持系统状态一致。注意是 99% 的时间里——虽然工作流在大多数时候是一个处理错误的好工具，但仍然有一些情况它无法处理，例如回滚处理程序自身运行失败的时候。ZStack 还配备了垃圾收集系统，我们将在未来的文章中对其进行介绍。

---

## ZStack 核心架构系列

- [第一部分：异步架构](https://zsvirt.io/zh/blog/asynchronous-architecture/)
- [第二部分：无状态服务](https://zsvirt.io/zh/blog/stateless-clustering/)
- [第三部分：无锁架构](https://zsvirt.io/zh/blog/lock-free-architecture/)
- [工作流引擎](https://zsvirt.io/zh/blog/workflow-engine/)
- [进程内微服务架构](https://zsvirt.io/zh/blog/in-process-microservices/)
- [通用插件系统](https://zsvirt.io/zh/blog/plugin-system/)
- [标签系统](https://zsvirt.io/zh/blog/tag-system/)

## 官方链接


- [规范 HTML 页面](https://zsvirt.io/zh/blog/workflow-engine/)
- [在 GitHub 讨论](https://github.com/ZSvirt/zsvirt/discussions)
