Skip to content

Commit

Permalink
[SD] Add "Salvaguarda distribuída" section (#1000)
Browse files Browse the repository at this point in the history
Co-authored-by: fmata97 <[email protected]>
  • Loading branch information
nunogoncalves03 and fmata97 authored Feb 28, 2024
1 parent cd29962 commit 19db57a
Show file tree
Hide file tree
Showing 7 changed files with 346 additions and 1 deletion.
221 changes: 220 additions & 1 deletion content/sd/0003-tempo-e-sincronizacao.md
Original file line number Diff line number Diff line change
Expand Up @@ -655,10 +655,227 @@ com o custo adicional de processamento para reconstruir os vetores.

:::

## Salvaguarda distribuída

Criar uma salvaguarda distribuída (ou _distribuded snapshot_) consiste
essencialmente em capturar o estado global do sistema num determinado instante,
o que pode ser útil porque permite:

- recuperar o sistema em caso de falha (_rollback_)
- analisar o sistema de forma a verificar certas propriedades
- depurar o sistema de forma geral

Guardar o estado de **todo** o sistema de forma coerente é um grande desafio,
já que não existe o conceito de "tempo global" (os relógios não estão sincronizados)
e o estado das aplicações consiste não só no estado dos seus processos, mas também
nas mensagens em trânsito.

### Abordagem ingénua

A primeira abordagem que nos pode surgir é existir um coordenador central
que pode ordenar aos processos que:

- Parem por completo o que estão a fazer
- Criem um _snapshot_ do seu estado atual
- Esperem por uma notificação do coordenador para retomar atividade (quando o estado
de todo o sistema estiver capturado)

O problema óbvio desta abordagem é que parar todo o sistema é ineficiente, pelo que
iremos procurar guardar o estado do sistema **sem o parar**.

### Corte coerente

Antes de falarmos de algoritmos que não precisam de parar o sistema, é preciso definir
a noção de corte coerente:

:::info[Corte coerente]

Um corte é coerente se, para cada evento que este contém, também inclui todos
os eventos que [_**aconteceram-antes**_](#eventos-e-relógios-lógicos) desse evento.

:::

Um evento corresponde a uma ação interna do processo ou ao envio/receção de uma
mensagem nos canais de comunicação. Note que o estado destes canais é relevante:
se descobrirmos que o processo $p_i$ registou o envio de uma mensagem $m$ para o
processo $p_j$ $(i \neq j)$, então, examinando se $p_j$ recebeu $m$, podemos
inferir se $m$ faz ou não parte do estado do canal entre $p_i$ e $p_j$.

![Tipos de cortes: (fortemente) coerente vs incoerente](./assets/0003-cut-types.svg#dark=3)

O corte mais à direita é **incoerente** porque $p_2$ incluiu a receção de $m_5$
mas $p_1$ não incluiu o envio desta mensagem, ou seja, este corte apresenta um
"efeito" sem uma "causa". É fácil perceber que a execução real do
sistema nunca esteve neste estado, visto que é impossível receber uma mensagem
que não foi enviada.

Em contraste, o corte do meio inclui o envio de $m_4$ mas não inclui a receção
desta mensagem. Ainda assim, este corte é **coerente** com a execução real do
sistema visto que as mensagens demoram algum tempo a chegar ao destinatário.

Por fim, o corte mais à esquerda é **fortemente coerente** porque todas as
causalidades estão representadas nos estados locais dos processos (não existem
mensagens em trânsito).

### Algoritmo simples

Consideremos um algoritmo em que:

- qualquer processo pode a qualquer momento iniciar uma salvaguarda global, guardando
o seu estado local
- quando o processo $p_i$ acaba de guardar o seu estado:
- envia um _marker_ a todos os outros processos e de seguida prossegue com a sua
execução normal
- quando o processo $p_i$ recebe um _marker_:
- se ainda não guardou o seu estado local, guarda-o

:::info[Exercício]

> Inicialmente, cada processo possui os tokens indicados na imagem.
> Sabendo que cada mensagem transfere 100 tokens entre 2 processos, qual vai ser o
> estado capturado pelo algoritmo considerando que P1 inicia um _snapshot_ no instante X?
![Diagrama do exercício](./assets/0003-snapshot-exercise.svg#dark=3)
<br>
**R:** $P1 = 800, ~P2 = 300, ~P3 = 800$

Justificação:

- P1: Enviou 2 mensagens, pelo que perdeu $2*100 = 200$ tokens
- P2: Não enviou nem recebeu mensagens, pelo que o número de tokens não se altera
- P3: Recebeu 2 mensagem de P1 e enviou 1 de volta, pelo que ganhou $100$ tokens

**IMPORTANTE**: A soma de tokens no início era 2000 mas no _snapshot_ é 1900!
Não se sabe que mensagens estão em trânsito...

:::

Este algoritmo apresenta uma falha, que é **não garantir a coerência do corte**:

![Corte incoerente](./assets/0003-inconsistent-cut.svg#dark=3)

O corte ilustrado (que pode ser entendido como um _snapshot_ iniciado por P1) é
incoerente (a soma de tokens é 2100).

É importante capturar também o estado dos canais de comunicação entre os processos!

### Algoritmo de Chandy-Lamport

Este algoritmo estende o anterior de forma a capturar também o estado dos canais.
Pressupõe que:

- não há falhas nos processos nem nos canais (ou seja, que são fiáveis)
- os canais são unidirecionais com uma implementação FIFO
- o grafo de processos e canais é fortemente ligado (isto é, existe um caminho
entre quaisquer dois processos)
- qualquer processo pode iniciar uma _snapshot_ global a qualquer momento
- os processos podem continuar a sua execução normal (e enviar/receber
mensagens) enquanto a snapshot está a decorrer

A única diferença do algoritmo anterior é que **o estado do canal também é guardado**:

:::info[Estado dos canais]

Cada processo guarda o seu estado local e, para cada canal de receção, guarda
também o conjunto de mensagens recebidas após o snapshot. Desta forma, o processo
regista quaisquer mensagens que tenham chegado depois de ter guardado o seu estado
e antes do remetente ter registado o seu estado.

:::

::::details[Pseudocódigo do algoritmo]
<br>

```php
Marker receiving rule for process p_i
On receipt of a marker message at p_i over channel c:
if (p_i has not yet recorded its state) it
records its process state now;
records the state of c as the empty set;
turns on recording of messages arriving over other incoming channels;
else
records the state of c as the set of messages it has received over c since it saved its state.
end if

Marker sending rule for process p_i
After p_i has recorded its state, for each outgoing channel c:
p_i sends one marker message over c
(before it sends any other message over c).
```

:::tip[Nota]

Qualquer processo pode iniciar o algoritmo a qualquer momento. Este age como se
tivesse recebido um _marker_ (através de um canal inexistente) e segue a regra
de receção de _markers_.

:::

::::

Neste algoritmo:

- nenhum processo decide quando termina
- todos os processos vão receber um _marker_ e portanto guardar o seu estado
- todos os processos receberam um _marker_ nos $N-1$ canais de receção e guardaram
os seus estados
- existe a possibilidade de um servidor central obter todos os estados locais
coerentes para construir um _snapshot_ global

:::info[Exercício]

> Inicialmente, cada processo possui os _tokens_ indicados na imagem.
> Sabendo que cada mensagem transfere 100 _tokens_ entre 2 processos, qual vai ser o
> estado capturado pelo algoritmo Chandy-Lamport considerando que P1 inicia um
> _snapshot_ no instante X?
![Diagrama do exercício](./assets/0003-snapshot-exercise.svg#dark=3)
<br>
**R:** $P1 = 800, ~P2 = 300, ~P3 = 800, ~Canal ~ P3-P1~(C31) = 100$

Justificação:

- P1: Enviou 2 mensagens, pelo que perdeu $2*100 = 200$ _tokens_
- P2: Não enviou nem recebeu mensagens, pelo que o número de _tokens_ não se altera
- P3: Recebeu 2 mensagem de P1 e enviou 1 de volta, pelo que ganhou $100$ _tokens_
- C31: Desde que P1 enviou o primeiro _marker_ até que recebeu de volta um de P3,
recebeu nesse canal 1 mensagem, ou seja, $100$ _tokens_ estavam a ser
transferidos no canal

**IMPORTANTE**: A soma de _tokens_ já se mantém a 2000! Ao escutar os canais,
conseguimos saber que existe uma mensagem em trânsito de P3 para P1.

:::

:::tip[Nota]

Mas será que um corte coerente garante que capturamos o estado global do sistema
num determinado instante?

![Snapshot vs sistema real](./assets/0003-snapshot-vs-system.svg#dark=3)

A verde está representado o estado real do sistema em cada fase e à direita temos
o que foi capturado pelo _snapshot_ : **apesar de coerente, o sistema nunca esteve
naquele estado...**

Podemos verificar este acontecimento com o auxílio de _vector clocks_:

![Snapshot vs sistema real vector clocks](./assets/0003-snapshot-vs-system-vector-clocks.svg#dark=3)

É possível observar que tanto $e_1$ é concorrente com $e_2$ como $e_3$ é com $e_4$,
pelo que o algoritmo não sabe em que ordem é que estes eventos aconteceram, já
que os relógios de cada processo não estão sincronizados. O que o algoritmo consegue
capturar é que o estado inicial é $(100, 100)$, após $e_1$ e $e_2$ é $(80, 40)$ e
por fim é $(140, 60)$.

![Snapshot vs sistema real vector clocks](./assets/0003-snapshot-vs-system-evolution.svg#dark=3)

:::

## Referências

- Coulouris et al - Distributed Systems: Concepts and Design (5th Edition)
- Secções 14.1, 14.2, 14.3 e 14.4
- Secções 14.1, 14.2, 14.3, 14.4 e 14.5
- Coulouris et al - Distributed Systems: Concepts and Design (5th Edition) - Instructor's Manual
- Soluções dos exercícios 14.10, 14.11, 14.12 e 14.13
- van Steen and Tanenbaum - [Distributed Systems](https://www.distributed-systems.net/index.php/books/ds4/)
Expand All @@ -667,3 +884,5 @@ com o custo adicional de processamento para reconstruir os vetores.
- 3a Fundamentos: Tempo
- Paul Krzyzanowski - [Assigning Lamport & Vector Timestamps](https://people.cs.rutgers.edu/~pxk/417/notes/clocks/index.html)
- Imagens dos exemplos de eventos e relógios lógicos
- Departamento de Engenharia Informática - Slides de Sistemas Distribuídos (2023/2024)
- SlidesTagus-Aula03b
21 changes: 21 additions & 0 deletions content/sd/assets/0003-cut-types.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 19db57a

Please sign in to comment.