Мои контакты


понедельник, 29 июня 2015 г.

Проектирование высоконагруженных приложений. Часть 3. База данных.

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


База данных.


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

Модель предметной области и типы БД.


По идее, хранилище для данных нужно выбирать исходя из тех данных, которые нужно хранить. Чтобы понять, что и как должно храниться, нужно построить модель предметной области, в которой обозначить потоки движения данных и отношения между сущностями.


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

Есть базы данных общего назначения, которые все знают — MySQL и PostgreSQL. Они применяются на большинстве проектов и при правильном обращении их вполне достаточно. Если перечислять специализированные решения, то получится большой список из десятков проектов. Наиболее популярные — это MongoDB, Redis (у Mail.ru есть своя NoSQL база данных Tarantool, которая тоже показывает неплохие результаты). Однако, для большинства проектов в общем случае будет достаточно стандартных MySQL или PostgreSQL.

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

Тюнинг базы данных.


Тюнинг БД — тема для отдельной книги, здесь вкратце упомяну об основных моментах, на которые нужно обратить внимание.

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

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

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

В-четвертых, излюбленная тема JOIN-ов. В запросах не должно быть JOIN-ов. Если нужно перемножить результаты двух таблиц — делайте это в памяти backend-а. Два (или больше) легких запроса к базе данных, а потом в памяти обрабатываете результаты.

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

Денормализация.


Для повышения эффективности выборки данных можно попробовать размещать их не самым оптимальным способом — денормализованно, т.е. дублировать, хранить в разных форматах и т.д. Преподаватели в университетах будут негодовать :)

Денормализация — это намеренное приведение структуры базы данных в состояние, не соответствующее правилам нормализации, для того, чтобы повысить эффективность чтения данных. В частном случае денормализация позволяет избавиться от JOIN-запросов.

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

Второй пример. Лента в Instagram. Как вы думаете она строится? "Выбери все последние фотографии тех, на кого я подписан, и отсортируй по времени"? Это не будет работать на таких нагрузках и объемах. Там для каждого пользователя есть своя лента, хранящаяся в Redis. Таким образом, один пост там может храниться в сотнях тысяч экземпляров. Да, дублирование, но это позволяет предоставлять пользователям качественный и быстрый сервис.

Пример с Instagram тесно связан с таким понятием как "масштабирование во времени", о котором мы будем говорить в следующей статье.

Шардинг.


На самом деле это основная техника масштабирования базы данных, она же самая сложная. Принцип простой. Вот есть у вас 15 миллионов пользователей. Всю информацию, которая относится к первым пяти миллионам вы храните на первом сервере, ко вторым пяти — на втором, остальные — на третьем. Таким образом, шардинг — это разбиение ваших данных на отдельных серверах.


Принцип-то простой, а вот реализовать это не просто. Самая главная проблема тут — по какому признаку разбивать данные на шарды? По пользователям, по сообщениям или как-то еще? Это сложный вопрос, решить который можно проанализировав опять же предметную область и бизнес-логику. Главное условие — данные внутри шарда должны быть максимально связанными, чтобы обратившись в нужный шард за данными пользователя, можно было сразу получить из него все, что нужно.

Как определять шард, с которого нужно прочитать/записать данные? Остаток от деления ID, первая буква логина и т.д. — эти способы могут сразу приходить на ум, но они не работают. Потому что как только оказывается, что какой-то сервер "вылетает" и нужно сразу вместо пяти серверов использовать три или четыре, то появляется проблемка: формула дает сбой. Об эффективном способе решения этой проблемы в следующем разделе.

Когда данные только добавляются и никогда не удаляются может работать принцип ящиков. Шард — это ящик. Ящик заполнился — добавили новый ящик. Но это такая простая ситуация, которая, как правило, проблем не вызывает. Гораздо интереснее, когда данные на разных шардах могут расти или не расти по разным причинам — так обычно и бывает в реальных проектах.  Есть классический пример с Lady Gaga и Facebook: если вы будете хранить все данные, относящиеся к Lady Gaga на сервере №21, то рано или поздно этот сервер переполнится. Тут уже нужно думать, что делать со всеми этими данными. Главная особенность этого сюжета — непредсказуемость, поэтому нужна гибкая техника, которая называется "виртуальные шарды".

[Рекомендую посмотреть выступление Алексея Рыбака (Badoo) и Константина Осипова (Mail.ru) на конференции Highload++ на тему шардинга: http://www.youtube.com/watch?v=MhGO7BBqSBU]

Виртуальные шарды.


Как определить на какой шард нужно делать запрос? Обычно определяется некоторая функция, которая по ключу отдает шард (номер или IP-адрес сервера).


Технику виртуальных шардов можно представить себе как промежуточное отображение, т.е. сначала вы ключ отображаете на некий промежуточный виртуальный шард, а потом этот виртуальный шард на соответствующий ему физический.

В чем суть? Вы разбиваете все пространство данных на заведомо большое и определяете, например, тысячу виртуальных шардов. При этом у вас на самом деле всего 1 физический сервер. Вы запускаете на этом сервере 10 инстансов PostgreSQL, а в каждом инстансе еще по 100 баз данных. Постепенно система начнет наполняться. Только теперь вы сможете без проблем, используя репликацию, разнести данные на отдельные серверы или инстансы.  Примерно вот так:


Таким образом, виртуальные шарды — это такая прослойка, которая позволяет backend-у общаться с конкретным шардом, при этом не не задумываясь о том, где этот шард физически находится. К примеру, нужно нам найти пользователя. Сначала он вычисляется "виртуально" — определяется виртуальный шард, затем берется какая-то таблица соответствий, по которой выясняется, где этот виртуальный шард находится физически.

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

У вас должен быть реализован какой-то центральный элемент этой системы, который будет знать, как выполняется шардинг. Кто-то должен знать, как "замапить" виртуальный шард на физический. Это уже некая договоренность между backend-ом и звеном хранения. Реализовать этот центральный элемент можно по-разному: это может быть просто конфигурационный файл, в котором вы прописываете соответствия виртуальному шарду физического, либо какой-то сервис, в котором реализуется логика этого же отображения. Все зависит от конкретной ситуации и конкретного проекта.

Репликация.


Что делать, когда один из серверов базы данных выйдет из строя? Срочно его поднимать :) Но в это время обслуживание части пользователей будет невозможно. Чтобы этого не случилось используется репликация. Репликация — это механизм синхронизации данных между несколькими серверами базы данных.

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

[Про настройку репликации в PostgreSQL можно почитать тут]

Партиционирование.


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

Партиционирование помогает, когда в вашем проекте часть данных читается очень активно, а часть — реже. Например, новостной сайт. Самые актуальные новости последней недели будут гораздо чаще читаться пользователями, поэтому здесь имеет смысл хранить эти данные отдельно от остальных, "поближе к пользователям". Вы просто выносите новости последней недели в отдельную таблицу или базу данных, в которую идут все запросы за свежими новостями. Помимо этого, есть "общая" база данных (архив), куда попадают вообще все новости. В эту базу данных идут все запросы за старыми новостями. Настройка партиционирования в PostgreSQL или MySQL выполняется достаточно просто, сейчас это практически полностью автоматизированный процесс.

К партиционированию можно отнести и ситуацию, когда мы начинаем разделять хранилище по типам данных. Например, начинаем хранить сообщения в какой-то NoSQL базе данных, а все остальное — в PostgreSQL. Каждая СУБД имеет какие-то свои фишки, позволяющие ей хранить определенные типы данных более эффективно, чем другие СУБД. Поняв это, можно существенно повысить эффективность всей системы.

Например, в вашем проекте есть возможность вести чат между пользователями. Все данные, включая сообщения чатов, хранятся в РСУБД, к примеру, PostgreSQL. Заметив, что формат общения и хранения данных у MongoDB, nodejs и клиентским javascript одинаковый (JSON), можно существенно снизить ненужные издержки, повысить эффективность чатов, обеспечить автоматическое масштабирование системы хранения сообщений и т.д. Nodejs позволяет писать быстрые и легкие сервера, MongoDB обеспечивает очень быструю запись (и чтение) данных, а также автоматическое масштабирование. Плюс ко всему, они общаются на одном "языке". Разумный и прагматичный подход — выбрать инструменты, которые лучше остальных подходят для решения конкретной задачи.

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