1603 字
8 分钟
限流器在拒绝它本该排的队

生产集群半个月回了 46974 次 503,其中 46950 次在 2 秒内返回。我的判断是:这些请求绝大多数本该排队。容量几十秒后就回来了,拒绝把一次能成功的创建变成了一次失败。

同一个请求在拒绝路径和排队路径上的不同结局

2 秒就是「没等」#

每台机器能跑的 pod 数有上限,这个集群一台 150 个。沙箱申报的资源很小(0.1 核 CPU、256Mi 内存),所以 pod 名额会先于 CPU 和内存用完。名额满了,新 pod 只能等空位,下层引擎等满 300 秒回 504。

沙箱服务看不到集群还剩多少名额,它能看到的只有自己收到了几个 504。于是熔断写成:10 分钟内 3 次 504,暂停建沙箱 60 秒。暂停期间,没带排队参数的请求立刻 503。

46974 次里 46950 次在 2 秒内返回。建一个沙箱从调度到拿到 IP 至少要几秒,2 秒内返回说明它一次调度都没等过。

同一段时间、同样的容量压力下,带了排队参数的请求 1825 次排队后创建成功,排队超时 0 次。一边全失败,一边全成功。

为什么默认是拒绝#

服务看不见余量。它不知道集群还剩几个名额,只能数自己失败了几次。靠失败次数只能做一个决定:停下来。它推不出「还差 2 个名额、8 秒后就有」,也就没法决定该等多久。

排队参数是后加的,默认 0。给强化学习框架用的接入插件不传它,直接调 HTTP 接口的也不传。默认关等于没有。

拒绝几乎不用写代码,回一个 503 带上 Retry-After,HTTP 里现成。排队得自己存住这些请求、把连接挂着、决定队列堆到多长要截断,一件现成的都没有。

立刻失败和稍后成功#

拒绝是把决定推回调用方:现在不行,你自己看着办。

调用方是训练任务,它收到 503 的默认处理是记一次失败。沙箱侧的异常有好几种返回:未就绪 503、暂停建沙箱 503、已退出 409、等待超时 504、命令超时;平台当时没有发布错误码表写明哪种能重试、命令是否已执行,控制程序只能自行归类。使用方反馈回来的是,等待和超时被记成了执行失败或代码错误。

排队是服务端替调用方等着,能建的时候建。差别不在错误码,在这次请求由谁来等。

我在讨论里纠正过一次措辞:这个不是准入,是等待,等可以的时候就创建。准入是一道判断题,当场出结果;等待是个过程,结果过一会儿才有。把等待实现成判断题,就会在容量只差 8 秒的时候回答「不行」。

对训练任务来说,立刻 503 比等 30 秒糟。等 30 秒这一步还在跑,立刻失败这一步就作废了,重试还要走一遍同样的判断,多半还是满的。

排队要配什么#

排队本身会变成新问题。

排队超时和队列深度。 每个请求自己带愿意等多久(0 到 3600 秒),等满仍没名额才返回 503,响应体写明实际等了多少秒。同时排队的请求上限 500,超出立刻拒绝。没有这两道,容量一直不恢复的时候队列会一直涨。

放行判据要读真实余量,不能写死个数。 最省事的写法是「同时最多 N 个」。但拒绝你的是 Kubernetes 的 ResourceQuota,它按「有效资源」记账:引擎注入的 init 容器没声明资源,被命名空间的默认上限补成 2 核,于是每个沙箱按 2 核记账,哪怕主容器只要 0.5 核。64 核的配额扣掉已占用的,按 0.5 核算能放 119 个,按真实记账只能放 29 个,实测同时就绪峰值正好卡在 28–29。写死的那个 N 一定会错,错的方向是放出去再被拒。

名额也不止一种。生产集群出过各机器 pod 名额被占满、约 13% 建沙箱请求超时,放行时要同时看各机器的 pod 名额和 CPU、内存余量。原型里加了四项检查:调用方同时存活数、在建并发数、配额余量、节点 pod 名额,每项都要过。60 个请求的突发实测排队 p50 19.2 秒、p95 52.3 秒,全部建成;把愿意等的时间压到 20 秒时 30 个被拒,消息精确到「卡在配额,limits.cpu 余量 1.5,本次要 2.0」。这句比「服务繁忙」有用,调用方看了才知道是该等还是该改请求。

永远排不上的请求不排队。 CPU 写成 500M 是 5 亿核(大写 M 是 SI 的兆),这个值合法、每一层都放行、永远排不到。第一版让它一直排着。改成单个请求就超过配额上限的直接交给准入策略裁决,0.04 秒当场拒。

排到一半的位置要能用幂等键找回来。 同步接口有客户端硬超时(SDK 60 秒),排队时长一旦超过它,就没法在一条连接里等完。做法是排队超 50 秒回 503,调用方用同一个请求编号重发,接回原来的排队位置。实测总用时和把客户端超时直接调到 900 秒相差不到 1%——连接挂着并不会让沙箱建得更快,断开再回来不损失什么。前提是请求编号幂等要严:实测 5 次重发拿到同一个沙箱,14 个沙箱的排队实验每轮正好 14 个,没有多建。

代价#

排队不会多出名额。名额被长寿命沙箱占着的时候,缩短 TTL、任务结束就删沙箱比排队管用。

排队计数现在放在进程内存里,多副本下就变成每个副本各算一份,阈值实际翻倍。记成已知风险,阈值配一半能抵消,没测过。

默认排队还没上线:带参数的请求会等,不带的仍然立刻 503。