Golang框架實戰-KisFlow流式計算框架(4)-資料流

aceld發表於2024-02-28

連載中...
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{} 也就是KisBatch
  • data: 流式計算各個層級的資料來源
  • 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中我們下面來簡單模擬一下業務的基礎計算邏輯。

我們暫時修改KisFunctionCKisFunctionE 兩個模組的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)-資料流

相關文章