前面一篇写了flink的原理以及单机安装配置,这篇主要讲Flink 的java API学习。今天想起了上周看到的MIT校训Mind and Hand,可以作为时刻提醒自己的语句,可以作为警醒自己的语句。心有多大,舞台就有多大。
1. DataStream
1.1 keyBy
逻辑上将数据流元素进行分区,具有相同key的记录被分到同一个分区
KeyedStream<String,Tuple> keyedStream = dataStream.keyBy(0);
如果需要定制key,在keyBy里定义new KeySelector<>()对象,实现getKey方法。
1.2 Iterator
输出计算结果时,对dataStream进行遍历,以写入数据库中。
2. Sink输出流
2.1 输出到HBase
public class HBaseOutputFormat implements OutputFormat<Tuple2<String, Integer>> {
private static final Logger logger = LoggerFactory.getLogger(HBaseOutputFormat.class);
private org.apache.hadoop.conf.Configuration conf = null;
private Connection conn = null;
private Table table = null;
private static String tableName = "Test";
private static Map<String,List<String>> columnFamilys = new HashMap<>();
private static String cf;
private static List<String> cols;
@Override
public void configure(Configuration configuration) {
}
public void InitConf(String cf, List<String> cols){
this.cf = cf;
this.cols = cols;
}
@Override
public void open(int i, int i1) throws IOException {
columnFamilys.put(cf,cols);
InitHBase.createTable(tableName,columnFamilys);
}
@Override
public void writeRecord(Tuple2<String, Integer> stringIntegerTuple2) throws IOException {
Map<String,String> tmp = new HashMap<>();
tmp.put(stringIntegerTuple2.f0,String.valueOf(stringIntegerTuple2.f1));
String rowkey = stringIntegerTuple2.f0;
if(rowkey.length() == 0){
rowkey = "null";
}
InitHBase.put(tableName,rowkey,cf,tmp);
}
@Override
public void close() throws IOException {
}
}
通过实现OutputFormat接口,读取Sink的数据。OutputFormat接口会每次读取一个Tuple2<String, Integer>格式的key/value对。
抽象方法
configure:配置输出格式,输出格式会根据配置值设置基本字段的地方,此方法总是在实例化输出格式上首先调用。
open:用于打开输出输出格式的并行实例,以配置存储其并行实例的结果,调用此方法时,将确保配置该方法的输出格式,所以一般会在open方法里进行数据库的连接,配置,建表等操作。
writeRecord:用于将数据写入数据源,在这里调用API进行数据库的写入。
close:关闭数据源的连接。
2.2 输出到mysql
flink自定义sink输出还有一种方式,继承RichSinkFunction类,实现configure(),open(),writeRecord(),close()方法,代码如下:
public class MysqlSink extends RichSinkFunction<Tuple2<String, Integer>> {
@Override
public void configure(Configuration configuration) {
}
@Override
public void open(int i, int i1) throws IOException {
}
@Override
public void writeRecord(Tuple2<String, Integer> stringIntegerTuple2) throws IOException {
}
@Override
public void close() {
}
}
实践中,open()方法在启动时会调用多次,这可能是flink的机制,为了确保open方法能够执行,这也是猜测,后面如果知道原因后,会填补这个空缺。mysql数据库的连接如果并发写数据库压力不大的话,最好写在writeRecord方法中,该方法会每次在reduce后得到的key,value结果对后都会执行一次,也就是写的时候创建数据库连接,写完后关闭数据库连接。在实践中,如果不每次连接和关闭的话,flink集群执行时会调用close方法两次,从而把你的连接会断。而且可能因为数据库的提交执行sql问题,数据会一直不见存进数据库里, 存在缓存里面,提交不了,丢失。当然这个还没有验证,后续验证后会更新此文章。
3. Flink time机制
3.1 Processing Time
指执行相应操作的机器的系统时间,因为在分布式和异步环境中,Processing Time并不能保证确定性,容易受到Event到达系统的速度以及数据在Flink系统内部处理的先后顺序的影响,所以Processing Time不能准确地反映数据产生的时间序列。
3.2 Ingestion Time
事件进入Flink的时间,Source处获取到这个数据的时间。虽然没有Processing Time那样因为Flink分布式系统的先后顺序和数据传输的影响,但存在数据传输过程的网络延迟,不能很好反映数据的时间序列情况。
3.3 Event Time
每条数据在其生产设备上发生的时间。这段时间通常嵌入在记录数据中,然后进入Flink,可以从记录中提取数据的时间戳,能充分反映数据的时间序列。
设置EventTime:
在创建运行环境后,需要设置时间戳提取器,并将TimeCharactersistic设置为EventTime。
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
该设置用于定义了流处理的时间使用事件时间。然后需要定义时间戳分配器。
使用事件时间作为处理时间需要每个事件都有一个事件时间戳,通常从数据中的某个字段得到。时间戳分配与生成watermark相结合,watermark告诉系统事件时间的处理进度。这里举出两种方法:
.assignTimestampsAndWatermarks(new AssignerWithPeriodicWatermarks<MobileEvent>() {
Long maxOutOfOrderness = 5000L;
@Nullable
@Override
public Watermark getCurrentWatermark() {
return new Watermark(System.currentTimeMillis() - maxOutOfOrderness);
}
@Override
public long extractTimestamp(MyEvent element, long previousElementTimestamp) {
return Long.parseLong(element.getCreationTime());
}
})
AssignerWithPeriodicWatermarks定期的分配时间戳和生成watermark,watermark生成的时间间隔通过ExecutionConfig.setAutoWatermarkInterval(...)
方法来定义。时间戳生成器getCurrentWatermark()方法每次都会被触发,如果返回结果不为空或者大于上一个的watermark,那么新的watermark将会被发送。在这里定义watermark为当前系统时间 - 最大允许延迟时间5秒。
.assignTimestampsAndWatermarks(new AssignerWithPeriodicWatermarks<MobileEvent>() {
private final long maxOutOfOrderness = 5000L;
private long currentMaxTimestamp;
@Nullable
@Override
public Watermark getCurrentWatermark() {
return new Watermark(currentMaxTimestamp - maxOutOfOrderness);
}
@Override
public long extractTimestamp(myEvent element, long previousElementTimestamp) {
long timestamp = Long.parseLong(element.getCreationTime());
currentMaxTimestamp = Math.max(timestamp,currentMaxTimestamp);
return timestamp;
}
})
第二种方法比第一种较动态,对于应付延迟不可控或大批事件延迟的情况具有比较好的适应。这里时间戳生成是取当前数据时间与当前最大时间戳之间的最大,在这里,如果后面的事件比前面的时间早到达,那么当前最大时间戳还是原来的,直到比该事件后的事件到达,才会更新,这样可以相对保护前面的事件延迟到达会被抛弃。
两种方法无关好坏,个人在比较乱序的情况下,第二种方法会完全乱套了,不能很好的反映数据的意义,所以针对场景进行选择。除了这两种,还有其他方法,这里不详细讲解了,有兴趣的可以到Flink的事件时间和watermarks(翻译Flink官方文档)
Windows操作
窗口化是Flink中阶段性处理数据流的方法,有时候我们需要对数据流进行阶段性的统计或聚合等操作,比如:在过去的一个小时广东各个区域线上化妆品成交量。在这种情况下,我们需要定义一个窗口,收集过去一小时的数据,并对这个窗口的数据进行calculate。窗口可以分为分组的流、非分组的流。区分是分组的stream调用keyBy(...)和window(...),非分组的stream调用windowAll(...)。
窗口分配器
窗口有几种:滚动窗口(Tumbling Windows)、滑动窗口(Sliding Windows)、会话窗口(Session Windows)、滚动计数窗口(Count Windows)。
- 滚动窗口(Tumbling Windows)
当我们需要统计每一小时用户购买的化妆品数量时,在flink中则使用Tumbling Windows,代码实现很简单,只要一句:
.timeWindow(Time.minutes(60))
- 滑动窗口(Sliding Windows)
我们需要每隔半个小时统计过去一小时用户购买的化妆品数量时,则需要使用到滑动窗口,实现如下:
.timeWindow(Time.minutes(60),Time.minutes(30))
- 会话窗口(Session Windows)
Session Windows是由数据的时间来决定的,比如根据用户id进行分组,得到如下的数据:
id1,09:00:00
id1,09:01:00
id1,09:03:00
id1,09:07:00
id1,09:14:00
id1,09:19:00
id1,09:30:00
id1,09:34:00
...
假设设置Session Window的时间gap为5分钟,则得到的窗口如下:
窗口1:(id1,09:00:00,09:12:00,3)
窗口2:(id1,09:14:00,09:24:00,2)
窗口3:(id2,09:30:00,09:34:00,2)
...
时间gap指数据间隔时间,上面设置时间间隔为5分钟,则数据时间间隔超过5分钟就会触发一个Session Window。代码实现如下:
.keyBy(_.userId)
.window(EventTimeSessionWindows.withGap(Time.minutes(5)))
当然,在设置使用数据时间时,需要定义时间戳生成器,从数据中提取时间戳。