Node.js : compression du cache avec Brotli dans Redis ?

Redis est devenu au fil des ans un incontournable pour le stockage de données en cache, notamment grâce à ses performances et les structures de données disponibles (en comparaison avec Memcached par exemple).

A l'instar de Magento, il est régulièrement utilisé pour du Full-Page Cache (FPC), où l'on met en cache l'intégralité du HTML d'une page web afin d'optimiser au maximum les temps de réponse, cruciaux pour l'expérience utilisateur.

Ces pages peuvent peser assez lourd, parfois plusieurs centaines de kilo-octets. Lors de pics de charge sur un projet, nous rencontrions alors 2 problématiques :

  • Saturation de la bande passante entre Redis et les frontaux web (application Node.js)
  • Impossibilité de stocker toutes les pages en cache : diminution du cache-hit ratio final et beaucoup d'évictions (en LRU)

Nous avons donc mis en place un mécanisme de compression/décompression de ces pages avec l'algorithme de compression Brotli créé par Google, et implémenté dans le module zlib de Node.js depuis la version 10.16.0.

Ce module offre la possibilité de définir le niveau de compression Brotli, de 0 (peu coûteux en CPU, compression normale) à 11 (très coûteux en CPU, compression maximale), avec un niveau par défaut à 11, le maximum donc.

Le taux de compression avec Brotli, quel que soit le niveau, a été immédiatement significatif : d'un facteur 8 à 10 (environ -80 à -90% de taille).

Nous sommes partis sur le niveau de compression minimal, soit 0, afin de limiter l'impact sur la consommation CPU, qui devient alors très faible en comparaison avec des niveaux plus élevés.

Le résultat parle de lui-même sur ce graphique de bande passante Redis (en MB/s):

Première barre verticale : mise en place de la compression Brotli en qualité 4
Deuxième barre verticale : passage au niveau de qualité 0

✅  Le gain est significatif, et l'intégralité des pages peuvent désormais être mises en cache, avec une utilisation totale de mémoire bien amoindrie.  

L'implémentation niveau code est semblable à ce qui suit :

const zlib = require('zlib')
const redis = require('ioredis')

// Compress data
value = 'my html page'
try {
  let compressedValue = zlib.brotliCompressSync(value, { params: {
    [zlib.constants.BROTLI_PARAM_QUALITY]: compressionQuality
  }})
  value = `BR:${compressedValue.toString('base64')}`
} catch (e) {
  console.log(`Unable to deflate Redis data: ${e}`)
}
redis.set(`my-page-key`, value)

// Decompress data
result = redis.get('my-page-key')
try {
  result = zlib.brotliDecompressSync(new Buffer(result.substr(2), 'base64'));
} catch (e) {
  console.log(`Unable to inflate Redis data: ${e}`);
}

Les exemples présentés ici utilisent des méthodes synchrones, il est également possible d'utiliser les méthodes asynchrones du module zlib. L'implémentation pourrait également être réalisée de manière globale en ajoutant des transformers à la librairie ioredis que nous utilisons : https://github.com/luin/ioredis#transforming-arguments--replies.

 

Crédits :