Flink 第1章 Flink资源与内存模型占用空间计算方式。

本文指导如何评估Flink任务资源需求,介绍1.13.2版本的配置参数,详细解读内存模型,包括JVM、Flink框架、Task和网络内存,通过实例解析内存分配过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Flink 第1章 Flink资源与内存模型


资源配置调优

开发了一些程序,那么怎么评估这些程序所需要的资源配比这些呢?
比如使用标准的Flink任务提交脚本 Generic CLI模式(通用客户端模型)

从1.11开始,增加了通用的客户端模型 使用-D指定kv变量(这里演示以1.13.2为准)。

bin/flink run \ 
-t yarn-per-job \ 
-d \
-p 5\   # 执行并行度
-Dyarn.application.queue=test \ # 指定yarn队列
-Djobmanager.memory.preocess.size=1024mb \ # 指定JM总进程的内存
-Dtaskmanager.memory.process.size=1024mb \ #指定每个TM的总进程大小
-Dtaskmanage.numberOfTaskSlots=2 \ #指定每个TM的slot数
-c com.huangyichun.flink.Demo \ #指定运行类
./flink-test-1.0.jar  #指定jar包


内存设置

在Flink1.10之后,对内存模型有很大的改动,变为以下图示:

两大部分,堆内存和堆外内存。


内存模型详解

JVM 特定内存

JVM本身需要使用到的内存,比如JVM的metaspace和over-head

JVM metaspace 元空间

taskmanager.memort.jvm-metaspace.size 256mb

JVM over-head 执行开销

JVM执行时自身所需要的内容,包括线程堆栈、IO、编译缓存等所使用的内存。有个OOM不就是 over-head

taskmanage.memory.jvm-overhead.fraction 0.1
taskmanage.memory.jvm-overhead.min 192mb
taskmanage.memory.jvm-overhead.max 1gb


Flink 框架内存

Flink框架,即TaskManager本身所占用的内存,不计入Slot的资源中。
分为两部分,堆内的和堆外的。

堆内

taskmanager.memory.framework.heap.size 128mb

堆外

taskmanager.memory.framework.off-heap.size 128mb


Task内存

Task 执行用户代码时所使用的的内存,也分为堆内和堆外。

堆内

taskmanager.memory.task.heap.size none
扣除其他部分的内存,剩余的

堆外

taskmanage.memory.task.off-heap.size 0
不适用堆外内存


网络内存

网络数据交换所使用的堆外内存大小,如网络数据交换缓冲区

网络内存

taskmanager.memory.network.fraction 0.1
这里注意并不是总内存的0.1,而是扣除JVM特定内存的0.1
taskmanager.memory.network.min 64mb
taskmanager.memory.nerwork.max 1gb


托管内存

托管内存,只要使用了RocksDB State Backend的本地内存和批的排序、哈希表、缓存中间结果就会占用这部分内存。

托管内存

taskmanager.memory.managed.fraction 0.4
taskmanager.memory.managed.size none
size为空则等于Flink内存*0.4

内存使用实例

比如程序直接提交到Yarn上去,设置了

taskmanager.memory.process.size 进程的总内存 = 4G。

那么从下往上开始计算。

JVM 执行开销

192mb<=4G*0.1<=1gb,那么就占用0.4G。

JVM 元空间

固定0.2G

Flink的内存

4G-0.4G-256mb =3.4G

管理内存

3.4G*0.4 = 1.36G

网络缓冲区

364mb<=3.4*0.1<=1gb,占用0.34G
Flink框架堆内对外
0.1G*2 = 0.2G

Task堆外

默认为0

Task堆内

4−0.4−0.2−1.36−0.34−0.2−0=1.5G4 - 0.4 -0.2 -1.36 - 0.34 -0.2 -0 =1.5 G40.40.21.360.340.20=1.5G
(页面上可能有一点点偏差,为显示问题)

问题:用户访问某网站页面,每次打开页面时都会记录一条(userid, pageurl, timestamp)信息,请设计一套流式计算Flink or Spark Streaming),实现以下功能:(考察点:考虑页面pv数据倾斜问题) 最近1小时内实时的页面uv量top10统计,输出结果如下: page2, 200000 page10, 1000 page15, 500 page7, 300 代码如下: import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner; import org.apache.flink.api.common.eventtime.WatermarkStrategy; import org.apache.flink.api.common.functions.AggregateFunction; import org.apache.flink.api.common.state.MapState; import org.apache.flink.api.common.state.MapStateDescriptor; import org.apache.flink.api.common.state.ValueState; import org.apache.flink.api.common.state.ValueStateDescriptor; import org.apache.flink.api.java.tuple.Tuple2; import org.apache.flink.configuration.Configuration; import org.apache.flink.streaming.api.datastream.DataStream; import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment; import org.apache.flink.streaming.api.functions.KeyedProcessFunction; import org.apache.flink.streaming.api.functions.windowing.ProcessWindowFunction; import org.apache.flink.streaming.api.windowing.assigners.TumblingEventTimeWindows; import org.apache.flink.streaming.api.windowing.time.Time; import org.apache.flink.streaming.api.windowing.windows.TimeWindow; import org.apache.flink.util.Collector; import java.util.*; public class test { public static void main(String[] args) throws Exception { StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); env.setParallelism(4); // 提高并行度 // 模拟数据源 DataStream<UserBehavior> dataStream = env.fromElements( new UserBehavior("user1", "pageA", 1000L), new UserBehavior("user2", "pageA", 2000L), new UserBehavior("user1", "pageB", 3000L), new UserBehavior("user3", "pageA", 4000L), new UserBehavior("user2", "pageB", 5000L), new UserBehavior("user4", "pageC", 6000L), new UserBehavior("user3", "pageB", 7000L), new UserBehavior("user5", "pageC", 8000L) ); // 修复1:明确时间戳分配器类型 DataStream<UserBehavior> timedStream = dataStream .assignTimestampsAndWatermarks( WatermarkStrategy.<UserBehavior>forMonotonousTimestamps() .withTimestampAssigner( (SerializableTimestampAssigner<UserBehavior>) (element, recordTimestamp) -> element.timestamp ) ); // 阶段1:分桶聚合(解决倾斜) DataStream<PageBucketCount> bucketStream = timedStream .map(new BucketMapper(10)) // 10个桶 .keyBy(value -> Tuple2.of(value.pageId, value.bucketId)) // 修复2:使用Tuple2明确类型 .window(TumblingEventTimeWindows.of(Time.minutes(10))) .aggregate(new BucketAggregate(), new BucketWindowResult()); // 阶段2:合并分桶结果 DataStream<PageViewCount> uvStream = bucketStream .keyBy(PageBucketCount::getPageId) .window(TumblingEventTimeWindows.of(Time.minutes(10))) .reduce((value1, value2) -> new PageBucketCount( value1.pageId, -1, // 桶ID不再需要 value1.count + value2.count, Math.max(value1.windowEnd, value2.windowEnd) ) ) .map(count -> new PageViewCount(count.pageId, count.count, count.windowEnd)); // TopN处理(优化状态管理) DataStream<String> result = uvStream .keyBy(PageViewCount::getWindowEnd) .process(new TopNPages(10)); result.print("Top10 Pages"); env.execute("Optimized Page UV TopN"); } // ===== 分桶映射(核心倾斜解决方案) ===== public static class BucketMapper implements org.apache.flink.api.common.functions.MapFunction<UserBehavior, PageBucket> { private final int bucketCount; private final Random random = new Random(); public BucketMapper(int bucketCount) { this.bucketCount = bucketCount; } @Override public PageBucket map(UserBehavior value) { int bucketId = random.nextInt(bucketCount); return new PageBucket(value.userId, value.pageId, bucketId, value.timestamp); } } // ===== 分桶聚合函数 ===== public static class BucketAggregate implements AggregateFunction<PageBucket, Set<String>, Long> { @Override public Set<String> createAccumulator() { return new HashSet<>(); } @Override public Set<String> add(PageBucket value, Set<String> accumulator) { accumulator.add(value.userId); return accumulator; } @Override public Long getResult(Set<String> accumulator) { return (long) accumulator.size(); } @Override public Set<String> merge(Set<String> a, Set<String> b) { a.addAll(b); return a; } } // ===== 分桶窗口结果 ===== public static class BucketWindowResult extends ProcessWindowFunction<Long, PageBucketCount, Tuple2<String, Integer>, TimeWindow> { @Override public void process(Tuple2<String, Integer> key, Context context, Iterable<Long> elements, Collector<PageBucketCount> out) { String pageId = key.f0; int bucketId = key.f1; Long count = elements.iterator().next(); out.collect(new PageBucketCount(pageId, bucketId, count, context.window().getEnd())); } } // ===== TopN处理(优化状态) ===== public static class TopNPages extends KeyedProcessFunction<Long, PageViewCount, String> { private final int topSize; private transient MapState<String, Long> pageViewState; private transient ValueState<Long> triggerTimeState; public TopNPages(int topSize) { this.topSize = topSize; } @Override public void open(Configuration parameters) { // 修复3:使用MapState优化状态访问 MapStateDescriptor<String, Long> stateDescriptor = new MapStateDescriptor<>("pageViewState", String.class, Long.class); pageViewState = getRuntimeContext().getMapState(stateDescriptor); ValueStateDescriptor<Long> timeDescriptor = new ValueStateDescriptor<>("triggerTime", Long.class); triggerTimeState = getRuntimeContext().getState(timeDescriptor); } @Override public void processElement(PageViewCount value, Context ctx, Collector<String> out) throws Exception { pageViewState.put(value.pageId, value.count); // 注册延迟触发定时器(避免重复注册) Long windowEnd = value.windowEnd; if (triggerTimeState.value() == null) { long triggerTime = windowEnd + 100; ctx.timerService().registerEventTimeTimer(triggerTime); triggerTimeState.update(triggerTime); } } @Override public void onTimer(long timestamp, OnTimerContext ctx, Collector<String> out) throws Exception { // 获取并排序TopN List<Map.Entry<String, Long>> allEntries = new ArrayList<>(); for (Iterator<Map.Entry<String, Long>> it = pageViewState.iterator(); it.hasNext(); ) { allEntries.add(it.next()); } allEntries.sort((e1, e2) -> Long.compare(e2.getValue(), e1.getValue())); StringBuilder result = new StringBuilder("===== Top10 Pages ("); result.append("窗口结束: ").append(timestamp - 100).append(") =====\n"); int count = Math.min(topSize, allEntries.size()); for (int i = 0; i < count; i++) { Map.Entry<String, Long> entry = allEntries.get(i); result.append(entry.getKey()).append(", ").append(entry.getValue()).append("\n"); } // 清理状态 pageViewState.clear(); triggerTimeState.clear(); out.collect(result.toString()); } } // ===== 数据结构 ===== public static class UserBehavior { public String userId; public String pageId; public Long timestamp; public UserBehavior(String userId, String pageId, Long timestamp) { this.userId = userId; this.pageId = pageId; this.timestamp = timestamp; } } public static class PageBucket { public String userId; public String pageId; public int bucketId; public Long timestamp; public PageBucket(String userId, String pageId, int bucketId, Long timestamp) { this.userId = userId; this.pageId = pageId; this.bucketId = bucketId; this.timestamp = timestamp; } } public static class PageBucketCount { public String pageId; public int bucketId; public Long count; public Long windowEnd; public PageBucketCount(String pageId, int bucketId, Long count, Long windowEnd) { this.pageId = pageId; this.bucketId = bucketId; this.count = count; this.windowEnd = windowEnd; } public String getPageId() { return pageId; } } public static class PageViewCount { public String pageId; public Long count; public Long windowEnd; public PageViewCount(String pageId, Long count, Long windowEnd) { this.pageId = pageId; this.count = count; this.windowEnd = windowEnd; } public Long getWindowEnd() { return windowEnd; } } } 报错如下: Exception in thread "main" org.apache.flink.api.common.functions.InvalidTypesException: The generic type parameters of 'Tuple2' are missing. In many cases lambda methods don't provide enough information for automatic type extraction when Java generics are involved. An easy workaround is to use an (anonymous) class instead that implements the 'org.apache.flink.api.java.functions.KeySelector' interface. Otherwise the type has to be specified explicitly using type information. 请给我修改后可以正常运行的代码
最新发布
07-03
<think>我们正在解决两个问题:1.FlinkTuple2keyBy操作的类型推断异常;2.实时UVTop10统计的实现。首先,我们分析类型推断异常的原因:在Flink中,使用Tuple2时,如果直接使用字段位置索引(如keyBy(0)),Java编译器可能会丢失泛型信息,导致类型推断失败。解决方案:-使用显式的TypeHint来指定Tuple2的泛型类型。-或者使用POJO类代替Tuple2,这样字段可以通过名称访问,避免类型擦除问题。-或者使用Flink的Field表达式(仅适用于Scala API,Java中不可用)。其次,我们实现实时UV Top10统计。UV(Unique Visitor)即独立访客数,我们需要统计每个页面的独立访客数,并实时输出当前UVTop10的页面。步骤:1.数据源:假设数据流中的每个元素是一个访问事件,包含页面ID和用户ID。2.去重:为了统计每个页面的UV,我们需要对同一个页面的同一个用户进行去重。可以使用窗口+布隆过滤器或者使用Flink的状态(如ValueState)来记录每个用户是否访问过该页面(但注意,如果使用状态,每个页面每个用户都要存,状态可能会很大)。另一种常见做法是使用HyperLogLog(HLL)进行近似去重统计。3.统计每个页面的UV:使用keyBy按页面分组,然后使用窗口(如滑动窗口或滚动窗口)来统计该窗口内的UV。注意,UV是独立访客数,所以我们需要对用户去重计数。4.全窗口排序:在每个窗口结束时,收集所有页面的UV计数,然后排序取Top10。考虑到性能,我们可以:-使用增量聚合:使用AggregateFunction结合WindowFunction(或ProcessWindowFunction)来增量聚合UV(使用HLL),然后在窗口结束时计算UV值,再排序取Top10。-或者,使用KeyedProcessFunction结合定时器,在窗口结束时触发排序。具体实现方案:方案1:使用窗口-数据源:DataStream<Tuple2<String, String>>或使用POJO:PageEvent(pageId,userId)-步骤:a.按页面分组:keyBy(pageId)b.开窗口:例如,使用1小时大小的滚动窗口,每小时统计一次。c.使用AggregateFunction进行增量聚合,内部使用HyperLogLog来去重计数(或者使用Flink内置的COUNTDISTINCT,但注意精确去重状态开销大,所以推荐HLL)。d.然后,将每个页面的UV值(即每个窗口的聚合结果)收集起来,再按窗口分组(即所有页面分到同一个组),然后排序取Top10。但是,注意:窗口函数只能处理同一个窗口内的数据。我们需要在窗口结束后,将窗口内所有页面的UV值收集起来,然后排序。因此,在窗口聚合后,我们还需要一个全窗口的排序操作。具体步骤:1.先按页面分组,开窗口,计算每个页面的UV(使用HLL).keyBy(event-> event.pageId).window(TumblingEventTimeWindows.of(Time.hours(1))).aggregate(new HyperLogLogAggregate(), newUVWindowFunction());//这里第一个参数是增量聚合,第二个是窗口函数,用于输出<pageId, uv>,注意窗口函数输出的是每个页面的UV2.然后,将每个页面的UV结果收集起来,按窗口分组(即所有页面UV结果属于同一个窗口,所以按窗口分组,即keyedBy windowEnd)。然后使用一个全局窗口,再使用ProcessWindowFunction来排序取Top10。3.第二步的代码:.keyBy(value-> value.windowEnd)//假设我们输出的UV结果中包含了窗口结束时间.window(GlobalWindows.create())//使用全局窗口,然后使用触发器在窗口结束时间触发.trigger(new UVTop10Trigger())//自定义触发器,当窗口结束时间到达时触发(实际上,每个窗口结束时间都是一个不同的全局窗口的key,所以我们需要在窗口结束时间到达时触发计算).process(new UVTop10ProcessFunction());但是,更简单的做法是:在第一步得到每个页面的UV结果后,我们使用一个KeyedProcessFunction(按窗口结束时间作为key),然后使用状态来收集该窗口内所有页面的UV,并设置定时器在窗口结束时间稍后一点触发排序。然而,Flink的窗口API已经提供了窗口结束时间,所以我们可以用窗口函数来处理。我们采用两步窗口:第一步:按页面分组,开窗口,计算每个页面的UV(使用HLL近似去重)第二步:将每个页面的UV结果(包含窗口结束时间、页面ID、UV值)收集起来,然后按窗口结束时间分组(这样同一个窗口的UV结果会分到同一个组),然后使用一个全局窗口,并自定义触发器(在窗口结束时间到达时触发)和ProcessWindowFunction,在ProcessWindowFunction中排序取Top10。注意:第二步中,我们使用全局窗口,但按窗口结束时间分组(keyBywindowEnd),然后使用触发器在事件时间到达窗口结束时间时触发(使用事件时间定时器)。但是,由于第一步的窗口是滚动窗口,每个窗口结束时间都是唯一的,所以我们可以这样处理。然而,更简单的方式是:在第一步的窗口函数中,我们输出一个结果流,每个元素是(窗口结束时间,页面ID,UV值)。然后,我们使用KeyedStream(按窗口结束时间)和ProcessFunction(非KeyedProcessFunction,因为KeyedProcessFunction是按key分区的,而我们希望同一个窗口结束时间的所有数据都在同一个分区处理)?不对,我们需要按窗口结束时间分区,所以使用keyBy(windowEnd)得到一个KeyedStream,然后使用.process(newKeyedProcessFunction<...>)。在KeyedProcessFunction中,我们可以使用状态来累积同一个窗口结束时间的各个页面的UV,然后设置一个定时器(在窗口结束时间+延迟时间,比如1ms)触发,然后在定时器触发时排序取Top10。这里我们采用KeyedProcessFunction的方式:第一步:计算每个页面的UV(使用窗口聚合)后,得到DataStream<PageUV>(包含字段:windowEnd,pageId, uv)第二步:keyBy(windowEnd),然后使用KeyedProcessFunction:-状态:使用ListState<PageUV>来存储该窗口结束时间对应的所有页面的UV数据。-定时器:在注册时,设置定时器为windowEnd +1(毫秒),这样确保在窗口结束时间到达后,所有数据都到齐(但实际中可能还有延迟,所以需要处理迟到数据?这里假设允许少量迟到,我们使用窗口的允许延迟机制,或者使用侧输出处理迟到数据,这里简化处理)。-在processElement方法中,将每个PageUV加入状态列表。-在onTimer方法中,从状态中取出所有PageUV,排序取Top10,然后输出。这样实现相对简单。下面我们给出代码示例(Java):步骤1:定义事件类(使用POJO避免类型推断问题)步骤2:按页面分组,开窗口,使用HLL统计UV(这里使用HLL需要添加依赖,如阿里巴巴的DataSketch HLL,或者使用Flink内置的布隆过滤器和自定义聚合函数?Flink没有内置HLL,需要自己实现或引入第三方库)由于Flink没有内置HLL,我们可以自己实现一个简单的HLL,但这里为了简便,我们使用精确去重(注意:精确去重状态开销大,仅适用于用户量不大的情况,如果用户量很大,建议使用HLL)。或者,我们使用Flink的StateTtlConfig来设置状态过期时间(窗口结束时间后状态可以清除)。这里我们先使用精确去重:使用AggregateFunction,内部使用一个Set来去重,但注意每个窗口都会有一个Set,如果用户量很大,内存占用会很高。因此,我们使用近似去重算法HLL。这里我们使用一个开源的HLL实现,比如使用`com.clearspring.analytics`的StreamLib中的HyperLogLog,或者使用`org.apache.datasketches`的HLLSketch。这里我们使用后者,因为更现代。添加依赖(在pom.xml): ```xml<dependency><groupId>org.apache.datasketches</groupId><artifactId>datasketches-java</artifactId><version>3.1.0</version> </dependency>```然后,我们实现一个AggregateFunction,使用HLLSketch来去重计数。步骤1:按页面分组,开窗口,聚合(增量聚合每个事件,更新HLL,然后窗口结束时得到UV值)步骤2:将聚合结果(窗口结束时间,页面ID,UV值)输出,然后按窗口结束时间分组,使用KeyedProcessFunction来收集并排序。下面我们分步实现。第一步:定义事件类(POJO)```javapublic classPageEvent{public StringpageId;public StringuserId;publiclong timestamp;publicPageEvent() {}publicPageEvent(String pageId,String userId, longtimestamp){this.pageId =pageId;this.userId =userId;this.timestamp= timestamp;}//为了在Flink中作为事件时间,需要指定事件时间字段public longgetTimestamp() {returntimestamp;}publicvoid setTimestamp(longtimestamp){this.timestamp =timestamp;}} ```第二步:构建流处理环境,设置事件时间和水印```javaDataStream<PageEvent>stream =env.addSource(...); DataStream<PageEvent> withTimestampsAndWatermarks =stream.assignTimestampsAndWatermarks(WatermarkStrategy.<PageEvent>forBoundedOutOfOrderness(Duration.ofSeconds(5)).withTimestampAssigner((event, timestamp) ->event.getTimestamp()));```第三步:按页面分组,开窗口,聚合(使用HLL) ```java//首先,我们定义AggregateFunction,使用HLLpublicclassHyperLogLogAggregate implementsAggregateFunction<PageEvent,HllSketch, Long> {@OverridepublicHllSketchcreateAccumulator() {returnnew HllSketch(12);//精度参数,12表示4%的误差}@OverridepublicHllSketchadd(PageEventvalue,HllSketchaccumulator){accumulator.update(value.userId);returnaccumulator;}@OverridepublicLong getResult(HllSketch accumulator) {return(long) Math.ceil(accumulator.getEstimate());}@Overridepublic HllSketch merge(HllSketcha,HllSketchb){a.merge(b);returna;}}```然后,我们使用窗口函数,注意这里我们使用ProcessWindowFunction来输出窗口信息(窗口结束时间和页面ID):但是,AggregateFunction只能输出UV值(Long),而我们需要页面ID和窗口结束时间。所以,我们使用AggregateFunctionProcessWindowFunction组合:```javaSingleOutputStreamOperator<PageUVResult> uvPerPage= withTimestampsAndWatermarks.keyBy(event-> event.pageId).window(TumblingEventTimeWindows.of(Time.hours(1))).aggregate(newHyperLogLogAggregate(),new ProcessWindowFunction<Long, PageUVResult, String, TimeWindow>(){@Overridepublic voidprocess(StringpageId,Context context, Iterable<Long>elements,Collector<PageUVResult>out){Long uv= elements.iterator().next();long windowEnd =context.window().getEnd();out.collect(newPageUVResult(pageId,uv,windowEnd));}});```其中,PageUVResult是一个POJO: ```javapublic classPageUVResult {publicString pageId;publiclonguv;publiclong windowEnd;publicPageUVResult(){}public PageUVResult(String pageId,long uv, longwindowEnd) {this.pageId= pageId;this.uv= uv;this.windowEnd= windowEnd;}@OverridepublicString toString() {return"PageUVResult{" +"pageId='" +pageId+ '\'' +",uv="+ uv+", windowEnd="+ windowEnd +'}';}} ```第四步:按窗口结束时间分组,然后使用KeyedProcessFunction排序取Top10```javaSingleOutputStreamOperator<String>top10Result =uvPerPage.keyBy(value-> value.windowEnd).process(newTopNPageUV(10)); //实现TopNPageUVpublicstatic classTopNPageUVextends KeyedProcessFunction<Long,PageUVResult,String>{private finalint n;privateListState<PageUVResult> pageUVState;public TopNPageUV(intn){this.n= n;}@Overridepublic voidopen(Configuration parameters) {ListStateDescriptor<PageUVResult> descriptor= newListStateDescriptor<>("page-uv-state", PageUVResult.class);pageUVState= getRuntimeContext().getListState(descriptor);}@Overridepublicvoid processElement(PageUVResult value, Contextctx,Collector<String> out) throwsException {//将每个页面的UV结果存入状态pageUVState.add(value);//注册一个定时器,在窗口结束时间+1毫秒时触发ctx.timerService().registerEventTimeTimer(value.windowEnd+1);}@Overridepublicvoid onTimer(longtimestamp,OnTimerContext ctx, Collector<String>out)throws Exception{List<PageUVResult>allPageUV =new ArrayList<>();for (PageUVResult result: pageUVState.get()){allPageUV.add(result);}//清除状态,释放资源pageUVState.clear();//按UV从大到小排序allPageUV.sort((o1, o2)-> (int)(o2.uv -o1.uv));//取TopNStringBuilderresult =new StringBuilder();result.append("窗口结束时间: ").append(new Timestamp(timestamp -1)).append("\n");for (int i=0;i <Math.min(n,allPageUV.size()); i++) {PageUVResultpage =allPageUV.get(i);result.append("No.").append(i+1).append(":").append("页面ID=").append(page.pageId).append("UV=").append(page.uv).append("\n");}out.collect(result.toString());}} ```注意:这里我们假设在窗口结束时间+1毫秒时,所有该窗口的数据都到齐了。实际中,可能会有迟到数据,我们可以通过设置窗口的允许延迟(使用allowedLateness)来处理,但这里我们简化处理。另外,关于类型推断异常:我们在代码中使用了POJO(PageEvent和PageUVResult),所以keyBy操作时不会出现Tuple2类型推断异常。如果使用Tuple2,则需要在keyBy时指定字段名称(如keyBy("f0"))或使用TypeHint。如果使用Tuple2,可以这样避免类型推断异常:```javaDataStream<Tuple2<String,String>>stream =...;//使用TypeHintstream.keyBy(value ->value.f0).window(...)...;```或者,在keyBy时指定字段位置,但需要添加returns方法指定类型:```javastream.keyBy(0).returns(Types.TUPLE(Types.STRING,Types.STRING))...```但更推荐使用POJO。总结:-使用POJO避免类型推断问题。-使用HLL进行近似去重统计UV。-使用窗口聚合计算每个页面的UV,然后按窗口结束时间分组,使用KeyedProcessFunction排序取Top10。相关问题:1.如何处理迟到数据?2.如果用户量非常大,使用精确去重(Set)会有什么问题?如何优化?3.HyperLogLog算法的原理是什么?在Flink中如何使用其他去重方法?4. Flink中窗口的类型有哪些?如何选择?5.如何测试Flink的窗口和状态相关的代码?</think>### Flink流式计算:解决Tuple2 keyBy类型推断异常实时UV Top10实现 #### 一、Tuple2 keyBy类型推断异常的解决方案 在Flink中使用`Tuple2`进行`keyBy`操作时,Java的类型擦除会导致类型推断失败。以下是三种解决方案: 1. **显式指定TypeHint(推荐)** ```java DataStream<Tuple2<String, Long>> stream = ...; stream.keyBy(value -> value.f0) .returns(new TypeHint<Tuple2<String, Long>>() {}) .window(TumblingProcessingTimeWindows.of(Time.minutes(10))); ``` 2. **使用POJO替代Tuple** ```java // 定义POJO类 public class PageView { public String pageId; public Long userId; // 必须有无参构造函数 public PageView() {} } DataStream<PageView> stream = ...; stream.keyBy("pageId") // 直接通过字段名访问 .window(...); ``` 3. **使用TupleField表达式(仅Scala API)** ```scala stream.keyBy("_1") // Scala通过元组位置访问 ``` > **根本原因**:Java编译器会擦除泛型类型信息,而Flink的类型系统需要明确知道`Tuple2<String, Long>`的具体类型才能正确序列化[^1]。 #### 二、实时UV Top10统计完整实现 使用**HyperLogLog**算法实现高效去重统计: ```java // 1. 定义数据模型 public class PageEvent { public String pageId; public Long userId; public Long timestamp; } // 2. 创建HLL聚合函数 public class HyperLogLogAgg extends AggregateFunction<Long, HllSketch> { @Override public HllSketch createAccumulator() { return new HllSketch(12); // 精度参数 } @Override public HllSketch add(PageEvent value, HllSketch acc) { acc.update(value.userId); return acc; } @Override public Long getResult(HllSketch acc) { return (long) acc.getEstimate(); } @Override public HllSketch merge(HllSketch a, HllSketch b) { a.merge(b); return a; } } // 3. 主处理流程 DataStream<PageEvent> input = env.addSource(kafkaSource) .assignTimestampsAndWatermarks(...); // 按页面分组计算UV DataStream<Tuple3<String, Long, Long>> uvStream = input .keyBy(event -> event.pageId) .window(TumblingEventTimeWindows.of(Time.minutes(10))) .aggregate(new HyperLogLogAgg(), (key, window, uv, out) -> out.collect(Tuple3.of(key, uv, window.getEnd())) ); // 全局Top10排序 uvStream.keyBy(value -> value.f2) // 按窗口结束时间分组 .process(new TopNProcess(10)) .print(); // 4. TopN处理函数 public static class TopNProcess extends KeyedProcessFunction<Long, Tuple3<String, Long, Long>, String> { private final int n; private transient ListState<Tuple3<String, Long, Long>> state; public TopNProcess(int n) { this.n = n; } @Override public void open(Configuration config) { state = getRuntimeContext().getListState( new ListStateDescriptor<>("uv-state", TypeInformation.of(new TypeHint<Tuple3<String, Long, Long>>() {})) ); } @Override public void processElement(Tuple3<String, Long, Long> value, Context ctx, Collector<String> out) { state.add(value); ctx.timerService().registerEventTimeTimer(value.f2 + 100); // 延迟100ms触发 } @Override public void onTimer(long timestamp, OnTimerContext ctx, Collector<String> out) throws Exception { List<Tuple3<String, Long, Long>> all = new ArrayList<>(); for (Tuple3<String, Long, Long> item : state.get()) { all.add(item); } state.clear(); // 排序取TopN all.sort((a, b) -> Long.compare(b.f1, a.f1)); StringBuilder result = new StringBuilder(); result.append("\n===== Top ").append(n).append(" Pages [") .append(new Date(timestamp - 100)).append("] =====\n"); for (int i = 0; i < Math.min(n, all.size()); i++) { result.append(i + 1).append(". ").append(all.get(i).f0) .append(": UV=").append(all.get(i).f1).append("\n"); } out.collect(result.toString()); } } ``` #### 三、关键优化点 1. **去重算法选择**: - HyperLogLog:误差率约1.5%,内存占用仅KB级 - 精确去重:使用`StateTtlConfig`设置状态过期时间 ```java StateTtlConfig ttlConfig = StateTtlConfig .newBuilder(Time.days(1)) .setUpdateType(StateTtlConfig.UpdateType.OnCreateAndWrite) .build(); ``` 2. **性能调优**: ```java env.setParallelism(4); env.enableCheckpointing(5000); // 5秒checkpoint env.getCheckpointConfig().setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE); ``` 3. **依赖配置**: ```xml <!-- pom.xml 添加HLL依赖 --> <dependency> <groupId>org.apache.datasketches</groupId> <artifactId>datasketches-java</artifactId> <version>3.1.0</version> </dependency> ``` #### 四、处理结果示例 ``` ===== Top 10 Pages [Thu Jan 01 10:00:00 CST 2023] ===== 1. /home: UV=24876 2. /product/123: UV=18762 3. /cart: UV=15243 ... ``` > **注意事项**:当用户规模超过1亿时,HyperLogLog的内存占用仅约1.5KB,而精确去重可能需要GB级内存[^2]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值