Flink源码阅读(七) 内存管理

1,218 阅读5分钟

Flink 的内存管理采用了显式的内存管理并用序列化方式存储对象,同时支持 on-heap 和 off-heap。在这点上 Flink 与 Spark 的MEMORY_ONLY_SER存储级别十分相似,不同点在于 Spark 仍以 on-heap 对象存储为主,而 Flink 则只支持序列化的对象存储。

Flink 内存管理概览

Flink 内存主要指 TaskManager 运行时提供的内存资源。TaskManager 主要由几个内部组件构成:

负责和 JobManager 等进程通信的 actor 系统,负责在内存不足时将数据溢写到磁盘和读回的 IOManager,还有负责内存管理的 MemoryManager。其中 actor 系统和 MemoryManager 会要求大量的内存。相应地,FlinkTaskManager 的运行时 JVM heap 分为 Network BuffersMemoryManagerFree 三个区域(在 streaming 模式下只存在 Network BuffersFree 两个区域,因为算子不需要缓存一次读入的大量数据)。

参考Flink官方文档: Detailed Memory Model:
ci.apache.org/projects/fl…

memory.png

下表列出了上面描述的所有内存组件,并引用了影响各个组件大小的Flink配置选项:

成分  	         配置选项  	                       描述  

框架堆内存	taskmanager.memory.framework.heap.size	专用于Flink框架的JVM堆内存(高级选项)
任务堆内存	taskmanager.memory.task.heap.size	专用于Flink应用程序的JVM堆内存可运行算子和用户代码

托管内存         taskmanager.memory.managed.size
                taskmanager.memory.managed.fraction	    由Flink管理的本机内存,保留用于排序,哈希表,中间结果的缓存和RocksDB状态后端
框架堆外内存	taskmanager.memory.framework.off-heap.size	专用于Flink框架的堆外直接(或本机)内存(高级选项)
任务堆外内存	taskmanager.memory.task.off-heap.size	专用于Flink应用程序以运行操作员的堆外直接(或本机)内存
网络内存	        taskmanager.memory.network.min
                taskmanager.memory.network.max
                taskmanager.memory.network.fraction	直接存储器保留用于任务之间的数据记录交换(例如缓冲用于传输通过网络),
             它是一种在总Flink存储器中占用的网络内存阈值
JVM元空间	taskmanager.memory.jvm-metaspace.size	Flink JVM进程的元空间大小

JVM开销	        taskmanager.memory.jvm-overhead.min
                taskmanager.memory.jvm-overhead.max
                taskmanager.memory.jvm-overhead.fraction	架空其他JVM保留本机内存:如线程堆栈,代码缓存,垃圾收集等的空间,这是一个上限分级成分的的总进程内存
                
                

Flink网络内存传输

这篇文章官方的ververica已经删掉了,所以只好引用这条链接

memory.jpg


1.根据算子InputGate申请的buffers缓冲列表,判断当前buffer的数据是否可用
2.锁住当前算子需要将申请的内存批量添加进内存的buffers资源
3.根据协议报解析,跳过协议报首部。添加本次从Buffer中消费的BufferConsumer单位
4.更新本次消费buffer的状态信息
5.增加收集每个buffer中的checkpoint barrier
6.根据协议包的格式,每个申请的Buffer协议包首部会有一个固定的序列号。按照生成的序列号存进优先队列。根据优先级队列的消息编号通知下游消息消费
7.通知下游算子可以消费的消息

ResultSubPartition.java

   /**
     * 添加给定的缓冲区。
     *
     * <p>该请求可以同步执行,也可以异步执行,具体取决于
     * 执行。
     *
     * <p> <strong>重要</ strong>:在添加新的{@link BufferConsumer}之前,必须先添加
     *处于完成状态。由于性能原因,此操作仅在
     *数据读取。在先前的缓冲区使用者仍处于打开状态时,可以添加优先级事件,
     *在这种情况下,开放缓冲区使用者将被覆盖。
     *
     * @param buffer消费要添加的缓冲区(将所有权转让给该分区对象)
     * @parampartialRecordLength为了以一个完整的开头开始要跳过的字节的长度
     *从基础{@cite MemorySegment}的位置索引0开始记录。
     * @return如果操作成功并且将bufferConsumer排入消费队列,则返回true。
     * @throws IOException在添加缓冲区时发生错误的情况下抛出
     */
    public abstract boolean add(BufferConsumer bufferConsumer, int partialRecordLength)
            throws IOException;

534534534.png

PipelinedSubpartition.java


private boolean add(BufferConsumer bufferConsumer, int partialRecordLength, boolean finish) {
        checkNotNull(bufferConsumer);
        
        // 通知下游算子当前buffer的数据是否可用
        final boolean notifyDataAvailable;
        int prioritySequenceNumber = -1;
        // 锁住当前buffers单例
        synchronized (buffers) {
            if (isFinished || isReleased) {
                bufferConsumer.close();
                return false;
            }

            // Add the bufferConsumer and update the stats
            //根据协议报解析,跳过协议报首部
            // 添加本次从Buffer中消费的BufferConsumer单位
            if (addBuffer(bufferConsumer, partialRecordLength)) {
                prioritySequenceNumber = sequenceNumber;
            }
            
            //更新本次消费buffer的状态信息
            updateStatistics(bufferConsumer);
            //增加收集每个buffer中的checkpoint barrier
            increaseBuffersInBacklog(bufferConsumer);
            notifyDataAvailable = finish || shouldNotifyDataAvailable();

            isFinished |= finish;
        }

        if (prioritySequenceNumber != -1) {
            //根据优先级队列的消息编号通知下游消息消费
            notifyPriorityEvent(prioritySequenceNumber);
        }
        if (notifyDataAvailable) {
            //通知下游算子可以消费的消息
            notifyDataAvailable();
        }

        return true;
    }

BufferConsumer的定义为接受每个算子中传输过来的网络数据,根据协议报解析成Flink内部需要的格式

/**
 * 不是用于生成{@link Buffer}的线程安全类。
 *
 * <p>它读取{@link BufferBuilder}写入的数据。
 *尽管它不是线程安全的,并且只能由一个线程使用,但是此线程可以与线程不同。
 *使用/写入{@link BufferBuilder}的线程。模式在这里很简单:一个线程将数据写入
 * {@link BufferBuilder},使用{@link BufferConsumer}可以读取不同的线程。
 */
@NotThreadSafe
public class BufferConsumer implements Closeable {

}
/** 流水线式的仅内存中的子分区,可以使用一次。
 *
 * <p>每当{@link ResultSubpartition#add(BufferConsumer,boolean)}添加完成的{@link  BufferConsumer}或第二秒
 * {@link BufferConsumer}(在这种情况下,我们将假定第一个完成),我们将
 * {@link PipelinedSubpartitionView#notifyDataAvailable()通知}通过创建的读取视图
 * {@link ResultSubpartition#createReadView(BufferAvailabilityListener)}的新数据可用性。除非致电
 * {@link #flush()}总是只在第一个完成的缓冲区打开时通知,并且
 *然后,读取器必须通过{@link #pollBuffer()}清空缓冲区,直到显示其返回值
 *没有更多的缓冲区可用。这导致缓冲区队列为空或具有一个
 *剩余的{@link BufferConsumer}尚未完成,通知将最终从这些地方再次开始。
 *
 * <p>显式调用{@link #flush()}将强制执行此操作
 * {@link PipelinedSubpartitionView#notifyDataAvailable()通知}
 * {@link BufferConsumer}存在于队列中。
 
 public class PipelinedSubpartition extends ResultSubpartition
        implements CheckpointedResultSubpartition, ChannelStateHolder {
        
        
 private static final Logger LOG = LoggerFactory.getLogger(PipelinedSubpartition.class);

	// ------------------------------------------------------------------------

	/** 该子分区的所有缓冲区。对缓冲区的访问在此对象上同步。. */
	private final ArrayDeque<BufferConsumer> buffers = new ArrayDeque<>();

	/** 当前在此子分区中的非事件缓冲区的数量 */
	@GuardedBy("buffers")
	private int buffersInBacklog;

	/** 读取视图消耗此子分区. */
	private PipelinedSubpartitionView readView;

	/** 指示子分区是否已完成的标志. */
	private boolean isFinished;

	@GuardedBy("buffers")
	private boolean flushRequested;

	/** 指示子分区是否已释放的标志. */
	private volatile boolean isReleased;

	/** 缓冲区总数(数据缓冲区和事件缓冲区). */
	private long totalNumberOfBuffers;

	/** 字节总数(数据缓冲区和事件缓冲区). */
	private long totalNumberOfBytes;

	/** 缓冲区的集合,这些缓冲区由检查点barrier跨越,并且需要持久保存快照. */
	private final List<Buffer> inflightBufferSnapshot = new ArrayList<>();
        
}
/**
	 * 返回带有单独索引的保留副本。这允许从同一{@link MemorySegment}读取两次。
         *
         * <p>警告:新返回的{@link BufferConsumer}将从其原始缓冲区复制其阅读器索引。
         * 换句话说,在复制之前已经消耗的数据对于返回的副本将不可见。
	 *
	 * @return 具有单独索引的self的保留副本
	 */
	public BufferConsumer copy() {
		return new BufferConsumer(buffer.retainBuffer(), writerPosition.positionMarker, currentReaderPosition);
	}

Continue..QwQ