Kudu
Kudu 介绍
背景介绍
在 Kudu 之前,大数据主要以两种方式存储:
- 静态数据 以 HDFS 作为存储引擎,适用于高吞吐的离线大数据分析场景。局限性就是数据无法随机读写。
- 动态数据 以 HBase、Cassandra 作为存储引擎,适用于大数据的随机读写场景。局限性就是没有高吞吐的大数据分析能力,不适用 OLAP。
从上面分析可知,这两种数据在存储方式上完全不同,进而导致使用场景完全不同,但是真实的场景中,边界可能没有那么清晰,面对既需要随机读写,又需要批量分析的大数据场景,该如何选择呢?这样的话,单纯的哪一种存储引擎都无法满足业务需求,我们需要组合大数据工具满足业务需求,如下所示:
如上图所示,数据实时写入 HBase,实时的数据更新也在 HBase 上完成,为了应对 OLAP 需求,我们定时将 HBase 数据写入静态的文件(如:HDFS Parquet)导入 OLAP 引擎(如:Impala、Hive、PrestoDB)。这一架构能满足既随机读写又满足 OLAP 的场景。但是也有缺点:(1)架构复杂。(2)时效性低。(3)难以应对后续的更新。
为了解决上述架构的这些问题,Kudu 应运而生。
从上图可以看出,Kudu 是一个折中的产品,
Kudu 是什么
Apache Kudu 是由 Cloudera 开源的存储引擎,可以他是一个融合了 HDFS 和 HBase 两者特性能的新组件,具备介于两者之间的新组件。Kudu 支持水平扩展,并且与 Cloudera Impala 和 Apache Spark 等查询计算引擎结合紧密。
应用场景
- 适用于那些既有随机读写,又有批量扫描分析的复合场景。
- 高计算量场景。
- 使用了高性能的存储设备,包括使用更多的内存。
- 支持数据更新,避免数据反复迁移。
- 支持跨地域的实时数据备份和查询。
架构
与 HDFS 和 HBase 相似也是主从架构,Kudu 使用单个 Master 节点,用来管理集群的元数据,并且使用任意数量的 Tablet Server(类似 HBase 中的 RegionServer)节点用来存储实际数据。可以部署多个 Master 节点来提高容错性。
Table
表(Table)是数据库中用来存储数据的对象,是有结构的数据集合。Kudu 表具有 schema 和全局有序的 primary key(主键)。Kudu 中的一个 table 会被水平分成多个 tablet 片段。
Tablet
一个 tablet 是一张 table 数据的连续片段,tablet 是 kudu 表的水平分区,类似于 HBase 的 region。每个 tablet 存储着一定连续 range 的数据(key),且 tablet 两两间的 range 不会重叠。一张标的所有 tablet 包含了这张表的所有 key 空间。tablet 会冗余存储。放置到多个 tablet server 上,并且在任何给定的时间点,其中一个副本被认为是 leader tablet,其余的被认为是 follower tablet。每个 tablet 都可以进行数据的读请求,但是只有 leader tablet 负责数据的写请求。
Tablet Server
tablet server 集群中的从节点,负责数据存储,并提供数据读写服务。一个 tablet server 存储了 table 表的 tablet,向 kudu client 提供读取数据服务。对给定的 tablet,一个 tablet server 充当 leader,其他 tablet server 充当该 tablet 的 follower 副本。只有 leader 节点才有写请求服务,然而 leader 或 follower 都提供读请求服务。一个 tablet server 可以服务多个 tablets,并且一个 tablet 可以被多个 tablet servers 服务着。
Master Server
master server 集群中的主节点,负责管理集群、元数据管理等功能。
Kudu 原理
table 与 schema
Kudu 设计是面向结构化存储的,因此,Kudu 的表需要用户在建表时定义它的 Schema 信息,这些 Schema 信息包含:列定义(含类型)、Primary Key 定义(用户指定的若干个列的有序组合)。数据的唯一性,依赖于用户所提供的 Primary Key 中的 Column 组合的值的唯一性。Kudu 提供了 Alter 命令来增删列,但位于 Primary Key 中的列是不允许删除的。
从用户角度看,Kudu 是一种存储结构化数据表的存储系统。在一个 Kudu 集群中可以定义任意数量的 table,每个 table 都需要预先定义好 schema。每个 table 的列数是确定的,每一列都需要有名字和类型,每个表中可以把其中一列或多列定义为主键。这么看来,Kudu 更像关系型数据库,而不是像 HBase、Cassandra 和 MongoDB 这些 NoSQL 数据库。不过 Kudu 目前还不能像关系型数据库一样支持二级索引。
Kudu 使用确定的列类型,而不是类似于
底层数据模型
Kudu 的底层数据文件存储
这套实现基于如下的几个设计目标:
- 可提供快速的列式查询
- 可支持快速的随机更新
- 可提供更为稳定的查询性能保障
详细
一张 table 会分为若干个 tablet,每个 tablet 包括 MetaData 元信息及若干个 RowSet。
RowSet 包含一个 MemRowSet 及若干个 DiskRowSet,DiskRowSet 中包含一个 BloomFile、AdhocIndex、BaseData、DeltaMem 及若干个 RedoFile 和 UndoFile。
- MemRowSet: 用于新数据 insert 及已在 MemRowSet 中的数据的更新,一个 MemRowSet 写满后会将数据刷到磁盘形成若干个 DiskRowSet。默认是 1G 或者 120s。
- DiskRowSet: 用于老数据的变更,后期定期对 DiskRowSet 做 compaction,以删除没用的数据及合并历史数据,减少查询过程中的 IO 开销。
- BloomFile: 根据一个 DiskRowSet 中的 key 生成一个 bloomfilter,用于快速模糊定位某个 key 是否在 DiskRowSet 中。
- AdhocIndex: 主键的索引,用于定位 key 在 DiskRowSet 中的具体哪个偏移位置。
- BaseData: 是 MemRowSet flush 下来的数据,按列存储,按主键有序。
- UndoFile: 是基于 BaseData 之前时间的历史数据,通过在 BaseData 上 apply UndoFile 中的记录,可以获得历史数据。
- RedoFile: 是基于 BaseData 之后时间的变更记录,通过在 BaseData 上 apply RedoFile 中的记录,可以获得较新数据。
- DeltaMem: 用于 DiskRowSet 中数据的变更,先写到内存中,写满后 flush 到磁盘形成 RedoFile。
对比分析
REDO/UNDO 与关系型数据库中的 REDO/UNDO log 类似(关系型数据库中,REDO Log 记录更新后的数据,可用来恢复尚未写入 Data File 的已成功事务更新数据。而 UNDO Log 用来记录事务更新之前的数据,可用以在事务失败时进行回滚)
MemRowSets 可以对比理解成 HBase 中的 MemStore,而 DiskRowSets 可理解成 HBase 中的 StoreFile/HFile。MemRowSets 中的数据被 Flush 到磁盘之后,形成 DiskRowSets。DiskRowSets 中的数据按照 32MB 大小为单位,按序划分为一个个的 DiskRowSet。DiskRowSet 中的数据按照 Column 进行组织,与 Parquet 类似。这是 Kudu 可支持一些分析性查询的基础。每个 Column 数据被存储在一个相邻的数据区域,而这个数据区域进一步被细分为一个个的小的 Page 单元,与 HBase File 中的 Block 类似,既然可对 Column Page 采用 Encoding 和 Compression 算法,那么对单条记录的更改就会比较困难了。
前面提到了 Kudu 可支持单条记录级别的更新/删除,是如何做到的?
DiskRowSet 是不可修改了,那么 Kudu 要如何应对数据的更新呢?
如上图所示,数据从 MemRowSet 刷到磁盘后形成了一份 DiskRowSet(只包含 base data),每份 DiskRowSet 在内存中都会有一个对应的 DeltaMemStore,负责记录此 DiskRowSet 后续的数据变更(更新、删除)。
DeltaMenStore 内部维护一个 B-树索引,映射到每个 row_offset 对应的数据变更。DeltaMemStore 数据增长到一定程度后转化成二进制文件存储到磁盘,形成一个 DeltaFile,随着 base data 对应数据的不断变更,DeltaFile 逐渐增长。
tablet 发现过程
当创建 Kudu 客户端时,其会从 Master 主服务器上获取 tablet 位置信息,然后直接与该 tablet 的服务器(Tablet Server)进行通讯。为了优化读取和写入路径,客户端将保留该信息的本地缓存,以防止他们在每次请求是需要查询主机的 tablet 位置信息。随着时间的推移,客户端的缓存可能变得过时,并且当写入发送到不再是 tablet leader 的 tablet server 时,则将被拒绝。然后客户端将通过查询 Master 主服务器发现新的 leader 位置,来更新其缓存。
写流程
当 Client 请求写数据时,先根据主键从 Master 中获取要访问的目标 tablets,然后到依次对应的 tablet 获取数据。
因为 Kudu 表存在主键约束,所以需要进行逐渐是否已经存在的判断,这里就涉及到之前说的索引结构对读写的优化了。一个 tablet 中存在很多个 RowSets,为了提升性能,我们要尽可能减少扫描的 RowSets 数量。
首先,我们先通过每个 RowSet 中记录的主键的(最大最小)范围,过滤掉一批不存在目标主键的 RowSets。
然后,再根据 RowSet 中的 Bloom Filter,过滤掉确定不存在目标主键的 RowSets。
最后,再通过 RowSets 中的 B-树索引,精确定位目标主键是否存在。
如果主键已经存在,则报错(主键重复),否则就进行写数据(写 MemRowSet)。
读流程
数据读取过程大致如下:先根据要扫描数据的主键范围定位到目标的 tablets,然后读取 tablets 中的 RowSets。
在读取每个 RowSet 时,先根据主键过滤要 scan 范围,然后加载范围内的 base data,在找到对应的 delta stores,应用所有变更,最后 union 上 MemRowSet 中的内容,返回数据给 Client。
更新流程
数据更新的核心是定位到待更新数据的位置,这块与写入的时候类似,就不展开了,等定位到具体位置后,然后将变更写到对应的 delta store 中。