Baixe o app para aproveitar ainda mais
Prévia do material em texto
DESCRIÇÃO A computação distribuída como base para desenvolvimento das aplicações atuais de tecnologia e sua efetivação como campo de conhecimento da Ciência da Computação. PROPÓSITO Compreender o funcionamento dos sistemas distribuídos − no ponto de vista do desenvolvimento das aplicações paralelas e distribuídas − e sua contribuição para acelerar as cargas de trabalho da atualidade, bem como mensurar os impactos dos métodos aplicados para resolução de problemas inerentes à distribuição de serviços, tais como: replicação, transparência, consumo de mensagens, comunicação etc. PREPARAÇÃO Antes de iniciar este conteúdo, tenha em mãos um ambiente de programação C: Code:: Blocks, Visual Studio, Eclipse CDT, dentre outros. OBJETIVOS MÓDULO 1 Analisar os conceitos básicos dos paradigmas de comunicação MÓDULO 2 Identificar os modelos utilizados para coordenação de tempo e para garantia da transparência de localização MÓDULO 3 Analisar os conceitos de consistência e replicação nos sistemas distribuídos MÓDULO 4 Identificar as principais interfaces de desenvolvimento de códigos para sistemas paralelos e distribuídos INTRODUÇÃO Nas últimas décadas, a taxa de crescimento do poder de processamento de minicomputadores, mainframes e os tradicionais supercomputadores tem sido algo em torno de 20% ao ano. E a taxa de crescimento do poder de processamento de microprocessadores e demais sistemas de apoio como GPUs − tipo de processador envolvendo algoritmos baseados em vetores e matrizes, como os usados na área de processamento gráfico/jogos − tem sido algo em média de 50% ao ano. O resultado desses avanços é a facilidade de interligar diversos computadores por meio de um barramento externo de alta velocidade como uma rede local. Podemos, então, definir uma nova era a partir dessas fusões de tecnologias que dão origem aos sistemas distribuídos: a distribuição do processamento entre diversos computadores diferentes. Muito mais do que subdividir tarefas, esse novo paradigma nos convida a criar tarefas computacionais mais especializadas, conforme a natureza da função de cada computador. Um exemplo clássico é a utilização do ambiente cliente/servidor usando comunicações por sockets, chamadas de procedimentos remotos, primitivas de passagens de mensagens, dentre muitas outras. Neste conteúdo, aprenderemos como é tratada a computação distribuída. Como as tecnologias convergem para entregar uma coleção de computadores autônomos, conectados por redes − locais ou remotas −, que aparentam aos seus utilizadores como um único sistema computacional, e em paradigmas inerentes aos fundamentos dos sistemas distribuídos, nos quais os controles para o desenvolvimento se tornam complexos, para criar a ilusão de um ambiente transparente, coerente, sincronizado e tolerante a falhas. MÓDULO 1 Analisar os conceitos básicos dos paradigmas de comunicação TROCA DE MENSAGENS As partes que compõem um sistema distribuído interagem pela rede para trabalhar de forma cooperativa. As partes envolvidas trocam dados e mensagens, utilizando serviços de comunicação fornecidos pelo sistema hospedeiro (host) e adotando protocolos de comunicação para que possam entender uns aos outros. Essa comunicação se dá principalmente por não existir, na maioria dos casos, uma memória física compartilhada distribuída. Portanto, nos sistemas distribuídos, essa comunicação se dá exclusivamente por passagens de mensagens nos agentes envolvidos por meio de protocolos e métodos próprios para conversação entre os hosts. O modelo cliente/servidor é a base para os sistemas de computação distribuída. Nele, um processamento cooperativo de requisições submetidas por um cliente a um servidor que as processa retorna um resultado, estando esses recursos de cliente e servidor espalhados sobre vários computadores. Cliente/servidor. Em um modelo cliente/servidor, temos dois agentes envolvidos nessa comunicação com funções muito bem definidas, na qual processos servidores entregam serviços para máquinas clientes. Com isso, o ambiente gera simplicidade e eficiência em todo o processo. Podemos afirmar, então, que: A troca de mensagens é um processo que envia uma mensagem pela rede destinada a um ou mais processos. A abrangência pode ser local ou remota, e as mensagens podem ter diversos comportamentos, como tamanho (fixo ou variável, limitado ou não), bem como ser síncronas ou assíncronas. Para aumentar a escalabilidade em um sistema distribuído, devemos fazer uso de algumas técnicas para resolver problemas relacionados a distância com perdas e atrasos dos pacotes. Uma delas é evitar a espera por respostas a requisições enviadas a dispositivos remotos, permitindo seguir executando outras tarefas independentemente da resposta a ser recebida por quem requisitou os serviços. Com isso, é possível trabalhar em outras tarefas e, com a chegada da resposta, o sistema pode tratar de forma coerente e concluir uma requisição enviada anteriormente. Essa técnica é denominada comunicação assíncrona. Muitas aplicações paralelas são candidatas nativas, assim como aplicações em lote, comumente usadas em processamento de Big Data. Elas podem ter suas execuções, tipicamente independentes, orquestradas para execução na qual outra tarefa fica responsável em monitorar a conclusão da comunicação. COMENTÁRIO Mas nem tudo são boas notícias. Existem muitas aplicações que não podem usar a comunicação assíncrona, principalmente aplicações interativas, quando o usuário envia uma requisição e tem que esperar até receber uma resposta. Podemos, no entanto, resolver esse problema entregando parte da resolução para o lado cliente executar. COMUNICAÇÃO EM GRUPO Coordenar sistemas distribuídos é uma das tarefas mais difíceis de se adotar devido a diversos fatores, tais como: Computadores podem falhar ou ficar indisponíveis por algum outro motivo. Novas máquinas podem ser inseridas e/ou removidas do sistema. Linhas de comunicações não são 100% confiáveis. Eventos podem ocorrer de forma concorrente. Em comunicações com chamadas a procedimentos remotos (RPC – Remote Procedure Calls), envolvemos somente dois processos que permitem que programas chamem procedimentos localizados em outras máquinas. Quando um processo na máquina A chama um procedimento na máquina B, o transmissor A será suspenso enquanto estiver executando o procedimento em B. As informações são transportadas do transmissor para o receptor por parâmetros, e retornam o resultado da execução do procedimento. Assim, nenhuma troca de mensagens será visível ao programador. A ideia é fazer com que uma chamada de procedimento remoto pareça o mais possível com uma chamada local, tendo que ser transparente. Sendo assim, o procedimento de chamada não precisa estar ciente de que o procedimento enviado está em execução em uma máquina diferente, e vice-versa. ATENÇÃO Esse processo, no entanto, gera um problema: ele não consegue ser executado por diversas máquinas ao mesmo tempo. Sempre ficará restrito a duas entidades, um transmissor e um receptor. Como podemos, então, resolver esse problema? A resposta é simples: comunicação em grupos. Com uma única operação, podemos enviar uma mensagem a vários destinos, permitindo que processos em um grupo sejam tratados como uma única abstração. Esse conjunto de processos cooperam entre si para prover um serviço, coordenando a troca de mensagens entre os membros do grupo e dos processos externos com o grupo. Os grupos são dinâmicos, por isso novos grupos podem ser criados e os antigos podem ser destruídos. Um processo pode juntar-se a um grupo ou deixá-lo, e é possível ser membro de diversos grupos ao mesmo tempo. São necessários, portanto, mecanismos para gerenciar grupos e os processos participantes dos grupos. Quando usamos mecanismos de comunicação em grupo, os processos tratam com coleções de outros processos de forma abstrata. Se um processo envia uma mensagem para um grupo de servidores,esse processo não precisa se preocupar com quantos são os servidores nem onde eles estão localizados. Já na chamada seguinte, o número de servidores desse grupo e suas localizações podem mudar, sendo essa alteração transparente para o processo, que envia a mensagem para o grupo. São aplicações de comunicação em grupos: Servidores altamente disponíveis e confiáveis. Replicação de bancos de dados. Conferências multimídia. Jogos distribuídos. Aplicações que em geral necessitem de uma alta taxa de disponibilidade, confiabilidade e tolerância a falhas. A comunicação pode ser implementada usando os conceitos de multicast, broadcast ou unicast: MULTICAST Os pacotes são enviados de uma só vez para todos os processos de um grupo. BROADCAST Os pacotes são enviados para todas as máquinas, e somente os processos que fazem parte do grupo não os descartam. UNICAST Transmissão estilo ponto a ponto, na qual o processo tem que enviar mensagem para cada membro do grupo. A maneira como será implementada a comunicação em grupo depende das características envolvidas na interligação dos equipamentos. Em algumas redes, é possível criar um endereço especial no qual várias máquinas podem escutar. Quando uma mensagem é enviada para um desses endereços, automaticamente ela será recebida por todas as máquinas que estão escutando esse endereço. Essa técnica é chamada de multicasting, e implementá-la é simples: basta atribuirmos um endereço de multicasting para cada grupo. Comunicação multicast. Temos, ainda, redes que suportam a comunicação em broadcasting, na qual as mensagens contêm um determinado endereço que é escutado por todas as máquinas (broadcast global) ou direcionado a uma sub-rede específica (broadcast local). COMENTÁRIO O broadcasting pode ser usado para implementar comunicação em grupo, embora não seja tão eficiente. Isso porque todos na rede irão receber as mensagens, e cada processo terá que avaliar se aquela mensagem recebida é para o grupo ao qual ele pertence ou não. Nesse caso, o processo terá gasto recurso de tempo e de processamento de forma desnecessária. Mesmo assim, temos a vantagem que uma única mensagem enviada poderá atingir a todos os equipamentos de uma determinada rede. Comunicação broadcast. Quando a comunicação é um a um, a denominamos de unicasting. Comunicação unicast. Além das características normais encontradas nos mecanismos de troca de mensagens − tais como bufferização e blocagem −, no tratamento de grupos temos novas características de organização, endereçamento e outras que serão detalhadas a seguir. Podemos ter grupos fechados, nos quais somente os membros podem mandar mensagens para os outros membros, que geralmente são utilizados para execução paralela. ATENÇÃO Os processos de fora não podem enviar mensagens para o grupo como um todo, embora possam enviar mensagens para membros individuais. E podemos ter os grupos abertos, nos quais qualquer processo pode enviar uma mensagem para qualquer grupo. Normalmente, esse tipo de mensagem é muito utilizado em servidores replicados. Grupos fechados x grupos abertos. Quanto à organização, possuímos: GRUPOS PARES OU SEMELHANTES Todos os processos são tratados como iguais ou pares, e as decisões são tomadas coletivamente. Essa característica está relacionada à estrutura interna do grupo. Em alguns grupos, os processos são iguais ou semelhantes, nenhum processo é gerente e todas as decisões são feitas coletivamente. GRUPOS HIERÁRQUICOS Um processo é o coordenador e os demais estão subordinados a ele − uma visão mestre/escravo. Nesse caso, quando uma requisição é feita, seja por um cliente externo ou por um dos membros do grupo, ela é enviada ao coordenador. Este decide, então, qual dos processos do grupo é o mais indicado para executar a solicitação. Grupos semelhantes x hierárquicos. Cada uma dessas organizações tem suas próprias vantagens e desvantagens. A vantagem é que na comunicação em grupos de semelhantes não existe um ponto único de falha. Se um dos processos falhar, o grupo continua, apenas se torna menor. A desvantagem é que a tomada de decisão é mais complicada. Qualquer decisão precisa envolver todos do grupo, gerando uma demora e uma sobrecarga na comunicação e dos eventos. No grupo hierárquico temos o oposto, já que todas as decisões são tomadas pelo coordenador, como qual processo do grupo vai executar qual atividade. Quando um processo termina, ele avisa ao coordenador que está pronto para outra atividade. A perda do coordenador em um grupo hierárquico, portanto, causa uma parada nas atividades do grupo. Nesse caso, algum algoritmo pode ser usado para eleger um processo coordenador, se o principal falhar. ATENÇÃO Em um mecanismo de comunicação em grupo, é necessário um controle sobre a criação e destruição de grupos, bem como permitir que processos se integrem a eles grupo ou os deixem. Um enfoque é ter um servidor de grupo para o qual todas as requisições possam ser feitas. Esse servidor pode manter um controle total de todos os seus grupos e sobre quais participantes pertencem a qual grupo, estratégia essa concentrada no modelo centralizado. Fácil de implementar, eficiente e de rápido entendimento. Infelizmente, ele compartilha com a maior desvantagem das técnicas centralizadas: um único ponto de falha. Se o servidor de grupos falhar, o gerenciamento do grupo deixa de existir. Provavelmente, a maioria dos grupos terá que ser reconstruída, interrompendo o trabalho que estava em andamento. O método oposto é gerenciar os grupos de forma distribuída. Em um grupo aberto, um processo de fora pode enviar uma mensagem para todos anunciando a sua presença. Um grupo fechado só aceita mensagem para todos de algum processo de fora se for uma solicitação para participar do grupo. Uma mensagem direta sem antes estar participando do grupo fechado não seria aceita. Para um processo deixar de fazer parte de um grupo, basta enviar uma mensagem de adeus (goodbye) a todos. Consideram-se, ainda, duas características relativas ao controle dos participantes do grupo. Se um membro falhar, ele efetivamente deixa de pertencer ao grupo. O problema é que pode não existir um aviso a todos os outros membros, como quando o processo deixa o grupo de forma padrão. Os demais membros têm que descobrir por conta própria que aquele participante não está respondendo e o excluir do grupo. Outro ponto é o sincronismo necessário entre as mensagens sendo enviadas e a entrada ou saída de um membro no grupo. Uma última preocupação relacionada ao controle dos participantes do grupo é quando muitas máquinas param de forma anormal. EXEMPLO Quando ocorre um particionamento da rede, deixando o grupo inoperante. Algum protocolo se faz necessário para reconstruir o grupo com as máquinas que restaram. Podemos citar, ainda, outros aspectos importantes em uma comunicação em grupos, como atomicidade ou broadcast atômico, em que uma mensagem chegará a todos os membros do grupo ou para nenhum (ou tudo ou nada). Quando qualquer processo enviar uma mensagem para um grupo, ele não deve preocupar-se se algum outro membro do grupo não recebeu essa mensagem. Com essa propriedade da atomicidade, o processo receberá uma mensagem de erro caso um ou mais integrantes do grupo tenham tido problemas no recebimento. Do ponto de vista de programação, tudo se passa como se fosse enviado para apenas um receptor. Outra propriedade importante é a ordenação de mensagens. As mesmas podem chegar desordenadas, e precisaríamos de um sistema de buffer para armazená-las e tratá-las de forma adequada. EXEMPLO Por exemplo, adotar uma postura de ordenação global − que entrega todas as mensagens exatamente na ordem na qual foram criadas − ou ordenação consistente − na qual o sistema decide qual mensagem precede outra ‒, quando as mensagens são enviadas quase ao mesmo tempo, e entregar as mensagens nessa ordem. Um mecanismo de comunicação em grupo precisater uma semântica bem definida com relação à ordem em que as mensagens serão entregues. A melhor garantia para isso é fazer com que as mensagens sejam entregues na mesma ordem em que foram enviadas. COMENTÁRIO Como referência de estudo do ambiente de controle de sistemas de gerenciamento de cluster focados em Big Data, existe a ferramenta ZooKeeper, que mostra uma visão prática de uma coordenação de eventos e comunicação efetiva em sistemas distribuídos. CÓDIGO MÓVEL Em sistemas distribuídos tradicionais, os componentes de aplicações são essencialmente estáticos. O código móvel denomina um conjunto de tecnologias de linguagem e plataforma de sistemas distribuídos que suportam a construção de programas de computador instalados em computadores servidores. Esses programas são transferidos, sob demanda, para os computadores clientes e automaticamente executados, da forma mais segura possível, para eles. Ou seja, executam atualizações dinâmicas fazendo injeção de código em programas/componentes em execução, adaptando-se a condições do processo de execução. Um componente é instanciado em um determinado nó do ambiente e permanece neste durante todo seu ciclo de vida. 1 Interações remotas entre componentes se dão por meio de troca de mensagens pela rede. As tecnologias que suportam mobilidade de código permitem que trechos de código possam ser transmitidos pela rede para serem executados em locais remotos. 2 3 A migração não é transparente aos desenvolvedores de aplicações distribuídas, mas explicitamente manipulada por eles para atingir objetivos próprios da aplicação. Mobilidade de código fornece flexibilidade para o gerenciamento da configuração de código em um ambiente distribuído, e pode oferecer vantagens para a execução de atividades distribuídas. A adequação de applets Java para a modelagem de interações com usuários finais, por exemplo, é observável pelo número de aplicações na Internet que utilizam essa tecnologia. Os applets são pequenos programas Java, criados pela Sun Microsystems em 1995, que são instalados em servidores Web e referenciados por páginas HTML. Quando um browser acessa essa página HTML que referencia o applet, ele automaticamente também transfere o código do applet e o executa. Os applets são, nos dias atuais, os principais responsáveis pela grande disseminação do conceito e das tecnologias de código móvel em geral. Grande parte dos esforços para o suporte a código móvel se destina ao desenvolvimento de sistemas de agentes móveis e de aplicações baseadas em tais sistemas. Um agente móvel (ou simplesmente agente) é um elemento de software autocontido responsável pela execução de uma tarefa, capaz de migrar de forma autônoma em um sistema distribuído. Um agente migra em um ambiente distribuído de um local lógico − denominado neste conteúdo de agência − a outro. Quando um agente migra, sua execução é suspendida na sua origem. Ele é transportado (isto é, seu código, estado de dados, estado de execução e informação de controle) para outro componente no sistema distribuído, no qual sua execução é retomada. Ou seja, o modelo de código móvel basicamente desloca a carga computacional para a máquina cliente. COMENTÁRIO Como percebemos até o momento, do ponto de vista da segurança, temos diversos problemas associados, e aos poucos esse modelo de computação distribuída vem sendo tratado com muito cuidado pelo mercado corporativo. Como características acopladas à tecnologia de código móvel, podemos apontar o uso de uma execução segura dentro de um sandbox, no qual o código é trazido de um servidor qualquer, denominado código estrangeiro, e executado em um espaço de nomes restrito (o sandbox). Dessa forma, na execução de seu código, ele terá acesso limitado à CPU, à memória e a quaisquer outros recursos computacionais gerenciados pelo sistema operacional por meio de métodos de controle de acesso lógico. EXEMPLO Podemos descrever os applets Java que, quando em execução dentro de um sandbox, apresentam diversas restrições. Já o modelo de código móvel usando ActiveX não utiliza o conceito de sandbox. Sua segurança de execução é baseada no processo de autenticação da procedência do código por assinaturas digitais validadas por uma entidade certificadora por intermédio de certificados digitais. Assim, o programa executa no ambiente operacional do usuário sem qualquer restrição computacional. ATENÇÃO Para um código ser móvel, ele deve possuir uma visão completa de abstração, de tal forma que não fique preso a detalhes e dependências do sistema operacional e/ou ao hardware escolhido. Ou seja, deve ser aderente em múltiplas plataformas e sistemas operacionais. Por meio de um projeto de máquina virtual consistente, é possível se garantir a execução homogênea de um mesmo código móvel em várias plataformas específicas sem que seja necessário se recompilar esse código. EXEMPLO Programas construídos na linguagem Java são compilados para um formato de código intermediário, chamado de bytecode. A máquina virtual Java que interpreta esses bytecodes é um microprocessador, implementado em software. Ele interpreta um conjunto de aproximadamente 160 instruções de uma máquina virtual especialmente projetada para executar programas Java. A execução de programas de código móvel é feita sob demanda dos clientes. Sendo assim, não há como prever quando um programa disponibilizado em um servidor será utilizado por quaisquer clientes. Para que funcione adequadamente sob essas condições de imprevisibilidade, todas as etapas precisam ocorrer da forma mais automática e rápida possível, sem que o usuário esteja envolvido em uma série de questionamentos sobre a configuração local do programa. As etapas são: Transmissão do código (do servidor para o cliente). Instalação do código (ativação da máquina virtual, carga do sandbox, verificação da consistência do código recebido, ligação entre o código recebido e as bibliotecas locais do cliente). Execução do código, com a criação de processos, alocação de memória para dados e instruções etc. Com isso em mente, entregamos transparência e desempenho aceitável na transmissão, instalação e execução do código. ORIENTAÇÃO A EVENTOS Um evento pode ser definido como uma mudança significativa do seu estado. EXEMPLO Quando um consumidor adquire um imóvel, o estado dele se modifica de "à venda" para "vendido". A arquitetura desse sistema pode tratar essa mudança de estado como um evento cuja ocorrência pode ser divulgada para outros aplicativos dentro da sua arquitetura. CONCEITOS DE SISTEMAS DE MENSAGENS De uma perspectiva formal, o que é produzido, publicado, propagado, detectado ou consumido é uma mensagem (geralmente assíncrona), chamada de notificação de evento, e não o próprio evento, que é a mudança de estado que acionou a emissão da mensagem. Isso se deve às arquiteturas orientadas a eventos, muitas vezes sendo projetadas sobre arquiteturas orientadas a mensagens, nas quais tal padrão de comunicação requer que uma das entradas seja somente texto (a mensagem), para diferenciar como cada comunicação deve ser tratada. Um sistema de mensagens é um dos mecanismos mais comumente usados para troca de informações entre aplicações. Ao escolher seu mecanismo de integração de aplicações, é importante ter em mente os princípios orientadores discutidos anteriormente. No caso de bancos de dados compartilhados, por exemplo, alterações feitas por um aplicativo podem afetar diretamente outros aplicativos que estão usando as mesmas tabelas de banco de dados. Ambas as aplicações são fortemente acopladas. Você pode querer evitar isso nos casos em que tenha regras adicionais a serem aplicadas antes de aceitar as alterações no outro aplicativo. Da mesma forma, você deve pensar sobre todos esses princípios orientadores antes de finalizar formas de integração entre suas aplicações. Um sistema de mensagens atua como um componente de integração entre vários aplicativos.Um sistema orientado a eventos normalmente consiste em: EMISSORES (OU AGENTES) Os emissores têm a responsabilidade de detectar, reunir e transferir eventos. Um emissor de evento não conhece os consumidores, nem mesmo sabe se existe ou não um consumidor e, caso exista, não sabe como o evento será utilizado ou processado. CONSUMIDORES (OU COLETORES) Os coletores têm a responsabilidade de aplicar uma reação assim que um evento seja apresentado. A reação pode ou não ser totalmente fornecida pelo próprio coletor. Por exemplo, o coletor pode ter apenas a responsabilidade de filtrar, transformar e encaminhar o evento para outro componente ou pode fornecer uma reação independente a tal evento. CANAIS DE EVENTOS Os canais de eventos são transmitidos dos emissores para consumidores. A implementação física dos canais pode ser baseada em componentes tradicionais, como middleware orientado à mensagem ou comunicação ponto a ponto. Em um projeto de um sistema de integração entre aplicações, alguns fatores/princípios devem ser colocados em mente, tais como: fracamente acoplado, definições de interfaces comuns, latência e confiabilidade. Vamos compreender cada um deles a seguir: FRACAMENTE ACOPLADO A interação entre as aplicações deve garantir uma dependência mínima entre elas. Isso garante que qualquer modificação em uma aplicação não irá afetar a outra. É diferente dos sistemas fortemente acoplados, nos quais uma aplicação é codificada com especificações predefinidas da outra aplicação, e qualquer mudança pode quebrar ou mudar suas funcionalidades com outras aplicações dependentes. PADRÕES DE INTERFACES COMUNS Deve garantir um formato de dados comum acordado para troca de mensagens entre aplicativos. Isso não apenas ajuda a estabelecer padrões de troca de mensagens entre aplicativos, mas também garante que algumas das melhores práticas de troca de informações possam ser aplicadas facilmente. Por exemplo, você pode escolher usar o formato de dados Avro (muito usado em Big Data) para trocar mensagens. Isso pode ser definido como seu padrão de interface comum para troca de informações. LATÊNCIA É o tempo que as mensagens levam para passar entre o remetente e o receptor. A maioria dos aplicativos deseja atingir uma baixa latência como um requisito crítico. Mesmo em um modo de comunicação assíncrono, a alta latência não é desejável, pois um atraso significativo no recebimento de mensagens pode causar perdas significativas para qualquer organização. CONFIABILIDADE Garante que a indisponibilidade temporária dos aplicativos não afete as aplicações dependentes que precisam trocar informações. Em geral, quando o aplicativo de origem envia uma mensagem para o aplicativo remoto, este pode estar lento ou não disponível devido a alguma falha. A comunicação de mensagens assíncronas e confiáveis garante que a fonte da aplicação continue o seu trabalho, e garante que o aplicativo remoto retomará sua tarefa mais tarde. Uma vez compreendidas essas premissas iniciais, exploraremos alguns conceitos básicos de controle de mensageria. FILA DE MENSAGENS Estruturas que também são referenciadas como canais. De uma forma bem simples, podemos definir como conectores que enviam/recebem mensagens entre as aplicações de forma oportuna e confiável. MENSAGENS (PACOTES DE DADOS) Uma mensagem é um pacote de dados que é transmitido por uma rede para uma fila de mensagens. O aplicativo remetente divide os dados em pacotes de dados menores e os envolve como uma mensagem com informações de protocolo e cabeçalho. Em seguida, ele o envia para a fila de mensagens. De maneira semelhante, um aplicativo receptor recebe uma mensagem e extrai os dados do invólucro para processá-los posteriormente. REMETENTE (PRODUTOR) Os aplicativos do remetente ou produtor são as fontes de dados que precisam ser enviados a um determinado destino. Eles estabelecem conexões com pontos de extremidade da fila de mensagens e enviam dados em pacotes de mensagens menores, que seguem padrões de interface comuns. COMENTÁRIO Dependendo do tipo de sistema em uso, os aplicativos remetentes podem decidir enviar dados um a um ou em lote. DESTINATÁRIO (CONSUMIDOR) São os destinatários das mensagens enviadas pelo aplicativo remetente. Eles extraem ou recebem, por meio de uma conexão persistente, dados de filas de mensagens. Em seguida, extraem dados desses pacotes de mensagens e os usam para processamento posterior. PROTOCOLOS DE TRANSMISSÃO DE DADOS Determinam as regras para controle das trocas de mensagens entre aplicativos. Diferentes sistemas de filas usam diferentes protocolos de transmissão de dados. Alguns exemplos de tais protocolos de transmissão de dados são: AMQP (Advance Message Queuing Protocol). STOMP (Streaming Text Oriented Message Protocol). MQTT (Message Queue Telemetry Protocol). HTTP (Hypertext Transfer Protocol). MODO DE TRANSFERÊNCIA O modo de transferência em um sistema de mensagens pode ser entendido como a maneira pela qual os dados são transferidos de uma aplicação de origem para a aplicação receptora. EXEMPLO Modos síncrono, assíncrono e em lote. SISTEMA DE MENSAGENS POINT-TO-POINT (PTP) Em um modelo PTP, os produtores de mensagens são chamados de remetentes (senders) e os consumidores, de destinatários (receivers). Eles trocam mensagens para um destino denominado fila. Os remetentes produzem mensagens para uma fila e os destinatários consomem mensagens dessa fila. O que distingue esse sistema é que uma mensagem pode ser consumida por apenas um único destinatário. Pode haver vários destinatários ouvindo na fila da mesma mensagem, mas apenas um deles a receberá. Sistema de mensageria PTP. COMENTÁRIO Note que também pode haver vários remetentes, que enviarão mensagens para a fila, mas estas serão recebidas por apenas um destinatário. Os remetentes podem compartilhar uma conexão ou usar conexões diferentes, mas todos podem acessar a mesma fila. Assim, mais de um destinatário pode consumir mensagens de uma fila, mas cada mensagem pode ser consumida por apenas um destinatário. Observe na imagem a seguir, que as mensagens 1 e 2 são consumidas por diferentes destinatários. Como no caso dos remetentes, os destinatários também podem compartilhar uma conexão ou usar conexões diferentes, assim como todos podem acessar a mesma fila. Sistema de mensageria PTP múltiplos remetentes/destinatários. ATENÇÃO Remetentes e destinatários não têm dependência de tempo; o destinatário pode consumir uma mensagem, independentemente de estar em execução quando o remetente produziu e enviou a mensagem. As mensagens são colocadas em uma fila na ordem em que são produzidas, mas a ordem em que são consumidas depende de fatores como data de vencimento ou prioridade da mesma, se um seletor é usado no consumo de mensagens e a taxa relativa de processamento da mensagem consumidora. Os remetentes e destinatários podem ser adicionados e/ou excluídos dinamicamente no tempo de execução, permitindo que o sistema se expanda ou contraia conforme necessário. SISTEMA DE MENSAGENS PUBLISHER/SUBSCRIBE (PUB/SUB) Neste tipo de modelo, um assinante (subscribe) registra seu interesse em um determinado tópico ou evento e é posteriormente notificado sobre este de forma assíncrona. Os assinantes são subsequentemente notificados de qualquer evento gerado por um editor (publisher) que corresponda ao seu interesse registrado. Esses eventos são gerados pelos editores. Um modelo de mensagem Pub/Sub é usado quando você precisa transmitir um evento ou mensagem para muitos consumidores. Ou seja, nesse modelo todos os assinantes que estiverem ouvindo o tópico receberão a mensagem. Além disso, estas podem ser retidas no tópico até que sejam entregues aos assinantes ativos. O ponto importante aqui é que vários assinantes podem consumir a mensagem. Sistema de mensageria Pub/Sub. Podemos resumir o fluxo do sistema Pub/Sub da seguinte forma: As mensagens são compartilhadas por meiode um canal chamado tópico. Um tópico é um local centralizado onde os editores (publishers) podem publicar e os assinantes (subscribers) podem consumir mensagens. Cada mensagem é entregue a um ou mais consumidores, chamados assinantes. O editor geralmente não sabe e não tem conhecimento de quais assinantes estão recebendo as mensagens de tópico. Estas são enviadas aos assinantes/consumidores sem a necessidade de terem sido solicitadas. As mensagens entregues a um tópico são enviadas automaticamente a todos os consumidores qualificados. Não há acoplamento dos editores com os assinantes. Ambos, assinantes e editores, podem ser adicionados dinamicamente em tempo de execução, o que permite ao sistema aumentar ou diminuir a complexidade ao longo do tempo. Todo consumidor que assina um tópico recebe sua própria cópia das mensagens publicadas nesse tópico. Uma única mensagem produzida por um editor pode ser copiada e distribuída para centenas ou mesmo milhares de assinantes. Representação gráfica do modelo Pub/Sub. Qualquer situação na qual seja necessário notificar vários consumidores de um evento é um bom uso do modelo Pub/Sub. EXEMPLO Você deseja enviar uma notificação a um tópico sempre que ocorrer uma exceção em seu aplicativo ou componente do sistema. Você pode não saber como essas informações serão usadas ou que tipos de componentes as usarão. O editor/transmissor não se importará ou precisará se preocupar com a forma como as informações serão usadas, ele simplesmente publica em um tópico. Essa é a magia de um sistema fracamente acoplado. PROTOCOLO DE ENFILEIRAMENTO DE MENSAGENS AVANÇADO (AQMP) Como abordado no início deste tópico, existem diferentes tipos de protocolos de transmissão que definem como os dados irão fluir entre transmissores, receptores e filas de mensagens. Vamos, então, abordar um protocolo que se faz presente em produtos concentrados em Big Data, como o Apache Kafka: o AQMP (Advanced Queue Messaging Protocol). AQMP é um protocolo aberto para enfileiramento de mensagens assíncronas que se desenvolveu e amadureceu ao longo de vários anos. AQMP fornece um conjunto completo de funcionalidades de mensagens que pode ser usado para suportar cenários de mensagens bem avançados. Conforme ilustrado na imagem a seguir, existem três componentes principais em qualquer sistema de mensagens baseado em AQMP: Arquitetura do AQMP. Nessa arquitetura, os produtores enviam mensagens aos brokers (corretores), que, por sua vez, as entregam aos consumidores. COMENTÁRIO Cada broker possui um componente chamado exchange (intercâmbio) que é responsável por rotear as mensagens dos produtores para as filas de mensagens apropriadas. Cada componente pode ser multiplicado, aumentando, assim, a escalabilidade da solução. Produtores e consumidores comunicam-se entre si por meio de fila de mensagens associadas aos brokers. O protocolo AQMP provê confiabilidade e garantia na entrega de mensagens ordenadas. Vimos aqui a importância dos sistemas de mensageria em sistemas distribuídos, que, em nossa tecnologia contemporânea, têm sido muito usados, principalmente no gerenciamento de mensagens de produtos de Big Data. OS PARADIGMAS DE COMUNICAÇÃO EM COMPUTAÇÃO DISTRIBUÍDA Para finalizar este módulo, assista ao vídeo a seguir. Nele, abordamos os paradigmas para comunicação em computação distribuída. VERIFICANDO O APRENDIZADO MÓDULO 2 Identificar os modelos utilizados para coordenação de tempo e para garantia da transparência de localização NOMEAÇÃO Nomes desempenham papel importante em todos os sistemas de computação, e são obviamente fundamentais em sistemas distribuídos, sendo amplamente usados para, por exemplo, compartilhar recursos e identificar entidades. ATENÇÃO Nomes simples são bons para as máquinas, mas não são convenientes para seres humanos. Então, para resolver isso, é necessário implementar um sistema de resolução de nomes por meio de uma nomeação estruturada. Em um sistema distribuído, essa implementação costuma ser partilhada por várias máquinas, e a forma como a distribuição é feita influencia diretamente na escalabilidade e eficiência do sistema. Os nomes responsáveis pela identificação unívoca de entidades e localizações desempenham um importante papel nos sistemas computacionais. Chamamos de entidade qualquer coisa, como discos, hosts, impressoras, arquivos, dentre outros. Todos são acessados por meio de um ponto, basicamente um endereço associado − como um servidor e seu endereço IP −, e saber o endereço IP não torna o processo amigável. Essa entidade pode oferecer mais de um ponto de acesso, e o mesmo pode mudar ao longo do tempo. COMENTÁRIO Os nomes são organizados no conceito de espaço de nomes. Um nome independente de sua localização significa que o nome da entidade é independente do seu endereço. Ou seja, nada poderá ser referenciado sobre o endereço da entidade associada. Vamos averiguar os seguintes exemplos a seguir: http://www.xyz.org/index.html http://www.xyz.br/index.html Podemos concluir que os dois exemplos são independentes de localização, mas, como podemos observar, o primeiro não referencia de forma consistente a localização da entidade. Caso um sistema distribuído seja reorganizado e servidores mudem de endereço, é importante que seja possível continuar a acessar o serviço de forma totalmente transparente. E, para isso, devemos adotar um nome designado para ser usado por seres humanos, constituídos por cadeias de caracteres de fácil entendimento, denominado “nome amigável”. Um identificador é um nome que reconhece uma entidade e possui as seguintes propriedades: 1 Um identificador referencia, no máximo, uma entidade. Cada entidade é referenciada por, no máximo, um identificador. 2 3 Um identificador sempre referencia a mesma entidade, isto é, nunca é reutilizado. O uso de identificadores permite que entidades sejam referenciadas sem ambiguidade. 4 . Podemos citar como exemplos de identificadores: ISBN de livros. Endereço MAC ethernet. Matrícula de um funcionário. Código de identificação de produtos. Um sistema de nomeação mantém uma vinculação nome-endereço, que, em sua forma mais simples, é apenas uma tabela de pares (nome, endereço). Em sistemas distribuídos de grande escala, é necessário usar tabelas descentralizadas, assim como funciona o Domain Name Service (serviço de nomes de domínio), no qual um nome é decomposto em várias partes e a resolução é feita por meio de consultas recursivas das partes. Assim, o servidor referenciado como nome totalmente qualificado xyz.com.br tem a seguinte árvore, com seus respectivos serviços de nome associados e distribuídos: . (ns) br (ns) com (ns) xyz (ns) Em uma nomeação hierárquica estruturada como o serviço de resolução de nomes (DNS), podemos resolver os nomes de duas maneiras. FORMA ITERATIVA javascript:void(0) O servidor responderá somente com o que sabe: o nome do próximo servidor a ser buscado, e o cliente procura iterativamente os outros servidores. FORMA RECURSIVA O servidor passa o resultado para o próximo servidor que encontrar, e ao cliente restará somente receber a mensagem de endereço de nome encontrado ou não encontrado. A principal desvantagem no método recursivo é uma maior exigência de desempenho aos servidores de nomes, pois estes precisam manipular a resolução completa de um caminho. No entanto, tem duas grandes vantagens: Armazenar temporariamente os resultados intermediários, diminuindo o custo de comunicação. Cada servidor de nomes no caminho aprende gradativamente o endereço de cada servidor de nomes de nível mais baixo. O DNS é, portanto, o sistema de resolução de nomes da Internet e base para grande parte dos sistemas distribuídos. De forma hierárquica, ele gera uma base de dados distribuída que permite localizar rapidamente qualquer entidade mapeada para a Internet ou dentro de uma rede local em uma visão de intranet. Sua implementação é feita no que chamamosde zonas, que não se sobrepõem. Cada zona é construída em um servidor de nomes, que possui autoridade por um pedaço do espaço de nome por ele gerenciado e pode ser replicado, garantindo maior disponibilidade nos serviços prestados. javascript:void(0) Resolução de nomes. NOMEAÇÃO EM ATRIBUTO Nesta abordagem, são fornecidas descrições a partir de termos de pares (atributo, valor), conhecidos como serviços de diretórios. Com estes, as entidades têm um conjunto de atributos associados que podem ser usados para procurá-las. Consultar valores em um sistema de nomeação baseado em atributos requer uma busca exaustiva nos descritores, o que pode acarretar sérios problemas de desempenho e escalabilidade. Como exemplo prático, temos implementações hierárquicas − como o LDAP (Protocolo Leve de Acesso a Diretório) − e implementações descentralizadas − as tabelas Hash distribuídas (DHT). A abordagem de implementações hierárquicas combina nomeação estruturada com nomeação baseada em atributos amplamente adotada, como no Active Directory da Microsoft ou no ambiente de código aberto com o OpenLDAP. O serviço de diretório LDAP é organizado na forma de registros, conhecidos como entrada de diretório. Nela, cada registro é composto por um conjunto de pares (atributo, valor), nos quais cada atributo possui um tipo associado e os valores podem ter valor único ou serem multivalorados. EXEMPLO Os atributos: C = Country (País), O = Organization (Organização), OU = Organization Unit (Unidade Organizacional). Populamos, então, com os atributos: uid=professor, C=br, O=xyz, OU=centro. A implementação de um serviço de diretório LDAP é semelhante à implementação do serviço de nomeação do DNS. Diretórios de grande escala particionam as árvores de diretório, distribuindo-as em vários servidores, equivalentes às zonas em DNS, mas com a diferença de suportar um número bem maior de valores para consultas, predefinidas em uma estrutura denominada esquemas. DISTRIBUTED HASH TABLES (DHT) As tabelas de Hash distribuídos são uma classe de sistemas distribuídos descentralizados que provém um serviço de busca parecida com a de uma tabela de Hash: pares (atributo, valor) são armazenados no DHT, e qualquer nó pode recuperar o valor associado a cada atributo. A responsabilidade por manter o mapa dos atributos até os valores é distribuída entre os nós de modo que uma mudança no grupo de nós cause uma interrupção mínima. Isso permite que o DHT funcione para uma grande escala de nós, com estes chegando, saindo e falhando continuamente, possibilitando uma busca eficaz, em vez de executar uma busca excessiva por toda a extensão do espaço de atributos. COMENTÁRIO Um problema do DHT, porém, é que ele só realiza buscas pelo nome exato do arquivo, no lugar de usar palavras-chave. Essa solução é muito utilizada para nomeação em sistemas peer-to- peer. Nesse ambiente, existe uma descentralização, não há uma entidade de coordenação central. O ambiente é altamente escalável, estando preparado para funcionar corretamente tanto com milhares de nós como para milhões de nós sem que haja uma queda na qualidade do serviço oferecido. É totalmente tolerante a falhas, e a entrada, a saída e a falha de peers não devem abalar o sistema, mesmo que essas ocorram frequentemente. Para prover essas características, uma técnica usada é fazer com que cada nó coordene apenas uma quantidade pequena de outros nós, quando comparada com a quantidade total na DHT. Dessa forma, é necessário realizar somente uma quantidade limitada de trabalho para cada mudança. São exemplos dessa estrutura: BitTorrent, eMule, YaCy, dentre muitos outros. NOMEAÇÃO No vídeo a seguir, apresentamos um resumo do módulo explicando sobre os sistemas de nomeação e suas técnicas. VERIFICANDO O APRENDIZADO MÓDULO 3 Analisar os conceitos de consistência e replicação nos sistemas distribuídos SINCRONIZAÇÃO DE RELÓGIOS A sincronização entre processos locais é a base de entendimento para a comunicação entre processos em sistemas distribuídos, de tal forma que devem responder a simples perguntas: Como as regiões críticas são implementadas em um sistema distribuído? Como esses recursos são alocados? Como é gerenciado o acesso concorrente? Como sabemos que os eventos estão de acordo com uma linha do tempo? Em um sistema local, as regiões críticas, a exclusão mútua e demais problemas de sincronização são resolvidos, na maioria das vezes, por métodos de semáforos e monitores. Obviamente, porém, estes não são recomendados para serem usados em sistemas distribuídos, porque eles “esperam” a existência de uma memória compartilhada. Em um sistema centralizado, o tempo não é ambíguo, então, quando um determinado processo consulta a hora, ele faz uma chamada ao sistema, que responde. EXEMPLO Se um processo A perguntar a hora e, um pouco mais tarde, um processo B fizer o mesmo, o valor obtido por B será maior do que o obtido por A. Entretanto, em um sistema distribuído, conseguir acordo nos horários não é trivial. RELÓGIOS LÓGICOS Os hosts possuem circuitos internos para gerenciar um relógio físico interno, composto por um cristal de quartzo que, sob pressão, oscila em uma frequência definida, gerando o correto batimento para o funcionamento do ambiente. COMENTÁRIO Todos os processos da máquina utilizam esse mesmo relógio, mas, com diversos servidores espalhados, é praticamente impossível manter esses cristais funcionando na mesma frequência. Esses relógios podem ficar defasados uns com os outros, mas como sincronizá-los para que estejam de acordo com o tempo real? Para isso, Cristian (1989) propôs usar um modelo cliente/servidor no qual clientes consultam a hora em um servidor de tempo de precisão. Em sistemas distribuídos, não se pode determinar com precisão o delay (atraso) de transmissão da rede. EXEMPLO Um cliente desejando sincronizar−se com um servidor envia−lhe uma requisição, e este responde devolvendo o tempo atual de seu clock. O problema é que, quando o cliente recebe de volta a mensagem, o tempo retornado já está desatualizado. É claro que a latência de uma Wan poderá fornecer uma hora desatualizada. Criou-se, então, o protocolo de tempo de rede (NTP – Network Time Protocol), que se ajusta em um modelo de pares de servidores no qual um consulta o outro para entregar uma maior precisão de relógio. Normalmente, um relógio de referência atômico, stratum 0, entregará essa precisão. COMO FUNCIONA O PROTOCOLO NTP? O servidor NTP consulta seus pares de tempo em tempo, perguntando a hora que cada um está registrando, e, com base nos valores retornados, calcula um horário médio e informa para todos os participantes adiantar ou atrasar seus relógios. Lamport (1978) mostrou que, embora a sincronização de relógios seja possível, não precisa ser absoluta, pois, se dois processos não interagem entre si, seus relógios não precisam ser sincronizados. Assim, não é necessário que todos os processos concordem com a hora exata, mas com a ordem em que os eventos ocorrem. SOLUÇÕES DE PROBLEMAS DE SINCRONIZAÇÃO EM SISTEMAS DISTRIBUÍDOS ALGORITMOS CENTRALIZADOS O caminho mais rápido para atingir exclusão mútua em sistemas distribuídos é similar ao feito em sistema com um único processador. Um processo é eleito como o coordenador (por exemplo, um rodando na máquina com o maior endereço da rede ou escolhido por algum algoritmo de eleição). Sempre que um processo quiser entrar na região crítica, ele envia uma requisição de mensagem para o coordenador, declarando qual região ele quer entrar e requisitando permissão. Se nenhum outro processo estiver atualmente naquela região crítica, o coordenador envia uma resposta de volta, dando permissão. Quando a resposta chega, o processo de requisição entra na região crítica. Quando o processo 1 sai dessa região, ele envia uma mensagem para o coordenador, liberando o acesso exclusivo. O coordenador toma as requisições da fila e envia ao processo uma mensagemde permissão. Se o processo ainda estiver bloqueado (por exemplo, é a primeira mensagem para ele), ele é desbloqueado e entra na região crítica. Esse algoritmo garante exclusão mútua: o coordenador somente deixa um processo, por tempo, entrar na região crítica. É também justo, já que as concessões às requisições são feitas na ordem na qual são recebidas. Nenhum processo espera para sempre. Além disso, o esquema é fácil de implementar, e requer somente três mensagens para uso da região crítica (requisição, concessão, liberação). Pode também ser usado para alocação mais geral dos recursos, em vez de somente gerenciar regiões críticas. COMENTÁRIO A abordagem centralizada também possui problemas. O coordenador é um único ponto de falha, logo, se quebrar, todo o sistema pode parar. Se os processos normalmente bloquearem depois de uma requisição, eles não poderão distinguir um coordenador "morto" de uma "permissão negada", já que em ambos nenhuma mensagem retorna. Em adição, em sistemas grandes, um único coordenador pode tornar um gargalo da performance. ALGORITMOS DISTRIBUÍDOS Como ter um único ponto de falha é um problema, os pesquisadores têm estudado alguns algoritmos para garantir a exclusão mútua distribuída. O algoritmo de Ricart e Agrawala requer que haja uma ordem total de todos os eventos no sistema. Ou seja, para qualquer par de eventos, como mensagens, não deve haver ambiguidade sobre qual é o primeiro. O algoritmo de Lamport, apresentado anteriormente, é um modo de alcançar essa ordenação, e pode ser usado para fornecer um timestamp para a exclusão mútua distribuída. Quando um processo quer entrar na região crítica, ele constrói uma mensagem contendo o nome da região de interesse, o número do processo e o tempo corrente. Envia, então, a mensagem para todos os outros processos, conceitualmente incluindo-o também. Um grupo confiável de comunicação, se disponível, pode substituir as mensagens individuais (multicast em vez de unicast). Quando um processo recebe uma requisição de outro, a ação que se executa depende do seu estado com relação à região crítica nomeada na mensagem. Três casos devem ser distinguidos: Se o receptor não é a região crítica e não quer entrar nesta, ele retorna um OK (ACK) para o transmissor. Se o receptor estiver na região crítica, ele não responderá. Simplesmente enfileira a requisição. Se o receptor quiser entrar na região crítica, mas ainda não o fez, ele compara o timestamp da mensagem com o conteúdo da mensagem que enviou para todos. A menor ganha. Se a mensagem que chegar tiver menor prioridade, o receptor envia uma mensagem de OK. Se sua própria mensagem tiver um timestamp inferior, o receptor enfileira a mensagem e não enviará nada. Depois de enviar as requisições pedindo a devida autorização para entrar na região crítica, um processo vai esperar até que todos tenham dado as devidas permissões. Assim que todas as permissões cheguem, ele poderá entrar na região crítica. Quando sair da região crítica, enviará um OK para todos os processos da sua fila e os deletará de sua fila. Se não houver um conflito, o mesmo funcionará como esperado. ALGORITMO EM ANEL Veremos, agora, uma abordagem totalmente diferente para obter a exclusão mútua em um sistema distribuído. Um anel circular é construído, e cada processo estará associado a uma posição no anel. Essas posições podem ser alocadas em uma ordem numérica dos endereços da rede ou de qualquer outra forma. Não importa qual é a ordem, importa somente que cada processo saiba que é o próximo dentro do anel. Quando o anel é inicializado, o processo recebe um token, que circula pelo anel. Quando um processo adquire o token do processo vizinho, é feita uma verificação para entrar na região crítica. Se precisar, o processo entra na região crítica, efetua o trabalho a ser feito e libera a região. Depois de sair, ele passa o token para o controle do anel. Não é permitido entrar em uma segunda região crítica usando o mesmo token. Se o processo recebe o token de seu vizinho e não está interessado em entrar na região crítica, ele somente o passa adiante. Como consequência, quando nenhum processo quer entrar em nenhuma região crítica, o token somente circula em alta velocidade dentro do anel. Desse modo, somente um processo que possuir o token poderá estar na região crítica. Visto que o token circula pelos processos em uma ordem bem definida, um starvation não poderá ocorrer. Uma vez que um processo decide entrar na região crítica, no pior caso, ele terá que esperar que todos os outros processos entrem na região crítica e liberem−na. COMENTÁRIO Como grande desvantagem, se o token for perdido, o mesmo deverá ser gerado novamente. Mas detectar essa perda é difícil, visto que o tempo entre sucessivas aparições do token da rede não é limitado. O fato de ele não ter sido avistado por uma hora não significa que ele foi perdido; alguém pode ainda estar utilizando-o. O algoritmo também tem problemas no caso de uma falha do processo, mas a sua recuperação é mais fácil do que nas outras situações. Um processo, ao passar o token para seu vizinho, pode perceber que ele está fora do ar. Nesse ponto, o processo fora do ar poderá ser removido do grupo, e o que possui o token enviará para o próximo processo, no anel. Para que isso aconteça, se faz necessário que todos mantenham a configuração atual do anel. ALGORITMO DE DETECÇÃO DE DEADLOCKS DISTRIBUÍDOS Um deadlock (impasse) é causado por uma situação na qual um conjunto de processos estará bloqueado permanentemente. Isto é, não conseguem prosseguir com sua execução, esperando um evento externo promovido por outro processo para sair da situação de bloqueio permanente. Um deadlock ocorre devido aos processos de alocação formarem um ciclo, no qual vários processos tentam alocar recursos para realizar suas tarefas, mas foram bloqueados por outros, que não liberam os seus para deixar que os outros sigam suas execuções. Assim, não terão todos os recursos disponíveis por não terem terminado suas tarefas. Algumas estratégias são adotadas para o tratamento de deadlock, evitando a ocorrência por meio de uma alocação de cuidados dos recursos, prevenindo de forma estática que os deadlocks não ocorram e permitindo que o deadlock ocorra, entretanto, que seja possível detectar e tentar se recuperar da situação. Podemos usar um algoritmo centralizado, que examina o estado do sistema para determinar se existe um deadlock e, nesse caso, executa uma ação corretiva. Em sistemas distribuídos, coletar e colecionar essas informações sobre necessidade/oferta de recursos é extremamente difícil e complicado de gerenciar. Mas é possível manter essas informações sobre alocação de recursos, criando um grafo de alocação de recursos e procurando ciclos nesse grafo. ATENÇÃO Caso o coordenador detecte um ciclo, ele matará um dos processos, acabando com o deadlock. ALGORITMO DE ELEIÇÃO Em sistemas distribuídos, diversos algoritmos necessitam que um processo exerça uma função especial, como coordenador, inicializador ou sequenciador. São exemplos: Coordenador de exclusão mútua com controle centralizado. Coordenador para detecção de deadlock distribuído. Sequenciador de eventos para ordenação consistente centralizada. A falha do coordenador compromete o serviço para vários processos, portanto, um novo coordenador deve assumir. Para isso, fazemos uma eleição. O objetivo é eleger um processo, entre os ativos, para desempenhar função especial. Para tal, existem algoritmos especiais para escolha de um processo que assumirá a posição de coordenador. Podemos citar os algoritmos bully (valentão) e o ring (anel). Para esses algoritmos, assume-se as seguintes premissas: Todo o processo no sistema tem prioridade única. Quando a eleição acontece, o processo com maior prioridade entre os processos ativos é eleito como coordenador. Na recuperação (volta à atividade), um processo falho pode tomar ações para juntar-se aogrupo de processos ativos. ALGORITMO DE BULLY Quando um processo nota que o coordenador não responde, uma eleição deverá ser iniciada. 1 Esse processo P envia uma mensagem de eleição para todos os processos com número maior que o seu. Se não houver nenhuma resposta, P ganha a eleição e se torna o coordenador. 2 3 Se alguém responde, a tarefa de P estará finalizada, e a eleição poderá continuar a partir dos processos com maior número. Quando a eleição finaliza, o vencedor enviará uma mensagem para todos os processos informando que será o novo coordenador. ALGORITMO DE RING (ANEL) Nesse algoritmo, os processos podem ser fisicamente ou logicamente ordenados. Eles sabem que são seus sucessores e, quando um processo nota que o coordenador não responde, constrói uma mensagem de eleição, inclui seu número e envia para seu sucessor. A mensagem circula pelo anel, e cada processo inclui seu número iniciador da eleição, recebe a mensagem de volta e determina quem é o coordenador baseado em qual processo possui o maior número. Após isso, envia a mensagem coordenador pelo anel. SINCRONIZAÇÃO EM SISTEMAS DISTRIBUÍDOS Os algoritmos para sincronização em sistemas distribuídos são apresentados no vídeo a seguir. REPLICAÇÃO Em nosso dia a dia, o dado, que é considerado o novo petróleo por muitos especialistas, é um ativo fundamental para tomadas de decisões de empresas e instituições. Portanto, ter os dados replicados é de suma importância, pois garante suas duas razões principais: confiabilidade e desempenho. Além disso, de forma embutida, garante a disponibilidade desses dados. Seguindo o conceito da disponibilidade, conseguimos manter os sistemas operacionais mesmo na queda de uma das réplicas, pois pode-se redirecionar o acesso a qualquer uma das suas cópias disponíveis, além de garantir uma melhor proteção contra dados corrompidos. O desempenho também pode ser alcançado, uma vez que é possível ter várias cópias espalhadas, e um redirecionamento de acesso pode permitir que a carga de trabalho seja balanceada entre suas várias réplicas. COMENTÁRIO Ter muitas cópias pode levar a problemas de consistência. Sempre que houver uma atualização, será necessário modificar todas as demais cópias para garantir a consistência dos dados, sendo esse o maior problema em projetos que envolvam esses conceitos. Usar uma memória local temporária para os dados acelera o desempenho e entrega um melhor “sentimento” de velocidade aos seus utilizadores, mas gera sempre uma informação desatualizada. Chegamos novamente, então, ao impasse desempenho versus escalabilidade, em que manter várias cópias pode significar problemas de escalabilidade. Assim, como um conjunto de cópias consistentes são sempre iguais, uma operação de leitura sempre retornará o mesmo valor. Mas, em uma operação de escrita, a atualização será propagada para todas as cópias antes que ocorra outra operação subsequente, independentemente de em qual réplica for realizada a operação. Chamamos de replicação síncrona ou consistência estrita a existência de uma atualização realizada de forma atômica. No entanto, teremos dificuldade em garantir que todas as réplicas estejam sincronizadas globalmente. Isso porque implementar métodos síncronos pode ser muito caro, na visão do desempenho distribuído, para as aplicações sensíveis a latência. COMENTÁRIO Em muitos casos, então, a melhor solução poderá ser o relaxamento das restrições de consistência estrita, ganhando maior desempenho em situações nas quais a estratégia a ser seguida seria que as cópias nem sempre seriam iguais em todos os lugares. Dessa forma, as aplicações deveriam estar preparadas para receber informações um pouco desatualizadas. Atualmente, cada vez mais vemos soluções de banco de dados NoSQL, bancos de dados distribuídos e replicados de alta escala — nos quais estão sendo aplicados os conceitos de consistência eventual, pois nem sempre podemos fazer uso de uma consistência estrita (síncrona) —, que toleram um alto grau de inconsistência. Isso porque existem situações que executam muito mais operações de leitura do que de escrita. Portanto, a questão aqui é a velocidade com que as atualizações devem ser disponibilizadas para os processos que realizam somente leitura. Se nenhuma atualização ocorrer por um tempo bastante longo, todas as réplicas ficarão gradativamente consistentes. Ou seja, na ausência de atualizações, todas as réplicas convergem em direção a cópias idênticas umas das outras, e temos a garantia de que as atualizações serão propagadas para todas as réplicas no tempo que for programado para tal. VERIFICANDO O APRENDIZADO MÓDULO 4 Identificar as principais interfaces de desenvolvimento de códigos para sistemas paralelos e distribuídos INTERFACES DE PROGRAMAÇÃO DISTRIBUÍDA Com a explosão da Internet, as aplicações tiveram que se adaptar a uma nova realidade de comunicação. Surgiu a necessidade de que elas parassem de executar em um ambiente fechado e proporcionassem uma distribuição de conteúdo entre elas mesmas. Para proporcionar que essa distribuição ou comunicação ocorra entre as aplicações, estas devem ser compostas em camadas, ou seja, devem se tornar aplicações distribuídas. COMENTÁRIO Criar uma aplicação distribuída é uma tarefa muito importante nos dias atuais. Isso porque, com a facilidade de acesso promovida com a difusão da Internet, as aplicações precisam, além de se comunicarem entre si, ter a maior disponibilidade e confiabilidade possível. Existem APIs que funcionam em sistemas operacionais para facilitar as tarefas de comunicação e de processamento, portanto, não precisam da utilização da Internet. Neste módulo, exploraremos algumas dessas APIs, tais como sockets, MPI, RPC e barramento, com alguns exemplos de implementação em linguagem C. PROGRAMAÇÃO EM SOCKETS O termo socket refere-se à Interface de Programação de Aplicações (API) implementada pelo grupo de distribuição de software UNIX da Universidade de Berkeley (BSD). A interface fornecida pelo socket permite que os processos acessem serviços de rede. A troca de mensagens é feita entre dois processos, usando vários mecanismos de transporte. A comunicação via sockets deve ser realizada sempre dentro de um domínio, que é caracterizado por uma abstração feita para designar os serviços de rede, uma estrutura de endereçamento comum e protocolos entre dois pontos de comunicação em uma rede. Dessa forma, processos que são executados em sistemas diferentes podem se comunicar por meio dos sockets somente se utilizarem o mesmo esquema de endereçamento. Em TCPI/IP, os sockets UDP e TCP são a interface provida pelos respectivos protocolos na interface da camada de transporte. Estrutura de comunicação em sockets. Como vimos, os sockets foram criados na forma de uma API que possibilita aplicações/processos se comunicarem, mas quais são essas interfaces que permitem essas comunicações? A seguir, listamos algumas das principais funções utilizadas ao criar um programa utilizando sockets. getaddrinfo() Traduz nomes para endereços socket() Cria um socket e retorna o descritor de arquivo bind() Associa o socket a um endereço socket e a uma porta connect() Tenta estabelecer uma conexão com um socket listen() Coloca o socket para aguardar conexões accept() Aceita uma nova conexão e cria um socket send() Caso conectado, transmite mensagens ao socket recv() Recebe as mensagens por meio do socket close() Desaloca o descritor de arquivo shutdown() Desabilita a comunicação do socket Quando se programa utilizando sockets, uma arquitetura muito empregada é a cliente/servidor (client/server). Para isso, temos que implementar um programa cliente e um programa servidor. Ambos fazem uso da mesma API de sockets. Os sockets UDP são basicamente canais não confiáveis, pois: Não garantem entrega dos datagramas; Podem entregar datagramas duplicados; Não garantem ordem de entrega dos datagramas; Não têm estado de conexão (escuta,estabelecida). Para programar em sockets UDP, utilizamos as seguintes funções: Para criar o socket. DatagramSocket s = new DatagramSocket(6789); Para receber um datagrama. receive(req) Para enviar um datagrama. send(resp); Para fechar um socket. close(); Para montar um datagrama para receber mensagem. new DatagramPacket(buffer, buffer.length); Para montar um datagrama para ser enviado. new DatagramPacket(msg, msg.length, inet, porta); Buffer e msg são byte[ ]. Como podemos observar, nesse modelo o send não é bloqueante, mas o receive é, a menos que possamos especificar um timeout. Em uma visão multithreads, teremos um thread principal que receberá as requisições e outras threads servindo aos respectivos clientes. Dentro da camada de transporte da pilha TCP/IP, temos o protocolo TCP, que implementa uma comunicação confiável. Na visão do desenvolvedor, é visto como um fluxo contínuo (stream), e seus dados são tratados como segmentos. Existe total garantia de entrega, ordenamento das mensagens e não ocorrência de duplicação. Estabelece uma conexão e, portanto, controla o estado da conexão (estabelece, escuta e fecha sessões). Possui, ainda, outras características integradas, como sessão ponto a ponto (um transmissor: um receptor), ou seja, sockets conectados e controle de congestionamento e de fluxo, para que o transmissor não sobrecarregue o receptor. Ainda dentro desse contexto, um stream é tratado como uma sequência de bytes transmitida e recebida continuamente por um processo. O TCP preocupa-se em segmentar o stream e, se necessário, entregar os segmentos à aplicação na ordem correta. Do ponto de vista do desenvolvedor de sockets TCP, basta gravar os dados em um buffer de saída para que sejam enviados e ler os dados de chegada em um buffer de entrada. Para programar em sockets TCP, utilizamos as seguintes funções: Servidor cria socket de escuta em uma porta (Ex.: 6789). ServerSocket ss = new ServerSocket(6789); Servidor aceita uma conexão e cria um novo socket para atendê-la. Socket a = ss.accept(); Cliente cria socket de conexão. Socket s = new Socket(“localhost”, 6789); Fecha o socket. close(); LADO CLIENTE O programa cliente primeiro cria um socket por meio da função "socket()". Em seguida, conecta-se ao servidor por meio da função "connect()" e inicia um loop (laço), que fica fazendo "send()" (envio) e "recv()" (recebimento) com as mensagens específicas da aplicação. É no par send/recv que temos a comunicação lógica. Quando alguma mensagem da aplicação diz que é o momento de terminar a conexão, o programa chama a função "close()" para finalizar o socket. LADO SERVIDOR O programa servidor também utiliza a mesma API de sockets. Ou seja, inicialmente ele também cria um socket. No entanto, diferentemente do cliente, o servidor precisa fazer um "bind()", que associa o socket a uma porta do sistema operacional, e depois utilizar o "listen()" para escutar novas conexões de clientes nessa porta. Quando um novo cliente faz uma nova conexão, a chamada "accept()" é utilizada para começar a se comunicar. Da mesma forma que no cliente, o servidor fica em um loop, recebendo e enviando mensagens por meio do par de funções "send()" e "recv()". Quando a comunicação com o cliente termina, o servidor volta a aguardar novas conexões de clientes. Estrutura de sockets cliente/servidor. EXEMPLO DE IMPLEMENTAÇÃO DE UM CÓDIGO PARA O LADO CLIENTE A seguir, apresentamos um exemplo de código em Python utilizando socket para o lado servidor. from socket import * s = socket(AF_INET, SOCK_STREAM) s.bind ((HOST, PORT)) s.listen(1) (conn, addr) = s.accept # Retorna novo socket e endereço client while True: data = conn,recv(1024) # Recebe os dados do cliente if not data: break # Para se o cliente para conn.send(str(data)+”*” # Retorna enviando os dados + um “*” conn.close() # Fecha a conexão Exemplo de um código em C para o lado servidor de um socket para ambiente Linux: #include <stdio.h> #include <stdlib.h> #include <sys/socket.h> #include <arpa/inet.h> #define NRCON 5 /* Numero de conexoes */ #define BUFFSIZE 32 #define PORTA 5001 void TrataCliente(int sock) { char buffer[BUFFSIZE]; int recebido = -1; /* Receive message */ if ((recebido = read(sock, buffer, BUFFSIZE)) < 0) { perror("erro no recebimento dos dados"); exit(-1); } printf("Dados recebidos %s \n", buffer); /* Send bytes and check for more incoming data in loop */ while (recebido > 0) { /* Send back received data */ if (write(sock, buffer, recebido, 0) != recebido) { perror("erro no envio dos dados"); exit(-1); } /* Check for more data */ if ((recebido = read(sock, buffer, BUFFSIZE)) < 0) { perror("erro no recebimento dos dados"); exit(-1); } printf("Dados recebidos %s \n", buffer); } close(sock); } int main(void) { int ssocket, csocket; struct sockaddr_in end_servidor, end_cliente; if ((ssocket = socket(PF_INET, SOCK_STREAM, 0)) < 0) { perror("Erro criação socket"); exit(-1); } /* Construct the server sockaddr_in structure */ memset(&end_servidor, 0, sizeof(end_servidor)); /* Clear struct */ end_servidor.sin_family = AF_INET; /* Internet/IP */ end_servidor.sin_addr.s_addr = htonl(INADDR_ANY); /* Incoming addr */ end_servidor.sin_port = htons(PORTA); /* server port */ /* Bind the server socket */ if (bind(ssocket, (struct sockaddr *) &end_servidor, sizeof(end_servidor)) < 0) { perror("erro bind"); exit(-1); } /* Listen on the server socket */ if (listen(ssocket, NRCON) < 0) { perror("erro listen"); exit(-1); } /* Run until cancelled */ while (1) { unsigned int clientlen = sizeof(end_cliente); /* Wait for client connection */ if ((csocket = accept(ssocket, (struct sockaddr *) &end_cliente, &clientlen)) < 0) { perror("erro accept"); exit(-1); } fprintf(stdout, "Cliente conectado: %s\n", inet_ntoa(end_cliente.sin_addr)); TrataCliente(csocket); } exit(0); } Exemplo de implementação de um código para o lado cliente A seguir, apresentamos um exemplo de código em Python utilizando socket para o lado servidor: from socket import * s = socket(AF_INET, SOCK_STREAM) s.connect((HOST, PORT)) # conecta ao servidor (bloqueia até aceitar) s.send('Ola, Mundo') # enviando os dados data = s.recv(1024) # recebe a resposta print data # mostra o resultado s.close() # fecha a sessão Exemplo de um código em C para o lado cliente de um socket para ambientes Linux: #include <stdio.h> #include <sys/socket.h> #include <arpa/inet.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <netinet/in.h> #define BUFFSIZE 32 int main(int argc, char *argv[]) { int csocket; struct sockaddr_in end_servidor; char buffer[BUFFSIZE]; char mensagem[15] = "Ola Mundo"; unsigned int comprimento; int recebido = 0; if (argc != 3) { fprintf(stderr, "Utilize: Cliente <server_ip> <port>\n"); exit(1); } /* Create the TCP socket */ if ((csocket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) { perror("Erro na criacao do socket"); exit(-1); } /* Construct the server sockaddr_in structure */ memset(&end_servidor, 0, sizeof(end_servidor)); /* Clear struct */ end_servidor.sin_family = AF_INET; /* Internet/IP */ end_servidor.sin_addr.s_addr = inet_addr(argv[1]); /* IP address */ end_servidor.sin_port = htons(atoi(argv[2])); /* server port */ /* Establish connection */ if (connect(csocket, (struct sockaddr *) &end_servidor, sizeof(end_servidor)) < 0) { perror("erro no conect"); exit(-1); } /* Send the word to the server */ comprimento = strlen(mensagem); if (write(csocket, mensagem, comprimento) != comprimento) { perror("erro no envio de dados"); exit(-1);} /* Receive the word back from the server */ fprintf(stdout, "Recebido: "); while (recebido < comprimento) { int bytes = 0; if ((bytes = read(csocket, buffer, BUFFSIZE - 1)) < 1) { perror("erro no recebimento dos dados"); exit(-1); } recebido += bytes; buffer[bytes] = '\0'; /* Assure null terminated string */ fprintf(stdout, buffer); } fprintf(stdout, "\n"); close(csocket); exit(0); } PROGRAMAÇÃO PARALELA POR INTERFACE DE PASSAGEM DE MENSAGENS (MPI) O conceito de programação paralela está diretamente relacionado ao uso de computadores paralelos − dotados de várias unidades de processamento, com capacidade de executar programas paralelamente. No caso de programas paralelos, temos instruções sendo executadas em diferentes processadores ao mesmo tempo. Além disso, há geração e troca de informação entre os processadores. O objetivo da programação paralela é transformar grandes algoritmos complexos em pequenas tarefas, que possam ser executadas simultaneamente por vários processadores, reduzindo, assim, o tempo de processamento. Mas isso demanda tempo para analisar o código a ser paralelizado, recodificar a aplicação, depurar e esperar para se encontrar a melhor solução para o problema. O paralelismo pode ser explorado de duas formas. No modo implícito, existe a exploração automática do paralelismo, na qual o compilador gera as operações paralelas. São exemplos: HPF (High Performance Fortran) e, parcialmente, alguns compiladores da Intel. No paralelismo explícito, é dever do programador o processo de paralelizar o seu código, usando APIs como MPI, PVM ou diretivas com o OpenMP. As sincronizações dos processos geram muitos problemas ao programador, pois as tarefas executadas em paralelo aguardam a finalização mútua para, só então, coordenar os resultados ou trocar dados e reiniciar novas tarefas. ATENÇÃO É necessário que haja uma perfeita coordenação do tamanho dos processos (granulosidade) e da comunicação entre eles para que o tráfego gerado não seja maior do que o processamento, e que, consequentemente, haja uma queda no desempenho dos processos em execução. O MPI é uma biblioteca com funções (API) para troca de mensagens, responsável pela comunicação e sincronização de processos em um cluster paralelo. Dessa forma, os processos de um programa paralelo podem ser escritos em uma linguagem de programação sequencial, tal como C, Python ou Fortran. O principal foco do MPI é disponibilizar uma interface que seja largamente utilizada no desenvolvimento de programas que utilizem troca de mensagens. Além de garantir a portabilidade dos programas paralelos, essa interface deve ser implementada eficientemente nos diversos tipos de máquinas paralelas existentes, sejam elas supercomputadores dedicados ou por meio de clusters de computadores do mercado. O objetivo principal é que o MPI se torne o padrão de interface mais utilizado em sistemas distribuídos, no qual cada processador executa um processo paralelo seguindo o modelo SPMD (Single Program Multiple Data – Único Programa sobre Múltiplos Dados). Isso porque o MPI define um conjunto de rotinas para facilitar a comunicação (troca de dados e sincronização) entre processos em memória. Ele é portável para qualquer arquitetura, tem aproximadamente 300 funções para programação e ferramentas para análise de desempenho. COMENTÁRIO A biblioteca MPI possui rotinas para programas em linguagem C / C++, Python, Fortran 77/90 e muitas outras linguagens de programação. Os programas são compilados e ligados à biblioteca MPI. Inicialmente, na maioria das implementações, um conjunto fixo de processos é criado. Os elementos mais importantes em implementações paralelas são: A comunicação de dados entre processos paralelos. O balanceamento da carga. Os processos podem usar mecanismos de comunicação ponto a ponto em operações para enviar mensagens de um determinado processo a outro. Um grupo de processos pode chamar operações coletivas de comunicação para executar operações globais. O MPI é capaz de suportar comunicação assíncrona e programação modular por meio de mecanismos de comunicadores que permitem ao usuário MPI definir módulos que englobem estruturas de comunicação interna. Cada processo executa e comunica-se com outras instâncias do programa, possibilitando a execução no mesmo processador ou em diferentes processadores. Essas instâncias podem se dar por meio de comunicação ponto a ponto ou coletiva. O melhor desempenho acontece quando esses processos são distribuídos entre diversos processadores. A comunicação básica consiste em enviar e receber dados de um processador para outro. Essa comunicação se dá por meio de uma rede de alta velocidade (como InfiniBand ou Ethernet 10G), na qual os processos estarão em um sistema de memória distribuída. O pacote de dados enviados pelo MPI requer vários pedaços de informações: O processo transmissor. O processo receptor. O endereço inicial de memória para onde os itens de dados deverão ser mandados. A mensagem de identificação. O grupo de processos que podem receber a mensagem. Ou seja, tudo isso ficará sob responsabilidade do programador em identificar o paralelismo e implementar um algoritmo utilizando construções com o MPI. O programa “Olá Mundo” (Hello World), a seguir, apresenta a estrutura básica de um programa MPI escrito em C: #include “mpi.h” #include <stdio.h> int main(int argc,char *argv[]) { int meu_id, numero_processos; // Rotinas de inicialização MPI_Init(&argc,&argv); MPI_Comm_size(MPI_COMM_WORLD, &numero_processos); MPI_Comm_rank(MPI_COMM_WORLD, &meu_id); fprintf(stdout,”Olá mundo! Sou o processo %i de %i criados”, meu_id, numero_processos); // Rotinas de finalização MPI_Finalize(); return 0; } A primeira linha desse programa inclui a biblioteca do MPI em C. O comando MPI_Init, com os argumentos passados pela linha de comando do mpirun, inicializa o ambiente MPI para esse processo. O comando MPI_Comm_size(MPI_COMM_WORLD, &numero_processos) retorna na variável "numero_processos" o número total de processos criados. O comando MPI_Comm_rank(MPI_COMM_WORLD, &meu_id) retorna na variável "meu_id" a identificação única desse processo, dentre os n processos criados. ATENÇÃO É importante destacar que os valores retornados variam de 0 até o número total de processos –1. O parâmetro MPI_COMM_WORLD, usado por esses comandos, identifica um objeto local, que representa o contexto de uma comunicação. Processos podem se comunicar, desde que estejam no mesmo grupo de comunicação. O comando MPI_Finalize() encerra o ambiente MPI para esse processo. Nesse exemplo, cada um desses n processos criados executará o comando fprintf. Consequentemente, n mensagens serão impressas. A identificação do processo usado nesse comando serve para diferenciar qual processo escreveu qual mensagem. Exemplo de código MPI bloqueante Nesse exemplo, é explorada a comunicação síncrona, na qual o processador que envia a mensagem aguarda um sinal do destinatário confirmando o recebimento da mesma. # include <mpi .h> # include <stdio .h> # define ROOT 0 # define MSGLEN 100 int main (int argc , char ** argv ) { int i, rank , size , nlen , tag = 999; char name [ MPI_MAX_PROCESSOR_NAME ]; char recvmsg [ MSGLEN ], sendmsg [ MSGLEN ]; MPI_Status stats ; MPI_Init (& argc , & argv ); MPI_Comm_rank ( MPI_COMM_WORLD , & rank ); MPI_Comm_size ( MPI_COMM_WORLD , & size ); MPI_Get_processor_name (name , & nlen ); sprintf ( sendmsg , " Hello world ! I am process %d out of %d on %s\n", rank , size , name ); if ( rank == ROOT ) { printf (" Message from process %d: %s", ROOT , sendmsg ); for (i = 1; i < size ; ++i) { MPI_Recv (& recvmsg , MSGLEN , MPI_CHAR , i, tag , MPI_COMM_WORLD , & stats ); printf (" Message from process %d: %s", i, recvmsg ); } } else { MPI_Send (& sendmsg , MSGLEN , MPI_CHAR , ROOT ,
Compartilhar