Flink简介
是什么?
Apache Flink 是一个框架和分布式处理引擎,用于对无界和有界数
据流进行状态计算。
为什么要用?
- 低延迟(来一个就处理一个)
- 高吞吐(能抵挡大量的数据,需要分布式)
- 结果的准确性和良好的容错性(分布式的话传输过程和数据处理的过程可能会出现乱序;一个挂了之后,回滚到非常近的状态,再跟着处理)
哪些要用流数据?
- 数据报表
- 广告投放
架构演变
事务处理
用户请求=》后台业务=》查询数据库
优点:实时性好
不足:数据量大后高并发难以支撑
分析处理
先把数据整理放到一个数仓,再进行分析和查询
优点:能支持高并发
不足:实时性不好
有状态的流处理
把数据不放在关系数据库,而放在本地内存,设置一个本地状态,相当于用本地内存中的状态代替了关系数据库中的表。这样的好处就是速度会快很多,在高并发时用集群做扩展。
改进
把数据从查数据库改为直接查内存,放到内存宕机了数据会丢失,所以需要定期去保存数据,(Periodic Checkpoint)出错了可以恢复。这样做好处就是速度快,但是如果在分布式下,数据的处理顺序会出现乱序。
lambda架构
- 保持低延迟和结果准确
- 用了两套系统:Batch Layer进行批处理,保证了数据的准确性。Speed Layer进行快速处理,保证了低延迟。
- 不足就是用了两套系统,复杂度高,占用系统资源
Flink出现
- 低延迟
- 高吞吐
- 数据准确不乱序
Flink特点
- 支持事件时间和处理时间语义
- 精确一次的状态保证
- 低延迟,每秒处理百万个事件,毫秒级延迟
- 与众多常用存储系统的连接
- 高可用,动态扩展,实现7*24小时全天运行
事件驱动
和传统的架构类似,数据到到flink,会在内存中进行处理,同时会定期存盘。当数据处理完后会写入事件日志中。
无界流和有界流
无界流:没有边界,代表数据不断地在产生,是实时的数据。
有界流:有边界,比如离线的数据,不会再产生了。
分层API
- 表级别的API(简单的操作)
- 数据流级别的API(实时计算时用DataStream,离线用DataSet)
- 事件状态级别的API
Flink和Spark Streaming的区别
底层架构不一样,Flink是以流的方式,Spark Streaming把数据分的很细,但底层还是以批处理的方式。
部署
配置
# jobManager 的IP地址
jobmanager.rpc.address: localhost
# JobManager 的端口号
jobmanager.rpc.port: 6123
# JobManager JVM heap 内存大小
jobmanager.heap.size: 1024m
# TaskManager JVM heap 内存大小
taskmanager.heap.size: 1024m
# 每个 TaskManager 提供的任务 slots 数量大小
taskmanager.numberOfTaskSlots: 1
# 程序默认并行计算的个数
parallelism.default: 1
# 文件系统来源
# fs.default-scheme
Standalone模式
- 启动:./start-cluster.sh
- 并行度优先级:代码层面>界面配置>默认配置文件配置
用命令方式执行:
…/flink run -c kmoon.zhu.StreamWordCount –p 2
FlinkTutorial-1.0-SNAPSHOT-jar-with-dependencies.jar --host lcoalhost –port 7777
Yarn模式
要求Flink具有Hadoop支持的版本,Hadoop环境要在2.2以上,并且集群中安装有HDFS服务。
Session-cluster模式
- 需要先启动集群,然后再提交作业。
- 向Yarn请求资源空间后,资源不变。
- 资源满了,其他作业需要等待
- 适合小规模执行时间短的作业
- 所有作业共享Dispatch和ResourceManager
Per-Job-Cluster模式
-
每个Job对应每个集群
-
每提交一个作业都会单独向yarn申请资源
-
作业之间相互不影响
-
适合大规模长时间运行的作业
-
所有作业独享Dispatcher和ResourceManager
Flink运行时的组件
作业管理器(JobManager)
- 让TaskManager干活
- 向管理器(ResourceManage)申请资源
- 协调检查点(checkpoints)进行数据存盘
任务管理器(TaskManager)
- 负责为JobManager干活
- 一个JobManager负责多个TaskManager,1个TaskManager包含多个插槽,插槽多少个代表并行度多少
- TaskManager会向资源管理器注册它的插槽,收到资源管理器指令后,TaskManager会把插槽提供给JobManager调用,JobManager就可以向插槽分配任务来执行
资源管理器(ResourceManager)
- 负责管理插槽(slot),插槽在flink中是最小的处理资源单元。
- 不同的环境和资源管理工具有不同的资源管理器,比如YARN、Mesos、K8s、standalone部署
- 当JobManager申请插槽资源时,如果ResourceManager没有足够的插槽,它则会向资源提供平台发起会话。
分发器Dispatcher)
- 可以跨作业运行,为应用提供了Rest接口
- 提供WebUI界面,方便展示和监控Flink
任务提交流程
YARN
任务调度原理
怎样进行并行计算?
每一个任务,每一步操作,都可以设置并行度,然后拆成并行的几个task,几个任务,就可以完成并行计算。
设置并行任务,分配到不同的slot上,多线程就可以执行起来。
并行的任务,需要占用多少slot?
一个流处理程序,到底包含多少个任务?
专业术语
并行度
- 特定算子的子任务的个数
- 整条流Stream的并行度是指当前所有并行度中最大的那个
Slots
- 执行一个独立任务/线程所需要的最小单元
- 默认按照CPU的核心数来分配资源,每个slots的内存是隔离的,但是CPU可以共享的。
- TaskManager对应一个进程,Slots对应一个线程
- 默认情况下,Flink允许子任务共享slot,即使他们是不同任务的子任务。这样的结果是,一个slot可以保存作业的整个管道。
- 一个slot可以保存作业整个管道的好处:如果其他的TaskManager挂掉了,并行度降低到1,也能保证作业能够完成执行。提高了健壮性。
- 使用slotSharingGroup对slot分组后,slot不会保存作业的整个管道
并行子任务的分配
一共有2+4+2+4+4=16个任务
需要slot=4
一般情况下,算子里面最大的并行度,代表了我们当前需要的slot数量
- 在运行时,Flink上运行的程序会被映射成“逻辑数据流”(dataflows),它包 含了这三部
- 每一个dataflow以一个或多个sources开始以一个或多个sinks结束。
执行图(ExecutionGraph)
Flink 中的执行图可以分成四层:StreamGraph -> JobGraph -> ExecutionGraph ->
物理执行图。
- StreamGraph:是根据用户通过 Stream API 编写的代码生成的最初的图。用
来表示程序的拓扑结构。 - JobGraph:StreamGraph 经过优化后生成了 JobGraph,提交给 JobManager 的
数据结构。主要的优化为,将多个符合条件的节点 chain 在一起作为一个节点,这
样可以减少数据在节点之间流动所需要的序列化/反序列化/传输消耗。 - ExecutionGraph : JobManager 根 据 JobGraph 生 成 ExecutionGraph 。
ExecutionGraph 是 JobGraph 的并行化版本,是调度层最核心的数据结构。 - 物理执行图:JobManager 根据 ExecutionGraph 对 Job 进行调度后,在各个
TaskManager 上部署 Task 后形成的“图”,并不是一个具体的数据结构。
数据传输形式
- 一个程序中,不同的算子可能具有不同的并行度
- 算子之间传输数据的形式可以是one-to-one模式也可以是redistributing模式,具体哪种形式,取决于算子的种类
- map、filter、flatMap等算子都是one-to-one模式
- Redistributing模式:stream的分区会发生改变,例如,keyBy 基于 hashCode 重
分区、而 broadcast 和 rebalance 会随机重新分区
任务链
- Flink采用了一种任务链优化技术。
- 如果并行度相同,并且为one-to-one,则这样的算子可以链接在一起形成一个task。
-
Flink流处理API
Environment
getExecutionEnvironmen
创建一个执行环境,表示当前执行程序的上下文。 如果程序是独立调用的,则
此方法返回本地执行环境;如果从命令行客户端调用程序以提交到集群,则此方法
返回此集群的执行环境,也就是说,getExecutionEnvironment 会根据查询运行的方
式决定返回什么样的运行环境,是最常用的一种创建执行环境的方式。
ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment();
程序与数据流
所有的Flink的程序都是由三部分组成:
Source、Transformation、Sink
Source:负责读取数据源
Transformation:利用各种算子进行处理加工
Sink:负责输出
Source
从集合读取数据
public class SourceTest1_Collection {
public static void main(String[] args) throws Exception{
// 创建执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
// 从集合中读取数据
DataStream<SensorReading> dataStream = env.fromCollection(Arrays.asList(
new SensorReading("sensor_1", 1547718199L, 35.8),
new SensorReading("sensor_6", 1547718201L, 15.4),
new SensorReading("sensor_7", 1547718202L, 6.7),
new SensorReading("sensor_10", 1547718205L, 38.1)
));
DataStream<Integer> integerDataStream = env.fromElements(1, 2, 4, 67, 189);
// 打印输出
dataStream.print("data");
integerDataStream.print("int");
// 执行
env.execute();
}
}
从文件读取数据
public class SourceTest2_File {
public static void main(String[] args) throws Exception{
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
// 从文件读取数据
DataStream<String> dataStream = env.readTextFile("D:\\Projects\\BigData\\FlinkTutorial\\src\\main\\resources\\sensor.txt");
// 打印输出
dataStream.print();
env.execute();
}
}
从kafka读取数据
public class SourceTest3_Kafka {
public static void main(String[] args) throws Exception{
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
Properties properties = new Properties();
properties.setProperty("bootstrap.servers", "localhost:9092");
properties.setProperty("group.id", "consumer-group");
properties.setProperty("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
properties.setProperty("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
properties.setProperty("auto.offset.reset", "latest");
// 从文件读取数据
DataStream<String> dataStream = env.addSource( new FlinkKafkaConsumer011<String>("sensor", new SimpleStringSchema(), properties));
// 打印输出
dataStream.print();
env.execute();
}
}
自定义Source
public class SourceTest4_UDF {
public static void main(String[] args) throws Exception{
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
// 从文件读取数据
DataStream<SensorReading> dataStream = env.addSource( new MySensorSource() );
// 打印输出
dataStream.print();
env.execute();
}
// 实现自定义的SourceFunction
public static class MySensorSource implements SourceFunction<SensorReading>{
// 定义一个标识位,用来控制数据的产生
private boolean running = true;
@Override
public void run(SourceContext<SensorReading> ctx) throws Exception {
// 定义一个随机数发生器
Random random = new Random();
// 设置10个传感器的初始温度
HashMap<String, Double> sensorTempMap = new HashMap<>();
for( int i = 0; i < 10; i++ ){
sensorTempMap.put("sensor_" + (i+1), 60 + random.nextGaussian() * 20);
}
while (running){
for( String sensorId: sensorTempMap.keySet() ){
// 在当前温度基础上随机波动
Double newtemp = sensorTempMap.get(sensorId) + random.nextGaussian();
sensorTempMap.put(sensorId, newtemp);
ctx.collect(new SensorReading(sensorId, System.currentTimeMillis(), newtemp));
}
// 控制输出频率
Thread.sleep(1000L);
}
}
@Override
public void cancel() {
running = false;
}
}}
Transform
map
把流数据转换成一个一个的数据输出
//把String转换成长度输出
DataStream<Integer> mapStream = inputStream.map(new MapFunction<String, Integer>() {
@Override
public Integer map(String value) throws Exception {
return value.length();
}
});
flatmap
把流数据转换成多条数据输出
// flatmap,按逗号分字段
DataStream<String> flatMapStream = inputStream.flatMap(new FlatMapFunction<String, String>() {
@Override
public void flatMap(String value, Collector<String> out) throws Exception {
String[] fields = value.split(",");
for( String field: fields )
out.collect(field);
}
});
filter
把流数据进行过滤后输出
// filter, 筛选sensor_1开头的id对应的数据
DataStream<String> filterStream = inputStream.filter(new FilterFunction<String>() {
@Override
public boolean filter(String value) throws Exception {
return value.startsWith("sensor_1");
}
});
KeyBy
把当前流分成多个分区,DataStream → KeyedStream:逻辑地将一个流拆分成不相交的分区,每个分
区包含具有相同 key 的元素,在内部以 hash 的形式实现的。
滚动聚合算子(Rolling Aggregation)
这些算子可以针对 KeyedStream 的每一个支流做聚合。
sum()
min()
max()
minBy()
maxBy()
KeyedStream<SensorReading, Tuple> keyedStream = dataStream.keyBy("id");
// 滚动聚合,取当前最大的温度值
DataStream<SensorReading> resultStream = keyedStream.maxBy("temperature");
resultStream.print("result");
Reduce
一个分组数据流的聚合操作,合并当前的元素
和上次聚合的结果,产生一个新的值,返回的流中包含每一次聚合的结果,而不是
只返回最后一次聚合的最终结果。
// reduce 聚合,取最小的温度值,并输出当前的时间戳
DataStream<SensorReading> reduceStream = keyedStream.reduce(new
ReduceFunction<SensorReading>() {
@Override
public SensorReading reduce(SensorReading value1, SensorReading value2)
throws Exception {
return new SensorReading(
value1.getId(),
value2.getTimestamp(),
Math.min(value1.getTemperature(), value2.getTemperature()));
}
});
split和select
用split切分流,切分后用select来得到不同的流数据
// 分流,按照温度值30度为界分为两条流
SplitStream<SensorReading> splitStream = dataStream.split(new OutputSelector<SensorReading>() {
@Override
public Iterable<String> select(SensorReading value) {
return (value.getTemperature() > 30) ? Collections.singletonList("high") : Collections.singletonList("low");
}
});
DataStream<SensorReading> highTempStream = splitStream.select("high");
DataStream<SensorReading> lowTempStream = splitStream.select("low");
DataStream<SensorReading> allTempStream = splitStream.select("high", "low");
Connect 和 CoMap
用Connect来连接两个流,连接后内部依然保持各自的数据和形式不发生任何变化,所以再需要用CoMap对流进行处理后变成一个流
ConnectedStreams<Tuple2<String, Double>, SensorReading> connectedStreams = warningStream.connect(lowTempStream);
DataStream<Object> resultStream = connectedStreams.map(new CoMapFunction<Tuple2<String, Double>, SensorReading, Object>() {
@Override
public Object map1(Tuple2<String, Double> value) throws Exception {
return new Tuple3<>(value.f0, value.f1, "high temp warning");
}
@Override
public Object map2(SensorReading value) throws Exception {
return new Tuple2<>(value.getId(), "normal");
}
});
Union
可以合并多条流,但是合并的数据类型得一样
highTempStream.union(lowTempStream, allTempStream);
Sink
数据输出到kafka
public class SinkTest1_Kafka {
public static void main(String[] args) throws Exception{
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
// // 从文件读取数据
// DataStream<String> inputStream = env.readTextFile("D:\\Projects\\BigData\\FlinkTutorial\\src\\main\\resources\\sensor.txt");
Properties properties = new Properties();
properties.setProperty("bootstrap.servers", "localhost:9092");
properties.setProperty("group.id", "consumer-group");
properties.setProperty("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
properties.setProperty("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
properties.setProperty("auto.offset.reset", "latest");
// 从文件读取数据
DataStream<String> inputStream = env.addSource( new FlinkKafkaConsumer011<String>("sensor", new SimpleStringSchema(), properties));
// 转换成SensorReading类型
DataStream<String> dataStream = inputStream.map(line -> {
String[] fields = line.split(",");
return new SensorReading(fields[0], new Long(fields[1]), new Double(fields[2])).toString();
});
dataStream.addSink( new FlinkKafkaProducer011<String>("localhost:9092", "sinktest", new SimpleStringSchema()));
env.execute();
}
}
数据输出到Redis
public class SinkTest2_Redis {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
// 从文件读取数据
DataStream<String> inputStream = env.readTextFile("D:\\MyConfiguration\\kmoon.zhu.TCENT\\Desktop\\Flink\\FlinkTutorial\\src\\main\\resources\\sensor.txt");
// 转换成SensorReading类型
DataStream<SensorReading> dataStream = inputStream.map(line -> {
String[] fields = line.split(",");
return new SensorReading(fields[0], new Long(fields[1]), new Double(fields[2]));
});
// 定义jedis连接配置
FlinkJedisPoolConfig config = new FlinkJedisPoolConfig.Builder()
.setHost("10.181.122.24")
.setPort(6379)
.build();
dataStream.addSink( new RedisSink<>(config, new MyRedisMapper()));
env.execute();
}
// 自定义RedisMapper
public static class MyRedisMapper implements RedisMapper<SensorReading>{
// 定义保存数据到redis的命令,存成Hash表,hset sensor_temp id temperature
@Override
public RedisCommandDescription getCommandDescription() {
return new RedisCommandDescription(RedisCommand.HSET, "sensor_temp");
}
@Override
public String getKeyFromData(SensorReading data) {
return data.getId();
}
@Override
public String getValueFromData(SensorReading data) {
return data.getTemperature().toString();
}
}
}
数据输出到ES
public class SinkTest3_Es {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
// 从文件读取数据
DataStream<String> inputStream = env.readTextFile("D:\\Projects\\BigData\\FlinkTutorial\\src\\main\\resources\\sensor.txt");
// 转换成SensorReading类型
DataStream<SensorReading> dataStream = inputStream.map(line -> {
String[] fields = line.split(",");
return new SensorReading(fields[0], new Long(fields[1]), new Double(fields[2]));
});
// 定义es的连接配置
ArrayList<HttpHost> httpHosts = new ArrayList<>();
httpHosts.add(new HttpHost("localhost", 9200));
dataStream.addSink(new ElasticsearchSink.Builder<SensorReading>(httpHosts, new MyEsSinkFunction()).build());
env.execute();
}
// 实现自定义的ES写入操作
public static class MyEsSinkFunction implements ElasticsearchSinkFunction<SensorReading>{
@Override
public void process(SensorReading element, RuntimeContext ctx, RequestIndexer indexer) {
// 定义写入的数据source
HashMap<String, String> dataSource = new HashMap<>();
dataSource.put("id", element.getId());
dataSource.put("temp", element.getTemperature().toString());
dataSource.put("ts", element.getTimestamp().toString());
// 创建请求,作为向es发起的写入命令
IndexRequest indexRequest = Requests.indexRequest()
.index("sensor")
.type("readingdata")
.source(dataSource);
// 用index发送请求
indexer.add(indexRequest);
}
}
}
数据输出到Mysql
public class SinkTest4_Jdbc {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
// 从文件读取数据
// DataStream<String> inputStream = env.readTextFile("D:\\Projects\\BigData\\FlinkTutorial\\src\\main\\resources\\sensor.txt");
//
// // 转换成SensorReading类型
// DataStream<SensorReading> dataStream = inputStream.map(line -> {
// String[] fields = line.split(",");
// return new SensorReading(fields[0], new Long(fields[1]), new Double(fields[2]));
// });
DataStream<SensorReading> dataStream = env.addSource(new SourceTest4_UDF.MySensorSource());
dataStream.addSink(new MyJdbcSink());
env.execute();
}
// 实现自定义的SinkFunction
public static class MyJdbcSink extends RichSinkFunction<SensorReading> {
// 声明连接和预编译语句
Connection connection = null;
PreparedStatement insertStmt = null;
PreparedStatement updateStmt = null;
@Override
public void open(Configuration parameters) throws Exception {
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "3997");
insertStmt = connection.prepareStatement("insert into sensor_temp (id, temp) values (?, ?)");
updateStmt = connection.prepareStatement("update sensor_temp set temp = ? where id = ?");
}
// 每来一条数据,调用连接,执行sql
@Override
public void invoke(SensorReading value, Context context) throws Exception {
// 直接执行更新语句,如果没有更新那么就插入
updateStmt.setDouble(1, value.getTemperature());
updateStmt.setString(2, value.getId());
updateStmt.execute();
if( updateStmt.getUpdateCount() == 0 ){
insertStmt.setString(1, value.getId());
insertStmt.setDouble(2, value.getTemperature());
insertStmt.execute();
}
}
@Override
public void close() throws Exception {
insertStmt.close();
updateStmt.close();
connection.close();
}
}
}
富函数(Rich Functions)
“富函数”是 DataStream API 提供的一个函数类的接口,所有 Flink 函数类都
有其 Rich 版本。它与常规函数的不同在于,可以获取运行环境的上下文,并拥有一
些生命周期方法,所以可以实现更复杂的功能。
RichMapFunction
RichFlatMapFunction
RichFilterFunction
DataStream<Tuple2<String, Integer>> resultStream = dataStream.map( new MyMapper() );
// 实现自定义富函数类
public static class MyMapper extends RichMapFunction<SensorReading, Tuple2<String, Integer>>{
@Override
public Tuple2<String, Integer> map(SensorReading value) throws Exception {
// getRuntimeContext().getState();
return new Tuple2<>(value.getId(), getRuntimeContext().getIndexOfThisSubtask());
}
@Override
public void open(Configuration parameters) throws Exception {
// 初始化工作,一般是定义状态,或者建立数据库连接
System.out.println("open");
}
@Override
public void close() throws Exception {
// 一般是关闭连接和清空状态的收尾操作
System.out.println("close");
}
}
数据重分区
rebalance:均匀分布
global全部都分配到进程1了
keyBy非均匀分布 sensor_1 全部都分配到了进程1
shuffle:随机分布
window窗口
flink把无限流切割成有限流的一种方式
window类型
➢ CountWindow:按照指定的数据条数生成一个 Window,与时间无关。
滚动窗口(Tumbling Window)
滑动窗口(Sliding Window)
➢ TimeWindow:按照时间生成 Window
滚动窗口(Tumbling Window)
滑动窗口(Sliding Window)
会话窗口(Session Window)
滚动窗口(Tumbling Window)
- 将数据依据固定的长度对窗口进行划分
- 使用时需要指定窗口的长度
- 时间对齐,窗口长度固定,没有重叠
- 可以算作滑动步长和窗口宽度相等的一种特殊的滑动窗口
适用场景:适合做BI统计(做每个时间段的聚合操作)
滑动窗口(Sliding Window)
- 滑动窗口由固定的窗口长度和滑动间隔组成
- 使用时需要指定窗口的长度和滑动间隔
- 时间对齐,窗口长度固定,可以有重叠
适用场景:对最近一个时间段内的统计(求某接口最近5min的失败率来决定是否要报警)
会话窗口(Session Window)
- 时间无对齐
- 当它在一个固定的时间周期内不再收到元素,即非活动间隔产生,那个这个窗口就会关闭
- 配置的是最小时间间隔
窗口分配器
- 我们可以用 .window() 来定义一个窗口,然后基于这个 window 去做一些聚 合或者其它处理操作。
- window()方法必须在KeyBy之后使用
创建不同类型的窗口
滚动时间窗口(tumbling time window)
.timeWindow(Time.seconds(15))
滑动时间窗口(sliding time window)
.timeWindow(Time.seconds(15),Time.seconds(5))
会话窗口(session window)
.window(EventTimeSessionWindows.withGap(Time.minutes(10))
创建不同类型的窗口
滚动计数窗口(tumbling count window)
.countWindow(5)
滑动计数窗口(sliding count window)
.countWindow(10,2)