Guia Completo de Cgroups do Systemd para Limitação e Isolamento de Recursos

Use cgroups, slices e propriedades de unidades do systemd para limitar CPU, memória e I/O sem editar arquivos cgroup brutos.

Guia Completo de Cgroups do Systemd para Limitação e Isolamento de Recursos

O Systemd já coloca serviços em grupos de controle do Linux. Você não precisa criar diretórios cgroup brutos manualmente para impedir que um worker em lote consuma toda a máquina. Em muitos casos, você pode adicionar algumas propriedades a um serviço ou slice, recarregar o systemd e obter controles de CPU, memória, tarefas e I/O que sobrevivem à reinicialização e aparecem nas ferramentas normais do systemctl.

O truque é escolher o tipo certo de limite. Um limite rígido de memória pode proteger o host, mas matar o serviço se definido muito baixo. Pesos de CPU são suaves até o sistema ficar ocupado. Cotas de CPU são rigorosas, mas podem adicionar latência. Limites de I/O dependem da pilha de armazenamento e da versão do cgroup. O controle de recursos não é uma caixa de seleção; é uma troca operacional.

Entendendo os Grupos de Controle (cgroups)

Antes de mergulhar na implementação do systemd, é essencial compreender os conceitos fundamentais dos cgroups. Cgroups são um mecanismo hierárquico no kernel Linux que permite agrupar processos e, em seguida, atribuir políticas de gerenciamento de recursos a esses grupos. Essas políticas podem incluir:

  • CPU: Limitando o tempo de CPU, priorizando o acesso à CPU.
  • Memória: Definindo limites de uso de memória, prevenindo condições de falta de memória (OOM).
  • I/O: Limitando operações de leitura/gravação em disco.
  • Rede: O controle de rede é possível através do controle de tráfego Linux e ferramentas relacionadas, mas as propriedades de unidade integradas do systemd são focadas principalmente em CPU, memória, contagem de processos, acesso a dispositivos e I/O de bloco.
  • Acesso a Dispositivos: Controlando o acesso a dispositivos específicos.

O kernel expõe as configurações de cgroup através de um sistema de arquivos virtual, normalmente montado em /sys/fs/cgroup. Cada controlador (por exemplo, cpu, memory) tem seu próprio diretório, e dentro deles, hierarquias de diretórios representam grupos e seus limites de recursos associados.

Arquitetura de Gerenciamento de Cgroup do Systemd

O Systemd abstrai a complexidade da manipulação direta de cgroup, fornecendo um sistema estruturado de gerenciamento de unidades. Ele organiza os processos em uma hierarquia de unidades, que são então mapeadas para hierarquias de cgroup. Os principais tipos de unidade relevantes para o gerenciamento de recursos são:

  • Slices: São contêineres abstratos para unidades de serviço. Slices formam uma hierarquia, permitindo a delegação de recursos. Por exemplo, um slice para sessões de usuário pode conter slices para aplicativos individuais. O systemd cria automaticamente slices para serviços do sistema, sessões de usuário e máquinas virtuais/contêineres.
  • Scopes: São normalmente usados para grupos temporários ou criados dinamicamente de processos, frequentemente associados a sessões de usuário ou serviços do sistema que não são gerenciados como unidades de serviço completas. Eles são transitórios e existem enquanto os processos dentro deles estiverem em execução.
  • Serviços: São as unidades fundamentais para gerenciar daemons e aplicações. Quando uma unidade de serviço é iniciada, o systemd coloca seus processos em uma hierarquia de cgroup, geralmente dentro de um slice. Limites de recursos podem ser aplicados diretamente às unidades de serviço.

A hierarquia padrão do systemd geralmente se parece com isso:

-.slice (Slice raiz)
  |- system.slice
  |  |- <nome_do_serviço>.service
  |  |- outro-serviço.service
  |  ...
  |- user.slice
  |  |- user-1000.slice
  |  |  |- session-c1.scope
  |  |  |  |- <aplicativo>.service (se iniciado pelo usuário)
  |  |  |  ...
  |  |  ...
  |  ...
  |- machine.slice (para VMs/contêineres)
  ...

Aplicando Limites de Recursos com Arquivos de Unidade do Systemd

O Systemd permite que você especifique limites de recursos de cgroup diretamente nos arquivos de unidade .service, .slice ou .scope. Essas diretivas são colocadas nas seções [Service], [Slice] ou [Scope], respectivamente.

Limites de CPU

As principais diretivas para controle de recursos de CPU são:

  • CPUQuota=: Limita o tempo total de CPU que a unidade pode usar. É especificado como uma porcentagem (por exemplo, 50% para meio núcleo de CPU) ou uma fração de um núcleo de CPU (por exemplo, 0.5). Também é possível especificar um valor em microssegundos por período. O período padrão é 100ms.
  • CPUWeight=: Define uma ponderação relativa para o tempo de CPU em sistemas cgroup v2. Uma unidade com um peso maior recebe uma parcela maior quando há contenção, mas não reserva CPU quando a máquina está ociosa.
  • CPUShares=: Ponderação mais antiga da era cgroup v1. Prefira CPUWeight= em distribuições modernas, a menos que você saiba que precisa de compatibilidade com v1.
  • CPUQuotaPeriodSec=: Define o período para CPUQuota. O padrão é 100ms.

Exemplo: Limitando um servidor web a 75% de um núcleo de CPU:

Crie ou edite um arquivo de serviço, por exemplo, /etc/systemd/system/mywebapp.service:

[Unit]
Description=Minha Aplicação Web

[Service]
ExecStart=/usr/bin/mywebapp
User=webappuser
Group=webappgroup

# Limitar a 75% de um núcleo de CPU
CPUQuota=75%

[Install]
WantedBy=multi-user.target

Após criar ou modificar o arquivo de serviço, recarregue o daemon do systemd e reinicie o serviço:

sudo systemctl daemon-reload
sudo systemctl restart mywebapp.service

Limites de Memória

Os limites de memória são controlados por diretivas como:

  • MemoryMax=: Define um limite rígido na quantidade de memória que os processos da unidade podem consumir. Pode ser especificado em bytes ou com sufixos como K, M, G, T (por exemplo, 512M).
  • MemoryLimit=: Ortografia mais antiga mantida em alguns sistemas para compatibilidade. Prefira MemoryMax= em versões modernas do systemd.
  • MemoryHigh=: Define um limite flexível. Quando este limite é aproximado, a recuperação de memória (swap) é acionada de forma mais agressiva, mas o limite rígido ainda não é aplicado.
  • MemorySwapMax=: Limita a quantidade de espaço de swap que a unidade pode usar.

Exemplo: Limitando um banco de dados a 2GB de RAM:

Crie ou edite um arquivo de serviço, por exemplo, /etc/systemd/system/mydb.service:

[Unit]
Description=Meu Serviço de Banco de Dados

[Service]
ExecStart=/usr/bin/mydb
User=dbuser
Group=dbgroup

# Limitar memória a 2 Gigabytes
MemoryMax=2G

[Install]
WantedBy=multi-user.target

Recarregue e reinicie:

sudo systemctl daemon-reload
sudo systemctl restart mydb.service

Limites de I/O

A limitação de I/O pode ser controlada usando diretivas como:

  • IOWeight=: Define um peso relativo para operações de I/O. Valores mais altos dão mais prioridade de I/O. Faixa de 1 a 1000 (padrão 500).
  • IOReadBandwidthMax=: Limita a largura de banda de leitura de I/O. Especificado como [<dispositivo>] <bytes_por_segundo>. Por exemplo, IOReadBandwidthMax=/dev/sda 100M limita operações de leitura em /dev/sda a 100MB/s.
  • IOWriteBandwidthMax=: Limita a largura de banda de gravação de I/O. Formato semelhante a IOReadBandwidthMax.

Exemplo: Limitando um serviço de processamento em segundo plano a 50MB/s em um disco específico:

Crie ou edite um arquivo de serviço, por exemplo, /etc/systemd/system/batchproc.service:

[Unit]
Description=Serviço de Processamento em Lote

[Service]
ExecStart=/usr/bin/batchproc
User=batchuser
Group=batchgroup

# Limitar operações de gravação a 50MB/s em /dev/sdb
IOWriteBandwidthMax=/dev/sdb 50M

# Dar uma prioridade de leitura moderada
IOWeight=200

[Install]
WantedBy=multi-user.target

Recarregue e reinicie:

sudo systemctl daemon-reload
sudo systemctl restart batchproc.service

Gerenciando e Monitorando Cgroups

O Systemd fornece ferramentas para inspecionar e gerenciar os cgroups associados às suas unidades.

Inspecionando o Status do Cgroup

O comando systemctl status fornece informações sobre a associação de cgroup de uma unidade e o uso de recursos.

systemctl status mywebapp.service

Procure por linhas indicando o caminho do cgroup. Por exemplo:

● mywebapp.service - Minha Aplicação Web
     Loaded: loaded (/etc/systemd/system/mywebapp.service; enabled; vendor preset: enabled)
     Active: active (running) since Tue 2023-10-27 10:00:00 UTC; 1 day ago
       Docs: man:mywebapp(8)
   Main PID: 12345 (mywebapp)
      Tasks: 5 (limit: 4915)
     Memory: 15.5M
        CPU: 2h 30m 15s
      CGroup: /system.slice/mywebapp.service
              └─12345 /usr/bin/mywebapp

Você também pode inspecionar diretamente o sistema de arquivos cgroup:

systemd-cgls # Exibe a hierarquia de cgroup gerenciada pelo systemd
systemd-cgtop # Semelhante ao top, mas para cgroups

Para ver os limites específicos aplicados ao cgroup de um serviço:

# Para limites de memória em um host típico cgroup v2
cat /sys/fs/cgroup/system.slice/mywebapp.service/memory.max

# Para limites de CPU
cat /sys/fs/cgroup/system.slice/mywebapp.service/cpu.max

Os caminhos e nomes de arquivos exatos variam de acordo com a versão do cgroup e a distribuição. Em sistemas cgroup v1, caminhos específicos do controlador como /sys/fs/cgroup/memory/... ainda podem existir. Em sistemas cgroup v2, a hierarquia unificada sob /sys/fs/cgroup/... é a visualização normal.

Modificando Limites de Cgroup em Tempo Real

Embora seja uma boa prática definir limites em arquivos de unidade, você pode ajustá-los temporariamente usando systemctl set-property:

sudo systemctl set-property mywebapp.service CPUQuota=50%

Dependendo da versão do systemd e das flags, set-property pode escrever um drop-in em /etc/systemd/system.control/ para propriedades persistentes. Use systemctl cat mywebapp.service e systemctl show mywebapp.service -p CPUQuota -p MemoryMax para confirmar o que aconteceu. Para infraestrutura como código e revisão por pares, um drop-in de unidade explícito geralmente é mais claro.

Slices para Delegação de Recursos

Slices são poderosos para gerenciar grupos de serviços ou aplicações. Você pode definir limites de recursos em um slice, e todos os serviços ou scopes dentro desse slice herdarão ou serão restringidos por esses limites.

Exemplo: Criando um slice dedicado para trabalhos em lote intensivos em recursos:

Crie um arquivo de slice, por exemplo, /etc/systemd/system/batch.slice:

[Unit]
Description=Slice de Processamento em Lote

[Slice]
# Limitar CPU total para todos os trabalhos neste slice a 1 núcleo
CPUQuota=100%
# Limitar memória total a 4GB
MemoryMax=4G

Agora, você pode configurar serviços para serem executados dentro deste slice usando a diretiva Slice= em seus arquivos .service:

[Unit]
Description=Trabalho em Lote Específico

[Service]
ExecStart=/usr/bin/mybatchjob

# Colocar este serviço no batch.slice
Slice=batch.slice

[Install]
WantedBy=multi-user.target

Recarregue o systemd, ative/inicie o slice se necessário (embora seja frequentemente ativado implicitamente) e inicie o serviço.

sudo systemctl daemon-reload
sudo systemctl start mybatchjob.service

Esta abordagem permite agrupar processos relacionados e gerenciar seu consumo coletivo de recursos.

Melhores Práticas e Considerações

  • Comece com Limites Incrementais: Ao definir limites, comece com valores conservadores e aumente gradualmente conforme necessário. Limites agressivos podem desestabilizar aplicações.
  • Monitore: Monitore regularmente o uso de recursos do seu sistema e o impacto das suas configurações de cgroup. Ferramentas como systemd-cgtop, htop, top e iotop são inestimáveis.
  • Entenda Cgroup v1 vs. v2: O systemd suporta tanto cgroup v1 quanto v2. Embora muitas diretivas sejam semelhantes, o v2 oferece uma hierarquia unificada e algumas diferenças comportamentais. Certifique-se de saber qual versão seu sistema está usando se encontrar problemas complexos.
  • Priorização vs. Limites Rígidos: Use CPUWeight para priorização quando os recursos são escassos, e CPUQuota para limites rígidos. Da mesma forma, MemoryHigh é para pressão antes do limite rígido, e MemoryMax é o limite rígido.
  • Serviço vs. Slice: Use unidades de serviço para aplicações individuais e slices para gerenciar grupos de aplicações relacionadas ou pools de recursos.
  • Documentação: Documente claramente os limites de recursos aplicados a serviços críticos, especialmente em ambientes de produção.
  • OOM Killer: Esteja ciente de que, se um processo exceder seu limite MemoryMax, o killer Out-Of-Memory (OOM) do kernel pode terminá-lo, mesmo que esteja dentro de um cgroup. O systemd pode gerenciar como o OOM killer se comporta para cgroups específicos usando diretivas como OOMPolicy=.

Uma Maneira Mais Segura de Implementar Limites

Comece com observação. Antes de adicionar limites, veja como o serviço se comporta durante a carga normal e durante sua pior carga esperada:

systemctl status mywebapp.service
systemd-cgtop
systemctl show mywebapp.service -p MemoryCurrent -p CPUUsageNSec -p TasksCurrent

Para memória, um bom primeiro passo é frequentemente MemoryHigh= em vez de MemoryMax=:

[Service]
MemoryHigh=1G
MemoryMax=1536M

MemoryHigh= diz ao kernel para aplicar pressão antes que o serviço atinja o teto rígido. MemoryMax= é a parede. Se o processo ultrapassá-lo e a memória não puder ser recuperada, o kernel pode matar um processo no cgroup. Isso pode ser exatamente o que você deseja para um worker descontrolado, mas é uma surpresa desagradável para um banco de dados, a menos que você tenha planejado isso.

Para CPU, decida se você quer justiça ou um limite rígido:

[Service]
CPUWeight=50

Isso reduz a prioridade sob contenção, mas ainda permite que o serviço use CPU ociosa. Para trabalhos em segundo plano, isso geralmente é melhor do que uma cota.

[Service]
CPUQuota=200%

Isso limita o serviço a aproximadamente dois núcleos de CPU de tempo. Isso é útil para um processador em lote barulhento, mas pode prejudicar aplicações sensíveis à latência se os threads de trabalho forem limitados durante picos de tráfego.

Para explosões de processos, adicione um limite de tarefas:

[Service]
TasksMax=200

Isso protege o host de tempestades de fork acidentais. Defina-o alto o suficiente para contagens normais de threads. Cargas de trabalho Java, banco de dados e semelhantes a navegadores podem usar mais tarefas do que você espera.

Drop-Ins em Vez de Editar Unidades do Fornecedor

Evite editar arquivos de unidade fornecidos por pacotes em /usr/lib/systemd/system/ ou /lib/systemd/system/. Use um drop-in:

sudo systemctl edit mywebapp.service

Em seguida, adicione:

[Service]
MemoryHigh=1G
MemoryMax=1536M
CPUWeight=80

Após salvar:

sudo systemctl daemon-reload
sudo systemctl restart mywebapp.service
systemctl cat mywebapp.service

systemctl cat mostra a unidade do fornecedor e sua substituição juntas. Isso torna a depuração futura muito mais fácil porque a configuração ativa é visível em um comando.

Slices para Equipes, Locatários e Classes de Carga de Trabalho

Slices se tornam úteis quando você para de pensar em um serviço de cada vez. Suponha que um host execute a API, um gerador de relatórios e vários workers de importação. Você pode não se importar qual worker de importação usa CPU, mas você se importa que todo o trabalho de importação junto não possa privar a API.

Crie um slice:

# /etc/systemd/system/import.slice
[Unit]
Description=Cargas de trabalho de importação e preenchimento

[Slice]
CPUWeight=30
MemoryHigh=4G
MemoryMax=5G

Coloque os serviços de importação dentro dele:

[Service]
Slice=import.slice
ExecStart=/usr/local/bin/import-worker

Agora o grupo tem pressão compartilhada. Isso é mais limpo do que colocar limites rígidos separados em cada worker e esperar que a matemática ainda funcione depois que alguém adicionar um novo.

Há um detalhe de nomenclatura que pega as pessoas: nomes de slice codificam hierarquia. customer-a.slice é um slice de nível superior. customer-a-batch.slice não é filho de customer-a.slice; é apenas outro nome de nível superior. Slices hierárquicos usam traços como separadores de uma maneira específica, então leia systemd.slice(5) antes de projetar uma grande árvore de slices.

O que os Limites de Recursos Não Podem Corrigir

Cgroups podem impedir que uma carga de trabalho sobrecarregue o host, mas não podem tornar uma máquina subdimensionada rápida. Se um banco de dados precisar de mais memória para seu conjunto de trabalho do que você permite, ele pode gastar mais tempo recuperando memória ou falhar sob carga. Se uma API precisar de tempos de resposta curtos, uma cota de CPU rigorosa pode criar atrasos de limitação que parecem latência aleatória. Se um dispositivo de armazenamento já estiver saturado, os pesos de I/O podem melhorar a justiça, mas não criar taxa de transferência.

Trate os limites como guardrails. Combine-os com configurações de nível de aplicação: tamanhos de buffer de banco de dados, contagens de workers, concorrência de fila, limites de heap JVM, GOMEMLIMIT do Go, flags de memória do Node ou qualquer outra coisa que seu runtime forneça. A melhor configuração geralmente é ambas: a aplicação conhece seu próprio modelo de memória e concorrência, e o systemd protege o resto da máquina se esse modelo quebrar.

O Modelo Mental para Manter

Use limites de nível de serviço para um daemon. Use limites de nível de slice para um grupo de cargas de trabalho relacionadas. Use pesos quando quiser prioridade sob contenção. Use cotas e limites rígidos de memória quando precisar de um limite firme e estiver preparado para as consequências. Verifique as propriedades efetivas com systemctl show, observe o comportamento com systemd-cgtop e mantenha a configuração em drop-ins ou arquivos de unidade que sua equipe possa revisar.