Como gerenciar emails automatizados enviados de um aplicativo Web


12

Estou projetando um aplicativo Web e estou pensando em como projetar a arquitetura para gerenciar o envio de emails automatizados.

Atualmente, tenho esse recurso integrado ao meu aplicativo Web e os emails são enviados com base nas interações / entradas do usuário (como a criação de um novo usuário). O problema é que a conexão direta a um servidor de email leva alguns segundos. Ampliando minha aplicação, este será um gargalo significativo no futuro.

Qual é a melhor maneira de gerenciar o envio de uma grande quantidade de emails automatizados na arquitetura do meu sistema?

Não haverá uma quantidade enorme de emails enviados (no máximo 2000 por dia). Os e-mails não precisam ser enviados imediatamente, até 10 minutos de atraso é bom.

Atualização: o serviço de enfileiramento de mensagens foi fornecido como resposta, mas como isso seria projetado? Isso seria tratado no aplicativo e processado durante um período silencioso ou preciso criar um novo 'aplicativo de email' ou serviço da web para gerenciar apenas a fila?


Você pode nos dar uma sensação aproximada de escala? Centenas, milhares ou milhões de e-mails? Além disso, os e-mails devem ser enviados imediatamente ou um pequeno atraso é aceitável?
precisa

O envio de email envolve a entrega de uma mensagem SMTP a um host de email de recebimento, mas isso não significa que a mensagem foi realmente entregue. Com tanta eficiência, todo envio de email é assíncrono e não faz sentido fingir "esperar pelo sucesso".
Kilian Foth 19/03/2013

1
Não estou "esperando pelo sucesso", mas preciso aguardar o servidor smtp para aceitar minha solicitação. @YannisRizos see update RE seu comentário
Gaz_Edge

Para os e-mails de 2000 (que é o máximo descrito), ele funcionará. Quando eles acontecem, digamos, 10 horas úteis, são 3 e-mails por minuto, o que é muito viável. Apenas certifique-se de configurar bem seu registro DNS e o provedor aceita que você os envie nesses valores. Pense também em: "o que o servidor de mensagens está inativo?". A carga de envio de 2000 e-mails não é algo para se preocupar.
Luc Franken

A resposta para onde é crontab
Tulains Córdova

Respostas:


15

A abordagem comum, como Ozz já mencionou , é uma fila de mensagens . De uma perspectiva de design, uma fila de mensagens é essencialmente uma fila FIFO , que é um tipo de dados bastante fundamental:

Fila FIFO

O que torna uma fila de mensagens especial é que, embora seu aplicativo seja responsável pelo enfileiramento, um processo diferente seria responsável pelo desenfileiramento. No jargão da fila, seu aplicativo é o remetente da (s) mensagem (s) e o processo de remoção da fila é o destinatário. A vantagem óbvia é que todo o processo é assíncrono, o destinatário trabalha independentemente do remetente, desde que haja mensagens a serem processadas. A desvantagem óbvia é que você precisa de um componente extra, o remetente, para que tudo funcione.

Como sua arquitetura agora conta com dois componentes que trocam mensagens, você pode usar o termo sofisticado comunicação entre processos para ela.

Como a introdução de uma fila afeta o design do seu aplicativo?

Certas ações em seu aplicativo geram emails. A introdução de uma fila de mensagens significaria que essas ações agora devem enviar mensagens para a fila (e nada mais). Essas mensagens devem conter a quantidade mínima absoluta de informações necessárias para criar os emails quando o destinatário os processar.

Formato e conteúdo das mensagens

O formato e o conteúdo de suas mensagens dependem totalmente de você, mas lembre-se de que quanto menor, melhor. Sua fila deve ser o mais rápida possível para gravar e processar, lançar uma grande quantidade de dados provavelmente criará um gargalo.

Além disso, vários serviços de enfileiramento na nuvem têm restrições no tamanho das mensagens e podem dividir mensagens maiores. Você não notará que as mensagens divididas serão exibidas quando solicitadas, mas você será cobrado por várias mensagens (supondo que você esteja usando um serviço que exige uma taxa).

Projeto do receptor

Como estamos falando de um aplicativo Web, uma abordagem comum para o seu receptor seria um simples script cron. Seria executado a cada xminuto (ou segundos) e:

  • Pop nquantidade de mensagens da fila,
  • Processe as mensagens (por exemplo, envie os e-mails).

Observe que estou dizendo pop em vez de obter ou buscar, porque o receptor não está apenas recebendo os itens da fila, mas também os limpando (por exemplo, removendo-os da fila ou marcando-os como processados). Como exatamente isso vai acontecer depende da sua implementação da fila de mensagens e das necessidades específicas do seu aplicativo.

É claro que o que estou descrevendo é essencialmente uma operação em lote , a maneira mais simples de processar uma fila. Dependendo das suas necessidades, convém processar as mensagens de uma maneira mais complicada (isso também exigiria uma fila mais complicada).

Tráfego

Seu receptor pode levar em consideração o tráfego e ajustar o número de mensagens processadas com base no tráfego no momento em que é executado. Uma abordagem simplista seria prever o horário de alto tráfego com base em dados de tráfego anteriores e supondo que você seguisse um script cron que é executado a cada xminuto; você poderia fazer algo assim:

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

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

Uma abordagem muito ingênua e suja, mas funciona. Se isso não acontecer, bem, a outra abordagem seria descobrir o tráfego atual do seu servidor a cada iteração e ajustar o número de itens do processo de acordo. Por favor, não micro-otimize, se não for absolutamente necessário, você estaria perdendo seu tempo.

Armazenamento na fila

Se seu aplicativo já usa um banco de dados, uma única tabela seria a solução mais simples:

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

Realmente não é mais complicado que isso. É claro que você pode torná-lo o mais complicado possível, por exemplo, adicionar um campo prioritário (o que significa que isso não é mais uma fila FIFO, mas se você realmente precisar, quem se importa?). Você também pode simplificar, ignorando o campo processado (mas precisará excluir as linhas depois de processá-las).

Uma tabela de banco de dados seria ideal para 2000 mensagens por dia, mas provavelmente não seria adequada para milhões de mensagens por dia. Há um milhão de fatores a serem considerados, tudo na sua infraestrutura desempenha um papel na escalabilidade geral do seu aplicativo.

De qualquer forma, supondo que você já tenha identificado a fila baseada em banco de dados como um gargalo, a próxima etapa seria procurar um serviço baseado em nuvem. O Amazon SQS é o único serviço que usei e fiz o que promete. Tenho certeza de que existem alguns serviços semelhantes por aí.

Filas baseadas em memória também são algo a considerar, especialmente para filas de curta duração. O memcached é excelente como armazenamento na fila de mensagens.

Qualquer que seja o armazenamento em que você decida montar sua fila, seja inteligente e abstraia. Nem o remetente nem o destinatário devem estar vinculados a um armazenamento específico; caso contrário, mudar para um armazenamento diferente posteriormente seria uma PITA completa.

Abordagem da vida real

Criei uma fila de mensagens para e-mails muito parecidos com o que você está fazendo. Ele estava em um projeto PHP e eu o construí em torno do Zend Queue , um componente do Zend Framework que oferece vários adaptadores para diferentes armazenamentos. Meus armazéns onde:

  • Matrizes PHP para teste de unidade,
  • Amazon SQS na produção,
  • MySQL nos ambientes de desenvolvimento e teste.

Minhas mensagens eram o mais simples possível, meu aplicativo criou pequenas matrizes com as informações essenciais ( [user_id, reason]). O armazenamento de mensagens era uma versão serializada dessa matriz (primeiro, era o formato interno de serialização do PHP, depois o JSON, não me lembro por que mudei). A reasoné uma constante e é claro que eu tenho um grande em algum lugar da tabela que mapeia reasona explicações mais completas (Eu consegui enviar cerca de 500 e-mails para os clientes com a enigmática reasonem vez da mensagem mais completa uma vez).

Leitura adicional

Padrões:

Ferramentas:

Leituras interessantes:


Uau. Apenas sobre a melhor resposta que já recebi aqui! Não posso agradecer o suficiente!
21413 Gaz_Edge

Eu, e tenho certeza, que outros milhões usam este FIFO com o Gmail e o Script do Google Apps. um filtro do Gmail rotula todos os e-mails recebidos com base em um critério, e isso é tudo, enfileira-os. Um script do Google Apps executa cada duração de X, recebe y primeiras mensagens, envia-as e retira-as da fila. Enxágüe e repita.
DavChana

6

Você precisa de algum tipo de sistema de filas.

Uma maneira simples poderia ser gravar em uma tabela de banco de dados e ter outras linhas de processos de aplicativos externos nessa tabela, mas existem muitas outras tecnologias de enfileiramento que você poderia usar.

Você pode ter uma importância nos emails, para que alguns sejam acionados quase imediatamente (redefinição de senha, por exemplo), e os de menor importância podem ser agrupados em lotes para serem enviados posteriormente.


você tem um diagrama ou exemplo de arquitetura que mostra como isso funciona? Por exemplo, a fila fica em um aplicativo diferente, por exemplo, aplicativo de email, ou é processado no aplicativo da Web durante um período silencioso. Ou devo criar um tipo de serviço da web para processá-los?
21413 Gaz_Edge

1
@Gaz_Edge Seu aplicativo envia itens para a fila. Um processo em segundo plano (provavelmente um script cron) exibe x itens da fila a cada n segundos e os processa (no seu caso, envia o email). Uma única tabela de banco de dados funciona bem como armazenamento de fila para pequenas quantidades de itens, mas geralmente as operações de gravação em um banco de dados são caras e, para quantidades maiores, convém procurar serviços como o SQS da Amazon .
precisa

1
@Gaz_Edge Não tenho certeza se consigo diagrama-lo mais simples do que escrevi "... grave em uma tabela de banco de dados e tenha outras linhas de processo de aplicativo externo nesta tabela ...." e, para a tabela, leia "qualquer fila "seja qual for a tecnologia que possa ser.
ozz

1
(cont ...) Você pode criar o processo em segundo plano que limpa a fila de maneira a levar em consideração o tráfego, por exemplo, pode instruí-lo a processar menos itens (ou nenhum) às vezes em que o servidor está sob estresse . Você precisará prever esses momentos estressantes observando os dados de tráfego anteriores (mais fácil do que parece, mas com uma grande margem de erro) ou solicitando que o processo em segundo plano verifique o status do tráfego sempre que for executado (mais preciso, mas a sobrecarga adicionada raramente é necessária).
yannis

@YannisRizos quer combinar seus comentários em uma resposta? Além disso, diagramas de arquitetura e design seria útil (eu estou determinado a levá-los a partir desta questão desta vez ;-)!)
Gaz_Edge

2

Não haverá uma quantidade enorme de emails enviados (no máximo 2000 por dia).

Além da fila, a segunda coisa que você deve considerar é enviar e-mails através de serviços especializados: MailChimp, por exemplo (eu não sou afiliado a este serviço). Caso contrário, muitos serviços de email, como o Gmail, em breve enviarão suas cartas para uma pasta de spam.


2

Eu modelei o sistema da minha fila em 2 tabelas diferentes como;

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]

Há 1-1 relação entre essas tabelas.

Tabela de mensagens para armazenar o conteúdo da mensagem. O conteúdo real (Para, CC, BCC, Assunto, Corpo etc.) é serializado no campo Gráfico no formato XML. As informações Outras de, Até são usadas apenas para relatar problemas sem desserializar o gráfico. Separar esta tabela permite particionar o conteúdo da tabela em um armazenamento em disco diferente. Quando você estiver pronto para enviar uma mensagem, precisará ler todas as informações; portanto, não há nada errado em serializar todo o conteúdo em uma coluna com índice de chave primária.

Tabela MessageState para armazenar o estado do conteúdo da mensagem com informações adicionais baseadas em data. A separação dessa tabela permite um mecanismo de acesso rápido com índices adicionais no armazenamento rápido de E / S. Outras colunas já são auto-explicativas.

Você pode usar um conjunto de encadeamentos separado que varre essas tabelas. Se o aplicativo e o pool residirem na mesma máquina, você poderá usar uma classe EventWaitHandle para sinalizar o pool do aplicativo sobre algo inserido nessas tabelas; caso contrário, a varredura periódica com um tempo limite é a melhor.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.