4. 存储结构
Druid中的数据表(称为数据源)是时间戳事件的集合,并分割为一组segments,其中每个段通常为5-10万行。正式地,我们将段定义为跨越某个时间段的数据行的集合。段表示Druid中的基本存储单元,复制和分发都是在段级别完成的。
Druid总是需要一个时间戳列,用来简化数据分发策略,数据保留策略和第一级查询修剪。Druid数据源划分成定义良好的时间间隔(通常为一小时或一天),并且可以进一步对来自其他列的值进行分区,以实现所需的段大小。分割段的时间粒度是数据量和时间范围的函数。如果数据集中的时间戳遍布在一年里,则按天进行分区。如果数据集中的时间戳遍布在一天里,则按小时进行分区。
段由数据源标识符进行唯一标识,标识符包括数据的时间间隔以及新段被创建时增加的版本字符串。版本字符串可以识别出段数据的新鲜度;新版本的段具有较新的数据视图(在一些时间范围内)。该段元数据由系统用于并发控制; 读操作总是从具有该时间范围的最新版本标识符的段中访问特定时间范围内的数据。
Druid段用列式存储。鉴于Druid最适合用于事件流的聚合计算(所有进入Druid的数据必须有一个时间戳),所以将聚合信息存储为列而不是行的优势已有详细记录[1]。 列存储允许更高效的利用CPU,因为只有实际需要的才会被加载和扫描。在面向行的数据存储器系统中,与行相关联的所有列必须作为聚合的一部分进行扫描。额外的扫描时间可以引入明显的性能退化[1]。
Druid有多种列类型来表示各种数据格式。根据列类型不同,使用不同的压缩方法来降低在内存和磁盘上存储列的成本。在表1中给出的示例中,page、user、gender和city列仅包含字符串。直接存储字符串需要不必要的代价,可以用字典编码来代替。字典编码是压缩数据的常用方法,并已用于其他数据存储系统,如PowerDrill [17]。在表1的示例中,我们可以将每个page映射到唯一的整数标识符。
Justin Bieber -> 0
Ke$ha -> 1
此映射允许我们将page列表示为整数数组,其中数组索引对应于原始数据集的行。 对于page列,我们可以表示为:[0, 0, 1, 1]
生成的整数数组本身非常适合进行压缩。在编码之上的通用压缩算法在列存储中非常常见。Druid使用LZF [24]压缩算法。
类似的压缩方法可以应用于数字列。例如,表1中characters added和characters removed列也可以表示为单个数组:
Characters Added -> [1800, 2912, 1953, 3194]
Characters Removed -> [25, 42, 17, 170]
在这种情况下,我们压缩原始值,而不是它们的字典表示。
4.1 数据过滤索引
在许多现实世界的OLAP工作流中,针对满足某些维度条件的某些度量集合的聚合结果发出查询。比如一个查询示例是:“旧金山的男性用户进行了多少次维基百科编辑?”此查询基于维度值的布尔表达式(city=='San Francisco' and gender='Male')过滤表1中的维基百科数据集。在许多实际数据集中,维度列包含字符串,度量列包含数值。Druid为字符串列创建额外的查找索引,以便只扫描属于特定查询过滤器的那些行。
让我们考虑表1中的page列。对于表1中的每个唯一页面,可以使用一些标记来指明哪些行可以看到特定页面。我们可以将此信息存储在二进制数组中,其中数组索引表示我们的行。 如果在特定行中看到特定页面,则该数组索引被标记为1.例如
Justin Bieber -> rows [0, 1] -> [1][1][0][0]
Ke$ha -> rows [2, 3] -> [0][0][1][1]
Justin Bieber在行0和1中可以看到。列值到行索引的映射形成了一个倒排索引[39]。 要知道哪些行包含Justin Bieber或Ke$ha,我们可以对这两个数组进行OR运算。
[0][1][0][1] OR [1][0][1][0] = [1][1][1][1]
这种对大型bitmap数据集执行布尔运算的方法通常用于搜索引擎中。OLAP负载的位图索引在[32]中有详细描述。位图压缩算法是一个明确的研究领域[2,44,42],并且经常利用游标编码算法。Druid选择使用Concise算法[10]。 图7说明了使用整数数组进行Concise压缩的字节数。结果是在一个cc2.8xlarge系统生成的,其中使用了单线程,2G堆,512m年轻带,和每个运行之间的强制GC。数据集是从Twitter garden hose[41]数据流收集的一天的数据。 数据集包含2,272,295行和12个不同基数的维度。作为一个额外的比较,我们也对数据集行排序以做到最大化压缩。
在未排序的情况下,Concise压缩后大小为53,451,144字节,总整数数组大小为127,248,520字节。 总的来说,Concise压缩集比整数数组小42%。在排序的情况下,总Concise压缩大小为43,832,884字节,总的整数数组大小为127,248,520字节。 有趣的是,在排序后,全局压缩只增加了很小一部分。
4.2 存储引擎
Druid的持久化组件允许不同的存储引擎以插件的方式接入,类似于Dynamo。这些存储引擎可以将数据存储在一个完全的in-memory结构的引擎中,例如JVM heap,或者是存储于 memory-mapped 结构的存储中。Druid中存储引擎可配置更换的这个能力依赖于一个特定的应用规范。一个in-memory的存储引擎要比memory-mapped存储引擎的成本昂贵得多,但是如果对于性能特别敏感的话,in-memory存储引擎则是更好的选择。默认情况下使用的是memory-mapped存储引擎。
当使用一个memory-mapped存储引擎的时候,Druid依赖于操作系统来对segment在内存中进行换入和换出操作。因为只有当segment加载到内存中了才可以被查询,所以memory-mapped存储引擎允许将最近的segment保留在内存中,而那些不会再被查询的segment则被换出。使用memory-mapped的主要缺点是当一个查询需要更多的segment并且已经超出了节点的内存容量时,在这种情况下,查询性能将会因为不断的在在内存中进行segment的换入和换出而下降。