• Nenhum resultado encontrado

método calculaMedia, que recebe como entrada o pid do processo e n_processos, lê a parte do arquivo atribuída àquele processo e devolve a média dos valores lidos. Após calcular a média, cada processo, exceto o 0, escreve o valor da média de sua parte do arquivo no vetor lista_de_medias, localizado na memória local do processo 0. Em seguida, na linha 22, os processos chamam bsp_sync, de modo a finalizar o superpasso e permitir que a BSPlib realize a escrita remota dos dados. Após o término da sincronização, o processo 0 lê os valores escritos pelos demais processos e realiza o cálculo da média dos valores contidos no arquivo.

Após o término do cálculo da média de todos os arquivos, os processo chamam bsp_end de modo a finalizar a execução da aplicação BSP.

2.3.2 Comunicação portável entre processos

Apesar de existirem diversas implementações do modelo BSP para diferentes arquiteturas, todas re- querem que os processos pertencentes a uma dada aplicação BSP devem executar em nós de mesma arquitetura. O problema para a comunicação em arquiteturas heterogêneas é que suas APIs não propor- cionam uma maneira de especificar o tipo de dado sendo transmitido. Os dados transmitidos são tratados como seqüências de bytes.

Para permitir a comunicação entre processos executando em máquinas de arquiteturas distintas, es- tendemos a API da BSPlib adicionando, a alguns métodos, um parâmetro extra que descreve o tipo de dado que está sendo passado a esta função. Mas para utilizar esta API estendida, o programador teria que modificar suas aplicações para que fizessem uso dessas novas funções. Para evitar este problema, utili- zamos um pré-compilador que automaticamente instrumenta o código fonte da aplicação. Informações sobre o tipo de dado passado para as funções são obtidas pelo pré-compilador, que as utiliza para inserir o novo parâmetro nos métodos modificados. Deste modo, uma aplicação escrita para a BSPlib original é automaticamente transformada para utilizar a versão estendida da BSPlib.

Uma limitação imposta por esta abordagem é que o programador deve evitar realizar conversões (casts) arbitrárias em dados que serão compartilhados com outros processos da aplicação. Isto porque a versão estendida da biblioteca BSPlib utiliza informações dos tipos de dados compartilhados no momento de realizar a conversão entre as representações destes dados nas diferentes arquiteturas.

2.4

Armazenamento de checkpoints

Grades oportunistas são tipicamente compostas por um conjunto de aglomerados de máquinas, cor- respondentes à organização física destas máquinas. Aplicações normalmente são executadas em um único aglomerado, mesmo no caso de aplicações paralelas. O mecanismo de checkpointing gera check-

gigabytes no caso de aplicações paralelas.

Poderíamos armazenar os checkpoints em um repositório dedicado daquele aglomerado, mas isto gera a necessidade de uma máquina reservada para esta tarefa. Um servidor central poderia ser um gargalo no momento em que os checkpoints locais de cada processo de uma aplicação paralela fossem transferidos simultaneamente, além de ser um ponto único de falha no sistema. O uso de um aglome- rado já conectado a um sistema de arquivos de rede, como o NFS, dispensa um servidor dedicado para checkpoints, mas sofre dos mesmos problemas de desempenho e tolerância a falhas.

Uma solução seria armazenar os checkpoints no próprios nós de processamento da grade. Mas a má- quina onde um processo é executado não pode ser utilizada para armazenar um checkpoint daquele pro- cesso de modo confiável, pois normalmente as falhas na execução do processo são causadas justamente porque a máquina onde ele executava ficou indisponível. Neste caso, o checkpoint ficaria inacessível para reiniciar a aplicação em outra máquina. Os checkpoints poderiam ser então armazenados em outros nós daquele aglomerado. Mas estes nós também não são dedicados e podem ficar indisponíveis a qualquer momento.

Deste modo, ao utilizarmos os nós de uma grade oportunista para o armazenamento de checkpoints, devemos utilizar alguma forma de redundância para garantir que os checkpoints possam ser recuperados. No caso de aplicações seqüenciais, uma estratégia simples é salvar uma única cópia do checkpoint, com cada checkpoint sendo armazenado num nó diferente. Assim, mesmo que o último checkpoint não esteja disponível, checkpoints anteriores podem ser utilizados.

A seguir, comparamos diversas estratégias para o armazenamento remoto de checkpoints em repo- sitórios não-dedicados. Estas estratégias são direcionadas a aplicações paralelas, de modo que cada

checkpoint global gerado é composto por diversos checkpoints locais. Para que um checkpoint global

possa ser utilizado, é necessário que todos os checkpoints locais estejam disponíveis.

2.4.1 Estratégias de armazenamento

Ao avaliar estratégias de armazenamento para checkpoints de aplicações paralelas, precisamos consi- derar diversas propriedades, como escalabilidade, custo computacional e tolerância a falhas. Outro ponto importante a ser considerado é decidir em quais repositórios os checkpoints devem ser armazenados. É possível distribuir os dados entre os nós executando a aplicação e em outros nós do aglomerado. Anali- samos diversas estratégias de armazenamento, como replicação e codificação de dados, com relação aos critérios acima.

Paridade de dados

Esta estratégia consiste em armazenar apenas uma cópia do checkpoint com a adição de informação de paridade [70]. Para calcular a paridade de um checkpoint, é preciso primeiro dividi-lo em m fragmen- tos e então calcular a paridade sobre estes fragmentos. Um checkpoint C de tamanho n é dividido em m

2.4 Armazenamento de checkpoints 23 fragmentos Ukde tamanho n/m:

C = (U0, U1, U2, ..., Um)

Uk= (uk0, uk1, ..., ukn/m), 0 ≤ k < m

Os elementos pi, 0 ≤ i < n/m do fragmento P , contendo a paridade dos demais fragmentos, são dados

por:

pi= (u0i ⊕ u1i ⊕ ... ⊕ umi ), 0 ≤ i < n/m,

onde ⊕ representa a operação de ou-exclusivo. Os fragmentos Uie P de cada processo são então distri-

buídos para o armazenamento em outros nós.

Utilizando a estratégia de paridade de dados, é possível reconstruir um fragmento arbitrário Ukcom-

binando o conteúdo dos demais fragmentos com a informação de paridade contida no fragmento P , através das equações:

Uk= (uk0, uk1, ..., ukn/m), 0 ≤ k < m

uki = (u0i ⊕ u1i ⊕ ... ⊕ uk−1i ⊕ pi⊕ uki ⊕ ... ⊕ umi ), 0 ≤ i < n/m

Se supormos que cada fragmento será armazenado em um nó diferente, vemos que esta estratégia provê tolerância à falha simultânea de um único nó. O tamanho do vetor de paridade P é inversamente proporcional ao número de fragmentos, de modo que a utilização de mais fragmentos requer menos espaço de armazenamento para os checkpoints. Por outro lado, por utilizar mais nós de armazenamento, é maior a probabilidade de ocorrer falhas simultâneas em dois ou mais nós. Além disso, é preciso criar mais conexões para realizar a transferência de arquivos.

Uma maneira bastante eficiente de implantar esta estratégia de armazenamento é utilizando os nós onde a aplicação paralela está sendo executada para armazenar os fragmentos dos checkpoints locais. Caso ocorra uma falha em um dos nós da aplicação, é possível reiniciá-la a partir da informação contida nos demais nós. Por outro lado, se uma segunda falha ocorrer, não será mais possível recuperar o estado da aplicação até que pelo menos uma das duas máquinas que falharam fique disponível novamente.

A vantagem de utilizar paridade, quando comparado a outras estratégias de armazenamento, é que seu cálculo é bastante eficiente, necessitando de apenas O(n) operações ou-exclusivo, onde n é o tamanho do vetor a ser codificado. Sua desvantagem é que a falha de dois ou mais nós contendo fragmentos do

checkpoint é o suficiente para tornar o checkpoint irrecuperável.

Algoritmo de dispersão de informação (IDA)

O algoritmo de dispersão de informação (IDA) [101] clássico, desenvolvido por Rabin, permite gerar uma codificação ótima dos dados com relação ao uso de memória. Usando IDA, é possível codificar um vetor U de tamanho n em m + k vetores codificados de tamanho n/m, com a propriedade de que o vetor

original U pode ser reconstruído utilizando apenas m vetores codificados. Utilizando esta codificação, é possível obter diferentes níveis de tolerância a falhas ajustando os valores de m e k. Na prática, é possível tolerar k falhas com uma sobrecarga de apenas k ∗ n/m elementos. Denominamos esta classe de codificação em fragmentos redundantes como códigos de “rasura” (erasure codes).

Este algoritmo requer o uso de operações matemáticas sobre um campo de Galois GF (q), um campo finito de q elementos, onde q é um número primo ou uma potência px de um número primo p. Quando

utilizamos q = px, operações aritméticas sobre o campo são realizadas representando os números como

polinômios de grau x − 1 e coeficientes em [0, p − 1]. Somas são calculadas utilizando operações de ou- exclusivo, enquanto multiplicações são calculadas multiplicando os polinômios módulo um polinômio irredutível de grau x.

O algoritmo requer a geração de m + k vetores linearmente independentes αide tamanho m. Estes

vetores podem ser facilmente obtidos escolhendo n valores distintos ai, 0 ≤ i < n e definindo αi =

(i, ai, ..., an−1i ), 0 ≤ i < n. Estes vetores são então organizados como uma matriz G definida como:

G = [αT0, αT1, . . . , αTm+k]

Dividimos então o vetor U em n/m palavras de informação Ui de tamanho m e geramos n/m palavras

codificadas Vide tamanho m + k, onde:

Vi= Ui× G

Finalmente, os m + k vetores codificados Ei, 0 ≤ i < m + k são definidos por:

Ei = (V0[i], V1[i], . . . , Vn/m[i])

Para recuperar as palavras de informação originais Ui, temos que recuperar m dos m + k fragmentos

codificados. Em seguida, construímos as palavras V′

j, que são equivalentes às palavras codificadas Vi,

mas contêm apenas os componentes dos m fragmentos recuperados. De modo similar, construímos uma matriz G′, contendo apenas elementos relativos aos fragmentos recuperados. Podemos então recuperar

as palavras de informação originais Uimultiplicando as palavras codificadas Vj′com a matriz inversa de

G′:

Ui= Vi′× G′−1,

O maior problema desta abordagem é que o processo de codificação requer O((m + k) ∗ n ∗ m) passos e o de decodificação O(n ∗ m ∗ m) passos. O processo inclui ainda a inversão de uma matriz mxm, mas m normalmente é um número pequeno.

Malluhi e Johnston [87] propuseram um algoritmo que melhora a complexidade de codificação para O(n ∗ m ∗ k) e melhora também a de decodificação. Eles mostraram que podemos diagonalizar as primeiras m colunas de G e ainda obter um algoritmo válido. Conseqüentemente, os primeiros m campos

2.5 Resumo 25