Zarządzanie Kubernetes: memory request i limit w praktyce 19 kwietnia 2026 | 7 min czytania

Zarządzanie Kubernetes: memory request i limit w praktyce

Jest to druga część artykułu o zarządzaniu zasobami w Kubernetes. W pierwszej części omówiliśmy, jak Kubernetes zarządza CPU, a w tej części skupimy się na pamięci.

Jak Kubernetes zarządza pamięcią?

Z poprzedniej części (link) dowiedzieliśmy się, że Kubernetes wykorzystuje cgroups do zarządzania zasobami, możemy przyjrzeć się bliżej, jak działa to w przypadku pamięci. Skupimy się na cgroups v2, ponieważ są one już od kernela w wersji 4.5, czyli dostępne domyślnie w takich dystrybucjach jak Ubuntu 16.04, Red Hat Enterprise Linux 9 (a dostępne od wersji 8), Amazon Linux 2023. Warto zaznaczyć, że sam mechanizm cgroups v2 oferuje memory.min pozwalający zagwarantować minimalną ilość pamięci dla procesu (w v1 takiego mechanizmu nie było). Jednak w standardowej konfiguracji Kubernetes memory request służy wyłącznie schedulerowi - niezależnie od wersji cgroups. Kubelet nie przekazuje wartości requesta do kernela jako memory.min, więc proces w kontenerze może zostać zreclaimowany lub zabity OOM nawet jeśli jego zużycie mieści się w zadeklarowanym request. Dopiero feature gate MemoryQoS (alpha od 1.22, wciąż alpha i domyślnie wyłączony w 1.36) sprawia, że kubelet mapuje requests.memory na memory.min w cgroup v2. Funkcja pozostaje w alfie z powodu potencjalnego livelocka kernela przy agresywnej alokacji w pobliżu memory.high — wymaga kernela ≥ 5.9 oraz wspieranego runtime’u (containerd/CRI-O).

W dokumentacji kontrolera pamięci https://www.kernel.org/doc/Documentation/admin-guide/cgroup-v2.rst jest napisane że kontroler śledzi utylizacje:

  • Userland memory - page cache and anonymous memory.
  • Kernel data structures such as dentries and inodes.
  • TCP socket buffers

Oraz posiada takie funkcje jak:

  • memory.current - pokazującą aktualne zużycie pamięci przez procesy w cgroupie
  • memory.min - pozwalającą na zagwarantowanie minimalnej ilości pamięci dla procesu (memory request). “hard protection”.
  • memory.low - ustawiająca próg, który nie będzie odzyskiwany, dopóki będą dostępne zasoby do odzyskania z innej cgrupy, która nie ma memory.low lub przekracza memory.low. “best effort memory protection”.
  • memory.high - jest używana do określenia progu, powyżej którego system operacyjny zacznie ograniczać spowalniać nowe alokacje dla tej cgroupy, ale jeszcze nie wykona OOM kill.
  • memory.max - jest używana do określenia maksymalnej ilości pamięci, którą proces może wykorzystać. Jeśli cgroupa przekroczy ten limit, proces wewnatrz tej cgroupy zostanie zabity przez system operacyjny z powodu braku pamięci. (OOM kill).

Memory Request – memory.min

Memory request w połączeniu z funkcją Memory QoS jest mapowany na memory.min w cgroups v2. Oznacza to, że jest to minimalna ilość pamięci, którą gwarantujemy cgroupie - pamięć ta nie zostanie odzyskana przez jądro kernel, o ile użycie mieści się w tej granicy. Jeśli ustawimy w Deployment:

resources:
  requests:
    memory: 500m

to - przy włączonym feature gate MemoryQoS - kubelet przekaże tę wartość do runtime’u kontenerowego (containerd / CRI-O) przez pole Unified w CRI, a ten ustawi memory.min w cgroupie kontenera. Robi to fragment kodu ResourceConfigForPod w kubelecie:

func ResourceConfigForPod(allocatedPod *v1.Pod, enforceCPULimits bool, cpuPeriod uint64,
 enforceMemoryQoS bool) *ResourceConfig {

  // ...

if enforceMemoryQoS {
	memoryMin := int64(0)
	if request, found := reqs[v1.ResourceMemory]; found {
		memoryMin = request.Value()
	}
	if memoryMin > 0 {
		result.Unified = map[string]string{
			Cgroup2MemoryMin: strconv.FormatInt(memoryMin, 10),
		}
	}
}

  // ...

Warto zwrócić uwagę, że request.Value() zwraca wartość w bajtach, więc w naszym przykładzie memory.min będzie miało wartość 524288000 (czyli 500 * 1024 * 1024), a nie dosłownie „500Mi" zapisane w pliku cgroupy.

Parametr enforceMemoryQoS w kodzie kubeleta jest pochodną feature gate MemoryQoS, który domyślnie jest wyłączony. Oznacza to, że w standardowej konfiguracji Kubernetes:

  • memory request nie jest przekazywany do kernela jako memory.min - kernel w ogóle nie dostaje tej informacji
  • memory request służy wyłącznie schedulerowi kube-scheduler do decyzji o umieszczeniu poda na nodzie

Gdy MemoryQoS jest włączony i kubelet ustawi memory.min na cgroupie poda, kernel traktuje tę wartość jako twardą gwarancję: dopóki zużycie pamięci cgroupy mieści się w efektywnej granicy min, jej strony nie zostaną odzyskane przez reclaim pod żadnym warunkiem - nawet przy presji pamięciowej na nodzie. Jeśli kernel nie jest w stanie utrzymać tej gwarancji, uruchamia OOM killera, który może zterminować procesy spoza chronionej cgroupy, żeby zwolnić potrzebną pamięć. To fundamentalna różnica w porównaniu ze standardową konfiguracją, gdzie pod z requestem 512Mi przy presji pamięci na nodzie jest zwykłym kandydatem do reclaimu i eviction jak każdy inny.

Memory Limit – memory.max

Memory limit w Kubernetes mapuje się na wartość memory.max w cgroups v2 (odpowiednik memory.limit_in_bytes z cgroups v1). Jest to twardy limit pamięci, który cgroupa może wykorzystać - jeśli procesy w niej spróbują alokować więcej, kernel wywoła OOM killera, który zabije proces(y) w tej cgroupie.

Jeśli ustawimy w Deployment:

resources:
  requests:
    memory: 250Mi
  limits:
    memory: 500Mi

to kubelet w funkcji ResourceConfigForPod pobierze limit w bajtach i zapisze go do struktury ResourceConfig:

if limit, found := limits[v1.ResourceMemory]; found {
    memoryLimits = limit.Value()
}
// ...
result.Memory = &memoryLimits

Ta wartość jest następnie przekazywana do runtime’u kontenerowego (containerd / CRI-O) przez CRI, a ten ustawia ją jako memory.max w cgroupie kontenera oraz agregowany limit w cgroupie poda. Ponownie - limit.Value() zwraca liczbę w bajtach, więc 500Mi trafia do cgroupy jako 524288000.

Kluczowy szczegół: limit jest ustawiany warunkowo - zależnie od klasy QoS

Tu pojawia się najciekawsza część ResourceConfigForPod. W przeciwieństwie do memory request (który przy włączonym enforceMemoryQoS jest ustawiany zawsze, gdy istnieje), memory limit w cgroupie jest ustawiany tylko wtedy, gdy Kubernetes ma na nim coś sensownego do zapisania - a to zależy od klasy QoS poda:

func ResourceConfigForPod(allocatedPod *v1.Pod, enforceCPULimits bool, cpuPeriod uint64, enforceMemoryQoS bool) *ResourceConfig {

  // ...

	// determine the qos class
	qosClass := v1qos.GetPodQOS(allocatedPod)

	// build the result
	result := &ResourceConfig{}
	if qosClass == v1.PodQOSGuaranteed {
		result.CPUShares = &cpuShares
		result.CPUQuota = &cpuQuota
		result.CPUPeriod = &cpuPeriod
		result.Memory = &memoryLimits
	} else if qosClass == v1.PodQOSBurstable {
		result.CPUShares = &cpuShares
		if cpuLimitsDeclared {
			result.CPUQuota = &cpuQuota
			result.CPUPeriod = &cpuPeriod
		}
		if memoryLimitsDeclared {
			result.Memory = &memoryLimits
		}
	} else {
		shares := uint64(MinShares)
		result.CPUShares = &shares
	}
	result.HugePageLimit = hugePageLimits

	if enforceMemoryQoS {
		memoryMin := int64(0)
		if request, found := reqs[v1.ResourceMemory]; found {
			memoryMin = request.Value()
		}
		if memoryMin > 0 {
			result.Unified = map[string]string{
				Cgroup2MemoryMin: strconv.FormatInt(memoryMin, 10),
			}
		}
	}

Guaranteed - wszystkie kontenery w podzie mają zadeklarowane requests == limits dla CPU i pamięcix. Kubelet bezwarunkowo ustawia result.Memory = &memoryLimits, więc cgroupa poda dostaje konkretny memory.max.

Burstable - przynajmniej jeden kontener ma jakiś request lub limit, ale warunki dla Guaranteed nie są spełnione. Tu memoryLimitsDeclared staje się istotne. Jest ustawione na true na początku funkcji, ale zmieniane na false przez ContainerFn, gdy którykolwiek kontener nie ma zadeklarowanego limitu pamięci (res.Memory().IsZero()):

BestEffort - żaden kontener nie ma ani requestów, ani limitów. Kod idzie w gałąź else i ustawia wyłącznie minimalne CPUShares (MinShares, czyli 2 - najniższa możliwa wartość w cgroupach). result.Memory nie jest ustawiane w ogóle, więc cgroupa poda nie ma własnego memory.max. Pod może używać pamięci tak długo, jak jest dostępna - ograniczony jest dopiero przez limit cgroupy kubepods-besteffort.slice i przez to, że w razie presji na node’zie jest pierwszy w kolejce do eksmisji.

Podsumowanie

Zarządzanie pamięcią w Kubernetes jest bardziej złożone niż zarządzanie CPU, głównie ze względu na różnice w implementacji cgroups v1 i v2 oraz fakt, że funkcja MemoryQoS - pozwalająca na respektowanie memory request przez kernel - jest nadal w fazie alpha i wymaga świadomego włączenia.

Warto zapamiętać kilka praktycznych wniosków z analizy ResourceConfigForPod:

  • Memory request bez MemoryQoS to tylko podpowiedź dla schedulera - kernel w ogóle nie wie, że pod „prosił" o jakąś pamięć. Dopiero włączenie feature gate MemoryQoS sprawia, że request trafia do cgroupy jako memory.min i staje się realną gwarancją.
  • Memory limit zachowuje się inaczej w zależności od klasy QoS - Guaranteed zawsze dostaje memory.max na poziomie cgroupy poda, Burstable tylko gdy wszystkie kontenery mają zadeklarowany limit, a BestEffort nigdy nie dostaje własnego limitu i jest ograniczany wyłącznie przez nadrzędną cgroupę kubepods-besteffort.slice.
  • Klasa QoS wynika z konfiguracji resources, nie ustawia się jej bezpośrednio - jedno pominięte limits.memory w jednym z kontenerów może przenieść cały pod z Guaranteed do Burstable i zmienić jego zachowanie pod presją pamięciową oraz kolejność eksmisji.
  • OOM killer w cgroups v2 z Kubernetes 1.28+ zabija całą grupę procesów kontenera naraz (memory.oom.group=1), a nie pojedynczy proces o najwyższym oom_score - to ważna zmiana zachowania dla aplikacji z wieloma procesami w jednym kontenerze (np. sidecary, workery, PostgreSQL).

Konsekwencja praktyczna: jeśli ustawiasz wyłącznie requesty bez limitów, licząc na „rezerwację" pamięci - to nie działa tak, jak się wydaje. Bez MemoryQoS kernel nie ma pojęcia o Twoich requestach, a sąsiedni pod w tej samej klasie QoS może Ci tę pamięć zabrać, zanim Twój workload ją w ogóle zaalokuje.

Jeśli chcesz zweryfikować architekturę Kubernetes, wyeliminować problemy z ograniczaniem przepustowości lub poprawić wydajność klastra, nasz zespół oferuje kompleksowe konsultacje Kubernetes oparte na rzeczywistym doświadczeniu.