每秒 950 万个请求每个请求都要跑一次机器学习推理打出一个安全评分决定这个请求是攻击流量还是正常用户。这是 Cloudflare WAF Attack Score 系统每天面对的现实。在这个规模下单次推理时间哪怕节省 1 毫秒乘以请求量后都是天文数字。2024 年Cloudflare 工程团队把这套系统的端到端执行时间从1519 微秒压到了275 微秒整体快了 5.5 倍。这篇文章把他们的优化过程从头到尾拆开来讲。原文链接https://blog.cloudflare.com/making-waf-ai-models-go-brr/WAF Attack Score 是什么WAF Attack Score 是架在 Cloudflare WAF 之上的一个机器学习层。传统 WAF 靠规则匹配工作——有规则的攻击能拦没规则的绕过去了。Attack Score 的目标是识别那些绕过了现有规则的攻击覆盖 SQLiSQL 注入、XSS跨站脚本、RCE远程代码执行等主要攻击类型。每个 HTTP 请求经过五个阶段原始输入取出请求的 URI、请求体、User-Agent 等字段归一化和清洗标准化内容替换特定字符去重特征提取对处理后的内容做 tokenize产出一个固定大小的浮点张量模型推理把张量送入预训练的 TensorFlow Lite 模型输出评分1 到 99 分1 接近恶意99 接近正常其中步骤 3 和步骤 4 是延迟的大头也是这次优化的核心战场。特征提取四轮迭代从 248μs 到 21μs基线HashMap 吃掉了 62% 的时间特征提取的核心操作是ngram 查找用一个滑动窗口每次取 3 个字节查找这个三元组在词汇表中的位置然后对应的张量维度加一。原始实现用 Rust 的HashMap来做这个查找。火焰图清楚地显示了瓶颈所在操作时间占比HashMap::get61.8%Regex::replace_all18.5%两个问题一起存在查哈希表太慢正则替换也很贵。基线数字9482 字节的输入需要248μs1000 字节的输入需要28μs。第一轮用 Aho-Corasick DFA 替换 HashMapAho-Corasick 算法构建一个有限状态机在线性时间内完成多模式匹配比 HashMap 的随机访问更缓存友好。开启 DFA 模式后性能最优staticrefNORM_VOCAB_AC:AhoCorasickAhoCorasick::builder().kind(Some(AhoCorasickKind::DFA)).build([abc,def,wuq,...]).unwrap();结果长输入快了1.64 倍中等输入快了1.71 倍。有改善但还不够。第二轮直接用 Rust match让编译器做优化这个反直觉的发现是整篇文章里最有意思的地方之一把 Aho-Corasick 扔掉改用 Rust 的match语句让编译器自己决定怎么生成代码。#[inline]constfnnorm_vocab_lookup(ngram:[u8;3])-usize{matchngram{babc1,bdef2,bwuq3,// ... 数千个 ngram_0,}}编译器会把这个match编译成一个跳转表jump table配合字节级比较速度快过 Aho-Corasick DFA 和所有测试过的完美哈希方案phf、ph、quickphf。结果长输入快了2.2 倍中等输入快了2.15 倍。第三轮用 WindowedReplacer 替换 Regex回头看另一个瓶颈Regex::replace_all占用了 18.5% 的时间。它的作用其实很简单——把所有连续小写字母序列替换成#然后对替换后的字节做滑动窗口迭代。团队自己实现了一个零分配的单次遍历迭代器WindowedReplacerpubstructWindowedReplacera{window:Window,input_iter:Itera,}核心思路不先分配缓冲区存替换结果而是在迭代时实时做替换替换结果直接流向 ngram 查找全程没有额外的内存分配。结果长输入比基线快了4.87 倍中等输入快了5.09 倍——比上一轮又翻了一倍多。第四轮无分支查找彻底消灭条件跳转看汇编代码会发现match语句最终还是包含大量cmp比较指令产生很多分支。现代 CPU 对分支预测失败的惩罚很重——猜错一次要丢弃已执行的指令重新从正确路径取指。能否完全消灭分支答案是预计算偏移查找表。设计思路预计算一个三维的字节偏移表NGRAM_OFFSETS[3][256]存每个字节在其位置上的偏移量ngram 的索引直接由三个字节各自的偏移值相加得出没有任何比较再用索引在NGRAM_TENSOR_IDX数组里取出张量维度下标用get_unchecked_mut跳过边界检查消除最后一个分支来源#[inline]constfnngram_index(ngram:[u8;3])-usize{(NGRAM_OFFSETS[0][ngram[0]asusize]NGRAM_OFFSETS[1][ngram[1]asusize]NGRAM_OFFSETS[2][ngram[2]asusize])asusize}整个查找路径是完全无分支的全是直接内存访问。查找表总大小约 500KB轻松放入 L2/L3 缓存缓存命中率极高。配合每次处理 6 个 ngram 的循环展开编译器可以对内层循环做自动向量化constCHUNK_SIZE:usize6;foriin(0..chunks_max_offset).step_by(CHUNK_SIZE){forngramininput[i..iCHUNK_SIZE2].windows(3){update_tensor_with_ngram(tensor,ngram.try_into().unwrap());}}最终结果输入基线最终提升9482 字节248μs21.5μs11.5 倍1000 字节28.2μs2.3μs12.1 倍44 字节 URL1.45μs0.26μs5.7 倍91 字节 UA2.87μs0.43μs6.6 倍输入越长收益越大因为无分支设计在长循环里的优势会被充分放大。模型推理从 246μs 到 56μs特征提取优化完推理成了新的瓶颈。推理时间和输入长度无关输入已被转换成固定大小的张量稳定在246μs左右。火焰图显示最贵的操作是矩阵乘法占 42%对应 TFLite 里的三重循环朴素实现for(intb0;bn_batch;b){for(intr0;rm_rows;r){floatdot_prod0.0f;for(intc0;cm_cols;c){dot_prod*matrix_ptr**vector_in_batch;}}}第一层开启 AVX2 SIMDTFLite 内置了 AVX2 加速的矩阵乘法只需要改编译参数arguments(--copt-marchx86-64-v3)开启后矩阵乘法从朴素循环切换到 8x8 块的 AVX2 向量化实现。推理时间从 246μs 降到130μs快了1.89 倍。第二层升级 TFLite 开启 XNNPACKXNNPACK 是专门为神经网络推理设计的高度优化计算库针对不同硬件架构做了深度调优。TFLite 官方基准测试工具的数据如下配置推理时间TFLite 2.6.0无 SIMD105.6μsTFLite 2.16.1SIMD115.2μsTFLite 2.16.1SIMD XNNPACK49.1μsXNNPACK 相比基础 SIMD 实现再降低57%。综合升级 TFLite 版本、SIMD 和 XNNPACK推理时间从 246μs 降到56μs整体快了4.38 倍。缓存让代码根本不需要跑所有代码层面的优化做完之后团队想到了一个更根本的问题能不能让推理完全不执行答案来自 Zipf 定律互联网流量天然是长尾分布的——少数请求极其频繁大量请求只出现一次。URL、HTTP 头、请求体都符合这个分布规律。基于这个观察把高频出现的输入和对应的推理结果缓存起来下次相同输入直接返回缓存结果完全跳过预处理和推理。实现方案是lua-resty-mlcache基于 LRU最近最少使用策略通过 Nginx 共享内存字典在多个 Worker 进程之间共享缓存状态。实测缓存命中率约 70%——超过三分之二的请求直接从缓存拿结果预处理和推理完全不执行。三阶段上线每天节省 32 年为了保证系统稳定性优化分三个阶段分批上线阶段改动执行时间降幅阶段一TFLite 开启 SIMD1519 → 884μs-41.8%阶段二升级 TFLite XNNPACK 预处理优化884 → 552μs-40.8%阶段三LRU 缓存552 → 275μs-50.2%总计1519 → 275μs-81.9%快 5.5 倍用一个更直观的换算来说明这个改善的量级Cloudflare WAF ML 平均每秒处理 950 万个请求每个请求节省 1244 微秒等价于每天节省约 32 年的处理时间。这还不包括去年 Bot Management 优化带来的每天节省 65 年——两者加起来每天节省近 100 年的处理时间。优化路径的启示这篇文章描述的不只是一次性能优化而是一套在真实生产场景里行之有效的方法论。第一用 profiler 说话不用直觉猜。火焰图显示 HashMap 占了 62%才知道从哪里下手矩阵乘法占 42%才知道推理侧的核心问题在哪里。第二优化有层次每层逻辑不同。无分支查找是 CPU 微架构层面的优化SIMD 是指令集层面的优化XNNPACK 是算法层面的优化LRU 缓存是系统设计层面的优化——四个层次叠加才得到 5.5 倍的整体提升。第三有时候不执行比执行得更快更有效。Amdahl 定律告诉我们优化部分代码的收益是有上限的。LRU 缓存让 70% 的请求完全绕过了所有计算路径这比任何代码层面的优化都更彻底。第四编译器有时比你聪明。用match替换精心调优的 Aho-Corasick DFA结果反而更快——因为编译器把match编译成了跳转表这个细节是手工优化很难意识到的。