Introduction
Bonjour, Habr! Je m'appelle Boris et dans ce travail, je partagerai avec vous mon expérience dans la conception et la mise en œuvre d'un service de publipostage de masse dans le cadre d'un système complet de notification des étudiants par les enseignants (ci-après également dénommé Ada), que je mets également en œuvre.
Enfer
Il est alors nécessaire d' annuler le nombre d'interruptions dans le processus éducatif pour les raisons suivantes:
- Les enseignants ne veulent pas partager leurs coordonnées personnelles;
- Les étudiants le sont vraiment aussi - ils n'ont tout simplement pas le choix;
- En raison des spécificités de mon alma mater, de nombreux enseignants sont forcés ou préfèrent utiliser des appareils mobiles sans accès à Internet;
- Si vous envoyez des messages via les dirigeants des groupes, alors l'effet d'un "téléphone endommagé" entre en jeu, ainsi que le facteur "oh, j'ai oublié :(".
Il tourne à peu près ainsi :
- L'enseignant via l'un des canaux de communication dont il dispose: SMS, télégramme, application SPA - envoie à Ada le texte du message et la liste des destinataires;
- Ada diffuse le message reçu à tous les étudiants intéressés * via différents canaux de communication.
* L'accès au service est fourni sur la base d'une demande volontaire.
Il est entendu que
- Le nombre total d'utilisateurs ne dépassera pas dix mille;
- Le ratio élèves-enseignant / membre du Bureau des affaires internes (bureau du doyen, centre de santé, bureau d'enregistrement militaire, etc.) sera maintenu au niveau de 10: 1;
- : « », « ))0» ..
- ;
- , , ;
- ;
- : - - , , .
Ce travail se compose de cinq parties: introductive, préparatoire, conceptuelle, sujet et finale.
Vous pouvez sauter la partie préparatoire en toute sécurité si vous êtes familier avec l'interprétation Redis du modèle Pub / Sub, ainsi que les mécanismes d'événements, les scripts LUA et la gestion des clés obsolètes.De plus, il est hautement souhaitable d'avoir au moins une idée de l'architecture de microservice du logiciel.
Dans la partie sujet, le code en Python est examiné, mais je pense qu'il y a suffisamment d'informations pour que vous puissiez écrire quelque chose comme ça sur n'importe quoi.
Préparatoire
Très rugueux et très abstrait ~ 5 minutes
Redis — [BSD 3-clause] , «-» ().
, .
, -, .
( ).
, , , LUA 5.1.
, .
, -, .
( ).
, , , LUA 5.1.
Détail et première main ~ 15 minutes
- Pub/Sub — Redis. , fire&forget , ,
PUBLISH
,SUBSCRIBE
-; - Redis Keyspace Notifications. ;
- EXPIRE — Redis. «How Redis expires keys»;
- Redis 6.0 Default Configuration File. . 939:948 (The default effort of the expire cycle…);
- EVAL — Redis.
EVAL
EVALSHA
, «Atomicity of scripts», «Global variables protection» «Available libraries»,cjson
; - Redis Lua Scripts Debugger. , . — ;
- . , .
Conceptuel
Approche naïve
La solution la plus évidente à laquelle vous pouvez penser: plusieurs méthodes de livraison (
send_vk
, send_telegram
etc.) et un gestionnaire qui les appellera avec les arguments requis.
Problème d'extensibilité
Si nous voulons ajouter une nouvelle méthode de livraison, nous serons obligés de modifier le code existant, et ce sont les limites de la plate-forme logicielle.
Problème de stabilité
L'une des méthodes est tombée en panne = tout le service est en panne.
Problème appliqué
Les API des différents canaux de communication diffèrent considérablement les unes des autres en termes d'interaction. Par exemple, VKontakte prend en charge les envois en masse, mais pas plus d'une centaine d'utilisateurs par appel. Telegram n'existe pas, mais il permet plus d'appels par seconde.
L'API VK fonctionne uniquement via HTTP; Telegram a une passerelle HTTP, mais elle est moins stable que MTProto et est moins bien documentée.
Il existe de nombreuses différences de ce type: longueur maximale du message
random_id
, interprétation et traitement des erreurs, etc. etc.
Comment gérer cela?
Il a été décidé de séparer le processus de placement des messages dans la file d'attente et les processus d'envoi (ci-après dénommés courriers) au niveau organisationnel, afin que le premier ne soupçonne même pas l'existence du second et vice versa, et Redis agirait comme un lien de connexion entre eux.
Pas clair? Commander un repas!
En attendant, vous attendez - laissez-moi vous présenter mon interprétation de cette noble action, en commençant par la conception et en terminant par la porte fermée derrière le courrier.
- Vous cliquez sur le gros bouton jaune "Commander";
- Yandex.Food trouve un coursier, informe le restaurant des articles sélectionnés et vous renvoie le numéro de commande afin de diluer l'incertitude des attentes;
- À la fin de la cuisson, le restaurant met à jour le statut de la commande et remet la nourriture au courrier;
- Le courrier, à son tour, vous donne la nourriture, puis marque la commande comme terminée.
Bon appétit!
Retour au design
Il est possible que le modèle donné dans le paragraphe précédent ne corresponde pas entièrement à la réalité, mais c'est elle qui a formé la base de la solution développée.
Les données associées au numéro de commande seront appelées historique , cela vous permet de répondre à tout moment aux questions suivantes:
- Qui a envoyé;
- Ce qu'il a envoyé;
- D'où;
- À qui;
- Qui l'a eu et comment.
L'historique est créé avec la commande sous forme de deux clés Redis distinctes, liées via un suffixe:
suffix={ }:{UNIX- }
=history:{suffix}
=delivery:{suffix}
La commande détermine à quel moment les courriers verront l'historique une fois, de sorte que, une fois l'envoi terminé, la réponse à la question «Qui l'a reçu et comment» sera modifiée en conséquence.
La «vision» des coursiers fonctionne grâce à un abonnement aux
DEL
clés d' événement dans le formulaire delivery:*
.
Au moment de la livraison, Redis supprime la clé de commande, après quoi les coursiers commencent à la traiter.
Puisqu'il y a plusieurs coursiers, il y a une forte probabilité de concurrence au stade du changement historique.
Vous pouvez l'éviter en définissant l'opération correspondante de manière atomique - dans Redis, cela se fait via le script LUA.
Les détails de la mise en œuvre seront discutés en détail dans le chapitre suivant. Il est maintenant important d'avoir une idée claire de la solution dans son ensemble, ce qui peut être aidé par la figure ci-dessous.
Statut de suivi
Le client peut suivre l'état de la livraison via la clé d'historique, qui est générée par une méthode API distincte du service en cours de développement avant que le message ne soit mis en file d'attente (tout comme le numéro de commande est généré par Yandex.Food au tout début).
Une fois la clé générée, un tracker avec un délai d'expiration est suspendu (éventuellement et également par une méthode distincte), qui surveillera le nombre de changements d'historique par les courriers (
SET
événements). Ce n'est que maintenant que le message est mis en file d'attente.
Si le courrier ne trouve pas de contacts destinataires dans son domaine - le canal de communication, il déclenche un événement artificiel
SET
via la commande PUBLISH
, montrant ainsi qu'il est «bien» et qu'il n'est plus nécessaire d'attendre.
Pourquoi jouer avec les événements dans Redis lorsque vous avez RabbitMQ et Celery
Il y a au moins cinq raisons objectives à cela:
Le système de notification (englobant) est implémenté sous la forme d'un ensemble de microservices. Pour des raisons de commodité, les interfaces, les méthodes d'initialisation des couches de données, le texte d'erreur, ainsi que certains blocs de logique répétitive ont été déplacés vers la bibliothèque
core
, qui, à son tour, repose sur: gino
(asyncio wrapper SQLAlchemy
) aioredis
et aiohttp
.
Vous pouvez voir différentes entités dans le code, par exemple
User
, Contact
ou Allegiance
. Les connexions entre eux sont présentées dans le schéma ci-dessous, une brève description se trouve sous le spoiler.
À propos des entités ~ 3 minutes
— .
: , , . ., .
, : , Telegram, . .
[allegiance].
[supergroup].
[ownership] .
: , , . ., .
, : , Telegram, . .
[allegiance].
[supergroup].
[ownership] .
Générer une clé d'historique
livraison / handlers / history_key / get - GitHub
Queue
delivery / handlers / queue / put -
Remarque GitHub :
- Commentaire 171: 174;
- Que toutes les manipulations avec Redis [164: 179] sont enveloppées dans une transaction.
Vue des courriers [94: 117]
core / livraison - GitHub
Mise à jour de l'historique par les coursiers
core / redis_lua - GitHub Les
instructions [48:60] ne convertissent pas les listes vides en dictionnaires (
[] -> {}
), car la plupart des langages de programmation, y compris CPython, les interprètent différemment de LUA.
ISS: autoriser la différenciation des tableaux et des objets pour une sérialisation correcte des objets vides - GitHub
Traqueur
livraison / handlers / track / post - GitHub - implémentation.
connect / telegram / handlers / select - GitHub [101: 134] - exemple d'utilisation dans l'interface utilisateur.
Courriers
Toute livraison depuis
task_stream
(@Sight Couriers) est gérée dans une coroutine asyncio distincte.
La stratégie générale pour gérer les contraintes de temps des API est la suivante: nous ne comptons pas les RPS (requêtes par seconde), mais nous / réagissons / répondons correctement aux réponses par type
http.TooManyRequests
.
Si l'interface implémente, en plus des délais globaux (pour l'application), des délais personnalisés, ils sont traités dans l'ordre de la file d'attente, c'est-à-dire nous envoyons d'abord à tout le monde que nous pouvons et alors seulement nous commençons à attendre, sinon très longtemps.
Télégramme
courier / telegram - GitHub
Comme indiqué précédemment, l'interface MTProto de Telegram surpasse son homologue HTTP en termes de stabilité et de taille de la documentation. Pour interagir avec lui, nous utiliserons une solution toute faite, à savoir LonamiWebs / Telethon .
En contact avec
courier / vk - L'
API GitHub VKontakte prend en charge les envois en masse en passant une liste d'identifiants à la méthode messages.send (pas plus d'une centaine), et vous permet également de «coller» jusqu'à vingt-cinq
messages.send
en une exécution , ce qui nous donne 2500 messages par appel.
Fait curieux
API,
execute
, .
Le final
Dans ce travail, une méthode d'organisation d'un système d'alerte de masse multicanal est proposée. La solution qui en résulte satisfait la demande (@ Exigences clés pour le service de messagerie) de la plupart des parties intéressées et suppose également la possibilité d'une expansion.
Le principal inconvénient est l'effet Fire & forget Pub / Sub, c'est-à-dire si la suppression de la clé de commande est nécessaire au moment de la maladie de l'un des courriers, personne ne recevra rien dans le domaine correspondant, ce qui, cependant, sera reflété dans l'historique.