dynflow是一个工作流引擎,foreman-katello中主要就是通过它来实现的工作流编排工作
特性概览
- 跟踪运行进程的进度
- 异步运行代码
- 当出错时返回任务,在需要的时候跳过一些任务
- 检测独立的部分并且同步运行
- 可以将简单的操作组合为复杂操作
- 从第三方库扩展工作流
- 在本地数据库事务和外部事务之间保持一致性
- 挂起长时间运行的步骤,不阻塞线程池
- 在可能的时候可以取消步骤
- 通过中间件来扩展操作
- 采用不同的适配器:存储引擎后端、事务,或者执行器
Dynflow是开发来以支持katello和foreman中的服务编排的
词汇表
- Action - 执行计划的构建块,是一个继承自
Dynflow::Action
的ruby类,定义每个阶段需要运行的代码 - Phase - 每个动作都有三个阶段:
plan
,run
,finalize
- Input - 一个传入到操作中的组成数据的hash
- Output - 一个由操作产生的数据的hash.它是持久化的而且可以传递给其他操作
- Execution plan - 工作流定义:
- Trigger and action - 进入计划阶段,以操作的plan开始,执行会立即开始
- Flow - run/finalize阶段的定义,保存有关可以并行/顺序执行的的步骤的信息
- Executor - 运行 基于执行计划(execution plan)的run和finalize方法的服务,它可以在相同的进程中作为计划阶段运行 或者 在不同的进程中(使用远程执行器)
- world - Dynflow运行代码的宇宙/空间?:它保存了所有需要的配置信息。通常每个dynflow进程中只有一个world,除此之外,它还保存了Persistence,Logger,Executor以及所有其他执行操作所需要的东西。这一概念允许我们避免共享状态。而且,world可以相互对话,这对于生产和高可用设置很有用,在多台主机上拥有多个world来处理执行计划。如果你仍然很疑惑并且是来自RoR世界(指是一名Rails开发?),就把它想象成是Ruby on Rails上的对象
例子
在examples目录下有action中的代码示例。运行这些文件(除了example_helper.rb以外)会让dynflow运行时进行初始化(包含可以探索特性和实验的web控制台)
如何使用
1.创建世界world
2.开发 vs 生产
开发环境中,执行运行在同一个进程中,而在生产环境中,会有一个执行器进程的概念
3.Action解析
[图片上传失败...(image-f9a24-1638328748521)]
当action被触发的时候,Dynflow在这个操作上执行plan方法,这代表着执行计划。它通过调用plan_action以及plan_self方法来构建执行计划,有效地的列出此执行计划应该运行的操作。或者说,它编译了一组 run方法将会调用的操作。而且它也代表着给了这些操作一个执行顺序。比如如下代码:
# 需要删除作为input数组传入进来的文件数组
def plan(files)
files.each do |filename|
plan_action MyActions::File::Destroy, filename
end
end
需要注意的是,在计划要运行的代码中不必只是调用其他action而已。事实上在plan中计划它自身也是很普遍的,这意味着它会把它自己的run方法调用放在执行计划阶段。为了实现这种效果你可以使用plan_self。这可以在MyActions::File::Destroy
中用到
class MyActions::File::Destroy < Dynflow::Action
def plan(filename)
plan_self path: filename
end
def run
File.rm(input.fetch(:path))
end
end
在上面的例子中,plan_self看上去只是plan_action MyActions::File::Destroy, filename
的一个缩写而已。需要注意地是plan_action总是会触发指定action的plan方法,而plan_self只是触发run和finalize方法,所以通过plan_action方法,我们可以实现无限循环。
同样需要注意地是,run方法不接受任何输入参数。它是通过input方法来引用参数的,其中的参数是我们在调用plan_self方法时传递的,input方法返回的是一个hash
与Input类似,run方法还会产生output。在run方法执行完成后,finalize方法可以调用。操作可以使用其他操作的的输出output作为它们的输入的一部分来建立依赖。操作的状态在每个阶段会被序列化。
从前面的接触中可以看到,大致有3个阶段:plan,run,finalize。计划阶段由trigger操作触发启动。run和finalize方法只有当你在plan方法中使用plan_self方法时才会调用。
3.1 Input和Output
输入和输出都是可由Action#input和Action#output方法访问的散列。他们需要序列化为JSON,所以它应该只包含基本Ruby类型的组合,如:哈希,数组,字符串,整数等。
注意
应该避免直接赋值
self.output = {key: value}
这样可能会删除其他数据,因此最好使用
output.update(key: data, another_key: another_data) # or for one key output[:key] = data
3.2 Triggering
触发操作表示启动plan阶段,紧接着就会立即执行。任何操作都是由如下操作触发的:
world_instance.trigger(AnAction, *args)
注意:
Foreman和Katello中的操作通常是由
ForemanTask.sync_task
和ForemanTask.async_task
触发的所以如下的部分并没有那么重要,如果你是使用ForemanTasks的话。
World#trigger方法返回TriggerResult类型的对象。它是使用Algebrick变体类型定义的
3.3 Scheduling任务调度
排程指的就是可以让任务在未来某个指定时间执行。任何操作都可以通过如下调用方式来延迟调用:
world_instance.delay(AnAction,
{ start_at: Time.now + 360, start_before: Time.now + 400 },
*args)
这一段脚本将会延迟AnAction的调用并且最终会在start_at到start_before之间运行。设置start_before为nil将会不带超时限制的延迟执行该任务。
当操作被延迟了,一个执行计划对象会被创建为状态为scheduled,但是它仍然不会运行执行计划,计划会在start_at时间到时执行。如果计划没有按时执行(例如,在start_before之后),执行计划会被标记为failed(它的状态被设置为stopped并且结果为error)
在这种情况下,参数必须被保存下来(以供之后计划启动时调用),所以必须有一种机制能够安全的序列化和反序列化它们以让它们能够存到数据库中。这是由serializer处理的。可以为每个操作单独设置serializer通过覆盖它的delay方法。
延迟计划的计划执行是由DelayedExecutor处理的,一个间歇性检查延迟的执行计划并执行它们的对象。已排程的计划不会自行执行任何操作,它们仅仅是等待被拾取并且被DelayedExecutor计划。这表示如果没有DelayedExecutor存在,它们的计划将会被推迟直到有调度器生成
3.4 计划阶段
计划总是使用线程来触发的action。计划阶段为运行阶段配置action的input。它通过执行action实例的plan方法来启动并传递来自World#trigger方法的参数
# 先调用trigger方法触发
world_instance.trigger(AnAction, *args)
# 之后会执行
an_action.plan(*args) # an_action是AnAction 的实例
计划方法继承自Dynflow::Action
并且默认情况下只要run方法存在的话它就会调用plan_self使用第一个参数作为输入参数。它也会调用fianlize方法只要它存在。
class AnAction < Dynflow::Action
def run
output.update self.input
end
end
world_instance.trigger AnAction, data: 'nothing'
上面的例子中,将会计划它自身将输入复制到输出中。
在大多数情况下plan方法是被覆盖来使用变形的参数计划它自身或者计划其他action。在Rails应用中,计划阶段的参数经常是ActiveRecord对象,将其用作action的输入参数。
让我们先看看参数变换:
class AnAction < Dynflow::Action
def plan(any_array)
# pick just numbers
plan_self numbers: any_array.select { |v| v.is_a? Number }
end
def run
# compute sum - simulating a time consuming operation
output.update sum: input[:numbers].reduce(&:+)
end
end
注意:
为执行job的action的输入提供足够的数据是一个很好的实践。这表示不用太多(比如直接传递ActiveRecord的attributes),因为这或许会产生性能影响而且会造成问题,当改变attributes的时候。
另一方面,输入里应该包含足够的数据来执行job,而无需去访问外部资源。因此,不要传递一个AR id进来然后再在run方法中重新查询这条记录,我们仅仅需要使用其中的一部分属性,最好是直接使用这些属性作为action的输入。
从实际和性能方面的考虑的话,遵从如下规则就是最好的实践
下面是一个action的例子:
class SumNumbers < Dynflow::Action
def plan(numbers)
plan_self numbers: numbers
end
def run
output.update sum: input[:numbers].reduce(&:+)
end
end
class SumManyNumbers < Dynflow::Action
def plan(numbers)
# references to planned actions
planned_sub_sum_actions = numbers.each_slice(10).map do |numbers|
plan_action SumNumbers, numbers
end
# prepare array of output references where each points to sum in the
# output of particular action
sub_sums = planned_sub_sum_actions.map do |action|
action.output[:sum]
end
# plan one last action which will sum the sub_sums
# it depends on all planned_sub_sum_actions because it uses theirs outputs
plan_action SumNumbers, sub_sums
end
end
world_instance.trigger SumManyNumbers, (1..100).to_a
上面的例子将会按照10个一组来并行计算数值。在所有子计算完成后一个最终数值再累加所有的子累加
注意
这个例子只是用于描绘planning的能力。在现实中这种计算密集的任务在MRI上运行的并不好。工作池可能会消耗完。但是这也问题不大,因为Dynflow主要是用于编排外部资源的(所以性能是否够好,不是太影响,主要用于编排)
Action在计划阶段获取会访问本地数据库,请看 数据库与事务
部分。
3.5 运行阶段
run方法实现了该Action主要实现的功能。在这个阶段输入是不可变的。这是所有可能失败的步骤的正确位置。Action的运行阶段允许有一些副作用比如:文件操作、调用其他系统、等等。本地数据库在这个阶段不应该被访问,请看 数据库与事务
。
3.6 最终阶段
finalize阶段的主要目的是在action成功结束后能够访问本地数据库,比如:基于新数据建立索引,更新记录作为完全创建,等等。最终阶段不会修改输入和输出。Action可以在最终阶段访问本地数据库并且必须确保幂等性,请看 数据库与事务
4. 依赖
正如前面提到,action可以使用其他action的输出作为输入。当这样做时,Dynflow会自动检测它们之间的依赖关系并构建相应的执行计划。
def plan
first_action = plan_action AnAction
second_action = plan_action AnAction, first_action.output[:a_key_in_output]
end
第二个Action用到了第一个Action的输出,所以第二个Action依赖于第一个Action。
再比如:
def plan
first_action = plan_action AnAction
second_action = plan_action AnAction
end
这种情况表示两个Action是相互独立的,那么它们将会并行地执行。
也有其他方式可以描述Action之间的依赖关系。Dynflow用户可以通过sequence和concurrence。这两个方法都将带上一个块来调用
5.数据库和事务
Dynflow被设计用于服务编排。通常按照如下流程来执行,我们使用一个user来作为示例。
- 触发User Creation,参数是一个未保存的AR user对象
- 计划: user存储到本地数据库(在Dynflow主机应用中)。记录标记为
未完成
- 运行:使用REST调用的所有user需要的外部调用。这个阶段会在所有外部调用成功之后结束
- 最终:在数据库中的记录标记为
已完成
准备好被使用。
在plan和finalize之间是有事务的(所有的action的plan方法是在一个事务中的)。如果在plan阶段发生了任何错误的话,任何在planning阶段对本地数据库的改变都会撤回。finalize阶段也一样,如果出现任何错误,所有变更都会撤回。因此所有finalize方法都必须保证幂等性。
Dynflow内部使用Sequel作为ORM,但是用户也可以选择他们所需要的来访问数据。有个接口TransactionAdapters::Abstract,它的实现可以提供不同ORM的事务。最常见的应该就是TransactionAdapters::ActiveRecord了
module Demo
class Action1 < Dynflow::Action
input_format do
param :id, Integer
end
def plan(id)
Dynflow::TransactionAdapters::ActiveRecord.new.transaction do
puts "pppppppppppppppppppppppppppp Action1"
User.create(name: 'eddy2')
# plan_self(id: id)
plan_action Action2
super
end
end
def run
puts "rrrrrrrrrrrrrrrrrrrrrrrrrrrr Action1,input: #{input}"
end
def finalize
puts "ffffffffffffffffffffffffffff Action1"
end
end
class Action2 < Dynflow::Action
def plan
#在Action2中报错,Action1中的数据变更也会撤回
raise 'wrong22!'
puts "pppppppppppppppppppppppppppp Action2"
plan_self(id: id)
end
def run
puts "rrrrrrrrrrrrrrrrrrrrrrrrrrrr Action2"
end
end
end
6.组合
使用Dynflow可以很容易地将Action组合在一起。典型的例子是一个Action将其他小型的Action组合在一起,而真正的实现逻辑都在小型的Action中:
class CreateInfrastructure < Dynflow::Action
def plan
sequence do
concurrence do
plan_action(CreateMachine, 'host1', 'db')
plan_action(CreateMachine, 'host2', 'storage')
end
plan_action(CreateMachine,
'host3',
'web_server',
:db_machine => 'host1',
:storage_machine => 'host2')
end
end
end
这种情况下,CreateInfrastructure不带run方法,它的目的仅仅是用于编排其他Action的运行顺序。
7.订阅
使用组合的方式可以帮助我们很好地拆解业务逻辑为小型的代码,但是它并不直接支持用插件来扩展。所以就有了订阅。
Action可以从插件、gem来订阅,或者任何其他早已加载了的Action,这样子它们可以自己扩展计划进程。
让我们看一个例子:
# This action can be extended without doing any
# other steps to support it.
class ACoreAppAction < Dynflow::Action
def plan(arguments)
plan_self(args: arguments)
plan_action(AnotherCoreAppAction, arguments.first)
end
def run
puts "Running core action: #{input[:args]}"
self.output.update success: true
end
end
下面是定义在插件中的另一个Action,它订阅了上述Action。
class APluginAction < Dynflow::Action
# plan this action whenever ACoreAppAction action is planned
def self.subscribe
ACoreAppAction
end
def plan(arguments)
# arguments are same as in ACoreAppAction#plan
plan_self(args: arguments)
end
def run
puts "Running plugin action: #{input[:args]}"
end
end
8.挂起
有时任务代表采用不同服务的任务(比如pulp的同步任务)。Dynflow会尽量不浪费计算机资源,所以它提供了工具来释放线程以执行其他Action,当等待外部任务的时候。
Dynflow允许actions挂起并且基于外部事件来唤醒。让我们来模拟一下外部服务:
class AnExternalService
def start_synchronization(report_to)
Thread.new do
sleep 1
report_to << :done
end
end
end
AnExternalService
可以调用start_synchronization这个方法并且它会稍后就会返回到report_to参数所代表的Action。它发送了事件 :done
class AnAction < Dynflow::Action
EXTERNAL_SERVICE = AnExternalService.new
def plan
plan_self
end
def run(event)
case event
when nil # first run
suspend do |suspended_action|
EXTERNAL_SERVICE.start_synchronization suspended_action
end
when :done # external task is done
output.update success: true
# let the run phase finish normally
else
raise 'unknown event'
end
end
end
运行流程如下:
- AnAction触发
- 开始执行计划
- 运行阶段开始
- run方法被调用但是没有事件(nil)
- 与案例分支匹配,启动外部同步
- Action启动同步
- Action被挂起,run方法的运行立即结束在suspend被调用后,它的块参数就是会在挂起之后立即计算
- Action会保存在内存中以用于之后唤醒,当收到事件的时候,但是它并不会阻塞任何线程
- Action收到done事件
- run方法再次执行
- 输出被更新为success: true
- 由于没有finalize阶段,action结束
这种事件机制非常复杂,它可以用于构建轮询Action。
9.轮询
要支持轮询的话,Dynflow中有一个模块可以实现。只需要引入它
在action中还需实现3个方法:
- done? - 通过外部数据判断任务已经完成
- invoke_external_task - 调用外部任务
- poll_external_task - 轮询外部任务状态并返回一个状态(JSON序列化数据比如:Hash,Array,String, etc.)将其存储在output中
def done?
external_task[:progress] == 1
end
def invoke_external_task
triger_the_task_with_rest_call
end
def poll_external_task
data = poll_data_with_rest_call
progress = calculate_progress data # => a float in 0..1
{ progress: progress
data: data }
end
end
Action在运行阶段会做如下事情:
- 在第一次运行时执行 invoke_external_task
- 挂起然后间歇性:
- 唤醒任务
- 调用poll_external_task轮询
- 调用done?检查是否完成
- true -> 完成run阶段
- false -> 进行下一次轮询
还有2个方法可以用于处理外部任务的数据,它可以手动覆盖:
* external_task - 读取外部任务存储的数据,默认情况下读取self.output[:task]
* external_task= - 写入外部任务存储的数据,默认情况下会写入self.output[:task] = value
还有一些其他已实现的特性:
* 轮询间隔逐渐延长
* 轮询失败时进行重试
10.状态
每个Action阶段都可以是以下几种状态中的一种:
- Pending - 还未执行
- Running - 其中一个Action阶段正在执行
- Success - Action阶段执行成功
- Error - 执行中出错
- Suspended - 只会发生在run阶段,Action正在休眠等待唤醒
- Skipped - 失败的Action可以被标记为已跳过
- Skipping - Action被标记为需要跳过,但是执行计划目前还没执行完成,等执行计划完成后就会被标记为跳过
执行计划有如下状态:
* Pending - 计划还未执行
* Scheduled - 已为延迟执行安排好调度
* Planning - 正在执行计划
* Planned - 已计划,run阶段还未开始
* Running - 正在运行run和finalize阶段
* Paused - 已暂停。只会在执行失败或者重新开始时发生
* Stopped - 执行计划已经完成
执行计划还有以下结果:
* Success - 在没有错误或跳过的情况下完成
* Warning - 有跳过的步骤
* Error - 一个或多个Action失败
* Pending - 执行仍然在运行
TODO: 如何访问到这些状态呢?
11.错误处理
plan阶段和run,finalize阶段的报错有个不同:
- plan阶段的报错,在调用trigger方法后就会直接报错,而run,finalize不会,这是因为run,finalize方法是在执行器中去执行的,而不是当前触发plan的这个线程了
如果在运行阶段发生错误,执行就会暂停。你可以在控制台查看错误。然后我们可以手动来修复问题,之后再重试该任务。在修复问题期间,我们可以先跳过该任务,修复完成后再继续执行跳过的任务。
如果在finalize阶段有错误的话,整个finalize阶段都会撤回并且可以在问题被修复时继续
TODO rescue策略
12.控制台
TODO 如何访问控制台
13.测试
TODO
14.长时间运行的actions
Dynflow作为一个任务编排工具,并没有去考虑多cpu计算(或者说多核计算)。即便有多个执行器,执行计划也只会在一个执行器上执行,所以不用JRuby的话,它并不能扩展。
长时间运行的action还有一个问题就是会阻塞worker。执行器只有一个被限制的worker池,如果更多的worker忙碌的话,会导致性能下降。
长时间运行的阻塞的action也是有问题的。
解决方案:
* 使用Action挂起 - 在条件满足时挂起Action,释放Worker
* 卸载计算 - 也就是将重cpu的部分卸载到不同的服务中
15.多队列
默认情况下,用的是单队列单worker的方式来运行所有Action。这会导致一些高优先级的action被阻塞。
要定位问题的话,可以定义额外的队列将其绑定到额外的worker池。
要使用队列,需要在定义world的时候定义额外的队列:
config = Dynflow::Config.new
config.queues.add(:slow, :pool_size => 5)
world = Dynflow::World.new(config)
使用队列的action只需要覆盖queue方法即可,比如:
class MyAction < Dynflow::Action
def queue
:slow
end
def run
sleep 60
end
end
16.中间件
每个Action都有一条中间件链,其包装了Action执行的各个阶段。它与Rack中间件很相似。要从Dynflow::Middleware继承创建新的中间件。有5个方法可以被覆盖:plan,run,finalize,plan_phase,finalize_phase。
def plan(*args)
pass *args
end
当覆盖时可以在pass方法之前或之后插入代码,这会执行链中的下一个中间件或者它自身(当其是链中最后一个中间件时)。大多数情况下pass总是会在覆盖的方法中调用的。
每个Action都有一个中间件链。中间件可以通过在Action类中使用use方法来添加。
class AnAction < Dynflow::Action
use AMiddleware, after: AnotherMiddleware
end
use方法有3个选项:
- before - 让其在指定中间件之后
- after - 让其在指定中间件之前
- replace - 让其替换指定中间件
17.子计划
要使用子计划的话,就需要引入Dynflow::Action::WithSubPlans
模块,并且覆盖create_sub_plans
方法。在create_sub_plans
方法内部,使用trigger方法来创建子任务来以不特定的顺序执行。父任务将会在不阻塞线程的情况下等待子任务结束。
class MyAction < Actions::EntryAction
include Dynflow::Action::WithSubPlans
...
def create_sub_plans
[
trigger(Actions::OtherAction, action_param1, action_opts),
trigger(Actions::AnotherAction)
]
end
end
18.执行计划钩子
Dynflow允许Action在执行计划生命周期中建立钩子。要使用钩子,必须在Action中定一个方法并将它注册为钩子。当前每个执行计划的状态都有钩子事件,当执行计划转换到状态时,钩子事件被执行。此外还有两个钩子failure success 这是在计划变更为stopped时(以error或者success结束)时调用的。
方法可以通过execution_plan_hooks.use
注册为钩子,再提供一个选项:on => HOOK_EVENT
作为参数,HOOK_EVENT
可以时钩子事件中的一个。万一选项没提供的话,方法就会在每次状态变更时都进行执行。类似地,以继承威力,钩子可以通过execution_plan_hooks.do_not_use
来禁用,选项也是一样的。在Action上定义的钩子会被子类所继承。
钩子会在执行计划下的每个Action中执行,但是执行顺序是无法保证的。
class MyAction < Actions::EntryAction
# Sets up a hook to call #state_change method when the execution plan changes
# its state
execution_plan_hooks.use :state_change
# Sets up a hook to call #success_notification method when the execution plan
# finishes successfully,
execution_plan_hooks.use :success_notification, :on => :success
# Disables running #state_change method when the execution plan starts or
# finishes planning
execution_plan_hooks.do_not_use :state_change, :on => [:planning, :planned]
def state_change(_execution_plan)
# Do something on every state change
end
def success_notification(_execution_plan)
# Display a notification
end
end
工作原理
1.TODO Action States
- 通用阶段和目前的阶段
- 如何启动执行计划
2.world解析
world代表的是dynflow的运行时而且它是作为一个外部服务运作的。它保存了所有的配置信息以及Dynflow需要用来执行任务的子组件。
Dynflow world由以下几个子组件组成:
- persistence持久化 - 提供持久化能力
- coordinator - 提供world之间的协调能力
- connector - 提供world之间的消息传递的能力
- executor - 它是world运行执行计划的运行时本身。并非所有的worlds都必须有执行器存在(也可能存在纯客户端worlds: 在生产环境下很有用?)
- client dispatcher客户端分发器 - 负责客户端请求和其他world之间的交互通信
- executor dispatcher执行器分发器 - 负责从其他worlds获取请求以及发送响应
- delayed executor - 负责计划以及调度任务的执行
底层技术隐藏在适配器抽象后面,允许为作业选择合适的技术,同时保持其余的DYNFLOW完整
3.client world vs. executor world
最简单的例子是,world同时处理客户端请求和执行器本身。这对于多合一(单机?)部署和开发环境很有用
在生产环境中,你也许希望分离开客户端world(通常是运行webserver的进程中或者客户端库中)和executor world(作为独立的服务运行)。这样的设定让执行可以变得更加的稳定和可用。
在Dynflow的架构中可能会出现多个客户端以及多个executor worlds
executor world有它自己的客户端分发器,所以它可以作为客户端触发其他执行计划(对于子计划很有用)
4.单数据库模型
Dynflow可以从持久层、协调层(实时同步)以及连接器方面识别到潜在的技术。
然后,我们意识到到对于大多数使用场景来说,一个单共享数据库以及足以满足用户执行作业的需求了。墙纸使用不同的技术意味着无用的开销。
因此,完全有可能使用但数据库来完全所有的工作。
注意:
一些简易的数据库比如sqlite已经足够让Dynflow启动并运行且完成工作了。最好的是,推荐使用PostgreSQL,因为Dynflow可以监听特性以有更好的响应时间。
5.world内部通信
客户端准备执行计划,将其保存到持久层让后将其id传递给客户端分发器
客户端分期创建一个IVar其代表其未来执行完成时的值。客户端可以使用这个值来等待执行计划完成或者创建到其结束的时候
客户端分发器创建一个信件,包含执行的请求,携带上receiver id 设置为AnyExecutor
连接器使用调度算法来选择将请求发送给哪个执行器并且替换AnyExecutor为它。将信件发送给指定的执行器
执行器端的连接器接收到信件并且询问执行器分发器来处理它
-
执行器分发器在执行计划上获取锁并且初始化一个执行。同时,让客户端知道作业已经被接收(这里在客户端要处理可能会有额外的逻辑,比如超时)
7-9. 执行器层将Accepted响应返回给客户端
10 执行器完成执行
11-12 执行器层发送finished响应
13 客户端分发器处理原始的IVar,这样客户端就可以找到完成的执行了。
这些行为在单world场景下也是相同的:在这种情况下world同时参与客户端和执行器端,通过内存内的连接器来直连
6. 持久层
持久性确保执行计划的序列化状态持续用于恢复和状态跟踪。执行计划数据存储在其中,实际状态。
与协调器不同,所有持久的数据都不必同时为所有世界提供:每个世界都需要它自身工作的数据。此外,所有数据不必在世界之间完全同步(只要世界上可用于世界上有关相关执行计划的最新数据)。
7.连接器
提供worlds之间的消息出啊等你。消息有如下结构的信件格式:
- sender_id - 产生信件的world的id
- receiverd id - 应该接收该信件的world的id,或者AnyExecutor(在有负载均衡的情况下)
- request id - 客户端唯一的id,用于比对请求-响应
- message - 消息体:连接器并不关心这个,因为它已经序列化了
8.协调器
这个组件(在多执行器设置下尤其重要):确保不会有2个执行器同时执行一个相同的执行计划(别名 锁)。也提供了系统中可用的world的信息(world注册)。不像持久层,它并不需要存储数据(可能重新计算),但是它需要提供一个全局化的共享状态。
协调器适用的主要类型的对象是一个由以下组成的记录:
- type - 记录类型
- id - 记录id(同类型下唯一)
- data - 基于type的任意格式的数据
有一个特殊类型记录叫做lock,它保持着属主信息。它被用来保存 哪个执行器是正在执行哪个执行计划的信息:执行器不允许启动执行,除非它成功获得了属于它的lock锁。