A Suposição que Voou Demasiado Perto do Sol
No início dos anos 1980, a NASA enfrentou uma questão que ainda assombra a engenharia safety-critical hoje: como tolerar bugs que ainda não encontraste? A resposta foi a programação N-version. Dá a mesma especificação a três equipas independentes. Executa os três programas em paralelo. Vota nos outputs. Se uma equipa escreveu um bug, as outras duas o superam em voto.
Parece senso comum matemático. Também está errado.
Em 1986, John Knight e Nancy Leveson publicaram o resultado de uma experiência em larga escala financiada pela NASA. Vinte e sete estudantes de pós-graduação em duas universidades escreveram implementações independentes da mesma especificação de interceptador de mísseis balísticos. Cada programa era individualmente extremamente fiável. Seis versões nunca falharam. Vinte e três passaram mais de 99,9% de um milhão de casos de teste gerados aleatoriamente.
Mas quando várias versões falharam no mesmo input, falharam juntas com uma frequência muito superior à prevista pela independência estatística. O z-score foi 100,55. Num intervalo de confiança de 99%, a hipótese nula de independência foi obliterada. A NASA apostou que o erro humano é aleatório. Knight e Leveson provaram que ele se agrupa.
O Que a Programação N-Version Realmente Promete
A lógica por detrás da programação N-version é emprestada da redundância de hardware. Se acoplas três giroscópios idênticos a um foguetão e um deriva, os outros dois superam a deriva em voto. As falhas são independentes porque os giroscópios são objetos físicos sujeitos a variação de fabricação independente, gradientes de temperatura e modos de vibração.
O software é diferente. Quando pedes a três equipas para resolver o mesmo problema, não estás a obter três releases de dados independentes. Estás a obter três humanos que leram a mesma especificação ambígua, aprenderam os mesmos algoritmos dos mesmos livros didáticos, e escrevem código na mesma linguagem com a mesma biblioteca standard. Os erros deles correlacionam porque os inputs deles correlacionam.
A arquitetura N-version parece-se com isto na prática:
from typing import Callable, List, TypeVar
T = TypeVar('T')
def n_version_vote(
implementations: List[Callable[[float], T]],
input_value: float
) -> T:
"""Run N versions and return the majority output."""
results = [impl(input_value) for impl in implementations]
# Simple majority vote
from collections import Counter
counts = Counter(str(r) for r in results)
most_common = counts.most_common(1)[0][0]
# Return the actual value that matched the winning string
for r in results:
if str(r) == most_common:
return r
raise RuntimeError("No consensus")
Este voter é a parte fácil. A parte difícil é a suposição escondida dentro dele: que implementations[0], implementations[1], e implementations[2] falham em subsets não correlacionados do espaço de input. Knight e Leveson mostraram que essa suposição é a falha.
De Onde Vêm as Falhas Correlacionadas
A experiência Knight-Leveson revelou dois mecanismos distintos de falha de modo comum, e nenhum é resolvível pedindo às equipas para “tentarem mais.”
O primeiro é a ambiguidade da especificação. A especificação do interceptador de mísseis continha casos limite que eram genuinamente difíceis. Oito dos vinte e sete programadores trataram mal o caso em que três pontos de radar eram colineares. Os erros eram diferentes. Um tinha um off-by-one num subscript de array. Um usou um algoritmo com pouca estabilidade numérica. Um esqueceu-se completamente de um caso limite. Mas as falhas agruparam-se nos mesmos inputs difíceis porque o próprio problema era difícil, não porque os programadores fossem descuidados.
Este é o “fator de dificuldade.” Quando um input está numa condição limite, requer matemática de vírgula flutuante complicada, ou envolve uma transição de estado subespecificada, equipas independentes tendem a ter dificuldade no mesmo sítio. As soluções deles diferem. As regiões de falha sobrepõem-se.
O segundo mecanismo é modelos mentais partilhados. Programadores treinados no mesmo currículo aplicam as mesmas heurísticas. Recorrem ao mesmo algoritmo de sorting, ao mesmo padrão de defensive copy, à mesma comparação epsilon para igualdade de vírgula flutuante. Quando esse default partilhado está errado para o problema em questão, todas as equipas caem do mesmo precipício.
O próprio Software Safety Guidebook da NASA acabou por reconhecer isto explicitamente. Nota que “muitos profissionais consideram a programação N-Version ineficaz, ou mesmo contraproducente.” Numa experiência da NASA num avião experimental, todos os problemas de software encontrados durante os testes vieram do sistema de gestão de redundância. O software de controlo de voo primário estava impecável. A camada N-version foi a única coisa que se estragou.
O Imposto de Complexidade de Que Ninguém Fala
Mesmo que a programação N-version funcionasse como prometido, extrai um preço elevado. Pagas por três implementações completas. Pagas por um voter que ele próprio contém lógica. Pagas pela sobrecarga operacional de executar três processos e reconciliar os outputs.
Esse voter não é um pedaço de código trivial. Tem de lidar com empates, timeouts, formatos de output divergentes, e o caso em que a maioria está errada. Brunelle e Eckhardt demonstraram isto em 1985 com o sistema operativo SIFT. Duas novas N-versions superaram em voto a implementação original, correta, e produziram uma resposta errada. O sistema de redundância criou o erro que era suposto prevenir.
A complexidade acumula-se de formas que são difíceis de medir até que te mordam. Agora tens três deployables para versionar, três matrizes de teste para manter, e três equipas que irão interpretar mudanças de especificação de forma diferente quando os requisitos inevitavelmente mudarem. A área de superfície para erro humano cresce mais depressa do que a fiabilidade melhora.
O Que Realmente Funciona em Vez Disso
A NASA não abandonou a tolerância a falhas. Abandonou a suposição específica de que a diversidade vem da independência. Sistemas modernos de alta garantia usam uma combinação de técnicas que abordam os modos de falha reais que Knight e Leveson identificaram.
Métodos formais para o caminho crítico. Em vez de esperar que três equipas interpretem uma especificação corretamente, escreve a especificação numa forma verificável por máquina. Ferramentas como TLA+, Coq, e SPIN verificam que um design satisfaz os seus invariantes antes de alguém escrever uma linha de código de implementação. A experiência Remote Agent da NASA usou SPIN para encontrar bugs de concorrência que tinham passado por testes extensivos.
Diversidade através de stacks tecnológicas dissimilares. Se precisares de redundância, fá-la profunda. O sistema de controlo de voo do Airbus A330 usa arquiteturas de hardware dissimilares, linguagens de programação diferentes, e compilers independentes para os seus canais primário e de backup. O objetivo não é meramente equipas independentes mas modos de falha independentes em todas as camadas da stack.
Simplificação em vez de duplicação. O NASA Software Safety Guidebook recomenda em última instância a programação N-version apenas para “small simple functions.” A lição é pouco glamourosa mas eficaz: o sistema mais fiável é aquele suficientemente simples para ser raciocinado diretamente. Cada linha de código de gestão de redundância é uma linha que pode falhar.
Eis como uma abordagem simplificada, guiada pela especificação, parece na prática. Em vez de três implementações opacas a votarem em outputs, codifica o invariante crítico explicitamente e verifica-o em runtime:
from dataclasses import dataclass
from typing import Optional
@dataclass(frozen=True)
class LaunchCommand:
thrust_level: float # 0.0 to 1.0
abort_flag: bool
def is_valid(self) -> bool:
"""Runtime invariant check derived from the formal spec."""
if not (0.0 <= self.thrust_level <= 1.0):
return False
if self.abort_flag and self.thrust_level > 0.0:
return False
return True
def execute_command(cmd: LaunchCommand) -> Optional[str]:
if not cmd.is_valid():
raise ValueError("Invariant violation: command violates safety spec")
# Execute only after the single, explicit check passes.
return f"Executing thrust={cmd.thrust_level}, abort={cmd.abort_flag}"
Isto não é programação N-version. É uma implementação com um limite de segurança explícito e testável. O limite é onde o teu esforço vai. Não em esperar que duas em três equipas acertaram.
O Eco Moderno
O artigo de Knight e Leveson de 1986 carrega um aviso que se torna mais relevante a cada ano. Falhas correlacionadas não requerem equipas correlacionadas. Requerem inputs correlacionados e dificuldade correlacionada. À medida que as ferramentas de coding assistido por IA proliferam, estamos a correr uma nova experiência N-version à escala planetária. Modelos treinados em corpora sobrepostos, promptados com padrões semelhantes, produzem código com modos de falha partilhados.
Investigação recente sobre geração de código por LLM sugere taxas de co-erro entre 15% e 30% para componentes gerados por IA. O fator beta, a fração de falhas atribuível a causa comum, pode já exceder os valores de programadores humanos que Knight e Leveson mediram. Estamos a repetir a experiência com mais participantes e a mesma suposição falhada.
A NASA aprendeu esta lição da maneira cara. Não precisas de três implementações. Precisas de uma implementação com um invariante que consigas declarar, verificar, e confiar. Tudo o resto é otimismo disfarçado de engenharia.
FAQ
O que é programação N-version?
A programação N-version é uma técnica de tolerância a falhas de software em que múltiplas equipas independentes implementam a mesma especificação. Os programas executam em paralelo e um voter seleciona o output da maioria, na suposição de que equipas independentes cometerão erros independentes.
A NASA realmente usou programação N-version?
Sim. A NASA financiou investigação inicial em software multi-version e num dado momento alguns documentos de política efetivamente mandatavam a programação N-version para sistemas tolerantes a falhas. A sequência de power-up dos motores do Space Shuttle usou-a, embora a NASA mais tarde restringisse o seu uso a funções pequenas e simples após estudos empíricos revelarem as suas limitações.
O que foi a experiência Knight-Leveson?
Em 1986, John Knight e Nancy Leveson fizeram vinte e sete programadores escreverem implementações independentes de uma especificação de interceptador de mísseis. Ao longo de um milhão de testes, os programas exibiram significativamente mais falhas coincidentes do que a independência estatística preveria, rejeitando a suposição central subjacente à programação N-version.
A programação N-version alguma vez vale a pena usar?
Em casos limitados. A orientação atual da NASA sugere que pode ser apropriada para funções pequenas e bem definidas onde a verdadeira diversidade pode ser imposta. Para desenvolvimento de aplicações geral, o custo de complexidade e o risco de falha correlacionada superam os benefícios. Métodos formais, verificação de invariantes em runtime, e simplificação são normalmente melhores investimentos.