Jak zarządzać automatycznymi wiadomościami e-mail wysyłanymi z aplikacji internetowej

12

Projektuję aplikację internetową i zastanawiam się, jak zaprojektować architekturę do zarządzania wysyłaniem automatycznych wiadomości e-mail.

Obecnie mam tę funkcję wbudowaną w moją aplikację internetową, a e-maile są wysyłane na podstawie danych wejściowych / interakcji użytkownika (takich jak utworzenie nowego użytkownika). Problem polega na tym, że bezpośrednie połączenie z serwerem poczty zajmuje kilka sekund. Zwiększając moją aplikację, będzie to znacząca szyjka butelki w przyszłości.

Jaki jest najlepszy sposób zarządzania wysyłaniem dużej liczby automatycznych wiadomości e-mail w ramach architektury mojego systemu?

Nie będzie wysyłanej dużej liczby wiadomości e-mail (maksymalnie 2000 dziennie). E-maile nie muszą być wysyłane natychmiast, opóźnienie do 10 minut jest w porządku.

Aktualizacja: Kolejkowanie wiadomości podano jako odpowiedź, ale jak by to zaprojektować? Czy zostanie to załatwione w aplikacji i przetworzone w ciszy, czy też muszę utworzyć nową „aplikację pocztową” lub usługę internetową, aby po prostu zarządzać kolejką?

Gaz_Edge
źródło
Czy możesz dać nam przybliżone poczucie skali? Setki, tysiące lub miliony maili? Czy e-maile powinny być również wysyłane natychmiast, czy dopuszczalne jest niewielkie opóźnienie?
yannis
Wysyłanie wiadomości e-mail obejmuje przekazanie wiadomości SMTP do hosta poczty odbierającej, ale nie oznacza to, że wiadomość została faktycznie dostarczona. Tak skutecznie wszystkie wysyłane wiadomości e-mail są asynchroniczne i nie ma sensu udawać, że „czekają na sukces”.
Kilian Foth
1
Nie „czekam na sukces”, ale muszę czekać, aż serwer smtp zaakceptuje moją prośbę. @YannisRizos zobacz aktualizację RE twój komentarz
Gaz_Edge
W przypadku 2000 (co jest twoim opisanym maks.) Mailem będzie po prostu działać. Kiedy zdarzają się w powiedzmy 10 godzinach roboczych, to 3 maile na minutę, co jest bardzo wykonalne. Tylko upewnij się, że dobrze skonfigurowałeś swój rekord DNS, a dostawca akceptuje wysyłanie ich w tych kwotach. Pomyśl także: „co to jest serwer pocztowy?”. Obciążenie wysyłaniem 2000 maili nie jest powodem do niepokoju.
Luc Franken
Odpowiedź na pytanie, gdzie jest CRONTAB
Tulains Córdova,

Odpowiedzi:

15

Powszechnym podejściem, jak już wspomniano Ozz , jest kolejka komunikatów . Z perspektywy projektowej kolejka komunikatów jest zasadniczo kolejką FIFO , która jest raczej podstawowym typem danych:

Kolejka FIFO

Tym, co sprawia, że ​​kolejka komunikatów jest wyjątkowa, jest to, że chociaż twoja aplikacja jest odpowiedzialna za kolejkowanie, inny proces byłby odpowiedzialny za de-kolejkowanie. W kolejce lingo aplikacja jest nadawcą wiadomości, a proces usuwania z kolejki jest odbiorcą. Oczywistą zaletą jest to, że cały proces jest asynchroniczny, odbiorca działa niezależnie od nadawcy, o ile istnieją komunikaty do przetworzenia. Oczywistą wadą jest to, że potrzebujesz dodatkowego komponentu, nadawcy, aby całość działała.

Ponieważ twoja architektura opiera się teraz na dwóch komponentach wymieniających wiadomości, możesz użyć do tego fantazyjnego terminu komunikacji międzyprocesowej .

W jaki sposób wprowadzenie kolejki wpływa na projekt aplikacji?

Niektóre działania w aplikacji generują wiadomości e-mail. Wprowadzenie kolejki komunikatów oznaczałoby, że te akcje powinny teraz wypychać wiadomości do kolejki (i nic więcej). Wiadomości te powinny zawierać absolutnie minimalną ilość informacji niezbędnych do zbudowania wiadomości e-mail, gdy odbiorca je przetworzy.

Format i treść wiadomości

Format i treść wiadomości zależy wyłącznie od Ciebie, ale należy pamiętać, że im mniejsze, tym lepiej. Twoja kolejka powinna być zapisywana i przetwarzana tak szybko, jak to możliwe, a rzucenie na nią dużej ilości danych prawdopodobnie stworzy wąskie gardło.

Ponadto kilka usług kolejkowania opartych na chmurze ma ograniczenia dotyczące rozmiarów wiadomości i może dzielić większe wiadomości. Nie zauważysz, podzielone wiadomości będą podawane jako jedna, gdy o nie poprosisz, ale zostaniesz obciążony za wiele wiadomości (zakładając oczywiście, że korzystasz z usługi, która wymaga opłaty).

Konstrukcja odbiornika

Ponieważ mówimy o aplikacji internetowej, powszechnym podejściem dla twojego odbiornika byłby prosty skrypt cron. Działa co xminutę (lub sekundy) i:

  • Pop nilość wiadomości z kolejki,
  • Przetwarzaj wiadomości (tj. Wysyłaj wiadomości e-mail).

Zauważ, że mówię „pop” zamiast „pobierz” lub „pobierz”, ponieważ odbiorca nie tylko pobiera elementy z kolejki, ale także je usuwa (tj. Usuwa je z kolejki lub oznacza jako przetworzone). To, jak dokładnie to nastąpi, zależy od implementacji kolejki komunikatów i specyficznych potrzeb aplikacji.

Oczywiście to, co opisuję, jest zasadniczo operacją wsadową , najprostszym sposobem przetwarzania kolejki. W zależności od potrzeb możesz chcieć przetwarzać wiadomości w bardziej skomplikowany sposób (wymagałoby to również bardziej skomplikowanej kolejki).

ruch drogowy

Twój odbiornik może wziąć pod uwagę ruch i dostosować liczbę przetwarzanych wiadomości na podstawie ruchu w czasie jego działania. Uproszczone podejście polegałoby na przewidywaniu godzin intensywnego ruchu na podstawie przeszłych danych o ruchu i przy założeniu, że korzystasz ze skryptu cron, który uruchamia się co xminutę, możesz zrobić coś takiego:

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

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

Bardzo naiwne i brudne podejście, ale działa. Jeśli tak się nie stanie, drugim podejściem byłoby sprawdzenie aktualnego ruchu serwera na każdej iteracji i odpowiednie dostosowanie liczby elementów procesu. Nie dokonuj mikrooptymalizacji, jeśli nie jest to absolutnie konieczne, tracisz czas.

Miejsce w kolejce

Jeśli Twoja aplikacja korzysta już z bazy danych, najprostszym rozwiązaniem będzie pojedyncza tabela na niej:

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

To naprawdę nie jest bardziej skomplikowane. Możesz oczywiście uczynić to tak skomplikowanym, jak potrzebujesz, możesz na przykład dodać pole priorytetowe (co oznaczałoby, że nie jest to już kolejka FIFO, ale jeśli naprawdę jej potrzebujesz, kogo to obchodzi?). Możesz również uprościć to, pomijając przetworzone pole (ale wtedy będziesz musiał usunąć wiersze po ich przetworzeniu).

Tabela bazy danych byłaby idealna dla 2000 wiadomości dziennie, ale prawdopodobnie nie byłaby dobrze skalowana dla milionów wiadomości dziennie. Należy wziąć pod uwagę milion czynników, wszystko w twojej infrastrukturze odgrywa rolę w ogólnej skalowalności twojej aplikacji.

W każdym razie, zakładając, że już zidentyfikowałeś kolejkę opartą na bazie danych jako wąskie gardło, następnym krokiem byłoby przyjrzenie się usłudze opartej na chmurze. Amazon SQS to jedyna usługa, z której korzystałem i zrobił to, co obiecał. Jestem pewien, że istnieje wiele podobnych usług.

Kolejki oparte na pamięci również należy rozważyć, szczególnie w przypadku kolejek krótkotrwałych. memcached doskonale nadaje się do przechowywania w kolejce wiadomości.

Niezależnie od miejsca, w którym zdecydujesz się zbudować kolejkę, bądź sprytny i abstrakcyjny. Ani nadawca, ani odbiorca nie powinni być przywiązani do konkretnego magazynu, w przeciwnym razie przejście na inny magazyn w późniejszym czasie byłoby kompletną PITA.

Prawdziwe podejście do życia

Zbudowałem kolejkę wiadomości e-mail, która jest bardzo podobna do tego, co robisz. To było na projekcie PHP i zbudowałem go wokół Zend Queue , komponentu Zend Framework, który oferuje kilka adapterów do różnych magazynów. Moje magazyny, w których:

  • Tablice PHP do testowania jednostkowego,
  • Amazon SQS w produkcji,
  • MySQL w środowisku programistycznym i testowym.

Moje wiadomości były tak proste, jak tylko mogą być, moja aplikacja utworzyła małe tablice z niezbędnymi informacjami ( [user_id, reason]). Magazyn komunikatów był serializowaną wersją tej tablicy (najpierw był to wewnętrzny format serializacji PHP, potem JSON, nie pamiętam, dlaczego się zmieniłem). reasonJest stała i oczywiście mam duży gdzieś tabeli, który mapuje reasondo pełniejszych wyjaśnień (I udało się wysłać e-maile do około 500 klientów z tajemniczy reasonzamiast pełniejszego wiadomości raz).

Dalsza lektura

Standardy:

Przybory:

Ciekawe czyta:

Yannis
źródło
Łał. Prawie najlepsza odpowiedź, jaką tu kiedykolwiek otrzymałem! Nie mogę ci wystarczająco podziękować!
Gaz_Edge
Ja i jestem pewien, że miliony innych używają tego FIFO z Gmailem i Google Apps Script. filtr Gmaila oznacza każdą przychodzącą pocztę na podstawie kryteriów i to wszystko, umieszcza je w kolejce. Skrypt Google Apps uruchamia się co X, pobiera pierwsze wiadomości, wysyła je, usuwa z nich kolejkę. Spłucz i powtórz.
DavChana,
6

Potrzebujesz jakiegoś systemu kolejkowania.

Jednym prostym sposobem może być zapisanie w tabeli bazy danych i utworzenie w tej tabeli kolejnych wierszy procesu aplikacji zewnętrznej, ale istnieje wiele innych technologii kolejkowania, których można użyć.

Możesz mieć znaczenie w wiadomościach e-mail, aby niektóre z nich były wykonywane niemal natychmiast (na przykład resetowanie hasła), a te o mniejszym znaczeniu mogły być grupowane w celu wysłania później.

ozz
źródło
czy masz schemat architektury lub przykład, który pokazuje, jak to działa? Na przykład, czy kolejka znajduje się w innej „aplikacji”, powiedzmy w aplikacji poczty, czy też pobiera proces z aplikacji internetowej w ciszy. Czy powinienem stworzyć rodzaj usługi internetowej, aby je przetworzyć?
Gaz_Edge
1
@Gaz_Edge Twoja aplikacja wypycha elementy do kolejki. Proces w tle (najprawdopodobniej skrypt crona) wyrzuca x elementów z kolejki co n sekund i przetwarza je (w twoim przypadku wysyła wiadomość e-mail). Pojedyncza tabela bazy danych działa dobrze jako pamięć kolejek dla niewielkich ilości elementów, ale ogólnie mówiąc operacje zapisu w bazie danych są drogie i dla większych ilości możesz chcieć przyjrzeć się usługom takim jak SQS Amazon .
yannis
1
@Gaz_Edge Nie jestem pewien, czy potrafię to przedstawić w prostszy sposób niż to, co napisałem „... napisz do tabeli bazy danych i umieść w niej inne wiersze procesu aplikacji zewnętrznej ...”, a dla tabeli przeczytaj „dowolną kolejkę „bez względu na technologię.
ozz
1
(cd ...) Możesz zbudować proces w tle, który czyści kolejkę w sposób uwzględniający ruch, na przykład możesz poinstruować go, aby przetwarzał mniej elementów (lub wcale ich nie ma) w czasie, gdy serwer jest obciążony . Będziesz musiał albo przewidzieć stresujące czasy, patrząc na przeszłe dane o ruchu (łatwiejsze niż się wydaje, ale z dużym marginesem błędu) lub zlecając procesowi w tle sprawdzanie stanu ruchu za każdym razem, gdy jest uruchamiany (dokładniej, ale dodatkowe koszty ogólne są rzadko konieczne).
yannis
@YannisRizos chcesz połączyć swoje komentarze w odpowiedź? Pomocne byłyby również diagramy i projekty architektury (tym razem jestem zdeterminowany, aby uzyskać je z tego pytania! ;-))
Gaz_Edge
2

Nie będzie wysyłanej dużej liczby wiadomości e-mail (maksymalnie 2000 dziennie).

Oprócz kolejki drugą rzeczą, którą powinieneś rozważyć, jest wysyłanie wiadomości e-mail za pośrednictwem wyspecjalizowanych usług: na przykład MailChimp (nie jestem powiązany z tą usługą). W przeciwnym razie wiele usług pocztowych, takich jak Gmail, wkrótce wyśle ​​Twoje listy do folderu ze spamem.

OZ_
źródło
2

Modelowałem mój system kolejek w innej tabeli 2 jako;

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]

Istnieje relacja 1-1 między tymi tabelami.

Tabela wiadomości do przechowywania treści wiadomości. Rzeczywista treść (Do, CC, BCC, Temat, Treść itp.) Jest serializowana do pola Wykres w formacie XML. Inne informacje From, To służą tylko do zgłaszania problemów bez deserializacji wykresu. Oddzielenie tej tabeli pozwala podzielić zawartość tabeli na inną pamięć dyskową. Gdy będziesz gotowy do wysłania wiadomości, musisz przeczytać wszystkie informacje, dlatego nie ma nic złego w serializacji całej zawartości do jednej kolumny z indeksem klucza podstawowego.

Tabela MessageState do przechowywania stanu zawartości wiadomości z dodatkowymi informacjami opartymi na dacie. Oddzielenie tej tabeli pozwala na szybki dostęp do mechanizmu z dodatkowymi indeksami szybkiego przechowywania IO. Inne kolumny są już oczywiste.

Możesz użyć oddzielnej puli wątków, która skanuje te tabele. Jeśli aplikacja i pula znajdują się na tym samym komputerze, można użyć klasy EventWaitHandle do zasygnalizowania puli z aplikacji o czymś wstawionym do tych tabel, w przeciwnym razie najlepsze jest okresowe skanowanie z przekroczeniem limitu czasu.

ertan
źródło