
Le développement de projets très chargés dans n'importe quel langage nécessite une approche spéciale et l'utilisation d'outils spéciaux, mais en ce qui concerne les applications en PHP, la situation peut tellement empirer que vous devez développer, par exemple,
votre propre serveur d'applications . Dans cet article, nous parlerons de la douleur que tout le monde connaît avec le stockage distribué des sessions et la mise en cache des données dans memcached et comment nous avons résolu ces problèmes dans un projet «ward».
Le héros du jour est une application PHP basée sur le framework symfony 2.3, qui n'est pas du tout incluse dans les plans d'affaires. En plus du stockage de session complètement standard, le projet a utilisé
la mise en cache de tout dans memcached dans toute sa mesure: réponses aux requêtes de la base de données et des serveurs d'API, divers indicateurs, verrous pour synchroniser l'exécution du code, et bien plus encore. Dans cette situation, l'échec memcached devient fatal pour que l'application fonctionne. De plus, la perte de cache entraîne de graves conséquences: le SGBD commence à craquer au niveau des coutures, des services API - demandes d'interdiction, etc. La stabilisation de la situation peut prendre des dizaines de minutes, et à ce moment le service ralentira terriblement ou deviendra complètement inaccessible.
Nous devions offrir la
possibilité d'une mise à l'échelle horizontale de l'application avec du petit sang , c'est-à-dire avec des changements minimes au code source et une préservation complète des fonctionnalités. Rendez le cache non seulement tolérant aux pannes, mais essayez également de minimiser la perte de données.
Quel est le problème avec memcached lui-même?
En général, l'extension memcached pour PHP prête à l'emploi prend en charge le stockage distribué des données et des sessions. Le mécanisme de hachage de clé cohérent vous permet de placer uniformément des données sur de nombreux serveurs, en adressant sans ambiguïté chaque clé spécifique à un serveur spécifique du groupe, et les outils intégrés de basculement offrent une haute disponibilité du service de mise en cache (mais, malheureusement,
pas de données ).
Avec le stockage des sessions, les choses vont un peu mieux: vous pouvez configurer
memcached.sess_number_of_replicas
, à la suite de quoi les données seront enregistrées sur plusieurs serveurs à la fois, et en cas de défaillance d'une instance memcached, les données seront transférées à partir d'autres. Cependant, si le serveur revient en service sans données (comme c'est généralement le cas après un redémarrage), une partie des clés sera redistribuée en sa faveur. En fait, cela signifiera la
perte de données de session , car il n'y a aucun moyen "d'aller" vers une autre réplique en cas de manque.
Les outils de bibliothèque standard visent principalement
une mise à
l' échelle
horizontale : ils vous permettent d'augmenter le cache à des tailles gigantesques et de lui donner accès à partir de code situé sur différents serveurs. Cependant, dans notre situation, la quantité de données stockées ne dépasse pas plusieurs gigaoctets, et les performances d'un ou deux nœuds sont largement suffisantes. Par conséquent, à partir d'un moyen régulier utile, ils ne pouvaient garantir la disponibilité de memcached qu'en maintenant au moins une instance de cache en état de fonctionnement. Cependant, je n’ai même pas réussi à profiter de cette opportunité… Rappelons ici l’antiquité du framework utilisé dans le projet, qui rendait impossible le fonctionnement de l’application avec le pool de serveurs. Nous n'oublierons pas non plus la perte de données de session: l'œil s'est tordu de la déconnexion massive des utilisateurs chez le client.
Idéalement, la
réplication d'un enregistrement dans des répliques memcached et d'exploration en cas de manque ou d'erreur était requise.
Mcrouter nous a aidés à mettre en œuvre cette stratégie.
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 vous permet de mettre à l'
échelle des installations memcached à des tailles folles. Une description détaillée de mcrouter se trouve dans
cette annonce . Entre autres
fonctionnalités étendues, il peut ce dont nous avons besoin:
- reproduire l'enregistrement;
- se replier sur les autres serveurs du groupe en cas d'erreur.
Pour la cause!
Configuration de Mcrouter
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 où la demande sera envoyée en fonction de la commande request. Le type
OperationSelectorRoute
lui en parle. - Les requêtes GET tombent dans le gestionnaire
RandomRoute
, qui sélectionne au hasard un pool ou une route parmi les objets du tableau children
. Chaque élément de ce tableau, à son tour, est 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 retournées au client. - Si nous
MissFailoverRoute
exclusivement MissFailoverRoute
avec un pool de trois serveurs, alors toutes les demandes viendraient en premier à la première instance memcached, et les autres recevraient des demandes sur le principe résiduel quand il n'y a pas de données. Une telle approche entraînerait une surcharge du premier serveur de la liste , il a donc été décidé de générer trois pools avec des adresses dans une séquence différente et de les sélectionner au hasard. - Toutes les autres demandes (et cet enregistrement) sont traitées à l'aide d'
AllMajorityRoute
. Ce gestionnaire envoie des demandes à tous les serveurs du pool et attend les réponses d'au moins N / 2 + 1 d'entre eux. J'ai dû abandonner l'utilisation d' AllSyncRoute
pour les opérations d'écriture, car cette méthode nécessite une réponse positive de tous les serveurs du groupe - sinon elle renverra SERVER_ERROR
. Bien que mcrouter place les données dans des caches accessibles, la fonction PHP appelante renvoie une erreur et génère une notification. AllMajorityRoute
pas si strict et permet de mettre hors service jusqu'à la moitié des nœuds sans les problèmes ci-dessus.
Le principal inconvénient de ce schéma est que s'il n'y a vraiment aucune donnée dans le cache, alors pour chaque requête du client, N requêtes vers memcached seront exécutées - vers
tous les serveurs du pool. Vous pouvez réduire le nombre de serveurs dans les pools, par exemple, à deux: en sacrifiant la fiabilité du stockage, nous obtiendrons plus de vitesse et moins de charge des requêtes aux clés manquantes.
NB : La documentation dans le wiki et les problèmes du projet (y compris ceux fermés), qui représentent tout un entrepôt de configurations diverses, peuvent également être des liens utiles pour apprendre mcrouter.Créer et exécuter mcrouter
L'application (et memcached elle-même) fonctionne pour nous dans Kubernetes - respectivement, au même endroit et sur mcrouter. Pour
construire le conteneur, nous utilisons
werf , dont la configuration ressemblera à ceci:
NB : Les listes de cet article sont publiées dans le référentiel flant / mcrouter . 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.imtqy.com/mcrouter/debrepo/xenial/PUBLIC.KEY - name: Add mcrouter Repo apt_repository: repo: deb https://facebook.imtqy.com/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 lancez un
graphique Helm . De l'intéressant - il n'y a qu'un générateur de configuration sur le nombre de répliques
(si quelqu'un a une option plus concise et élégante - partagez 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 déployons dans l'environnement de test et vérifions:
La recherche dans le texte n'a pas donné d'erreur, mais à la demande de "
mcrouter php ", le plus ancien problème de projet non
divulgué est apparu au premier plan - le
manque de support pour le protocole binaire memcached.
NB : Le protocole ASCII dans memcached est plus lent que binaire, ainsi que des moyens réguliers de hachage de clé cohérent qui ne fonctionnent qu'avec le protocole binaire. Mais cela ne crée pas de problèmes pour un cas particulier.Le truc est dans le chapeau: il ne reste plus qu'à passer au protocole ASCII et ça marchera .... Cependant, dans ce cas, l'habitude de chercher des réponses dans la
documentation de php.net a joué une blague cruelle. Vous ne trouverez pas la bonne réponse là-bas ... à moins, bien sûr, de feuilleter jusqu'à la fin, où dans la section
"Notes contributives des utilisateurs", il y aura une
réponse correcte et
injustement méritée .
Oui, le nom d'option correct 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 le pod avec PHP!
Conclusion
Ainsi, à l'aide des seuls changements d'infrastructure, nous avons pu résoudre le problème posé: le problème de la tolérance aux pannes memcached a été résolu, 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 pour travailler sur la plateforme: lorsque tous les composants ont 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, enfouit le problème et n'en provoque pas de nouvelles - pourquoi pas?
PS
Lisez aussi dans notre blog: