Doris 写入报 -235:别急着调大版本上限,先把高频小导入合并掉

举报
luckyz 发表于 2026/11/04 18:22:04 2026/11/04
【摘要】 某制造企业的设备状态明细表刚上线时跑得很顺,上线第三周开始,写入端隔三差五收到失败返回,BE 日志里出现这样一行报错:[E-235] Failed to init rowset builder官方导入 FAQ 对这个报错的解释很直接:导入频率太高,数据还没来得及被合并,单个 tablet 的版本数就超出了上限。也就是说,问题不是某一批数据写坏了,而是写入的节奏把存储层拖垮了。这篇文章就讲清楚...

某制造企业的设备状态明细表刚上线时跑得很顺,上线第三周开始,写入端隔三差五收到失败返回,BE 日志里出现这样一行报错:

[E-235] Failed to init rowset builder

官方导入 FAQ 对这个报错的解释很直接:导入频率太高,数据还没来得及被合并,单个 tablet 的版本数就超出了上限。也就是说,问题不是某一批数据写坏了,而是写入的节奏把存储层拖垮了。这篇文章就讲清楚:版本数是怎么涨上去的,以及为什么调大上限只能算缓刑。

一个导入,就是一个版本

Doris 的存储引擎基于 LSM 结构,写入并不直接改写旧文件,而是每次导入产生一个新的 Rowset,也就是 tablet 的一个新版本。后台的 Compaction 会不断把多个小 Rowset 合并成大的 Rowset,版本数随之下降。只要导入速度和合并速度基本平衡,版本数就会在一个区间里波动。

一旦写入端改成“来一条写一次”或者“每秒写一批”,平衡就被打破了。假设一张表按设备编号分桶,一个分桶对应的 tablet 每秒收到一次导入,一分钟就是 60 个新版本,一小时就是 3600 个。Compaction 再勤快,也很难长期追上这种节奏。版本数越积越多,查询要同时读的文件也越来越多,最终写入被直接拒绝,报错就是 -235。

所以看到 -235,先不要条件反射地改配置,先回答三个问题:导入到底有多频繁、Compaction 有没有跟上、是所有 tablet 都超限还是个别 tablet 超限。如果是个别 tablet 超限,先看分桶键是不是倾斜了,比如绝大部分设备状态都落在同一个分桶里;如果是全表普遍超限,问题基本就在写入节奏上。

先算一笔账:你的导入频率是多少

定位时最有用的不是报错本身,而是写入端的导入次数。把写入日志按分钟汇总,看一分钟内对同一张表发起了多少次 Stream Load。如果一分钟几十次甚至上百次,基本可以断定是节奏问题。

下面是一张典型的设备状态明细表,分桶键用设备编号,副本数按常规取 3:

CREATE TABLE ods.device_status (
    event_time DATETIME NOT NULL,
    device_id VARCHAR(64) NOT NULL,
    status_code INT,
    temperature DECIMAL(10, 2)
)
DUPLICATE KEY(event_time, device_id)
DISTRIBUTED BY HASH(device_id) BUCKETS 16
PROPERTIES (
    "replication_num" = "3"
);

这张表本身没有问题,问题出在写入方式上。原来每采集到一批设备状态就立刻发起一次 Stream Load,数据量常常只有几十行。下面这段攒批逻辑是更合理的节奏:攒够 10000 行或者满 30 秒,先到者先发一批,单批行数和时间窗口都是经验值,可以根据业务延迟要求再调:

buffer = []
last_flush = time.time()

def append_record(record):
    buffer.append(record)
    if len(buffer) >= 10000 or time.time() - last_flush >= 30:
        flush(buffer)
        buffer.clear()

batch_seq = 0

def flush(rows):
    global batch_seq
    batch_seq += 1
    label = f"device_status_batch_{batch_seq}"
    with open("/tmp/device_status_batch.csv", "w") as f:
        for row in rows:
            f.write(",".join(str(v) for v in row) + "\n")
    subprocess.run([
        "curl", "--location-trusted", "-u", "root:",
        "-H", f"label:{label}",
        "-H", "column_separator:,",
        "-T", "/tmp/device_status_batch.csv",
        "http://doris-fe:8030/api/ods/device_status/_stream_load",
    ], check=True)

把每秒一次改成每 30 秒一次,版本产生速度直接降为原来的三十分之一,Compaction 才有机会把版本数压回去。攒批的代价是数据可见延迟从秒级变成 30 秒级,对设备状态这类准实时分析来说通常可以接受;如果业务要求秒级可见,就不要用这招硬攒,改走下面这条路。

Label 是用来防重放的,不是用来凑数的

攒批之后还有一个容易踩的坑:重试。Stream Load 的 label 在同一个库内唯一,同一批数据用同一个 label 重发只会被接受一次,这是 Doris 保证至多一次语义的关键机制。攒批逻辑里如果每次重试都换一个新 label,一次失败的导入重试三次,就会变成三个版本,白白抵消攒批的效果。

正确的做法是:label 按批次生成,同一批数据的首次发送和重试用同一个 label;不同批次用不同 label。比如按批次时间窗口生成 label,同一窗口的数据重试时 label 不变。这样即使网络抖动导致客户端以为失败、实际服务端已经提交,重发也不会重复写入。

如果写入走的是 INSERT 小批量而不是 Stream Load,还可以评估 Group Commit。它把多次小写入在服务端攒成一次提交,从源头上减少版本数。建表时有两个相关的表属性,官方文档给出的默认值分别是:group_commit_interval_ms 默认 10000 毫秒,也就是 10 秒;group_commit_data_bytes 默认 134217728 字节,也就是 128MB。两者先到者先触发提交:

CREATE TABLE ods.device_status_group_commit (
    event_time DATETIME NOT NULL,
    device_id VARCHAR(64) NOT NULL,
    status_code INT,
    temperature DECIMAL(10, 2)
)
DUPLICATE KEY(event_time, device_id)
DISTRIBUTED BY HASH(device_id) BUCKETS 16
PROPERTIES (
    "replication_num" = "3",
    "group_commit_interval_ms" = "5000",
    "group_commit_data_bytes" = "67108864"
);

这里把间隔从默认 10 秒收紧到 5 秒、数据量从默认 128MB 收紧到 64MB,是一组偏向低延迟的经验值:提交更频繁,可见性更好,但每次提交的数据量更小。如果业务能接受 10 秒延迟,直接用默认值更省事,不必为了调参而调参。

实在顶不住,再动版本上限

如果攒批和 Group Commit 都做了,个别高峰时段仍然偶发 -235,才轮到调上限。BE 配置 max_tablet_version_num 限制单个 tablet 的版本数,官方 BE 配置文档给出的默认值是 2000,官方导入 FAQ 建议一般不要超过 5000。把它写进 be.conf:

max_tablet_version_num=3000

从 2000 放宽到 3000,只是在给 Compaction 多争取一点追赶时间,版本数的增长速度本身没有变。如果写入节奏不改,版本数迟早会再次撞上新的上限,而且版本数越高,查询要合并的文件越多,Compaction 的负担也越重。这就是为什么说调上限是缓刑:它能让系统在高峰期不立刻拒绝写入,但救不了长期失衡的写入节奏。

调完之后要盯两个指标:版本数是不是回落了,以及 Compaction 有没有积压。如果版本数回落后又开始单边上涨,说明攒批还不够,继续放宽上限只会把问题拖大。

小结

[E-235] Failed to init rowset builder 的本质,是导入产生的版本数涨得比 Compaction 合并的速度快。排查时先算导入频率,再看是个别 tablet 还是全表超限;处理时先攒批,把每秒一次的小导入合并成每 30 秒或每万行一批,重试时同一批数据用同一个 label 防重放,走 INSERT 小批量写入的表可以评估 Group Commit,用默认 10 秒和 128MB 的提交阈值起步。最后才是把 max_tablet_version_num 从默认 2000 适度放宽到 3000 以内、且一般不超过 5000 的经验区间。顺序不能反:先治写入节奏,再动上限,版本数才真正降得下来。

【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。