Estou tentando entender como as solicitações e limites de memória funcionam com o cgroup v2. No manifesto do Kubernetes, podemos configurar a solicitação e o limite de memória. Esses valores são então usados para configurar a interface do cgroup:
- memory.min está definido para solicitação de memória
- memory.max está definido como limite de memória
- memory.high é definido como limite de memória * 0,8, a menos que a solicitação de memória == limite, caso em que memory.high permanece indefinido
- memory.low está sempre desmarcado
memory.max é bastante autoexplicativo: quando um processo no cgroup tenta alocar uma página e isso coloca o uso de memória acima de memory.max e não é possível recuperar páginas suficientes do cgroup para satisfazer a solicitação dentro de memory.max, então o OOM killer é invocado para encerrar um processo dentro do cgroup. memory.high é mais difícil de entender: a documentação do kernel diz que o cgroup é colocado sob "alta pressão de recuperação" quando a marca d'água alta é atingida, mas o que isso significa exatamente?
Mais adiante diz:
Quando atingido, ele limita as alocações, forçando-as a uma recuperação direta para eliminar o excesso, mas nunca invoca o assassino OOM.
Estou correto em assumir que isso significa que quando o cgroup tenta alocar uma página além da marca d'água memory.high, ele vai olhar sincronicamente para o lruvecs e tentar recuperar o máximo de páginas do final das listas até que esteja de volta abaixo da marca d'água alta? Ou a "pressão de recuperação" é algo que acontece assincronamente (por meio do kswapd)?
Pergunta 2: Qual é o objetivo de usar memory.high no Kubernetes? Até onde eu sei, os nós do Kubernetes normalmente são executados sem espaço de swap. As únicas páginas que podem ser recuperadas são páginas anônimas (se houver swap suficiente disponível) e cache de página. Como não há swap, isso deixa apenas o cache de página. O problema é que o cache de página também seria recuperado ao atingir memory.max, antes de invocar o OOM killer como último recurso se nada puder ser recuperado. Então memory.high é essencialmente inútil:
Enquanto o cache de página for usado, ele sempre pode ser recuperado e memory.max também faria isso. Com memory.high, estamos apenas limitando o aplicativo antes do que precisamos. Poderíamos muito bem definir memory.max mais baixo em primeiro lugar.
Se nenhum cache de página significativo for usado (o que provavelmente é o caso da maioria dos aplicativos que executam o Kubernetes hoje), então nada pode ser recuperado, portanto, não há limitação (nenhuma paginação de memória anônima não utilizada, nenhuma thrashing visível nas informações de bloqueio de pressão que nos avisariam) e encontraremos memory.max sem saber. Usar memory.high não tem efeito.
Não acho que ele vá imediatamente para a recuperação direta (síncrona, como você chama) naquele ponto, mas não tenho certeza. Na minha experiência, ele acabará atingindo a recuperação direta com a memória. Altas demandas são esticadas demais. Certamente aumentará a pressão da memória de qualquer maneira.
Executar sem espaço de swap geralmente é estúpido e tem sido assim há muito tempo. Independentemente disso, no entanto, as únicas páginas que são recuperáveis estão, de fato, principalmente no cache de páginas. Existem outras estratégias que podem acontecer.
Mas as opções são escassas.
Em geral, suas observações correspondem às minhas realidades também quando não há swap para remover páginas anônimas - MemoryHigh torna o thrashing muito pior, pois você mantém o cache da sua página no mínimo absoluto e acaba fazendo muito IO.
Também o desativamos em instâncias LXD/LXC, pois ele causa sobrecarga desnecessária (é um limite rígido no código que temos que voltar mais tarde para "consertar").
No entanto, o MemoryLow pode ser útil como um mecanismo de reserva suave para dizer ao kernel "não roube páginas deste grupo de controle abaixo deste intervalo de memória, escolha outra vítima".