Como evitar um conflito distribuído durante uma conexão mútua entre dois nós?

11

Suponha que tenhamos dois nós de mesmo nível: o primeiro nó pode enviar uma solicitação de conexão para o segundo, mas também o segundo pode enviar uma solicitação de conexão para o primeiro. Como evitar uma conexão dupla entre os dois nós? Para resolver esse problema, seria suficiente tornar sequenciais as operações executadas para criar conexões TCP de entrada ou saída.

Isso significa que cada nó deve processar sequencialmente cada nova operação de criação de conexão, tanto para conexões de entrada quanto para conexões de saída. Dessa maneira, mantendo uma lista de nós conectados, antes de aceitar uma nova conexão de entrada de um nó ou antes de enviar uma solicitação de conexão a um nó, será suficiente verificar se esse nó já está presente na lista.

Para tornar sequenciais as operações de criação de conexões, é suficiente executar um bloqueio na lista de nós conectados: de fato, para cada nova conexão, o identificador do novo nó conectado é adicionado a esta lista. No entanto, gostaria de saber se essa abordagem pode causar um conflito distribuído :

  • o primeiro nó pode enviar uma solicitação de conexão para o segundo;
  • o segundo nó pode enviar uma solicitação de conexão para o primeiro;
  • assumindo que as duas solicitações de conexão não sejam assíncronas, os dois nós bloqueiam todas as solicitações de conexão recebidas.

Como eu poderia resolver esse problema?

ATUALIZAÇÃO: No entanto, ainda tenho que bloquear a lista toda vez que uma nova conexão (de entrada ou saída) é criada, já que outros threads podem acessar essa lista, e o problema de conflito ainda permanece.

ATUALIZAÇÃO 2: Com base no seu conselho, escrevi um algoritmo para impedir a aceitação mútua de uma solicitação de login. Como cada nó é um par, pode haver uma rotina de cliente para enviar novas solicitações de conexão e uma rotina de servidor para aceitar conexões de entrada.

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
}

Exemplo: O IP: porta do nó A é A: 7001 - O IP: porta do nó B é B: 8001.

Suponha que o nó A tenha enviado uma solicitação de login ao nó B: 8001. Nesse caso, o nó A chama a rotina de login enviando enviando seu próprio endereço de escuta (A: 7001). Como conseqüência, a tabela vizinhos_ do nó A contém o endereço do nó remoto (B: 8001): esse endereço está associado ao estado CONNECTING. O nó A está aguardando o nó B aceitar ou negar a solicitação de login.

Enquanto isso, o nó B também pode ter enviado uma solicitação de conexão para o endereço do nó A (A: 7001), e o nó A pode estar processando a solicitação do nó B. Portanto, a tabela de vizinhos do nó B contém o endereço do controle remoto. nó (A: 7001): esse endereço está associado ao estado CONNECTING. O nó B está aguardando o nó A aceitar ou negar a solicitação de login.

Se o lado do servidor do nó A rejeitar a solicitação de B: 8001, devo ter certeza de que o lado do servidor do nó B aceitará a solicitação de A: 7001. Da mesma forma, se o lado do servidor do nó B rejeitar a solicitação de A: 7001, devo ter certeza de que o lado do servidor do nó A aceitará a solicitação do B: 8001.

De acordo com a regra "pequeno endereço" , nesse caso, o nó A rejeitará a solicitação de login pelo nó B, enquanto o nó B aceitará a solicitação do nó A.

O que você acha disso?

enzom83
fonte
Esses tipos de algoritmos são muito difíceis de analisar e provar. No entanto, existe um pesquisador especialista em muitos aspectos da computação distribuída. Confira a página de publicações de Leslie Lamport em: research.microsoft.com/en-us/um/people/lamport/pubs/pubs.html
DeveloperDon

Respostas:

3

Você pode tentar uma abordagem "otimista": conecte primeiro e depois desconecte se detectar uma conexão mútua simultânea. Dessa forma, você não precisaria manter de fora as solicitações de conexão enquanto estiver fazendo novas conexões: quando uma conexão de entrada for estabelecida, bloqueie a lista e verifique se você possui uma conexão de saída com o mesmo host. Se sim, verifique o endereço do host. Se for menor que o seu, desconecte sua conexão de saída; caso contrário, desconecte o de entrada. Seu host de mesmo nível fará o oposto, porque os endereços serão comparados de maneira diferente e uma das duas conexões será descartada. Essa abordagem permite evitar novas tentativas de conexão e potencialmente ajuda a aceitar mais solicitações de conexão por unidade de tempo.

dasblinkenlight
fonte
No entanto, ainda tenho que bloquear a lista toda vez que uma nova conexão (de entrada ou saída) é criada, já que outros threads podem acessar essa lista, e o problema de conflito ainda permanece.
enzom83
@ enzom83 Não, o impasse não é possível nesse esquema, porque o par nunca precisa esperar que você conclua a operação que requer bloqueio. O mutex é para proteger os internos da sua lista; depois de adquiri-lo, você sai em um período conhecido, porque não precisa esperar por nenhum outro recurso dentro da seção crítica.
dasblinkenlight
Ok, mas o conflito pode ocorrer se a solicitação de conexão não for assíncrona e se for executada dentro da seção crítica: nesse caso, o nó não poderá deixar o mutex até que sua solicitação de conexão seja aceita. Caso contrário, eu devo executar o bloqueio na lista apenas para adicionar (ou remover) um nó: nesse caso, devo verificar se há conexões duplicadas, etc. Finalmente, outra opção seria enviar uma solicitação de conexão assíncrona.
enzom83
1
@ enzom83 Desde que você não solicite conexões de dentro da seção crítica, não obteria um conflito distribuído. Essa é a ideia por trás da abordagem otimista - você executa um bloqueio na lista apenas para adicionar ou remover um nó e, se encontrar uma conexão recíproca ao adicionar um nó, interrompe-o após sair da seção crítica, usando o "endereço menor" regra (da resposta).
precisa saber é o seguinte
4

Quando um nó envia uma solicitação para outro, ele pode incluir um número inteiro aleatório de 64 bits. Quando um nó recebe uma solicitação de conexão, se já enviou uma, é mantido com o número mais alto e eliminado os outros. Por exemplo:

Tempo 1: A tenta se conectar a B, envia o número 123.

Tempo 2: B tenta se conectar a A, envia o número 234.

Tempo 3: B recebe a solicitação de A. Como a própria solicitação de B tem um número maior, ela nega a solicitação de A.

Tempo 4: A recebe a solicitação de B. Como a solicitação de B tem um número maior, A a aceita e descarta sua própria solicitação.

Para gerar o número aleatório, certifique-se de propagar seu gerador de números aleatórios com / dev / urandom, em vez de usar a propagação padrão do seu gerador de números aleatórios, que geralmente é baseado no tempo do relógio de parede: há uma chance não ignorável de que dois nós receberá a mesma semente.

Em vez de números aleatórios, você também pode distribuir números antecipadamente (ou seja, apenas numerar todas as máquinas de 1 a n), ou usar um endereço MAC, ou alguma outra maneira de encontrar um número em que a probabilidade de colisão seja tão pequena quanto possível. ignorável.

Martin C. Martin
fonte
3

Se eu entendi, o problema que você está tentando evitar é o seguinte:

  • O nó1 solicita conexão do nó 2
  • O nó1 bloqueia a lista de conexões
  • O nó2 solicita conexão do nó 1
  • O nó2 bloqueia a lista de conexões
  • O nó2 recebe solicitação de conexão do nó1, rejeita porque a lista está bloqueada
  • Nó1 recebe solicitação de conexão do nó2, rejeita porque a lista está bloqueada
  • Nenhum acaba se conectando.

Eu posso pensar em algumas maneiras diferentes de lidar com isso.

  1. Se você tentar se conectar a um nó e ele rejeitar sua solicitação com uma mensagem "lista bloqueada", aguarde um número aleatório de milissegundos antes de tentar novamente. (A aleatoriedade é crítica. Torna muito menos provável que ambos esperem exatamente a mesma quantidade de tempo e repitam o mesmo problema ad infinitum .)
  2. Sincronize os relógios dos dois sistemas com um servidor de horário e envie um carimbo de data / hora junto com a solicitação de conexão. Se uma solicitação de conexão vier de um nó ao qual você está tentando se conectar, os dois nós concordam que aquele que tentou se conectar primeiro vence e a outra conexão é fechada.
Mason Wheeler
fonte
O problema também é que o nó que recebe a solicitação não pode rejeitar a conexão, mas permaneceria em espera indefinidamente para obter o bloqueio na lista.
enzom83