Resource requests e limits no Kubernetes
1. Visão geral
Ao executar vários Pods no mesmo cluster, você precisa informar ao Kubernetes quanto CPU e memória cada container necessita. Se você não declarar, um serviço pode consumir todos os recursos do node e impactar outros serviços. Se declarar incorretamente, o Pod pode ser continuamente OOMKilled ou executar lentamente devido à limitação.
Este artigo explica o que são requests e limits e como o Kubernetes os utiliza. Também discute classes QoS e como configurar aplicações Java/Spring Boot apropriadamente com esses valores.
2. O que são requests e limits
Cada container em um Pod pode declarar dois tipos de valores:
- requests: a quantidade mínima de recursos garantida ao container. O scheduler usa esse valor para escolher em qual node colocar o Pod.
- limits: a quantidade máxima que o container tem permissão para usar. O kubelet e o container runtime aplicam esse limite em nível de kernel.
Os dois tipos de recursos mais comuns são cpu e memory. Também existem ephemeral-storage e recursos estendidos como GPU.
2.1. Unidades de CPU e memória
CPU é medida em cores. 1 significa um vCPU, enquanto 500m (500 millicores) significa meio core. Memória é medida em bytes. Kubernetes aceita sufixos binários (Ki, Mi, Gi) e sufixos decimais (K, M, G).
Iniciantes frequentemente confundem 512M com 512Mi. Para ver a diferença claramente, vamos escrever uma pequena classe para converter esses valores:
import java.util.Map;
public final class ResourceQuantity {
private static final Map<String, Long> SUFFIXES = Map.of(
"Ki", 1024L,
"Mi", 1024L * 1024,
"Gi", 1024L * 1024 * 1024,
"K", 1_000L,
"M", 1_000_000L,
"G", 1_000_000_000L
);
private ResourceQuantity() {
}
public static long cpuToMillicores(String value) {
if (value.endsWith("m")) {
return Long.parseLong(value.substring(0, value.length() - 1));
}
return Math.round(Double.parseDouble(value) * 1000);
}
public static long memoryToBytes(String value) {
for (String suffix : new String[]{"Ki", "Mi", "Gi", "K", "M", "G"}) {
if (value.endsWith(suffix)) {
String number = value.substring(0, value.length() - suffix.length());
return Long.parseLong(number) * SUFFIXES.get(suffix);
}
}
return Long.parseLong(value);
}
}
A função cpuToMillicores lida com dois formatos: com sufixo m e números decimais como 1.5. A função memoryToBytes verifica o sufixo de dois caracteres (Mi) antes do sufixo de um caractere (M), porque "512Mi" também termina com "i" e não deveria ser correspondido por erro.
Teste com JUnit 5 e AssertJ:
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class ResourceQuantityTest {
@Test
void givenMillicoreOrDecimal_whenConvert_thenReturnsMillicores() {
assertThat(ResourceQuantity.cpuToMillicores("500m")).isEqualTo(500);
assertThat(ResourceQuantity.cpuToMillicores("1.5")).isEqualTo(1500);
}
@Test
void givenBinaryAndDecimalSuffix_whenConvert_thenValuesDiffer() {
long mebibytes = ResourceQuantity.memoryToBytes("512Mi");
long megabytes = ResourceQuantity.memoryToBytes("512M");
assertThat(mebibytes).isEqualTo(536_870_912L);
assertThat(megabytes).isEqualTo(512_000_000L);
assertThat(mebibytes - megabytes).isEqualTo(24_870_912L);
}
}
Os dois valores diferem em cerca de 24 MB. Com um container perto do limite, isso é suficiente para decidir se o Pod vive ou é eliminado.
Nota: Não escreva
memory: 512m. O sufixomnormalmente significa “milli”, então esse valor é 0,512 bytes. O Kubernetes ainda aceitará, e o erro é muito difícil de detectar.
3. Declarando em um Deployment
O exemplo a seguir usa um order-service escrito em Spring Boot:
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
spec:
replicas: 2
selector:
matchLabels:
app: order-service
template:
metadata:
labels:
app: order-service
spec:
containers:
- name: app
image: nonla/order-service:1.4.0
ports:
- containerPort: 8080
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "1"
memory: "768Mi"
O significado de cada valor:
requests.cpu: 500m: o scheduler coloca o Pod em um node que tenha pelo menos 0,5 core não requisitado.requests.memory: 512Mi: similar, mas para memória.limits.cpu: "1": o container pode usar no máximo 1 core. Exceder isso causa throttling.limits.memory: 768Mi: exceder isso causa um kill do container pelo kernel.
Aplique o manifest e verifique o resultado:
kubectl apply -f order-service.yaml
kubectl get pod -l app=order-service -o jsonpath='{.items[0].spec.containers[0].resources}'
{"limits":{"cpu":"1","memory":"768Mi"},"requests":{"cpu":"500m","memory":"512Mi"}}
4. Como o scheduler usa requests
O scheduler não olha o uso real do node. Ele apenas soma o total de requests dos Pods já no node e compara com allocatable. Você pode ver esse número:
kubectl describe node worker-1 | grep -A 8 "Allocated resources"
Allocated resources:
(Total limits may be over 100 percent, i.e., overcommitted.)
Resource Requests Limits
-------- -------- ------
cpu 3200m (80%) 6 (150%)
memory 5Gi (66%) 9Gi (118%)
O resultado mostra duas coisas:
- O total de
requestsnunca ultrapassa 100% do allocatable. Se nenhum node tiver espaço suficiente, o Pod fica em estadoPendingcom eventoInsufficient cpuouInsufficient memory. - O total de
limitspode ultrapassar 100%. Isso é chamado overcommit. O overcommit é seguro quando os Pods não usam seus limites ao mesmo tempo.
Consequência: se você definir requests muito alto, o node parece “cheio” mesmo com CPU ociosa. Se definir muito baixo, muitos Pods são agrupados em um node e disputam recursos.
5. Quando o container ultrapassa o limit
CPU e memória são tratadas de formas muito diferentes.
5.1. CPU: é limitado
CPU é um recurso compressível. Quando o container usa toda sua quota, o kernel (via cgroup CFS) pausa o container até o próximo ciclo, geralmente 100ms. O container não morre, apenas fica mais lento. O sintoma é latência p99 aumentando drasticamente enquanto kubectl top parece normal.
kubectl top pod -l app=order-service
NAME CPU(cores) MEMORY(bytes)
order-service-7d9f8c6b5-k2xqp 998m 610Mi
order-service-7d9f8c6b5-tm4wz 412m 590Mi
O primeiro Pod está quase no limite de 1, então provavelmente está sendo limitado.
5.2. Memória: é eliminado
Memória é um recurso incompressível. Quando o container ultrapassa limits.memory, o OOM killer do kernel encerra o processo imediatamente:
kubectl describe pod order-service-7d9f8c6b5-k2xqp | grep -A 4 "Last State"
Last State: Terminated
Reason: OOMKilled
Exit Code: 137
Started: Tue, 13 Oct 2026 09:12:03 +0700
O código de saída 137 significa que o processo recebeu SIGKILL (128 + 9). A aplicação não tem chance de executar shutdown hooks ou gravar logs.
Outro cenário: o container usa mais do que requests.memory mas abaixo do limit. Quando o node fica com pouca memória, o kubelet evict esses Pods antes daqueles que usam exatamente o que requisitaram.
6. Classe QoS
Baseado em como você declara requests e limits, Kubernetes atribui a cada Pod uma classe QoS. Essa classe determina a ordem de eviction quando o node fica sem recursos.
| Classe QoS | Condição | Ordem de eviction |
|---|---|---|
Guaranteed |
Todo container tem requests == limits para CPU e memória |
Por último |
Burstable |
Tem pelo menos um request ou limit, mas não satisfaz Guaranteed | No meio |
BestEffort |
Nenhum container declara nada | Primeiro |
O Pod order-service acima é da classe Burstable. Você pode verificar com:
kubectl get pod -l app=order-service -o jsonpath='{.items[*].status.qosClass}'
Burstable Burstable
Para serviços críticos como payment-service, você pode definir requests iguais aos limits para que o Pod seja Guaranteed:
resources:
requests:
cpu: "1"
memory: "1Gi"
limits:
cpu: "1"
memory: "1Gi"
Se você define apenas limits e deixa requests em branco, Kubernetes automaticamente define requests iguais aos limits. O Pod se torna Guaranteed.
7. Aplicações Java em container
A partir do Java 10, a JVM reconhece cgroups. Java 21 suporta tanto cgroup v1 quanto v2. A JVM lê limits.memory para calcular o tamanho da heap, e lê limits.cpu para calcular o número de processadores. O problema está nos valores padrão: a heap máxima é apenas 25% do memory limit. Sua aplicação está desperdiçando a maior parte da memória que pediu.
Adicione esse componente ao Spring Boot para registrar o que a JVM realmente consegue ver:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
@Component
public class ContainerResourceLogger {
private static final Logger log = LoggerFactory.getLogger(ContainerResourceLogger.class);
@EventListener(ApplicationReadyEvent.class)
public void logResources() {
Runtime runtime = Runtime.getRuntime();
long maxHeapMi = runtime.maxMemory() / (1024 * 1024);
log.info("availableProcessors={}, maxHeap={}Mi",
runtime.availableProcessors(), maxHeapMi);
}
}
Com um limit de 768Mi e nenhuma configuração adicional, o log mostra:
INFO ContainerResourceLogger : availableProcessors=1, maxHeap=192Mi
Você deve ajustar a proporção de heap usando MaxRAMPercentage em vez de hardcoded -Xmx:
env:
- name: JAVA_TOOL_OPTIONS
value: "-XX:MaxRAMPercentage=75.0 -XX:+ExitOnOutOfMemoryError"
Após o redeploy:
INFO ContainerResourceLogger : availableProcessors=1, maxHeap=576Mi
Os 25% restantes são reservados para metaspace, thread stacks, code cache, direct buffers e GC. Não defina essa proporção acima de 90%. A heap pode não estar cheia, mas a memória total do processo já ultrapassou o limit, e o container é OOMKilled sem nenhum OutOfMemoryError nos logs.
Nota:
availableProcessorsé calculado a partir do CPU limit, depois arredondado para cima. Um limit de500mresulta em 1 processador. Nesse caso, a JVM normalmente escolheSerialGC, e pools de threads comoForkJoinPool.commonPool()têm apenas 1 worker. Se sua aplicação precisa de muitos threads paralelos, verifique esses valores novamente.
8. Definindo padrões para o namespace
Para evitar que Pods BestEffort entrem no cluster, você pode usar LimitRange para atribuir valores padrão por namespace:
apiVersion: v1
kind: LimitRange
metadata:
name: default-resources
namespace: shop
spec:
limits:
- type: Container
defaultRequest:
cpu: "200m"
memory: "256Mi"
default:
cpu: "500m"
memory: "512Mi"
Containers que não declaram resources receberão automaticamente esses valores quando forem criados.
Combine com ResourceQuota para limitar o total de recursos que todo o namespace pode requisitar:
apiVersion: v1
kind: ResourceQuota
metadata:
name: shop-quota
namespace: shop
spec:
hard:
requests.cpu: "8"
requests.memory: "16Gi"
limits.memory: "24Gi"
Quando um namespace tem quota para CPU ou memória, Pods que não declarem requests/limits correspondentes serão rejeitados pelo API server. Essa é a razão para usar LimitRange junto.
9. Erros comuns e quando não usar
- Não declarar nada. O Pod se torna
BestEffort, é evicted primeiro, e o scheduler não tem dados para balancear. - Memory limit inferior à necessidade no startup. Spring Boot geralmente usa mais memória durante a inicialização do contexto. Meça com
kubectl topem um ciclo de carga real antes de fixar um número. - Requests muito altos “para estar seguro”. O node parece cheio mesmo com CPU ociosa, levando o autoscaler a adicionar nodes desnecessários e custar caro.
- CPU limit muito restritivo para aplicações JVM. O compilador JIT e GC rodam em paralelo com a aplicação, então facilmente sofrem throttling na inicialização. O resultado é startup lento e readiness probe falhando.
- Definir
-Xmxigual ao memory limit. O container certamente seráOOMKilled, porque a JVM usa memória além da heap.
Nota: Muitos times escolhem não definir CPU limit para serviços sensíveis a latência, apenas definindo CPU request. O container pode então usar CPU ociosa do node sem sofrer throttling, enquanto o request ainda garante um mínimo. Essa abordagem não é adequada para clusters compartilhados entre múltiplos times, porque sem limit um serviço quebrado pode desacelerar todos os outros. Por outro lado, você deveria sempre definir memory limit: sem memory limit, um memory leak pode colocar todo o node em falta de memória.
10. Conclusão
Neste artigo, você viu que requests determina scheduling e o nível de recurso garantido, enquanto limits determina o teto. Exceder CPU limit causa throttling, enquanto exceder memory limit causa OOMKilled. A forma como você combina esses dois valores cria a classe QoS, que determina a ordem de eviction quando o node fica sem recursos.
Com aplicações Java, use MaxRAMPercentage para que a heap seja proporcional ao memory limit, e deixe espaço suficiente para a parte non-heap. Finalmente, use LimitRange e ResourceQuota para que todo o namespace siga o mesmo padrão. Os melhores números sempre vêm da medição de carga real, não de suposições.