itmo_conspects

Лекция 8. Надежность

На прошлой лекции велся разговор о производительности и то, каким способами она добивается (ну или нет)

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

Различают два вида сохранности (целостности):

1) Физическая сохранность - обеспечение сохранности данных в случае аппаратных и физических ошибок

2) Логическая сохранность - обеспечение сохранности в ходе чтения и изменения данных

При этом мы пытаемся достичь надежности хранения (данные сохранились) и доступа (данные сохранились и к ним можно получить доступ)

Физическая сохранность

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

И если в случае с оперативной памятью, когда, начав работать, она почти с нулевой вероятностью сломается, то у жестких дисков даже есть специальная метрика - наработка на отказ: количество часов, в ходе которых работающий диск сломается с некой вероятностью. Причем это значение получают очень интересно: диски заставляют работать в невыносимых условиях ( повышенная частота запросов, температура и т.д.), затем полученные данные умножают на некий коэффициент - и дело в шляпе

Можем сделать так: взять три диска, один из которых будет под избыточные данные; сами данные разделим на две части, которые параллельно будем записывать на два диска, а на избыточный диск записывать побитовый XOR этих двух частей. Таким образом, получается, что при отказе одного диска, мы можем восстановить данные. Помимо этого роль избыточного диска могут и принимают другие диски, чтобы балансировать нагрузку - мы получаем массив RAID5

Кроме этих двух типов RAID есть и другие, отличающиеся своей надежностью и количеством затраченных средств

Следующим шагом является создание системы хранения данных (СХД) - специального компьютера со своей операционной системой и десятками дисков, который анализирует, что ему приходит и куда это положить - например, более востребованные к доступу данные класть на более быстрый диск

Далее человечество пришло к дата-центрам - специальным зданиям, в которых в специальных условиях крутятся сотни болванок и хранят данные многих бизнесов. Дата-центры разделяют на тиры, которые раздает Uptime Institute, где 4-ый тир - самый крутой с самым большим временем безотказной работы

Логическая безопасность

Допустим, что мы работаем в банке:

Пользователь Остаток на счете
A 100
B 200

Чтобы пользователю A перевести пользователю B 50 шекелей, нужно совершить четыре (или больше) действия:

  1. Проверить, что у A есть 50 шекелей
  2. Проверить, что B живой, и его счет тоже живой
  3. Вычесть 50 шекелей у A
  4. Прибавить на счет B 50 шекелей

Заметим, что между 3 и 4 действиями база данных теряет свой инвариант - тогда объединим все эти действия в одну группу.

Транзакция - это последовательность действий с базой данных, в которой либо все действия выполняются успешно, либо не выполняется ни одно из них

Транзакция переводит базу данных из одного логически согласованное состояния в другое логически согласованное Результатом транзакции может быть либо совершение изменений (commit), либо откат к исходному состоянию (rollback)

Чтобы ваша последовательность действий называлась транзакцией, нужно соблюдение этих свойств ACID:

Ситуация: пользователи A и C захотели перевести пользователю B по 100 шекелей

Пользователь Остаток на счете
A 100
B 200
С 100

В итоге может произойти такая последовательность действий:

В итоге произошла гонка обновлений (race-condition) - вторая транзакция слишком рано прочитала баланс у пользователя B и изменила его баланс на 300, когда он и так уже был 300 в ходе работы первой транзакции. Это проблема получила название проблемы потерянного обновления

Решение - 1 уровень изоляции “Незавершенное чтение”: требуем, что бы только одна транзакция могла записывать данные

Другой кейс: C решил перевести 100 шекелей к A, а A, получив от C деньги, захотел перевести 200 шекелей B

В итоге первая транзакция может где-то после начисления денег A откатиться, но вторая транзакция выполнилась до отката первой - у A уже нулевой баланс, поэтому у A в ходе отката получится -100

Возникает проблема грязного чтения, ее решение - 2 уровень изоляции “Завершенное чтение”: если какая-то транзакция изменяет данные, то никакая другая не может их читать

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

Номер счета Пользователь Остаток на счете
123456 A 800
243698 A 200
190428 С 100

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

Произошла проблема неповторяемого чтения. Чтобы решить ее, обложим себя 3 уровнем изоляции - “Воспроизводимое чтение”: если транзакция считывает данные, то никакая другая транзакция не может изменить их

Четвертый случай - пользователь A решил открыть новый счет:

Номер счета Пользователь Остаток на счете
123456 A 800
243698 A 100
190428 С 100
824082 A 1000

Расчет процентов начался перед созданием счета - транзакция не заблокировала действие на создание счета, поэтому начисленный процент опять остался неверным

Появилась проблема фантомного чтения и 4 уровень изоляции, который решает ее - “Сериализуемость” (или “Сериализация”): если транзакция выполняется к данным, то никакая другая транзакция не может изменять или добавлять записи, если они могут быть прочитаны изначальной транзакцией

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