Apache Druid分析型数据库设计-存储设计

Apache Druid系列博客


官方英文原文:Storage design

数据源(datasource)和段(segment)

Druid数据存储在数据源中,这和传统关系数据库管理系统的表类似。每个数据源都按事件分区,也能可选地按其它属性进一步分区。每个时间范围叫做chunk(比如,如果数据源按天分区,则一个chunk为一天)。在chunk中,数据被分区成一个或多个 ,每个段都是一个文件,通常包含多达几百万行数据。由于段被组织成时间chunk,因此有时可以将段考虑为在如下图形式的时间线上:

时间chunk时间线上的段

一个数据源可能只有几个段,也可能有数十万甚至数百万个段。每个段都由MiddleManager创建为可变(mutable)且未提交(uncommitted)的。数据一旦被添加到未提交的段,就可以被查询。段构建进程通过生成压缩且经索引的数据文件来加速以后的查询:

  • 转换为列式存储
  • 使用位图索引进行索引
  • 压缩
    • 字符串列的id存储最小化(id storage minimization)的字典编码
    • 位图索引(bitmap index)的位图压缩
    • 所有列的类型感知(Type-aware)压缩

段会被定期提交(commit)并发布(publish)到深度存储,变为不可变的(immutable),并且从MiddleManager移动到Historical进程。段的条目也会被写入元数据存储,其是关于段的元数据的自描述位,包括段的模式、大小和在深度存储中的位置等内容。这些条目告诉Coordinator集群上有哪些数据是可用的。

有关段文件格式的详细信息,请参见 段文件

有关在 Druid 中建模数据的详细信息,请参阅 模式设计

Indexing和handoff

Indexing是创建新段的机制,而handoff是发布新段并使其开始由Historical提供服务的机制。从indexing方来看:

  1. Indexing任务开始运行并构建新段。Indexing必须在构建段之前决定段的标识符(identifier)。对于追加的任务(如Kafka任务或追加模式的索引任务),这是通过调用Overlord上的“allocate” API来潜在地向现有段集添加新分区来完成的。对于覆盖的任务(如Hadoop任务或追加模式的索引任务),这是通过锁定间隔并创建新版本号和新段集来完成的。
  2. 如果indexing任务是实时任务(如Kafka任务),则此时可以立刻查询该段。它是可用的,但未发布。
  3. 当indexing任务完成段的数据读取后,它会将其推送到深度存储,然后通过将记录写入元数据存储来发布它。
  4. 如果indexing任务是实时任务,那么为了确保数据连续可用于查询,它会等待Historical进程加载这个段;如果indexing任务不是实时任务,它会立即退出。

从Coordinator/Historical方来看:

  1. Coordinator定期(默认情况下为1分钟)轮询元数据存储以查找新发布的段。
  2. 当Coordinator发现已发布和使用(used)但不可用的段,它会选择一个Historical进程来加载该段并指示Historical这样做。
  3. Historical加载该段并开始提供服务。
  4. 此时,如果indexing任务在等待handoff,它就会退出。

段标识符(segment identifier)

段都有一个由四部分组成的标识符,包含以下组件:

  • 数据源名称
  • 时间间隔(对于包含段的time chunk;和摄取时指定的segmentGranularity有关)
  • 版本号(通常是与段集首次启动的时间对应的ISO8601时间戳)
  • 分区号(一个整数,在数据源+间隔+版本中是唯一的;不一定是连续的)

例如,段所在的数据源为clarity-cloud0,time chunk为2018-05-21T16:00:00.000Z/2018-05-21T17:00:00.000Z,版本为2018-05-21T15:56:09.909Z,分区号为1,则它的标识符为:

clarity-cloud0_2018-05-21T16:00:00.000Z_2018-05-21T17:00:00.000Z_2018-05-21T15:56:09.909Z_1

分区为0(chunk的第一个分区)的段省略分区号不写,如下例所示,该段和前例在相同的time chunk,但分区号为0而不是1:

clarity-cloud0_2018-05-21T16:00:00.000Z_2018-05-21T17:00:00.000Z_2018-05-21T15:56:09.909Z

段版本控制(segment versioning)

版本号提供了一种多版本并发控制(multi-version concurrency control, MVCC)的形式来支持批模式(batch-mode)的覆盖。如果之前所做的只是附加数据,那么每个time chunk将只有一个版本;但是当覆盖了数据,Druid将无缝地从查询旧版本切换到查询新的更新版本。具体来说,相同数据源、相同时间间隔但更高版本号的新段集被创建。这是向Druid系统的其余部分发出的信号,表明旧的版本应该从集群中删除,由新版本取代。

这种切换对用户来说似乎是瞬时发生的。这是因为,Druid首先加载新数据(但不允许其被查询),然后,一旦新数据全部加载完毕,就将所有新查询切换到使用那些新段,然后在几分钟过后丢弃旧的段。

段生命周期

每个段都有一个生命周期,包含以下三个主要部分:

  1. 元数据存储:段元数据(是一个的小JSON负载,通常不超过几KB)在段构建完成后存放在元数据存储中。将段的记录插入元数据存储的动作称为发布(publishing)。这些元数据记录有一个名为used的布尔标志,它控制段是否可被查询。由实时任务创建的段将在发布之前就是可用的,因为它们仅在段完成时才发布,并且不会接受任何额外行的数据。
  2. 深度存储:段数据文件在段构建完成后被推送到深度存储,这发生在将元数据发布到元数据存储之前。
  3. 查询可用:段可用于在一些Druid数据服务器上查询,如实时任务或Historical进程。

您可以用Druid SQL的sys.segments检查目前活动段的状态,包括以下标志:

  • is_published:如果段元数据已发布到元数据存储且used为true,则为true。
  • is_available:如果段当前可用于实时任务或Historical进程查询,则为true。
  • is_realtime:如果段仅对实时任务可用,则为true。对于使用实时摄取的数据源,这通常会开始为true,然后由于段被发布和handoff而变成false。
  • is_overshadowed:如果段已发布(used设为true)并且被其他一些已发布的段完全遮盖则为true。通常这是一种瞬态,处于这种状态的段很快就会将其used标志自动设置为 false。

可用性和一致性

Druid的摄取和查询之间在架构上是分离的,如上文indexing和handoff中所述。这意味着要理解Druid的可用性和一致性属性,就必须分开看待每个功能。

从摄取一方来看,Druid的主要摄取方法都是基于拉取(pull)的,并提供事务保证。这意味着可以保证使用这些方法的摄取将以全有或全无的方式发布:

  • 受监督的(supervised)“可定位流(seekable-stream)”摄取方法,如KafkaKinesis。使用这些方法,Druid在同一事务中将流偏移与段元数据一同提交到其元数据存储。需要注意的是,如果摄取任务失败,可以回滚尚未发布的数据摄取。在这种情况下,部分摄取的数据会被丢弃,然后Druid将从最后提交的一组流偏移中恢复摄取,这确保了提交行为有且只有一次。
  • 基于Hadoop的批摄取(batch ingestion)。每个任务在单个事务中发布所有段元数据。
  • 本地批摄取。在并行模式下,监督任务在子任务完成后,在单个事务中发布所有段元数据;在简单(单任务)模式下,单任务完成后在单个事务中发布所有段元数据。

另外,一些摄取方法提供幂等性(idempotency)保证,就是说重复执行相同的摄取不会导致摄取重复的数据:

  • 受监督的“可搜索流(seekable-stream)”摄取方法,如KafkaKinesis是幂等的,这是由于流偏移和段元数据一起存储且以锁步(lock-step)方式更新。
  • 基于Hadoop的批摄取是幂等的,除非输入源之一与要摄取到的Druid数据源是同一个。在这种情况下,运行相同的任务两次是非幂等的,因为这是在添加(adding)现有数据而不是覆盖(overwriting)它。
  • 本地批摄取是幂等的,除非appendToExisting为true,或者输入源之一与要摄取到的Druid数据源是同一个。在这两种情况下,运行相同的任务两次是非幂等的,因为这是在添加现有数据而不是覆盖它。

从查询一方来看,Druid的Broker负责确保段集在给定的查询中是一致的。它会根据当前可用的内容选择合适的段集版本,以便在查询开始时使用。这由原子替换(atomic replacement)提供支持,该特性可确保从用户的角度来看,查询会立即从旧版本的数据切换(flip)到较新的数据集合,而不会影响一致性或性能。(参阅上文 段版本控制)这用于基于Hadoop的批摄取、appendToExisting为false时的本地批摄取和压缩。

需要注意的是,原子替换对于每个time chunk独立发生。如果批处理任务或压缩涉及到多个time chunk,那么每个time chunk都会在任务结束后不久进行原子替换,但是替换不会同时发生。

通常,Druid中的原子替换基于与段版本一起工作的核心集(core set)的概念。当时间chunk被覆盖时,将创建具有更高版本号的新核心段集。核心集必须全部可用,然后Broker才会用它们来代替旧集。每个时间chunk的每个版本也只能有一个核心集。Druid也将在每个时间chunk一次只使用一个版本。这些属性共同提供了Druid的原子替换保证。

Druid还支持一种实验性的段锁定(segment locking)模式,通过在摄取任务的上下文中将forceTimeChunkLock设置为false来激活。在这种情况下,Druid 使用现有版本为time chunk创建一个原子更新组(atomic update group),而不是创建一个具有新版本号的新核心集。每个time chunk可以有多个具有相同版本号的原子更新组,每一个都用相同的版本号替换同一time chunk中特定的早期段集。Druid将查询最新的完全可用的一个。这是更强大版本的核心集概念,因为它支持原子替换一个time chunk的数据的子集,以及同时进行原子替换和追加。

如果段由于多个Historical同时掉线(超出复制因子(replication factor))而变得不可用,则Druid查询将仅包含仍然可用的段。在后台,Druid将尽可能快地将这些不可用的段重新加载到其他Historical上,此时它们将再次包含在查询中。


Apache Druid系列博客

猜你喜欢

转载自blog.csdn.net/u010393510/article/details/128121817