Kudu底层架构:混合存储模型解析

Kudu系统的架构设计融合了Bigtable、HBase以及Spanner等系统的核心理念。从其开发团队构成可以看出,多位核心成员来自HBase社区并担任重要角色,这使得Kudu的设计理念深受HBase的影响,同时也体现出创新性的改进。
与依赖HDFS这类高抽象层次分布式文件系统的方案不同,Kudu选择自主研发了一套基于表(Table)/分区(Tablet)/副本(Replica)视图的底层存储架构。这一设计决策基于以下几个关键考量:
• 实现高效的列式数据检索能力 • 支持快速的单条记录随机更新操作 • 提供更可预测的查询性能保证

在Kudu中,每个数据表被划分为多个tablet单元,每个tablet包含元数据信息及若干个行集(RowSet)。RowSet由一个内存行集(MemRowSet)和多个磁盘行集(DiskRowSet)组成。每个DiskRowSet又包含布隆过滤器(BloomFile)、主键索引(Ad_hoc Index)、基础数据(BaseData)、增量内存(DeltaMem)以及多个重做日志(RedoFile)和撤销日志(UndoFile)(通常UndoFile只有一个)。
内存行集(MemRowSet):负责处理新数据的插入操作以及已存在于内存行集中的数据更新。当内存行集达到预设容量(默认为1GB或120秒)后,数据会被刷新到磁盘,形成多个磁盘行集(DiskRowSet)。
磁盘行集(DiskRowSet)用于存储历史数据的变更操作。系统后台会定期对磁盘行集进行压缩(Compaction)操作,清理无效数据并合并历史版本,从而降低查询过程中的I/O开销。
布隆过滤器(BloomFile):根据磁盘行集中的键值生成布隆过滤结构,用于快速判断某个键值是否存在于当前磁盘行集中。
主键索引(Ad_hoc Index):建立键值与数据在磁盘行集中偏移位置的映射关系,支持快速定位。
基础数据(BaseData):内存行集刷新后形成的数据,按列存储且按主键有序排列。
撤销日志(UndoFile):存储基础数据生成之前的变更记录,通过在基础数据上应用撤销日志中的记录,可以获取历史数据版本。
重做日志(RedoFile):存储基础数据生成之后的变更记录,通过在基础数据上应用重做日志中的记录,可以获得最新数据版本。
增量内存(DeltaMem):用于缓存磁盘行集中数据的变更操作,当达到容量限制后会刷新到磁盘形成重做日志文件。
为实现上述设计目标,Kudu采用了一种类似"Fractured Mirrors"的混合列式存储架构。在底层实现中,Tablet被进一步细分为被称为RowSet的基本单元:

内存行集(MemRowSet)可类比HBase中的MemStore,而磁盘行集(DiskRowSet)则类似于HBase中的HFile。内存行集采用行视图存储方式,其数据结构基于B-Tree实现。
当内存行集中的数据被刷新到磁盘后,便形成磁盘行集(DiskRowSet)。磁盘行集以32MB为单位,按顺序划分为多个DiskRowSet。
DiskRowSet中的数据按列进行组织,与Parquet文件格式类似。这种设计为Kudu支持分析型查询奠定了基础。每一列数据存储在连续的内存区域,该区域进一步被划分为多个页面(Page),类似于HBase File中的Block。每个列页面可以采用特定的编码算法和通用压缩算法进行处理。
由于列页面支持编码和压缩,对单条记录的修改操作会变得相对复杂。
那么,Kudu是如何实现单条记录级别的更新/删除操作的呢?
与HBase类似,Kudu通过插入新的记录来描述更新或删除操作。一个DiskRowSet包含两部分数据:基础数据(Base Data)和变更数据(Delta Stores)。更新/删除操作生成的记录被保存在变更数据部分。

从上图(源自Kudu源代码工程)可以看出:
变更数据部分实际上包含重做(REDO)和撤销(UNDO)两部分,这里的REDO和UNDO概念与传统关系型数据库中的日志类似(在关系型数据库中,REDO日志记录了更新后的数据,可用于恢复尚未写入数据文件的已提交事务数据;而UNDO日志记录了事务修改前的数据,可用于事务失败时的回滚操作),但在实现细节上存在一些差异:
• 重做增量文件(REDO Delta Files)包含了基础数据自上次刷新/压缩以来的所有变更值。这些重做增量文件按时间戳顺序排列。 • 撤销增量文件(UNDO Delta Files)包含了基础数据在上次刷新/压缩之前的变更值。这种设计确保基于较早时间戳的查询能够获得一致性的数据视图。撤销增量文件按时间戳倒序排列。