上一篇文章中,笔者简单介绍了一下分布式数据库 Pinot 的核心组件,本文主要针对其中的存储模型会做部分讲解。
如果你对读写磁盘有不错的基础的话,看起来会更轻松一些,如果没有也没关系,我会简单讲解一下这么设计的好处,会涉及一些八股,实在看不懂的可以留言,笔者知道的话会给你们的评论做解答。
要先理解概念就得先看一下图,在脑海里面多增加印象。那么还是老样子,直接偷一波官方的图。
从上图,最直观的感受就是 N 个 Segment 组成一个 Table,N 个 Table 在 Tenant 下,诺干 Table 组成一个大的 Cluster。其中 Segment 是根据时间做的分区,Table 就是逻辑表,Isolate 是根据租户做隔离。
直观着看其实就是这个逻辑,但是为什么要有 Segment,租户是用来干什么的。那这块我们就先要结合 Pinot 的设计理念来探讨。
Pinot 的几个标准如下,这个是他们官方网站里面写的。
针对上面的标准,我简单拆分为控制和性能
要做到上述的标准需要借助一些开源工具,高可用,水平可扩张,动态配置更改,这几块基本都是需要一个一致性比较高的注册中心。Pinot 目前使用的就是 apache Helix 来作为解决方案。
那么我们可以初步把存储模型设计的核心归到延迟与存储和不可变数据。其实脱离了场景谈优化没有探讨意义,所以在笔者的观点来看,Pinot 的核心点还是围绕时间为基础,界定场景是一些更时间关系更大的相关分析场景。那么我们定一个场景就是基于查询最近某个周期范围内,我们拆分成这么多 Segment 会带来速度的提升么?
先不回答这个问题,先做一些简单的 IO 复习。其实要做到读的快,写的快不外乎两点,介质和并发。 一个线程按照 page 对其的概念,你按照每个 block 是 4KB 去读数据,那么也不会打满整个介质的 IO。用简单的公式来表达就是: 吞吐量 = 线程 * IO速度
大家知道不同介质下面他们的读写速度是不同的,我从网上找了数据资料,先贴到下面的表格里面。
介质 | 顺序读 | 顺序写 | 随机读 | 随机写 |
---|---|---|---|---|
机械硬盘 | 84MB/s | 79MB/s | 1MB/s | 1MB/s |
固态硬盘 | 220.7MB/s | 77.2MB/s | 24.654MB/s | 68.910MB/s |
从表中我们大概可以看出来,顺读写在不同介质下的查询不大,但是读的性能差距比较明显。
简单讲就是开 N 个并发去读文件数据,但是并发读一个文件的话,其实还是会有些许性能损耗,比如文件锁。所以最好说同时读 N 个文件来打满磁盘的IO,这样冲突会更少。
上面两个点转换到读取 Segment 上,那么就是说如果一个 Table 是有 10 个 Segment 组成,那么通过 10 个线程并发去读,每个线程负责一个 Segment ,这样就可以更快的读取所有的数据。从以上知识点,我们就可以比较直观的知道拆分这么多 Segment 的简单好处。但是拆分多个 Segment 有个问题就是如果 Segment 数过多,你的查询又需要查很多 Segment,这种场景则又会出现查询放大的问题,这种时候就又导致查询变慢,因为会影响到整个集群的吞吐。这种只能根据自己的场景去做测试来确定最佳数值。
其实 Segment 归为实时表的 Segment 和 离线表的 Segment,数据可以通过某种方法生成一个 Segment 并且传到 Deep Storage 中,Server 会拉取数据存到本地的机器中。从这种流程,我们就可以知道,如果要修改某个 Segment 里面的数据就得重新替换这个 Segment,所以更改数据在 Pinot 这个数据库中非常麻烦,但是也不是没办法解决这个问题。
消费完的
消费ING
从上面可以看出来,根据配置持久化的 Segment 最终会存储到 Deep Storage 中,我们用的是 hdfs,所以是 hdfs 开头的逻辑。
实时消费 MQ 生成数据到 Segment 也有一套机制来保证数据被 exactly once。就算消费部分数据失败或者重启也会有相应的保障。这块逻辑这边就不细讲了,粗讲就是自己控制了 offset,如果消费成功并且落盘才会持久化到 zk 上,这样下次重启的时候就知道消费到哪了,而且文件和文件也有 CRC 的校验,CRC 也会存一份到注册中心,主要是减少读取文件造成的不必要损耗,直接用空间换时间来达到校验的目的,这个设计还是挺好的。很多数据库的比赛都可以借鉴这个机制。
Table,Tenant 就暂时不在这讲解了,放到下一章,因为设计到的内容还是比较多,有实时表,离线表,混合表,索引这块,基本是围绕 Table 来做的。