K8s 源码:APIServer 限流四件套——MaxInFlight、Client、EventRateLimit、APF 全解析

前言:那个把 APIServer 写崩的 controller

凌晨 3 点告警:集群 APIServer 全部 503,kubectl 全部超时。

切到 dashboard 一看:APIServer 的 apiserver_request_total{code="503"} 飙到天上去了。

最后排查到根因——同事新部署的一个自定义 controller 有个死循环 bug:每秒疯狂创建 50+ event,几个副本一起 ~8000 QPS。我们集群关了 APF(保留默认的 --max-requests-inflight=400),event 写请求把池子占满,list/watch 全部排不进队,全部超时

最讽刺的是:event 本来就不重要,但因为没有优先级隔离,它把核心的 list/watch 都饿死了。

这次事故后我们做了三件事:

  1. 启用 APF(K8s 1.20+ 默认 GA,但生产集群居然有一半没启用
  2. 配置 EventRateLimit 准入控制器兜底
  3. 跑去啃了一遍 APIServer 限流的源码——这篇就是那次啃源码的笔记

本节重点

  • MaxInFlightLimit:chan 信号量实现的整体限流
  • Client 限流:client-go 默认 QPS=5 的来历
  • EventRateLimit:专门限 event 的令牌桶
  • APF:按优先级+公平队列的现代方案(生产必备)

一、四种限流策略全景图

       客户端 (kubectl / controller)
              │
              │ ① Client 限流 (client-go QPS=5, Burst=10)
              ▼
       ┌──────────────────────────┐
       │  APIServer HTTP 入口      │
       └──────────┬───────────────┘
                  ▼
       ┌──────────────────────────────────────────────┐
       │ ② 整体限流(二选一)                          │
       │   ├─ MaxInFlightLimit (默认 / 老方案)         │
       │   │   ├─ --max-requests-inflight=400          │
       │   │   └─ --max-mutating-requests-inflight=200 │
       │   └─ APF / PriorityAndFairness (1.20+ GA)     │
       │       ├─ FlowSchema:分类                      │
       │       └─ PriorityLevelConfiguration:并发预算   │
       └──────────┬───────────────────────────────────┘
                  ▼
       ┌──────────────────────────┐
       │ ③ 准入控制层               │
       │   └─ EventRateLimit       │  ← 只限 event
       └──────────┬───────────────┘
                  ▼
            进入业务处理(认证/鉴权/storage)

💡 关键认知:MaxInFlight 和 APF 是互斥的——开了 APF,MaxInFlight 就关闭。EventRateLimit 是个独立的兜底,专门给 event 用。


二、MaxInFlightLimit:用 chan 做信号量

2.1 它解决什么问题?

APIServer 默认有两个旗子:

  • --max-requests-inflight=400:只读请求并发上限
  • --max-mutating-requests-inflight=200:修改请求并发上限

为什么分开? 因为修改请求要写 etcd,比只读贵得多。如果不区分,一波写请求把池子占满,整个集群的 watch 都断了。

2.2 入口:DefaultBuildHandlerChain

位置:staging/src/k8s.io/apiserver/pkg/server/config.go

if c.FlowControl != nil {
    // APF 模式
    requestWorkEstimator := flowcontrolrequest.NewWorkEstimator(c.StorageObjectCountTracker.Get)
    handler = filterlatency.TrackCompleted(handler)
    handler = genericfilters.WithPriorityAndFairness(handler, c.LongRunningFunc, c.FlowControl, requestWorkEstimator)
    handler = filterlatency.TrackStarted(handler, "priorityandfairness")
} else {
    // MaxInFlight 模式
    handler = genericfilters.WithMaxInFlightLimit(handler, c.MaxRequestsInFlight, c.MaxMutatingRequestsInFlight, c.LongRunningFunc)
}

二选一:

  • c.FlowControl != nil → 走 APF
  • 否则 → 走 MaxInFlight

💡 怎么判断当前集群开没开 APF?

kubectl get --raw '/metrics' | grep apiserver_flowcontrol_request_concurrency_limit
# 有输出 = APF 开了
# 无输出 = MaxInFlight 模式

2.3 注册 watermark 维护 hook

位置同上,GenericAPIServer.New 里:

if c.FlowControl != nil {
    const priorityAndFairnessFilterHookName = "priority-and-fairness-filter"
    if !s.isPostStartHookRegistered(priorityAndFairnessFilterHookName) {
        err := s.AddPostStartHook(priorityAndFairnessFilterHookName, func(context PostStartHookContext) error {
            genericfilters.StartPriorityAndFairnessWatermarkMaintenance(context.StopCh)
            return nil
        })
    }
} else {
    const maxInFlightFilterHookName = "max-in-flight-filter"
    if !s.isPostStartHookRegistered(maxInFlightFilterHookName) {
        err := s.AddPostStartHook(maxInFlightFilterHookName, func(context PostStartHookContext) error {
            genericfilters.StartMaxInFlightWatermarkMaintenance(context.StopCh)
            return nil
        })
    }
}

这是给 metrics 用的 watermark 维护协程——周期性把"当前最高并发数"(watermark)上报,让 Prometheus 能看到。

2.4 WithMaxInFlightLimit 核心逻辑

位置:staging/src/k8s.io/apiserver/pkg/server/filters/maxinflight.go

① 短路:limit=0 时不限流
if nonMutatingLimit == 0 && mutatingLimit == 0 {
    return handler
}

🚨 生产坑:有运维同事为了"图省事"把 --max-requests-inflight=0——以为是不限流,结果就是真的不限流,APIServer 任由 OOM。0 不是无限大,0 是关闭限流。要无限大用一个超大数(如 10000)。

② 信号量 = 带缓冲 chan
var nonMutatingChan chan bool
var mutatingChan chan bool
if nonMutatingLimit != 0 {
    nonMutatingChan = make(chan bool, nonMutatingLimit)
    watermark.readOnlyObserver.SetX1(float64(nonMutatingLimit))
}
if mutatingLimit != 0 {
    mutatingChan = make(chan bool, mutatingLimit)
    watermark.mutatingObserver.SetX1(float64(mutatingLimit))
}

核心技巧:用一个长度 = limit 的带缓冲 chan 当信号量。

  • 来一个请求 → chan <- true(占一个槽)
  • 请求完成 → <-chan(释放一个槽)
  • chan 满了 → 写入 default 分支 → 返回 429

💡 为什么用 chan 而不用计数器+锁? chan 在 Go runtime 里是 lockfree 的(无竞争场景)、自带 FIFO 语义、select default 天然支持非阻塞写——比手写锁简单且更高效。这是个值得记下来的 Go 并发模式。

③ 跳过长连接请求
// Skip tracking long running events.
if longRunningRequestCheck != nil && longRunningRequestCheck(r, requestInfo) {
    handler.ServeHTTP(w, r)
    return
}

BasicLongRunningRequestCheck 判断的"长连接请求"包括:

func BasicLongRunningRequestCheck(longRunningVerbs, longRunningSubresources sets.String) apirequest.LongRunningRequestCheck {
    return func(r *http.Request, requestInfo *apirequest.RequestInfo) bool {
        if longRunningVerbs.Has(requestInfo.Verb) {        // watch
            return true
        }
        if requestInfo.IsResourceRequest && longRunningSubresources.Has(requestInfo.Subresource) {  // exec/log/portforward
            return true
        }
        if !requestInfo.IsResourceRequest && strings.HasPrefix(requestInfo.Path, "/debug/pprof/") {
            return true
        }
        return false
    }
}

主要是 watch、exec、log、portforward、pprof——这些请求会长时间挂着,如果纳入限流,几个 watch 就把池子占满了。

🚨 踩坑:但这也意味着 watch 不受 MaxInFlight 控制——如果有客户端疯狂建 watch 不关,APIServer 内存会涨爆(每个 watch ~几 MB)。APF 修复了这点——watch 也纳入 SeatCount 计算。

④ 选择 chan:只读 or 修改
var c chan bool
isMutatingRequest := !nonMutatingRequestVerbs.Has(requestInfo.Verb)
if isMutatingRequest {
    c = mutatingChan
} else {
    c = nonMutatingChan
}

nonMutatingRequestVerbs 是个固定集合:{"get", "list", "watch"}——其他 verb(create/update/patch/delete/…)全算 mutating。

⑤ 核心:select + default 非阻塞写
select {
case c <- true:
    // 进入处理
    if isMutatingRequest {
        watermark.recordMutating(len(c))
    } else {
        watermark.recordReadOnly(len(c))
    }
    defer func() {
        <-c  // 处理完释放槽位
        if isMutatingRequest {
            watermark.recordMutating(len(c))
        } else {
            watermark.recordReadOnly(len(c))
        }
    }()
    handler.ServeHTTP(w, r)
default:
    // 队列已满,走 429 分支
    ...
}

精髓select + default 实现了非阻塞的信号量获取——拿不到立刻走 default 分支,绝不会卡住请求。

⑥ 队列满了:system:masters 永远放行
default:
    if currUser, ok := apirequest.UserFrom(ctx); ok {
        for _, group := range currUser.GetGroups() {
            if group == user.SystemPrivilegedGroup {
                handler.ServeHTTP(w, r)
                return
            }
        }
    }

SystemPrivilegedGroup = "system:masters",对应 ClusterRole cluster-admin

💡 为什么 system:masters 不限流? 集群内部组件(controller-manager、scheduler)、管理员 kubeconfig 都属于这个组——如果连管理员都被限流,集群挂了你都救不回来

🚨 反过来的坑:千万别给业务应用绑 cluster-admin。我见过把 controller 跑成 cluster-admin 的——结果它疯狂请求时绕过限流把 APIServer 打爆,限流形同虚设。最小权限原则永远不过时。

⑦ 真的满了:返回 429
if isMutatingRequest {
    metrics.DroppedRequests.WithContext(ctx).WithLabelValues(metrics.MutatingKind).Inc()
} else {
    metrics.DroppedRequests.WithContext(ctx).WithLabelValues(metrics.ReadOnlyKind).Inc()
}
metrics.RecordRequestTermination(r, requestInfo, metrics.APIServerComponent, http.StatusTooManyRequests)
tooManyRequests(r, w)

tooManyRequests 内部做了三件事:

  1. 设置 Retry-After: 1 响应头
  2. HTTP 状态码 429
  3. 返回 “Too many requests” 消息体

💡 客户端怎么处理 429? client-go 看到 429 + Retry-After 头会自动重试(exponential backoff)——所以业务代码大部分时候感觉不到限流。但指标里能看到rest_client_requests_total{code="429"}

2.5 MaxInFlightLimit 的硬伤

  • 没有优先级:低优先级的 event 和高优先级的 leader election 抢同一个池子
  • 没有公平性:一个发疯的 client 能占完所有槽位
  • 粒度太粗:只区分读写,不区分用户/资源
  • watch 不受控:长连接全部豁免

这就是为什么社区搞了 APF。


三、Client 限流:那个让你 list 半天的 QPS=5

3.1 client-go 默认值的来历

// staging/src/k8s.io/client-go/rest/config.go
const (
    DefaultQPS   = 5
    DefaultBurst = 10
)

你没看错——client-go 默认 QPS 只有 5,Burst 10。这是个 2014 年定下的"保守"值,到现在都没改。

3.2 怎么影响你的代码

config, _ := clientcmd.BuildConfigFromFlags("", kubeconfig)
// 默认 QPS=5, Burst=10
clientset, _ := kubernetes.NewForConfig(config)

// list 1000 个 namespace 下的 pod
for _, ns := range namespaces {
    clientset.CoreV1().Pods(ns).List(ctx, metav1.ListOptions{})
}
// 跑得贼慢——客户端自己限速到 5 QPS

🚨 生产踩坑:写过一个 controller 同步几千个 namespace 的资源,跑得贼慢,CPU 也没满、APIServer 也不忙——抓 pprof 一看大量 goroutine 卡在 rate.Wait根因:client-go 默认 QPS=5。

修复

config.QPS = 100
config.Burst = 200

3.3 实现:golang.org/x/time/rate(令牌桶)

client-go 用的是标准库 golang.org/x/time/rate 的令牌桶:

type RateLimiter interface {
    TryAccept() bool
    Accept()  // 阻塞直到拿到 token
    Stop()
    QPS() float32
    Wait(ctx context.Context) error
}

每次请求前 Accept() 阻塞等令牌——所以客户端限流的本质是延迟,不是拒绝

3.4 Client 限流的局限

  • 客户端自己管自己:管理员无法控制
  • 作弊太容易:直接改代码绕过
  • 粒度只到 client 实例:一个 process 起 10 个 client 就×10

所以 client 限流只是"礼貌",不是"安全网"。真正的限流必须在 server 端。


四、EventRateLimit:专门为 event 设计的兜底

4.1 它解决什么问题?

event 是 K8s 里最容易爆炸的资源——一个出错的 controller 一秒可以产 100+ event。MaxInFlight 不区分资源,event 会直接把 mutating 池占满。

EventRateLimit 在 1.13+ 作为 admission plugin 出现,专门给 event 加层令牌桶。

4.2 启用方式

kube-apiserver \
  --enable-admission-plugins=EventRateLimit \
  --admission-control-config-file=/etc/kubernetes/admission-config.yaml

admission-config.yaml:

apiVersion: apiserver.config.k8s.io/v1
kind: AdmissionConfiguration
plugins:
  - name: EventRateLimit
    path: /etc/kubernetes/event-config.yaml

event-config.yaml:

apiVersion: eventratelimit.admission.k8s.io/v1alpha1
kind: Configuration
limits:
  - type: Namespace
    qps: 50
    burst: 100
    cacheSize: 2000
  - type: User
    qps: 10
    burst: 50
  - type: Server
    qps: 5000
    burst: 20000

四种 limit type:

  • Server:全局,所有 event 共享一个桶
  • Namespace:每个 namespace 一个桶
  • User:每个用户一个桶
  • SourceAndObject:每个 (source, involvedObject) 一个桶

4.3 原理

每种 limit 都用独立的令牌桶

来一个 event create 请求
       │
       ▼
   遍历每个匹配的 limit
       │
       ├─ Server 桶:能拿到 token?
       ├─ Namespace 桶:能拿到 token?
       └─ User 桶:能拿到 token?
       │
   全部 ok → 放行
   任意一个失败 → 返回 429

注意是"全部匹配"——任何一个桶满了都拒绝。

4.4 优缺点

✅ 实现简单、可分级
✅ 在 admission 层拦截,比 APF 更靠后但更精准

只限 event,其他资源不管
❌ 通过 webhook/admission 实现,只能拦截 mutating 请求
❌ 所有 namespace 的限流一视同仁(没有 priority 概念)

💡 生产建议:即使开了 APF,EventRateLimit 仍然推荐配上——APF 按用户/优先级分流,EventRateLimit 按资源类型兜底。两个一起用最稳。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

加倍巴巴

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值