[笔记] Kudu 存储引擎 | Kudu 文件系统

Block 抽象接口 操作系统把磁盘抽象成文件,Kudu 则在文件之上再加了一层抽象——Block。在 Kudu 中,一列数据、一个 BloomFilter、一份主键索引,最终都变成一个或多个 Block 写入磁盘。Block 是 Kudu 存储引擎与本地文件系统之间的分界线:上层组件只需面对 Block 接口的 Append / Read,不必关心底层是一个独立文件(FileBlockManager)还是日志容器中的一段字节区间(LogBlockManager)。 这种隔离方式与 Unix 的设备抽象思路一致——Unix 内核用 struct file 加上 read/write 函数指针屏蔽底层设备差异;Kudu 用 Block 基类加上虚函数屏蔽底层文件系统的组织差异。 BlockId:Block 的身份标识 每个 Block 都有一个全局唯一的身份标识 BlockId(src/kudu/fs/block_id.h),本质上是一个 64 位无符号整数: 1 2 3 4 class BlockId { private: uint64_t id_; }; BlockId 可以序列化到 protobuf(CopyToPB / FromPB),也可以打印成 16 位十六进制字符串供调试。它是不透明的——上层代码不应假设 ID 的分配规则或数值含义,只需把它当作一把钥匙,用来在 BlockManager 中取回对应的数据。 Block 基类 Block 是所有 Block 的基类,接口极为简洁——只有一个方法: 1 2 3 4 5 class Block { public: virtual ~Block() = default; virtual const BlockId& id() const = 0; }; id() 返回该 Block 的 BlockId。这足以让上层代码通过 ID 引用任何 Block,而不必关心它是可读的还是可写的。Block 的两个子类——WritableBlock 和 ReadableBlock——分别定义了写路径和读路径的完整接口。 ...

发表于:2024-10-23  ·  更新于: 2024-10-23

[笔记] Kudu 存储引擎 | memrowset 实现详解

数据结构 MemRowSet 是 Kudu tablet 中用来暂存新写入数据的内存结构。它的底层存储是一棵并发 B-tree(CBTree),每个叶节点条目存放一个 key-value 对:key 是主键的编码形式(字典序 = 主键逻辑序),value 是一个 MRSRow。 为了支持快照一致性,MemRowSet 从不原地更新已插入的行数据——所有后续变更都以 Mutation 节点的形式挂在行的 redo 链表上,相当于每行自带一条"redo log"。当 MemRowSet 被 flush 到磁盘时,所有内存——包括行数据、Mutation 节点、变长列数据——都从 Arena 中统一释放。 MRSRow MRSRow 是行在 CBTree 中的存储表示。每个 MRSRow 占据一段连续内存,包含一个定长的 Header 和紧随其后的行数据。根据它的构造函数看出其内存布局如下: 1 2 3 4 5 传入的 Slice s 指向的连续内存: |<---------- s.size() ---------->| | Header (24B) | row_data | ↑ ↑ header_ row_slice_ (remove_prefix 后) Header 有三个字段:insertion_timestamp 记录行的插入时间戳,供 MVCC 读取时判断可见性;redo_head 和 redo_tail 分别指向该行 Mutation 链表的头和尾。新插入的行 redo_head/redo_tail 均为 nullptr——它没有任何变更历史。每当一次 UPDATE 或 DELETE 到来,就会在链表尾部追加一个新的 Mutation 节点: ...

发表于:2024-09-24  ·  更新于: 2026-04-15

Kudu Tablet Server 非事务写入流程分析

序 本文记录 kudu 源码阅读笔记,记录了 kudu tserver 端非事务写入的完整流程。 写入流程总览 Kudu 的一次“非事务”写(普通单/批行 Insert/Upsert/Update/Delete,不带 txn_id)在 tserver 侧需要经历以下阶段: Client 组装 WriteRequestPB 并通过 RPC 发送 (tserver_service.proto: Write). RPC 入口:TabletServiceImpl::Write() 做权限、资源(内存 / 线程池)与节流检查,构造 WriteOpState。 提交给 TabletReplica::SubmitWrite(),创建 WriteOp 与 OpDriver(LEADER 模式)。 OpDriver::ExecuteAsync() 进入 Prepare 阶段:解码行、锁定 schema、行级锁 / 分区锁(非事务写仅行锁)、权限校验。 (LEADER)向 RAFT 复制:给 ReplicateMsg 分配 timestamp,RaftConsensus::Replicate() 开始复制;Follower 侧由共识回放直接创建 Follower OpDriver 并 Start。 RAFT 提交(ReplicationFinished())后进入 Apply 阶段:WriteOp::Apply() 调用 Tablet::ApplyRowOperations() 执行实际插入/更新/删除(写入 MemRowSet / DeltaMemStore)。 生成 CommitMsg 写入 WAL;WriteOp::Finish() 更新 MVCC 状态与各种 metrics;释放行锁、schema 锁。 RPC 回调发送 WriteResponsePB 给客户端(含 per-row errors、资源 metrics、timestamp 等)。 流程总览图如下: ...

发表于:2024-08-07  ·  更新于: 2024-08-07

Kudu 概览

为什么需要 Kudu 在 Hadoop 生态中结构化的数据通常使用两种方式进行存储:一是通过 Apache Parquet 等二进制数据格式,将静态数据集存储在 HDFS 上,二是将可变数据以半结构化的方式存储在 HBase 中。这里面临的问题是存储在 HDFS 上的数据无法提供单个记录的随机访问,HBase 中存储的数据尽管允许低延迟的读取和写入,但是基于 SQL 分析的应用中顺序读取的吞吐方面要远远落后于静态文件格式。 当需要在一个系统中需要这两种访问模式的时候,HDFS 上的静态数据集提供的分析性能力与 HBase 的低延迟行级随机访问能力之间的差距就需要复杂的架构设计,一种可以想到的方式就是采用类似于 Lambda 架构的方案:架构中通过 HBase 构建一个实时层,通过 HDFS 构建一个批处理层。数据流式的进入实时层,然后定期将数据导入到 Parquet 批处理层,以提供历史分析。这一架构看似能够解决这个差异性的问题,但是却有这样几个缺点: 由于要管理两个层的数据流之间的同步,这样应用层的架构会变得复杂; 需要跨两个系统管理一致性备份、安全和监控; 新数据达到数据在实时层和可用于分析的批处理层之间会表现出滞后; 现实世界中,系统需要容纳后到达的数据、还需要对迁移到不可变存储中数据进行删除,这一过程会设计到昂贵的分区重写以及手动干预。 为了解决上面的问题,Kudu 被设计以一种折中的方式解决这些问题。Kudu 是一种全新设计和实现的新存储系统,旨在填补 HDFS 等高吞吐量顺序访问存储系统与 HBase 或 Cassandra 等低延迟随机访问系统之间的空白。特别是,Kudu 为行级插入、更新和删除提供了一个简单的 API,同时以类似于 Parquet的吞吐量提供表扫描。 用户视角下的 Kudu 表与模式 在用户视角下,Kudu 是一个结构化数据表的存储系统,可以有任意数量的表,每个表都有一个由有限数量的列组成的明确定义的模式。每列具有列名和类型。对于列设置类型有这样两个好处: 特定的类型在进行列式存储的时候可以进行充分的压缩; 显式类型允许我们将类似 SQL 的元数据公开给其他系统。 Kudu 表有主键,主键负责唯一性约束,并充当更新和删除行的唯一索引。同样与关系型数据库类似的是,用户在创建表的时候需要定义表的架构,插入不存在的列或者违反主键唯一性约束都会引起错误,用户可以添加删除列但是不能删除主键列。 与关系型数据库不同的是 Kudu 表不提供二级索引。 写操作 创建表之后的写操作必须指定主键,Kudu 提供了多种语言的客户端。这些 API 允许精确控制批处理和异步错误处理,以在执行批量数据操作(例如数据加载或大型更新)时分摊往返成本。Kudu 不提供跨行事务。 读操作 Kudu 提供 Scan 操作从表中检索数据,扫描时,用户可以添加谓词进行过滤,支持列和常量值之间的比较以及主键范围,谓词操作会下推到 Kudu 后台,减少数据扫描传递时的磁盘和网络开销。 ...

发表于:2024-07-31  ·  更新于: 2024-07-31