Serviços para Tolerância a Faltas no Ambiente Operacional Seljuk-Amoeba Sheila Regine A. Vasconcelos [email protected] Francisco Vilar Brasileiro [email protected] Universidade Federal da Paraíba - UFPB/Campus II Centro de Ciências e Tecnologia - CCT Departamento de Sistemas e Computação - DSC Laboratório de Sistemas Distribuídos - LSD Av. Aprígio Veloso, 882 58109-970, Campina Grande, Paraíba http://www.dsc.ufpb.br/~lsd RESUMO A obtenção de confiança no funcionamento em aplicações distribuídas depende da utilização de mecanismos apropriados para tolerância a faltas. A arquitetura Seljuk se propõe a fornecer um ambiente propício ao desenvolvimento e à execução de aplicações distribuídas robustas. Os principais serviços para tolerância a faltas usados por aplicações distribuídas robustas são discutidos, e a forma como estes serviços são fornecidos pelo ambiente operacional Seljuk-Amoeba é descrita. ABSTRACT Fault tolerance mechanisms must be introduced into systems if a dependable behaviour is to be attained. The Seljuk architecture provides a number of services to facilitate the implementation and support the execution of dependable distributed applications. This paper discusses the main fault tolerance services used by dependable distributed applications and how these services are provided by the Seljuk-Amoeba operating environment. 1. INTRODUÇÃO A sociedade moderna está cada vez mais dependente dos sistemas de computação para a realização de tarefas no seu dia-a-dia. Muitos destes sistemas fornecem a base para serviços críticos, como transações financeiras, dispositivos de segurança, controle de tráfego aéreo, monitoração de pacientes, entre outros, cuja falha pode levar a uma catástrofe com graves conseqüências como a perda de dinheiro ou, o que é pior, de vidas humanas. Este crescimento da dependência da sociedade no correto funcionamento dos sistemas computacionais, ao mesmo tempo que aumenta as oportunidades para os desenvolvedores de tais sistemas, aumenta também a responsabilidade desta comunidade de profissionais no sentido de encontrar maneiras cada vez mais eficientes de atender às expectativas da sociedade. Para conseguir isto, é necessário lançar mão de técnicas especiais que viabilizem a construção de sistemas com alto nível de confiança no funcionamento1. Neste cenário, despontam as técnicas 1 Confiança no funcionamento é o termo sugerido por [Lemos-Veríssimo 91] como tradução para o termo em inglês dependability introduzido em [Laprie 89]. para tolerância a faltas2, cuja meta é possibilitar um sistema a prover o serviço adequado mesmo na presença de um certo número de faltas; ou seja, tais técnicas procuram evitar a falha do sistema como um todo apesar da ocorrência de falhas em alguns dos seus componentes. Diversos mecanismos vêm sendo desenvolvidos para garantir tolerância a faltas sob uma variedade de ambientes. Porém, na grande maioria dos casos, a implementação destes mecanismos fica a cargo da aplicação, com pouco ou nenhum suporte do sistema operacional, o que aumenta consideravelmente a complexidade do desenvolvimento de tais aplicações. De fato, um dos principais problemas encontrados no desenvolvimento de aplicações distribuídas robustas (aquelas com requisitos consideráveis de confiança no funcionamento) reside justamente na dificuldade introduzida pela necessidade de se tolerar e tratar faltas [Cristian 91]. Uma possível maneira de reduzir esta complexidade é disponibilizar os mecanismos para tolerância a faltas sob a forma de serviços, que poderiam ser utilizados diretamente pela aplicação sem que esta tivesse que se preocupar com sua implementação [Mullender et al. 90, Ng 90, Huang-Kintala 93]. Dentre os diversos serviços para tolerância a faltas, destacam-se: replicação do processamento, comunicação em grupo, diagnóstico de faltas e reconfiguração do sistema. Tais serviços, por sua vez, são mais facilmente implementados se for possível impor alguma restrição à semântica de falha dos componentes do sistema que formam a infra-estrutura de processamento. A arquitetura Seljuk [Brasileiro 97] define um ambiente operacional para desenvolvimento e execução de aplicações distribuídas tolerantes a faltas, que se propõe a reduzir a complexidade do desenvolvimento destas aplicações, atacando as duas vertentes acima colocadas: ele tanto provê mecanismos que possibilitam restringir a semântica de falha da infra-estrutura de execução das aplicações quanto disponibiliza serviços de apoio à construção de aplicações robustas. Além disso, o Seljuk se apresenta como uma arquitetura flexível que oferece ao usuário a opção de obter níveis especificáveis de confiança no funcionamento na base de “serviço por serviço”, considerando o fato que muitas aplicações não requerem alta confiabilidade e disponibilidade de todos os seus serviços e nem sempre podem admitir o custo envolvido na utilização de mecanismos especiais para tolerância a faltas. Este artigo descreve uma proposta de como os serviços básicos para tolerância a faltas podem ser oferecidos à aplicação pela arquitetura Seljuk, com o máximo de transparência possível, permitindo que os desenvolvedores de sistemas se libertem da obrigação de construir, eles próprios, os mecanismos para tolerância a faltas e possam voltar seus esforços para a implementação do real serviço que o sistema deve fornecer, assim facilitando e minimizando o seu trabalho. Os serviços são especificados no contexto do ambiente operacional Seljuk-Amoeba, que é uma instanciação da arquitetura Seljuk baseada no micro-núcleo distribuído Amoeba [Mullender et al. 90]. O restante deste texto está organizado da seguinte forma. Na Seção 2, visando tornar o artigo auto-contido, fazemos uma breve apresentação da arquitetura Seljuk, enfatizando os seus objetivos e a sua estrutura. A Seção 3 discute aspectos de tolerância a faltas em sistemas distribuído, identificando os principais serviços comumente usados na contrução e execução de aplicações distribuídas robustas. A proposta para implementação de tais serviços no ambiente Seljuk-Amoeba é discutida na Seção 4. Por fim, a Seção 5 traz as nossas conclusões. 2. 2 A ARQUITETURA SELJUK De acordo com a terminologia proposta por [Lemos-Veríssimo 91], os termos falta, erro e falha traduzem, respectivamente, os termos em inglês fault, error e failure. 2.1. Objetivos A arquitetura Seljuk tem como objetivo principal prover uma plataforma de desenvolvimento que facilite a construção de aplicações distribuídas robustas através da disponibilização de duas classes de serviços: i) serviços que permitem restringir a semântica de falha dos componentes que formam a infra-estrutura de processamento sobre a qual a aplicação irá executar; e ii) serviços que implementam, ou auxiliam a implementação, dos principais mecanismos para tolerância a faltas. A primeira classe de serviços é responsável pela disponibilização de serviços de processamento com diferentes semânticas de falha, que possam satisfazer a suposição estabelecida pela aplicação sobre o modo de falha dos componentes do sistema distribuído. A definição da semântica de falha assumida é feita pela aplicação no instante de sua ativação. A plataforma fica então responsável por prover os mecanismos necessários para garantir tal semântica, considerando obviamente as limitações do hardware disponível (processadores e canais de comunicação) e a semântica de falha real deste hardware (que também é definida pela aplicação). Tais serviços são discutidos detalhadamente em [Brasileiro 97] e [Gallindo-Brasileiro 97]. A segunda classe, por sua vez, inclui os serviços de mais alto nível que provêem o suporte básico para a construção de aplicações distribuídas robustas, quais sejam: replicação do processamento, comunicação em grupo, diagnóstico de faltas e reconfiguração do sistema. A implementação destes serviços deve levar em conta a semântica de falha assumida pela aplicação, requisitando ao próprio ambiente operacional do Seljuk os serviços de processamento que garantam tal semântica. Neste caso, tal requisição é feita no instante da ativação do serviço solicitado pela aplicação. 2.2. Estrutura A arquitetura Seljuk segue um modelo organizado em camadas, conforme ilustra a Figura 1. Aplicações Middleware Sistema Operacional Distribuído Processadores e Canais de Comunicação Figura 1: Estrutura do Ambiente Operacional Seljuk A camada mais baixa da arquitetura, denominada “Processadores e Canais de Comunicação”, abriga os componentes de hardware que fornecem os serviços básicos de processamento e comunicação. A camada seguinte é constituída pelo “Sistema Operacional Distribuído”, responsável por controlar e gerenciar os diversos componentes do sistema, considerando principalmente a distribuição de tarefas pelos vários processadores e as necessidades de comunicação decorrentes disto. É nesta camada onde estão implementados os serviços que garantem a semântica de falha requerida pela aplicação e alguns dos serviços necessários para a implementação dos mecanismos para tolerância a faltas. A camada intermediária, denominada “Middleware”, implementa os demais serviços para tolerância a faltas, tirando proveito dos serviços fornecidos pelas camadas inferiores. Finalmente, a camada superior agrupa os componentes que implementam o serviço oferecido pela aplicação propriamente dita. 3. TOLERÂNCIA A FALTAS EM SISTEMAS DISTRIBUÍDOS Tolerância a faltas em sistemas distribuídos é realizada ao longo de várias fases relacionadas tanto ao processamento do erro quanto ao tratamento da falta. Enquanto o processamento do erro tem por objetivo a remoção de erros do estado computacional, se possível antes da ocorrência de uma falha no serviço entregue pelo sistema, o tratamento da falta tenta prevenir que as faltas que geraram tais erros venham a ser ativadas novamente [Lee-Anderson 90]. 3.1. Processamento do Erro A replicação de componentes de software individuais em unidades de processamento distintas de um sistema distribuído provê a redundância que é necessária para o processamento do erro. Neste contexto, o processamento do erro envolve o gerenciamento das interações entre as várias réplicas do componente de software para detecção, recuperação ou compensação do erro a fim de mascarar o fato de que um ou mais componentes do sistema possam ter falhado. Três modelos básicos de computação replicada estão disponíveis: a) Modelo de Réplicas Ativas. Neste modelo, todas as réplicas processam concorrentemente e na mesma ordem todas as mensagens de entrada de modo que seus estados são sincronizados e, na ausência de faltas, todas elas produzem as mesmas mensagens de saída e na mesma ordem. Isto requer que as réplicas apresentem comportamento determinístico na ausência de faltas. A técnica de replicação ativa pode ser usada para tolerar faltas do serviço de processamento ou do próprio componente de software3 sob suposições tanto de falha silenciosa quanto de falha arbitrária (Bizantina). A fim de tolerar falhas arbitrárias, as saídas de todas as réplicas são comparadas e a decisão majoritária é usada. Se, por outro lado, os componentes do sistema possuem semântica de falha silenciosa, a replicação ativa pode ser usada sem votação, uma vez que toda mensagem produzida por qualquer uma das réplicas pode ser assumida como correta. Como resultado, os requisitos do sistema de comunicação são simplificados e melhor desempenho é alcançado, já que os resultados podem ser propagados imediatamente após terem sido gerados em vez de ficarem pendentes esperando o processo de votação. b) Modelo de Réplicas Passivas. Nesta abordagem, uma das réplicas (a réplica primária) processa as mensagens de entrada e provê as mensagens de saída. Os estados internos das demais réplicas (as réplicas backup) são regularmente atualizados por meio de checkpoints da réplica primária. Se a réplica primária (ou a unidade de processamento na qual ela executa) falha, uma das réplicas backups é ativada e começa a executar a partir de seu checkpoint mais recente. Para muitas aplicações, a principal vantagem desta técnica é que ela não requer que as réplicas sejam determinísticas. Além do mais, os requisitos de processamento são minimizados: o processamento de checkpoints geralmente requer menos recursos 3 Desde que métodos para tolerância a faltas de projeto, tais como blocos de recuperação [Randell 75] ou programação com N-versões [Avizienis 85], tenham sido usados na implementação das diferentes réplicas do componente de software. do que a execução replicada. Como a técnica de replicação passiva não possui qualquer mecanismo para validar as mensagens de saída da réplica primária, esta abordagem assume semântica de falha silenciosa tanto para o componente de software replicado quanto para a infra-estrutura de processamento. c) Modelo de Réplicas Semi-ativas. Esta técnica pode ser vista como um híbrido das duas técnicas anteriores. Apenas uma das réplicas (a réplica líder) processa as mensagens de entrada e provê as mensagens de saída. Os estados internos das demais réplicas (as réplicas seguidoras) são atualizados por processamento direto das mensagens de entrada ou por processamento das mensagens de notificação enviadas pela líder. As decisões sobre operações que afetam o determinismo da réplica e, possivelmente, sobre a ordem de processamento das mensagens de entrada são tomadas pela líder e comunicadas às seguidoras através de mensagens de notificação. A suposição de falha silenciosa, assumida por este modelo, garante que as mensagens propagadas pela líder são sempre corretas. Em caso de falha da réplica líder, um algoritmo de eleição é executado para escolher uma nova líder entre as secundárias. A Tabela 1 apresenta os parâmetros a serem considerados no momento da escolha entre uma das técnicas apresentadas acima [Powell 92]. Técnica de replicação Overhead do processamento de erro Não-determinismo da réplica Comportamento em falha arbitrária Ativa Mais baixo Proibido Tolerado Passiva Mais alto Permitido Proibido Semi-ativa Baixo Resolvido Proibido Tabela 1. Principais Características das Técnicas de Replicação As técnicas de replicação descritas acima possuem diferentes requisitos quanto à comunicação entre as diversas réplicas do componente de software. Tais requisitos são satisfeitos por propriedades específicas do serviço de comunicação. Uma destas propriedades é a confiabilidade, que garante que a mensagem enviada para o grupo de réplicas é recebida por todos os destinos operacionais. Além do aspecto de confiabilidade, uma outra característica importante dos serviços de comunicação diz respeito à ordem de entrega das mensagens. Há basicamente quatro formas de ordenação de mensagens [Powell 92]: Nenhuma ordenação: a entrega de mensagens não obedece qualquer tipo de ordenação. Ordenação fifo (first-in-first-out): todas as mensagens enviadas por um transmissor são entregues a todos os destinos operacionais na ordem em que foram enviadas; nada se pode afirmar sobre a ordem das mensagens enviadas por transmissores distintos. Ordenação causal: mensagens não concorrentes [Lamport 78] são entregues a todos os destinos operacionais obedecendo suas relações de causalidade; se as mensagens são concorrentes, a ordem de entrega é indefinida. Ordenação total: todos os destinos operacionais recebem todas as mensagens na mesma ordem. O modelo de replicação ativa necessita de um serviço de comunicação que ofereça as propriedades de unanimidade4 e ordenação total, isto é, todas as réplicas corretas recebem as mesmas mensagens e na mesma ordem. Os serviços de comunicação em grupo [ChangMaxemchuk 84, Cristian et al. 85, Birman-Joseph 87, Brasileiro-Ezhichelvan 95] são normalmente usados para esse fim. Já no caso das outras duas técnicas de replicação, a passiva e a semi-ativa, há uma réplica privilegiada que pode realizar as operações de ordenação e instruir as demais réplicas. Neste caso, um protocolo de comunicação mais simples pode ser usado, provendo apenas unanimidade e nenhuma ordenação ou, no máximo, ordenação fifo. 3.2. Tratamento da Falta A replicação de componentes de software é a abordagem utilizada para processar os erros gerados por faltas no sistema. Se o erro é causado por uma falta temporária, a simples reinicialização do sistema a partir de um estado válido eliminará completamente o erro, visto que a falta não mais existe. Porém, se a falta é permanente, o erro reincidirá, já que a falta que o gerou permanece no sistema. A fim de evitar isto, o componente apresentando a falta deve ser identificado e não mais utilizado na computação subseqüente à recuperação do erro. O tratamento da falta, portanto, pode ser visto como a facilidade de autoconserto que identifica e desativa os componentes em falta e, dependendo da disponibilidade de recursos, cria novas réplicas, permitindo assim que o nível de tolerância a faltas seja mantido. O tratamento da falta envolve: diagnóstico da falta (para determinar a causa dos erros observados), apassivação da falta (para prevenir que faltas diagnosticadas sejam ativadas novamente) e, se possível, reconfiguração do sistema (para restaurar o nível de redundância do sistema) [Powell 92]. A atividade de diagnóstico da falta é necessária para: i) encontrar o componente que está apresentando a falta; e ii) decidir se a falta é permanente ou não. Caso seja detectado que a falta é permanente, a apassivação da falta deve ser executada5 e a reconfiguração do sistema, considerada. O diagnóstico é alcançado através da realização de testes de um componente do sistema sobre outros componentes. Qualquer algoritmo de diagnóstico deve idealmente satisfazer as seguintes propriedades [Shin-Ramanathan 87]: Corretude: todo componente diagnosticado pelo algoritmo como sendo incorreto é de fato incorreto; e Completude: todo componente incorreto no sistema é identificado pelo algoritmo. Além disso, um serviço de diagnóstico ideal deve permitir que todos os componentes corretos do sistema concordem sobre quais componentes apresentam falha, ou seja, cada componente correto deve chegar a um mesmo diagnóstico do sistema. Quando os componentes do sistema apresentam semântica de falha silenciosa, estes requisitos podem ser alcançados de forma razoavelmente simples. Por outro, se os componentes se comportam de forma arbitrária quando em falha, não se conhece nenhum protocolo que possa garantir um diagnóstico completo em todas as situações. Neste caso, o máximo que se pode conseguir é um diagnóstico parcial do sistema. Após a fase de diagnóstico da falta, os componentes diagnosticados como incorretos devem ser removidos do sistema. Entretanto, a exclusão destes componentes degrada o nível de redundância do sistema e, por conseguinte, a qualidade dos serviços para tolerância a faltas. Se o 4 A unanimidade é uma forma mais forte de confiabilidade que garante que qualquer mensagem entregue a um receptor, é entregue a todos os receptores corretos [Powell 92]. Ou seja, a mensagem enviada ou é entregue a todos os receptores corretos ou a nenhum deles. 5 As atividades de apassivação da falta geralmente são realizadas implicitamente na fase de reconfiguração do sistema. tempo de missão da aplicação não é conhecido ou se ainda está longe de se esgotar, faz-se necessário que o sistema seja reconfigurado a fim de restaurar o nível de redundância requerido pelos protocolos de processamento de erro, para garantir que o sistema continuará funcionando corretamente durante todo o seu tempo de missão, mesmo diante de faltas posteriores. A reconfiguração do sistema, porém, só pode ser considerada se há recursos redundantes suficientes, já que tal tarefa acarreta a realocação e reinicialização das réplicas que falharam ou que residiam em unidades de processamento que falharam. Quando faltas de projeto são consideradas e, portanto, as próprias réplicas do componente de software são passíveis de falha, a nova réplica pode ser ativada na mesma unidade de processamento daquela que falhou ou em qualquer outra unidade correta do sistema. Em ambos os casos, porém, deve-se considerar que a nova réplica precisa ter projeto e implementação diferentes da anterior. Quando a falha ocorre em uma unidade de processamento, algumas ou todas as réplicas que estavam executando naquela unidade passam a funcionar incorretamente ou simplesmente param de funcionar. Para que o grau de tolerância a faltas seja mantido de forma que faltas futuras possam ainda ser toleradas, tais réplicas precisam ser criadas novamente. Se a falta pode ser considerada temporária, e assim a simples reinicialização do estado da réplica é suficiente para trazê-la de volta ao funcionamento normal, ou se a manutenção corretiva da unidade de processamento que falhou foi realizada, a localização em que a nova réplica é criada pode ser a mesma em que a réplica original estava executando antes da falha. Se, por outro lado, a antiga localização da réplica não está disponível, qualquer outra localização pode ser usada, desde que esta faça parte do domínio de replicação6 do componente [Powell 92]. No caso de não haver recursos suficientes para a realocação de novas réplicas, alguns componentes de software terão de ser abandonados em favor de outros mais críticos ou, no mínimo, a operação tolerante a faltas será degradada. Na ausência de recursos disponíveis, portanto, a reconfiguração do sistema deve ser adiada até que alguma unidade se recupere (após a manutenção). Concluindo, a reconfiguração de um sistema é tal que o componente apresentando falta ou é abandonado ou é usado numa configuração diferente de forma que não conduza à reativação da falta e, conseqüentemente, da falha. O importante é que esta tarefa seja executada on-line e sem nenhuma intervenção manual. Além disso, ela deve ser dinâmica no sentido de que a redundância presente no sistema é usada para realizar a tarefa do componente incorreto [Jalote 94]. Com base nas considerações levantadas nas sub-seções acima, podemos identificar quatro serviços básicos para a construção de aplicações tolerantes a faltas: i) Replicação dos componentes de software; ii) Comunicação em grupo; iii) Diagnóstico de faltas; e iv) Reconfiguração do sistema. Ao se analisar a tecnologia atualmente disponível para a construção de aplicações distribuídas, observa-se que a maior parte destes serviços são implementados pela própria aplicação, com pouco ou nenhum suporte do sistema operacional. A construção de aplicações distribuídas robustas poderia ser bastante simplificada, se a aplicação pudesse utilizar diretamente estes serviços, sem precisar implementá-los. Ou seja, se a aplicação pudesse contar com um ambiente operacional que fornecesse tais serviços de forma transparente e flexível e com ônus apenas para aquelas aplicações que realmente fizessem uso deles. É a isto que se propõe o ambiente operacional Seljuk. 6 O domínio de replicação de um componente de software é o conjunto de localidades onde as réplicas de tal componente podem ser alocadas de modo que a consistência de processamento seja mantida. 4. SERVIÇOS PARA TOLERÂNCIA A FALTAS NO SELJUK-AMOEBA A implementação da arquitetura Seljuk será desenvolvida sobre sistemas operacionais baseados na tecnologia de micro-núcleo, cujo princípio básico é minimizar o tamanho do sistema operacional que executa em modo supervisor com o objetivo de aumentar a flexibilidade do sistema. Toda a funcionalidade do sistema operacional que não é provida pelo micro-núcleo fica a cargo de processos servidores que executam em modo usuário e, portanto, podem ser modificados/adaptados mais facilmente para atender às exigências das diferentes aplicações. Um dos micro-núcleos de maior destaque no meio acadêmico e científico, por sua disponibilidade tanto a nível de código fonte quanto de documentação, é o Amoeba [Mullender et al. 90], que, por este motivo, foi escolhido como o sistema hospedeiro para a primeira implementação do Seljuk, batizada de Seljuk-Amoeba. Nesta seção, descrevemos uma proposta para implementação de serviços para tolerância a faltas no ambiente operacional Seljuk-Amoeba. A implementação dos serviços para tolerância a faltas neste ambiente assume que os componentes do sistema possuem semântica de falha silenciosa. Para garantir tal semântica, estes serviços invocam o serviço de processamento confiável oferecido pelo próprio Seljuk-Amoeba. Este serviço é fornecido por um servidor denominado FT Run Server, cujos detalhes são apresentados em [Gallindo-Brasileiro 97]. Os servidores que implementam os serviços para tolerância a faltas são considerados livres de faltas (a realização desse pressuposto pode ser atingida através da utilização de um nodo com semântica de falha mascarada [Gallindo-Brasileiro 97] para executar os servidores). Como a implementação dos serviços de replicação, diagnóstico e reconfiguração se apóiam no serviço de comunicação, começaremos a proposta tratando deste serviço. 4.1. Serviço de Comunicação Confiável e em Grupo O sistema de comunicação do Amoeba é implementado no núcleo em duas camadas. A camada inferior implementa o Fast Local Internet Protocol (FLIP), que é um protocolo que opera em modo datagrama, não orientado a conexão. Comunicação confiável é fornecida pelo Amoeba pelos dois serviços da camada superior - comunicação em grupo e RPC (Remote Procedure Call), que utilizam o serviço não confiável fornecido pelo FLIP para enviar mensagens. Portanto, o Amoeba já oferece primitivas para comunicação em grupo que provêem entrega atômica de mensagens a todos os membros do grupo mesmo na presença de falhas dos canais de comunicação e, opcionalmente, dos processadores. Ou seja, a comunicação em grupo do Amoeba garante entrega confiável e ordenada de mensagens a todos os membros corretos do grupo. As primitivas de grupo provêem, portanto, uma abstração que permite os programadores construirem aplicações consistindo de um ou mais processos executando em diferentes máquinas. Todos os membros do grupo vêem todos os eventos relacionados ao seu grupo na mesma ordem, incluindo aqueles eventos de junção/remoção de um membro e de recuperação de falhas no grupo. As primitivas para gerenciamento de grupo e comunicação em grupo integradas ao Amoeba, bem como os seus algoritmos e medidas de desempenho, são descritas detalhadamente em [Kaashoek-Tanenbaum 94] e apresentadas resumidamente na Tabela 2 abaixo. Tais primitivas só podem ser chamadas por processos que fazem parte do grupo. Primitiva CreateGroup JoinGroup LeaveGroup SendToGroup ReceiveFromGroup Descrição Cria um novo grupo com as características definidas pelo chamador. Torna o chamador um membro do grupo. Remove o chamador do grupo. Envia atomicamente uma mensagem para todos os membros do grupo. Bloqueia o chamador até a chegada de uma mensagem. ResetGroup GetInfoGroup ForwardRequest Inicia a recuperação depois da falha de um membro do grupo. Retorna informações sobre o estado do grupo, tais como: número de membros e identificador do chamador no grupo. Repassa um pedido endereçado ao grupo para um outro membro do grupo. Tabela 2: Primitivas para comunicação em grupo do Amoeba A estrutura de grupos do Amoeba é fechada, significando que para um cliente ter acesso ao serviço provido por um grupo, ele deve fazer uma RPC com um dos membros do grupo. Este membro, por sua vez, usa comunicação em grupo para informar aos outros membros do seu grupo, de forma consistente, o serviço solicitado pelo cliente. No contexto do nosso trabalho, um grupo é formado por réplicas de um mesmo processo (digamos, um servidor) que cooperam para fornecer um serviço único correto. A cada grupo é associado um identificador único, denominado porta. Todos os membros do grupo que implementa aquele servidor conhecem a porta que ele escuta. O conceito de portas provê a transparência desejada para os clientes do serviço, que não precisam tomar conhecimento que o servidor que está atendendo seu pedido é replicado. Usando a comunicação em grupo nativa do Amoeba, baseada em grupos fechados, a interação dos clientes com o servidor replicado se daria da seguinte forma: o cliente que deseja se comunicar com o servidor faz uma RPC, indicando a porta do servidor. Apenas uma réplica está escutando efetivamente aquela porta e recebe o pedido, ficando encarregada de distribuí-lo para as demais réplicas. Isto, porém, requer que a aplicação implementando aquele servidor seja codificada usando as primitivas de comunicação em grupo (CreateGroup(), JoinGroup(), SendToGroup(), ReceiveFromGroup(), etc.). Isto vai de encontro ao nosso objetivo de tornar a replicação o mais transparente possível para o programador. Cabe então ao ambiente operacional Seljuk-Amoeba prover facilidades que atendam aos requisitos de comunicação e transparência, simultaneamente. Do mesmo modo que as primitivas nativas do Amoeba, as primitivas a serem acrescentadas pelo Seljuk-Amoeba deverão ser flexíveis, permitindo que se possa balancear desempenho contra requisitos de tolerância a faltas. Três novas primitivas foram identificadas: CreateRepGroup(), que permite a criação de um grupo de réplicas; JoinRepGroup() que permite que novos membros sejam adicionados a um grupo criado por CreateRepGroup(); e ResetRepGroup(), que permite a inicialização do processo de recuperação do grupo. Todas elas podem ser ativadas por um processo que não faz parte do grupo. Estas primitivas serão melhor compreendidas no decorrer deste texto. 4.2. Servidor de Replicação O serviço de replicação de processamento no Seljuk-Amoeba é oferecido por um servidor de replicação - o RepServer - implementado na camada Middleware da arquitetura Seljuk. A fim de criar um componente de software replicado, daqui por diante chamado de processo replicado, a seguinte invocação é feita ao RepServer, através de uma RPC: Replicate(file, rep-type, rep-degree, failure-semantics) Esta chamada solicita ao RepServer a criação de um processo replicado para executar o código contido em file. A técnica de replicação a ser usada é indicada pelo parâmetro rep-type; o grau de replicação (isto é, o número de réplicas a serem criadas) é determinado por rep-degree e a semântica de falha real considerada para os processadores do sistema é definida em failuresemantics. Se a semântica de falha definida para os processadores é menos restritiva do que a semântica de falha silenciosa (que é a exigida por esta implementação do RepServer), ao receber tal pedido, o RepServer deve requisitar ao FT Run Server, também via RPC, a criação de um nodo7 com semântica de falha silenciosa. Esta chamada, quando realizada com sucesso, retorna para o RepServer um descritor do nodo criado, que contém, entre outras informações, um identificador para o nodo e uma lista dos processadores que compõem o nodo. O FT Run Server cria nodos a partir de processadores que estejam com menor carga de processamento. O RepServer faz tantas chamadas ao FT Run Server quantas forem necessárias para criar um número de nodos que seja suficiente para executar todas as réplicas solicitadas, ou seja, repdegree chamadas são feitas. Em cada uma destas chamadas (exceto a primeira), o RepServer repassa para o FT Run Server uma lista com todos os processadores que já foram utilizados na composição dos nodos criados pelas chamadas anteriores. Isto evita que mais de uma réplica do mesmo processo sejam executadas em um mesmo processador, o que levaria a diminuição do grau de resiliência daquele processo, contrariando o nosso objetivo. Se todos os nodos forem criados com sucesso8, o RepServer pode proceder; caso contrário, um erro é retornado para o chamador. Quando a aplicação já considera uma semântica de falha silenciosa para a infra-estrutura de processamento, o serviço de criação de nodos não é necessário. Neste caso, o FT Run Server é invocado apenas para disponibilizar para a aplicação rep-degree processadores, selecionando, mais uma vez, aqueles que disponham de melhor condição para executar as réplicas do processo. O fato de uma réplica está sendo executada em um processador único ou em um nodo, que é um conjunto de processadores, é indiferente para o nosso nível de abstração, já que o ambiente Seljuk-Amoeba provê o serviço de processamento confiável, implementado pelo conceito de nodos, de forma transparente para os usuários deste serviço [Gallindo-Brasileiro 97]. Sob o ponto de vista do RepServer, o comportamento de um nodo é equivalente ao comportamento de um processador que possua a mesma semântica de falha implementada pelo nodo. Por questões de generalidade, daqui por diante, adotaremos o termo nodo para referenciar tanto um processador quanto um conjunto deles. Caso todos os nodos tenham sido alocados com sucesso, o RepServer solicita o serviço de processamento de cada um destes nodos para que ele crie um processo para executar o código contido em file. Feito isto, é necessário que se forme um grupo com todas as réplicas do processo para que elas possam interagir a fim de proverem um serviço único e confiável. Isto é feito pelo RepServer por meio de uma nova primitiva de gerenciamento de grupo oferecida pelo SeljukAmoeba: CreateRepGroup(port, member-list) Esta chamada cria um grupo composto pelos membros indicados em member-list, que deverão escutar todas as mensagens enviadas para a porta port, e retorna um identificador de grupo, utilizado nas chamadas posteriores. Além disso, ela cria um objeto de configuração para o grupo, que guarda as informações relativas à composição corrente do grupo, tais como: uma lista dos membros do grupo e uma lista dos nodos que hospedam estes membros. Note que este grupo só é visível para o processo que o criou (neste caso, o RepServer) e para o núcleo do sistema operacional. As ações realizadas daqui por diante dependem da técnica de replicação usada e, por este motivo, serão tratadas separadamente logo abaixo. Replicação Ativa Na replicação ativa, cada uma das réplicas processa todas as mensagens destinadas ao processo replicado e provê mensagens de saída. Ou seja, todas as réplicas executam num modo ativo. Desta forma, mensagens de saída duplicadas são inevitavelmente geradas. Porém, todas elas possuem o mesmo número de porta do transmissor e o mesmo número de sequência, podendo assim ser facilmente descartadas pelo núcleo da máquina receptora. 7 Nodos são as unidades de processamento fornecidas pelo FT Run Server com a semântica de falha requerida pela aplicação (no nosso caso, pelo servidor de replicação). 8 Um nodo pode não ser criado devido a restrições na disponibilidade de recursos. No Seljuk-Amoeba, qualquer aplicação distribuída que utilize apenas troca de mensagens na comunicação e que tenha um comportamento determinístico, pode ser replicada de forma ativa de maneira completamente transparente, ou seja, o ambiente disponibiliza suporte para execução replicada mesmo para aquelas aplicações que não foram projetadas para serem executadas de forma replicada (desde que elas atendam às restrições mencionadas acima). Replicação Passiva Na replicação passiva, apenas uma réplica (a primária) processa as mensagens de entrada e provê as mensagens de saída. As demais réplicas (as backups) são passivas visto que, na ausência de faltas, elas não executam qualquer processamento, exceto a atualização de seus estados internos. Um processo a ser replicado passivamente deve, portanto, ser implementado de forma que ele possa operar num modo ativo, executando efetivamente o processamento, ou num modo passivo, onde ele simplesmente atualiza seu estado. Durante sua execução, a réplica do processo pode reverter de modo passivo para ativo, o que significa que ela deixou de ser backup e passou a ser primária. A plataforma em estudo provê uma biblioteca de funções a serem usadas na implementação de processos seguindo os modelos de replicação passiva e semi-ativa. Dentre elas, temos a função Election(), que executa um algoritmo para decidir qual das réplicas do grupo será a primária. Esta função retorna um código indicando se a réplica que fez a chamada deve operar em modo ativo ou passivo (i.e., deve ser primária ou backup). Uma outra função importante é a InputLog(), utilizada pelas backups para guardar todas as mensagens de entrada recebidas pela primária. Tal função é necessária para que, quando uma réplica backup assume o papel de primária, ela não precise solicitar o reenvio das mensagens de entrada já recebidas (e possivelmente processadas) pela antiga réplica primária antes da sua falha, o que poderia conduzir ao chamado efeito dominó [Randell 75]. A atualização do estado das réplicas backups é feita através das funções Checkpoint() e GetCheckpoint(). A réplica primária chama a função Checkpoint() passando como parâmetro uma função de empacotamento, que é responsável por compor uma mensagem que reflita o estado interno atual da réplica primária. Esta mensagem é recebida pelas réplicas backups por meio da chamada GetCheckpoint(), que, por sua vez, tem como parâmetro uma função de desempacotamento capaz de decompor a mensagem recebida e atualizar o estado da réplica a partir da informação nela contida. As funções de empacotamento e desempacotamento são específicas da aplicação. Cada vez que uma operação de atualização de estado é executada com sucesso, as mensagens existentes no log de entrada podem ser descartadas, pois apenas as mensagens recebidas a partir deste ponto serão necessárias para a recuperação em caso de falha da réplica primária. Quando a falha de uma réplica primária é detectada, a função Election() é chamada para eleger, entre as réplicas backups, uma nova réplica primária. O modo de operação da réplica eleita reverte do modo passivo para o modo ativo e ela começa a executar a partir do último checkpoint, processando as mensagens guardadas no log. Isto provavelmente vai gerar algumas mensagens de saída idênticas àquelas já produzidas pela réplica primária anterior, porém tais mensagens são descartadas pelo destinatário, como explicado na replicação ativa. Replicação Semi-Ativa Na replicação semi-ativa, apesar de apenas uma das réplicas (a líder) produzir mensagens de saída, todas as outras réplicas (as seguidoras) também recebem e processam as mensagens de entrada. Este processamento é necessário para que as seguidoras atualizem seus estados internos e, em caso de falha da líder, possam assumir o seu papel logo após esta falha tenha sido detectada, com o mínimo de atraso possível. Como na replicação passiva, a função Election() deve ser executada no início do processamento para decidir qual réplica será a líder. Em caso de falha da réplica líder, uma nova líder é eleita entre as seguidoras, passando a disseminar mensagens para o mundo exterior. Como pode haver uma diferença de sincronização entre as réplicas, pode ocorrer que a réplica seguidora esteja um pouco à frente da líder e, no momento que ela assume seu novo papel, algumas mensagens de saída deixem de ser disseminadas. Para evitar isto, a réplica líder envia, de tempos em tempos, uma mensagem especial notificando as suas seguidoras sobre o número sequencial da última mensagem gerada. Este valor é armazenado pela réplica seguidora e atualizado sempre que uma nova mensagem deste tipo chega. Além disso, cada seguidora utiliza a função OutputLog() para guardar as mensagens por ela produzidas (mas não disseminadas para o mundo exterior). Quando uma mensagem de notificação enviada pela líder chega, as mensagens no log de saída que possuam identificador menor ou igual aquele indicado pela notificação são eliminadas do log. Quando uma seguidora assume o papel de líder, ela pode se encontrar em duas situações: (a) o log está vazio, o que indica que a líder estava à frente da seguidora ou que a seguidora e a líder estavam aproximadamente sincronizadas; ou (b) o log não está vazio, o que indica que a seguidora estava processando mais rápido que a líder. Assim, quando uma seguidora passa a ser líder ela deve verificar se o log está vazio ou não; se não está, ela primeiro envia todas as mensagens no log, para só depois começar a enviar as novas mensagens produzidas. Por outro lado, se o log está vazio, ela deve verificar qual foi o valor indicado na última mensagem de notificação do líder e só enviar, dentre as novas mensagens produzidas, aquelas que tenham um número de seqüência maior que aquele indicado para a última mensagem enviada pela líder, antes da falha. Note que, as mensagens enviadas pela réplica líder entre o envio de uma mensagem de notificação e a sua falha seriam novamente enviadas pela nova líder. Mas isto não é um problema, pois o próprio receptor destas mensagens se encarrega de descartar as duplicatas. Como visto anteriormente, na replicação semi-ativa o potencial comportamento não determinístico das réplicas pode levar à divergência de estados entre elas. É preciso, portanto, controlar possíveis comportamentos não determinísticos. Este tipo de comportamento pode ser conseqüência de duas situações: i) processamento não determinístico da aplicação (p. ex., um cálculo feito a partir dos dados lidos de um sensor); e ii) não determinismo de componentes do sistema operacional (p. ex., tratamento de eventos assíncronos). Estes problemas são resolvidos no Seljuk-Amoeba da seguinte maneira. Toda operação que tenha comportamento não determinístico é implementada de forma que o seu resultado é calculado pela réplica e, em seguida, enviado para si mesma através de uma mensagem. Esta mensagem seria, a priori, recebida por todas as réplicas do grupo. Porém, como cada réplica segue esta implementação, várias mensagens com mesmo número de seqüência são produzidas. Note que estas mensagens serão ordenadas normalmente pelo mecanismo de ordenação que gerencia o grupo de réplicas e, desta forma, apenas um dos valores calculados será recebido e utilizado por todas as réplicas (aquele valor calculado pela réplica que conseguiu ordenar seu valor em primeiro lugar); os outros valores serão descartados como duplicatas de uma mensagem já recebida. No caso dos eventos assíncronos, estes são transformados em mensagens especiais, que são enviadas para as outras réplicas do nodo. Os handlers destes eventos são implementados na forma de threads que bloqueiam a espera de uma mensagem sinalizando o evento correspondente. Novamente, o mecanismo de ordenação do grupo de réplicas garante que os mesmos eventos serão tratados no mesmo ponto de execução por todas as réplicas. Um seqüenciador de eventos é usado para possibilitar o descarte de eventos duplicados, já tratados. 4.3. Serviço de Diagnóstico de Falta e Detecção de Falhas Há dois níveis de falhas a se considerar: a falha dos nodos e a falha das próprias réplicas. O próprio protocolo de comunicação em grupo do Amoeba [Kaashoek-Tanenbaum 94] provê detecção de falhas dos nodos onde as réplicas de um processo estão executando de acordo com o grau de resiliência especificado na chamada CreateGroup(). Uma falha é detectada pelo núcleo rodando em um nodo por meio do envio de uma mensagem BC_ALIVEREQ para o nodo que está há um certo tempo sem enviar qualquer mensagem. Se após algumas tentativas nenhuma mensagem de resposta BC_ALIVE chega, o nodo solicitante assume que o destino falhou e entra no modo de recuperação, marcando o grupo que possuia réplica executando naquele nodo como não-usável. Todas as chamadas ReceiveFromGroup() subseqüentes, feitas pela réplica local, retornam um erro e a réplica local deve chamar ResetGroup() para reorganizar o grupo. Tal réplica se torna coordenadora do processo de recuperação, ficando responsável, entre outras coisas, por atualizar os logs de mensagens de todas as réplicas do grupo. Neste ponto, todos os membros do grupo revertem para um modo de recuperação. Esta mesma idéia poderia ser utilizada em um nível mais alto para detectar falhas das réplicas. Neste caso, sempre que uma réplica faz uma chamada ReceiveFromGroup(), ela inicializa um temporizador. Se nenhuma mensagem chega no tempo estabelecido, uma falha é assumida e uma chamada ResetGroup() é feita. Mais uma vez, observamos que esta funcionalidade não é transparente para a aplicação, que inevitavelmente toma conhecimento da ocorrência da falha. Por este motivo, optou-se por executar em cada nodo um servidor de detecção que funciona como um daemon que, de tempos em tempos, inspeciona o funcionamento da réplica local e dos outros nodos que executam réplicas do mesmo grupo (esta informação é mantida em cada nodo). Quando a falha de uma réplica é detectada, o servidor de detecção de falhas faz uma chamada ResetRepGroup(group-id), solicitando ao núcleo que ele inicie o processo de recuperação para reorganizar o grupo identificado por group-id. 4.4. Serviço de Reconfiguração Cada vez que uma réplica (ou um nodo) falha e uma chamada ResetRepGroup() é feita, o número de membros do grupo diminui e, conseqüentemente, a capacidade de tolerar novas faltas vai sendo paulatinamente reduzida. A fim de manter o grau de resiliência da aplicação, a reconfiguração do processo replicado se faz necessária. Tal reconfiguração implica que um novo nodo deve ser alocado e que uma nova réplica do processo deve ser iniciada. Esta função é realizada pelo próprio RepServer. Para tanto, após todos os procedimentos de recuperação terem sido concluídos, o servidor de detecção que requisitou a recomposição do grupo, faz a seguinte solicitação ao RepServer: CreateNewRep(file,new-rep-degree, failure-semantics, group-id) Ao receber tal pedido, o RepServer examina as informações armazenadas para o grupo identificado por group-id para verificar quantos membros compõem atualmente o grupo. Caso este número seja menor que new-rep-degree (o que certamente vai ocorrer), novas réplicas precisam ser criadas. Neste caso, RepServer passa a agir da mesma forma descrita na Sub-seção 4.2. Ele requisita os serviços do FT Run Server para a criação de novos nodos e o serviço de processamento dos nodos criados para executar o código indicado em file. Além disso, o RepServer faz uma chamada a primitiva de gerenciamento de grupo JoinRepGroup(group-id, member-list) para que os membros indicados em member-list sejam acrescentados ao grupo group-id. Observe a flexibilidade de reconfiguração provida pelo ambiente Seljuk-Amoeba. O servidor de detecção de falhas pode decidir que o grau de resiliência inicialmente previsto para aquele processo replicado não mais é necessário ser mantido, porque, por exemplo, a execução do processamento já está próxima do fim. Neste caso, ele pode simplesmente não fazer a chamada CreateNewRep ou ainda pode fazê-la indicando um grau de resiliência menor que o indicado na chamada Replicate(). Por outro lado, se for constatado que a freqüência da ocorrência de falhas está muito alta, o grau de resiliência pode ser aumentado passando um new-rep-degree maior. 5. CONCLUSÕES Os primeiros sistemas construídos para serem tolerantes a faltas eram soluções ad-hoc aplicáveis primariamente a problemas específicos. Porém, com o aumento da dependência da sociedade nos sistemas de computador e com o conseqüente crescimento dos requisitos de confiança no funcionamento das aplicações, surgiu a necessidade de se proporcionar, ao desenvolvedor de aplicações, meios que permitissem o reaproveitamento do esforço desprendido na construção de outros sistemas tolerantes a faltas, minimizando assim o seu trabalho. Uma forma de se conseguir isto é prover um ambiente operacional que ofereça suporte para construção e execução de aplicações distribuídas robustas, livrando o programador do trabalho de implementar os mecanismos para tolerância a faltas. A plataforma aqui apresentada provê serviços que implementam os principais mecanismos para tolerância a faltas utilizados na construção de aplicações distribuídas robustas. Uma das vantagens da nossa proposta é que ela é implementada inteiramente em software, não requerendo qualquer recurso especial de hardware. Além disso, os serviços propostos apresentam-se bastante flexíveis à medida que a aplicação pode, em tempo de ativação, selecionar o grau de tolerância a faltas desejado. Além disso, apenas aquelas aplicações que requisitem tais serviços pagam (sob a forma de queda de desempenho) pelo seu uso. As diferentes abordagens para replicação podem conviver e cooperar no ambiente SeljukAmoeba. A replicação do processamento é transparente para o ambiente externo, de modo que uma aplicação replicada se comporta aparentemente da mesma forma que uma não-replicada, com a grande diferença que uma aplicação replicada é resiliente, conseguindo prover o serviço adequado mesmo na presença de um certo número de faltas. AGRADECIMENTOS Os autores agradecem o apoio financeiro do CNPq (processo 300.646/96-8). REFERÊNCIAS [Birman-Joseph 87] K.P. Birman e T.A. Joseph, “Reliable Communication in the Presence of Failures,” ACM Transactions on Computer Systems, Vol. 5, No. 1, pp. 47-76, fevereiro de 1987. [Brasileiro 97] F.V.Brasileiro, “Seljuk: Um ambiente para Suporte ao Desenvolvimento e à Execução de Aplicações Distribuídas Robustas”, submetido ao VII Simpósio de Computadores Tolerantes a Falhas, fevereiro de 1997. [Brasileiro-Ezhichelvan 95] F.V. Brasileiro e P.D. Ezhilchelvan, “Atomic Broadcast Using Time-outs instead of Synchronised Time”, Anais do VI Simpósio de Computadores Tolerantes a Falhas, Canela, Brasil, pp. 223-238, agosto de 1995. [Chang-Maxemchuk 84] Jo-Mei Chang e N. F. Maxemchuk, “Reliable Broadcast Protocols”, ACM Transactions on Computer Systems, Vol. 2, No. 3, pp. 251-273, agosto de 1984. [Cristian 91] F. Cristian, “Understanding Fault-Tolerant Distributed Systems,” Communications of the ACM, Vol. 34, N. 2, pp. 56-78, fevereiro de 1991. [Cristian et al. 85] F. Cristian, H. Aghili, R. Strong, e D. Dolev, “Atomic Broadcast: from Simple Message Diffusion to Byzantine Agreement”, Digest of Papers, FTCS-15, Ann Arbor, USA, pp. 200-206, junho de 1985. [Gallindo-Brasileiro 97] E.L. Gallindo e F.V. Brasileiro, “Processamento Confiável no Ambiente Operacional Seljuk-Amoeba”, submetido ao VII Simpósio de Computadores Tolerantes a Falhas, fevereiro de 1997. [Huang-Kintala 93] Y. Huang e C. Kintala, “Software Implemented Fault Tolerance: Technologies and Experience”, Proceedings of the 23rd FTCS, Tolouse, França, pp. 2-9, 1993. [Jalote 94] Pankaj Jalote, Fault Tolerance in Distributed Systems, Prentice Hall, New Jersey, 1994, ISBN 0-13-301367-7. [Kaashoek-Tanenbaum 94] F. Kaashoek e A.S. Tanenbaum, “Efficient Reliable Group communication for Distributed Systems”, Submetido à publicação em 1994. [Lamport 78] L. Lamport, “Time, Clocks and Ordering of Events”, Communications of the ACM, Vol. 21, No. 7, pp. 558-565, julho de 1978. [Laprie 89] J. C. Laprie, “Dependability: a Unifying Concept for Computing and Fault Tolerance”, in Dependabilty of Resilient Computers, T. Anderson (Ed.), BSP Professional Books, 1989. [Lee-Anderson 90] P.A. Lee. e D. A. Anderson, Fault Tolerance - Principles and Practice, Spring-Verlag, 1990. [Lemos-Veríssimo 91] R. de Lemos e Paulo Veríssimo, “Confiança no Funcionamento Proposta para uma Terminologia em Português”, comunicação pessoal, dezembro de 1991. [Mullender et al. 90] S.J. Mullender, G. van Rossum, A.S. Tanembaum, R. van Renesse, e H. van Staveren, “Amoeba: A Distributed Operating System for the 1990's”, IEEE Computer, Vol. 23, No. 5, pp. 44-53, maio de 1990. [Ng 90] T.P. Ng, “The Design and Implementation of a Reliable Distributed Operating System - ROSE”, Proceedings of ICDCS, pp. 2-11, 1990. [Powell 92] D. Powell (Ed.), Delta-4 - A Generic Architecture for Dependable Distributed Computing, Spring-Verlag, 1992, ISBN 3-540-54985-4. [Randell 75] B. Randell, “System Structure for Software Fault Tolerance,” IEEE Transactions on Software Engineering, Vol. 1, No. 2, pp. 220-232, junho de 1975. [Shin-Ramanathan 87] K.G. Shin, e P. Ramanathan, “Diagnosis of Processors with Byzantine Faults in a Distributed Computing System”, Digest of Papers, FTCS-17, Pittsburgh, USA, pp. 55-60, junho de 1987.