Flink 内存分配源码深度剖析:从启动计算到运行时动态分配的完整链路

0 阅读17分钟

上一篇讲了 Flink 内存模型的整体架构(TaskManager 总内存 → JVM 进程内存 → 堆内/堆外 → 各内存区域),这篇聚焦 Flink 内存分配的源码深度剖析。TaskManager 启动时如何计算各内存区域大小?MemoryManager 如何管理堆外内存?NetworkBufferPool 和 LocalBufferPool 如何分配网络缓冲?Task 启动时如何分配内存?运行时如何动态申请和释放?Task 完成后如何回收?这篇从源码级别跟踪 Flink 内存分配的完整链路。


一、内存分配整体架构

下面这张图是内存分配整体架构,包括启动时内存计算、内存管理器组件和运行时内存分配三大部分。

在这里插入图片描述

Flink 内存分配分为三个阶段:

启动时内存计算:TaskManager 启动时,TaskManagerOptions 和 FlinkMemoryUtils 根据配置计算各内存区域大小。首先从 process.size(总进程内存)中减去 JVM Metaspace 和 JVM Overhead,得到 Flink 可用内存(flink.size)。然后计算 Network Memory(按 network.fraction 比例)、Managed Memory(按 managed.fraction 比例)、Framework Heap(固定大小),剩余部分为 Task Heap。最后设置 JVM 参数 -Xmx/-Xms(堆内存)和 MaxDirectMemorySize(堆外内存)。

内存管理器组件:TaskManager 创建多个内存管理器组件。MemoryManager 负责管理堆外内存(Managed Memory)的分配和释放,创建时预分配所有内存页。NetworkEnvironment 负责管理网络内存(Network Memory)和网络连接,创建全局 NetworkBufferPool 预分配所有网络缓冲。TaskExecutorMemoryManager 负责管理 Task 级别的内存。NetworkBufferPool 是全局网络缓冲池,LocalBufferPool 是每个 ResultPartition/InputGate 的本地缓冲池,MemoryPool 是堆外内存页池。

运行时内存分配:Task 运行过程中,内存动态申请和释放。Task 启动时分配 Task Heap(JVM 直接管理)和网络缓冲(从 LocalBufferPool 申请)。算子运行时通过 MemoryManager.allocatePages() 申请堆外内存(批处理排序/哈希表使用)。网络数据传输时通过 LocalBufferPool.requestBuffer() 申请网络缓冲。RocksDB 状态后端从 Managed Memory 分配内存。Task 完成后回收所有分配的内存和缓冲。


二、内存分配完整流程

下面这张图是内存分配完整流程,包括6步分配流程、方法调用链、核心类和常见问题。

在这里插入图片描述

内存分配分为6步:

第1步:启动时内存计算。TaskManager 启动时,FlinkMemoryUtils.generateJvmParameters() 方法根据配置计算各内存区域大小。首先解析 process.size(总进程内存),减去 JVM Metaspace(默认 128MB)和 JVM Overhead(默认 10%,范围 192MB-1024MB),得到 Flink 可用内存。然后计算 Network Memory(Flink Memory × network.fraction,默认 10%,范围 64MB-1024MB),Managed Memory = (Flink Memory - Network) × managed.fraction(默认 40%),Framework Heap(默认 128MB),剩余为 Task Heap。最后生成 JVM 参数:-Xmx/-Xms 设置堆内存大小,-XX:MaxDirectMemorySize 设置堆外内存大小(Managed + Network)。

第2步:创建 MemoryManager。内存计算完成后,MemoryBuilder.build() 方法创建 MemoryManager 实例。MemoryManager 负责管理堆外内存(Managed Memory)的分配和释放。创建时根据 Managed Memory 大小和页大小(pageSize,默认 32KB)计算内存页数,创建 MemoryPool 内存池。MemoryPool.allocateAllPages() 方法预先分配所有内存页(使用 ByteBuffer.allocateDirect()),运行时通过 allocatePages() 和 releasePages() 方法动态分配和回收。MemoryManager 还支持内存预留(reserveMemory)和释放(releaseMemory),确保 Task 之间的内存隔离。

第3步:初始化 NetworkEnvironment。创建 MemoryManager 后,NetworkEnvironment.create() 方法初始化网络环境。NetworkEnvironment 负责管理网络内存(Network Memory)和网络连接。创建时根据 Network Memory 大小和缓冲大小(bufferSize,默认 32KB)计算网络缓冲数,创建全局 NetworkBufferPool。NetworkBufferPool 构造方法中预先分配所有网络缓冲(使用 ByteBuffer.allocateDirect()),运行时通过 requestBuffer() 和 recycle() 方法动态分配和回收。每个 ResultPartition 和 InputGate 会创建自己的 LocalBufferPool,从全局 NetworkBufferPool 申请独占缓冲(buffers-per-channel,默认 2)和浮动缓冲(floating-buffers-per-gate,默认 8)。

第4步:Task 启动分配内存。JobMaster 提交 Task 后,TaskExecutor.submitTask() 方法创建 Task 实例并启动。Task 启动时,TaskExecutorMemoryManager 为 Task 分配内存资源。首先为 Task 的 ResultPartition 和 InputGate 创建 LocalBufferPool,从全局 NetworkBufferPool 申请独占缓冲和浮动缓冲。然后为算子创建状态后端(RocksDBStateBackend 从 Managed Memory 分配内存,初始化 Block Cache、Write Buffer、Index Filter)。Task Heap 由 JVM 直接管理,不需要显式分配,JVM 在运行时自动为对象分配堆内存。

第5步:运行时动态申请/释放。Task 运行过程中,内存动态申请和释放。算子处理数据时,通过 MemoryManager.allocatePages() 申请堆外内存页(批处理排序/哈希表使用),使用完成后 releasePages() 释放。网络数据传输时,ResultPartition 通过 LocalBufferPool.requestBuffer() 申请网络缓冲,写入数据后通过 Netty 发送到下游,发送完成后 Buffer.recycle() 回收。InputGate 接收数据后读取 Buffer,处理完成后 recycle()。RocksDB 状态后端通过 Block Cache 和 Write Buffer 动态管理 Managed Memory,数据写入 MemTable,满后 flush 到磁盘,Compaction 时合并 SST 文件。反压时,LocalBufferPool 缓冲耗尽,requestBuffer() 阻塞等待,反压向上游传播。

第6步:Task 完成回收内存。Task 执行完成(成功或失败)后,Task.cleanup() 方法回收所有分配的内存资源。首先关闭 ResultPartition 和 InputGate,销毁 LocalBufferPool,将所有网络缓冲归还全局 NetworkBufferPool。然后关闭状态后端(RocksDB 关闭,释放 Block Cache、Write Buffer 等 Managed Memory),释放所有堆外内存页。最后 TaskExecutorMemoryManager 清理 Task 的所有内存资源。Task 完成后,其占用的内存和缓冲全部回收,可供其他 Task 使用。内存泄漏会导致可用内存持续减少,最终 OOM。


三、核心类源码剖析

3.1 MemoryManager 内存管理器

public class MemoryManager {
    private final MemoryPool memoryPool;
    private final long totalMemorySize;
    private final int pageSize;
    private final Map<Object, Long> reservedMemory;

    public static MemoryManager create(
            MemorySize memorySize,
            int pageSize,
            MemoryType memoryType) {
        return new MemoryBuilder()
            .setMemorySize(memorySize)
            .setPageSize(pageSize)
            .setMemoryType(memoryType)
            .build();
    }

    private MemoryManager(MemoryPool memoryPool, int pageSize) {
        this.memoryPool = memoryPool;
        this.pageSize = pageSize;
        this.totalMemorySize = (long) memoryPool.getNumberOfPages() * pageSize;
        this.reservedMemory = new HashMap<>();
    }

    // 申请内存页
    public List<MemorySegment> allocatePages(
            Object owner,
            int numberOfPages) {
        List<MemorySegment> pages = new ArrayList<>(numberOfPages);
        try {
            for (int i = 0; i < numberOfPages; i++) {
                MemorySegment segment = memoryPool.allocatePage();
                pages.add(segment);
            }
        } catch (MemoryPoolException e) {
            // 申请失败,释放已申请的页
            releasePages(pages);
            throw e;
        }
        return pages;
    }

    // 释放内存页
    public void releasePages(List<MemorySegment> pages) {
        for (MemorySegment page : pages) {
            memoryPool.release(page);
        }
    }

    // 预留内存
    public void reserveMemory(Object owner, long size) {
        long available = memoryPool.getAvailableMemorySize();
        if (size > available) {
            throw new MemoryReservationException(
                "Cannot reserve " + size + " bytes, only " + available + " available");
        }
        reservedMemory.merge(owner, size, Long::sum);
        memoryPool.reserve(size);
    }

    // 释放预留内存
    public void releaseMemory(Object owner, long size) {
        Long reserved = reservedMemory.remove(owner);
        if (reserved != null) {
            memoryPool.release(size);
        }
    }

    public long getTotalMemorySize() { return totalMemorySize; }
    public int getPageSize() { return pageSize; }
    public long getAvailableMemorySize() {
        return memoryPool.getAvailableMemorySize();
    }
}

MemoryManager 是 Flink 的堆外内存管理器,负责 Managed Memory 的分配和释放。create() 静态方法通过 MemoryBuilder 构建 MemoryManager 实例。构造方法中创建 MemoryPool,预分配所有内存页。allocatePages() 方法从内存池申请指定数量的内存页,申请失败时释放已申请的页并抛出异常。releasePages() 方法释放内存页回内存池。reserveMemory() 和 releaseMemory() 方法用于内存预留和释放,确保 Task 之间的内存隔离,避免一个 Task 占用所有内存。

3.2 NetworkBufferPool 全局网络缓冲池

public class NetworkBufferPool {
    private final int numberOfSegmentsToAllocate;
    private final int segmentSize;
    private final List<MemorySegment> availableMemorySegments;
    private final Set<LocalBufferPool> allBufferPools;
    private int totalNumberOfRequestedBuffers;

    public NetworkBufferPool(int numberOfSegmentsToAllocate, int segmentSize) {
        this.numberOfSegmentsToAllocate = numberOfSegmentsToAllocate;
        this.segmentSize = segmentSize;
        this.availableMemorySegments = new ArrayList<>(numberOfSegmentsToAllocate);
        this.allBufferPools = new HashSet<>();

        // 预分配所有网络缓冲
        for (int i = 0; i < numberOfSegmentsToAllocate; i++) {
            availableMemorySegments.add(
                MemorySegmentFactory.allocateUnpooledSegment(segmentSize));
        }
    }

    // 申请缓冲
    public MemorySegment requestMemorySegment() throws InterruptedException {
        synchronized (availableMemorySegments) {
            // 缓冲不足时阻塞等待
            while (availableMemorySegments.isEmpty()) {
                availableMemorySegments.wait();
            }
            return availableMemorySegments.remove(
                availableMemorySegments.size() - 1);
        }
    }

    // 回收缓冲
    public void recycle(MemorySegment segment) {
        synchronized (availableMemorySegments) {
            availableMemorySegments.add(segment);
            availableMemorySegments.notifyAll();
        }
    }

    // 创建 LocalBufferPool
    public LocalBufferPool createBufferPool(
            int numExclusiveBuffers,
            int numFloatingBuffers) {
        LocalBufferPool bufferPool = new LocalBufferPool(
            this, numExclusiveBuffers, numFloatingBuffers);
        synchronized (allBufferPools) {
            allBufferPools.add(bufferPool);
        }
        return bufferPool;
    }

    // 销毁 LocalBufferPool
    public void destroyBufferPool(LocalBufferPool bufferPool) {
        synchronized (allBufferPools) {
            allBufferPools.remove(bufferPool);
        }
        bufferPool.lazyDestroy();
    }

    public int getTotalNumberOfMemorySegments() {
        return numberOfSegmentsToAllocate;
    }
    public int getNumberOfAvailableMemorySegments() {
        synchronized (availableMemorySegments) {
            return availableMemorySegments.size();
        }
    }
    public int getSegmentSize() { return segmentSize; }
}

NetworkBufferPool 是全局网络缓冲池,TaskManager 级别唯一。构造方法中根据网络缓冲数和缓冲大小预分配所有网络缓冲(使用 ByteBuffer.allocateDirect())。requestMemorySegment() 方法申请缓冲,缓冲不足时阻塞等待(实现反压),缓冲可用时从可用列表中移除并返回。recycle() 方法回收缓冲,添加回可用列表并通知等待的线程。createBufferPool() 方法为每个 ResultPartition/InputGate 创建 LocalBufferPool,指定独占缓冲数和浮动缓冲数。destroyBufferPool() 方法销毁 LocalBufferPool,回收所有缓冲。

3.3 LocalBufferPool 本地缓冲池

public class LocalBufferPool implements BufferPool {
    private final NetworkBufferPool networkBufferPool;
    private final int numberOfExclusiveBuffers;
    private final int numberOfFloatingBuffers;
    private final List<MemorySegment> availableBuffers;
    private int numberOfRequestedBuffers;
    private boolean isDestroyed;

    public LocalBufferPool(
            NetworkBufferPool networkBufferPool,
            int numberOfExclusiveBuffers,
            int numberOfFloatingBuffers) {
        this.networkBufferPool = networkBufferPool;
        this.numberOfExclusiveBuffers = numberOfExclusiveBuffers;
        this.numberOfFloatingBuffers = numberOfFloatingBuffers;
        this.availableBuffers = new ArrayList<>();
        this.numberOfRequestedBuffers = 0;

        // 从全局池申请独占缓冲
        for (int i = 0; i < numberOfExclusiveBuffers; i++) {
            try {
                availableBuffers.add(networkBufferPool.requestMemorySegment());
                numberOfRequestedBuffers++;
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                throw new RuntimeException("Failed to allocate exclusive buffers", e);
            }
        }
    }

    // 申请缓冲
    @Override
    public MemorySegment requestBuffer() throws InterruptedException {
        synchronized (availableBuffers) {
            // 先从本地可用缓冲申请
            while (availableBuffers.isEmpty()) {
                if (isDestroyed) {
                    throw new IllegalStateException("Buffer pool is destroyed");
                }
                // 尝试从全局池申请浮动缓冲
                if (numberOfRequestedBuffers <
                        numberOfExclusiveBuffers + numberOfFloatingBuffers) {
                    try {
                        MemorySegment segment =
                            networkBufferPool.requestMemorySegment();
                        availableBuffers.add(segment);
                        numberOfRequestedBuffers++;
                    } catch (InterruptedException e) {
                        // 全局池缓冲不足,阻塞等待
                        availableBuffers.wait(1000);
                    }
                } else {
                    // 已达最大缓冲数,阻塞等待
                    availableBuffers.wait();
                }
            }
            return availableBuffers.remove(availableBuffers.size() - 1);
        }
    }

    // 回收缓冲
    @Override
    public void recycle(MemorySegment segment) {
        synchronized (availableBuffers) {
            availableBuffers.add(segment);
            availableBuffers.notifyAll();
        }
    }

    // 销毁缓冲池
    @Override
    public void lazyDestroy() {
        synchronized (availableBuffers) {
            isDestroyed = true;
            // 归还所有缓冲到全局池
            for (MemorySegment segment : availableBuffers) {
                networkBufferPool.recycle(segment);
            }
            availableBuffers.clear();
            numberOfRequestedBuffers = 0;
            availableBuffers.notifyAll();
        }
    }

    @Override
    public int getNumberOfAvailableBuffers() {
        synchronized (availableBuffers) {
            return availableBuffers.size();
        }
    }
    @Override
    public int getMaxNumberOfBuffers() {
        return numberOfExclusiveBuffers + numberOfFloatingBuffers;
    }
}

LocalBufferPool 是每个 ResultPartition/InputGate 的本地缓冲池。构造方法中从全局 NetworkBufferPool 申请独占缓冲(Exclusive Buffers,默认每通道 2 个),保证最低传输能力。requestBuffer() 方法申请缓冲,先从本地可用缓冲申请,不足时尝试从全局池申请浮动缓冲(Floating Buffers,默认每 Gate 8 个),浮动缓冲也不足时阻塞等待(实现反压)。recycle() 方法回收缓冲到本地池。lazyDestroy() 方法销毁缓冲池,将所有缓冲归还全局池。独占缓冲保证最低传输能力,浮动缓冲在通道间动态分配,提高缓冲利用率。

3.4 FlinkMemoryUtils 内存工具类

public class FlinkMemoryUtils {
    // 生成 JVM 参数
    public static String generateJvmParameters(FlinkMemory flinkMemory) {
        StringBuilder jvmParams = new StringBuilder();

        // 堆内存参数
        MemorySize heapSize = flinkMemory.getHeapMemorySize();
        jvmParams.append("-Xms").append(heapSize.getMebiBytes()).append("m ");
        jvmParams.append("-Xmx").append(heapSize.getMebiBytes()).append("m ");

        // 堆外内存参数(Managed + Network)
        MemorySize offHeapSize = flinkMemory.getOffHeapMemorySize();
        jvmParams.append("-XX:MaxDirectMemorySize=")
            .append(offHeapSize.getMebiBytes()).append("m ");

        // Metaspace 参数
        MemorySize metaspaceSize = flinkMemory.getJvmMetaspaceSize();
        jvmParams.append("-XX:MaxMetaspaceSize=")
            .append(metaspaceSize.getMebiBytes()).append("m ");

        return jvmParams.toString();
    }

    // 计算 Flink 内存
    public static FlinkMemory generateFlinkMemory(
            TaskManagerOptions options) {
        // 1. 获取总进程内存
        MemorySize processSize = options.getProcessMemorySize();

        // 2. 计算 JVM Overhead
        MemorySize jvmOverhead = calculateJvmOverhead(processSize, options);

        // 3. 计算 JVM Metaspace
        MemorySize jvmMetaspace = options.getJvmMetaspaceSize();

        // 4. Flink 可用内存 = 总内存 - Metaspace - Overhead
        MemorySize flinkSize = processSize.subtract(jvmMetaspace).subtract(jvmOverhead);

        // 5. 计算 Network Memory
        MemorySize networkMemory = calculateNetworkMemory(flinkSize, options);

        // 6. 计算 Managed Memory
        MemorySize managedMemory = calculateManagedMemory(
            flinkSize.subtract(networkMemory), options);

        // 7. Framework Heap
        MemorySize frameworkHeap = options.getFrameworkHeapSize();

        // 8. Task Heap = 剩余
        MemorySize taskHeap = flinkSize
            .subtract(networkMemory)
            .subtract(managedMemory)
            .subtract(frameworkHeap);

        return new FlinkMemory(
            taskHeap, frameworkHeap, networkMemory, managedMemory,
            jvmMetaspace, jvmOverhead);
    }

    private static MemorySize calculateJvmOverhead(
            MemorySize processSize, TaskManagerOptions options) {
        double fraction = options.getJvmOverheadFraction();
        MemorySize min = options.getJvmOverheadMin();
        MemorySize max = options.getJvmOverheadMax();
        MemorySize overhead = processSize.multiply(fraction);
        return overhead.clip(min, max); // 限制在 [min, max] 范围
    }

    private static MemorySize calculateNetworkMemory(
            MemorySize flinkSize, TaskManagerOptions options) {
        double fraction = options.getNetworkFraction();
        MemorySize min = options.getNetworkMin();
        MemorySize max = options.getNetworkMax();
        MemorySize network = flinkSize.multiply(fraction);
        return network.clip(min, max);
    }
}

FlinkMemoryUtils 是内存计算工具类,负责根据配置计算各内存区域大小并生成 JVM 参数。generateJvmParameters() 方法根据 FlinkMemory 生成 JVM 启动参数:-Xms/-Xmx 设置堆内存,-XX:MaxDirectMemorySize 设置堆外内存(Managed + Network),-XX:MaxMetaspaceSize 设置元空间。generateFlinkMemory() 方法是核心计算逻辑:从总进程内存中减去 Metaspace 和 Overhead 得到 Flink 可用内存,然后按比例计算 Network Memory 和 Managed Memory,减去 Framework Heap,剩余为 Task Heap。calculateJvmOverhead() 和 calculateNetworkMemory() 方法按比例计算并限制在 [min, max] 范围内。


四、网络内存与 Managed 内存分配

下面这张图是网络内存与 Managed 内存分配,包括网络缓冲池三层架构、Managed 内存两大用途和缓冲申请释放流程。

在这里插入图片描述

网络缓冲池三层架构

Flink 网络内存采用三层缓冲池架构:

  • L1:NetworkBufferPool(全局缓冲池):TaskManager 级别全局唯一,创建时预分配所有网络缓冲。管理总缓冲数 = Network Memory / bufferSize(默认 32KB)。所有 LocalBufferPool 从全局池申请缓冲,全局池负责缓冲的创建和销毁。缓冲耗尽时,requestMemorySegment() 阻塞等待,实现反压。
  • L2:LocalBufferPool(本地缓冲池):每个 ResultPartition 和 InputGate 各有一个。创建时从全局池申请独占缓冲(Exclusive Buffers,默认每通道 2 个)和浮动缓冲(Floating Buffers,默认每 Gate 8 个)。独占缓冲保证最低传输能力,浮动缓冲在通道间动态分配。LocalBufferPool 负责缓冲的分配和回收,使用完后归还全局池。
  • L3:Buffer(缓冲实例):单个网络缓冲,默认大小 32KB。包含数据区和引用计数。ResultPartition 通过 requestBuffer() 申请 Buffer,写入数据后通过 Netty 发送到下游,发送完成后 recycle() 回收。InputGate 接收数据后读取 Buffer,处理完成后 recycle()。Buffer 的引用计数确保多消费者场景下的安全回收。

Managed 内存两大用途

Managed Memory 有两大用途:

RocksDB 状态后端:

  • Block Cache(数据块缓存):默认占 Managed Memory 的 50%,缓存热点数据块,减少磁盘 IO。读性能关键,大状态作业建议调大。
  • Write Buffer(写缓冲):默认 64MB × 2(每个 Column Family 两个 MemTable),数据先写入 MemTable,满后 flush 到磁盘。写性能关键。
  • Index Filter(索引过滤器):默认占 Managed Memory 的 10%,索引和布隆过滤器,加速数据查找,减少磁盘 IO。
  • 其他开销:迭代器、快照、Compaction 开销,使用剩余 Managed Memory。Compaction 时内存需求增大,需预留足够空间。

批处理排序/哈希表:

  • 排序(Sort):批处理排序算子使用 Managed Memory 存储排序数据。内存不足时溢写磁盘(spill),排序完成后合并。大排序作业需要大 Managed Memory。
  • 哈希表(Hash Table):批处理 Join/聚合算子使用 Managed Memory 存储哈希表。内存不足时溢写磁盘,分桶处理。大 Join/聚合作业需要大 Managed Memory。
  • 内存申请方式:通过 MemoryManager.allocatePages() 申请内存页,使用完成后 releasePages() 释放。支持预留和动态分配,确保 Task 间内存隔离。
  • 溢写机制:Managed Memory 不足时,数据溢写到磁盘(IOManager 管理临时文件),处理完成后删除临时文件。溢写会降低性能,需要合理配置 Managed Memory。

网络缓冲申请释放流程

网络缓冲申请释放流程4步:

  1. 申请缓冲:ResultPartition.requestBuffer() → LocalBufferPool.requestBuffer() → 本地可用缓冲不足时从全局 NetworkBufferPool 申请
  2. 写入数据:序列化数据写入 Buffer,Buffer 引用计数 +1
  3. 网络发送:Netty 发送到下游 InputGate,发送完成后 Buffer 引用计数 -1,引用计数为 0 时 recycle()
  4. 回收缓冲:Buffer.recycle() → LocalBufferPool.recycle() → 归还本地可用缓冲或全局 NetworkBufferPool

反压机制:下游 InputGate 处理慢时,LocalBufferPool 缓冲耗尽,requestBuffer() 阻塞等待,ResultPartition 无法写入数据,反压向上游传播,直到 Source 算子暂停读取数据。反压是 Flink 流控的核心机制,确保系统稳定运行,不会因下游处理慢导致 OOM。


五、关键配置参数

# TaskManager 总进程内存
taskmanager.memory.process.size: 4096MB

# Flink 可用内存(不包含 JVM 开销)
taskmanager.memory.flink.size: 3558MB

# 任务堆内存(用户算子和 HashMapStateBackend 状态)
taskmanager.memory.task.heap.size: 1794MB

# 框架堆内存(Flink 框架使用)
taskmanager.memory.framework.heap.size: 128MB

# 管理内存比例(RocksDB 状态、批处理排序、哈希表)
taskmanager.memory.managed.fraction: 0.4
taskmanager.memory.managed.size: 1281MB

# 网络内存比例(数据传输缓冲、Shuffle)
taskmanager.memory.network.fraction: 0.1
taskmanager.memory.network.min: 64MB
taskmanager.memory.network.max: 1024MB

# JVM 元空间大小
taskmanager.memory.jvm-metaspace.size: 128MB

# JVM 开销比例
taskmanager.memory.jvm-overhead.fraction: 0.1
taskmanager.memory.jvm-overhead.min: 192MB
taskmanager.memory.jvm-overhead.max: 1024MB

# 网络缓冲配置
taskmanager.network.memory.buffers-per-channel: 2
taskmanager.network.memory.floating-buffers-per-gate: 8
taskmanager.network.memory.buffer-size: 32KB

# 内存页大小
taskmanager.memory.segment-size: 32KB

# RocksDB 内存管理
state.backend: rocksdb
state.backend.rocksdb.memory.managed: true
state.backend.rocksdb.memory.write-buffer-ratio: 0.5
state.backend.rocksdb.memory.high-prio-pool-ratio: 0.1

# 每个 TaskManager 的 Slot 数
taskmanager.numberOfTaskSlots: 1

六、常见问题与最佳实践

常见问题

  1. 网络缓冲不足

    • 原因:并行度高、buffers-per-channel 配置小、Network Memory 不足、反压严重。
    • 解决:增大 Network Memory(network.fraction=0.15-0.2)、调大 buffers-per-channel 和 floating-buffers-per-gate、优化下游算子处理速度。
  2. Managed 内存不足

    • 原因:RocksDB 状态大、批处理排序/哈希表内存需求大、managed.fraction 配置小、溢写磁盘频繁。
    • 解决:增大 Managed Memory(managed.fraction=0.5-0.6)、优化状态大小(配置 TTL)、减少批处理数据量。
  3. 内存泄漏

    • 原因:Task 完成后内存未回收、缓冲未归还全局池、状态后端未关闭、用户代码资源未释放。
    • 解决:检查 close() 方法确保资源释放、监控内存使用趋势、升级 Flink 版本(修复已知泄漏 bug)。
  4. 直接内存 OOM

    • 原因:MaxDirectMemorySize 配置过小、堆外内存使用超限、RocksDB 未纳入 managed 内存、Netty 直接内存泄漏。
    • 解决:增大 JVM Overhead、开启 RocksDB managed 内存(state.backend.rocksdb.memory.managed=true)、检查直接内存使用。
  5. 启动时内存计算错误

    • 原因:配置矛盾(同时设置 process.size 和 flink.size)、各区域大小之和超过总内存、min/max 范围配置错误。
    • 解决:只设置 process.size 让 Flink 自动计算、检查各区域配置、确保 min ≤ 计算值 ≤ max。
  6. Task 启动失败(缓冲分配失败)

    • 原因:全局 NetworkBufferPool 缓冲不足、独占缓冲需求超过总缓冲数、Task 并行度过高。
    • 解决:增大 Network Memory、减少 buffers-per-channel、降低并行度、增加 TaskManager 数量。

最佳实践

  1. 只设置 process.size,让 Flink 自动计算:生产环境推荐只设置 taskmanager.memory.process.size,让 Flink 根据比例自动计算各区域大小,避免配置矛盾。需要精细调优时再单独设置各区域大小。

  2. 高并行度作业调大网络内存:高并行度作业通道数多,网络缓冲需求大,建议 network.fraction=0.15-0.2,同时调大 floating-buffers-per-gate(如 16-32),减少反压。

  3. 大状态作业调大 Managed 内存:RocksDB 大状态作业需要大 Managed Memory,建议 managed.fraction=0.5-0.6,同时开启 state.backend.rocksdb.memory.managed=true,由 Flink 统一管理 RocksDB 内存,避免内存超限 OOM。

  4. 批处理作业调大 Managed 内存:批处理排序/Join/聚合使用 Managed Memory,建议 managed.fraction=0.5,减少溢写磁盘,提高性能。Network Memory 也可适当调大(network.fraction=0.15),加速 Shuffle。

  5. 监控内存池使用情况:通过 Flink Web UI 和 Metrics 监控 MemoryManager(堆外内存使用)、NetworkBufferPool(全局缓冲使用)、LocalBufferPool(本地缓冲使用)的指标,及时发现内存不足和泄漏。关注 Metrics:Status.Flink.Memory.Managed.Used、Status.Flink.Memory.Network.Used。

  6. 合理设置缓冲大小:大流量作业可调大 buffer-size 到 64KB,减少缓冲申请次数和网络开销;小流量作业保持 32KB,减少内存占用。缓冲大小需要与网络 MTU 和延迟匹配。

  7. 确保资源在 close() 中释放:用户代码中创建的外部资源(连接、文件、线程池、缓冲)必须在 RichFunction.close() 中释放,防止内存泄漏。使用 try-with-resources 确保资源关闭。Task 完成后 Flink 会回收框架管理的内存,但用户代码管理的资源需要手动释放。


七、总结

Flink 内存分配是一个6步流程:启动时内存计算 → 创建 MemoryManager → 初始化 NetworkEnvironment → Task 启动分配内存 → 运行时动态申请/释放 → Task 完成回收内存。核心类包括 MemoryManager(堆外内存管理器,allocatePages/releasePages)、NetworkBufferPool(全局网络缓冲池,预分配所有缓冲,requestMemorySegment/recycle)、LocalBufferPool(本地缓冲池,独占缓冲+浮动缓冲,requestBuffer/recycle)、FlinkMemoryUtils(内存计算工具类,generateFlinkMemory/generateJvmParameters)、TaskExecutorMemoryManager(TaskExecutor 内存管理器)、MemoryPool(内存页池)。

网络内存采用三层缓冲池架构:NetworkBufferPool(全局池)→ LocalBufferPool(本地池,每 ResultPartition/InputGate 一个)→ Buffer(缓冲实例,默认 32KB)。缓冲耗尽时 requestBuffer() 阻塞等待,实现反压。Managed Memory 有两大用途:RocksDB 状态后端(Block Cache + Write Buffer + Index Filter + 其他开销)和批处理排序/哈希表(内存不足时溢写磁盘)。RocksDB 必须开启 managed 内存,由 Flink 统一管理,避免内存超限 OOM。

内存计算遵循固定公式:Flink Memory = Process Size - Metaspace - Overhead;Network = Flink Memory × network.fraction;Managed = (Flink Memory - Network) × managed.fraction;Task Heap = Flink Memory - Framework Heap - Network - Managed。生产环境推荐只设置 process.size,让 Flink 自动计算各区域大小。

只设置 process.size 让 Flink 自动计算、高并行度作业调大网络内存、大状态作业调大 Managed 内存、批处理作业调大 Managed 内存、监控内存池使用、合理设置缓冲大小、确保资源在 close() 中释放,这7条最佳实践可以显著提高 Flink 作业的内存稳定性,减少 OOM 和内存泄漏问题,为生产环境的稳定运行提供保障。