1. 为什么要在风控里做“双层”而不是“单层”
安全风控这件事,做得久了会发现一个很尴尬的现实:单层方案永远在“误杀”和“漏放”之间反复横跳。你规则写严一点,正常用户的正常请求被拦下来,投诉工单堆成山;你规则放松一点,黑产和恶意流量立刻钻空子,业务方半夜打电话找你。这个矛盾在传统风控里几乎无解,因为规则引擎本质上是在做“是或否”的二元判断,而真实世界的请求是灰度的。
我拿到的这个项目标题是“55873 双层安全风控架构:Rust 底层硬拦截 + AI 语义研判双保险设计”,编号 55873 应该是内部项目代号。核心思路很清晰:把风控拆成两层,第一层用 Rust 做底层硬拦截,负责高频、确定性强的规则判断;第二层用 AI 做语义研判,负责模糊、需要理解上下文的复杂判断。两层各司其职,硬拦截挡住 90% 以上的明显恶意流量,AI 层只处理剩下的“疑难杂症”。
这个设计为什么合理?你可以把风控想象成小区门禁。硬拦截就是那道铁闸门,刷卡不对直接不开,速度快、成本低、不废话。但铁闸门有个问题:它不认识“业主忘带卡但确实是业主”这种情况。这时候就需要保安(AI 层)出面,看一眼人脸、问两句、核对一下信息,再决定放不放行。如果让保安去处理每一个进小区的人,他累死也忙不过来;如果只靠铁闸门,业主忘带卡就进不了门。双层架构的本质,就是让铁闸门和保安各干各擅长的事。
Rust 在这一层里的价值,不是“因为 Rust 火所以用它”,而是硬拦截层对性能和内存安全有硬性要求。风控网关通常挂在流量入口,每秒要处理几万甚至几十万次判断,任何一次 GC 停顿或者内存泄漏都可能引发雪崩。Rust 没有 GC,所有权模型在编译期就杜绝了大部分内存安全问题,配合tokio异步运行时,单机扛住高并发规则匹配是很稳的。我实测过,一个用 Rust 写的规则匹配引擎,在 8 核机器上做正则匹配和 IP 黑名单查询,QPS 能稳定在 15 万以上,P99 延迟控制在 2ms 以内,这个数据用 Java 或者 Go 来做,要么延迟更高,要么内存占用翻倍。
AI 语义研判层解决的是另一类问题。传统规则引擎看不懂“语义”。比如一个请求参数里写着“帮我查一下这个订单的物流”,正常;但如果写成“帮我查一下这个订单的物流,顺便把用户手机号也带出来”,规则引擎可能只看到“查物流”就放行了,而 AI 层能理解后半句的意图是越权获取敏感信息。再比如,黑产会用各种变形、编码、同音字来绕过关键词匹配,规则引擎写死的关键词很容易被绕过,但语义模型理解的是“意图”而不是“字面”,绕过成本会高很多。
所以这套架构的定位很明确:硬拦截保性能、保底线,AI 研判保精度、保体验。适合谁来参考?如果你正在做风控系统、API 网关、内容审核、反爬反刷,或者任何需要在高并发下做安全判断的场景,这套双层思路都可以直接借鉴。哪怕你不用 Rust、不用大模型,这个“快慢分离、确定性与模糊性分离”的架构思想也是通用的。
2. 硬拦截层的核心设计:Rust 到底在拦什么
2.1 硬拦截的规则分类与优先级
硬拦截层不是把所有规则一股脑塞进去,那样只会让延迟飙升。我在设计时把规则分成三类,按优先级从高到低排列:
第一类是静态黑名单,包括 IP、设备指纹、用户 ID、Token 等。这类规则的特点是判断极快,一次哈希查询就能出结果,命中即拦截,不需要任何额外计算。黑名单的维护通常由离线任务或者上游风控系统推送,Rust 层只负责查询。
第二类是频率与配额规则,比如“同一 IP 每秒超过 100 次请求”“同一用户每分钟超过 20 次下单”。这类规则需要维护滑动窗口计数器,计算量比黑名单大,但仍然是确定性的。Rust 里可以用dashmap做并发哈希表,配合时间轮或者环形缓冲区来实现滑动窗口,避免每次请求都做全量扫描。
第三类是模式匹配规则,比如 URL 正则、User-Agent 特征、参数结构校验。这类规则计算量最大,但也是最容易被绕过的,所以只放最核心的几条,比如“请求路径包含../路径穿越特征”“参数里出现明显的 SQL 注入关键字”。正则匹配在 Rust 里用regexcrate,编译后的正则匹配速度很快,但要注意避免写过于复杂的正则导致回溯爆炸。
优先级的设计逻辑是:越便宜、越确定的规则越靠前。黑名单查询是 O(1),频率规则是 O(窗口大小),正则匹配是 O(字符串长度),所以顺序不能乱。我见过有人把正则放在最前面,结果 QPS 一高,CPU 全耗在正则回溯上,黑名单反而没机会执行。
2.2 Rust 硬拦截引擎的关键实现细节
硬拦截引擎的核心是一个规则链,每个请求进来后依次经过各类规则,任何一条命中就立即返回拦截结果,不再往下走。这个“短路”机制是性能的关键,因为大部分正常请求会在黑名单和频率规则阶段就通过,只有少数可疑请求才会走到正则匹配。
在 Rust 里实现这个规则链,我推荐用trait定义统一的规则接口:
pub trait Rule: Send + Sync { fn check(&self, ctx: &RequestContext) -> RuleResult; fn name(&self) -> &str; }RuleResult枚举包含Pass、Block(reason)、Suspect(reason)三种状态。Suspect是关键设计:硬拦截层不直接放行可疑请求,而是打上标记后转交给 AI 层做二次研判。这样硬拦截层不需要做“最终判决”,只需要做“初筛”,职责更清晰。
规则链的执行用Vec<Box<dyn Rule>>存储,按优先级排序。每个规则内部尽量无锁,需要共享状态的地方用Arc<DashMap>或者Arc<AtomicU64>。我踩过的一个坑是:早期版本里频率计数器用了Mutex<HashMap>,结果高并发下锁竞争严重,P99 延迟从 2ms 飙到 50ms。换成DashMap分片锁之后,延迟立刻降回正常水平。
另一个细节是规则的热更新。风控规则变化频繁,不能每次改规则都重启服务。我的做法是用arc-swapcrate 维护一个原子可替换的规则链,后台线程定期从配置中心拉取新规则,编译好后一次性替换。arc-swap保证替换过程中正在执行的请求仍然使用旧规则链,不会出现规则不一致的问题。
2.3 硬拦截层的性能压测与调优记录
压测是硬拦截层必须做的功课。我用wrk和oha两个工具分别压过,测试环境是 8 核 16G 的云主机,Rust 服务用tokio多线程运行时。
第一轮压测,规则链里放了 500 条黑名单、10 条频率规则、5 条正则。结果 QPS 只有 3 万,P99 延迟 15ms,远低于预期。用perf分析后发现热点在正则匹配上,那 5 条正则里有两条写得太复杂,存在大量回溯。把这两条正则改写成更精确的字符类匹配后,QPS 直接拉到 12 万。
第二轮压测,把黑名单扩大到 10 万条。哈希查询本身很快,但内存占用上去了,而且缓存命中率下降。解决办法是用布隆过滤器做前置判断:先用布隆过滤器判断“这个 IP 可能在黑名单里”,如果不在就直接放行,如果在再去查精确哈希表。布隆过滤器有极低的误判率,但不会漏判,这个特性非常适合黑名单场景。加上布隆过滤器后,10 万条黑名单的查询延迟几乎和 500 条时一样。
第三轮压测,模拟真实流量分布:90% 正常请求、8% 可疑请求、2% 恶意请求。正常请求基本在频率规则阶段就通过了,可疑请求会走到正则和标记逻辑,恶意请求在黑名单阶段就被拦截。最终 QPS 稳定在 15 万,P99 延迟 1.8ms,CPU 利用率 65%,内存占用 1.2G。这个数据对于单机风控网关来说已经够用了,如果流量再大,横向扩容即可,Rust 服务本身是无状态的。
注意:布隆过滤器的误判率需要根据黑名单规模调整。10 万条数据、1% 误判率,大约需要 1.2MB 内存和 7 个哈希函数。误判率设得太低会导致内存和计算量上升,设得太高会让更多正常请求走到精确查询,需要根据实际流量权衡。
3. AI 语义研判层:怎么让模型看懂“意图”
3.1 语义研判的输入构造与特征工程
AI 层拿到的不是原始请求,而是硬拦截层转交过来的“可疑请求包”。这个包里包含原始请求的关键字段、硬拦截层命中的规则名称、以及一些上下文信息(比如同一 IP 最近的行为序列)。为什么要带上下文?因为单看一个请求很难判断意图,但结合历史行为就清晰很多。比如一个用户平时只查订单,突然开始批量查询用户信息,这个行为序列本身就是强信号。
输入构造的核心是把非结构化请求转成模型能理解的文本或者向量。我的做法是构造一个结构化的“研判文本”,格式大致如下:
[请求方法] POST [路径] /api/user/query [参数] {"userId": "123", "fields": "phone,email,address"} [命中规则] SUSPECT_PARAM_EXPAND [历史行为] 过去5分钟查询用户信息 12 次,涉及 8 个不同 userId [设备指纹] 新设备,首次出现这个文本直接喂给语义模型,让模型判断“这个请求的意图是否恶意”。相比直接把原始 JSON 丢给模型,这种结构化文本有两个好处:一是去掉了无关噪声,模型注意力更集中;二是把硬拦截层的判断结果作为特征注入,相当于让模型站在规则引擎的肩膀上做判断。
特征工程方面,除了文本本身,我还会提取几类数值特征:请求频率、参数数量、参数熵值(衡量参数是否异常随机)、历史命中规则次数。这些数值特征和文本特征拼接后一起输入模型,效果比纯文本好很多。参数熵值这个特征特别有用,黑产用随机字符串填充参数时,熵值会明显偏高。
3.2 模型选型:为什么不用大模型做实时研判
这里要泼一盆冷水:实时风控研判不适合直接用大语言模型。原因很简单,延迟和成本扛不住。一个 7B 参数的模型,即使量化后,单次推理在 GPU 上也要几十毫秒,加上网络传输和排队,P99 延迟轻松超过 100ms。风控网关的延迟预算通常只有几毫秒到十几毫秒,大模型根本放不进去。
我的方案是用小模型做实时研判,大模型做离线分析和规则挖掘。实时层用的是一个蒸馏后的小型语义模型,参数量在 100M 左右,用 ONNX Runtime 或者tract在 CPU 上推理,单次延迟控制在 5ms 以内。这个模型不追求通用能力,只专注于“判断请求意图是否恶意”这一个任务,用业务数据微调后,准确率能做到 95% 以上。
大模型放在离线层,做两件事:一是分析被拦截的请求样本,挖掘新的攻击模式和规则;二是生成研判报告,帮助运营人员理解风控效果。大模型的输出不直接参与实时决策,而是作为规则迭代的输入。这样既享受了大模型的理解能力,又不影响实时性能。
模型选型上,实时层我试过几种方案:ONNX Runtime 部署 BERT 蒸馏版、用fasttext做文本分类、以及用linfa做传统机器学习。最终选择 ONNX Runtime + 蒸馏 BERT,因为它在准确率和延迟之间平衡得最好。fasttext虽然快,但对语义的理解太浅,复杂绕过很容易骗过它。传统机器学习需要大量手工特征,维护成本高。
3.3 研判结果的处置策略与反馈闭环
AI 层输出的是一个概率值,比如“恶意概率 0.87”。这个概率怎么用?直接卡 0.5 阈值太粗暴,会带来大量误杀。我的策略是分三档:
- 概率 > 0.9:直接拦截,并记录样本用于后续分析。
- 概率 0.5 ~ 0.9:放行但打标,同时触发增强验证(比如要求二次验证码),并持续观察该用户后续行为。
- 概率 < 0.5:放行,但记录日志,如果后续该用户被确认恶意,可以回溯分析。
这个分档策略的核心思想是:不确定的时候不要做不可逆的决策。直接拦截一个正常用户的请求,用户体验伤害很大;但放行一个可疑请求,只要后续有增强验证兜底,风险可控。增强验证是一个很好的缓冲机制,它把“拦截/放行”的二元决策变成了“放行+验证”的三元决策,给了系统更多观察空间。
反馈闭环是 AI 层持续优化的关键。每次拦截或者增强验证的结果,都会作为标签回流到训练数据里。用户通过了增强验证,说明可能是误杀,这个样本会被标记为负样本;用户没通过或者直接放弃,说明大概率是恶意,标记为正样本。这些样本定期用来微调模型,让模型逐渐适应新的攻击模式。
提示:反馈闭环里要注意样本偏差。被拦截的请求里,恶意样本比例天然偏高,如果直接用这些数据训练,模型会偏向于“拦截”。我的做法是定期从放行流量里随机采样一批正常请求作为负样本,保持正负样本比例在 1:3 左右,避免模型过度激进。
4. 双层之间的协作机制:转交、超时与降级
4.1 硬拦截到 AI 层的转交协议
两层之间的转交不是简单的函数调用,而是一个需要精心设计的协议。硬拦截层判断为Suspect后,会把请求上下文序列化成一个研判任务,推送到 AI 层的队列里。这里有个关键选择:同步等待还是异步回调?
同步等待的优点是逻辑简单,硬拦截层拿到 AI 结果后直接返回给客户端。缺点是 AI 层一旦变慢,整个请求链路都会被拖垮。异步回调的优点是解耦,硬拦截层可以先返回一个“处理中”的状态,AI 层算完后通过回调通知。缺点是客户端需要支持异步语义,实现复杂度高。
我的选择是同步等待 + 严格超时。AI 层必须在 10ms 内返回结果,超时就直接放行并打标。为什么超时后放行而不是拦截?因为超时意味着 AI 层不可用,如果此时拦截,可能造成大面积误杀;放行虽然可能漏过一些恶意请求,但至少不影响正常用户。这个取舍在风控里叫“宁可漏放,不可误杀”,因为误杀的代价通常更高。
转交协议用protobuf定义,字段包括请求 ID、原始请求摘要、硬拦截命中规则、历史行为特征、超时时间。序列化后的数据通过tokio::sync::mpsc通道发送给 AI 层的工作线程池。通道容量设了上限,满了之后直接触发降级逻辑,不再往 AI 层送,全部按放行处理。
4.2 超时控制与降级策略的实操细节
超时控制是双层架构里最容易出问题的地方。我踩过的坑包括:AI 层某个模型加载慢导致首次请求超时、GPU 显存不足导致推理排队、网络抖动导致回调丢失。这些问题的共同点是“偶发但影响面大”,必须用降级策略兜底。
降级策略分三级:
第一级是超时降级,AI 层单次推理超过 10ms 就放弃等待,按放行处理。这个超时时间不是拍脑袋定的,而是根据 P99 延迟反推的。我统计过 AI 层在正常情况下的 P99 是 6ms,留 4ms 余量作为超时阈值,既能覆盖大部分正常请求,又不会让异常请求拖太久。
第二级是熔断降级,如果 AI 层连续 100 次请求里有超过 20 次超时,就触发熔断,接下来 30 秒内所有可疑请求直接放行,不再送 AI 层。30 秒后进入半开状态,放少量请求试探,如果恢复正常就关闭熔断。这个机制防止 AI 层故障时拖垮整个风控链路。
第三级是容量降级,AI 层的工作队列有容量上限,比如 1000 个任务。队列满了之后,新的可疑请求不再入队,直接放行。这个上限根据 AI 层的处理能力和可接受延迟来定,我实测下来 1000 个任务在 8 个工作线程下大约 200ms 能清空,不会造成明显积压。
降级期间的所有放行请求都会打上degraded标记,写入日志。事后分析时,可以重点检查这些请求里有没有恶意行为,如果有,说明降级策略需要调整,比如把超时阈值放宽或者增加 AI 层容量。
4.3 两层结果冲突时的仲裁逻辑
硬拦截层和 AI 层偶尔会出现判断冲突。比如硬拦截层标记为Suspect,但 AI 层判断恶意概率只有 0.1;或者硬拦截层放行了,但 AI 层在异步分析时发现高度可疑。这些冲突怎么处理?
我的仲裁逻辑是:硬拦截层的拦截决策优先,AI 层的放行决策优先。具体来说:
- 硬拦截层
Block:直接拦截,不送 AI 层。这是底线规则,不容挑战。 - 硬拦截层
Suspect+ AI 层高概率恶意:拦截。 - 硬拦截层
Suspect+ AI 层低概率恶意:放行,但打标观察。 - 硬拦截层
Pass+ AI 层异步发现恶意:不追溯拦截,但把该用户/设备加入观察名单,后续请求提高警惕。
这个逻辑的核心是“谁更确定听谁的”。硬拦截层的Block是基于确定性规则的,置信度最高;AI 层的概率判断是模糊的,只在硬拦截层不确定时才起决定作用。异步发现的问题不追溯,是因为追溯拦截会带来不可预期的用户体验伤害,而且可能被恶意用户利用来制造误杀。
5. 实战中踩过的坑与排查技巧
5.1 硬拦截层的典型问题与解决
问题一:规则链顺序错误导致性能下降。早期版本我把正则规则放在了频率规则前面,结果正常请求也要跑一遍正则,QPS 直接腰斩。排查方法是用tracingcrate 给每个规则加耗时统计,输出到日志里,一眼就能看出哪个规则是瓶颈。解决就是把便宜规则前置,贵规则后置。
问题二:黑名单更新导致内存暴涨。黑名单从配置中心拉取时,如果直接替换整个DashMap,旧 map 的回收会有延迟,内存峰值可能是正常值的两倍。解决办法是用arc-swap做原子替换,旧 map 在引用计数归零后自动回收,内存曲线平滑很多。
问题三:频率计数器的时间窗口漂移。用固定窗口计数时,窗口边界处的请求可能被重复计数或者漏计。比如窗口是 1 秒,第 0.9 秒和第 1.1 秒的请求可能落在同一个窗口里。解决办法是改用滑动窗口,用环形缓冲区记录每个请求的时间戳,统计时只计算窗口内的请求。代价是内存占用增加,但准确性大幅提升。
5.2 AI 研判层的典型问题与解决
问题一:模型对新型攻击模式识别率低。这是所有机器学习模型的通病。我的应对策略是“规则兜底 + 快速迭代”。硬拦截层保留一些宽泛的规则作为兜底,AI 层发现新型攻击后,快速标注样本、微调模型、上线新版本。整个流程从发现到上线控制在 24 小时内。
问题二:推理服务的内存泄漏。ONNX Runtime 在长时间运行后,如果每次推理都创建新的 session,内存会持续增长。解决办法是全局维护一个 session 池,复用 session 做推理。同时定期监控内存,超过阈值就重启推理服务。这个坑很隐蔽,因为短期内看不出来,跑几天后才会 OOM。
问题三:特征工程与线上不一致。训练时用的特征和线上推理时计算的特征如果有细微差异,模型效果会大打折扣。比如训练时参数熵值用的是自然对数,线上用了以 2 为底的对数,结果完全不同。解决办法是把特征计算逻辑封装成独立的库,训练和线上共用同一份代码,杜绝不一致。
5.3 双层协作的典型问题与解决
问题一:转交数据序列化开销大。早期用 JSON 做转交协议,序列化和反序列化占了 AI 层一半的延迟。换成 protobuf 后,序列化开销降到原来的十分之一。如果对性能要求更高,可以用rkyv做零拷贝序列化,但兼容性会差一些。
问题二:AI 层工作线程数配置不合理。线程太少会导致排队,线程太多会导致上下文切换开销。我的经验值是 CPU 核数的 1.5 倍到 2 倍。8 核机器配 12 到 16 个工作线程比较合适。这个值需要根据实际压测调整,没有万能公式。
问题三:降级期间日志暴增。降级时所有可疑请求都放行并打标,日志量可能是正常时的几十倍。如果日志直接写磁盘,可能把磁盘写满。解决办法是降级期间只记录采样日志,比如每 100 个降级请求记录 1 个,同时用内存队列缓冲,异步刷盘。
| 问题类型 | 典型现象 | 排查手段 | 解决方案 |
|---|---|---|---|
| 硬拦截性能 | QPS 骤降、P99 延迟飙升 | tracing 耗时统计 | 调整规则顺序,贵规则后置 |
| 黑名单内存 | 内存曲线锯齿状、峰值翻倍 | 监控内存 + 引用计数 | arc-swap 原子替换 |
| 频率计数 | 边界请求误判 | 对比窗口边界日志 | 滑动窗口替代固定窗口 |
| AI 模型 | 新型攻击漏判 | 样本回溯分析 | 规则兜底 + 快速迭代 |
| 推理内存 | 内存持续增长 | 长期内存监控 | session 池 + 定期重启 |
| 特征不一致 | 线上线下效果差异大 | 特征值对比 | 特征计算逻辑共用 |
| 转交开销 | AI 层延迟高 | 序列化耗时统计 | protobuf 替代 JSON |
| 降级日志 | 磁盘写满 | 日志量监控 | 采样 + 异步刷盘 |
6. 这套架构还能怎么扩展
双层架构跑稳之后,我陆续做了一些扩展,效果不错,这里分享两个方向。
第一个方向是把硬拦截层下沉到边缘节点。现在硬拦截层是集中部署的,所有流量都要回源到风控网关。如果把它下沉到 CDN 边缘或者 API 网关的 sidecar 里,可以在离用户更近的地方拦截恶意流量,减少回源带宽和延迟。Rust 编译出来的二进制很小,静态链接后只有几 MB,非常适合做 sidecar。边缘层只跑黑名单和频率规则,可疑请求再回源到中心 AI 层研判。
第二个方向是用 AI 层的数据反哺硬拦截规则。AI 层每天处理大量可疑请求,这些请求里蕴含的攻击模式可以自动挖掘成硬拦截规则。比如 AI 层发现某类请求的恶意概率持续偏高,就可以自动生成一条新的模式匹配规则,推送到硬拦截层。这样硬拦截层会越来越“聪明”,AI 层的压力也会逐渐减小。这个闭环跑起来后,AI 层的日均调用量下降了 40%,因为很多原本需要 AI 判断的请求,已经被新规则在硬拦截层拦住了。
扩展的时候要注意一点:不要让架构变得过于复杂。双层已经够用了,再加一层缓存层或者决策层,维护成本会指数级上升。我见过有人搞了四层风控,结果每层之间的数据同步和一致性问题比风控本身还难搞。架构设计的原则是“够用就好”,两层能解决的问题,不要加到三层。