Escalabilidade e Performance – Parelelismo – Parte 01

Introdução

Em posts anteriores sobre escalabilidade e desempenho, foram frisados vários pontos e técnicas que devem ser levadas em conta no momento de desenvolver uma aplicação partindo das premissas de escalabilidade horizontal e desempenho. Agora vamos trazer isso para um contexto real dentro do AdvPL, explorando uma análise de um cenário muito comum: Processamento de grandes quantidades de dados.

Cenário proposto

Imagine um cenário onde você precisa realizar um processamento de um arquivo TXT (ou CSV), que pode ter muitos MB … O ponto importante é que você pode receber vários arquivos em um curto espaço de tempo, e estes arquivos precisam ser processados e alimentar tabelas do sistema da forma mais rápida possível. Nesta abordagem, vamos abordar a análise de caso e os pontos importantes a considerar.

Fase 01 – Determinar origem dos arquivos

A primeira coisa a pensar é onde estarão estes arquivos. Para eles serem processados mais rapidamente, eles precisam estar pelo menos em uma pasta no RootPath do ambiente. Se estes arquivos vêm de fora do sistema, e você tem que pegá-los em algum lugar fora da sua rede, a melhor alternativa de tráfego é um FTP. O FTP é um protocolo criado especificamente para transferência de arquivos. Nada impede de você usar um webservice para transferir o arquivo em pedaços, mas isto vai trazer um custo (overhead) do HTTP e do XML-SOAP. Se os arquivos estão na própria rede interna, eles podem ser copiados via sistema de arquivos do sistema operacional mesmo, com compartilhamento de rede, numa pasta dentro do RootPath, mas isto vai ser tão rápido quanto FTP, a menos que você utilize um comando mais robusto de cópia, como o ROBOCOPY.

Fase 02 – Determinar pontos de paralelismo

Os arquivos já estão na pasta. Agora, vamos verificar se é possível aplicar algum tipo de paralelismo neste processo. Se os arquivos precisam impreterivelmente obedecer a ordem de chegada, onde cada arquivo precisa ser processado por inteiro antes do próximo arquivo iniciar o processo, uma parte boa do paralelismo já foi perdida … Se os arquivos não tem ordem de processamento, isto é, cada um pode ser processado aleatoriamente, você poderia usar múltiplos JOBS , onde cada um processaria um arquivo inteiro por vez.

Mas nem tudo está perdido … Se dentro de um grande arquivo, as linhas não tiverem ordem de processamento, isto é, cada linha é uma unidade independente de informação, e você não precisa necessariamente ter terminado de processar a linha 999 para processar a linha 1000, … ainda temos uma ótima possibilidade de paralelismo. Vamos assumir que este cenário é possível no nosso exemplo.

Certo, então para aproveitar o máximo possível de paralelismo, eu posso quebrar o arquivo em vários pedaços, e colocar um JOB para processar cada pedaço do arquivo. A pergunta a ser feita agora é: O processamento de uma determinada linha vai exigir algum recurso exclusivo durante o processamento, que também pode ser ou será efetivamente necessário para processar outra linha?

Por exemplo, de cada linha a ser processada do arquivo precisar atualizar um mesmo registro da base de dados, caso duas linhas distintas, sendo processadas pelos jobs precisem fazer um lock de alteração de um mesmo registro de uma tabela no SGBD, fatalmente um dos processos ficará esperando o outro terminar a transação e soltar o lock para conseguir fazer o seu trabalho. Isto implica em uma serialização do processamento neste ponto.

Se as chances disso ocorrer são pequenas durante o processamento, a maioria dos processos somente vai sofrer uma contenção ou serialização em pontos isolados, ainda temos muito a ganhar com processos em paralelo. Agora, se grandes blocos de registros correm o risco de colidir, mesmo assim nem tudo está perdido. No momento de quebrar o arquivo em pedaços, cada pedaço deve agrupar o máximo de linhas que utilizem o mesmo registro, para que um pedaço do arquivo evite concorrer com os demais.

Fase 03 – Quebrando o arquivo

Agora, vamos “quebrar o arquivo”. Vamos pensar bem nesta etapa, afinal existem muitas formas de fazer isso. Precisamos encontrar aquela que melhor se adequa a nossa necessidade, consumindo um mínimo de recursos possível. Se o consumo de um recurso for inevitável, precisamos encontrar a forma mais eficiente de fazê-lo.

Normalmente subimos Jobs ou Threads de processamento na mesma instância do Application Server onde estamos rodando o nosso programa. Porém, subir 2, 3 ou mais threads no mesmo Application Server, quando temos uma máquina com 8 cores HT — como um servidor Intel com processador DUAL-Quadricore HT, é mais interessante subir alguns jobs de processamento em mais de um Application Server. Isto pode exigir alguns controles adicionais, mas os resultados costumam ser muito interessantes.

O lugar onde subir estes Jobs também é importante. Na máquina onde está a unidade de disco com o ambiente do ERP, a leitura de arquivos é feita direto pelo FileSystem do Sistema Operacional, é muito mais rápido do que ler pela rede. Porém, normalmente nesta máquina utilizamos um c-Tree Server, para compartilhar os meta-dados (Dicionários, SXS) do ERP com todos os demais serviços. Colocar muito processamento nesta máquina pode usar muitos recursos dela, e prejudicar os demais processos.

A alternativa mais flexível que evita este risco, é poder subir os Jobs em qualquer um dos serviços de qualquer uma das máquinas disponíveis no parque de máquinas que rodam os Application Server’s. Para isso, fatalmente teremos que usar a rede. Porém, se usarmos ela com sabedoria, podemos minimizar impactos. Vamos partir para esta alternativa.

Fase 04 – Multiplos processos

Existem várias formas de colocarmos JOBS dedicados para realizar um trabalho em vários serviços. Podemos fazer a nossa aplicação iniciar os Jobs de processamento apenas quando necessário, usando por exemplo o RPC do AdvPL, onde no programa principal enumeramos os IPs e Portas dos Application Server’s que serão endereçados para esta atividade, e através de RPC subimos um ou mais Jobs em cada um, usando por exemplo a função StartJob(), ou podemos deixar em cada um dos serviços de processamento um ou mais jobs no ar, inicializados na subida de cada Application Server usando a seção [ONSTART] do appserver.ini, e depois fazendo RPC para cada serviço e distribuir os trabalhos usando IPC (Internal Procedure Call) do AdvPL.

Subir um pedaço grande inteiro do arquivo na memória inicialmente pode parecer uma boa alternativa, porém lembre-se que em se tratando de processos paralelos, várias juntas podem consumir muita memória, E , se muita memória é ocupada e endereçada, os processos de lidar com esta memória podem ficar mais lentos do que lidar com pedaços menores. Nas minhas experiências, buffers de 8 KB ou 16K dão conta do recado, fazendo um acesso eficiente em disco, e gerando um pedaço de dados que não vai “doer” na memória dos processos.

A parte importante aqui é: Como quebrar um arquivo grande de forma eficiente, e distribuir seus pedaços para processamento? Uma primeira idéia que me veio na cabeça foi fazer com que o programa principal de processamento abrisse a tabela e fizesse a leitura de várias linhas. Após acumular na memória um bloco de linhas, com o mínimo de processamento possível (por exemplo colocando as linhas dentro de um array), este bloco de linhas pode ser enviado por RPC para um dos slaves, e disparar um IpcGo() para uma das threads disponíveis no slave onde o RPC está conectado.

Desta forma, ao invés de mandar linha por linha em cada requisição, onde em cada uma você vai ter um pacote de rede muito pequeno, ao agrupar um número de linhas que chegasse perto de 64 KB de dados, enviar este bloco de uma vez, em um único evento de envio de rede por RPC, para o Slave “da vez”, ele receberia este pacote de uma vez só, isso aproveitaria bem melhor a banda de rede, e o processo alocado naquele Application Server que recebeu este pacote estaria dedicado a processar todas as linhas recebidas naquela solicitação.

Fase 05 – Sincronização de Processos

Parece lindo, mas ainda têm 2 problemas: Partindo de um ambiente com 4 serviços de Protheus, com 2 threads dedicadas em cada um, você teria um total de 8 threads espalhadas. Fazendo um Round-robin entre os serviços, a primeira requisição iria para a T1S1 (primeira thread do serviço 1), a próxima para T1S2, depois T1S3, T1S4, T2S1, T2S2, T2S3, T2S4, e agora as 8 threads estão ocupadas processando cada uma 64 KB de linhas.

Quando você for enviar a requisição 09, ela vai novamente para o serviço 1 … se nenhuma das threads do serviço 1 terminou de processar o pacote anterior, você gastou 64 KB da banda de rede trafegando um pacotão de coisas, para um servidor ocupado … Então você tenta enviar para o servidor 2, gasta mais banda de rede, e… nenhuma thread livre …. Mesmo que você crie uma mensagem de “consulta”, onde você não passasse os dados, apenas perguntasse se tem threads livres naquele servidor, você ficaria metralhando a rede com pacotes pequenos em todos os serviços mapeados para o processo, até que um deles iria responder “estou livre”, para você então mandar para ele mais um pacote de linhas.

Este pequeno percalço pode ser resolvido com um mecanismo chamado de “CallBack”. Nada impede que o seu Job possa fazer um RPC de volta para você, para avisar que um determinado pedaço foi processado. Com isso você pode usar o RPC com IPC em “mão dupla”, com dois pares de conexões. Quando o seu programa principal enviar as 8 requisições, ele entra em um loop de espera por uma mensagem de IPC. Cada JOB alocado precisa receber no momento da alocação um IP e porta e ambiente para conectar-se de volta, para notificar os estados de processamento. Quando um JOB terminar de processar um pacote, ele manda via RPC um sinal de IPC para avisar que ele terminou, informando “quem ele é” na mensagem. Como o seu processo principal vai estar esperando receber uma notificação via IPC, assim que ela chegar, o seu programa principal pega o próximo pacote de linhas e manda para aquela thread via RPC+IPC, pois assim que esta thread enviar a notificação de retorno, ela vai entrar em modo de espera de IPC para uma nova solicitação de processamento.

Este sincronismo também pode ser de mão única … ao invés do seu Job principal fazer um “push” das requisições de processamento, ele pode acionar uma vez os jobs dedicados para serem alocados para este processo, onde o job dedicado imediatamente faz o callback, e passa a “pedir” requisições de processamento, e notificar o programa principal pelo mesmo canal a cada pacote processado.

Existem outras formas de sincronismo, como por exemplo usar o banco de dados. O programa principal cria uma tabela para fins temporários no SGDB, e alimenta esta tabela com os pacotes a serem processados. Porém, se este mecanismo for utilizado, você acaba consumindo tempo e recursos para popular uma tabela grande no SGDB, e cada processo faz uma busca indexada na tabela por um pacote que ainda não foi processado. Ao conseguir locar o registro, o programa de processamento deve mudar o status para “em processamento” e manter o lock até o final do processo. No caso, o programa principal vai alimentando a tabela, enquanto os jobs dedicados vão pegando os registros não processados e fazendo suas tarefas. Quando o programa principal terminar de popular a tabela, ele passa a verificar quais registros ainda estão em processamento, aguardando todos ficarem prontos, e verificando se os jobs de processamento que pegaram os registros ainda estão no ar. NO final das contas, acaba sendo um mecanismo tão complexo quanto o sincronismo “online”, ainda com a desvantagem de colocar o SGDB no meio do caminho.

Fase 06 – Tratamentos de erro

Agora o bicho pega … Imaginar que tudo vai funcionar como um relógio é o mundo lindo … mas e se um job cair, e se a resposta não vir, e se todos os envios falharem, como é que o programa principal fica sabendo se aconteceu algo horrível ? Essa parte do controle é realmente um “parto” de ser feita … existem vários caminhos, mas normalmente os mais fáceis são os mais sujeitos a erros do tipo “falso-positivo” ou “falso-negativo”. Uma das formas mais interessantes de controle é ter um mecanismo confiável para saber se um determinado JOB que recebeu uma requisição ainda está no ar, e criar no programa principal um mecanismo de registro de envios, para dar baixa de cada pacote enviado conforme os eventos de retorno são recebidos, onde deve constar neste mecanismo o momento de envio, para que se um pacote começar a demorar muito, você possa consultar se o job que recebeu aquele pacote ainda está no ar — apenas está demorando um pouco mais — ou se ele saiu do ar — normalmente por erro.

Com estes processos sincronizados e as métricas definidas, o programa principal que inicia o processamento pode, após garantir que um pacote de linhas foi enviado e o job de destino “caiu” sem terminar o processo, você pode direcionar este pacote para outra thread em outro serviço, evitando assim ter que reiniciar o trabalho.

Vale lembrar que cada processo deve ter um transacionamento, que deve ser mantido aberto pelo menor tempo possível, e que os programas de processamento não vão ter “interface”, não será possível (não de maneira simples) perguntar pro operador do sistema o que fazer ou dar uma opção de múltipla escolha para um determinado tratamento dentro de uma linha em um job. Normalmente você trata no programa os cenários possíveis, e qualquer coisa que não estiver de acordo, você pode rejeitar o processamento e gerar um log do que foi recusado, para que sejam tomadas as providências quanto aquelas informações quando o processo terminar, e você puder rodar um reprocessamento apenas do que faltou, após arrumar os dados ou a condição não tratada.

Outras alternativas

Se, ao invés de usar Jobs dedicados, você subir vários jobs de acordo com a quantidade de pedaços da sua tabela, você pode subir um número de Jobs que pode gerar um colapso nos recursos do sistema, como CPU, rede, memória, disco, SGDB …. É mais saudável trabalhar com um número limitado de processos, e medir com testes se a quantidade pode ser aumentada sem comprometer a disponibilidade do resto do ambiente. Este processo é empírico, começa com um job em cada serviço, roda um processamento inteiro, verifica se algum recurso está sendo consumido excessivamente, aumenta alguns jobs, roda de novo …

Cuidados Especiais

Se você resolver subir os jobs sob demanda, usando um fator limitante, de qualquer modo você precisa de um mecanismo para saber se os jobs terminaram o que tinham que fazer, para você poder subir novos Jobs ou enviar requisições de processamento para um Job que já está livre.

É importante lembrar que, por mais abundantes que sejam os recursos, uma carga de processamento mal dimensionada pode derrubar o ambiente, gerando uma indisponibilidade geral de outros serviços. Um programa errado que começa a comer memória com farinha pode esgotar a memoria física de um equipamento, principalmente usando uma Build 64 Bits do Protheus, fazendo a máquina inteira entrar em “Swap” e paginação de memória em disco … eu já vi equipamentos literalmente entrarem em Negação de Serviço (DoS), onde não era possível sequer abrir um Terminal Services pra tentar parar os serviços.

Conclusão

Eu sei, neste post eu só “abrir o apetite”, e abri caminho para muitas reflexões importantes. No próximo post, vamos abordar cada uma destas etapas acompanhada de um exemplo prático 😉

Até o próximo post, pessoal ! Desejo a todos TERABYTES de sucesso 😀

Referências

Grid Computing – Parte 02

Neste tópico, vamos aprofundar um pouco na questão do paralelismo, nas formas de consegui-lo e os requisitos necessários para uma rotina ser quebrada em tarefas para aderir ao processamento em paralelo.

O paradigma do paralelismo

Como já vimos em posts anteriores sobre escalabilidade e performance, podemos ganhar ambos quebrando um grande processo ou tarefa em partes menores, e verificando se algumas destas partes podem ser executadas em paralelo. O interesse nessa área cresceu devido às limitações físicas que impedem o aumento da frequência (Clock) dos processadores. Porém, existe um grau de complexidade intrínseco no desenvolvimento de aplicações paralelas, em comparação com a codificação de um programa de processamento sequencial, pois sua implementação precisa prever e lidar com situações como a condição de corrida (race condition), onde a comunicação e a sincronização de diferentes sub-tarefas é uma das maiores barreiras para atingir desempenho em programas paralelos.

A abordagem científica e acadêmica destas dependências costuma utilizar um vocabulário complexo, vamos abordar de forma mais simples e direta. Uma determinada tarefa computacional complexa realizada por um algoritmo possui uma sequência chamada de ‘caminho crítico’. Este termo também é usado em Gerência de Projetos e Análise e Desenvolvimento de Sistemas. Na prática, nenhum pograma poderá rodar mais rápido do que a maior cadeia ou sequência de processos depententes. Entretanto, a maioria dos algoritmos não consiste de somente uma cadeia de processos dependentes, geralmente há oportunidades de executar cálculos independentes em paralelo.

Determinando tarefas paralelizáveis

As premissas gerais de etapas ou tarefas menores de processos que são paralelizáveis estão enumeradas abaixo, vamos abordar cada uma em mais detalhes ao decorrer do artigo. Vamos chamar de tarefa uma etapa do processo que desejamos avaliar sua aderência ao processamento paralelo.

1) Não pode haver dependências de valores

Os valores de entrada das tarefas não podem depender dos valores de entrada ou de saída de nenhuma outra tarefa. Por exemplo, um laço de execução que calcula um rendimento de um investimento em uma poupança não é paralelizável, pois o cálculo do rendimento de cada mês será executado com o saldo final do mês anterior.

2) Não pode haver condição de corrida

Nenhuma das tarefas deve alocar um recurso comum às demais tarefas em modo exclusivo. Isto caracteriza uma condição de corrida (race condition), e o tratamento disso implica na serialização da tarefa. Por exemplo, várias tarefas de processamento de notas fiscais de saída concorrentes, onde uma etapa de processamento envolve bloquear em modo exclusivo um registro do cadastro do produto para uma atualização de saldo ou um acumulador estatístico daquele produto vai eliminar o paralelismo de duas tarefas ou mais tarefas que forem ao mesmo tempo atualizar este produto, pois a primeira que conseguir o bloqueio exclusivo fará a atualização, enquanto as outras duas ficam esperando a liberação deste registro, jogando uma parte do ganho obtido com o paralelismo no lixo. Caso o acumulador em questão tiver que ser acessado por todas as tarefas, a coisa fica pior

Para este tipo de caso, deve ser verificado se compensa criar uma fila e/ou lista re requisições, e um processo único dedicado para estas requisições. Com apenas uma fila de requisições, esta pode tornar-se o caminho crítico deste processo. Tudo depende da velocidade que entram novos elementos nesta lista, e da velocidade que são realizados os processos dela, em relação às tarefas feitas em paralelo.

Por exemplo, se você têm 200 tarefas a realizar, onde cada uma demora em média 10 segundos, um processamento sequencial destas tarefas demoraria 2000 segundos ( 33 minutos aproximadamente). Porém, utilizando 10 processos simultâneos, cada processo executaria umas 20 requisições, onde o tempo e custo da infraestrutura de distribuição não será considerado, esse bloco de 20 requisições de 10 segundos cada demoraria 200 segundos, apresentando um ganho de 10 vezes, afinal cada um dos dez processos dedicados terminaria as suas atividades em 200 segundos, isso reduziria o tempo para 3 minutos e meio.

Porém, se destes 10 segundos, 1 segundo precisa ser dedicado ao acesso exclusivo de um recurso comum a todas as tarefas, cada acesso de uma tarefa colocaria as outras 9 em espera, a primeira tarefa em espera perderia 1 segundo, a segunda perderia 2, e assim sucessivamente, prejudicando bastante o tempo total para a finalização de todas as tarefas.

Para contornar isso, criamos uma fila e um processo dedicado a processar estas requisições, então cada tarefa demoraria 9 segundos, pois no momento que ela precisasse fazer o acesso exclusivo ao recurso, ela acrescentaria esta requisição na fila de um processo dedicado. Logo, cada um dos 10 processos dedicados ao processamento geral de tarefas executaria 20 requisições de 9 segundos, somando 180 segundos. No total de 200 requisições, cada uma colocaria em uma fila uma requisição de 1 segundo, logo o processo dedicado para processar a fila demoraria 200 segundos. Neste ponto, o processo da fila passou a ser o caminho critico, pois as tarefas distribuídas já terminaram, mas a fila ainda não está vazia. Mesmo assim, houve um ganho considerável em adotar esta medida.

Sabe-se neste ponto que, quanto mais processos em paralelo forem colocados para as tarefas de 9 segundos, , mais rápido estas tarefas serão executadas, mas a fila de tarefas sequenciais terá um tempo fixo de processamento, logo não será eficiente colocar mais de 10 processos dedicados, pois isto não vai diminuir o tempo da fila.

3) Considere o custo implícito na distribuição das tarefas

Quando o paralelismo das tarefas será executada em computadores distribuídos, deve-se levar em conta a latência na transferência dos parâmetros e do retorno das tarefas entre a aplicação que solicitou o processamento e o agente alocado para o processo. No exemplo anterior, desconsideramos o tempo de latência, mas sabe-se que o ganho escalar com o aumento de paralelismo não segue uma progressão geométrica.

Ao usar um mecanismo de distribuição de tarefas para múltiplos computadores, devemos levar em conta a latência de rede para trafegar os parâmetros de cada tarefa, isto dente a ser proporcional à quantidade de dados necessários, e pode acabar sendo um fator limitante do número de processos paralelos que você poderá usar simultaneamente, afinal não adianta deixar 50 processos dedicados se a banda de rede bater 100 % de uso com 35 processos, você começa a perder tempo para passar para cada processo o que ele tem que fazer.

Se o seu processo for muito curto, e a quantidade de dados trafegados for grande, o tempo gasto com a distribuição e controle destes processos pode consumir com a eficiência do paralelismo… neste caso estas tarefas poderiam ser executadas mais rápidas sequencialmente por um processo único, ou ainda poderiam ser distribuídas entre threads ou serviços na mesma máquina, usando recursos mais leves de comunicação entre processos na mesma máquina (como memória compartilhada por exemplo). Estas variantes serão abordadas em um próximo post.

Conclusão

No princípio isso pode parecer um monstro de duas cabeças, e quando você chega mais perto, parecem sete … Mas não entre em pânico, vamos nos aprofundar neste tema em doses homeopáticas, e depois de alguns exemplos de processos didáticos devidamente explicados, eu garanto que vai ficar mais fácil.

Até o próximo post, pessoal 😉

Referências

COMPUTAÇÃO PARALELA. In: WIKIPÉDIA, a enciclopédia livre. Flórida: Wikimedia Foundation, 2014. Disponível em: <http://pt.wikipedia.org/w/index.php?title=Computa%C3%A7%C3%A3o_paralela&oldid=38961464>. Acesso em: 30 nov. 2014.

CONDIÇÃO DE CORRIDA. In: WIKIPÉDIA, a enciclopédia livre. Flórida: Wikimedia Foundation, 2014. Disponível em: <http://pt.wikipedia.org/w/index.php?title=Condi%C3%A7%C3%A3o_de_corrida&oldid=40376481>. Acesso em: 30 nov. 2014.

CHEDE, CESAR TAURION. Grid Computing, um novo paradigma computacional. Rio de Janeiro: Brasport, 2004.

Grid Computing – Parte 01

Introdução

Hoje vou entrar em um assunto que não é novo, mas que é pouco explorado devido a complexidade intrínseca de implementação: Grid Computing. Lembrando dos princípios e técnicas de performance e escalabilidade, onde uma delas é procurar obter benefícios de escalabilidade com execução de operações simultaneamente em vários equipamentos (paralelismo).

A computação em grid ou distribuída pode ser vista como um tipo particular de computação distribuída, onde vários equipamentos completos e independentes (não apenas CPUs) são interconectados por uma rede (Ethernet), atuando juntos para processar grandes tarefas.

A definição de computação paralela e distribuída são muito próximas, mas é possível classificá-las independentemente por um critério básico: Em computação paralela, todos os processos têm acesso a uma memória compartilhada para trocar informações entre processos, enquanto na computação distribuída, os processos trocam mensagens entre si, normalmente por uma conexão de rede.

Tamanhos de Grid

Um Grid pode englobar computadores geograficamente próximos ou distantes, e consequentemente ter uma pequena, média ou grande escala. Um dos maiores projetos de Grid colaborativo em larga escala (mundial) foi o projeto SETI@home, da NASA, lançado em 1999, onde cada voluntário fazia o download de um programa para processamento de dados de áudio em seu computador, e quando o equipamento estava IDLE ou na proteção de tela, o programa baixava da internet automaticamente um pacote de áudio para análise, e após o processamento enviava o resultado de volta ao servidor. A capacidade de processamento deste conjunto, com aproximadamente 145 mil equipamentos ativos em 233 países era capaz de processar 668 teraFLOPS!

Onde cabe o uso de Grid

Normalmente Grids de computadores são usados na resolução de problemas matemáticos, científicos e acadêmicos, bem como em empresas para aplicações como descoberta de novos remédios, previsão monetária, análise sísmica, e processamento de retaguarda para comércio eletrônico e Web Services.

Porém, se um problema computacional pode ser adequadamente paralelizável, o uso de uma camada leve de infra-estrutura de grid pode permitir que programas stand-alone recebam partes do processo para serem executadas em várias instâncias de processamento distribuídas em múltiplos equipamentos.

Vale atenção especial na sentença “adequadamente paralelizável”, veremos que atender a esta premissa não é cabível a todos os processos de um sistema, e envolve mais de uma característica de processo, relacionadas a entrada e saída de dados e ao processamento em si. Veremos estes detalhes em profundidade no momento oportuno. Vamos ver primeiro onde ele pode ser aplicado!

Cálculo de Folha de Pagamento

Um cálculo da folha de pagamento dos funcionários de uma empresa, onde cada funcionário possui seu registro de ponto e histórico de exceções em um período. Este é um bom exemplo onde o paralelismo de um grid de processamento serve muito bem.

Em uma aplicação de cálculo de folha de instância única, um programa de cálculo de folha de pagamento é executado em um processo, onde este processo vai calcular, em sequência, a folha de pagamento de cada funcionário no período, um a um. Este programa, em uma única instância sendo executada em um único equipamento, não consegue utilizar todo o poder de processamento deste equipamento.

Logo, em um primeiro momento, podemos ganhar paralelismo alterando o programa para que ele iniciar vários jobs de processamento na mesma máquina, onde cada um deles recebe um código de funcionário diferente para calcular, onde o número de jobs iniciados indica quantos cálculos simultâneos serão feitos. O programa principal pega a lista de códigos ded funcionários a calcular, e passa o código de cada funcionário para o primeiro job livre, até que todos estejam ocupados, e cada job que termina de calcular um funcionário pega ou recebe o código do próximo da lista.

Agora sim, com um número maior de jobs, podemos usar todo o poder de processamento desta máquina, onde aumentamos o número de jobs dedicados até um ponto antes de algum limite computacional ser atingido, como tráfego de rede entre a máquina de processamento e o Banco de Dados, ou as CPUs do equipamento não atingirem 100%.

Se mesmo assim, o uso de uma máquina com jobs não seja suficiente, pois existe por exemplo um tempo curto para o fechamento da folha, e o número de funcionários cresceu com uma fusão de empresas ou absorção do quadro de funcionários de uma empresa do grupo, a solução é usar o processamento de mais de uma máquina. Neste caso, caberia bem o uso de uma infra-estrutura de Grid. Adicionando-se em cada equipamento uma ou mais instâncias de um serviço dedicado ao cálculo dos funcionários, e criando um mecanismo client-server entre o serviço que está solicitando o processamento dos funcionários e todos os jobs disponíveis e dedicados a este cálculo, vários funcionários são processados ao mesmo tempo em equipamentos diferentes, e os resultados de cada cálculo é gravado no Banco de Dados.

Ganho esperado

Esta escalabilidade não é infinita, mais cedo ou mais tarde algum limite computacional do conjunto vai ser atingido, e não necessariamente dobrar o número de máquinas vai dobrar a sua capacidade de processamento, afinal temos que levar em conta uma fatia de tempo do mecanismo de distribuição em trafegar os códigos para o cálculo, e que cada processo pode pedir mais informações ao mesmo tempo e de funcionários diferentes para o Banco de Dados, mas mesmo assim um processo que demorava 10 horas para ser executado em 4 jobs dentro de uma máquina, ao ser divididos em 12 jobs distribuídos em 3 máquinas pode terminar a tarefa completa por exemplo em apenas 4 horas.

Dificuldades intrínsecas

Uma das primeiras dificuldades em um Grid deste tipo é determinar o número ideal jobs dedicados por máquina a serem dedicados para um processamento distribuído. Este número é empirico, e será determinado pela natureza do processamento versus capacidade de tráfego de dados e transações do ambiente computacional. Normalmente um grid busca aproveitar o poder de processamento de um conjunto de máquinas que normalmente estão executando outros processos de outras partes da aplicação, que não consomem todo o recurso computacional da máquina. Porém, um número fixo de jobs de grid executados nesta máquina, junto com outros processos em execução, podem atingir um limite computacional deste equipamento, prejudicando os outros processos desta máquina. A engine de Grid ideal precisaria ser capaz de monitorar as máquinas envolvida, atuando sobre a quantidade de jobs dedicados durante a execução, para atingir a “crista” da onda da curva de desempenho sem esgotar os recursos das máquinas envolvidas.

Determinar quais processos aderem com eficiência à execução em Grid, como eu mencionei anteriormente, é um processo um pouco mais complexo, e envolvem muitas características do processamento, e devido à sua extensão, este tema será abordado com uma lupa em um próximo post.

Deve-se também projetar uma infra-estrutura para distribuição das mensagens de processamento de forma eficiente e o mais leve possível, garantindo ao final do processo que todas as mensagens tenham sido entregues e processadas com sucesso, e em caso de falha ou ausência de retorno — como por exemplo uma queda ou término anormal de um agente de processamento — esta infra-estrutura possa permitir por exemplo o re-envio daquela requisição a outro agente, ou notificar o programa cliente desta ocorrência para que este tome a decisão sobre o que fazer.

ERP Microsiga e Grid de Processamento ADVPL

O ERP Microsiga Protheus possui algumas rotinas que utilizam um mecanismo de processamento em Grid, onde cada máquina virtual servidora do Protheus pode ser configurada como um agente de processamento, e um serviço único é configurado como o coordenador da distribuição dos agentes para processos do Grid ADVPL. Cada funcionalidade que utiliza o Grid compartilha internamente de uma camada comum de abstração “client” do Grid, onde o programador deve implementar uma função de preparação de processo dedicado e uma função de execução de requisição, e a infra-estrutura do grid garante a inicialização dos processos, distribuição de requisições e controle de execução das requisições de um processo que se utiliza do Grid. Não conheço todas as rotinas, mas sei com certeza que o cálculo de folha de pagamento do ERP possui uma opção de processamento para usar o Grid ADVPL.

Este post é a pontinha do iceberg … a cada post deste tema vamos nos aprofundar um pouco mais nas particularidades deste paradigma, que pessoalmente eu acho muito, muito interessante ! Até o próximo post, pessoal 😉

Referências

Wikipedia contributors. Distributed computing. Wikipedia, The Free Encyclopedia. November 20, 2014, 04:59
UTC. Available at: http://en.wikipedia.org/w/index.php?title=Distributed_computing&oldid=634650954.
Accessed November 27, 2014.

Wikipedia contributors. Grid computing. Wikipedia, The Free Encyclopedia. November 23, 2014, 13:50 UTC.
Available at: http://en.wikipedia.org/w/index.php?title=Grid_computing&oldid=635101070. Accessed November
27, 2014.

Wikipedia contributors. SETI@home. Wikipedia, The Free Encyclopedia. November 26, 2014, 13:42 UTC.
Available at: http://en.wikipedia.org/w/index.php?title=SETI@home&oldid=635510528. Accessed November 27,
2014.