Slide 1

Slide 1 text

DmitryTsepelev DUMP 2022 Задача: нужно сбросить всем пользователям флаг подтверждения почты 1

Slide 2

Slide 2 text

DmitryTsepelev DUMP 2022 2 UPDATE users SET email_conf i rmed = FALSE;

Slide 3

Slide 3 text

DmitryTsepelev DUMP 2022 3 А что если данных много? 🤔 SELECT pg_size_pretty(pg_total_relation_size('users')); - - 200 Gb

Slide 4

Slide 4 text

DmitryTsepelev DUMP 2022 Обновляем в цикле 4 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 ( . . . );

Slide 5

Slide 5 text

DmitryTsepelev DUMP 2022 5 Ой, места на диске не осталось SELECT pg_size_pretty(pg_total_relation_size('users')); - - 400 Gb

Slide 6

Slide 6 text

DmitryTsepelev DUMP 2022 6 @dmitrytsepelev 🌎 dmitrytsepelev.dev

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

DmitryTsepelev DUMP 2022 8

Slide 9

Slide 9 text

DmitryTsepelev DUMP 2022 БД и разработчики • есть таблички с данными; • данные можно изменять и запрашивать; • таблички можно объединять (JOINs) в запросах; • таблички связаны с помощью foreign keys; • есть constraints для валидации данных. 9

Slide 10

Slide 10 text

DmitryTsepelev DUMP 2022 Делаем биллинг 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 = ?; 10

Slide 11

Slide 11 text

ACID

Slide 12

Slide 12 text

DmitryTsepelev DUMP 2022 ACID • Atomicity • Consistency • Isolation • Durability 12

Slide 13

Slide 13 text

DmitryTsepelev DUMP 2022 Делаем биллинг 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 13 А вдруг user.update выбросит исключение? 🤔

Slide 14

Slide 14 text

DmitryTsepelev DUMP 2022 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; 14

Slide 15

Slide 15 text

DmitryTsepelev DUMP 2022 Транзакция • переводит БД из одного состояния в другое; • может содержать несколько операций; • фиксируется только если все операции прошли успешно; • фиксация изменений происходит одновременно; • работает независимо от других транзакций. 15

Slide 16

Slide 16 text

DmitryTsepelev DUMP 2022 Спин–офф: локи 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; 16

Slide 17

Slide 17 text

DmitryTsepelev DUMP 2022 ACID • Atomicity • Consistency • Isolation • Durability 17

Slide 18

Slide 18 text

DmitryTsepelev DUMP 2022 Целостность и консистентность • целостные данные соответствуют ограничениям БД (UNIQUE, NOT NULL, …); • корректные данные соответствуют всем ограничениям приложения; • БД не всегда может гарантировать корректность. 18

Slide 19

Slide 19 text

DmitryTsepelev DUMP 2022 ACID • Atomicity • Consistency • Isolation • Durability 19

Slide 20

Slide 20 text

DmitryTsepelev DUMP 2022 Isolation 20 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

Slide 21

Slide 21 text

DmitryTsepelev DUMP 2022 ACID • Atomicity • Consistency • Isolation • Durability 21

Slide 22

Slide 22 text

Как обеспечить изоляцию транзакций?

Slide 23

Slide 23 text

DmitryTsepelev DUMP 2022 Изоляция на локах • можно блокировать все, что читает либо пишет транзакция; • двухфазный лок (2PL): сначала всё блокируем, затем всё освобождаем; • в этом случае две транзакции, касающиеся одних строк работают последовательно, таким образом они изолированы. 23

Slide 24

Slide 24 text

Спасибо! @dmitrytsepelev 🌎 dmitrytsepelev.dev 24

Slide 25

Slide 25 text

DmitryTsepelev DUMP 2022 25

Slide 26

Slide 26 text

Как БД хранит данные

Slide 27

Slide 27 text

DmitryTsepelev DUMP 2022 Как хранятся данные на диске 27 users id name 1 John 2 Jane 42 (< 1GB) users forks Database orders id user_id 1 2 123 (< 1GB) orders forks Disk

Slide 28

Slide 28 text

DmitryTsepelev DUMP 2022 Как хранятся данные на диске 28 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)

Slide 29

Slide 29 text

DmitryTsepelev DUMP 2022 Типы форков • основной (хранит данные); • инициализационный (для нежурналируемых таблиц); • карта свободного пространства (отслеживает объем свободного места на страницах); • карта видимости (для определения необходимости очистки страницы или заморозки). 29

Slide 30

Slide 30 text

DmitryTsepelev DUMP 2022 Tuple • tuple = header + user data; • tuple почти никогда не обновляется (БД может, мы нет); • DELETE = UPDATE xmax; • UPDATE = INSERT + DELETE. 30 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

Slide 31

Slide 31 text

DmitryTsepelev DUMP 2022 Tuple header 31 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 │ └────┴──────────────────┴───────┴─────────┴──────┘

Slide 32

Slide 32 text

DmitryTsepelev DUMP 2022 Tuple header 32 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 │ ¤ │ ¤ │ . . . │ └───────────────┴─────────┴────────┴──────────┴────────┴─────────────┴────────────┴────────┴────────┴───────┴────────┘

Slide 33

Slide 33 text

DmitryTsepelev DUMP 2022 Update tuple 33 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 │ └────┴──────────────────────┴───────┴─────────┴──────┘

Slide 34

Slide 34 text

DmitryTsepelev DUMP 2022 Update tuple 34 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) │ └─────────┴─────────┴────────┘

Slide 35

Slide 35 text

DmitryTsepelev DUMP 2022 Delete tuple 35 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) │ └─────────┴─────────┴────────┘

Slide 36

Slide 36 text

Multi Version Concurrency Control

Slide 37

Slide 37 text

DmitryTsepelev DUMP 2022 MVCC • чтение не блокирует запись; • запись не блокирует чтение; • каждая транзакция видит свой «слепок» данных. 37

Slide 38

Slide 38 text

DmitryTsepelev DUMP 2022 Snapshot • физическая копия данных не делается; • «видимые» данные определяются исходя из условий. 38 SELECT * FROM users WHERE xmin < = txid_current() AND (xmax = 0 OR txid_current() < xmax); - - - ^^ ^^ - - - запись еще не удалена запись удалена позже

Slide 39

Slide 39 text

DmitryTsepelev DUMP 2022 Snapshot 39 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 список активных транзакций

Slide 40

Slide 40 text

DmitryTsepelev DUMP 2022 Savepoints 40 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 │

Slide 41

Slide 41 text

DmitryTsepelev DUMP 2022 Savepoints • механизм для "вложенных" транзакций; • позволяет откатить часть изменений, не откатывая всю транзакцию; • txid_current возвращает идентификатор главной транзакции! 41 BEGIN; SAVEPOINT s1; INSERT INTO users (email) VALUES ('[email protected]'); ROLLBACK TO s1;

Slide 42

Slide 42 text

DmitryTsepelev DUMP 2022 Где кроме PSQL используется MVCC? • MySQL (InnoDB/Falcon/Archive storage) • MariaDB (XtraDB/PBXT) • MongoDB (WiredTiger storage) • Oracle (> 4) • CouchDB • Couchbase • … 42

Slide 43

Slide 43 text

Спецэффекты

Slide 44

Slide 44 text

DmitryTsepelev DUMP 2022 Аномалии изоляции • Dirty Read; • Nonrepeatable Read; • Phantom Read; • Serialization Anomaly. 44

Slide 45

Slide 45 text

DmitryTsepelev DUMP 2022 Уровни изоляции транзакций 45 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

Slide 46

Slide 46 text

DmitryTsepelev DUMP 2022 Как работают уровни изоляции? 46 • в Read Committed снепшот создается перед каждым запросом; • в Repeatable Read и Serializable снепшот создается только один раз.

Slide 47

Slide 47 text

DmitryTsepelev DUMP 2022 Как работает Serializable 47 • гарантируется, что результат работы транзакций не зависит от порядка выполнения; • для полностью корректной работы нужно обходить граф зависимостей; • упрощенно: ищем ситуации, когда одна транзакция изменила строку, а другая — прочитала предыдущую версию этой строки; • простой подход может вызывать ложно–положительные срабатывания.

Slide 48

Slide 48 text

DmitryTsepelev DUMP 2022 Почему не использовать Serializable всегда? 48 • придется использовать его для ВСЕХ транзакций; • придется всегда ловить исключение и перезапускать операцию; • Serializable работает медленнее других уровней.

Slide 49

Slide 49 text

DmitryTsepelev DUMP 2022 49 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; ❓ ❓ ❓

Slide 50

Slide 50 text

DmitryTsepelev DUMP 2022 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] │ └────┴─────────────────────┘ Проблема: нет явного порядка 50

Slide 51

Slide 51 text

DmitryTsepelev DUMP 2022 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] │ └────┴─────────────────────┘ Проблема: нет явного порядка 51

Slide 52

Slide 52 text

DmitryTsepelev DUMP 2022 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) │ └─────────┴─────────┴────────┘ Проблема: нет явного порядка * 52 * если планировщик воспользуется индексом, поддерживающим сортировку — порядок будет

Slide 53

Slide 53 text

DmitryTsepelev DUMP 2022 Проблема: нет явного порядка 53

Slide 54

Slide 54 text

DmitryTsepelev DUMP 2022 Фича: определяем была ли запись создана в upsert 54 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 │ └─────────┴──────────┴──────────┘

Slide 55

Slide 55 text

DmitryTsepelev DUMP 2022 Фича: определяем была ли запись создана в upsert 55 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) │ └─────────┴──────────────────┴───────┴─────────┴─────────┴───────┘ 🤔🤔🤔

Slide 56

Slide 56 text

DmitryTsepelev DUMP 2022 Фича: определяем была ли запись создана в upsert 56 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

Slide 57

Slide 57 text

DmitryTsepelev DUMP 2022 57 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')); ❓ ❓ ❓

Slide 58

Slide 58 text

DmitryTsepelev DUMP 2022 Проблема: большой апдейт может раздуть таблицу 58 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

Slide 59

Slide 59 text

DmitryTsepelev DUMP 2022 Проблема: как освободить место на диске после удаления данных? 59 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

Slide 60

Slide 60 text

DmitryTsepelev DUMP 2022 Проблема: ID транзакций могут «кончиться» • тип ID транзакции XID, 32 бита (~4 000 000 000 значений); • больший тип увеличит место для хранения заголовка тапла; • БД начинает счет заново при достижении максимального значения; • VACUUM «замораживает» (freeze) старые таплы. 60

Slide 61

Slide 61 text

DmitryTsepelev DUMP 2022 Выводы • MVCC нужен для обеспечения изоляции транзакций; • на низком уровне данные не обновляются, а записываются; • реализация MVCC иногда приводит к неожиданным сайд–эффектам. 61

Slide 62

Slide 62 text

DmitryTsepelev DUMP 2022 Куда пойти дальше • PostgreSQL изнутри — https:/ /postgrespro.ru/education/books/ internals; • 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. 62

Slide 63

Slide 63 text

Спасибо! @dmitrytsepelev 🌎 dmitrytsepelev.dev 63