|
| 1 | +# CK virtual-time scheduling: A/B benchmark results (2026-07-27) |
| 2 | + |
| 3 | +Run on a **local homelab box, not production**. It is a *production-like* |
| 4 | +topology only in shape: one shared control plane (Postgres + one shared run-queue |
| 5 | +Redis + ClickHouse + the webapp/engine on this branch) and three separate managed |
| 6 | +worker clusters registered as distinct worker groups, all as containers on a |
| 7 | +single machine. No production data, traffic, or infrastructure was involved. |
| 8 | +Method and harness: `2026-07-26-ck-vtime-benchmark.md`. |
| 9 | + |
| 10 | +All numbers are **relative** (flag OFF vs ON, identical load, same box). Absolute |
| 11 | +throughput on this single-box homelab is nowhere near prod scale and is not |
| 12 | +reported as such; only the OFF-vs-ON delta is meaningful. |
| 13 | + |
| 14 | +## Arm 1: queue-level micro-benchmark (the isolated, defensible numbers) |
| 15 | + |
| 16 | +Real `RunQueue` driven directly against a dedicated Redis, flag OFF (age-ordered |
| 17 | +CK dequeue) vs ON (virtual time), 5 trials, quantum 1 / window multiplier 3. |
| 18 | +Every arm served every message exactly once (conservation checked), so the |
| 19 | +comparison is sound. Wait is in logical dequeue steps. |
| 20 | + |
| 21 | +| scenario | metric | baseline (OFF) | vtime (ON) | delta | |
| 22 | +| --- | --- | --- | --- | --- | |
| 23 | +| **ckSkew** (heavy backlog + light keys) | light-key first-serve step | 480 | 4 | **−99%** | |
| 24 | +| | light-key wait p50 | 556 | 96 | −83% | |
| 25 | +| | light-key wait p95 | 628 | 188 | −70% | |
| 26 | +| | light-key wait p99 | 636 | 196 | −69% | |
| 27 | +| | Jain fairness (contention) | 0.34 | 1.00 | +192% | |
| 28 | +| | drain step (work conservation) | 636 | 636 | 0% | |
| 29 | +| **ckTrickle** (bulk + trickle keys) | trickle first-serve step | 480 | 4 | −99% | |
| 30 | +| | trickle wait p95 | 592 | 172 | −71% | |
| 31 | +| | Jain fairness | 0.50 | 1.00 | +100% | |
| 32 | +| **ckSybil** (20 keys flood + 1 light; caps can't fix) | light first-serve step | 25 | 2 | −92% | |
| 33 | +| | light wait p95 | 34 | 27 | −21% | |
| 34 | +| **ckManyKeys** (61 variants > pass-1 window) | light first-serve step | 72 | 9 | −88% | |
| 35 | +| | drain step (no starvation) | 81 | 79 | drains fully | |
| 36 | +| **ckBalanced** (4 symmetric keys, no-harm) | worst-key wait p95 | 92 | 92 | 0% | |
| 37 | +| **ckHeavyIdle** (lone key, work conservation) | drain step | 59 | 59 | 0% (exact) | |
| 38 | +| (all scenarios) | Redis ops per dequeue+ack | baseline | +7% to +23% | cost | |
| 39 | + |
| 40 | +Reading it: a light key behind a backlog goes from waiting out the whole backlog |
| 41 | +(first-serve step 480) to being served almost immediately (4), wait p95 drops |
| 42 | +~70%, and contention fairness goes from lopsided (Jain 0.34) to even (1.00). The |
| 43 | +symmetric and lone-key cases are untouched, and drain steps are identical, so the |
| 44 | +fair order is work-conserving and does no harm. The cost is a modest per-dequeue |
| 45 | +Redis op increase. |
| 46 | + |
| 47 | +## Arm 2: end-to-end on the three worker regions (realism check) |
| 48 | + |
| 49 | +Deployed task on one shared base queue, per-run concurrency keys, environment |
| 50 | +concurrency ceiling pinned to 5 so the CK dequeue order is the bottleneck. |
| 51 | +Noisy-neighbor load: tenant A floods across 30 keys (240 runs), tenant B sends 30 |
| 52 | +runs across 3 keys, runs spread across the three regions, 1.5s hold. Metric is |
| 53 | +per-run start latency (`startedAt - createdAt`, ms). Flag flipped OFF vs ON with a |
| 54 | +control-plane redeploy between arms; identical load each arm. |
| 55 | + |
| 56 | +| tenant | metric | baseline (OFF) | vtime (ON) | delta | |
| 57 | +| --- | --- | --- | --- | --- | |
| 58 | +| **B (victim, 3 keys)** | start latency mean | 221,476 | 136,344 | **−38%** | |
| 59 | +| | start latency p50 | 219,901 | 133,930 | **−39%** | |
| 60 | +| | start latency p95 | 259,092 | 221,061 | −15% | |
| 61 | +| | start latency p99 | 259,136 | 221,078 | −15% | |
| 62 | +| A (flood, 30 keys) | start latency mean | 95,130 | 100,136 | +5% | |
| 63 | +| A | start latency p50 | 95,049 | 95,689 | ~0% | |
| 64 | + |
| 65 | +Reading it: under age order the light tenant waits behind the flood (mean ~221s); |
| 66 | +with virtual time it takes fair turns and drops to ~136s (mean −38%, p50 −39%), |
| 67 | +while the flood tenant is essentially unchanged (+5%, it stops jumping the queue). |
| 68 | +The smaller p95/p99 gain reflects per-**key** fairness: tenant B's keys carry |
| 69 | +deeper per-key backlogs here (10 runs/key vs the flood's 8), so B's last runs |
| 70 | +stay in the fair rotation longer. Absolute latencies are the cap-5 serialization |
| 71 | +plus per-run worker startup on a single box; only the OFF-vs-ON delta is the |
| 72 | +signal. |
| 73 | + |
| 74 | +## Bottom line |
| 75 | + |
| 76 | +Both arms agree: the fair order removes the starvation of a light concurrency key |
| 77 | +behind a large or sharded backlog, does not harm the balanced or lone-key cases, |
| 78 | +stays work-conserving, and costs a small, bounded per-dequeue Redis overhead. The |
| 79 | +micro-benchmark isolates the scheduler (the defensible numbers); the end-to-end |
| 80 | +run confirms the same effect on real deployed runs across the worker regions. |
0 commit comments