Definição e implementação de uma solução distribuída de cache de dados com estrutura hierárquica Bruno Miguel Ferreira Veigas Dissertação para obtenção do Grau de Mestre em Engenharia Electrotécnica e de Computadores Júri Presidente: Prof. Cjgldadlkh Njgjkjala Bklalsjdla Ylkhjçasdj (12pt normal) Orientador: Prof. Doutor João Paulo Baptista de Carvalho Vogais: Doutora Bjlçsajasdlçl Mjlajçlaffljsd Khsaçlsahfd (12pt normal) Prof. Mçlassjfçldjsal Nçlaçlafjl Uçlçjljçfçjld (12pt normal) Prof. Mçlassjfçldjsal Nçlaçlafjl Uçlçjljçfçjld (12pt normal) Setembro de 2007 Agradecimentos A realização deste projecto não teria sido possível sem a ajuda e o apoio de algumas pessoas, às quais eu gostaria de agradecer. Em primeiro lugar, ao meu orientador, professor Doutor João Carvalho, por ter proposto este trabalho e pela orientação que me deu. À empresa WeDo Consulting, que financiou este projecto e reuniu as condições necessárias para a sua realização. Um agradecimento especial ao Nuno Homem e ao João Lopes, pelo acompanhamento do desenrolar do projecto e pelas sugestões. Aos meus colegas de trabalho ao longo do curso, sem os quais estes anos de trabalho árduo teriam sido ainda mais complicados. Um agradecimento especial ao Roberto Cabrita, pelas dezenas de centenas de horas de trabalho conjunto. Gostaria de agradecer aos meus amigos e à minha família, pelo seu total apoio para a realização deste grande projecto de 5 anos, que termina com esta dissertação de mestrado. Um agradecimento muito especial à minha namorada, pelo apoio incondicional que me deu, para eu puder atingir este meu grande objectivo. Por fim, relembrar algumas das pessoas que já partiram e que eu tenho a certeza que me teriam dado todo o seu apoio: António Cozinheiro, Gonçalo Azoia e João Veigas. i Resumo Hoje em dia, cada vez mais, os sistemas distribuídos são uma opção a considerar. A compra e manutenção de grandes servidores e super computadores é muito dispendiosa. A criação de grids de computadores, com hardware com boa relação preço/desempenho e facilmente substituível, parece ser a solução a adoptar. Para permitir que a computação seja feita de forma distribuída, são necessários mecanismos que garantam a consistência da informação partilhada. É neste contexto que surge o conceito de cache distribuída. Pretende-se com este trabalho definir e implementar uma solução distribuída de cache de dados com estrutura hierárquica. A cache é um dos componentes de um servidor aplicacional. Pretende-se escalar o sistema para vários servidores aplicacionais. O sistema deverá ser capaz de permitir a vários servidores aplicacionais partilharem um conjunto de dados em modo de leitura e de escrita, assegurando a consistência da informação partilhada. Algumas das operações deverão ser efectuadas de forma síncrona para garantir a robustez da solução a falhas. Começa-se por apresentar uma solução simples para o problema, que admite que um dos servidores aplicacionais não pode falhar. Progressivamente, vão sendo introduzidas melhorias na solução, com vista à correcção da restrição anterior e obtenção de um melhor desempenho do sistema distribuído. Faz-se a integração com a cache centralizada previamente existente e efectuam-se testes para verificar o correcto funcionamento da solução proposta. Palavras Chave Cache distribuída Dados com estrutura hierárquica Tolerância a falhas Sistema distribuído Comunicação por grupos Eleição estável iii Abstract Now a days, more and more, distributed systems are an option to consider, as the purchase and maintenance of big servers and super computers is very expensive. The creation of computer grids, using hardware with a good relation price/performance and easily replaceable, seems to be the best option available. To allow the computation to be made in a distributed manner, mechanisms to guarantee the consistency of the shared information are needed. In this context appears the concept of distributed cache. This work aims the definition and the implementation of a distributed solution of cache with hierarchical data structure. The cache is one of the application server components. It is intended to scale the system to some application servers. The system must allow the share of data between several application servers, in both read and write modes, assuring the consistency of the shared information. Some of the operations need to be performed in a synchronous way to guarantee the robustness of the solution to failures. This work starts with the presentation of a simple solution of the problem, admitting that one of the application servers cannot fail. Gradually, improvements are introduced toward the final solution, in order to overcome the previous restriction and to achieve a better performance of the distributed system. The solution is then integrated with the previously existing centralized cache and tests are performed to verify the correct functioning of the solution proposed. Keywords Distributed cache Hierarchical data structure Fault tolerance Distributed system Group communication Stable election v Conteúdo Agradecimentos i Resumo iii Abstract v Conteúdo vii Lista de Figuras xi Lista de Tabelas xiii Lista de Siglas xv I Contextualização e tecnologia utilizada 1 Introdução 1 1.1 Descrição do problema e objectivos . . . . . . . . . . . . . . . . . . . . . . . . . 1 1.2 Discrição genérica de uma cache . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 1.3 Sistemas distribuídos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 1.3.1 O que é um sistema distribuído? . . . . . . . . . . . . . . . . . . . . . . . 2 1.3.2 Características de um sistema distribuído . . . . . . . . . . . . . . . . . . 2 1.4 Contribuições deste trabalho . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 1.5 Convenções de redação . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 1.6 Estrutura do texto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 2 Enquadramento 2.1 Soluções de cache distribuída 5 . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 2.1.1 JBoss Cache . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 2.1.2 EHCache . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6 2.1.3 Tangosol Coherence . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6 2.1.4 SwarmCache . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6 2.1.5 OSCache . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 2.1.6 FKache . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 2.1.7 The Bamboo DHT . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 2.1.8 ShiftOne Java Object Cache . . . . . . . . . . . . . . . . . . . . . . . . . 8 2.2 Publicações . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8 vii 3 Objectos distribuídos 11 3.1 Objectos distribuídos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 3.2 Ligação de um cliente a um objecto (bind) . . . . . . . . . . . . . . . . . . . . . . 12 3.3 Passagem de parâmetros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12 3.4 Objectos distribuídos em Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 3.4.1 O modelo de objectos distribuídos em Java . . . . . . . . . . . . . . . . . 13 3.4.2 Invocação de objectos remotos em Java . . . . . . . . . . . . . . . . . . . 14 3.4.3 O registo RMI (rmiregistry ) . . . . . . . . . . . . . . . . . . . . . . . . . . 14 3.4.4 Implementação de objectos remotos . . . . . . . . . . . . . . . . . . . . . 15 3.4.5 Limpeza de objectos distribuídos (Garbage Collecting) . . . . . . . . . . . 16 4 Comunicação fiável em multicast 17 4.1 Comunicação fiável em multicast básica . . . . . . . . . . . . . . . . . . . . . . . 17 4.2 Escalabilidade em comunicação fiável em multicast . . . . . . . . . . . . . . . . 18 4.2.1 Controlo de feedback não hierárquico . . . . . . . . . . . . . . . . . . . . 19 4.2.2 Controlo de feedback hierárquico . . . . . . . . . . . . . . . . . . . . . . . 20 4.3 Multicast Atómico . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21 4.3.1 Sincronismo Virtual . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22 4.4 The Jgroup Project . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23 4.4.1 Composição do Jgroup . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24 4.4.2 O serviço de registo do Jgroup . . . . . . . . . . . . . . . . . . . . . . . . 27 4.4.3 Configuração do sistema distribuído . . . . . . . . . . . . . . . . . . . . . 28 II Desenvolvimento de uma cache de dados distribuída 5 Serviço de Cache centralizado 31 5.1 Estrutura hierárquica . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31 5.2 Arquitectura . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33 5.2.1 Camada de apresentação . . . . . . . . . . . . . . . . . . . . . . . . . . . 33 5.2.2 Camada de partilha de dados . . . . . . . . . . . . . . . . . . . . . . . . . 35 5.2.3 Camada de leitura e escrita . . . . . . . . . . . . . . . . . . . . . . . . . . 36 5.3 Políticas de substituição . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37 6 Solução inicial 39 6.1 Arquitectura da solução . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39 6.2 Obtenção e registo de referências . . . . . . . . . . . . . . . . . . . . . . . . . . 40 6.3 Contagem de clientes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42 6.3.1 Incremento do número de clientes . . . . . . . . . . . . . . . . . . . . . . 42 6.3.2 Decremento do número de clientes . . . . . . . . . . . . . . . . . . . . . 43 6.4 Estrutura hierárquica dos dados em cache . . . . . . . . . . . . . . . . . . . . . 45 6.5 Tolerância a falhas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46 6.5.1 Java RMI . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47 6.5.2 Pontos de falha . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47 6.5.3 Recuperação da ocorrência de falhas . . . . . . . . . . . . . . . . . . . . 47 6.6 Sincronização e performance . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51 6.7 Integração com a cache local . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51 viii 6.8 Estatísticas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51 6.9 Testes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52 6.10 Escalabilidade da solução . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52 7 Melhoramentos na solução inicial 53 7.1 Arquitectura da solução . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53 7.2 Eleição . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54 7.3 Replicação . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56 7.4 Tolerância a falhas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57 7.4.1 Falha do coordenador após a invocação de um método por parte de uma cache cliente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57 7.4.2 Falhas relacionadas com o algoritmo de eleição . . . . . . . . . . . . . . 58 7.4.3 Falha durante a sincronização com o coordenador . . . . . . . . . . . . . 58 7.5 Testes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59 8 Conclusões e trabalhos futuros 61 8.1 Conclusões . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61 8.2 Trabalhos futuros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62 Apêndice Bibliografia 65 A Interacções entre o serviço de cache e os restantes serviços do servidor aplicacional B Diagramas UML da solução melhorada 69 71 ix Lista de Figuras 3.1 Invocação de um método num objecto remoto. . . . . . . . . . . . . . . . . . . . 11 3.2 Passagem de objectos por referência e por valor. . . . . . . . . . . . . . . . . . . 13 4.1 Uma solução simples para comunicação fiável em multicast, onde são conhecidos todos os receptores e se assume que não há falhas em nenhum dos processos. (a) Transmissão da mensagem. (b) Feedback. . . . . . . . . . . . . . 18 4.2 Vários receptores agendaram um pedido de retransmissão, mas o primeiro pedido de retransmissão leva à supressão dos outros. . . . . . . . . . . . . . . . . 19 4.3 Vista geral de uma solução hierárquica de comunicação fiável em multicast. Cada coordenador reencaminha a mensagem para os seus filhos e atende pedidos de retransmissão. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20 4.4 Organização lógica de um sistema distribuído, para distinguir a recepção da entrega de mensagens. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22 4.5 O princípio de funcionamento do sincronismo virtual em multicast. . . . . . . . . 23 4.6 Exemplo do preenchimento do ficheiro config.xml. . . . . . . . . . . . . . . . . . 28 5.1 Contexto de execução básico. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32 5.2 Relações entre entidades. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32 5.3 Arquitectura do serviço de cache. . . . . . . . . . . . . . . . . . . . . . . . . . . . 33 5.4 Organização do serviço de cache em camadas. . . . . . . . . . . . . . . . . . . 34 5.5 Exemplo de contexto. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34 5.6 Interacção entre as camadas de apresentação e partilha de dados. . . . . . . . 36 6.1 Arquitectura em camadas do serviço de cache distribuído. . . . . . . . . . . . . . 40 6.2 Arquitectura da camada de cache distribuída. . . . . . . . . . . . . . . . . . . . . 40 6.3 Obtenção e registo de referências. . . . . . . . . . . . . . . . . . . . . . . . . . . 41 6.4 Contagem de clientes - incremento. . . . . . . . . . . . . . . . . . . . . . . . . . 43 6.5 Contagem de clientes - decremento. . . . . . . . . . . . . . . . . . . . . . . . . . 44 6.6 Falha na invocação de um método remoto e respectiva recuperação. . . . . . . . 48 6.7 Crash de um cliente e respectiva recuperação. . . . . . . . . . . . . . . . . . . . 49 6.8 Falha antes do registo de um objecto. . . . . . . . . . . . . . . . . . . . . . . . . 50 7.1 Arquitectura da camada de cache distribuída melhorada. . . . . . . . . . . . . . 53 7.2 Algoritmo de eleição. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55 A.1 Arquitectura do serviço de cache incluindo interacções com restantes serviços. . 70 xi B.1 Diagrama de classes - Parte 1/2. . . . . . . . . . . . . . . . . . . . . . . . . . . . 72 B.2 Diagrama de classes - Parte 2/2. . . . . . . . . . . . . . . . . . . . . . . . . . . . 73 xii Lista de Tabelas 5.1 Esquema da tabela de relações. . . . . . . . . . . . . . . . . . . . . . . . . . . . 32 5.2 Preenchimento da tabela de relações para o exemplo da Figura 5.1. . . . . . . . 33 xiii Lista de Siglas ACK - ACKnowledgement (feedback positivo) AOP - Aspect-Oriented Programming API - Application Programming Interface BSD - Berkeley Software Distribution DHT - Distributed Hash Table DRS - Dependable Registry Service EGMI - External GMI GMI - Group Method Invocation GMIS - GMI Service GMS - Group Membership Service IGMI - Internal GMI JDK - Java Development Kit JMS - Java Message Service JSR - Java Specification Request JSP - JavaServer Pages JVM - Java Virtual Machine NACK - Negative ACK (feedback negativo) RMI - Remote Method Invocation POJO - Plain Old Java Object SMS - State Merging Service SRM - Scalable Reliable Multicast UML - Unified Modeling Language xv Parte I Contextualização e tecnologia utilizada Capítulo 1 Introdução Este trabalho é de carácter multidisciplinar. Visto que existem limitações em relação ao número máximo de páginas, não é possível descrever detalhadamente todos os conceitos utilizados. Assim, admite-se que o leitor tem conhecimentos de programação de sistemas, linguagem de programação Java e bases de dados. Como referências pode consultar-se [36], [2] e [32], respectivamente. 1.1 Descrição do problema e objectivos Pretende-se com este trabalho definir e implementar uma solução distribuída de cache de dados com estrutura hierárquica, partindo de uma solução centralizada já existente. A cache é um dos componentes de um servidor aplicacional. Pretende-se escalar o sistema para vários servidores aplicacionais. O sistema deverá ser capaz de permitir a vários servidores aplicacionais partilharem um conjunto de dados de execução de processos de negócio, em modo de leitura e de escrita, e assegurar a consistência desta informação. Algumas das operações deverão ser efectuadas de forma síncrona para garantir a persistência dos dados e a robustez da solução a falhas. A solução para o problema deve alterar a cache existente apenas nos locais estritamente necessários. Pretende-se que as alterações sejam mínimas para manter a compatibilidade com os diversos serviços com os quais a cache interage (Anexo A), de forma a diminuir o risco de ocorrência de problemas, e os testes necessários para verificar o correcto funcionamento da solução. A solução deve ser totalmente implementada em Java. 1.2 Discrição genérica de uma cache Uma cache é memória de acesso rápido, usada para guardar um subconjunto de um conjunto maior de dados, cuja obtenção é lenta ou "cara"do ponto de vista computacional. Geralmente a capacidade da cache é reduzida, não permitindo armazenar todos os dados existentes. Assim é necessária a existência de políticas de substituição, que possam colocar em cache novos dados quando esta estiver totalmente preenchida. As políticas de substituição são uma tentativa de optimização da utilização da cache [28]. 1 Uma vez que a eficiência da cache depende da hit rate (número de pedidos que são satisfeitos recorrendo apenas aos dados existentes em cache, face a pedidos que necessitam de ser computados ou se encontram armazenados em tipos de memória mais lentos), a política de substituição tenta prever o padrão de pedidos futuros. Desta forma, a escolha de uma política de substituição adequada é uma das principais preocupações no desenho de uma cache. Outra decisão que tem de ser tomada no desenho de uma cache é a estratégia de escrita a adoptar. As alterações efectuadas nos dados em cache têm de ser propagadas para os dados originais. Numa estratégia write-through cada operação de escrita em cache desencadeia uma escrita síncrona nos dados originais. Alternativamente, pode ser utilizada uma estratégia write-back, na qual as alterações têm efeito imediato apenas nos dados em cache. Na estratégia write-back, as alterações só têm efeito nos dados originais quando os dados são removidos da cache, ou quando tal é especificado directamente pela aplicação. Uma entrada da cache é um elemento guardado na cache. Este elemento tem de conter a informação a guardar, assim como atributos necessários para a execução da política de substituição, tais como a idade e o número de referências. 1.3 Sistemas distribuídos Esta secção apresenta noções básicas de sistemas distribuídos. Estas noções permitirão ajudar na compreensão da tecnologia utilizada e das soluções apresentadas. 1.3.1 O que é um sistema distribuído? Existem diversas definições de sistemas distribuídos. Segundo [37], "‘um sistema distribuído é um conjunto de computadores independentes que para os utilizadores aparentam ser um único sistema coerente"’. Esta definição refere quer o hardware (máquinas autónomas) quer o software (interacção de utilizadores com o sistema). 1.3.2 Características de um sistema distribuído As principais características de um sistema distribuído são: transparência, consistência, escalabilidade e abertura. Transparência As diferenças entre os vários computadores e a forma como eles comuni- cam entre si deve estar escondida dos utilizadores, assim como o facto dos processos e os recursos se encontrarem fisicamente distribuídos por múltiplos computadores. Existem diferentes formas de transparência, tais como: acesso, localização, falha, etc. Consistência É importante que utilizadores e aplicações possam interagir com o sistema distribuído de forma uniforme e consistente, independentemente de quando e onde é que a interacção ocorre. 2 Escalabilidade Deve ser relativamente fácil expandir ou escalar um sistema distribuído. Esta característica está directamente relacionada com a existência de computadores independentes. Deve ser possível a expansão do sistema quer em número de utilizadores, quer em termos de recursos disponíveis, e aumentar a distância entre utilizadores e recursos. Abertura Um sistema distribuído aberto é um sistema que oferece serviços segundo regras padrão que descrevem a sintaxe e a semântica desses serviços. Para permitir flexibilidade um sistema distribuído aberto deve estar organizado em pequenos componentes, com interfaces bem definidas, e facilmente substituíveis. 1.4 Contribuições deste trabalho Apesar de existirem diversas bibliotecas e produtos que permitem a implementação de caches distribuídas (Secção 2.1), a informação relativa aos detalhes de funcionamento dessas soluções é praticamente inexistente. Poderia ter-se optado pela utilização de uma dessas soluções. No entanto, visto que este trabalho foi desenvolvido em colaboração com uma empresa, optou-se pelo desenvolvimento de uma solução à medida. Este tipo de solução permite maior facilidade de integração com a cache centralizada já existente e um total controlo do seu funcionamento. Além disso, as soluções já existentes geralmente têm associadas funcionalidades adicionais, que são desnecessárias para a resolução deste problema específico. Uma vez que os detalhes de funcionamento e implementação das diversas soluções encontradas são escassos, a publicação deste trabalho é uma contribuição para a definição e implementação de uma cache distribuída. 1.5 Convenções de redação Ao longo do texto serão efectuados diversos tipos de referência. Referências bibliográficas serão indicadas entre parêntesis rectos, por exemplo [17]. Referências a capítulos ou secções serão indicadas na forma captítulo.secção.subsecção, prefixadas pelo identificador correspondente, por exemplo Secção 4.2, corresponde à secção 2 do capítulo 4. Referências a figuras e tabelas serão indicadas na forma capítulo.figura/tabela, prefixadas pelo identificador correspondente, por exemplo Tabela 6.3, corresponde à terceira tabela do capítulo 6. Termos em inglês serão indicados em itálico, assim como referências a objectos apresentados nos exemplos. Nomenclatura relativa a código fonte é indicada na forma codigoFonte. 1.6 Estrutura do texto Esta dissertação está dividida essencialmente em duas grandes partes. A primeira parte apresenta o problema, contextualiza-o e descreve a tecnologia utilizada. A segunda parte apresenta o ponto de partida, bem como o trabalho desenvolvido no âmbito da dissertação. 3 O Capítulo 2 aborda algumas publicações relevantes na área de interesse deste trabalho. Apresenta ainda algumas bibliotecas e produtos existentes, com funcionalidades de cache distribuída. Os Capítulos 3 e 4 explicam o funcionamento da tecnologia utilizada no desenvolvimento deste trabalho. O Capítulo 3 explica de forma geral o conceito de objecto distribuído, ilustra o seu funcionamento e particulariza para o caso do modelo de objectos distribuídos em Java. O Capítulo 4 introduz os mecanismos de comunicação fiável em multicast. Explicitam-se os problemas associados a este tipo de comunicação e explica-se de forma geral a comunicação por grupos. Particulariza-se a comunicação por grupos para o caso da biblioteca Jgroup. A segunda parte da dissertação inicia-se com o Capítulo 5. Neste capítulo apresenta-se a solução centralizada previamente existente, que serviu de ponto de partida para o desenvolvimento do trabalho. O Capítulo 6 descreve a arquitectura e o funcionamento de uma solução distribuída de cache de dados com estrutura hierárquica. Descreve-se ainda a integração com a cache centralizada inicial e os testes efectuados. O Capítulo 7 introduz melhorias na solução inicial, permitindo corrigir os problemas existentes. O Capítulo 8 apresenta as principais conclusões que se podem tirar desta dissertação e menciona possíveis melhoramentos a efectuar na solução apresentada. Por fim, apresentam-se alguns apêndices que complementam o texto principal do trabalho. 4 Capítulo 2 Enquadramento Hoje em dia, cada vez mais, os sistemas distribuídos são uma opção a considerar. A compra e manutenção de grandes servidores e super computadores é muito dispendiosa. A criação de grids de computadores, com hardware com boa relação preço/desempenho e facilmente substituível, parece ser a solução a adoptar. Esta é a política adoptada pela Google, que segundo um artigo do The New York Times de Junho de 2006 [20], possuía mais de 450 000 computadores, dispersos por 25 locais do globo. Para permitir que a computação seja feita de forma distribuída, são necessários mecanismos que garantam a consistência da informação partilhada. É neste contexto que surgem os conceitos de cache distribuída e memória distribuída partilhada. Existe uma tentativa de uniformização de uma API para soluções de caching escritas em Java, JSR (Java Specification Request) 107 [16]. Caso várias soluções optem por implementar esta API, será possível proceder à sua substituição facilmente. Algumas das soluções que implementam a API: Tangosol, EHCache, FKache, etc. Nas próximas secções apresentam-se diversas soluções de cache distribuída encontradas e algumas publicações referentes à temática desta dissertação. 2.1 2.1.1 Soluções de cache distribuída JBoss Cache A biblioteca JBoss Cache [17] permite colocar em cache os objectos Java utilizados mais frequentemente, para melhorar o desempenho das aplicações. Eliminando acessos desnecessários à base de dados é possível diminuir o tráfego da rede e aumentar a escalabilidade das aplicações. Fornece funcionalidades transaccionais, assim como um grande conjunto de configurações para lidar com acessos concorrentes aos dados, da forma mais eficiente possível para a aplicação em questão. Adicionalmente, replica o conteúdo para outras instâncias de cache, em execução em máquinas virtuais Java (JVMs) ou servidores distintos, permitindo a implementação de funcionalidades de clustering. 5 Existem duas APIs distintas, permitindo a utilização daquela que melhor se adequar às necessidades. Uma das APIs oferece uma estrutura hierárquica em árvore, baseada em nós, enquanto a outra (POJO Cache) oferece mecanismos de replicação mais granulares, permitindo obter um melhor desempenho. A POJO (Plain Old Java Object) Cache [41] permite preservar as relações entre objectos durante a replicação ou persistência (com recurso a uma base de dados). A replicação é feita de forma transparente com recurso ao paradigma AOP (Aspect-Oriented Programming). Este paradigma [43] tenta ajudar na definição da arquitectura da aplicação, permitindo uma melhor divisão das funcionalidades, obtendo uma arquitectura mais modular. Todas as actualizações de um POJO são interceptadas, sendo propagadas para as diversas réplicas de forma automática. Devido à utilização do paradigma AOP, é necessário efectuar uma “pré-compilação” da aplicação com o aopc (compilador AOP), disponibilizado juntamente com a biblioteca. A biblioteca está disponível sobre licença LGPL [13]. 2.1.2 EHCache EHCache [39] é uma biblioteca em Java com funcionalidades de cache distribuída, que implementa a API JSR107. As suas principais características são a rapidez, leveza, escalabilidade, extensibilidade, possibilidade de persistência dos dados, possibilidade de utilização com Java EE, alta qualidade de implementação e a possibilidade de utilização de forma distribuída. Relativamente à sua característica distribuída as funcionalidades oferecidas são: descoberta de pares, entrega fiável, replicação síncrona ou assíncrona, cópia ou invalidação de réplicas, replicação transparente e extensibilidade. Esta biblioteca está disponível sobre licença Apache [11]. 2.1.3 Tangosol Coherence Tangosol Coherence [33] é uma solução comercial que permite a gestão de dados em memória para aplicações J2EE distribuídas e servidores aplicacionais. Coherence torna a partilha e gestão de dados, entre vários servidores, tão simples como se apenas de um servidor se tratasse. Isto é possível devido à utilização de protocolos de replicação e propagação de alterações. Coherence implementa a API JSR107 e fornece gestão de dados replicados e distribuídos, sobre um protocolo fiável e escalável de comunicação entre pares (peer-to-peer ). Não introduz nenhum ponto de falha. Efectua a recuperação de forma totalmente transparente no caso de ocorrência falhas, redistribuindo os dados quando um servidor deixa de estar operacional. Quando um novo servidor se junta, ou quando um servidor que falhou recupera, a carga é redistribuída de forma transparente. 2.1.4 SwarmCache SwarmCache [42] é uma solução simples mas eficaz de cache distribuída, em Java. Utiliza comunicação IP multicast para comunicar com um qualquer número de anfitriões numa rede local. 6 Foi especialmente desenhado para uso em aplicações clustered, em que o número de operações de leitura é muito superior ao número de operações de escrita, permitindo assim um melhor desempenho. Internamente a SwarmCache utiliza uma biblioteca de comunicação por grupos (JGroups). Foram desenvolvidos wrappers que permitem a utilização da SwarmCache com motores persistentes, nomeadamente Hibernate e JPOX. Aparentemente o desenvolvimento da SwarmCache foi suspenso, tendo sido a última versão (1.0 RC2) lançada em Outubro de 2003. A biblioteca está disponível sobre licença LGPL [13]. 2.1.5 OSCache OSCache [24] é uma solução de cache para conteúdo de JSP (JavaServer Pages), respostas de servlets Java, ou objectos Java arbitrários, totalmente implementada em Java. Fornece soluções de cache em memória e persistência em disco, tendo sido desenhada com vista a atingir um bom desempenho. Suporta aplicações distribuídas, fornecendo escalabilidade e tolerância a falhas, sem ser necessário fazer alterações no código que interage com a cache. A solução está disponível sobre uma licença derivada da licença Apache [11]. 2.1.6 FKache FKache [18] é uma solução de cache distribuída, totalmente implementada em Java, que implementa parcialmente a API JSR 107. Relativamente às funcionalidades distribuídas, tudo é feito de forma transparente. Quando uma cache inicia junta-se automaticamente a outras caches existentes na rede. Todas as actualizações/invalidações são automaticamente propagadas para todas as caches da rede. Caso um objecto não resida na cache local é feito um pedido a outras caches. Esta implementação pretendia ter melhor desempenho do que todas as outras soluções de código aberto e comercial, mas aparentemente o seu desenvolvimento foi suspenso, tendo sido a última versão (1.0 beta6) lançada em Fevereiro de 2005. A biblioteca está disponível sobre licença LGPL [13]. 2.1.7 The Bamboo DHT Uma forma de criar uma cache distribuída é recorrer a uma tabela de dispersão distribuída (DHT - Distributed Hash Table). Este tipo de tabelas faz um mapeamento chave-valor e disponibiliza operações de put, get e remove. The Bamboo DHT [27] é uma das várias implementações de DHT existentes. Nesta solução os vários pares chave-valor encontram-se dispersos pelas diversas caches e são replicados à medida que as caches necessitam de aceder a outros objectos já existentes em caches distintas. Visto que existem alguns problemas de segurança, os autores aconselham a que este software seja utilizado apenas para investigação e não para ambientes de produção. Este software encontra-se licenciado sobre licença BSD [25]. 7 2.1.8 ShiftOne Java Object Cache ShiftOne Java Object Cache [31] é uma biblioteca Java que implementa várias políticas estritas de caching de objectos, tais como tamanho máximo e idade máxima, apresentando uma framework leve de configuração. As funcionalidades distribuídas são obtidas com recurso a bibliotecas externas (JGroups ou JMS). A biblioteca está disponível sobre licença LGPL [13]. 2.2 Publicações Apesar de existirem diversas implementações de caches distribuídas, as publicações relativas ao seu desenvolvimento são praticamente inexistentes. Uma das bibliotecas encontradas que tem publicações associadas é a Cali [47]. Cali é uma framework desenvolvida em C++ para sistemas de cache local e sistemas distribuídos. O artigo em questão, descreve as estruturas de dados e as políticas de gestão. Relativamente às funcionalidades distribuídas, esta biblioteca apenas fornece primitivas send/receive, necessitando de recorrer a pacotes externos para a implementação da troca de mensagens. Também não implementa mecanismos de localização de objectos. A inserção de um objecto na cache distribuída é feita com recurso a uma função de dispersão, ou seja, em função do hash code obtido, o objecto é atribuído a um determinado nó do sistema distribuído. Os acessos aos objectos são efectuados remotamente, não existindo migrações de objectos. O artigo não refere a existência de mecanismos de controlo de clientes de objectos remotos, nem de tolerância a falhas. O facto de um objecto ser atribuído a um nó em função do hash code também não parece ser a melhor opção a tomar, pois esse nó pode nem sequer utilizar o objecto em questão, enquanto que o nó que fez a inserção no sistema distribuído terá de efectuar todos os acessos de forma remota. Colocando de lado as implementações, [6] descreve um serviço de cache genérico para grids de computadores. A ideia é ter um serviço de cache que possa ser utilizado simultâneamente por várias aplicações, de forma a melhorar a eficiência do sistema. As descrições são feitas apenas ao nível da arquitectura. Cache distribuída e replicação são técnicas que podem ser utilizadas de forma independente, no entanto grande parte das vezes é conveniente combiná-las [40]. A informação relativa a esta temática encontra-se dispersa por várias áreas. Por exemplo, os modelos de consistência são estudados no contexto de memória distribuída partilhada, enquanto que questões relacionadas com como, onde e quando guardar dados remotamente, é investigada com maior detalhe em sistemas de informação distribuídos, como é o caso da World Wide Web [26, 29, 38]. Em [34] comparam-se quatro algoritmos básicos para a implementação de memória distribuída partilhada, concluindo-se que o desempenho dos algoritmos é sensível ao comportamento das aplicações. 8 O algoritmo “servidor central” é o mais simples de todos, no qual existe um servidor responsável pelo acesso a todos os dados partilhados, que mantém a única cópia dos dados. Operações de leitura e de escrita desencadeiam um pedido ao servidor central, que responde com os dados ou com um ACK, quer se trate uma operação de leitura ou de escrita, respectivamente. No algoritmo de “migração” os dados são sempre migrados para o sítio onde são acedidos, ou seja, trata-se de um protocolo “único leitor/único escritor”. No algoritmo de “replicação para leitura” é passada uma réplica do objecto que se pretende aceder. Operações de leitura passam a ser feitas localmente, enquanto que operações de escrita têm de invalidar ou actualizar as restantes réplicas, para manter a consistência da informação. Este algoritmo já permite “múltiplos leitores/único escritor”. Por fim, o algoritmo de “replicação total” que permite “múltiplos leitores/múltiplos escritores”. Neste caso é um pouco mais complicado garantir a consistência da informação partilhada, pois a operação de escrita tem de ser efectuada de forma atómica em todas as réplicas. 9 Capítulo 3 Objectos distribuídos O paradigma orientado a objectos é actualmente bastante utilizado. Uma dos seus principais aspectos prende-se com o facto de ser possível definir uma interface pública e esconder a implementação atrás dessa interface. Desta forma, os objectos podem ser facilmente substituíveis ou adaptáveis, desde que se respeite a interface previamente definida. 3.1 Objectos distribuídos Um objecto encapsula dados (estado) e as operações para manipular esses mesmos dados (métodos). Os métodos são disponibilizados através de interfaces. Não há forma de manipular o estado de um objecto sem ser através da invocação de métodos definidos nas interfaces. Um objecto pode implementar várias interfaces e uma interface pode ser disponibilizada por diversos objectos. A separação entre as interfaces e os objectos que as implementam é fundamental para os sistemas distribuídos. A Figura 3.1 ilustra a invocação de um método remoto. Figura 3.1: Invocação de um método num objecto remoto. 11 Na altura em que o cliente pretende ligar-se (bind) ao objecto remoto, é carregada para o espaço de memória do cliente uma implementação da interface do objecto, chamada proxy. O trabalho da proxy é empacotar a invocação de métodos em mensagens e desempacotar as respostas, devolvendo o resultado ao cliente. O objecto propriamente dito, reside numa máquina remota (servidor) e oferece uma interface igual àquela a que o cliente tem acesso. As mensagens recebidas são passadas para o skeleton, que as desempacota e invoca o método correspondente na interface do objecto, alojado no servidor. O skeleton é também responsável por empacotar a resposta e reencaminhá-la para a proxy. Um objecto cujo estado reside apenas numa máquina, designa-se por objecto remoto. 3.2 Ligação de um cliente a um objecto (bind) Geralmente um sistema que suporta objectos distribuídos tem mecanismos de suporte a referências globais de objectos, válidas em todo o sistema distribuído. Essas referências podem ser passadas entre processos a executar em diferentes máquinas. Após a obtenção de uma referência para um objecto é necessário efectuar uma ligação ao objecto referenciado, antes de puder invocar métodos. Essa ligação traduz-se na colocação de uma proxy no espaço de memória do cliente, tal como referido na Secção 3.1. A ligação pode ser feita de forma explícita ou implícita. Na ligação explícita o cliente tem de invocar uma função especial para fazer a ligação ao objecto, podendo depois disso invocar os métodos pretendidos. Na ligação implícita podem invocar-se os métodos pretendidos, sendo a ligação estabelecida de forma transparente. 3.3 Passagem de parâmetros As referências para objectos podem ser passadas como parâmetros na invocação de métodos. As referências são passadas por valor, o que neste caso corresponde à sua cópia de de uma máquina para a outra. Após receber uma referência, um processo apenas necessita de fazer a ligação ao objecto no momento em que precisar de invocar um método nesse mesmo objecto. Caso todos as operações fossem efectuadas sobre referências, o desempenho do sistema poderia ser muito baixo, principalmente no caso de objectos de pequena dimensão, como inteiros ou booleanos. Cada invocação por parte de um cliente que não estivesse no mesmo espaço de endereçamento do servidor, iria gerar um pedido entre diferentes espaços de endereçamento, ou pior ainda, entre diferentes máquinas. Por esse motivo, por vezes é feita distinção no tratamento de objectos locais e objectos remotos. Ao invocar um método em que é passada a referência de um objecto como parâmetro, essa referência é copiada e passada por valor, caso se trate de uma referência para um objecto remoto. Neste caso, o objecto é passado literalmente por referência. Caso a referência seja para um objecto local, é passada uma cópia do objecto, ou seja, o objecto é passado por valor. Estas situações estão ilustradas na Figura 3.2. Existe um servidor em execução na máquina C e um cliente na máquina A. O cliente tem referências para um objecto local O1 e 12 um objecto remoto O2, que passa como parâmetros na invocação de um método remoto. Ao invocar o método remoto o cliente passa uma cópia de O1 e uma cópia da referência para O2. Copiar um objecto por completo pode não ser desejável. Assim sendo é forçoso fazer uma distinção entre objectos locais e objectos remotos. Esta distinção viola o princípio da transparência e torna mais complicada a implementação de aplicações distribuídas. Figura 3.2: Passagem de objectos por referência e por valor. 3.4 Objectos distribuídos em Java A invocação de métodos remotos, Java Remote Method Invocation (RMI), faz parte da plataforma Java e é um modelo para objectos distribuídos. Este modelo encontra-se integrado na linguagem e pretende manter a semântica da invocação a métodos locais, facilitando a implementação e utilização de objectos remotos. Desta forma é possível manter um grau elevado de transparência. Porém, no desenvolvimento do Java resolveu-se tornar a distribuição aparente nos casos em que a transparência era ineficiente, difícil, ou impossível de realizar. 3.4.1 O modelo de objectos distribuídos em Java O Java adopta os objectos remotos como a única forma de objectos distribuídos. As interfaces são implementadas por intermédio de uma proxy, tal como descrito na Secção 3.1. Uma proxy aparenta ser um objecto local no espaço de endereçamento do cliente. Existem algumas diferenças entre objectos locais e objectos remotos em Java, mas uma das mais importantes diz respeito ao acesso em exclusivo a determinada região (monitor). Em Java é possível declarar um método associado a um monitor, com recurso à directiva synchronized. No caso de objectos locais, recorrendo a esta directiva, é possível serializar por completo o acesso. No caso de objectos remotos torna-se mais complicado, pois é necessário bloquear os clientes do lado dos clientes (proxy ) ou do lado dos servidores. 13 A abordagem adoptada pela equipa de desenvolvimento do Java RMI foi restringir o bloqueio de objectos remotos apenas para os proxies. Isto significa que na prática os objectos remotos não podem ser protegidos de acessos simultâneos, por parte de processos que estão a utilizar proxies diferentes, recorrendo apenas à directiva synchronized. Desta forma é necessário recorrer a técnicas explícitas de bloqueio, como por exemplo declarar os métodos remotos como synchronized. 3.4.2 Invocação de objectos remotos em Java Uma vez que as diferenças entre um objecto local e um remoto são dificilmente visíveis ao nível da linguagem, a invocação de métodos remotos também é feita de modo quase transparente. Qualquer tipo primitivo ou objecto pode ser passado como argumento de um método remoto, desde que possa ser empacotado. Em Java empacotável é sinónimo de serializável. Para o objecto ser serializável tem de implementar a interface serializable. Teoricamente todos os objectos podem ser serializáveis, com excepção de objectos dependentes do sistema operativo, tais como descritores de ficheiros e sockets. Durante a invocação de um método remoto os objectos locais são passados por valor, ao contrário das referências para objectos remotos que são passadas por referência, tal como descrito na Secção 3.3. Uma referência para um objecto remoto consiste num endereço de rede e um porto, para além de um identificador dentro do espaço de endereçamento do servidor. Uma referência para um objecto remoto tem ainda de conter a pilha de protocolos, usada para comunicação entre o servidor e o cliente. Um objecto remoto é constituído com base em duas classes, a classe servidor e a classe cliente. A classe servidor contém a implementação dos métodos disponibilizados para invocação remota, assim como o estado do objecto. O skeleton é gerado a partir da especificação da interface. A classe cliente contém a implementação do código que é necessário executar no lado de cliente e uma implementação de uma proxy. A proxy é gerada a partir da especificação da interface, tal como o skeleton. O funcionamento da proxy está descrito na Secção 3.1. Para cada invocação remota a proxy estabelece uma ligação com o servidor, sendo a ligação terminada após o retorno da invocação. É por esse motivo que a proxy necessita do endereço de rede e do porto, mencionados anteriormente. Uma proxy tem toda a informação necessária que um cliente necessita para invocar métodos num objecto remoto. Em Java as proxies são serializáveis por isso podem ser passadas como parâmetro na invocação de métodos remotos e usadas como referências para os objectos remotos respectivos. 3.4.3 O registo RMI (rmiregistry) Uma aplicação típica RMI, consiste em dois programas separados: cliente e servidor. O servidor disponibiliza objectos e o cliente invoca métodos nesses objectos. Antes de puder invocar métodos o cliente tem de adquirir referências para os objectos. As referências podem ser obtidas durante a execução normal do programa, como retorno da invocação de um método, ou com recurso ao registo RMI (rmigregistry). 14 Para um cliente puder obter uma referência a partir do rmiregistry é necessário que o servidor tenha efectuado o registo previamente. O registo é um objecto remoto que faz o mapeamento entre nomes (Strings) e referências para objectos remotos. Um processo servidor pode ter o seu próprio registo, ou pode ser utilizado um registo por cada anfitrião. A classe java.rmi.registry.LocateRegistry pode ser usada para obter uma referência para o registo no anfitrião especificado, ou para criar um objecto registo. 3.4.4 Implementação de objectos remotos Uma interface remota é uma interface que declara métodos que podem ser invocados de uma máquina virtual Java remota. Uma interface remota tem de estender directa ou indirectamente a interface java.rmi.Remote. Esta interface funciona como marcador, não definindo qualquer método. Para além das excepções específicas lançadas pela aplicação, a declaração de um método remoto tem de incluir o lançamento da excepção java.rmi.RemoteException ou de uma das suas superclasses. A excepção java.rmi.RemoteException é lançada quando a invocação do método remoto falha por qualquer motivo. Algumas das razões para o lançamento da excepção são: problemas de comunicações, erro no empacotamento ou desempacotamento de parâmetros ou valores de retorno, erros de protocolo, etc. Para a criação e exportação de objectos remotos utilizam-se as classes java.rmi.server.UnicastRemoteObject e java.rmi.activation.Activatable. Enquanto que a classe java.rmi.server.UnicastRemoteObject define um objecto cuja referência é válida apenas enquanto o processo servidor estiver vivo, a classe java.rmi.activation.Activatable é uma classe abstracta que define um objecto que inicia a sua execução quando os seus métodos são invocados, parando a execução quando necessário. Visto que não foram utilizados objectos “activos” no desenvolvimento do trabalho, o funcionamento da classe java.rmi.activation.Activatable não será aprofundado. A classe java.rmi.server.UnicastRemoteObject ou uma classe que a estenda, exporta os objectos aquando a sua construção. Esta exportação consiste na escuta de um porto TCP. Caso a classe não herde de java.rmi.server.UnicastRemoteObject, é possível exportar um objecto recorrendo explicitamente ao método exportObject da classe java.rmi.server.UnicastRemoteObject. Geralmente, uma classe que implementa uma interface remota estende a classe java.rmi.server.UnicastRemoteObject, herdando os comportamentos remotos das superclasses de java.rmi.server.UnicastRemoteObject (equals, hashCode e toString) ou estende uma outra classe remota. A classe pode implementar qualquer número de interfaces e definir métodos que não estão declarados na interface remota, ficando esses métodos disponíveis apenas localmente. Para parar de exportar o objecto existe o método unexportObject da classe java.rmi.server.UnicastRemoteObject. A paragem torna o objecto indisponível para invocações remotas. 15 3.4.5 Limpeza de objectos distribuídos (Garbage Collecting) Tal como num sistema local, é desejável que num sistema distribuído os objectos que já não estão referenciados sejam removidos automaticamente. O sistema RMI tem um algoritmo de contagem de referências baseado em [5]. A contagem de clientes que referenciam um objecto é feita para cada máquina virtual Java. Cada vez que uma nova referência chega a uma máquina virtual Java, é enviada uma mensagem referenced para o servidor do objecto. Se a referência já existir apenas é incrementado um contador na máquina virtual do cliente. À medida que as referências vão deixando de ser utilizadas, o contador é decrementado e quando a última referência local for descartada é enviada uma mensagem unreferenced para o servidor. As mensagens de referenced e unreferenced são utilizadas pelo servidor para actualizar a contagem de clientes para o objecto em questão. Quando não existirem mais clientes a referenciar o objecto, passa a existir uma referência fraca. A referência fraca permite que o objecto seja removido do sistema, caso não haja nenhuma referência local para esse objecto no servidor. Enquanto existir uma referência local para o objecto é possível passar uma referência para o cliente, e o objecto não pode ser removido do sistema. Um objecto que necessite de notificação para quando não existirem mais referências, tem de implementar a interface java.rmi.server.Unreferenced. Para além do mecanismo de contagem de referências, uma referência está associada a um tempo de aluguer (lease). Se um cliente não renovar o aluguer considera-se que houve uma falha do cliente, e o número de clientes é decrementado. É possível controlar o tempo de aluguer recorrendo à propriedade de sistema java.rmi.dgc.leaseValue. Por omissão o tempo de aluguer é de 10 minutos. De referir que um período de aluguer curto permite ter um melhor controlo da falha de clientes e consequentemente uma limpeza mais rápida dos objectos não referenciados, mas leva a um aumento das comunicações associadas à renovação do respectivo aluguer. É possível que um objecto seja removido prematuramente caso ocorram problemas de comunicação, como por exemplo uma partição da rede. Neste caso a invocação de um método remoto por parte do cliente irá gerar uma excepção java.rmi.RemoteException, que tem de ser tratada pela aplicação cliente. 16 Capítulo 4 Comunicação fiável em multicast Pretende-se com este trabalho desenvolver uma cache de dados distribuída. A solução tem de ser tolerante a falhas, de forma a maximizar a disponibilidade da cache. Uma forma de aumentar a tolerância a falhas de um sistema é recorrer a técnicas de replicação. Para fazer a replicação de forma eficiente é necessário o envio de actualizações em multicast. Existem diversos protocolos para comunicação em multicast. O artigo [23] faz um sumário de diversos protocolos existentes e classifica-os segundo as suas características (fiabilidade, retransmissão, gestão de grupos, ...). As características com especial interesse são a fiabilidade e a existência de grupos de comunicação. É neste contexto que surgem os grupos de comunicação fiável. Este tipo de serviço garante que uma mensagem é entregue a todos os membros do grupo. No entanto, garantir a entrega das mensagens pode ser uma tarefa nada trivial, especialmente na presença de falhas. Sendo a fiabilidade o principal requisito, serão referidos de seguida alguns dos protocolos de comunicação em multicast que cumprem este requisito. Começa-se por apresentar uma solução básica. 4.1 Comunicação fiável em multicast básica Grande parte das camadas de transporte oferecem mecanismos de comunicação fiável ponto-a-ponto, mas raramente oferecem mecanismos com garantia de entrega para um conjunto de processos. Quando estes mecanismos não estão disponíveis pode sempre recorrer-se a comunicação fiável ponto-a-ponto, estabelecendo uma ligação com cada um dos processos com os quais se quer comunicar. Esta abordagem é de fácil implementação e pode ser uma boa solução para grupos pequenos, mas não é muito eficiente. Na comunicação fiável em multicast, uma mensagem que é enviada para um grupo de processos deve ser entregue a cada membro do grupo. As dificuldades surgem quando um processo se junta ao grupo, ou quando um dos processos do grupo falha. Torna-se necessário fazer a distinção entre comunicação fiável na presença de processos que podem falhar e comunicação fiável quando se assume que todos os processos funcionam de forma correcta. No primeiro caso, considera-se que a comunicação é fiável quando se pode garantir que todos os membros do grupo que não falharam recebem a mensagem. 17 Admitindo que existe acordo de quem são os elementos do grupo, assumindo que os processos não falham, e ainda, que os processos não se juntam nem saem do grupo durante a comunicação, apenas é necessário garantir que a mensagem é entregue a cada membro do grupo. Desta forma é relativamente fácil implementar uma solução. Considere-se o caso em quem existe apenas um processo que quer enviar uma mensagem para vários processos. Assumindo que estão disponíveis mecanismos de multicast não fiável, podem ocorrer perdas de pacotes, sendo a mensagem entregue apenas a alguns dos processos. A Figura 4.11 ilustra uma possível solução para o problema. O processo emissor atribuí um número de sequência a cada mensagem enviada em multicast. Admite-se que as mensagens são recebidas pela ordem em que foram enviadas. Cada mensagem enviada em multicast fica guardada no histórico do processo emissor enquanto não for recebida uma confirmação de entrega (ACK) por parte de cada um dos receptores. Se um receptor detectar que uma mensagem está em falta, envia um NACK, indicando ao emissor que é necessário efectuar uma retransmissão. Figura 4.1: Uma solução simples para comunicação fiável em multicast, onde são conhecidos todos os receptores e se assume que não há falhas em nenhum dos processos. (a) Transmissão da mensagem. (b) Feedback. 4.2 Escalabilidade em comunicação fiável em multicast Existem diversos problemas na solução apresentada na Secção 4.1. A solução não é escalável, pois admitindo que existem N receptores, o emissor tem de estar preparado para receber N ACKs. Uma possível solução para este problema é enviar apenas NACKs. No entanto o problema não fica resolvido, pois pode ocorrer uma situação em que uma mensagem não é entregue a 1 Figura 18 7-8 de [37] nenhum dos receptores, originando N NACKs. Além disso esta possível solução levantaria um problema relacionado com a dimensão do histórico do emissor, pois caso um dos receptores recebesse correctamente todas as mensagens, o emissor não receberia feedback e as mensagens teriam de ser guardadas eternamente. Logicamente isso não seria implementável, sendo necessário definir um tamanho máximo para o histórico que ao ser atingido levaria ao descarte de mensagens, podendo mais tarde um pedido de retransmissão não ser satisfeito. Diversas soluções foram propostas com o intuito de atingir escalabilidade em comunicações fiáveis em multicast. Seguidamente apresentam-se duas dessas soluções. 4.2.1 Controlo de feedback não hierárquico Como referido na Secção 4.2 um dos principais problemas é devido ao grande número de mensagens de feedback. Assim sendo, torna-se necessário arranjar formas de diminuir o número de mensagens de retorno. Um dos mecanismo adoptado foi a supressão de feedback e é o esquema que está na origem do protocolo Scalable Reliable Multicast (SRM) descrito em [10]. No protocolo SRM os receptores apenas enviam NACKs. Os NACKs são enviados em multicast, pelos receptores que necessitam da retransmissão de um pacote perdido. O pedido de retransmissão não tem de ser necessariamente satisfeito pelo emissor, podendo ser satisfeito por qualquer um dos processos que tenha a informação. Para evitar a existência de múltiplas cópias dos dados retransmitidos, as retransmissões são feitas em multicast para todo o grupo. O mecanismo de supressão de feedback entra em acção antes do envio de um NACK ou antes da retransmissão de dados, onde um processo espera um tempo aleatório e no qual suprime a sua própria transmissão, caso a ouça por parte de outro elemento do grupo. A Figura 4.22 ilustra o funcionamento do mecanismo de supressão de feedback. Figura 4.2: Vários receptores agendaram um pedido de retransmissão, mas o primeiro pedido de retransmissão leva à supressão dos outros. Os mecanismos de supressão de feedback conduzem a soluções que escalam bem, contudo também sofrem de problemas. 2 Figura 7-9 de [37] 19 O principal problema está em conseguir que apenas um NACK seja enviado, ou que a retransmissão seja feita apenas por um processo. É preciso que o tempo de propagação das mensagens seja baixo, para que as mensagens em multicast sejam rapidamente recebidas pelos diversos processos. Este requisito verifica-se nas redes locais, mas não para o caso de redes largas em que é espectável que o número de mensagens seja superior ao desejado, ou seja, superior a uma mensagem por cada NACK ou retransmissão global necessária. Poderia optar-se pela segmentação do tempo em slots, mas essa solução levaria à necessidade de sincronização das estações, o que levanta outros tantos problemas. Outro problema prende-se com a necessidade de processamento de mensagens, por parte de processos que já receberam a mensagem correctamente, ou seja, mensagens que são inúteis para eles. 4.2.2 Controlo de feedback hierárquico A supressão de feedback tal como descrita na Secção 4.2.1 é uma solução não hierárquica. Para puder obter uma solução realmente escalável, para grupos muito grandes, é preciso recorrer a abordagens hierárquicas. A Figura 4.33 apresenta a visão geral de uma solução hierárquica. Para simplificar, assumese que apenas um processo quer transmitir uma mensagem em multicast para um grande grupo de processos. O grupo de processos está dividido em partições, cada uma formando um subgrupo, que estão organizadas em forma de árvore. O subgrupo que contem o emissor forma a raiz da árvore. Dentro de cada um dos subgrupos pode ser utilizado qualquer esquema de multicast que funcione para pequenos grupos. Figura 4.3: Vista geral de uma solução hierárquica de comunicação fiável em multicast. Cada coordenador reencaminha a mensagem para os seus filhos e atende pedidos de retransmissão. Cada subgrupo nomeia um coordenador local, que é responsável por atender pedidos de retransmissão por parte dos receptores do seu subgrupo. Desta forma, cada coordenador 3 Figura 20 7-10 de [37] local terá o seu próprio histórico. Se o próprio coordenador tiver perdido uma mensagem, ele pede ao coordenador do subgrupo pai para retransmitir a mensagem. Num esquema baseado em ACK, um coordenador local envia um ACK para o seu pai se tiver recebido a mensagem. Se o coordenador tiver recebido ACKs para a mensagem por parte de todos os membros do subgrupo, bem como por parte dos seus filhos, a mensagem pode ser removida do histórico. O problema principal desta solução tem a ver com a construção da árvore, pois por vezes há necessidade de construir a árvore de forma dinâmica. Posto isto, a construção de mecanismos fiáveis de comunicação em multicast que escalem para um grande número de receptores, dispersos por uma rede wide, é um problema de difícil resolução. Não existe apenas uma solução e cada nova solução apresentada introduz novos problemas. Alguns dos protocolos que adoptam soluções de controlo de feedback hierárquico: TreeBased Multicast Transport Protocol [46], Log-Based Receiver-Reliable Multicast [15], Reliable Multicast Transport Protocol [19], Structure-Oriented Resilient Multicast [45]. 4.3 Multicast Atómico Nas Secções 4.1 e 4.2 admitia-se que os processos não falhavam. Esta secção aborda a comunicação fiável em multicast, na presença de falhas. Em sistemas distribuidos, geralmente o necessário é garantir que a mensagem é entregue a todos os processos, ou a nenhum. Por vezes é ainda necessário garantir que todos os processos recebem as mensagens pela mesma ordem. Este problema é conhecido por multicast atómico. Para perceber porque é que a propriedade atómica é tão importante considere-se o exemplo de uma base de dados replicada, construída como uma aplicação sobre um sistema distribuído. O sistema distribuído oferece mecanismos de envio de mensagens em multicast e, em particular, permite a criação de grupos de processos para os quais as mensagens podem ser enviadas de forma fiável. A base de dados replicada é então construída como um grupo de processos, com um processo para cada uma das réplicas. Operações de actualização são sempre enviadas em multicast para todas as réplicas e efectuadas localmente, ou seja, é utilizado um protocolo de replicação activa. Supondo que uma série de actualizações está para ser executada, mas que durante a execução de uma das actualizações uma das réplicas falha. A réplica que falhou perdeu a actualização, mas todas as outras réplicas efectuam a actualização correctamente. Quando a réplica recupera, pode ter perdido diversas actualizações. Torna-se necessário sincronizar o estado com as outras réplicas, efectuando as operações pela ordem correcta. Supondo agora que o sistema distribuído suporta comunicação em multicast atómico: a actualização enviada para todas as réplicas antes de uma delas falhar é efectuada em todas a réplicas que não falharam, ou em nenhuma. Na comunicação em multicast atómico, uma operação só é efectuada se todos os processos estiverem de acordo em relação aos membros do grupo. Neste caso, a operação só teria sucesso se os restantes membros do grupo chegassem a acordo em relação à falha da réplica. Ao recuperar, a réplica tem de se juntar novamente ao grupo. Não receberá actualizações enquanto não estiver registada como membro. Para se juntar ao grupo é necessário que o seu estado esteja coerente com o dos restantes membros do grupo. 21 Desta forma a comunicação em multicast atómico garante que os processos que não falharam mantêm a base de dados consistente, e obriga à reconciliação dos processos após a sua recuperação e nova junção ao grupo. 4.3.1 Sincronismo Virtual Na presença de falhas, os mecanismos de comunicação fiável em multicast podem ser univocamente definidos com base em alterações nos membros do grupo. Antes de mais é necessário fazer uma distinção entre a recepção de uma mensagem e a entrega de uma mensagem. Adopta-se um modelo em que o sistema distribuído consiste numa camada de comunicação. A Figura 4.44 ilustra este modelo e a distinção entre recepção e entrega de mensagens. Figura 4.4: Organização lógica de um sistema distribuído, para distinguir a recepção da entrega de mensagens. Uma mensagem recebida é guardada na camada de comunicação, até puder ser entrega à aplicação, que se encontra num nível superior. A ideia base do multicast atómico consiste na entrega de uma mensagem em multicast, univocamente associada a uma lista de processos à qual tem de ser entregue. Esta lista constituí uma vista do grupo, correspondente aos processos existentes no grupo, vistos pelo emissor, na altura em que a mensagem foi enviada. Um ponto importante é o facto de cada processo da lista ter a mesma vista, ou seja, todos os processos têm de acordar que a mensagem tem de ser entregue a cada um deles. Supondo que uma mensagem m é enviada no momento é que o emissor tem a vista G. Enquanto a mensagem está a ser enviada, um processo entra ou sai do grupo. A mudança é anunciada para todos os processos em G, ou seja, ocorre uma mudança de vista, enviando uma mensagem mv anunciando a entrada ou saída de um processo. Passam a existir 4 Figura 22 7-11 de [37] duas mensagens em trânsito: m e mv. O que é necessário garantir é que a mensagem m é entregue a todos os elementos de G ou que m não é entregue a nenhum. Existe apenas uma situação na qual o envio da mensagem m pode falhar, que corresponde à falha do emissor, caso contrário a mensagem deve ser entregue a todos os processos da vista G, que continuam a executar correctamente. Caso o emissor falhe, a mensagem pode ser entregue a todos os outros processos na vista G, ou ignorada por todos. Um sistema de comunicação fiável em multicast com estas características designa-se por virtualmente síncrono [4]. Como exemplo de funcionamento, considere-se a Figura 4.55 . Num dado momento o processo P1 junta-se ao grupo, o qual passa a ser constituído pelos processos P1 a P4. Após o envio de algumas mensagens para o grupo o processo P3 falha. Contudo, antes de falhar a mensagem é enviada para P2 e P4, mas não para P1. O sincronismo virtual garante que a mensagem não é entregue a nenhum dos processos, ou seja, é como se o processo P3 não tivesse enviado a mensagem antes de falhar. Figura 4.5: O princípio de funcionamento do sincronismo virtual em multicast. Após o processo P3 ter sido removido do grupo, a comunicação continua entre os restantes membros. Mais tarde o processo P3 recupera e junta-se ao grupo, mas apenas após ter actualizado o seu estado com os restantes processos. O princípio do sincronismo virtual admite que todas as comunicações em multicast ocorrem entre alterações de vistas, ou seja, uma alteração de vista funciona como uma barreira através da qual nenhuma comunicação em multicast pode passar. 4.4 The Jgroup Project O projecto Jgroup é uma integração da tecnologia de comunicação de grupos com objectos distribuídos. Jgroup suporta um paradigma de programação de grupos de objectos, que permite o desenvolvimento de serviços fiáveis e de alta disponibilidade, baseado em replicação. 5 Figura 7-12 de [37] 23 O Jgroup estende as funcionalidades do Java RMI apresentado na Secção 3.4, permitindo esconder dos clientes o facto de um objecto puder ser implementado por um grupo de objectos, em vez de um único. Por outro lado, recorrendo à comunicação por grupos os servidores podem coordenar as suas acções por forma a manter a consistência global do sistema distribuído. Na implementação da comunicação por grupos o Jgroup segue de perto a semântica do sincronismo virtual, apresentada na Secção 4.3.1. O Jgroup permite que os servidores formem um grupo de comunicação interna, com comunicação em multicast, para manter a consistência do sistema distribuído, não sendo necessário os clientes juntarem-se ao grupo. Por omissão, os cliente interagem com o grupo de servidores através de uma interface externa, com uma semântica anycast, em que a operação é efectuada em qualquer um dos elementos do grupo servidor. No entanto é também possível invocar métodos nos servidores em multicast. Esta característica é bastante importante, pois permite que os grupos tenham apenas a dimensão necessária para atingir a disponibilidade e fiabilidade pretendida, permitindo a existência de um grande número de clientes e consequentemente que o sistema seja escalável. Jgroup é baseado no modelo de objectos distribuídos em Java e encontra-se implementado totalmente em Java. O código fonte do Jgroup encontra-se disponível livremente na Internet [3] sobre licença Lesser General Public License (LGPL) [13]. 4.4.1 Composição do Jgroup O Jgroup pode ser decomposto em 3 serviços: Group Membership Service (GMS), Group Method Invocation Service (GMIS) e State Merging Service (SMS). O SMS é um serviço que permite que o sistema distribuído esteja particionado, podendo posteriormente voltar a ser formado apenas por uma única partição. Este serviço existe para fazer a junção de partições. Uma vez que neste trabalho não se pretende ter um sistema particionado, este serviço não será utilizado e consequentemente o seu funcionamento não será detalhado. Seguidamente descreve-se o GMS e GMIS. 4.4.1.1 Group Membership Service - GMS Um grupo é um conjunto de objectos servidores que cooperam para disponibilizar um serviço distribuído. Para aumentar a flexibilidade, a composição do grupo pode variar à medida que novos servidores são adicionados e os existentes são removidos. Servidores que pretendam contribuir para o serviço distribuído juntam-se ao grupo, tornando-se membros. Mais tarde, um membro pode decidir deixar de contribuir saindo do grupo. A qualquer momento, a lista de membros de um grupo inclui os servidores que estão operacionais e que se juntaram ao grupo, mas ainda não sairam do grupo. A tarefa do GMS é registar variações voluntárias na lista de membros, assim como variações involuntárias devidas a falhas e a problemas de comunicação entre servidores. Todas as variações na lista de membros são reportadas a cada um deles, através da instalação de vistas, tal como descrito na Secção 4.3.1. Uma vista consiste numa lista dos membros e de um identificador único. 24 A especificação do GMS teve em conta diversos aspectos. O serviço tem de registar as alterações de forma correcta e atempada6 , para que as vistas instaladas possam reflectir informação recente acerca da composição do grupo. Uma vista só pode ser instalada depois de todos os elementos contidos nessa vista chegarem a acordo da composição da mesma. Por fim o GMS tem de garantir que duas vistas instaladas por dois servidores diferentes são instaladas pela mesma ordem. As últimas duas propriedades são necessárias para que localmente um servidor possa saber o que se está a passar no sistema distribuído. 4.4.1.2 Group Method Invocation Service - GMIS O Jgroup difere de outros sistemas de grupos de objectos devido à adopção de apenas um paradigma de comunicação, totalmente baseada em invocação de métodos em grupos. Clientes e servidores interagem com os grupos, invocando remotamente métodos neles. Ainda que clientes e servidores partilhem o mesmo paradigma de comunicação, distinguese entre invocação de métodos internos ao grupo (Internal Group Method Invocation - IGMI), utilizados pelos servidores e invocação de métodos de forma externa ao grupo (External Group Method Invocation - EGMI), utilizados pelos clientes. Existem diversas razões para fazer esta distinção: • Visibilidade: Os métodos necessários para implementar o serviço de replicação não devem ser visíveis para os clientes. Os clientes devem apenas ter acesso às interfaces que definem o serviço, enquanto os métodos para comunicação entre servidores devem ser privados aos membros do grupo. • Transparência: O Jgroup fornece um mecanismo de invocação para os clientes que pretende ser transparente em relação à implementação do Java RMI, ou seja, os clientes não têm de saber se estão a invocar um método num grupo de servidores, ou apenas num único. • Eficiência: Caso as especificações da EGMI e da IGMI fossem iguais, seria necessário que os clientes se tornassem membros do grupo, o que levaria a que a escalabilidade do sistema fosse mais reduzida. A EGMI tem uma semântica mais fraca do que a IGMI. Ao tornar esta diferença aparente para o programador é possível tornar o sistema mais escalável, limitando os custos mais elevados da comunicação entre membros do grupo, a um número tipicamente inferior ao que seria necessário, caso os clientes fizessem parte do grupo. Ao desenvolver sistemas distribuídos utilizando o Jgroup os métodos para comunicação interna são agrupados para formar a interface remota interna do objecto servidor, enquanto que os métodos externos são agrupados para formar a sua interface remota externa. Em tempo de execução, é gerada uma proxy capaz de atender a invocação de métodos em grupos, baseada nas interfaces remotas do objecto servidor. Esta proxy permite a um objecto cliente, ou servidor, comunicar com o grupo de objectos servidores, como se duma invocação Java RMI habitual se tratasse. Para puder invocar métodos internos ao grupo, os servidores têm de obter um proxy de grupo por parte do Jgroup, em execução na máquina virtual Java local. Por outro lado, clientes 6 Não existem garantias temporais. Apenas é garantido que a instalação de novas vistas não será adiada indefini- damente. 25 que precisem de interagir com um grupo necessitam de obter uma referência a partir de um serviço de registo. O Jgroup tem um serviço de registo próprio, que será descrito na Secção 4.4.2. Este serviço é uma extensão do registo incluído no Java RMI e assim sendo o funcionamento básico é o descrito na Secção 3.4.3. À semelhança das definições feitas na Secção 4.3.1, é necessário distinguir entre fazer e completar a invocação de um método. Fazer a invocação de um método corresponde ao momento em que a invocação é feita. Completar a invocação do método corresponde ao momento em que a execução do método termina. De seguida explica-se o funcionamento da EGMI e da IGMI. Internal Group Method Invocation - IGMI Ao contrário do Java RMI tradicional, a IGMI retorna um vector de resultados, em vez de um único valor. A IGMI está disponível de forma síncrona e assíncrona. Na forma síncrona, o processo que invocou o método fica bloqueado até que um vector com os resultados de cada um dos membros do grupo lhe possa ser entregue. Podem haver situações em que este comportamento não é desejável, por causa do tempo que o processo pode ficar bloqueado. Além disso é necessário ter consciência que podem ocorrer deadlocks caso existam invocações circulares. Na forma assíncrona, o processo que invocou o método não fica bloqueado mas especifica um objecto de callback, que será notificado quando o resultado estiver disponível. Se o método invocado não tiver valor de retorno (void) um processo ao fazer uma invocação tem duas possibilidades: especificar o objecto de callback para ser notificado que a invocação está completa, ou especificar null indicando que não pretende ser notificado. Uma IGMI é completa de acordo com o sincronismo virtual, descrito na Secção 4.3.1. É garantido que uma IGMI termina com um vector de resultados (contendo, pelo menos, o valor de retorno computado pelo próprio processo que fez a invocação), ou com uma das excepções definidas no método invocado. Além disso, se um servidor S completar uma IGMI numa vista, todos os servidores com a mesma vista também completam a mesma invocação, ou S irá instalar uma nova vista. Desta forma é garantido que a invocação será completa por todos os servidores do grupo que não falharam. A IGMI satisfaz a propriedade de integridade na qual uma invocação só é completa por cada um dos servidores no máximo uma vez, e apenas se algum dos servidores tiver feito a invocação previamente. O Jgroup garante que cada IGMI é completa no máximo numa vista, ou seja, se servidores completaram a mesma IGMI, não o podem ter feito em vistas diferentes. Desta forma, garante-se que todos os resultados contidos na vector de resposta foram computados durante a mesma vista. External Group Method Invocation - EGMI A EGMI que caracteriza a interacção entre cliente e servidor é completamente transparente para o cliente, que a utiliza como se de uma RMI padrão se tratasse. A EGMI permite dois tipos de invocação: anycast e multicast. Uma invocação em anycast é completa pelo menos por um servidor do grupo, a não ser que não exista nenhum servidor 26 operacional. Uma invocação em multicast é completa por todos os servidores do grupo. A escolha do tipo de invocação pode ser especificada para cada um dos métodos da interface remota. Por defeito a semântica é anycast. Para usar multicast é necessário indicar que o método lança a excepção McastRemoteException. Durante a geração do código da proxy e do skeleton a interface é inspeccionada para verificar qual a semântica especificada. É garantido que se existir pelo menos um servidor operacional a EGMI retornará um valor, ou uma das excepções especificadas na declaração do método será lançada. Além disso, uma EGMI é efectuada no máximo uma vez por cada servidor e apenas se um cliente tiver invocado o método anteriormente. Estas duas propriedades aplicam-se quer para invocação de métodos em multicast, quer para unicast. No caso do multicast é também garantido o sincronismo virtual. IGMI e EGMI diferem num aspecto importante. Se uma IGMI completar, é garantido que o fará com a mesma vista para todos os servidores, no entanto uma EGMI pode concluir em diferentes vistas concorrentes. Esta situação é possível caso um servidor tenha completo a invocação, mas tenha sido particionado antes de entregar o resultado ao cliente. O proxy do cliente detecta uma falha do servidor e invoca novamente o método. A única solução para o problema seria o cliente juntar-se ao grupo, antes de invocar o método. Desta forma o cliente participaria no protocolo de acordo de vistas e veria a invocação do método atrasada, até uma nova vista ser instalada. No entanto, essa solução traria problemas de escalabilidade, tal como referido na Seccção 4.4.1.2. Os problemas anteriores têm origem na divisão do sistema em partições, contudo tal como referido na Secção 4.4.1, não se pretende ter um sistema particionado. 4.4.2 O serviço de registo do Jgroup Ao introduzir o paradigma de grupos de objectos, referiu-se que a transparência era um ponto importante, não pudendo os clientes distinguir a invocação de um método num grupo de servidores replicados, de uma simples invocação Java RMI. A Secção 3.4.3 explica como é que se pode obter uma referência a partir do registo RMI padrão. Este registo introduz um ponto de falha único. Caso falhe não é possível obter referências para objectos remotos. Além disso, o registo RMI padrão não permite obter referências para grupos de objectos. Para colmatar estas falhas o Jgroup incluí o Dependable Registry Service (DRS), onde os grupos de servidores podem registar as suas referências e os clientes as podem obter. O DRS é constituído por um grupo de servidores que mantêm um registo replicado dos pares nome/grupo de servidores, resolvendo o problema do ponto de falha único do registo RMI. De notar que a implementação do DRS recorre ao próprio Jgroup, ou seja, existe um conjunto de servidores organizados em grupo, para fornecer o serviço, ao qual os clientes acedem através de EGMI. O registo do Jgroup apresenta vantagens em relação ao registo RMI. Como já referido uma delas é a oferta de um registo altamente disponível. Outra é o facto de os clientes não precisarem de saber a localização do registo, como acontecia na implementação incluída no JDK. Os clientes interagem com as replicas como se de apenas um servidor se tratasse. 27 4.4.3 Configuração do sistema distribuído De forma a puder fazer alterações no sistema distribuído, sem ter de recompilar as aplicações, a configuração das máquinas que estão a utilizar o Jgroup é feita com recurso a um ficheiro de configuração. Além da composição do sistema distribuído, o ficheiro permite ainda a configuração de portos de acesso e a definição de parâmetros do sistema de transporte. A Figura 4.6 ilustra o preenchimento do ficheiro. <?xml version="1.0" encoding="us-ascii"?> <!DOCTYPE Configuration SYSTEM "config.dtd"> <Configuration version="1.0"> <BootstrapRegistry port="44002"/> <Transport payload="1024" maxTTL="10" TTLWarning="5"/> <DistributedSystem> <Domain name="ist.utl.pt" address="226.1.1.1" port="44001"> <Host name="sigma01" port="44000"/> <Host name="sigma03" port="44000"/> <Host name="sigma04" port="44000"/> <Host name="sigma05" port="44000"/> <Host name="sigma06" port="44000"/> </Domain> </DistributedSystem> </Configuration> Figura 4.6: Exemplo do preenchimento do ficheiro config.xml. O atributo port do elemento BootstrapRegistry permite definir o porto de comunicação no qual serviço de registo do Jgroup ficará à escuta. O elemento Transport permite definir alguns parâmetros da camada de transporte, tais como: tamanho do conteúdo do pacote, número máximo de saltos entre a origem e o destino, aviso quando um determinado número de saltos é ultrapassado, etc. Relativamente ao sistema distribuído, é necessário configurar o domínio, endereço para comunicação em multicast e o respectivo porto. É ainda necessário configurar o nome das máquinas e o endereço para comunicação em unicast. De notar que é necessária a existência deste ficheiro em todas as máquinas que estão a executar a aplicação. 28 Parte II Desenvolvimento de uma cache de dados distribuída Capítulo 5 Serviço de Cache centralizado A elaboração deste trabalho foi efectuada com base num serviço de cache centralizado já existente, que será descrito ao longo deste capítulo. A aplicação em funcionamento sobre a cache é responsável pela execução de tarefas de fluxos de processo. Cada fluxo de processo tem associado um identificador, a partir do qual é possível identificar univocamente o seu contexto de execução. Um contexto de execução representa relações entre entidades existentes no sistema, para um fluxo específico. Uma entidade pode ser referenciada por diversos contextos simultaneamente. A estrutura de cache existente é uma estrutura de dois níveis, em que o nível superior apenas tem conhecimento de contextos, enquanto o nível inferior opera com entidades. As relações entre entidades podem ser de um para um, ou de um para muitos. São as relações entre entidades que dão origem a uma estrutura hierárquica. 5.1 Estrutura hierárquica Em termos de representação, um contexto de execução é um objecto da classe CachedParameterMap e uma entidade é um objecto da classe CachedObject. Um CachedParameterMap representa relações entre CachedObjects. Um CachedObject é constituído por um conjunto de tipos elementares (boolean, int, string, ...), com representação física em entidades da base de dados. Este objecto armazena em memória os valores lidos da base de dados, bem como bandeiras (flags), que permitem saber se o objecto sofreu alterações em memória que ainda não foram reflectidas na base de dados. A Figura 5.1 representa um CachedParameterMap. Este CachedParameterMap contém relações entre dois CachedObjects: ORDER e CLIENT. O CachedObject ORDER é constituído por OrderID, DataLancamento e ReferenciaDeOrdem, correspondendo os seus valores a uma entrada da tabela ENT_T_ORDER, na base de dados. O CachedObject CLIENT é constituído por ClientID, Nome, Morada e TelefoneDeContacto, correspondendo os seus valores a uma entrada da tabela ENT_T_CLIENT, na base de dados. 31 Figura 5.1: Contexto de execução básico. Existe uma relação de um para um, entre os CachedObjects da Figura 5.1, mas tal como se ilustra na Figura 5.2 também podem existir relações de um para N. Figura 5.2: Relações entre entidades. Um CacheParameterMap é definido univocamente pelo CachedObject ORDER. As associações entre CachedObjects são válidas para um contexto específico e dão origem a entradas na tabela PRV_T_RELATION, da base de dados, com o esquema indicado na Tabela 5.1. Tabela 5.1: Esquema da tabela de relações. ORDER_ID ORIG_ENTITY ORIG_KEY DEST_ENTITY DEST_KEY RELATION_INDEX A coluna ORDER_ID corresponde ao identificador do CachedObject ORDER e identifica univocamente o contexto de execução. As colunas ORIG_ENTITY e DEST_ENTITY correspondem à definição da relação hierárquica de pai para filho, tendo como identificadores as entradas das colunas ORIG_KEY e DEST_KEY. A coluna RELATION_INDEX indica o índice 32 da relação (0 a N-1). A uma relação de um para um está sempre associado o índice 0. Para o exemplo da Figura 5.1, as entradas da tabela PRV_T_RELATION são as indicadas na Tabela 5.2. Tabela 5.2: Preenchimento da tabela de relações para o exemplo da Figura 5.1. ORDER_ID ORIG_ENTITY ORIG_KEY DEST_ENTITY DEST_KEY RELATION_INDEX 99 ENT_ORDER 99 ENT_CLIENT 56 0 5.2 Arquitectura A Figura 5.3 ilustra a arquitectura do serviço de cache. Figura 5.3: Arquitectura do serviço de cache. Existem três camadas distintas no serviço de cache: apresentação, partilha de dados e leitura e escrita. As três camadas encontram-se organizadas de acordo com a Figura 5.4. A camada de apresentação disponibiliza aos diversos processos os dados organizados de acordo com uma determinada estrutura. A camada de partilha de dados mantém a coerência da informação, garantindo que múltiplas estruturas de visualização lêem e modificam os mesmos dados. Por fim, a camada de leitura e escrita garante as funcionalidades básicas de gestão de uma cache: ler dados para memória e sincronizar com a base de dados as alterações efectuadas em memória. 5.2.1 Camada de apresentação A classe de suporte da camada de apresentação é a CachedParameterMap. É nesta camada que os objectos estão organizados hierarquicamente, tal como descrito na Secção 5.1. Os principais métodos disponibilizados por esta camada são: getParameter, setParameter, synch, assign e unassign. 33 Figura 5.4: Organização do serviço de cache em camadas. Os métodos getParameter e setParameter permitem aceder e alterar, respectivamente, o valor de um parâmetro específico de um CachedObject. As invocações dos métodos são feitas tendo em conta a estrutura hierárquica do CachedParameterMap. Considere-se o contexto da Figura 5.5. O acesso ao campo Morada é feito usando o seguinte caminho: Ordem.Cliente.Morada. Este caminho representa um nome lógico do parâmetro. Figura 5.5: Exemplo de contexto. Os métodos assign e unassign permitem inserir e remover um elemento da estrutura hierárquica, respectivamente. Estas alterações têm efeito imediato na base de dados, ou seja, é utilizada uma estratégia write-through para alterações na estrutura hierárquica. 34 O método sync permite guardar (na base de dados) as alterações efectuadas (em memória), em todos os CachedObjects referenciados pelo CachedParameterMap. Desta forma, as alterações de CachedOjects são feitas com uma estratégia write-back. 5.2.2 Camada de partilha de dados A camada de partilha de dados é implementada pelas classes CachedObjectsManager e ResourceCacheServiceBase, que assentam na utilização dos CachedObjects. O serviço de cache propriamente dito é fornecido pela classe CachedObjectManager, enquanto que a classe ResourceCacheServiceBase é responsável pelo acesso aos CachedObjects. Os principais métodos disponibilizados pela classe CachedObjectsManager à camada de apresentação são: assign, unassign, synch, getParameter e setParameter. Os nomes dos métodos são iguais aos da camada de apresentação, Secção 5.2.1, assim como as funções por eles desempenhadas. De notar a diferença no acesso aos parâmetros do CachedObject. Na passagem da camada de apresentação para a camada de partilha de dados ocorre uma transformação dos nomes dos parâmetros. Passam de nomes lógicos para nomes físicos, correspondentes aos existentes na base de dados. A transformação referida é feita com recurso a serviços externos à cache (serviço de definição de contextos e serviço de definição de entidades). O serviço de cache trabalha em conjunto com estes e outros serviços. As funcionalidades dos serviços não serão descritas pois estão fora do âmbito deste trabalho, sendo apenas referidas quando se considere necessário. A arquitectura do serviço de cache, contemplando as interacções com os restantes serviços pode ser consultada no Anexo A. Os principais métodos disponibilizados pela classe ResourceCacheServiceBase são exclusiveUseWait, release e remove. O método exclusiveUseWait permite aceder em exclusividade a um objecto da cache. Caso um objecto esteja em uso, a invocação é suspensa até que o objecto esteja livre. Se o objecto não estiver em cache, é carregado a partir da base de dados e colocado na cache. O método release liberta um objecto de utilização, permitindo que seja utilizado por outro processo. Por fim, o método remove remove um objecto da cache, mas não o remove da base de dados. A classe ResourceCacheServiceBase permite o armazenamento de objectos de qualquer tipo. O acesso aos objectos é feito com recurso a um objecto chave. As relações entre os diversos CachedObjects de um contexto, não se encontram nos CachedParameterMaps, mas sim todas juntas no Map _referenceN, do CachedObjectsManager. As relações 1 para N estão expressas em memória sob a forma de vectores de referências, que o serviço de cache identifica através da chave composta: <ORDER_ID>:<Entidade 1>:<Entidade N>:<Campo da Entidade N que aponta para a Entidade 1>:<Identificador da Entidade 1>. A identificação das relações 1 para 1 é feita através da chave composta: <OR- DER_ID>:<Entidade 1>:<Identificador da Entidade 1>:<Entidade N>. Sempre que se actualiza uma relação 1 para 1 é necessário actualizar a relação 1 para N correspondente. Relembrar o exemplo ilustrado na Figura 5.2, em que cada ordem tem associado apenas um cliente, mas um cliente pode estar associado a várias ordens. 35 Interacção entre as camadas de apresentação e partilha de dados Considerando novamente o contexto da Figura 5.5 e relembrando que o acesso à morada é feito através do caminho: Ordem.Cliente.Morada. Para se chegar ao valor da morada, o serviço de cache através da referência Ordem, obtém o CachedObject Ordem com identificador 100. Seguidamente consulta a referência Cli_id através da qual obtém o CachedObject Cliente, com identificador 66. A partir deste momento já é possível consultar/alterar o campo Morada. A Figura 5.6 ilustra este acesso, assim como a interacção entre as camadas de apresentação e partilha de dados. Figura 5.6: Interacção entre as camadas de apresentação e partilha de dados. 5.2.3 Camada de leitura e escrita A camada de leitura e escrita é suportada pela classe CachedObjectsDbStore. Esta classe tem como principais funções a leitura e escrita dos dados das entidades, ou seja, os dados contidos num CachedObject, na base de dados. A camada de leitura e escrita interage com as duas classes da camada de partilha de dados. Os principais métodos da classe CachedObjectsDbStore invocados pela classe ResourceCacheServiceBase são: add, get e delete. O método add escreve um CachedObject na base de dados, executando uma actualização ou inserção, consoante o objecto exista ou não na base de dados. O método get obtém um objecto a partir da base de dados. Em relação à classe CachedObjectsManager, os principais métodos invocados, da camada inferior, são: getRelationsList, addNewRowRelation, deleteRowRelation, createNew e delete. Os três primeiros métodos utilizam a tabela PRV_T_RELATION da base de dados, cujo esquema se encontra na Tabela 5.1. O método getRelationsList procura relações entre duas entidades, tendo em conta uma ordem específica e devolve um Map com as relações encontradas. O método addNewRowRelation adiciona uma linha à tabela de relações e o método removeRowRelation remove uma linha. O método createNew cria uma entrada na tabela, correspondente à entidade especificada na chave, e devolve um CachedObject preenchido com os valores por omissão. Finalmente o método delete, invocado por ambas as 36 classes, apaga um objecto da base de dados. 5.3 Políticas de substituição Esta cache não define uma política de substituição, no verdadeiro sentido da palavra, mas sim uma política de remoção de objectos. Numa política de substituição existe um algoritmo, que é executado de cada vez que a inserção de um objecto em cache, leva a que o número máximo de objectos permitidos seja ultrapassado. Há forçosamente uma substituição de um objecto em cache. Na política de remoção de objectos, se um objecto que deveria ser removido estiver a ser utilizado, a remoção não é feita, ou seja, o número de objectos em cache pode ultrapassar o máximo definido. Visto que a inserção na cache é feita de forma dinâmica, o verdadeiro limite ao tamanho da cache passa a ser a memória livre disponível no servidor. Entenda-se por objecto em utilização, um objecto que foi bloqueado e que ainda não foi libertado. Para evitar a existência eterna de objectos em memória, existe uma thread que executa periodicamente, na tentativa de remover os objectos que já o deveriam ter sido. Uma política de remoção de objectos tem vantagens e inconvenientes face a uma política de substituição. A principal vantagem é que um objecto que se tem a certeza que ainda é necessário não é removido da cache, evitando que as suas alterações sejam escritas na base de dados, seja descartado de memória e carregado novamente da base de dados. A principal desvantagem é o facto de deixar de existir efectivamente um valor máximo, ou seja, o valor máximo existe mas não que tem de ser cumprido de forma rígida. A cache permite definir a política de remoção de objectos. É possível escolher uma de três políticas distintas: criação, modificação e frequência de acesso. Na política de criação, o objecto que se encontra há mais tempo em cache é o primeiro a ser descartado. Na política de modificação, o primeiro objecto a ser removido é o que foi modificado/acedido há mais tempo. Por fim, na política de frequência de acesso, o objecto acedido/modificado menos vezes, num determinado intervalo de tempo, é o primeiro a ser descartado. 37 Capítulo 6 Solução inicial Com este trabalho pretende-se desenvolver uma cache de dados distribuída, partindo da cache local descrita no Capítulo 5. A abordagem inicial do problema foi baseada essencialmente em Remote Method Invocation do Java. Esta abordagem apresenta a vantagem de não ter de lidar com o problema da consistência. Existe apenas um objecto (sem cópias ou réplicas associadas) e uma ou mais referências remotas para esse mesmo objecto. Independentemente de se tratar de uma operação de leitura ou escrita, existe sempre uma invocação remota do objecto sobre o qual se pretende efectuar a operação, tal como descrito no Capítulo 3. 6.1 Arquitectura da solução Aproveitando a arquitectura em camadas apresentada na Figura 5.4, uma solução óbvia passa pela introdução de uma camada distribuída, entre a camada de partilha de dados e a camada de leitura e escrita. A Figura 6.1 ilustra esta arquitectura, com duas caches a partilhar dados. Esta parece ser a solução mais simples, porque a gestão distribuída dos objectos passaria a ser feita em termos de objectos elementares, que têm representação em entidades da base de dados (CachedObjects). Estes objectos apresentam como principal vantagem a constituição por tipos elementares, logo serializáveis, e que podem ser facilmente transmitidos entre máquinas distintas. Além disso, pretende-se manter a concorrência ao mais alto nível e desta forma é possível que um CachedObject seja partilhado por caches distintas. A nova camada terá uma função de cache de nível superior à cache local, na qual um objecto que não existe localmente pode existir numa cache remota. Esta arquitectura em níveis é análoga à existente em hardware nos processadores, nos quais podem existir vários níveis de cache (cache de nível 1, cache de nível 2, ...). À medida que se sobe nos níveis, ou que se desce na arquitectura em camadas, a obtenção do objecto torna-se mais custosa, do ponto de vista temporal. A Figura 6.2 representa a arquitectura da camada de cache distribuída. Esta camada é constituída por um gestor de objectos distribuídos e pelas diversas caches (não é necessário que sejam duas, podem ser mais, ou apenas uma). Existe comunicação entre o gestor e as 39 Figura 6.1: Arquitectura em camadas do serviço de cache distribuído. caches. As caches também comunicam entre si. Figura 6.2: Arquitectura da camada de cache distribuída. O gestor pode estar alojado no mesmo servidor que uma das caches, ou num servidor distinto. 6.2 Obtenção e registo de referências A Figura 6.3 ilustra os passos necessários para obtenção e registo de referências. Admitase que a cache X necessita de um objecto para o qual não tem referência, ou seja, ocorreu um miss na cache local. A cache X faz um pedido ao gestor, especificando a chave do objecto pretendido. Admitindo que o objecto não está a ser utilizado por nenhuma outra cache, o gestor cria um lock 40 (a) (b) Figura 6.3: Obtenção e registo de referências. associado à chave especificada. O gestor dá indicação à cache X que o objecto não está em uso e que tem permissão para fazer o carregamento do objecto a partir da base de dados. Neste momento a cache Y faz um pedido ao gestor, com a mesma chave especificada pela cache X. O gestor verifica que o objecto está a ser carregado por uma outra cache, e a thread responsável pelo processamento do pedido da cache Y fica bloqueada. Em simultâneo, a cache X procede ao carregamento do objecto da base de dados. A leitura da base de dados é feita com recurso às funcionalidades da camada de leitura e escrita. A cache X faz a exportação do objecto, tornando-o num objecto remoto. 41 A cache X acede ao gestor novamente e faz o registo do objecto. Durante o registo o lock é substituído pela referência para o objecto remoto registado, efectuando a libertação de todas as threads que possam estar bloqueadas nesse lock. É inserida uma referência para o objecto na cache local de X. Devido à libertação do lock, a thread responsável pelo processamento do pedido da cache Y é desbloqueada. A cache Y é informada que o objecto está em uso e recebe uma referência para o objecto remoto. É inserida uma referência para o objecto remoto na cache local de Y. De notar que com esta solução, o gestor tem de ter capacidade para alojar referências para todos os objectos existentes nas diversas caches. 6.3 Contagem de clientes A contagem de clientes é feita por objecto. Cada objecto monitoriza os clientes que o estão a utilizar. As próximas secções explicitam de que forma é feito o registo e a remoção de um cliente, assim como detalhes do registo e da remoção de referências do gestor. 6.3.1 Incremento do número de clientes A Figura 6.4 ilustra o incremento do número de clientes. Existem duas situações em que o incremento é efectuado: registo do objecto e aquisição de uma referência para o objecto. A Figura 6.4a apresenta em detalhe o registo de um objecto, no que respeita à contagem de clientes. Após o carregamento do objecto o1 da base de dados, por parte da cache X, o1 encontra-se com 0 clientes. A cache X faz a exportação de o1, tornando-o num objecto remoto. A cache X faz o registo de o1 no gestor. Durante o registo o gestor invoca remotamente o método registerClient em o1, registando um novo cliente. Durante o registo do cliente, o número de clientes passa a 1 e é criado o objecto r1, no espaço de endereçamento da cache X. O objecto r1 contém uma referência para o1. A cache X faz a exportação de r1, tornando-o num objecto remoto. Uma referência para r1 é passada para o gestor, como valor de retorno do método registerClient. Por sua vez, esta referência é passada do gestor para a cache X, como retorno do método register. A cache X coloca a referência para r1 no seu refMap. A Figura 6.4b apresenta de forma detalhada a obtenção de uma referência para um objecto remoto, com incidência na contagem de clientes. A cache Y efectua um pedido ao gestor, especificando a chave do objecto o1. O gestor verifica que o objecto pretendido já está em utilização e invoca remotamente o método registerClient em o1 para efectuar o registo do novo cliente. Durante o registo do cliente o número de clientes de o1 passa a 2 e é criado um novo objecto r2, que referencia o objecto o1. O objecto r2 é exportado pela cache X, tornando-se num objecto remoto. Uma referência para r2 é passada como valor de retorno do método registerClient, para o gestor. A cache Y é informada de que o objecto pretendido está em uso e recebe referências 42 (a) Detalhe do registo de um objecto. (b) Detalhe da obtenção de uma referência. Figura 6.4: Contagem de clientes - incremento. para os objectos remotos o1 e r2. A referência para o1 é colocada na cache local, enquanto que a referência para r2 é colocada no refMap da cache Y. 6.3.2 Decremento do número de clientes O decremento dos clientes de um objecto é efectuado sempre que a referência para o objecto remoto é removida da cache local. Quando não existem mais clientes o objecto é removido do gestor. A Figura 6.5 ilustra os processos de remoção de clientes e de remoção 43 do gestor. O estado inicial do sistema distribuído é o apresentado na Figura 6.4b. (a) Remoção do cliente local. (b) Remoção de cliente remoto e remoção do gestor. Figura 6.5: Contagem de clientes - decremento. A Figura 6.5a ilustra a remoção de um objecto da sua cache local. A cache X começa por remover do refMap a referência para o objecto r1. Invoca o método unregisterClient do objecto o1, que decrementa o número de clientes para um. O objecto o1 ainda não pode ser removido do sistema distribuído, pois ainda tem um cliente. Entretanto, o objecto r1 é limpo pelo garbage collector, pois deixou de ser referenciado. Deixa de existir uma referência na cache local para o1, mas apesar disso ele não é limpo pelo garbage collector, pois ainda existem referências para o objecto no sistema distribuído. 44 Os detalhes sobre a limpeza de objectos distribuídos encontram-se na Secção 3.4.5. A Figura 6.5b ilustra a remoção da referência para um objecto remoto, assim como a remoção de um objecto do sistema distribuído. O estado inicial do sistema é o apresentado na Figura 6.5a. A cache Y remove a referência local que tem para o objecto remoto r2. Invoca o método remoto unregisterClient, sobre o1, que decrementa o número de clientes, ficando este a zero. Entretanto, o objecto r2 é limpo pelo garbage collector, pois não é referenciado localmente pela cache X e deixou de ser referenciado remotamente pela cache Y. Como não existem clientes para o objecto o1, este invoca o método unregister, no gestor. O gestor invoca o método remoto getClientNumber, no objecto o1, para verificar se número de clientes é efectivamente nulo. Esta verificação é necessária para garantir que entretanto não houve um pedido ao gestor para a utilização do objecto o1. Uma vez que não existem mais clientes de o1, a referência é removida do gestor. A invocação do método unregisterClient, por parte da cache Y, retorna e a referência para o1 é removida do espaço de endereçamento da cache Y. Uma vez que deixam de existir referências para o1 no sistema distribuído, o objecto é limpo pelo garbage collector, tal como descrito na Secção 3.4.5. 6.4 Estrutura hierárquica dos dados em cache É necessário garantir que alterações efectuadas na estrutura hierárquica são visíveis por todas as caches, que estão a utilizar um determinado contexto. As relações entre entidades têm significado ao nível do CachedParameterMap, mas estão todas juntas num mesmo Map, tal como descrito na Secção 5.2.2. Uma possível solução para a propagação das alterações seria criar um objecto idêntico ao CachedObject, mas para as relações entre entidades. Este novo objecto seria tratado de forma idêntica ao CachedObject, tal como descrito na Secção 6.2. Admitindo que existem C CachedObjects num contexto, seriam necessárias pelo menos C1 relações, ou seja, seria necessário lidar com C-1 objectos adicionais, no sistema distribuído. Além do aumento da complexidade, uma solução deste género necessitaria de grandes alterações no serviço de cache, e traria complicações adicionais devido às interacções com o serviço de definição de contextos. Outra solução possível seria a criação de um objecto idêntico ao CachedObject, mas que em vez conter apenas uma relação, teria todas as relações de um determinado contexto. Desta forma existiria apenas um objecto adicional, aos C objectos já existentes. Apesar do aumento da complexidade ser reduzido, esta solução provocaria ainda bastantes alterações na cache inicial. De forma a minimizar as alterações ao serviço existente, é necessário recorrer a uma nova abordagem. Uma nova abordagem passa pela introdução da comunicação por grupos, descrita na Secção 4.3. A implementação foi elaborada com recurso à biblioteca Jgroup, descrita na Secção 4.4. 45 A ideia seria criar um grupo de comunicação, entre as diversas caches, para cada um dos contextos. Ou seja, duas caches que estivessem a utilizar o mesmo contexto, seriam membros do mesmo grupo. As operações de assign e unassign seriam efectuadas de forma síncrona em todas as caches que estivessem a usar o contexto em questão. Originalmente seria utilizada uma chave que identificasse univocamente o contexto, para a identificação dos grupos de comunicação. No entanto verificou-se que o identificador do grupo é um número inteiro. Mantendo a ideia anterior, criou-se apenas um grupo de comunicação com todas as caches. Cada cache mantem uma lista dos contextos que está a utilizar. De cada vez que é efectuada uma operação no grupo, a cache antes de fazer a actualização verifica se está a utilizar o contexto em questão. Em caso negativo não faz nada. Esta solução apresenta a desvantagem de ter de processar a operação, mesmo quando o contexto em questão não está a ser utilizado pela cache, mas foi a única encontrada para resolver o problema de forma simples. Pode acontecer que exista uma associação ou des-associação que influencie a execução de fluxos de processo, a correr em servidores distintos. No entanto este comportamento também podia ocorrer para fluxos de processo em execução no mesmo servidor, na cache inicial. O que se pretende é manter o mesmo comportamento da cache inicial. As caches apenas têm em memória as relações necessárias para os contextos que estão em utilização, ou seja, não existe nenhuma cache que tenha conhecimento de todas as relações existentes em base de dados. Assim, quando uma cache pretende utilizar um contexto, faz o carregamento das relações a partir da base de dados. Se existir uma actualização no contexto antes do carregamento da base de dados ter terminado, é efectuado um novo carregamento da base de dados. De notar que as alterações são efectuadas primeiro em base de dados e só depois em memória. 6.5 Tolerância a falhas Devido à sua natureza, um sistema distribuído encontra-se sujeito a mais tipos de falhas do que um sistema centralizado. É necessário minimizar a ocorrência dessas falhas através de mecanismos de recuperação, pois pretende-se alta disponibilidade por parte do sistema. As falhas podem ter diversas origens. Os principais tipos de falhas estão relacionados com: crash de servidor, omissão e timing. Uma falha devida a um crash ocorre quando um servidor termina a sua execução de forma prematura, estando a trabalhar de forma correcta até esse momento. Uma falha por omissão ocorre quando o servidor falha na resposta a um pedido. Esta falha pode dever-se ao facto de o servidor nunca ter recebido o pedido, ou de ter ocorrido uma falha no envio da resposta. Este é o tipo de falha considerado no caso da existência de problemas de comunicação. Uma falha de timing ocorre quando um servidor dá uma resposta, antes do cliente ter alocado o buffer necessário para fazer o alojamento, ou devido a uma resposta tardia por parte do servidor, sendo esta a situação mais frequente. Existem outros tipos de falhas, relacionados com respostas arbitrárias por parte do servidor (falhas "‘Bizantinas"’) e incorrecções nos valores de resposta. 46 6.5.1 Java RMI Uma vez que a comunicação é feita através do Java RMI é necessário perceber como é que os erros são tratados por este modelo para objectos distribuídos. Em Java RMI, a ocorrência de falhas na invocação de métodos remotos é aparente para o programador através do lançamento da excepção java.rmi.RemoteException, tal como descrito na Secção 3.4.4. O principal problema está em saber qual o tipo de falha que ocorreu. Utilizando o Java RMI não é possível saber qual a causa da falha, o que leva a que o tratamento dos diversos tipos seja feito da mesma maneira para todos eles. O Java RMI utiliza o protocolo TCP para comunicação, o que implica que há garantias de entrega das mensagens desde que exista um caminho entre a origem e o destino, e que que esteja um processo à escuta no porto especificado. Contudo não é possível estar indefinidamente à espera de uma resposta, existindo mecanismos de timeout para resolver essa situação. Assim sendo não é possível saber se o servidor teve um crash ou se simplesmente está ocupado a processar um pedido. Caso não seja possível estabelecer uma ligação entre a origem e o destino, não existe forma de saber se o servidor se encontra em baixo, ou se é um problema de comunicação. Devido à impossibilidade de identificar correctamente o tipo de falha ocorrida, na implementação efectuada optou-se por assumir que todas as falhas são devidas a crash de servidores. 6.5.2 Pontos de falha A arquitectura desta solução é constituída pelas várias caches e um único gestor, tal como descrito na Secção 6.1. É possível recuperar das falhas das caches, mas não da falha do gestor. Desta forma o gestor apresenta-se como um ponto de falha do sistema. Caso ocorram falhas no gestor todo o sistema falha. Relembrar que o próprio Java RMI introduz um ponto de falha adicional, o registo RMI, tal como descrito na Secção 4.4.2. A instância do registo RMI é criada pelo gestor, de forma a concentrar os diversos pontos de falha num só. O sistema possui uma única base de dados. Visto que não existe replicação, a base de dados é também um ponto de falha, sem a qual o sistema distribuído não funciona. Assim sendo é conveniente colocar o gestor no mesmo servidor onde se encontra a base de dados. Desta forma obtém-se um único ponto de falha, caso os problemas estejam relacionados com ligações de rede. 6.5.3 Recuperação da ocorrência de falhas Estão previstas a ocorrência de falhas na invocação de métodos em objectos de cache remotos, assim como falhas na execução das caches. Existem três situações distintas onde podem ocorrer falhas: invocação de um método remoto, crash de clientes e antes do registo de uma referência. As próximas secções descrevem os passos efectuados durante a recuperação, para as três situações referidas. 47 6.5.3.1 Falha na invocação de um método remoto Caso a falha seja no acesso a um dos objectos de cache remotos, a cache que invocou o método acede ao gestor e remove a referência para o objecto que falhou. Reinicia-se o pedido e haverá uma cache que obterá permissão para carregar o objecto da base de dados. A Figura 6.6 ilustra a ocorrência deste tipo de falha. Para este exemplo, assume-se que o estado do sistema distribuído é o representado na Figura 6.4b. Figura 6.6: Falha na invocação de um método remoto e respectiva recuperação. A cache X falha, ou seja, a sua execução termina de forma abrupta. A cache Y tenta invocar um método no objecto o1, que se encontrava no espaço de endereçamento da cache X. É lançada uma excepção RemoteException que é apanhada pela cache Y. A referência para o objecto remoto o1 é removida da cache Y. A cache Y invoca o método remoto unregister do gestor, especificando a chave e a referência a remover. O gestor remove do seu espaço de endereçamento a referência para o objecto o1. A invocação do método unregister retorna e a cache Y remove do seu refMap a referência para o objecto remoto r2. A cache Y reinicia o processo de obtenção/registo de referência descrito na Secção 6.2. Quando caches distintas se apercebem da falha do mesmo objecto, ambas tentam fazer a sua remoção do gestor. No entanto a operação de remoção só é efectuada uma vez, mesmo que já tenha sido efectuado o registo de uma nova referência, com a chave do objecto que se pretende remover, ou seja, é uma operação idempotente. Consegue-se esta propriedade com recurso a um método de remoção, em que além da chave do objecto que se pretende remover também se especifica o objecto em si (neste caso a referência para o objecto remoto). Desta forma a remoção só é efectuada se o objecto especificado for igual ao objecto mapeado pela chave. Podia ter-se optado por carregar o objecto da base de dados e efectuar novo registo, após a remoção do objecto que falhou. Não foi essa a solução adoptada, pois podiam existir vários clientes a detectar a falha. Nesse caso existiriam mais acessos à base de dados do que os necessários e seria necessário recorrer na mesma ao gestor, para decidir qual das cópias do 48 objecto seria a utilizada. 6.5.3.2 Crash de clientes Geralmente as caches são clientes de objectos locais e também de objectos remotos. É necessário fazer a monitorização destes clientes. Durante o funcionamento normal do sistema, a monitorização é feita como descrito na Secção 6.3. Contudo, os clientes estão sujeitos a falhas. As falhas têm de ser detectadas, para que o número de clientes possa ser decrementado. Se o decremento de clientes não fosse efectuado, um objecto que tivesse como cliente, um cliente que falhou, nunca seria removido do sistema distribuído, a não ser que a cache onde o objecto está alojado também falhasse. Para fazer a detecção, recorreu-se explicitamente ao mecanismo de contagem de referências do Java RMI, utilizando a interface Unreferenced, tal como descrito na Secção 3.4.5. Foram criados objectos que funcionam como identificadores de clientes. A título de exemplo, considerem-se os objectos r1 e r2 da Figura 6.4b. Existe apenas uma única referência para estes objectos, detida por cada um dos clientes de o1. O objecto é notificado quando deixa de ser referenciado pelo cliente, sendo o número de clientes decrementado, caso não o tenha sido da forma descrita na Secção 6.3.2. O decremento por ocorrência de falhas é um complemento e não um substituto dos mecanismos descritos na Secção 6.3.2. O decremento por falha é mais lento, pois baseia-se nas leases do Java RMI. Não era possível utilizar uma técnica semelhante recorrendo somente às referências para os objectos de cache, pois para além das referências dos clientes, existe também a referência no gestor. A Figura 6.7 ilustra a ocorrência da falha de um cliente. Inicialmente existem dois clientes do objecto o1, um local (cache X) e outro remoto (cache Y). Figura 6.7: Crash de um cliente e respectiva recuperação. 49 A cache Y termina a sua execução de forma abrupta. O objecto r2 deixa de ser referenciado. r2 é notificado pelo sistema RMI que já não é referenciado. r2 utiliza a referência que tem para o1 para invocar o método unregisterClient. O número de clientes de o1 é decrementado e o objecto r2 é removido do sistema distribuído pelo garbage collector. 6.5.3.3 Falha antes do registo de um objecto É possível que uma cache falhe após ter recebido permissão para carregar o objecto pretendido da base de dados, mas antes de o ter conseguido registar. A Figura 6.8 ilustra esta situação, bem como a respectiva recuperação. Figura 6.8: Falha antes do registo de um objecto. A cache X faz o pedido de um objecto ao gestor. O objecto pretendido não está disponível, sendo criado um lock no gestor. A cache X recebe informação que o objecto não está em uso e que obtém permissão para o carregar da base de dados. Antes de efectuar o carregamento do objecto, a cache X falha. Entretanto, a cache Y faz um pedido ao gestor, para o mesmo objecto especificado pela cache X. Devido à existência do lock, o gestor sabe que o objecto está a ser carregado, e por isso bloqueia a thread responsável por processar o pedido da cache Y. Como o lock não foi removido atempadamente numa operação de registo, ocorre timeout e o lock é removido pelo gestor. O tratamento é feito pelo gestor, sem que a cache Y tenha conhecimento da ocorrência da falha. É criado um novo lock e a cache Y recebe permissão para carregar o objecto da base de dados. 50 6.6 Sincronização e performance A sincronização foi elaborada de forma a tentar manter a concorrência ao mais alto nível possível. Este requisito é fundamental pois a cache faz parte de um sistema que corre em máquinas multiprocessador. Neste caso podem não ser apenas processos que se encontram à espera de ser escalonados, mas sim processadores que estão parados, a desperdiçar o seu tempo de processamento. Tendo em conta a performance, foram criados locks de leitura e locks de escrita. Estes locks são utilizados no acesso à estrutura do gestor, onde estão as referências para os objectos remotos. Desta forma apenas as operações de registo e remoção necessitam de fazer lock de toda a estrutura, enquanto que pedidos de obtenção podem ser satisfeitos em simultâneo. 6.7 Integração com a cache local A integração com a cache local foi efectuada da forma descrita nas secções anteriores deste capítulo. Neste secção abordam-se apenas os aspectos mais relevantes, que não foram considerados nas secções anteriores. Inicialmente as novas funcionalidades foram testadas de forma isolada, para verificar o correcto funcionamento da solução. Foi uma fase demorada, pois alguns detalhes passaram despercebidos na análise inicial do código existente, tais como interacções entre outros serviços do servidor aplicacional. Essas interacções tornaram necessária a serialização de alguns objectos. A classe CachedObject foi estendida (DistributedCachedObject), passando a implementar uma interface remota, disponibilizando os métodos que inicialmente eram locais. Todos os acessos directos a atributos (essencialmente bandeiras) passaram a ser efectuados através de métodos, e disponibilizados pela interface remota. Foram efectuadas alterações no método de sincronização do CachedObject com a base de dados. Inicialmente acedia-se a cada um dos parâmetros do CachedObject para obter o seu valor e de seguida procedia-se à actualização na base de dados. Se se mantivesse este comportamento haveria uma invocação remota para cada parâmetro existente no CachedObject. A alteração efectuada consiste em invocar remotamente apenas o método de sincronização. A sincronização propriamente dita passa a ser feita pela cache que detem o objecto. 6.8 Estatísticas Foram introduzidos mecanismos que permitem a monitorização da utilização do sistema distribuído de cache. A monitorização é feita pelo gestor. Existem contadores do número de operações de get, register e unregister, assim como contadores de hit e miss. Já existiam mecanismos idênticos para a utilização da cache centralizada. Manteve-se o que já existia, considerando-se que apenas ocorre um miss no caso em que a cache obtém permissão para carregar o objecto da base de dados, ou seja, caso um pedido seja satisfeito por uma outra cache incrementa-se na mesma o contador de hit. 51 6.9 Testes Durante a execução de testes aos novos componentes introduzidos, tentou-se utilizar uma abordagem estrutural. Neste tipo de abordagem são escolhidos casos de teste que exercitem, idealmente, todo o código [8]. Apesar de se ter tentado seguir esta metodologia, não foi elaborada uma bateria de testes. Houve duas fazes de teste distintas: uma fase de testes aos novos componentes antes da integração e outra após a integração. Na primeira fase de testes recorreu-se ao debugger e a output que permitisse identificar a execução de determinados blocos do programa. O recurso ao debugger foi essencial, pois permitiu através de execução passo-a-passo verificar o correcto funcionamento dos mecanismo de lock, assim como os mecanismos desenvolvidos para a tolerância a falhas. Seguidamente passou-se à integração da solução com o sistema previamente existente e iniciou-se a segunda fase de testes. Nesta fase os testes foram feitos essencialmente com recurso à execução de fluxos de processo. Adicionalmente recorreu-se a output para identificação da execução de determinados blocos, aos logs do servidor aplicacional e tabelas da base de dados. Foram testadas a leitura e escrita de parâmetros de CachedObjects, a operação de atribuição (assign) e a operação de sincronização. Foram efectuados testes simples e em execução concorrente sobre os mesmos dados. Verificou-se que os fluxos terminaram sem erro, que os valores na base de dados estavam de acordo com o esperado numa execução correcta do fluxo, e que acessos concorrentes aos mesmos dados utilizavam o valor da última modificação. 6.10 Escalabilidade da solução A complexidade do sistema está dispersa pelas diversas caches. Estas caches actuam como servidores e clientes de objectos remotos. Admite-se que num ambiente de execução real a probabilidade de um objecto estar a ser partilhado por caches distintas é baixa. No entanto não existem valores, pois o sistema desenvolvido não chegou a ser testado nesta situação. O bootleneck da solução apresentada é o gestor. O gestor tem de ser consultado cada vez que existe um miss na cache local, e no momento da remoção do objecto do sistema distribuído. Apesar de não terem sido efectuados testes de escalabilidade, preve-se que o sistema tenha um bom desempenho para um número pequeno de caches clientes, sendo a performance degradada de forma aproximadamente linear à medida que o número de caches vai aumentando. É espectável que com o aumento de caches clientes, o número de objectos em utilização pelas diversas caches aumente. Este aumento do número de clientes levará a um aumento das comunicações entre as diversas caches, degradando a performance do sistema. 52 Capítulo 7 Melhoramentos na solução inicial O principal problema da solução apresentada no Capítulo 6 é a possibilidade de ocorrência de falhas no gestor. Admite-se que o gestor não pode falhar e sem ele o sistema distribuído é encerrado. Neste capítulo apresenta-se uma solução melhorada, que corrige o problema da falha do gestor, assim como o problema do registo RMI. A possibilidade de ocorrência de uma falha do sistema de gestão de base de dados não está directamente relacionada com o serviço de cache, e pode ser colmatada com recurso a soluções de bases de dados replicadas, disponibilizadas pela própria Oracle. Utiliza-se a tecnologia de comunicação por grupos, para replicar a informação contida no gestor e passa a utilizar-se o registo disponibilizado pelo Jgroup, em vez do registo RMI. Relembrar que o registo RMI introduz um ponto de falha, e que o registo do Jgroup corrige esse problema, tal como descrito na Secção 4.4.2. 7.1 Arquitectura da solução A arquitectura em camadas da solução é a mesma que foi apresentada na Figura 6.1, no entanto foram efectuadas alterações na camada distribuída. A Figura 7.1 representa a nova arquitectura da camada de cache distribuída. A situação ilustrada é para o caso de existirem dois servidores aplicacionais, mas podem existir mais, ou apenas um. Figura 7.1: Arquitectura da camada de cache distribuída melhorada. Cada servidor além de possuir uma cache, possuí também um gestor. Dos vários gestores 53 que existem no sistema, apenas um (coordenador) atende pedidos das diversas caches. Os outros gestores funcionam como backups. Caso ocorra uma falha na máquina que aloja o coordenador, há uma eleição e é nomeado um novo coordenador. 7.2 Eleição Num sistema distribuído, um algoritmo utilizado para seleccionar um nó distinto, ou um líder para coordenar alguma actividade é conhecido como algoritmo de eleição. Geralmente não interessa qual o processo que desempenha esse papel, mas algum tem de o fazer. A primeira ideia que surgiu, para a resolução do problema da falha do gestor, consistia na implementação de um algoritmo de eleição. As diversas caches executariam o algoritmo e aquela que vencesse a eleição criaria uma instância do gestor, que seria utilizada por todas as caches. O algoritmo de eleição permitiria recuperar da falha do gestor. Existem diversos algoritmos de eleição, sendo os mais conhecidos o Bully, proposto por Garcia-Molina [14], e o Ring [12]. Relativamente a desenvolvimentos recentes, o artigo [9] propõe novas abordagens, com recurso a métodos tolerantes a falhas, apresentando soluções mais eficientes do que as inicialmente propostas. Nos algoritmos referidos, cada vez que um novo processo inicia a execução ocorre uma nova eleição. São utilizados identificadores para cada um dos participantes da eleição. Estes identificadores definem prioridades. O participante com maior prioridade ganha a eleição. O facto de poder ser eleito um novo coordenador, mesmo sem o anterior ter falhado, é indesejável para este sistema. As referências já registadas teriam de ser transferidas para o novo coordenador, ou invalidadas e obtidas novamente. Tentou-se alterar o algoritmo Ring para que este comportamento não se verificasse, mas sem sucesso. A tentativa foi feita com o Ring e não com o Bully, pois no primeiro uma eleição ocorre com recurso a 2(n − 1) mensagens (caso a falha não seja detectada concorrentemente por diversos processos), enquanto que o segundo necessita de n − 2 mensagens, no melhor caso, mas no pior caso tem complexidade O(n2 ) (situação em que é o processo com menor prioridade que inicia a eleição). Com o objectivo de encontrar um algoritmo de eleição que não apresentasse esta característica, iniciou-se uma longa pesquisa. Foram analisados dezenas de algoritmos, até encontrar um que satisfaz a propriedade de estabilidade. Segundo [1], um algoritmo de eleição é estável se garantir que uma vez eleito o coordenador, este permanece coordenador enquanto não falhar e as suas ligações estiverem operacionais. O documento [1] apresenta dois algoritmos distintos: um onde pode ocorrer perda de mensagens e outro onde as perdas não podem ocorrer. Visto que se optou pelo RMI para a troca de mensagens, a comunicação é feita com recurso ao protocolo TCP. Uma vez que a camada de transporte já oferece garantias de entrega, optou-se pela implementação do protocolo que admite que a comunicação é fiável. O pseudo-código encontra-se representado na Figura 7.21 . O símbolo ⊥ identifica uma situação de ausência de coordenador, p é o identificador do processo, n é o número de processos que podem executar o algoritmo, r é o número da ronda, s e k são números de novas 1 Figura 54 2 de [1] rondas, δ é um parâmetro que permite especificar o tempo entre o envio de mensagens por parte do coordenador/candidato. Figura 7.2: Algoritmo de eleição. A execução é feita por rondas r = 0, 1, 2, .... Para iniciar a ronda s, um processo envia (START, s) para um processo específico, designado "coordenador da ronda s", correspondendo ao processo s mod n. Coloca a variável que identifica a ronda (r ) com o valor s, despromove o coordenador anterior e reinicia o temporizador. De δ em δ tempo, o processo verifica se é o coordenador da ronda e em caso afirmativo envia (OK, r) para todos os processos (tarefa 0). Quando um processo recebe (OK, k) para a ronda actual (k = r), reinicia o temporizador e, caso não haja coordenador e esta seja a segunda mensagem de (OK, k) que recebe, elege como coordenador o processo k mod n. Se o valor do temporizador for superior a 2δ, ocorreu um timeout por parte do coordenador/candidato actual. Envia uma mensagens de (STOP, r) para o coordenador/candidato actual e inicia a ronda seguinte. Este timeout é o mecanismo que permite identificar a ocorrência de uma falha no coordenador/candidato. Se um processo receber uma mensagem de (STOP, k), com um valor da ronda igual ou superior ao actual, abdica da candidatura/coordenação e inicia a ronda k + 1. Ao receber uma mensagem (OK, k) ou (START, k), com um valor da ronda superior ao actual, inicia a ronda k. É importante uma escolha adequada do parâmetro δ. Um valor muito pequeno de δ pode levar à tomada de decisões prematuras, e faz com que o número de mensagens enviadas pelo coordenador seja grande. Um valor grande para δ 55 leva a um tempo da eleição grande, o que pode fazer com que algumas tarefas do servidor se encontrem suspensas por um longo período de tempo. É necessário ter em consideração este compromisso. Admitindo que raramente ocorre uma falha de um servidor, o valor de δ pode ser elevado. Foram efectuados testes para um valor de 5 segundos, com resultados satisfatórios. Para valores próximos de 2 segundos verificou-se que em situações de elevada carga do servidor eram tomadas decisões precipitadas. Relativamente à implementação, como referido a comunicação é feita por RMI, via Jgroup, ou seja, utiliza-se o Jgroup sem recurso à comunicação por grupos. Não foram criados grupos de comunicação, pois na execução do algoritmo, parte das mensagens tem de ser entregue apenas a um dos processos. Ao optar pela comunicação por grupos seria necessário recorrer ainda a comunicação unicast. Além disso, as garantias que o sincronismo virtual oferece (entrega a todos os processos do grupo, ou a nenhum) não são um requisito para o algoritmo de eleição, e assim não se sobrecarrega o sistema com funcionalidades desnecessárias. 7.3 Replicação A existência de um algoritmo de eleição, permite ao sistema recuperar das falhas do gestor, tal como descrito na Secção 7.2. Contudo, caso ocorresse uma falha no gestor seria necessário voltar a registar todos os objectos em uso pelas caches, ou descartar as referências actuais e re-obter as referências. Este podia ser um processo demorado, que teria de interromper momentaneamente a execução de todos os servidores, para garantir a consistência do sistema. A solução adoptada foi a replicação da informação do gestor. Cada cache possui uma instância do gestor. Existe apenas um gestor que é utilizado (coordenador), funcionando os outros como backups. Quando o coordenador falha, existe uma eleição e o novo coordenador fica imediatamente disponível para desempenhar as suas funções. Desta forma, não é necessário invalidar as referências para objectos em utilização pelas caches, nem registar as referências já existentes, pois o novo coordenador já tem uma visão consistente do sistema distribuído. A única operação efectuada no gestor que não implica a invocação de métodos no grupo, do qual os backups fazem parte, é a operação de get, para o caso em que já existe uma referência remota no gestor. Em todas as outras operações a modificação (inserção/remoção) de referências foi isolada, sendo executada de forma síncrona, em todos os elementos do grupo. Poderia pensar-se que seria possível a cada uma das caches efectuar a operação sobre o gestor alojado no seu espaço de endereçamento, sendo a operação propagada pelas diversas caches, com recurso à comunicação por grupos. Contudo, aproveitando o que estava feito da solução inicial, isso não é possível. O seguinte exemplo ilustra essa impossibilidade. Duas caches estão a tentar obter uma referência para o mesmo objecto. O objecto pretendido não se encontra em cache. Em ambos os gestores é adquirido o lock de escrita. Ambos os gestores instalam um objecto de lock no grupo, perdendo-se o primeiro que foi instalado. 56 Ambas as caches adquirem o objecto da base de dados, e ambas fazem o registo, que é propagado pelo grupo de servidores. A partir deste momento o sistema encontra-se num estado inconsistente, pois as caches possuem objectos distintos, enquanto uma deveria possuir o objecto e outra uma referência. Neste caso, seria necessário implementar um mecanismo de lock distribuído. Uma vez que já existia um algoritmo de eleição implementado, optou-se por contornar esses problemas utilizando o coordenador. 7.4 Tolerância a falhas Visto que apenas houve alterações na interacção com o gestor, apenas esses aspectos serão referidos nesta secção, além da introdução do algoritmo de eleição. Os restantes cenários de falha já foram abordados na Secção 6.5. 7.4.1 Falha do coordenador após a invocação de um método por parte de uma cache cliente O coordenador pode falhar após a realização da operação no grupo e antes de efectuar o retorno para o cliente. O cliente não tem forma de saber se a operação foi realmente efectuada ou não. Após a falha do coordenador há uma nova eleição. É necessário garantir que a operação é efectuada, para que o sistema distribuído se mantenha num estado consistente. Uma operação de get pode ser efectuada mais do que uma vez, desde que não sejam guardadas as referências que identificam a cache como cliente do objecto. Visto que a operação apenas é repetida caso haja uma falha do gestor, antes do retorno para o cliente, o cliente não tem forma de guardar a referência de cliente, logo este problema não se põe. Caso a coordenador falhe e a operação tenha sido efectuada, o que sucede é que vão ocorrer dois ou mais registos de novos clientes do objecto (dependendo do número de vezes consecutivas que o coordenador falhou). Momentaneamente o número de clientes do objecto estará incorrecto, mas será reposto após a expiração das leases do RMI. Caso o objecto ainda não estivesse em utilização, a repetição da operação de get levaria a um bloqueio no lock, significando que o objecto estaria a ser carregado. Visto que a cache não chegou a obter permissão para carregar o objecto e está a repetir a operação, ocorrerá um timeout e reinicia-se o processo de obtenção de referência. A operação de register também pode ser efectuada várias vezes. O problema que poderia existir com um novo registo de cliente é exactamente o mesmo que para a operação get, logo não há problema. Caso o objecto tenha sido registado anteriormente, o registo não volta a ser efectuado e a cache que invocou o método recebe uma referência para o objecto previamente registado. Por fim, a operação unregister também pode ser efectuada várias vezes, pois a referência só é removida do gestor, caso a chave e a referência especificada coincidam com as armazenadas no gestor. Resumindo, qualquer uma das operações sobre o gestor pode ser efectuada mais do que uma vez, desde que não se guardem as referências relativas ao registo de clientes. Quando uma cache se apercebe que ocorreu uma falha no coordenador, todas as operações que necessitem da sua intervenção são suspensas. As operações são retomadas após a notificação do fim da eleição. 57 7.4.2 Falhas relacionadas com o algoritmo de eleição Um dos aspectos mais importantes é a manutenção da consistência do sistema distribuído. É necessário garantir que os processos que participam na eleição estão em condições de ser eleitos. Um processo antes de iniciar o algoritmo de eleição tem de tentar sincronizar o seu estado com o coordenador, caso este exista. Esta verificação aparentemente simples, revelou-se bastante complexa devido à possibilidade de ocorrência de algumas situações. Antes de mais, relembrar que um processo apenas é eleito coordenador após o envio de duas mensagens de OK, sendo ele o coordenador da ronda. A título de exemplo considere-se a seguinte situação. Dois processos (com identificadores 1 e 2) estão a executar o algoritmo de eleição, não existindo ainda coordenador. O coordenador da ronda é o processo 2, tendo já enviado a primeira mensagem de OK para todos os processos. Entretanto, um novo processo (com identificador 0) inicia a sua execução, verificando se existe coordenador. Como não existe, começa a executar o algoritmo de eleição. O processo 2 envia para todos a segunda mensagem de OK, tornando-se no coordenador para os processos 1 e 2. No entanto ainda não é visto como coordenador pelo processo 0, pois este recebeu apenas uma mensagem de OK. O processo 1 sincroniza-se com o coordenador e passa a receber todas as actualizações. São efectuadas operações sobre o coordenador, recebendo o processo 1 as respectivas actualizações. O coordenador falha antes de enviar uma nova mensagem de OK, ou seja, o processo 0 nunca chega a ver o processo 2 como coordenador. O processo 1 detecta a falha do coordenador e o processo 0 detecta a falha do coordenador da ronda. Inicia-se uma nova ronda, sendo o processo 0 o coordenador da ronda. O processo 0 ganha a eleição, mesmo existindo uma réplica do coordenador que falhou. O sistema distribuído fica num estado inconsistente. Para resolver o problema recorreu-se ao Jgroup com uma interface externa de comunicação em anycast. Um processo após a sincronização com o gestor e antes de se juntar ao grupo de actualizações, coloca uma referência no registo. Visto que todos os processos executam esta operação, no registo fica uma referência para um grupo. Um processo que se encontra na situação descrita no parágrafo anterior, antes de enviar a segunda mensagem de OK invoca um método nesta nova interface, que terminará com sucesso caso exista uma réplica em condições de ser eleita. A invocação do método é uma espécie de operação de ping. Caso termine com sucesso o coordenador da ronda suprime o envio da segunda mensagem de OK, abdicando da sua oportunidade de se tornar coordenador, e mantendo a consistência do sistema distribuído. 7.4.3 Falha durante a sincronização com o coordenador Para minimizar os efeitos da ocorrência de falhas e garantir que a junção ao grupo é efectuada no momento correcto, a sincronização com o coordenador é feita através de um método de callback. Um gestor que se queira sincronizar com o coordenador invoca o método syncWithCoordinator do coordenador, especificando a sua própria referência remota. O coordenador faz um lock da estrutura de referências em modo de leitura, e cria um array com todas as referências e as respectivas chaves. Recorrendo à referência previamente especificada, o coordenador invoca o método de callback syncReplica, passando por valor o array 58 das chaves e as estatísticas actuais da utilização da cache. O gestor que pretende tornar-se uma réplica guarda os valores que lhe foram passados pelo coordenador, coloca no registo a sua referência do "‘grupo de ping"’ e junta-se ao grupo de actualizações. O coordenador liberta o lock de leitura e a invocação do método syncWithCoordinator retorna. Assim garante-se que o coordenador apenas altera o seu estado depois da réplica estar sincronizada e pronta para receber novas actualizações. Se ocorrer uma falha na invocação do método syncWithCoordinator, a réplica (ou aspirante a réplica) tem informação local suficiente para saber se a sincronização já foi feita, ou se é necessário invocar novamente o método quando for eleito um novo coordenador. Pode parecer que existe um caso de falha que pode colocar o sistema distribuído num estado inconsistente, que está a ser deixado de fora. O que é que acontece se uma nova cache inicia a sua execução e o coordenador falha antes de ocorrer sincronização? Como já foi referido na Secção 7.4.2, se existirem réplicas uma delas será eleita coordenadora, mas o que acontece se não existirem réplicas ou todas elas falharem? A resposta é simples. Uma cache só pode obter/registar referências após conhecer o coordenador. Isto implica que se o coordenador falhou e nenhuma das réplicas tomou o seu lugar é porque todas as caches que poderiam deter referências falharam. Assim sendo, não existem referências para invalidar e o sistema encontra-se num estado consistente. 7.5 Testes Relativamente a testes foram efectuados os referidos na Secção 6.9, tendo novamente dividido os testes em duas fases distintas. 59 Capítulo 8 Conclusões e trabalhos futuros 8.1 Conclusões Esta dissertação explana os passos e as decisões tomadas durante o desenvolvimento e implementação de uma solução distribuída de cache de dados com estrutura hierárquica. Começa-se por introduzir os conceitos básicos para a compreensão do funcionamento de um sistema distribuído, bem como os problemas associados a este tipo de sistemas. Referem-se alguns artigos relacionados com caches distribuídas. Apresentam-se vários produtos que oferecem funcionalidades de cache distribuída. A maioria dos produtos apresentados são de código aberto, e estão sujeitos a licenças que permitem a sua integração em produtos comerciais. Explicitam-se as suas principais características, com especial ênfase nas vantagens e inconvenientes de cada um. Verifica-se que a documentação relativa ao funcionamento e detalhes de implementação dos diversos produtos encontrados é insuficiente para perceber de forma exacta o seu funcionamento. Além disso, os produtos geralmente estão acompanhados de funcionalidades adicionais, desnecessárias para este caso específico. Assim, optou-se por desenvolver uma solução à medida, que interferisse apenas nos pontos estritamente necessários com a solução centralizada existente. Descrevem-se as funcionalidades e a arquitectura do serviço de cache centralizado, que serviu como ponto de partida para o desenvolvimento deste trabalho. A fase inicial de desenvolvimento centrou-se no criação de uma solução simples e de fácil implementação. No entanto, a solução não surgiu de uma forma tão simples quanto o desejado, pois é necessário garantir que o sistema distribuído se encontra num estado consistente, e que continua a sua execução mesmo quando alguns dos elementos falham. Esta solução permite a resolução do problema, assumindo a existência de um processo que não falha (gestor). O servidor que aloja o gestor tem de ser o primeiro a iniciar, e caso um dos outros servidores detecte a sua falha termina a execução. Além disso, o registo RMI introduzia um ponto de falha adicional, mas que podia ser minimizado, caso a sua instância fosse iniciada pelo servidor que se admitia que não podia falhar. Visto que a solução inicial assumia que o gestor não podia falhar, foi necessário efectuar alguns melhoramentos para puder remover esta limitação. Para resolver o problema do ponto 61 de falha implementou-se um algoritmo de eleição. O algoritmo de eleição permite que no caso de ocorrência de uma falha no gestor haja eleição de um novo gestor. Na grande maioria dos algoritmos de eleição existentes, ocorre uma eleição cada vez que um novo processo inicia o algoritmo. Este comportamento é indesejável, pois poderia ser nomeado um novo gestor e seria necessário invalidar ou voltar a registar todas as referências para objectos já existentes. Após uma intensa pesquisa encontrou-se um algoritmo que satisfaz a propriedade de estabilidade, segundo a qual apenas ocorre uma nova eleição caso o actual gestor falhe. Concluiu-se que apesar da existência de um algoritmo de eleição estável, caso ocorresse uma falha do gestor seria necessário invalidar ou voltar a registar as referências para objectos já existentes, pois não existiam cópias de segurança. Optou-se por contornar o problema, pois caso o número de objectos existentes no sistema distribuído fosse elevado a recuperação seria lenta. Recorreu-se à biblioteca de comunicação por grupos Jgroup. Esta biblioteca foi utilizada para replicar a informação do gestor. Continua a ser utilizado apenas um gestor (coordenador), enquanto que as réplicas têm função apenas de backup. Caso o coordenador falhe, qualquer uma das outras réplicas está em condições de assumir a coordenação. Esta solução permite que o sistema esteja disponível desde que um qualquer servidor esteja em execução. Poderá assim dizer-se que os objectivos propostos foram cumpridos. 8.2 Trabalhos futuros A solução apresentada não é óptima, existindo certamente alguns aspectos que ainda podem ser melhorados. Um desses aspectos prende-se com a criação de cópias locais. Estima-se que mais de 80% das operações sobre CachedObjects seja de leitura. Uma das formas de aumentar o desempenho do sistema seria utilizar uma cópia do objecto, em vez de uma referência remota. Todos os pedidos de leitura seriam resolvidos localmente. Os pedidos de escrita seriam redireccionados para a cache que obteve permissão para carregar o objecto da base de dados, e as alterações seriam propagadas para todas as cópias. Pode ser aproveitado o mecanismo de registo de clientes para passar uma referência de callback, permitindo uma posterior propagação das actualizações. Não existe a necessidade de recorrer à comunicação por grupos para a propagação das actualizações. Do ponto de vista da consistência é suficiente que alterações sobre um mesmo objecto sejam vistas pela mesma ordem. Elimina-se assim a necessidade da passagem de todos os pedidos por um gestor centralizado, para fazer a serialização dos pedidos. Quer a utilização de comunicação por grupos quer a serialização de todos os pedidos iriam comprometer a escalabilidade do sistema. Este tipo de solução levaria a uma implementação do protocolo primary-backup que fornece uma implementação de consistência sequencial, visto que o primário (cache que carregou o objecto da base de dados) pode ordenar todo os pedidos de escrita. Evidentemente que todos os processos vêem todos as operações de escrita pela mesma ordem, independentemente da cópia que utilizam para as operações de leitura. De notar ainda que utilizando um protocolo bloqueante, os processos vão sempre ver os efeitos da sua escrita mais recente. 62 Apêndice Bibliografia [1] M. K. Aguilera, C. Delporte-Gallet, H. Fauconnier, and S. Toueg. Stable leader election. In DISC ’01: Proceedings of the 15th International Conference on Distributed Computing, pages 108–122, London, UK, 2001. Springer-Verlag. [2] K. Arnold and J. Gosling. The Java Programming Language. Addison-Wesley, Third edition, 2000. [3] O. Babaoglu, B. E. Helvik, H. Meling, A. Montresor, and H. Kolltveit. Página web do The Jgroup Project - http://jgroup.sourceforge.net. [4] K. Birman and T. Joseph. Exploiting virtual synchrony in distributed systems. In SOSP ’87: Proceedings of the eleventh ACM Symposium on Operating systems principles, pages 123–138, New York, NY, USA, 1987. ACM Press. [5] A. Birrell, D. Evers, G. Nelson, S. Owicki, and E. Wobber. Distributed garbage collection for network objects. Technical Report 116, Digital, Systems Research Center, 130 Lytton Avenue, Palo Alto, CA 94301, 1993. [6] Y. Cardenas, J.-M. Pierson, and L. Brunie. Uniform Distributed Cache Service for Grid Computing. In IEEE, editor, In 16th DEXA: In 2th International Workshop on Grid and Peer-to-Peer Computing Impacts on Large Scale Hereogeneous Distributed Database Systems., pages 351–355. IEEE Computer Society, Aug. 2005. [7] W. Consulting. Desenho da Solução Activis - WDS03NOV01900, 2004. [8] R. G. Crespo. Transparências da disciplina de software de telecomunicações - introdução aos testes, 2007. [9] M. EffatParvar, M. Effatparvar, A. Bemana, and M. Dehghan. Determining a central controlling processor with fault tolerant method in distributed system. In ITNG ’07: Proceedings of the International Conference on Information Technology, pages 658–663, Washington, DC, USA, 2007. IEEE Computer Society. [10] S. Floyd, V. Jacobson, C.-G. Liu, S. McCanne, and L. Zhang. A reliable multicast framework for light-weight sessions and application level framing. IEEE/ACM Transactions on Networking, 5(6):784–803, 1997. [11] T. A. S. Foundation. The Apache Software License, Version 2.0 - http://www.apache.org/licenses/LICENSE-2.0, 2004. [12] G. N. Frederickson and N. A. Lynch. Electing a leader in a synchronous ring. J. ACM, 34(1):98–115, 1987. 65 [13] I. Free Software Foundation. GNU Lesser General Public License - http://www.gnu.org/licenses/lgpl-2.1.txt, 1999. [14] H. Garcia-Molina. Elections in a distributed computing system. IEEE Trans. Comput., C-31(1):48–59, Jan. 1982. [15] H. W. Holbrook, S. K. Singhal, and D. R. Cheriton. Log-based receiver-reliable multicast for distributed interactive simulation. In SIGCOMM, pages 328–341, 1995. [16] java.net Expert Group. JSR 107 - Java Caching API Home - https://jsr107.dev.java.net/. [17] JBoss. JBoss Cache - http://labs.jboss.com/jbosscache/. [18] F. Karlstrom and P. Fajeau. FKache Open Source - http://jcache.sourceforge.net. [19] J. C. Lin and S. Paul. RMTP: A reliable multicast transport protocol. In INFOCOM, pages 1414–1424, San Francisco, CA, Mar. 1996. [20] J. Markoff and S. Hansell. Hiding in plain sight, google seeks more power. New York Times, June 2006. [21] A. Montresor. Jgroup tutorial and programmer’s manual, 2000. [22] A. Montresor. System Support for Programming Object-Oriented Dependable Applications in Partitionable Systems. PhD thesis, University of Bologna, Italy, Mar. 2000. Technical Report UBLCS-2000-10. [23] K. Obraczka. Multicast transport protocols: a survey and taxonomy, 1998. [24] Opensymphony. OSCache - http://www.opensymphony.com/oscache/. [25] O. S. I. OSI. The BSD License - http://www.opensource.org/licenses/bsd-license.php. [26] P. Prabhakar. Implementation and comparison of distributed caching schemes. In ICON ’00: Proceedings of the 8th IEEE International Conference on Networks, page 491, Washington, DC, USA, 2000. IEEE Computer Society. [27] S. C. Rhea. The Bamboo Distributed Hash Table - A Robust, Open-Source DHT http://bamboo-dht.org/. [28] L. Rizzo and L. Vicisano. Replacement policies for a proxy cache. IEEE/ACM Trans. Netw., 8(2):158–170, 2000. [29] P. Rodriguez, C. Spanner, and E. Biersack. Analysis of web caching architectures: Hierarchical and distributed caching, 2001. [30] G. Seshadri. Tutorials & code camps - jguru: Remote method invocation (rmi). web, 2000. [31] ShiftOne. ShiftOne Java Object Cache - http://jocache.sourceforge.net/. [32] A. Silberschatz, H. F. Korth, and S. Sudarshan. Database System Concepts. McGrawHill, Inc., New York, NY, USA, Fifth edition, 2005. [33] T. D. G. Solutions. overview.jsp. 66 Tangosol Coherence - http://www.tangosol.com/coherence- [34] M. Stumm and S. Zhou. Algorithms implementing distributed shared memory. Computer, 23(5):54–64, 1990. [35] I. Sun Microsystems. Java Remote Method Invocation Specification - Java 2 SDK, Standard Edition, v1.5.0, 2004. [36] A. S. Tanenbaum. Modern Operating Systems. Prentice Hall PTR, Upper Saddle River, NJ, USA, Second edition, 2001. [37] A. S. Tanenbaum and M. van Steen. Distributed Systems, Principles and Paradigms. Prentice-Hall, 2002. [38] T. Tay and Y. Zhang. Peer-distributed web caching with incremental update scheme. IEE proceedings. Communications, 152(3):327–334, 2005. [39] E. P. Team. EHCache Home - http://ehcache.sourceforge.net/. [40] O. Theel and M. Pizka. Distributed caching and replication. In HICSS ’99: Proceedings of the Thirty-second Annual Hawaii International Conference on System Sciences-Volume 8, Washington, DC, USA, 1999. IEEE Computer Society. [41] B. Wang. Performance we should expect from PojoCache - http://wiki.jboss.org/wiki/Wiki.jsp?page=WhatShouldWeExpectOfThePojoCachePerformance, 2006. [42] J. Watkinson. SwarmCache - Cluster-aware Caching for Java - http://swarmcache.sourceforge.net/. [43] Wikipedia. Aspect-Oriented Programming - http://en.wikipedia.org/wiki/Aspect- oriented_programming. [44] Wikipedia. Cache - http://en.wikipedia.org/wiki/Cache. [45] X. Xu, A. Myers, H. Zhang, and R. Yavatkar. Resilient multicast support for continuousmedia applications, 1997. [46] R. Yavatkar, J. Griffoen, and M. Sudan. A reliable dissemination protocol for interactive collaborative applications. In ACM Multimedia, pages 333–344, 1995. [47] J. Zola. Cali, efficient library for cache implementation. In ENC ’04: Proceedings of the Fifth Mexican International Conference in Computer Science (ENC’04), pages 415–420, Washington, DC, USA, 2004. IEEE Computer Society. 67 Apêndice A Interacções entre o serviço de cache e os restantes serviços do servidor aplicacional A Figura A.1 ilustra a arquitectura do serviço de cache inicial, tendo em consideração as interacções entre os restantes serviços. 69 Figura A.1: Arquitectura do serviço de cache incluindo interacções com restantes serviços. 70 Apêndice B Diagramas UML da solução melhorada As Figuras B.1 e B.2 apresentam os diagramas UML de classes, da camada distribuída da solução melhorada. 71 Figura B.1: Diagrama de classes - Parte 1/2. 72 Figura B.2: Diagrama de classes - Parte 2/2. 73