Retomando e propondo escalabilidade orientada a eventos
Olá, eu sou João Brito, sua pessoa anfitriã favorita, e estou de volta. Vamos continuar.
Já discutimos limitações do uso de CPU e memória como sinais de escalabilidade. CPU e memória nem sempre representam os recursos que realmente precisamos observar. A CPU não conta a história completa; quando ela “mente”, a taxa de execução cai. Vamos adotar escalabilidade orientada a eventos.
Contextualizando a fila e integrando KEDA ao HPA
Qual é o cenário? O sinal que importa está fora de vista. Podemos ter uma réplica executando lentamente enquanto há 5 mil jobs na fila. A solução é escalar observando esses jobs, essa fila.
O KEDA possui uma ampla lista de eventos que pode coletar ou receber: Redis, RabbitMQ, Kafka e muitas outras fontes. Esses eventos podem ser enviados para que o KEDA os interprete. E por que olhamos para o HPA? Porque o KEDA interage diretamente com o HPA. Ele cria o próprio HPA, já que é o HPA que interage com os pods.
O KEDA utiliza um CRD (Custom Resource Definition). Instalamos o KEDA, que inclui um Operator (operador). Esse operador observa os objetos em um loop, consulta a fonte de eventos, cria o HPA e interage com o HPA para que o HPA gerencie a quantidade de pods.
Quanto às métricas do API Server (servidor de API), elas podem ser internas — usando o metrics-server que já utilizamos — ou externas. Há também um Admission Webhook (webhook de admissão) para garantir que nenhum YAML malformado chegue e quebre outros componentes. Os CRDs são sensíveis a YAMLs com problemas, então precisamos ter cuidado.
Habilitando scale to zero e avaliando cold start
Existe um objetivo importante: escalar a zero (scale to zero). Em tarefas assíncronas que não ocorrem o tempo todo, quando nada está acontecendo, não faz sentido manter uma máquina — ou, neste caso, um pod — ligado, ocupando espaço no cluster que poderia ser usado para outra coisa ou até removido. O HPA sozinho não consegue deixar uma implantação com zero pods. Com o KEDA, usando CRD, conseguimos. Ele observa a fila vazia e, com zero pods, não há custo. Quando chega uma mensagem, o KEDA interage com o HPA e escala para o primeiro pod. Com outras referências, entendemos quantos pods são necessários conforme a quantidade de eventos, quanto cada worker processa, e a fila cresce de acordo com isso. Se a fila estiver vazia, perfeito. O inverso também funciona: se houver menos demanda, reduz a quantidade de pods e aguarda o próximo pico.
Há, no entanto, um alerta aqui: escalar a zero tem um preço, que é o cold start (inicialização a frio). Já falamos sobre o custo de trabalhar com o passado (backlog). Quando chega a primeira mensagem e não há nenhum pod em execução, se essa mensagem chega ao cluster e se perde — por exemplo, se não houver reintento, espera ou tempo limite —, o primeiro pod ainda vai demorar um tempo para iniciar a aplicação do worker e começar a trabalhar de fato. Portanto, isso não serve para requisições síncronas e sensíveis à latência; é inviável nesses casos. A arquitetura deve ser pensada para permitir que uma parte fique parada; precisa haver fallback (contingência) e diversas estratégias. Para filas assíncronas, como Redis, RabbitMQ e Kafka, alguns segundos de espera são aceitáveis; nesse contexto, scale to zero é excelente. Se a fila está vazia, zero workers: sucesso.
Ajustando o ScaledObject e preparando a instalação
O CRD que o KEDA injeta no cluster é o ScaledObject. No nosso exemplo, cada pod pode processar 50 itens. Definimos um mínimo de 0 réplicas, máximo de 10, um passo de escala de 5 em 5 e cerca de 30 segundos de cooldown (tempo de resfriamento), que é o tempo de espera antes de chegar a zero — o tempo que aguarda antes de desligar o último pod. Isso evita picos de “liga/desliga, liga/desliga”. Se não houver mais demanda, desliga a fila de uma vez. Em uma fila assíncrona que recebe mensagens de tempos em tempos, podemos reduzir um pouco o cooldown para esperar acumular alguns itens na fila e então ligar o pod, ou aumentar um pouco para que, quando a fila estiver terminando, dois ou três itens que chegarem atrasados ainda sejam processados sem oscilações excessivas.
Vamos preparar o terreno para criar esse novo formato com o KEDA. Primeiro, removemos o worker atual e criamos nosso ScaledObject. Antes disso, precisamos instalar o KEDA. Para instalar, vamos utilizar o Helm (gerenciador de pacotes do Kubernetes). Adicionamos o repositório do KEDA e instalamos o KEDA propriamente dito. Aguardamos um momento para que tudo fique online. Em seguida, vemos a saída com um pouco de ASCII art (arte em ASCII): KEDA — Kubernetes Event-driven Autoscaling. Podemos ver os pods do KEDA já em execução: o Webhook (webhook), o Metrics API Server (servidor de métricas da API) e o Operator (operador).
Removendo o HPA e instalando o KEDA com Helm
Para começar, removemos o HPA atual do worker, liberando o caminho para o KEDA assumir o controle do escalonamento via ScaledObject:
kubectl delete hpa worker -n demo
Esse comando remove o HPA associado ao deployment worker no namespace demo.
Em seguida, instalamos o KEDA com o Helm, adicionando o repositório oficial e aguardando a instalação completa:
helm repo add kedacore https://kedacore.github.io/charts
helm repo update
helm install keda kedacore/keda --namespace keda --create-namespace --version 2.19.0 --wait
Com isso, o KEDA é instalado no namespace keda e o --wait garante que os componentes fiquem prontos antes de prosseguir.
Agora, verificamos se os componentes do KEDA estão rodando (Webhook, Metrics API Server e Operator):
Ao listar os pods, confirmamos que o KEDA está operacional e pronto para criar/gerir HPAs a partir de eventos externos.
Criando e validando o ScaledObject para Redis
Com o worker removido e o KEDA ativo, criamos nosso primeiro ScaledObject para Redis com mínimo 0, máximo 10, intervalo de consulta (polling) a cada 5 segundos e cooldown de 30 segundos. Também configuramos o trigger de Redis com a lista jobs, listLength de 50 por réplica e uma ativação inicial com activationListLength de 5:
cat <<EOF | kubectl apply -f -
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: worker
namespace: demo
spec:
scaleTargetRef:
name: worker
minReplicaCount: 0
maxReplicaCount: 10
pollingInterval: 5
cooldownPeriod: 30
triggers:
- type: redis
metadata:
address: redis.demo.svc.cluster.local:6379
listName: jobs
listLength: "50"
activationListLength: "5"
enableTLS: "false"
EOF
Esse manifesto cria o ScaledObject no namespace demo, instruindo o KEDA a observar o tamanho da lista jobs no Redis e a escalar o deployment worker de acordo.
Logo após criar, validamos se o ScaledObject está presente e pronto:
kubectl get scaledobject worker -n demo
Assim confirmamos o estado do recurso, visualizamos mínimo/máximo e o trigger configurado (Redis). Também poderíamos ver outros tipos de triggers disponíveis (Kafka, RabbitMQ, Prometheus, Datadog, Trace, gRPC externo etc.), inclusive a possibilidade de escrever um trigger customizado.
Em resumo, utilizamos o KEDA para escalabilidade orientada a eventos, integrando-se ao HPA por meio de CRDs, observando filas e outras fontes de eventos, habilitando inclusive scale to zero em cenários assíncronos, com atenção aos impactos de cold start e às validações via Admission Webhook para evitar problemas com YAMLs malformados.
Observando o escalonamento ao esvaziar a fila
Vamos analisar o comportamento de escalonamento partindo do final da fila. A fila estava grande; aceleramos o tempo para que ela terminasse. Em seguida, vimos 10 réplicas. Quando a fila acabou, aguardamos os 30 segundos definidos e, como esperado, voltamos a 0 réplicas. A métrica foi coletada novamente, aplicou-se a regra de 30 segundos e retornou para 0 réplicas. Era o comportamento previsto.
Para observar rapidamente o estado da fila, o número de réplicas do deployment worker e se o ScaledObject está ativo, podemos usar este trecho no terminal (e repeti-lo periodicamente, se desejado):
TS=$(date +%H:%M:%S)
LLEN=$(kubectl exec deploy/redis -n demo -- redis-cli LLEN jobs)
REPL=$(kubectl get deploy worker -n demo --no-headers | awk '{print $2}')
ACTIVE=$(kubectl get scaledobject worker -n demo \
-o jsonpath='{.status.conditions[?(@.type=="Active")].status}')
printf "[%s] fila=%-5s | replicas=%-4s | ACTIVE=%s\n" "$TS" "$LLEN" "$REPL" "$ACTIVE"
sleep 8
Esse comando imprime timestamp, tamanho da fila, réplicas atuais e o status “Active” do ScaledObject. Você pode colocá-lo dentro de um loop while no shell para acompanhar continuamente.
Consultando o HPA do KEDA e repovoando a fila
Agora, faremos o contrário. Antes, vamos observar o HPA. Temos NULL, que é 0. A cobertura não chega a 0. O mínimo de pods está configurado como 1, mas o sistema está com 0 réplicas. Assim, notamos que o HPA não tem consciência disso. Na nossa lista, temos no mínimo 50 tarefas.
Para consultar o estado atual dos HPAs no namespace demo:
Dessa forma, vemos o HPA que o KEDA gerencia e percebemos que o “salto” de 0 para 1 é orquestrado pelo KEDA, não pelo HPA clássico.
Vamos fazer o inverso: adicionar tarefas novamente no Redis e observar o crescimento. Limpamos a tela e executamos o comando para popular a lista de tarefas. Em seguida, verificamos a fila. Já temos 4 réplicas, avançando para 8, com a meta de atingir 10. As réplicas estão iniciando; lembramos que saímos de 0. Temos 5 mil tarefas, com 50 por réplica, para facilitar a conta, e já chegamos ao objetivo. Novamente, em cerca de 40 e poucos segundos — quase 1 minuto — alcançamos as 10 réplicas em funcionamento.
Para popular a fila do Redis com 5.000 itens rapidamente:
kubectl exec deploy/redis -n demo -- sh -c \
"for i in \$(seq 1 5000); do echo \"LPUSH jobs job-\$i\"; done | redis-cli --pipe"
Esse comando envia os LPUSH via pipe para o redis-cli, preenchendo a lista jobs de uma vez.
Inspecionando métricas e confirmando o HPA do worker
A partir desse ponto, as 10 réplicas, isto é, os trabalhadores, atuam sobre a nossa fila. Vamos verificar a métrica que o HPA está considerando agora. Vamos encerrar o loop for e exibir uma réplica. Aqui está: o KEDA, nesse momento, com 10 réplicas. Observamos o tamanho da fila e a métrica. Fizemos um describe no ScaledObject, e o nome da métrica utilizada é jobs de Redis, para facilitar a identificação. Já passamos por isso; nos slides mostramos o terminal — o slide aceita tudo — e está documentado lá.
Para checar especificamente o HPA criado pelo KEDA para o worker:
kubectl get hpa keda-hpa-worker -n demo
Assim, confirmamos as metas e o status do HPA que o KEDA gerou.
E para inspecionar as métricas externas associadas ao ScaledObject (incluindo o nome “jobs de Redis”):
kubectl describe scaledobject worker -n demo | grep -A 1 "External Metric Names"
Esse detalhe evidencia quais métricas externas o HPA está consumindo por meio do KEDA.
Reforçando o scale from zero e definindo limites
Quando temos 1 réplica, a fila desce até zero, e retornamos a 0 réplicas. O HPA, sozinho, não faz isso. Ao rodar em conjunto com o KEDA, é o KEDA que assume o passo de zero para um. É o KEDA que provoca o cold start (inicialização a frio). Observamos o pico de réplicas: em menos de 1 minuto, todas as réplicas estavam em funcionamento, atingindo o topo.
Registramos o horário (por volta das nove e vinte e nove). O sistema recebe o disparo, fica ativo e, de 0 réplicas, passa a ativo — scale from zero (escalar a partir de zero). Surge a primeira réplica e, então, progride: 4, 8, 10, até o limite configurado. É importante tomar cuidado para não deixar isso ilimitado. Em caso de problema em cascata, se ocorrer um flood (enxurrada) na fila por diversos motivos — da aplicação ou não —, é possível saturar o seu cluster (agrupamento) inteiro se não houver limites claros. A fila pode crescer sem limites; normalmente, a fila não terá limites além do que o Redis suportar. Como consequência, você aumentará os trabalhadores e poderá saturar o cluster de trabalhadores, levando à indisponibilidade de componentes.
O ponto principal aqui é: em menos de um minuto, o trabalhador passou de 0 para 10 e recebeu o sinal correto. Assim, ele pode permanecer em zero pelo tempo necessário, e o KEDA receberá o evento do Redis e ativará o trabalhador.
Comentando métricas, custos e casos de uso do KEDA
Uma observação adicional, que já vimos no terminal: há um detalhe — um “easter egg” — relacionado à unidade em millicores (milicores). Nosso objetivo, o list length, é 50. Tenha atenção para não interpretar um valor incorreto, como se fosse um bilhão. E temos nossa external metric (métrica externa) aqui: from zero, o nome que atribuímos à nossa métrica.
Em tom de brincadeira, comentamos que a “matriz de decisão nunca vá ao HPA clássico”. O HPA clássico baseia-se em CPU e memória. Ao movermos para o KEDA, pensamos no comportamento e no desperdício mínimo de CPU, que nunca é zero: se deixarmos apenas o HPA rodando, haverá sempre consumo residual e ocorrerá cold start (inicialização a frio). Entre os benefícios do KEDA, podemos alcançar custo zero. Como assim? O KEDA continuará rodando — os três pods do KEDA estarão ativos, incluindo o operator (operador). Se compararmos apenas um HPA e um pod versus esses três, há algum custo. Porém, em um cluster (agrupamento) com muitos pods, muitos deploys (implantações), filas, bases de dados etc., três pods tornam-se irrelevantes frente ao benefício que trazem. Se considerarmos apenas “mais um pod ocioso”, aí sim, haverá custo — mas, no cenário amplo, compensa.
Em tráfego HTTP estável, podemos utilizar o HPA e obter benefícios. Já com o KEDA, lidamos bem com tarefas em batch (lote), filas assíncronas, pipelines (fluxos) de dados, casos de machine learning (aprendizado de máquina), IA e workflows (fluxos de trabalho). Há picos específicos em cada parte desse processo. Quando temos um pipeline (fluxo) de dados, injetamos por um lado, trabalhadores consomem e encaminham ao próximo passo, e assim sucessivamente. Vamos escalonando apenas o trecho necessário. O KEDA auxilia nisso. No entanto, não é uma bala de prata. Se a sua carga de trabalho for previsível em CPU, podemos utilizar o HPA com tranquilidade — sempre ponderando.
Concluindo e apontando para escala preditiva
Em nossa transição evolutiva, saímos do reativo manual do HPA para o reativo externo com o KEDA. Continua sendo reativo, mas mais inteligente, pois pode obter métricas de outras fontes. Ao combinar HPA e KEDA, pode parecer que o KEDA é muito mais inteligente, mas não é magia: é apenas event-driven (orientado a eventos), assim como o HPA. A diferença é que o KEDA se conecta a mais lugares, trazendo mais contexto.
Para onde vamos? Para a escala preditiva. Antes de escalar, queremos antecipar o problema. Trocamos o problema do “quando o sistema deve agir?”. Saímos do dilema do sinal ruim: o KEDA já resolve essa parte. Estamos empilhando os tópicos necessários: Kubernetes, HPA, KEDA, e seguimos nesse caminho. Mudemos nosso olhar. Não esqueçamos: o KEDA não compete com o HPA; o KEDA trabalha junto. Ele funciona quase como um adaptador universal, um adaptador de sinais para o seu HPA.
Nos vemos na próxima, seguimos por aqui. Até então!