1. 表与schema

 Kudu设计是面向结构化存储的,因此Kudu的表需要用户在建表时定义它的Schema信息,这些Schema信息包含:

  • 列定义(含类型)
  • Primary Key定义(用户指定的若干个列的有序组合)

 数据的唯一性,依赖于用户所提供的Primary Key中的Column组合的值的唯一性。
 Kudu提供了Alter命令来增删列,但位于Primary Key中的列是不允许删除的。
 Kudu当前并不支持二级索引。
 从用户角度来看,Kudu是一种存储结构化数据表的存储系统。
 在一个Kudu集群中可以定义任意数量的table,每个table都需要预先定义好schema。每个table的列数是确定的,每一列都需要有名字和类型,每个表中可以把其中一列或多列定义为主键。这么看来,Kudu更像关系型数据库,而不是像HBase、Cassandra和MongoDB这些NoSQL数据库。
 不过Kudu目前还不能像关系型数据一样支持二级索引。
 Kudu使用确定的列类型,而不是类似于NoSQL的“everything is byte”(一切都是字节)。
这可以带来两点好处:

  • 确定的列类型使Kudu可以进行类型特有的编码
  • 可以提供 SQL-like 元数据给其他上层查询工具使用,比如BI工具。

2. kudu的底层数据模型

 Kudu的底层数据文件的存储,未采用HDFS这样的较高抽象层次的分布式文件系统,而是自行开发了一套可基于Table/Tablet/Replica视图级别的底层存储系统
这套实现基于如下的几个设计目标:

  • 可提供快速的列式查询

  • 可支持快速的随机更新

  • 可提供更为稳定的查询性能保障
    kudu原理

  • 一个Table会被分成若干个tablet,其中Tablet的数量是根据hash或者是range进行设置的

  • 一个Tablet中包含MetaData信息和多个RowSet信息,其中MetaData信息是block和block在data中的位置。

  • 一个RowSet包含一个MemRowSet和多个DiskRowSet,其中MemRowSet用于存储insert数据和update后的数据,写满后会刷新到磁盘中也就是多个DiskRowSet中,默认是1G刷新一次或者是2分钟。

  • DiskRowSet用于老数据的mutation(改变),比如说数据的更新操作,后台定期对DiskRowSet进行合并操作,删除历史数据和没有的数据,减少查询过程中的IO开销。

  • 一个DiskRowSet包含1个BloomFilter,1个Ad_hoc Index,多个UndoFile、RedoFile、BaseData、DeltaMem

    • BloomFile:根据一个DiskRowSet中的key生成一个bloom filter,用于快速模糊定位某个key是否在DiskRowSet中存在。
    • Ad_hoc Index:是主键的索引,用于定位key在DiskRowSet中的具体哪个偏移位置。
    • BaseData是MemRowSet flush下来的数据,按列存储,按主键有序。
    • UndoFile是基于BaseData之前时间的历史数据,通过在BaseData上apply UndoFile中的记录,可以获得历史数据。
    • RedoFile是基于BaseData之后时间的变更(mutation)记录,通过在BaseData上apply RedoFile中的记录,可获得较新的数据。
    • DeltaMem用于DiskRowSet中数据的变更mutation,先写到内存中,写满后flush到磁盘形成RedoFile。
      kudu原理
  • MemRowSets可以对比理解成HBase中的MemStore, 而DiskRowSets可理解成HBase中的HFile。MemRowSets中的数据按照行视图进行存储,数据结构为B-Tree。

  • MemRowSets中的数据被Flush到磁盘之后,形成DiskRowSets。

  • DiskRowSets中的数据,按照32MB大小为单位,按序划分为一个个的DiskRowSet。 DiskRowSet中的数据按照Column进行组织,与Parquet类似。

 这是Kudu可支持一些分析性查询的基础。每一个Column的数据被存储在一个相邻的数据区域,而这个数据区域进一步被细分成一个个的小的Page单元,与HBase File中的Block类似,对每一个Column Page可采用一些Encoding算法,以及一些通用的Compression算法。 既然可对Column Page可采用Encoding以及Compression算法,那么,对单条记录的更改就会比较困难了。
前面提到了Kudu可支持单条记录级别的更新/删除,是如何做到的?
 与HBase类似,也是通过增加一条新的记录来描述这次更新/删除操作的。DiskRowSet是不可修改了,那么 KUDU 要如何应对数据的更新呢?在KUDU中,把DiskRowSet分为了两部分:

  • base data: 负责存储基础数据
  • delta stores:delta stores负责存储 base data 中的变更数据.
    kudu原理

 如上图所示,数据从 MemRowSet 刷到磁盘后就形成了一份 DiskRowSet(只包含 base data),每份 DiskRowSet 在内存中都会有一个对应的 DeltaMemStore,负责记录此 DiskRowSet 后续的数据变更(更新、删除)。DeltaMemStore 内部维护一个 B-树索引,映射到每个 row_offset 对应的数据变更。DeltaMemStore 数据增长到一定程度后转化成二进制文件存储到磁盘,形成一个 DeltaFile,随着 base data 对应数据的不断变更,DeltaFile 逐渐增长。

3. Tablet的发现过程

 当创建Kudu客户端时,其会从主服务器上获取tablet位置信息,然后直接与服务于该tablet的服务器进行交谈。
 为了优化读取和写入路径,客户端将保留该信息的本地缓存,以防止他们在每个请求时需要查询主机的tablet位置信息。
 随着时间的推移,客户端的缓存可能会变得过时,并且当写入被发送到不再是tablet领导者的tablet服务器时,则将被拒绝。然后客户端将通过查询主服务器发现新领导者的位置来更新其缓存。
kudu原理

4. kudu的写流程

kudu原理

 如上图,当 Client 请求写数据时,先根据主键从Master Server中获取要访问的目标 Tablets,然后到依次对应的Tablet获取数据。
 因为KUDU表存在主键约束,所以需要进行主键是否已经存在的判断,这里就涉及到之前说的索引结构对读写的优化了。一个Tablet中存在很多个RowSets,为了提升性能,我们要尽可能地减少要扫描的RowSets数量。
 首先,我们先通过每个 RowSet 中记录的主键的(最大最小)范围,过滤掉一批不存在目标主键的RowSets,然后在根据RowSet中的布隆过滤器,过滤掉确定不存在目标主键的 RowSets,最后再通过RowSets中的 B-树索引,精确定位目标主键是否存在。
 如果主键已经存在,则报错(主键重复),否则就进行写数据(写 MemRowSet)。

5. kudu的读流程

kudu原理

 如上图,数据读取过程大致如下:先根据要扫描数据的主键范围,定位到目标的Tablets,然后读取Tablets 中的RowSets。
 在读取每个RowSet时,先根据主键过滤要scan范围,然后加载范围内的base data,再找到对应的delta stores,应用所有变更,最后union上MemRowSet中的内容,返回数据给Client。

6. kudu的更新流程

kudu原理

 数据更新的核心是定位到待更新数据的位置,这块与写入的时候类似,就不展开了,等定位到具体位置后,然后将变更写到对应的delta store 中。

7. Kudu的优化

7.1 Kudu关键配置

TabletServer 在开始拒绝所有传入的写入之前可以消耗的最大内存量:memory_limit_hard_bytes=1073741824
kudu原理

分配给 Kudu Tablet Server 块缓存的最大内存量:block_cache_capacity_mb=512
kudu原理

7.2 Kudu的使用限制

7.2.1 主键

  • 创建表后,不能更改主键。必须删除并重新创建表以选择新的主键。
  • 创建表的时候,主键必须放在最前边。
  • 主键不能通过 update 更新,如果要修改主键就必须先删除行,然后重新插入。这种操作不是原子性的。(kudu的删除和插入操作无法事务)
  • 不支持自动生成主键,可以通过内置的 uuid 函数表示为主键值。
  • 联合主键由 kudu 编码后,大小不能超过 16KB。

7.2.2 Cells

在编码或压缩之前,任何单个单元都不得大于 64KB。
在 Kudu 完成内部复合键编码之后,组成复合键的单元格总共限制为 16KB。
如果插入不符合这些限制的行时会报错误并返回给客户端。

7.2.3 字段

  • 默认情况下,Kudu 不允许创建超过 300 列的表。官方建议使用较少列的 Schema 设计以获得最佳性能。
  • 不支持 CHAR、VARCHAR、DATE 和数组等复杂类型。
  • 现有列的类型和是否允许为空,一旦设置后,是不可修改的。
  • Decimal 类型的精度不可修改。也不允许通过更改表来更改 Decimal 列的精度和小数位数
  • 删除列不会立即回收空间。首先必须运行压缩。

7.2.4 表

  • 表中的副本数必须为奇数,最多为 7
  • 复制因子(在表创建时设置)不能更改
  • 无法手动运行压缩,但是删除表将立即回收空间

7.2.5 其他限制

  • 不支持二级索引。
  • 不支持多行事务。
  • 不支持外键。
  • 列名和表名之类的标识符仅限于有效的 UTF-8 字符串并且其最大长度为 256 个字符。

7.2.6 分区限制

  • 表必须根据一个主键 or 联合主键被预先切成 tablet,不支持自动切。表被创建后不支持修改分区字段,支持添加和删除 range 分区(意思分区表,分区字段需提前定义好,kudu 不会自动分)。
  • 已经存在的表不支持自动重新分区,只能创建新表时指定。
  • 丢失副本时,必须通过手动修复方式来恢复。

7.2.7 扩展建议和限制

  • 建议 TabletServer 最多为 100 台。
  • 建议 Master 最多 3 台。
  • 建议每个 TabletServer 最大数据为 8T(压缩后)。
  • 建议每台 TabletServer 的 tablet 数为 1000,最多 2000。
  • 创建表的时候,建议在每个 Tablet Server 上,每个表的 Tablet 数最大为 60,也就是 3 节点的话,3 副本,创表分区最大 60,这样每个单 TabletServer 上该表的 Tablets 也就为 60。
  • 建议每个 Tablet 最大为 50GB,超出后可能导致压缩和启动有问题。
  • 建议单 Tablet 的大小<10GB。

7.2.8 守护进程

  • 部署至少 4G 内存,理想情况下应超过 16GB。
  • 预写日志(WAL)只能存储在一个磁盘上。
  • 不能直接删除数据目录,必须使用重新格式化数据目录的方式来达到删除目的。
  • TabletServer 不能修改 IP 和 PORT。
  • Kudu 对 NTP 有严格要求,如果时间不同步时,Kudu 的 Master 和 TabletServer 会崩溃。
  • Kudu 仅使用 NTP 进行了测试,不支持其他时间同步工具。

7.2.9 集群管理限制

  • 不支持滚动重启。
  • 建议 Kudu 集群中的最大点对点延迟为 20 毫秒。推荐的最小点对点带宽是 10GB。
  • 如果要使用位置感知功能将平板服务器放置在不同的位置,官方建议先测量服务器之间的带宽和延迟,以确保它们符合上述指导原则。
  • 首次启动群集时,必须同时启动所有 Master 服务。

7.2.10 复制和备份限制

  • Kudu 当前不支持任何用于备份和还原的内置功能。鼓励用户根据需要使用 Spark 或 Impala之类的工具导出或导入表。

7.2.11 Impala集成限制

  • 创建 Kudu 表时,建表语句中的主键字段必须在最前面。
  • Impala 无法更新主键列中的值。
  • Impala 无法使用以下命令创建 Kudu 表 VARCHAR 或嵌套类型的列。
  • 名称包含大写字母或非 ASCII 字符的 Kudu 表在 Impala 中用作外部表时,必须分配一个备用名称。
  • 列名包含大写字母或非 ASCII 字符的 Kudu 表不能用作 Impala 中的外部表。可以在 Kudu 中重命名列以解决此问题。
  • !=和 like 谓词不会下推到 Kudu,而是由 Impala 扫描节点评估。相对于其他类型的谓语,这会导致降低性能。
  • 使用 Impala 进行更新,插入和删除是非事务性的。如果查询在部分途中失败,则其部分效果不会回滚。
  • 单个查询的最大并行度受限于 Table 中 Tablet 的数量。为了获得良好的分析性能,每位主机目标为 10 片或更多 tablets。
  • Impala 的关键字(PARTITIONED、LOCATION、ROWFORMAT)不适用于在创建 Kudu 表时使用。

7.2.12 Spark集成限制

  • 必须使用 JDK8,自 Kudu-1.5.0 起,Spark 2.2 是默认的依赖项版本。
  • Kudu 表只能在 Spark SQL 中注册为临时表。
  • 无法使用 HiveContext 查询 Kudu 表。

相关文章: