Usando mcrouter para escalar memcached horizontalmente

Usando mcrouter para escalar memcached horizontalmente

Desarrollar proyectos de alta carga en cualquier lenguaje requiere un enfoque especial y el uso de herramientas especiales, pero cuando se trata de aplicaciones en PHP, la situación puede agravarse tanto que hay que desarrollar, por ejemplo, propio servidor de aplicaciones. En esta nota hablaremos sobre los problemas familiares que plantea el almacenamiento distribuido de sesiones y el almacenamiento en caché de datos en Memcached y cómo resolvimos estos problemas en un proyecto de “barrio”.

El héroe de la ocasión es una aplicación PHP basada en el marco Symfony 2.3, que no está incluida en los planes comerciales de actualización. Además del almacenamiento de sesiones bastante estándar, este proyecto hizo pleno uso de política de "almacenar todo en caché" en memcached: respuestas a solicitudes a la base de datos y servidores API, varios indicadores, bloqueos para sincronizar la ejecución del código y mucho más. En tal situación, una falla de Memcached resulta fatal para el funcionamiento de la aplicación. Además, la pérdida de caché tiene graves consecuencias: el DBMS comienza a estallar, los servicios API comienzan a prohibir las solicitudes, etc. Estabilizar la situación puede llevar decenas de minutos y, durante este tiempo, el servicio será terriblemente lento o no estará disponible en absoluto.

Necesitábamos proporcionar la capacidad de escalar horizontalmente la aplicación con poco esfuerzo, es decir. con cambios mínimos en el código fuente y funcionalidad completa preservada. Haga que el caché no solo sea resistente a fallas, sino que también intente minimizar la pérdida de datos.

¿Qué tiene de malo el propio Memcached?

En general, la extensión memcached para PHP admite datos distribuidos y almacenamiento de sesiones de forma inmediata. El mecanismo de hash de claves consistente le permite colocar datos de manera uniforme en muchos servidores, dirigiendo de manera única cada clave específica a un servidor específico del grupo, y las herramientas de conmutación por error integradas garantizan una alta disponibilidad del servicio de almacenamiento en caché (pero, desafortunadamente, sin datos).

Las cosas van un poco mejor con el almacenamiento de sesiones: puedes configurar memcached.sess_number_of_replicas, como resultado de lo cual los datos se almacenarán en varios servidores a la vez y, en caso de falla de una instancia de Memcached, los datos se transferirán desde otros. Sin embargo, si el servidor vuelve a estar en línea sin datos (como suele ocurrir después de un reinicio), algunas de las claves se redistribuirán a su favor. De hecho esto significará pérdida de datos de sesión, ya que no hay forma de “ir” a otra réplica en caso de fallo.

Las herramientas de biblioteca estándar están dirigidas principalmente a horizontal Escalado: permiten aumentar el caché a tamaños gigantescos y brindar acceso a él desde código alojado en diferentes servidores. Sin embargo, en nuestra situación, el volumen de datos almacenados no supera varios gigabytes y el rendimiento de uno o dos nodos es suficiente. En consecuencia, las únicas herramientas estándar útiles podrían ser garantizar la disponibilidad de Memcached manteniendo al menos una instancia de caché en condiciones de funcionamiento. Sin embargo, ni siquiera fue posible aprovechar esta oportunidad... Aquí vale la pena recordar la antigüedad del marco utilizado en el proyecto, por lo que fue imposible hacer que la aplicación funcionara con un grupo de servidores. Tampoco nos olvidemos de la pérdida de datos de la sesión: el ojo del cliente se movía ante el cierre masivo de sesiones de los usuarios.

Lo ideal era que fuera necesario replicación de registros en memcached y omisión de réplicas en caso de error o equivocación. Nos ayudó a implementar esta estrategia. mcrouter.

mcrouter

Este es un enrutador Memcached desarrollado por Facebook para solucionar sus problemas. Es compatible con el protocolo de texto memcached, que permite escalar instalaciones memcached hasta proporciones demenciales. Puede encontrar una descripción detallada de mcrouter en este anuncio. Entre otras cosas amplia funcionalidad puede hacer lo que necesitamos:

  • replicar registro;
  • recurra a otros servidores del grupo si se produce un error.

Por la causa!

configuración del mcrouter

Iré 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 piscinas? ¿Por qué se repiten los servidores? Averigüemos cómo funciona.

  • En esta configuración, mcrouter selecciona la ruta a la que se enviará la solicitud según el comando de solicitud. El chico le dice esto OperationSelectorRoute.
  • Las solicitudes GET van al controlador RandomRouteque selecciona aleatoriamente un grupo o ruta entre objetos de matriz children. Cada elemento de esta matriz es a su vez un controlador. MissFailoverRoute, que recorrerá cada servidor del pool hasta recibir una respuesta con datos, que serán devueltos al cliente.
  • Si usáramos exclusivamente MissFailoverRoute con un grupo de tres servidores, todas las solicitudes llegarían primero a la primera instancia de Memcached y el resto recibiría solicitudes de forma residual cuando no haya datos. Un enfoque de este tipo llevaría a carga excesiva en el primer servidor de la lista, por lo que se decidió generar tres grupos con direcciones en diferentes secuencias y seleccionarlos al azar.
  • Todas las demás solicitudes (y esto es un registro) se procesan utilizando AllMajorityRoute. Este controlador envía solicitudes a todos los servidores del grupo y espera respuestas de al menos N/2 + 1 de ellos. De uso AllSyncRoute para operaciones de escritura tuvo que ser abandonado, ya que este método requiere una respuesta positiva de todos servidores en el grupo; de lo contrario, regresará SERVER_ERROR. Aunque mcrouter agregará los datos a las cachés disponibles, la función PHP que llama devolverá un error y generará aviso. AllMajorityRoute no es tan estricto y permite sacar de servicio hasta la mitad de las unidades sin los problemas descritos anteriormente.

Principal menos Este esquema es que si realmente no hay datos en el caché, entonces para cada solicitud del cliente se ejecutarán N solicitudes a Memcached - para todos servidores en el grupo. Podemos reducir el número de servidores en los grupos, por ejemplo, a dos: sacrificando la confiabilidad del almacenamiento, obtenemosоmayor velocidad y menos carga desde solicitudes hasta claves faltantes.

NB: También puede encontrar enlaces útiles para aprender mcrouter documentación en wiki и problemas del proyecto (incluidos los cerrados), que representan todo un almacén de varias configuraciones.

Construyendo y ejecutando mcrouter

Nuestra aplicación (y Memcached) se ejecuta en Kubernetes; en consecuencia, mcrouter también se encuentra allí. Para ensamblaje de contenedores usamos patio, cuya configuración se verá así:

NB: Los listados que figuran en el artículo se publican en el repositorio. plano/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.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)

... y esbozarlo Carta de timón. Lo interesante es que solo existe un generador de configuración basado en la cantidad de réplicas. (si alguien tiene una opción más lacónica y elegante que la comparta en los comentarios):

{{- $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)

Lo implementamos 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 del texto del error no dio ningún resultado, pero sí la consulta “microuter php"En primer plano estaba el problema más antiguo sin resolver del proyecto: falta de apoyo Protocolo binario memcached.

NB: El protocolo ASCII en Memcached es más lento que el binario, y los medios estándar de hash de claves consistentes solo funcionan con el protocolo binario. Pero esto no crea problemas para un caso concreto.

El truco está en la bolsa: todo lo que tienes que hacer es cambiar al protocolo ASCII y todo funcionará.... Sin embargo, en este caso, la costumbre de buscar respuestas en documentación en php.net jugó una broma cruel. No encontrarás la respuesta correcta allí... a menos, por supuesto, que te desplaces hasta el final, donde en la sección "Notas aportadas por el usuario" será fiel y respuesta injustamente rechazada.

Sí, el nombre de opción correcto es memcached.sess_binary_protocol. Debe estar deshabilitado, después de lo cual las sesiones comenzarán a funcionar. ¡Todo lo que queda es poner el contenedor con mcrouter en un pod con PHP!

Conclusión

Por lo tanto, con solo cambios de infraestructura pudimos resolver el problema: se resolvió el problema con la tolerancia a fallas de Memcached y se aumentó la confiabilidad del almacenamiento en caché. Además de las ventajas obvias para la aplicación, esto dio margen de maniobra al trabajar en la plataforma: cuando todos los componentes tienen reserva, la vida del administrador se simplifica enormemente. Sí, este método también tiene sus inconvenientes, puede parecer una "muleta", pero si ahorra dinero, entierra el problema y no causa otros nuevos, ¿por qué no?

PS

Lea también en nuestro blog:

Fuente: habr.com

Añadir un comentario