El checkout es una ruta de escritura. No es un cuadro de búsqueda. No es una navegación de catálogo amigable con la caché. Es un cúmulo de inserts, updates, constraints y la paranoia de “¿realmente les cobramos dos veces?”—todo llegando en pequeñas ráfagas con la forma de campañas de marketing.
Cuando las escrituras en ecommerce se torcen, rara vez es sutil. Picos de latencia, retardos en la replicación, sobreventa de inventario y de pronto tu plan de “alta disponibilidad” es un chat grupal y una oración. La pregunta no es “MariaDB o PostgreSQL, ¿cuál es más rápido?”. La pregunta es: ¿cuál se ahoga primero bajo tu patrón de escrituras específico, y qué haces antes de que ocurra en un sábado.
La incómoda verdad: ambos pueden ahogarse
MariaDB y PostgreSQL son bases de datos serias. Ambas pueden ofrecer un rendimiento aterrador y latencias sorprendentemente civilizadas. También las dos pueden desmoronarse de maneras muy predecibles cuando tratas las escrituras de ecommerce como una estampida: reservas de inventario, máquinas de estado de pagos, cotizaciones de envío, uso de cupones y “auditar todo para siempre”.
Lo que difiere es cómo fallan, cuánto aviso recibes y qué tan caro es eliminar ese modo de fallo.
- PostgreSQL suele estar limitado por la dinámica de WAL y checkpoints, deuda de vacuum y riesgo de wraparound de transaction ID si ignoras el mantenimiento. Cuando está infeliz, verás presión de IO, bloat y colas de bloqueo que parecen “la base de datos está viva pero nada se mueve”.
- MariaDB (InnoDB) suele estar limitado por la presión de redo/binlog, dinámica de lag en la replicación, contención de mutex/bloqueos alrededor de filas calientes y, ocasionalmente, ajustes “los afinamos para ser rápidos” que silenciosamente eliminan tus garantías de recuperación. Cuando está infeliz, verás replicación rezagada, picos de deadlocks y commits que empiezan a hacer cola como una mala fila de seguridad en un aeropuerto.
Elige según tu patrón de escrituras, tu madurez operativa y tu disposición a imponer disciplina en el diseño del esquema y de las transacciones. Si no puedes imponer disciplina, escoge el sistema cuyo modo de fallo puedas detectar y mitigar más rápido. El orgullo no es una estrategia de monitoreo.
Patrones de escritura que rompen un ecommerce
La mayoría de las plataformas de ecommerce no mueren por “demasiadas consultas”. Mueren por unos pocos patrones de escritura específicos que escalan de forma no lineal.
1) Filas calientes: inventario, cupones y contadores
Si un SKU se vuelve viral, una sola fila de inventario se convierte en un recurso disputado. Si tienes “usos de cupón restantes” en una fila, se vuelve disputada. Si rastreas “orders_today” como un contador único, te creaste un DDoS interno.
Ambas bases de datos pueden manejar contención, pero ninguna puede desafiar la física: una sola fila solo puede actualizarse tantas veces por segundo antes de serializar todo tu checkout. La solución rara vez es “más CPU”. Normalmente es cambiar el modelo de datos o aislar el camino caliente.
2) Transacciones largas durante flujos de checkout
El checkout es un flujo de trabajo. Los flujos de trabajo invitan a transacciones largas: reservar inventario, llamar al proveedor de pago, calcular impuestos, hablar con la API de envío, escribir filas de auditoría y luego confirmar. Es una excelente forma de mantener bloqueos mientras esperas al internet.
Si tu transacción abarca llamadas a la red, básicamente estás manteniendo tu base de datos como rehén hasta que suene el pager de otro proveedor.
3) Índices secundarios por todas partes
Cada índice es una escritura. Cada índice también necesita mantenimiento. En tablas con muchas escrituras (orders, order_items, payments, inventory_events), el entusiasmo por índices es cómo compras latencia y amplificación de IO al precio completo.
4) Auditoría “append-only” intensa sin particionado
Las tablas de auditoría son los nuevos logs. Excepto que los logs rotan y las tablas de auditoría a menudo no. Las tablas append-only con mucho tráfico sin particionado invitan a bloat, índices gigantes, borrados lentos y estrés de vacuum (Postgres) o presión de purge (InnoDB).
5) Replicación y durabilidad convertidas en un truco de rendimiento
Eventualmente alguien dice: “Podemos hacerlo más rápido si relajamos la durabilidad.” No están equivocados; están apostando con el estado del negocio. Puedes relajar la durabilidad de forma segura en algunos lugares. El checkout no es uno de ellos.
Broma #1: Desactivar la durabilidad para acelerar el checkout es como quitar los cinturones de seguridad porque llegas tarde: tu ETA mejora hasta que deja de hacerlo.
Cómo suele ahogarse PostgreSQL primero
La mecánica de escritura de Postgres en un párrafo (la versión que necesitas a las 2 a. m.)
Postgres usa MVCC con versiones de tupla: las actualizaciones crean nuevas versiones de fila, las versiones antiguas permanecen hasta que el vacuum las recupera. Cada transacción escribe WAL para durabilidad y replicación. Los checkpoints vacían páginas sucias. Si autovacuum se queda atrás, obtienes bloat y más IO. Si los checkpoints son demasiado agresivos o WAL está limitado, obtienes picos de IO y latencia en commits. Si ignoras el mantenimiento de transaction ID, puedes acercarte al wraparound y verte obligado a un vacuum de emergencia en el peor momento.
Modo de fallo A: presión de WAL y checkpoints → picos de latencia
Bajo carga intensa de escritura, el flujo de WAL se convierte en el latido cardiaco. Si WAL no puede escribirse lo suficientemente rápido (disco lento, IOPS saturados, mal comportamiento de fsync), los commits hacen cola. Entonces llegan los checkpoints y empiezan a volcar muchos buffers sucios, compitiendo con las escrituras en primer plano. El síntoma normalmente es picos de latencia p99 y utilización de IO al máximo, no necesariamente CPU.
Operativamente, Postgres tiende a “delatarse” a través de métricas: frecuencia de checkpoints, buffers escritos por backends, tiempos de escritura WAL. Si las miras.
Modo de fallo B: deuda de vacuum → bloat → todo se vuelve más lento
MVCC es genial hasta que olvidas limpiarlo. Si tus tablas con muchas escrituras se actualizan constantemente y autovacuum no puede seguir el ritmo, obtienes bloat. Bloat significa índices más grandes, heap más grande, más fallos de caché, más IO. La base de datos sigue haciendo el mismo trabajo lógico, pero ahora arrastra una maleta más grande.
En ecommerce, los generadores habituales de bloat son: actualizaciones de estado de pedidos, actualizaciones de estado de inventario, reintentos de pagos y cualquier patrón de upsert que se convierta en una tormenta de actualizaciones.
Modo de fallo C: colas de bloqueo que parecen una base de datos lenta
Postgres es bueno con bloqueo a nivel de fila, pero transacciones largas, DDL en producción y patrones mal concebidos de “SELECT … FOR UPDATE” pueden causar cadenas de bloqueo. Escucharás: “Postgres está lento hoy.” No está lento; está bloqueado.
Modo de fallo D: demasiadas conexiones (y la ilusión de escalar)
Postgres usa un modelo de procesos; demasiadas conexiones crean sobrecarga de memoria y cambio de contexto. Un sistema con muchas escrituras y 2.000 conexiones activas puede parecer un problema de CPU pero suele ser un problema de gestión de conexiones. La solución es aburrida: pooling, timeouts sensatos y backpressure.
Cómo suele ahogarse MariaDB primero
Mecánica de escritura de InnoDB en un párrafo (la versión “¿por qué están atascados los commits?”)
InnoDB usa redo logs y un buffer pool. Las escrituras van a memoria y redo; las páginas se vacían después. Los binlogs registran cambios para replicación. Un commit puede implicar volcar redo y/o binlog según los ajustes de durabilidad y el comportamiento de group commit. Bajo carga, el sistema suele estar limitado por el ancho de banda de escritura del redo log, el comportamiento de flush o la aplicación de cambios en la replicación.
Modo de fallo A: el lag de replicación se convierte en la verdadera caída
Muchos setups de ecommerce dependen de réplicas para lecturas. Bajo carga de escritura, las réplicas pueden quedar atrás. Entonces tu aplicación lee estado obsoleto: el inventario parece disponible cuando no lo está, el estado del pago parece “pendiente” cuando está “capturado” y los clientes reciben emails de confirmación duplicados. La base de datos puede estar sana; la arquitectura no.
El lag de replicación en MariaDB se amplifica a menudo por: transacciones grandes, apply de un solo hilo (dependiendo de la configuración) y diseños de esquema que fuerzan contención en la réplica al reproducir las escrituras.
Modo de fallo B: contención en filas calientes y deadlocks
InnoDB suele ser excelente, pero las filas calientes (inventario, contadores, filas de cupones) crean contención de bloqueo. Con suficiente concurrencia, aparecen deadlocks. Los deadlocks no son intrínsecamente malos: InnoDB los detecta y revierte una transacción, pero deadlocks repetidos pueden convertirse en un techo de rendimiento y en una estampida de reintentos.
Modo de fallo C: ajustes de fsync y flush que son rápidos hasta que te caes
MariaDB facilita intercambiar durabilidad por rendimiento (y la gente lo hace). Ajustes como flush relajado de redo o sync_binlog reducen los fsyncs e incrementan el throughput. Luego un nodo se cae y descubres que tu “riesgo pequeño” es un carrito lleno de pedidos perdidos.
Modo de fallo D: amplificación de escritura por índices secundarios y splits de página
Tablas con muchas escrituras y múltiples índices secundarios pueden causar mucho IO aleatorio. Si la clave primaria está mal elegida (p. ej., UUID aleatorio sin estrategia de ordenación), puedes amplificar los splits de página y desgastar el buffer pool. Esto no es exclusivo de MariaDB, pero el índice agrupado de InnoDB hace que la elección de la PK sea especialmente relevante.
Guía de decisión: qué elegir para un checkout con muchas escrituras
Si tu patrón de escritura es “muchas actualizaciones a las mismas filas”
Este es el problema de filas calientes. Ambos sistemas sufren. Tu mejor movimiento es la cirugía del modelo de datos:
- Usar eventos append-only para movimientos de inventario y calcular disponibilidad con una vista derivada (o una proyección cacheada) en lugar de actualizar constantemente una fila.
- Shardear contadores (por minuto/por cubeta) en lugar de un contador global único.
- Usar claves de idempotencia para que los reintentos no multipliquen las escrituras.
La elección de base de datos ayuda menos que el diseño del esquema y de las transacciones aquí.
Si tu patrón de escritura es “append-only, alto volumen”
Postgres suele ser más fácil de mantener estable si inviertes en particionado y en tunear autovacuum; ofrece introspección fuerte. MariaDB también puede comportarse muy bien, pero debes vigilar la presión de redo/binlog y la velocidad de apply de réplicas como un halcón.
Si dependes fuertemente de réplicas de lectura para lecturas relacionadas con la corrección
Sé cuidadoso con la replicación asíncrona de MariaDB a menos que la diseñes para ello. La replicación asíncrona de Postgres también se retrasa, pero los equipos de Postgres suelen combinarla con patrones más claros: read-your-writes en la primaria para flujos críticos y un pooler con lógica de enrutamiento. De cualquier modo: si una lectura afecta a una decisión de escritura (inventario restante, validez de cupón), lee desde una fuente con la consistencia adecuada.
Si quieres una línea: ¿quién se ahoga primero?
- PostgreSQL tiende a ahogarse primero por IO y deuda de mantenimiento: throughput de WAL, picos de flush en checkpoints, vacuum quedando atrás, sobrecarga de conexiones.
- MariaDB tiende a ahogarse primero por dinámicas de commit/replicación y contención: comportamiento de flush de redo/binlog, lag de réplicas, contención en filas calientes, tormentas de deadlocks.
No es un veredicto; es una advertencia. Puedes prevenir ambos. Pero no puedes prevenir lo que no mides.
Datos interesantes y contexto histórico (útiles, no trivia)
- La genealogía MVCC de PostgreSQL remonta a trabajos académicos de los años 80; su diseño multiversión es la razón por la que las lecturas no bloquean las escrituras de la manera clásica.
- InnoDB no fue originalmente “el motor de MySQL”; se convirtió en el predeterminado más tarde porque solucionó la era de “ups, bloqueos a nivel de tabla” que los primeros usuarios de MySQL recuerdan bien.
- MariaDB se creó como un fork por preocupaciones sobre la gobernanza de MySQL; muchos comportamientos operativos permanecen con forma MySQL, incluidas convenciones de replicación.
- La replicación de Postgres maduró más tarde que la replicación estilo binlog de MySQL; la replicación por streaming moderna es robusta, pero el ecosistema sigue promoviendo mejores prácticas como el pooling de conexiones con más insistencia.
- Las actualizaciones en Postgres no son in-place; cada actualización crea una nueva tupla. Eso es una característica y también la razón por la que el bloat es algo que debes presupuestar en sistemas con muchas escrituras.
- La PK agrupada de InnoDB significa que los datos de la tabla se organizan físicamente por la clave primaria. La elección de la PK puede influir fuertemente en la amplificación de escritura y los splits de página.
- Postgres tiene “HOT updates” (heap-only tuple updates) que pueden evitar actualizaciones de índice en algunos casos, lo que puede reducir significativamente IO de escritura para ciertos patrones.
- Ambos sistemas pueden usar “async commit” y otros compromisos de durabilidad. La trampa es tratar esos controles como funciones de rendimiento en lugar de multiplicadores de riesgo.
- Las transacciones grandes perjudican la replicación desproporcionadamente en ambos ecosistemas: retrasan la visibilidad en réplicas y hacen que la recuperación/rewind tras un crash sea más dolorosa.
Guion de diagnóstico rápido
Cuando la latencia de escritura del checkout se dispara, tienes minutos para decidir: ¿es CPU, IO, bloqueos, replicación o acumulación de conexiones en el lado de la aplicación? Aquí está el orden que encuentra el cuello de botella más rápido en la práctica.
Primero: confirma que el síntoma es real y su alcance
- ¿Subió la latencia de commit p95/p99, o solo un subconjunto de endpoints?
- ¿Son todas las escrituras, o solo una tabla (inventario/pedidos/pagos)?
- ¿Está afectada la primaria o solo las réplicas?
Segundo: revisa bloqueos y esperas, no solo “queries lentas”
Los problemas de escritura suelen ser colas de bloqueo disfrazadas de lentitud. Encuentra las sesiones bloqueadas, encuentra al bloqueador y decide si matar una transacción o arreglar el patrón.
Tercero: revisa saturación de IO y comportamiento de fsync
Si el almacenamiento está al máximo, la base de datos no puede confirmar rápido. Revisa utilización del dispositivo, tiempos de espera (await) y tasas de escritura WAL/binlog. Si estás en almacenamiento en red, asúmelo culpable hasta demostrar lo contrario.
Cuarto: salud de la replicación y lag
El lag no es solo un problema de reporting; puede romper la corrección si lees desde réplicas. Si el lag sube durante el incidente, deja de enrutar lecturas críticas a réplicas de inmediato.
Quinto: deuda de mantenimiento
En Postgres: autovacuum, bloat, churn de checkpoints. En MariaDB: purge lag, history list length, presión del buffer pool. El mantenimiento es donde “estaba bien ayer” va a morir.
Idea parafraseada de Werner Vogels (reliability/ops): “Todo falla; diseña para poder recuperarte rápido.” Esa es la postura que quieres durante incidentes de escritura: la contención rápida vence al diagnóstico perfecto.
Tareas prácticas: comandos, salidas y decisiones
Estas son tareas operativas reales que puedes ejecutar durante un incidente o una revisión de rendimiento. Cada una incluye: comando, qué significa la salida y qué decidir después. Los comandos asumen Linux y acceso local al host de BD o a un jump host con los clientes adecuados.
Tarea 1: ¿El servidor está limitado por IO ahora mismo?
cr0x@server:~$ iostat -x 1 5
Linux 6.1.0 (db-primary) 12/29/2025 _x86_64_ (16 CPU)
avg-cpu: %user %nice %system %iowait %steal %idle
12.44 0.00 6.21 22.35 0.00 59.00
Device r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await svctm %util
nvme0n1 120.0 950.0 4800.0 76000.0 150.2 9.80 10.3 0.7 78.0
dm-0 118.0 940.0 4700.0 75500.0 150.6 14.10 15.5 0.8 92.0
Significado: Alto %iowait y %util del dispositivo cerca de saturación con await elevado sugiere que IO es el limitador; los commits harán cola.
Decisión: Reduce la presión de escritura (feature flags, límites de tasa), asegúrate de que los dispositivos de WAL/binlog no compitan con los de datos y considera aumentar ajustes de checkpoint/WAL (Postgres) o el comportamiento de group commit de redo/binlog (MariaDB) después del incidente.
Tarea 2: ¿Qué proceso está causando daño de IO?
cr0x@server:~$ pidstat -d 1 5
Linux 6.1.0 (db-primary) 12/29/2025 _x86_64_ (16 CPU)
# Time UID PID kB_rd/s kB_wr/s kB_ccwr/s Command
12:01:01 PM 999 21450 50.00 68000.00 0.00 postgres
12:01:01 PM 999 21990 0.00 4200.00 0.00 postgres
12:01:01 PM 999 18210 0.00 39000.00 0.00 mariadbd
Significado: Puedes ver si el daemon de BD está escribiendo intensamente. Si ves múltiples daemons escribiendo, podrías estar en el host equivocado o ejecutando ambos.
Decisión: Si las escrituras están dominadas por la BD, entra en la BD para ver bloqueos/WAL. Si no, encuentra el escritor real (backup, logrotate, job descontrolado).
Tarea 3 (Postgres): ¿Estamos esperando por bloqueos o por IO?
cr0x@server:~$ psql -X -c "SELECT wait_event_type, wait_event, count(*) FROM pg_stat_activity WHERE state <> 'idle' GROUP BY 1,2 ORDER BY 3 DESC;"
wait_event_type | wait_event | count
----------------+---------------------+-------
Lock | transactionid | 23
IO | WALWrite | 7
LWLock | BufferMapping | 4
| | 2
Significado: Muchas sesiones esperando en Lock/transactionid indica contención o transacciones largas. IO/WALWrite apunta a un cuello de botella en escritura WAL.
Decisión: Si predominan esperas por bloqueo, identifica bloqueadores y acorta transacciones. Si WALWrite domina, enfócate en storage/ajustes WAL y comportamiento de checkpoints.
Tarea 4 (Postgres): ¿Quién bloquea a quién?
cr0x@server:~$ psql -X -c "SELECT a.pid AS blocked_pid, a.usename, a.query AS blocked_query, pg_blocking_pids(a.pid) AS blocking_pids FROM pg_stat_activity a WHERE cardinality(pg_blocking_pids(a.pid)) > 0;"
blocked_pid | usename | blocked_query | blocking_pids
------------+---------+--------------------------------------+--------------
18722 | app | UPDATE inventory SET reserved = ... | {18410}
18740 | app | UPDATE inventory SET reserved = ... | {18410}
Significado: Un PID bloqueador que mantiene un lock está deteniendo muchas escrituras de checkout—clásico fila caliente o transacción larga.
Decisión: Inspecciona la sesión bloqueadora; si es seguro, cancélala. Luego corrige el patrón de la aplicación (reducir alcance del lock, evitar llamadas de red dentro de la transacción, rediseñar inventario).
Tarea 5 (Postgres): ¿Están los checkpoints causando thrashing?
cr0x@server:~$ psql -X -c "SELECT checkpoints_timed, checkpoints_req, buffers_checkpoint, buffers_backend, checkpoint_write_time, checkpoint_sync_time FROM pg_stat_bgwriter;"
checkpoints_timed | checkpoints_req | buffers_checkpoint | buffers_backend | checkpoint_write_time | checkpoint_sync_time
------------------+-----------------+-------------------+-----------------+-----------------------+----------------------
122 | 480 | 9823412 | 2210344 | 8931201 | 1203310
Significado: Un checkpoints_req alto relativo a los timed sugiere que la presión de WAL está forzando checkpoints. Alto buffers_backend sugiere que los backends están haciendo su propio flushing—a menudo malo para la latencia.
Decisión: Tras contener, ajusta max_wal_size, checkpoint_timeout y checkpoint_completion_target, y valida el throughput de almacenamiento para WAL y datos.
Tarea 6 (Postgres): ¿Autovacuum está al día en tablas calientes?
cr0x@server:~$ psql -X -c "SELECT relname, n_dead_tup, n_live_tup, last_autovacuum FROM pg_stat_user_tables ORDER BY n_dead_tup DESC LIMIT 10;"
relname | n_dead_tup | n_live_tup | last_autovacuum
--------------+------------+------------+--------------------------
orders | 18422012 | 22100410 | 2025-12-29 11:02:14+00
payments | 9221011 | 8012200 | 2025-12-29 10:41:02+00
inventory | 5120099 | 210440 | 2025-12-29 07:12:55+00
Significado: Muchas tuplas muertas en relación a las vivas sugiere que el vacuum está atrasado o que tu patrón de actualización es agresivo. Que inventory tenga más muertas que vivas es una señal clara.
Decisión: Ajusta umbrales de autovacuum por tabla, reduce churn de actualizaciones o muévete a eventos append-only. Considera particionar tablas con alta rotación.
Tarea 7 (Postgres): ¿Estamos ahogados en conexiones?
cr0x@server:~$ psql -X -c "SELECT state, count(*) FROM pg_stat_activity GROUP BY 1 ORDER BY 2 DESC;"
state | count
---------------------+-------
idle | 820
active | 160
idle in transaction | 44
Significado: Muchos idle puede estar bien, pero idle in transaction es peligroso: mantiene snapshots/locks y bloquea el vacuum.
Decisión: Impone timeouts en la aplicación, mata sesiones “idle in transaction” y usa un pooler. También corrige los caminos de código que dejan transacciones abiertas.
Tarea 8 (MariaDB): ¿Estamos bloqueados o deadlockeando?
cr0x@server:~$ mysql -e "SHOW ENGINE INNODB STATUS\G" | sed -n '1,120p'
...
LATEST DETECTED DEADLOCK
------------------------
*** (1) TRANSACTION:
TRANSACTION 98234123, ACTIVE 0 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 5 lock struct(s), heap size 1136, 3 row lock(s)
MySQL thread id 18220, OS thread handle 140201, query id 921103 app updating
UPDATE inventory SET reserved=reserved+1 WHERE sku_id=771 AND warehouse_id=3;
*** (2) TRANSACTION:
TRANSACTION 98234124, ACTIVE 0 sec fetching rows
...
Significado: Tienes deadlocks reales en filas calientes (inventory). Los deadlocks causarán reintentos; los reintentos amplifican la carga.
Decisión: Reduce la concurrencia para el SKU caliente (servicio de cola/reservación), asegúrate de un orden de locks consistente en todo el código y añade idempotencia para reducir tormentas de reintentos.
Tarea 9 (MariaDB): ¿El lag de replicación es el verdadero incendio?
cr0x@server:~$ mysql -e "SHOW SLAVE STATUS\G" | egrep "Seconds_Behind_Master|Slave_IO_Running|Slave_SQL_Running|Last_SQL_Error"
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
Seconds_Behind_Master: 148
Last_SQL_Error:
Significado: La réplica está 148 segundos detrás. Las lecturas desde ella están obsoletas; si la app la usa para checks de inventario/cupones, venderás fantasmas.
Decisión: Enruta lecturas críticas a la primaria; degrada lecturas no críticas a caché; investiga throughput de apply y transacciones largas en la primaria.
Tarea 10 (MariaDB): ¿Confirmamos de forma segura o “rápido y suelto”?
cr0x@server:~$ mysql -e "SHOW VARIABLES WHERE Variable_name IN ('innodb_flush_log_at_trx_commit','sync_binlog');"
+------------------------------+-------+
| Variable_name | Value |
+------------------------------+-------+
| innodb_flush_log_at_trx_commit | 2 |
| sync_binlog | 0 |
+------------------------------+-------+
Significado: innodb_flush_log_at_trx_commit=2 y sync_binlog=0 son impulsores clásicos de throughput que reducen garantías de durabilidad.
Decisión: Para checkout, muévete hacia 1/1 (o un compromiso conscientemente elegido). Si no puedes permitir eso, necesitas cambios arquitectónicos (batching, eventing o distinto almacenamiento) en lugar de pretender que la física negociará.
Tarea 11 (Postgres): ¿La generación de WAL está explotando?
cr0x@server:~$ psql -X -c "SELECT now() as ts, pg_current_wal_lsn();"
ts | pg_current_wal_lsn
-----------------------------+--------------------
2025-12-29 12:05:13.112+00 | 3A/9F2C1B80
Significado: Una muestra no es suficiente; compara LSNs a lo largo del tiempo. Si salta rápido, estás generando WAL rápidamente—a menudo por actualizaciones, churn de índices o un job masivo.
Decisión: Identifica las tablas que generan más WAL (vía estadísticas/herramientas de extensiones), pausa escrituras en bulk o reduce mantenimiento de índices (elimina índices no usados tras validación).
Tarea 12 (Linux): ¿El sistema de archivos miente sobre la latencia de flush?
cr0x@server:~$ sudo blktrace -d /dev/nvme0n1 -w 3 -o - | blkparse -i -
8,0 1 1 0.000000000 21450 Q WS 0 + 8 [postgres]
8,0 1 2 0.000210000 21450 G WS 0 + 8 [postgres]
8,0 1 3 0.004900000 21450 C WS 0 + 8 [0]
Significado: Estás viendo completaciones de sincronización de escritura; si los timestamps de completación (C) se alargan, la latencia de flush del storage está dañando los commits.
Decisión: Valida clase de almacenamiento, firmware, políticas RAID/cache. Para discos cloud, considera IOPS aprovisionadas o NVMe local para WAL/redo. Si no puedes mejorar el storage, reduce la frecuencia de fsync solo donde la corrección lo permita (no en checkout).
Tarea 13 (Postgres): Encuentra ofensores “idle in transaction” con texto de consulta
cr0x@server:~$ psql -X -c "SELECT pid, usename, now()-xact_start AS xact_age, state, left(query,120) AS q FROM pg_stat_activity WHERE state='idle in transaction' ORDER BY xact_age DESC LIMIT 10;"
pid | usename | xact_age | state | q
-------+---------+-------------+----------------------+------------------------------------------------------------
19221 | app | 00:12:14.22 | idle in transaction | SELECT * FROM carts WHERE cart_id=$1 FOR UPDATE;
Significado: Alguien inició una transacción, tomó un lock y quedó idle. Esto bloquea vacuum y puede bloquear escritores.
Decisión: Mata la sesión durante el incidente. Luego corrige el código de la app: alcance de transacciones, timeouts y evita lecturas con bloqueo a menos que realmente las necesites.
Tarea 14 (MariaDB): Revisa history list length de InnoDB y salud del purge
cr0x@server:~$ mysql -e "SHOW ENGINE INNODB STATUS\G" | egrep -i "History list length|Log sequence number|Last checkpoint"
History list length 923441
Log sequence number 1123341234412
Last checkpoint at 1123340031129
Significado: Un history list length grande puede indicar lag de purge, a menudo debido a transacciones largas. Eso incrementa retención de undo y puede degradar rendimiento.
Decisión: Encuentra y elimina transacciones de larga duración, especialmente en réplicas o sesiones de analítica. Considera separar OLTP de reporting.
Tarea 15 (Ambos): ¿Estamos haciendo transacciones enormes por accidente?
cr0x@server:~$ sudo journalctl -u checkout-worker -n 50 --no-pager
Dec 29 12:06:01 app-1 checkout-worker[3112]: bulk_reservation job started: cart_id=... items=420
Dec 29 12:06:02 app-1 checkout-worker[3112]: db transaction open for cart_id=...
Dec 29 12:06:55 app-1 checkout-worker[3112]: db transaction commit cart_id=... duration_ms=52981
Significado: Una transacción de 52 segundos no es “un poco lenta”. Es una fábrica de locks/undo/WAL. Dañará tanto a Postgres como a MariaDB.
Decisión: Fragmenta el trabajo, mueve llamadas externas fuera de las transacciones y diseña pasos idempotentes para poder confirmar con frecuencia.
Tres microhistorias corporativas desde las trincheras de la ruta de escritura
Microhistoria 1: El incidente causado por una suposición errónea
Manejaban un marketplace de ecommerce con picos estacionales. El equipo migró tráfico de lectura a réplicas y dijo al negocio que la primaria “solo manejaría escrituras”. Sonaba limpio: primaria para escrituras, réplicas para lecturas, todos felices.
La suposición errónea fue sutil: asumieron que “las lecturas son seguras en cualquier lado”. Pero su flujo de checkout leía disponibilidad de inventario y validez de cupones desde la réplica para reducir carga. Con lag normal—segundos—funcionaba mayormente. Durante una venta flash, el lag de replicación subió. La réplica mostraba inventario aún disponible. Los clientes siguieron comprando. La primaria rechazaba correctamente algunas actualizaciones, pero la aplicación ya había prometido disponibilidad y aplicado descuentos basados en lecturas obsoletas.
Soporte se inundó con “¿por qué me cobraron si dijeron que estaba en stock?” Ingeniería miró gráficos. La primaria no estaba caída. La CPU estaba bien. La réplica estaba “verde” salvo por una métrica que nadie pagó: el lag.
La solución no fue un tuning heroico. Cambiaron la política: cualquier lectura que influya en una decisión de escritura (inventario, canje de cupón, estado de pago) debe ser read-your-writes consistente. En la práctica: leer desde la primaria o desde un almacén con consistencia por quórum, y cachear solo después del commit. También añadieron un corte duro: si el lag de réplica excede un umbral, la app deja de usar réplicas para esos endpoints.
La lección del postmortem fue directa: las lecturas obsoletas son un bug de corrección, no una característica de rendimiento. Las réplicas son para escalar, no para fingir que el tiempo es opcional.
Microhistoria 2: La optimización que salió mal
Un minorista con MariaDB tenía un p99 terrible en el checkout. Alguien encontró los sospechosos habituales: demasiados fsyncs, latencia de commit y almacenamiento que estaba justo pero no genial. “Optimizaron” relajando ajustes de durabilidad. Los commits se hicieron más rápidos. Todos se felicitaron y volvieron a sacar features.
Dos meses después, un kernel panic tumbó la primaria. El failover ocurrió. El sitio siguió en pie. Y entonces empezó lo extraño: un puñado de pedidos existían en logs de la aplicación pero no en la base; algunos pagos fueron capturados pero la máquina de estado de pedidos tenía pasos faltantes; soporte tuvo que reconciliar manualmente el estado usando reportes del proveedor de pagos.
No hay nada como descubrir que construiste un pequeño sistema contable donde “eventualmente consistente” significa “eventualmente alguien llora”.
Revirtieron la durabilidad para el esquema de checkout y movieron el trabajo de rendimiento a capas más seguras: menos índices en tablas calientes, transacciones más pequeñas y cola explícita para reservas de inventario. La ganancia real vino de reducir contención y amplificación de escritura—no de pretender que los discos nunca fallan.
Microhistoria 3: La práctica aburrida pero correcta que salvó el día
Una compañía de suscripciones usaba Postgres. No eran sofisticados. Eran disciplinados. Cada trimestre hacían un “simulacro de ruta de escritura”: simular un pico, verificar alertas y ensayar el playbook. También tenían una regla de apariencia aburrida: nada de DDL en horarios pico, y cada cambio de esquema debía estar planificado con un plan reversible.
Una tarde, una campaña de marketing funcionó mejor de lo esperado. Las escrituras aumentaron. La latencia subió pero se mantuvo acotada. Luego una réplica empezó a rezagarse mucho. El on-call no debatió filosofía; siguió la checklist: enrutar lecturas críticas a la primaria, dejar caer features de escritura no esenciales (eventos de wishlist, historial de navegación) y vigilar WAL/checkpoints.
Encontraron el verdadero problema: un job en background había empezado a actualizar una gran fracción de una tabla caliente, creando deuda de vacuum. Porque ellos controlaban tuplas muertas y comportamiento de autovacuum, fue obvio. Pausearon el job, dejaron que autovacuum se recuperara y luego reintrodujeron el job con batching y prioridad baja.
El incidente nunca llegó a los clientes. No porque Postgres sea mágico. Porque alguien hizo el trabajo aburrido: visibilidad, simulacros y cambios controlados.
Errores comunes: síntoma → causa raíz → solución
1) Síntoma: “La base de datos está lenta” pero la CPU está baja
Causa raíz: colas de bloqueo o esperas de IO. La base de datos no está calculando; está esperando.
Solución: En Postgres, inspecciona los wait events en pg_stat_activity y los PIDs bloqueadores; mata al bloqueador si es necesario. En MariaDB, revisa InnoDB status por esperas de locks/deadlocks. Luego acorta transacciones y reduce actualizaciones de filas calientes.
2) Síntoma: p99 se dispara cada pocos minutos
Causa raíz: comportamiento de checkpoints/flush periódicos (checkpoints en Postgres, flush storms en MariaDB), o jobs de background periódicos.
Solución: Esparce IO con tuning de checkpoints (Postgres) y asegura separación WAL/datos donde sea posible. Para MariaDB, verifica tamaño de redo log y ajustes de durabilidad, y audita jobs en background por actualizaciones en ráfaga.
3) Síntoma: réplicas están “up” pero clientes ven estado inconsistente
Causa raíz: lag de replicación y la app lee desde réplicas para lógica relacionada con corrección.
Solución: Fuerza read-your-writes para checkout e inventario. Añade enrutamiento consciente del lag. Añade idempotencia para que reintentos no dupliquen escrituras.
4) Síntoma: picos de deadlocks durante promociones
Causa raíz: filas calientes + orden de locks inconsistente + reintentos agresivos.
Solución: Estandariza orden de locks, reduce alcance de transacciones, mueve la contención a una cola/allocator e implementa backoff exponencial con jitter en reintentos.
5) Síntoma: uso de disco de Postgres sube, consultas más lentas semana tras semana
Causa raíz: bloat por actualizaciones; autovacuum atrasado; índices creciendo.
Solución: Ajusta autovacuum por tabla, reduce churn de actualizaciones, considera particionado y ajustes de fillfactor. Si el bloat es severo, planifica una reescritura controlada (vacuum full / reindex / rebuild) con downtime o estrategia online.
6) Síntoma: commits en MariaDB lentos, pero lecturas bien
Causa raíz: presión de flush redo/binlog, latencia de fsync o group commit de binlog que no da abasto.
Solución: Valida rendimiento de fsync del almacenamiento. Asegúrate de no compartir redo/binlog con vecinos ruidosos. Mantén transacciones pequeñas. Si la replicación está habilitada, verifica que los ajustes de binlog coincidan con los requisitos de durabilidad.
7) Síntoma: autovacuum de Postgres corre “todo el tiempo” y aún no alcanza
Causa raíz: amplificación de escritura por índices excesivos y actualizaciones frecuentes; memoria de mantenimiento demasiado pequeña; transacciones largas que impiden limpieza.
Solución: Elimina índices no usados, divide jobs de actualización, corrige transacciones largas y ajusta workers/costs de autovacuum. También revisa por “idle in transaction”.
Broma #2: Si tu solución es “aumentar max_connections”, básicamente estás resolviendo un embotellamiento añadiendo más coches.
Listas de verificación / plan paso a paso
Paso a paso: hacer que las escrituras de ecommerce sean aburridas (la meta)
- Define los límites de corrección. Identifica lecturas que influyen decisiones de compra (inventario, canje de cupones, estado de pago). Obliga que sean consistentes con las escrituras.
- Acorta transacciones. No hagas llamadas de red dentro de transacciones DB. Si debes coordinar, usa claves de idempotencia y una máquina de estado con commits frecuentes.
- Elimina la vanidad de índices. Para tablas con muchas escrituras, conserva solo índices que justifiquen su coste. Valida con estadísticas de consultas, no por opiniones.
- Particiona los monstruos append-only. Orders/events/audit deben particionarse por tiempo o por tenant si crecen rápido y se consultan por rangos recientes.
- Haz raras las filas calientes. Sustituye contadores globales por contadores por cubeta. Sustituye “actualización fila de inventario por artículo” por asignación de reserva o event sourcing más proyección.
- Backpressure en lugar de backlog. Limita la tasa de escrituras de checkout en el borde cuando la BD esté estresada. Un “intenta de nuevo” controlado supera una falla en cascada.
- Instrumenta la ruta de escritura. Mide latencia de commit, tiempo en espera por locks, lag de replicación y profundidad de colas en la capa de aplicación.
- Diseña reintentos como parte del sistema. Los reintentos deben ser idempotentes y con jitter, o te crearás tu propia caída.
- Separa OLTP de analítica. Consultas de reporting y jobs de “exportar todo” no deberían compartir presupuesto de locks e IO con el checkout.
- Practica failover y respuesta a lag. No quieras inventar tu respuesta al lag durante una promoción.
Qué haría yo hoy si tuviera que elegir para un core ecommerce con muchas escrituras
- Si tienes madurez operativa y quieres introspección profunda y semánticas transaccionales robustas: PostgreSQL, con atención seria a WAL/checkpoints y vacuum desde el día uno.
- Si tu organización ya es nativa MySQL/MariaDB y puedes imponer disciplina transaccional e higiene en la replicación: MariaDB, pero trata el lag de replicación como un SLO de primera clase y no “optimices” la durabilidad en escrituras que mueven dinero.
Preguntas frecuentes
1) ¿PostgreSQL siempre es mejor para cargas con muchas escrituras?
No. Postgres suele ser más fácil de razonar cuando lo instrumentas bien, pero puede caer por límites de WAL/IO, deuda de vacuum o sobrecarga de conexiones. El éxito en cargas de escritura depende más de la forma de las transacciones y la capacidad de IO.
2) ¿MariaDB siempre es más rápido en inserts?
A veces, especialmente con esquemas simples y los compromisos de durabilidad adecuados. Pero las escrituras de ecommerce no son solo inserts—son inserts más índices más constraints más replicación más actualizaciones de filas calientes. “Inserts más rápidos” no es lo mismo que “checkout estable bajo contención”.
3) ¿Cuál es la razón principal por la que las escrituras del checkout en Postgres se vuelven lentas?
En producción: presión de IO alrededor de WAL/checkpoints combinada con bloat o autovacuum quedando atrás. Cadenas de bloqueo son un cercano segundo, usualmente causadas por transacciones largas o lecturas con bloqueo mal diseñadas.
4) ¿Cuál es la razón principal por la que las escrituras del checkout en MariaDB se vuelven lentas?
Presión de commit/flush más contención. Luego el lag de replicación convierte tu arquitectura en una fábrica de bugs de corrección si lees desde réplicas para decisiones.
5) ¿Debería usar claves primarias UUID para pedidos?
Puedes, pero entiende el coste de escritura. Los UUID aleatorios pueden aumentar splits de página y reducir localidad (especialmente doloroso con almacenamiento agrupado como InnoDB). Si necesitas IDs globalmente únicos, considera variantes UUID ordenadas por tiempo o claves sustitutas separadas cuando proceda.
6) ¿Puedo “resolver” filas calientes con SELECT FOR UPDATE?
Puedes serializar correctamente, pero también puedes serializar el throughput. El bloqueo es corrección, no rendimiento. La solución escalable es reducir la contención: asignar inventario por lotes, shardar por almacén/cubeta o usar un servicio de reservas que controle la concurrencia.
7) ¿Necesito pooling de conexiones?
Para Postgres: casi siempre sí, a escala. Para MariaDB: sigue siendo beneficioso, pero el umbral de dolor suele ser diferente. De cualquier modo, el crecimiento descontrolado de conexiones es como convertir la latencia en una novela de misterio.
8) ¿La replicación asíncrona es aceptable para ecommerce?
Sí, si la tratas como asíncrona. Eso significa: no leas desde réplicas para decisiones que afectan dinero o inventario a menos que toleres estaleness y tengas controles compensatorios. Muchos equipos dicen que lo toleran; sus colas de soporte discrepan.
9) ¿Cuál es la mejora de rendimiento más segura para checkout con muchas escrituras?
Reduce la amplificación de escritura: menos índices en tablas calientes, transacciones más pequeñas y mover escrituras no esenciales fuera del camino crítico (colas de eventos). Las mejoras de almacenamiento ayudan también, pero no arreglan patrones de escritura malos.
10) ¿Qué base de datos ofrece mejores herramientas para diagnosticar cuellos de botella de escritura?
Postgres generalmente tiene introspección incorporada más potente alrededor de esperas, WAL y comportamiento de vacuum. MariaDB también tiene buenas herramientas, pero a menudo dependerás más de snapshots de InnoDB status y métricas de replicación. De cualquier modo, la herramienta solo importa si alertas sobre las cosas correctas.
Siguientes pasos que puedes ejecutar
Si hoy ejecutas un ecommerce con muchas escrituras y quieres menos sorpresas, haz esto en orden:
- Mapea los límites de tus transacciones de checkout y elimina llamadas externas dentro de ellas. Haz el flujo idempotente.
- Identifica filas calientes (inventario, cupones, contadores) y rediseñalas para evitar contención de fila única.
- Audita índices en las 5 tablas con más escrituras. Elimina lo que no sea necesario para lecturas críticas.
- Instrumenta los cuellos de botella: eventos de espera/WAL/checkpoints/vacuum en Postgres; deadlocks/redo/binlog/lag de replicación en MariaDB.
- Implementa enrutamiento consciente del lag para que las réplicas nunca corrompan silenciosamente la lógica de negocio.
- Corre una prueba de carga que parezca vida real: espigada, sesgada a unos pocos SKUs calientes, con reintentos y timeouts. A la base de datos no le importa tu QPS promedio.
Después de eso, la elección entre MariaDB y PostgreSQL se vuelve más clara. No porque uno sea “mejor”, sino porque finalmente sabrás qué tipo de dolor compras—y cómo evitar que llegue al checkout.