FlinkX 分片讀取原理

FlinkX 分片讀取原理

在數據同步工具中,將數據從源頭讀取到數據緩存是最重要的一環之一,算是左膀。所以在整個流程,從技術場景上,一定要支持數據的分片與並行讀取、流控,從業務角度上,需要支持髒值處理與增量讀取。

image-20200523220805741

而今天重點來探討一下分片讀取的原理。

分片原理

分片是將待讀取的數據平均分配,儘量的使各個分片任務均衡,不會讓數據傾斜從而導致個別節點的同步壓力過大(硬件-網卡、cpu等)。

下面是配置了一個讀取通道爲3的作業配置示例:

"speed": {
     "channel": 3,
     "bytes": 0
},

重點類&方法

  • InputSplit (輸入分片類)表示輸入的分片,並且會在運行過程中進行傳輸,所以需要進行序列化,是Flink的數據讀取核心類。

    image-20200523223305856

  • BaseRichInputFormat#createInputSplits 創建分片,會對錯誤進行捕獲,包裝輸出,

    image-20200523223659952

    此方法實際是FLink中的InputSplitSource

    image-20200523223808762

    org.apache.flink.api.common.io.InputFormat.java

    image-20200523223909531

    org.apache.flink.core.io.InputSplitSource.java

    由上可以的得知,真實的分片邏輯有具體的實現子類進行提供,將InputSplit結果返回給調度系統,而分片的調度由Flink底層進行提供(因爲reader讀取數據返回的是DataStream)。如下圖所示的關係

    image-20200523224812698

    Flink、FlinkX在分片邏輯中的關係
  • BaseRichInputFormat#createInputSplitsInternal 創建實際的分片抽象方法,由實際driver創建

    image-20200523223248125

通用JDBC 的分片策略

JDBC分片中的幾個概念:

  • splitKey
  • numPartitions Math.max(speed.channel,speed.readerChannel)

具體實現邏輯及代碼如下:

@Override
    public InputSplit[] createInputSplitsInternal(int minPart) throws IOException {
        DistributedJdbcInputSplit[] inputSplits = new DistributedJdbcInputSplit[numPartitions];

        if(splitKey != null && splitKey.length()> 0){
            Object[][] parmeter = DbUtil.getParameterValues(numPartitions);
            for (int j = 0; j < numPartitions; j++) {
                DistributedJdbcInputSplit split = new DistributedJdbcInputSplit(j,numPartitions);
                List<DataSource> sourceCopy = deepCopyList(sourceList);
                for (int i = 0; i < sourceCopy.size(); i++) {
                    sourceCopy.get(i).setSplitByKey(true);
                    sourceCopy.get(i).setParameterValues(parmeter[j]);
                }
                split.setSourceList(sourceCopy);
                inputSplits[j] = split;
            }
        } else {
            int partNum = sourceList.size() / numPartitions;
            if (partNum == 0){
                for (int i = 0; i < sourceList.size(); i++) {
                    DistributedJdbcInputSplit split = new DistributedJdbcInputSplit(i,numPartitions);
                    split.setSourceList(Arrays.asList(sourceList.get(i)));
                    inputSplits[i] = split;
                }
            } else {
                for (int j = 0; j < numPartitions; j++) {
                    DistributedJdbcInputSplit split = new DistributedJdbcInputSplit(j,numPartitions);
                    split.setSourceList(new ArrayList<>(sourceList.subList(j * partNum,(j + 1) * partNum)));
                    inputSplits[j] = split;
                }

                if (partNum * numPartitions < sourceList.size()){
                    int base = partNum * numPartitions;
                    int size = sourceList.size() - base;
                    for (int i = 0; i < size; i++) {
                        DistributedJdbcInputSplit split = inputSplits[i];
                        split.getSourceList().add(sourceList.get(i + base));
                    }
                }
            }
        }

        return inputSplits;
    }

如上述代碼所示:執行邏輯解釋如下

image-20200523232649004

邏輯1: 如果有splitKey的話,會按照Key進行分片,沒有Source 會將數據分爲numPartions份,每一個channel讀取對應的一份數據。

image-20200523231828682

**邏輯2:**將配置的source分爲多份。每個channel讀取對應的source列表

image-20200523232413314

MongoDB的分片策略實現

 public InputSplit[] createInputSplitsInternal(int minNumSplits) throws IOException {
        ArrayList<MongodbInputSplit> splits = new ArrayList<>();

        MongoClient client = null;
        try {
            client = MongodbClientUtil.getClient(mongodbConfig);
            MongoDatabase db = client.getDatabase(mongodbConfig.getDatabase());
            MongoCollection<Document> collection = db.getCollection(mongodbConfig.getCollectionName());

            long docNum = filter == null ? collection.countDocuments() : collection.countDocuments(filter);
            if(docNum <= minNumSplits){
                splits.add(new MongodbInputSplit(0,(int)docNum));
                return splits.toArray(new MongodbInputSplit[splits.size()]);
            }

            long size = Math.floorDiv(docNum, minNumSplits);
            for (int i = 0; i < minNumSplits; i++) {
                splits.add(new MongodbInputSplit((int)(i * size), (int)size));
            }

            if(size * minNumSplits < docNum){
                splits.add(new MongodbInputSplit((int)(size * minNumSplits), (int)(docNum - size * minNumSplits)));
            }
        } finally {
            MongodbClientUtil.close(client, null);
        }

        return splits.toArray(new MongodbInputSplit[splits.size()]);
    }

  1. 獲取所有當前collection的count,即發出命令如:{"aggregate": "bond_info", "pipeline": [{"$match": {}}, {"$group": {"_id": 1, "n": {"$sum": 1}}}], "cursor": {}}
  2. 然後按照minNumSplits進行分片,如果分片取餘不爲0,那麼會創建minNumSplits+1個分片,這裏沒有考慮多個source 的情況。

並行讀取

並行讀取指的先由分片邏輯分完片返回分片結果後,執行引擎爲每一個分片創建一個讀取通道,從而達到數據並行到達源的效果。

很多人稱之爲併發讀取,個人理解還是稱之爲並行讀取更合適,因爲從任務的時間維度上來,屬於並行執行。當然可能從數據源的角度上來講,這裏屬於併發讀取,不對這些概念做些無聊的爭執。後續如果有些並行讀取或者併發讀取指的都是同一個概念。

從上一節的關鍵類InputSplitSource裏面可以看出來,其實並行讀取底層還是Flink在支持的。所以這裏不做過多的探討,後續Flink關鍵分析裏面見。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章