3084 字
约 10 分钟
18
S02-A12 · 匿名流量的处置:没有身份的请求怎么算配额

按用户 ID 限流上线后,监控显示大部分拦截来自同一个桶——那个桶负责的是所有未登录请求。登录用户各自 100 次每分钟,未登录请求几百万人挤在一个"匿名"桶里共享同样的额度。要么把额度调大到失去意义,要么每天误伤一批正常访客。

匿名请求的麻烦不在"量大",而在于它没有稳定标识。限流、配额、缓存、风控这一整套机制都建立在"知道这是谁"的前提上,匿名请求把这个前提拿掉了。拿掉之后剩下的都是弱标识,而每一种弱标识都有一个特定的失效方式。


1. 弱标识的三个失效方式

没有登录态时能用的标识只有三种,成本逐级上升,可靠性也逐级上升,但没有一种能当作稳定身份。

标识 稳定性 成本 主要失效方式
IP 地址 低 零 共享(NAT、出口网关)、漂移(移动网络切基站)、伪造(代理池)
设备指纹 中 需前端配合 浏览器升级即变、隐私模式归零、可伪造
会话 Cookie 中高 需下发与存储 可清除、可批量伪造、首次请求没有

IP 是最便宜也最不可靠的一个。家庭宽带的 IP 可能保持几天不变,也可能每天重新分配;企业出口、学校、运营商级 NAT 背后可能是几千个真实用户共用一个地址。用 IP 做配额粒度,等于把一个楼里的人当成一个人。

// 先看清 IP 的真实集中度,再决定用它做多细的粒度
log_format anon '$remote_addr $http_x_forwarded_for "$request" $status $request_time';

// 统计 Top IP 的请求占比:头部占比过高说明 NAT 严重,IP 粒度不可用
awk '{print $1}' /var/log/nginx/access.log \
| sort | uniq -c | sort -rn | head -50 \
| awk '{sum+=$1} END {print "Top50 IP 占比:", sum*100/NR"%"}'
// 注意:nginx 日志里的 remote_addr 是上一跳代理地址,必须先用 real_ip 模块还原真实客户端 IP

这里有个前置问题常常被跳过:如果前面有 CDN 或网关,remote_addr 根本不是客户端 IP。没有正确配置 set_real_ip_from 与 real_ip_header 时,所有请求看起来都来自同一两个地址,基于 IP 的一切策略都会立即失效。

2. NAT 与移动网络:误伤的主要来源

限流的灾难以批量投诉的形式出现,而它们的来源相当集中。

网络环境 标识表现 误伤形态 缓解方向
企业/学校出口 数千用户一个 IP 一个人触发限流,全公司被拦 IP 粒度放宽,加会话粒度兜底
运营商级 NAT 数百至数千用户一个 IP 地域性集体投诉 不做纯 IP 拦截,改用挑战式验证
移动网络 切换基站即换 IP 同一用户被当成多个新用户 提高 IP 维度的额度,避免按 IP 计数
公共 WiFi 大量设备一个 IP 时段性集中误伤 识别出口特征,单独放宽
代理/VPN 池 一个请求源反复换 IP 限流形同虚设 不指望 IP 拦住,靠行为特征

识别这几种环境不需要复杂模型,出口 IP 的请求分布特征本身就能区分:

// 用访问日志区分"共享出口"与"单一用户",判断该不该对该 IP 做限流
SELECT client_ip,
       COUNT(DISTINCT sid)                    AS sessions,
       COUNT(*)                               AS reqs,
       ROUND(COUNT(*) / NULLIF(COUNT(DISTINCT sid), 0), 1) AS reqs_per_session,
       COUNT(DISTINCT ua_hash)                AS ua_kinds
FROM access_log
WHERE ts > NOW() - INTERVAL 1 DAY
GROUP BY client_ip
HAVING reqs > 500
ORDER BY reqs DESC;
// sessions 与 ua_kinds 同时很大 → 共享出口,按 IP 限流会误伤
// sessions 很小而 reqs 极大 → 单一来源高频,值得单独处理

误伤的隐蔽性在于它不会产生告警。被拦的用户大部分直接离开,只有极小比例会来投诉;投诉到达客服时,工程师手上已经看不到当时的限流日志。所以匿名场景的限流必须有一个"只记录不拦截"的观察期,用它来估算误伤规模,而不是靠上线后听投诉。

3. 配额的分配与身份迁移

匿名请求该给多少额度,一半是技术问题,一半是产品决策,两者必须分开讨论。

决策 技术侧能给的输入 产品侧要回答的问题
匿名额度是登录的几分之一 匿名与登录请求的成本分布差异 是否愿意用体验换注册引导
匿名额度按什么维度分 各弱标识的集中度与稳定性 哪些用户群体更容易被误伤
登录后额度是否重置 登录前后会话能否关联 重置会不会被用来刷
未登录的浏览是否算入配额 各接口的成本量级 哪些行为必须允许未登录完成

登录后配额重置这个点容易被忽略。如果登录瞬间额度清零,那么"匿名蹭满额度 → 登录 → 再蹭满"就成了一条免费通道。可行的做法是把匿名阶段已用额度带进登录后的账户,或者对登录后的短时间窗做单独计数。

会话标识的生命周期要和配额窗口对齐。配额按分钟计,会话 Cookie 却设成一年,那么一个被限流的会话在 Cookie 存续期内一直是同一个键;反过来会话只有五分钟,配额窗口就没法跨请求累积。这两者不是独立参数,调一个必须调另一个。

// 配额分层:粗粒度兜底、细粒度精确,两层都要有出口
anon_quota:
  global:            // 总量兜底,防全局打满
    limit: 500000/min
  by_ip:             // 粗粒度,额度给宽,只拦明显异常
    limit: 3000/min
    exclude_cidrs: ["10.0.0.0/8", "203.0.113.0/24"]   // 已知出口
  by_session:        // 主粒度,匿名请求的真实约束
    limit: 120/min
    key: "sid"
    on_missing_key: issue   // 无会话时先下发再计数,不要直接放行
  on_login: carry_over      // 登录后继承已用额度,避免清零被刷

4. 挑战式验证:把拦截换成举证

纯拒绝是最省事也最伤人的处理。挑战式验证在两者之间加了一档:不直接拒绝,而是要求请求方证明自己是人。

方式 对正常用户的成本 对自动化的成本 适用位置
图形验证码 中(要辨认、要重试) 低到中(可打码) 低频、可接受打断的入口
滑块/轨迹 低 中 通用入口
无声挑战(JS 执行) 极低 中(需模拟浏览器) 高频接口前置
延迟排队 中(等待时间) 高(占用连接) 高成本接口

挑战式验证还有一个绕不开的成本:它对无障碍使用者与老旧设备不友好,而且失败后的重试路径往往比正常路径更脆。因此它适合放在"少数请求"上——也就是前置条件应该足够严格,让正常情况下几乎没人遇到挑战。

把它当限流的下一档而不是替代,才有意义:先记录、再挑战、最后拒绝。顺序反了就会在还没搞清楚正常流量长什么样的时候,先把正常用户挡在外面。

还有个容易忽略的副作用:挑战式验证会产生额外的请求量(失败重试、资源加载),在系统已经很紧张的时候引入它,可能把压力推得更高。它适合放在平时,不适合放在故障中。

5. 排错:五种"限流拦了不该拦的"

现象 成因 处理方向
某一时段集中投诉 企业出口或公共 WiFi 共享 IP 被限 用 IP 集中度统计定位,放宽该 IP 粒度
日志里所有请求同一 IP 反代未还原真实客户端 IP 配置 set_real_ip_from 与 real_ip_header
限流后爬虫流量没下降 代理池导致 IP 维度失效 停止依赖 IP,改用会话与行为特征
匿名额度调大后接口被打满 单一维度无总量兜底 加全局总量层,保留 IP 与全局双层
用户登录后仍被限流 登录后未携带或迁移匿名额度 明确迁移规则,日志里打印使用的配额键

第二条是最容易排查也最致命的:如果真实 IP 没被还原,那么不仅限流失效,基于地理位置、黑白名单、访问统计的所有下游能力都是错的。这类问题只能在链路层面修,不能在下游想办法。

// 验证真实客户端 IP 是否被正确还原
curl -s -H "X-Forwarded-For: 198.51.100.7" https://TARGET/api/whoami
// 返回的客户端 IP 应为 198.51.100.7;仍是上游代理地址说明 real_ip 配置缺失
// 注意:仅当来源属于可信代理段时才应信任 XFF,否则该头可被随意伪造

6. 边界:什么情况不需要匿名配额体系

场景 为什么不需要 做法
纯内部服务,无匿名入口 所有调用方都有身份 按服务身份限流即可
流量极小且无爬虫动机 单点策略足够 只做全局总量控制
匿名与登录行为高度重合 区分粒度没有收益 统一按会话计数
接口本身成本极低 限流收益低于误伤成本 不做限流,只做监控
业务目标是扩大匿名浏览 限制匿名与目标冲突 配额只防滥用,不做引导

前面几节的判据,可以压成一张决策表:

// 匿名流量处置的顺序:每一步都是下一步的前提
1. 真实 IP 是否还原        ← 没做到,后面全部失效
2. IP 集中度是否可接受      ← 头部占比过高,放弃 IP 粒度
3. 会话标识是否已下发      ← 首次请求必须先发再计
4. 是否有全局总量兜底      ← 防止任一维度被绕过后全站打满
5. 是否有只记录不拦截的观察期 ← 用它估算误伤,而不是听投诉
6. 挑战式验证放在最后一档  ← 常态下应极少触发

判断口径可以归纳成一句:匿名配额的目的是防止少数人挤占多数人的资源,不是引导注册。一旦这条目的被混淆,额度就会被当成增长工具来调,而它对误伤的判断标准会同时失效。


7. 自测

  1. 弱标识有哪三种?各自最主要的失效方式是什么?
  2. 为什么有 CDN 或反向代理时,必须先用 real_ip 模块还原客户端地址。
  3. 运营商级 NAT 场景下,为什么纯 IP 拦截不可用?
  4. 登录后配额清零为什么可能成为一条免费通道?如何避免?
  5. "先记录、再挑战、最后拒绝"这个顺序,为什么不能颠倒?
  6. 匿名配额的目的如果不是引导注册,那么它的判据应该是什么?
S02-A12 · 匿名流量的处置:没有身份的请求怎么算配额
https://www.clxhxhhr.top/posts/6379/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。