Configurando limite de recursos em aplicações Java (JVM) no Kubernetes
Newsletter about software engineering, team management, team building, books and lots of notes I take after reading/studying (mine or yours)… :D
Fazer deploy de software desenvolvido usando tecnologias que foram criadas para ter escalabilidade vertical para escalar horizontalmente (micro serviço, nano serviço e etc) em produção pode gerar alguns desafios que não estamos preparados. Principalmente quando o software esta rodando em JVM e não foi declarado limites de recursos.
-Xms
, -Xmx
e seus problemas
Ao estudar sobre a JVM você provavelmente passara pelos parâmetros de alocação inicial (Xms
) e alocação máxima (Xmx
) de memória, os parâmetros funcionam rigorosamente bem. Trazendo um exemplo, ao definir -Xms128M
e Xmx256M
, e começar monitorar a aplicação com VisualVM, você alguma como essa:
java -Xms128m -Xmx256m hello.java
Ao ler a documentação da JVM (a parte de Sizing the Generations) parece que funcionara como magica, no exemplo acima a aplicação querer mínimo de 128Mb de memória (JVM alocará assim que a aplicação iniciar), mas não deixando passar do limite de 256Mb. Vamos dar uma olhada como ficou na prática:
public class Hello {
public static void main(String[] args) throws Exception {
while (true) {
new Hello().hello();
Thread.sleep(1000);
}
}
public void hello() {
System.out.println("Hello, World");
}
}
😱 não saiu como eu imaginava, parece que usou um pouco mais que 256Mb... O motivo desse comportamento é que a JVM usa memória para outros processos como metaspace, cache de código e etc, o Xmx
limita só sua aplicação não a JVM como todo.
Alguns cuidados que precisamos ter ao limitar recursos
Ao rodar software dentro do Kubernetes, temos que ter atenção no limite físico das máquinas que fazem parte do Cluster Kubernetes, para um pod não consumir todo recurso e outros pods acabar "morrendo".
Vou testar exemplificar...
Configurando Xms
e Xmx
na imagem Docker
Você pode passar estas flags (parâmetros) para JVM subir seu JAR na imagem Docker:
# Dockerfile
# ...
ENTRYPOINT ["java", "-Xms128M ", "-Xmx256m", "-jar", "hello-service-1.0.0.jar"]
Caso esteja usando Jib, pode declarar dentro do build.gradle
:
// build.gradle
jib {
to {
image = "hello-service:tag"
}
container {
environment = ["JAVA_TOOL_OPTIONS": "-Xms128M -Xmx256M"]
}
}
Limitando memória dos pods no Kubernetes
Ao usar Xmx
no pods você pode facilmente chegar em uma configuração que causará restart contante, por motivos de Exceed a Container's memory limit (OOM)
, por motivos a limite de memória, lembre do printscreen acima que mesmo limitando a aplicação a 256Mb a JVM usou mais de 700Mb. Dependendo do seu software deve ser definido o limite de recurso no próprio Kubernetes no YAML
do seu deployment:
apiVersion: apps/v1
kind: Deployment
metadata:
# ...
spec:
# ...
template:
# ...
spec:
containers:
- image: hello-service:tag
name: hello-service
livenessProbe:
initialDelaySeconds: 300
resources:
requests:
memory: 128Mi
limits:
memory: 256Mi
Vou assumir que você declarou os limites acima para seus pods, já que você também declarou os mesmos limites na sua imagem Docker via flags -Xms
e -Xmx
. Você faz deploy do seu software, ele funciona "perfeitamente", como não teve problema vai para cama descansar (geralmente colocar software em produção é cansativo). No dia seguinte você da roda kubectl get pods
para ter orgulho de ter colocado seu software no Kubernetes e se depara com isso:
❯ ~ kubectl get pods
NAME READY STATUS RESTARTS AGE
hello-service-3x85997760-qapo7 1/1 Running 156 9h
💣 você não esperava ver isso né? Agora que conhece como funciona a JVM (com printscren do htop) você já sabe o que esta acontecendo: o uso de memória da JVM não é só do seu software, então ele é morto diversas vezes (campo RESTARTS do get pods
) porquê o pod passou do limite de memória declarado e entra em um fluxo sem fim de restart.
Abordagem correta
Primeiro de tudo: assumiremos que os limites dentro da sua imagem Docker estejam bem definidos - com o que o software realmente precisa. Caso você queira reservar um valor fixo de memória para seu software e não ter concorrência com outro, você deve declarar Xms
= Xmx
, mas use com moderação.
Para evitar Exceed a Container's memory limit no pods do Kubernetes, você deve considerar os seguintes itens:
requests
(parâmetro doresources
) !=Xms
- o valor dorequests
deve ser maior queXms
, essa diferença depende do que seu software fará. Caso seja um micro serviço "simples", ~30% de acréscimo é o suficiente, mas se seu software tem muitas conexões externas ou qualquer operação que consome memória recomendo estudar a particularidade do seu software;limits
(parâmetro doresources
) !=Xmx
- O limite de memória sugerido para seu pod depende de muitas coisas e a imagem base do Java usada é um fator extremamente importante. Não vou passar um valor nem fórmulamagica, recomendo fazer diversos benchmarks para conhecer melhor o software que esta desenvolvendo;- Declarar
limits
com valores mais alto no começo é sempre uma boa prática, mas ir otimizando (diminuindo ou aumentando) conforme for conhecendo o comportamento do seu software em produção, sempre monitorando se o pod não entra em OOM; livenessProbe.initialDelaySeconds
- Tempo que o Kubernetes deve esperar seu software subir (JVM) antes de dar que não funcionou. Esse parâmetro é complicado, JVM tem fama de ser "lenta" para fazer boot, se seu software tem muitas conexões externas ela levará mais tempo.
Olhando para o caso que foi descrito acima: declarar requests
como ~320Mb e limits
como ~512Mb seria um ótimo começo dado os valores do Xms
e Xmx
.
Não existe bala de prata, a melhor forma de chegar em valores reais para limites de recursos no Kubernetes é conhecer seu software e fazer benchmarks para entender o comportamento dele, acada software é único e se comporta de uma forma diferente.
Recentemente coloquei um micro serviço usando Spring Boot para rodar e o serviço não subia, depois de horas e horas debugando e "brigando" com limite recurso do software e Kubernetes percebi que o problema era o
initialDelaySeconds
. #ficaadica