推荐算法Chapter1.1 数据预处理与特征工程


1.用户行为日志字段

在推荐系统中,数据决定了模型效果的上限。用户行为日志(User Behavior Logs)不仅是训练数据的来源,更是理解用户意图的唯一窗口。


1. 核心字段总览表

字段类别 关键字段 含义 面试深度视角 (Deep Dive)
主体标识 User_ID 唯一标识用户 ID 稀疏性:高维稀疏特征,通常通过 Embedding 映射到低维空间。
客体标识 Item_ID 唯一标识物品 冷启动问题:对于新 Item_ID,需降级到使用 Category/Tag 等属性特征。
行为类型 Action_Type 点击、购买、点赞等 多目标建模:不同行为代表不同强度的意图(点击是兴趣,购买是决策)。
时间维度 Timestamp 行为发生的精确时间 兴趣衰减:用户 1 分钟前的行为比 1 年前的重要得多。
环境上下文 Context 设备、地理位置、网络 场景化差异:Wi-Fi 下用户倾向看长视频,4G 下倾向看图文。
附加属性 Attributes 播放时长、评分、金额 负反馈挖掘:播放时长极短的“点击”通常被视为强负反馈(误点)。

2. 知识点深度挖掘

A. 行为权重的数学处理 (Action Weighting)

在计算用户偏好得分时,不能简单地加总行为。通常会根据业务逻辑赋予权重。

公式示例:用户的综合兴趣得分 $S$ 可以表示为不同行为的加权和:

其中 $w_i$ 是行为权重(如:购买=5.0, 点击=1.0),$f(t_i)$ 是随时间衰减的函数。

B. 时序模式与行为序列 (Behavior Sequence)

现在的 SOTA 模型(如 DIN, BST)不再把用户行为看作独立的点,而是一串序列

  • 用途:通过 Timestamp 排序,提取用户最近 50/100 个点击记录。
  • 面试加分:提到 Target Attention 机制。即利用当前候选物品(Candidate Item)去与历史行为序列做 Attention,找出历史中真正与当前相关的兴趣点。

C. 隐藏的字段:位置偏置 (Position Bias)

日志中通常还包含一个未被提起的关键字段:Position(物品在页面上的排名)。

  • 知识点:用户点击第一名的物品,可能仅仅是因为它排在第一,而不是因为最喜欢。
  • 工程对策:在离线训练时将 Position 作为特征输入,在线推理时将其设为默认值(如 0),以此消除位置偏差。

3. 面试官连环炮 (Predictive Q&A)

Q1: 如果日志中 User_ID 缺失(游客模式),推荐系统如何工作?

  • 回答
    1. 利用 Context(上下文) 特征:如地理位置、热门趋势。
    2. 利用 Session-based 推荐:根据该用户在当前会话(Session)内的前几次点击,利用 RNN 或 Transformer 预测下一次行为。
    3. 利用 Device_ID:在合规前提下,通过设备指纹进行跨会话追踪。

Q2: 为什么“曝光未点击”的数据在日志中比“点击”的数据更多,且更难处理?

  • 回答
    • 数据倾斜:正负样本极度不平衡(1:100 甚至更高)。
    • 真假负样本:曝光未点击不代表不喜欢,可能只是用户没看到。处理时通常会采用 负采样(Negative Sampling) 策略,或在 Loss 函数中对正负样本进行加权平衡。

Q3: 播放时长 (Duration) 这个字段在短视频推荐里怎么用?

  • 回答
    • 不能直接用绝对时长,因为视频长度不同。
    • 通常使用 完播率 (Completion Rate)时长分位数
    • 深度应用:将时长作为回归目标(Regression),与点击率(CTR)进行多任务融合(Multi-task Learning),解决“标题党”问题。

4. 避坑指南:特征穿越 (Feature Leakage)

在处理行为日志时,最严重的工程错误是使用了未来的数据预测过去

  • 反思:在构建特征时,必须确保特征的生成时间戳早于预测行为的时间戳。
  • 实践:严格执行 Point-in-time Join(时点关联)。
计算带时间衰减的用户点击特征
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
from pyspark.sql import SparkSession
from pyspark.sql import functions as F
from pyspark.sql.window import Window
import math

# 1. 初始化 Spark
spark = SparkSession.builder.appName("TimeDecayFeatures").getOrCreate()

# 2. 模拟原始日志数据 (用户ID, 行为类型, 时间戳)
data = [
("user_A", "click", "2023-10-01 10:00:00"),
("user_A", "click", "2023-10-05 10:00:00"),
("user_A", "click", "2023-10-07 09:00:00"), # 最近的一次
("user_B", "click", "2023-09-01 10:00:00"), # 很久以前的一次
("user_B", "click", "2023-10-07 08:00:00"),
]

df = spark.createDataFrame(data, ["user_id", "action", "timestamp"]) \
.withColumn("timestamp", F.to_timestamp("timestamp"))

# 3. 设置超参数
# 假设当前时间是 2023-10-07 10:00:00
current_time = "2023-10-07 10:00:00"
# 衰减因子 lambda: 决定衰减速度。
# 例如:设置半衰期为 3 天,则 lambda = ln(2) / 3 ≈ 0.231
decay_lambda = math.log(2) / 3

# 4. 计算时间衰减得分
# 公式: Score = exp(-lambda * delta_t)
# 其中 delta_t 是当前时间与行为时间的差值(单位:天)
decay_df = df.withColumn("current_ts", F.to_timestamp(F.lit(current_time))) \
.withColumn("delta_t",
(F.unix_timestamp("current_ts") - F.unix_timestamp("timestamp")) / (3600 * 24) # 换算成天
) \
.withColumn("decay_score", F.exp(F.lit(-decay_lambda) * F.col("delta_t")))

# 5. 按用户聚合,得到最终的活跃度特征
user_activity_features = decay_df.groupBy("user_id").agg(
F.count("action").alias("total_click_count"), # 原始总计数
F.sum("decay_score").alias("weighted_activity_score"), # 衰减后的加权得分
F.max("timestamp").alias("last_action_time") # 最近一次行为时间
)

user_activity_features.show()

2.特征工程之用户活跃度与物品热度

在推荐系统面试中,特征工程是决定模型上线效果的关键。简单的统计量(如点击次数)往往不足以打动面试官,他们更看重你对数据分布、统计噪声以及工程落地的深度思考。


一、 用户活跃度特征 (User Activity Features)

用户活跃度旨在刻画用户的“粘性”与“意图强度”。

1. 三维提取框架 (RFM 逻辑)

  • 强度 (Intensity):总点击、总购买、总收藏。
    • 进阶: 计算“行为转化比”,如 购买次数 / 点击次数,识别高转化精准用户。
  • 频度 (Frequency):日均/周均行为次数、访问天数比例。
    • 进阶: 计算 行为方差。波动大的用户可能是受营销活动驱动,波动小的用户是忠实老客。
  • 近期性 (Recency):距今时间、最近 1h/24h 行为数。
    • 进阶:时间衰减 (Exponential Decay)
      相比滑动窗口,工业界更倾向于使用指数衰减公式来更新用户分数: 其中 $\lambda$ 是衰减因子,$\Delta t$ 是时间差。这种方式无需存储历史明细,对 Flink 流式计算极其友好。

2. 会话特征 (Session Features)

  • 会话长度:单次打开 App 的点击序列长度。
  • 会话频率:每天触发多少个 Session。
  • 用途:识别用户是“闲逛型”还是“目的明确型”。

二、 物品热度特征 (Item Popularity Features)

物品热度决定了系统处理“马太效应”和“冷启动”的能力。

1. 全局与局部热度

  • 全局热度:历史累计点击、购买。反映物品的长期生命力。
  • 时效热度:过去 1 小时、1 天的活跃度。捕捉“突发爆款”。

2. 核心指标:点击率 (CTR) 的平滑处理

直接计算 $Clicks / Impressions$ 在小样本下会失效(例如 1 投 1 中)。

  • 面试加分:贝叶斯平滑 (Bayesian Smoothing)
    给分子分母加上先验参数 $\alpha$ 和 $\beta$: 这能让展示量极低的新物品 CTR 趋向于全局平均值,防止其通过高噪声虚假占榜。

3. 物品冷启动 (Cold Start)

  • 类目平滑:当新物品无数据时,取其所属叶子类目(Category)或标签(Tag)的平均热度。
  • 相似度传递:利用内容向量(Embedding)寻找相似物品,借鉴老物品的热度。

三、 深度思考:分布处理与工程实现

1. 长尾分布与数据归一化

用户活跃度和物品热度通常服从长尾分布(Power Law)

  • 技巧:直接输入原始大数值会导致模型梯度爆炸。通常使用 Log 变换 处理:

2. 工程落地:Lambda 架构

  • 离线层 (Spark):利用 Window FunctionsGroupBy 计算 T+1 的长期统计特征(如过去 30 天点击量)。
  • 实时层 (Flink):维护 State 计算实时特征(如过去 5 分钟热度)。
  • 存储层:特征存入 Redis 或 Feature Store,供模型推理服务(Inference)毫秒级查询。

四、 面试高频追问预测

  • Q:如果热度特征非常强,模型总是推荐热门物品怎么办?
    • A:这叫“马太效应”。可以在训练时对热门样本降采样,或者在特征中引入 Position Bias(位置偏置)并进行校准,或者在排序后增加 Diversity(多样性)打散算子。
  • Q:如何定义“活跃用户”?
    • A:不能只看点击。通常结合 活跃天数 (DAU)核心动作(如播放完、购买)。在短视频中,完播率高于 80% 的用户行为权重大于单纯的点击。

3.多模态特征融合与增强用户表征

在多模态推荐中,核心挑战在于如何将非结构化数据(文本/图像)的语义信息与结构化数据(用户行为/ID)的统计信息有机结合,以解决冷启动和兴趣泛化问题。


1. 整体架构方案 (Three-Stage Framework)

一个工业级方案通常分为三个核心阶段:

第一阶段:多模态特征预处理 (Feature Extraction)

  • 文本编码器:使用预训练的 BERTSentence-BERT 将物品标题/描述编码为稠密向量(如 768 维)。
    • 深度优化:由于 BERT 向量维度较高且与 ID Embedding 空间不一致,需通过一个投影层 (Linear Projection Layer) 将其映射到低维空间(如 32 或 64 维)。
  • ID 嵌入:随机初始化 Item_ID 的 Embedding,随推荐任务端到端训练,捕捉协同过滤信号。

第二阶段:行为序列增强建模 (Sequence Modeling)

  • 特征融合 (Early Fusion):对于用户历史序列中的每个物品,将 ID_EmbeddingText_Vector 进行拼接 (Concat)相加 (Add)
    • 公式推导:增强物品表征 $E_t = \text{MLP}([V_{id} \oplus V_{text}])$。
  • 时序建模:将增强后的物品序列送入序列模型:
    • Transformer (BST):利用 Self-Attention 捕捉物品间的语义相关性。
    • GRU (DIEN):利用兴趣进化层捕捉用户动态兴趣的流转。

第三阶段:深度交互与多塔融合 (Deep Fusion)

  • 目标注意力 (Target Attention):利用当前候选物品的文本特征作为 Query,去计算历史行为序列中各物品的权重。
    • 业务逻辑:即使 ID 不匹配,如果文本语义接近(如都是“轻薄、透气”),注意力权重也会升高。
  • 多塔结构:将序列模型输出、用户静态特征、上下文特征拼接,通过 MLP 映射为最终的增强用户表征向量

2. 工业界核心挑战与对策 (Deep Dive)

挑战 核心痛点 工业界解决方案 (面试加分项)
实时性/推理延迟 BERT 等大模型在线推理极慢(>50ms)。 离线向量化:预先计算所有物品的文本向量存入 Feature Store,在线仅进行 Embedding Lookup
语义偏移 预训练文本向量与推荐任务目标不完全对齐。 微调/投影层:在推荐模型内部加入投影层,让文本向量在推荐任务的 Loss 指引下进行微调(Fine-tune)。
特征冲突 (Overshadowing) 强 ID 特征可能让模型忽略弱文本特征。 门控融合 (Gated Fusion):引入类似 GRU 的门控单元,由模型动态学习 ID 和文本的融合比例。
冷启动 新物品缺失 ID Embedding。 语义退化策略:当 ID Embedding 缺失时,完全依赖文本向量进行表征,实现 Zero-shot 推荐

3. 方案评估与迭代

离线评估 (Offline)

  • 指标:在测试集上对比引入多模态特征前后的 AUCGAUCNDCG
  • 消融实验:分别验证“纯 ID”、“纯文本”、“融合方案”的效果,确保文本特征确实带来了增益。

线上实验 (Online A/B Test)

  • 核心指标:CTR(点击率)、CVR(转化率)。
  • 长尾指标:重点关注冷启动物品的曝光量和点击率,这通常是多模态方案提升最明显的地方。

四、 面试高频追问预测

  • Q:如果除了文本还有图像(如商品主图),你会如何扩展这个方案
    • A:采用类似的思路。使用预训练的 ResNet 或 Vision Transformer (ViT) 提取视觉向量。可以采用 张量融合 (Tensor Fusion) 或者简单的 Concat。更进阶的方法是使用 CLIP 这种图文对齐模型,天然地将图像和文本映射到同一个语义空间。
  • Q:加入文本特征后,模型收敛变慢了,可能是什么原因
    • A:可能是两种特征的学习率(Learning Rate)不一致。预训练向量通常已经很成熟,只需要微调;而 ID Embedding 需要从头学习。可以尝试给不同的特征组设置不同的优化器参数。
  • Q:文本特征对解决“冷启动”具体是怎么起作用的
    • A:新物品没有历史交互,其 ID Embedding 是随机初始化的,没有信息量。但新物品一定有标题和描述。通过文本表征,模型可以发现“这个新物品的描述与用户喜欢的某个老物品非常接近”,从而实现语义上的精准分发。

5. 总结:为什么要这么设计?

金句总结
“多模态融合的本质是利用预训练模型的先验语义知识来弥补推荐场景中行为数据的稀疏性。通过将非结构化文本映射到统一的表征空间,我们让模型具备了‘理解’物品的能力,而不仅仅是‘记录’点击。”

4.数据倾斜问题

数据倾斜是大规模分布式特征工程中最常见的工程瓶颈。其本质是热点 Key 导致个别 Task 处理的数据量远超其他 Task,从而触发长尾效应(Stragglers),拖慢整个 Job 的完成时间。


一、 聚合操作引起的倾斜 (GroupBy/Count Skew)

现象:热门商品(爆款 Item_ID)的点击量极高,导致 Reduce 阶段某个分区内存溢出(OOM)或运行极慢。

1. 核心对策:两阶段聚合 (Salting)

  • 第一阶段(局部聚合):给 Key 加上随机前缀(如 0~9_Item_A),将原本路由到同一分区的热点 Key 分散到多个分区进行预聚合。
  • 第二阶段(全局聚合):去掉随机前缀,对预聚合的结果进行二次汇总,得到最终结果。
    • 适用场景countsum 等满足结合律的聚合运算。
    • 面试加分:在 Spark 中,可提到 spark.sql.adaptive.enabled(自适应查询执行 AQE),它能自动检测分区大小并在运行时合并或拆分倾斜分区。
示例:双十一 iPhone 点击统计

场景设定:双十一期间统计各商品点击总数。iPhone_17 是超级爆款,产生了 100 万次点击;普通商品(如充电线)只有 10 次。假设每个 Task 的处理上限为 20 万条,若直接聚合,处理 iPhone_17 的 Task 必然崩溃(OOM)。

阶段一:局部聚合(加盐分散压力)

给每条 iPhone_17 记录随机附加 $0 \sim 4$ 的盐值($k=5$),Key 改变后,100 万条数据被哈希分散到 5 个不同的 Task:

打盐后的 Key Task 局部计数
0_iPhone_17 Task 0 200,050
1_iPhone_17 Task 1 199,980
2_iPhone_17 Task 2 200,010
3_iPhone_17 Task 3 199,950
4_iPhone_17 Task 4 200,010

此时,没有任何一个 Task 超过 20 万条的负载上限,任务全部成功完成。

阶段二:全局聚合(去盐还原结果)

将 5 条中间结果去掉前缀,还原为 iPhone_17,再由单个 Task 进行最终加法:

方案对比

维度 普通聚合 (Single Stage) 两阶段聚合 (Two-Stage)
最大单点压力 1,000,000 条(导致崩溃) 200,050 条(平稳运行)
计算复杂度 $O(N)$ $O(\frac{N}{k} + k)$
Shuffle 数据量 100 万条原始记录 100 万条 + $k$ 条中间结果
核心模式 多对一 多对多对一

二、 连接操作引起的倾斜 (Join/Sequence Skew)

现象:按 User_ID 分组提取行为序列时,爬虫或超级活跃用户产生的日志成千上万,导致 Shuffle 过程极慢。

1. 过滤与分流

  • 爬虫治理:识别行为频次远超常人的 ID,将其单独存储或在特征计算中剔除。
  • 分而治之:将倾斜的 Key 拆分出来单独处理,其余按常规逻辑走。

2. Broadcast Join(大表 Join 小表)

  • 原理:将 Reduce-side Join 转化为 Map-side Join,使用广播变量将小表分发到每个 Executor,彻底避免 Shuffle。
  • 使用条件:小表数据量在内存可接受范围内(通常 < 几百 MB)。

3. 采样与扩容 (Skew Join Optimization)

  • 对大表进行采样,找出 Top N 个倾斜 Key。
  • 对另一张表中这些 Key 对应的数据进行“膨胀”(拷贝多份),从而配合大表加盐后的并行计算。
示例:用户行为表 Join 用户属性表

场景设定:推荐系统中,将行为日志表(A)与用户属性表(B)关联,目的是给每条行为记录打上年龄段标签,供模型做特征增强。

1
SELECT A.*, B.age_group FROM A JOIN B ON A.user_id = B.user_id

执行这条 SQL 的目的:是为了知道“点击了这个商品的人,到底是哪个年龄段的?” 这是特征工程中最关键的一步。有了 age_group,模型才能学习到:“iPhone 17 主要是 20-30 岁的青年人在点击”,从而实现精准推荐。

字段 规模
行为日志表 A user_id, item_id 1 亿条
用户属性表 B user_id, age_group 每人 1 行

热点问题User_999 是一个爬虫账号,在表 A 中产生了 200 万条点击。单 Task 内存上限为 50 万条。若直接 Shuffle Join,处理 User_999 的 Task 必然 OOM。

常规 Join 的失败过程

Shuffle 阶段以 hash(user_id) % 分区数 分发数据,所有 User_999 的 200 万条行为与属性表中的 1 条画像,全部被路由到同一个 Task X,远超 50 万条上限,任务崩溃。

加盐扩容 Join(Salting & Expansion Join)

第一步:对大表加盐

预先识别 User_999 为倾斜键,给其 200 万条记录随机打上 $0 \sim 3$ 的盐值(扩容因子 $k=4$):

加盐后的 Key Task 数据量
User_999_0 Task 0 ~50 万条
User_999_1 Task 1 ~50 万条
User_999_2 Task 2 ~50 万条
User_999_3 Task 3 ~50 万条

第二步:对小表扩容

属性表中 User_999 的那 1 行记录复制 4 份,分别命名为 User_999_0User_999_3,确保每个加盐 Key 都能找到匹配项。

第三步:并行执行 Join

每个 Task 处理约 50 万 + 1 条,全部在内存上限以内,任务顺利完成。最终合并结果时去掉盐值前缀,输出与常规 Join 完全相同。

方案对比

维度 常规 Shuffle Join 加盐扩容 Join
单点最大压力 2,000,001 条(OOM) 500,001 条(平稳)
小表存储代价 $1 \times \text{Size}_B$ $(1 + k) \times \text{Size}_B$
计算复杂度 $O(N \log N)$(单点瓶颈) $O!\left(\frac{N}{k} \log \frac{N}{k}\right)$(全并行)
核心模式 多对一 Shuffle 加盐分散 + 小表膨胀

加盐扩容的代价是小表存储量增加了 $k$ 倍。实践中 $k$ 通常取 4~10,小表一般远小于大表,这个代价完全可以接受。


三、 特征编码引起的倾斜 (Encoding Skew)

现象:地理位置中的”未知”或兴趣标签中的”其他”占比过高,导致特征工程(如 One-Hot 编码或频率统计)时计算压力集中在少数分区。

1. 类别重组

  • 将极低频的类别统一归并为 Other
  • 对极高频的默认值(如 Unknown)进行特殊标记或随机映射,分散其压力。

2. 重分区 (Re-partition)

  • 在处理特征前手动调用 .repartition(num_partitions, "feature_col")
    • 深度理解:Spark 默认的 HashPartitioner 在 Key 分布极不均匀时会退化。可自定义 RangePartitioner 或适当增大分区数来缓解。
示例:用户行为表 Join 用户属性表

在特征工程阶段,特征编码(Feature Encoding) 是将类别特征(如标签、城市、职业)转化为数值向量的关键步骤。当某些类别(如“未知”或“热门标签”)的出现频率远超其他类别时,会触发计算瓶颈。


1. 场景设定:兴趣标签的独热编码 (One-Hot Encoding Skew)

  • 数据规模:1000 万条行为记录。
  • 特征字段interest_tag(用户兴趣标签)。
  • 数据分布(数值层面)
    • 热点 Key:标签 Unknown(由于缺失值填充或默认值)出现了 800 万次
    • 次热点:标签 Sports 出现了 10 万次
    • 长尾类:其余 1000 个标签(如 Jazz, Baking)平均每个仅出现几百次。
  • 计算任务:对该特征进行分布式独热编码(One-Hot)或计数编码(Count Encoding)。

2. 倾斜爆发过程 (The Bottleneck)

在分布式处理(如 Spark)进行特征转换时,底层逻辑如下:

  1. 分组 (Grouping):系统根据标签的哈希值进行 Shuffle,确保相同标签的所有样本进入同一个 Task。
  2. 负载崩溃
    • Task A:接收到所有 Unknown 标签,处理 800 万条 数据。
    • Task B:接收到 Sports,处理 10 万条 数据。
    • Task C:接收到所有长尾标签,处理量不足 10 万条
  3. 结果:Task B 和 C 在几秒钟内收工,而 Task A 成为“长尾任务”,运行数小时甚至因内存耗尽(OOM)导致整个 Job 失败。

3. 深度解决方案:随机化映射与阈值截断

为了治理这种倾斜,我们需要在编码算子执行前进行特征重塑

A. 高频“无效”类:随机化映射 (Random Mapping)

对于 Unknown 这种不带具体语义但量级巨大的类别,采用“逻辑合并,物理分散”的策略。

  • 具体操作:将 Unknown 随机替换为 Unknown_0, Unknown_1, ..., Unknown_9
  • 数值变化
    • 原本 1 个 Task 处理 800 万条数据,现在分散到 10 个 Task,每个处理 80 万条
    • 虽然 80 万依然高于普通类,但已降至单机处理的“安全水位”之下。
B. 低频“长尾”类:阈值截断 (Frequency Truncation)
  • 具体操作:统计所有标签频率,将出现次数低于 100 次的标签统一编码为 Other
  • 数学意义:压缩特征空间的维度(从几千维降到几百维),防止模型过拟合,并平衡了分区数量。

4. 模型端的承接逻辑 (Downstream Processing)

随机化映射后,如何保证模型认为 Unknown_0Unknown_9 是同一个东西?

  1. 权重共享 (Weight Sharing):在深度学习模型(如 Wide&Deep)的 Embedding 层,让所有 Unknown_i 映射到同一个 Embedding ID。
  2. 映射还原:在离线计算统计特征(如点击率)时,先按子类计算,最后在输出阶段将 Unknown_0~9 的结果进行加权汇总。

5. 面试高阶视角:为什么不直接剔除 Unknown

核心观点:在推荐系统中,“缺失”本身就是一种特征信号。

  • 业务逻辑:如果一个用户的年龄或兴趣是 Unknown,可能代表他是新用户(冷启动场景),或者是隐私敏感用户。这两类人的行为模式与普通用户有显著差异。
  • 工程总结:我们不能通过删除数据来规避倾斜,而应通过“引入熵(随机性)”来打散热点,确保数据处理流水线(Pipeline)的鲁棒性。

四、 三种倾斜场景对比

倾斜场景 典型触发条件 核心对策
聚合倾斜 爆款商品点击数极高 两阶段聚合(加盐 + 全局汇总)
Join/序列倾斜 爬虫用户行为日志膨胀 爬虫过滤 + Broadcast Join + 采样扩容
编码倾斜 高频默认值(Unknown)占比过高 类别重组 + 自定义重分区

五、 面试高频追问预测

  • Q:如何定位是哪个 Key 导致了倾斜?

    • A
      1. 查看 Spark UI:观察各 Task 的 Shuffle Read Size/Records,若某 Task 数据量远大于 Median,即存在倾斜。
      2. 日志采样:对原始数据采样后执行 df.sample(...).groupBy("key").count().orderBy(desc("count")),找出频率异常高的 Key。
  • Q:如果不能加盐(如必须按 ID 严格排序提取序列),怎么办?

    • A
      1. 窗口函数截断:限制每个用户的序列长度,只保留最近的 N 条,从源头减小单 Task 的数据量。
      2. 资源调整:增大 Executor 内存,或调大 spark.sql.shuffle.partitions
  • Q:Flink 实时流处理中如何解决数据倾斜?

    • A
      1. 增量聚合:使用 AggregateFunction 代替全量 ProcessFunction,减少状态积压。
      2. MiniBatch 机制:降低状态访问频率,缓解热点 Key 的写入压力。
      3. 二次聚合:对 KeyBy 倾斜同样可套用”加盐局部聚合 + 全局汇总”的思路。

在实时推荐场景中,处理用户行为日志(如点击、曝光、搜索)的核心目标是:低延迟捕捉用户兴趣转向,并实现复杂的实时特征计算。


1. 核心架构与原理对比

特性 Spark Streaming (Micro-Batch) Flink (Native Stream)
处理模型 微批处理:将流切分为极小的 RDD 批次(通常 500ms~数秒)。 原生流处理:逐条记录(Event-by-Event)处理,真正的连续流。
延迟性 秒级(Second-level)。 毫秒级 (Millisecond-level)
吞吐量 极高(在大批量数据吞吐下表现优异)。 高(且在高压力下延迟抖动更小)。
时间机制 主要依赖处理时间(Processing Time)。 完美支持 事件时间 (Event Time) 和水印 (Watermark)。

2. 深度对比:为什么推荐系统更青睐 Flink?

A. 状态管理 (State Management) —— 推荐特征的基石

  • 场景:计算“用户最近 N 次点击序列”或“实时 CTR”。这需要保存海量的用户中间状态。
  • Flink 优势:提供原生 Keyed State(如 MapState),支持增量检查点(Incremental Checkpoints)。状态支持 TTL(过期策略),能自动清理 1 小时前的陈旧兴趣数据,内存管理极其高效。
  • Spark 劣势:状态管理(如 updateStateByKey)相对笨重,在大规模用户状态下,Checkpoint 压力巨大,容易导致长尾延迟。

B. 事件时间与乱序处理 (Handling Out-of-order Data)

  • 场景:移动端日志上传受网络影响,常出现“点击”比“曝光”晚到的乱序情况。
  • Flink 优势:通过 Watermark(水位线) 机制,Flink 可以优雅地等待迟到数据,确保实时特征(如转化率)计算的准确性。
  • Spark 劣势:微批架构难以天然处理跨批次的乱序数据,对事件时间的支持不如 Flink 原生。

C. 容错与精确一次 (Exactly-Once)

  • Flink:基于 Chandy-Lamport 算法 的分布式快照,在不停止处理的情况下完成状态备份,开销极低且保证一致性。
  • Spark:依赖 WAL(预写日志)或频繁的 RDD Checkpoint,I/O 消耗大,在大状态场景下易拖慢处理速度。

3. 选型理由总结:为什么我选 Flink?

  1. 极低延迟:推荐系统对“新鲜度”要求极高。毫秒级响应能让用户刚看完整型视频,下一秒就刷到相关器材,转化效率更高。
  2. 复杂的窗口计算:Flink 支持滑动窗口、滚动窗口及会话窗口 (Session Window),非常适合分析用户在单次 App 打开期间的连续行为流。
  3. 成熟的批流一体:Flink SQL 实现了真正的代码复用。我们可以用同一套 SQL 逻辑既跑离线特征回溯,又跑在线特征提取,极大降低了维护两套代码(Lambda 架构)的成本。
  4. 动态扩容能力:在线系统经常遇到大促流量波动,Flink 的状态兼容性允许我们在不丢失用户实时偏好的前提下,平滑增加计算资源。


💡 面试加分金句

“Spark Streaming 是用的思维去模拟,而 Flink 是用的思维去包含。在实时推荐这种对‘时效性’和‘细粒度状态’有极致追求的场景下,Flink 的原生流架构具备不可替代的代差优势。”


Author: YANG
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint polocy. If reproduced, please indicate source YANG !
  TOC