Apache Druid 底层存储设计(列存储与全文检索)
导读:首先你将通过这篇文章了解到 Apache Druid 底层的数据存储方式。其次将知道为什么 Apache Druid 兼具数据仓库,全文检索和时间序列的特点。最后将学习到一种优雅的底层数据文件结构。
今日格言:优秀的软件,从模仿开始的原创。
了解过 Apache Druid 或之前看过本系列前期文章的同学应该都知道 Druid 兼具数据仓库,全文检索和时间序列的能力。那么为什么其可以具有这些能力,Druid 在实现这些能力时做了怎样的设计和努力?
Druid 的底层数据存储方式就是其可以实现这些能力的关键。本篇文章将为你详细讲解 Druid 底层文件 Segment 的组织方式。
带着问题阅读:
- Druid 的数据模型是怎样的?
- Druid 维度列的三种存储数据结构如何?各自的作用?
- Segment 文件标识组成部分?
- Segment 如何分片存储数据?
- Segment 新老版本数据怎么生效?
Segment 文件
Druid 将数据存储在 segment 文件中,segment 文件按时间分区。在基本配置中,将为每一个时间间隔创建一个 segment 文件,其中时间间隔可以通过granularitySpec
的segmentGranularity
参数配置。为了使 Druid 在繁重的查询负载下正常运行,segment 的文件大小应该在建议的 300mb-700mb 范围内。如果你的 segment 文件大于这个范围,那么可以考虑修改时间间隔粒度或是对数据分区,并调整partitionSpec
的targetPartitonSize
参数(这个参数的默认值是 500 万行)。
数据结构
下面将描述 segment 文件的内部数据结构,该结构本质上是列式的,每一列数据都放置在单独的数据结构中。通过分别存储每个列,Druid 可以通过仅扫描实际需要的那些列来减少查询延迟。
Druid 共有三种基本列类型:时间戳列,维度列和指标列,如下图所示:
timestamp
和metric
列很简单:在底层,它们都是由 LZ4 压缩的 interger 或 float 的数组。一旦查询知道需要选择的行,它就简单的解压缩这些行,取出相关的行,然后应用所需的聚合操作。与所有列一样,如果查询不需要某一列,则该列的数据会被跳过。
维度列
就有所不同,因为它们支持过滤和分组操作,所以每个维度都需要下列三种数据结构:
- 将值(始终被视为字符串)映射成整数 ID 的字典,
- 用 1 编码的列值列表,以及
- 对于列中每一个不同的值,用一个bitmap指示哪些行包含该值。
为什么需要这三种数据结构?字典
仅将字符串映射成整数 id,以便可以紧凑的表示 2 和 3 中的值。3 中的
bitmap
也称为反向索引,允许快速过滤操作(特别是,位图便于快速进行 AND 和 OR 操作)。最后,group by和TopN需要 2 中的值列表
,换句话说,仅基于过滤器汇总的查询无需查询存储在其中的维度值列表
。
为了具体了解这些数据结构,考虑上面示例中的“page”列,下图说明了表示该维度的三个数据结构。
1: 编码列值的字典
{
"Justin Bieber": 0,
"Ke$ha": 1
}
2: 列数据
[0,0,1,1]
3: Bitmaps - 每个列唯一值对应一个
value="Justin Bieber": [1,1,0,0]
value="Ke$ha": [0,0,1,1]
注意bitmap
和前两种数据结构不同:前两种在数据大小上呈线性增长(在最坏的情况下),而 bitmap 部分的大小则是数据大小和列基数的乘积。压缩将在这里为我们提供帮助,因为我们知道,对于“列数据”中的每一行,只有一个位图具有非零的条目。这意味着高基数列将具有极为稀疏的可压缩高度位图。Druid 使用特别适合位图的压缩算法来压缩 bitmap,如roaring bitmap compressing
(有兴趣的同学可以深入去了解一下)。
如果数据源使用多值列,则 segment 文件中的数据结构看起来会有所不同。假设在上面的示例中,第二行同时标记了“ Ke $ ha” 和 “ Justin Bieber”主题。在这种情况下,这三个数据结构现在看起来如下:
1: 编码列值的字段
{
"Justin Bieber": 0,
"Ke$ha": 1
}
2: 列数据
[0,
[0,1], <--Row value of multi-value column can have array of values
1,
1]
3: Bitmaps - one for each unique value
value="Justin Bieber": [1,1,0,0]
value="Ke$ha": [0,1,1,1]
^
|
|
Multi-value column has multiple non-zero entries
注意列数据和Ke$ha
位图中第二行的更改,如果一行的一个列有多个值,则其在“列数据“中的输入是一组值。此外,在”列数据“中具有 n 个值的行在位图中将具有 n 个非零值条目。
命名约定
segment 标识通常由数据源
,间隔开始时间
(ISO 8601 format),间隔结束时间
(ISO 8601 format)和版本号
构成。如果数据因为超出时间范围被分片,则 segment 标识符还将包含分区号
。如下:
segment identifier=datasource_intervalStart_intervalEnd_version_partitionNum
Segment 文件组成
在底层,一个 segment 由下面几个文件组成:
version.bin
4 个字节,以整数表示当前 segment 的版本。例如,对于 v9 segment,版本为 0x0, 0x0, 0x0, 0x9。meta.smoosh
存储关于其他 smooth 文件的元数据(文件名和偏移量)。XXXXX.smooth
这些文件中存储着一系列二进制数据。
这些
smoosh
文件代表一起被“ smooshed”的多个文件,分成多个文件可以减少必须打开的文件描述符的数量。它们的大小最大 2GB(以匹配 Java 中内存映射的 ByteBuffer 的限制)。这些smoosh
文件包含数据中每个列的单独文件,以及index.drd
带有有关该 segment 的额外元数据的文件。还有一个特殊的列,称为
__time
,是该 segment 的时间列。
在代码库中,segment 具有内部格式版本。当前的 segment 格式版本为v9
。
列格式
每列存储为两部分:
- Jackson 序列化的 ColumnDescriptor
- 该列的其余二进制文件
ColumnDescriptor 本质上是一个对象。它由一些有关该列的元数据组成(它是什么类型,它是否是多值的,等等),然后是可以反序列化其余二进制数的序列化/反序列化 list。
分片数据
分片
对于同一数据源,在相同的时间间隔内可能存在多个 segment。这些 segment 形成一个block
间隔。根据shardSpec
来配置分片数据,仅当block
完成时,Druid 查询才可能完成。也就是说,如果一个块由 3 个 segment 组成,例如:
sampleData_2011-01-01T02:00:00:00Z_2011-01-01T03:00:00:00Z_v1_0
sampleData_2011-01-01T02:00:00:00Z_2011-01-01T03:00:00:00Z_v1_1
sampleData_2011-01-01T02:00:00:00Z_2011-01-01T03:00:00:00Z_v1_2
在对时间间隔的查询2011-01-01T02:00:00:00Z_2011-01-01T03:00:00:00Z
完成之前,必须装入所有 3 个 segment。
该规则的例外是使用线性分片规范。线性分片规范不会强制“完整性”,即使分片未加载到系统中,查询也可以完成。例如,如果你的实时摄取创建了 3 个使用线性分片规范进行分片的 segment,并且系统中仅加载了两个 segment,则查询将仅返回这 2 个 segment 的结果。
模式变更
替换 segment
Druid 使用 datasource,interval,version 和 partition number 唯一地标识 segment。如果在一段时间内创建了多个 segment,则分区号仅在 segment ID 中可见。例如,如果你有一个一小时时间范围的 segment,但是一个小时内的数据量超过单个 segment 所能容纳的时间,则可以在同一小时内创建多个 segment。这些 segment 将共享相同的 datasource,interval 和 version,但 partition number 线性增加。
foo_2015-01-01/2015-01-02_v1_0
foo_2015-01-01/2015-01-02_v1_1
foo_2015-01-01/2015-01-02_v1_2
在上面的示例 segment 中,dataSource = foo,interval = 2015-01-01 / 2015-01-02,version = v1,partitionNum =0。如果在以后的某个时间点,你使用新的模式重新索引数据,新创建的 segment 将具有更高的版本 ID。
foo_2015-01-01/2015-01-02_v2_0
foo_2015-01-01/2015-01-02_v2_1
foo_2015-01-01/2015-01-02_v2_2
Druid 批量索引(基于 Hadoop 或基于 IndexTask 的索引)可确保每个间隔的原子更新。在我们的示例中,在将所有v2
segment2015-01-01/2015-01-02
都加载到 Druid 集群中之前,查询仅使用v1
segment。一旦v2
加载了所有 segment 并可以查询,所有查询将忽略v1
segment 并切换到这些v2
segment。之后不久,v1
segment 将被集群卸载。
请注意,跨越多个 segment 间隔的更新仅是每个间隔内具有原子性。在整个更新过程中,它们不是原子的。例如,当你具有以下 segment:
foo_2015-01-01/2015-01-02_v1_0
foo_2015-01-02/2015-01-03_v1_1
foo_2015-01-03/2015-01-04_v1_2
在v2
构建完并替换掉v1
segment 这段时间期内,v2
segment 将被加载进集群之中。因此在完全加载v2
segment 之前,群集中可能同时存在v1
和v2
segment。
foo_2015-01-01/2015-01-02_v1_0
foo_2015-01-02/2015-01-03_v2_1
foo_2015-01-03/2015-01-04_v1_2
在这种情况下,查询可能会同时出现v1
和和v2
segment。
segment 多个不同模式
同一数据源的 segment 可能具有不同的 schema。如果一个 segment 中存在一个字符串列(维),但另一个 segment 中不存在,则涉及这两个 segment 的查询仍然有效。缺少维的 segment 查询将表现得好像维只有空值。同样,如果一个 segment 包含一个数字列(指标),而另一部分则没有,则对缺少该指标的 segment 的查询通常会“做正确的事”。缺少该指标的聚合的行为就好像该指标缺失。
最后
一、文章开头的问题,你是否已经有答案
- Druid 的数据模型是怎样的?(时间戳列,维度列和指标列)
- Druid 维度列的三种存储数据结构如何?各自的作用?(编码映射表、列值列表、Bitmap)
- Segment 文件标识组成部分?(datasource,interval,version 和 partition numbe)
- Segment 如何分片存储数据?
- Segment 新老版本数据怎么生效?
二、知识扩展
- 什么是列存储?列存储和行存储的区别是什么?
- 你了解 Bitmap 数据结构吗?
- 深入了解
roaring bitmap compressing
压缩算法。 - Druid 是如何定位到一条数据的?详细流程是怎样的?
系列推荐
Mysql:小主键,大问题
Mysql大数据量问题与解决
你应该知道一些其他存储——列式存储
时间序列数据库(TSDB)初识与选择(InfluxDB、OpenTSDB、Druid、Elasticsearch对比)
十分钟了解Apache Druid(集数据仓库、时间序列、全文检索于一体的存储方案)
Apache Druid 底层存储设计(列存储与全文检索)
Apache Druid 的集群设计与工作流程
*请持续关注,后期将为你拓展更多知识。对 Druid 感兴趣的同学也可以回顾我之前的系列文章。
关注公众号 MageByte,设置星标点「在看」是我们创造好文的动力。后台回复 “加群” 进入技术交流群获更多技术成长。
Apache Druid 底层存储设计(列存储与全文检索)的更多相关文章
- hive行存储与列存储
首先判断hive表是行存储还是列存储 判断方法: 1.使用hiveSQL"show create table table_name",这种方式,可以查看建表时候指定的那种方式; 2 ...
- Apache Druid 的集群设计与工作流程
导读:本文将描述 Apache Druid 的基本集群架构,说明架构中各进程的作用.并从数据写入和数据查询两个角度来说明 Druid 架构的工作流程. 关注公众号 MageByte,设置星标点「在看」 ...
- SQL Server 列存储索引强化
SQL Server 列存储索引强化 SQL Server 列存储索引强化 1. 概述 2.背景 2.1 索引存储 2.2 缓存和I/O 2.3 Batch处理方式 3 聚集索引 3.1 提高索引创建 ...
- Oracle 12.1.0.2 New Feature翻译学习【In-Memory column store内存列存储】【原创】
翻译没有追求信达雅,不是为了学英语翻译,是为了快速了解新特性,如有语义理解错误可以指正.欢迎加微信12735770或QQ12735770探讨oracle技术问题:) In-Memory Column ...
- SQL Server 2016新特性:列存储索引新特性
SQL Server 2016新特性:列存储索引新特性 行存储表可以有一个可更新的列存储索引,之前非聚集的列存储索引是只读的. 非聚集的列存储索引支持筛选条件. 在内存优化表中可以有一个列存储索引,可 ...
- SQL Server 2014 聚集列存储
SQL Server 自2012以来引入了列存储的概念,至今2016对列存储的支持已经是非常友好了.由于我这边线上环境主要是2014,所以本文是以2014为基础的SQL Server 的列存储的介绍. ...
- SQL Server 列存储索引概述
第一次接触ColumnStore是在2017年,数据库环境是SQL Server 2012,Microsoft开始在SQL Server 2012中推广列存储索引,到现在的SQL Server 201 ...
- Druid.io索引过程分析——时间窗,列存储,LSM树,充分利用内存,concise压缩
Druid底层不保存原始数据,而是借鉴了Apache Lucene.Apache Solr以及ElasticSearch等检索引擎的基本做法,对数据按列建立索引,最终转化为Segment,用于存储.查 ...
- Druid(准)实时分析统计数据库——列存储+高效压缩
Druid是一个开源的.分布式的.列存储系统,特别适用于大数据上的(准)实时分析统计.且具有较好的稳定性(Highly Available). 其相对比较轻量级,文档非常完善,也比较容易上手. Dru ...
随机推荐
- Kubelet
Kubelet 相关博客 Kubelet组件深度解析 Kubelet组件解析 Kubelet运行机制分析 Kubelet与apiserver通信 ___ Kubelet组件运行在Node节点上,维持运 ...
- 3D打印如何重组制造格局?
全球化的竞争正变得毫无底线,国与国之间只有利益,没有同情,也就是说美国品牌想把自己的工厂移回本土,是不会考虑中国工人的生存现状的,更不会顾及这里的GDP和环境问题,甚至还会依靠经济能力去奴役其他国家 ...
- C++扬帆远航——17(递归函数求阶乘)
/* * Copyright (c) 2016,烟台大学计算机与控制工程学院 * All rights reserved. * 文件名:阶乘.cpp * 作者:常轩 * 微信公众号:Worldhell ...
- C++走向远洋——41(深复制体验,3,)
*/ * Copyright (c) 2016,烟台大学计算机与控制工程学院 * All rights reserved. * 文件名:text.cpp * 作者:常轩 * 微信公众号:Worldhe ...
- 达拉草201771010105《面向对象程序设计(java)》第十三周学习总结
达拉草201771010105<面向对象程序设计(java)>第十三周学习总结 第一部分:理论知识 事件处理基础: 事件源:能够产生事件的对象都可 以成为事件源,如文本框.按钮等.一个事件 ...
- Ubuntu 16.04 apt 国内源
一.推荐几个 Ubuntu 16.04 国内的 apt 源 1. 阿里源 # deb cdrom:[Ubuntu 16.04 LTS _Xenial Xerus_ - Release amd64 (2 ...
- 石油测井专题(六)MCM工艺在LWD的应用
在上一篇的MCM工艺我们提到过石英挠性加速度计的伺服电路采用此工艺可以有效提高仪器产品的稳定性和寿命. MCM相对于印制电路板(PCB)来讲,MCM技术采用了更短的连接长度和更紧密的器件布局,从而降低 ...
- ubuntu 安装flask+nginx+gunicorn 待定
第一步 先检查服务器环境 pip python3 mysql redis 能下就下,该升级就升级 第二步 如果你的flask程序在github上 请使用git clone 地址 下载下来(如果是私 ...
- JavaMail(二):利用JavaMail发送复杂邮件
上一篇文章我们学习了利用JavaMail发送简单邮件,这篇文章我们利用JavaMail发送稍微复杂一点的邮件(包含文本.图片.附件).这里只贴出核心代码,其余代码可参考JavaMail(一):利用Ja ...
- django 用户认证 user对象
django中的用户模型 内部带有很多的属性方法,我们可以直接使用 1 is_staff Boolean.决定用户是否可以访问admin管理界面.默认False. 2 is_active Boolea ...