Envío masivo multicanal en Redis

Introductorio



¡Hola, Habr! Mi nombre es Boris y en este trabajo compartiré con ustedes mi experiencia en el diseño e implementación de un servicio de mailing masivo como parte de un sistema integral para alertar a los estudiantes sobre los maestros (en adelante también referido como Ada), el cual también implemento.







Infierno



Luego es necesario anular el número de interrupciones en el proceso educativo por las siguientes razones:



  1. Los profesores no quieren compartir datos personales de contacto;
  2. Los estudiantes también lo son, simplemente no tienen muchas opciones;
  3. Debido a las especificidades de mi alma mater, muchos profesores se ven obligados o prefieren utilizar dispositivos móviles sin acceso a Internet;
  4. Si envías mensajes a través de los líderes de los grupos, entonces entra en juego el efecto de un "teléfono dañado", así como el factor "oh, lo olvidé :(".


Se está ejecutando sobre el modo :



  1. El profesor a través de uno de los canales de comunicación que tiene a su disposición: SMS, Telegram, aplicación SPA - envía a Ada el texto del mensaje y la lista de destinatarios;
  2. Ada transmite el mensaje recibido a todos los estudiantes interesados ​​* a través de varios canales de comunicación.


* El acceso al servicio se proporciona mediante solicitud voluntaria.



Se asume que



  1. El número total de usuarios no superará los diez mil;
  2. La proporción alumno - maestro / miembro de la Oficina de Asuntos Internos (decanos, centro de salud, mostrador de registro militar, etc.) se mantendrá en el nivel de 10: 1;
  3. : « », « ))0» ..




  1. ;
  2. , , ;
  3. ;
  4. : - - , , .


Este trabajo consta de cinco partes: introductoria, preparatoria, conceptual, temática y final.



Puede omitir de manera segura la parte preparatoria si está familiarizado con la interpretación de Redis del patrón Pub / Sub, así como con los mecanismos de eventos, la secuencia de comandos LUA y el manejo de claves obsoletas; además, es muy conveniente tener al menos alguna idea de la arquitectura de microservicio del software.



En la parte del tema, se revisa el código en Python, pero creo que hay suficiente información para que pueda escribir algo como esto en cualquier cosa.



Preparatorio



Muy tosco y muy abstracto ~ 5 minutos
Redis — [BSD 3-clause] , «-» ().



, .



, -, .



( ).



, , , LUA 5.1.



Detalles y de primera mano ~ 15 minutos
  1. Pub/Sub — Redis. , fire&forget , , PUBLISH, SUBSCRIBE -;
  2. Redis Keyspace Notifications. ;
  3. EXPIRE — Redis. «How Redis expires keys»;
  4. Redis 6.0 Default Configuration File. . 939:948 (The default effort of the expire cycle…);
  5. EVAL — Redis. EVAL EVALSHA, «Atomicity of scripts», «Global variables protection» «Available libraries», cjson;
  6. Redis Lua Scripts Debugger. , . — ;
  7. . , .


Conceptual



Enfoque ingenuo



La solución más obvia en la que pueda pensar: múltiples métodos de entrega ( send_vk, send_telegrametc.) y un controlador que los llamará con los argumentos requeridos.



Problema de extensibilidad



Si queremos agregar un nuevo método de entrega, nos veremos obligados a modificar el código existente, y estas son las limitaciones de la plataforma de software.



Problema de estabilidad



Uno de los métodos se ha roto = todo el servicio se ha roto.



Problema aplicado



Las API de diferentes canales de comunicación difieren significativamente entre sí en términos de interacción. Por ejemplo, VKontakte admite envíos masivos, pero no más de cientos de usuarios por llamada. Telegram no existe, pero permite más llamadas por segundo.



La API de VK solo funciona a través de HTTP; Telegram tiene una puerta de enlace HTTP, pero es menos estable que MTProto y está menos documentado.



Existen muchas de estas diferencias: longitud máxima del mensaje random_id, interpretación y manejo de errores, etc. etc.



Como lidiar con esto?



Se decidió separar el proceso de colocación de mensajes en cola y los procesos de envío (en adelante, couriers) a nivel organizacional, para que el primero ni siquiera sospechara la existencia de los segundos, y viceversa, y Redis actuara como enlace de conexión entre ellos.



¿Poco claro? ¡Pedir una comida!



Mientras tanto, está esperando, permítame presentarle mi interpretación de esta noble acción, comenzando con el diseño y terminando con la puerta cerrada detrás del mensajero.







  1. Haces clic en el gran botón amarillo "Pedido";
  2. Yandex.Food busca un mensajero, informa al restaurante sobre los artículos seleccionados y le devuelve el número de pedido para diluir la incertidumbre de las expectativas;
  3. Una vez finalizada la cocción, el restaurante actualiza el estado del pedido y entrega la comida al mensajero;
  4. El mensajero, a su vez, le entrega la comida y luego marca el pedido como completado.


¡Buen provecho!



Volver al diseño



Es posible que el modelo dado en el párrafo anterior no se corresponda completamente con la realidad, pero fue ella quien formó la base de la solución desarrollada.



Los datos asociados con el número de pedido se llamarán historial , le permite responder las siguientes preguntas en cualquier momento :



  1. Quién envió;
  2. Lo que envió;
  3. De donde;
  4. A quien;
  5. Quién lo consiguió y cómo.


El historial se crea junto con el pedido como dos claves Redis independientes, vinculadas mediante un sufijo:



suffix={ }:{UNIX-  }
=history:{suffix}
=delivery:{suffix}


El pedido determina cuándo los mensajeros verán el historial una vez, de modo que, una vez completado el envío, la respuesta a la pregunta "Quién lo recibió y cómo" se cambiará en consecuencia.



La "visión" de los mensajeros funciona mediante una suscripción a las DELclaves del evento en el formulario delivery:*.



Cuando llega el momento de la entrega, Redis borra la clave del pedido, tras lo cual los mensajeros comienzan a procesarlo.



Dado que hay varios mensajeros, existe una alta probabilidad de competencia en la etapa de cambio histórico.







Puede evitarlo definiendo la operación correspondiente de forma atómica; en Redis, esto se hace a través de secuencias de comandos LUA.



Los detalles de implementación se discutirán en detalle en el próximo capítulo. Ahora es importante tener una idea clara de la solución en su conjunto, lo que puede ayudar con la figura siguiente.







Estado de seguimiento


El cliente puede rastrear el estado de la entrega a través de la clave de historial que se genera mediante un método de API separado del servicio que se está desarrollando antes de que el mensaje se ponga en cola (al igual que el número de pedido lo genera Yandex.Food al principio).



Después de que se genera la clave, se cuelga un rastreador con un tiempo de espera (opcionalmente y también por un método separado), que monitoreará el número de cambios en el historial por parte de los mensajeros ( SETeventos). Solo que ahora el mensaje está en cola.







Si el mensajero no encuentra los contactos del destinatario en su dominio, el canal de comunicación, activa un evento artificial a SETtravés del comando PUBLISH, lo que demuestra que está "bien" y que no hay necesidad de esperar más.



¿Por qué meterse con eventos en Redis si tienes RabbitMQ y apio?



Hay al menos cinco razones objetivas para esto:



  1. Redis , RabbitMQ/Celery — ;
  2. Redis , , , IPC;
  3. Redis’a SQL- ;
  4. . , API-, ;
  5. Celery asyncio, asyncio .




El sistema de notificación (que abarca) se implementa en forma de un conjunto de microservicios. Por conveniencia, las interfaces, los métodos para inicializar capas de datos, el texto de error, así como algunos bloques de lógica repetitiva se han movido a la biblioteca core, que, a su vez, se basa en: gino(envoltorio asyncio SQLAlchemy) aioredisy aiohttp.



Puede ver diferentes entidades en el código, por ejemplo User, Contacto Allegiance. Las conexiones entre ellos se presentan en el diagrama a continuación, una breve descripción se encuentra debajo del spoiler.





Acerca de las entidades ~ 3 minutos
— .



: , , . ., .



, : , Telegram, . .



[allegiance].



[supergroup].



[ownership] .



Generando una clave de historial



delivery / handlers / history_key / get - GitHub



Cola



delivery / handlers / queue / put -



Nota de GitHub :



  1. Comentario 171: 174;
  2. Que todas las manipulaciones con Redis [164: 179] están envueltas en una transacción.


Vista de los mensajeros [94: 117]



núcleo / entrega - GitHub



Actualización del historial por mensajería



core / redis_lua - GitHub Las



instrucciones [48:60] no convierten listas vacías en diccionarios ( [] -> {}), ya que la mayoría de los lenguajes de programación, incluido CPython, los interpretan de manera diferente a LUA.



ISS: permite la diferenciación de matrices y objetos para una correcta serialización de objetos vacíos - GitHub



Rastreador



delivery / handlers / track / post - GitHub - implementación.

connect / telegram / handlers / select - GitHub [101: 134] - ejemplo de uso en la interfaz de usuario.



Mensajeros



Cualquier entrega de task_stream(@Sight Couriers) se maneja en una corrutina asyncio separada.



La estrategia general para lidiar con las limitaciones de tiempo de las API es la siguiente: no contamos RPS (solicitudes por segundo), pero reaccionamos / respondemos correctamente a las respuestas por tipo http.TooManyRequests.



Si la interfaz implementa, además de global (para la aplicación), límites de tiempo personalizados, entonces se procesan en el orden de la cola, es decir Primero enviamos a todos los que podemos y solo entonces comenzamos a esperar, si no mucho.



Telegrama



courier / telegram - GitHub

Como se señaló anteriormente, la interfaz MTProto de Telegram supera a su contraparte HTTP en términos de estabilidad y tamaño de la documentación. Para interactuar con él, usaremos una solución lista para usar, a saber, LonamiWebs / Telethon .



En contacto con



courier / vk: la

API de GitHub VKontakte admite envíos masivos pasando una lista de identificadores al método messages.send (no más de cien), y también le permite "pegar" hasta veinticinco messages.senden una ejecución , lo que nos da 2500 mensajes por llamada.



Hecho curioso
API, execute , .



El final



En este trabajo se propone un método para organizar un sistema de alerta masiva multicanal. La solución resultante satisface la solicitud (@ Requisitos clave para el servicio de mailing) de la mayoría de las partes interesadas, y también asume la posibilidad de expansión.



La principal desventaja es el efecto Pub / Sub de fuego y olvido, es decir, si es necesario borrar la clave de pedido en el momento de la enfermedad de uno de los mensajeros, nadie recibirá nada en el dominio correspondiente, lo que quedará reflejado en el historial.



All Articles