版本信息:
- Flink 1.17.1
- Doris 1.2.3
- Flink Doris Connector 1.4.0
写入方式
采用 String 数据流,依照社区网站的样例代码,在sink之前将数据转换为DataStream
运行异常
通过Stream Load返回结果json中的ErrorUrl可以看到如题的异常
Reason: actual column number in csv file is less than schema column number. actual number: 10, ..., schema column number: 11; src line: [...]
数据库表明明只有10个字段,提示schema column number却是11个。是自己眼花数错字段了吗?经过反复确认及同事确认,没有错,目标表就是10个字段,我写入的也是10个字段,是Flink Doris Connector 的bug吗?
分析过程
既然怀疑是bug,那就去扒代码。
实际数据写入逻辑封装在org.apache.doris.flink.sink.writer.DorisWriter
,该类实现了org.apache.flink.api.connector.sink.SinkWriter
接口。查看该类发现,写入Doris的过程实际是使用微批写入的。
@Override
public void write(IN in, Context context) throws IOException {
checkLoadException();
byte[] serialize = serializer.serialize(in);
if(Objects.isNull(serialize)){
//ddl record
return;
}
if(!loading) {
//Start streamload only when there has data
dorisStreamLoad.startLoad(currentLabel);
loading = true;
}
dorisStreamLoad.writeRecord(serialize);
}
@Override
public List<DorisCommittable> prepareCommit(boolean flush) throws IOException {
if(!loading){
//There is no data during the entire checkpoint period
return Collections.emptyList();
}
// disable exception checker before stop load.
loading = false;
Preconditions.checkState(dorisStreamLoad != null);
RespContent respContent = dorisStreamLoad.stopLoad(currentLabel);
if (!DORIS_SUCCESS_STATUS.contains(respContent.getStatus())) {
String errMsg = String.format("stream load error: %s, see more in %s", respContent.getMessage(), respContent.getErrorURL());
throw new DorisRuntimeException(errMsg);
}
if (!executionOptions.enabled2PC()) {
return Collections.emptyList();
}
long txnId = respContent.getTxnId();
return ImmutableList.of(new DorisCommittable(dorisStreamLoad.getHostPort(), dorisStreamLoad.getDb(), txnId));
}
每一条记录都会触发write操作,从上述代码可以看到根据boolean变量loading的值,程序将会触发dorisStreamLoad.startLoad(currentLabel);
,而loading的状态在preCommit方法中进行修改,而preCommit是在checkpoint时触发,所以数据提交动作是通过checkpoint触发的。查看startLoad源代码
/**
* start write data for new checkpoint.
* @param label
* @throws IOException
*/
public void startLoad(String label) throws IOException{
loadBatchFirstRecord = true;
HttpPutBuilder putBuilder = new HttpPutBuilder();
recordStream.startInput();
LOG.info("stream load started for {} on host {}", label, hostPort);
try {
InputStreamEntity entity = new InputStreamEntity(recordStream);
putBuilder.setUrl(loadUrlStr)
.baseAuth(user, passwd)
.addCommonHeader()
.addHiddenColumns(enableDelete)
.setLabel(label)
.setEntity(entity)
.addProperties(streamLoadProp);
if (enable2PC) {
putBuilder.enable2PC();
}
pendingLoadFuture = executorService.submit(() -> {
LOG.info("start execute load");
return httpClient.execute(putBuilder.build());
});
} catch (Exception e) {
String err = "failed to stream load data with label: " + label;
LOG.warn(err, e);
throw e;
}
}
DorisStreamLoad类负责将数据实际写入Doris,在上面的代码中我看到了一个陌生的词汇HiddenColumns
,“隐藏列”,什么是隐藏列?.addHiddenColumns(enableDelete)
的参数enableDelete
是一个boolean值,继续扒代码发现,默认值enableDelete = true;
,addHiddenColumn(true)?是否意味着我的put操作数据中必须包含隐藏列?继续扒
public HttpPutBuilder addHiddenColumns(boolean add) {
if(add){
header.put("hidden_columns", LoadConstants.DORIS_DELETE_SIGN);
}
return this;
}
在http请求header中添加了一个配置,似乎是指明了"hidden_columns"="DORIS_DELETE_SIGN",看着好像是一个列名称,使用IDEA的跟踪调用功能,查看下哪里用到了这个变量。
跟踪这些代码更确信,这是一个列名称。我的10列加上这一列就是11列啊,设置enableDelete = false
,是否意味着我的put操作不再包含这一隐含列?
解决方案
修改构造DorisSink的代码添加.setDeletable(false);
DorisExecutionOptions.Builder executionBuilder = DorisExecutionOptions.builder();
executionBuilder.setLabelPrefix(labelPrefix) //streamload label prefix
.setDeletable(false);
重新运行代码,写入成功,问题解决。
总结
出现该异常是因为,Flink Doris Connector 在构造Sink时默认用户写入数据中包含了隐藏列__DORIS_DELETE_SIGN__
。
尽管问题解决了,但是还是有很多疑问,什么是隐藏列,__DORIS_DELETE_SIGN__
这个隐藏列是什么意思,从前面的代码中可以看出其取值为0或1,导入数据时为什么默认需要传递该列,该列在最前面还是在最后面?不传递该列是否会有问题?