Примітка: У цій статті фокус зроблено на збереженні прив'язки сесій (session affinity) між кількома різними портами сервісів. Передбачається, що налаштування TLS, перевірки працездатності (health checks) та загальні практики розгортання HAProxy у вас уже впроваджені.

Побудова високої доступності (High Availability) для YSoft SafeQ 6 виявилася набагато складнішим завданням, ніж просто встановлення балансувальника навантаження перед двома вузлами додатків.

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


Виклик: незалежні порти, але єдина сесія

На відміну від класичного веб-додатка, який працює через одну адресу HTTPS, YSoft SafeQ 6 встановлює кілька паралельних з'єднань через різні порти TCP та HTTPS під час одного логічного процесу (наприклад, автентифікація на терміналі принтера, надсилання завдання на друк та сканування).

З точки зору принтера (клієнта), трафік іде на різні сервіси, які працюють на різних портах:

               ┌──► Порт 5011 (HTTP Auth)  ──► Frontend TS (insecure)
               │
Printer/Client ┼──► Порт 5022 (HTTPS TS)   ──► Frontend TS (secure)
               │
               └──► Порт 5555 (TCP SPOC)   ──► Frontend terminal_5555

Усередині HAProxy кожен із цих портів належить до окремого frontend та backend:

  • З точки зору HAProxy — це абсолютно незалежні TCP-сесії.
  • З точки зору SafeQ — усі ці запити мають потрапити на один і той самий фізичний сервер (вузол) для збереження цілісності сесії та уникнення проблем із синхронізацією стану.

Якщо принтер проходить автентифікацію на Сервері 1 (порт 5011), але наступний запит на отримання списку друку (порт 5022) потрапляє на Сервер 2, авторизація не спрацює, оскільки другий сервер нічого не знає про поточну сесію.


Чому окремі Stick Tables не працюють

Найпростіший підхід до балансування такої архітектури — прописати окремі таблиці відповідності (stick-table) для кожного бекенду:

backend ts_http
    stick-table type ip size 100k expire 30m
    stick on src
    server NODE1 192.168.1.100:5011 check

backend ts_https
    stick-table type ip size 100k expire 30m
    stick on src
    server NODE1 192.168.1.100:5022 ssl verify none check

Навіть якщо обидва бекенди відстежують клієнта за однією IP-адресою джерела (src), їхні таблиці маршрутизації є повністю ізольованими.

Коли принтер надсилає перший HTTP-запит, балансувальник може призначити йому NODE1. Через частку секунди, коли принтер робить HTTPS-запит, бекенд ts_https приймає рішення про балансування незалежно (наприклад, за алгоритмом leastconn), і надсилає запит на NODE2. Сесія користувача переривається.


Спільне рішення про маршрутизацію (Shared Stick Tables)

Замість того, щоб приймати рішення про маршрутизацію окремо для кожного порту, можна змусити HAProxy використовувати рішення, прийняте першим бекендом. Фактично, перше успішне підключення фіксує сервер для всього подальшого життєвого циклу сесії.

Ось конфігурація, яка вирішує це завдання:

frontend terminal
    bind *:5011
    bind *:5012 ssl crt /etc/haproxy/ssl/ha-safeq.pem
    bind *:5022 ssl crt /etc/haproxy/ssl/ha-safeq.pem verify none
    bind *:5610 ssl crt /etc/haproxy/ssl/ha-safeq.pem
    mode http
    acl is_ssl ssl_fc
    use_backend TS_ssl if is_ssl
    default_backend TS_insecure

backend TS_insecure
    balance leastconn
    option redispatch
    mode http
    
    # 1. Цей бекенд створює спільну таблицю відповідності сесій
    stick-table type ip size 100k expire 30m
    stick on src
    server LVIV 192.168.1.100:5011 check inter 5s fall 3 rise 2
    server KYIV 192.168.1.200:5011 check inter 5s fall 3 rise 2

backend TS_ssl
    balance leastconn
    option redispatch
    mode http
    # 2. Повторно використовуємо таблицю з бекенду TS_insecure
    stick on src table TS_insecure
    # 3. Імена серверів МАЮТЬ точно збігатися з першим бекендом
    server LVIV 192.168.1.100 ssl verify none check port 5022 inter 5s fall 3 rise 2
    server KYIV 192.168.1.200 ssl verify none check port 5022 inter 5s fall 3 rise 2

Як це працює:

  1. Бекенд TS_insecure визначає первинний напрямок. Коли принтер звертається до порту 5011, HAProxy обирає сервер (наприклад, LVIV) і записує зв'язку [IP принтера -> LVIV] у таблицю TS_insecure.
  2. Коли цей же принтер робить наступні запити на HTTPS-порти (5012, 5022 тощо), вони потрапляють у бекенд TS_ssl.
  3. Замість вибору нового сервера, TS_ssl перевіряє спільну таблицю (stick on src table TS_insecure) і миттєво перенаправляє запит на вже призначений сервер (LVIV).

Примітка: Алгоритм балансування (balance leastconn) використовується лише тоді, коли для клієнта ще немає запису в таблиці відповідності. Як тільки запис створено, усі наступні запити оминають алгоритм вибору і йдуть за збереженим маршрутом.

5011 (HTTP Auth) ──► Потрапляє в TS_insecure ──► Створює запис у таблиці ──► Йде на LVIV
                                                      │
5022 (HTTPS TS)  ──► Потрапляє в TS_ssl      ──► Зчитує запис з таблиці  ──► Йде на LVIV
                                                      │
5610 (HTTPS)     ──► Потрапляє в TS_ssl      ──► Зчитує запис з таблиці  ──► Йде на LVIV

Технічні нюанси, про які варто знати

Під час тестування відмовостійкості у продакшені я зіткнувся з двома важливими підводними каменями, які можуть порушити роботу цієї схеми:

1. Імена серверів мають бути ідентичними в усіх бекендах

Усі бекенди, що ділять одну таблицю відповідності, повинні використовувати абсолютно однакові імена для серверів (у нашому випадку LVIV та KYIV). Якщо назвати сервери по-різному (наприклад, lviv-node та kyiv-node), зв'язка сесій перестане працювати, оскільки HAProxy не зможе порівняти ідентифікатори серверів у таблиці.

2. М'яке перемикання при аварії (option redispatch)

Якщо один із серверів виходить з ладу, HAProxy врешті-решт позначить його як DOWN через перевірки працездатності (health checks). Проте клієнти можуть продовжувати надсилати запити на мертвий сервер через збережені старі записи в stick table.

Директива option redispatch змушує HAProxy ігнорувати таблицю відповідності, якщо цільовий сервер недоступний, миттєво перенаправляючи трафік на інший «живий» сервер.

Для сирих TCP-портів (наприклад, порти друку LPR/RAW) також рекомендується додати параметр on-marked-down shutdown-sessions у налаштування рядків серверів. Це дозволить миттєво закривати активні «завислі» TCP-сесії при падінні сервера, змушуючи клієнтів відразу перепідключатися до робочого вузла, не чекаючи таймаутів TCP.


Висновки

Створення високодоступних систем вимагає не лише розуміння синтаксису балансувальника, а й детального знання логіки роботи самого додатка.

HAProxy за замовчуванням балансує індивідуальні TCP/HTTP сесії. Але складні корпоративні платформи, такі як YSoft SafeQ 6, вимагають збереження цілісності сесії між декількома абсолютно різними портами.

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


Схожі замітки