
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:
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: