大数跨境
0
0

从 40 亿整数到 HBase:一个 Membership Test 问题的抽象与演化

从 40 亿整数到 HBase:一个 Membership Test 问题的抽象与演化 AI 原力注入
2025-12-27
0
导读:这篇文章从一个经典的"40 亿整数判存在"问题出发,带你理解工程系统(以 HBase 为例)的设计思路:当数据规模大到无法全部放入内存时,系统如何通过巧妙的数据结构和读取路径设计,高效地回答"某个数据

 

阅读指南

这篇文章从一个经典的"40 亿整数判存在"问题出发,带你理解工程系统(以 HBase 为例)的设计思路:当数据规模大到无法全部放入内存时,系统如何通过巧妙的数据结构和读取路径设计,高效地回答"某个数据在不在集合中"这类问题。

建议你这样阅读:

  • • 第一遍:先抓住主要思路:问题目标 → 约束条件 → 解决方案的选择
  • • 第二遍:再关注技术细节:Bitmap 的使用限制、Bloom Filter 的误判原理、HBase 读取路径中过滤和索引的分工

学习目标

读完这篇文章,希望你能够:

  • • 用"Membership Test"这个概念来统一理解各种看似不同的问题
  • • 解释为什么 Bitmap 在某些情况下是"完美解决方案",但在 HBase 场景中却不可行
  • • 理解 Bloom Filter 在 HBase 中的作用:主要是减少不必要的磁盘读写操作,而不是直接给出最终答案

需要提前了解的知识

(如果不太熟悉也没关系,可以边看边学习):

  • • 哈希函数的基本特点(分布均匀、结果确定)
  • • 基本的外部存储访问成本:磁盘或 SSD 的随机读取比内存访问慢很多

一、问题的起点:一个看似“算法题”的问题

给定 40 亿个不重复、未排序的整数,在内存限制为 1GB 的条件下,如何快速判断某个整数是否在这 40 亿个数中?

这是学习数据结构和算法时很可能会遇到的一个问题。虽然看起来像一道算法题,但实际上包含了多个系统层面的限制条件:

  • • 数据规模巨大(40 亿)
  • • 数据无序
  • • 查询可能是高频的
  • • 内存受限(1GB)

在继续之前,我们先暂停一下,思考一个更基础的问题:

这个问题的核心目标到底是什么?

不是排序,不是遍历,而是——
判断某个元素是否属于一个巨大集合

这类问题在计算机科学中有一个统一的名字:

大规模集合成员查询问题(Membership Test Problem)

为了让后面的讨论更“工程化”,我们把问题写成一个更明确的接口(只描述你要做什么,而不提前绑定实现):


   
   
   
    
   
   
   # 说明:Membership Test 的目标接口
build(S): 给定集合 S,构建可查询的表示
contains(x): 查询 x 是否属于 S

后续所有方案,都是在不同约束下实现同一个接口,并在“内存、构建成本、查询吞吐、准确性”之间做取舍。


二、为什么“直觉解法”都行不通?

在分析问题时,先看看“直觉上可能想到但实际行不通的解法”是很有帮助的。

1. 排序 + 二分查找?

  • • 排序需要将 40 亿整数加载到内存或进行外排序
  • • 查询虽然是 O(log N),但整体成本极高
  • • 不满足“快速判断”的工程预期

❌ 不合适

2. HashSet / HashMap?

  • • 40 亿整数(4,000,000,000 个)
  • • 每个整数在 Java 中:
    • • int 值:4 字节
    • • 对象头:约 12-16 字节
    • • 指针开销:8 字节(64 位系统)
    • • 考虑装载因子(0.75):实际需要约 53 亿个槽位

👉 总内存消耗:40 亿 × (4 + 16 + 8) ≈ 40 亿 × 28 字节 ≈ 11.2GB
(远超 1GB 限制,即使考虑优化也至少需要 4-5GB)

❌ 不可行

3. 顺序扫描?

  • • 时间复杂度 O(N)
  • • 对高频查询完全不可接受

❌ 不现实

4. 一个重要的转折点

到这里,你可能会感到有些困惑。此时非常关键的一步是重新审视问题目标

我们真的需要“存下这 40 亿个整数的值”吗?

我们真正关心的只有一件事:

某个整数在 / 不在

这句话背后隐含了一个工程上的“降维”思想:如果你的查询只需要 1 bit 的答案(在/不在),那么存储结构最好也尽可能接近 1 bit 的信息量,而不是把整数以对象、指针等形式“膨胀存储”。


三、Bitmap:在“理想约束”下的完美答案

1. Bitmap 的基本思想

如果整数的取值范围是有限的(例如 32 位无符号整数),可以使用一个位数组:

  • • 下标:整数值
  • • 值:1 表示存在,0 表示不存在

每个整数只占 1 bit


2. 内存是否可行?

  • • 32-bit 整数最大值域:2³² = 4,294,967,296 个可能值
  • • 所需位数:2³² bits = 4,294,967,296 bits
  • • 折算为内存:
    [
    2^{32} \text{ bits} ÷ 8 = 2^{29} \text{ bytes} = 536,870,912 \text{ bytes} = 512 \text{ MiB}
    ]

👉 完全在 1GB(1,024 MiB)内存限制内

内存计算

  • •   bit ÷ 8 =   byte = 512 MiB
  • • (注:MiB =   byte,便于与   计算对齐)

3. Bitmap 的性能特征

维度 Bitmap
查询复杂度
O(1)
内存占用
~512 MiB
准确性
100%
实现复杂度
极低

可以说,在当前约束条件下,Bitmap 几乎是一个完美解

为了更具象,下面给出一份“位图存取”的伪代码(把整数映射到 bit 位):


   
   
   
    
   
   
   # 说明:示例展示 Bitmap 的核心访问方式(按位设置/测试)
# 假设 key 落在 [0, U) 且 U 可控(例如 U = 2**32)

# 注意:这里使用无符号整数,实际工程中需要考虑负数处理


def
 set_bit(bitmap: bytearray, key: int) -> None:
    byte_index = key >> 3          # key // 8(等价于整除8)
    bit_offset = key & 7           # key % 8(取模8,得到0-7的偏移)
    bitmap[byte_index] |= 1 << bit_offset  # 设置对应位为1

def
 test_bit(bitmap: bytearray, key: int) -> bool:
    byte_index = key >> 3
    bit_offset = key & 7
    return
 (bitmap[byte_index] >> bit_offset) & 1 == 1  # 测试对应位是否为1

# 位操作解释:

# >> 3:右移3位相当于除以8,得到字节索引

# & 7:与7进行位与操作相当于对8取模,得到位偏移(0-7)

# 1 << bit_offset:将1左移对应位数,创建位掩码

四、关键反思:Bitmap 成立依赖哪些前提?

这是本文第一个关键知识点

Bitmap 并不是“普适解法”,它隐含了多个非常强的前提条件:

  1. 1. Key 是整数
  2. 2. 值域是有限且可控的
  3. 3. Key 分布相对稠密
  4. 4. 可以为整个值域分配连续内存

此时我们需要思考这么一个问题:

如果这些前提中有任何一个被打破,会发生什么?

理解"前提被打破"的后果,有助于你将这个思路应用到其他问题:

  • • 如果值域不可控:需要为“可能从未出现”的值也分配 bit 位,空间迅速失控。
  • • 如果极度稀疏:集合大小   明明很小,但值域   很大,Bitmap 的空间消耗由   决定而不是由   决定,性价比很差。
  • • 如果 Key 不是整数:需要先把 Key 映射到整数域,映射必须可逆(才能做到 0 误判),这通常会回到“存储原始值”的问题。

五、问题换一层皮:HBase 面临的“同一个问题”

现在,我们把问题稍作变化。

1. 问题的新表述

在 HBase 中,系统每天都要面对类似的问题:

给定海量 RowKey,如何快速判断某个 RowKey 是否存在?

我们做一个一一对应:

原问题 HBase 场景
40 亿整数
40 亿 RowKey
判断是否存在
Get(rowKey)
内存受限
RegionServer 内存有限
数据无序
HFile(SSTable)
多次查询
高并发读请求

从抽象层面看,这仍然是一个 Membership Test 问题

为了让映射更清晰,可以把 HBase 的一次读取(Get)拆成两个子问题:

  • • Membership Test:某个 RowKey 是否可能出现在某个数据文件(HFile)里?
  • • 精确定位:如果“可能存在”,如何在外存结构中快速定位到目标 Key 对应的 Cell?

HBase 的工程实现恰好把这两件事拆开:先用 Bloom Filter 过滤不必要的文件访问,再用 HFile 的索引结构完成精确查找。(来源:Apache HBase Reference Guide;《HBase: The Definitive Guide》)


六、为什么 Bitmap 在 HBase 中不可行?

这是第二个关键知识点

Bitmap 的前提 HBase 的现实
固定整数值域
RowKey 是变长 byte[]
稠密分布
Key 高度稀疏
单一集合
多个 HFile
连续内存映射
内存极其宝贵

结论非常明确:

不是 Bitmap 不够优秀,而是 HBase 的约束条件彻底否定了 Bitmap。

把这句话落到更具体的工程细节上:

  • • RowKey 是 byte[](变长),值域理论上近乎无穷,Bitmap 无法“枚举所有可能”。
  • • 即使把 byte[] 通过哈希映射到固定整数域,也会引入哈希冲突;除非额外存储原 Key 来消解冲突,否则无法做到 0 误判,这又把空间压力拉回来了。
  • • HBase 数据分片在多个 Region、多个 Store、多个 HFile 中;Bitmap 更像“对一个固定全集做全量映射”,与这种多文件、多层结构的组织方式不匹配。

七、工程系统的选择:Bloom Filter + 外存精查

1. 工程系统的现实取舍

在 HBase 这样的系统中,目标并不是:

“一次判断就给出 100% 正确答案”

而是:

尽可能少地触发昂贵的磁盘 IO

这对应一个典型的系统事实:一次“无谓的外存查找”(读了一堆文件后发现不存在)可能比一次简单的内存计算(哈希 + bit 测试)贵几个数量级。Bloom Filter 的价值就在于把“高代价操作”变成“少发生的操作”。


2. Bloom Filter 的角色

Bloom Filter 的作用可以用一句话概括:

判断“是否有必要继续查”

  • • 如果 Bloom Filter 判断“不存在”
    → 一定不存在,直接跳过该 HFile
  • • 如果判断“可能存在”
    → 再去磁盘做精确查找

这里有一个必须明确的语义:

  • • Bloom Filter 只有“误判存在”(False Positive),不会“误判不存在”(False Negative)。
  • • 因此它适合作为“过滤器”,不适合作为最终答案来源。(来源:Bloom, 1970)

Bloom Filter 的误判率与参数之间的关系常用近似公式表示(  为 bit 数,  为插入元素数,  为哈希函数个数):

  • • 误判率:
  • • 最优  (使误判率最小):

(来源:Bloom, 1970;Mitzenmacher & Upfal, Probability and Computing


3. HBase 的实际读路径(简化)


   
   
   
    
   
   
   # 说明:读路径示意,展示“先过滤、再精查”的分工
Get(rowKey)
  ↓
MemStore(内存)
  ↓
BlockCache(内存)
  ↓
Bloom Filter(是否可能在该 HFile)
  ↓
HFile 精确查找(磁盘)

这里形成了一个非常经典的工程组合:

Bloom Filter(内存,快速过滤) + 外存精确结构(HFile)

补充两点,避免“过度简化”带来的误解:

  • • HBase 的 Bloom Filter 通常是“每个 HFile(StoreFile)一个”,用于判断“这个文件里是否可能有该 Key”,从而减少对不相关文件的访问。(来源:Apache HBase Reference Guide)
  • • “HFile 精确查找”内部还包含索引与块读取:它不是线性扫描,而是借助多级索引定位到目标数据块,再读取并在块内二分/顺序验证。(来源:Apache HBase Reference Guide;《HBase: The Definitive Guide》)

八、Bitmap vs HBase:放在同一个抽象层下对比

现在,我们可以把两个方案放回到统一的 Membership Test 框架中:

维度
Bitmap
HBase
Key 类型
固定整数
变长字节
值域
可控
不可控
内存策略
全量映射
概率过滤
是否误判
允许
工程目标
极致简单
控制读放大

这说明了一个非常重要的结论:

算法不是被问题本身选择的,而是被约束条件选择的。

对我们来说,真正应掌握的是“选择依据”:

  • • 当值域   可控且稠密时,Bitmap 的空间与   线性相关,但常数极小,查询极快。
  • • 当值域不可控且稀疏时,应让空间主要随集合大小   增长(例如 Bloom Filter、哈希索引、LSM 的索引层)。
  • • 当系统的主要瓶颈来自 IO 时,优先用内存结构去减少 IO 次数,往往比把单次查找做得“更精确”更重要。

九、统一抽象:Membership Test 的三类解法

从这个角度看,Membership Test 的常见解法可以抽象为三类:

  1. 1. Bitmap
    • • 值域可控、Key 稠密
    • • 追求极致性能和确定性
  2. 2. Bloom Filter + 精确结构
    • • Key 不可控、规模巨大
    • • 工程系统的主流选择
  3. 3. 索引结构(B+Tree / LSM)
    • • 需要绝对精确、可扩展
    • • 接受更高的访问成本

要建立"何时用哪类"的直觉,可以用一条简单但实用的判断线索:

  • • 你能否为“所有可能的 Key”分配一个稳定的编号且编号空间不大?
    • • 能:优先考虑 Bitmap 或其压缩变体
    • • 不能:在“概率过滤 + 精确结构”与“纯精确索引”之间权衡

在 HBase 这类系统中,常见组合是:LSM(MemStore + 多个 HFile)作为精确结构,Bloom Filter 作为概率过滤层,用于降低读放大。


十、深入思考

这里有一些值得深入思考的问题,帮助你检验学习成果:

  1. 1. 如果 HBase 的 RowKey 是 32-bit 整数,Bitmap 是否可行?
  2. 2. Bloom Filter 的误判率应该如何配置?
  3. 3. 为什么 HBase 选择“每个 HFile 一个 Bloom Filter”?
  4. 4. 在什么场景下,Bloom Filter 的收益会非常低?

尝试用本文的逻辑去分析这些问题:明确约束、列出指标、权衡取舍。不必追求唯一的标准答案,关键在于推导过程是否合理。


结语

从“40 亿个整数”到“HBase 的 RowKey”,问题的表象发生了巨大变化,但问题的本质从未改变

如何在巨大集合中,低成本地回答“它在不在?”

理解这一点,比记住任何一个具体实现都更重要。


参考资料

  • • Burton H. Bloom. “Space/Time Trade-offs in Hash Coding with Allowable Errors.” Communications of the ACM, 1970.
  • • Michael Mitzenmacher, Eli Upfal. Probability and Computing: Randomization and Probabilistic Techniques in Algorithms and Data Analysis.
  • • Apache HBase Reference Guide(官方文档):https://hbase.apache.org/book.html
  • • Lars George. HBase: The Definitive Guide(关于 HFile、读路径与 Bloom Filter 的工程描述)

 


【声明】内容源于网络
0
0
AI 原力注入
微软 CEO 萨提亚曾说:“所有产品都值得用 AI 重做一遍。” 我们正处在一场深刻变革中,唯有用 AI 赋能自身,才能拥抱未来。原力注入从云原生迈向 AI 新时代,期待在这个伟大时代中持续成长、不断突破。
内容 515
粉丝 0
AI 原力注入 微软 CEO 萨提亚曾说:“所有产品都值得用 AI 重做一遍。” 我们正处在一场深刻变革中,唯有用 AI 赋能自身,才能拥抱未来。原力注入从云原生迈向 AI 新时代,期待在这个伟大时代中持续成长、不断突破。
总阅读226
粉丝0
内容515