Alta disponibilidade com Kubernetes na prática

Orquestração de containers na vida real

O Kubernetes é um sistema open source para automação de deployments, scaling e gerenciamento de containers. Está em constante evolução e tem uma comunidade extremamente ativa e dinâmica.

Um deployment de aplicação no Kubernetes criado com configuração padrão não vai ter HA (High Availability).

Abaixo, listo algumas coisas que aprendemos ao longo de quase um ano e meio com uma aplicação crítica em produção.

No final do artigo, coloquei um exemplo completo com todas essas coisas nos seus devidos lugares!


Rollout

Um rollout ocorre sempre que alguma alteração é feita no seu deployment. A seguir temos uma configuração que garante HA durante esse processo.

Vamos ver o que cada parte significa:

replicas

Aqui temos a quantidade de réplicas que serão criadas nesse rollout. É importante saber que a quantidade atual de réplicas não é levada em conta, então uma boa estratégia é ler a quantidade atual de réplicas e substituir no seu arquivo de deployment:

Assim evitamos que uma aplicação rodando com muitas réplicas fique com uma quantidade insuficiente após o processo de rollout.

minReadySeconds

Nessa opção configuramos o tempo (em segundos), que os pods criados pelo seu deployment irão esperar para estarem disponíveis. Se sua aplicação consome muita CPU na inicialização, essa configuração pode ser uma ótima alternativa para estabilização dos pods antes de começarem a receber requisições.

rollingUpdate

Essa estratégia cria um novo ReplicaSet a partir das alterações feitas no deployment e vai diminuindo a quantidade de pods do ReplicaSet antigo, à medida que consegue aumentar a quantidade de pods do ReplicaSet novo. Dessa forma o rollout ocorre gradativamente e é possível acompanhar se quantidade de erros/tempo de resposta aumentam durante o processo.

maxSurge

Nesse campo especificamos a quantidade de pods que o novo ReplicaSet vai criar a mais antes de começar a terminar os pods do antigo ReplicaSet. Quanto maior o valor, mais rápido o rollout ocorre.

maxUnavailable

Essa é a principal opção para atingirmos HA no processo de rollout. Por padrão, o Kubernetes define uma margem de 25%, então, caso não coloquemos 0 (que significa sem nenhum pod indisponível), existe a possibilidade da aplicação ficar com uma quantidade insuficiente de pods no momento do rollout, as chances de indisponibilidade aumentam consideravelmente.

Healthcheck

Sua aplicação deve prover um endpoint que diga para o deployment que uma instância está realmente pronta para receber tráfego.

Vamos às explicações:

livenessProbe

Essa seção é usada pelo Kubernetes para verificar se os pods da sua aplicação estão funcionais. Se em algum momento, dadas a configurações, essa verificação falhar, o seu pod será reiniciado. Boas práticas para a implementação desse endpoint são checar conectividade com as dependências, espaço em disco e tudo que possa influenciar no funcionamento da aplicação.

readinessProbe

Durante um rollout o Kubernetes usa essa seção para saber se os pods da sua aplicação estão prontos para receber tráfego. É muito importante para obter HA durante o processo de rollout. Boas práticas para a implementação desse endpoint incluem Signal Handling, por exemplo, encerrar conexões persistentes quando receber SIGTERM ou SIGINT, aguardar dados necessários estarem em memória, conexões com dependências estabelecidas, etc.

failureThreshold

Informa quantas vezes uma falha deve ocorrer para a verificação ser considerada com falha e o pod da aplicação sofrer um restart ou não receber tráfego.

httpGet

Como estamos falando de uma API HTTP, usamos esse campo para configurar o path e porta onde o Kubernetes vai bater para as verificações de readiness e liveness.

initialDelaySeconds/periodSeconds

De quanto em quanto tempo o Kubernetes vai executar as verificações, sendo que a primeira pode ter um valor diferente das subsequentes. Se a aplicação demora para inicializar, vale a pena colocar um valor próximo desse tempo.

Distribuição de Pods

É muito importante garantir que os pods da sua aplicação não estão concentrados em poucos nodes do cluster, pois em caso de alguma falha no cluster, quanto mais bem distribuídos estiverem seus pods, menor o impacto. Para isso é possível configurar uma anti afinidade entre os pods da sua aplicação, por exemplo, para que eles nunca fiquem em um mesmo node.

podAntiAffinity

Nessa seção especificamos que, segundo os critérios definidos, novos pods da aplicação não vão ficar em um node que já tenha um pod da mesma aplicação.

requiredDuringSchedulingIgnoredDuringExecution

Com essa configuração definimos que as regras de anti afinidade serão aplicadas somente durante eventos de scale, ou seja, se um pod for editado para conter algum dos critérios da regra durante sua execução, isso não vai fazer com que a regra seja aplicada.

labelSelector

Essa seção define que vamos usar labels dos pods para verificação da regra. Você pode colocar labels nos metadados dos seus pods:

matchExpressions

Essa parte contém as expressões usadas para verificar se um pod não deve ser colocado em algum componente da topologia do cluster. Nesse exemplo estamos verificando que uma determinada label tenha um determinado valor.

topologyKey

Como nosso objetivo é distribuição dos pods entre nodes usamos o hostname para definir isso, também poderíamos usar zona, região, tipo de instância, existem várias possibilidades.

Alocação de recursos

No Kubernetes existem níveis de qualidade de serviço que o cluster infere dependendo de como você configura os recursos que seus pods vão utilizar. Para ter esse nível de qualidade garantido é necessário sempre ter limits e requests iguais.

requests

Definição de memória e CPU que seus pods vão requisitar para serem colocados em um node, essa é a quantidade mínima de recursos que um node deve ter disponível para receber um pod da aplicação.

limits

Definição de memória e CPU máximas que seus pods vão poder usar em um node que ele já esteja. Uma vez que ele atingir um dos valores, será reiniciado.

Com os valores iguais o comportamento de eviction e scheduling do cluster deve ficar mais previsível, já que não há variação nos recursos durante a execução dos pods da aplicação.

HPA

Para garantir a disponibilidade de sua aplicação (principalmente quando ela não tem estado, como depender de um banco de dados, por exemplo) é importante ter uma forma automática de aumentar a quantidade de réplicas que ela possui. Para isso podemos configurar HPA (Horizontal Pod Autoscaler) para nossos deployments.

scaleTargetRef

Aqui relacionamos esse HPA com o deployment pelo nome.

minReplicas

Quantidade mínima de pods independente da quantidade de CPU utilizada por eles.

maxReplicas

Limite de pods que esse HPA vai manter.

targetCPUUtilizationPercentage

Qual o percentual de CPU que o HPA deve manter. Conforme esse percentual for atingido, novos pods serão criados, ou caso o percentual estiver abaixo, pods serão removidos. Sempre buscando estar o mais próximo possível dele.

PDB

Podemos configurar o mínimo de replicas disponíveis de nossa aplicação durante uma manutenção ou recuperação do cluster. Para isso temos o PDB (Pod Disruption Budget).

matchLabels

Aqui relacionamos nossos pods com esse PDB através das suas labels.

minAvailable

Percentual mínimo de pods que devem estar disponíveis em caso de problemas no cluster.

É importante ficar atento nesse passo para não colocar um minAvailable inatingível, como 100%, pois assim os nodes do cluster que tenham pods da sua aplicação nunca poderão ser terminados, travando atualizações no cluster, por exemplo.

Exemplo Completo

Juntando todas essas boas práticas chegamos a esse arquivo de configuração com todas elas juntas:

Circuit Breaker

Saindo um pouco das configurações no Kubernetes, uma boa prática de HA é cuidar bem de como sua aplicação se comunica com as suas dependências.

Por exemplo, não é aceitável que uma aplicação deixe de responder em todos os contextos por conta de uma dependência que esteja fora do ar.

Para mitigar isso temos um design pattern já bem conhecido e popularizado pela Netflix com o Hystrix, que é o circuit breaker.

É recomendado tirar vantagem dessa implementação. Mas é importante saber que além dessa proteção na comunicação temos que ter visibilidade do que está acontecendo com os circuitos definidos na aplicação, para isso é importante ter um agregador das métricas de todas as réplicas que sua aplicação irá gerar quando utilizar o Hystrix.

O Turbine é o componente responsável por isso na stack da Netflix. Basicamente ele tem um serviço de discovery que busca por instâncias rodando o Hystrix dependendo da implementação feita. Nós acabamos fazendo um fork e implementando um discovery de pods no Kubernetes, que você pode conferir o código aqui: https://github.com/GrupoZapVivaReal/Turbine/blob/1.x/turbine-contrib/src/main/java/com/netflix/turbine/discovery/KubernetesInstanceDiscovery.java

Existem implementações mais genéricas que também merecem ser conferidas, por exemplo: https://github.com/fabric8io/kubeflix.

E finalmente para exibição dessas métricas, existe o Hystrix Dashboard. Basta apontar para o endpoint que o Turbine disponibiliza com todas as métricas agregadas e é possível acompanhar em real time todos os circuitos que a aplicação definiu.

Conclusão

Com algumas simples configurações e implementações é possível atingir um alto nível de HA com o Kubernetes. Espero que esse artigo forneça uma base para quem está começando a criar seus deployments com essa excelente ferramenta!

Para se aprofundar mais no assunto recomendo a leitura dos links da documentação oficial que coloquei abaixo!

Mais informações

https://kubernetes.io/

https://en.wikipedia.org/wiki/High_availability

https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#writing-a-deployment-spec

https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#interlude-built-in-node-labels

https://kubernetes.io/docs/tasks/configure-pod-container/quality-service-pod/#qos-classes

https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/

https://kubernetes.io/docs/tasks/run-application/configure-pdb/

https://kubernetes.io/docs/concepts/workloads/pods/disruptions/

https://github.com/Netflix/Hystrix

https://martinfowler.com/bliki/CircuitBreaker.html

https://github.com/Netflix/Turbine