日志存储基础:行存储 vs 列存储
做审计日志存储,你第一个要面对的选择就是:用行存还是列存?
很多人觉得这不就是个存储格式吗,随便选一个能跑就行。但我见过太多项目,一开始用行存很爽,后来日志量一上去,查询慢到怀疑人生,最后不得不迁移,代价巨大。所以这一章,我们来把行存储和列存储的底裤扒干净。
行存储:写入快,但查询吃大亏
行存储,说白了就是一条日志记录挨着一条存。比如一条审计日志有时间、用户、事件类型、IP、详情等字段,行存储会把它们整整齐齐摆在一行里,连续写进磁盘。
优点很明显:写入速度极快。我习惯用行存储处理高频写入场景,因为不需要考虑字段拆分,直接 append 就行。另外,如果你要读取一整条日志(比如通过主键查某条记录),行存储也最高效,一次磁盘 IO 就能拿到全部字段。
缺点也很致命——分析型查询性能极差。你想想看,如果你想统计过去24小时内“登录失败”事件的总数,行存储必须把涉及的时间段内所有行都读进内存,即使你只关心 event_type 这一个字段,也要把整行都带出来。IO 浪费非常严重。
列存储:查询快,但写入需要技巧
列存储的思路正好反过来:把同一列的所有值连续存储在一起。比如事件类型字段,所有日志的 event_type 会存在一个独立的数据块里,时间字段存在另一个块。
这样做的好处是,分析查询时只需要读取你关心的列。比如前面那个统计登录失败事件的查询,列存储只需要读取 event_type 那一列,其他字段根本不会碰磁盘。压缩效果也更好——同一列的数据类型相同,重复值更多,压缩比动辄5:1甚至10:1。
列存储的另一个痛点是什么?点查慢。如果你要根据唯一ID查一条完整的日志,列存储需要从每个列文件读取对应位置的数据,然后拼接成行。IO 次数等于列数,性能远不如行存储。
适用场景对比:一张表说清楚
| 维度 | 行存储 | 列存储 |
|---|---|---|
| 写入模式 | 追加写入,高性能 | 列拆分写入,有放大效应 |
| 单行点查 | ★☆☆☆☆ 极快 | ★★★☆☆ 较慢(需拼接) |
| 范围查询 | ★★★☆☆ 一般(读全行) | ★★★★★ 极快(只读列) |
| 聚合分析 | ★★☆☆☆ 慢 | ★★★★★ 快 |
| 压缩率 | 低(字段混合,重复少) | 高(同类型,可字典编码) |
| 典型引擎 | MySQL InnoDB,PostgreSQL | ClickHouse,Parquet,ORC |
我踩过的坑:选型不当的代价
我记得在做一个安全审计平台时,初期直接用MySQL Innodb存审计日志。刚开始每天几百万条,写入和查询都还行。到了后来日增5000万条,一张表超过10亿行。业务需要统计“每个用户最近7天触发了多少次高危事件”。行存储查询一次要90秒,业务方忍不了。
后来我改用ClickHouse的MergeTree引擎(列存储),把数据按天分区。同样查询,秒级返回。但代价是什么?实时写入需要先写到Kafka做缓冲,再批量刷到ClickHouse。写一条查一条的场景彻底废了,还好我们回查需求不多。
嗯,选型没有银弹。你必须在写性能、读模式、分析需求之间做权衡。下面这张图把行存储和列存储的物理布局画清楚了,每次选型我建议先从这张图入手思考。
如何选?我常用的决策树
在实际项目中,我习惯按下面几步做判断:
- 先看写入模式:是实时逐条写入,还是批量化加载?实时写入优先考虑行存,批处理优先列存。
- 再查查询需求:是不是超过80%的查询都是“按时间范围统计某个事件类型的数量”?如果是,列存几乎是唯一选项。
- 确认回查频率:有没有大量“根据log_id查完整记录”的场景?如果有,需评估列存的拼接代价。
- 考虑存储成本:日志量大,磁盘空间敏感?列存储压缩通常能省3-5倍空间。
曾经为了图省事,所有日志都往一个行存储表里塞。结果到了BI看板阶段,查询慢到业务方投诉。后来不得不在行存储上层加了一层物化视图,才勉强顶住。其实当初如果一开始就区分热数据和冷数据——热数据行存支持写,冷数据转列存做分析——就不会这么被动。你想想看,这个教训换成存储成本,其实很值得提前规划。