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 też zanaczyć, że w starszej wersji cgroup v1 memory request był ignorowany przez kernela, a jedynie wykorzystywany przez scheduler, ponieważ ta wersja nie posiadała mechanizmu pozwalającego na zagwarantowanie minimalnej ilości pamięci dla procesu, którą w v2 jest memory.min. Nawet jeżeli pod miał request: 512Mi to kernel mógł go zignorować, a proces mógł zostać zabity przez system operacyjny z powodu braku pamięci (OOM kill). W cgroups v2 memory request jest respektowany, ponieważ posiada funkcję memory.min, która pozwala na zagwarantowanie minimalnej ilości pamięci dla procesu.

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.

Gdy memory.min jest ustawione, jeśli użycie pamięci cgroupy mieści się w jej efektywnej granicy min, pamięć tej cgroupy nie zostanie odzyskana w żadnych warunkach. Jeśli kernel nie jest w stanie utrzymać co najmniej memory.min bajtów dla procesów cgroupy, wywoła OOM killera - potencjalnie terminując procesy poza tą cgroupą, żeby zwolnić pamięć.

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

MemoryQoS jest w Kubernetes od wersji 1.22 (sierpień 2021) i po ponad czterech latach wciąż pozostaje w fazie alpha i jest wyłączony domyślnie - również w v1.36. Prace zostały zastopowane z powodu potencjalnego livelocka, który może wystąpić, gdy proces w kontenerze agresywnie alokuje pamięć w pobliżu memory.high. Dlatego w produkcji MemoryQoS należy włączać świadomie, po weryfikacji wersji kernela (wymagany ≥ 5.9 z powodu wspomnianego livelocka) oraz runtime’u kontenerowego.

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.