FlinkX 分片讀取原理
在數據同步工具中,將數據從源頭讀取到數據緩存是最重要的一環之一,算是左膀
。所以在整個流程,從技術場景上,一定要支持數據的分片與並行讀取、流控,從業務角度上,需要支持髒值處理與增量讀取。
而今天重點來探討一下分片讀取的原理。
分片原理
分片是將待讀取的數據平均分配,儘量的使各個分片任務均衡,不會讓數據傾斜從而導致個別節點的同步壓力過大(硬件-網卡、cpu等)。
下面是配置了一個讀取通道爲3的作業配置示例:
"speed": {
"channel": 3,
"bytes": 0
},
重點類&方法
-
InputSplit
(輸入分片類)表示輸入的分片,並且會在運行過程中進行傳輸,所以需要進行序列化,是Flink的數據讀取核心類。 -
BaseRichInputFormat#createInputSplits
創建分片,會對錯誤進行捕獲,包裝輸出,此方法實際是FLink中的InputSplitSource
org.apache.flink.api.common.io.InputFormat.java org.apache.flink.core.io.InputSplitSource.java 由上可以的得知,真實的分片邏輯有具體的實現子類進行提供,將
InputSplit
結果返回給調度系統,而分片的調度由Flink底層進行提供(因爲reader讀取數據返回的是DataStream)。如下圖所示的關係Flink、FlinkX在分片邏輯中的關係 -
BaseRichInputFormat#createInputSplitsInternal
創建實際的分片抽象方法,由實際driver創建
通用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;
}
如上述代碼所示:執行邏輯解釋如下
邏輯1: 如果有splitKey的話,會按照Key進行分片,沒有Source 會將數據分爲numPartions
份,每一個channel讀取對應的一份數據。
**邏輯2:**將配置的source分爲多份。每個channel讀取對應的source列表
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()]);
}
- 獲取所有當前collection的count,即發出命令如:
{"aggregate": "bond_info", "pipeline": [{"$match": {}}, {"$group": {"_id": 1, "n": {"$sum": 1}}}], "cursor": {}}
- 然後按照
minNumSplits
進行分片,如果分片取餘不爲0,那麼會創建minNumSplits+1
個分片,這裏沒有考慮多個source 的情況。
並行讀取
並行讀取指的先由分片邏輯分完片返回分片結果後,執行引擎爲每一個分片創建一個讀取通道,從而達到數據並行到達源的效果。
很多人稱之爲併發讀取,個人理解還是稱之爲並行讀取更合適,因爲從任務的時間維度上來,屬於並行執行。當然可能從數據源的角度上來講,這裏屬於併發讀取,不對這些概念做些無聊的爭執。後續如果有些並行讀取或者併發讀取指的都是同一個概念。
從上一節的關鍵類InputSplitSource
裏面可以看出來,其實並行讀取底層還是Flink在支持的。所以這裏不做過多的探討,後續Flink關鍵分析裏面見。