Книга «Проектирование приложений, интенсивно использующих данные» (Designing Data Intensive Applications, DDIA) обязательна к прочтению, если вы интересуетесь backend -инжинирингом. Мир инженерии данных полон модных словечек и хайпа, но Мартин Клеппман проделал потрясающую работу по анализу всех основных технологий.
Вот краткое изложение главы 5 из DDIA о репликации.
Репликация — это когда вы храните копию своих данных на нескольких разных машинах. Эти машины связаны через сеть, поэтому все они доступны для ваших backend-серверов.
Вместо того, чтобы использовать одну машину в качестве вашей базы данных, вы теперь используете распределенную базу данных, хранящуюся на нескольких машинах.
Есть несколько причин, по которым вы хотите реплицировать свои данные на несколько компьютеров.
- Сокращение задержки — пользователи в Индии могут отправлять запросы на узлы, расположенные в Дели, а пользователи в Америке могут отправлять запросы на узлы, расположенные в Нью-Йорке.
- Лучшая доступность — если один из узлов по какой-либо причине выйдет из строя, у вас будет другой узел, который сможет взять на себя управление и отвечать на запросы данных.
- Увеличение пропускной способности при чтении — сразу несколько узлов могут отвечать на запросы на чтение вместо того, чтобы одна машина выполняла всю работу. Многие рабочие задачи масштабируются по чтению (состоят в основном из операций чтения и небольшого процента операций записи), поэтому увеличение пропускной способности операций чтения чрезвычайно полезно.
Трудная часть репликации заключается в обработке изменений в реплицированных данных.
Когда вы получаете запрос на запись, который изменяет вашу базу данных, как убедиться, что все реплики отражают эти новые данные?
Как запретить репликам, которые не обновились, отвечать устаревшими данными на запросы чтения?
Есть 3 популярные стратегии записи изменений во все ваши реплики
- Репликация с одним лидером (Single Leader Replication) — один узел назначается ведущим. Остальные узлы являются последователями. Запросы на запись поступают к ведущему узлу, который затем распространяет изменения на последователей. Это стратегия репликации, используемая многими базами данных, такими как PostgreSQL, MongoDB, MySQL и другими.
- Репликация с несколькими лидерами (Multi Leader Replication) — аналогична репликации с одним ведущим, но теперь несколько узлов могут выступать в качестве ведущих и обрабатывать запросы на запись. Репликация с несколькими лидерами обычно реализуется с помощью внешних инструментов, таких как Tungstein Replicator для MySQL, BDR для PostgreSQL и GoldenGate для Oracle.
- Репликация без лидера (Leaderless Replication) — все узлы-реплики могут принимать запросы на запись от клиентов, поэтому нет узла-лидера. Riak и Cassandra являются примерами баз данных, использующих стратегии репликации без лидера. Amazon использовал репликацию без лидера для своей собственной системы Dynamo, поэтому Riak и Cassandra также известны как Dynamo-стиль.
Примечание. Система Dynamo Amazon отличается от DynamoDB Amazon. DynamoDB основан на многих принципах Dynamo, но имеет другую реализацию. DynamoDB использует репликацию с одним лидером.
Почти все распределенные базы данных используют один из этих трех подходов, и все они имеют свои плюсы и минусы.
Тем не менее, репликация с одним лидером является наиболее популярной стратегией репликации для распределенных баз данных. Поэтому мы углубимся в стратегию с одним лидером. Если вам интересно узнать больше о стратегиях с несколькими лидерами или без лидеров, ознакомьтесь с самой книгой.
Репликация с одним ведущим узлом
Репликация с одним лидером работает следующим образом.
- Одна из реплик создается ведущей (leader). Запросы на запись от клиентов будут отправляться лидеру, который запишет новые данные в свое локальное хранилище.
- Другие реплики известны как последователи (follower-ы). Всякий раз, когда лидер записывает новые данные в свое локальное хранилище, он также отправляет изменения данных всем последователям.
- Каждый фоловер получает журнал изменений данных от ведущего и обновляет свою локальную копию базы данных, применяя все новые записи.
- Когда клиент хочет прочитать из базы данных, запросы на чтение могут быть отправлены на любой из узлов в базе данных — лидер или ведомый.
Запись в базу данных может быть асинхронной, синхронной и полусинхронной.
Для асинхронной записи лидер получит запрос клиента на запись и обновит собственное локальное хранилище. Затем он ответит, что запись прошла успешно. После этого ответа лидер отправит сообщение всем узлам-последователям с изменением данных, которые содержались в запросе клиента на запись.
При синхронной записи ведущий сначала удостоверится, что каждый подчиненный узел записал изменение данных в свою локальную базу данных. Как только ведущий узел получит подтверждение от всех последователей, он ответит сообщением об успешной записи.
Для полусинхронной записи лидер будет ждать подтверждения записи от определенного количества узлов-последователей (этот параметр можно настроить), пока не ответит сообщением об успешной записи.
На практике синхронная запись используется редко. При синхронной стратегии записи запросы на запись будут занимать очень много времени (поскольку вам придется ждать ответа от каждого фоловера) и часто будут завершаться сбоем (в случае, когда один или несколько узлов-последователей не отвечают).
Поэтому инженеры обычно используют полусинхронную или асинхронную стратегию.
Компромисс между полусинхронной и асинхронной стратегиями записи сводится к тому, насколько быстро вы хотите обрабатывать свои запросы на запись (асинхронная запись выполняется быстрее) и насколько надежными вы хотите, чтобы ваши запросы на запись были (асинхронные стратегии записи имеют больше шансов потерять данные записи, если узел-лидер выходит из строя перед отправкой изменений записи подписчикам).
Две проблемы, которые часто возникают при репликации с одним лидером:
- Обработка недоступности узла
- Задержка репликации
Обработка сбоев узла
Отключения узлов неизбежны, особенно если вы используете большую распределенную базу данных с множеством подчиненных узлов.
Существует два типа сбоев узла: сбои ведомого узла и сбои ведущего узла.
Сбой фоловера: догоняющее восстановление
Если узел-последователь выходит из строя, он может довольно легко восстановиться. Последователи ведут журнал всех изменений данных, полученных от лидера, в локальном энергонезависимом хранилище. Следовательно, подписчик знает последнюю обработанную им транзакцию.
Последователь запросит у лидера все изменения, произошедшие с момента последней транзакции, а затем обновит свое локальное состояние, чтобы оно соответствовало актуальному.
Сбой лидера: отказоустойчивость
Справиться с отказом лидера сложнее. Один из узлов-последователей должен быть повышен до нового лидера, а клиенты должны быть перенастроены для отправки своих записей новому лидеру. Другие последователи также должны начать получать изменения данных от нового лидера.
Этот процесс называется отказоустойчивостью.
В процессах аварийного переключения есть множество вещей, которые могут пойти не так
- Если используется асинхронная репликация, новый лидер может не получить все записи от старого лидера до того, как произойдет сбой. Это означает более слабые гарантии надежности.
- Когда первоначальный лидер возвращается в сеть, он может быть неправильно сконфигурирован и думать, что он все еще является лидером. Это распространенная ошибка, которую часто называют “расщепленным мозгом” (split brain).
- Могут возникнуть проблемы с загрузкой, поскольку ваша база данных не может принимать новые записи, пока происходит процесс отработки отказа. Если ведущий узел часто выходит из строя, это может привести к засорению базы данных.
Задержка репликации
Когда вы используете стратегию репликации с одним лидером с полусинхронной или асинхронной записью, вы часто сталкиваетесь с проблемами согласованности, когда клиент будет считывать устаревшие данные с узла-последователя, который не был полностью обновлен.
Это несоответствие является временным состоянием, и если вы немного подождете, то все последователи в конечном итоге наверстают упущенное. Поэтому этот эффект известен как окончательная согласованность (eventual consistency).
Однако окончательная согласованность — это расплывчатый термин, и он не указывает, как долго длится задержка репликации. Это может быть несколько секунд или даже несколько минут.
Поэтому даже при «окончательной согласованности» задержка репликации может стать большой проблемой для ваших пользователей.
Чтобы облегчить эти проблемы, существует несколько подходов, которые можно использовать для уменьшения некоторых распространенных проблем, с которыми сталкиваются пользователи.
Мы рассмотрим некоторые из этих подходов и проблемы, которые они решают.
Чтение собственных записей
Допустим, вы создаете клон Twitter. Пользователь может опубликовать твит со своего компьютера, который отправит запрос на запись в вашу распределенную базу данных.
Этот запрос на запись реплицируется асинхронно, поэтому лидер ответит, что запись прошла успешно после изменения его локального состояния. Затем он отправит изменение всем узлам-последователям.
Если пользователь обновит свою страницу сразу после публикации твита и попытается перезагрузить ее, новый запрос на чтение предыдущих твитов пользователя может перейти к узлу-последователю, который еще не был проинформирован о новом твите.
Поэтому профиль пользователя в Твиттере не будет показывать его новый твит после того, как он обновится.
Очевидно, что это может сильно разочаровать пользователей.
Согласованность “Чтение своих собственных записей” (Read Your Own Writes) — это решение, которое гарантирует, что если пользователь перезагрузит страницу, он всегда увидит все обновления, которые он отправил сам. Она, правда, не дает никаких обещаний в отношении других пользователей.
Согласованность Read Your Own Writes может быть реализована разными способами. Один из возможных способов — отследить, когда пользователь в последний раз отправлял обновление. Если он отправил обновление в течение последней минуты, то запросы на чтение должен обрабатываться главным узлом.
Монотонное чтение
Возвращаясь к примеру с клоном Twitter, асинхронная репликация приведет к тому, что некоторые базы-фоловеры будут отставать от других узлов с точки зрения обновлений.
Таким образом, пользователь может посетить сайт и получить твиты от базы-фоловера, который обновлен. После этого он может перезагрузить свою страницу, а затем получить твиты от узла-подписчика, который отстает.
Это приведет к тому, что его лента в Твиттере «переместится назад во времени», поскольку данные, которые он получает, устарели. Это, очевидно, означает плохой пользовательский опыт.
Монотонное чтение является гарантией того, что такого рода аномалии не произойдет.
Одним из способов достижения этой гарантии является обеспечение того, чтобы каждый пользователь всегда читал из одного и того же узла-последователя (разные пользователи могут читать с разных реплик).
Реплика может быть выбрана на основе хэша идентификатора пользователя, а не случайным образом.
Согласованное префиксное префикса
Допустим, у вас есть пользователь A и пользователь Б в вашем приложении-клоне Twitter. Пользователь А публикует в Твиттере фотографию своей собаки. Пользователь Б отвечает на это фото в твиттере комплиментом собаке.
Существует причинно-следственная связь между двумя твитами, когда ответный твит пользователя Б не имеет никакого смысла, если вы не видите твит пользователя А.
Пользователь C подписан как на пользователя A, так и на пользователя Б. Если твит пользователя A проходит через большую задержку репликации, чем твит пользователя Б, то пользователь C может увидеть ответный твит пользователя Б, не получив твит пользователя A.
Согласованное префиксное чтение (Consistent Prefix Reads) — это гарантия, устраняющая эту аномалию. Эта гарантия гласит, что если последовательность записей приходит в определенном порядке, то любой, кто читает эти записи, увидит, что они появлялись именно в таком же порядке.
Это можно сделать, если база данных всегда применяет процедуры записи в одном и том же порядке, но при сегментировании базы данных возникают сложности. Проверьте DDIA для получения более подробной информации об этом.