写入模型本质:每次插入都生成一个部件
ClickHouse 的 MergeTree 家族有个硬规则:每一条 INSERT 语句都会在磁盘上生成一个不可变的数据部件,部件里带稀疏主键索引、列压缩块和元数据。后台线程会异步把这些小部件合并成更大的部件,合并越及时,查询时的部件扫描越少。
这个机制决定了写入优化的唯一核心矛盾:插入次数太多,部件数暴涨,合并线程忙死,查询也跟着变慢;插入批次太大,端到端延迟高,客户端内存压力上升。天翼云托管实例在后台会调合并线程池和部件数告警阈值,但源头仍在客户端——你以什么节奏、什么体量塞数据进去,直接决定实例健不健康。
和联机事务库的另一点不同是:ClickHouse 默认插入是同步且幂等的,事务回滚、行级锁这些概念不存在。写入失败重发同一批数据通常是安全的,只要批次内容和顺序没变。这个特性让网络断了重发在 ClickHouse 世界里不是洪水猛兽,而是被设计接纳的常态。
还有一个容易被忽略的事实:每个部件除了数据文件,还附带索引文件和标记文件。小部件虽然数据量小,但索引和标记的固定开销并不少。因此大量小部件不仅合并压力大,磁盘空间利用率也低。这也是为什么控制部件数不仅仅是性能问题,也是存储成本问题。
客户端攒批:把单行提交改成定时刷盘
最容易踩的坑是每产生一条消息就 INSERT 一次。在 ClickHouse 里这意味着每条消息生成一个部件,几千条消息就能把部件数顶到后台合并处理不过来的地步。正确做法是客户端在内存里积攒,按行数或按时间双阈值触发刷盘。
行数阈值一般放在一个不算小也不算畸形的区间,太少没意义,太多客户端内存顶不住。时间阈值用来兜底低峰期——流量稀的时候不能等行数攒够才发,否则数据躺在你内存里好几秒都不落库。实际工程中常把时间阈值设得比行数阈值更保守,保证近实时。
攒批时还有两个隐藏收益。其一,批次在客户端按表的主键排序键预排序,服务端可以跳过排序步骤直接写部件,摄入速度明显提升。其二,批次整体压缩后再走网络,原生二进制格式比逐行 JSON 文本省带宽且服务端解析轻——JSON 每行都要解析字段名和类型,二进制列格式服务端几乎不用解析。天翼云实例同时暴露原生 TCP 与 HTTP 接口,高吞吐写入走原生接口配合二进制格式是默认推荐。
攒批的实现有多种方式。如果你使用官方客户端,它通常内置了攒批能力,只需要配置合适的行数阈值和时间阈值即可。如果你使用自定义客户端,需要在应用层维护一个缓冲区,达到阈值时触发刷盘。无论哪种方式,核心原则是一样的:不要让 ClickHouse 直接面对单行插入。
服务端落盘:从接收到写部件的完整路径
一条批量 INSERT 到达服务端后,经历接收、解压、按格式解析、按表结构拼内存块、按主键排序、建稀疏索引、列压缩、写新部件这几步。其中常量开销与批次大小无关,这也是为什么大批次能把固定开销摊薄——小批次时固定开销占比畸高,大批次时几乎可忽略。
分区键在这里参与决定部件归属。按天分区的表,跨天的批次会拆成多个部件分别落对应分区。因此客户端攒批时若可能跨分区边界,要么在业务层按分区键分桶攒批,要么接受服务端拆部件的额外代价。排序键则决定部件内部行的物理顺序,直接影响查询时的主键裁剪效率,写入期把同排序键的数据聚在一起,对后续查询是双赢。
天翼云控制台能看到的部件数、合并中部件数、后台合并耗时这几个指标,本质就是观察这条落盘链路的体检表。部件数持续高位不降,说明要么写入太碎,要么合并线程资源被查询挤压。
还有一个调优点:服务端写入时的排序步骤是可以跳过的。如果客户端在发送前已经按排序键排好了序,服务端检测到数据有序后可以直接跳过排序,节省大量CPU。这也是为什么推荐客户端预排序的原因之一——不仅节省自己的CPU,还节省服务端的CPU。
异步插入与缓冲表:客户端不能攒批时的兜底
有些场景客户端天然没法攒批——成千上万个边缘设备各自上报心跳、可观测性采集器零散打点、突发流量下攒批会撑爆内存。这时候硬套客户端攒批反而引火烧身。ClickHouse 提供两条兜底路:异步插入和缓冲表。
异步插入把攒批动作委托给服务端。客户端发来的小 INSERT 先进服务端内存缓冲区,由服务端按自身阈值合并成批次再写部件。代价是数据在缓冲区里那一小段时间不保证立即可查,且缓冲区受服务端内存约束,极端情况下可能丢。它适合不在乎这几秒延迟、但别让客户端复杂化的场景。
缓冲表则是另一种思路:建一张 Buffer 引擎表叠在真实 MergeTree 上,所有 INSERT 先写缓冲表,缓冲表按层数、时间、行数、字节数四组阈值把数据刷进底表。它把突发小写入在表引擎层平滑成大批次,对应用完全透明,应用还是 INSERT 缓冲表,不感知底层在攒批。天翼云实例里若跑日志类高突增流量,缓冲表比应用侧自己攒批更稳,因为刷盘逻辑在数据库内闭环,不受应用重启影响。
使用缓冲表时需要注意一个细节:缓冲表中的数据在刷到底表之前,如果数据库异常重启,这部分数据会丢失。因此对于不能接受任何数据丢失的场景,缓冲表不是合适的选择。这种情况下应该使用异步插入,并将异步插入的持久化选项打开。
分布式写入路由:本地表还是分布式表
集群场景下写入目标有两个选择。其一是直接 INSERT 本地表,客户端自己按分片键做负载均衡,把数据送到对应分片的主副本,副本同步靠内部参数开启后由 ClickHouse 内部管。这条路最省,没有中间转发跳。
其二是 INSERT 分布式表,客户端随便连哪个节点都行,分布式表负责把行按分片规则转发到正确分片。这条路对应用友好——不用管分片拓扑,但多一跳转发,且如果内部副本同步没开,分布式表自己会往各副本写,造成副本间不一致风险。天翼云托管集群通常建议开启内部副本同步,写本地表;若图省事写分布式表,性能略折损但要接受。
分片键的选择本身也是写入实践的一部分。按用户标识哈希分片能让同用户数据落同分片,利于按用户查询;按时间取模分片能让写入在分片间均匀,但按时间范围查询会扫所有分片。物联网设备上报场景常把设备标识作为分片键,让单设备时间序列尽量局部化。
还有一个工程细节:写分布式表时,如果分片键的计算结果导致数据倾斜,某些分片的写入压力会远大于其他分片。这时候需要检查分片键的选择是否合理,或者在业务层做预处理,让数据分布更均匀。
物化视图的写入副作用:看不见的第二次写
ClickHouse 的物化视图不是查询时算,而是源表 INSERT 时触发,把新增数据按视图定义再写一份到目标表。这意味着你以为只写了一张表,实际上若挂了物化视图,写入链路里悄悄多了一跳——源表部件生成后,视图引擎会把聚合结果写进自己的部件。
这在实时大屏场景是神技:原始明细进源表,按小时聚合的物化视图自动维护,前端查聚合表而非扫明细。但代价是写入放大——源表写一次,视图目标表也写一次,且视图目标表同样受部件数规则约束。高吞吐写入时若挂了三四个物化视图,后台合并压力会成倍上涨。
实践里的克制做法是:明细表只挂必要的、查询真正高频的物化视图;视图目标表用 SummingMergeTree 或 AggregatingMergeTree 让合并期继续聚合,而不是用普通 MergeTree 存重复明细;建视图时锁源表,大表千万别在业务高峰期建。天翼云实例的慢查询与部件数监控能间接暴露视图写得太多的问题——源表写入平稳但集群部件数疯涨,八成是物化视图在背后发力。
物化视图的另一个副作用是写入延迟增加。因为每次写入都要触发视图的计算和写入,源表的写入完成时间会相应延长。对于对写入延迟极其敏感的场景,需要权衡物化视图带来的查询加速收益和写入延迟代价。
排障思路:写入变慢先查什么
写入延迟抬头时,按链路顺序查。
客户端侧看是否退化成单行提交、是否压缩没开、是否走 JSON 格式硬解析。服务端侧看部件数是否超阈值、后台合并是否积压、CPU 是否被合并线程占满。网络侧看是否小批次高频率把 TCP 建连开销吃光——建议插入速率控制在合理范围,别每秒几百次 INSERT。
分布式场景先看是不是写分布式表导致转发跳多、是不是内部副本同步没开造成双写。物化视图场景把视图临时卸掉观察写入是否恢复,恢复就是视图写入放大。
还有一个隐蔽点:表排序键选错,写入期排序成本高,且查询期主键裁剪失效,合并也慢。建表时排序键不是随便选的,它同时决定写入排序代价和查询裁剪效率,是写入链路的前置开关。
部件数持续高位是一个危险信号。它不仅意味着查询变慢,还意味着磁盘空间被大量小部件的元数据浪费。如果发现部件数居高不下,可以先检查写入频率是否过高,再检查合并线程的配置是否合理,最后检查是否有物化视图在背后制造更多部件。
结语
天翼云实时数据库 ClickHouse 的实时写入,不是 INSERT 越快越好,而是把客户端攒批、服务端少建部件、后台合并跟得上、分布式路由不绕路、物化视图不滥用这条链路上每一段的脾气都顺过来。它和联机事务库的世界观相反——这里鼓励大批次、鼓励只追加、鼓励按主键预排序、鼓励把聚合推到写入期用物化视图算好;这里不提供行锁、不提供事务回滚、不保证插入后下一微秒就可查。开发工程师把日志、埋点、设备时序导进 ClickHouse 时,只要记住每次 INSERT 都在造一个部件这个朴素事实,就能避开绝大部分写入事故的坑,让列存引擎的向量化查询优势不被自己写的碎插入拖垮。