Concurrency and Quotas: Starting Multiple Episodes at Once Without Blowing Through Any Provider's Limits
Produce multiple episodes in parallel, use a queue, a token bucket, and priorities to control the load on each provider, and handle the queuing, starvation, and placeholder problems long tasks hit under concurrency.
Today's Goals
- Run multiple episodes in parallel with a queue, and cap concurrency and rate separately per provider
- Prioritize tasks so an urgent rush job doesn't starve a queued long-running task
- Make the right backoff and degradation decisions among over-limit, rate-limited, and queue-timeout conditions
Yesterday's engine runs one episode reliably and resumes from a checkpoint. Today we push it from one episode at a time to five episodes at once, and you immediately hit a new problem: machines are plentiful, quota is not. When you finish, scroll back up and tick the three goals off.
Plain-Language Walkthrough
Three units at once and one crane
When a production scales up, the line producer's first thought is "run several units in parallel." The second thought usually arrives within half a day: three units all want the camera crane, and there is one crane. You can hire more people and rent more lights, but that crane is one crane.
On a generative pipeline the crane is provider quota. Your process can open a hundred connections and your cloud host can grow to sixteen cores, but the rate ceiling MiniMax gives you does not move. Those numbers are public; we copy them from the official rate-limit page, at the topped-up tier:
| API | Topped-up RPM |
|---|---|
| Speech T2A v2 | 20 |
| Image generation | 10 |
| Video generation v1 | 20 |
| Text MiniMax-M3 | 200 |
Convert those into units you feel. One episode has forty storyboard entries, each with at least one line, so forty syntheses. Speech is twenty per minute, so dubbing alone queues for two minutes — for one episode. Five in parallel is two hundred calls, ten minutes. First frames are tighter: images are ten per minute, so five episodes' two hundred first frames take twenty minutes, twice as slow as dubbing.
Those two numbers overturn a natural intuition. Most people assume the parallel bottleneck is "video generation is slow" and pour effort into optimizing video; but computed against quota, the stage that jams the whole line is often the one that looks cheapest and fastest. An image is two and a half cents and returns in seconds, and you may only call it ten times a minute.
So today's job is not "raise the concurrency setting" but to install a gate for each provider's each API class, so your process holds itself inside the quota. That sounds like self-restraint, and it is the opposite: only by limiting yourself deliberately can you run continuously at the top edge of the quota. The unlimited version overshoots, gets refused, backs off, retries, overshoots again — and averages lower throughput.
The unit of parallelism: per episode or per shot
Before installing gates, settle one thing: what is the unit of parallelism?
Per episode is the easy option: five processes, each running one episode's six stages. Isolation is clean and one episode's collapse does not touch the other four; the downside is a large failure radius. An episode that blows up at stage five leaves two hours of artifacts hanging, and the money is already spent.
Per shot cuts finer: every shot of every episode enters one pool, and whatever's dependencies are satisfied runs. Utilization is high — one episode waits on its script while another's forty shots saturate the speech quota; the downside is orchestration complexity: you maintain the dependency relationships yourself and must still answer "what percentage of episode three is complete?"
This course chooses parallel by shot, reported by episode. Execution granularity is the shot, because only that saturates quota; but external reporting, the cost ledger and failure re-runs are per episode, because readers care whether episode three can ship, not how shot seventeen of episode three is doing. The combination costs one extra layer: tasks carry a label for the episode they belong to, and statistics aggregate by label. Those few dozen extra lines are worth it.
And a counterintuitive judgment in passing: parallelism should not be decided by your CPU core count. There is almost no local computation on this line; it is all waiting on the network. What really decides parallelism is quota, plus how much you are willing to pay for "how much has to be redone when one thing fails."
One gate per provider
A gate has two distinct parts, and people often merge them into one and then debug a pile of inexplicable behavior.
The first is the concurrency cap, governing how many requests are in flight at any instant. You choose that number, to protect your own process, memory and wallet. The second is the token bucket, governing how many requests go out per minute — that number is the vendor's, straight from the RPM table above.
Why not just one? With only a concurrency cap, if every request returns quickly you can send hundreds a minute and still hit the limit. With only a token bucket, twenty per minute is correct but all twenty might be in flight at once, leaving twenty pending video downloads in memory. You need both.
The token bucket's shape deserves a sentence. The laziest rate limiter sleeps before each request — twenty RPM means sleep three seconds each time. That does limit the rate, and it also throws away burst capacity: the real quota lets you fire twenty times at the start of a minute and stay quiet for fifty seconds. A token bucket preserves the burst, at the cost of maintaining a window and a counter.
// Token bucket: capacity tokens issued per window. The key is that tryTake does not block -
// on failure it returns false at once so the scheduler can do something else instead of waiting.
class TokenBucket {
constructor(capacity, windowMs = 60_000) {
this.capacity = capacity
this.windowMs = windowMs
this.tokens = capacity
this.windowStart = Date.now()
}
tryTake() {
const now = Date.now()
if (now - this.windowStart >= this.windowMs) {
this.windowStart = now
this.tokens = this.capacity
}
if (this.tokens <= 0) return false
this.tokens -= 1
return true
}
}
// RPM comes from the official rate-limit page; the concurrency cap is yours. Two different things.
const QUOTA = {
image: { rpm: 10, concurrency: 2 },
tts: { rpm: 20, concurrency: 3 },
video: { rpm: 20, concurrency: 2 },
}import time
class TokenBucket:
"""capacity tokens per window. try_take never blocks: on failure the scheduler moves on."""
def __init__(self, capacity: int, window_s: float = 60.0) -> None:
self.capacity = capacity
self.window_s = window_s
self.tokens = capacity
self.window_start = time.monotonic()
def try_take(self) -> bool:
now = time.monotonic()
if now - self.window_start >= self.window_s:
self.window_start = now
self.tokens = self.capacity
if self.tokens <= 0:
return False
self.tokens -= 1
return True
# RPM comes from the official rate-limit page; the concurrency cap is yours. Two different things.
QUOTA = {
"image": {"rpm": 10, "concurrency": 2},
"tts": {"rpm": 20, "concurrency": 3},
"video": {"rpm": 20, "concurrency": 2},
}Note that tryTake is non-blocking. That single detail decides whether the whole scheduler works, and section four shows the consequence.
Rate limiting is not an error, it is a signal
MiniMax's response body carries base_resp.status_code, where 0 is success; today's relevant codes are 1002 rate limiting and 1039 (rate limiting on the TPM dimension). Many people bundle those with 1004 authentication failure, 1008 insufficient balance and 2013 invalid parameter into one catch that logs a line, which is a waste.
Split the codes into two classes and it becomes clear: those a retry might fix (rate limiting, server errors) and those a retry will never fix (authentication, balance, parameters, moderation). Back off and retry the first; never retry the second even once — retrying just commits the same error five times in a minute while occupying quota.
For the retryable class, backoff must do three things, and skipping any one means it is not done:
First, exponential backoff with jitter. If five episodes hit the limit together and all retry after a fixed one second, they wake together and collide again. Jitter multiplies each backoff by a random factor between 0.7 and 1.3 to spread them out.
Second, do not hold an execution slot while backing off. That is the next section's subject.
Third, slow down deliberately after being rate limited. This is the one most often missed. Being limited means your send rate exceeded what the vendor will accept right now, so charging back at the old speed after the backoff just collides again. The right move is to penalize the token bucket: issue half the tokens for the next window or two, and restore once the collisions stop.
// Exponential backoff plus jitter. Jitter stops five episodes waking together and colliding again.
const backoffMs = (attempt) => Math.round(300 * 2 ** (attempt - 1) * (0.7 + Math.random() * 0.6))
function onFailure(task, err, gate) {
const rateLimited = err.statusCode === 1002 || err.statusCode === 1039
if (rateLimited) {
gate.stats.rateLimited += 1
gate.bucket.penalize() // half tokens for the next two windows
}
// auth 1004, balance 1008, params 2013, moderation 1026/1027 are never retried
if (!err.retryable || task.attempt + 1 >= 5) return scheduler.fail(task, err)
scheduler.requeue(task, backoffMs(task.attempt + 1))
}import random
def backoff_s(attempt: int) -> float:
"""Exponential backoff plus jitter, so five episodes do not wake together and collide."""
return 0.3 * 2 ** (attempt - 1) * random.uniform(0.7, 1.3)
def on_failure(task, err, gate, scheduler) -> None:
if err.status_code in (1002, 1039):
gate.stats.rate_limited += 1
gate.bucket.penalize() # half tokens for the next two windows
# auth 1004, balance 1008, params 2013, moderation 1026/1027 are never retried
if not err.retryable or task.attempt + 1 >= 5:
scheduler.fail(task, err)
return
scheduler.requeue(task, backoff_s(task.attempt + 1))How a single async task is submitted, polled and abandoned on timeout was D4's material and is not repeated; today is only about how the gate opens when many tasks arrive at once.
Priority and fairness: three tiers, and how not to starve
Rush jobs always exist. Operations says at midnight that episode six is needed tomorrow morning, so that episode jumps the line. The mechanism is direct: three task tiers — rush, normal, low — scheduled by tier first and, within a tier, first come first served by enqueue time.
The real trap is the next step: low-priority tasks may never get scheduled at all. As long as rush jobs keep arriving, the earliest low-priority batch stays at the tail forever. That is starvation.
The cheapest anti-starvation mechanism is aging: the longer a task waits in the queue, the higher its effective priority. Wait one tier's worth of time and it rises one tier. Even under a constant stream of rush jobs, long-waiting tasks float up.
Aging needs one hard-coded constraint: promotion has a ceiling and must never reach the rush tier. Otherwise after half an hour the queue is all rush jobs and the tier means nothing. This course lets low rise only to normal, reserving rush for human line-jumping.
There is a subtler trap that makes priority completely ineffective while looking fine in the logs. If tasks are dispatched first and then wait for quota inside, then when all three worker slots hold low-priority tasks that are all waiting on tokens, a rush job never even gets dispatched — being first in the queue is useless if nobody comes to collect it.
That is why the previous section stressed that tryTake must be non-blocking. The correct structure is that the scheduler asks the gate for permission before handing out work: it dispatches only on success, and skips to the next candidate on failure. Then, when quota is tight, tokens go by priority to the task that most deserves them.
Long tasks should not hold a worker slot
Video generation is the only long task on this line. Submit, receive a task_id, then wait — the vendor publishes no typical duration, so you work from measurement, and it fluctuates with the queue.
The problem is that if your code is one await video.generate(...), that execution flow is pinned by a task doing nothing but waiting. Three worker slots and eighteen video tasks: the moment the first three videos are submitted, the whole line stalls. Scripts could run, dubbing could run, the timeline could run — and no slot is free to run them.
The fix splits a long task's lifecycle in two: submission occupies a worker slot (it is a short HTTP request), and waiting does not. The gate's ticket stays held during the wait — the vendor really does have a task pending, and you should not lie to yourself about that — but your execution flow should return to the queue immediately for other work.
// start() returning undefined means done; returning a function means "handed to the vendor, now just waiting".
const task = {
id: 'ep6-clip-s01',
gate: 'video',
start: async () => {
// Note: no await. Wrap the promise in a function and hand it back; the slot is returned now.
const inflight = providers.video.generate({ prompt, outPath, durationSec: 6 })
return async () => {
await inflight
run.record({ nodeId: 'ep6-clip-s01', kind: 'video', units: 6 })
}
},
}
const pending = await task.start()
if (pending) {
log('submitted to the vendor, releasing the worker slot for other work')
background.push(pending().then(() => scheduler.complete(task)))
} else {
scheduler.complete(task)
}import asyncio
async def start_clip(providers, run, prompt, out_path):
"""None means done; a coroutine means "handed to the vendor, now just waiting"."""
# create_task attaches it to the event loop, and this execution flow returns immediately
inflight = asyncio.create_task(
providers.video.generate(prompt=prompt, out_path=out_path, duration_s=6)
)
async def wait() -> None:
await inflight
run.record(node_id="ep6-clip-s01", kind="video", units=6)
return wait
pending = await task.start()
if pending is not None:
log("submitted to the vendor, releasing the worker slot for other work")
background.append(asyncio.create_task(finish(pending, task)))
else:
scheduler.complete(task)The same logic applies to any wait: waiting for a token, waiting out a backoff, waiting on a downstream dependency — none should hold a worker slot. The test is easy to remember: if your code does nothing during that interval, it should not be holding a slot.
How this relates to the message-queue material
Readers with a distributed-systems background will find this familiar: is this not a priority work queue plus a rate limiter? Yes. In production, what you wrote today is usually a Redis Streams consumer group, or an off-the-shelf task queue, plus rate limiting at the gateway.
So why write it yourself? Because the judgment this course teaches is not in the queue itself but in what dimension the gate buckets by. An off-the-shelf queue gives you concurrency control, but it does not know that your images and speech come from two different quota pools, nor that your video tasks spend eighty percent of their time waiting rather than computing. Only you know those, and only you can configure them.
If you are building a real production system, the recommended boundary is: use something off the shelf for queue persistence, consumer groups and failure redelivery, and write the gate and priority policy yourself — it is one or two hundred lines and is the part that most needs tuning against your bill. This platform's "30 Days from Frontend Engineer to Agent Engineer" course has a day on Redis Streams consumer groups and the pending list; if you want to replace today's in-memory queue with a cross-process one, that is the cheapest place to pick it up.
Source Reading
Hands-On Lab
The lab compresses the rate window to six seconds by default so the whole flow finishes in a dozen seconds, and no quota number is changed; setting RATE_WINDOW_MS to 60000 gives real behavior. The starter leaves five exercises, and running it unmodified shows five of six checks failing, one flipping to a pass per exercise.
Fault injection uses the course-wide INJECT variable, with one value today:
MOCK=1 pnpm start # happy path
INJECT=ratelimit MOCK=1 pnpm start # first 1 image and first 2 speech calls return status_code=1002Injection applies only at the provider egress, and the gate, scheduling, priority and aging logic all run normally — that is what distinguishes it from skipping the step.
- Implement non-blocking admission with a token bucket and a concurrency semaphore, run once, and confirm "blocked by token" is no longer zero and peak in-flight stays within the cap.
- Queue five episodes at once and watch the image row in the gate statistics — it will be the first to jam, matching the arithmetic in the walkthrough.
- Run again with INJECT=ratelimit, watch the backoff log, and confirm the backoff has jitter, the bucket is penalized, and tasks eventually succeed.
- Add priority ordering and aging, and watch the rush job's jump count and the starvation-protection log appear together.
- Change video tasks to return the worker slot after submission, and compare total elapsed time before and after.
Interview Questions
Today's three questions are in the question bank below, focused on parallel granularity versus failure radius, per-provider rate-limit design, and correct handling of rate-limit responses. Read the analysis before the key points — practicing the derivation beats memorizing them. The cn / global labels let you pick by target market.
Checklist and Tomorrow
- I can run multiple episodes in parallel with a queue, and cap concurrency and rate separately per provider
- I can prioritize tasks so an urgent rush job doesn't starve a queued long-running task
- I can make the right backoff and degradation decisions among over-limit, rate-limited, and queue-timeout conditions
- I can explain what the concurrency cap and the token bucket each govern, and why both are needed
- I can explain why waiting for quota must not hold a worker slot, or priority silently stops working
- All 5 lab acceptance criteria pass
- I can answer at least 2 of the 3 interview questions without looking at the key points
Tomorrow (D10) we build the screening room: a web console for reviewing images and cuts shot by shot, editing lines, and regenerating a single shot. The order is deliberate — today we scaled output to five parallel episodes, and once volume rises, human checking becomes the new bottleneck; and for a human to intervene, the first requirement is being able to see. More importantly, you now know that every regeneration is deducted from quota, so tomorrow's arithmetic of "which nodes must re-run when one line changes" finally means something: get it wrong and the time saved is repaid in duplicate calls.
Interview questions
Many jobs share one vendor's quota. How would you design the rate limiting?多个任务共用一家厂商的额度,你会怎么设计限流?
Common in ChinaCommon overseasIntermediate#rate-limiting#concurrency#schedulingHow to reason about it · think before answering
- The hinge is the word shared. Naming a token bucket only answers half of it; the interviewer wants your bucketing dimension and what else sits around the bucket.
- Dimension first: bucket per vendor and per API family, never one global bucket. Image and speech quotas at the same vendor are separate pools, and merging them lets the tight one throttle the loose one.
- Then the parts: one bucket is not enough. A token bucket caps rate (calls per minute, a number the vendor sets); a semaphore caps concurrency (how many are in flight, a number you set to protect memory and spend). Rate alone lets a fast endpoint fire hundreds per minute; concurrency alone lets twenty downloads pile up.
- The engineering detail that decides everything: admission must be non-blocking. If a job is dispatched first and then waits for a token inside the worker, low-priority work pins every worker slot and priority silently stops working. Ask the gate before dispatch, and skip to the next candidate when it says no.
- Finally the numbers: concurrency should not track CPU cores, since this pipeline is almost all network wait. Derive it from vendor quota and from how much work one failure forces you to redo.
- Expected follow-up: where does bucket state live. In-memory is fine for one process; across processes it belongs in Redis behind an atomic token-take script, or each process limits itself and the sum still blows the quota.
分析过程 · 先想清楚再作答
- 这题的题眼是「共用」两个字。只回答一个令牌桶算答了一半,面试官想听的是你按什么维度分桶、以及桶之外还需要什么。
- 先给维度:限流要按「厂商 + 接口类别」分桶,不能全局一个桶。同一家的图像和语音是两个独立的配额池,混在一起会让紧的那个把松的那个也拖住。
- 再给部件:一个桶不够,要两个。令牌桶管速率(一分钟发几次,数字是厂商定的),信号量管并发(同一时刻挂着几个,数字是你自己定的用来保护内存和钱包)。只有速率控制的话,快速返回的接口一分钟能发几百次;只有并发控制的话,二十个请求同时在飞会把内存挂满。
- 然后是关键的工程细节:拿许可的动作必须是非阻塞的。如果任务先被派出去、再在执行流里等令牌,工作槽会被一批低优先任务占死,优先级就静默失效了。正确结构是调度器派活之前先问闸门要许可,拿不到就跳过它去看下一个候选。
- 最后落到取值:并发上限不该按 CPU 核数定,这条线几乎没有本地计算,全在等网络;它该按「一次失败要重跑多少东西」和厂商配额来定。
- 可预期的追问是「桶的状态放哪」。单进程放内存就够;多进程要放 Redis,用一个原子脚本取令牌,否则每个进程各限各的,加起来照样超。
Key points
- Bucket per vendor and per API family; image and speech at one vendor get separate gates.
- Each gate has two parts: a token bucket for rate (the vendor's RPM) and a semaphore for in-flight concurrency (your own number).
- Admission is non-blocking: if the gate says no, the job stays queued instead of holding a worker slot.
- Take the concurrency slot before the token, or a rejected admission silently burns quota.
- Across processes, move bucket state to Redis and take tokens atomically.
答题要点
- 按「厂商 + 接口类别」分桶,一家的图像和语音各一道闸门。
- 每道闸门两个部件:令牌桶控速率(厂商给的 RPM),信号量控并发(自己定的在飞上限)。
- 准入必须非阻塞,拿不到许可就把任务留在队列里,绝不占着工作槽干等。
- 先抢并发票再取令牌,顺序反了会白白扣掉配额。
- 多进程部署时桶的状态要外置到 Redis,用原子操作取令牌。
Beyond backing off and retrying, what else should happen when you get rate limited?收到限流响应之后,除了退避重试还该做什么?
Common in ChinaCommon overseasDeep dive#rate-limiting#error-handling#retryHow to reason about it · think before answering
- This question separates people who have actually been throttled in production. Exponential backoff with jitter is only the first half of the answer.
- Frame it correctly: throttling is a signal, not an error. It says your current send rate exceeds what the vendor will accept right now, so it deserves a feedback action, not just a retry.
- Action one is to slow down on purpose: penalize the bucket so the next window or two issues half the tokens. Without that, you finish the backoff and hit the same wall at the same speed.
- Action two is to not hold an execution slot while waiting. Requeue the job with a not-before timestamp and hand the slot back immediately.
- Action three is classification. Throttling and server errors are retryable; auth failure, insufficient balance, invalid parameters and content-policy rejections are not, and retrying them just repeats one mistake five times while consuming quota. At MiniMax, 1002 is rate limiting and 1039 is the token-per-minute variant, while 1004 is auth, 1008 is balance, 2013 is bad parameters and 1026 or 1027 are content rejections.
- Action four is to record throttle counts as a metric. That number is the only evidence you have when you later retune the gate.
- Expected follow-up: how to cap the backoff. Cap it at what the business can wait for, then degrade instead of retrying: smaller resolution, shorter duration, or push the job into the next batch.
分析过程 · 先想清楚再作答
- 这题在考你有没有真在生产里被限流打过。只答「指数退避加抖动」是标准答案的前半段,面试官等的是后半段。
- 先把限流摆正位置:它不是错误,是信号。它告诉你此刻的发送速率超过了厂商愿意接受的速率。既然是信号,就该有反馈动作,而不只是重试。
- 第一个动作是主动降速:把令牌桶罚一档,接下来一两个窗口只发一半令牌。不降速的话,退避结束后你会用同样的速度再撞一次,重试次数越多越糟。
- 第二个动作是别在退避里占着执行流。正确做法是把任务重新入队并记一个「不早于」时间戳,槽位立刻还回去给别的任务。
- 第三个动作是分类:限流和服务端错误可以重试,鉴权失败、余额不足、参数错误、内容审核不通过一次都不该重试——重试只会让你在一分钟里把同一个错误犯五遍,还白占配额。MiniMax 这边 1002 是限流、1039 是 TPM 维度的限流,1004 鉴权、1008 余额、2013 参数、1026 和 1027 是内容审核。
- 第四个动作是把限流次数记进指标。撞得多说明闸门配小了或者配大了,这个数字是你回头调参数的唯一依据。
- 可预期的追问是「退避上限怎么定」。定在业务能等的时间上,超过就转降级:换更小的分辨率、更短的时长,或者干脆排到下一批。
Key points
- Treat throttling as a signal: back off and also penalize the bucket so the next window issues fewer tokens.
- Requeue with a not-before timestamp instead of sleeping inside the worker slot.
- Add jitter, or everything throttled together wakes together and collides again.
- Separate retryable from non-retryable: auth, balance, bad parameters and content rejections get zero retries.
- Emit a throttle counter as a metric, and switch to degradation once backoff hits its ceiling.
答题要点
- 把限流当信号:退避的同时给令牌桶降档,接下来的窗口只发一半令牌。
- 退避期间把任务重新入队并记一个不早于时间戳,工作槽立刻还回去。
- 退避要带抖动,否则同时被限的任务会同时醒来再撞一次。
- 严格区分可重试与不可重试:鉴权、余额、参数、内容审核一次都不重试。
- 把限流次数记成指标,它是回头调闸门参数的唯一依据;退避到上限就转降级而不是继续重试。
Priority queues starve low-priority work. How do you prevent that?优先级队列容易出现饿死,你会怎么防?
Common in ChinaCommon overseasBasic#scheduling#priority-queue#fairnessHow to reason about it · think before answering
- This is the easy one, and most candidates stop after saying aging. The signal is in the two conditions they forget to attach.
- The mechanism first: aging, where effective priority rises with waiting time, one step per threshold crossed, with first-in-first-out inside a tier.
- Condition one: cap the promotion, and never let it reach the top tier. Otherwise after half an hour every queued job is top priority and the tier means nothing. Our rule is that low may rise to normal, and the top tier stays reserved for human escalation.
- Condition two is the one people miss: if a job waits for resources after dispatch, priority silently stops working, because worker slots are pinned by low-priority jobs waiting on quota and the urgent job is never picked up. Admission must happen before dispatch.
- Close with the observable: track average and maximum wait per tier plus a promotion counter. Those two numbers tell you directly whether the aging threshold is right.
- Expected follow-up: alternatives to aging. Reserved shares work too, where every fourth dispatch must go to a low-priority job. That is weighted fair queuing, more controllable but noisier to implement.
分析过程 · 先想清楚再作答
- 这题是送分题,但很多人只答一个「老化」就停了,拿不到区分度。区分度在两个补充条件上。
- 先说机制:老化,也就是等待越久有效优先级越高,每等过一个阈值就升一档。同档内按入队时间先来先服务。
- 第一个补充条件是升档要封顶,而且不许升进最高那一档。否则跑上半小时,队列里全是最高优先级,这一档就名存实亡了。本课的口径是低优先最多升到普通,最高档只留给人工插队。
- 第二个补充条件更容易被忽略:如果任务被派出去之后才开始等资源,优先级会静默失效——工作槽被一批低优先任务占着等资源,高优先任务连被取走的机会都没有。所以准入要在调度之前完成。
- 结论里要给出可观测量:按优先级统计平均等待与最长等待,再加一个升档次数。这两组数字能直接告诉你老化阈值配得对不对。
- 可预期的追问是「除了老化还有别的办法吗」。有:给低优先级预留一部分固定配额(比如每四次调度必须让一个低优先的过),这是加权公平调度的思路,比老化更可控但实现更啰嗦。
Key points
- Use aging: effective priority rises with wait time, first-in-first-out within a tier.
- Cap promotion and never let it reach the top tier, or the top tier stops meaning anything.
- Admit before dispatch, otherwise worker slots pinned on quota make priority silently useless.
- Track per-tier average wait, max wait and promotion count, and tune the aging threshold from those.
- The alternative is weighted fair queuing with a reserved share for low priority: more controllable, more code.
答题要点
- 用老化:等待时间越长有效优先级越高,同档内先来先服务。
- 升档要封顶,绝不能升进最高那一档,否则最高档形同虚设。
- 准入要放在调度之前,否则工作槽被低优先任务占着等资源,优先级会静默失效。
- 按优先级统计平均等待、最长等待与升档次数,用它来校准老化阈值。
- 备选方案是给低优先级预留固定份额的加权公平调度,比老化更可控但实现更复杂。