本文根据Apache Flink系列直播整理而成,由Apache Flink贡献者、OPPO大数据平台研发负责人张俊老师分享,社区志愿者张友亮整理。主要内容包括:
网络流控是为了解决生产者和消费者的处理速度不匹配的问题。例如,如果生产者的传输速度为2MB/s,而消费者只有1MB/s的处理能力,那么数据将会积压在接收缓冲区中。如果缓冲区是有限的,新数据将被丢弃;如果是无限的,则可能会导致消费者内存耗尽。
为了解决速度不匹配的问题,一种常见的做法是在生产者端实施静态限速。例如,如果生产者以2MB/s的速度发送数据,通过限流机制,实际发送速率会降至1MB/s,从而与消费者的处理速率相匹配。
然而,这种方法有两个主要问题: - 事先难以确定消费者可以接受的最大处理速率 - 消费者的处理能力会动态变化
为解决静态限速的局限性,引入了动态反馈机制。这种方式允许消费者向生产者发送反馈,告知其可以接受的发送速率。根据反馈类型,可以分为负反馈(告知生产者降低发送速率)和正反馈(告知生产者提高发送速率)。
在Storm中,每个Bolt都有一个专门的线程监控反压情况。一旦检测到Bolt中的接收队列出现阻塞,该线程会将此状态记录在ZooKeeper中,Spout会据此暂停发送数据,从而实现生产者和消费者的速率匹配。
Spark Streaming通过实时收集目标节点的数据,再通过控制器将接收速度反馈给接收器,从而实现速率匹配。
Flink在V1.5之前的反压机制主要是通过TCP的流量控制来实现的。数据从Socket接收,每5秒执行一次WordCount,然后提交至集群。
在提交任务到集群之前,Client端会将StreamGraph转换为JobGraph,这是提交至集群的基本单元。在此过程中,会对任务进行优化,合并不需要Shuffle机制的节点。
JobGraph提交到集群后,生成ExecutionGraph,任务被分解为多个子任务。ExecutionGraph最终会被JobManager的调度器调度执行。
反压机制分为两个阶段: - 跨TaskManager的反压传播 - TaskManager内部的反压传播
发送数据通过ResultPartition进行,每个ResultPartition包含多个ResultSubPartition。每个TaskManager有一个共享的Network BufferPool,用于内存管理。当InputChannel的缓冲区满时,会向Local BufferPool请求更多内存,若Local BufferPool无法提供,则转向Network BufferPool。
如果下游Task(Sink)处理速度下降,InputChannel的缓冲区将被填满,导致向Local BufferPool请求内存失败,进而向Network BufferPool请求。当所有可用内存都被消耗完时,Netty会停止读取数据,最终导致发送端停止发送数据。
在TaskManager内部,下游TaskManager的反压导致本TaskManager的ResultSubPartition无法继续写入数据,RecordWriter的写操作会被阻塞。RecordReader也会停止从InputChannel读取数据,最终导致TaskManager的缓冲区耗尽。
在多个Task复用同一Socket的情况下,单个Task的反压会导致其他Task也无法使用该Socket,从而影响Checkpoint的正常执行。
Flink引入了信用反压机制,类似于TCP的窗口机制。每次ResultSubPartition向InputChannel发送数据时,都会告知下游准备发送的数据量。下游计算出所需的缓冲区数量后,返回一个信用额度给上游,告知其可以发送数据。通过这种方式,反压机制可以更快地反馈,减少了延迟。
尽管动态反压提供了更好的灵活性,但静态限速仍然有其应用场景。例如,在某些情况下,外部存储可能无法有效反馈反压,此时可以通过静态限速来避免数据过载。因此,选择适当的策略是关键。