Базы данных и NoSQL (Гл. 4 книги «Масштабируемые и высокопроизводительные веб-приложения»)
В этой главе мы обсуждаем базы данных, реляционные и NoSQL, которые работают на одной машине. Именно этот режим работы будет являться тем кирпичиком, на котором строятся распределенные базы данных.Накопление знанийВажнейший критерий выбора той или иной базы данных — накопленные знания о ней. Например, реляционной модели несколько десятков лет, и за это время накопилось огромное количество знаний о том, как правильно уместить в нее ту или иную предметную область. Распространенные базы данных, такие как MySQL, накопили огромное количество информации о ее поведении в разных условиях, а также о том, какие системы в принципе можно сконструировать на основе этой базы данных, так или иначе изменив ее или приспособив.Никогда не следует сбрасывать со счетов это богатство. Всегда надо понимать, что новая неизвестная система должна будет пройти через период накопления знаний в рамках каждой конкретной команды разработчиков. Как предметная область укладывается в модель данных этой базы? Как эта база ведет себя в разных условиях, при разных паттернах доступа к ней? Как она ведет себя при отказе оборудования? Насколько можно раздвинуть рамки применения базы от стандартных? Где можно получить совет или воспользоваться чужим опытом работы с базой, бесплатно или за деньги?
Реляционная модель данных Реляционной модели данных несколько десятков лет. В реляционной модели база состоит из таблиц, которые состоят из строк и колонок. У каждой колонки есть свой тип данных (строка, число, логическое значение, дата, текст, бинарный блоб). Все строки однотипны.Обычно каждый вид объектов хранится в отдельной таблице (например, таблица пользователей или таблица проектов). Обычно у каждого объекта есть уникальный идентификатор. Идентификатор может быть как условным, т. е. просто числом, так и вытекающим из предметной области, например номер паспорта человека или ISBN для книг. Обычно пользуются условными идентификаторами.
Объекты (таблицы) могут быть связаны друг с другом с помощью идентификатора. Например, если у нас есть таблица отделов и таблица сотрудников, то в таблице отделов есть идентификатор отдела, а в таблице сотрудников есть идентификатор сотрудника и идентификатор отдела, к которому он принадлежит. В теории реляционных баз данных этот случай называется «один ко многим» (одному отделу принадлежит много сотрудников).
Возможен также случай «многие ко многим». Например, есть таблица проектов и таблица разработчиков. Над одним проектом могут работать много разработчиков, и один разработчик может работать над несколькими проектами. В этом случае обычно создается третья таблица — таблица связей с двумя полями: идентификатором проекта и идентификатором разработчика. Каждая связь между разработчиком и проектом выражается в виде строки в таблице связей. Если разработчик пока еще не назначен ни на один проект, то в таблице связей просто не будет ни одной записи про него.
Серверы реляционных БД обеспечивают стандартные операторы доступа к данным в таблицах, такие как SELECT, INSERT, UPDATE и DELETE. Разные серверы предоставляют также некоторые дополнительные операторы. Извлекать данные из таблиц можно по множеству различных критериев. Есть «ядро» стандарта SQL, который поддерживается практически всеми серверами, и всегда есть те или иные расширения стандарта, которые можно использовать при работе с конкретным сервером БД.
Оптимизация доступа «Жесткость» реляционной модели данных позволяет различные оптимизации доступа к данным. Самый очевидный пример — это создание индексов по полям таблицы для быстрого доступа. Например, на таблице сотрудников можно создать индекс на поле «идентификатор отдела», и тогда операция «получить список сотрудников того или иного отдела» будет работать быстрее. Индекс — это просто материализованная структура данных (см. гл. 1), такая как B-дерево или хэш. Важно понимать, как устроена эта структура данных, чтобы можно было делать выводы о том, как она будет работать в том или ином случае.Важную роль в проектировании реляционной БД играет нормализация и денормализация модели данных. Нормальная форма БД — это такая, где информация не повторяется. Для скорости и эффективности иногда базу данных денормализуют, и тогда в ней появляется дублирующая информация.
Например, у нас есть таблица клиентов и таблица продаж. Некоторые клиенты считаются «важными», потому что они купили на сумму больше N. Мы могли бы каждый раз для каждого клиента извлекать список его продаж, суммировать стоимость и сравнивать ее с N. В то же время для скорости мы можем добавить в таблицу клиентов поле-флажок «важный» и постоянно поддерживать его в целостном состоянии — например, при заведении новой продажи проверять, стала ли общая сумма больше N и если да, то выставлять этот флажок в «TRUE». При ошибках программирования такие поля могут рассинхронизироваться и тогда базу данных приходится «чинить».
Удачная денормализация может сильно увеличить производительность. Однако, денормализация — это не панацея, она может привести и к негативным последствиям.
Как эффективно работать в реляционной БД с такими структурами данных, как иерархическое дерево или граф? За многие годы накоплены огромный опыт в этой области. Например, иерархические деревья для скорости можно хранить с помощью materialized path.Данные в стиле key-value можно хранить как в виде очевидной трех-колоночной таблицы «id_объекта-ключ-значение», так и (иногда) в виде «широкой» таблицы.
Для некоторых структур данных, начиная с определенного размера, практически невозможно эффективно уложиться в реляционную БД, и приходится использовать специализированные решения. Например, граф дружеских связей между миллиардом людей практически невозможно обрабатывать с помощью стандартных графовых алгоритмов в рамках реляционной модели даже на современном оборудовании.
Другие модели данных Одно из значений термина «NoSQL» — это отход от реляционной модели в пользу более специфических (или более обобщенных) моделей данных. Например, традиционно успешными NoSQL-системами являются системы хранения пар «ключ-значение», такие как Redis или Memcache. Их модель данных предельно проста — это в сущности ассоциативный массив, где ключи имеют строковый тип, а значения могут содержать любые данные. Как и любой ассоциативный массив, такие системы поддерживают ограниченный набор операций с данными — прочитать значение по ключу, установить значение ключа, удалить ключ и связанное с ним значение. Операция «получить список ключей» может не поддерживаться в таких системах.Другой пример успешных NoSQL-систем — это документные хранилища. Объекты в таких хранилищах обычно являются ассоциативными массивами свободной структуры, то есть в одной и той же «таблице» могут храниться разные по сути объекты. Примеры систем такого класса — MongoDB и Cassandra. В зависимости от того, какие реально данные хранятся в конкретной базе, ее производительность может сильно варьироваться. Например, если оптимизировать такую «таблицу», храня в ней однотипные объекты,
Третий пример специализированных NoSQL-систем — это графовые базы данных. Они специальным образом заточены под обработку конкретной структуры данных, причем обычно для работы с большим объемом данных (потому что на небольших объемах может прекрасно справиться стандартная реляционная реализация).
Очень важным примером NoSQL-систем являются обычные файловые системы, такие как Ext4 или NTFS. Они предназначены для хранения объектов в виде иерархической структуры с содержимым свободного формата. Сами базы данных, реляционные и NoSQL, обычно используют для хранения своего содержимого именно файловые системы, и иногда взаимодействие между этими двумя подсистемами становится важным в том или ином случае.
Еще один важный случай — системы полнотекстового поиска, такие как Elastic Search или Google Search Engine.
Большие объемы и сложные алгоритмы Принципиальная проблема проектирования системы, использующей базы данных, состоит в том, что на сравнительно небольших объемах данных работает практически любая система, а при сравнительно больших объемах данных — постепенно перестает работать практически любая система. Это означает, что в процессе развития системы и увеличения объема данных приходится заново продумывать работу с данными, менять модель хранения данных или даже заменять сервер БД на другой.Традиционно считается, что увеличение объема данных на каждый следующий порядок требует перепроектирования базы данных. Иногда с этим пытаются бороться, проектируя базу сразу на два-три порядка вперед, однако это не всегда возможно в полной мере. Вопрос работы с увеличивающимися данными — нерешенная в общем случае инженерная задача.
Другой стандартной проблемой является внезапно появляющаяся необходимость применять к уже существующим данным новые алгоритмы, обычно с высокими требованиями по скорости. Например, в некоторой компании хранится вся информация о продажах товарах, пригодная для бухгалтерии и ежемесячных отчетов. Однако вызовы времени требуют начать ежедневно и ежечасно анализировать информацию об истории продаж и принимать бизнес-решения на основе этого анализа — в какие магазины направлять товары, какие рекламные кампании стартовать, что еще предлагать людям, которые покупают те или иные товары. Такие алгоритмы могут потребовать принципиально изменить способ хранения данных, при этом сохраняя совместимость с существующей системой и с существующими данными. Вопрос работы в таких условиях — нерешенная инженерная задача.
Поведение при отказе оборудования Любое оборудование рано или поздно откажет: диски, память, процессор, электрическое питание и т. д. В этой главе мы рассмотрим случай одной физической машины, на которой крутится сервер. Пусть у этой физической машины внезапно пропадает питание. После восстановления питания она снова загружается и запускает сервер БД. Что произойдет с данными? Каждая система БД, реляционная и NoSQL, имеет свою стратегию обработке таких отказов.
Вообще говоря, возможна «нулевая стратегия», когда все данные просто теряются и база данных становится пустой. Примером крайне успешной NoSQL-системы с такой стратегией является Memcache.
ACID: Атомарность, согласованность, изоляция и надежность Реляционные системы БД традиционно поддерживают ту или иную стратегию, которая обеспечивает набор гарантий, который называется ACID: atomicity, consistency, isolation, durability (атомарность, согласованность, изоляция, надежность). Эти термины относятся к обработке транзакций.Транзакция — это набор операций, которые рассматриваются как единое целое. Классический пример транзакции — это пересылка денег между двумя банковскими счетами. Для этого мы должны уменьшить сумму на одном счете и одновременно с этим увеличить сумму на другом счете.
Atomicity (атомарность) — это гарантия того, что при любом поведении оборудования либо будет выполнены обе этих операции, либо не выполнено ни одной. То есть даже если мы «снимем деньги с одного счета», и в эту микросекунду произойдет скачок напряжения — после перезагрузки базы и введения ее в рабочий режим мы снова увидим прежнюю сумму на исходном счете.
Consistency (согласованность) — это наименее четко определенная гарантия. Кроме того, этот термин еще и используется в определении CAP-теоремы (о которой см. ниже), и там он означает нечто другое (но близкое). Наиболее общо можно сказать, что согласованность гарантирует некоторое «разумное» поведение базы данных, такое что программист не получит особых сюрпризов при работе с базой данных, а также при отказах оборудования.
Isolation (изоляция) означает, что во время выполнения транзакции другие параллельно выполняющиеся операции «не видят» промежуточное состояние. Например, мы посчитали общую сумму на счетах. Теперь, если мы начнем выполнять пересылку денег, «снимем деньги с одного счета» и в эту микросекунду другой процесс снова попробует посчитать «общую сумму на счетах», то мы получим прежнюю сумму, и не меньше.
Durability (надежность) означает, что после успешного завершения транзакции ее результаты уже не будут потеряны ни при каких условиях. Например, мы выполним пересылку денег, закроем транзакцию и получим от сервера сообщение об успешном завершении транзакции. Через микросекунду произойдет скачок напряжения. Надежность гарантирует, что когда машина снова загрузится и войдет в рабочий режим — информация о пересылке денег сохранится в базе данных.
Традиционно базы данных, поддерживающие ACID, позволяют и до некоторой степени нарушать его, с помощью т. н. «transaction isolation level» (уровней изоляции транзакций). Например, на уровне «uncommitted read» параллельные транзакции могут «увидеть» промежуточные состояния других транзакций.
Ослабление гарантий Вообще, ослабление гарантий часто позволяет увеличивать эффективность ценой особых требований к интерпретации результатов (так, они могут быть недостаточно точными или просто некорректными). Для некоторых случаев это может быть оправдано: например, если мы хотим показать на сайте общее количество зарегистрированных пользователей, то в общем нам неинтересно точнейшее значение — достаточно сказать «около сотни» или просто показать «какое-то» число, потому что никто никогда не сможет подтвердить или опровергнуть каждое конкретное значение.Многие NoSQL-системы попросту отказываются от поддержки ACID, и вместо этого объявляют какой-то свой уникальный набор гарантий, который может быть где угодно в спектре от «нулевого» до более-менее близкого к «полному ACID». Например, некоторые версии некоторых систем просто могут при отказе машины оставить базу данных в поврежденном состоянии, так что потребуется ее ручное или полуавтоматическое восстановление после перезагрузки, при этом не гарантируется, что все записанные данные при этом будут сохранены.
Гарантии, ослабленные на уровне отдельной машины, можно «восстановить» или даже построить на их основе существенно более надежную систему, если объединить физические машины в сеть и потребовать специального режима работы с ними. Об этом подробнее см. ниже.
Параллельный доступ к базе данных Обычно у базы данных много клиентов, которые параллельно делают операции как чтения, так и записи. База данных обязана в этом случае выполнять гарантии, которые в ней заложены. Например, реляционные БД обычно обеспечивают изоляцию транзакций (см. выше).Поддержка параллельного доступа к базам данных часто требует существенных усилий от разработчика сервера, который должен обеспечить скорость и надежность такого доступа. Есть много различных алгоритмов и структур данных, лежащих в основе параллельного доступа.
Например, чтобы добавить запись в таблицу, нам надо выделить новую страницу в таблице, а также обновить индекс. Если параллельно другой клиент добавляет другую запись, то ему надо выделить еще одну страницу (или воспользоваться той же самой?), и снова обновить индекс (или может быть совместить две операции обновления индекса?). Что если первый клиент начал транзакцию, анонсировал добавление записи, подождал две секунды, и откатил транзакцию? Что если один клиент увеличил значение поля на единицу, а второй уменьшил на единицу? Что если в любую микросекунду может произойти скачок напряжения, и система после перезагрузки должна вернуться к «корректному» состоянию, несмотря на все многочисленные комбинации промежуточных условий и состояний?
Соблюдение гарантий в условиях параллельного доступа при сохранении производительности — огромная и сложная инженерная задача. Все сервера БД решают ее с помощью более-менее стандартных подходов, однако конкретная реализация этих подходов, и тонкости, с ними связанные — разные в каждом сервере БД.
Традиционно считается, что увеличение на порядок количества одновременных клиентов базы данных требует пересмотра ее архитектуры. В общем виде этот вопрос — нерешенная инженерная задача.
Административные функции Все серверы баз данных обеспечивают множество административных функций, связанных с жизнью сервера на отдельной машине. Среди таких функций — резервное копирование; восстановление из резервной копии; оптимизация места, занимаемого таблицами; распределение файлов с данным по различным дискам и файловым системам; сетевой доступ к серверу БД (см. также соответствующую главу книги) и эффективность такого доступа.Также некоторые сервера умеют эффективно использовать специальные функции операционной системы (зачастую, в свою очередь, разработанные специально для серверов БД). Типичный пример — поддержка асинхронного ввода-вывода.
Есть также административные функции, связанные с объединением физических машин в сеть. Например, это настройка топологии репликации, а также управление машинами в составе кластеров. Подробнее об этом см. ниже.
Все эти, и многие другие функции, по-своему реализованы в разных серверах баз данных. Административные функции, их проработанность и удобство, являются важным критерием выбора сервера БД, подходящего для конкретной задачи.
Современные серверы баз данных обеспечивают множество тонких настроек производительности. Сравнение скорости работы разных БД в специальных условиях — увлекательное и не всегда осмысленное занятие.
Важно понимать, что любой настроенный под некоторые условия сервер всегда можно «поставить на колени», изменив паттерны доступа к данным, увеличив количество клиентов или увеличив количество хранимых данных. Паттерны доступа к данным меняются по мере эволюции системы. Количество клиентов растет по мере роста популярности системы. Количество хранимых данных обычно также растет по мере развития системы. Все это приводит к тому, что старые рекорды и успехи становятся неактуальными, и требуется заново проводить процесс тонкой подстройки системы, а иногда и думать о смене архитектуры доступа к данным.
Распределенные базы данных Как мы уже говорили, каждая физическая машина в любой момент времени может сломаться. Кроме того, у любой физической машины есть предел производительности, которую она может обеспечить. Эти два обстоятельства заставляют объединять машины в сеть и рассматривать их как распределенную базу данных.Распределенные базы данных заставляют заново задуматься о всех вопросах, которые мы обсуждали для случая одной физической машины: о модели данных, протоколе доступа к данным и о гарантиях, которые обеспечиваются в случае отказа оборудования.
Подробнее мы обсудим этот вопрос в следующей главе нашей книги.