Druid Kafka 数据源消费到 Segment 生成全链路深度分析

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 合并/持久化

二、整体架构流程图

scss 复制代码
用户提交 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 流程说明

  1. 启动 :用户通过 REST API 提交 KafkaSupervisorSpec,Overlord 创建 KafkaSupervisor 实例并调用 start()
  2. 定时调度start() 中注册定时任务 buildRunTask(),按 ioConfig.period 周期性执行 runInternal()
  3. runInternal() 核心循环
scss 复制代码
updatePartitionDataFromStream()  → 获取 Kafka Topic 的分区列表
discoverTasks()                  → 发现已运行的 KafkaIndexTask
updateTaskStatus()               → 更新任务状态
checkTaskDuration()              → 检查任务是否超过 taskDuration
checkPendingCompletionTasks()    → 检查待完成的任务
checkCurrentTaskState()          → 检查当前任务状态
createNewTasks()                 → 创建缺失的任务
  1. 创建任务createTasksForGroup()KafkaSupervisor.createIndexTasks()

    • 根据 replicas 配置创建多个 KafkaIndexTask 实例
    • 每个 Task 包含 DataSchemaTuningConfigIOConfig(含起止 offset)
    • 通过 taskQueue.add(indexTask) 提交到 Overlord 的任务队列
  2. 分区分配策略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 调用链

scss 复制代码
KafkaIndexTask.run(toolbox)
  → getRunner().run(toolbox)                    // SeekableStreamIndexTask.run()
    → IncrementalPublishingKafkaIndexTaskRunner.run(toolbox)
      → runInternal(toolbox)                    // SeekableStreamIndexTaskRunner.runInternal()

4.3 runInternal() 初始化阶段

scss 复制代码
// 1. 创建 Kafka 消费者
RecordSupplier recordSupplier = task.newTaskRecordSupplier();
// → KafkaIndexTask.newTaskRecordSupplier()
// → new KafkaRecordSupplier(consumerProperties, configMapper)
// → 内部创建 KafkaConsumer<byte[], byte[]>,设置 auto.offset.reset=none

// 2. 创建 Appenderator(实时模式)
appenderator = task.newAppenderator(toolbox, metrics, rowIngestionMeters, parseExceptionHandler);
// → toolbox.getAppenderatorsManager().createRealtimeAppenderatorForTask(...)
// → 创建 StreamAppenderator 实例

// 3. 创建 StreamAppenderatorDriver
driver = task.newDriver(appenderator, toolbox, metrics);
// → new StreamAppenderatorDriver(appenderator, segmentAllocator, handoffNotifier, ...)
// → segmentAllocator 使用 ActionBasedSegmentAllocator(通过 Overlord 分配 Segment ID)

// 4. 启动 Job,恢复元数据
Object restoredMetadata = driver.startJob(lockHelper);
// → 恢复之前持久化的 commit 元数据(如果有)
// → 恢复 currOffsets(当前消费位置)

4.4 关键组件创建关系

scss 复制代码
KafkaIndexTask
  ├── KafkaRecordSupplier (封装 KafkaConsumer)
  ├── StreamAppenderator (管理 Sink/FireHydrant/IncrementalIndex)
  └── StreamAppenderatorDriver (管理 Segment 分配/发布/Handoff)
        ├── ActionBasedSegmentAllocator (通过 Overlord 分配 SegmentId)
        ├── SegmentHandoffNotifier (监听 Historical 加载完成)
        └── ActionBasedUsedSegmentChecker (检查已使用的 Segment)

五、第三层:消费循环层

5.1 核心代码位置

SeekableStreamIndexTaskRunner.javarunInternal() 方法

5.2 主循环流程

scss 复制代码
while (stillReading) {
    // 1. 检查暂停请求
    if (possiblyPause()) { ... }
    
    // 2. 检查停止请求
    if (stopRequested.get()) break;
    
    // 3. 检查并发布已完成的 Sequence
    maybePersistAndPublishSequences(committerSupplier);
    
    // 4. 从 Kafka 拉取消息
    List<OrderedPartitionableRecord> records = getRecords(recordSupplier, toolbox);
    // → IncrementalPublishingKafkaIndexTaskRunner.getRecords()
    //   → recordSupplier.poll(pollTimeout)
    //     → KafkaConsumer.poll(Duration.ofMillis(timeout))
    //     → 将 ConsumerRecord 包装为 OrderedPartitionableRecord<Integer, Long, KafkaRecordEntity>
    
    // 5. 遍历每条记录
    for (OrderedPartitionableRecord record : records) {
        // 5a. 验证 offset 在有效范围内
        boolean shouldProcess = verifyRecordInRange(
            record.getPartitionId(), record.getSequenceNumber()
        );
        
        if (shouldProcess) {
            // 5b. 解析消息为 InputRow
            List<InputRow> rows = parser.parse(record.getData(), ...);
            // → StreamChunkParser.parse()
            //   → InputFormat.createReader() 或 InputRowParser.parseBatch()
            //   → 应用 TransformSpec 转换
            //   → 过滤 minimumMessageTime / maximumMessageTime 之外的行
            
            // 5c. 找到对应的 SequenceMetadata
            SequenceMetadata sequenceToUse = sequences.stream()
                .filter(seq -> seq.canHandle(this, record))
                .findFirst().orElse(null);
            
            // 5d. 写入每一行
            for (InputRow row : rows) {
                AppenderatorDriverAddResult addResult = driver.add(
                    row, sequenceToUse.getSequenceName(), committerSupplier,
                    true,   // skipSegmentLineageCheck
                    false   // allowIncrementalPersists (先不允许,等批次处理完)
                );
                // → StreamAppenderatorDriver.add()
                //   → BaseAppenderatorDriver.append()
                //     → 分配 SegmentId(如果需要)
                //     → appenderator.add(segmentId, row, committerSupplier)
                //       → Sink.add(row) → IncrementalIndex.add(row)
                
                // 5e. 检查是否需要创建新 Segment
                boolean isPushRequired = addResult.isPushRequired(
                    maxRowsPerSegment, maxTotalRows
                );
                if (isPushRequired && !sequenceToUse.isCheckpointed()) {
                    sequenceToCheckpoint = sequenceToUse;
                }
                
                isPersistRequired |= addResult.isPersistRequired();
            }
            
            // 5f. 触发异步持久化
            if (isPersistRequired) {
                driver.persistAsync(committerSupplier.get());
                // → appenderator.persistAll(wrappedCommitter)
                //   → 将 IncrementalIndex 持久化为磁盘文件
                //   → IndexMergerV9.persist()
            }
            
            // 5g. 更新 offset
            lastReadOffsets.put(record.getPartitionId(), record.getSequenceNumber());
            currOffsets.put(record.getPartitionId(), record.getSequenceNumber() + 1);
        }
        
        // 5h. 检查是否读到了 endOffset
        if (!moreToReadAfterThisRecord) {
            assignment.remove(record.getStreamPartition());
            stillReading = !assignment.isEmpty();
        }
    }
    
    // 6. 定期 Checkpoint
    if (System.currentTimeMillis() > nextCheckpointTime) {
        sequenceToCheckpoint = getLastSequenceMetadata();
    }
    if (sequenceToCheckpoint != null && stillReading) {
        // 向 Supervisor 发送 Checkpoint 请求
        CheckPointDataSourceMetadataAction checkpointAction = ...;
        toolbox.getTaskActionClient().submit(checkpointAction);
    }
}

5.3 maybePersistAndPublishSequences() --- 增量发布

scss 复制代码
private void maybePersistAndPublishSequences(Supplier<Committer> committerSupplier) {
    for (SequenceMetadata sequenceMetadata : sequences) {
        sequenceMetadata.updateAssignments(currOffsets, this::isMoreToReadBeforeReadingRecord);
        // 如果 Sequence 已关闭(所有分区都读完了)且未在发布中
        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() 流程

scss 复制代码
// SeekableStreamIndexTaskRunner.publishAndRegisterHandoff()
protected void publishAndRegisterHandoff(SequenceMetadata sequenceMetadata) {
    // 1. 调用 driver.publish() 发布 Segment
    ListenableFuture<SegmentsAndCommitMetadata> publishFuture = driver.publish(
        sequenceMetadata.createPublisher(...),  // TransactionalSegmentPublisher
        sequenceMetadata.getCommitterSupplier(...).get(),
        Collections.singletonList(sequenceMetadata.getSequenceName())
    );
    publishWaitList.add(publishFuture);
    
    // 2. 发布成功后注册 Handoff
    Futures.addCallback(publishFuture, new FutureCallback<>() {
        @Override
        public void onSuccess(SegmentsAndCommitMetadata published) {
            // 从 sequences 列表中移除已发布的 sequence
            sequences.remove(sequenceMetadata);
            publishingSequences.remove(sequenceMetadata.getSequenceName());
            persistSequences();  // 持久化 sequence 状态
            
            // 注册 Handoff 等待
            driver.registerHandoff(published);
        }
    });
}

6.3 StreamAppenderatorDriver.publish() 内部流程

typescript 复制代码
public ListenableFuture<SegmentsAndCommitMetadata> publish(...) {
    List<SegmentIdWithShardSpec> theSegments = getSegmentIdsWithShardSpecs(sequenceNames);
    
    // 步骤1: pushInBackground --- 合并并推送到 Deep Storage
    // 步骤2: publishInBackground --- 发布元数据到 MetaStore
    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

javascript 复制代码
// BaseAppenderatorDriver.pushInBackground()
ListenableFuture<SegmentsAndCommitMetadata> pushInBackground(...) {
    return Futures.transform(
        appenderator.push(segmentIdentifiers, wrappedCommitter, useUniquePath),
        // → StreamAppenderator.push()
        //   → persistAll() 确保所有数据已持久化
        //   → 遍历每个 Sink 调用 mergeAndPush()
        segmentsAndMetadata -> {
            // 校验 push 的 segment 与请求的一致
            return segmentsAndMetadata;
        },
        executor
    );
}

6.5 registerHandoff() --- 等待 Historical 加载

scss 复制代码
// StreamAppenderatorDriver.registerHandoff()
public ListenableFuture<SegmentsAndCommitMetadata> registerHandoff(
    SegmentsAndCommitMetadata sam
) {
    for (SegmentIdWithShardSpec segmentId : waitingSegmentIdList) {
        handoffNotifier.registerSegmentHandoffCallback(
            new SegmentDescriptor(
                segmentId.getInterval(), segmentId.getVersion(), ...
            ),
            Execs.directExecutor(),
            () -> {
                // Historical 加载完成后的回调
                metrics.incrementHandOffCount();
                appenderator.drop(segmentId);  // 从本地删除 Segment
            }
        );
    }
    return resultFuture;
}

七、第五层:Segment 生成层(mergeAndPush)

7.1 核心代码位置

StreamAppenderator.javamergeAndPush() 方法

7.2 mergeAndPush() 详细流程

scss 复制代码
private DataSegment mergeAndPush(
    SegmentIdWithShardSpec identifier, Sink sink, boolean useUniquePath
) {
    // 1. 前置校验
    //    - Sink 不再可写
    //    - 所有 FireHydrant 已 swap(已持久化)
    
    // 2. 检查是否已推送(幂等性)
    if (descriptorFile.exists() && !useUniquePath) {
        return objectMapper.readValue(descriptorFile, DataSegment.class);
    }
    
    // 3. 收集所有 QueryableIndex
    List<QueryableIndex> indexes = new ArrayList<>();
    for (FireHydrant fireHydrant : sink) {
        Pair<ReferenceCountingSegment, Closeable> segmentAndCloseable =
            fireHydrant.getAndIncrementSegment();
        QueryableIndex queryableIndex =
            segmentAndCloseable.lhs.asQueryableIndex();
        indexes.add(queryableIndex);
    }
    
    // 4. 合并所有 Hydrant 的 QueryableIndex
    File mergedFile = indexMerger.mergeQueryableIndex(
        indexes,                                    // 多个持久化的 Hydrant
        schema.getGranularitySpec().isRollup(),     // 是否 Rollup
        schema.getAggregators(),                    // 聚合器
        schema.getDimensionsSpec(),                 // 维度规格
        mergedTarget,                               // 输出目录
        tuningConfig.getIndexSpec(),                // 索引规格(压缩、位图类型等)
        tuningConfig.getIndexSpecForIntermediatePersists(),
        new BaseProgressIndicator(),
        tuningConfig.getSegmentWriteOutMediumFactory(),
        tuningConfig.getMaxColumnsToMerge()
    );
    // → IndexMergerV9.mergeQueryableIndex()
    //   → 多 Segment 字典合并 (DictionaryMergingIterator)
    //   → 行合并 (RowCombiningTimeAndDimsIterator 或 MergingRowIterator)
    //   → 列式写入 + 倒排索引构建
    //   → 生成 V9 格式 Segment 文件
    
    // 5. 推送到 Deep Storage(带重试,最多 5 次)
    DataSegment segment = RetryUtils.retry(
        () -> dataSegmentPusher.push(mergedFile, segmentToPush, useUniquePath),
        exception -> exception instanceof Exception,
        5
    );
    
    // 6. 写入描述文件(用于幂等性检查)
    objectMapper.writeValue(descriptorFile, segment);
    
    return segment;
}

八、完整时序图

scss 复制代码
用户                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 关键方法

csharp 复制代码
// 创建 KafkaConsumer
private KafkaConsumer<byte[], byte[]> getKafkaConsumer() {
    // 设置 key/value 反序列化器为 ByteArrayDeserializer
    // 设置 auto.offset.reset = none(不自动重置 offset)
    // 设置 enable.auto.commit = false(手动管理 offset)
    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(),     // partitionId (Integer)
            record.offset(),        // sequenceNumber (Long)
            Collections.singletonList(new KafkaRecordEntity(record))
        ));
    }
    return result;
}

// 定位到指定 offset
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 合并/持久化
相关推荐
摇曳的精灵2 小时前
Spring boot注解实现信息脱敏
java·spring boot·后端·注解脱敏·信息脱敏
程序猿大帅2 小时前
记一次线上翻车:加了Redisson分布式锁,数据还是被并发打穿了
后端
weixin_704266052 小时前
项目总结一
java·前端·spring boot·后端·spring
JimmtButler2 小时前
一台电脑,两个 Git 身份:公司 GitLab + 个人 GitHub 共存
后端
全栈王校长2 小时前
Nest 中间件 Middleware - 就像 Vue 的路由守卫
后端·nestjs
全栈王校长2 小时前
Nest ValidationPipe 参数验证 - 就像前端的表单校验
后端·nestjs
猫咪老师2 小时前
Day11 Python 关于线程和进程的最详细介绍!
后端·python
林木882 小时前
Druid 实时数据摄入与持久化全链路深度分析
后端
李日灐2 小时前
【优选算法3】二分查找经典算法面试题
开发语言·c++·后端·算法·面试·二分查找·双指针