sábado, 27 de junho de 2009

Tutorial Voldemort

Segue um tutorial que desenvolvi a respeito de meus estudos com o Voldemort (http://project-voldemort.com), um sistema Java de banco de dados em arquivos, distribuído e com alta disponibilidade.


Armazenamento Chave-Valor



Para fornecer alta performance e disponibilidade, o Voldemort utiliza somente acessos à simples dados de pares de Chave-Valor, com métodos exemplificados a seguir:

value = store.get(key)
store.put(key, value)
store.delete(key)


Desvantagens


-Sem filtros complexos de Queries;
-Todos os Joins devem ser feitos no código;
-Sem chaves estrangeiras;
-Sem Triggers ou Views.


Vantagens


-Somente são possível buscas eficientes, com uma performance previsível;
-Fácil distribuição em um Cluster;
-A orientação à serviços geralmente desabilita chaves estrangeiras e força os Joins para serem feitos via código (porque as chaves referem-se a dados armazenados em outro serviço);
-Usando um banco de dados relacional é necessária uma camada de cache para aumentar a performance das leituras. A camada de cache tipicamente força a um mapeamento chave-valor de qualquer forma;
-Geralmente acaba com o XML ou outros Blobs denormalizados para melhorar a performance;
-Clara separação de armazenamento e lógica (SQL encoraja a mistura de lógica de negócios com operações de armazenamento para melhor eficiência);
-Sem troca Objeto-Relacional.


Arquitetura


Existem várias camadas de código no Voldemort que implementam interfaces simples para o Get, Put e Delete utilizados. Cada uma destas camadas é reponsável por executar uma das seguintes operações:

-Comunicação TCP/IP;
-Serialização;
-Reconciliação de versão;
-Roteamento entre nós;
-Etc.


Partições de Dados e Replicação


Dados precisam ser particionados em um cluster de servidores para que nenhum servidor precise manter todo o conjunto de dados por completo. Mesmo que os dados possam ser armazenados em um único disco, acessos ao disco para pequenos valores é vencido por tempo de busca, portanto o particionamento tem o efeito de melhoria da eficiência do chache dividindo o conjunto de dados completo em pedaços menores que podem estar completamente na memória do servidor que armazena tal partição. Isto significa que os servidores do cluster não são substituíveis e as requisições necessitem ser roteadas para o servidor que mantém o dado requerido, e não somente qualquer servidor disponível de forma aleatória.
Além do mais, os servidores falham regularmente, tornam-se sobrecarregados ou são desligados para manutenção. Sendo assim, não podemos armazenar nossos dados somente em um servidor, ou a probabilidade de perda de dados será muito grande.
A maneira mais simples de contornar isto poderia ser separar os dados em partições (uma por servidor) e armazenar cópias de uma dada chave em um determinado número de servidores. Estes sistemas têem uma boa característica de permitir a qualquer um o cálculo da localização de um valor somente conhecendo-se sua chave, o que nos permite fazer buscas de maneira ponto-a-ponto e sem conectar à um servidor centralizado que mapeia todas as chaves dos servidores. O lado ruim desta técnica ocorre quando um servidor é adicionado ou removido do cluster.



Hashing Consistente



Hashing Consistente é uma técnica que permite evitar esses problemas e o Voldemort usa esta técnica para calcular a localização de cada chave nas máquinas do cluster. Desta forma, ele tem a característica de que, se um servidor falhar, a carga será igualmente distribuída pelos servidores restantes do cluster.


Formatos de Dados e Queries


Em um banco de dados relacional os dados são quebrados em tabelas 2D. O equivalente no Voldemot são os “Stores”. Cada chave é única em um Store e cada chave pode ter no máximo 1 valor.
O Voldemort suporta semânticas de Hashtable, sendo que um simples valor pode ser modificado e retornado por sua chave primária. Isto facilita a distribuição entre máquinas pois tudo pode ser separado pela chave primária.
Note entretanto que ele não suporta relacionamento um-para-muitos, somente suporta Listas como valores (o correspondente a um java.util.Map onde o valor é um java.util.List), oferencendo o mesmo resultado final. A simplicidade destas Queries pode ser uma vantagem, sendo que cada uma tem uma performace previsível, é fácil dividir a performance do serviço pelo número de operações de armazenamento executadas e rapidamente pode-se estimar sua carga.



Modelo de Dados e Serialização



A reialização no Voldemort é plugável, desta forma pode-se utilizar um serializador de terceiros existente ou desenvolver um próprio. Num formato de nivel mais baixo, o Voldemort utiliza somente vetores de bytes para ambas chaves e valores. Formatos de dados de mais alto nível são opções de configuração para cada Store (são suportados quaisquer formatos que implementem a classe Serializer, que trata da tradução entre bytes e objetos). Os seguintes tipos de dados são suportados de forma padrão pelo Voldemort, tanto para as chaves quanto para os valores, configurando-os adequadamente no Store:

-JSON;
-String
-Java-Serialization (serialização Java padrão);
-Protobuf (serialização do Goolge);
-Identity (vetor de bytes).


Consistência e Versionamento


Executando-se múltiplas e simultaneas escritas distribuídas sobre multiplos servidores, a consistência dos dados se torna um difícil problema. A solução tradicional para este problema são as Transações Distribuídas, mas que são muito lentas e frágeis pois necessitam de que todos os servidores estejam disponíveis durante o processo da transação. Uma alternativa é tolerar a possibiliade da inconsistência dos dados e tratá-las durante o momento de sua leitura. Esta é a técnica utilizada pelo Voldemort.
As aplicações geralmente executam uma sequência de ações de leitura-modificação-atualização ao se modificar dados. O valor de uma chave esta consistente se, na falta de atualizações, todas as leituras da chave retornam o mesmo valor. Num mundo de somente-leitura os dados são criados de uma forma consistente e não são alterados. Quando adicionamos escritas e replicação, encontramos diversos problemas: precisamos alterar múltiplos valores em várias máquinas e deixar as coisas em um estado consistente. Com as falhas dos servidores, isto se torna muito difícil; com a presença de partições em rede isto se torna quase impossível. Desta forma, o Voldemort utiliza Versionamento e Reparação de Leitura, que oferece a melhor garantia de disponibilidade e a maior eficiência possível.


Versionamento em Um Sistema Distribuído


Uma solução para um sistema simples de versionamento pode ser a utilização de somente uma trava otimista (armazenamos um único valor contador ou “relógio” para cada dado e somente permitimos alterações quando apresentam os valores corretos de relógio). Isto funciona bem em um banco de dados centralizado, mas peca em um sistema distribuído onde os servidores aparecem e desaparecem e a replicação pode levar um certo tempo. Para isto, um simples valor não contém informação suficiente de escrita para permitir-nos jogar fora versões antigas.
Uma resposta para isso é o chamado Vetor de Relógios de Versionamento. Um vetor de relógio de versionamento mantém um contador para cada escrita no servidor, nos permitindo calcular quando duas versões estão em comflito e quando uma versão precede ou sucede outra. Um vetor de relógio de versionamento é uma lista de pares de servidor:versao, como no exemplo a seguir:

[1:45, 2:3, 5:55]

A versão indica qual servidor é o “mestre” para determinado número de escritas. Um conflito de versões pode ser visto a seguir:

[1:2, 2:1]
[1:1, 2:2]


Camada de Persistência


O Voldemort suporta uma API simples de persistência usando o BDB Java Edition como padrão. O MySQL e o amarzenamento em memória também são suportados. Para adicionar uma nova implementação de persistência você precisa implementar os métodos de Put, Get, Delete e prover um Iterator sobre os valores do Store.



Configuração



Existem 3 arquivos de configuração que controlam as operações dos servidores do cluster do Voldemort:

-Cluster.xml: Guarda as informações de todos os servidores (nós) do cluster. É o mesmo arquivo utilizado em todos os nós do cluster.

-Stores.xml: Guarda as informações de todos os Stores (tabelas) utilizados no cluster. Contém informações sobre o número requerido de leituras e escritas com sucesso para manter a sua consistência, além de como as chaves e valores serão serializados em bytes. É o mesmo arquivo utilizado em todos os nós do cluster.

-Server.properties: Guarda as informações sobre os parâmetros de controle de um nó particular. Inclui informações do ID do nó para sua identificação dentro do cluster, tamanho do Pool de Threads, configurações do mecanismo de persistência (como BDB ou MySQL), etc. Este arquivo é diferente para cada nó do cluster.

Finalmente, existe a variável de ambiente VOLDEMORT_HOME, que controla o diretório onde estão os arquivos de configuração do Voldemort.
Para maiores detalhes sobre a configuração e outros parâmetros do Voldemort, consulte sua documentação: http://project-voldemort.com/configuration.php

Programação

Servidor

Para subir um servidor do cluster Voldemort, pode-se executar o seguinte código especificando-se a pasta onde se encontrão os arquivos de configuração do respectivo nó servidor:

VoldemortConfig config = VoldemortConfig.loadFromVoldemortHome("/home/Voldemort_Teste/Server1");

VoldemortServer server = new VoldemortServer(config);

server.start();


O endereço indicado refere-se ao diretório que contém as pastas Config e Data utilizados pelo Voldemort. Na pasta config encontram-se os arquivos de copnfigurações Cluster.xml, Stores.xml e Server.properties. A pasta Data refere-se para a pasta onde o Voldemort salvará os dados.

Cliente

Um cliente do cluster Voldemort pode ser criado a partir do seguinte código:

int numThreads = 10;

int maxQueuedRequests = 10;

int maxConnectionsPerNode = 10;

int maxTotalConnections = 100;

String bootstrapUrl = "tcp://localhost:6666";

StoreClientFactory factory = new SocketStoreClientFactory(numThreads, numThreads, maxQueuedRequests, maxConnectionsPerNode, maxTotalConnections, bootstrapUrl);


Para salvar dados no cluster, é necessário criar um Store definindo-se seu nome e os tipos de dados utilizados como Chave e Valor (de acordo com os tipos definidos no arquivo de configuração Stores.xml utilizado pelo cluster). Pode-se ver no seguinte código a criação do Store que se chamará Test e possuirá chaves do tipo Integer e terá valores do tipo TransacaoOnline:

StoreClient < Integer, TransacaoOnline > client = factory.getStoreClient("Test");


Para salvar um par de chave-valor, utiliza-se o Store criado, como no seguinte código:

client.put(Chave, Objeto_TransacaoOnline);

Para ler um par de chave-valor, utiliza-se também o Store criado, como no seguinte código, onde o objeto do tipo TransacaoOnline buscado pela chave Chave será armazenado em Novo_Objeto_TransacaoOnline:

Novo_Objeto_TransacaoOnline = client.getValue(Chave);

Nenhum comentário:

Postar um comentário