背景
- xx产品侧规划了全新的能力升级, 主要思路为:改变之前通过xx等手工生成xx的方式,通过标准化流程尽可能的减少人工介入,提升产出效率。
- xx入库、xx生成链路存在链路长、链路不稳定问题,由于目前缺乏比较好的监控、检测、补偿机制,导致链路中断后无法实时感知且无法及时介入推进,对用户体验有一定的影响。
- 当前架构多是通过hard code的方式支撑各渠道的接入,存在代码耦合度高、难以扩展、难以修改的问题。代码质量因开发同学能力差异参差不齐。缺少统一的架构规约对编码进行约束。
目标
业务目标: - 支撑xx产品规划的业务能力建设。
架构目标: - 通过灵活的链路编排,实现链路节点的复用和差异化处理,降低新渠道的接入成本。
- 支持未来对单个链路任意数量的链路节点的灵活扩展。
- 提供故障感知、链路推进的能力,达到对链路的精准感知和管控。
- 架构规约,通过架构规范(扩展子类、新增配置)开闭原则来限制开发同学的修改、扩展,一定程度的提升编码质量
设计思路 - 事件驱动,所有请求均转化为事件,所有动作均由事件触发,事件与执行解耦。统一分发、处理。
- 事件、状态、动作解耦,多维度灵活组合。
- 基于状态机的流程编排及控制各节点精确流转。
- 基于状态机的故障感知、数据补偿及流程推进。
术语 - 客户订单:客户需求的承载实体。
- 标准化订单:通过标准化字段对原始客户订单归一化之后的订单实体。
- 任务:全称为:标准化订单任务,基于标准化订单发起的xx制作任务。
- xx策略:指出具体的制作方向。是对标准化订单的细化。创意策略系统的产出物。
- 脚本xx:根据xx策略,让算法自动生成、拼接行程的xx部分。是xx系统的产出物。
- 补偿注册:通知补偿模块将当前逻辑语句块(方法等)纳入到补偿管控范围。纳入管控范围后,将会对被注册模块周期性的执行检测、或者补偿。
- 故障感知:检测到链路出现故障并上报。不同场景、节点的检测逻辑不同,因此需要不同的检测策略。
- 补偿策略:检测到链路因为各种问题不推进时需要执行的逻辑。根据场景、错误具体原因不同,可以执行的补偿策略如重试、兜底查询、告警等各不相同。前期先至少可以做到告警,后根据具体原因和场景配置不同的策略。
- 状态机:一种对状态流转进行管理的机制。整个状态机由多个状态组成,状态之间流转称为状态迁移。状态迁移由事件触发。
- 状态机-源状态:状态机当前所处的状态。
- 状态机-目标状态:状态机准备迁移的下一个状态
- 状态机-动作:发生状态迁移后执行的动作。
- Event: 事件,领域事件,可以认为是推进状态机发生状态迁移的触发源。根据具体场景不同,可以定义自己的Event。
- EventDispatcher: 事件分发中心,可以认为是事件中转站,根据事件类型不同会分发到不同的目的地。整个系统采用事件驱动,统一由EventDispatcher分发,不允许上层直接驱动状态机。
- EventRouter:维护事件和目的的路由关系,用于事件分发。本系统为维护event和所属状态机的路由关系。
xx状态机
策略任务维度状态机
生成任务维度状态机
应用架构
略
xx1.0位于xx-xx应用内,xx-xx后续定位为后台运营服务,1.0逻辑在2.0上线后逐步废弃。
xx2.0运行时服务,xx-xx-core,为新建应用。承载标准化订单、合成任务等。
逻辑架构
xx2.0运行时服务(新建)
略
领域模型
略
整体交互图
创建订单场景
略
收到算法投递的xx成功&失败事件&补偿xx失败逻辑
略
关键类
子系统改造
各子系统设计方案见子目录对应模块设计文档。
状态机使用
增加新的节点(如在两个状态state1和state2间增加审批节点,且审批通过后通知用户):
- StateType枚举增加:待审批-approving、已审批-approved、审批拒绝-approveRefuesed。
- EventType枚举增加:审批通过事件-approvedEvent、审批拒绝事件-approveRefuesedEvent。
- 创建ApprovedNotifySlot,并实现execute方法。用于审批通过后通知用户。
public class ApprovedNotifyActionSlot implement StateAction {
public onDo(State from, State to, Event event) {
// 通知到用户逻辑
}
} - StateTransitBuilder配置该状态的迁移路由(谁迁移到它,它迁移到哪)
StateTransiBuilder builder = StateTransiBuilderFactory.create();
builder.create()
.from(state1)
.to(approving)
.on(event1);
builder.create()
.from(approving)
.to(approved)
.on(approvedEvent)
.do(SlotChainBuilder.build()
.add(new ApprovedNotifySlot));
builder.create()
.from(approved)
.to(state2)
.on(approvedEvent);
builder.create()
.from(approving)
.to(apprvoeRefuesed)
.on(apprvoeRefuesedEvent);
现有状态内追加action。
- 新增审批通过、拒绝事件
- 新增审批处理Slot,在handle内实现审批校验逻辑。
其他问题 - 平滑迁移问题:对链路增减状态后,发布过程中如何做到平滑迁移
- 状态机新增节点
- 没有问题。
- 状态机删除节点
- 逻辑上删除了某个状态,数据库中某状态机仍是旧状态,可能会出现该旧状态无法流转到新状态的问题。
- 解决方案一:不允许删除状态
- 状态不允许删除,如果需要删除某状态,直接把状态对应的action置空即可。
- 缺点:以后状态越来越多,不好维护。状态空跑也会增加无意义的消耗。
- 解决方案二:增加版本控制
- 对状态机增加版本控制,状态增减一次版本加+1。
- 状态机流转路由与状态机版本匹配。一段时间后,旧版本的状态机完全处理完毕后,则可以直接下线旧版本逻辑。
- 缺点:根据场景不同,可能旧版本的状态机下线时间不确定。
- 解决方案三(采用该方案):持久化迁移规则快照
- 创建状态机实例时,持久化当前状态迁移规则快照,当从内存中无法查询到对应状态迁移规则时,降级使用迁移规则快照
- 实现简单,缺点还能接受。
- 状态机状态数量未发生变化,但在两状态之间增加执行的节点。
- 一个state对应一个slot chain,当State发生迁移时,会按需执行对应的slot chain。
- 对应的slot chain追加slot即可。
- 状态机新增节点
- 随着未来状态越来越多,同一个Event可能要被多个state消费。
- 方案一:EventRouter配置多个事件路由,Event的消费target应该由EventDispather统一分发。
- 缺点:链路长,性能不够好。
- 优点:所有Event统一分发和消费,入口统一。统一由EventDispather管理和监控,便于事件跟踪。
- 方案二:同一个Event配置多个观察者,即onEvent时通知到多个slot
- 优点:链路短,且简单。
- 缺点:存在多个事件路由逻辑:eventRouter、Event内部。
- 倾向于方案一:
- 开发同学上手简单、开发成本低,优于性能。
- 方案一:EventRouter配置多个事件路由,Event的消费target应该由EventDispather统一分发。
数据模型
状态机数据模型
暂时无法在飞书文档外展示此内容
其他子模块的数据模型见各模块设计文档
上下游协作
算法
- 算法目前在处理完成xx(xx、xx)后直接发送消息到mq,如果出错会带上错误信息。但未维护任务状态。可以放在redis或者mysql里,并对外提供查询任务接口。dubbo主动查询和mq异步链路两条链路整体可靠性更高。
- 算法侧是否可以将粗粒度的链路进一步细分,并记录每一步的状态(埋点、日志)。业务层感知后可以告警或者根据具体细分原因,采取对应的补偿策略。具体采用何种补偿策略应该根据场景和错误原因case by case去看,前期至少要先告警出来,后不断打磨。
事件、状态枚举定义
public enum StateType {
CheckXxSuccess(10000),
CheckXxFailed(10001),
PreviewXxSuccess(10002),
PreviewXxFailed(10003),
GenerateXxSuccess(10004),
GenerateXxFailed(10005);
StateType(int stateType) {
this.stateType = stateType;
}
public int getType() {
return stateType;
}
private int stateType;
}
public enum EventType {
CreateOrder(60000),
CheckXXSuccess(60001),
CheckXXFailed(60002),
PreviewXXSuccess(60003),
PreviewXXFailed(60004),
GenerateXXSucess(60005),
GenerateXXFailed(60006);
EventType(int eventType) {
this.eventType = eventType;
}
public int getType() {
return eventType;
}
private int eventType;
}