状态机
1 项目概述
1.1 背景介绍及目标
1.1.1 背景
状态机工作流 -- State Machine(强调的是状态的改变)
State Machine 工作流则一般用于 方向不明确的事情当中,一件事完成 (state activity) 面临着很多不同的选择,并且,这个选择要依赖于人的决定(事件驱动)。 State machine 往往是可逆的。一个状态 (State Activity) 有可能要被多次的执行。 比如在线购物便可以看成一个 state machine workflow. 工作流的进行往往有赖于外部事件(比如人)的推动。 再有,如看电视,可以开电视,可以关电视,可以随便转台,也可以专注看某个节目,状态与状态间并没有一个必然的顺序关系。 如此,我们便可以使用 state machine workflow.
1.1.2 场景
机器的生命周期管理(外部机器,初始化中,备机,线上运行中,故障,维修,下架)
实例的生命周期管理
审批流
实例生命周期
例子:
> 实例的部署状态
+-------------------add-----------------------+ +-recover-+
| V V |
none—>allocated—>deployed—>installed—>ready->running---->dead---->removed
| ^ | ^
+--kill--+ +--delete--+
备注:这里的 dead 状态指与上下游实例已解除关系,非停止实例
(1) 触发 event(unit delete)
(2) 状态机 State1(dead) 转变为 State2(removed)
(3) 触发相关 action(下发 task 将实例删除)
> 实例的运行状态(比如 redis)
状态有 pfail,fail 等
> 容器的运行状态
* created
* running
* stopped
* paused
* deleted
机器生命周期管理
机器管理
(1) 触发 event(对处于故障中的某台机器触发维修)
(2) 状态机 State1(故障中) 转变为 State2(维修中)
(3) 触发相关 action(重装机器 task)
| resource_pool
|
+---------+ | +--------------------------------------------------+
|m_outside| <--+---| OFFLINE |
+---------+ | +--------------------------------------------------+
| ^ ^
| |(offline) | (offline)
| | | (backup)
+---------+ | | +-----+ (init) +------+ <---------- +-----+
|m_outside| ---------|--------->| NEW |---------------------->|INITED| (online) |READY|
+---------+ | | +-----+ +------+ ----------> +-----+
| | ^ | |
| | | | |
| | |(repair) | (abnormal) | (abnormal)
| | | | |
| | | v v
| +-------------------+ (migrate_unit) +--------------------------+
| | ERROR | <-------------------| ERROR_DETECTED |
| +-------------------+ +--------------------------+
状态说明
NEW : 新机器,刚刚被添加到系统中,此状态代表机器正在按照我们要求的系统模板和初始化策略被重装系统和初始化系统环境
INITED : 此状态代表机器已经初始化完成,随时可以上线部署业务(从 READY 状态可以转为 INITED, 不会在此机器上分配新实例,此机器上可能还有线上实例)
READY : 此状态代表机器已经正常在线上工作(新创建实例只会处于 READY 状态的机器)
ERROR_DETECTED : 此状态代表机器管理系统发现机器有故障,并通知上层平台需要迁移服务
ERROR : 此状态代表上层平台已经确认将服务迁移至其他机器,并进入了维修程序
OFFLINE : 此状态代表机器完成软件卸载,也离开了机器故障检测的范围,不再受机器管理系统的管理
1.2 名词说明
+event------+ +FSM----------------------+ +Action-------------+
| | | +---------+ | | |
| | | +--+---+--+ | | |
| api1 | | +-+ | | | task1 |
| | | | +-----+ | | |
| |====> | +---V--+ | |====> | task2 |
| api2 | | +------+ | | | |
| | | +----V---+ | | task3 |
| | | +--------+ | | |
| | | | | |
| | | | | |
+-----------+ +-------------------------+ +-------------------+
有限状态机,(英语:Finite-state machine, FSM),又称有限状态自动机,简称状态机,是表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型。
事件-> 状态 -> 动作
1.3 Roadmap
2 需求分析
2.1 功能需求
2.2.1 有限状态机一般都有以下特点:
1 可以用状态来描述事物,并且任一时刻,事物总是处于一种状态
2 事物拥有的状态总数是有限的
3 通过触发事物的某些行为,可以导致事物从一种状态过渡到另一种状态
4 事物状态变化是有规则的,A 状态可以变换到 B,B 可以变换到 C,A 却不一定能变换到 C
5 同一种行为,可以将事物从多种状态变成同种状态,但是不能从同种状态变成多种状态。
2.2.2 这里需要注意的两个问题:
1、避免把某个“程序动作”当作是一种“状态”来处理
那么如何区分“动作”和“状态”?
“动作”是不稳定的,即使没有条件的触发,“动作”一旦执行完毕就结束了;
而“状态”是相对稳定的,如果没有外部条件的触发,一个状态会一直持续下去。
2、状态划分时漏掉一些状态,导致跳转逻辑不完整。
维护一张状态表就非常必要,而且有意义了。
从表中可以直观看出那些状态直接存在跳转路径,那些状态直接不存在。如果不存在,就把对应的单元格置灰。
每次写代码之前先把表格填写好,并且对置灰的部分重点 review,看看是否有“漏态”,然后才是写代码。QA 拿到这张表格之后,写测试用例也是手到擒来。
2.2 非功能需求
2.3 调研
2.3.1 状态机
2.3.1.1 transitions
https://github.com/pytransitions/transitions
from transitions import Machine
class Matter(object):
pass
def print_info(self):
print("Hello,World")
model = Matter()
#The states argument defines the name of states
states=['solid', 'liquid', 'gas', 'plasma']
# The trigger argument defines the name of the new triggering method
transitions = [
{'trigger': 'melt', 'source': 'solid', 'dest': 'liquid' },
{'trigger': 'evaporate', 'source': 'liquid', 'dest': 'gas'},
{'trigger': 'sublimate', 'source': 'solid', 'dest': 'gas'},
{'trigger': 'ionize', 'source': 'gas', 'dest': 'plasma','before':'print_info'}]
machine = Machine(model=model, states=states, transitions=transitions, initial='solid')
# Test
print(model.state) # solid
model.melt()
print(model.state) # liquid
model.evaporate()
print(model.state) # gas
model.ionize()
print(model.state) # plasma
2.3.1.2 python-statemachine
https://github.com/fgmacedo/python-statemachine
2.3.1.3 OpenStack automaton
https://github.com/openstack/automaton
创建一个简单的状态机
from automaton import machines
m = machines.FiniteMachine()
m.add_state('up')
m.add_state('down')
m.add_transition('down', 'up', 'jump')
m.add_transition('up', 'down', 'fall')
m.default_start_state = 'down'
print(m.pformat())
输出
+---------+-------+------+----------+---------+
| Start | Event | End | On Enter | On Exit |
+---------+-------+------+----------+---------+
| down[^] | jump | up | . | . |
| up | fall | down | . | . |
+---------+-------+------+----------+---------+
变更一个状态机状态
m.initialize()
m.process_event('jump')
print(m.pformat())
print(m.current_state)
print(m.terminated)
m.process_event('fall')
print(m.pformat())
print(m.current_state)
print(m.terminated)
输出
+---------+-------+------+----------+---------+
| Start | Event | End | On Enter | On Exit |
+---------+-------+------+----------+---------+
| down[^] | jump | up | . | . |
| @up | fall | down | . | . |
+---------+-------+------+----------+---------+
up
False
+----------+-------+------+----------+---------+
| Start | Event | End | On Enter | On Exit |
+----------+-------+------+----------+---------+
| @down[^] | jump | up | . | . |
| up | fall | down | . | . |
+----------+-------+------+----------+---------+
down
False
初始化 m.initialize() 完后,这个状态图挺有意思的
@
用于标记当前状态
[^]
用于标记初始状态,使用m.default_start_state = 'down'
声明
[$]
用于标记终止状态,使用m.add_state("lies down", terminal=True)
声明
2.3.2 可视化
基于 tornado WebSocket 模块实现了"服务端直接向客户端推送数据而不需要客户端进行请求"
基于 pygraphviz
https://github.com/pytransitions/transitions/issues/258
3 总体设计
3.1 系统架构
一般来说会有个简单的架构图,并配以文字对架构进行简要说明;
3.2 模块简介
架构图中如果有很多模块,需要对各个模块的功能进行简要介绍;
3.3 设计与折衷
设计与折衷是总体设计中最重要的部分;
3.4 潜在风险
4 详细设计
详细设计重点在“详细”
4.1 模块 xx
(有了数据库+接口+流程,别的同学拿到详设文档,基本也能够搞定了)
4.1.1 交互流程
简要的交互可用文字说明,复杂的交互建议使用流程图,交互图或其他图形进行说明
4.1.2 数据库设计
4.1.3 接口形式
5 状态机
5.1 状态机使用
State1 当发生了 Event1 事件时,执行 Action1 到达 State2
5.1.1 创建状态机
from xlib import statemachine as fsm
class TrafficLightMachine(fsm.StateMachine):
green = fsm.State('Green', initial=True)
yellow = fsm.State('Yellow')
red = fsm.State('Red')
slowdown = green.to(yellow)
stop = yellow.to(red)
go = red.to(green)
>>> traffic_light = TrafficLightMachine()
创建完状态机后,状态机有如下类属性
5.1.1.1 类属性
states_map (dict)
{
'green': State('Green', identifier='green', value='green', initial=True),
'yellow': State('Yellow', identifier='yellow', value='yellow', initial=False),
'red': State('Red', identifier='red', value='red', initial=False)
}
transitions (list)
[
Transition(State('Red', identifier='red', value='red', initial=False), (State('Green', identifier='green', value='green', initial=True),), identifier='go'),
Transition(State('Green', identifier='green', value='green', initial=True), (State('Yellow', identifier='yellow', value='yellow', initial=False),), identifier='slowdown'),
Transition(State('Yellow', identifier='yellow', value='yellow', initial=False), (State('Red', identifier='red', value='red', initial=False),), identifier='stop')
]
states (list)
[
State('Green', identifier='green', value='green', initial=True),
State('Red', identifier='red', value='red', initial=False),
State('Yellow', identifier='yellow', value='yellow', initial=False)
]
5.1.1.2 实例属性
self.model (object)
比如为从 peewee 返回的数据库记录对象
self.state_field (str)
state
self.start_value
初始状态
self.current_state_value
当前状态 value ========> 定义 state 时传的 value 值,如果定义时没有传值的话,默认是 self.current_state.name 的小写
举个栗子:
green = fsm.State('Green', value=100, initial=True)
使用 self.current_state_value 时,返回的 100
使用 self.current_state.name 返回的是 Green
5.1.2 获取当前状态机实例的状态
>>> traffic_light.current_state
State('Green', identifier='green', value='green', initial=True)
>>> traffic_light.current_state == TrafficLightMachine.green == traffic_light.green
True
For each state, there's a dynamically created property in the form ``is_<state.identifier>``, that
returns ``True`` if the current status matches the query:
>>> traffic_light.is_green
True
>>> traffic_light.is_yellow
False
>>> traffic_light.is_red
False
5.1.3 触发动作
>>> traffic_light.slowdown()
>>> traffic_light.current_state
State('Yellow', identifier='yellow', value='yellow', initial=False)
>>> traffic_light.is_yellow
True
不能从不合法的 state 进行触发动作
>>> traffic_light.is_yellow
True
>>> traffic_light.slowdown()
Traceback (most recent call last):
...
TransitionNotAllowed: Can't slowdown when in Yellow.
其他方法触发动作
>>> traffic_light.is_yellow
True
>>> traffic_light.run('stop')
>>> traffic_light.is_red
True
5.2 持久化到数据库
5.2.1 简单例子
>>> class MyModel(object):
... def __init__(self, state):
... self.state = state
...
>>> obj = MyModel(state='red')
..................................................
>>> traffic_light = TrafficLightMachine(model=obj)
>>> traffic_light.is_red
True
>>> obj.state
'red'
>>> obj.state = 'green'
>>> traffic_light.is_green
True
>>> traffic_light.slowdown()
>>> obj.state
'yellow'
>>> traffic_light.is_yellow
True
5.2.2 持久化到 MySQL
# 从 peewee 获取记录对象
job = job_model.Job.get_or_none(unit_ip=ip, unit_matrixid=matrix_id)
# 获取状态机对象
jobstate = job_statemachine.JobMachine(model=job, state_field="job_state")
* model : MySQL 记录对象
* state_field: 状态字段
.................................................
可以在 on_enter_{state} 中使用 self.model.save() 进行保存变更的数据到 MySQL
5.3 Callbacks
5.3.1 Callbacks when running events
code
from statemachine import StateMachine, State
class TrafficLightMachine(StateMachine):
"A traffic light machine"
green = State('Green', initial=True)
yellow = State('Yellow')
red = State('Red')
slowdown = green.to(yellow)
stop = yellow.to(red)
go = red.to(green)
def on_slowdown(self):
print('exe_slowdown')
def on_stop(self):
print('exe_stop')
def on_go(self):
print('exe_go')
操作
>>> stm = TrafficLightMachine()
>>> stm.slowdown()
exe_slowdown
>>> stm.stop()
exe_stop
>>> stm.go()
exe_go
5.3.2 callback when entering/exiting states
from statemachine import StateMachine, State
class TrafficLightMachine(StateMachine):
"A traffic light machine"
green = State('Green', initial=True)
yellow = State('Yellow')
red = State('Red')
cycle = green.to(yellow) | yellow.to(red) | red.to(green)
def on_enter_green(self):
print('enter_green')
def on_enter_yellow(self):
print('enter_yellow')
def on_enter_red(self):
print('enter_red')
操作
>>> stm = TrafficLightMachine()
>>> stm.cycle()
enter_green
>>> stm.cycle()
enter_yellow
>>> stm.cycle()
enter_red
5.3.3 Callback 之间的关系
green = State('Green')
yellow = State('Yellow')
cycle = green.to(yellow)
def on_exit_green(self):
print('exit green')
def on_enter_yellow(self):
print('enter yellow')
def on_cycle(self):
print "--------------------------", self.current_state_value
当触发 cycle 动作时,输出如下内容
-------------------------- green # 先执行 on_cycle
exit green # 然后执行 on_exit_green
enter yellow # 最后执行 on_enter_yellow
5.3.4 callback 异常对状态变更的影响
import traceback
from xlib import statemachine as fsm
class TrafficLightMachine(fsm.StateMachine):
green = fsm.State('Green', initial=True)
yellow = fsm.State('Yellow')
red = fsm.State('Red')
slowdown = green.to(yellow)
stop = yellow.to(red)
go = red.to(green)
def on_slowdown(self):
print('exe_slowdown')
def on_stop(self):
print('exe_stop')
def on_go(self):
print('exe_go')
def on_exit_green(self):
print('exit_green')
def on_exit_yellow(self):
print('exit_yellow')
def on_exit_red(self):
print('exit_red')
def on_enter_green(self):
print('enter_green')
def on_enter_yellow(self):
print('enter_yellow')
def on_enter_red(self):
print('enter_red')
stm = TrafficLightMachine()
try:
stm.slowdown()
except BaseException:
print traceback.format_exc()
print stm.current_state
假设从状态 green 转变为状态 yellow
on_slowdown 异常时,则状态变更失败
on_exit_green 异常时,则状态变更失败
on_enter_yellow 异常时,则不影响状态变更
6 状态机实践
6.1 状态机
简易状态机图,不考虑异常状态
|
------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------
action | go_ready go_deployed go_online go_removed
| | | | |
| | | | |
| | | | |
| V V V V
|
| 旧实例已添加到迁移列表 实例确认可迁移 新实例部署已完成 实例屏蔽打开或已主从同步完成 实例可下线状态 实例已下线完成
| 1000 2000 3000 4000 5000 6000
+--------------+ | +--------------+ +--------------+ +----------------+ +--------------+ +---------------+ +---------------+
state |oldunit_online|------------+-->|migrate_create|===>|migrate_ready | =======>|newunit_deployed|======>|newunit_online|======>|oldunit_offline|=====>|oldunit_removed|
+--------------+ ^ | +--------------+ +--------------+ +----------------+ +--------------+ ^ +---------------+ +---------------+
| | |
| | |
| | |
外界触发 =============================================================================================================================================================
| | |
| | +------------+
action | | | go_offline |
| | +------------+
|
|
接口 job_create job_trigger
code
from xlib.statemachine import StateMachine
from xlib.statemachine import State
class JobMachine(StateMachine):
migrate_create = State(name="migrate_create", value=1000, initial=True)
migrate_ready = State(name="migrate_ready", value=2000)
newunit_deployed = State(name="newunit_deployed", value=3000)
newunit_online = State(name="newunit_online", value=4000)
oldunit_offline = State(name="oldunit_offline", value=5000)
oldunit_removed = State(name="oldunit_removed", value=6000)
go_ready = migrate_create.to(migrate_ready)
go_deployed = migrate_ready.to(newunit_deployed)
go_online = newunit_deployed.to(newunit_online)
go_offline = newunit_online.to(oldunit_offline)
go_removed = oldunit_offline.to(oldunit_removed)
...
# 新实例 online --------------------------
# on_enter_{state} 表示进入 {state} 时,需要执行的操作,这个时候需要进行保存当前状态,以及添加需要执行的任务等
def on_enter_newunit_online(self):
self.model.save()
# on_exit_{state} 表示离开 {state} 时,需要执行的操作,这个时候主要就是检查当前状态的任务是否完成,是否符合预期
# 如果不符合预期,可以通过 assert 进行弹出异常
def on_exit_newunit_online(self):
self._check_task_done()
# on_{action} 表示执行某个动作时,触发操作
def on_go_online(self):
pass
定义数据库字段
from datetime import datetime
from xlib.db.peewee import CharField
from xlib.db.peewee import IntegerField
from xlib.db.peewee import DateTimeField
import xlib.db
# Define a model class
class Job(xlib.db.BaseModel):
"""
Job 表结构
"""
# If none of the fields are initialized with primary_key=True,
# an auto-incrementing primary key will automatically be created and named 'id'.
job_id = CharField(unique=True, max_length=64)
job_source = CharField(max_length=64)
job_state = IntegerField()
...
c_time = DateTimeField(column_name="c_time", default=datetime.now)
u_time = DateTimeField(column_name="u_time", default=datetime.now)
if __name__ == "__main__":
xlib.db.my_database.connect()
xlib.db.my_database.drop_tables([Job])
xlib.db.my_database.create_tables([Job])
使用
job = job_model.Job.get_or_none(xxx=xxx)
jobstate = job_statemachine.JobMachine(model=job, state_field="job_state" )
状态机父类,支持传三个参数
class BaseStateMachine(object):
...
def __init__(self, model=None, state_field='state', start_value=None)
...
6.2 状态机在后端架构中位置 -- 个人理解
以管理机器生命周期为例
建表
建立 1 张表存放机器记录,其中有含有机器状态的字段(是个数字)
建立 1 张表存放状态变更记录
状态机本身不需要建表,使用类的属性来描述状态,使用方法来描述行为
接口
创建记录接口
编写管理机器记录的接口,从数据库中获取的机器记录使用状态机解析下当前的状态(数字 ===>字符串)
创建记录的时候,同时需要往状态变更记录表中插入一条记录
状态变更接口
状态机实例.run('stop') 进行修改状态机状态
并在状态机中添加所有行为的 callback, 用于更新数据库中对应机器的状态,和添加对应变更的变更记录
状态变更时有很多流程执行,这里有两种方案
外部触发流程接口,流程接口中进行调用状态变更接口 --------------------------> 【目前先按照这种方式】
外部触发状态变更接口,在触发变更的时候执行 job callback 进行完成流程任务
备注
外部触发状态变更接口,在触发变更的时候执行 job callback 时,可以通过 on_exit_{state} 做些检查工作
类比一下
发起一个部署任务 job service_add_unit (部署任务 job 包含多个 task)
task1: add_container
-----> task1 含有子 task
task2: add_xns_instance
6.3 常见问题处理
触发动作时,上一个状态的 exit 检查
假设我们使用 on_exit_{state}
来检查当前状态的任务是否已完成
当我们进行正常变更状态时,检查当前状态的任务是否已完成是必须的
但当我们想要变更为异常状态的时候,则此时需要可直接变更状态,而无需检查之前的任务
目前处理方式
通过在 on_{action} 方法中增加 self.action 属性
然后再 on_exit_{state} 中根据 self.action 属性值判断是否需要进行额外检查
7 传送门
https://github.com/sailthru/stolos
https://github.com/openstack/mistral
Openstack taskflow
Last updated