Druid Kafka 数据源消费到 Segment 生成全链路深度分析
一、概述
本文档基于 Apache Druid 源码,系统梳理了 Kafka 数据源从消费到生成 Segment 的完整流程。整个流程涉及 5 个层级,从 Supervisor 管理层到底层存储层,形成完整的数据处理链路。
核心类总览
| 层级 | 核心类 | 职责 |
|---|
| Supervisor 管理层 | KafkaSupervisor / SeekableStreamSupervisor | 管理 Task 生命周期,分配分区 |
| Task 执行层 | KafkaIndexTask / SeekableStreamIndexTask | 创建 Runner、RecordSupplier、Appenderator |
| Runner 消费层 | SeekableStreamIndexTaskRunner / IncrementalPublishingKafkaIndexTaskRunner | 消费主循环、增量发布 |
| 消费者层 | KafkaRecordSupplier | 封装 KafkaConsumer,提供 poll/seek/assign |
| Driver 层 | StreamAppenderatorDriver / BaseAppenderatorDriver | 管理 Segment 分配/发布/Handoff |
| Appenderator 层 | StreamAppenderator | add/persistAll/push/mergeAndPush |
| Segment 生成层 | IndexMergerV9 | V9 格式 Segment 合并/持久化 |
二、整体架构流程图
用户提交 Supervisor Spec
│
▼
KafkaSupervisor 启动
│
▼
定时 runInternal 循环
│
├── updatePartitionDataFromStream() → 获取 Kafka Topic 分区信息
├── discoverTasks() → 发现已有任务
├── createNewTasks() → 创建新任务
│ │
│ ▼
│ createTasksForGroup() → KafkaIndexTask 提交到 TaskQueue
│
▼
KafkaIndexTask.run()
│
▼
IncrementalPublishingKafkaIndexTaskRunner.runInternal()
│
├── 创建 KafkaRecordSupplier (封装 KafkaConsumer)
├── 创建 StreamAppenderator
├── 创建 StreamAppenderatorDriver
├── driver.startJob() 恢复元数据
│
▼
主消费循环 (while stillReading)
│
├── recordSupplier.poll() → 从 Kafka 拉取消息
├── StreamChunkParser.parse() → 解析为 InputRow
├── driver.add() → 写入 Appenderator
│ │
│ ├── Sink.add() → IncrementalIndex.add()
│ │
│ ├── 触发持久化? → driver.persistAsync()
│ │ → IndexMergerV9.persist()
│ │
│ └── 达到 maxRowsPerSegment? → checkpoint + 创建新 Segment
│
├── maybePersistAndPublishSequences() → 增量发布已完成的 Sequence
│
▼
消费循环结束
│
├── driver.persist() 最终持久化
│
├── publishAndRegisterHandoff()
│ │
│ ├── driver.publish()
│ │ ├── pushInBackground() → mergeAndPush() → Deep Storage
│ │ └── publishInBackground() → 发布元数据到 MetaStore
│ │
│ └── registerHandoff() → 等待 Historical 加载
│
▼
TaskStatus.SUCCESS
三、第一层:KafkaSupervisor 管理层
3.1 核心类与文件
| 类 | 文件路径 |
|---|
KafkaSupervisor | extensions-core/kafka-indexing-service/src/main/java/org/apache/druid/indexing/kafka/supervisor/KafkaSupervisor.java |
SeekableStreamSupervisor | indexing-service/src/main/java/org/apache/druid/indexing/seekablestream/supervisor/SeekableStreamSupervisor.java |
3.2 流程说明
- 启动:用户通过 REST API 提交
KafkaSupervisorSpec,Overlord 创建 KafkaSupervisor 实例并调用 start()
- 定时调度:
start() 中注册定时任务 buildRunTask(),按 ioConfig.period 周期性执行 runInternal()
runInternal() 核心循环:
updatePartitionDataFromStream() → 获取 Kafka Topic 的分区列表
discoverTasks() → 发现已运行的 KafkaIndexTask
updateTaskStatus() → 更新任务状态
checkTaskDuration() → 检查任务是否超过 taskDuration
checkPendingCompletionTasks() → 检查待完成的任务
checkCurrentTaskState() → 检查当前任务状态
createNewTasks() → 创建缺失的任务
-
创建任务:createTasksForGroup() → KafkaSupervisor.createIndexTasks()
- 根据
replicas 配置创建多个 KafkaIndexTask 实例
- 每个 Task 包含
DataSchema、TuningConfig、IOConfig(含起止 offset)
- 通过
taskQueue.add(indexTask) 提交到 Overlord 的任务队列
-
分区分配策略:getTaskGroupIdForPartition(partitionId) = partitionId % taskCount
- 每个 TaskGroup 负责一组 Kafka 分区
- 每个 TaskGroup 有
replicas 个副本任务
四、第二层:KafkaIndexTask 执行层
4.1 核心类与文件
| 类 | 文件路径 |
|---|
KafkaIndexTask | extensions-core/kafka-indexing-service/src/main/java/org/apache/druid/indexing/kafka/KafkaIndexTask.java |
SeekableStreamIndexTask | indexing-service/src/main/java/org/apache/druid/indexing/seekablestream/SeekableStreamIndexTask.java |
IncrementalPublishingKafkaIndexTaskRunner | extensions-core/kafka-indexing-service/src/main/java/org/apache/druid/indexing/kafka/IncrementalPublishingKafkaIndexTaskRunner.java |
KafkaRecordSupplier | extensions-core/kafka-indexing-service/src/main/java/org/apache/druid/indexing/kafka/KafkaRecordSupplier.java |
4.2 调用链
KafkaIndexTask.run(toolbox)
→ getRunner().run(toolbox)
→ IncrementalPublishingKafkaIndexTaskRunner.run(toolbox)
→ runInternal(toolbox)
4.3 runInternal() 初始化阶段
RecordSupplier recordSupplier = task.newTaskRecordSupplier();
appenderator = task.newAppenderator(toolbox, metrics, rowIngestionMeters, parseExceptionHandler);
driver = task.newDriver(appenderator, toolbox, metrics);
Object restoredMetadata = driver.startJob(lockHelper);
4.4 关键组件创建关系
KafkaIndexTask
├── KafkaRecordSupplier (封装 KafkaConsumer)
├── StreamAppenderator (管理 Sink/FireHydrant/IncrementalIndex)
└── StreamAppenderatorDriver (管理 Segment 分配/发布/Handoff)
├── ActionBasedSegmentAllocator (通过 Overlord 分配 SegmentId)
├── SegmentHandoffNotifier (监听 Historical 加载完成)
└── ActionBasedUsedSegmentChecker (检查已使用的 Segment)
五、第三层:消费循环层
5.1 核心代码位置
SeekableStreamIndexTaskRunner.java 的 runInternal() 方法
5.2 主循环流程
while (stillReading) {
if (possiblyPause()) { ... }
if (stopRequested.get()) break;
maybePersistAndPublishSequences(committerSupplier);
List<OrderedPartitionableRecord> records = getRecords(recordSupplier, toolbox);
for (OrderedPartitionableRecord record : records) {
boolean shouldProcess = verifyRecordInRange(
record.getPartitionId(), record.getSequenceNumber()
);
if (shouldProcess) {
List<InputRow> rows = parser.parse(record.getData(), ...);
SequenceMetadata sequenceToUse = sequences.stream()
.filter(seq -> seq.canHandle(this, record))
.findFirst().orElse(null);
for (InputRow row : rows) {
AppenderatorDriverAddResult addResult = driver.add(
row, sequenceToUse.getSequenceName(), committerSupplier,
true,
false
);
boolean isPushRequired = addResult.isPushRequired(
maxRowsPerSegment, maxTotalRows
);
if (isPushRequired && !sequenceToUse.isCheckpointed()) {
sequenceToCheckpoint = sequenceToUse;
}
isPersistRequired |= addResult.isPersistRequired();
}
if (isPersistRequired) {
driver.persistAsync(committerSupplier.get());
}
lastReadOffsets.put(record.getPartitionId(), record.getSequenceNumber());
currOffsets.put(record.getPartitionId(), record.getSequenceNumber() + 1);
}
if (!moreToReadAfterThisRecord) {
assignment.remove(record.getStreamPartition());
stillReading = !assignment.isEmpty();
}
}
if (System.currentTimeMillis() > nextCheckpointTime) {
sequenceToCheckpoint = getLastSequenceMetadata();
}
if (sequenceToCheckpoint != null && stillReading) {
CheckPointDataSourceMetadataAction checkpointAction = ...;
toolbox.getTaskActionClient().submit(checkpointAction);
}
}
5.3 maybePersistAndPublishSequences() — 增量发布
private void maybePersistAndPublishSequences(Supplier<Committer> committerSupplier) {
for (SequenceMetadata sequenceMetadata : sequences) {
sequenceMetadata.updateAssignments(currOffsets, this::isMoreToReadBeforeReadingRecord);
if (!sequenceMetadata.isOpen()
&& !publishingSequences.contains(sequenceMetadata.getSequenceName())) {
publishingSequences.add(sequenceMetadata.getSequenceName());
driver.persist(committerSupplier.get());
publishAndRegisterHandoff(sequenceMetadata);
}
}
}
这是 增量发布(Incremental Publishing) 的关键:在消费过程中,已完成的 Sequence 可以提前发布,不需要等到整个 Task 结束。
六、第四层:发布与 Handoff 层
6.1 核心类与文件
| 类 | 文件路径 |
|---|
StreamAppenderatorDriver | server/src/main/java/org/apache/druid/segment/realtime/appenderator/StreamAppenderatorDriver.java |
BaseAppenderatorDriver | server/src/main/java/org/apache/druid/segment/realtime/appenderator/BaseAppenderatorDriver.java |
6.2 publishAndRegisterHandoff() 流程
protected void publishAndRegisterHandoff(SequenceMetadata sequenceMetadata) {
ListenableFuture<SegmentsAndCommitMetadata> publishFuture = driver.publish(
sequenceMetadata.createPublisher(...),
sequenceMetadata.getCommitterSupplier(...).get(),
Collections.singletonList(sequenceMetadata.getSequenceName())
);
publishWaitList.add(publishFuture);
Futures.addCallback(publishFuture, new FutureCallback<>() {
@Override
public void onSuccess(SegmentsAndCommitMetadata published) {
sequences.remove(sequenceMetadata);
publishingSequences.remove(sequenceMetadata.getSequenceName());
persistSequences();
driver.registerHandoff(published);
}
});
}
6.3 StreamAppenderatorDriver.publish() 内部流程
public ListenableFuture<SegmentsAndCommitMetadata> publish(...) {
List<SegmentIdWithShardSpec> theSegments = getSegmentIdsWithShardSpecs(sequenceNames);
return Futures.transformAsync(
pushInBackground(wrapCommitter(committer), theSegments, true),
sam -> publishInBackground(null, null, null, sam, publisher, Function.identity()),
Execs.directExecutor()
);
}
6.4 pushInBackground() — Push 到 Deep Storage
ListenableFuture<SegmentsAndCommitMetadata> pushInBackground(...) {
return Futures.transform(
appenderator.push(segmentIdentifiers, wrappedCommitter, useUniquePath),
segmentsAndMetadata -> {
return segmentsAndMetadata;
},
executor
);
}
6.5 registerHandoff() — 等待 Historical 加载
public ListenableFuture<SegmentsAndCommitMetadata> registerHandoff(
SegmentsAndCommitMetadata sam
) {
for (SegmentIdWithShardSpec segmentId : waitingSegmentIdList) {
handoffNotifier.registerSegmentHandoffCallback(
new SegmentDescriptor(
segmentId.getInterval(), segmentId.getVersion(), ...
),
Execs.directExecutor(),
() -> {
metrics.incrementHandOffCount();
appenderator.drop(segmentId);
}
);
}
return resultFuture;
}
七、第五层:Segment 生成层(mergeAndPush)
7.1 核心代码位置
StreamAppenderator.java 的 mergeAndPush() 方法
7.2 mergeAndPush() 详细流程
private DataSegment mergeAndPush(
SegmentIdWithShardSpec identifier, Sink sink, boolean useUniquePath
) {
if (descriptorFile.exists() && !useUniquePath) {
return objectMapper.readValue(descriptorFile, DataSegment.class);
}
List<QueryableIndex> indexes = new ArrayList<>();
for (FireHydrant fireHydrant : sink) {
Pair<ReferenceCountingSegment, Closeable> segmentAndCloseable =
fireHydrant.getAndIncrementSegment();
QueryableIndex queryableIndex =
segmentAndCloseable.lhs.asQueryableIndex();
indexes.add(queryableIndex);
}
File mergedFile = indexMerger.mergeQueryableIndex(
indexes, // 多个持久化的 Hydrant
schema.getGranularitySpec().isRollup(),
schema.getAggregators(),
schema.getDimensionsSpec(),
mergedTarget,
tuningConfig.getIndexSpec(),
tuningConfig.getIndexSpecForIntermediatePersists(),
new BaseProgressIndicator(),
tuningConfig.getSegmentWriteOutMediumFactory(),
tuningConfig.getMaxColumnsToMerge()
);
DataSegment segment = RetryUtils.retry(
() -> dataSegmentPusher.push(mergedFile, segmentToPush, useUniquePath),
exception -> exception instanceof Exception,
5
);
objectMapper.writeValue(descriptorFile, segment);
return segment;
}
八、完整时序图
用户 Overlord KafkaSupervisor KafkaIndexTask TaskRunner
│ │ │ │ │
│ POST /supervisor │ │ │ │
│───────────────────>│ │ │ │
│ │ 创建 Supervisor │ │ │
│ │───────────────────>│ │ │
│ │ │ start() 启动定时 │ │
│ │ │ │ │
│ │ │ [每隔 period] │ │
│ │ │ runInternal() │ │
│ │ │ ├─ getPartitions() │ │
│ │ │ ├─ discoverTasks() │ │
│ │ │ └─ createNewTasks()│ │
│ │ │ │ │
│ │ taskQueue.add() │ │ │
│ │<───────────────────│ │ │
│ │ │ │ │
│ │ 分配到 MiddleManager│ │ │
│ │───────────────────────────────────────>│ │
│ │ │ │ createTaskRunner() │
│ │ │ │───────────────────>│
│ │ │ │ │
TaskRunner Kafka Appenderator Driver DeepStorage
│ │ │ │ │
│ 创建 Consumer │ │ │ │
│───────────────────>│ │ │ │
│ 创建 Appenderator │ │ │ │
│────────────────────────────────────────>│ │ │
│ 创建 Driver │ │ │ │
│─────────────────────────────────────────────────────────────>│ │
│ startJob() │ │ │ │
│─────────────────────────────────────────────────────────────>│ │
│ │ │ │ │
│ [主消费循环] │ │ │ │
│ │ │ │ │
│ poll(timeout) │ │ │ │
│───────────────────>│ │ │ │
│<───────────────────│ ConsumerRecords │ │ │
│ │ │ │ │
│ parse → InputRow[] │ │ │ │
│ │ │ │ │
│ [每条 InputRow] │ │ │ │
│ driver.add(row) │ │ │ │
│─────────────────────────────────────────────────────────────>│ │
│ │ │ add(segmentId,row) │ │
│ │ │<────────────────────│ │
│ │ │ Sink.add→Index.add │ │
│ │ │ │ │
│ [触发持久化] │ │ │ │
│ persistAsync() │ │ │ │
│─────────────────────────────────────────────────────────────>│ │
│ │ │ persistAll() │ │
│ │ │<────────────────────│ │
│ │ │ IndexMergerV9 │ │
│ │ │ .persist() │ │
│ │ │ │ │
│ [Sequence 完成] │ │ │ │
│ publish() │ │ │ │
│─────────────────────────────────────────────────────────────>│ │
│ │ │ push(segments) │ │
│ │ │<────────────────────│ │
│ │ │ mergeAndPush() │ │
│ │ │ IndexMergerV9 │ │
│ │ │ .mergeQueryableIndex() │
│ │ │ │ │
│ │ │ push(mergedFile) │ │
│ │ │────────────────────────────────────────>│
│ │ │<────────────────────────────────────────│
│ │ │ │ DataSegment │
│ │ │ │ │
│ │ │ │ publishInBackground│
│ │ │ │ → MetaStore │
│ │ │ │ │
│ │ │ │ registerHandoff │
│ │ │ │ → 等待 Historical │
│ │ │ │ │
│ [Historical 加载完成] │ │ │
│ │ │ drop(segmentId) │ │
│ │ │<────────────────────│ │
│ │ │ │ │
│ TaskStatus.SUCCESS │ │ │ │
九、KafkaRecordSupplier 详解
9.1 核心职责
KafkaRecordSupplier 封装了 KafkaConsumer<byte[], byte[]>,提供统一的消息拉取接口。
9.2 关键方法
private KafkaConsumer<byte[], byte[]> getKafkaConsumer() {
return new KafkaConsumer<>(properties);
}
public List<OrderedPartitionableRecord<Integer, Long, KafkaRecordEntity>> poll(long timeout) {
ConsumerRecords<byte[], byte[]> records = consumer.poll(Duration.ofMillis(timeout));
List<OrderedPartitionableRecord<Integer, Long, KafkaRecordEntity>> result = new ArrayList<>();
for (ConsumerRecord<byte[], byte[]> record : records) {
result.add(new OrderedPartitionableRecord<>(
record.topic(),
record.partition(),
record.offset(),
Collections.singletonList(new KafkaRecordEntity(record))
));
}
return result;
}
public void seek(StreamPartition<Integer> partition, Long sequenceNumber) {
consumer.seek(
new TopicPartition(partition.getStream(), partition.getPartitionId()),
sequenceNumber
);
}
public void assign(Set<StreamPartition<Integer>> streamPartitions) {
consumer.assign(
streamPartitions.stream()
.map(p -> new TopicPartition(p.getStream(), p.getPartitionId()))
.collect(Collectors.toSet())
);
}
十、关键设计要点
10.1 增量发布(Incremental Publishing)
| 特性 | 说明 |
|---|
| 核心思想 | 消费过程中,已完成的 Sequence 可以提前发布,不需要等到 Task 结束 |
| 触发条件 | Sequence 的所有分区都读到了 endOffset |
| 优势 | 减少数据延迟,降低内存压力 |
| 实现 | maybePersistAndPublishSequences() 在每次消费循环中检查 |
10.2 Exactly-Once 语义保障
| 机制 | 说明 |
|---|
| useUniquePath | Kafka Task 使用 useUniquePath=true 推送到 Deep Storage,避免重复数据 |
| 事务性发布 | TransactionalSegmentPublisher 确保 Segment 元数据和 offset 原子性更新 |
| 幂等性检查 | mergeAndPush() 中检查 descriptorFile 是否已存在,避免重复推送 |
10.3 Offset 管理
| 机制 | 说明 |
|---|
| currOffsets | 当前消费位置,每消费一条消息后 +1 |
| lastReadOffsets | 最后读取的 offset |
| Committer | 将 Kafka offset 与 Segment 元数据绑定,持久化到 commit.json |
| Checkpoint | 定期向 Supervisor 发送 Checkpoint,支持任务失败后从断点恢复 |
10.4 Handoff 等待
| 机制 | 说明 |
|---|
| registerHandoff | 发布后注册 Handoff 回调 |
| SegmentHandoffNotifier | 监听 Coordinator 的 Segment 加载通知 |
| 完成条件 | Historical 加载完成后,从本地删除 Segment |
| 超时处理 | 如果 Handoff 超时,Task 仍然返回 SUCCESS(数据已在 Deep Storage) |
10.5 多 Hydrant 合并
| 机制 | 说明 |
|---|
| Hydrant 来源 | 一个 Sink 可能有多次持久化产生的多个 FireHydrant |
| 合并时机 | push 阶段通过 mergeQueryableIndex 合并所有 Hydrant |
| 合并内容 | 字典合并(DictionaryMergingIterator)+ 行合并 + 倒排索引合并 |
| 输出 | 单个 V9 格式 Segment 文件 |
10.6 重试与容错
| 机制 | 说明 |
|---|
| Deep Storage 推送重试 | 支持 5 次重试,应对网络抖动 |
| commit.json | 记录已持久化的 hydrant 数量,支持从磁盘恢复 |
| persistError 传播 | 异步持久化错误通过 persistError 变量传播到主线程 |
| OffsetOutOfRange 处理 | IncrementalPublishingKafkaIndexTaskRunner 特殊处理 offset 越界 |
十一、涉及的核心源码文件汇总
| 层级 | 文件 | 说明 |
|---|
| Supervisor 层 | extensions-core/kafka-indexing-service/.../supervisor/KafkaSupervisor.java | Kafka Supervisor 实现,管理 Task 生命周期 |
| indexing-service/.../seekablestream/supervisor/SeekableStreamSupervisor.java | 流式 Supervisor 基类,包含 runInternal 主循环 |
| Task 层 | extensions-core/kafka-indexing-service/.../KafkaIndexTask.java | Kafka 索引任务,创建 Runner 和 RecordSupplier |
| indexing-service/.../seekablestream/SeekableStreamIndexTask.java | 流式任务基类,创建 Appenderator 和 Driver |
| Runner 层 | extensions-core/kafka-indexing-service/.../IncrementalPublishingKafkaIndexTaskRunner.java | Kafka 任务运行器,处理 OffsetOutOfRange |
| indexing-service/.../seekablestream/SeekableStreamIndexTaskRunner.java | 核心:runInternal 消费主循环 + 发布逻辑 |
| 消费层 | extensions-core/kafka-indexing-service/.../KafkaRecordSupplier.java | 封装 KafkaConsumer,提供 poll/seek/assign |
| Driver 层 | server/.../appenderator/StreamAppenderatorDriver.java | 流式 Driver,管理 publish/handoff |
| server/.../appenderator/BaseAppenderatorDriver.java | Driver 基类,pushInBackground/publishInBackground |
| Appenderator 层 | server/.../appenderator/StreamAppenderator.java | 核心:add/persistAll/push/mergeAndPush |
| Segment 生成层 | processing/.../segment/IndexMergerV9.java | V9 格式 Segment 合并/持久化 |