dnsbalancer — II

У попередньому записі я розповів про основні концепції, які ми використовуємо для балансування DNS-запитів від клієнтів. Тепер давайте подивимося, що змінилося за чотири місяці.

А за цей час змінилося три речі.

По-перше, виросло навантаження. Переважна більшість клієнтів перелізла на нові DNS-сервери, і тепер через primary-балансувальник ми пропускаємо 14,5 тисяч запитів на секунду, а сумарно через обидва — близько 25 тисяч (невеликий пік був від DDoS, із яким балансувальники успішно впоралися без будь-яких тригерів). Ресурси віртуальних машин при цьому не додавалися і не забиралися. Після вимкнення старих рекурсорів до цього навантаження додасться ще 2 кілопакета (хом’ячки зі статикою, з якими ми потихеньку боремося). Асинхронна концепція обробки запитів не змінювалася — ті самі воркери, по одному на процесорне ядро, ядро ОС балансує запити між ними завдяки SO_REUSEPORT, клієнтський conntrack вимкнений, запити до рекурсорів надсилаються з фіксованих портів (дякуючи підтримці connect() для UDP).

По-друге, я таки змінив схему збереження відповідності «клієнт-рекурсор». Тепер хеш-таблиця не використовується. Натомість, для кожного запиту просто підставляється новий ID з лінійного інкременту і запам’ятовується відповідність двох ідентифікаторів у таблиці. Хеш CRC64 при цьому все ще використовується на випадок, якщо в черзі збереться більше 65536 запитів (highload!), але перший крок вибору домена колізій відбувається без підрахунку хеша. Схему з підстановкою ID я піддивився у конкурентів — dnsdist від PowerDNS. Однак ця штука ще не допиляна, вона не вміє багатопоточність і більше 65 кілозапитів. Тому ми тут зі своїм рішенням попереду всієї планети (звісно, серед тих опенсоурсних проектів, які я зміг нагуглити).

По-третє, я прикрутив функцію фільтрації запитів. Нам це знадобилося для того, щоб, по-перше, ховати локальні зони від зовнішніх запитів (оскільки PowerDNS не вміє views), а, по-друге, щоб відбивати DDoS. В ACL можна указати, що отакі-от домени (за точним збігом, з піддоменами чи за регекспами, а також за типом запитів — так ми, наприклад, ріжемо всі ANY-запити) ми не обслуговуємо з таких-то адрес. Замість простого дропа можна послати у відповідь NXDOMAIN або навіть специфічну IP-адресу, якщо хочеться завернути трафік на певну заглушку. Загалом, потужна і гнучка схема. Тут, звісно, є ще простір для покращень: по-перше, потрібна інтеграція з MySQL (щоб правила можна було міняти «на льоту»), по-друге, я не знаю, як воно масштабуватиметься, якщо таких правил буде тисяча. Але покладені функції зараз схема успішно виконує.

Я дуже люблю графіки. Тому додатково я прикрутив різну корисну статистику. Наприклад, можна дивитися затримки в обробці DNS-запитів. Якщо взяти primary-балансер, графік виглядає якось так:

lats

Великий пік — запити, видані з кеша рекурсора, тому там затримка близько чверті мілісекунди. Малий пік справа — це запити не з кеша, тому на них і витрачаються десятки мілісекунд. Як бачите, кеш рекурсорів надзвичайно ефективний, незважаючи на те, що зараз він у нас живе всього 30 хв.

Також можу показати графік інтенсивності запитів за добу з первинного балансера:

reqs

Ми на одному графіку тут малюємо кількість пакетів на вхід і на вихід. Там, де виникають диспропорції — це працюють ACL. Нас останній тиждень намагаються DDoS-ити через відкриті рекурсори клієнтів, і комплексна боротьба з цим явищем успішна, але ще в процесі.

Нагадую, код балансера відкритий (GPLv3-only), а документація по ньому завжди актуальна.

UPDATE 1: забув згадати про один недолік LDNS, який виплив після того, як я замінив хештаблицю простою підстановкою ID. Спочатку підстановка виконувалася силами LDNS — пакет розбирався на частини, викликалася відповідна функція LDNS, пакет збирався назад. Усе працювало добре на тестовому стенді, а після викатування оновлення на робочі сервери в частини клієнтів почалися чудеса. Виявилося, що LDNS не підтримує компресію відповідей (це така штука, яка дає змогу не дублювати FQDN у кожному RR, а просто проставляти двобайтові зсуви на спільний для всіх RR FQDN, що сильно економить трафік, зменшуючи розмір пакета), і тому UDP, який прийшов від рекурсора і займав менше 512 байт, але містив багато RR у відповіді (наприклад, 20), після перепаковування мав розмір близько 700 байт, що порушує RFC. Від цього деяким маршрутизаторам зносило голову (у частини полікувалося оновленням прошивки, у частини не лікувалося нічим, наприклад, у Apple AirPort), і переставали працювати деякі ресурси (наприклад, Viber). На графіках така поведінка виглядала значною диспропорцією вхідного і вихідного трафіку, що мене спочатку здивувало, але я не звернув на це належної уваги. Полікувалося це тим, що я обійшовся на цьому етапі без функцій LDNS і став записувати новий ID прямо в сирий пакет (через htons(), звісно). ID у DNS-пакеті завжди займає перші два байта, тому це зробити дуже легко.

Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *

*

Цей сайт використовує Akismet для зменшення спаму. Дізнайтеся, як обробляються ваші дані коментарів.