2022大数据面试总结

文章目录

Java

AQS

AQS主要分为两部分,

int 类型同步状态变量,通过cas 对其进行操作保证线程安全

同步队列,双端队列,FIFO ,用来存放在锁上阻塞的线程,当一个线程尝试获取锁时,如果已经被占用,那么当前线程就会呗构造成一个Node 节点加入到同步队列的尾部,队列的头节点事成功获取锁的节点,当头节点线程释放锁时,会唤醒后面的节点并释放当前头节点的引用,支持公平非公平,差别是直接插入队列,还是先竞争锁,竞争失败外插入队列

1. jvm调优思路

jvm调优思路

2 JVM 调试实战 - 网站流量浏览量暴增后,网站反应页面响很慢

  1. 问题推测:在测试环境测速度比较快,但是一到生产就变慢,所以推测可能是因为垃圾收集导致的业务线程停顿。

  2. 定位:为了确认推测的正确性,在线上通过 jstat -gc 指令 看到 JVM 进行 GC 次数频率非常高,GC 所占用的时间非常长,所以基本推断就是因为 GC 频率非常高,所以导致业务线程经常停顿,从而造成网页反应很慢。

  3. 解决方案:因为网页访问量很高,所以对象创建速度非常快,导致堆内存容易填满从而频繁 GC,所以这里问题在于新生代内存太小,所以这里可以增加 JVM 内存就行了,所以初步从原来的 2G 内存增加到 16G 内存。

  4. 第二个问题:增加内存后的确平常的请求比较快了,但是又出现了另外一个问题,就是不定期的会间断性的卡顿,而且单次卡顿的时间要比之前要长很多。

  5. 问题推测:之前的优化加大了内存,所以推测可能是因为内存加大了,从而导致单次 GC 的时间变长从而导致间接性的卡顿。

  6. 定位:还是通过 jstat -gc 指令 查看到 的确 FGC 次数并不是很高,但是花费在 FGC 上的时间是非常高的,根据 GC 日志 查看到单次 FGC 的时间有达到几十秒的。

  7. 解决方案:因为 JVM 默认使用的是 PS+PO 的组合,PS+PO 垃圾标记和收集阶段都是 STW,所以内存加大了之后,需要进行垃圾回收的时间就变长了,所以这里要想避免单次 GC 时间过长,所以需要更换并发类的收集器,因为当前的 JDK 版本为 1.7,所以最后选择 CMS 垃圾收集器,根据之前垃圾收集情况设置了一个预期的停顿的时间,上线后网站再也没有了卡顿问题。

G1可以通过设置-XX:MaxGCPauseMills来设置用户允许的停顿时间(默认为200ms)

3. synchronized实现原理

当一个线程试图访问同步代码块时,他首先必须得到锁,退出或抛出异常时必须释放锁。那么锁到底存在哪里呢?锁里面会存储什么信息呢?

从JVM规范中可以看到Synchronized在JVM里的实现原理,JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,但两者的实现细节不一样。代码块同步是使用monitorenter和monitorexit指令实现的,而方法同步是使用另外一种方式实现的,细节在JVM规范里并没有详细说明。但是,方法的同步同样可以使用这两个指令来实现。

monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处,JVM要保证每个monitorenter必须有对应的monitorexit与之配对。任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁。

上面太简单了,主要看这个博客:
https://blog.csdn.net/qq_24095055/article/details/105452404

3. Synchronized的可重入的实现机理

每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。

当执行 monitorenter 时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1。

在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么Java虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁。

当执行 monitorexit 时,Java虚拟机则需将锁对象的计数器减1。计数器为零代表锁已被释放。

4. JVM如何隔离线程?

可以通过ThreadLocal实现线程隔离

线程隔离流程

ThreadLocal类中的静态内部类ThreadLocalMap,这个Map是ThreadLocal实现线程隔离的精髓。

Thread类中有这样子一个成员变量:

/* 与此线程相关的ThreadLocalMap */
ThreadLocal.ThreadLocalMap threadLocals = null;

因此,一个线程对应有自己单独的一个ThreadLocalMap。所以ThreadLocal才可以实现线程隔离。

使用注意

我们使用ThreadLocal为不同线程分配副本,副本并不是指同一个对象,因此倘若为不同的线程set()同一个对象的引用,还是无法避免导致不同线程都对该对象进行了操作,因此我们分配的副本就是不同的对象。ThreadLocal的最大意义就在于它实现了将副本写进了Thread对象内部的Threadlocals 这个映射对象(它是一个ThreadLocal.ThreadLocalMap类对象),我们的set、get方法都只能从当前线程中设置或者获得副本。

5. Java如何使用堆外内存

在JAVA中,JVM内存指的是堆内存。

机器内存中,不属于堆内存的部分即为堆外内存。

堆外内存也被称为直接内存。

在JAVA中,可以通过Unsafe和NIO包下的ByteBuffer来操作堆外内存。

参考:https://www.jianshu.com/p/17e72bb01bf1

6. volatile的作用?重排序的原理?用到的指令集?

多线程下的可见性&防止重排序

在这里插入图片描述
在这里插入图片描述

7. threadlocal为什么存储使用弱引用?它存储用的map和hashmap的区别?为什么这么用?

在这里插入图片描述
ThreadLocal解决的问题:即让每个线程都有属于自己的本地资源,避免多线程之间的共享。

ThreadLocal 和线程的关系如下图所示:
在这里插入图片描述
每个线程内部有个 threadLocalMap,map 里面存储的 key 是 threadLocal 对象,这样调用 threadLocal .get就可以根据当前线程找到本地的 map ,然后根据调用的 threadLocal 对象找到对应的 value。
在这里插入图片描述
在实现上 threadLocalMap 是一个由 Entry 对象组成的数组,Entry 对 key 的引用为弱引用,对 value 的引用为强引用。
在这里插入图片描述
整体相关的对象引用链如下:
在这里插入图片描述
基于上面的图,我们来看看谈到 threadLocal 经常说的内存泄漏指的是什么。

由于线程池内线程生命周期较长,所以图中下方的那条强引用链会一直存在,而图上方的强引用链随着方法的调用结束出栈之后就不复存在了,所以当前的 threadLocal 对象只有一条弱引用存在(key的弱引用)。

如果发生 gc ,在内存不足的时候 threadLocal 对象就会被回收,这样就会残留无用的 Entry 在线程对象中(key都没了,根本访问不到 value,所以无用)。这就是所说的内存泄漏(残留了无用的 Entry 无法回收)。

那既然会有内存泄漏为什么还这样实现?

就是因为 key 对 threadLocal 对象之间是弱引用,这样在栈上没有 threadLocal 引用这个强引用之后(你可以认为之后不会在用这个 threadlocal 对象),threadLocal 对象才得以被回收。

如果 key 对 threadLocal 对象之间是强引用,那么无用的 threadLocal 对象就无法被回收了,这其实造成了更大的内存泄漏。

设计者知晓会出现这种情况,所以在多个地方都做了清理无用 Entry 的操作。

比如通过 key 查找 Entry 的时候,如果下标无法直接命中,那么就会向后遍历数组,此时遇到 key 为 null 的 Entry 就会清理掉,还有扩容的时候,等等。

当然,最佳实践是在用完之后就手动把它 remove 掉,这样就避免了内存泄漏的存在。

void yesDosth {
 threadlocal.set(xxx);
 try {
  // do sth
 } finally {
  threadlocal.remove(); //手动清理
 }
}

参考:https://mp.weixin.qq.com/s/bECVeuxE-WIYmvXbF2V3QA

强引用 > 软引用 > 弱引用 > 虚引用

8. 异步IO-Netty

9. 设计模式了解哪些,有哪些使用场景?

单例模式

  • 全局复用一个数据库连接池,可以通过单例模式来获取。
  • 任何多线程共享实例的场景,如pv、uv的统计。
  • 线程池的获取和使用。

工厂模式

  • 日志使用,通过LoggerFactory.getLogger(yourClass.class)来获取类的日志对象
  • 特征生成根据特征的大类别调用不同的工厂方法生成不同的传参实例
    • FeatureModelFactory.generateModel();
    • FeatureModelFactory.generateBehaviorModel();
  • 同上生成对应的rpc client service
    • MotanClientFactory.featureServiceInstance
    • MotanClientFactory.behaviorFeatureServiceInstance

操作系统&计算机网络等

1. 计算机网络-TCP传包/滑动窗口机制

计算机网络-传输层

2. 进程、线程和协程有什么区别

https://blog.csdn.net/qq_24095055/article/details/128178648

Flink

1. flink有几种部署模式?分别是?

三种

  • 会话模式(Session Mode)
  • 单作业模式(Per-Job Mode)
  • 应用模式(Application Mode)

会话模式

先启动一个集群,保持一个会话,在这个会话中通过客户端提交作业。集群启动时所有资源就都已经确定,所有提交的作业会竞争集群中的资源。

这样的好处很明显,我们只需要一个集群,就像一个大箱子,所有的作业提交之后都塞进去;集群的生命周期是超越于作业之上的,铁打的营盘流水的兵,作业结束了就释放资源,集群依然正常运行。当然缺点也是显而易见的:因为资源是共享的,所以资源不够了,提交新的作业就会失败。另外,同一个 TaskManager 上可能运行了很多作业,如果其中一个发生故障导致 TaskManager 宕机,那么所有作业都会受到影响。

会话模式比较适合于单个规模小、执行时间短的大量作业。

单作业模式(Per-Job Mode)

会话模式因为资源共享会导致很多问题,所以为了更好地隔离资源,我们可以考虑为每个提交的作业启动一个集群,这就是所谓的单作业(Per-Job)模式

单作业模式也很好理解,就是严格的一对一,集群只为这个作业而生。同样由客户端运行应用程序,然后启动集群,作业被提交给 JobManager,进而分发给 TaskManager 执行。作业完成后,集群就会关闭,所有资源也会释放。这样一来,每个作业都有它自己的 JobManager 管理,占用独享的资源,即使发生故障,它的 TaskManager 宕机也不会影响其他作业。这些特性使得单作业模式在生产环境运行更加稳定,所以是实际应用的首选模式。需要注意的是,Flink 本身无法直接这样运行,所以单作业模式一般需要借助一些资源管理框架来启动集群,比如 YARN、Kubernetes。

应用模式(Application Mode)

前面两种模式应用代码都是在客户端上执行,然后由客户端提交给 JobManager 的。但是这种方式客户端需要占用大量网络带宽,去下载依赖和把二进制数据发送给 JobManager;加上很多情况下我们提交作业用的是同一个客户端,就会加重客户端所在节点的资源消耗。

所以解决办法就是,我们不要客户端了,直接把应用提交到 JobManger 上运行。而这也就代表着,我们需要为每一个提交的应用单独启动一个 JobManager,也就是创建一个集群。这个 JobManager 只为执行这一个应用而存在,执行结束之后 JobManager 也就关闭了,这就是所谓的应用模式

应用模式与单作业模式,都是提交作业之后才创建集群;单作业模式是通过客户端来提交的,客户端解析出的每一个作业对应一个集群;而应用模式下,是直接由 JobManager 执行应用程序的,并且即使应用包含了多个作业,也只创建一个集群。

总结一下

  • 在会话模式下,集群的生命周期独立于集群上运行的任何作业的生命周期,并且提交的所有作业共享资源。
  • 单作业模式为每个提交的作业创建一个集群,带来了更好的资源隔离,这时集群的生命周期与作业的生命周期绑定。
  • 应用模式为每个应用程序创建一个会话集群,在 JobManager 上直接调用应用程序的 main()方法。

我们所讲到的部署模式,相对是比较抽象的概念。实际应用时,一般需要和资源管理平台结合起来,选择特定的模式来分配资源、部署应用。

2. 为什么使用YARN?YARN 平台上 Flink 是如何集成部署的?

独立(Standalone)模式由 Flink 自身提供资源,无需其他框架,这种方式降低了和其他第三方资源框架的耦合性,独立性非常强。但我们知道,Flink 是大数据计算框架,不是资源调度框架,这并不是它的强项;所以还是应该让专业的框架做专业的事,和其他资源调度框架集成更靠谱。而在目前大数据生态中,国内应用最为广泛的资源管理平台就是 YARN 了。所以接下来我们就将学习,在强大的 YARN 平台上 Flink 是如何集成部署的。

整体来说,YARN 上部署的过程是:客户端把 Flink 应用提交给 Yarn 的 ResourceManager, Yarn 的 ResourceManager 会向 Yarn 的 NodeManager 申请容器。在这些容器上,Flink 会部署 JobManager 和 TaskManager 的实例,从而启动集群。Flink 会根据运行在 JobManger 上的作业所需要的 Slot 数量动态分配 TaskManager 资源。

在 Flink1.8.0 之前的版本,想要以 YARN 模式部署 Flink 任务时,需要 Flink 是有 Hadoop 支持的。从 Flink 1.8 版本开始,不再提供基于 Hadoop 编译的安装包,若需要 Hadoop 的环境支持,需要自行在官网下载 Hadoop 相关版本的组件 flink-shaded-hadoop-2-uber-2.7.5-10.0.jar,并将该组件上传至 Flink 的 lib 目录下。在 Flink 1.11.0 版本之后,增加了很多重要新特性,其中就包括增加了对Hadoop3.0.0以及更高版本Hadoop的支持,不再提供“flink-shaded-hadoop-*”jar 包,而是通过配置环境变量完成与 YARN 集群的对接。

在将 Flink 任务部署至 YARN 集群之前,需要确认集群是否安装有 Hadoop,保证 Hadoop版本至少在 2.2 以上,并且集群中安装有 HDFS 服务。

3. Flink ON YARN 如何保障高可用

YARN 模式的高可用和独立模式(Standalone)的高可用原理不一样。

  • Standalone 模式中, 同时启动多个 JobManager, 一个为“领导者”(leader),其他为“后备”(standby), 当 leader 挂了, 其他的才会有一个成为 leader。
  • 而 YARN 的高可用是只启动一个 Jobmanager, 当这个 Jobmanager 挂了之后, YARN 会再次启动一个, 所以其实是利用的 YARN 的重试次数来实现的高可用。

(1)在 yarn-site.xml 中配置

<property>
  <name>yarn.resourcemanager.am.max-attempts</name>
  <value>4</value>
  <description>
    The maximum number of application master execution attempts.
  </description>
</property>

注意: 配置完不要忘记分发, 和重启 YARN。

(2)在 flink-conf.yaml 中配置。

yarn.application-attempts: 3
high-availability: zookeeper
high-availability.storageDir: hdfs://hadoop102:9820/flink/yarn/ha
high-availability.zookeeper.quorum: 
hadoop102:2181,hadoop103:2181,hadoop104:2181
high-availability.zookeeper.path.root: /flink-yarn

(3)启动 yarn-session。
(4)杀死 JobManager, 查看复活情况。

注意: yarn-site.xml 中配置的是 JobManager 重启次数的上限, flink-conf.xml 中的次数应该小于这个值。

4. 详细说说flink exactly once实现原理

5. flink数据重分区方式

当上下游算子并行度不一样时,默认的数据传递方式是rebalance,当下游算子并行度一样时,默认的数据传递方式是forward。

1. keyBy

keyBy :DataStream -> KeyedStream,按照key的hashcode将一个流划分为不相交的分区。具有相同 Keys 的所有记录在同一分区
在这里插入图片描述
2. broadcast

broadcast :DataStream -> DataStream,给下游算子所有的subtask都广播一份数据

举例:设置并行度为3,打印broadcast下游subtask的数据

StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(3);

DataStream<String> dataStream = env.fromElements("hello", "world", "flink");
DataStream<String> broadcast = dataStream.broadcast();

broadcast.print();
env.execute();

执行输出:

3> hello
3> world
3> flink
2> hello
2> world
2> flink
1> hello
1> world
1> flink

并行度为3,每一个算子都有三个subtask,所以经过broadcast后,下游每一个subtask就会接收到所有上游任务发送过来的数据。

3. rebalance

rebalance :DataStream -> DataStream,随机轮询发送数据

举例来说,假如A算子作为上游算子,有3个SubTask,并行度为3;下游B算子,有2个SubTask,并行度为2,数据传递方式是rebalance。数据具体传递形式:首先生成一个随机数,决定第一个数据发往下游的哪个subtask,假如生成随机是i,下游的任务数是n,则A的SubTask1中第一个数据发送到B的第(i+1)%n的subtask,执行i=(i+1)%n, A的SubTask1第二个数据发送到B的第(i+1)%n的subtask,i=(i+1)%n,从而轮询发送数据。同理,A的SubTask2也是如此。

当上下游算子并行度不一样时,默认的数据传递方式是rebalance,当下游算子并行度一样时,默认的数据传递方式是forward。

forward也是flink中的算子,因为它只是让数据在当前的分区进行上下游传递,并没有进行shuffle,所以不属于shuffle类的算子。

4. rescale

rescale :DataStream -> DataStream,重新分组,在组内进行rebalance(轮询),数据传输的范围小一点

如下图所示,假如上游有2个分区(即两个subtask),下游4个分区,rebalance是让每一个上游subtask对下游轮询发送数据,而rescale是将上下游分区的任务平均划分为2组,在每个分组内rebalance发送数据。
在这里插入图片描述
5. shuffle

shuffle :DataStream -> DataStream,完全随机发送数据,也就是说,上游任务发送给下游任务的数据是随机发送的

6. global

global :DataStream -> DataStream,数据传递给下游第一个分区(或下游第一个slot或下游算子的第一个并行子任务),一般将所有数据汇总在一起时使用

7. partitionCustom

partitionCustom :DataStream -> DataStream,用户自定义重分区方式

参考:https://blog.csdn.net/qq_37555071/article/details/122415430

6. 为什么使用flink 1.13,新特性?优化点?

  1. Flink 1.13带来了一个改进的背压度量系统(使用任务邮箱计时而不是线程堆栈采样),以及一个重新设计的作业数据流图形表示,用颜色编码和繁忙度和背压比率表示。可以根据flink ui的dag中每个task的颜色和反压程度比例快速定位性能瓶颈节点。
  2. 提供了CPU火焰图,可以快速查看当前哪些方法在消耗CPU的资源?各个方法消耗的CPU的资源的多少对比?堆栈上的哪些调用会导致执行特定的方法?
    rest.flamegraph.enabled: true
    
  3. 响应式伸缩???
  4. 优化了大规模作业调度以及批执行模式下网络 Shuffle 的性能,从而进一步提高了流作业与批作业的执行性能
  5. 最主要的还是flink sql的优化和性能提升
  6. flink ui上 tm jm 各个部分内存使用情况展示更直观友好

参考:https://blog.csdn.net/lihengzkj/article/details/116633389

7. flink on yarn搭建过程说一下?

以下是 Flink 1.13.1 在 YARN 上的搭建过程的主要步骤:

  1. 安装 Hadoop 和 YARN
  2. 下载 Flink 二进制发布版或源代码
  3. 配置 Flink-conf.yaml 文件,设置 YARN 集群相关参数
  4. 编译 Flink 源代码(如果下载的是源代码)
  5. 启动 Flink on YARN
  6. 提交 Flink 应用程序
  7. 监控 Flink on YARN 的应用程序

8. 应用(Application)模式了解吗

应用模式与单作业模式的提交流程非常相似,只是初始提交给 YARN 资源管理器的不再是具体的作业,而是整个应用。一个应用中可能包含了多个作业,这些作业都将在 Flink 集群中启动各自对应的 JobMaster。

9. Flink重复注册定时器会发生什么

如果在processElement方法中没处理一条数据,都注册定时器,不会导致定时任务重复调用吗?

答案是不会,因为Flink内部使用的 HeapPriorityQueueSet 来存储定时器,一个注册请求到来时,其add()方法会检查是否已经存在,如果存在则不会加入。

可以去看registerProcessTimeTimer源码进行佐证。

10. 分析一下flink四层调度图的演变过程

在这里插入图片描述
1. 逻辑流图(StreamGraph)

这是根据用户通过 DataStream API 编写的代码生成的最初的 DAG 图,用来表示程序的拓扑结构。这一步一般在客户端完成。

我们可以看到,逻辑流图中的节点,完全对应着代码中的四步算子操作:

源算子 Source(socketTextStream())→扁平映射算子 Flat Map(flatMap()) →分组聚合算子 Keyed Aggregation(keyBy/sum()) →输出算子 Sink(print())。

2. 作业图(JobGraph)

StreamGraph 经过优化后生成的就是作业图(JobGraph),这是提交给 JobManager 的数据结构,确定了当前作业中所有任务的划分。主要的优化为: 将多个符合条件的节点链接在一起合并成一个任务节点,形成算子链,这样可以减少数据交换的消耗。JobGraph 一般也是在客户端生成的,在作业提交时传递给 JobMaster。

在图 4-12 中,分组聚合算子(Keyed Aggregation)和输出算子 Sink(print)并行度都为 2,而且是一对一的关系,满足算子链的要求,所以会合并在一起,成为一个任务节点。

3. 执行图(ExecutionGraph)

JobMaster 收到 JobGraph 后,会根据它来生成执行图(ExecutionGraph)。ExecutionGraph 是 JobGraph 的并行化版本,是调度层最核心的数据结构。

从图 4-12 中可以看到,与 JobGraph 最大的区别就是按照并行度对并行子任务进行了拆分,并明确了任务间数据传输的方式。

4. 物理图(Physical Graph)

JobMaster 生成执行图后, 会将它分发给 TaskManager;各个 TaskManager 会根据执行图部署任务,最终的物理执行过程也会形成一张“图”,一般就叫作物理图(Physical Graph)。这只是具体执行层面的图,并不是一个具体的数据结构。

对应在上图 4-12 中,物理图主要就是在执行图的基础上,进一步确定数据存放的位置和收发的具体方式。有了物理图,TaskManager 就可以对传递来的数据进行处理计算了。

所以我们可以看到,程序里定义了四个算子操作:源(Source)->转换(flatMap)->分组聚合(keyBy/sum)->输出(print);合并算子链进行优化之后,就只有三个任务节点了;再考虑并行度后,一共有 5 个并行子任务,最终需要 5 个线程来执行。

11. flink 如何处理乱序数据

简单总结:使用 eventTime + watermark + 时间窗口 配合解决。

时间时间eventTime + watermark

flink中由于有乱序数据,我们需要设置一个延迟时间来等所有数据到齐。比如下图的例子中,我们可以设置延迟时间为 2 秒,这样 0~10 秒的窗口会在时间戳为 12 的数据到来之后,才真正关闭计算输出结果,这样就可以正常包含迟到的 9 秒数据了。
在这里插入图片描述
但是这样一来,0~10 秒的窗口不光包含了迟到的 9 秒数据,连 11 秒和 12 秒的数据也包含进去了。我们为了正确处理迟到数据,结果把早到的数据划分到了错误的窗口——最终结果都是错误的。

所以在 Flink 中,窗口其实并不是一个“框”,流进来的数据被框住了就只能进这一个窗口。相比之下,我们应该把窗口理解成一个“桶”,如下图所示。在 Flink 中,窗口可以把流切割成有限大小的多个“存储桶”(bucket);每个数据都会分发到对应的桶中,当到达窗口结束时间时,就对每个桶中收集的数据进行计算处理。
在这里插入图片描述
我们可以梳理一下事件时间语义下,之前例子中窗口的处理过程:

  1. 第一个数据时间戳为 2,判断之后创建第一个窗口[0, 10),并将 2 秒数据保存进去;
  2. 后续数据依次到来,时间戳均在 [0, 10)范围内,所以全部保存进第一个窗口;
  3. 11 秒数据到来,判断它不属于[0, 10)窗口,所以创建第二个窗口[10, 20),并将 11 秒的数据保存进去。由于水位线设置延迟时间为 2 秒,所以现在的时钟是 9 秒,第一个窗口也没有到关闭时间;
  4. 之后又有 9 秒数据到来,同样进入[0, 10)窗口中;
  5. 12 秒数据到来,判断属于[10, 20)窗口,保存进去。这时产生的水位线推进到了 10秒,所以 [0, 10)窗口应该关闭了。第一个窗口收集到了所有的 7 个数据,进行处理计算后输出结果,并将窗口关闭销毁;
  6. 同样的,之后的数据依次进入第二个窗口,遇到 20 秒的数据时会创建第三个窗口[20, 30)并将数据保存进去;遇到 22 秒数据时,水位线达到了 20 秒,第二个窗口触发计算,输出结果并关闭。

这里需要注意的是,Flink 中窗口并不是静态准备好的,而是动态创建——当有落在这个窗口区间范围的数据达到时,才创建对应的窗口。另外,这里我们认为到达窗口结束时间时,窗口就触发计算并关闭,事实上“触发计算”和“窗口关闭”两个行为也可以分开。

12. TaskManager 内存模型

在这里插入图片描述
内存模型详解

  • JVM 特定内存:JVM 本身使用的内存,包含 JVM 的 metaspaceover-head
    1. JVM metaspace:JVM 元空间
      taskmanager.memory.jvm-metaspace.size,默认 256mb
    2. JVM over-head 执行开销:JVM 执行时自身所需要的内容,包括线程堆栈、IO、编译缓存等所使用的内存。
      taskmanager.memory.jvm-overhead.fraction,默认 0.1
      taskmanager.memory.jvm-overhead.min,默认 192mb
      taskmanager.memory.jvm-overhead.max,默认 1gb
      总进程内存*fraction,如果小于配置的 min(或大于配置的 max)大小,则使用 min/max 大小
  • 框架内存:Flink 框架,即 TaskManager 本身所占用的内存,不计入 Slot 的资源中
    1. 堆内:taskmanager.memory.framework.heap.size,默认 128MB
    2. 堆外:taskmanager.memory.framework.off-heap.size,默认 128MB
  • Task 内存:Task 执行用户代码时所使用的内存
    1. 堆内:taskmanager.memory.task.heap.size,默认 none,由 Flink 内存扣除掉其他部分的内存得到。
    2. 堆外:taskmanager.memory.task.off-heap.size,默认 0,表示不使用堆外内存
  • 网络内存:网络数据交换所使用的堆外内存大小,如网络数据交换缓冲区
    1. 堆外:taskmanager.memory.network.fraction,默认 0.1
    2. taskmanager.memory.network.min,默认 64mb
    3. taskmanager.memory.network.max,默认 1gb
    4. Flink 内存*fraction,如果小于配置的 min(或大于配置的 max)大小,则使用 min/max 大小
  • 托管内存:用于 RocksDB State Backend 的本地内存和批的排序、哈希表、缓存中间结果。
    1. 堆外:taskmanager.memory.managed.fraction,默认 0.4
    2. taskmanager.memory.managed.size,默认 none
    3. 如果 size 没指定,则等于 Flink 内存*fraction

案例分析

基于Yarn模式,一般参数指定的是总进程内存,taskmanager.memory.process.size,比如指定为 4G,每一块内存得到大小如下:

(1)计算 Flink 内存

  • JVM 元空间 256m
  • JVM 执行开销: 4g*0.1=409.6m,在[192m,1g]之间,最终结果 409.6m
  • Flink 内存=4g-256m-409.6m=3430.4m

(2)网络内存=3430.4m*0.1=343.04m,在[64m,1g]之间,最终结果 343.04m
(3)托管内存=3430.4m*0.4=1372.16m
(4)框架内存,堆内和堆外都是 128m
(5)Task 堆内内存=3430.4m-128m-128m-343.04m-1372.16m=1459.2m

在这里插入图片描述

上面JVM Options 为 TM 中打印的日志,也说明了内存分配是正确的。

那JobManager呢

JobManager一般我们都是默认给分配2G内存,这个我们一般不太关注,因为它不会处理数据也不会存储数据。

13. 你们的flink作业如何分配资源?

开发完成后,先进行压测。任务并行度给 10 以下,测试单个并行度的处理上限。然后总 QPS/单并行度的处理能力 = 并行度。

开发完 Flink 作业,压测的方式很简单,先在 kafka 中积压数据,之后开启 Flink 任务,出现反压,就是处理瓶颈。相当于水库先积水,一下子泄洪。

不能只从 QPS 去得出并行度,因为有些字段少、逻辑简单的任务,单并行度一秒处理
几万条数据。而有些数据字段多,处理逻辑复杂,单并行度一秒只能处理 1000 条数据。
最好根据高峰期的 QPS 压测,并行度*1.2 倍,富余一些资源。

Source 端并行度的配置

数据源端是 Kafka,Source 的并行度设置为 Kafka 对应 Topic 的分区数。sink是kafka也一样。

Transform 端并行度的配置

Keyby 之前的算子

一般不会做太重的操作,都是比如 map、filter、flatmap 等处理较快的算子,并行度可以和 source 保持一致。

Keyby 之后的算子

如果并发较大,建议设置并行度为 2 的整数次幂,例如:128、256、512;小并发任务的并行度不一定需要设置成 2 的整数次幂;大并发任务如果没有 KeyBy,并行度也无需设置为 2 的整数次幂;(也不绝对,不是强制要求)

14. RocksDB 大状态调优

RocksDB 是基于 LSM Tree 实现的(类似 HBase),写数据都是先缓存到内存中,所以 RocksDB 的写请求效率比较高。RocksDB 使用内存结合磁盘的方式来存储数据,每次获取数据时,先从内存中 blockcache 中查找,如果内存中没有再去磁盘中查询。使用 RocksDB 时,状态大小仅受可用磁盘空间量的限制,性能瓶颈主要在于 RocksDB 对磁盘的读请求,每次读写操作都必须对数据进行反序列化或者序列化。当处理性能不够时,仅需要横向扩展并行度即可提高整个 Job 的吞吐量。
在这里插入图片描述
从 Flink1.10 开始,Flink 默认将 RocksDB 的内存大小配置为每个 task slot 的托管内存。调试内存性能的问题主要是通过调整配置项 taskmanager.memory.managed.size 或者 taskmanager.memory.managed.fraction 以增加 Flink 的托管内存(即堆外的托管内存)。进一步可以调整一些参数进行高级性能调优,这些参数也可以在应用程序中通过 RocksDBStateBackend.setRocksDBOptions(RocksDBOptionsFactory)指定。下面介绍提高资源利用率的几个重要配置:

开启 State 访问性能监控

Flink 1.13 中引入了 State 访问的性能监控,即 latency trackig state。此功能不局限于 State Backend 的类型,自定义实现的 State Backend 也可以复用此功能。开启后可以看到状态读取的耗时metrics。

State 访问的性能监控会产生一定的性能影响,所以,默认每 100 次做一次取样 (sample),对不同的 State Backend 性能损失影响不同:

  • 对于 RocksDB State Backend,性能损失大概在 1% 左右
  • 对于 Heap State Backend,性能损失最多可达 10%
state.backend.latency-track.keyed-state-enabled:true #启用访问状态的性能监控
state.backend.latency-track.sample-interval: 100 #采样间隔
state.backend.latency-track.history-size: 128 #保留的采样数据个数,越大越精确
state.backend.latency-track.state-name-as-variable: true #将状态名作为变量

正常开启第一个参数即可。

bin/flink run \
	-t yarn-per-job \
	-d \
	-p 5 \
	-Drest.flamegraph.enabled=true \
	-Dyarn.application.queue=test \
	-Djobmanager.memory.process.size=1024mb \
	-Dtaskmanager.memory.process.size=4096mb \
	-Dtaskmanager.numberOfTaskSlots=2 \
	-Dstate.backend.latency-track.keyed-state-enabled=true \ # 开启State 访问性能监控在这
	-c com.atguigu.flink.tuning.RocksdbTuning \
	/opt/module/flink-1.13.1/myjar/flink-tuning-1.0-SNAPSHOT.jar

这个metrics的值单位是ns(纳秒),除去1000000才是ms(毫秒)。
1ms = 1000微秒 = 1000000纳秒

开启增量检查点和本地恢复

1)开启增量检查点

RocksDB 是目前唯一可用于支持有状态流处理应用程序增量检查点的状态后端,可以修改参数开启增量检查点:

state.backend.incremental: true #默认 false,改为 true。
或代码中指定
new EmbeddedRocksDBStateBackend(true)

2)开启本地恢复

当 Flink 任务失败时,可以基于本地的状态信息进行恢复任务,可能不需要从 hdfs 拉取数据。本地恢复目前仅涵盖键控类型的状态后端(RocksDB),MemoryStateBackend 不支持本地恢复并忽略此选项。

state.backend.local-recovery: true

3)设置多目录

如果有多块磁盘,也可以考虑指定本地多目录

state.backend.rocksdb.localdir: /data1/flink/rocksdb,/data2/flink/rocksdb,/data3/flink/rocksdb

注意:不要配置单块磁盘的多个目录,务必将目录配置到多块不同的磁盘上,让多块磁盘来分担压力。

bin/flink run \
	-t yarn-per-job \
	-d \
	-p 5 \
	-Drest.flamegraph.enabled=true \
	-Dyarn.application.queue=test \
	-Djobmanager.memory.process.size=1024mb \
	-Dtaskmanager.memory.process.size=4096mb \
	-Dtaskmanager.numberOfTaskSlots=2 \
	-Dstate.backend.incremental=true \ # 开启增量检查点
	-Dstate.backend.local-recovery=true \ # 开启优先本地恢复
	-Dstate.backend.latency-track.keyed-state-enabled=true \ # 开启state访问性能监控
	-c com.atguigu.flink.tuning.RocksdbTuning \
	/opt/module/flink-1.13.1/myjar/flink-tuning-1.0-SNAPSHOT.jar

调整预定义选项

Flink 针对不同的设置为 RocksDB 提供了一些预定义的选项集合,其中包含了后续提到的一些参数,如果调整预定义选项后还达不到预期,再去调整后面的 block、writebuffer 等参数。

当 前 支 持 的 预 定 义 选 项 有 DEFAULT 、 SPINNING_DISK_OPTIMIZED 、 SPINNING_DISK_OPTIMIZED_HIGH_MEM 或 FLASH_SSD_OPTIMIZED。有条件上 SSD 的,可以指定为 FLASH_SSD_OPTIMIZED

#设置为机械硬盘+内存模式
state.backend.rocksdb.predefined-options: SPINNING_DISK_OPTIMIZED_HIGH_MEM

增大 block 缓存

整个 RocksDB 共享一个 block cache,读数据时内存的 cache 大小,该参数越大读数据时缓存命中率越高,默认大小为 8 MB,建议设置到 64 ~ 256 MB。

state.backend.rocksdb.block.cache-size: 64m #默认 8m

增大 write buffer 和 level 阈值大小

RocksDB 中,每个 State 使用一个 Column Family,每个 Column Family 使用独占的 write buffer,默认 64MB,建议调大。

调整这个参数通常要适当增加 L1 层的大小阈值 max-size-level-base,默认 256m。该值太小会造成能存放的 SST 文件过少,层级变多造成查找困难,太大会造成文件过多,合并困难。建议设为 target_file_size_base(默认 64MB)的倍数,且不能太小,例如 5~10 倍,即 320~640MB。

state.backend.rocksdb.writebuffer.size: 128m
state.backend.rocksdb.compaction.level.max-size-level-base: 320m

增大 write buffer 数量

每个 Column Family 对应的 writebuffer 最大数量,这实际上是内存中“只读内存表“的最大数量,默认值是 2。对于机械磁盘来说,如果内存足够大,可以调大到 5 左右

state.backend.rocksdb.writebuffer.count: 5

增大后台线程数和 write buffer 合并数

1)增大线程数
用于后台 flush 和合并 sst 文件的线程数,默认为 1,建议调大,机械硬盘用户可以改为 4 等更大的值

state.backend.rocksdb.thread.num: 4

flink 1.13 版本 默认值已经改为4了。

2)增大 writebuffer 最小合并数
将数据从 writebuffer 中 flush 到磁盘时,需要合并的 writebuffer 最小数量,默认值为 1,可以调成 3。

state.backend.rocksdb.writebuffer.number-to-merge: 3

开启分区索引功能

Flink 1.13 中对 RocksDB 增加了分区索引功能,复用了 RocksDB 的 partitioned Index & filter 功能,简单来说就是对 RocksDB 的 partitioned Index 做了多级索引。

也就是将内存中的最上层常驻,下层根据需要再 load 回来,这样就大大降低了数据 Swap 竞争。线上测试中,相对于内存比较小的场景中,性能提升 10 倍左右。如果在内存管控下 Rocksdb 性能不如预期的话,这也能成为一个性能优化点。

state.backend.rocksdb.memory.partitioned-index-filters:true #默认 false

参数设定案例

bin/flink run \
	-t yarn-per-job \
	-d \
	-p 5 \
	-Drest.flamegraph.enabled=true \
	-Dyarn.application.queue=test \
	-Djobmanager.memory.process.size=1024mb \
	-Dtaskmanager.memory.process.size=4096mb \
	-Dtaskmanager.numberOfTaskSlots=2 \
	-Dstate.backend.incremental=true \ # 开启增量检查点
	-Dstate.backend.local-recovery=true \ # 开启本地恢复
	-Dstate.backend.rocksdb.predefined-options=SPINNING_DISK_OPTIMIZED_HIGH_MEM \ # 调整预定义选项 设置为机械硬盘+内存模式
	-Dstate.backend.rocksdb.block.cache-size=64m \ # 增大 block 缓存大小,默认8m
	-Dstate.backend.rocksdb.writebuffer.size=128m \ # 每个 Column Family 使用独占的 write buffer,默认 64MB,建议调大。
	-Dstate.backend.rocksdb.compaction.level.max-size-level-base=320m \ # 关联 level 阈值大小
	-Dstate.backend.rocksdb.writebuffer.count=5 \ # 每个 Column Family 对应的 writebuffer 最大数量,默认2
	-Dstate.backend.rocksdb.thread.num=4 \ # 用于后台 flush 和合并 sst 文件的线程数,默认1
	-Dstate.backend.rocksdb.writebuffer.number-to-merge=3 \ # 增大 writebuffer 最小合并数,默认1
	-Dstate.backend.rocksdb.memory.partitioned-index-filters=true \ # 开启分区索引功能
	-Dstate.backend.latency-track.keyed-state-enabled=true \ # 开启 State 访问性能监控
	-c com.atguigu.flink.tuning.RocksdbTuning \
	/opt/module/flink-1.13.1/myjar/flink-tuning-1.0-SNAPSHOT.jar

15. 你们的Checkpoint 设置说一下

一般需求,我们的 Checkpoint 时间间隔可以设置为分钟级别(1 ~5 分钟)。对于状态很大的任务每次 Checkpoint 访问 HDFS 比较耗时,可以设置为 5~10 分钟一次 Checkpoint,并且调大两次 Checkpoint 之间的暂停间隔,例如设置两次 Checkpoint 之间至少暂停 4 或 8 分钟。同时,也需要考虑时效性的要求,需要在时效性和性能之间做一个平衡,如果时效性要求高,结合 end-to-end 时长,设置秒级或毫秒级。如果 Checkpoint 语义配置为 EXACTLY_ONCE,那么在 Checkpoint 过程中还会存在 barrier 对齐的过程,可以通过 Flink Web UI 的 Checkpoint 选项卡来查看 Checkpoint 过程中各阶段的耗时情况,从而确定到底是哪个阶段导致 Checkpoint 时间过长然后针对性的解决问题。

RocksDB 相关参数在前面已说明,可以在 flink-conf.yaml 指定,也可以在 Job 的代码
中调用 API 单独指定,这里不再列出。

// 使⽤ RocksDBStateBackend 做为状态后端,并开启增量 Checkpoint
RocksDBStateBackend rocksDBStateBackend = new RocksDBStateBackend("hdfs://hadoop1:8020/flink/checkpoints", true);
env.setStateBackend(rocksDBStateBackend);
// 开启 Checkpoint,间隔为 3 分钟
env.enableCheckpointing(TimeUnit.MINUTES.toMillis(3));
// 配置 Checkpoint
CheckpointConfig checkpointConf = env.getCheckpointConfig();
checkpointConf.setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE)
// 最小间隔 4 分钟
checkpointConf.setMinPauseBetweenCheckpoints(TimeUnit.MINUTES.toMillis(4))
// 超时时间 10 分钟
checkpointConf.setCheckpointTimeout(TimeUnit.MINUTES.toMillis(10));
// 保存 checkpoint
checkpointConf.enableExternalizedCheckpoints(CheckpointConfig.ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION);

16. 反压如何处理

反压的理解

简单来说,Flink 拓扑中每个节点(Task)间的数据都以阻塞队列的方式传输,下游来不及消费导致队列被占满后,上游的生产也会被阻塞,最终导致数据源的摄入被阻塞。

反压(BackPressure)通常产生于这样的场景:短时间的负载高峰导致系统接收数据的速率远高于它处理数据的速率。许多日常问题都会导致反压,例如,垃圾回收停顿可能会导致流入的数据快速堆积,或遇到大促、秒杀活动导致流量陡增。

反压的危害

反压如果不能得到正确的处理,可能会影响到 checkpoint 时长和 state 大小,甚至可能会导致资源耗尽甚至系统崩溃。

  1. 影响 checkpoint 时长:barrier 不会越过普通数据,数据处理被阻塞也会导致 checkpoint barrier 流经整个数据管道的时长变长,导致 checkpoint 总体时间(End to End Duration)变长。
  2. 影响 state 大小:barrier 对齐时,接受到较快的输入管道的 barrier 后,它后面数据会被缓存起来但不处理,直到较慢的输入管道的 barrier 也到达,这些被缓存的数据会被放到 state 里面,导致 checkpoint 变大。

这两个影响对于生产环境的作业来说是十分危险的,因为 checkpoint 是保证数据一致性的关键,checkpoint 时间变长有可能导致 checkpoint 超时失败,而 state 大小同样可能拖慢 checkpoint 甚至导致 OOM (使用 Heap-based StateBackend)或者物理内存使用超出容器资源(使用 RocksDBStateBackend)的稳定性问题。

因此,我们在生产中要尽量避免出现反压的情况。

定位反压节点

解决反压首先要做的是定位到造成反压的节点,排查的时候,先把 operator chain 禁用,方便定位到具体算子。

利用 Flink Web UI 定位

Flink Web UI 的反压监控提供了 SubTask 级别的反压监控,1.13 版本以前是通过周期性对 Task 线程的栈信息采样,得到线程被阻塞在请求 Buffer(意味着被下游队列阻塞)的频率来判断该节点是否处于反压状态。默认配置下,这个频率在 0.1 以下则为 OK,0.1 至 0.5 为 LOW,而超过 0.5 则为 HIGH。

Flink 1.13 优化了反压检测的逻辑(使用基于任务 Mailbox 计时,而不在再于堆栈采样),并且重新实现了作业图的 UI 展示:Flink 现在在 UI 上通过颜色和数值来展示繁忙和反压的程度。

利用 Metrics 定位

监控反压时会用到的 Metrics 主要和 Channel 接受端的 Buffer 使用率有关,最为有用的是以下几个 Metrics:
在这里插入图片描述
其中 inPoolUsage = floatingBuffersUsage + exclusiveBuffersUsage。

分析反压的大致思路是:如果一个 Subtask 的发送端 Buffer 占用率很高,则表明它
被下游反压限速了;如果一个 Subtask 的接受端 Buffer 占用很高,则表明它将反压传导
至上游。

反压的原因及处理

注意:反压可能是暂时的,可能是由于负载高峰、CheckPoint 或作业重启引起的数据积压而导致反压。如果反压是暂时的,应该忽略它。另外,请记住,断断续续的反压会影响我们分析和解决问题。

定位到反压节点后,分析造成原因的办法主要是观察 Task Thread。按照下面的顺序,一步一步去排查。

查看是否数据倾斜

如何处理后面会专门讲,简单说就是两阶段聚合。

使用火焰图分析

如果不是数据倾斜,最常见的问题可能是用户代码的执行效率问题(频繁被阻塞或者性能问题),需要找到瓶颈算子中的哪部分计算逻辑消耗巨大。

最有用的办法就是对 TaskManager 进行 CPU profile,从中我们可以分析到 Task Thread 是否跑满一个 CPU 核:如果是的话要分析 CPU 主要花费在哪些函数里面;如果不是的话要看 Task Thread 阻塞在哪里,可能是用户函数本身有些同步的调用,可能是 checkpoint 或者 GC 等系统活动导致的暂时系统暂停。

开启火焰图功能

Flink 1.13 直接在 WebUI 提供 JVM 的 CPU 火焰图,这将大大简化性能瓶颈的分析,默认是不开启的,需要修改参数:

rest.flamegraph.enabled: true #默认 false

WebUI 查看火焰图
在这里插入图片描述
火焰图是通过对堆栈跟踪进行多次采样来构建的。每个方法调用都由一个条形表示,其中条形的长度与其在样本中出现的次数成正比。

  • On-CPU: 处于 [RUNNABLE, NEW]状态的线程
  • Off-CPU: 处于 [TIMED_WAITING, WAITING, BLOCKED]的线程,用于查看在样本中发现的阻塞调用。

分析火焰图

颜色没有特殊含义,具体查看:

  • 纵向是调用链,从下往上,顶部就是正在执行的函数
  • 横向是样本出现次数,可以理解为执行时长。

看顶层的哪个函数占据的宽度最大。只要有"平顶"(plateaus),就表示该函数可能存在性能问题。

分析 GC 情况

TaskManager 的内存以及 GC 问题也可能会导致反压,包括 TaskManager JVM 各区内存不合理导致的频繁 Full GC 甚至失联。通常建议使用默认的 G1 垃圾回收器

可以通过打印 GC 日志(-XX:+PrintGCDetails),使用 GC 分析器(GCViewer 工具)来验证是否处于这种情况。

在 Flink 提交脚本中,设置 JVM 参数,打印 GC 日志:

bin/flink run \
-t yarn-per-job \
-d \
-p 5 \
-Drest.flamegraph.enabled=true \
-Denv.java.opts="-XX:+PrintGCDetails -XX:+PrintGCDateStamps" \ # 打印GC日志
-Dyarn.application.queue=test \
-Djobmanager.memory.process.size=1024mb \
-Dtaskmanager.memory.process.size=2048mb \
-Dtaskmanager.numberOfTaskSlots=2 \
-c com.atguigu.flink.tuning.UvDemo \
/opt/module/flink-1.13.1/myjar/flink-tuning-1.0-SNAPSHOT.jar

因为是 on yarn 模式,运行的节点一个一个找比较麻烦。可以打开 WebUI,选择 JobManager 或者 TaskManager,点击 Stdout,即可看到 GC 日志,点击下载按钮即可将 GC 日志通过 HTTP 的方式下载下来。
在这里插入图片描述
通过 GC 日志分析出单个 Flink Taskmanager 堆总大小、年轻代、老年代分配的内存空间、Full GC 后老年代剩余大小等。最重要的指标是 Full GC 后,老年代剩余大小这个指标。

17. 如何处理数据倾斜?为什么不能直接用二次聚合来处理?

判断是否存在数据倾斜

相同 Task 的多个 Subtask 中,个别 Subtask 接收到的数据量明显大于其他 Subtask 接收到的数据量,通过 Flink Web UI 可以精确地看到每个 Subtask 处理了多少数据,即可判断出 Flink 任务是否存在数据倾斜。通常,数据倾斜也会引起反压。

另外,有时 Checkpoint detail 里不同 SubTask 的 State size 也是一个分析数据倾斜的有用指标。

keyBy 后的聚合操作存在数据倾斜场景

为什么不能直接用二次聚合来处理

Flink 是实时流处理,如果 keyby 之后的聚合操作存在数据倾斜,且没有开窗口(没攒批)的情况下,简单的认为使用两阶段聚合,是不能解决问题的。因为这个时候 Flink 是来一条处理一条,且向下游发送一条结果,对于原来 keyby 的维度(第二阶段聚合)来讲,数据量并没有减少,且结果重复计算。

使用 LocalKeyBy 的思想

在 keyBy 上游算子数据发送之前,首先在上游算子的本地对数据进行聚合后,再发送到下游,使下游接收到的数据量大大减少,从而使得 keyBy 之后的聚合操作不再是任务的瓶颈。类似 MapReduce 中 Combiner 的思想,但是这要求聚合操作必须是多条数据或者一批数据才能聚合,单条数据没有办法通过聚合来减少数据量。从 Flink LocalKeyBy 实现原理来讲,必然会存在一个积攒批次的过程,在上游算子中必须攒够一定的数据量,对这些数据聚合后再发送到下游。

实现方式:

  • DataStreamAPI 需要自己写代码实现
  • SQL 可以指定参数,开启 miniBatch 和 LocalGlobal 功能(推荐,后续介绍)

DataStream API 自定义实现的案例

以计算每个 mid 出现的次数为例,keyby 之前,使用 flatMap 实现 LocalKeyby 功能

package com.herobin.flink.debug_local.test;

import org.apache.flink.api.common.functions.RichFlatMapFunction;
import org.apache.flink.api.common.state.ListState;
import org.apache.flink.api.common.state.ListStateDescriptor;
import org.apache.flink.api.common.typeinfo.Types;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.runtime.state.FunctionInitializationContext;
import org.apache.flink.runtime.state.FunctionSnapshotContext;
import org.apache.flink.streaming.api.checkpoint.CheckpointedFunction;
import org.apache.flink.util.Collector;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @program: flink_project
 * @description: 使用 LocalKeyBy 的思想解决数据倾斜问题
 * @author: zhangbin19
 * @create: 2022-05-29 17:27
 **/
public class LocalKeyByFlatMapFunc extends RichFlatMapFunction<Tuple2<String, Long>, Tuple2<String, Long>> implements CheckpointedFunction {
    
    //Checkpoint 时为了保证 Exactly Once,将 buffer 中的数据保存到该 ListState 中
    private ListState<Tuple2<String, Long>> listState;
    
    //本地 buffer,存放 local 端缓存的 mid 的 count 信息
    private HashMap<String, Long> localBuffer;
    
    //缓存的数据量大小,即:缓存多少数据再向下游发送
    private int batchSize;
    
    //计数器,获取当前批次接收的数据量
    private AtomicInteger currentSize;
    
    //构造器,批次大小传参
    public LocalKeyByFlatMapFunc(int batchSize) {
        this.batchSize = batchSize;
    }

    @Override
    public void flatMap(Tuple2<String, Long> value, Collector<Tuple2<String, Long>>
            out) throws Exception {
        // 1、将新来的数据添加到 buffer 中
        Long count = localBuffer.getOrDefault(value, 0L);
        localBuffer.put(value.f0, count + 1);
        // 2、如果到达设定的批次,则将 buffer 中的数据发送到下游
        if (currentSize.incrementAndGet() >= batchSize) {
            // 2.1 遍历 Buffer 中数据,发送到下游
            for (Map.Entry<String, Long> midAndCount : localBuffer.entrySet()) {
                out.collect(Tuple2.of(midAndCount.getKey(),
                        midAndCount.getValue()));
            }
            // 2.2 Buffer 清空,计数器清零
            localBuffer.clear();
            currentSize.set(0);
        }
    }

    @Override
    public void snapshotState(FunctionSnapshotContext context) throws Exception {
        // 将 buffer 中的数据保存到状态中,来保证 Exactly Once
        listState.clear();
        for (Map.Entry<String, Long> midAndCount : localBuffer.entrySet()) {
            listState.add(Tuple2.of(midAndCount.getKey(), midAndCount.getValue()));
        }
    }

    @Override
    public void initializeState(FunctionInitializationContext context) throws Exception {
        // 从状态中恢复 buffer 中的数据
        listState = context.getOperatorStateStore().getListState(
                new ListStateDescriptor<Tuple2<String, Long>>(
                        "localBufferState",
                        Types.TUPLE(Types.STRING, Types.LONG)
                )
        );
        localBuffer = new HashMap();
        if (context.isRestored()) {
            // 从状态中恢复数据到 buffer 中
            for (Tuple2<String, Long> midAndCount : listState.get()) {
                // 如果出现 pv != 0,说明改变了并行度,ListState 中的数据会被均匀分发到新的 subtask 中
                // 单个 subtask 恢复的状态中可能包含多个相同的 mid 的 count 数据
                // 所以每次先取一下 buffer 的值,累加再 put
                long count = localBuffer.getOrDefault(midAndCount.f0, 0L);
                localBuffer.put(midAndCount.f0, count + midAndCount.f1);
            }
            // 从状态恢复时,默认认为 buffer 中数据量达到了 batchSize,需要向下游发
            currentSize = new AtomicInteger(batchSize);
        } else {
            currentSize = new AtomicInteger(0);
        }
    }

}

keyBy 之前发生数据倾斜场景

如果 keyBy 之前就存在数据倾斜,上游算子的某些实例可能处理的数据较多,某些实例可能处理的数据较少,产生该情况可能是因为数据源的数据本身就不均匀,例如由于某些原因 Kafka 的 topic 中某些 partition 的数据量较大,某些 partition 的数据量较少。

对于不存在 keyBy 的 Flink 任务也会出现该情况。这种情况,需要让 Flink 任务强制进行 shuffle。使用 shuffle、rebalance 或 rescale 算子即可将数据均匀分配,从而解决数据倾斜的问题。

keyBy 后的窗口聚合操作存在数据倾斜

因为使用了窗口,变成了有界数据(攒批)的处理,窗口默认是触发时才会输出一条结
果发往下游,所以可以使用两阶段聚合的方式:

1)实现思路:

第一阶段聚合:key 拼接随机数前缀或后缀,进行 keyby、开窗、聚合

注意:聚合完不再是 WindowedStream,要获取 WindowEnd 作为窗口标记作为第二阶段分组依据,避免不同窗口的结果聚合到一起)

第二阶段聚合:按照原来的 key 及 windowEnd 作 keyby、聚合

总结:数据倾斜可以分为两种情况,一种是上游数据就是倾斜的,如topic partition 写入不均,这种最好联系上游进行均匀写入,不然只能更改下发规则,敢为shuffle方式打散,保证均衡。不过一般也不会出现这种情况,因为kafka默认写入的方式是轮训写入的。
第二种情况是key by之后存在热点key导致的数据倾斜,例如某个明星发了个小作文,他的微博访问qps会非常大,我们如果按照mid进行聚合的话,就会出现很严重的热点key的问题,这样我们可以先按照 mid+随机后缀 进行预聚合,减少数据量级,再去掉随机后缀进行二次聚合。

18. 自己测试的话这么造数据测试?

  • 可以自定义实现sourceFunction
  • 使用flink 1.11开始提供的 DataGen 连接器,主要是用于生成一些随机数,用于在没有数据源的时候,进行流任务的测试以及性能测试等。

19. Flink Job 优化点有哪些?

1. 算子指定好 UUID

简单总结:算子指定好uuid且保存savepoint后,即使重新提交作业并行度发生改变,算子也可以根据uuid找到之前的状态进行回复,而没有制定的话因为并行度发生改变可能就无法对应找到自己算子的状态了。

对于有状态的 Flink 应用,推荐给每个算子都指定唯一用户 ID(UUID)。 严格地说,仅需要给有状态的算子设置就足够了。但是因为 Flink 的某些内置算子(如 window)是有状态的,而有些是无状态的,可能用户不是很清楚哪些内置算子是有状态的,哪些不是。所以从实践经验上来说,我们建议每个算子都指定上 UUID。

默认情况下,算子 UID 是根据 JobGraph 自动生成的,JobGraph 的更改可能会导致 UUID 改变。手动指定算子 UUID ,可以让 Flink 有效地将算子的状态从 savepoint 映射到作业修改后(拓扑图可能也有改变)的正确的算子上。比如替换原来的 Operator 实现、增加新的Operator、删除Operator等等,至少我们有可能与Savepoint中存储的Operator 状态对应上。这是 savepoint 在 Flink 应用中正常工作的一个基本要素。

Flink 算子的 UUID 可以通过 uid(String uid) 方法指定,通常也建议指定 name。

#算子.uid("指定 uid")
.reduce(...).uid("uv-reduce").name("uv-reduce")

2. 链路延迟测量

对于实时的流式处理系统来说,我们需要关注数据输入、计算和输出的及时性,所以处理延迟是一个比较重要的监控指标,特别是在数据量大或者软硬件条件不佳的环境下。Flink 提供了开箱即用的 LatencyMarker 机制来测量链路延迟。开启如下参数:

metrics.latency.interval: 30000 #默认 0,表示禁用,单位毫秒

监控的粒度,分为以下 3 档:

  • single:每个算子单独统计延迟;
  • operator(默认值):每个下游算子都统计自己与 Source 算子之间的延迟;
  • subtask:每个下游算子的 sub-task 都统计自己与 Source 算子的 sub-task 之间的延迟。
metrics.latency.granularity: operator #默认 operator

一般情况下采用默认的 operator 粒度即可,这样在 Sink 端观察到的 latency metric 就是我们最想要的全链路(端到端)延迟。subtask 粒度太细,会增大所有并行度的负担,不建议使用。

LatencyMarker 不会参与到数据流的用户逻辑中的,而是直接被各算子转发并统计。为了让它尽量精确,有两点特别需要注意:

  • 保证 Flink 集群内所有节点的时区、时间是同步的:ProcessingTimeService 产生时间戳最终是靠 System.currentTimeMillis()方法,可以用 ntp 等工具来配置。
  • metrics.latency.interval 的时间间隔宜大不宜小:一般配置成 30000(30 秒)左右。一是因为延迟监控的频率可以不用太频繁,二是因为 LatencyMarker 的处理也要消耗一定性能。

可以通过下面的 metric 查看结果:

flink_taskmanager_job_latency_source_id_operator_id_operator_subtask_index_latency

在这里插入图片描述
3. 开启对象重用
在这里插入图片描述
当调用了 enableObjectReuse 方法后,Flink 会把中间深拷贝的步骤都省略掉,SourceFunction 产生的数据直接作为 MapFunction 的输入,可以减少 gc 压力。但需要特别注意的是,这个方法不能随便调用,必须要确保下游 Function 只有一种,或者下游的 Function 均不会改变对象内部的值。否则可能会有线程安全的问题。

开启方式:提交时制定参数:

bin/flink run \
	-t yarn-per-job \
	-d \
	-p 5 \
	-Drest.flamegraph.enabled=true \
	-Dyarn.application.queue=test \
	-Djobmanager.memory.process.size=1024mb \
	-Dtaskmanager.memory.process.size=2048mb \
	-Dtaskmanager.numberOfTaskSlots=2 \
	-Dpipeline.object-reuse=true \ # 开启对象重用
	-Dmetrics.latency.interval=30000 \
	-c com.atguigu.flink.tuning.UidDemo \
	/opt/module/flink-1.13.1/myjar/flink-tuning-1.0-SNAPSHOT.jar

4. 细粒度滑动窗口优化

1)细粒度滑动的影响

当使用细粒度的滑动窗口(窗口长度远远大于滑动步长)时,重叠的窗口过多,一个数据会属于多个窗口,性能会急剧下降。

我们经常会碰到这种需求:以 3 分钟的频率实时计算 App 内各个子模块近 24 小时的 PV 和 UV。我们需要用粒度为 1440 / 3 = 480 的滑动窗口来实现它,但是细粒度的滑动窗口会带来性能问题,有两点:

  1. 状态

    对于一个元素,会将其写入对应的(key, window)二元组所圈定的 windowState 状态
    中。如果粒度为 480,那么每个元素到来,更新 windowState 时都要遍历 480 个窗口并写
    入,开销是非常大的。在采用 RocksDB 作为状态后端时,checkpoint 的瓶颈也尤其明显。

  2. 定时器

    每一个(key, window)二元组都需要注册两个定时器:一是触发器注册的定时器,用于
    决定窗口数据何时输出;二是 registerCleanupTimer()方法注册的清理定时器,用于在窗
    口彻底过期(如 allowedLateness 过期)之后及时清理掉窗口的内部状态。细粒度滑动窗
    口会造成维护的定时器增多,内存负担加重。

解决思路

我们一般使用滚动窗口+在线存储+读时聚合的思路作为解决方案:

  1. 从业务的视角来看,往往窗口的长度是可以被步长所整除的,可以找到窗口长度和窗口步长的最小公约数作为时间分片(一个滚动窗口的长度);
  2. 每个滚动窗口将其周期内的数据做聚合,存到下游状态或打入外部在线存储(内存数据库如 Redis,LSM-based NoSQL 存储如 HBase);
  3. 扫描在线存储中对应时间区间(可以灵活指定)的所有行,并将计算结果返回给前端展示。

20. Checkpoint 失败场景

Checkpoint 失败大致分为两种情况:Checkpoint Decline 和 Checkpoint Expire。

Checkpoint Decline

我们能从 jobmanager.log 中看到类似下面的日志:

Decline checkpoint 10423 by task 0b60f08bf8984085b59f8d9bc74ce2e1 of job 85d268e6fbc19411185f7e4868a44178.

我们可以在 jobmanager.log 中查找 execution id,找到被调度到哪个 taskmanager上,类似如下所示:

2019-09-02 16:26:20,972 INFO [jobmanager-future-thread-61]
org.apache.flink.runtime.executiongraph.ExecutionGraph - XXXXXXXXXXX
(100/289) (87b751b1fd90e32af55f02bb2f9a9892) switched from SCHEDULED to
DEPLOYING.
2019-09-02 16:26:20,972 INFO [jobmanager-future-thread-61]
org.apache.flink.runtime.executiongraph.ExecutionGraph - Deploying
XXXXXXXXXXX (100/289) (attempt #0) to slot
container_e24_1566836790522_8088_04_013155_1 on hostnameABCDE

从上面的日志我们知道该 execution 被调度到 hostnameABCDE 的 container_e24_1566836790522_8088_04_013155_1 slot 上,接下来我们就可以到 container container_e24_1566836790522_8088_04_013155 的 taskmanager.log 中查找 Checkpoint 失败的具体原因了。

另外对于 Checkpoint Decline 的情况,有一种情况在这里单独抽取出来进行介绍:Checkpoint Cancel。

当前 Flink 中如果较小的 Checkpoint 还没有对齐的情况下,收到了更大的 Checkpoint,则会把较小的 Checkpoint 给取消掉。我们可以看到类似下面的日志:

$taskNameWithSubTaskAndID: Received checkpoint barrier for checkpoint 20 before completing current checkpoint 19. Skipping current checkpoint.

这个日志表示,当前 Checkpoint 19 还在对齐阶段,我们收到了 Checkpoint 20 的 barrier。然后会逐级通知到下游的 task checkpoint 19 被取消了,同时也会通知 JM 当前 Checkpoint 被 decline 掉了。

在下游 task 收到被 cancelBarrier 的时候,会打印类似如下的日志:

DEBUG
$taskNameWithSubTaskAndID: Checkpoint 19 canceled, aborting alignment.
或者
DEBUG
$taskNameWithSubTaskAndID: Checkpoint 19 canceled, skipping alignment.
或者
WARN
$taskNameWithSubTaskAndID: Received cancellation barrier for checkpoint 20 before
completing current checkpoint 19. Skipping current checkpoint.

上面三种日志都表示当前 task 接收到上游发送过来的 barrierCancel 消息,从而取消了对应的 Checkpoint。

Checkpoint Expire

如果 Checkpoint 做的非常慢,超过了 timeout 还没有完成,则整个 Checkpoint 也会失败。当一个 Checkpoint 由于超时而失败是,会在 jobmanager.log 中看到如下的日志:

Checkpoint 1 of job 85d268e6fbc19411185f7e4868a44178 expired before completing.

表示 Chekpoint 1 由于超时而失败,这个时候可以可以看这个日志后面是否有类似下面的日志:

Received late message for now expired checkpoint attempt 1 from 0b60f08bf8984085b59f8d9bc74ce2e1 of job 85d268e6fbc19411185f7e4868a44178.

可以按照上面的方法找到对应的 taskmanager.log 查看具体信息。

我们按照下面的日志把 TM 端的 snapshot 分为三个阶段:开始做 snapshot 前,同步阶段,异步阶段,需要开启 DEBUG 才能看到:

DEBUG
Starting checkpoint (6751) CHECKPOINT on task taskNameWithSubtasks (4/4)

上面的日志表示 TM 端 barrier 对齐后,准备开始做 Checkpoint。

DEBUG
2019-08-06 13:43:02,613 DEBUG
org.apache.flink.runtime.state.AbstractSnapshotStrategy -
DefaultOperatorStateBackend snapshot (FsCheckpointStorageLocation
{fileSystem=org.apache.flink.core.fs.SafetyNetWrapperFileSystem@70442baf,
checkpointDirectory=xxxxxxxx, sharedStateDirectory=xxxxxxxx,
taskOwnedStateDirectory=xxxxxx, metadataFilePath=xxxxxx, reference=(default),
fileStateSizeThreshold=1024}, synchronous part) in thread Thread[Async calls on
Source: xxxxxx_source -> Filter (27/70),5,Flink Task Threads] took 0 ms.

上面的日志表示当前这个 backend 的同步阶段完成,共使用了 0 ms。

DEBUG
DefaultOperatorStateBackend snapshot (FsCheckpointStorageLocation
{fileSystem=org.apache.flink.core.fs.SafetyNetWrapperFileSystem@7908affe,
checkpointDirectory=xxxxxx, sharedStateDirectory=xxxxx,
taskOwnedStateDirectory=xxxxx, metadataFilePath=xxxxxx, reference=(default),
fileStateSizeThreshold=1024}, asynchronous part) in thread
Thread[pool-48-thread-14,5,Flink Task Threads] took 369 ms

上面的日志表示异步阶段完成,异步阶段使用了 369 ms。

在现有的日志情况下,我们通过上面三个日志,定位 snapshot 是开始晚,同步阶段做的慢,还是异步阶段做的慢。然后再按照情况继续进一步排查问题。

21. Checkpoint 慢 如何排查解决

Checkpoint 慢的情况如下:比如 Checkpoint interval 1 分钟,超时 10 分钟,Checkpoint 经常需要做 9 分钟(我们希望 1 分钟左右就能够做完),而且我们预期 state size 不是非常大。对于 Checkpoint 慢的情况,我们可以按照下面的顺序逐一检查。

1. Source Trigger Checkpoint 慢

这个一般发生较少,但是也有可能,因为 source 做 snapshot 并往下游发送 barrier 的时候,需要抢锁(Flink1.10 开始,用 mailBox 的方式替代当前抢锁的方式,所以已经避免了)。

2. 使用增量 Checkpoint

现在 Flink 中 Checkpoint 有两种模式,全量 Checkpoint 和 增量 Checkpoint,其中全量 Checkpoint 会把当前的 state 全部备份一次到持久化存储,而增量 Checkpoint,则只备份上一次 Checkpoint 中不存在的 state,因此增量 Checkpoint 每次上传的内容会相对更好,在速度上会有更大的优势。

现在 Flink 中仅在 RocksDBStateBackend 中支持增量 Checkpoint,如果你已经使用 RocksDBStateBackend,可以通过开启增量 Checkpoint 来加速。

3. 作业存在反压或者数据倾斜

task 仅在接受到所有的 barrier 之后才会进行 snapshot,如果作业存在反压,或者有数据倾斜,则会导致全部的 channel 或者某些 channel 的 barrier 发送慢,从而整体影响 Checkpoint 的时间。

4. Barrier 对齐慢

从前面我们知道 Checkpoint 在 task 端分为 barrier 对齐(收齐所有上游发送过来的 barrier),然后开始同步阶段,再做异步阶段。如果 barrier 一直对不齐的话,就不会开始做 snapshot。
barrier 对齐之后会有如下日志打印:

DEBUG Starting checkpoint (6751) CHECKPOINT on task taskNameWithSubtasks (4/4)

如果 taskmanager.log 中没有这个日志,则表示 barrier 一直没有对齐,接下来我们需要了解哪些上游的 barrier 没有发送下来,如果你使用 At Least Once 的话,可以观察下面的日志:

DEBUG Received barrier for checkpoint 96508 from channel 5

表示该 task 收到了 channel 5 来的 barrier,然后看对应 Checkpoint,再查看还剩哪些上游的 barrier 没有接受到。

5. 主线程太忙,导致没机会做 snapshot

在 task 端,所有的处理都是单线程的,数据处理和 barrier 处理都由主线程处理,如果主线程在处理太慢(比如使用 RocksDBBackend,state 操作慢导致整体处理慢),导致 barrier 处理的慢,也会影响整体 Checkpoint 的进度,可以通过火焰图分析。

6. 同步阶段做的慢

同步阶段一般不会太慢,但是如果我们通过日志发现同步阶段比较慢的话,对于非 RocksDBBackend 我们可以考虑查看是否开启了异步 snapshot,如果开启了异步 snapshot 还是慢,需要看整个 JVM 在 干 嘛 , 也 可 以 使 用 火焰图分析 。对于 RocksDBBackend 来说,我们可以用 iostate 查看磁盘的压力如何,另外可以查看 tm 端 RocksDB 的 log 的日志如何,查看其中 SNAPSHOT 的时间总共开销多少。

RocksDB 开始 snapshot 的日志如下:

2019/09/10-14:22:55.734684 7fef66ffd700 [utilities/checkpoint/checkpoint_impl.cc:83]
Started the snapshot process -- creating snapshot in directory
/tmp/flink-io-87c360ce-0b98-48f4-9629-2cf0528d5d53/XXXXXXXXXXX/chk-92729

snapshot 结束的日志如下:

2019/09/10-14:22:56.001275 7fef66ffd700 [utilities/checkpoint/checkpoint_impl.cc:145]
Snapshot DONE. All is good

6. 异步阶段做的慢

对于异步阶段来说 , tm 端主要将 state 备份到持久化存储上 , 对于非 RocksDBBackend 来说,主要瓶颈来自于网络,这个阶段可以考虑观察网络的 metric,或者对应机器上能够观察到网络流量的情况(比如 iftop)。

对于 RocksDB 来说,则需要从本地读取文件,写入到远程的持久化存储上,所以不仅需要考虑网络的瓶颈,还需要考虑本地磁盘的性能。另外对于 RocksDBBackend 来说,如果觉得网络流量不是瓶颈,但是上传比较慢的话,还可以尝试考虑开启多线程上传功能(Flink 1.13 开始,state.backend.rocksdb.checkpoint.transfer.thread.num 默认值是 4)。

22. Flink 如何支持 Kafka 动态发现分区

当 FlinkKafkaConsumer 初始化时,每个 subtask 会订阅一批 partition,但是当 Flink 任务运行过程中,如果被订阅的 topic 创建了新的 partition,FlinkKafkaConsumer 如何实现动态发现新创建的 partition 并消费呢?

在使用 FlinkKafkaConsumer 时,可以开启 partition 的动态发现。通过 Properties 指定参数开启(单位是毫秒):

FlinkKafkaConsumerBase.KEY_PARTITION_DISCOVERY_INTERVAL_MILLIS 

该参数表示间隔多久检测一次是否有新创建的 partition。默认值是 Long 的最小值,表示不开启,大于 0 表示开启。开启时会启动一个线程根据传入的 interval 定期获取 Kafka 最新的元数据,新 partition 对应的那一个 subtask 会自动发现并从 earliest 位置开始消费,新创建的 partition 对其他 subtask 并不会产生影响。

代码如下所示:

properties.setProperty(FlinkKafkaConsumerBase.KEY_PARTITION_DISCOVERY_INTERVAL_MILLIS, 30 * 1000 + "");

23. flink exactly-once与atleast-once区别 实现原理

Exactly-once VS At-least-once

算子做快照时,如果等所有输入端的barrier都到了才开始做快照,那么就可以保证算子的exactly-once;如果为了降低延时而跳过对其,从而继续处理数据,那么等barrier都到齐后做快照就是at-least-once了,因为这次的快照掺杂了下一次快照的数据,当作业失败恢复的时候,这些数据会重复作用系统,就好像这些数据被消费了两遍。

感觉这里说的不对,exaclty-once 快流到达后会阻塞,不继续处理barrior后面的数据,直到对其完ck完才继续处理,而 atleast-once 会继续处理,所以对其完ck的状态是包含了处理快流barrior之后数据的状态,这样再发生failover恢复的时候,这部分数据就等于被处理了两遍。

注:对齐只会发生在算子的上端是join操作以及上游存在partition或者shuffle的情况,对于直连操作类似map、flatMap、filter等还是会保证exactly-once的语义。

端到端的Exactly once实现

下面以一个简单的flink读写kafka作为例子来说明(kafka0.11版本开始支持exactly-once语义)。如图所示:
在这里插入图片描述
在我们今天要讨论的 Flink 应用程序示例中,我们有:

  • 从 Kafka 读取数据的数据源(在 Flink 为 KafkaConsumer)
  • 窗口聚合
  • 将数据写回 Kafka 的数据接收器(在 Flink 为 KafkaProducer)

要使数据接收器提供 Exactly-Once 语义保证,必须在一个事务中将所有数据写入 Kafka。提交捆绑了两个检查点之间的所有写入数据。这可确保在发生故障时能回滚所有写入的数据。

但是,在具有多个并发运行的接收器任务的分布式系统中,简单的提交或回滚是远远不够的,因为必须确保所有组件在提交或回滚时一致才能确保一致的结果。Flink 使用两阶段提交协议及预提交阶段来解决这一问题。

检查点的启动表示我们的两阶段提交协议的预提交阶段。当检查点启动时,Flink JobManager 会将检查点 Barrier 注入数据流中(将数据流中的记录分为进入当前检查点的集合与进入下一个检查点的集合)。

Barrier 在算子之间传递。对于每个算子,它会触发算子状态后端生成状态的快照。
在这里插入图片描述
数据源存储 Kafka 的偏移量,完成此操作后将检查点 Barrier 传递给下一个算子。

这种方法只适用于算子只有内部状态(Internal state)的情况。内部状态是 Flink 状态可以存储和管理的所有内容 - 例如,第二个算子中的窗口总和。当一个进程只有内部状态时,除了写入到已定义的状态变量之外,不需要在预提交阶段执行任何其他操作。Flink 负责在检查点成功的情况下正确提交这些写入,或者在出现故障时中止这些写入。
在这里插入图片描述
但是,当一个进程具有外部状态(External state)时,状态处理会有所不同。外部状态通常以写入外部系统(如Kafka)的形式出现。在这种情况下,为了提供 Exactly-Once 语义保证,外部系统必须支持事务,这样才能和两阶段提交协议集成。

我们示例中的数据接收器具有外部状态,因为它正在向 Kafka 写入数据。在这种情况下,在预提交阶段,除了将其状态写入状态后端之外,数据接收器还必须预先提交其外部事务。
在这里插入图片描述
当检查点 Barrier 通过所有算子并且触发的快照回调成功完成时,预提交阶段结束。所有触发的状态快照都被视为该检查点的一部分。检查点是整个应用程序状态的快照,包括预先提交的外部状态。如果发生故障,我们可以回滚到上次成功完成快照的时间点。

下一步是通知所有算子检查点已成功完成。这是两阶段提交协议的提交阶段,JobManager 为应用程序中的每个算子发出检查点完成的回调。

数据源和窗口算子没有外部状态,因此在提交阶段,这些算子不用执行任何操作。但是,数据接收器有外部状态,因此此时应该提交外部事务:
在这里插入图片描述
小结

  1. Flink 检查点是支持两阶段提交协议并提供端到端的 Exactly-Once 语义的基础。
  2. 这个方案的一个优点是: Flink 不像其他一些系统那样,通过网络传输存储(materialize)数据 - 不需要像大多数批处理程序那样将计算的每个阶段写入磁盘。
  3. Flink 新的 TwoPhaseCommitSinkFunction 提取了两阶段提交协议的通用逻辑,并使构建端到端的 Exactly-Once 语义的应用程序(使用 Flink 和支持事务的外部系统)成为可能。
  4. 从 Flink 1.4.0 开始,Pravega 和 Kafka 0.11 producer 都提供了 Exactly-Once 语义;在 Kafka 0.11 中首次引入了事务,这使得 Kafka 在 Flink 实现 Exactly-Once producer 成为可能。
  5. Kafka 0.11 producer 是在 TwoPhaseCommitSinkFunction 基础之上实现的,与 At-Least-Once 语义的 Kafka producer 相比,它的开销非常低。

24. JobManager下有哪些组件?

  • JobMaster
  • ResourceManager
  • Dispatcher

25. 提交flink作业的时候发生类冲突怎么处理?了解child-first/parent-first机制吗?它们如何指定classloader?

Flink作为基于JVM的框架,在flink-conf.yaml中提供了控制类加载策略的参数classloader.resolve-order,可选项有child-first(默认)和parent-first

Flink的parent-first类加载策略就是照搬双亲委派模型的。也就是说,用户代码的类加载器是Custom ClassLoader,Flink框架本身的类加载器是Application ClassLoader。用户代码中的类先由Flink框架的类加载器加载,再由用户代码的类加载器加载。但是,Flink默认并不采用parent-first策略,而是采用下面的child-first策略。

我们已经了解到,双亲委派模型的好处就是随着类加载器的层次关系保证了被加载类的层次关系,从而保证了Java运行环境的安全性。但是在Flink App这种依赖纷繁复杂的环境中,双亲委派模型可能并不适用。例如,程序中引入的Flink-Kafka Connector总是依赖于固定的Kafka版本,用户代码中为了兼容实际使用的Kafka版本,会引入一个更低或更高的依赖。而同一个组件不同版本的类定义有可能会不同(即使类的全限定名是相同的),如果仍然用双亲委派模型,就会因为Flink框架指定版本的类先加载,而出现莫名其妙的兼容性问题,如NoSuchMethodErrorIllegalAccessError等。

鉴于此,Flink实现了ChildFirstClassLoader类加载器并作为默认策略。它打破了双亲委派模型,使得用户代码的类先加载。

26. 你们用per job模式提交还是session模式提交?为什么新版本flink取消了per job模式?

per job

可能因为application模式和per-job的区别,不用client了…

应用模式与单作业模式的提交流程非常相似,只是初始提交给 YARN 资源管理器的不再
是具体的作业,而是整个应用。一个应用中可能包含了多个作业,这些作业都将在 Flink 集群
中启动各自对应的 JobMaster。

flink per job 和 application 模式的区别???

27. flink application mode

现在遇到一个大问题是部署服务是一个消耗资源比较大的服务,并且很难计算出实际资源限制。比如,如果我们取负载的平均值,则可能导致部署服务的资源真实所需的值远远大于限制值,最坏的情况是在一定时间影响所有的线上应用。但是如果我们将取负载的最大值,又会造成很多不必要的浪费。基于此,Flink 1.11 引入了另外一种部署选项 Application Mode, 该模式允许更加轻量级,可扩展的应用提交进程,将之前客户端的应用部署能力均匀分散到集群的每个节点上。

Session 模式
Session 模式假定已经存在一个集群,提交的应用都在该集群里执行。因此会导致资源的竞争。该模式的优势是你无需为每一个提交的任务花费精力去分解集群。但是,如果Job异常或是TaskManager 宕掉,那么该TaskManager运行的其他Job都会失败。除了影响到任务,也意味着潜在需要更多的恢复操作,重启所有的Job,会并发访问文件系统,会导致该文件系统对其他服务不可用。此外,单集群运行多个Job,意味着JobManager更大的负载。这种模式适合启动延迟非常重要的短期作业

Per-Job 模式
在Per-Job模式下,集群管理器框架(例如YARN或Kubernetes)用于为每个提交的Job启动一个 Flink 集群。Job完成后,集群将关闭,所有残留的资源(例如文件)也将被清除。此模式可以更好地隔离资源,因为行为异常的Job不会影响任何其他Job。另外,由于每个应用程序都有其自己的JobManager,因此它将记录的负载分散到多个实体中。考虑到前面提到的Session模式的资源隔离问题,Per-Job模式适合长期运行的Job,这些Job可以接受启动延迟的增加以支持弹性。

总而言之,在Session 模式下,集群生命周期独立于集群上运行的任何Job,并且集群上运行的所有Job共享其资源。Per-Job模式选择为每个提交的Job承担拆分集群的费用,以提供更好的资源隔离保证,因为资源不会在Job之间共享。在这种情况下,集群的生命周期将与job的生命周期绑定在一起。

应用提交

Flink 应用的执行包含两个阶段:

  • pre-flight: 在main()方法调用之后开始。
  • runtime: 一旦用户代码调用 execute() 就会触发该阶段。

main()方法使用Flink的API(DataStream API,Table API,DataSet API)之一构造用户程序。当main()方法调用env.execute()时,用户定义的pipeline将转换为Flink运行时可以理解的形式,称为job graph,并将其传送到集群中。

尽管有一些不同,但是 对于 Session 模式 和 Per-Job模式 , pre-flight 阶段都是在客户端完成的。

对于那些在自己本地计算机上提交任务的场景(本地计算机包含了所有运行Job所需的依赖),这通常不是问题。但是,对于通过诸如部署服务之类的远程进行提交的场景,此过程包括:

  • 下载应用所需的依赖
  • 执行main()方法提取 job graph
  • 将依赖和 job graph 传输到集群
  • 有可能需要等待结果

这样客户端大量消耗资源,因为它可能需要大量的网络带宽来下载依赖项并将二进制文件运送到集群,并且需要CPU周期来执行main()方法。随着更多用户共享同一客户端,此问题会更加明显。
在这里插入图片描述
红色,蓝色和绿色代表3个应用程序,每个应用程序三个并发。黑色矩形代表不同的进程:TaskManagers,JobManagers和 Deployer(集中式部署服务)。并且我们假设在所有情况下都只有一个Deployer进程。彩色三角形表示提交进程的负载,而彩色矩形表示TaskManager和JobManager进程的负载。如图所示,不管是per-job 还是 session 模式, 部署程序承担相同的负载。它们的区别在于Job的分配和JobManager的负载。在session模式下,集群中的所有作业只有一个JobManager,而在per-job模式下,每个Job都有一个JobManager。另外,在session 模式下的Job 被随机分配给TaskManager,而在per-job 模式下,每个TaskManager只有单个Job。

Application Mode
在这里插入图片描述
Application 模式 尝试去将per-job 模式的资源隔离性和轻量级,可扩展的应用提交进程相结合。为了实现这个目的,它会每个Job 创建一个集群,但是 应用的main()将被在JobManager 执行

每个应用程序创建一个集群,可以看作创建仅在特定应用程序的Job之间共享的session集群,并在应用程序完成时销毁。通过这种架构,Application模式可以提供与 per-job 模式相同的资源隔离和负载平衡保证,但前提是保证一个完整应用程序的粒度。显然,属于同一应用程序的Job应该被关联起来,并视为一个单元。

在JobManager 中执行 main()方法,更大大减轻客户端的资源消耗。更进一步讲,由于每个应用程序有一个JobManager,因此可以更平均地分散网络负载。上图对此进行了说明,在该图中,这次客户端负载已转移到每个应用程序的JobManager。

在Application 模式下,与其他模式不一样的是,main() 方法在集群上而不是在客户端执行。这可能会对您的代码产生影响,例如,您必须使用应用程序的JobManager可以访问使用registerCachedFile()在环境中注册的任何路径。

与per-job 模式相比,Application 模式允许提交由多个Job组成的应用程序。Job执行的顺序不受部署模式的影响,但受启动Job的调用的影响。使用阻塞的 execute()方法,将是一个顺序执行的效果,结果就是"下一个"Job的执行被推迟到“该”Job完成为止。相反,一旦提交当前作业,非阻塞executeAsync()方法将立即继续提交“下一个”Job。

减少网络需求

如上所述,通过在JobManager上执行应用程序的main()方法,Application 模式可以节省很多提交应用所需的资源。但是仍有改进的空间。

专注于YARN, 因为社区对于yarn的优化支持更全面。即使使用 Application 模式,仍然需要客户端将用户jar发送到JobManager。此外,对于每个应用程序,客户端都必须将“ flink-dist”路径输送到集群,该目录包含框架本身的二进制文件,包括flink-dist.jar,lib/ 和plugin/ 目录。这两个可以占用客户端大量的带宽。此外,在每个提交中传送相同的flink-dist二进制文件不仅浪费带宽,而且浪费存储空间,只需允许应用程序共享相同的二进制文件就可以缓解。

对于Flink1.11 , 引入了下面的两个选项可供大家使用:

  1. 指定目录的远程路径,YARN可以在该目录中找到Flink分发二进制文件
  2. 指定YARN可以在其中找到用户jar的远程路径

对于1.,我们利用YARN的分布式缓存,并允许应用程序共享这些二进制文件。因此,如果由于先前在同一TaskManager上执行的应用程序而导致某个应用程序恰巧在其TaskManager的本地存储上找到Flink的副本,则它甚至不必在内部下载它。

注意两种优化都可用于YARN上的所有部署模式,而不仅仅是Application模式。

示例: Application 模式 on Yarn

Application 模式下,使用以下语句提交一个应用:

./bin/flink run-application -t yarn-application ./MyApplication.jar

使用此命令,所有配置参数都可以通过其配置选项(以-D为前缀)来指定。有关可用配置选项的目录,请参阅Flink的配置页面

例如,用于指定JobManager和TaskManager的内存大小的命令如下所示:

./bin/flink run-application -t yarn-application \
    -Djobmanager.memory.process.size=2048m \
    -Dtaskmanager.memory.process.size=4096m \
    ./MyApplication.jar

为了进一步节省将Flink发行版传送到集群的带宽,请考虑将Flink发行版预上传到YARN可以访问的位置,并使用yarn.provided.lib.dirs配置选项,如下所示:

./bin/flink run-application -t yarn-application \
    -Djobmanager.memory.process.size=2048m \
    -Dtaskmanager.memory.process.size=4096m \
    -Dyarn.provided.lib.dirs="hdfs://myhdfs/remote-flink-dist-dir" \
    ./MyApplication.jar

最后,为了进一步节省提交应用程序jar所需的带宽,您可以将其预上传到HDFS,并指定指向./MyApplication.jar的远程路径,如下所示:

./bin/flink run-application -t yarn-application \
    -Djobmanager.memory.process.size=2048m \
    -Dtaskmanager.memory.process.size=4096m \
    -Dyarn.provided.lib.dirs="hdfs://myhdfs/remote-flink-dist-dir" \
    hdfs://myhdfs/jars/MyApplication.jar

这将使Job提交特别轻巧,因为所需的Flink jar和应用程序jar将从指定的远程位置获取,而不是由客户端传送到集群。客户端将唯一传送到集群的是你的应用程序配置,其中包括上述所有路径。

28. flink cdc了解吗?

尚硅谷大数据技术之 Flink-CDC

如何用 Flink SQL CDC 实现实时数据同步?

29. flink故障恢复的流程(从检查点恢复状态)

flink故障恢复的流程(从检查点恢复状态)

30. flink 反压的底层原理(不仅限buffer)

先从限流开始说起:

数据流程
在这里插入图片描述
整体流程可类比为生产者->消费者体系。上游生产者发送数据(2M/s)至Send Buffer,途径网络传输(5M/s)到Receive Buffer, 最终下游Consumer消费(<1M/s)。

这明显是不行的,下游速度慢于上游速度,数据久积成疾~ 需要做限流。

限流
在这里插入图片描述
这很好理解。既然上游处理较快,那么我添加一个限流机制将其速度降下来,让上下游速度基本一致,这样不就解决了吗。。

其实不然,这里有几个问题:

  1. 我无法提前预估下游实际速度(流速限制设置多少)
  1. 常碰到网络波动等情况,上下游的流速是动态变化的

考虑到这些原因,我的内部提供一种强大的反压机制:
在这里插入图片描述

上下游动态反馈,如果下游速度慢,则上游限速;否则上游提速。实现动态自动反压的效果。

反压机制示意
在这里插入图片描述
上游发送网络数据前经过自身的Network Buffer层,之后往下传输到Channel Buffer层(Netty通道)。最终通过网络传输,层层传递达到下游。

Network Buffer、Channel Buffer和Socket Buffer通俗理解就是用户态和内核态的区别,处于不同的交换空间和操作系统。

反压机制原理

前面做了一些铺垫,这里总结反压机制的运行流程:
在这里插入图片描述

  1. 每个TaskManager维护共享Network BufferPool(Task共享内存池),初始化时向Off-heap Memory中申请内存。
  2. 每个Task创建自身的Local BufferPool(Task本地内存池),并和Network BufferPool交换内存。
  3. 上游Record WriterLocal BufferPool申请buffer(内存)写数据。如果Local BufferPool没有足够内存则向Network BufferPool申请,使用完之后将申请的内存返回Pool
  4. Netty Buffer拷贝buffer并经过Socket Buffer发送到网络,后续下游端按照相似机制处理。
  5. 当下游申请buffer失败时,表示当前节点内存不够,则逐层发送反压信号给上游,上游慢慢停止数据发送,直到下游再次恢复。

在这里插入图片描述
所以,反压机制类似于Java中的阻塞队列,如下图为内存级的反压工作原理示意。
在这里插入图片描述
Task任务通过与Local BufferPoolNetwork BufferPool协作进行内存申请和释放,同时下游内存使用情况实时反馈给上游,实现动态反压。

参考:https://mp.weixin.qq.com/s/9pghVGfsWLLIpSRnj14XfA

31. 非对其checkpoint执行流程

作为 Flink 最基础也是最关键的容错机制,Checkpoint 快照机制很好地保证了 Flink 应用从异常状态恢复后的数据准确性。

同时 Checkpoint 相关的 metrics 也是诊断 Flink 应用健康状态最为重要的指标,成功且耗时较短的 Checkpoint 表明作业运行状况良好,没有异常或反压。

然而,由于 Checkpoint 与反压的耦合,反压反过来也会作用于 Checkpoint,导致 Checkpoint 的种种问题。

针对于此,Flink 在 1.11 引入 Unaligned Checkpint 来解耦 Checkpoint 机制与反压机制,优化高反压情况下的 Checkpoint 表现。

对齐场景:task接收到上游所有 Barrier,算子进行本地的 Checkpoint 快照,并在完成后异步上传本地快照,同时将 Barrier 以广播方式继续下发给下游。当某个 Checkpoint 的所有 Barrier 到达 DAG 末端且所有算子完成快照,则标志着全局快照的成功。
在有多个输入 Channel 的情况下,为了数据准确性,算子会等待所有流的 Barrier 都到达之后才会开始本地的快照,这种机制被称为 Barrier 对齐。在对齐的过程中,算子只会继续处理的来自未出现 Barrier Channel 的数据,而其余 Channel 的数据会被写入输入队列,直至在队列满后被阻塞。当所有 Barrier 到达后,算子进行本地快照,输出 Barrier 到下游并恢复正常处理。

开启方式

在 flink-conf.yaml 中设置:

state.checkpoints.unaligned.enabled: true

Checkpoint 与反压的耦合

目前的 Checkpoint 算法在大多数情况下运行良好,然而当作业出现反压时,阻塞式的 Barrier 对齐反而会加剧作业的反压,甚至导致作业的不稳定。

首先, Chandy-Lamport 分布式快照的结束依赖于 Marker 的流动,而反压则会限制 Marker 的流动,导致快照的完成时间变长甚至超时。无论是哪种情况,都会导致 Checkpoint 的时间点落后于实际数据流较多。这时作业的计算进度是没有被持久化的,处于一个比较脆弱的状态,如果作业出于异常被动重启或者被用户主动重启,作业会回滚丢失一定的进度。如果 Checkpoint 连续超时且没有很好的监控,回滚丢失的进度可能高达一天以上,对于实时业务这通常是不可接受的。更糟糕的是,回滚后的作业落后的 Lag 更大,通常带来更大的反压,形成一个恶性循环。

其次,Barrier 对齐本身可能成为一个反压的源头,影响上游算子的效率,而这在某些情况下是不必要的。比如典型的情况是一个的作业读取多个 Source,分别进行不同的聚合计算,然后将计算完的结果分别写入不同的 Sink。通常来说,这些不同的 Sink 会复用公共的算子以减少重复计算,但并不希望不同 Source 间相互影响。

Unaligned Checkpoint

为了解决这个问题,Flink 在 1.11 版本引入了 Unaligned Checkpoint 的特性。

一直以来 Flink 的 Aligned Checkpoint 通过 Barrier 对齐,将本地快照延迟至所有 Barrier 到达,从而巧妙地避免了对算子输入队列的状态进行快照,但代价是比较不可控的 Checkpoint 时长和吞吐量的降低。实际上这和 Chandy-Lamport 算法是有一定出入的。

举个例子,假设我们对两个数据流进行 equal-join,输出匹配上的元素。按照 Flink Aligned Checkpoint 的方式,系统的状态变化如下(图中不同颜色的元素代表属于不同的 Checkpoint 周期):
在这里插入图片描述
图 a: 输入 Channel 1 存在 3 个元素,其中 2 在 Barrier 前面;Channel 2 存在 4 个元素,其中 2、9、7 在 Barrier 前面。

图 b: 算子分别读取 Channel 一个元素,输出 2。随后接收到 Channel 1 的 Barrier,停止处理 Channel 1 后续的数据,只处理 Channel 2 的数据。

图 c: 算子再消费 2 个自 Channel 2 的元素,接收到 Barrier,开始本地快照并输出 Barrier。

对于相同的情况,Chandy-Lamport 算法的状态变化如下:
在这里插入图片描述
图 a: 同上。

图 b: 算子分别处理两个 Channel 一个元素,输出结果 2。此后接收到 Channel 1 的 Barrier,算子开始本地快照记录自己的状态,并输出 Barrier

图 c: 算子继续正常处理两个 Channel 的输入,输出 9。特别的地方是 Channel 2 后续元素会被保存下来,直到 Channel 2 的 Barrier 出现(即 Channel 2 的 9 和 7)。保存的数据会作为 Channel 的状态成为快照的一部分。

两者的差异主要可以总结为两点:

  • 快照的触发是在接收到第一个 Barrier 时还是在接收到最后一个 Barrier 时。
  • 是否需要阻塞已经接收到 Barrier 的 Channel 的计算。

从这两点来看,新的 Unaligned Checkpoint 将快照的触发改为第一个 Barrier 且取消阻塞 Channel 的计算,算法上与 Chandy-Lamport 基本一致,同时在实现细节方面结合 Flink 的定位做了几个改进。

首先,不同于 Chandy-Lamport 模型的只需要考虑算子输入 Channel 的状态,Flink 的算子有输入和输出两种 Channel,在快照时两者的状态都需要被考虑。

其次,无论在 Chandy-Lamport 还是 Flink Aligned Checkpoint 算法中,Barrier 都必须遵循其在数据流中的位置,算子需要等待 Barrier 被实际处理才开始快照。而 Unaligned Checkpoint 改变了这个设定,允许算子优先摄入并优先输出 Barrier。如此一来,第一个到达 Barrier 会在算子的缓存数据队列(包括输入 Channel 和输出 Channel)中往前跳跃一段距离,而被”插队”的数据和其他输入 Channel 在其 Barrier 之前的数据会被写入快照中(图中黄色部分)。
在这里插入图片描述
这样的主要好处是,如果本身算子的处理就是瓶颈,Chandy-Lamport 的 Barrier 仍会被阻塞,但 Unaligned Checkpoint 则可以在 Barrier 进入输入 Channel 就马上开始快照。这可以从很大程度上加快 Barrier 流经整个 DAG 的速度,从而降低 Checkpoint 整体时长

回到之前的例子,用 Unaligned Checkpoint 来实现,状态变化如下:
在这里插入图片描述
图 a: 输入 Channel 1 存在 3 个元素,其中 2 在 Barrier 前面;Channel 2 存在 4 个元素,其中 2、9、7 在 Barrier 前面。输出 Channel 已存在结果数据 1。

图 b: 算子优先处理输入 Channel 1 的 Barrier,开始本地快照记录自己的状态,并将 Barrier 插到输出 Channel 末端。

图 c: 算子继续正常处理两个 Channel 的输入,输出 2、9。同时算子会将 Barrier 越过的数据(即输入 Channel 1 的 2 和输出 Channel 的 1)写入 Checkpoint,并将输入 Channel 2 后续早于 Barrier 的数据(即 2、9、7)持续写入 Checkpoint。

比起 Aligned Checkpoint 中不同 Checkpoint 周期的数据以算子快照为界限分隔得很清晰,Unaligned Checkpoint 进行快照和输出 Barrier 时,部分本属于当前 Checkpoint 的输入数据还未计算(因此未反映到当前算子状态中),而部分属于当前 Checkpoint 的输出数据却落到 Barrier 之后(因此未反映到下游算子的状态中)。这也正是 Unaligned 的含义: 不同 Checkpoint 周期的数据没有对齐,包括不同输入 Channel 之间的不对齐,以及输入和输出间的不对齐。而这部分不对齐的数据会被快照记录下来,以在恢复状态时重放。换句话说,从 Checkpoint 恢复时,不对齐的数据并不能由 Source 端重放的数据计算得出,同时也没有反映到算子状态中,但因为它们会被 Checkpoint 恢复到对应 Channel 中,所以依然能提供只计算一次的准确结果。

当然,Unaligned Checkpoint 并不是百分百优于 Aligned Checkpoint,它会带来的已知问题就有:

  • 由于要持久化缓存数据,State Size 会有比较大的增长,磁盘负载会加重。
  • 随着 State Size 增长,作业恢复时间可能增长,运维管理难度增加。
  • 目前看来,Unaligned Checkpoint 更适合容易产生高反压同时又比较重要的复杂作业。对于像数据 ETL 同步等简单作业,更轻量级的 Aligned Checkpoint 显然是更好的选择。

总结

Flink 1.11 的 Unaligned Checkpoint 主要解决在高反压情况下作业难以完成 Checkpoint 的问题,同时它以磁盘资源为代价,避免了 Checkpoint 可能带来的阻塞,有利于提升 Flink 的资源利用率。

32. flink min-batch 了解吗

https://baijiahao.baidu.com/s?id=1708998984782185452&wfr=spider&for=pc

33. 滑动窗口窗口长度太长步长太小会有什么问题?例如窗口长度一天,步长5分钟

问题一 - 状态写入

首先是吞吐量,因为使用了 RocksDB 作为 Statebackend,所以写入和读取的速度都不会像在堆内那么快。例如在一天窗口长度和五分钟步长的情况下,针对每一个元素,需要遍历 288 个窗口并往对应的 State 写入数据,在窗口算子中,开销最大的往往就是对 State 的读写。所以同一个操作相比于滚动窗口,吞吐量下滑在 288 倍以上。

问题二 - 定时器

因为每个 key Window 对都对应一个 State,所以它们需要知道什么时候触发输出和清除,这个就需要依赖于定时器。

在 Flink 的实现中,基于 RocksDB 实现了一个定时器的最小堆,这个最小堆是根据定时器的注册时间排序的并且可以去重,时间小的会被先 pop 出来。

在窗口算子的实现中,针对每一个 key window 对,需要至少注册一个触发输出的定时器和一个清理窗口状态的计时器(因为有 allowLateness 这个 API)。虽然定时器可以去重,但是同样去重也会需要对状态的读取成本。由于定时器的读取与写入相对于 State 来说成本较低,但是还是对吞吐量有降级。

解决方案

我们一般使用滚动窗口+在线存储+读时聚合的思路作为解决方案:

(1)从业务的视角来看,往往窗口的长度是可以被步长所整除的,可以找到窗口长度和窗口步长的最小公约数作为时间分片(一个滚动窗口的长度);

(2)每个滚动窗口将其周期内的数据做聚合,存到下游状态或打入外部在线存储(内存数据库如Redis,LSM-based NoSQL存储如HBase);

(3)扫描在线存储中对应时间区间(可以灵活指定)的所有行,并将计算结果返回给前端展示。

参考:https://www.infoq.cn/article/sIhs_qY6HCpMQNblTI9M

34. timer trigger触发机制

在 Flink 中,定时器(Timer)是一种用于在特定时间间隔内执行特定操作的组件。定时器可以与 Flink 的各种窗口和算子相结合,实现复杂的时间处理逻辑。定时器的触发机制基于 Flink 的事件时间(Event Time)概念。

Flink 的定时器触发机制主要包括以下几个方面:

  1. 定时器创建:在 Flink 的算子中,可以通过 registerTimer 方法创建定时器。这个方法接收两个参数:定时器的时间间隔(以毫秒为单位)和定时器的触发操作。定时器创建后,将按照指定的时间间隔进行触发。

  2. 定时器与窗口的结合:在 Flink 中,定时器可以与各种窗口(如滚动窗口、滑动窗口和会话窗口)相结合。定时器触发时,可以执行特定操作,如计算窗口的结束时间、清除过期的窗口等。

  3. 定时器与算子的结合:在 Flink 中,定时器可以与各种算子(如 source、filter 和 window)相结合。定时器触发时,可以执行特定操作,如数据生成、数据过滤和窗口计算等。

  4. 定时器的延迟:Flink 中的定时器支持延迟触发。这意味着,如果定时器在触发时间到达时,还没有收到足够的数据,定时器会等待数据到达后再进行触发。这种延迟触发机制可以确保 Flink 的计算结果更加准确和完整。

  5. 定时器的重复触发:Flink 中的定时器可以设置为重复触发。这意味着,定时器在每次触发后,会立即开始计算下一个触发时间,并等待该时间到达后再进行触发。这种重复触发机制可以用于实现周期性的时间处理任务。

总之,Flink 的定时器触发机制基于事件时间概念,可以与各种窗口和算子相结合,实现复杂的时间处理逻辑。通过合理设置定时器的参数,可以有效提高 Flink 的计算性能和结果准确性。

35. Flink的双流join的底层原理?

  • union:union 支持双流 Join,也支持多流 Join。多个流类型必须一致;
  • connector:connector 支持双流 Join,两个流的类型可以不一致;
  • join:该方法只支持 inner join,即:相同窗口下,两个流中,Key都存在且相同时才会关联成功;
  • coGroup:同样能够实现双流 Join。即:将同一 Window 窗口内的两个DataStream 联合起来,两个流按照 Key 来进行关联,并通过 apply()方法 new CoGroupFunction() 的形式,重写 join() 方法进行逻辑处理。
  • intervalJoin:Interval Join 没有 Window 窗口的概念,直接用时间戳作为关联的条件,更具表达力。
    需要注意的是interval join实现的也是inner join,且目前只支持事件时间
  • 想实现多流join怎么办
    目前无法一次实现,可以考虑先union然后再二次处理;或者先进行connnect操作再进行join操作,仅建议~

可查看 https://herobin.blog.csdn.net/article/details/123294111 第七部分

join方案和demo可以看这个
https://blog.csdn.net/qq_24095055/article/details/124518516

https://blog.csdn.net/qq_24095055/article/details/124560386

36. flink 窗口触发的原理

窗口的计算是依赖触发器进行的,每种类型的窗口都有自己的触发器机制,如果用户没有指定,那么会使用默认的触发器。例如 TumblingEventTimeWindows 中自带的触发器如下:

@Override 
public Trigger<Object, TimeWindow> getDefaultTrigger(StreamExecutionEnvironment env) { 
   return EventTimeTrigger.create(); 
} 

可以看到源码中包含了一个 DefaultTrigger:EventTimeTrigger,可以去看看对应源码。

EventTimeTrigger 触发器的工作原理是:判断当前的水印是否超过了窗口的结束时间,如果超过则触发对窗口内数据的计算,否则不触发计算

Flink 本身提供了不同种类的触发器供我们使用,如下图所示:
在这里插入图片描述
触发器的类图如上图所示,它们的实际含义如下:

  • EventTimeTrigger:通过对比 Watermark 和窗口的 Endtime 确定是否触发窗口计算,如果 Watermark 大于 Window EndTime 则触发,否则不触发,窗口将继续等待。
  • ProcessTimeTrigger:通过对比 ProcessTime 和窗口 EndTime 确定是否触发窗口,如果 ProcessTime 大于 EndTime 则触发计算,否则窗口继续等待。
  • ContinuousEventTimeTrigger:根据间隔时间周期性触发窗口或者 Window 的结束时间小于当前 EndTime 触发窗口计算。
  • ContinuousProcessingTimeTrigger:根据间隔时间周期性触发窗口或者 Window 的结束时间小于当前 ProcessTime 触发窗口计算。
  • CountTrigger:根据接入数据量是否超过设定的阈值判断是否触发窗口计算。
  • DeltaTrigger:根据接入数据计算出来的 Delta 指标是否超过指定的 Threshold 去判断是否触发窗口计算。
  • PurgingTrigger:可以将任意触发器作为参数转换为 Purge 类型的触发器,计算完成后数据将被清理。
  • 我们在这里可以选择使用:ContinuousProcessingTimeTrigger 来周期性的触发窗口阶段性计算。

实现代码如下:

dataStream.windowAll(TumblingProcessingTimeWindows.of(Time.days(1), Time.hours(-8))) 
		  .trigger(ContinuousProcessingTimeTrigger.of(Time.seconds(20))) 

我们使用 ContinuousProcessingTimeTrigger 每隔 20 秒触发计算,输出中间结果。

37. flink state有哪几种,如何使用

state主要分为两大类:Keyed State 和 Operator State

  • Operator State 可以用在所有算子上
  • Keyed State 只能用在 KeyedStream 上

无论是 Keyed State 还是 Operator State,Flink 的状态都是基于本地的,即每个算子子任务维护着这个算子子任务对应的状态存储,算子子任务之间的状态不能相互访问

Keyed State 有如下几种数据结构:

  • ValueState
  • MapState
  • AppendingState
    • ListState
    • ReducingState
    • AggregatingState
  • ReadOnlyBrodcastState

Operator State 有如下几种数据结构:

  • List state
  • Union list state
  • Broadcast state

通过继承 RichFlatMapFunction 来访问 State,通过 getRuntimeContext().getState(descriptor) 来获取状态的句柄。而真正的访问和更新状态则在 Map 函数中实现。

Operator State 的实际应用场景不如 Keyed State 多,一般来说它会被用在 Source 或 Sink 等算子上,用来保存流入数据的偏移量或对输出数据做缓存,以保证 Flink 应用的 Exactly-Once 语义。

同样,我们对于任何状态数据还可以设置它们的过期时间。如果一个状态设置了 TTL,并且已经过期,那么我们之前保存的值就会被清理。

想要使用 TTL,我们需要首先构建一个 StateTtlConfig 配置对象;然后,可以通过传递配置在任何状态描述符中启用 TTL 功能。

Demo:

public static void main(String[] args) throws Exception {

   final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

   env.fromElements(Tuple2.of(1L, 3L), Tuple2.of(1L, 5L), Tuple2.of(1L, 7L), Tuple2.of(1L, 5L), Tuple2.of(1L, 2L))
         .keyBy(0)
         .flatMap(new CountWindowAverage())
         .printToErr();

       env.execute("submit job");

}


public static class CountWindowAverage extends RichFlatMapFunction<Tuple2<Long, Long>, Tuple2<Long, Long>> {

     private transient ValueState<Tuple2<Long, Long>> sum;
       
     public void flatMap(Tuple2<Long, Long> input, Collector<Tuple2<Long, Long>> out) throws Exception {

           Tuple2<Long, Long> currentSum;
           // 访问ValueState
           if(sum.value()==null){
               currentSum = Tuple2.of(0L, 0L);
           }else {
               currentSum = sum.value();
           }

           // 更新
           currentSum.f0 += 1;

           // 第二个元素加1
           currentSum.f1 += input.f1;

           // 更新state
           sum.update(currentSum);

           // 如果count的值大于等于2,求知道并清空state
           if (currentSum.f0 >= 2) {
               out.collect(new Tuple2<>(input.f0, currentSum.f1 / currentSum.f0));
               sum.clear();
           }
   	}


   public void open(Configuration config) {
       ValueStateDescriptor<Tuple2<Long, Long>> descriptor =
               new ValueStateDescriptor<>(
                       "average", // state的名字
                       TypeInformation.of(new TypeHint<Tuple2<Long, Long>>() {})
                       ); // 设置默认值


       StateTtlConfig ttlConfig = StateTtlConfig
               .newBuilder(Time.seconds(10))
               .setUpdateType(StateTtlConfig.UpdateType.OnCreateAndWrite)
               .setStateVisibility(StateTtlConfig.StateVisibility.NeverReturnExpired)
               .build();

       descriptor.enableTimeToLive(ttlConfig)

       sum = getRuntimeContext().getState(descriptor);
   }
}

38. fink如何进行分流

使用 SideOutPut 分流

  1. 先定义 outputtag
  2. 在process中通过ctx.output将数据分发到不同的output中
  3. 通过stream.getSideOutput获取某个outputtag对应的数据

Demo:

public static void main(String[] args) throws Exception {

    StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
    //获取数据源
    List data = new ArrayList<Tuple3<Integer,Integer,Integer>>();
    data.add(new Tuple3<>(0,1,0));
    data.add(new Tuple3<>(0,1,1));
    data.add(new Tuple3<>(0,2,2));
    data.add(new Tuple3<>(0,1,3));
    data.add(new Tuple3<>(1,2,5));
    data.add(new Tuple3<>(1,2,9));
    data.add(new Tuple3<>(1,2,11));
    data.add(new Tuple3<>(1,2,13));


    DataStreamSource<Tuple3<Integer,Integer,Integer>> items = env.fromCollection(data);

    OutputTag<Tuple3<Integer,Integer,Integer>> zeroStream = new OutputTag<Tuple3<Integer,Integer,Integer>>("zeroStream") {};
    OutputTag<Tuple3<Integer,Integer,Integer>> oneStream = new OutputTag<Tuple3<Integer,Integer,Integer>>("oneStream") {};


    SingleOutputStreamOperator<Tuple3<Integer, Integer, Integer>> processStream= items.process(new ProcessFunction<Tuple3<Integer, Integer, Integer>, Tuple3<Integer, Integer, Integer>>() {
        @Override
        public void processElement(Tuple3<Integer, Integer, Integer> value, Context ctx, Collector<Tuple3<Integer, Integer, Integer>> out) throws Exception {

            if (value.f0 == 0) {
                ctx.output(zeroStream, value);
            } else if (value.f0 == 1) {
                ctx.output(oneStream, value);
            }
        }
    });

    DataStream<Tuple3<Integer, Integer, Integer>> zeroSideOutput = processStream.getSideOutput(zeroStream);
    DataStream<Tuple3<Integer, Integer, Integer>> oneSideOutput = processStream.getSideOutput(oneStream);

    zeroSideOutput.print();
    oneSideOutput.printToErr();


    //打印结果
    String jobName = "user defined streaming source";
    env.execute(jobName);
}

39. flink如何做维表关联

  • 实时查询维表
  • 预加载全量数据
  • LRU 缓存
  • 其他

具体看这里第二十讲:https://herobin.blog.csdn.net/article/details/108576816

40. flink消费kafka大概流程

整体的流程:FlinkKafkaConsumer 首先创建了 KafkaFetcher 对象,然后 KafkaFetcher 创建了 KafkaConsumerThread 和 Handover,KafkaConsumerThread 负责直接从 Kafka 中读取 msg,并交给 Handover,然后 Handover 将 msg 传递给 KafkaFetcher.emitRecord 将消息发出。

41. flink支持的窗口函数有哪些

目前 Flink 支持的窗口函数包含 3 种:

  • ReduceFunction 增量聚合
  • AggregateFunction 增量聚合
  • ProcessWindowFunction 全量聚合

具体可看第二十六讲:
https://herobin.blog.csdn.net/article/details/108576816

42. Flink State TTL 是怎么做到数据过期的?

具体可看第六、七部分
https://herobin.blog.csdn.net/article/details/124410328

43. Flink Checkpoint 在 HDFS 的存储格式?

这里也分 keyed-state 和 operator-state 进行说明。Flink 会将 Checkpoint 数据存储在一个带有编号的 chk 目录中。

比如说一个 Flink 任务的 keyed-state 的 subTask 个数是 10,operator-state 对应的 subTask 也是 10,那么 chk 会存一个元数据文件 _metadata,10 个 keyed-state 文件,10 个 operator-state 的文件。

文件格式是:

/配置的checkpoint路径/jobName/applicationId/chk-次数/_metadata或operater_state文件或operator-state文件
demo: 
/flink_yarn/flink-checkpoints/activityfllow_dag_samplefeature0311/82e7fcfd7d59751b233e4f3829aab4fd/chk-8/_metadata

在这里插入图片描述

44. 使用eventTime时如何触发计时器timer(watermark触发timer机制)

在这里插入图片描述
一个算子的实例在收到 watermark 的时候,首先要更新当前的算子时间,这样的话在 ProcessFunction 里方法查询这个算子时间的时候,就能获取到最新的时间。第二步它会遍历计时器队列,这个计时器队列就是我们刚刚说到的 timer,你可以同时注册很多 timer,Flink 会把这些 Timer 按照触发时间放到一个优先队列中。第三步 Flink 得到一个时间之后就会遍历计时器的队列,然后逐一触发用户的回调逻辑。 通过这种方式,Flink 的某一个任务就会将当前的 watermark 发送到下游的其他任务实例上,从而完成整个 watermark 的传播,从而形成一个闭环。

45. slot 是 jvm 共享的吗

每个 TaskManager 都是一个 JVM 进程,可以在单独的线程中执行一个或多个 subtask。为了控制一个 TaskManager 中接受多少个 task,就有了所谓的 task slots(至少一个)。

每个 task slot 代表 TaskManager 中资源的固定子集。例如,具有 3 个 slot 的 TaskManager,会将其托管内存 1/3 用于每个 slot。分配资源意味着 subtask 不会与其他作业的 subtask 竞争托管内存,而是具有一定数量的保留托管内存。注意此处没有 CPU 隔离;当前 slot 仅分离 task 的托管内存

通过调整 task slot 的数量,用户可以定义 subtask 如何互相隔离。每个 TaskManager 有一个 slot,这意味着每个 task 组都在单独的 JVM 中运行(例如,可以在单独的容器中启动)。具有多个 slot 意味着更多 subtask 共享同一 JVM。同一 JVM 中的 task 共享 TCP 连接(通过多路复用)和心跳信息。它们还可以共享数据集和数据结构,从而减少了每个 task 的开销。

默认情况下,Flink 允许 subtask 共享 slot,即便它们是不同的 task 的 subtask,只要是来自于同一作业即可。结果就是一个 slot 可以持有整个作业管道。允许 slot 共享有两个主要优点

  • Flink 集群所需的 task slot 和作业中使用的最大并行度恰好一样。无需计算程序总共包含多少个 task(具有不同并行度)。

  • 容易获得更好的资源利用。如果没有 slot 共享,非密集 subtask(source/map())将阻塞和密集型 subtask(window) 一样多的资源。通过 slot 共享,我们示例中的基本并行度从 2 增加到 6,可以充分利用分配的资源,同时确保繁重的 subtask 在 TaskManager 之间公平分配。

注意,slot之间是共享jvm的,只有托管内存(RockDB状态后端使用的部分)是互相隔离的,对内存、CPU、TCP这些都是共享的。

46. slot是设置越多越好吗,一个tm 3个slot每个开4个处理线程,和一个tm直接开12个slot有啥差别吗?

也不是越多越好,首先一个tm里的slot是共享tcp连接和cpu等资源的,如果是计算密集型要设置的少一点,不然线程持续获取不到cpu资源也没法执行,如果是IO密集型可以多分配些slot,让cpu得到充分利用。

一个tm 3个slot 每个开3个线程和直接开12个slot,感觉是没啥大的差别,emmm应该也有一点,因为slot是可以被多个task复用的,如果有一个算子计算压力比较大我们分了好多slot,那下游并行度低的算子就没法和它chain到一起了,算子之间会多了传输的序列化和反序列化的开销。

47. open 和 构造器的区别

open 方法进行初始化功能更加完善,可以获取环境的上下文信息,创建本地状态,访问广播变量和metrics指标监控等信息。

48. flink sql 了解吗?

参考:https://mp.weixin.qq.com/mp/appmsgalbum?action=getalbum&__biz=MzkxNjA1MzM5OQ==&scene=1&album_id=2005503056204890112&count=3#wechat_redirect

49. 说明下一个 Flink SQL 如何变成一个的 Flink 作业的过程

Flink 引擎接收到一个 SQL 文本后,通过 SqlParser 将其解析成 SqlNode。通过查询 Catalog 中的元数据信息,对 SqlNode 中的表、字段、类型、udf 等进行校验,校验通过后会转换为 LogicalPlan,通过 Optimizer 优化后转换为 ExecPlan。ExecPlan 是 SQL 层的最终执行计划,通过 CodeGen 技术将执行计划翻译成可执行算子,用 Transformation 来描述,最后转换为 JobGraph 并提交到 Flink 集群上执行。

https://mp.weixin.qq.com/s/QlgQyh-sx4lMxdKo0rW-1g

50. flink sql中catalog_name db_name table_name三者的区别

在 Flink SQL 中,catalog_namedb_nametable_name 分别代表不同的概念:

  1. catalog_name(目录名):它是 Flink 的概念,用于管理和组织数据源。一个 Flink 程序可以连接多个不同的数据源,每个数据源都可以被称为一个目录(catalog)。目录可以包含一个或多个数据库。

  2. db_name(数据库名):在一个目录中,可以创建多个数据库(database)。数据库用于逻辑上组织和隔离数据表。类似于传统数据库中的概念,不同的数据库之间可以相互独立,拥有自己的表和视图。

  3. table_name(表名):表是数据库中的基本存储单元,用于存储数据。表由列(column)和行(row)组成,每个列定义了特定类型的数据,每个行包含了一条记录。表名用于唯一标识一个表,可以在查询中引用表名来操作表中的数据。

总结一下,catalog_name 是 Flink 中用于管理数据源的概念,db_name 是目录下的一个逻辑数据库,而 table_name 是数据库中的一个具体表。在 Flink SQL 查询中,通过指定这三者的名称,可以定位到具体的数据表,以便进行数据操作和查询。

51. Flink CDC + Hudi 推进实时业务落地

https://mp.weixin.qq.com/s/VPwBWGZUdacIrG-63EXVFw

Hadoop

1. hdfs-ha高可用方案

HA 概述

  1. 所谓 HA(High Available),即高可用(7*24 小时不中断服务)。
  2. 实现高可用最关键的策略是消除单点故障。HA 严格来说应该分成各个组件的 HA 机制:HDFS 的 HA 和 YARN 的 HA。
  3. Hadoop2.0 之前,在 HDFS 集群中 NameNode 存在单点故障(SPOF)。
  4. NameNode 主要在以下两个方面影响 HDFS 集群
    NameNode 机器发生意外,如宕机,集群将无法使用,直到管理员重启
    NameNode 机器需要升级,包括软件、硬件升级,此时集群也将无法使用

HDFS HA 功能通过配置 Active/Standby 两个 NameNodes 实现在集群中对 NameNode 的
热备来解决上述问题。如果出现故障,如机器崩溃或机器需要升级维护,这时可通过此种方
式将 NameNode 很快的切换到另外一台机器。

HDFS-HA 工作机制

通过双 NameNode 消除单点故障

HDFS-HA 工作要点

  1. 元数据管理方式需要改变
    • 内存中各自保存一份元数据;
    • Edits 日志只有 Active 状态的 NameNode 节点可以做写操作;
    • 两个 NameNode 都可以读取 Edits;
    • 共享的 Edits 放在一个共享存储中管理(qjournal 和 NFS 两个主流实现);
  2. 需要一个状态管理功能模块
    实现了一个 zkfailover,常驻在每一个 namenode 所在的节点,每一个 zkfailover 负责监
    控自己所在 NameNode 节点,利用 zk 进行状态标识,当需要进行状态切换时,由 zkfailover 来负责切换,切换时需要防止 brain split 现象的发生。
  3. 必须保证两个 NameNode 之间能够 ssh 无密码登录
  4. 隔离(Fence),即同一时刻仅仅有一个 NameNode 对外提供服务

HDFS-HA 自动故障转移工作机制

下面学习如何配置部署 HA 自动进行故障转移。自动故障转移为 HDFS 部署增加了两个新组件:ZooKeeperZKFailoverController(ZKFC)进程,如图 3-20 所示。ZooKeeper 是维护少量
协调数据,通知客户端这些数据的改变和监视客户端故障的高可用服务。HA 的自动故障转
移依赖于 ZooKeeper 的以下功能:

  1. 故障检测:集群中的每个 NameNode 在 ZooKeeper 中维护了一个持久会话,如果
    机器崩溃,ZooKeeper 中的会话将终止,ZooKeeper 通知另一个 NameNode 需要触发故障转移。

  2. 现役 NameNode 选择:ZooKeeper 提供了一个简单的机制用于唯一的选择一个节点为 active 状态。如果目前现役 NameNode 崩溃,另一个节点可能从 ZooKeeper 获得特殊的排外锁以表明它应该成为现役 NameNode。

    ZKFC 是自动故障转移中的另一个新组件,是 ZooKeeper 的客户端,也监视和管理
    NameNode 的状态。每个运行 NameNode 的主机也运行了一个 ZKFC 进程,ZKFC 负责:

    • 健康监测:ZKFC 使用一个健康检查命令定期地 ping 与之在相同主机的 NameNode,只要该 NameNode 及时地回复健康状态,ZKFC 认为该节点是健康的。如果该节点崩溃,冻结或进入不健康状态,健康监测器标识该节点为非健康的。
    • ZooKeeper 会话管理:当本地 NameNode 是健康的,ZKFC 保持一个在 ZooKeeper 中打开的会话。如果本地 NameNode 处于 active 状态,ZKFC 也保持一个特殊的 znode 锁,该锁使用了 ZooKeeper 对短暂节点的支持,如果会话终止,锁节点将自动删除。
    • 基于 ZooKeeper 的选择:如果本地 NameNode 是健康的,且 ZKFC 发现没有其它的节点当前持有 znode 锁,它将为自己获取该锁。如果成功,则它已经赢得了选择,并负责运行故障转移进程以使它的本地 NameNode 为 Active。故障转移进程与前面描述的手动故障转移相似,首先如果必要保护之前的现役 NameNode,然后本地 NameNode 转换为 Active 状态。

在这里插入图片描述

2. yarn如何实现高可用方案(YARN-HA工作机制)?

  1. 官方文档:
    http://hadoop.apache.org/docs/r2.7.2/hadoop-yarn/hadoop-yarn-site/ResourceManagerHA.html
  2. YARN-HA 工作机制
    在这里插入图片描述

3. hdfs HA 共享存储是怎么实现的?

4. yarn如何实现资源隔离?

双层调度器(Two-Level Scheduler)

顾名思义,双层调度器将整个调度工作划分为两层:中央调度器和框架调度器。中央调度器管理集群中所有资源的状态,它拥有集群所有的资源信息,按照一定策略(例如 FIFO、Fair、Capacity、Dominant Resource Fair)将资源粗粒度地分配给框架调度器,各个框架调度器收到资源后再根据应用申请细粒度将资源分配给容器执行具体的计算任务。在这种双层架构中,每个框架调度器看不到整个集群的资源,只能看到中央调度器给自己的资源,如图所示:
在这里插入图片描述
紫色和绿色的圆圈所在的方框是框架调度器,可以看到中央调度器把全部资源的两个子集分别交给了两个框架调度器,注意看,这两个子集是没有重合的,这种机制类似于并发中的悲观并发。

主调度器拥有整个集群资源的的状态,通过 Offer(主动提供,而不是被动请求)方式通知每个二级调度器有哪些可用的资源。每个二级调度器根据自己的需求决定是否占有提供的资源,决定占有后,该分区内的资源由二级调度器全权负责。

5. YARN 启动一个 MapReduce 作业的流程

如图所示:
在这里插入图片描述
第 1 步:客户端向 ResourceManager 提交自己的应用,这里的应用就是指 MapReduce 作业。

第 2 步:ResourceManager 向 NodeManager 发出指令,为该应用启动第一个 Container,并在其中启动 ApplicationMaster。

第 3 步:ApplicationMaster 向 ResourceManager 注册。

第 4 步:ApplicationMaster 采用轮询的方式向 ResourceManager 的 YARN Scheduler 申领资源。

第 5 步:当 ApplicationMaster 申领到资源后(其实是获取到了空闲节点的信息),便会与对应 NodeManager 通信,请求启动计算任务。

第 6 步:NodeManager 会根据资源量大小、所需的运行环境,在 Container 中启动任务。

第 7 步:各个任务向 ApplicationMaster 汇报自己的状态和进度,以便让 ApplicationMaster 掌握各个任务的执行情况。

第 8 步:应用程序运行完成后,ApplicationMaster 向 ResourceManager 注销并关闭自己。

6. HDFS 多副本放置策略

大型的 HDFS 实例在通常分布在多个机架的多台服务器上,不同机架上的两台服务器之间通过交换机进行通讯。在大多数情况下,同一机架中的服务器间的网络带宽大于不同机架中的服务器之间的带宽。因此 HDFS 采用机架感知副本放置策略,对于常见情况,当复制因子为 3 时,HDFS 的放置策略是:

在写入程序位于 datanode 上时,就优先将写入文件的一个副本放置在该 datanode 上,否则放在随机 datanode 上。之后在另一个远程机架上的任意一个节点上放置另一个副本,并在该机架上的另一个节点上放置最后一个副本。此策略可以减少机架间的写入流量,从而提高写入性能。

7. 先有am还是先有applicationId?

先有applicationId,再启动的am

可以从YARN工作原理详述中看出来:
在这里插入图片描述
1. 作业提交

client 调用 job.waitForCompletion 方法,向整个集群提交 MapReduce 作业 (第 1 步) 。新的作业 ID(应用 ID) 由资源管理器分配 (第 2 步)。作业的 client 核实作业的输出, 计算输入的 split, 将作业的资源 (包括 Jar 包,配置文件, split 信息) 拷贝给 HDFS(第 3 步)。 最后, 通过调用资源管理器的 submitApplication() 来提交作业 (第 4 步)。

2. 作业初始化

当资源管理器收到 submitApplciation() 的请求时, 就将该请求发给调度器 (scheduler), 调度器分配 container, 然后资源管理器在该 container 内启动应用管理器进程, 由节点管理器监控 (第 5 步)。

MapReduce 作业的应用管理器是一个主类为 MRAppMaster 的 Java 应用,其通过创造一些 bookkeeping 对象来监控作业的进度, 得到任务的进度和完成报告 (第 6 步)。然后其通过分布式文件系统得到由客户端计算好的输入 split(第 7 步),然后为每个输入 split 创建一个 map 任务, 根据 mapreduce.job.reduces 创建 reduce 任务对象。

3. 任务分配

如果作业很小, 应用管理器会选择在其自己的 JVM 中运行任务。

如果不是小作业, 那么应用管理器向资源管理器请求 container 来运行所有的 map 和 reduce 任务 (第 8 步)。这些请求是通过心跳来传输的, 包括每个 map 任务的数据位置,比如存放输入 split 的主机名和机架 (rack),调度器利用这些信息来调度任务,尽量将任务分配给存储数据的节点, 或者分配给和存放输入 split 的节点相同机架的节点。

4. 任务运行

当一个任务由资源管理器的调度器分配给一个 container 后,应用管理器通过联系节点管理器来启动 container(第 9 步)。任务由一个主类为 YarnChild 的 Java 应用执行, 在运行任务之前首先本地化任务需要的资源,比如作业配置,JAR 文件, 以及分布式缓存的所有文件 (第 10 步。 最后, 运行 map 或 reduce 任务 (第 11 步)。

YarnChild 运行在一个专用的 JVM 中, 但是 YARN 不支持 JVM 重用。

5. 进度和状态更新

YARN 中的任务将其进度和状态 (包括 counter) 返回给应用管理器, 客户端每秒 (通 mapreduce.client.progressmonitor.pollinterval 设置) 向应用管理器请求进度更新, 展示给用户。

6. 作业完成

除了向应用管理器请求作业进度外, 客户端每 5 分钟都会通过调用 waitForCompletion() 来检查作业是否完成,时间间隔可以通过 mapreduce.client.completion.pollinterval 来设置。作业完成之后, 应用管理器和 container 会清理工作状态, OutputCommiter 的作业清理方法也会被调用。作业的信息会被作业历史服务器存储以备之后用户核查。

8. ApplicationMaster 生命周期

ApplicationMaster 管理主要由三个服务构成,分别是 ApplicationMasterLauncher、AMLivelinessMonitor 和 ApplicationMasterService,它们共同管理应用程序的 ApplicationMaster 的生命周期。ApplicationMaster 服务从创建到销毁的流程如下:

  1. 这里再加个前置步骤:提交是客户端通过yarnClient的submitApplication()函数与 RM 进行 RPC 通信来提交应用。
  2. 用户向 ResourceManager 提交应用程序,ResourceManager 收到提交请求后,先向资源调度器申请用以启动 ApplicationMaster 的资源,待申请到资源后,再由 ApplicationMasterLauncher 与对应的 NodeManager 通信,从而启动应用程序的 ApplicationMaster。
  3. ApplicationMaster 启动完成后,ApplicationMasterLauncher 会通过事件的形式,将刚刚启动的 ApplicationMaster 注册到 AMLivelinessMonitor,以启动心跳监控。
  4. ApplicationMaster 启动后,先向 ApplicationMasterService 注册,将自己所在 host、端口号等信息汇报给它。
  5. ApplicationMaster 运行过程中,周期性地向 ApplicationMasterService 汇报“心跳”信息(“心跳”信息中包含想要申请的资源描述)。
  6. ApplicationMasterService 每次收到 ApplicationMaster 的心跳信息后,将通知 AMLivelinessMonitor 更新该应用程序的最近汇报心跳的时间。
  7. 当应用程序运行完成后,ApplicationMaster 向 ApplicationMasterService 发送请求,注销自己。
  8. ApplicationMasterService 收到注销请求后,标注应用程序运行状态为完成,同时通知 AMLivelinessMonitor 移除对它的心跳监控。

Kafka

1. Kafka能否保证顺序消费?

2. kafka如何做到高并发。为啥快?

Kafka 高性能,是多方面协同的结果,包括

  • 顺序 I/O
    • 在顺序读写的情况下,磁盘的顺序读写速度和内存持平
    • 磁盘是机械结构,每次读写都会寻址->写入,顺序io只寻址一次
    • Kafka 利用了一种分段式的、只追加 (Append-Only) 的日志,把自身的读写操作限制为顺序 I/O,也就使得它在各种存储介质上能有很快的速度
    • 每一个 Partition 其实都是一个文件 ,收到消息后 Kafka 会把数据插入到文件末尾
    • 采用只读设计 ,Kafka 不会修改、删除数据
    • Kafka 的 message 是不断追加到本地磁盘文件末尾的,而不是随机的写入,这使得 Kafka 写入吞吐量得到了显著提升
  • 页缓存
    • 即便是顺序写入硬盘,硬盘的访问速度还是不可能追上内存。所以 Kafka 的数据并不是实时的写入硬盘 ,它充分利用了现代操作系统分页存储来利用内存提高 I/O 效率。具体来说,就是把磁盘中的数据缓存到内存中,把对磁盘的访问变为对内存的访问。
    • Kafka 接收来自 socket buffer 的网络数据,应用进程不需要中间处理、直接进行持久化时。可以使用mmap 内存文件映射。
    • Kafka 中大量使用了页缓存,这是 Kafka 实现高吞吐的重要因素之一。
    • 消息先被写入页缓存,由操作系统负责刷盘任务。
  • 零拷贝
    • Kafka 通过利用 Java 的 NIO 框架,尤其是 java.nio.channels.FileChannel 里的 transferTo 这个方法,解决了前面提到的在 Linux 等类 UNIX 系统上的数据拷贝问题。此方法能够在不借助作为传输中介的应用程序的情况下,将字节数据从源通道直接传输到接收通道。
  • Broker 性能
    • 日志记录批处理:Kafka 的 Clients 和 Brokers 会把多条读写的日志记录合并成一个批次,然后才通过网络发送出去。日志记录的批处理通过使用更大的包以及提高带宽效率来摊薄网络往返的开销。
    • 批量压缩
    • 非强制刷新缓冲写操作*
  • 分布式 partition 存储
  • ISR 数据同步
  • 以及“无所不用其极”的高效利用磁盘、操作系统特性。

总结

mmap 和 sendfile

  • Linux 内核提供、实现零拷贝的 API。
  • mmap 将磁盘文件映射到内存,支持读和写,对内存的操作会反映在磁盘文件上。
  • sendfile 是将读到内核空间的数据,转到 socket buffer,进行网络发送。
  • RocketMQ 在消费消息时,使用了 mmap;Kafka 使用了 sendfile。

Kafka 为啥这么快?

  • Partition 顺序读写,充分利用磁盘特性,这是基础。
  • Producer 生产的数据持久化到 Broker,采用 mmap 文件映射,实现顺序的快速写入。
  • Customer 从 Broker 读取数据,采用 sendfile,将磁盘文件读到 OS 内核缓冲区后,直接转到 socket buffer 进行网络发送。
  • Broker 性能优化:日志记录批处理、批量压缩、非强制刷新缓冲写操作等。
  • 流数据并行

参考:https://www.jianshu.com/p/53b8ec516a0b

3. 写kafka,ack有几种机制

  • properties.put("acks", "0")

    • 意味着producer不等待broker同步完成的确认,继续发送下一条(批)信息
    • 提供了最低的延迟。但是最弱的持久性,当服务器发生故障时,就很可能发生数据丢失。例如leader已经死亡,producer不知情,还会继续发送消息broker接收不到数据就会数据丢失
  • properties.put("acks", "1")

    • 意味着producer要等待leader成功收到数据并得到确认,才发送下一条message。此选项提供了较好的持久性较低的延迟性。
    • Partition的Leader死亡,follwer尚未复制,数据就会丢失
  • properties.put("acks", "-1")

    • 意味着producer得到follwer确认,才发送下一条数据
    • 持久性最好,延时性最差。

三种机制性能递减,可靠性递增

4. kafka的页缓存

页缓存是操作系统实现的一种主要的磁盘缓存,以此用来减少对磁盘I/O的操作。具体来说,就是把磁盘中的数据缓存到内存中,把对磁盘的访问变为对内存的访问。

当一个进程准备读取磁盘上的文件内容时,操作系统会先查看待读取的数据所在的页(page)是否在页缓存(page cache)中,如果存在(命中)则直接返回数据,从而避免了对物理磁盘I/O操作;如果没有命中,则操作系统会向磁盘发起读取请示并将读取的数据页写入页缓存,之后再将数据返回进程。同样,如果一个进程需要将数据写入磁盘,那么操作系统也会检测数据对应的页是否在页缓存中,如果不存在,则会先在页缓存中添加相应的页,最后将数据写入对应的页。被修改过后的页也就变成了脏页,操作系统会在合适的时间把脏页中的数据写入磁盘,以操作数据的一致性。

Linux操作系统中的vm.dirty_background_ratio参数用来指定当脏页数量达到系统内存的百分之多少之后就会触发pdflush/flush/kdmflush等后台回写进程的运行来处理脏页,一般设置为小于10%的值即可,但不建议设置为0.与这个参数对应的还一个vm.dirty_ratio参数,它用来指定当脏页数量达到系统内存的百分之多少之后就不得不开始对脏页进行处理,在此过程中,新的I/O请求会被阻挡直至所有脏页被冲刷到磁盘中。

对一个进程页言,它会在进程内部缓存处理所需的数据,然而这些数据有可能还缓存在操作系统的页缓存中,因此同一份数据有可能被缓存了2次。并且,除非使用Direct I/O的方式,否则页缓存很难被禁止。此外,Java对象的内存开销非常大,通常会是真实数据大小的几倍甚至更多,空间使用率你下;Java的垃圾回收会随着堆内数据的增多而变得越来越慢。基于这些因此,使用文件系统并依赖于页缓存的做法明显要优于维护一个进程内缓存或其它结构,至少可以省去一份进程内部的缓存消耗,同时还可以通过结构紧凑的字节码来替代使用对象的方式以节省更多的空间。如此,可以在32GB的机器上使用28GB至30GB的内存而不用担心GC所带来的性能问题。此外,即使Kafka服务重启,页缓存还是会保持有效,然而进程内的缓存却需要重建。这样也极大地简化了代码逻辑,因为维护页缓存和文件之间的一致性交由系统来负责,这样会比进程内维护更加安全有效。

Kafka中大量使用了页缓存,这是Kafka实现高吞吐的重要因此之一。虽然消息都是先被写入页缓存,然后由操作系统负责具体的刷盘任务,但在Kafka中同样提供了同步刷盘及间断性强制刷盘(fsync)的功能,这些功能可以通过log.flush.interval.message、log.flush.interval.ms等参数来控制。同步刷盘可以提高 消息的可行性,防止由于机器掉电等异常造成处于页缓存而没有及时写入磁盘的消息丢失。不过一般不建议这么做,刷盘任务就应交由操作系统去调配,消息的可靠性应该由多副本机制来保障,而不是由同步刷盘这种严重影响性能的行为来保障。

5. kafka如何实现零拷贝?

传统的读取数据并发送到网络的步骤如下:

  1. 操作系统将数据从磁盘文件中读取到内核空间的页面进行缓存;
  2. 应用程序将数据从内核空间读入用户空间缓冲区;
  3. 应用程序将读到的数据写回到内核空间并放入socket缓冲区;
  4. 操作系统将数据从socket缓冲区复制到网卡接口,此时数据才能通过网络进行发送。

在这里插入图片描述
kafka的零拷贝技术

通常情况下,kafka的消息会有多个订阅者,生产者发布的消息会被不同的消费者多次消费,为了优化这个流程,kafka使用了“零拷贝技术”,如下:
在这里插入图片描述
“零拷贝技术”只用将磁盘文件的数据复制到页面缓冲区一次,然后将数据从页面的缓存直接发送到网络中(发送给不同的订阅者时,都可以使用同一个页面进行缓存),避免了重复复制操作。

如果有10个消费者,传统方式下,数据复制次数为4 * 10 = 40 次,而是用零拷贝技术只需要复制1 + 10 = 11 次,其中一次为从磁盘复制到页面缓存,10 次表示10个消费者各自读取一次页面缓存,由此可以看出kafka的效率是非常高的。

在 Kafka 中,体现 Zero Copy 使用场景的地方有两处:基于 mmap 的索引和日志文件读写所用的 TransportLayer。

先说第一个。索引都是基于 MappedByteBuffer 的,也就是让用户态和内核态共享内核态的数据缓冲区,此时,数据不需要复制到用户态空间。不过,mmap 虽然避免了不必要的拷贝,但不一定就能保证很高的性能。在不同的操作系统下,mmap 的创建和销毁成本可能是不一样的。很高的创建和销毁开销会抵消 Zero Copy 带来的性能优势。由于这种不确定性,在 Kafka 中,只有索引应用了 mmap,最核心的日志并未使用 mmap 机制。

再说第二个。TransportLayer 是 Kafka 传输层的接口。它的某个实现类使用了 FileChannel 的 transferTo 方法。该方法底层使用 sendfile 实现了 Zero Copy。对 Kafka 而言,如果 I/O 通道使用普通的 PLAINTEXT,那么,Kafka 就可以利用 Zero Copy 特性,直接将页缓存中的数据发送到网卡的 Buffer 中,避免中间的多次拷贝。相反,如果 I/O 通道启用了 SSL,那么,Kafka 便无法利用 Zero Copy 特性了。

mmap 通过内存映射,将文件映射到内核缓冲区,同时,用户空间可以共享内核空间的数据。这样,在进行网络传输时,就可以减少内核空间到用户空间的拷贝次数。

6. kafka 如何保证写入不重

7. kafka 如何分配replica

Kafka分配Replica的算法如下(注意:下面的broker、partition副本数这些编号都是从0开始编号的):

  • 将所有存活的N个Brokers和待分配的Partition排序
  • 将第i个Partition分配到第(i mod n)个Broker上,这个Partition的第一个 Replica存在于这个分配的Broker上,并且会作为partition的优先副本( 这里就基本 说明了一个topic的partition在集群上的大致分布情况 )
  • 将第i个Partition的第j个Replica分配到第((i + j) mod n)个Broker上 假设集群一共有4个brokers,一个topic有4个partition,每个Partition有3个副本。下图是 每个Broker上的副本分配情况。

8. kafka 如何判断broker是否存活

对于Kafka而言,定义一个Broker是否“活着”包含两个条件:

  • 一是它必须维护与ZooKeeper的session(这个通过ZooKeeper的Heartbeat机
    制来实现)
  • 二是Follower必须能够及时将Leader的消息复制过来,不能“落后太多”

9. kafka 如何保证分区平衡

分区的leader replica均衡分布在broker上。此时集群的负载是均衡的。这就叫做分区平衡。

简单总结:kafka会定时触发分区平衡操作,也可以主动触发分区平衡,例如开始replica均匀分配,但是其中一个broker挂了,导致重分配后分区不平衡了,但是优先副本不会改变,分区平衡操作会将replica优化恢复到优先副本上。

在讲分区平衡前,先讲几个概念:

  1. AR: assigned replicas,已分配的副本。每个partition都有自己的AR列表,里面存储着这个partition最初分配的所有replica。注意AR列表不会变化,除非增加分区。
  2. PR(优先replica):AR列表中的第一个replica就是优先replica,而且永远是优先 replica。最初,优先replica和leader replica是同一个replica。
  3. ISR:in sync replicas,同步副本。每个partition都有自己的ISR列表。ISR是会根据同 步情况动态变化的。

最初ISR列表和AR列表是一致的,但由于某个节点死掉,或者某个节点的follower replica 落后leader replica太多,那么该节点就会被从ISR列表中移除。此时,ISR和AR就不再一致。

接下来我们通过一个例子来理解分区平衡。

  1. 根据以上信息,一个拥有3个replica的partition,最初是下图的样子。
    在这里插入图片描述

可以看到AR和ISR保持一致,并且初始时刻,优先副本和leader副本都指向replica 0.

  1. 接下来,replica 0所在的机器下线了,那么情况会变成如下图所示:
    在这里插入图片描述

可以看到replica 0已经从ISR中移除掉了。同时,由于重新选举,leader副本变成了replica 1,而优先副本还是replica 0。优先副本是不会改变的。

由于最初时,leader副本在broker均匀分布,分区是平衡的。但此时,由于此partition的 leader副本换成了另外一个,所以此时分区平衡已经被破坏。

  1. replica 0所在的机器修复了,又重新上线,情况如下图:
    在这里插入图片描述

可以看到replica 0重新回到ISR列表中,不过此时他没能恢复leader的身份。只能作为 follower当一名小弟。

此时分区依旧是不平衡的。那是否意味着分区永远都会不平衡下去呢?不是的。

  1. kafka会定时触发分区平衡操作,也可以主动触发分区平衡。这就是所谓的分区平衡操 作,操作完后如下图。
    在这里插入图片描述

可以看到此时leader副本通过选举,会重新变回来replica 0,因为replica 0是优先副本, 其实优先的含义就是选择leader时被优先选择。这样整个分区又回到了初始状态,而初始时,leader副本是均匀分布的。此时已经分区平衡了。

由此可见,分区平衡操作就是使leader副本和优先副本保持一致的操作。可以把优先副本理 解为分区的平衡状态位,平衡操作就是让leader副本归位。

10. kafka 生产者分区策略有哪些

所谓分区策略是决定生产者将消息发送到哪个分区的算法

  • 轮询策略(默认)
    也称 Round-robin 策略,即顺序分配。比如一个主题下有 3 个分区,那么第一条消息被发送到分区 0,第二条被发送到分区 1,第三条被发送到分区 2
  • 随机策略
    也称 Randomness 策略。所谓随机就是我们随意地将消息放置到任意一个分区上
  • 按消息键保序策略
    Kafka 允许为每条消息定义消息键,简称为 Key。这个 Key 的作用非常大,它可以是一个有着明确业务含义的字符串,比如客户代码、部门编号或是业务 ID 等;也可以用来表征消息元数据。一旦消息被定义了 Key,那么你就可以保证同一个 Key 的所有消息都进入到相同的分区里面,由于每个分区下的消息处理都是有顺序的,故这个策略被称为按消息键保序策略

    前面提到的 Kafka 默认分区策略实际上同时实现了两种策略:如果指定了 Key,那么默认实现按消息键保序策略;如果没有指定 Key,则使用轮询策略。

  • 基于地理位置的分区策略

11. kafka 分区重平衡机制

重平衡,也就是 Rebalance。Rebalance 就是让一个 Consumer Group 下所有的 Consumer 实例就如何消费订阅主题的所有分区达成共识的过程。在 Rebalance 过程中,所有 Consumer 实例共同参与,在协调者组件的帮助下,完成订阅主题分区的分配。但是,在整个过程中,所有实例都不能消费任何消息,因此它对 Consumer 的 TPS 影响很大。

所谓协调者,在 Kafka 中对应的术语是 Coordinator,它专门为 Consumer Group 服务,负责为 Group 执行 Rebalance 以及提供位移管理和组成员管理等。

具体来讲,Consumer 端应用程序在提交位移时,其实是向 Coordinator 所在的 Broker 提交位移。同样地,当 Consumer 应用启动时,也是向 Coordinator 所在的 Broker 发送各种请求,然后由 Coordinator 负责执行消费者组的注册、成员管理记录等元数据管理操作。

所有 Broker 在启动时,都会创建和开启相应的 Coordinator 组件。也就是说,所有 Broker 都有各自的 Coordinator 组件。那么,Consumer Group 如何确定为它服务的 Coordinator 在哪台 Broker 上呢?答案就在我们之前说过的 Kafka 内部位移主题 __consumer_offsets 身上。

目前,Kafka 为某个 Consumer Group 确定 Coordinator 所在的 Broker 的算法有 2 个步骤。

  • 第 1 步:确定由位移主题的哪个分区来保存该 Group 数据:partitionId=Math.abs(groupId.hashCode() % offsetsTopicPartitionCount)。
  • 第 2 步:找出该分区 Leader 副本所在的 Broker,该 Broker 即为对应的 Coordinator。

Rebalance 发生的时机有三个:

  • 组成员数量发生变化
  • 订阅主题数量发生变化
  • 订阅主题的分区数发生变化

重平衡过程是如何通知到其他消费者实例的?答案就是,靠消费者端的心跳线程(Heartbeat Thread)。

Kafka Java 消费者需要定期地发送心跳请求(Heartbeat Request)到 Broker 端的协调者,以表明它还存活着。在 Kafka 0.10.1.0 版本之前,发送心跳请求是在消费者主线程完成的,也就是你写代码调用 KafkaConsumer.poll 方法的那个线程。

这样做有诸多弊病,最大的问题在于,消息处理逻辑也是在这个线程中完成的。因此,一旦消息处理消耗了过长的时间,心跳请求将无法及时发到协调者那里,导致协调者“错误地”认为该消费者已“死”。自 0.10.1.0 版本开始,社区引入了一个单独的心跳线程来专门执行心跳请求发送,避免了这个问题。

但这和重平衡又有什么关系呢?其实,重平衡的通知机制正是通过心跳线程来完成的。当协调者决定开启新一轮重平衡后,它会将“REBALANCE_IN_PROGRESS”封装进心跳请求的响应中,发还给消费者实例。当消费者实例发现心跳响应中包含了“REBALANCE_IN_PROGRESS”,就能立马知道重平衡又开始了,这就是重平衡的通知机制。

12. kafka 位移提交机制

https://herobin.blog.csdn.net/article/details/123860062 第十八部分

13. kafka ISR 机制

ISR(In-sync Replicas) 副本集合。ISR 中的副本都是与 Leader 同步的副本,相反,不在 ISR 中的追随者副本就被认为是与 Leader 不同步的。那么,到底什么副本能够进入到 ISR 中呢?

我们首先要明确的是,Leader 副本天然就在 ISR 中。也就是说,ISR 不只是追随者副本集合,它必然包括 Leader 副本。甚至在某些情况下,ISR 只有 Leader 这一个副本。

另外,能够进入到 ISR 的追随者副本要满足一定的条件。Kafka 判断 Follower 是否与 Leader 同步的标准是 Broker 端参数 replica.lag.time.max.ms 参数值。这个参数的含义是 Follower 副本能够落后 Leader 副本的最长时间间隔,当前默认值是 10 秒。这就是说,只要一个 Follower 副本落后 Leader 副本的时间不连续超过 10 秒,那么 Kafka 就认为该 Follower 副本与 Leader 是同步的,即使此时 Follower 副本中保存的消息明显少于 Leader 副本中的消息。

14. kafka Unclean 领导者选举(Unclean Leader Election)

既然 ISR 是可以动态调整的,那么自然就可以出现这样的情形:ISR 为空。因为 Leader 副本天然就在 ISR 中,如果 ISR 为空了,就说明 Leader 副本也“挂掉”了,Kafka 需要重新选举一个新的 Leader。可是 ISR 是空,此时该怎么选举新 Leader 呢?

Kafka 把所有不在 ISR 中的存活副本都称为非同步副本。通常来说,非同步副本落后 Leader 太多,因此,如果选择这些副本作为新 Leader,就可能出现数据的丢失。毕竟,这些副本中保存的消息远远落后于老 Leader 中的消息。在 Kafka 中,选举这种副本的过程称为 Unclean 领导者选举。Broker 端参数 unclean.leader.election.enable 控制是否允许 Unclean 领导者选举。

开启 Unclean 领导者选举可能会造成数据丢失,但好处是,它使得分区 Leader 副本一直存在,不至于停止对外提供服务,因此提升了高可用性。反之,禁止 Unclean 领导者选举的好处在于维护了数据的一致性,避免了消息丢失,但牺牲了高可用性。

如果你听说过 CAP 理论的话,你一定知道,一个分布式系统通常只能同时满足一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance)中的两个。显然,在这个问题上,Kafka 赋予你选择 C 或 A 的权利。

你可以根据你的实际业务场景决定是否开启 Unclean 领导者选举。不过,我强烈建议你不要开启它,毕竟我们还可以通过其他的方式来提升高可用性。如果为了这点儿高可用性的改善,牺牲了数据一致性,那就非常不值当了。

15. 关于高水位和Leader Epoch的讨论

https://herobin.blog.csdn.net/article/details/123903829 第27部分

CK&ES

1. 介绍下Clickhouse,为什么使用它?

Clickhouse是一款高性能列式存储OLAP数据库。

  • 列式存储:减少磁盘 I/O 操作和 CPU 消耗,提高查询速度,有利于压缩。
    • 对于列的聚合,计数,求和等统计操作原因优于行式存储。
    • 某一列的数据类型都是相同的,更容易进行数据压缩,大大提高了数据的压缩比重。
    • 由于数据压缩比更好,节省了磁盘空间,对于cache也有了更大的发挥空间。
  • 查询速度快(列存+分区+充分利用CPU资源)
    • 顺序写,写入先写入缓存,会有专门的线程定期进行合并。
    • Clickhouse采用类LSM Tree的结构,数据写入后定期在后台Compaction。
    • Clickhouse在数据导入时全部是顺序append写,写入后数据段不可更改,在后台compaction时也是多个段merge sort后顺序写会磁盘。
    • 50MB-200MB/s的写入吞吐能力,按照每行100Byte估算,大约相当于50w-200w/s的写入速度。
  • 极致的并行处理能力,极大的降低了查询延时(充分利用CPU)
    • Clickhouse将数据划分为多个partition,每个partition再进一步划分为多个index granularity(索引粒度),然后通过多个CPU核心分别处理其中的一部分来实现并行数据处理。在这种设计下,单条Query就能利用整机所有CPU。
    • Clickhouse即使对于大量数据的查询也能够化整为零平行处理。但是有一个弊端就是对于单条查询使用多cpu,就不利于同时并发多条查询。所以对于高qps的查询业务,Clickhouse并不是强项。

2. 介绍下CK的表引擎

  • TinyLog:以列文件的形式保存在磁盘上,不支持索引,没有并发控制。一般保存少量数据的小表,用于平时测试用。
  • Memory:内存引擎,数据以未压缩的原始形式直接保存在内存当中,服务器重启数据就会消失。读写操作不会互相阻塞,不支持索引。简单查询下有非常高的性能表现(超过10G/s)。
  • MergeTree:Clickhouse中最强大的表引擎当属MergeTree(合并树)引擎及该系列(*MergeTree)中的其他引擎,支持索引和分区,地位可以相当于innodb之于Mysql。
    • 三个相关核心概念:主键(可选)、分区(可选)、排序(必选)
    • partition by 分区:降低扫描的范围,优化查询速度。任何一个批次的数据写入都会产生一个临时分区,不会纳入任何一个已有的分区。写入后某个时刻(大概10-15分钟后),Clickhouse 会自动执行合并操作
    • primary key主键:数据的一级索引,但是却不是唯一约束。索引类型为粒度索引(稀疏索引),默认粒度8192。用很少的索引数据,定位更多的数据,代价就是只能定位到索引粒度的第一行,然后再进行一点扫描
    • order by: 设定了分区内的数据按照哪些字段顺序进行有序保存。是 MergeTree 中唯一一个必填项,甚至比 primary key 还重要,因为当用户不设置主键的情况下,很多处理会依照 order by 的字段进行处理。要求:主键必须是 order by 字段的前缀字段。
    • 数据TTL:Time To Live,Merge Tree 提供了可以管理数据表或者列的生命周期的功能。
      • 数据会在 create_time 之后 10 秒丢失:alter table t_order_mt3 MODIFY TTL create_time + INTERVAL 10 SECOND;
      • 涉及判断的字段必须是Date或者Datetime类型
      • 过期删除多种策略(默认DELETE,真实删除,还有移动到指定路径)
  • ReplaceMergeTree:特性完全继承MergeTree,多了去重功能。MergeTree 可以设置主键,但是 primary key 其实没有唯一约束的功能。如果你想处理掉重复的数据,可以借助这个 ReplacingMergeTree。
    • 实际上是使用 order by 字段作为唯一键
    • 去重不能跨分区
    • 只有同一批插入(新版本)或合并分区时才会进行去重
    • 认定重复的数据保留版本字段值最大的
    • 如果版本字段相同则按插入顺序保留最后一笔
  • SummingMergeTree:“预聚合”的引擎 SummingMergeTree
    • 以 SummingMergeTree()中指定的列作为汇总数据列
    • 可以填写多列必须数字列,如果不填,以所有非维度列且为数字列的字段为汇总数据列
    • 以 order by 的列为准,作为维度列
    • 其他的列按插入顺序保留第一行
    • 不在一个分区的数据不会被聚合
    • 只有在同一批次插入(新版本)或分片合并时才会进行聚合
    • 关联问题:用flink写clickhouse,如果作业发生failover重复写入了怎么办,这种时候就可以用ReplacingMergeTree引擎来做到去重。

大数据其他

1. 数据湖了解吗?

2. Canal原理

很简单,就是把自己伪装成Slave,假装从Master复制数据。

在这里插入图片描述
MySQL主从复制过程

  1. Master主库将改变记录,写到二进制日志(Binary Log)中;
  2. Slave从库向MySQL Master发送dump协议,将Master主库的binary log events拷贝到它的中继日志(relay log);
  3. Slave从库读取并重做中继日志中的事件,将改变的数据同步到自己的数据库。

方案设计

1. 海量实时数据去重,如何设计方案?

  • 布隆过滤器
  • RocksDB 状态后端去重 keyby 后 维护一个 ValueState isExists
  • 外部存储(redis、Hbase)

https://www.jianshu.com/p/f6042288a6e3

也可以看看这里的第二十一讲
https://herobin.blog.csdn.net/article/details/108576816

2. 电商场景,要统计每个用户过去一年的订单数,每5分钟输出一次,方案如何设计?

  • 使用状态后端存储,根据 uid 进行 keyby 分组,用 mapstate 进行存储,mapstate 的 key 为 时间格式,value 为 这5分钟的订单总和,通过 ttl 设置状态过期,计算的时候,根据某个 uid 取到对应的 mapstate,遍历keyset 取出对应的 value 求和,就是用户一年内的订单总和了。

特别注意,状态TTL仅对时间特征为处理时间时生效,对事件时间是无效的。

3. mapreduce 词频统计流程

4. 项目中实时数仓的搭建方案,元数据存储,架构等设计问题

5. 设计每隔10min输出最近1h内top100博文,mid会倾斜,方案设计与优劣比较

6. 样本拼接方案设计,优化思路,join成功之后,为什么不直接下发?

7. topN 热门商品实现

第二十八讲:
https://herobin.blog.csdn.net/article/details/108576816

8. 1小时的滚动窗口,一小时处理一次的压力比较大,想让他5分钟处理一次.怎么办?

自定义触发器,4个方法,一个Close三个用于控制计算和输出

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值