Use mcrouter para escalar memcached horizontalmente



El desarrollo de proyectos altamente cargados en cualquier idioma requiere un enfoque especial y el uso de herramientas especiales, pero cuando se trata de aplicaciones en PHP, la situación puede empeorar tanto que tiene que desarrollar, por ejemplo, su propio servidor de aplicaciones . En este artículo hablaremos sobre el dolor que todos conocen con 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 culpable de la celebración es una aplicación PHP basada en el marco de Symfony 2.3, que no está incluida en los planes de negocios. Además del almacenamiento de sesión completamente estándar, el proyecto utilizó la política de almacenamiento en caché de todo en Memcached con todas sus fuerzas: respuestas a consultas a la base de datos y servidores API, varios indicadores, bloqueos para sincronizar la ejecución de código y mucho más. En esta situación, la falla de memcached se vuelve fatal para que la aplicación funcione. Además, la pérdida de caché conlleva serias consecuencias: el DBMS comienza a agrietarse, servicios API - solicitudes de prohibición, etc. La estabilización de la situación puede llevar decenas de minutos, y en este momento el servicio se ralentizará terriblemente o se volverá completamente inaccesible.

Necesitábamos proporcionar la posibilidad de escalado horizontal de la aplicación con sangre pequeña , es decir. con cambios mínimos en el código fuente y preservación completa de la funcionalidad. Haga que el caché no solo sea tolerante a fallas, sino que también intente minimizar la pérdida de datos.

¿Qué tiene de malo memcached?


En general, la extensión memcached para PHP fuera de la caja admite el almacenamiento distribuido de datos y sesiones. El mecanismo de hash de clave consistente le permite colocar datos de manera uniforme en muchos servidores, dirigiendo inequívocamente cada clave específica a un servidor específico en el grupo, y las herramientas integradas de conmutación por error proporcionan una alta disponibilidad del servicio de almacenamiento en caché (pero, desafortunadamente, no son datos ).

Con el almacenamiento de sesiones, las cosas están un poco mejor: puede configurar memcached.sess_number_of_replicas , como resultado de lo cual los datos se guardarán en varios servidores a la vez, y en caso de falla de una instancia de memcached, los datos se transferirán de otros. Sin embargo, si el servidor vuelve al servicio sin datos (como suele suceder después de un reinicio), parte de las claves se redistribuirán a su favor. De hecho, esto significará la pérdida de datos de la sesión , ya que no hay forma de "ir" a otra réplica en caso de una falla.

Las herramientas de biblioteca estándar están dirigidas principalmente al escalado horizontal : le permiten aumentar el caché a tamaños gigantescos y proporcionarle acceso desde el código ubicado en diferentes servidores. Sin embargo, en nuestra situación, la cantidad de datos almacenados no supera varios gigabytes, y el rendimiento de uno o dos nodos es suficiente. En consecuencia, desde un medio regular útil, solo podían garantizar la disponibilidad de memcached mientras mantenían al menos una instancia de caché en condiciones de funcionamiento. Sin embargo, no logré aprovechar esta oportunidad ... Aquí debemos recordar la antigüedad del marco utilizado en el proyecto, lo que hizo imposible que la aplicación funcionara con el grupo de servidores. Tampoco nos olvidaremos de la pérdida de datos de la sesión: el ojo se movió por el cierre de sesión masivo de los usuarios del cliente.

Idealmente, se requería la replicación de un registro en las réplicas de memoria caché y de rastreo en caso de falla o error. Mcrouter nos ayudó a implementar esta estrategia.

mcrouter


Este es un enrutador de memoria caché desarrollado por Facebook para resolver sus problemas. Admite el protocolo de texto memcached, que le permite escalar instalaciones memcached a tamaños locos. Una descripción detallada de mcrouter se puede encontrar en este anuncio . Entre otras funciones amplias, puede lo que necesitamos:

  • replicar el registro;
  • Respaldar a otros servidores del grupo en caso de error

A la causa!

Configuración de 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? Veamos como funciona.

  • En esta configuración, mcrouter selecciona la ruta donde se enviará la solicitud en función del comando de solicitud. El tipo OperationSelectorRoute le cuenta sobre esto.
  • Las solicitudes GET caen en el controlador RandomRoute , que selecciona aleatoriamente un grupo o ruta entre los objetos en la matriz RandomRoute . Cada elemento de esta matriz, a su vez, es un controlador MissFailoverRoute que MissFailoverRoute través de cada servidor en el grupo hasta que reciba una respuesta con datos, que serán devueltos al cliente.
  • Si MissFailoverRoute 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 según el principio residual cuando no hay datos. Tal enfoque llevaría a una sobrecarga del primer servidor de la lista , por lo que se decidió generar tres grupos con direcciones en una secuencia diferente y seleccionarlos al azar.
  • Todas las demás solicitudes (y este 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. Tuve que abandonar el uso de AllSyncRoute para las operaciones de escritura, ya que este método requiere una respuesta positiva de todos los servidores del grupo; de lo contrario, devolverá SERVER_ERROR . Aunque mcrouter pondrá los datos en cachés accesibles, la función PHP que llama devolverá un error y generará un aviso. AllMajorityRoute no AllMajorityRoute tan estricto y permite desmantelar hasta la mitad de los nodos sin los problemas anteriores.

La principal desventaja de este esquema es que si realmente no hay datos en la memoria caché, entonces, para cada solicitud del cliente, se ejecutarán N solicitudes a memcached, a todos los servidores del grupo. Puede reducir el número de servidores en grupos, por ejemplo, a dos: sacrificando la confiabilidad del almacenamiento, obtendremos más velocidad y menos carga de las solicitudes a las claves faltantes.

NB : la documentación en la wiki y los problemas del proyecto (incluidos los cerrados), que representan un almacén completo de varias configuraciones, también pueden ser enlaces útiles para aprender mcrouter.

Construye y ejecuta mcrouter


La aplicación (y memcached en sí) funciona para nosotros en Kubernetes, respectivamente, en el mismo lugar y mcrouter. Para construir el contenedor, usamos werf , cuya configuración se verá así:

NB : Los listados en este artículo se publican en el repositorio de 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 )

... y lanza una tabla de Helm . De lo interesante: solo hay un generador de configuración en la cantidad de réplicas (si alguien tiene una opción más concisa y elegante, 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 )

Llegamos al 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 en el texto no dio un error, pero a petición de " mcrouter php ", el problema de proyecto no cerrado más antiguo apareció en primer plano: la falta de soporte para el protocolo binario memcached.

NB : el protocolo ASCII memcached es más lento que el binario, así como los medios estándar de hashing de claves consistentes que funcionan solo con el protocolo binario. Pero esto no crea problemas para un caso particular.

La cosa está en el sombrero: solo queda cambiar al protocolo ASCII y funcionará ... Sin embargo, en este caso, el hábito de buscar respuestas en la documentación en php.net jugó una broma cruel. No encontrará la respuesta correcta allí ... a menos que, por supuesto, vaya hasta el final, donde en la sección "Notas contribuidas por el usuario" habrá una respuesta correcta e inmerecidamente cegada .

Sí, el nombre de la opción correcta es memcached.sess_binary_protocol . Debe deshabilitarse, después de lo cual las sesiones comenzarán a funcionar. ¡Solo queda colocar el contenedor con mcrouter en el pod con PHP!

Conclusión


Por lo tanto, solo con la ayuda de cambios de infraestructura, pudimos resolver el problema planteado: se resolvió el problema con la tolerancia a fallos de memoria caché, se aumentó la fiabilidad del almacenamiento en caché. Además de las ventajas obvias para la aplicación, esto daba margen de maniobra al trabajar en la plataforma: cuando todos los componentes tienen una 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:

Source: https://habr.com/ru/post/457510/


All Articles