
Le développement de projets fortement chargés dans n'importe quel langage nécessite une approche particulière et l'utilisation d'outils spécifiques, mais lorsqu'il s'agit d'applications PHP, la situation peut devenir si aiguë qu'il faut parfois développer, par exemple, . Dans cet article, nous aborderons une question familière à tous concernant le stockage distribué des sessions et la mise en cache des données dans memcached, ainsi que la manière dont nous avons résolu ces problèmes dans un projet qui nous tenait à cœur.
Le protagoniste est une application PHP basée sur le framework symfony 2.3, dont la mise à jour n'est absolument pas prévue dans les plans de l'entreprise. En plus du stockage standard des sessions, ce projet utilisait pleinement la politique de « mise en cache de tout » dans memcached : réponses aux requêtes de bases de données et de serveurs API, divers indicateurs, verrouillages pour synchroniser l'exécution du code et bien d'autres. Dans une telle situation, une panne de memcached devient fatale pour le fonctionnement de l'application. De plus, la perte de cache entraîne de graves conséquences : la base de données commence à craquer sous la pression, les services API bloquent les requêtes, etc. La stabilisation de la situation peut prendre des dizaines de minutes, pendant lesquelles le service risque d'être extrêmement lent ou même complètement inaccessible.
Nous avions besoin d'assurer la possibilité de mise à l'échelle horizontale de l'application sans trop de changements, c'est-à-dire avec un minimum de modifications du code source et en conservant l'intégralité de la fonctionnalité. Rendre le cache non seulement résistant aux pannes, mais aussi essayer de minimiser les pertes de données qu'il contient.
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 cohérent des clés permet de répartir les données de manière équilibrée sur plusieurs serveurs, en dirigeant chaque clé spécifique vers un serveur déterminé du groupe, et les outils de basculement intégrés garantissent une haute disponibilité du service de mise en cache (mais, malheureusement, pas des données).
La gestion des sessions est légèrement meilleure : il est possible de configurer memcached.sess_number_of_replicas, résultant en la sauvegarde immédiate des données sur plusieurs serveurs, et en cas de défaillance d'une instance de memcached, les données seront fournies par d'autres serveurs. Cependant, si le serveur revient en ligne sans données (comme cela arrive généralement après un redémarrage), une partie des clés sera redistribuée en sa faveur. En fait, cela signifiera une perte de données de session, car il n'est pas possible de « consulter » une autre réplique en cas d'échec.
Les outils standards de la bibliothèque sont principalement destinés à l'évolutivité : ils permettent d'augmenter le cache à des tailles gigantesques et d'assurer l'accès depuis le code hébergé sur différents serveurs. Cependant, dans notre situation, le volume des données stockées ne dépasse pas quelques gigaoctets, et la performance d'un ou deux nœuds est largement suffisante. Par conséquent, les outils standards pourraient uniquement assurer la disponibilité de memcached en maintenant au moins une instance du cache opérationnelle. Cependant, même cette possibilité n'a pas pu être exploitée... Il convient de rappeler l'ancienneté du framework utilisé dans le projet, ce qui a rendu impossible le bon fonctionnement de l'application avec un pool de serveurs. Ne pas oublier non plus les pertes de données de session : des déconnexions massives d'utilisateurs causaient des problèmes chez le client.
Idéalement, il était nécessaire de répliquer les enregistrements dans memcached et de contourner les répliques en cas d'échec ou d'erreur. Mettre en œuvre cette stratégie a été facilité par .
mcrouter
. C'est un routeur pour memcached, développé par Facebook pour résoudre ses problèmes. Il supporte le protocole texte de memcached, ce qui permet d'évoluer les installations de memcached à des tailles folles. Une description détaillée de mcrouter peut être trouvée dans . En plus d'une , il peut faire ce dont nous avons besoin :
- répliquer un enregistrement ;
- effectuer un fallback sur d'autres serveurs du groupe en cas d'erreur.
Allons-y !
Configuration de mcrouter
Je vais passer directement à la configuration :
{
"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 pools ? Pourquoi les serveurs se répètent-ils ? Voyons comment cela fonctionne.
- Dans cette configuration, mcrouter choisit le chemin que prendra la requête selon la commande de la requête. C'est ce qu'indique le type
OperationSelectorRoute. - Les requêtes GET passent par le gestionnaire
RandomRoute, qui choisit au hasard un pool ou un chemin parmi les objets du tableauchildren. Chaque élément de ce tableau est à son tour un gestionnaireMissFailoverRoute, qui parcourra chaque serveur dans le pool jusqu'à obtenir une réponse avec les données à retourner au client. - Si nous utilisions exclusivement
MissFailoverRouteun pool de trois serveurs, toutes les requêtes iraient d'abord au premier exemplaire de memcached, tandis que les autres recevraient les requêtes par conséquent, lorsque les données sont manquantes. Cette approche mènerait à une charge excessive sur le premier serveur de la liste, c'est pourquoi il a été décidé de générer trois pools avec des adresses dans un ordre différent et de les choisir au hasard. - Toutes les autres requêtes (celles d'écriture) sont traitées à l'aide de
AllMajorityRoute. Ce gestionnaire envoie les requêtes à tous les serveurs du pool et attend des réponses d'au moins N/2 + 1 d'entre eux. L'utilisation deAllSyncRoutepour les opérations d'écriture a dû être abandonnée, car cette méthode nécessite une réponse positive de tous tous les serveurs du groupe — sinon, elle renverraSERVER_ERROR. Bien que mcrouter puisse stocker les données dans les caches disponibles, la fonction PHP appelante renverra une erreur et générera un avis.AllMajorityRouten'est pas aussi strict et permet de sortir jusqu'à la moitié des nœuds de l'exploitation sans les problèmes mentionnés ci-dessus.
Le principal inconvénient de ce schéma est que si les données ne sont effectivement pas présentes dans le cache, alors pour chaque requête du client, il y aura effectivement N requêtes à memcached — vers de tout les serveurs du pool. On peut réduire le nombre de serveurs dans les pools, par exemple, à deux : en sacrifiant la fiabilité de stockage, nous obtiendrons bdeune plus grande vitesse et une charge réduite des requêtes aux clés manquantes.
NB: Des liens utiles pour étudier mcrouter peuvent également être et (y compris ceux qui sont fermés), représentant un véritable trésor de différentes configurations.
Compilation et lancement de mcrouter
L'application (et le memcached lui-même) fonctionne dans Kubernetes — par conséquent, mcrouter y est également situé. Pour la construction du conteneur nous utilisons , le fichier de configuration va ressembler à ce qui suit :
NB: Les listings présentés dans l'article sont publiés dans le dépôt .
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: Installer les prérequis
apt:
name: [ 'apt-transport-https', 'tzdata', 'locales' ]
update_cache: yes
- name: Ajouter la clé APT de mcrouter
apt_key:
url: https://facebook.github.io/mcrouter/debrepo/xenial/PUBLIC.KEY
- name: Ajouter le dépôt mcrouter
apt_repository:
repo: deb https://facebook.github.io/mcrouter/debrepo/xenial xenial contrib
filename: mcrouter
update_cache: yes
- name: Définir le fuseau horaire
timezone:
name: "Europe/Moscow"
- name: S'assurer qu'une locale existe
locale_gen:
name: en_US.UTF-8
state: present
install:
- name: Installer mcrouter
apt:
name: [ 'mcrouter' ]()
… et nous lançons Helm-chart. Fait intéressant — ici, il n'y a que le générateur de config en fonction du 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 -}}
{{- \/\* Remplissons le tableau avec deux copies des serveurs : "0 1 2 0 1 2" \*\/ -}}
{{- range until 2 -}}
{{- range $i, $_ := until $count -}}
{{- $servers = append $servers (printf "mc-%d.mc:11211" $i) -}}
{{- end -}}
{{- end -}}
{{- \/\* En se décalant dans le tableau, nous obtenons N segments : "[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|poold" $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 }}
}
}
}
}()
Nous 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 par texte d'erreur n'a pas donné de résultat, cependant, la recherche pour «» était parmi les premières à mentionner le plus ancien problème non résolu du projet — du protocole binaire memcached.
NB: Le protocole ASCII pour memcached est plus lent que le binaire, et les outils natifs de hachage consistant des clés ne fonctionnent qu'avec le protocole binaire. Mais cela ne pose pas de problème pour notre cas spécifique.
C'est dans la poche : il ne reste plus qu'à passer au protocole ASCII et tout fonctionnera.... Cependant, dans ce cas, l'habitude de chercher des réponses dans a joué un tour cruel. Vous n'y trouverez pas de réponse correcte... à moins que vous ne fassiez défiler jusqu'à la fin, où dans la section «Notes contributeurs utilisateurs» vous trouverez une réponse juste et .
Oui, le nom correct de l'option est memcached.sess_binary_protocol. Il faut la désactiver, après quoi les sessions commenceront à fonctionner. Il ne reste plus qu'à placer le conteneur avec mcrouter dans le pod avec PHP !
Conclusion
Ainsi, grâce à des changements d'infrastructure, nous avons réussi à résoudre la tâche posée : la question de la tolérance aux pannes de memcached a été résolue, la fiabilité du cache a été améliorée. En plus des avantages évidents pour l'application, cela a donné une marge de manœuvre lors des travaux sur la plateforme : lorsque tous les composants ont une redondance, la vie de l'administrateur devient beaucoup plus facile. Oui, cette méthode a ses inconvénients et peut sembler être une « béquille », mais si elle permet d'économiser de l'argent, de cacher un problème et de ne pas en créer de nouveaux, pourquoi pas ?
P.S.
Lisez aussi dans notre blog :
- «Pratique avec dapp» (par exemple symfony-demo): et ;
- «».
Source : habr.com
