Всем привет. На связи Владислав Родин. В настоящее время я преподаю на портале OTUS курсы, посвященные архитектуре ПО и архитектуре ПО, подверженного высокой нагрузке. В преддверии старта нового потока курса «Архитектор высоких нагрузок» я решил написать небольшой авторский материал, которым хочу поделиться с вами.
Из-за того, что на 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 и откатом транзакции возникает временной зазор, в течении которого параллельные транзакции могут увидеть те изменения, которых в базе быть не должно.
Если немного подумать, то можно всего лишь поменяв шаги алгоритма местами, исправить проблему фантомных чтений в данном сценарии:
Теперь мы коммитим изменения только если они зареплицировались.
Как всегда, идеальных решений не бывает, бывает набор решений, каждое из которых обладает своими преимуществами и недостатками и подходит для решения различных классов задач. Это совершенно верно и для выбора механизма синхронизации данных реплицированной базы данных. Набор преимуществ, которыми обладает полусинхронная репликация, является достаточно солидным и интересным, чтобы ее можно было признать заслуживающей внимания, несмотря на ее малую распространенность.
На этом все. До встречи на курсе!
Введение
Из-за того, что на HDD может выполняться лишь порядка 400-700 операций в секунду (что несравнимо с типичными rps'ами, приходящимися на высоконагруженную систему), классическая дисковая база данных является узким горлышком архитектуры. Поэтому необходимо уделить отдельное внимание паттернам масштабирования данного хранилища.
На текущий момент имеются 2 паттерна масштабирования базы: репликация и шардирование. Шардирование позволяет масштабировать операцию записи, и, как следствие, снижать rps на запись, приходящийся на один сервер вашего кластера. Репликация позволяет делать тоже самое, но с операциями чтения. Именно этому паттерну и посвящена данная статья.
Репликация
Если смотреть на репликацию совсем верхнеуровнево, это простая штука: у вас был один сервер, на нем лежали данные, а затем этот сервер перестал справляться с нагрузкой на чтение этих данных. Вы добавляете еще пару серверов, синхронизируете данные на всех серверах, а пользователь может читать из любого сервера вашего кластера.
Несмотря на кажущуюся простоту, существует несколько вариантов классификации различных реализаций данной схемы:
- По ролям в кластере (master-master или master-slave)
- По пересылаемым объектам (row-based, statement-based или mixed)
- По механизму синхронизации нод
Сегодня мы разберемся именно с 3-им пунктом.
Как происходит коммит транзакции
Данная тема напрямую не относится к репликации, по ней может быть написана отдельная статья, однако поскольку без понимания механизма коммита транзакции дальнейшее прочтение бесполезно, позволю себе напомнить самые основные вещи. Коммит транзакции происходит в 3 этапа:
- Запись транзакции в журнал базы данных.
- Применение транзакции в движке базы данных.
- Возвращение подтверждения клиенту об успешном применении транзакции.
В различных базах в данном алгоритме могут возникать нюансы: например, в движке InnoDB базы MySQL имеется 2 журнала: один для репликации (binary log), а другой для поддержания ACID (undo/redo log), тогда как в PostgreSQL имеется один журнал, выполняющий обе функции (write ahead log = WAL). Но выше представлена именно общая концепция, позволяющая такие нюансы не учитывать.
Синхронная (sync) репликация
Давайте добавим в алгоритм коммита транзакции логику по реплицированию полученных изменений:
- Запись транзакции в журнал базы данных.
- Применение транзакции в движке базы данных.
- Отправка данных на все реплики.
- Получение подтверждения от всех реплик о выполнении на них транзакции.
- Возвращение подтверждения клиенту об успешном применении транзакции.
При данном подходе мы получаем ряд недостатков:
- клиент ждет применения изменений на всех репликах.
- с увеличением числа нод в кластере мы уменьшаем вероятность того, что операция записи пройдет успешно.
Если с 1-ым пунктом все более или менее ясно, то причины 2-го пункта стоит пояснить. Если при синхронной репликации мы не получим ответ хотя бы от одной ноды, мы откатываем транзакцию. Таким образом, увеличивая число нод в кластере, вы увеличиваете вероятность того, что операция записи сорвется.
Можем ли мы ждать подтверждения лишь от некоторой доли нод, например, от 51% (кворум)? Да, можем, однако в классическом варианте требуется подтверждение от всех нод, ведь именно так мы сможем обеспечить полную консистентность данных в кластере, что является несомненным преимуществом такого вида репликации.
Асинхронная (async) репликация
Давайте видоизменим предыдущий алгоритм. Данные на реплики мы будем посылать «когда-то потом», и «когда-то потом» изменения будут на репликах применены:
- Запись транзакции в журнал базы данных.
- Применение транзакции в движке базы данных.
- Возвращение подтверждения клиенту об успешном применении транзакции.
- Отправка данных на реплики и применение изменений ими.
Данный подход приводит к тому, что кластер работает быстро, ведь мы не держим клиента в ожидании пока данные дойдут до реплик да еще и будут закоммичены.
Но условие сбрасывания данных на реплики «когда-то потом» может привести к потери транзакции, причем к потери подтвержденной пользователю транзакции, ведь если данные не успели зареплицироваться, подтверждение клиенту об успешности выполнения операции отправлено, а у ноды, на которую пришли изменения, вылетел HDD, мы теряем транзакцию, что может приводить к очень неприятным последствиям.
Полусинхронная (semisync) репликация
Наконец-то мы добрались до полусинхронной репликации. Такой вид репликации не очень известен и не очень распространен, однако он представляет немалый интерес, поскольку он может совместить преимущества как синхронной, так и асинхронной репликаций.
Попробуем объединить 2 предыдущих подхода. Не будем долго держать клиента, но потребуем, чтобы данные зареплицировались:
- Запись транзакции в журнал базы данных.
- Применение транзакции в движке базы данных.
- Отправка данных на реплики.
- Получение подтверждения от реплики о получении изменений (применены они будут «когда-то потом»).
- Возвращение подтверждения клиенту об успешном применении транзакции.
Обратите внимание на то, что при таком алгоритме потеря транзакции происходит только в случае падения как ноды, принимающей изменения, так и ноды-реплики. Вероятность такого сбоя признается малой, и данные риски принимаются.
Но при данном подходе возможен риск фантомных чтений. Представим себе следующий сценарий: на шаге 4 мы не получили подтверждение ни от одной реплики. Мы должны данную транзакцию откатывать, а клиенту подтверждение не возвращать. Поскольку данные были применены на шаге 2, между окончанием шага 2 и откатом транзакции возникает временной зазор, в течении которого параллельные транзакции могут увидеть те изменения, которых в базе быть не должно.
Lose-less semisync репликация
Если немного подумать, то можно всего лишь поменяв шаги алгоритма местами, исправить проблему фантомных чтений в данном сценарии:
- Запись транзакции в журнал базы данных.
- Отправка данных реплики.
- Получение подтверждения от реплики о получении изменений (применены они будут «когда-то потом»).
- Применение транзакции в движке базы данных.
- Возвращение подтверждения клиенту об успешном применении транзакции.
Теперь мы коммитим изменения только если они зареплицировались.
Вывод
Как всегда, идеальных решений не бывает, бывает набор решений, каждое из которых обладает своими преимуществами и недостатками и подходит для решения различных классов задач. Это совершенно верно и для выбора механизма синхронизации данных реплицированной базы данных. Набор преимуществ, которыми обладает полусинхронная репликация, является достаточно солидным и интересным, чтобы ее можно было признать заслуживающей внимания, несмотря на ее малую распространенность.
На этом все. До встречи на курсе!
gbg
400-700 IOPS на HDD!? Даже SAS-диски на 15k RPM едва-едва переползают за 200.
zuborg
raid1 почти линейно масштабируется относительно random seek iops, raid0 и raid10 бывает тоже, если stripe и нагрузка подходящие
gbg
Полагаю, стоит уточнить, что речь идет о дисковом массиве, а не о единичном диске.