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
sudoen el servidor. - Acceso como
rootdel motor por el socket local (sudo mysqlosudo mariadb): en las instalaciones de los paquetes de la distribución,root@localhostentra porauth_socket/unix_socketsin 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ón | Fichero principal | Carpetas incluidas | Servicio |
|---|---|---|---|
| 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 |
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=12Gmy_print_defaults mysqldjunta 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.cnfo/etc/my.cnf.d/zz-contoso.cnf: se lee al final y una actualización del paquete no lo pisa. En MariaDB, el binario esmariadbdy las secciones[mariadbd]y[mariadb]también valen. - En MySQL 8,
performance_schema.variables_infodice de dónde sale cada valor:SELECT VARIABLE_NAME, VARIABLE_SOURCE, VARIABLE_PATH FROM performance_schema.variables_info WHERE VARIABLE_NAME='max_connections';devuelveGLOBAL+ ruta del fichero,PERSISTED(unSET PERSIST, guardado enmysqld-auto.cnfdentro del directorio de datos) oDYNAMIC(unSET GLOBALque se perderá al reiniciar). MariaDB no tiene esta tabla niSET 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.
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_readsson las lecturas que no estaban en memoria y fueron a disco. Frente aread_requests, aquí menos del 0,01 %: el pool es suficiente. Si ese porcentaje sube y el disco va al 100 % de uso eniostat -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:
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:14810112kBUn 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_ADMINoSUPER(en MariaDB 10.5+,CONNECTION ADMIN): es la que te deja entrar comorootcuando 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_addressyadmin_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_filesomax_connections, el servicio de systemd necesitaLimitNOFILEmás alto (un override consudo systemctl edit mysql).
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:
SET GLOBAL slow_query_log_file = '/var/log/mysql/mysql-slow.log';SET GLOBAL long_query_time = 1;SET GLOBAL slow_query_log = ON;[mysqld]innodb_buffer_pool_size = 12Gmax_connections = 300slow_query_log = ONslow_query_log_file = /var/log/mysql/mysql-slow.loglong_query_time = 1long_query_timeestá 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: elSET GLOBALvale 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_fileylog_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-serveromariadb). No activeslog_queries_not_using_indexesen 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 ?:
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 tordena 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(conmysqldumpslowcomo enlace en muchas distribuciones).pt-query-digestviene en el paquete percona-toolkit y sirve para los dos motores.
5. EXPLAIN y un índice
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 filesortSe lee de arriba abajo:
type: ALLykey: 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 delWHEREy siga por la delORDER BYresuelve las dos cosas.
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 scanALGORITHM=INPLACE, LOCK=NONEcrea 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 esANALYZE SELECT .... Los dos ejecutan la consulta: nunca con unUPDATEoDELETEen 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.4 | MariaDB 10.11 / 11.x | |
|---|---|---|
| Estado de la réplica | SHOW REPLICA STATUS (SHOW SLAVE STATUS ya no existe en 8.4) | SHOW REPLICA STATUS o SHOW SLAVE STATUS (son sinónimos) |
| Campos de los hilos | Replica_IO_Running, Replica_SQL_Running | Slave_IO_Running, Slave_SQL_Running |
| Retraso | Seconds_Behind_Source | Seconds_Behind_Master |
| Posición en el origen | SHOW BINARY LOG STATUS | SHOW MASTER STATUS |
| Arrancar y parar | START REPLICA / STOP REPLICA | START 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.
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 = ONysuper_read_only = ON(MySQL); en MariaDB,read_only = ON, teniendo en cuenta que quien tenga el privilegioREAD_ONLY ADMIN(incluido enSUPERhasta MariaDB 11.0) sigue pudiendo escribir. - No saltes el error a ciegas (
sql_replica_skip_countero 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
Yesy 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:
mysql -h srv-db-01.contoso.local -u crm_app -p crmERROR 1040 (08004): Too many connections- Entra igualmente, como
rootpor el socket local: usa la conexión reservada del paso 3.
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.
- 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_timeouten su valor por defecto, 28.800 segundos (8 horas): el servidor tarda 8 horas en cerrar una conexión abandonada.
- Devuelve el servicio, cerrando solo las dormidas de ese origen y con más de 10 minutos:
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.
- Que el servidor cierre lo abandonado:
wait_timeouta un valor razonable, por ejemplo 600 segundos, en caliente y en el fichero:
SET GLOBAL wait_timeout = 600;# y en zz-contoso.cnf: wait_timeout = 600Afecta 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.
- El pool, dimensionado: la suma de los pools de todos los nodos y herramientas tiene que caber en
max_connectionscon 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: conThreads_runningen 3, la aplicación usa a la vez muy pocas conexiones.
- 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
LimitNOFILEdel servicio.SET GLOBAL max_connections = 400;y el fichero; en MySQL,SET PERSISThace 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 conmysqld_exportery 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
sudoon the server. - Access as the engine's
rootthrough the local socket (sudo mysqlorsudo mariadb): in distribution package installs,root@localhostlogs in throughauth_socket/unix_socketwith 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:
| Install | Main file | Included folders | Service |
|---|---|---|---|
| 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 |
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=12Gmy_print_defaults mysqldgathers 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.cnfor/etc/my.cnf.d/zz-contoso.cnf: it is read last and a package upgrade does not overwrite it. In MariaDB the binary ismariadbdand the[mariadbd]and[mariadb]sections also work. - In MySQL 8,
performance_schema.variables_infosays where each value comes from:SELECT VARIABLE_NAME, VARIABLE_SOURCE, VARIABLE_PATH FROM performance_schema.variables_info WHERE VARIABLE_NAME='max_connections';returnsGLOBAL+ the file path,PERSISTED(aSET PERSIST, stored inmysqld-auto.cnfin the data directory) orDYNAMIC(aSET GLOBALthat will be lost on restart). MariaDB has neither this table norSET 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.
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_readsare the reads that were not in memory and went to disk. Againstread_requests, here under 0.01%: the pool is big enough. If that percentage climbs and the disk sits at 100% utilisation iniostat -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:
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:14810112kBA 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_ADMINorSUPER(in MariaDB 10.5+,CONNECTION ADMIN): it is what lets you in asrootwhen 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_addressandadmin_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_filesormax_connections, the systemd service needs a higherLimitNOFILE(an override withsudo systemctl edit mysql).
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:
SET GLOBAL slow_query_log_file = '/var/log/mysql/mysql-slow.log';SET GLOBAL long_query_time = 1;SET GLOBAL slow_query_log = ON;[mysqld]innodb_buffer_pool_size = 12Gmax_connections = 300slow_query_log = ONslow_query_log_file = /var/log/mysql/mysql-slow.loglong_query_time = 1long_query_timeis 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: theSET GLOBALapplies 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_fileandlog_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-serverormariadb). Do not enablelog_queries_not_using_indexesin 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 ?:
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 tsorts 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(withmysqldumpslowas a link on many distributions).pt-query-digestships in the percona-toolkit package and works for both engines.
5. EXPLAIN and an index
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 filesortRead it top to bottom:
type: ALLandkey: 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 theWHEREcolumn and continues with theORDER BYcolumn solves both.
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 scanALGORITHM=INPLACE, LOCK=NONEbuilds 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 isANALYZE SELECT .... Both execute the query: never with anUPDATEorDELETEin 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.4 | MariaDB 10.11 / 11.x | |
|---|---|---|
| Replica status | SHOW REPLICA STATUS (SHOW SLAVE STATUS no longer exists in 8.4) | SHOW REPLICA STATUS or SHOW SLAVE STATUS (synonyms) |
| Thread fields | Replica_IO_Running, Replica_SQL_Running | Slave_IO_Running, Slave_SQL_Running |
| Lag | Seconds_Behind_Source | Seconds_Behind_Master |
| Position on the source | SHOW BINARY LOG STATUS | SHOW MASTER STATUS |
| Start and stop | START REPLICA / STOP REPLICA | START 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.
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 = ONandsuper_read_only = ON(MySQL); in MariaDB,read_only = ON, bearing in mind that anyone with theREAD_ONLY ADMINprivilege (part ofSUPERup to MariaDB 11.0) can still write. - Do not skip the error blindly (
sql_replica_skip_counteror 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
Yesand 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:
mysql -h srv-db-01.contoso.local -u crm_app -p crmERROR 1040 (08004): Too many connections- Get in anyway, as
rootthrough the local socket: it uses the reserved connection from step 3.
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.
- 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_timeoutat its default, 28,800 seconds (8 hours): the server takes 8 hours to close an abandoned connection.
- Restore service, closing only the sleeping ones from that source older than 10 minutes:
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.
- Make the server close what is abandoned:
wait_timeoutto a sensible value, for example 600 seconds, live and in the file:
SET GLOBAL wait_timeout = 600;# and in zz-contoso.cnf: wait_timeout = 600It 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.
- A properly sized pool: the sum of the pools of every node and tool has to fit in
max_connectionswith 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: withThreads_runningat 3, the application uses very few connections at the same time.
- 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 PERSISTdoes 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 withmysqld_exporterand 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 →