Utiliser mcrouter pour mettre à l'échelle Memcached horizontalement

Utiliser mcrouter pour mettre à l'échelle Memcached horizontalement

Développer des projets à forte charge dans n'importe quel langage nécessite une approche particulière et l'utilisation d'outils spéciaux, mais lorsqu'il s'agit d'applications en PHP, la situation peut devenir tellement aggravée qu'il faut développer, par exemple, propre serveur d'applications. Dans cette note, nous parlerons des problèmes familiers liés au stockage de session distribué et à la mise en cache des données dans Memcached et de la manière dont nous avons résolu ces problèmes dans un seul projet « ward ».

Le héros de l'occasion est une application PHP basée sur le framework symfony 2.3, qui n'est pas du tout incluse dans les business plans de mise à jour. En plus d'un stockage de session assez standard, ce projet a pleinement utilisé politique de « tout mettre en cache » dans memcached : réponses aux requêtes adressées aux serveurs de base de données et d'API, divers drapeaux, verrous pour synchroniser l'exécution du code et bien plus encore. Dans une telle situation, une panne de memcached devient fatale au fonctionnement de l'application. De plus, la perte de cache entraîne de graves conséquences : le SGBD commence à exploser, les services API commencent à interdire les requêtes, etc. La stabilisation de la situation peut prendre des dizaines de minutes, et pendant ce temps, le service sera terriblement lent ou totalement indisponible.

Nous devions fournir la possibilité de faire évoluer l'application horizontalement avec peu d'effort, c'est à dire. avec des modifications minimes du code source et toutes les fonctionnalités préservées. Rendez le cache non seulement résistant aux pannes, mais essayez également de minimiser les pertes de données.

Quel est le problème avec Memcached lui-même ?

En général, l'extension memcached pour PHP prend en charge le stockage de données et de sessions distribuées dès le départ. Le mécanisme de hachage cohérent des clés vous permet de placer les données de manière uniforme sur de nombreux serveurs, en adressant de manière unique chaque clé spécifique à un serveur spécifique du groupe, et les outils de basculement intégrés garantissent une haute disponibilité du service de mise en cache (mais, malheureusement, pas de données).

Les choses vont un peu mieux avec le stockage de session : vous pouvez configurer memcached.sess_number_of_replicas, de sorte que les données seront stockées sur plusieurs serveurs à la fois, et en cas de panne d'une instance memcached, les données seront transférées depuis d'autres. Cependant, si le serveur revient en ligne sans données (comme cela arrive généralement après un redémarrage), certaines clés seront redistribuées en sa faveur. En fait, cela signifiera perte de données de session, puisqu'il n'y a aucun moyen « d'aller » vers une autre réplique en cas d'échec.

Les outils de bibliothèque standard sont principalement destinés à horizontal mise à l'échelle : ils permettent d'augmenter le cache jusqu'à des tailles gigantesques et d'y donner accès à partir du code hébergé sur différents serveurs. Cependant, dans notre situation, le volume de données stockées ne dépasse pas plusieurs gigaoctets, et les performances d'un ou deux nœuds sont largement suffisantes. En conséquence, les seuls outils standards utiles pourraient être d’assurer la disponibilité de Memcached tout en maintenant au moins une instance de cache en état de fonctionnement. Cependant, il n'a pas été possible de profiter même de cette opportunité... Il convient ici de rappeler l'ancienneté du framework utilisé dans le projet, c'est pourquoi il était impossible de faire fonctionner l'application avec un pool de serveurs. N’oublions pas non plus la perte de données de session : l’œil du client s’est contracté suite à la déconnexion massive des utilisateurs.

Idéalement, il fallait réplication des enregistrements dans Memcached et contournement des répliques en cas d'erreur ou d'erreur. Nous a aidé à mettre en œuvre cette stratégie mcrouter.

mcrouter

Il s'agit d'un routeur Memcached développé par Facebook pour résoudre ses problèmes. Il prend en charge le protocole de texte Memcached, qui permet faire évoluer les installations Memcached dans des proportions insensées. Une description détaillée de mcrouter peut être trouvée dans cette annonce. Entre autres large fonctionnalité il peut faire ce dont nous avons besoin :

  • répliquer l'enregistrement ;
  • effectuez un repli vers d'autres serveurs du groupe si une erreur se produit.

Pour la cause!

configuration du micro-ordinateur

Je vais directement à la config :

{
 "pools": {
   "pool00": {
     "servers": [
       "mc-0.mc:11211",
       "mc-1.mc:11211",
       "mc-2.mc:11211"
   },
   "pool01": {
     "servers": [
       "mc-1.mc:11211",
       "mc-2.mc:11211",
       "mc-0.mc:11211"
   },
   "pool02": {
     "servers": [
       "mc-2.mc:11211",
       "mc-0.mc:11211",
       "mc-1.mc:11211"
 },
 "route": {
   "type": "OperationSelectorRoute",
   "default_policy": "AllMajorityRoute|Pool|pool00",
   "operation_policies": {
     "get": {
       "type": "RandomRoute",
       "children": [
         "MissFailoverRoute|Pool|pool02",
         "MissFailoverRoute|Pool|pool00",
         "MissFailoverRoute|Pool|pool01"
       ]
     }
   }
 }
}

Pourquoi trois piscines ? Pourquoi les serveurs sont-ils répétés ? Voyons comment cela fonctionne.

  • Dans cette configuration, mcrouter sélectionne le chemin vers lequel la requête sera envoyée en fonction de la commande request. Le gars lui dit ça OperationSelectorRoute.
  • Les requêtes GET vont au gestionnaire RandomRoutequi sélectionne aléatoirement un pool ou une route parmi les objets du tableau children. Chaque élément de ce tableau est à son tour un gestionnaire MissFailoverRoute, qui passera par chaque serveur du pool jusqu'à ce qu'il reçoive une réponse avec des données, qui seront renvoyées au client.
  • Si on utilisait exclusivement MissFailoverRoute avec un pool de trois serveurs, toutes les requêtes seraient adressées en premier à la première instance memcached, et le reste recevrait les requêtes de manière résiduelle lorsqu'il n'y a pas de données. Une telle approche conduirait à charge excessive sur le premier serveur de la liste, il a donc été décidé de générer trois pools avec des adresses dans des séquences différentes et de les sélectionner de manière aléatoire.
  • Toutes les autres demandes (et ceci est un enregistrement) sont traitées en utilisant AllMajorityRoute. Ce gestionnaire envoie des requêtes à tous les serveurs du pool et attend les réponses d'au moins N/2 + 1 d'entre eux. De l'utilisation AllSyncRoute car les opérations d'écriture ont dû être abandonnées, car cette méthode nécessite une réponse positive de la part de tous serveurs du groupe - sinon il reviendra SERVER_ERROR. Bien que mcrouter ajoute les données aux caches disponibles, la fonction PHP appelante renverra une erreur et générera un avis. AllMajorityRoute n'est pas si stricte et permet de mettre hors service jusqu'à la moitié des unités sans les problèmes décrits ci-dessus.

Principal moins Ce schéma est le suivant : s'il n'y a vraiment aucune donnée dans le cache, alors pour chaque requête du client, N requêtes adressées à memcached seront effectivement exécutées - pour tous serveurs dans le pool. On peut par exemple réduire à deux le nombre de serveurs dans les pools : en sacrifiant la fiabilité du stockage, on obtientоune vitesse plus élevée et moins de charge due aux demandes de clés manquantes.

NB: Vous pouvez également trouver des liens utiles pour apprendre mcrouter documentation sur wiki и problèmes du projet (y compris fermés), représentant tout un entrepôt de configurations diverses.

Construire et faire fonctionner un mcrouter

Notre application (et Memcached lui-même) fonctionne dans Kubernetes - par conséquent, mcrouter s'y trouve également. Pour assemblage de conteneurs nous utilisons cour, dont la configuration ressemblera à ceci :

NB: Les listings donnés dans l'article sont publiés dans le référentiel plat/microrouteur.

configVersion: 1
project: mcrouter
deploy:
 namespace: '[[ env ]]'
 helmRelease: '[[ project ]]-[[ env ]]'
---
image: mcrouter
from: ubuntu:16.04
mount:
- from: tmp_dir
 to: /var/lib/apt/lists
- from: build_dir
 to: /var/cache/apt
ansible:
 beforeInstall:
 - name: Install prerequisites
   apt:
     name: [ 'apt-transport-https', 'tzdata', 'locales' ]
     update_cache: yes
 - name: Add mcrouter APT key
   apt_key:
     url: https://facebook.github.io/mcrouter/debrepo/xenial/PUBLIC.KEY
 - name: Add mcrouter Repo
   apt_repository:
     repo: deb https://facebook.github.io/mcrouter/debrepo/xenial xenial contrib
     filename: mcrouter
     update_cache: yes
 - name: Set timezone
   timezone:
     name: "Europe/Moscow"
 - name: Ensure a locale exists
   locale_gen:
     name: en_US.UTF-8
     state: present
 install:
 - name: Install mcrouter
   apt:
     name: [ 'mcrouter' ]

(werf.yaml)

... et dessinez-le Tableau de barre. Ce qui est intéressant c'est qu'il n'y a qu'un générateur de config basé sur le nombre de répliques (si quelqu'un a une option plus laconique et élégante, partagez-la dans les commentaires):

{{- $count := (pluck .Values.global.env .Values.memcached.replicas | first | default .Values.memcached.replicas._default | int) -}}
{{- $pools := dict -}}
{{- $servers := list -}}
{{- /* Заполняем  массив двумя копиями серверов: "0 1 2 0 1 2" */ -}}
{{- range until 2 -}}
 {{- range $i, $_ := until $count -}}
   {{- $servers = append $servers (printf "mc-%d.mc:11211" $i) -}}
 {{- end -}}
{{- end -}}
{{- /* Смещаясь по массиву, получаем N срезов: "[0 1 2] [1 2 0] [2 0 1]" */ -}}
{{- range $i, $_ := until $count -}}
 {{- $pool := dict "servers" (slice $servers $i (add $i $count)) -}}
 {{- $_ := set $pools (printf "MissFailoverRoute|Pool|pool%02d" $i) $pool -}}
{{- end -}}
---
apiVersion: v1
kind: ConfigMap
metadata:
 name: mcrouter
data:
 config.json: |
   {
     "pools": {{- $pools | toJson | replace "MissFailoverRoute|Pool|" "" -}},
     "route": {
       "type": "OperationSelectorRoute",
       "default_policy": "AllMajorityRoute|Pool|pool00",
       "operation_policies": {
         "get": {
           "type": "RandomRoute",
           "children": {{- keys $pools | toJson }}
         }
       }
     }
   }

(10-mcrouter.yaml)

Nous le déployons dans l'environnement de test et vérifions :

# php -a
Interactive mode enabled

php > # Проверяем запись и чтение
php > $m = new Memcached();
php > $m->addServer('mcrouter', 11211);
php > var_dump($m->set('test', 'value'));
bool(true)
php > var_dump($m->get('test'));
string(5) "value"
php > # Работает! Тестируем работу сессий:
php > ini_set('session.save_handler', 'memcached');
php > ini_set('session.save_path', 'mcrouter:11211');
php > var_dump(session_start());
PHP Warning:  Uncaught Error: Failed to create session ID: memcached (path: mcrouter:11211) in php shell code:1
Stack trace:
#0 php shell code(1): session_start()
#1 {main}
  thrown in php shell code on line 1
php > # Не заводится… Попробуем задать session_id:
php > session_id("zzz");
php > var_dump(session_start());
PHP Warning:  session_start(): Cannot send session cookie - headers already sent by (output started at php shell code:1) in php shell code on line 1
PHP Warning:  session_start(): Failed to write session lock: UNKNOWN READ FAILURE in php shell code on line 1
PHP Warning:  session_start(): Failed to write session lock: UNKNOWN READ FAILURE in php shell code on line 1
PHP Warning:  session_start(): Failed to write session lock: UNKNOWN READ FAILURE in php shell code on line 1
PHP Warning:  session_start(): Failed to write session lock: UNKNOWN READ FAILURE in php shell code on line 1
PHP Warning:  session_start(): Failed to write session lock: UNKNOWN READ FAILURE in php shell code on line 1
PHP Warning:  session_start(): Failed to write session lock: UNKNOWN READ FAILURE in php shell code on line 1
PHP Warning:  session_start(): Unable to clear session lock record in php shell code on line 1
PHP Warning:  session_start(): Failed to read session data: memcached (path: mcrouter:11211) in php shell code on line 1
bool(false)
php >

La recherche du texte de l'erreur n'a donné aucun résultat, mais la requête "mcrouter php"Au premier plan se trouvait le problème non résolu le plus ancien du projet - manque de soutien protocole binaire memcached.

NB: Le protocole ASCII dans memcached est plus lent que le protocole binaire, et les moyens standard de hachage de clé cohérent ne fonctionnent qu'avec le protocole binaire. Mais cela ne pose pas de problèmes dans un cas précis.

L'astuce est dans le sac : il suffit de passer au protocole ASCII et tout fonctionnera.... Cependant, dans ce cas, l'habitude de chercher des réponses dans documentation sur php.net a fait une blague cruelle. Vous n’y trouverez pas la bonne réponse… à moins bien sûr de faire défiler jusqu’à la fin, où dans la section "Notes contribuées par l'utilisateur" sera fidèle et réponse injustement rejetée.

Oui, le nom correct de l'option est memcached.sess_binary_protocol. Il doit être désactivé, après quoi les sessions commenceront à fonctionner. Il ne reste plus qu'à mettre le conteneur avec mcrouter dans un pod avec PHP !

Conclusion

Ainsi, avec de simples changements d'infrastructure, nous avons pu résoudre le problème : le problème de tolérance aux pannes de Memcached a été résolu et la fiabilité du stockage en cache a été augmentée. En plus des avantages évidents pour l'application, cela laissait une marge de manœuvre lors du travail sur la plateforme : lorsque tous les composants disposent d'une réserve, la vie de l'administrateur est grandement simplifiée. Oui, cette méthode a aussi ses inconvénients, elle peut ressembler à une « béquille », mais si elle permet d'économiser de l'argent, d'enterrer le problème et n'en provoque pas de nouveaux - pourquoi pas ?

PS

A lire aussi sur notre blog :

Source: habr.com

Ajouter un commentaire