Attention au query cache

février 28, 2012

Selon le livre «Audit & optimisation, MySQL 5 – éditions Eyrolles», le cache de requêtes ou query cache est un système de cache mémoire interne à MySQL, transparent pour l’application, qui ne stocke que les requêtes SELECT et leurs résultats.

L’apport de ce cache est particulièrement dépendant de votre application. Il est coutume de dire qu’il est (très) pénalisant dans des environnements où les requêtes d’écritures sont nombreuses, notamment à cause de son mécanisme d’invalidation (et de problèmes de contentions de façon générale).

A l’opposé, il peut être intéressant de l’activer, dans des environnements à forte charges de lectures, si les mêmes requêtes reviennent très fréquemment, plus particulièrement lors de l’utilisation de tables MyISAM.

 

Cependant, un environnement à forte charge en lecture n’est pas une condition suffisante pour s’assurer de bonne performances avec le query cache, c’est ce que nous allons voir dans cet article.

 

Le contexte

L’idée principale de cet article, n’est pas de voir ce que MySQL « à dans le ventre », mais de connaître le coût du cache de requêtes, avec un hit ratio faible, dans une configuration «relativement proche» de ce que nous avons en production chez Viadeo.

J’ai utilisé sysbench pour générer 100 connexions simultanées, qui exécutent des SELECT sur une table InnoDB de 10 millions d’enregistrements :

sysbench –num-threads=100 –max-time=900 –max-requests=20000000 –test=oltp –oltp-table-size=20000000 –mysql-table-engine=innodb –oltp-test-mode=complex –oltp-read-only run

 

Les détails du bench sont dans le paragraphe divers, au bas de l’article.

 

Résumé

Avec un query cache hit très bas, même dans un contexte 100% SELECT, l’impact du cache de requêtes est désastreux sur les performances, que ce soit en TPS (graph 1) ou en 95ème centile (graph 2).

 

dasini.net - MySQL query cache

 

Les chiffres

— Query cache à 128 Mo

SHOW GLOBAL VARIABLES LIKE 'query_cache%';
+------------------------------+-----------+
| Variable_name | Value |
+------------------------------+-----------+
| query_cache_limit | 2097152 |
| query_cache_min_res_unit | 4096 |
| query_cache_size | 134217728 |
| query_cache_strip_comments | OFF |
| query_cache_type | ON |
| query_cache_wlock_invalidate | OFF |
+------------------------------+-----------+

OLTP test statistics:
queries performed:
read: 2728040
write: 0
other: 389720
total: 3117760
transactions: 194860 (216.45 per sec.)
deadlocks: 0 (0.00 per sec.)
read/write requests: 2728040 (3030.30 per sec.)
other operations: 389720 (432.90 per sec.)

Test execution summary:
total time: 900.2544s
total number of events: 194860
total time taken by event execution: 90014.3415
per-request statistics:
min: 40.23ms
avg: 461.94ms
max: 1276.36ms
approx. 95 percentile: 666.36ms

Threads fairness:
events (avg/stddev): 1948.6000/10.21
execution time (avg/stddev): 900.1434/0.08

Chiffres clés :

  • QC size : 128 Mo
  • TPS : 216,45
  • 95%tile : 666,36 ms

 

 

— Query cache à 256 Mo

SET GLOBAL query_cache_type=1; 
SET GLOBAL query_cache_size=268435456; 
SHOW GLOBAL VARIABLES LIKE 'query_cache%';
+------------------------------+-----------+
| Variable_name | Value |
+------------------------------+-----------+
| query_cache_limit | 2097152 |
| query_cache_min_res_unit | 4096 |
| query_cache_size | 268435456 |
| query_cache_type | ON |
| query_cache_wlock_invalidate | OFF |
+------------------------------+-----------+

OLTP test statistics:
queries performed:
read: 5601204
write: 0
other: 800172
total: 6401376
transactions: 400086 (444.47 per sec.)
deadlocks: 0 (0.00 per sec.)
read/write requests: 5601204 (6222.60 per sec.)
other operations: 800172 (888.94 per sec.)

Test execution summary:
total time: 900.1385s
total number of events: 400086
total time taken by event execution: 90006.3589
per-request statistics:
min: 33.56ms
avg: 224.97ms
max: 601.63ms
approx. 95 percentile: 320.52ms

Threads fairness:
events (avg/stddev): 4000.8600/11.78
execution time (avg/stddev): 900.0636/0.04

Chiffres clés :

  • QC size : 256 Mo
  • TPS : 444,47
  • 95%tile : 320,52 ms

 

 

— Query cache à 64 Mo

SET GLOBAL query_cache_type=1; 
SET GLOBAL query_cache_size=67108864; 
SHOW GLOBAL VARIABLES LIKE 'query_cache%';
+------------------------------+----------+
| Variable_name | Value |
+------------------------------+----------+
| query_cache_limit | 2097152 |
| query_cache_min_res_unit | 4096 |
| query_cache_size | 67108864 |
| query_cache_type | ON |
| query_cache_wlock_invalidate | OFF |
+------------------------------+----------+
OLTP test statistics:
queries performed:
read: 7319270
write: 0
other: 1045610
total: 8364880
transactions: 522805 (580.84 per sec.)
deadlocks: 0 (0.00 per sec.)
read/write requests: 7319270 (8131.73 per sec.)
other operations: 1045610 (1161.68 per sec.)
Test execution summary:
total time: 900.0881s
total number of events: 522805
total time taken by event execution: 90002.3195
per-request statistics:
min: 35.97ms
avg: 172.15ms
max: 380.79ms
approx. 95 percentile: 214.74ms
Threads fairness:
events (avg/stddev): 5228.0500/16.33
execution time (avg/stddev): 900.0232/0.02

Chiffres clés :

  • QC size : 64 Mo
  • TPS : 580,84
  • 95%tile : 214,74 ms

 

 

— Query cache OFF à 0 Mo

mysql> SET GLOBAL query_cache_type=0; 
SET GLOBAL query_cache_size=0; 
SHOW GLOBAL VARIABLES LIKE 'query_cache%';
+------------------------------+---------+
| Variable_name | Value |
+------------------------------+---------+
| query_cache_limit | 2097152 |
| query_cache_min_res_unit | 4096 |
| query_cache_size | 0 |
| query_cache_strip_comments | OFF |
| query_cache_type | OFF |
| query_cache_wlock_invalidate | OFF |
+------------------------------+---------+

OLTP test statistics:
queries performed:
read: 60504052
write: 0
other: 8643436
total: 69147488
transactions: 4321718 (4801.84 per sec.)
deadlocks: 0 (0.00 per sec.)
read/write requests: 60504052 (67225.74 per sec.)
other operations: 8643436 (9603.68 per sec.)
Test execution summary:
total time: 900.0132s
total number of events: 4321718
total time taken by event execution: 89974.6206
per-request statistics:
min: 1.14ms
avg: 20.82ms
max: 466.96ms
approx. 95 percentile: 71.97ms
Threads fairness:
events (avg/stddev): 43217.1800/158.09
execution time (avg/stddev): 899.7462/0.01

 

Chiffres clés :

  • QC size : 0 Mo
  • TPS : 4801,84
  • 95%tile : 71,97 ms

 

 

Conclusion

 

Les requêtes créées par sysbench lors de ce test, génèrent un query cache hit ratio particulièrement faible < 10%

Ce dernier se calcul de la façon suivante : Qcache_hits / (Qcache_hits +Com_select)

 

Avec un QC hit ratio si bas, activer le QC à un impact désastreux sur les performances, même en lecture seule, est ce quelque soit sa taille (64 / 128 / 256 Mo ).

Lui mettre une taille beaucoup plus grande n’est pas une bonne idée (cf : « Audit & optimisation, MySQL 5 – éditions Eyrolles»).

 

Il est cependant possible de le paramétrer pour minimiser les pertes de performances (si il doit impérativement être activé). Dans le contexte de ce bench, la diminution du query_cache_min_res_unit, 512 au lieu de 4096 (car les résultat renvoyés sont très petit) associé à un query_cache_size de 256Mo à permit d’atteindre près de 2000 TPS (mieux que les 580 TPS obtenus avec 64Mo mais bien loin des 4800 TPS avec le QC désactivé).

 

A noter également qu’une diminution du nombre de connexions concurrentes n’inverse pas la tendance, même si l’écart diminue un peu (en d’autres termes plus il y a de connexions concurrentes, plus il y a de contentions (plutôt logique) !)

 

Une question que n’adresse pas ce test: A partir de quel pourcentage de hit ratio il est (serait) bénéfique d’activer le QC ?

J ‘essaierai d’y répondre dans un prochain article

 

Et vous, votre cache de requêtes vous veut il du bien ?

 

P.S. Les résultats sont (évidemment) les mêmes avec MariaDB 5.3.3 & Percona Server 5.5.19

 

 

Divers

Protocole

  • Start du serveur.
  • 2 run de 900 secondes
  • Je prend le meilleur des 2
  • Buffer pool suffisamment grand pour contenir toutes les données.

 

Serveur

  • Intel(R) Xeon(R) CPU X7542 @ 2.67GHz (x16)
  • RAM: 64 Go
  • OS: Ubuntu server

 

MySQL server version: 5.5.19 Percona Server with XtraDB (GPL), Release rel24.0, Revision 204

 

InnoDB configuration:

# InnoDB

  • innodb_file_per_table
  • innodb_buffer_pool_size = 8G
  • innodb_buffer_pool_instances = 2
  • innodb_log_file_size = 152428800
  • innodb_flush_log_at_trx_commit = 2
  • innodb_log_buffer_size = 8388608
  • innodb_support_xa = 1
  • innodb_thread_concurrency = 0
  • innodb_io_capacity = 200
  • innodb_doublewrite = 1
  • innodb_max_dirty_pages_pct = 75

 

Table information

CREATE TABLE `sbtest` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`k` int(10) unsigned NOT NULL DEFAULT '0',
`c` char(120) COLLATE utf8_swedish_ci NOT NULL DEFAULT '',
`pad` char(60) COLLATE utf8_swedish_ci NOT NULL DEFAULT '',
PRIMARY KEY (`id`),
KEY `k` (`k`)
) ENGINE=InnoDB AUTO_INCREMENT=10000001 DEFAULT CHARSET=utf8 COLLATE=utf8_swedish_ci

 

Name: sbtest
Engine: InnoDB
Version: 10
Row_format: Compact
Rows: 10000060
Avg_row_length: 224
Data_length: 2247098368
Max_data_length: 0
Index_length: 137019392
Data_free: 4194304
Auto_increment: 10000001
Create_time: 2012-02-10 13:15:11
Update_time: NULL
Check_time: NULL
Collation: utf8_swedish_ci
Checksum: NULL

 

 

Type de requêtes:

  • SELECT c from sbtest where id=9992481
  • SELECT DISTINCT c from sbtest where id between 9997686 and 9997786 order by c
  • SELECT c from sbtest where id=10068344
  • SELECT SUM(K) from sbtest where id between 10073907 and 10074006
  • SELECT c from sbtest where id between 9330963 and 9331062

 

Calcul du query cache hit ratio:

Qcache_hits / (Qcache_hits +Com_select) < 1%

show global status like 'qcache%';
+-------------------------+----------+
| Variable_name | Value |
+-------------------------+----------+
| Qcache_free_blocks | 21304 |
| Qcache_free_memory | 76320360 |
| Qcache_hits | 407451 |
| Qcache_inserts | 4745672 |
| Qcache_lowmem_prunes | 4691295 |
| Qcache_not_cached | 543 |
| Qcache_queries_in_cache | 54377 |
| Qcache_total_blocks | 130059 |
+-------------------------+----------+

 

mysql> show global status like 'com_sel%';
+---------------+---------+
| Variable_name | Value |
+---------------+---------+
| Com_select | 4746219 |
+---------------+---------+

 

select 407451/(407451+4746219);
+-------------------------+
| 407451/(407451+4746219) |
+-------------------------+
|                  0.0791 |
+-------------------------+

=> Taux de réussite du cache de requête est d'environ 8%

 

9 Responses to “Attention au query cache”

  1. A lire concernant les benchmarks

  2. A lire concernant les benchmarks : http://www.mysqlperformanceblog.com/2012/02/25/introducing-new-type-of-benchmark/

  3. Quel est le point de contention ici ?
    Il serait interessant de savoir pourquoi le QC une fois activé pose pb : CPU, I/O…

  4. Un article qui soulève beaucoup d’interrogation…
    Vous nous démontrez que le cache de requêtes est clairement pénalisant dans une application.

    Je trouve cela étrange …

  5. Cédric et MrSlayers,
    effectivement à première vue cela peut paraître étrange, et pour être honnête, je ne m’attendais pas à un écart aussi important dans ce contexte d’utilisation (only select).

    Dans l’inconscient collectif, cache est un mot magique en ce qui concerne les performances, oui mais…
    Malheureusement le Query cache de MySQL est protégé par un Mutex unique, ce qui explique la contention lors d’accès concurrent importants.
    Sans parler des autres inconvénients liés aux requêtes d’écritures ou à Innodb.

    Néanmoins, l’idée de cet article, n’est pas de dire que le query cache c’est le mal !!!
    Il est juste du devoir du DBA, de se poser la question.

  6. Je n’ai pas encore pu travailler sur la question que je me pose depuis ce bench:
    « A partir de quel pourcentage de hit ratio il est (serait) bénéfique d’activer le QC ? »
    Dans notre contexte de production, chez Viadeo, pour un QC hit ratio de 20%, avec 60% de read et 40% de write, la désactivation du QC sur un de nos slave lui à semble t’il permit (difficile à dire en fait) de prendre plus de requêtes de lecture que ces petits camarades.
    A confirmer…

  7. Sa baser que le hit ratio est rarement pertinent en soit, si la valeur est basse, c’est effectivement mauvais signe mais il faut surtout regarder le Hit/Insert et le Insert/Prune ratio (prune étant les requêtes retirées pour libérer de l’espace) qu’on peux récupérer directement depuis ‘mysqlreport’.

    Les deux doivent être supérieur à 1:1.
    Dans le cas d’un Hit/Insert ratio trop bas, cela signifie qu’il y à plus de nouvelles requêtes mises en cache que d’interrogations, ça peut être le cas par exemple si le cache n’est pas encore rempli (cache flushé il y à peu de temps ou serveur redémarré) ou peu de requêtes identiques.

    Quant au Insert/Prune, s’il est trop bas, cela peux signifier que le query_cache_size est mal calibré et/ou que les requêtes sont rarement identiques.

    Il faut également vérifier que la valeur du ‘Block fragmentation’ ne soit pas trop haute, une valeur au delà de 15-20% signifie que vous avez beaucoup de petites requêtes en cache et que le ‘query_cache_min_res_unit’ est trop élevé.

    Il est également habituellement recommandé de ne pas mettre une valeur de query_cache_size trop élevée pour des raisons de performance car il se base sur un seul mutex, ce qui est d’autant plus limitant sur les machines multi-cpu.

    Une valeur trop basse de query_cache_size fera que le cache passera son temps à remplacer les anciennes requêtes par des nouvelles (prune) et perdra également en efficacité.

    Dans le cas de serveurs SQL d’hébergements Web mutualisé (beaucoup de bases et de tables mais un petit nombre de requêtes sur chaque bases et pas de jointures entre bases), il est recommandé d’avoir plusieurs serveurs de taille moyenne (voire petite) qu’un gros afin de pouvoir mieux exploiter le query_cache (mais consomme fatalement un peu plus de RAM au total qu’un seul serveur/cache).

    Un fonctionnalité propre à XtraDB est ‘query_cache_strip_comments’ qui comme son nom l’indique retire les commentaires des requêtes mises en cache (et permet au cache d’être plus efficace) car par défaut, deux requêtes identiques mais avec un commentaire différent ne seront pas prises en compte comme étant identiques par le query_cache.

    Sur ces serveurs ayant en moyenne entre 1000 et 3000 bases, traitant en moyenne entre 500 et 2000 requêtes/sec, entre 65% et 90% de SELECT, ayant un query_cache_size de 128M, un ‘query_cache_min_res_unit’ situé entre 2048 et 4096 ; tous ont un hit rate du query_cache >95% et des ratio Hit/Insert et Insert/Prune > 1:1 (mais de grosses disparités de ce coté d’un serveur à l’autre par contre).

    Il est donc prudent et simple de bien régler en fonction de ses besoins le query_cache (ou désactiver en cas de trop grosse prod), cela peut être fait simplement en se basant sur les données fournies par ‘mysqlreport’ sans oublier de prendre en compte les ordre de grandeur des valeurs et non simplement les pourcentages (notamment prunes/sec).

  8. Très intéressant cette article.

    Une question comment obtiens t – on les statistiques sur OLTP et en particulier le temps moyen d’ execution sur percentiles ?

    OLTP test statistics:

    queries performed:

    read: 60504052

    write: 0

    other: 8643436

    total: 69147488

    transactions: 4321718 (4801.84 per sec.)

    deadlocks: 0 (0.00 per sec.)

    read/write requests: 60504052 (67225.74 per sec.)

    other operations: 8643436 (9603.68 per sec.)

    Test execution summary:

    total time: 900.0132s

    total number of events: 4321718

    total time taken by event execution: 89974.6206

    per-request statistics:

    min: 1.14ms

    avg: 20.82ms

    max: 466.96ms

    approx. 95 percentile: 71.97ms

    OLTP test statistics:

  9. Bonjour,
    voici l’article Wikipedia, tu devrai trouver toutes les infos necessaires.

    Olivier