土法炼钢 · 系统与基础设施

【HAProxy 数据面】ACL / 规则引擎:求值顺序、副作用与 stick-table 耦合

文章导航

分类入口
networkproxy
标签入口
#haproxy#acl#use_backend#http-request#stick-table#content-switching#3.4.3

目录

HTX 与 HTTP 路径 之后,请求已经进入可做条件判断与改写的阶段。现场里大量「ACL 写了却不生效」并不是语法错,而是求值相位错了:连接期规则读不到 Host,或 track-sc 排在 reject 后面导致计数永远为空。本文钉规则引擎在数据面的位置与副作用,不写 ACL fetch / matcher 菜谱(见 network/56)。

本文是「HAProxy / 数据面代理内核」系列第 6 篇(共 14 篇)。→ 系列目录

篇目 核心内容
第 5 篇 · HTX / HTTP 内部表示与改写落点
第 6 篇 · ACL / 规则引擎 求值顺序与副作用
第 7 篇 · Backend / LB 选主与队列

上一篇HTX 与 HTTP 路径 · 下一篇Backend / server / LB

版本锚定:HAProxy 3.4.3 Starter Guide §3.4.3–3.4.4(ACL / content switching)、Configuration Manualtcp-request connection / tcp-request content / http-request / use_backend;源码对照 tag v3.4.3。无伪造规则命中日志。

本篇不展开:ACL 关键字大全、map 文件运维、Lua/SPOE 扩展。选中 backend 之后的算法与队列见第 7 篇;表容量与 peers 见 第 8 篇


一、规则引擎在路径上的位置

HAProxy 的「条件」不是单独的解释器进程,而是挂在既有会话路径上的规则列表。Starter Guide §3.4.3:条件由多个 ACL 经 AND / OR / NOT 组合;每个 ACL 由 sample fetch、可选 converter、pattern 与匹配方法构成;OR 已真或 AND 已假时短路,后续子句不求值。ACL 与 map 共享同一套内部结构,差别只是 ACL 返回 found / not found。

对数据面排障更关键的是:同一条连接会穿过多个规则面(ruleset),每个面有自己的可读样本与可执行动作:

flowchart TD
  accept["accept FD"] --> rqcon["tcp-request connection"]
  rqcon --> rqcont["tcp-request content"]
  rqcont --> http_req["http-request rules"]
  http_req --> use_be["use_backend / default_backend"]
  use_be --> be_rules["backend http-request"]
  be_rules --> lb["LB / stick / queue"]
相位 典型可读信息 典型动作
tcp-request connection 源地址、端口、连接速率等;缓冲尚未分配 accept / reject / track-sc* / PROXY 期望
tcp-request content 随缓冲填充逐步可见的 TCP/HTTP 内容 accept / reject / switch-mode / track-sc*
frontend http-request 完整 HTTP 请求(mode http) deny / allow / set-header / track-sc* / 变量
use_backend 与 ACL 相同的 L7 样本 选定 backend 名
backend http-request 已进入该 backend 后的请求 再一层改写 / 拒绝

Configuration Manualtcp-request connection:规则按声明顺序求值;无匹配且无规则时默认 accept。对 http-request:规则在 frontend / listen / backend 中按声明顺序求值;条件在动作执行前求值,动作只执行一次。defaults(命名段)中的规则先于所属 proxy 段。


二、use_backend:内容交换是选路,不是「再跑一遍 ACL 百科」

Starter Guide §3.4.4 把 content-based switching 说成:连接或请求到达 frontend,携带的信息经处理后,用 ACL 条件决定由哪个 backend 处理。常见模式是 Host / path 分流;也可让不同类流量走不同 LB 算法,或用 per-backend 连接限额做租户隔离。

工程上要记住三点:

  1. use_backend 列表是有序的:通常「先命中先生效」,其余不再尝试;未命中则落到 default_backend(若配置)。这与「所有 ACL 同时成立再投票」不同。
  2. 动态 backend 名可以绕过显式 ACL:用 map / sample 直接生成 backend 名。文档称此类配置有过大规模 backend 数量的生产报告;本篇不复述数字,只强调机制——选路成本从「ACL 链」转到「名字解析与 backend 对象查找」。
  3. 选路成功不等于上游可服务:backend 选定后仍可能因无可用 server、队列满、timeout queue 返回 503。归因要分轴,见 第 7 篇

对照 Envoy Router:Envoy 终局给出 cluster 名;HAProxy 终局给出 backend 名。二者都是「名字选定后还有 LB / 池 / 健康集合」——不要在 frontend ACL 层假装已经完成上游可用性判断。


三、动作有副作用:顺序即语义

规则引擎最容易踩坑的地方,不是 matcher 写错,而是前一条动作改写了后一条条件所依赖的状态。手册对 http-request / tcp-request connection 明确写了:条件在动作前求值;若多条动作依赖同一条件,第一条成功改变条件求值结果的动作,足以使后续同条件动作不再触发。官方用「从多个来源给空变量赋值」作为合法用法——反过来说,限流与放行的顺序一旦写反,就会得到完全不同的计数语义。

经典对照(语义示意,非菜谱):

# A:先 reject 再 track → 被拒连接可能从未计入 sc0
tcp-request connection reject if { src_conn_rate gt 10 }
tcp-request connection track-sc0 src

# B:先 track 再按 sticky counter 拒绝 → 速率窗口包含当前连接
tcp-request connection track-sc0 src
tcp-request connection reject if { sc0_conn_rate gt 10 }

手册示例正是围绕「白名单 accept、是否在 reject 前 track」展开:先 reject 而不 track,可以把滥用源的连接速率「盖帽」为不把被拒连接算进去;先 track 再按 sc0_conn_rate reject,则滥用源在减速之前会一直被挡住。两种都合法,取决于你要的失败模式。

同理,http-request deny 若写在 track-sc / set-var 之前,后续排障会看到「表是空的但 403 很多」——不是 stick-table 坏了,是动作顺序切断了记账路径。

副作用还包括:


四、与 stick-table 限流 / 标记的耦合

ACL 条件可以读取 sticky counter(sc0_* / sc1_* / sc2_*)与表上 store 的速率、计数、GPC 等;动作侧用 track-sc*sc-inc-gpc* 等写入。Starter Guide §3.4.5:tracking 最多可同时跟踪 3 个来自不同表的 key,且不必启用 stickiness。stickiness 另有最多 8 路并行学习源的叙述——两者不要混成「一个数字」。

耦合点可以画成:

步骤 机制 失败时表现
声明表 stick-table type … size … expire … store … 类型/容量错误 → 配置拒载或运行期插不进
绑定计数器 track-sc0 src(或 content / http-request 相位) 相位过早 → fetch 尚不可用;过晚 → 已被 deny
ACL 读表 sc0_http_req_rate gt N 读错 sc 槽或错表 → 条件恒假/恒真
动作 deny / tarpit / silent-drop / 标 GPC 只标不拦 → 日志有滥用、流量仍过

手册示例「frontend 用 gpc0 拒绝、backend 用 http_req_rate 检测并 sc0_inc_gpc0(http) 标记」说明:检测与拦截可以拆在不同 proxy,靠表名跨段共享。这把「ACL 菜谱」上升成分布式(同进程内)状态机——容量、过期、peers 复制则交给第 8 篇。

限流类条件还有一个数据面含义:它们把共享内存中的计数拉进每请求热路径。nbthread 下表在进程内共享(见第 8 篇);若误用历史 nbproc,每进程一张表,ACL 里的「全局限流」会静默变成「每进程限流」。配置看起来「ACL 都对」,语义已经错了。


五、短路、命名 ACL 与可观测边界

Starter Guide §3.4.3:OR 已真 / AND 已假时短路。含义是:

可观测上,本系列约定:无本机实跑则不粘贴伪造 show acl / 日志。排障时应先确认相位(connection vs content vs http-request),再确认动作是否在读表之前执行,最后才怀疑 matcher。stats 上的 deny / req_denied 类计数能说明「被规则挡住」,不能单独证明「是哪一条」——需要配置顺序与(若有)日志格式中的终止状态一起看,细节见后续排障篇。

相对 Envoy HTTP filter 链:Envoy 用 filter 顺序与 StopIteration 表达副作用;HAProxy 用 ruleset 声明顺序与 accept/deny/track 表达。二者都是「顺序即控制流」,不是「声明式策略自动收敛」。


六、内容检查延迟与「规则又跑了一遍」

tcp-request content 与 HTTP 下部分规则不是「只在连接建立瞬间跑一次」。手册写明:在 TCP content inspection 阶段,随着请求内容更新,ACL 规则会反复求值,直到出现 accept / reject / switch-mode,或检查延迟到期仍无匹配。这对「半包到达时 Host 还不完整」的场景是必要的,也带来两个数据面后果:

  1. 同一条 track-sc 不要假设只触发一次记账语义而不读文档——设计限流时应以 sticky counter 的速率窗口为准,而不是「规则执行次数」。
  2. 昂贵 ACL(大 regex、多层 map)在缓冲填充过程中可能被多次触及,直到 accept。把重计算放进「必须等完整头部才有意义」的条件里,并合理设置 inspect delay,否则会在慢客户端上放大 CPU。

HTTP 路径上,frontend 的 http-request 在请求头可用后按声明顺序执行;随后才是 use_backend。把「依赖改写后的 path」的选路写在改写动作之前,会稳定地选错 backend——这是顺序 bug,不是 fetch 名记错。

命名 defaults 段若定义了 http-request / tcp-request 规则,手册要求:该 defaults 不能同时被「仅 frontend 能力」与「仅 backend 能力」的 proxy 共用,listen 尤其容易踩 Ambiguity。机制含义是:规则继承不是无结构的文本粘贴,而是带能力集约束的合成。


七、谱系、争论与开放问题

谱系:内容交换与条件转发是经典 L7 LB 能力;HAProxy 把 ACL 建成与 map 同构的模式匹配核,再叠 sticky counter,使「限流 / 封禁 / 选路」共用同一套 sample 管道(Starter Guide §3.4.2–3.4.5)。这与 API 驱动代理里「独立 RateLimit filter + Router」的拆分不同——HAProxy 更强调同进程规则列表 + 共享表

争论:配置文件里堆命名 ACL 是否可维护。官方文本已偏向「过多命名 ACL 难排障」;对立面是运维希望复用与单测式片段。没有统一正确答案,但数据面事实是:复用越多,跨相位引用越容易看错顺序

开放问题:规则命中路径的一等可观测(「哪一条 http-request 终止了事务」)在开源工具链里仍弱于「自己在日志 format 里打标」。策略即代码(GitOps)与 Runtime 改 map/ACL 的边界,放到 Runtime API 与选型篇讨论。


八、参考资料

规范 / 官方文档(A)

源码(A)

站内

实验台账


九、小结

  1. ACL 是挂在会话路径上的条件核;真正决定命运的是各相位 ruleset 的声明顺序与动作。
  2. use_backend 是有序内容交换;选中名字之后仍有 LB / 队列 / 健康集合。
  3. trackreject/deny 的相对顺序改变限流语义;副作用是一等公民。
  4. 与 stick-table 的耦合经 sticky counter 完成;跨 frontend/backend 标记可行,但受表共享模型(nbthread vs nbproc)约束。
  5. content 规则可随缓冲填充反复求值;昂贵条件与 inspect delay 属于同一条性能轴。

系列目录 · 上一篇:HTX 与 HTTP 路径 · 下一篇:Backend / server / LB

读完这篇,下一步读什么

优先读同系列或同问题的下一篇,把单篇消费变成主题集群。


By .