La Suposición que Voló Demasiado Cerca del Sol
A principios de los años ochenta, NASA se enfrentó a una pregunta que aún atormenta a la ingeniería safety-critical hoy en día: ¿cómo toleras bugs que aún no has encontrado? Su respuesta fue la programación N-version. Entrega la misma especificación a tres equipos independientes. Ejecuta los tres programas en paralelo. Vota sobre los outputs. Si un equipo escribió un bug, los otros dos lo superarían en votos.
Suena como sentido común matemático. También es incorrecto.
En 1986, John Knight y Nancy Leveson publicaron el resultado de un experimento a gran escala financiado por NASA. Veintisiete estudiantes de posgrado en dos universidades escribieron implementaciones independientes de la misma especificación de interceptor de misiles balísticos. Cada programa era individualmente extremadamente fiable. Seis versiones nunca fallaron. Veintitrés superaron el 99,9% de un millón de casos de prueba generados aleatoriamente.
Pero cuando múltiples versiones fallaban con el mismo input, fallaban juntas con mucha más frecuencia de lo que la independencia estadística predecía. El z-score fue 100,55. En un intervalo de confianza del 99%, la hipótesis nula de independencia fue destrozada. NASA había apostado a que el error humano es aleatorio. Knight y Leveson demostraron que se agrupa.
Qué Promete Realmente la Programación N-Version
La lógica detrás de la programación N-version está tomada de la redundancia de hardware. Si acoplas tres giroscopios idénticos a un cohete y uno se desvía, los otros dos superan en votos a la desviación. Las fallas son independientes porque los giroscopios son objetos físicos sujetos a variación independiente de manufactura, gradientes de temperatura y modos de vibración.
El software es diferente. Cuando pides a tres equipos que resuelvan el mismo problema, no obtienes tres releases de dados independientes. Obtienes tres humanos que leyeron la misma especificación ambigua, aprendieron los mismos algoritmos de los mismos libros de texto, y escriben código en el mismo lenguaje con la misma biblioteca estándar. Sus errores se correlacionan porque sus inputs se correlacionan.
La arquitectura N-version se ve así en la práctica:
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 es la parte fácil. La parte difícil es la suposición que se esconde dentro de él: que implementations[0], implementations[1] e implementations[2] fallan en subconjuntos descorrelacionados del espacio de inputs. Knight y Leveson demostraron que esa suposición es el defecto.
De Dónde Vienen las Fallas Correlacionadas
El experimento de Knight-Leveson reveló dos mecanismos distintos para fallas de modo común, y ninguno se resuelve pidiendo a los equipos que “se esfuercen más.”
El primero es la ambigüedad de la especificación. La especificación del interceptor de misiles contenía edge cases genuinamente difíciles. Ocho de los veintisiete programadores manejaron mal el caso donde tres puntos de radar eran colineales. Los errores eran diferentes. Uno tenía un off-by-one en un subíndice de array. Uno usó un algoritmo con poca estabilidad numérica. Uno olvidó un boundary case por completo. Pero las fallas se agrupaban en los mismos inputs difíciles porque el problema en sí era difícil, no porque los programadores fueran descuidados.
Este es el “factor de dificultad.” Cuando un input se sitúa en una boundary condition, requiere matemática de punto flotante complicada, o involucra una transición de estado poco especificada, los equipos independientes tienden a tener dificultades en el mismo lugar. Sus soluciones difieren. Sus regiones de falla se solapan.
El segundo mecanismo son los modelos mentales compartidos. Los programadores entrenados en el mismo currículo aplican las mismas heurísticas. Recurren al mismo algoritmo de ordenamiento, al mismo patrón de defensive copy, a la misma comparación epsilon para igualdad de punto flotante. Cuando ese default compartido es incorrecto para el problema en cuestión, cada equipo se precipita por el mismo acantilado.
El propio Software Safety Guidebook de NASA eventualmente reconoció esto explícitamente. Señala que “muchos profesionales consideran la programación N-version como ineficaz, o incluso contraproducente.” En un estudio de NASA sobre una aeronave experimental, cada problema de software encontrado durante las pruebas provino del sistema de gestión de redundancia. El software primario de control de vuelo era impecable. La capa N-version fue lo único que se rompió.
El Impuesto a la Complejidad del que Nadie Habla
Incluso si la programación N-version funcionara como se anuncia, extrae un precio elevado. Pagas por tres implementaciones completas. Pagas por un voter que en sí mismo contiene lógica. Pagas por el overhead operacional de ejecutar tres procesos y reconciliar sus outputs.
Ese voter no es un fragmento de código trivial. Debe manejar empates, timeouts, formatos de output divergentes, y el caso donde la mayoría está equivocada. Brunelle y Eckhardt demostraron esto en 1985 con el sistema operativo SIFT. Dos nuevas N-versions superaron en votos a la implementación original, correcta, y produjeron una respuesta errónea. El sistema de redundancia creó el error que se suponía que debía prevenir.
La complejidad se acumula de maneras que son difíciles de medir hasta que te muerden. Ahora tienes tres deployables para versionar, tres matrices de prueba para mantener, y tres equipos que interpretarán los cambios de especificación de manera diferente cuando los requisitos inevitablemente cambien. La superficie de ataque para el error humano crece más rápido de lo que mejora la fiabilidad.
Qué Funciona Realmente en su Lugar
NASA no abandonó la tolerancia a fallas. Abandonó la suposición específica de que la diversidad proviene de la independencia. Los sistemas modernos de alta garantía usan una combinación de técnicas que abordan los modos de falla reales que Knight y Leveson identificaron.
Métodos formales para el critical path. En lugar de esperar que tres equipos interpreten una especificación correctamente, escribe la especificación en una forma verificable por máquina. Herramientas como TLA+, Coq y SPIN verifican que un diseño satisface sus invariantes antes de que alguien escriba una línea de código de implementación. El propio Remote Agent Experiment de NASA usó SPIN para encontrar bugs de concurrencia que habían pasado desapercibidos tras pruebas extensivas.
Diversidad a través de stacks tecnológicos disímiles. Si necesitas redundancia, hazla profunda. El sistema de control de vuelo del Airbus A330 usa arquitecturas de hardware disímiles, diferentes lenguajes de programación y compilers independientes para sus canales primario y de respaldo. El objetivo no es meramente equipos independientes sino modos de falla independientes en cada capa del stack.
Simplificación sobre duplicación. El Software Safety Guidebook de NASA recomienda finalmente la programación N-version solo para “funciones pequeñas y simples.” La lección es poco glamorosa pero efectiva: el sistema más fiable es aquel lo suficientemente simple para razonar sobre él directamente. Cada línea de código de gestión de redundancia es una línea que puede fallar.
Así es como se ve un enfoque simplificado y guiado por especificaciones en la práctica. En lugar de tres implementaciones opacas votando sobre outputs, codifica la invariante crítica explícitamente y verifícala en 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}"
Esto no es programación N-version. Es una implementación con un boundary de seguridad explícito y testeable. El boundary es donde va tu esfuerzo. No en esperar que dos de tres equipos lo hicieran bien.
El Eco Moderno
El artículo de 1986 de Knight y Leveson porta una advertencia que se vuelve más relevante cada año. Las fallas correlacionadas no requieren equipos correlacionados. Requieren inputs correlacionados y dificultad correlacionada. A medida que las herramientas de coding asistidas por IA proliferan, estamos ejecutando un nuevo experimento N-version a escala planetaria. Modelos entrenados en corpus superpuestos, prompts con patrones similares, producen código con modos de falla compartidos.
Investigaciones recientes sobre generación de código con LLM sugieren tasas de co-error entre el 15% y el 30% para componentes generados por IA. El beta factor, la fracción de fallas atribuibles a causa común, ya puede exceder los valores de programadores humanos que Knight y Leveson midieron. Estamos repitiendo el experimento con más participantes y la misma suposición defectuosa.
NASA aprendió esta lección de la manera costosa. No necesitas tres implementaciones. Necesitas una implementación con una invariante que puedas enunciar, verificar y confiar. Todo lo demás es optimismo disfrazado de ingeniería.
Preguntas Frecuentes
¿Qué es la programación N-version?
La programación N-version es una técnica de tolerancia a fallas de software donde múltiples equipos independientes implementan la misma especificación. Los programas se ejecutan en paralelo y un voter selecciona el output de la mayoría, bajo la suposición de que equipos independientes cometerán errores independientes.
¿NASA usó realmente programación N-version?
Sí. NASA financió investigaciones tempranas sobre software multi-version y en algún momento algunos documentos de política efectivamente mandataban programación N-version para sistemas tolerantes a fallas. La secuencia de encendido de motores del Space Shuttle la usó, aunque NASA más tarde restringió su uso a funciones pequeñas y simples tras estudios empíricos que revelaron sus limitaciones.
¿Qué fue el experimento de Knight-Leveson?
En 1986, John Knight y Nancy Leveson hicieron que veintisiete programadores escribieran implementaciones independientes de una especificación de interceptor de misiles. A lo largo de un millón de pruebas, los programas exhibieron fallas coincidentes significativamente más frecuentes de lo que la independencia estadística predeciría, rechazando la suposición central que sustenta la programación N-version.
¿Vale la pena usar programación N-version alguna vez?
En casos limitados. La guía actual de NASA sugiere que puede ser apropiada para funciones pequeñas y bien definidas donde se pueda imponer verdadera diversidad. Para el desarrollo de aplicaciones en general, el costo de complejidad y el riesgo de fallas correlacionadas superan los beneficios. Los métodos formales, la verificación de invariantes en runtime y la simplificación suelen ser mejores inversiones.