3. Desplegar sin romper: versiones, vuelta atrás y cambios en caliente
Nivel 12: Probar, desplegar y dar el paso
Un despliegue es cambiar una pieza de un sistema que tiene dinero puesto. La diferencia con cualquier otro programa es que aquí el estado no vive solo en tu disco: vive también en el bróker, en forma de órdenes vivas y posiciones abiertas que no se enteran de que has hecho un cambio de versión.
El despliegue no coincide con el mercado abierto
La ventana se define en el mismo calendario que el orquestador del nivel 9 ya conoce, y no basta con que el mercado esté cerrado. Hay que parar el proceso a propósito, comprobar que no queda nada vivo y solo entonces cambiar nada. Ese orden importa: comprobar con el proceso todavía en marcha es comprobar una foto que puede caducar mientras copias ficheros.
from datetime import datetime, timezone
def guardia_de_despliegue(calendario, broker, estado):
ahora = datetime.now(timezone.utc)
if calendario.mercado_abierto(ahora):
raise SystemExit("mercado abierto: despliegue abortado")
if broker.ordenes_abiertas():
raise SystemExit("hay ordenes vivas: cancelalas o espera a que venzan")
if estado.fase != "reposo":
raise SystemExit("la maquina de estados no esta en reposo")
return TrueVolver atrás en un minuto
El criterio no es "tener copia de seguridad": es que la versión anterior siga instalada y arrancable con un solo comando. La forma más simple que funciona: cada versión en su propio directorio, con su propio entorno, y un enlace llamado actual que apunta a la que está en uso. Desplegar es mover el enlace y arrancar; volver atrás es moverlo al directorio anterior y arrancar. Segundos, no una reconstrucción.
Tres condiciones para que eso sea cierto de verdad:
- Las dependencias viven dentro del directorio de la versión, con versiones fijadas. Si la vieja tuviera que reinstalar paquetes, no hay vuelta atrás.
- La configuración va versionada junto al código, nunca editada a mano en el servidor.
- Hay una prueba de humo posterior al arranque y anterior a habilitar el envío de órdenes: conectar, pedir posiciones, pedir el reloj del bróker, comprobar la versión del estado.
Y el detalle que casi nadie cumple: la vuelta atrás hay que ensayarla. Un rollback que nunca se ha ejecutado es una intención, no un procedimiento.
El estado tiene que sobrevivir al cambio de versión
Aquí se rompen los despliegues aparentemente inocentes. El estado persistente del nivel 8 lo escribió la versión vieja y lo va a leer la nueva. Si la nueva añade un campo obligatorio, cambia el significado de uno existente o renombra una fase, el arranque falla; o peor, no falla y hace otra cosa. La disciplina es la de cualquier base de datos: el estado lleva número de versión de esquema y el arranque lo comprueba.
ESQUEMA_ESPERADO = 7
def migrar_6_a_7(e):
e = dict(e)
e["precio_referencia"] = e.get("precio_decision")
e["version"] = 7
return e
MIGRACIONES = {6: migrar_6_a_7}
def cargar_estado(bruto):
v = bruto.get("version", 0)
while v < ESQUEMA_ESPERADO:
if v not in MIGRACIONES:
raise RuntimeError("no se como migrar el estado desde esa version")
bruto = MIGRACIONES[v](bruto)
v = bruto["version"]
if v > ESQUEMA_ESPERADO:
raise RuntimeError("estado escrito por una version mas nueva: no arranco")
return brutoEl caso peligroso es el último: una versión vieja leyendo un estado que escribió una nueva. Ocurre justo cuando vuelves atrás, que es el peor momento posible para descubrirlo. Por eso el ensayo del rollback se hace con el estado de la versión nueva delante, no con uno limpio.
Órdenes vivas mientras despliegas
Tres políticas, y se elige una por escrito antes de necesitarla:
- Cancelar todo antes de parar. Lo más simple y lo correcto para casi todo el mundo. Exige que la estrategia tolere quedarse sin órdenes pendientes.
- Dejarlas vivas y adoptarlas al arrancar. La versión nueva consulta las órdenes abiertas y las incorpora a su estado por identificador de intención. Solo funciona si esos identificadores sobreviven al cambio de versión.
- Dejarlas vencer. Validez hasta el cierre y despliegue después. Ordenado, pero inservible si operas en varios husos.
Lo que no es una política es parar el proceso y confiar en que no hubiera nada.
La configuración caliente es la vía rápida al desastre
Bajar un límite de exposición editando un fichero en el servidor a las once de la mañana funciona, y ese es el problema: funciona sin dejar rastro. Nadie sabrá dentro de un mes qué valor estaba activo, el siguiente despliegue lo revertirá en silencio y el análisis del incidente del nivel 11 se hará sobre una configuración que no era la que había. La configuración se trata como código: repositorio, revisión, mismo procedimiento de despliegue, y el sistema registra al arrancar el hash del fichero que ha cargado. Si algún parámetro debe poder cambiarse en caliente, y los interruptores del nivel 11 lo necesitan, ese cambio pasa por una vía que escribe quién, cuándo, valor anterior y valor nuevo.
Knight Capital, 1 de agosto de 2012
Knight Capital era uno de los mayores creadores de mercado de Estados Unidos. Desplegó código nuevo en sus servidores de ejecución y en uno de los ocho la actualización no llegó. El código nuevo reutilizaba una bandera de configuración que en el código antiguo activaba una función de pruebas retirada años antes. Al abrir el mercado, siete servidores hicieron lo previsto y el octavo empezó a ejecutar el comportamiento viejo, comprando y vendiendo sin freno a ritmo de mercado.
En menos de una hora la pérdida antes de impuestos rondó los 440 millones de dólares y dejó a la firma al borde de la quiebra; sobrevivió con una inyección de capital de emergencia y acabó absorbida por otra compañía, y la SEC la sancionó después por deficiencias en sus controles. Fallaron cuatro cosas de este módulo a la vez: un despliegue que no verificó que las ocho máquinas quedaran iguales, una bandera reutilizada en lugar de retirada, código muerto todavía instalado y ningún corte automático por pérdida acumulada.