Flink实时计算topN热榜

开发 架构
为了统计每个窗口下活跃的用户,我们需要再次按窗口进行分组,这里根据UserViewCount中的windowEnd进行keyBy()操作。

[[386499]]

本文转载自微信公众号「Java大数据与数据仓库」,作者柯广。转载本文请联系Java大数据与数据仓库公众号。

topN的常见应用场景,最热商品购买量,最高人气作者的阅读量等等。

1. 用到的知识点

  • Flink创建kafka数据源;
  • 基于 EventTime 处理,如何指定 Watermark;
  • Flink中的Window,滚动(tumbling)窗口与滑动(sliding)窗口;
  • State状态的使用;
  • ProcessFunction 实现 TopN 功能;

2. 案例介绍

通过用户访问日志,计算最近一段时间平台最活跃的几位用户topN。

  • 创建kafka生产者,发送测试数据到kafka;
  • 消费kafka数据,使用滑动(sliding)窗口,每隔一段时间更新一次排名;

3. 数据源

这里使用kafka api发送测试数据到kafka,代码如下:

  1. @Data 
  2. @NoArgsConstructor 
  3. @AllArgsConstructor 
  4. @ToString 
  5. public class User { 
  6.  
  7.     private long id; 
  8.     private String username; 
  9.     private String password
  10.     private long timestamp
  11.  
  12. Map<String, String> config = Configuration.initConfig("commons.xml"); 
  13.  
  14. @Test 
  15. public void sendData() throws InterruptedException { 
  16.     int cnt = 0; 
  17.  
  18.     while (cnt < 200){ 
  19.         User user = new User(); 
  20.         user.setId(cnt); 
  21.         user.setUsername("username" + new Random().nextInt((cnt % 5) + 2)); 
  22.         user.setPassword("password" + cnt); 
  23.         user.setTimestamp(System.currentTimeMillis()); 
  24.         Future<RecordMetadata> future = KafkaUtil.sendDataToKafka(config.get("kafka-topic"), String.valueOf(cnt), JSON.toJSONString(user)); 
  25.         while (!future.isDone()){ 
  26.             Thread.sleep(100); 
  27.         } 
  28.         try { 
  29.             RecordMetadata recordMetadata = future.get(); 
  30.             System.out.println(recordMetadata.offset()); 
  31.         } catch (InterruptedException e) { 
  32.             e.printStackTrace(); 
  33.         } catch (ExecutionException e) { 
  34.             e.printStackTrace(); 
  35.         } 
  36.         System.out.println("发送消息:" + cnt + "******" + user.toString()); 
  37.         cnt = cnt + 1; 
  38.     } 

这里通过随机数来扰乱username,便于使用户名大小不一,让结果更加明显。KafkaUtil是自己写的一个kafka工具类,代码很简单,主要是平时做测试方便。

4. 主要程序

创建一个main程序,开始编写代码。

创建flink环境,关联kafka数据源。

  1. Map<String, String> config = Configuration.initConfig("commons.xml"); 
  2.  
  3. Properties kafkaProps = new Properties(); 
  4. kafkaProps.setProperty("zookeeper.connect", config.get("kafka-zookeeper")); 
  5. kafkaProps.setProperty("bootstrap.servers", config.get("kafka-ipport")); 
  6. kafkaProps.setProperty("group.id", config.get("kafka-groupid")); 
  7.  
  8. StreamExecutionEnvironment senv = StreamExecutionEnvironment.getExecutionEnvironment(); 

EventTime 与 Watermark

  1. senv.setStreamTimeCharacteristic(TimeCharacteristic.EventTime); 

设置属性senv.setStreamTimeCharacteristic(TimeCharacteristic.EventTime),表示按照数据时间字段来处理,默认是TimeCharacteristic.ProcessingTime

  1. /** The time characteristic that is used if none other is set. */ 
  2. private static final TimeCharacteristic DEFAULT_TIME_CHARACTERISTIC = TimeCharacteristic.ProcessingTime; 

这个属性必须设置,否则后面,可能窗口结束无法触发,导致结果无法输出。取值有三种:

  • ProcessingTime:事件被处理的时间。也就是由flink集群机器的系统时间来决定。
  • EventTime:事件发生的时间。一般就是数据本身携带的时间。
  • IngestionTime:摄入时间,数据进入flink流的时间,跟ProcessingTime还是有区别的;

指定好使用数据的实际时间来处理,接下来需要指定flink程序如何get到数据的时间字段,这里使用调用DataStream的assignTimestampsAndWatermarks方法,抽取时间和设置watermark。

  1. senv.addSource( 
  2.         new FlinkKafkaConsumer010<>( 
  3.                 config.get("kafka-topic"), 
  4.                 new SimpleStringSchema(), 
  5.                 kafkaProps 
  6.         ) 
  7. ).map(x ->{ 
  8.     return JSON.parseObject(x, User.class); 
  9. }).assignTimestampsAndWatermarks(new BoundedOutOfOrdernessTimestampExtractor<User>(Time.milliseconds(1000)) { 
  10.     @Override 
  11.     public long extractTimestamp(User element) { 
  12.         return element.getTimestamp(); 
  13.     } 
  14. }) 

前面给出的代码中可以看出,由于发送到kafka的时候,将User对象转换为json字符串了,这里使用的是fastjson,接收过来可以转化为JsonObject来处理,我这里还是将其转化为User对象JSON.parseObject(x, User.class),便于处理。

这里考虑到数据可能乱序,使用了可以处理乱序的抽象类BoundedOutOfOrdernessTimestampExtractor,并且实现了唯一的一个没有实现的方法extractTimestamp,乱序数据,会导致数据延迟,在构造方法中传入了一个Time.milliseconds(1000),表明数据可以延迟一秒钟。比如说,如果窗口长度是10s,0~10s的数据会在11s的时候计算,此时watermark是10,才会触发计算,也就是说引入watermark处理乱序数据,最多可以容忍0~t这个窗口的数据,最晚在t+1时刻到来。

 

具体关于watermark的讲解可以参考这篇文章

https://blog.csdn.net/qq_39657909/article/details/106081543

窗口统计

业务需求上,通常可能是一个小时,或者过去15分钟的数据,5分钟更新一次排名,这里为了演示效果,窗口长度取10s,每次滑动(slide)5s,即5秒钟更新一次过去10s的排名数据。

  1. .keyBy("username"
  2. .timeWindow(Time.seconds(10), Time.seconds(5)) 
  3. .aggregate(new CountAgg(), new WindowResultFunction()) 

我们使用.keyBy("username")对用户进行分组,使用.timeWindow(Time size, Time slide)对每个用户做滑动窗口(10s窗口,5s滑动一次)。然后我们使用 .aggregate(AggregateFunction af, WindowFunction wf) 做增量的聚合操作,它能使用AggregateFunction提前聚合掉数据,减少 state 的存储压力。较之.apply(WindowFunction wf)会将窗口中的数据都存储下来,最后一起计算要高效地多。aggregate()方法的第一个参数用于

这里的CountAgg实现了AggregateFunction接口,功能是统计窗口中的条数,即遇到一条数据就加一。

  1. public class CountAgg implements AggregateFunction<User, Long, Long>{ 
  2.     @Override 
  3.     public Long createAccumulator() { 
  4.         return 0L; 
  5.     } 
  6.  
  7.     @Override 
  8.     public Long add(User value, Long accumulator) { 
  9.         return accumulator + 1; 
  10.     } 
  11.  
  12.     @Override 
  13.     public Long getResult(Long accumulator) { 
  14.         return accumulator; 
  15.     } 
  16.  
  17.     @Override 
  18.     public Long merge(Long a, Long b) { 
  19.         return a + b; 
  20.     } 

.aggregate(AggregateFunction af, WindowFunction wf) 的第二个参数WindowFunction将每个 key每个窗口聚合后的结果带上其他信息进行输出。我们这里实现的WindowResultFunction将用户名,窗口,访问量封装成了UserViewCount进行输出。

  1. private static class WindowResultFunction implements WindowFunction<Long, UserViewCount, Tuple, TimeWindow> { 
  2.  
  3.  
  4.     @Override 
  5.     public void apply(Tuple key, TimeWindow window, Iterable<Long> input, Collector<UserViewCount> out) throws Exception { 
  6.         Long count = input.iterator().next(); 
  7.         out.collect(new UserViewCount(((Tuple1<String>)key).f0, window.getEnd(), count)); 
  8.     } 
  9.  
  10. @Data 
  11. @NoArgsConstructor 
  12. @AllArgsConstructor 
  13. @ToString 
  14. public static class UserViewCount { 
  15.     private String userName; 
  16.     private long windowEnd; 
  17.     private long viewCount; 
  18.  

TopN计算最活跃用户

为了统计每个窗口下活跃的用户,我们需要再次按窗口进行分组,这里根据UserViewCount中的windowEnd进行keyBy()操作。然后使用 ProcessFunction 实现一个自定义的 TopN 函数 TopNHotItems 来计算点击量排名前3名的用户,并将排名结果格式化成字符串,便于后续输出。

  1. .keyBy("windowEnd"
  2. .process(new TopNHotUsers(3)) 
  3. .print(); 

ProcessFunction 是 Flink 提供的一个 low-level API,用于实现更高级的功能。它主要提供了定时器 timer 的功能(支持EventTime或ProcessingTime)。本案例中我们将利用 timer 来判断何时收齐了某个 window 下所有用户的访问数据。由于 Watermark 的进度是全局的,在 processElement 方法中,每当收到一条数据(ItemViewCount),我们就注册一个 windowEnd+1 的定时器(Flink 框架会自动忽略同一时间的重复注册)。windowEnd+1 的定时器被触发时,意味着收到了windowEnd+1的 Watermark,即收齐了该windowEnd下的所有用户窗口统计值。我们在 onTimer() 中处理将收集的所有商品及点击量进行排序,选出 TopN,并将排名信息格式化成字符串后进行输出。

这里我们还使用了 ListState 来存储收到的每条 UserViewCount 消息,保证在发生故障时,状态数据的不丢失和一致性。ListState 是 Flink 提供的类似 Java List 接口的 State API,它集成了框架的 checkpoint 机制,自动做到了 exactly-once 的语义保证。

  1. private static class TopNHotUsers extends KeyedProcessFunction<Tuple, UserViewCount, String> { 
  2.  
  3.     private int topSize; 
  4.     private ListState<UserViewCount> userViewCountListState; 
  5.  
  6.     public TopNHotUsers(int topSize) { 
  7.         this.topSize = topSize; 
  8.     } 
  9.  
  10.     @Override 
  11.     public void onTimer(long timestamp, OnTimerContext ctx, Collector<String> out) throws Exception { 
  12.         super.onTimer(timestamp, ctx, out); 
  13.         List<UserViewCount> userViewCounts = new ArrayList<>(); 
  14.         for(UserViewCount userViewCount : userViewCountListState.get()) { 
  15.             userViewCounts.add(userViewCount); 
  16.         } 
  17.  
  18.         userViewCountListState.clear(); 
  19.  
  20.         userViewCounts.sort(new Comparator<UserViewCount>() { 
  21.             @Override 
  22.             public int compare(UserViewCount o1, UserViewCount o2) { 
  23.                 return (int)(o2.viewCount - o1.viewCount); 
  24.             } 
  25.         }); 
  26.  
  27.         // 将排名信息格式化成 String, 便于打印 
  28.         StringBuilder result = new StringBuilder(); 
  29.         result.append("====================================\n"); 
  30.         result.append("时间: ").append(new Timestamp(timestamp-1)).append("\n"); 
  31.         for (int i = 0; i < topSize; i++) { 
  32.             UserViewCount currentItem = userViewCounts.get(i); 
  33.             // No1:  商品ID=12224  浏览量=2413 
  34.             result.append("No").append(i).append(":"
  35.                     .append("  用户名=").append(currentItem.userName) 
  36.                     .append("  浏览量=").append(currentItem.viewCount) 
  37.                     .append("\n"); 
  38.         } 
  39.         result.append("====================================\n\n"); 
  40.  
  41.         Thread.sleep(1000); 
  42.  
  43.         out.collect(result.toString()); 
  44.  
  45.     } 
  46.  
  47.     @Override 
  48.     public void open(org.apache.flink.configuration.Configuration parameters) throws Exception { 
  49.         super.open(parameters); 
  50.         ListStateDescriptor<UserViewCount> userViewCountListStateDescriptor = new ListStateDescriptor<>( 
  51.                 "user-state"
  52.                 UserViewCount.class 
  53.         ); 
  54.         userViewCountListState = getRuntimeContext().getListState(userViewCountListStateDescriptor); 
  55.  
  56.     } 
  57.  
  58.     @Override 
  59.     public void processElement(UserViewCount value, Context ctx, Collector<String> out) throws Exception { 
  60.         userViewCountListState.add(value); 
  61.         ctx.timerService().registerEventTimeTimer(value.windowEnd + 1000); 
  62.     } 

结果输出

可以看到,每隔5秒钟更新输出一次数据。

 

参考

 

http://wuchong.me/blog/2018/11/07/use-flink-calculate-hot-items/

 

责任编辑:武晓燕 来源: Java大数据与数据仓库
相关推荐

2021-06-06 13:10:12

FlinkPvUv

2021-07-16 10:55:45

数仓一体Flink SQL

2015-07-31 10:35:18

实时计算

2022-12-29 09:13:02

实时计算平台

2019-06-27 09:12:43

FlinkStorm框架

2016-12-28 14:27:24

大数据Apache Flin搜索引擎

2015-08-31 14:27:52

2015-10-09 13:42:26

hbase实时计算

2017-09-26 09:35:22

2019-11-21 09:49:29

架构运维技术

2021-06-03 08:10:30

SparkStream项目Uv

2019-02-18 15:23:21

马蜂窝MESLambda

2021-07-05 10:48:42

大数据实时计算

2011-10-28 09:05:09

2016-11-02 09:02:56

交通大数据计算

2021-03-10 14:04:10

大数据计算技术

2017-01-15 13:45:20

Docker大数据京东

2020-09-10 17:41:14

ClickHouse数据引擎

2022-08-24 09:19:03

美团计算

2014-04-11 10:35:49

实时计算
点赞
收藏

51CTO技术栈公众号