跳过正文
  1. 全部/
  2. 笔记/
  3. 实习/

百度问题

目录
架构与数据流(Q1-Q3)
#
1. 你先整体讲一下百度网盘这套反作弊系统的架构,数据流是怎么走的?从请求进入到最终拦截,中间经过了哪些组件?
#

用户请求 ↓ Gateway (Nginx/OpenResty) ↓ Antifilter (Lua 模块,嵌入网关进程) │ - 从 DB 加载策略到共享内存(热更新) │ - 匹配请求 URI + header/query/body 参数 │ - 命中 → 转发到 AntiOnlineServer │ - 未命中 → 透传业务后端 ↓ AntiOnlineServer (Go 服务) │ - 从 Redis 读取规则缓存(miss 则查 DB 回填) │ - 多 Goroutine 并发执行策略 │ - 11 种规则类型:黑库/日活/昊天镜/下载计数/SVIP检测/rand校验… │ - 返回 proxy_type: 1=拦截, 0=放行 ↓ 处置动作:封禁 / 验证码 / 观察 / 记录日志 Antifilter 和 AntiOnlineServer 的分工:Antifilter 只做轻量匹配(URI+参数正则),AntiOnlineServer 做重量过检(查 DB、查缓存、调风控接口)。关键设计:Antifilter 到 AntiOnlineServer 的调用设了超时,到时间没返回就当放行,保证网关响应延迟可控。

2. 为什么这个场景要拆成“离线判定 + 在线拦截”两段式,而不是全部实时处理?
#

SVIP 风控的场景:

  • 在线链路必须快(网关层不能等太久)
  • 查用户真实会员状态需要调 DB 或外部接口(慢)
  • 破解端的行为特征是批量+持续的,不需要每个请求都实时判定 所以 rule7 做异步采集(推 MQ),rule8 做同步拦截(查 Redis)——在线路径只做 O(1) 的 Redis 查询,真正的判定逻辑在 Worker 里异步完成。
3. 你们为什么选择 Pulsar,而不是 Kafka、RabbitMQ?当时主要考虑了哪些特性?
#
  • Pulsar 支持多租户隔离(tenant/namespace 模型),百度内部多业务线共用集群
  • 存储计算分离,积压不丢消息
  • 支持消息确认 + 重试,离线 Worker 消费失败可以重试
风控业务(Q4-Q6)
#
4. 你提到“破解端伪造会员状态”,具体是怎么伪造的?客户端和服务端的会员状态为什么会不一致?
#

破解端用户在端上篡改其会员状态从而在端上盗用 SVIP 权益(以倍速播放为代表),其真实身份为普通用户。

  • Android/iOS 端反编译 APK/IPA,修改本地会员标记位
  • 客户端把 vip=2(SVIP)写死在请求参数里
  • 服务端某些接口不做服务端校验就返回了 SVIP 权益(如倍速播放、高清画质) 对抗方式:不看客户端声称的 vip 字段,而是查服务端数据库里的真实会员状态。这就是 Worker 做的事情。
5. 你们的“风险指纹”里包含哪些维度?为什么选择设备维度而不是 UID 维度?
#

文档里的指纹是 MD5(clienttype + devuid + version)。选设备维度是因为:

  • 黑产会批量注册 UID(一个设备换多个 UID 刷)
  • devuid 是设备唯一标识,比 UID 更难伪造
  • 加 version 是为了区分"同一设备升级客户端"的正常场景
6. 你这里用了 MD5 指纹。MD5 已经不安全了,为什么还能用于这个场景?
#

MD5 在这里不是做加密或签名,而是做特征压缩——把几个字段拼起来生成一个固定长度的 key 用于 Redis 查找。碰撞概率在这个场景下可接受(Redis key 空间足够大,且是黑名单场景——即使碰撞也是误拦而非漏拦,安全侧偏向拦截)。

去重与并发(Q7-Q10)
#
7. 如果同一个设备频繁换 UID,你们这套方案还能识别吗?有没有对抗“养号”的能力?
#

Rule7 的 MD5 指纹是 MD5(clienttype + devuid + version),所以:

  • 同设备换 UID → 能识别,因为 devuid 不变
  • 同设备换 version(升级客户端)→ 会生成不同指纹,这是故意设计的豁免

但对抗养号有限:养号阶段不在 vip=2 的接口上报活,rule7 采不到。需要配合其他策略——比如 BDUSS 关联特征(UID+BDUSS+DEVUID+UA 四维关联)来发现"一个设备绑多个 BDUSS/UID"的养号行为。

8. 你提到“UID 天级锁去重”,具体怎么实现?Redis Key 怎么设计?为什么是“天级”?
#
  • Redis Key:rule7:daily_lock:{uid}
  • Value:任意值(用 SET NX 或 EXISTS 判断)
  • TTL:当天剩余秒数(ttl = 次日0点 - now)
  • 逻辑:采集特征前先 SETNX key 1 EX ttl,成功才推 MQ,失败说明今天已经入过队
9. 如果 Pulsar 短时间积压了大量消息,会对风控链路产生什么影响?你们怎么处理消费堆积?
#

影响:积压意味着离线判定延迟,这段时间内破解端可以继续盗用 SVIP 权益。 处理:

  • Worker 扩容(增加消费实例数)
  • Pulsar 本身支持消息过期(TTL),超时可以丢弃——但风控场景丢消息 = 漏拦
  • 更好的方案:Worker 加并发(见 Q10)
10. 离线 Worker 的消费模型是什么?是多协程消费吗?怎么保证吞吐?
#
  • 多 Goroutine 并发从 Pulsar 拉取
  • 每个 Goroutine 独立:拉消息 → 查 DB(用户会员表)→ 判黑 → 写 Redis
  • 如果查 DB 是瓶颈,可以加 Redis 缓存降低 DB 压力
  • 限流:用 channel 做并发控制(类似 anti_verify_email.go 里 semaphore 模式,max 30 goroutine)
11. 你们 Redis 黑名单的数据结构是什么?String、Set、Bitmap 还是 BloomFilter?为什么这么选?
#

频控白名单:Redis SET,key=nd-anti-freq-{uri},SAdd/SRem 批处理 500 条/批 信用封禁:Redis HASH,key=n-credpoints-dev-credit:{sceneId}:{key}:{value},9 个 field(ban_key/value/scene_id/ban_scene/report_type/operate_type/ban_status/ban_datetime/ban_type) 外链豁免:Redis String,key=nd-anti-freq:walian:uid:{UID},TTL=365 天 设备封禁:Redis String,key=n-credpoints-dev-{scene}-{deviceType}-{devuid},SetEX 设 ban 时长

12. 在线拦截的时候 Redis 查询是同步阻塞的吗?如果 Redis 出现抖动怎么办?
#

不是; 第一是设置超时时间,避免请求一直阻塞。 比如 Redis 查询超过几十毫秒就快速失败。 第二是降级策略。 如果 Redis 短时间不可用,一般不会直接把所有请求都拦掉,而是:

  • 降级放行
  • 或只对高风险用户继续拦截 避免误伤正常用户。
13. 风控系统最怕误杀。你们怎么平衡“召回率”和“误杀率”?
#
14. 假设 Redis 黑名单误写了一个正常用户,恢复机制怎么做?
#
15. 你提到“频控触发、黑名单写入、冷却恢复”的端到端验证,你具体怎么验证的?用了什么测试手段?
#

解封接口(DeleteAntiCreditBan):事务里更新 DB 状态 + 删 Redis key + 写操作日志 操作日志(anti_credit_ban_log 表)记录完整的封禁/解封历史 频控白名单:SRem 从 SET 移除即恢复

16. 你们如何保证“离线判定结果”和“在线拦截状态”一致?有没有出现过数据不同步?
#
17. Redis 黑名单是永久的还是带 TTL 的?TTL 怎么设计?
#
18. 如果一个风险用户刚被识别,但 Redis 还没写进去,这个时间窗口里的请求怎么办?有没有一致性问题?
#
19. 你们有没有做过灰度拦截?比如先记录日志不真正拒绝请求?
#
20. 线上风控系统通常 QPS 很高。你们有没有做性能优化?比如 Redis Pipeline、本地缓存、批量消费之类?
#
21. 你提到 AEGIS 支持“策略灰度发布”,灰度是怎么实现的?按 UID、流量比例还是设备?
#
22. “网关层热加载”具体怎么实现?配置更新后怎么通知网关刷新?
#
23. Redis SET 缓存同步为什么选择 SAdd/SRem,而不是直接全量覆盖?
#
24. 如果多个管理员同时修改策略配置,会不会发生覆盖问题?怎么解决?
#
25. 你这里用了 GORM 的 OnConflict。底层生成的 SQL 是什么?为什么它能实现幂等?
#

INSERT INTO anti_freq_whitelist (scene, value, key, bl_status, remark, ban_type) VALUES (?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE remark = VALUES(remark), bl_status = VALUES(bl_status), ban_type = VALUES(ban_type) 它依赖唯一索引。

如果数据不存在,就 INSERT;
如果唯一键冲突,就自动 UPDATE,而不会报错。

所以能实现幂等,也就是同一条数据重复提交多次,最终结果还是一致的,不会产生重复数据。

26. GORM 在高并发场景下你觉得有什么缺点?有没有踩过坑?
#
27. 你们 MySQL 表有没有做索引优化?比如 URI 模糊匹配怎么避免全表扫描?
#
28. 你说“统一基于 created_at desc 展示最新策略”,为什么这个细节能降低线上风险?你遇到过什么真实问题吗?
#
29. 如果让你重新设计这套系统,你觉得当前架构最大的瓶颈或者隐患是什么?
#
  1. Redis 单点风险:在线拦截强依赖 Redis,Redis 挂了 = 整个风控失效(不过百度内部有 Redis 集群 + 哨兵)
  2. 离线判定延迟:Worker 消费到判定完成有几秒延迟,这段时间破解端可以继续盗用
  3. Antifilter Lua 共享内存瓶颈:策略量大了以后,网关内存压力大——这也是为什么要拆分到 AntiOnlineServer
30. 如果现在让你把这套风控系统升级到“千万级 DAU + 更复杂黑灰产对抗”,你会优先改哪几个地方?
#
Reply by Email