1 Mistral背景

Mistral是一个OpenStack生态圈中比较新的项目,该项目的目标是:

The project is to provide capability to define, execute and manage tasks and workflows without writing code.

截至到目前开发还不到2年,最初是由Mirantis公司贡献给Openstack社区的工作流组件,提供Workflow As a Service服务,类似AWS的SWS(Simple Workflow Serivce),Hadoop生态圈中的oozie服务。它虽然没有Nova、Cinder等核心组件那么流行,部署率也不是很高,社区Pike版本的统计还没有出来,Ocata统计中Mistral的成熟度为1/7,部署率为5%,参考OpenStack Mistral,但还是得到很多开发者和用户关注,项目活跃度还是比较高的。

注意它和OpenStack资源编排服务Heat不同,二者功能可能会有重叠,但Heat注重基础资源的编排,而Mistral则主要是用于任务编排。Heat的主要应用场景是创建租户基础资源模板,管理员可以创建一个资源模板,基于这个模板用户一次请求就可以完成虚拟机创建及配置、挂载数据卷、创建网络和路由、设置安全组等。而Mistral的典型应用场景包括执行计划任务Cloud Cron,调度任务Task Scheduling,执行复杂的运行时间长的业务流程等。我们目前使用的场景是基于Cloud Cron创建定时任务,比如定时创建虚拟机快照、定时创建数据库备份等。

2 Mistral的几个概念

要研究Mistral,首先需要了解Mistral包含哪些实体,了解这些实体的关系以及转化过程。其中我总结了几个核心实体关系图如下:

  • action:action是最小执行单元,可以理解为一条命令或者一个OpenSack API请求。

  • workflow:Mistral的核心,Mistral主要围绕着workflow工作的,其由DSL语言定义,由各种action以及执行逻辑组成。

  • cron-trigger: 定时任务,通过crontab设定workflow执行周期。

  • execution:workflow进入运行状态即为execution,它是runtime态的,因此有执行状态,如running、error、success等。

  • task:一个execution由一个或者多个task构成,task也有运行时状态,如running、error、sucess等。

  • action-execution:task由多个action-execution构成,action进入运行时状态即为action-execution。

如果说workflow等同于程序,则execution相当于一个进程,task则类似于线程,action为一个函数或者一个计算机指令。

另外一个比较特别的实体member,这个主要用于分享资源给其它租户,和Glance的member功能是一样的。

接下来我们对以上涉及的几个实体概念进行详细介绍。

2.1 action

action是Mistral中最小执行单元(执行指令),对应一个命令或者一次API请求。内置OpenStack相关的actions实际上封装了所有OpenStack组件的pythonclient接口,比如nova.servers_start对应python-novaclient项目的novaclient/v2/servers.py模块的start()方法。目前nova包含227个action,cinder包含128个action,glance包含20个action,几乎涵盖了所有虚拟机管理、volume管理等。以cinder backup为例,包含的actions列表如下:

<span style="color:#00000a"><code><span style="color:#1a1a1a">int32bit $ mistral action-list | awk </span></code><code><span style="color:#f1403c">'/\scinder.backup/{print $4,$8}'</span></code><code><span style="color:#1a1a1a"> | tr -d </span></code><code><span style="color:#f1403c">','</span></code><code><span style="color:#1a1a1a"> | sed </span></code><code><span style="color:#f1403c">'s/ / -> /'</span></code>
<code><span style="color:#1a1a1a">cinder.backups_create -> volume_id</span></code>
<code><span style="color:#1a1a1a">cinder.backups_delete -> backup</span></code>
<code><span style="color:#1a1a1a">cinder.backups_export_record -> backup_id</span></code>
<code><span style="color:#1a1a1a">cinder.backups_find -> </span></code><code><span style="color:#0084ff">action_region</span></code><code><span style="color:#1a1a1a"><strong>=</strong></span></code><code><span style="color:#f1403c">""</span></code>
<code><span style="color:#1a1a1a">cinder.backups_findall -> </span></code><code><span style="color:#0084ff">action_region</span></code><code><span style="color:#1a1a1a"><strong>=</strong></span></code><code><span style="color:#f1403c">""</span></code>
<code><span style="color:#1a1a1a">cinder.backups_get -> backup_id</span></code>
<code><span style="color:#1a1a1a">cinder.backups_import_record -> backup_service</span></code>
<code><span style="color:#1a1a1a">cinder.backups_list -> </span></code><code><span style="color:#0084ff">detailed</span></code><code><span style="color:#1a1a1a"><strong>=</strong></span></code><code><span style="color:#0084ff">true</span></code>
<code><span style="color:#1a1a1a">cinder.backups_reset_state -> backup</span></code></span>

其中前面的字段是action名字,后面的是参数。用户可以通过action-get子命令查看action更详细的信息:

<span style="color:#00000a"><code><span style="color:#1a1a1a">mistral action-get cinder.backups_create</span></code>
<code><span style="color:#1a1a1a">+-------------+--------------------------------------------------------------------------------------------------------------------------+</span></code>
<code><span style="color:#1a1a1a">| Field       | Value                                                                                                                    |</span></code>
<code><span style="color:#1a1a1a">+-------------+--------------------------------------------------------------------------------------------------------------------------+</span></code>
<code><span style="color:#1a1a1a">| Name        | cinder.backups_create                                                                                                    |</span></code>
<code><span style="color:#1a1a1a">| Is system   | True                                                                                                                     |</span></code>
<code><span style="color:#1a1a1a">| Input       | volume_id, container=null, name=null, description=null, incremental=false, force=false, snapshot_id=null, backup_id=null |</span></code>
<code><span style="color:#1a1a1a">| Description | Creates a volume backup.                                                                                                 |</span></code>
<code><span style="color:#1a1a1a">|             |                                                                                                                          |</span></code>
<code><span style="color:#1a1a1a">|             | :param volume_id: The ID of the volume to backup.                                                                        |</span></code>
<code><span style="color:#1a1a1a">|             | :param container: The name of the backup service container.                                                              |</span></code>
<code><span style="color:#1a1a1a">|             | :param name: The name of the backup.                                                                                     |</span></code>
<code><span style="color:#1a1a1a">|             | :param description: The description of the backup.                                                                       |</span></code>
<code><span style="color:#1a1a1a">|             | :param incremental: Incremental backup.                                                                                  |</span></code>
<code><span style="color:#1a1a1a">|             | :param force: If True, allows an in-use volume to be backed up.                                                          |</span></code>
<code><span style="color:#1a1a1a">|             | :rtype: :class:`VolumeBackup`                                                                                            |</span></code>
<code><span style="color:#1a1a1a">| Tags        | <none>                                                                                                                   |</span></code>
<code><span style="color:#1a1a1a">| Created at  | 2017-04-11 08:19:52                                                                                                      |</span></code>
<code><span style="color:#1a1a1a">| Updated at  | None                                                                                                                     |</span></code>
<code><span style="color:#1a1a1a">+-------------+--------------------------------------------------------------------------------------------------------------------------+</span></code></span>

除了OpenStack相关的action以外,Mistral还包含如下内置actions:

<span style="color:#00000a"><code><span style="color:#1a1a1a">std.async_noop</span></code>
<code><span style="color:#1a1a1a">std.echo</span></code>
<code><span style="color:#1a1a1a">std.email</span></code>
<code><span style="color:#1a1a1a">std.fail</span></code>
<code><span style="color:#1a1a1a">std.http</span></code>
<code><span style="color:#1a1a1a">std.javascript</span></code>
<code><span style="color:#1a1a1a">std.mistral_http</span></code>
<code><span style="color:#1a1a1a">std.noop</span></code>
<code><span style="color:#1a1a1a">std.ssh</span></code>
<code><span style="color:#1a1a1a">std.ssh_proxied</span></code>
<code><span style="color:#1a1a1a">std.wait_ssh</span></code></span>

需要注意的是,Mistral目前尚不支持动态增删action,如果需要添加自定义action必须手写代码,修改setup.cfg配置文件并重新安装部署Mistral服务,参考官方文档Creating custom action,本人写了一个脚本实现了自动发现和注册自定义action的功能,参考mistral-actions。不过Mistral支持创建Ad-hoc actions,即封装已有的action为新的action,类似于编程语言的继承关系或者模板。比如std.email需要传递很多参数,如果某些参数固定并且可以重复使用的话,我们可以创建一个action继承自std.email,创建一个新文件error_email.yaml内容如下:

<span style="color:#00000a"><code><span style="color:#1a1a1a">---</span></code>
<code><span style="color:#1a1a1a">version: '2.0'</span></code>
<code><span style="color:#1a1a1a">error_email:</span></code>
<code><span style="color:#1a1a1a">  </span></code><code><span style="color:#1a1a1a">input:</span></code>
<code><span style="color:#1a1a1a">    </span></code><code><span style="color:#1a1a1a">- execution_id</span></code>
<code><span style="color:#1a1a1a">  </span></code><code><span style="color:#1a1a1a">base: std.email</span></code>
<code><span style="color:#1a1a1a">  </span></code><code><span style="color:#1a1a1a">base-input:</span></code>
<code><span style="color:#1a1a1a">    </span></code><code><span style="color:#1a1a1a">to_addrs: ['admin@mywebsite.org']</span></code>
<code><span style="color:#1a1a1a">    </span></code><code><span style="color:#1a1a1a">subject: 'Something went wrong with your Mistral workflow :('</span></code>
<code><span style="color:#1a1a1a">    </span></code><code><span style="color:#1a1a1a">body: |</span></code>
<code><span style="color:#1a1a1a">        </span></code><code><span style="color:#1a1a1a">Please take a look at Mistral Dashboard to find out what's wrong</span></code>
<code><span style="color:#1a1a1a">        </span></code><code><span style="color:#1a1a1a">with your workflow execution <% $.execution_id %>.</span></code>
<code><span style="color:#1a1a1a">        </span></code><code><span style="color:#1a1a1a">Everything's going to be alright!</span></code>
<code><span style="color:#1a1a1a">        </span></code><code><span style="color:#1a1a1a">-- Sincerely, Mistral Team.</span></code>
<code><span style="color:#1a1a1a">    </span></code><code><span style="color:#1a1a1a">from_addr: 'mistral@openstack.org'</span></code>
<code><span style="color:#1a1a1a">    </span></code><code><span style="color:#1a1a1a">smtp_server: 'smtp.google.com'</span></code>
<code><span style="color:#1a1a1a">    </span></code><code><span style="color:#1a1a1a">smtp_password: 'SECRET'</span></code></span>

注意:本文中action以及workflow定义均使用yaml格式,Mistral同样支持json格式,二者可以相互转化。

以上为to_addrs、subject、body等设置了默认参数值,我们基于该yaml文件创建新的action:

<span style="color:#00000a"><code><span style="color:#1a1a1a">mistral action-create error_email.yaml</span></code></span>

以后就可以复用这个action,只需要传递execution_id,而不需要重复to_addrs、subject等参数了:

<span style="color:#00000a"><code><span style="color:#1a1a1a">my_workflow:</span></code>
<code><span style="color:#1a1a1a">  </span></code><code><span style="color:#1a1a1a">tasks:</span></code>
<code><span style="color:#1a1a1a">    </span></code><code><span style="color:#1a1a1a">...</span></code>
<code><span style="color:#1a1a1a">    </span></code><code><span style="color:#1a1a1a">send_error_email:</span></code>
<code><span style="color:#1a1a1a">      </span></code><code><span style="color:#1a1a1a">action: error_email execution_id=<% execution().id %></span></code></span>

2.2 task

task用来描述Workflow中包含的工作步骤,用来定义执行一个action之后,执行成功做什么,执行失败做什么等等,整个workflow就是由task构成的DAG图,具体使用方法查看workflow小节。

2.3 workflow

我们知道Mistral的目标就是提供workflow as service服务,因此workflow是Mistral的主体部分,一个workflow由至少一个task组成,task描述了具体的执行步骤和行为,workflow则描述了task之间的执行顺序、依赖关系、方式以及输入、输出等。

概念不多说,先上个官方例子(这个例子有问题,不能直接运行,仅作为demo使用):

<span style="color:#00000a"><code><span style="color:#1a1a1a">---</span></code>
<code><span style="color:#1a1a1a">version: '2.0'</span></code>
<code><span style="color:#1a1a1a"> </span></code>
<code><span style="color:#1a1a1a">create_vm:</span></code>
<code><span style="color:#1a1a1a">  </span></code><code><span style="color:#1a1a1a">description: Simple workflow example</span></code>
<code><span style="color:#1a1a1a">  </span></code><code><span style="color:#1a1a1a">type: direct</span></code>
<code><span style="color:#1a1a1a"> </span></code>
<code><span style="color:#1a1a1a">  </span></code><code><span style="color:#1a1a1a">input:</span></code>
<code><span style="color:#1a1a1a">    </span></code><code><span style="color:#1a1a1a">- vm_name</span></code>
<code><span style="color:#1a1a1a">    </span></code><code><span style="color:#1a1a1a">- image_ref</span></code>
<code><span style="color:#1a1a1a">    </span></code><code><span style="color:#1a1a1a">- flavor_ref</span></code>
<code><span style="color:#1a1a1a">  </span></code><code><span style="color:#1a1a1a">output:</span></code>
<code><span style="color:#1a1a1a">    </span></code><code><span style="color:#1a1a1a">vm_id: <% $.vm_id %></span></code>
<code><span style="color:#1a1a1a"> </span></code>
<code><span style="color:#1a1a1a">  </span></code><code><span style="color:#1a1a1a">tasks:</span></code>
<code><span style="color:#1a1a1a">    </span></code><code><span style="color:#1a1a1a">create_server:</span></code>
<code><span style="color:#1a1a1a">      </span></code><code><span style="color:#1a1a1a">action: nova.servers_create name=<% $.vm_name %> image=<% $.image_ref %> flavor=<% $.flavor_ref %></span></code>
<code><span style="color:#1a1a1a">      </span></code><code><span style="color:#1a1a1a">publish:</span></code>
<code><span style="color:#1a1a1a">        </span></code><code><span style="color:#1a1a1a">vm_id: <% task(create_server).result.id %></span></code>
<code><span style="color:#1a1a1a">      </span></code><code><span style="color:#1a1a1a">on-success:</span></code>
<code><span style="color:#1a1a1a">        </span></code><code><span style="color:#1a1a1a">- wait_for_instance</span></code>
<code><span style="color:#1a1a1a"> </span></code>
<code><span style="color:#1a1a1a">    </span></code><code><span style="color:#1a1a1a">wait_for_instance:</span></code>
<code><span style="color:#1a1a1a">      </span></code><code><span style="color:#1a1a1a">action: nova.servers_find id=<% $.vm_id %> status='ACTIVE'</span></code>
<code><span style="color:#1a1a1a">      </span></code><code><span style="color:#1a1a1a">retry:</span></code>
<code><span style="color:#1a1a1a">        </span></code><code><span style="color:#1a1a1a">delay: 5</span></code>
<code><span style="color:#1a1a1a">        </span></code><code><span style="color:#1a1a1a">count: 15</span></code></span>

具体的含义先不用过多纠结。只需要知道上面的例子定义了一个名称为create_vm的workflow,输入包含了三个必要参数,分别为vm_name、image_ref、flavor_ref,输出虚拟机的idvm_id。整个workflow包含了两个task,第一个task是create_server,它调用OpenStack的nova.servers_create创建虚机,on-success指定task执行成功的操作,这里是执行wait_for_instance,如果第一个task执行失败则第二个task不会执行。通过retry设定轮询间隔和轮询次数,只有当新创建的虚拟机状态为ACTIVE才算整个workflow执行成功。

workflow包含以下两种类型:

  • Direct Workflow

  • Reverse Workflow

2.3.1 direct workflow

我们前面的例子就属于Direct Workflow,这种workflow可以简单理解为正向流程,即后一个task执行需要依赖前一个task执行结果,如图:

该类型的task主要通过以下三个指令控制下一个task的执行:

  • on-success:此任务执行成功后需要执行的任务列表。

  • on-error:此任务执行出错后需要执行的任务列表。

  • on-complete:此任务执行结束后(不管成功还是失败)需要执行的任务列表

注意理解以上三个指令的语义,尤其是on-error指令,它类似于编程语言的异常,默认情况下如果没有on-error指令,则不会执行后面的task,并把当前workfow执行结果execution标识为ERROR。我们看一个例子:

<span style="color:#00000a"><code><span style="color:#1a1a1a">---</span></code>
<code><span style="color:#1a1a1a">version: "2.0"</span></code>
<code><span style="color:#1a1a1a">start_server:</span></code>
<code><span style="color:#1a1a1a">  </span></code><code><span style="color:#1a1a1a">type: direct</span></code>
<code><span style="color:#1a1a1a">  </span></code><code><span style="color:#1a1a1a">input:</span></code>
<code><span style="color:#1a1a1a">    </span></code><code><span style="color:#1a1a1a">- server_id</span></code>
<code><span style="color:#1a1a1a">  </span></code><code><span style="color:#1a1a1a">description: Start the specified server.</span></code>
<code><span style="color:#1a1a1a">  </span></code><code><span style="color:#1a1a1a">tasks:</span></code>
<code><span style="color:#1a1a1a">    </span></code><code><span style="color:#1a1a1a">start_server:</span></code>
<code><span style="color:#1a1a1a">      </span></code><code><span style="color:#1a1a1a">description: Start the specified server.</span></code>
<code><span style="color:#1a1a1a">      </span></code><code><span style="color:#1a1a1a">action: nova.servers_start server=<% $.server_id %></span></code>
<code><span style="color:#1a1a1a">      </span></code><code><span style="color:#1a1a1a">on-error:</span></code>
<code><span style="color:#1a1a1a">        </span></code><code><span style="color:#1a1a1a">- noop</span></code>
<code><span style="color:#1a1a1a">      </span></code><code><span style="color:#1a1a1a">on-complete:</span></code>
<code><span style="color:#1a1a1a">        </span></code><code><span style="color:#1a1a1a">- wait_for_server_to_active</span></code>
<code><span style="color:#1a1a1a">    </span></code><code><span style="color:#1a1a1a">wait_for_server_to_active:</span></code>
<code><span style="color:#1a1a1a">      </span></code><code><span style="color:#1a1a1a">action: int32bit.nova.servers.assert_power_status server_id=<% $.server_id %> status='running'</span></code>
<code><span style="color:#1a1a1a">      </span></code><code><span style="color:#1a1a1a">retry:</span></code>
<code><span style="color:#1a1a1a">        </span></code><code><span style="color:#1a1a1a">delay: 5</span></code>
<code><span style="color:#1a1a1a">        </span></code><code><span style="color:#1a1a1a">count: 5</span></code>
<code><span style="color:#1a1a1a">      </span></code><code><span style="color:#1a1a1a">on-complete:</span></code>
<code><span style="color:#1a1a1a">        </span></code><code><span style="color:#1a1a1a">- wait_for_all_tasks</span></code>
<code><span style="color:#1a1a1a">    </span></code><code><span style="color:#1a1a1a">wait_for_all_tasks:</span></code>
<code><span style="color:#1a1a1a">      </span></code><code><span style="color:#1a1a1a">join: all</span></code>
<code><span style="color:#1a1a1a">      </span></code><code><span style="color:#1a1a1a">action: std.noop</span></code>
<code><span style="color:#1a1a1a">      </span></code><code><span style="color:#1a1a1a">publish:</span></code>
<code><span style="color:#1a1a1a">        </span></code><code><span style="color:#1a1a1a">error_task: <% tasks(execution().id, false, 'ERROR') %></span></code></span>

以上是一个很简单workfow用于实现虚拟机的关机,我们期望的结果是只需要保证虚拟机最终状态是running即可。我们看start_server这个task的on-error为noop,即什么都不要做,但这不是多余的,如果没有该指令,start_server执行失败(比如虚拟机本来就是running状态),则会立即退出整个execution执行,并且execution状态为ERROR,这并不是我们期望的结果。

三个指令的关系可以用编程语言理解:

<span style="color:#00000a"><code><span style="color:#1a1a1a"><strong>try</strong></span></code><code><span style="color:#1a1a1a">:</span></code>
<code><span style="color:#1a1a1a">    </span></code><code><span style="color:#1a1a1a">do_action</span></code>
<code><span style="color:#1a1a1a">    </span></code><code><span style="color:#1a1a1a">do_on_success</span></code>
<code><span style="color:#1a1a1a"><strong>except</strong></span></code><code><span style="color:#1a1a1a">:</span></code>
<code><span style="color:#1a1a1a">    </span></code><code><span style="color:#1a1a1a">do_on_error</span></code>
<code><span style="color:#1a1a1a"><strong>finally</strong></span></code><code><span style="color:#1a1a1a">:</span></code>
<code><span style="color:#1a1a1a">    </span></code><code><span style="color:#1a1a1a">do_complete</span></code></span>

workflow也支持并行,即同时执行多个task,类似于一个进程同时跑多个线程,这种行为称为fork,使用join指令等待所有的task执行结束,比如:

<span style="color:#00000a"><code><span style="color:#1a1a1a">tasks:</span></code>
<code><span style="color:#1a1a1a">  </span></code><code><span style="color:#1a1a1a">A:</span></code>
<code><span style="color:#1a1a1a">    </span></code><code><span style="color:#1a1a1a">action: action.x</span></code>
<code><span style="color:#1a1a1a">    </span></code><code><span style="color:#1a1a1a">on-success:</span></code>
<code><span style="color:#1a1a1a">      </span></code><code><span style="color:#1a1a1a">C</span></code>
<code><span style="color:#1a1a1a">  </span></code>
<code><span style="color:#1a1a1a">  </span></code><code><span style="color:#1a1a1a">B:</span></code>
<code><span style="color:#1a1a1a">    </span></code><code><span style="color:#1a1a1a">action: action.y</span></code>
<code><span style="color:#1a1a1a">    </span></code><code><span style="color:#1a1a1a">on-success:</span></code>
<code><span style="color:#1a1a1a">      </span></code><code><span style="color:#1a1a1a">C</span></code>
<code><span style="color:#1a1a1a">  </span></code>
<code><span style="color:#1a1a1a">  </span></code><code><span style="color:#1a1a1a">C:</span></code>
<code><span style="color:#1a1a1a">    </span></code><code><span style="color:#1a1a1a">join: all</span></code>
<code><span style="color:#1a1a1a">    </span></code><code><span style="color:#1a1a1a">action: action.z</span></code>
<code><span style="color:#1a1a1a">    </span></code><code><span style="color:#1a1a1a">publish:</span></code>
<code><span style="color:#1a1a1a">      </span></code><code><span style="color:#1a1a1a">ret</span></code></span>

join后面的all表示等待所有task完成,你也可以设置为1或者one,这样只要其中任意一个task执行结束就好了,类型于Java并发编程的invokeAll和invokeAny的关系。

2.3.2 Reverse Workflow

在这个类型的Wrokflow中,任务的关系是反向依赖的,即执行A,如果A中声明了依赖的任务B,则需要先执行B,如图:

其中一个task的依赖使用requires指令定义。比如:

<span style="color:#00000a"><code><span style="color:#1a1a1a">tasks:</span></code>
<code><span style="color:#1a1a1a">   </span></code><code><span style="color:#1a1a1a">A:</span></code>
<code><span style="color:#1a1a1a">     </span></code><code><span style="color:#1a1a1a">action: action.x</span></code>
<code><span style="color:#1a1a1a"> </span></code>
<code><span style="color:#1a1a1a">   </span></code><code><span style="color:#1a1a1a">B:</span></code>
<code><span style="color:#1a1a1a">     </span></code><code><span style="color:#1a1a1a">action: action.y</span></code>
<code><span style="color:#1a1a1a"> </span></code>
<code><span style="color:#1a1a1a">   </span></code><code><span style="color:#1a1a1a">C:</span></code>
<code><span style="color:#1a1a1a">     </span></code><code><span style="color:#1a1a1a">action: action.z</span></code>
<code><span style="color:#1a1a1a">       </span></code><code><span style="color:#1a1a1a">requires: [A</span></code><code><span style="color:#1a1a1a">,</span></code><code><span style="color:#1a1a1a">B]</span></code></span>

需要注意的是,reverse workflow不能使用on-success、on-error以及on-complete指令。

2.4 DSL语言简介

我们前面定义ad-hoc actions以及workflow都使用的是yaml或者json,我们称为schema(模式),schema不仅可以使用yaml、json定义,也可以使用xml等其它任何表示语言,它和数据库的schema是类似的,它包括两个方面约束:

  • 包括哪些字段。

  • 字段的值类型是什么。

对schema进行定义的一套规则语法,我们称为DSL(Domain Specific Language),Mistral的DSL参考:Mistral DSL v2。

Mistral的DSL schema语法校验是通过JSON Schema完成,下面是一个非常简单的例子:

<span style="color:#00000a"><code><span style="color:#1a1a1a">{</span></code>
<code><span style="color:#1a1a1a">    </span></code><code><span style="color:#175199">"title"</span></code><code><span style="color:#1a1a1a">: </span></code><code><span style="color:#f1403c">"Person"</span></code><code><span style="color:#1a1a1a">,</span></code>
<code><span style="color:#1a1a1a">    </span></code><code><span style="color:#175199">"type"</span></code><code><span style="color:#1a1a1a">: </span></code><code><span style="color:#f1403c">"object"</span></code><code><span style="color:#1a1a1a">,</span></code>
<code><span style="color:#1a1a1a">    </span></code><code><span style="color:#175199">"properties"</span></code><code><span style="color:#1a1a1a">: {</span></code>
<code><span style="color:#1a1a1a">        </span></code><code><span style="color:#175199">"firstName"</span></code><code><span style="color:#1a1a1a">: {</span></code>
<code><span style="color:#1a1a1a">            </span></code><code><span style="color:#175199">"type"</span></code><code><span style="color:#1a1a1a">: </span></code><code><span style="color:#f1403c">"string"</span></code>
<code><span style="color:#1a1a1a">        </span></code><code><span style="color:#1a1a1a">},</span></code>
<code><span style="color:#1a1a1a">        </span></code><code><span style="color:#175199">"lastName"</span></code><code><span style="color:#1a1a1a">: {</span></code>
<code><span style="color:#1a1a1a">            </span></code><code><span style="color:#175199">"type"</span></code><code><span style="color:#1a1a1a">: </span></code><code><span style="color:#f1403c">"string"</span></code>
<code><span style="color:#1a1a1a">        </span></code><code><span style="color:#1a1a1a">},</span></code>
<code><span style="color:#1a1a1a">        </span></code><code><span style="color:#175199">"age"</span></code><code><span style="color:#1a1a1a">: {</span></code>
<code><span style="color:#1a1a1a">            </span></code><code><span style="color:#175199">"description"</span></code><code><span style="color:#1a1a1a">: </span></code><code><span style="color:#f1403c">"Age in years"</span></code><code><span style="color:#1a1a1a">,</span></code>
<code><span style="color:#1a1a1a">            </span></code><code><span style="color:#175199">"type"</span></code><code><span style="color:#1a1a1a">: </span></code><code><span style="color:#f1403c">"integer"</span></code><code><span style="color:#1a1a1a">,</span></code>
<code><span style="color:#1a1a1a">            </span></code><code><span style="color:#175199">"minimum"</span></code><code><span style="color:#1a1a1a">: </span></code><code><span style="color:#0084ff">0</span></code>
<code><span style="color:#1a1a1a">        </span></code><code><span style="color:#1a1a1a">}</span></code>
<code><span style="color:#1a1a1a">    </span></code><code><span style="color:#1a1a1a">},</span></code>
<code><span style="color:#1a1a1a">    </span></code><code><span style="color:#175199">"required"</span></code><code><span style="color:#1a1a1a">: [</span></code><code><span style="color:#f1403c">"firstName"</span></code><code><span style="color:#1a1a1a">, </span></code><code><span style="color:#f1403c">"lastName"</span></code><code><span style="color:#1a1a1a">]</span></code>
<code><span style="color:#1a1a1a">}</span></code></span>

以上定义了一个Person schema,其中包括两个必需参数firstName和lastName以及一个可选参数age,前二者的类型为string,age的值类型为integer,并且最小值限制为0。更多关于json schema可参考json schema官方文档,作者还写了本非常不错的电子书《Understanding JSON Schema》。

Mistral解析json schema使用的python库是jsonschema,其使用方法也非常简单:

<span style="color:#00000a"><code><span style="color:#1a1a1a"><strong>>>></strong></span></code><code><span style="color:#1a1a1a"> </span></code><code><span style="color:#1a1a1a"><strong>from</strong></span></code><code><span style="color:#1a1a1a"> </span></code><code><span style="color:#646464">jsonschema</span></code><code><span style="color:#1a1a1a"> </span></code><code><span style="color:#1a1a1a"><strong>import</strong></span></code><code><span style="color:#1a1a1a"> validate</span></code>
<code><span style="color:#1a1a1a"> </span></code>
<code><span style="color:#1a1a1a"><strong>>>></strong></span></code><code><span style="color:#1a1a1a"> </span></code><code><span style="color:#999999"><em># A sample schema, like what we'd get from json.load()</em></span></code>
<code><span style="color:#1a1a1a"><strong>>>></strong></span></code><code><span style="color:#1a1a1a"> schema </span></code><code><span style="color:#1a1a1a"><strong>=</strong></span></code><code><span style="color:#1a1a1a"> {</span></code>
<code><span style="color:#1a1a1a"><strong>...</strong></span></code><code><span style="color:#1a1a1a">     </span></code><code><span style="color:#f1403c">"type"</span></code><code><span style="color:#1a1a1a"> : </span></code><code><span style="color:#f1403c">"object"</span></code><code><span style="color:#1a1a1a">,</span></code>
<code><span style="color:#1a1a1a"><strong>...</strong></span></code><code><span style="color:#1a1a1a">     </span></code><code><span style="color:#f1403c">"properties"</span></code><code><span style="color:#1a1a1a"> : {</span></code>
<code><span style="color:#1a1a1a"><strong>...</strong></span></code><code><span style="color:#1a1a1a">         </span></code><code><span style="color:#f1403c">"price"</span></code><code><span style="color:#1a1a1a"> : {</span></code><code><span style="color:#f1403c">"type"</span></code><code><span style="color:#1a1a1a"> : </span></code><code><span style="color:#f1403c">"number"</span></code><code><span style="color:#1a1a1a">},</span></code>
<code><span style="color:#1a1a1a"><strong>...</strong></span></code><code><span style="color:#1a1a1a">         </span></code><code><span style="color:#f1403c">"name"</span></code><code><span style="color:#1a1a1a"> : {</span></code><code><span style="color:#f1403c">"type"</span></code><code><span style="color:#1a1a1a"> : </span></code><code><span style="color:#f1403c">"string"</span></code><code><span style="color:#1a1a1a">},</span></code>
<code><span style="color:#1a1a1a"><strong>...</strong></span></code><code><span style="color:#1a1a1a">     },</span></code>
<code><span style="color:#1a1a1a"><strong>...</strong></span></code><code><span style="color:#1a1a1a"> }</span></code>
<code><span style="color:#1a1a1a"> </span></code>
<code><span style="color:#1a1a1a"><strong>>>></strong></span></code><code><span style="color:#1a1a1a"> </span></code><code><span style="color:#999999"><em># If no exception is raised by validate(), the instance is valid.</em></span></code>
<code><span style="color:#1a1a1a"><strong>>>></strong></span></code><code><span style="color:#1a1a1a"> validate({</span></code><code><span style="color:#f1403c">"name"</span></code><code><span style="color:#1a1a1a"> : </span></code><code><span style="color:#f1403c">"Eggs"</span></code><code><span style="color:#1a1a1a">, </span></code><code><span style="color:#f1403c">"price"</span></code><code><span style="color:#1a1a1a"> : </span></code><code><span style="color:#0084ff">34.99</span></code><code><span style="color:#1a1a1a">}, schema)</span></code>
<code><span style="color:#1a1a1a"> </span></code>
<code><span style="color:#1a1a1a"><strong>>>></strong></span></code><code><span style="color:#1a1a1a"> validate(</span></code>
<code><span style="color:#1a1a1a"><strong>...</strong></span></code><code><span style="color:#1a1a1a">     {</span></code><code><span style="color:#f1403c">"name"</span></code><code><span style="color:#1a1a1a"> : </span></code><code><span style="color:#f1403c">"Eggs"</span></code><code><span style="color:#1a1a1a">, </span></code><code><span style="color:#f1403c">"price"</span></code><code><span style="color:#1a1a1a"> : </span></code><code><span style="color:#f1403c">"Invalid"</span></code><code><span style="color:#1a1a1a">}, schema</span></code>
<code><span style="color:#1a1a1a"><strong>...</strong></span></code><code><span style="color:#1a1a1a"> )                                   </span></code><code><span style="color:#999999"><em># doctest: +IGNORE_EXCEPTION_DETAIL</em></span></code>
<code><span style="color:#1a1a1a">Traceback (most recent call last):</span></code>
<code><span style="color:#1a1a1a">    </span></code><code><span style="color:#1a1a1a"><strong>...</strong></span></code>
<code><span style="color:#1a1a1a">ValidationError: </span></code><code><span style="color:#f1403c">'Invalid'</span></code><code><span style="color:#1a1a1a"> </span></code><code><span style="color:#1a1a1a"><strong>is</strong></span></code><code><span style="color:#1a1a1a"> </span></code><code><span style="color:#1a1a1a"><strong>not</strong></span></code><code><span style="color:#1a1a1a"> of </span></code><code><span style="color:#0084ff">type</span></code><code><span style="color:#1a1a1a"> </span></code><code><span style="color:#f1403c">'number'</span></code></span>

你也可以直接通过jsonschema CLI进行校验:

<span style="color:#00000a"><code><span style="color:#1a1a1a">$ jsonschema -i sample.json sample.schema</span></code></span>

3 Mistral实践

3.1 Mistral部署

Mistral相对其它OpenStack服务比较简单,也不需要像Trove那样调整网络。

Mistral主要包含以下三个服务:

  • mistral-api

  • mistral-engine

  • mistral-executor

以上三个服务的功能不详细介绍,配置可参考官方文档Mistral Configuration Guide。这里需要指出的是,Mistral的所有服务都是支持水平扩展的,即可以同时运行多个服务实例。

另外,Mistral服务的心跳和状态监控和Nova、Cinder等不一样,Mistral不是通过不断刷数据库实现心跳的,而是通过tooz coordinator的member管理实现的,当进程启动时,会自动注册member,进程挂了或者退出时,会从member中移除,由此判断该服务是否运行。因此,如果需要使用服务状态功能,需要配置coordinator,coordinator的backend可以是zookeeper、redis、memcached等,这里以memcached为例,配置如下:

<span style="color:#00000a"><code><span style="color:#0084ff">[coordination] # From mistral.config</span></code>
<code><span style="color:#0084ff">backend_url</span></code><code><span style="color:#1a1a1a"> </span></code><code><span style="color:#1a1a1a"><strong>=</strong></span></code><code><span style="color:#1a1a1a"> </span></code><code><span style="color:#f1403c">memcached://localhost:11211 </span></code>
<code><span style="color:#0084ff">heartbeat_interval</span></code><code><span style="color:#1a1a1a"> </span></code><code><span style="color:#1a1a1a"><strong>=</strong></span></code><code><span style="color:#1a1a1a"> </span></code><code><span style="color:#f1403c">5.0 </span></code></span>

配置了coordinator后,就可以使用mistral service-list查看服务列表了:

<span style="color:#00000a"><code><span style="color:#1a1a1a">$ mistral service-list</span></code>
<code><span style="color:#1a1a1a">+----------------+----------------+</span></code>
<code><span style="color:#1a1a1a">| Name           | Type           |</span></code>
<code><span style="color:#1a1a1a">+----------------+----------------+</span></code>
<code><span style="color:#1a1a1a">| controller_77391 | engine_group   |</span></code>
<code><span style="color:#1a1a1a">| controller_80355 | api_group      |</span></code>
<code><span style="color:#1a1a1a">| controller_77494 | executor_group |</span></code>
<code><span style="color:#1a1a1a">+----------------+----------------+</span></code></span>

以上controller是hostname,77391是服务的pid,type包含api、engine、executor三类。

原理也很简单,Mistral服务会每隔heartbeat_interval调用heartbeat方法发送心跳,如果backend是memcached,则会设置一个key-value,key为group id,value为”It’s alive!”,ttl为30s,实现代码如下:

<span style="color:#00000a"><code><span style="color:#1a1a1a">@_translate_failures</span></code>
<code><span style="color:#1a1a1a">   </span></code><code><span style="color:#1a1a1a"><strong>def</strong></span></code><code><span style="color:#1a1a1a"> </span></code><code><span style="color:#f1403c"><strong>heartbeat</strong></span></code><code><span style="color:#1a1a1a">(</span></code><code><span style="color:#999999">self</span></code><code><span style="color:#1a1a1a">):</span></code>
<code><span style="color:#1a1a1a">       </span></code><code><span style="color:#999999">self</span></code><code><span style="color:#1a1a1a"><strong>.</strong></span></code><code><span style="color:#1a1a1a">client</span></code><code><span style="color:#1a1a1a"><strong>.</strong></span></code><code><span style="color:#0084ff">set</span></code><code><span style="color:#1a1a1a">(</span></code><code><span style="color:#999999">self</span></code><code><span style="color:#1a1a1a"><strong>.</strong></span></code><code><span style="color:#1a1a1a">_encode_member_id(</span></code><code><span style="color:#999999">self</span></code><code><span style="color:#1a1a1a"><strong>.</strong></span></code><code><span style="color:#1a1a1a">_member_id),</span></code>
<code><span style="color:#1a1a1a">                       </span></code><code><span style="color:#999999">self</span></code><code><span style="color:#1a1a1a"><strong>.</strong></span></code><code><span style="color:#1a1a1a">STILL_ALIVE,</span></code>
<code><span style="color:#1a1a1a">                       </span></code><code><span style="color:#1a1a1a">expire</span></code><code><span style="color:#1a1a1a"><strong>=</strong></span></code><code><span style="color:#999999">self</span></code><code><span style="color:#1a1a1a"><strong>.</strong></span></code><code><span style="color:#1a1a1a">membership_timeout)</span></code>
<code><span style="color:#1a1a1a">       </span></code><code><span style="color:#999999"><em># Reset the acquired locks</em></span></code>
<code><span style="color:#1a1a1a">       </span></code><code><span style="color:#1a1a1a"><strong>for</strong></span></code><code><span style="color:#1a1a1a"> lock </span></code><code><span style="color:#1a1a1a"><strong>in</strong></span></code><code><span style="color:#1a1a1a"> </span></code><code><span style="color:#999999">self</span></code><code><span style="color:#1a1a1a"><strong>.</strong></span></code><code><span style="color:#1a1a1a">_acquired_locks:</span></code>
<code><span style="color:#1a1a1a">           </span></code><code><span style="color:#1a1a1a">lock</span></code><code><span style="color:#1a1a1a"><strong>.</strong></span></code><code><span style="color:#1a1a1a">heartbeat()</span></code>
<code><span style="color:#1a1a1a">       </span></code><code><span style="color:#1a1a1a"><strong>return</strong></span></code><code><span style="color:#1a1a1a"> </span></code><code><span style="color:#0084ff">min</span></code><code><span style="color:#1a1a1a">(</span></code><code><span style="color:#999999">self</span></code><code><span style="color:#1a1a1a"><strong>.</strong></span></code><code><span style="color:#1a1a1a">membership_timeout,</span></code>
<code><span style="color:#1a1a1a">                  </span></code><code><span style="color:#999999">self</span></code><code><span style="color:#1a1a1a"><strong>.</strong></span></code><code><span style="color:#1a1a1a">leader_timeout,</span></code>
<code><span style="color:#1a1a1a">                  </span></code><code><span style="color:#999999">self</span></code><code><span style="color:#1a1a1a"><strong>.</strong></span></code><code><span style="color:#1a1a1a">lock_timeout)</span></code></span>

可以查看memcached值:

<span style="color:#00000a"><code><span style="color:#1a1a1a">$ telnet localhost </span></code><code><span style="color:#0084ff">11211</span></code>
<code><span style="color:#1a1a1a">Trying </span></code><code><span style="color:#0084ff">127</span></code><code><span style="color:#1a1a1a">.0.0.1...</span></code>
<code><span style="color:#1a1a1a">Connected to localhost.</span></code>
<code><span style="color:#1a1a1a">Escape character is </span></code><code><span style="color:#f1403c">'^]'</span></code><code><span style="color:#1a1a1a">.</span></code>
<code><span style="color:#1a1a1a">stats cachedump </span></code><code><span style="color:#0084ff">2</span></code><code><span style="color:#1a1a1a"> </span></code><code><span style="color:#0084ff">0</span></code>
<code><span style="color:#1a1a1a">ITEM _TOOZ_MEMBER_controller_77494 </span></code><code><span style="color:#1a1a1a"><strong>[</strong></span></code><code><span style="color:#0084ff">11</span></code><code><span style="color:#1a1a1a"> b; </span></code><code><span style="color:#0084ff">1502183642</span></code><code><span style="color:#1a1a1a"> s</span></code><code><span style="color:#1a1a1a"><strong>]</strong></span></code>
<code><span style="color:#1a1a1a">ITEM _TOOZ_MEMBER_controller_80355 </span></code><code><span style="color:#1a1a1a"><strong>[</strong></span></code><code><span style="color:#0084ff">11</span></code><code><span style="color:#1a1a1a"> b; </span></code><code><span style="color:#0084ff">1502183640</span></code><code><span style="color:#1a1a1a"> s</span></code><code><span style="color:#1a1a1a"><strong>]</strong></span></code>
<code><span style="color:#1a1a1a">ITEM _TOOZ_MEMBER_controller_77391 </span></code><code><span style="color:#1a1a1a"><strong>[</strong></span></code><code><span style="color:#0084ff">11</span></code><code><span style="color:#1a1a1a"> b; </span></code><code><span style="color:#0084ff">1502183640</span></code><code><span style="color:#1a1a1a"> s</span></code><code><span style="color:#1a1a1a"><strong>]</strong></span></code>
<code><span style="color:#1a1a1a">END</span></code>
<code><span style="color:#1a1a1a">get _TOOZ_MEMBER_controller_77391</span></code>
<code><span style="color:#1a1a1a">VALUE _TOOZ_MEMBER_controller_77391 </span></code><code><span style="color:#0084ff">1</span></code><code><span style="color:#1a1a1a"> </span></code><code><span style="color:#0084ff">11</span></code>
<code><span style="color:#1a1a1a">It</span></code><code><span style="color:#f1403c">'</span></code><code><span style="color:#1a1a1a">s alive!</span></code>
<code><span style="color:#1a1a1a">END</span></code></span>

3.2 开始使用Mistral

3.2.1 创建workflow

以一个官方的简单workflow为例,yaml文件为my_workflow.yaml,

<span style="color:#00000a"><code><span style="color:#1a1a1a">---</span></code>
<code><span style="color:#1a1a1a">version: "2.0"</span></code><code><span style="color:#1a1a1a">my_workflow:</span></code>
<code><span style="color:#1a1a1a">  </span></code><code><span style="color:#1a1a1a">type: direct</span></code><code><span style="color:#1a1a1a">  </span></code><code><span style="color:#1a1a1a">input:</span></code>
<code><span style="color:#1a1a1a">    </span></code><code><span style="color:#1a1a1a">- names</span></code><code><span style="color:#1a1a1a">  </span></code><code><span style="color:#1a1a1a">tasks:</span></code>
<code><span style="color:#1a1a1a">    </span></code><code><span style="color:#1a1a1a">task1:</span></code>
<code><span style="color:#1a1a1a">      </span></code><code><span style="color:#1a1a1a">with-items: name in <% $.names %></span></code>
<code><span style="color:#1a1a1a">      </span></code><code><span style="color:#1a1a1a">action: std.echo output=<% $.name %></span></code>
<code><span style="color:#1a1a1a">      </span></code><code><span style="color:#1a1a1a">on-success: task2</span></code><code><span style="color:#1a1a1a">    </span></code><code><span style="color:#1a1a1a">task2:</span></code>
<code><span style="color:#1a1a1a">      </span></code><code><span style="color:#1a1a1a">action: std.echo output="Done"</span></code></span>

注意以上names参数是一个数组,with-times也是workflow的一个指令,它会遍历数组的所有元素,action为std.echo,即打印name参数,执行成功后执行task2,输出"Done"。

使用mistral workflow-create子命令创建workflow:

<span style="color:#00000a"><code><span style="color:#1a1a1a">$ mistral workflow-create my_workflow.yaml</span></code>
<code><span style="color:#1a1a1a">+------------------------------------+-------------+--------+---------+---------------------+------------+</span></code>
<code><span style="color:#1a1a1a">|ID                                  | Name        | Tags   | Input   | Created at          | Updated at |</span></code>
<code><span style="color:#1a1a1a">+------------------------------------+-------------+--------+---------+---------------------+------------+</span></code>
<code><span style="color:#1a1a1a">|9b719d62-2ced-47d3-b500-73261bb0b2ad| my_workflow | <none> | names   | 2017-04-13 08:44:49 | None       |</span></code>
<code><span style="color:#1a1a1a">+------------------------------------+-------------+--------+---------+---------------------+------------+</span></code></span>

3.2.2 执行workflow

创建workflow相当于创建了一个任务模板,并没有实际执行,我们需要通过execution-create子命令触发执行,执行时需要传递参数,参数以json格式传递:

<span style="color:#00000a"><code><span style="color:#1a1a1a">$ mistral execution-create my_workflow '{"names": ["John", "Mistral", "Ivan", "Crystal"]}'</span></code>
<code><span style="color:#1a1a1a">+-------------------+--------------------------------------+</span></code>
<code><span style="color:#1a1a1a">| Field             | Value                                |</span></code>
<code><span style="color:#1a1a1a">+-------------------+--------------------------------------+</span></code>
<code><span style="color:#1a1a1a">| ID                | 49213eb5-196c-421f-b436-775849b55040 |</span></code>
<code><span style="color:#1a1a1a">| Workflow ID       | 9b719d62-2ced-47d3-b500-73261bb0b2ad |</span></code>
<code><span style="color:#1a1a1a">| Workflow name     | my_workflow                          |</span></code>
<code><span style="color:#1a1a1a">| Description       |                                      |</span></code>
<code><span style="color:#1a1a1a">| Task Execution ID | <none>                               |</span></code>
<code><span style="color:#1a1a1a">| State             | RUNNING                              |</span></code>
<code><span style="color:#1a1a1a">| State info        | None                                 |</span></code>
<code><span style="color:#1a1a1a">| Created at        | 2017-03-06 11:24:10                  |</span></code>
<code><span style="color:#1a1a1a">| Updated at        | 2017-03-06 11:24:10                  |</span></code>
<code><span style="color:#1a1a1a">+-------------------+--------------------------------------+</span></code></span>

执行后,可以通过execution-list查看执行状态。

3.2.3 查看task执行状态

除了使用execution-list查看整个workflow执行的结果,还可以通过task-list查看其所有的task执行状态:

<span style="color:#00000a"><code><span style="color:#1a1a1a">$ mistral task-list 49213eb5-196c-421f-b436-775849b55040</span></code>
<code><span style="color:#1a1a1a">+--------------------------------------+-------+---------------+--------------------------------------+---------+------------+---------------------+---------------------+</span></code>
<code><span style="color:#1a1a1a">| ID                                   | Name  | Workflow name | Execution ID                         | State   | State info | Created at          | Updated at          |</span></code>
<code><span style="color:#1a1a1a">+--------------------------------------+-------+---------------+--------------------------------------+---------+------------+---------------------+---------------------+</span></code>
<code><span style="color:#1a1a1a">| f639e7a9-9609-468e-aa08-7650e1472efe | task1 | my_workflow   | 49213eb5-196c-421f-b436-775849b55040 | SUCCESS | None       | 2017-03-06 11:24:11 | 2017-03-06 11:24:17 |</span></code>
<code><span style="color:#1a1a1a">| d565c5a0-f46f-4ebe-8655-9eb6796307a3 | task2 | my_workflow   | 49213eb5-196c-421f-b436-775849b55040 | SUCCESS | None       | 2017-03-06 11:24:17 | 2017-03-06 11:24:18 |</span></code>
<code><span style="color:#1a1a1a">+--------------------------------------+-------+---------------+--------------------------------------+---------+------------+---------------------+---------------------+</span></code></span>

通过task-get-result查看task的输出,即std.echo结果:

<span style="color:#00000a"><code><span style="color:#1a1a1a">$ mistral task-get-result f639e7a9-9609-468e-aa08-7650e1472efe</span></code>
<code><span style="color:#1a1a1a">[</span></code>
<code><span style="color:#1a1a1a">    </span></code><code><span style="color:#1a1a1a">"John",</span></code>
<code><span style="color:#1a1a1a">    </span></code><code><span style="color:#1a1a1a">"Mistral",</span></code>
<code><span style="color:#1a1a1a">    </span></code><code><span style="color:#1a1a1a">"Ivan",</span></code>
<code><span style="color:#1a1a1a">    </span></code><code><span style="color:#1a1a1a">"Crystal"</span></code>
<code><span style="color:#1a1a1a">]</span></code></span>

3.2.4 查看action执行状态

以上通过task已经获取了执行结果,可以进一步获取每个action的执行情况:

<span style="color:#00000a"><code><span style="color:#1a1a1a">$ mistral action-execution-list f639e7a9-9609-468e-aa08-7650e1472efe</span></code>
<code><span style="color:#1a1a1a">+--------------------------------------+----------+---------------+-----------+--------------------------------------+---------+----------+---------------------+---------------------+</span></code>
<code><span style="color:#1a1a1a">| ID                                   | Name     | Workflow name | Task name | Task ID                              | State   | Accepted | Created at          | Updated at          |</span></code>
<code><span style="color:#1a1a1a">+--------------------------------------+----------+---------------+-----------+--------------------------------------+---------+----------+---------------------+---------------------+</span></code>
<code><span style="color:#1a1a1a">| 4e0a60be-04df-42d7-aa59-5107e599d079 | std.echo | my_workflow   | task1     | f639e7a9-9609-468e-aa08-7650e1472efe | SUCCESS | True     | 2017-03-06 11:24:12 | 2017-03-06 11:24:16 |</span></code>
<code><span style="color:#1a1a1a">| 5bd95da4-9b29-4a79-bcb1-298abd659bd6 | std.echo | my_workflow   | task1     | f639e7a9-9609-468e-aa08-7650e1472efe | SUCCESS | True     | 2017-03-06 11:24:12 | 2017-03-06 11:24:16 |</span></code>
<code><span style="color:#1a1a1a">| 6ae6c19e-b51b-4910-9e0e-96c788093715 | std.echo | my_workflow   | task1     | f639e7a9-9609-468e-aa08-7650e1472efe | SUCCESS | True     | 2017-03-06 11:24:12 | 2017-03-06 11:24:16 |</span></code>
<code><span style="color:#1a1a1a">| bed5a6a2-c1d8-460f-a2a5-b36f72f85e19 | std.echo | my_workflow   | task1     | f639e7a9-9609-468e-aa08-7650e1472efe | SUCCESS | True     | 2017-03-06 11:24:12 | 2017-03-06 11:24:17 |</span></code>
<code><span style="color:#1a1a1a">+--------------------------------------+----------+---------------+-----------+--------------------------------------+---------+----------+---------------------+---------------------+</span></code></span>

以上由于task1使用了with-items循环,因此会对应多个actions,你也可以获取其中一个action的结果:

<span style="color:#00000a"><code><span style="color:#1a1a1a">$ mistral action-execution-get-output 4e0a60be-04df-42d7-aa59-5107e599d079</span></code>
<code><span style="color:#1a1a1a">{ "result": "John" } </span></code></span>

4 定时任务

4.1 cron介绍

cron是一个在类Unix操作系统上的任务计划程序。它可以让用户在指定时间段周期性地运行命令或者shell脚本,通常被用在系统的自动化维护或者管理。

crontab 的基本格式是:

<span style="color:#00000a"><code><span style="color:#1a1a1a">┌───────────── </span></code><code><span style="color:#1a1a1a">minute (0 - 59)</span></code>
<code><span style="color:#1a1a1a"> │ ┌───────────── </span></code><code><span style="color:#1a1a1a">hour (0 - 23)</span></code>
<code><span style="color:#1a1a1a"> │ │ ┌───────────── </span></code><code><span style="color:#1a1a1a">day of month (1 - 31)</span></code>
<code><span style="color:#1a1a1a"> │ │ │ ┌───────────── </span></code><code><span style="color:#1a1a1a">month (1 - 12)</span></code>
<code><span style="color:#1a1a1a"> │ │ │ │ ┌───────────── </span></code><code><span style="color:#1a1a1a">day of week (0 - 6) (Sunday to Saturday;</span></code>
<code><span style="color:#1a1a1a"> │ │ │ │ │                                       </span></code><code><span style="color:#1a1a1a">7 is also Sunday)</span></code>
<code><span style="color:#1a1a1a"> │ │ │ │ │</span></code>
<code><span style="color:#1a1a1a"> │ │ │ │ │</span></code>
<code><span style="color:#1a1a1a"> </span></code><code><span style="color:#1a1a1a">* * * * *  command to execute</span></code></span>

更详细的cron语法介绍可以参考维基百科–Cron。

Mistral使用了Python的crontiner库解析crontab,这个库封装得特别好,我们只需要会用两个方法即可,一个是构造方法__init__,另一个是获取下一次执行时间方法get_next()。构造方法签名如下:

<span style="color:#00000a"><code><span style="color:#1a1a1a"><strong>def</strong></span></code><code><span style="color:#1a1a1a"> __init__(</span></code><code><span style="color:#999999">self</span></code><code><span style="color:#1a1a1a">, cron_format, start_time</span></code><code><span style="color:#1a1a1a"><strong>=</strong></span></code><code><span style="color:#1a1a1a">time</span></code><code><span style="color:#1a1a1a"><strong>.</strong></span></code><code><span style="color:#1a1a1a">time(), day_or</span></code><code><span style="color:#1a1a1a"><strong>=</strong></span></code><code><span style="color:#999999">True</span></code><code><span style="color:#1a1a1a">) </span></code></span>

其中cron_format就是标准的cron格式,start_time是开始执行时间,默认从当前时间开始,day_or是处理day和week冲突情况下的处理办法,day_or默认为true,day和week满足其中一个条件就会执行,比如* * 1 * 1,则每个月的1号或者周一都会执行。

get_next方法签名如下:

<span style="color:#00000a"><code><span style="color:#1a1a1a"><strong>def</strong></span></code><code><span style="color:#1a1a1a"> </span></code><code><span style="color:#f1403c"><strong>get_next</strong></span></code><code><span style="color:#1a1a1a">(</span></code><code><span style="color:#999999">self</span></code><code><span style="color:#1a1a1a">, ret_type</span></code><code><span style="color:#1a1a1a"><strong>=</strong></span></code><code><span style="color:#0084ff">float</span></code><code><span style="color:#1a1a1a">) </span></code></span>

其中ret_type指定返回类型,默认为浮点数,可以指定为datetime类型。

<span style="color:#00000a"><code><span style="color:#1a1a1a"><strong>>>></strong></span></code><code><span style="color:#1a1a1a"> </span></code><code><span style="color:#1a1a1a"><strong>from</strong></span></code><code><span style="color:#1a1a1a"> </span></code><code><span style="color:#646464">croniter</span></code><code><span style="color:#1a1a1a"> </span></code><code><span style="color:#1a1a1a"><strong>import</strong></span></code><code><span style="color:#1a1a1a"> croniter</span></code>
<code><span style="color:#1a1a1a"><strong>>>></strong></span></code><code><span style="color:#1a1a1a"> </span></code><code><span style="color:#1a1a1a"><strong>from</strong></span></code><code><span style="color:#1a1a1a"> </span></code><code><span style="color:#646464">datetime</span></code><code><span style="color:#1a1a1a"> </span></code><code><span style="color:#1a1a1a"><strong>import</strong></span></code><code><span style="color:#1a1a1a"> datetime</span></code>
<code><span style="color:#1a1a1a"><strong>>>></strong></span></code><code><span style="color:#1a1a1a"> base </span></code><code><span style="color:#1a1a1a"><strong>=</strong></span></code><code><span style="color:#1a1a1a"> datetime(</span></code><code><span style="color:#0084ff">2010</span></code><code><span style="color:#1a1a1a">, </span></code><code><span style="color:#0084ff">1</span></code><code><span style="color:#1a1a1a">, </span></code><code><span style="color:#0084ff">25</span></code><code><span style="color:#1a1a1a">, </span></code><code><span style="color:#0084ff">4</span></code><code><span style="color:#1a1a1a">, </span></code><code><span style="color:#0084ff">46</span></code><code><span style="color:#1a1a1a">)</span></code>
<code><span style="color:#1a1a1a"><strong>>>></strong></span></code><code><span style="color:#1a1a1a"> </span></code><code><span style="color:#0084ff">iter</span></code><code><span style="color:#1a1a1a"> </span></code><code><span style="color:#1a1a1a"><strong>=</strong></span></code><code><span style="color:#1a1a1a"> croniter(</span></code><code><span style="color:#f1403c">'*/5 * * * *'</span></code><code><span style="color:#1a1a1a">, base)  </span></code><code><span style="color:#999999"><em># every 5 minutes</em></span></code>
<code><span style="color:#1a1a1a"><strong>>>></strong></span></code><code><span style="color:#1a1a1a"> </span></code><code><span style="color:#1a1a1a"><strong>print</strong></span></code><code><span style="color:#1a1a1a"> </span></code><code><span style="color:#0084ff">iter</span></code><code><span style="color:#1a1a1a"><strong>.</strong></span></code><code><span style="color:#1a1a1a">get_next(datetime)   </span></code><code><span style="color:#999999"><em># 2010-01-25 04:50:00</em></span></code>
<code><span style="color:#1a1a1a"><strong>>>></strong></span></code><code><span style="color:#1a1a1a"> </span></code><code><span style="color:#1a1a1a"><strong>print</strong></span></code><code><span style="color:#1a1a1a"> </span></code><code><span style="color:#0084ff">iter</span></code><code><span style="color:#1a1a1a"><strong>.</strong></span></code><code><span style="color:#1a1a1a">get_next(datetime)   </span></code><code><span style="color:#999999"><em># 2010-01-25 04:55:00</em></span></code>
<code><span style="color:#1a1a1a"><strong>>>></strong></span></code><code><span style="color:#1a1a1a"> </span></code><code><span style="color:#1a1a1a"><strong>print</strong></span></code><code><span style="color:#1a1a1a"> </span></code><code><span style="color:#0084ff">iter</span></code><code><span style="color:#1a1a1a"><strong>.</strong></span></code><code><span style="color:#1a1a1a">get_next(datetime)   </span></code><code><span style="color:#999999"><em># 2010-01-25 05:00:00</em></span></code>
<code><span style="color:#1a1a1a"><strong>>>></strong></span></code>
<code><span style="color:#1a1a1a"><strong>>>></strong></span></code><code><span style="color:#1a1a1a"> </span></code><code><span style="color:#0084ff">iter</span></code><code><span style="color:#1a1a1a"> </span></code><code><span style="color:#1a1a1a"><strong>=</strong></span></code><code><span style="color:#1a1a1a"> croniter(</span></code><code><span style="color:#f1403c">'2 4 * * mon,fri'</span></code><code><span style="color:#1a1a1a">, base)  </span></code><code><span style="color:#999999"><em># 04:02 on every Monday and Friday</em></span></code>
<code><span style="color:#1a1a1a"><strong>>>></strong></span></code><code><span style="color:#1a1a1a"> </span></code><code><span style="color:#1a1a1a"><strong>print</strong></span></code><code><span style="color:#1a1a1a"> </span></code><code><span style="color:#0084ff">iter</span></code><code><span style="color:#1a1a1a"><strong>.</strong></span></code><code><span style="color:#1a1a1a">get_next(datetime)   </span></code><code><span style="color:#999999"><em># 2010-01-26 04:02:00</em></span></code>
<code><span style="color:#1a1a1a"><strong>>>></strong></span></code><code><span style="color:#1a1a1a"> </span></code><code><span style="color:#1a1a1a"><strong>print</strong></span></code><code><span style="color:#1a1a1a"> </span></code><code><span style="color:#0084ff">iter</span></code><code><span style="color:#1a1a1a"><strong>.</strong></span></code><code><span style="color:#1a1a1a">get_next(datetime)   </span></code><code><span style="color:#999999"><em># 2010-01-30 04:02:00</em></span></code>
<code><span style="color:#1a1a1a"><strong>>>></strong></span></code><code><span style="color:#1a1a1a"> </span></code><code><span style="color:#1a1a1a"><strong>print</strong></span></code><code><span style="color:#1a1a1a"> </span></code><code><span style="color:#0084ff">iter</span></code><code><span style="color:#1a1a1a"><strong>.</strong></span></code><code><span style="color:#1a1a1a">get_next(datetime)   </span></code><code><span style="color:#999999"><em># 2010-02-02 04:02:00</em></span></code>
<code><span style="color:#1a1a1a"><strong>>>></strong></span></code>
<code><span style="color:#1a1a1a"><strong>>>></strong></span></code><code><span style="color:#1a1a1a"> </span></code><code><span style="color:#0084ff">iter</span></code><code><span style="color:#1a1a1a"> </span></code><code><span style="color:#1a1a1a"><strong>=</strong></span></code><code><span style="color:#1a1a1a"> croniter(</span></code><code><span style="color:#f1403c">'2 4 1 * wed'</span></code><code><span style="color:#1a1a1a">, base)  </span></code><code><span style="color:#999999"><em># 04:02 on every Wednesday OR on 1st day of month</em></span></code>
<code><span style="color:#1a1a1a"><strong>>>></strong></span></code><code><span style="color:#1a1a1a"> </span></code><code><span style="color:#1a1a1a"><strong>print</strong></span></code><code><span style="color:#1a1a1a"> </span></code><code><span style="color:#0084ff">iter</span></code><code><span style="color:#1a1a1a"><strong>.</strong></span></code><code><span style="color:#1a1a1a">get_next(datetime)   </span></code><code><span style="color:#999999"><em># 2010-01-27 04:02:00</em></span></code>
<code><span style="color:#1a1a1a"><strong>>>></strong></span></code><code><span style="color:#1a1a1a"> </span></code><code><span style="color:#1a1a1a"><strong>print</strong></span></code><code><span style="color:#1a1a1a"> </span></code><code><span style="color:#0084ff">iter</span></code><code><span style="color:#1a1a1a"><strong>.</strong></span></code><code><span style="color:#1a1a1a">get_next(datetime)   </span></code><code><span style="color:#999999"><em># 2010-02-01 04:02:00</em></span></code>
<code><span style="color:#1a1a1a"><strong>>>></strong></span></code><code><span style="color:#1a1a1a"> </span></code><code><span style="color:#1a1a1a"><strong>print</strong></span></code><code><span style="color:#1a1a1a"> </span></code><code><span style="color:#0084ff">iter</span></code><code><span style="color:#1a1a1a"><strong>.</strong></span></code><code><span style="color:#1a1a1a">get_next(datetime)   </span></code><code><span style="color:#999999"><em># 2010-02-03 04:02:00</em></span></code>
<code><span style="color:#1a1a1a"><strong>>>></strong></span></code>
<code><span style="color:#1a1a1a"><strong>>>></strong></span></code><code><span style="color:#1a1a1a"> </span></code><code><span style="color:#0084ff">iter</span></code><code><span style="color:#1a1a1a"> </span></code><code><span style="color:#1a1a1a"><strong>=</strong></span></code><code><span style="color:#1a1a1a"> croniter(</span></code><code><span style="color:#f1403c">'2 4 1 * wed'</span></code><code><span style="color:#1a1a1a">, base, day_or</span></code><code><span style="color:#1a1a1a"><strong>=</strong></span></code><code><span style="color:#999999">False</span></code><code><span style="color:#1a1a1a">)  </span></code><code><span style="color:#999999"><em># 04:02 on every 1st day of the month if it is a Wednesday</em></span></code>
<code><span style="color:#1a1a1a"><strong>>>></strong></span></code><code><span style="color:#1a1a1a"> </span></code><code><span style="color:#1a1a1a"><strong>print</strong></span></code><code><span style="color:#1a1a1a"> </span></code><code><span style="color:#0084ff">iter</span></code><code><span style="color:#1a1a1a"><strong>.</strong></span></code><code><span style="color:#1a1a1a">get_next(datetime)   </span></code><code><span style="color:#999999"><em># 2010-09-01 04:02:00</em></span></code>
<code><span style="color:#1a1a1a"><strong>>>></strong></span></code><code><span style="color:#1a1a1a"> </span></code><code><span style="color:#1a1a1a"><strong>print</strong></span></code><code><span style="color:#1a1a1a"> </span></code><code><span style="color:#0084ff">iter</span></code><code><span style="color:#1a1a1a"><strong>.</strong></span></code><code><span style="color:#1a1a1a">get_next(datetime)   </span></code><code><span style="color:#999999"><em># 2010-12-01 04:02:00</em></span></code>
<code><span style="color:#1a1a1a"><strong>>>></strong></span></code><code><span style="color:#1a1a1a"> </span></code><code><span style="color:#1a1a1a"><strong>print</strong></span></code><code><span style="color:#1a1a1a"> </span></code><code><span style="color:#0084ff">iter</span></code><code><span style="color:#1a1a1a"><strong>.</strong></span></code><code><span style="color:#1a1a1a">get_next(datetime)   </span></code><code><span style="color:#999999"><em># 2011-06-01 04:02:00</em></span></code></span>

当然,还有一个get_prev方法,获取上一次执行的时间,用法和get_next()一样。

4.2 创建定时任务

Mistral支持cloud cron功能,即创建定时任务,其定义语法和linux crontab基本一致,前面已经介绍过。

mistral还支持定义开始执行时间、执行次数等:

<span style="color:#00000a"><code><span style="color:#1a1a1a">int32bit $ mistral cron-trigger-create --pattern '* * * * *' --count 5 test-hello-world hello-world</span></code>
<code><span style="color:#1a1a1a">+----------------------+--------------------------------------+</span></code>
<code><span style="color:#1a1a1a">| Field                | Value                                |</span></code>
<code><span style="color:#1a1a1a">+----------------------+--------------------------------------+</span></code>
<code><span style="color:#1a1a1a">| ID                   | a3a0ed3f-a5ef-4416-af9f-33cef498bbb6 |</span></code>
<code><span style="color:#1a1a1a">| Name                 | test-hello-world                     |</span></code>
<code><span style="color:#1a1a1a">| Workflow             | hello-world                          |</span></code>
<code><span style="color:#1a1a1a">| Params               | {}                                   |</span></code>
<code><span style="color:#1a1a1a">| Pattern              | * * * * *                            |</span></code>
<code><span style="color:#1a1a1a">| Next execution time  | 2017-08-28 02:36:00                  |</span></code>
<code><span style="color:#1a1a1a">| Remaining executions | 1                                    |</span></code>
<code><span style="color:#1a1a1a">| Status               | READY                                |</span></code>
<code><span style="color:#1a1a1a">| Created at           | 2017-08-28 02:35:08                  |</span></code>
<code><span style="color:#1a1a1a">| Updated at           | None                                 |</span></code>
<code><span style="color:#1a1a1a">+----------------------+--------------------------------------+</span></code></span>

以上任务会每分钟执行一次,执行5次后结束。

查看cron任务列表:

<span style="color:#00000a"><code><span style="color:#1a1a1a">int32bit $ mistral cron-trigger-list</span></code>
<code><span style="color:#1a1a1a">+--------------------------------------+------------------+-------------+--------+-------------+---------------------+----------------------+-----------+---------------------+---------------------+</span></code>
<code><span style="color:#1a1a1a">| ID                                   | Name             | Workflow    | Params | Pattern     | Next execution time | Remaining executions | Status    | Created at          | Updated at          |</span></code>
<code><span style="color:#1a1a1a">+--------------------------------------+------------------+-------------+--------+-------------+---------------------+----------------------+-----------+---------------------+---------------------+</span></code>
<code><span style="color:#1a1a1a">| 88fd87ba-2429-4995-abba-54bfff91ba13 | int32bit-test-1  | hello-world | {}     | */1 * * * * | 2017-08-17 08:47:00 |                    0 | COMPLETED | 2017-08-17 08:41:49 | 2017-08-17 08:46:58 |</span></code>
<code><span style="color:#1a1a1a">| a3a0ed3f-a5ef-4416-af9f-33cef498bbb6 | test-hello-world | hello-world | {}     | * * * * *   | 2017-08-28 02:36:00 |                    4 | READY     | 2017-08-28 02:35:08 | None                |</span></code>
<code><span style="color:#1a1a1a">+--------------------------------------+------------------+-------------+--------+-------------+---------------------+----------------------+-----------+---------------------+---------------------+</span></code></span>

通过execution-list查看执行结果,其中cron id为关联的cron任务:

<span style="color:#00000a"><code><span style="color:#1a1a1a">int32bit $ mistral execution-list</span></code>
<code><span style="color:#1a1a1a">+--------------------------------------+--------------------------------------+---------------+--------------------------------------+-------------------+---------+------------+---------------------+---------------------+</span></code>
<code><span style="color:#1a1a1a">| ID                                   | Workflow ID                          | Workflow name | Cron ID                              | Task Execution ID | State   | State info | Created at          | Updated at          |</span></code>
<code><span style="color:#1a1a1a">+--------------------------------------+--------------------------------------+---------------+--------------------------------------+-------------------+---------+------------+---------------------+---------------------+</span></code>
<code><span style="color:#1a1a1a">| fe8d752e-8a96-45d3-a13c-0fdca58951cc | 86c581b9-e08d-46a0-ad0d-cf3f1d30bf4d | hello-world   | None                                 | <none>            | SUCCESS | None       | 2017-08-28 02:32:23 | 2017-08-28 02:32:24 |</span></code>
<code><span style="color:#1a1a1a">| 039af1e2-177e-4905-9c29-ccfbcdfedbff | 86c581b9-e08d-46a0-ad0d-cf3f1d30bf4d | hello-world   | a3a0ed3f-a5ef-4416-af9f-33cef498bbb6 | <none>            | SUCCESS | None       | 2017-08-28 02:35:58 | 2017-08-28 02:35:59 |</span></code>
<code><span style="color:#1a1a1a">+--------------------------------------+--------------------------------------+---------------+--------------------------------------+-------------------+---------+---------</span></code></span>

注意:

  • 社区版本定时任务没有State字段,执行完后会自动删除,因此不会记录已经完成的定时任务。

  • 社区版本的execution没有cron id关联。

5 社区最新进展

OpenStack在8月30日发布了最新版本Pike,其中比较重要的几个新特性如下:

  • action支持多region了,用户可以通过action_region参数指定region。

  • workflow可以指定namespace,不同的namespace可以使用相同的名字。

  • 支持使用 <% execution().created_at %>获取workflow的执行时间。

  • mistral-engine可以配置为local模式,action直接在本地执行而不需要通过RPC发给executor执行。

参考文档

  • https://docs.openstack.org/developer/mistral/

  • https://en.wikipedia.org/wiki/Cron

以上内容转裁自:

https://zhuanlan.zhihu.com/p/29078865

Mistral : 1 Mistral基础相关推荐

  1. Mistral : 2 Mistral表结构分析

    目标: 1 弄清楚如何操作postgresql 2 弄清楚mistral中表结构 1 postgresql 1.1 登录 psql 报错: psql: FATAL:  role "root& ...

  2. OpenStack工作流服务Mistral简介

    1 Mistral背景 Mistral是一个OpenStack生态圈中比较新的项目,该项目的目标是: The project is to provide capability to define, e ...

  3. stackstorm 14.编写stackstorm的mistral

    1 mistral动作流基础 因为workflows也是属于action,因此workflows在actions下面 因为mistral工作流比较特殊,它和之前执行的actions中对应的shell脚 ...

  4. openstack环境中安装mistral

    确认keystone版本是v3,必须是v3: . admin-openrc.sh openstack endpoint list |grep keystone 确认git客户端是否安装,如果没有先安装 ...

  5. CentOS OpenStack Pike tacker 之 mistral 安装实录

    格式有点乱有空再整理 一.安装mistral组件(官网手册为Ubuntu版,操作有点坑) "For information on how to install and configure t ...

  6. stackstorm 6. 工作流之Mistral

    1 Mistral Mistral是一个用于管理和执行动作流的Openstack项目.Mistral是可以作为一个单独的 mistral服务在StackStorm中安装.一个Mistral工作流可以通 ...

  7. openstack mistral的安装

    说明:本文基于使用rdo安装的allinone的opentack的Pika版本进行安装 mistral Mistral提供Workflow As a Service.典型的应用场景包括任务计划服务Cl ...

  8. OpenStack Pike 版本的 Mistral 安装

    OpenStack Pike 版本的  Mistral 安装部署 # 安装环境使用的centos 7.3 1. 安装 Mistral 安装包. # yum -y install openstack-m ...

  9. 【Mistral】 workflow实例一, yaml文件里的变量定义,action调用等

    以下实例引用自https://github.com/openstack/mistral-extra/blob/master/examples/v2/calculator/calculator.yaml ...

最新文章

  1. JavaScript - 数据类型和变量
  2. 进程间通信————有名管道
  3. PHP 从结果集中取得一行作为关联数组:
  4. Linux如何查看所有用户和用户组信息(cat groups whoami)
  5. python unittest断言_python unittest之断言及示例
  6. 之江天枢正式开源!一文详解天枢核心优势
  7. 将文本文件内容存储在DataSet中的方法总结
  8. 【毕设】JAVA+SQL办公自动化系统(源代码+论文+外文翻译)
  9. android自定义view案例,Android自定义View,你摸的透透的了?
  10. WCDMA功率控制与BER/BLER
  11. Excel键盘快捷键大全(二)
  12. GEE:LandTrendr时间序列曲线拟合
  13. JZOJ5401. 【NOIP2017提高A组模拟10.8】Star Way To Heaven
  14. ブリアー / 三星枪
  15. UHS-II文档学习
  16. 定时任务的10种写法,长见识了
  17. Luogu_P4140 奇数国
  18. 如何快速把Excel数据导入SQL数据库
  19. C++模板类的运算符重载
  20. 计算机排序操作步骤,win7电脑更改磁盘卷标排列顺序的操作步骤-电脑自学网

热门文章

  1. 为wp博客添加html网页,WordPress博客添加B站追番页面
  2. 直播流媒体服务器 srs介绍 1
  3. 阿里巴巴往届笔试面试题大全
  4. 小程序想要跳转外部第三方链接的配置方法
  5. 关于多端能力服务统一,我有话要说...
  6. 深蓝学院motion planning作业的一些问题
  7. java实现文件同步_Java负载均衡服务器实现上传文件同步
  8. Opencv For Unity2.3.4 所有场景预览
  9. JS中export怎么用
  10. htc g11 hboot 2.0官方解锁后刷第三方rom