Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Как и зачем MVCC мешает нам пользоваться базой ...

Как и зачем MVCC мешает нам пользоваться базой данных и почему это не плохо

Несмотря на распространение различных NoSQL решений для хранения данных в последние годы, реляционные базы данных остаются популярным выбором среди разработчиков из–за своей универсальности и предсказуемости. Большинство из них следуют принципам ACID, который дает нам определенные гарантии. Однако, сделать так, чтобы БД им соответствовала не так то просто, и мы рассмотрим один из подходов к этому — MVCC. Иногда его работа приводит к неожиданным сайд–эффектам, и в докладе мы рассмотрим несколько примеров.

В качестве примеров я буду использовать PostgreSQL, но MVCC работает одинаково практически везде, так что адаптировать новые знания к своей любимой БД будет довольно просто. Доклад будет полезен как начинающим, так и продвинутым бэкэндерам.

Dmitry Tsepelev

September 25, 2021
Tweet

More Decks by Dmitry Tsepelev

Other Decks in Programming

Transcript

  1. DmitryTsepelev 404fest'21 4 А что если данных много? 🤔 SELECT

    pg_size_pretty(pg_total_relation_size('users')); - - 200 Gb
  2. DmitryTsepelev 404fest'21 Обновляем в цикле 5 User.in_batches do |batch| batch.update_all(email_conf

    i rmed: false) end SELECT users.id FROM users WHERE users.id > 0 ORDER BY users.id ASC LIMIT 1000; UPDATE users SET email_conf i rmed = FALSE WHERE users.id IN ( . . . ); SELECT users.id FROM users WHERE users.id > 1000 ORDER BY users.id ASC LIMIT 1000; UPDATE users SET email_conf i rmed = FALSE WHERE users.id IN ( . . . );
  3. DmitryTsepelev 404fest'21 6 Ой, места на диске не осталось SELECT

    pg_size_pretty(pg_total_relation_size('users')); - - 400 Gb
  4. DmitryTsepelev 404fest'21 БД и разработчики • есть таблички с данными;

    • данные можно изменять и запрашивать; • таблички можно объединять (JOINs) в запросах; • таблички связаны с помощью foreign keys; • есть constraints для валидации данных. 10
  5. DmitryTsepelev 404fest'21 Делаем биллинг def handle_payment(user, payment) payment.update(processed: true) subscription_active_until

    = [user.subscription_active_until, Date.current].max + payment.days_covered user.update(subscription_active_until: subscription_active_until) end UPDATE payments SET processed = true WHERE id = ?; UPDATE users SET subscription_active_until = '2021-10-23' WHERE id = ?; 11
  6. DmitryTsepelev 404fest'21 Делаем биллинг def handle_payment(user, payment) payment.update(processed: true) subscription_active_until

    = [user.subscription_active_until, Date.current].max + payment.days_covered user.update(subscription_active_until: subscription_active_until) end 14 А вдруг user.update выбросит исключение? 🤔
  7. DmitryTsepelev 404fest'21 Atomicity (доделываем биллинг) def handle_payment(user, payment) ActiveRecord :

    : Base.transaction do payment.update(processed: true) subscription_active_until = [user.subscription_active_until, Date.current].max + payment.days_covered user.update(subscription_active_until: subscription_active_until) end end BEGIN; UPDATE payments SET processed = true WHERE id = ?; UPDATE users SET subscription_active_until = '2021-10-23' WHERE id = ?; COMMIT; 15
  8. DmitryTsepelev 404fest'21 Транзакция • переводит БД из одного состояния в

    другое; • может содержать несколько операций; • фиксируется только если все операции прошли успешно; • фиксация изменений происходит одновременно; • работает независимо от других транзакций. 16
  9. DmitryTsepelev 404fest'21 Спин–офф: локи def handle_payment(user, payment) user.with_lock do return

    if payment.reload.processed? # на случай если платеж уже запроцессили payment.update(processed: true) subscription_active_until = [user.subscription_active_until, Date.current].max + payment.days_covered user.update(subscription_active_until: subscription_active_until) end end BEGIN; SELECT * FROM payments WHERE id = ? FOR UPDATE; UPDATE payments SET processed = true WHERE id = ?; UPDATE users SET subscription_active_until = '2021-10-23' WHERE id = ?; COMMIT; 17
  10. DmitryTsepelev 404fest'21 Целостность и консистентность • целостные данные соответствуют ограничениям

    БД (UNIQUE, NOT NULL, …); • корректные данные соответствуют всем ограничениям приложения; • БД не всегда может гарантировать корректность. 19
  11. DmitryTsepelev 404fest'21 Isolation 21 BEGIN; UPDATE payments SET processed =

    true WHERE id = 1; SELECT processed FROM payments WHERE id = 1; - - true COMMIT; BEGIN; SELECT processed FROM payments WHERE id = 1; - - false COMMIT; BEGIN; SELECT processed FROM payments WHERE id = 1; - - true
  12. DmitryTsepelev 404fest'21 Как хранятся данные на диске 24 users id

    name 1 John 2 Jane 42 (< 1GB) users forks Database orders id user_id 1 2 123 (< 1GB) orders forks Disk
  13. DmitryTsepelev 404fest'21 Как хранятся данные на диске 25 users id

    name 1 John 2 Jane Disk 42 (< 1GB) users forks Database orders id user_id 1 2 123 (1GB) orders forks 123.1 (< 1GB) Fork fi le page 1 (8 kB) page 2 (8 kB) page 3 (8 kB)
  14. DmitryTsepelev 404fest'21 Tuple • tuple = header + user data;

    • tuple почти никогда не обновляется (БД может, мы нет); • DELETE = UPDATE xmax; • UPDATE = INSERT + DELETE. 26 xmin xmax ctid infomask id name 123 125 (3, 15) 111 1 Jon 123 0 (4, 1) 111 2 Jane 125 0 (3, 15) 111 1 Jonh
  15. DmitryTsepelev 404fest'21 Tuple header 27 BEGIN; SELECT txid_current(); ┌──────────────┐ │

    txid_current │ ├──────────────┤ │ 8249614 │ └──────────────┘ INSERT INTO users (email) VALUES ('[email protected]'); SELECT *, ctid, xmin, xmax FROM users; ┌────┬──────────────────┬───────┬─────────┬──────┐ │ id │ email │ ctid │ xmin │ xmax │ ├────┼──────────────────┼───────┼─────────┼──────┤ │ 1 │ [email protected] │ (0,1) │ 8249614 │ 0 │ └────┴──────────────────┴───────┴─────────┴──────┘
  16. DmitryTsepelev 404fest'21 Tuple header 28 CREATE EXTENSION pageinspect; SELECT (t_infomask

    & 256) > 0 AS xmin_commited, * FROM heap_page_items(get_raw_page('users', 0)); ┌───────────────┬─────────┬────────┬──────────┬────────┬─────────────┬────────────┬────────┬────────┬───────┬────────┐ │ xmin_commited │ t_xmin │ t_xmax │ t_f i eld3 │ t_ctid │ t_infomask2 │ t_infomask │ t_hoff │ t_bits │ t_oid │ t_data │ ├───────────────┼─────────┼────────┼──────────┼────────┼─────────────┼────────────┼────────┼────────┼───────┼────────┤ │ f │ 8249614 │ 0 │ 11 │ (0,1) │ 2 │ 2050 │ 24 │ ¤ │ ¤ │ . . . │ └───────────────┴─────────┴────────┴──────────┴────────┴─────────────┴────────────┴────────┴────────┴───────┴────────┘ COMMIT; ┌───────────────┬─────────┬────────┬──────────┬────────┬─────────────┬────────────┬────────┬────────┬───────┬────────┐ │ xmin_commited │ t_xmin │ t_xmax │ t_f i eld3 │ t_ctid │ t_infomask2 │ t_infomask │ t_hoff │ t_bits │ t_oid │ t_data │ ├───────────────┼─────────┼────────┼──────────┼────────┼─────────────┼────────────┼────────┼────────┼───────┼────────┤ │ t │ 8249614 │ 0 │ 11 │ (0,1) │ 2 │ 2050 │ 24 │ ¤ │ ¤ │ . . . │ └───────────────┴─────────┴────────┴──────────┴────────┴─────────────┴────────────┴────────┴────────┴───────┴────────┘
  17. DmitryTsepelev 404fest'21 Update tuple 29 BEGIN; SELECT txid_current(); ┌──────────────┐ │

    txid_current │ ├──────────────┤ │ 8249618 │ └──────────────┘ UPDATE users SET email = '[email protected]' WHERE id = 1; SELECT *, ctid, xmin, xmax FROM users; ┌────┬──────────────────────┬───────┬─────────┬──────┐ │ id │ email │ ctid │ xmin │ xmax │ ├────┼──────────────────────┼───────┼─────────┼──────┤ │ 1 │ [email protected] │ (0,2) │ 8249618 │ 0 │ └────┴──────────────────────┴───────┴─────────┴──────┘
  18. DmitryTsepelev 404fest'21 Update tuple 30 SELECT t_xmin, t_xmax, t_ctid FROM

    heap_page_items(get_raw_page('users', 0)); ┌─────────┬─────────┬────────┐ │ t_xmin │ t_xmax │ t_ctid │ ├─────────┼─────────┼────────┤ │ 8249616 │ 8249618 │ (0,2) │ │ 8249619 │ 0 │ (0,2) │ └─────────┴─────────┴────────┘
  19. DmitryTsepelev 404fest'21 Delete tuple 31 BEGIN; SELECT txid_current(); ┌──────────────┐ │

    txid_current │ ├──────────────┤ │ 8249620 │ └──────────────┘ DELETE FROM users WHERE id = 1; SELECT t_xmin, t_xmax, t_ctid FROM heap_page_items(get_raw_page('users', 0)); ┌─────────┬─────────┬────────┐ │ t_xmin │ t_xmax │ t_ctid │ ├─────────┼─────────┼────────┤ │ 8249616 │ 8249619 │ (0,2) │ │ 8249619 │ 8249621 │ (0,2) │ └─────────┴─────────┴────────┘
  20. DmitryTsepelev 404fest'21 MVCC • чтение не блокирует запись; • запись

    не блокирует чтение; • каждая транзакция видит свой «слепок» данных. 33
  21. DmitryTsepelev 404fest'21 Snapshot • физическая копия данных не делается; •

    «видимые» данные определяются исходя из условий. 34 SELECT * FROM users WHERE xmin < = txid_current() AND (xmax = 0 OR txid_current() < xmax); - - - ^^ ^^ - - - запись еще не удалена запись удалена позже
  22. DmitryTsepelev 404fest'21 Snapshot T1 T2 T3 T4 S1 S2 S3

    • S1: 🤷 • S2: T1 • S3: T1, T2 35
  23. DmitryTsepelev 404fest'21 Snapshot 36 BEGIN; SELECT txid_current(); ┌──────────────┐ │ txid_current

    │ ├──────────────┤ │ 8257515 │ └──────────────┘ SELECT txid_current_snapshot(); ┌──────────────────────────────┐ │ txid_current_snapshot │ ├──────────────────────────────┤ │ 8257515 : 8257515 : 8257515 │ └──────────────────────────────┘ INSERT INTO users (email) VALUES ('[email protected]'); SELECT *, ctid, xmin, xmax FROM users; ┌────┬──────────────────┬───────┬─────────┬──────┐ │ id │ email │ ctid │ xmin │ xmax │ ├────┼──────────────────┼───────┼─────────┼──────┤ │ 2 │ [email protected] │ (0,3) │ 8257515 │ 0 │ └────┴──────────────────┴───────┴─────────┴──────┘ xmin xmax список активных транзакций
  24. DmitryTsepelev 404fest'21 Savepoints 37 BEGIN; SELECT txid_current(); ┌──────────────┐ │ txid_current

    │ ├──────────────┤ │ 8257515 │ └──────────────┘ SELECT txid_current_snapshot(); ┌──────────────────────────────┐ │ txid_current_snapshot │ ├──────────────────────────────┤ │ 8257515 : 8257515 : 8257515 │ └──────────────────────────────┘ INSERT INTO users (email) VALUES ('[email protected]'); SELECT *, ctid, xmin, xmax FROM users; ┌────┬──────────────────┬───────┬─────────┬──────┐ │ id │ email │ ctid │ xmin │ xmax │ ├────┼──────────────────┼───────┼─────────┼──────┤ │ 2 │ [email protected] │ (0,3) │ 8257516 │ 0 │ └────┴──────────────────┴───────┴─────────┴──────┘
  25. DmitryTsepelev 404fest'21 Savepoints • механизм для "вложенных" транзакций; • позволяет

    откатить часть изменений, не откатывая всю транзакцию; • txid_current возвращает идентификатор главной транзакции! 38 BEGIN; SAVEPOINT s1; INSERT INTO users (email) VALUES ('[email protected]'); ROLLBACK TO s1;
  26. DmitryTsepelev 404fest'21 Где кроме PSQL используется MVCC? • MySQL (InnoDB/Falcon/Archive

    storage) • MariaDB (XtraDB/PBXT) • MongoDB (WiredTiger storage) • Oracle (> 4) • CouchDB • Couchbase • … 39
  27. DmitryTsepelev 404fest'21 Уровни изоляции транзакций 42 Isolation Level Dirty Read

    Nonrepeatable Reads Phantom Reads Serialization Anomaly Read Uncommited (not PG) (not PG) x x Read Commited x x x Repeatable Read (not PG) x Serializable
  28. DmitryTsepelev 404fest'21 Как работают уровни изоляции? 43 • в Read

    Committed снепшот создается перед каждым запросом; • в Repeatable Read и Serializable снепшот создается только один раз.
  29. DmitryTsepelev 404fest'21 Как работает Serializable 44 • гарантируется, что результат

    работы транзакций не зависит от порядка выполнения; • для полностью корректной работы нужно обходить граф зависимостей; • упрощенно: ищем ситуации, когда одна транзакция изменила строку, а другая — прочитала предыдущую версию этой строки; • простой подход может вызывать ложно–положительные срабатывания.
  30. DmitryTsepelev 404fest'21 Почему не использовать Serializable всегда? 45 • придется

    использовать его для ВСЕХ транзакций; • придется всегда ловить исключение и перезапускать операцию; • Serializable работает медленнее других уровней.
  31. DmitryTsepelev 404fest'21 46 INSERT INTO users (email) VALUES ('[email protected]'); INSERT

    INTO users (email) VALUES ('[email protected]'); INSERT INTO users (email) VALUES ('[email protected]'); SELECT * FROM users; ┌────┬──────────────────┐ │ id │ email │ ├────┼──────────────────┤ │ 1 │ [email protected] │ │ 2 │ [email protected] │ │ 3 │ [email protected] │ └────┴──────────────────┘ ⏱ SELECT * FROM users; ❓ ❓ ❓
  32. DmitryTsepelev 404fest'21 INSERT INTO users (email) VALUES ('[email protected]'); INSERT INTO

    users (email) VALUES ('[email protected]'); INSERT INTO users (email) VALUES ('[email protected]'); SELECT * FROM users; ┌────┬──────────────────┐ │ id │ email │ ├────┼──────────────────┤ │ 1 │ [email protected] │ │ 2 │ [email protected] │ │ 3 │ [email protected] │ └────┴──────────────────┘ ⏱ SELECT * FROM users; ┌────┬─────────────────────┐ │ id │ email │ ├────┼─────────────────────┤ │ 2 │ [email protected] │ │ 3 │ [email protected] │ │ 1 │ [email protected] │ └────┴─────────────────────┘ Проблема: нет явного порядка 47
  33. DmitryTsepelev 404fest'21 INSERT INTO users (email) VALUES ('[email protected]'); INSERT INTO

    users (email) VALUES ('[email protected]'); INSERT INTO users (email) VALUES ('[email protected]'); SELECT * FROM users; ┌────┬──────────────────┐ │ id │ email │ ├────┼──────────────────┤ │ 1 │ [email protected] │ │ 2 │ [email protected] │ │ 3 │ [email protected] │ └────┴──────────────────┘ UPDATE users SET email = '[email protected]' WHERE id = 1; SELECT * FROM users; ┌────┬─────────────────────┐ │ id │ email │ ├────┼─────────────────────┤ │ 2 │ [email protected] │ │ 3 │ [email protected] │ │ 1 │ [email protected] │ └────┴─────────────────────┘ Проблема: нет явного порядка 48
  34. DmitryTsepelev 404fest'21 SELECT t_xmin, t_xmax, t_ctid FROM heap_page_items(get_raw_page('users', 0)); ┌─────────┬─────────┬────────┐

    │ t_xmin │ t_xmax │ t_ctid │ ├─────────┼─────────┼────────┤ │ 8257533 │ 8257536 │ (0,4) │ │ 8257534 │ 0 │ (0,2) │ │ 8257535 │ 0 │ (0,3) │ │ 8257536 │ 0 │ (0,4) │ └─────────┴─────────┴────────┘ Проблема: нет явного порядка 49
  35. DmitryTsepelev 404fest'21 Фича: определяем была ли запись создана в upsert

    50 INSERT INTO users (name, email) VALUES ('John', '[email protected]'); INSERT INTO users (name, email) VALUES ('John!', '[email protected]'), ('Jane', '[email protected]') ON CONFLICT (email) DO UPDATE SET name = EXCLUDED.name RETURNING id, (xmax = '0') AS inserted, name AS new_name; ┌─────────┬──────────┬──────────┐ │ id │ inserted │ new_name │ ├─────────┼──────────┼──────────┤ │ 1 │ f │ John! │ │ 2 │ t │ Jane │ └─────────┴──────────┴──────────┘
  36. DmitryTsepelev 404fest'21 Фича: определяем была ли запись создана в upsert

    51 select *, xmin, xmax, ctid from users; ┌─────────┬──────────────────┬───────┬─────────┬─────────┬───────┐ │ id │ email │ name │ xmin │ xmax │ ctid │ ├─────────┼──────────────────┼───────┼─────────┼─────────┼───────┤ │ 1 │ [email protected] │ John! │ 8843025 │ 8843025 │ (0,2) │ │ 2 │ [email protected] │ Jane │ 8843025 │ 0 │ (0,3) │ └─────────┴──────────────────┴───────┴─────────┴─────────┴───────┘ 🤔🤔🤔
  37. DmitryTsepelev 404fest'21 Фича: определяем была ли запись создана в upsert

    52 select *, xmin, xmax, ctid from users; ┌─────────┬──────────────────┬───────┬─────────┬─────────┬───────┐ │ id │ email │ name │ xmin │ xmax │ ctid │ ├─────────┼──────────────────┼───────┼─────────┼─────────┼───────┤ │ 1 │ [email protected] │ John! │ 8843025 │ 8843025 │ (0,2) │ │ 2 │ [email protected] │ Jane │ 8843025 │ 0 │ (0,3) │ └─────────┴──────────────────┴───────┴─────────┴─────────┴───────┘ 🤔🤔🤔 В некоторых случаях xmax может означать всего лишь лок: https://stackover fl ow.com/a/39204667
  38. DmitryTsepelev 404fest'21 53 INSERT INTO users (email) SELECT random() :

    : text FROM generate_series(1, 1000000); SELECT pg_size_pretty(pg_total_relation_size('users')); - - 72 MB UPDATE users SET email = 'something'; SELECT pg_size_pretty(pg_total_relation_size('users')); ❓ ❓ ❓
  39. DmitryTsepelev 404fest'21 Проблема: большой апдейт может раздуть таблицу 54 INSERT

    INTO users (email) SELECT random() : : text FROM generate_series(1, 1000000); SELECT pg_size_pretty(pg_total_relation_size('users')); - - 72 MB UPDATE users SET email = 'something'; SELECT pg_size_pretty(pg_total_relation_size('users')); - - 135 MB
  40. DmitryTsepelev 404fest'21 Проблема: удаление данных из таблицы не освобождает место

    на диске 55 VACUUM users; SELECT pg_size_pretty( pg_total_relation_size('users') ); - - 135 MB VACUUM FULL users; SELECT pg_size_pretty( pg_total_relation_size('users') ); - - 64 MB
  41. DmitryTsepelev 404fest'21 Проблема: ID транзакций могут «кончиться» • тип ID

    транзакции XID, 32 бита (~4 000 000 000 значений); • больший тип увеличит место для хранения заголовка тапла; • БД начинает счет заново при достижении максимального значения; • VACUUM «замораживает» (freeze) старые таплы. 56
  42. DmitryTsepelev 404fest'21 Выводы • MVCC нужен для обеспечения изоляции транзакций;

    • на низком уровне данные не обновляются, а записываются; • реализация MVCC иногда приводит к неожиданным сайд– эффектам. 57
  43. DmitryTsepelev 404fest'21 Куда пойти дальше • Цикл статей про MVCC:

    • 🇬🇧 https://postgrespro.com/blog/pgsql/5967856; • 🇷🇺 https://habr.com/ru/company/postgrespro/blog/442804/; • The Internals of PostgreSQL — http://www.interdb.jp/pg/; • Serializable Snapshot Isolation in PostgreSQL — https://arxiv.org/pdf/ 1208.4179.pdf; • Database Internals — https://www.databass.dev. 58