Industria TI: la mentira de «reescribir desde cero» — por qué falla y qué funciona

¿Te fue útil?

Heredas un sistema mantenido con tareas cron, conocimiento tribal y un esquema de base de datos que parece diseñado durante un simulacro de emergencia. Alguien pronuncia las palabras mágicas: “Reescribámoslo desde cero”. Asienten las cabezas. Se refrescan las hojas de ruta. Aparece un nuevo repositorio como un cuaderno nuevo el 1 de enero.

Luego sucede la producción. La reescritura no conoce a tus clientes, tus casos límite, tus restricciones operativas ni la gravedad de tus datos. Y la rotación de on-call definitivamente no se apuntó para “dos sistemas, ambos rotos, para siempre”.

La mentira: por qué «reescribir desde cero» parece verdad

Las reescrituras venden esperanza. Ofrecen un corte limpio respecto al desorden acumulado: no más frameworks legacy, no más “parches temporales” de 2017, no más módulos inatestables, no más esa rutina almacenada que todos temen tocar. El argumento es emocionalmente correcto. El problema es que los sistemas en producción no funcionan con emociones. Funcionan con invariantes.

Una reescritura desde cero suele venderse como un proyecto técnico. En realidad es una apuesta organizacional: que puedes reconstruir no solo código, sino comportamiento, semántica de datos, postura operativa y manejo de fallos—mientras el sistema antiguo continúa evolucionando bajo carga real de clientes.

Aquí está la parte que la gente omite en la presentación de la reescritura: el sistema antiguo es un registro fósil de incidentes reales. Contiene el tejido cicatricial de caídas, intentos de fraude, dispositivos cliente extraños, fallos parciales y sorpresas regulatorias. Ese tejido cicatricial es feo. También es valioso.

Las reescrituras ignoran que los “requisitos” no están en el sistema de tickets. Están en las gráficas de producción, en las notas de on-call y en las suposiciones silenciosas que mantienen las luces encendidas. Cuando reescribes, borras esas suposiciones—y luego las redescubres a las 2:13 a. m.

Broma #1: Un plan de reescritura es como comprarte una cinta de correr nueva para ponerte en forma. La compra se siente productiva; lo que de verdad cuenta es ponerse a correr.

Por qué las reescrituras fallan en producción (las razones reales)

1) La paridad de funciones es una trampa, no un hito

Los equipos tratan la “paridad de funciones” como una lista de comprobación. En la práctica, el sistema antiguo no tiene funciones; tiene comportamientos. Los comportamientos incluyen valores por defecto no documentados, peculiaridades temporales, expectativas de idempotencia, semántica de reintentos y flujos de corrección de datos que ocurren fuera del camino feliz.

Cuando una reescritura apunta a la paridad, se fija en la superficie visible de UI/API y pierde las partes desordenadas que importan: cómo se comporta el sistema cuando una pasarela de pagos se agota, cuando un downstream está lento, cuando un cliente reintenta un POST, cuando los relojes se desincronizan o cuando tienes que reprocesar un día de eventos.

2) Los datos son el producto, y los datos pesan

La mayoría de los sistemas son sistemas de datos con cara de UI. Una reescritura que no empiece por la semántica de datos—qué significan los registros, cómo cambian con el tiempo, qué se permite que sea eventualmente consistente—derivará hacia “un nuevo esquema de base de datos que se siente más bonito” y luego chocará con la realidad en el corte.

La migración de datos no es un proyecto de fin de semana. Es un ejercicio sostenido de fiabilidad con backfills, escrituras duales (o captura de cambio de datos), reconciliación y planes de reversión. Si tu plan de reescritura no incluye meses de ejecución de ambas rutas de datos, no estás planificando un corte; estás lanzando una moneda al aire.

3) La reescritura crea una organización con cerebro dividido

Dos bases de código significan dos prioridades, dos colas de bugs, dos modelos operativos y una base de clientes compartida que espera el mismo servicio. Normalmente las personas sénior se ven atraídas por la reescritura, dejando el sistema legacy con capacidad reducida y riesgo creciente. Entonces ocurre un incidente en el legado y el calendario de la reescritura se ve saqueado para responder. La reescritura se ralentiza. El legado se degrada. Todos pierden.

4) La preparación operativa no es “ahora tenemos Kubernetes”

Los stacks modernos pueden empeorar las cosas cuando se usan como atrezzo de credibilidad. Cambiar un conjunto de modos de fallo por otro no es progreso; son nuevas formas de alertar a la gente.

La preparación operativa trata de: SLOs bien definidos, instrumentación, calidad de alertas, despliegues controlados, modelado de capacidad, gestión de dependencias, runbooks y una cultura capaz de sostener el cambio continuo. Si el equipo de reescritura no puede ejecutar bien el sistema antiguo, tampoco ejecutará bien el nuevo—solo que con YAML más brillante.

5) El rendimiento es una propiedad emergente y no se puede probar con unit tests

El sistema antiguo tiene trucos de rendimiento nacidos en batalla: cachés en capas extrañas, tablas desnormalizadas, agregados precomputados, índices cuidadosamente ubicados, coalescencia de peticiones y reglas de “no hacer eso en el hot path”. La reescritura suele arrancar limpia, luego aparecen regresiones de rendimiento bajo carga tipo producción. Entonces pegas cachés y colas y trabajos en background, reconstruyendo finalmente la misma complejidad—sin la memoria institucional.

6) El nuevo sistema es correcto a pequeña escala y equivocado a gran escala

Las revisiones de código detectan problemas locales. No detectan el comportamiento a nivel de sistema bajo fallos parciales. Las reescrituras fallan porque modelan el mundo como fiable y consistente. La producción no es ninguna de las dos cosas.

7) Seguridad y cumplimiento no son “para después”

Las reescrituras suelen posponer controles de seguridad, rastros de auditoría, reglas de retención y acceso con privilegios mínimos. Luego descubres que los “registros raros” del sistema legacy estaban ahí porque un auditor hizo una pregunta muy específica. O bien te pones a improvisar o retrasas el corte. Ambas opciones son caras.

Hechos e historia: la industria ya pasó por esto

  • 1980s–1990s: Grandes organizaciones intentaron repetidamente reescrituras impulsadas por herramientas CASE de sistemas mainframe legacy; muchas colapsaron por el alcance y la complejidad de la migración de datos.
  • El esfuerzo del Año 2000 (Y2K) enseñó a las empresas una lección brutal: reemplazarlo todo rara vez es factible; la remediación y el triage basado en riesgo suelen ganar.
  • La era de los rollouts ERP “big bang” mostró un patrón: los cortes fallan cuando los procesos de negocio no se mapean a flujos de trabajo reales y excepciones.
  • El auge de la arquitectura orientada a servicios (SOA) prometió modularidad; muchos proyectos entregaron monolitos distribuidos con más latencia y depuración más difícil.
  • La popularidad de microservicios (mediados de 2010s) incrementó la tentación de reescribir, pero también aumentó el costo de la madurez operativa: tracing, mapeo de dependencias y contención de fallos se volvieron obligatorios.
  • La maduración de las herramientas de captura de cambio de datos (CDC) hizo que las migraciones incrementales fueran más prácticas, cambiando la economía lejos de las reescrituras tipo big-bang.
  • La elasticidad cloud redujo algunos riesgos de capacidad, pero introdujo otros: vecinos ruidosos, cuotas de servicio y incidentes por facturación.
  • Observabilidad como disciplina (métricas, logs, traces) se volvió mainstream; expuso que muchas caídas “legacy” eran en realidad problemas de dependencias y capacidad.

Tres mini-historias de las trincheras corporativas

Mini-historia 1: El incidente causado por una suposición errónea

Una empresa SaaS mediana reescribió su servicio de facturación en un nuevo lenguaje para “hacerlo mantenible”. El equipo hizo un trabajo cuidadoso en tests unitarios y en el contrato API. Construyeron un esquema de base de datos limpio y lo lanzaron detrás de una feature flag.

La suposición errónea fue sutil: asumieron que todos los clientes tratarían POST /charge como no idempotente y nunca reintentarían automáticamente. El sistema legacy había implementado silenciosamente idempotencia usando un token suministrado por el cliente, porque años antes un cliente móvil reintentaba peticiones en redes inestables.

La reescritura no implementó eso. Bajo un evento rutinario de jitter de red entre regiones, un subconjunto de clientes reintentó cargos. El nuevo servicio creó múltiples cargos. Soporte se llenó de tickets. Finanzas intervino. Los ingenieros recibieron páginas por un “incidente de corrección de datos”, que es del tipo que no deja de doler incluso cuando las gráficas se ponen verdes.

La solución no fue “añadir más tests”. La solución fue tratar la idempotencia y los reintentos como requisitos de primera clase, documentarlos como invariantes y construir herramientas de reconciliación. También añadieron un canario que simulaba reintentos y verificaba que el libro mayor permaneciera estable.

La moraleja: si no modelas explícitamente el comportamiento del cliente, la red lo hará por ti.

Mini-historia 2: La optimización que salió mal

Un equipo de plataforma interna de una empresa reescribió una tubería de reporting para reducir costos. Reemplazaron un job de agregación basado en base de datos por una canalización streaming y ajustaron el batching agresivamente para minimizar CPU y almacenamiento.

La optimización lucía bien en benchmarks sintéticos. En producción fue distinto. Su batching aumentó la latencia end-to-end y creó carga en ráfagas en servicios downstream. Una tubería “barata” se convirtió en generadora de thundering herd. El downstream limitó la tasa. Los reintentos se acumularon. Los buffers internos del sistema streaming crecieron, y entonces la lógica de backpressure empezó a dropear mensajes bajo carga sostenida.

Ahora el sistema tenía dos problemas en lugar de uno: los reportes llegaban tarde y algunos estaban equivocados. El equipo pasó semanas construyendo controles compensatorios: colas dead-letter, herramientas de replay y un proceso de corrección de “datos tardíos”. Los costos subieron, no bajaron, porque la sobrecarga operativa también es un costo—pagado en atención humana.

Finalmente aflojaron el batching, aceptaron un coste de cómputo steady-state mayor y pusieron SLOs estrictos alrededor de frescura y corrección. La “optimización” había optimizado lo equivocado: la factura, no el producto.

Mini-historia 3: La práctica aburrida pero correcta que salvó el día

Una compañía que modernizaba un stack de autenticación resistió la tentación de reescribir y hizo algo dolorosamente poco glamuroso: construyó una suite de pruebas de compatibilidad basada en tráfico real de producción. No solo tests unitarios—peticiones grabadas, tokens de borde y modos de fallo representativos.

Desplegaron el nuevo servicio primero como lector shadow. Validaba tokens y calculaba decisiones pero no las aplicaba. Durante semanas comparó sus salidas con las decisiones del sistema legacy y registró discrepancias con suficiente contexto para depurar.

Encontraron una larga cola de raridades: tolerancia a desincronización de relojes, un algoritmo de firma más antiguo aún usado por un cliente y un código de error específico del que dependía un socio. Nada de esto estaba en la especificación. Todo importaba.

Cuando finalmente cortaron el tráfico, el lanzamiento fue casi aburrido. On-call recibió algunas páginas—principalmente por dashboards demasiado sensibles—y luego todo se estabilizó. La “práctica aburrida” fue tratar la migración como un ejercicio de recolección de evidencia, no como un salto heroico.

Qué funciona realmente: patrones que sobreviven al contacto con la realidad

Comienza con invariantes, no con arquitectura

Antes de debatir frameworks, escribe las invariantes. Las cosas que deben mantenerse verdaderas incluso cuando las dependencias fallan:

  • Reglas de idempotencia: qué operaciones se pueden reintentar de forma segura y cómo.
  • Restricciones de corrección de datos: qué “no puede ocurrir” (doble cargo, saldo negativo, registro de auditoría perdido).
  • Presupuestos de latencia y objetivos de disponibilidad: qué tolera el usuario.
  • Requisitos de consistencia: dónde la consistencia eventual es aceptable y dónde no.
  • Requisitos de reversión: qué significa deshacer un deploy, una migración o un backfill.

Usa el patrón strangler fig (y hazlo de verdad)

El patrón strangler fig funciona porque respeta que los sistemas en producción son ecosistemas vivos. No reemplazas el árbol en un día; haces crecer un sistema nuevo alrededor y mueves responsabilidades gradualmente.

En la práctica, esto significa:

  • Poner una capa de enrutamiento (API gateway, reverse proxy o ingress de service mesh) delante del sistema antiguo.
  • Mover un endpoint, un flujo de trabajo o una porción de dominio a la vez.
  • Mantener una reversión rápida: enrutar el tráfico de vuelta de inmediato.
  • Usar lecturas shadow y comparación cuando sea posible.

Prefiere “reemplazar detrás de una interfaz” sobre “reescribir todo”

Cuando un subsistema está realmente podrido, reemplázalo detrás de una interfaz estable. Mantén el contrato. Mantén las métricas. Mantén los runbooks operativos. Cambia los internos. Esto reduce el radio de impacto y evita que los equipos reconstruyan todo el mundo solo para arreglar una pared.

Migración de datos: dual-write o CDC, más reconciliación

Elige tu veneno con cuidado:

  • Dual-write: la aplicación escribe en la tienda vieja y la nueva. Conceptualmente más simple, pero más difícil de hacer correcto bajo fallos parciales.
  • CDC: trata la base de datos antigua como la fuente de la verdad y streamea los cambios a la nueva tienda. A menudo más robusto, pero requiere disciplina en ordenamiento y evolución de esquemas.

Sea cual sea, necesitas reconciliación: jobs periódicos que comparen recuentos, checksums e invariantes entre viejo y nuevo. Sin reconciliación estás operando con sensaciones.

Construye paridad operativa antes que paridad funcional

La paridad operativa es la capacidad de ejecutar, depurar y recuperar. Incluye:

  • Dashboards que muestren saturación, errores, latencia y salud de dependencias.
  • Alertas accionables (páginas por síntomas, no ruido).
  • Runbooks que asuman fallos parciales e incluyan reversión.
  • Pruebas de carga que coincidan con la forma de producción, no solo el volumen.

Una cita que deberías pegar en tu monitor

La esperanza no es una estrategia. — James Cameron

La operación no es cínica; es alérgica al pensamiento mágico. Planifica para los modos de fallo que definitivamente tendrás.

Mantén el sistema antiguo saludable mientras migras

Aquí es donde el liderazgo necesita madurar. Si dejas al legado sin recursos mientras construyes el reemplazo, el legado colapsará y consumirá al equipo de reemplazo. Asigna capacidad explícita para trabajo de fiabilidad del legacy durante la migración. Trátalo como reducción de riesgo, no como “esfuerzo desperdiciado”.

Broma #2: Lo único peor que un sistema frágil es dos sistemas frágiles que discrepan sobre de quién es la culpa.

Tareas prácticas: comandos, salidas, qué significa y qué decides

Estas son las tareas que realmente ejecutas cuando alguien dice “la reescritura solucionará el rendimiento/la fiabilidad”. No discutas. Mide. Cada tarea abajo incluye un comando realista, salida típica, qué significa esa salida y la decisión que tomas a partir de ella.

1) Identificar saturación de CPU vs quejas de latencia

cr0x@server:~$ uptime
 14:22:01 up 37 days,  4:11,  2 users,  load average: 18.42, 17.96, 16.88

Qué significa: Una carga promedio muy por encima del número de CPUs (necesitas saber cores) sugiere contención de CPU o acumulación en la cola de ejecutables, posiblemente también espera I/O según la carga.

Decisión: No empieces una reescritura porque “está lento”. Primero determina si estás limitado por CPU, I/O o bloqueos. Luego: comprueba desglose de CPU y cola de ejecución.

2) Comprobar utilización por CPU e iowait

cr0x@server:~$ mpstat -P ALL 1 3
Linux 6.5.0 (prod-app-01)  02/04/2026  _x86_64_  (16 CPU)

22:22:10     CPU    %usr   %nice    %sys %iowait    %irq   %soft  %steal  %guest  %gnice   %idle
22:22:11     all   62.11    0.00   11.83   18.44    0.00    0.52    0.00    0.00    0.00    7.10
22:22:11       0   71.00    0.00   12.00   10.00    0.00    0.00    0.00    0.00    0.00    7.00

Qué significa: Alto %iowait sugiere que la CPU espera en disco/almacenamiento en red. Alto %usr sugiere carga de cómputo. Aquí es mixto: CPU ocupada y esperando I/O.

Decisión: Investiga latencia de almacenamiento y base de datos antes de reescribir lógica de aplicación. Una reescritura no cambiará tus discos.

3) Comprobar presión de memoria e intercambio

cr0x@server:~$ free -h
               total        used        free      shared  buff/cache   available
Mem:            62Gi        54Gi       1.1Gi       1.8Gi       6.9Gi       4.2Gi
Swap:          8.0Gi       2.7Gi       5.3Gi

Qué significa: El uso de swap es no trivial. Si el sistema está intercambiando activamente, la latencia tail aumentará.

Decisión: Antes de reescribir, arregla el dimensionamiento de memoria, fugas o límites de contenedores. Si no puedes ejecutar el servicio actual sin swap, el nuevo probablemente lo hará también—solo más rápido.

4) Comprobar intercambio activo y fallos mayores

cr0x@server:~$ vmstat 1 5
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
 8  2 2795520 1187328  9120 6852140   0  64  1220  1780 8210 9230 61 11  7 21  0
 7  1 2795584 1169000  9120 6854100   0 128  1100  1650 8030 9012 60 12  8 20  0

Qué significa: so (swap out) indica intercambio activo. wa también es alto, consistente con espera I/O.

Decisión: Trata esto como un incidente operativo, no como una oportunidad de hoja de ruta. Reduce huella de memoria, arregla vecinos ruidosos o escala. Reescribir no curará el swapping.

5) Encontrar los mayores consumidores de CPU

cr0x@server:~$ ps -eo pid,comm,%cpu,%mem --sort=-%cpu | head
  PID COMMAND         %CPU %MEM
 8123 java            345.2 18.4
 9001 redis-server     72.1  3.2
 7442 nginx            38.0  0.8

Qué significa: Un solo proceso consumiendo múltiples cores puede ser esperado, pero confirma que se alinea con el throughput. Si la CPU está alta y el throughput es bajo, estás en un bucle o con contención de locks.

Decisión: Perfila el proceso caliente y revisa contención de hilos. Si el argumento de la reescritura es “el nuevo lenguaje será más rápido”, exige evidencia con flame graphs primero.

6) Identificar rápidamente cuellos de botella en disco

cr0x@server:~$ iostat -xz 1 3
Linux 6.5.0 (prod-db-01)  02/04/2026  _x86_64_  (16 CPU)

Device            r/s     w/s   rMB/s   wMB/s  await  svctm  %util
nvme0n1         220.0   410.0    35.2    88.1  18.40   0.90  92.50

Qué significa: Alto %util y await creciente implica que el dispositivo está saturado o con colas. Bajo svctm con alto await indica profundidad de cola/latencia, no lentitud bruta del dispositivo.

Decisión: Necesitas tuning de consultas, cambios de índices o distribución de I/O. Una reescritura que mantenga los mismos patrones de acceso se topará con el mismo muro.

7) Comprobar capacidad del sistema de archivos y agotamiento de inodos

cr0x@server:~$ df -h
Filesystem      Size  Used Avail Use% Mounted on
/dev/nvme0n1p2  900G  855G   45G  96% /
cr0x@server:~$ df -i
Filesystem       Inodes   IUsed    IFree IUse% Mounted on
/dev/nvme0n1p2  5900000 5892000     8000  100% /

Qué significa: Disco casi lleno es malo; el agotamiento de inodos es más sigiloso y puede romper despliegues, logging y archivos temporales.

Decisión: Detente. Limpia. Añade políticas de retención. Si tu reescritura es “porque los despliegues fallan” y la razón son inodos, no necesitas un nuevo código—necesitas mantenimiento.

8) Verificar errores de red y retransmisiones

cr0x@server:~$ netstat -s | egrep -i 'retrans|listen|listenoverflows|packet receive errors' | head
    12455 segments retransmitted
    37 packet receive errors

Qué significa: Retransmisiones y errores de recepción pueden producir “latencia aleatoria”. Tu app puede ser inocente.

Decisión: Investiga NIC, desajustes de MTU, load balancers sobrecargados o problemas cross-AZ antes de reescribir la capa de servicio.

9) Inspeccionar estados de conexiones TCP (fugas o clientes lentos)

cr0x@server:~$ ss -s
Total: 14021
TCP:   10234 (estab 812, closed 9132, orphaned 5, timewait 7210)

Transport Total     IP        IPv6
RAW       0         0         0
UDP       29        25        4
TCP       1102      1011      91
INET      1131      1036      95
FRAG      0         0         0

Qué significa: Excesivo timewait puede indicar conexiones de corta vida sin keep-alives, o comportamiento de reintento agresivo del cliente.

Decisión: Ajusta la reutilización de conexiones, parámetros del load balancer y comportamiento cliente. Una reescritura no cambiará la física de TCP.

10) Comprobar throttling de contenedores (los límites de CPU de Kubernetes duelen)

cr0x@server:~$ kubectl -n payments top pods | head
NAME                           CPU(cores)   MEMORY(bytes)
payments-api-6d8d6c6b6c-2qz7m  980m         740Mi
payments-api-6d8d6c6b6c-pk9h4  995m         755Mi
cr0x@server:~$ kubectl -n payments describe pod payments-api-6d8d6c6b6c-2qz7m | egrep -i 'Limits|Requests|throttl' -n | head -n 20
118:    Limits:
119:      cpu:     1
120:      memory:  1Gi

Qué significa: Pods al tope del límite de CPU probablemente experimentan throttling, causando picos de latencia que parecen “el nuevo servicio es más lento”.

Decisión: Revisa límites/requests de CPU y políticas HPA. Si reescribes sobre Kubernetes sin entender el throttling, solo habrás movido el problema a YAML.

11) Identificar contención de locks en la base de datos (un punto ciego clásico)

cr0x@server:~$ psql -U postgres -d appdb -c "select pid, wait_event_type, wait_event, state, query from pg_stat_activity where wait_event_type is not null order by pid limit 5;"
 pid  | wait_event_type |   wait_event   | state  |                  query
------+-----------------+----------------+--------+------------------------------------------
 4142 | Lock            | transactionid  | active | UPDATE invoices SET status='paid' ...
 4221 | Lock            | relation       | active | ALTER TABLE ledger ADD COLUMN ...

Qué significa: Las peticiones están bloqueadas por locks. Los problemas de rendimiento pueden deberse a DDL de migración, no a la calidad del código de la aplicación.

Decisión: Programa migraciones pesadas, reduce el alcance de locks, usa técnicas de cambio de esquema online. No reescribas porque “Postgres está lento” mientras estás reteniendo locks.

12) Comprobar queries lentas y escoger los mayores culpables

cr0x@server:~$ psql -U postgres -d appdb -c "select calls, mean_exec_time, rows, left(query,120) as q from pg_stat_statements order by mean_exec_time desc limit 5;"
 calls | mean_exec_time | rows | q
-------+----------------+------+------------------------------------------------------------
   412 |         982.14 |   12 | SELECT * FROM orders WHERE customer_id = $1 ORDER BY created_at DESC LIMIT 50
   201 |         744.33 |    1 | SELECT balance FROM accounts WHERE id = $1 FOR UPDATE

Qué significa: Tienes objetivos concretos: añadir índices, cambiar la forma de la consulta, reducir bloqueos. Esto suele ser más barato que reescribir.

Decisión: Arregla las queries calientes primero. Si aún quieres reescribir, al menos traslada las lecciones de las queries para que el nuevo sistema no las repita.

13) Validar lag de replicación antes de un corte

cr0x@server:~$ mysql -e "SHOW SLAVE STATUS\G" | egrep -i 'Seconds_Behind_Master|Slave_IO_Running|Slave_SQL_Running'
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
Seconds_Behind_Master: 43

Qué significa: El lag de replicación significa que tu “lectura desde el nuevo sistema” podría estar desactualizada. Esto puede romper expectativas de usuarios durante la migración.

Decisión: O aceptas explicitamente la desactualización (y la diseñas) o no cortes lecturas hasta que el lag sea consistentemente bajo.

14) Detectar cambios en tasa de errores durante un canary

cr0x@server:~$ kubectl -n payments logs deploy/payments-api --since=5m | egrep -c " 5[0-9][0-9] "
27

Qué significa: Un conteo de 5xx en aumento tras un deploy es un fallo de canario hasta que se demuestre lo contrario.

Decisión: Revierte rápido y luego depura con traces y comprobaciones de dependencias. No “empujes” porque la hoja de ruta de la reescritura lo exige.

15) Confirmar que hay suficientes descriptores de archivo bajo carga

cr0x@server:~$ ulimit -n
1024
cr0x@server:~$ cat /proc/$(pgrep -n nginx)/limits | egrep -i "open files"
Max open files            1024                 1024                 files

Qué significa: 1024 FDs suele ser demasiado bajo para proxies/servicios con mucho tráfico. Puedes obtener fallos de conexión que parecen “la nueva app es inestable”.

Decisión: Aumenta límites, verifica ajustes del runtime de contenedores y vuelve a probar. Otra vez: arregla lo fundamental antes de rediseñar el universo.

Guía de diagnóstico rápido: qué revisar primero/segundo/tercero

Este es el procedimiento cuando alguien afirma “el sistema legacy es el cuello de botella” o “la reescritura será más rápida”. Puedes realizar esto en menos de una hora en un incidente en vivo (con cuidado) o en un entorno staging con carga parecida a producción.

Primero: ¿es saturación, errores o latencia de dependencias?

  • Comprueba tasas de error (picos 5xx/4xx, timeouts). Si los errores subieron, el rendimiento puede ser síntoma de un fallo parcial.
  • Comprueba saturación: CPU iowait, await de disco, retransmisiones de red, conexiones DB, pools de hilos.
  • Comprueba salud de dependencias: DB, cache, broker de mensajes, APIs externas, DNS, expiración de certificados.

Objetivo: clasificar el problema: bounded por cómputo, I/O, bloqueo, red o fallo de dependencia.

Segundo: Encuentra el bucle ajustado en la ruta de la petición

  • Escoge un endpoint visible por el usuario (mayor tráfico o mayor latencia).
  • Trázalo end-to-end (tracing distribuido si está disponible; si no, correlación de logs con IDs de petición).
  • Mide tiempo en: CPU app, consulta DB, cache, upstream, serialización, reintentos.

Objetivo: identificar dónde se va el tiempo, no dónde parece ir.

Tercero: Valida con un experimento controlado

  • Haz un solo cambio (índice, TTL de cache, tamaño de pool de conexiones, límite de CPU).
  • Canarízalo a una pequeña fracción de tráfico.
  • Compara: percentiles de latencia, tasas de error, métricas de saturación.

Objetivo: decisiones basadas en evidencia. Si la propuesta de reescritura no aguanta este nivel de escrutinio, es un proyecto de moral, no de ingeniería.

Errores comunes: síntomas → causa raíz → solución

“Reescribimos y la latencia empeoró”

Síntomas: p95/p99 de latencia aumentó, CPU bien, dashboards muestran más llamadas de red.

Causa raíz: Descomponiste en servicios sin presupuesto de latencia, convirtiendo llamadas en proceso en cadenas RPC. Construiste un monolito distribuido.

Solución: Colapsa límites chatty, agrupa llamadas, introduce cache local y aplica presupuestos por salto. Prefiere APIs de grano grueso sobre límites microservicio “puros”.

“El cutover funcionó y luego apareció deriva de datos”

Síntomas: Los informes no coinciden, saldos difieren, clientes ven estados inconsistentes días después.

Causa raíz: Dual-write sin semántica exactly-once; falta reconciliación; eventos fuera de orden; reglas inconsistentes de zona horaria/redondeo.

Solución: Implementa jobs de reconciliación y chequeos de invariantes; usa claves de idempotencia; define fuente autoritativa por campo; adopta CDC con garantías de orden cuando sea posible.

“El nuevo sistema es estable, pero on-call es peor”

Síntomas: Más alertas, depuración más difícil, más unknown unknowns.

Causa raíz: Observabilidad y runbooks fueron pospuestos; las alertas se basan en métricas crudas en lugar de señales de impacto al usuario; falta tracing.

Solución: Instrumenta las señales doradas (latencia, tráfico, errores, saturación). Añade tracing. Reescribe alertas para que sean basadas en síntomas y vinculadas a SLOs.

“No podemos entregar porque perseguimos paridad para siempre”

Síntomas: El proyecto de reescritura corre por trimestres/años, el negocio sigue añadiendo funciones al legacy, la reescritura nunca alcanza.

Causa raíz: Mentalidad big-bang; no hay cortes incrementales; el equipo de reescritura aislado de prioridades reales del producto.

Solución: Patrón strangler con slices verticales delgados. Mueve un flujo de trabajo end-to-end. Congela algunas funciones legacy o redirige nuevas funciones solo al camino nuevo.

“Reemplazamos el esquema de la base de datos y todo empeoró”

Síntomas: Queries lentas, contención de locks, ventanas de migración que se expanden, rollbacks riesgosos.

Causa raíz: El rediseño del esquema ignoró patrones de acceso y restricciones operativas; índices faltantes; migraciones no acotadas.

Solución: Empieza con perfilado de queries y estrategia de índices. Usa migraciones online, backfills y constraints por fases. Mantén el esquema antiguo como capa adaptadora cuando sea necesario.

“Reescribimos para mejorar seguridad e introdujimos nuevas brechas”

Síntomas: Falta de rastros de auditoría, controles de autorización más débiles, proliferación de secretos.

Causa raíz: Los controles de seguridad eran implícitos en el legacy y no fueron modelados; el nuevo stack se lanzó sin modelado de amenazas.

Solución: Inventaría las invariantes de seguridad (reglas de authz, logging, retención). Añade checks automáticos en CI. Usa privilegios mínimos y gestión centralizada de secretos desde el día uno.

Listas de comprobación / plan paso a paso

Checklist de decisión: ¿deberías reescribir en absoluto?

  1. ¿Puedes nombrar el cuello de botella? Si no, haz mediciones primero (ver tareas y guía de diagnóstico).
  2. ¿El problema es mantenibilidad del código o comportamiento del sistema? Si los incidentes son mayormente de capacidad/dependencia, reescribir el código no ayudará.
  3. ¿Existe un contrato estable? Si la interfaz es inestable, fíjala antes de mover los internos.
  4. ¿Hay un plan de datos? Si no puedes articular dual-write/CDC, reconciliación y reversión, no estás listo.
  5. ¿Tienes madurez operativa? Dashboards, alertas, tracing, runbooks, despliegues por fases. Si no, construye eso primero.
  6. ¿Puedes disponer de personal para dos sistemas? Si no, haz reemplazo incremental, no reescrituras paralelas.

Un plan de modernización más seguro (funciona incluso con tiempo limitado)

  1. Inventaria las invariantes: idempotencia, reglas de corrección, retención, authz, códigos de error, límites de tasa.
  2. Instrumenta el sistema legacy si está ciego: añade request IDs, histogramas de latencia, taxonomías de error.
  3. Pon una capa de enrutamiento adelante: gateway/proxy que pueda dividir tráfico y revertir al instante.
  4. Escoge una porción vertical: un flujo de trabajo que entregue valor real y ejercite dependencias reales.
  5. Shadow primero: el sistema nuevo calcula respuestas y registra discrepancias, pero no las sirve.
  6. Canary: 1% de tráfico, luego 5%, luego 25%, midiendo SLOs e invariantes.
  7. Corta caminos de lectura con cuidado: las lecturas desactualizadas son visibles para el usuario. Usa presupuestos de consistencia y comportamiento claro.
  8. Corta las escrituras al final: asegúrate de que idempotencia, reintentos y reconciliación estén probados.
  9. Desmantela en fragmentos: elimina endpoints legacy a medida que drenan a cero tráfico; mantiene rutas de acceso de archivo para auditoría.

Checklist de lanzamiento para un componente migrado

  • SLO definido; dashboards muestran señales doradas.
  • Alertas ajustadas; on-call tiene runbooks e instrucciones de reversión.
  • Capacidad probada con una forma de carga similar a producción.
  • Timeouts y reintentos de dependencias configurados (con presupuestos).
  • Idempotencia implementada para operaciones inseguras.
  • Jobs de reconciliación de datos en marcha; proceso de triage para discrepancias definido.
  • Controles de seguridad validados: paridad de authz, logs de auditoría, retención.
  • Game day realizado: fallo de dependencia, DB lenta, deploy parcial, reversión.

Preguntas frecuentes

1) ¿Cuándo está realmente justificada una reescritura?

Cuando el sistema actual no puede evolucionar de forma segura: runtime sin soporte con riesgo de seguridad no parcheable, restricciones de licencia o una arquitectura que bloquea requisitos críticos del negocio. Incluso entonces, prefiere reemplazo incremental detrás de interfaces estables.

2) ¿No es la migración incremental más lenta que reescribir?

La incremental parece más lenta porque es honesta sobre operar dos realidades. Las grandes reescrituras parecen rápidas hasta que llega la integración, los datos y las operaciones—entonces el tiempo explota. La migración incremental gana al entregar valor temprano y reducir riesgo existencial.

3) Tenemos código de pésima calidad. ¿No exige eso una reescritura?

El código malo exige límites, pruebas alrededor de invariantes y visibilidad operativa. A menudo puedes aislar los módulos peores y reemplazarlos detrás de una interfaz. Una reescritura completa reinicia la calidad de código a “desconocida”, lo cual no es automáticamente mejor.

4) ¿Cómo evitamos “dos sistemas para siempre”?

Migrando en porciones que retiren completamente responsabilidades legacy. No construyas un sistema paralelo que duplique todo antes de lanzar. Enruta tráfico, corta una porción y luego borra la porción legacy. La eliminación es un hito.

5) ¿Cuál es el mayor riesgo oculto en las reescrituras?

La deriva semántica: el nuevo sistema se comporta diferente bajo reintentos, fallos parciales y entradas extrañas. Los usuarios no crean tickets por “deriva semántica”. Crean tickets por dinero perdido, datos incorrectos y “tu API es inestable”.

6) ¿La arquitectura de microservicios requiere una reescritura?

No. Puedes extraer servicios de un monolito con el tiempo. El primer paso suele ser crear límites modulares internos y extraer un dominio con propiedad clara y contratos de datos.

7) ¿Cómo manejamos la migración de datos sin tiempo de inactividad?

Usa CDC o dual-write, luego reconcilia. Corta lecturas cuando la desactualización sea aceptable o esté mitigada; corta escrituras al final con idempotencia fuerte. Ten siempre una ruta de reversión y un plan para backfills.

8) ¿Qué debe medir el liderazgo para saber que la migración es saludable?

No puntos de historia. Mide cumplimiento de SLO, tasa de incidentes, frecuencia de reversión, tiempo para detectar/recuperar y progreso de migración en superficie legacy retirada (endpoints/flujos eliminados).

9) ¿Cómo evitamos que los ingenieros «se vuelvan locos» queriendo reescribirlo todo?

Define una porción vertical delgada que llegue a producción y exige que cada expansión incluya un plan de salida para la ruta legacy equivalente. Premia la eliminación y la estabilidad operativa, no la novedad.

Siguientes pasos que puedes entregar este trimestre

Si estás en una reunión donde alguien propone una reescritura como panacea, esto es lo que haces en su lugar—prácticamente, sin drama:

  1. Ejecuta la guía de diagnóstico rápido y publica la clasificación del cuello de botella. Saca el debate del ámbito estético.
  2. Escribe las invariantes (idempotencia, corrección, presupuestos de latencia, reglas de authz). Hazlas revisables y testeables.
  3. Escoge un flujo de trabajo y migralo usando enrutamiento + canary + reversión. Demuestra que puedes mover slices con seguridad.
  4. Invierte en paridad operativa: dashboards, tracing, higiene de alertas, runbooks. Haz que sea más fácil ejecutar sistemas que discutirlos.
  5. Convierte la reconciliación de datos en una característica de producto, no en una misión secundaria. Si no puedes probar la corrección de datos, no tienes corrección.

La mentira de reescribir desde cero sobrevive porque ofrece una historia donde la complejidad desaparece. En sistemas reales, la complejidad no desaparece; se mueve. Tu trabajo es moverla a lugares donde sea medible, controlable y aburrida. Lo aburrido está subestimado. Lo aburrido entrega.

← Anterior
Panel de control de NVIDIA desaparecido: recupéralo sin conjeturas
Siguiente →
WSL2 + Kubernetes: la configuración que no derrite tu portátil

Deja un comentario