High Performance MySQL [Cap. 1: Arquitectura e historia de MySQL]

High Performance MySQL

Lo esencial
  • MySQL tiene tres capas: la gestión de conexiones, un servidor que analiza y planifica, y debajo, motores de almacenamiento intercambiables.
  • El optimizador nunca toca el disco. Pide filas al motor a través de una API mínima, una fila cada vez.
  • La concurrencia es un equilibrio entre la granularidad de los bloqueos y el coste de gestionarlos. InnoDB paga por los bloqueos de fila y MVCC para que los lectores casi nunca esperen.
  • Los niveles de aislamiento deciden qué anomalías aceptas. MySQL usa REPEATABLE READ por defecto, no READ COMMITTED.

Tres capas y una frontera que importa

Imagina un servidor MySQL como una pila. Arriba está la fontanería que necesita cualquier servicio de red: aceptar conexiones, autenticarlas y asignar un hilo a cada una. En el medio está lo que la gente quiere decir cuando habla de "MySQL": el parser, el optimizador, las funciones integradas y todo lo que funciona igual sin importar cómo se guarden los datos, como las vistas, los triggers y las rutinas almacenadas.

Abajo están los motores de almacenamiento. Guardan y recuperan filas, nada más; no analizan SQL. El servidor se comunica con ellos mediante la API de handler, un vocabulario reducido de llamadas como "posiciónate en esta clave", "dame la siguiente fila" o "inserta esta fila". El motor se elige por tabla, no por servidor, y por eso una misma base de datos puede, en teoría, mezclar motores.

appmysql CLIQueryCrackclientesGestión de conexiones · autenticación · un hilo por conexióncapa SQLparseroptimizadorejecutorhandler API: cada Handler_read_* es una llamada que cruza esta líneamotores de almacenamiento (uno por tabla)MyISAMMEMORYInnoDB
El servidor analiza y planifica; el motor de almacenamiento solo guarda y lee filas. QueryCrack cuenta las llamadas que cruzan la handler API.

Esa API de handler es exactamente lo que mide QueryCrack. Cada contador Handler_read_* corresponde a una de esas llamadas del servidor a InnoDB.

Conexiones y privilegios

Cada conexión de cliente la atiende un hilo dentro del proceso del servidor. Crear hilos tiene un coste, así que servidores y drivers los reutilizan: una caché de hilos en el lado del servidor y pools de conexiones en el lado del cliente. Los pools también funcionan como limitador, porque acotan cuántos clientes pueden atacar la base de datos a la vez.

La autenticación (usuario, host, contraseña, certificado TLS) solo deja entrar a una conexión. Lo que pueda hacer después es una decisión aparte que toma el sistema de privilegios, con un detalle que llega hasta tablas y columnas concretas. Dar a un usuario acceso de solo lectura a un único esquema es un uso cotidiano de ese sistema.

Del texto SQL a un plan

Una consulta se analiza y se convierte en un árbol, se valida y luego pasa al optimizador, que puede reescribirla, reordenar los joins y elegir índices. El optimizador no sabe cómo organiza el motor los bytes; en su lugar, le pide estadísticas y estimaciones de coste, y planifica a partir de ellas. EXPLAIN muestra el resultado, y los hints del optimizador te permiten imponerte cuando sabes algo que él no sabe.

Las ediciones anteriores del libro describen una caché de consultas que devolvía resultados guardados para textos SELECT idénticos. Escalaba mal con escrituras y se eliminó en MySQL 8.0, así que cachear resultados ahora es responsabilidad de la aplicación.

Bloqueos y granularidad

Cualquier recurso compartido con lectores y escritores concurrentes necesita reglas. La pareja clásica es el bloqueo compartido (de lectura), que muchos lectores pueden tener a la vez, y el bloqueo exclusivo (de escritura), que bloquea a todos los demás. La corrección es fácil si lo bloqueas todo; el rendimiento viene de bloquear lo menos posible.

GRANULARIDADCONTRAPARTIDA
Bloqueos de tablaBaratos de gestionar, pero un solo escritor detiene a todos los lectores de la tabla. Los toma la capa del servidor y todavía los usan muchas operaciones ALTER TABLE.
Bloqueos de filaMáxima concurrencia, a cambio de gestionar muchos bloqueos. Los implementa el motor (InnoDB), no el servidor.

Los bloqueos de fila de InnoDB son más sutiles que "una fila por sentencia". Para evitar que aparezcan filas dentro de un rango que otra transacción ya leyó, también bloquea los huecos entre entradas del índice (gap locks y next-key locks). Por eso dos inserciones en huecos vecinos pueden esperarse mutuamente y, de vez en cuando, provocar un deadlock.

Transacciones y ACID

Una transacción agrupa sentencias para que tengan éxito o fallen juntas. Mover stock de la fila de un almacén a otra son dos UPDATE; sin una transacción, una caída entre ambos hace desaparecer inventario. ACID nombra las garantías que lo hacen seguro:

  • Atomicidad: se aplican todas las sentencias o ninguna.
  • Consistencia: la base de datos pasa de un estado válido a otro.
  • Aislamiento: los cambios en curso quedan ocultos a otras transacciones en un grado configurable.
  • Durabilidad: una vez confirmado, un cambio sobrevive a una caída. Lo fuerte que sea esa promesa (volcar a disco en cada commit, replicar antes de confirmar) es un coste que eliges tú.

Los deadlocks son parte normal de trabajar con transacciones. InnoDB detecta los ciclos y revierte la transacción más barata de deshacer; las aplicaciones deben estar preparadas para reintentar.

Niveles de aislamiento

NIVELQUÉ PUEDES VER
READ UNCOMMITTEDLos cambios sin confirmar de otras transacciones ("lecturas sucias"). Rara vez compensa.
READ COMMITTEDSolo datos confirmados, pero la misma consulta puede devolver filas distintas dos veces dentro de una transacción. Es el nivel por defecto en PostgreSQL.
REPEATABLE READUna instantánea estable durante toda la transacción. Es el nivel por defecto de MySQL; los gap locks de InnoDB también bloquean la mayoría de las filas fantasma.
SERIALIZABLELas lecturas simples toman bloqueos compartidos, así que las transacciones se comportan como si se ejecutaran una tras otra. Es correcto, y propenso a esperas por bloqueos.

MVCC: lectores que no bloquean

Con solo bloqueos de fila, los lectores seguirían esperando a los escritores. InnoDB evita la mayor parte de eso con el control de concurrencia multiversión: un SELECT simple lee de una instantánea consistente en lugar de tomar bloqueos. Cada versión de fila registra la transacción que la escribió, y las versiones anteriores se reconstruyen a partir del undo log cuando una instantánea las necesita. Así, dos transacciones pueden ver contenidos distintos de la misma tabla en el mismo instante, y ambas tener razón.

El precio es almacenamiento y limpieza: el historial de undo debe conservarse hasta que ninguna instantánea lo necesite, y por eso una transacción abierta durante horas vuelve el servidor más lento para todos. MVCC se aplica a READ COMMITTED y REPEATABLE READ; los otros dos niveles no usan instantáneas de la misma forma.

Motores de almacenamiento: InnoDB y los demás

InnoDB es el motor por defecto y casi siempre la respuesta correcta: transaccional, resistente a caídas, con bloqueo por fila y copias de seguridad en caliente. Guarda cada tabla dentro del B-Tree de su clave primaria (un índice agrupado), así que las búsquedas por clave primaria son rápidas y cada índice secundario lleva una copia de la clave primaria. Mantén las claves primarias pequeñas.

MyISAM, la alternativa histórica, tiene bloqueos a nivel de tabla, no tiene transacciones y se recupera mal de las caídas. Sus antiguos argumentos de venta, la búsqueda full-text y las tablas comprimidas de solo lectura, ahora también los cubre InnoDB. Al elegir un motor, las preguntas son las de siempre: ¿necesitas transacciones, copias de seguridad en caliente y una recuperación rápida tras una caída? Si la respuesta es sí, es InnoDB. Mezclar motores en una misma base de datos es posible, pero invita a casos límite extraños.

Cómo se refleja en QueryCrack

Cada reto se ejecuta sobre InnoDB mediante una conexión de un usuario con privilegios mínimos: SELECT sobre un esquema y nada más. Ese privilegio basta para inspeccionar el esquema, así que Ejecutar también acepta SHOW INDEX, DESCRIBE y EXPLAIN. El evaluador lee los contadores handler antes y después de tu consulta, así que lo que optimizas es el número de llamadas que la capa del servidor hace al motor de almacenamiento. Una clave primaria agrupada hace que algunas de esas llamadas sean muy baratas, y ese es todo el truco detrás de count_em_bugs.

Preguntas frecuentes

¿El optimizador forma parte de InnoDB?

No. Vive en la capa del servidor y funciona con cualquier motor. Al motor solo le pide estadísticas y filas.

¿Por qué una transacción abierta e inactiva perjudica el rendimiento?

Su instantánea retiene versiones antiguas de filas en el undo log. El purge no puede descartarlas, el historial crece y las lecturas tienen que recorrer cadenas de versiones más largas.

¿Debería cambiar el nivel de aislamiento para ganar velocidad?

Solo sabiendo lo que haces. READ COMMITTED reduce los gap locks y puede recortar las esperas por bloqueos, pero cambia lo que tus consultas pueden ver. Mide primero y revisa el código que depende de lecturas repetibles.

Practica esto

Diez millones de filas, ningún índice en la columna por la que filtras y una clave primaria agrupada que lo ordena todo en silencio. Encuentra el camino barato.

Fuente: Schwartz, Zaitsev y Tkachenko, High Performance MySQL, capítulo 1: MySQL Architecture and History, con notas actualizadas para MySQL 8. Son nuestras notas, no un sustituto del libro. La selección de capítulos sigue la serie de lectura de Sayed Alesawy.