背景
Cloudera 在 2016 年发布了新型的分布式存储系统 --kudu,kudu 目前也是 apache 下面的开源项目. Hadoop 生态圈中的技术繁多, HDFS 作为底层数据存储的地位一直很牢固. 而 HBase 作为 Google BigTable 的开源产品, 一直也是 Hadoop 生态圈中的核心组件, 其数据存储的底层采用了 HDFS, 主要解决的是在超大数据集场景下的随机读写和更新的问题. Kudu 的设计有参考 HBase 的结构, 也能够实现 HBase 擅长的快速的随机读写, 更新功能. 那么同为分布式存储系统, HBase 和 Kudu 二者有何差异? 两者的定位是否相同? 我们通过分析 HBase 与 Kudu 整体结构和存储结构等方面对两者的差异进行比较.
整体结构
HBase 的整体结构
HBase 的主要组件包括 Master,zookeeper 服务, RegionServer,HDFS.
Master: 用来管理与监控所有的 HRegionServer, 也是管理 HBase 元数据的模块.
zookeeper: 作为分布式协调服务, 用于保存 meta 表的位置, master 的位置, 存储 RS 当前的工作状态.
RegionServer: 负责维护 Master 分配的 region,region 对应着表中一段区间内的内容, 直接接受客户端传来的读写请求.
HDFS: 负责最终将写入的数据持久化, 并通过多副本复制实现数据的高可靠性.
Kudu 的整体结构
Kudu 集群中存在两种主要组件:
(1)TServer, 负责管理 Tablet,tablet 是负责一张表中某块内容的读写, 接收其他 TServer 中 leader tablet 传来的同步信息.
(2)Master, 集群中的管理节点, 用于管理 tablet 的基本信息, 表的信息, 并监听 TServer 的状态. 多个 Master 之间通过 Raft 协议实现数据同步和高可用.
主要区别
Kudu 结构看上去跟 HBase 差别并不大, 主要的区别包括:
1,Kudu 将 HBase 中 zookeeper 的功能放进了 Master 内, Kudu 中 Master 的功能比 HBase 中的 Master 任务要多一些.
2,Hbase 将数据持久化这部分的功能交给了 Hadoop 中的 HDFS, 最终组织的数据存储在 HDFS 上. Kudu 自己将存储模块集成在自己的结构中, 内部的数据存储模块通过 Raft 协议来保证 leader Tablet 和 replica Tablet 内数据的强一致性, 和数据的高可靠性. 为什么不像 HBase 一样, 利用 HDFS 来实现数据存储, 猜测可能是因为 HDFS 读小文件时的时延太大, 所以 Kudu 自己重新完成了底层的数据存储模块, 并将其集成在 TServer 中.
数据存储方式
HBase
HBase 是一款 Nosql 数据库, 典型的 KV 系统, 没有固定的 schema 模式, 建表时只需指定一个或多个列族名即可, 一个列族下面可以增加任意个列限定名. 一个列限定名代表了实际中的一列, HBase 将同一个列族下面的所有列存储在一起, 所以 HBase 是一种面向列族式的数据库.
HBase 将每个列族中的数据分别存储, 一个列族中的每行数据中, 将 rowkey, 列族名, 列名, timestamp 组成最终存取的 key 值, 另外为了支持修改, 删除, 增加了一个表征该行数据是否删除的标记. 在同一个列族中的所有数据, 按照 rowkey:columnfamily:columnQulifier:timestamp 组成的 key 值大小进行升序排列, 其中 rowkey,columnfamily,columnQulifier 采用的是字典顺序, 其值越大, key 越大, 而 timestamp 是值越大, key 越小. HBase 通过按照列族分开存储, 相对于行式存储能够实现更高的压缩比, 这也是其比较重要的一个特性.
HBase 对一行数据进行更新时, HBase 也是相当于插入一行新数据, 在读数据时 HBase 按照 timestamp 的大小得到经过更新过的最新数据.
Kudu
Kudu 是一种完全的列式存储引擎, 表中的每一列数据都是存放在一起, 列与列之间都是分开的.
为了能够保存一部分历史数据, 并实现 MVCC,Kudu 将数据分为三个部分. 一个部分叫做 base data, 是当前的数据; 第二个部分叫做 UNDO records, 存储的是从插入数据时到形成 base data 所进行的所有修改操作, 修改操作以一定形式进行组织, 实现快速查看历史数据; 第三个部分是 REDO records, 存储的是还未 merge 到当前数据中的更新操作. 下图中表示的是在 Kudu 中插入一条数据, 更新数据两个操作的做法, 当然做法不唯一, 不唯一的原因是 Kudu 可以选择先不将更新操作合并到 base data 中.
差异分析
(1)HBase 是面向列族式的存储, 每个列族都是分别存放的, HBase 表设计时, 很少使用设计多个列族, 大多情况下是一个列族. 这个时候的 HBase 的存储结构已经与行式存储无太大差别了. 而 Kudu, 实现的是一个真正的面向列的存储方式, 表中的每一列都是单独存放的; 所以 HBase 与 Kudu 的差异主要在于类似于行式存储的列族式存储方式与典型的面向列式的存储方式的差异.
(2)HBase 是一款 NoSQL 类型的数据库, 对表的设计主要在于 rowkey 与列族的设计, 列的类型可以不指定, 因为 HBase 在实际存储中都会将所有的 value 字段转换成二进制的字节流. 因为不需要指定类型, 所以在插入数据的时候可以任意指定列名(列限定名), 这样相当于可以在建表之后动态改变表的结构. Kudu 因为选择了列式存储, 为了更好的提高列式存储的效果, Kudu 要求在建表时指定每一列的类型, 这样的做法是为了根据每一列的类型设置合适的编码方式, 实现更高的数据压缩比, 进而降低数据读入时的 IO 压力.
(3)HBase 对每一个 cell 数据中加入了 timestamp 字段, 这样能够实现记录同一 rowkey 和列名的多版本数据, 另外 HBase 将数据更新操作, 删除操作也是作为一条数据写入, 通过 timestamp 来标记更新时间, type 来区分数据是插入, 更新还是删除. HBase 写入或者更新数据时可以指定 timestamp, 这样的设置可以完成某些特定的操作.
Kudu 也在数据存储中加入了 timestamp 这个字段, 不像 HBase 可以直接在插入或者更新数据时设置特殊的 timestamp 值, Kudu 的做法是由 Kudu 内部来控制 timestamp 的写入. 不过 Kudu 允许在 scan 的时候设置 timestamp 参数, 使得客户端可以 scan 到历史数据.
(4)相对于 HBase 允许多版本的数据存在, Kudu 为了提高批量读取数据时的效率, 要求设计表时提供一列或者多列组成一个主键, 主键唯一, 不允许多个相同主键的数据存在. 这样的设置下, Kudu 不能像 HBase 一样将更新操作直接转换成插入一条新版本的数据, Kudu 的选择是将写入的数据, 更新操作分开存储.
(5)当然还有一些其他的行式存储与列式存储之间在不同应用场景下的性能差异.
写入和读取过程
一, HBase
HBase 作为一种非常典型的 LSM 结构的分布式存储系统, 是 Google bigtable 的 apache 开源版本. 经过近 10 年的发展, HBase 已经成为了一个成熟的项目, 在处理 OLTP 型的应用如消息日志, 历史订单等应用较适用. 在 HBase 中真正接受客户端读写请求的 RegionServer 的结构如下图所示:
关于 HBase 的几个关键点:
(1)在 HBase 中, 充当写入缓存的这个结构叫做 Memstore, 另外会将写入操作顺序写入 HLOG(WAL)中以保证数据不丢失.
(2)为了提高读的性能, HBase 在内存中设置了 blockcache,blockcache 采用 LRU 策略将最近使用的数据块放在内存中.
(3)作为分布式存储系统, 为保证数据不因为集群中机器出现故障而导致数据丢失, HBase 将实际数据存放在 HDFS 上, 包括 storefile 与 HLOG.HBase 与 HDFS 低耦合, HBase 作为 HDFS 的客户端, 向 HDFS 读写数据.
1,HBase 写过程
(1)客户端通过客户端上保存的 RS 信息缓存或者通过访问 zk 得到需要读写的 region 所在的 RS 信息;
(2)RS 接受客户端写入请求, 先将写入的操作写入 WAL, 然后写入 Memstore, 这时 HBase 向客户端确认写入成功;
(3)HBase 在一定情况下将 Memstore 中的数据 flush 成 storefile(可能是 Memstore 大小达到一定阈值或者 region 占用的内存超过一定阈值或者手动 flush 之类的),storefile 以 HFile 的形式存放在 HDFS 上;
(4)HBase 会按照一定的合并策略对 HDFS 上的 storefile 进行合并操作, 减少 storefile 的数量.
2,HBase 读过程
HBase 读数据的过程比较麻烦, 原因包括:
(1)HBase 采用了 LSM-tree 的多组件算法作为数据组织方式, 这种算法会导致一个 region 中有多个 storefile;
(2)HBase 中采用了非原地更新的方式, 将更新操作和删除操作转换成插入一条新数据的形式, 虽然这样能够较快的实现更新与删除, 但是将导致满足指定 rowkey, 列族, 列名要求的数据有多个, 并且可能分布在不同的 storefile 中;
(3)HBase 中允许设置插入和删除数据行的 timestamp 属性, 这样导致按顺序落盘的 storefile 内数据的 timestamp 可能不是递增的.
下面介绍从 HBase 中读取一条指定(rowkey,column family,column)
(1)读过程与 HBase 客户端写过程第一步一样, 先尝试获取需要读的 region 所在的 RS 相关信息;
(2)RS 接收读请求, 因为 HBase 中支持多版本数据(允许存在 rowkey, 列族名, 列名相同的数据, 不同版本的数据通过 timestamp 进行区分), 另外更新与删除数据都是通过插入一条新数据实现的. 所以要准确的读到数据, 需要找到所有可能存储有该条数据的位置, 包括在内存中未 flush 的 memstore, 已经 flush 到 HDFS 上的 storefile, 所以需要在 1 memstore +N storefile 中查找;
(3)在找到的所有数据中通过判断 timestamp 值得到最终的数据.
二, Kudu
(1)Kudu 中的 Tablet 是负责表中一块内容的读写工作, Tablet 由一个或多个 Rowset 组成. 其中有一个 Rowset 处于内存中, 叫做 Memrowset,Memrowset 主要负责处理新的数据写入请求. DiskRowSet 是 MemRowset 达到一定程序刷入磁盘后生成的, 实质上是由一个 CFile(Base Data), 多个 DeltaFile(UNDO records &REDO records)和位于内存的 DeltaMemStore 组成. Base data,UNDO records, 和 REDO records 都是不可修改的, DeltaMemStore 达到一定大小后会将数据刷入磁盘生成新的 REDO records.Kudu 后台会有一个类似 HBase 的 compaction 线程按照一定的 compaction 策略对 tablet 进行合并处理:
a, 将多个 DeltaFile(REDO records)合并成一个大的 DeltaFile;
b, 将多个 REDO reccords 文件与 Base data 进行合并, 并生成新的 UNDO records;
c, 将多个 DiskRowset 之间进行合并, 减少 DiskRowset 的数量.
(2)Kudu 将最终的数据存储在本地磁盘上, 为了保证数据可靠性, Kudu 为一个 tablet 设置了多个副本(一般为 3 或 5 个). 所以一个 tablet 会由多个 TServer 负责维护, 其中有个副本称为 leader tablet, 写入的请求只能通过 leader tablet 来处理, 副本之间通过 Raft 协议保证其他副本与 leader tablet 的强一致性.
1,Kudu 写过程
Kudu 与 HBase 不同, Kudu 将写入操作分为两种, 一种是插入一条新数据, 一种是对一条已插入数据的更新.
1,Kudu 插入一条新数据
(1)客户端连接 Master 获取表的相关信息, 包括分区信息, 表中所有 tablet 的信息;
(2)客户端找到负责处理读写请求的 tablet 所负责维护的 TServer.Kudu 接受客户端的请求, 检查请求是否符合要求(表结构);
(3)Kudu 在 Tablet 中的所有 rowset(memrowset,diskrowset)中进行查找, 看是否存在与待插入数据相同主键的数据, 如果存在就返回错误, 否则继续;
(4)Kudu 在 MemRowset 中写入一行新数据, 在 MemRowset 数据达到一定大小时, MemRowset 将数据落盘, 并生成一个 diskrowset 用于持久化数据, 还生成一个 memrowset 继续接收新数据的请求.
2,Kudu 对原有数据的更新
(1)客户端连接 Master 获取表的相关信息, 包括分区信息, 表中所有 tablet 的信息;
(2)Kudu 接受请求, 检查请求是否符合要求;
(3)因为待更新数据可能位于 memrowset 中, 也可能已经 flush 到磁盘上, 形成 diskrowset. 因此根据待更新数据所处位置不同, kudu 有不同的做法:
当待更新数据位于 memrowset 时
a, 找到待更新数据所在行, 然后将更新操作记录在所在行中一个 mutation 链表中; 在 memrowset 将数据落盘时, Kudu 会将更新合并到 base data, 并生成 UNDO records 用于查看历史版本的数据和 MVCC,UNDO records 实际上也是以 DeltaFile 的形式存放;
当待更新数据位于 DiskRowset 中
b, 找到待更新数据所在的 DiskRowset, 每个 DiskRowset 都会在内存中设置一个 DeltaMemStore, 将更新操作记录在 DeltaMemStore 中, 在 DeltaMemStore 达到一定大小时, flush 在磁盘, 形成 Delta 并存在方 DeltaFile 中;
实际上 Kudu 提交更新时会使用 Raft 协议将更新同步到其他 replica 上去, 当然如果在 memrowset 和 diskrowset 中都没有找到这条数据, 那么返回错误给客户端; 另外当 DiskRowset 中的 deltafile 太多时, Kudu 会采用一定的策略对一组 deltafile 进行合并.
2,Kudu 读过程
1, 客户端连接 Master 获取表的相关信息, 包括分区信息, 表中所有 tablet 的信息;
2, 客户端找到需要读取的数据的 tablet 所在的 TServer,Kudu 接受读请求, 并记录 timestamp 信息, 如果没有显式指定, 那么表示使用当前时间;
3,Kudu 找到待读数据的所有相关信息, 当目标数据处于 memrowset 时, 根据读取操作中包含的 timestamp 信息将该 timestamp 前提交的更新操作合并到 base data 中, 这个更新操作记录在该行数据对应的 mutation 链表中;
4, 当读取的目标数据位于 diskrowset 中, 在所有 DeltaFile 中找到所有目标数据相关的 UNDO record 和 REDO records,REDO records 可能位于多个 DeltaFile 中, 根据读操作中包含的 timestamp 信息判断是否需要将 base data 进行回滚或者利用 REDO records 将 base data 进行合并更新.
三, Kudu 与 HBase 在读写上过程中的差异
1, 写过程
(1)HBase 写的时候, 不管是新插入一条数据还是更新数据, 都当作插入一条新数据来进行; 而 Kudu 将插入新数据与更新操作分别看待.
(2)Kudu 表结构中必须设置一个唯一键, 插入数据的时候必须判断一些该数据的主键是否唯一, 所以插入的时候其实有一个读的过程; 而 HBase 没有太多限制, 待插入数据将直接写进 memstore.
(3)HBase 实现数据可靠性是通过将落盘的数据写入 HDFS 来实现, 而 Kudu 是通过将数据写入和更新操作同步在其他副本上实现数据可靠性.
结合以上几点, 可以看出 Kudu 在写的性能上相对 HBase 有一定的劣势.
2, 读过程
(1)在 HBase 中, 读取的数据可能有多个版本, 所以需要结合多个 storefile 进行查询; Kudu 数据只可能存在于一个 DiskRowset 或者 MemRowset 中, 但是因为可能存在还未合并进原数据的更新, 所以 Kudu 也需要结合多个 DeltaFile 进行查询.
(2)HBase 写入或者更新时可以指定 timestamp, 导致 storefile 之间 timestamp 范围的规律性降低, 增加了实际查询 storefile 的数量; Kudu 不允许人为指定写入或者更新时的 timestamp 值, DeltaFile 之间 timestamp 连续, 可以更快的找到需要的 DeltaFile.
(3)HBase 通过 timestamp 值可以直接取出数据; 而 Kudu 实现多版本是通过保留 UNDO records(已经合并过的操作)和 REDO records(未合并过的操作)完成的, 在一些情况下 Kudu 需要将 base data 结合 UNDO records 进行回滚或者结合 REDO records 进行合并然后才能得到真正所需要的数据.
结合以上三点可以得出, 不管是 HBase 还是 Kudu, 在读取一条数据时都需要从多个文件中搜寻相关信息. 相对于 HBase,Kudu 选择将插入数据和更新操作分开, 一条数据只可能存在于一个 DiskRowset 或者 memRowset 中, 只需要搜寻到一个 rowset 中存在指定数据就不用继续往下找了, 用户不能设置更新和插入时的 timestamp 值, 减少了在 rowset 中 DeltaFile 的读取数量. 这样在 scan 的情况下可以结合列式存储的优点实现较高的读性能, 特别是在更新数量较少的情况下能够有效提高 scan 性能.
另外, 本文在描述 HBase 读写过程中没有考虑读写中使用的优化技术如 Bloomfilter,timestamp range 等. 其实 Kudu 中也有使用类似的优化技术来提高读写性能, 本文只是简单的分析, 因此就不再详细讨论读写过程. 如有需要了解 HBase 的详细读写过程, 可以参考范欣欣的 HBase - 数据写入流程解析 http://ks.netease.com/blog?id=3996 等一系列 HBase 相关文章.
其他差异
HBase: 使用的 java, 内存的释放通过 GC 来完成, 在内存比较紧张时可能引发 full GC 进而导致服务不稳定;
Kudu: 核心模块用的 C++ 来实现, 没有 full gc 的风险.
总结
本文主要简单介绍了一下 Kudu, 并在整体结构, 数据存储结构还有读写过程等方面上对 HBase 和 Kudu 这两款分布式存储系统进行大体上的比较. Kudu 通过要求完整的表结构设置, 主键的设定, 以列式存储作为数据在磁盘上的组织方式, 更新和数据分开等技巧, 使得 Kudu 能够实现像 HBase 一样实现数据的随机读写之外, 在 HBase 不太擅长的批量数据扫描 (scan) 具有较好的性能. 而批量读数据正是 olap 型应用所关注的重点, 正如 Kudu 官网主页上描述的, Kudu 实现的是既可以实现数据的快速插入与实时更新, 也可以实现数据的快速分析. Kudu 的定位不是取代 HBase, 而是以降低写的性能为代价, 提高了批量读的性能, 使其能够实现快速在线分析.
本文只是简单的分析一下 Kudu 的结构, 并与 HBase 比较, 若有什么不对的地方, 可以一起探讨交流.
本文来自网易云社区, 经作者闵涛授权发布
来源: https://www.cnblogs.com/163yun/p/9646008.html