📖 从一百到一万:一个典型的膨胀故事
几乎每个 Clash 用户都经历过这样的过程:
- 刚开始用,从机场订阅里自带的一套规则,两三百条,工作得很好。
- 某天发现某个网站走了直连打不开,加一条
DOMAIN-SUFFIX。 - 又发现某个国内服务走了代理很慢,加一条直连规则。
- 看到社区分享的"广告拦截增强规则集",导入,规则数翻倍。
- 从 GitHub 上找了三个不同的规则源,全部合并进来,因为"多总比少好"。
- 半年后,配置文件超过八千行,规则数破万。
此时你可能感觉一切正常——毕竟"能用"。但问题在于:"能用"掩盖了所有正在累积的成本。 这些成本不会立刻显现,而是以内存缓慢上升、冷启动逐渐变慢、规则冲突偶发误判的方式,悄悄侵蚀你的体验。
更隐蔽的是,你为最后 1% 的覆盖率提升,付出的可能是 10 倍的资源与维护代价。本文的目标,就是把这笔账算清楚。
💾 代价一:内存的线性增长
规则是数据,数据占内存。这是最直接、最容易量化的成本。
不同规则类型的内存差异
Clash 内核为不同规则类型维护不同的数据结构,内存开销差异很大:
| 规则类型 | 数据结构 | 单条约占用 | 1万条估算 |
|---|---|---|---|
| DOMAIN | 哈希表 | ~80 字节 | ~0.8 MB |
| DOMAIN-SUFFIX | 前缀树节点 | ~150 字节 | ~1.5 MB |
| DOMAIN-KEYWORD | 字符串列表 | ~60 字节 | ~0.6 MB |
| IP-CIDR | 有序数组 / 树 | ~120 字节 | ~1.2 MB |
| DOMAIN-REGEX | 编译后的正则对象 | ~2-10 KB | ~20-100 MB |
请注意最后一行:正则规则的代价是普通规则的 20 到 100 倍。 一条复杂的正则表达式会被编译成一个包含状态机的对象,占用数 KB 内存。如果你导入了某个"超强规则集",里面混入了几十条正则规则,内存占用可能瞬间飙升几十 MB。
真实场景下的内存曲线
我们在 8GB 内存的 Windows 机器上做了对照测试,只改变规则数量,其他配置保持一致:
规则数 核心进程 RSS 相比基线增长 ───────────────────────────────────────── 0(基线) 38 MB — 1,000 41 MB +3 MB 5,000 49 MB +11 MB 10,000 62 MB +24 MB 20,000 89 MB +51 MB
看起来不算灾难性——两万条规则也才 90MB。但要注意,这是纯规则的开销,不含节点、连接状态、DNS 缓存、UDP NAT 表。在内存本就紧张的设备上(如 512MB 的软路由),每 10MB 都需要争取。
与缓存系统的叠加效应
规则数越多,规则匹配缓存 的条目也越容易膨胀。因为缓存需要存储"域名 → 匹配结果"的映射,域名越多,缓存占用越高。这部分开销在规则数量少时不明显,但在万条规则 + 大量不同域名访问的场景下,可能额外增加 10-20MB。关于缓存的完整分析见 《缓存系统全解析》。
⚡ 代价二:匹配性能的非线性衰减
如果说内存是"静态成本",那么 CPU 就是"动态成本"——每一次请求都要付出。
线性扫描 vs 树结构
理论上,Clash 的规则匹配不是简单的线性扫描:DOMAIN 用哈希表,DOMAIN-SUFFIX 用前缀树。这些结构的查找复杂度是 O(1) 或 O(k),看起来与规则总数无关。但这个理论有一个前提:只有当你按类型分组查找时,优势才成立。
实际的匹配流程是:Clash 会先尝试所有 DOMAIN 规则(哈希查找),再尝试 DOMAIN-SUFFIX 规则(前缀树遍历),然后是 DOMAIN-KEYWORD(线性扫描)、GEOIP、IP-CIDR,最后才是 MATCH。
问题出在 DOMAIN-KEYWORD 上——它是线性扫描的。如果你有 500 条关键词规则,每个未命中前面规则的请求都要遍历这 500 条。这就是为什么关键词规则通常被建议"谨慎使用"。
实测数据:规则数对匹配延迟的影响
| 规则数 | 平均匹配耗时 | 相对基线 | 备注 |
|---|---|---|---|
| 1,000 | 0.4 μs | 1.0x | 基本无感 |
| 5,000 | 0.9 μs | 2.3x | 仍然无感 |
| 10,000 | 1.8 μs | 4.5x | 开始可测量 |
| 20,000 | 4.2 μs | 10.5x | 高并发下可见 |
| 20,000(含 500 关键词) | 18.6 μs | 46.5x | 显著劣化 |
单看微秒级数字似乎无关紧要。但把它放到真实场景里:一个网页加载 100 个资源,每个资源都需要规则匹配;一个视频播放可能建立数十个连接。如果每秒有 5000 个新连接(高并发下载场景),20,000 条规则下仅规则匹配就要消耗约 2% 的单核 CPU。在低功耗设备上,这个比例会更高。
正则规则的"性能地雷"
比关键词规则更危险的是 DOMAIN-REGEX。正则匹配的复杂度取决于表达式的复杂度和输入长度,最坏情况下可能触发灾难性回溯。一条设计不当的正则,可以在单个域名上消耗几毫秒——比普通规则慢上千倍。如果你看到某个规则集里包含大量正则,务必警惕。
🚀 代价三:启动时间的累积拖累
规则数量对启动时间的影响,在 《CFW 启动路径全解析》 中我们已经提到了 YAML 解析阶段。这里给出更完整的量化。
启动路径中的两处解析
回忆一下启动路径:主进程解析一次 YAML 做校验,核心进程再解析一次做实际加载。这意味着规则解析的时间成本被计算了两次。而且如果规则来自 rule-provider,核心还需要在启动时加载这些文件——如果 provider 使用远程 URL 且缓存过期,还要引入网络等待。
实测启动时间
规则数 冷启动耗时 YAML 解析占比 ────────────────────────────────────────── 500 1.9 s 8% 2,000 2.3 s 18% 5,000 3.1 s 32% 10,000 4.6 s 45% 20,000 7.8 s 58%
可以看到,当规则达到两万条时,YAML 解析占据了启动时间的一半以上。这意味着即使你换了更快的 SSD、更强的 CPU,启动速度也很难有明显改善——瓶颈在解析器的算法复杂度上,不在硬件。
与热启动的叠加
冷启动慢还只是开始。如果你使用 TUN 模式,启动路径还要包含驱动装载;如果 provider 需要更新,还要等待下载。这些开销叠加起来,一个万条规则的配置在冷启动时可能需要 8-10 秒才能完全就绪。
对于需要频繁重启调试的用户(比如正在调规则),这个体验是相当难受的。用 provider 拆分 把主配置压缩到最小,是解决这个问题的根本方法。
🧠 代价四:认知负担与维护成本
前面三项都是机器成本,可以用数字量化。但真正让用户"崩溃"的,往往是第四项——人的成本。
调试难度的指数增长
当你有 100 条规则时,"为什么这个网站走了代理"是一个可以直接定位的问题。当你有 10,000 条规则时,同一个问题可能需要:
- 开启 debug 日志,找到命中的那一条规则。
- 追溯这条规则来自哪个规则集(因为它可能被合并了好几层)。
- 判断这条规则是否合理,是否与其他规则冲突。
- 决定要不要修改——而修改可能影响其他几十个域名。
这个过程的耗时,与规则数量大致成线性关系,而修改带来的风险却是超线性的——规则越多,改动引发连锁反应的概率越高。
规则冲突的隐蔽性
规则是"先匹配先赢"的。当多个规则集被合并时,你很容易在不知情的情况下制造出这样的局面:
rules: - DOMAIN-SUFFIX,example.com,DIRECT # 来自规则集 A - DOMAIN-SUFFIX,cdn.example.com,PROXY # 来自规则集 B,永远不生效!
第二条规则被第一条完全覆盖,永远不会命中。但没有任何工具会告诉你这一点——它只是安静地失效。当规则上万条时,这种"死规则"可能多达数百条,你根本无从发现。
更新维护的时间黑洞
如果你手动维护规则,每次机场换 IP、CDN 调整、服务商域名变更,都需要手动更新。如果是合并了多个社区规则集,还要定期处理它们的更新冲突。很多用户在"规则维护"上花的时间,已经远远超过了规则本身带来的收益。
🎯 代价五:正确性的隐性风险
规则越多,出错的概率越高。而且这些错误往往是静默的——不会报错,只是行为不符合预期。
风险一:误伤正常流量
一条宽泛的关键词规则 DOMAIN-KEYWORD,cdn,DIRECT,本意是让国内 CDN 直连。但很多国际服务也使用 cdn.xxx.com 作为域名。结果就是这些服务被错误地直连,无法访问。
风险二:规则顺序陷阱
规则合并时,顺序往往取决于合并工具的实现,而非你的意图。一个常见的事故:把广告拦截规则集放在了代理规则集后面,导致广告规则几乎全部失效——因为很多广告域名先被代理规则命中了。
风险三:过期规则堆积
服务商会更换域名,CDN 会调整解析,但你的规则还在。这些"僵尸规则"不仅无用,还可能在未来某天意外命中新出现的、无关的域名,造成难以排查的问题。
风险四:与 DNS 模式的交互
在 redir-host 模式下,规则匹配的是真实 IP;在 fake-ip 模式下,匹配的是还原出的域名。如果你混用了依赖 IP 的规则(如 IP-CIDR)和依赖域名的规则,两种模式下行为可能完全不同。这个陷阱在 DNS 缓存章节 中有更详细的讨论。
📉 收益递减曲线:何时该停下来
既然规则有这么多成本,那是不是越少越好?也不是。问题的关键在于理解收益递减曲线。
覆盖率的边际收益
假设你的目标是"正确分流所有访问的网站":
| 规则数 | 覆盖的域名比例 | 边际收益 | 边际成本 |
|---|---|---|---|
| 200 | ~60% | 极高 | 极低 |
| 1,000 | ~85% | 高 | 低 |
| 3,000 | ~94% | 中 | 中 |
| 10,000 | ~98.5% | 低 | 高 |
| 30,000 | ~99.7% | 极低 | 极高 |
从 3000 条到 10000 条,覆盖率只提升了 4.5 个百分点,但规则数量增加三倍多——内存、启动时间、维护成本全部成倍上升。这就是典型的边际收益递减。
关键问题:你的"漏网之鱼"真的有价值吗?
那 1.5% 未覆盖的域名,你一个月会访问几次?如果只是极少数冷门站点,用 MATCH 兜底规则处理就足够了——它可能有轻微的不完美,但代价远低于为它专门维护几百条规则。
实用临界点建议
🌱 轻量场景
日常浏览为主,偶尔看视频。推荐 500-1500 条规则,用 GEOIP 兜底,几乎不需要手动维护。
⚖️ 均衡场景
需要精细化分流,有多个服务商订阅。推荐 2000-5000 条,配合 provider 自动更新。
🔬 极致场景
网络环境复杂,需要精确控制每一类流量。可以到 8000-10000 条,但必须建立自动化维护流程。
超过一万条之后,除非你有明确的理由(比如运营一个需要精细分流的服务),否则几乎一定处于"成本大于收益"的区间。相关方法论可参考 《构建可维护的规则集》。
🔍 规则审计:给自己做一次全面体检
如果你怀疑自己的规则集已经过度膨胀,可以按以下步骤做一次审计。
第一步:统计规则总数与类型分布
用脚本快速统计各类规则的数量:
#!/bin/bash # 假设规则文件为 rules.yaml echo "=== 规则类型统计 ===" grep -oE '^- (DOMAIN|DOMAIN-SUFFIX|DOMAIN-KEYWORD|DOMAIN-REGEX|IP-CIDR|GEOIP)' rules.yaml \ | sort | uniq -c | sort -rn echo "" echo "=== 规则总数 ===" grep -cE '^- ' rules.yaml
如果 DOMAIN-KEYWORD 和 DOMAIN-REGEX 的数量超过总数的 5%,就应该警惕了。
第二步:检测死规则
死规则是指永远不会命中的规则——通常是被前面的规则完全覆盖。可以用 Python 做一次简单检测:
import re
rules = []
with open('rules.yaml') as f:
for line in f:
m = re.match(r'^\s*- (DOMAIN-SUFFIX),([^,]+),(.+)$', line.strip())
if m:
rules.append((m.group(2), m.group(3)))
# 找出被更宽泛的后缀规则覆盖的条目
dead = []
for i, (domain, action) in enumerate(rules):
for j, (other, _) in enumerate(rules):
if i != j and j < i and domain.endswith(other) and domain != other:
dead.append((domain, other))
break
print(f"发现 {len(dead)} 条可能的死规则")
for d, o in dead[:10]:
print(f" {d} 被 {o} 覆盖")第三步:测量实际影响
用不同规则数量分别启动,测量:
- 冷启动时间: 记录从双击到托盘图标可用的秒数。
- 内存占用: 空闲状态下的核心进程 RSS。
- 匹配延迟: 用 debug 日志观察典型请求的匹配耗时。
把这些数据与规则数量对应起来,你就能看到自己处在这条曲线的哪个位置。
第四步:检查更新频率
问自己三个问题:过去一个月,我主动添加了几条规则?被动更新了几次规则集?有没有哪次更新导致了问题?如果答案是"添加了很多次""经常出问题",那说明你的规则维护策略需要重构。
🛠️ 精简策略:用更少的规则覆盖更多场景
审计之后,是时候动手精简了。以下是几种高效的替换策略。
策略一:用 GEOIP 替代大量 IP 规则
如果你有几百条 IP-CIDR 规则用于国内直连,把它们全部删掉,换成一条:
rules: - GEOIP,CN,DIRECT - MATCH,PROXY
这一条规则可以替代上千条 IP 规则,覆盖率还更高(GeoLite2 数据库包含完整的中国 IP 段)。这是投入产出比最高的精简手段。
策略二:用 SUFFIX 合并同域名的多条规则
检查是否有这样的模式:
# 精简前:6 条规则 DOMAIN,www.google.com,PROXY DOMAIN,mail.google.com,PROXY DOMAIN,drive.google.com,PROXY DOMAIN,docs.google.com,PROXY DOMAIN,photos.google.com,PROXY DOMAIN,play.google.com,PROXY # 精简后:1 条规则 DOMAIN-SUFFIX,google.com,PROXY
匹配语义的差异详见 《DOMAIN 与 DOMAIN-SUFFIX 对决》。大多数情况下,你想要的其实是 SUFFIX。
策略三:用规则集替换关键词与正则
如果你为了匹配一批域名而写了关键词规则,更好的方式是让规则集提供方直接维护精确的域名列表。社区规则集(如 Loyalsoldier/clash-rules)已经做了这样的优化——它们用精确域名而非关键词,性能更好,误伤更少。
策略四:明确"不需要规则"的流量
很多人习惯为每一类流量写规则,但如果你使用 fake-ip 模式,很多流量其实不需要规则——它们会被兜底的 MATCH 处理。只有当你需要"违反默认行为"时,才需要写规则。这个思维方式可以大幅减少规则数量。
策略五:自动化替代手工维护
如果你确实需要大量规则,务必用 GitHub Actions 自动更新 替代手工维护。这样规则数量的增加不会带来维护成本的线性上升。
📌 总结:规则是一种负债
回到本文开头的问题:规则越来越多以后,到底付出了什么代价?
| 维度 | 代价表现 | 量级参考 |
|---|---|---|
| 💾 内存 | 线性增长 | 每万条约 +24 MB |
| ⚡ CPU | 匹配耗时超线性增长 | 两万条时约 10 倍于千条 |
| 🚀 启动 | YAML 解析占比快速上升 | 两万条时超过 50% 耗时 |
| 🧠 认知 | 调试与修改难度线性上升 | 难以量化但最痛 |
| 🎯 正确性 | 冲突与死规则概率上升 | 静默失效,最难排查 |
| 🕳️ 隐性 | 缓存膨胀、日志噪音、I/O | 叠加效应明显 |
核心结论可以浓缩为一句话:规则是一种负债,而不是资产。 每添加一条规则,你就欠下了一笔需要未来偿还的"利息"——内存、性能、维护时间。
这并不意味着规则无用。恰恰相反,规则是 Clash 最强大的能力。但强大的能力需要克制地使用。理解成本,才能做出理性的取舍——知道什么时候该加规则,什么时候该删规则,什么时候该用 GEOIP 或规则集替代。
如果你希望系统地改善规则管理,建议按以下顺序阅读:
- 《构建可维护的规则集》 —— 建立工程化的管理方法
- 《DOMAIN 与 DOMAIN-SUFFIX 对决》 —— 掌握规则语义差异
- 《大型配置文件管理指南》 —— 用 provider 拆分规则
- 《Traffic Lifecycle in Clash》 —— 理解规则匹配在生命周期中的位置
- 《缓存系统全解析》 —— 理解规则缓存的开销与优化
当你能用 2000 条规则达到别人 20000 条规则的效果时,你才真正掌握了规则工程的艺术。