Swap sous Linux avec MySQL

Vous venez de découvrir que votre serveur Linux possède des données dans son espace de swap et vous ne savez pas comment l’interpréter. Faut-il s’affoler, ou n’est-ce qu’une charge passagère qui date de l’année passée ? Ou pire encore, votre superviseur Nagios/Zabbix vient de vous remonter une alerte critique précisant que 90% de l’espace de swap sont occupés.
Cet article se propose de vous aider à comprendre la différence entre pagination et swap, l’impact réel sur le système, vos bases de données et vos applications, puis à élaborer une stratégie de configuration adaptée à vos objectifs et votre environnement qu’il soit ou non de production.
Les exemples cités ci-après correspondent à des contextes de serveur hébergeant des bases MySQL, mais la démarche serait similaire avec des bases Oracle. Et il est fort probable que tout DBA Oracle sera à même de traduire aisément la troisième étape de cet article plus spécifique aux bases MySQL, à son contexte équivalent sous Oracle.

Six étapes pour y voir plus clair :

1. La première étape consiste à déterminer l’ampleur du problème
2. La deuxième étape permet d’identifier les process affectés
3. La troisième étape vérifie la configuration des bases MySQL
4. La quatrième étape tente de limiter l’activité de swapping
5. La cinquième étape aide à protéger au mieux les bases MySQL de production
6. Point particulier : les architectures NUMA

1. Déterminer l’ampleur du problème

L’évolution de l’occupation de l’espace de swap dans les jours passés sera la première base d’analyse.

  • Notez l’espace occupé dans l’espace de swap actuellement sur votre système :
    # free -h   # ou -m pour un résultat en Mo
                 total       used       free     shared    buffers     cached
    Mem:           15G        14G       1.4G        40K       225M        10G
    -/+ buffers/cache:       3.1G        12G
    Swap:         511M       442M        69M

    ainsi que l’organisation physique de cet espace de swap et son éventuelle répartition sur plusieurs partitions :

    [root@mysqlserver ~]# cat /proc/swaps   # idem swapon -s
    Filename         Type            Size    Used    Priority
    /dev/dm-1        partition       524284  442136  -1
    /dev/dm-8        partition       2621436 764972  -2
    

    Ci-dessus, il est probable que la seconde partition (dm-8) ait été ajoutée à chaud par un ingénieur système suite à un problème (c’est la taille de cette partition qui me fait penser cela).
    Si cette intervention est récente, il est possible d’en retrouver une trace dans un des fichiers logs du système (messages, dmesg, syslog ou encore kern.log sous /var/log). Tout dépendra de la configuration de la rotation (/etc/logrotate.conf).
    Et si l’intervention est très récente – moins d’un mois – vous retrouverez même à quelle heure précise elle a eu lieu grâce aux logs présentés ci-dessous.

  • En effet, sur les serveurs Linux, il est possible à posteriori d’analyser la fréquence de la pagination/swapping heure par heure, car deux commandes sar (System Activity Report) sont lancées périodiquement, l’une une fois par heure avec toutes les options de sar, l’autre toutes les 10mn.
    [root@mysqlserver ~]# cat  /etc/cron.d/sysstat
    # Run system activity accounting tool every 10 minutes
    */10 * * * * root /usr/lib64/sa/sa1 1 1
    # 0 * * * * root /usr/lib64/sa/sa1 600 6 &
    # Generate a daily summary of process accounting at 23:53
    53 23 * * * root /usr/lib64/sa/sa2 -A

    Cette configuration a été mise en place en crontab lors de l’installation du package sysstat. Si le package n’est pas déjà installé sur votre serveur, ce qui serait tout de même surprenant, il vous suffira de lancer :

    [root@mysqlserver ~]# yum install sysstat

    et si vous êtes sous RedHat 7 ou une Debian assez récente, vous ajouterez :

    [root@mysqlserver ~]# systemctl start sysstat
    [root@mysqlserver ~]# systemctl status sysstat

    et uniquement si la seconde ligne du résultat du status précédent ne se termine pas par « enable) », activez son lancement automatique à chaque reboot par :

    [root@mysqlserver ~]# systemctl enable sysstat

    Les résultats des sar sont conservés dans un fichier quotidien sur 28 jours glissants. Le nom du fichier reprend le numéro du jour du mois.
    Pour être plus précis, deux fichiers sont générés par jour, l’un étant le binaire de l’autre. Par exemple, pour le 19 du mois, le fichier sa19 est le binaire du fichier texte sar19. Mais peu importe, les boucles for ci-dessous vous permettront d’exploiter ces fichiers au mieux sans vous poser trop de questions.
    Si vous souhaitez modifier la durée de conservation des logs sar, il vous suffit de modifier la variable HISTORY du fichier de configuration /etc/sysconfig/sysstat, après avoir lu le petit commentaire qui lui précède  😉 puis de demander au démon de prendre en compte votre modification par la commande :

    [root@mysqlserver ~]# systemctl reload sysstat

    Ces différentes syntaxes de la commande systemctl vous permettent de découvrir ainsi le côté simple et sympathique de systemd.

  • Avant de lancer une commande pour exploiter les logs des sar, vérifiez la présence de ces logs et éventuellement notez les plus récents :
    [root@mysqlserver ~]# function l {   ls -alt  "$@" | less -eFmRX ; }   # ~/.bashrc ?
    [root@mysqlserver ~]# l /var/log/sa/
    -rw-r--r--.  1 root root 899124 Oct 19 23:53 sar19
    -rw-r--r--.  1 root root 830484 Oct 19 23:50 sa19
    -rw-r--r--.  1 root root 899124 Oct 18 23:53 sar18
    -rw-r--r--.  1 root root 830484 Oct 18 23:50 sa18
    ...

Maintenant, il vous suffit de préciser les numéros des jours que vous souhaitez étudier dans les exemples de boucles for ci-dessous à la place des 19 et 20 de la première ligne.
Dans les résultats que vous obtiendrez, le constat d’une progression régulière (colonnes pswpout/s et kbswpused) de seulement quelques pages, dénote d’une simple pagination et non d’un « swapping » violent et pénalisant.

  • Par exemple, pour lister l’activité (-W : activité swap -S : espace de swap -r : mémoire -u: CPU) sur deux jours précis (et sur la tranche horaire 9h-11h pour limiter le nombre de lignes du résultat ci-dessous) :
    for J in 19 20
    do
       echo -e "\n---------------------------\n Le $J \n---------------------------"
       sar -WSru -f /var/log/sa/sa"$J"  -s 09:00:00 -e 11:00:00
    done | less

    Notez dans le listing ci-dessous que le 19 à 10H20 MySQL a été relancée, d’où les pages de swapin (remontée du swap vers la RAM) .
    Notez aussi que la colonne %memused ne décroche pas des 99% !

    for J in 19 20
     Le 19
    ---------------------------
    Linux 2.6.32-431.29.2.el6.x86_64 (HVMP1509)     10/19/2016      _x86_64_     (8 CPU)
    09:00:01 AM     CPU     %user     %nice   %system   %iowait    %steal     %idle
    09:10:01 AM     all     15.30      0.86      1.60      0.94      0.00     81.30
    09:20:01 AM     all     19.91      0.00      1.38      0.97      0.00     77.74
    09:30:01 AM     all     19.19      0.00      1.49      0.62      0.00     78.70
    09:40:01 AM     all     14.26      0.00      1.26      0.79      0.00     83.69
    09:50:01 AM     all     14.75      0.00      1.26      1.01      0.00     82.98
    10:00:01 AM     all     14.03      0.00      1.14      1.13      0.00     83.69
    10:10:01 AM     all     15.64      0.00      1.31      0.92      0.00     82.12
    10:20:01 AM     all     21.96      0.00      2.31      2.87      0.00     72.86
    10:30:01 AM     all     18.15      0.00      1.58      1.35      0.00     78.92
    10:40:01 AM     all     27.00      0.00      2.23      1.01      0.00     69.76
    10:50:02 AM     all     29.91      0.00      2.29      0.72      0.00     67.08
    Average:        all     19.08      0.08      1.62      1.12      0.00     78.10
    09:00:01 AM  pswpin/s pswpout/s
    09:10:01 AM      0.07      0.00
    09:20:01 AM      0.01      0.00
    09:30:01 AM      0.00      0.00
    09:40:01 AM      0.00      0.80
    09:50:01 AM      0.01      2.50
    10:00:01 AM      0.00      0.44
    10:10:01 AM      1.27      0.08
    10:20:01 AM    152.08      0.67
    10:30:01 AM      0.13      0.36
    10:40:01 AM      0.14      0.30
    10:50:02 AM      0.09      0.76
    Average:        13.88      0.54
    09:00:01 AM kbmemfree kbmemused  %memused kbbuffers  kbcached  kbcommit   %commit
    09:10:01 AM    776312  32096240     97.64     44692  20491224  19062152     54.51
    09:20:01 AM    483676  32388876     98.53     76364  20754124  19062264     54.51
    09:30:01 AM    190600  32681952     99.42     96464  21021320  19062128     54.51
    09:40:01 AM    192228  32680324     99.42    112396  20999268  19062132     54.51
    09:50:01 AM    134004  32738548     99.59    125872  21051188  19062132     54.51
    10:00:01 AM    134868  32737684     99.59    148340  21022664  19062128     54.51
    10:10:01 AM    135572  32736980     99.59    160976  21000248  19080660     54.56
    10:20:01 AM    135752  32736800     99.59    175108  20542328  19028484     54.41
    10:30:01 AM    292240  32580312     99.11    183384  20338208  19022076     54.40
    10:40:01 AM    136280  32736272     99.59    201608  20470692  19022072     54.40
    10:50:02 AM    311068  32561484     99.05    210036  20290640  19022044     54.40
    Average:       265691  32606861     99.19    139567  20725628  19049843     54.48
    09:00:01 AM kbswpfree kbswpused  %swpused  kbswpcad   %swpcad
    09:10:01 AM    298452   1798696     85.77     58476      3.25
    09:20:01 AM    298464   1798684     85.77     58476      3.25
    09:30:01 AM    298464   1798684     85.77     58476      3.25
    09:40:01 AM    296544   1800604     85.86     58348      3.24
    09:50:01 AM    290592   1806556     86.14     58440      3.23
    10:00:01 AM    289544   1807604     86.19     58116      3.22
    10:10:01 AM    290332   1806816     86.16     60124      3.33
    10:20:01 AM   1256076    841072     40.11     93152     11.08
    10:30:01 AM   1255320    841828     40.14     93496     11.11
    10:40:01 AM   1254740    842408     40.17     93548     11.10
    10:50:02 AM   1253020    844128     40.25     93784     11.11
    Average:       643777   1453371     69.30     71312      4.91
    ---------------------------
     Le 20
    ---------------------------
    Linux 2.6.32-431.29.2.el6.x86_64 (HVMP1509)     10/20/2016      _x86_64_        (8 CPU)
    09:00:01 AM     CPU     %user     %nice   %system   %iowait    %steal     %idle
    09:10:01 AM     all     15.39      0.85      1.45      1.02      0.00     81.29
    09:20:01 AM     all     16.48      0.00      0.99      1.03      0.00     81.50
    09:30:01 AM     all     12.41      0.00      1.08      0.99      0.00     85.51
    09:40:02 AM     all     12.49      0.00      1.05      0.81      0.00     85.66
    09:50:01 AM     all     12.05      0.00      0.99      0.76      0.00     86.20
    10:00:01 AM     all     12.36      0.00      1.04      0.75      0.00     85.85
    10:10:01 AM     all     11.89      0.00      1.03      1.20      0.00     85.88
    10:20:02 AM     all     11.84      0.00      1.00      0.75      0.00     86.42
    10:30:01 AM     all     13.38      0.00      1.13      0.77      0.00     84.72
    10:40:01 AM     all     13.57      0.00      1.17      0.76      0.00     84.50
    10:50:01 AM     all     15.42      0.00      1.24      0.81      0.00     82.53
    Average:        all     13.39      0.08      1.10      0.88      0.00     84.55
    09:00:01 AM  pswpin/s pswpout/s
    09:10:01 AM      0.19      0.05
    09:20:01 AM      0.39      0.00
    09:30:01 AM      0.08      0.79
    09:40:02 AM      0.09      0.19
    09:50:01 AM      0.00      0.28
    10:00:01 AM      0.00      0.23
    10:10:01 AM      0.01      0.25
    10:20:02 AM      0.00      0.26
    10:30:01 AM      0.00      0.22
    10:40:01 AM      0.00      0.33
    10:50:01 AM      0.00      0.42
    Average:         0.07      0.28
    09:00:01 AM kbmemfree kbmemused  %memused kbbuffers  kbcached  kbcommit   %commit
    09:10:01 AM    449936  32422616     98.63     43788  20000932  19018168     54.38
    09:20:01 AM    203072  32669480     99.38     73292  20217384  19018156     54.38
    09:30:01 AM    158904  32713648     99.52    104844  20229124  19018128     54.38
    09:40:02 AM    137276  32735276     99.58    120940  20233156  19018128     54.38
    09:50:01 AM    136096  32736456     99.59    133436  20225648  19018128     54.38
    10:00:01 AM    173368  32699184     99.47    145904  20177664  19018132     54.38
    10:10:01 AM    135512  32737040     99.59    169808  20189536  19018120     54.38
    10:20:02 AM    154820  32717732     99.53    180640  20165360  19018152     54.38
    10:30:01 AM    178324  32694228     99.46    189004  20133076  19018156     54.38
    10:40:01 AM    159396  32713156     99.52    197744  20142368  19018160     54.38
    10:50:01 AM    171380  32701172     99.48    210940  20118624  19018128     54.38
    Average:       187099  32685453     99.43    142758  20166625  19018141     54.38
    09:00:01 AM kbswpfree kbswpused  %swpused  kbswpcad   %swpcad
    09:10:01 AM   1194036    903112     43.06     84020      9.30
    09:20:01 AM   1194408    902740     43.05     84576      9.37
    09:30:01 AM   1192520    904628     43.14     84144      9.30
    09:40:02 AM   1192068    905080     43.16     84376      9.32
    09:50:01 AM   1191400    905748     43.19     84324      9.31
    10:00:01 AM   1190844    906304     43.22     84300      9.30
    10:10:01 AM   1190256    906892     43.24     84376      9.30
    10:20:02 AM   1189656    907492     43.27     84344      9.29
    10:30:01 AM   1189120    908028     43.30     84600      9.32
    10:40:01 AM   1188336    908812     43.34     84348      9.28
    10:50:01 AM   1187344    909804     43.38     84472      9.28
    Average:      1190908    906240     43.21     84352      9.31
  • Version moins verbeuse pour une analyse rapide dès lors que vous connaissez déjà votre serveur :
    for J in 19 20
    do
       echo -e "\n---------------------------\n Le $J \n---------------------------"
       sar -WS -f /var/log/sa/sa"$J"
    done | less
  • Version pour balayer tous les fichiers logs de sar :
    cd /var/log/sa/   ;   for J in $(ls -tr sa[0-9][0-9])
    do
       echo -e "\n---------------------------\n Le $J \n---------------------------"
       sar -WS -f "$J"
    done | less

Pour compléter votre analyse, vous trouverez ci-dessous quelques commandes et options utiles.

  • En premier lieu, et c’est de loin la commande la plus importante pour vérifier l’utilisation de la mémoire du système, car un problème de swap est avant tout un manque de mémoire détecté par votre noyau :
    $ cat /proc/meminfo

    Chaque ligne du résultat a son importance qui dépend de votre niveau d’expérience du système Linux. Ce résultat vient compléter celui de la commande free citée en tête de cet article.
    Si la valeur du cache du système vous semble trop importante et vous perturbe, il est possible de libérer la plus grande de cette partie de la mémoire en toute sécurité par la commande suivante avant d’observer à nouveau le comportement de votre système (mais en général un DBA n’aime pas trop libérer la mémoire cache 😉 ).
    Opération sans risque car elle ne libère pas la mémoire cache en cours d’utilisation par des process, juste celle inutilisée.

    # sync && sync && echo 1 > /proc/sys/vm/drop_caches
  • Le système conserve aussi des statistiques globales. Ce sont des totaux depuis le dernier boot du serveur.
    Se faire une idée par deux prises de valeurs à une journée ou une heure d’intervalle :

    $ vmstat -s

    Observez les lignes "pages paged in", « pages paged out« , « pages swapped in » et « pages swapped out » qui font ressortir plus clairement les notions de pagination et de swap.
    Lorsqu’une commande renvoie un nombre de pages mémoire, il vous faut multiplier cette valeur le plus souvent par 4 pour obtenir un résultat en Ko. La commande getconf vous donnera la taille d’une page mémoire :

    $ getconf PAGE_SIZE  # renvoie la taille en octet d'une page mémoire
  • Vous pouvez observer l’activité en temps réel des pages mémoire qui entrent et sortent de votre espace de swap :
    Activité de pagination par seconde sur 7 secondes

    $ sar -W 1 7
  • ou encore par l’intermédiaire de la commande vmstat qui donne aussi l’activité globale de votre système (plus difficile à interpréter si vous découvrez cette commande)
    $ vmstat -w 1 7   # certains se demandent encore pourquoi -w n'est pas par défaut
  • et même si ce n’est pas directement lié à l’activité mémoire, il peut être judicieux d’observer l’activité disque par process (du même genre que la commande top mais dédiée aux I/O)
    $ iotop -o -d 0.5   # rafraîchir toutes les demi-secondes (utiliser ensuite les flèches droite et gauche)

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

2. Déterminer quels sont les process affectés

Le but est de vérifier si ce sont bien les bases MySQL qui sont à l’origine de la consommation de la mémoire puis d’éviter que les démons mysqld deviennent candidats au kill du Killer OOM (Out Of Memory), et plus particulièrement pour les bases de production.
Le Killer OOM est une fonction du noyau Linux chargée d’arrêter un ou plusieurs process lorsque le système vient à manquer cruellement de mémoire.
Le plus souvent le Killer OOM choisira le process le plus consommateur de RAM (ce n’est pas le seul critère pris en compte mais souvent le plus déterminant dans la consommations des ressources).
La trace de son passage se retrouvera dans les logs du noyau par un message du type :

horodate kernel: Out of memory: Kill process 3441 (mysqld) score 433 or sacrifice child
  • Pour analyser l’occupation de l’espace de swap par les 7 process les plus gourmands :
    # printf "SwapUsed\tName\tTgid Pid\n"
    for file in /proc/[0-9][0-9]*/status
    do
       awk 'BEGIN     { pname=" " ; tgid=0 ; pid=0 }
            /Name/    { pname=$2 }
            /Tgid/    { tgid=$2 }
            /PidName/ { pid=$2 }
            $1 ~ /^VmSwap.*/ { print $2,$3 "\t" pname, tgid }
           ' "$file"
    done | sort -n | tail -7  ;  printf "SwapUsed\tName\tTgid Pid\n"
    SwapUsed   Name    Tgid Pid
    2728 kB    vmtoolsd 47721
    3232 kB    tuned 2684
    4096 kB    mysqld 37719
    5736 kB    munin-node 14059
    18360 kB   miniserv.pl 44877
    26564 kB   mysqld 35777
    901208 kB  mysqld 50045
    SwapUsed   Name    Tgid Pid
  • Pour déterminer l’occupation de l’espace de swap par l’ensemble des process démons mysqld (en s’appuyant sur smaps plutôt que status ci-dessus) :
    [mysql@mysqlserver ~]$
    for pid in  $(pidof mysqld)
    do
       awk 'BEGIN     { swap_used=0 }
            /^Swap:/  { swap_used+=$2 }
            END       { print "Occupation du swap par les process mysqld = " swap_used " kB" }
           ' /proc/$pid/smaps
    done
    Occupation du swap par les process mysqld = 185748 kB
  • La commande top permet aussi de lister les process occupant l’espace de swap.
    Dans top, taper « O » (o majuscule), puis « p » pour trier sur la colonne swap qui sera ajoutée automatiquement dans votre affichage. Et n’oubliez pas de lire les premières lignes en haut de l’écran qui sont un petit récapitulatif de meminfo et vmstat 🙂
    Pour une utilisation classique de la commande (sans ce tri sur la colonne swap), il est judicieux de faire varier la fréquence de rafraîchissement, d’autant qu’il est possible de descendre en dessous de la seconde : taper « s » puis « 0.5 » pour une demi-seconde. Une période de plusieurs secondes donnera un résultat différent dans le classement des process car l’analyse par le système est différente.
    Je suis certain que maintenant vous n’utiliserez plus top comme avant.

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

3. Vérifier la configuration des bases MySQL

Le but est d’ajuster les paramètres de configuration de chacune des bases en fonction de l’activité des bases, de leur rôle et des choix de répartition des ressources mémoire du serveur.
Certains paramètres ont un impact direct sur la consommation mémoire. Il est bon de les passer en revue et éventuellement de les réajuster. La plupart des paramètres peuvent être ajustés à chaud en version 5.7 de MySQL.
Pour les bases InnoDB, il est recommandé de ne pas utiliser le cache fichier de Linux. Pour cela, ajuster le paramètre innodb_flush_method=O_DIRECT.
Ci-dessous vous trouverez la liste des paramètres qui ont le plus d’impact sur la consommation mémoire :

  • innodb_buffer_pool_size
    Attention : 5 à 10% seront probablement alloués en sus. Surallocation nécessaire pour les structures des métadonnées, voire plus si vous utilisez la compression ou encore si la taille d’une page est inférieure à 16 Ko (cf. innodb_page_size)
    Par exemple 12 Go est souvent une valeur optimale pour 16 Go de RAM.
  • innodb_additional_mem_pool_size
  • innodb_log_buffer_size
  • key_buffer_size
  • buffer MyISAM similaire à innodb_buffer_pool
  • query_cache_size
  • innodb_ft_total_cache_size
    cache pour les index de recherche FULL TEXT, par défaut 610 Mo.
  • table_open_cache
    table_definition_cache
    open_files_limit
  • performance_schema
    à passer à off
  • max_connections
    à limiter autant que faire se peut
  • tmp_table_size
    max_heap_table_size
    Les deux sont liées aux allocations des tables temporaires qui ont aussi un impact non négligeable sur les ressources mémoire, et qui dépendent du nombre de connexions
  • read_buffer_size
    sort_buffer_size

    dépendent du nombre de sessions !
  • Exemples de commandes pour lister la configuration actuelle :
    mysql> SHOW GLOBAL STATUS LIKE 'innodb_buffer_pool%';
    mysql> SHOW GLOBAL VARIABLES LIKE 'innodb_buffer_pool_%';
    mysql> SHOW GLOBAL VARIABLES LIKE 'innodb_additional%';
    mysql> SHOW GLOBAL VARIABLES LIKE 'innodb_log_%';
    mysql> SHOW GLOBAL VARIABLES LIKE '%_size';
    mysql> SHOW GLOBAL VARIABLES LIKE 'max_connect%';
    mysql> SHOW GLOBAL STATUS LIKE   '%CONNEC%';
    mysql> SHOW GLOBAL VARIABLES LIKE 'table%';
    mysql> SELECT @@innodb_buffer_pool_size/1024/1024/1024;
    mysql> SELECT @@innodb_buffer_pool_instances;
    mysql> SELECT @@innodb_buffer_pool_chunk_size;
    mysql> SHOW GLOBAL VARIABLES LIKE "%table%cache%";
    mysql> SHOW GLOBAL VARIABLES LIKE "%open_file%";
    mysql> SHOW VARIABLES LIKE '%query_cache%';
    mysql> SHOW GLOBAL VARIABLES LIKE 'innodb_ft_total_cache_size';
    mysql> SHOW full processlist ;
    mysql> SHOW ENGINE PERFORMANCE_SCHEMA STATUS ;
    mysql> SHOW ENGINE INNODB STATUS ;
    mysql> SHOW GLOBAL VARIABLES LIKE "performance_schema%" ;
    mysql> SHOW GLOBAL STATUS LIKE "performance_schema%";

    Si performance_schema est positionné à ON alors ce dernier show sur performance_schema% devrait ramener 0 sur toutes les lignes.
    Pour éviter une dégradation des performances de votre moteur MySQL, il est conseillé d’avoir un nombre de fragments « chunk » qui n’excède pas 1000, c’est-à-dire le rapport entre innodb_buffer_pool_size et innodb_buffer_pool_chunk_size.

  • Pour évaluer l’allocation possible de la mémoire par un serveur MySQL à partir du nombre de sessions maximal :
    SELECT (  @@key_buffer_size
            + @@query_cache_size
            + @@innodb_buffer_pool_size
            + @@innodb_additional_mem_pool_size
            + @@innodb_log_buffer_size
            + @@max_connections * ( @@read_buffer_size + @@read_rnd_buffer_size + @@sort_buffer_size + @@join_buffer_size + @@binlog_cache_size + @@thread_stack+ @@tmp_table_size )
           ) / (1024 * 1024 ) AS MAX_MEMORY_Mo
    ;
  • La taille du buffer cache du moteur MySQL peut être ajustée à chaud. Mais notez toutefois que cela ne sera effectif que lorsque toutes les transactions et opérations en cours provenant des API InnoDB seront terminées.
    mysql> SET GLOBAL innodb_buffer_pool_size=402653184;
  • Pour comptabiliser le nombre de tables InnoDB, et obtenir quelques statistiques sur l’utilisation de la mémoire :
    [mysql@mysqlserver ~]$ mysql -e "SELECT concat(TABLE_SCHEMA, '.', TABLE_NAME) FROM information_schema.tables WHERE engine = 'InnoDB'"
    mysql> use performance_schema
    mysql> UPDATE setup_instruments SET ENABLED='YES' WHERE NAME LIKE 'memory/%';
    mysql> SELECT * FROM memory_summary_global_by_event_name
           WHERE EVENT_NAME = 'memory/sql/TABLE'\G

Évidemment, il est beaucoup plus simple de procéder à ces réglages lorsque le serveur (la VM) n’héberge qu’une seule base. Si plusieurs bases sont mutualisées, tout est alors bien plus complexe. Dans ce cas, la première recommandation est d’éviter d’installer autre chose que des bases de production sur un serveur de production, et de faire preuve de bon sens 🙂
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

4. Limiter l’activité de swapping

Vérification faite de la configuration de vos bases MySQL, vous souhaitez exploiter au plus juste la RAM disponible.
Vous avez noté par ailleurs que votre serveur subit très occasionnellement et de manière très brève un surcroît d’activité qui entraîne un petit surcoût d’allocation mémoire et approche la limite disponible. C’est d’ailleurs cette situation qui a déclenché l’activité de swapping : votre Linux est inquiet et commence à anticiper le manque de RAM car vous flirtez en permanence avec les 90% d’occupation de la RAM.
Vous en avez bien conscience et vous ne pouvez ou ne souhaitez pas pour l’instant ajouter de la RAM à votre VM/serveur (ce serait pourtant la meilleure solution si vous ne pouvez réduire votre configuration à l’étape précédente).
Votre souhait est maintenant de rassurer votre noyau et ainsi de limiter la pagination.
Le choix du déclenchement de cette pagination par le noyau peut être induit par le paramètre swappiness (une valeur proche de 0 freine ce déclenchement).

  • Par exemple pour modifier la sensibilité du noyau jusqu’au prochain reboot :
    [root@mysqlserver ~]# cat /proc/sys/vm/swappiness
    30
    [root@mysqlserver ~]# sysctl vm.swappiness=10
    [root@mysqlserver ~]# cat /proc/sys/vm/swappiness
    10

    Vous pouvez descendre jusqu’à 1 mais évitez 0.

  • Pour prise en compte lors des prochains reboot, il vous faut modifier la valeur du paramètre vm.swappiness dans /etc/sysctl.conf puis pour demander au système de relire le fichier, lancer sysctl -p
  • Pour libérer l’espace de swap (càd recharger les pages en mémoire) et vérifier le nouveau comportement du noyau (comme suite à un reboot), utilisez les commandes swapoff et swapon.
    Ce sera utile par exemple dans le cas où le swap est plein à 100% depuis plusieurs jours :

    # swapoff -a  &&  swapon -a
  • La valeur 0 du paramètre noyau panic_on_oom indique que le système fera appel à sa fonction killer OOM lors d’un manque total de mémoire pour ainsi ne pas impacter la majeure partie des autres process.
    Le paramètre noyau overcommit_memory a aussi une incidence sur le comportement du noyau en cas de surcharge. Disons que si votre espace de swap est au moins équivalent à votre RAM, vous pourriez envisager de passer sa valeur à 2 sachant que la valeur par défaut est 0. Cette dernière correspond à un algorithme avec un bon compromis dans la plupart des cas.
    Personnellement, je n’aime pas voir un serveur « swapper » plusieurs gigas de RAM. Les serveurs récents sont souvent dotés de dizaines de Go de RAM. Il me semble de bon sens de ne pas systématiquement affecter autant de swap que de RAM malgré ce que recommande certains éditeurs. Pour preuve les changements des pré-requis d’Oracle tout au long des releases de sa version 11.
    Le paramètre noyau overcommit_ratio n’est fonctionnel que lorsque la valeur 2 est affectée au paramètre overcommit_memory. Il correspond au pourcentage de RAM à prendre en compte.

    # cat /proc/sys/vm/overcommit_memory
    # cat /proc/sys/vm/panic_on_oom
    0

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

5. Protéger au mieux les bases MySQL de production

Le but est de privilégier la disponibilité des bases de production sur des serveurs qui hébergent aussi des bases de recettes, de développement ou de formation.
Comme pour les bases Oracle, les bases MySQL savent exploiter les HugePages aussi bien pour le cache principal que pour les zones de mémoire supplémentaires.
Plusieurs articles de ce blog traitent déjà de ce sujet en détail. En résumé, il est tout d’abord nécessaire de procéder à la configuration du système comme par exemple la déclaration du nombre de pages à allouer par le noyau linux en tant que HugePages, puis à paramétrer MySQL en conséquent :

  • Pour la configuration du système, les paramètres devront être ajoutés dans le fichier /etc/sysctl.conf
    Les exemples ci-dessous sont tous basés sur un objectif d’affectation de 1 Go de HugePages.

    # echo "vm.nr_hugepages=512"   >> /etc/sysctl.conf   # 512 pages de 2 Mo, soit 1 Go
    # echo "vm.hugetlb_shm_group=500"  >> /etc/sysctl.conf   # remplacer 500 par votre n° de groupe mysql, càd le résultat de id -g mysql

    Ajustez éventuellement la configuration de la mémoire partagée grâce aux paramètres kernel.shmmax (valeur en octets) et kernel.shmall (valeur en nombre de pages).
    Pour demander au système de relire le fichier, utilisez la commande sysctl.
    Les modifications peuvent être ensuite visualisées dans /proc/meminfo.

    sysctl -p
    cat /proc/meminfo
  • Vérifiez et ajustez aussi les limites systèmes dans le fichier /etc/security/limits.conf » (ou /etc/security/limits.d/99-mysql-limits.conf en RHEL 7), puis vérifiez avec ulimit -l
    Par exemple :

    mysql  soft  memlock  1048576
    mysql  hard  memlock  1048576   # soit 512 * 2048
  • Si des Transparent HugePages ont été allouées (cf. grep AnonHugePages /proc/meminfo), il est préférable de les désactiver soit en ajoutant le paramètre transparent_hugepage=never dans le fichier grub.conf à la fin de la ligne du lancement du noyau (avant le chargement du ramdisque initrd), soit par un script d’init contenant les commandes suivantes (en RHEL 7 n’oubliez pas de déclarer le script dans SElinux) :
    echo never  > /sys/kernel/mm/transparent_hugepage/enabled
    echo never  > /sys/kernel/mm/transparent_hugepage/defrag
  • Pour activer l’emploi des HugePages disponibles à MySQL, lancer le serveur avec l’option --large-pages, ou ajouter les lignes suivantes dans votre fichier de configuration my.cnf :
    [mysqld]
    large-pages

    Si le serveur InnoDB ne peut allouer sa mémoire dans les HugePages, il se contentera des pages mémoire traditionnelles et tracera un warning dans le log d’erreurs du type « Warning: Using conventional memory pool ».

En sus de la configuration des HugePages, il est possible d’indiquer au noyau que tel process est plus important à vos yeux que les autres.

  • Pour qu’un process démon mysqld soit moins candidat au kill du Killer OOM il suffira d’ajuster la valeur du oom_score_adj qui lui est propre a une valeur basse.
    Pour lister la valeur actuelle du paramètre (0 par défaut) pour chacune des bases MySQL :

    for pid in  $(pidof mysqld)
    do
       echo -e "\n oom_score_adj de mysqld pid $pid = \c"
       cat  /proc/"$pid"/oom_score_adj
    done

    Les valeurs acceptées vont de -1000 à +1000. Une valeur négative va réduire les chances que le process soit élu par le Killer OOM le cas échéant. Toutefois considérez que ce n’est qu’une indication et que le noyau pourra tout de même en juger autrement.
    Exemple d’affectation de la valeur négative -800 à votre process préféré, et constat du résultat du score à une potentielle élection :

    cat /proc/"$pid_mysqld_prod"/oom_score
    24
    echo '-800'  >/proc/"$pid_mysqld_prod"/oom_score_adj
    cat /proc/"$pid_mysqld_prod"/oom_score
    2

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

6. Point particulier : les architectures NUMA

Dernier point à prendre en compte : les architectures NUMA peuvent aussi être à l’origine du déclenchement d’une activité de swapping en cas de manque de mémoire d’un des nœuds NUMA.
La commande numactl permet de retrouver le nombre de nœuds et leur numéro (0, 1 …), la mémoire affectée à chaque nœud, ainsi que la « distance » entre les nœuds (càd le coût d’accès à chaque zone de mémoire).
Par exemple, la fin du résultat de la commande suivante, à la suite de « node distances« , fait apparaître un petit tableau qui présente le coût d’accès d’une zone de mémoire pour chacun des nœuds. Dans cet exemple, il est possible d’en déduire que pour chacun des nœuds, l’accès à la mémoire dédiée à l’autre nœud présente un surcoût du simple au double.

$ numactl --hardware
available: 2 nodes (0-1)
node 0 cpus: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
node 0 size: 262010 MB
node 0 free: 68707 MB
node 1 cpus: 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
node 1 size: 262144 MB
node 1 free: 2666 MB
node distances:
node   0   1
  0:  10  21
  1:  21  10

La consultation alors de /proc/$(pidof mysqld)/numa_maps permet de lister l’allocation mémoire d’un process et de constater de sa répartition entre chaque zone mémoire.
Sur un serveur hébergeant un grand nombre de bases Oracle, le grep suivant a permis de faire ressortir uniquement les process pour lesquels les valeurs sont problématiques :

for process in  /proc/[0-9][0-9]*
do
  if  egrep "N0=[0-9][0-9][0-9][0-9][0-9][0-9]* .*N1=[0-9][0-9][0-9][0-9][0-9][0-9]*" $process/numa_maps  2>/dev/null
  then
      echo -e "$process   $(cat $process/cmdline) \n"
  fi
done

Par exemple, notez les valeurs retournées pour N0 et N1 ci-dessous :

e9600000 default anon=68096 dirty=68096 active=68095 N0=31604 N1=36492
/proc/140963   /u01/app/oracle/product/agent12c/core/12.1.0.5.0/jdk/bin/java-Xmx303M-XX:MaxPermSize=96M-server-Djava.security.egd=file:///dev/./urandom-Dsun.lang.ClassLoader.allowArraySyntax=true-XX: +UseLinuxPosixThreadCPUClocks-XX:...

Pour s’affranchir de cette problématique de mémoire dédiée à chaque nœud, il suffit d’ajouter le paramètre suivant à votre fichier de configuration MySQL :

innodb_numa_interleave=1   # version 5.6 minimum

Si votre version de MySQL est antérieure à la 5.6, il vous faudra plutôt lancer votre serveur avec la commande suivante :

# numactl --interleave=all

Ou encore prévoir une section mysqld_safe dans votre my.cnf qui fera mention par exemple d’un script mysqld_numa comme ci-dessous :

[mysqld_safe]
# wrapper mysqld
mysqld=mysqld_numa

Script mysqld_numa.sh :

#!/bin/sh
numactl=/usr/bin/numactl
mysqld=/usr/sbin/mysqld   # MAJ le chemin d’accès du démon
exec "$numactl" --interleave=all  "$mysqld"  "$@"

Dernière recommandation commune à bien d’autres SGBD sous Linux, ajouter la déclaration suivante dans /etc/sysctl.conf

vm.zone_reclaim_mode = 0