Orchestration Fundamentals · MemphisLab Docs
MemphisLab Docs
k3s Platform

Orchestration Fundamentals

O que é orquestração de containers, como o cluster decide onde e como rodar cada coisa, e por que k3s

O problema que orquestração resolve

Rodar um container isolado (docker run) responde “como eu executo isso”. Não responde “o que acontece quando o processo morre”, “como eu distribuo 4 réplicas em 4 máquinas diferentes”, “como uma réplica nova sabe onde está o banco” ou “como eu troco a versão sem parar de atender request”. Orquestração é a camada que resolve essas perguntas de forma genérica, para qualquer carga de trabalho, em vez de cada equipe reinventar um script de restart e um load balancer manual.

Kubernetes (e por extensão k3s, que é uma distribuição dele) faz isso com dois princípios que sustentam tudo o resto: modelo declarativo e control loop.

Modelo declarativo

Você não diz “suba um pod”. Você diz “eu quero 3 réplicas deste Deployment rodando” e escreve isso como um objeto (YAML) que vai para a API do cluster. O cluster manter esse estado verdadeiro é responsabilidade dele, não sua. Se um pod morre, ninguém “reinicia” nada manualmente — o cluster nota que o estado atual (2 réplicas) diverge do estado desejado (3) e cria uma réplica nova para fechar a diferença.

É a diferença entre um script que você roda uma vez (imperativo: “faça isso agora”) e uma regra que fica ativa continuamente (declarativo: “mantenha isso sempre verdadeiro”). Todo objeto do cluster — Deployment, Service, NetworkPolicy, o Cluster de Postgres do CloudNativePG — segue esse modelo.

Control loop / reconciliation

O mecanismo que faz o modelo declarativo funcionar é o control loop: um processo que roda em looping infinito, comparando estado desejado (o que está gravado no etcd, ver abaixo) contra estado observado (o que está realmente rodando), e agindo para fechar qualquer diferença. Kubernetes não tem “um” control loop — tem dezenas, cada um dono de um tipo de recurso: o Deployment controller garante que existam N ReplicaSets do jeito certo, o ReplicaSet controller garante que existam N Pods, o Node controller monitora se um node parou de responder.

Esse padrão se repete fora do núcleo do Kubernetes. O ArgoCD é um control loop que reconcilia o cluster contra um repositório Git (ver gitops-argocd). O operator do CloudNativePG é um control loop que reconcilia um Cluster de Postgres contra StatefulSets, Services e Secrets reais (ver postgres-operator). Entender control loop uma vez explica os três.

Scheduler: como o cluster decide onde rodar um pod

Quando um Pod é criado sem estar atribuído a um node, o scheduler decide em qual node ele vai rodar, em duas fases: filtering (elimina nodes que não servem — sem CPU/memória suficiente, sem o nodeSelector exigido, com uma taint que o pod não tolera) e scoring (dos nodes que sobraram, pontua e escolhe o melhor, por critérios como distribuição de carga).

Esse mecanismo é observável de verdade neste cluster: finatto-k3s-worker-01 é arm64 enquanto o resto é amd64 (ver topology), e ele é o único node de workload sem estar sob NAT em outro provedor — é onde a maior parte da carga de aplicação roda. Um Pod cuja imagem só existe para uma arquitetura específica precisa de nodeSelector: kubernetes.io/arch para não ser agendado no node errado; sem isso, o scheduler tentaria rodar um binário arm64 num node amd64 e o container falharia ao iniciar. O chart de aplicação (charts/app) também usa priorityClassName para influenciar o scheduler sob pressão de recursos — não filtra onde o pod pode rodar, mas decide qual pod é despejado primeiro se faltar espaço:

# charts/app/templates/deployment.yaml
priorityClassName: {{ .Values.priorityClass | default "standard" }}
{{- include "app.affinity" . | nindent 6 }}
{{- with .Values.nodeSelector }}
nodeSelector:
  {{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
  {{- toYaml . | nindent 8 }}
{{- end }}

Ver network-and-scaling para como as três PriorityClass deste cluster (platform-critical, critical, standard) são usadas.

etcd e consenso (Raft)

O estado desejado de todo o cluster — cada Deployment, Secret, ConfigMap — vive no etcd, um banco de chave-valor distribuído. “Distribuído” aqui importa: não é um banco único que pode cair, é um conjunto de réplicas (membros) que precisam concordar entre si sobre qual é o valor atual de cada chave, mesmo se um membro cair ou a rede engasgar entre eles. O algoritmo que garante esse acordo é o Raft: a cada escrita, um membro líder propõe o valor novo e só confirma a escrita depois que a maioria dos membros gravou. Se o líder cair, os membros restantes elegem um líder novo entre si.

“Maioria” é por isso que o número de membros importa e por que ele é sempre ímpar: com 3 membros, uma escrita precisa de 2 confirmações — o cluster tolera 1 membro fora do ar e continua funcionando. Com 4 membros, ainda seria preciso maioria de 3 pra escrever, mas agora com o dobro de latência de rede do quarto nó sem ganhar tolerância a falha nenhuma (perder 1 de 4 ainda deixa 3, que já bastava). É exatamente o raciocínio por trás dos “3 servers, não 4” documentado em topology: os dois membros etcd de baixa latência confirmam uma escrita sem esperar o terceiro, que fica mais longe.

Por que k3s e não Kubernetes vanilla

A motivação real desta escolha, confirmada institucionalmente, foi pegada de recursos como fator principal, com simplicidade operacional para time pequeno como fator secundário — não foi uma preferência abstrata por “mais leve”.

k3s é uma distribuição de Kubernetes que remove ou substitui peças que só fazem sentido em certos contextos:

  • Empacotamento em binário único — todos os componentes de control plane (API server, scheduler, controller manager) e o próprio etcd rodam dentro de um processo k3s server, em vez de processos separados via kubeadm. Menos processos residentes, menos overhead de memória fixo por node.
  • Componentes in-tree de cloud provider fora por padrão — Kubernetes vanilla carrega suporte para se integrar com provedores de nuvem específicos (LoadBalancer nativo da AWS/GCP, por exemplo). Esse cluster não roda atrás de um provedor único — são 3 provedores diferentes — então esse código nunca seria usado aqui, e k3s já vem sem ele.
  • etcd é opcional — k3s pode rodar com SQLite embarcado em vez de etcd para clusters de nó único. Este cluster não usa esse modo: com 3 servers, faz sentido rodar etcd real para ter quórum de verdade (ver seção anterior). A opção existe, mas não é usada aqui.

O resultado prático que valida a motivação: dois dos quatro nodes (finatto-k3s-server-02/03) têm 2 vCPU e 2GB de RAM — memória insuficiente para um control plane de Kubernetes vanilla instalado via kubeadm de forma confortável, onde só o conjunto de processos de control plane facilmente passa de 1GB antes de contar o etcd e o sistema operacional. k3s sustenta control plane + etcd nesses dois nodes porque cortou o que sobrava.