Synchronisation du cache Redis pour le service Go


Présentation


Pendant le raffinement d'un projet, il est devenu nécessaire de mettre en cache les données fréquemment demandées. L'implémentation de la mise en cache est possible de différentes manières, mais je voulais l'implémenter avec un minimum de modifications par rapport au projet d'origine. Le résultat, ses avantages et ses inconvénients sont décrits ci-dessous.


Comment était tout?


Initialement, pour chaque requête contenant l'identifiant de l'objet demandé, une requête a été exécutée dans la base de données PostgreSQL (DB). Plus précisément, plusieurs requêtes, car pour former une réponse complète, il fallait appliquer à plusieurs tables de base de données. À la suite du traitement des demandes, un objet assez complexe a été formé, dont certains domaines sont représentés par des interfaces. En mémoire, cet objet occupe environ 250 ko.


Les performances avec cette implémentation n'étaient pas excellentes, pas plus de 3500 RPS (demande par seconde) lors de la demande des mêmes données avec 1000 threads concurrents.


La question s'est immédiatement posée, mais comment augmenter le RPS: changer de routeur, optimiser la base de données, mettre en cache les données? Le routeur a été assez bien utilisé ( github.com/julienschmidt/httprouter ), et le remplacement du routeur dans un grand projet prendra beaucoup de temps et il y a un risque élevé de rupture de quelque chose. Pour optimiser le travail avec la base de données, vous devrez également réécrire une partie substantielle du code (maintenant github.com/jmoiron/sqlx est utilisé). De toute évidence, la mise en cache est le moyen le plus optimal d'augmenter le RPS.


Solution simple


La chose la plus simple qui me vient à l'esprit est l'utilisation d'un cache en mémoire. Lors de l'utilisation d'un tel cache, environ 20 000 RPS ont été obtenus. Les performances du cache en mémoire sont excellentes, mais vous ne pouvez pas utiliser un tel cache avec de nombreuses instances de service. Vous ne savez jamais vers quelle instance du service une demande sera envoyée, et il peut y avoir des demandes non seulement pour recevoir des données, mais aussi pour supprimer / mettre à jour.


Les performances obtenues avec le cache en mémoire ont été prises comme standard dans une nouvelle recherche de solution.


Idée, mauvaise idée


Est-il possible de mettre le résultat de la requête tel qu'il est dans la base de données NoSQL Redis? Il s'agit d'une solution typique pour la mise en cache des demandes de réponse. Les données sont stockées en mémoire, lorsque vous utilisez plusieurs instances du service, toutes peuvent utiliser un cache commun. Cette solution a été rapidement mise en œuvre. Et les tests ont montré ... Et les tests ont montré que les performances n'avaient pas beaucoup augmenté.
Des recherches supplémentaires ont montré que les principales pertes de performances sont associées au marshaling et au démarshaling. La conversion d'une structure en JSON et vice versa nécessite l'utilisation de la réflexion, qui est extrêmement coûteuse en performances. Il est impossible de refuser le marshaling / unmarshaling, car il est nécessaire d'obtenir un objet à part entière du cache avec la possibilité d'appeler des méthodes de structures, et pas seulement d'obtenir les valeurs de champs individuels. L'utilisation de diverses bibliothèques avec l'optimisation du marshaling / unmarshaling n'a pas non plus enregistré, il y avait une croissance, mais le cache en mémoire était très loin. Par conséquent, il a été décidé de ne pas se lier d'amitié avec le "hérisson et le serpent" et de créer une cache hybride.


Hybride "serpent et hérisson"


Vous ne pouvez pas l'appeler un hybride à part entière (voir. Fig.), En fait, il s'est avéré un cache en mémoire, mais avec une synchronisation via Redis (la bibliothèque github.com/go-redis/redis a été utilisée ). Seul l'identifiant unique de l'objet demandé à la base de données (objet ID) sera stocké dans Redis. Il sera ajouté à Redis lors du traitement d'une demande de création d'un objet, ou d'une demande d'obtention d'un objet existant dans la base de données. L'ID d'objet servira de clé pour la valeur dans Redis, et la valeur sera l'UUID généré (identifiant unique universel, identifiant unique universel ”). L'UUID sera généré uniquement lorsque l'objet sera ajouté à Redis. La raison pour laquelle cet UUID est nécessaire sera décrite plus loin.



Schéma fonctionnel de l'interaction des composants pour la synchronisation du cache via Redis


Le cache en mémoire est implémenté sur la base de sync.Map. Pour les éléments de cache hybride, TTL (durée de vie, durée de vie) est défini, et si Redis nettoie les éléments «fétides», le cache en mémoire est nettoyé par le temporisateur (time.AfterFunc). Il passe à travers tous les éléments du cache et vérifie si l'élément est «pourri». Si un élément de cache est accédé, sa durée de vie est prolongée; une opération similaire est effectuée avec des clés dans Redis.


Donc, maintenant selon l'algorithme. Si une demande arrive et que nous devons récupérer l'objet, la séquence d'actions suivante est effectuée:


  1. Nous regardons s'il y a un objet avec un objet ID donné dans Redis, si c'est le cas, alors nous pouvons prendre le cache d'instance de service de la mémoire:
    1. Si l'objet n'est pas dans le cache en mémoire, nous le prenons de la base de données et ajoutons le cache avec l'UUID de Redis au cache en mémoire et mettons à jour le TTL de la clé dans Redis.
    2. Si l'objet se trouve dans le cache en mémoire, nous le prenons dans le cache, vérifions si l'UUID dans le cache et dans Redis correspondent, et si c'est le cas, puis mettez à jour le TTL dans le cache et dans Redis. Si l'UUID ne correspond pas, supprimez l'objet du cache en mémoire, retirez-le de la base de données, ajoutez le cache avec l'UUID de Redis à la mémoire.
  2. Si l'objet n'est pas dans Redis, alors si l'objet est dans le cache, supprimez-le du cache. Prenez un objet de la base de données et ajoutez-le au cache et à Redis. Pour éliminer la situation lorsque la mise à jour / suppression d'une entrée est plus rapide que l'ajout au cache ( commentaire andreyverbin ), ajoutez un objet avec un UUID nul au cache. Ensuite, lors du premier accès au cache, la différence d'UUID avec Redis sera révélée et les données de la base de données seront à nouveau demandées.

Si une demande de suppression d'un objet arrive, il est immédiatement supprimé de la base de données, puis les opérations de cache:


  1. Supprimez l'objet dans Redis.
  2. Supprimez l'objet dans le cache en mémoire.

Maintenant, si une demande similaire arrive dans une autre instance du service, bien que l'objet puisse toujours être dans le cache en mémoire, il ne sera pas utilisé.


Mise à jour de l'objet, après mise à jour dans la base de données:


  1. Supprimez l'objet dans Redis.
  2. Supprimez l'objet dans le cache en mémoire.

Lorsque vous demandez un objet dans une autre instance du service, il sera révélé qu'il n'est pas dans Redis, vous devez donc le retirer de la base de données. S'il existe une autre instance du service et que la demande lui a été envoyée après la mise à jour de l'objet et après l'ajout par la deuxième instance de Redis, alors, lors de la vérification de l'UUID, une différence sera révélée et la troisième instance du service prendra également l'objet de la base de données.


C'est-à-dire en fait, dans toute situation incompréhensible, nous pensons que notre cache est incorrect et nous devons extraire des données de la base de données.


Conclusion


La solution développée présente des avantages et des inconvénients.


Avantages


  • Le schéma de mise en cache développé a permis d'atteindre environ 19000 RPS, ce qui est presque équivalent aux tests avec cache en mémoire.
  • Le code de projet d'origine a un nombre minimum de modifications.

Inconvénients


  • Si Redis tombe en panne, le service diminue considérablement ses performances et repose sur l'utilisation de la base de données.
  • Chaque instance du service nécessitera plus de mémoire car elle possède son propre cache en mémoire.

Étant donné que la haute performance était plus importante, je ne considère pas les inconvénients comme critiques. Dans le futur, il y a une idée d'écrire une bibliothèque pour simplifier la mise en œuvre du cache hybride, car il est nécessaire d'utiliser une mise en cache similaire dans d'autres projets.

Source: https://habr.com/ru/post/fr482704/


All Articles