Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SD] Overall improvements and cleanup #1005

Merged
merged 3 commits into from
Mar 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 93 additions & 37 deletions content/sd/0002-apis-de-comunicacao.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,77 +18,117 @@ type: content

## Representação externa de dados e _marshalling_

A informação armazenada em programas é representada com diversas estruturas de dados, enquanto que a informação transmitida em mensagens consiste numa sequência de _bytes_. Independentemente da forma de comunicação utilizada, as estruturas de dados devem ser _flattened_ (convertidas numa sequência de bytes) antes da transmissão e reconstruídas na chegada.
A informação armazenada em programas é representada com diversas estruturas de
dados, enquanto que a informação transmitida em mensagens consiste numa sequência
de _bytes_. Independentemente da forma de comunicação utilizada, as estruturas de
dados devem ser _flattened_ (convertidas numa sequência de bytes) antes da
transmissão e reconstruídas na chegada.

Visto que nem todos os computadores armazenam os valores primitivos da mesma forma, um dos seguintes métodos pode ser utilizado para permitir que dois computadores troquem dados binários:
Visto que nem todos os computadores armazenam os valores primitivos da mesma forma,
um dos seguintes métodos pode ser utilizado para permitir que dois computadores
troquem dados binários:

- Os valores são convertidos para um formato externo acordado antes da transmissão e convertidos para o formato local ao serem recebidos
- Os valores são transmitidos no formato do remetente, juntamente com uma indicação do formato utilizado, e o destinatário converte os valores se necessário
- Os valores são convertidos para um formato externo acordado antes da transmissão
e convertidos para o formato local ao serem recebidos
- Os valores são transmitidos no formato do remetente, juntamente com uma indicação
do formato utilizado, e o destinatário converte os valores se necessário

## Protocolos _request-reply_

Esta forma de comunicação foi concebida para suportar as _roles_ e trocas de mensagens das interações típicas cliente-servidor. Normalmente, a comunicação _request-reply_ é síncrona porque o processo cliente bloqueia até receber resposta do servidor. A comunicação assíncrona pode ser útil em situações em que os clientes podem receber as respostas mais tarde.
Esta forma de comunicação foi concebida para suportar as _roles_ e trocas de
mensagens das interações típicas cliente-servidor. Normalmente, a comunicação
_request-reply_ é síncrona porque o processo cliente bloqueia até receber resposta
do servidor. A comunicação assíncrona pode ser útil em situações em que os clientes
podem receber as respostas mais tarde.

Embora muitas implementações atuais usem [TCP](/rc/transporte/#tcp---transmission-control-protocol), um protocolo baseado em [UDP](/rc/transporte/#udp---user-datagram-protocol) evita _overheads_ desnecessários, em particular:
Embora muitas implementações atuais usem
[TCP](/rc/transporte/#tcp---transmission-control-protocol), um protocolo baseado em
[UDP](/rc/transporte/#udp---user-datagram-protocol) evita _overheads_ desnecessários,
em particular:

- Os _acknowledgements_ são redundantes, uma vez que os pedidos são seguidos por respostas
- Estabelecer uma conexão envolve dois pares extra de mensagens para além do par necessário (pedido e resposta)
- O controlo de fluxo é redundante para a maioria das invocações, visto que usam argumentos e resultados pequenos
- Os _acknowledgements_ são redundantes, uma vez que os pedidos são seguidos por
respostas
- Estabelecer uma conexão envolve dois pares extra de mensagens para além do par
necessário (pedido e resposta)
- O controlo de fluxo é redundante para a maioria das invocações, visto que usam
argumentos e resultados pequenos

:::tip[Nota]

Muitas vezes é difícil decidir o tamanho apropriado para o _buffer_ no qual vamos receber os datagramas (isto aplica-se tanto do lado do cliente como do servidor). O tamanho limitado dos datagramas pode não ser adequado para sistemas [RMI](https://pt.wikipedia.org/wiki/RMI) ou [RPC](/sd/apis-de-comunicacao/#rpc), uma vez que os argumentos e resultados dos _procedures_ podem ter um tamanho arbitrário.
Muitas vezes é difícil decidir o tamanho apropriado para o _buffer_ no qual vamos
receber os datagramas (isto aplica-se tanto do lado do cliente como do servidor).
O tamanho limitado dos datagramas pode não ser adequado para sistemas
[RMI](https://pt.wikipedia.org/wiki/RMI) ou [RPC](#rpc), uma vez que os argumentos e
resultados dos _procedures_ podem ter um tamanho arbitrário.

:::

### Exemplo de aplicação UDP

Vamos analisar o comportamento de uma aplicação cliente-servidor que utiliza UDP para a troca de mensagens.
Vamos analisar o comportamento de uma aplicação cliente-servidor que utiliza UDP
para a troca de mensagens.
Ao utilizar UDP, sabemos que as mensagens podem:

- Perder-se
- Chegar fora de ordem
- Chegar repetidas

Imaginemos um caso em que o cliente não recebe resposta ao seu pedido. Tem agora 2 opções:
ou **retorna erro** ou **repete o envio**.
Imaginemos um caso em que o cliente não recebe resposta ao seu pedido.
Tem agora 2 opções: ou **retorna erro** ou **repete o envio**.

Se chegar uma resposta depois do reenvio, pode ter acontecido 1 de 3 cenários:

- O pedido original não chegou ao servidor (operação executada apenas 1 vez)
- A resposta ao pedido original perdeu-se (operação executada 2 vezes)
- O servidor recebeu o pedido original e começou a processá-lo, mas _crashou_, podendo a operação ter sido realizada ou não caso não seja atómica (operação executada 1 ou 2 vezes)
- O servidor recebeu o pedido original e começou a processá-lo, mas _crashou_,
podendo a operação ter sido realizada ou não caso não seja atómica (operação
executada 1 ou 2 vezes)

O facto de a operação ser repetida, além de desperdiçar tempo, pode ter efeitos inesperados caso esta não seja **idempotente** (operação que produz o mesmo estado e devolve a mesma resposta independentemente do número de vezes que for executada).
O facto de a operação ser repetida, além de desperdiçar tempo, pode ter efeitos
inesperados caso esta não seja **idempotente** (operação que produz o mesmo estado
e devolve a mesma resposta independentemente do número de vezes que for executada).

Como é que o servidor pode evitar isto? Tem que ser capaz de verificar se o _id_ do pedido já foi recebido anteriormente:
Como é que o servidor pode evitar isto? Tem que ser capaz de verificar se o _id_
do pedido já foi recebido anteriormente:

- Se for a primeira vez: executa a operação normalmente
- Se for repetido: deve ter guardado o histórico de respostas a pedidos executados e retornar a resposta correspondente (isto implica o armazenamento de _ids_ e suas respostas)
- Se for repetido: deve ter guardado o histórico de respostas a pedidos executados
e retornar a resposta correspondente (isto implica o armazenamento de _ids_ e suas
respostas)

Conseguimos assim perceber que programar sistemas distribuídos através de _sockets_ é bastante complexo e vulnerável a falhas, pelo que iremos utilizar mais um nível de abstração, o RPC.
Conseguimos assim perceber que programar sistemas distribuídos através de _sockets_
é bastante complexo e vulnerável a falhas, pelo que iremos utilizar mais um nível de
abstração, o RPC.

## RPC

O **RPC** (_Remote Procedure Call_) permite que um cliente execute funções (_procedures_) remotamente num servidor
mas de forma aparentemente local. O facto da execução ser remota levanta dois problemas principais:
O **RPC** (_Remote Procedure Call_) permite que um cliente execute funções
(_procedures_) remotamente num servidor mas de forma aparentemente local.
O facto da execução ser remota levanta dois problemas principais:

1. A rede entre o cliente e o servidor não é _reliable_, tendo tendência a perder e reordenar mensagens
2. Os computadores onde ambos os processos correm podem ter arquiteturas muito diferentes
1. A rede entre o cliente e o servidor não é _reliable_, tendo tendência a perder
e reordenar mensagens
2. Os computadores onde ambos os processos correm podem ter arquiteturas muito
diferentes

Programação típica com RPC:
![Programação típica com RPC](./assets/0002-rpc-programming.svg#dark=3)

As **Interface definition languages** (IDLs) foram concebidas para permitir que os procedimentos sejam invocados noutras linguagens de programação. Uma IDL fornece uma notação para definir interfaces e parâmetros de operações (input e output).
As **Interface definition languages** (IDLs) foram concebidas para permitir que
os procedimentos sejam invocados noutras linguagens de programação. Uma IDL
fornece uma notação para definir interfaces e parâmetros de operações (input e output).

Este processo segue um mecânismo de _request reply_:

1. O cliente passa os argumentos do procedure para o _local stub_, que os transforma num _request_ (mantendo assim a ilusão de que se está a chamar um procedure local)
1. O cliente passa os argumentos do procedure para o _local stub_, que os transforma
num _request_ (mantendo assim a ilusão de que se está a chamar um procedure local)
2. O _stub_ invoca o protocolo RPC para enviar este request para o servidor
3. O servidor recebe o request, que é traduzido pelo seu _stub_ de forma a obter os argumentos (utilizando o mesmo protocolo)
3. O servidor recebe o request, que é traduzido pelo seu _stub_ de forma a obter os
argumentos (utilizando o mesmo protocolo)
4. Executa localmente o procedure
5. Devolve uma resposta ao cliente de acordo com o protocolo
6. O _stub_ do cliente traduz a resposta e obtém assim o valor, que devolve ao programa que chamou o procedure
6. O _stub_ do cliente traduz a resposta e obtém assim o valor, que devolve ao
programa que chamou o procedure

![Implementação do RPC](./assets/0002-rpc-implementation.png#dark=3)

Expand All @@ -101,23 +141,35 @@ Existem várias semânticas de invocação (e mecanismos associados):
- _**At-most-once**_ : O procedure é executado no máximo 1 vez
- mecanismos: RPC timeout + Resend + **Message id + Response history**
- _**Exactly-once**_ : O procedure é executado exatamente 1 vez
- mecanismos: RPC timeout + Resend + Message id + Response history + **Transaction rollback**
- mecanismos: RPC timeout + Resend + Message id + Response history + **Transaction
rollback**

:::tip[Transparência]

Os criadores do RPC queriam tornar as chamadas de procedimento remoto idênticas às chamadas locais (sem distinção na sintaxe). No entanto, as chamadas de procedimento remoto são mais suscetíveis a falhas, devido à presença de uma rede, outro computador e outro processo. Isto exige transparência para que os clientes sejam capazes de recuperar caso haja falhas.
Os criadores do RPC queriam tornar as chamadas de procedimento remoto idênticas às
chamadas locais (sem distinção na sintaxe). No entanto, as chamadas de procedimento
remoto são mais suscetíveis a falhas, devido à presença de uma rede, outro computador
e outro processo. Isto exige transparência para que os clientes sejam capazes de
recuperar caso haja falhas.

:::

A implementação que iremos utilizar na cadeira é o **gRPC**.

## gRPC

Tem como base [HTTP/2](/rc/aplicacao/#http-20) e por sua vez [TLS](https://en.wikipedia.org/wiki/Transport_Layer_Security) e [TCP](/rc/transporte/#tcp---transmission-control-protocol). É um mecanismo focado em serviços cloud (e não no paradigma cliente/servidor) baseado no RPC utilizado internamente pela Google.

- Utiliza uma **IDL** (_Interface Definition Language_) para especificar os tipos dos dados e quais as operações disponíveis.
- Um exemplo de IDL é o [**protobuf**](https://protobuf.dev/) utilizado pela Google, que fornece o seu compilador **protoc** para gerar código (por ex. Java)
- Fornece ferramentas de **geração de código a partir da IDL** (conversão de dados + invocação remota)
Tem como base [HTTP/2](/rc/aplicacao/#http-20) e por sua vez
[TLS](https://en.wikipedia.org/wiki/Transport_Layer_Security) e
[TCP](/rc/transporte/#tcp---transmission-control-protocol). É um mecanismo focado
em serviços cloud (e não no paradigma cliente/servidor) baseado no RPC utilizado
internamente pela Google.

- Utiliza uma **IDL** (_Interface Definition Language_) para especificar os tipos
dos dados e quais as operações disponíveis.
- Um exemplo de IDL é o [**protobuf**](https://protobuf.dev/) utilizado pela Google,
que fornece o seu compilador **protoc** para gerar código (por ex. Java)
- Fornece ferramentas de **geração de código a partir da IDL** (conversão de dados +
invocação remota)
- As chamadas remotas podem ser **síncronas ou assíncronas**

Uma chamada remota gRPC:
Expand All @@ -131,20 +183,24 @@ Uma chamada remota gRPC:
- Meta-dados opcionais
- Um _trailer_ (indica se a chamada foi bem sucedida ou se ocorreu um erro)

Dado que estamos a utilizar a rede, podem acontecer falhas (o que deve ser tido em conta ao escrever os clientes)
e portanto podem ser devolvidas respostas com códigos de resultado em vez do tipo definido na IDL. Alguns exemplos:
Dado que estamos a utilizar a rede, podem acontecer falhas (o que deve ser tido em
conta ao escrever os clientes) e portanto podem ser devolvidas respostas com códigos
de resultado em vez do tipo definido na IDL. Alguns exemplos:

- **0** - OK
- **5** - not found
- **13** - internal

De forma a saber como contactar o servidor, o **gRPC** utiliza por omissão [DNS](/rc/aplicacao/#dns---domain-name-system) como servidor de nomes.
De forma a saber como contactar o servidor, o **gRPC** utiliza por omissão
[DNS](/rc/aplicacao/#dns---domain-name-system) como servidor de nomes.

## Referências

- Coulouris et al - Distributed Systems: Concepts and Design (5th Edition)
- Secções 4.2 e 5.1-5.3
- Larry Peterson and Bruce Davie - [Computer Networks: A Systems Approach](https://github.com/SystemsApproach/book) © Elsevier 2012 (License [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/))
- Larry Peterson and Bruce Davie -
[Computer Networks: A Systems Approach](https://github.com/SystemsApproach/book)
© Elsevier 2012 (License [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/))
- Secção 5.3
- Imagem que ilustra funcionamento do RPC
- Departamento de Engenharia Informática - Slides de Sistemas Distribuídos (2022/2023)
Expand Down
2 changes: 1 addition & 1 deletion content/sd/0003-tempo-e-sincronizacao.md
Original file line number Diff line number Diff line change
Expand Up @@ -657,7 +657,7 @@ com o custo adicional de processamento para reconstruir os vetores.

## Salvaguarda distribuída

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

Expand Down
6 changes: 3 additions & 3 deletions content/sd/0004-coordenacao-e-consenso.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ type: content

## Exclusão mútua distribuída

Já fomos introduzido ao problema da exclusão mútua em
Já fomos introduzidos ao problema da exclusão mútua em
[**Sistemas Operativos (SO)**](/so/programação/), que tem como objetivo garantir
que dois processos/_threads_ não acedem concorrentemente ao mesmo recurso
(ex: ficheiro, bloco de memória, ...), uma vez que tal pode causar incoerências
Expand Down Expand Up @@ -170,7 +170,7 @@ Este algoritmo associa um **_voting set_** $V_i$ (também chamados **quóruns**)
cada processo $p_i$ $(i = 1,2,...,N)$, onde $V_i \subseteq \{p_1, p_2, ..., p_N\}$.
Os _sets_ $V_i$ são escolhidos de forma a que, para todo $i,j = 1,2,...,N$:

- $p_i \in V_i$ (atenção que um processo pode pertencer a mais que um _voting set_)
- $p_i \in V_i$
- $V_i \cap V_j \neq \varnothing$ – há pelo menos um membro comum entre quaisquer
dois _voting sets_
- $|V_i| = K$ – de forma a ser justo, todos os _voting sets_ têm o mesmo tamanho
Expand Down Expand Up @@ -296,7 +296,7 @@ Tolerância a falhas:

Tal como vimos anteriormente, muitos algoritmos distribuídos precisam de atribuir
cargos especiais a certos processos. Por exemplo, na variante
["servidor central"](/sd/coordenacao-e-consenso/#algoritmo-do-servidor-central)
["servidor central"](#algoritmo-do-servidor-central)
dos algoritmos para exclusão mútua, o servidor é escolhido entre os processos que
precisam de utilizar a secção crítica. É necessário um **algoritmo de eleição**
para esta escolha, sendo essencial que todos os processos concordem com a mesma.
Expand Down
Binary file modified content/sd/assets/0004-central-server.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified content/sd/assets/0004-ricart-and-agrawala.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified content/sd/assets/0004-ring-based.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified content/sd/assets/0004-ring-election-example.png
100755 → 100644
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading