Flink Slot 分配机制深度解析:从申请到部署的完整链路

1 阅读10分钟

前言

当你提交一个 Flink 作业后,JobMaster 如何向 ResourceManager 申请 Slot?ResourceManager 又如何从 TaskExecutor 获取资源?TaskExecutor 提供 Slot 后,JobMaster 如何接收并部署 Task?

本文将基于 Flink 1.17 源码,完整剖析 Slot 从申请到部署的 5 大阶段。

一、整体流程概览

发起 Slot 请求
声明资源需求
申请容器(YARN 模式)分配容器启动 TaskExecutor
请求分配 Slot
提供 Slot部署 

核心角色职责

组件职责关键类
JobMaster作业调度协调者DefaultScheduler
SlotPoolJobMaster 的 Slot 资源池DeclarativeSlotPoolBridge
ResourceManager集群资源管理者YarnResourceManager / StandaloneResourceManager
SlotManagerResourceManager 的 Slot 分配器DeclarativeSlotManager
TaskExecutor任务执行节点TaskExecutor

二、阶段一:JobMaster 发起 Slot 请求

2.1 触发时机

当 ExecutionGraph 构建完成后,Scheduler 开始调度任务:

// DefaultScheduler.java
@Override
public void startScheduling() {
    // 转换状态:CREATED -> SCHEDULED
    transitionToScheduled(getExecutionGraph().getAllExecutionVertices());
    
    // 分配 Slot 并部署任务
    allocateSlotsAndDeploy(executionVertexDeploymentOptions);
}

2.2 核心流程:allocateSlotsAndDeploy()

private void allocateSlotsAndDeploy(
        List<ExecutionVertexDeploymentOption> executionVertexDeploymentOptions) {
    
    // 1. 转换 ExecutionVertex 状态为 SCHEDULED
    transitionToScheduled(executionVertices);
    
    // 2. 创建 SlotProfile 检索器(包含位置偏好、资源需求等)
    Map<ExecutionVertexID, SlotProfile> slotProfiles = 
        createSlotProfiles(executionVertexDeploymentOptions);
    
    // 3. 按 SlotSharingGroup 分组
    Map<SlotSharingGroup, List<ExecutionVertexID>> groupedVertices = 
        groupBySlotSharingGroup(executionVertices);
    
    // 4. 为每个 SlotSharingGroup 分配一个 SharedSlot
    for (Map.Entry<SlotSharingGroup, List<ExecutionVertexID>> entry : 
         groupedVertices.entrySet()) {
        
        SlotSharingGroup group = entry.getKey();
        List<ExecutionVertexID> vertices = entry.getValue();
        
        // 5. 生成 Slot 请求 ID(AllocationID)
        AllocationID allocationId = new AllocationID();
        
        // 6. 计算 Slot 需要的资源
        ResourceProfile resourceProfile = 
            calculateResourceProfile(group, vertices);
        
        // 7. 获取 Slot 的偏好位置(TaskManager)
        Collection<TaskManagerLocation> preferredLocations = 
            getPreferredLocations(vertices);
        
        // 8. 创建 Slot 请求对象
        SlotRequestId slotRequestId = new SlotRequestId();
        PhysicalSlotRequest slotRequest = new PhysicalSlotRequest(
            slotRequestId,
            allocationId,
            resourceProfile,
            preferredLocations
        );
        
        // 9. 尝试从可用 Slot 分配
        Optional<PhysicalSlot> availableSlot = 
            slotPool.tryAllocateFromAvailable(slotRequest);
        
        if (availableSlot.isPresent()) {
            // 直接使用可用 Slot
            deployTask(vertices, availableSlot.get());
        } else {
            // 10. 请求新的 Slot
            requestNewSlot(slotRequest);
        }
    }
}

2.3 关键概念:SharedSlot

什么是 SharedSlot?

SharedSlot 允许同一 SlotSharingGroup 的多个 SubTask 复用同一个物理 Slot。

内部结构:SharedSlot 内部是一个 MultiTaskSlot 树形结构:

SharedSlot (物理 Slot)
└── MultiTaskSlot (Root)
    ├── MultiTaskSlot (Source, parallelism=4)
    │   ├── SingleTaskSlot (Source-0)
    │   ├── SingleTaskSlot (Source-1)
    │   ├── SingleTaskSlot (Source-2)
    │   └── SingleTaskSlot (Source-3)
    ├── MultiTaskSlot (Map, parallelism=4)
    │   ├── SingleTaskSlot (Map-0)
    │   └── ...
    └── MultiTaskSlot (Sink, parallelism=4)
        └── ...

生命周期管理:

就像 GC 的引用计数,每个 MultiTaskSlot 都被对应的子任务"引用"。SharedSlot 的引用计数等于树上所有活跃子任务的数量。只有当所有子任务都结束(正常或异常),计数归零,物理 Slot 才会被释放。

示例:

// 假设有 3 个算子,并行度都是 4
Source(4) -> Map(4) -> Sink(4)
​
// 不使用 Slot Sharing:需要 4 + 4 + 4 = 12 个 Slot
// 使用 Slot Sharing:只需要 max(4, 4, 4) = 4 个 Slot
​
// 每个 SharedSlot 内部运行:
// Slot-0: Source-0, Map-0, Sink-0
// Slot-1: Source-1, Map-1, Sink-1
// Slot-2: Source-2, Map-2, Sink-2
// Slot-3: Source-3, Map-3, Sink-3

2.4 关键源码位置

  • DefaultScheduler.allocateSlotsAndDeploy() - 分配入口
  • SlotSharingExecutionSlotAllocator.allocateSlotsFor() - 分配逻辑
  • DeclarativeSlotPoolBridge.allocateSlot() - 池化管理

三、阶段二:声明资源需求

3.1 requestNewSlot() 方法

当 SlotPool 中没有可用 Slot 时,会调用此方法:

private void requestNewSlot(PhysicalSlotRequest slotRequest) {
    
    // 1. 判断作业类型
    boolean isStreamingJob = executionMode == ExecutionMode.STREAMING;
    
    if (isStreamingJob) {
        // 流式作业:请求长期占用的 Slot
        requestBulkSlot(slotRequest);
    } else {
        // 批处理作业:请求临时 Slot
        requestSingleSlot(slotRequest);
    }
}
​
private void requestBulkSlot(PhysicalSlotRequest slotRequest) {
    
    // 2. 创建待处理的请求对象
    PendingRequest pendingRequest = new PendingRequest(
        slotRequest.getSlotRequestId(),
        slotRequest.getAllocationId(),
        slotRequest.getResourceProfile()
    );
    
    // 3. 放入待处理队列
    pendingRequests.put(
        slotRequest.getAllocationId(), 
        pendingRequest
    );
    
    // 4. 累加资源需求
    totalResourceRequirements.add(slotRequest.getResourceProfile());
    
    // 5. 通过回调函数通知 ResourceManager
    resourceRequirementChangeListener.notifyResourceRequirementsChanged(
        jobId, 
        totalResourceRequirements
    );
}

3.2 PendingRequest 的作用

PendingRequest 是 Slot 请求和 Slot 分配之间的桥梁:

class PendingRequest {
    private final SlotRequestId slotRequestId;  // 请求标识
    private final AllocationID allocationId;     // 分配标识
    private final ResourceProfile resourceProfile; // 资源需求
    private final CompletableFuture<PhysicalSlot> slotFuture; // 异步结果
    
    // 当 Slot 分配完成后,通过这个 Future 通知调度器
}

关键映射关系:

// SlotPool 内部维护
Map<AllocationID, PendingRequest> pendingRequests;
​
// 后续 TaskExecutor 提供 Slot 时,根据 AllocationID 精确匹配

3.3 关键源码位置

  • DeclarativeSlotPoolBridge.increaseResourceRequirements() - 声明需求
  • SlotPoolImpl.requestNewAllocatedSlot() - 旧版实现
  • ResourceRequirement - 资源需求数据结构

四、阶段三:ResourceManager 分配 Slot

4.1 接收资源需求

ResourceManager 收到 JobMaster 的资源需求后:

// YarnResourceManager.java
@Override
public void notifySlotRequirements(
        JobID jobId, 
        Collection<ResourceRequirement> resourceRequirements) {
    
    // 1. 调用 SlotManager 处理资源需求
    slotManager.processResourceRequirements(resourceRequirements);
}

4.2 SlotManager 的分配逻辑

// DeclarativeSlotManager.java
private void processResourceRequirements(
        Collection<ResourceRequirement> requirements) {
    
    // 1. 通知 ResourceTracker 更新资源需求
    resourceTracker.notifyResourceRequirements(jobId, requirements);
    
    // 2. 为每个 Job 分配 Slot(逻辑分配)
    for (ResourceRequirement requirement : requirements) {
        
        // 3. 检查是否有空闲的 TaskExecutor
        Optional<TaskExecutorConnection> availableTM = 
            findAvailableTaskExecutor(requirement.getResourceProfile());
        
        if (availableTM.isPresent()) {
            // 4. 向 TaskExecutor 发送 Slot 请求
            requestSlotFromTaskExecutor(
                availableTM.get(), 
                requirement
            );
        } else {
            // 5. 没有可用 TaskExecutor,需要启动新的
            startNewTaskExecutor(requirement.getResourceProfile());
        }
    }
}

4.3 YARN 模式的特殊处理:动态申请容器

在 YARN 模式下,ResourceManager 会动态向 YARN 申请容器:

// YarnResourceManager.java
private void startNewTaskExecutor(ResourceProfile resourceProfile) {
    
    // 1. 将 Slot 请求转换为 YARN 的 ContainerRequest
    Resource resource = Resource.newInstance(
        (int) resourceProfile.getTotalMemory().getMebiBytes(),
        resourceProfile.getCpuCores().getValue().intValue()
    );
    
    Priority priority = Priority.newInstance(1);
    ContainerRequest containerRequest = new ContainerRequest(
        resource,
        null,  // 节点偏好
        null,  // 机架偏好
        priority
    );
    
    // 2. 发起异步容器申请
    yarnClient.addContainerRequest(containerRequest);
}

// YARN 回调方法
@Override
public void onContainersAllocated(List<Container> containers) {
    
    for (Container container : containers) {
        // 3. 在对应 NodeManager 上启动 TaskExecutor
        launchTaskExecutor(container);
    }
}

private void launchTaskExecutor(Container container) {
    
    // 4. 构造启动命令
    String command = String.format(
        "$JAVA_HOME/bin/java -Xmx%dm %s %s",
        container.getResource().getMemory(),
        TaskManagerRunner.class.getName(),
        configurationDirectory
    );
    
    // 5. 提交到 NodeManager
    nodeManagerClient.startContainer(container, command);
}

流程总结:

JobMaster 需要 Slot
    ↓
ResourceManager 收到需求
    ↓
检查是否有空闲 TaskExecutor
    ↓ (没有)
向 YARN 申请容器 (ContainerRequest)
    ↓
YARN ResourceManager 分配容器
    ↓
在 NodeManager 上启动 TaskExecutor
    ↓
TaskExecutor 启动后注册到 ResourceManager
    ↓
ResourceManager 请求 TaskExecutor 分配 Slot

4.4 关键源码位置

  • DeclarativeSlotManager.processResourceRequirements() - 需求处理
  • YarnResourceManager.onContainersAllocated() - YARN 回调
  • TaskExecutorProcessSpec - TaskExecutor 资源规格

五、阶段四:TaskExecutor 提供 Slot

5.1 TaskExecutor 接收请求

ResourceManager 向 TaskExecutor 发送 Slot 请求后:

// TaskExecutor.java
@Override
public CompletableFuture<Acknowledge> requestSlot(
        SlotID slotId,
        JobID jobId,
        AllocationID allocationId,
        ResourceProfile resourceProfile,
        String targetAddress,
        ResourceManagerId resourceManagerId,
        Time timeout) {
    
    // 1. 验证是否已连接到 ResourceManager
    if (!isConnectedToResourceManager(resourceManagerId)) {
        return FutureUtils.completedExceptionally(
            new TaskManagerException("Not connected to ResourceManager")
        );
    }
    
    // 2. 持久化 Slot 分配快照(用于故障恢复)
    taskSlotTable.allocateSlot(
        slotId, 
        jobId, 
        allocationId, 
        resourceProfile, 
        timeout
    );
    
    // 3. 调用核心分配方法
    return allocateSlotForJob(jobId, slotId, allocationId, resourceProfile);
}

5.2 allocateSlotForJob() 核心逻辑

private CompletableFuture<Acknowledge> allocateSlotForJob(
        JobID jobId,
        SlotID slotId,
        AllocationID allocationId,
        ResourceProfile resourceProfile) {
    
    // 1. 获取或创建 Job 对象
    JobTable.Job job = jobTable.getOrCreateJob(jobId);
    
    // 2. 注册到 JobMaster(建立连接)
    CompletableFuture<JobMasterGateway> jobMasterGatewayFuture = 
        jobMasterGatewayFuture.getOrConnect(jobId);
    
    return jobMasterGatewayFuture.thenCompose(jobMasterGateway -> {
        
        // 3. 向 JobMaster 提供 Slot
        return jobMasterGateway.offerSlots(
            getResourceID(),           // TaskExecutor ID
            Collections.singleton(
                new SlotOffer(
                    allocationId,
                    slotId.getSlotNumber(),
                    resourceProfile
                )
            ),
            timeout
        );
    });
}

5.3 TaskSlotTable 的作用

TaskSlotTable 是 TaskExecutor 内部的 Slot 状态管理器:

class TaskSlotTable {
    
    // Slot 状态枚举
    enum State {
        FREE,      // 空闲
        ALLOCATED, // 已分配(但未部署 Task)
        ACTIVE     // 活跃(正在运行 Task)
    }
    
    // 内部数据结构
    Map<SlotID, TaskSlot> slots;  // Slot ID -> Slot 对象
    Map<AllocationID, SlotID> allocations; // 分配 ID -> Slot ID
    
    // 分配 Slot
    boolean allocateSlot(SlotID slotId, JobID jobId, AllocationID allocationId) {
        TaskSlot slot = slots.get(slotId);
        if (slot.getState() != State.FREE) {
            return false; // Slot 不可用
        }
        
        slot.setState(State.ALLOCATED);
        slot.setJobId(jobId);
        slot.setAllocationId(allocationId);
        allocations.put(allocationId, slotId);
        
        return true;
    }
}

5.4 关键源码位置

  • TaskExecutor.requestSlot() - 请求入口
  • TaskSlotTable.allocateSlot() - 状态管理
  • TaskExecutor.offerSlotsToJobManager() - 提供 Slot

六、阶段五:JobMaster 接收 Slot

6.1 接收 TaskExecutor 的 Slot Offer

JobMaster 收到 TaskExecutor 的 Slot 提供后:

// JobMaster.java
@Override
public CompletableFuture<Collection<SlotOffer>> offerSlots(
        ResourceID taskManagerId,
        Collection<SlotOffer> slots,
        Time timeout) {
    
    // 委托给 SlotPool 处理
    return slotPoolService.offerSlots(taskManagerId, slots, timeout);
}

// DeclarativeSlotPoolBridge.java
@Override
public CompletableFuture<Collection<SlotOffer>> offerSlots(
        ResourceID taskManagerId,
        Collection<SlotOffer> slots,
        Time timeout) {
    
    List<SlotOffer> acceptedSlots = new ArrayList<>();
    
    for (SlotOffer slotOffer : slots) {
        
        AllocationID allocationId = slotOffer.getAllocationId();
        
        // 1. 根据 AllocationID 查找之前创建的 PendingRequest
        PendingRequest pendingRequest = pendingRequests.get(allocationId);
        
        if (pendingRequest == null) {
            // 没有对应的请求,拒绝此 Slot
            continue;
        }
        
        // 2. 检查资源是否满足需求
        ResourceProfile required = pendingRequest.getResourceProfile();
        ResourceProfile offered = slotOffer.getResourceProfile();
        
        if (!offered.isMatching(required)) {
            // 资源不匹配,拒绝
            continue;
        }
        
        // 3. 创建 AllocatedSlot 对象
        AllocatedSlot allocatedSlot = new AllocatedSlot(
            allocationId,
            taskManagerId,
            slotOffer.getSlotIndex(),
            offered,
            taskManagerGateway
        );
        
        // 4. 将 Slot 加入可用资源池
        availableSlots.put(allocationId, allocatedSlot);
        
        // 5. 完成 PendingRequest 的 Future
        pendingRequest.getFuture().complete(allocatedSlot);
        
        // 6. 从待处理队列移除
        pendingRequests.remove(allocationId);
        
        acceptedSlots.add(slotOffer);
    }
    
    return CompletableFuture.completedFuture(acceptedSlots);
}

6.2 精确匹配机制

Q1:如何将物理 Slot 与 PendingRequest 精确匹配?

通过 AllocationID 作为唯一标识:

// SlotPool 内部维护映射
Map<AllocationID, PendingRequest> pendingRequests;

// 匹配逻辑
PendingRequest pendingRequest = pendingRequests.get(allocationId);

流程回顾:

  1. 阶段一:JobMaster 生成 AllocationID,创建 PendingRequest
  2. 阶段二:将 AllocationID 包含在资源需求中
  3. 阶段三:ResourceManager 将 AllocationID 传递给 TaskExecutor
  4. 阶段四:TaskExecutor 提供 Slot 时携带 AllocationID
  5. 阶段五:JobMaster 根据 AllocationID 匹配 PendingRequest

Q2:如果 Slot 资源大于需求,会拒绝吗?

不会拒绝,也不会截断。Flink 采用最低资源保障策略:

// ResourceProfile.java
public boolean isMatching(ResourceProfile required) {
    return this.cpuCores.compareTo(required.cpuCores) >= 0
        && this.taskHeapMemory.compareTo(required.taskHeapMemory) >= 0
        && this.taskOffHeapMemory.compareTo(required.taskOffHeapMemory) >= 0
        && this.managedMemory.compareTo(required.managedMemory) >= 0;
}

只要物理 Slot 的资源 ≥ 请求的资源,匹配就成功。多余的资源不会被截断,而是直接分配给这个 Task 使用。

示例:

请求:2 CPU, 4GB 内存
提供:4 CPU, 8GB 内存
结果:接受,Task 可以使用全部 4 CPU 和 8GB 内存

6.3 部署 Task

Slot 匹配成功后,立即触发 Task 部署:

// DefaultScheduler.java
private void deployTask(ExecutionVertex vertex, AllocatedSlot slot) {
    
    // 1. 创建 TaskDeploymentDescriptor
    TaskDeploymentDescriptor tdd = 
        TaskDeploymentDescriptorFactory.fromExecutionVertex(
            vertex, 
            slot.getSlotNumber()
        );
    
    // 2. 获取 TaskManager 的 Gateway
    TaskExecutorGateway taskExecutorGateway = slot.getTaskManagerGateway();
    
    // 3. RPC 调用部署 Task
    CompletableFuture<Acknowledge> deploymentFuture = 
        taskExecutorGateway.submitTask(tdd, jobMasterGateway, timeout);
    
    // 4. 部署成功后,转换状态:SCHEDULED -> DEPLOYING -> RUNNING
    deploymentFuture.thenAccept(ack -> {
        vertex.transitionState(ExecutionState.DEPLOYING);
    });
}

Q3:部署是在 RPC 主线程还是异步线程?

在 JobMaster 的调度主线程(不是 RPC 主线程):

// JobMaster 内部的 MainThreadExecutor
private final ComponentMainThreadExecutor mainThreadExecutor;

// 所有调度操作都在这个线程执行
mainThreadExecutor.execute(() -> {
    // 状态转换
    vertex.transitionState(ExecutionState.DEPLOYING);
    // ExecutionGraph 更新
    executionGraph.notifyVertexStateChange(vertex);
});

这个线程专门负责:

  • 状态转换(SCHEDULED → DEPLOYING → RUNNING)
  • ExecutionGraph 更新
  • Checkpoint 协调
  • Failover 处理

6.4 关键源码位置

  • DeclarativeSlotPoolBridge.offerSlots() - 接收 Slot
  • DefaultScheduler.deployOrHandleError() - 部署任务
  • Execution.deploy() - 部署核心逻辑

七、故障场景与恢复机制

7.1 TaskManager 宕机怎么办?

问题:如果 SharedSlot 所属的 TaskManager 突然宕机,JobMaster 如何感知?后续如何处理?

感知机制:心跳超时

// HeartbeatManager.java
private void checkHeartbeatTimeout(ResourceID taskManagerId) {
    
    long lastHeartbeat = lastHeartbeatTimestamps.get(taskManagerId);
    long now = System.currentTimeMillis();
    
    if (now - lastHeartbeat > heartbeatTimeout) {
        // 心跳超时,认为 TaskManager 已失联
        listener.notifyHeartbeatTimeout(taskManagerId);
    }
}

恢复流程:

  1. 释放失效 Slot
// SlotPool.java
@Override
public void notifyTaskManagerFailure(ResourceID taskManagerId) {
    
    // 释放所有属于该 TaskManager 的 Slot
    Collection<AllocatedSlot> slots = 
        allocatedSlots.getSlotsForTaskManager(taskManagerId);
    
    for (AllocatedSlot slot : slots) {
        freeSlot(slot.getAllocationId(), 
                 new TaskManagerException("TaskManager failed"));
    }
}
  1. 触发 Region 故障恢复
// DefaultScheduler.java
@Override
public void handleGlobalFailure(Throwable cause) {
    
    // 根据 Failover 策略决定重启范围
    Set<ExecutionVertexID> verticesToRestart = 
        failoverStrategy.getTasksNeedingRestart(failedVertexId, cause);
    
    // 回滚到最近的 Checkpoint
    restoreFromCheckpoint(verticesToRestart);
    
    // 重新调度
    allocateSlotsAndDeploy(verticesToRestart);
}
  1. Source 从 Checkpoint 恢复
// SourceOperator.java
@Override
public void initializeState(StateInitializationContext context) {
    
    // 从 Checkpoint 恢复 offset
    ListState<Long> offsetState = context.getOperatorStateStore()
        .getListState(new ListStateDescriptor<>("offset", Long.class));
    
    if (context.isRestored()) {
        // 恢复模式:读取保存的 offset
        for (Long offset : offsetState.get()) {
            reader.seek(offset);
        }
    }
}

7.2 Region 故障恢复策略

问题:一个 SubTask 失败时,Failover 策略如何决定重启哪些 Task?

Region 的定义

Region 由 数据交换类型 决定:

边类型特征持久化示例
Pipelined数据实时流转,上下游同时在线❌Forward、Rescale、Rebalance
Blocking下游等上游全部完成才读取✅Batch 模式的 Shuffle

流处理: 所有边默认为 Pipelined → 整个作业是一个 Region → 失败导致全局重启(从 Checkpoint 恢复)

批处理:Flink 按需插入 Blocking 边 → 形成多个 Region → 支持局部故障恢复

Region 故障恢复示例

// 批处理作业
Source(10) --Blocking--> Map(20) --Pipelined--> Filter(20) --Blocking--> Sink(5)

// Region 划分:
// Region-1: Source(10)
// Region-2: Map(20) -> Filter(20)
// Region-3: Sink(5)

// 如果 Filter-5 失败:
// - Region-2 重启(Map 和 Filter 全部重启)
// - Region-1 不受影响(数据已持久化)
// - Region-3 等待 Region-2 恢复

7.3 关键源码位置

  • HeartbeatManager.checkHeartbeatTimeout() - 心跳检测
  • RestartPipelinedRegionFailoverStrategy - Region 故障恢复
  • CheckpointCoordinator.restoreLatestCheckpointedState() - Checkpoint 恢复

八、常见问题与最佳实践

8.1 为什么任务一直处于 SCHEDULED 状态?

可能原因:

  1. Slot 不足
# 检查 JobManager 日志
No available slot for ExecutionVertex

# 解决方案:增加 TaskManager 数量或 Slot 数
taskmanager.numberOfTaskSlots: 4
  1. 资源规格不匹配
// 检查配置
taskmanager.memory.process.size: 2g
# vs
env.setParallelism(100)  // 并行度过高
  1. YARN 资源不足
# 检查 YARN 日志
AM has exceeded maximum number of container allocations

# 解决方案:增加 YARN 队列容量
yarn.scheduler.capacity.root.default.maximum-allocation-vcores: 32

8.2 如何优化 Slot 利用率?

方法 1:启用 Slot Sharing(默认开启)

// 默认所有算子在 "default" 组
env.socketTextStream(...).map(...).filter(...)

// 手动设置分组
stream.map(...).slotSharingGroup("heavy-computation");

方法 2:合理设置并行度

// 不要所有算子都用相同并行度
Source(2)   // 读取速度慢,低并行度
  .map(...).setParallelism(8)  // CPU 密集,高并行度
  .keyBy(...)
  .reduce(...).setParallelism(4)  // 聚合,中等并行度
  .sink(...).setParallelism(2);   // 写入有锁,低并行度

方法 3:禁用不合理的 Sharing

// 将 CPU 密集型算子隔离到独立 Slot
stream.map(heavyComputation).slotSharingGroup("cpu-intensive");

8.3 Slot 数量怎么计算?

公式:

不使用 Slot Sharing:
  总 Slot 数 = Σ(每个算子的并行度)

使用 Slot Sharing:
  总 Slot 数 = max(各算子的并行度)

示例:

Source(4) -> Map(8) -> KeyBy -> Reduce(8) -> Sink(2)

// 不共享:4 + 8 + 8 + 2 = 22 个 Slot
// 共享:max(4, 8, 8, 2) = 8 个 Slot

TaskManager 配置:

# 每个 TaskManager 提供 4 个 Slot
taskmanager.numberOfTaskSlots: 4

# 需要启动的 TaskManager 数量
# 使用 Slot Sharing:8 / 4 = 2 个 TaskManager
# 不使用:22 / 4 = 6 个 TaskManager

8.4 如何调试 Slot 分配问题?

方法 1:查看 Flink Web UI

JobManager -> Running Jobs -> 点击作业 -> Overview
  - 查看 "Total Task Slots" 和 "Available Task Slots"
  - 点击 TaskManager 查看每个 Slot 的使用情况

方法 2:启用详细日志

# conf/log4j.properties
logger.slotpool.name = org.apache.flink.runtime.jobmaster.slotpool
logger.slotpool.level = DEBUG

logger.resourcemanager.name = org.apache.flink.runtime.resourcemanager
logger.resourcemanager.level = DEBUG

方法 3:REST API 查询

# 查看 Slot 状态
curl http://localhost:8081/jobs/<job-id>/vertices

# 查看 TaskManager
curl http://localhost:8081/taskmanagers

九、总结

9.1 五阶段核心要点

主要角色关键操作核心类
JobMaster发起 Slot 请求,创建 PendingRequestDefaultScheduler
SlotPool声明资源需求,通知 ResourceManagerDeclarativeSlotPoolBridge
ResourceManager分配 Slot,向 YARN 申请容器YarnResourceManager
TaskExecutor提供 Slot,注册到 JobMasterTaskExecutor
JobMaster接收 Slot,部署 TaskExecution.deploy()

9.2 关键设计思想

  1. 异步非阻塞:所有 RPC 调用都是异步的,通过 CompletableFuture 协调
  2. 精确匹配:AllocationID 贯穿整个流程,确保 Slot 分配的准确性
  3. 资源复用:SharedSlot 通过树形结构实现多 Task 共享
  4. 故障恢复:心跳机制 + Region 故障恢复 + Checkpoint,保证高可用
  5. 动态扩展:YARN 模式支持根据需求动态申请容器