├── README.md ├── chapter1 ├── pom.xml ├── src │ └── main │ │ └── java │ │ └── me │ │ └── zjy │ │ └── Main.java └── README.md ├── chapter2 ├── pom.xml ├── src │ └── main │ │ └── java │ │ └── me │ │ └── zjy │ │ ├── ComputeCpuMax.java │ │ ├── ComputeCpuMiddle.java │ │ └── ComputeCpuAvg.java └── README.md ├── chapter3 ├── pom.xml ├── src │ └── main │ │ └── java │ │ └── me │ │ └── zjy │ │ ├── BandwidthMonitor.java │ │ └── BandwidthMonitorWithEventTime.java ├── README.md └── img │ ├── stream_watermark_out_of_order.svg │ ├── tumbling-windows.svg │ ├── sliding-windows.svg │ └── session-windows.svg ├── .gitignore └── pom.xml /README.md: -------------------------------------------------------------------------------- 1 | # monitor-systam-flink-quickstart 2 | 3 | 学习Flink过程中,相比不断看各种原理而言,有目的的尝试开发更能有效的理解Flink计算框架各个机制在实际使用中的用途。例如,窗口机制可以让我们对流数据做定时监控,事件事件能让我们的监控报警更加准确。 4 | 5 | 写下这个系列,一切以流数据运用在监控报警作为使用场景,深入浅出,一方面自我总结Flink,另外一方面希望能为像我一样刚接触Flink的朋友快速入门Flink。 6 | 7 | ## 章节 8 | 9 | 1. chapter1: 第一个实时报警程序 10 | 2. chapter2: 初识有状态计算 11 | 3. chapter3: 窗口计算 12 | 13 | 持续更新中 ... -------------------------------------------------------------------------------- /chapter1/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | monitor-systam-flink-quickstart 7 | com.cnc 8 | 1.0.0 9 | 10 | 4.0.0 11 | 12 | chapter1 13 | 1.0.0 14 | 15 | 16 | -------------------------------------------------------------------------------- /chapter2/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | monitor-systam-flink-quickstart 7 | com.cnc 8 | 1.0.0 9 | 10 | 4.0.0 11 | 12 | chapter2 13 | 1.0.0 14 | 15 | 16 | -------------------------------------------------------------------------------- /chapter3/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | monitor-systam-flink-quickstart 7 | com.cnc 8 | 1.0.0 9 | 10 | 4.0.0 11 | 12 | chapter3 13 | 1.0.0 14 | 15 | 16 | -------------------------------------------------------------------------------- /chapter2/src/main/java/me/zjy/ComputeCpuMax.java: -------------------------------------------------------------------------------- 1 | package me.zjy; 2 | 3 | import org.apache.flink.api.common.functions.MapFunction; 4 | import org.apache.flink.api.java.tuple.Tuple3; 5 | import org.apache.flink.streaming.api.datastream.DataStream; 6 | import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment; 7 | 8 | /** 9 | * @author zhuangjy 10 | * @create 2019-07-18 21:13 11 | */ 12 | public class ComputeCpuMax { 13 | 14 | public static void main(String[] args) throws Exception { 15 | StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); 16 | DataStream text = env.socketTextStream("localhost", 8080); 17 | text.map(new MapFunction>() { 18 | @Override 19 | public Tuple3 map(String value) throws Exception { 20 | String[] items = value.split(" "); 21 | String host = items[1]; 22 | String cpu = items[2]; 23 | double usage = Double.parseDouble(items[3]); 24 | return new Tuple3<>(host, cpu, usage); 25 | } 26 | }).keyBy(0).max(2).print(); 27 | env.execute("ComputeCpuMax"); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Windows template 3 | # Windows thumbnail cache files 4 | Thumbs.db 5 | Thumbs.db:encryptable 6 | ehthumbs.db 7 | ehthumbs_vista.db 8 | 9 | # Dump file 10 | *.stackdump 11 | 12 | # Folder config file 13 | [Dd]esktop.ini 14 | 15 | # Recycle Bin used on file shares 16 | $RECYCLE.BIN/ 17 | 18 | # Windows Installer files 19 | *.cab 20 | *.msi 21 | *.msix 22 | *.msm 23 | *.msp 24 | 25 | # Windows shortcuts 26 | *.lnk 27 | 28 | ### macOS template 29 | # General 30 | .DS_Store 31 | .AppleDouble 32 | .LSOverride 33 | 34 | # Icon must end with two \r 35 | Icon 36 | 37 | # Thumbnails 38 | ._* 39 | 40 | # Files that might appear in the root of a volume 41 | .DocumentRevisions-V100 42 | .fseventsd 43 | .Spotlight-V100 44 | .TemporaryItems 45 | .Trashes 46 | .VolumeIcon.icns 47 | .com.apple.timemachine.donotpresent 48 | 49 | # Directories potentially created on remote AFP share 50 | .AppleDB 51 | .AppleDesktop 52 | Network Trash Folder 53 | Temporary Items 54 | .apdisk 55 | 56 | ### Example user template template 57 | ### Example user template 58 | 59 | # IntelliJ project files 60 | .idea 61 | *.iml 62 | out 63 | gen 64 | ### Maven template 65 | target/ 66 | pom.xml.tag 67 | pom.xml.releaseBackup 68 | pom.xml.versionsBackup 69 | pom.xml.next 70 | release.properties 71 | dependency-reduced-pom.xml 72 | buildNumber.properties 73 | .mvn/timing.properties 74 | .mvn/wrapper/maven-wrapper.jar 75 | 76 | -------------------------------------------------------------------------------- /chapter1/src/main/java/me/zjy/Main.java: -------------------------------------------------------------------------------- 1 | package me.zjy; 2 | 3 | import org.apache.flink.api.common.functions.FilterFunction; 4 | import org.apache.flink.api.common.functions.MapFunction; 5 | import org.apache.flink.api.java.tuple.Tuple3; 6 | import org.apache.flink.streaming.api.datastream.DataStream; 7 | import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment; 8 | 9 | /** 10 | * @author zhuangjy 11 | * @create 2019-07-18 20:20 12 | */ 13 | public class Main { 14 | 15 | public static void main(String[] args) throws Exception { 16 | StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); 17 | DataStream text = env.socketTextStream("localhost", 8080); 18 | text.map(new MapFunction>() { 19 | @Override 20 | public Tuple3 map(String value) throws Exception { 21 | String[] items = value.split(" "); 22 | String host = items[1]; 23 | String cpu = items[2]; 24 | double usage = Double.parseDouble(items[3]); 25 | return new Tuple3<>(host, cpu, usage); 26 | } 27 | }).filter(new FilterFunction>() { 28 | @Override 29 | public boolean filter(Tuple3 value) throws Exception { 30 | // 返回超过90保留,否则过滤掉 31 | return value.f2 > 90; 32 | } 33 | }).print(); 34 | env.execute("Window WordCount"); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /chapter3/src/main/java/me/zjy/BandwidthMonitor.java: -------------------------------------------------------------------------------- 1 | package me.zjy; 2 | 3 | import org.apache.flink.api.common.functions.FilterFunction; 4 | import org.apache.flink.api.common.functions.MapFunction; 5 | import org.apache.flink.api.common.functions.ReduceFunction; 6 | import org.apache.flink.api.java.tuple.Tuple2; 7 | import org.apache.flink.streaming.api.TimeCharacteristic; 8 | import org.apache.flink.streaming.api.datastream.DataStream; 9 | import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment; 10 | import org.apache.flink.streaming.api.windowing.time.Time; 11 | 12 | /** 13 | * @author zhuangjy 14 | * @create 2019-07-18 21:58 15 | */ 16 | public class BandwidthMonitor { 17 | 18 | 19 | public static void main(String[] args) throws Exception { 20 | StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); 21 | // 设置处理时间,如果不设置的话默认为处理时间 22 | env.setStreamTimeCharacteristic(TimeCharacteristic.ProcessingTime); 23 | 24 | DataStream text = env.socketTextStream("localhost", 8080); 25 | text.map(new MapFunction>() { 26 | @Override 27 | public Tuple2 map(String s) throws Exception { 28 | String[] items = s.split(" "); 29 | // 返回 channel -> 流量 30 | return new Tuple2<>(items[1], Long.parseLong(items[2])); 31 | } 32 | }).keyBy(0) 33 | // 滚动窗口 34 | .timeWindow(Time.minutes(1)) 35 | // 时间窗口 36 | // .timeWindow(Time.minutes(1), Time.seconds(15)) 37 | .reduce((ReduceFunction>) (stringLongTuple2, t1) -> new Tuple2<>(stringLongTuple2.f0, stringLongTuple2.f1 + t1.f1)) 38 | // 过滤出带宽值低于100Mbps域名 39 | .filter((FilterFunction>) stringLongTuple2 -> stringLongTuple2.f1 * 8.0 / 60 / 1024 / 1024 < 100) 40 | .print(); 41 | 42 | env.execute("BandwidthMonitor"); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /chapter2/src/main/java/me/zjy/ComputeCpuMiddle.java: -------------------------------------------------------------------------------- 1 | package me.zjy; 2 | 3 | import org.apache.flink.api.common.functions.MapFunction; 4 | import org.apache.flink.api.java.tuple.Tuple; 5 | import org.apache.flink.api.java.tuple.Tuple2; 6 | import org.apache.flink.streaming.api.datastream.DataStream; 7 | import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment; 8 | import org.apache.flink.streaming.api.functions.windowing.ProcessWindowFunction; 9 | import org.apache.flink.streaming.api.windowing.time.Time; 10 | import org.apache.flink.streaming.api.windowing.windows.TimeWindow; 11 | import org.apache.flink.util.Collector; 12 | 13 | import java.util.ArrayList; 14 | import java.util.Collections; 15 | import java.util.List; 16 | 17 | /** 18 | * @author zhuangjy 19 | * @create 2019-07-18 21:58 20 | */ 21 | public class ComputeCpuMiddle { 22 | 23 | public static void main(String[] args) throws Exception { 24 | StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); 25 | DataStream text = env.socketTextStream("localhost", 8080); 26 | text.map(new MapFunction>() { 27 | @Override 28 | public Tuple2 map(String value) throws Exception { 29 | String[] items = value.split(" "); 30 | String host = items[1]; 31 | double usage = Double.parseDouble(items[3]); 32 | return new Tuple2<>(host, usage); 33 | } 34 | }).keyBy(0).timeWindow(Time.minutes(1)).process(new ProcessWindowFunction, Double, Tuple, TimeWindow>() { 35 | @Override 36 | public void process(Tuple tuple, Context context, Iterable> elements, Collector out) throws Exception { 37 | List values = new ArrayList<>(); 38 | elements.forEach(t -> values.add(t.f1)); 39 | Collections.sort(values); 40 | 41 | if (values.isEmpty()) { 42 | out.collect(0.0); 43 | } else if (values.size() % 2 != 0) { 44 | out.collect(values.get(values.size() / 2)); 45 | } else { 46 | out.collect((values.get(values.size() / 2) + values.get(values.size() / 2 - 1)) / 2); 47 | } 48 | } 49 | }).print(); 50 | env.execute("ComputeCpuMiddle"); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /chapter2/src/main/java/me/zjy/ComputeCpuAvg.java: -------------------------------------------------------------------------------- 1 | package me.zjy; 2 | 3 | import org.apache.flink.api.common.functions.AggregateFunction; 4 | import org.apache.flink.api.common.functions.MapFunction; 5 | import org.apache.flink.api.java.tuple.Tuple2; 6 | import org.apache.flink.streaming.api.datastream.DataStream; 7 | import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment; 8 | import org.apache.flink.streaming.api.windowing.time.Time; 9 | 10 | /** 11 | * @author zhuangjy 12 | * @create 2019-07-18 21:58 13 | */ 14 | public class ComputeCpuAvg { 15 | 16 | public static void main(String[] args) throws Exception { 17 | StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); 18 | DataStream text = env.socketTextStream("localhost", 8080); 19 | text.map(new MapFunction>() { 20 | @Override 21 | public Tuple2 map(String value) throws Exception { 22 | String[] items = value.split(" "); 23 | String host = items[1]; 24 | double usage = Double.parseDouble(items[3]); 25 | return new Tuple2<>(host, usage); 26 | } 27 | }).keyBy(0) 28 | // 设置时间窗口,每分钟滚定一次,读取最近一分钟的数据 29 | .timeWindow(Time.minutes(1)) 30 | // 聚合函数,计算每个主机的数据条数、总cpu使用率 31 | .aggregate(new AggregateFunction, Tuple2, Double>() { 32 | @Override 33 | public Tuple2 createAccumulator() { 34 | // 初始化一个累加器 35 | return new Tuple2<>(0, 0.0); 36 | } 37 | 38 | @Override 39 | public Tuple2 add(Tuple2 value, Tuple2 accumulator) { 40 | // 新元素产生时加到现有累加器上 41 | accumulator.f0 += 1; 42 | accumulator.f1 += value.f1; 43 | return accumulator; 44 | } 45 | 46 | @Override 47 | public Double getResult(Tuple2 accumulator) { 48 | // 获取累加器对应值(计算均值) 49 | return accumulator.f0 == 0 ? 0.0 : accumulator.f1 / accumulator.f0; 50 | } 51 | 52 | @Override 53 | public Tuple2 merge(Tuple2 a, Tuple2 b) { 54 | // 合并累加器,可重用对象 55 | a.f0 += b.f0; 56 | a.f1 += b.f1; 57 | return a; 58 | } 59 | }).print(); 60 | env.execute("ComputeCpuAvg"); 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /chapter3/src/main/java/me/zjy/BandwidthMonitorWithEventTime.java: -------------------------------------------------------------------------------- 1 | package me.zjy; 2 | 3 | import org.apache.commons.lang3.StringUtils; 4 | import org.apache.flink.api.common.functions.FilterFunction; 5 | import org.apache.flink.api.common.functions.MapFunction; 6 | import org.apache.flink.api.common.functions.ReduceFunction; 7 | import org.apache.flink.api.java.tuple.Tuple2; 8 | import org.apache.flink.api.java.tuple.Tuple3; 9 | import org.apache.flink.streaming.api.TimeCharacteristic; 10 | import org.apache.flink.streaming.api.datastream.DataStream; 11 | import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment; 12 | import org.apache.flink.streaming.api.functions.AssignerWithPeriodicWatermarks; 13 | import org.apache.flink.streaming.api.functions.timestamps.BoundedOutOfOrdernessTimestampExtractor; 14 | import org.apache.flink.streaming.api.watermark.Watermark; 15 | import org.apache.flink.streaming.api.windowing.time.Time; 16 | 17 | import javax.annotation.Nullable; 18 | import java.time.LocalDateTime; 19 | import java.time.ZoneOffset; 20 | import java.util.concurrent.TimeUnit; 21 | 22 | public class BandwidthMonitorWithEventTime { 23 | 24 | public static void main(String[] args) throws Exception { 25 | StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); 26 | // 设置事件时间 27 | env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime); 28 | DataStream text = env.socketTextStream("localhost", 8080); 29 | // 在执行任何操作前需要先执行watermark设置 30 | text.assignTimestampsAndWatermarks(new BoundedOutOfOrdernessTimestampExtractor(Time.minutes(1L)) { 31 | @Override 32 | public long extractTimestamp(String element) { 33 | int time = (int) LocalDateTime.parse(element.split(" ")[0]).toEpochSecond(ZoneOffset.ofHours(8)); 34 | return time * 1000L; 35 | } 36 | }).map(new MapFunction>() { 37 | @Override 38 | public Tuple3 map(String s) throws Exception { 39 | String[] items = s.split(" "); 40 | // 返回 时间 -> channel -> 流量 41 | Integer time = (int) LocalDateTime.parse(items[0]).toEpochSecond(ZoneOffset.ofHours(8)); 42 | String channel = items[1]; 43 | Long flow = Long.parseLong(items[2]); 44 | return new Tuple3<>(time, channel, flow); 45 | }}).keyBy(1) 46 | .timeWindow(Time.minutes(5),Time.seconds(5)) 47 | .reduce((ReduceFunction>) (integerStringLongTuple3, t1) -> new Tuple3<>(integerStringLongTuple3.f0,integerStringLongTuple3.f1,integerStringLongTuple3.f2 + t1.f2)) 48 | .map(new MapFunction, Tuple2>() { 49 | @Override 50 | public Tuple2 map(Tuple3 integerStringLongTuple3) throws Exception { 51 | return new Tuple2<>(integerStringLongTuple3.f1,integerStringLongTuple3.f2 * 8.0 / 60 / 1024 / 1024); 52 | } 53 | }) 54 | // 过滤出带宽值低于100Mbps域名 55 | .filter((FilterFunction>) stringDoubleTuple2 -> stringDoubleTuple2.f1 < 100.0).print(); 56 | 57 | env.execute("BandwidthMonitorWithEventTime"); 58 | } 59 | 60 | 61 | 62 | } 63 | -------------------------------------------------------------------------------- /chapter1/README.md: -------------------------------------------------------------------------------- 1 | 2 | # 第一个实时报警程序 3 | 4 | 我们希望构建一个实时监控系统,假设我们有非常多的机器。每台机器都会定时上一个CPU使用率指标。上报格式为CSV,如下所示: 5 | 6 | ``` 7 | 1563452056 10.8.22.1 cpu0 80.5 8 | 1563452055 10.8.22.1 cpu1 77.5 9 | 1563452051 10.8.22.1 cpu2 10.5 10 | 1563452058 10.8.22.1 cpu2 100.0 11 | 1563452058 10.8.22.2 cpu2 10.0 12 | ... 13 | ``` 14 | 15 | 上面的数据分别表示: 16 | 1. 机器上报指标的时间 17 | 2. 机器IP 18 | 3. 哪个cpu核上报的 19 | 4. cpu使用率 20 | 21 | 每台机器每分钟会上报自己的每个核的使用率(即每分钟每个核最多一条数据)。 22 | 23 | 我们知道,cpu使用率太高的话机器可能就无法正常运作了。因此有效、快速的监控到机器cpu跑高十分重要。 24 | 25 | 为了低时延监控,我们选择了Flink实现第一个实时报警程序,假设现在所有的 Cpu 数据都上报到了我们的一个 Socket Server, 我们的Flink程序会从Socket Server 实时拉取数据并进行监控。 26 | 27 | ## 监控策略 28 | 29 | 第一步,我们希望实时打印出所有的主机对应核以及cpu使用率,这样我们可以实时的看到所有上报的数据。 30 | 31 | ``` java 32 | StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); 33 | DataStream text = env.socketTextStream("localhost", 8080); 34 | text.map(new MapFunction>() { 35 | @Override 36 | public Tuple3 map(String value) throws Exception { 37 | String[] items = value.split(" "); 38 | String host = items[1]; 39 | String cpu = items[2]; 40 | double usage = Double.parseDouble(items[3]); 41 | return new Tuple3<>(host, cpu, usage); 42 | } 43 | }).print(); 44 | env.execute("Window WordCount"); 45 | ``` 46 | 47 | ``` java 48 | StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); 49 | ``` 50 | 51 | 这一行表示创建执行环境,是Flink的第一步。通过StreamExecutionEnvironment这个入口类,我们可以用来设置参数和创建数据源以及提交任务。 52 | 53 | 下一步,我们创建了一个DataStream,该DataStream实时消费本地的8080 Socket Server数据。 54 | 55 | 再下一步,我们对DataStream做了一系列操作,通过Map算子将数据转化为 (host,cpu核,value) 的三元组,然后调用了print实时打印出上面的三元组。 56 | 57 | ``` java 58 | env.execute("Window WordCount"); 59 | ``` 60 | 61 | 最后的 env.execute 调用是启动实际Flink作业所必需的。所有算子操作(例如创建源、聚合、打印)只是构建了内部算子操作的图形。只有在execute()被调用时才会在提交到集群上或本地计算机上执行。 62 | 63 | ## 运行程序 64 | 65 | 我们使用nc命令模拟一个Socket Server: 66 | ``` bash 67 | nc -lk 8080 68 | ``` 69 | 70 | 启动程序后,flink自动连接上我们socket server并进行实时监听,我们在终端输入监控数据: 71 | 72 | ``` 73 | 1563452056 10.8.22.1 cpu0 80.5 74 | 1563452051 10.8.22.1 cpu2 10.5 75 | 1563452051 10.8.22.1 cpu2 10.5 76 | ``` 77 | 78 | 接下来会实时看到程序终端打印结果: 79 | 80 | ``` 81 | 3> (10.8.22.1,cpu0,80.5) 82 | 4> (10.8.22.1,cpu2,10.5) 83 | 1> (10.8.22.1,cpu2,10.5) 84 | ``` 85 | 86 | 说数据被实时收到并且处理了。 87 | 88 | 到这里,我们最简单的MVP版本已开发完毕,但是在使用中,我们会不断发现打出来的数据越来越多了,甚至是很多无用的数据。 89 | 例如,我们只关心cpu使用率 > 90 的数据,那么这时候我们可以使用Flink的filter算子帮我们过滤数据。 90 | 修改后的代码如下所示: 91 | 92 | ``` java 93 | StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); 94 | DataStream text = env.socketTextStream("localhost", 8080); 95 | text.map(new MapFunction>() { 96 | @Override 97 | public Tuple3 map(String value) throws Exception { 98 | String[] items = value.split(" "); 99 | String host = items[1]; 100 | String cpu = items[2]; 101 | double usage = Double.parseDouble(items[3]); 102 | return new Tuple3<>(host, cpu, usage); 103 | } 104 | }).filter(new FilterFunction>() { 105 | @Override 106 | public boolean filter(Tuple3 value) throws Exception { 107 | // 返回90保留否则过滤掉 108 | return value.f2 > 90; 109 | } 110 | }).print(); 111 | env.execute("Window WordCount"); 112 | ``` 113 | 114 | 修改后,再输入: 115 | ``` 116 | 1563452051 10.8.22.1 cpu2 10.5 117 | 1563452051 10.8.22.1 cpu2 99.2 118 | ``` 119 | 120 | 发现仅仅输出后面那条数据: 121 | ``` 122 | 2> (10.8.22.1,cpu2,99.2) 123 | ``` 124 | 125 | 即我们有效的过滤了无用的数据,可以安心的看着我们的监控大屏啦! 126 | 127 | ## 总结 128 | 129 | 通过上面的案例,我们掌握了以下几个知识点: 130 | 131 | * Flink程序开发的框架 132 | * Map算子操作做数据转换、Filter算子操作过滤不需要的数据 133 | * Flink事件驱动计算模式的开发 -------------------------------------------------------------------------------- /chapter2/README.md: -------------------------------------------------------------------------------- 1 | 2 | # 初识有状态计算 3 | 4 | 在上一章节中,我们构建了一个实时监控CPU使用率的报警程序。 5 | 6 | 但是该程序还有很多的缺陷,例如该程序虽然能计算实时数据,但是都是无状态的。 7 | 8 | > 什么是无状态、有状态计算: 9 | > 10 | > 无状态流处理分别接收每条记录,然后根据最新输入的记录输出记录。有状态的计算会维护状态(根据每条输入记录进行更新),并给予最新输入的记录和当前的状态值生成输出记录。 11 | 12 | 这次,我们希望在上一个程序的基础上实现两种监控: 13 | 14 | 1. 监控CPU峰值:实时监控所有主机CPU使用率,每次打印出该机器历史峰值 15 | 2. 监控CPU均值:每分钟监控最近一分钟每台机器CPU平均使用率,打印主机以及平均CPU使用率信息 16 | 3. 监控CPU中位数:每分钟监控最近一分钟每台机器CPU平均使用率,打印主机以及平均CPU使用率中位数 17 | 18 | 本次的这两种策略都是有状态的,因为每次处理完数据,都需要维护状态。 19 | 20 | ## 1. 编写程序 - 监控CPU峰值 21 | 22 | 这里,我们来实现监控CPU峰值程序。 23 | 24 | ``` java 25 | StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); 26 | DataStream text = env.socketTextStream("localhost", 8080); 27 | text.map(new MapFunction>() { 28 | @Override 29 | public Tuple3 map(String value) throws Exception { 30 | String[] items = value.split(" "); 31 | String host = items[1]; 32 | String cpu = items[2]; 33 | double usage = Double.parseDouble(items[3]); 34 | return new Tuple3<>(host, cpu, usage); 35 | } 36 | }).keyBy(0).max(2).print(); 37 | env.execute("ComputeCpuMax") 38 | ``` 39 | 40 | 我们在现有的案例中,仅仅做了非常小的改动就实现了,我们的需求。 41 | 42 | 我们使用keyBy函数实现了按主机聚合的操作,函数参数为作为agg key的元素索引,host在我们三元组中处于第一个元素,因此这里填写0. 43 | ``` java 44 | keyBy(0) 45 | ``` 46 | 47 | 然后,我们调用状态函数max函数,实时计算当前值和当前已记录的峰值的最大值,若当前值大于峰值则更新,并且打印记录。 48 | 49 | 50 | ## 2. 运行程序 - 监控CPU峰值 51 | 52 | 我们先后输入数据: 53 | 54 | ``` 55 | 1563452056 10.8.22.1 cpu0 80.5 56 | 1563452050 10.8.22.1 cpu0 78.4 57 | 1563452056 10.8.22.1 cpu0 99.9 58 | ``` 59 | 60 | 接下来会实时看到程序终端打印结果: 61 | 62 | ``` 63 | 3> (10.8.22.1,cpu0,80.5) 64 | 3> (10.8.22.1,cpu0,80.5) 65 | 3> (10.8.22.1,cpu0,99.9) 66 | ``` 67 | 68 | 从结果,我们可以看到我们状态被实时更新,并且最大值也在收到数据时实时打印。这里我们就完成了一个最简单的状态计算程序。 69 | 70 | ## 3. 编写程序 - 监控CPU均值 71 | 72 | 这里,我们来实现监控CPU均值程序,这个案例与上面不同的是上面的案例每条数据对会触发完成的计算输出结论,这里我们引入了Flink时间窗口API。 73 | 74 | 时间窗口是Flink的核心特性之一,时间窗口表示程序可以获取某段指定时间的数据针对这段时间的数据做一定的计算。因为在流操作中,数据是无穷无尽的,因此均值、中位值等计算通常是指一定时间窗口内的数据,如最近一分钟的数据。 75 | 76 | > 在监控系统中,时间窗口也具有极高的价值,因为我们通常关注的计算策略通常为最近N分钟,指标的某种聚合结果。 77 | 78 | Flink还提供了滑动窗口、技数窗口、会话窗口等高级特性,我们这里先只关注基本的滚动窗口使用,下一章节我们再针对窗口实现原理做深入学习。 79 | 80 | ``` java 81 | StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); 82 | DataStream text = env.socketTextStream("localhost", 8080); 83 | text.map(new MapFunction>() { 84 | @Override 85 | public Tuple2 map(String value) throws Exception { 86 | String[] items = value.split(" "); 87 | String host = items[1]; 88 | double usage = Double.parseDouble(items[3]); 89 | return new Tuple2<>(host, usage); 90 | } 91 | }).keyBy(0) 92 | // 设置时间窗口,每分钟滚定一次,读取最近一分钟的数据 93 | .timeWindow(Time.minutes(1)) 94 | // 聚合函数,计算每个主机的数据条数、总cpu使用率 95 | .aggregate(new AggregateFunction, Tuple2, Double>() { 96 | @Override 97 | public Tuple2 createAccumulator() { 98 | // 初始化一个累加器 99 | return new Tuple2<>(0, 0.0); 100 | } 101 | @Override 102 | public Tuple2 add(Tuple2 value, Tuple2 accumulator) { 110 | // 获取累加器对应值(计算均值) 111 | return accumulator.f0 == 0 ? 0.0 : accumulator.f1 / accumulator.f0; 112 | } 113 | @Override 114 | public Tuple2 merge(Tuple2 a, Tuple2 b) { 115 | // 合并累加器,可重用对象 116 | a.f0 += b.f0; 117 | a.f1 += b.f1; 118 | return a; 119 | } 120 | }).print(); 121 | env.execute("ComputeCpuAvg"); 122 | ``` 123 | 124 | 这次的代码看起来比先前长了很多,我们先是对先前keyBy的结果使用了时间窗口: 125 | 126 | ``` java 127 | .timeWindow(Time.minutes(1)); 128 | ``` 129 | 130 | 该代码表示对每个收到数据的主机每分钟会申明一个滚动时间窗口,计算最近这一分钟内的数据(这里频率以及窗口大小都是一分钟,因此是滚动窗口)。 131 | 132 | 接下来我们要实现求均值,我们知道求均值的计算方式是: 133 | 134 | ``` 135 | (CPU使用率总和) / 数据条数 136 | ``` 137 | 138 | 因此,我们申明了一个增量累加函数 aggregate,该函数定义为: 139 | 140 | ``` java 141 | public SingleOutputStreamOperator aggregate(AggregateFunction function) 142 | ``` 143 | 144 | 其中,T为我们的上游数据类型,ACC是累加数值的类型,R是返回的结果值。该函数的原理为窗口中一开始没数据时定义一个默认的ACC元素,然后有数据产生时调用add方法累加到ACC元素上, 145 | 实际上在这里merge函数不会被触发(添加日志后证实),merge函数只有在窗口合并的时候调用,合并两个容器,当窗口函数到达触发时刻调用getResult方法返回结果。 146 | 147 | 我们可以看到,整个过程是增量计算的(无需保留所有原始数据),因此在大数据量时是非常高效的。常用的增量计算函数还有 reduce 函数,我们在开发中应该优先使用增量计算函数减少资源的浪费。 148 | 149 | 150 | ## 4. 运行程序 - 监控CPU均值 151 | 152 | 输入数据: 153 | ``` 154 | 1563452056 10.8.22.1 cpu0 80.5 155 | 1563452050 10.8.22.1 cpu0 78.4 156 | 1563452056 10.8.22.1 cpu0 99.9 157 | 1563452056 10.8.22.2 cpu1 20.2 158 | ``` 159 | 160 | 等了一分钟后程序终端输出 161 | ``` 162 | 3> 86.26666666666667 163 | 3> 20.2 164 | ``` 165 | 166 | 说明主机都被正确计算了均值。 167 | 168 | 又等了若干分钟后,由于没输入数据,在终端也再看不到数据输出了,说明滚动窗口不断产生不断迭代。 169 | 170 | 171 | ## 5. 编写程序 - 监控CPU中位数 172 | 173 | 监控CPU中位数计算方式同均值类似,不过中为数由于需要保留所有的数据排序后得到因此不可进行增量计算。因此这里我们在timeWindow后使用了process方法。 174 | 175 | process方法可以申明一个 ProcessWindowFunction 实现类,该实现类主要覆写了方法: 176 | 177 | ```java 178 | /** 179 | * Evaluates the window and outputs none or several elements. 180 | * 181 | * @param key The key for which this window is evaluated. 182 | * @param context The context in which the window is being evaluated. 183 | * @param elements The elements in the window being evaluated. 184 | * @param out A collector for emitting elements. 185 | * 186 | * @throws Exception The function may throw exceptions to fail the program and trigger recovery. 187 | */ 188 | public abstract void process(KEY key, Context context, Iterable elements, Collector out) throws Exception; 189 | ``` 190 | 191 | 从注释中,我们可以知道该函数的几个参数含义: 192 | 193 | * key: 窗口对应的Key值,例如这里我们的Key值实际上就是ip地址(KeyBy内容) 194 | * context:正在运行的窗口上下文,包括开始时间、结束时间等元信息 195 | * elements: 窗口中的所有正在被计算的元素 196 | * out: 数据结果收集器 197 | 198 | 调整后的代码如下: 199 | 200 | ``` java 201 | StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); 202 | DataStream text = env.socketTextStream("localhost", 8080); 203 | text.map(new MapFunction>() { 204 | @Override 205 | public Tuple2 map(String value) throws Exception { 206 | String[] items = value.split(" "); 207 | String host = items[1]; 208 | double usage = Double.parseDouble(items[3]); 209 | return new Tuple2<>(host, usage); 210 | } 211 | }).keyBy(0).timeWindow(Time.minutes(1)).process(new ProcessWindowFunction, Tuple, TimeWindow>() { 212 | @Override 213 | public void process(Tuple tuple, Context context, Iterable> elements, CoDouble> out) throws Exception { 214 | List values = new ArrayList<>(); 215 | elements.forEach(t -> values.add(t.f1)); 216 | Collections.sort(v 217 | if (values.isEmpty()) { 218 | out.collect(0.0); 219 | } else if (values.size() % 2 != 0) { 220 | out.collect(values.get(values.size() / 2)); 221 | } else { 222 | out.collect((values.get(values.size() / 2) + values.get(values.size() / 2 - 1)) / 2); 223 | } 224 | } 225 | }).print(); 226 | env.execute("ComputeCpuMiddle") 227 | ``` 228 | 229 | 我们实现了自己的ProcessWindowFunction实现类,以及其对应的process方法。在该方法中,我们重排了窗口中的元素并且计算出中位值后返回。 230 | 231 | > 从代码中我们可以看到process灵活度会比reduce、aggregate高很多。但是process由于需要计算所有数据,在数据量下,大窗口是非常影响程序运行效率的。因此能用reduce、aggregate的就千万不要使用process! 232 | 233 | 234 | ## 6. 运行程序 - 监控CPU中位数 235 | 236 | 输入数据: 237 | ``` 238 | 1563452056 10.8.22.1 cpu0 80.5 239 | 1563452050 10.8.22.1 cpu0 78.4 240 | 1563452056 10.8.22.1 cpu0 99.9 241 | 1563452056 10.8.22.2 cpu1 20.2 242 | ``` 243 | 244 | 输出数据: 245 | ``` 246 | 3> 80.5 247 | 3> 20.2 248 | ``` 249 | 250 | 中位数被正确计算出来。 251 | 252 | 253 | ## 总结 254 | 255 | 在这节中,我们通过计算: 256 | 257 | 1. CPU峰值 258 | 2. CPU均值 259 | 3. CPU中位值 260 | 261 | 学习到了有状态计算下一些核心知识点: 262 | 263 | * 使用keyBy汇聚相同key的数据 264 | * 滚动窗口api timeWindow的使用 265 | * reduce/aggregate/process等窗口处理函数,并且明白了他们的区别(是否增量计算) 266 | 267 | 通过这些知识点,我们现在可以开发出很灵活的监控程序了。但是这还远远不够,因为我们目前的计算都是针对 *处理时间* 而非 *计算时间*,对于数据延迟、滑动窗口计算等问题均未考虑。 268 | 269 | 下一节,我们将学习watermark、数据时间等知识点,并且尝试更多的窗口类型,如计数窗口、会话窗口等,通过这些机制,我们可以定制出更准确、优雅的报警程序。 -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.cnc 8 | monitor-systam-flink-quickstart 9 | pom 10 | 1.0.0 11 | 12 | chapter1 13 | chapter2 14 | chapter3 15 | 16 | 17 | 18 | UTF-8 19 | 1.8.0 20 | 1.8 21 | 2.11 22 | ${java.version} 23 | ${java.version} 24 | 25 | 26 | 27 | 28 | apache.snapshots 29 | Apache Development Snapshot Repository 30 | https://repository.apache.org/content/repositories/snapshots/ 31 | 32 | false 33 | 34 | 35 | true 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | org.apache.flink 45 | flink-java 46 | ${flink.version} 47 | provided 48 | 49 | 50 | 51 | org.apache.flink 52 | flink-streaming-java_${scala.binary.version} 53 | ${flink.version} 54 | provided 55 | 56 | 57 | 58 | org.slf4j 59 | slf4j-log4j12 60 | 1.7.7 61 | runtime 62 | 63 | 64 | 65 | log4j 66 | log4j 67 | 1.2.17 68 | runtime 69 | 70 | 71 | 72 | org.junit.jupiter 73 | junit-jupiter-api 74 | RELEASE 75 | test 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | org.apache.maven.plugins 85 | maven-compiler-plugin 86 | 3.1 87 | 88 | ${java.version} 89 | ${java.version} 90 | 91 | 92 | 93 | 94 | 95 | 96 | org.apache.maven.plugins 97 | maven-shade-plugin 98 | 3.0.0 99 | 100 | 101 | 102 | package 103 | 104 | shade 105 | 106 | 107 | 108 | 109 | org.apache.flink:force-shading 110 | com.google.code.findbugs:jsr305 111 | org.slf4j:* 112 | log4j:* 113 | 114 | 115 | 116 | 117 | 119 | *:* 120 | 121 | META-INF/*.SF 122 | META-INF/*.DSA 123 | META-INF/*.RSA 124 | 125 | 126 | 127 | 128 | 130 | com.cnc.StreamingJob 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | org.eclipse.m2e 145 | lifecycle-mapping 146 | 1.0.0 147 | 148 | 149 | 150 | 151 | 152 | org.apache.maven.plugins 153 | maven-shade-plugin 154 | [3.0.0,) 155 | 156 | shade 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | org.apache.maven.plugins 166 | maven-compiler-plugin 167 | [3.1,) 168 | 169 | testCompile 170 | compile 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | add-dependencies-for-IDEA 191 | 192 | 193 | 194 | idea.version 195 | 196 | 197 | 198 | 199 | 200 | org.apache.flink 201 | flink-java 202 | ${flink.version} 203 | compile 204 | 205 | 206 | org.apache.flink 207 | flink-streaming-java_${scala.binary.version} 208 | ${flink.version} 209 | compile 210 | 211 | 212 | 213 | 214 | 215 | 216 | -------------------------------------------------------------------------------- /chapter3/README.md: -------------------------------------------------------------------------------- 1 | 2 | # 窗口计算 3 | 4 | 在上一个章节中,我们尝试了有状态计算,分别实现了机器峰值、均值、中位值的监控。但是我们做的还远远不够,这节,我们将借助Flink提供的事件时间、watermark以及三种窗口计算(滑动窗口、计数窗口、会话窗口)实现更加稳定可靠的监控程序。 5 | 6 | 7 | 8 | ## 1. 窗口类型 9 | 10 | 什么是窗口呢?可以这么理解,在批处理中,我们一般经历如下过程: 11 | 12 | 1. 拉取所有数据 13 | 2. 计算 14 | 3. 输出结果 15 | 16 | 可以这么说, **一个批处理中,对应的窗口就是全部的数据。 **在这里窗口的意义显得有点多余。 17 | 18 | 但是在流处理中却是必须的,因为**流数据是无穷无尽,类似一条不断的河流一样。**这里就必须使用窗口来界定需要计算的数据范围。窗口的概念在监控系统中也十分的重要。例如,我们有某个域名每分钟的流量,我们想监控某个域名的平均带宽是否低于某个指定阈值。 19 | 20 | > 带宽的含义:一些主机服务商会给带宽以不同的含义。在这里,带宽几乎变成一个单位时间内的流量概念。意思是单位时间内的下行数据总量。意味着如果一个公司提供每月2GB的带宽,意思就是用户每月最多只能下载2GB的内容。 —— 《维基百科》 21 | > 22 | > 这里为了简化: 23 | > 24 | > 1. 流量 = B(字节)当前流量值 25 | > 26 | > 2. 带宽 = Mbps 带宽描述的是某段时间内,单位时间的流量。 其值 = (周期内总流量值)* 8 / 时间 / 1024 / 1024 27 | 28 | 显然,我们的监控通常是监控当前的实时带宽更有价值。下面,我们分别来实现几种监控: 29 | 30 | 1. 每分钟计算当前这一分钟的实际带宽值,若低于100Mbps则产生报警 31 | 2. 每15s计算最近1分钟的实际带宽值,若低于100Mbps则产生报警 32 | 33 | 显然,方式2更加敏感,在监控中也更为常用。 34 | 35 | 对于方式1,有一个很重要的特性,即运行频率以及查看的时间区间(窗口大小)均设置为1分钟,是一种滚动的方式(把它想象成一个正方形滚动),这叫做 **滚动窗口**,它每次计算的数据是独一无二,无任何重叠的。 36 | 37 | ![tumbling-windows](./img/tumbling-windows.svg) 38 | 39 | 而对于第二项监控,它同样有一个很特殊的特性:它的运行频率以及查看的时间区间(时间窗口)不一致,是一种不断滑动,不断计算的过程,计算的过程中会有一部分数据被多次计算。 40 | 41 | ![sliding-windows](./img/sliding-windows.svg) 42 | 43 | 下面,我们来尝试编写实现代码: 44 | 45 | ``` java 46 | StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); 47 | DataStream text = env.socketTextStream("localhost", 8080); 48 | text.map(new MapFunction>() { 49 | @Override 50 | public Tuple2 map(String s) throws Exception { 51 | String[] items = s.split(" "); 52 | // 返回 channel -> 流量 53 | return new Tuple2<>(items[1], Long.parseLong(items[2])); 54 | } 55 | }).keyBy(0) 56 | // 滚动窗口 57 | .timeWindow(Time.minutes(1)) 58 | // 时间窗口 59 | // .timeWindow(Time.minutes(1), Time.seconds(15)) 60 | .reduce((ReduceFunction>) (stringLongTuple2, t1) -> new Tuple2<>(stringLongTuple2.f0, stringLongTuple2.f1 + t1.f1)) 61 | // 过滤出带宽值低于100Mbps域名 62 | .filter((FilterFunction>) stringLongTuple2 -> stringLongTuple2.f1 * 8.0 / 60 / 1024 / 1024 < 100) 63 | .print(); 64 | env.execute("BandwidthSlideMonitor"); 65 | ``` 66 | 67 | 从代码中可以发现,从滚动窗口到滑动窗口的转换非常简单,只要添加一个滑动周期参数即可。 68 | 69 | 下面测试输入输出: 70 | 71 | ``` 72 | 2019-08-28T10:00:00 www.163.com 10000 73 | 2019-08-28T10:01:00 www.163.com 100 74 | 2019-08-28T10:02:00 www.163.com 100 75 | 2019-08-28T10:03:00 www.163.com 1000 76 | ``` 77 | 78 | 运行结果: 79 | 80 | 1. 滚动时间窗口:等待1分钟左右得到输出 (www.163.com,11200) 即我们输入的所有流量值总和 81 | 2. 滑动时间窗口:等待15秒左右得到输出 (www.163.com,11200) 即我们输入的所有流量值总和 82 | 83 | 84 | 85 | 这里我们发现有个问题,即我们的时间窗口虽然是基于时间滑动的,但是数据却无法关联上时间的特性。即我们运行的时候,窗口说是要拉1分钟的数据,但是这个1分钟的数据的定义是从上次开始至触发期间收集到的数据,和数据所带有的数据时间完全无关。**在监控系统中,数据乱序、延迟是非常常见的,倘若无法基于正确的数据时间计算,很可能会出现错误的结果。因此这就涉及到“时间的概念”。** 86 | 87 | 88 | 89 | ### 1.1 时间类型 90 | 91 | 在流处理中有三种时间的概念: 92 | 93 | 1. 处理时间(Process Time) 94 | 2. 事件时间(Event Time) 95 | 3. 摄入时间 (Ingest Time) 96 | 97 | 对于我们上面的例子中,数据如下,假如我们是在2019-08-28T11:30:00时刻接受到下面这些数据。 98 | 99 | ``` 100 | 2019-08-28T10:00:00 www.163.com 10000 101 | 2019-08-28T10:01:00 www.163.com 100 102 | 2019-08-28T10:02:00 www.163.com 100 103 | 2019-08-28T10:03:00 www.163.com 1000 104 | ``` 105 | 106 | 倘若我们的逻辑是希望监控最近10分钟内的数据,那么实际上在30分的时候我们希望取到的是20分~30分发生的事情。这个时候我们若使用的是处理时间,那我们关注的是20分~30分期间**收到的数据**而不是实际上这些数据真正发生的时间。因此虽然我们的数据都是一个小时前的数据,我们也能触发计算。 107 | 108 | 处理时间是三种时间中使用最简单的一种,也是Flink中默认的时间。 109 | 110 | 但是,处理时间是 **不稳定**,严重依赖数据的时效性,如果数据延迟较大,那么可能在触发计算的时候没办法取到完整的数据,那么就可能导致错误的计算结果。同时,使用数据时间的计算程序无法 "复盘" 历史数据。例如,我们的监控程序修改了计算逻辑,并且我们获取了昨天一整天的数据,现在希望用新逻辑重跑这些数据输出结果,如果使用的是处理时间,由于处理时间依赖于系统时间,所以无法有效复盘。 111 | 112 | 所有,为了解决上述的问题,Flink 支持了**稳定的事件时间。** 113 | 114 | 事件时间,顾名思义,就是这条事件真实发生的时间,如上面的例子,分别表示域名在10点连续4分钟的时间。 115 | 116 | 使用时间时间时,我们就能获得准确的计算,我们复盘数据时,程序可依靠数据时间准确的触发计算。 117 | 118 | **事件时间是Flink中最重要的特性之一。也是监控系统报警计算实现回测,保证正确性严重依赖的特性之一。** 119 | 120 | 最后一种时间是**摄取时间**,即进入Flink程序的时间,有时候,我们的数据没办法打上准确的事件时间,但是又希望比处理时间更精确,那么可以设置为摄取时间。 121 | 122 | 理想情况下(无任何传输延迟),事件时间(数据产生时间) = 摄入时间 = 处理时间。 123 | 124 | 125 | 126 | ## 1.2 编写代码 127 | 128 | 下面看看如何设置处理时间与事件时间: 129 | 130 | ``` java 131 | StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); 132 | // 设置处理时间,如果不设置的话默认为处理时间 133 | env.setStreamTimeCharacteristic(TimeCharacteristic.ProcessingTime); 134 | ``` 135 | 136 | 处理时间的设置非常简单,假如没有设置系统也会默认使用处理时间。 137 | 138 | 而对于事件时间,**就必须要手工设置。** 139 | 140 | 141 | 142 | 对于事件时间的设置,除了上述的 setStreamTimeCharacteristic 方法以外,还有两个必须要做的事情: 143 | 144 | 1. 指定时间时间列 145 | 2. 设置Watermark 146 | 147 | 对于第一点很好理解,你必须告诉Flink你的事件时间是哪一列,而对于Watermark,我们现在来好好捋一捋概念。 148 | 149 | 150 | 151 | ### Watermark 152 | 153 | 在说watermark的概念前,我们可以先了解下 **数据乱序** 的概念。 154 | 155 | 如下所示的数据,分别表示顺序的三分钟数据,在上一节中,我们说过理论上数据产生时间 = 数据业务时间 = 数据摄入(接收)时间。 156 | 157 | ``` 158 | 2019-08-28T10:00:00 www.163.com 10000 159 | 2019-08-28T10:01:00 www.163.com 100 160 | 2019-08-28T10:02:00 www.163.com 100 161 | ``` 162 | 163 | 然而,可能我们在00分的时候调用了一次数据发送,由于网络原因或者下游组件堆积等原因等到03分才被接收到,而01分以及02分的数据都在产生的时候顺利被接收到,所以对于我们的程序接收到的数据看起来是这样的: 164 | 165 | ``` 166 | 2019-08-28T10:01:00 www.163.com 100 167 | 2019-08-28T10:02:00 www.163.com 100 168 | 2019-08-28T10:00:00 www.163.com 10000 169 | ``` 170 | 171 | 因此,数据产生了乱序。**数据延迟,是流式计算中不可避免的,同时也是产生乱序的根本原因。** 172 | 173 | 除此之外,在一些消息队列中,消息队列的处理机制也可能导致数据乱序,例如在Kafka中,只能保证单个分区中的数据是按照写入顺序,分区与分区间的数据无序的,这就可能导致我们有三条数据,第一条最早的数据在分区1中,而其他数据在分区2。消费程序获取数据时由于是从不同分区获取,并不能保证最早拉到的一定是最早的那条数据。 174 | 175 | 那么,我们怎么来解决数据乱序的问题呢? 176 | 177 | 在以前,我们写JStorm程序的时候,为了解决乱序问题,我们想到了 ”延迟等待“ 的方式。设置一个固定的延迟时间,例如1分钟。我们每分钟会计算一次,计算的时候取一分钟的数据,即典型的滚动窗口模式,当设置了数据延迟时,会晚一个周期计算,如下所示: 178 | 179 | ``` 180 | 数据时间 10 11 12 13 181 | 系统时间 11 12 13 14 182 | 不设置延迟情况下,每分钟取前一分钟的数据点进行计算 183 | 184 | 数据时间 10 11 12 13 185 | 系统时间 12 13 14 15 186 | 延迟时间 = 1分钟 187 | ``` 188 | 189 | 这样,我们相当于设置了一个容忍时间,即延迟在1分钟以内的情况下,我们能准确计算。如果数据延迟超过了1分钟的时间,那么我们之前的做法是直接丢弃数据。 190 | 191 | **而在 Flink 中,定义的 Watermark 机制,与我们之前自己想的数据延迟设定一定程度上不谋而合,并且更加强大、全面、鲁棒性也更好!** 192 | 193 | 194 | 195 | Watermark 翻译为中文即为水位线。水位线是Flink中用于度量事件时间的机制。Watermark 可以被像数据一样定义在DataStream中,该Stream一般会携带一个时间戳 k : 196 | 197 | ``` 198 | Watermark(k) 199 | ``` 200 | 201 | 上面的定义表示,事件时间在k之前的已经全部到达。 202 | 203 | ![stream_watermark_out_of_order](./img/stream_watermark_out_of_order.svg) 204 | 205 | 如图所示,数据不断在产生,图中的数字表示事件时间,而W表示水位线,W(11) 告诉程序此时时间时间低于11的全部到达。 206 | 207 | 看到这里,有的人可能会有疑惑,我怎么设置水位线,能保证在此之前的数据全部到达呢? 208 | 209 | 实际上,流式计算中通常是不可能预先确保数据都已到达,Flink 中提供了 Allowed Lateness 机制,让开发者可以选择对滞后于 Watermark 的数据的处理操作: 210 | 211 | 1. 直接丢弃数据(默认情况) 212 | 2. 允许一定的延迟,当我们设定了 allowedLateness(T) 方法,那么当Watermark触发后,还允许有T秒的延迟,若数据在这段时间中到达,会再次触发一次窗口计算逻辑(为了达到这样的效果Flink必须保持窗口状态,直到T时间过后) 213 | 3. 单独处理延迟数据,Flink 支持定义一个流,延迟的数据会输出到这个流中 214 | 215 | ``` java 216 | final OutputTag lateOutputTag = new OutputTag("late-data"){}; 217 | 218 | DataStream input = ...; 219 | 220 | SingleOutputStreamOperator result = input 221 | .keyBy() 222 | .window() 223 | .allowedLateness(