57
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

Bizantium - ULisboa · 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

  • 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