Kudu,对应中文的含义应该是非洲的一种带条纹的大羚羊。在软件行业,大家新开发一个软件或者系统都喜欢给软件一个响亮的代号或者名字,比如苹果的OS的 Mavericks , Lion等等。Cloudera则给自己新开发的大 数据存储 系统命名为Kudu,我猜想背后的原因可能还是Kudu代表了速度快吧。在Cloudera官方的博客上,对于Kudu的描述是:一个弥补 HDFS 和 HBase 之间的缺口的新型的存储,它能够更有效的利用现代硬件的CPU和IO资源,既能够支持分析,又能够支持更新、删除和实时查询。
又到一年的国庆节假期,七天假期转眼间就要过去了。在全国人民大迁徙的情况下,去各处旅游,参加朋友圈摄影展似乎不是一个明智的举动。用两天的时间错开高峰回家看看老爸,十月四日回京后,立刻就迎来了大雾霾天。跑步的计划是没办法实施了,剩下就只能读书或者看电影了。老婆大人生日,陪老婆看了口碑片的“夏洛,特烦恼”,不过鉴于自己可怜的文笔,写个电影观后感也没人捧场,能够给自己这个七天假期画个完满句号的似乎只能是写技术相关的文章了。恰好节前 Cloudera 发布了一个新的 Hadoop 生态系统上的存储Kudu,那我就花些时间啃一下Cloudera关于Kudu的论文,解析一下这个Kudu是个什么鬼吧?
Kudu,对应中文的含义应该是非洲的一种带条纹的大羚羊。在软件行业,大家新开发一个软件或者系统都喜欢给软件一个响亮的代号或者名字,比如苹果的OS的 Mavericks , Lion等等。Cloudera则给自己新开发的大 数据存储 系统命名为Kudu,我猜想背后的原因可能还是Kudu代表了速度快吧。在Cloudera官方的博客上,对于Kudu的描述是:一个弥补 HDFS 和 HBase 之间的缺口的新型的存储,它能够更有效的利用现代硬件的CPU和IO资源,既能够支持分析,又能够支持更新、删除和实时查询。
说到开发Kudu的初衷,Cloudera的解释是他们在客户的现场做大数据项目时发现,真正客户面临的问题在当前的Hadoop生态系统下,都是一个混合的架构,如下图所示:
在这个架构中,HBase被用来当作数据载入和更新的存储,这个存储适合于实时的查询,而数据随后被处理为parquet格式的文件,从而适合后续的利用Impala等工具进行分析。而Kudu则主要针对这个混合架构的需求所设计开发的一个存储系统,希望能够降低这种混合架构系统的复杂性,同时能够满足客户类似的需求。
Kudu设计的目标:
- 对于scan和随机访问都有非常好的性能,从而降低客户构造混合架构的复杂度
- 很高的CPU利用效率,从而提高用户在现代CPU使用上的投入产出比
- 很高的IO利用效率,从而更好的使用现代的存储
- 能够对数据根据数据所在位置进行更新,从而减少额外的处理和数据的移动
- 支持多数据中心的双活集群复制
Kudu在Hadoop生态系统中所处的角色如下图:
从这个图可以看到,kudu期望自己既能够满足分析的需求(快速的数据scan),也能够满足查询的需求(快速的随机访问),那Kudu到底采用了什么技术来达到这个目的的呢?
想要了解Kudu的详细的设计,不得不去看一下kudu的论文,下边我们就看一下Kudu实现的一些技术的架构和考虑,其思想对于我们设计一个 分布式 的存储系统,也有很多的借鉴意义。
从使用者的角度来看Kudu
从使用者的角度来看,kudu是以表的形式进行结构数据存储的存储系统。一个kudu集群有多个表,每个表都是由shema进行定义,包含有限个列,每列有一个名字和类型,并且可以选择是否支持空值。这些列的一些有序的列可以定义为表的主键,主键有唯一性约束,并且做为删除和更新的索引。这些特性与传统的关系型数据库非常的相似,但是与 Cassandra ,mongodb,riak, bigtable 等分布式数据存储却非常的不同。
与使用关系型数据库一样,kudu的用户必须要先在创建表时给定表的 schema ,如果插入不存在的列会报错,并且违反唯一性约束也会有相应的错误。用户可以通过alter table来增加或者删除列,但是主键列不能够被删除。虽然采用类似于关系型数据库的表设计,不过Kudu的设计不支持二级索引,这个限制和HBase是一样的。另外,当前的kudu要求每一个表必须定义一个逐渐列。
对于kudu的写操作来说,insert,update, delete 都必须指定主键才能进行,另外也和HBase一样,kudu不支持跨行级别的事务。对于读来讲,kudu只提供了一个scan操作来从表读取数据,不过用户可以给定一些条件来过滤结果。目前kudu支持两种条件,一种是用一列和一个常量进行比较,另外一种是给定主键的范围。对于kudu的查询来讲,用户还可以限定只返回部分列,因为Kudu的实际存储是列式的存储,这种限定可以大幅度的提高性能。
kudu的架构
kudu的集群架构与HDFS,HBase架构类似,Kudu有一个Master Server负责 元数据 的管理,多个Tablet Server负责数据的存储。Master Server可以通过复制来是先容错和故障恢复。
与大部分的 分布式数据库系统 一样,kudu的表是水平分区的,这些水平分区的表叫做tablets。每一行都会根据它的主键唯一的映射到一个tablet上。对于吞吐量要求比较高的情况下,一个大表可以分为10到100个tablets,每个tablet差不多可以是10G大小。
至于 partition 的方式,Kudu支持在创建表时给定一个partition schema,这个partition schema是由0个或者多个 hash 分区规则以及一个可选的范围分区规则组成。hash分区规则由主键列的子集列和分区bucket数量构成,例如:
DISTRIBUTE BY HASH(hostname, ts) INTO 16 BUCKETS
这个规则基本是采用拼接需要hash的列,对桶数求余,然后生成一个32位的整形作为结果的分区key。
而范围分区规则则是基于有序的主键列,将列的值按照能够保持顺序的编码进行处理,然后将数据进行相应的分区。
相对于目前大部分的 NoSQL 系统来讲,Kudu的分区策略还是比较完善的,更像是传统的数据库或者 MPP 数据库。
Kudu的复制
Kudu采用了Raft协议来实现表数据的复制,在具体的实现过程中,Kudu对于Raft协议也做了一些修改和增强,具体的表现在:
- 在Leader选举失败时,采用指数算法(Exponential Backoff)来重试
- 当一个新的leader在联系一个follower时,如果follower的log与自己不相同,kudu会立即跳回到最后的committedIndex,这能够大大减少在网络上传输的冗余的操作,而且实现起来简单,并且能够保证不一致的操作在一次往返后就被抛弃掉。
另外,kudu的复制不是复制磁盘上的存储的表的数据,而是复制操作日志。表的每个副本的物理存储是完全 解耦 的,它带来了如下的好处:
- 当一个副本在进行后台的一些物理层的操作(flush或者compact)时,一般其他节点不会同时对同一个tablet做同样的操作,因为Raft协议的提交是过半数副本应答,这样后台物理层操作对于提交的影响将会降低。
- 在开发过程中,kudu团队遇到过一些罕见的物理存储层race condition情况,因为物理存储的松耦合,这些race condition都没有造成数据的丢失。
Kudu的Master节点
前面提到了Kudu的集群架构,具体到Kudu的Master进程,它主要的职责包括:
- 它是一个catalog manager,维护table, tablets的信息,以及创建table的schema,复制的级别以及其他的一些元数据。
- 它是集群的协调者,跟踪集群中server的存活状态以及在某个server死掉后对数据进行重新分布。
- 它是一个tablet directory,跟踪那个tablet server存储哪个tablet的副本
由于Kudu的master是很多操作的中心节点,它的实现是否优化对整个集群的稳定性和对数据的访问都会有非常大的影响。Kudu本身在master节点的实现上有很多的优化,具体的包括:
- 在实现catalog manager时,采用内存write-through cache来维护catalog
- 在创建表时,它会异步的选择tablet server,并且异步的将副本信息发送给tablet server
- 从master server到tablet server的所有的信息都是幂等的,因此当失败时,可以重试。
- 另外catalog 的信息的物理存储也是tablet,因此可以方便的采用Raft协议进行复制,从而实现高可靠型。
- master server是目录信息的唯一来源,但是对于集群的动态状态信息,master server只是观察者。tablet server来负责tablet副本的位置,当前的Raft配置信息,以及tablet当前的版本信息等等。这一切的实现都是依赖于Raft协议来完成的。具体的细节,这里不再阐述。
- Kudu的客户端维护了一个包含最近访问的每个tablet的信息的元数据cache,从而能够提高性能和降低网络开销。如果本地cache的信息已经陈旧,客户端会从master节点更新自己的cache.
Kudu的存储设计
在tablet server上,每个tablet副本都做为一个完全独立的实体,从而与上层的 分布式系统 和分区进行解耦。在Kudu的tablet存储设计中,主要考虑如下几个因素:
- 快速的列扫描,能够达到可以媲美Parquet和ORCFile的类似的性能,从而可以支撑分析业务
- 低延时的随机更新,在随机访问时,希望达到Olog(n)的时间复杂度
- 性能的一致性
为了达到这个目的,Kudu从头设计了一个全新的混合列式存储架构。在这个新的存储架构中,引入了一个 存储单元 RowSets,每个tablets都是由多个RowSets组成,Rowsets分为内存中的MemRowSets和磁盘的DiskRowSets。任何一条存活的数据记录都只属于一个RowSet。在任意时刻,每个tablet都只有一个唯一的MemRowSets,用于存储最近插入的行。有一个后台线程定期会flush MemRowSets到磁盘,当MemRowSets被flush时,一个新的空的MemRowSets会被创建来替换它,而被flush的MemRowSets则会变成一个或者多个DiskRowSets。Flush过程是完全并行的,对正在flush的MemRowSets的读操作还会在MemRowsets上进行,而更新和删除操作则会被记录下来,在flush完成后更新到磁盘上。
MemRowSets的实现是一个支持并发的内存B-Tree,借鉴了MassTree的实现,并且做了一些修改:
- 不支持在树上进行元素的删除,而是采用MVCC记录删除的信息。因为MemRowSets最终会flush到磁盘,因此记录的删除可以推迟到flush的过程中。
- 不支持在树上对元素的任意的修改,而只是在值的修改不改变值占用的空间大小时才支持。
- 叶子节点的连接是通过一个next指针来实现,这样可以显著提高顺序scan的性能。
- 为了提高随机访问的scan的性能,采用了比较大的节点的空间大小,每个是4个CPU cache-lines的大小(256字节)
- 使用SSE2预取指令集以及LLVM编译优化来提高树的访问性能
DiskRowSets的实现同样做了很多实现的优化来提高性能,包括:
DiskRowSets在实现时被分成了两个部分,一个基础的数据部分(base data)以及一个变化存储(delta stores)。Base data是采用列式存储来存储数据,每一列被切分成一个连续的数据块写到磁盘,并且分成小的页来支持更细粒度的随机访问。它还包含一个内嵌的B-Tree索引,从而方便定位页。base data的存储支持bzip2,gzip,LZ4的压缩。
除了每列数据会flush到磁盘,Kudu还在磁盘写入了一个主键索引列,存储了每一行的主键编码,同时还写了一个布隆过滤器到磁盘,从而方便判断一行是否存在。
因为列式存储的数据不容易更新,所以base data在写到磁盘后就不会再改变,变化的值都是通过delta stores来进行存储。delta stores通过在内存的DeltaMemStores和磁盘上的DeltaFiles来实现。DeltaMemStore也是一个支持并发的B-Tree。DeltaFiles是一个二进制的列式数据块。delta stores包含了列数据的所有的变化,维护了一个从(row_offset,timestamp)数组到RowChangeList记录的映射。
Kudu存储的实现对于列数据采用Lazy Materializtion从而提高读取的性能。
因为变化存在delta stores中,如果delta store数据非常多,则会发生性能问题。Kudu有后台线程会定期根据delta stores的大小来进行压缩,将变化写回到base data中。
除了delta store的压缩,RowSet也会定期做压缩,通过RowSet压缩,可以实现:
1,移除删掉的行
2,通过基于key的合并,减少同样key范围DiskRowSets的数量。
与Hadoop生态系统的集成
Kudu提供对Hadoop Input和Output数据的binding,从而可以方便的与Hadoop MapReduce集成。这个binding同样可以方便的与Spark集成,一个小胶水层可以将Kudu表bind为Spark的DataFrame或者Spark SQL的table。通过这个集成,Spark可以支持Kudu的几个关键的特性:
- 数据本地性 – 能够知道表的数据在哪个tablet server上,从而支持本地数据处理和计算
- 列规划 – 提供了一个简单的API使用户可以选择哪些列是他们的任务中需要的,从而减少IO读取
- 委托下沉 – 提供一个简单的API去指定数据在被传递给任务时可以在服务端进行计算,从而提高性能。
另外,由于Impala也是Cloudera主导开发的,Kudu和Impala也做了深度的集成。
性能评测
论文对Kudu的性能与parquet进行了对比,同样都是采用Impala,Kudu性能比parquet有31%的提高。
另外论文还利用TPC-H对Impala-Kudu和Phoenix-HBase、Impala-Parquet进行了性能的比较,结果如下图:
对于随机访问,论文通过YCSB对Kudu和HBase进行了对比的测试,从结果来看Kudu的吞吐率还是比HBase稍差,如下图:
在大数据技术发展到今天,针对不同的场景,有不同的可选的解决方案。对于存储来讲,parquet、ORCFile等列式存储大大提高了数据存储的效率以及进行数据分析的性能,而基于BigTable思想的HBase,Cassandra则弥补了传统关系型数据库在大数据集上水平扩展能力的不足。不过,对于目前的大数据应用场景来讲,既需要快速的进行分析,也需要对数据的修改、删除和快速的随机读取。在这种需求的情况下,kudu无疑给出了一个可选的答案。当然kudu目前还在逐渐的完善过程中,版本还是beta版本,不过我相信它在不久的将来毕竟会得到很大的发展,在越来越多的大数据架构中占据自己的一席之地。