MapReduce:分而治之的计算基石
在处理海量日志数据时,比如电商平台每天产生的用户点击行为,MapReduce 是最基础的算法模型。它的核心思想是把大任务拆成小块,分别处理后再合并结果。实际运行中常遇到任务卡在 99% 的情况,多数是因为“数据倾斜”——个别 reduce 任务负载过高。这时候得检查 key 的分布,比如某个用户 ID 出现频率远高于其他,导致对应 reduce 处理不过来。
可以通过在 map 阶段加入随机前缀打散热点 key,处理完再归并:
String newKey = originalKey + "_" + RandomStringUtils.randomNumeric(2);等数据均匀分布后,再通过二次 reduce 去掉前缀汇总结果。
Spark 中的内存溢出问题
用 Spark 做实时推荐时,经常加载用户画像数据到内存做 join。但程序跑着跑着就报 java.lang.OutOfMemoryError: Java heap space。这通常不是算法本身的问题,而是使用方式不当。
比如用了 broadcast join 却广播了一个超大表,节点内存撑不住。应该先判断表大小,超过 100MB 就别广播。改用 bucketed join 或者调整分区数让 shuffle 更均衡。
spark.sql("SET spark.sql.autoBroadcastJoinThreshold=50MB")Bloom Filter:快速判断数据是否存在
在过滤已处理过的订单记录时,直接查数据库太慢。Bloom Filter 可以用很小的空间判断一个元素“一定不存在”或“可能存在”。但配置失误会导致误判率飙升。
比如预计插入 100 万条数据,但只分配了 10MB 空间,哈希冲突变多,原本没处理的订单也被判定为已处理。正确做法是根据预期数据量和可接受误判率计算 bit 数组长度:
int numBits = (int) (-expectedInsertions * Math.log(fpp) / (Math.log(2) * Math.log(2)));
int numHashFunctions = Math.max(1, (int) Math.round((double) numBits / expectedInsertions * Math.log(2)));
// fpp: 允许的误判率,如 0.01
// expectedInsertions: 预计插入数量流处理中的窗口累积异常
Kafka + Flink 做实时统计时,发现每小时的 UV 数据总是偏高。排查发现是事件时间乱序严重,加上窗口允许迟到数据,导致同一条记录被多个窗口重复计算。
解决方案是在 Flink 中启用去重状态,用 keyBy(userId) 后记录每个用户的最新事件时间,丢弃过期数据:
dataStream
.keyBy(event -> event.getUserId())
.process(new DeduplicationProcessFunction());
// 在 processElement 中判断 eventTime 是否早于已记录的最大时间同时调整 watermark 生成策略,避免过激推进时间。
LSH 用于相似用户聚类
想找出行为相似的用户群做精准营销,直接两两比较计算余弦相似度复杂度太高。局部敏感哈希(LSH)能把相似向量大概率映射到同一个桶里。
但上线后发现聚类效果差,可能是因为哈希函数设计不合理。比如用 MinHash 计算 Jaccard 相似度时,hash 函数数量太少,导致分桶粗糙。一般建议至少用 128 个 hash 函数,并通过验证集调参。