CPU limits for instances: CPUWeight in the existing memory tiers (sketch) Idea open
Question asked: can CARLOS limit the memory and CPU impact of apps and instances, or be extended to? This sketch covers what is enforced today and a small extension for CPU. No code has been written yet.
What is enforced today
Memory: yes, per instance, in two platform-chosen tiers. No number comes from a manifest.
| Tier | memory.max | memory.high | Who gets it | |---|---|---|---| | TierDefault | 512M | 90% (~460M) | everything | | TierChildren | 1G | 90% (~920M) | capabilities.children |
- The tiers are defined in
internal/cgroup/cgroup.go(TierDefault,TierChildren,HighOf).internal/host/memtier.gopicks the tier from the manifest'schildrenbit. - Unit-backed instances:
instancectl.Server.setTierrunssystemctl set-property --runtime carlos-instances-<iid>.slice MemoryMax= MemoryHigh= MemorySwapMax=.parseRequestrefuses anymemory_maxthat is not one of the two tier numbers. - Exec children of the edge:
cgroup.Manager.applyLimitwritesmemory.max,memory.high,memory.swap.max(= max, so zram cannot absorb the overflow) andmemory.oom.group=1. - Unit-backed apps:
carlos-app@.servicesetsMemoryMax=512M.caps/render.gosets 1G forchildrenand 4G for the doc engine. - Outer wall:
carlos.slicehasMemoryHigh=70%andMemoryMax=80%over the edge and all instances. Tenants run withOOMScoreAdjust=500, so the kernel kills a tenant before the agent.
Task count: yes. TasksMax=256 per instance unit, 1024 for children.
CPU and IO: measured, not limited. Delegate=cpu memory io pids and the +cpu +memory +io +pids write in SetupAt exist for metrics attribution only. Nothing writes cpu.weight, cpu.max or io.*, and no instance or app unit sets CPUWeight= or CPUQuota=. One busy instance can use every core on the box.
Proposal: CPU in the same tier
1. Weight first, quota optional
CPUWeight(proportional share) is the main lever. It has no cost when the box is idle. Under contention, it stops one tenant from starving the others and the edge.CPUQuota(hard ceiling) is optional and off by default. On a mostly idle single-tenant box it only adds latency. On burstable instances (t4g), a quota does not save credits; it only hides the spend from the tenant. It is still worth having as an operator knob for a known-abusive app.
2. Make the tier a name on the wire, not a number
instancectl.Client.Start and Client.Seed send memory_max=<bytes>, and the server checks it against a whitelist of two numbers. Adding a CPU dimension that way means whitelisting combinations. A cleaner shape:
// internal/cgroup
type Limit struct {
Name string // "default" | "children" — the wire form
MemoryMax int64
CPUWeight int // 1..10000; 0 = write nothing
CPUMax string // cpu.max form, e.g. "100000 100000" (one core); "" = write nothing
}
var (
TierDefault = Limit{Name: "default", MemoryMax: 512 << 20, CPUWeight: 100}
TierChildren = Limit{Name: "children", MemoryMax: 1 << 30, CPUWeight: 100}
)
func TierByName(name string) (Limit, bool)- The client sends
tier=default. The server looks it up withTierByName, so a tier still cannot be invented over the socket. The validation stays where it is and gets simpler. setTierappendsCPUWeight=(andCPUQuota=whenCPUMaxis set) to the sameset-propertycall.applyLimitwritescpu.weight/cpu.maxafter the memory files, with the same best-effort, one-log-line behaviour.caps/render.goandcarlos-app@.serviceget matchingCPUWeight=lines, so a declared capability behaves the same on both backings (the rule the code comments already state).- Rollout: the wire change needs the edge and instance-control to agree. Either accept both
memory_maxandtierfor one release, or ship them together. They live on the same box and roll as one unit, so shipping together is probably fine. To confirm.
3. Starting numbers
- All tenant tiers at
CPUWeight=100(the systemd default). This changes nothing by itself, but it makes the knob explicit and gives the next step something to adjust. - The edge: raise its weight relative to
carlos-instances.slice(e.g.CPUWeight=200oncarlos-edge.service). The edge is the proxy and activator for every tenant, so starving it hurts everyone. This is the one change with an immediate effect.
4. Disk IO (later)
IOWeight= is cheap and follows the same pattern. IOReadBandwidthMax= / io.max needs a device major:minor and is fussier. I would leave IO out of the first cut.
Open questions
- Is the cpu controller actually enabled down to the per-host instance slices on a live box?
Delegate=cpuon the edge enables it along the edge's own path. Whethercarlos-instances.slice'scgroup.subtree_controlincludescpudepends on systemd's controller propagation. If it does not, today's "fair share" between instances is not happening at all, and settingCPUWeight=is what turns it on. Check:cat /sys/fs/cgroup/carlos.slice/carlos-instances.slice/cgroup.subtree_control. I could not reach the box from this session. - Per-app or per-instance sizing. The design rule is "no number comes from a manifest". If an operator needs to give one instance more room, I suggest a third named tier, or a platform-side per-instance tier override that
memTierreads (registry or instance config). Taking numbers from the manifest would reverse the rule rather than extend it. - Visibility. This overlaps with #184 (Expose effective app memory limits and OOM kills). Whatever exposes the effective memory tier should expose the CPU weight/quota and
cpu.stat'snr_throttled/throttled_usectoo. A throttled tenant otherwise looks like a slow app with no explanation, the same failure mode #184 describes for memory.
Rough size
Small: one struct change, two writer sites (applyLimit, setTier), a wire-parameter change in instancectl, a few unit lines, plus tests for the TierByName whitelist and the set-property arguments.