- PVSM.RU - https://www.pvsm.ru -
Всем привет. На связи Владислав Родин. В настоящее время я преподаю на портале OTUS курсы, посвященные архитектуре ПО и архитектуре ПО, подверженного высокой нагрузке. В преддверии старта нового потока курса «Архитектор высоких нагрузок» [1] я решил написать небольшой авторский материал, которым хочу поделиться с вами.
Из-за того, что на HDD может выполняться лишь порядка 400-700 операций в секунду (что несравнимо с типичными rps'ами, приходящимися на высоконагруженную систему), классическая дисковая база данных является узким горлышком архитектуры. Поэтому необходимо уделить отдельное внимание паттернам масштабирования данного хранилища.
На текущий момент имеются 2 паттерна масштабирования базы: репликация и шардирование. Шардирование позволяет масштабировать операцию записи, и, как следствие, снижать rps на запись, приходящийся на один сервер вашего кластера. Репликация позволяет делать тоже самое, но с операциями чтения. Именно этому паттерну и посвящена данная статья.
Если смотреть на репликацию совсем верхнеуровнево, это простая штука: у вас был один сервер, на нем лежали данные, а затем этот сервер перестал справляться с нагрузкой на чтение этих данных. Вы добавляете еще пару серверов, синхронизируете данные на всех серверах, а пользователь может читать из любого сервера вашего кластера.
Несмотря на кажущуюся простоту, существует несколько вариантов классификации различных реализаций данной схемы:
Сегодня мы разберемся именно с 3-им пунктом.
Данная тема напрямую не относится к репликации, по ней может быть написана отдельная статья, однако поскольку без понимания механизма коммита транзакции дальнейшее прочтение бесполезно, позволю себе напомнить самые основные вещи. Коммит транзакции происходит в 3 этапа:
В различных базах в данном алгоритме могут возникать нюансы: например, в движке InnoDB базы MySQL имеется 2 журнала: один для репликации (binary log), а другой для поддержания ACID (undo/redo log), тогда как в PostgreSQL имеется один журнал, выполняющий обе функции (write ahead log = WAL). Но выше представлена именно общая концепция, позволяющая такие нюансы не учитывать.
Давайте добавим в алгоритм коммита транзакции логику по реплицированию полученных изменений:
При данном подходе мы получаем ряд недостатков:
Если с 1-ым пунктом все более или менее ясно, то причины 2-го пункта стоит пояснить. Если при синхронной репликации мы не получим ответ хотя бы от одной ноды, мы откатываем транзакцию. Таким образом, увеличивая число нод в кластере, вы увеличиваете вероятность того, что операция записи сорвется.
Можем ли мы ждать подтверждения лишь от некоторой доли нод, например, от 51% (кворум)? Да, можем, однако в классическом варианте требуется подтверждение от всех нод, ведь именно так мы сможем обеспечить полную консистентность данных в кластере, что является несомненным преимуществом такого вида репликации.
Давайте видоизменим предыдущий алгоритм. Данные на реплики мы будем посылать «когда-то потом», и «когда-то потом» изменения будут на репликах применены:
Данный подход приводит к тому, что кластер работает быстро, ведь мы не держим клиента в ожидании пока данные дойдут до реплик да еще и будут закоммичены.
Но условие сбрасывания данных на реплики «когда-то потом» может привести к потери транзакции, причем к потери подтвержденной пользователю транзакции, ведь если данные не успели зареплицироваться, подтверждение клиенту об успешности выполнения операции отправлено, а у ноды, на которую пришли изменения, вылетел HDD, мы теряем транзакцию, что может приводить к очень неприятным последствиям.
Наконец-то мы добрались до полусинхронной репликации. Такой вид репликации не очень известен и не очень распространен, однако он представляет немалый интерес, поскольку он может совместить преимущества как синхронной, так и асинхронной репликаций.
Попробуем объединить 2 предыдущих подхода. Не будем долго держать клиента, но потребуем, чтобы данные зареплицировались:
Обратите внимание на то, что при таком алгоритме потеря транзакции происходит только в случае падения как ноды, принимающей изменения, так и ноды-реплики. Вероятность такого сбоя признается малой, и данные риски принимаются.
Но при данном подходе возможен риск фантомных чтений. Представим себе следующий сценарий: на шаге 4 мы не получили подтверждение ни от одной реплики. Мы должны данную транзакцию откатывать, а клиенту подтверждение не возвращать. Поскольку данные были применены на шаге 2, между окончанием шага 2 и откатом транзакции возникает временной зазор, в течении которого параллельные транзакции могут увидеть те изменения, которых в базе быть не должно.
Если немного подумать, то можно всего лишь поменяв шаги алгоритма местами, исправить проблему фантомных чтений в данном сценарии:
Теперь мы коммитим изменения только если они зареплицировались.
Как всегда, идеальных решений не бывает, бывает набор решений, каждое из которых обладает своими преимуществами и недостатками и подходит для решения различных классов задач. Это совершенно верно и для выбора механизма синхронизации данных реплицированной базы данных. Набор преимуществ, которыми обладает полусинхронная репликация, является достаточно солидным и интересным, чтобы ее можно было признать заслуживающей внимания, несмотря на ее малую распространенность.
На этом все. До встречи на курсе [1]!
Автор: rodinvv
Источник [2]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/web-razrabotka/348819
Ссылки в тексте:
[1] «Архитектор высоких нагрузок»: https://otus.pw/jvDY/
[2] Источник: https://habr.com/ru/post/491106/?utm_campaign=491106&utm_source=habrahabr&utm_medium=rss
Нажмите здесь для печати.