sistema distribuído imune a falhas bizantinas engenharia

64
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

Upload: trinhminh

Post on 09-Jan-2017

218 views

Category:

Documents


1 download

TRANSCRIPT

Page 1: Sistema Distribuído Imune a Falhas Bizantinas Engenharia

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

Page 2: Sistema Distribuído Imune a Falhas Bizantinas Engenharia

I

Agradecimentos

 

Quero  deixar  uma  palavra  de  agradecimento  ao  Prof.  João  Paulo B.  Carvalho  pelo  apoio  na 

realização deste trabalho. 

Page 3: Sistema Distribuído Imune a Falhas Bizantinas Engenharia

II

Page 4: Sistema Distribuído Imune a Falhas Bizantinas Engenharia

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

Page 5: Sistema Distribuído Imune a Falhas Bizantinas Engenharia

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

Page 6: Sistema Distribuído Imune a Falhas Bizantinas Engenharia

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 

Page 7: Sistema Distribuído Imune a Falhas Bizantinas Engenharia

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 

Page 8: Sistema Distribuído Imune a Falhas Bizantinas Engenharia

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 

Page 9: Sistema Distribuído Imune a Falhas Bizantinas Engenharia

VIII

Page 10: Sistema Distribuído Imune a Falhas Bizantinas Engenharia

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

Page 11: Sistema Distribuído Imune a Falhas Bizantinas Engenharia

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.

Page 12: Sistema Distribuído Imune a Falhas Bizantinas Engenharia

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

Page 13: Sistema Distribuído Imune a Falhas Bizantinas Engenharia

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.

Page 14: Sistema Distribuído Imune a Falhas Bizantinas Engenharia

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.

Page 15: Sistema Distribuído Imune a Falhas Bizantinas Engenharia

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.

Page 16: Sistema Distribuído Imune a Falhas Bizantinas Engenharia

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.

Page 17: Sistema Distribuído Imune a Falhas Bizantinas Engenharia

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.

Page 18: Sistema Distribuído Imune a Falhas Bizantinas Engenharia

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.

Page 19: Sistema Distribuído Imune a Falhas Bizantinas Engenharia

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.

Page 20: Sistema Distribuído Imune a Falhas Bizantinas Engenharia

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

Page 21: Sistema Distribuído Imune a Falhas Bizantinas Engenharia

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.

Page 22: Sistema Distribuído Imune a Falhas Bizantinas Engenharia

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]

Page 23: Sistema Distribuído Imune a Falhas Bizantinas Engenharia

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

Page 24: Sistema Distribuído Imune a Falhas Bizantinas Engenharia

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

Page 25: Sistema Distribuído Imune a Falhas Bizantinas Engenharia

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.

Page 26: Sistema Distribuído Imune a Falhas Bizantinas Engenharia

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]

Page 27: Sistema Distribuído Imune a Falhas Bizantinas Engenharia

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.

Page 28: Sistema Distribuído Imune a Falhas Bizantinas Engenharia

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.

Page 29: Sistema Distribuído Imune a Falhas Bizantinas Engenharia

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

Page 30: Sistema Distribuído Imune a Falhas Bizantinas Engenharia

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]

Page 31: Sistema Distribuído Imune a Falhas Bizantinas Engenharia

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.

Page 32: Sistema Distribuído Imune a Falhas Bizantinas Engenharia

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.

Page 33: Sistema Distribuído Imune a Falhas Bizantinas Engenharia

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.

.

Page 34: Sistema Distribuído Imune a Falhas Bizantinas Engenharia

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.

   

Page 35: Sistema Distribuído Imune a Falhas Bizantinas Engenharia

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:

Page 36: Sistema Distribuído Imune a Falhas Bizantinas Engenharia

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

Page 37: Sistema Distribuído Imune a Falhas Bizantinas Engenharia

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.

Page 38: Sistema Distribuído Imune a Falhas Bizantinas Engenharia

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.

Page 39: Sistema Distribuído Imune a Falhas Bizantinas Engenharia

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.

Page 40: Sistema Distribuído Imune a Falhas Bizantinas Engenharia

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.

Page 41: Sistema Distribuído Imune a Falhas Bizantinas Engenharia

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.

Page 42: Sistema Distribuído Imune a Falhas Bizantinas Engenharia

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

Page 43: Sistema Distribuído Imune a Falhas Bizantinas Engenharia

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.

Page 44: Sistema Distribuído Imune a Falhas Bizantinas Engenharia

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.

Page 45: Sistema Distribuído Imune a Falhas Bizantinas Engenharia

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.

Page 46: Sistema Distribuído Imune a Falhas Bizantinas Engenharia

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

Page 47: Sistema Distribuído Imune a Falhas Bizantinas Engenharia

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

Page 48: Sistema Distribuído Imune a Falhas Bizantinas Engenharia

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.

Page 49: Sistema Distribuído Imune a Falhas Bizantinas Engenharia

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.

Page 50: Sistema Distribuído Imune a Falhas Bizantinas Engenharia

41

Figura 13: Comunicação entre máquinas diferentes e entre processos Leader e Follower

Page 51: Sistema Distribuído Imune a Falhas Bizantinas Engenharia

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

Page 52: Sistema Distribuído Imune a Falhas Bizantinas Engenharia

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.

Page 53: Sistema Distribuído Imune a Falhas Bizantinas Engenharia

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

Page 54: Sistema Distribuído Imune a Falhas Bizantinas Engenharia

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.

Page 55: Sistema Distribuído Imune a Falhas Bizantinas Engenharia

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.

Page 56: Sistema Distribuído Imune a Falhas Bizantinas Engenharia

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

Page 57: Sistema Distribuído Imune a Falhas Bizantinas Engenharia

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.

Page 58: Sistema Distribuído Imune a Falhas Bizantinas Engenharia

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.

Page 59: Sistema Distribuído Imune a Falhas Bizantinas Engenharia

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.

Page 60: Sistema Distribuído Imune a Falhas Bizantinas Engenharia

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

Page 61: Sistema Distribuído Imune a Falhas Bizantinas Engenharia

52

Page 62: Sistema Distribuído Imune a Falhas Bizantinas Engenharia

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    

Page 63: Sistema Distribuído Imune a Falhas Bizantinas Engenharia

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    

Page 64: Sistema Distribuído Imune a Falhas Bizantinas Engenharia

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