連載中...
Golang框架實戰-KisFlow流式計算框架(1)-概述
Golang框架實戰-KisFlow流式計算框架(2)-專案構建/基礎模組-(上)
Golang框架實戰-KisFlow流式計算框架(3)-專案構建/基礎模組-(下)
Golang框架實戰-KisFlow流式計算框架(4)-資料流
3.1 資料型別定義
KisFlow中可以傳遞任意型別資料作為Flow的資料來源。而且KisFlow支援批次資料的流逝計算處理。
首先需要對KisFlow中內部支援的資料型別做一個基本的定義,我們將這部分的定義程式碼寫在kis-flow/common/
中的data_type.go
檔案中。
kis-flow/common/data_type.go
package common
// KisRow 一行資料
type KisRow interface{}
// KisRowArr 一次業務的批次資料
type KisRowArr []KisRow
/*
KisDataMap 當前Flow承載的全部資料,
key : 資料所在的Function ID
value: 對應的KisRow
*/
type KisDataMap map[string]KisRowArr
KisRow
:表示一行資料,可以是任意的資料型別,比如字串,json字串,一些序列化的二進位制資料, protobuf,yaml字串等,均可。KisRowArr
:表示多行資料,也就是一次提交的批次資料,他是KisRow的陣列集合。KisDataMap
:表示當前Flow承載的全部資料。是一個map[string]KisRowArr
型別,其中key為資料所在的Function ID,value為資料。
3.2 KisFlow資料流處理
在KisFlow模組中,新增一些存放資料的成員,如下:
kis-flow/flow/kis_flow.go
// KisFlow 用於貫穿整條流式計算的上下文環境
type KisFlow struct {
// 基礎資訊
Id string // Flow的分散式例項ID(用於KisFlow內部區分不同例項)
Name string // Flow的可讀名稱
Conf *config.KisFlowConfig // Flow配置策略
// Function列表
Funcs map[string]kis.Function // 當前flow擁有的全部管理的全部Function物件, key: FunctionID
FlowHead kis.Function // 當前Flow所擁有的Function列表表頭
FlowTail kis.Function // 當前Flow所擁有的Function列表表尾
flock sync.RWMutex // 管理連結串列插入讀寫的鎖
ThisFunction kis.Function // Flow當前正在執行的KisFunction物件
ThisFunctionId string // 當前執行到的Function ID (策略配置ID)
PrevFunctionId string // 當前執行到的Function 上一層FunctionID(策略配置ID)
// Function列表引數
funcParams map[string]config.FParam // flow在當前Function的自定義固定配置引數,Key:function的例項KisID, value:FParam
fplock sync.RWMutex // 管理funcParams的讀寫鎖
// ++++++++ 資料 ++++++++++
buffer common.KisRowArr // 用來臨時存放輸入位元組資料的內部Buf, 一條資料為interface{}, 多條資料為[]interface{} 也就是KisBatch
data common.KisDataMap // 流式計算各個層級的資料來源
inPut common.KisRowArr // 當前Function的計算輸入資料
}
buffer
: 用來臨時存放輸入位元組資料的內部Buf, 一條資料為interface{}, 多條資料為[]interface{} 也就是KisBatchdata
: 流式計算各個層級的資料來源inPut
: 當前Function的計算輸入資料
後續章節會使用到這幾個成員屬性,這裡先做為了解。
因為data是一個map
型別,所以需要在NewKisFlow()
中,對其進行初始化操作:
kis-flow/flow/kis_flow.go
// NewKisFlow 建立一個KisFlow.
func NewKisFlow(conf *config.KisFlowConfig) kis.Flow {
flow := new(KisFlow)
// 例項Id
flow.Id = id.KisID(common.KisIdTypeFlow)
// 基礎資訊
flow.Name = conf.FlowName
flow.Conf = conf
// Function列表
flow.Funcs = make(map[string]kis.Function)
flow.funcParams = make(map[string]config.FParam)
// ++++++++ 資料data +++++++
flow.data = make(common.KisDataMap)
return flow
}
3.2.2 業務提交資料介面
KisFlow的開發者在編寫業務時,可以透過flow例項來進行提交業務源資料,所以我們需要給Flow
抽象層新增一個提交資料的介面:
kis-flow/kis/flow.go
package kis
import (
"context"
"kis-flow/common"
"kis-flow/config"
)
type Flow interface {
// Run 排程Flow,依次排程Flow中的Function並且執行
Run(ctx context.Context) error
// Link 將Flow中的Function按照配置檔案中的配置進行連線
Link(fConf *config.KisFuncConfig, fParams config.FParam) error
// CommitRow ++++++ 提交Flow資料到即將執行的Function層 ++++
CommitRow(row interface{}) error
}
新增介面 CommitRow(any interface{}) error
。
在kis-flow/flow/kis_flow_data.go
中實現KisFlow的該介面。
kis-flow/flow/kis_flow_data.go
func (flow *KisFlow) CommitRow(row interface{}) error {
flow.buffer = append(flow.buffer, row)
return nil
}
CommitRow()
為提交Flow資料, 一行資料,如果是批次資料可以提交多次。 所有提交的資料都會暫存在flow.buffer
成員中,作為緩衝區。
3.2.3 KisFlow內部資料提交
現在開發者可以透過CommitRow()
將資料提交到buffer中,但是在KisFlow內部需要一個內部介面來將buffer提交到KisFlow的data中,作為之後當前Flow全部Function的上下文資料供使用。所以我們這裡需要再提供兩個介面。分別是首次提交資料commitSrcData()
和中間層提交資料commitCurData()
兩個函式。
A. 首層資料提交
kis-flow/flow/kis_flow_data.go
// commitSrcData 提交當前Flow的資料來源資料, 表示首次提交當前Flow的原始資料來源
// 將flow的臨時資料buffer,提交到flow的data中,(data為各個Function層級的源資料備份)
// 會清空之前所有的flow資料
func (flow *KisFlow) commitSrcData(ctx context.Context) error {
// 製作批次資料batch
dataCnt := len(flow.buffer)
batch := make(common.KisRowArr, 0, dataCnt)
for _, row := range flow.buffer {
batch = append(batch, row)
}
// 清空之前所有資料
flow.clearData(flow.data)
// 首次提交,記錄flow原始資料
// 因為首次提交,所以PrevFunctionId為FirstVirtual 因為沒有上一層Function
flow.data[common.FunctionIdFirstVirtual] = batch
// 清空緩衝Buf
flow.buffer = flow.buffer[0:0]
log.Logger().DebugFX(ctx, "====> After CommitSrcData, flow_name = %s, flow_id = %s\nAll Level Data =\n %+v\n", flow.Name, flow.Id, flow.data)
return nil
}
//ClearData 清空flow所有資料
func (flow *KisFlow) clearData(data common.KisDataMap) {
for k := range data {
delete(data, k)
}
}
實際上commitSrcData()
在整個的Flow
執行週期只會執行一次,這個作為當前Flow
的始祖源資料。
commitSrcData()
的最終目的是 將buffer的資料提交到data[FunctionIdFirstVirtual]
中。 這裡要注意的是FunctionIdFirstVirtual
是一個虛擬fid,作為所有Function
的上游Function ID。 並且首次提交之後,flow.buffer
的資料將被清空。
B. 中間層資料提交
kis-flow/flow/kis_flow_data.go
//commitCurData 提交Flow當前執行Function的結果資料
func (flow *KisFlow) commitCurData(ctx context.Context) error {
//判斷本層計算是否有結果資料,如果沒有則退出本次Flow Run迴圈
if len(flow.buffer) == 0 {
return nil
}
// 製作批次資料batch
batch := make(common.KisRowArr, 0, len(flow.buffer))
//如果strBuf為空,則沒有新增任何資料
for _, row := range flow.buffer {
batch = append(batch, row)
}
//將本層計算的緩衝資料提交到本層結果資料中
flow.data[flow.ThisFunctionId] = batch
//清空緩衝Buf
flow.buffer = flow.buffer[0:0]
log.Logger().DebugFX(ctx, " ====> After commitCurData, flow_name = %s, flow_id = %s\nAll Level Data =\n %+v\n", flow.Name, flow.Id, flow.data)
return nil
}
commitCurData()
會在每次Function執行計算後,將當前Function的計算結果資料進行提交。 commitCurData() 會在Flow的流式計算過程中被執行多次。
commitCurData()
的最終目的是將將buffer的資料提交到data[flow.ThisFunctionId]
中 。ThisFunctionId也就是當前正在執行Function,同時也是下一層將要執行的Function的上一層。
提交之後,flow.buffer
的資料將被清空。
3.2.4 獲取正在執行Function的源資料
至於每層Function的源資料如何得到,我們可以透過getCurData()
方法得到。 透過PrevFunctionId
進行索引,因為獲取當前Function的源資料,就是上一層Function的結果資料,所以我們透過PrevFunctionId
來得到上一層Function的Id,從data[PrevFunctionId
] 中可以得到資料來源。
kis-flow/flow/kis_flow_data.go
// getCurData 獲取flow當前Function層級的輸入資料
func (flow *KisFlow) getCurData() (common.KisRowArr, error) {
if flow.PrevFunctionId == "" {
return nil, errors.New(fmt.Sprintf("flow.PrevFunctionId is not set"))
}
if _, ok := flow.data[flow.PrevFunctionId]; !ok {
return nil, errors.New(fmt.Sprintf("[%s] is not in flow.data", flow.PrevFunctionId))
}
return flow.data[flow.PrevFunctionId], nil
}
3.2.5 資料流鏈式排程處理
下面我們就要在flow.Run()
方法中,來加入資料流的處理動作。
kis-flow/flow/kis_flow.go
// Run 啟動KisFlow的流式計算, 從起始Function開始執行流
func (flow *KisFlow) Run(ctx context.Context) error {
var fn kis.Function
fn = flow.FlowHead
if flow.Conf.Status == int(common.FlowDisable) {
//flow被配置關閉
return nil
}
// ========= 資料流 新增 ===========
// 因為此時還沒有執行任何Function, 所以PrevFunctionId為FirstVirtual 因為沒有上一層Function
flow.PrevFunctionId = common.FunctionIdFirstVirtual
// 提交資料流原始資料
if err := flow.commitSrcData(ctx); err != nil {
return err
}
// ========= 資料流 新增 ===========
//流式鏈式呼叫
for fn != nil {
// ========= 資料流 新增 ===========
// flow記錄當前執行到的Function 標記
fid := fn.GetId()
flow.ThisFunction = fn
flow.ThisFunctionId = fid
// 得到當前Function要處理與的源資料
if inputData, err := flow.getCurData(); err != nil {
log.Logger().ErrorFX(ctx, "flow.Run(): getCurData err = %s\n", err.Error())
return err
} else {
flow.inPut = inputData
}
// ========= 資料流 新增 ===========
if err := fn.Call(ctx, flow); err != nil {
//Error
return err
} else {
//Success
// ========= 資料流 新增 ===========
if err := flow.commitCurData(ctx); err != nil {
return err
}
// 更新上一層FuncitonId遊標
flow.PrevFunctionId = flow.ThisFunctionId
// ========= 資料流 新增 ===========
fn = fn.Next()
}
}
return nil
}
- 在run() 剛執行的時候,對PrevFunctionId 進行初始化,設定為
FunctionIdFirstVirtual
。 - 在run() 剛執行的時候,執行
commitSrcData()
將業務賦值的的buffer資料提交到data[FunctionIdFirstVirtual
]中。 - 進入迴圈,執行每個Function的時候,
getCurData()
獲取到當前Function的源資料,並且放在flow.inPut
成員中。 - 進入迴圈,更正
ThisFunctionId
遊標為當前Function ID。 - 進入迴圈,每個Funciton執行完畢後,將Function產生的結果資料透過
commitCurData()
進行提交,並且改變PrevFunctionId
為當前FunctionID, 進入下一層。
很顯然,我們還需要讓Flow
給開發者提供一個獲取Input資料的介面。
kis-flow/kis/flow.go
package kis
import (
"context"
"kis-flow/common"
"kis-flow/config"
)
type Flow interface {
// Run 排程Flow,依次排程Flow中的Function並且執行
Run(ctx context.Context) error
// Link 將Flow中的Function按照配置檔案中的配置進行連線
Link(fConf *config.KisFuncConfig, fParams config.FParam) error
// CommitRow 提交Flow資料到即將執行的Function層
CommitRow(row interface{}) error
// ++++++++++++++++++++++
// Input 得到flow當前執行Function的輸入源資料
Input() common.KisRowArr
}
實現如下:
kis-flow/flow/kis_flow_data.go
// Input 得到flow當前執行Function的輸入源資料
func (flow *KisFlow) Input() common.KisRowArr {
return flow.inPut
}
3.3 KisFunction的資料流處理
由於我們的Function排程模組還目前還沒有實現,所以有關Function在執行Call()
方法的時候,只能暫時將業務計算的邏輯寫死在KisFlow框架中。 在下一章節,我們會將這部分的計算邏輯開放給開發者進行註冊自己的業務。
現在Flow已經將資料傳遞給了每層的Function,那麼在Function中我們下面來簡單模擬一下業務的基礎計算邏輯。
我們暫時修改KisFunctionC
和 KisFunctionE
兩個模組的Call()
程式碼.
假設KisFunctionC 是 KisFunctionE的上層。
kis-flow/function/kis_function_c.go
type KisFunctionC struct {
BaseFunction
}
func (f *KisFunctionC) Call(ctx context.Context, flow kis.Flow) error {
log.Logger().InfoF("KisFunctionC, flow = %+v\n", flow)
//TODO 呼叫具體的Function執行方法
//處理業務資料
for i, row := range flow.Input() {
fmt.Printf("In KisFunctionC, row = %+v\n", row)
// 提交本層計算結果資料
_ = flow.CommitRow("Data From KisFunctionC, index " + " " + fmt.Sprintf("%d", i))
}
return nil
}
kis-flow/function/kis_function_e.go
type KisFunctionE struct {
BaseFunction
}
func (f *KisFunctionE) Call(ctx context.Context, flow kis.Flow) error {
log.Logger().InfoF("KisFunctionE, flow = %+v\n", flow)
// TODO 呼叫具體的Function執行方法
//處理業務資料
for _, row := range flow.Input() {
fmt.Printf("In KisFunctionE, row = %+v\n", row)
}
return nil
}
3.4 資料流單元測試
下面我們模擬一個簡單的計算業務,測試下每層的Function是否可以得到資料,並且將計算結果傳遞給下一層。
kis-flow/test/kis_flow_test.go
func TestNewKisFlowData(t *testing.T) {
ctx := context.Background()
// 1. 建立2個KisFunction配置例項
source1 := config.KisSource{
Name: "公眾號抖音商城戶訂單資料",
Must: []string{"order_id", "user_id"},
}
source2 := config.KisSource{
Name: "使用者訂單錯誤率",
Must: []string{"order_id", "user_id"},
}
myFuncConfig1 := config.NewFuncConfig("funcName1", common.C, &source1, nil)
if myFuncConfig1 == nil {
panic("myFuncConfig1 is nil")
}
myFuncConfig2 := config.NewFuncConfig("funcName2", common.E, &source2, nil)
if myFuncConfig2 == nil {
panic("myFuncConfig2 is nil")
}
// 2. 建立一個 KisFlow 配置例項
myFlowConfig1 := config.NewFlowConfig("flowName1", common.FlowEnable)
// 3. 建立一個KisFlow物件
flow1 := flow.NewKisFlow(myFlowConfig1)
// 4. 拼接Functioin 到 Flow 上
if err := flow1.Link(myFuncConfig1, nil); err != nil {
panic(err)
}
if err := flow1.Link(myFuncConfig2, nil); err != nil {
panic(err)
}
// 5. 提交原始資料
_ = flow1.CommitRow("This is Data1 from Test")
_ = flow1.CommitRow("This is Data2 from Test")
_ = flow1.CommitRow("This is Data3 from Test")
// 6. 執行flow1
if err := flow1.Run(ctx); err != nil {
panic(err)
}
}
這裡我們透過flow.CommitRow()
提交了3行資料,每行資料是一個字串,當然資料格式可以任意,資料型別也可以任意,只需要在各層的Function業務自身確定拉齊好即可。
cd到kis-flow/test/
下執行命令:
go test -test.v -test.paniconexit0 -test.run TestNewKisFlowData
結果如下:
=== RUN TestNewKisFlowData
context.Background
====> After CommitSrcData, flow_name = flowName1, flow_id = flow-8b607ae6d55048408dae1f4e8f6dca6f
All Level Data =
map[FunctionIdFirstVirtual:[This is Data1 from Test This is Data2 from Test This is Data3 from Test]]
KisFunctionC, flow = &{Id:flow-8b607ae6d55048408dae1f4e8f6dca6f Name:flowName1 Conf:0xc00015a780 Funcs:map[func-2182fa1a049f4c1c9eeb641f5292f09f:0xc0001381e0 func-f3e7d7868f44448fb532935768ea2ca1:0xc000138190] FlowHead:0xc000138190 FlowTail:0xc0001381e0 flock:{w:{state:0 sema:0} writerSem:0 readerSem:0 readerCount:0 readerWait:0} ThisFunction:0xc000138190 ThisFunctionId:func-f3e7d7868f44448fb532935768ea2ca1 PrevFunctionId:FunctionIdFirstVirtual funcParams:map[func-2182fa1a049f4c1c9eeb641f5292f09f:map[] func-f3e7d7868f44448fb532935768ea2ca1:map[]] fplock:{w:{state:0 sema:0} writerSem:0 readerSem:0 readerCount:0 readerWait:0} buffer:[] data:map[FunctionIdFirstVirtual:[This is Data1 from Test This is Data2 from Test This is Data3 from Test]] inPut:[This is Data1 from Test This is Data2 from Test This is Data3 from Test]}
In KisFunctionC, row = This is Data1 from Test
In KisFunctionC, row = This is Data2 from Test
In KisFunctionC, row = This is Data3 from Test
context.Background
====> After commitCurData, flow_name = flowName1, flow_id = flow-8b607ae6d55048408dae1f4e8f6dca6f
All Level Data =
map[FunctionIdFirstVirtual:[This is Data1 from Test This is Data2 from Test This is Data3 from Test] func-f3e7d7868f44448fb532935768ea2ca1:[Data From KisFunctionC, index 0 Data From KisFunctionC, index 1 Data From KisFunctionC, index 2]]
KisFunctionE, flow = &{Id:flow-8b607ae6d55048408dae1f4e8f6dca6f Name:flowName1 Conf:0xc00015a780 Funcs:map[func-2182fa1a049f4c1c9eeb641f5292f09f:0xc0001381e0 func-f3e7d7868f44448fb532935768ea2ca1:0xc000138190] FlowHead:0xc000138190 FlowTail:0xc0001381e0 flock:{w:{state:0 sema:0} writerSem:0 readerSem:0 readerCount:0 readerWait:0} ThisFunction:0xc0001381e0 ThisFunctionId:func-2182fa1a049f4c1c9eeb641f5292f09f PrevFunctionId:func-f3e7d7868f44448fb532935768ea2ca1 funcParams:map[func-2182fa1a049f4c1c9eeb641f5292f09f:map[] func-f3e7d7868f44448fb532935768ea2ca1:map[]] fplock:{w:{state:0 sema:0} writerSem:0 readerSem:0 readerCount:0 readerWait:0} buffer:[] data:map[FunctionIdFirstVirtual:[This is Data1 from Test This is Data2 from Test This is Data3 from Test] func-f3e7d7868f44448fb532935768ea2ca1:[Data From KisFunctionC, index 0 Data From KisFunctionC, index 1 Data From KisFunctionC, index 2]] inPut:[Data From KisFunctionC, index 0 Data From KisFunctionC, index 1 Data From KisFunctionC, index 2]}
In KisFunctionE, row = Data From KisFunctionC, index 0
In KisFunctionE, row = Data From KisFunctionC, index 1
In KisFunctionE, row = Data From KisFunctionC, index 2
--- PASS: TestNewKisFlowData (0.00s)
PASS
ok kis-flow/test 0.636s
經過日誌的詳細校驗,結果是符合我們預期的。
好了,目前資料流的最簡單版本已經實現了,下一章我們將Function的業務邏輯開放給開發者,而不是寫在KisFlow框架中.
3.5 【V0.2】原始碼
https://github.com/aceld/kis-flow/releases/tag/v0.2
作者:劉丹冰Aceld github: https://github.com/aceld
KisFlow開源專案地址:https://github.com/aceld/kis-flow
連載中...
Golang框架實戰-KisFlow流式計算框架(1)-概述
Golang框架實戰-KisFlow流式計算框架(2)-專案構建/基礎模組-(上)
Golang框架實戰-KisFlow流式計算框架(3)-專案構建/基礎模組-(下)
Golang框架實戰-KisFlow流式計算框架(4)-資料流