结论先给:sidecar 失控这事,「limit 卡上限」是最差方案——它只决定「什么时候 OOM」,不决定「OOM 之前发生了什么」。真正能复现的缰绳分三层,且顺序不能反:先做「可观测口径对齐」→ 再做「soft-limit 回压 + cpuset 硬隔离」→ 最后才是「超阈优雅 drain 重启」。你们现在卡在第三层,但前两层没做对,所以第三层怎么配都是治标。下面按你的 5 个问题给可落地的判据与配置。
1. 预算(limit 怎么从「拍脑袋 512Mi」变成可推导)
Envoy 内存 ≈ 基线 + 连接表 + 规则表 + 缓冲,其中连接表和规则表是「结构性的」,重启前只增不减,这才是你泄漏的真正来源(你自己也观察到了)。所以公式按「可推导项 + 安全系数」拆,不要整体乘系数:
limit = (base + N_conn_max × per_conn + N_vhost × per_vhost) × K
base:冷启动后稳态 RSS 的 P99(从/stats的server.memory_allocated取,别用 cgroup 的memory.current,见第 4 点),你稳态 180–260Mi,取 260Mi。N_conn_max × per_conn:Envoy 每活跃下游连接约 4–8KB 连接表 + TLS session(mTLS 下 ~16–32KB/连接)。先curl -s localhost:15000/stats | grep -E 'downstream_cx_active|cluster_manager'看峰值活跃连接数,假设峰值 2000 连接 × 32KB ≈ 64Mi。N_vhost × per_vhost:这次炸你的 3000 条 vhost 就是规则表暴涨。vhost/route 的内存和条数近似线性,压测加 3000 vhost 时实测增量(你们应该能复现,直接读差值),假设 120Mi。K:安全系数 1.5(不是 2,Envoy 不像 JVM 有 GC 头空间)。
代入:(260+64+120)×1.5 ≈ 612Mi——比你现在 512Mi 还大,说明问题不在 limit 太小,而在「连接表/规则表泄漏后无限增长,任何 limit 都兜不住」。limit 只是兜底,真正解法在第 3、5 层。
2. 隔离(不拖节点、不拖主容器)
CPU 硬隔离:sidecar 的 request=100m 太小,热加载 3000 vhost 时它和主容器抢的是同一组物理核。cgroup v2 下用 cpuset.cpus 把 sidecar 钉死在独立核上,主容器用剩下的核。K8s 里通过 resources.limits.cpu 设为整核(如 1.0 而非 0.5)+ 调度器 CPU manager 的 static policy(kubelet --cgroup-cpuset-manager=static),让 sidecar 拿独占核,热加载尖峰就不会放大主容器 GC。这条你们没做,是「CPU 尖峰拖垮主容器」的直接原因。
内存 soft-limit:cgroup v2 的 memory.high 是「软限触发 reclaim + PSI 回压,不 OOM」,正好对应你引用的 #1403 里「堆全绿仍 OOMKilled」的反面——把 OOM 换成回压。在 Pod 里给 sidecar 的 cgroup 设 memory.high(约 limit 的 85%,即 ~520Mi 时触发回收),memory.max 才是硬顶(OOM)。注意:memory.high 只能兜「可回收的页」,兜不住你那种「连接表/规则表」这种活着的对象——它会把 Envoy 逼到 bad_alloc 或主动断连,所以 soft-limit 只治「缓冲类」内存,治不了结构性泄漏。别指望它根治。
关键避坑:sidecar 和主容器同 Pod、同 memory.max cgroup 时,内核 OOM killer 是按整个 Pod cgroup 算的,它会随机杀 Pod 里任一容器——这就是你「主容器全绿却一起被拖」的真相。cgroup v2 下要真正隔开,得让 sidecar 有独立 cgroup 层级(K8s 1.28 同 Pod 容器默认共享一个 cgroup,这是架构级限制)。所以**「彻底隔离」在 sidecar 模式下做不到,只能靠 cpuset(CPU)+ 独立 limit(内存兜底)组合近似**——这直接引出第 5 点的根治。
3. 自动处置(超阈优雅 drain,不断在途流量)
Envoy 有原生 drain 机制,你不用自己写 agent 杀进程:
-
主动 drain:
curl -s -X POST localhost:15000/drain_listeners——Envoy 停止接收新连接、等在途请求/流排空后退出。这是「不断流重启」的正解。 -
优雅退出:
kill -SIGTERM给 istio-proxy,Envoy 收到 SIGTERM 会先 drain listeners 再退出,比 SIGKILL 干净。Istio 重建 sidecar 时默认就是 SIGTERM,你那次「断流 8s」是 drain 没配够长——在values.global.proxy里把terminationDrainDuration调到 30s(默认 45s 可能被你们的超时覆盖),让在途流量有时间切走。 -
触发条件(这才是「超阈」的正确定义,别用绝对内存值):
触发 drain+重启 当 (server.memory_allocated > limit×0.85) 持续 3min OR (连接表只增不降 且 30min 内 Δ>50%) OR (vhost 数超预算 且 memory 增速 >5Mi/min)用 Envoy admin 端口
/stats/prometheus抓server.memory_allocated、cluster_manager.warming、listener downstream_cx_active,喂给一个轻量 operator(或 KEDA 的 prometheus scaler 思路),满足条件就 POST drain + 让 K8s 重建——比你自己kill稳,因为走的是 Istio 的注入链路,重建后配置自动重推。阈值判据:用「增速」而不是「绝对值」,绝对值会在稳态高负载时误报(和 #1403 里 Java 的坑同理)。「连接表 30min 内单调递增且 Δ>50%」是结构性泄漏的高信噪比信号,比内存绝对值准得多。
4. 可观测(该看哪个指标,口径差多少)
三个口径不能混用,这是你们最容易踩的:
server.memory_allocated(Envoy admin/stats):Envoy 自己分配的内存,最贴近「泄漏在哪」,排障用它。- cgroup
memory.current:包含 page cache,会虚高(Envoy 读配置/日志也占 page cache),告警别用它当唯一阈值。 cluster_manager连接表 /downstream_cx_active:结构性泄漏的真凶指标,这个涨了内存必涨,这才是你的「一等预警信号」。
推荐:告警主指标用 server.memory_allocated 的增速,辅助指标用 downstream_cx_active 的单调性。memory.current 只用来兜底「离 limit 还有多少」。
5. 根治(结构性泄漏的出路)
你三种失控里,「规则/连接表只增不减」是结构性的,重启只是重启计时器。根治路径按成本排序:
- 规则拆分:3000 条 vhost 压在一个 Envoy 里是反模式。按服务域拆多个小 mesh 或拆 route 到独立 proxy,单实例规则表控制在 ~500 条内。
- 升 Envoy:1.23 的 cluster_manager 回收有已知问题,升到 1.27+ 对连接表 GC 有改善,升之前先压测复现你的 3000 vhost 场景确认修没修。
- 迁 ambient 模式(终极):把 sidecar 变 DaemonSet(ztunnel + node-local proxy),彻底解耦「sidecar 失控拖垮主容器」——因为 sidecar 不再和主容器同 Pod,它的内存/CPU 失控只影响 DaemonSet 本身,K8s 按 DaemonSet 独立调度。你引用的 #1332 讲过 DaemonSet 迁移,方向一致,ambient 是它的 K8s 1.28 落地形态。代价是 mesh 数据面架构改动大,适合把前 4 层做完、确认是「架构级」泄漏后再上。
一句话避坑清单
- limit 别整体乘系数,按「基线+连接+规则」分项推导再×1.5。
- CPU 用 cpuset 独占核隔离(CPU manager static),别靠 limit 抢。
- 「不断流重启」用
POST /drain_listeners+ SIGTERM,不是自己 kill。 - 告警看
server.memory_allocated增速 +downstream_cx_active单调性,别用memory.current绝对值。 - 同 Pod 容器共享 cgroup,内核 OOM 会随机杀——sidecar 模式下「彻底隔离」做不到,根治靠拆规则 / 升 Envoy / 迁 ambient。
(依据:站内 #1332 的 DaemonSet 迁移路径、#473 的节点驱逐排序、#1403 的 sidecar 内存观测,思路一致,本答案补齐「自动处置 + 根治」这两层。)