☰
基于LSM-Tree的冷热分离架构:存储压缩与查询加速实践
2026/10/6 4:45:34 网站建设 项目流程

做数据存储的人迟早会碰到这样一个问题:业务跑得好好的,数据量也不断攀升,结果某天线上告警突然弹出“磁盘空间不足”,运维同学慌慌张张找你说“得扩容了”。第一次遇到这种场景时,你可能考虑的是纵向扩容、横向拆分,但当集群规模达到PB级别,任何“一刀切”的方案都会把成本和性能同时拖进泥潭。我这两年一直在跟PB级数据平台打交道,最深的体会就是:数据不能一视同仁地对待,必须根据访问频率把它们分开,让热的更热、让冷的更冷。这就是冷热分离架构的核心思路,而把这种思路真正落地到引擎层,目前最值得实践的技术底座就是LSM-Tree——它天然的分层存储结构、顺序写内存表、后台合并压缩机制,让存储压缩和查询加速不再是一对矛盾,而是可以协同发力的两个杠杆。这篇文章就把我在这套架构下踩过的坑、实际跑通的做法、以及后来延伸到图数据存储时遇到的邻接表与CSR压缩存储内存量级对比问题,一起梳理出来分享给大家。

1. PB级存储的挑战:从"一视同仁"到"冷热分层"

1.1 冷热分离的本质:数据访问的热度不均衡

我们先说一个反直觉的现象。很多团队在建设大数据平台初期,都喜欢把“所有数据均匀地放在一起”,觉得这样逻辑简单、查询方便。等到数据量到了几十TB甚至上百TB,这种“平均主义”就会开始反噬——SSD容量撑不住、存储成本飙升、查询延迟变得不可控。为什么会这样?因为业务数据的访问热度从来都不是均匀分布的。

我举一个非常典型的时序监控场景:一台服务器每5秒上报一次指标,每天产生约17000个数据点,一年大概600万个点。这些点在生成后的24小时内会被高频查询,比如实时监控大屏、告警排查;但过了三天、一周、一个月之后,这些点可能一个月才被翻出来一次,甚至永远不再被访问。如果让这两类数据共用存储路径、使用同样的压缩策略、享受同级别的缓存和索引,那性能肯定好不了,成本也会白白流失。

冷热分离要做的事情,就是把这类数据按访问热度切分成热数据与冷数据:热数据保留在高速路径——内存缓存、高刷SSD、低压缩比甚至不压缩;冷数据下沉到廉价路径——大容量HDD、高压缩比、低频索引更新。这个思想本身不新鲜,但难点在于“怎么在存储引擎层面自动完成温度分层”,而不是靠业务侧每次查询时手动指定“我要查热数据”还是“我要查冷数据”。这就要用到LSM-Tree了,它的分层天然就是一套温度分级系统。

1.2 冷热分离的层级划分:逻辑温度与物理介质

在落地冷热分离时,需要分清两个层面:逻辑层的“温度”和物理层的“介质”。

从逻辑层来看,温度可以由几个维度决定:

  • 时间维度:写入时间越久的数据,温度越低。这是最通用的规则,适合日志、监控、风控流水等场景。
  • 业务维度:某些业务类型天然就是热数据,比如订单状态、用户实时会话;某些业务就是冷归档,比如发票存证、历史合同。
  • 访问频次维度:通过统计单位时间内的查询命中次数来动态标记温度,适合推荐系统、用户画像这类数据热度会漂移的场景。

从物理层来看,冷热数据最终落在什么介质上,决定了成本能否降下来。在我维护的集群里,热数据走NVMe SSD,温数据走SATA SSD,冷数据走HDD甚至对象存储。三者成本大概是10:3:1的差距,因此哪怕只是把30%的数据从SSD挪到HDD,整体存储成本都能降下来一大截。

这里需要特别提醒的是,冷热分离不等于简单地把旧数据导到另一个表。如果只是导出,那查询路径上就要跨表跨库处理,且导出过程中还会产生双写一致性问题。而在LSM-Tree架构下,分层的SSTable文件天然就能映射到不同存储介质上——你不需要改变查询方式,只需要让引擎知道“哪个层级的数据该放在什么介质上”即可,后面我会详细展开。

2. LSM-Tree 为什么是冷热分离的最佳底子

2.1 LSM-Tree 工作原理速览

LSM-Tree(Log-Structured Merge-Tree)可以说是我这些年在PB级存储场景中最信任的引擎结构。它的核心设计是在内存中先写一个有序结构(MemTable),同时写一份WAL(Write-Ahead Log)保证崩溃恢复。当MemTable达到阈值后,冻结为不可变的SSTable(Sorted String Table),落盘到L0层;随后后台进程会不断将多层SSTable合并(Compaction)排序,逐层下沉到L1、L2、L3……

一句话总结:写入是顺序追加,读取是分层查找,空间是异步合并回收。这种设计的反馈体现在性能上就是写入吞吐极其惊人——因为不存在随机写磁盘,所有写操作都由内存缓冲和顺序IO承接,这也是现代大数据存储引擎普遍选择LSM结构的原因。

但要强调一点:LSM-Tree的读路径远没有写路径那么轻松。从L0开始,每一层都可能存在小概率的key重叠,查询需要逐层检查,最坏情况下读放大倍数会很高。所以LSM-Tree在查询加速方面必须下大功夫——布隆过滤器、块索引、缓存层、并行IO,这些我会在第4章里逐一讲。

2.2 分层结构如何天然对应冷热温度

这里是我认为LSM-Tree最妙的地方:它自带的多层SSTable结构,天然就是一套“温度层”。

新写入的数据肯定在MemTable里,这是最热的数据,访问延迟最低。然后它落到L0,也还算热,数据新鲜度较高。随着Compaction的不断推进,数据逐渐下沉到L1、L2、L3……越深的层级,数据的年龄越大,查询频率也越来越低。

这给冷热分离带来了极大的便利:按层做策略即可。我可以把L0~L1放在SSD上,L2放在SATA盘上,L3直接映射到对象存储;压缩比也可以逐层调节,L0用最快的压缩算法,L3用最狠的压缩算法。数据从热到冷的流动是引擎自动完成的,完全不用业务方介入。这比传统的“主表+归档表”双写模式省心太多了,也不用担心漏归档或查漏数据。

当然,这里有个前提:温度必须跟层级深度基本正相关。如果业务上有明确的热点大key,比如某个超热门用户的数据,它即便很早写入也依然要被高频查询,这时如果只按层判断,数据很可能被下沉到冷层而导致查询变慢。我的解决方案是增加一张“热键清单”,在查询路径中先判断key是否命中热键清单,命中的话强制走热数据路径。这一点在实际落地时很重要。

3. 存储压缩策略:冷数据变成"压缩饼干"

3.1 压缩层次与算法选型

冷热分离在存储上的收益主要来自两方面:一是把冷数据挪到廉价介质上,二是把冷数据压缩得更紧。接下来聊压缩。

数据压缩在存储引擎里可以发生在几个层次上:

行压缩 / 记录压缩:针对单条记录做压缩,粒度最小,适合更新频繁或随机读要求高的场景。但压缩率有限。

页/块压缩:以页或块为单位压缩,SSTable里的每个block就是一个压缩单元。读取时只需要解压命中的那个block,随机读效率高。

表/文件级压缩:整个SSTable文件统一压缩,压缩率最高,但读取任何一个block都需要解压整个文件,只适合极冷的数据。

实际落地时,我通常是三级结合。热数据的SSTable直接用LZ4或Snappy块压缩,单block解压快,对CPU的开销小;冷数据的SSTable用Zstandard(zstd)甚至LZMA做文件级压缩,牺牲一点访问延迟换取存储空间的成倍节省。

这里我给一个经验数据:在我们平台的监控数据场景中,同样是增量数据,Snappy的压缩比大约2.5:1,zstd可以到5:1到8:1,LZMA甚至能到10:1以上。你会看到,同一个文件,选择不同压缩算法,存储消耗相差3到4倍。这就是冷热分离中压缩策略的分水岭——热端要速度,冷端要空间。

3.2 按温度分级的压缩策略实操

下面是我在实践中验证过的一套压缩策略配置,拿出来给大家参考:

数据温度所在层级存储介质压缩算法压缩块大小预期压缩比
热数据MemTable/L0NVMe SSDLZ416KB1.5:1-2:1
温数据L1/L2SATA SSDSnappy32KB2:1-3:1
冷数据L3+HDDzstd(level 9)128KB5:1-8:1
归档数据对象存储对象存储LZMA2整文件10:1+

这套配置的直接收益是什么?我拿一个具体的集群来算:原始数据量大约25PB,如果全部用LZ4存储,总存储空间大约是10PB到12PB;如果热数据LZ4、温数据Snappy、冷数据zstd分层处理,整体下来可以压到4PB到5PB之间。再加上把真正最冷的归档数据用LZMA处理,我最终只用了大约4.3PB的物理空间就把25PB的逻辑数据装了进去。这个空间节省在云厂商那里折算下来,一年省下的存储费用非常可观。

3.3 压缩不是越多越好:CPU与查询延迟的平衡

说完了压缩省空间的好处,必须泼一盆冷水:压缩是有代价的。解压需要额外的CPU开销,压缩比越高,解压成本越大。这个问题在热数据上尤其致命。

我记得有一次优化查询慢的问题,排查到最后发现根因竟是压缩策略配反了——热数据层用了高压缩比算法,导致每次查询命中的block都要花大量CPU去解压。在并发查询高的时候,CPU直接被解压打满,P99延迟从50ms飙到了2秒。后来把热层切回LZ4,延迟立刻降回80ms以内。

所以压缩策略的选型逻辑应该很简单:

  • 热数据的访问频率高,应该优先保证查询性能,宁可多花一点存储空间,也要用低开销算法;
  • 冷数据的访问频率低,延迟容忍度高,应该优先保证压缩率,少占用磁盘和存储成本;
  • 温数据居于两者之间,用中档算法。

另外还有一个小技巧:定期对冷数据层做一次 Compaction 重压缩。因为数据在热层时是用LZ4压缩的,下沉到冷层后如果只是原样携带,就不会享受到高压缩算法带来的空间收益。需要触发一次“recompaction”让文件用新的压缩算法重新写入。很多团队第一次做冷热分层时都会漏掉这一步,导致冷数据明明已经放到HDD上了,占用空间却还是热数据级别,白费了冷热分离的功夫。

4. 查询加速:让热数据秒回、冷数据也能用

4.1 布隆过滤器:避免无效读放大

LSM-Tree的读放大,是查询性能的头号杀手。什么叫读放大?就是查询一个key时,你需要一层层往下去找,每一层都可能做磁盘读操作;如果这个key根本不存在,你仍然要查过每一层才能确认,白白浪费N次IO。

布隆过滤器就是解决这个问题的第一道防线。每个SSTable文件的元数据里都带有一个布隆过滤器,记录了这个文件里所有key的指纹。查询时先用布隆过滤器判断“这个文件里有没有可能包含我要找的key”,如果判断为“没有”,就跳过这个SSTable的扫描,直接进入下一层。

注意这里有个关键点:布隆过滤器的假阳性率。因为它是基于哈希的集合结构,存在一定的误判概率,会把“不在”误判成“可能在”。假阳性率越低,需要的内存空间就越大。根据我的体验,配置到1%的假阳性率是比较均衡的点位——每个key大约需要9.6bit的内存开销,误判情况基本可以接受。如果切到0.1%,内存开销会增加到14~15bit/key,收益并不明显,性价比不太划算。

我见过不少团队布隆过滤器搭了,但性能提升却不明显。排查下来发现是粒度问题——他们是给整个SSTable文件建一个布隆过滤器,文件本身好几GB大,导致过滤器极稀疏,误判率极高,几乎等于没建。正确做法是给SSTable内每个block(比如64KB一个block)建一个粒度更细的过滤器,这样才能精准地跳过无关数据块。

4.2 索引与缓存:查询路径上的第二、三道防线

如果布隆过滤器是初筛,那稀疏索引就是精确定位。

SSTable内部的数据是按key排序的,因此可以每隔N个key记录一个索引项(即稀疏索引),查询时先在索引中做二分查找,定位到可能包含目标key的block偏移量,然后只加载这一个block进行比对。块缓存(Block Cache)在这里也有大用处——它是基于LRU(Least Recently Used)的内存缓存,保存最近访问过的数据块。热数据命中缓存的概率高,就几乎不用走磁盘;冷数据虽然较少命中,但一旦有人查询,至少还有索引帮助快速定位。

实践中有个注意事项:缓存大小不是越大越好。块缓存过大,会把操作系统页缓存顶掉,反而减少页面缓存对文件的整体加速效果。我的经验是,块缓存大小设置在可用内存的10%~20%比较合适,具体还要看你的查询是重读场景多还是冷读场景多。

4.3 查询加速的全局路径优化:从过滤到裁剪再到并行IO

单个查询的性能优化到一定程度后,要从全局路径上再抠一部分提升。

第一步,分区裁剪。在设计表结构时,把时间信息作为组合key的一部分(比如day + user_id + metric_id),查询时直接根据时间范围裁剪掉不必要的分区和SSTable文件。这一步在PB级场景中收益极大——不需要扫描全表,光靠裁剪就能减少90%以上的扫描量。

第二步,并行IO与预取。LSM引擎在读取冷数据时,通常会把查询拆成多个子任务,并发地从不同磁盘或对象存储中拉取数据块。对于HDD上的冷数据,预取和并发能够明显摊薄机械盘随机读的延迟。我记得有一次把冷数据查询从串行改为8路并发预取后,P95延迟从4.2秒降到了1.1秒——效果立竿见影。

第三步,查询结果缓存。不要只依赖块缓存,业务层(或服务层)对重复的热点查询做结果级缓存,比如最近5分钟内的相同聚合查询直接短路返回。这对于监控大屏、报表类业务特别有用,因为这类查询的重复度极其高。

这些技术组合起来,冷数据的查询速度虽然不可能跟热数据完全一致,但完全可以做到“可接受”。在我的平台里,热数据查询P99在30ms左右,冷数据查询P99在800ms左右,对大多数业务来说这已经够好了——毕竟用户不会每一天都去翻一个月前的日志。

5. 一个绕不开的疑问:邻接表与CSR压缩存储,内存消耗量级对比

做存储的人总是会碰到各种格式选型问题。前阵子我们图数据团队讨论一个图查询引擎的存储设计时,内部就争过一个问题:邻接表和CSR压缩存储的内存空间消耗,到底是不是同一个量级?这个问题本身跟冷热分离不是同一层,但如果你在PB级规模下做图数据的冷热分层,就绕不开它,而且它的结论直接关系到你该把图数据的索引和关系结构放在内存里还是冷落到磁盘上。我把当时的对比数据整理一下,直接给大家一个可参考的答案。

5.1 邻接表:直观但冗余

先看邻接表。它的思路很直白:为每个顶点维护一个列表,存储它所有邻居的ID。比如顶点v的邻接表就是v -> [u1, u2, u3, ...]。

这种结构的优点是实现简单、遍历邻居方便,但缺点是内存开销大。为什么大?因为每个顶点不仅本身要存一条记录(顶点ID、指针等),每条边的信息还要额外存一份邻居ID,而且如果是有向图,每条边需要存一次;如果是无向图,每条边还会被存储两次(u的邻接表存一次v,v的邻接表又存一次u)。

另外为了支持动态增加和快速遍历,很多邻接表实现还得为每个顶点的邻居列表维护额外的负载因子、指针数组等控制信息。分摊下来,一个邻居ID的实际内存开销往往会达到16到24字节,而不仅仅是8字节的int。

5.2 CSR:紧凑的顶点与边的排列

再看CSR(Compressed Sparse Row)。CSR把图数据分成两个数组:一个是偏移数组(offset),长度等于顶点数+1;一个是邻接数组(adj),长度等于总边数。offset[i]指向顶点i的邻居在adj数组中的起始位置,offset[i+1]则指向结束位置。整个图数据就两块连续内存。

这里的关键是,它不存指针、不存对象头、每条边就一个ID值(通常4字节)。如果顶点ID范围在2^32以内,用uint32就能装下;如果图更大,用uint64。这样算下来,CSR的内存消耗大约是:

  • 偏移数组:(V+1) × 4/8字节
  • 邻接数组:E × 4/8字节(有向图);无向图如果按对称形式存储,也是2E × 4/8字节,但实际上很多CSR实现可以配合对称剪枝或半存储来减少一半)

对比下来,同样规模的图,CSR的内存开销通常只有邻接表的40%~60%。如果邻接表实现得比较粗糙(每个邻居要多存一个next指针),CSR甚至能省下70%以上的内存。

所以回到那个问题:邻接表和CSR压缩存储的内存空间消耗是同一个量级吗?我的结论是:不是同一个量级。CSR在顶点规模大、边密度高的场景下,内存优势是碾压级的。

我举一个当时评估的真实数字。假设有1000万个顶点、2亿条有向边,用uint32存ID:

  • 邻接表:每个顶点对象约40字节(含动态数组指针等),邻居ID按每条边8字节(ID + 指针)来保守估算,内存大约是 1000万×40 + 2亿×8 ≈ 400MB + 1.6GB ≈ 2.0GB。
  • CSR:偏移数组 1000万×4 ≈ 40MB,邻接数组 2亿×4 ≈ 800MB,合计约840MB。

差了大概2.5倍。如果无向图按一份存储,差距还会更大。

这个结论在冷热分离架构下的实际意义是:对于图数据,关系结构本身适合用CSR格式压缩后放在内存缓存或SSD上;但如果你的图是超大规模,边关系无法全部常驻内存,就可以把不同热度子图的CSR分成冷热两层,热子图的CSR放内存,冷子图的CSR以压缩格式放磁盘或对象存储,查询时按需加载。

5.3 从邻接表到CSR:压缩转换的实操要点

如果你想把现有的图存储从邻接表迁移到CSR,有几点比较值得注意:

  1. ID重映射:CSR要求顶点ID尽量连续紧凑,否则偏移数组会产生很大的空洞。如果原图顶点ID稀疏(比如业务ID是字符串或长随机数),需要先做一层重映射,将顶点映射到连续的整数ID,否则CSR的压缩优势会被浪费掉。

  2. 动态更新代价高:CSR的静态数组结构决定了它不适合频繁插入和删除边。如果图数据需要实时更新,我的建议是做一个“LSM式”的分层结构——新增边先放在一个面向写入的临时结构(类似邻接表或缓冲SSTable)里,后台定期把增量合并进主CSR结构。这其实就跟LSM的分层合并思路呼应上了。

  3. 压缩后的索引:CSR本身就是一种极紧凑的存储,但如果数据要落到冷层,还可以进一步压缩,比如用差分编码存储相邻偏移量、用变长整数编码压缩邻接数组中的ID差值序列。我做过一组对比,在原始CSR之上再做差分变长编码,冷数据的体积还能再压缩50%以上。

6. 我的实操体会:冷热分离不是一次改造,而是一个持续调优的过程

最后说点跟具体技术无关、但直接影响落地效果的大白话经验。

第一,冷热分离的阈值一定要基于真实访问日志来定,不要拍脑袋。我们在生产环境上线第一版冷热分离时,最初把“7天前的日志都归为冷数据”,结果发现某些关键业务要回溯12天的数据做审计,于是每次查询都打到冷层,延迟暴涨。后来花了两个星期统计访问日志,按访问频率分布重新划分阈值,改成“90天以上且无访问记录”才定义为冷数据,问题才真正解决。数据温度不是一成不变的,定期分析访问模式、动态调整分层规则是必要的。

第二,把所有策略做成可观测的。冷热比例、每层压缩比、每层查询延迟、Compaction积压量、布隆过滤器命中率,这些指标必须能在监控大盘上实时看到。不然冷热分离就会变成一个“黑盒”,数据跑得慢了你都不知道是卡在哪个环节。

第三,冷热分离要跟容量规划联动。有了冷热分层后,热层的容量其实是相对稳定的,因为旧数据会持续下沉;冷层的容量则需要按“累积周期 × 每日写入量 ÷ 目标压缩比”来预估。这样你可预测地在成本支出之前就把扩容安排合理,而不是等到磁盘满了再救火。

这套基于LSM-Tree的冷热分离架构,从整体思路到压缩策略再到查询加速,一路走下来让我最大的感受就是:存储系统的性能天花板,往往不是靠某一项黑科技抬上去的,而是靠把“数据温度”这个维度用好,让每一条数据待在你最愿意为它花钱的那个阶段。希望这篇实践分享能给你一些可以参考的思路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询