Edit page

Tempo e Sincronização

Tempo

Num sistema centralizado não existe ambiguidade quanto ao tempo. Quando um processo A pretende saber o tempo atual, pode consultar o relógio do sistema. Se momentos mais tarde o processo B obtiver o tempo, este será superior (ou possivelmente igual) ao tempo obtido pelo processo A.

Num sistema distribuído o problema é mais complexo. Não existe tempo global, cada computador tem o seu próprio relógio e o tempo que estes relógios indicam pode ser diferente.

Relógios Físicos

Praticamente todos os computadores têm um circuito para contar o tempo, baseados num cristal de quartzo. Este cristal oscila a uma frequência conhecida, e o circuito conta o número de oscilações, podendo assim determinar o tempo. Ao fim de um determinado número de oscilações, o circuito gera uma interrupção a que se chama tick de relógio.

Como qualquer outra medição, existe uma incerteza associada à frequência do cristal, e por isso a frequência de ticks de relógio pode variar entre relógios. Mesmo para o mesmo relógio, a frequência pode variar com fatores externos como a temperatura. A variação é normalmente pequena, mas o erro acumulado pode levar a que dois relógios apresentem uma diferença significativa no tempo que contam.

Tempo Universal Coordenado (UTC)

Dada esta incerteza, surge a necessidade de um tempo universal, que sirva de referência para todos os relógios. Para tal, o International Time Bureau (BIH) recolhe medições de 450 relógios atómicos (capazes de uma precisão extremamente superior à dos relógios de quartzo) em 80 laboratórios distintos e calcula o tempo médio. Esta medição é o Tempo Atómico Internacional (TAI).

Por mais preciso que o TAI seja, não tem em conta o desvio da hora solar, pelo que um segundo standard foi criado, baseado no TAI mas incluindo leap seconds, o Tempo Universal Coordenado (UTC). Vários satélites transmitem UTC, sendo possível obter precisões na ordem dos nanosegundos.

Métricas de Desempenho de Relógios

Com uma referência externa, pode-se definir métricas para avaliar o desempenho de relógios. Para as definir, temos que tt é o tempo UTC e Cp(t)C_p(t) é o tempo contado pelo relógio pp no instante tt.

O skew é a diferença entre o tempo contado por dois relógios, ou seja:

skew(p,q)=Cp(t)Cq(t)skew(p,q) = C_p(t) - C_q(t)

A deriva do relógio é a taxa a que um dado relógio se afasta do tempo de referência:

drift_rate(p)=dCp(t)dtdrift\_rate(p) = \frac{dC_p(t)}{dt}

Para um relógio perfeito, drift_rate(p)=1drift\_rate(p) = 1. Por norma, fabricantes de relógios garantem um desvio máximo. Ou seja, para um desvio máximo ρ\rho:

1ρdrift_rate(p)1+ρ1-\rho \leq drift\_rate(p) \leq 1+\rho

Diz-se que um relógio está correto quando o seu drift rate respeita a especificação do fabricante. Esta condição impede saltos no tempo, porém, tal nem sempre é desejável. Uma condição mais flexível é a de monotonia, que impede que o relógio retroceda no tempo:

t>tCp(t)>Cp(t)t' > t \Rightarrow C_p(t') > C_p(t)

Nota

Garantir a monotonia de um relógio é fundamental para o correto funcionamento de programas que se baseiam no tempo. Por exemplo, a ferramenta make do UNIX é utilizada para compilar apenas os ficheiros que foram modificados desde a última compilação. Se o tempo retroceder entre compilações, a ferramenta não funciona como desejado.

Para conjuntos de relógios, pode-se ainda falar de precisão e exatidão.

Precisão e Exatidão

A precisão é a medida da dispersão das medições dos vários relógios. Para uma precisão π\pi, temos que a diferença entre quaisquer dois relógios nunca excede π\pi, ou seja:

p,q:skew(p,q)π\forall_{p,q}: \lvert skew(p,q) \rvert \leq \pi

Por outro lado, a exatidão é a medida da diferença do conjunto de relógios com uma referência externa. Para uma exatidão α\alpha, temos que a diferença entre qualquer relógio e o tempo de referência nunca excede α\alpha, ou seja:

p:Cp(t)tα\forall_{p}: \lvert C_p(t) - t \rvert \leq \alpha

Sincronização de Relógios Físicos

Num sistema distribuído em que uma das máquinas está instalada com um recetor de UTC, pode-se usar este relógio como referência para sincronizar os outros. Trata-se de sincronização externa, em que é usada uma referência externa para obter exatidão e precisão no sistema.

Porém, a exatidão nem sempre é relevante, sendo mais importante a precisão para que todos os nós do sistema concordem sobre o tempo. Nestes casos usa-se sincronização interna, em que os relógios são sincronizados entre si.

Algoritmo de Cristian

O Algoritmo de Cristian é um algoritmo de sincronização externa que usa um servidor de tempo.

O processo pp envia uma mensagem m1m_1 ao servidor de tempo SS e espera pela resposta m2m_2, que inclui CS(tS,m2)C_S(t_{S,m_2}), com tS,m2t_{S,m_2} sendo o momento em que a resposta m2m_2 sai de SS.

sequenceDiagram p->>Servidor de tempo: m₁: What is the time? Servidor de tempo->>p: m₂: My clock says this.

O processo pp não pode usar o tempo incluído na mensagem m2m_2, pois estaria a desprezar o tempo de transmissão. Assim, pp mede o RTTRTT e, assumindo que o tempo de transmissão de pp para SS é igual ao de SS para pp, estima o tempo atual como:

Cp(t)CS(tS,m2)+RTT2C_p(t) \leftarrow C_S(t_{S,m_2}) + \frac{RTT}{2}

Caso o ajuste de tempo requira um avanço no tempo, ocorre diretamente. No entanto, se for necessário um retrocesso, de modo a não se violar a monotonia com um salto para trás, a frequência de clock ticks é reduzida, até o tempo estimado ser alcançado.

É importanto notar que não é garantido simetria no tempo de transmissão, pelo que a estimativa do tempo tem alguma incerteza. No pior caso, em que uma das mensagens é enviada instantaneamente mas a resposta demora RTTRTT, o tempo estimado estará errado por RTT2\frac{RTT}{2}. A precisão deste algoritmo é, portanto, RTT2\frac{RTT}{2}.

Pode-se melhorar a precisão caso se conheça o tempo mínimo de transmissão, ficando RTTRTTmin2\frac{RTT - RTT_{min}}{2}.

Este algoritmo é simples, no entanto, tem um único ponto de falha. Se o servidor de tempo falhar, o sistema não consegue sincronizar os relógios. Cristian propõe que o pedido seja feito em multicast a vários servidores de tempo, selecionando o que responder primeiro, resultando na melhor precisão.

Exercício

Um cliente pretende sincronizar-se com um servidor e para tal regista o RTT e os tempos enviados pelo servidor:

RTT (ms) Time (hh:mm:ss)
22 10:54:23.674
25 10:54:25.450
20 10:54:28.342

1. Com qual dos valores deve o servidor se sincronizar de modo a obter a melhor precisão?

R: Com o terceiro (10:54:28.342), pois apresenta o menor RTT (20 ms20~ms)

2. Qual o valor do relógio após o acerto?

R: C(t)=t+RTT2=C(t) = t + \frac{RTT}{2} = 10:54:28.342 +20ms2=+ \frac{20ms}{2} = 10:54:28.352

3. Qual a precisão do acerto?

R: ± 10 ms\pm~10~ms

4. E se soubermos que o tempo de envio de uma mensagem é no mínimo de 8ms, essa precisão é alterada? Se sim, qual o novo valor?

R: ± (RTTRTTmin2)=± (2028)=± 2 ms\pm~(\frac{RTT-RTT_{min}}{2}) = \pm~(\frac{20}{2}-8) = \pm~2~ms

Algoritmo de Berkeley

O Algoritmo de Berkeley é um algoritmo de sincronização interna. Por simplicidade, o exemplo será dado para um sistema com 3 processos, o processo coordenador CC e os processos pp e qq, mas o algoritmo pode ser estendido a qualquer número de processos.

Neste algoritmo, é eleito um coordenador CC, responsável por periodicamente enviar pedidos a todos os outros processos, que devem responder com o seu tempo.

sequenceDiagram participant p Coordenador->>p: m₁: What is the time? p->>Coordenador: m₂: My clock says this. Coordenador->>q: m₃: What is the time? q->>Coordenador: m₄: My clock says this. Coordenador->>p: m₅: Offset your clock by this. Coordenador->>q: m₆: Offset your clock by this.

O coordenador recebe as respostas m2m_2 e m4m_4 e calcula a média dos tempos, incluindo o próprio:

Tavg=Cp(tp,m2)+Cq(tq,m4)+CC(t)3T_{avg} = \frac{C_p(t_{p,m_2}) + C_q(t_{q,m_4}) + C_C(t)}{3}

O coordenador calcula a diferença entre o tempo médio e o tempo de cada processo e envia a diferença para que os processos ajustem o seu relógio. As diferenças podem ser positivas ou negativas e são calculadas da seguinte forma:

Δtp=TavgCp(tp,m2)\Delta t_{p} = T_{avg} - C_p(t_{p,m_2})
Δtq=TavgCq(tq,m4)\Delta t_{q} = T_{avg} - C_q(t_{q,m_4})
ΔtC=TavgCC(t)\Delta t_{C} = T_{avg} - C_C(t)

Cada processo ajusta o seu relógio, de acordo com a diferença recebida.

O algoritmo apresentado foi simplificado, na versão real é tido em conta o tempo de transmissão, da forma como foi feito no Algoritmo de Cristian. É ainda definido um valor máximo para o RTT (dependendo da exatidão desejada), permitindo ao coordenador eliminar ocasionais leituras que ultrapassem esse valor.

Em caso de falha do coordenador, um novo coordenador é eleito. Falar-se-á de eleições na próxima publicação.

Exercício

Existem 3 máquinas A, B e C, sendo o master A. A enviou a sua hora (13:15:15) a todos e recebeu as seguintes respostas:

Time (hh:mm:ss)
[ A = 13:15:15 ]
B = 13:15:05
C = 13:16:07

Qual é o acerto enviado pelo master a cada máquina?

Tavg=CB(tB,m)+CC(tC,m)+CA(t)3=13:15:29T_{avg} = \frac{C_B(t_{B,m}) + C_C(t_{C,m}) + C_A(t)}{3} = 13:15:29
ΔtB=TavgCB(tB,m)=+ 24s\Delta t_{B} = T_{avg} - C_B(t_{B,m}) = +~24s
ΔtC=TavgCC(tC,m)= 38s\Delta t_{C} = T_{avg} - C_C(t_{C,m}) = -~38s
ΔtA=TavgCA(t)=+ 14s\Delta t_{A} = T_{avg} - C_A(t) = +~14s

Network Time Protocol (NTP)

Tanto o Algoritmo de Cristian como o Algoritmo de Berkeley são algoritmos desenhados para operar em intranets. O NTP define um protocolo para distribuir informação de tempo através da Internet.

Os objetivos de desenho do NTP são:

  • Prestar um serviço que permita que os clientes na Internet sejam sincronizados com precisão com o UTC.
  • Prestar um serviço confiável que possa sobreviver a longos períodos de perda de conectividade.
  • Permitir que os clientes se resincronizem com frequência suficiente para compensar as taxas de desvio encontradas na maioria dos computadores.
  • Proteger contra interferências com o serviço de tempo, quer sejam intencionais ou acidentais.

O protocolo NTP baseia-se no Algoritmo de Cristian, mas em vez apenas medir o RTTRTT, registam-se os valores reportados por pp para o envio de m1m_1 e recepção de m2m_2, Cp(tp,m1)C_p(t_{p,m_1}) e Cp(tp,m2)C_p(t_{p,m_2}), e os valores reportados por qq para a recepção de m1m_1 e envio de m2m_2, Cq(tq,m1)C_q(t_{q,m_1}) e Cq(tq,m2)C_q(t_{q,m_2}). Desta forma, o tempo entre a chegada de uma mensagem e o envio da próxima não é contabilizado.

sequenceDiagram p->>q: m₁: When do you receive this message? q->>p: m₂: I got it then and am sending it now.

Calcula-se a diferença entre os tempos reportados entre o envio e recepção de cada mensagem:

δm1=Cq(tq,m1)Cp(tp,m1)\delta_{m_1} = C_q(t_{q,m_1}) - C_p(t_{p,m_1})
δm2=Cp(tp,m2)Cq(tq,m2)\delta_{m_2} = C_p(t_{p,m_2}) - C_q(t_{q,m_2})

Podemos, por fim, calcular a diferença dos deltas para obter o offset θ\theta e a média para obter o delay δ\delta:

θ=δm1δm22δ=δm1+δm22\theta = \frac{\delta_{m_1} - \delta_{m_2}}{2} \qquad \delta = \frac{\delta_{m_1} + \delta_{m_2}}{2}

O offset é uma estimativa do skew(p,q)skew(p,q) com incerteza de δ2\frac{\delta}{2}. Quanto menor o delay, melhor a estimativa. O algoritmo armazena os últimos 8 pares (θ,δ)(\theta, \delta), escolhendo de entre estes o par com o menor δ\delta de forma a obter o θ\theta mais preciso. Este valor é então usado para ajustar o relógio de pp:

Cp(t)Cp(t)+θC_p(t) \leftarrow C_p(t) + \theta

O protocolo NTP pode funcionar em 3 modos:

  • Multicast: O procedimento descrito anteriormente não se aplica e simplesmente é enviado o tempo atual em multicast para todos os clientes. Só se deve usar este modo em LANs de alta velocidade.
  • Chamada a procedimento: O protocolo decorre de acordo com o descrito anteriormente.
  • Simétrico: O protocolo decorre de acordo com o descrito anteriormente, mas a sincronização é feita em ambos os sentidos.

Aplicar NTP de forma simétrica implica que não só pp afeta o relógio de qq, como também qq afeta o relógio de pp. Isto pode causar problemas, caso um dos relógios seja mais exato que o outro. Para resolver este problema, o NTP divide as máquinas em níveis (ou strata):

  • Uma máquina de nível 1 é um servidor com um relógio de referência, como é o caso de um computador instalado com um receptor de UTC.
  • Uma máquina de nível 2 sincroniza com uma máquina de nível 1.
  • Uma máquina de nível 3 sincroniza com uma máquina de nível 2, etc...

Uma máquina só ajusta o seu relógio com uma máquina de nível inferior.

Eventos e Relógios Lógicos

Nem sempre é necessário contar o tempo de forma exata. Se dois processos não interagem, a falta de sincronização não pode ser observada, pelo que não poderá causar problemas. Para além disso, em muitos casos, o que realmente é relevante é a ordem em que eventos ocorrem e não o tempo absoluto.

Trata-se de um evento qualquer operação que transforma o estado do processo. Para além de alterações de estado, a recepção recv(m)recv(m) e o envio send(m)send(m) de qualquer mensagem mm também são eventos.

Num só processo pip_i, é trivial determinar a ordem em que eventos ocorrem. Diz-se que ee antecede ee' no processo pip_i se ee é obsevado por pip_i antes de ee'. Esta relação pode ser representada por eiee \rightarrow_i e'.

Exemplo

Eventos em Três Processos

Algumas relações de antecedência em pip_i encontradas no diagrama:

  • a0ba \rightarrow_0 b
  • h1ih \rightarrow_1 i
  • k2mk \rightarrow_2 m
  • b0fb \rightarrow_0 f

Em sistemas distribuídos, expande-se a definição de happens-before para incluir transmissões de mensagens. Diz-se que ee antecede ee' se antecede em algum processo ou se correspondem ao envio e recepção de uma mesma mensagem. Para além disso, antecedência é transitiva.

  • HB1: Se pi:eie\exists{p_i}: e \rightarrow_i e', então eee \rightarrow e'.
  • HB2: Se m:e=send(m)e=recv(m)\exists{m}: e = send(m) \wedge e' = recv(m), então eee \rightarrow e'.
  • HB3: Se eeeee \rightarrow e' \wedge e' \rightarrow e'', então eee \rightarrow e''.

Com estas definições, observa-se que para uma sequência de eventos ei,i=0..Ne_i, i = 0..N, se for possível aplicar HB1 ou HB2 a qualquer par de eventos (ei,ei+1)(e_i, e_{i+1}), então, por HB3, e0eNe_0 \rightarrow e_N.

Nem todos os eventos têm uma ordem de antecedência definida. Diz-se que eventos distintos ee, ee' são concorrentes sse eeeee \nrightarrow e' \wedge e' \nrightarrow e. Esta relação é representada por eee \parallel e'. Nada se pode dizer (nem necessita ser dito) sobre a ordem em que estes eventos ocorrem.

Exemplo

Eventos em Três Processos

Algumas relações de antecedência encontradas no diagrama:

  • aba \rightarrow b
  • hch \rightarrow c
  • bjb \rightarrow j
  • hmh \rightarrow m

Algumas relações de concorrência encontradas no diagrama:

  • ala \parallel l
  • bhb \parallel h

Nota

Se dois eventos têm uma relação happened-before, então o primeiro pode ou não ter causado o segundo. Esta relação apenas sugere potenciais causalidades, podendo não haver qualquer ligação entre os eventos (eventos independentes).

Relógio Lógico de Lamport

Leslie Lamport propôs um algoritmo simples para capturar a ordem de eventos num sistema distribuído. Cada processo pip_i mantém um relógio lógico monótono LiL_i que pode ser usado para atribuir uma estampilha temporal Li(e)L_i(e) a cada evento ee. Quando o processo em que se atribuiu a estampilha não é relevante, usa-se L(e)L(e).

Os relógios são atualizados de acordo com as seguintes regras:

  • LC1: LiL_i é incrementado sempre que um evento é observado por pip_i.
  • LC2: Quando pip_i envia uma mensagem mm, inclui na mensagem a estampilha tt com o valor de LiL_i após executar LC1.
  • LC3: Quando pip_i recebe uma mensagem mm, atualiza LiL_i tal que Limax(Li,t)L_i \coloneqq \max(L_i, t) e depois executa LC1.
Exemplo

Logical Clocks

P0P_0, P1P_1 e P2P_2 mantêm, respetivamente, os relógios L0L_0, L1L_1 e L2L_2.

Quando P1P_1 envia a mensagem mhcm_{h \rightarrow c} inclui t=1t = 1 na mensagem. Quando P0P_0 recebe a mensagem, compara L0L_0 com a estampilha recebida:

L0max(L0,t)+1=max(2,1)+1=3L_0 \coloneqq \max(L_0, t) + 1 = \max(2, 1) + 1 = 3

Quando P0P_0 envia a mensagem mdmm_{d \rightarrow m} inclui t=4t = 4 na mensagem. Quando P2P_2 recebe a mensagem, compara L2L_2 com a estampilha recebida:

L2max(L2,t)+1=max(2,4)+1=5L_2 \coloneqq \max(L_2, t) + 1 = \max(2, 4) + 1 = 5

Relógios lógicos de Lamport garantem a seguinte propriedade:

eeL(e)<L(e)e \rightarrow e' \Rightarrow L(e) < L(e')

No entanto, o inverso não é verdadeiro. Isto é, L(e)<L(e)eeL(e) < L(e') \nRightarrow e \rightarrow e'. Pode-se encontrar um caso destes no exemplo anterior: L(k)<L(c)L(k) < L(c), mas kck \parallel c.

Demonstração: eeL(e)<L(e)e \rightarrow e' \Rightarrow L(e) < L(e')

Hipótese de indução: Se eee \rightarrow e', então L(e)<L(e)L(e) < L(e')

Passo base (HB1): Se ee e ee' ocorrem no mesmo processo, é imediato por LC1 que L(e)<L(e)L(e) < L(e')

Passo base (HB2): Se existe uma mensagem mm tal que ee é o evento de envio e ee' é o evento de receção, então L(e)<L(e)L(e) < L(e') por LC2 e LC3

Passo indutivo (HB3): Se eee \rightarrow e' e eee' \rightarrow e'', então L(e)<L(e)L(e) < L(e') e L(e)<L(e)L(e') < L(e''), por HI \square

Vector Clocks

Para superar esta limitação, Mattern e Fridge desenvolveram o Vector Clock: uma alternativa em que garante também a implicação no sentido contrário.

O Vector Clock é um tuplo de NN inteiros, um para cada processo participante no sistema distribuído. À semelhança do relógio de Lamport, cada processo pip_i mantém um vector clock ViV_i que pode ser usado para atribuir uma estampilha temporal Vi(e)V_i(e) a cada evento ee. Quando o processo em que se atribuiu a estampilha não é relevante, usa-se V(e)V(e).

  • VC1: Inicialmente, Vi[j]=0V_i[j] = 0 para todo o i,j=1,2,...,Ni, j = 1, 2, ..., N.
  • VC2: Vi[i]V_i[i] é incrementado sempre que um evento é observado por pip_i.
  • VC3: Quando pip_i envia uma mensagem mm, inclui na mensagem a estampilha tt com o valor de ViV_i após executar VC2.
  • VC4: Quando pip_i recebe uma mensagem mm, atualiza ViV_i realizando um merge com tt e depois executa VC2.

A operação de merge dos vector clock ViV_i e tt consiste em atualizar cada campo do vector clock ViV_i tal que Vi[j]max(Vi[j],t[j])V_i[j] \coloneqq \max(V_i[j], t[j]), para todo o j=1,2,...,Nj = 1, 2, ..., N.

Uma possível intuição para estes valores é a seguinte:

  • Vi[i]V_i[i] é o número de eventos que pip_i estampilhou.
  • Vi[j]V_i[j] (iji \neq j) é o número de eventos que pip_i já sabe que pjp_j estampilhou.
Exemplo

Vector Clocks

P0P_0, P1P_1 e P2P_2 mantêm, respetivamente, os relógios V0V_0, V1V_1 e V2V_2.

Quando P1P_1 envia a mensagem mhcm_{h \rightarrow c} inclui t=(0,1,0)t = (0,1,0) na mensagem. Quando P0P_0 recebe a mensagem, faz merge de V0V_0 com a estampilha recebida:

V0[0]max(V0[0],t[0])+1=max(2,0)+1=3V0[1]max(V0[1],t[1])=max(0,1)=1V0[2]max(V0[2],t[2])=max(0,0)=0\begin{align} \nonumber V_0[0] &\coloneqq \max(V_0[0], t[0]) + 1 = \max(2, 0) + 1 = 3 \\ \nonumber V_0[1] &\coloneqq \max(V_0[1], t[1]) = \max(0, 1) = 1 \\ \nonumber V_0[2] &\coloneqq \max(V_0[2], t[2]) = \max(0, 0) = 0 \end{align}

Ou seja, V0(3,1,0)V_0 \coloneqq (3, 1, 0).

Quando P0P_0 envia a mensagem mdmm_{d \rightarrow m} inclui t=(4,1,0)t = (4, 1, 0) na mensagem. Quando P2P_2 recebe a mensagem, faz merge de V2V_2 com a estampilha recebida:

V2[0]max(V0[0],t[0])=max(0,4)=4V2[1]max(V0[1],t[1])=max(0,1)=1V2[2]max(V0[2],t[2])+1=max(2,0)+1=3\begin{align} \nonumber V_2[0] &\coloneqq \max(V_0[0], t[0]) = \max(0, 4) = 4 \\ \nonumber V_2[1] &\coloneqq \max(V_0[1], t[1]) = \max(0, 1) = 1 \\ \nonumber V_2[2] &\coloneqq \max(V_0[2], t[2]) + 1 = \max(2, 0) + 1 = 3 \end{align}

Ou seja, V2(4,1,3)V_2 \coloneqq (4, 1, 3).

Pode-se comparar vector clocks da seguinte forma:

  • V=VV[j]=V[j]V = V' \Leftrightarrow V[j] = V'[j], para todo o j=1,2,...,Nj = 1, 2, ..., N.
  • VVV[j]V[j]V \leq V' \Leftrightarrow V[j] \leq V'[j], para todo o j=1,2,...,Nj = 1, 2, ..., N.
  • V<VVVVVV < V' \Leftrightarrow V \leq V' \wedge V \neq V'

Ou seja, temos que um vector clock é menor quando nenhum dos seus valores é maior, mas não são iguais. Comparando deste modo, obtemos a seguinte propriedade (uma versão da propriedade do relógio de Lamport mas com equivalência):

eeV(e)<V(e)e \rightarrow e' \Leftrightarrow V(e) < V(e')
Demonstração: eeV(e)<V(e)e \rightarrow e' \Leftrightarrow V(e) < V(e')

Começamos por mostrar que eeV(e)<V(e)e \rightarrow e' \Rightarrow V(e) < V(e')

Hipótese de indução: Se eee \rightarrow e', então V(e)<V(e)V(e) < V(e')

Passo base (HB1): Se ee e ee' ocorrem no mesmo processo, é imediato por VC2 que V(e)<V(e)V(e) < V(e')

Passo base (HB2): Se existe uma mensagem mm tal que ee é o evento de envio e ee' é o evento de receção, então V(e)<V(e)V(e) < V(e') por VC3 e VC4

Passo indutivo (HB3): Se eee \rightarrow e' e eee' \rightarrow e'', então V(e)<V(e)V(e) < V(e') e V(e)<V(e)V(e') < V(e''), por HI

De seguida, mostra-se que V(e)<V(e)eeV(e) < V(e') \Rightarrow e \rightarrow e'

Para isto, prova-se que para um par de eventos ee e ee':

ee¬(V(e)V(e))¬(V(e)V(e))e \parallel e' \Rightarrow \neg(V(e) \leq V(e')) \wedge \neg(V(e') \leq V(e))

Seja ee e ee' um par de eventos concorrentes tal que ee ocorre em pip_i e ee' ocorre em pjp_j. Como os eventos são concorrentes, sabemos que nenhuma mensagem de pip_i durante ou após o evento ee propagou a sua estampilha para pjp_j pelo momento em que ocorre ee', e vice versa.

Como ee ocorreu em pip_i e pjp_j ainda não foi "informado", é garantido que Vi[i]>Vj[i]V_i[i] > V_j[i]. Temos também que Vj[j]>Vi[j]V_j[j] > V_i[j]. Provando-se assim o lema e o seu contra-recíproco:

V(e)V(e)V(e)V(e)¬(ee)V(e) \leq V(e') \lor V(e) \leq V(e') \Rightarrow \neg(e \parallel e')

Se dois eventos não são concorrentes, então tem de existir uma relação de happens-before, é evidente que V(e)<V(e)eeV(e) < V(e') \Rightarrow e' \rightarrow e é falso.

Logo, V(e)<V(e)eeV(e) < V(e') \Rightarrow e \rightarrow e' e temos que:

eeV(e)<V(e)e \rightarrow e' \Leftrightarrow V(e) < V(e') \square

Nota

Em comparação com os timestamps de Lamport, os vector timestamps têm a desvantagem de precisar de uma maior capacidade de armazenamento e transmissão de mensagens, proporcional ao número de processos. No entanto, existem técnicas para armazenar e transmitir quantidades menores de dados, com o custo adicional de processamento para reconstruir os vetores.

Salvaguarda distribuída

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:

  • 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:

Corte coerente

Um corte é coerente se, para cada evento que este contém, também inclui todos os eventos que aconteceram-antes 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 pip_i registou o envio de uma mensagem mm para o processo pjp_j (ij)(i \neq j), então, examinando se pjp_j recebeu mm, podemos inferir se mm faz ou não parte do estado do canal entre pip_i e pjp_j.

Tipos de cortes: (fortemente) coerente vs incoerente

O corte mais à direita é incoerente porque p2p_2 incluiu a receção de m5m_5 mas p1p_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 m4m_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 pip_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 pip_i recebe um marker:
    • se ainda não guardou o seu estado local, guarda-o

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
R: P1=800, P2=300, P3=800P1 = 800, ~P2 = 300, ~P3 = 800

Justificação:

  • P1: Enviou 2 mensagens, pelo que perdeu 2100=2002*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 100100 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

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:

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.

Pseudocódigo do algoritmo
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).

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 N1N-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

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
R: P1=800, P2=300, P3=800, Canal P3P1 (C31)=100P1 = 800, ~P2 = 300, ~P3 = 800, ~Canal ~ P3-P1~(C31) = 100

Justificação:

  • P1: Enviou 2 mensagens, pelo que perdeu 2100=2002*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 100100 tokens
  • C31: Desde que P1 enviou o primeiro marker até que recebeu de volta um de P3, recebeu nesse canal 1 mensagem, ou seja, 100100 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.

Nota

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

Snapshot vs sistema real

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

É possível observar que tanto e1e_1 é concorrente com e2e_2 como e3e_3 é com e4e_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)(100, 100), após e1e_1 e e2e_2 é (80,40)(80, 40) e por fim é (140,60)(140, 60).

Snapshot vs sistema real vector clocks

Referências

  • Coulouris et al - Distributed Systems: Concepts and Design (5th Edition)
    • 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
    • Secções 5.1 e 5.2
  • Departamento de Engenharia Informática - Slides de Sistemas Distribuídos (2022/2023)
    • 3a Fundamentos: Tempo
  • Paul Krzyzanowski - Assigning Lamport & Vector Timestamps
    • Imagens dos exemplos de eventos e relógios lógicos
  • Departamento de Engenharia Informática - Slides de Sistemas Distribuídos (2023/2024)
    • SlidesTagus-Aula03b