拓冰建站拓冰建站
首页 / 资讯中心 / 正文

行式存储的技术变革:从被嫌弃到重回大数据一线

1. 先把“行式存储”这件事说清楚1.1 行式存储是什么一页纸讲透底层逻辑最近圈子里的讨论风向确实变了。前几年聊大数据开口闭口都是列式存储、列压缩、分析型数仓好像不用 Parquet 就没法跟人打招呼。但今年明显感觉不一样了行式存储这个“老家伙”重新被大家翻出来讨论而且不是那种考古式的回顾是真有人在一线项目里拿它解决实际问题。要理解这场“技术变革”得先回到最底层把行式存储这个概念的根基摸清楚。行式存储通俗点说就是数据在磁盘或者内存里按照“一行一行”的方式连续摆放。每一行的所有字段挨在一起写入的时候一次把整行写下去读取的时候如果只要某一个字段也得先把整行从存储介质上拉出来。你可以把一张表想象成一个本子每页记一个人的完整档案姓名、电话、地址全部写在一块翻到哪页看到的就是一个人的全套信息。这个逻辑其实特别贴近人的直觉因为绝大多数的业务系统从一开始就是按“一个对象一条记录”来建模的。MySQL 的 InnoDB 引擎是典型的行式存储实现它的聚簇索引直接把整行数据挂在主键的叶子节点上PostgreSQL 的堆表结构同样如此每一行以 tuple 的形式连续存放。关系型数据库统治了软件行业几十年行式存储几乎是默认选项大家甚至都不会特意去想“我用的到底是个什么存储布局”。但到了大数据这个技术栈里情况就没那么单纯了。HDFS 里躺着的文件、Hive 表对应的数据目录、HBase 里的一张表它们底层的物理组织方式可能完全不一样有的按行连续写有的按列分块存有的干脆每行自己带一堆元数据散落在不同文件里。很多做大数据开发的同学写了几年 SQL其实并不清楚自己查的数据在磁盘上到底是怎么摆的。这不怪谁因为大数据框架把这块封装得太深了加上列式存储的营销话术太猛行存储在大数据语境里一度被讲得像是上个世纪的遗留物。可事实是行式存储从来没有离开过一线。它只是换了个马甲换了个场景以更适应大数据生态的方式重新长了出来。1.2 行存、列存、行列混合的边界在哪里要想搞懂行式存储的“技术变革”光理解行存自己还不够得把它放到和列存、行列混合的对比中去看边界才清晰。我拿一个具体的订单表来说。表里有 order_id、user_id、amount、status、created_at 五列一共一千万行。如果你的业务是“查某个订单的完整详情”SQL 写出来是 where order_id 12345这时候行式存储占尽优势因为订单 12345 的那一行数据在磁盘上是连续存放的主键索引一定位一次 I/O 就能把整行取回来五列一次到位延迟轻轻松松做到毫秒级。但如果你的业务是“统计所有订单的总金额”SQL 写出来是 select sum(amount) from orders这时候行式存储就抓瞎了它必须把这一千万行全部读出来哪怕你只关心 amount 这一列每一行的其他四列也跟着被拖出来白读白白消耗 I/O 和内存。列式存储解决的就是这个痛点。它在磁盘上把同一列的数据放在一起amount 这一列单独占一段连续空间做统计时只需要扫描这一段其他四列碰都不用碰。配合上列级别的压缩算法效果立竿见影扫描的数据量可能只有行存方案的十分之一甚至更低。这也是过去十几年大数据分析场景几乎被列存储“屠杀”的根本原因。Parquet、ORC 这类列式文件格式在 Hive、Spark、ClickHouse 里的统治地位就是这么建立起来的。存储布局 | 写入方式 | 典型读取场景 | 代表系统 | 典型代价 行式存储 | 整行连续写入 | 点查、明细查询、高并发更新 | MySQL、PostgreSQL、HBase、TiKV | 宽表分析时无效 I/O 大 列式存储 | 按列独立存储 | 全表扫描、聚合统计 | Parquet、ORC、ClickHouse | 单行整查需要跨列重组 行列混合 | 两种布局并存 | 点查和分析都要 | TiDB、Doris、Snowflake | 存储与维护成本更高所以你看行式和列式本质上没有谁绝对碾压谁它们各自的优势都建立在“业务到底怎么读数据”这个前提上。所谓行式存储的技术变革真正发生的事是在大数据时代行式存储不再作为列式存储的“对立面”出现而是找到了自己真正不可替代的位置并且在这个位置上做了一系列深度的底层优化。接下来的几个章节我把这场变革的来龙去脉拆开讲。2. 大数据场景里行式存储为什么曾被“嫌弃”过2.1 从“全表扫描”说起分析型负载对行存的天然不友好大数据这个概念火起来的最初几年主流场景几乎都是分析型的。日志分析、用户行为统计、报表计算这些东西的共同特点是数据量大单次查询要扫的表级数据多但每次真正用到的列很少。这种负载对行式存储来说几乎是灾难。我给你算一笔账看完你就明白问题出在哪了。假设一张用户行为宽表有一百列数据量是一千万行单行平均大小是 1KB整张表在磁盘上占 10GB 左右。现在有一条分析 SQL只需要查 user_id、event_type、created_at 三列做分组统计。如果是行式存储查询引擎要把 10GB 的数据全部读一遍然后从每一行里抽出那三列做计算有效数据其实只有 10GB 的百分之三左右也就是 300MB剩下 97% 的 I/O 全部浪费掉了。三列尚且如此如果只查一列浪费比例更夸张。列式存储呢它把每一列单独存储这条 SQL 只需要读取三列对应的数据块假设这三列加起来占存储总量的百分之五那么理论 I/O 只有 500MB 左右缩了二十倍。在大数据量、高并发分析场景里二十倍的 I/O 差距几乎决定了系统能不能扛得住。压缩率也是行存的另一个短板。行式存储把不同数据类型混在一起连续摆放整行作为一个压缩单元时里面既有整数、字符串、时间戳字段类型跨度大压缩算法很难找到规律压缩比通常只有 2:1 到 3:1。列式存储因为同一列的数据类型一致值域分布集中压缩算法可以发挥出四五倍甚至十倍的压缩比一列数字如果都是 0 和 1 的枚举值甚至能压到更夸张的程度。所以早期的数仓选型几乎是一边倒地倒向列式存储Hadoop 生态里 Parquet 和 ORC 能迅速取代 SequenceFile 和 TextFile不是没道理的。SequenceFile 就是当年 Hadoop 里典型的行式存储文件格式我早期做数据开发的时候还用过那时候跑一条聚合 SQL盯着监控面板看磁盘吞吐飙到顶但任务就是跑不快最后改成 Parquet 重跑了一遍扫描的数据量直接掉了一个数量级那个冲击感到现在还记得。2.2 但行存从未退场点查、写入与事务是它的护城河如果列式存储真能通吃一切那今天的标题也不会是“行式存储的技术变革”而是“行式存储的墓志铭”。但现实是列式存储也有自己的软肋而且这几个软肋恰好都戳在大数据业务最痛的地方。第一个软肋是点查。所谓点查就是按某个 key 精确匹配一条或者少数几条记录比如“查订单 12345 的物流状态”“查用户 678 的账户余额”。这类请求单次涉及的数据量很小但对延迟极其敏感通常在几十毫秒以内必须返回。列式存储在这类场景里表现很糟糕因为数据按列分散存储取一条完整记录需要把散落在各处的列片段重新拼装回来涉及多次随机 I/O拼装成本很高。而主流的 OLAP 引擎本来也不是为这种查询设计的没有高效的索引机制来支撑点查。反观行式存储数据是连续摆放的配合主键索引或者哈希索引一次定位、一次读取整行数据就出来了。第二个软肋是高频写入和更新。列式存储为了压缩效率和扫描性能数据文件通常做成不可变或者极少变动的写入要经过缓冲区攒批、排序、定期合并这样一套流程单条随机写入的成本很高更新就更麻烦了本质上是标记删除加异步重写。而行式存储天然支持就地写入一条记录插进来直接落到对应位置配合事务日志和锁机制可以做到每秒一二十万甚至更高的单库写入吞吐。银行转账、电商下单、IM 消息这种业务底层几乎清一色行式存储原因就在这。第三个软肋是事务。行式存储从关系型数据库时代就带着完善的事务能力ACID 语义、行级锁、MVCC这套东西在 OLTP 场景里打磨了几十年成熟度极高。列式存储的很多引擎连简单的事务支持都做不好多表关联的强一致更新更是奢望。所以行式存储从来没有被“淘汰”它只是在大数据分析这个细分场景里被边缘化了。可一旦业务需求从单一的离线分析扩展到实时查询、高并发写入、事务处理行式存储的价值立刻就会凸显出来。技术变革的前提条件已经明确了行存有不可替代的场景列存有难以逾越的瓶颈那接下来要解决的就是“怎么让行存在大数据量下变得更快、更稳、更省”。3. 技术变革的核心行式存储这几年到底变了什么3.1 硬件与生态的底层推力如果你去翻十年前的论文和博客会发现当年讨论行式存储性能问题的时候大家默认的前提是内存贵、磁盘慢、CPU 性能有限。而技术变革最根本的推动力恰恰是这些前提这几年全变了。内存价格一路下跌一台普通的存储型服务器配 512GB 内存已经不是什么稀罕事1TB 内存的机器在大型互联网公司也不少见。NVMe SSD 普及之后顺序读写的吞吐跑到 3GB/s 以上随机读的 IOPS 从机械硬盘的几百涨到几十万级。这意味着什么意味着“把整张表扫描一遍”这个操作的成本比十年前下降了至少一两个数量级。行式存储最被人诟病的全表扫描问题在新的硬件条件下虽然没有彻底消失但已经不是那种让人绝望的致命伤。更关键的是大数据生态的底层架构变了。早期的 Hadoop 体系设计目标很简单粗暴用一堆廉价机器扛住海量数据算不过来就堆机器。这种设计思路下存储和计算都被迫往“顺序扫描优先”的方向走列式存储的赢面自然最大。但现在的大数据架构不再无条件地牺牲延迟换吞吐实时数仓、湖仓一体、HTAP 这些概念流行起来之后系统需要同时满足“大规模数据分析”和“低延迟在线查询”两种需求。做一个能跑离线报表的数仓已经不够了你最好还能让业务方直接查明细、查单条、做实时更新。这种需求倒逼着存储层要重新审视行式存储的价值。还有一个被很多人忽略的生态信号数据湖体系里的表格式比如 Hudi、Iceberg、Delta Lake它们在底层是把数据文件按主键做了排序布局的有的还支持主键级别的小文件更新和点查。这些表格式的底层文件虽然是列式的但它们的更新索引、position delete、morphing 这类机制实际上是借鉴了大量行式存储的思路。换句话说行式存储的理念正在以一种隐蔽的方式渗透进列式存储的地盘。3.2 存储引擎内部的自我革命如果说硬件和生态是外部推力那存储引擎内部的优化才是这场变革真正的内核。这部分的改进我把它总结成三个方向读写模型、索引机制、数据格式。读写模型方面LSM-Tree 的全面普及是绕不开的一个里程碑。LSM-Tree 的设计理念很有意思它把随机写转换成顺序写写入先进内存里的 MemTable攒到一定程度再顺序落盘成 SSTable后台再用异步 compaction 把多个 SSTable 合并成一个更大的。这套模型天然适合行式存储因为数据从写入到落盘始终是以“完整的一行”为单位的读的时候配合内存中的行索引和布隆过滤器可以快速过滤掉无关的 SSTable点查效率非常可观。HBase 的 HFile、TiKV 的 RocksDB 底层都是这套机制的具体实现。我可以负责任地说没有 LSM-Tree行式存储在大数据量下根本没法做到既保持高吞吐写入又维持毫秒级的读取延迟。索引机制上布隆过滤器和大内存下的行缓存成了标配。布隆过滤器是一个非常节省空间的判断器它告诉你“这个 key 肯定不在这个文件里”或者“这个 key 可能在这个文件里”命中后能直接跳过大量无关的 SSTable 文件。行缓存则把最近访问过的行直接放在内存里配合热数据的前缀压缩极大缓解了重复读取的 I/O 压力。数据格式层面的变化更值得一提。早期的 HBase 底层直接存字节数组靠着开发者自己控制序列化方式灵活但容易踩坑。现在的主流行存系统都有了成熟的行编码格式比如 Avro、Thrift、Protobuf 序列化后的行数据以及类似 TiDB 中按主键排序的行编码结构。这类格式在每个字段前附加元数据信息既能单独访问某一列又保持整行连续存储某种程度上已经是“行存里的列存雏形”了。3.3 行列混合与智能路由把行存放回它该在的位置行式存储真正的“技术变革”我认为既不是硬件升级也不是格式改良而是它和列式存储终于在大数据体系里找到了和平共处的方式也就是行列混合架构。这里面的思路其实很朴素同样的数据在行式存储里放一份服务于高频点查和高并发写入在列式存储里放一份服务于大规模分析扫描。系统层自动把数据同步到两份存储中查询引擎根据 SQL 的特点自动路由到合适的存储或者在底层把两边的结果合并返回。听起来简单做起来很难但真正落地这件事的系统这几年已经纷纷冒出来了。TiDB 的 TiKV 和 TiFlash 是典型代表。TiKV 是一个基于 RocksDB 的行式存储引擎负责事务、点查、高并发写入TiFlash 是列式存储通过 Raft Learner 机制从 TiKV 实时同步数据专门处理分析型查询。应用发一条 SQL 过来优化器判断它适合走行存还是列存自动选择执行路径。用户不需要关心数据在哪个存储里只需要写 SQL 就行这就是 HTAP 要做的事。Doris 的 Unique Key 模型走的也是行存和列存配合的路线。数据在列式存储的主体上额外为高频更新的 key 建立行式索引实现低延迟的实时更新。ClickHouse 里同样有相关的探索MergeTree 家族在部分场景下比如低频更新、频繁点查把单行数据放到紧凑格式里存储配合主键索引快速定位效果也相当好。这套“智能路由”的设计让我觉得行式存储终于不用再和列存打擂台了。过去做架构选型要么选行存数据库忍受分析能力的缺失要么选列存数仓忍受点查和写入的拉胯。现在最前沿的做法是存储层打架查询层调停应用层无感。这才是真正意义上的技术变革——不是某一种存储赢了而是两种存储的边界被重新划定并且由系统而不是人来维护这条边界。但话说回来行列混合不是银弹它带来存储成本的翻倍、同步延迟的复杂度、查询路由的挑战。所以实际项目里到底怎么用行式存储还是得回到业务需求去判断。4. 实操视角项目里怎么选型与落地行式存储4.1 先做一次需求体检用 5 个问题判断要不要行存纸上谈兵容易真到了项目评审会上被问到“你这个场景到底该用行式存储还是列式存储”没点实操判断依据确实容易卡壳。我在实际项目里总结了一套“需求体检”流程一共五个问题答完基本就能确定方向。第一个问题这个数据的读取模式是什么如果百分之八十以上的查询都是按主键取单个或少量记录比如按用户 ID 查用户详情、按设备 ID 查最新状态那八成需要行式存储。反过来如果查询基本都是扫大范围数据做聚合、做报表那就老老实实走列存。第二个问题数据怎么写入的高并发随机写、频繁更新已有记录、有事务要求这些需求会快速排除掉传统列存引擎。行式存储和 LSM-Tree 架构的数据库在这类场景里几乎是唯一选择。第三个问题单条记录有多大这里有个经验值如果单行数据大小超过几十 KB比如存了用户的完整画像 JSON、实验数据详情列式存储的行重组开销会非常大而行式存储天然适合处理这种大行数据。反过来如果每行就几个字段列存通常是更好的选择。第四个问题延迟要求有多高点查延迟要求在 100ms 以内行存没问题要求在 10ms 以内还得配缓存如果能容忍秒级列存也可以凑合但要评估并发数。延迟越敏感行存越有优势。第五个问题要不要跨行事务只要答案是需要基本就锁定到支持事务的行式数据库或者行列混合数据库了。列式存储大多不支持跨行事务这是硬伤不是优化能解决的。把这五个问题的答案落到一张表里选型方向就清晰了核心判断项 | 选行式存储 | 选列式存储 | 选行列混合 读取模式 | 主键点查、明细查询 | 大范围扫描、聚合分析 | 两者都有且占比都不低 写入模式 | 高并发随机写、频繁更新 | 批量追加写 | 实时写入 批量分析并存 单行大小 | 偏大大于几十KB | 偏小几KB以内 | 视具体实现而定 延迟要求 | 毫秒级 | 秒级可接受 | 点查毫秒、分析秒级 事务要求 | 需要 | 不需要或弱一致即可 | 需要并且要接分析4.2 两个落地案例剖析特征服务与轨迹点存储光给理论不给案例和小白读说明书没什么区别。我挑两个我接触过的具体场景把行式存储的落地细节展开讲。第一个场景是用户特征服务。业务方要做一个实时推荐系统需要在用户访问的瞬间根据用户 ID 拉取这个用户的最近几天行为特征拼成特征向量喂给模型。特征是稀疏的每个用户可能有一两百个特征字段但真正非空的可能只有二三十个。数据量是亿级用户需求是 p99 延迟在 30ms 以内每天还有大量的增量特征写入。这个场景如果用列式存储点查延迟大概率在几百毫秒以上根本满足不了要求。我们的方案是选了 HBase 行式存储rowkey 设计成 user_id 反转后的哈希前缀加原始 user_id这样既避免了 user_id 前缀相同导致的热点又能通过 rowkey 精确定位到某一行。特征字段以 Protobuf 序列化后塞进一个 column family 里的不同 qualifier配合 HFile 的布隆过滤器一次 get 请求在缓存命中时能控制在 5ms 内未命中时也就 20ms 左右。这套方案稳定跑了一年多最大的感受是行式存储在大数据量下做点查确实是列存替代不了的。第二个场景是遥感卫星轨迹点存储这个相对小众但很有意思。卫星过境时会持续下传海量的轨迹点数据每个点包含时间、经纬度、高度、姿态角等十几个字段一天下来就是几亿行。业务方要做的是时间窗口查询给定一个时间段和一个空间区域把经过这个区域的所有轨迹点拉出来同时还要支撑在一张大屏上实时展示卫星当前位置。这个场景里每个轨迹点天然就是“一行”查询模式以按时间范围扫描为主偶尔需要按轨道号精查。我们用的是 HBase 加预分区rowkey 设计成“时间反转加卫星编号”这样同一颗卫星同一时段的轨迹点物理上相邻时间范围查询通过一次 scan 就能高效完成。空间过滤放到查询层做把区域匹配转换成经纬度条件配合列族设计把嵌套的向量数据拆成多个 qualifier避免单行过大。实际跑下来几亿行的轨迹点表时间窗口查询响应基本都在几百毫秒内比原来用二进制文件的方案快了一个量级。这两个案例的共同点是什么都是“数据量大数据多但查询模式不是宽表聚合而是按某个 key 或者按时间范围取一批行”。这类场景行式存储天然顺手列式存储反而别扭。5. 实操中的坑行式存储项目问题排查手册5.1 行膨胀与宽表为什么你的表越查越慢行式存储有一个很容易被忽略的坑就是单行字段越加越多、越加越大最终把性能拖垮。很多人做设计的时候觉得“反正是行存所有字段放一行不是最自然的吗”初期数据量小没事等数据量上来之后问题才会暴露。原因是这样的行式存储在读取一行时通常会把整行都加载到内存或者 page 里即使你的查询只需要其中一个字段。单行越大单次 I/O 的搬运量就越大内存的缓存命中率就越低点查延迟自然就上去了。我在一个项目里测过一个用户画像表单行从 2KB 涨到 20KB 之后同样 QPS 下的平均点查延迟从 8ms 涨到了 45ms接近六倍的恶化。排查这类问题先看表结构的平均行大小选几个典型 rowkey 做一次 get在客户端打印返回数据的字节数如果经常超过几十 KB就要考虑拆列族或者拆表了。把高频访问的字段放进一个列族低频字段放另一个列族HBase 支持按列族独立读取能有效减少无效 I/O。如果实在拆不开退一步在行内做字段级别的压缩比如对大字符串字段做压缩后再存能缓解一些但治标不治本根子上的方案还是别把什么都塞进一行。5.2 热点与倾斜读多写少的假象行式存储另一个高频问题是热点。尤其是用了自增 ID 或者时间戳这类单调递增的值作为 rowkey 主键时新写入的数据全部扎堆在最后一个 Region 或者分片上导致单个节点的负载飙升其他节点闲得发慌。这个现象在 HBase、TiDB 这类自动分片系统里尤其明显。我记得有个线上事故TiDB 集群某张表的主键是自增 ID每天凌晨定时任务批量写入结果某个 TiKV 节点 CPU 直接打满耗时从平时的十分钟拖到一小时。排查之后发现罪魁祸首就是自增主键造成的写热点所有新插入的数据都通过同一个 Region 的 Leader 写入压力全压在一台机器上。解决方法其实不复杂把主键的 AUTO_INCREMENT 换成在应用层生成随机 ID或者使用 TiDB 的 AUTO_RANDOM 属性打散写入。这类问题在传统单机数据库里不明显因为单机数据量小写热点也就是一块盘的事但在分布式行存系统里会被放大几倍。遇到类似问题建议先看监控面板上的 Region 分布和写入流量如果发现某个节点的写入吞吐明显高于其他节点八九不离十就是热点。解决思路无非三个加盐对 key 做哈希取模后拼前缀、反转把时间戳这类高位单调字段反过来、分桶随机化预分区数量做足key 按随机因子打散。5.3 序列化与压缩的隐性开销最后一个坑藏在数据格式层面。行式存储的核心操作是写入一行、读出整行数据的序列化和反序列化没法避免。Java 原生的序列化性能糟糕到了令人发指的程度吞吐低不说产生的字节数组还特别臃肿行存系统如果默认用 Java 序列化等于自己给自己挖坑。我见过有些团队把对象直接塞进 HBase读出来的字节数比用 Protobuf 序列化大了两三倍点查延迟直接翻倍。解决套路很成熟能不用原生序列化就坚决不用优先选 Protobuf、Thrift、Avro选一个团队熟悉且生态兼容的把这套序列化方案定为团队标准。压缩算法的选择也有讲究行式存储对压缩速度要求高因为每次点查都要解压解压太慢会吃掉大量 CPU。实测下来 LZ4 和 Snappy 在行存场景下综合表现最稳压缩比不算最好但解压速度快适合在线查询路径ZSTD 压缩比优秀但解压开销明显适合低频访问的冷数据。热数据用 LZ4 或 Snappy冷数据用 ZSTD可以做成分层策略不一定整个集群只用一种。这些坑总结下来其实是一个核心原则行式存储的性能优化重点在“读放大”也就是每次查询到底搬了多少无效数据。把单行体积控制住、把不被查询的字段隔离好、把序列化压缩的开销降到最低行式存储在大数据量下是完全可以做到又稳又快的。所谓的技术变革往回看是硬件和存储引擎的升级往前看落实到每一行 SQL、每一个 rowkey 设计、每一次序列化选择上才是真正能带来工程红利的地方。我个人这几年的体会是行式存储从来不是大数据体系里的配角只是过去我们把“大数据等于分析、分析等于列存”这个等式理解得太绝对了。现在实时业务越来越重数据服务越来越靠近业务一线行存和列存各自守住自己的最佳场景然后通过行列混合架构握手才是主流的方向。对于正在做技术选型的同学我的建议就一句话别被概念带走回到你的读写模型和延迟指标上去答案会自己浮出来。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门