本文旨在详解 Flink TaskManager 的内存模型以及其各部分内存占比的计算逻辑。首先,结合官网展示了当前 Flink 的内存模型,并在之后结合 JVM 自身内存模型和管理机制结合讲解 Flink 内存模型的各个部分,最后结合源码解释了各部分内存占比的计算逻辑。

1 内存模型组成部分

本节摘自官网:https://nightlies.apache.org/flink/flink-docs-release-1.20/zh...
image.png

组成部分配置参数描述
框架堆内存(Framework Heap Memory)taskmanager.memory.framework.heap.size用于 Flink 框架的 JVM 堆内存(进阶配置)。
任务堆内存(Task Heap Memory)taskmanager.memory.task.heap.size用于 Flink 应用的算子及用户代码的 JVM 堆内存。
托管内存(Managed memory)taskmanager.memory.managed.size taskmanager.memory.managed.fraction由 Flink 管理的用于排序、哈希表、缓存中间结果及 RocksDB State Backend 的本地内存。
框架堆外内存(Framework Off-heap Memory)taskmanager.memory.framework.off-heap.size用于 Flink 框架的堆外内存(直接内存或本地内存)(进阶配置)。
任务堆外内存(Task Off-heap Memory)taskmanager.memory.task.off-heap.size用于 Flink 应用的算子及用户代码的堆外内存(直接内存或本地内存)。
网络内存(Network Memory)taskmanager.memory.network.min taskmanager.memory.network.max taskmanager.memory.network.fraction用于任务之间数据传输的直接内存(例如网络传输缓冲)。该内存部分为基于 Flink 总内存的受限的等比内存部分。这块内存被用于分配网络缓冲
JVM Metaspacetaskmanager.memory.jvm-metaspace.sizeFlink JVM 进程的 Metaspace。
JVM 开销taskmanager.memory.jvm-overhead.min taskmanager.memory.jvm-overhead.max taskmanager.memory.jvm-overhead.fraction用于其他 JVM 开销的本地内存,例如栈空间、垃圾回收空间等。该内存部分为基于进程总内存的受限的等比内存部分。

2 Flink 内存模型对内存的逻辑划分

Flink 作为一个 JVM 进程,首先复习一下 JVM 内存的简单划分:

  • Heap memory:JVM 进程内部使用的内存,用于存储 java 对象,受 GC 控制;
  • Native memory/Off-heap:Java 进程使用的用户地址空间上的非堆内存,不受 GC 控制,包含元空间(Metaspace)以及 Direct memory;
  • Direct memory:Native Memory,意味着共享硬件内的底层缓冲区,目标是为了减少内存拷贝,可简单理解为 NIO 中提供的 allocate 方法获取的内存;
  • Metaspace:Java8 后替代永久代的设计,之前类和方法、字符串常量池等信息存储在永久代,而永久代使用堆内存,容易出现 OOM,且增加了 GC 的复杂性,而 metaspace 使用 native memory,降低了该风险。

参考资料:

  1. What is the difference between off-heap, native heap, direct memory and native memory?

JVM 其实真正控制的内存部分就是堆内、堆外(Direct 部分)、元空间,严格来说元空间也属于堆外,但是 JVM 对这部分特别定义为了元空间。因此 Flink 在启动 TM 时可以利用 -XMs、-Xmx 等 JVM 配置控制堆内内存,利用 -XX:MaxDirectMemorySize 和 -XX:MaxMetaspaceSize 分别控制 direct memory 和元空间。其中 task off-heap、framework off-heap、network memory 这三部分均属于 direct memory。

而其他内存,比如通过 JNI 调用 C 代码申请的内存其实是不在 JVM 管控内的,因此这部分内存也没办法通过 JVM 的参数控制,比如 Flink 的内存模型中,jvm overhead 和 managed memory 这两部分内存是完全不受 JVM 管控的,属于 native memory(非 direct),只能通过容器或者系统对 JVM 整体进程进行监控 kill。

一般情况下,配置 Flink 内存最简单的方法是配置总内存。此外,Flink 也支持更细粒度的内存配置方式。

3 内存计算推断逻辑

3.1 推断入口及推断模式

TaskManager 的内存计算推断的逻辑入口在 org.apache.flink.runtime.clusterframework.TaskExecutorProcessUtils#processSpecFromConfig。

public static TaskExecutorProcessSpec processSpecFromConfig(final Configuration config) {
    try {
        return createMemoryProcessSpec(
                config, PROCESS_MEMORY_UTILS.memoryProcessSpecFromConfig(config));
    } catch (IllegalConfigurationException e) {
        throw new IllegalConfigurationException(
                "TaskManager memory configuration failed: " + e.getMessage(), e);
    }
}

org.apache.flink.runtime.util.config.memory.ProcessMemoryUtils#memoryProcessSpecFromConfig 有三种推断模式(优先级自上而下):

public CommonProcessMemorySpec<FM> memoryProcessSpecFromConfig(Configuration config) {
    if (options.getRequiredFineGrainedOptions().stream().allMatch(config::contains)) {
        // all internal memory options are configured, use these to derive total Flink and
        // process memory
        return deriveProcessSpecWithExplicitInternalMemory(config);
    } else if (config.contains(options.getTotalFlinkMemoryOption())) {
        // internal memory options are not configured, total Flink memory is configured,
        // derive from total flink memory
        return deriveProcessSpecWithTotalFlinkMemory(config);
    } else if (config.contains(options.getTotalProcessMemoryOption())) {
        // total Flink memory is not configured, total process memory is configured,
        // derive from total process memory
        return deriveProcessSpecWithTotalProcessMemory(config);
    }
    return failBecauseRequiredOptionsNotConfigured();
}
  1. 如果以下部分的内存已经显式配置 size,进入 deriveProcessSpecWithExplicitInternalMemory

    • taskmanager.memory.task.heap.size
    • taskmanager.memory.managed.size
  2. 显式配置了 taskmanager.memory.flink.size,进入 deriveProcessSpecWithTotalFlinkMemory
  3. 显式配置了 taskmanager.memory.process.size,进入 deriveProcessSpecWithTotalProcessMemory

3.2 deriveProcessSpecWithExplicitInternalMemory

private CommonProcessMemorySpec<FM> deriveProcessSpecWithExplicitInternalMemory(
        Configuration config) {
    FM flinkInternalMemory = flinkMemoryUtils.deriveFromRequiredFineGrainedOptions(config);
    JvmMetaspaceAndOverhead jvmMetaspaceAndOverhead =
            deriveJvmMetaspaceAndOverheadFromTotalFlinkMemory(
                    config, flinkInternalMemory.getTotalFlinkMemorySize());
    return new CommonProcessMemorySpec<>(flinkInternalMemory, jvmMetaspaceAndOverhead);
}
  1. 根据各部分内存的细粒度配置推断整个 flink taskmanaged 的进程内存分配;
  2. 计算 jvm metaspace 和 jvm overhead 部分需要占用的内存 jvmMetaspaceAndOverhead。

3.3 deriveProcessSpecWithTotalFlinkMemory

private CommonProcessMemorySpec<FM> deriveProcessSpecWithTotalFlinkMemory(
            Configuration config) {
    MemorySize totalFlinkMemorySize =
            getMemorySizeFromConfig(config, options.getTotalFlinkMemoryOption());
    FM flinkInternalMemory =
            flinkMemoryUtils.deriveFromTotalFlinkMemory(config, totalFlinkMemorySize);
    JvmMetaspaceAndOverhead jvmMetaspaceAndOverhead =
            deriveJvmMetaspaceAndOverheadFromTotalFlinkMemory(config, totalFlinkMemorySize);
    return new CommonProcessMemorySpec<>(flinkInternalMemory, jvmMetaspaceAndOverhead);
}
  1. 根据配置获取 totalFlinkMemorySize;
  2. 根据 totalFlinkMemorySize 推断 flink taskmanager 进程各部分的内存分配: flinkInternalMemory;
  3. 计算 jvm metaspace 和 jvm overhead 部分需要占用的内存。

3.4 deriveProcessSpecWithTotalProcessMemory

private CommonProcessMemorySpec<FM> deriveProcessSpecWithTotalProcessMemory(
        Configuration config) {
    MemorySize totalProcessMemorySize =
            getMemorySizeFromConfig(config, options.getTotalProcessMemoryOption());
    JvmMetaspaceAndOverhead jvmMetaspaceAndOverhead =
            deriveJvmMetaspaceAndOverheadWithTotalProcessMemory(config, totalProcessMemorySize);
    MemorySize totalFlinkMemorySize =
            totalProcessMemorySize.subtract(
                    jvmMetaspaceAndOverhead.getTotalJvmMetaspaceAndOverheadSize());
    FM flinkInternalMemory =
            flinkMemoryUtils.deriveFromTotalFlinkMemory(config, totalFlinkMemorySize);
    return new CommonProcessMemorySpec<>(flinkInternalMemory, jvmMetaspaceAndOverhead);
}
  1. 根据配置获取 totalProcessMemorySize;
  2. 计算 jvm metaspace 和 jvm overhead 部分需要占用的内存 jvmMetaspaceAndOverhead;
  3. 计算得到 totalFlinkMemorySize = totalProcessMemorySize - jvmMetaspaceAndOverhead;
  4. 根据 totalFlinkMemorySize 推断 flink taskmanager 进程各部分的内存分配: flinkInternalMemory。

3.5 Jvm Metaspace 以及 overhead 部分推断

  1. 从配置中获取 taskmanager.memory.jvm-metaspace.size 的配置(默认为 256M);
  2. 将 metaspace 这部分内存加到 totalFlinkMemorySize 中得到 totalFlinkAndJvmMetaspaceSize;
  3. 如果配置了总进程内存,则利用总进程和其他部分的内存情况计算 得到 jvm overhead 部分的内存:

    jvmOverheadSize = totalProcessSize - totalFlinkMemorySize - jvmMetaspaceSize
  4. jvmOverheadSize。
  5. 如果未配置总进程内存,则利用 totalFlinkMemorySize + jvmMetaspaceSize 和 taskmanager.memory.jvm-overhead.fraction 的值进行相对计算:

    jvmOverheadSize = max(jvmOverheadMinSize, min(jvmOverheadMaxSize, (totalFlinkMemorySize + jvmMetaspaceSize) * (jvmOverHeadFraction /(1- jvmOverHeadFraction)))

3.6 Flink 各部分内存的详细计算逻辑(TaskExecutorFlinkMemoryUtils)

TaskExecutorFlinkMemoryUtils 提供了两个方法:

  1. 给定 flink memory size(不包含 jvm metaspace 和 overhead),推断内部的细粒度配置;
  2. 从细粒度的资源配置中推导出总的 flink memory size。

3.6.1 deriveFromTotalFlinkMemory

先计算几个较为固定的内存部分:

  • taskmanager.memory.framework.heap.size,默认为 128 M
  • taskmanager.memory.framework.off-heap.size,默认为 128 M
  • taskmanager.memory.task.off-heap.size,默认为 0

检查是否显式配置了 taskmanager.memory.task.heap.size

  • 如果配置了

    • 计算 heap memory:从配置中获取 taskmanager.memory.task.heap.size
    • 计算 managed memory:先看 taskmanager.memory.managed.size ,如果未指定,则看 taskmanager.memory.managed.fraction(默认为 0.4):
      total flink memory * taskmanager.memory.managed.fraction,并限制在 0~Long.MAX_VALUE 之间。
    • 计算 network buffer memory:

      network buffer memory = total flink memory
                              - framework heap/off-heap memory 
                              - task heap/off-heap memory 
                              - managed memory
    • 检查 network buffer memory 是否符合配置规范,比如在 taskmanager.memory.network.mintaskmanager.memory.network.max 之间,且如果显式配置了 taskmanager.memory.network.fraction ,则与该比例需要一致。
  • 如果没配置

    • 计算 managed memory,和上述逻辑一致;
    • 计算 network buffer memory,和上述计算 managed memory 逻辑一致,唯一区别是 task.manager.memory.network.fraction 的默认值为 0.1;
    • 计算 heap memory:

      heap memory = total flink memory
                    - framework heap/off-heap memory 
                    - task off-heap memory 
                    - managed memory
                    - network buffer memory

3.6.2 deriveFromRequiredFineGrainedOptions

如果方法名描述得那样,进入该方法的前提是 taskmanager.memory.task.heap.size
taskmanager.memory.managed.size 均已显式配置。

  1. 先计算 heap memory 和 managed memory;
  2. 计算 framework heap memory 和 framework off-heap memory,默认为 128M;
  3. 计算 task off-heap memory,默认为 0;
  4. 检查是否显式配置了 taskmanager.memory.flink.size
  5. 如果配置了,计算

    network buffer memory = total flink memory 
                             - framework heap/off-heap memory 
                             - task heap/off-heap memory 
                             - managed memory

    并检查 network buffer memory 配置是否合理,见上文;

  6. 如果未配置,则优先通过配置计算 network buffer memory,再累加各部分内存得到 flink memory size。

4 内存配置优先级

从上述的计算逻辑可以看出来,大部分内存配置都有两种,一个是具体大小的配置,一个是比例配置(fraction),当同时指定二者时,会优先采用指定的大小(Size)。 若二者均未指定,会根据默认占比进行计算。

其次,内存的配置计算可以分为两类:

第一类,配置了多少就给多少,如果细粒度配置这部分给完计算发现超出总申请内存,则直接报错,这部分内存包含:

  • framework heap memory;
  • framework off-heap memory;
  • managed memory;
  • task off-heap memory;

第二类,根据配置和实际分配情况做一些微调:

  • task heap memory;
  • network memory。

如果配置 task heap memory,那么优先分配该块,剩下的内存分配给 network memory,否则则优先分配 network memory。


Mulavar
33 声望19 粉丝