Як я міг уникнути розподіленого глухого кута під час взаємного зв’язку між двома вузлами?


11

Припустимо, у нас є два однорангові вузли: перший вузол може надіслати запит на з'єднання другому, а також другий може надіслати запит на з'єднання до першого. Як уникнути подвійного зв’язку між двома вузлами? Для вирішення цього питання достатньо зробити послідовні операції, що виконуються для створення вхідних або вихідних TCP-з'єднань.

Це означає, що кожен вузол повинен обробляти послідовно кожну нову операцію створення з'єднання, як для вхідних, так і для вихідних з'єднань. Таким чином, підтримуючи список підключених вузлів, перш ніж приймати нове вхідне з'єднання від вузла або перед відправленням запиту на з'єднання до вузла, буде достатньо перевірити, чи цей вузол вже присутній у списку.

Для здійснення послідовних операцій створення з'єднань достатньо виконати блокування у списку підключених вузлів: насправді для кожного нового з'єднання до цього списку додається ідентифікатор нового підключеного вузла. Однак мені цікаво, чи може такий підхід спричинити поширений глухий кут :

  • перший вузол міг надіслати запит на з'єднання другому;
  • другий вузол може надіслати запит на з'єднання до першого;
  • припускаючи, що два запити на з'єднання не є асинхронними, обидва вузли блокують будь-які вхідні запити на з'єднання.

Як я міг вирішити цю проблему?

ОНОВЛЕННЯ: Однак мені все одно доводиться блокувати список щоразу, коли створюється нове (вхідне чи вихідне) з'єднання, оскільки інші потоки можуть отримати доступ до цього списку, тоді проблема з тупиком все ще залишатиметься.

ОНОВЛЕННЯ 2: На основі вашої поради я написав алгоритм, щоб запобігти взаємному прийняттю запиту на вхід. Оскільки кожен вузол є одноранговим, він може мати клієнтську процедуру для надсилання нових запитів на з'єднання та процедуру сервера для прийому вхідних з'єднань.

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
}

Приклад: Порт IP: вузол A дорівнює A: 7001 - IP: порт вузла B дорівнює B: 8001.

Припустимо, що вузол A надіслав запит на вхід у вузол B: 8001. У цьому випадку вузол A викликає процедуру входу, надсилаючи, надсилаючи власну адресу прослуховування (A: 7001). Як наслідок, сусідній_таблиця вузла A містить адресу віддаленого вузла (B: 8001): ця адреса пов'язана зі станом ЗВ'ЯЗКУ. Вузол A чекає, коли вузол B прийме або відхилить запит на вхід.

Тим часом, вузол B також, можливо, надіслав запит на з'єднання на адресу вузла A (A: 7001), тоді вузол A може обробляти запит вузла B. Отже, сусіда_ таблиця вузла B містить адресу віддаленого вузол (A: 7001): ця адреса асоціюється зі станом З’єднання. Вузол B чекає на вузол A прийняття або відхилення запиту на вхід.

Якщо сторона сервера вузла A відхиляє запит від B: 8001, я повинен бути впевнений, що сторона сервера вузла B прийме запит від A: 7001. Аналогічно, якщо серверна частина вузла B відхиляє запит від A: 7001, я повинен бути впевнений, що сторона сервера вузла A прийме запит від B: 8001.

Згідно з правилом "малої адреси" , у цьому випадку вузол A буде відхиляти запит на вхід вузлом B, тоді як вузол B прийме запит від вузла A.

Що ти думаєш про це?


Такі види алгоритмів досить важкі для аналізу та доведення. Однак є дослідник, який є експертом у багатьох аспектах розподілених обчислень. Перегляньте сторінку публікацій Леслі Лампорта за адресою: research.microsoft.com/en-us/um/people/lamport/pubs/pubs.html
DeveloperDon

Відповіді:


3

Ви можете спробувати "оптимістичний" підхід: спочатку підключіться, а потім відключіться, якщо виявите одночасний взаємний зв'язок. Таким чином, вам не потрібно буде уникати запитів на з'єднання під час встановлення нових з'єднань: коли встановлено вхідне з'єднання, заблокуйте список і побачите, чи є у вас вихідне з'єднання з тим самим хостом. Якщо ви це зробите, перевірте адресу хоста. Якщо він менше вашого, відключіть вихідне з'єднання; в іншому випадку відключіть вхідний. Ваш одноранговий хост зробить навпаки, оскільки адреси будуть порівнюватися по-різному, і одне з двох з'єднань буде відхилено. Такий підхід дозволяє уникати повторних спроб з'єднань і, можливо, допомагає приймати більше запитів на з'єднання за одиницю часу.


Однак мені все одно доводиться блокувати список щоразу, коли створюється нове (вхідне або вихідне) з'єднання, оскільки інші потоки можуть отримати доступ до цього списку, тоді проблема з тупиком все ще залишатиметься.
enzom83

@ enzom83 Ні, тупик не можливий за цією схемою, тому що одноліткові ніколи не потрібно чекати, коли ви завершите операцію, яка потребує блокування. Мютекс - це захист внутрішніх даних вашого списку; як тільки ви придбаєте його, ви залишаєте за відому кількість часу, тому що не потрібно чекати будь-якого іншого ресурсу всередині критичного розділу.
dasblinkenlight

Гаразд, але тупикова ситуація може статися, якщо запит на з'єднання не є асинхронним , і якщо він виконується всередині критичного розділу: у цьому випадку вузол не може залишити мутекс, поки його запит на з'єднання не буде прийнято. В іншому випадку я повинен виконати блокування у списку лише для того, щоб додати (або видалити) вузол: у цьому випадку я повинен перевірити наявність дублікатів з'єднань тощо. Нарешті, ще одним варіантом було б надіслати запит на асинхронне з'єднання.
enzom83

1
@ enzom83 Поки ви не вимагаєте з'єднань з критичного розділу, ви не отримаєте розподілену тупикову ситуацію. Ось ідея оптимістичного підходу - ви виконуєте блокування у списку лише для того, щоб додати або видалити вузол, і якщо під час додавання вузла ви виявите зворотне з'єднання, ви розриваєте його після виходу з критичного розділу, використовуючи "меншу адресу" правило (з відповіді).
dasblinkenlight

4

Коли один вузол надсилає запит іншому, він може включати випадкове 64-бітове ціле число. Коли вузол отримує запит на з'єднання, якщо він вже надіслав один власний, він зберігає той, який має найбільше число, а інші скидає. Наприклад:

Час 1: A намагається з'єднатися з B, надсилає число 123.

Час 2: B намагається з'єднатися з A, надсилає номер 234.

Час 3: B отримує запит A. Оскільки власний запит В має більшу кількість, він відхиляє запит А.

Час 4: А отримує запит В. Оскільки запит B має більшу кількість, A приймає його та відміняє власний запит.

Щоб генерувати випадкове число, переконайтеся, що ви засіли генератор випадкових чисел за допомогою / dev / urandom, а не використовуйте висівання за замовчуванням генератора випадкових чисел, яке часто базується на годинниковому годиннику: існує невідомий шанс, що два вузли отримає таке ж насіння.

Замість випадкових чисел ви також можете розповсюджувати числа достроково (тобто просто нумерувати всі машини від 1 до n), або використовувати MAC-адресу чи інший спосіб пошуку числа, де ймовірність зіткнення настільки мала, щоб бути невідомий.


3

Якщо я розумію, проблема, якої ви намагаєтеся уникнути, виглядає так:

  • Node1 запитує з'єднання з вузла 2
  • Node1 блокує список з'єднань
  • Node2 запитує з'єднання з вузла 1
  • Node2 блокує список з'єднань
  • Node2 отримує запит на з'єднання від node1, відхиляє, оскільки список заблокований
  • Node1 отримує запит на з'єднання від node2, відхиляє, оскільки список заблокований
  • Ні один не закінчується з'єднанням один з одним.

Я можу придумати кілька різних способів впоратися з цим.

  1. Якщо ви спробуєте підключитися до вузла, і він відхиляє ваш запит повідомленням "список заблокований", зачекайте випадкову кількість мілісекунд, перш ніж повторити спробу. (Випадковість є критичною. Це робить набагато менш ймовірним, що обидва будуть чекати точно стільки ж часу, і повторювати ту саму проблему ad infinitum .)
  2. Синхронізуйте годинник обох систем із сервером часу та надішліть позначку часу разом із запитом на з'єднання. Якщо запит на з'єднання надходить з вузла, до якого ви зараз намагаєтесь підключитися, то обидва вузли погоджуються, що той, хто намагався з'єднати перший, виграє, а другий з'єднання закривається.

Проблема також полягає в тому, що вузол, який отримує запит, не може відхилити з'єднання, але він буде залишатися в очікуванні нескінченно, щоб отримати блокування у списку.
enzom83
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.