Upload
others
View
3
Download
0
Embed Size (px)
Citation preview
Bizantium
Replicação Bizantina de Bases de Dados
Cristóvão Tavares Honorato
Dissertação para obtenção do Grau de Mestre em
Engenharia Informática e de Computadores
Júri
Presidente: Prof. José Delgado
Orientador: Prof. Paulo Ferreira
Co-Orientador: Prof. Nuno Preguiça
Vogais: Prof. Helena Galhardas
Abril de 2009
AbstractDatabase systems are a key component behind many of today’s computer systems. As a consequence,
it is crucial that database systems provide correct and continuous service despite unpredictable circum-
stances, such as software bugs or attacks.
This dissertation presents the design of Byzantium, a Byzantine fault-tolerant database replication
middleware that provides snapshot isolation (SI) semantics. SI is very popular because it allows in-
creased concurrency when compared to serializability, while providing similar behavior for typical work-
loads. In out design, clients execute transactions speculativelly in a primary replica. Only the commit
operation is executed as PBFT operation, which reduces to a minimum the number of PBFT operations
issued. Thus, we are able to minimize the heavy overhead that PBFT features.
Byzantium improves on existing proposals by allowing increased concurrency and not relying on any
centralized component. Our middleware can be used with off-the-shelf database systems and it is built
on top of an existing BFT library.
i
AbstractActualmente as bases de dados transaccionais constituem um componente chave na infra-estrutura
da maioria dos sistemas existentes. Como consequência, é crucial que os sistemas transaccionais
forneçam um serviço correcto e contínuo, apesar da existência de circunstâncias imprevistas como
ataques, erros de software, falhas de hardware ou enganos de um operador.
Esta dissertação apresenta o desenho do Bizantium, um middleware de replicação Bizantina que
implementa a semântica Snapshot Isolation (SI), e é baseado na biblioteca de replicação PBFT. O SI
é actualmente um nível de isolamento popular pois permite um nível de concorrência superior quando
comparado com semânticas que implementam o nível Serializable. No nosso desenho, clientes execu-
tam transacções especulativamente numa réplica, para apenas confirmarem os resultados no momento
do commit, através de uma operação PBFT. Como tal, o nosso desenho reduz o número de operações
PBFT emitidas, evitando o overhead do protocolo de replicação.
O Bizantium melhora relativamente a propostas de replicação semelhantes ao permitir um nível de
concorrência superior, e ao não depender de nenhum componente coordenador centralizado. O nosso
middleware pode ser usado com implementações standard de sistemas transaccionais e é construído
no topo de uma biblioteca de replicação BFT já existente.
ii
AgradecimentosQueria deixar uma nota de agradecimento às duas pessoas que me orientaram e que tornaram esta
tese possível, Professor Rodrigo Rodrigues e Professor Nuno Preguiça. O acompanhamento e auxílio
que me facultaram foram sempre excelentes.
iii
Index
Abstract i
Abstract ii
Agradecimentos iii
1 Introdução 1
1.1 Replicação para tolerar faltas Bizantinas . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.2 Solução proposta . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.3 Contribuição . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.4 Organização da Dissertação . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
2 Trabalho Relacionado 4
2.1 Replicação convencional . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
2.1.1 Tipos de replicação . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
2.1.2 Modelo fail-stop vs modelo bizantino . . . . . . . . . . . . . . . . . . . . . . . . . . 5
2.2 Replicação com faltas Bizantinas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
2.3 Replicação de Base de Dados com faltas Bizantinas . . . . . . . . . . . . . . . . . . . . . 6
2.4 Execução especulativa . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
3 Contexto 9
3.1 Snapshot Isolation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
3.1.1 Definição . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
3.1.2 Garantias da semântica . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
3.2 Practical Byzantine Fault Tolerance . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
3.2.1 Modelo do sistema . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
3.2.2 Propriedades do algoritmo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
3.2.3 Visão geral do algoritmo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
3.2.4 Interface da biblioteca . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
4 Desenho 17
4.1 Modelo do sistema . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
4.2 Arquitectura do Sistema . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
4.3 Como mapear transacções em operações BFT . . . . . . . . . . . . . . . . . . . . . . . . 19
4.4 Visão geral da solução . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
4.4.1 Principais Passos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
4.4.2 Consistência dos dados . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
4.4.3 Correcção dos dados . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
4.5 Funcionamento do sistema . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
4.5.1 Operação begin . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
iv
4.5.2 Execução Especulativa . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
4.5.3 Operação commit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
4.5.4 Cliente Binzantino . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
4.5.5 Réplica primária inactiva . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
4.5.6 Operação rollback . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
4.6 Lidar com concorrência no sistema . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
4.6.1 Controlo de concorrência com first updater wins . . . . . . . . . . . . . . . . . . . 28
4.6.2 Consequências do controlo de concorrência . . . . . . . . . . . . . . . . . . . . . 30
5 Implementação 32
5.1 Operações . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
5.2 Cliente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
5.3 Servidor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
5.3.1 Proxy BFT/Bizantium . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
5.3.2 Núcleo do servidor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
6 Avaliação 37
6.1 Visão geral do benchmark TPC-C . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
6.2 Configuração experimental . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
6.3 Bizantium vs BFT Standard . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
6.4 Bizantium vs Execução Convencional . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
7 Conclusão 45
Bibliography 47
v
List of Figures
3.1 Duas transacções concorrentes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
3.2 Algoritmo BFT, funcionamento normal . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
3.3 Algoritmo BFT, mudança de vista . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
3.4 Interface da biblioteca BFT . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
4.1 Arquitectura do Sistema . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
4.2 Código cliente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
4.3 Código servidor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
4.4 Código servidor, suportando clientes Binzantinos . . . . . . . . . . . . . . . . . . . . . . . 26
5.1 Mensagens de begin e de uma operação . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
5.2 Vista da implementação cliente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
5.3 Interface de comunicação . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
5.4 Vista da implementação servidor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
5.5 Núcleo do servidor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
vi
List of Tables
6.1 Comparação BFT Standard - Bizantium (Resumo) . . . . . . . . . . . . . . . . . . . . . . 38
6.2 Bizantium, execução read-only com 2 clientes . . . . . . . . . . . . . . . . . . . . . . . . 39
6.3 BFT Standard, execução read-only com 2 clientes . . . . . . . . . . . . . . . . . . . . . . 39
6.4 Bizantium, execução read-only com 3 clientes . . . . . . . . . . . . . . . . . . . . . . . . 40
6.5 BFT Standard, execução read-only com 3 clientes . . . . . . . . . . . . . . . . . . . . . . 40
6.6 Bizantium, execução read-only com 4 clientes . . . . . . . . . . . . . . . . . . . . . . . . 41
6.7 BFT Standard, execução read-only com 4 clientes . . . . . . . . . . . . . . . . . . . . . . 41
6.8 Comparação Bizantium - Execução Convencional (Resumo) . . . . . . . . . . . . . . . . 42
6.9 Bizantium, 2 clientes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
6.10 Execução Convencional, 2 clientes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
6.11 Bizantium, 3 clientes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
6.12 Execução Convencional, 3 clientes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
6.13 Bizantium, 4 clientes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
6.14 Execução Convencional, 4 clientes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
vii
1 Introdução
As bases de dados transaccionais formam actualmente um componente chave na infra-estrutura de
muitos dos sistemas. Sistemas de comércio electrónico, websites ou sistemas de informação corpo-
rativos são apenas alguns exemplos de dependências fortes relativamente a um serviço de base de
dados. Como consequência, é crucial que as bases de dados forneçam um serviço correcto e con-
tínuo, mesmo que circunstâncias imprevistas surjam, e.g., erros de software, falhas de hardware ou
ataques.
Os sistema de processamento transaccional são tipicamente sistemas complexos, sofisticados e ex-
tensos, geralmente constituídos por milhões de linhas de código. Este tipo de sistema necessita garan-
tir as propriedades da semântica ACID, e ao mesmo tempo atingir bons níveis de desempenho e de
disponibilidade. Como é habitual em projectos de grande dimensão, podemos contar com um grande
número de erros de software. Um destes erros pode fazer com que o sistema falhe imediatamente
através de um crash. No caso de um crash, o sistema tira partido de mecanismos de recuperação,
como write-ahead logs, e o único impacto sentido pelo cliente é o tempo de espera durante o período
de recuperação. No entanto, um bug pode dar origem a faltas bizantinas. Uma falta bizantina é consti-
tuída por qualquer comportamento arbitrário diferente do correcto, i.e., qualquer tipo de comportamento
incorrecto não necessariamente um crash. Este tipo de falta pode causar que uma resposta incorrecta
seja entregue ao cliente (e.g., cliente emite um select), ou a serialização no estado da base de dados
de informação incorrecta (e.g., cliente emite um update). De facto, um estudo recente aponta que a
maioria dos erros presentes nos logs de três bases de dados comerciais, causam que o sistema falhe
de uma forma Binzatina ao invés de induzirem directamente um crash[1].
1.1 Replicação para tolerar faltas Bizantinas
Uma aplicação pode melhorar a disponibilidade do seu serviço de base de dados através do recurso
à replicação. Muitas técnicas foram desenhas para implementar sistemas com alta disponibilidade,
e podem ser aplicadas directamente a bases de dados. Estas técnicas centram-se em replicar um
serviço, e usar um algoritmo para coordenar as réplicas. Uma visão transparente é fornecida ao cliente,
enquanto que o serviço replicado contínua operacional mesmo que uma fracção das réplicas falhe.
Este tipo de esquemas assume um comportamento benigno por parte dos nós intervenientes, ou seja,
é assumido que um nó apenas falha por crash. Este modelo é apelidado na literatura de fail-stop model.
Na realidade, um nó pode aparentar ser correcto, mas estar a comportar-se incorrectamente, assu-
mindo comportamento Bizantino. As técnicas tradicionais de replicação endereçam com sucesso prob-
lemas de disponibilidade (i.e., ocorrência de faltas benignas), mas falham em aumentar a resistência do
sistema a faltas bizantinas. Estes esquemas normalmente baseiam-se numa vista, que é comandada
por um nó primário. Quando uma operação é executada, o primário recebe a operação, executa-a e
então envia os resultados da operação para as restantes réplicas. As réplicas por sua vez actualizam o
seu estado. Quando um primário falha, os restantes nós iniciam uma mudança de vista garantindo que
o sistema está sempre disponível. Na eventualidade de o primário assumir comportamento Bizantino,
1
este irá contaminar o estado das restantes réplicas porque as coordena.
Algumas técnicas de replicação bizantina para bases de dados foram propostas, no entanto estas
não contemplam níveis de concorrência adequados e limitam fortemente o desempenho do sistema.
Um esquema eficiente foi proposto, mas este baseia-se num nó seguro para coordenar as operações[2].
A biblioteca de replicação PBFT[3] fornece uma forma eficiente de replicação bizantina. O PBFT é
baseado num esquema de replicação por máquina de estados, e tem como requisito que as oper-
ações do serviço replicado sejam determinísticas. Os autores mostram que a biblioteca é eficiente a
replicar serviços como o sistema de ficheiros NFS. No entanto, uma aplicação directa desta biblioteca
à replicação de uma base de dados iria produzir um sistema pouco eficiente, porque para garantir a
equivalência das execuções de cada uma das réplicas, é necessário que cada operação seja execu-
tada sequencialmente, o que não é um nível de concorrência adequado a um sistema de base de
dados.
Acreditamos que existe uma lacuna em termos de replicação descentralizada de bases de dados,
tolerando faltas bizantinas.
1.2 Solução proposta
Neste trabalho é proposto o Bizantium, um middleware de replicação bizantina para bases de dados,
baseado no sistema PBFT e que disponibiliza uma semântica Snapshot Isolation.
Na nossa solução os clientes acedem ao sistema usando uma interface JDBC convencional, pelo
que o Bizantium pode ser usado por qualquer aplicação sem modificação. Ao executar as operações
de uma transacção, o sistema de middleware controla a execução da mesma nas várias réplicas,
garantindo a tolerância a faltas bizantinas. Para tal, o sistema recorre a uma biblioteca BFT que usa
como caixa preta.
Utilizamos o sistema PBFT e as caracteristicas do Snapshot Isolation para mapear transacções nas
diferentes réplicas. Com o objectivo de minimizar o overhead de todo o sistema (dado o peso de exe-
cutar operações tolerantes a faltas bizantinas), o sistema executa especulativamente uma transacção
numa réplica. Apenas no momento do commit é verificada a validade da execução, recorrendo a uma
operação executada através do protocolo PBFT. Caso os resultados da execução especulativa se con-
firmem, a transacção é concluída fazendo progredir o estado das réplicas (correctas). Caso se verifique
a incorrecção da execução especulativa, a transacção é abortada.
Para o correcto funcionamento do sistema, é necessário fazer diferentes verificações de forma a
evitar situações de deadlock, isto para o caso de sistemas de bases de dados que utilizem locks no seu
modelo de concorrência.
1.3 Contribuição
Esta dissertação contemplou o desenho e implementação do Bizantium, uma solução de replicação
com tolerância a faltas bizantinas. O Bizantium utiliza uma biblioteca de replicação bizantina como
2
base, mas minimiza o número de operações com acordo Bizantino por transacção. Permitimos que um
cliente execute operações directamente numa única réplica, de uma forma especulativa.
1.4 Organização da Dissertação
Após a introdução efectuada neste capítulo, esta dissertação continua no capítulo 2 com uma síntese
do trabalho relacionado. No capítulo 3, são apresentados conceitos com os quais o nosso desenho
está fortemente associado; o nível de isolamento Snapshot Isolation e a biblioteca de replicação BFT.
No capítulo 4, é apresentado o desenho e as principais características do Bizantium. Na secção 5 são
descritos, com algum nível de detalhe, os pormenores de implementação do nosso sistema. O capítulo
6 endereça a avaliação do desempenho do Bizantium e a sua comparação com outros esquemas
relevantes. Finalmente, o capítulo 7 faz um balanço de todo o trabalho desenvolvido no âmbito desta
dissertação.
3
2 Trabalho Relacionado
2.1 Replicação convencional
Quase todo o trabalho desenvolvido em replicação, foi desenhado para tolerar apenas faltas benignas
[1]. Este tipo de falta é vulgarmente conhecida por crash, e pode ser definida como um fenómeno
em que um determinado processo entra num estado inactivo, para permanecer indefinidamente nessa
condição. Estas técnicas têm como principal objectivo aumentar a disponibilidade do sistema. Este
acréscimo de disponibilidade é conseguido ao introduzir no sistema mais do que uma cópia do seu
estado; o serviço continua operacional, mesmo que algumas destas cópias estejam inacessíveis (e.g.,
um crash no servidor que alojava uma das cópias replicadas).
Uma técnica base usada para replicar serviços, é a técnica de cópia primária, e funciona da seguinte
forma. Uma réplica é designada a primária; as restantes são secundárias. O primário é responsável
por processar as operações sobre o estado do serviço; alterações ao estado são notificadas às réplicas
secundárias pela réplica primária. Quando um nó falha, a estrutura primário-secundário tem de ser
reorganizada. Esta técnica foi primeiro introduzida por Alsberg, através do seu conceito de resiliência
[4]. Stonebreaker deu também a sua contribuição para este assunto, através do seu modelo [5].
Quóruns, são outra técnica usada para replicar dados. Este tipo de abordagem, baseia-se em
recolher junto das diferentes réplicas, um determinado nível consenso sobre uma operação, tal que,
o estado do sistema se mantenha consistente, sem que seja necessário contactar todas as réplicas
em cada operação. Por outras palavras, este tipo de sistemas assume que, observar uma porção
maioritária dos nós do sistema, é equivalente a observá-los por completo. Um quórum com recurso a
pesos dinâmicos foi introduzido em [6]. Este tipo de quórum introduz flexibilidade, por exemplo, uma
configuração pode ser adoptada para privilegiar a disponibilidade (pesos elevados em nós mais fiáveis)
ou para privilegiar o desempenho (peso acrescido nas réplicas com maior capacidade).
Em [7] foi introduzida uma técnica de replicação que utiliza o conceito de vista. Este esquema usa
uma combinação de replicação por réplica primária, com quoruns. O primário é usado para atribuir
numeros de sequência a todos os pedidos submetidos ao sistema. Quando um primário falha, um
esquema de mudança de vista é usado. Quoruns são usados para garantir que as mudanças de vista
são feitas com sucesso.
2.1.1 Tipos de replicação
Tipicamente os sistemas de replicação são classificados como eager ou lazy. Genericamente esta
classificação é feita com base no facto de os sistemas propagarem, ou não, modificações ao estado do
serviço dentro dos limites da transacção.
Ténicas eager baseiam-se em actualizar todas as réplicas de uma forma atómica, como parte da
própria transacção. Desta forma todas as réplicas estão permanentemente sincronizadas. Este tipo de
técnicas de replicação garante uma execução serializável das transacções, o que faz com que estas es-
tejam imunes a problemas de concorrência. Ainda assim, as técnicas eager penalizam o desempenho
das escritas e aumentam o tempo de resposta das operações em geral.
4
Protocolos de replicação Lazy optam por executar transacções localmente, e apenas propagar as
alterações para os restantes nós, depois de o commit estar já efectuado. Esta propagação é assim
feita assincronamente, e de um modo geral, em períodos regulares no tempo. Esta solução é óbvia
para aplicações móveis, em que os nós estão normalmente desconectados. Existem também alguns
sistemas em que todos os nós estão continuamente conectados, mas que graças à natureza do negócio
que suportam, podem usar esquemas lazy para dessa forma aumentarem o seu desempenho global.
A replicação convencional também pode ser distinguida entre esquemas que asseguram semânticas
transaccionais fortes, como o serializable [8, 9], ou semânticas mais fracas [10, 11, 12] como o Snapshot
Isolation, que permitem níveis de concorrência superiores.
2.1.2 Modelo fail-stop vs modelo bizantino
As técnicas de replicação convencional introduzidas no início deste capítulo toleram apenas faltas be-
nignas, no entanto existem também outro tipo de faltas que não são contempladas pelo modelo benigno.
No modelo fail-stop, ou benigno, um nó é assumido como correcto a menos que esteja numa
condição inactiva. Este tipo de falta é vulgarmente conhecida como crash. Esquemas que apenas li-
dam com faltas benignas, têm como principalmente objectivo aumentar a disponibilidade de um serviço
através da introdução de redundância.
O modelo Bizantino por seu lado, contempla um conjunto de faltas mais alargado, designadas de
bizantinas. Uma falta Bizantina consiste de um comportamento incorrecto por parte de um nó. Este
comportamento não está no entanto limitado a uma falha por crash, como no modelo fail-stop. Uma
falta Bizantina pode ser definida como qualquer comportamento arbitrário, diferente daquele que é o
correcto. Técnicas que toleram este tipo de faltas não fazem nenhuma assunção acerca do compor-
tamento das entidades do seu modelo, e como tal, toleram qualquer tipo de comportamento anómalo
como, ataques, falha de um operador, erros de software ou falhas de hardware.
2.2 Replicação com faltas Bizantinas
De uma forma geral, um sistema que tolera faltas Bizantinas constrói execuções equivalentes em difer-
entes réplicas, para no final executar uma votação. O resultado votado como correcto pela maioria das
réplicas é considerado o correcto e entregue ao cliente.
Construir um sistema eager, que no contexto de uma base de dados eficientemente tolere faltas
Bizantinas, não é uma tarefa trivial. Um sistema eager mantém o estado constantemente actualizado
em cada uma das réplicas do sistema. Para além disso é necessário garantir que as diferentes exe-
cuções são perfeitamente equivalentes entre si [13]. Para tal, as réplicas necessitam acordar sobre as
operações que vão executar. Este acordo corresponde a atingir consenso, que é por si só uma oper-
ação bastante pesada [14, 15, 16]. Por outro lado, não é suficiente garantir uma igualdade na ordem
com que diferentes operações se iniciam, para garantir que as diferentes execuções de um serviço
são equivalentes entre si. Este facto é particularmente verdade quando o serviço em questão se trata
de uma base de dados. Assim, por forma a obter execuções equivalentes e paralelas de um serviço
5
de base de dados, é necessário executar operações em cada réplica de uma forma sequencial, i.e.,
garantir a ordem com que as operações são executadas e para além disso, esperar que uma operação
termine a sua execução, antes de a execução da operação seguinte ser iniciada.
Um protocolo de replicação por máquina de estados como o BFT [17], que é discutido em mais
detalhe na secção 3.2, pode ser usado como base para implementar uma base de dados tolerante a
faltas Bizantinas. O BFT garante que as diferentes máquinas de estado replicadas recebem a mesma
sequência de operações. Embora as operações sejam entregues por ordem, é complexo executá-las
em diferentes réplicas com concorrência. Para que a serialização seja assegurada, as operações têm
de ser executadas sequencialmente, ou seja sem qualquer concorrência. No caso dos sistemas de
bases de dados, e da linguagem SQL, não é directo extrair os conjuntos de escrita e de leitura de uma
operação. Uma extracção deste tipo envolveria acessos à própria base de dados, o que por si só é uma
operação que tem de ser serializada.
2.3 Replicação de Base de Dados com faltas Bizantinas
Existem poucas propostas para a replicação Bizantina de bases de dados. Os modelos propostos por
Garcia-Molina et al. [18] e por Gashi et al. [19, 18] não permitem que transacções se executem concor-
rentemente, o que limita o desempenho do sistema. O Binzantium melhora em relação a estes sistemas
porque tira partido de uma semântica transaccional mais fraca para, sempre que possível, evitar recor-
rer ao pesado protocolo de consenso, permitindo que as transacções se executem concorrentemente.
O algoritmo Commit Barrier Scheduling [2] introduz um esquema de replicação de bases de dados
tolerante a faltas Bizantinas, e que apresenta uma forma de replicação eager. Uma entidade primária e
segura, apelidada de shepherd, controla toda a execução no sistema replicado. Este através da obser-
vação do comportamento do controlo de concorrência na réplica primária, cria nas restantes réplicas
execuções equivalentes e que contemplam um nível de concorrência máximo.
O sistema apresentado em [2] requer que a base de dados utilizada forneça o nível de isolamento
serializable, e que este seja implementado através de strict two-phase locking. Neste tipo de sistemas
as transacções adquirem locks de leitura e de escrita nos registos que acedem, para apenas os lib-
ertarem quando terminam. É o facto de os locks serem detidos por uma transacção até ao momento
do seu commit/abort, que possibilita as observações (exteriores à base de dados) sobre a concorrên-
cia. Devido ao seu baixo grau de complexidade, o shepherd é considerado pelos autores como uma
entidade não susceptível a faltas, i.e., o shepherd é assumido como correcto. Os clientes interagem
com o sistema via shepherd, que por sua vez comunica com as réplicas, coordenando-as. O shepherd
recebe operações de clientes, coordena as execuções nas diferentes réplicas, e recebe os resultados.
Só após recolher um número suficiente de resultados coerentes, é que o shepherd se decide por uma
acção a tomar, por exemplo, responder a um cliente.
Neste sistema as operações são executadas com um nível adequado de concorrência. Uma ré-
plica é designada de primária, e corre operações de uma forma ligeiramente avançada em relação às
restantes réplicas. A ordem com que as transacções se completam no primário, determina um ordem
6
que respeita a propriedade de serialização. Para melhorar o desempenho das execuções nas réplicas
secundárias, o shepherd observa no primário quais as operações que se executam concorrentemente
sem apresentar conflitos. Todas estas operações não conflituosas são enviadas imediatamente para as
restantes réplicas. Exemplificando, o shepherd observa que, para duas transacções activas (sem com-
mit/abort emitido), T1 e T2, se a query Q1 ∈ T1 completar a sua execução depois da query Q2 ∈ T2 se
ter executado com sucesso, então Q1 e Q2, não são conflituosas e podem ser executadas em qualquer
ordem relativa entre si.
O esquema apresentado em [2] é simples e eficiente. No entanto, estas qualidades devem-se ao
facto de o modelo contemplar uma entidade central, e não faltosa - o shepherd. Esta simplificação não
está de todo alinhada com as questões que motivam a construção de sistemas de replicação Bizantina.
Ao invés de contemplar uma pesada fase de consenso, este esquema baseia-se totalmente no seu
shephed para coordenar toda a execução. O facto de o uso de um protocolo de consenso ser evitado,
afecta positivamente o desempenho deste sistema.
2.4 Execução especulativa
A execução especulativa é uma técnica destinada a melhorar o desempenho de um sistema. A es-
peculação pode ser genericamente definida como a execução de código, cujo resultado pode não
ser necessário. Por exemplo, quando um ponto de espera é atingido (e.g., invocação remota), um
mecanismo especulativo pode ser usado para ultrapassar esta espera. Outro exemplo de aplicação
da execução especulativa, é o seu uso no desenho de processadores para optimizar instruções que
contemplam saltos condicionais. A especulação é uma das técnicas que usámos para optimizar o pro-
tocolo do Bizantium e executar especulativamente transacções. Neste sentido, existe um vasto trabalho
relacionado na área da execução especulativa, o qual vamos abordar sumáriamente no restante desta
secção.
O Zyzzyva [20] é um protocolo onde a especulação é apresentada para reduzir o custo da tolerância
a faltas Bizantinas. Neste protocolo um primário propõe uma ordem para os pedidos submetidos por
clientes. Assim, as réplicas ao invés de correrem um pesado protocolo de consenso, aceitam a ordem
estabelecida pelo primário e executam especulativamente os pedidos. Como resultado, um primário
faltoso pode fazer com que o estado de algumas das réplicas correctas se torne divergente, fazendo
com que estas enviem respostas inconsistentes para os clientes. Para colmatar este problema as re-
spostas contêm informação (acerca da execução), que vai ser posteriormente analisada pelo cliente.
Após analisar a metadata, se o cliente classificar a execução especulativa como estável, este usa a
resposta recebida. Em caso contrário, o cliente reúne elementos (nomeadamente, pedaços da infor-
mação recebida a descrever as execuções) que provem a incorrecção da réplica primária, iniciando
uma mudança de vista junto das réplicas. O Zyzzyva, difere dos diferentes protocolos de replicação
Bizantina, ao mover as responsabilidades de verificação de estado para o cliente. É esta diferença que
possibilita a execução especulativa e a ausência do recurso a um protocolo de consenso.
Outra forma de especulação mais genérica, é a disponibilizada pelo Speculator. Este é uma exten-
7
são ao kernel Linux que suporta a execução de processos especulativos. É possível executar processos
em modo especulativo, e no final abortar toda a execução caso esta se tenha provado incorrecta; neste
caso, todo o processo retorna ao estado inicial. O Speculator garante uma execução correcta ao evi-
tar que processos especulativos exteriorizem outputs, até que as especulações das quais os outputs
dependem, sejam tornadas definitivas. O speculator, como uma extensão ao kernel, foi introduzido por
[21]. Foi usado como uma maneira de mascarar latências I/O num sistema distribuído. Esta funcionali-
dade pode ser no entanto adaptada a qualquer tempo de espera genérico.
8
3 Contexto
A nossa solução é baseada no protocolo de replicação BFT. Este protocolo serviu de base construir o
Bizantium. O nosso sistema está alicerçado nas características do protocolo de replicação original, e
no nível de isolamento que exigimos para a base de dados. Neste capítulo introduzimos na secção 3.1
a semântica Snapshot Isolation, e na secção 3.2 o sistema PBFT.
3.1 Snapshot Isolation
O Snapshot Isolation (SI), é uma semântica de controlo de concorrência definida em [22]. Esta con-
templa características atractivas, como o facto de as leituras nunca serem bloqueadas, ou o facto de as
anomalias mais comuns serem evitadas. No entanto, o SI é vulnerável a alguns problemas de concor-
rência [23, 24, 22, 25].
Nesta semântica, as transacções executam-se sobre um corte (snapshot) dos dados da BD. O
snapshot observado por cada transacção é constituído pelos dados que se encontram em estado
definitivo (estado commited) no momento de início da mesma. Como resultado, uma transacção é
totalmente alheia a qualquer modificação levada a cabo por uma transacção concorrente. Para o SI,
duas transacções são conflituosas se os seus conjuntos de escrita se intersectam. Em caso de conflito,
apenas é permitido que uma das transacções em questão seja executada definitivamente no estado da
BD, ao passo que as restantes são abortadas e obrigadas a reiniciar.
No Snapshot Isolation, operações de leitura executam-se sempre sem qualquer bloqueio, e como tal,
esta semântica permite níves de concorrência superiores relativamente a implementações que utilizam
técnicas de locking (e.g., 2PL) para controlar escritas e leituras de uma transacção. Quanto à con-
sistência, o SI não permite nenhum dos fenómenos (phenomena) anómalos de concorrência definidos
no ANSI-SQL [22, 26].
No restante desta secção o Snapshot Isolation é definido em mais detalhe, e são introduzidos con-
ceitos necessários à compreensão das garantias oferecidas pela semântica.
3.1.1 Definição
Uma transacção T1 executando sobre Snapshot Isolation, lê sempre dados de um corte válido no mo-
mento (tempo lógico) em que se iniciou; a este momento chamamos T1_t_inicial. Escritas de outras
transacções concorrentes não são visíveis para T1. Quando T1 está prestes a efectuar commit, é-lhe
atríbuido um valor T1_t_final, esta é autorizada a efectuar commit se nenhuma outra transacção con-
corrente T2 (i.e., uma transacção cujo período de actividade [T2_t_inicial; T2_t_final] se intersecte com
[T1_t_inicial; T1_t_final]) e que ja tenha feito commit, tenha escrito registos que T1 tenciona escrever;
esta regra é chamada de first-committer-wins.
9
3.1.2 Garantias da semântica
Uma execução diz-me em série (serial), se transacções são executadas sequencialmente, ou seja,
sem qualquer concorrência. Uma execução diz-se serializável (serializable) se é possível para duas
transacções concorrentes, encontrar uma execução em série das mesmas, que seja equivalente à exe-
cução original. Ser equivalente neste contexto, significa que a transacção produz os mesmos resultados
e tem o mesmo efeito no estado da base de dados.
Em [26] é definida a norma ANSI-SQL. Três fenómenos de concorrência são definidos: Dirty Read,
Fuzzy Read e Phantom. Quatro níveis de isolamento foram definidos, à custa destes três fenómenos:
Read Uncommitted, Read Committed, Repeatable Read e Serializable.
Em [22], uma extensão é proposta ao ANSI-SQL[26]. Berenson et al. prova que os três fenómenos
originais, definidos no ANSI-SQL, não estão a ser interpretados de forma estrita. É provado que o
nível de isolamento definido como Serializable em [26], na verdade permite que existam violações à
prioridade de serialização. Ao nível de isolamento definido em [26] como Serializable, Berenson passa
a apelidar de Anomaly Serializable, já que este não permite nenhum dos fenómenos de concorrência
definidos, mas permite a existência de execuções que não respeitam a propriedade de serialização.
Para colmatar a falha de interpretação existente no ANSI-SQL, Beresenson reescreve em [22] os
fenómenos de Dirty Read, Fuzzy Read e Phantom, conferindo-lhes interpretações semelhantes, mas
ligeiramente mais abrangentes. É introduzido um novo fenómeno não contemplado no ANSI-SQL, com
o nome de Dirty Write. Berenson mostra que qualquer execução que evite estes quatro fenómenos, é
uma execução serializável [22].
Embora o SI evite os fenómenos clássicos, da forma como estes foram definidos no ANSI-SQL (i.e.,
o SI é Anomaly Serializable), o SI não respeita a nova definição que Berenson propõe para o fenómeno
Phantom. Para provar que o Snapshot Isolation não respeita a propriedade de serialização, Berenson
et al. introduz uma anomalia chamada Write Skew, e mostra que o SI é susceptível à mesma [22].
A anomalia Write Skew é exemplificada na situação seguinte. Uma pessoa P é titular de duas contas
bancárias distintas, sendo que X e Y são os balanços respectivos. O contracto que o cliente tem com
o banco permite-lhe efectuar levantamentos de M unidades de qualquer uma das contas, desde que
X + Y ≥ M . Se os balanços das contas forem, X = 100 e Y = 0, considere-se o cenário onde P
inicia dois levantamentos simultâneos, cada um com o valor de 100 unidades. Estes levantamentos são
processados pelas transacções T1 e T2, que podem ser observadas na figura 3.1.
Figure 3.1: Duas transacções concorrentes
O Snapshot Isolation permite que ambos os levantamentos efectuem commit, já que não é encon-
trado nenhum conflito. Como tal, P consegue efectuar dois levantamentos, com valor total de 200
unidades, o que significa que contas terminam com um balanço final de −100. Esta situação não é pos-
sível em nenhuma execução em série das duas transacções, e constitui uma violação à propriedade de
10
serialização.
Em [23], é apresentada uma anomalia chamada Read-only Transactional Anomaly à qual o SI é
vulnerável. O problema é ilustrado com um exemplo, que é semelhante ao exemplo da anomalia Write
Skew, mas envolve uma transacção de leitura. Duas transacções de escrita são efectuadas concor-
rentemente com uma transacção de leitura, e Fekete mostra que o resultado produzido pela transacção
de leitura é impossível de reproduzir em qualquer execução em série das três transacções, demon-
strando a existência de uma violação da propriedade de serialização. Para mais detalhes ver [23].
Vários métodos propostos para colmatar este tipo de problemas, que podem surgir no SI, são pro-
postos em [25, 24].
3.2 Practical Byzantine Fault Tolerance
Este capítulo fornece uma visão geral sobre o algoritmo PBFT[17, 27] e sobre o BFT, a biblioteca que o
implementa. Apenas são discutidos os aspectos do algoritmo que são relevantes para esta tese. Para
uma descrição completa, ver [3].
Começamos por descrever o modelo do sistema em 3.2.1. A secção 3.2.2, descreve o problema
resolvido pelo método. A secção 3.2.3, dá uma visão geral do algoritmo. Finalmente, a secção 3.2.4
apresenta a interface para a biblioteca BFT.
3.2.1 Modelo do sistema
O algoritmo assume um sistema distribuído assíncrono, onde nós estão ligados por uma rede de co-
municação. Esta rede pode falhar, atrasar ou duplicar a entrega de mensagens. Um modelo Bizantino
de falhas é assumido, i.e., nós que falham podem comportar-se arbitrariamente, sujeitos apenas às
restrições mencionadas em baixo. É considerado que um oponente suficientemente forte, é capaz de
coordenar nós incorrectos, atrasar as comunicações, injectar mensagens na rede, ou até atrasar nós
correctos, com o objectivo de causar o máximo de dano possível ao sistema replicado. Este oponente,
não pode no entanto, atrasar nós correctos indefinidamente.
Técnicas criptográficas são usadas para estabelecer chaves de sessão, autenticar mensagens e
produzir resumos. É assumido que um oponente (e os nós que este controla), não são suficiente fortes
para quebrarem ou inverterem as técnicas criptográficas utilizadas.
3.2.2 Propriedades do algoritmo
Este algoritmo é uma forma de replicação por máquina de estados [13, 28]: o serviço é modelado como
uma máquina de estados, que está replicada em diferentes nós de um sistema distríbuido. O algoritmo
pode ser usado para implementar qualquer serviço que possua um estado e operações sobre o mesmo.
As operações não estão limitadas a simples escritas ou leituras sobre o estado; estas podem efectuar
qualquer computação determinística.
O serviço é implementado por um conjunto de réplicas R, em que cada réplica é identificada por
um inteiro pertencente a {0, ..., |R| − 1}. Cada réplica mantem uma cópia do estado do serviço, e
11
implementa as operações sobre o mesmo. Por questões de simplicidade, é assumido que |R| = 3f + 1,
onde f representa o número máximo de réplicas que podem ser faltosas.
Como todos as técnicas de replicação por máquina de estado, este algoritmo requer que cada
réplica mantenha uma cópia local do estado do serviço. As réplicas devem todas iniciar no mesmo
estado, e devem ter uma execução determinística, na medida em que a execução de uma operação
sobre o estado, com determinados argumentos, deve produzir sempre o mesmo resultado e fazer com
que a transição seja equivalente.
Este algoritmo garante segurança para uma execução, se no máximo f réplicas forem faltosas
dentro de um período de vulnerabilidade Tv. Segurança significa que o serviço replicado respeita
a propriedade de linearibilidade [29]: este comporta-se como uma implementação centralizada onde
operações são executadas atomicamente e de uma forma sequencial. A prova de segurança, para uma
versão simplificada do protocolo, é feita em [27].
O algoritmo também garante progresso: clientes correctos recebem eventualmente as respostas
para as suas invocações, se (1) no máximo f réplicas forem faltosas dentro da janela de vulnerabilidade
Tv, e se (2) ataques denial-of-service não durarem para sempre.
3.2.3 Visão geral do algoritmo
O funcionamento geral do algoritmo é descrito seguidamente. Clientes enviam pedidos para as réplicas,
quando desejam executar operações, enquanto que todas as réplicas correctas executam as mesmas
operações na mesma ordem. Como as réplicas são determinísticas e começam no mesmo estado,
todas as réplicas correctas produzem respostas idênticas para as operações executadas. O cliente
aguarda por f + 1 respostas de réplicas diferentes, com resultados idênticos. Como pelo menos uma
das réplicas que respondeu é correcta, este é o resultado correcto para a operação.
O problema mais acentuado que este algoritmo enfrenta, prende-se com garantir que todas as
réplicas correctas encontram uma ordem total para a execução de pedidos, mesmo que falhas ocorram.
Um esquema primário-secundário é usado para resolver este problema. Graças a este mecanismo, as
réplicas vão sucessivamente pertencendo a configurações que têm o nome de vistas. Numa vista, uma
réplica é a primária, e as restantes são secundárias. No BFT o primário de uma vista é a réplica p, tal
que, p = vmod |R|, onde v é o número da vista actual, sendo que as diferentes vistas são numeradas
consecutivamente de uma forma crescente.
O primário escolhe uma ordem para a execução dos pedidos emitidos pelos clientes. A ordem é
definida através da atribuição de um número de sequência a cada um dos pedidos. Ainda assim, um
primário pode ser faltoso, como tal, os secundários iniciam mudanças de vistas quando existem motivos
para acreditar que o primário é faltoso.
Para tolerar faltas Bizantinas, todos os passos dados por um nó são baseados em obter um certifi-
cado. Um certificado é um conjunto de mensagens provenientes de diferentes réplicas, e que garantem
a validade de um determinado predicado. Um exemplo de um predicado deste tipo é: "‘o resultado de
uma operação emitida por um cliente, é r"’.
O tamanho do conjunto de mensagens num certificado pode ser f + 1 ou 2f + 1, dependendo
12
do tipo de predicado e do passo que está a ser dado. A correcção do sistema depende do facto de
um certificado nunca conter mais de f mensagens enviadas por réplicas faltosas. Um certificado de
tamanho f + 1 é suficiente para provar que o predicado é correcto, já que contém pelo menos uma
mensagem de uma réplica não faltosa. Um certificado de tamanho 2f + 1 assegura que é possível
convencer outras réplicas da validade do predicado, mesmo quando f réplicas são faltosas.
Outros algoritmos de tolerância a faltas Bizantinas [30, 31], baseiam-se no poder de assinaturas
digitais para autenticar mensagens e construir certificados. Por sua vez, este algoritmo usa message
authentication codes (MACs)[32], para autenticar todas as mensagens no protocolo. Um MAC é uma
pequena string de bits que é função de uma mensagem e de uma chave. Esta chave apenas é partil-
hada entre o emissor e o receptor da mensagem. O emissor junta o MAC à mensagem do protocolo,
para que o receptor possa verificar a autenticidade da comunicação.
O uso de MACs melhora substancialmente o desempenho do algoritmo - MACs usam técnicas de
criptografia simétrica, ao passo que assinaturas digitais usam técnicas de chave pública - mas também
tornam o algoritmo mais complicado: o receptor pode ser incapaz de convencer uma terceira entidade
de que a mensagem é autêntica, já que esta terceira parte pode não conhecer o segredo usado para
gerar o MAC associado à mensagem.
3.2.3.1 Processamento de pedidos
Quando o primário recebe um pedido, este usa um protocolo de três fases para garantir um multicast
atómico dos pedidos para as diferentes réplicas. As três fases são pre-prepare, prepare e commit. As
fases de pre-prepare e prepare, são usadas para ordenar totalmente os pedidos dentro da mesma vista,
mesmo quando o primário (que propõe a ordem) é faltoso. As fases de prepare e commit, são usadas
para garantirar que os pedidos estão totalmente ordenados mesmo em diferentes vistas.
A figura 3.2 mostra a operação do algoritmo no caso normal, em que o primário é correcto. Neste
exemplo, a réplica 0 é o primário e a réplica 3 é incorrecta. O cliente inicia por enviar o seu pedido para o
primário, este envia para todas as réplicas uma mensagem de pre-prepare. Esta mensagem propõe um
número de sequência para o pedido, e se as restantes réplicas concordarem com a ordem proposta,
estas trocam entre si mensagens de prepare. Quando uma réplica obtem um certificado de 2f + 1
mensagens de prepare, esta envia para as restantes uma mensagem de commit. Ao reunirem 2f + 1
mensagens de commit, correspondendo ao pre-prepare de um determinado pedido, as réplicas estão
em condições de executarem esse pedido, produzindo uma resposta e actualizando o seu estado. Esta
resposta é enviada directamente para o cliente, que espera por f + 1 respostas, de diferentes réplicas,
com o mesmo resultado.
Cada réplica guarda o estado do serviço, um log que contém informação sobre os pedidos, e um
inteiro que indica qual a vista actual da réplica. A informação guardada no log acerca de cada pedido
contempla o número de sequência associado ao mesmo, bem como o seu estado actual; as possibil-
idades para este estado são: unknown (estado inicial), pre-prepared, prepared, e commited. A figura
3.1 mostra também a evolução do estado do pedido à medida que o protocolo avança.
Cada réplica pode descartar entradas do log assim que os pedidos correspondentes tenham sido
13
Figure 3.2: Algoritmo BFT, funcionamento normal
executados por, pelo menos, f +1 réplicas não faltosas. Esta condição é necessária para assegurar que
qualquer pedido vai ser conhecido, mesmo depois de uma mudança de vista. O custo desta verificação
é reduzido já que esta apenas é efectuada quando um pedido com um número de sequência divisivel
por uma constante (e.g., K = 128) é executado. O estado produzido por uma execução deste tipo é
apelidado de checkpoint. Quando uma réplica produz um checkpoint, esta envia para todas as restantes
uma mensagem de checkpoint contendo o seu estado d, e o número de sequência do ultimo pedido
cuja execução é reflectida no estado, n. Após enviar esta mensagem, a réplica espera até obter um
certificado com 2f + 1 mensagens de checkpoint válidas e consistentes. Neste ponto o checkpoint é
dado como estável e a réplica está em condições de apagar todas as entradas do log com números de
sequência inferiores ou iguais a n; outros certificados de checkpoint são também descartados.
Criar checkpoints através da copia integral do estado seria demasiado dispendioso. A biblioteca
constrói o checkpoint de forma a que este apenas contenha diferenças em relação ao estado actual.
3.2.3.2 Mudanças de vista
O protocolo de mudança de vista permite que o sistema efectue progresso, mesmo quando o primário
falha. O protocolo deve ainda assim preservar a segurança: deve ser assegurado que réplicas correctas
acordam quanto aos números de sequência de operações em estado commited, mesmo que uma
mudança de vista tenha ocorrido entretanto.
Mudanças de vista são iniciadas por timeouts, que previnem os secundários de aguardarem in-
definidamente que pedidos se executem. Um secundário encontra-se em espera quando já recebeu
um pedido válido, mas ainda não o executou. O secundário inicia um contador (para medir o timeout)
quando recebe um pedido e ainda não tem um contador activado. Este contador é parado quando a
réplica já não se encontra em espera que o pedido se execute, mas pode ser reiniciado se nesse ponto
a réplica estiver a esperar outro pedido distinto.
Se o contador do secundário i expira na vista v, o secundário ínicia uma mudança de vista para
mover o sistema para a vista v + 1. Este pára de aceitar mensagens (apenas aceita mensagens de
checkpoint, view-change e new-view) e envia para todas as restantes réplicas uma mensagem de view-
change. A figura 3.3 ilustra esta situação.
14
Figure 3.3: Algoritmo BFT, mudança de vista
O novo primário p, para a vista v + 1, obtém um certificado com 2f + 1 mensagens de view-change
para a vista v + 1. Depois de obter o certificado para a nova vista, e de actualizar o seu log, p envia
para todas as réplicas uma mensagem de new-view, e entra na vista v+1: neste ponto, o novo primário
está em condições de processar mensagens pela vista v +1. Um secundário aceita uma mensagem de
new-view para a vista v + 1, se esta estiver própriamente assinada, ou seja, se contiver um certificado
de nova vista apropriado, e se o novo número de sequência não for conflituoso com pedidos em estado
commited e pertencentes a vistas anteriores. Se todos estes requisitos forem cumpridos e o nó aceitar
a mudança de vista, então este está em condições de aceitar mensagens para esta nova configuração.
3.2.4 Interface da biblioteca
A biblioteca BFT implementa o algoritmo descrito anteriormente. A sua interface básica pode ser ob-
servada na figura 3.4.
Figure 3.4: Interface da biblioteca BFT
O procedimento invoke é chamado pelo cliente para invocar uma operação no sistema replicado.
Este procedimento executa todo o lado cliente do protocolo de replicação, e retorna o resultado ao
cliente quando réplicas suficientes já responderam.
Quando a biblioteca necessita de executar uma operação numa réplica, esta faz uma chamada
a execute, que leva a cabo a execução da operação conforme foi especificada para o serviço. Os
argumentos para este procedimento incluem: um buffer com a operação requisitada (e respectivos
argumentos), um buffer para preencher o resultado da operação, o identificador do cliente que emitiu
o pedido, e um boleano indicando se o pedido foi processado através da optimização de leitura. O
código do serviço pode utilizar o identificador de cliente para executar controlo de acesso, e o boleano
15
recebido para rejeitar pedidos que modifiquem o estado do serviço, mas que tenham sido marcados
como apenas de leitura.
16
4 Desenho
Já foram desenvolvidos sistemas que disponibilizam um meio de construir serviços replicados, tolerando
faltas bizantinas. A utilização directa de um desses sistemas para replicar uma base de dados, teria
como resultado final um sistema com um overhead muito grande, devido ao protocolo de consenso, e a
disponibilizar um fraco nível de concorrência na execução de transacções. Estes constituem maus indi-
cadores de desempenho. Para além disso, se a base de dados seleccionada utilizar locks na implemen-
tação do seu modelo de concorrência, quaisquer duas transacções que disputem um lock despoletam
um deadlock em todo o sistema.
O problema do deadlock surge do facto de as operações serem executadas sequencialmente, i.e.,
o sistema espera que uma operação termine antes de a próxima ter início. Se duas operações (per-
tencentes a transacções distintas) disputarem um lock, a primeira adquire-o com sucesso, enquanto a
segunda operação tenta obter o lock e fica indefinidamente à espera, colocando o sistema em deadlock.
Na verdade, a nossa solução pode ser vista como uma optimização, à aplicação directa do sistema
PBFT, para construir um sistema de base de dados replicado [3]. Assim, o desenho foi guiado pelo
objectivo de evitar os aspectos onde a abordagem naive falha:
• A concorrência com que cada réplica executa operações.
• A execução de um pesado protocolo de consenso para cada operação emitida.
• Evitar o problema de deadlock, garantindo o progresso (liveness) do sistema.
4.1 Modelo do sistema
O Bizantium usa o algoritmo de replicação por máquina de estados, PBFT, como um dos seus com-
ponentes base. Como tal, o modelo e as garantias disponibilizadas por este sistema (ver secção 3.2)
são herdadas. Assumimos um modelo Bizantino onde os nós (clientes ou servidores) podem exibir
qualquer comportamento arbitrário. Assumimos também que um adversário pode coordenar nós in-
correctos, mas não consegue quebrar as técnicas criptográficas usadas. Um máximo de f réplicas
incorrectas são assumidas, de um total de n = 3f + 1 réplicas.
O nosso sistema garante a propriedade de segurança (ver secção 3.2.2) em qualquer sistema dis-
tribuído assíncrono onde os nós estão ligados por uma rede. Esta rede pode falhar, corromper ou
atrasar a entrega de mensagens. O progresso (liveness) do sistema é garantido se o atraso na entrega
das mensagens não crescer indefinidamente.
4.2 Arquitectura do Sistema
O Bizantium foi construído como um sistema de middleware que fornece replicação para bases de
dados, com tolerância a faltas Bizantinas. A arquitectura do sistema, que pode ser observada na figura
4.1, é composta por um conjunto de n = 3f + 1 servidores e um número finito de clientes.
Cada servidor é composto por dois proxy distintos, o Byzantium replica proxy e o BFT replica proxy,
que estão ligados ao Replica Core; este por sua vez está ligado a uma base de dados local. A base
17
Figure 4.1: Arquitectura do Sistema
de dados contém uma cópia integral do estado do serviço. Em conjunto os dois proxy recebem todo
o input do servidor. O BFT replica proxy recebe o input proveniente da biblioteca de replicação PBFT,
através da implementação da rotina execute, que é chamada pela biblioteca no momento em que uma
operação é entregue (ver secção 3.2.4). O Bizantium replica proxy recebe o input dos clientes do
Bizantium; as comunicações recebidas por este proxy não são serializadas pelo protocolo PBFT, esta é
uma característica importante do nosso desenho (explicado adiante). O Replica Core é responsável por
executar e coordenar as operações do sistema na base de dados. O objectivo deste último componente
é garantir um comportamento correcto de cada uma das réplicas, mais concretamente, garantir que num
momento hipotético onde nenhuma operação esteja a ser executada no sistema, o estado de todas as
réplicas correctas é equivalente.
O sistema de base de dados usado em cada servidor pode ser diferente. Esta diferenciação garante
um grau mais baixo de correlação entre faltas, se estas forem causadas por bugs de software [2]. Ape-
nas impomos para as bases de dados que implementem uma semântica Snapshot Isolation, e que
permitam a criação savepoints. Esta última característica é bastante comum nos sistemas transac-
cionais actuais.
Aplicações que usam o Bizantium correm em nós cliente e acedem ao nosso sistema através de uma
interface standard - o JDBC. Assim, aplicações que acedem a sistemas convencionais usando JDBC,
podem usar o Bizantium sem qualquer modificação. O driver JDBC que construímos é responsável por
implementar o lado cliente do nosso protocolo, o Bizantium Client Proxy. Algumas partes do lado cliente
do protocolo consistem em invocar operações que são processadas através do protocolo de replicação
PBFT, como tal, o lado cliente do Bizantium está ligado com o lado cliente da biblioteca PBFT. Esta
18
ligação está representada através do BFT client proxy.
Por motivos de flexibilidade, no nosso desenho o PBFT é usado como uma caixa negra. Isto per-
mite uma trocar esta biblioteca por uma semelhante, que forneça as mesmas garantias (replicação por
máquina de estados e lineriabilidade) e que tenha uma interface de programação semelhante.
4.3 Como mapear transacções em operações BFT
O primeiro aspecto a considerar ao desenhar um sistema de base de dados tolerante a faltas bizantinas,
é como mapear operações pertencentes a uma transacção, para o esquema de replicação máquina de
estados que vai ser usado como motor da solução.
Uma solução básica seria propagar cada operação como uma operação da máquina de estados.
Esta abordagem produziria, nas diferentes réplicas, execuções equivalentes, garantindo uma con-
vergência do estado do sistema. No entanto, como já foi referido, esta abordagem não é boa em
termos de desempenho. Por um lado, as diferentes bases de dados estão restringidas a executar oper-
ações sequencialmente, sem qualquer tipo de concorrência associada. Por outro lado, cada operação
estaria sujeita ao overhead do protocolo de replicação, que por norma é significativo. Para além dos
problemas de desempenho, e conforme referido no início deste capítulo, este tipo de soluções pode
colocar o sistema em deadlock, caso a base de dados usada utilize locks explícitos para sincronizar
transacções.
A nossa solução é baseada no facto de cada transacção ser considerada definitiva, apenas no
momento em que o seu commit tem sucesso. Assim, executamos especulativamente a transacção,
baseando-nos nos resultados de uma réplica (possivelmente Bizantina) até ao momento do commit.
Então, antes da operação de commit ser executada nas réplicas, estas verificam se os resultados
das leituras e das escritas estavam correctos, abortando a transacção no caso de alguma incorrecção
ser detectada. Se o commit falhar, cabe ao cliente lidar com a situação, tipicamente reexecutando a
transacção. Esta abordagem apenas requer que duas operações sejam executadas como operações
BFT: a operação de begin e o commit/rollback. A operação de begin vai construir o snapshot em que a
transacção se executará - é necessário executar esta operação nas diferentes réplicas em momentos
equivalentes, para mais tarde ser possível verificar a correcção da execução da transacção. A operação
de commit/rollback necessita de ser serializada para assegurar que transacções conflituosas ou con-
correntes têm o mesmo resultado em todas as réplicas (apenas uma destas transacções pode efectuar
com sucesso commit).
4.4 Visão geral da solução
Esta secção é uma introdução informal ao Bizantium, em que o seu funcionamento é descrito de uma
forma simplificada.
19
4.4.1 Principais Passos
A execução típica de uma transacção no nosso sistema, pode ser subdivida em três passos principais:
• Estabelecimento do snapshot
• Corpo da transacção
• Finalização e verificação de resultados
4.4.1.1 Estabelecimento do snapshot
No momento em que a transacção tem inicio, o cliente envia um pedido que estabelece um novo objecto
transacção em cada uma das réplicas. A mensagem é serializada via PBFT e entregue nas réplicas.
4.4.1.2 Corpo da transacção
Após a transacção iniciada, o cliente executa as respectivas operações contactando directamente uma
réplica. A réplica onde o cliente escolhe executar as suas operações é apelidada de primária, e pode
ser escolhida ao acaso. Uma vez escolhida, a réplica primária recebe e executa todas as operações
associadas à transacção. Como é intuitivo, estas operações são executadas na transacção que foi
criada no passo 1.
4.4.1.3 Finalização e verificação de resultados
A dado ponto o cliente irá terminar a transacção que iniciou. Este poderá querer confirmar o trabalho
que efectuou emitindo um commit, ou cancelar todo o trabalho através de um rollback. Neste ponto
o cliente emite o seu pedido, serializando-o como uma operação do protocolo PBFT. Neste pedido
(de commit ou rollback) o cliente inclui informação que irá permitir reproduzir e testar a validade da
execução levada a cabo no passo 2. São feitas verificações a diferentes níveis:
• Correcção da interacção com a réplica primária. Uma réplica primária Bizantina, pode ter fornecido
resultados incorrectos para as operações submetidas pelo cliente.
• Correcção do cliente. Um cliente malicioso poderia coordenar um ataque na tentativa de fazer o
estado do sistema replicado divergir.
• Garantia das propriedades do Snapshot Isolation. Duas ou mais transacções, em réplicas primárias
distintas, podem entrar em conflito. Pelas propriedades da semântica SI apenas uma pode termi-
nar com sucesso.
Toda a execução levada a cabo no passo 2 é verificada posteriormente. Assim um cliente ao
executar-se com base nos resultados obtidos em 2, está sujeito a ter de mais tarde abortar e reini-
ciar (por exemplo, no caso de um primário incorrecto). Graças a este facto, classificamos a execução
em 2 como especulativa.
20
4.4.2 Consistência dos dados
No nosso desenho, cada uma das transacções cliente tem um estado associado em cada réplica: uma
representação (transacção) por réplica. Para qualquer réplica correcta, o nosso sistema garante que o
corte de dados (snapshot) observado no momento da criação da transacção, é equivalente em todos os
nós. Ou seja, o nosso esquema garante que as várias representações criadas no passo inicial partilham
o mesmo snapshot.
Esta propriedade é válida porque tratamos de forma especial as operações que podem influenciar o
snapshot associado a uma transacção, como é o caso das operações de begin, commit e rollback. Na
nossa solução estas operações são tratadas como operações do protocolo de replicação PBFT, o que
faz com que estas operações sejam entregues e executadas nas réplicas respeitando uma ordem total.
Exemplificando, se a replica R1 é correcta, recebendo e executando a operação de commit C1 antes
de uma operação de begin B1, então qualquer outra réplica correcta receberá e executará C1 antes de
B1.
Através destas assunções, podemos permitir que um cliente execute as suas operações directa-
mente numa réplica primária, com a garantia de que a qualquer momento, a aplicação da mesma
sequência de operações que ocorreram na réplica primária, mas nas réplicas secundárias, irá produzir
o mesmo resultado; trata-se de uma aplicação simples das características da semântica SI.
Como descrito no capítulo 3, o SI baseia-se num controlo de concorrência onde uma transacção não
observa modificações feitas foram do seu âmbito (i.e., outras transacções concorrentes). No entanto,
conflitos podem existir. Estes são resolvidos de uma forma directa; a primeira transacção a confirmar a
sua execução (através de um commit) é serializada, enquanto as restantes são abortadas ao tentarem
executar commit - first commiter wins. De forma semelhante no nosso sistema, dois (ou mais) clientes
que se executem especulativamente em réplicas primárias diferentes, podem estar a efectuar trabalhos
conflituosos. A abordagem usada para resolver o problema é semelhante à descrita antes, o primeiro
cliente a efectuar commit (de acordo com a ordem imposta pelo PBFT) serializa a sua transacção, ao
passo que todas as transacções conflituosas seguintes serão forçadas a abortar.
4.4.3 Correcção dos dados
Como explicado, o PBFT é usado por clientes para executar operações totalmente ordenadas entre si.
Como o PBFT trata o problema de um cliente Bizantino numa operação individual, o nosso sistema
apenas necessita garantir a validade das operações que são emitidas. Por outras palavras, existe
a garantia de que uma operação emitida via PBFT por um cliente, é entregue em todas as réplicas
totalmente ordenada e com garantias de integridade.
No momento de commit, toda a sequência de operações trocada entre o cliente e a réplica primária,
bem como um resumo criptográfico dos resultados, são enviados como parâmetro da operação PBFT.
Cada uma das réplicas secundárias aplica a sequência de operações recebida, e produz também um
resumo criptográfico dos resultados que obteve, comparando-o com o resumo que recebeu. Na per-
spectiva de uma réplica correcta, a interacção que o cliente efectuou com o seu primário é correcta
21
apenas se os resumos coincidem.
Na perspectiva do cliente, a sua interacção com a réplica primária é considerada correcta no mo-
mento em que a execução da operação de commit termina com sucesso. O facto de a operação
ser concluída com sucesso, indica que a fase de verificação que engloba foi também concluída com
sucesso. Isto constitui prova para o cliente de que o primário se comportou correctamente.
4.5 Funcionamento do sistema
Nesta secção, explicamos em mais detalhe o processo de executar uma transacção no nosso sistema.
O código executado pelo lado cliente pode ser observado na figura 4.2, enquanto que o código exe-
cutado pelas réplicas está representado na figura 4.3. Alguns detalhes são omitidos, como tratamento
de erros, por motivos de simplicidade do conteúdo. Apenas discutimos o problema de um cliente ser
Bizantino na secção 4.5.4, como tal, até lá é assumido um comportamento benigno por parte dos
clientes.
Figure 4.2: Código cliente
4.5.1 Operação begin
A aplicação inicia-se tipicamente pelo estabelecimento de uma ligação. No momento em que o cliente
cria a ligação, este não tem nenhuma transacção associada, e como tal, o nosso middleware inicia o
22
Figure 4.3: Código servidor
processo de criação de uma nova transacção.
O objectivo é criar em todas as réplicas uma nova transacção, cujo snapshot seja equivalente. Esta
nova transacção (i.e. cada uma das novas representações criadas nas réplicas) tem de estar associada
à ligação, já que no momento do commit o proxy cliente do Bizantium necessita de indexar globalmente
a transacção. Por outro lado, independentemente de qual o primário escolhido pelo cliente, este tem de
conseguir indexar a transacção que corresponde ao seu fluxo de execução especulativo.
A emissão de uma operação de BEGIN é tratada no nosso sistema como uma operação PBFT, o
cliente envia como parâmetros da operação um identificador universal e a identificação da réplica que
vai ser a primária na transacção. A escolha da réplica primária pode ser feita aleatoriamente. Por
motivos de distribuição de carga, essa é a abordagem que adoptámos.
Cada réplica ao receber uma operação de BEGIN, vai criar uma nova transacção, e associá-la
ao identificador recebido. O Bizantium client proxy ganha assim uma forma de indexar globalmente a
transacção no momento de commit. Um repositório é também criado para guardar pedidos e resultados
correspondentes a execuções especulativas, isto no caso de a réplica actual ser a primária.
Esta funcionalidade pode ser observada na figura 4.2. O código do proxy cliente do Bizantium, ao
23
criar uma nova ligação, invoca a função db_begin(). Este gera um novo uid. Uma réplica é escolhida
aleatoriamente para executar especulativamente as operações - a réplica primária. Neste ponto o
cliente executa através do PBFT uma operação de BEGIN, invocando o procedimento BFT_exec(<
BEGIN, ... >) . Cada réplica cria uma nova transacção, e associa-a ao identificador universal recebido.
O facto de as operações de BEGIN e COMMIT serem executadas via PBFT, juntamente com as pro-
priedades oferecidas pelo mesmo, asseguram que a transacção criada em cada réplica é equivalente.
4.5.2 Execução Especulativa
Depois de executada a operação BEGIN através do nosso protocolo, qualquer réplica está em condições
de receber operações de escrita e leitura. No entanto a réplica primária já foi escolhida. O cliente então
executa as suas operações especulativamente nesta réplica. Toda a comunicação feita com a réplica
primária é efectuada directamente, e não através da biblioteca PBFT.
Para executar uma operação, o cliente envia uma mensagem para a sua réplica. Nesta mensagem é
incluída informação sobre a execução, concretamente, qual o nome da réplica primária, o identificador
global da transacção e a operação em questão (e.g., uma leitura ou escrita). No final, o cliente guarda
a operação e a resposta que obteve. Este último passo é importante, pois é esta informação que
mais tarde vai ser entregue junto das restantes réplicas, com o objectivo de determinar a correcção da
execução especulativa.
Quando uma réplica recebe um pedido solicitando a execução especulativa de uma operação, esta
utiliza o id enviado pelo cliente para indexar a transacção em questão, e verifica se o cliente está a
contactar a réplica que antes escolheu para ser primária. A réplica então executa a operação, guarda o
pedido (e respectiva resposta) e envia o resultado ao cliente.
A figuras 4.2 e 4.3, contêm o código que diz respeito a esta fase. Exemplificando, quando uma
aplicação cliente executa um statement JDBC, o código do driver (código cliente) vai a dado ponto
correr o procedimento db_op(trxHandle, op). Esta função envia os dados relativos à execução para a
réplica (função replica_exec), registando a reposta e o pedido associados à operação. Quanto à réplica
primária, esta quando recebe um pedido de operação especulativa, executa a função replica_exec.
4.5.3 Operação commit
A execução da transacção é terminada com operação de COMMIT. Uma mensagem de commit, acom-
panhada de toda a informação necessária ao funcionamento do protocolo, é executada via PBFT. Para
este passo ter sucesso, cada réplica tem de verificar que a execução especulativa feita anteriormente
é válida. Esta verificação é feita através de diversos passos.
Todas as réplicas para além da primária, têm de executar as operações associadas à transacção, e
verificar se os seus resultados coincidem com os resultados produzidos pela réplica primária. Tendo em
conta um comportamento determinístico por parte da base de dados, e assumindo que a transacção
se irá executar num snapshot equivalente em todas as réplicas (operação begin), se o primário é cor-
recto então todas as restantes réplicas correctas irão obter os mesmos resultados. Se o primário tiver
24
produzido resultados incorrectos, os resultados obtidos na verificação não irão coincidir. Neste caso,
as réplicas correctas irão abortar a transacção e o cliente irá receber uma excepção, sinalizando o
comportamento Bizantino.
Finalmente, verificamos em todas as réplicas que as propriedades do Snapshot Isolation se mantêm
para a transacção. Esta verificação é semelhante às verificações feitas por sistemas de replicação de
bases de dados não Bizantinas (e.g.,[10]) e é descrita em pormenor na secção seguinte.
Esta funcionalidade pode ser observada na figura 4.3. No momento do commit, o proxy cliente do
Bizantium invoca o procedimento db_commit. Ao receber esta mensagem, cada uma das réplicas corre
a funcionalidade que está descrita na função BFT_exec (linha 6) da figura 4.3.
4.5.4 Cliente Binzantino
O sistema necessita de tolerar clientes Bizantinos, que tentem fazer com que o comportamento do
sistema se desvie do correcto. O protocolo de replicação PBFT reduz bastante o espectro de ataques
que um cliente pode executar, pois fornece garantias ao nível da ordem e da integridade na forma como
as suas operações se executam. Assim as verificações que necessitamos fazer prendem-se com a
validade das operações.
Um cliente executa operações na sua réplica primária e mais tarde propaga a sequência completa
de operações (e o resultado) para todas as réplicas. Esta lista de operações é recebida por todas as
réplicas no momento de commit, e neste instante, o primário não executa as operações uma vez que
já as executou adiantadamente. Um cliente Binzantino poderia explorar esta característica propagando
uma lista de operações diferente da sequência de operações que foram previamente executadas junto
do primário. Este ataque teria como resultado a divergência do estado da réplica primária em relação
aos restantes nós do sistema.
Perante um ataque deste tipo, as réplicas secundárias não estão em condições de detectar qualquer
inconsistência; não estão na posse de elementos suficientes para tal. Assim torna-se óbvio que a
solução tem de partir da réplica primária. A hipótese de introduzir uma troca extra de mensagens para
sinalizar as restantes réplicas, é uma solução pouco elegante, menos eficiente e complexa de serializar.
A nossa solução passa por fazer o estado da réplica primária retornar a um savepoint definido no início
da transacção, e executar a mesma lista de operações que as outras réplicas executaram, i.e., a lista
propagada pelo cliente Bizantino. Desta forma, todos os nós irão produzir a mesma execução, atingindo
o mesmo estado.
O código executado pelo réplica para suportar clientes Bizantinos, pode ser observado na figura
4.4. Para conseguir comparar se a sequência de operações executadas especulativamente coincide
com a sequência submetida com a operação de commit, o primário também regista as operações e
os seus resultados à medida que estes são executados (linha 42). No momento de commit, se a lista
recebida diferir das operações que estão registadas, o coordenador descarta as operações executadas
na transacção actual e executa as operações recebidas na lista.
Para descartar as operações executadas, um savepoint é usado. Este é um mecanismo bastante
generalizado nas bases de dados actuais, e permite fazer com uma transacção retorne a um dado
25
Figure 4.4: Código servidor, suportando clientes Binzantinos
estado onde um savepoint foi declarado. Quando uma operação de begin se executa, um savepoint é
criado imediatamente, ou seja no snapshot inicial (linha 3). Mais tarde, quando for necessário descartar
as operações executadas e usar o snapshot inicial, a transacção é retrocedida até ao estado inicial
(linha 17). Isto assegura que todas as réplicas, incluindo a primária, executam a mesma sequência de
operações no mesmo corte de dados, garantindo a convergência do estado das diferentes réplicas e
um correcto funcionamento do sistema.
4.5.5 Réplica primária inactiva
Uma réplica primária Bizantina pode retornar resultados incorrectos ou não responder de todo. Con-
forme foi explicado anteriormente, a primeira situação é resolvida através da verificação, no momento de
26
commit, da validade da execução especulativa. Isto garante que as réplicas correctas apenas aceitam
transacções para as quais o primário emitiu resultados correctos em todas as operações processadas.
Se a réplica primária não responde a um pedido de operação, o cliente selecciona um novo primário
para substituir o anterior. Neste ponto, o cliente repete as operações que já tinha até esse ponto
executado, mas no novo primário. Se os resultados obtidos não coincidirem, o cliente aborta desde logo
a transacção através de uma operação de rollback e emite uma excepção sinalizando comportamento
Bizantino. Se os resultados coincidirem, o cliente prossegue com a execução.
No momento de commit, uma réplica que acredite ser primária de uma transacção, verifica se a
sequência de operações enviada pelo cliente é a mesma que a própria executou. Assim, se um coorde-
nador que foi substituído estiver activo, este irá descobrir que operações adicionais foram executadas.
Como explicado em secções anteriores, o nó descarta as operações executadas na transacção, e ex-
ecuta a lista de operações recebidas, tal como qualquer outra réplica. Isto garante um comportamento
correcto do sistema, já que todas as réplicas, incluindo um primário substituído, executam a mesma
sequência de operações no mesmo snapshot de base de dados.
4.5.6 Operação rollback
Quando uma transacção termina através de uma operação de ROLLBACK, uma abordagem possível é
abortar a transacção em todas as réplicas sem verificar a validade dos resultados retornados durante a
execução. No nosso sistema esta abordagem pode ser implementada através de uma operação PBFT
que aborte a transacção em cada réplica.
Esta forma de implementar a operação garante um correcto funcionamento do sistema, o trabalho
é simplesmente abortado e nenhuma inconsistência no estado surge desta linha de acção. No entanto,
no caso de uma réplica primária Bizantina, a aplicação pode ter observado dados incorrectos durante
o curso da transacção, que podem ter levado à decisão errada de emitir um rollback. Por exemplo, con-
siderando uma transacção tentando reservar um lugar num determinado voo, que ainda tem lugares
disponíveis, quando a transacção efectua uma query à base de dados, um primário pode incorrecta-
mente indicar ao cliente que o voo se encontra esgotado. Como consequência, a aplicação pode decidir
terminar a transacção através de uma operação de ROLLBACK. Se nenhuma verificação dos resulta-
dos que foram retornados for feita, o cliente toma a decisão de abortar a transacção baseado numa
observação incorrecta do estado da base de dados.
Para detectar este tipo de situações, decidimos incluir uma fase de verificação de resultados no
processamento de operações de ROLLBACK. Assim, a execução de um rollback torna-se semelhante
à execução de um commit. Se a verificação falhar, a operação de ROLLBACK gera a emissão de uma
excepção, sinalizando comportamento Bizantino.
4.6 Lidar com concorrência no sistema
A semântica SI apresentada e assumida em capítulos anteriores, tem cariz optimista. Na versão apre-
sentada os conflitos são apenas resolvidos no momento de commit, com recurso à regra first commiter
27
wins. No caso em que as bases de dados usadas na solução implementam esta versão do Snap-
shot Isolation, o desenho apresentado até ao momento é suficiente para manter no sistema o nível
de isolamento que pretendemos. Para fundamentar esta afirmação, consideremos duas transacções
especulativas T1, T2 conflictuosas entre si, e que se executam respectivamente em R1 e R2. Se por
exemplo T1 emitir uma operação de commit, esta operação executar-se-ia em todas as réplicas de uma
forma totalmente ordenada, ou seja, garantidamente antes de qualquer operação de T2. O controlo de
concorrência de cada base de dados iria aceitar e serializar T1. Quando T2 emitir o seu commit um con-
flito será detectado por todas as bases de dados, e a transacção abortada; é uma aplicação da regra
first commiter wins. Este exemplo é válido para um qualquer número de transacções especulativas e
conflituosas, que se executam em réplicas diferentes, com a particularidade de que apenas a primeira
será aceite, todas as restantes serão abortadas.
Na realidade, nem todas as bases de dados que fornecem Snapshot Isolation implementam uma
semântica totalmente optimista. É usual sistemas transaccionais implementarem uma versão do Snap-
shot Isolation em que os conflitos são resolvidos de uma forma distinta daquela apresentada por nós
em cima. Estes sistemas ao invés de detectarem os conflitos simplesmente no momento de commit
(first commiter wins), fazem com que as transacções que modificam um registo o façam em exclusão
mútua, por exemplo com recurso a um lock (controlo de concorrência pessimista). Esta forma de re-
solver conflitos no SI é apelidada de first updater wins, e não inibe a principal qualidade da semântica,
que é o facto de as transacções de leitura se efectuarem sem qualquer bloqueio. Para além disso, esta
alteração fornece garantias adicionais, na medida em que as transacções deixam de ser notificadas
da existência de conflitos apenas no momento de commit. Ao tentar alterar um registo que está a ser
escrito concorrentemente, a transacção é mantida em espera até que a transacção que detém o dire-
ito de acesso efectue commit ou abort. No caso de a transacção com prioridade efectuar commit, a
transacção conflituosa é desde logo sinalizada e obrigada a abortar o seu trabalho. No caso em que
a transacção prioritária aborta, a transacção que fora posta em espera pode retomar a sua execução.
Este tipo de interpretação do SI tem potencial para fazer diminuir o atraso com que as transacções
tomam conhecimento de um conflito; esta detecção é feita algures durante a vida útil da transacção, ao
invés de relegada apenas para o momento do seu commit.
4.6.1 Controlo de concorrência com first updater wins
Como referido em cima, muitos sistemas utilizam a regra de first updater wins para resolução de confli-
tos sob o SI. Todas as restantes propriedades da semântica mantêm-se, com excepção desta particu-
laridade na forma como conflitos são detectados. Esta forma de implementar o SI abre no entanto um
problema no desenho da nossa solução. Para resolver este problema implementámos um controlo de
concorrência que funciona de uma forma independente em relação ao controlo de concorrência imple-
mentado pelas bases de dados. O problema e a sua solução são descritos nas secções seguintes.
28
4.6.1.1 O problema
Assumindo a versão do SI que utiliza a regra de first updates wins, considerem-se duas transac