Processamento confiável de eventos do Azure Functions
O processamento de eventos é um dos cenários mais comuns associados à arquitetura sem servidor. Este artigo descreve como criar um processador de mensagens confiável com Azure Functions para evitar a perda de mensagens.
Desafios dos fluxos de eventos em sistemas distribuídos
Considere um sistema que envia eventos a uma taxa constante de 100 eventos por segundo. A essa taxa, em minutos, várias instâncias de funções paralelas podem consumir os eventos 100 de entrada a cada segundo.
No entanto, qualquer uma das seguintes condições menos ideais é possível:
- E se o editor de eventos enviar um evento corrompido?
- E se a sua instância do Functions encontrar exceções sem tratamento?
- E se um sistema downstream ficar offline?
Como lidar com essas situações enquanto se preserva a taxa de transferência do aplicativo?
Com as filas, as mensagens confiáveis vêm naturalmente. Quando emparelhado com um gatilho do Functions, a função cria um bloqueio na mensagem da fila. Se o processamento falhar, o bloqueio será liberado para permitir que outra instância seja processada novamente. Em seguida, o processamento continua até que a mensagem seja avaliada com êxito ou adicionada a uma fila de suspeitas.
Mesmo que uma mensagem de fila possa permanecer em um ciclo de repetição, outras execuções paralelas continuam a manter a remoção da fila de mensagens restantes. O resultado é que a taxa de transferência geral permanece em grande parte não afetada por uma mensagem inadequada. No entanto, as filas de armazenamento não garantem pedidos e não são otimizadas para as demandas de alta taxa de transferência exigidas pelos Hubs de Eventos.
Por outro lado, os Hubs de Eventos do Azure não incluem um conceito de bloqueio. Para permitir recursos como alta taxa de transferência, vários grupos de consumidores e capacidade de reprodução, os eventos de Hubs de Eventos se comportam mais como um player de vídeo. Os eventos são lidos de um ponto no fluxo por partição. No ponteiro, você pode ler para frente ou para trás dessa localização, mas precisa escolher mover o ponteiro para eventos a serem processados.
Quando ocorrerem erros em um fluxo, se você decidir manter o ponteiro no mesmo ponto, o processamento de eventos será bloqueado até que o ponteiro seja avançado. Em outras palavras, se o ponteiro for interrompido para lidar com problemas de processamento de um só evento, os eventos não processados começarão a se acumular.
O Azure Functions evita deadlocks avançando o ponteiro do fluxo, independentemente de êxito ou falha. Como o ponteiro continua avançando, suas funções precisam lidar com falhas adequadamente.
Como o Azure Functions consome eventos de Hubs de Eventos
O Azure Functions consome eventos do Hub de Eventos ao percorrer as seguintes etapas:
- Um ponteiro é criado e mantido no Armazenamento do Azure para cada partição do hub de eventos.
- Quando novas mensagens são recebidas (em um lote por padrão), o host tenta disparar a função com o lote de mensagens.
- Se a função concluir a execução (com ou sem exceção), o ponteiro avançará e um ponto de verificação será salvo na conta de armazenamento.
- Se as condições impedirem a conclusão da execução da função, o host não conseguirá avançar o ponteiro. Se o ponteiro não for avançado, as verificações posteriores acabarão processando as mesmas mensagens.
- Repita as etapas 2 a 4
Esse comportamento revela alguns pontos importantes:
- Exceções sem tratamento podem causar a perda de mensagens. As execuções que resultam em uma exceção continuarão a avançar o ponteiro. A definição de uma política de repetição atrasará o andamento do ponteiro até que toda a política de repetição tenha sido avaliada.
- As funções garantem a entrega pelo menos uma vez. Seu código e sistemas dependentes podem precisar considerar o fato de que a mesma mensagem pode ser recebida duas vezes.
Tratamento de exceções
Como regra geral, cada função deve incluir um bloco try/catch no nível mais alto de código. Especificamente, todas as funções que consomem eventos de Hubs de Eventos devem ter um bloco catch
. Dessa forma, quando uma exceção é gerada, o bloco captura trata o erro antes de o ponteiro progredir.
Mecanismos e políticas de nova tentativa
Algumas exceções são transitórias por natureza e não são exibidas novamente quando uma operação é tentada outra vez mais tarde. É por isso que a primeira etapa é sempre repetir a operação. Você pode aproveitar as políticas de nova tentativa do aplicativo de funções ou criar lógica de repetição dentro da execução da função.
Apresentar comportamentos de tratamento de falhas às suas funções permite que você defina políticas básicas e avançadas de repetição. Por exemplo, você pode implementar uma política que segue um fluxo de trabalho ilustrado pelas seguintes regras:
- Tente inserir uma mensagem três vezes (potencialmente com um atraso entre repetições).
- Se o resultado eventual de todas as novas tentativas for uma falha, adicione uma mensagem a uma fila para que o processamento possa continuar no fluxo.
- As mensagens corrompidas ou não processadas são tratadas posteriormente.
Observação
Polly é um exemplo de uma biblioteca de resiliência e de tratamento de falhas transitórias para aplicativos C#.
Erros de não exceção
Alguns problemas surgem mesmo quando um erro não está presente. Por exemplo, considere uma falha que ocorra no meio de uma execução. Nesse caso, se uma função não concluir a execução, o ponteiro de deslocamento nunca avançará. Se o ponteiro não avançar, qualquer instância que for executada após uma falha de execução continuará lendo as mesmas mensagens. Essa situação fornece uma garantia "pelo menos uma vez".
A garantia de que cada mensagem seja processada pelo menos uma vez implica que algumas mensagens podem ser processadas mais de uma vez. Seus aplicativos de funções precisam estar cientes dessa possibilidade e devem ser criados com base nos princípios de idempotência.
Parar e reiniciar a execução
Embora alguns erros possam ser aceitáveis, e se seu aplicativo apresentar falhas significativas? Talvez você queira parar de disparar eventos até que o sistema alcance um estado íntegro. A oportunidade de pausar o processamento geralmente é obtida com um padrão de disjuntor. O padrão de disjuntor permite que seu aplicativo "interrompa o circuito" do processo de evento e retome em um momento posterior.
Há duas partes necessárias para implementar um disjuntor em um processo de evento:
- Estado compartilhado em todas as instâncias para acompanhar e monitorar a integridade do circuito
- Processo mestre que pode gerenciar o estado do circuito (aberto ou fechado)
Os detalhes da implementação podem variar, mas, para compartilhar o estado entre as instâncias, você precisa de um mecanismo de armazenamento. Você pode optar por armazenar o estado no Armazenamento do Azure, um Cache Redis ou qualquer outra conta que possa ser acessada por uma coleção de funções.
Os Aplicativos Lógicos do Azure ou as Durable Functions são uma opção natural para gerenciar o fluxo de trabalho e o estado do circuito. Outros serviços também podem funcionar, mas os aplicativos lógicos são usados para este exemplo. Usando aplicativos lógicos, você pode pausar e reiniciar a execução de uma função, obtendo o controle necessário para implementar o padrão de disjuntor.
Definir um limite de falha entre instâncias
Para considerar várias instâncias processando eventos simultaneamente, é necessário persistir o estado externo compartilhado para monitorar a integridade do circuito.
Uma regra que você pode optar por implementar pode impor isso:
- Se houver mais de 100 falhas eventuais dentro de 30 segundos em todas as instâncias, interrompa o circuito e pare de disparar em novas mensagens.
Os detalhes da implementação variam de acordo com suas necessidades, mas, em geral, você pode criar um sistema que:
- Registre falhas em uma conta de armazenamento (Armazenamento do Azure, Redis etc.)
- Quando uma nova falha for registrada, inspecione a contagem de rolagem para ver se o limite foi atingido (por exemplo, mais de 100 nos últimos 30 segundos).
- Se o limite for atingido, emita um evento para a Grade de Eventos do Azure informando ao sistema para interromper o circuito.
Como gerenciar o estado do circuito com os Aplicativos Lógicos do Azure
A descrição a seguir destaca um modo de criar um Aplicativo Lógico do Azure para interromper o processamento de um aplicativo Functions.
Os Aplicativos Lógicos do Azure vêm com conectores internos para diferentes serviços, recursos de orquestrações com estado e é uma opção natural para gerenciar o estado do circuito. Depois de detectar que o circuito precisa ser interrompido, você pode criar um aplicativo lógico para implementar o seguinte fluxo de trabalho:
- Disparar um fluxo de trabalho de Grade de Eventos e parar a função do Azure (com o conector de recursos do Azure)
- Enviar um email de notificação que inclui uma opção para reiniciar o fluxo de trabalho
O destinatário do email pode investigar a integridade do circuito e, quando apropriado, reiniciar o circuito por meio de um link no email de notificação. À medida que o fluxo de trabalho reinicia a função, as mensagens são processadas do último ponto de verificação do Hub de Eventos.
Usando essa abordagem, nenhuma mensagem é perdida, todas as mensagens são processadas na ordem e você pode interromper o circuito, se necessário.
Recursos
Próximas etapas
Para saber mais, consulte os recursos a seguir: