Desenvolvimento

Como você compartilha os dados em um aplicação paralela/distribuída?

TL;DR: Esse post é um pequeno overview sobre quatro técnicas de compartilhamento e comunicação em um ambiente concorrente/paralelo/distribuído, é pra quem gosta :D.

Vivemos há um bom tempo em uma era onde as CPUs já não processam somente uma instrução por ciclo de clock. Já temos paralelismo em nível de instruções há muito tempo e de diferentes formas, em arquiteturas VLIW/EPIC, onde o compilador é responsável por encontrar as instruções que possam ser executadas em paralelo, ou até processadores SuperScalar que fazem isso através de algoritmos implementados em hardware, existem também o misto de ambos, que imagino já ser o mais comum hoje. Subindo de nível, os Sistemas Operacionais atuais em sua maioria já estão extremamente otimizados para aproveitar esse potencial e características desses processadores. Sem contar todo trabalho para tentar aproveitar também o potencial das GPUs.

Parallel Groves

Mas em user space como aproveitar ou utilizar bem esse potencial ou característica dessas novas máquinas? Eventualmente precisamos lidar com problemas ou situações onde devemos escrever nossos softwares para trabalhar em ambientes multi-thread ou multi-processos, distribuídos em várias máquinas ou não, seja por necessidade de performance (tempo de resposta, por ex.), por escalabilidade, alta-disponibilidade, ou até por características do sistema. Situações essas cada vez mais comuns nessa era do web-scale onde o crescimento de usuários da Internet expande em grandes proporções e massa de informações geradas em alguns fatores a mais.

Eu me interesso muito por esse assunto, já tive que me envolver muito com ambientes multi-processos, às vezes em busca de escalabilidade, às vezes em busca da maximização do uso da máquina. Vou falar sobre esses projetos em breve. Esse post é exatamente o gatilho para eu iniciar a falar sobre esse assunto daqui em diante, então esperem por novos textos. Nesse primeiro, vou falar de modelos de compartilhamento e troca de informações mais comuns nas implementações desse tipo de sistema. Vou falar pela mais comum, a tradicional memória compartilhada (Shared Memory), de STM (Software Transactional Memory), de Coordination Languages (ou flow programming) até chegar em Message Passing Concurrency. Existem alguns outros, mas essas são os mais comuns e com quais já trabalhei, então nem fiz uma revisão bibliográfica direito, esta tudo saindo da minha cabeça, qualquer erro, me avise, por favor. Mas vamos lá, vou tentar ser breve, algo que não consigo normalmente.

Shared Memory Communication

A tradicional memória compartilhada possui vários problemas. É comum e implementáveis pelas linguagens/plataformas mais populares. A memória compartilhada nada mais é que um região na memória onde reside um dado, geralmente mutável, e que é protegido por locks/guards que evitam que mais de um processo simultaneamente acesse essa região e modifique o dado, podendo deixa-lo em um estado inconsistente e trazendo diversos efeitos colaterais. Houve seu tempo, e hoje na era do web-scale, essa técnica não é mais comum muito menos recomendada. Mas mesmo que você ache que não trabalhe ou nunca mexeu com isso, um banco de dados, aqueles mesmos, PostgreSQL, Oracle, whatever-SQL e whatever-NoSQL são uma espécie memória compartilhada. Mas, obviamente, eles são otimizados e preparados para trabalhar dessa forma,  na sua maioria funcionam utilizando-se das mais variadas técnicas, uma delas é STM.

STM (Software Transactional Memory)

STM é uma espécie de memória compartilhada, mas a forma como a informação compartilhada é lida e alterada por um processo é o que muda. Ao invés de realizar locks em toda a região que deve ser protegida, o dado a ser modificado é copiado para uma região exclusiva do processo, sem qualquer lock ou guard, e não importa se outro processo teve a mesma necessidade ao mesmo tempo. A modificação será “sincronizada” através de um “commit”, de maneira atômica, na parte compartilhada pelos processos. Mas podem haver conflitos, e as soluções podem ser: a última gravação é a que vale ou a última gravação é recusada pois o dado inicialmente utilizado já estava obsoleto e uma nova tentativa deve ser feita. Faça uma analogia com transações com o Banco de Dados, ou até de mecanismos de controle de versão, é a mesma idéia. No final das contas, STM alivia um pouco a parte sequencial necessária. Pode até ser implementado em qualquer linguagem, mas em algumas linguagens isso é nativo, como Haskell usando o compilador GHC.

Coordination Languages (ou flow-based programming)

Coordination Languages pode-se pensar como uma memória global associativa e compartilhada mas o acesso a essas informações é realizado através de determinadas primitivas. Em Linda, mais especificamente, os dados pode ser trafegados de um processo a outro através das primitivas e os dados (chamados tuples) ficam armazenados no tuplespace. As vantagens dessa abordagem é que não é complicado escrever um design eficiente e escalável, e tem uma flexibilidade maior em relação a Message Passing Concurrency usando Actors pois a forma como você compõe a ligação e comunicação entre processos é mais livre, porém, isso as vezes implica na necessidade de locks, mas isso é encapsulado pelas bibliotecas e frameworks e você apenas precisa saber como funciona pra evitar dead-locks ou starvations. Há outra vantagem de que os tuplespaces podem ser persistentes (em disco). Imagine que seu sistema “crasheie”, e quando você reinicia o sistema, ele continua de onde parou, como se nada tivesse acontecido?

Estou trabalhando e um biblioteca em que estou implementando essa técnica, vai haver um post (em desenvolvimento) a parte sobre isso. A primeira versão trabalhará inicialmente em memória, mas como todo o acesso é via primitivas (uma API), há planos de versões com tuplespaces persistentes em disco, inicialmente penso em usar o Redis. Também há planos para trabalhar em tuplespaces distribuídos, e o bacana disso que é possível encadear tuplespaces. Agora imagine um tuplespace distribuído e persistente. Que tal?

Message Passing Concurrency

Em Message Passing (passagem de mensagens) não há região compartilhada e a informação trafega de processo em processo através de mensagens assíncronas. Imagine que cada processo é uma pessoa em uma casa com uma caixa de correio, a informação fica em envelopes e são enviadas e recebidas através de um serviço de troca de mensagens, e você não se preocupa com esse serviço, ele simplesmente funciona como deveria. Enquanto não chega nenhuma mensagem, você pode trabalhar em outras tarefas, ou simplesmente pode esperar pela mensagem. Quando a informação chega, você abre o envelope, ele é seu e ninguém o compartilha, você pode alterá-lo sem afetar ninguém, e pode responde-lo, para o remetente ou envia-la pra quem quer que seja. Esse modelo é o mais adequado principalmente quando há grandes necessidades de escalabilidade, pois suporta com mais facilidade muito mais processos pois não há locks ou problemas de sincronização de processos.

Em termos de performance (tempo de resposta, maximização de uso de CPU), Message Passing, nem sempre é o mais adequado, pois o overhead da comunicação entre os processos é algo a se considerar. Também não é modelo fácil de se implementar. Mas não há locks ou side-effects! Erlang e sua VM possui isso build-in na linguagem. Então, se seu problema é escalabilidade, exemplo, suportar centenas de milhares de usuários simultâneos, Erlang pode ser uma boa escolha. O Facebook tem um case de sucesso a respeito, é bom dar uma olhada.

Conclusão

Já passei por todas essa técnicas e digo que as duas últimas são as mais eficientes, mas a realidade é cruel e não é sempre que você consegue um design se fazendo do uso de apenas uma dessas técnicas, mas não se preocupe, essas técnicas são utilizadas em design mais alto nível, muitas vezes em um nível micro você ainda terá memória compartilhada e todos os problemas que ela acarreta, que não são tão terríveis assim se você fizer tudo direito. Então existe toda uma mistura, sem neurose, ok?! Para uma boa escolha, estude sua necessidade, se o problema é tempo de resposta, escalabilidade ou evitar gargalhos (I/O). Faça também benchmarks pra descobrir quais são os bottlenecks, na maioria das vezes, será I/O, seja de disco ou rede.

Estou começando a escrever sobre esse assunto aqui no blog da Jera, se você tiver um interesse particular nesse assunto, venha nos visitar ou se estiver longe demais pra vir aqui, comente a respeito! Até!

Fonte da imagem