Comment gérer les e-mails automatisés envoyés à partir d'une application Web

12

Je conçois une application web et je me demande comment concevoir l'architecture pour gérer l'envoi de courriels automatisés.

Actuellement, cette fonctionnalité est intégrée à mon application Web et les e-mails sont envoyés en fonction des entrées / interactions de l'utilisateur (comme la création d'un nouvel utilisateur). Le problème est que la connexion directe à un serveur de messagerie prend quelques secondes. Faire évoluer mon application, ce sera un goulot d'étranglement important à l'avenir.

Quelle est la meilleure façon de gérer l'envoi d'une grande quantité d'e-mails automatisés dans mon architecture système?

Il n'y aura pas énormément d'emails envoyés (2000 par jour maximum). Les e-mails n'ont pas besoin d'être envoyés immédiatement, jusqu'à 10 minutes de retard sont très bien.

Mise à jour: la mise en file d'attente des messages a été donnée comme réponse, mais comment cela serait-il conçu? Serait-ce géré dans l'application et traité pendant une période calme, ou dois-je créer une nouvelle «application de messagerie» ou un service Web pour gérer simplement la file d'attente?

Gaz_Edge
la source
Pouvez-vous nous donner une idée approximative de l'échelle? Des centaines, des milliers ou des millions de mails? De plus, les e-mails doivent-ils être envoyés immédiatement ou un petit décalage est-il acceptable?
yannis
L'envoi d'e-mails implique la remise d'un message SMTP à un hôte de messagerie destinataire, mais cela ne signifie pas que le message a effectivement été remis. Donc, efficacement, tout envoi d'e-mails est asynchrone, et il est inutile de prétendre "attendre le succès".
Kilian Foth,
1
Je n'attends pas le succès, mais je dois attendre que le serveur smtp accepte ma demande. @YannisRizos voir la mise à jour de votre commentaire
Gaz_Edge
Pour 2000 (qui est le maximum décrit), cela fonctionnera. Quand ils se produisent dans 10 heures ouvrées, c'est 3 mails par minute, ce qui est très faisable. Assurez-vous simplement de bien configurer votre enregistrement DNS et le fournisseur accepte que vous les envoyiez dans ces montants. Pensez aussi à: "qu'est-ce qui est en panne sur le serveur de messagerie?". La charge d'envoi de 2000 mails n'est pas quelque chose à craindre.
Luc Franken
La réponse à l'endroit où est CRONTAB
Tulains Córdova

Réponses:

15

L'approche commune, comme Ozz l'a déjà mentionné , est une file d'attente de messages . Du point de vue de la conception, une file d'attente de messages est essentiellement une file d'attente FIFO , qui est un type de données plutôt fondamental:

File d'attente FIFO

Ce qui rend une file d'attente de messages spéciale, c'est que même si votre application est responsable de la mise en file d'attente, un processus différent serait responsable de la mise en file d'attente. Dans le jargon de mise en file d'attente, votre application est l'expéditeur du ou des messages et le processus de retrait de la file d'attente est le destinataire. L'avantage évident est que l'ensemble du processus est asynchrone, le récepteur fonctionne indépendamment de l'expéditeur, tant qu'il y a des messages à traiter. L'inconvénient évident est que vous avez besoin d'un composant supplémentaire, l'expéditeur, pour que le tout fonctionne.

Étant donné que votre architecture repose désormais sur deux composants échangeant des messages, vous pouvez utiliser le terme de communication inter-processus de fantaisie pour cela.

Comment l'introduction d'une file d'attente affecte-t-elle la conception de votre application?

Certaines actions de votre application génèrent des e-mails. L'introduction d'une file d'attente de messages signifierait que ces actions devraient désormais pousser les messages vers la file d'attente (et rien de plus). Ces messages doivent contenir le minimum absolu d'informations nécessaires à la construction des e-mails lorsque votre destinataire les traite.

Format et contenu des messages

Le format et le contenu de vos messages dépendent entièrement de vous, mais vous devez garder à l'esprit le plus petit sera le mieux. Votre file d'attente doit être aussi rapide à écrire et à traiter que possible, y jeter une masse de données créera probablement un goulot d'étranglement.

En outre, plusieurs services de mise en file d'attente basés sur le cloud ont des restrictions sur la taille des messages et peuvent diviser des messages plus volumineux. Vous ne le remarquerez pas, les messages fractionnés seront servis comme un seul lorsque vous les demanderez, mais vous serez facturé pour plusieurs messages (en supposant bien sûr que vous utilisez un service payant).

Conception du récepteur

Comme nous parlons d'une application Web, une approche courante pour votre récepteur serait un simple script cron. Il fonctionnerait toutes les xminutes (ou secondes) et il:

  • Pop nquantité de messages de la file d'attente,
  • Traitez les messages (c'est-à-dire envoyez les e-mails).

Notez que je dis pop au lieu d'obtenir ou de récupérer, c'est parce que votre récepteur ne récupère pas seulement les éléments de la file d'attente, il les efface également (c'est-à-dire les supprimer de la file d'attente ou les marquer comme traités). La manière exacte dont cela se produira dépend de votre implémentation de la file d'attente de messages et des besoins spécifiques de votre application.

Bien sûr, ce que je décris est essentiellement une opération par lots , le moyen le plus simple de traiter une file d'attente. Selon vos besoins, vous souhaiterez peut-être traiter les messages de manière plus compliquée (cela nécessiterait également une file d'attente plus compliquée).

Circulation

Votre récepteur peut prendre en compte le trafic et ajuster le nombre de messages qu'il traite en fonction du trafic au moment de son exécution. Une approche simpliste serait de prédire vos heures de trafic élevé en fonction des données de trafic passées et en supposant que vous êtes allé avec un script cron qui s'exécute toutes les xminutes, vous pouvez faire quelque chose comme ceci:

if( 
    now() > 2pm && now() < 7pm
) {
    process(10);
} else {
    process(100);
}

function process(count) {
    for(i=0; i<=count; i++) {
        message = dequeue();
        mail(message)
    }
}

Une approche très naïve et sale, mais ça marche. Si ce n'est pas le cas, l'autre approche serait de découvrir le trafic actuel de votre serveur à chaque itération et d'ajuster le nombre d'éléments de processus en conséquence. Veuillez ne pas micro-optimiser si ce n'est pas absolument nécessaire, vous perdriez votre temps.

Stockage de file d'attente

Si votre application utilise déjà une base de données, alors une seule table dessus serait la solution la plus simple:

CREATE TABLE message_queue (
  id int(11) NOT NULL AUTO_INCREMENT,
  timestamp timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  processed enum('0','1') NOT NULL DEFAULT '0',
  message varchar(255) NOT NULL,
  PRIMARY KEY (id),
  KEY timestamp (timestamp),
  KEY processed (processed)
) 

Ce n'est vraiment pas plus compliqué que ça. Vous pouvez bien sûr le rendre aussi compliqué que vous le souhaitez, vous pouvez, par exemple, ajouter un champ prioritaire (ce qui signifierait qu'il ne s'agit plus d'une file d'attente FIFO, mais si vous en avez réellement besoin, peu importe?). Vous pouvez également le rendre plus simple, en sautant le champ traité (mais vous devrez alors supprimer les lignes après les avoir traitées).

Une table de base de données serait idéale pour 2000 messages par jour, mais elle ne serait probablement pas adaptée à des millions de messages par jour. Il y a un million de facteurs à considérer, tout dans votre infrastructure joue un rôle dans l'évolutivité globale de votre application.

Dans tous les cas, en supposant que vous avez déjà identifié la file d'attente basée sur la base de données comme un goulot d'étranglement, l'étape suivante serait d'examiner un service basé sur le cloud. Amazon SQS est le seul service que j'ai utilisé et a fait ce qu'il promet. Je suis sûr qu'il existe de nombreux services similaires.

Les files d'attente basées sur la mémoire sont également quelque chose à considérer, en particulier pour les files d'attente de courte durée. memcached est excellent comme stockage de file d'attente de messages.

Quel que soit le stockage sur lequel vous décidez de construire votre file d'attente, soyez intelligent et abstrait. Ni votre expéditeur ni votre récepteur ne doivent être liés à un stockage spécifique, sinon le basculement vers un autre stockage à une date ultérieure serait un PITA complet.

Approche de la vie réelle

J'ai créé une file d'attente de messages pour les e-mails très similaire à ce que vous faites. C'était sur un projet PHP et je l'ai construit autour de Zend Queue , un composant du Framework Zend qui propose plusieurs adaptateurs pour différents stockages. Mes stockages où:

  • Tableaux PHP pour les tests unitaires,
  • Amazon SQS en production,
  • MySQL sur les environnements de développement et de test.

Mes messages étaient aussi simples que possible, mon application a créé de petits tableaux avec les informations essentielles ( [user_id, reason]). Le magasin de messages était une version sérialisée de ce tableau (d'abord c'était le format de sérialisation interne de PHP, puis JSON, je ne me souviens pas pourquoi j'ai changé). C'est reasonune constante et bien sûr, j'ai un grand tableau quelque part qui correspond reasonà des explications plus complètes (j'ai réussi à envoyer environ 500 e-mails aux clients avec le cryptique reasonau lieu du message plus complet une fois).

Lectures complémentaires

Normes:

Outils:

Lectures intéressantes:

yannis
la source
Sensationnel. À peu près la meilleure réponse que j'ai jamais reçue ici! Je ne vous remercierai jamais assez!
Gaz_Edge
Moi, et je suis sûr que des millions d'autres utilisent ce FIFO avec Gmail et Google Apps Script. un filtre Gmail étiquette tout courrier entrant en fonction d'un critère et c'est tout, les met en file d'attente. Un script Google Apps s'exécute toutes les durées X, obtient les premiers messages y, les envoie, les retire de la file d'attente. Rincer et répéter.
DavChana
6

Vous avez besoin d'une sorte de système de file d'attente.

Une façon simple pourrait être d'écrire dans une table de base de données et d'avoir une autre ligne de processus d'application externe dans cette table, mais il existe de nombreuses autres technologies de mise en file d'attente que vous pourriez utiliser.

Vous pouvez avoir une importance sur les e-mails afin que certains soient traités presque immédiatement (réinitialisation du mot de passe par exemple), et ceux de moindre importance peuvent être groupés pour être envoyés plus tard.

ozz
la source
avez-vous un schéma ou un exemple d'architecture qui montre comment cela fonctionne? Par exemple, la file d'attente se trouve-t-elle dans une autre «application», par exemple l'application de messagerie, ou obtient-elle un processus à partir de l'application Web pendant une période de silence. Ou dois-je créer une sorte de service web pour les traiter?
Gaz_Edge
1
@Gaz_Edge Votre application envoie des éléments dans la file d'attente. Un processus d'arrière-plan (un script cron le plus probable) fait apparaître x éléments de la file d'attente toutes les n secondes et les traite (dans votre cas, envoie l'e-mail). Une seule table de base de données fonctionne bien comme stockage de file d'attente pour de petites quantités d'éléments, mais en règle générale, les opérations d'écriture sur une base de données sont coûteuses et pour des quantités plus importantes, vous voudrez peut-être regarder des services comme SQS d'Amazon .
yannis
1
@Gaz_Edge Je ne suis pas sûr de pouvoir le représenter plus simplement que ce que j'ai écrit "... écrire dans une table de base de données et avoir une autre ligne de processus d'application externe dans cette table ...." et pour la table, lire "n'importe quelle file d'attente "quelle que soit la technologie qui pourrait être.
ozz
1
(suite ...) Vous pouvez créer le processus d'arrière-plan qui efface la file d'attente d'une manière qui prend en considération votre trafic, par exemple, vous pouvez lui demander de traiter moins d'éléments (ou aucun du tout) lorsque votre serveur est stressé . Vous devrez soit prédire ces moments stressants en regardant vos données de trafic passées (plus faciles qu'il n'y paraît, mais avec une grande marge d'erreur) ou en faisant vérifier par votre processus d'arrière-plan l'état du trafic à chaque fois qu'il s'exécute (plus précis, mais les frais généraux ajoutés sont rarement nécessaires).
yannis
@YannisRizos veut combiner vos commentaires en une réponse? De plus, des diagrammes et des conceptions d'architecture seraient utiles (je suis déterminé à les obtenir de cette question cette fois! ;-))
Gaz_Edge
2

Il n'y aura pas énormément d'emails envoyés (2000 par jour maximum).

En plus de la file d'attente, la deuxième chose à considérer est l'envoi d'e-mails via des services spécialisés: MailChimp, par exemple (je ne suis pas affilié à ce service). Sinon, de nombreux services de messagerie, tels que gmail, enverront bientôt vos lettres dans un dossier spam.

OZ_
la source
2

J'ai modélisé mon système de file d'attente dans 2 tableaux différents comme;

CREATE TABLE [dbo].[wMessages](
  [Id] [uniqueidentifier]  NOT NULL,
  [FromAddress] [nvarchar](255) NOT NULL,
  [FromDisplayName] [nvarchar](255) NULL,
  [ToAddress] [nvarchar](255) NOT NULL,
  [ToDisplayName] [nvarchar](255) NULL,
  [Graph] [xml] NOT NULL,
  [Priority] [int] NOT NULL,
  PRIMARY KEY CLUSTERED ( [Id] ASC ))

CREATE TABLE [dbo].[wMessageStates](
  [MessageId] [uniqueidentifier] NOT NULL,
  [Status] [int] NOT NULL,
  [LastChange] [datetimeoffset](7) NOT NULL,
  [SendAfter] [datetimeoffset](7) NULL,
  [SendBefore] [datetimeoffset](7) NULL,
  [DeleteAfter] [datetimeoffset](7) NULL,
  [SendDate] [datetimeoffset](7) NULL,
  PRIMARY KEY CLUSTERED ( [MessageId] ASC )) ON [PRIMARY]
) ON [PRIMARY]

Il existe une relation 1-1 entre ces tables.

Tableau des messages pour stocker le contenu du message. Le contenu réel (To, CC, BCC, Subject, Body, etc.) est sérialisé dans le champ Graph au format XML. Les informations Autre de, À sont uniquement utilisées pour signaler les problèmes sans désérialiser le graphique. La séparation de cette table permet de partitionner le contenu de la table sur un autre stockage sur disque. Une fois que vous êtes prêt à envoyer un message, vous devez lire toutes les informations, donc rien de mal à sérialiser tout le contenu sur une colonne avec l'index de clé primaire.

Table MessageState pour stocker l'état du contenu du message avec des informations supplémentaires basées sur la date. La séparation de cette table permet un mécanisme d'accès rapide avec des index supplémentaires sur le stockage d'E / S rapide. D'autres colonnes sont déjà explicites.

Vous pouvez utiliser un pool de threads séparé qui analyse ces tables. Si l'application et le pool vivent sur la même machine, vous pouvez utiliser une classe EventWaitHandle pour signaler au pool de l'application quelque chose inséré dans ces tables, sinon une analyse périodique avec un délai d'attente est la meilleure.

ertan
la source