|
|
已crjry表为例,主键模型并且分桶为1。现象为:当表为空的时候一开始写入很快,随着数据量增加,StreamLoad变得越来越慢,到最后超时。
|
|
|
|
|
|
## 问题一、主键模型写放大
|
|
|
在Doris中,主键模型(Unique Key Model)的实现方式是 **Merge-on-Write (MoW)**。理解它的工作原理,就能明白为什么您的问题会如此严重。
|
|
|
|
|
|
**Merge-on-Write 工作流程(对于更新或插入操作)**:
|
|
|
|
|
|
1. **读取 (Read)**: 当新数据写入时,Doris需要先根据主键找到对应的旧数据行所在的底层文件。
|
|
|
2. **合并 (Merge)**: 在内存中,将旧数据行标记为删除,并与新数据行合并。
|
|
|
3. **写入 (Write)**: 将合并后的新数据(包含被删除的旧行和新行)写入一个新的文件版本(Rowset)。
|
|
|
|
|
|
这个过程也叫 **“读后写”** 或 **“写放大 (Write Amplification)”**。一次逻辑上的写入,在物理层面变成了一次“读取+写入”的操作,其资源开销远大于直接追加数据的模型(Append Model)。
|
|
|
|
|
|
## 问题二、数据倾斜
|
|
|
1. **底层机制 (Ingredient 1)**: 您使用了**主键模型 (Merge-on-Write)**,这决定了每次写入的成本都很高。
|
|
|
2. **写入模式 (Ingredient 2)**: 您的Java应用在进行**高频次的Stream Load**,这意味着在短时间内有大量的“读后写”请求被触发。
|
|
|
3. **致命缺陷 (The Catalyst)**: 您的表存在严重的**数据倾斜**,导致所有高频写入请求都砸向了**同一个Tablet** (`tablet_id=1173992`)。
|
|
|
|
|
|
## 故障拆解
|
|
|
|
|
|
1. **热点Tablet过载**: 所有高频的、高成本的“读后写”操作全部集中在单一的Tablet上。这个Tablet所在的BE节点的CPU和磁盘I/O被瞬间打满。
|
|
|
2. **Compaction不堪重负**: 每次写入都会为这个Tablet生成一个新的小文件版本(Rowset)。后台的Compaction任务拼命想把这些小文件合并起来,但它的速度远远跟不上写入生成的速度。Compaction和写入操作激烈地争抢系统资源,进一步加剧了BE节点的负载。
|
|
|
3. **写入超时 (Doris端)**: 在极高的负载下,某一个写入事务的最后一步——`Publish Version`(让版本生效),无法在规定时间内完成。于是,Doris BE日志中出现了第一个关键错误:`PUBLISH_TIMEOUT`。
|
|
|
4. **版本链断裂**: 这个超时的事务虽然在FE(元数据)层面可能被认为是成功的,但在BE(数据)层面,它的版本没有被成功应用。这就造成了Tablet版本链上的一个“空洞”。例如,成功了版本`325762`,但版本`325763`超时失败了。
|
|
|
5. **连锁失败 (Doris端)**: 后续所有发往这个Tablet的写入请求,比如想写入版本`325764`,都会发现`325763`版本缺失,于是Doris拒绝写入,并抛出第二个关键错误:`version not continuous`。
|
|
|
6. **请求超时 (客户端)**: 与此同时,您的Java应用程序还在苦苦等待Doris返回Stream Load的成功响应。但由于Doris内部已经陷入“超时->版本不连续”的恶性循环,根本无法处理完这个请求。最终,Java客户端的HTTP `ReadTimeout`被触发,应用日志中抛出了我们看到的异常:`java.net.SocketTimeoutException: Read timed out`。
|
|
|
|
|
|
**结论:主键模型对数据倾斜和高频更新场景极其敏感**
|
|
|
|
|
|
## 解决办法
|
|
|
|
|
|
在无法优化导入的前提下:
|
|
|
1. streamload 发起端增加 group_commit 参数
|
|
|
```
|
|
|
# 导入时在 header 中增加"group_commit:async_mode"配置
|
|
|
|
|
|
curl --location-trusted -u {user}:{passwd} -T data.csv -H "group_commit:async_mode" -H "column_separator:," http://{fe_host}:{http_port}/api/db/dt/_stream_load
|
|
|
{
|
|
|
"TxnId": 7009,
|
|
|
"Label": "group_commit_c84d2099208436ab_96e33fda01eddba8",
|
|
|
"Comment": "",
|
|
|
"GroupCommit": true,
|
|
|
"Status": "Success",
|
|
|
"Message": "OK",
|
|
|
"NumberTotalRows": 2,
|
|
|
"NumberLoadedRows": 2,
|
|
|
"NumberFilteredRows": 0,
|
|
|
"NumberUnselectedRows": 0,
|
|
|
"LoadBytes": 19,
|
|
|
"LoadTimeMs": 35,
|
|
|
"StreamLoadPutTimeMs": 5,
|
|
|
"ReadDataTimeMs": 0,
|
|
|
"WriteDataTimeMs": 26
|
|
|
}
|
|
|
|
|
|
# 返回的 GroupCommit 为 true,说明进入了 group commit 的流程
|
|
|
# 返回的 Label 是 group_commit 开头的,是真正消费数据的导入关联的 label
|
|
|
```
|
|
|
|
|
|
group_commit 可以实现在服务端赞批的功能,当size达到表设置的 group_commit_data_bytes 大小时,提交一次,可以节省事务和compaction开销
|
|
|
|
|
|
Group Commit 的默认提交数据量为 64 MB,用户可以通过修改表的配置调整:
|
|
|
```text
|
|
|
# 修改提交数据量为 128MB
|
|
|
ALTER TABLE dt SET ("group_commit_data_bytes" = "134217728");
|
|
|
```
|
|
|
|
|
|
*如果设置太大,数据会缓存到内存中,导致内存占用率高,这里需要注意*
|
|
|
|
|
|
如果设置了group_commit还是出现磁盘占用高的问题,可以尝试临时关闭表压缩功能
|
|
|
```text
|
|
|
ALTER TABLE dt SET ("disable_auto_compaction" = "true");
|
|
|
```
|
|
|
设置完成后,这张表的tablet不会压缩,但是会占用大量磁盘空间,非紧急情况不要使用。 |