Як ми змігрували 300 мільйонів рядків у PostgreSQL

Привіт, мене звати Олександр Прокопович, я Go Engineer у Solidgate — українській продуктовій фінтех-компанії, що працює в ніші пейментів і допомагає розбудовувати платіжну інфраструктуру для інтернет-бізнесів.

Ця стаття була написана як ретроспектива нашого досвіду мігрування великої кількості даних. Ми дослідимо різні підходи до міграції, вимоги, типи даних, а також розкажемо, як ми з’ясували, що UPDATE даних суттєво складніший за INSERT чи DELETE. Якщо ви плануєте проводити великі міграції даних або хочете поглибити свої знання в PostgreSQL — цей матеріал для вас.

Передумови

Під час мажорного рефакторингу одного з наших сервісів, ми помітили деякі неоптимальні патерни доступу до даних нашої бази. Розглянемо приклад, який імітує поведінку, що у нас була. Існує три сутності — user, transaction і transaction_details. Сутність деталей транзакції прилінкована до транзакції, а транзакція до користувача. Тож, за наявності деталей транзакції, щоб дістати з бази дані про користувача, потрібно спочатку з деталей дістати транзакцію, і лише тоді з транзакції взяти user_id.

Для нас оптимізація подібних патернів доступу суттєво спрощувала життя — замість трьох запитів могли робити лише один. Тому вирішили денормалізувати дані деяких таблиць з метою підвищення швидкості доступу до ряду сутностей. Наша міграція оновлювала багато полів, але задля спрощення розглядатимемо в прикладах запитів лише оновлення поля user_id в таблиці transaction_details шляхом перенесення його значення з таблиці transactions.

Короткий опис того, що ми мали, і якими були наші вимоги до міграції:

  • База, в якій проводилася міграція, знаходилася під постійним навантаженням і обслуговувала live-трафік ~20rps.

  • Важливо було збалансувати навантаження на базу, аби уникнути downtime критичних ендпоїнтів, які обробляють платежі. Водночас ми не мали жорстких часових обмежень виконання міграції (допоки міграція не потребувала використання робочого часу інших інженерів). Швидке рішення було для нас краще за те, що було б ідеальним рішенням, але при цьому залучало б більший людський ресурс.
  • У нас не було змоги апргейднути, а потім даунгрейнднути базу, але ми додали максимальну кількість provisioned IOPS для нашого диска.
  • Поля, які ми планували оновити (user_id), модифікувалися в target-table (transaction_details) лише під час створення рядка і потім лишалися незмінними, а в origin-table (transactions) були іммутабельними, тож нам не потрібно було хвилюватися за data race.
  • Zero data loss — ми не могли зупинити запис даних, змігрувати їх і потім знову запустити міграцію.

Підготовка

  1. Перш за все ми почали заповнювати новостворені поля даними. Таким чином зафіксувавли загальну кількість рядків усіх таблиць, які нам потрібно було оновити на позначці близько 320 мільйонів, з найбільшою таблицею, яка містила 140 мільйонів записів.
  2. Деякі конфігураційні дані, які нам були потрібні, лежали в іншій базі. Оскільки ці дані були іммутабельні, а нові записи нам були нецікаві (бо вони вже автоматично донасичувалися даними), для прискорення міграції ми повністю скопіювали потрібну таблицю з однієї бази в іншу.

Дослідження підходів до міграції

AWS DMS

Ми використовуємо AWS як cloud-provider для наших сервісів. Одним із продуктів AWS є сервіс для міграції даних між базами — DMS (Data Migration Service). AWS DMS дозволяє зробити міграцію між гомогенними базами — Full Load and CDC, яка полягає в копіюванні основного чанку даних у базі й наступного застосування всіх змін, які відбулися в базі через CDC. Під час цієї міграції можна налаштувати тригери, які модифікуватимуть дані — у результаті ми отримаємо готову копію початкової бази, але з усіма потрібними змінами.

Переваги:

  • швидкість виконання скрипта міграції;
  • якщо в планах є апгрейд бази, можна одночасно виконати і апгрейд, і міграцію, зекономивши час.

Недоліки:

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

Own wheel-reinvented DMS

Якщо підхід із міграцією основного чанка і дозаповненням даних через CDC працює, але при цьому апгрейдити базу не потрібно, то навіщо тоді DMS, якщо можна виконати ті самі операції над наявною базою? Розглянемо приклад, як могла б виглядати така міграція:

-- запамʼятовуємо latest_updated_at;
SELECT updated_at FROM origin_table ORDER BY updated_at DESC LIMIT 1;
-- копіюємо таблицю або наступним способом, або через pg_dump/pg_restore, тут відбуваються всі наші модифікації даних;
CREATE TABLE origin_table_copy FROM (SELECT * FROM origin_table);
-- створюємо індекс на id
-- створємо тригер на insert/update/delete який писатиме з origin_table в origin_table_copy;
-- інкрементально відновлюємо всі змінені дані;
UPDATE origin_table_copy SET x=y, ... WHERE updated_at BETWEEN latest_updated_at AND latest_updated_at + (10 * interval '1 minute');
-- відновлюємо всі констрейнти, створюємо індекси, рестартуємо sequences;
-- перемикаємо трафік між таблицями

Переваги:

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

Недоліки:

  • водночас компонентів, які потрібно протестувати, суттєво більше, ніж у DMS. Також значно складніша логіка самої міграції, відповідно — більше шансів на помилку.

Batch Updates

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

Спочатку ми спробували доволі тривіальний і прямолінійний підхід — в одній sql-транзакції агрегувати всі дані та одразу оновлювати таргетну таблицю:

CREATE INDEX tx_details_user_id_null_idx
    ON transaction_details (id)
    WHERE user_id IS NULL

UPDATE transaction_details td
SET user_id = cte.user_id
FROM (SELECT t.user_id, td.id
      FROM transactions t
  JOIN transaction_details td ON td.transaction_id=t.id
      WHERE td.user_id IS NULL
      LIMIT _limit
) AS cte
WHERE td.id = cte.id;

Це працювало дуже повільно, швидкість була приблизно 3k rows/min. Нескладна математика підказує — у такому темпі міграція однієї таблиці зайняла б близько 50 днів (за умови, що ми вимикали б міграцію на ніч). Перша проблема запиту вище в тому, що ми витрачаємо час на виконання JOIN. Щоб уникнути цього, створили допоміжну таблицю, в яку додали преагреговані дані:

CREATE TABLE tmp AS
SELECT ROW_NUMBER() OVER() AS row_id,
       td.id AS td_id,
       t.id AS t_id
FROM transaction_details td
JOIN transactions t ON td.transaction_id = t.id
WHERE td.user_id IS NULL;

CREATE INDEX tmp_idx ON tmp(row_id);
-- і відповідно міграція:
UPDATE transaction_details td
SET user_id = COALESCE(td.user_id, cte.user_id)
FROM (SELECT user_id,
             td_id
      FROM tmp
      WHERE row_id > 200000 AND row_id <= 210000) AS cte
WHERE td.td_id = cte.td_id;

Друга проблема, з якою ми зіткнулися, була менш очевидною, але не менш вагомою. Річ у тім, що індекс tx_details_user_id_null_idx не пришвидшує, а сповільнює UPDATE, оскільки при оновленні рядків таблиці цей індекс також потрібно оновлювати. Відтак переваги швидкого читання таблиці нівелюються повільним оновленням індексу. Тому ми його дропнули. Тепер оновлення даних відбувалося зі швидкістю ~5k rows/min, проте ця швидкість нас все ще не влаштовувала.

Ми спробували ще декілька доволі сміливих ідей: підчищати dead tuples вакуумом після кожної ітерації (це майже ні на що не вплинуло), погратися з налаштуваннями постгресу temp_buffers, work_mem, effective_cache_size (також мінімум імпакту). Врешті-решт, через декілька ітерацій змін ми прийшли до фінальної версії, швидкість якої нас задовольняла — 30k rows/min. Але про все по черзі.

Спочатку створюємо таблицю, в якій агрегуємо всі потрібні нам дані:

-- пре-агреговуємо дані, потрібні для апдейту
CREATE TABLE tmp AS
SELECT td.id         AS td_id,
       t.user_id         AS user_id,
       td.updated_at AS updated_at
FROM transaction_details td
JOIN transactions t ON t.id = td.transaction_id
WHERE td.user_id IS NULL;
-- створюємо індекс, аби швидко вичитувати перші N рекордів і потім дропати їх
CREATE INDEX tmp_idx ON tmp (updated_at, td_id) INCLUDE (user_id)

Ми зʼясували, що важливий аспект — це сортування індексу по updated_at. Під час оновлення даних наш ідеальний сценарій — читати всі дані з таблиці, які будуть оновлені, в тій послідовності, в якій вони лежать на диску. Це дає змогу зменшити overhead на нього. В PostgreSQL операція UPDATE — це INSERT+DELETE, тому при апдейті нові версії рядків можуть бути записані в інший регіон памʼяті, ніж поточні. Як наслідок — ймовірність того, що рядки з близьким значенням updated_at лежатимуть близько, вище за будь-які два довільних рядки, відсортовані за UUID.

Варто звернути увагу, що це має бути саме updated_at, а не created_at. Якщо fillfactor таблиці — 100% (default value), то апдейти даних із високою ймовірністю потраплятимуть на іншу table page, і дані, що лежать близько, ризикують знаходитися далеко один від одного після створення. Також ми робили спроби сортувати за ctid, втім, як не дивно, це виявилося значно повільнішим за updated_at.

При оновленні даних оновлюються й індекси, які референсять стару версію цих даних. Відтак, якщо є змога, доцільно дропнути якомога більше індексів таблиці, яка оновлюється, і відновити їх після міграції. Ми помітили приблизно 10%-30% покращення у швидкості UPDATE операції після видалення декількох застарілих індексів.

Для прикладу швидкість виконання запитів на клоні з та без жодного індексу:

With Indexes

W/O Indexes

Batch Size 1000

36 000

111 000

Щоб зменшити overhead апдейтів, MVCC може використовувати HOT (Heap-Only-Tuples) оптимізацію. Для цього потрібно, аби апдейт відповідав певним умовам:

  • апдейт не модифікує колонки, які використовуються в індексах таблиці;
  • на table page, яка містить стару версію рядка, достатньо місця для нової версії рядка.

За другий пункт відповідає fillfactor — тому якщо в роботі з базою переважає update-heavy workload, доцільно поміркувати про створення таблиць із fillfactor <70-90%. Це покращить швидкість оновлення даних і під час міграції.

Наша міграція виглядала наступним чином:

DO $$
    DECLARE
        _id int := 0;
        _rowsLimit INT := 3000;
        _updatedTotal INT := 0;
        _updatedInBatch INT;
        start_time timestamp := CLOCK_TIMESTAMP();
        update_time INT;
        execution_time INT;
    BEGIN
        -- це налаштування нам потрібно було для того аби вимкнути тригери на таргетній таблиці
        SET session_replication_role='replica';

        LOOP
            RAISE NOTICE 'Started Iteration: %', _id;
            -- ці рядки з cte можна було записати в змінну і перевикористати під час delete-операції, але ми вчасно не звернули на це увагу, бо delete-операції було доволі далеко до нашого bottleneck-у.
            WITH cte AS (SELECT td_id, user_id
                         FROM tmp
                         ORDER BY updated_at
                         LIMIT _rowsLimit
            )
            UPDATE transaction_details td
            SET user_id = COALESCE(td.user_id, cte.user_id)
            FROM cte
            WHERE td.id = cte.td_id;

            COMMIT;

            GET DIAGNOSTICS _updatedInBatch = ROW_COUNT;
            _updatedTotal := _updatedTotal + _updatedInBatch;
            update_time := EXTRACT(EPOCH FROM clock_timestamp() - start_time);
            RAISE NOTICE 'UPDATE executed time: % sec.', update_time;

            WITH cte AS (SELECT td_id, updated_at
                         FROM tmp
                         ORDER BY updated_at
                         LIMIT _rowsLimit
            )
            DELETE FROM tmp t
                USING cte
            -- порівняння по `updated_at` потрібно для того аби підхопився індекс
            WHERE t.updated_at = cte.updated_at
              AND t.td_id = cte.td_id;

            execution_time := EXTRACT(EPOCH FROM clock_timestamp() - start_time);
            RAISE NOTICE 'Finished Iteration: %, updated total: %, ALL time: % sec.', _id, _updatedTotal, execution_time;

            COMMIT;

            IF _updatedInBatch = _rowsLimit THEN
                PERFORM pg_sleep(0.5);
            ELSE
                RAISE NOTICE 'All IDs were updated. Exit.';
                EXIT;
            END IF;

            _id := _id+1;
        END LOOP;
    END $$;

Тестування

Query planner враховує стан таблиці й бази, тому відповідно будь-який аналіз запитів має виконуватися на реальних даних у продовій базі. Оскільки на проді завжди є ризик щось зламати, ми підняли повноцінний клон нашого прода і виконували дослідження всіх гіпотез саме на ньому.

Крім того, ми протестували фрагменти міграції через функціональні тести в коді, аби переконатися, що вони виконують саме те, що нам потрібно. Перед початком міграції на проді ми запустили її на наших тестових середовищах і верифікували, що дані мігруються правильно.

Моніторинг та Performance Tuning

Під час міграції важливо памʼятати про domain-specific data access patterns.

  • Чи характерні для сервісів, які працюють з базою спайки трафіку? Якщо так, потрібно поміркувати над зменшенням швидкості міграції даних.
  • Чи трапляються вночі синхронізації даних, які створюють додаткове навантаження на базу? Можливо, варто вимикати міграцію на ніч.
  • Як та коли відбувається вивантаження даних для DWH чи кешу і чи може міграція вплинути на це? У нас була певна логіка CDC, побудована на тригерах — її довелося вимкнути, аби не покласти сервіси, що нею користуються (кожного дня оновлювалося приблизно 20 мільйонів рядків).
  • За скільки буде виконуватися апдейт одного батчу? Оскільки ми оновлюємо в батчі тисячі значень, кожен рядок буде залоканий FOR UPDATE. Чи може це негативно вплинути на конкурентні запити?
  • Чи є сутності, які користуються базою/таблицею, даунтайм яких напряму впливає на виручку компанії? Для нас такими сутностями були ендпоїнти, які відповідають за створення платежів. Під час міграції важливо визначати такі чутливі місця і відстежувати, чи не падають запити по таймауту.
  • Хто і як моніторить процес міграції та хто сидить біля kill switch скрипта на випадок загрози інциденту?
  • Після міграції утвориться значна кількість dead tuples і гарною практикою буде їх підчистити за допомогою VACUUM ANALYZE.

Висновки

З часом може виникнути потреба в донасичуванні старих даних новими, але з ростом бази даних ця задача стає дедалі нетривіальнішою. Increased query latency, disk degradation, data loss/data races — всі ці проблеми постають перед інженерами в іншому світлі, коли мова заходить про велику кількість даних, а для виконання звичних маніпуляцій потрібно залучати все більш креативні підходи та виділяти більше часу на підготовку.

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

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті

👍ПодобаєтьсяСподобалось23
До обраногоВ обраному13
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Колись писав скрипт, щоб мігрувати 120млн рекордів в SQL Server, дуже схоже завдання було за своєю суттю. Написав такий скрипт, яким досі користуюсь (адаптував під даний приклад)
Міграція тривала ~80хв

pastebin.com/kTfuLhbh

дякую, гарно підмітили
треба оновити

обратите внимание — exists там тоже не особо нужен, можно взять просто @@rowcount от предыщуего инсерта
и добавьте обработку ошибок, так будет кошернее

Перечитал статью, не особо понял откуда возникли такие злобніе проблемі
Спишу на особенности постгре
зы афтор неправильно употребляет слово «аггрегированный»

Скоріш за все проблема була через те що занадто великий батч на 1000 записів юзали для таблиці на 140 млн записів.
Сам недавно таблицю на 3 млн записів апдейтив батчами по 20-30к записів, ліньки було писати логіку на батчі по 100 записів, в результаті потратив цілий день на апдейт

обслуговувала live-трафік ~20rps.

это что у вас за трафик? На 20 туда можно было и sqlite поставить было. У меня на проде 1K QPS влезало в базу постгри. Очень странно что на больших таблицах не используется partitioning

AWS також має zeroetl. aws.amazon.com/what-is/zero-etl

В теорії там реалізовано change data capture (CDC) для реплікації. Але цікаво як воно веде себе в реальності. Чи ви пробували цю технологію?

Технологію не пробували, але якщо говорити про дата-міграцію через копіювання + CDC, то DMS нам підходив як інструмент, у нас з ним є експертиза і все таке, але, по-перше, для цього потрібно було б виділяти інфра-інженера з іншої команди на довгий проміжок часу, по-друге, якщо під час міграції щось пішло б не так, то відкатити, пофіксити, і заново накатити було б суттєво важче (з RDS достатньо зклонувати таблицю і попередньо прогнати всю міграцію переконавшись що вона правильно накатується).

Нипанятна. Т.е. технологического окна на какой-нибудь редифинишн в системе принципиально нет? Окей, бывает.
Второй момент, какого-то партиционирования у вас нет? Хотя бы для tmp-таблицы, чтобы на каждой итерации работать с отдельной партицией вместо относительно дорогого delete.

P.S. Поясните человеку, не имевшему дела с постгресом, updated_at — это какой-то стандартный механизм, или просто поле, куда вы триггером таймстамп пишете? Если второе, то не может быть ситуации, что при одинаковом значении этого поля для двух последних (по _rowsLimit) записей, вы в первом запросе проапдейтите один td_id, а во втором удалите другой)?

— Партиціонування немає, бо розмір таблиць поки що не створює проблем.
— Гарне питання! По-перше, updated_at — це поле типу TIMESTAMP WITHOUT TIMEZONE в яке ми самі пишемо час. По-друге, річ у тім що читання і видалення відбувається з використанням btree індекса tmp_idx, дані в якому відсортовані по двох ключах (updated_at, td_id), тобто доти доки ми паралельно не читаємо і видаляємо щось з індекса результат буде детерміністичний навіть якщо при читанні ми сортуємо лише по updated_at. Але що якщо з якоїсь причини це не так і результат не буде детерміністичним? Наприклад query planner вирішив не використовувати індекс. Зауваження слушне, але ми про це особливо не думали тому що 1. кількість таких випадків на нашу думку була б малою (на практиці ні під час тестів, ні під час міграції не помітили жодного), 2. якби таке трапилося ми б ідентифікували рядки які не оновилися і домігрували б їх.

TIMESTAMP WITHOUT TIMEZONE в яке ми самі пишемо час

Цікаво — чому саме without, а не timestamptz (як на мене, одні мінуси, жодного плюса ) ? У вас всі клієнти жорстко контролюються з приводу того, як саме вони в це поле пишуть?

Цікаво — чому саме without, а не timestamptz (як на мене, одні мінуси, жодного плюса ) ? У вас всі клієнти жорстко контролюються з приводу того, як саме вони в це поле пишуть?

Схоже на аудит поле, тому є шанс, що всі «клієнти» в одній таймзоні.
Хз як воно в го, але в джаві таймзони часто хендлядся на рівні драйвера, як результат в залежності від таймзони апп сервера, сервера БД і 100500 конфігів в неочевидних місцях можуть бути різні результати.

Ну да. Я до того і веду, що з timestamptz, якщо з кліента приїхав коректний literal, то він буде у базі збережений коректно, як би його кліент не подав, а з timestamp without time zone — суцільна лотерея.

Ми оперуємо виключно в UTC, тому з точки зору запису — так, слідкуємо аби завжди писати в UTC, але з точки зору читання використання without timezone дозволяє уникнути випадкової неявної конвертації в різні часові зони.

Хм. При читанні timestamp without timezone треба знати, яку TZ до нього приліпити, щоб вийшов справжній timestamp, а timestamptz просто сунув в якийсь Time.of_string і все.

Ну, вам звісно видніше, я просто зачепився за те, що мені кинулось в очі. Вибачайте :)

Якщо хтось ще раптово дочитає сюди і йому буде цікаво почитати далі, про ще це я, я для них залишу оце посилання

Та ні, чому, цілком справедливе питання! :) Ми самі колись розмірковували над тим чому не timestampz, але інколи доводиться жити з тим що є. Моє припущення чому був зроблений такий вибір — це якщо оперуєш лише в UTC, то вибираєш між тим чи слідкувати за тим що пишеш UTC, чи за тим що повертаєш UTC. На практиці нам це не сильно заважає, ми раз на рівні сервіса визначаємо в якій часовій зоні пишемо час, і більше про це не думаємо.

Як ми змігрували 300 мільйонів рядків у PostgreSQL

Если не задрачивать проект под особенности реализации базы данных, то миграция такой базы происходит максимум за 5 минут. Написание кода под какой-нибудь ОРМ поможет.

то миграция такой базы происходит максимум за 5 мину

Мне кажется вы слегка заблуждаетесь

Скорость упирается исключительно в скорость чтения/записи винта, через bulk-операции, а речь о базе в несколько десятков гигабайт.

Скорость упирается исключительно в скорость чтения/записи винта, через bulk-операции, а речь о базе в несколько десятков гигабайт.

даже если «просто переложит» — то всё равно сомнительно
если не «просто переложить» а «переложит с преобразованием» — то тут уже совсем сомнительно.

Без разницы, каким образом. То есть, пишете простой алгоритм, который читает базу с одной структурой, и на основе её пишет базу с другой структурой. Бутылочное горлышко тут скорость винчестера. На втором месте — объём оперативной памяти, нужно чтоб цельная таблица занимала не больше примерно 70% RAM, иначе будет свопинг на диск, и перфоманс просядет на порядки. Впрочем как вариант, можно читать json записанный на диск. Думаю, многие делали хотябы раз рефакторинг структуры базы данных, с целью повышения перфоманса работы с ней. Гораздо проще и выгоднее потратить несколько минут на преобразование базы, чем городить 3-этажные запросы.

На втором месте — объём оперативной памяти, нужно чтоб цельная таблица занимала не больше примерно 70% RAM

Т.е. для переклдаывания, скажем, 1Тб данных надо иметь 700Гб рам минимум — а лучше больше
«потому что своппинг на диск» ©
В бд... Своппинг....

Впрочем как вариант, можно читать json записанный на диск.

Блядь.

Гораздо проще и выгоднее потратить несколько минут на преобразование базы, чем городить 3-этажные запросы.

Еще раз б***ь.

Т.е. для переклдаывания, скажем, 1Тб данных надо иметь 700Гб рам минимум

Если вы вообще в принципе с этой базой можете работать, без свопинга страниц памяти на диск, то и процесс пойдёт нормально. У меня вообще закрадывается такое подозрение, будто бы у вас базы данных — это какой-то музейный экспонат, который нельзя руками трогать.

база звісно не цукрова, але все ж простіше триповерховий селект написати ніж рефакторінг бд

ви я бачу великий теоретик
зробіть мені рефакторинг бд пенсійного фонду
де триста таблиць лише у одній системі
та по 100 стовпців у деяких (так, це маразм але так є...)

ви я бачу великий теоретик

даже начинающий теоретик БД не стал бы нести такую ересь

Я вообще не теоретик БД, мне их устройство и богатый внутренний мир до лампочки. Пиковый перфоманс у всех топовых систем управления баз данных, над которыми работают — одинаковый, и упирается в возможности железа, и больше ни во что. То есть, по большому счёту без разницы что юзать, то ли PostreSQL, то ли MySQL, то ли Mongo. Ну да, встраиваемая sqlite кардинально отличается из топовых, это да.

у всех топовых систем управления баз данных
без разницы что юзать, то ли PostreSQL, то ли MySQL, то ли Mongo.
упирается в возможности железа, и больше ни во что.

Блядь....

У меня для вас плохая новость: я именно этим раньше и занимался с подобными базами.

У меня для вас плохая новость: я именно этим раньше и занимался с подобными базами.

основываясь на ваших ответах: не верю ©

Ну значит мои ответы не соответствуют вашим представлениям о прекрасном, это вполне нормально.

чому погана?
на здоровья!

(як практик не вірю бо рефакторінг великої бд то найскладніша робота у світі як на мене, а ви пишете як ділетант)

В общем-то согласен с этим, и об этом конечно стоит написать в жире, и подробно рассказать на каком-нибудь спринт-ревью, о том какая титаническая работа была проделана. С другой стороны, у нас тут не спринт-ревью.

и об этом конечно стоит написать в жире, и подробно рассказать на каком-нибудь спринт-ревью,

Блядь.....

без свопинга страниц памяти на диск,

Что, простите? какой «своппинг страниц памяти на диск»???

У меня вообще закрадывается такое подозрение, будто бы у вас базы данных — это какой-то музейный экспонат, который нельзя руками трогать.

Конечно, именно так и есть

«потому что своппинг на диск» ©
В бд... Своппинг....

Чуть не упустил этот нюанс. Здесь речь о системном свапе. Таблица/коллекция загружается в оперативную память целиком при обращении к данным. Соответственно, если свободной оперативной памяти не хватает, какие-то страницы памяти выгружаются. В некоторых случаях это приводит к зацикленному свапу туда-сюда. Вы в этом можете легко убедиться сами.

т.е. если я пишу SELECT id, value FROM Table_A AS A WHERE A.id=1 в память полезет все таблица, верно?

Вы можете увидеть результат по работе винчестера. Если таблица не поделена, то полезет вся сразу. Разбиение на партишены по 32 мегабайта, которое кое-где делается по дефолту — ситуацию не исправит, речь же выше о преобразовании всей базы. Честно говоря, мы тут херню обсуждаем. Но понимание херни у нас разное.

Вы можете увидеть результат по работе винчестера. Если таблица не поделена, то полезет вся сразу. Разбиение на партишены по 32 мегабайта, которое кое-где делается по дефолту — ситуацию не исправит, речь же выше о преобразовании всей базы. Честно говоря, мы тут херню обсуждаем. Но понимание херни у нас разное.

Это для какой СУБД у нас такое?

Разбиение по 32? В Mongo, в Arango.

а, вон оно как....
хотя я имел в виду — єто б***ь у какой нормальной современной СУБД скорость работі радикально зависит от того — вмещается ли она в память или нет....

Таблица/коллекция загружается в оперативную память целиком при обращении к данным

Блядь.....

Эх, сейчас бы за несколько минут преобразовать базу с терабайтами данных и кучей приложений, работающих с ней...

Насчёт терабайтов — это вы несколько преувеличили, даже слитая база линкедина или фейсбука занимает примерно тот же порядок объёма, что и описанная в этом топике. То есть это десятки гигабайт, без медиа-файлов. Чтоб раздуть на терабайты это надо писать туда какую-то поточную лабуду. Конечно править код в приложениях — это долго, но с другой стороны рефакторинг БД — это вполне хороший способ повысить перфоманс. Поскольку БД — это практически всегда бутылочное горлышко для приложений.

Вы здоровы вообще?
2014 год, ФБ:
engineering.fb.com/...​data-warehouse-to-300-pb
Десятки гигабайт, да-да...

Ну люди пишут о ежедневном входном трафике 600ТБ. Как вы думаете, все пользователи своими ручками смогут настрочить столько в мессенджерах? Здесь речь идёт о медиа-данных, которые реально могут занимать объёмы устремляющиеся в бесконечность.

Насчёт терабайтов — это вы несколько преувеличили, даже слитая база линкедина или фейсбука занимает примерно тот же порядок объёма, что и описанная в этом топике. То есть это десятки гигабайт, без медиа-файлов. Ч

Блядь.....

це залежить від довжини тих рядків, але він правий, 300млн це ніщо для типової бд з ораклом, ну не 5 хвилин але десь 15

типової бд з ораклом

>в статье речь про Postgres

И ничего ж не смущает благородного дона.

>в статье речь про Postgres

Є багато «вхідних» від яких буде залежати час.
Але на пг можна скластися в даунтайм менше години (дещо менший об’єм робити за 30 хв). Але знову ж, дефолтне значення — +Х хв, копіювання даних батчем — +Х хв, копіювання через складні запити — +Х може й годин, дуже хитрі індекси — теж може додати години, патерн навантаження (наприклад де все постійно оновлюється/видаляється) — ще купа часу, і тд.
Проблема тут в тому, що додатковий час — це не проблема викликана СУБД, ця проблема викликана про...милкою в дизайні, ускладненням рішення там де це не було потрібно.

ну так по чому й б’ють, як не по голові. абстрактна сферична міграція в умовах, наближених до ідеальних, з зарані написаними і протестованими скриптами, з диску на диск на локальному компі дійсно упреться тільки в швидкість.
А чим далі від неідеальних умов, тим більші нюанси.

А чим далі від неідеальних умов, тим більші нюанси.

Так. От тільки неідеальність умов може бути викликана факторами, що поза контролем розробника (наприклад, дуже специфічними моделями даних) або викликані прямими діями розробників (наприклад, одні знайомі мали серйозні проблеми, бо вклинились в лог транзакцій самої бд і використовували його для стрімінгу в іншу систему)

мене «смущає» але я на постгресі такого не робив
пишу що знаю особисто

Колись дуже давно ми робили подібне. Довго обговорювали як саме реалізувати. Було прийняте рішення, робити в декілька етапів. Спочатку міграція бази без 100% актуалізації, потім коли вже є репліка бази, робимо синхронізацію, синхронізація зайняла найбільше часу. Все відбулося ок. Але я підозрюю,що у вас була складніша задача через те, що маєте Flexible Schemas & Dynamic Relationships, вгадав :) ?

Не певен що ви маєте на увазі, втім ми розглядали те що ви описуєте (розділ AWS DMS), це робочий варіант, але ми обрали інший підхід :)

не зрозумів насправді в чому проблема..

transaction_details -> JOIN transaction.user_id JOIN -> users(.***)

це виглядає як 1-1-1 відношення, і то швидко якщо транзакшн дітейлс вигрібається по індексу і лімітовано... не швидко коли фільтра/сорт ганяють по зджоєним даним на пару мільонів записів )

проблема десь не тут... чи можете поділитись квєрьой яку хотілось оптимізувати? (якщо не секрет, можна змінити там назви імена і вот ето сьо)

Відписав Дмитру нижче на схоже питання

Перша картинка s.dou.ua/...​files/unnamed_PndVs3L.png

підскажіть будь-ласка, як побудувати оту жовту лінію на графіку? це ж grafana у вас?

Думаю це можна зробити через prometheus.io/...​rying/functions/#increase (якщо у вас prometheus і я правильно зрозумів, що він відображає)

Ця панель побудована через promql, зелена лінія — це irate, а жовта — це rate за 15хв

Схоже ви влиштували справжню битву з вітряками і схоже, що у core модулі системи прямо на проді.
Оптимізувати join таблиць в сценарії отримання є не один готовий способів в posgtress починаючи деякі з них суппер прості(які можут мати хвилини блокування на 200 млн рекордів, або взагалі не мати в залежності від вибраного підходу) — мат вью і партиціонування таблиці, що в обоих випадках дало би скоріше за все краще як рішення швидше, з пітримкою failover/recovery в процессі міграції бази, що більше підходить як глобальне/універсальне рішення для проблеми отримання данних з великих таблиць і набагто простіше без 2 степного диплоймента, апдейту бази вручну на проді на протязі декількох днів.

В цілому було б доцільно розібратися в правильності ваших технологічних рішень(оптимізація читання inner join по transactionids (не думаю що там щось складніше оптимізувалось враховуючи опис проблеми) і зменшення запитів у транзакційних сценаріях. Що вам зайві запити в базу робили якісь проблеми(у випадку rdbms завжди більш доцільно буде зробити 5 запитів на 1-3 мс, ніж 1 запит на 100 мс навіть враховуючи дорогий i\o для вашого go додатку) з вашим навантаженням в 20 rps так точно.

Можливо це не досить добре пояснив в статті, але ми оновлювали більш ніж одну колонку для кожної з таблиць, і майже для кожної з них причини відрізнялися одна від одної, деякі з проблем були семантичними і мова йшла про консистентність даних, а не про швидкий доступ. Були й інші причини, але в статті вирішив сфокусуватися на «як», радше ніж «чому» і прийняти за передумову що така міграція була доцільною. Тому згадав лише денормалізацію, як одну з причин, оскільки її доволі просто пояснити для людей поза контекстом.

мій внутрішній прінципал інженер не дає мені просто піти — стало щє цікавіше! (і для саморозвитку в тому числі)

чи можна дізнатись трохи більше за проблеми з консістенці? зазвичай постгрєс і прочіє реляційкі цим не страждають..

Це не були проблеми пов’язані з тим *як* ми пишемо дані (ізоляція транзакцій чи реплікація даних). Тут радше був нюанс з тим як зберігалися деякі дані та до яких сутностей вони були прив’язані. Раніше існуючий підхід працював, але з розширенням функціоналу системи довелося внести зміни. На жаль, це все що я можу сказати :)

Це не були проблеми пов’язані з тим *як* ми пишемо дані (ізоляція транзакцій чи реплікація даних).

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

P.S. Стаття норм

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

Єто скорее всего от того, что в статье пітаются показать техническое и технологическое решение, но не на примере реальной системі (чтобі не раскрівать её реализацию), а на «уявных примерах» — которые могут выглядеть довольно странно и нелепо в отрыве от реальной реальности

Ага, тепер більш зрозуміло. щє 2 питання і все :)
1) Які недолікі в дизайні системи/аплікухи викликали неузгодженість даних?
2) чи була можливість доналити дані атомарно, без транзакцій?

Виправте, будь ласка, “Heat-Only-Tuples” -> “Heap-Only Tuples”

Heat-Only-Tuples це коли в девелоперів горить. :D

А які причини змусили вас розбити транзакцію на `transactions` та `transaction_details`?

А які причини змусили вас розбити транзакцію на `transactions` та `transaction_details`?

Я так понимаю — уменьгить минимально неободимый для работы набор данные
Транзакции — айди, дата, сумма, колиент — «достаточно для практической деятельности и основных отчетов»
а всякие там «номер отделения, дата обработки, кассир, оператор, отпечаток пальца» — детали

Я вас розумію, але.. не розумію і все одно спробую спитати очевидне: чого не тримати все що стосується транзакції в одному місці а в залежності від потреб робити селект на потрібний набір полів?

Відчуваю що автор надав недостатньо деталей щоб зрозуміти його задачу/проблему повністю і через те у мене є враження що він «рихтує» наслідки, а не вирішує оригінальну проблему (я тут можу бути як правий так і ні)

Я вас розумію, але.. не розумію і все одно спробую спитати очевидне: чого не тримати все що стосується транзакції в одному місці а в залежності від потреб робити селект на потрібний набір полів?

Потому что ширина рядка получается очень большая, и на одну операцию чтения мы будем получать меньше данных
а так мы получаем максимально узкую и плотную таблицу данных, читать которую проще — и где-то там у нас лежит детализация, которая нужна далеко не всего — и она нам не мешает
можно вообще по разным дискам/группа разнести
детализацию можно чистить независимо от common части
очень удобно с разніх точек зрения

Це обфусковані назви використані суто для прикладу, таких таблиць у нас немає :)

Підписатись на коментарі