Capítulo 9: Simplicidade
Escrito por Max Luebbe
Editado por Tim Harvey
O preço da Confiabilidade é a busca pela simplicidade máxima.
C.A.R. Hoare, palestra do Prêmio Turing
Os sistemas de software são inerentemente dinâmicos e instáveis. Isso geralmente é verdade para sistemas complexos em geral. Um sistema de software só pode ser perfeitamente estável se existir no vácuo. Se pararmos de alterar a base de código, paramos de introduzir bugs. Se o hardware ou as bibliotecas subjacentes nunca mudarem, nenhum desses componentes introduzirá bugs. Se congelarmos a base de usuários atual, nunca teremos que dimensionar o sistema. Na verdade, um bom resumo da abordagem SRE para o gerenciamento de sistemas é: “No final do dia, nosso trabalho é manter a agilidade e a estabilidade em equilíbrio no sistema” (aspas para Johan Anderson).
Estabilidade do sistema versus agilidade
Às vezes, faz sentido sacrificar a estabilidade em prol da agilidade. Muitas vezes abordei um problema desconhecido de domínio conduzindo o que chamo de codificação exploratória – definindo uma vida útil explícita para qualquer código que escrevo com o entendimento de que vou precisar tentar e falhar uma vez para realmente entender a tarefa que preciso concluir. O código que vem com uma data de expiração pode ser muito mais liberal com cobertura de teste e lançamento porque nunca será enviado para produção ou visto pelos usuários.
Para a maioria dos sistemas de software em produção, queremos uma mistura equilibrada de estabilidade e agilidade. Os SREs trabalham para criar procedimentos, práticas e ferramentas que tornam o software mais confiável. Ao mesmo tempo, os SREs garantem que esse trabalho tenha o menor impacto possível na agilidade do desenvolvedor. Na verdade, por experiência, o SRE descobriu que processos confiáveis tendem a aumentar a agilidade do desenvolvedor: implementações rápidas e confiáveis em produção tornam as alterações mais fáceis de visualizar. Como resultado, quando um bug surge, leva menos tempo para localizá-lo e corrigi-lo. Incorporar confiabilidade ao desenvolvimento permite que os desenvolvedores concentrem a atenção no que realmente importa – a funcionalidade e o desempenho de seus softwares e sistemas.
A virtude do que é chato
Ao contrário de quase tudo na vida, “chato” é na verdade um atributo positivo quando se trata de software! Não queremos que nossos programas sejam espontâneos e interessantes; queremos que eles sigam o roteiro e cumpram seus objetivos de negócios de maneira previsível. Nas palavras do engenheiro do Google Robert Muth, “Ao contrário de uma história de detetive, a falta de empolgação, suspense e de quebra-cabeças é, na verdade, uma propriedade desejável do código-fonte”. Surpresas em produção são os inimigos da SRE.
Como Fred Brooks sugere em seu ensaio “No Silver Bullet—Essence and Accidents of Software Engineering”, é muito importante considerar a diferença entre a complexidade essencial e a complexidade acidental. A complexidade essencial é a complexidade inerente a uma determinada situação que não pode ser removida da definição de um problema, enquanto a complexidade acidental é mais fluida e pode ser resolvida com esforço de engenharia. Por exemplo, escrever um servidor web envolve lidar com a complexidade essencial de servir páginas web rapidamente. No entanto, se escrevermos um servidor da web em Java, podemos introduzir uma complexidade acidental ao tentar minimizar o impacto no desempenho do garbage collector.
Visando minimizar a complexidade acidental, as equipes SRE devem:
- Recuar quando uma complexidade acidental é introduzida nos sistemas pelos quais são responsáveis
- Esforçar-se constantemente para eliminar a complexidade dos sistemas que integram e pelos quais assumem responsabilidade operacional
Eu não desistirei do meu código!
Como os engenheiros são seres humanos que frequentemente formam um apego emocional às suas criações, confrontos sobre expurgos em grande escala da árvore de origem não são incomuns. Alguns podem protestar: “E se precisarmos desse código mais tarde?” “Por que simplesmente não comentamos o código para que possamos adicioná-lo facilmente de novo mais tarde?” ou “Por que não bloqueamos o código com uma bandeira em vez de excluí-lo?” Todas essas sugestões são terríveis. Os sistemas de controle de origem facilitam a reversão das alterações, enquanto centenas de linhas de código comentado criam distrações e confusão (especialmente à medida que os arquivos de origem continuam a evoluir), e o código que nunca é executado, bloqueado por um sinalizador que está sempre desativado, é um bomba-relógio metafórica esperando para explodir, como dolorosamente experimentado por Knight Capital, por exemplo (veja “Order In the Matter of Knight Capital Americas LLC“).
Correndo o risco de parecer extremo, quando você considera um serviço da web que deve estar disponível 24 horas por dia, 7 dias por semana, até certo ponto, cada nova linha de código escrita é uma responsabilidade. O SRE promove práticas que tornam mais provável que todo código tenha um propósito essencial, como examinar o código para ter certeza de que ele realmente direciona os objetivos de negócios, remover rotineiramente o código morto e construir a detecção de inchaço em todos os níveis de teste.
A métrica “Linhas de código negativas”
O termo “software bloat” foi cunhado para descrever a tendência do software de se tornar mais lento e maior com o tempo, como resultado de um fluxo constante de recursos adicionais. Embora o software bloat pareça intuitivamente indesejável, seus aspectos negativos tornam-se ainda mais claros quando considerados sob a perspectiva do SRE: cada linha de código alterada ou adicionada a um projeto cria o potencial para a introdução de novos defeitos e bugs. Um projeto menor é mais fácil de entender, testar e frequentemente tem menos defeitos. Tendo essa perspectiva em mente, talvez devessemos ter reservas quando do desejo de adicionar novos recursos a um projeto. Uma das codificações mais satisfatórias que já fiz foi excluir milhares de linhas de código em um momento em que não era mais útil.
APIs mínimas
O poeta francês Antoine de Saint Exupery escreveu: “A perfeição é finalmente alcançada não quando não há mais nada a acrescentar, mas quando não há mais nada a tirar“. Este princípio também se aplica ao projeto e construção de software. APIs são uma expressão particularmente clara de por que essa regra deve ser seguida.
Escrever APIs claras e minimalistas é um aspecto essencial do gerenciamento da simplicidade em um sistema de software. Quanto menos métodos e argumentos fornecermos aos consumidores da API, mais fácil será de entendê-la e mais esforço poderemos dedicar para tornar esses métodos os melhores possíveis. Novamente, um tema recorrente aparece: a decisão consciente de não assumir certos problemas nos permite focar em nosso problema central e tornar as soluções que explicitamente definimos para criar APIs substancialmente melhores. Quando falamos em software, menos é mais! Uma API pequena e simples geralmente também é uma marca registrada de um problema bem compreendido.
Modularidade
Expandindo para fora das APIs e binários únicos, muitas das regras básicas que se aplicam à programação orientada a objetos também se aplicam ao projeto de sistemas distribuídos. A capacidade de fazer alterações em partes do sistema de forma isolada é essencial para a criação de um sistema com suporte. Especificamente, o acoplamento fraco entre binários, ou entre binários e configuração, é um padrão de simplicidade que promove simultaneamente a agilidade do desenvolvedor e a estabilidade do sistema. Se um bug for descoberto em um programa que é um componente de um sistema maior, esse bug pode ser corrigido e colocado em produção independentemente do resto do sistema.
Embora a modularidade que as APIs ofereçam possa parecer simples, não é tão aparente que a noção de modularidade também se estenda à forma como as alterações nas APIs são introduzidas. Apenas uma única alteração em uma API pode forçar os desenvolvedores a reconstruir todo o seu sistema e correr o risco de introduzir novos bugs. As APIs de controle de versão permitem que os desenvolvedores continuem a usar a versão da qual seu sistema depende enquanto atualizam para uma versão mais recente de maneira segura e ponderada. A cadência de lançamento pode variar em todo o sistema, em vez de exigir um impulso de produção total de todo o sistema sempre que um recurso é adicionado ou melhorado.
À medida que um sistema fica mais complexo, a separação de responsabilidades entre APIs e entre binários torna-se cada vez mais importante. Esta é uma analogia direta com o design de classe orientada a objetos: assim como se entende que é uma prática ruim escrever uma classe “pegadinha” que contenha funções não relacionadas, também é uma prática ruim criar e colocar em produção um “utilitário” ou binário “misc”. Um sistema distribuído bem projetado consiste em colaboradores, cada um dos quais com um propósito claro e bem definido.
O conceito de modularidade também se aplica a formatos de dados. Um dos pontos fortes centrais e objetivos de design dos buffers de protocolo do Google era criar um formato de cabo que fosse compatível com versões anteriores e posteriores. (Buffers de protocolo, também chamados de “protobufs”, são um mecanismo extensível neutro de plataforma e linguagem para serializar dados estruturados).
Simplicidade de lançamento
Lançamentos simples são geralmente melhores do que lançamentos complexos. É muito mais fácil medir e compreender o impacto de uma única alteração em vez de um lote de alterações lançadas simultaneamente. Se liberarmos 100 alterações não relacionadas em um sistema ao mesmo tempo e o desempenho piorar, entender quais alterações afetaram o desempenho e como o fizeram exigirá um esforço considerável ou mesmo instrumentação adicional. Se o lançamento for realizado em lotes menores, podemos nos mover mais rápido com mais confiança porque cada alteração de código pode ser entendida isoladamente no sistema maior. Essa abordagem de lançamento pode ser comparada à descida gradiente no aprendizado de máquina, aonde encontramos uma solução ideal dando pequenos passos de cada vez, considerando que cada alteração resulta em uma melhoria ou degradação.
Uma conclusão simples
Este capítulo repetiu um tema continuamente: a simplicidade do software é um pré-requisito para a confiabilidade. Não estamos sendo preguiçosos quando consideramos como podemos simplificar cada etapa de uma determinada tarefa. Em vez disso, estamos esclarecendo o que realmente queremos realizar e como podemos fazer isso mais facilmente. Cada vez que dizemos “não” a um recurso, não estamos restringindo a inovação; estamos mantendo o ambiente livre de distrações para que o foco permaneça totalmente na inovação e a engenharia real possa prosseguir.
Fonte: Google SRE Book