Apache Flink 可以使用多用方式在不同的环境中部署,相对于部署环境的多样性而言,Flink集群的基本构建方式仍是相同的,所应用的操作原则也是相似的。
在本教程中,你将会学习如何管理和运行Flink任务,了解如何部署和监控应用程序,Flink如何从失败的任务中恢复,并且进行一些日常操作任务的演练,比如升级和扩容
场景说明
本场景需要的操作环境为:一个长期运行的Flink Session Cluster 和一个kafka集群。
Flink集群由一个JobManager
以及一个或多个TaskManager
组成。JobManager
负责Job提交、监控和资源的管理。TaskManagers
中运行Worker
进程,它负责执行那些构成Flink Job 的实际任务的执行。在本教程中,将会从启动一个单一的TaskManager
开始,但是之后会扩展到运行多个TaskManager
。在这里我们使用一个专用的Client Container
用于提交Flink Job,并在之后执行各种操作。Flink集群本身并不需要Client Container
,只是为了使用方便,才引入了它。
kafka集群由一个 Zookeeper Server 和 一个 Kafka Broker组成。
开始的时候,向JobManager
提交一个名为 Flink Event Count
的 Flink Job 。此外,还创建了两个 kafka Topic input
和output
。
该Job从 kafka 主题 input
中消费点击事件clickEvents
, 每个点击事件都包含一个时间戳 timestamp
和一个page
属性。这些事件以page
为key进行分组,并且基于 15s的时间窗口进行计数。结果数据被写入到kafka主题output
。
There are six different pages and we generate 1000 click events per page and 15 seconds. Hence, the output of the Flink job should show 1000 views per page and window.(这段后半句不知如何翻译更加妥当姑且在下面给出自己的理解)
总共有6种不同的 page
,每个page
每 15s 会产生 1000个点击事件。因此,Flink Job的输出对于每个 page
的每个时间窗口 应该显示 1000个视图。
环境搭建
操作的环境只需要简单的几个步骤就可以搭建起来。我们会引导你完成必要的操作命令,并且展示如何验证所有正在运行的 是正确的。
我们假定你已经在机器上部署了 Docker
(1.12+) 和 dokcer-compose
(2.1+)。
所需要的配置文件在flink-playgrounds
仓库中,拉取到本地并启动Docker
环境:
git clone --branch release-1.12 https://github.com/apache/flink-playgrounds.git
cd flink-playgrounds/operations-playground
docker-compose build
docker-compose up -d
之后,你可以运行下面的命令来查看正在运行的 Docker
容器:
docker-compose ps
Name Command State Ports
-----------------------------------------------------------------------------------------------------------------------------
operations-playground_clickevent-generator_1 /docker-entrypoint.sh java ... Up 6123/tcp, 8081/tcp
operations-playground_client_1 /docker-entrypoint.sh flin ... Exit 0
operations-playground_jobmanager_1 /docker-entrypoint.sh jobm ... Up 6123/tcp, 0.0.0.0:8081->8081/tcp
operations-playground_kafka_1 start-kafka.sh Up 0.0.0.0:9094->9094/tcp
operations-playground_taskmanager_1 /docker-entrypoint.sh task ... Up 6123/tcp, 8081/tcp
operations-playground_zookeeper_1 /bin/sh -c /usr/sbin/sshd ... Up 2181/tcp, 22/tcp, 2888/tcp, 3888/tcp
上面的信息显示Client container
已经成功提交 Flink job 并退出,和数据生成器一样集群所有的组件都处于正在运行的状态。
你可以通过下面的命令 停止 Docker正在运行的环境:
docker-compose down -v
环境讲解
在搭建好的环境中你可以尝试和验证很多事情。在下面两部分内容中,我们将向你展示如何与Flink集群进行交互,并演示Flink的一些关键特性。
Flink WebUI
观察Flink集群自然是以 Flink WebUI作为出发点,默认链接是 [http://localhost:8081](http://localhost:8081)
。如果一切运行正常,你可以看到Flink WebUI 的首页包含一个TaskManager
和正在运行的Job Click Event Count
。
Flink WebUI 包含了 关于Flink集群及集群上的Job 的 大量有用且有趣的信息,例如
JobGraph``Metrics``Checkpointing Statistics``TaskManager Status
等
日志
JobManager
JobManager
日志可以通过 docker -compose 命令进行查看
docker-compose logs -f jobmanager
在JobManager初始化启动完成之后,你应该主要查看每一个检查点完成的日志信息。
TaskManager
TaskManager
日志的查看方式和JobManager
相同。
docker-compose logs -f taskmanager
Flink CLI
Flink CLI 相关命令可以在 Client Container
容器内使用。例如,你可以使用下面的命令打印出关于 Flink CLI的帮助信息
docker-compose run --no-deps client flink --help
Flink REST API
Flink REST API 可以通过本机的 localhost:8081 进行访问,也可以在Client Container
中通过 jobmanager:8081进行访问。例如,使用下面的命令获取所有正在运行的Job:
curl localhost:8081/jobs
Kafka Topics
你可以运行以下命令查看写入kafka 的数据记录:
//input topic (1000 records/s)
docker-compose exec kafka kafka-console-consumer.sh \
--bootstrap-server localhost:9092 --topic input
//output topic (24 records/min)
docker-compose exec kafka kafka-console-consumer.sh \
--bootstrap-server localhost:9092 --topic output
核心特性探索
现在,你已经学会了如何与Flink以及Docker容器进行交互,让我们来看一看常见的操作命令。所有的这些命令都是相互独立的,也就是说你可以按照任何顺序去执行它们。大多数的命令都可以通过 CLI 和 REST API 去执行。
列出正在运行的Job
- CLI
命令
docker-compose run --no-deps client flink list
预期输出
Waiting for response...
------------------ Running/Restarting Jobs -------------------
16.07.2019 16:37:55 : : Click Event Count (RUNNING)
--------------------------------------------------------------
No scheduled jobs.
- REST API
请求
curl localhost:8081/jobs
预期响应结果
{
"jobs": [
{
"id": "",
"status": "RUNNING"
}
]
}
作业在提交时会被分配一个 JobID
,通过CLI或者REST API 进行操作时需要携带它。
Job 失败与恢复
在Job(部分)失败的情况下,Flink对事件处理依然能够提供精确一次的保障。在本次练习中你可以观察到并在一定程度上榜验证这种行为。
步骤1:观察输出
如上所述,在本次练习中事件正好按照每个窗口包含1000条记录的规则生成。因此,你可以跟踪输出的主题,并检查恢复后 所有的窗口都存在并且计数是正确的,以证明Flink成功地从Taskmanager
故障中恢复并且没有造成数据的丢失或重复。
为此,需要从开始读取output
主题的数据直到故障恢复之后:
docker-compose exec kafka kafka-console-consumer.sh \
--bootstrap-server localhost:9092 --topic output
步骤2:模拟失败
可以通过杀死TaskManager
进程来模拟局部故障,这与在生产环境中,TaskManager
进程的丢失、运行Taskmanager
的机器故障,或者仅仅是用户代码或框架本身抛出的暂时性异常等相对应。
docker-compose kill taskmanager
几秒钟之后,JobManager
将会注意到TaskManager
进程的丢失,然后取消受影响的Job,立马重新提交以恢复该Job。当Job重启后,它包含的任务仍然处于 SCHEDULED
状态。如下图中紫色方框所示:
注意:虽然Job的所有任务都处于SCHEDULED
状态,但整个Job的状态却显示为 RUNNING
。
此时,J由于没有可使用的资源(TaskManager
提供的 TaskSlots
),Job中任务的状态不会从 SCHEDULED
切换到 RUNNING
。在直到有新的TaskManager可用之前,Job将会经历一个不断取消和重新提交的循环。
与此同时,数据生成器会保持向 input
主题写入 ClickEvents
数据。在哪生产环境中也经常出现Job挂掉但源头还在不断产生数据的情况。
步骤3: 失败恢复
一旦TaskManager
重启成功,它将会自动重新连接 JobManager
。
docker-compose up -d taskmanager
当 TaskManager注册成功之后,JobManager 就会将处于 SCHEDULED 状态的所有任务调度到该 TaskManager
的可用TaskSlots中运行,此时所有的任务将会从失败前最近一次成功的 checkpoint
进行恢复,一旦恢复成功,它们的状态将转变为 RUNNING
。
接下来该Job将快速处理 kafka input
事件的全部积压(在Job中断1期间累积的数据),并且以更快的速度(> 24条记录/分钟)产生输出,直到它追上 kafka 的 lag 延迟为止。此时观察 output
主题的输出,对于所有 key (page) 的 每一个时间窗口的计数刚好是1000.由于我们使用的 是 FlinkKafkaProducer
的至少一次的 模式,所以有一定的几率看到一些重复的 结果数据出现。
注意:在大部分生产环境中都需要一个资源管理器 (Kubernetes、Yarn,、Mesos)对 失败的 Job 进行自动重启
Job升级与扩容
升级Flink Job 一般需要两步:
第一,使用 保存点 savepoint
优雅地停止 Flink Job。Savepoint
是在定义明确、全局一致的的时间点生成的 应用程序的完整一致性快照。
第二,从 Savepoint 启动升级后的 Flink Job。 在此,“升级”包含如下几种含义:
- 配置升级(比如 Job 并行度修改)
- Job 拓扑升级(比如添加或者删除算子)
- Job 的用户自定义函数升级
在开始升级之前,你可能需要实时查看_Output_topic 输出, 以便观察在升级过程中没有数据丢失或损坏。
docker-compose exec kafka kafka-console-consumer.sh \
--bootstrap-server localhost:9092 --topic output
步骤1:停止Job
要优雅停止 Job,需要使用 JobID 通过 CLI 或 REST API 调用 stop
命令。 你可以通过 l列出所有正在运行的Job的相关命令或 Flink WebUI 界面获取JobID,然后可以使用JobID来停止对应的Job进程:
- CLI
命令
docker-compose run --no-deps client flink stop
**预期输出**
Suspending job "" with a savepoint.
Savepoint completed. Path: file:
- RESTAPI
请求
# triggering stop
curl -X POST localhost:8081/jobs//stop -d '{"drain": false}'
**预期响应结果**
{
"request-id": ""
}
请求
# check status of stop action and retrieve savepoint path
curl localhost:8081/jobs//savepoints/
预期响应结果
Savepoint
已保存在state.savepoint.dir
指定的路径中,该配置在flink-conf.yaml
中定义,flink-conf.yaml
在本机的/tmp/flink-savepoints-directory/
目录下。 在下一步操作中我们会用到这个 Savepoint
路径,如果我们是通过 REST API 操作的, 那么 Savepoint
路径会随着响应结果一起返回,我们可以直接查看文件系统来确认 Savepoint 保存情况。
步骤2a: 重启Job(不做任何变更)
现在你可以从Savepoint
重启升级后的Job。这里为了简单起见在重启之前并没有对程序做任何修改。
- CLI
命令
docker-compose run --no-deps client flink run -s \
-d /opt/ClickCountJob.jar \
--bootstrap.servers kafka:9092 --checkpointing --event-time
预期输出
Job has been submitted with JobID
- REST API
请求
# Uploading the JAR from the Client container
docker-compose run --no-deps client curl -X POST -H "Expect:" \
-F "jarfile=@/opt/ClickCountJob.jar" http://jobmanager:8081/jars/upload
预期响应结果
{
"filename": "/tmp/flink-web-/flink-web-upload/",
"status": "success"
}
请求
# Submitting the Job
curl -X POST http://localhost:8081/jars//run \
-d '{"programArgs": "--bootstrap.servers kafka:9092 --checkpointing --event-time", "savepointPath": ""}'
预期响应结果
{
"jobid": ""
}
一旦该 Job 再次处于RUNNING
状态,你将从output
主题中看到数据在以更高的速率产生, 这是由于刚启动的 Job 正在处理停止期间积压的大量数据。另外,你还会看到在升级期间 没有产生任何数据丢失:所有窗口的计数都刚好是1000。
步骤2b: 重启Job(修改并行度)
在从 Savepoint 重启 Job 之前,你还可以通过修改并行度来达到扩容 Job 的目的。
- CLI
命令
docker-compose run --no-deps client flink run -p 3 -s \
-d /opt/ClickCountJob.jar \
--bootstrap.servers kafka:9092 --checkpointing --event-time
预期输出
Starting execution of program
Job has been submitted with JobID
- REST API
请求
# Uploading the JAR from the Client container
docker-compose run --no-deps client curl -X POST -H "Expect:" \
-F "jarfile=@/opt/ClickCountJob.jar" http://jobmanager:8081/jars/upload
预期响应结果
{
"filename": "/tmp/flink-web-/flink-web-upload/",
"status": "success"
}
请求
# Submitting the Job
curl -X POST http://localhost:8081/jars//run \
-d '{"parallelism": 3, "programArgs": "--bootstrap.servers kafka:9092 --checkpointing --event-time", "savepointPath": ""}'
预期响应结果
{
"jobid": ""
}
现在 Job 已重新提交,但由于我们提高了并行度所以导致 TaskSlots 不够用(1 个 TaskSlot 可用,总共需要 3 个),最终 Job 会重启失败。通过如下命令:
docker-compose scale taskmanager=2
你可以向 Flink 集群添加第二个 TaskManager(为 Flink 集群提供 2 个 TaskSlots 资源), 它会自动向 JobManager 注册,TaskManager 注册完成后,Job 会再次处于 “RUNNING” 状态。
一旦 Job 再次运行起来,从output
主题 的输出中你会看到在扩容期间数据依然没有丢失: 所有窗口的计数都正好是 1000。
查询Job指标
可以通过 JobManager 提供的 REST API 来获取系统和用户指标
具体请求方式取决于我们想查询哪类指标,Job 相关的指标分类可通过jobs/
请求
curl "localhost:8081/jobs//metrics?get=lastCheckpointSize"
预期响应结果
[
{
"id": "lastCheckpointSize",
"value": "9378"
}
]
REST API 不仅可以用于查询指标,还可以用于获取正在运行中的 Job 详细信息
请求
# 可以从结果中获取感兴趣的 vertex-id
curl localhost:8081/jobs/
预期响应结果
{
"jid": "",
"name": "Click Event Count",
"isStoppable": false,
"state": "RUNNING",
"start-time": 1564467066026,
"end-time": -1,
"duration": 374793,
"now": 1564467440819,
"timestamps": {
"CREATED": 1564467066026,
"FINISHED": 0,
"SUSPENDED": 0,
"FAILING": 0,
"CANCELLING": 0,
"CANCELED": 0,
"RECONCILING": 0,
"RUNNING": 1564467066126,
"FAILED": 0,
"RESTARTING": 0
},
"vertices": [
{
"id": "",
"name": "ClickEvent Source",
"parallelism": 2,
"status": "RUNNING",
"start-time": 1564467066423,
"end-time": -1,
"duration": 374396,
"tasks": {
"CREATED": 0,
"FINISHED": 0,
"DEPLOYING": 0,
"RUNNING": 2,
"CANCELING": 0,
"FAILED": 0,
"CANCELED": 0,
"RECONCILING": 0,
"SCHEDULED": 0
},
"metrics": {
"read-bytes": 0,
"read-bytes-complete": true,
"write-bytes": 5033461,
"write-bytes-complete": true,
"read-records": 0,
"read-records-complete": true,
"write-records": 166351,
"write-records-complete": true
}
},
{
"id": "",
"name": "Timestamps/Watermarks",
"parallelism": 2,
"status": "RUNNING",
"start-time": 1564467066441,
"end-time": -1,
"duration": 374378,
"tasks": {
"CREATED": 0,
"FINISHED": 0,
"DEPLOYING": 0,
"RUNNING": 2,
"CANCELING": 0,
"FAILED": 0,
"CANCELED": 0,
"RECONCILING": 0,
"SCHEDULED": 0
},
"metrics": {
"read-bytes": 5066280,
"read-bytes-complete": true,
"write-bytes": 5033496,
"write-bytes-complete": true,
"read-records": 166349,
"read-records-complete": true,
"write-records": 166349,
"write-records-complete": true
}
},
{
"id": "",
"name": "ClickEvent Counter",
"parallelism": 2,
"status": "RUNNING",
"start-time": 1564467066469,
"end-time": -1,
"duration": 374350,
"tasks": {
"CREATED": 0,
"FINISHED": 0,
"DEPLOYING": 0,
"RUNNING": 2,
"CANCELING": 0,
"FAILED": 0,
"CANCELED": 0,
"RECONCILING": 0,
"SCHEDULED": 0
},
"metrics": {
"read-bytes": 5085332,
"read-bytes-complete": true,
"write-bytes": 316,
"write-bytes-complete": true,
"read-records": 166305,
"read-records-complete": true,
"write-records": 6,
"write-records-complete": true
}
},
{
"id": "",
"name": "ClickEventStatistics Sink",
"parallelism": 2,
"status": "RUNNING",
"start-time": 1564467066476,
"end-time": -1,
"duration": 374343,
"tasks": {
"CREATED": 0,
"FINISHED": 0,
"DEPLOYING": 0,
"RUNNING": 2,
"CANCELING": 0,
"FAILED": 0,
"CANCELED": 0,
"RECONCILING": 0,
"SCHEDULED": 0
},
"metrics": {
"read-bytes": 20668,
"read-bytes-complete": true,
"write-bytes": 0,
"write-bytes-complete": true,
"read-records": 6,
"read-records-complete": true,
"write-records": 0,
"write-records-complete": true
}
}
],
"status-counts": {
"CREATED": 0,
"FINISHED": 0,
"DEPLOYING": 0,
"RUNNING": 4,
"CANCELING": 0,
"FAILED": 0,
"CANCELED": 0,
"RECONCILING": 0,
"SCHEDULED": 0
},
"plan": {
"jid": "",
"name": "Click Event Count",
"nodes": [
{
"id": "",
"parallelism": 2,
"operator": "",
"operator_strategy": "",
"description": "ClickEventStatistics Sink",
"inputs": [
{
"num": 0,
"id": "",
"ship_strategy": "FORWARD",
"exchange": "pipelined_bounded"
}
],
"optimizer_properties": {}
},
{
"id": "",
"parallelism": 2,
"operator": "",
"operator_strategy": "",
"description": "ClickEvent Counter",
"inputs": [
{
"num": 0,
"id": "",
"ship_strategy": "HASH",
"exchange": "pipelined_bounded"
}
],
"optimizer_properties": {}
},
{
"id": "",
"parallelism": 2,
"operator": "",
"operator_strategy": "",
"description": "Timestamps/Watermarks",
"inputs": [
{
"num": 0,
"id": "",
"ship_strategy": "FORWARD",
"exchange": "pipelined_bounded"
}
],
"optimizer_properties": {}
},
{
"id": "",
"parallelism": 2,
"operator": "",
"operator_strategy": "",
"description": "ClickEvent Source",
"optimizer_properties": {}
}
]
}
}
请查阅REST API 参考,该参考上有完整的指标查询接口信息,包括如何查询不同种类的指标(例如 TaskManager 指标)。
延伸扩展
你可能已经注意到了`Click Event Count`这个 Job 在启动时总是会带上`--checkpointing`和 `--event-time`两个参数。如果我们去除这两个参数,那么 Job 的行为也会随之改变。
--checkpointing
参数开启了checkpoint
配置,checkpoint
是 Flink 容错机制的重要保证。 如果你没有开启checkpoint
,那么当Job失败并重启后,你将会看到数据丢失现象发生。-
--event-time
参数开启了 Job 的事件时间机制,该机制会使用ClickEvent
自带的时间戳进行统计。 如果不指定该参数,Flink 将结合当前机器时间使用事件处理时间进行统计。如此一来,每个窗口计数将不再是准确的 1000 了。Click Event Count
这个 Job 还有另外一个选项,该选项默认是关闭的,你可以在client container
的docker-compose.yaml
文件中添加该选项从而观察该 Job 在反压下的表现,该选项描述如下: --backpressure
将一个额外算子添加到 Job 中,该算子会在偶数分钟内产生严重的反压(比如:10:12 期间,而 10:13 期间不会)。这种现象可以通过多种网络指标观察到,比如:outputQueueLength
和outPoolUsage
指标,通过 WebUI 上的反压监控也可以观察到。