Як одна невелика помилка може заблокувати прод і що з цим робити
Привіт! Мене звати Микита Савін, я DevOps Infrastructure Architect в P2H. У березні цього року пройшла онлайн-конференція DevOps fwdays’23, яка присвячена DevOps практикам та інструментам. Серед спікерів були розробники та інженери компаній SoftServe, Spotify, Luxoft, Snyk, Xebia | Xpirit, Solidify, Zencore, Mondoo та інші.
Я виступив з доповіддю про те, як одна невелика помилка може заблокувати прод і що з цим робити — «How we block production. Triangulate issue, fix and postmortem».
Ділюсь з вами цим кейсом, висновками і порадами про те, що робити у таких випадках і сподіваюсь, що ця інформація стане вам у пригоді (але краще, щоб ні).
Коротко про продукт
Компанія розробляє E-Government платформу для замовника на Близькому Сході. Робота платформи пов’язана з ринком праці, а цільова аудиторія продукту — громадяни та бізнеси країни.
Розробка працює вже кілька років і постійно змінюється та доповнюється, обростаючи новими сервісами. Платформа базується на асинхронній архітектурі з огляду на особливості роботи з точками інтеграції в урядових організаціях країни.
Tech Stack та процеси
- Microservice architecture.
- Front end: Vue.js, React.js.
- Back end: Ruby, Ruby on Rails, Java, PHP.
- Message broker: RabbitMQ.
- Global cache: Elasticsearch.
- Infrastructure: Docker.
- Monitoring, observation, and tracing: Grafana, Grafana Loki, Grafana Tempo, Prometheus, OpenTelemetry, Vector.
- Integrations: IBM APP Connect, IBM API Connect, Absher, Unifonic, mada, SADAD, and more.
Проєкт базується на мікросервісній архітектурі. Зараз в продакшені понад сто різних мікросервісів і більшість з них написана на Ruby. Нові мікросервіси стартують на Java.
Для організації асинхронності проєкту був обраний патерн Enterprise Service Bus (ESB) і Message broker RabbitMQ. Storage level побудований на базі Elasticsearch та PostgreSQL, а інфраструктура — Docker, Docker Compose та внутрішній провайдер країни, щоб забезпечити вимоги data licality державного регулятора.
Для моніторингу використовується Grafana Stack та велика кількість точок інтеграції з різними міністерствами та приватними установами. RabbitMQ функціонує як кластер з чотирьох нодів, які доступні через load balancer prod-rabbit-new-lb.
Поява проблеми і дії команди
Проблема була знайдена за допомогою автоматичних «алярмів» на Prometheus Alarms, а саме:
- Average page processing time fired;
- Number of gateway timeouts fired.
Операційна команда одразу перевірила «алярми» в ручному режимі — сайт працював дуже повільно. Тому був відкритий інцидент і сформована war room, що включав operation команду, L3 support та представників власника сервісу.
Проблема стрімко погіршувалась, бо після 15 хвилин з моменту виявлення сервіс практично перестав реагувати і власник сервісу був змушений перемикнути його в maintenance mode та обмежити використання для клієнтів.
Тріангуляція
Як ви розумієте, пошук був досить напруженим. Для роботи з подібними проблемами в нас був короткий чекліст з досить очевидними, але ефективними пунктами.
Anamnes checklist:
- Чи змінювали ми щось нещодавно в системі?
- Чи були в нас нещодавно якісь деплойменти?
- Чи нормально працює RMQ і чи немає в нас перенавантажень?
- Чи помічали ми нещодавно щось незвичайне в логах чи моніторинговій системі?
- За необхідності перевірити всі елементи системи по черзі.
Оскільки архітектура проєкту базується на ESB, він дуже чутливий до особливостей роботи RMQ, перевірка якого стоїть одним з перших пунктів в чеклісті. Якщо ж RMQ працює добре, варто перевірити, чи немає перевантажень і які саме ресурси використовуються.
Ми одразу звернули увагу на те, що load balancer для RMQ використовує досить великий обсяг трафіку, що не є нормою. В нормальному режимі load balancer використовує близько 30 Mbit трафіку в секунду, то тут використовувалось 300 Mbit.
Спочатку ми не звернули увагу на те, що цифра дуже «рівна» і витратили час на (досить гарячковий) пошук по системі: хто генерує трафік у RMQ, звідки сиплються повідомлення і чому цього не видно з моніторингу.
Витративши хвилин 20 часу на пошук джерела трафіку, ми знову повернулися до лоадбалансера і зацікавилися тим, що протягом 20 хвилин трафік продовжував бути 300 Мбіт в обидві сторони.
Перевірили характеристики порту — бінго! Порт на 200 Мбіт. Хтось якимось чином виїдає всю смугу пропускання на мережевому порту лоадбалансера! Таким чином ми визначили, що щось використовує усю смугу пропускання для RMQ і це одна з проблем, що спричинили відмову в обслуговуванні системи.
Починаємо перевіряти з цієї точки: кількість повідомлень в чергах виглядала нормально, але кількість повідомлень, взятих з черг і поставлених в чергу, була близько нуля.
Тобто кластер RMQ був зайнятий чимось, що використовує усю смугу пропускання, але нові повідомлення в чергу не ставляться, а старі не видаляються. Виглядало це все як якийсь cache poisoner, тому ми почали копати глибше.
В логах нічого незвичайного не було, але в контрольній панелі RMQ була трохи збільшена кількість повідомлень, що очікують підтвердження споживачів, на що ми звернули увагу.
Як ви знаєте, в RMQ є декілька типів повідомлень. Ми використовуємо тип з підтвердженням отримання з боку клієнта, тобто клієнт опрацьовує меседж і інформує про це RMQ.
Якщо з якоїсь причини клієнт не підтвердить опрацювання для RMQ, то RMQ не видалятиме меседж з черги, а через деякий час повертає його назад у чергу та віддає іншому клієнту. Це мало б гарантувати, що повідомлення не буде втрачено і буде кимось опрацьоване.
Саме так і виглядала наша ситуація, тому для перевірки гіпотези ми вирішили знайти сервіс, який це робить.
Рішення проблеми
Після детального вивчення Node exporters, був знайдений instance, який генерує незвичний обсяг трафіку, який метчиться з трафіком, що надходить на load balancer RMQ. В цьому інстансі було знайдено групу контейнерів, які постійно рестартували, що спричиняло ж і постійний рестарт сервісу.
Після ще детальнішого вивчення ми виявили, що сервіс не рестартував, а вбивався через OOM Killer (out-of-memory killer) і вже потім рестартував автоматично. Оскільки сервіс було створено на Ruby, який за замовчуванням у фреймфорку Sneakers робить prefetch декількох повідомлень з RMQ.
А виглядає це так, наче сервіс робить pregetch, повідомлення дуже великі, вони знаходяться в пам’яті, яка перевищує ліміти Докера і контейнер вбивається. Після чого з’єднання з RMQ втрачається, RMQ поміщає попередньо вибрані (prefetched) повідомлення знову в чергу і віддає їх новому сервісу після рестарту, але якомусь іншому, який намагається читати з цієї черги.
У зв’язку з тим, що таких повідомлень набралась достатня кількість, ми отримали отруєння кешу в RQM, вся смуга пропускання якого була зайнята опрацюванням prefetched повідомлень і поверненням їх назад у чергу.
Таким чином:
- Короткострокове рішення — у ручному режимі збільшити обсяг лімітів пам’яті на Docker, після чого система почала працювати і відновилась за 10 хвилин, а інцидент з моменту появи було вичерпано приблизно за півтори години.
- Довгострокове рішення — вивчити сервіс, знайти місце, де була генерація повідомлень великого розміру і пофіксити це. Рішення було реалізоване у той же день.
Висновки
Щоб запобігти подібним проблемам у майбутньому, ми покращили систему моніторингу та почали збирати метрики з того, що:
- якийсь сервіс дуже часто рестартує з посилкою «аларм»;
- якийсь контейнер в Docker було вбито через OOM killer;
- якийсь процес у системі був вбитий через OOM killer.
А також збір метрик щодо розміру пакета в RMQ та генерація алармів, якщо пакет занадто великий.
З того часу ми більше не стикались з подібними проблемами, метрики у рестарті контейнера OOM killer припинились для пошуку інших проблем в системі і всі жили довго та щасливо.
15 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів