Обеспечение отказоустойчивости хранилищ



    Всем привет! Недавно состоялся открытый вебинар «Обеспечение отказоустойчивости хранилищ». На нём рассмотрели, какие проблемы возникают при проектировании архитектур, почему выход из строя серверов — это не оправдание для падения сервера и как сокращать время простоя до минимума. Вебинар провёл Иван Ремень, руководитель направления серверной разработки в «Ситимобил» и преподаватель курса «Архитектор высоких нагрузок».



    Зачем задумываться об отказоустойчивости хранилищ?


    Задуматься об отказоустойчивости масштабируемых хранилищ и понять базовые проблемы кэширования следует ещё на стадии стартапа. Понятно, что когда вы пишете стартап, вы в самом начале делаете минимальную версию продукта. Но чем сильнее вы будете расти, тем быстрее вы упрётесь в производительность, что может привести к полной остановке бизнеса. А если вы получите деньги от инвесторов, то они, разумеется, тоже будут требовать постоянного роста и новых бизнес-фич. Чтобы найти правильный баланс, нужно выбирать между скоростью и качеством. При этом нельзя жертвовать ни тем, ни другим, а если жертвовать — то осознанно и в определённых пределах. Впрочем, универсальных рецептов здесь нет, как и идеальных решений.

    Упираемся в базу на чтение


    Это первый сценарий развития событий. Представьте, что у нас 1 сервер, нагрузка на процессор или жёсткий диск которого составляет 99 %. При этом:

    • 90 % запросов — это чтение;
    • 10 % запросов — это запись.

    Лучший выход в данной ситуации — подумать о репликах. Почему? Это самое дешёвое и самое простое решение.



    Репликация классифицируется:

    1. По синхронности:

    • синхронная;
    • асинхронная;
    • полусинхронная.

    2. По переносимым данным:

    • логическая (row-based, statement-based, mixed);
    • физическая.

    3. По числу нод на запись:

    • master/slave;
    • master/master.

    4. По инициатору:

    • pull;
    • push.

    А теперь задача про ведро воды. Представьте, что у нас MySQL и асинхронная master-slave репликация. В ДЦ идёт уборка, в результате которой уборщица спотыкается и выливает ведро воды на сервер с master-базой. Автоматика успешно переключает один самый свежий слейв в master-режим. И всё продолжает работать. Где подвох?

    Ответ прост — у нас теряются транзакции, которые мы не успели зареплицировать. Следовательно, нарушается свойство D из ACID.

    Теперь поговорим о том, как работает асинхронная репликация (MySQL):

    1. запись транзакции в движок хранилища (InnoDB);
    2. запись транзакции в бинарный лог;
    3. завершение транзакции в движке хранилища;
    4. возвращение подтверждения клиенту;
    5. передача части лога в реплику;
    6. выполнение транзакции на реплике (п. 1-3).

    А теперь вопрос, что нужно поменять в вышеперечисленных пунктах, чтобы у нас никогда в конечном итоге не терялись репликации?

    А поменять местами нужно всего два пункта: 4-й и 5-й («передача части лога в реплику» и «возвращение подтверждения клиенту»). Таким образом, если у нас вылетит мастер ноды, у нас всегда где-то останется лог транзакций (пункт 2). А если запись транзакции в бинарный лог прошла, значит выполнение транзакции тоже когда-нибудь произойдёт.

    В результате мы получаем полусинхронную репликацию (MySQL), которая работает следующим образом:

    1. запись транзакции в движок хранилища (InnoDB);
    2. запись транзакции в бинарный лог;
    3. завершение транзакции в движке хранилища;
    4. передача части лога в реплику;
    5. возвращение подтверждения клиенту;
    6. выполнение транзакции на реплике (п. 1-3).

    Sync vs semi-sync и async vs semi-sync


    Почему-то в России большинство людей не слышали про полусинхронную репликацию. К слову, она хорошо реализована в PostgreSQL и не очень в MySQL. Подробнее об этом читайте здесь, но тезисно можно сформулировать следующее:

    • полусинхронная репликация всё также отстает (но не так сильно), как и асинхронная;
    • мы не теряем транзакции;
    • достаточно довезти данные только до одного слейва.

    Кстати, полусинхронная репликация применяется в Facebook.

    Упираемся в базу на запись


    Поговорим о диаметрально противоположной проблеме, когда имеем:

    • 90 % запросов — запись;
    • 10 % запросов — чтение;
    • 1 сервер;
    • нагрузку — 99 % (процессор или жёсткий диск).

    Здесь приходит на помощь всем известный шардинг. Но сейчас давайте поговорим о другом:



    Очень часто в таких случаях начинают применять master-master. Однако он не помогает в данной ситуации. Почему? Всё просто: записи на сервер не становится меньше. Ведь репликация подразумевает, что данные есть на всех нодах. Со statement-based репликацией, по сути, SQL будет выполняться на ВСЕХ нодах. C row-based чуть полегче, но всё равно дорого. А ещё master-master имеет проблемы с конфликтами.

    На самом деле master-master имеет смысл использовать в следующих ситуациях:

    • отказоустойчивость по записи (идея в том, что вы всегда пишете только в один master). Реализовать можно с помощью Virtual IP address;
    • геораспределённые системы.

    Однако следует помнить, что репликация master-master — это всегда сложно. И нередко master-master приносит больше проблем, чем решает.

    Шардинг


    Мы уже упоминали шардинг. Если кратко, то шардинг — это верный способ масштабировать запись. Идея в том, что мы распределяем данные по независимым (но не всегда) серверам. Каждый шард может реплицироваться независимо.

    Первое правило шардинга — данные, которые используются вместе, должны лежать в одном шарде. Здесь работает формула sharding_key -> shard_id. Соответственно, sharding_key у данных, которые используются вместе, должны совпадать. Первая сложность заключается в том, что если вы неправильно подберёте sharding_key, вам потом будет очень сложно всё перешардировать. Во-вторых, если у вас есть какой-то sharding_key, некоторые запросы будут выполняться очень тяжело. Например, вы не сможете найти среднее значение.

    Чтобы это продемонстрировать, давайте представим, что у нас есть два шарда по три значения в каждом: (1; 2; 3) (0; 0; 500). Среднее значение будет равно (1+2+3+500)/6 = 84,33333.

    А теперь представьте, что у нас два независимых сервера. И пересчитаем среднее значение отдельно по каждому шарду. На первом из них получим 2, на втором — 166,66667. И даже если мы потом эти значения усредним, мы всё равно получим число, которое будет отличаться от правильного: (2+166,66667)/2 = 86,33334.

    То есть среднее средних не равно среднему от всего:

    avg(a, b, c, d) != avg(avg(a, b) + (avg(c, d))

    Простая математика, но её важно помнить.

    Задача на шардинг


    Допустим, у нас есть система диалогов в социальной сети. В диалоге может быть только 2 человека. Все сообщения лежат в одной таблице, в которой есть:

    • ID сообщения;
    • ID отправителя;
    • ID получателя;
    • текст сообщения;
    • дата отправки сообщения;
    • какие-нибудь флаги.

    Какой ключ шардирования стоит выбрать, исходя из того, что у нас есть первое правило шардирования, описанное выше?

    Вариантов решения этой классической задачи несколько:

    • crc32(id_src//id_dst);
    • crc32(1//2) != crc32(2//1);
    • crc32(from+to)%n;
    • crc32(min(from, to). max(from, to))%n.

    Кеши


    И пару слов про кеши. Можно сказать, что кеши — это антипаттерн, хотя с данным утверждением можно спорить (многие любят применять кешы). Но по большему счёту кеши нужны только для повышения скорости отдачи ответа. И их нельзя ставить, чтобы держать нагрузку.

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

    Проблемы кешей тезисно:

    • старт с холодным кешем;
    • проблема инвалидации кеша;
    • консистентность кеша.

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



    Что ж, спасибо за внимание! Чтобы ничего не пропустить из прошедшей лекции, лучше смотрите вебинар полностью.
    OTUS. Онлайн-образование
    Цифровые навыки от ведущих экспертов

    Комментарии 3

      +1
      И даже если мы потом эти значения усредним, мы всё равно получим число, которое будет отличаться от правильного: (2+166,66667)/2 = 86,33334

      Неверно, на самом деле:
      (2+166.66667)/2 = 84.333335

      Т.е. оно совпадает со средним от всего.

      Я думаю это из-за того что на каждом сервере количество элементов одинаково, вот поэтому пример получился неудачным, не смогли примером доказать про среднее среднего…

        0
        Если использование кэша на отдачу данных по частым одинаковым запросам (за не столь частыми исключениями) — это антипаттерн, то паттерны — это зло…
          +1

          Добрый день, я автор вэбинара.


          Для ускорения ответа — идеально. Для того, чтобы держать нагрузку — антипатерн. Уйдёт кэш, тут же ляжет база.

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое