Вибір MQ для високонавантаженого проекту
Сучасні системи, що масштабуються, складаються з мікросервісів, кожен з яких відповідає за своє обмежене завдання. Така архітектура дозволяє не допускати надмірного розростання вихідного коду та контролювати технічний обов'язок.
У нашому проекті десятки мікросервісів, кожен з яких зарезервований: дві або більше абсолютно ідентичні копії сервісу встановлені на різних фізичних серверах, і клієнт (інший мікросервіс) може звертатися до будь-якої незалежно.
Якщо мікросервіс перестає відповідати на запити внаслідок аварії, його клієнти мають бути миттєво перенаправлені на резервний. Для управління потоком запитів часто використовують звані черги повідомлень (message queues).
Черга, що нещодавно використовується, перестала нас влаштовувати за параметрами відмовостійкості і ми замінили її. Нижче ми ділимося досвідом вибору.
Перевірене рішення
Лідером популярності у розробників є RabbitMQ. Це перевірене часом рішення класу Enterprise з гарантіями доставки, гнучкою системою маршрутизації та підтримкою різних стандартів. Керівники проектів люблять його, як на початку 80-х покупці комп'ютерів любили IBM PC. Це кохання найбільш точно виражається фразою “Nobody ever got fired for buying IBM.”
Нам RabbitMQ не підійшов тому, що він повільний та дорогий в обслуговуванні.
Продуктивність RabbitMQ не перевищує десятки тисяч повідомлень за секунду. Це добрий результат для багатьох застосувань, але зовсім незадовільний для нашого випадку.
Конфігурувати кластер RabbitMQ непросто, це забирає цінні ресурси devops. Крім того, ми краєм вуха чули про нарікання по роботі кластера - він не вміє зливати черги з конфліктами, що виникли в ситуації split brain (коливнаслідок розриву мережі утворюються два ізольовані вузли, кожен з яких вважає, що він головний).
Черга з урахуванням розподіленого лога
Ми подивилися на Apache Kafka, яка народилася всередині компанії LinkedIn як система агрегації логів. Kafka вміє вичавлювати більшу продуктивність із дискової підсистеми, ніж RabbitMQ, оскільки вона пише дані послідовно (sequential I/O), а не випадково (random I/O). Але жодних гарантій, що запис на диск завжди буде послідовним, отримати не можна.
У Kafka дані діляться за розділами (partition) і щоб дотримуватися порядку доставки кожен одержувач повідомлень читає дані з одного розділу. Це може призвести до блокування черги у випадку, коли одержувач з будь-яких причин обробляє повідомлення повільніше, ніж звичайно.
Крім того, для управління кластером Kafka потрібен окремий сервіс (zookeeper), що знову ж таки ускладнює обслуговування та навантажує devops.
Ми не готові ризикувати в production втратою продуктивності, тому продовжили пошук.
«Гарантована» доставка повідомлень
Є чудова табличка від Jeff Dean, ветерана Google (працює там із 1999 року):
Видно, що запис на диск в 15 разів повільніше відправки по мережі.
Це вірно, тільки гарантії немає. Адже якщо відправник упаде в момент передачі повідомлення або впаде сам процес черги до запису на диск, то повідомлення пропаде. Виходить, черга тільки створює ілюзію гарантії доставки, а повідомлення, як і раніше, можуть губитися.
Високопродуктивні черги
Щоб не втрачати повідомлення всередині процесу черги можна просто … прибрати процес черги та замінити його бібліотекою, що вбудовується у процес мікросервісу.
Багато розробників знайомі збібліотекою ZeroMQ. Вона показує фантастичну швидкість, перетравлюючи мільйони повідомлень за секунду. Однак у ній (з ідеологічних причин) немає вбудованих засобів моніторингу та управління кластером, тому при її використанні навантаження на devops ще вище. Ми продовжили шукати більш практичні варіанти.
Черга на СУБД?
У якийсь момент ми майже зневірилися і нам здалося, що простіше вже написати чергу самим поверх СУБД. Це може бути SQL база даних або одне з численних NoSQL-рішень.
Наприклад, Redis має спеціальні функції для реалізації черг. Оскільки Redis зберігає дані у пам'яті, продуктивність прекрасна. Цей варіант був розумним, але бентежило, що надбудова Sentinel, призначена для об'єднання кількох вузлів Redis в кластер, виглядала дещо штучно, ніби приробленою збоку.
З використанням класичної СУБД для отримання повідомлень довелося б використовувати техніку “long polling”. Це негарно і загрожує затримками в доставці. Та й не хотілося писати навколішки.
Інтуїція підказувала, що ми не перші шукаємо чергу з розумними вимогами до продуктивності та простоти адміністрування, і у 2017 році у нашого завдання має бути готове рішення.
Рішення знайдено: NATS
NATS - відносно молодий проект, створений Derek Collison, за плечима якого понад 20 років роботи над розподіленими чергами повідомлень.
За продуктивністю NATS випереджає всі черги з гарантованою доставкою. NATS написаний мовою Go, але має клієнтські бібліотеки всім популярних мов. Крім того, клієнти NATS також знають топологію кластера та здатні самостійно перепідключатися у разі втрати зв'язку зі своїм вузлом.
Результати використання у production
Оскільки NATS не записує повідомлення на диск, сервіси-отримувачі повинні акуратно завершувати свою роботу - спочатку відписуватися від нових повідомлень, потім обробляти отримані раніше і потім зупиняти процес.
Сервіси-відправники повинні у разі помилок повторювати спробу надсилання повідомлення. (Втім, це не специфічно для NATS і так потрібно робити під час роботи з будь-якою чергою).
Щоб напевно знати про втрачені повідомлення, ми зробили найпростіше логування: записуємо час відправки і час отримання.
Ми встановили процес NATS на всі віртуальні машини із мікросервісами. За результатами спостереження протягом 2 місяців NATS не втратив жодного повідомлення.
Ми задоволені своїм вибором. Сподіваємося, наш досвід виявиться корисним для вас.
Хардкорна конфа за С++. Ми запрошуємо лише профі.