kudu是cloudera在2012开始秘密研发的一款介于hdfs和hbase之间的高速分布式存储数据库。兼具了hbase的实时性、hdfs的高吞吐,以及传统数据库的sql支持。作为一款实时、离线之间的存储系统。定位和spark在计算系统中的地位非常相似。如果把mr+hdfs作为离线计算标配,storm+hbase作为实时计算标配。spark+kudu有可能成为未来最有竞争力的一种架构。

      也就是kafka->spark->kudu这种架构,未来此架构是否会风靡,暂且不表。来分析下kudu的一些特性:

传统的实时/离线计算架构几乎都是分离的,用户需要把实时的数据从一个实时队列系统导入到在线存储,再导出到离线存储系统。有时候离线和实时的数据又存再join或并用的可能性。在计算领域spark已经几乎做到的离线+准实时(秒)的架构一体化覆盖。但是毕竟spark只是计算系统,还缺乏一套存储系统与之相配合。hbase查询不友好,alluxis数据结构和程度差。kudu目前看来是最优的候选系统,再加上有cloudera背书,应该不错。

      传统系统几大痛点:

      1、应用系统需要在实时、离线系统之间把数据倒来倒去,写很复杂的code

      2、系统庞杂,各种备份,安全策略,监控系统

      3、系统从实时系统中流出到离线系统才好做OLAP分析,这层转换存在延迟

      4、现实上来说,系统总是会存在落后的数据、对过去数据的修改,删除。然而当过去的数据已经被归档,这些操作需要昂贵的重写以及partiion交换或者各种人工干预

 

Kudu弥补了高速顺序吞吐系统和低延迟随机访问系统之间的gap。它同时提供行级别的插入,更新,删除。也提供类似Parquet的批量scan,列读取等。

 

2.1 表结构和schema

      和nosql不同,kudu中的字段带type,例如Int32或者String。几个列组合起来形成主键(和hbase类比rowkey需要自己来维护),就是mysql中唯一索引概念同时又是主键。作为update或者deleted的index(但并不是说只能通过这个组合键更新或删除)

      看起来和关系数据库比较像吧,但kudu又可以随时更新表字段添加或删除(主键列们除外)

      nosql中提倡“所有数据都是bytes”,kudu不这样做:

      1、明确的类型让我们可以更好的做packing,例如integer

      2、更好的和sql类型的工具系统、bi系统、数据导出系统集成

      不过kudu不提供除了主键之外的二级索引或者唯一键索引,当前kudu需要用户指定固定的主键列,未来可能会生成代替的key

 

2.2 写操作

      insert、update、delete的aip必须指定主键,当然谓词操作在高级操作API中是支持的。kudu提供java和c++的api,python在实验中

 

2.3读操作

      只提供Scan接口,条件提供<,>,=几种过滤符

 

一致性模型:

      这块主要是如何协调多个client端读写时的先后顺序以及加锁操作等。kudu学习了spanner的commit-wait的模式。start,之后commit-wait,两阶段提交。中间的时间采用HybridTime算法实现

 

时间戳:

      和hbase反向设计,写不支持时间戳,避免用户乱用。读可以加时间戳读取过去一个时间的快照。这一点上感觉各有优劣吧,对读更加友好

 

经典的主从结构,和bigtable、gfs一脉相承。

 

分区比较特别:

      同时支持hash partition和range partition。这一点和hbase的rowkey设计比较像。例如插入的是时间顺序字段,就按照时间戳做hash partition,利于写压力分担到各个tablet。range-partition和hbase类似,根据主键列的顺序来划分range

      文中还有一段话是说可以按时间range分区+metirice名称、hostname的hash分区一起来决定真实的分区是什么

 

备份:

       为了提供高可用以及大规模,kudu采用自己的备份机制,一般是3、或者5份,master会管理这些备份。kudu采用Raft算法来复制这些tablets。客户端先找到leader replice,若果当前已经不是就转给leader,本地说记录,通过MVCC机制来管理读写并发。这些操作会被同步费followers。主备份机器上如果写完了wal,这个写就可以被提交给其他备份。但是kudu又并不是墙纸指定这个机制,一定要在操作日志写完之后commit,避免因为leader的磁盘慢导致性能问题。对于磁盘写“成功”,kudu提供两种模式供用户选择——写入buffer cache或者一定要调用fsync才算成功

       当followers挂掉,leader会再次同步数据给其他备份机,如果leader挂了,Raft这个时候会被启用选举一个新的leader(和paxos的区别还待研究)。kudu采用500ms心跳,1500ms的选举时间

       Kudu采用Raft时做了两个改进:

      1、指数级的回退机制,保障在集群繁忙时的选举效率

      2、在leader联系一个follower时,会每次一次进一步来发现点位。但是kudu会直接跳到最后一次所知道的committedIndex,加快寻找速度

      同时,这里的replicate有个大胆的设计——同步operation而非数据本身。这和hdfs分3份存储很不一样。hbase利用了hdfs的文件系统保持3份备份。而kudu则同步oplog同步实体文件,看起来每个备份都会有额外的replay的操作

      看看kudu怎么说吧:

      1、当主备份数据在做flushes或者compaction时,从备份不需要做这些操作,避免对集群的冲击以及额外的性能损耗

      2、开发过程中发现会有少量的物理存储层竞争,因为物理存储并不会被复制,所以这些问题不会导致不可恢复的错误。我们可以探测到任何一个备份的错误,并对其做修复

 

      这一点来说kudu避开了操作的复杂复制,因为对主备份数据的操作具有事物的复杂性,这些再同步到从备份代价就很大了。hbase因为底层采用hdfs所以不存在此问题。所以从kudu的角度来看这确实是一个好的设计。

      然而据我分析还有另一层原因——kudu底层假设目前的物理存储硬件并没有那么脆弱,恢复并不是一个经常性事件。区别于hdfs认为失效是常规事件(上万台服务器硬盘故障的频率和吃饭没两样,廉价硬件堆出来存储)。kudu明显是高富帅方案,既然要告诉分析也不能用太差的硬件吧。另外还特别和intel合作优化享受最新硬件IO方面的性能提升。

 

      配置变更

      主要讲了新增备份数,以及减少备份或者server配置的一些操作,还ok。没有太特别的

      

      KuDu Master

      1、作为目录管理机,保存了tables的柜机,tablets是否存在,他们的schemas,复制级别,其他的元数据信息。当创建、alter、删除表,master都会向tablets协调这些操作并保证最终完成

      2、集群协调者,存储当前哪些server存在,哪些是存活的。server挂掉后启动复制机制

      3、tablet目录,每个tablet都存储在哪些server上

      Kudu采用集中式、可复制的设计,而非P2P模式设计简化实现,debuging以及操作

 

     解读:主从式设计目前来看是实现简单,效率最高的一种方式。只要把master元信息做备份master快速恢复,几乎能解决大部分问题。

 

     目录管理机:

      catalog信息也是一个tablet,类似hbase里的meta表,这个信息会在内存中穿写。这种单层目录树(hbase有双层)在目前的硬件架构下并不会成为扩展瓶颈。kudu开发者任务如果未来可能成为瓶颈就把它缓存到页cache中。(又一次证明通过硬件来简化软件设计的思路,这种简化并不能说是退步,感觉bigtable已经把存储系统设计到极致了,kudu的改进尽量通过硬件能力来弥补,同时带来效率和性能的提升)

     目录管理机存储了表当前状态,当前版本的table schema,哪些tablet属于table。建表语句会先到master这边往catalog写一个数据,更新为CREATING state。并且异步选择一些servers作为tablets的存储,在master端建好tablet metadata,再异步请求到每个server。replica 创建失败或者超时,master可以回滚重做都很方便。master如果自己挂了,下次启动时会确保每个操作被传递给server,这种协议设计成幂等,也就是可以反复重做。(应该是server端简单判断下发的重复消息,然后做出忽略操作)

     catalog table也是Kudu的一个tablet,所以master信息很好被重载到另外一个master接管这一切。  这里设计和hbase比较像,hdfs中的namenode需要引入一个外部存储来做ha也是增加了复杂性。只要记住最初的几个备份就行,不过hdfs有个特殊就是元数据信息可能很大,一两个block也存不下。不像hbase、kudu元信息大小可控好存。

     

      集群协调者:

      普通的协调者作用,不过里面有一点对于复制,master并不直接参与,而是放手让leader replica的server来协调。master会关注大局,比如有些本该删除的备份还依然汇报心跳,他就会出面发出deleteRPC。check最终结果没问题,避免leader replica有时候出些岔子。 master充分授权(replica leader),制定规则(Raft),但是最后结果检查并且兜底。感觉是一个好的技术管理模式,分布式系统到最后和团队管理都有点像了。

 

      tablet目录:

      client一般像master请求tablet位置,并在本地做缓存。然后直接联系server,如果server此时已经不是leader replica,返回失败,client再去master请求。这样减少了client对master请求的压力。对于不经常改变的信息这种快速失败重试的机制是很ok的。目前kudu在270节点的cluseter,一次tablet location 查询99.99%在3.2ms内,95%在374微秒,75%在91微秒。kudu开发者任务当前这个不会成为瓶颈,后续也可以通过再把location信息做分区,给几个不同的机器来提供服务。(走hbase的metatable路线,做起来也没什么问题)

     

      Tablet 存储

      tablet server并不采用分布式文件系统来存储物理文件。这样kudu就可以自由的开发基于table、基于tablet或者基于replica的存储层。当前还只是有一个存储层。存储设计目标:

      1、列扫描要快:像Parquet和ORCFile一样,快速的scan各个列中的数据

      2、低延迟:快速的找到随机指定的行

      3、相比于高峰性能,用户更愿意接受一个持续稳定性能的组件

      所以,kudu自己设计了一套列存架构。这个应该是核心了,列存在目前有好多优秀设计的前提下自己设计一套符合kudu独特架构的,也确实是有必要。

 

      RowSets,又分为MemRowSets,DiskRowSets,分别对应内存和磁盘上的数据存储。

      MemRowSets采用B-tree的数据结构,乐观锁。

      1、不支持删除元素,MVCC来表示删除

      2、不支持随意update,但是运行value 的size不改变的update。这种许可通过linked list来对比和置换操作实现

      3、我们把叶子节点都加上next指针,形成链表,提升scan性能

      4、不实现全量的字典树,而只是做了一颗单tree,因为我们并不准备考虑大规模的随机查询吞吐量

 

      memRowSet按照行存储数据,采用了LLVM、JIT-complile、memcmp等一些特殊技术来家属查询,当然这种查询可以是根据主键范围(更快)或者根据独立的字段查询

 

      DiskRowSet实现

      按照32MB一个来flush一个到磁盘上,这个大小是保障每个DiskRowSet不会太大,而且能支持不断增长的compaction需求。DiskRowSets按照primary的范围分段,互补重叠。这个和hbase类似,但是kudu是预先划定好partition的,所以基本上可以认为这个划分和memRowSet中能很好对应上。

      DiskRowSet包含了base data 和delta 存储。base data是按照列来组织,每个列被分block存储在硬盘上。这种存储在加上B-tree索引让row可以被快速查询到。

      列存储采用bitshuffle来encoding,fromt coding也可选。压缩采用LZ4,gzip,bzip2.用户可以根据列内容需要去定制——text列对应gzip,integers要按bit来packed,还有其他的一些页参考了Impala和Parquet的设计。

      primary所包含的列单独拿出来做encode,再配合Bloom filter做快速判存

      列编码想要update非常困难,一旦flush就不会再改变。update和deletes会被存储在一个叫delta的存储中。delta store可以存在内存、可以存在磁盘。DeltaMemStore和上面的设计一样是一个B-tree。这里面存储了row_offset和修改时间timestamp。有点类似SET column id = 'foo' 或者delete。

      所以flush的时候memRowSet会带上DeltaStore一起往DiskRowSet中存储。flush的时候回直接刷delta 到磁盘。(刷的时候还不和base data做合并操作)

      

      插入方法:

      kudu区分insert和upsert,对于insert可以直接保存,对于upsert会查询原有记录中是否存在。所以这也是overwrite和append的坑 。再就是由于唯一键的设计导致每次插入都要查询下这个键是否存在于历史数据中。。。这一点决定了写入速度不可能比hbase快呀。而且特别是对于同一个主键的反复写性能就更加局限了,因为无法扩展。醉了

      kudu提出了两个补救方案:1、缩减查找的DiskRowSet范围;2、加速单个DiskRowSet的查找效率

      Bloom filter再加上LRU page cache,做到1.;通过index+page cache来做到2  (hbase也同样做了这两个,但是插入时不用look up是绝对优势)

 

      读方法:

      读操作经常是批量的,KUDU每次只读一个列。找到row对应的列值,并读取。

     

       DeltaCompaction,RowSetCompaction这些和hbase非常类似了,就不再细写了。总的来说当前的存储如何支撑sql查询

 

       还有几点补充:

       Kudu很好的结合MR(虽然现在没什么人用了),spark,impala。甚至sql引擎直接用的impala的。在读写性能上的表现类比了impala on parquet/HDFS,读稍微会快一些。比phoenix快很多。随机读方面性能指数也不错。

      但是写方面,就表现一般了,特别是和hbase比差很多。原因前面也说过写的时候要去lookup primary key。目前看来写最快的就是hbase了,对于写已经设计到极致没有之一!

 

      总结一句,对于近似online的分布式数据库kudu是一个甜品级应用,享受了impala的查询,以及稍弱于hbase的写入。还是实时写实时读的!配上spark会是一个不错的选择。但是由于目前还是孵化中的版本,坑还是有点多的。生产系统使用的时候需要慎重!