«Чорний понеділок» нашої компанії або як ми пережили 5,5 годин даунтайму та знайшли рішення
Привіт! Я Дмитро Дзюбенко, і вже понад десять років працюю у фінтех-індустрії, з яких вісім — як співзасновник і CTO компанії Corefy. Це B2B SaaS-платформа, що допомагає онлайн-бізнесам керувати всіма аспектами платіжних операцій: здійснювати платежі та виплати, організовувати звітність, а також дає можливість запустити власного платіжного провайдера.
Раніше я ділився досвідом розгортання інфраструктури платіжного процесингу на AWS. Сьогодні ж хочу розповісти про найважчий день в історії компанії Corefy, щоб ви могли навчитися на наших помилках і уникнути подібних ситуацій у себе.
Бекграунд: вибір варіанту реалізації аналітики
У 2018 році Corefy вперше вирушав на WebSummit. Ми розуміли, що, хоча продукт у нас вже був, нічого не зможе краще підкреслити його цінність для потенційних клієнтів, ніж візуалізація процесингових даних за допомогою графіків. Усі люблять дивитися на графіки про гроші, особливо якщо це власні гроші. Тому ми почали шукати можливі рішення реалізації аналітики.
Власний сервіс аналітики
До Corefy ми працювали над іншим проєктом, після якого у нас залишився чудовий дешборд із набором віджетів та можливістю їх кастомізації для користувачів. Спочатку була ідея використати його, однак через велику кількість вимог до кастомізації графіків у нас вийшло б щось на кшталт власної Grafana, тож від цього задуму вирішили відмовитися.
Grafana
Це рішення здавалося ідеальним. Ми навіть створили PoC, який інтегрував відрендерені графіки Grafana в наш дашборд. Графіки вставлялися в нього через Embedded, а через нашу nginx-проксі користувачам передавалися вже відрендерені зображення. Користувач навіть не помітив би, що під капотом працює Grafana. Таке рішення було досить легко масштабувати, проте рівень інтерактивності залишав бажати кращого — це були статичні графіки, в яких неможливо було нічого змінити.
Redash
Цей open-source сервіс був написаний на Python і ми могли досить легко інтегрувати його з нашим продуктом. Крім того, він мав значну перевагу над Grafana — можливість експорту аналітичних даних у форматах .csv та .xls, що дуже зручно для ведення фінансової звітності будь-якої компанії. Сервіс також відмінно відповідав нашим вимогам щодо UX і підтримував сповіщення, які користувачі могли налаштовувати через зручний інтерфейс. Не менш важливим фактором на той момент було те, що його вартість становила 0 USD.
Зваживши переваги та недоліки кожного з цих варіантів, ми вирішили зупинитися на Redash.
Запуск сервісу з Redash
Для того, щоб система могла будувати графіки, необхідно було забезпечити підключення до бази даних. Однак, якщо база даних є спільною для всіх клієнтів, виникає проблема з розмежуванням прав доступу — одна організація не повинна мати доступ до даних інших. Оскільки ми використовували базу даних PostgreSQL, розглядали три можливі варіанти:
- Row Level Security (RLS). Цей інструмент дає змогу динамічно налаштовувати правила доступу до певних рядків даних за допомогою SQL. Наприклад, сапорт може мати доступ до бази даних, але не бачити ордерів на суми понад 100$. Однак RLS працює на рівні даних і вимагає надання доступу на читання всієї схеми БД.
- View. Створення view, у яких фільтрація здійснюється на основі організації, що дозволяє обмежити доступ до даних тільки для певних користувачів.
- Окремі storages. Це було б ідеальне рішення, однак ми не були до нього готові з погляду витрат, організації, складу команди.
Зрештою, ми обрали варіант View. Він дозволяв краще контролювати доступ користувачів до конкретних даних, не розкриваючи оригінальну схему БД, яка є нашою інтелектуальною власністю.
Варто підкреслити, що протягом тривалого часу реєстрація у нас була відкритою, і для кожної нової організації автоматично створювався окремий відповідник у Redash з індивідуальним джерелом даних. Запам’ятайте це.
В результаті спрощено сервіс виглядав так:
Щоб користуватися нашим продуктом, юзер повинен створити обліковий запис, прив’язаний до певної організації. Під час реєстрації організації в дашборді автоматично створюється роль у БД, яка дає доступ користувачу до необхідних даних у схемі. Після цього організація інтегрується з Redash, де налаштовується datasource і з’являються дефолтні графіки.
CREATE FUNCTION create_user(in organization_id text, in password_salt text) RETURNS text AS $$ DECLARE user_password_salt text = password_salt; user_username text = concat('analytics_', lower(organization_id)); user_password text = md5(concat(organization_id, user_password_salt)); BEGIN CREATE SCHEMA IF NOT EXISTS analytics; EXECUTE format('CREATE ROLE %s WITH ENCRYPTED PASSWORD ''%s''', user_username, user_password); EXECUTE format('ALTER ROLE %s WITH LOGIN', user_username); EXECUTE format('GRANT CONNECT ON DATABASE %s TO %s', current_database(), user_username); EXECUTE format('REVOKE ALL ON ALL TABLES IN SCHEMA public FROM %s', user_username); EXECUTE format('GRANT SELECT ON ALL TABLES IN SCHEMA analytics TO %s', user_username); EXECUTE format('GRANT SELECT ON ALL SEQUENCES IN SCHEMA analytics TO %s', user_username); EXECUTE format('GRANT USAGE ON SCHEMA analytics TO %s', user_username); EXECUTE format('ALTER DEFAULT PRIVILEGES IN SCHEMA analytics GRANT SELECT ON TABLES TO %s', user_username); EXECUTE format('ALTER ROLE %s SET search_path TO analytics', user_username); RETURN user_username; END $$ LANGUAGE plpgsql VOLATILE SECURITY DEFINER;
Щоб View розумів, що треба обмежувати доступ, ми використовували таку умову:
create or replace view analytics.table as SELECT id, code FROM methods WHERE (lower((organization_id)::text) = replace((CURRENT_USER)::text, 'analytics_'::text, ''::text));
З погляду кінцевого користувача в дашборді з’явилася кнопка, яка дозволяла перейти до зовнішнього сервісу в межах своєї організації. Там він міг створювати власні віджети, експортувати аналітичні дані у форматі .csv та виконувати інші завдання. Сервіс працював бездоганно, особливо з огляду на те, що на той момент в нашій БД було ще мало даних.
Зі зростанням кількості клієнтів ми розуміли, що подальший розвиток значно підвищить навантаження на базу даних, а її вартість може вирости до, як нам тоді здавалося, космічних масштабів. Тож ми вирішили дослідити можливості self-hosted сервісів.
Наш сетап PostgreSQL
Redash пройшов через усі інфраструктурні зміни, описані в попередній статті, включно з роботою на AWS RDS, AWS RDS provisioned disks, EC2 Patroni cluster, і AWS RDS Aurora.
На початку ми використовували AWS RDS, що забезпечувало базову надійність і масштабованість. Проте зі зростанням навантаження та обсягу даних почали відчувати обмеження цієї конфігурації. Тому вирішили перейти на кластер EC2 з Patroni, який надавав більший контроль над налаштуваннями PostgreSQL і дозволяв забезпечити високу доступність за допомогою автоматичного перемикання на репліки в разі збою.
Patroni — це надійне рішення для управління кластерами PostgreSQL з високою доступністю, яке використовує Etcd або Consul для зберігання конфігурації та координації між вузлами. Ця архітектура дозволила масштабувати нашу базу даних відповідно до потреб бізнесу та забезпечити стабільну роботу сервісу.
На момент інциденту ми працювали саме на кластері EC2 з Patroni. Архітектура нашої платформи виглядала так:
Про Vacuum
У PostgreSQL є такий прекрасний механізм, як Multiversion Concurrency Control (MVCC). Саме він робить PostgreSQL надійним в плані послідовності виконання ваших транзакцій на основі присвоєних їм ідентифікаторів. Завдяки цьому механізму в PostgreSQL зберігається кожна версія рядка, і залишається в таблиці до моменту, поки не буде видалена.
Тому існує така мейнтенанс-процедура як Vacuum, яка вичищає всі старі версії рядків. У неї є дві цілі: скинути ідентифікатори цих транзакцій, щоб запобігти переповненню лічильника транзакцій (wraparound), та звільнити місце, зайняте видаленими або оновленими рядками. Vacuum проходить по таблицях і видаляє рядки, які більше не потрібні, дозволяючи базі даних повторно використовувати цей простір.
Про Wraparound
У PostgreSQL кожна транзакція отримує унікальний ідентифікатор (Transaction ID або XID), який є
Якщо не виконувати регулярний anti-wraparound Vacuum, щоб оновити метадані рядків і помітити старі транзакції як завершені, база даних може втратити можливість визначати, які рядки є видимими для поточних транзакцій. Це може призвести до пошкодження даних або збоїв у роботі бази даних.
Щоб запобігти цим проблемам, PostgreSQL автоматично запускає процеси autovacuum для обслуговування таблиць. Якщо ці процеси не можуть виконатися, наприклад, через вимкнення autovacuum або блокування таблиць, база даних може перейти в режим лише для читання.
Забезпечення надійності сервісу
Для гарантованої стабільності та безперебійної роботи сервісу ми розробили комплексний підхід до забезпечення надійності, який містив такі ключові компоненти.
План відновлення після аварій (Disaster Recovery Plan, DRP) з використанням бекапів та відновлення (Backup-Restore). Ми передбачили регулярне створення резервних копій усіх критичних даних та конфігурацій системи. Ці бекапи зберігалися в захищених сховищах і регулярно перевірялися на цілісність. У разі серйозних збоїв або втрати даних, ми могли швидко відновити роботу сервісу, мінімізуючи час простою та запобігаючи втраті важливої інформації. Важливою частиною DRP було також періодичне тестування процесів відновлення, щоб упевнитися в їхній ефективності та готовності до реальних інцидентів.
Автоматичне перемикання на репліку (Replica Failover). Для забезпечення високої доступності бази даних використовували реплікацію на декілька серверів. Основний сервер бази даних постійно синхронізувався з репліками, що дозволяло зберігати актуальність даних у режимі реального часу. У випадку відмови основного сервера, система автоматично перемикалася на одну з реплік без втрати даних і з мінімальним впливом на користувачів. Такий механізм забезпечував безперервність сервісу та підвищував його стійкість до апаратних або програмних збоїв.
Створення знімків системи (Snapshots). Ми регулярно робили знімки стану наших серверів та баз даних. Знімки дозволяли швидко відкотитися до стабільної версії системи у разі непередбачених проблем, таких як критичні помилки при оновленнях або впровадженні нових функцій. Використання знімків прискорювало процес відновлення окремих компонентів і зменшувало час простою, оскільки не вимагало повного відновлення з бекапів.
Ми були впевнені, що регулярні бекапи, снепшоти та фейловер захистять нас від будь-яких проблем. Але цей інцидент показав нам, що навіть найбільш продумані плани можуть виявитися недостатніми без урахування найгірших можливих сценаріїв.
День інциденту або наш «чорний понеділок»
2 листопада 2020 року — дата, яку ми ще довго памʼятатимемо. Це мав би бути звичайний понеділок, але все пішло інакше.
О 9:00 записи в процесинговій БД перестали оброблятись, але ми про це ще не знали. Повідомлення в загальний чат інцидентів надійшло лише о 9:14.
Одразу після того, як я це прочитав, подумав, що проблема лише в тому, що одна з реплік відстала, і дані на дешборді Grafana просто не оновлюються. Я вирішив, що якщо ми просто перемкнемо datasource в Grafana на primary, все налагодиться. Але нічого не налагодилося, а в логах з’явився такий рядок:
SQLSTATE[54000]: Program limit exceeded: 7 ERROR: database is not accepting commands to avoid wraparound data loss in database "paycore_production" HINT: Stop the postmaster and vacuum that database in single-user mode. You might also need to commit or roll back old prepared transactions, or drop stale replication slots.
Це найстрашніший лог у моєму житті, особливо з огляду на те, що на сервері вже було 2 бази даних по 2 ТБ.
Тож о 9:24 ми вже зрозуміли, що маємо серйозні проблеми, і почали робити все можливе, щоб їх вирішити.
Ми спробували запустити процедуру failover, але з’ясувалося, що дані на всіх репліках та всі снепшоти були пошкоджені на фізичному рівні.
Через години спроб ми вирішили йти іншим шляхом — видалити всі історичні дані. Для відновлення процесингу на той момент нам потрібні були дані конфігурації, дані доступу для роботи з різними системами, збережені карти та клієнти. Тому ми застосували команду truncate table для всіх інших даних, які не були критично важливі на той момент. Це дозволило скоротити обсяг бази даних до 20 ГБ, після чого ми запустили vacuum full, зробили дамп і розгорнули базу на одному з серверів нашого кластеру.
О 14:27 процесинг запустився, і робота була відновлена. Протягом наступного місяця ми поступово, невеликими частинами, переносили ті 2 ТБ даних у робочу БД.
Розслідування інциденту
Нам було важливо зрозуміти ключову причину цього інциденту, особливо враховуючи, що в самому PostgreSQL існують механізми для запобігання таким ситуаціям. Перш за все ми почали перевіряти параметри vacuum і увімкнули його на певних таблицях, де раніше відключили вручну, оскільки використовували pg_repack для запуску vacuum у зручний для нас час.
Саме по собі відключення vacuum на таблицях не могло призвести до таких наслідків. Тому, поки ми відновлювали історичні дані, паралельно шукали в логах причину, чому не спрацював anti-wraparound.
Причини, які нам вдалось ідентифікувати:
- Після переїзду на потужніший кластер БД не було проведено модифікацію параметрів autovacuum для таблиць.
- Не була увімкнена опція log_autovacuum_min_duration, яка б виявляла проблеми із запуском процесу autovacuum.
- Метрики для autovacuum не знімалися.
- При переході на уніфіковану систему моніторингу pgwatch ми втратили метрики по dead_tuples/live_tuples.
Ліквідація наслідків
Після аналізу причин простою тривалістю п’ять з половиною годин ми розробили план усунення наслідків та впровадили заходи, які б допомогти запобігти подібним інцидентам у майбутньому, зокрема:
- Відновлення даних для забезпечення нормальної роботи клієнтів.
- Виведення даних про dead_tuples/last_autovacuum у систему моніторингу.
- Налаштування кожної процесингової таблиці з індивідуальними опціями для процесу autovacuum.
- Налаштування логічної реплікації на одну БД.
- Розробка плану повного відновлення роботи системи у разі виходу з ладу БД.
- Впровадження культури моделювання інцидентів.
- Розподіл клієнтів на різні групи інфраструктур (що є особливо важливо для B2B SaaS-бізнесу).
Також, щоб запобігти подальшим проблемам на фізичному рівні, ми вирішили створити ще одну логічну репліку. У випадку повторення подібної проблеми, навіть якщо ми не знатимемо її точну причину, це дозволить мати додатковий невеликий сервер із логічною реплікацією таблиць для швидкого відновлення процесингу. У майбутньому це рішення допоможе автоматизувати дії, які під час інциденту ми виконували вручну.
Тоді це здавалося нам єдиним здоровим рішенням і досить довго ми з ним жили.
Хто винен
У той час ми так і не змогли встановити ключову причину цього серйозного інциденту, оскільки були зосереджені на більш пріоритетних завданнях — відновленні роботи та стабілізації системи. Лише через кілька років нам вдалося дійти до істини.
Готуючись до виступу на Highload fwdaysʼ24, я вирішив провести власне розслідування, в ході якого переглянув усі Release Notes PostgreSQL, які були актуальні на момент інциденту. І саме там, у версії 13.10, я знайшов справжню причину — помилка «wrong tuple length» наприкінці виконання Vacuum виникала, якщо Vacuum потрібно було оновити значення datfrozenxid поточної бази даних, а база даних мала стільки наданих привілеїв, що її значення datacl було винесено за межі основної таблиці.
Як я вже згадував, реєстрація користувачів у нас була відкрита, і з часом їх кількість перевищила 600. Саме це і стало критичним моментом.
У цьому тестовому середовищі за допомогою Docker compose ви зможете відтворити проблему, підставивши версію PostgreSQL 13.9.
Висновки: сподівайся на краще, готуйся до гіршого
Маючи дві репліки і WAL-и, ми були впевнені, що наш Recovery Point Objective становить лічені секунди. Втрата даних для нас була неприпустимою, і, по суті, цього не сталося.
Проте, переживши той інцидент, усвідомили свій найбільший прорахунок — ми не мали уявлення, скільки часу займе відновлення бази даних розміром у 2 ТБ з бекапу, бо завжди використовували снепшоти й доливання даних з wal-g з S3. Лише згодом зрозуміли, наскільки важливо регулярно перевіряти Recovery Time Objective, особливо з урахуванням зростання обсягу баз даних.
Тож завжди сподівайтесь на краще, але готуйтесь до гіршого.
Зима близько — збираємо на обігрівачі
Дякую кожному, хто дочитав статтю до цього моменту, і запрошую вас долучитися до збору на обігрівачі, аби разом подякувати захисникам, які дають нам можливість продовжувати працювати й обмінюватися досвідом.
Скоро настануть холоди, до яких потрібно бути готовими, тож ми збираємо гроші на три автономні обігрівачі Webasto для ЗСУ.
Посилання на банку: send.monobank.ua/jar/56w6JJkA31
Номер картки банки: 5375 4112 2267 6913
Ще раз дякую і залишаємось на звʼязку!
18 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів