Skip to content

Use GOMEMLIMIT to guard against OOM kills in controllers - Upgrade go to 1.19 #9947

@alpeb

Description

@alpeb

Problem and repro

In some situations the destination container sees its memory consumption progressively augment until it gets OOM killed. We have been able to reproduce that by rolling out emojivoto's pods every 30s (here the rollouts started at 11:55):
Screenshot from 2022-12-05 12-22-22
(tracking here container_memory_working_set_bytes{container="destination"}, which is the metric the OOM killer watches)

We took heap dumps before starting the rollouts and 25m after, and generated this comparative result:

# (using edge-22.12.1)
$ k -n linkerd port-forward linkerd-destination-5dd6c5985f-sl2jz 9996 &
$ curl http://localhost:9996/debug/pprof/heap?gc=1 > heap-before.out
$ while true; do kubectl -n emojivoto rollout restart deploy ; sleep 30; done
# wait 10m
$ curl http://localhost:9996/debug/pprof/heap?gc=1 > heap-after.out
$ go tool pprof -png --base heap-before.out heap-after.out

profile001

This shows 68.71% of the allocated memory during this time span was requested by k8s.io/apimachinery/pkg/watch.(*StreamWatcher).receive, so not linkerd's fault.

GC tuning

Lowering GOGC down to 1 forces GC to run as frequently as possible, but the curve above just smoothens up while overall memory consumption and trend remains the same.

However, using GOMEMLIMIT (introduced in go 1.19) helps out. If we set it to 35MiB we get this, triggering the rollouts at 14:32:
Screenshot from 2022-12-05 14-55-56
Before, we went from 26MB up to 42MB during the rollouts, and now from 25MB to 26MB in about the same time. Memory usage still climbs, but not as bad as before.

GOMEMLIMIT requires go 1.19, and it's actually recommended to use in containers with a memory limit; according to A Guide to the Go Garbage Collector:

Do take advantage of the memory limit when the execution environment of your Go program is entirely within your control, and the Go program is the only program with access to some set of resources (i.e. some kind of memory reservation, like a container memory limit).
A good example is the deployment of a web service into containers with a fixed amount of available memory.
In this case, a good rule of thumb is to leave an additional 5-10% of headroom to account for memory sources the Go runtime is unaware of.

Course of action

Besides bumping go to 1.19 in all the base images, we can add the env var GOMEMLIMIT to the manifests only if the helm value memory.limit exists (values-ha.yaml sets it for the control plane containers), set to 90% its value.

Ref #8270

Metadata

Metadata

Assignees

No one assigned

    Labels

    priority/P2Nice-to-have for Release

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions