Kudu-基本架构、模式设计、Docker部署

简介

介绍

Kudu是一个分布式列式存储引擎/系统,由Cloudera开源后捐献给Apache基金会很快成为顶级项目。

用于对大规模数据快速读写的同时进行快速分析。

官网:https://kudu.apache.org/

Kudu运行在一般的商用硬件上,支持水平扩展和高可用,集HDFS的顺序读和HBase的随机读于一身,同时具备高性能的随机写,以及很强大的可用性(单行事务,一致性协议),支持与Impala/spark计算引擎。

MMSIZE

近年来Kudu的应用越来越广泛,在阿里、小米、网易等公司的大数据架构中,Kudu 都有着不可替代的地位。

Kudu出现的背景

Kudu给人的感觉是HDFS+HBase的杂合体,为什么会出现呢?

MSIZE

在Kudu出现之前,需要为不同形态的数据需要分别存储以适应不同场景:

MSIZE

最终导致几个问题:

  • 数据过度冗余:数据需要存储多份以支撑多个应用,这样造成了存储等资源的浪费。
  • 架构复杂导致开发、运维、测试成本高:同时维护多套存储系统,架构复杂,开发、运维、测试成本相对较高。
  • 数据不一致容易误解:多套数据由于程序bug或者其他原因很容易出现数据不一致的情况,往往会造成业务方的误解。

为了解决上述问题业界做了很多尝试,例如HBase+Hive整合:

MSIZE

上述方案虽然在一定程度上起到了作用,但是依然改变不了HBase不适合高吞吐量离线大数据分析的事实。

所以Kudu一出现定位就是Fast Analytics on Fast Data,是一个既支持随机读写、又支持 OLAP 分析的大数据存储引擎

MMSIZE

从上图可以看出,Kudu 是一个折中的产品,它平衡了随机读写和批量分析的性能。从 KUDU 的诞生可以说明一个观点:底层的技术发展很多时候都是上层的业务推动的,脱离业务的技术很可能是空中楼阁。

当然Kudu身上还有很多概念或者标签,有分布式文件系统(好比HDFS),有一致性算法(好比Zookeeper),有Table(好比Hive Table),有Tablet(好比Hive Table Partition),有列式存储(好比Parquet),有顺序和随机读取(好比HBase),所以看起来kudu是一个轻量级的 HDFS +Zookeeper + Hive + Parquet + HBase,除此之外,kudu还有自己的特点,快速写入+读取,使得kudu+impala非常适合OLAP场景,尤其是Time-series场景。

MMSIZE

Kudu使用场景

Kudu的特性决定了它适合以下场景:

  1. 适用于那些既有随机访问,也有批量数据扫描的复合场景

  2. CPU密集型的场景

  3. 使用了高性能的存储设备,包括使用更多的内存

  4. 要求支持数据更新,避免数据反复迁移的场景

  5. 支持跨地域的实时数据备份和查询

从更实际角度来讲,我们不一定非要按照上面的场景来靠,如果未来可能会出现上述1的场景,当下的问题又能通过Kudu来解决,那么Kudu就是个比较好的选择。例如下面几个更实际的应用场景:

  • 实时更新的数据应用(例如物联网数据产品)

    刚刚到达的数据就马上要被终端用户使用访问到,未来还要做大规模的数据分析。

  • 时间序列相关的应用(例如APM)

    需要同时支持:

    • 海量历史数据查询(数据顺序扫描)。
    • 必须非常快地返回关于单个实体的细粒度查询(随机读)。
  • 实时预测模型的应用(机器学习)

    支持根据所有历史数据周期地更新模型。

OLTP与OLAP

MMSIZE

行式存储与列式存储

MMSIZE

MMSIZE

一般来说OLTP数据库使用行式存储,OLAP数据库使用列式存储:

  • 在查询某些列的数据时,行存储需要把整行数据读出来再过滤。
  • 列存储不同列的数据分开存储在不同文件,面对分析查询(只涉及部分列)可以大幅降低IO,因此列式存储相对于行式存储在OLAP场景下的优势可以这么来理解:
    • 对于分析查询,一般只需要用到少量的列,在列式存储中,只需要读取所需的数据列即可。 例如,如果您需要100列中的5列,则I / O减少20倍。
    • 按列分开存储,按数据包读取时因此更易于压缩。 列中的数据具有相同特征也更易于压缩, 这样可以进一步减少I / O量。
    • 由于减少了I / O,因此更多数据可以容纳在系统缓存中,进一步提高分析性能。

与其他存储对比

Kudu VS 关系型数据库

MSIZE

怎么理解Kudu是HTAP(混合事务/分析处理,兼具OLTP和OLAP的一些特性)

  • 像OLAP:Kudu很容易跟Hive、Impala、 SparkSQL等OLAP SQL查询引擎整合
  • 像OLTP:Kudu支持根据主键快速检索,并且能够在对表进行分析查询时连续不断写入数据
Kudu VS 常见大数据存储

回归大数据存储领域,我们把Kudu跟HDFS和HBase/Cassandra做一个简单比较:

MMSIZE

这里有一个Kudu跟Parquet的对比:

MMSIZE

对比总结

MMSIZE

  • Kudu不适合所有场景,他也不会完全取代HDFS、HBase等大数据存储组件。在有些场景下甚至HDFS或者HBase更为合适。
  • 如果你正在找寻一款既支持实时查询又支持大规模数据分析和机器学习场景的存储,Kudu将是一个很好的选择,它将在搞定需求的同时极大降低运维复杂度。

Kudu基本架构

架构

MMSIZE

如上图,跟HBase类似,Kudu是典型的主从架构。一个Kudu集群由主节点即Master和若干个从节点即Tablet Server组成。Master负责管理集群的元数据(类似于HBase 的Master),Tablet Server负责数据存储(类似HBase的RegionServer)。在生产环境,一般部署多个Master实现高可用(奇数个、典型的是3个),Tablet Server一般也是奇数个。

除了Master和Tablet Server,Kudu中还有很多术语:

  • Table(表):Kudu中Table(表)的概念跟其他关系型数据库一样,table是数据存储在 Kudu 的位置。表具有schema(表结构)和全局有序的primary key(主键)。table被水平分成很多段,每个段称为Tablet。

  • 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: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 服务着。

  • 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发出心跳(默认值为每秒一次)。

  • Raft Consensus Algorithm:Kudu 使用 Raft consensus algorithm 作为确保常规 tablet 和 master 数据的容错性和一致性的手段。

    通过 Raft协议,tablet 的多个副本选举出 leader,它负责接受请求和复制数据写入到其他follower副本。一旦写入的数据在大多数副本中持久化后,就会向客户确认。给定的一组N副本(通常为 3 或 5 个)能够接受最多(N - 1)/2 错误的副本的写入。

  • Catalog Table(目录表):catalog table是Kudu 的元数据表。它存储有关tables和tablets的信息。

    catalog table(目录表)不能被直接读写,它只能通过客户端 API中公开的元数据操作访问。

    catalog table存储以下两类元数据:

    • Tables:table schemas 表结构,locations 位置,states 状态
    • Tablets:现有tablet 的列表,每个 tablet 的副本所在哪些tablet server,tablet的当前状态以及开始和结束的keys(键)。

Kudu中的相关概念和机制

作为分布式存储,一个至关重要的事情就是把负载分散到多个服务器上(读写负载/数据分布),让集群中的每个节点都充分的且尽量均衡的参与读写操作,会大幅提高整体吞吐和降低延迟。

主键

Kudu是有主键的,跟关系型数据库的主键是概念相通的。Kudu基于主键来重组和索引数据,但是没有二级键、没有二级索引,数据存储到哪台服务器也跟主键密切相关。因此主键设计不好将直接影响查询性能,通常还会导致热点问题。

热点问题

熟悉HBase或者其他分布式存储的人对热点问题一定不陌生,任何一个分布式系统(无论存储还是计算)都无法回避这个问题。所谓热点问题(hotspotting),就是在分布式系统中大多数读或者写请求落到一台或者几台服务器时,我们称之为热点。

MMSIZE

热点问题产生的原因:热点问题多半是分区设计跟读写模式不匹配造成的,以车联网电池温度传感器上传数据为例:

MSIZE

因此要解决热点问题,必须考虑综合考虑具体业务在以下三方面的事情:

  1. 读操作模式(吞吐和延迟)

  2. 写操作模式(吞吐和延迟)

  3. 存储开销(压缩比)

分区

Kudu目前支持三种分区方式:

  • 范围分区
  • 哈希分区
  • 多级分区

依然以温度传感器数据为例我们来看看各种分区。

(1)范围分区

MMSIZE

(2)哈希分区

MMSIZE

(3)多级分区

MMSIZE

架构补充

  • kudu集群是典型的主从架构

    主是master:管理集群、管理元数据

    从是tablet server:负责最终数据的存储对外提供数据读写能力,里面存储的都是一个个tablet

  • master服务器

    master上有一张特殊的Catalog Table,它只有一个tablet,且在内存中有完整的缓存。缓存Catalogtable数据主要是为了提高性能,因为客户端需要通过它来定位数据的位置以便查询。关于master有几点需要注意:

    1. master不需要很大的存储,也不需要非常好的硬件

    2. master应该配置多台,起码要拥有跟副本因子一样数量的master(–default_num_replicas=3)

  • tablet服务器

    tablet server相当于HDFS DataNode跟HBase Region server的合体,它需要负责执行所有与数据相关的操作:存储、访问、编码、压缩、compaction和复制。与master相比它工作繁重,因此它可以水平扩展。我们在实际使用过程中需要了解tablet server的一些限制(截止到Kudu 1.10.0),以下是一些很好的建议:

    MMSIZE

其他限制:

  1. tablet server不能非常优雅的退役

  2. tablet server不能修改IP和端口

  3. tablet server强依赖NTP时间同步,如果时间不同步就会宕机(master亦是如此)

注意:随着版本升级和硬件的改善,上述限制会发生变化,请参阅对应版本的文档。

https://kudu.apache.org/releases/1.10.0/docs/known_issues.html

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目前还不能像关系型数据一样支持二级索引,因此要想获取最佳查询性能Rowkey设计尤为关键。

Kudu使用确定的列类型,而不是类似于NoSQL的“everything is byte”。带来好处:

  • 确定的列类型使Kudu可以进行类型特有的编码,进一步提高压缩率,进而较少磁盘IO;
  • 可以提供元数据给其他上层查询工具,因此更加方便与其他计算框架整合。
Kudu存储模型

Kudu自身的架构,部分借鉴了Bigtable/HBase/Spanner的设计思想。在作者列表中,有几位是HBase社区的Committer/PBC成员,因此在学习Kudu的过程中能很深刻的感受到HBase对Kudu设计的一些影响 。

Kudu的底层数据文件的存储,未采用HDFS这样的较高抽象层次的分布式文件系统,而是自行开发了一套可基于Table/Tablet/Replica视图级别的底层存储系统。

这套实现基于如下的几个设计目标:

  • 可提供快速的列式查询
  • 可支持快速的随机更新
  • 可提供更为稳定的查询性能保障

Kudu的数据模型见下图:

MMSIZE

一张table会分成若干个tablet,每个tablet包括MetaData元信息及若干个RowSet。

RowSet包含一个MemRowSet及若干个DiskRowSet,DiskRowSet中包含一个BloomFile、Ad_hocIndex、BaseData、DeltaMem及若干个RedoFile和UndoFile( UndoFile一般情况下只有一个 )。

RowSet组成:

MMSIZE

MemRowSets与DiskRowSets的区别:

MMSIZE

如上图所示,MemRowSets可以对比理解成HBase中的MemStore,而DiskRowSets可理解成HBase中 的HFile。

MemRowSets中的数据被Flush到磁盘之后,形成DiskRowSets。 DisRowSets中的数据,按照32MB大小为单位,按序划分为一个个的DiskRowSet。 DiskRowSet中的数据按照Column进行组织,与Parquet类似。

这是Kudu可支持一些分析性查询的基础。每一个Column的数据被存储在一个相邻的数据区域,而这个数据区域进一步被细分成一个个的小的Page单元,与HBase File中的Block类似,对每一个ColumnPage可采用一些Encoding算法,以及一些通用的Compression算法。 既然可对Column Page可采用Encoding以及Compression算法,那么,对单条记录的更改就会比较困难了。

前面提到了Kudu可支持单条记录级别的更新/删除,是如何做到的?

与HBase类似,也是通过增加一条新的记录来描述这次更新/删除操作的。DiskRowSet是不可修改了,那么 Kudu 要如何应对数据的更新呢?在Kudu中,把DiskRowSet分为了两部分:BaseData、DeltaStores。BaseData 负责存储基础数据,DeltaStores负责存储 BaseData 中的变更数据。

MMSIZE

如上图所示,数据从 MemRowSet 刷到磁盘后就形成了一份 DiskRowSet(只包含 base data),每份DiskRowSet 在内存中都会有一个对应的 DeltaMemStore,负责记录此 DiskRowSet 后续的数据变更(更新、删除)。DeltaMemStore 内部维护一个 B-树索引,映射到每个 row_offffset 对应的数据变更。

DeltaMemStore 数据增长到一定程度后转化成二进制文件存储到磁盘,形成一个 DeltaFile,随着base data 对应数据的不断变更,DeltaFile 逐渐增长。下图是DeltaFile生成过程的示意图:

MMSIZE

Delta数据部分应该包含REDO与UNDO两部分,这里的REDO与UNDO与关系型数据库中的REDO与UNDO日志类似(在关系型数据库中,REDO日志记录了更新后的数据,可以用来恢复尚未写入Data File的已成功事务更新的数据。而UNDO日志用来记录事务更新之前的数据,可以用来在事务失败时进行回滚),但也存在一些细节上的差异:

  • REDO Delta Files包含了Base Data自上一次被Flush/Compaction之后的变更值。REDO Delta Files按照Timestamp顺序排列。
  • UNDO Delta Files包含了Base Data自上一次Flush/Compaction之前的变更值。这样才可以保障基于一个旧Timestamp的查询能够看到一个一致性视图。UNDO按照Timestamp倒序排列。
tablet发现过程

Kudu客户端无论在执行写入还是读取操作之前都会先从master获取tablet位置信息,这个过程叫做tablet发现。

当创建Kudu客户端时,其会从主服务器上获取tablet位置信息,然后直接与服务于该tablet的服务器进行交谈。

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

MMSIZE

Kudu写流程

MMSIZE

当 Client 请求写数据时,先根据主键从Master中获取要访问的目标 Tablets,然后到依次到对应的Tablet获取数据。

因为KUDU表存在主键约束,所以需要进行主键是否已经存在的判断,这里就涉及到之前说的索引结构对读写的优化了。一个Tablet中存在很多个RowSets,为了提升性能,我们要尽可能地减少要扫描的RowSets数量。

首先,我们先通过每个 RowSet 中记录的主键的(最大最小)范围,过滤掉一批不存在目标主键的RowSets,然后在根据RowSet中的布隆过滤器,过滤掉确定不存在目标主键的 RowSets,最后再通过RowSets中的 B-树索引,精确定位目标主键是否存在。

如果主键已经存在,则报错(主键重复),否则就进行写数据(写MemRowSet)。

Kudu读流程

MMSIZE

数据读取过程大致如下:先根据要扫描数据的主键范围,定位到目标的Tablets,然后读取Tablets 中的RowSets。

在读取每个RowSet时,先根据主键过滤要scan范围,然后加载范围内的BaseData,再找到对应的DeltaMemStores,应用所有变更,最后union上MemRowSet中的内容,返回数据给Client。

Kudu更新流程

MMSIZE

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

模式设计

HTAP的实现方式

前面说到了Kudu要填补的是HDFS+HBase的空白,既要解决高并发随机读写,又要同时兼顾大规模分析处理,所以具有OLTP和OLAP的一些特征,因此它是典型的HTAP(在线事务处理/在线分析处理混合模式)。

在过去没有 Kudu之前,要实现HTAP的系统要么OLTP和OLAP混合,要么采用Lamba架构,我们先来看看传统的解决方案然后再看如何做好模式设计用Kudu直接搞定HTAP。

(1)OLTP+OLAP拆分

MMSIZE

OLTP和OLAP拆分之后之后,事务性应用和分析型应用分别走两边确实解决了问题,但是弊端很明显:

  • 分析型应用无法获取最新的数据
  • OLTP横向扩展性不足
  • 维护这样一套系统复杂度很高

(2)Lambda架构

MMSIZE

Lambda架构将工作负载分为实时层和批处理层,我们是用实施层检索和分析最新的数据,使用批处理层分析历史数据。这样会带来两个特别的问题:

  • 两套系统、两份代码,开发、运维、测试都很复杂
  • 整个处理链条中有一处出现问题就需要重跑数据

现在有了第三个选择。

(3)Kudu+优良模式设计

Kudu能处理HTAP场景的能力在此不再累述,但是Kudu要想真正发挥最佳性能就必须重视表和模式设计。

模式设计的目的

Kudu作为一个分布式的存储引擎算是比较奇怪的存在,一般来说NoSQL都是弱化模式或者无模式的,

但是Kudu缺反其道而行之,带来的好处一目了然:

  • 非常易于跟其他组件整合以支持SQL或者进行分布式计算
  • 非常利于从其他关系型数据库迁移数据

但是模式是一把双刃剑,有严格的模式意味着你必须按照业务好好设计,否则可用性、性能将不保。

Kudu模式设计主要的主要目的是很好的支持:

  • 数据的读写均匀分散到每个Tablet Server,以充分挖掘集群的潜力(受分区设计影响)
  • tablet能够以稳定、可预测的速率增加,负载随时间推移能够保持稳定(受分区设计影响)
  • 扫描时读取查询所需的最少数据量(主要受主键设计影响,但分区设计也会起到重要作用)

好的shema设计取决于要处理数据的特征、对数据的操作以及集群的拓扑结构。Schema设计对于kudu集群性能最大化来说是最重要的事情。shema设计包含三大块:

  • 列设计
  • 主键设计
  • 分区设计

MMSIZE

列设计

数据类型

Kudu的每个列都必须指定明确的数据类型的,非主键可以为null,目前支持的数据类型如下:

MMSIZE

Kudu利用强类型列和列式存储格式来提供高效的编码和序列化。为了充分利用这些功能,应将列指定为适当的类型,而不是使用字符串或二进制列来模拟“无模式”表。除了编码之外,Kudu还允许在每列的基础上指定压缩。

注意:同HBase不同,kudu没有提供version和timestamp来跟踪行的变化。如果需要的话,需要自行设计一列。

Decimal类型
  • decimal是具有固定刻度和精度的十进制数字类型,适合于财务等算术运算(flfloat与double不精确有舍入行为)。decimal类型对于大于int64的整数和主键中具有小数值的情况也很有用。

  • 精度:表示该列可以表示的总位数,与小数点的位置无关。此值必须介于1和38之间,并且没有默认值。例如,精度为4表示最大值为9999的整数值,或者表示最多99.99带有两个小数位值。您还可以表示相应的负值,而不用对精度进行任何更改。例如,-9999到9999的范围仍然只需要4的精度。

  • 刻度:表示小数位数。该值必须介于0和精度之间。刻度为0会产生整数值,没有小数部分。如果精度和刻度相等,则所有数字都在小数点后面。例如,精度和刻度等于3的小数可以表示介于-0.999和0.999之间的值。

  • decimal列类型编码默认

    性能考虑:Kudu将每个值存储在尽可能少的字节中,具体取决于decimal指定的精度。因此,不建议为了方便使用最高精度。这样做可能会对性能,内存和存储产生负面影响。在编码和压缩之前:

    • 精度为9或更小的十进制值以4个字节存储。
    • 精度为10到18的十进制值以8个字节存储。
    • 精度大于18的十进制值以16个字节存储。
  • alter命令不能修改的decimal列的精度和刻度

列编码

可以根据列的类型进行编码,不同类型的列支持不同编码。

数据类型-编码对照表

MMSIZE

编码:

MMSIZE

列压缩

Kudu允许列使用LZ4、Snappy或zlib压缩编解码器进行压缩。如果减少存储空间比扫描性能更重要,请考虑使用压缩。

每个数据集的压缩方式都不同,但一般来说LZ4是性能最佳的编解码器,而zlib空间压缩比最大。

默认情况下,使用BitLufflfflffle编码的列固有地使用LZ4压缩进行压缩(不建议修改),其他编码默认不进行压缩。

主键设计

主键设计注意事项
  • 每个Kudu表必须声明由一列或多列组成的主键。与RDBMS主键一样,Kudu主键强制执行唯一性约束。尝试插入具有与现有行相同的主键值的行将导致重复键错误。
  • 主键列必须是非可空的,并且不可以是boolean,flfloat或double类型。
  • 表创建指定主键后,主键中的列集就不能更改。
  • 与RDBMS不同,Kudu不提供列的自增,因此应用程序必须提供完整的主键。
  • 删除和更新时必须指定完整主键。Kudu本身不支持范围删除或更新。即都是通过主键完成操作。
  • 主键值无法修改,但是可以删除后重新插入来变相实现。
主键索引
  • Kudu中只有主键才会被索引,没有二级索引。
  • 扫描Kudu行时,在主键列上使用等于或范围谓词来找行性能最佳,非主键列在数据量大的情况下性能不好,建议把查询用到的列尽量设置为主键列。
  • 主键索引优化可以使扫描跳过个别Tablet,要想使扫描操作跳过很多Tablet需要借助分区设计。
  • 主键索引是有序的,如果主键有多列则按照组合排序,即先按第一列排序,第一列一样则按第二列排序,以此类推。

MMSIZE

时间戳主键回填问题

(1)回填场景

我们已经知道每次插入数据时,Kudu都会在主键索引存储中查找主键,以判断主键是否存在来决定插入还是报错。当主键是时间戳或者主键的第一列是时间戳的时候情况比较特殊,一般来说分两种场景:

MMSIZE

(2)如何解决回填性能问题

  • 使主键更具可压缩性

主键压缩更小,则相同内存能够被缓存的主键索引就更多,从而减少磁盘IO

  • 使用SSD,随机寻道要比机械旋转磁盘快几个数量级
  • 更改主键结构,以使回填写入命中连续的主键范围

分区设计

kudu中的表被分成很多tablet分布在多个tserver上,每一行属于一个tablet,行划分到哪个tablet由分区决定,分区是在表创建期间设置的。

写入频繁时,考虑将写入动作平衡到所有tablet之间能够有效降低单个tablet的压力,对于小范围扫描操作比较多的情况,如果所扫描的数据都为一个tablet上则可以提高性能。

kudu没有默认分区,建议读写都较重的table可以设置和tserver服务器数量相同的分区数。

kudu提供两种类型的分区:范围分区和哈希分区。表可以有多级分区,组合使用范围和哈希或者多个哈希组合使用。

分区设计好坏由场景+三个维度去考量:

  • 是否有读热点
  • 是否有写热点
  • Tablet可扩展性
范围分区

Kudu允许在运行时动态添加和删除范围分区,而不会影响其他分区的可用性。删除分区将删除属于该分区的tablet以及其中包含的数据,后续插入到已删除的分区中将失败。可以添加新分区,但它们不得与任何现有范围分区重叠。Kudu允许在单个事务更改表操作中删除和添加任意数量的范围分区。

动态添加和删除范围分区对于时间序列特别有用。随着时间的推移,可以添加范围分区以覆盖即将到来的时间范围。例如,存储事件日志的表可以在每个月开始之前添加月份分区,以便保存即将发生的事件。可以删除旧范围分区,以便根据需要有效地删除历史数据。

哈希分区

哈希分区按哈希值将行分配到存储桶中的一个。在单级散列分区表中,每个桶只对应一个tablet,在表创建期间设置桶的数量。通常,主键列用作要散列的列,但与范围分区一样,可以使用主键列的任何子集。

当不需要对表进行有序访问时,散列分区是一种有效的策略。散列分区对于在tablet之间随机写入非常有效,这有助于缓解热点和不均匀的tablet大小。

多级分区

Kudu允许表在单个表上组合多个级别的分区。零个或多个哈希分区可以与范围分区组合。除了各个分区类型的约束之外,多级分区的唯一附加约束是多级哈希分区不能散列相同的列。

如果使用正确,多级分区可以保留各个分区类型的好处,同时减少每个分区类型的缺点。多级分区表中的tablet总数是每个级别中分区数的乘积。

修剪分区

当通过扫描条件能够完全确定分区的时候,kudu就会自动跳过整个分区的扫描。要确定哈希分区,扫描条件必须包含每个哈希列的等值判定条件。多级分区表的扫描可以单独利用每一级的分区界定。

分区案例

我们用一个案例体会一下不同分区的优缺点和考量,现有以下表结构存放的是主机的度量信息:

CREATE TABLE metrics ( 
    host STRING NOT NULL, -- 主机 
    metric STRING NOT NULL, -- 度量指标 
    time INT64 NOT NULL, -- 时间戳 
    value DOUBLE NOT NULL, -- 值 
    PRIMARY KEY (host, metric, time), -- 主键 
); 

(1)采用范围分区

对time列进行范围分区,假如每年对应一个分区,数据包括2014,2015和2016,至少可以使用两种分区方式:有界范围分区和无界范围分区。

MMSIZE

上图显示了metrics表可以在time列上进行范围分区的两种方式。

在第一个示例中(蓝色),使用默认范围分区边界,分割为2015-01-01和2016-01-01。这导致三个tablet:第一个包含2015年之前的值,第二个包含2015年的值,第三个包含2016年之后的值。

第二个示例(绿色)使用范围分区绑定[(2014-01-01), (2017-01-01)],并在2015-01-01和处拆分2016-01-01。第二个例子可能等效地被通过的范围分区边界表示 [(2014-01-01), (2015-01-01)],[(2015-01-01), (2016-01-01)]以及[(2016-01-01), (2017-01-01)]。第一个示例具有无界的下部和上部范围分区,而第二个示例则是有界的。

上面的每个范围分区示例都允许时间限制扫描来修剪落在扫描时间范围之外的分区。当存在许多分区时,这可以极大地提高性能。写作时,这两个例子都存在潜在的热点问题。由于度量标准往往总是在当前时间写入,因此大多数写入将进入单个范围分区。

第二个示例比第一个示例更灵活,因为它允许将未来年份的范围分区添加到表中。在第一个示例中,所有后续写入2016-01-01 都将落入最后一个分区,因此分区最终可能会变得太大而无法让单个tabletServer处理。

(2)采用哈希分区

在host和metric列上进行哈希分区:

MMSIZE

在上面的示例中,host和 metric列上的哈希分区为四个桶。与之前的范围分区示例不同,此分区策略将均匀地在表中的所有tablet上进行写入,这有助于整体写入吞吐量。扫描特定host和metric可以通过指定等式来利用分区修剪,将扫描的tablet数量减少到一个。使用纯哈希分区策略时要注意的一个问题是,随着越来越多的数据插入表中,tablet可能会无限增长。最终tablet将变得太大,无法容纳单个tablet服务器。【哈希分区无法动态添加】

(3)哈希+范围组合分区

分区策略对比:

MMSIZE

哈希分区可以最大限度地提高写入吞吐量,而范围分区可以避免无限制的tablets增长问题。这两种策略都可以利用分区修剪来优化不同场景下的扫描。使用多级分区,可以将这两种策略结合起来,以获得两者的好处,同时最大限度地减少每种策略的缺点,如下图:

MMSIZE

在上面的示例中,time列上的范围分区与host和metric列上的散列分区相结合。可以将此策略视为具有两个分区维度:一个用于散列级别,另一个用于范围级别。在当前时间写入此表将并行化到散列桶的数量,在这种情况下读取可以利用时间限制和特定主机和度量谓词来修剪分区。可以添加新的范围分区,这样可以创建4个额外的tablet(就像在图表中添加了新列一样)。

(4)双哈希组合分区

MMSIZE

只要没有共同的哈希列,Kudu就可以在同一个表中支持任意数量的散列分区级别。

在上面的示例中,表被host散列为4个桶,并将散列分区metric为3个桶,产生12个tablet。尽管在使用此策略时,写入将倾向于在所有Tablet中传播,但与多个独立列上的散列分区相比,它更容易出现热点,因为单个主机或度量标准的所有值将始终属于单个tablet。扫描可以分别利用host和metric列上的等式谓词来修剪分区。

多级散列分区也可以与范围分区相结合,从逻辑上增加了分区的另一个维度。

模式变更

Kudu1.10.0能够支持的模式更改是:

  • 重命名表
  • 重命名主键列
  • 重命名,添加或删除非主键列
  • 添加和删除范围分区

可以在单个事务操作中组合多个更改。

局限性

Kudu目前有一些已知的局限性可能会影响到架构设计。

MMSIZE

注意:以上限制随着Kudu版本更新会出现变化,目前截止到Kudu1.10.0。

最佳实践

这里总结模式设计的小窍门供参考:

  • 分区:大多数情况下至少一个列是哈希分区,极大可能另一个列是范围分区。只有范围分区的情况极少,因为不能避免写热点,除非有哈希分区,典型的例子就是时间序列。

  • 大对象:string, binary在未压缩之前不能大于64K,虽然有配置可以调大这个值,但千万不要这么做,避免出现未知错误。

    如果确实要存储超过64K的JSON、XML大对象,有两个办法:

    • 先对json、XML压缩再存储,编码方式设置为Plain且关闭压缩;
    • 如果远远超过64K,则可以把对象保存到HBase或者HDSF中,然后再去Kudu这边保存该对象的"外键",即HBase的Rowkey、HDFS的路径。
  • decimal(十进制数):Kudu1.7开始的版本推荐用decimal代替flfloat和double,且可以出现在主键中(flfloat和double就不可以),查询性能更佳,且更适合算数运算。

  • 不重复的字符串:如果一个表的主键只有一个string列推荐采用Prefifix压缩;如果是多个string列构成主键,则推荐Plain编码+LZ4压缩。

  • 压缩:bitshufflfflffle编码的列会自动使用LZ4压缩进行压缩,其他编码的列可以根据情况选择是否采用LZ4压缩,LZ4通常比Snappy快。

  • 对象命名:表名和列名都小写可以避免混乱(Impala不区分大小写,API操作区分大小写)。表名必须唯一,如果在Impala中创建内部Kudu表,则表名会默认加上前缀,如impala::default.person

  • 列的数量:列数不能超过300个,如果你在迁移数据时确实有300个以上的列,则可以拆分为多个表,每个表都要保留主键,以便可以通过视图将它们合并在一起。

Docker部署Kudu

如果只是为了评估Kudu的功能或者自己搭建学习环境资源不够,Docker方式部署Kudu是一个很好的选择(目前还是实验性功能),可以让你十来分钟就构建集群Kudu集群用于评估和学习。

在部署之前,请找到一台宿主机,例如我们之前使用的node01。因为Kudu相对独立,他并不依赖Hadoop体系下的其他组件,所以你可以把其他组件都停掉。

Kudu源代码准备

  • 克隆Kudu源代码

    如果你的网速比较快,可选择如下方式克隆kudu源代码:

    git clone https://github.com/apache/kudu cd kudu
    
  • 使用离线代码包

    如果下载速度很慢的,可以使用老师提供的离线包(kudu.tar.gz),解压缩即可:

    #解压缩 
    unzip kudu.zip 
    #进入kudu目录 
    cd kudu
    

    kudu目录结构如下:

    MSIZE

    我们看到有个docker目录,实际上构建docker容器的Dockerfifile及其他文件就在该目录下。

启动Kudu Docker集群

(1)配置环境变量

设置如下环境变量:

#设置KUDU_QUICKSTART_IP环境变量 
export KUDU_QUICKSTART_IP=$(ifconfig | grep "inet " | grep -Fv 127.0.0.1 | awk '{print $2}' | tail -1) 
#选择kudu docker镜像版本,默认会使用latest版本 
export KUDU_QUICKSTART_VERSION="1.10.0"

如果想要永久生效,可以把环境变量写到环境变量文件里去。

(2)启动集群

接下来使用docker-compose启动一个3个Master和5个tablet server组成的集群:

#加上-d选项就可以在后台运行了 
docker-compose -f docker/quickstart.yml up -d

注意:Docker Compose是 docker 提供的一个命令行工具,用来定义和运行由多个容器组成的应用。执行过程因为要去远程拉取kudu的docker镜像所以时间有点久。

小提示:如果拉取镜像很慢,可以使用国内的镜像,网易镜像慢可以切换为阿里云的,自己可以换一换,每个地区网络环境不一样。

最终会启动了8个容器,分别运行3个kudu-master和5个kudu-tserver:

MMSIZE

集群采用docker网络,因此三个master的RPC地址分别是:7051,7151,7251,web服务端口分别是:8051,8151,8251。

因此可以访问web ui(访问哪个kudu master都可以,IP换成宿主机的IP,注意是安装Docker的那台机器):http://192.168.2.25:8051/

看到如下界面集群就启动完毕了:

MMSIZE

我们可以进入到一个某个容器内部:

docker exec -it $(docker ps -aqf "name=kudu-master-1") /bin/bash

然后可以在容器内部执行健康检查了:

kudu cluster ksck kudu-master-1:7051,kudu-master-2:7151,kudu-master-3:7251

MMSIZE

退出容器:

exit

运行示例程序

依然先进入kudu源代码目录:

git checkout 1.10.0 
export KUDU_USER_NAME=kudu 
cd examples/java/java-example 
mvn package 
java -DkuduMasters=localhost:7051,localhost:7151,localhost:7251 -jar target/kudu-java-example-1.0-SNAPSHOT.jar

示例程序是一个Java程序,它会建表、插入数据、修改表结构、查询数据、删除表,正常情况会出现如下界面:

MSIZE

关闭集群

通过如下两种方式关闭集群:

#先回到kudu目录 
#一定要切换回master分支 
git checkout master 
#优雅的关闭集群,由docker-compose决定容器的停止顺序 
docker-compose -f docker/quickstart.yml down

或者:

#stop 所有kudu容器 
docker stop $(docker ps -aqf "name=kudu")

如果你想删除集群状态,可以删除docker容器和存储卷即可:

docker rm $(docker ps -aqf "name=kudu") 
docker volume rm $(docker volume ls --filter name=kudu -q)

注意:这仅用于演示和学习以及功能测试,不应用于生产或性能/规模测试。

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值