View
222
Download
1
Category
Preview:
Citation preview
Sistema Distribuído Imune a Falhas Bizantinas
Manuel José Machado de Matos Fernandes
Dissertação para obtenção do Grau de Mestre em
Engenharia Electrotécnica e de Computadores
Júri Presidente: Prof. Nuno Horta
Orientador: Prof. João Paulo Carvalho
Vogal: Prof. Carlos Almeida
Dezembro 2008
I
Agradecimentos
Quero deixar uma palavra de agradecimento ao Prof. João Paulo B. Carvalho pelo apoio na
realização deste trabalho.
II
III
Resumo
As falhas arbitrarias no processamento de dados, são impossíveis de prever, e muitas vezes até
de detectar, provocando assim erros em resultados, que aparentemente não estarão errados. Para
ultrapassar estes casos, e garantir que os dados não são corrompidos quer por falhas de software ou
hardware, quer por falhas introduzidas propositadamente, a replicação, a comparação e a encriptação
são processos muito úteis.
A garantia de imunidade a erros pode ser particularmente importante no caso de execução remota
de programas. Existem ferramentas, como o Condor, que aproveitam bem os tempos mortos de
processamento em máquinas de uma rede, mas não garantem que não são introduzidas falhas
indetectáveis nas comunicações ou mesmo nas execuções.
O trabalho desenvolvido visa colmatar essa lacuna nas ferramentas do tipo Condor nos casos em
que a ocorrência dessa falhas é crítica. Para isso são implementadas medidas de segurança e é
implementado um algoritmo de replicação.
Este trabalho é formado por um processo, no entanto esse processo tem duas funções distintas. A
função principal pode representar o papel de Administrador do sistema, mas também como Cliente
que executa ou envia para execução, programas cujos resultados requerem a garantia de que não
sejam corrompidos. A outra função é mais simples e é responsável pela aquisição de ficheiros a
enviar para localizações remotas.
As comunicações entre processos e entre máquinas tem um importante papel neste trabalho, pois
é da cooperação entre estes que pode surgir o sucesso dos objectivos propostos, como a imunidade
a erros.
Palavras-chave: Falhas Bizantinas, Voltan Machine, Criptografia, Sistemas Distribuídos,
Replicação, Transparência
IV
Abstract
Failures in data processing are impossible to predict, and many times to even detect, thus
provoking errors in results, that apparently are correct. To skip these situations and guarantee that the
data is not corrupted by software or hardware faults, or even by faults introduced on purpose,
replication, comparison and encryption are very useful processes.
The immunity against errors can be very important in the remote execution situation. There are
tools, like Condor, which manage the idle times of network machines processing, but they don’t
guarantee that faults are not introduced in the communications or even in executions.
This work’s goal is to fill this gap in tools such as Condor when the occurrence of these errors is
critical. Security procedures and a replication algorithm were used to achieve this goal.
This work is composed by one process however this process has two distinct functions. The main
function not only plays the role of Administrator of the system, but can also be a Client that executes
or submits to execution, programs that require a guarantee of non corruption of data. The function is
simpler and it’s responsible for the acquisition of files to send to remote locations.
The communications between processes and machines is relevant in this work, because the
success of the proposed goals like the immunity to errors arises from the cooperation between them.
Keywords: Byzantine Failures, Distributed Systems, Voltan Machine, Cryptography, Replication,
Transparency
V
Índice
Lista de Figuras VII
Lista de Tabelas VII
1. Introdução 1
2. Comunicação entre Máquinas 3
2.1 Modelo OSI, Modelo da Internet e Protocolos de Comunicação 3
2.2 Atrasos nas Comunicações 4
2.3 Segurança em Redes de Computadores 5
2.4 Conclusão 8
3. Sistemas Distribuídos 9
3.1 Objectivos dos sistemas distribuídos 9
3.1.1 Ligação entre utilizadores e recursos 10
3.1.2 Transparência 10
3.2 Remote Procedure Calls (RPC) 11
3.3 Tolerância a Faltas 11
3.3.1 Modelos de falhas 12
3.3.2 Mascarar falhas por redundância 14
3.3.3 Atingir tolerância a faltas em sistemas distribuídos 14
3.3.4 Falhas em comunicações através de RPC 15
3.4 Grids e Clusters 17
3.5 Conclusão 18
4. O Algoritmo de Voltan 19
4.1 Estrutura do sistema 19
4.2 Estruturação de um processo de Voltan 20
4.3 Conclusão 22
5. Abordagem Tomada 23
5.1 Estrutura Geral de Funcionamento 23
5.2 Implementação do algoritmo de Voltan 24
5.3 Não-Determinismos 26
5.4 Classes Importantes 26
5.5 Comunicações Estabelecidas 34
5.5.1 Comunicações TCP 35
VI
5.5.2 Comunicações UDP 37
5.5.3 Comunicações RMI 39
5.6 Escrita dos resultados em Disco 42
5.7 Funções de Hash e Criptografia 42
6. Resultados de Testes 44
7. Conclusão 49
Bibliografia 51
Anexos 53
Anexo I 53
VII
Lista de Figuras
Figura 1: Esquema de encriptação 8
Figura 2: Interligação das aplicações 20
Figura 3: Estrutura de um processo de Voltan [3] 22
Figura 4: Esquema da implementação do algoritmo de Voltan neste trabalho 25
Figura 5: Thread RecebeThread 27
Figura 6: Thread RecebeThread com vários inputs 28
Figura 7: Sequência principal do processo Bizant 29
Figura 8: Esquema Geral de comunicações do Sistema 35
Figura 9: Comunicação Bizant-RecebeThread 36
Figura 10: Inicio do Registo de um Cliente no Administrador 37
Figura 11: Finalização do processo de registo de um Cliente 38
Figura 12: Processo de saída de um Cliente 38
Figura 13: Comunicação entre máquinas diferentes e entre processos Leader e Follower 41
Figura 14: Gráfico das execuções de um ficheiro Java, para 25 inputs 45
Figura 15: Gráfico das execuções de um ficheiro Java, para 50 inputs 46
Figura 16: Gráfico das execuções de um ficheiro Java, para 100 inputs 47
Lista de Tabelas
Tabela 1: Tempos de execução do ficheiro de teste, em execução local ............................................ 47
Tabela 2: Tempos de execução do ficheiro de teste em execução remota .......................................... 48
Tabela 3: Tempos de execução média e mediana com diferentes cargas no sistema ........................ 48
VIII
1
1. Introdução
Este trabalho tem como objectivo a implementação e o estudo de um sistema de computação
distribuída em clusters ou grids com a capacidade de ser imune a falhas do tipo bizantino. Desta
forma foi desenvolvido um sistema ao qual são submetidos trabalhos para execução. Esses trabalhos
são executados remotamente, implementando replicação e segurança nas comunicações. O objectivo
é garantir que na execução de cada trabalho não são introduzidas falhas bizantinas que
comprometam os resultados obtidos. Caso essas falhas aconteçam elas são detectadas e o sistema
vai descartar essas execuções.
No sistema desenvolvido, cada máquina pode desempenhar vários papéis. Os três conceitos
existentes neste sistema são:
• Administrador - Uma máquina que será acima de tudo gestora de recursos, e que
permite, entre outros, comunicar às outras máquinas as disponibilidades do sistema para a
execução, atribuir máquinas para a execução de trabalhos, etc.
• Máquinas de Submissão - Dentro do sistema podem existir uma ou mais máquinas
que submetem trabalhos ao sistema, estas máquinas denominam-se Máquinas de Submissão.
• Máquinas de Execução - São máquinas que executam os trabalhos e devolvem os
resultados.
Qualquer Máquina de Execução é igualmente uma Máquina de Submissão, assim como o
Administrador tem também essas funções. No entanto, enquanto no sistema existem várias máquinas
de execução e submissão, Administrador apenas existe uma.
Um sistema distribuído caracteriza-se por ser um sistema que embora contenha várias máquinas
ligadas em rede, apresenta-se perante o utilizador como um único sistema simples. No entanto de
forma a atingir o objectivo que é aparentar ser um único sistema, é necessário cumprir alguns pontos
importantes:
• As comunicações têm de ser o mais fiáveis possível; assim o conceito de pilha de
protocolos é muito importante, dividindo os tipos de comunicações e simplificando a sua
implementação.
• É igualmente importante aliar segurança às comunicações de forma a garantir que os
dados a serem transmitidos e recebidos não foram sujeitos a alterações no canal, quer devido
a intercepções maliciosas ou por erros imprevisíveis. Assim são utilizados métodos de
criptografia e assinatura digital dos dados transmitidos.
• Outro ponto importante na implementação de um sistema distribuído é que em caso
de erro, esse erro seja tratado para que o utilizador não perceba que ocorreu um erro.
Enquanto alguns erros são possíveis de detectar e assim serem ultrapassados, como por exemplo
erros devido a uma máquina estar em baixo, ou devido a um processo que funcione mal, há outros
2
que são difíceis de detectar por serem arbitrários. Estes erros arbitrários são os que podem ter
consequências piores, pois é difícil prever que parte do sistema podem atingir e quando.
As falhas bizantinas caracterizam-se por terem uma ocorrência aleatória. No entanto é possível
implementar estratégias que permitem decidir com um bom grau de certeza se o sistema está perante
falhas deste tipo.
Neste trabalho foi utilizada a linguagem de programação Java por várias razões, nomeadamente o
facto de ser uma linguagem orientada a objectos, de fácil implementação e capacidade de correr em
todos, ou quase todos, os sistemas operativos. Tem uma funcionalidade muito útil para este trabalho,
RMI (remote method invocation) que permite, transparentemente, implementar chamadas a métodos
contidos em objectos remotos. Tem igualmente, funções que implementam algoritmos de hash, de
encriptação e de assinatura digital.
Por fim, de forma a demonstrar o funcionamento deste trabalho, é exigível a realização de testes
que consigam ser padrões para a garantia da correcção da execução e da performance do sistema,
pois um sistema demasiado pesado e lento, dificilmente colhe vantagens na sua utilização.
A realização deste trabalho está inserida no âmbito da execução remota de um ficheiro, mas
escapando à possibilidade de sofrer falhas arbitrárias durante o processo, ou seja, escapando à
possibilidade de sofrer falhas bizantinas.
Em qualquer sistema distribuído a comunicação de dados e a execução de processos estão
susceptíveis a erros do tipo bizantino. Pois, perante execuções remotas de processos, chamadas de
procedimentos remotos, ou simples acesso a bases de dados, podem surgir erros intrínsecos das
comunicações, ou de hardware, bem como erros introduzidos voluntariamente por utilizadores mais
ou menos escrupulosos. Para ultrapassar a probabilidade de erros deste género, é imperiosa a
necessidade de utilização de algoritmos, ou métodos, que garantam que a probabilidade de
ocorrência de erros seja virtualmente nula.
A inspiração para este trabalho decorreu de um programa chamado Condor [2], cujo objectivo é,
utilizar os tempos mortos de processamento de uma lista de máquinas disponíveis para executar
programas de elevada complexidade de computação.
No entanto, como já foi mencionado, este tipo de sistema está susceptível a falhas do tipo
bizantino, pois, a qualquer momento uma falha não detectada poderá adulterar a execução sem que
isso seja notado. Para evitar isto, surge a necessidade de replicação da execução, de forma a poder
garantir que a probabilidade de resultados erróneos diminui drasticamente.
De forma a atingir esta necessidade de replicação, a utilização de um algoritmo de replicação é
imperiosa. O algoritmo escolhido foi o algoritmo de Voltan [3,4]. Este algoritmo utiliza a replicação, e
comparação de dados como a sua principal arma contra falhas. Assim, executando o mesmo ficheiro,
diversas vezes, em máquinas diferentes, e comparando os seus resultados, pode-se garantir que a
probabilidade de uma falha bizantina que afecte todas as execuções, é tendencialmente nula.
No entanto, programas que se baseiem em geração de números aleatórios ou em chamadas ao
tempo de relógio por exemplo, estão sempre sujeitos a que desta comparação não resulte uma
conclusão definitiva, pois o valor utilizado em cada execução poderá ser diferente.
3
2. Comunicação entre Máquinas
As redes de computadores são um dos campos tecnológicos mais importantes do nosso tempo.
Hoje em dia, a Internet liga milhões de computadores à volta do mundo, providenciando
comunicação, armazenamento e computação globais. Ou seja, entre muitas características, permite a
partilha de recursos a nível de computação bem como transferências de dados, permitindo localizar o
utilizador destes sistemas a um nível descentralizado, global e mais eficiente aproveitando recursos
remotos.
Numa rede de computadores, existem máquinas com diversas funções. Os servidores fornecem
serviços à rede que permitem a partilha de dados, de ficheiros, ou igualmente permitem gerir recursos
da rede. Do outro lado da cadeia, estão os clientes, são as máquinas que utilizam esses serviços
providenciados. Nem sempre um servidor é uma máquina dedicada a funções de fornecimento de
serviços nem um cliente é um utilizador desses serviços. Muitas vezes numa rede de computadores é
possível que uma máquina aparentemente cliente forneça serviços a outra, e ao mesmo tempo,
desfrute de serviços fornecidos por uma outra ou seja pode ser cliente e servidor ao mesmo tempo.
[1,6,7]
2.1 Modelo OSI, Modelo da Internet e Protocolos de Comunicação
Para a comunicação entre máquinas, tal como para a comunicação entre pessoas é necessário
haver protocolos ou seja, um procedimento de comunicação de modo a que a informação transmitida
seja bem recebida e descodificada. No nosso dia-a-dia seguimos tais protocolos, como por exemplo a
utilização de frases iniciando conversas e terminando, e igualmente esperando a resposta da mesma
forma na comunicação entre máquinas, por exemplo, são utilizados protocolos iniciando
transmissões, esperando respostas e terminando ligações.
Um protocolo define um formato e uma ordem de mensagens trocadas entre duas ou mais
entidades, assim como acções tomadas na transmissão e/ou recepção de uma mensagem ou outro
evento. Para a comunicação entre máquinas foi compilada uma pilha de protocolos divididos em
camadas com funções separadas de forma a dividir os níveis de abstracção das comunicações e
implementar vários níveis de protocolos. Assim nasceu o modelo OSI (de Open Systems
Interconnection Reference Model) constituído por 7 camadas: a camada física, de ligação de dados,
de rede, de transporte, de sessão, de apresentação e de aplicação. Este modelo evoluiu e hoje em
dia na rede mais utilizada por todos (a Internet) utiliza um modelo apenas com 5 camadas, em que as
camadas de apresentação e de sessão foram agregadas à camada de aplicação.
A camada de aplicação é a responsável pelas aplicações de rede, por exemplo pesquisadores de
Internet, ou outro tipo de aplicações. Implementa vários protocolos como por exemplo o HTTP que
4
suporta a Internet, o SMTP que suporta a transacção de correio electrónico, ou mesmo o FTP que
suporta a transferência de ficheiros na rede.
A camada de transporte suporta os serviços de transporte de mensagens entre camadas de
aplicação de clientes e servidores. Os protocolos mais conhecidos desta camada de aplicação são o
TCP (Transmission Control Protocol) e o UDP (User Datagram Protocol). O TCP fornece um serviço
orientado à conexão. Este serviço inclui garantia de entrega de mensagens ao destinatário e controlo
de fluxo. O TCP também divide mensagens longas em pedaços mais pequenos de forma a optimizar
os recursos de transmissão. O protocolo UDP fornece um serviço sem qualquer tipo de conexão, ou
seja os dados são enviados para o destinatário sem que haja qualquer tipo de garantias de entrega,
nem tão pouco de tempo de entrega.
No entanto, tipicamente as comunicações UDP são mais rápidas. Enquanto o TCP comporta
protocolos em camadas de níveis mais baixos para garantir o encaminhamento dos dados e assim
consumindo mais recursos na, o UDP é livre desses controlos simplificando a sua transmissão. A
camada de rede é responsável por determinar os caminhos para a transmissão de dados entre duas
máquinas. Esta camada implementa o protocolo IP (Internet Protocol) responsável pelo
encaminhamento dos pacotes de informação na rede. Todas as redes que implementem esta camada
implementam também este protocolo. Existem variados protocolos de encaminhamento, mas este é o
mais importante.
A camada de ligação de dados é a camada responsável por garantir a comunicação entre um nó e
outro na rede. A camada de rede está dependente da camada de ligação de dados para implementar
os seus protocolos de encaminhamento. Os serviços fornecidos por esta camada dependem do
protocolo de ligação utilizado. Alguns exemplos de protocolos de ligação de dados são o PPP ou a
Ethernet. Como os pacotes de dados atravessam várias ligações entre a origem e o destino, podem
ser tratados por diferentes protocolos de ligação de dados no seu caminho e a camada de rede
recebe um serviço diferente em cada protocolo de ligação diferente.
A camada física é a responsável pela transmissão dos próprios bits de cada segmento entre um
nó e outro. Os protocolos de transmissão de dados a nível físico são igualmente diferentes uns dos
outros, e o mesmo protocolo de ligação de dados pode implementar vários protocolos da camada
física, pois estes protocolos podem depender do meio de comunicação (se em fios de cobre ou fibra
óptica por exemplo). Assim em cada caso os bits de informação são transmitidos de uma forma
diferente. [1,6,7]
2.2 Atrasos nas Comunicações
Na transmissão de dados numa rede de computadores, os pacotes de informação atravessam
inúmeros nós, constituídos por routers ou outras máquinas, que encontram os caminhos mais
indicados e servem de encaminhadores dos mesmos pacotes. Nestes nós são introduzidos atrasos
nas comunicações.
5
O tempo requerido para examinar o cabeçalho do pacote e determinar para onde redireccionar o
mesmo faz parte do atraso de processamento. O atraso de processamento pode incluir também
outros factores, como o tempo que leva a detectar erros ao nível de bit. Os atrasos de processamento
em routers de alta velocidade são tipicamente na ordem de microssegundos ou menos até. Depois
desta análise no nó, o pacote é enviado para uma fila, onde vai aguardar a transmissão.
Na fila o pacote tem um tempo de espera para transmissão. O tempo de espera nesta fila depende
do número de pacotes que chegaram antes e aguardam igualmente a transmissão. Assim o tempo de
atraso na fila depende muito do tráfego da rede. Ou seja, se nenhum pacote chegou anteriormente, e
a fila está vazia, então o atraso será nulo. Se por outro lado o tráfego na rede for pesado, e estiverem
assim muitos pacotes por serem transmitidos, o atraso será muito longo. Na prática o atraso na fila
pode variar entre a ordem de microssegundos e milissegundos.
Em todas as comunicações existe um atraso de propagação, que se caracteriza pelo tempo que
leva uma mensagem a percorrer o caminho entre o nó A ao nó B. Neste caso, será o tempo que 1 bit
leva a percorrer esse caminho. Claro que o tempo dispendido nesta propagação depende muito do
meio de comunicação. Ou seja, para a mesma distância, a propagação em fibra óptica é mais rápida
que em fios de cobre. Em média a velocidade de propagação está entre 200.000.000 m/s e
300.000.000 m/s. [7]
Os atrasos nas comunicações são importantes, no âmbito desta dissertação, porque alguns erros
podem-se dever a atrasos demasiado longos. Em certos casos um processo pode esperar por uma
resposta, e devido ao atraso demasiado longo na sua chegada, definir que a resposta foi perdida, que
houve erros no servidor ou que o seu próprio pedido foi perdido. No entanto a recepção tardia dessa
resposta pode provocar algum tipo de comportamento imprevisível caso não seja tratado
convenientemente.
2.3 Segurança em Redes de Computadores
A segurança em redes de computadores é um factor essencial nas comunicações. Garantir que
uma transmissão é segura é um factor de estabilidade de um sistema, seja esse sistema um sistema
comercial (por exemplo e-commerce) e aí é importante adquirir a confiança do consumidor, seja um
sistema empresarial, ou uma aplicação particular, onde é igualmente importante garantir ao utilizador
que irá obter os resultados correctos derivados da utilização da aplicação. A segurança em redes
permite também garantir a um certo nível, que erros não intencionais (como por exemplo erros de
acesso ao disco, erros de transmissão, etc.) não irão provocar danos nos resultados obtidos. Assim
por estes dois motivos, é importante num sistema distribuído implementar um certo nível de
segurança, dependendo dos objectivos do sistema.
Uma comunicação segura tem as seguintes características:
• Confidencialidade, apenas o emissor e o receptor podem aceder ao conteúdo da
mensagem, ou seja apenas eles podem descodificar uma mensagem encriptada, e se alguém
interceptar essa mensagem, não terá a possibilidade de entender o seu conteúdo.
6
• Autenticação, confirmar se o parceiro de comunicação é mesmo quem deve ser e não
alguém que se faz passar por ele.
• Integridade da mensagem, mesmo que as partes envolvidas na comunicação se
autenticarem, ou seja souberem com quem comunicam, precisam de garantir que a mensagem
trocada não sofre alterações no caminho, sejam elas intencionais, provocadas por um
interceptor, sejam elas não intencionais, por erros aleatórios.
• Disponibilidade e controlo de acesso, como a segurança é um capítulo muito
importante nas comunicações de hoje em dia, é importante que esta seja disponibilizada o
mais possível nos sistemas. Assim como o controlo do acesso a esses mesmos sistemas, por
parte de utilizadores que podem não ter legitimidade para esse mesmo acesso.
O mecanismo mais comum para implementar estes pontos importantes de segurança, é utilizando
a criptografia de forma a codificar, autenticar e distribuir a possibilidade de acesso a mensagens por
parte de utilizadores legítimos.
A criptografia é um método antigo que comporta várias técnicas, utilizadas para encobrir
mensagens e mascará-las. No entanto as técnicas de criptografia utilizadas em redes hoje em dia
pouco têm a ver com as técnicas antigas.
De uma forma geral a implementação da encriptação baseia-se no seguinte procedimento. Tendo
uma mensagem que se deseja enviar, m, um algoritmo capaz de produzir uma mensagem cifrada é
utilizado. Esse algoritmo necessita de receber igualmente uma chave, Ka. A aplicação do algoritmo à
mensagem utilizando a chave dada será Ka(m).
Para que o receptor possa ler de novo a mensagem original é necessário descodificar a
mensagem encriptada. Para atingir este fim é utilizada uma outra chave, Kb. O resultado da aplicação
do algoritmo utilizando esta chave Kb é a mensagem original, m=Kb(Ka(m)).
Em algoritmos de chaves simétricas, ambas as chaves Ka e Kb são iguais. No caso de algoritmos
de chaves públicas estas são diferentes, de forma a garantir que apenas o emissor pode codificar a
mensagem, embora muitos outros a possam ler.
No caso das chaves simétricas, um problema que pode surgir é que ambas as partes que vão
comunicar têm de acordar uma chave que ambos irão usar. No entanto esta situação implica que
toda a comunicação de negociação acontece numa comunicação segura, o que nem sempre
acontece. Assim surgiram algoritmos de chaves públicas, entre eles o mais conhecido e utilizado o
RSA.
Estes algoritmos consistem na codificação da mensagem a enviar utilizando uma chave que
apenas o codificador tem conhecimento, a chave privada. No entanto, para a leitura da mensagem
codificada é utilizada uma chave diferente e que pode ser partilhada com outros utilizadores, a chave
pública.
É essencial que a partir de uma chave pública seja impossível obter a chave privada, protegendo
assim a mensagem de ser alterada durante a sua transmissão. De forma a atingir esse objectivo deve
ser utilizado um algoritmo apropriado.
7
Neste trabalho, como é utilizada a assinatura digital, o algoritmo utilizado para criar chaves pública
e privada é o DSA (Digital Signature Algorithm). A diferença entre a utilização do RSA e do DSA
reside essencialmente no facto de que o RSA se destina a encriptar o conjunto de dados, no caso
deste trabalho seriam os dados que constituem os ficheiros transferidos entre máquinas. O DSA
consiste em encriptar o resultado de uma função de Hash, com o objectivo de que a consistência dos
dados e a sua origem, sejam verificados no destino.
Deste modo no RSA a chave pública é usada para codificar a mensagem a enviar, enquanto a
chave privada é usada pelo receptor para a descodificar e obter a mensagem original. No DSA, por
outro lado, a chave privada é usada para codificar a Hash, enquanto a chave pública é usada para
verificar a assinatura.
A criação das chaves pública e privada no DSA consiste nos seguintes passos:
• Escolher uma função de Hash H, no caso deste trabalho o SHA-1.
• Decidir o tamanho da chave L, tipicamente o tamanho será de 1024 bits, ou múltiplos.
• Escolher um número primo q com o mesmo número de bits que H.
• Escolher um número primo de L bits, tal que p‐1 é múltiplo de q, ou seja .
• Escolher um número h tal que 1 h p‐1 e mod p 1
• Escolher x de um modo aleatório tal que 0 x q.
• Calcular .
• A chave pública é (p ,q, g, y) e a chave privada é x.
A razão principal para a utilização de assinaturas digitais neste trabalho prende-se no facto de que
encriptar mensagens pode ser um processo muito dispendioso. Quando as mensagens são muito
grandes torna-se muito extenso a nível computacional produzir mensagens completamente
encriptadas.
Uma forma simples de tornear esta dificuldade consiste em transformar uma mensagem de
comprimento variável, numa outra de comprimento fixo e mais curto, que será encriptada e enviada
juntamente com a mensagem original. A esta mensagem chama-se message digest. Uma message
digest é em muitos sentidos como uma cheksum, serve para garantir a integridade do conteúdo da
mensagem original, e por consequência garantir que a mensagem original não foi de nenhuma forma
alterada durante a transmissão.
A message digest é calculada utilizando uma função de Hash. Uma característica importante para
um algoritmo gerador de message digests é a de garantir que é impossível encontrar mensagens
diferentes cujas Hash tenham um resultado idêntico, de forma a identificar univocamente a
mensagem original e assim garantir que durante a transmissão da mensagem não foram introduzidos
erros, seja com intencionalidade ou por falhas do sistema.
O procedimento de criação, envio, recepção e descodificação de dados de uma forma segura tem
os seguintes passos:
• Escolhe-se um algoritmo de criação de Hash, por exemplo o SHA-1, utilizando esse
algoritmo é criada uma nova mensagem, de comprimento fixo.
8
• Seguidamente essa Hash de comprimento fixo é assinada digitalmente. Desta forma
garante-se que a Hash não foi alterada na transmissão identificando o criador da mensagem,
como foi dito anteriormente apenas quem tem acesso à chave privada pode assinar a Hash.
• A mensagem original e a Hash assinada são enviadas juntamente para o receptor.
• O receptor recebe a mensagem e a Hash, verifica a assinatura, e caso esta seja a
correcta prossegue para a verificação dos dados
• A verificação dos dados é feita gerando uma nova Hash para os dados recebidos,
esta Hash tem de ser igual à recebida de modo a garantir que os dados estão correctos.
O processo de criação de mensagens de Hash, da sua assinatura posteriormente do seu envio,
recepção e descodificação, está representado na figura 1. [7]
Figura 1: Esquema de encriptação
2.4 Conclusão
A segurança nas comunicações entre sistemas é um ponto muito importante em qualquer sistema
ligado em rede. Desta forma é importante implementar critérios que assegurem que as comunicações
efectuadas são enviadas e recebidas com integridade.
Alguns desses critérios são criação de mensagens de Hash e assinatura digital das mensagens
trocadas entre sistemas.
9
3. Sistemas Distribuídos
Um sistema distribuído é aquele em que as máquinas cooperam na realização de tarefas com
vista a um objectivo comum. Este é o tipo de sistema que é implementado neste trabalho.
Várias definições de sistemas distribuídos têm sido criadas na literatura sobre o assunto, todas
elas diferentes. Para este trabalho vamos apoiar-nos na seguinte definição que nos é dada por
Tanenbaum, A.S. e Steen M.V. no livro “Distributed Systems, Principles and Paradigms”,
Um sistema distribuído é uma colecção de computadores independentes que aparentam ser
apenas um sistema simples e coerente, para os seus utilizadores.
Esta definição engloba dois aspectos. Em primeiro lugar o hardware ou seja as máquinas são
autónomas. Em segundo o software os utilizadores pensam que estão a utilizar um único sistema.
Ambos os aspectos são essenciais.
Uma característica importante de um sistema distribuído é que as diferenças entre os vários
sistemas que compõem o sistema distribuído e a forma como estão ligados e comunicam, é
escondida do utilizador. Uma outra característica importante é que utilizadores e aplicações podem
interagir duma forma consistente e uniforme, independentemente de onde e quando a interacção tem
lugar.
Os sistemas distribuídos devem também ser relativamente fáceis de expandir. Esta característica
é uma consequência directa de ter computadores independentes e ao mesmo tempo de esconder a
forma como esses computadores estão ligados. Um sistema distribuído deve normalmente estar
sempre disponível, embora talvez algumas partes possam não estar. Os utilizadores e aplicações não
devem notar que o sistema está a ser actualizado para servir mais utilizadores e aplicações.
Para suportar computadores e redes heterogéneas enquanto se oferece uma vista de um só
sistema, os sistemas distribuídos são normalmente organizados em camadas de software
logicamente localizadas entre o nível mais alto de utilizadores e aplicações e uma camada abaixo
constituída por sistemas operativos. Assim, a esta camada é atribuído o nome de middleware. [1,7]
3.1 Objectivos dos sistemas distribuídos
Um sistema distribuído é uma interacção complexa entre processos, máquinas e utilizadores. Não
é pelo facto de ser possível construir sistemas distribuídos que significa necessariamente que é uma
boa ideia para o problema em questão. Na verdade deve-se ponderar se o objectivo a que se propõe
deve implicar o esforço da construção e da utilização de um sistema distribuído.
10
3.1.1 Ligação entre utilizadores e recursos
O principal objectivo de um sistema distribuído é de facilitar o acesso remoto de utilizadores a
recursos, e de os partilhar com outros utilizadores de uma forma controlada. Os recursos podem ser
virtualmente qualquer coisa, nas tipicamente são impressoras, computadores, unidades de
armazenamento, dados, ficheiros, páginas web, e redes, nomeando só alguns. Há várias razões para
querer partilhar recursos. Uma razão óbvia é económica. Por exemplo, é mais barato partilhar uma
impressora do que comprar uma impressora por utilizador. Assim como faz sentido partilhar recursos
caros como super-computadores e sistemas de armazenamento de alta performance. [1]
3.1.2 Transparência
Um importante objectivo de um sistema distribuído é esconder o facto de que os seus processos e
recursos estão fisicamente distribuídos por vários computadores. Um sistema distribuído que tem a
capacidade de se apresentar aos utilizadores e aplicações como um único sistema computacional é
dito como sendo transparente.
O conceito de transparência pode ser aplicado a vários aspectos de um sistema distribuído.
Transparência de acesso está relacionado com esconder a forma como os dados são
representados e como os recursos são acedidos pelos utilizadores. Por exemplo, para enviar um
inteiro com origem num sistema baseado em Intel para uma máquina Sun SPARC requer que se
tenha em consideração que no sistema Intel os seus bytes são ordenados segundo o formado little
endian (ou seja o byte de mais baixa ordem é transmitido primeiro) e que o sistema Sun SPARC
utiliza o formato big endian (o byte de maior ordem é transmitido primeiro).
Transparência de localização refere-se ao facto de que os utilizadores não fazem distinção de
onde um recurso está localizado fisicamente no sistema. O naming desempenha um papel
fundamental da transparência de localização.
Transparência de migração está relacionada com a capacidade de mover recursos no sistema
sem que o acesso seja afectado. Uma situação ainda mais forte é quando um recurso pode ser
realocado enquanto está a ser acedido e utilizado, neste caso diz-se que o sistema tem transparência
de realocação.
A replicação tem um papel fundamental num sistema distribuído, pois alguns recursos podem
estar replicados de modo a aumentarem a sua disponibilidade ou de forma a melhorar a performance.
A transparência de replicação está relacionada com esconder o facto de um recurso estar replicado,
do utilizador.
Num sistema distribuído um utilizador pode querer aceder ao mesmo recurso que outro, por
exemplo, ao mesmo ficheiro ou a uma tabela numa base de dados distribuída, mas não deve ser
possível que ele saiba que está a aceder a um recurso partilhado, assim a concorrência deve ser
transparente também.
11
Uma outra forma de transparência é a capacidade que um sistema tem de em caso de alguma
falha nalgum nó do mesmo, esse erro não seja perceptível ao utilizador. A este tipo de transparência
chama-se transparência a falhas.
O último tipo de transparência é a de persistência, ou seja mascarar o facto de um recurso estar
em memória volátil ou em disco. Este caso acontece normalmente em bases de dados orientadas a
objectos em que podem ser chamados métodos que para aceder aos dados, e o que acontece por
trás daquilo que o utilizador vê, é que o servidor copia os dados do disco para a memória, realiza a
operação e grava de novo os dados em disco. [1]
3.2 Remote Procedure Calls (RPC)
A ideia fundamental do que é uma chamada remota a um procedimento pode-se resumir ao facto
de que é possível que um programa num computador A possa chamar um procedimento de um
programa localizado no computador B sem que o utilizador do computador A se aperceba. É possível
passar argumentos para esse procedimento, como se de um procedimento local se tratasse e
nenhum tipo de comunicação é visível para o utilizador. Ou seja, no que ao utilizador diz respeito, é
uma chamada normal a um procedimento local.
Embora esta pareça ser uma ideia simples e elegante, alguns problemas subsistem. Por exemplo,
como os procedimentos “chamador” e “chamado” correm em máquinas diferentes eles são
executados em diferentes espaços de endereços, o que causa complicações. Parâmetros e
resultados têm de ser passados, o que pode ser complicado, especialmente se as máquinas não
forem idênticas. Finalmente ambas as máquinas podem falhar, o que pode causar problemas
diferentes. De qualquer modo todos estes problemas são tratados, e as chamadas a procedimentos
remotas são largamente utilizadas. [1]
3.3 Tolerância a Faltas
Uma característica presente em sistemas distribuídos que os distingue de sistemas simples é a
noção de falha parcial. Uma falha parcial pode acontecer quando um componente num sistema
distribuído falha. Esta falha pode afectar o funcionamento de alguns componentes, enquanto ao
mesmo tempo deixa que outros componentes funcionem bem. Por outro lado, uma falha num sistema
simples pode levar a falhas em todos os componentes podendo facilmente fazer com que toda a
aplicação colapse.
Um objectivo no desenho de sistemas distribuídos é construir um sistema de tal forma que pode
automaticamente recuperar de falhas parciais sem que isso afecte de forma considerável a
performance do sistema. Em particular, quando uma falha acontece, o sistema distribuído deve
12
continuar a operar de uma forma aceitável, ou seja deve tolerar faltas e continuar a operar mesmo
com a sua presença.
Ser tolerante a faltas está relacionado com os seguintes conceitos:
Disponibilidade é definida como a propriedade que um sistema tem de estar pronto a ser
utilizado imediatamente. Em geral é a probabilidade de um sistema estar a operar correctamente num
determinado momento e está disponível para realizar as funções que os seus utilizadores desejam.
Fiabilidade refere-se à propriedade que um sistema tem para trabalhar continuamente sem
falhas.
Segurança refere-se à capacidade de que um sistema tem de quando falhar, não provocar uma
situação catastrófica.
Capacidade de Manutenção refere-se à capacidade que um sistema em falha tem para ser
reparado. Um sistema de alta capacidade de manutenção tem normalmente uma grande
disponibilidade especialmente se as falhas puderem ser detectadas e reparadas automaticamente.
Um sistema diz-se que está em falha quando não consegue atingir os objectivos a que se propôs.
Nomeadamente se o objectivo de um servidor é fornecer um número de serviços aos utilizadores, um
sistema falha quando um ou mais serviços não são completamente providenciados.
A causa de um erro chama-se falta. Descobrir o que causou o erro é, claramente, importante. Por
exemplo uma transmissão incorrecta, ou um mau meio de comunicação podem facilmente danificar
pacotes de dados provocando um erro. Neste caso é relativamente fácil remover a falta. No entanto
erros na transmissão podem ser causados por mau tempo, por exemplo em comunicações sem fios,
e neste caso não é possível remover a falta.
Faltas são geralmente classificadas como transientes, intermitentes ou permanentes. As faltas
transientes ocorrem uma vez e depois desaparecem. Se a operação for repetida a falta desaparece.
As faltas intermitentes ocorrem, depois desaparecem, depois reaparecem e assim por diante. Um
mau contacto num fio de ligação pode causar este tipo de faltas.
As faltas permanentes são aquelas que continuam a existir até que o componente em falha seja
corrigido. [1]
3.3.1 Modelos de falhas
Um sistema que falha não está a providenciar serviços adequadamente. Se considerarmos que
um sistema distribuído é uma colecção de servidores que comunicam entre eles e com clientes, um
sistema que não está a funcionar correctamente significa que servidores, canais de comunicação ou
possivelmente ambos, não estão a funcionar correctamente. Assim há vários tipos de falhas que
caracterizam a sua seriedade e as suas características:
• Uma falha de quebra (crash failure) acontece quando um servidor pára
prematuramente, mas estava a trabalhar bem até parar. Um importante aspecto deste tipo de
falhas é que quando uma quebra acontece, esse servidor deixa de ser detectável.
13
• Uma falha de omissão acontece quanto um servidor deixa de responder a pedidos,
Muitas coisas podem acontecer de errado. Por exemplo podem haver falhas de omissão de
recepção, em que o servidor nunca recebeu o pedido por qualquer razão incluindo falha nas
comunicações. Podem haver também falhas de omissão de envio, no caso em que o servidor
recebe o pedido, executa bem a sua tarefa, mas algum erro o impede de enviar os dados de
volta. Muitos tipos de falhas de omissão, não relacionados com comunicações podem
acontecer devido a falhas de software.
• Uma outra classe de falhas está relacionada com o tempo. Falhas temporais
acontecem quando uma resposta acontece fora de um intervalo de tempo específico. Por
exemplo disponibilizar dados muito cedo pode provocar falhas no buffer, que ainda está cheio.
• Um tipo de falhas sério é a falha de resposta em que a resposta de um servidor está
simplesmente incorrecta. Pode acontecer por recepção de valores errados, ou por falhas no
estado de transição, em que um servidor reage inesperadamente a um determinado pedido.
• O tipo de falhas mais sério são as falhas arbitrárias também conhecidas por falhas Bizantinas. Pode acontecer que um servidor produz um output que nunca devia ter produzido,
mas não é possível que seja detectado como incorrecto. Pior ainda, um servidor pode estar a
trabalhar maliciosamente com outros de forma a produzir intencionalmente, respostas erradas.
Falhas arbitrárias estão proximamente relacionadas com falhas de quebra. A definição
apresentada anteriormente sobre falha de quebra, é a forma mais benigna de um servidor
parar. Na verdade, um servidor que tenha a capacidade de parar quando falha, permite que os
outros processos possam detectar essa falha. Neste caso as falhas são denominadas por fail-
stop.
Há casos em que o servidor pode antecipadamente saber que vai falhar podendo assim avisar os
processos com quem está relacionado dessa situação para que o sistema consiga lidar com essa
falha. No entanto é difícil que um servidor que falhe consiga avisar os outros processos da avaria.
Assim são os outros processos que têm de concluir se houve erro ou não. No entanto num sistema
destes, denominado sistema fail-silent pode acontecer que os processos decidem que houve uma
paragem de um servidor, mas na verdade ele está apenas lento, sofrendo de falhas de performance.
Por fim existe um tipo de falhas que está relacionado com erros no servidor, quando este envia
respostas aleatórias e portanto erradas. No entanto essas respostas são detectadas pelos processos
como sendo repostas erradas, e são assim descartadas. A este tipo de falhas são chamadas de fail-
safe. [1]
14
3.3.2 Mascarar falhas por redundância
Se um sistema deve ser tolerante a faltas, a melhor maneira de atingir este fim é esconder a
ocorrência de falhas, dos outros processos. A técnica chave para esconder faltas é através da
redundância. Há três tipos de redundância possível, redundância de informação, redundância
temporal, e redundância física:
• Com redundância de informação, bits extra são adicionados de forma a permitir a
recuperação dos dados através dos bits truncados. Por exemplo um código de Hamming pode
ser adicionado para transmitir dados de modo a que possam ser recuperados, apesar do ruído
no canal de transmissão.
• Com a redundância temporal, uma acção é realizada, e depois se for necessário, é
realizada de novo. A redundância temporal é especialmente útil em faltas transientes ou
intermitentes.
• Com redundância física, equipamento extra ou processos são adicionados para que
o sistema tolere faltas nalguns componentes, como um todo. A redundância física pode ser
conseguida por hardware ou por software. [1]
3.3.3 Atingir tolerância a faltas em sistemas distribuídos
A maneira chave para tolerar processos que exibem faltas é organizar vários processos idênticos,
em grupos. A propriedade chave de um grupo é que uma mensagem é enviada a um grupo e não
apenas a um só processo. Assim, se algum membro do grupo falhar, algum outro processo pode
tomar o seu lugar.
Grupos de processos podem ser dinâmicos. Podem ser criados novos grupos e grupos mais
antigos podem ser destruídos. Um processo pode-se juntar a um grupo ou sair dele durante uma
operação. Um processo pode ser membro de vários grupos ao mesmo tempo. Consequentemente
são necessários mecanismos para gerir os grupos e os seus membros.
Uma importante distinção entre grupos tem de ser a sua estrutura interna, Em alguns grupos todos
os processos são iguais. Ninguém é o chefe e todas as decisões são tomadas pelo colectivo. Noutros
grupos, existe algum tipo de hierarquia. Por exemplo, um processo é o coordenador e todos os outros
são trabalhadores. Neste modelo, quando um pedido de trabalho é gerado, quer por um cliente
externo ou por um dos trabalhadores, é enviado ao coordenador. O coordenador decide então qual
dos trabalhadores está em melhores condições de desenvolver a tarefa, e envia o pedido para ele.
Para gerir os membros do grupo é necessário um método de forma a criar e destruir grupos. Uma
forma possível de atingir este fim é através de um servidor de grupos, para onde todos os pedidos
podem ser enviados. Este servidor terá uma base de dados com os dados completos de todos os
grupos. Este método é directo, eficiente e simples de implementar. No entanto um sistema
15
centralizado causa vários problemas, como a existência de um só ponto de falha, o que em caso de
ocorrer um erro inviabiliza a sua correcta utilização, ou o facto de ter uma escalabilidade reduzida e
assim ter possibilidades limitadas de ter a sua capacidade aumentada. Assim, a alternativa é gerir o
grupo de uma forma distribuída. Por exemplo, utilizando multicast, caso seja possível, para que um
processo exterior possa enviar mensagens ao grupo anunciando a sua intenção de se juntar.
Um aspecto importante no uso de grupos de modo a tolerar faltas, é de quanta replicação é
necessária. Um sistema é dito que tem tolerância a K faltas se consegue sobreviver a faltas em K
componentes e mesmo assim ir de encontro ás suas especificações. Se um componente falha
silenciosamente, então ter K+1 componentes é o suficiente para fornecer K tolerância, pois se K
simplesmente param, a resposta pode ser dada pelo outro que sobra.
Por outro lado, se os processos exibem falhas Bizantinas, continuando a correr mesmo quando
estão errados enviando assim mensagens falsas ou arbitrárias, é necessário um mínimo de 2K+1
processos de forma a atingir K tolerância. No pior caso os K processos que falham podem produzir o
mesmo resultado quer acidentalmente ou maliciosamente, no entanto o resto dos processos vão
continuar a produzir o resultado certo, assim o cliente pode confiar na maioria. [1]
3.3.4 Falhas em comunicações através de RPC
O objectivo das comunicações RPC é, como dito anteriormente, esconder as comunicações
fazendo parecer que um procedimento remoto é um procedimento local. No entanto, caso haja falhas
no cliente ou no servidor, o funcionamento de chamadas a procedimentos remotos pode não
funcionar bem. Deste modo as diferenças entre procedimentos locais e remotos não são tão fáceis de
mascarar.
Assim podem-se considerar cinco formas de falhas em chamadas a procedimentos remotos:
• O cliente não consegue localizar o servidor
• O pedido do cliente ao servidor foi perdido
• O servidor desliga-se quando recebe a mensagem
• A mensagem de resposta do servidor para o cliente foi perdida
• O cliente desliga-se depois de enviar o pedido
Pode acontecer que o cliente não consegue localizar um servidor adequado. O servidor pode estar
em baixo, ou pode acontecer que o processo cliente foi compilado utilizando uma determinada versão
de stub diferente da do servidor. Neste caso, o binder não conseguirá estabelecer ligação. Uma forma
de lidar com este tipo de erro é, no caso por exemplo da linguagem Java, utilizar o tratamento de
excepções. No entanto este tipo de solução acaba com a transparência que é um aspecto desejado
no desenho destes sistemas, pois ficará explicito no código que o erro em causa é de comunicação.
O segundo ponto da lista refere-se a mensagens perdidas. Este é o erro mais fácil de corrigir,
basta que o sistema operativo ou stub cliente inicie um temporizador quando é enviado o pedido. Se
16
o temporizador expirar antes da resposta ou ACK sejam retornados, a mensagem é enviada de novo.
Se a mensagem foi mesmo perdida, o servidor não vai distinguir que houve uma retransmissão. No
entanto se muitos pedidos forem perdidos, o cliente pode erradamente concluir que o servidor está
em baixo, e neste caso estamos no ponto anterior. Se o pedido não foi perdido, a única coisa a fazer
é deixar o servidor lidar com a retransmissão.
A falha seguinte nesta lista é quando há um problema no servidor, e ele em baixo. Neste caso
podem acontecer várias situações.
(a) Um pedido pode ter chegado ao servidor, ele pode ter processado o pedido e depois
entrar em erro.
(b) Um pedido pode ter sido recebido pelo servidor, mas nunca chegar a ser processado,
pois o servidor entra num estado de erro logo a seguir.
Em ambos os casos o tratamento do erro deve ser diferente. No caso (a) o servidor deve devolver
ao cliente uma resposta de erro enquanto no caso (b) o pedido pode ser simplesmente retransmitido.
O problema é que o cliente não sabe qual dos erros aconteceu, apenas sabe que o seu temporizador
expirou.
Há três filosofias para lidar com este problema. A técnica “pelo menos um” em que a ideia é
continuar a tentar que uma resposta seja recebida. Espera-se que o servidor se ligue de novo, ou
liga-se a um outro servidor, e tenta-se a operação de novo. Neste caso garante-se que o RPC foi
executado pelo menos uma vez, mas possivelmente mais que uma.
A técnica “no máximo um” apenas tenta uma vez e caso haja erro, devolve imediatamente uma
mensagem de erro. Neste caso sabe-se que o RPC foi executado uma vez no máximo, mas
possivelmente nunca o foi.
A terceira técnica é não garantir nada. Se um servidor entra em erro e fica em baixo, o cliente não
recebe nenhuma ajuda nem garantias sobre o que se passou. O RPC pode ter sido executado desde
zero vezes a um número muito grande de vezes. A principal virtude desta técnica é a sua
simplicidade de implementação.
Nenhuma destas técnicas é realmente atractiva. O ideal era uma técnica de “exactamente um”
mas de uma forma geral é impossível de garantir.
Deste modo a possibilidade de quebra no servidor muda radicalmente a natureza do RPC e
claramente distingue o processamento num só processador do processamento num sistema
distribuído.
Respostas perdidas são um problema que põe ser difícil de lidar igualmente. A solução óbvia é
que o sistema operativo do cliente utilize um temporizador. Se nenhuma resposta for recebida
durante um certo período de tempo, o pedido é enviado de novo. No entanto a causa desta situação
pode ser porque a resposta foi efectivamente perdida ou porque o servidor está muito lento a
devolver a resposta. A diferença entre estas situações pode realmente ser importante.
Se por exemplo o pedido for algo como dados de um ficheiro localizado no disco, a resposta pode
ser criada as vezes que forem necessárias sem que haja problemas. O ficheiro original continua no
disco, e não é alterado, logo a resposta será sempre a mesma. Um pedido que tem esta propriedade
diz-se que é idempotente.
17
3.4 Grids e Clusters
Um Cluster de computadores é um grupo de computadores ligados, trabalhando juntos de tal
forma que em muitos aspectos formam um único computador. Os componentes de um cluster são
normalmente, mas nem sempre, ligados entre eles através de uma ligação de rede local rápida. O
uso de clusters está normalmente associado a um aumento de performance ou de disponibilidade em
relação a um só computador, embora tipicamente com custos mais elevado do que computadores
únicos de semelhantes características.
Há vários tipos de clusters de forma a atingir diversas funcionalidades. Os clusters de alta
disponibilidade, são clusters utilizados para prevenir falhas de serviço. Ou seja, o mesmo serviço está
disponível em vários nós, para que caso um nó não esteja a funcionar correctamente, o serviço seja
garantido através de outro nó. Ou seja, estes clusters operam através de nós redundantes. Os
clusters de carga balanceada têm como forma de operação, a distribuição de tarefas através de
múltiplos nós. Normalmente este cluster é configurado de forma a ter execuções redundantes assim
como os clusters de alta disponibilidade.
Os clusters estão também relacionados com a computação através de Grids, pois os componentes
de uma grid podem ser clusters ou computadores singulares. Uma grid pode ser definida como um
tipo de sistema paralelo e distribuído que possibilita a partilha, a selecção e agregação de recursos
distribuídos por múltiplos domínios administrativos baseado na sua disponibilidade de recursos,
capacidade, performance, custo e requisitos de qualidade de serviço por parte dos utilizadores.
A grande diferença entre uma grid e um cluster reside no facto de que uma grid assemelha-se
mais a um componente aplicativo, que visa ligar em rede computadores distribuídos geograficamente.
Estes Computadores não têm qualquer tipo de conhecimento entre eles em termos de segurança,
facto que num cluster não acontece, pois num cluster os seus componentes estão num espaço
considerado comum e à partida, com níveis de segurança que lhes permite confiarem nas suas
intercomunicações.
A computação através de grids permite partilhar recursos dispersos, e simular assim, um super-
computador a um custo muito mais reduzido. Esta capacidade é possível devido à escalabilidade que
uma rede de computadores que compõe uma grid oferece. Assim torna-se mais barato ter diversos
computadores normais, que não exigem nenhum tipo de modificação, ligados em rede a fornecer
computação paralela, do que construir super-computadores que permitam obter o mesmo nível de
computação. O maior problema de computação através de grid está no facto de que as
comunicações entre computadores não ser uma comunicação rápida, como seria num super-
computador, no entanto é o suficiente para aplicações que podem correr autonomamente, sem a
necessidade comunicar resultados intermédios com frequência. [1,6,7]
18
3.5 Conclusão
Um sistema distribuído é constituído por processos que cooperam de forma a atingir um objectivo
comum.
Normalmente um sistema distribuído aparenta ser um sistema simples, não evidenciando ser
constituído por processos normalmente separados fisicamente. Desta forma um sistema distribuído
implementa transparência a vários níveis.
O facto de um sistema deste tipo ser constituído por diferentes processos normalmente ligados em
rede, leva a que possam acontecer erros em vários pontos e por diferentes motivos, provocando
respostas igualmente diferentes.
Desta forma é importante saber em que pontos podem acontecer esses erros e ter estratégias
para os combater e evitar.
19
4. O Algoritmo de Voltan
Um processo que implemente o algoritmo de Voltan permite que, em caso de falha, esta seja
tratada de uma forma silenciosa pelo sistema. Devido à replicação de processos e à redundância de
operações é possível detectar que um processo teve falhas, através da comparação dos seus
resultados, de facto como todas as réplicas recebem os mesmos dados (têm essa garantia devido a
comparações e a assinaturas digitais dos dados recebidos) caso haja diferenças nos resultados
produzidos isso significa que algum processo falhou.
É assumido que um processo em falha pode exibir um comportamento de falhas Bizantinas
incontroláveis. È assumido também que cada processo sem falhas tem a capacidade de assinar cada
mensagem, que envia com uma assinatura não adulterável e dependente do conteúdo. É assumido
igualmente que um processo sem falhas é capaz de autenticar cada mensagem assinada que recebe.
Assinaturas digitais são uma forma simples de atingir esta funcionalidade.
É assumido que computações não replicadas são constituídas por processos que comunicam
apenas através de mensagens. É assumido igualmente, a menos que dito o contrário, que qualquer
computação realizada por algum processo numa determinada mensagem é determinística.
O algoritmo de Voltan é aqui detalhadamente explicado, embora a sua implementação possa ter
diferentes caminhos, dependendo do objectivo do sistema. [3,4]
4.1 Estrutura do sistema
Um processo de Voltan recebe e envia mensagens para outros processos de Voltan através das
suas portas de entrada e de saída. Para encontrar a propriedade de falha silenciosa, um só processo
lógico é formado por duas réplicas. Assim que uma réplica cria uma mensagem de saída, assina-a e
passa a cópia para o seu parceiro. Quando a réplica recebe a mensagem de saída assinada
compara-a com a gerada localmente. Se a comparação tiver sucesso a réplica assina a mensagem
(que agora tem duas assinaturas) e envia-a para a porta específica. Se a comparação falha então é
assinalado um estado de divergência e por consequência uma falha. Neste ponto o processo de
réplica termina.
Pela descrição anterior consegue-se perceber que um processo de Voltan produz uma saída
correcta ou uma saída incorrecta detectada. Mensagens incorrectas detectadas pelo sistema (que
apenas serão no máximo assinadas uma vez) podem ser originadas pelo mau funcionamento da
réplica (por exemplo o processo de comparação funcionar mal). De notar que um processo de falha
silenciosa apenas detecta uma falha, e não a mascara. Para atingir esse fim outro tipo de processos
deverá ser utilizando em conjunto com este.
20
Foram investigados diversos esquemas de organização de modo a atingir requerimentos de
acordo e ordem, para integrar o processo de Voltan. No esquema mais eficaz são atribuídos papéis a
desempenhar ás duas réplicas que formam um processo lógico. Uma é chamada de leader (líder) e
outra de follower (seguidor). O líder é responsável por determinar a ordem pela qual as mensagens
serão processadas e sinalizar essa ordem ao seguidor. As duas réplicas da aplicação podem estar a
correr no mesmo processador ou em processadores distintos (a segunda hipótese será preferível
para evitar falhas de modo comum). A interligação entre processos Leader e Follower das duas
réplicas está demonstrada na figura 2. [3,4]
Figura 2: Interligação das aplicações
4.2 Estruturação de um processo de Voltan
Cada par líder/seguidor (leader/follower) constitui um elemento lógico. Ou seja, é da comunicação
que ambos estabelecem, e da sua partilha de informação, que surge um resultado. No entanto,
normalmente vários elementos lógicos podem ser ligados (normalmente dois) de forma a garantir
melhores resultados.
Isto implica mais comunicações entre cada unidade, mais comparações e portanto maior
segurança de que o resultado obtido é realmente o desejado.
Como está explicito na figura 3, o sistema é estruturado como um número de fios de execução de
controlo cooperantes. Cada fio de execução opera independentemente comunicando através de filas
de mensagens.
O sistema funciona da seguinte forma:
A thread de recepção (Recv) aceita apenas mensagens novas e autenticadas com dupla
assinatura para processamento (descartando o resto incluindo duplicados). As mensagens aceites
são posicionadas na fila de mensagens entregues (DMQ). A thread de aplicação escolhe uma
21
mensagem desta fila, depositando ao mesmo tempo uma cópia desta mensagem na fila de
mensagens a transmitir (TXQ) de forma a enviar a mensagem para a thread de transmissão (TX). O
protocolo de comunicação entre TX e a thread de recepção (RX) assegura uma entrega em FIFO
(First In First Out) fiável.
Quando a RX no seguidor (follower) recebe uma mensagem duplamente assinada vai introduzi-la
na DMQ da aplicação a correr, uma cópia da mensagem é igualmente depositada na associação de
mensagens recebidas internamente (IRMP) para detectar mensagens perdidas.
As threads da aplicação em ambas as réplicas procedem com o cálculo do próximo estado.
Quando esse estado é formado uma mensagem de saída é criada e uma cópia dessa mensagem é
assinada e introduzida na TXQ para ser transmitida para a outra réplica. A mensagem não assinada é
guardada internamente na IRMP para ser comparada mais tarde.
Quando uma thread de recepção recebe uma mensagem assinada vai introduzi-la na pilha de
mensagens candidatas a serem transmitidas externamente (ECMP) de forma a ser comparada mais
tarde.
A thread de comparação (Comp) compara as mensagens com idênticos números de sequência,
entre a ICMP e a ECMP. Se a comparação tem sucesso, então a mensagem da ECMP é assinada de
novo e é esta mensagem duplamente assinada é posta na fila de votação (VMQ). Uma comparação
mal sucedida causa que a replica terminará e assim o processo parará de produzir mensagens
duplamente assinadas.
A thread final que opera no sistema é o emissor. Esta thread pega mensagens da VMQ e envia-as
para os seus destinos.
A thread de recepção (Recv) do seguidor (follower) aceita igualmente novas mensagens
autenticadas por dupla assinatura, mas procede de um modo um pouco diferente do líder (leader).
Assim que cada mensagem é aceite, o seguidor vai verificar o conteúdo da sua pilha de mensagens
recebidas IRMP. Caso a mensagem já esteja nesta pilha, então o par de mensagens é apagado.
Se a mensagem não está nesta pilha, então o receptor guarda a mensagem lá e associa-lhe um
tempo de espera t1. Se a mensagem não for recebida pelo líder antes de terminar esse tempo t1, o
seguidor passa essa mensagem ao líder para ordenação introduzindo a mensagem em TXQ.
Nesta altura à mensagem localizada na IRMP é associado um outro tempo limite t2. Se a
mensagem não foi recebida com origem no líder como uma mensagem ordenada dentro deste tempo
limite t2, então o seguidor assume que o líder falhou, desliga a sua thread de comparação e termina a
execução.
Este procedimento assegura que mesmo que o líder em correcto funcionamento, falhe a recepção
de uma mensagem válida para processamento e se o seguidor a receber, esta mensagem estará
disponível para ordenação e processamento por parte do sistema. [3,4]
22
Figura 3: Estrutura de um processo de Voltan [3]
4.3 Conclusão
O algoritmo de Voltan implementa replicação de processos. O seu objectivo é detectar falhas,
nomeadamente Bizantinas e permitir ao sistema agir em conformidade perante a presença das
mesmas.
Para conseguir isto o algoritmo de Voltan implementa uma série de replicações de processos e
comparação de resultados, permitindo detectar alguma discrepância. Caso haja diferenças o
algoritmo considera que houve um erro.
23
5. Abordagem Tomada
O sistema desenvolvido neste trabalho é um sistema em que a sua característica principal é a
cooperação entre máquinas e processos de modo a garantir a execução de uma aplicação, sem que
haja erros ou falhas arbitrárias.
De forma a implementar um sistema deste tipo, é necessário compreender de que forma os
diversos processos irão comunicar entre si. Para alguns é fundamental a garantia de entrega de
dados, é essencial que os dados cheguem ao destino assim como garantir que chegam em
segurança. Noutros casos, porque as comunicações são mais simples (podem ser apenas instruções)
que podem ser retransmitidas, é mais importante a rapidez de transmissão.
A funcionalidade Java RMI (remote method invocation) é igualmente um ponto fundamental, pois
implementa a chamada a procedimentos remotos (RPC) e permite implementar transparência em
certas comunicações. Ou seja, permite que o sistema aparente estar localizado apenas numa
máquina.
Como o objectivo do sistema é a execução de aplicações sem serem afectadas por falhas
aleatórias, uma forma de garantir que a probabilidade de falhas na execução é muito baixa, é a
implementação de um algoritmo que utilize a redundância (mais uma característica importante de um
sistema distribuído). O algoritmo escolhido foi o algoritmo de Voltan.
Por fim, implementando o algoritmo de Voltan, garantindo transparência e segurança nas
comunicações, é possível implementar um sistema distribuído imune a falhas aleatórias (Bizantinas).
5.1 Estrutura Geral de Funcionamento
O sistema implementado tem duas partes distintas, uma que se destina a enviar um ficheiro em
disco, para execução com o respectivo input, e outro que implementa o sistema em si mesmo, ou
seja, trata do reenvio do ficheiro para execução nas máquinas remotas.
A primeira parte aqui referida, tem uma estrutura simples, pois acede a um ficheiro de
configuração, com os dados referentes aos ficheiros a enviar para execução. Esses ficheiros para
execução são, depois, lidos para a memória, e enviados através de uma ligação TCP local, para a
outra parte do sistema.
O sistema pode correr com uma de duas funcionalidades:
• Pode ser Administrador, e neste caso tem de ser o primeiro a iniciar a execução. As
suas funções são de gestão do sistema, nomeadamente no que respeita à gestão do registo e
remoção de máquinas de uma lista que mantém em memória. Tem a função também de
comunicação dos dados das máquinas que estão disponíveis para execução com destino ás
máquinas de submissão.
24
• Pode ser igualmente um Cliente. Neste caso as suas funções são, por um lado enviar
ficheiros para execução (Máquina de Submissão). Nesta funcionalidade, pedem ao
Administrador máquinas para executar o ficheiro, e depois da recepção das execuções,
compara-as para determinar possíveis diferenças, e consequentemente, marcar como errada a
execução. Por outro lado, podem ser máquinas de execução de ficheiros (Máquina de
Execução). Nesta função, o Cliente recebe um ficheiro, com o respectivo input, com origem
numa Máquina de Submissão, comunica através de TCP com a outra máquina que também,
em paralelo, está a executar o mesmo ficheiro, de modo a seguir o algoritmo de Voltan, e
procede ás verificações de consistência do ficheiro, execução, e retorno dos resultados da
execução para a Máquina de Submissão original.
5.2 Implementação do algoritmo de Voltan
A implementação do algoritmo de Voltan neste trabalho foi um pouco alterada pois num sistema
de Voltan completo, como explicado anteriormente, cada processo compara dados recebidos com os
de outros processos três vezes, além de também comparar as execuções outras três vezes, o que
implica um total de seis comparações em cada processo, e um total de vinte e quatro comparações
no total.
Este elevado número de comparações, além de ser desnecessário, pode ser demasiado pesado
para o sistema. Assim, a comunicação entre os vários processo Leader e Follower foi diminuída e
consequentemente as comparações entre ficheiros. A necessidade de menos comparações advém
do facto de que a maioria das comunicações importantes ser feita por TCP, que garante a entrega de
dados, e introduzindo segurança com hash e criptografia, que garante a ausência de falhas.
Assim, o sistema de comunicações implementado foi restringido a comunicações entre os
Leaderes, e entre o Leader e o respectivo Follower. O Leader 1 recebe da submission machine os
dados a processar e compara esses dados com os que lhe são enviados pelo Leader 2, que
entretanto também recebeu da submission machine.
Além de comparar, o Leader 1 envia os seus dados também para o Leader 2. Depois desta
comparação inicial, cada Leader envia os dados para o respectivo Follower, que executa e envia o
resultado dessa execução para o Leader. Depois o Leader compara a sua execução com a do
Follower seguidamente a comparar a sua execução com a execução do outro Leader, e envia o
resultado para a submission machine, que compara os dados recebidos de uma máquina e de outra.
Ou seja, como Os followers são fios de execução criados a partir dos respectivos leaders, e como
as comparações essenciais são realizadas nos leaders, e finalmente no servidor central, torna-se
redundante comparar as execuções entre followers e entre followers e leaders de diferentes unidades
lógicas.
.
25
Figura 4: Esquema da implementação do algoritmo de Voltan neste trabalho
Neste trabalho, devido à utilização do mecanismo Java RMI, que cria automaticamente threads de
execução, o processo Leader é uma thread, que apenas executa um ficheiro, sendo que, se a mesma
máquina receber vários ficheiros para executar, lança threads diferentes.
Esta é uma diferença de notar, pois no algoritmo original, cada processo, trata várias mensagens,
utilizando filas e ordenação das mensagens a executar, utilizando para isso threads diferentes para a
comunicação, comparação, execução, etc.
A abordagem deste trabalho, evita portanto, a complicação da gestão de filas e de organização de
mensagens, no entanto, não sendo esse o objectivo desta mudança, foi uma simplificação da
implementação. Na figura 4 está esquematizada a implementação do algoritmo de Voltan neste
trabalho.
26
5.3 Não-Determinismos
Chamadas sistema não determinísticas, como chamadas ao relógio actual, ou a geração de
números aleatórios, pode causar diferença de estados, e consequente sinalização de erro. Pois no
que se refere a chamadas ao relógio local, como há diferenças entre o relógio de cada máquina,
execuções de programas que exijam para produzir um resultado válido, um tempo de relógio, devido
a essas diferenças entre máquinas, a comparação dos resultados produzidos será sempre diferente.
Assim, os resultados, mesmo que estejam correctos, não serão iguais e portanto, a comparação vai
sempre dar um erro. O mesmo acontece em relação à utilização de números aleatórios.
Portanto, para resolver este problema, e para cada uma das origens do problema, as abordagens
são diferentes. No que respeita ás chamadas ao relógio, a solução seria transmitir o valor do relógio
local da máquina que submete os trabalhos, para as máquinas que os vão executar. Seguidamente
esse valor de relógio será introduzido para a execução, e forma a que os resultados produzidos
pudessem ser comparados com a segurança de que todos os pressupostos iniciais são os mesmos.
No que respeita à utilização de números aleatórios, a solução passaria por a máquina que
submete trabalhos, produzir um número aleatório, e este ser transmitido às máquinas que executam.
No entanto a utilização destes valores pode ser complexa, pois é difícil definir que programas
usam chamadas não determinísticas a menos que se possa aceder ao código do programa a
executar.
5.4 Classes Importantes
A aplicação desenvolvida assenta o seu funcionamento em objectos cuja importância se realça de
entre a complexidade de código patente nos códigos-fonte de ambos os programas. [5]
No entanto como foi mencionado, está dividida em duas partes com funções distintas. Uma parte,
é a principal, tem tarefas de registo de máquinas no sistema, de execução de tarefas e de submissão
de tarefas ao sistema. A outra parte destina-se a submeter para execução no sistema. Esta tem
apenas a função de reunir os dados necessários e enviá-los, localmente, à outra parte da aplicação.
A aplicação Bizant está dividida em duas partes. Uma das partes tem como funcionalidade
principal a recepção de dados e o seu envio posterior para a parte principal. A aplicação ao utilizar
esta funcionalidade lê os dados contidos num ficheiro de configuração que contém as informações
sobre os ficheiros a submeter ao sistema. Na verdade podem ser submetidos vários ficheiros ou
apenas um único. Os parâmetros de entrada, caso existam, podem estar localizados neste ficheiro de
configurações, ou em ficheiros separados, mas com referência nas configurações.
As tarefas desta parte são executadas fundamentalmente em duas classes principais:
27
• Classe RecebeThread
Esta é a thread principal da parte mais simples da aplicação. Nesta thread é feita a leitura
do ficheiro de configurações. De entre os métodos utilizados neste pacote, destacam-se os métodos de leitura de
ficheiros quer executável ( leFicheiro() ), de input ( leInput() ). O método leInput(), é utilizado no
envio de um ficheiro com apenas um input. Ambos os métodos apenas lêem do disco os
ficheiros respectivos e guardam-nos em memória sob a forma de arrays de bytes.
Existem duas formas distintas para o envio do mesmo ficheiro executável, mas com vários
inputs. A primeira baseia-se na configuração em que se dá, no ficheiro de configuração, um só
ficheiro, mas com um input diferente em cada linha. Nesta forma de envio múltiplo é utilizado o
método enviaVarios(), assim por cada linha lida do ficheiro de inputs, é feita uma ligação TCP
para o módulo principal para o envio desse input e do ficheiro, que está em memória, através
da thread enviaFileThread().
• Classe EnviaFileThread Esta thread é lançada para o envio de pares ficheiro/input, ambos fazendo parte da classe
Ficheiros. Cria um novo socket ligado ao módulo principal da aplicação, e envia esse objecto.
Nas figuras 5 e 6 está esquematizada a utilização com um (Figura 5) ou vários (figura 6) inputs.
Figura 5: Thread RecebeThread
28
Figura 6: Thread RecebeThread com vários inputs
A segunda parte da aplicação, a parte que implementa as comunicações e transmissões de dados
entre máquinas, tem como tarefas fundamentais registar máquinas no sistema, decidir quais as
máquinas disponíveis para execução, enviar dados para execução, e em caso de ser máquina de
execução, receber os dados e implementar o algoritmo de Voltan. Na figura 7 está esquematizada a
sequência de passos que são implementados.
29
Figura 7: Sequência principal do processo Bizant
A leitura das configurações do sistema é feita pelo método seguinte:
• leConf()
Este método tem a simples função de leitura do ficheiro de configurações, e atribuição de
valores a variáveis importantes, tais como portos de ligação, RMI, UDP e TCP, IP do
Administrador, importante caso a execução seja em modo Cliente, tamanho máximo dos
ficheiros a enviar para execução.
30
A aplicação Bizant tem dois perfis principais, Administrador e Cliente. De forma a aplicar o perfil de
Administrador, é utilizado o método seguinte, caso contrário a máquina é considerada como tendo
perfil Cliente. Todas as outras comunicações entre Administrador e Cliente estão detalhadas em
baixo.
• administrador()
Em primeiro lugar inicia a comunicação RMI, através do método iniciaRMI(), para depois
acrescentar à lista de máquinas, as suas próprias características. Seguidamente lança a thread
Escuta para escutar ligações UDP vindas dos clientes.
• cliente()
O método cliente() está encarregado das comunicações de registo para com o
Administrador, feitas através da thread ComunicaUDP, e chama o método iniciaRMI(), para
inicializar as ligações RMI.
• iniciaRMI()
Neste método são inicializadas as comunicações RMI, através da classe remota PoolList.
As comunicações RMI começam pela criação de um registo no porto definido para o
funcionamento (definido nas configurações), e depois é feita a ligação da classe remota ao
endereço, em formato url, da máquina local.
No que diz respeito ao Administrador, as comunicações RMI são fundamentalmente para o
pedido de máquinas disponíveis para execução, e no que diz respeito ao cliente a
comunicação RMI está relacionada com o envio de ficheiros para execução, e recepção dos
resultados das respectivas execuções.
As máquinas estão dividas também em dois sub-perfis: Máquinas de submissão e máquinas de
execução. O método seguinte é responsável pela implementação da máquina de submissão no
sistema.
• submissionMachine()
É responsável por lançar a thread TrataRecebe, que é a thread que recebe os ficheiros
vindos da thread RecebeThread, e vai depois tratar do envio para as Execution Machines.
Neste método está localizado o accept relacionado com o socket TCP da comunicação
entre Bizant e a thread RecebeThread.
31
• Classe TrataRecebe
Esta classe, também uma thread, é responsável pela recepção de ficheiros a executar,
enviados pela thread RecebeThread.
Se o modo for de Administrador, ao receber um ficheiro para executar, vai aceder à sua lista
de máquinas, e procurar duas que tenham as características certas para correr esse programa,
e depois, vai chamar a classe EnviaFicheiros para tratar do envio desses ficheiros, por
métodos remotos, para as máquinas correspondentes.
Se o modo for de Cliente, o processo é semelhante ao anterior, excepto que o acesso à lista
de máquinas disponíveis é feito através de métodos remotos (RMI). Mas como o objectivo da
invocação de métodos remotos, é garantir transparência na execução, as diferenças a nível de
código entre os dois modos de execução, são quase imperceptíveis.
O registo de máquinas no servidor é uma tarefa realizada pelas duas classes seguintes, sendo
que uma pertence ao Administrador, quando aguarda por ligações de Clientes, e a outra é a
responsável pelas ligações dos Clientes:
• Classe Escuta
Esta classe representa uma thread de execução, encontrada apenas no modo de execução
Administrador, e a sua função é tratar da recepção de pedidos de registo por máquinas cliente,
também de recepção de mensagens com a informação de que uma máquina cliente se
desligou, para que o Administrador retire da sua lista, essa mesma máquina.
Por último, esta classe tem também uma outra função de comunicação. Quando o
Administrador é desligado de forma elegante, na classe Console, responsável pela interface
gráfica, é enviada uma mensagem para a thread escuta, que por sua vez, reenvia essa mesma
mensagem a todas as máquinas da lista. As comunicações serão explicadas em maior
pormenor mais à frente.
• Classe ComunicaUDP
Esta classe é, também, uma thread. É utilizada em modo de Cliente, para iniciar o processo
de registo no Administrador. Ao iniciar o processo em modo Cliente, é uma das primeiras
coisas que faz, de forma a estar disponível para a qualquer momento executar algum
programa.
Nesta thread são também tratadas as situações em que o Administrador se desliga, e
igualmente, a tentativa de reiniciar as próprias ligações.
32
• daPcs()
Este método é chamado para devolver duas máquinas disponíveis para executar. Em
primeiro lugar, recebe o tipo de sistema operativo onde deverá ser executado o programa.
Assim, este método procura aleatoriamente entre as máquinas disponíveis com esse mesmo
sistema operativo. Além de procurar máquinas com sistemas operativos exigidos para a
execução, procura também máquinas dispostas a executar ficheiros de tamanho maior ou igual
àquele que se deseja executar. Caso não haja máquinas suficientes, lança uma excepção.
No entanto a escolha é feita com dois pressupostos à partida, em primeiro lugar, a máquina
que pediu não é escolhida, e que estão, pelo menos, além dessa máquina, mais duas na lista,
podendo incluir o próprio Administrador, pois também pode realizar a função de Execution
Machine
Esta classe é transversal na aplicação realizando diversas tarefas de várias funções. Implementa
a interface PoolListI, que é a interface remota responsável pelas comunicações RMI.
• Classe Remota: PoolList Esta é a classe fundamental do trabalho. Pois engloba funções variadas, desde a execução
de ficheiros, manutenção da lista de máquinas, retorno de máquinas com capacidade de
execução etc.
As classes descritas seguidamente são responsáveis pela implementação do algoritmo de
Voltan no sistema.
• Classe EnviaFicheiros
Nesta classe, que é a classe fundamental no que respeita ao envio de dados para as
máquinas remotas, são lançadas duas threads LancaEMThread, utilizadas para lançar, cada
uma delas, uma ligação para a execução dos dados. Essa ligação é feita através da invocação
de métodos remotos, neste caso o método EM().
Como foi descrito no capítulo sobre o algoritmo de Voltan, é necessário haver duas
máquinas a executar, e comunicarem entre elas. A escolha do porto de comunicação é feita
por uma das máquinas, que vai transmitir à máquina de submissão, que porto é o escolhido.
Depois da comunicação do porto, a máquina de submissão está preparada para lançar a
execução dos dados na segunda máquina de execução.
Antes do envio dos ficheiros para execução, os dados são assinados, através de hash e
criptografados, como explicado mais adiante.
No fim das execuções, são recebidos os resultados, e tratados, seguidamente por uma
outra classe, ComparaFiles.
33
• Classe LancaEMThread
A função desta classe, que como se estende da classe Thread, é então uma thread, é
fundamentalmente lançar o método remoto EM(), passa como argumentos os dados
necessários consoante a função da máquina remota for leader 1 ou leader 2, nomeadamente
os dados para a comunicação entre o leader 1 e a máquina de submissão, pois o leader 1
comunica para a SM o porto em que está à escuta. Se a função da máquina remota for de
leader 2, envia-lhe os dados que recebeu do leader 1. Em ambos os casos é enviado o objecto
contendo o ficheiro a executar.
• Classe ComparaFiles
Como dito anteriormente, esta classe é responsável pela comparação dos ficheiros
recebidos, e escrita dos mesmos no disco. Ao receber dois pares de ficheiros a comparar
(STDIN e STDERR), verifica as suas assinaturas, de forma a garantir que os dados não foram
corrompidos na transmissão. Depois realiza a comparação propriamente dita, através do
método compara(), que chama o outro método comparaDois(), e aqui, ou os ficheiros são
iguais e são escritos no disco, através do método escreveFileNoDisco(), ou se são diferentes,
são escritas no disco as mensagens "FICHEIRO RECEBIDO ERRADO", de forma a que não
haja dúvidas sobre a conclusão da execução.
• Classe EscreveExecThread Esta classe, que também é estendida da classe Thread e por isso uma thread também, é
responsável pela escrita dos dados resultantes da execução, em disco. Assim, esta thread
recebe uma lista especial, chamada BloquingQueue. O acesso a esta lista é feita do seguinte
modo, a thread que quiser escrever dados nesta lista adquire o acesso de escrita, e quando
estiver despachada, liberta o acesso, se uma outra thread quiser ler dados desta lista, o
processo é semelhante. No caso implementado a lista apenas contem um objecto de cada vez,
ou seja, só é adicionado outro objecto à lista, depois de retirado o objecto que lá se encontrava.
Assim, é garantido que o acesso ao disco é feito duma forma sequencial e sincronizada.
• EM(), leader1() e leader2()
Estes métodos estão relacionados. O primeiro a ser chamado é o EM(), que depois, se a
execução for destinada a ser realizada pelo leader1(), esse método é chamado, caso contrário
é chamado o método leader2(), Estes dois métodos, situados em máquinas diferentes, são
considerados um par. Comunicam entre eles, primeiro os dados a executar, de forma a
confirmar a sua consistência, quer do ponto de vista das assinaturas, quer de igualdade de
34
dados. E por fim comparam as execuções realizadas, também sobre ambos os pontos de vista,
de assinatura e igualdade de dados.
• Leader() e Executa()
O método Leader() é chamado por ambos os métodos leader1() e leader2(), e nele é
implementada uma thread interna, referente ao Follower. O Leader envia para dentro da thread
Follower os dados a executar, e o Follower confirma as assinaturas e executa, devolvendo no
fim os resultados da execução para o Leader, que entretanto também procedeu à sua
execução. Compara os resultados, e envia os dados de volta. Todas as comunicações são
feitas através de UDP, localmente.
O método Executa(), utilizando várias chamadas de sistema, e criando um processo filho,
executa o ficheiro. Primeiro verificando que tipo de execução é, ou seja, em que sistema
operativo irá ser realizada a execução. Depois chama o método Runtime.getRuntime().exec(),
e depois, recebe o OutputStream, de forma a que envia para esse stream os dados do STDIN,
correspondentes a esta execução.
Seguidamente, recebe os streams InputStream e ErrorStream, para onde serão escritos os
valores do STDOUT e STDERR, e utiliza estes dados para criar um objecto Ficheiros com
esses dados, e retorna-os.
5.5 Comunicações Estabelecidas
No seio de um sistema distribuído, estão em relevo as comunicações entre máquinas, os seus
protocolos, e o paradigma por trás da sua utilização, ou seja os objectivos a atingir com a utilização
de determinado protocolo de comunicação.
O requerimento da garantia de entrega de dados e em ordem, nem sempre é fundamental,
passando assim para plano de destaque outras características como a rapidez. Em muitos casos até,
a necessidade de transmissão geral, ou broadcast, de dados sem que a perca de dados seja
dramática, é fundamental. Para atingir esse objectivo não são necessárias garantias de entrega de
dados, simplificando os protocolos.
As comunicações por invocação remota de dados são utilizadas largamente em sistemas
distribuídos, tomando por isso um papel relevante nas comunicações entre máquinas do sistema,
oferecendo assim, ao sistema uma maior transparência da execução. O esquema das comunicações
implementadas no sistema desenvolvido está presente na figura 8.
35
Figura 8: Esquema Geral de comunicações do Sistema
5.5.1 Comunicações TCP
As comunicações por TCP foram escolhidas para transferência de dados importantes devido a não
estarem sujeitas a uma probabilidade de ocorrência de erros nem a perdas de pacotes elevada.
Nestas condições estão duas comunicações, a comunicação entre threads Bizant-RecebeThread, e a
comunicação entre Leaders. Nesta última revela-se de fundamental importância uma garantia maior
de transferência de dados.
Bizant-RecebeThread
As comunicações TCP entre a thread RecebeThread e o processo Bizant são realizadas em porto
definido no ficheiro de configurações do Bizant. O processo Bizant escuta nesse porto, e a thread
RecebeThread, por cada par ficheiro executável/input, que quer enviar, realiza uma ligação por TCP,
através de uma outra thread EnviaFilesThread, para o envio desse mesmo par com destino ao Bizant.
Este último, sempre que recebe uma ligação da RecebeThread, lança uma thread TrataRecebe, de
forma a garantir o máximo de paralelismo no tratamento dos dados, e melhor aproveitamento do
tempo disponível para a sua execução, como é visível na figura 9.
No fim da transferência de dados, esta ligação é desligada, e o processo Recebe, ou finaliza, ou
caso esteja a enviar múltiplos ficheiros, continua o seu trabalho, para no fim finalizar.
36
Figura 9: Comunicação Bizant-RecebeThread
Leader 1 – Leader 2
Nesta ligação, à comunicação TCP está atribuído um papel importante. Depois da criação do
socket TCP, e respectivo envio do porto de localização de volta para a Submission Machine, o Leader
1 fica à espera de uma ligação por parte do Leader 2. Este por sua vez tem acesso ao porto de
localização e IP, através da recepção por argumento de um objecto contendo essas informações,
chamado addrPort. Assim, mal esteja em posse desses dados, inicia uma ligação para com o Leader
1.
Mal esteja estabelecida a ligação entre os Leaders, o Leader 1 envia para o Leader 2 o objecto
que recebeu contendo o ficheiro executável e input. O mesmo fará o Leader 2, e seguidamente,
ambos comparam os dados que receberam por parte do SM com os dados que receberam do seu
par. Além da comparação, é feita também a verificação da assinatura, para garantir que não houve
corrupção voluntária, ou não, dos dados durante a comunicação.
Caso haja erros, é feita a devolução para a SM de um objecto contendo essa informação. Se, pelo
contrário, tudo estiver correcto, decorre a execução. Mais tarde, depois da execução, e comparação
com o respectivo Follower, cada um dos constituintes do par envia para o outro, o resultado da
execução, pela mesma ligação TCP. Mais uma vez é feita uma comparação de dados e de
assinaturas para confirmar a correcção dos dados, e por fim, é feito o envio, por retorno do método
remoto, dos resultados das execuções de cada processo Leader para a máquina SM originária.
37
5.5.2 Comunicações UDP
As comunicações UDP foram escolhidas fundamentalmente devido à sua rapidez. Embora
estejam sujeitas ao risco de perda de informação, a informação comunicada entre máquinas é
sempre relativamente curta (são apenas comandos simples), e sujeita a confirmações. Por outro lado,
algumas transmissões não contêm informação fundamental e a sua perda não provoca erros no
sistema, e neste caso as comunicações utilizando o protocolo UDP são adequadas.
Registo do cliente no administrador
O protocolo de registo de um Cliente no Administrador, processa-se duma forma simples. O
Cliente envia uma mensagem UDP com a String “CON” (figura 10), e fica à espera durante um
determinado período de tempo, neste caso 10s, pela resposta do Administrador. Essa resposta
deverá ser a mensagem “OK” (figura 11). No caso de não receber nenhuma mensagem em 10s volta
a enviar, e repete o processo 5 vezes, e caso não tenha sucesso, pára o envio da tentativa de registo
e mostra uma mensagem ao utilizador dizendo que o Administrador não está alcançável.
Pelo lado do Administrador, ao receber um requerimento para registo, verifica se já contém essa
máquina na sua lista, e caso seja negativo, introduz os dados da máquina requerente, na sua lista.
Caso já a contenha, não altera nada.
Figura 10: Inicio do Registo de um Cliente no Administrador
38
Figura 11: Finalização do processo de registo de um Cliente
Desligar Cliente ou Administrador Quando uma máquina se desliga do sistema, envia uma mensagem em forma de string, por
comunicação UDP, localizada igualmente na thread escuta. Essa mensagem é “OFF” (figura 12), e
perante a recepção de uma mensagem deste tipo, o Administrador irá retirar da sua lista a entrada
referente ao IP da máquina que requereu a sua saída do sistema.
O Administrador, ao desligar elegantemente acede á lista de máquinas do sistema através de uma
comunicação UDP local. O objectivo é enviar uma mensagem em forma de String “ADMOFF”, e
seguidamente o Administrador cessa a sua execução sem que haja necessidade de receber
confirmações de recepção.
Ao receber a mensagem “ADMOFF”, os Clientes recebem a informação de que o envio de
ficheiros para execução será inútil pois o Administrador está em baixo e resta-lhes esperar que volte
a estar activo.
Figura 12: Processo de saída de um Cliente
Pedido de lista de máquinas
O pedido de lista de máquinas é feito através de uma ligação UDP local entre a thread Console,
relacionada com a interface gráfica, e a thread escuta. Para esta comunicação é utilizada um objecto,
chamado PacotePort, no qual são introduzidos os dados da mensagem a transmitir, neste caso este
objecto é transportado, carregando a String “list”. Este objecto é recebido pela thread escuta, e
39
imediatamente é enviado de volta para a console, uma lista contendo todas as máquinas ligadas ao
sistema no momento, para que a thread Console as mostre no ecrã.
Envio do porto de escuta do Leader 1
Para a comunicação TCP entre o Leader 1 e o Leader 2, é necessário antes de mais, que o
Leader 2 saiba a que porto situado no Leader 1 se deverá ligar. Para corresponder a esta exigência o
Leader 1, situado na máquina remota, sob a forma de uma thread gerada automaticamente pelo
mecanismo de RMI, e também gerada a quando da chamada do método remoto EM() (que por sua
vez chamará os métodos leader1() ou leader2() ), vai escolher um porto para fazer o seu bind(), e
enviar por UDP, de volta à máquina SM, o porto que conseguiu ligar.
Os portos UDP utilizados são escolhidos pelo sistema, consoante os portos disponíveis. O objecto
remoto sabe a que porto deve remeter a resposta pois, é-lhe passado por argumento fazendo uma
chamada de getLocalPort() ao Socket utilizado. Seguidamente, o porto de escuta do Leader 1, é
passado para o Leader 2 de forma a este se poder ligar.
Envio do ficheiro a executar para o Follower, e envio da execução para o Leader
A comunicação entre o Leader e o Follower é também feita através de UDP local. Como já foi
mencionado, o Follower é uma thread interna, e portanto, a comunicação entre threads tem de ser
feita recorrendo a um mecanismo. Neste caso foi escolhido a comunicação por datagramas. Isto
permite a comunicação entre o Leader e a sua thread filha, o Follower.
Depois desta negociação inicial, o Leader envia para o Follower os dados a executar, e este
verifica a assinatura dos dados, para assim depois executar o ficheiro. No final da execução, envia
para o Leader os seus resultados e recebe do Leader os seus resultados. Ambos irão comparar as
conclusões, e assinalar o erro em caso de o haver, ou retornar a execução do ficheiro.
5.5.3 Comunicações RMI
A comunicação por RMI é feita com base em TCP, mas as chamadas de funções importantes, tais
como accept() ou connect() não são explicitas na linguagem. No entanto, o RMI tem as
características do TCP, nomeadamente as mais importantes para este trabalho que são, a garantia
de transmissão de dados, e até, em alguns momentos, a capacidade de na ligação expirarem
temporizadores.
40
Main - iniciaRMI() No método iniciaRMI(), da classe Main, é inicializado o registry do RMI, através da classe
LocateRegistry e do método dessa mesma classe createRegistry(). O registry é uma interface que
permite que uma máquina receba ligações por RMI. Este registo é inicializado num porto específico,
recebido através do ficheiro de configurações do Bizant. Depois de criado o registry, é feita a ligação
entre o nome da máquina, que é dado em formato de url, e o objecto remoto, através da classe
Naming e do método bind().
Esta inicialização é feita quer o modo de execução seja Administrador, quer seja Cliente, pois
ambos utilizam frequentemente este tipo de comunicação.
RMI na thread TrataRecebe e no objecto enviaFicheiros
As comunicações RMI são utilizadas neste método por duas razões. Em primeiro lugar há o caso
de uma máquina Cliente estar a encarnar o papel de Submission Machine, e neste caso o seu
primeiro passo é pedir ao Administrador, os nomes de dois Clientes dispostos a ser máquinas de
execução. Para isso, é preciso iniciar a ligação RMI, e com esse fim, é utilizado o método lookup() da
classe Naming, e fazer a ligação através da interface remota PoolListI. A partir deste momento, a
máquina local tem acesso aos métodos remotos, localizados no Administrador, e pode chamar de
uma forma bastante simples, os métodos necessários para completar esta operação, que neste caso,
é o método daPcs().
Para ambos os métodos de execução são, depois, feitas novas ligações RMI, para com as
máquinas de execução. Este processo é igual ao que já foi explicado anteriormente, e assim obtêm-
se duas interfaces remotas, que são passadas por argumento para o objecto EnviaFicheiros.
De novo, as chamadas dos métodos remotos são feitas duma forma bastante transparente, e
neste caso, são enviados por argumento, no caso do Leader 1, o objecto Ficheiros, contendo os
dados para a execução, o porto e IP de comunicação UDP para com o SM, e o resto dos argumentos
não são utilizados para este tipo de Leader. Para o Leader 2, além dos argumentos atrás
mencionados, é também enviado um objecto contendo os dados para o início da ligação TCP com o
Leader 1.
Em ambos os Leader, o retorno deste método é um objecto Ficheiros, contendo a execução
respectiva.
As comunicações entre processos, contendo pedidos, resultados de execuções e comparações
está representado na figura 13.
41
Figura 13: Comunicação entre máquinas diferentes e entre processos Leader e Follower
42
5.6 Escrita dos resultados em Disco
Ao escrever ficheiros no disco, é importante garantir que não há colisão de recursos, ou seja, é
necessário garantir que a partilha do acesso ao disco, para escrita e leitura, seja feita de forma
síncrona. Claro que a utilização de ciclos de espera activa não é eficiente do ponto de vista de
recursos do processador e da memória. Assim, no que concerne à escrita dos dados na máquina de
execução, e como o método que faz esta função é remoto, o sincronismo no acesso ao disco é
garantido pela palavra-chave synchronized, ou seja, o método que vai escrever no disco adquire o
acesso ao disco e as outras threads produzidas pelas outras execuções paralelas, terão de esperar
que este método liberte o acesso, para elas poderem avançar.
No que respeita à escrita dos dados produzidos pela execução, na máquina que submeteu o
trabalho, o processo é um pouco diferente. Neste caso foi utilizada uma thread cuja função é apenas
receber dados para escrever no disco, sincronamente. As threads que receberam os dados,
introduziram os dados numa lista especial chamada BlockingQueue que tem métodos de acesso aos
seus dados com características atómicas, ou seja, bloqueiam à espera de dados a escrever, e
quando houver dados na lista, um semáforo é assinalado que indica a disponibilidade para continuar
o processo, neste caso, a escrita dos dados.
Este tipo de partilha de recursos está compreendido na classe de produtor/consumidor, pois os
dados são produzidos por várias threads, e consumidos apenas por uma, a que os escreve no disco.
5.7 Funções de Hash e Criptografia
Para garantir que não são introduzidos erros na comunicação entre processos, quer sejam
processos remotos, ou locais, são utilizadas funções de hash, e também funções de criptografia e de
assinatura digital.
Uma função de hash é um procedimento bem definido que converte um conjunto grande e
possivelmente variável de dados, num conjunto mais pequeno de tamanho fixo. Os valores
produzidos por este procedimento podem servir como uma etiqueta única desses mesmos dados. O
algoritmo substitui e transpõe os dados de forma a criar estas etiquetas únicas. Estas etiquetas são
chamadas hash sums, hash values, hash codes ou simplesmente hashes. As hash sums são
normalmente utilizadas como índices em hash tables ou ficheiros de hash. Funções de hash
criptográficas são normalmente utilizadas para variados propósitos de segurança de informação em
aplicações.
Utilizar funções de hash para detectar erros, é bastante simples. A função de hash é calculada
para os dados do remetente, e o valor da hash é enviado juntamente com os dados. O receptor
43
calcula de novo a hash dos dados que recebeu. Se os valores não forem iguais, houve um erro na
transmissão de dados. Isto é chamado de redundancy check.
As classes Java utilizadas para implementar o algoritmo de hash são MessageDigest, que por sua
vez contem o método getInstance() onde é referido que algoritmo é utilizado, e neste caso foi
escolhido o algoritmo SHA-1, pois é suficiente para garantir que os erros são detectados na recepção.
Assim, a hash é calculada passando por argumento para o método update(), os dados sobre os quais
se quer calcular, e por fim, utilizando o método digest() que retorna um vector de bytes contendo a
hash.
Na altura do registo de cada máquina no sistema é criado um par de chaves: uma pública e uma
privada. Estas chaves ficam relacionadas com a máquina. Para criar estas chaves é utilizada a classe
KeyPairGenerator. Sempre que essa máquina precisar de enviar ou receber dados vai utilizar as
chaves previamente criadas. No momento em que os dados são codificados, é utilizada a chave
privada da máquina utilizada em conjunto com a assinatura digital através da classe Signature.
No caso de uma máquina receber dados de outra e os deseja descodificar vai utilizar para este fim
a chave pública que a outra máquina lhe disponibiliza. Em primeiro lugar a assinatura é verificada de
forma a garantir a imunidade a alterações nas comunicações, e de seguida a descodificação da hash
é feita de forma inversa à codificação, ou seja é calculada de novo a hash dos dados, e comparada
com a hash que foi transmitida a acompanhar os mesmos dados. Se ambas forem iguais, significa
que os dados foram transmitidos sem perdas e sem erros.
Para obter a chave pública que lhe permite descodificar a assinatura digital do ficheiro transmitido,
a máquina que recebe os dados vai pedir ao Administrador que lhe envie a sua chave pública, para
verificar os dados que acabou de receber do mesmo, e a chave pública da outra máquina com quem
vai comunicar. Depois em cada verificação vai usar a chave pública correspondente.
O envio da chave pública é feito através de um procedimento remoto, ou seja implica uma nova
ligação TCP feita pelo RMI.
A criptografia é necessária neste trabalho para garantir, com elevado grau de certeza, que as
mensagens não são adulteradas, intencionalmente ou não, durante as comunicações.
44
6. Resultados de Testes
Em anexo (Anexo I) estão representados os resultados de um teste realizado de forma a
demonstrar que o sistema detecta erros. Para isso foi submetido um programa de teste que varia do
utilizado anteriormente no facto de escolher aleatoriamente um valor entre 0 e 100, e se esse valor for
superior a um valor padrão, que neste caso foi 95, o output do programa é diferente do que em caso
contrário. O objectivo desta alteração é forçar o programa a ter respostas diferentes, ou seja, a
simular erros na execução.
Assim, para este teste foi executado este programa de teste com 100 inputs com o valor de 10, ou
seja para calcular o factorial de 10. Foram enviados 100 inputs de forma a poder ter uma amostra
grande de resultados e poder tirar conclusões estatísticas viáveis.
De notar que aparecem resultados errados repetidos, isto deve-se ao facto de que, em caso de
erro, os ficheiros errados são ambos retornados à máquina de submissão. Consegue-se concluir que
de entre 100 execuções, 15 delas revelaram-se erradas, correspondendo a 15% das execuções
totais. Tendo em conta que a fronteira que ditava as diferenças nas execuções se situava em 95,
pode-se concluir que uma percentagem de 15% de erros é um valor esperado aceitável, tendo em
conta que a função que gera número aleatórios gera valores aproximadamente identicamente
distribuídos.
De forma a garantir que todas as tarefas são cumpridas com sucesso, torna-se importante o teste
das funcionalidades do sistema. Devido à escassez de meios, pois um sistema destes exige diversas
máquinas independentes de modo a que a replicação implementada não pese em demasia na
execução geral, não foi possível testar a escalabilidade do sistema, de forma definitiva, ou seja,
apenas se pode tirar conclusões sobre esta matéria devido à submissão e respectiva execução de
programas com múltiplos inputs, enviados pela mesma máquina. Esta mesma escassez implicou a
utilização de programas que criam ambientes virtuais de utilização, dentro da mesma máquina,
simulando assim, diversas máquinas dentro de uma só. Um exemplo de software com estas
características é o VMware.
Para testar a escalabilidade, o tempo de execução, e a variação do tempo de execução com o
número de inputs, foi decidido fazer testes com um ficheiro padrão, um executável Java, que tem a
vantagem de poder correr em qualquer sistema operativo que tenha o Java Virtual Machine presente
(neste caso, como o trabalho é desenvolvido em Java, todas as máquinas têm o JVM presente). O
programa consiste no cálculo do factorial de um valor, que é dado pelo STDIN, e depois escreve para
o STDOUT o valor correspondente. O factorial só pode ser calculado entre 0 e 20, por motivos
matemáticos e computacionais, pois não existem factoriais de valores negativos, e o factorial de
valores superiores a 20 pode causar overflow.
Assim, para testar também os diversos níveis de processamento na execução destes programas,
os testes foram feitos para três valores de factoriais, 5, 10 e 15. Estes valores foram escolhidos por
45
compreenderem uma complexidade de processamento diferente entre eles, nalguns casos
consideráveis.
Depois, para testar várias cargas do sistema, para cada processamento, decidiu-se introduzir
ficheiros com 25 valores, com 50 valores e com 100 valores.
Por fim foi realizado um teste para testar a detecção de erros, provocados de forma propositada,
de forma a provar que este sistema detecta erros aleatórios.
De forma a obter resultados mais verosímeis seria necessário, testar a performance e a
capacidade da aplicação desenvolvida em cumprir os objectivos propostos, num cluster e/ou grid
reais. Dessa forma a análise dos resultados estaria mais próxima da realidade.
Figura 14: Gráfico das execuções de um ficheiro Java, para 25 inputs
Como é possível observar na Figura 14, o tempo de execução aumenta para qualquer valor de
input.
Sem surpresas, o factorial de 15, processo mais pesado a nível de computação, é aquele que
demora mais tempo na execução.
Pode-se observar que para todos há um aumento do tempo de execução, mas de notar que, numa
fase mais estável dos tempos de execução, eles estão compreendidos entre os valores de
aproximadamente 9000 ms e 22500 ms.
46
Figura 15: Gráfico das execuções de um ficheiro Java, para 50 inputs
Para uma carga superior, o aumento do tempo de execução aumenta consideravelmente, pois a
máquina que submete tem de tratar das threads todas da execução, neste caso 50, e isto
naturalmente que faz aumentar os tempos de execução. De notar que com este nível de carga a
diferença da computação dos valores, não produz efeitos, ou seja, como se pode observar na Figura
15, aquele que tem uma computação mais simples, é o que precisa de mais tempo de execução.
Como se pode observar, para a fase mais estável das execuções, o tempo de execução situa-se
entre aproximadamente 25000 ms e 36000 ms.
47
Figura 16: Gráfico das execuções de um ficheiro Java, para 100 inputs
Na Figura 16 está representado o último teste relacionado com o ficheiro Java. Neste teste foram
executados programas com 100 inputs, o que na prática é equivalente a dizer-se que foram
executados 100 programas em paralelo. Neste caso é possível observar aquilo que já vinha sendo
observável nos gráficos anteriores, é que quanto maior for a carga, maior é o tempo de execução. No
entanto em certas alturas apresenta características de constância, ou seja, não varia muito de certos
valores. E nestas condições as execuções tomam variações temporais um pouco imprevisíveis.
Assim, nessa fase mais constante os tempos variam aproximadamente entre 45000 ms e 70000 ms.
De notar que a variação para a carga de 25 é de entre 9000 ms e 22500 ms, para a carga de 50 é
de entre 25000 ms e 36000 ms e para a carga de 100 é de entre 45000 ms e 70000 ms. Ou seja,
existe um aumento significativo no tempo de execução.
As variações dos tempos de execução podem ser devidas a outros processos a correr na mesma
máquina. Um dos processos que pode perturbar a execução é o garbage collector do Java que pode
ocupar algum tempo de processamento. Em todo o caso o objectivo é ter uma noção da ordem de
grandeza da carga que este sistema causa pelo que essas variações podem ser ignoradas.
Factorial(5) Factorial(10) Factorial(15)
655.1 ms 871.2 ms 968.4 ms Média
Tabela 1: Tempos de execução do ficheiro de teste, em execução local
Na Tabela 1 estão os tempos de execução do ficheiro de teste para os inputs de teste. Estes
tempos foram determinados em execução local, ou seja, sem serem submetidos ao programa
48
desenvolvido para este trabalho. Foram calculadas as médias e as medianas dos tempos
determinados, de forma a avaliar de um modo mais rigoroso os tempos de execução.
Como o ficheiro de teste é muito simples, e a execução é também muito simples, e por isso os
tempos de execução são bastante baixos. Nota-se no entanto que há um acréscimo nos tempos de
execução conforme se aumenta o valor do input. Recorda-se que este programa de teste calcula um
factorial.
Factorial(5) Factorial(10) Factorial(15)
1305.8 ms 1196.7 ms 1076.4 ms média
Tabela 2: Tempos de execução do ficheiro de teste em execução remota
Submetendo o ficheiro de teste a uma execução remota, e se essa execução for feita sem que
haja uma carga elevada no sistema, os tempos de execução resultantes são os representados na
Tabela 2. Como se pode observar, há um aumento nos tempos de execução, embora esse aumento
seja mais notado nos valores de input mais baixos. Assim, há aumento temporal médio de 49,8%
para o Factorial(5), de 27,2% para o Factorial(10) e de 10% para o Factorial(15).
Assim, pode-se concluir que o acréscimo temporal da execução remota, não é muito elevado.
Para uma computação baixa, há um aumento relativo significativo, mas como o tempo de execução
local por si só corresponde a um valor baixo, o acréscimo da execução em rede é praticamente
imperceptível.
Média Factorial(5) Factorial(10) Factorial(15)
25 13442.84 ms 12523.52 ms 16230.96 ms
50 33558.3 ms 28327.02 ms 29484.82 ms
100 57824.68 ms 55908.34 ms 53089.36 ms
Tabela 3: Tempos de execução média e mediana com diferentes cargas no sistema
Na Tabela 3 estão representadas as médias e medianas dos tempos de execução para diferentes
cargas. Como já foi observado nos gráficos, os tempos de execução aumentam com a carga.
Estes testes foram realizados utilizando 3 computadores ligados em rede. Deste modo foi possível
testar a escalabilidade por parte da máquina de submissão, e por parte da máquina de execução,
pois a máquina de submissão enviava sempre os dados a executar para as mesmas duas máquinas.
Assim, algumas variações temporais presentes podem ser também resultado do estado dos vários
sistemas presentes.
49
7. Conclusão
Como foi realçado ao longo deste trabalho, os principais pilares para a construção de um sistema
distribuído imune a falhas bizantinas, assentam na redundância e na segurança da transferência de
dados. Para conseguir atingir estes objectivos, o algoritmo de Voltan revelou-se uma abordagem
interessante, pois aplica alguns destes conceitos, associado à criptografia e a algoritmos de hash,
torna-se numa abordagem robusta a este tipo de problemas.
Foi explicado em que medida as comunicações, e os protocolos de comunicações são importantes
para este tipo de sistemas. É importante garantir que os dados não são adulterados durante a sua
transferência, mas é igualmente importante que as transferências de dados sejam o mais rápidas
possível, é aqui que entram as comunicações através de UDP.
As comunicações através de TCP são igualmente importantes, mas utilizadas fundamentalmente
quando há uma necessidade de garantir as entregas de dados, pois essa é uma das principais
características do protocolo TCP. Por fim, as comunicações utilizando o RMI, são aquelas que estão
mais relacionadas com o conceito de sistemas distribuídos transparentes, pois permite a chamada de
métodos localizados em máquinas remotas.
Havia muitas possibilidades de melhorar as potencialidades de um sistema deste tipo.
Nomeadamente no que diz respeito ao escalonamento de trabalhos, à recolha de dados das
máquinas ligadas ao sistema, em suma, tudo o que diz respeito à gestão do sistema. De facto, a
possibilidade de utilizar melhor os recursos das diversas máquinas, como o espaço em disco e a
memória disponível, poderia elevar as capacidades de um sistema deste tipo, diminuindo o tempo de
execução remota, ou o impacto da carga no sistema.
Claro que um programa como o Condor, que tem funcionalidades ao nível de gestão da rede,
muito mais desenvolvidas, se estivesse embebido na base teoria do tipo de sistemas como o
abordado neste trabalho, ou seja, imune a falhas bizantinas, poderia ser muito mais eficiente que o
desenvolvido neste trabalho.
De realçar que a abordagem inicial prevista neste trabalho seria exactamente este tipo de
abordagem, ou seja embeber o Condor num ambiente imune a falhas bizantinas. Mas devido à
incapacidade de acesso ao código fonte, e igualmente o facto de os programadores responsáveis
pelo Condor não facilitarem esse acesso, a abordagem teve de divergir um pouco desta perspectiva
inicial.
Como já foi referido atrás, este sistema não tem a capacidade de responder a execuções com
não-determinismos, ou seja, chamadas ao relógio do sistema ou números aleatórios. Para conseguir
atingir esta capacidade seria necessário aceder ao código fonte do programa a executar, ou seja, em
vez de submeter programas executáveis, submeter programas com o acesso ao seu código fonte, e
compilá-los de seguida, podendo assim determinar em que pontos estão localizados os não-
determinismos, e possivelmente, contornar este problema transferindo para as máquinas remotas os
valores necessários para essa execução, ou seja transferir dados idênticos para todas as execuções.
50
O Java tem a capacidade de gerir a memória de uma forma mais amigável, ou seja, utilizando o
Garbage Collector, o Java detecta que dados estão em memória e já não são necessários, e apaga-
os dando espaço a outros dados e objectos, evitando assim memory leaks.
Assim, pode-se concluir que um sistema distribuído imune a falhas bizantinas pode não ter um
custo em termos de tempo de execução muito elevado se comportar mais e melhores funcionalidades
de gestão das capacidades das máquinas do sistema. No entanto este custo depende da carga
submetida e do número de máquinas presentes no sistema.
No entanto os ficheiros a submeter a este sistema não podem ser muito grandes nem o seu peso
em termos de computação. Pois ao todo a execução de cada ficheiro é composta por quatro
execuções, duas em cada máquina o que pode ocupar muito tempo de processador. Além do tempo
de execução há igualmente o tempo utilizado para transferências de dados, que conforme forem mais
dados se torna obviamente maior também.
Em casos em que o trabalho a realizar pelo sistema é composto pela execução de dados muito
grandes e complexos em termos de computação existe o risco de não haver benefícios numa
execução deste tipo, a menos que a necessidade de uma garantia de imunidade a erros seja
imperiosa.
51
Bibliografia [1] Tanenbaum, A.; Steen, M.(2002) Distributed Systems Principles and Paradigms. Prentice Hall.
[2] Condor Team, University of Wisconsin-Madison: Condor Version 6.8.4 Manual
[3] Black, D., Low, C.; Shrivastava, S., The Voltan Application Programming Environment for Fail-
Silent Processes, IEEE Trans. On Computers, Nov. 1996
[4] Brasileiro, F.; Ezhilchelvan, P.;Shrivastava, S.; Speirs, N.; Tao, S., Implementing Fail-Silent
Nodes for Distributed Systems, Distributed Systems Engineering, 5, June 1998, pp.66-77
[5] Eckel, B. ( 2003) Thinking in Java Edition. Prentice Hall
[6] Tanenbaum, A.(2002) Modern Operating Systems. Prentice Hall.
[7] Kurose, J.F.; Ross, K.W.; Computer Networking a top/down approach
52
53
Anexos
Anexo I
Nesta tabela estão representados os resultados dos testes realizados á aplicação. São
assinaladas as execuções certas e erradas. De notar que aparecem resultados errados repetidos, isto
deve-se ao facto de que, em caso de erro, os ficheiros errados são ambos retornados à máquina de
submissão.
Certos Errados Errados Repetidos
stdout_in59.txt certo 1 stdout_in8.txt certo 1
stdout_in65.txt certo 1 stdout_stdout_in77.txt errado 1
stdout_in77.txt errado 1 stdout_in39.txt certo 1
stdout_in3.txt errado 1 stdout_in38.txt certo 1
stdout_stdout_in78.txt errado 1 stdout_in45.txt certo 1
stdout_in10.txt certo 1 stdout_in51.txt certo 1
stdout_in69.txt certo 1 stdout_in83.txt certo 1
stdout_in71.txt certo 1 stdout_in27.txt certo 1
stdout_in95.txt certo 1 stdout_in74.txt certo 1
stdout_stdout_in49.txt errado 1 stdout_in86.txt certo 1
stdout_in34.txt certo 1 stdout_in48.txt certo 1
stdout_stdout_in49.txt errado 1 stdout_in41.txt certo 1
stdout_in9.txt certo 1 stdout_in81.txt certo 1
stdout_stdout_in94.txt errado 1 stdout_in64.txt certo 1
stdout_in94.txt errado 1 stdout_in15.txt certo 1
stdout_in43.txt certo 1
54
stdout_in90.txt certo 1
stdout_in18.txt certo 1 stdout_in58.txt certo 1
stdout_in98.txt certo 1 stdout_in53.txt errado 1
stdout_stdout_in53.txt errado 1 stdout_in68.txt certo 1
stdout_in96.txt certo 1 stdout_in84.txt certo 1
stdout_in88.txt certo 1 stdout_in1.txt certo 1
stdout_in89.txt certo 1 stdout_in91.txt errado 1
stdout_in55.txt certo 1 stdout_in30.txt certo 1
stdout_in35.txt certo 1 stdout_in85.txt certo 1
stdout_in6.txt errado 1 stdout_in57.txt certo 1
stdout_in11.txt certo 1 stdout_in25.txt certo 1
stdout_in0.txt certo 1 stdout_in20.txt certo 1
stdout_in29.txt certo 1 stdout_in97.txt certo 1
stdout_stdout_in13.txt errado 1 stdout_in13.txt errado 1
stdout_in75.txt certo 1 stdout_stdout_in61.txt errado 1
stdout_in61.txt errado 1 stdout_in44.txt certo 1
stdout_in28.txt certo 1 stdout_in21.txt certo 1
stdout_in70.txt certo 1 stdout_stdout_in5.txt errado 1
stdout_in47.txt certo 1 stdout_in40.txt certo 1
stdout_in14.txt certo 1 stdout_in79.txt certo 1
stdout_in66.txt certo 1 stdout_in5.txt errado 1
stdout_stdout_in6.txt errado 1 stdout_stdout_in91.txt errado 1
stdout_in54.txt certo 1
55
stdout_in93.txt errado 1
stdout_in63.txt certo 1 stdout_in93.txt errado 1
stdout_in52.txt certo 1 stdout_in32.txt certo 1
stdout_in60.txt certo 1 stdout_in56.txt certo 1
stdout_in87.txt certo 1 stdout_in99.txt errado 1
stdout_in92.txt errado 1 stdout_in72.txt certo 1
stdout_in26.txt certo 1 stdout_in31.txt certo 1
stdout_in80.txt certo 1 stdout_in46.txt certo 1
stdout_in16.txt certo 1 stdout_in24.txt certo 1
stdout_in7.txt certo 1 stdout_in73.txt certo 1
stdout_in22.txt certo 1 stdout_in67.txt certo 1
stdout_in42.txt certo 1 stdout_in19.txt certo 1
stdout_in4.txt certo 1 stdout_in37.txt certo 1
stdout_in78.txt errado 1 stdout_in36.txt certo 1
stdout_in2.txt certo 1 stdout_in50.txt certo 1
stdout_in17.txt certo 1 stdout_in23.txt certo 1
stdout_in12.txt certo 1 stdout_stdout_in3.txt errado 1
stdout_in76.txt certo 1 stdout_in99.txt errado 1
stdout_in92.txt errado 1 stdout_in82.txt certo 1
stdout_stdout_in33.txt errado 1 stdout_in33.txt errado 1
stdout_in62.txt certo 1 85 15 15
Recommended