HBase 的诞生——为什么 HDFS 不够用,列族存储的设计哲学

摘要:

本文从 Google 2006 年发表的 BigTable 论文出发,深入剖析 HBase 诞生的历史必然性。通过分析互联网数据爆炸时代关系型数据库的扩展困境、HDFS 的随机读写盲区,以及半结构化数据对存储模型的全新要求,逐步推导出”列族存储”这一设计哲学的内在逻辑。文章重点回答三个核心问题:大规模互联网数据究竟遇到了什么样的存储危机?为什么 HDFS + MapReduce 的批处理模型无法解决随机访问问题?列族模型相较于行存储、纯列存储,在工程实践上做了哪些深思熟虑的取舍?理解这些问题,是读懂 HBase 所有后续设计决策的根基。


第 1 章 时代背景:互联网数据爆炸与存储系统的生死危机

1.1 一个具体的工程问题:Google 究竟要存什么

2003 年到 2006 年,Google 陆续发表了三篇奠定大数据时代基础的论文——GFS(Google File System)、MapReduceBigTable。这三篇论文不是偶然出现的,而是 Google 工程师在解决真实业务问题时逼出来的工程结晶。要理解 HBase 为什么诞生,必须先理解 Google 当时面临的是什么样的具体问题。

彼时 Google 的核心业务是全球最大规模的网页爬虫与索引系统。每天,Google 的爬虫要抓取数十亿个网页,对每一个 URL,它需要存储以下数据:

  • 该 URL 的原始 HTML 内容(可能有数百 KB)
  • 该 URL 被多少其他页面引用(链接关系图)
  • 该 URL 上次被爬取的时间戳
  • 该 URL 的语言、编码格式等元数据
  • 该 URL 对应页面上的所有锚文本

这里出现了一个极度不规则的数据特征:每个 URL 所拥有的属性数量天差地别。一个简单的个人主页可能只有几个入链,而像 cnn.com 这样的新闻门户则可能有数百万个入链。一篇新闻页面的内容可能 3 个月就过期了,而一个产品页面的内容可能 5 年都不怎么变化。

如果用关系型数据库来存储这些数据,会发生什么?

1.2 关系型数据库的扩展边界:不是 MySQL 不好,而是场景不对

关系型数据库(RDBMS,如 MySQL、PostgreSQL、Oracle)是 20 世纪软件工程的伟大成就。它们解决了结构化数据的存储、事务、一致性和查询问题,支撑了整整一代企业级应用。但互联网的崛起将一批全新的工程挑战带到了数据库系统面前。

挑战一:数据规模的量变引发质变

传统企业数据库的设计假设是”数据量在 GB 到 TB 级别,能够运行在单台或少数几台服务器上”。这个假设在 ERP、CRM、电商订单系统等场景下是成立的。但 Google 的网页爬虫数据是 PB 级别的,而且以每天数十亿次写入的速度持续增长。

关系型数据库在这个量级下会遇到一个根本性的瓶颈:垂直扩展(Scale-Up)有上限,水平扩展(Scale-Out)极其复杂

单台服务器的 CPU、内存、磁盘 I/O 能力是有上限的。当数据量超过单台机器的处理能力时,你必须把数据分散到多台机器上——这就是”分片”(Sharding)。但关系型数据库对分片的支持非常原始,所有的跨节点 JOIN、跨节点事务、分布式索引都需要应用层自己处理,工程复杂度极高。

Google 的工程师们在自己的业务中反复踩坑后,得出了一个痛苦的结论:传统关系型数据库不是为水平扩展而设计的

挑战二:Schema 的刚性与业务的柔性之间的矛盾

关系型数据库要求你在建表时预先定义好所有的列(Schema-on-Write)。这在业务逻辑稳定的企业系统中没有问题,但在互联网业务中,数据的形态往往随着产品需求快速变化。

Google 的网页索引系统就是一个典型例子。随着 Google 不断升级其搜索算法,它需要为每个网页存储的特征维度会持续增加。每次增加一个新的特征维度,就需要对整个数亿行的表执行 ALTER TABLE ADD COLUMN——这在 RDBMS 中是一个极其耗时、甚至需要锁表的操作。

更糟糕的是,在海量稀疏数据场景下,绝大多数行的新增列值为 NULL。在行存储模型中,NULL 值虽然不存储实际数据,但依然会占据一定的元数据空间,并且在查询时需要被跳过处理。

挑战三:随机读写的性能困境

这是理解 HBase 诞生最关键的一点,我们在后面专门用一章来讨论。

1.3 NoSQL 运动的兴起:不是抛弃 SQL,而是打破一统

理解上述背景之后,我们可以看到,2000 年代中期互联网公司集体面临的困境催生了”NoSQL”(Not Only SQL)运动。NoSQL 并不是一个统一的技术标准,而是一个松散的工程共识:

对于特定类型的问题(海量数据、高并发写入、半结构化数据),传统 RDBMS 的强一致性事务和固定 Schema 是一种代价高昂的束缚,可以根据业务需求做出取舍,换取更好的可扩展性和灵活性。

这一时期诞生了多种不同方向的 NoSQL 系统:

类型代表系统核心取舍典型场景
键值存储Redis、Memcached极简模型,极高吞吐缓存、会话存储
文档存储MongoDB、CouchDB灵活 Schema,支持嵌套文档内容管理、用户配置
列族存储HBase、Cassandra稀疏宽表,海量写入时序数据、行为日志
图数据库Neo4j、JanusGraph关系优先,图遍历高效社交图谱、知识图谱

HBase 属于”列族存储”(Column-Family Store)这一类,而它的直接设计来源,正是 Google 的 BigTable 论文。


第 2 章 HDFS 的能与不能:为什么批处理不等于一切

2.1 HDFS 的设计哲学:为大文件的顺序读写而生

在理解 HBase 为什么在 HDFS 之上构建之前,我们需要非常清晰地理解 HDFS 自身的设计边界——它能做什么,更重要的,它不能做什么。

HDFS(Hadoop Distributed File System)是 Google GFS 论文的开源实现。GFS 的设计论文明确写道,它的目标是服务于以下工作负载:

  • 文件大小通常在 GB 到 TB 量级,存储数量在百万级别(而非十亿级别的小文件)
  • 主要的读操作是大批量流式读取,极少有随机读
  • 写入主要是追加写(Append),几乎不做随机位置的修改
  • 系统运行在廉价的商用服务器上,故障是常态而非例外

基于这些设计假设,HDFS 做出了一系列非常明确的工程选择:

Block 大小极大(默认 128MB):减少元数据开销,优化大文件传输的网络利用率。但这也意味着,如果你有一亿条用户日志记录,每条只有几百字节,存成 HDFS 小文件会产生天文数字的 NameNode 元数据压力。

不支持随机写:HDFS 文件一旦创建,只支持追加(Append),不支持在任意位置修改。这一限制极大地简化了 NameNode 的元数据管理和 DataNode 的数据一致性维护,但也彻底封死了”更新特定记录”这一操作。

高吞吐优先,低延迟不是目标:HDFS 的设计明确指出,它优化的是数据吞吐量(Throughput),而不是访问延迟(Latency)。一次 HDFS 的 open() 操作需要和 NameNode 通信,再定位到 DataNode,延迟往往在数十毫秒量级。这对批处理任务毫无影响,但对需要毫秒级响应的在线查询而言,是灾难性的。

2.2 HDFS + MapReduce 的批处理模型:完美的离线世界

HDFS 搭配 MapReduce(以及后来的 Apache Spark),构建了一套完美的大规模批处理解决方案。它能处理的问题有一个共同特征:

问题的输入是全量数据集,输出是计算结果,不需要针对单条记录进行随机访问。

比如:

  • “统计过去一个月所有用户的 PV/UV” → 全量扫描日志文件,聚合计算 → 完全适合 MapReduce
  • “对 Web 索引数据集做 PageRank 计算” → 多轮迭代,每轮全量读取 → 适合 MapReduce/Spark
  • “ETL:把原始日志转换为结构化数仓表” → 全量变换 → 适合批处理

但还有另一类问题,是 HDFS + MapReduce 无法回答的:

问题的输入是一个”键”(比如用户 ID 或 URL),输出是该键对应的记录,需要在毫秒级完成响应。

比如:

  • “查询用户 user_12345 最近 10 次的购买记录” → 随机点查 → MapReduce 无法完成
  • “实时更新某商品库存数量” → 随机更新 → HDFS 不支持
  • “根据订单 ID 查询订单状态” → 按主键查询 → HDFS 无法支持

这就是 HDFS 的盲区,也是 HBase 存在的根本理由。

2.3 随机访问的本质:索引与局部性原理

为什么关系型数据库能做到随机读写,而 HDFS 不能?这需要从存储系统的基本原理出发来理解。

随机访问的关键是索引(Index)。关系型数据库之所以能在毫秒级返回 SELECT * FROM orders WHERE id = 12345,是因为它在磁盘上维护了一棵 B+树 索引。B+树 的每个节点对应磁盘上的一个固定大小的 Page(通常 4KB 或 16KB),根据键的大小比较,可以在 O(log N) 次磁盘 I/O 内定位到目标记录。

HDFS 没有这样的索引机制。它的元数据(NameNode 中的 FsImage)只记录了每个文件被切分成哪些 Block、每个 Block 在哪些 DataNode 上。要找到文件内某个特定位置的数据,你必须知道它的字节偏移量——而对于一个存储了海量记录的大文件,你根本不知道”用户 12345 的记录”在哪个字节偏移处,除非把文件全部扫描一遍。

这正是 MapReduce 的工作方式:全量扫描,然后筛选。这种方式对于批处理绰绰有余,但对于在线服务而言,每次查询都触发一个全量扫描是完全无法接受的。

核心矛盾

HDFS 擅长的:顺序读,大块传输,高吞吐。 在线服务需要的:随机读,小块定位,低延迟。 这两者在工程上是根本对立的。HBase 的出现,正是为了在 HDFS 之上,通过一套精妙的索引与缓存机制,弥合这个鸿沟。

2.4 “构建在 HDFS 之上”意味着什么

既然 HDFS 有如此多的限制,为什么 HBase 还要构建在它之上,而不是自己实现一套存储层?

这是一个非常关键的工程决策,背后的逻辑是:

HDFS 解决了分布式存储的难题,HBase 不需要重复造轮子。

分布式存储系统面临的最大挑战不是如何存数据,而是如何处理节点故障、数据副本、网络分区、数据一致性这些问题。HDFS 已经用成熟的工程实践解决了这些问题:

  • 3 副本机制保证数据不丢
  • DataNode 心跳检测自动识别节点故障
  • NameNode 负责全局数据布局的元数据管理
  • 数据本地性感知优化计算效率

HBase 将自身定位为 HDFS 之上的随机访问层:它不关心数据最终如何存储在磁盘集群上,那是 HDFS 的职责;它只负责在 HDFS 上构建高效的索引结构,让”按键随机访问”成为可能。

这种分层设计是软件工程中的经典思想:每一层只做自己最擅长的事,通过标准接口组合出更强大的能力。


第 3 章 BigTable:一篇论文如何定义了一个时代

3.1 Jeffrey Dean 与 Sanjay Ghemawat 的工程哲学

Google BigTable 论文于 2006 年在 OSDI(操作系统设计与实现大会)上发表,作者包括 Fay Chang、Jeffrey Dean、Sanjay Ghemawat 等 Google 核心工程师。这篇论文的意义不仅在于描述了一个具体的系统,更在于它系统性地提出了”用于结构化数据的分布式存储系统”的设计思路。

论文的开篇第一句话就直接点明了设计目标:

“Bigtable is a distributed storage system for managing structured data that is designed to scale to a very large size: petabytes of data across thousands of commodity servers.”

注意这里的每一个词都经过精心选择:

  • “structured data”(结构化数据):不是文档,不是纯 KV,而是有一定结构的数据
  • “petabytes”:PB 级别,这在 2006 年是一个令人震撼的数字
  • “thousands of commodity servers”:廉价商用服务器,而非 IBM 大型机

BigTable 被用于 Google 内部超过 60 个不同的产品,包括 Google Analytics、Google Finance、Orkut(Google 的社交网络)、Personalized Search,以及最核心的 Google Web Indexing(网页索引)。

这些产品的数据特征差异极大,但 BigTable 能够为它们提供统一的存储服务,这本身就说明它的设计具有极强的通用性和灵活性。

3.2 BigTable 的数据模型:三维稀疏有序映射

BigTable 论文对数据模型的定义非常精炼:

“A Bigtable is a sparse, distributed, persistent multi-dimensional sorted map. The map is indexed by a row key, column key, and a timestamp; each value in the map is an uninterpreted array of bytes.”

翻译成中文:

BigTable 是一个稀疏的、分布式的、持久化的、多维有序映射表。 这张表通过行键(Row Key)、列键(Column Key)和时间戳(Timestamp)进行索引;表中的每个值是一个不被解释的字节数组。

这个定义极其简洁,但它蕴含了深刻的设计哲学。让我们逐一拆解:

“稀疏(Sparse)“:大多数行只填充少数几列,剩余列为空。与关系型数据库不同,空列不占用任何存储空间,不需要存储 NULL 占位符。这对于网页爬虫这类数据非常重要——不同 URL 拥有的特征维度差异极大。

“多维(Multi-dimensional)“:数据的坐标不是一维的行号或两维的(行,列),而是三维的(行键,列键,时间戳)。时间戳这个维度使得 BigTable 天然支持数据的多版本存储——同一个单元格可以保存历史上多个时间点的值,查询时可以指定读取最新版本还是特定历史版本。

“有序(Sorted)“:所有行按行键的字节序进行排序存储。这一点极其重要,它决定了如何设计行键。相邻行键的数据在物理存储上也相邻,这使得”范围扫描”(Scan)成为高效操作——只需要从起始行键顺序读取到终止行键即可。

“字节数组(Array of bytes)“:值是无类型的字节序列,BigTable 不关心值的语义,类型解释完全由应用层负责。这种设计极大地简化了系统本身的复杂度,同时保留了极致的灵活性。

3.3 BigTable 与 HBase 的传承关系

HBase 是 BigTable 的开源实现,两者在设计理念上高度对应,但在具体实现上由于依赖的基础设施不同而存在一些差异:

BigTable 概念HBase 对应概念说明
GFSHDFS底层分布式文件系统
Chubby(分布式锁服务)ZooKeeper协调服务
SSTable(Sorted String Table)HFile磁盘上的有序不可变文件
TabletRegion表的水平分片
Tablet ServerRegionServer负责若干 Region 的服务节点
MasterHMaster负责元数据管理的主节点
MemtableMemStore内存中的写缓冲区

HBase 于 2007 年作为 Apache Hadoop 的子项目启动,2010 年成为 Apache 顶级项目。它的核心贡献是将 BigTable 的设计思想与 Hadoop 生态无缝结合,使得大数据处理(HDFS + MapReduce)与在线随机访问(HBase)能够共享同一套基础设施。

设计哲学传承

BigTable 论文是 HBase 所有设计决策的”宪法”。当你对 HBase 的某个行为感到困惑时,回到 BigTable 论文中往往能找到答案——那里记录了 Google 工程师们面对真实业务问题时做出的艰难取舍。


第 4 章 列族模型的设计哲学:在行存储与纯列存储之间找到平衡

4.1 三种存储模型的本质差异

在进入列族模型的详细讨论之前,我们需要先理解三种基本的存储组织方式,以及它们各自的优势与局限。

行存储(Row-Oriented Storage)

行存储是关系型数据库的传统模式。每行的所有列数据在磁盘上连续存放。

-- 示意:行存储的物理布局
[Row 1: user_id=001, name="Alice", age=28, city="Beijing", email="alice@xx.com"]
[Row 2: user_id=002, name="Bob",   age=32, city="Shanghai",email="bob@xx.com"]
[Row 3: user_id=003, name="Carol", age=25, city="Guangzhou",email="carol@xx.com"]

优势:读取整行数据时,一次顺序 I/O 可以获取该行的所有列,效率极高。写入一条完整记录时,也只需要一次顺序写。这使得行存储非常适合 OLTP(在线事务处理)场景——插入订单、更新用户信息、查询某条订单的全部详情。

劣势:当只需要读取少数几列时(比如”统计所有用户的城市分布”,只需读 city 列),行存储必须读取整行的所有数据,绝大部分 I/O 都是无效的。在宽表(列数很多)的场景下,这种浪费非常严重。

列存储(Column-Oriented Storage)

列存储将同一列的所有行数据在磁盘上连续存放。

-- 示意:列存储的物理布局
[Column user_id: 001, 002, 003, ...]
[Column name:    "Alice", "Bob", "Carol", ...]
[Column age:     28, 32, 25, ...]
[Column city:    "Beijing", "Shanghai", "Guangzhou", ...]

优势:分析查询(OLAP)时,如果只需要读取少数几列,列存储可以精确地只读取这几列的数据,避免无效 I/O。同列数据类型相同,压缩率极高(如 city 列可能只有几十个不同的值,用字典压缩效果极佳)。这使得列存储非常适合数据仓库、OLAP 分析。

劣势:读取完整一行需要访问多个不同的列文件,I/O 模式变得复杂。更糟糕的是,插入一行数据需要修改所有列的存储文件,写入开销极高。这使得纯列存储不适合高并发随机写入的场景。

列族存储(Column-Family Store)

列族存储是行存储和列存储的折中方案,也是 BigTable/HBase 的核心创新之一。

其基本思想是:将列分组为若干”列族”(Column Family),同一列族内的列用行存储的方式组织,不同列族之间用列存储的方式隔离。

-- 示意:列族存储的物理布局
-- Column Family "info"(存储基本信息)
[Row 001: info:name="Alice", info:age=28]
[Row 002: info:name="Bob",   info:age=32]

-- Column Family "address"(存储地址信息)
[Row 001: address:city="Beijing",  address:zip="100000"]
[Row 002: address:city="Shanghai", address:zip="200000"]

这种设计的精妙之处在于:

  • 同一列族内的数据存储在同一个 HFile 中,读取列族内的多列时无需跨文件 I/O
  • 不同列族物理隔离,查询时可以只读取相关列族,避免加载不需要的数据
  • 每个列族可以独立配置压缩算法、TTL、版本数等属性
  • 稀疏列不占用存储空间

4.2 为什么不直接用纯列存储:写入的代价

理解列族模型的关键,在于理解 HBase 的核心场景是高并发随机写入 + 随机点查,而不是 OLAP 分析查询。

纯列存储(如 Apache ParquetApache ORC)为什么不适合 HBase 的场景?

问题根源:纯列存储的不可变性

以 Parquet 为例,它将每列数据存储在独立的 Column Chunk 中,每个 Column Chunk 按页(Page,通常 1MB)组织。当你需要更新某行某列的值时,你不能只修改该列的某个字节——因为列数据是压缩存储的,一旦修改就需要重新压缩整个 Page,然后更新该 Column Chunk,这个代价极高。

更严重的问题是:纯列存储格式通常是批量写入的,它假设数据是一批一批地被写入(如每天的 ETL 任务),而不是每秒数十万次的随机单行写入。

HBase 的 LSM-Tree 存储引擎(将在第 04 篇文章中详细讲解)通过将随机写入转化为顺序写入,解决了高并发写入的问题。但这套机制与纯列存储格式是不兼容的——LSM-Tree 的核心是 MemStore(内存中的写缓冲)和 HFile(磁盘上的有序不可变文件),而 HFile 的基本组织单位正是列族(Column Family)。

4.3 列族设计的工程约束:为什么列族数量不能太多

这是一个 HBase 生产实践中非常重要的约束,也是理解列族模型物理本质的好入口。

每个列族在 RegionServer 上对应一个独立的 Store,每个 Store 包含:

  • 一个 MemStore(内存写缓冲,默认 128MB)
  • 若干个 HFile(磁盘上的有序文件)

如果一张表有 3 个列族,那么该表的每个 Region 在 RegionServer 上就会有 3 个 Store,对应 3 个 MemStore,占用 3 × 128MB = 384MB 的内存。如果你有 100 个 Region,就是 3 × 100 × 128MB = 38.4GB 的 MemStore 内存占用。

这就是为什么 HBase 官方文档强烈建议列族数量不超过 3 个,绝大多数场景下 1 个列族足够——过多的列族会导致内存爆炸,同时增加 Flush 和 Compaction 的复杂度(将在后续文章中详细讨论)。

生产避坑

列族数量直接影响内存消耗和 Compaction 压力。 如果你发现自己想要创建 5 个以上的列族,大概率是设计思路出了问题——这些”列族”更可能应该是不同的表(Table),而不是同一张表的不同列族。

4.4 列族内的”列”:真正的稀疏灵活性

理解列族的另一个关键点:在 HBase 中,列族(Column Family)在建表时预先定义,但列族内的列限定符(Column Qualifier)可以在运行时动态添加,数量无限制

这与关系型数据库的 ALTER TABLE ADD COLUMN 有本质区别:

-- HBase 伪代码:动态添加列,无需 Schema 变更
put 'user_table', 'row_001', 'info:name', 'Alice'    -- 添加 info:name 列
put 'user_table', 'row_001', 'info:new_feature', '42' -- 随时添加新列,无需 DDL

-- 不同行可以有完全不同的列,不影响彼此
put 'user_table', 'row_002', 'info:age', '28'         -- row_002 没有 name,有 age
put 'user_table', 'row_002', 'info:location', 'SH'    -- row_002 有 location,row_001 没有

这种设计被称为**“稀疏列”**。在 HBase 的物理存储中,不存在的列不占用任何空间——它们根本就不在 HFile 中。这与关系型数据库的 NULL 值有本质区别:RDBMS 的 NULL 在某些情况下仍会占据元数据空间,而 HBase 的缺失列是真正的”不存在”。

这个特性使得 HBase 非常适合以下场景:

  • 用户行为矩阵:数亿用户 × 数百万商品的点击/购买矩阵,绝大多数用户只与极少数商品有交互,稀疏度极高
  • 动态标签系统:每个用户可以被打上任意多的标签,标签以列限定符形式存储,标签名就是列名
  • 网页特征存储:不同 URL 有不同的特征维度,不需要为所有 URL 预分配所有可能的特征列

第 5 章 数据如何组织:从概念视图到物理存储的映射

5.1 HBase 的概念视图:一张逻辑上的宽表

让我们以 Google BigTable 论文中的经典例子——网页存储表——来理解 HBase 的数据视图。

假设我们有一张存储网页信息的 HBase 表,表名为 webtable,包含两个列族:

  • contents:存储页面的 HTML 内容
  • anchor:存储指向该页面的锚文本(每个外链是一列)

在概念视图上,这张表看起来像这样:

行键                列族:列限定符              时间戳       值
-------------------------------------------------------------------
com.cnn.www        contents:html            t9          "<html>..."
com.cnn.www        contents:html            t8          "<html>..."(旧版本)
com.cnn.www        anchor:cnnsi.com         t9          "CNN"
com.cnn.www        anchor:my.look.ca        t8          "CNN.com"
com.example.www    contents:html            t5          "<html>..."

这里有几个关键的观察:

  1. com.cnn.www 这一行有 anchor:cnnsi.comanchor:my.look.ca 两个列,而 com.example.www 没有任何 anchor 列——稀疏性体现
  2. contents:html 存储了两个版本(时间戳 t9 和 t8)——多版本机制体现
  3. 行键采用了反转域名格式(com.cnn.www 而不是 www.cnn.com),这样所有 CNN 旗下的 URL 在按字节序排序时会聚集在一起——行键设计原则体现

5.2 行键排序的物理意义:范围扫描的效率来源

HBase 中所有行都按行键的字节序排序存储。这意味着:

  • user_00001 存储在 user_00002 之前
  • user_00002 存储在 user_00010 之前(字典序,而不是数值序)
  • zz_user 存储在所有 a_userb_user 之后

这个排序特性对查询效率有深远的影响:

范围扫描(Scan)是高效的:如果你想查询所有 UID 在 user_10000user_20000 之间的用户数据,HBase 只需要定位到 user_10000 所在的 Region,然后顺序读取,直到 user_20000——这是一次顺序 I/O,非常高效。

点查(Get)也是高效的:定位特定行键时,HBase 利用 Region 的起止行键范围和 HFile 的 Block 索引,通过 O(log N) 次索引查找定位到目标行。

行键设计决定数据局部性:如果你把用户的所有行为记录都用 user_id + timestamp 作为行键,那么同一用户的所有数据在物理上是相邻的,读取某用户的历史记录只需要一次短距离的范围扫描。

这一点与关系型数据库的聚簇索引(Clustered Index)思想完全一致——但在 HBase 中,行键就是唯一的聚簇维度,没有第二选择。

生产避坑

行键设计是 HBase 最重要的 Schema 设计决策,重要性远超列族设计。一旦表建好并写入数据,修改行键需要全表重写,代价极高。在设计阶段,必须充分考虑:什么是最常见的访问模式(点查还是范围扫描)?如何避免热点(单调递增的行键会导致所有写入集中到最后一个 Region)?

5.3 物理存储视图:列族的物理隔离

在物理存储层面,上面的概念视图实际上被分成了两个独立的存储单元:

Column Family “contents” 的 HFile 内容:

com.cnn.www/contents:html/t9  →  "<html>..."(HTML 内容)
com.cnn.www/contents:html/t8  →  "<html>..."(HTML 旧版本)
com.example.www/contents:html/t5 →  "<html>..."

Column Family “anchor” 的 HFile 内容:

com.cnn.www/anchor:cnnsi.com/t9  →  "CNN"
com.cnn.www/anchor:my.look.ca/t8 →  "CNN.com"

注意:com.example.www 在 “anchor” 的 HFile 中不存在任何记录——没有占位符,没有 NULL,就是物理上不存在。这就是”稀疏”的真正含义。

如果你只查询 “anchor” 列族,HBase 完全不需要读取 “contents” 列族的 HFile,I/O 完全隔离。


第 6 章 HBase 与同类系统的横向对比:找到正确的使用场景

6.1 HBase vs. Cassandra:同根异枝的架构分歧

Apache Cassandra 同样是受 BigTable 启发的列族存储系统(还受 Amazon Dynamo 论文影响),但两者在架构上有根本性的差异:

维度HBaseCassandra
一致性模型强一致性(单行操作)最终一致性(可调节一致性级别)
架构模式主从架构(HMaster + RegionServer)无中心对等架构(所有节点对等)
数据分布基于行键范围分片(Region)基于一致性哈希环分片
底层存储依赖 HDFS(外部文件系统)自管理本地磁盘存储
顺序扫描天然支持,按行键范围高效扫描支持但效率较低(哈希打散了顺序性)
写入延迟稍高(需要写 WAL 到 HDFS)较低(直接写本地磁盘)
运维依赖重(依赖 ZooKeeper + HDFS)轻(自包含,依赖少)
适合场景Hadoop 生态,需要与 MapReduce/Spark 集成纯在线服务,多数据中心,高可用优先

HBase 选择强一致性和主从架构,牺牲了部分可用性(HMaster 故障期间无法执行 DDL),换来了简单的一致性模型(不需要处理写入冲突)和对范围扫描的天然支持。Cassandra 选择最终一致性和对等架构,获得了更高的写入可用性和更低的运维复杂度,但放弃了行键的顺序性。

选择建议:如果你的系统已经在 Hadoop 生态中,需要 HBase 数据与 Spark/MapReduce 作业无缝对接,选 HBase。如果你是纯在线服务,不依赖 Hadoop,需要多数据中心复制,选 Cassandra。

6.2 HBase vs. Redis:不同抽象层次的存储系统

有工程师会问:HBase 和 Redis 都能做随机键值查询,为什么不用 Redis 来替代 HBase?

这是一个对两个系统的定位有根本性误解的问题:

维度HBaseRedis
数据规模PB 级GB 到 TB 级(受内存限制)
存储介质磁盘(HDFS),内存只是缓存主要在内存中
数据持久化天然持久化(HDFS + WAL)需要配置 RDB/AOF
数据结构稀疏宽表,多版本丰富数据结构(Hash、List、Set…)
查询能力支持范围扫描(Scan)不支持范围扫描(除有序集合外)
延迟毫秒级微秒级
成本低(磁盘廉价)高(内存昂贵)

Redis 是内存数据库,适合缓存、实时计数、排行榜等对延迟要求极高(微秒级)但数据量不超过内存容量的场景。HBase 是磁盘数据库,适合 PB 级海量历史数据的随机访问,延迟在毫秒级。

在实际生产架构中,Redis 和 HBase 往往是配合使用而不是竞争关系:Redis 作为 HBase 的热数据缓存层,承接高频访问的热点数据;HBase 作为持久化存储层,存储全量历史数据。

6.3 HBase vs. 关系型数据库:何时不应该用 HBase

HBase 不是万能的,以下场景中,传统 RDBMS 是更好的选择:

场景 1:需要复杂 SQL 查询和多表 JOIN

HBase 没有 SQL 引擎(虽然有 Apache Phoenix 这个 SQL 层),不支持多表 JOIN,不支持聚合函数的实时计算(需要 MapReduce 离线处理)。如果你的业务逻辑依赖复杂的关联查询,RDBMS 是更合适的选择。

场景 2:需要 ACID 多行事务

HBase 只保证单行操作的原子性(行级 ACID),不支持跨行事务。如果你需要”扣减库存同时新建订单”这样的多行原子操作,HBase 无法满足。

场景 3:数据量在 TB 以下且增长缓慢

HBase 的运维成本较高,依赖 HDFS + ZooKeeper,需要一定的运维门槛。如果数据量不是特别大,RDBMS 配合读写分离就能很好地解决问题,引入 HBase 会增加不必要的复杂度。

场景 4:需要二级索引

HBase 的唯一内置索引是行键(主键)。如果你需要按非行键列进行查询(比如”查询所有 city='Beijing' 的用户”),HBase 需要全表扫描,代价极高。这种情况下,要么用 Elasticsearch 等搜索引擎作为二级索引,要么选择原生支持二级索引的数据库。

核心使用场景总结

HBase 的甜蜜区间是:数据量在 TB 到 PB 级别、需要按行键(或行键前缀)进行随机点查和范围扫描、写入量大(每秒数万到百万级别)、数据是稀疏宽表或需要多版本存储。典型场景包括:消息系统的历史消息存储、用户行为日志的存档与查询、时序数据存储(如监控指标)、网页爬虫数据存储。


第 7 章 HBase 的生态位:在大数据架构中的精准定位

7.1 Lambda 架构中的 HBase

Lambda 架构 是大数据领域经典的架构模式,它将数据处理分为三层:

  • 批处理层(Batch Layer):用 MapReduce/Spark 对全量历史数据进行离线计算,结果准确但延迟高(小时级)
  • 速度层(Speed Layer):用 Storm/Flink 对实时增量数据进行流计算,延迟低(秒级)但数据不完整
  • 服务层(Serving Layer):将批处理和流计算的结果合并,对外提供低延迟查询服务

在这个架构中,HBase 通常扮演服务层的角色,原因很简单:

  1. 批处理层(Spark)的计算结果可以直接通过 Spark-HBase 连接器写入 HBase
  2. 速度层(Flink)的实时结果也可以通过 HBase 客户端增量更新到 HBase
  3. 服务层的在线查询直接读取 HBase,延迟在毫秒级

HBase 同时支持大批量写入(用于批处理层的结果写入)和随机读写(用于在线查询),是服务层的理想载体。

7.2 实时 OLAP 架构中的 HBase

随着业务对数据时效性要求的提高,Kappa 架构 逐渐替代 Lambda 架构,HBase 在其中的角色也随之演化。

在实时数仓场景中,常见的技术栈是:

Kafka(消息队列)→ Flink(实时计算)→ HBase(实时明细存储)
                                    ↘ ClickHouse(实时聚合分析)

HBase 在这里负责存储明细数据(比如每一条用户行为记录),提供按用户 ID 快速查询历史行为的能力。ClickHouse 负责聚合分析(比如统计某商品的总点击量),提供 OLAP 查询能力。两者互补,分工明确。

7.3 HBase 在互联网大厂的真实应用

HBase 在国内外互联网大厂中有着广泛的生产应用:

阿里巴巴:阿里云 HBase 服务支撑了阿里内部交易、日志、用户画像等众多业务。据阿里 2019 年的技术分享,其 HBase 集群总规模超过 100PB,日写入量超过 100 万亿条记录。

Facebook(Meta):Facebook 的消息系统使用了类 HBase 的系统(实际上是 Facebook 自研的 Apache Cassandra,但底层思想与 HBase 相同),存储数千亿条消息记录。

小米:小米的用户行为数据平台使用 HBase 存储用户行为日志,支撑了小米 Push 推送系统的个性化推荐能力。

滴滴:滴滴的行程记录、司机评分等核心数据使用 HBase 存储,数据量在 PB 级别。

这些案例共同印证了 HBase 在海量数据、高并发读写场景下的工程价值。


第 8 章 设计哲学的统一:HBase 到底在解决什么问题

8.1 三个核心问题的回答

现在我们可以系统地回答本文开头提出的三个核心问题。

问题一:大规模互联网数据遇到了什么样的存储危机?

互联网数据的核心特征是:规模超大(PB 级)、结构稀疏(不同实体属性差异大)、写入频繁(每秒百万级写入)、访问模式既有批量分析又有随机查询。关系型数据库无法水平扩展,无法高效存储稀疏数据,在高并发写入下性能急剧退化。HDFS 解决了批量存储问题,但无法提供随机访问能力。这个空缺正是 HBase 要填补的。

问题二:为什么 HDFS + MapReduce 无法解决随机访问问题?

HDFS 的设计哲学是”大文件、顺序读写、高吞吐”,没有任何按记录键快速定位的索引机制。MapReduce 的执行模式是”全量扫描 + 筛选”,每次查询的代价是扫描整个数据集。这两者的结合天然不支持毫秒级的随机点查,而在线服务恰恰需要毫秒级响应。

问题三:列族模型相较于行存储、纯列存储做了哪些取舍?

列族模型放弃了纯列存储的极致压缩比和分析查询性能(不适合全列扫描的 OLAP 分析),也放弃了行存储的全行 I/O 效率(不适合频繁读取完整宽行)。但它在两者之间找到了甜蜜点:通过列族内聚、列族间隔离,在支持稀疏宽表的同时保持合理的 I/O 效率;通过 MemStore + HFile 的 LSM 架构,在 HDFS 之上实现了高并发随机写入。

8.2 “不够用”的辩证理解:没有银弹

理解了上述背景,我们可以更准确地理解本文标题”为什么 HDFS 不够用”——这里的”不够”不是说 HDFS 设计得不好,而是说特定业务场景超出了 HDFS 的设计边界

软件系统的设计本质上是在约束条件下做出取舍(Tradeoff)。HDFS 的设计取舍是:牺牲随机访问能力,换取高吞吐的批量顺序读写能力。这个取舍在批处理场景下是完全正确的。

HBase 在 HDFS 之上引入了自己的取舍:通过 MemStore 缓冲随机写入,通过 HFile 的 Block 索引支持随机读,通过 WAL 保证持久性——这套机制给随机访问能力带来了代价:写入放大(Write Amplification,同一份数据被写多次)、读取放大(Read Amplification,可能需要读取多个 HFile)、空间放大(Space Amplification,需要额外空间进行 Compaction)。这些代价是 HBase 选择 LSM-Tree 这一存储引擎时必须接受的。

设计哲学

理解一个技术系统,最重要的不是记住它的 API 和配置参数,而是理解它的设计取舍(Tradeoffs):它优化了什么,为此付出了什么代价,在什么场景下这种取舍是值得的。这是贯穿整个 HBase 专栏的核心思维框架。

8.3 后续文章的铺垫:逐层揭开 HBase 的面纱

经过本文的讨论,我们建立了理解 HBase 的基本框架:

  • HBase 是什么:HDFS 之上的随机读写层,受 BigTable 启发,采用列族存储模型
  • HBase 为什么出现:填补 HDFS 批处理能力与在线随机访问需求之间的空缺
  • 列族模型的核心逻辑:行存储与列存储的工程折中,稀疏列的物理实现,列族的物理隔离
  • HBase 的适用场景:海量、稀疏、高并发写入、按行键随机点查/范围扫描

在接下来的文章中,我们将逐步深入:

  • 第 02 篇:精确理解 HBase 的四维坐标模型(RowKey/CF/Qualifier/Timestamp),以及多版本机制的工程实现
  • 第 03 篇:理解 Master、RegionServer、ZooKeeper 三个组件如何分工协作
  • 第 04 篇:深入 LSM-Tree 存储引擎,理解 MemStore 和 HFile 的设计逻辑
  • 后续:读写链路、Compaction、Region 管理、高可用与生产调优

每一层都会回答一个关键问题:这个设计选择是为了解决什么问题,如果不这样设计会怎样


思考题

  1. HBase 的列族(Column Family)设计要求在建表时预先定义,但列限定符(Column Qualifier)可以动态添加,数量没有上限。这种”半结构化”设计在存储层是如何实现的?如果一张表有 100 万个不同的列限定符,HBase 的存储效率与关系型数据库相比有何优劣?
  2. HBase 不支持跨行事务,每次操作只保证单行的原子性。在需要多行原子操作的业务场景(如转账:扣减 A 行余额,增加 B 行余额),通常的工程解法是什么?HBase 的 checkAndPutcheckAndDelete 提供了条件写入,它能否用来实现乐观锁模式的多行事务?
  3. BigTable 论文提出了列族存储的思想,HBase 是它的开源实现。但 BigTable 的底层存储是 GFS,HBase 的底层是 HDFS。HDFS 是为大文件顺序读写优化的,而 HBase 需要支持随机读写。HBase 是如何通过 MemStore + LSM-Tree 的设计,在 HDFS 这个”不支持随机写”的文件系统上实现高效随机写入的?

参考资料

  • [1] Chang, F., et al. “Bigtable: A Distributed Storage System for Structured Data.” OSDI, 2006.
  • [2] Ghemawat, S., Gobioff, H., & Leung, S. T. “The Google File System.” SOSP, 2003.
  • [3] Dean, J., & Ghemawat, S. “MapReduce: Simplified Data Processing on Large Clusters.” OSDI, 2004.
  • [4] Apache HBase Reference Guide: https://hbase.apache.org/book.html
  • [5] Lars George. “HBase: The Definitive Guide.” O’Reilly Media, 2011.
  • [6] Stonebraker, M. “SQL Databases v. NoSQL Databases.” Communications of the ACM, 2010.