C
ClaudioGodoy
Kubernetes - Consumo de memória elevado em aplicações que escrevem em disco
Operações de entrada e saída no disco são custosas, a maioria dos sistemas operacionais implementam estratégias de
caching
na escrita e leitura de dados no sistema de arquivos. No caso do Kernel Linux, ele utiliza algumas estratégias, como por exemplo o Page Cache, cujo objetivo principal é armazenar os dados lidos pelo sistema de arquivos em cache
, para que a próxima operação de leitura esse dado esteja disponível em memória
.Ao analisar as métricas coletadas pelo Prometheus de uma aplicação Go rodando em um pod do Kubernetes, identificamos um consumo de memória muito superior ao esperado, dado que a aplicação não fazia nada além de escrever dados aleatórios em poucos arquivos. Através de uma série de analises e pesquisas conseguimos relacionar o comportamento de
page caching
ao problema em mencionado.
Aplicação
A aplicação usada como objeto de estudo esta no repositório: go-disk-writer. Ela é simples, e sua única função é escrever um
buffer
em arquivo, repetidas vezes até um certo limite:
Code:
func writeLoop(path string, maxFileSize int, count int) error {
buffer := make([]byte, 10*1024)
for i := 0; i < len(buffer); i++ {
buffer[i] = byte(i)
}
currentFile := 0
for {
file, err := openFile(path, currentFile)
if err != nil {
return err
}
var fileSize uint64 = 0
for {
written, err := file.Write(buffer)
if err != nil {
panic(err)
}
fileSize += uint64(written)
if fileSize >= uint64(maxFileSize) {
currentFile++
if currentFile >= (count) {
return nil
}
break
}
}
}
}
O trecho que código acima esta utilizando o pacote os. O sistema operacional realiza a chamada SYS_CALL write.
Kubectl top
O repositória da aplicação contém o arquivo de definição
pod.yaml
, do qual utilizei para rodar o pod
através do comando kubectl apply -f pod.yaml
.Para monitorar o consumo de recursos do
pod
, utilizei o comando watch kubectl top pod
:
Code:
NAME CPU(cores) MEMORY(bytes)
disk-writer 1m 408Mi
Após o termino da escrita em disco, a coluna
MEMORY
estabilizou na casa dos 400Mi
. Este comportamento é inesperado, pois além do consumo alto de memória durante o processamento, ele continua acima do esperado com a aplicação em repouso.De onde vem a informação do comando
kubectl top pod
? Esse foi o questionamento que levantei após me deparar com esse comportamento.A resposta está no pipeline de métricas do Kubernetes:
metrics api
cAdvisor: DaemonSet para coletar, agregar e expor métricas de contêineres incluído noKubelet
.
kubelet: Agente para gerenciar recursos de contêineres. As métricas de recursos são acessíveis usando os endpoints/metrics/resource
e/stats
da API dokubelet
.
node level resource metrics: API fornecida pelokubelet
para descobrir e recuperar estatísticas resumidas por nodes disponíveis através do endpoint/metrics/resource
.
metrics-server: Componente adicional do cluster que coleta e agrega métricas de recursos obtidas de cadakubelet
. Fornece as métricas para uso pelo HPA, VPA e pelo comandokubectl top
.
API de Métricas:API
doKubernetes
que suporta o acesso aCPU
e memória usados para escalonamento automático.
O comando
kubectl top
acessa as métricas de CPU
e memória através da API de Métricas, que inicia uma cadeia de comunicação entre componentes chegando até o container runtime.Working Set
Working set é a métrica em bytes que indica a quantidade de memória consumida por um
pod
. A documentação aponta que esse métrica é uma estimativa calculada pelo sistema operacional.Trecho retirado da documentação oficial: "Em um mundo ideal, oworking set
é a quantidade de memória em uso que não pode ser liberada sob pressão de memória. No entanto, o cálculo varia de acordo com o sistema operacional do host e geralmente faz uso intensivo de heurísticas para produzir uma estimativa."
cAdvisor
O cAdvisor é o componente mais próximo do container runtime, e ele é o responsável por coletar o working set, atualmente ele está na versão
v1.3
.Na função
setMemoryStats
do arquivo cadvisor/blob/master/container/libcontainer/handler.go, o seguinte calculo é realizado:
Code:
inactiveFileKeyName := "total_inactive_file"
if cgroups.IsCgroup2UnifiedMode() {
inactiveFileKeyName = "inactive_file"
}
workingSet := ret.Memory.Usage
if v, ok := s.MemoryStats.Stats[inactiveFileKeyName]; ok {
ret.Memory.TotalInactiveFile = v
if workingSet < v {
workingSet = 0
} else {
workingSet -= v
}
}
ret.Memory.WorkingSet = workingSet
Este trecho é importante, pois aqui é onde o
cAdvisor
captura as estatísticas do cgroup, e calcula o working_set
. Repare que ele subtrai a estatística inactive_file
.Cgroups
Cgroup é um recurso do
kernel
do Linux
que permite agrupar processos de forma hierárquica e controlar a alocação de recursos do sistema para esses grupos de maneira configurável. Com cgroups
, é possível gerenciar e limitar o uso de recursos como:CPU
: Definir quanto tempo de processamento cada grupo de processos pode utilizar.Memória
: Limitar a quantidade de memória que cada grupo de processos pode usar.I/O de Disco
: Controlar a quantidade de operações de entrada e saída que cada grupo pode realizar em dispositivos de armazenamento.Rede
: Gerenciar a largura de banda de rede disponível para cada grupo de processos.
Podemos inspecionar as estáticas de memória do
cgroup
do pod
que usamos como exemplo, através do passo a passo abaixo.Conectar no
pod
: kubectl exec disk-writer -it -- bash
.Navegar até o diretório das estatísticas do
cgroup
: cd /sys/fs/cgroup
.Listar todos os arquivos relacionados à memória:
ls | grep -e memory.stat -e memory.current
.
Code:
memory.current
memory.stat
O valor de
memory.current
representa a quantidade total de memória usada pelo cgroup, enquanto memory.stat
fornece uma visão detalhada sobre como essa memória está distribuída e gerida.Verificar o arquivo
memory.current
que é a quantidade total de memória alocado pelo cgroup
: cat memory.current
.
Code:
13124435968 # aproximadamente 12512 MB
Este valor é muito maior que os
408Mi
que o comando kubectl top pod
resultou anteriormente.Verifique o
inactive_file
no arquivo memory.stat
: cat memory.stat | grep inactive_file
Code:
inactive_file 12692201472 # aproximadamente 12108 MB
No parágrafo anterior, verificamos que o
cAdvisor
realiza a subtração do inactive_file
, para calcular o working_set
, se fizermos o mesmo, o resultado seria: 12512MB−12108MB=404MB
.O
pod
que utilizamos como exemplo consume aproximadamente 408Mi
, valor muito acima do esperado para uma aplicação que um baixo nível de alocação de memória.Por dentro do arquivo
memory.stat
, realizei um calculo de distribuição percentual das maiores estatisticas em relação ao memory.current
, e o resultado foi: slab_reclaimable: 421,722,560 bytes (94.2%)
.slab_reclaimable
Conclusão
A análise detalhada sobre o consumo de memória em aplicações que realizam operações de escrita em disco no Kubernetes revelou um comportamento específico do sistema de arquivos e do
kernel Linux
. O uso intensivo do Page Cache
, para otimizar as operações de leitura e escrita em disco pode resultar em um alto consumo de memória, mesmo após a conclusão das operações de escrita. Esse comportamento é refletido na métrica working_set
, que exclui a memória cacheada inativa, resultando em discrepâncias entre o consumo real e o reportado pelo comando kubectl top
.O estudo demonstrou que uma parcela significativa da memória alocada estava relacionada a objetos slab reclaimable, que são estruturas de dados cacheadas pelo kernel para otimizar a alocação de memória. Esses dados, embora ainda armazenados em cache, podem ser liberados quando o sistema estiver sob pressão de memória, o que explica a diferença entre o consumo de memória observado diretamente no
cgroup
e o valor reportado pelas métricas de Kubernetes.Portanto, o comportamento de alto consumo de memória em aplicações que escrevem em disco pode ser atribuído a essa estratégia de caching do sistema operacional. Embora não represente necessariamente um problema de desempenho, é crucial entender como o Kubernetes e o kernel Linux gerenciam memória para otimizar e monitorar o uso de recursos adequadamente em ambientes de produção.
Continue reading...