Repository navigation
atelet: run at system-cluster-critical - #3328
Tim Hockin (thockin) merged 4 commits into
Conversation
876a493 to
61b99ec
Compare
When local snapshots fill a node's disk, the kubelet evicts atelet and rejects its replacements until the pressure clears. Without atelet, nothing uploads or detaches the snapshots, so the node can stay in DiskPressure until someone frees space by hand. The kubelet never evicts a critical pod under node pressure and admits one while the node is under pressure. Some clusters admit critical pods outside kube-system only in a namespace with a matching ResourceQuota, so the manifest adds one scoped to system-node-critical. Signed-off-by: Ivy <[email protected]>
61b99ec to
9cbf717
Compare
Benjamin Elder (BenTheElder)
left a comment
There was a problem hiding this comment.
Agent assisted sources and auditing, review by me :-)
| spec: | ||
| serviceAccountName: atelet | ||
| priorityClassName: ate-node-critical | ||
| priorityClassName: system-node-critical |
There was a problem hiding this comment.
Did we consider system-cluster-critical? The kubelet treats any priority >= 2000000000 as critical, so it still won't evict atelet or reject its replacement, but the -997 oom_score_adj only applies to the system-node-critical name, and it can't preempt cluster-critical pods.
https://github.com/kubernetes/kubernetes/blob/f28b4c9efbca5c5c0af716d9f2d5702667ee8a45/pkg/kubelet/types/pod_update.go#L188-L195
atelet and objectstore-plugin have no memory limits, and an OOM kill in a worker container takes the whole container and all of its actors with it, so I'd rather a leaking atelet be the one the kernel picks. If we stay at node-critical I think we need memory limits on both ... we might want to do that, but maybe not now.
There was a problem hiding this comment.
We did consider it. atelet works much like the kubelet. ateapi reaches every worker through the atelet on its node and won't dial unless there is exactly one, so a node can't host actors without it. system-node-critical is the class for agents like that.
The -997 oom_score_adj is the main difference, and it's what we want when workers fill the node's memory. A worker OOM kill loses its actors' state. atelet restarting would stop activations and checkpoints on the whole node. Both classes already keep atelet safe from kubelet eviction, so the score only matters once the kernel has to pick.
I agree about the leak risk. -997 is what we want when workers fill the node, but a leaking atelet would take workers down with it. Should we add memory limits to atelet and objectstore-plugin in a follow-up, so a leak only restarts atelet? Does that need to be done as part of M3 or can it wait?
There was a problem hiding this comment.
[...] so a node can't host actors without it
I mean, it can't interact with the actors, but they will stay up and not lose state if it goes down.
The -997 oom_score_adj is the main difference, and it's what we want when workers fill the node's memory. A worker OOM kill loses its actors' state. atelet restarting would stop activations and checkpoints on the whole node. Both classes already keep atelet safe from kubelet eviction, so the score only matters once the kernel has to pick.
Er, but my point is that if we do have to pick, we'd rather pick atelet than ate-worker. atelet can be recovered. ate-worker loses actor data.
[...] but a leaking atelet would take workers down with it [...]
how? if atelet crashes and restarts, it should re-discover the worker pod(s), without losing any critical state.
Should we add memory limits to atelet and objectstore-plugin in a follow-up, so a leak only restarts atelet?
Limits will require tuning, but I think we can accomplish the same by using system-cluster-critical instead of node-critical. Despite the names the only difference is a small amount of priority and the oom_score_adj.
If we're at the point of evicting a pod on one of these nodes, probably atelet actually is the most viable to evict.
Another system-node-critical pod would be something like kube-proxy, without which we can't use atelet <> ate-api anyhow.
There was a problem hiding this comment.
My main concern is disk. Only atelet frees what substrate writes under /var/lib/ate, which includes local snapshots, actor image layers, actor bundles, and actor dirs. The kubelet doesn't count that hostPath toward any pod, and its image and container GC don't reach it (in #3101 image GC freed 0 bytes). So atelet has to stay on the node, or the node can't recover from disk pressure without manual cleanup. Agreed that both classes keep atelet on the node, so I'm assuming you mean "OOM kill [the container]" not "viable to evict [the pod]"?
On memory, I don't think system-cluster-critical gets us the same as limits. The kubelet's memory-pressure eviction skips critical pods and evicts the next pod in rank under either class. The class only matters when the kernel OOMs before the kubelet acts.
| Memory pressure | system-node-critical (-997) |
system-cluster-critical (~995 for a 128Mi request) |
|---|---|---|
| atelet leaks, kubelet acts first | Workers evicted | Workers evicted |
| atelet leaks, kernel acts first | Workers OOM-killed | atelet OOM-killed, restarts in place |
| Workers spike, kernel acts first | A worker OOM-killed | atelet or objectstore-plugin likely killed first, which frees little and interrupts suspends and uploads. Then a worker. |
With a memory limit, an atelet leak OOM-kills only atelet before the node runs low, under either class. I slightly prefer system-node-critical with limits on atelet and objectstore-plugin in a follow-up. Although tuning them will take some work, since some of their memory use isn't bounded yet. If you feel strongly about system-cluster-critical, I can make the switch.
There was a problem hiding this comment.
Er, but my point is that if we do have to pick, we'd rather pick atelet than ate-worker. atelet can be recovered. ate-worker loses actor data.
Hmm, but atelet is singleton per node that services all workers. If atelet is gone, nothing cleans up the shared snapshots, and all checkpoint could fail due to lack of disk space.
There was a problem hiding this comment.
Memory limits will require tuning.
To me this is the main reason not to cherrypick this PR into release-1.0 branch. Do we have enough time to properly tune the memory limits? As a temporary workaround, Chenyi Wang (@chw120) igooch can we create a playbook on how to configure the atelet's detach timer and disk capacity to avoid running into #3101 ?
After GA we should look into separating /var/lib/ate/snapshots to its own dedicated disk instead of sharing the boot disk, it will prevent the issue where disk pressure doesn't trigger kubelet to evict atelet.
There was a problem hiding this comment.
We also need to handle our own eviction BEFORE kubelet does. #3478 (comment)
There was a problem hiding this comment.
Could we support both depending on whether limits.memory is set on atelet / objectstore-plugin?
- Without
limits.memory(default inatelet.yaml): Usesystem-cluster-critical. It prevents kubelet from evictingateletunderDiskPressure([Bug]: Node-local snapshots from async suspends can fill a node's disk; kubelet then evicts the workers and atelet, and the node stays in DiskPressure #3101), without risking an unboundedateletleak OOM-killingateworkerpods (so it's also safe forrelease-1.0). - With
limits.memoryconfigured (either by operators who set limits in their deployment, or in a follow-up PR after tuning): Recommendsystem-node-critical(and note this in theatelet.yamlcomment), sincelimits.memorywill restart onlyateletif it leaks, while-997protectsateletfrom node OOMs. - Allow both in the
atelet-critical-podsResourceQuota(values: ["system-node-critical", "system-cluster-critical"]) so GKE admits either one out of the box.
There was a problem hiding this comment.
Both classes keep atelet on the node for disk, the kubelet skips any critical pod in evictPod, so that part is equal.
I read your table the other way for memory: With cluster-critical the worst case is an atelet restart, with node-critical a leak kills workers and loses actor data.
I'd take cluster-critical now (+1 to chw120's suggestion to allow both in the quota), and once atelet has tuned limits it gets -997 anyway if requests == limits, Guaranteed pods get the same adj regardless of class.
There was a problem hiding this comment.
I switched the default to system-cluster-critical as you proposed, and the quota now allows both classes so we can move to system-node-critical once atelet and objectstore-plugin have memory limits.
Reruns on my GKE test cluster at system-cluster-critical matched the node-critical runs, with atelet never evicted and every snapshot uploaded.
The kubelet enforces an emptyDir sizeLimit only by evicting the pod, and it never evicts a critical pod. --asset-staging-capacity is the only bound now, so remove the limit and the test that checked the capacity fits under it. Link the upstream docs and quota behind the critical-pods ResourceQuota. Signed-off-by: Ivy <[email protected]>
|
/cc |
| apiVersion: scheduling.k8s.io/v1 | ||
| kind: PriorityClass | ||
| # 5. Allow system-node-critical pods in ate-system | ||
| # atelet runs at system-node-critical because a node can't host actors |
There was a problem hiding this comment.
non-blocking: actors do stay up without atelet (see the priorityClassName thread), whichever class we land on can this give the disk reason instead: atelet is the only thing that frees /var/lib/ate.
| # its replacement. Some clusters, like GKE, admit critical pods outside | ||
| # kube-system only with a quota scoped to their class. | ||
| # https://kubernetes.io/docs/concepts/policy/resource-quotas/#limiting-priorityclass-consumption-by-default | ||
| # The limit matches the upstream kube-system quota for critical pods, so it |
There was a problem hiding this comment.
non-blocking: I think this was my goof the "default" here is really kube-up.sh
We should probably describe the upstream mechanism instead.
e.g. clusters whose ResourceQuota admission config sets limitedResources for the critical classes ...
| spec: | ||
| serviceAccountName: atelet | ||
| priorityClassName: ate-node-critical | ||
| priorityClassName: system-node-critical |
There was a problem hiding this comment.
Both classes keep atelet on the node for disk, the kubelet skips any critical pod in evictPod, so that part is equal.
I read your table the other way for memory: With cluster-critical the worst case is an atelet restart, with node-critical a leak kills workers and loses actor data.
I'd take cluster-critical now (+1 to chw120's suggestion to allow both in the quota), and once atelet has tuned limits it gets -997 anyway if requests == limits, Guaranteed pods get the same adj regardless of class.
Both critical classes keep the kubelet from evicting atelet and admit its replacement under DiskPressure. system-node-critical also gives atelet oom_score_adj -997, so with no memory limits a leak in atelet or objectstore-plugin makes the kernel kill workers first. Use system-cluster-critical until both containers have memory limits, and let the quota admit either class. Signed-off-by: Ivy <[email protected]>
Chenyi Wang (chw120)
left a comment
There was a problem hiding this comment.
LGTM. Thanks igooch for fixing it.
9958dd9 to
6de26e5
Compare
Signed-off-by: Ivy <[email protected]>
6de26e5 to
bc0c919
Compare
Part of #3101. When local snapshots fill a node's disk, the kubelet evicts atelet and rejects every replacement while the node has DiskPressure. Uploads and detaches stop, so nothing frees the space. Only atelet frees what substrate writes under `/var/lib/ate`, because the kubelet doesn't count that hostPath toward any pod and its image GC doesn't reach it. The kubelet never evicts a pod at priority 2000000000 or above, and it admits one under DiskPressure. So atelet now runs at `system-cluster-critical`. In test runs based off of #3101 atelet was never evicted for either `system-node-critical` or `system-cluster-critical` and uploaded on schedule. `system-node-critical` would also give atelet `oom_score_adj` -997. With no memory limits on atelet and objectstore-plugin, a leak in either would then make the kernel kill workers first. Per discussion in this PR the manifest comment says to switch once both containers have memory limits. GKE admits critical pods outside kube-system only in a namespace with a `ResourceQuota` scoped to their class. The manifest adds one that allows both classes, so switching later needs only the `priorityClassName` change. Its `pods: 1G` limit matches GKE's own `gcp-critical-pods` quota. Clusters that don't restrict the critical classes, like kind, admit atelet without it. The kubelet enforces an emptyDir `sizeLimit` only by evicting the pod, so the `asset-staging` limit no longer applies. I removed it and the test that checked `--asset-staging-capacity` fits under it. An install upgraded across this change keeps the old `ate-node-critical` PriorityClass. Delete it with `kubectl delete priorityclass ate-node-critical`. I used an AI agent (Claude Code) to run the #3101 tests, make this change and draft this description. --------- Signed-off-by: Ivy <[email protected]> (cherry picked from commit 1b8f2e0) Signed-off-by: dberkov <[email protected]>
Move Commit from the v1.0.0-rc.2 tag (e59281c) to v1.0.0-rc.3 (4ee8f66ac237adfc3303086b12275aefc150fd6f), five commits later. go.mod is unchanged (go 1.27.0), so MinGoVersion stays; substrate-pin-check agrees. What rc.3 changes for the installer: - atelet runs at system-cluster-critical (agent-substrate/substrate#3328) instead of upstream's own ate-node-critical PriorityClass, which the manifests no longer ship. GKE's ResourceQuota admission config limits the system-* classes to namespaces with a quota scoped to them, and rc.3's atelet.yaml ships that quota (atelet-critical-pods in ate-system, scopeSelector on PriorityClass, pods: 1G) for exactly that reason, so `deploy ate-system` admits the DaemonSet with no change here. The system pools' taints and labels, the cordon and placement checks (which look at ate-api-server, not atelet) and the autoscaling step are untouched by it. An upgrade from rc.2 leaves the orphaned ate-node-critical PriorityClass behind, unused and harmless. - The gVisor durable directory root is now mode 0777 (#3457), the objectstore-plugin exchange skips the system roots for a `.svc` issuer, and the control-plane binaries resolve gRPC peers through a k8s resolver. None of these touch what the installer passes. - cmd/ate-setup and docs/guides/upgrade.md are byte-identical to rc.2, so the micro-VM staging path, the delete and demo commands, the cordon and placement checks, and the install record format are as at rc.2. All eleven core images are published on GHCR at v1.0.0-rc.3; the demo images still are not. Verified: make substrate-pin-check, make verify, go test ./... in tools/setup-gcp, and in installer/ gofmt, go vet and go test for the root, internal and uishots packages, the ui suite included. The dry-run frames render; guard.svg is regenerated for the new short commit and the other four are byte-identical. TestFetchTreesFetchesBothCommits fails on this machine before and after the change, for an environmental reason unrelated to the pin.
Part of #3101.
When local snapshots fill a node's disk, the kubelet evicts atelet and rejects every replacement while the node has DiskPressure. Uploads and detaches stop, so nothing frees the space. Only atelet frees what substrate writes under
/var/lib/ate, because the kubelet doesn't count that hostPath toward any pod and its image GC doesn't reach it.The kubelet never evicts a pod at priority 2000000000 or above, and it admits one under DiskPressure. So atelet now runs at
system-cluster-critical. In test runs based off of #3101 atelet was never evicted for eithersystem-node-criticalorsystem-cluster-criticaland uploaded on schedule.system-node-criticalwould also give ateletoom_score_adj-997. With no memory limits on atelet and objectstore-plugin, a leak in either would then make the kernel kill workers first. Per discussion in this PR the manifest comment says to switch once both containers have memory limits.GKE admits critical pods outside kube-system only in a namespace with a
ResourceQuotascoped to their class. The manifest adds one that allows both classes, so switching later needs only thepriorityClassNamechange. Itspods: 1Glimit matches GKE's owngcp-critical-podsquota. Clusters that don't restrict the critical classes, like kind, admit atelet without it.The kubelet enforces an emptyDir
sizeLimitonly by evicting the pod, so theasset-staginglimit no longer applies. I removed it and the test that checked--asset-staging-capacityfits under it.An install upgraded across this change keeps the old
ate-node-criticalPriorityClass. Delete it withkubectl delete priorityclass ate-node-critical.I used an AI agent (Claude Code) to run the #3101 tests, make this change and draft this description.