Auditer une base de données MySQL ou MariaDB

Tout comme vous prenez soin de monitorer et maintenir vos applications à jour, il est également important de prendre soin des moteurs qui servent vos données car il s'agit de la brique la plus critique de votre application : si vos données sont corrompues ou ne peuvent pas être mises à jour dans le temps imparti, votre application sera directement impactée. Il est donc important d'auditer votre base de données, même dans le cas ou vous ne rencontrez pas encore de problème particulier mais afin de vous prévenir d'être victime de votre succès par la suite.

Audit MySQL MariaDB

Dans cet article, j'ai volontairement choisi de me baser sur les bases de données MySQL / MariaDB, ayant eu une expérience d'audit sur ce moteur de base de données.

Avant tout, connaissez votre base de données

Le plus important pour gérer au mieux une base de données est de connaitre les données et de vous rendre compte de l'utilisation de chaque table.

La requête suivante vous aidera donc à vous donner un aperçu rapide d'une base de données:


SELECT table_name, engine, row_format, table_rows, ROUND(((data_length + index_length) / 1024 / 1024), 2) AS "size (mb)"
FROM information_schema.tables
WHERE table_schema = "video"
ORDER BY (data_length + index_length) DESC;

Cette requête vous retournera quelque-chose du type :

table_name engine row_format table_rows size (mb)
content InnoDB Dynamic 304434 210.89
video InnoDB Dynamic 277810 143.67
media InnoDB Dynamic 502889 101.31

Les données remontées par ces champs sont les suivantes :

  • table_name : Le nom de la table,
  • engine : Le moteur utilisé pour stocker les données dans cette table (par défaut InnoDB depuis la version 5.5.5 de MySQL),
  • row_format : Le type de formattage de la table (la valeur par défaut est définie par la variable innodb_default_row_format), sa valeur peut être DYNAMIC, COMPRESSED, REDUNDANT ou COMPACT,
  • table_rows : Le nombre de ligne dans la table,
  • size : La taille (en megabytes) de la table,

Formattage des fichiers contenant vos données

Comme il a été dit précédemment, InnoDB est maintenant le moteur par défaut de MySQL et MariaDB. InnoDB a évolué avec le temps et gère aujourd'hui deux types de format de fichiers : [Barracuda](https://dev.mysql.com/doc/refman/5.6/en/glossary.html#glos_antelope" target="_blank">Antelope et <a href="https://dev.mysql.com/doc/refman/5.6/en/glossary.html#glos_barracuda).

Antelope gère les formats de données (row_format) [COMPACT](https://dev.mysql.com/doc/refman/5.6/en/glossary.html#glos_redundant_row_format" target="_blank">REDUNDANT et <a href="https://dev.mysql.com/doc/refman/5.6/en/glossary.html#glos_compact_row_format). Dans l'ordre, le format REDUNDANT était utilisé par défaut dans MySQL puis il a été remplacé par le type COMPACT qui permettait de mieux gérer la représentation des valeurs nulles et la longueur des colonnes.

Barracuda est la dernière version de format de données qui prend en charge le formattage des données (row_format) [DYNAMIC](https://dev.mysql.com/doc/refman/5.6/en/glossary.html#glos_compressed_row_format" target="_blank">COMPRESSED et <a href="https://dev.mysql.com/doc/refman/5.6/en/glossary.html#glos_dynamic_row_format). Ces deux formats permettent de stocker des champs très volumineux en dehors du reste des données et l'ensemble des indexes et champs volumineux sont compressés, permettant de réaliser des économies de mémoire et d'espace disque. Le format DYNAMIC est une version améliorée qui permet également d'ajouter moins de données dans le buffer utilisé lorsque vous exécutez des requêtes, ce qui vous fait donc économiser des opérations I/O et des performances CPU et mémoire.

Pour résumer, vous préférerez aujourd'hui utiliser le moteur Barracuda (utilisé par défaut), plus récent et plus optimisé. De plus, il vous permet de mieux gérer les champs larges de type BLOB ou TEXT et permet de gérer des colonnes plus larges (varchar(255) avec utf8mb4).

Configuration système

La configuration système de votre serveur dépend beaucoup de l'utilisation de votre base de données, tant en terme de stockage que de requêtage.

InnoDB alloue par défaut 128MB de RAM pour son buffer et cette valeur peut être modifiée via le paramètre innodb_buffer_pool_size. Il est en général recommandé d'allouer à ce paramètre environ 60% de la mémoire physique disponible.


SHOW VARIABLES LIKE 'innodb_buffer_pool_size';
Variable_name Value
innodb_buffer_pool_size 134217728

Cette valeur est donc la valeur par défaut (134217728 / 1024 / 1024 = 128).

Configuration MySQL

MySQL expose environ 500 variables de configuration, nous allons uniquement passer sur les plus importantes et celles qui peuvent rapidement vous apporter de l'aide. Ce sont ces valeur que vous devrez affiner en fonction des éléments que nous analyserons dans la suite de cet article.

  • innodb_flush_log_at_trx_commit : Permet que le log de transaction soit écrit à chaque transaction et évite ainsi toute perte de données en cas de crash système ou machine (valeur 1), cependant, la valeur 2 est recommandée car elle permet de stocker sur le disque une fois par seconde et évite un gros afflux d'I/O disque,
  • innodb_io_capacity : InnoDB se limite par défaut à 200 I/O par seconde pour ses opérations de flush, buffer et merge de données. En cas de forte activité, n'hésitez pas à augmenter cette limite à 400 I/O par seconde, ou plus si nécessaire.
  • innodb_lock_wait_timeout : Il s'agit du temps d'attente par InnoDB lors de l'acquisition d'un vérou avant de le relâcher. Défini à 50 secondes par défaut, il peut être intéressant de baisser ou d'augmenter cette limite en fonction de votre utilisation afin d'éviter de vous retrouver avec des données vérouillées alors que votre application cherche à les modifier de nouveau.
  • innodb_log_buffer_size : Cette valeur (définie à 16MB par défaut) permet de définir la taille du buffer utilisée pour les données de log : un buffer de log élargi vous permettra d'économiser en I/O disque avant que la transaction ne soit commitée.
  • innodb_log_file_size : Ce paramètre est très important, en cas de forte charge, ce paramètre permet de transformer des opérations disques aléatoires en séquentielles, améliorant l'utilisation globale de la base de données. Par défaut, 48MB sont alloués, n'hésitez pas à augmenter cette valeur à 64 ou 128MB en fonction de l'utilisation de votre base.
  • innodb_thread_concurrency : Il s'agit du nombre de threads alloués par InnoDB afin de servir les requêtes en concurrence. Il est recommandé d'allouer entre 2 et 8 fois le nombre de CPUs disponibles sur la machine (avec une valeur maximale de 256).
  • key_buffer_size : En plus du buffer InnoDB, MySQL utilise ce buffer afin de stocker les tables temporaires qui peuvent être utilisées pour certaines de vos requêtes. Il est recommandé de garder une valeur de 64 à 128MB.
  • max_connections : Ce paramètre limite le nombre de connexions à votre base MySQL, en fonction de son utilisation, ce paramètre peut être augmenté.
  • max_heap_table_size : Ce paramètre permet de définir la taille maximale des tables temporaires implicites en mémoire.
  • open_files_limit : Il s'agit du nombre maximal de fichiers pouvant être ouverts par MySQL, il est recommandé d'augmenter cette limite si votre base de données accepte beaucoup de connexions.
  • query_cache_limit : Ce paramètre permet de définir la taille maximale des résultats retournés par votre requête qui seront mis en cache. Réduire cette valeur permet d'éviter qu'un gros résultat de requête ne soit caché et invalide le résultat de requêtes plus petites et plus fréquentes (ayant plus besoin du cache).
  • query_cache_size : Limite la mémoire allouée au cache des résultats de vos requêtes. Une taille inférieure à 512 MB est généralement recommandée (en fonction de votre mémoire disponible).
  • transaction-isolation : Le passage en READ-COMMITTED est recommandé pour ce paramètre, permettant que chaque lecture cohérente, même à l'intérieur d'une même transaction, établit et lit son propre snapshot frais (réduit le nombre de deadlocks).
  • slow_query_log : Il est recommandé de laisser actif le log des requêtes lentes, en augmentant la valeur du paramètre long_query_time permettant de définir le seuil d'une requête lente.

N'hésitez pas à naviguer sur les différents paramètres afin de voir quelles seront les meilleures valeurs pour vous.

Analyse du trafic SQL

Afin de pouvoir définir ces valeurs, il convient également d'analyser le trafic de votre base de données et donc des requêtes effectuées sur votre base. Pour cela, vous pouvez décider d'activer le log général des requêtes pendant quelques secondes seulement. C'est ce que nous allons faire ici pendant 60 secondes :

SET GLOBAL general_log = 1; SELECT SLEEP(60); SET GLOBAL general_log = 0;

Vous pouvez également activer les slow query logs en adaptant la valeur du paramètre long_query_time :

SET GLOBAL slow_query_log = 1;
SET GLOBAL long_query_time = 0.1;
SET GLOBAL log_queries_not_using_indexes = 1;

Une fois vos logs générés, vous pouvez utiliser installer l'outil percona-tookit afin d'éffectuer une analyse plus poussée et des statistiques sur les différents logs :

$ apt-get install percona-toolkit

Vous pourrez ensuite générer ces statistiques à partir de l'outil pt-query-digest :

$ cd /var/log/mysql
$ pt-query-digest --type=genlog query.log > mysql.log.digest
$ pt-query-digest mysql-slow.log > mysql-slow.log.digest

Vous aurez ainsi dans ces fichiers un en-tête vous présentant des statistiques sur l'ensemble des requêtes enregistrées et ainsi voir les patterns de requêtes les plus effectués ou encore prenant le plus de temps :

# 270ms user time, 10ms system time, 26.00k rss, 14.00k vsz
# Current date: Sun Aug 26 14:17:47 2018
# Hostname: b85e368d128b
# Files: query.log
# Overall: 242 total, 4 unique, 0 QPS, 0x concurrency ___________________
# Time range: 2018-08-26T14:15:03.611597Z to 2018-08-26T14:16:03.612118Z
# Attribute          total     min     max     avg     95%  stddev  median
# ============     ======= ======= ======= ======= ======= ======= =======
# Exec time              0       0       0       0       0       0       0
# Query size       137.17k       8   1.89k  580.40   1.04k  472.00  299.03

# Profile
# Rank Query ID           Response time Calls R/Call V/M   Item
# ==== ================== ============= ===== ====== ===== ===============
#    1 0x87AF0AF11E8DFF98  0.0000  0.0%    40 0.0000  0.00 SELECT product
#    2 0xB5403927856EA576  0.0000  0.0%    40 0.0000  0.00 SELECT media media_image media_video
#    3 0x615E6B9AC6FB3863  0.0000  0.0%    27 0.0000  0.00 SELECT pattern
#    4 0x24A0450BEC6D831A  0.0000  0.0%    23 0.0000  0.00 SELECT category taxonomy

Puis, pour chaque requête, vous pouvez rechercher dans le fichier en vous basant sur l'identifiant de la requête (Query ID) afin d'obtenir plus de détail :

# Query 1: 0 QPS, 0x concurrency, ID 0x87AF0AF11E8DFF98 at byte 150147 ___
# This item is included in the report because it matches --limit.
# Scores: V/M = 0.00
# Time range: 2018-08-26T14:15:52.308078Z to 2018-08-26T14:15:52.583549Z
# Attribute    pct   total     min     max     avg     95%  stddev  median
# ============ === ======= ======= ======= ======= ======= ======= =======
# Count         16      40
# Exec time      0       0       0       0       0       0       0       0
# Query size     5   7.46k     190     191  190.90  183.58       0  183.58
# String:
# Databases    video
# Query_time distribution
#   1us
#  10us
# 100us
#   1ms
#  10ms
# 100ms
#    1s
#  10s+
# Tables
#    SHOW TABLE STATUS FROM `video` LIKE 'product'\G
#    SHOW CREATE TABLE `video`.`product`\G
# EXPLAIN /*!50100 PARTITIONS*/
SELECT t0.pro_id AS pro_id_1, t0.pro_object_type AS pro_object_type_2, t0.pro_object_id AS pro_object_id_3 FROM product t0 WHERE t0.pro_object_type = 'media' AND t0.pro_object_id = 79 LIMIT 1\G

Profiling de requêtes SQL

Afin d'analyser plus en détail vos requêtes SQL, vous pouvez également activer le profiling sur votre session uniquement puis jouer vos requêtes :

SET SESSION profiling = 1;
SELECT * FROM mytable WHERE myfield LIKE '%toto';

Puis, pour avoir le résultat de vos profiles :

SHOW PROFILES;

Query_ID Duration Query
1 0.00026850 "SELECT * FROM mytable WHERE myfield LIKE '%toto'"

SHOW PROFILE CPU FOR QUERY 1;

Notez que le "CPU" n'est pas obligatoire, mais l'information peut être intéressante à avoir en retour. Ceci vous donnera le retour suivant :

Status Duration CPU_user CPU_system
starting 0.000000 0.000096 0.000000
checking permissions 0.000011 0.000000 0.000000
Opening tables 0.000022 0.000000 0.000000
init 0.000037 0.000000 0.000000
System lock 0.000011 0.000000 0.000000
optimizing 0.000011 0.000000 0.000000
statistics 0.000070 0.000000 0.000000
preparing 0.000011 0.000000 0.000000
executing 0.000003 0.000000 0.000000
... ... ... ...

Pour plus d'informations sur les éléments retournés par le profiler, rendez-vous sur l'URL suivante : https://dev.mysql.com/doc/refman/8.0/en/profiling-table.html

Analyse des compteurs MySQL

MySQL vient également avec tout un tas de compteurs, affichés via la requête :

SHOW GLOBAL STATUS;

Encore une fois, beaucoup d'éléments sont retournés mais un petit échantillon de compteurs vous aideront déjà à analyser l'activité de votre base de données.

Vous pourrez par exemple récupérer des informations sur les connexions :

  • Aborted_clients : Il s'agit du nombre de connexions ayant été annulées car le client a échoué sans fermer la connexion,
  • Aborted_connects : Il s'agit du nombre de connexions ayant échouées : un nombre important peut être indiqué dans le cas ou un check TCP (health check) serait effectué : en effet, un check TCP n'est pas suffisant pour déterminer si une base de données est fonctionnelle,

Des compteurs InnoDB sont également remontés :

  • Innodb_buffer_pool_read_requests : Il s'agit du nombre de lectures effectuées.
  • Innodb_buffer_pool_reads : Il s'agit du nombre de lectures effectuées depuis le disque, ne pouvant être satisfaites par le buffer pool.

Si le ratio descend en dessous de 99.95%, songez à augmenter la taille du buffer pool InnoDB afin de ne pas dégrader les performances de votre base.

  • Created_tmp_disk_tables : Il s'agit du nombre de tables temporaires créées en utilisant le disque.
  • Created_tmp_files : Il s'agit du nombre de fichiers créés pour satisfaire les tables temporaires.
  • Created_tmp_tables : Il s'agit du nombre de tables temporaires créées depuis le démarrage du serveur.

Le ratio de tables temporaires crées sur le disque ne doit pas excéder 10-20%, idéalement, essayez de réduire le ratio en améliorant vos requêtes.

Les compteurs suivants vous donneront également des informations complémentaires sur l'utilisation du query cache MySQL :

  • Qcache_free_blocks
  • Qcache_free_memory
  • Qcache_hits
  • Qcache_inserts
  • Qcache_lowmem_prunes
  • Qcache_not_cached
  • Qcache_queries_in_cache
  • Qcache_total_blocks

Vous pouvez ainsi mesurer le ratio entre Inserts et Hits afin de vous rendre compte de l'utilisation du cache. Vous pourrez ainsi faire la chasse aux requêtes qui ne sont pas cachées. Très important : n'oubliez pas que les requêtes contenant un champ TEXT ou BLOB ou un résultat supérieur au paramètre "query_cache_limit" ne seront jamais cachées. Préferrez donc un VARCHAR(255) à un champ TEXT.

Enfin, vous trouverez également des informations sur les requêtes SELECT effectuées sur votre base :

  • Select_full_join
  • Select_full_range_join
  • Select_range
  • Select_range_check
  • Select_scan

Ici, un nombre important de "Select_scan" signifie que beaucoup de "full table scan" sont effectués sur votre base, il convient alors d'analyser les requêtes avec les outils cités précédemment dans cet article pour observer ce qui ne va pas. Vous aurez certainement besoin de revoir vos indexes.

Optimisez vos requêtes SQL

Utilisez le mot-clé EXPLAIN (ou EXPLAIN EXTENDED) devant vos requêtes qui vous permettent de voir le plan d'exécution de MySQL et vous assurer que les indexes sont correctement utilisés.

Aussi, évitez les quotes dans vos requêtes lorsque vous effectuez des filtres sur des champs numériques afin d'éviter que MySQL ait besoin de faire des conversions sur les colonnes et donc de parcourir l'intégralité de la table.

Il est également important de surveiller vos clé primaires et valeurs d'auto_increment. Voici une requête qui vous permettra de voir le taux d'utilisation de vos clés primaires ainsi que le type de ceux-ci :

SELECT TABLE_SCHEMA,TABLE_NAME,COLUMN_NAME,COLUMN_TYPE, DATA_TYPE, auto_increment, max_value, ROUND(auto_increment / max_value * 100,2) as 'pct_used'
FROM (
    SELECT t.TABLE_SCHEMA,t.TABLE_NAME,c.COLUMN_NAME,c.DATA_TYPE,c.COLUMN_TYPE, t.auto_increment, IF(Locate('unsigned', column_type) = 0,
    CASE data_type
        WHEN 'tinyint' THEN '127'
        WHEN 'smallint' THEN '32767'
        WHEN 'mediumint' THEN '8388607'
        WHEN 'int' THEN '2147483647'
        WHEN 'bigint' THEN '9223372036854775807'
    END ,
    CASE data_type
        WHEN 'tinyint' THEN '255'
        WHEN 'smallint' THEN '65535'
        WHEN 'mediumint' THEN '16777215'
        WHEN 'int' THEN '4294967295'
        WHEN 'bigint' THEN '18446744073709551615'
    END) AS max_value
    FROM INFORMATION_SCHEMA.TABLES t
    JOIN INFORMATION_SCHEMA.COLUMNS c ON (t.TABLE_SCHEMA = c.TABLE_SCHEMA AND t.TABLE_NAME = c.TABLE_NAME)
    WHERE c.COLUMN_KEY = 'PRI' AND EXTRA='auto_increment'
) bigq
ORDER BY pct_used DESC;

Vous aurez le retour suivant :

TABLE_SCHEMA TABLE_NAME COLUMN_NAME COLUMN_TYPE DATA_TYPE auto_increment max_value pct_used
video content con_id int(11) int 659268 2147483647 0.03
video media med_id int(11) int 1057114 2147483647 0.05
video taxonomy tax_id int(11) int 7028 2147483647 0.00

Dans le cas ou vous n'avez pas de clé primaire, InnoDB devra créer une clé primaire cachée qui ne sera pas du tout optimisée.

Faite également la chasse aux colonnes TEXT / BLOB afin de pouvoir bénéficier du cache query MySQL, voici une requête qui vous permettra de remonter toutes les colonnes de type BLOB ou TEXT :

SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, COLUMN_TYPE
FROM information_schema.columns
WHERE (COLUMN_TYPE LIKE '%TEXT%' OR COLUMN_TYPE LIKE '%BLOB%')
AND TABLE_SCHEMA NOT IN ('performance_schema', 'information_schema', 'mysql');

Si vous avez un doute sur un champ, vous pouvez rechercher la longueur maximale présente dans vos données pour cette clonne en jouant la requête suivante :

SELECT MAX(LENGTH(myfield)) FROM mytable;

J'espère que ces quelques informations auront pu vous aider à optimiser les performances de votre base de données, aussi bien en terme de requête qu'en terme de ressources physiques car les deux sont étroitement liées. N'hésitez pas à me contacter si je peux vous aider à aller de l'avant en vous fournissant plus d'informations.