Separar capacidade, elegibilidade e saúde
Antes de uma janela de manutenção, desenha o caminho de substituição de uma instância: onde pode executar, que recursos precisa, que armazenamento monta e como passa a receber tráfego. Um nó existente não é automaticamente um destino utilizável. Pode não ter memória residual, estar fora da zona do disco ou não satisfazer a seleção do workload. Uma toleration permite um taint correspondente, mas não escolhe exclusivamente esse pool; combina-a com uma restrição de seleção quando esse é o requisito. No caso fictício de reporting desta aula, a necessidade é manter capacidade para seis Pods depois da falha de uma zona. Esta frase exige um cálculo e um ensaio diferentes de demonstrar seis Pods Ready antes da manutenção. Regista capacidade residual, requisitos por Pod, domínios de falha e condições de elegibilidade. Usa valores medidos e identifica quem os atualiza, porque outro workload pode consumir a margem entre o desenho do projeto e a execução da mudança.
Dar a cada controlo o seu âmbito
Um PDB não impede avarias e não controla o RollingUpdate de um Deployment. A estratégia do workload, o orçamento de interrupção e a atualização dos nós são mecanismos diferentes. Num upgrade surge GKE, o nó adicional precisa de recursos antes de substituir o antigo. Se a quota só permite os quatro nós já existentes, maxSurge=1 não cria uma quinta vaga por intenção. Prepara essa margem ou ensaia e aprova uma alternativa compatível com a disponibilidade requerida. Também não assumes que um PDB bloqueia a operação indefinidamente: a estratégia surge documentada respeita o PDB e o período de terminação até uma hora, podendo depois forçar a eviction. Confirma a estratégia concreta e a documentação aplicável antes da mudança. As maintenance exclusions também têm exceções, incluindo intervenções por vulnerabilidades críticas. Para o negócio, comunica a manutenção planeada e a capacidade de recuperação, evitando transformar configurações operacionais numa garantia absoluta de ausência de interrupções.
Conservar as restrições do armazenamento
Um volume pode sobreviver a um Pod e continuar inutilizável para o substituto escolhido. Com provisioning zonal antecipado, a localização do disco pode ser incompatível com o local obrigatório do consumidor. Para novos PVC e drivers compatíveis, WaitForFirstConsumer permite considerar a topologia do Pod durante binding e provisioning. Não move um disco existente e não deve ser combinado com nodeName como atalho, porque esse campo contorna o scheduler. Outro limite é a exclusividade: ReadWriteOnce refere-se a um nó e pode permitir vários Pods nesse nó. Avalia ReadWriteOncePod quando precisas do âmbito de um Pod, confirmando suporte CSI e requisitos da aplicação. No plano de desativação, separa eliminação técnica de libertação efetiva de custos. Uma política Bucket Lock já bloqueada não pode ser reduzida; criar um bucket para dados futuros não liberta os objetos antigos. Estas restrições devem constar do orçamento, da sequência de mudança e do handover, antes de assumir que remover uma aplicação elimina todos os recursos associados.
Validar o caminho completo de conectividade
Investiga cada camada a partir da evidência. Um endpoint PSC aceite prova um estado de ligação, mas não prova a existência de um hostname personalizado no resolver da aplicação. Se o teste com IP e nome corretos funciona e a aplicação recebe NXDOMAIN, verifica o registo privado e a visibilidade da zona. Para NetworkPolicy nativa com enforcement ativo, uma origem isolada em egress e um destino isolado em ingress precisam ambos de permitir o fluxo. As permissões de várias policies combinam-se; acrescentar uma policy restrita não elimina um allow amplo já aplicado. Confirma o conjunto efetivo e não apenas o ficheiro acabado de alterar. Public NAT, por sua vez, dá saída e tráfego de resposta associado, mas não publica um serviço para conexões de entrada novas. No ensaio, regista origem, destino, nome, porta, identidade do workload e resultado observado. Um teste a partir do portátil de um administrador não substitui o caminho real do serviço.
Exercício: calcular vagas sem somar recursos incompatíveis
O modelo local recebe o orçamento residual de cada nó em millicores e MiB. Para Pods idênticos, calcula quantas cópias cabem em CPU e memória e escolhe o menor número inteiro. Num nó com 6000 millicores e 6144 MiB, um Pod de 2000 millicores e 4096 MiB só cabe uma vez: a memória limita o resultado. Dois nós com 1500 millicores livres cada um também não recebem um Pod que pede 2000; a CPU não se soma entre nós para executar um Pod. Antes de executar, prevê esses dois resultados. Corre python3 content/labs/pca-infrastructure-capacity/run.py e observa também o efeito de tornar um nó inelegível. O programa valida unidades inteiras, IDs únicos e nós excluídos conhecidos. Não contacta Kubernetes nem prevê o scheduler real: omite volumes, afinidade, topology spread, quotas e concorrência. Um resultado fitsModel=true é apenas uma condição de capacidade neste modelo simplificado, nunca uma autorização de produção.
Comparar manutenção de um nó com perda de uma zona
O caso principal tem quatro nós, dois por zona, cada um com 4000 millicores e 8192 MiB residuais. Cada Pod pede 2000 millicores e 3072 MiB. Há duas vagas por nó, oito no total. Retirar um nó deixa seis vagas; retirar os dois nós de uma zona deixa quatro. O requisito de seis réplicas após falha de zona não está cumprido, mesmo que a manutenção de um nó passe no cálculo. A função voluntary_allowance mostra apenas a conta simplificada entre saudáveis e mínimo; não reproduz disruptionsAllowed do controlador nem impede a falha involuntária. Para corrigir o desenho, considera capacidade elegível adicional ou um modo degradado formalmente aceite com carga menor. Depois ensaia latência e comportamento real. Faz também um teste de failover da base: Cloud SQL HA pode recuperar no mesmo endereço, enquanto o cliente conserva conexões fechadas. A aplicação precisa de restabelecer acesso e tratar operações interrompidas segundo regras seguras de retry.
Tratar ML e APIs como dependências operacionais
Um pipeline de ML também tem tarefas em curso, efeitos persistentes e quotas partilhadas. Em Agent Platform, failure_policy=fast deixa de agendar novas tarefas após falha; tarefas já agendadas podem continuar. Não interpreta esta política como rollback de ficheiros, resultados ou chamadas externas. Define que outputs ficam provisórios e como são reconhecidos numa repetição. Num job batch terminado, 970 sucessos, vinte falhas e dez entradas incompletas exigem tratar trinta entradas antes de declarar cobertura das mil originais. A lista partialFailures pode ser limitada, por isso não substitui as contagens e a reconciliação de outputs. Para uma API como Vision, distingue quota de pedidos de quota da feature. No exemplo configurado de 100 pedidos por minuto e 600 páginas por minuto, com oito páginas por pedido, o teto teórico é 75 pedidos. São números fictícios de configuração, não valores padrão. O plano deve considerar throttling, backlog, identificação das entradas e prazo de entrega do resultado ao negócio.
Entregar uma mudança que RUN consegue manter
Organiza a evidência por requisito e responsável: capacidade depois da falha, estratégia de atualização, acesso a volumes, conectividade real, recuperação de conexões e completude de processamento. Para cada um, guarda configuração, versão, ensaio, resultado e limitação conhecida. Se só executaste o cálculo local, escreve isso; não o apresentes como teste de GKE. Na reunião, uma mensagem útil em inglês seria: “Single-node maintenance fits the capacity model; zone loss does not. Acceptance remains pending additional capacity and a recovery rehearsal.” Esta formulação permite decidir sobre tempo, custo e risco sem confundir testes diferentes. A passagem a RUN deve incluir quem aprova uma exceção, quem observa o backlog, quem consegue recuperar conexões e quando interromper uma mudança. Resume a aula com três perguntas: o substituto pode executar, consegue servir a carga e existe evidência desse comportamento na falha que prometemos suportar? Uma resposta negativa deve produzir uma ação concreta antes da aceitação.
"""Original finite capacity model. No scheduler, Kubernetes API or cloud calls.
All nodes supply residual CPU/memory available for identical workload Pods.
Only integer resource fit, explicit eligibility and node removal are modeled.
"""
import json
def integer(value, name, minimum=0):
if type(value) is not int or value < minimum:
raise ValueError(name + ' must be an integer >= ' + str(minimum))
return value
def capacity(nodes, cpu_m, memory_mib, unavailable=()):
integer(cpu_m, 'pod CPU', 1)
integer(memory_mib, 'pod memory', 1)
if not isinstance(nodes, list):
raise ValueError('nodes must be a list')
if not isinstance(unavailable, (list, tuple, set)):
raise ValueError('unavailable must be a collection of IDs')
if any(not isinstance(x, str) or not x for x in unavailable):
raise ValueError('unavailable IDs must be nonempty strings')
blocked = set(unavailable)
known, slots = set(), {}
for n in nodes:
if not isinstance(n, dict) or set(n) != {'id', 'zone', 'cpu_m', 'memory_mib', 'eligible'}:
raise ValueError('exact node schema required')
if not isinstance(n['id'], str) or not n['id'] or n['id'] in known:
raise ValueError('unique nonempty node IDs required')
if not isinstance(n['zone'], str) or not n['zone']:
raise ValueError('nonempty zone required')
if type(n['eligible']) is not bool:
raise ValueError('eligibility must be explicit boolean')
integer(n['cpu_m'], 'node CPU')
integer(n['memory_mib'], 'node memory')
known.add(n['id'])
slots[n['id']] = 0 if n['id'] in blocked or not n['eligible'] else min(n['cpu_m'] // cpu_m, n['memory_mib'] // memory_mib)
if blocked - known:
raise ValueError('unknown unavailable node ID')
by_zone = {}
for n in nodes:
by_zone[n['zone']] = by_zone.get(n['zone'], 0) + slots[n['id']]
return {'totalSlots': sum(slots.values()), 'slotsByNode': dict(sorted(slots.items())),
'slotsByZone': dict(sorted(by_zone.items()))}
def removal_plan(nodes, replicas, cpu_m, memory_mib, unavailable=()):
integer(replicas, 'replicas')
result = capacity(nodes, cpu_m, memory_mib, unavailable)
return result | {'replicas': replicas, 'fitsModel': result['totalSlots'] >= replicas,
'shortfall': max(0, replicas - result['totalSlots'])}
def voluntary_allowance(healthy, minimum_healthy):
# Simplified arithmetic, not a reproduction of the Kubernetes PDB controller.
integer(healthy, 'healthy')
integer(minimum_healthy, 'minimum healthy')
return max(0, healthy - minimum_healthy)
def node(key, zone='a', cpu_m=4000, memory_mib=8192, eligible=True):
return dict(id=key, zone=zone, cpu_m=cpu_m, memory_mib=memory_mib, eligible=eligible)
def run():
checks = []
def check(name, value):
if not value: raise AssertionError(name)
checks.append(name)
def reject(name, fn):
try: fn()
except ValueError: checks.append(name)
else: raise AssertionError(name)
nodes = [node('a1'), node('a2'), node('b1', 'b'), node('b2', 'b')]
baseline = removal_plan(nodes, 6, 2000, 3072)
single = removal_plan(nodes, 6, 2000, 3072, ['a1'])
zone = removal_plan(nodes, 6, 2000, 3072, ['a1', 'a2'])
check('baseline eight slots', baseline['totalSlots'] == 8 and baseline['fitsModel'])
check('one node removed six slots', single['totalSlots'] == 6 and single['fitsModel'])
check('one zone removed four slots', zone['totalSlots'] == 4 and not zone['fitsModel'])
check('zone loss shortfall two', zone['shortfall'] == 2)
check('memory limits slots', capacity([node('m', cpu_m=6000, memory_mib=6144)], 2000, 4096)['totalSlots'] == 1)
fragmented = [node('f1', cpu_m=1500), node('f2', cpu_m=1500)]
check('aggregate CPU cannot fit a single pod', capacity(fragmented, 2000, 1024)['totalSlots'] == 0)
check('ineligible node contributes zero', capacity([node('x', eligible=False)], 1000, 1024)['totalSlots'] == 0)
check('zero residual CPU contributes zero', capacity([node('z', cpu_m=0)], 1000, 1024)['totalSlots'] == 0)
check('zero residual memory contributes zero', capacity([node('z', memory_mib=0)], 1000, 1024)['totalSlots'] == 0)
check('per-zone counts retained', baseline['slotsByZone'] == {'a': 4, 'b': 4})
check('input ordering does not matter', capacity(list(reversed(nodes)), 2000, 3072) == capacity(nodes, 2000, 3072))
check('empty nodes zero slots', capacity([], 1000, 1024)['totalSlots'] == 0)
check('zero replicas fit empty model', removal_plan([], 0, 1000, 1024)['fitsModel'])
check('all nodes removed no slots', capacity(nodes, 2000, 3072, [n['id'] for n in nodes])['totalSlots'] == 0)
check('repeated removal ID counted once', capacity(nodes, 2000, 3072, ['a1', 'a1']) == capacity(nodes, 2000, 3072, ['a1']))
check('one voluntary disruption in simplified model', voluntary_allowance(6, 5) == 1)
check('no allowance below minimum', voluntary_allowance(4, 5) == 0)
check('no allowance at minimum', voluntary_allowance(5, 5) == 0)
reject('zero CPU request rejected', lambda: capacity(nodes, 0, 1024))
reject('zero memory request rejected', lambda: capacity(nodes, 1000, 0))
reject('boolean request rejected', lambda: capacity(nodes, True, 1024))
reject('fractional request rejected', lambda: capacity(nodes, 1.5, 1024))
reject('negative residual CPU rejected', lambda: capacity([node('n', cpu_m=-1)], 1000, 1024))
reject('negative residual memory rejected', lambda: capacity([node('n', memory_mib=-1)], 1000, 1024))
reject('duplicate node ID rejected', lambda: capacity([node('x'), node('x')], 1000, 1024))
reject('unknown excluded node rejected', lambda: capacity(nodes, 1000, 1024, ['missing']))
reject('string exclusion collection rejected', lambda: capacity(nodes, 1000, 1024, 'a1'))
reject('nonboolean eligibility rejected', lambda: capacity([node('x', eligible=1)], 1000, 1024))
reject('empty zone rejected', lambda: capacity([node('x', zone='')], 1000, 1024))
reject('negative replica count rejected', lambda: removal_plan(nodes, -1, 1000, 1024))
reject('negative healthy count rejected', lambda: voluntary_allowance(-1, 0))
reject('boolean minimum rejected', lambda: voluntary_allowance(1, False))
check('input nodes unchanged', nodes == [node('a1'), node('a2'), node('b1', 'b'), node('b2', 'b')])
return {'passed': len(checks), 'checks': checks, 'baseline': baseline, 'singleNode': single,
'zoneLoss': zone, 'network': False, 'vendorExecution': False, 'persistentWrites': False}
if __name__ == '__main__':
print(json.dumps(run(), indent=2))
Quatro nós distribuídos por duas zonas suportam seis Pods após perder um nó, mas apenas quatro Pods após perder uma zona inteira.
Armadilhas comuns
Interpretar PDB como garantia contra avarias, somar CPU entre nós, usar RWO como exclusividade de Pod, confiar em DNS por o PSC estar aceite e tratar fail fast como rollback.
Tópicos relacionados: Migração e reconciliação de dados · Redes híbridas e recuperação de armazenamento · Critérios de aceitação e autonomia de RUN
Calcula a capacidade restante, confirma elegibilidade e dependências, e aceita a mudança com evidência do cenário de falha requerido.
Referência: Kubernetes disruptions · Current linked standard guide; edition date unconfirmed (2026-09-30 inspection)