Come posso evitare un deadlock distribuito durante una connessione reciproca tra due nodi?


11

Supponiamo di avere due nodi peer: il primo nodo può inviare una richiesta di connessione al secondo, ma anche il secondo può inviare una richiesta di connessione al primo. Come evitare una doppia connessione tra i due nodi? Per risolvere questo problema, sarebbe sufficiente rendere sequenziali le operazioni eseguite per creare connessioni TCP in entrata o in uscita.

Ciò significa che ogni nodo dovrebbe elaborare in sequenza ogni nuova operazione di creazione della connessione, sia per le connessioni in entrata che per quelle in uscita. In questo modo, mantenendo un elenco di nodi connessi, prima di accettare una nuova connessione in entrata da un nodo o prima di inviare una richiesta di connessione a un nodo, sarà sufficiente verificare se questo nodo è già presente nell'elenco.

Per rendere sequenziali le operazioni di creazione delle connessioni, è sufficiente eseguire un blocco sull'elenco dei nodi connessi: infatti, per ogni nuova connessione, l'identificatore del nuovo nodo connesso viene aggiunto a questo elenco. Tuttavia, mi chiedo se questo approccio può causare deadlock distribuito :

  • il primo nodo potrebbe inviare una richiesta di connessione al secondo;
  • il secondo nodo potrebbe inviare una richiesta di connessione al primo;
  • supponendo che le due richieste di connessione non siano asincrone, entrambi i nodi bloccano eventuali richieste di connessione in entrata.

Come potrei risolvere questo problema?

AGGIORNAMENTO: Tuttavia, devo ancora bloccare l'elenco ogni volta che viene creata una nuova connessione (in entrata o in uscita), poiché altri thread possono accedere a questo elenco, quindi il problema del deadlock rimarrebbe comunque.

AGGIORNAMENTO 2: Sulla base dei tuoi consigli ho scritto un algoritmo per impedire l'accettazione reciproca di una richiesta di accesso. Poiché ogni nodo è un peer, potrebbe avere una routine client per inviare nuove richieste di connessione e una routine server per accettare le connessioni in entrata.

ClientSideLoginRoutine() {
    for each (address in cache) {
        lock (neighbors_table) {
            if (neighbors_table.contains(address)) {
                // there is already a neighbor with the same address
                continue;
            }
            neighbors_table.add(address, status: CONNECTING);

        } // end lock

        // ...
        // The node tries to establish a TCP connection with the remote address
        // and perform the login procedure by sending its listening address (IP and port).
        boolean login_result = // ...
        // ...

        if (login_result)
            lock (neighbors_table)
                neighbors_table.add(address, status: CONNECTED);

    } // end for
}

ServerSideLoginRoutine(remoteListeningAddress) {
    // ...
    // initialization of data structures needed for communication (queues, etc)
    // ...

    lock(neighbors_table) {
        if(neighbors_table.contains(remoteAddress) && its status is CONNECTING) {
            // In this case, the client-side on the same node has already
            // initiated the procedure of logging in to the remote node.

            if (myListeningAddress < remoteListeningAddress) {
                refusesLogin();
                return;
            }
        }
        neighbors_table.add(remoteListeningAddress, status: CONNECTED);

    } // end lock
}

Esempio: l'IP: la porta del nodo A è A: 7001 - L'IP: la porta del nodo B è B: 8001.

Supponiamo che il nodo A abbia inviato una richiesta di accesso al nodo B: 8001. In questo caso, il nodo A chiama la routine di accesso inviando inviando il proprio indirizzo di ascolto (A: 7001). Di conseguenza, neighbors_table del nodo A contiene l'indirizzo del nodo remoto (B: 8001): questo indirizzo è associato allo stato CONNECTING. Il nodo A è in attesa che il nodo B accetti o rifiuti la richiesta di accesso.

Nel frattempo, anche il nodo B potrebbe aver inviato una richiesta di connessione all'indirizzo del nodo A (A: 7001), quindi il nodo A potrebbe elaborare la richiesta del nodo B. Quindi, la tabella_gione_di vicini del nodo B contiene l'indirizzo del telecomando nodo (A: 7001): questo indirizzo è associato allo stato CONNECTING. Il nodo B è in attesa che il nodo A accetti o rifiuti la richiesta di accesso.

Se il lato server del nodo A rifiuta la richiesta da B: 8001, allora devo essere sicuro che il lato server del nodo B accetterà la richiesta da A: 7001. Allo stesso modo, se il lato server del nodo B rifiuta la richiesta da A: 7001, allora devo essere sicuro che il lato server del nodo A accetterà la richiesta da B: 8001.

Secondo la regola del "piccolo indirizzo" , in questo caso il nodo A rifiuterà la richiesta di accesso dal nodo B, mentre il nodo B accetterà la richiesta dal nodo A.

Cosa ne pensi di questo?


Questi tipi di algoritmi sono piuttosto difficili da analizzare e provare. Tuttavia, esiste un ricercatore esperto in molti aspetti dell'informatica distribuita. Dai un'occhiata alla pagina delle pubblicazioni di Leslie Lamport all'indirizzo: research.microsoft.com/en-us/um/people/lamport/pubs/pubs.html
Sviluppatore

Risposte:


3

È possibile provare un approccio "ottimista": connettersi prima, quindi disconnettersi se si rileva una connessione reciproca simultanea. In questo modo non è necessario tenere fuori le richieste di connessione mentre si stanno effettuando nuove connessioni: quando viene stabilita una connessione in entrata, bloccare l'elenco e vedere se si dispone di una connessione in uscita allo stesso host. In tal caso, controlla l'indirizzo dell'host. Se è più piccolo del tuo, disconnetti la connessione in uscita; in caso contrario, scollegare quello in arrivo. Il tuo host peer farà il contrario, perché gli indirizzi verranno confrontati in modo diverso e una delle due connessioni verrà eliminata. Questo approccio consente di evitare di riprovare le connessioni e potenzialmente aiuta ad accettare più richieste di connessione per unità di tempo.


Tuttavia, devo ancora bloccare l'elenco ogni volta che viene creata una nuova connessione (in entrata o in uscita), poiché altri thread possono accedere a questo elenco, quindi il problema del deadlock rimarrebbe comunque.
enzom83

@ enzom83 No, il deadlock non è possibile con questo schema, perché il peer non ha mai bisogno di aspettare che tu completi l'operazione che richiede il blocco. Il mutex serve a proteggere gli interni della tua lista; una volta acquisito, si parte in un periodo di tempo noto, poiché non è necessario attendere altre risorse all'interno della sezione critica.
dasblinkenlight,

Ok, ma il deadlock potrebbe verificarsi se la richiesta di connessione non è asincrona e se viene eseguita all'interno della sezione critica: in questo caso, il nodo non può lasciare il mutex fino a quando la sua richiesta di connessione non è stata accettata. Altrimenti dovrei eseguire il blocco sull'elenco solo per aggiungere (o rimuovere) un nodo: in questo caso dovrei verificare connessioni duplicate, ecc. Infine, un'altra opzione sarebbe quella di inviare una richiesta di connessione asincrona.
enzom83

1
@ enzom83 Se non si richiedono connessioni all'interno della sezione critica, non si otterrà un deadlock distribuito. Questa è l'idea alla base dell'approccio ottimistico: esegui un blocco nell'elenco solo per aggiungere o rimuovere un nodo e se trovi una connessione reciproca quando aggiungi un nodo, lo rompi dopo aver lasciato la sezione critica, usando "l'indirizzo più piccolo" regola (dalla risposta).
dasblinkenlight,

4

Quando un nodo invia una richiesta a un altro, potrebbe includere un numero intero a 64 bit casuale. Quando un nodo riceve una richiesta di connessione, se ne ha già inviato uno proprio, mantiene quello con il numero più alto e rilascia gli altri. Per esempio:

Tempo 1: A tenta di connettersi a B, invia il numero 123.

Tempo 2: B tenta di connettersi ad A, invia il numero 234.

Tempo 3: B riceve la richiesta di A. Dato che la richiesta di B ha un numero più alto, essa nega la richiesta di A.

Tempo 4: A riceve la richiesta B. Poiché la richiesta di B ha un numero più alto, A lo accetta e lascia cadere la propria richiesta.

Per generare il numero casuale, assicurati di eseguire il seeding del generatore di numeri casuali con / dev / urandom, anziché utilizzare il seeding predefinito del generatore di numeri casuali, che spesso si basa sull'orologio del wall clock: esiste una possibilità non ignorabile che due nodi otterrà lo stesso seme.

Invece di numeri casuali, puoi anche distribuire numeri in anticipo (cioè numerare tutte le macchine da 1 a n), oppure utilizzare un indirizzo MAC o un altro modo di trovare un numero in cui la probabilità di collisione è così piccola da essere ignorabile.


3

Se ho capito, il problema che stai cercando di evitare va così:

  • Il nodo 1 richiede la connessione dal nodo 2
  • Nodo1 blocca l'elenco delle connessioni
  • Nodo2 richiede la connessione dal nodo 1
  • Nodo2 blocca l'elenco delle connessioni
  • Nodo2 riceve la richiesta di connessione dal nodo1, rifiuta perché l'elenco è bloccato
  • Il nodo1 riceve la richiesta di connessione dal nodo2, rifiuta perché l'elenco è bloccato
  • Nessuno dei due finisce per connettersi tra loro.

Posso pensare a un paio di modi diversi per affrontarlo.

  1. Se si tenta di connettersi a un nodo e rifiuta la richiesta con un messaggio "elenco bloccato", attendere un numero casuale di millisecondi prima di riprovare. (La casualità è fondamentale. Rende molto meno probabile che entrambi aspettino lo stesso tempo esatto e ripetano lo stesso problema all'infinito .)
  2. Sincronizza gli orologi di entrambi i sistemi con un time server e invia un timestamp insieme alla richiesta di connessione. Se una richiesta di connessione proviene da un nodo a cui si sta attualmente tentando di connettersi, entrambi i nodi concordano sul fatto che uno dei due abbia tentato di connettersi per primo vince e l'altra viene chiusa.

Il problema è anche che il nodo che riceve la richiesta non può rifiutare la connessione, ma rimarrebbe in attesa indefinitamente per ottenere il blocco sulla lista.
enzom83,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.