ZODB - Zope Object Database
Aviso: Esta é a visão de impressão com todas as páginas do tutorial em uma página. A versão paginada está disponível aqui, se você preferir esta.
Introdução
O ZODB (Zope Object Database) é um banco de dados orientado a objetos que é utilizado principalmente pelo Zope[1], um servidor de aplicações escrito na linguagem Python[2]. As últimas versões do ZODB foram projetadas para permitir que o banco de dados seja utilizado sem o Zope, em aplicações standalone, mas isso ainda não é muito comum. Como o principal "usuário" do ZODB é o Zope, o projeto do banco de dados ZODB possui algumas caracteristicas bastante peculiares, resultado da influência da plataforma Zope e da comunidade de desenvolvedores Python no desenvolvimento do SGBD.
O ZODB tem como objetivo principal a transparência para o desenvolvedor Python. Nesse sentido, o ZODB é um mecanismo de persistência para objetos Python, onde não existe a necessidade de expressar de forma explicita que um objeto deve ser armazenado ou pesquisado no banco de dados (como é feito com o SQL em bancos de dados relacionais). O desenvolvedor tem que apenas "fingir" que a aplicação nunca deixará de funcionar e que todos os objetos estão disponíveis em memória, apesar de estarem persistidos no SGBD.
Toda estrutura que siga um modelo descrito por um pickle[3] (representação persistente de um tipo da linguagem Python), pode ser persistido nativamente no ZODB. Na prática isso significa que objetos que estejam instanciados em memória podem ser armazenados no banco de dados mantendo essa mesma estrutura.
O ZODB persiste os objetos armazenando uma representação simples, que é formada pela classe do objeto e uma estrutura de dicionário contendo nas chaves as propriedades do objeto e nos valores associados as chaves os valores que cada propriedade possui. Essa é uma representação muito eficiente e que na prática permite, através da implementação de uma interface definida no ZODB, que o meio de armazenamento tenha implementações alternativas, voltadas a objetivos específicos (como velocidade, tolerância a falhas, escalabilidade ou mesmo compatibilidade com modelos relacionais).
O ZODB não impõe que estruturas de tabelas e indices sejam usadas para armazenar os dados (o que parece um pesadelo para os desenvolvedores habituados apenas aos bancos de dados relacionais). Ao invés disso, o desenvolvedor pode utilizar qualquer estrutura de dados nativa da linguagem para armazenar e pesquisar seus objetos no SGBD como: listas, dicionários ou árvores B.
Controle de Concorrência
O ZODB implementa transações, sub-transações em 2 níveis, versões e controle de concorrência através de marcadores de tempo para evitar a ocorrência de deadlocks. O ZODB não utiliza bloqueios.
O ZODB permite que multiplos threads em uma aplicação acessem os mesmos objetos persistentes. Cada thread usa uma ou mais conexões com o banco de dados. Cada conexão com o banco de dados possui sua própria cópia dos objetos persistentes. A lógica das aplicações é expressa através de métodos nos objetos e como cada thread possui sua própria cópia dos objetos persistentes, o acesso a métodos é limitado a um único thread. Dessa forma a lógica da aplicação pode ser escrita sem a preocupação com concorrência.
O protocolo otimista de marcadores de tempo que o ZODB utiliza define que mudanças em objetos individuais são feitas independentemente. Dessa forma, cópias dos objetos não precisam ser bloqueadas. Todas as mudanças são sincronizadas no commit das transações.
Somente uma transação pode executar commit por vez. Se dois threads modificam o mesmo objetos em conexões diferentes, um thread terá garantido o seu commit primeiro. Quando o segundo thread executar o seu commit, uma exceção ConflictError será lançada. A aplicação pode capturar esses conflitos e executar novamente a transação (o que é feito de forma automática pelo Zope, por exemplo). Quando a transação é executada novamente, o estado dos objetos afetados refletem as mudanças feitas pelas transações que já executaram o commit.
Recentemente, em março de 2003, houve uma discussão a respeito do nível de isolamento do ZODB[4]. Até então, a implementação estável do ZODB atendia aos requisitos definidos pela norma ISO para ser considerado no nível de isolamento "read committed". Através de um "patch" escrito por Dieter Maurer, desenvolvedor que colabora com comunidade de desenvolvedores python, o ZODB passaria a ser considerado como uma implementação "repeatable read". Por consenso da comunidade, a implementação do último nível de isolamento foi descartada por questões de performance.
O ZODB permite que algumas configurações sejam feitas para otimizar o controle de concorrência. Dentre elas, é possível criar uma conexão com o banco de dados em modo read-only. Como os conflitos são tratados apenas nas operações que envolvem escrita, bancos de dados que não permitem escrita possuem uma performace significativa maior definido a diminuição do overhead de processamento de conflitos. Outra configuração bastante comum é a quantidade de threads que o ZODB utiliza para processar transações concorrentes. Existem ainda uma série de configurações para permitir que o ZODB funcione com processamento distribuido, numa arquitetura escalável e tolerante a falhas, usando o ZEO - Zope Enterprise Objects[5].
2.1. Subtransações
O ZODB permite 2 níveis de transações encadeadas. Transações podem ser subdividias em sub-transações. Sub-transações podem executar commit ou abort sem afetar a transação na qual estão contidas.
Sub-transações são usadas normalmente para reduzir a quantidade de consumo de memória em transações que modificam muitos objetos. Objetos modificados não podem ser "desativados" e permanecem em memória até que a transação execute commit. Com sub-transações, objetos podem participar de transações menores que, quando executam commit, removem da memória os objetos sem fazer as modificações finais, visto que a transação na qual estão contidos também pode ser abortada.
2.2. Versões
No ZODB, as transações também podem participar de versões. Versões são similares a transações de longa duração. Mudanças podem ser efetivadas em uma versão do banco de dados. Somente usuários dessa versão visualizam as mudanças feitas nessa versão. Uma versão ainda pode ser efetivada no banco de dados principal ou em outras versões.
Versões disponibilizam um mecanismo que permite que mudanças sejam feitas durante um período longo de tempo e em muitos objetos. Mudanças feitas em uma versão não são visíveis até que a versão execute commit ou que a versão descarte as modificações. No Zope, esse recurso permite que mudanças significativas sejam feitas em aplicações web que estejam rodando sem afetar os usuários da aplicação.
Ao invés do protocolo de marcadores de tempo, o uso de versões exige que um protocolo de bloqueio seja implementado, pois versões podem se extender por um longo perído de tempo, uma situação onde uma abordagem otimista como a de marcadores de tempo não é considerada efetiva.
O supporte a versões deve ser provido pela implementação de storage utilizada. Suporte a versões é um serviço opcional dos storages.
Recuperação de Paradas e Falhas
O ZODB possui uma implementação modular que permite que a sua camada de armazenamento (storage) seja implementada seguindo a especificação de uma interface. Atualmente, existem 3 implementações principais para storage, cada uma delas com características específicas no que diz respeito a recursos para recuperação de paradas e falhas.
A principal implementação de storage para o ZODB é a FileStorage, mantida por Jeremy Hylton (que também é o principal desenvolvedor do ZODB). Além dela, existem ainda a DirectoryStorage[6] mantida por Toby Dickenson e a BDBStorage[7] mantida por Barry Warsaw (que utiliza o BerkeleyDB como base).
3.1. FileStorage
O FileStorage é projetado para ter portabilidade. Sua implementação utiliza um arquivo de log único para armazenar todos os objetos e transações. Novas revisões de objetos são armazenadas no final do log. Em memória é mantida uma lista de apontadores para a posição de cada objeto no storage.
O FileStorage suporta Undo e exige que seja feito "pack" do banco de dados periodicamente para excluir versões antigas de objetos e objetos não referenciados (considerando inclusive referências cíclicas). O FileStorage ainda supporta Versions e ZEO.
É a implementação de storage com maior performance. O FileStorage exige uma quantidade de 2 a 10% de memória em comparação ao tamanho do arquivo de log (pois com pouca memória, a performance é muito prejudicada).
O FileStorage não foi planejado para suportar falhas. Apesar de recuperar automaticamente o banco de dados em situações de falta de energia, truncando o arquivo na última transação completada e copiando o restante do arquivo para um "backup" (que permite inspeção futura), o FileStorage não está preparado para situações onde o meio de armazenamento não seja confiável.
O FileStorage não possibilita a replicação de dados e, em caso de crash do arquivo de log, disponibiliza apenas um ferramenta para remoção dos objetos corrompidos; algo que pode resultar em um arquivo de log inaceitável, exigindo que o backup do log seja restaurado integralmente.
No FileStorage o backup pode ser feito "a quente", ou seja, com o banco de dados sendo executado e mantendo a consistência das transações em execução.
3.2. DirectoryStorage
O DirectoryStorage utiliza arquivos e diretórios para armazenar as revisões dos objetos do ZODB. O DirectoryStorage utiliza um arquivo por objeto mais um arquivo por transação. Apesar da implementação simples, o DirectoryStorage foi projetado para ter tolerância a falhas e permitir a recuperação de desastres.
O fato de ser baseado em uma estrutura de diretórios permite que os dados sejam facilmente replicados (em vários discos ou em várias máquinas). A única exigência para um funcionamento seguro é que seja utilizado um filesystem robusto para armazenar a estrutura de diretórios. Esse filesystem não pode armazenar arquivos no /lost+found em caso de falhas e deve ter boa performance com arquivos pequenos. O filesystem recomendado pelo desenvolvedor é o reiserfs.
O DirectoryStorage suporta Undo (e também exige "pack" periódico) mas não suporta versões. É compatível com ZEO. Possui performance inferior ao BDBStorage, mas exige pouca memória para executar.
Além de replicação on-line, o DirectoryStorage também permite backup "a quente", prevendo inclusive situações como falta de energia durante o processo de backup, onde o backup nunca é deixado pela metade (princípio de atomicidade transacional do backup). Além disso, todos os registros possuem um checksum MD5.
3.3. BDBStorage
O BDBStorage usa a edição do BerkeleyDB mantida pela Sleepycat[9]. O BerkeleyDB é um banco de dados embarcado (embedded database) famoso por sua robusteza, performance e escalabilidade. É utilizado em soluções de missão crítica por grandes empresas como a Hitachi, HP, Sun, Amazon, NASDAQ, FujiXerox, Alcatel, British Telecom, Cisco, RSA Security, EMC, Veritas e Motorola.
O Berkeley DB pode ser usado em aplicações que necessitem de
storages concorrentes de alta performance, na recuperação de pares
de chaves e valores. O software é distribuido como uma biblioteca que
pode ser compilada diretamente nas aplicações. Também pode
ser acessado através de interfaces definidas para algumas liguagens como:
C, C++, Java, Perl, Python e TCL. É importante saber que o Berkeley DB
não é um servidor de banco de dados que manipula conexões
de rede, não pode ser considerado como um SGBD relacional ou orientado
a objetos e também não possui uma linguagem de consulta como o
SQL.
O BerkeleyDB é distribuido em 6 versões diferentes[8], sendo que
2 delas são voltadas ao armazenamento nativo de XML. Dentre as versões
convencionais, a versão "Berkeley DB High Availability" possui
a totalidade de recursos e permitirá a criação de um storage
ZODB com replicação, distribuição e balanceamento
de carga.
O DBDStorage tem a grande vantagem de utilizar as ferramentas do BerkeleyDB para administração. Dessa forma, situações de parada ou falha podem ser recuperadas utilizando ferramentas reconhecidamente confiáveis.
O Berkeley DB suporta transações ACID, Undo e Redo (write-ahead logging), recuperação de falhas com checkpoints, utiliza bloqueio para o controle de concorrência (two-phase locking) e detecta deadlocks.
No BDBStorage, cerca de 20 tabelas são utilizadas para armazenar as estruturas do storage, o que inclui os objetos e contadores de referências.
O BDBStorage possui duas variantes, uma implementação mínima e uma completa. Na implementação completa, Undo e versões são suportadas. Na versão mínima esses recursos não estão disponiveis.
A performance do BDBStorage é um pouco inferior a do FileStorage. No entanto, o BerkeleyDB possibilita que a quantidade de memória de cache utilizada seja configurada de forma bastante flexivel.
Assim como no FileStorage, o BDBStorage requer que "packs" periódicos sejam feitos, suporta backup "a quente" e recupera-se automaticamente de falhas.
Referências
[1]http://www.zope.org
[2]http://www.python.org
[3]http://www.python.org/doc/current/lib/module-pickle.html
[4]http://mail.zope.org/pipermail/zodb-dev/2003-March/004683.html
[5]http://www.zope.org/Products/ZEO
[6]http://dirstorage.sourceforge.net
[7]http://www.zope.org/Wikis/ZODB/BerkeleyStorage
[8]http://www.sleepycat.com/products/sidebyside.shtml
[9]http://http://www.sleepycat.com/index.shtml

