官方文档:https://kudu.apache.org/releases/1.11.1/docs/known_issues.html
如上图,跟 HBase 类似,Kudu 是典型的主从架构。一个 Kudu 集群由主节点即 Master 和若干个从节点即 Tablet Server 组成。Master 负责管理集群的元数据(类似于 HBase 的 Master),TabletServer 负责数据存储(类似 HBase 的 RegionServer)。在生产环境,一般部署多个 Master 实现高可用(奇数个、典型的是 3 个),Tablet Server 一般也是奇数个。
除了 Master 和Tablet Server,Kudu 中还有很多术语,为便于理解统一盘点如下:
Kudu 中 Table(表)的概念跟其他关系型数据库一样,Table 是数据存储在 Kudu 的位置。表具有schema(表结构)和全局有序的primary key(主键)。table 被水平分成很多段,每个段称为Tablet。
表(Table)是数据库中用来存储数据的对象,是有结构的数据集合。kudu 中的表具有 schema(纲要)和全局有序的 primary key(主键)。kudu 中一个table 会被水平分成多个被称之为 tablet 的片段。
一个 tablet 是 一张表 table 连续的 segment(片段),类似于 HBase 的 region 或关系型数据库partition(分区)。每个 tablet 存储着一定连续 range 的数据(key),且 tablet 两两间的 range 不会重叠。一张表的所有 tablet 包含了这张表的所有 key 空间。
tablet 会冗余存储。放置到多个 tablet server上,并且在任何给定的时间点,其中一个副本被认为是leader tablet,其余的被认之为follower tablet。每个 tablet 都可以进行数据的读请求,但只有Leader tablet 负责写数据请求。
Tablet server 是 Kudu 集群中的从节点,负责数据存储,并提供数据读写服务。
一个 Tablet server 存储了 table 表的 tablet,向 kudu client 提供读取数据服务。对于给定的 tablet,一个 tablet server 充当 leader,其他 tablet server 充当该 tablet 的 follower 副本。
只有 leader 服务写请求,然而 leader 或 followers 为每个服务提供读请求 。一个 tablet server 可以服务多个 tablets ,并且一个 tablet 可以被多个 tablet servers 服务着。
tablet server相当于HDFS DataNode跟HBase Region server的合体,它需要负责执行所有与数据相关的操作:存储、访问、编码、压缩、compaction和复制。与master相比它工作繁重,因此它可以水平扩展。
集群中的主节点,负责集群管理、元数据管理等功能。
保持跟踪所有的 tablets、tablet servers、catalog tables(目录表)和其它与集群相关的 metadata。在给定的时间点,只能有一个起作用的master(也就是 leader)。
如果当前的 leader 消失,则选举出一个新的 master,使用 Raft 协议来进行选举。master 还协调客户端的 metadata operations(元数据操作)。
例如,当创建新表时,客户端内部将请求发送给 master。 master 将新表的元数据写入 catalogtable(目录表,元数据表),并协调在tablet server 上创建 tablet 的过程。
所有 master 的元数据都存储在一个 tablet 中,可以复制到所有其他候选的 master。tablet server 以设定的间隔向 master 发出心跳(默认值为每秒一次)。
Kudu 使用 Raft consensus algorithm 作为确保常规 tablet 和 master 数据的容错性和一致性的手段。通过 Raft 协议,tablet 的多个副本选举出 leader,它负责接受请求和复制数据写入到其他 follower 副本。一旦写入的数据在大多数副本中持久化后,就会向客户确认。给定的一组 N 副本(通常为 3 或 5个)能够接受最多 (N - 1)/2 错误的副本的写入。
Catalog table 是 Kudu 的元数据表。它存储有关 tables 和 tablets 的信息。catalog table(目录表)不能被直接读写,它只能通过客户端 API 中公开的元数据操作访问。catalog table 存储以下两类元数据:
作为分布式存储,一个至关重要的事情就是把负载分散到多个服务器上(读写负载/数据分布),让集群中的每个节点都充分的且尽量均衡的参与读写操作,会大幅提高整体吞吐和降低延迟。本节带着大家探讨一下Kudu通过什么机制实现数据最优分布。
Kudu 是有主键的,跟关系型数据库的主键是概念相通的。Kudu 基于主键来重组和索引数据,但是没有二级键、没有二级索引,数据存储到哪台服务器也跟主键密切相关。因此主键设计不好将直接影响查询性能,通常还会导致热点问题。
任何一个分布式系统(无论存储还是计算)都无法回避这个问题。所谓热点问题(hotspotting),就是在分布式系统中大多数读或者写请求落到一台或者几台服务器。
热点问题多半是分区设计跟读写模式不匹配造成的,以车联网电池温度传感器上传数据为例:
因此要解决热点问题,必须考虑综合考虑具体业务在以下三方面的事情:
Kudu目前支持三种分区方式:范围分区、哈希分区、多级分区
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 目前还不能像关系型数据一样支持二级索引,因此要想获取最佳查询性能Rowkey设计尤为关键。
Kudu使用确定的列类型,而不是类似于NoSQL的“everything is byte”。带来好处:
Kudu自身的架构,部分借鉴了 Bigtable/HBase/Spanner 的设计思想。在作者列表中,有几位是HBase 社区的 Committer/PBC 成员,因此在学习 Kudu 的过程中能很深刻的感受到 HBase 对 Kudu设计的一些影响 。
Kudu的底层数据文件的存储,未采用 HDFS 这样的较高抽象层次的分布式文件系统,而是自行开发了一套可基于Table/Tablet/Replica视图级别的底层存储系统。
这套实现基于如下的几个设计目标:
Kudu 的数据模型见下图:
说明:
RowSet组成:
组成 | 说明 |
---|---|
MemRowSet | 用于新数据insert及已在MemRowSet中的数据的更新,一个MemRowSet写满后会将数据刷到磁盘形成若干个DiskRowSet。默认是1G或者或者120S。 |
DiskRowSet | 用于老数据的变更,后台定期对DiskRowSet做compaction,以删除没用的数据及合并历史数据,减少查询过程中的IO开销。 |
BloomFile | 根据一个DiskRowSet中的key生成一个bloom filter,用于快速模糊定位某个key是否在DiskRowSet中。 |
AdhocIndex | 是主键的索引,用于定位key在DiskRowSet中的具体哪个偏移位置。 |
BaseData | 是MemRowSet flush下来的数据,按列存储,按主键有序。 |
UndoFile | 是基于BaseData之前时间的历史数据,通过在BaseData上apply UndoFile中的记录,可以获得历史数据。 |
RedoFile | 是基于BaseData之后时间的变更记录,通过在BaseData上apply RedoFile中的记录,可获得较新的数据。 |
DeltaMem | 用于DiskRowSet中数据的变更,先写到内存中,写满后flush到磁盘形成RedoFile。 |
MemRowSets与DiskRowSets的区别:
如上图所示,MemRowSets可以对比理解成HBase中的MemStore,而DiskRowSets可理解成HBase中的HFile。
MemRowSets中的数据被Flush到磁盘之后,形成DiskRowSets。DisRowSets中的数据,按照32MB大小为单位,按序划分为一个个的DiskRowSet。DiskRowSet中的数据按照Column进行组织,与Parquet类似。
这是Kudu可支持一些分析性查询的基础。每一个Column的数据被存储在一个相邻的数据区域,而这个数据区域进一步被细分成一个个的小的Page单元,与HBaseFile中的Block类似,对每一个ColumnPage可采用一些Encoding算法,以及一些通用的Compression算法。既然可对ColumnPage可采用Encoding以及Compression算法,那么,对单条记录的更改就会比较困难了。
前面提到了Kudu可支持单条记录级别的更新/删除,是如何做到的?
与HBase类似,也是通过增加一条新的记录来描述这次更新/删除操作的。DiskRowSet是不可修改了,那么Kudu要如何应对数据的更新呢?在Kudu中,把DiskRowSet分为了两部分:BaseData、DeltaStores。BaseData负责存储基础数据,DeltaStores负责存储BaseData中的变更数据。
如上图所示,数据从MemRowSet刷到磁盘后就形成了一份DiskRowSet(只包含basedata),每份DiskRowSet在内存中都会有一个对应的DeltaMemStore,负责记录此DiskRowSet后续的数据变更(更新、删除)。DeltaMemStore内部维护一个B-树索引,映射到每个row_offset对应的数据变更。DeltaMemStore数据增长到一定程度后转化成二进制文件存储到磁盘,形成一个DeltaFile,随着basedata对应数据的不断变更,DeltaFile逐渐增长。
下图是DeltaFile生成过程的示意图:
Delta数据部分应该包含REDO与UNDO两部分,这里的REDO与UNDO与关系型数据库中的REDO与UNDO日志类似(在关系型数据库中,REDO日志记录了更新后的数据,可以用来恢复尚未写入DataFile的已成功事务更新的数据。而UNDO日志用来记录事务更新之前的数据,可以用来在事务失败时进行回滚),但也存在一些细节上的差异:
Kudu客户端无论在执行写入还是读取操作之前都会先从master获取tablet位置信息,这个过程叫做tablet发现。
当创建Kudu客户端时,其会从主服务器上获取tablet位置信息,然后直接与服务于该tablet的服务器进行交谈。
为了优化读取和写入路径,客户端将保留该信息的本地缓存,以防止他们在每个请求时需要查询主机的tablet位置信息。随着时间的推移,客户端的缓存可能会变得过时,并且当写入被发送到不再是tablet领导者的tablet服务器时,则将被拒绝。然后客户端将通过查询主服务器发现新领导者的位置来更新其缓存。
当Client请求写数据时,先根据主键从Master中获取要访问的目标Tablets,然后到依次到对应的Tablet获取数据。
因为KUDU表存在主键约束,所以需要进行主键是否已经存在的判断,这里就涉及到之前说的索引结构对读写的优化了。一个Tablet中存在很多个RowSets,为了提升性能,我们要尽可能地减少要扫描的RowSets数量。
首先,我们先通过每个RowSet中记录的主键的(最大最小)范围,过滤掉一批不存在目标主键的RowSets,然后在根据RowSet中的布隆过滤器,过滤掉确定不存在目标主键的RowSets,最后再通过RowSets中的B-树索引,精确定位目标主键是否存在。
如果主键已经存在,则报错(主键重复),否则就进行写数据(写MemRowSet)。
数据读取过程大致如下:先根据要扫描数据的主键范围,定位到目标的Tablets,然后读取Tablets 中的RowSets。
在读取每个RowSet时,先根据主键过滤要scan范围,然后加载范围内的BaseData,再找到对应的DeltaMemStores,应用所有变更,最后union上MemRowSet中的内容,返回数据给Client。
数据更新的核心是定位到待更新数据的位置,这块与写入的时候类似,就不展开了,等定位到具体位置后,然后将变更写到对应的DeltaMemStore 中。
参考:https://github.com/apache/kudu/tree/master/examples