news 2026/10/20 3:00:37

Flink实时广告点击量统计:黑名单过滤与窗口聚合实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Flink实时广告点击量统计:黑名单过滤与窗口聚合实战

1. 从一条广告点击日志说起:这个统计任务到底在算什么

广告点击量统计这件事,听起来像是数据平台里最基础的一环,但真正落到实时计算场景里,坑远比想象中多。我最早接触这个需求的时候,第一反应是“不就是按广告位分组计数吗”,结果上线第一天就被脏数据教做人——某个广告位在十分钟内被同一个设备点了两万多次,下游报表直接爆表。后来才明白,点击量统计的核心难点从来不是计数本身,而是如何在数据洪流里把无效点击、恶意点击、重复点击干净利落地剔除掉。

这篇内容要聊的,就是基于 Flink 做页面广告点击量统计,并且把黑名单过滤这条链路完整地串起来。所谓黑名单过滤,指的是在统计之前,先把那些已知的恶意用户、异常设备、刷量 IP 从数据流里筛掉,只让“干净”的点击进入计数逻辑。它解决的是数据质量问题,适合已经对 Flink 有基本了解、正在做实时指标统计、并且被脏数据困扰的开发者参考。如果你还在用批处理跑 T+1 的报表,或者实时任务里完全没有过滤逻辑,那这套思路值得你花时间看完。

我先把整体链路用一句话说清楚:广告点击事件从消息队列进来,先经过黑名单过滤算子,再按广告位维度做窗口聚合,最后把统计结果写到下游存储。听起来简单,但每一步都有讲究。比如黑名单数据从哪来、怎么动态更新、过滤算子放在哪个位置、窗口用滚动还是滑动、状态怎么管理、迟到数据怎么处理,这些细节决定了你的任务能不能稳定跑下去。

这里有个很多人一开始会忽略的点:黑名单过滤和点击量统计,本质上是一对矛盾体。过滤要求尽可能严格,宁可错杀不可放过;统计又要求尽可能完整,不能漏掉真实点击。怎么平衡?我的经验是分层处理——强黑名单直接丢弃,弱黑名单打标但不丢弃,让下游根据业务需要决定是否计入。这个思路后面会详细展开。

另外要说明的是,本文涉及的所有代码和配置都是基于常见实践整理的示例,实际落地时需要根据你的数据规模、集群资源和业务规则做调整。我不会给你一套“复制就能跑”的万能模板,因为实时计算这行,脱离具体场景谈方案就是耍流氓。但我会把每个决策背后的逻辑讲透,让你知道为什么这么选,以及不这么选会踩什么坑。

2. 黑名单过滤到底该放在哪一层:位置选择的三种方案对比

2.1 为什么过滤位置是个需要认真对待的问题

很多人拿到需求,第一反应是“在数据源后面加个 filter 算子不就完了”。这个想法没错,但太粗糙。过滤位置的选择,直接影响三件事:状态大小、计算开销、以及黑名单更新的实时性。我见过有团队把黑名单过滤放在窗口聚合之后,结果窗口里已经算进了脏数据,再过滤只能把整个窗口结果丢掉,等于白算。也见过把黑名单加载到每个算子里做本地缓存的,黑名单一更新就得重启任务,运维成本极高。

所以过滤位置不是随便放的,它取决于你的黑名单规模、更新频率、以及你对延迟的容忍度。下面我把常见的三种方案拆开讲,每种都给出适用场景和实测表现。

2.2 方案一:数据源接入后立即过滤

这是最直观的做法,在 Source 之后直接接一个ProcessFunction或者RichFilterFunction,每条数据进来先查黑名单,命中就丢弃,不命中就往下游发。

DataStream<AdClickEvent> filteredStream = sourceStream .filter(new RichFilterFunction<AdClickEvent>() { private transient ValueState<Boolean> blacklistState; @Override public void open(Configuration parameters) { // 初始化状态描述符,用于存储黑名单标记 ValueStateDescriptor<Boolean> descriptor = new ValueStateDescriptor<>("blacklist", Boolean.class); blacklistState = getRuntimeContext().getState(descriptor); } @Override public boolean filter(AdClickEvent event) throws Exception { // 查询外部黑名单或本地缓存 boolean isBlacklisted = BlacklistCache.contains(event.getUserId()); return !isBlacklisted; } });

这个方案的好处是脏数据最早被拦截,下游所有算子都不用为无效数据付出计算成本。如果你的黑名单规模不大(比如几万到几十万条),完全可以把它加载到每个 TaskManager 的堆内缓存里,查询走本地内存,延迟在微秒级。

但问题也很明显:黑名单更新不及时。本地缓存意味着每个并行子任务都持有一份副本,黑名单变更后需要广播到所有节点,Flink 里可以用广播状态(Broadcast State)来实现,但广播本身有延迟,而且黑名单越大,广播开销越高。我实测过一个百万级黑名单用广播状态更新,每次全量广播要几秒钟,期间新数据可能漏过过滤。

提示:如果黑名单更新频率低于每分钟一次,且规模在十万级以内,这个方案完全够用。但如果黑名单是实时变化的(比如风控系统每秒都在产出新的恶意用户),就得考虑下面的方案。

2.3 方案二:旁路查询外部存储

把黑名单放在外部存储(比如 Redis、HBase),过滤算子每条数据都去查一次。这样黑名单更新是实时的,风控系统写入后立刻生效。

public class BlacklistQueryFunction extends RichFlatMapFunction<AdClickEvent, AdClickEvent> { private transient Jedis jedis; @Override public void open(Configuration parameters) { // 初始化连接池,注意每个并行子任务独立连接 jedis = new Jedis("blacklist-host", 6379); } @Override public void flatMap(AdClickEvent event, Collector<AdClickEvent> out) { // 用 userId 作为 key 查询黑名单 Boolean exists = jedis.sismember("ad:blacklist:users", event.getUserId()); if (exists == null || !exists) { out.collect(event); } // 命中黑名单的直接丢弃,不往下游发 } }

这个方案的优点是黑名单实时性最好,且不受本地内存限制,理论上可以支撑千万级甚至更大的黑名单。但代价是每条数据都要走一次网络 IO,如果 QPS 很高,Redis 很容易成为瓶颈。我压测过一个场景,单 Redis 实例在 5 万 QPS 下响应时间开始明显抖动,超过 8 万 QPS 就出现超时。

优化思路有两个:一是加本地缓存做多级查询,热点黑名单放本地,冷门数据走 Redis;二是用异步 IO(Flink 的AsyncDataStream)来避免同步等待阻塞算子线程。异步 IO 的写法这里不展开,核心是把查询请求批量发出去,用回调处理结果,能显著提升吞吐。

2.4 方案三:广播状态动态更新

这是 Flink 官方推荐的动态规则匹配方案。把黑名单作为广播流,主数据流和广播流连接(connect),在KeyedBroadcastProcessFunction里用广播状态存储黑名单,每条数据进来查状态即可。

// 黑名单广播流 BroadcastStream<String> blacklistBroadcast = blacklistStream .broadcast(blacklistStateDescriptor); // 主数据流连接广播流 DataStream<AdClickEvent> filtered = clickStream .connect(blacklistBroadcast) .process(new BlacklistFilterFunction()); // 过滤函数核心逻辑 public class BlacklistFilterFunction extends KeyedBroadcastProcessFunction<String, AdClickEvent, String, AdClickEvent> { @Override public void processElement(AdClickEvent event, ReadOnlyContext ctx, Collector<AdClickEvent> out) throws Exception { // 从广播状态读取黑名单 ReadOnlyBroadcastState<String, Boolean> state = ctx.getBroadcastState(blacklistStateDescriptor); if (state.get(event.getUserId()) == null) { out.collect(event); } } @Override public void processBroadcastElement(String userId, Context ctx, Collector<AdClickEvent> out) throws Exception { // 更新广播状态 BroadcastState<String, Boolean> state = ctx.getBroadcastState(blacklistStateDescriptor); state.put(userId, true); } }

这个方案兼顾了实时性和性能:黑名单更新通过广播流实时同步,查询走本地状态,没有网络开销。但要注意,广播状态是存在每个并行子任务的内存里的,黑名单太大会撑爆内存。我的经验是广播状态适合百万级以内的黑名单,再大就得考虑分片或者换方案。

三种方案的对比我整理成表格,方便你按场景选:

方案黑名单规模更新实时性查询延迟运维复杂度适用场景
本地缓存过滤十万级以内分钟级微秒级低黑名单稳定、QPS 高
旁路查询外部存储千万级秒级毫秒级中黑名单频繁更新
广播状态百万级以内秒级微秒级中高实时性与性能兼顾

2.5 我的实际选择与理由

在最近一个广告点击统计项目里,我最终用的是广播状态 + 本地缓存兜底的混合方案。具体来说,风控系统产出的黑名单通过广播流实时同步到过滤算子,同时算子内部维护一个 LRU 本地缓存,缓存最近命中的黑名单用户。这样即使广播有短暂延迟,本地缓存也能挡住大部分重复的恶意点击。

为什么不选旁路查询?因为那个项目的点击 QPS 峰值到了 12 万,Redis 扛不住。为什么不选纯本地缓存?因为风控要求黑名单变更后 5 秒内生效,本地缓存的广播延迟满足不了。混合方案是在性能和实时性之间找到的平衡点,实测下来过滤延迟稳定在 2 秒以内,CPU 开销比纯旁路查询低了 60%。

3. 点击量统计的窗口设计:滚动、滑动还是会话窗口

3.1 窗口类型的选择逻辑

过滤完之后,干净的数据要进入窗口做聚合。Flink 提供了三种常用窗口:滚动窗口(Tumbling)、滑动窗口(Sliding)、会话窗口(Session)。选哪种,取决于你的报表需求。

滚动窗口是固定长度、不重叠的窗口,比如每 5 分钟统计一次点击量。它的优点是实现简单、状态可控,每个事件只属于一个窗口。缺点是统计结果是“跳变”的,5 分钟整点才输出一次,中间没有更新。

滑动窗口是固定长度、有重叠的窗口,比如窗口长度 10 分钟、滑动步长 1 分钟。它每 1 分钟输出一次过去 10 分钟的统计结果,曲线更平滑。但代价是每个事件会属于多个窗口,状态和计算量成倍增加。我实测过,滑动步长是窗口长度的 1/10 时,状态大小大约是滚动窗口的 10 倍。

会话窗口是按活动间隙切分的,比如用户停止点击 30 秒后认为会话结束。它适合分析用户行为序列,但不太适合广告点击量这种固定周期的统计需求。

我的建议很直接:广告点击量统计优先用滚动窗口。因为报表通常就是按固定周期出数的,滚动窗口的状态管理最简单,出问题时排查也容易。如果业务方要求“实时曲线”,可以用滑动窗口,但一定要控制好步长和窗口长度的比例,别搞出 1 秒步长、1 小时窗口这种配置,状态会爆炸。

3.2 窗口聚合的代码实现与增量计算

滚动窗口的聚合代码大概长这样:

DataStream<AdClickCount> windowedCount = filteredStream .keyBy(AdClickEvent::getAdSlotId) .window(TumblingEventTimeWindows.of(Time.minutes(5))) .aggregate(new ClickCountAggregator(), new ClickCountWindowFunction());

这里有个关键点:用aggregate而不是apply。aggregate是增量聚合,每来一条数据就更新累加器,窗口触发时只输出最终结果,状态里只存累加器(一个 long 值)。apply是全量聚合,窗口触发时把所有数据拉出来遍历,状态里存的是所有原始事件。两者在状态大小和性能上差距巨大,数据量大时apply基本不可用。

// 增量聚合函数:每来一条数据就累加 public class ClickCountAggregator implements AggregateFunction<AdClickEvent, Long, Long> { @Override public Long createAccumulator() { return 0L; } @Override public Long add(AdClickEvent event, Long accumulator) { return accumulator + 1; } @Override public Long getResult(Long accumulator) { return accumulator; } @Override public Long merge(Long a, Long b) { return a + b; } }

这个累加器只占 8 字节,即使有几十万个广告位,状态也就几 MB。如果用apply,每个窗口要缓存所有事件,内存占用是数量级的差距。

3.3 事件时间与水位线的处理

广告点击统计必须用事件时间,不能用处理时间。因为数据从产生到进入 Flink 有延迟,用处理时间会导致统计结果和实际发生时间对不上。事件时间需要配合水位线(Watermark)使用,水位线告诉 Flink “某个时间点之前的数据都到齐了”。

DataStream<AdClickEvent> withWatermark = stream .assignTimestampsAndWatermarks( WatermarkStrategy.<AdClickEvent>forBoundedOutOfOrderness(Duration.ofSeconds(5)) .withTimestampAssigner((event, timestamp) -> event.getClickTime()) );

forBoundedOutOfOrderness表示允许数据最多迟到 5 秒。这个值怎么定?我的经验是看数据源的乱序程度。如果消息队列本身有序,1 秒就够;如果经过多个中间环节,可能需要 10 秒甚至更多。设太小会丢迟到数据,设太大会增加窗口触发延迟。

注意:水位线是全局推进的,如果某个并行子任务没有数据,它的水位线不会前进,会拖慢整个任务。解决办法是设置withIdleness,让空闲子任务的水位线自动推进。

3.4 迟到数据的三种处理策略

即使设了水位线,还是会有迟到数据。Flink 提供了三种处理方式:

  • 默认丢弃:窗口触发后到达的数据直接扔掉。适合对完整性要求不高的场景。
  • allowedLateness:允许窗口触发后再保留一段时间,迟到数据还能触发更新。比如设 1 分钟,窗口触发后 1 分钟内的迟到数据会重新计算并输出更新结果。
  • sideOutput:把迟到数据输出到侧流,单独处理。适合需要记录迟到数据做分析的场景。
OutputTag<AdClickEvent> lateTag = new OutputTag<AdClickEvent>("late-data"){}; SingleOutputStreamOperator<AdClickCount> result = filteredStream .keyBy(AdClickEvent::getAdSlotId) .window(TumblingEventTimeWindows.of(Time.minutes(5))) .allowedLateness(Time.minutes(1)) .sideOutputLateData(lateTag) .aggregate(new ClickCountAggregator(), new ClickCountWindowFunction()); DataStream<AdClickEvent> lateStream = result.getSideOutput(lateTag);

我一般会同时开allowedLateness和sideOutput:前者保证统计结果尽量准确,后者用来监控迟到数据的量。如果侧流数据量突然增大,说明数据源出了问题,需要排查。

4. 状态管理与容错:让任务能稳定跑上几个月

4.1 状态后端的选择

Flink 的状态后端决定了状态存在哪、怎么存。常见的有三种:HashMapStateBackend(内存)、EmbeddedRocksDBStateBackend(RocksDB)、以及基于文件系统的状态后端。

对于广告点击统计这种场景,状态大小通常在几百 MB 到几 GB 之间。如果状态能放进内存,用HashMapStateBackend性能最好,读写都是内存操作。但如果状态可能超过内存,就得用RocksDBStateBackend,它把状态存在本地磁盘,支持超出内存的状态量,代价是读写有磁盘 IO 开销。

// 配置 RocksDB 状态后端 Configuration config = new Configuration(); config.setString("state.backend", "rocksdb"); config.setString("state.backend.rocksdb.localdir", "/data/flink/rocksdb"); config.setString("state.checkpoints.dir", "hdfs:///flink/checkpoints");

我的经验是:如果广告位数量在十万级以内,用内存状态后端;超过十万,或者窗口状态很大,用 RocksDB。另外 RocksDB 的本地目录一定要配到 SSD 上,机械硬盘的随机读写会让性能下降一个数量级。

4.2 Checkpoint 配置与调优

Checkpoint 是 Flink 容错的核心机制,它定期把状态快照写到持久化存储。配置不当会导致 Checkpoint 超时、失败,甚至影响正常数据处理。

StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); env.enableCheckpointing(60000); // 每 60 秒一次 env.getCheckpointConfig().setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE); env.getCheckpointConfig().setMinPauseBetweenCheckpoints(30000); // 两次间隔至少 30 秒 env.getCheckpointConfig().setCheckpointTimeout(120000); // 超时 2 分钟 env.getCheckpointConfig().setMaxConcurrentCheckpoints(1); // 不并发 env.getCheckpointConfig().setTolerableCheckpointFailureNumber(3); // 容忍 3 次失败

几个关键参数的解释:

  • Checkpoint 间隔:太短会增加开销,太长会导致故障恢复时重放数据过多。我一般设 1 到 5 分钟,看数据量和恢复时间要求。
  • 最小间隔:防止前一个 Checkpoint 还没完,后一个就开始了。设成间隔的一半左右比较稳妥。
  • 超时时间:如果状态很大,写 HDFS 可能超过默认的 10 分钟,需要调大。
  • 容忍失败次数:偶尔失败不用让整个任务挂掉,设个容忍值更稳。

提示:Checkpoint 失败最常见的原因是状态太大导致写入超时。如果频繁失败,先看状态大小,再考虑调大超时或优化状态结构。

4.3 重启策略与故障恢复

任务挂了怎么恢复?Flink 提供了多种重启策略:

// 固定延迟重启:最多重启 5 次,每次间隔 10 秒 env.setRestartStrategy(RestartStrategies.fixedDelayRestart( 5, Time.seconds(10) )); // 指数延迟重启:间隔逐渐增大,适合避免频繁重启打爆集群 env.setRestartStrategy(RestartStrategies.exponentialDelayRestart( Time.seconds(1), Time.minutes(5), 2.0, Time.hours(1), 10 ));

我一般用指数延迟重启,因为如果任务是因为代码 bug 挂的,固定间隔重启会反复失败,指数延迟能给运维争取排查时间。另外要配合告警,任务重启次数超过阈值就通知人介入。

4.4 大状态下的性能优化

状态大了之后,性能问题会逐渐暴露。我踩过的几个坑:

第一个坑是状态序列化开销。Flink 默认用 Java 序列化,效率低。换成 Kryo 或者自定义序列化器能提升不少。对于简单的 POJO,实现Serializable接口并注册到 Kryo 就行。

第二个坑是 RocksDB 的写放大。RocksDB 的 LSM 树结构在写入量大时会有明显的写放大,导致磁盘 IO 飙升。解决办法是调大write_buffer_size和max_write_buffer_number,让更多数据在内存里合并后再落盘。

第三个坑是 Checkpoint 期间的背压。Checkpoint 时状态要快照,如果状态太大,快照期间数据处理会变慢,产生背压。可以开启增量 Checkpoint(RocksDB 支持),只上传变化的部分,大幅减少快照数据量。

// 开启 RocksDB 增量 Checkpoint env.getCheckpointConfig().enableExternalizedCheckpoints( CheckpointConfig.ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION ); config.setString("state.backend.rocksdb.incremental", "true");

5. 黑名单过滤与统计链路的联调实测

5.1 测试数据的构造

联调阶段最麻烦的是造数据。真实广告点击数据不好拿,我一般用脚本生成模拟数据,关键是要覆盖各种边界情况:

  • 正常点击:随机用户、随机广告位、时间戳递增
  • 黑名单用户点击:用已知的黑名单 ID 构造
  • 重复点击:同一用户对同一广告位短时间多次点击
  • 迟到数据:时间戳明显早于当前水位线的数据
  • 乱序数据:时间戳不按顺序到达
import random import time import json def generate_click_event(): user_id = f"user_{random.randint(1, 100000)}" ad_slot_id = f"slot_{random.randint(1, 1000)}" click_time = int(time.time() * 1000) - random.randint(0, 10000) return json.dumps({ "userId": user_id, "adSlotId": ad_slot_id, "clickTime": click_time }) # 生成 10 万条测试数据 with open("test_clicks.json", "w") as f: for _ in range(100000): f.write(generate_click_event() + "\n")

5.2 过滤效果的验证方法

怎么确认黑名单过滤真的生效了?我的做法是在过滤算子前后各埋一个计数器,统计进入和流出的数据量,差值就是被过滤掉的数量。

public class BlacklistFilterFunction extends RichFilterFunction<AdClickEvent> { private transient LongCounter inputCounter; private transient LongCounter filteredCounter; @Override public void open(Configuration parameters) { inputCounter = getRuntimeContext().getLongCounter("input_count"); filteredCounter = getRuntimeContext().getLongCounter("filtered_count"); } @Override public boolean filter(AdClickEvent event) throws Exception { inputCounter.add(1); boolean isBlacklisted = BlacklistCache.contains(event.getUserId()); if (isBlacklisted) { filteredCounter.add(1); } return !isBlacklisted; } }

然后在 Flink Web UI 的 Metrics 页面看这两个计数器的值。如果filtered_count一直是 0,要么是黑名单没加载成功,要么是测试数据里没有黑名单用户。我遇到过黑名单广播流因为序列化问题一直没数据的情况,就是靠这个计数器发现的。

5.3 统计结果的核对

窗口输出的统计结果,要和预期值核对。最简单的办法是用批处理跑一遍同样的数据,对比结果。如果实时和批处理的差值在允许范围内(考虑迟到数据),说明统计逻辑没问题。

另一个办法是抽样核对:从原始数据里随机抽几个广告位,手动数一下点击量,和 Flink 输出的结果对比。这个方法适合数据量不大的场景,能发现一些聚合逻辑的细节问题。

我踩过的一个坑是窗口边界问题:滚动窗口是左闭右开的,比如[00:00, 00:05),00:05:00 整点的数据会落到下一个窗口。如果业务方理解成闭区间,对账时就会差一条。这种问题一定要在联调阶段和业务方确认清楚。

5.4 压力测试与性能瓶颈定位

上线前必须做压力测试。我用的是自己写的压测脚本,往消息队列里灌数据,逐步加大 QPS,观察 Flink 任务的各项指标:

指标正常范围异常表现可能原因
吞吐量随 QPS 线性增长增长停滞算子成为瓶颈
背压0 或低持续高背压下游处理慢
Checkpoint 时长稳定逐渐变长状态膨胀
CPU 使用率60% 以下持续 90%+计算密集或 GC
GC 时间低频繁 Full GC内存不足或泄漏

背压是最直观的指标。Flink Web UI 里每个算子都有背压指示,红色表示背压严重。如果过滤算子背压高,说明黑名单查询慢;如果窗口算子背压高,说明聚合逻辑重或者状态大。

我遇到过一次窗口算子背压,排查后发现是allowedLateness设得太长(10 分钟),导致窗口状态一直不释放。改成 1 分钟后背压立刻消失。这个参数不是越大越好,要根据数据迟到情况合理设置。

6. 几个容易翻车的细节和我的应对经验

6.1 黑名单数据的格式一致性

黑名单从风控系统同步过来,格式一定要统一。我遇到过风控系统有时候发 JSON、有时候发纯文本的情况,导致广播流解析失败,黑名单更新中断。解决办法是在广播流入口加一层格式校验,解析失败的数据写到侧流告警,不让它影响主流程。

DataStream<String> validatedBlacklist = rawBlacklist .process(new ProcessFunction<String, String>() { @Override public void processElement(String value, Context ctx, Collector<String> out) { try { // 尝试解析,验证格式 JSON.parseObject(value); out.collect(value); } catch (Exception e) { // 格式错误,输出到侧流 ctx.output(new OutputTag<String>("invalid-blacklist"){}, value); } } });

6.2 黑名单删除的同步问题

黑名单不只有新增,还有删除(比如误判后解封)。如果广播流只发新增,删除操作就同步不过去。我的做法是广播流里带上操作类型,ADD表示加入黑名单,REMOVE表示移出。

@Override public void processBroadcastElement(BlacklistOp op, Context ctx, Collector<AdClickEvent> out) throws Exception { BroadcastState<String, Boolean> state = ctx.getBroadcastState(descriptor); if ("ADD".equals(op.getType())) { state.put(op.getUserId(), true); } else if ("REMOVE".equals(op.getType())) { state.remove(op.getUserId()); } }

这个细节很容易被忽略,上线后才发现解封的用户还是被过滤,业务方投诉才回头改。

6.3 窗口结果输出的幂等性

统计结果写到下游存储时,要考虑幂等性。因为 Checkpoint 恢复后,窗口可能重新触发,同一个窗口的结果会输出两次。如果下游是数据库,用INSERT ... ON DUPLICATE KEY UPDATE或者先删后插来保证幂等。如果下游是消息队列,消费端要做去重。

我一般会在结果里带上窗口的起止时间,下游按广告位 + 窗口起始时间做主键,重复写入直接覆盖。

6.4 监控告警的配置

任务上线不是终点,监控才是。我必配的几个告警:

  • Checkpoint 连续失败:超过 3 次就告警,可能是状态太大或存储有问题。
  • 背压持续过高:某个算子背压超过 5 分钟,说明处理能力不足。
  • 黑名单过滤量为 0:如果一段时间内过滤量突然归零,可能是黑名单同步断了。
  • 任务重启:非预期重启要立即通知,排查是资源问题还是代码 bug。

监控指标可以从 Flink 的 Metrics 系统导出到 Prometheus,再用 Grafana 做面板。这套组合用起来很顺手,社区里资料也多。

6.5 资源规划的保守原则

最后说资源规划。我见过太多任务因为资源给少了,上线后频繁背压、Checkpoint 失败。我的原则是按峰值 QPS 的 1.5 倍规划资源,并且留出至少 30% 的内存余量给状态和 GC。

具体来说,如果峰值 QPS 是 10 万,单并行度能处理 1 万 QPS,那至少开 15 个并行度。内存方面,如果状态预估 2 GB,TaskManager 内存至少给 4 GB,留一半给堆外和 GC。这个配置不是拍脑袋,是压测出来的经验值。资源给多了浪费,给少了出事,宁可稍微宽裕一点。

广告点击量统计这个任务,技术栈不复杂,但细节特别多。黑名单过滤、窗口聚合、状态管理、容错恢复,每一环都有坑。我把这几年的经验整理出来,希望能帮你少走点弯路。实际落地时,建议先在测试环境把链路跑通,用模拟数据验证过滤和统计逻辑,再做压力测试确定资源配置,最后小流量上线观察一段时间。稳扎稳打,比一步到位靠谱得多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 3:00:37

C++ 构造函数和析构函数示例详解

前言 构造函数和析构函数是 C 对象生命周期的两个端点。构造函数负责把一片原始内存变成有效对象&#xff0c;析构函数负责在对象消失前释放它持有的资源。理解这两端&#xff0c;才能理解 RAII 为什么是 C 资源管理的基石。 常见误解有几个。第一&#xff0c;以为构造函数「分…

作者头像 李华
网站建设 2026/10/20 2:59:25

Windows 11 系统优化:卡顿诊断与四大目标治疗的完整指南

Windows 11 系统优化&#xff1a;卡顿诊断与四大目标治疗的完整指南 【免费下载链接】windows11 &#x1f30e; Windows 11 Settings, Tweaks, Scripts 项目地址: https://gitcode.com/GitHub_Trending/wi/windows11 开机要等一分多钟、空闲时硬盘指示灯常亮、更新在不打…

作者头像 李华
网站建设 2026/10/20 2:59:00

AI辅助PLA打印:从切片优化到异常检测的全流程实战

PLA是目前玩桌面级3D打印绕不开的材料&#xff0c;价格便宜、颜色多、打印温度低&#xff0c;但真要说它省心&#xff0c;老玩家第一个不同意。翘边、拉丝、堵头、表面纹理不稳定&#xff0c;这些“小毛病”看着不起眼&#xff0c;却能让一次本应8小时完成的打印在最后一层翻车…

作者头像 李华
网站建设 2026/10/20 2:58:26

IS-IS缺省路由原理与配置:从ATT位到防环排查

IS-IS系列写到第十八篇&#xff0c;前十七篇把邻居建立、LSP同步、区域划分、路由计算这些底子都打完了&#xff0c;今天这篇补一个很重要但容易被忽视的环节——缺省路由。在真实的运营商骨干网和大型数据中心里&#xff0c;缺省路由不夸张地说是全网流量的“最后一根救命稻草…

作者头像 李华
网站建设 2026/10/20 2:58:09

网络性能四大核心指标:带宽、时延、抖动与丢包详解

1. 从一次视频会议卡顿说起&#xff0c;为什么要关注这四个指标我印象很深的一次经历&#xff0c;是某天下午开一个重要的远程会议&#xff0c;画面突然开始模糊&#xff0c;声音断断续续&#xff0c;讲的人一句话要重复两三遍&#xff0c;听众那边拼命打“听不清”。当时第一反…

作者头像 李华
网站建设 2026/10/20 2:58:04

Hive面试高频真题全解析:原理、SQL优化与数据倾斜实战

又到了招聘季&#xff0c;后台很多同学在准备大数据开发面试&#xff0c;问得最多的就是Hive。说实话&#xff0c;Hive在数仓岗位里就是基本功&#xff0c;基本每一轮技术面都跑不掉。我翻了翻最近几年美团、京东这类大厂的面经&#xff0c;发现Hive的考察重点其实很集中&#…

作者头像 李华