在工作中经常会遇到去重的场景,例如基于 App 的用户行为日志分析系统,用户的行为日志从手机客户端上报到 Nginx 服务端,通过 Logstash、Flume 或其他工具将日志从 Nginx 写入到 Kafka 中。
由于用户手机客户端的网络可能出现不稳定,所以手机客户端上传日志的策略是:宁可重复上报,也不能丢日志。所以导致 Kafka 中必然会出现日志重复的情况,即:同一条日志出现了 2 条或 2 条以上。
通常情况下,Flink 任务的数据源都是 Kafka,若 Kafka 中数据出现了重复,在实时 ETL 或者流计算时都需要考虑对日志主键进行去重,否则会导致流计算结果偏高或结果不准确的问题,例如用户 a 在某个页面只点击了一次,但由于日志重复上报,所以用户 a 在该页面的点击日志在 Kafka 中出现了 2 次,最后统计该页面的 click 数时,结果就会偏高。
这里只阐述了一种可能造成 Kafka 中数据重复的情况,在生产环境中很多情况都可能造成 Kafka 中数据重复,这里不一一列举,本节主要讲述出现了数据重复后,该如何处理。
实现去重的通用解决方案
Kafka 中数据出现重复后,各种解决方案都比较类似,一般需要一个全局 set 集合来维护历史所有数据的主键。当处理新日志时,需要拿到当前日志的主键与历史数据的 set 集合按照规则进行比较,若 set 集合中已经包含了当前日志的主键,说明当前日志在之前已经被处理过了,则当前日志应该被过滤掉,否则认为当前日志不应该被过滤应该被处理,而且处理完成后需要将新日志的主键加入到 set 集合中,set 集合永远存放着所有已经被处理过的数据。程序流程图如下图所示:
image
处理流程很简单,关键在于如何维护这个 set 集合,可以简单估算一下这个 set 集合需要占用多大空间。本小节要解决的问题是百亿数据去重,所以就按照每天 1 百亿的数据量来计算。
由于每天数据量巨大,因此主键占用空间通常会比较大,如果主键占用空间小意味着表示的数据范围就比较小,就可能导致主键冲突,例如:4 个字节的 int 类型表示数据范围是为 -2147483648~2147483647,总共可以表示 42 亿个数,如果这里每天百亿的数据量选用 int 类型做为主键的话,很明显会有大量的主键发生冲突,会将不重复的数据认为是发生了重复。
用户的行为日志是在手机客户端生成的,没有全局发号器,一般会选取 UUID 做为日志的主键,UUID 会生成 36 位的字符串,例如:"f106c4a1-4c6f-41c1-9d30-bbb2b271284a"。每个主键占用 36 字节,每天 1 百亿数据,36 字节 * 100亿 ≈ 360GB。这仅仅是一天的数据量,所以该 set 集合要想存储空间不发生持续地爆炸式增长,必须增加一个功能,那就是给所有的主键增加 ttl(Time To Live的缩写,即:过期时间)。
如果不增加 ttl,10 天数据量的主键占用空间就 3.6T,100 天数据量的主键占用空间 36T,所以在设计之初必须考虑为主键设定 ttl。如果要求按天进行去重或者认为日志发生重复上报的时间间隔不可能大于 24 小时,那么为了系统的可靠性 ttl 可以设置为 36 小时。每天数据量 1 百亿,且 set 集合中存放着 36 小时