流计算与消息(一):通过Flink理解流计算的原理

本文详细讲解了如何使用Flink进行实时数据流统计,如每分钟按IP计数Web请求,展示了如何定义Job、数据转换和实时汇总。通过代码实例,探讨了流计算在实时数据分析中的应用及其执行原理。

 在生产中,消息队列和流计算往往是相互配合,一起来使用的。而流计算也是后端程序员技术栈中非常重要的一项技术。

01 哪些问题适合用流计算解决?

哪些问题适合用流计算来解决?或者说,流计算它的应用场景是什么样的呢?

答:对实时产生的数据进行实时统计分析,这类场景都适合使用流计算来实现。 

你在理解这句话的时候,需要特别注意的是,这里面有两个“实时”,一个是说,数据是“实时”产生的,另一个是说,统计分析这个过程是“实时”进行的,统计结果也是第一时间就计算出来了。

举几个例子:

每分钟按照 IP 统计 Web 请求次数;

电商在大促时,实时统计当前下单量;

实时统计 App 中的埋点数据,分析营销推广效果。

02 用代码定义 Job 并在 Flink 中执行

我们用 Flink 来实现一个实时统计任务:接收 NGINX 的 access.log,每 5 秒钟按照 IP 地址统计 Web 请求的次数。这个统计任务它一个非常典型的,按照 Key 来进行分类汇总的统计任务,并且汇总是按照一定周期来实时进行的,我们日常工作中遇到的很多统计分析类的需求,都可以套用这个例子的模式来实现,所以我们就以它为例来做一个实现。

$nc localhost 9999
14:37:11 192.168.1.3
14:37:11 192.168.1.2
14:37:12 192.168.1.4
14:37:14 192.168.1.2
14:37:14 192.168.1.4
14:37:14 192.168.1.3
...

一起来跟我看一下定义这个流计算任务的代码:



object SocketWindowIpCount {


  def main(args: Array[String]) : Unit = {
    // 获取运行时环境
    val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
    // 按照EventTime来统计
    env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
    // 设置并行度
    env.setParallelism(4)


    // 定义输入:从Socket端口中获取数据输入
    val hostname: String = "localhost"
    val port: Int = 9999
    // Task 1
    val input: DataStream[String] = env.socketTextStream(hostname, port, '\n')


    // 数据转换:将非结构化的以空格分隔的文本转成结构化数据IpAndCount
    // Task 2
    input
      .map { line => line.split("\\s") }
      .map { wordArray => IpAndCount(new SimpleDateFormat("HH:mm:ss").parse(wordArray(0)), wordArray(1), 1) }


    // 计算:每5秒钟按照ip对count求和
      .assignAscendingTimestamps(_.date.getTime) // 告诉Flink时间从哪个字段中获取
      .keyBy("ip") // 按照ip地址统计
      // Task 3
      .window(TumblingEventTimeWindows.of(Time.seconds(5))) // 每5秒钟统计一次
      .sum("count") // 对count字段求和


    // 输出:转换格式,打印到控制台上
      .map { aggData => new SimpleDateFormat("HH:mm:ss").format(aggData.date) + " " + aggData.ip + " " + aggData.count }
      .print()
    env.execute("Socket Window IpCount")
  }


  /** 中间数据结构 */
  case class IpAndCount(date: Date, ip: String, count: Long)
}

解读一下这段代码:

首先需要获取流计算的运行时环境,也就是这个 env 对象,对 env 做一些初始化的设置。然后,我们再定义输入的数据源,这里面就是我刚刚讲的,运行在 9999 端口上的日志服务。

在代码中,env.socketTextStream(hostname, port, ‘\n’) 这个语句中的三个参数分别是主机名、端口号和分隔符,返回值的数据类型是 DataStream[String],代表一个数据流,其中的每条数据都是 String 类型的。它告诉 Flink,我们的数据源是一个 Socket 服务。这样,Flink 在执行这个计算任务的时候,就会去连接日志服务来接收数据。

定义完数据源之后,需要做一些数据转换,把字符串转成结构化的数据 IpAndCount,便于后续做计算。在定义计算的部分,依次告诉 Flink:时间从 date 字段中获取,按照 IP 地址进行汇总,每 5 秒钟汇总一次,汇总方式就是对 count 字段求和。

之后定义计算结果如何输出,在这个例子中,我们直接把结果打印到控制台上就好了。

总结下来,无论是使用 Flink、Spark 还是其他的流计算框架,定义一个流计算的任务基本上都可以分为:定义输入、定义计算逻辑和定义输出三部分,通俗地说,也就是:数据从哪儿来,怎么计算,结果写到哪儿去,这三件事儿。

输出:

1> 18:40:10 192.168.1.2 23
4> 18:40:10 192.168.1.4 16
4> 18:40:15 192.168.1.4 27
3> 18:40:15 192.168.1.3 23
1> 18:40:15 192.168.1.2 25
4> 18:40:15 192.168.1.1 21
1> 18:40:20 192.168.1.2 21
3> 18:40:20 192.168.1.3 31
4> 18:40:20 192.168.1.1 25
4> 18:40:20 192.168.1.4 26

对于流计算的初学者,特别不好理解的一点是,我们上面编写的这段代码,它只是“用来定义计算任务的代码”,而不是“真正处理数据的代码”。对于普通的应用程序,源代码编译之后,计算机就直接执行了,这个比较好理解。而在 Flink 中,当这个计算任务在 Flink 集群的计算节点中运行的时候,真正处理数据的代码并不是我们上面写的那段代码,而是 Flink 在解析了计算任务之后,动态生成的代码。

03 Job 是如何在 Flink 集群中执行的?

这张图稍微有点儿复杂,我们先忽略细节看整体。Flink 的集群和其他分布式系统都是类似的,集群的大部分节点都是 TaskManager 节点,每个节点就是一个 Java 进程,负责执行计算任务。另外一种节点是 JobManager 节点,它负责管理和协调所有的计算节点和计算任务,同时,客户端和 Web 控制台也是通过 JobManager 来提交和管理每个计算任务的。

我们编写好计算任务的代码后,打包成 JAR 文件,然后通过 Flink 的客户端提交到 JobManager 上。计算任务被 Flink 解析后,会生成一个 Dataflow Graph,也叫 JobGraph,简称 DAG,这是一个有向无环图(DAG),比如我们的这个例子,它生成的 DAG 是这样的:

图中的每个节点是一个 Task,每个 Task 就是一个执行单元,运行在某一个 TaskManager 的进程内。你可以想象一下,就像电流流过电路图一样,数据从 Source Task 流入,进入这个 DAG,每流过一个 Task,就被这个 Task 做一些计算和变换,然后数据继续流入下一个 Task,直到最后一个 Sink Task 流出 DAG,就自然完成了计算。

对于图中的 3 个 Task,每个 Task 对应执行了什么计算,完全可以和我们上面定义计算任务的源代码对应上,我也在源代码的注释中,用"//Task n"的形式给出了标注。第一个 Task 执行的计算很简单,就是连接日志服务接收日志数据,然后将日志数据发往下一个 Task。第二个 Task 执行了两个 map 变换,把文本数据转换成了结构化的数据,并添加 Watermark(水印)。Watermark 这个概念可以先不用管,主要是用于触发按时间汇总的操作。第三个 Task 执行了剩余的计算任务,按时间汇总日志,并输出打印到控制台上。

这个 DAG 仍然是一个逻辑图,它到底是怎么在 Flink 集群中执行的呢?你注意到图中每个 Task 都标注了一个 Parallelism(并行度)的数字吗?这个并行度的意思就是,这个 Task 可以被多少个线程并行执行。比如图中的第二个任务,它的并行度是 4,就代表 Task 在 Flink 集群中运行的时候,会有 4 个线程都在执行这个 Task,每个线程就是一个 SubTask(子任务)。注意,如果 Flink 集群的节点数够多,这 4 个 SubTask 可能会运行在不同的 TaskManager 节点上。

建立了 SubTask 的概念之后,我们再重新回过头来看一下这个图中的两个箭头。第一个箭头连接前两个 Task,这个箭头标注了 REBALANCE(重新分配),因为第一个 Task 并行度是 1,而第二个 Task 并行度是 4,意味着从第一个 Task 流出的数据将被重新分配给第二个 Task 的 4 个线程,也就是 4 个 SubTask(子任务)中,这样就实现了并行处理。这和消息队列中每个主题分成多个分区进行并行收发的设计思想是一样的。

再来看连接第二、第三这两个 Task 的箭头,这个箭头上标注的是 HASH,为什么呢?可以看到,第二个 Task 中最后一步业务逻辑是:keyBy(“ip”),也就是按照 IP 这个字段做一个 HASH 分流。你可以想一下,第三个 Task,它的并行度是 4,也就是有 4 个线程在并行执行汇总。如果要统计每个 IP 的日志条数,那必须得把相同 IP 的数据发送到同一个 SubTask(子任务)中去,这样在每个 SubTask(子任务)中,对于每一条数据,只要在对应 IP 汇总记录上进行累加就可以了。

反之,要是相同 IP 的数据被分到多个 SubTask(子任务)上,这些 SubTask 又可能分布在多个物理节点上,那就没办法统计了。所以,第二个 Task 会把数据按照 IP 地址做一个 HASH 分流,保证 IP 相同的数据都发送到第三个 Task 中相同的 SubTask(子任务)中。这个 HASH 分流的设计是不是感觉很眼熟?我们之前课程中讲到的,严格顺序消息的实现方法:通过 HASH 算法,让 key 相同的数据总是发送到相同的分区上来保证严格顺序,和 Flink 这里的设计就是一样的。

最后在第三个 Task 中,4 个 SubTask 并行进行数据汇总,每个 SubTask 负责汇总一部分 IP 地址的数据。最终打印到控制台上的时候,也是 4 个线程并行打印。你可以回过头去看一下输出的计算结果,每一行数据前面的数字,就是第三个 Task 中 SubTask 的编号。

希望大家可以关注下公众号,会定期分享自己从业经历、技术积累及踩坑经验,支持一下,鞠躬感谢~

关注公众号回复:“资料全集”

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值