
El desarrollo de proyectos de alta carga en cualquier lenguaje requiere un enfoque especial y la utilización de herramientas específicas, pero cuando se trata de aplicaciones en PHP, la situación puede complicarse tanto que es necesario desarrollar, por ejemplo, . En este artículo, abordaremos el conocido problema del almacenamiento distribuido de sesiones y la caché de datos en memcached, y cómo resolvimos estos asuntos en un proyecto 'bajo nuestra tutela'.
El protagonista es una aplicación en PHP basada en el framework symfony 2.3, cuya actualización no forma parte de los planes del negocio. Además del almacenamiento estándar de sesiones, en este proyecto se utilizaba a fondo la política de 'caché de todo' en memcached: respuestas a consultas en bases de datos y servidores API, diversas banderas, bloqueos para sincronizar la ejecución del código y mucho más. En estas circunstancias, una falla en memcached se convierte en fatal para el funcionamiento de la aplicación. Además, la pérdida de caché conlleva serias consecuencias: la base de datos comienza a fallar, los servicios API bloquean las peticiones, etc. La estabilización de la situación puede llevar decenas de minutos, durante los cuales el servicio puede experimentar un rendimiento extremadamente lento o incluso dejar de estar disponible.
Necesitamos garantizar la posibilidad de escalar horizontalmente la aplicación con un mínimo de alteraciones en el código fuente y manteniendo toda su funcionalidad. Hacer la caché no solo resistente a fallos, sino también tratar de minimizar la pérdida de datos en ella.. ¿Qué hay de malo con memcached en sí?
De hecho, la extensión de memcached para PHP 'de fábrica' soporta el almacenamiento distribuido de datos y sesiones. El mecanismo de hash consistente de claves permite distribuir los datos uniformemente en varios servidores, dirigiendo de manera única cada clave específica a un servidor determinado del grupo, y los medios integrados de failover garantizan una alta disponibilidad del servicio de caché (pero, desafortunadamente,
no de los datos. El almacenamiento de sesiones se maneja un poco mejor: se puede configurar).
memcached.sess_number_of_replicas memcached.sess_number_of_replicas, lo que permite que los datos se conserven de inmediato en varios servidores, y en caso de fallo de una instancia de memcached, los datos se proporcionarán desde otros. Sin embargo, si el servidor vuelve a estar operativo sin datos (como sucede a menudo después de un reinicio), algunas claves se redistribuirán a su favor. Esto significará pérdida de datos de sesión, ya que no hay posibilidad de 'ir' a otra réplica en caso de error.
Las herramientas estándar de la biblioteca están dirigidas, principalmente, a escalado horizontal: permiten aumentar la caché a tamaños gigantes y asegurar su acceso desde el código alojado en diferentes servidores. Sin embargo, en nuestra situación, el volumen de datos almacenados no supera varios gigabytes, y la potencia de uno o dos nodos es más que suficiente. Por lo tanto, las herramientas nativas solo podrían garantizar la disponibilidad de memcached manteniendo al menos una instancia de la caché en funcionamiento. Sin embargo, incluso esta posibilidad no se pudo aprovechar... Aquí cabe recordar la antigüedad del marco utilizado en el proyecto, lo que complicó la tarea de hacer que la aplicación funcionara con un grupo de servidores. No olvidemos las pérdidas de datos de sesiones: la mirada del cliente se tensaba ante el desconectarse masivo de usuarios.
En ideal, se requería replicación de registros en memcached y bypass de réplicas en caso de error o falla. Para implementar esta estrategia, nos ayudó .
mcrouter
Es un enrutador para memcached, desarrollado por Facebook para abordar sus problemas. Soporta el protocolo de texto de memcached, que permite escalar instalaciones de memcached a tamaños enormes. Una descripción detallada de mcrouter se puede encontrar en . Además de su puede hacer lo que necesitamos:
- replicar registros;
- hacer fallback a otros servidores del grupo en caso de error.
¡Manos a la obra!
Configuración de mcrouter
Pasaré directamente a la configuración:
{
"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"
]
}
}
}
}¿Por qué tres grupos? ¿Por qué se repiten los servidores? Vamos a entender cómo funciona esto.
- En esta configuración, mcrouter elige el camino por el que se enviará la solicitud según el tipo de comando.
OperationSelectorRoute. - Las solicitudes GET llegan al manejador
RandomRoute, que elige aleatoriamente un grupo o ruta entre los objetos del arreglochildren. Cada elemento de este arreglo, a su vez, es un manejadorMissFailoverRoute, que recorrerá cada servidor en el grupo hasta obtener una respuesta con los datos, que será devuelta al cliente. - Si usáramos únicamente
MissFailoverRoutecon un grupo de tres servidores, todas las solicitudes llegarían primero a la primera instancia de memcached, y las otras recibirían solicitudes por el principio del residuo cuando los datos no estén disponibles. Este enfoque llevaría a una carga excesiva en el primer servidor de la lista, por lo que se decidió generar tres grupos con direcciones en diferentes secuencias y elegirlas aleatoriamente. - Todas las demás solicitudes (es decir, las de escritura) se manejan mediante
AllMajorityRoute. Este manejador envía solicitudes a todos los servidores del grupo y espera respuestas de al menos N/2 + 1 de ellos. Se tuvo que abandonar el uso deAllSyncRoutepara las operaciones de escritura, ya que este método requiere una respuesta positiva de todas los servidores del grupo; de lo contrario, devolveráSERVER_ERROR. Aunque mcrouter almacene los datos en los cachés disponibles, la función PHP llamada devolverá un error y generará un aviso.AllMajorityRouteno es tan estricto y permite desconectar hasta la mitad de los nodos sin los problemas mencionados anteriormente.
La principal desventaja de este esquema es que si no hay datos en la caché, efectivamente se realizarán N solicitudes a memcached por cada solicitud del cliente — a todos los servidores en el grupo. Se podría reducir el número de servidores en los grupos, por ejemplo, a dos: sacrificando la fiabilidad del almacenamiento, obtendremos bmayor audiencia.una mayor velocidad y menor carga por solicitudes a claves faltantes.
NB: Los enlaces útiles para estudiar mcrouter también pueden ser y (incluyendo los cerrados), que representan un verdadero tesoro de diversas configuraciones.
Compilación y ejecución de mcrouter
La aplicación (y el propio memcached) se ejecuta en Kubernetes, por lo que mcrouter también se coloca allí. Para la compilación del contenedor utilizamos , la configuración se verá de la siguiente manera:
NB: Los listados presentados en el artículo están publicados en el repositorio .
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: Instalar requisitos previos
apt:
name: [ 'apt-transport-https', 'tzdata', 'locales' ]
update_cache: yes
- name: Agregar clave APT de mcrouter
apt_key:
url: https://facebook.github.io/mcrouter/debrepo/xenial/PUBLIC.KEY
- name: Agregar repositorio de mcrouter
apt_repository:
repo: deb https://facebook.github.io/mcrouter/debrepo/xenial xenial contrib
filename: mcrouter
update_cache: yes
- name: Establecer zona horaria
timezone:
name: "Europe/Moscow"
- name: Asegurarse de que exista un locale
locale_gen:
name: en_US.UTF-8
state: present
install:
- name: Instalar mcrouter
apt:
name: [ 'mcrouter' ]()
… y generamos Helm-chart. Lo interesante es que aquí solo está el generador de la configuración según la cantidad de réplicas (si alguien tiene una opción más concisa y elegante, compártanla en los comentarios):
{{- $count := (pluck .Values.global.env .Values.memcached.replicas | first | default .Values.memcached.replicas._default | int) -}}
{{- $pools := dict -}}
{{- $servers := list -}}
{{-
/* Llenamos el arreglo con dos copias de servidores: "0 1 2 0 1 2" */ -}}
{{- range until 2 -}}
{{- range $i, $_ := until $count -}}
{{- $servers = append $servers (printf "mc-%d.mc:11211" $i) -}}
{{- end -}}
{{- end -}}
{{-
/* Al desplazarnos por el arreglo, obtenemos N segmentos: "[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 }}
}
}
}
}()
Desplegamos en el entorno de prueba y verificamos:
# 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 búsqueda por el texto del error no dio resultado, sin embargo, al buscar "" el problema más antiguo no cerrado del proyecto apareció primero — el protocolo binario de memcached.
NB: El protocolo ASCII en memcached es más lento que el binario, además, las herramientas estándar de hash consistente de claves solo funcionan con el protocolo binario. Pero esto no crea problemas para el caso específico.
Es cuestión de tiempo: solo queda cambiar al protocolo ASCII y funcionará…. Sin embargo, en este caso, la costumbre de buscar respuestas en jugó una mala pasada. No encontrarás la respuesta correcta allí… a menos que, por supuesto, llegues al final, donde en la sección «Notas de usuarios» habrá una respuesta correcta y .
Sí, el nombre correcto de la opción es memcached.sess_binary_protocol. Debes desactivarla, y luego las sesiones comenzarán a funcionar. Solo queda colocar el contenedor con mcrouter en el pod con PHP.
Conclusión
Así, solo con cambios en la infraestructura, hemos logrado resolver la tarea planteada: se ha resuelto la cuestión de la tolerancia a fallos de memcached, aumentando la confiabilidad del almacenamiento en caché. Además de los evidentes beneficios para la aplicación, esto proporcionó espacio para maniobras al trabajar en la plataforma: cuando todos los componentes tienen respaldo, la vida del administrador se simplifica enormemente. Sí, este método también tiene sus inconvenientes, puede parecer un 'aparato provisional', pero si ahorra dinero, oculta el problema y no genera nuevos — ¿por qué no?
P.D.
También puedes leer en nuestro blog:
- «Práctica con dapp» (tomando como ejemplo symfony-demo): y ;
- «».
Fuente: habr.com
