GuíasGuides · Nivel 3Level 3

MySQL y MariaDB: dónde está la configuración, el buffer pool de InnoDB, el registro de consultas lentas, un índice, la réplica y el error Too many connectionsMySQL and MariaDB: where the configuration lives, the InnoDB buffer pool, the slow query log, an index, the replica and the Too many connections error

Qué fichero manda en my.cnf y conf.d, cuánta memoria dar al buffer pool y cómo comprobarla, max_connections, el registro de consultas lentas leído con mysqldumpslow o pt-query-digest, EXPLAIN y un índice, el estado de la réplica en MySQL 8.4 y en MariaDB, y la aplicación que recibe Too many connections.Which file wins in my.cnf and conf.d, how much memory to give the buffer pool and how to check it, max_connections, the slow query log read with mysqldumpslow or pt-query-digest, EXPLAIN and an index, replica status in MySQL 8.4 and MariaDB, and the application that gets Too many connections.

Qué vas a conseguir

Llevar el día a día de un servidor MySQL o MariaDB en Linux más allá de las copias: saber qué fichero de configuración se está aplicando de verdad, dar al buffer pool de InnoDB la memoria que necesita sin que el kernel mate el proceso, encontrar las consultas que hacen ir lenta la aplicación y arreglar la peor con un índice, leer el estado de la réplica y resolver el ticket clásico: la aplicación da "Too many connections".

Antes de empezar

  • MySQL 8.4 LTS, o MariaDB 10.11 u 11.4 LTS, en Ubuntu, Debian o RHEL, con sudo en el servidor.
  • Acceso como root del motor por el socket local (sudo mysql o sudo mariadb): en las instalaciones de los paquetes de la distribución, root@localhost entra por auth_socket / unix_socket sin contraseña.
  • Las cuentas, la copia programada y la restauración probada están en la guía de MySQL, MariaDB y PostgreSQL: permisos mínimos y copias; el bloqueo por una transacción abierta y el espacio en disco, en la de PostgreSQL, MySQL y SQL Server. Esta guía no los repite.
  • Una ventana de cambios para lo que exige reiniciar el servicio y para crear índices en tablas grandes.

1. Dónde está la configuración (y cuál manda)

El servidor lee varios ficheros en orden y el último valor leído gana. Dónde están depende del motor y de la distribución:

InstalaciónFichero principalCarpetas incluidasServicio
MySQL en Ubuntu / Debian/etc/mysql/my.cnf/etc/mysql/conf.d/, /etc/mysql/mysql.conf.d/ (mysqld.cnf)mysql
MariaDB en Ubuntu / Debian/etc/mysql/my.cnf/etc/mysql/conf.d/, /etc/mysql/mariadb.conf.d/ (50-server.cnf)mariadb
MySQL en RHEL/etc/my.cnf/etc/my.cnf.d/mysqld
MariaDB en RHEL/etc/my.cnf/etc/my.cnf.d/ (mariadb-server.cnf)mariadb
bash · srv-db-01
mysqld --verbose --help 2>/dev/null | grep -A1 "Default options"Default options are read from the following files in the given order:/etc/my.cnf /etc/mysql/my.cnf ~/.my.cnfmy_print_defaults mysqld--user=mysql--bind-address=10.20.40.11--innodb_buffer_pool_size=8G--max_connections=300--innodb_buffer_pool_size=12G
  • my_print_defaults mysqld junta todas las líneas de la sección [mysqld] de todos los ficheros. Si un parámetro sale dos veces, se aplica el último: aquí 12G, no 8G. Es el origen del "lo cambié y no hace nada".
  • Pon tus cambios en un fichero propio, /etc/mysql/mysql.conf.d/zz-contoso.cnf o /etc/my.cnf.d/zz-contoso.cnf: se lee al final y una actualización del paquete no lo pisa. En MariaDB, el binario es mariadbd y las secciones [mariadbd] y [mariadb] también valen.
  • En MySQL 8, performance_schema.variables_info dice de dónde sale cada valor: SELECT VARIABLE_NAME, VARIABLE_SOURCE, VARIABLE_PATH FROM performance_schema.variables_info WHERE VARIABLE_NAME='max_connections'; devuelve GLOBAL + ruta del fichero, PERSISTED (un SET PERSIST, guardado en mysqld-auto.cnf dentro del directorio de datos) o DYNAMIC (un SET GLOBAL que se perderá al reiniciar). MariaDB no tiene esta tabla ni SET PERSIST.

Para lo que es dinámico, SET GLOBAL aplica el cambio en caliente y el fichero lo mantiene tras el reinicio: haz siempre las dos cosas.

2. El buffer pool de InnoDB y la memoria

El buffer pool es la caché de datos e índices de InnoDB, y el parámetro que más influye en el rendimiento. El valor por defecto (128 MB) es para un portátil, no para un servidor.

  • En un servidor dedicado a la base de datos, entre el 50 % y el 75 % de la RAM. Con 16 GB, unos 10-12 GB. El resto es para el sistema, las conexiones y la caché de ficheros.
  • Si comparte servidor con la aplicación, bastante menos, y se mide.
mysql · srv-db-01
SELECT ROUND(@@innodb_buffer_pool_size/1024/1024/1024,1) AS pool_gb, @@max_connections AS max_conn;+---------+----------+| pool_gb | max_conn |+---------+----------+|    12.0 |      300 |+---------+----------+SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read%s';+----------------------------------+-------------+| Variable_name                    | Value       |+----------------------------------+-------------+| Innodb_buffer_pool_read_requests | 98411250117 || Innodb_buffer_pool_reads         | 1822304     |+----------------------------------+-------------+SET GLOBAL innodb_buffer_pool_size = 12*1024*1024*1024;
  • Innodb_buffer_pool_reads son las lecturas que no estaban en memoria y fueron a disco. Frente a read_requests, aquí menos del 0,01 %: el pool es suficiente. Si ese porcentaje sube y el disco va al 100 % de uso en iostat -x, falta memoria.
  • El tamaño se cambia en caliente (MySQL 5.7+ y MariaDB 10.2+) y se ajusta a un múltiplo de innodb_buffer_pool_chunk_size. SHOW STATUS LIKE 'Innodb_buffer_pool_resize_status' dice cuándo ha terminado. Pon el mismo valor en el fichero.

Lo que ve el sistema:

bash · srv-db-01
ps -o pid,rss,cmd -C mysqld    PID   RSS CMD   1184 13921408 /usr/sbin/mysqldfree -g               total        used        free      shared  buff/cache   availableMem:              15          13           0           0           1           1journalctl -k --since "-7d" | grep -i "out of memory"nov 01 03:14:22 srv-db-01 kernel: Out of memory: Killed process 1184 (mysqld) total-vm:16123400kB, anon-rss:14810112kB

Un mysqld muerto por el OOM killer del kernel aparece como un reinicio sin motivo en el log de errores (/var/log/mysql/error.log en Debian y Ubuntu, /var/log/mysqld.log en RHEL; MariaDB suele escribir en el journal: journalctl -u mariadb). La causa casi nunca es solo el buffer pool: es el pool más cada conexión con sus búferes (sort_buffer_size, join_buffer_size, read_buffer_size, tablas temporales hasta tmp_table_size). Con 300 conexiones activas a la vez, unos pocos MB por conexión son varios GB. Por eso max_connections y la memoria se miran juntos.

3. max_connections

  • Por defecto, 151 en los dos motores. Es dinámico: SET GLOBAL max_connections = 300; y el valor en el fichero.
  • MySQL y MariaDB guardan una conexión más para una cuenta con CONNECTION_ADMIN o SUPER (en MariaDB 10.5+, CONNECTION ADMIN): es la que te deja entrar como root cuando la aplicación ya ha llenado todas. Si la aplicación usa una cuenta con esos privilegios, también se come esa.
  • En MySQL 8.4 puedes definir además una interfaz de administración (admin_address y admin_port, por defecto 33062) que no cuenta para el límite; requiere reinicio para activarla.
  • El límite real lo pone también el sistema: cada conexión usa descriptores de fichero. Si el log de arranque dice Changed limits: max_open_files o max_connections, el servicio de systemd necesita LimitNOFILE más alto (un override con sudo systemctl edit mysql).
mysql · srv-db-01
SHOW GLOBAL STATUS WHERE Variable_name IN ('Threads_connected','Threads_running','Max_used_connections','Connection_errors_max_connections');+-----------------------------------+-------+| Variable_name                     | Value |+-----------------------------------+-------+| Connection_errors_max_connections | 0     || Max_used_connections              | 142   || Threads_connected                 | 61    || Threads_running                   | 3     |+-----------------------------------+-------+

Max_used_connections es el máximo desde el último arranque y Connection_errors_max_connections, cuántas conexiones se rechazaron por el límite. Llévalos a la monitorización: si el máximo se acerca al límite, el ticket del paso 7 está de camino.

4. El registro de consultas lentas

Se activa en caliente, sin reinicio:

mysql · srv-db-01
SET GLOBAL slow_query_log_file = '/var/log/mysql/mysql-slow.log';SET GLOBAL long_query_time = 1;SET GLOBAL slow_query_log = ON;
/etc/mysql/mysql.conf.d/zz-contoso.cnf
[mysqld]innodb_buffer_pool_size = 12Gmax_connections         = 300slow_query_log          = ONslow_query_log_file     = /var/log/mysql/mysql-slow.loglong_query_time         = 1
  • long_query_time está en segundos (admite decimales, 0.5) y por defecto es 10: casi nada llega a 10 segundos, por eso el registro suele estar vacío aunque esté activo. Ojo: el SET GLOBAL vale para las conexiones nuevas; las del pool de la aplicación siguen con el valor anterior hasta que se reconectan.
  • En MariaDB 10.11 y posteriores los nombres nuevos son log_slow_query, log_slow_query_file y log_slow_query_time; los antiguos siguen funcionando como alias.
  • El fichero crece rápido con un umbral bajo: entra en logrotate (Debian y Ubuntu ya traen /etc/logrotate.d/mysql-server o mariadb). No actives log_queries_not_using_indexes en producción sin un límite: registra todo lo que hace un recorrido completo, aunque tarde 1 ms.

Leerlo a mano no sirve: hay miles de entradas casi iguales. Dos herramientas las agrupan por consulta, con los valores sustituidos por N o ?:

bash · srv-db-01
sudo mysqldumpslow -s t -t 3 /var/log/mysql/mysql-slow.logReading mysql slow query log from /var/log/mysql/mysql-slow.logCount: 1840  Time=2.41s (4434s)  Lock=0.00s (0s)  Rows=12.0 (22080), crm_app[crm_app]@[10.20.30.21]  SELECT * FROM pedidos WHERE cliente_id = N ORDER BY fecha DESC LIMIT NCount: 96  Time=8.12s (779s)  Lock=0.00s (0s)  Rows=1.0 (96), informes[informes]@[10.20.30.40]  SELECT COUNT(*) FROM pedidos WHERE estado = 'S' AND fecha >= 'S'sudo pt-query-digest /var/log/mysql/mysql-slow.log --limit 3 | sed -n '/Profile/,/^$/p'# Profile# Rank Query ID                     Response time   Calls R/Call V/M# ==== ============================ =============== ===== ====== =====#    1 0x6A1E3F0C2D9B47A1E0F43B2C... 4434.4 71.2%   1840 2.4100  0.31 SELECT pedidos#    2 0x91C47D2B0E3A6F1C55D2A0B8... 779.5 12.5%     96 8.1200  1.02 SELECT pedidos
  • -s t ordena por tiempo total: la consulta de 2,4 segundos que se ejecuta 1.840 veces pesa más que la de 8 segundos que corre 96. Arreglar la primera es lo que nota el usuario.
  • En MariaDB, la herramienta es mariadb-dumpslow (con mysqldumpslow como enlace en muchas distribuciones). pt-query-digest viene en el paquete percona-toolkit y sirve para los dos motores.

5. EXPLAIN y un índice

mysql · srv-db-01 · base de datos crm
EXPLAIN SELECT * FROM pedidos WHERE cliente_id = 4187 ORDER BY fecha DESC LIMIT 20\G           id: 1  select_type: SIMPLE        table: pedidos         type: ALLpossible_keys: NULL          key: NULL         rows: 1843221     filtered: 10.00        Extra: Using where; Using filesort

Se lee de arriba abajo:

  • type: ALL y key: NULL: recorre la tabla entera (1,8 millones de filas) para devolver 20. Es lo que hay que quitar.
  • Using filesort: ordena en memoria o en disco después de leer. Un índice que empiece por la columna del WHERE y siga por la del ORDER BY resuelve las dos cosas.
mysql · srv-db-01 · base de datos crm
SHOW INDEX FROM pedidos;ALTER TABLE pedidos ADD INDEX idx_pedidos_cliente_fecha (cliente_id, fecha), ALGORITHM=INPLACE, LOCK=NONE;Query OK, 0 rows affected (41.88 sec)EXPLAIN SELECT * FROM pedidos WHERE cliente_id = 4187 ORDER BY fecha DESC LIMIT 20\G         type: refpossible_keys: idx_pedidos_cliente_fecha          key: idx_pedidos_cliente_fecha         rows: 37        Extra: Backward index scan
  • ALGORITHM=INPLACE, LOCK=NONE crea el índice sin bloquear las escrituras (DDL en línea de InnoDB). Si el motor no puede hacerlo así, da un error en lugar de bloquear la tabla en silencio: mejor que lo diga.
  • Aun así consume disco y E/S y, si hay réplica, la réplica lo aplica después y acumula retraso mientras tanto. En tablas grandes, en la ventana de cambios.
  • EXPLAIN ANALYZE (MySQL 8) ejecuta la consulta y da tiempos reales; en MariaDB el equivalente es ANALYZE SELECT .... Los dos ejecutan la consulta: nunca con un UPDATE o DELETE en producción.
  • Comprueba el efecto el día siguiente con mysqldumpslow: la consulta tiene que haber desaparecido del primer puesto. Y no crees índices "por si acaso": cada uno ralentiza las escrituras.

6. El estado de la réplica

El retraso y los dos hilos en Yes están en la guía de permisos mínimos y copias. Lo que conviene tener claro aquí es qué comando y qué nombres usa cada motor:

MySQL 8.4MariaDB 10.11 / 11.x
Estado de la réplicaSHOW REPLICA STATUS (SHOW SLAVE STATUS ya no existe en 8.4)SHOW REPLICA STATUS o SHOW SLAVE STATUS (son sinónimos)
Campos de los hilosReplica_IO_Running, Replica_SQL_RunningSlave_IO_Running, Slave_SQL_Running
RetrasoSeconds_Behind_SourceSeconds_Behind_Master
Posición en el origenSHOW BINARY LOG STATUSSHOW MASTER STATUS
Arrancar y pararSTART REPLICA / STOP REPLICASTART REPLICA o START SLAVE

En MariaDB los campos siguen con los nombres antiguos aunque el comando sea SHOW REPLICA STATUS: un script de monitorización que busque Seconds_Behind_Source en MariaDB no encuentra nada y parece que todo va bien.

mysql · réplica srv-db-01b (MySQL 8.4)
SHOW REPLICA STATUS\G             Source_Host: 10.20.40.11      Replica_IO_Running: Yes     Replica_SQL_Running: No   Seconds_Behind_Source: NULL          Last_SQL_Errno: 1062          Last_SQL_Error: Coordinator stopped because there were error(s) in the worker(s). ... Duplicate entry '88123' for key 'pedidos.PRIMARY'       Executed_Gtid_Set: 3e11fa47-71ca-11e1-9e33-c80aa9429562:1-4421907
  • Un hilo SQL parado por un duplicado casi siempre significa que alguien escribió en la réplica. La réplica debe estar con read_only = ON y super_read_only = ON (MySQL); en MariaDB, read_only = ON, teniendo en cuenta que quien tenga el privilegio READ_ONLY ADMIN (incluido en SUPER hasta MariaDB 11.0) sigue pudiendo escribir.
  • No saltes el error a ciegas (sql_replica_skip_counter o una transacción vacía con GTID): puedes dejar la réplica con datos distintos sin que nada avise. Anota la fila, compara origen y réplica y, si hay dudas, se reconstruye la réplica desde una copia nueva. Esa decisión, con el responsable de la aplicación.
  • Antes de conmutar o de sacar copias de la réplica, los dos hilos en Yes y el retraso en 0.

7. El ticket: "la aplicación da 'Too many connections'"

Lunes, 9:10. El CRM no carga para nadie. En el log de la aplicación (Java) se repite: Data source rejected establishment of connection, message from server: "Too many connections". Desde un cliente:

bash · srv-app-01
mysql -h srv-db-01.contoso.local -u crm_app -p crmERROR 1040 (08004): Too many connections
  1. Entra igualmente, como root por el socket local: usa la conexión reservada del paso 3.
mysql · srv-db-01
sudo mysqlSHOW GLOBAL STATUS LIKE 'Threads_connected';+-------------------+-------+| Threads_connected | 301   |+-------------------+-------+SELECT user, SUBSTRING_INDEX(host,':',1) AS cliente, command, COUNT(*) AS n, MAX(time) AS max_s FROM performance_schema.processlist GROUP BY 1,2,3 ORDER BY n DESC LIMIT 5;+-----------+-------------+---------+-----+-------+| user      | cliente     | command | n   | max_s |+-----------+-------------+---------+-----+-------+| crm_app   | 10.20.30.22 | Sleep   | 212 | 26840 || crm_app   | 10.20.30.21 | Sleep   |  61 |   410 || crm_app   | 10.20.30.21 | Query   |   6 |     2 || informes  | 10.20.30.40 | Sleep   |  18 |  3311 || root      | localhost   | Query   |   1 |     0 |+-----------+-------------+---------+-----+-------+

En MariaDB, la misma consulta con information_schema.processlist (en MySQL 8.4 también existe, pero está obsoleta). SHOW FULL PROCESSLIST da lo mismo sin agrupar, útil cuando son pocas.

  1. Qué dice la tabla: no hay consultas pesadas (6 en Query, de 2 segundos); hay 212 conexiones dormidas desde un solo servidor de aplicación, 10.20.30.22, la más antigua de hace 7 horas y media. Ese servidor no devuelve las conexiones o tiene un pool mal dimensionado. Las causas habituales:
  • Un nodo nuevo de la aplicación (o un segundo contenedor) con el mismo pool: 2 nodos × pool de 150 = 300, justo el límite.
  • Un fallo en el código que abre conexiones fuera del pool y no las cierra; se nota porque el número crece cada hora sin bajar nunca.
  • wait_timeout en su valor por defecto, 28.800 segundos (8 horas): el servidor tarda 8 horas en cerrar una conexión abandonada.
  1. Devuelve el servicio, cerrando solo las dormidas de ese origen y con más de 10 minutos:
bash · srv-db-01
sudo mysql -N -e "SELECT CONCAT('KILL ', id, ';') FROM performance_schema.processlist WHERE user='crm_app' AND host LIKE '10.20.30.22:%' AND command='Sleep' AND time > 600" > /tmp/kill-crm.sqlwc -l /tmp/kill-crm.sql208 /tmp/kill-crm.sqlsudo mysql < /tmp/kill-crm.sqlsudo mysql -e "SHOW GLOBAL STATUS LIKE 'Threads_connected'"+-------------------+-------+| Threads_connected | 93    |+-------------------+-------+

Reiniciar la aplicación de srv-app-01/srv-app-02 también las libera, pero corta a los usuarios conectados; reiniciar MySQL las corta a todos y no arregla nada: en unas horas vuelven.

  1. Que el servidor cierre lo abandonado: wait_timeout a un valor razonable, por ejemplo 600 segundos, en caliente y en el fichero:
mysql · srv-db-01
SET GLOBAL wait_timeout = 600;# y en zz-contoso.cnf: wait_timeout = 600

Afecta a las conexiones nuevas. Y tiene una condición: el pool de la aplicación tiene que renovar sus conexiones antes de ese tiempo (en HikariCP, maxLifetime e idleTimeout por debajo de 600 segundos). Si no, la aplicación intenta usar una conexión que el servidor ya cerró y el error cambia a Communications link failure o MySQL server has gone away. Pásalo al equipo de la aplicación antes de bajar el valor, no después.

  1. El pool, dimensionado: la suma de los pools de todos los nodos y herramientas tiene que caber en max_connections con margen. Con 2 nodos de CRM, la herramienta de informes y las copias: 2 × 40 + 20 + 5 = 105, holgado con 300. Un pool de 150 por nodo casi nunca hace falta: con Threads_running en 3, la aplicación usa a la vez muy pocas conexiones.
  1. Subir el límite, solo si hace falta y con números: si después de ajustar los pools la suma real sigue rozando el límite, se sube, pero comprobando la memoria del paso 2 (buffer pool + conexiones × búferes) y el LimitNOFILE del servicio. SET GLOBAL max_connections = 400; y el fichero; en MySQL, SET PERSIST hace las dos cosas a la vez. Subirlo a 2.000 "para que no vuelva a pasar" solo cambia el error por un OOM a las 3 de la mañana.

En el ticket: hora, Threads_connected y de dónde venían las conexiones, cuántas se cerraron, el cambio en wait_timeout y en el pool, y la alerta nueva: Threads_connected > 80 % de max_connections durante 5 minutos.

Siguientes pasos

  • Monitoriza Threads_connected, Max_used_connections, Connection_errors_max_connections, el retraso de la réplica y el tamaño del registro lento con mysqld_exporter y la guía de Grafana.
  • Si la aplicación tiene muchos nodos o funciones que abren y cierran conexiones sin parar, un ProxySQL o MaxScale delante de la base de datos hace de pool común.
  • La guía de PostgreSQL cubre las mismas tareas en el otro motor: autenticación, conexiones y consultas pesadas.

¿Falta algo o en tu entorno es distinto? Escríbeme y lo corrijo.

What you'll get

Running a MySQL or MariaDB server on Linux day to day beyond backups: knowing which configuration file is really applied, giving the InnoDB buffer pool the memory it needs without the kernel killing the process, finding the queries that slow the application down and fixing the worst one with an index, reading replica status, and resolving the classic ticket: the application says "Too many connections".

Before you start

  • MySQL 8.4 LTS, or MariaDB 10.11 or 11.4 LTS, on Ubuntu, Debian or RHEL, with sudo on the server.
  • Access as the engine's root through the local socket (sudo mysql or sudo mariadb): in distribution package installs, root@localhost logs in through auth_socket / unix_socket with no password.
  • Accounts, the scheduled backup and the tested restore are in the MySQL, MariaDB and PostgreSQL: least privilege and backups guide; blocking by an open transaction and disk space, in the PostgreSQL, MySQL and SQL Server one. This guide does not repeat them.
  • A change window for whatever needs a service restart and for creating indexes on large tables.

1. Where the configuration lives (and which one wins)

The server reads several files in order and the last value read wins. Where they are depends on the engine and the distribution:

InstallMain fileIncluded foldersService
MySQL on Ubuntu / Debian/etc/mysql/my.cnf/etc/mysql/conf.d/, /etc/mysql/mysql.conf.d/ (mysqld.cnf)mysql
MariaDB on Ubuntu / Debian/etc/mysql/my.cnf/etc/mysql/conf.d/, /etc/mysql/mariadb.conf.d/ (50-server.cnf)mariadb
MySQL on RHEL/etc/my.cnf/etc/my.cnf.d/mysqld
MariaDB on RHEL/etc/my.cnf/etc/my.cnf.d/ (mariadb-server.cnf)mariadb
bash · srv-db-01
mysqld --verbose --help 2>/dev/null | grep -A1 "Default options"Default options are read from the following files in the given order:/etc/my.cnf /etc/mysql/my.cnf ~/.my.cnfmy_print_defaults mysqld--user=mysql--bind-address=10.20.40.11--innodb_buffer_pool_size=8G--max_connections=300--innodb_buffer_pool_size=12G
  • my_print_defaults mysqld gathers every line of the [mysqld] section from every file. If a parameter shows up twice, the last one applies: here 12G, not 8G. That is the source of "I changed it and nothing happens".
  • Put your changes in a file of your own, /etc/mysql/mysql.conf.d/zz-contoso.cnf or /etc/my.cnf.d/zz-contoso.cnf: it is read last and a package upgrade does not overwrite it. In MariaDB the binary is mariadbd and the [mariadbd] and [mariadb] sections also work.
  • In MySQL 8, performance_schema.variables_info says where each value comes from: SELECT VARIABLE_NAME, VARIABLE_SOURCE, VARIABLE_PATH FROM performance_schema.variables_info WHERE VARIABLE_NAME='max_connections'; returns GLOBAL + the file path, PERSISTED (a SET PERSIST, stored in mysqld-auto.cnf in the data directory) or DYNAMIC (a SET GLOBAL that will be lost on restart). MariaDB has neither this table nor SET PERSIST.

For dynamic settings, SET GLOBAL applies the change live and the file keeps it after a restart: always do both.

2. The InnoDB buffer pool and memory

The buffer pool is InnoDB's cache of data and indexes, and the parameter with the biggest effect on performance. The default (128 MB) is for a laptop, not a server.

  • On a server dedicated to the database, between 50% and 75% of RAM. With 16 GB, around 10-12 GB. The rest is for the OS, connections and the file cache.
  • If it shares the server with the application, quite a bit less, and measured.
mysql · srv-db-01
SELECT ROUND(@@innodb_buffer_pool_size/1024/1024/1024,1) AS pool_gb, @@max_connections AS max_conn;+---------+----------+| pool_gb | max_conn |+---------+----------+|    12.0 |      300 |+---------+----------+SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read%s';+----------------------------------+-------------+| Variable_name                    | Value       |+----------------------------------+-------------+| Innodb_buffer_pool_read_requests | 98411250117 || Innodb_buffer_pool_reads         | 1822304     |+----------------------------------+-------------+SET GLOBAL innodb_buffer_pool_size = 12*1024*1024*1024;
  • Innodb_buffer_pool_reads are the reads that were not in memory and went to disk. Against read_requests, here under 0.01%: the pool is big enough. If that percentage climbs and the disk sits at 100% utilisation in iostat -x, memory is short.
  • The size changes live (MySQL 5.7+ and MariaDB 10.2+) and is rounded to a multiple of innodb_buffer_pool_chunk_size. SHOW STATUS LIKE 'Innodb_buffer_pool_resize_status' says when it has finished. Put the same value in the file.

What the OS sees:

bash · srv-db-01
ps -o pid,rss,cmd -C mysqld    PID   RSS CMD   1184 13921408 /usr/sbin/mysqldfree -g               total        used        free      shared  buff/cache   availableMem:              15          13           0           0           1           1journalctl -k --since "-7d" | grep -i "out of memory"Nov 01 03:14:22 srv-db-01 kernel: Out of memory: Killed process 1184 (mysqld) total-vm:16123400kB, anon-rss:14810112kB

A mysqld killed by the kernel's OOM killer shows up as an unexplained restart in the error log (/var/log/mysql/error.log on Debian and Ubuntu, /var/log/mysqld.log on RHEL; MariaDB usually writes to the journal: journalctl -u mariadb). The cause is almost never the buffer pool alone: it is the pool plus every connection with its buffers (sort_buffer_size, join_buffer_size, read_buffer_size, temporary tables up to tmp_table_size). With 300 connections active at once, a few MB per connection is several GB. That is why max_connections and memory are looked at together.

3. max_connections

  • Default 151 on both engines. It is dynamic: SET GLOBAL max_connections = 300; and the value in the file.
  • MySQL and MariaDB keep one extra connection for an account with CONNECTION_ADMIN or SUPER (in MariaDB 10.5+, CONNECTION ADMIN): it is what lets you in as root when the application has filled all the others. If the application uses an account with those privileges, it eats that one too.
  • In MySQL 8.4 you can also define an administrative interface (admin_address and admin_port, default 33062) that does not count towards the limit; enabling it needs a restart.
  • The OS also sets a real limit: each connection uses file descriptors. If the startup log says Changed limits: max_open_files or max_connections, the systemd service needs a higher LimitNOFILE (an override with sudo systemctl edit mysql).
mysql · srv-db-01
SHOW GLOBAL STATUS WHERE Variable_name IN ('Threads_connected','Threads_running','Max_used_connections','Connection_errors_max_connections');+-----------------------------------+-------+| Variable_name                     | Value |+-----------------------------------+-------+| Connection_errors_max_connections | 0     || Max_used_connections              | 142   || Threads_connected                 | 61    || Threads_running                   | 3     |+-----------------------------------+-------+

Max_used_connections is the peak since the last start and Connection_errors_max_connections how many connections were refused because of the limit. Put them in monitoring: if the peak gets close to the limit, the ticket in step 7 is on its way.

4. The slow query log

It is enabled live, without a restart:

mysql · srv-db-01
SET GLOBAL slow_query_log_file = '/var/log/mysql/mysql-slow.log';SET GLOBAL long_query_time = 1;SET GLOBAL slow_query_log = ON;
/etc/mysql/mysql.conf.d/zz-contoso.cnf
[mysqld]innodb_buffer_pool_size = 12Gmax_connections         = 300slow_query_log          = ONslow_query_log_file     = /var/log/mysql/mysql-slow.loglong_query_time         = 1
  • long_query_time is in seconds (decimals allowed, 0.5) and defaults to 10: almost nothing reaches 10 seconds, which is why the log is usually empty even when enabled. Note: the SET GLOBAL applies to new connections; those in the application's pool keep the old value until they reconnect.
  • In MariaDB 10.11 and later the new names are log_slow_query, log_slow_query_file and log_slow_query_time; the old ones still work as aliases.
  • The file grows fast with a low threshold: add it to logrotate (Debian and Ubuntu already ship /etc/logrotate.d/mysql-server or mariadb). Do not enable log_queries_not_using_indexes in production without a limit: it logs everything that does a full scan, even if it takes 1 ms.

Reading it by hand is useless: there are thousands of near-identical entries. Two tools group them by query, with values replaced by N or ?:

bash · srv-db-01
sudo mysqldumpslow -s t -t 3 /var/log/mysql/mysql-slow.logReading mysql slow query log from /var/log/mysql/mysql-slow.logCount: 1840  Time=2.41s (4434s)  Lock=0.00s (0s)  Rows=12.0 (22080), crm_app[crm_app]@[10.20.30.21]  SELECT * FROM pedidos WHERE cliente_id = N ORDER BY fecha DESC LIMIT NCount: 96  Time=8.12s (779s)  Lock=0.00s (0s)  Rows=1.0 (96), informes[informes]@[10.20.30.40]  SELECT COUNT(*) FROM pedidos WHERE estado = 'S' AND fecha >= 'S'sudo pt-query-digest /var/log/mysql/mysql-slow.log --limit 3 | sed -n '/Profile/,/^$/p'# Profile# Rank Query ID                     Response time   Calls R/Call V/M# ==== ============================ =============== ===== ====== =====#    1 0x6A1E3F0C2D9B47A1E0F43B2C... 4434.4 71.2%   1840 2.4100  0.31 SELECT pedidos#    2 0x91C47D2B0E3A6F1C55D2A0B8... 779.5 12.5%     96 8.1200  1.02 SELECT pedidos
  • -s t sorts by total time: the 2.4-second query that runs 1,840 times weighs more than the 8-second one that runs 96 times. Fixing the first is what the user notices.
  • In MariaDB the tool is mariadb-dumpslow (with mysqldumpslow as a link on many distributions). pt-query-digest ships in the percona-toolkit package and works for both engines.

5. EXPLAIN and an index

mysql · srv-db-01 · crm database
EXPLAIN SELECT * FROM pedidos WHERE cliente_id = 4187 ORDER BY fecha DESC LIMIT 20\G           id: 1  select_type: SIMPLE        table: pedidos         type: ALLpossible_keys: NULL          key: NULL         rows: 1843221     filtered: 10.00        Extra: Using where; Using filesort

Read it top to bottom:

  • type: ALL and key: NULL: it scans the whole table (1.8 million rows) to return 20. That is what has to go.
  • Using filesort: it sorts in memory or on disk after reading. An index that starts with the WHERE column and continues with the ORDER BY column solves both.
mysql · srv-db-01 · crm database
SHOW INDEX FROM pedidos;ALTER TABLE pedidos ADD INDEX idx_pedidos_cliente_fecha (cliente_id, fecha), ALGORITHM=INPLACE, LOCK=NONE;Query OK, 0 rows affected (41.88 sec)EXPLAIN SELECT * FROM pedidos WHERE cliente_id = 4187 ORDER BY fecha DESC LIMIT 20\G         type: refpossible_keys: idx_pedidos_cliente_fecha          key: idx_pedidos_cliente_fecha         rows: 37        Extra: Backward index scan
  • ALGORITHM=INPLACE, LOCK=NONE builds the index without blocking writes (InnoDB online DDL). If the engine cannot do it that way, it returns an error instead of silently locking the table: better that it says so.
  • It still uses disk and I/O and, if there is a replica, the replica applies it afterwards and builds up lag meanwhile. On large tables, in the change window.
  • EXPLAIN ANALYZE (MySQL 8) runs the query and gives real timings; in MariaDB the equivalent is ANALYZE SELECT .... Both execute the query: never with an UPDATE or DELETE in production.
  • Check the effect the next day with mysqldumpslow: the query should have dropped from the top spot. And do not create indexes "just in case": each one slows down writes.

6. Replica status

Lag and the two threads at Yes are in the least privilege and backups guide. What matters here is which command and which names each engine uses:

MySQL 8.4MariaDB 10.11 / 11.x
Replica statusSHOW REPLICA STATUS (SHOW SLAVE STATUS no longer exists in 8.4)SHOW REPLICA STATUS or SHOW SLAVE STATUS (synonyms)
Thread fieldsReplica_IO_Running, Replica_SQL_RunningSlave_IO_Running, Slave_SQL_Running
LagSeconds_Behind_SourceSeconds_Behind_Master
Position on the sourceSHOW BINARY LOG STATUSSHOW MASTER STATUS
Start and stopSTART REPLICA / STOP REPLICASTART REPLICA or START SLAVE

In MariaDB the fields keep the old names even when the command is SHOW REPLICA STATUS: a monitoring script looking for Seconds_Behind_Source on MariaDB finds nothing and everything looks fine.

mysql · replica srv-db-01b (MySQL 8.4)
SHOW REPLICA STATUS\G             Source_Host: 10.20.40.11      Replica_IO_Running: Yes     Replica_SQL_Running: No   Seconds_Behind_Source: NULL          Last_SQL_Errno: 1062          Last_SQL_Error: Coordinator stopped because there were error(s) in the worker(s). ... Duplicate entry '88123' for key 'pedidos.PRIMARY'       Executed_Gtid_Set: 3e11fa47-71ca-11e1-9e33-c80aa9429562:1-4421907
  • An SQL thread stopped by a duplicate almost always means someone wrote to the replica. The replica should run with read_only = ON and super_read_only = ON (MySQL); in MariaDB, read_only = ON, bearing in mind that anyone with the READ_ONLY ADMIN privilege (part of SUPER up to MariaDB 11.0) can still write.
  • Do not skip the error blindly (sql_replica_skip_counter or an empty GTID transaction): you can leave the replica with different data and nothing will warn you. Note the row, compare source and replica and, if in doubt, rebuild the replica from a fresh backup. That decision goes with the application owner.
  • Before failing over or taking backups from the replica, both threads at Yes and lag at 0.

7. The ticket: "the application says 'Too many connections'"

Monday, 9:10. The CRM does not load for anyone. The (Java) application log keeps repeating: Data source rejected establishment of connection, message from server: "Too many connections". From a client:

bash · srv-app-01
mysql -h srv-db-01.contoso.local -u crm_app -p crmERROR 1040 (08004): Too many connections
  1. Get in anyway, as root through the local socket: it uses the reserved connection from step 3.
mysql · srv-db-01
sudo mysqlSHOW GLOBAL STATUS LIKE 'Threads_connected';+-------------------+-------+| Threads_connected | 301   |+-------------------+-------+SELECT user, SUBSTRING_INDEX(host,':',1) AS client, command, COUNT(*) AS n, MAX(time) AS max_s FROM performance_schema.processlist GROUP BY 1,2,3 ORDER BY n DESC LIMIT 5;+-----------+-------------+---------+-----+-------+| user      | client      | command | n   | max_s |+-----------+-------------+---------+-----+-------+| crm_app   | 10.20.30.22 | Sleep   | 212 | 26840 || crm_app   | 10.20.30.21 | Sleep   |  61 |   410 || crm_app   | 10.20.30.21 | Query   |   6 |     2 || informes  | 10.20.30.40 | Sleep   |  18 |  3311 || root      | localhost   | Query   |   1 |     0 |+-----------+-------------+---------+-----+-------+

In MariaDB, the same query with information_schema.processlist (it also exists in MySQL 8.4, but is deprecated). SHOW FULL PROCESSLIST gives the same without grouping, handy when there are few.

  1. What the table says: there are no heavy queries (6 in Query, at 2 seconds); there are 212 sleeping connections from a single application server, 10.20.30.22, the oldest from seven and a half hours ago. That server is not returning connections or has a badly sized pool. The usual causes:
  • A new application node (or a second container) with the same pool: 2 nodes × a pool of 150 = 300, exactly the limit.
  • A code bug that opens connections outside the pool and never closes them; you can tell because the number grows every hour and never drops.
  • wait_timeout at its default, 28,800 seconds (8 hours): the server takes 8 hours to close an abandoned connection.
  1. Restore service, closing only the sleeping ones from that source older than 10 minutes:
bash · srv-db-01
sudo mysql -N -e "SELECT CONCAT('KILL ', id, ';') FROM performance_schema.processlist WHERE user='crm_app' AND host LIKE '10.20.30.22:%' AND command='Sleep' AND time > 600" > /tmp/kill-crm.sqlwc -l /tmp/kill-crm.sql208 /tmp/kill-crm.sqlsudo mysql < /tmp/kill-crm.sqlsudo mysql -e "SHOW GLOBAL STATUS LIKE 'Threads_connected'"+-------------------+-------+| Threads_connected | 93    |+-------------------+-------+

Restarting the application on srv-app-01/srv-app-02 also frees them, but cuts off connected users; restarting MySQL cuts off everyone and fixes nothing: in a few hours they are back.

  1. Make the server close what is abandoned: wait_timeout to a sensible value, for example 600 seconds, live and in the file:
mysql · srv-db-01
SET GLOBAL wait_timeout = 600;# and in zz-contoso.cnf: wait_timeout = 600

It affects new connections. And it comes with a condition: the application's pool must renew its connections before that time (in HikariCP, maxLifetime and idleTimeout below 600 seconds). Otherwise the application tries to use a connection the server has already closed and the error changes to Communications link failure or MySQL server has gone away. Tell the application team before lowering the value, not after.

  1. A properly sized pool: the sum of the pools of every node and tool has to fit in max_connections with room to spare. With 2 CRM nodes, the reporting tool and backups: 2 × 40 + 20 + 5 = 105, comfortable with 300. A pool of 150 per node is almost never needed: with Threads_running at 3, the application uses very few connections at the same time.
  1. Raise the limit only if needed, and with numbers: if after fixing the pools the real total still brushes the limit, raise it, but check the memory from step 2 (buffer pool + connections × buffers) and the service's LimitNOFILE. SET GLOBAL max_connections = 400; and the file; in MySQL, SET PERSIST does both at once. Raising it to 2,000 "so it never happens again" only swaps the error for an OOM at 3 a.m.

In the ticket: time, Threads_connected and where the connections came from, how many were closed, the change to wait_timeout and the pool, and the new alert: Threads_connected > 80% of max_connections for 5 minutes.

Next steps

  • Monitor Threads_connected, Max_used_connections, Connection_errors_max_connections, replica lag and the size of the slow log with mysqld_exporter and the Grafana guide.
  • If the application has many nodes or functions that open and close connections non-stop, a ProxySQL or MaxScale in front of the database acts as a shared pool.
  • The PostgreSQL guide covers the same tasks on the other engine: authentication, connections and heavy queries.

Is something missing, or different in your environment? Get in touch and I'll fix it.

Tech Career Lab · newsletter

¿Te ha servido? Cada dos semanas envío una idea práctica para perfiles IT: LinkedIn, proyectos, networking y procesos de selección.Found this useful? Every two weeks I send one practical idea for IT professionals: LinkedIn, projects, networking and hiring processes.

Apúntate y llévate las 33 ideasSign up and get the 33 ideas →