Batch

Visão Geral

O que é?

Esse componente provê maneiras de estruturar o processamento de forma paralela e distribuída, para aproveitar o máximo poder computacional. Define um mecanismo de particionamento de execução de processos, resolvendo problemas de concorrência, numa maneira transacional e tolerante a falhas. Os processos podem ser executados localmente numa configuração multi-thread ou distribuidamente pela rede através de JMS e outros meios.

Como funciona?

Paralelizar e distribuir para resolver processamentos longos e complexos, separando o processo em diversas etapas, sendo que uma etapa pode depender da outra. Desafios:

  • controle de concorrência
  • rastreabilidade: caso ocorra um problema, como vou saber aonde e por que ocorreu? O processo deu certo ou deu errado? Por que deu errado?
  • balanceamento de carga: aproveitar o máximo de poder de processamento.

Dividir para Conquistar

Apenas paralelizar não é suficiente. O processamento pode estar sendo utilizado de forma errada. Podemos acabar utilizando muito mais processamento do que precisamos. Um exemplo disso é o persistence context do Hibernate. Se a tarefa é muito longa, a sessão fica viva por muito tempo e a quantidade de objetos do persistence context começa a aumentar e o processamento para gerenciá-lo também. Se a sessão é longa, o persistence context vira um inimigo. Se o escopo é pequeno ele vira um amigo. A solução adotada é dividir em Fases (dividir para conquistar), ou seja, dividir o Processo em Fases menores até um tamanho que faça sentido. Cada Fase deve atuar em um conjunto de dados em comum, para manter o persistence context num tamanho coerente, “cacheando” apenas os dados que são utilizados com frequência.

../../_images/Jmine-tec-batch-dividir-para-conquistar.png

A entidade que representa um processo é MProcess e representa qualquer coisa que possa ser disparado em paralelo e distribuído e pode ter várias fases. A entidade que representa uma fase pe MPhase e define como deve ser uma fase e pertence a um MProcess. Cada fase pode ter dependências entre outras fases. Ex.: Calcular o quanto vale estoque de rf precisa primeiro precificar.

Depêndencia entre fases

Como uma fase pode depender da execução de outras fases, é preciso saber a ordem que tudo será executado. Para isso, a dependência entre as fases é representada num grafo orientado e as fases são executadas de acordo com a ordem topológica. A imagem abaixo mostra a ordem topológica do grafo de fases, onde cada vértice é uma fase, e cada aresta é uma dependência.

../../_images/Jmine-tec-batch-grafo.jpg

Tamanho do Grão

Mas apenas dividir as fases não é suficiente. O banco de dados pode ficar sobrecarregado. Isso acontece quando fazemos muitas perguntas pro banco e a aplicação fica ociosa, esperando respostas do banco. A solução adotada é o conceito de Unidade de Processamento, que consiste em agrupar os elementos em um tamanho razoável para diminuir o overhead de comunicação, ou seja, encontrar o “tamanho certo do grão” ou o tamanho certo da tarefa. Se o grão é pequeno ou seja, a tarefa é curta, acontece overhead de comunicação, pois é preciso controlar lock de sincronização. Se o grão é grande, ou seja, a tarefa é longa, o custo da comunicação diminui mas o paralelismo também diminui e os processadores ficam ociosos. É preciso ajustar o tamanho do grão para aproveitar os recursos ao máximo.

../../_images/Jmine-tec-batch-grao-overhead.jpeg

Fila de mensagens

Além de ajustar o tamanho do grão, outra solução adotada é jogar cada Unidade de Processamento em uma fila de mensagens (Message Queue).

../../_images/Jmine-tec-batch-mq-mdb.png

MDBs (Message Driven Beans) consomem as mensagens, ou seja, as unidade de processamento, garantindo balanceamento de carga.

../../_images/Jmine-tec-batch-mq-mdbs.jpg

Além disso, temos a vantagem de podermos adicionar mais máquinas em tempo real, ou seja, mais poder de processamento.

../../_images/Jmine-tec-batch-processamento-mdbs.jpeg

Rastreabilidade e Tolerância a falhas

A leitura da mensagem ocorre numa transação, ou seja, se a máquina morre, a mensagem volta pra fila e outro MDB vai processar. Isso é o que chamamos de Transação XA, que nos garante um ambiente tolerante à falhas. Se ocorreu um erro a nível de fase, ela não é comitada, para não gerar inconsistências. O processamento pode ocorrer mesmo que alguma fase falhe. Se uma fase falha, ela é marcada com status de falha. O usuário pode então verificar qual foi o problema, resolvê-lo e continuar o processamento. Como sabemos quais fases rodaram com sucesso e quais não, podemos continuar da fase que falhou, não sendo necessário refazer todo o processo. Para saber quais fases não rodaram ou falharam ele vai ver o status das fases. O que for marcado com sucesso não será feito novamente. Os status das PhaseInstance são:

  • OPENED
  • STARTED
  • ERROR
  • FINISHED

Os processos e fases são salvos no banco de dados para manter a rastreabilidade. Podemos descobrir diversas informações sobre o processo. Por exemplo, se queremos processar as carteiras A, B, e C:

  • O que aconteceu com cada processo e com cada fase?
  • Essa instância do processo processou o que? As carteiras A, B, e C.
  • Processou aonde? Na máquina “x”.
  • O que houve? Das 120 fases, nas primeiras 80 ocorreu tudo bem, na 81ª ocorreu uma exceção na linha “y” e nas outras 15 fases ocorreram sem problemam, sendo que as últimas 4 não puderam rodar pois dependiam da fase 81.
../../_images/Jmine-tec-batch-grafo-de-fases-sucessos-e-falhas.jpg

Estrutura

Esse componente provê uma abstração de um Processo e suas Fases. Fases podem declarar dependência para outras fases, consequentemente formando um grafo orientado. Uma fase é essencialmente um bean que foi definido na configuração de Spring e é esperado que seja executado no contexto de um elemento. Esse elemento pode ser de qualquer tipo, desde que implemente a interface PhaseableElement. Para executar um processo e seus elementos, é preciso construir um ProcessingUnit. Um ProcessingUnit representa um grupo de elementos que serão processados numa thread ou nó. Para rastrear quais fases foram executadas dado um elemento, uma PhaseInstance é salva no banco de dados.

Qual é o ganho?

Com o Processamento batch, ganhamos processamento distribuído, paralelo, balanceamento de carga e tolerância a falhas. Conseguimos aproveitar o máximo poder computacional, seja das máquinas que vão realizar o processamento quanto das máquinas de banco de dados.

  • processamento distribuído: podemos quebrar um processo longo e pesado em partes menores
  • paralelismo: resolve os problemas de concorrência
  • balanceamento de carga: os mdbs consomem as unidades de processamento
  • tolerância a falha: se um nó “morrer” a mensagem volta para a fila
  • elasticidade: é possível adicionar capacidade de processamento enquanto a aplicação está “de pé”
  • rastreabilidade: podemos verificar se os processos ocorreram com sucesso ou falharam e o motivo da falha
  • monitoramento: É possível monitorar a fila

Lembrando que o componente só define conceitos e estruturas que possibilitam o processamento paralelo e distribuído mas é o usuário que precisa criar as fases e estruturar os processos, cuidando para não estressar banco de dados e para que o código da fase seja “performático”. Além disso, é preciso sempre monitorar a performance, fazendo o “ajuste fino” para cada máquina, cliente, hardware e produto.

Conceitos

Processamento Batch

É um processamento longo e pesado que envolve muitas unidades e deve ser executado em background. É separado em diversas etapas, que são executadas em paralelo, sendo que uma etapa depende da outra. Utiliza várias máquinas com alto poder de processamento.

Thread Pool

Thread Pool é um padrão onde um número de threads são criadas para cumprir um número de tarefas, as quais são geralmente organizadas numa fila. Tipicamente, existem muito mais tarefas do que threads. Assim que uma thread completa sua tarefa, ela requisita a próxima tarefa da fila até que todas as tarefas estejam completas. A thread pode então terminar ou dormir até que hajam novas tarefas disponíveis.

MQ - Message Queue

MQs provêem um protocolo de comunicação assíncrona, em que o sender e o receiver da mensagem não precisam interagir com a fila de mensagens ao mesmo tempo. Mensagens colocadas na fila são armazenadas até que o destinatário recupere-as. No contexto do batch, garante o balanceamento de carga.

MDB - Message Driven Bean

Message Driven Beans são objetos de negócio nos quais a execução é acionada por mensagens ao invés de chamadas de métodos. Eles podem se inscrever em filas ou tópicos de mensagens. Diferente de session beans, um MDB não possui uma interface direta com seu cliente, ele apenas ouve mensagens que chegam e processa elas automaticamente. Podem estar em varias máquinas diferentes e podem existir mais de um MDB por servidor de aplicação.

Transação XA

Transação XA envolve um gerenciador de transação, com um ou mais bancos de dados (ou outros recursos, como JMS) todos envoltos numa única transação global. Transações não XA não possuem um gerenciador de transações e um único recurso está fazendo todo o trabalho de transação (transações locais). Durante o processamento, a mensagem é consumida, se algo falhar, garante que as transações são revertidas e a mensagem volta para a fila. Mesmo terminando de processar e não conseguindo acessar a fila, será feito o rollback.

MetaProcess

MetaProcess é a meta informação de um processo. Representada pela entidade MProcess, seu método principal é createProcess, uma factory responsável por instanciar um Process e as ProcessingUnit e retornar a lista de unidades de processamento criadas.

MetaPhase

MetaPhase é a meta informação de uma fase. Representada pela entidade MPhase, define-se as fases que essa fase depende para ser executada e quais as PhaseInstances dessa fase. O atributo beanName é importantíssimo, pois é o nome do bean no spring que representa a execução da MPhase.

ProcessingUnit

Uma unidade de processamento é um conjunto de elementos a serem processados pelo controle de fases. É necessário que os elementos sejam agrupados em conjuntos de tamanho razoável para diminuir o overhead de comunicação. Representada pela entidade ProcessingUnit, deve ser estendida e os métodos addChildren e getChildren devem ser implementados para tornar possível a adição e a obtenção dos elementos que devem ser processados.

PhaseInstance

PhaseInstance é um bean que armazena o estado da execução de uma fase para um elemento. Ela deve ser persistível para garantir a rastreabilidade. Representada pela entidade PhaseInstance, mantém dados como data da execução do processo, horários inicial e final de execucação de uma fase, além do status da execução do processo.

O status da execução do processo pode ser:

  • Não Iniciado (0)
  • Iniciado (1)
  • Finalizado com erros (2)
  • Finalizado sem erros (3)

ExecPhase

ExecPhase representa a execução de uma fase. Sua definição básica é dada pela interface ExecPhase. Seus métodos são:

  • shouldExecute: determina se uma fase deve ou não ser executada;
  • execute: executa a fase.

As ExecPhases são gerenciadas por uma Phase Manager.

PhaseManager

Phase Manager é responsável por gerenciar a execução das ExecPhases. Dada uma definição de processo (MProcess), ela determina qual ExecPhase deve ser executada e qual a ordem de execução das suas respectivas dependências, além de outras funções como atualizar o status de PhaseInstance e ProcessingUnit. Representada pela classe abstrata AbstractPhaseManager, recebe MProcess como um dos parâmetros. Com a ajuda do grafo de fases, representado pela classe PhaseGraph, ela obtém a lista de MPhases ordenada, respeitando as dependências. A classe deve ser estendida e os métodos createEmptyEvent, getAllPhaseInstances e doCreatePhaseInstance devem ser implementados. Opcionalmente, pode-se implementar hooks para adicionar comportamento antes ou depois da execução de uma ExecPhase, ProcessingUnit ou um Element.

Exemplos

Em primeiro lugar, é necessário criar um elemento implementando PhaseableElement.

Criando Phases

Fases devem ser definidas implementado ExecPhase e devem ser registradas no Spring.

Cadastrando MProcess e MPhases

Com as fases registradas no Spring, podemos cadastrar as definições das fases e dos processos no banco de dados. Isso pode ser feito utilizando os serviços disponíveis do modulo.

Criando o PhaseManager

Para que possamos executar o processo, é preciso criar um PhaseManager, herdando de AbstractPhaseManager e implementando os métodos abstratos.

Executando um processo

Para executar um Processo, basta instanciar o PhaseManager e chamar o método execute passando a Unidade de Processamento.