最近幾篇文章,我會帶大家一起設計一個流程編排框架,從項目的分析、設計、實現、重構、測試方面去了解整個編排框架,也會用到一些設計開發原則及設計模式,話不多說,我們先來看下編排框架的一個背景。
背景
對於交易這樣複雜的業務場景,隨着時間增加、功能逐漸增多,代碼越來越多,所以系統就會考慮使用微服務框架;但是使用微服務框架之後,原有的業務並沒有發生變化,與傳統架構相比,微服務架構下會更依賴通過各微服務之間的協調來實現業務流程,這種協作就是流程編排。編排設計到方法節點、服務調用、條件選擇、串行、並行、子流程等;當然在這個過程中也需要考慮通訊層、分佈式事務;需要有一個完善的編排框架來支撐。
一般常見2種編排方式
第一種:面向可執行的流程,通過一個可執行的流程來協同內部及外部的服務交互,通過流程來控制總體的目標、涉及的操作、服務調用順序。首先要有一個流程控制服務,該服務接收請求,依照業務邏輯規則,依次調用各個微服務,並最終完成處理邏輯。流程控制服務時時刻刻都知道每一筆業務究竟進行到了什麼地步,監控業務成了相對簡單的事情。
第二種:API網關,API網關可以看作一種簡單的接口聚合/拆分的方式:每筆業務到來後先到達網關,網關調用各微服務,並最終聚合/拆分需反饋的結果。如果有開放接口文檔還可以用編排定製產品,直接達到商用的效果。
項目背景
我們常規的項目是怎麼做這種編排的呢?我們列舉一個例子,A->B->C:這種場景是比較簡單,對於開發人員來說就是新建一個類先執行A邏輯同步等待執行,在執行B邏輯同步等待結果在執行C;然後對所有結果進行處理返回;但是這個邏輯不可能是不變的,可能第二天又說在B->C加一個D邏輯,有需要在之前的業務代碼裏面做業務流程新增邏輯,而且需要負責的測試;當然後面可能還有更多的需求提供來,比如其它邏輯要應用這個編排,或者在執行B時需要加條件執行C和D,對於開發人員來說是遲早奔潰的事情。
在這個例子中我們可以看到幾個問題:
-
流程擴展性不強,需要對流程做變更的時候需要在原有邏輯修改代碼,對於流程的風險也是比較大的;
-
流程可讀性不行,簡單流程邏輯我們還可以理解,但是節點多了之後,就是產生各種邏輯、各種設計模式,不管對於老員工或者新員工來說,都是一筆不小的負擔;
-
代碼冗餘,在項目裏面有過多的我是看到的只是一堆代碼類似,但是功能實現不一樣的邏輯;某一段邏輯出現問題,需要改涉及到的所有流程,還有可能改漏的風險性;
-
缺少開發規範,一萬個人就有一萬個哈姆雷特,只要能實現功能,只要能完成任務就行,根本考慮不到誰來接手的問題;
-
缺少監控手段,不知道這個流程定義了幾個節點,每個節點耗時,是那個節點出現的問題,對於運維來說也是大問題;
針對上面的這些問題,我們有什麼建議呢?我先來說說的我的;
我們可以把被調用的服務或者是簡單封裝成一個個的基礎組件,規範輸入輸出,把調用條件封裝成一個個要素;通過文件或者是外部接口做服務編排;比如我定義一個test.flow.yml文件對我的基礎組件就行編排,完全不用再代碼裏面去做業務邏輯實現。
我們希望我們開發出來的東西有一定的影響能力,即使做不到在行業裏面有影響力,起碼也要做到在公司範圍內有影響力。所以,從一開始,我們就不想把這個編排框架,做成只有我們項目可以,而是一個通用框架,可以集成到各個系統,甚至集成到微服務治理平臺中。實際上,這也體現了業務開發中要具備的抽象意識、框架意識。我們要善於識別出通用的功能模塊,將它抽象成通用的框架、組件、類庫等。
需求分析
剛剛我們花了一些篇幅項目背景和背景需求,接下來,我們再對需求進行更加詳細的分析和整理。
功能性需求
首先我們需要定義編排規則,但是沒有可視化操作的界面的前提下,我們會把編排定義在yml、properties、xml中,我們先看一個demo示例:
name: openAccount
id: test
desc: 條件執行
input: com.service.flow.sample.common.model.TestInput
output: com.service.flow.sample.common.model.TestOutput
temp: com.service.flow.sample.common.model.TestTemp
startNode: node1
nodes:
- node:
id: node1
name: methodInvoke
component: com.service.flow.sample.common.component.TestComponent:test1
desc: 單節點執行
input: com.service.flow.sample.common.model.TestInput
type: method
next: node2
- node:
id: node2
name: beanInvoke
component: testComponent:test2
desc: 單節點執行
input: com.service.flow.sample.common.model.TestInput
type: bean
next: node3
- node:
id: node3
name: conditionByAge
component: temp.getAge>20:node4,temp.getAge<20:node5
desc: 單節點執行
input: com.service.flow.sample.common.model.TestInput
type: condition
- node:
id: node4
name: beanInvoke
component: testComponent:test2
desc: 單節點執行
input: com.service.flow.sample.common.model.TestInput
type: bean
- node:
id: node5
name: beanInvoke
component: testComponent:test2
desc: 單節點執行
input: com.service.flow.sample.common.model.TestInput
type: bean
-
我們需要定義一些元素請求參數、輸出參數、執行ID、流程描述、臨時變量、節點類型,執行條件;
-
我們需要提供多種編排註冊方式yml、json、xml、properties、text、接口;
-
我們採用鏈表的形式執行所有的節點,減少內存開銷;
-
需要支持多種類型節點,方法節點、服務節點、條件節點、Bean節點、子流程節點等;
-
我們需要對流程上下文做擴展;
-
我們框架需要基於Spring做基層框架,並提供對應的starter,做可集成框架;
非功能性需求
易用性方面,我們希望提供流程提示,有能力的情況下提供可視化編排,我們將以Spring框架作爲基礎框架,我們還希望編排框架能否非常方便地集成到使用 Spring 框架的項目中。
擴展性、靈活性方面,我們希望能夠自己定義各種組件和元素模型,我們希望編排初始化的時候提供各種擴展接口,將不同類型的文本,轉換成編排模型;
性能方面:我們希望模型在項目啓動的時候就初始化好,只初始化一次;節點執行的時候採用遞歸鏈表的形式,減少不必要的代碼循環;
容錯性方面,不能因爲編排框架而影響的本身的業務邏輯,即使不用這個框架也不至於框架大亂。
功能需求其實沒有多少,但將非功能性需求考慮進去之後,明顯就複雜了很多。還是那句老話,寫出能用的代碼很簡單,寫出好用的代碼很難。對於編排框架來說,非功能性需求是設計與實現的難點。怎麼做到易用、靈活、可擴展、低延遲、高容錯,纔是開發的重點。