Внутреннее устройство баз данных

Алекс Петров

Copyright © 2019 Oleksandr Petrov

<

Данный документ предоставляется по лицензии Creative Commons Attribution 3.0 License, за исключением разделов со специальными оговорками.

http://creativecommons.org/licenses/by/3.0/legalcode

Первая публикация на английском языке: Октябрь 2019

Опубликовано O’Reilly Media, Inc.,

1005 Gravenstein Highway North,

Sebastopol,

CA 95472.

ISBN 978-1-492-04034-7

oreilly.com

2019-11-23

 Состав исполнителей

Издания на английском языке

Автор

Олександр Петров

Выпускающий редактор

Майкл Лукайдис

Редактор выпуска

Мишель Кроунин

Редактор производства

Кристофер Фаучер

Литературный редактор

Ким Коуфир

Корректор

Соня Саруба

Составитель индекса

Джудит МакКонвил

Художник- декоратор

Дэйвид Футейто

Разработчик обложки

Карен Монтгомери

Иллюстратор

Ребекка Димарест

Питеру Хинтьенсу, от которого я получил свою первую подписанную книгу:

вдохновляющему программисту распределённых систем, автору, философу и другу.

 Предисловие

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

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

Некоторые из этих баз данных были сосредоточены на горизонтальном масштабировании (вширь) - повышении производительности и увеличении ёмкости за счёт запуска нескольких экземпляров базы данных, действующих как единое логическое устройство: Gamma Database Machine Project, Teradata, Greenplum, Parallel DB2 и прочие. И сегодня горизонтальное масштабирование продолжает оставаться одним из наиболее важных свойств, которое клиенты ожидают от баз данных. Это можно объяснить растущей популярностью облачных служб. Часто более простым является раскрутка некого нового экземпляра и добавление его в кластер,чем масштабировать вертикально (вверх), перемещая свою базу данных в большую, более сложную машину. Миграции могут быть длительными и болезненными, причём потенциально приводят к простоям.

Около 2010 года появился некий новый класс в конечном счёте согласованных баз данных и стали популярными такие термины как NoSQL, а впоследствии и big data. За последние 15 лет сообщество разработчиков программного обеспечения с открытым исходным кодом, крупные интернет- компании и производители баз данных создали настолько много баз данных и инструментов, что легко потеряться, пытаясь разобраться в вариантах применения, деталях и особенностях.

Статья Dynamo [DECANDIA07], опубликованная командой Anazon в 2007, оказала настолько сильное влияние на сообщество баз данных, что за короткий переиод вдохновила на множество вариантов и реализаций. Самыми известными из нихбыли созданная Facebook Apache Cassandra, созданный LinkedIn Проект Voldemort и созданный бывшими инженерами Akamai Riak.

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

 Аудитория этой книги

В обсуждениях на технических конференциях я часто слышу один и тот же вопрос: "Как мне больше узнать о внутреннем устройстве баз данных? Я даже не знаю с чего начать." Большинство книг по системам баз данных не содержат подробностей о реализации механизмов хранения и охватывают сами методы доступа, такие как B- деревья, на достаточно высоком уровне. Имеется очень мало книг, обсуждающих концепции наиболее позднего времени, например, различные варианты B- деревьев и хранилища структурированных журналов (logstructured storage), поэтому обычно я рекомендую читать статьи.

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

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

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

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

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

 Зачем мне читать эту книгу?

мы часто слышим как люди обсуждают базы данных в терминах тех понятий и алгоритмов, которые их реализуют: "Эта база данных использует gossip для распространения участия" (см. Главу 12), "Они реализовали Dynamo" или "Это в точности так же, как было описано в статье Spanner" (см. Главу 13). Или, когда вы обсуждаете алгоритмы и структуры данных, вы можете слышать нечто навроде "ZAB и Raft имеют много общего" (см. Главу 14). "Bw- дереья подобны B- деревьям, реализованным поверх хранилища структурированного журнала" (см. Главу 6), или, "Они пользуются братскими указателями как в Blink- деревьях" (см. Главу 5).

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

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

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

 Сфера этой книги

Это не книга о системах управления реляционными базами данных, и не о базах NoSQL, а об алгоритмах и понятиях, применяемых всеми видами систем баз данных с сосредоточенностью на механизмах хранения и тех компонентах, которые отвечают за распределённость.

некоторые понятия, такие как планирование очередей, оптимизация запросов, составление расписаний, сама реляционная модель и некоторые иные, они все уже обсуждались в некоторых великолепных учебниках по системам баз данных. Ряд этих понятий уже описывался с точки зрения пользователя, но данная книга сосредоточена на внутреннем устройстве. Вы можете обнаружить некие указатели на полезную литературу в Заключение Части II и в Выводах каждой из глав. В этих книгах вы скорее всего обнаружите ответы на множество вопросов относящихся к базам данных, которые могут у вас иметься.

Язык запросов не обсуждается, так как нет единого общего языка для всех упоминающихся в этой книге системах баз данных.

Для сбора материала данной книги я изучил около 15 книг и более 300 статей, бесчисленное множество блог постов и документацию для различных баз данных с открытым исходным кодом. Отлитым в граните правилом того включать или нет конкретное понятие в эту книгу был вопрос: "Обсуждают ли люди из индустрии баз данных и исследовательских кругов эту концепцию?". Когда ответом является "Да", я добавлял такое понятие в длинный перечень предметов для обсуждения.

 Построение этой книги

Существуют некоторые образцы расширяемых баз данных с подключаемыми компонентами (например, [DECANDIA07]), но они достаточно редки. В то же время имеется множество экземпляров в которых базы данных используют подключаемые хранилища. Точно так же мы редко слышим рассуждения производителей баз данных относительно выполнения запросов, в то время как они настоятельно желают обсуждать сохранение состоятельности своих баз данных.

Наиболее существенные различия между системами баз данных сосредоточены вокруг двух аспектов: как они хранят и как они распределяют свои данные. (Порой также и прочие системы могут быть важными, но они не рассматриваются здесь.) Эта книга разбита на части, в которых рассматриваются подсистемы и компоненты, ответственные за хранение Часть I и за распределение Часть II.

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

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

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

Часть II о том, как организовать множество узлов в некий кластер базы данных. Мы начинаем с важности понимания теоретических понятий построения отказоустойчивых распределённых систем, того чем распределённые системы отличаются от приложений с единственным узлом, а также с того, с какими проблемами, ограничениями и сложностями мы сталкиваемся в распределённой среде.

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

Поскольку одной из самых сложных задач в распределённых системах является обеспечение согласованности данных, мы обсуждаем понятия репликаций, за которыми следуют модели согласованности, возможные расхождения между репликами и согласованность в конечном счёте. Так как системы с окончательной согласованностью при распространении данных полагаются антиэнтропию и сплетни (gossip, болтовню), мы обсуждаем некоторые подходы антиэнтропии и сплетен. Наконец, мы обсуждаем логическую состоятельность в контексте транзакций баз данных и заканчиваем алгоритмами консенсуса.

Было бы невозможно написать эту книгу без всех имеющихся исследований и публикаций. Вы обнаружите множество ссылок на статьи и публикации в самом тексте, в квадратных скобках, например [DECANDIA07]. Вы можете пользоваться этими ссылками для изучения дополнительных подробностей о связанных с ними понятиях.

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

 Используемые в этой книге соглашения

В этой книге применяются следующие соглашения:

Italic

Указывает на новые термины, URL, адреса электронной почты, имена файлов и файловые расширения.

Constant width

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

[Совет]
Совет

Этот элемент обозначает некий совет или уловку.

[Замечание]
Замечание

Данный элемент обозначает некое общее замечание.

[Предостережение]
Предостережение

Этот элемент указывает некое предостережение или предупреждение.

 Использование примеров книги

Эта книга здесь для того чтобы помочь вам выполнить ваше задание. В целом, когда пример кода предлагается в этой книге, вы можете использовать его в своих программах или в документации. Вам не требуется обращаться к нам за разрешением, если вы не воспроизводите значительную часть этого кода. К примеру, написание программы, которая пользуется несколькими фрагментами кода из данной книги не требует разрешения. Продажа или распространение CD- ROM с примерами из книг O’Reilly требует разрешение на это. Для ответа на вопрос со ссылкой на данную книгу разрешение не требуется. Включение значительной части кода примера в документацию вашего продукта потребует разрешения.

Мы ценим, но не требуем, указания авторства. Такое указание обычно содержит название, автора, издателя и ISBN, например: "Database Internals by Alex Petrov (O’Reilly). Copyright 2019 Oleksandr Petrov, 978-1-492-04034-7".

Если вы полагаете, что использование примеров кода выходит за рамки указанного выше добросовестного использования или разрешения, свяжитесь с нами через permissions@oreilly.com.

 Интерактивное обучение O’Reilly

Более 40 лет O’Reilly Media предоставляет обучение технологией и ведением дел, знаний и догадок в помощь успеху предприятий.

Наша уникальная сетевая среда экспертов и новаторов делятся своими знаниями и опытом через книги, статьи, конференции, а также через нашу платформу интерактивного обучения. Платформа интерактивного обучения O’Reilly предоставляет вам доступ по запросу к курсам обучения в реальном масштабе времени, различным тренировочным курсам вживую, пути глубокого изучения, среды интерактивного кодирования и обширную коллекцию текстов и видео от O’Reilly и более чем 200 прочих издательств. Для получения дополнительных сведений обращайтесь, пожалуйста к http://oreilly.com.

 Как поддерживать связь с нами

Адресуйте, пожалуйста, комментарии и вопросы относительно этой книги самому издательству:

O’Reilly Media, Inc.
1005 Gravenstein Highway North
Sebastopol, CA 95472
800-998-9938 (in the United States or Canada)
707-829-0515 (international or local)
707-829-0104 (fax)

У нас имеется для данной книги веб страница, в которой мы перечисляем ошибки, примеры и все дополнительные сведения. Вы можете получить доступ к этой странице через http://bit.ly/database-internals.

Для комментариев и технических вопросов относительно данной книги, пожалуйста, направьте электронное письмо по адресу: bookquestions@oreilly.com.

Для дополнительных сведениях о книгах, курсах, конференциях и новостях следите за нашим вебсайтом http://www.oreilly.com.

Найдите нас на Facebook: http://facebook.com/oreilly.

Следуйте за нами в Twitter: http://twitter.com/oreillymedia.

Смотрите нас на YouTube: http://www.youtube.com/oreillymedia.

 Благодарности

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

Я бы хотел выразить признательность всем людям, которые просмотрели рукописи и предоставили свои отзывы, что придаёт уверенность в том, что материалы этой книги верны и изложение точное: Dmitry Alimov, Peter Alvaro, Carlos Baquero, Jason Brown, Blake Eggleston, Marcus Eriksson, Francisco Fernández Castaño, Heidi Howard, Vaidehi Joshi, Maximilian Karasz, Stas Kelvich, Michael Klishin, Predrag Knežević, Joel Knighton, Eugene Lazin, Nate McCall, Christopher Meiklejohn, Tyler Neely, Maxim Neverov, Marina Petrova, Stefan Podkowinski, Edward Ribiero, Denis Rytsov, Kir Shatrov, Alex Sorokoumov, Massimiliano Tomassi и Ariel Weisberg.

Конечно же, эта книга не была бы возможной без поддержки от моей семьи: моей жены Марины и моей дочери Александры, которые поддерживали меня на каждом этапе этого пути.

 Содержание

Предисловие

Предисловие

Аудитория этой книги

Зачем мне читать эту книгу?

Сфера этой книги

Построение этой книги

Используемые в этой книге соглашения

Использование примеров книги

Интерактивное обучение O’Reilly

Как поддерживать связь с нами

Благодарности

Часть 1. Механизмы хранения

Глава 1. Введение и обзор

Архитектура DBMS

Сопоставление DBMS в памяти и основанных на дисках

Живучесть в основанных на памяти хранилищах

Сопоставление DBMS ориентированных на столбцы и на строки

Схема ориентированных на строки данных

Схема ориентированных на столбцы данных

Различия и оптимизация

Хранилища с широкими столбцами

Файлы данных и индексные файлы

Файлы данных

Файлы индексации

Первичный индекс как некая косвенная адресация

Буферизация, неизменность и упорядоченность

Выводы

Глава 2. Основы B- деревьев

Деревья двоичного поиска

Балансировка деревьев

Деревья для основанных на дисках хранилищах

Основанные на дисках структуры

Устройства жёстких дисков

Устройства твердотельных дисков

Структуры на дисках

Вездесущие B- деревья

Иерархия В- деревьев

Ключи разделители

Сложность поиска в В- деревьях

Алгоритм поиска в В- деревьях

Ключи счётчика

Расщепление узла В- дерева

Слияние узла В- дерева

Выводы

Глава 3. Форматы файлов

Мотивация

Двоичное кодирование

Типы примитивов

Строки и данные переменной длины

Двоично упакованные данные: Булевы, перечисления и флаги

Общие принципы

Структура страницы

Перекидные страницы

Схема ячейки

Комбинирование ячеек в страницы с прорезями

Управление данными переменной длины

Сопровождение версий

Подсчёт контрольных сумм

Выводы

Глава 4. Реализация B- деревьев

Заголовок страницы

Магические числа

Братские ссылки

Крайние правые указатели

Верхние ключи узла

Страницы переполнения

Двоичный поиск

Двоичный поиск по косвенным указателям

Распространение расщеплений и слияний

Хлебные крошки

Повторная балансировка

Исключительно правые добавления в конец

Загрузка в навал

Сжатие

Уборка и сопровождение

Вызванная обновлениями и удалениями фрагментация

Дефрагментация страниц

Выводы

Глава 5. Обработка и восстановление транзакции

Управление буфером

Семантики кэширования

Вытеснение кэша

Блокировка страниц в кэше

Страничный обмен

FIFO и LRU

CLOCK

LFU

Восстановление

Семантики журнала

Сопоставление журналов действий и данных

Политики воровства и принуждения

ARIES

Управление одновременностью

Возможность преобразования в последовательную форму

Изоляция транзакций

Аномалии чтения и записи

Уровни изоляции

Оптимистичное управление одновременностью

Управление одновременностью множества версий

Пессимистичное управление одновременностью

Управление одновременностью на основе блокировок

Взаимные блокировки

Блокировки

Защёлки

Блокировка читатели- писатель

Сцепляющая защёлка

Деревья Blink

Выводы

Глава 6. Варианты B- деревьев

Копирование записью

Реализация копирования записью: LMDB

Абстракция обновления узлов

Ленивые B- деревья

WiredTiger

Дерево ленивой адаптации

FD- деревья

Фрагментарное каскадирование

Логарифмические прогоны

Bw- деревья

Цепочки обновлений

Культивирование одновременности при помощи Compare- and- Swap

Операции структурного изменения

Консолидация и сборка мусора

Не замечающие кэш B- деревья

Схема ван Эмде Боаса

Выводы

Глава 7. Хранилище структурированного журнала

Деревья LSM

Структура деревьев LSM

Двухкомпонентные LSM деревья

Многокомпонентные LSM деревья

Таблицы в памяти

Обновления и удаления

Поиск по дереву LSM

Слияние итераций

Улаживание

Сопровождение в деревьях LSM

Упаковка по уровням

Стягиваемая по размеру упаковка

Чтение, запись и увеличение пространства

Предположение RUM

Подробности реализации

Таблицы сортированных строк

Фильтры Блюма

Список пропусков

Доступ к дискам

Сжатие

Неупорядоченное хранилище LSM

Bitcask

WiscKey

Одновременность в деревьях LSM

Составление журналов в стек

Слой трансляции флеш- устройства

Журналирование файловой системы

Составление стека LLAMA и Mindful

SSD Open-Channel

Выводы

Заключение Части I

Часть 2. Распределённые системы

Глава 8. Введение и обзор

Одновременное исполнение

Разделяемое состояние в распределённой системе

Хитрости распределённых вычислений

Обработка

Такты и время

Согласованность состояния

Локальное и удалённое исполнение

Потребность обработки отказов

Сетевые разделы и частичные отказы

Каскадные отказы

Абстракции распределённых систем

Соединения

Справедливо утрачиваемые соединения

Подтверждения сообщений

Повторная передача сообщений

Проблемы с повторной передачей сообщений

Порядок сообщений

В точности одна доставка

Проблема двух генералов

Невозможность FLP

Синхронность систем

Модели отказов

Отказы крушения

Отказы пропуска

Произвольные отказы

Обработка отказов

Выводы

Глава 9. Выявление отказов

Сигналы сердцебиения и Ping

Свободный от таймаутов определитель отказа

Независимые сигналы сердцебиения

Определитель отказа Phi- начисления

Gossip и определитель отказа

Постановка задачи обратного выявления отказа

Выводы

Глава 10. Выбор лидера

Алгоритм задиры

Отказоустойчивый следующий- в- строю

Оптимизация соискатель/ ординарный

Алгоритм приглашения

Алгоритм кольца

Выводы

Глава 11. Репликация и согласованность

Достижение доступности

Печально известная CAP

Аккуратное применение CAP

Сбор и выход

Совместно используемая память

Упорядочение

Модели согласованности

Строгая согласованность

Линеаризация

Момент линеаризации

Стоимость линеаризации

Последовательная согласованность

Причинная согласованность

Векторные часы

Модели сеанса

Согласованность в конечном счёте

Настраиваемая согласованность

Свидетельские реплики

Строгая согласованность в конечном счёте и CRDTs

Выводы

Глава 12. Анти- энтропия и рассеивание

Восстановление чтением

Чтения свёрток

Наводящая передача

Деревья Merkle

Векторы побитной маски версии

Рассеивание Gossip

Механизмы Gossip

Перекрывающиеся сетевые среды

Гибридный Gossip

Частичные представления

Выводы

Глава 13. Распределённые транзакции

Превращение действий в проявляющиеся атомарными

Двухфазная фиксация

Отказы пособников в P2C

Отказы координатора в P2C

Трёхфазная фиксация

Координатор отказов в P3C

Распределённые транзакции с применением Calvin

Распределённые транзакции при помощи Spanner

Разбиение баз данных на разделы

Согласованное хэширование

Распределённые транзакции посредством Percolator

Избежание координации

Выводы

Глава 14. Консенсус

Широковещание

Атомарное широковещание

Виртуальная синхронизация

ZAB

Paxos

Алгоритм Paxos

Кворум в Paxos

Сценарии отказов

Multi-Paxos

Быстрый Paxos

Уравнивающий Paxos

Гибкий Paxos

Обобщённое решение для консенсуса

Обобщённый алгоритм Paxos

Raft

Роль лидера в Raft

Сценарии отказов

Византийский консенсус

Алгоритм pBFT

Восстановление и установка контрольных точек

Выводы

Заключение Части II

Дополнение A. Библиография

Дополнение B. Дерево индексации в твердотельных устройствах

Краткий обзор

Введение

Подготовка и относящиеся к ней работы

Оптимизация произвольных записей на SSD

Оптимизированное для записи дерево индексации

FD- дерево

Принципы разработки для индексации в SSD

Обзор FD- дерева

Операции в FD- дереве

Возврат отчуждения операций в FD- дереве

Анализ стоимости и модель стоимости

Результаты экспериментов

Настройка эксперимента

Анализ модели

Производительность вставки и удаления

Производительность операции возврата отчуждения

Сравнение производительности флеш SSD

Сравнение производительности шпиндельного диска

Выводы

Ссылки

Приложение A. Начальные сведения по флэш SSD

Приложение B. Псевдокод алгоритма

Приложение С. Анализ стоимости и модель стоимости

С1. Анализ времени поиска

С2. Анализ времени вставки

С3. Модель стоимости для установки параметров

Приложение D. Подробности экспериментальной настройки

Указатель

Дополнение A. Библиография

Содержание

Дополнение A. Библиография

[ABADI12] Abadi, Daniel. 2012. “Consistency Tradeoffs in Modern Distributed Database System Design: CAP is Only Part of the Story.” Computer 45, no. 2 (February): 37-42. https://doi.org/10.1109/MC.2012.33.

[ABADI17] Abadi, Daniel. 2017. “Distributed consistency at scale: Spanner vs. Calvin.” Fauna (blog). April 6, 2017. https://fauna.com/blog/distributed-consistency-at-scale-spanner-vs-calvin.

[ABADI13] Abadi, Daniel, Peter Boncz, Stavros Harizopoulos, Stratos Idreaos, and Samuel Madden. 2013. The Design and Implementation of Modern Column- Oriented Database Systems. Hanover, MA: Now Publishers Inc.

[ABRAHAM13] Abraham, Ittai, Danny Dolev, and Joseph Y. Halpern. 2013. “Distributed Protocols for Leader Election: A Game-TheoreticPerspective.” In Distributed Computing, edited by Yehuda Afek, 61-75. Berlin: Springer, Berlin, Heidelberg.

[AGGARWAL88] Aggarwal, Alok, and Jeffrey S. Vitter. 1988. “The input/output complexity of sorting and related problems.” Communications of the ACM 31, no. 9 (September): 1116-1127. https://doi.org/10.1145/48529.48535.

[AGRAWAL09] Agrawal, Devesh, Deepak Ganesan, Ramesh Sitaraman, Yanlei Diao, Shashi Singh. 2009. “Lazy-Adaptive Tree: an optimized index structure for flash devices.” Proceedings of the VLDB Endowment 2, no. 1 (January): 361-372.

[AGRAWAL08] Agrawal, Nitin, Vijayan Prabhakaran, Ted Wobber, John D. Davis, Mark Manasse, and Rina Panigrahy. 2008. “Design tradeoffs for SSD performance.” USENIX 2008 Annual Technical Conference (ATC ’08), 57-70. USENIX.

[AGUILERA97] Aguilera, Marcos K., Wei Chen, and Sam Toueg. 1997. “Heartbeat: a Timeout-Free Failure Detector for Quiescent Reliable Communication.” In Distributed Algorithms, edited by M. Mavronicolas and P. Tsigas, 126-140. Berlin: Springer, Berlin, Heidelberg.

[AGUILERA01] Aguilera, Marcos Kawazoe, Carole Delporte-Gallet, Hugues Fauconnier, and Sam Toueg. 2001. “Stable Leader Election.” In Proceedings of the 15th International Conference on Distributed Computing (DISC ’01), edited by Jennifer L. Welch, 108-122. London: Springer-Verlag.

[AGUILERA16] Aguilera, M. K., and D. B. Terry. 2016. “The Many Faces of Consistency.” Bulletin of the Technical Committee on Data Engineering 39, no. 1 (March): 3-13.

[ALHOUMAILY10] Al-Houmaily, Yousef J. 2010. “Atomic commit protocols, their integration, and their optimisations in distributed database systems.” International Journal of Intelligent Information and Database Systems 4, no. 4 (September): 373–412. https://doi.org/10.1504/IJIIDS.2010.035582.

[ARJOMANDI83] Arjomandi, Eshrat, Michael J. Fischer, and Nancy A. Lynch. 1983. “Efficiency of Synchronous Versus Asynchronous Distributed Systems.” Journal of the ACM 30, no. 3 (July): 449-456. https://doi.org/10.1145/2402.322387.

[ARULRAJ17] Arulraj, J. and A. Pavlo. 2017. “How to Build a Non-Volatile Memory Database Management System.” In Proceedings of the 2017 ACM International Conference on Management of Data: 1753-1758. https://doi.org/10.1145/3035918.3054780.

[ATHANASSOULIS16] Athanassoulis, Manos, Michael S. Kester, Lukas M. Maas, Radu Stoica, Stratos Idreos, Anastasia Ailamaki, and Mark Callaghan. 2016. “Designing Access Methods: The RUM Conjecture.” In International Conference on Extending Database Technology (EDBT). https://stratos.seas.harvard.edu/files/stratos/files/rum.pdf.

[ATTIYA94] Attiyaand, Hagit and Jennifer L. Welch. 1994. “Sequential consistency versus linearizability.” ACM Transactions on Computer Systems 12, no. 2 (May): 91-122. https://doi.org/10.1145/176575.176576.

[BABAOGLU93] Babaoglu, Ozalp and Sam Toueg. 1993. “Understanding NonBlocking Atomic Commitment.” Technical Report. University of Bologna.

[BAILIS14a] Bailis, Peter. 2014. “Linearizability versus Serializability.” Highly Available, Seldom Consistent (blog). September 24, 2014. https://www.bailis.org/blog/linearizability-versus-serializability.

[BAILIS14b] Bailis, Peter, Alan Fekete, Michael J. Franklin, Ali Ghodsi, Joseph M. Hellerstein, and Ion Stoica. 2014. “Coordination Avoidance in Database Systems.” Proceedings of the VLDB Endowment 8, no. 3 (November): 185-196. https://doi.org/10.14778/2735508.2735509.

[BAILIS14c] Bailis, Peter, Alan Fekete, Ali Ghodsi, Joseph M. Hellerstein, and Ion Stoica. 2014. “Scalable Atomic Visibility with RAMP Transactions.” ACM Transactions on Database Systems 41, no. 3 (July). https://doi.org/10.1145/2909870.

[BARTLETT16] Bartlett, Robert P. III, and Justin McCrary. 2016. “How Rigged Are Stock Markets?: Evidence From Microsecond Timestamps.” UC Berkeley Public Law Research Paper. https://doi.org/10.2139/ssrn.2812123.

[BAUDET19] Baudet, Mathieu, Avery Ching, Andrey Chursin, George Danezis, François Garillot, Zekun Li, Dahlia Malkhi, Oded Naor, Dmitri Perelman, and Alberto Sonnino. 2019. “State Machine Replication in the Libra Blockchain.” https://developers.libra.org/docs/assets/papers/libra-consensus-state-machine-replication-in-the-libra-blockchain.pdf.

[BAYER72] Bayer, R., and E. M. McCreight. 1972. “Organization and maintenance of large ordered indices.” Acta Informatica 1, no. 3 (September): 173-189. https://doi.org/10.1007/BF00288683.

[BEDALY69] Belady, L. A., R. A. Nelson, and G. S. Shedler. 1969. “An anomaly in space-time characteristics of certain programs running in a paging machine.” Communications of the ACM 12, no. 6 (June): 349-353. https://doi.org/10.1145/363011.363155.

[BENDER05] Bender, Michael A., Erik D. Demaine, and Martin Farach-Colton. 2005. “Cache-Oblivious B-Trees.” SIAM Journal on Computing 35, no. 2 (August): 341-358. https://doi.org/10.1137/S0097539701389956.

[BERENSON95] Berenson, Hal, Phil Bernstein, Jim Gray, Jim Melton, Elizabeth O’Neil, and Patrick O’Neil. 1995. “A critique of ANSI SQL isolation levels.” ACM SIGMOD Record 24, no. 2 (May): 1-10. https://doi.org/10.1145/568271.223785.

[BERNSTEIN87] Bernstein, Philip A., Vassco Hadzilacos, and Nathan Goodman. 1987. Concurrency Control and Recovery in Database Systems. Boston: AddisonWesley Longman.

[BERNSTEIN09] Bernstein, Philip A. and Eric Newcomer. 2009. Principles of Transaction Processing. San Francisco: Morgan Kaufmann.

[BHATTACHARJEE17] Bhattacharjee, Abhishek, Daniel Lustig, and Margaret Martonosi. 2017. Architectural and Operating System Support for Virtual Memory. San Rafael, CA: Morgan & Claypool Publishers.

[BIRMAN07] Birman, Ken. 2007. “The promise, and limitations, of gossip protocols.” ACM SIGOPS Operating Systems Review 41, no. 5 (October): 8-13. https://doi.org/10.1145/1317379.1317382.

[BIRMAN10] Birman, Ken. 2010. “A History of the Virtual Synchrony Replication Model” In Replication, edited by Bernadette Charron-Bost, Fernando Pedone, and André Schiper, 91-120. Berlin: Springer-Verlag, Berlin, Heidelberg.

[BIRMAN06] Birman, Ken, Coimbatore Chandersekaran, Danny Dolev, Robbert vanRenesse. 2006. “How the Hidden Hand Shapes the Market for Software Reliability.” In First Workshop on Applied Sofware Reliability (WASR 2006). IEEE.

[BIYIKOGLU13] Biyikoglu, Cihan. 2013. “Under the Hood: Redis CRDTs (Conflict-free Replicated Data Types).” http://lp.redislabs.com/rs/915-NFD-128/images/WP-RedisLabs-Redis-Conflict-free-Replicated-Data-Types.pdf.

[BJØRLING17] Bjørling, Matias, Javier González, and Philippe Bonnet. 2017. “LightNVM: the Linux open-channel SSD subsystem.” In Proceedings of the 15th Usenix Conference on File and Storage Technologies (FAST’17), 359-373. USENIX.

[BLOOM70] Bloom, Burton H. 1970. “Space/time trade-offs in hash coding with allowable errors.” Communications of the ACM 13, no. 7 (July): 422-426. https://doi.org/10.1145/362686.362692.

[BREWER00] Brewer, Eric. 2000. “Towards robust distributed systems.” Proceedings of the nineteenth annual ACM symposium on Principles of distributed computing (PODC ’00). New York: Association for Computing Machinery. https://doi.org/10.1145/343477.343502.

[BRZEZINSKI03] Brzezinski, Jerzy, Cezary Sobaniec, and Dariusz Wawrzyniak. 2003. “Session Guarantees to Achieve PRAM Consistency of Replicated Shared Objects.” In Parallel Processing and Applied Mathematics, 1–8. Berlin: Springer, Berlin, Heidelberg.

[BUCHMAN18] Buchman, Ethan, Jae Kwon, and Zarko Milosevic. 2018. “The latest gossip on BFT consensus.” https://arxiv.org/pdf/1807.04938.pdf.

[CACHIN11] Cachin, Christian, Rachid Guerraoui, and Luis Rodrigues. 2011. Introduction to Reliable and Secure Distributed Programming (2nd Ed.). New York: Springer.

[CASTRO99] Castro, Miguel. and Barbara Liskov. 1999. “Practical Byzantine Fault Tolerance.” In OSDI ’99 Proceedings of the third symposium on Operating systems design and implementation, 173-186.

[CESATI05] Cesati, Marco, and Daniel P. Bovet. 2005. Understanding the Linux Kernel. Third Edition. Sebastopol: O’Reilly Media, Inc.

[CHAMBERLIN81] Chamberlin, Donald D., Morton M. Astrahan, Michael W. Blasgen, James N. Gray, W. Frank King, Bruce G. Lindsay, Raymond Lorie, James W. Mehl, Thomas G. Price, Franco Putzolu, Patricia Griffiths Selinger, Mario Schkolnick, Donald R. Slutz, Irving L. Traiger, Bradford W. Wade, and Robert A. Yost. 1981. “A history and evaluation of System R.” Communications of the ACM 24, no. 10 (October): 632–646. https://doi.org/10.1145/358769.358784.

[CHANDRA07] Chandra, Tushar D., Robert Griesemer, and Joshua Redstone. 2007. “Paxos made live: an engineering perspective.” In Proceedings of the twentysixth annual ACM symposium on Principles of distributed computing (PODC ’07), 398-407. New York: Association for Computing Machinery. https://doi.org/10.1145/1281100.1281103.

[CHANDRA96] Chandra, Tushar Deepak, and Sam Toueg. 1996. “Unreliable failure detectors for reliable distributed systems.” Journal of the ACM 43, no. 2 (March): 225-267. https://doi.org/10.1145/226643.226647.

[CHANG79] Chang, Ernest, and Rosemary Roberts. 1979. “An improved algorithm for decentralized extrema-finding in circular configurations of processes.” Communications of the ACM 22, no. 5 (May): 281–283. https://doi.org/10.1145/359104.359108.

[CHANG06] Chang, Fay, Jeffrey Dean, Sanjay Ghemawat, Wilson C. Hsieh, Deborah A.Wallach, Mike Burrows, Tushar Chandra, Andrew Fikes, and Robert E. Gruber. 2006. “Bigtable: A Distributed Storage System for Structured Data.” In 7th USENIX Symposium on Operating Systems Design and Implementation (OSDI ’06). USENIX.

[CHAZELLE86] Chazelle, Bernard, and Leonidas J. Guibas. 1986. “Fractional Cascading, A Data Structuring Technique.” Algorithmica 1: 133-162. https://doi.org/10.1007/BF01840440.

[CHOCKLER15] Chockler, Gregory, and Dahlia Malkhi. 2015. “Active disk paxos with infinitely many processes.” In Proceedings of the twenty-first annual symposium on Principles of distributed computing (PODC ’02), 78-87. New York: Association for Computing Machinery. https://doi.org/10.1145/571825.571837.

[COMER79] Comer, Douglas. 1979. “Ubiquitous B-Tree.” ACM Computing Survey 11, no. 2 (June): 121-137. https://doi.org/10.1145/356770.356776.

[CORBET18] Corbet, Jonathan. 2018. “PostgreSQL’s fsync() surprise.” https://lwn.net/Articles/752063.

[CORBETT12] Corbett, James C., Jeffrey Dean, Andrew Fikes, Christopher Frost, JJ Furman, Sanjay Ghemawat, Andrey Gubarev, Christopher Heiser, Peter Hochschild, Wilson Hsieh, Sebastian Kanthak, Eugene Kogan, Hongyi Li, Alexander Lloyd, Sergey Melnik, David Mwaura, David Nagle, Sean Quinlan, Rajesh Rao, Lindsay Rolig, Yasushi Saito, Michal Szymaniak, Christopher Taylor, Ruth Wang, and Dale Woodford. 2012. “Spanner: Google’s Globally-Distributed Database.” In 10th USENIX Symposium on Operating Systems Design and Implementation (OSDI ’12), 261-264. USENIX.

[CORMODE04] Cormode, G. and S. Muthukrishnan. 2004. “An improved data stream summary: The count-min sketch and its applications.” Journal of Algorithms 55, No. 1 (April): 58-75. https://doi.org/10.1016/j.jalgor.2003.12.001.

[CORMODE11] Cormode, Graham, and S. Muthukrishnan. 2011. “Approximat ing Data with the Count-Min Data Structure.” http://dimacs.rutgers.edu/~graham/pubs/papers/cmsof.pdf.

[CORMODE12] Cormode, Graham and Senthilmurugan Muthukrishnan. 2012. “Approximating Data with the Count-Min Data Structure.”.

[CHRISTIAN91] Cristian, Flavin. 1991. “Understanding fault-tolerant distributed systems.” Communications of the ACM 34, no. 2 (February): 56-78. https://doi.org/10.1145/102792.102801.

[DAILY13] Daily, John. 2013. “Clocks Are Bad, Or, Welcome to the Wonderful World of Distributed Systems.” Riak (blog). November 12, 2013. https://riak.com/clocks-are-bad-or-welcome-to-distributed-systems.

[DECANDIA07] DeCandia, Giuseppe, Deniz Hastorun, Madan Jampani, Gunavardhan Kakulapati, Avinash Lakshman, Alex Pilchin, Swaminathan Sivasubramanian, Peter Vosshall, and Werner Vogels. 2007. “Dynamo: amazon’s highly available key-value store.” SIGOPS Operating Systems Review 41, no. 6 (October): 205-220. https://doi.org/10.1145/1323293.1294281.

[DECHEV10] Dechev, Damian, Peter Pirkelbauer, and Bjarne Stroustrup. 2010. “Understanding and Effectively Preventing the ABA Problem in DescriptorBased Lock-Free Designs.” Proceedings of the 2010 13th IEEE International Symposium on Object/Component/Service-Oriented Real-Time Distributed Computing (ISORC ’10): 185–192. https://doi.org/10.1109/ISORC.2010.10.

[DEMAINE02] Demaine, Erik D. 2002. “Cache-Oblivious Algorithms and Data Structures.” In Lecture Notes from the EEF Summer School on Massive Data Sets. Denmark: University of Aarhus.

[DEMERS87] Demers, Alan, Dan Greene, Carl Hauser, Wes Irish, John Larson, Scott Shenker, Howard Sturgis, Dan Swinehart, and Doug Terry. 1987. “Epidemic algorithms for replicated database maintenance.” In Proceedings of the sixth annual ACM Symposium on Principles of distributed computing (PODC ’87), 1-12. New York: Association for Computing Machinery. https://doi.org/10.1145/41840.41841.

[DENNING68] Denning, Peter J. 1968. “The working set model for program behavior”. Communications of the ACM 11, no. 5 (May): 323-333. https://doi.org/10.1145/363095.363141.

[DIACONU13] Diaconu, Cristian, Craig Freedman, Erik Ismert, Per-Åke Larson, Pravin Mittal, Ryan Stonecipher, Nitin Verma, and Mike Zwilling. 2013. “Hekaton: SQL Server’s Memory-Optimized OLTP Engine.” In Proceedings of the 2013 ACM SIGMOD International Conference on Management of Data (SIGMOD ’13), 1243-1254. New York: Association for Computing Machinery. https://doi.org/10.1145/2463676.2463710.

[DOWNEY12] Downey, Jim. 2012. “Be Careful with Sloppy Quorums.” Jimy Downey (blog). March 5, 2012. https://jimdowney.net/2012/03/05/be-careful-with-sloppy-quorums.

[DREPPER07] Drepper, Ulrich. 2007. What Every Programmer Should Know About Memory. Boston: Red Hat, Inc.

[DUNAGAN04] Dunagan, John, Nicholas J. A. Harvey, Michael B. Jones, Dejan Kostić, MarvinTheimer, and Alec Wolman. 2004. “FUSE: lightweight guaranteed distributed failure notification.” In Proceedings of the 6th conference on Symposium on Operating Systems Design & Implementation - Volume 6 (OSDI’04), 11-11. USENIX.

[DWORK88] Dwork, Cynthia, Nancy Lynch, and Larry Stockmeyer. 1988. “Consensus in the presence of partial synchrony.” Journal of the ACM 35, no. 2 (April): 288-323. https://doi.org/10.1145/42282.42283.

[EINZIGER15] Einziger, Gil and Roy Friedman. 2015. “A formal analysis of conservative update based approximate counting.” In 2015 International Conference on Computing, Networking and Communications (ICNC), 260-264. IEEE.

[EINZIGER17] Einziger, Gil, Roy Friedman, and Ben Manes. 2017. “TinyLFU: A Highly Efficient Cache Admission Policy.” In 2014 22nd Euromicro International Conference on Parallel, Distributed, and Network-Based Processing, 146-153. IEEE.

[ELLIS11] Ellis, Jonathan. 2011. “Understanding Hinted Handoff.” Datastax (blog). May 31, 2011. https://www.datastax.com/dev/blog/understanding-hinted-handoff.

[ELLIS13] Ellis, Jonathan. 2013. “Why Cassandra doesn’t need vector clocks.” Datastax (blog). September 3, 2013. https://www.datastax.com/dev/blog/whycassandra-doesnt-need-vector-clocks.

[ELMASRI11] Elmasri, Ramez and Shamkant Navathe. 2011. Fundamentals of Database Systems (6th Ed.). Boston: Pearson.

[FEKETE04] Fekete, Alan, Elizabeth O’Neil, and Patrick O’Neil. 2004. “A readonly transaction anomaly under snapshot isolation.” ACM SIGMOD Record 33, no. 3 (September): 12-14. https://doi.org/10.1145/1031570.1031573.

[FISCHER85] Fischer, Michael J., Nancy A. Lynch, and Michael S. Paterson. 1985. “Impossibility of distributed consensus with one faulty process.” Journal of the ACM 32, 2 (April): 374-382. https://doi.org/10.1145/3149.214121.

[FLAJOLET12] Flajolet, Philippe, Eric Fusy, Olivier Gandouet, and Frédéric Meunier. 2012. “HyperLogLog: The analysis of a near-optimal cardinality estimation algorithm.” In AOFA ’07: Proceedings of the 2007 International Conference on Analysis of Algorithms.

[FOWLER11] Fowler, Martin. 2011. “The LMAX Architecture.” Martin Fowler. July 12, 2011. https://martinfowler.com/articles/lmax.html.

[FOX99] Fox, Armando and Eric A. Brewer. 1999. “Harvest, Yield, and Scalable Tolerant Systems.” In Proceedings of the Seventh Workshop on Hot Topics in Operating Systems, 174-178.

[FREILING11] Freiling, Felix C., Rachid Guerraoui, and Petr Kuznetsov. 2011. “The failure detector abstraction.” ACM Computing Surveys 43, no. 2 (January): Article 9. https://doi.org/10.1145/1883612.1883616.

[MOLINA82] Garcia-Molina, H. 1982. “Elections in a Distributed Computing System.” IEEE Transactions on Computers 31, no. 1 (January): 48-59. https://dx.doi.org/10.1109/TC.1982.1675885.

[MOLINA92] Garcia-Molina, H. and K. Salem. 1992. “Main Memory Database Systems: An Overview.” IEEE Transactions on Knowledge and Data Engineering 4, no. 6 (December): 509-516. https://doi.org/10.1109/69.180602.

[MOLINA08] Garcia-Molina, Hector, Jeffrey D. Ullman, and Jennifer Widom. 2008. Database Systems: Te Complete Book (2nd Ed.). Boston: Pearson.

[GEORGOPOULOS16] Georgopoulos, Georgios. 2016. “Memory Consistency Models of Modern CPUs.” https://es.cs.uni-kl.de/publications/datarsg/Geor16.pdf.

[GHOLIPOUR09] Gholipour, Majid, M. S. Kordafshari, Mohsen Jahanshahi, and Amir Masoud Rahmani. 2009. “A New Approach For Election Algorithm in Distributed Systems.” In 2009 Second International Conference on Communication Teory, Reliability, and Quality of Service, 70-74. IEEE. https://doi.org/10.1109/CTRQ.2009.32.

[GIAMPAOLO98] Giampaolo, Dominic. 1998. Practical File System Design with the be File System. San Francisco: Morgan Kaufmann.

[GILAD17] Gilad, Yossi, Rotem Hemo, Silvio Micali, Georgios Vlachos, and Nickolai Zeldovich. 2017. “Algorand: Scaling Byzantine Agreements for Cryptocurrencies.” Proceedings of the 26th Symposium on Operating Systems Principles (October): 51–68. https://doi.org/10.1145/3132747.3132757.

[GILBERT02] Gilbert, Seth and Nancy Lynch. 2002. “Brewer’s conjecture and the feasibility of consistent, available, partition-tolerant web services.” ACM SIGACT News 33, no. 2 (June): 51-59. https://doi.org/10.1145/564585.564601.

[GILBERT12] Gilbert, Seth and Nancy Lynch. 2012. “Perspectives on the CAP Theorem.” Computer 45, no. 2 (February): 30-36. https://doi.org/10.1109/MC.2011.389.

[GOMES17] Gomes, Victor B. F., Martin Kleppmann, Dominic P. Mulligan, and Alastair R. Beresford. 2017. “Verifying strong eventual consistency in distributed systems.” Proceedings of the ACM on Programming Languages 1 (October). https://doi.org/10.1145/3133933.

[GONÇALVES15] Gonçalves, Ricardo, Paulo Sérgio Almeida, Carlos Baquero, and Victor Fonte. 2015. “Concise Server-Wide Causality Management for Eventually Consistent Data Stores.” In Distributed Applications and Interoperable Systems, 66-79. Berlin: Springer.

[GOOSSAERT14] Goossaert, Emmanuel. 2014. “Coding For SSDs.” CodeCapsule (blog). February 12, 2014. http://codecapsule.com/2014/02/12/coding-for-ssds-part-1-introduction-and-table-of-contents.

[GRAEFE04] Graefe, Goetz. 2004. “Write-Optimized B-Trees.” In Proceedings of the Tirtieth international conference on Very large data bases - Volume 30 (VLDB ’04), 672-683. VLDB Endowment.

[GRAEFE07] Graefe, Goetz. 2007. “Hierarchical locking in B-tree indexes.” https://www.semanticscholar.org/paper/Hierarchical-locking-in-B-tree-indexes-Graefe/270669b1eb0d31a99fe99bec67e47e9b11b4553f.

[GRAEFE10] Graefe, Goetz. 2010. “A survey of B-tree locking techniques.” ACM Transactions on Database Systems 35, no. 3, (July). https://doi.org/10.1145/1806907.1806908.

[GRAEFE11] Graefe, Goetz. 2011. “Modern B-Tree Techniques.” Foundations and Trends in Databases 3, no. 4 (April): 203-402. https://doi.org/10.1561/1900000028.

[GRAY05] Gray, Jim, and Catharine van Ingen. 2005. “Empirical Measurements of Disk Failure Rates and Error Rates.” Accessed March 4, 2013. https://arxiv.org/pdf/cs/0701166.pdf.

[GRAY04] Gray, Jim, and Leslie Lamport. 2004. “Consensus on Transaction Commit.” ACM Transactions on Database Systems 31, no. 1 (March): 133-160. https://doi.org/10.1145/1132863.1132867.

[GUERRAOUI07] Guerraoui, Rachid. 2007. “Revisiting the relationship between non-blocking atomiccommitment and consensus.” In Distributed Algorithms, 87-100. Berlin: Springer, Berlin, Heidelberg. https://doi.org/10.1007/BFb0022140.

[GUERRAOUI97] Guerraoui, Rachid, and André Schiper. 1997. “Consensus: The Big Misunderstanding.” In Proceedings of the Sixth IEEE Computer Society Workshop on Future Trends of Distributed Computing Systems, 183-188. IEEE.

[GUPTA01] Gupta, Indranil, Tushar D. Chandra, and Germán S. Goldszmidt. 2001. “On scalable and efficient distributed failure detectors.” In Proceedings of the twentieth annual ACM symposium on Principles of distributed computing (PODC ’01) New York: Association for Computing Machinery. https://doi.org/10.1145/383962.38401.

[HADZILACOS05] Hadzilacos, Vassos. 2005. “On the relationship between the atomic commitment and consensus problems.” In Fault-Tolerant Distributed Computing, 201-208. London: Springer-Verlag.

[HAERDER83] Haerder, Theo, and Andreas Reuter. 1983. “Principles of transaction-oriented database recovery.” ACM Computing Surveys 15 no. 4 (December):287–317. https://doi.org/10.1145/289.291.

[HALE10] Hale, Coda. 2010. “You Can’t Sacrifice Partition Tolerance.” Coda Hale (blog). https://codahale.com/you-cant-sacrifice-partition-tolerance.

[HALPERN90] Halpern, Joseph Y., and Yoram Moses. 1990. “Knowledge and common knowledge in a distributed environment.” Journal of the ACM 37, no. 3 (July): 549-587. https://doi.org/10.1145/79147.79161.

[HARDING17] Harding, Rachael, Dana Van Aken, Andrew Pavlo, and Michael Stonebraker. 2017. “An Evaluation of Distributed Concurrency Control.” Proceedings of the VLDB Endowment 10, no. 5 (January): 553-564. https://doi.org/10.14778/3055540.3055548.

[HAYASHIBARA04] Hayashibara, N., X. Defago, R.Yared, and T. Katayama. 2004. “The Φ Accrual Failure Detector.” In IEEE Symposium on Reliable Distributed Systems, 66-78. https://doi.org/10.1109/RELDIS.2004.1353004.

[HELLAND15] Helland, Pat. 2015. “Immutability Changes Everything.” Queue 13, no. 9 (November). https://doi.org/10.1145/2857274.2884038.

[HELLERSTEIN07] Hellerstein, Joseph M., Michael Stonebraker, and James Hamilton. 2007. “Architecture of a Database System.” Foundations and Trends in Databases 1, no. 2 (February): 141-259. https://doi.org/10.1561/1900000002.

[HERLIHY94] Herlihy, Maurice. 1994. “Wait-Free Synchronization.” ACM Transactions on Programming Languages and Systems 13, no. 1 (January): 124-149. http://dx.doi.org/10.1145/114005.102808.

[HERLIHY10] Herlihy, Maurice, Yossi Lev, Victor Luchangco, and Nir Shavit. 2010. “A Provably Correct Scalable Concurrent Skip List.” https://www.cs.tau.ac.il/~shanir/nir-pubs-web/Papers/OPODIS2006-BA.pdf.

[HERLIHY90] Herlihy, Maurice P., and Jeannette M. Wing. 1990. “Linearizability: a correctness condition for concurrent object.” ACM Transactions on Programming Languages and Systems 12, no. 3 (July): 463-492. https://doi.org/10.1145/78969.78972.

[HOWARD14] Howard, Heidi. 2014. “ARC: Analysis of Raft Consensus.” Technical Report UCAM-CL-TR-857. Cambridge: University of Cambridge.

[HOWARD16] Howard, Heidi, Dahlia Malkhi, and Alexander Spiegelman. 2016. “Flexible Paxos: Quorum intersection revisited.” https://arxiv.org/abs/1608.06696.

[HOWARD19] Howard, Heidi, and Richard Mortier. 2019. “A Generalised Solution to Distributed Consensus.” https://arxiv.org/abs/1902.06776.

[HUNT10] Hunt, Patrick, Mahadev Konar, Flavio P. Junqueira, and Benjamin Reed. 2010. “ZooKeeper: wait-free coordination for internet-scale systems.” Proceedings of the 2010 USENIX conference on USENIX annual technical conference (USENIXATC’10), 11. USENIX, 11. USENIX.

[INTEL14] Intel Corporation. 2014. “Partition Alignment of Intel® SSDs for Achieving Maximum Performance and Endurance.” (February). https://www.intel.com/content/dam/www/public/us/en/documents/technology-briefs/ssd-partition-alignment-tech-brief.pdf.

[JELASITY04] Jelasity, Márk, Rachid Guerraoui, Anne-Marie Kermarrec, and Maarten van Steen. 2004. “The Peer Sampling Service: Experimental Evaluation of Unstructured Gossip-Based Implementations.” In Middleware ’04 Proceedings of the 5th ACM/IFIP/USENIX international conference on Middleware, 79-98. Berlin: Springer-Verlag, Berlin, Heidelberg.

[JELASITY07] Jelasity, Márk, Spyros Voulgaris, Rachid Guerraoui, Anne-Marie Kermarrec, and Maarten van Steen. 2007. “Gossip-based Peer Sampling.” In ACM Transactions on Computer Systems 25, no. 3 (August) http://doi.org/10.1145/1275517.1275520.

[JONSON94] Johnson, Theodore, and Dennis Shasha. 1994. “2Q: A Low Overhead High Performance Buffer Management Replacement Algorithm." In VLDB ’94 Proceedings of the 20th International Conference on Very Large Data Bases, 439-450. San Francisco: Morgan Kaufmann.

[JUNQUEIRA07] Junqueira, Flavio, Yanhua Mao, and Keith Marzullo. 2007. “Classic Paxos vs. fast Paxos: caveat emptor.” In Proceedings of the 3rd workshop on on Hot Topics in System Dependability (HotDep’07). USENIX.

[JUNQUEIRA11] Junqueira, Flavio P., Benjamin C. Reed, and Marco Serafini. 2011. “Zab: High-performance broadcast for primary-backup systems.” 2011 IEEE/IFIP 41st International Conference on Dependable Systems & Networks (DSN) (June): 245–256. https://doi.org/10.1109/DSN.2011.595822.

[KANNAN18] Kannan, Sudarsun, Nitish Bhat, Ada Gavrilovska, Andrea ArpaciDusseau, and Remzi Arpaci-Dusseau. 2018. “Redesigning LSMs for Nonvolatile Memory with NoveLSM.” In USENIX ATC ’18 Proceedings of the 2018 USENIX Conference on Usenix Annual Technical Conference, 993-1005. USENIX.

[KARGER97] Karger, D., E. Lehman, T. Leighton, R. Panigrahy, M. Levine, and D. Lewin. 1997. “Consistent hashing and random trees: distributed caching protocols for relieving hot spots on the World Wide Web.” In STOC ’97 Proceedings of the twenty-ninth annual ACM symposium on Teory of computing, 654-663. New York: Association for Computing Machinery.

[KEARNEY17] Kearney, Joe. 2017. “Two Phase Commit an old friend.” https://www.joekearney.co.uk/posts/two-phase-commit (blog). January 6, 2017. https://www.joekearney.co.uk/posts/two-phase-commit.

[KEND94] Kendall, Samuel C., Jim Waldo, Ann Wollrath, and Geoff Wyant. 1994. “A Note on Distributed Computing.” Technical Report. Mountain View, CA: Sun Microsystems, Inc.

[KREMARREC07] Kermarrec, Anne-Marie, and Maarten van Steen. 2007. “Gossiping in distributed systems.” SIGOPS Operating Systems Review 41, no. 5 (October): 2-7. https://doi.org/10.1145/1317379.1317381.

[KERRISK10] Kerrisk, Michael. 2010. The Linux Programming Interface. San Francisco: No Starch Press.

[KHANCHANDANI18] Khanchandani, Pankaj, and Roger Wattenhofer. 2018. “Reducing Compare-and-Swap to Consensus Number One Primitives.” https://arxiv.org/abs/1802.03844.

[KIM12] Kim, Jaehong, Sangwon Seo, Dawoon Jung, Jin-Soo Kim, and Jaehyuk Huh. 2012. “Parameter-Aware I/O Management for Solid State Disks (SSDs).” IEEE Transactions on Computers 61, no. 5 (May): 636-649. https://doi.org/10.1109/TC.2011.76.

[KINGSBURY18a] Kingsbury, Kyle. 2018. “Sequential Consistency”. https://jepsen.io/consistency/models/sequential.

[KINGSBURY18b] Kingsbury, Kyle. 2018. “Strong consistency models.” Aphyr (blog). August 8, 2018. https://aphyr.com/posts/313-strong-consistency-models.

[KLEPPMANN15] Kleppmann, Martin. 2015. “Please stop calling databases CP or AP.” Martin Kleppmann (blog). May 11, 2015. https://martin.kleppmann.com/2015/05/11/please-stop-calling-databases-cp-or-ap.html.

[KLEPPMANN14] Kleppmann, Martin, and Alastair R. Beresford. 2014. “A Conflict-Free Replicated JSON Datatype.” https://arxiv.org/abs/1608.03960.

[KNUTH97] Knuth, Donald E. 1997. The Art of Computer Programming, Volume 1 (3rd Ed.): Fundamental Algorithms. Boston: Addison-Wesley Longman.

[KNUTH98] Knuth, Donald E. 1998. The Art of Computer Programming, Volume 3: (2nd Ed.): Sorting and Searching. Boston: Addison-Wesley Longman.

[KOOPMAN15] Koopman, Philip, Kevin R. Driscoll, and Brendan Hall. 2015. “Selection of Cyclic Redundancy Code and Checksum Algorithms to Ensure Critical Data Integrity.” U.S. Department of Transportation Federal Aviation Administration. https://www.faa.gov/aircraf/air_cert/design_approvals/air_sofware/media/TC-14–49.pdf.

[KORDAFSHARI05] Kordafshari, M. S., M. Gholipour, M. Mosakhani, A. T. Haghighat, and M. Dehghan. 2005. “Modified bully election algorithm in distributed systems.” Proceedings of the 9th WSEAS International Conference on Computers (ICCOMP’05), edited by Nikos E. Mastorakis, Article 10. Stevens Point: World Scientific and Engineering Academy and Society.

[KRASKA18] Kraska, Time, Alex Beutel, Ed H. Chi, Jeffrey Dean, and Neoklis Polyzotis. 2018. “The Case for Learned Index Structures.” In SIGMOD ’18 Proceedings of the 2018 International Conference on Management of Data, 489-504. New York: Association for Computing Machinery.

[LAMPORT77] Lamport, Leslie. 1977. “Proving the Correctness of Multiprocess Programs.” IEEE Transactions on Sofware Engineering 3, no. 2 (March): 125-143. https://doi.org/10.1109/TSE.1977.229904.

[LAMPORT78] Lamport, Leslie. 1978. “Time, Clocks, and the Ordering of Events in a Distributed System.” Communications of the ACM 21, no. 7 (July): 558-565

[LAMPORT79] Lamport, Leslie. 1979. “How to Make a Multiprocessor Computer That Correctly Executes Multiprocess Programs.” IEEE Transactions on Computers 28, no. 9 (September): 690-691. https://doi.org/10.1109/TC.1979.1675439.

[LAMPORT98] Lamport, Leslie. 1998. “The part-time parliament.” ACM Transactions on Computer Systems 16, no. 2 (May): 133-169. https://doi.org/10.1145/279227.279229.

[LAMPORT01] Lamport, Leslie. 2001. “Paxos Made Simple.” ACM SIGACT News (Distributed Computing Column) 32, no. 4 (December): 51-58. https://www.microsof.com/en-us/research/publication/paxos-made-simple.

[LAMPORT05] Lamport, Leslie. 2005. “Generalized Consensus and Paxos.” https://www.microsof.com/en-us/research/publication/generalized-consensus-and-paxos.

[LAMPORT06] Lamport, Leslie. 2006. “Fast Paxos.” Distributed Computing 19, no. 2 (July): 79-103. https://doi.org/10.1007/s00446-006-0005-x.

[LAMPORT09] Lamport, Leslie, Dahlia Malkhi, and Lidong Zhou. 2009. “Vertical Paxos and Primary-Backup Replication.” In PODC ’09 Proceedings of the 28th ACM symposium on Principles of distributed computing, 312-313. https://doi.org/10.1145/1582716.1582783.

[LAMPSON01] Lampson, Butler. 2001. “The ABCD’s of Paxos.” In PODC ’01 Proceedings of the twentieth annual ACM symposium on Principles of distributed computing, 13. https://doi.org/10.1145/383962.383969.

[LAMPSON79] Lampson, Butler W., and Howard E. 1979. “Crash Recovery in a Distributed Data Storage System.” https://www.microsof.com/en-us/research/publication/crash-recovery-in-a-distributed-data-storage-system.

[LARRIVEE15] Larrivee, Steve. 2015. “Solid State Drive Primer.” Cactus Technologies (blog). February 9th, 2015. https://www.cactus-tech.com/resources/blog/details/solid-state-drive-primer-1-the-basic-nand-flash-cell.

[LARSON81] Larson, Per-Åke, and Åbo Akedemi. 1981. “Analysis of indexsequential files with overflow chaining”. ACM Transactions on Database Systems. 6, no. 4 (December): 671-680. https://doi.org/10.1145/319628.319665.

[LEE15] Lee, Collin, Seo Jin Park, Ankita Kejriwal, Satoshi Matsushita, and John Ousterhout. 2015. “Implementing linearizability at large scale and low latency.” In SOSP ’15 Proceedings of the 25th Symposium on Operating Systems Principles, 71-86. https://doi.org/10.1145/2815400.2815416.

[LEHMAN81] Lehman, Philip L., and s. Bing Yao. 1981. “Efficient locking for concurrent operations on B-trees.” ACM Transactions on Database Systems 6, no. 4 (December): 650-670. https://doi.org/10.1145/319628.319663.

[LEITAO07] Leitao, Joao, Jose Pereira, and Luis Rodrigues. 2007. “Epidemic Broadcast Trees.” In SRDS ’07 Proceedings of the 26th IEEE International Symposium on Reliable Distributed Systems, 301-310. IEEE.

[LEVANDOSKI14] Levandoski, Justin J., David B. Lomet, and Sudipta Sengupta. 2013. “The Bw-Tree: A B-tree for new hardware platforms.” In Proceedings of the 2013 IEEE International Conference on Data Engineering (ICDE ’13), 302-313. IEEE. https://doi.org/10.1109/ICDE.2013.6544834.

[LI10] Li, Yinan, Bingsheng He, Robin Jun Yang, Qiong Luo, and Ke Yi. 2010. “Tree Indexing on Solid State Drives.” Proceedings of the VLDB Endowment 3, no. 1-2 (September): 1195-1206. https://doi.org/10.14778/1920841.1920990.

[LIPTON88] Lipton, Richard J., and Jonathan S. Sandberg. 1988. “PRAM: A scalable shared memory.” Technical Report, Princeton University. https://www.cs.princeton.edu/research/techreps/TR-180-88.

[LLOYD11] Lloyd, W., M. J. Freedman, M. Kaminsky, and D. G. Andersen. 2011. “Don’t settle for eventual: scalable causal consistency for wide-area storage with COPS.” In Proceedings of the Twenty-Tird ACM Symposium on Operating Systems Principles (SOSP ’11), 401-416. New York: Association for Computing Machinery. https://doi.org/10.1145/2043556.2043593.

[LLOYD13] Lloyd, W., M. J. Freedman, M. Kaminsky, and D. G. Andersen. 2013. “Stronger semantics for low-latency geo-replicated storage.” In 10th USENIX Symposium on Networked Systems Design and Implementation (NSDI ’13), 313-328. USENIX.

[LU16] Lu, Lanyue, Thanumalayan Sankaranarayana Pillai, Hariharan Gopalakrishnan, Andrea C. Arpaci-Dusseau, and Remzi H. Arpaci-Dusseau. 2017. “WiscKey: Separating Keys from Values in SSD-Conscious Storage.” ACM Transactions on Storage (TOS) 13, no. 1 (March): Article 5. https://doi.org/10.1145/3033273.

[MATTERN88] Mattern, Friedemann. 1988. “Virtual Time and Global States of Distributed Systems.” http://courses.csail.mit.edu/6.852/01/papers/VirtTime_Glob-State.pdf.

[MCKENNEY05a] McKenney, Paul E. 2005. “Memory Ordering in Modern Microprocessors, Part I.” Linux Journal no. 136 (August): 2.

[MCKENNEY05b] McKenney, Paul E. 2005. “Memory Ordering in Modern Microprocessors, Part II.” Linux Journal no. 137 (September): 5.

[MEHTA17] Mehta, Apurva, and Jason Gustafson. 2017. “Transactions in Apache Kafka.” Confluent (blog). November 17, 2017. https://www.confluent.io/blog/transactions-apache-kafka.

[MELLORCRUMMEY91] Mellor-Crummey, John M., and Michael L. Scott. 1991. “Algorithms for scalable synchronization on shared-memory multiprocessors.” ACM Transactions on Computer Systems 9, no. 1 (February): 21-65. https://doi.org/10.1145/103727.103729.

[MELTON06] Melton, Jim. 2006. “Database Language SQL.” In International Organization for Standardization (ISO), 105–132. Berlin: Springer. https://doi.org/10.1007/b137905.

[MERKLE87] Merkle, Ralph C. 1987. “A Digital Signature Based on a Conventional Encryption Function.” A Conference on the Teory and Applications of Cryptographic Techniques on Advances in Cryptology (CRYPTO ’87), edited by Carl Pomerance. London: Springer-Verlag, 369–378. https://dl.acm.org/citation.cfm?id=704751.

[MILLER78] Miller, R., and L. Snyder. 1978. “Multiple access to B-trees.” Proceedings of the Conference on Information Sciences and Systems, Baltimore: Johns Hopkins University (March).

[MILOSEVIC11] Milosevic, Z., M. Hutle, and A. Schiper. 2011. “On the Reduction of Atomic Broadcast to Consensus with Byzantine Faults.” In Proceedings of the 2011 IEEE 30th International Symposium on Reliable Distributed Systems (SRDS ’11), 235-244. IEEE. https://doi.org/10.1109/SRDS.2011.36.

[MOHAN92] Mohan, C., Don Haderle, Bruce Lindsay, Hamid Pirahesh, and Peter Schwarz. 1992. “ARIES: a transaction recovery method supporting finegranularity locking and partial rollbacks using write-ahead logging.” Transactions on Database Systems 17, no. 1 (March): 94-162. https://doi.org/10.1145/128765.128770.

[MORARU11] Moraru, Iulian, David G. Andersen, and Michael Kaminsky. 2013. “A Proof of Correctness for Egalitarian Paxos.” https://www.pdl.cmu.edu/PDLFTP/associated/CMU-PDL-13-111.pdf.

[MORARU13] Moraru, Iulian, David G. Andersen, and Michael Kaminsky. 2013. “There Is More Consensus in Egalitarian Parliaments.” In Proceedings of the Twenty-Fourth ACM Symposium on Operating Systems Principles (SOSP ’13), 358-372. https://doi.org/10.1145/2517349.2517350.

[MURSHED12] Murshed, Md. Golam, and Alastair R. Allen. 2012. “Enhanced Bully Algorithm for Leader Node Election in Synchronous Distributed Systems.” Computers 1, no. 1: 3-23. https://doi.org/10.3390/computers1010003.

[NICHOLS66] Nichols, Ann Eljenholm. 1966. “The Past Participle of ‘Overflow:’ ‘Overflowed’ or ‘Overflown.’” American Speech 41, no. 1 (February): 52–55. https://doi.org/10.2307/453244.

[NIEVERGELT74] Nievergelt, J. 1974. “Binary search trees and file organization.” In Proceedings of 1972 ACM-SIGFIDET workshop on Data description, access and control (SIGFIDET ’72), 165-187. https://doi.org/10.1145/800295.811490.

[NORVIG01] Norvig, Peter. 2001. “Teach Yourself Programming in Ten Years.” https://norvig.com/21-days.html.

[ONEIL93] O’Neil, Elizabeth J., Patrick E. O’Neil, and Gerhard Weikum. 1993. “The LRU-K page replacement algorithm for database disk buffering.” In Proceedings of the 1993 ACM SIGMOD international conference on Management of data (SIGMOD ’93), 297-306. https://doi.org/10.1145/170035.170081.

[ONEIL96] O’Neil, Patrick, Edward Cheng, Dieter Gawlick, and Elizabeth O’Neil. 1996. “The log-structured merge-tree (LSM-tree).” Acta Informatica 33, no. 4: 351-385. https://doi.org/10.1007/s002360050048.

[ONGARO14] Ongaro, Diego and John Ousterhout. 2014. “In Search of an Understandable Consensus Algorithm.” In Proceedings of the 2014 USENIX conference on USENIX Annual Technical Conference (USENIX ATC’14), 305-320. USENIX.

[OUYANG14] Ouyang, Jian, Shiding Lin, Song Jiang, Zhenyu Hou, Yong Wang, and Yuanzheng Wang. 2014. “SDF: software-defined flash for web-scale internet storage systems.” ACM SIGARCH Computer Architecture News 42, no. 1 (February): 471-484. https://doi.org/10.1145/2654822.2541959.

[PAPADAKIS93] Papadakis, Thomas. 1993. “Skip lists and probabilistic analysis of algorithms.” Doctoral Dissertation, University of Waterloo. https://cs.uwaterloo.ca/research/tr/1993/28/root2side.pdf.

[PUGH90a] Pugh, William. 1990. “Concurrent Maintenance of Skip Lists.” Technical Report, University of Maryland. https://drum.lib.umd.edu/handle/1903/542.

[PUGH90b] Pugh, William. 1990. “Skip lists: a probabilistic alternative to balanced trees.” Communications of the ACM 33, no. 6 (June): 668-676. https://doi.org/10.1145/78973.78977.

[RAMAKRISHNAN03] Ramakrishnan, Raghu, and Johannes Gehrke. 2002. Database Management Systems (3rd Ed.). New York: McGraw-Hill.

[RAY95] Ray, Gautam, Jayant Haritsa, and S. Seshadri. 1995. “Database Compression: A Performance Enhancement Tool.” In Proceedings of 7th International Conference on Management of Data (COMAD). New York: McGraw Hill.

[RAYNAL99] Raynal, M., and F. Tronel. 1999. “Group membership failure detection: a simple protocol and its probabilistic analysis.” Distributed Systems Engineering 6, no. 3 (September): 95-102. https://doi.org/10.1088/0967-1846/6/3/301.

[REED78] Raynal, M., and F. Tronel. 1999. “Group membership failure detection: a simple protocol and its probabilistic analysis.” Distributed Systems Engineering 6, no. 3 (September): 95-102. https://doi.org/10.1088/0967-1846/6/3/301.

[REED78] Reed, D. P. 1978. “Naming and synchronization in a decentralized computer system.” Technical Report, MIT. https://dspace.mit.edu/handle/1721.1/16279.

[REN16] Reed, D. P. 1978. “Naming and synchronization in a decentralized computer system.” Technical Report, MIT. https://dspace.mit.edu/handle/1721.1/16279.

[REN16] Ren, Kun, Jose M. Faleiro, and Daniel J. Abadi. 2016. “Design Principles for Scaling Multi-core OLTP Under High Contention.” In Proceedings of the 2016 International Conference on Management of Data (SIGMOD ’16), 1583-1598. https://doi.org/10.1145/2882903.2882958.

[ROBINSON08] Robinson, Henry. 2008. “Consensus Protocols: Two-Phase Commit.” The Paper Trail (blog). November 27, 2008. https://www.the-paper-trail.org/post/2008-11-27-consensus-protocols-two-phase-commit.

[ROSENBLUM92] Rosenblum, Mendel, and John K. Ousterhout. 1992. “The Design and Implementation of a Log Structured File System.” ACM Transactions on Computer Systems 10, no. 1 (February): 26-52. https://doi.org/10.1145/146941.146943.

[ROY12] Roy, Arjun G., Mohammad K. Hossain, Arijit Chatterjee, and William Perrizo. 2012. “Column-oriented Database Systems: A Comparison Study.” In Proceedings of the ISCA 27th International Conference on Computers and Teir Applications, 264-269 .

[RUSSEL12] Russell, Sears. 2012. “A concurrent skiplist with hazard pointers.” http://rsea.rs/skiplist.

[RYSTSOV16] Rystsov, Denis. 2016. “Best of both worlds: Raft’s joint consensus + Single Decree Paxos.” Rystsov.info (blog). January 5, 2016. http://rystsov.info/2016/01/05/raf-paxos.html.

[RYSTSOV18] ystsov, Denis. 2018. “Replicated State Machines without logs.” https://arxiv.org/abs/1802.07000.

[SATZGER07] Satzger, Benjamin, Andreas Pietzowski, Wolfgang Trumler, and Theo Ungerer. 2007. “A new adaptive accrual failure detector for dependable distributed systems.” In Proceedings of the 2007 ACM symposium on Applied computing (SAC ’07), 551-555. https://doi.org/10.1145/1244002.1244129.

[SAVARD05] Savard, John. 2005. “Floating-Point Formats.” http://www.quadibloc.com/comp/cp0201.htm.

[SCHWARZ86] Schwarz, P., W. Chang, J. C. Freytag, G. Lohman, J. McPherson, C. Mohan, and H. Pirahesh. 1986. “Extensibility in the Starburst database system.” In OODS ’86 Proceedings on the 1986 international workshop on Objectoriented database systems, 85–92. IEEE.

[SEDGEWICK11] Shapiro, Marc, Nuno Preguiça, Carlos Baquero, and Marek Zawirski. 2011. “Conflict-free Replicated Data Types.” In Stabilization, Safety, and Security of Distributed Systems, 386-400. Berlin: Springer, Berlin, Heidelberg.

[SHAPIRO11b] Shapiro, Marc, Nuno Preguiça, Carlos Baquero, and Marek Zawirski. 2011. “A comprehensive study of Convergent and Commutative Replicated Data Types.” https://hal.inria.fr/inria-00555588/document.

[SHEEHY10a] Sheehy, Justin. 2010. “Why Vector Clocks Are Hard.” Riak (blog). April 5, 2010. https://riak.com/posts/technical/why-vector-clocks-are-hard.

[SHEEHY10b] Sheehy, Justin, and David Smith. 2010. “Bitcask, A Log-Structured Hash Table for Fast Key/Value Data.”.

[SILBERSCHATZ10] Silberschatz, Abraham, Henry F. Korth, and S. Sudarshan. 2010. Database Systems Concepts (6th Ed.). New York: McGraw-Hill.

[SINHA97] Skeen, Dale. 1982. “A Quorum-Based Commit Protocol.” Technical Report, Cornell University.

[SKEEN83] Skeen, Dale, and M. Stonebraker. 1983. “A Formal Model of Crash Recovery in a Distributed System.” IEEE Transactions on Sofware Engineering 9, no. 3 (May): 219-228. https://doi.org/10.1109/TSE.1983.236608.

[SOUNDARARARJAN06] Soundararajan, Gokul. 2006. “Implementing a Better Cache Replacement Algorithm in Apache Derby Progress Report.” https://pdfs.semanticscholar.org/220b/2fe62f13478f1ec75cf17ad085874689c604.pdf.

[STONE98] Stone, J., M. Greenwald, C. Partridge and J. Hughes. 1998. “Performance of checksums and CRCs over real data.” IEEE/ACM Transactions on Networking 6, no. 5 (October): 529-543. https://doi.org/10.1109/90.731187.

[TANENBAUM14] Tanenbaum, Andrew S., and Herbert Bos. 2014. Modern Operating Systems (4th Ed.). Upper Saddle River: Prentice Hall Press.

[TANENBAUM06] Tanenbaum, Andrew S., and Maarten van Steen. 2006. Distributed Systems: Principles and Paradigms. Boston: Pearson.

[TARIQ11] Tariq, Ovais. 2011. “Understanding InnoDB clustered indexes.” Ovais Tariq (blog). January 20, 2011. http://www.ovaistariq.net/521/understandinginnodb-clustered-indexes/#.XTtaUpNKj5Y.

[TERRY94] Terry, Douglas B., Alan J. Demers, Karin Petersen, Mike J. Spreitzer, Marvin M. Theimer, and Brent B. Welch. 1994. “Session Guarantees for Weakly Consistent Replicated Data.” In PDIS ’94 Proceedings of the Tird International Conference on Parallel and Distributed Information Systems, 140–149. IEEE.

[THOMAS79] Thomas, Robert H. 1979. “A majority consensus approach to concurrency control for multiple copy databases.” ACM Transactions on Database Systems 4, no. 2 (June): 180–209. https://doi.org/10.1145/320071.320076.

[THOMSON12] Thomson, Alexander, Thaddeus Diamond, Shu-Chun Weng, Kun Ren, Philip Shao, and Daniel J. Abadi. 2012. “Calvin: Fast distributed transactions for partitioned database systems.” In Proceedings of the ACM SIGMOD International Conference on Management of Data (SIGMOD ’12). New York: Association for Computing Machinery. https://doi.org/10.1145/2213836.2213838.

[VANRENESSE98] van Renesse, Robbert, Yaron Minsky, and Mark Hayden. 1998. “A Gossip-Style Failure Detection Service.” In Middleware ’98 Proceedings of the IFIP International Conference on Distributed Systems Platforms and Open Distributed Processing, 55–70. London: Springer-Verlag.

[VENKATARAMAN11] Venkataraman, Shivaram, Niraj Tolia, Parthasarathy Ranganathan, and Roy H. Campbell. 2011. “Consistent and Durable Data Structures for Non-Volatile Byte-Addressable Memory.” In Proceedings of the 9th USENIX conference on File and stroage technologies (FAST’11), 5. USENIX.

[VINOSKI08] Vinoski, Steve. 2008. “Convenience Over Correctness.” IEEE Internet Computing 12, no. 4 (August): 89–92. https://doi.org/10.1109/MIC.2008.75.

[VIOTTI16] Viotti, Paolo, and Marko Vukolić. 2016. “Consistency in NonTransactional Distributed Storage Systems.” ACM Computing Surveys 49, no. 1 (July): Article 19. https://doi.org/0.1145/2926965.

[VOGELS09] Vogels, Werner. 2009. “Eventually consistent.” Communications of the ACM 52, no. 1 (January): 40–44. https://doi.org/10.1145/1435417.1435432.

[WALDO96] Waldo, Jim, Geoff Wyant, Ann Wollrath, and Samuel C. Kendall. 1996. “A Note on Distributed Computing.” Selected Presentations and Invited Papers SecondInternational Workshop on Mobile Object Systems—Towards the Programmable Internet (July): 49–64. https://dl.acm.org/citation.cfm?id=747342.

[WANG13] Wang, Peng, Guangyu Sun, Song Jiang, Jian Ouyang, Shiding Lin, Chen Zhang, and Jason Cong. 2014. “An Efficient Design and Implementation of LSM-tree based Key-Value Store on Open-Channel SSD.” EuroSys ’14 Proceedings of the Ninth European Conference on Computer Systems (April): Article 16. https://doi.org/10.1145/2592798.2592804.

[WANG18] Wang, Ziqi, Andrew Pavlo, Hyeontaek Lim, Viktor Leis, Huanchen Zhang, Michael Kaminsky, and David G. Andersen. 2018. “Building a Bw-Tree Takes More Than Just Buzz Words.” Proceedings of the 2018 International Conference on Management of Data (SIGMOD ’18), 473–488. https://doi.org/10.1145/3183713.3196895.

[WEIKUM01] Weikum, Gerhard, and Gottfried Vossen. 2001. Transactional Information Systems: Teory, Algorithms, and the Practice of Concurrency Control and Recovery. San Francisco: Morgan Kaufmann Publishers Inc.

[XIA17] Xia, Fei, Dejun Jiang, Jin Xiong, and Ninghui Sun. 2017. “HiKV: A Hybrid Index Key-Value Store for DRAM-NVM Memory Systems.” Proceedings of the 2017 USENIX Annual Technical Conference (USENIX ATC ’17), 349–362. USENIX.

[YANG14] Yang, Jingpei, Ned Plasson, Greg Gillis, Nisha Talagala, and Swaminathan Sundararaman. 2014. “Don’t stack your Log on my Log.” INFLOW (October). https://www.usenix.org/system/files/conference/inflow14/inflow14-yang.pdf.

[ZHAO15] Zhao, Wenbing. 2015. “Fast Paxos Made Easy: Theory and Implementation.” International Journal of Distributed Systems and Technologies 6, no. 1 (January): 15-33. https://doi.org/10.4018/ijdst.2015010102.

Глава 12. Анти- энтропия и рассеивание

Содержание

Глава 12. Анти- энтропия и рассеивание

Восстановление чтением

Чтения свёрток

Наводящая передача

Деревья Merkle

Векторы побитной маски версии

Рассеивание Gossip

Механизмы Gossip

Перекрывающиеся сетевые среды

Гибридный Gossip

Частичные представления

Выводы

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

Быстрое и надёжное распространение может быть менее применимым для записей данных и более важным для необходимых метаданных по всему кластеру, например сведения об участниках (присоединяющиеся и покидающие его узлы), состояния узлов, отказы, изменения схемы и т.п. Содержащие эти сведения сообщения как правило не часты и малы, но должны распространяться настолько быстро и надёжно, насколько это возможно.

Такие обновления обычно могут распространяться во все узлы данного кластера с применением одного из имеющихся трёх подходов групп широкого общения [DEMERS87]. Схематические описания этих шаблонов взаимодействия отображены на Рисунке 12-1:

Широковещательное оповещение от одного процесса всех прочих.

Периодический одноранговый информационный обмен. Одноранговые партнёры соединяются парами и обмениваются сообщениями.

Кооперативное широкое вещание, при котором получатели сообщений становятся широковещателями и помогают распылять необходимые сведения быстрее и более надёжно.

Рисунок 12-1

Широковещание (a), анти- энтропия (b), gossip (c)

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

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

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

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

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

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

Восстановление чтением

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

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

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

Некоторые базы данных в стиле Dynamo решили отменить требование контактов со всеми репликами и используют место этого настраиваемые уровни согласованности. Для возврата согласованных результатов нам нет нужды в установке контакта со всеми имеющимися репликами и их восстановлением, а лишь с тем числом узлов, которое удовлетворяет заданному уровню согласованности. Когда мы выполняем чтения или записи кворума, мы всё ещё получаем согласованные результаты, однако некоторые из имеющихся реплик всё ещё могут не содержать все записи.

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

Блокирующее восстановление чтением обеспечивает монотонность чтения (см. раздел Модели сеанса) для чтений кворума: как только данный клиент считает некое особое значение, последующие чтения возвращают то значение,которое не старше уже увиденного, так как состояния реплики восстановлены. Если мы не пользуемся для чтения кворумами, мы теряем такие гарантии монотонности, так как данные могут быть не доставлены своему целевому узлу к моменту последующего чтения. В то же самое время, блокирующие восстановления чтением приносят в жертву доступность по причине того что восстановления обязаны выдавать подтверждения от своих целевых реплик, а само чтение не может быть возвращено пока они не ответят.

Для выявления того какие именно записи различаются в откликах реплик, некоторые базы данных (например, Apache Cassandra) пользуются особыми итераторами со слиянием ожидающих, которые реконструируют различия между такими сливаемыми результатами и индивидуальными входными данными. Их данные на выходе затем используются имеющимся координатором для уведомления соответствующих реплик относительно пропущенных данных.

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

Чтения свёрток

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

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

[Замечание]
Замечание

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

Наводящая передача

Другой подход анти- энтропии с названием наводящей передачи (hinted handoff) [DECANDIA07], это некий механизм восстановления стороны записи. Когда некий целевой узел отказывает в подтверждении записи, имеющийся координатор записи, или одна из реплик сохраняют особую запись с названием наводка (hint), которая выдаётся ответом соответствующему целевому узлу как только он вернётся обратно.

В Apache Cassandra до тех пор пока не используется уровень согласованности ANY [ELLIS11], наводящие записи не учитываются счётчиками в сторону значения множителя репликаций (см. раздел Настраиваемая согласованность), так как сами данные в установленном журнале наводок не доступны для чтений и используются только в помощь отлавливанию запаздывающих участников.

Некоторые базы данных, например, Riak, совместно с наводящими передачами применяют сырые кворумы (sloppy quorums). При сырых кворумах, в случае отказов реплик операции записи могут пользоваться дополнительными жизнеспособными узлами из своего перечня узлов и этим узлам не приходится выступать в качестве целевых реплик для данных исполняемых операций.

Например, допустим, у нас имеется кластер из пяти узлов с узлами {A, B, C, D, E}, где {A, B, C} являются репликами для соответствующей исполняемой операции записи, а узел B упал. A, выступая в роли координатора для данного запроса указывает на узел D для удовлетворения сырому кворуму и поддерживает желательные гарантии доступности и долговечности. Теперь данные реплицированы в {A, D, C}. Тем не менее, эта запись в D будет обладать некой наводкой в своих метаданных, так как эта запись первоначально предназначалась для B. Как только B восстановится, D попытается пробросить эту наводку обратно в него. Как только эта наводка воспроизводится в B, она может быть безопасно удалена без снижения общего числа реплик[DECANDIA07].

При аналогичных обстоятельствах, если узлы {B, C} на короткий промежуток времени отделяются от остального кластера возникающим делением сетевой среды на разделы, а сырой кворум записи был выполнен для {A, D, E}, некоторое чтение в {B, C}, немедленно следующее за данной записью не будет наблюдать данного самого последнего чтения [DOWNEY12]. Иными словами, сырые кворумы улучшают доступность за счёт согласованности.

Деревья Merkle

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

Как мы уже обсуждали, обнаружение в точности какие именно строки расходятся в репликах требует попарного обмена и сопоставления соответствующих записей данных. Это в высшей степени не практично и затратно. Для снижения стоимости восстановления согласия многие базы данных применяют деревья Меркель [MERKLE87].

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

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

Рисунок 12-2

Дерево Меркль. Сеые блоки представляют диапазоны записи данных. Белые блоки представляют иерархию некого дерева хэшей.

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

Так как деревья Меркель вычисляются рекурсивно снизу вверх, некое изменение в данных включает повторное вычисление всего своего поддерева. Также существует некий компромисс значения размера дерева (соответственно размеров обмена данными) и его точности (насколько мелко и точно данные поделены на диапазоны).

Векторы побитной маски версии

Самые последние исследования данного предмета ввели векторы побитной маски версии [GONÇALVES15], которые могут применяться для разрешения конфликтов на основании новизны: каждый узел отслеживает одноранговый журнал операций которые происходили локально или были реплицированы. В процессе анти- энтропии журналы сопоставляются и пропущенные сведения реплицируются в необходимый целевой узел.

Каждая координируемая неким узлом запись представляется некой точкой (i, n): неким событием последовательности с номером i локального узла, координируемой соответствующим узлом n. Данная последовательность с номером i начинается с 1 и последовательно увеличивается при каждом выполнении этим узлом операции записи.

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

Для логических часов самго узла события координируются самим узлом и не будут иметь зазоров. Когда некоторые записи не реплицировались с прочих узлов, наши часы будут содержать пробелы. Для обратного восстановления синхронности двух узлов они могут обмениваться логическими часами, выявляя промежутки, представленные такими пропущенными точками, а затем реплицируя связанные с ними записи данных. Для этого нам требуется реконструировать сами записи данных, на которые ссылается каждая точка. Эти сведения хранятся в неком контейнере причинных точек (DCC, dotted causal container), который устанавливает соответствие точек причинным сведениям для данного ключа. Таким образом перехватываются разрешения конфликтов, имеющие причинную связь между соответствующими записями.

Рисунок 12-3 (адаптировано из [GONÇALVES15]) отображает некий пример представления значения состояния трёх узлов в определённой системе, P₁, P₂ и P₃ с точки зрения P₂, отслеживающей какие значения она наблюдает. Всякий раз, когда P₂ делает некую запись или реплицирует значение, она обновляет эту таблицу.

Рисунок 12-3

Образец вектора побитной маски версии

В процессе репликации P₂ создаёт некое компактное представление этого состояния и создаёт какое- то соответствие от значения идентификатора узла к паре последних значений, в отношении которых он наблюдал последовательные записи, а также какую- то побитовую маску, в которой прочие наблюдавшиеся записи кодируются 1. (3, 01101₂) здесь означает, что узел P₂ наблюдал последовательные обновления для третьего значения во второй, третьей и пятой позициях относительно 3 (то есть наблюдались значения с последовательными номерами 5, 6 и 8).

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

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

Рассеивание Gossip

Массы всегда выступают питательной средой для физических эпидемий.

- Карл Юнг

Для вовлечения в процесс прочих процессов и распространять обновления со всем богатством широкого вещания и надёжностью анти- энтропии мы можем применять протоколы gossip (болтовни).

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

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

Некий процесс, хранящий какую- то запись, которая должна быть распространена имеет название инфицированного. Все прочие процессы, которые пока ещё не получили этого обновления далее являются восприимчивыми. Инфекционные процессы, которые не желают дальше распространять данное новое состояние после какого- то периода активного рассеивания именуются снятыми (removed) [DEMERS87]. Все процессы начинают с состояния восприимчивых. Всякий раз когда возникает некое обновление для какой- то записи данных, получивший их процесс переходит в состояние инфицированного и запускает рассеивание данного обновления на прочие случайные соседние процессы, инфицируя их. Как только инфицированные процессы достигли определения того что данное обновление было распространено, они переходят в состояние снятого.

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

Gossip может применяться для асинхронной доставки сообщений в разнородных системах без централизации, в которых узлы могут не иметь долговременного членства или обладать некой организационной топологией. Так как протоколы gossip обычно не требуют координации в явном виде, они могут оказываться полезными в системах с гибким участием (когда узлы часто присоединяются и открепляются) или в смешанных сетевых средах.

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

Механизмы Gossip

Процессы периодически выбирают случайным образом f одноранговых собратьев (где Code это настраиваемый параметр, носящий название веера - fanout) и обмениваются с ними "горячими" сведениями. Всякий раз когда данный процесс узнаёт о новом фрагменте сведений от своих однаранговых партнёров, он попытается передать их далее. Поскольку одноранговые пратнёры выбираются случайным образом, всегда будет иметься некое перекрытие и сообщения будут доставляться с повторами и могут продолжать циркуляцию какое- то время. Избыточность сообщений это некое измерение, которое перехватывает число перекрытий, происходящих при повторных доставках. Избыточность является неким необходимым свойством, и оно критически важно для того как работает gossip.

Общая продолжительность, требуемая для данной системы для достижения сходимости носит название латентности. Существует небольшая разница между достижением сходимости (остановки данного процесса gossip) и доставкой данного сообщения всем одноранговым партнёрам, так как может иметься короткий промежуток времени когда оповещения получены уже всеми участниками одноранговой сети, а gossip ещё продолжается. Веерность и латентность зависят от имеющегося размера системы: в некой крупной системе нам либо приходится увеличивать значение веерности для удержания стабильного значения латентности, либо допускать большую латентность.

Со временем, когда узлы обнаружат что они снова и снова получают одни и те же сведения, данное сообщение начинает утрачивать важность и узлам в конечном счёте придётся прекращать его передачу. Утрата заинтересованности может рассчитываться либо вероятность (значение вероятности распространения останова рассчитывается для каждого процесса на каждом шаге), либо с использованием некого порогового значения (ведётся подсчёт получаемых дубликатов и это распространение останавливается после достижения ими слишком высокого значения). Оба подхода должны исходить из учёта значения размера кластера и веерности. Подсчёт дублирований для измерения расходимости может улучшать латентность и снижать избыточность [DEMERS87].

В терминах согласованности, протоколы gossip предлагают согласованность сходимости [DECANDIA07]: узлы обладают более высокой вероятностью обладать тем же самым представлением событий, которые произошли дальше в прошлом.

Перекрывающиеся сетевые среды

Даже несмотря на то что протоколы gossip важны и полезны, обычно они применяются для узкого круга проблем. Подходы без эпидемий способны распространять соответствующее сообщение с несомненной вероятностью, меньшей избыточностью и в целом более оптимальным образом [BIRMAN07]. Алгоритмы gossip часто заслуживают похвалы за свою масштабируемость и тот факт, что и имеется возможность распространения сообщения за log N раундов (где N это значение размера кластера) [KREMARREC07], однако важно иметь в виду и сохранность значения числа избыточных сообщений, вырабатываемых в процессе раундов gossip. Для достижения надёжности протоколы на основе gossip производят некоторые доставки дублирующих сообщений.

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

Золотой серединой между этими двумя подходами является построение некой временной фиксированной топологии в системе gossip. Это может достигаться созданием некой перекрывающейся сетевой среды (overlay network) из одноранговых узлов: узлы могут делать выборку своих партнёров и подбирать наилучшие точки контактов на основании близости (обычно измеряемой латентностью).

Узлы в определённой системе способны организовывать форму связующих деревьев (spanning trees): ненаправленного графа без зацикливаний с отличающимися гранями, охватывающими всю сетевую среду. Обладая таким графом, сообщения способны распространяться за фиксированное число шагов.

Рисунок 12-4 показывает некий образец связующего дерева (этот пример применяется исключительно для целей иллюстрации: узлы в сетевой среде обычно не выравнены сеткой):

Мы достигаем полной согласованности между всеми точками без применения всех имеющихся граней.

Мы можем терять связность для определённого дерева целиком в случае разрыва всего одной связи.

Рисунок 12-4

Связующее дерево. Тёмные точки представляют узлы. Тёмные линии представляют сетевую сеть перекрытия. Серые линии представляют прочие доступные имеющиеся соединения между узлами.

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

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

Гибридный Gossip

Деревья со множественной адресацией Push/ lazy-push (Plumtrees, сливовые деревья) [LEITAO07] представляют некий компромисс между эпидемическими примитивами и примитивами широкого вещания на основании деревьев. Сливовые деревья работают через создание некого связующего дерева перекрывающихся узлов для активного распространения сообщений и с наименьшими накладными расходами. При нормальных условиях узлы отправляют полные сообщения всего лишь небольшому подмножеству одноранговых узлов предоставляя через одноранговую сеть службу выборки.

Каждый узел отправляет полное сообщение этому небольшому подмножеству узлов, а для всех остальных имеющихся узлов он лениво отправляет лишь значение идентификатора сообщения. Когда соответствующий узел получает сообщение со значением идентификатора некого сообщения, которое он никогда не видел, он может запросить у своих однорагоговых собратьев познакомит его с ним. Такой этап ленивой активной доставки (lazy- push) обеспечивает высокую надёжность и предоставляет некий способ быстрого лечения имеющегося дерева широкого вещания. В случае отказов протокол отступает к соответствующему подходу gossip через этапы ленивой активной доставки широковещательно распространяя соответствующее сообщение и восстанавливая необходимое перекрытие.

По причине самой природы распределённых систем, всякий узел и каждое соединение между узлами способны испытывать отказ в любой момент времени, делая невозможным обход по своему дереву когда этот сегмент становится недоступным. Ленивая сетевая среда gossip способствует уведомлению одноранговых партнёров относительно видимых сообщения для построения и восстановления своего дерева.

Рисунок 12-5 демонстрирует иллюстрацию такой двойственной связи: узлы соединены неким действенным связующим деревом (сплошные линии) и соответствующей ленивой сетью gossip (пунктирные линии). Эта иллюстрация не демонстрирует какие- то особые сетевые топологии, а всего лишь соединения между узлами.

Рисунок 12-5

Ленивые и энергичные сетевые среды. Сплошные линии представляют широковещательное дерево. Пунктирные линии обозначают ленивые gossip.

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

Частичные представления

Широковещательные сообщения всем известным одноранговым собратьям и сопровождение полного представления всего кластера может становиться затратным и непрактичным, в особенности когда имеющееся перемешивание (churn, мера общего числа присоединяющихся к системе и покидающих её узлов) достаточно высокое. Во избежание этого протоколы gossip зачастую применяют некую службу выборки одноранговых партнёров (peer sampling service). Эта служба сопровождает частичное представление всего кластера, которое периодически освежается при помощи gossip. Частичные представления перекрываются, так как в протоколах gossip желательна некая степень избыточности, однако слишком большая избыточность означает, что мы выполняем дополнительную работу.

Например, протокол HyParView (Hybrid Partial View, Гибридного частичного представления) [LEITAO07] поддерживает небольшое активное представление и большое пассивное представление своего кластера. Узлы из активного представления создают некое перекрытие и могут использоваться для рассеивания. Пассивное представление применяется для поддержки списка узлов, которые могут применяться для замены отказавших из имеющегося активного представления.

Периодически узлы выполняют операцию перетасовки, в процессе которой они обмениваются своими активными и пассивными представлениями. На протяжении такого обмена узлы добавляют участников как из пассивного, так и из активного представлений от своих одноранговых партнёров в свои пассивные представления, отключая самые старые значения для ограничения размера списка.

Активное представление обновляется на основании значения состояния изменений узлов в данном представлении и запросов от своих одноранговых собратьев. Когда процесс P₁ подозревает что P₂, один из одноранговых собратьев в его активном представлении, отказал, P₁ удаляет P₂ из своего активного представления и пытается установить соединение с заменяющим его процессом P₃ из пассивного представления. Если соединение неудачно, P₃ удаляется из пассивного представления P₁.

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

Данный подход способствует снижению общего числа сообщений в данной системе, применяя для рассеивания лишь узлы активного представления, в то же время поддерживая высокую надёжность применением пассивного представления в качестве некого механизма восстановления. Одним из измерений производительности и качества является то, насколько быстро служба выборки одноранговых партнёров сводит стабильное перекрытие в случаях реорганизации топологии [JELASITY04]. Результаты HyParView здесь достаточно высокие, благодаря тому как поддерживаются представления и так как он даёт преимущество раскручивающимся процессам.

HyParView и Plumtree (Сливовое дерево) применяют подход гибридного gossip: применяя небольшое подмножество одноранговых собратьев для широковещательных сообщений и отступая обратно к более широкой одноранговой сети в случае отказов и появления сетевых разделов. Обе системы не полагаются на некое глобальное представление, включающее в себя всех имеющихся одноранговых партнёров, что может быть полезным не только по причине большого числа узлов в данной системе (что не является таковым в большинстве случаев, но также по причине стоимостей, связанных с сопровождением актуального списка участников в каждом из узлов. Частичные представления позволяют узлам активно взаимодействовать только с небольшим подмножеством соседних узлов.

Выводы

Согласуемые в конечном счёте системы допускают расхождение состояния реплик. Настраиваемая согласованность позволяет нам обменивать согласованность на доступность и наоборот. Расхождение реплик может разрешаться с помощью одного из механизмов анти- энтропии:

Наводящая передача

Временно сохраняет записи в соседних узлах в случае когда его целевой узел остановлен и воспроизводит её для данной цели когда она возвращается в строй.

Восстановление чтением

Согласовывает запрашиваемые диапазоны данных в процессе своего чтения сравнивая отклики, выявляя пропущенные записи и отправляя их запаздывающим репликам.

Деревья Меркель

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

Побитные векторы соответствия версии

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

Такие подходы анти- энтропии оптимизируют один из трёх существенных параметров: сферу снижения, новизну и завершённость. Мы способны снизить сферу анти- энтропии только выполняя синхронизацию данных, которые активно запрашивались (восстановление чтением) или индивидуальных пропущенных записей (наводящая передача). Когда мы предполагаем что большинство отказов временное, а участники восстанавливаются после них настолько быстро, насколько это возможно, мы можем сохранять сам журнал наиболее последних событий расхождения и в точности знать что синхронизировать в случае отказа (побитные векторы соответствия версии). Когда нам требуется сопоставлять все наборы данных целиком во множестве узлов попарно, а также действенно определять место отличий между ними, мы можем хэшировать все данные и сопоставлять значения хэшей (деревья Меркель).

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

Многие современные системы применяют gossip для выявления отказов и сведений об участниках [DECANDIA07]. HyParView применяется в Partisan, высоко- производительной, обладающей большой масштабируемостью распределённой вычислительной инфраструктуре. Plumtree применялись в ядре Riak для сведений обо всём кластере.

[Замечание]
Последующее чтение

Если вы желаете узнать больше относительно упоминавшихся в этой главе понятий, вы можете ссылаться на следующие источники:

Протоколы gossip

Perrin, Matthieu. 2017. Distributed Systems: Concurrency and Consistency, (1st Ed.). Elsevier, UK: ISTE Press.

Shah, Devavrat. 2009. “Gossip Algorithms.” Foundations and Trends in Networking, 3, no. 1 (January): 1-125. https://doi.org/10.1561/1300000014.

Jelasity, Márk. 2003. “Gossip-based Protocols for Large-scale Distributed Systems.” Dissertation. http://www.inf.u-szeged.hu/~jelasity/dr/doktori-mu.pdf.

Demers, Alan, Dan Greene, Carl Hauser, Wes Irish, John Larson, Scott Shenker, Howard Sturgis, Dan Swinehart, and Doug Terry. 1987. “Epidemic algorithms for cated database maintenance.” In Proceedings of the sixth annual ACM Symposium on Principles of distributed computing (PODC ’87), 1-12. New York: Association for Computing Machinery. https://doi.org/10.1145/41840.41841.

Глава 11. Репликация и согласованность

Содержание

Глава 11. Репликация и согласованность

Достижение доступности

Печально известная CAP

Аккуратное применение CAP

Сбор и выход

Совместно используемая память

Упорядочение

Модели согласованности

Строгая согласованность

Линеаризация

Момент линеаризации

Стоимость линеаризации

Последовательная согласованность

Причинная согласованность

Векторные часы

Модели сеанса

Согласованность в конечном счёте

Настраиваемая согласованность

Свидетельские реплики

Строгая согласованность в конечном счёте и CRDTs

Выводы

Прежде чем мы двинемся в обсуждении консенсуса и алгоритмов атомарной фиксации, давайте соберём воедино последние фрагменты для их глубинного осознания: модели согласованности. Модели согласованности важны, так как они поясняют семантику видимости и поведение самой системы при наличии множества копий данных.

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

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

Репликация данных это некий способ введения избыточности сопровождением множества копий данных в такой системе. Тем не менее, поскольку атомарное обновление множества копий данных это задача, эквивалентная консенсусу [MILOSEVIC11], может быть достаточно дорогостоящим выполнять это действие для каждой операции в базе данных. Мы можем изучить некие более действенные в отношении стоимости и более гибкие способы выполнения согласованным поиска данных с точки зрения своего пользователя, в то же время допуская до некой степени расхождение между участниками.

Репликации особенно важны в реализациях со множеством центров обработки данных. Географические репликации в этом случае служат множеству целей: они увеличивают доступность и возможность устоять при отказах в одном или более центрах обработки данных предоставляя избыточность. Они также помогают снижению величины задержек помещая копии данных физически ближе к своим клиентам.

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

Достижение доступности

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

Доступность системы чрезвычайно важное свойство: при разработке программного обеспечения мы всегда стремимся достичь высокой доступности и пытаемся сводить к минимуму время простоя. Команды программистов бравируют своими измерениями времени работы без остановов. Мы столь сильно заботимся о доступности по ряду причин: программное обеспечение стало неотъемлемой частью сообщества и многие важные моменты не могут происходить без него: банковские транзакции, взаимодействие, путешествия и тому подобное.

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

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

Печально известная CAP

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

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

Мы бы желали достичь и согласованности и доступности, и в тоже время допускать разбиение сетевой среды на разделы. Сетевая среда может расщепляться на различные части когда процессы не имеют возможности взаимодействия друг с другом: некоторые отправляемые между разбитыми на разделы узлами сообщения не достигают своих получателей.

Доступность требует от любого не отказавшего узла доставки результатов, в то время как согласованность требует линеаризуемости результатов. Гипотеза CAP, сформулированная Эриком Брюером, обсуждает компромиссы между допустимостью Согласованности, Доступности и Разделов (Consistency, Availability и Partition) [BREWER00].

Требования доступности невозможно удовлетворить в некой асинхронной системе и мы не можем реализовать некую систему, которая одновременно обеспечивает и доступность и согласованность при наличии сетевых разделов [GILBERT02]. Мы можем строить системы, которые обеспечивают строгую согласованность в тоже время предоставляя наилучшие усилия в отношении доступности или гарантировать доступность при предоставлении наилучших усилий для согласованности [GILBERT12]. Наилучшие усилия в данном случае подразумевают, что когда всё работает, данная система не будет целенаправленно нарушать какие бы то ни было гарантии, но эти гарантии допускается ослаблять и нарушать в случае разбиения на сетевые разделы.

Иными словами, CAP описывает совокупность тесно связанных между собой событий потенциальных выборов, когда при различных сторонах имеющегося спектра у нас имеется система, которая:

Допускает согласованность и разделы

Системы CP предпочитают отказывать запросам для обслуживания потенциально несогласованных данных.

Допускает доступность и разделы

Системы AP утрачивают требование согласованности и позволяют обслуживать потенциально несогласованные значения в процессе запроса.

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

Предположение PACELEC [ABADI12], некое расширение гипотезы CAP, постулирует, что при наличии сетевых разделов существует некий выбор между согласованностью и доступностью (PAC). Иначе (E - Else), даже когда эта система работает нормально, нам всё ещё придётся выбирать между задержкой и согласованностью.

Аккуратное применение CAP

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

CAP подразумевает, что мы можем сталкиваться с задачами согласованности даже когда все узлы работают, но между ними имеются проблемы согласованности , так как мы ожидаем от каждого не отказавшего узла корректных откликов вне зависимости от того сколько узлов отказало.

Гипотеза CAP иногда иллюстрируется неким треугольником, как если бы могли повернуть ручку и получить в той или иной степени все три параметра. Однако когда мы поворачиваем ручку и меняем согласованность на доступность, допустимость разделов является неким свойством, которое мы в здравом уме можем настраивать в угоду чего то иного [HALE10].

[Совет]
Совет

Согласованность в CAP определяется несколько иначе чем в ACID (см. Главу 5). Согласованность ACID описывает согласованность транзакции: транзакция переводит свою базу данных из одного согласованного состояния в другое, поддерживая все имеющиеся инварианты базы данных (такие как однозначность ограничений и ссылочную целостность). В CAP она означает что операции являются атомарность (операции завершаются успешно или отказывают целиком) и согласованы (операции никогда не оставляют данные в несогласованном состоянии).

Доступность в CAP также отличается от обсуждавшейся ранее высокой доступности [KLEPPMANN15]. Определений CAP не накладывает никаких ограничений на задержку выполнения. Кроме того, доступность в базе данных, в противоположность CAP, не требует от каждого не отказавшего узла откликаться на каждый запрос.

Гипотеза CAP используется для объяснения распределённых систем, причин сценариев отказов и оценивает возможные ситуации, однако важно помнить, что нет чёткой линии раздела между отказом от согласованности и обслуживанием непредсказуемых результатов.

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

Сбор и выход

Гипотеза CAP обсуждает согласованность и доступность лишь в их самых строгих формах: линеаризуемочти и способности данной системы в конечном итоге откликаться на все запросы.

Это принуждает нас осуществлять сложный компромисс между этими двумя свойствами. Однако некоторые приложения способны извлекать преимущества из слегка ослабляемых предположений и мы можем задуматься об этих свойствах в их слегка более ослабленном виде.

Вместо того, чтобы быть либо согласованной, либо доступной, системы могут предоставлять ослабленные гарантии. Мы можем определить два настраиваемых измерения: Сбор (harvest) и Выход (yeld) для выбора между тем как всё- таки составлять верное поведение [FOX99].

Сбор

Определяет насколько завершён запрос: когда наш запрос возвращает 100 строк, но может осуществить выборку лишь 99 из- за недоступности части узлов, это всё же будет лучше чем провалить запрос целиком и не вернуть ничего.

Выход

Определяет общее число полностью успешно выполненных запросов в сравнении с общим числом предпринятых попыток. Извлечение дохода (yield) отличается от периода работоспособного состояния (uptime), поскольку, к примеру, занятой узел не простаивает, однако всё ещё способен отказывать части запросов.

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

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

Совместно используемая память

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

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

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

На Рисунке 11-1 вы можете видеть процессы P₁ и P₂, выполняющие различные операции:

Та операция, которая выполняется P₂, запускается после уже завершённой операции, выполняемой P₁ и эти две операции являются последовательными.

Существует некое перекрытие между этими двумя операциями, поэтому эти две операции одновременные.

Та операция, которая исполняется P₂, запускается после и завершается после операции, исполняемой P₁ Эти операции также одновременны.

Рисунок 11-1

Последовательные и одновременные операции

Множество читателей и писателей могут осуществлять доступ к определённому регистру одновременно. Операции чтения и записи с регистрами не являются прямыми и требуют какого- то времени. Одновременные выполняемые различными процессами операции чтения/ записи не последовательные (serial) в зависимости от того как регистры вели себя при перекрытии операций, они могут по разному упорядочиваться и способны производить различные результаты. В зависимости от того как вели себя регистры от одновременных операций, мы различаем три типа регистров:

Безопасные

Считывание в безопасные регистры может возвращать произвольные значения внутри данного диапазона этих регистров в процессе одновременных записей (что не звучит очень практичным, но может описывать саму семантику некой асинхронной системы, которая не фиксирует этот порядок). Безопасные регистры с двоичными значениями могут возникать в качестве мерцающих (то есть возвращаемые результаты перемежаются между этими двумя значениями) на протяжении считываний, одновременных с записями.

Обычные

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

Атомарные

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

Упорядочение

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

Давайте определим некую систему, в которой процессы способны выполнять операции read(register) и write(register, value) с совместно используемыми регистрами. Каждый процесс выполняет свои собственные последовательно действие (то есть всякая вызванная операция должна завершиться прежде чем он сможет запустить следующую). Такое сочетание последовательного исполнения процессов формирует некую глобальную историю при которой некоторые операции могут выполняться одновременно.

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

Process 1:      Process 2:
write(x, 1)     read(x)
                read(x)

Когда мы просматриваем эти события, не совсем ясно что мы получаем на выходе операций read(x) в обоих случаях. У нас имеются различные возможные истории:

Запись завершена до обоих считываний.

Запись и два чтения могут перемежаться и запись может быть выполнена между двумя считываниями.

Оба считывания выполнены до завершения записи.

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

Когда все эти операции выполняются определённым единственным процессом, мы смогли бы задать строгий порядок событий, однако то сложнее сделать в случае со множеством процессов. Мы можем сгруппировать все потенциальные сложности в две группы:

Операции могут перекрываться.

Эффекты от имеющихся не перекрывающихся вызовов могут быть невидимыми немедленно.

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

Модели согласованности

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

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

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

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

Мы можем представлять себе согласованность с точки зрения состояния, описывая допустимые инварианты состояния и установление разрешённых взаимодействий между копиями размещаемых в различных репликах копиях данных. В качестве альтернативы мы можем рассматривать согласованность операции, которая производится неким сторонним представлением для данного хранилища данных, описывать операции и накладывать ограничения на тот порядок, в котором они происходят [TANENBAUM06] [AGUILERA16].

Без неких глобальных часов трудно представлять распределённые операции в точном и детерминированном порядке. Это подобно Специальной Теории Относительности для данных: всякий участник имеет свою собственную точку зрения на состояние и время.

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

Модели согласованности добавляют другое измерение в то, что обсуждалось в разделе Печально известная CAP. Теперь нам приходится жонглировать не только согласованностью и доступностью, но также рассматривать согласованность в терминах стоимости синхронизации [ATTIYA94]. Стоимость синхронизации может содержать задержки, вспомогательные затрачиваемые на дополнительно выполняемые операции циклы ЦПУ, применяемый для сохранения восстанавливаемых сведений дисковый ввод/ вывод, время ожидания, сетевой ввод/ вывод и всё прочее, что может предупреждаться избежанием синхронизации.

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

Мы обсудим модели согласованности в терминах процессов (клиентов), вызывающих операции read и write для определённого состояния базы данных. Поскольку мы обсуждаем согласованность в контексте реплицируемых данных, мы предполагаем что наша база данных способна обладать множеством реплик.

Строгая согласованность

Строгая согласованность эквивалентна полной прозрачности репликаций: любая запись всякого процесса немедленно доступна для соответствующих последующих чтений любым процессом. Она вовлекает в себя понятие глобальных часов и, если в момент времени t₁ произошла write(x, 1) , любое read(x) вернёт вновь записанное значение 1 в любой момент времени t₂ > t₁.

К сожалению, это всего лишь теоретическая модель, и она не возможна в реализации, ибо законы физики и сами способы работы распределённых систем накладывают ограничение на то, насколько быстро могут происходить вещи [SINHA97].

Линеаризация

Линеаризуемость это самый сильный одиночный объект для модели согласованности с отдельной операцией. При данной модели эффекты записи становятся видимыми для всех читателей в точности один раз в некий момент времени между его началом и окончанием, и никакой клиент не способен наблюдать состояние перехода или сторонние эффекты частичной (то есть незавершённой, всё ещё пребывающей на лету) или невыполненной (то есть прерванной до завершения) операций записи [LEE15].

Одновременные операции представляются как одна из возможных последовательных историй при которой удерживаются свойства видимости. При линеаризуемости существует некий недетерминизм, ибо могут иметься более одного способа упорядочения [HERLIHY90].

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

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

Линеаризуемость признаётся как порядком последовательных операций локального процесса, так и установленным порядком запущенных параллельно операций относительно прочих процессов и определяет всеобщий порядок происходящих событий.

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

Даже несмотря на то, что операции одновременны и обладают неким перекрытием, сами действия, благодаря которым они становятся видимыми до некой степени заставляют их казаться последовательными. Никакая операция не происходит мгновенно, однако всё ещё проявляется как атомарная.

Давайте рассмотрим следующую историю:

Process 1:      Process 2:     Process 3:
write(x, 1)     write(x, 2)    read(x)
                               read(x)
                               read(x)

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

Самая первая операция чтения может вернуть значения 1, 2 и ∅ (первоначальное значение, состояние до обеих записей) так как обе записи всё ещё в процессе полёта. Самое первое считывание может получить порядок до обеих записей, между первой и второй записями и после обеих записей.

Наше второе чтение может вернуть только 1 и 2, так как самая первая запись уже выполнена, а вторая ещё пока не завершилась.

Третье считывание способно вернуть лишь 2, так как вторая запись по порядку идёт после первой.

Рисунок 11-2

Образец линеаризации

Момент линеаризации

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

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

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

[Замечание]
Замечание

Большинство языков программирования в наши дни предлагает некие атомарные примитивы, которые допускают атомарные операции write и compare-and-swap (CAS). Атомарная операция write не принимает в расчёт текущее значение регистра, в то время как CAS, которая перемещает одно значение в другое только когда предыдущее значение неизменно [HERLIHY94]. Считывание определённого значения, его изменение и последующая запись при помощи CAS это более сложно чем простая проверка и установка нового значения по причине возможности проблемы ABA [DECHEV10]: если CAS ожидает наличия в этом регистре значения A, оно будет установлено даже если было установлено значение B и затем переключено обратно в A другими прочими одновременными операциями записи. Иначе говоря, наличие такого значения A не гарантирует того, что данное значение не было изменено с момента последнего считывания.

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

Рисунок 11-3 показывает, что линеаризация предполагает жёсткие границы и наличие часов в реальном времени с тем, чтобы эффекты операции становились видимыми между t₁, когда была вызвана данная операция и t₂, когда этот процесс получил отклик.

Рисунок 11-3

Временные ограничения операции линеаризации

Рисунок 11-4 иллюстрирует, что момент линеаризации рассекает историю на до и после. До данного момента линеаризации видно старое значение, после него видимым является новое значение.

Рисунок 11-4

Момент линеаризации

Стоимость линеаризации

В наши дни многие системы избегают реализации линеаризации. Даже ЦПУ не предлагает по умолчанию линеаризуемости при доступе к основной памяти. Это происходит по причине того, что инструкции синхронизации затратны, медленны и вовлекают в себя обмен между ЦПУ узлов с признанием несостоятельности кэширования. Тем не менее, существует возможность линеаризуемости при помощи примитивов нижнего уровня [MCKENNEY05a] [MCKENNEY05b].

При одновременном программировании для введения линеаризуемости мы можем пользоваться операциями сравнения- и- перестановки (compare-and-swap). Многие алгоритмы занимаются подготовкой с последующим CAS для замены местами указателей и их публикацией. Например, мы можем реализовать некую очередь одновременности созданием связанного списка узлов, который затем атомарно добавляется в конец имеющегося хвоста самого списка [KHANCHANDANI18].

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

Что занимательно, линеаризуемость в её традиционном понимании относится к некому локальному свойству и подразумевает составление из независимо реализуемых и удостоверяемых элементов [HERLIHY90]. Иначе говоря, некая система, в которой все элементы линеаризованы, также линеаризуема. Это очень полезное свойство, но нам следует помнить, что его сфера ограничена единственным объектом и, даже если операции с двумя независимыми объектами и линеаризуемы, операции в которые вовлечены оба объекта должны полагаться на дополнительные средства синхронизации.

[Замечание]
Инфраструктура с повторным использованием для линеаризуемости

Инфраструктура с повторным использованием для линеаризуемости (RIFL, Reusable Infrastructure for Linearizability) это некий механизм для реализации линеаризуемости вызовов удалённой обработки (RPC, remote procedure calls) [LEE15]. В RIFL сообщения уникально идентифицируются значением идентификатора клиента и монотонно возрастающим последовательным номером..

Для назначения идентификатора клиента RIFL применяет найм (leases), вызывая общую для всей системы службу: уникальные идентификаторы применяются для установления уникальности и разрыва связей последовательной нумерации. Когда определённый отказавший клиент пытается выполнить некую операцию при помощи найма с истекшим сроком, эта операция не будет фиксирована: этому клиенту придётся выполнить новый найм и повторить действие.

Если определённый сервер испытывает крушение до того как он смог выдать уведомление о записи, его клиент может попытаться повторить эту операцию не зная о том, что она уже была применена. Мы даже можем завершить ситуацией, при которой клиент C1 записал значение V1, но не получил подтверждающего уведомления. Тем временем, клиент C2 записал значение V2. Если клиент C1 повторяет свою операцию и записывает V1, соответствующая запись C2 будет утрачена. Чтобы избежать такого развития событий, нашей системе требуется предотвращать вторичного выполнение повторяемых попыток операций. Когда соответствующий клиент повторяет попытку своей операции, RIFL возвращает завершённый объект, указывая что та операция, с которой он связан уже был исполнен и возвращает полученный результат.

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

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

Основным преимуществом RIFL является то, что обеспечивая гарантию того, что данный RPC не может быть выполнен более одного раза, некая операция может быть превращена в линеаризуемую обеспечивая атомарную видимость ей результатов, причём основная часть подробностей реализации не зависит от лежащей в основе системы хранения.

Последовательная согласованность

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

Процессы могут наблюдать исполняемые прочими участниками операции в соответствующем согласованном порядке их собственной истории, однако это представление может произвольно устаревать с глобальной точки зрения [KINGSBURY18a]. Порядок исполнения между процессами не определён, так как нет никакого совместного представления времени.

Последовательная согласованность вначале была введена в контексте одновременности, описываясь как некий способ корректного выполнения программ множеством процессоров. Самое первоначальное описание требовало упорядочения в очередь (FIFO, в порядке возникновения) запросов к памяти в одной и той же самой ячейке, не выставляя глобального порядка для перекрывающихся записей в независимые ячейки памяти и допуская считывания выборок необходимых ячеек памяти, или самых последних значений из самой очереди, когда эта очередь не пуста [LAMPORT79]. Этот пример помогает понимать семантику последовательной согласованности. Операции могут упорядочиваться различными способами (в зависимости от самого порядка возникновения, или даже произвольно в случае одновременно появляющихся двух записей), однако все процессы наблюдают эти операции в одном и том же порядке.

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

[Замечание]
Замечание

Последовательная согласованность часто путается с линеаризуемостью, так как оба понятия имеют аналогичную семантику. Последовательная согласованность, как и линеаризуемость, требует глобальной упорядоченности операций, однако линеаризуемочть требует согласованности как для локального порядка в каждом из процессов, так и глобального порядка. Иначе говоря, линеаризуемость ожидает некого порядка операций в реальном масштабе времени. При последовательной согласованности упорядочение необходимо только для записей от того же самого источника [VIOTTI16]. Другим важным отличием является композиция: мы можем сочетать линеаризуемые истории и всё ещё ожидать что результат будет линеаризуемым,в то время как расписания последовательной согласуемости не сочетаются в композиции [ATTIYA94].

Рисунок 11-5 показывает как write(x,1) и write(x,2) могут становиться видимыми для P₃ и P₄. Даже хотя в терминах ходиков (wall-clock) 1 была записана до 2, он может идти по порядку после 2. В то же самое время, поскольку P₃ уже считал значение 1, P₄ всё ещё может продолжать чтение 2. Однако, оба допустимы порядка, 1 → 2 и 2 → 1, так как они согласованны для различных читателей. Что здесь важно, так это то, что и P₃, и P₄ наблюдают значения в том же самом порядке: сначала 2, затем 1 [TANENBAUM14].

Рисунок 11-5

Упорядочение в последовательно согласованности

Считывания устаревших сведений можно пояснить, например, расхождением реплик: даже хотя записи распространяются в различные реплики в одном и том же порядке, они могут появляться в различное время.

Основным отличием от линеаризуемости является отсутствие глобальных принудительных ограничений по времени. При линеаризуемости некая операция вступает в действие внутри временных границ ходиков. Ко времени завершения операции записи W₁ её результаты уже должны быть применены, и каждый читатель должен видеть то значение, которое по крайней мере не позднее записанного W₁. Аналогично, после возврата операции считывания R₁, всякая происходящая после неё операция должна возвращать увиденное R₁ значение, или более позднее (что, естественно, должно следовать одному и тому же правилу).

Последовательная согласованность ослабляет это требование: результаты операции могут становиться видимыми после такого завершения, до тех пор пока этот порядок находится в согласии с точкой зрения его персонального процесса. Записи из одного и того же источника не могут &quuot;перепрыгивать&quuot; друг через друга: установленного в их программе порядка, относительно их собственного процесса исполнения, который должен сохраняться. Другим ограничением является то, что тот порядок, в котором операции должны появляться должны быть согласованными для всех читателей.

Аналогично линеаризуемости, современные ЦПУ не обеспечивают последовательной согласованности по умолчанию, так как сам процессор способен переупорядочивать инструкции, нам следует применять барьеры памяти (также носящие название ограждений - fences) чтобы обеспечивать последовательность видимости записей одновременно исполняемых потоков по порядку [DREPPER07] [GEORGOPOULOS16].

Причинная согласованность

Видите ли, существует лишь одна постоянная, одна универсальная, и именно выступающая единственной истиной: причинность. Действие. Реакция. Причина и следствие.

- МеровингенМатрица. Перезагрузка.

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

Прежде всего, давайте рассмотрим зачем нам требуется причинность и как должны распространяться записи без причинной взаимосвязи. На Рисунке 11-6 выполняющие записи процессы P₁ и P₂ не связаны в причинном порядке. Получаемые в результате этих операций результаты могут распространяться читателям в различное время и в не установленном порядке. Процесс P₃ видит значение 1 до того как он заметит значение 2, в то время как процесс P₄ сначала видит 2, а уж затем 1.

Рисунок 11-6

Не обладающие причинной зависимостью операции записи

Рисунок 11-7 показывает некий образец причинно связанных записей. Дополнительно к записанным значениям, нам теперь требуется определить значение логических часов, которое установило бы причинный порядок между операциями. P₁ стартует с операции write(x,∅,1)→t₁, которая начинается со своего начального значения ∅, P₂ исполняет другую операцию, write(x, t₁, 2) и определяет, что она логически следует за t₁, требующей распространения операции только в том порядке, который установлен их логическими часами.

Рисунок 11-7

Причинно связанные операции записи

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

Рисунок 11-8 отображает процессы P₁ и P₂, делающие причинно связанные записи, которые распространяются в P₃ и P₄ в их логическом порядке, это предоствращает нас от той ситуации, которая отображена на Рисунке 11-6; вы можете сопоставить истории P₃ и P₄ на обоих рисунках.

Рисунок 11-8

Операции записи с причинными взаимными зависимостями

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

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

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

Те два заметных и часто цитируемых проекта, которые реализованы при помощи причинной согласованности являются COPS (Clusters of Order-Preserving Servers, Кластеры серверов сохранения заказов) [LLOYD11] и Eiger [LLOYD13]. Оба проекта реализуют причинность через некую библиотеку (реализуемую как некий сервер интерфейса, к которому подключены пользователи) и отслеживают зависимости для обеспечения согласованности. COPS отслеживает зависимости версиями ключей, в то время как Eiger устанавливает вместо этого порядок операций (операции в Eiger могут зависеть от исполняемых в прочих узлах операций; например, в случае транзакций со множеством разделов). Оба проекта не содержат выходящих из строя операций, как это в конечном счёте могут осуществлять согласованные хранилища. Вместо этого они выявляют и обрабатывают конфликты: в COPS это выполняется проверкой порядка значений ключей и использованием особенных для приложения функций, а Eiger реализует правило выигрывает- последний- записывающий.

Векторные часы

Установление причинно- следственной связи помогает данной системе восстанавливать последовательность событий даже когда сообщения доставляется не по порядку, заполнять промежутки между получаемыми сообщениями и избегать публикации операций, когда некоторые сообщения всё ещё отсутствуют. Например, если каждое из определяющих свои зависимости сообщений {M1(∅, t1), M2(M1, t2), M3(M2, t3)} являются причинно связанными и распространялись без соблюдения порядка, сам процесс буферирует их до тех пор, пока он способен собирать все зависимости операций и восстанавливать ил причинный порядок [KINGSBURY18b]. Многие базы данных, например, Dynamo [DECANDIA07] и Riak [SHEEHY10a] используют вектор часов [LAMPORT78] [MATTERN88] для установления порядка причинности.

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

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

Мы уже обсуждали согласованность в терминах доступа к регистрам совместно используемой памяти и упорядочения операций ходиками, а также впервые упомянули потенциально возможное расхождение реплик при обсуждении последовательной согласованности. Поскольку необходимо упорядочивать только операции записи в одну и ту же область памяти, мы не можем в конечном результате оказаться в ситуации, при которой мы будем иметь конфликт когда значения независимы [LAMPORT79].

Так как мы находимся в поиске модели согласованности, которая допускала бы повышение и доступности, и производительности, нам придётся позволять расхождение реплик не только за счёт обслуживания устаревших операций считывания, но и путём принятия потенциально вступающих в конфликт записей, а потому данной системе позволяется создание двух независимых цепочек событий. Рисунок 11-9 отображает такое расхождение: с точки зрения одной из реплик, мы видим историю как 1, 5, 7, 8 , в то время как другая сообщает 1, 5, 3. Riak позволяет пользователям наблюдать и разрешать расходящиеся истории [DAILY13].

Рисунок 11-9

Расходящиеся истории причинной согласованности

[Замечание]
Замечание

Для реализации причинной согласованности нам приходится сохранять историю причин, добавлять сборку мусора и запрашивать своего пользователя на примирение расходящихся историй в случае их конфликта. Векторные часы способны сообщать нам о происхождении такого конфликта, но не предлагают в точности как его разрешать, так как семантика разрешения зачастую является особенной для приложения. По этой причине некоторые в конечном счёте согласованные базы данных, например, Apache Cassandra, не упорядочивают операции по причинам и применяют вместо этого для разрешения конфликта правило выигрывает- последний- записывающий [ELLIS13].

Модели сеанса

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

Модели сеанса [VIOTTI16] (также носящие название клиент- центрических моделей согласованности [TANENBAUM06]) способствуют рассуждениям относительно значения состояния распределённой системы с точки зрения самого клиента: как каждый клиент наблюдает значение состояния данной системы при вызове операций чтения и записи.

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

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

Одним из разумных ожиданий является то, что всякая вызванная клиентом запись видна ему. Именно это предположение скрывается за моделью согласованности чтения- собственных- записей, которая постулирует, что каждая операция считывания, следующая за соответствующей записью в той же самой или иной реплике обязана наблюдать своё изменённое значение. Например, read(x), которое выполнялось немедленно после write(x,V) вернёт установленное значение V.

Модель монотонных чтений ограничивает значение видимости значения и состояния тем, что если определённое read(x) наблюдало величину значения V, все последующие чтения должны наблюдать значение по крайней мере настолько же позднее как V или некоторые более поздние значения.

Модель монотонных записей предполагает, что возникающие от одного и того же клиента значения появляются в том порядке, в котором этот клиент их исполняет. Если, согласно установленному порядку сеанса данного клиента, write(x,V2) была выполнена после write(x,V1), их эффект должен быть виден в том же самом порядке (то есть вначале V1, затем V2) для всех прочих пользователей. Без данного предположения старые данные способны "воскресать", приводя в результате к утрате данных.

Записи- следую- за- чтениями (иногда именуемая причинностью сеанса) обеспечивает что записи идут по порядку за записями, которые наблюдались в предыдущих операциях чтения. Например, когда write(x,V2) идёт по порядку после read(x), которое вернуло V1, write(x,V2) будет идти по порядку после write(x,V1).

[Предостережение]
Предостережение

Модели сеансов не делают предположений относительно операций, выполняемых различными процессами (клиентами) или из различных логических сеансов [TANENBAUM14]. Эти модели описывают порядок с точки зрения одного отдельного процесса. Тем не менее, те же самые гарантии должны сохраняться для каждого процесса в этой системе. Иными словами, когда P₁ способен считывать свои собственные записи, P₂ также должен иметь возможность считывать его собственные записи..

Сочетание монотонных чтений, монотонных записей и считывания- собственных- записей даёт согласованность PRAM (Pipelined RAM) [LIPTON88] [BRZEZINSKI03], также именуемую как согласованность FIFO. PRAM обеспечивает, что появившиеся из одного из процессов операции записи будут распространяться в том порядке, в котором они исполнялись этим процессом. В отличии от последовательной согласованности, записи различных процессов могут наблюдаться в различном порядке.

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

Согласованность в конечном счёте

Синхронизация затратна как в отношении программирования множества процессоров, так и в распределённых системах. Как мы уже обсуждали в Моделях согласованности, мы можем ослаблять гарантии согласованности и применять модели, которые допускают некое расхождение между узлами. К примеру, последовательная согласованность допускает распространение чтений с различной скоростью.

При согласованности в конечном счёте обновления распространяются по рассматриваемой системе асинхронно. Формально, она постулирует, что при отсутствии каких бы то ни было дополнительных обновлений, выполняемых для определённого элемента данных, в конечном счёте все доступы возвратят самое последнее записанное значение [VOGELS09]. В случае некоторого конфликта, данное понятие самого последнего может измениться, поскольку сами значения от разошедшихся реплик примиряются при помощи некой стратегии разрешения конфликтов, например, выигрывает- последняя- запись или применяющая Векторные часы.

В конечном счёте это занимательное понятие для описания распространения значения, так как оно не определяет никаких жёстких временных рамок в которые это должно произойти. Когда служба доставки не производит ничего иного кроме гарантий "в конечном счёте", это не звучит как нечто, на что можно полагаться.

Тем не менее, на практике это хорошо работает и многие базы данных в наши дни описываются как имеющие согласованность в конечном счёте.

Настраиваемая согласованность

Системы с согласованностью в конечном счёте иногда описываются в терминах CAP: вы можете менять доступность на согласованность и наоборот (см.раздел Печально известная CAP). С точки зрения стороны сервера, согласуемые в конечном счёте системы обычно реализуют настраиваемую согласованность, при которой данные реплицируются, считываются и записываются применяя три изменчивости:

Множитель репликации N

Число узлов, которое будет хранить некую копию данных.

Согласованность записи W

Число узлов, которое должно выдать подтверждение записи для её успешности.

Согласованность чтения R

Число узлов, которое должно отвечать на операцию чтения для её успешности.

Выбирая уровни согласованности, при которых (R + W > N), данная система способна обеспечивать возврат наиболее последних записанных значений, поскольку всегда имеется некое перекрытие между наборами чтений и записи. Например, когда N = 3, W = 2 и R = 2, данная система будет безразлична к отказу всего одного узла. Два узла из трёх обязаны выдавать подтверждение соответствующей записи. В идеальном сценарии данная система также асинхронно реплицирует эту запись в свой третий узел. Когда третий узел отключён, механизмы анти- энтропии (см. Главу 12) в конечном счёте распространят её.

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

[Совет]
Совет

При выполнении некой записи соответствующий координатор обязан поставлять её в N узлов, но может дожидаться её выполнения лишь W узлами прежде чем продолжить (или W - 1 в случае этого координатора тоже реплика). Остальные операции записи могут выполняться асинхронно или вообще завершаться отказом. Аналогично, при выполнении считывания, данный координатор обязан собрать по крайней мере R откликов. Некоторые базы данных применяют умозрительное исполнение и указывают дополнительные запросы чтения для снижения задержки отклика координатора. Это означает, что если один из пердварительно представленных запросов на чтение завершился отказом, или возникает медленно, вместо этого в сторону R могут вычисляться умозрительные запросы..

Системы с интенсивной записью могут порой указывать W = 1 и R = N, что делает возможными записи при получении подтверждения для их успешности лишь от одного узла, однако это потребовало бы от всех реплик (даже потенциально отказавших) доступности на чтение. То же самое справедливо и при комбинации W = N, R = 1: самое последнее значение может быть считано с любого узла, до тех пор пока записи успешны только после их применения ко всем репликам.

Увеличение уровней согласованностей чтения или записи увеличивает задержки и повышает требования доступности для узлов в процессе запросов. Их снижение улучшает доступность системы при принесении в жертву согласованности.

[Замечание]
Кворумы

Некий состоящий из ⌊N/2⌋ + 1 узлов уровень согласованности носит название кворума, большинства узлов. В случае разбиения сетевой среды на части или отказа узлов, в системе с 2f + 1 узлами, живые узлы способны продолжать приём чтений и записей при недоступности до f узлов до тех пор, пока оставшаяся часть кластера не станет снова доступной. Иными словами, такая система безразлична к отказу не более f узлов.

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

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

Свидетельские реплики

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

Мы можем улучшить стоимость хранения при помощи понятия, носящего название свидетельской реплики (witness replicas).. Вместо того чтобы хранить копию соответствующей записи в каждой реплике, мы можем расщеплять реплики на подмножества копий и свидетельств. Реплики копий по- прежнему содержат как и ранее записи данных. При обычных операциях реплики свидетельства единственно хранят те записи, которые указывают факт того, что данная операция записи произошла. Тем не менее, может произойти некая ситуация, при которой общее число копий реплик слишком низко. Например, когда у нас имеются три копии реплик и два свидетельства, а две копии реплик падают, мы приходим к кворуму из одной реплики и двух свидетельств реплик.

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

Давайте рассмотрим некую систему с репликациями в трёх узлах, два из которых хранят копии данных, а третья обслуживается в качестве свидетельской: [1c, 2c, 3w]. Мы предпринимаем попытку выполнения записи, однако 2c временно недоступна и мы не можем завершить операцию. В этом случае мы временно сохраняем необходимую запись в своей свидетельской реплике 3w. Когда в конце концов 2c: вернётся обратно в сторй, механизмы восстановления приведут её в современное состояние и удалят избыточную копию у свидетельской.

При неком ином сценарии мы можем попытаться выполнить некое чтение, а запись имеется в 1c и 3w, но не в 2c. Так как для построения кворума достаточно любых двух реплик, когда имеются подмножества узлов из двух реплик, будь то копии двух реплик [1c, 2c] или одна реплика копии и одно свидетельство, [1c, 3w] или [2c, 3w], мы способны гарантированно обслуживать согласованный результаты. При чтении из [1c, 2c] мы выбираем самую последнюю запись из 1c и можем реплицировать её в 2c, так как в ней значение пропущено. В случае когда доступна лишь [2c, 3w], самая последняя запись будет выбрана из 3w. Для восстановления первоначальной конфигурации и приведения 2c в актуальное состояние, необходимая запись может быть реплицирована в неё и удалена из свидетельской.

В более общем случае, когда мы обладаем n репликами копий и m репликами свидетельств, мы можем предоставить те же самые гарантии что и n + m копий, когда мы следуем двум правилам:

Операции чтения и записи выполняются с применением большинства (т.е., N/2 + 1 участниками)

По крайней мере одна из имеющихся реплик в этом кворуме обязательно является копией

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

Применение свидетельских реплик помогает снижению стоимости хранения в то же время сохраняя инварианты согласованности. Существуют различные реализации данного подхода; например, Spanner [CORBETT12] и Apache Cassandra.

Строгая согласованность в конечном счёте и CRDTs

Мы обсудили некоторые модели строгой согласованности, такие как линеаризуемость и сериализация, а также сформировали слабую согласованность: согласованность в конечном счёте. Возможная золотая середина находится промеж них, предлагая некоторые преимущества обеих моделей и это строгая согласованность в конечном счёте. Для данной модели обновления в серверы допускаются поздними и с нарушением порядка, однако когда все обновления в конечном итоге распространены в целевые узлы, между ними могут быть разрешены конфликты и они могут быть слиты для производства одного и того же допустимого состояния [GOMES17].

При некоторых обстоятельствах мы можем ослаблять свои требования согласованности, допуская операциям сохранять дополнительное состояние, которое позволяет согласовывать расходящиеся состояния (иными словами, сливать) после выполнения. Одним из наиболее выдающихся образцов такого подхода является реализация CRDTs (Conflict-Free Replicated Data Types, Освобождённые от конфликтов реплицируемые типы данных, [SHAPIRO11a]), например, в REDIS [BIYIKOGLU13].

CRDTs это специализированные структуры данных, которые предотвращают само наличие конфликтов и позволяют операциям с этими типами данных применяться в любом порядке без изменения результата. Данное свойство может быть чрезвычайно полезным в распределённых системах. Например, в системе со множеством узлов, которая применяет свободные от конфликтов реплицируемые счётчики, мы можем увеличивать приращениями значения в каждом из узлов независимо, причём даже когда они не способны взаимодействовать друг с другом по причине наличия сетевых разделов. Как только взаимодействие восстановится, результаты от всех узлов могут быть согласованы и не буде утеряна ни одна из операций, применённых в процессе такого разбиения на разделы.

Это делает полезным CRDTs для согласуемых в конечном счёте систем, так как состоянием реплик в таких системах допустимо временно расходиться. Реплики могут исполнять операции локально, причём без предварительной синхронизации с прочими узлами, а операции в конечном счёте распространяются во все прочие реплики, и при этом потенциально не в установленном порядке. CRDTs позволяет нам восстанавливать полное состояние системы из локальных индивидуальных состояний или последовательностей операций.

Самым простым примером CRDTs являются основанные на операциях CmRDTs (Commutative Replicated Data Types, Коммутативные реплицируемые типы данных). Для работы с CmRDTs нам необходимо позволить операциям быть:

Свободными от сторонних эффектов

Их приложение не изменяет состояние системы.

Коммутативными

Порядок аргументов не имеет значения: x • y = y • x. Иначе говоря, нет разницы сливается ли х c y, либо y сливается с х.

Причинная упорядоченность

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

Например, мы можем реализовать исключительно растущий счётчик. Каждый сервер может хранить некий вектор состояния, соответствующего последним известным обновлениям счётчика от прочих участников, инициализированный нулями. Каждому серверу дозволено обновлять только своё собственное значение в этом векторе. При распространении обновлений, функция merge(state1, state2) сольёт значения состояний от двух серверов.

Например, у нас имеются три сервера с инициализированными начальными векторами состояний:

Node 1:          Node 2:          Node 3:
[0, 0, 0]        [0, 0, 0]        [0, 0, 0]

Когда мы обновляем счётчики в первом и третьем узлах, их состояния изменяются следующим образом:

Node 1:          Node 2:          Node 3:
[1, 0, 0]        [0, 0, 0]        [0, 0, 1]

При распространении обновлений мы применяем функцию слияния для сочетания результатов, указывая максимальные значения для каждого из слотов:

ode 1 (Node 3 state vector propagated):
merge([1, 0, 0], [0, 0, 1]) = [1, 0, 1]

Node 2 (Node 1 state vector propagated):
merge([0, 0, 0], [1, 0, 0]) = [1, 0, 0]

Node 2 (Node 3 state vector propagated):
merge([1, 0, 0], [0, 0, 1]) = [1, 0, 1]

Node 3 (Node 1 state vector propagated):
merge([0, 0, 1], [1, 0, 0]) = [1, 0, 1]

Для определения значения текущего состояния вектора вычисляется сумма значений во всех слотах: sum([1, 0, 1]) = 2. Данная функция слияния коммутативна. Поскольку серверам дозволено обновлять лишь свои собственные значения, а эти значения независимы, не требуется никакой дополнительной координации.

Существует возможность создания Позитивно- негативного счётчика (PN- Counter, Positive-Negative-Counter), который поддерживает как приращения так и вычитания применяя полезные нагрузки, состоящие из двух векторов: P, который применяется для приращений, и N, в котором хранятся вычитания. В некой большой системе во избежание распространения гигантских векторов мы може пользоваться super-peers (суперодноранговостью). Суперодноранговость распространяет состояния счётчиков и помогает избегать постоянного однорангового дребезга [SHAPIRO11b].

Для сохранения и репликации значений мы можем применять регистры. Самой простой версией такого регистра является регистр выигрывает- последний- записавший (LWW регистр, last-write-wins), который хранит некую уникальную, глобально упорядоченную временную отметку, прикрепляемую к каждому значению для разрешения конфликтов. В случае конфликта записи мы сохраняем лишь одно значение с самой большой временной отметкой. Когда мы не можем допускать отбрасывания значений, мы можем применять особую для приложения логику слияния и пользоваться регистром со множественным значением, который хранит все значения, которые были записаны и позволяет самому приложению указывать верное.

Другим примером CRDTs выступает неупорядоченное только- растущее множество (G-Set, grow-only set). Каждый узел поддерживает своё собственное локальное состояние и может добавлять в его конец элементы. Добавление элементов производит некое допустимое множество. Слияние двух множеств также коммуникативная операция. Аналогично счётчикам, мы можем пользоваться двумя множествами для поддержки как добавлений, так и удалений. В этом случае мы можем сохранять некий инвариант: только содержащиеся во множестве добавлений могут добавляться в имеющееся множество удалений. Для восстановления текущего состояния этих множеств, все содержащиеся в множестве удалений вычитаются из множества добавлений [SHAPIRO11b].

Неким примером бесконфликтного типа, который сочетает более сложные структуры является бесконфликтный тип реплицируемых JSON данных, допускающий такие изменения, как вставки, удаления и назначения в глубоко встраиваемые документы JSON с типами списком и соответствий. Этот алгоритм выполняет операции слияния на стороне клиента и не требует распространения операций неким особенным образом[KLEPPMANN14].

Существует достаточно много возможностей, которые предоставляют нам CRDTs м мы можем наблюдать всё больше хранилищ данных, которые используют это понятие для предоставления SEC (Strong Eventual Consistency, Строгой согласованности в конечном счёте). Это мощное понятие которое можно добавлять в наш арсенал инструментов для построения отказоустойчивых распределённых систем.

Выводы

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

Мы обсудили некоторые модели согласованности с единственной операцией в порядке от тех, которые дают больше гарантий к тем, у которых их меньше (Эти краткие определения даны исключительно для повтора, читателю рекомендуется обратиться к полным определениям для осознания контекста):

Линеаризуемость

Операции проявляются как применяемые мгновенно, а также поддерживается порядок операций в реальном времени.

Последовательная согласованность

Эффекта от операций распространяются в неком общем порядке, причём этот порядок соответствует тому порядку, коим они выполнялись своими индивидуальными процессами.

Причинная согласованность

Эффекты от имеющих причинную взаимосвязь операций видятся в одном и том же порядке всеми процессами.

Согласованность PRAM/FIFO

Эффекты от операций становятся видимыми в том же самом порядке, как они исполняются отдельными процессами. Записи от отдельных процессов могут наблюдаться в различном порядке.

После этого мы обсудили модели множества сеансов:

Считывание- собственных- записей

Операции чтения отражают свои предыдущие записи. Записи распространяются в данной системе и становятся доступными для последующих чтений того же самого клиента.

Монотонные чтения

Любое чтение, наблюдавшее некое значение не способно наблюдать значение, которое старше уже наблюдавшегося.

Монотонные записи

Поступающие от одного и того же клиента записи распространяются прочим клиентам в том порядке, в каком они сделаны этим клиентом.

Записи- следуют за- считываниями

Операции записи упорядочиваются после записей, которые вступили в действие после их наблюдения в предыдущих считываниях того же самого клиента.

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

Мы также обсудили понятия согласованности в конечном счёте и настраиваемой. основанные на кворуме системы пользуются большинством для обслуживания согласованности данных. Для снижения стоимости хранения могут применяться свидетельские реплики.

[Замечание]
Последующее чтение

Если вы желаете узнать больше относительно упоминавшихся в этой главе понятий, вы можете ссылаться на следующие источники:

Модели согласованности

Perrin, Matthieu. 2017. Distributed Systems: Concurrency and Consistency, (1st Ed.). Elsevier, UK: ISTE Press.

Viotti, Paolo and Marko Vukolić. 2016. “Consistency in Non-Transactional Distributed Storage Systems.” ACM Computing Surveys , 49, no. 1 (July): Article 19. https://doi.org/0.1145/2926965.

Bailis, Peter, Aaron Davidson, Alan Fekete, Ali Ghodsi, Joseph M. Hellerstein, and Ion Stoica. 2013. “Highly available transactions: virtues and limitations.” Proceedings of the VLDB Endowment 7, (no. 3 (November): 181-192. https://doi.org/10.14778/2732232.2732237.

Aguilera, M.K., and D.B. Terry. 2016. “The Many Faces of Consistency.” Bulletin of the Technical Committee on Data Engineering 39, no. 1 (March): 3-13.

Глава 5. Обработка и восстановление транзакции

Содержание

Глава 5. Обработка и восстановление транзакции

Управление буфером

Семантики кэширования

Вытеснение кэша

Блокировка страниц в кэше

Страничный обмен

FIFO и LRU

CLOCK

LFU

Восстановление

Семантики журнала

Сопоставление журналов действий и данных

Политики воровства и принуждения

ARIES

Управление одновременностью

Возможность преобразования в последовательную форму

Изоляция транзакций

Аномалии чтения и записи

Уровни изоляции

Оптимистичное управление одновременностью

Управление одновременностью множества версий

Пессимистичное управление одновременностью

Управление одновременностью на основе блокировок

Взаимные блокировки

Блокировки

Защёлки

Блокировка читатели- писатель

Сцепляющая защёлка

Деревья Blink

Выводы

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

Некая транзакция является неделимым элементом работы системы управления базой данных, которая позволяет вам представлять множество операций как единый этап. Выполняемые транзакциями операции включают считывание и сохранение записей базы данных. Некая транзакция базы данных обязана сохранять атомарность (atomicity), связность (consistency), изоляцию (isolation) и долговечность (durability). Эти свойства обычно носят название ACID [HAERDER83]:

Атомарность

Этапы транзакции невидимы, что означает, что либо все связанные с этой транзакцией этапы успешно выполнены, либо не сделан ни один из них. Иными словами, транзакции не должны применяться частично. Каждая транзакция может быть либо зафиксирована (commit, внести все изменения операций записи выполняемых в процессе видимости этой транзакции), либо прерваться (abort, откатить обратно все эффекты, которые ещё пока не стали видными). Фиксация (commit) является финальным действием. После прерывания данная транзакция может быть попробована вновь.

Связность

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

Изоляция

Множество одновременно выполняемых транзакция обязано иметь возможность исполнения без взаимодействия, как если бы никакие иные транзакции больше не выполнялись в то же самое время.

Изоляция определяет когда вносимые в состояние этой базы данных изменения иогут стать видимыми и какие изменения могут становиться видимыми для имеющихся одновременными транзакций. Многие базы данных используют уровни изоляции, которые слабее чем приведённое определение изоляции по причинам производительности. В зависимости от тех методов и подходов, которые применяются для управления одновременностью, изменения могут быть видимыми или не видимыми прочим одновременным транзакциям (см. раздел Уровни изоляции).

Долговечность

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

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

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

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

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

Распределённые (множества разделов) транзакции требуют дополнительной координации и удалённого исполнения. Мы обсудим протоколы распределённых транзакций в Главе 13.

Управление буфером

Большинство баз данных строится с применением двухуровневой иерархии памяти:более медленного постоянного хранилища (диск) и более быстрой основной памяти (ОЗУ). Для снижения общего числа доступов к постоянному хранилищу страницы кэшируются в оперативной памяти. Когда необходимая страница запрашивается снова на уровне хранения, возвращается её кэшированная копия.

Доступные в памяти кэшированные страницы могут применяться повторно в том предположении, что никакие процессы больше не изменили её данные на диске. Этот подход иногда именуется как виртуальный диск [BAYER72]. Доступы по чтению виртуального диска к физическому хранилищу выполнятся только когда нет уже доступными этих страниц в оперативной памяти. Более распространённым названием для этого понятия являются кэширование страниц или буферный пул. Такой кэш страниц отвечает за кэширование чтений страниц с диска в оперативную память. В случае крушения некой базы данных или неверного её останова, кэшированное содержимое теряется.

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

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

При загрузке некэшированных страниц с диска они именуются подгружаемыми (paged in). После того как в кэшированную страницу вносятся какие- бы то ни было изменения, она именуется испорченной (dirty), пока эти изменения не сброшены (flushed) обратно на диск.

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

На Рисунке 5-1 вы можете видеть взаимоотношения между логическим представлением страниц B- дерева, их кэшированной версиями и самими страницами на диске. Имеющийся кэш страниц загружает страницы в свободные разъёмы не по порядку, поэтому нет прямого соответствия между тем как страницы упорядочены на диски и в оперативной памяти.

Рисунок 5-1

Кэш страниц

Самые первичные функции кэшированных страниц можно суммировать следующим образом:

Они удерживают кэшированные страницы в оперативной памяти.

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

Когда некая запрашиваемая страница отсутствует в оперативной памяти и достаточно пространства для её размещения, она подгружается своим кэшем страниц и возвращается её кэшированная версия.

Если запрошена уже кэшированная версия, возвращается её версия из кэша.

В случае когда для такой новой страницы недостаточно пространства, вытесняются и их содержимое сбрасывается на диск.

[Замечание]
Обход кэширования страниц ядром

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

Линус Торвальдс критиковал применение O_DIRECT по причине того что он не является асинхронным и не обладает упреждающим чтением и прочими средствами для инструктирования своего ядра относительно шаблонов доступа. Тем не менее, пока операционные системы не начнут предлагать лучшие механизмы, O_DIRECT всё ещё продолжит быть полезным.

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

Семантики кэширования

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

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

При выполнения доступа механизмом хранения (иными словами, при запросах) к определённой странице, мы вначале проверяем не кэшировано ли уже её содержимое и в этом случае возвращается содержимое кэша данной страницы. Если же содержимое такой страницы ещё не кэшировано, сам кэш транслирует полученный логический адрес страницы или номер страницы в её физический адрес, загружает её содержимое в оперативную память и возвращает её кэшированную версию в своё механизм хранения. После возврата, такой буфер с кэшированными страницами именуется как снабжённый ссылкой (referenced), а его механизм хранения обязан возвращать его обратно в такой кэш страниц или разименовывать (удалять ссылки) его по завершению. Данный кэш страниц может получить инструкции по избежанию вытеснения страниц закрепляя (pinning) их.

В случае изменения страниц (например, когда к ней в конец добавляется некий элемент), она помечается как испорченная. Установленный на определённой странице флаг испорченности (dirty) указывает что её содержимое не синхронизировано с диском и для обеспечения долговечности подлежит сбросу.

Вытеснение кэша

Удержание кэшей наполненными это хорошо: мы можем обслуживать больше чтений без проходов к постоянному хранилищу и больше записей в те же самые страницы могут быть буферированы совместно. Однако, имеющийся кэш страниц обладает ограниченной ёмкостью и, рано или поздно, для обслуживания нового содержимого старые страницы подлежат вытеснению. Когда содержимое синхронно со своим диском (то есть уже сброшено или никогда не подвергалось изменениям) и данная страница не закреплена и не снабжена ссылкой, она может быть прямо вытеснена. Испорченные страницы необходимо сбрасывать перед их вытеснением. Снабжённые ссылкой страницы нельзя вытеснять пока ими пользуется некий иной поток.

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

Другим важным свойством для отслеживания на глазу является долговечность: если происходит крушение базы данных, все не сброшенный данные будут утрачены. Чтобы убедиться что все изменения стали постоянными, сбросы координируются имеющимся процессом контрольных точек. Такой процесс контрольных точек управляет журналом упреждающей записи (WAL, write-ahead log) и кэшем страниц и обеспечивает гарантию того что они работают в строгом соответствии. Из WAL могут отвергаться только записи журнала, связанные с теми применёнными кэшированными страницами, которые были сброшены на диск. Испорченные страницы не могут вытесняться пока не завершится данный процесс.

Это означает, что всегда существует некий компромисс между некоторыми обстоятельствами:

Откладывание сброса для снижения общего числа доступа к диску

Упреждающий сброс страниц для того чтобы допускать быстрое вытеснение

Непкая действенная пометка страниц для вытеснения и сброса

Удержание размера кэша в рамках его границ в оперативной памяти

Избежание утраты данных по причине того что они не стали постоянными в своём первичном хранилище

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

Блокировка страниц в кэше

Необходимость выполнения дисковых вводов/ выводов для каждого чтения или записи непрактична: последовательные считывания могут требовать одну и ту же страницу, точно та же как последовательные записи могут менять одну и ту же самую страницу. Так как B- деревья становятся "более узкими" по мере подъёма вверх, узлы более верхнего уровня (те, которые ближе к самому корню) попадаются для большинства операций чтения. Расщепления и слияния также в конечном счёте распространяются до таких узлов верхнего уровня. Это означает, что всегда имеется по крайней мере часть дерева, которая может получать значительные преимущества от её кэширования.

Мы можем "блокировать" страницы, которые обладают высокой вероятностью быть задействованной в ближайшее время. Блокированные страницы в имеющемся кэше носят название закреплённых (pinning). Закреплённые страницы остаются в памяти более длительное время, что способствует снижению общего числа доступов к диску и улучшению производительности [GRAEFE11].

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

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

[Замечание]
Предварительная выборка и немедленное вытеснение

Кэширование страниц к тому же позволяет механизму хранения обладать тонкой грануляцией контроля над упреждающей выборкой и вытеснением. Например, при обходе терминальных узлов для возникающих случайным образом операций поиска, могут предварительно загружаться последующие листья. Аналогично, когда некий процесс сопровождения загружает определённую страницу, она может быть вытеснена непосредственно сразу после завершения данного процесса, так как маловероятно то она будет полезной для уже находящихся в полёте запросов. Некоторые базы данных, к примеру, PostgreSQL, применяют некий циклический буфер (иными словами, политику FIFO замещения страниц) для крупных последовательных сканирований.

Страничный обмен

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

Для этого мы можем сказать что страницы должны вытесняться согласно установленной политики вытеснения (также иногда носящей название политики страничного обмена - page-replacement). Она предпринимает попытку поиска страниц, к которым снова вскорости доступ будет менее вероятно. Когда такая страница вытесняется из имеющегося кэша, на её место может быть загружена необходимая новая страница.

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

Кажется логичным, что мы можем снизить общее число вытеснение просто применяя больший кэш. Тем не менее, это не так. Один из примеров, демонстрирующих эту дилемму носит название Аномалии Bélády [BEDALY69]. Он показывает, что увеличение общего числа страниц способно увеличивать общее число вытеснений когда применяемый алгоритм страничного обмена не оптимален. При запросе траниц сразу по вытеснению споследующей загрузкой вновь траницы начинают конкурировать за пространство в своём кэше. По этой причине нам следует с умом рассматривать применяемый нами алгоритм с тем, чтобы он бы улучшал текущую ситуацию, не превращая её в худшую.

FIFO и LRU

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

Естественным расширением алгоритма FIFO выступает дольше всех не использованная (LRU, least-recently used) [TANENBAUM14]. Он также поддерживает некую очередь кандидатов на вытеснение в порядке вставки, но позволяет вам помещать некую страницу обратно в самый хвост своей очереди при повторном доступе, как если бы это было самым первым обращением к данной странице. Однако, обновление ссылок и повторное подключение узлов при каждом обращении может становиться затратным при некой среде с одновременной обработкой.

Существуют и прочие основанные на LRU стратегии вытеснения из кэша. Например, 2Q (LRU с двумя очередями - Queue) поддерживает две очереди и помещает страницы в свою первую очередь в процессе первоначального доступа и перемещает её во вторую горячую очередь при последующих обращениях, позволяя вам отличать последние и имеющие частые обращения страницы [JONSON94]. LRU-K идентифицирует страницы с частыми ссылками отслеживая самые последние k обращений и применяя эти сведения для оценки времени доступа на основе страниц [ONEIL93].

CLOCK

при некоторых обстоятельствах эффективность может оказываться важнее чем частота. Часто в качестве компактных, дружественных к кэшированию и альтернативы с одновременным доступом для LRU часто используются вариации алгоритма CLOCK [DECANDIA07]. Например, Linux применяет некий вариант алгоритма CLOCK.

Очистка CLOCK удерживает ссылки на страницы и связанные с ними биты доступа в неком циклическом буфере. Некоторые варианты применяют счётчики вместо битов для учёта частот. Всякий раз при обращении к соответствующей странице её бит устанавливается в 1. Этот алгоритм работает посредством обхода циклического буфера, проверяя биты доступа:

Когда бит доступа равен 1 и страница разыменована, его значение устанавливается равным 0 и проверяется следующая страница.

Когда значение бита доступа уже равно 0, данная страница становится неким претендентом и заносится в расписание на вытеснение.

Если данная страница в данный момент снабжена ссылкой, её бит доступа остаётся без изменений. Предполагается, что значение бита некой страницы с обращением к ней не может быть равным 0, а потому она не может быть вытеснена. Это превращает снабжённые ссылками страницы в наименее вероятные для обмена.

Рисунок 5-2

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

Неким преимуществом применения циклического буфера является то, что и сама стрелка указателя часов, и содержимое способны изменяться операциями сравни- и- поменяй и не требуют дополнительных механизмов блокировки. Этот алгоритм прост в его осознании и реализации часто используется как в руководствах [TANENBAUM14], так и в реальных системах.

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

LFU

Для улучшения данного положения вещей мы можем начать отслеживать события ссылок на страницы вместо событий подкачки . Одним из позволяющих нам выполнять такое отслеживание подходов выступают наименее часто применяемые (LFU? least-frequently used) страницы.

TinyLFU, политика вытеснения страниц на основе частот [EINZIGER17], в точности это и делает: вместо вытеснения страниц на основании новизны подкачки, он упорядочивает страницы при помощи частоты использования. Он реализуется в популярной библиотеке Java с названием Caffeine.

Для поддержки компактной истории обращений к кэшу TinyLFU применяет гистограмму частот [CORMODE11], так как хранение всей истории целиком может быть запретно расточительным для практических целей.

Элементы могут пребывать в одной из трёх очередей:

Приёма (Admission), поддерживающей вновь добавляемые элементы, реализуемую с применением политики LRU.

Испытания (Probation), удерживающей элементы, скорее всего подлежащие вытеснению.

Защищённую (Protected), хранящую элементы, которые должны оставаться в данной очереди более продолжительное время.

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

Рисунок 5-3 отображает логические связи между очередями приёма, испытания и защищённой, установленным фильтром частот и вытеснением.

Рисунок 5-3

Очереди приёма, защиты и испытания TinyLFU

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

Восстановление

Системы баз данных строятся поверх различных аппаратных средств и уровней программного обеспечения, которые могут обладать своими собственными проблемами со стабильностью и надёжностью. Системы баз данных сами по себе, а также лежащие в основе программное обеспечение и компоненты оборудования, могут подвергаться отказам. Реализующим базу данных приходится рассматривать такие сценарии отказов и гарантировать что все данные, которые получили "обещание" быть записанными, на самом деле записаны.

Журнал упреждающей записи (WAL, write-ahead log, для краткости, также носящий название журнала фиксаций - commit log) является расположенной на диске вспомогательной структурой с записью только в конец, применяемой для восстановления после крушений и транзакций. Сами кэшированные страницы делают возможной буферизацию изменений содержимого страниц в оперативной памяти. Пока всё содержимое кэша не сброшено на диск, только расположенная на диске копия хранит всю историю действий в своём WAL. Многие базы данных применяют журналы упреждающей записи с исключительным добавлением в конец; например, PostgreSQL и MySQL.

Основные функциональные возможности журнала упреждающей записи можно суммировать следующим образом:

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

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

Делать возможным восстановление изменений в оперативной памяти из имеющегося журнала операций в случае некого крушения.

Дополнительно к этим функциональным возможностям, журналы упреждающей записи играют некую важную роль в обработке транзакций. трудно переоценить имеющиеся возможности такого WAL, ибо он гарантирует что данные превращает данные в постоянно хранимые и они доступны в случае некого крушения, поскольку данные без фиксации повторно воспроизводятся из такого журнала и полностью восстанавливают состояние базы данных до крушения. В данном разделе мы часто будем упоминать ARIES (Algorithm for Recovery and Isolation Exploiting Semantics, Алгоритм семантик эксплуатации восстановления и изоляции), некий современный алгоритм восстановления, который широко применяется и цитируется [MOHAN92].

[Замечание]
Сопоставление PostgreSQL и fsync()

PostgreSQL использует контрольные точки для гарантии того что файлы индексов и данных были обновлены всеми сведениями вплоть до определённой записи в его файле журнала. Этим процессом контрольной точки периодически осуществляется за раз сброс всех испорченных (изменённых) страниц. Синхронизация содержимого испорченных страниц с диском выполняется путём вызова ядра fsync(), который, как предполагается, синхронизует испорченные страницы с диском и снимет флаг dirty с этих страниц ядра. Как вы могли бы догадаться, fsync вернёт некую ошибку когда он не способен сбросить страницы на диск.

В Linux и некоторых прочих операционных системах fsync снимает флаг dirty даже с неудачно сброшенных страниц после ошибок ввода/ вывода. Кроме того, отчёты об ошибках выдаются лишь в те дескрипторы файлов, которые были открыты на момент отказа, а потому fsync не вернёт никаких ошибок, которые произошли до того, как этот дескриптор был вызван в результате открытия [CORBET18]..

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

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

Семантики журнала

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

Сам WAL состоит из регистрационных записей. Каждая запись обладает неким уникальным, монотонно увеличивающимся номером регистрационной последовательности (LSN, log sequence number). Обычно LSN представляется неким внутренним счётчиком или временной отметкой. Так как регистрационные записи не обязательно занимают блок диска целиком, их содержимое кэшируется в имеющемся буфере журнала и сбрасываются на диск некой принудительной (force) операцией. Такие операции принуждения происходят по мере заполнения имеющихся буферов журнала и могут запрашиваться имеющимся диспетчером транзакций или кэшем страниц. Все регистрационные записи обязаны быть сброшены на диск в порядке LSN.

Помимо записей индивидуальных операций, данный WAL хранит записи, указывающие завершение транзакций. Некая транзакция не может рассматриваться как фиксированная пока данный журнал не будет принуждён к фиксации его записей соответствующих LSN.

Для гарантии того что данная система может продолжать правильную работу после крушения в процессе отката или восстановления, некоторые системы применяют регистрационный записи компенсации (CLR, compensation log records) на протяжении возврата и сохраняют их в своём журнале.

Сам WAL обычно сцеплен с некой первичной структурой хранения особым интерфейсом, который позволяет отсекать (trimming) его всякий раз по достижению некой контрольной точки. Ведение журнала является одним из наиболее критически важной стороной корректности го базы данных, которую порой замысловато сделать верной: даже лёгкие несоответствия между отсечениями журнала и гарантией того, что все его данные выполнены в его первичной структуре хранения, могут вызвать утрату данных.

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

Сброс всего содержимого на диск достаточно непрактичен и потребует паузы для всех выполняемых действий пока не выполнится данная контрольная точка, по этой причине многие системы баз данных реализуют нечёткие контрольные точки (fuzzy checkpoints). В этом случае указатель last_checkpoint, хранимый в заголовке этого журнала, начинается с особой регистрационной записи begin_checkpoint и заканчивается регистрационной записью end_checkpoint, содержащими сведения относительно определённых испорченных страниц и самого содержимого некой таблицы транзакции. До тех пор, пока все имеющиеся страницы, определённые такой записью не сброшены, данная контрольная точка рассматривается как незавершённая. Страницы сбрасываются асинхронно и, когда он выполнен, запись last_checkpoint обновляется соответствующим LSN соответствующей записи begin_checkpoint и, в случае краха, процесс восстановления запустится отсюда [MOHAN92].

Сопоставление журналов действий и данных

Некоторые сисемы баз данных, например, System R [CHAMBERLIN81]? применяют теневые страницы: некую технологий копирования- записью (copy- on- write), гарантирующую долговечность данных и атомарность транзакции. Новое содержимое помещается в соответствующую новую неопубликованную теневую страницу и делается видимой переворачиванием некого указателя, со старой страницы на новую, которая хранит обновлённое содержимое.

Всякое изменение состояния может быть представлено образом-до и образом-после или соответствующими операциями выполнения повторно (redo), либо демонтажа (undo). применение некой операции повторного выполнения к образу-до производит некий образ-после. Аналогично, операция демонтажа для некого образа-после производит образ-до.

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

Физическое ведение журнала записывающее образы до- и после- требует регистрации целиком страниц, на которые оказывает воздействие данная операция. Некий применяющий к данной странице операции логический журнал должен определять, например, "insert a data record X for key Y", а для соответствующей операции демонтажа, к примеру, "remove the value associated with Y".

На практике многие системы баз данных используют некую комбинацию двух таких подходов, применяя логическую регистрацию для выполнения какого- то демонтажа (для одновременности и производительности) и физическую регистрацию для повторного выполнения (для улучшения времени восстановления) [MOHAN92].

Политики воровства и принуждения

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

Некий метод восстановления, который позволяет сбрасывать какую- то изменённую определённой транзакцией страницу даже до фиксации этой транзакции имеет название политики воровства (steal). Политика без воровства не допускает никакого сброса содержимого на диск никакой незафиксированной транзакции. Воровство некой испорченной страницы здесь означает сброс её содержимого из оперативной памяти на диск и загрузке на её место другой страницы с диска.

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

Политики воровства и принуждения важны для понимания, так как они подразумеваются для демонтажа и поторного выполнения транзакции. Демонтаж (undo) откатывает обратно обновления и принуждает страницы зафиксировать транзакции, в то время как повторное выполнение (redo) применяет изменения, выполненные транзакциями на диске.

Применение политик без воровства делает возможным реализовать восстановление применяя только записи повторного выполнения: старую копию, содержащуюся в соответствующей странице на диске и изменения, хранимые в имеющемся журнале [WEIKUM01]. Применяя политику без воровства мы потенциально можем буферировать некоторые обновления на страницы, задерживая их. Так как содержимое страницы должно на это время кэшироваться в памяти, может потребоваться больший кэш страниц.

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

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

ARIES

ARIES это некий алгоритм воровства/ без принуждения. Он применяет физическое повторное выполнение для улучшения производительности в процессе восстановления (так как изменения могут быть установлены быстрее) и логический демонтаж для улучшения одновременности в процессе обычных действий (поскольку операции логического демонтажа могут независимо применяться к страницам). Он пользуется записями WAL для реализации повторяемой истории в процессе восстановления для полного перестроения самого состояния базы данных до демонтажа не фиксированных транзакций и создаёт компенсационные регистрационные записи в процессе демонтажа [MOHAN92].

При перезапуске соответствующей системы базы данных после крушения восстановление проходит за три фазы:

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

Фаза повторного выполнения (redo) повторяет всю историю вверх до самого момента крушения и восстанавливает свою базу данных в её предыдущее состояние. Эта фаза выполняется для незавершённых транзакций, а также для тех, которые фиксированы, но чьё содержимое не было сброшено на устройство постоянного хранения.

Фаза демонтажа (undo) откатывает обратно все незавершённые транзакции и восстанавливает свою азу данных до последнего согласованного состояния. Все действия откатываются обратно в реверсивном хронологическом порядке. В случае крушения этой базы данных снова в процессе восстановления, операции демонтажа транзакции зарегистрированы также во избежание их повторения снова.

Для идентификации регистрационных записей ARIES пользуется LSN, отслеживая изменённые выполнением транзакций страницы в соответствующей таблице испорченных страниц и применяя физическое повторное выполнение, логический демонтаж и нечёткие контрольные точки. Даже несмотря на то, что описывающая такую систему статья была выпущена в 1992, большинство понятий, подходов и образцов всё ещё значимы и сегодня в обработке транзакций и восстановлении.

Управление одновременностью

Когда мы обсуждали архитектуру системы управления базой данных в Архитектура DBMS мы упоминали, что наши диспетчер транзакций и диспетчер блокировок работают совместно над обработкой контроля одновременности. Управление одновременностью это набор технологий для обработки взаимодействия между одновременно исполняемыми транзакциями. Эти технологии могут быть грубо разбиты на следующие категории:

Оптимистичный контроль одновременностью (OCC, Optimistic concurrency control)

Позволяет транзакциям выполнять одновременные операции чтения и записи и определять будет или нет получаемый в результате комбинации этого исполнения результат поддаваться упорядочению в последовательную форму (сериализации). Иными словами, транзакции не блокируют друг друга, сопровождают истории своих действий и проверяют эти истории на возможные конфликты перед фиксацией. Когда некий результат конфликтует, одна из конфликтующих транзакций прерывается.

Контроль одновременности множества версий (MVCC, Multiversion concurrency control)

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

Пессимистичный (также именуемый как консервативный) контроль одновременности (PCC)

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

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

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

Возможность преобразования в последовательную форму

Транзакции составляются из операций чтения и записи, выполняемых для определённого состояния базы данных и бизнес логики (преобразований, применяемых к самому считанному содержимому). Некое расписание является списком операций, требуемых для выполнения набора транзакций с точки зрения самой системы базы данных (то есть, только тех, которые взаимодействуют с состоянием этой базы данных, например, операций чтения, записи, фиксации и прерывания), так как все прочие операции предполагаются не имеющими побочных эффектов (иными словами, не воздействующими на значение состояния базы данных) [MOLINA08].

Некое расписание выполнено если имеет выполненными все операции для каждой исполняемой в нём транзакции. Правильные расписания являются эквивалентами для имеищегося изначально перечня операций, однако их части могут выполняться параллельно или переупорядочиваться с целями оптимизации, пока они не нарушают свойств ACID и собственно правильности получаемых результатов индивидуальных транзакций [WEIKUM01].

О расписании говорят как о последовательном (serial), когда транзакции в нём выполняются полностью независимо и пез перекрытий: все предыдущие транзакции полностью выполнены до того как начнётся следующая. О последовательном выполнении рассуждать просто, в отличии от всех возможных чередований между несколькими транзакциями из множества шагов. Однако постоянное выполнение транзакций одной за другой значительно бы ограничивало пропускную способность системы и травмировало бы производительность.

Нам требуется отыскать некий способ одновременного выполнения операций транзакций при поддержании правильности и простоты некого последовательного расписания. Мы можем достигать этого сериализуемыми (преобразуемыми в последовательную форму) расписаниями. расписание преобразуется в последовательную форму когда оно эквивалентно некому полностью последовательному расписанию для того же самого набора транзакций. Иными словами, оно производит тот же самый результат, как если бы выполнялся некий набор транзакций одна за другой в неком порядке. Рисунок 5-4 отображает три одновременные транзакции, а также возможные истории выполнения (3! = 6 возможностей, в любом возможном порядке).

Рисунок 5-4

Одновременные транзакции и их возможные истории последовательного выполнения

Изоляция транзакций

Системы баз данных с транзакциями допускают различные уровни изоляции транзакций. Некий уровень изоляции предписывает как и где части данной транзакции могут и должны становиться видимыми для прочих транзакций. Иначе говоря, уровни изоляции описывают ту степень, до которой транзакции изолированы от прочих одновременно выполняемых транзакций и какие аномалии могут встречаться в процессы выполнения.

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

Аномалии чтения и записи

Собственно стандартный SQL [MELTON06] выделяет и описывает аномалии чтения, которые могут происходить в процессе выполнения одновременных транзакций: испорченные, неповторяемые и фантомные чтения.

Некое испорченное чтение (dirty) это ситуация, при которой транзакция способна считать незафиксированные изменения другой транзакции. К примеру, транзакция T₁ обновляет некую запись пользователя новым значением в поле адреса, а транзакция T₂ считывает и изменяет адрес до фиксации T₁. Транзакция T₁ прерывается о откатывает обратно результаты своего выполнения. Тем не менее, T₁ уже имела возможность считать это значение, поэтому она получила доступ к значению, которое никогда не было фиксировано.

Некое неповторяемое чтение (иногда носящее название нечёткого - fuzzy - чтения) это ситуация, при которой транзакция запрашивает ту же самую строку дважды и получает различные результаты. Например, это может происходить когда транзакция T₁ считывает некую строку, затем транзакция T₂ изменяет её и фиксирует это изменение. Если T₁ запросит ту же самую строку снова до завершения своего выполнения, полученный результат будет отличаться от предыдущего.

Когда в процессе определённой транзакции мы применяем диапазон считываний (т.е. считываем не отдельную запись данных, а некий диапазон записей), мы можем наблюдать некие фантомные записи. Фантомное чтение происходит когда транзакция запрашивает тот же самый набор строк дважды и получает различные результаты. Это аналогично неповторяемому чтению, но происходит для диапазона запросов.

Такще существуют аномалии записи с аналогичной семантикой: утерянное обновление, испорченная запись и перекос записи.

Утерянное обновление происходит когда обе транзакции T₁ и T₂ пытаются обновить имеющееся значение V. T₁ и T₂ считывают установленное значение V. T₁ обновляет V и выполняет фиксацию, а T₂ обновляет V после этого и также делает фиксацию. Так как эти транзакции не осведомлены о наличии друг друга, когда им обеим разрешена фиксация, результаты T₁ будут переписаны результатами T₂ и будет утрачено обновление T₁.

Испорченная запись это некая ситуация, при которой одна транзакция получает некое незафиксированное значение (т.е. испорченное чтение), изменяет и сохраняет его. Иными словами, когда результаты транзакции основаны на значении, которое никогда не было фиксировано.

Перекос записи (write skew) происходит когда каждая транзакция по отдельности соблюдает необходимые инварианты, но их комбинация не удовлетворяет этим инвариантам. К примеру, транзакции T₁ и T₂ изменяют значения для двух счетов, A₁ и A₂. A₁ стартует со 100$, а A₂ начинается со 150$. Величина значения счёта может быть отрицательной до тех пор, пока сумма двух счетов не отрицательна: A₁ + A₂ >= 0. Каждая из T₁ и T₂ пытается удержать 200$ с A₁ и A₂, соответственно. Поскольку на момент времени старта этих транзакций A₁ + A₂ = 250, 250$ доступна в качестве итого. Обе транзакции предполагают что они сохраняют инварианты и им допустимы фиксации. После соответствующей фиксации A₁ имеет -100$, а A₂ обладает -50$. Что, совершенно ясно, нарушает основное требование удержания суммы счетов положительной [FEKETE04].

Уровни изоляции

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

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

Самым сильным уровнем изоляции является преобразуемый в последовательную форму (сериализуемый). Как мы уже обсуждали в разделе Возможность преобразования в последовательную форму, он гарантирует что результаты транзакций будут отображаться в некотором порядке, как если бы они выполнялись последовательно (т.е. без перекрытия по времени). Запрет одновременного исполнения имел бы реально имеющееся отрицательное воздействие на производительность такой базы данных Транзакции можно переупорядочить когда соблюдаются их внутренние инварианты и могут выполняться одновременно, но их отображения будут появляться неким последовательным образом.

Таблица 5-5 отображает уровни изоляции и допускаемые ими аномалии.

Таблица 5-5. Уровни изоляции и допускаемые ими аномалии

Испорченные
Dirty
Неповторяемые
Non- repeatable
Фантомные
Phantom

Чтение не фиксированных
Read Uncommited

Допускает

Допускает

Допускает

Чтение фиксированных
Read Commited

    -

Допускает

Допускает

Повторяемое чтение
Repeatable Read

    -

    -

Допускает

Сериализуемые
Serializable

    -

    -

    -

Не имеющие зависимостей транзакции могут выполняться в любом порядке так как их результаты полностью независимы. В отличии от линеаризуемости (которую мы обсудим в её контексте в распределённых системах; см. раздел Линеаризация), возможность преобразования в последовательность (сериализуемость) это свойство множества операций выполняться в произвольном порядке. Она не подразумевает и не пытается навязывать какой бы то ни было определённый порядок выполнения транзакций. Изоляция в терминах ACID означает сериализуемость [BAILIS14a]. К сожалению, реализация возможности преобразования в последовательность требует координации. Говоря иначе, выполняемые одновременно транзакции должны координироваться для сохранения инвариантов и накладывая некий последовательный порядок на конфликтующие выполнения [BAILIS14b].

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

Если две транзакции пытаются изменить одно и то же значение, фиксация допускается только для одной из них. Это предотвращает аномалию потери обновлений. Например, транзакции T₁ и T₂, обе, пытаются изменить V. Они считывают текущее значение V из того моментального снимка, который содержит изменения всех транзакций, фиксированных до их зпуска. Всякий раз, когда транзакция пытается выполнить фиксацию первой, она это сделает, а другая будет вынуждена прерваться. Такие отказавшие транзакции будут повторяться вместо переписывания имеющегося значения.

При изоляции моментального снимка может происходить аномалия Перекоса записи (write skew). так как если две транзакции выполняют чтение локального состояния, изменяют независимые записи и сохраняют локальные инварианты, им обеим дозволяется фиксация [FEKETE04]. более подробно Мы обсудим изоляцию моментальных снимков в их контексте распределённых транзакций в разделе Распределённые транзакции посредством Percolator.

Оптимистичное управление одновременностью

Оптимистический контроль одновременности предполагает что транзакции конфликтуют достаточно редко и, вместо использования блокировок и блокирующего исполнения транзакций, мы можем удостоверять транзакции на предмет предотвращения конфликтов чтения/ записи с одновременно выполняемыми транзакциями и обеспечивать возможность преобразования в последовательность до фиксации их результатов. Обычно исполнение транзакции расщепляется на три фазы [WEIKUM01]

Фаза чтения

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

Фаза проверки

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

Фаза записи

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

Подтверждение может быть осуществлено проверкой наличия конфликтов с самими транзакциями, которые уже были фиксированы (обратная ориентация), либо с теми транзакциями, которые в настоящий момент пребывают в фазе проверки (прямая ориентация). Фазы проверки и записи различных транзакций должны выполняться атомарно. Никаким транзакциям не допускается фиксация пока некие иные транзакции подвержены подтверждению. Поскольку фазы проверки и записи обычно короче чем первичная фаза чтения, это допустимый компромисс.

Контроль одновременности с обратной ориентацией гарантирует что для любой пары транзакций T₁ и T₂ сохраняются следующие свойства:

Когда T₁ зафиксирована до начала фазы чтения T₂, тогда и для T₂ допустима фиксация.

Если T₁ была фиксирована до соответствующей фазы записи T₂ и сам набор записи T₁ не пересекается с набором чтения T₂. Иными словами, T₁ не должна записывать никакие значения, которые должна видеть T₂.

Фаза чтения T₁ завершилась до соответствующей фазы чтения T₂ и имеющийся набор записи T₂ не пересекается с имеющимся набором записи T₁. Другими словами, транзакции работали на независимых наборах записей данных, а потому им обеим позволена фиксация.

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

Управление одновременностью множества версий

Контроль одновременности множества версий (MVCC) является неким способом достижения согласованности транзакций в системах управления базами данных за счёт допуска множества версий записи и применения монотонно увеличивающихся идентификаторов или временных отметок транзакций. Это позволяет чтениям и записям происходить при минимальной координации на уровне своего хранилища, так как чтения могут продолжать доступ к более старым значениям пока не фиксированы новые.

MVCC различает версии с фиксацией и без фиксации, которые относятся к значениям версий фиксированных и не фиксированных транзакций. Самая последняя фиксированная версия определённого значения предполагается текущей. Обычно, основная цель имеющегося диспетчера транзакций в этом случае заключается в обладании самым последним по времени не фиксированным значением.

В зависимости от реализованного рассматриваемой системой базы данных значения уровня изоляции, операциям чтения может разрешаться или нет доступ к значениям без фиксации [WEIKUM01]. Одновременность со множеством версий может реализовываться при помощи технологий блокировок, расписаний и разрешения конфликтов (например, двухфазного блокирования), либо упорядочения временных отметок. Одним из главных вариантов применения MVCC состоит в реализации изоляции моментальных снимков [HELLERSTEIN07].

Пессимистичное управление одновременностью

Схемы пессимистического контроля за одновременностью более консервативны нежели оптимистические. Эти схемы определяют конфликты транзакций в процессе их выполнения и блокируют или прерывают их выполнение.

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

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

Аналогично, операции записи с временной отметкой ниже нежели max_read_timestamp конфликтовали бы с более поздними чтениями. Однако, операции записи с меньшими временными отметками по отношению к max_write_timestamp допустимы, так как мы можем безопасно игнорировать устаревшие записанные значения. Такое предположение обычно носит название Правила записи Томаса [THOMAS79]. сразу по окончанию осуществления операций чтения или записи, обновляется соответствующее максимальное значение временной метки. Прерванные транзакции запускаются повторно с неким новым временным штампом, так как в противном случае они гаратнированно были бы снова прерваны [RAMAKRISHNAN03].

Управление одновременностью на основе блокировок

Схемы с контролем одновременности на основе блокировок являются неким видом пессимистичного управления одновременностью, который пользуется блокировками в явном виде для своих объектов базы данных вместо того чтобы разрешать расписания, как протоколы подобные временным меткам делают упорядочение. Некими обратными сторонами блокировок выступают проблемы состязательности и масштабирования [REN16].

Одной из наиболее распространённых технологий на основе блокировок является двух- фазная блокировка (2PL, two-phase locking), которая разделяет управление блокировкой на две фазы:

Фазу роста (также носящую название фазы расширения, в процессе которой запрашиваются все необходимые для определённой транзакции блокировки и никакие блокировки не высвобождаются.

Фаза сжатия, на протяжении которой все полученные на фазе роста блокировки высвобождаются.

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

[Предостережение]
Предостережение

Несмотря на схожесть в названиях, двух- фазная блокировка это понятие, которое совершенно отлично от двух- фазной фиксации (см. раздел Двухфазная фиксация). Двухфазная фиксация это протокол, применяемый для распределённых по множеству разделов транзакций, в то время как двух- фазная блокировка это механизм управления одновременностью, зачастую используемый для реализации возможности преобразования в последовательную форму.

Взаимные блокировки

В протоколах блокирования транзакции пытаются заполучить блокировки над необходимыми объектами своей базы данных и, когда блокировка не может быть немедленно предоставлена, транзакции придётся дожидаться пока эта блокировка не высвободится. Может сложиться ситуация, при которой две транзакции в попытке достичь необходимых им для продолжения исполнения блокировок в конечном итоге окажутся дожидающимися друг друга на высвобождение прочих удерживаемых ими блокировок. Такая ситуация носит название взаимной блокировки (deadlock).

Рисунок 5-6 отображает некий пример взаимной блокировки: T₁ удерживает блокировку L₁ и ожидает высвобождения блокировки L₂, в то время как T₂ удерживает блокировку L₂ и ожидает высвобождения L₁..

Рисунок 5-6

Образец взаимной блокировки

Простейшим способом обработки взаимной блокировки является введение таймаутов и прерывание длительно выполняющихся транзакций в предположении что они могут быть взаимно блокированными. Иная стратегия, консервативное 2PL, требует получение всех необходимых блокировок до того как они могут быть выполнить какие бы то ни было свои действия и прерывания когда они не способны их получить. Однако, такие подходы значительно ограничивают одновременность систем и системы баз данных в основном пользуются диспетчером транзакций для выявления или избежания (иными словами, предотвращения) взаимного блокирования.

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

Циклы в данном графе указывают наличие некой взаимной блокировки: транзакция T₁ дожидается T₂, которая в свою очередь ожидает T₁. Выявление взаимных блокировок может выполняться периодически (один раз в заданном временном интервале) или непрерывно (при каждом обновлении графа ожиданий) [WEIKUM01]. Одна из транзакций (как правило, та, которая пытается заполучить свою блокировку наиболее последней) прерывается.

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

Если транзакция T₁ пытается заполучить блокировку, в настоящее время удерживаемую T₂ и T₁ обладает более высоким приоритетом (она началась раньше T₂), для избежания взаимного блокирования мы можем воспользоваться одним из следующих ограничений:

Дожидаться смерти

T₁ получает разрешение выполнить блокировку и ждать эту блокировку. В противном случае T₁ прерывается и перезапускается. Иначе говоря, некая транзакция может быть блокирована лишь транзакцией с более поздней временной меткой.

Ожидание ранения

T₂ прерывается и перезапускается ( T₁ ранит T₂). В противном случае (когда T₂ была запущена до T₁), разрешается ожидание для T₁. Иными словами, некая транзакция может быть блокирована только транзакцией с меньшим временным штампом.

Обработка транзакций требует для обработки взаимных блокировок некого расписания. В то же самое время, защёлки (см. раздел Защёлки) полагаются на самого программиста, который обеспечит невозможность возникновения взаимного блокирования и не полагаются на механизмы избежания взаимных блокировок.

Блокировки

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

При обработке транзакций имеется отличие между тем механизмом, который ограждает имеющуюся логическую и физическую целостность. Те два понятия, которые отвечают за логической и физической целостностью это, соответственно, блокировки (locks) и защёлки (latches). Эти названия являются несколько неудачными, так как то что носит название защёлки здесь обычно относится к некоторой блокировке в системе программирования, однако мы проясним это отличие и его последствия в данном разделе.

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

Блокировки обладают большим весом нежели защёлки и удерживаются на протяжении всей транзакции.

Защёлки

С другой стороны, защёлки ограждают имеющееся физическое представление: содержимого терминальной страницы, изменяемого в процессе операции вставки, обновления и удаления. Изменяемое в процессе операций содержимое не терминальной страницы и некая структура дерева имеют результатом расщепления и слияния, которые распространяются от незаполненных и переполненных листьев. Защёлки ограждают имеющееся физическое представление дерева (содержимое страницы и саму структуру дерева) в процессе таких операций и достигается на уровне самой страницы. Любая страница может быть защёлкнута чтобы допускать одновременный доступ к ней. Технологии управления одновременностью без блокировок всё ещё обязаны пользоваться защёлками.

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

Те же самые правила применяются к обновлениям указателей родителей или братьев. Общее правило заключается в том, чтобы удерживать защёлки наименее продолжительное время - а именно, когда данная страница считывается или обновляется - для увеличения одновременности.

Взаимодействия между одновременными операциями грубо можно сгруппировать в три категории:

Одновременные чтения, когда несколько потоков пытаются получить доступ к одной и той же странице без её изменения.

Одновременные обновления, при которых несколько потоков пытаются выполнить изменения в одной и той же странице.

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

Эти сценарии также применимы для доступа, который перекрывается сопровождением базы данных (таком как фоновые процессы, описанные в разделе Уборка и сопровождение)

Блокировка читатели- писатель

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

Некая блокировка RW делает возможным одновременный доступ множества читателей и лишь писатели (которых обычно меньше) должны получать исключительный доступ к своему объекту. Таблица 5-7 отображает таблицу совместимости блокировок читатели- писатель: только читатели способны совместно разделять обладание блокировкой, в то время как все прочие сочетания читателей и писателей обязаны получать исключительное обладание.

Таблица 5-7. Таблица совместимости блокировки читатели- писатель

Читатель
Писатель

Читатель

Разделяют

Исключительно

Писатель

Исключительно

Исключительно

На Рисунке 5-8 a) у нас имеется множество читателей с доступом к определённому объекту, в то время как писатель в ожидает своей очереди, так как он не может изменять данную страницу пока читатели обладают к ней доступом. На Рисунке 5-8 b) writer 1 удерживает исключительную блокировку к своему объекту, в то время как другой писатель и три читателя вынуждены ожидать.

Рисунок 5-8

Блокировка читатели- писатель

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

[Замечание]
Технологии длительного ожидания и очередей

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

Очереди обычно реализуются при помощи инструкций сравнения- и- перестановки, применяемых для выполнения действий, гарантированно атомарно блокирующих получение и обновление очереди. Когда данная очередь пуста, запрашивающий поток получает доступ немедленно. В противном случае этот поток сам ставится в конец для ожидания своей очереди и крутится в значении переменной, которая может обновиться лишь потоком, который предшествует ему в данной очереди. Это помогает снизить объём обмена ЦПУ для получения и высвобождения блокировки [MELLORCRUMMEY91].

Сцепляющая защёлка

Самый простой подход для получения защёлки состоит в захвате по пути от корня к своему целевому листу всех присутствующих защёлок. Это создаёт некое узкое место в одновременности и в большинстве случаев его можно избежать. значение времени, на протяжении которого некая защёлка удерживается, должно быть минимизировано. Одной из таких возможных в достижении оптимизаций носит название сцепляющей защёлки (latch crabbing, или соединительной защёлки) [RAMAKRISHNAN03].

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

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

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

Рисунок 5-9 показывает проход от корня к листу в процессе вставки:

Защёлка записи запрашивается на уровне корня.

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

Данная операция спускается вниз к своему следующему уровню. Запрашивается защёлка записи, имеющийся лист назначения проверяется на потенциальные структурные изменения и её родительская защёлка высвобождается.

Рисунок 5-9

Сцепляющая защёлка в процессе вставки

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

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

[Замечание]
Обновление защёлки и охота за указателем

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

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

Вы можете заметить, что все сами описанные до сих пор механизмы приобретали некую защёлку в узле имеющегося корня. Все запросы обязаны проходить через узел своего корня, так как вначале должны заполниться все его потомки. Это означает, что сам узел корня может всегда защёлкиваться оптимистично необходимая стоимость некого повтора (погоня за указателем) платится редко.

Деревья Blink

Деревья Blink строятся поверх B*- деревьев (см. раздел Повторная балансировка) и добавляют верхние ключи, (см. раздел Верхние ключи узла), а также указатели ссылок одного уровня [LEHMAN81]. Некий верхний ключ указывает на значение самого верхнего возможного ключа в поддереве. Все узлы за исключением корневого в неком Blink- дереве обладают двумя указателями: неким указателем потомка, исходящим от его родителя и некий братский указатель от имеющегося слева узла, расположенного на том же самом уровне.

Blink допускают некое состояние с названием половинного расщепления (half-split),при котором данный узел уже обладает ссылкой братского указателя, но не дочерней ссылкой своего предка. Половинное расщепление идентифицируется значением верхнего ключа. Когда значение искомого ключа превосходит значение верхнего ключа в данном узле (что нарушает значение инварианта верхнего ключа), наш алгоритм поиска заключает, что данная структуры одновременно изменяется и для продолжения поиска следует по имеющейся братской ссылке.

Значение указателя должно быстро добавляться к его предку, обеспечивая наилучшую производительность, однако сам процесс поиска не должен прерываться или перезапускаться, так как в этом дереве доступны все элементы. Основное преимущество при этом состоит в том, что нам не приходится удерживать блокировку своего предка при сходе к необходимому дочернему уровню, причём даже когда этот потомок намерен расщепляться: мы можем делать новый узел видимым через его братскую ссылку и лениво обновлять указатель его предка не жертвуя правильностью [GRAEFE10].

Хотя это слегка и менее действенно чем прямое нисхождение от имеющегося предка и требует доступа к некой дополнительной странице, это в результате приводит к верному спуску от- корня- к- листу при упрощении одновременного доступа. Поскольку расщепления относительно редкая операция и B- деревья не часто сжимаются, этот вариант исключительный и его стоимость незначительна. Такой подход обладает достаточным числом преимуществ: он снижает конкуренцию, предотвращая удержание блокировки предка в процессе расщеплений и снижает общее число блокировок, удерживаемых в процессе изменения структуры дерева неким постоянным числом. Что ещё более важно, это делает возможным одновременное чтение структурных изменений дерева и предотвращает взаимные блокировки, которые в противном случае приходили бы в результате одновременных изменений, восходящих к соответствующим родительским узлам.

Выводы

В этой главе мы обсудили те компоненты механизма хранения, которые ответственны за обработку транзакций и восстановление. При реализации обработки транзакций нам были представлены две задачи:

Для улучшения эффективности нам необходимо допускать одновременное выполнение транзакций.

Для сохранения правильности нам требуется гарантировать что одновременно выполняющиеся транзакции сохраняют свойства ACID.

Одновременное выполнение транзакций может вызывать различные виды аномалий чтения и записи. Присутствие или отсутствие таких аномалий определяется и ограничивается реализацией различных уровней изоляции. Подходы к контролю одновременности определяют как планируются и выполняются транзакции.

Кэширование страниц отвечает за снижение общего числа доступов к диску: оно кэширует страницы в оперативной памяти и позволяет выполнение из них операций чтения и записи. Когда этот кэш достигает своей ёмкости, страницы вытесняются и сбрасываются обратно на диск. Чтобы быть уверенным что не сброшенные изменения не утратятся в случае крушений узла, а также для поддержки отката транзакции, мы пользуемся журналами упреждающей записи. Имеющиеся кэш страниц и журналы упреждающей записи координируются при помощи политик принуждения и воровства, обеспечивая эффективное исполнение всех транзакций и обратного отката без ущерба для долговечности.

[Замечание]
Последующее чтение

Если вы желаете узнать больше относительно упоминавшихся в этой главе понятий, вы можете ссылаться на следующие источники:

Обработка транзакций и восстановления в целом

Weikum, Gerhard, and Gottfried Vossen. 2001. Transactional Information Systems: Teory, Algorithms, and the Practice of Concurrency Control and Recovery. San Francisco: Morgan Kaufmann Publishers Inc. {Прим. пер.: https://www.elsevier.com/books/transactional-information-systems/weikum/978-1-55860-508-4 } .

Bernstein, Philip A. and Eric Newcomer. 2009. Principles of Transaction Processing.. San Francisco: Morgan Kaufmann. {Прим. пер.: https://www.elsevier.com/books/principles-of-transaction-processing/bernstein/978-1-55860-623-4 } .

Graefe, Goetz, Guy, Wey & Sauer, Caetano. 2016. “Instant Recovery with WriteAhead Logging: Page Repair, System Restart, Media Restore, and System Failover, (2nd Ed.)” in Synthesis Lectures on Data Management 8, 1-113. https://doi.org/10.2200/S00710ED2V01Y201603DTM044.

Mohan, C., Don Haderle, Bruce Lindsay, Hamid Pirahesh, and Peter Schwarz. 1992. “ARIES: a transaction recovery method supporting fine-granularity locking and partial rollbacks using write-ahead logging.” in Transactions on Database Systems 17, no. 1 (March): 94-162. https://doi.org/10.1145/128765.128770.

Контроль одновременности в B- деревьях

Wang, Paul. 1991. “An In-Depth Analysis of Concurrent B-Tree Algorithms.” MIT Technical Report. https://apps.dtic.mil/dtic/tr/fulltext/u2/a232287.pdf .

Goetz Graefe. 2010. A survey of B-tree locking techniques. ACM Trans. Database Syst. 35, 3, Article 16 (July 2010), 26 pages. {Прим. пер.: https://15721.courses.cs.cmu.edu/spring2016/papers/a16-graefe.pdf } .

Параллельные и одновременные структуры данных

McKenney, Paul E. 2012. “Is Parallel Programming Hard, And, If So,What Can You Do About It?” https://arxiv.org/abs/1701.00854 .

Herlihy, Maurice and Nir Shavit. 2012. Te Art of Multiprocessor Programming, Revised Reprint (1st Ed.). San Francisco: Morgan Kaufmann. {Прим. пер.: https://www.elsevier.com/books/the-art-of-multiprocessor-programming-revised-reprint/herlihy/978-0-12-397337-5 } .

Хронологические разработки в самих полях обработки транзакций

Diaconu, Cristian, Craig Freedman, Erik Ismert, Per-Åke Larson, Pravin Mittal, Ryan Stonecipher, Nitin Verma, and Mike Zwilling. 2013. “Hekaton: SQL Server’s Memory-Optimized OLTP Engine.” in Proceedings of the 2013 ACM SIGMOD International Conference on Management of Data (SIGMOD ’13), 1243-1254. New York: Association for Computing Machinery. https://doi.org/10.1145/2463676.2463710 .

Kimura, Hideaki. 2015. “FOEDUS: OLTP Engine for a Thousand Cores and NVRAM.” in Proceedings of the 2015 ACM SIGMOD International Conference on Management of Data (SIGMOD ’15), 691-706. https://doi.org/10.1145/2723372.2746480 .

Yu, Xiangyao, Andrew Pavlo, Daniel Sanchez, and Srinivas Devadas. 2016. “Tic-Toc: Time Traveling Optimistic Concurrency Control.” in Proceedings of the 2016 International Conference on Management of Data (SIGMOD ’16), 1629-1642. https://doi.org/10.1145/2882903.2882935 .

Kim, Kangnyeon, Tianzheng Wang, Ryan Johnson, and Ippokratis Pandis. 2016. “ERMIA: Fast Memory-Optimized Database System for Heterogeneous Workloads.” in Proceedings of the 2016 International Conference on Management of Data (SIGMOD ’16), 1675-1687. https://doi.org/10.1145/2882903.2882905 .

Lim, Hyeontaek, Michael Kaminsky, and David G. Andersen. 2017. “Cicada: Dependably Fast Multi-Core In-Memory Transactions.” in Proceedings of the 2017 ACM International Conference on Management of Data (SIGMOD ’17), 21-35. https://doi.org/10.1145/3035918.3064015 .

Глава 13. Распределённые транзакции

Содержание

Глава 13. Распределённые транзакции

Превращение действий в проявляющиеся атомарными

Двухфазная фиксация

Отказы Пособников в P2C

Отказы координатора в P2C

Трёхфазная фиксация

Координатор отказов в P3C

Распределённые транзакции с применением Calvin

Распределённые транзакции при помощи Spanner

Разбиение баз данных на разделы

Согласованное хэширование

Распределённые транзакции посредством Percolator

Избежание координации

Выводы

Для сопровождения порядка в некой распределённой системе нам приходится обеспечивать по крайней мере некую согласованность. В разделе Модели согласованности мы обсуждали модели согласованности единственного объекта единственной операции, которая помогла нам порассуждать относительно таких индивидуальных операций. Однако в базах данных нам часто требуется исполнять множество операций атомарно.

Атомарные операции объяснялись в терминах переходов состояний: до того как стартовала некая транзакция наша база данных пребывала в состоянии A; к моменту её завершения это состояние перешло из A в B. В терминах операции это просто понять, так как транзакции не имеют никакого предварительно определённого прикреплённого состояния. Вместо этого они применяют операции к записям данных, запускаясь в некоторый момент времени. Это предоставляет нам некую гибкость в терминах составления расписания и исполнения: транзакции могут менять порядок и даже выполняться повторно.

Основная сосредоточенность обработки транзакций состоит в определении допустимых историй, ля моделирования и представления возможных сценариев перекрытия исполнения. История в данном случае представляет некий граф зависимостей: какие из транзакций были выполнены прежде чем исполняется текущая транзакция. Об истории мы говорим как о подлежащей сериализуемости, когда она эквивалентна (то есть имеет тот же самый граф зависимости) некоторой истории, которая выполняет эти транзакции последовательно. Вы можете вернуться к понятиям историй, их эквивалентности, сериализуемости и прочим концепциям в разделе Возможность преобразования в последовательную форму. В целом, данная глава дублирует для распределённых систем Главу 5, в которой мы обсуждали обработку транзакций локально в узле.

Транзакции единственного раздела вовлекают пессимистические (основанные на блокировке или отслеживании) или оптимистические (попытка и валидация) схемы контроля одновременностью, которые обсуждались в Главе 5, однако ни один из этих подходов не решает основную проблему транзакций со множеством разделов, которые требуют координации между различными серверами, распределённые фиксации и протоколы отката.

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

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

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

Другой важной стороной являются сетевые разделы и отказы узлов: узлы в такой системе отказывают и восстанавливаются независимо, однако их состояния должны оставаться согласованными. Это означает, что такое требование атомарности сохраняется не только для своих локальных операций, но и для исполняемых в прочих узлах операций: изменения должны распространяться на долговременной основе во все те узлы, которые вовлечены в данную транзакцию, либо ни в один из них [LAMPSON79].

Превращение действий в проявляющиеся атомарными

Чтобы заставить множество операций представляться как атомарные, в особенности когда часть из них являются удалёнными, нам требуется применять некий класс алгоритмов, носящих название фиксируемых атомарно. Атомарная фиксация не допускает рассогласования между своими участниками: некая транзакция не будет зафиксирована когда хотя бы один из участников возражает против неё. В то же самое время это означает, что отказавшие процессы должны достичь того же самого согласия, что и остальные из этой когорты компаньонов. Другим важным заключением из этого факта является то, что алгоритмы атомарной фиксации не работают в присутствии Византийских отказов: когда данный процесс лжёт относительно своего состояния или решений по произвольным значениям, так как это вступает в противоречие с единодушием [HADZILACOS05].

Та основная задача, которую пытается решать атомарность состоит в достижения согласия по тому стоит или нет выполнять предложенную транзакцию. Компаньоны не могут выбираться, подвергаться воздействию или изменять саму предполагаемую транзакцию либо предлагать какую- то альтернативу: они лишь могут либо за, либо против того желают ли они её исполнить [ROBINSON08].

Алгоритмы атомарной фиксации не устанавливают строгих требований на саму семантику или операции подготовки, фиксации или отката транзакции. Разрабатывающие базу данных должны принимать решение по:

Тому когда эти данные рассматриваются готовыми к фиксации и остаётся лишь переключить ссылку чтобы сделать эти изменения общедоступными.

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

Как откатить обратно все сделанные этой транзакцией изменения если данный алгоритм принял решение не выполнять фиксацию.

Мы уже обсуждали в Главе 5 эти процессы для реализаций локального узла.

Многие распределённые системы применяют алгоритмы атомарной фиксации - например, MySQL (для распределённых транзакций) и Kafka (для производителей и потребителей итераций) [MEHTA17].

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

Мы обсудим два алгоритма атомарной фиксации: двухфазную фиксацию, которая решает задачу фиксации, но не допустима при отказах самого координирующего процесса; а также трёхфазную фиксацию [SKEEN83], которая решает проблему неблокируемой атомарной фиксации (отличная статья отмечает что в "предположении высоконадёжной сетевой среды", иными словами, в некой сетевой среде, предотвращающей разбиение на разделы [ALHOUMAILY10]; следствия данного предположения были обсуждены в разделе этой статьи по описанию алгоритма), и позволяет участникам выполнение даже в случае отказа координатора [BABAOGLU93].

Двухфазная фиксация

Давайте начнём с наиболее прямолинейного протокола для распределённой фиксации, который допускает атомарные обновления множества разделов. (Для дополнительных сведений по разделам вы можете обратиться к секции Разбиение баз данных на разделы)/ Двухфазная фиксация (2PC, Two-phase commit) обычно обсуждается в контексте транзакций базы данных. 2PC выполняется в две фазы. В прроцессе своей первой фазы распространяется определённое значение и собираются голоса. В процессе второй фазы просто перекидывают свой переключатель, делая полученные в первой фазе результаты видимыми.

2PC предполагает наличие некого лидера (или координатора), который удерживает значение состояния, собирает голоса и выступает первичной точкой ссылки для раунда принятия согласия. Остальные узлы именуются когортой (Пособниками). Пособники, в данном случае, это обычно разделы, которые работают с разъединёнными наборами данных для которых и выполняются транзакции. Имеющийся координатор и все Пособники ведут журналы локальных операций для каждого исполняемого шага. Участники голосуют принимать или отклонять некоторое значение, собираемые установленным координатором. Наиболее часто это значение некоторый идентификатор данной подлежащей исполнению транзакции, однако 2PC может использоваться и в иных контекстах.

Установленный координатор может быть неким узлом, который принимает какой- то запрос на исполнение определённой транзакции или он может указываться случайным образом, при помощи алгоритма выбора лидера, назначаться вручную или даже фиксироваться на всё время жизни данной системы. Сам протокол не помещает ограничения на роль своего координатора и данная роль может передаваться другому участнику для надёжности или производительности.

Как и предполагает её название, двухфазная фиксация осуществляется за два шага:

Подготовка

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

Фиксация/ прерывание

Операции внутри некой транзакции могут изменять состояние в различных разделах (каждое представляется неким Пособником). Если хотя бы один из Пособников голосует за прерывание данной транзакции, их координатор отправляет им всем сообщение Abort. Только когда все Пособники дали положительные голоса, их координатор рассылает им завершающее сообщение Commit.

Этот процесс показан на Рисунке 13-1.

Рисунок 13-1

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

На протяжении фазы подготовки установленный координатор распространяет предлагаемое значение и собирает голоса от участников относительно того будет или нет данное предлагаемое значение фиксировано. Пособники могут, например, выбирать отклонение предложения когда иная конфликтующая транзакция уже фиксировала некое иное значение.

После того как установленный координатор собрал все голоса, он может принять решение о фиксации этой транзакции или прервать её. Когда все Пособники проголосовали положительно, он принимает решение о фиксации и уведомляет их отправкой сообщения Commit. В противном случае этот координатор отправляет всем Пособникам сообщение Abort и данная транзакция откатывает обратно. Иными словами, когда данное предложение отклоняет один узел, прерывается весь раунд.

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

В контексте систем баз данных всякий раунд 2PC обычно отвечает за некую отдельную транзакцию. На протяжении своей фазы подготовки содержимое транзакции (операции, идентификаторы и прочие метаданные) передаются от установленного координатора имеющимся Пособникам. Сама транзакция выполняется Пособниками локально и остаётся в состоянии частичной фиксации (иногда именуемой предварительной фиксацией), приводя их в готовность для установленного координатора финализировать исполнение в процессе следующей фазы либо фиксируя, либо прерывая её. К моменту фиксации данной транзакции, её содержимое уже сохранено на постоянной основе во всех прочих узлах [BERNSTEIN09].

Отказы Пособников в P2C

Давайте рассмотрим различные сценарии отказов. Например, как отображено на Рисунке 13-2, когда один из Пособников отказывает на фазе предложения, установленный координатор не способен продолжать некую фиксацию, так как ему необходимы все голоса положительными. Когда один из Пособников недоступен, наш координатор прервёт данную транзакцию. Это требование имеет негативное воздействие на доступность: отказ отдельного узла может препятсвовать прохождению транзакции. Некоторые системы, например Spanner (см. раздел Распределённые транзакции при помощи Spanner), выполняет 2PC для групп Paxos вместо того чтобы делать это для индивидуальных узлов для улучшения доступности протокола.

Рисунок 13-2

Отказ Пособника в процессе фазы предложения

Основная стоящая за 2PC идея состоит в обещании от Пособников, что раз уж они откликнулись положительно на данное предложение, они не отзовут обратно своё решение, а потому лишь координатор способен прервать данную транзакцию.

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

Поскольку этот протокол обладает множеством пятен в которых процессы дожидаются прочих участников (пока установленный координатор собирает голоса, или когда Пособники ожидают соответствующей фазы фиксации/ прерывания), отказы соединений способны приводить к потере сообщений и такое ожидание будет происходить неопределённо долго. Когда установленный координатор не получает отклика от соответствующей реплики в процессе фазы предложения, он может включить некий таймаут и прервать данную транзакцию.

Отказы координатора в P2C

Когда один из Пособников не получает команды фиксации или прерывания от своего координатора на протяжении установленной второй фазы, как это показывается на Рисунке 13-3, он обязан попытаться определить какое именно решение было сделано его координатором. Его координатор мог принять решение по данному значению, но не имел возможности выполнить взаимодействие со своей определённой репликой. В подобных случаях, сведения о принятом решении могут реплицироваться из соответствующих журналов транзакций одноранговых партнёров или из имеющейся резервной копии координатора. Реплицируемые решения по фиксации безопасны, ибо они всегда единодушны: вся точка целиком в 2PC либо фиксирована либо прервана на всех площадках, а фиксация на одном из Пособников подразумевает что все прочие Пособники также должны иметь фиксацию.

Рисунок 13-3

Отказ координатора после фазы предложения

На протяжении своей первой фазы установленный координатор собирает голоса и, впоследствии, обещания от Пособников, что они дождутся его команды фиксации или прерывания в явном виде. Если установленный координатор падает после сбора голосов, но до широковещательного оповещения о результатах голосования, его Пособники приходят в конце концов в некое состояние неопределённости. Это отражено на Рисунке 13-4. Пособники не знают что в точности решил их координатор и были или нет ккие- то участники (потенциально также недоступные) уведомлены о результатах данной транзакции [BERNSTEIN87].

Рисунок 13-4

Отказ координатора прежде чем он способен выполнить контакт со своими Пособниками

Неспособность установленного координатора продолжать фиксацию или прерывание оставляет данный кластер в неком неопределённом состоянии. Это означает, что Пособники не будут иметь возможности изучить окончательное решение в случае непрерывного отказа своего координатора. По причине данного свойства мы говорим, что 2PC является блокирующим алгоритмом атомарной фиксации.Если установленный координатор никогда не восстановится, его заместителю придётся собирать голоса для данной транзакции заново и продолжить принятие окончательного решения.

Многие базы данных применяют 2PC: MySQL, PostgreSQL, MongoDB (однако документация сообщает, что относительно v3.6, 2PC предоставляет лишь подобную транзакции семантику) и прочие. Двухфазная фиксация часто используется для реализации распределённых транзакций по причине своей простоты (о ней просто рассуждать, реализовывать и отлаживать) и низким накладным расходам (сложность сообщения и общее число проходов туда- обратно на низком уровне). Важно реализовывать надлежащим образом механизмы восстановления и иметь резервные копии узлов координаторов для снижения только что описанных шансов на отказ.

Трёхфазная фиксация

Для того чтобы сделать некий протокол атомарной фиксации надёжным в отношении отказов координаторов и избегать неопределённых состояний, протокол трёхфазной фиксации (3PC, three-phase commit) добавляет некий дополнительный шаг и таймауты с обеих сторон, которые способны позволить Пособникам продолжать либо фиксацией либо прерыванием в случае отказа координатора, в зависимости от значения состояния системы. 3PC предполагает некую синхронную модель и что отказы взаимодействия невозможны [BABAOGLU93].

3PC добавляет некую фазу подготовки перед шагом фиксации/ прерывания, которая взаимодействует с состояниями Пособников, собранными установленным координатором на протяжении фазы предложения, позволяя данному протоколу выполнять передачу даже при отказе координатора. Все прочие свойства 3PC и требование обладания координатором для соответсвующего раунда аналогичны его двухфазному собрату. Другим полезным добавлением в 3PC являются таймауты на стороне Пособников. В зависимости от того на каком шагу выполняется в настоящий момент данный процесс, по таймауту форсируется либо решение о фиксации, либо о прерывании.

Как отображено на Рисунке 13-5, раунд трёхфазной фиксации состоит из трёх этапов:

Предложение

Установленный координатор отправляет некое предлагаемое значение и собирает имеющиеся голоса.

Подготовка

Установленный координатор уведомляет Пособников относительно полученных результатов голосования. Если все голоса прошли и все Пособники приняли решение о фиксации, наш координатор отправляет сообщение Prepare, выдавая им инструкцию о подготовке к фиксации. В противном случае отправляется сообщение Abort и данный раунд завершается.

Фиксация

Пособники уведомляются своим координатором о фиксации данной транзакции.

Рисунок 13-5

Трёхфазная фиксация

На протяжении шага подготовки, аналогично 2PC, установленный координатор распространяет своё предлагаемое значение и собирает голоса от Пособников, как это показано на Рисунке 13-5. Если наш координатор испытывает на данной фазе крушение и все операции выходят по таймауту, или если один из имеющихся Пособников голосует отрицательно, данная транзакция будет прервана.

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

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

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

Координатор отказов в P3C

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

Как мы уже обсуждали ранее, 2PC не способен восстанавливать отказы координатора и Пособники могут застрять в состоянии неопределённости до тех пор пока в строй не вернётся их координатор. 3PC избегает блокирования процессов в подобном случае и позволяет Пособникам продолжать с неким детерминированным решением.

Наихудшим из сценариев для 3PC является некое разбиение на сетевые разделы, отображаемое на Рисунке 13-6. Некоторые узлы успешно перешли в состояние подготовленных и теперь способны продолжать фиксацию по завершению таймаута. Часть же не может взаимодействовать со своим координатором и они будут прерваны после своего таймаута. В результате это приводит к расщеплению сознания: некоторые узлы продолжат фиксацией, а какие- то будут прерваны, причём всё в соответствии с установленным протоколом, оставляя участников в неком несогласованном и противоречивом состоянии.

Рисунок 13-6

Отказ координатора в процессе второй фазы

Хотя в теории 3PC и решает, до некоторой степени, основную проблему с блокированием 2PC, она обладает большими накладными расходами сообщений, вводит потенциальные противоречия и не работает должным образом при наличии разбиения на сетевые разделы. Именно это может служить самой основной причиной почему 3PC широко не применяется на практике.

Распределённые транзакции с применением Calvin

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

Традиционные системы баз данных исполняют транзакции применяя двухфазную блокировку или оптимистический контроль одновременности и не имеют никакого детерминированного порядка транзакции. Это означает, что узлам приходится координировать для сохранения необходимого порядка. Детерминированный порядок транзакции удаляет накладные расходы координации на протяжении фазы выполнения и, поскольку все реплики получили те же самые исходные данные, они также производят эквивалентные вывода. Этот подход общеизвестен как Calvin, быстры протокол транзакции [THOMSON12]. Одним из бросающихся в глаза примеров реализации распределённых транзакций при помощи Calvin является FaunaDB.

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

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

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

Поток исполнителя (worker) управляемый планировщиком, выполняет свои действия за четыре шага:

Анализирует имеющиеся наборы чтения и записи, определяет записи данных набора чтения, локальные для узла, и создаёт список активных участников (т.е. тех, которые содержат необходимые элементы набора записи и будут осуществлять изменение имеющихся данных).

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

Если данный поток исполнителя осуществляется в неком узле активного участника, он получает записи данных, передаваемые от прочих участников в качестве некого дубликата выполненных на протяжении шага 2 операций.

Наконец, он исполняет некий пакет транзакций, сохраняя результаты в локальном хранилище. Он не должен переправлять результаты выполнения прочим узлам, поскольку они получили те же самые входные данные для транзакций и выполнения и самостоятельно сохраняют результаты локально.

Обычная реализация Calvin объединяет в одном месте подсистемы программы упорядочения, планировщика, исполнителя и хранения как это отображает Рисунок 13-7. Чтобы убедиться что программа упорядочения в точности достигла согласия какие транзакции выполнять в текущих эпохе/ пакете, Calvin пользуется алгоритмом консенсуса Paxos (см. раздел Paxos) или асинхронные репликации, в которых лидером выступает некое выделенное обслуживание реплики. Хотя применение некого лидера способно улучшать латентность, это происходит за счёт более высокой стоимости восстановления, поскольку узлам приходится для выполнения по порядку повторно воспроизводить значение состояния своего отказавшего лидера.

Рисунок 13-7

Архитектура Calvin

Распределённые транзакции при помощи Spanner

Calvin часто противопоставляется другому подходу для управления распределёнными транзакциями с названием Spanner [CORBETT12]. Его реализации (или производные) включают некоторые базы данных с открытым исходным кодом, с наиболее известными CockroachDB и YugaByte DB. В то время как Calvin устанавливает глобальный порядок выполнения транзакции достигая согласия относительно последовательности. Spanner использует двухфазную фиксацию поверх групп согласия для раздела (иными словами для Фрагмента, Осколка, Черепка - shard). Spanner имеет достаточно сложную настройку и мы рассмотрим в рамках данной книги лишь подробности верхнего уровня.

Для достижения согласованности и фиксации порядка транзакции Spanner пользуется TrueTime высокоточным API часов- ходиков, которые также выставляют некое ограничение неопределённости, позволяя локальным операциям вводить искусственные замедления чтобы дождаться пересечения границу неопределённости.

Spanner предлагает три основных типа операций: транзакции чтение- запись, транзакции только чтения и чтения моментального снимка. Транзакции чтения- записи требуют блокировок, пессимистического контроля одновременности и наличия соответствующей лидирующей реплики. Транзакции исключительного чтения свободны от блокировки и могут исполняться для любой реплики. Некий лидер требуется только для чтения самой последней временной отметки, которая получает самое последнее фиксированное значение соответствующей группы Paxos. Считывания в такую определённую временную метку согласованно, поскольку значения снабжены версиями и содержимое моментального снимка не может изменяться при записи. Всякая запись данных имеет назначенную ей временную метку (tinestamp), которая содержит значение времени фиксации соответствующей транзакции. Это также подразумевает, что может храниться множество версий с временными отметками определённой записи.

Рисунок 13-8 отображает архитектуру Spanner. Всякий Спансервер (реплика, некий обслуживающий данные клиента экземпляр) содержит различные Плитки (tablets), причём к ним присоединены машины состояний Paxos (см. раздел Paxos). Реплики группируются в наборы реплик, носящие название групп Paxos, некого элемента помещения и репликации данных. Всякая группа Paxos обладает долгоживущим лидером (см. раздел Multi-Paxos). Лидеры взаимодействуют друг с другом в процессе транзакций со множеством Фрагментов (multishard).

Рисунок 13-8

Архитектура Spanner

Всякая запись обязана проходить через лидера группы Paxos, в то время как считывания могут обслуживаться напрямую из Плиток в актуальных репликах. Соответствующий лидер удерживает некую таблицу блокировок, которая применяется для реализации контроля одновременности с использованием механизма двухфазной блокировки (см. раздел Управление одновременностью на основе блокировок) и некого диспетчера транзакции, который отвечает за распределённые транзакции множества Фрагментов. Требующие синхронизации операции (такие как записи и чтения в рамках некой транзакции) должны овладевать соответствующими блокировками из установленной таблицы блокировок, в то время как прочие операции (считывания моментальных снимков) могут получать доступ к данным напрямую.

Для транзакция со множеством Фрагментов лидеры групп должны выполнять координацию и осуществлять двухфазные фиксации для обеспечения согласованности, а также применять двухфазные блокировки для гарантии изолированности. Так как соответствующий алгоритм 2PC требует присутствия всех участников для некой успешной фиксации, он травмирует доступность. Spanner разрешает это применяя группы Paxos вместо индивидуальных участников в качестве Пособников. Это означает, что 2PC способен продолжать действия даже когда некоторые его участники в данной группе остановлены. Внутри соответствующей группы Paxos 2PC контактирует только с тем узлом, который служит лидером.

Группы Paxos используются для согласованной репликации состояний диспетчера транзакций по множеству узлов. Сам лидер Paxos вначале овладевает блокировками записи и выбирает временную отметку записи, которая гарантирует что она будет иметь значение больше чем любая предыдущая временная отметка транзакции и записывает через Paxos некую запись 2PC prepare. Установленный координатор транзакции собирает временные отметки и вырабатывает некую временную отметку фиксации, которая обладает значением больше всех имеющихся временных отметок подготовки и регистрирует в журнале запись commit через Paxos. Затем он дожидается пока не произойдёт фиксация после значения выбранной для ней временной отметки, так как ему требуется обеспечить что клиент будет наблюдать результаты, чьи временные отметки относятся к прошлому. После этого он отправляет данную временную отметку своему клиенту и лидерам, которые регистрируют в журнале соответствующую запись commit с этим новым значением временной отметки в своих локальных группах Paxos и теперь вольны высвобождать соответствующие блокировки.

Транзакциям с единственным Фрагментом не приходится консультироваться со своим диспетчером транзакций (и, позднее, не приходится выполнять некую двухфазную фиксацию по платформам), так как для обеспечения порядка транзакции и согласованности внутри соответствующего Фрагмента достаточно консультаций внутри группы Paxos и таблицы блокировок.

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

Чтобы суммировать, Spanner применят Paxos для согласованной репликации журнала транзакции, двухфазную фиксацию для транзакция по Фрагментам и TrueTime для детерминированного упорядочения транзакций. Это означает, что транзакции со множеством разделов обладают большей стоимостью по причине некого дополнительного раунда двухфазной фиксации, в сравнении с Calvin [ABADI17]. Важно понимать оба подхода, так как они позволяет нам выполнять транзакции в разбитых на части распределённых хранилищах данных.

Разбиение баз данных на разделы

При обсуждении Spanner и Calvin мы достаточно интенсивно пользовались термином разбиения на разделы. Давайте теперь обсудим его более подробно. Так как хранение всех записей базы данных в неком отдельном узле для большинства современных приложений достаточно нереально, многие базы данных применяют разбиение на разделы: некое логическое деление данных на управляемые сегменты меньшего размера.

Наиболее прямолинейным способом разбиения данных является расщепление на диапазоны и разрешение управления лишь определёнными диапазонами (разделами) лишь наборам реплик (replica sets). При выполнении запросов, клиенты (или координаторы запроса) должны осуществлять маршрутизацию запросов на основании соответствующего ключа маршрутизации к верному набору реплик как для чтений, так и для записей. Такая схема разбиения обычно носит наименования фрагментации (sharding): каждый набор реплик выступает как некий отдельный источник для какого- то подмножества данных.

Для более действенного применения разделов, для них следует устанавливать размеры, принимая во внимание имеющуюся нагрузку и распределений значений. Это означает, что диапазоны с частым доступом, интенсивными чтением/ записью могут расщепляться на разделы с меньшим размером для разделения по ним имеющейся нагрузки. В то же самое время, когда некоторые диапазоны более плотны чем иные, хорошей мыслью может быть также расщепление их на меньшие разделы. Например, когда мы цепляемся за диапазоны zip code в качестве ключа маршрутизации, поскольку население определённой страны неравномерно разделено, некоторые диапазоны индекса могут иметь выделенными им больше сведений (т.е. людей и заказов).

При добавлении или удалении в соответствующем кластере узлов наша база данных должна перестраивать разбиение на разделы для соблюдения баланса данных. Для обеспечения согласованных перемещений нам требуется передислоцировать соответствующие данные до того как мы обновим метаданные своего кластера и запустим запросы маршрутизации к своим новым целям. Некоторые базы данных осуществляют автоматическую фрагментацию и передислоцируют необходимые данные при помощи алгоритмов размещения, которые определяют оптимальные разбиения. Такие алгоритмы пользуются сведениями о нагрузках чтения, записи и объёмах данных в каждом из Фрагментов.

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

Наиболее прямолинейный способ установления соответствия хэш- значений идентификаторам узлов состоит в получении остатка от деления соответствующего значения хэша на размер данного кластера (по модулю - modulo). Когда у нас имеется в нашей системе N узлов, значение идентификатора целевого узла закрепляется вычислением hash(v) modulo N. Самой основной проблемой при таком подходе является то, что по мере добавления или удаления узлов и изменении размера кластера с N на N', многие возвращаемые hash(v) modulo N' значения будут отличаться от своих первоначальных. Это означает, что придётся перемещать большую часть данных.

Согласованное хэширование

Для ослабления указанной проблемы некоторые базы данных, например, Apache Cassandra и Riak (помимо прочих), применяют иную схему разбиения на разделы с названием согласованного хэширования (consistent hashing). Как уже упоминалось ранее, значения ключей маршрутизации хэшируются. Возвращаемые соответствующей хэш- функцией значения ставятся в соответствие в неком кольце, с тем, чтобы после самого большего возможного значения они возвращались к своему наименьшему. Всякий узел получает своё собственное место в данном кольце и становится ответственным за соответствующий диапазон значений между своим предшественником и его собственным местом.

Применение согласованного хэширования способствует снижению общего числа передислокаций, необходимых для поддержания баланса: некое изменение в данном кольце воздействует лишь на имеющихся непосредственных соседей данного покинувшего или присоединившегося узла, а не на весь кластер целиком. Значение слова согласованный в этом определении подразумевает, что при изменении размера соответствующей таблицы хэширования, когда у нас имеется K возможных ключей хэша и n узлов, в среднем нам придётся передислоцировать только K/n ключей. Иначе говоря, выдача некой согласованной хэш- функции изменяется минимально по мере изменения диапазона функции [KARGER97].

Распределённые транзакции посредством Percolator

Возвращаясь обратно к предмету распределённых транзакций, может оказаться сложным рассуждение относительно уровней изоляции по причине допуска аномалий чтения и записи. Когда соответствующим приложением не требуется сериализуемость, один из способов избежания аномалий записи описывается в SQL-92 и он состоит в применении некой модели транзакции с названием изоляции моментального снимка (SI, snapshot isolation).

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

Изоляция моментального снимка предотвращает скошенное чтение (read skew), некую аномалию, допускаемую на уровне фиксации- чтения. Например, предполагается, что сумма x и y должна составлять 100. Транзакция T₁ выполняет операцию read(x) и считывает значение 70. T₂ обновляет два значения, write(x, 50) и write(у, 50), а также выполняет фиксацию. Если T₁ попытается выполнить read(y) и продолжит выполнение транзакции на основании значения y(50), заново фиксированного T₂, это повлечёт к некому рассогласованию. То значение x, которое T₁ считала перед фиксацией T₂ и данное новое значение y не согласованы друг с другом. Так как изоляция моментальным снимком относит значения лишь к определённому моменту времени, видимого для транзакций, такое новое значение y, 50 не будет видно для T₁ [BERENSON95].

Изоляция моментальным снимком обладает рядом удобных свойств:

Она допускает только повторяемые считывания фиксированных данных.

Значения согласованы, так как они считываются из соответствующего моментального снимка с некой определённой временной меткой.

Конфликтующие записи прерываются и выполняется их повторная попытка для предотвращения несогласованностей.

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

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

Percolator это библиотека, которая реализует API транзакций поверх распределённой базы данных Bigtable (см. раздел Хранилища с широкими столбцами). Это прекрасный пример построения API транзакций поверх имеющейся системы. Percolator сохраняет записи данных, фиксирует положение точки данных (метаданные сохранения) и выполняет блокировки в различных столбцах. Во избежание условий состязательности и для надёжности блокирования таблиц в неком отдельном вызове RPC он использует условную изменчивость API Bigtable, которая позволяет ему выполнять операции чтения- изменения- записи единственным удалённым вызовом.

Каждая транзакция обязана консультироваться с предсказателем временной метки (timestamp oracle, некий источник монотонно возрастающих по всему кластеру временных штампов) дважды: для временной метки запуска транзакции и в процессе фиксации. Записи буферируются и фиксируются при помощи управляемой клиентом двухфазной фиксации (см. раздел Двухфазная фиксация).

Рисунок 13-9 отображает как содержимое определённой таблицы изменяется на протяжении исполнения необходимых шагов транзакции:

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

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

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

Рисунок 13-9

Шаги исполнения транзакции Percolator. Транзакция кредитует $150 с Account2 и дебетует Account1

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

Изоляция моментального снимка является важной и полезной абстракцией, обычно применяемой при обработке транзакций. Так как она упрощает семантики, предотвращает ряд аномалий и открывает некую возможность для улучшения одновременности и производительности, многие системы MVCC предлагают данный уровень изоляции.

Одним из образцов баз данных, основанных на данной модели Percolator является TiDB (Ti это сокращение от Titatium). TiDB строго согласованна, обладает высокой доступностью и является горизонтально масштабируемой базой данных с открытым исходным кодом, причём совместимой с MySQL.

Избежание координации

Ещё одним обсуждающим затраты на сериализуемость и попытку снижения общего числа координаций и при этом всё ещё предоставляющим обеспечение строгой согласованности примером является уклонение от координации [BAILIS14b]. Можно избегать координации и при этом сохранять ограничения целостности данных, когда операции инвариантно совпадают. Инвариантное совпадение (I- совпадающие, Invariant Confluence) определяется как некое свойство, которое обеспечивает что два допустимых инвариантами, но расходящихся состояния базы данных могут быть слиты в единое допустимое, конечное состояние. Инварианты в данном случае сохраняют согласованность в терминах ACID.

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

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

Транзакции выполняются для имеющихся локальных версий базы данных (моментальных снимков). Когда некой транзакции для выполнения требуется какое- то состояние из прочих разделов, такое состояние делается для неё доступным локально. Когда некая транзакция фиксируется, выполненные в данном локальном моментальном снимке и полученные в результате изменения мигрируют и сливаются с имеющимися моментальными снимками во всех прочих узлах. Некая модель системы, которая позволяет уклоняться от координации обязана обеспечивать такие свойства:

Глобальная допустимость

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

Доступность

Если все удерживаемые состояния достижимы всеми клиентами, данная транзакция обязана достигать решения фиксации, либо прерываться, когда её фиксация приводила бы к нарушению одного из инвариантов данной транзакции.

Сходимость

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

Свобода координации

Выполнение локальной транзакции не зависит от тех операций с имеющимися локальными состояниями, которые выполняются от имени прочих узлов.

Одним из примечательных образцов реализации уклонения от координации являются транзакции RAMP (Read-Atomic Multi Partition, Атомарное чтение из множества разделов) [BAILIS14c]. RAMP пользуется контролем одновременности множества версий и метаданными текущих выполняемых на- лету операций для извлечения любых пропущенных состояний из прочих узлов, делая возможным одновременное выполнение операций чтение и записи. Например, могут выявляться читатели, перекрывающиеся с неким писателем, изменяющим ту же самую запись и, когда это необходимо, выполнять восстановление за счёт извлечения необходимых сведений из метаданных выполняемой на- лету записи в неком дополнительном раунде взаимодействия.

Использование подходов на основе блокировок в распределённой среде может быть не самой лучшей идеей, а вместо этого RAMP предоставляет два свойства:

Независимость синхронизации

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

Независимость разделов

Клиенты не должны контактировать с разделами, значения которых не вовлечены в их транзакции.

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

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

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

Чтобы позволить продолжение исполнения читателям и писателям без блокирования прочих одновременных с ними читателей и писателей, при поддержке уровня изоляции данного атомарного чтения как локально, так и для всей системы (во всех прочих разделах, изменяемых данной фиксацией транзакции), записи в RAMP устанавливаются и делаются видимыми при помощи двухфазной фиксации:

Подготовка

Самый первый этап готовит и помещает записи в их соответствующие целевые разделы не делая их видимыми.

Фиксация/ прекращение

Данный второй этап публикует необходимые изменения состояния, выполненные данной операцией записи текущей фиксируемой транзакции, делая их доступными атомарно во всех разделах, либо откатывая обратно эти изменения.

RAMP делает возможным присутствие в любой определённый момент времени множества версий одной и той же записи: самого последнего значения, находящихся в исполнении на- лету не зафиксированных изменений, а также утративших силу версий, перезаписанных более поздними транзакциями. Устаревшие версии должны удерживаться вокруг только для находящихся в процессе запросов чтения. Как только все одновременные считывания завершены, утратившие силу значения могут быть отвергнуты.

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

Выводы

В этой главе мы обсудили различные пути реализации распределённых транзакций. Прежде всего мы обсудили два алгоритма атомарных фиксаций: двух- и трёх- этапные фиксации. Самым большим преимуществом этих алгоритмов является то, что они просты для понимания и реализации, однако имеют некоторые недостатки. В 2PC, некому координатору (или по крайней мере его представителя) обязан быть в рабочем состоянии на протяжении всего процесса фиксации, что значительно снижает доступность. 3PC в некоторых случаях снимает это требование, но в случае появления сетевых разделов склонен к расщеплению сознания.

Распределённые транзакции в современных системах баз данных часто реализуются при помощи алгоритмов консенсуса, которые мы намерены обсудить в своей следующей главе. Например, и Calvin и Spanner, обсуждавшиеся в данной главе, применяют Paxos.

Алгоритмы консенсуса более сложны чем алгоритмы атомарной фиксации, однако обладают намного лучшими свойствами отказоустойчивости, отделяют решения от их инициаторов и позволяют участникам принимать решение по некому значению, вместо того чтобы принимать или нет определённое значение [GRAY04].

[Замечание]
Последующее чтение

Если вы желаете узнать больше относительно упоминавшихся в этой главе понятий, вы можете ссылаться на следующие источники:

Интеграция атомарных фиксаций с обработкой локальной транзакции и и подсистемами восстановления

Silberschatz, Abraham, Henry F. Korth, and S. Sudarshan. 2010. Database Systems Concepts, (6th Ed.). New York: McGraw-Hill.

Garcia-Molina, Hector, Jeffrey D. Ullman, and Jennifer Widom. 2008. Database Systems: Te Complete Book(2nd Ed.). Boston: Pearson.

Последние достижения в области распределённых транзакций (упорядочены в хронологическом порядке; данный список не является исчерпывающим)

Cowling, James and Barbara Liskov. 2012. “Granola: low-overhead distributed transaction coordination.” In Proceedings of the 2012 USENIX conference on Annual Technical Conference (USENIX ATC ’12): 21-21. USENIX.

Balakrishnan, Mahesh, Dahlia Malkhi, Ted Wobber, Ming Wu, Vijayan Prabhakaran, Michael Wei, John D. Davis, Sriram Rao, Tao Zou, and Aviad Zuck. 2013. “Tango: distributed data structures over a shared log.” In Proceedings of the Twenty-Fourth ACM Symposium on Operating Systems Principles (SOSP ’13): 324-340.

Ding, Bailu, Lucja Kot, Alan Demers, and Johannes Gehrke. 2015. “Centiman: elastic, high performance optimistic concurrency control by watermarking.” In Proceedings of the Sixth ACM Symposium on Cloud Computing (SoCC ’15): 262-275.

Dragojević, Aleksandar, Dushyanth Narayanan, Edmund B. Nightingale, Matthew Renzelmann, Alex Shamis, Anirudh Badam, and Miguel Castro. 2015. “No compromises: distributed transactions with consistency, availability, and performance.” In Proceedings of the 25th Symposium on Operating Systems Principles (SOSP ’15): 54-70.

Zhang, Irene, Naveen Kr. Sharma, Adriana Szekeres, Arvind Krishnamurthy, and Dan R. K. Ports. 2015. “Building consistent transactions with inconsistent replication.” In Proceedings of the 25th Symposium on Operating Systems Principles (SOSP ’15): 263-278.

Глава 14. Консенсус

Содержание

Глава 14. Консенсус

Широковещание

Атомарное широковещание

Виртуальная синхронизация

ZAB

Paxos

Алгоритм Paxos

Кворум в Paxos

Сценарии отказов

Multi-Paxos

Быстрый Paxos

Уравнивающий Paxos

Гибкий Paxos

Обобщённое решение для консенсуса

Обобщённый алгоритм Paxos

Raft

Роль лидера в Raft

Сценарии отказов

Византийский консенсус

Алгоритм pBFT

Восстановление и установка контрольных точек

Выводы

Заключение Части II

Мы обсудили достаточно много понятий распределённых систем, начиная с основ, таких как ссылки и процессы, проблемы с распределёнными вычислениями; затем прошлись по моделям отказов, их выявлением и выборам лидера; обсудили модели согласованности; и в конце концов мы готовы собрать это всё воедино для вершины исследования распределённых систем: распределённого консенсуса.

Алгоритмы консенсуса в распределённых системах делают возможным для множества процессов достигать согласия по некому значению. Невозможность FLP показывает что невозможно обеспечить согласие в некой целиком асинхронной системе за ограниченное время. Даже когда доставка сообщения гарантирована, одному процессу невозможно узнать испытал ли другой крушение, либо то что он медленно работает.

В Главе 9 мы обсуждали что существует компромисс между точностью выявления отказов и тем насколько быстро такой отказ может быть определён. Алгоритмы консенсуса полагаютс на модель асинхронности и гарантию безопасности, в то время как некий определитель внешних отказов может предоставлять сведения относительно прочих процессов, гарантируя жизнеспособности [CHANDRA96]. Так как выявление отказов не всегда целиком точны, будут иметься ситуации при которых алгоритмы консенсуса ожидают выявления некого отказа процесса, либо когда данный алгоритм перезапускается потому как некоторые процессы неверно подозреваются в отказе.

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

С теоретической точки зрения, алгоритмы консенсуса обладают тремя свойствами:

Согласованность

Значение данного решения одно и то же для всех Корректных процессов.

Обоснованность

Принятое значение было предложено одним из имеющихся процессов.

Завершаемость

Все Корректные процессы в конечном итоге достигают определённого решения.

Всякое из этих свойств чрезвычайно важно. Само согласие встроено в человеческое понимание консенсуса. Такое словарное определение консенсуса имеет в себе слово "единодушие". Это означает что в соответствии с таким согласием никакому процессу не дозволяется иметь мнение, отличное от выдаваемого на выходе. Представляйте себе его как некое согласие встретить в некий определённый момент и в определённом месте своих приятелей: все вы хотите встретиться, согласовывается лишь сама специфика данного события.

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

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

Широковещание

Некое широковещание это абстракция взаимодействия, часто применяемая в распределённых системах. Алгоритмы широковещания используются для рассеивания сведений по некому набору процессов. Существует много алгоритмов широковещания, делающих различные предположения и предоставляющих различные гарантии. Широковещание является неким важным примитивом и применяется во множестве мест, включая и алгоритмы консенсуса. Мы уже обсуждали одну из форм широковещания - рассеяние gossip (см. раздел Рассеивание Gossip).

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

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

Для надёжности широкого вещания необходимо обеспечивать кто все Корректные процессы получили одни и те же сообщения, даже если сам отправитель испытывает крушение в процессе передачи.

Для реализации некой наивной версии некого надёжного широковещания, мы можем пользоваться неким определителем отказов и какой- то механизм отхода. Наиболее прямолинейный механизм отхода состоит в разрешении каждому принимающему данное сообщение процессу переправления его всем прочим процессам, о которых он осведомлён. Когда соответствующий процесс источника отказывает, прочие процессы выявляют это падение и продолжают широковещательно распространять это сообщение, действенно заполняя данную сетевую среду N² сообщениями (как это показано на Рисунке 14-1). Даже если такой отправитель претерпел крушение, сообщения всё ещё подбираются и доставляются всей оставшейся системой, улучшая её надёжность и позволяя всем получателям видеть те же самые сообщения [CACHIN11].

Рисунок 14-1

Широковещание

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

Атомарное широковещание

Даже несмотря на то, что наш только что описанный заполняющий алгоритм может гарантировать доставку, он не обеспечивает доставку в каком бы то ни было определённом порядке. Сообщения достигают в конце концов своего получателя, причём в некий неизвестный момент времени. Когда нам необходимо доставлять сообщения по порядку, нам приходится применять атомарное широковещание (также носящего название тотально упорядоченной групповой передачи), которая гарантирует и надёжную доставку и тотальное упорядочение.

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

В сумме некое атомарное широковещание обязано гарантировать два существенных свойства:

Атомарность

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

Упорядоченность

Всем не отказавшим процессам сообщения доставляются в одном и том же порядке.

Сообщения в данном случае доставляются атомарно: каждое сообщение либо доставляется всем процессам, либо ни одному из них, причём когда такое сообщение доставлено, все прочие сообщения все прочие сообщения упорядочиваются до или после этого сообщения.

Виртуальная синхронизация

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

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

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

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

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

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

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

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

ZAB

Одной из самых популярных и широко известных реализаций обсуждаемого атомарного широковещания является ZAB, применяемый Apache Zookeeper [HUNT10] [JUNQUEIRA11], иерархическое распределённое хранилище ключ- значение, в котором оно используется для обеспечения необходимого тотального порядка событий и атомарной доставки, требующейся для сопровождения согласованности между установленными состояниями реплик.

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

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

Как только установлен некий предполагаемый лидер, он выполняет некий протокол в три фазы:

Обнаружение

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

Синхронизация

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

Широкое вещание

Как только все Последователи вернулись в синхронное состояние, запускаются активные сообщения. На протяжении этого этапа установленный лидер получает сообщения клиентов, устанавливает их порядок и выполняет их широковещание всем имеющимся Последователям: он отправляет некое новое предложение, дожидается кворума Последователей ответами подтверждений и, наконец, фиксирует его. Данный процесс аналогичен двухфазной фиксации без прерываний: голоса являются всего лишь подтверждениями и клиент не может голосовать против некого допустимого предложения лидера. Тем не менее, предложения от лидеров из неверных эпох не должны подтверждаться. Данная фаза широковещания продолжается вплоть до крушения её Лидера, разбиение на разделы Последователей или ожидания наступления крушения по причине задержки соответствующего предложения.

Рисунок 14-2 отображает все три фазы данного алгоритма ZAB и обмен сообщений на каждом из этапов.

Рисунок 14-2

Резюме протокола ZAB

Безопасность данного протокола обеспечивается когда Последователи гарантированно принимают предложения от своего Лидера для данной установленной эпохи. Два процесса могут попытаться быть выбранными, но лишь один из них может победить и установить себя в качестве Лидера эпохи. Это также предполагает что процессы выполняют предписанные этапы в духе доброй воли и следуют установленному протоколу.

Как Лидер, так и Последователи полагаются на сердцебиения для определения жизнеспособности соответствующего удалённого процесса. Когда установленный Лидер не получает сердцебиений от необходимого кворума Последователей, он уходит с поста Лидера и перезапускает весь процесс выборов. Аналогично, когда один из его Последователей определяет крушение своего Лидера, он запускает процесс новых выборов.

Сообщения тотально упорядочены и установленный Лидер не предпринимает попыток отправить своё следующее сообщение пока не было подтверждено предыдущее сообщение. Даже когда некоторые сообщения получаются Последователями более одного раза, их повторное приложение не производит дополнительных сторонних эффектов при следовании порядку доставки. ZAB способен обрабатывать множество нерешённых одновременных изменений состояний клиентов, так как некий уникальный лидер будет получать запросы на запись, устанавливать необходимый порядок событий и широковещательно распространять эти изменения.

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

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

Paxos

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

Вероятно наиболее широко известным алгоритмам консенсуса является Paxos. Он впервые был предложен Лесли Дампортом в его статье "The Part-Time Parliament" [LAMPORT98]. В этой статье консенсус описывается в выражениях терминологии, вдохновлённой законодательным и избирательным процессом на Эгейском острове Паксос. В 2001 этот автор выпустил следующую статью, озаглавленную "Paxos Made Simple" [LAMPORT01], которая вводила более простую терминологию, которая теперь и применяется для объяснения данного алгоритма.

Участники Paxos способны выполнять одну из трёх ролей: Заявителей, Получателей или Обучаемых.

Заявители

Получают от клиентов значения, создают предложения по принятию этих значений и пытаются собрать голоса от Получателей.

Получатели

Голосуют за принятие или отклонение предложенного Заявителем значения. Для отказоустойчивости данный алгоритм требует наличия множества Получателей, однако для его жизнеспособности с целью принятия выставленного предложения требуется лишь кворум (большинство) голосов Получателей.

Обучаемые

Играют роль реплик, храня все выводимые сведения по принятым предложениям.

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

Всякое предложение состоит из некого значения, предлагаемого соответствующим клиентом, а также монотонно возрастающим номером предложения. Этот номер затем используется для обеспечения общего порядка исполняемых операций и устанавливает взаимосвязи происхождения -до/ -после между ними. Номера предложений также часто реализуются при помощи неких пар (id, timestamp), где также сопоставляется и идентификатор узла, который может применяться в качестве разрывающих связей (break ties) для временных отметок.

Алгоритм Paxos

Сам алгоритм Paxos может быть в целом разделён на два этапа: фазу голосования (или предложения) и репликацию. На протяжении этапа голосования, Вносящие предложение состязаются за установление своего лидерства. В процессе репликации, установленный Вносящий предложение распространяет соответствующее значение Получателям.

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

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

На протяжении этапа предложения установленный Вносящий предложение отправляет некое сообщение Prepare(n) (где n это номер предложения) некому большинству Получателей и предпринимает попытку сбора их голосов.

Когда некий Получатель принимает этот подготовительный запрос, он обязан ответить, сохраняя следующие инварианты [LAMPORT01]:

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

Если этот Получатель ранее уже принял - получил сообщение Accept!(m,vaccepted) - любое иное предложение, он отвечает сообщением Promise(m,vaccepted), уведомляя своего Вносящего предложение чтоон уже принял определённое предложение с порядковым номером m.

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

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

На протяжении соответствующего этапа репликации, после сбора голосов большинства, установленный Заявитель может запустить необходимую репликацию, в которой он фиксирует значение предложения отправляя Получателям некое сообщение Accept!(n, v) со значением v и номером предложения n. vэто то значение, которое связано с имеющим наивысший номер из всех принятых им от Получателей откликов предложением или любое его собственное значение когда их отклики не содержат старых принятых предложений.

Соответствующий Получатель принимает это предложение с номером n, если только на протяжении этапа предложения он уже не ответил Prepare(m), где m больше чем n. Когда Получатель отвергает данное предложение, он уведомляет об этом своего Заявителя, отправляя самое большое последовательное число, которое он наблюдал в своём запросе содействуя своему Заявителю поймать его [LAMPORT01].

Вы можете рассмотреть обобщённое отображение некого раунда Paxos на Рисунке 14-3.

Рисунок 14-3

Алгоритм Paxos: нормальное исполнение

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

Обучаемый обязан отыскать то значение, по которому было принято решение, с которым он может ознакомиться после получения уведомлений от большинства Получателей. Чтобы позволить имеющемуся Обучаемому узнать о новом установленном значении настолько быстро, насколько это возможно, Получатели могут уведомлять их о таком значении как только они его приняли. Если имеются более одного Обучаемых, каждому Получателю придётся уведомлять всех Обучаемых. Один или более обучаемых могут быть Выдающимися (distinguished), и в этом случае он будет уведомлять и прочих Обучаемых относительно принимаемых значений.

Итак, основная цель самого первого этапа алгоритма состоит в установлении некого лидера для данного раунда и понимания каое значение подлежит принятию, что делает возможным такому лидеру продолжить вторым этапом: широковещательным распространением этого значения. На практике, мы бы желали снизить общее число этапов в данном алгоритме, потому мы позволяем своему Заявителю предлагать более одного значения. Мы обсудим это более подробно в разделе Multi-Paxos.

Кворум в Paxos

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

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

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

Жизнеспособность гарантируется при наличии f отказавших процессов. Для этого данный протокол требует в общей сложности 2f + 1 процессов с тем, чтобы когда произойдёт f отказов, всё ещё будет иметься f + 1 процессов, способных продолжать работу. Применяя кворумы, вместо того чтобы требовать присутствия всех процессов, Paxos (и прочие алгоритмы консенсуса) обеспечивают результаты даже в сучае возникновения f отказов. В разделе Гибкий Paxos мы обсудим кворумы слегка в иной терминологии и опишем как строит протоколы, требующие лишь пересечения кворумов между этапами алгоритма.

[Совет]
Совет

Важно помнить, что кворумы описывают лишь определённые блочные свойства данной системы. Для обеспечения безопасности, на каждом этапе нам приходится дожидаться откликов по крайней мере от кворума узлов. Мы можем отправлять предложения и принимать команды для большего числа узлов; для продолжения нам просто не требуется дожидаться их откликов. Мы можем отправить сообщения большему числу узлов (некоторые системы применяют спекулятивное исполнение: вызывают избыточное число запросов, что способствует достижению требуемого числа откликов в случае отказов узлов), однако для обеспечения жизнеспособности, мы можем продолжать как только мы обнаружили необходимый кворум.

Сценарии отказов

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

Рисунок 14-4 отображает такую ситуацию:

Заявитель P₁ проходит этап выборов с неким предложением номер 1, однако отказывает после отправки соответствующего значения V1 всего лишь одному получателю A₁.

Другой Заявитель P₂ запускает новый раунд с более высоким номером предложения 2, собирает некий кворум из откликов Получателей (в данном случае A₁ и A₂) и продолжает фиксацию своего старого значения V1, предложенного P₁.

Рисунок 14-4

Сценарий отказа Paxos: Вносящий предложение отказывает полагаясь на своё старое значение

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

В неком приложении клиент/ сервер, в котором определённый клиент соединяется лишь с первоначальным Заявителем, это может повлечь за собой ситуации, при которых данный клиент не знает об окончательном результате исполнения данного раунда Paxos (к примеру, такое положение вещей описывается в https://databass.dev/links/68).

Тем не менее, также возможны и иные сценарии, как это отображено на Рисунке 14-5. Например:

P₁ отказывает в точности так же как и в предыдущем примере, после отправки своего значения V1 лишь к A₁.

Наш следующий Заявитель, P₂ запускает некий новый раунд с более высоким номером предложения 2 и собирает некий кворум откликов Получателей, однако на этот раз первыми откликаются A₂ и A₃. После сбора кворума, P₂ фиксирует своё собственное значение,несмотря на тот факт, что теоретически присутствует некое иное фиксируемое значение в A₁.

Рисунок 14-5

Сценарий отказа Paxos: Вносящий предложение отказывает полагаясь на своё новое значение

В данных обстоятельствах имеется ещё одна возможность, отображаемая на Рисунке 14-6:

Заявитель P₁ отказывает лишь после единственного Получателя A₁, принявшего выставленное значение V1. A₁ вскорости после приёма данного предложения отказывает, причём до того как он смог оповестить своего следующего Заявителя о своём значении.

Заявитель P₂, который запустил необходимый новый раунд после отказа P₁ не перекрывался с A₁ и продолжает вместо этого фиксацией своего значения.

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

Рисунок 14-6

Сценарий отказа Paxos: Вносящий предложение отказывает вслед за отказом своего Получателя

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

Хотя Получатели и обещают не принимать никакие предложения с более низкими номерами, они всё ещё отвечают множеству подготовительных запросов до тех пор, пока не появится самый последний с неким более высоким последовательным номером. Когда некий Заявитель пытается фиксировать данное значение, он может обнаружить что Получатели уже ответили на некий подготовительный запрос с более высоким последовательным номером. Это может повлечь к тому, что множество процессов постоянно повтряют свою попытку и препятствуют друг другу в последующем развитии. Такая проблема обычно разрешается через вовлечение некого случайного отката, который в конечном счёте позволит одному из имеющихся Заявителей продолжать, вто время как другой засыпает.

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

Multi-Paxos

До сиз пор мы обсуждали классический алгоритм Paxos, в котором мы придерживаемся некого произвольного Заявителя и пытаемся запустить некий раунд Paxos. Одной из основных проблем данного подхода состоит в том, что для всякого раунда происходящего в данной системе репликации необходим некий раунд предложения. Лишь после того как соответствующий Заявитель установлен для данного раунда, что происходит после отклика большинства Получателей неким Promise на соответствующий Prepare, он способен запустить данную репликацию. Чтобы избежать соответствующего этапа предложения и позволить установленному Заявителю повторно применять его общепризнанное положение, мы можем применять Множественный Paxos (Multi-Paxos), который вводит некое понятие Лидера: некого Высокопоставленного Заявителя (distinguished proposer) [LAMPORT01]. Именно это является критическим добавлением, которое значительно улучшает действенность алгоритма.

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

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

Аналогичная ситуация может происходить и в Множественном Paxos: мы пытаемся выполнить некое чтение из известного Лидера после того как уже избран другой Лидер, возвращая устаревшие сведения, что входит в противоречие с необходимостью для консенсуса обеспечения линеаризуемости. Во избежание этого и дабы гарантировать что никакой иной процесс не способен успешно представить значения, некоторые реализации Множественного Paxos применяют аренду (leases). Установленный Лидер периодически взаимодействует с имеющимися участниками, уведомляя их что он всё ещё работает, действенно продлевая свою аренду. Участники должны откликаться и позволять этому Лидеру продолжать свои операции, обещая что они не будут принимать предложения от прочих лидеров на данный период аренды [CHANDRA07].

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

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

Для предотвращения роста журнала до неопределённо большого размера, к его содержимому следует применять обсуждавшуюся ранее структуру. После того как данный журнал синхронизирует содержимое со своей первичной структурой, создаётся некий моментальный снимок, а сам журнал может быть усечён. Журнал и состояние моментальных снимков обязаны быть взаимно согласованными, причём изменения моментального снимка должны применяться атомарно с усечением откидываемого сегмента журнала [CHANDRA07].

Мы можем представлять себе отдельное постановление Paxos как некий однократно- записываемый регистр: у нас есть некий разъём, в который мы имеем возможность помещать некое значение и как только мы записали туда своё значение, невозможны никакие последующие изменения. На протяжении самого первого этапа предложения состязаются за обладание данным регистром,а на втором этапе один из них записывает своё значение. В то же самое время, Множественный Paxos может представляться нам как некий дописываемый исключительно в конец журнал, состоящий из некой последовательности таких значений: за раз мы можем записывать единственное значение, все значения строго упорядочены и мы не имеем возможности изменять уже записанные значения [RYSTSOV16]. Имеются примеры алгоритмов консенсуса, которые предлагают наборы регистров считывания- изменения- записи и используют разделение состояния вместо того чтобы реплицировать конечные автоматы, например, Active Disk Paxos [CHOCKLER15] и CASPaxos [RYSTSOV18].

Быстрый Paxos

Мы можем снизить общее число проходов туда и обратно на один по сравнению со своим классическим алгоритмом Paxos, позволив любому Заявителю взаимодействовать с Получателями напрямую вместо того чтобы полагаться на установленного Лидера. Для этого нам требуется увеличить размер своего кворума до 2f + 1 (где f это общее число процессов, для которых допустим отказ), в сравнении с f + 1 для классического Paxos, а общее число Получателей до 3f + 1 [JUNQUEIRA07]. Такая организация носит название Быстрого Paxos [LAMPORT06].

Наш алгоритм классического Paxos обладает неким условием, когда на протяжении выполняемого этапа репликации установленный Заявитель может выцеплять любое значение, которое он собирал на своём этапе предложения. Быстрый Paxos обладает двумя типами раундов: классическим, при котором данный алгоритм продолжается тем же самым способом , что и в своей классической версии и быстрым, когда он разрешает Получателям принимать прочие значения.

При определении данного алгоритма мы будем именовать своего Заявителя, который должен собрать достаточное число откликов на этапе предложения Координатором и зарезервировать термин Заявителя для всех прочих Заявителей. некоторые описания Быстрого Paxos сообщают, что клиенты могут вступать в контакт с Получателями напрямую [ZHAO15].

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

Рисунок 14-7 отображает некий образец классического и быстрого раундов в Быстром Paxos. Исходя из этого рисунка может показаться, что отображённый быстрый раунд имеет больше шагов исполнения, но имейте в виду, что в классическом раунде для представления своего значения установленному Заявителю необходимо пройти через своего Координатора для получения фиксации своего значения.

Рисунок 14-7

Алгоритм быстрого Paxos: быстрые и классические раунды

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

Это означает, что Получатели, после приёма значений от различных Заявителей, могут принимать решение в пользу конфликтных значений. Когда установленный Координатор выявляет некий конфликт (коллизию значений), ему необходимо повторно инициировать некий этап Propose, чтобы позволить Получателям сойтись к какому- то единому значению.

Одним из недостатков Быстрого Paxos является растущее число проходов в оба конца и задержек запросов при столкновениях при большой скорости запросов. [JUNQUEIRA07] показал, что по причине растущего числа реплик и, в конечном счёте, обмениваемых между участниками сообщений, несмотря на снижение числа этапов, Быстрый Paxos может обладать большими задержками нежели его классический конкурент.

Уравнивающий Paxos

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

[Замечание]
Замечание

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

Вместо применения Лидера и предлагаемых номеров для последовательных команд мы можем пользоваться Лидером для конкретной фиксации этой определённой команды и установления порядка просмотра и установки зависимостей. Такой подход обычно именуется Уравнивающим Paxos (Egalitarian Paxos) или EPaxos [MORARU11]. Основная мысль допуска независимой фиксации не конфликтующих записей в имеющемся реплицируемом конечном автомате впервые была предложена в [LAMPORT05] и носила название Обобщённого Paxos. EPaxos это первая реализация Обобщённого Paxos.

EPaxos делает попытку предложить как преимущества классического алгоритма Paxos, так и Множественного Paxos. Классический Paxos предлагает высокую доступность, так как Лидер устанавливается в каждом раунде, но имеет более высокую сложность сообщений. Множественный Paxos предлагает высокую пропускную способность и требует меньшего числа сообщений, но Лидер способен превращаться в узкое место.

EPaxos запускается фазой Предварительного приёма, в процессе которого некий процесс превращается в Лидера для данной определённой цели. Каждое предложение содержит:

Зависимости

Все команды, которые потенциально взаимодействуют с текущим предложением, но для них нет необходимости быть уже фиксированными.

Последовательный номер

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

После сбора этих сведений, он направляет сообщение Pre-Accept быстрому кворуму реплик. Быстрый кворум это ⌈3f/4⌉ реплик, где f это значение числа возможных отказов.

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

Когда установленный Лидер не получает достаточного числа откликов или если его получаемый от своих реплик список команд отличен и содержит пересекающиеся команды, он обновляет свои зависимости неким новым перечнем зависимостей и последовательным номером. Этот новый список зависимостей основывается на отклика предыдущих реплик и сочетает все собранные зависимости. Установленный новый последовательный номер обязан быть больше чем самый наивысший наблюдавшийся в репликах последовательный номер. После этого, данный Лидер отправляет свою новую, изменённую команду в ⌊f/2⌋ + 1 реплик. После осуществления этого, данный Лидер может окончательно зафиксировать данное предложение.

На самом деле у нас имеются два сценария:

Быстрый путь

Когда зависимости находятся в соответствии и установленный Лидер способен безопасно продолжать этап фиксации только с репликами быстрого кворума.

Медленный путь

Когда существуют разногласия между имеющимися репликами и их перечни команд следует обновить прежде чем установленный Лидер будет иметь возможность продолжать фиксацию.

Рисунок 14-8 отображает эти сценарии - P₁ инициирует запуск быстрого пути, а P₅ инициирует запуск медленного пути:

P₁ запускается с предложением номер 1 и без зависимостей и отправляет сообщение. PreAccept(1, ∅). Поскольку журналы команд P₂ и P₃ пусты, P₁ может продолжать выполнение фиксации.

P₅ создаёт некое предложение с последовательным номером 2. Так как его журнал команд на данный момент пуст, он также объявляет отсутствие зависимостей о отправляет сообщение PreAccept(2, ∅). P₄ не осведомлён об имеющемся фиксированном предложении 1, однако P₃ уведомляет P₅ относительно данного конфликта и отправляет свой журнал команд: {1}.

P₅ обновляет свой список локальных зависимостей и отправляет сообщение чтобы убедиться что реплики имеют те же самые зависимости: Accept(2,{1}). Как только соответствующие реплики откликаются, он может фиксировать данное значение.

Рисунок 14-8

Выполнение алгоритма EPaxos

Две команды, A и B, пересекаются только если имеет значение их порядок; иначе говоря, когда исполнение A перед B и выполнение B до A приводит к различным результатам.

Фиксация выполняется откликом клиенту и асинхронным уведомлением реплик сообщением Commit. Команды выполняются после их фиксации.

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

Для выполнения команды реплики строят некий граф зависимостей и исполняют все команды в порядке, обратном зависимостям. Говоря иначе, прежде чем какая- то команда может быть выполнена, необходимо выполнить все её зависимости (и, затем и все их зависимости). Так как только пересекающиеся команды обязаны зависеть друг от друга, подобная ситуация должна быть достаточно редкой для большей части рабочих нагрузок [MORARU13].

Аналогично Paxos, EPaxos применяет номера предложений, что предотвращает распространение устаревших сообщений. Последовательный номер составляется из некой эпохи (идентификатора конфигурации данного текущего кластера, который изменяется когда узлы покидают этот кластер или присоединяются к нему), монотонно последовательно увеличивающегося счётчика локального узла и некого идентификатора реплики. Когда какая- то реплика получает предложение с последовательным номером ниже уже наблюдавшегося ею, она отвечает отрицательным подтверждением на данное предложение и отправляет в отклике свой наивысший последовательный номер, а также известный ей обновлённый перечень команд.

Гибкий Paxos

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

Нам требуется ответить на два важных вопроса:

Есть ли необходимость взаимодействовать с большинством серверов на каждом этапе исполнения?

Все ли кворумы имеют пересечения? Иначе говоря, должны ли иметь общие узлы используемый нами кворум для выбора Высокопоставленного Заявителя (первый этап), применяемый нами для определения значения кворум (второй этап) и каждый экземпляр исполнения (например, когда одновременно выполняются несколько экземпляров второго этапа)?

Так как мы всё ещё обсуждаем консенсус, мы не можем изменять никакие определения безопасности: наш алгоритм обязан обеспечивать установленные соглашения.

Во Множественном Paxos собственно фаза выбора Лидера не является частой и нашему Высокопоставленному Лидеру позволено фиксировать различные значения без возврата к соответствующему этапу выборов, потенциально оставаясь установленным Лидером длительное время. В разделе Настраиваемая согласованность мы обсудили формулу, способствующую нам в поиске конфигурации в которой мы обладаем пересечениями между своими наборами узлов. Одним из примеров было простое ожидание того что один узел подтвердит соответствующую запись (и позволит имеющимся запросам во всех остальных узлах завершаться асинхронно) и чтение со всех доступных узлов. Иными словами, пока мы имеем R + W > N, существует по крайней мере один общий узел между наборами чтения и записи.

Можем ли мы воспользоваться этой же логикой для консенсуса? Оказывается, да, причём в Paxos нам всего лишь требуется чтобы группа узлов из нашего первого этапа (которая выбирает Лидера) перекрывалась с группой второго этапа (участвующей в принятии предложений).

Говоря другими словами, некий кворум не обязательно должен определяться как некое большинство, а всего лишь как какая- то не пустая группа узлов. Если мы определим общее число участников как N, N, общее число узлов, требуемых для успешности этапа предложения как Q₁, а общее число узлов для успешности этапа приёма как Q₂, нам всего лишь требуется чтобы Q₁ + Q₂ > N. Так как наш второй этап как правило более распространён чем первый, Q₂ может содержать лишь N/2 Получателей, в то время как Q₁ настраивается на соответствующее большее значение (Q₁ = N - Q₂ + 1). Эта находка является важным наблюдением, критически важным для понимания консенсуса. Тот алгоритм, который мы применяем в данном подходе носит название Гибкого Paxos [HOWARD16].

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

Гибкий Paxos делает возможным компромисс с доступностью в пользу задержек: мы снижаем общее число узлов, участвующих во втором этапе, но нам приходится собирать больше голосов, что требует большего числа участников доступными на этапе выборов своего Лидера. Хорошая новость состоит в том, что данная конфигурация может продолжать соответствующий этап репликации и быть безразличной вплоть до отказа N - Q₂ + 1 узлов, пока установленный Лидер стабилен и не требуется новый раунд выборов.

Другим вариантом Paxos, использующим ту же идею пересечения кворумов является Вертикальный Paxos. Вертикальный Paxos делает различия между кворумами чтения и записи. Эти кворумы обязаны пересекаться. Некий Лидер должен собирать кворум меньшего размера чтений для одного или более предложений с меньшими номерами и кворум большего размера записи для своего собственного предложения [LAMPORT09]. [LAMPSON01] также проводит различие между кворумами выдачи и решения, что транслируется в этапы подготовки и принятия и снабжает нас определением кворума, аналогичным Гибкому Paxos.

Обобщённое решение для консенсуса

Paxos порой может оказываться слегка сложным при обсуждении: множество ролей, этапов и сложность отслеживания всех возможных вариантов. Однако мы можем представлять себе его и в более простых выражениях. Вместо того чтобы разделять роли между участниками и получать раунды принятия решений, мы можем пользоваться простым набором понятий и правил для достижения обеспечения единственного решения Paxos. Мы обсудим этот подход лишь вкратце, поскольку это относительно новая разработка [HOWARD19] - его важно знать, однако мы ещё не видели его реализаций и практических применений.

У нас имеется некий клиент и набор серверов. Каждый сервер обладает множеством регистров. некий регистр имеет идентифицирующий его индекс, запись в него может производиться только один раз и он может находиться в трёх состояниях: незаписанном, содержащем некое значение и содержащим nil (особое пустое значение).

Регистры с одним и тем же значением, расположенные в различных серверах образуют некий набор регистров. Всякий набор регистров может обладать одним или более кворумами. В зависимости от значения состояния регистров в нём, кворум может быть в одном из состояний без решения (undecided, Any и Maybe v), либо с решением (None и Decided v):

Any

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

Maybe v

Когда этот кворум принимает решение, его решением может быть только v.

None

Этот кворум не способен принимать решение по конкретному значению.

Decided v

Данный кворум принял решение со значением v.

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

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

Правила записи слегка сложнее, так как для обеспечения безопасности алгоритма нам необходимо сохранять ряд инвариантов. Прежде всего, нам требуется быть уверенными что наш клиент не просто предлагает новые значения: ему разрешается записывать в регистр некое определённое значение только когда он получил его в качестве входных данных или считал его из некого регистра. Клиенты не способны записывать значения, которые допускают различные кворумы в одном и том же регистре для принятия решения по различным значениям. И, наконец, клиенты не могут записывать значения, которые отменяют предыдущие решения, принятые в более ранних наборах регистров (принятые в наборе регистров вплоть до r - 1 решения обязаны быть None, Maybe v или Decided v.)

Обобщённый алгоритм Paxos

Совмещая эти три правила воедино, мы можем реализовать некий обобщённый алгоритм Paxos, который достигает согласия относительно некого отдельного значения при помощи регистров с единственной записью [HOWARD19]. Допустим, у нас имеются три сервера [S0, S1, S2], регистры [R0, R1, …] и клиенты [C0, C1, …], причём эти клиенты способны записывать лишь в соответствующие назначенные подмножества регистров. Для всех регистров мы применяем кворумы простого большинства ({S0, S1}, {S0, S2}, {S1, S2}).

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

В процессе фазы 1

Наш клиент проверяет что тот регистр, в который он намерен выполнить запись не записан через отправку команды P1A(register) в его сервер. Когда этот регистр не записан, все регистры вплоть до register - 1 установлены в значение nil, что препятствует клиентам запись в предыдущие регистры. Соответствующий сервер откликается неким набором регистров, записанных до сих пор. Если клиент получает отклики от большинства серверов, он выбирает либо соответствующее значение из полученного регистра с наибольшим индексом, либо своё собственное значение в случае когда нет представленного значения. В противном случае он перезапускает этот первый этап.

В процессе фазы 2

Наш клиент уведомляет все серверы относительно того значения, которое он выбрал на протяжении своего первого этапа, отправляя им P2A(register, value). Если большинство серверов откликаются на это сообщение, он может выдать своё выбранное значение. В противном случае он вновь начинает с фазы 1.

Рисунок 14-9 отображает обобщённый Paxos (адаптировано из [HOWARD19]). Клиент C0 предпринимает попытку фиксации значения V. В процессе своего первого этапа его таблица состояний пуста, а серверы S0 и S1 откликаются с пустыми значениями набора регистров, что указывает на то, что до сих пор не записан ни один из регистров. На протяжении второго этапа он может представить своё значение V, так как никакое иное значение не было записано.

Рисунок 14-9

Обобщённый Paxos

В этот момент, какой- то иной клиент может сделать серверам запрос поиска значения текущего состояния. Кворум {S0, S1} достиг состояния Decided A, а кворумы {S0, S2} и {S1, S2} достигли значения состояния Maybe V для R0, а потому C1 выбирает значение V. В этот момент никакой из клиентов не способен принять решения отличного от V.

Данный подход позволяет понять семантику Paxos. Вместо того чтобы представлять себе значение состояния с точки зрения взаимодействия удалённых акторов (то есть Заявителей, обнаруживающих может или нет некий Получатель быть уже принявшим некое иное предложение), мы можем представлять себе его в терминах самого последнего известного состояния, что превращает наш процесс принятия решения в простой и удаляет возможные неопределённости. Также может быть более простой реализация неизменного состояния и передачи сообщений.

Мы также можем провести параллели с оригинальным Paxos. Например, в неком сценарии, в котором наш клиент определяет что один из имеющихся предыдущих наборов регистров обладает соответствующим решением Maybe V, он выбирает V и пробует зафиксировать его снова, что схоже с тем, как некий Заявитель в Paxos может предлагать своё значение после произошедшего отказа имевшегося ранее Заявителя, и который мог зафиксировать это значение по крайней мере у одного Получателя. Точно так же, когда конфликты в Лидерах Paxos разрешаются перезапуском данного голосования с большим номером предложения, в нашем обобщённом алгоритме все незаписанные регистры с низшими рангами установлены в значение nil.

Raft

Paxos был основным алгоритмом консенсуса на протяжении более десяти лет, однако в сообществе распределённых систем он был известен как трудный для обсуждения. В 2013 появился некий новый алгоритм с названием Raft (Плот). Разработавшие его исследователи хотели создать некий алгоритм, который просто понять и реализовать. Впервые он был представлен в статье с названием "В поисках понятного алгоритма консенсуса" [ONGARO14].

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

Локально участники хранят журнал, содержащий последовательность команд, выполняемых соответствующим конечным автоматом. Поскольку получаемые процессами входные данные идентичны, а журналы содержат одинаковые команды в одном и том же порядке, применение этих команд к соответствующему конечному автомату обеспечивает одинаковый вывод. Raft упрощает получение консенсуса, создавая понятие Лидирующего первокласного гражданина. Лидер применяется для координации манипуляций и репликаций конечного автомата. Имеется множество сходств между Raft и алгоритмами атомарного широковещания, а также Множественного Paxos: некий обособленный Лидер всплывает из репликаций, принимает атомарные решения и устанавливает порядок сообщений.

Каждый участник Raft может выполнять одну из трёх ролей:

Кандидат

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

Лидер

Некий текущий, временный Лидер кластера, который обрабатывает запросы клиента и взаимодействует с неким реплицируемым конечным автоматом. Этот лидер выбирается на период, именуемый сроком (term). Всякий срок идентифицируется монотонно приращиваемым числом и может продолжаться некий произвольный период времени. Новый Лидер выбирается в случае крушения текущего, когда он перестаёт отвечать, либо в других процессах имеется ожидание того что он должен отказать, что может происходить по причине разбиения сетевой среды на разделы или задержек сообщений.

Последователь

Некий пассивный участник, который сохраняет регистрационные записи и откликается на запросы от установленного Лидера и Кандидатов. Последователь в Raft это некая роль аналогичная Получателю и Обучаемому из Paxos. Все процессы начинают с Последователей (Follower).

Чтобы гарантировать глобальное частичное упорядочение без того чтобы полагаться на синхронизацию часов, время подразделяется на сроки (также называемые эпохами), на протяжении которых установленный Лидер является уникальным и стабильным. Сроки монотонно нумеруются и все команды уникально идентифицируются сроком и соответствующим номером сообщения внутри этого срока [HOWARD14].

Может так получиться, что различные участники не пришли к согласию какой из сроков является текущим, поскольку они могли обнаружить соответствующий новый срок в разное время или могли пропустить выборы Лидера для одного из множества сроков. Так как всякое сообщение содержит некий идентификатор срока, когда один из участников обнаруживает что его срок не актуален, он обновляет свой срок на имеющий более высокий номер [ONGARO14]. Это означает, что могут иметься на лету несколько сроков в любой определённый момент времени, однако в случае некого конфликта побеждает тот, у которого более высокий номер. Некий узел обновляет свой срок только когда он запускает новый процесс выборов или обнаруживает что его срок не актуален.

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

Рисунок 14-10 показывает диаграмму последовательности, представляющую все основные компоненты обсуждаемого алгоритма Raft:

Выбор Лидера

Кандидат P₁ отправляет сообщение RequestVote всем прочим процессам. Это сообщение содержит значение срока кандидата, самый последний известный ему срок и значение идентификатора самой последней наблюдавшейся им регистрационной записи. После сбора большинства голосов этот Кандидат успешно избирается в качестве Лидера на даннй срок. Все процессы отдают свой голос не более чем за одного кандидата.

Периодические сердцебиения

Данный протокол пользуется механизмом сердцебиений для обеспечения необходимой жизнеспособности участников. Сам Лидер периодически отправляет сердцебиения всем Последователям для сопровождения своего срока. Когда Последователь не получает новых сердцебиений на протяжении некого периода, носящего название таймаута выборов, он предполагает что его Лидер отказал и запускает новые выборы.

Репликация журнала/ широковещание

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

Рисунок 14-10

Резюме алгоритма согласия Raft

Роль лидера в Raft

Некий Лидер может быть выбран только из тех узлов, которые хранят все фиксированные записи: если в процессе данных выборов сведения соответствующего Последователя более актуальны (иначе говоря, имеют более высокий идентификатор срока или более длинную последовательность зарегистрированных записей, когда сроки равны) чем у рассматриваемого Кандидата, его голос отрицательный.

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

Будучи избранным, этот Лидер обязан принимать запросы клиентов (которые также могут передаваться ему с прочих узлов) и реплицировать их своим Последователям. Это осуществляется добавлением в конец его журнала соответствующей записи и её параллельной отправки всем Последователям.

Когда Последователи получают некое сообщение AppendEntries, они добавляют все записи из этого сообщения в конец своего локального журнала, давая своему Лидеру знать что они приняли их на постоянное хранение. Как только достаточное число реплик отправило свои подтверждения, данная запись рассматривается фиксированной и соответствующим образом помечается в журнале установленного Лидера.

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

Рисунок 14-11 показывает данный процесс:

В конец журнала установленного Лидера добавляется новая команда x = 8.

Прежде чем это значение может быть фиксировано, его следует реплицировать в большинство участников.

Как только установленный Лидер покончит с репликациями, он фиксирует это значение локально.

Решение о данной фиксации реплицируется всем Последователям.

Рисунок 14-11

Процедура фиксации в Raft с P₁ в качестве лидера

Рисунок 14-12 показывает некий образец раунда консенсуса, в котором P₁ выступает имеющим наиболее последнее представление событий Лидером. Этот Лидер продолжает реплицирование всех записей своим Последователям и фиксирует их после сбора подтверждений. Фиксация некой записи также фиксирует все предшествующие ей записи в соответствующем журнале. Только установленный Лидер способен принимать решение по вопросу будет или нет фиксирована данная запись. Все регистрируемые записи помечаются идентификатором срока (числом в правом верхнем углу каждого блока регистрационной записи) и неким индексом регистрации, указывающим его положение в данном журнале. Фиксированные записи гарантированно реплицируются в установленный кворум участников и безопасно применяются к установленному конечному автомату именно в том порядке, в котором они возникают в соответствующем журнале.

Рисунок 14-12

Машина состояний Raft

Сценарии отказов

Когда множество Последователей принимают решение выступить Кандидатами и никакой из Кандидатов не набирает большинства голосов, такая ситуация носит название разделения голосов (split vote). Для снижения значения вероятности множества последовательных выборов, заканчивающихся разделением голосов, Raft применяет выставляемые случайным образом флажки часов. Один из имеющихся Кандидатов может запустить свои следующие выборы раньше и набрать достаточное число голосов, в то время как прочие проспят и предоставят дорогу ему. Такой подход ускоряет подобные выборы без необходимости дополнительно координации между кандидатами.

Последователи могут останавливаться или замедляться с откликом, и установленному Лидеру придётся предпринять всё от него зависящее чтобы обеспечить доставку сообщения. Он может попробовать отправку сообщения снова когда не получает некого подтверждения за ожидаемый по времени срок . В качестве оптимизации производительности, он может отправлять параллельно множество сообщений.

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

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

Raft гарантирует, что никогда не будет отображаться никакое не зафиксированное сообщение в качестве фиксированного, но, по причине замедления сетевой среды или репликации, уже зафиксированные сообщения могут всё ещё рассматриваться как пребывающие в процессе выполнения, что является достаточно безопасным качеством, причём его можно обойти, повторяя команду клиента до её окончательной фиксации [HOWARD14].

Для выявления отказов установленный Лидер обязан отправлять своим Последователям сердцебиения. Таким образом установленный Лидер поддерживает свой срок. Когда один из узлов замечает что его текущий Лидер остановился, он пытается инициировать новые выборы. Вновь установленному Лидеру приходится восстанавливать значение состояния своего кластера до самой последней известной актуальной записи журнала. Это осуществляется через поиск общей земли (common ground, самой наивысшей записи, по которой имеют согласие и этот Лидер и Последователь) и ориентирует Последователей отвергать все (не зафиксированные) записи, добавляемые в конец с этого момента. Затем он отправляет самые последние записи из своего журнала, перезаписывая историю своих Последователей. Собственные записи журнала установленного Лидера никогда не удаляются и не перезаписываются: он имеет возможность только добавлять записи в конец своего журнала.

Итого наш алгоритм Raft предоставляет следующие гарантии:

За раз для заданного срока может выбираться лишь один Лидер; никакие два Лидера не могут быть активными на протяжении одного и того же срока.

Установленный лидер не имеет возможности удалять содержимое в своём журнале или изменять его порядок; он способен только добавлять в его конец новые записи.

Фиксированные записи регистрации гарантированно присутствуют в журналах для последующих Лидеров и не могут отыгрываться обратно, так как до того как данная запись зафиксирована, известно что она реплицирована установленным Лидером.

Все сообщения уникально идентифицируются идентификаторами сообщения и срока; ни текущие, ни последующие Лидеры не могут повторно применять один и тот же идентификатор для иной записи.

С момента своего появления Raft превратился в очень популярный и в настоящее время применяется во множестве баз данных и прочих распределённых систем, включая CockroachDB, Etcd и Consul. Это можно связать с его простотой, но также может означать и то, что Raft оправдывает ожидания стать надёжным алгоритмом консенсуса.

Византийский консенсус

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

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

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

Мы обсудим здесь только один алгоритм Византийского консенсуса, Практическую Задачу Византийских генералов (pBFT, Practical Byzantine Fault Tolerance). [CASTRO99]. pBFT предполагает независимость отказов узлов (т.е. отказы не скоординированы, но вся система целиком не может быть захвачена одномоментно или, по крайней мере, одним и тем же применяемым методом). Такая система делает слабые предположения о синхронизации, например, что вы можете ожидать в качестве обычного поведения сетевой среды: сбои могут появляться, но они не являются неопределённо долгими и в конечном счёте восстанавливаются.

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

Алгоритм pBFT

Для того чтобы pBFT обеспечивал как безопасность, так и живучесть, отказавшими может быть не более (n - 1)/3 реплик (где n это общее число участников). Для того чтобы система переносила f скомпрометированных узлов, ей требуется иметь по крайней мере n = 3f + 1 узлов. Это именно так, ибо большинству узлов необходимо прийти к согласию по данному значению: может оказать f реплик и может иметься f реплик, которые не отвечают, но могут не являться отказавшими (например, по причине сетевого разделения, отказа питания или сопроводительных работ). Данный алгоритм должен быть способным собирать достаточное число откликов от не отказавших, но всё ещё численно превосходящих отказавшие.

Свойства консенсуса pBFT аналогичны свойствам прочих алгоритмов консенсуса: все не отказавшие реплики обязаны согласовывать как сам набор полученных значений, так и их порядок, причём несмотря на возможные отказы.

Для различения конфигураций кластера pBFT применяет представления (view). В каждом представлении одна из реплик выступает первичной, а все остальные рассматриваются как резервные копии. Все узлы последовательно нумеруются и значением индекса первичного узла выступает v mod N, где v это значение идентификатора представления, а N это общее число узлов в данной текущей конфигурации. Данное представление может измениться в случаях падения своего первичного. Клиенты выполняют свои операции для установленного первичного. Этот первичный осуществляет широковещание соответствующих запросов резервным копиям, которые исполняют такие запросы и отправляют некий отклик обратно своему клиенту. Соответствующий клиент дожидается отклика от f + 1 реплик с одним и тем же результатом для успеха каждой операции.

После того как первичный узел получает некий запрос клиента, протокол исполнения следует трём фазам:

Предварительная подготовка

Установленный первичный узел широковещательно отправляет сообщения, содержащие некий идентификатор представления, уникального и монотонно растущего, некую полезную нагрузку (запрос клиента) и свёртку (digest) полезной нагрузки. Свёртка вычисляется при помощи сильной устойчивой к конфликтам функции хэширования и подписываются этим отправителем. Соответствующая резервная копия принимает данное сообщение когда его представление совпадает с представлением первичного и сам запрос клиента не был испорчен: вычисленная свёртка полезной нагрузки совпадает с полученной.

Подготовка

Если резервная копия получила сообщение предварительной подготовки, оно вступает на этап подготовки и запускает широковещательные сообщения Prepare, содержащие идентификатор представления, идентификатор сообщения и свёртку полезной нагрузки, но без самой полезной нагрузки, во все прочие реплики (включая и саму первичную). Реплики могут выходить из этого состояния Подготовки только когда получили 2f Prepare от различных резервных копий, которые совпадают с сообщением, полученным на этапе Предварительной подготовки: они обязаны иметь то же самое представление, тот же самый идентификатор и ту же самую свёртку.

Фиксация

После этого соответствующая резервная копия перемещается на этап Фиксации, на котором она широковещательно распространяет сообщения Commit всем прочим репликам и дожидается сбора 2f + 1 совпадающих сообщений Commit (возможно, включающих и её собственное) от прочих участников.

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

Значение числа 2f является важным, так как этому алгоритму требуется убедиться что клиенту запроса откликнулось по крайней мере f + 1 реплик.

Рисунок 14-13 показывает схему последовательности раунда алгоритма pBFT для обычного случая: рассматриваемый клиент отправляет запрос в P₁ а узлы перемещаются по этапам собирая достаточное число совпадающих откликов от ведущих себя как положено одноранговых партнёров. P₄ может оказаться отказавшим или может откликаться несовпадающими сообщениями, поэтому его отклики не будут приниматься в расчёт.

Рисунок 14-13

Консенсус pBFT, операции нормального случая

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

После того как реплики собрали достаточное число фиксированных сообщений, они уведомляют своего клиента, завершая этот раунд. Их клиент не может быть уверен относительно полностью корректного исполнения пока он не получит f + 1 совпадающих откликов.

Изменения представления происходят когда реплики замечают, что их первичная является не активной и они полагают что оно могла отказать. Обнаружившие отказ первичной реплики узлы перестают откликаться на последующие сообщения (кроме контрольной точки и тех что связаны с изменением представления), широковещательно транслируют уведомление об изменении представления и дожидаются подтверждений. Когда устанавливаемая новым представлением первичная реплика получает 2f событий изменения представления, она инициирует новое представление.

Для снижения общего числа сообщений в данном протоколе клиент может собирать 2f + 1 совпадающих откликов от узлов, которые в предварительном порядке выполняют какой- то запрос (т.е. после того как они собрали достаточное число совпадающих сообщений Prepared). Когда клиент не способен собрать достаточного числа совпадающих предварительных откликов, он повторяет попытку и дожидается f + 1 откликов без предварительного порядка, как это описывалось ранее.

Требующие только чтения операции в pBFT могут выполняться только проходом в одну сторону. Соответствующий клиент отправляет некий запрос во все реплики. Реплики выполняют этот запрос в своих состояниях предварительного порядка, затем фиксируются все получаемые на выходе состояния и отправляется отклик клиенту. После сбора 2f + 1 откликов с одним и тем же значением от различных реплик данная операция завершается.

Восстановление и установка контрольных точек

Реплики сохраняют принимаемые сообщения в неком стабильном журнале. Все сообщения должны сохраняться пока они не выполнятся по крайней мере f + 1 узлами. Данный журнал может применяться для ускорения прочих реплик в случае разбиения сети на разделы, однако восстановленным репликам требуется некое средство проверки того что полученное ими состояние правильное, так как в противном случае восстановление может применяться как некое направление атаки.

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

После каждых N запросов, где N это настраиваемая постоянная, установленная первичная реплика выполняет стабильную контрольную точку, в которой она широковещательно отправляет все последовательные номера самых последних запросов, исполнение которых отражается в данном состоянии и значение свёртки данного состояния. Затем она дожидается 2f + 1 отклик в ответ. Эти ответы служат доказательством для данной контрольной точки и гарантией того, что реплики могут быть безопасно отбрасывать для данного порядкового номера состояние для всех сообщений предварительной подготовки, подготовки, фиксации и контрольной точки.

Отказоустойчивость к Византийским отказам (Byzantine fault tolerance, BFT, Задача Византийских генералов) существенна для понимания и применения систем хранения в потенциально состязательных сетевых средах. В большинстве случаев для этого достаточно для аутентификации и шифрования межузлового взаимодействия, однако когда нет доверительных отношений между установленными частями общей системы, необходимо пользоваться аналогичными pBFT алгоритмами.

Поскольку устойчивые к Византийским отказам алгоритмы выставляют значительные накладные расходы на количество сообщений при обмене, важно понимать их варианты использования. Прочие протоколы, такие как [BAUDET19] и [BUCHMAN18] пытаются оптимизировать алгоритм pBFT для систем с большим числом участников.

Выводы

Алгоритмы консенсуса являются одним из самых интересных, но в то же самое время самым сложным предметом в распределённых системах. На протяжении последних лет появились новые алгоритмы и множество реализаций имеющихся алгоритмов, что доказывает растущую важность и популярность данного предмета.

В этой главе мы обсудили классический алгоритм Paxos и ряд вариантов Paxos, каждый из которых улучшает его различные свойства:

Multi-Paxos

Позволяет Заявителю удерживать свою роль и реплицировать множество значения вместо только одного.

Быстрый Paxos

Делает возможным снижение числа сообщений применяя быстрые раунды когда Получатели могут продолжать обмен сообщениями с Заявителем, отличающимся от установленного им Лидера.

ЕPaxos

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

Гибкий Paxos

Ослабляет требования кворума и запрашивает лишь пересечения кворума своего первого этапа (голосования) с кворумом для своего второго этапа (репликации).

Raft упрощает выражения в которых описвается консенсус и создаёт руководство высококлассного гражданина в своём алгоритме. Raft разделяет репликацию журнала, выбор Лидера и безопасность.

Для обеспечения безопасного консенсуса в состязательных средах следует пользоваться устойчивыми к Византийским отказам алгоритмами; например, pBFT. В pBFT участники перекрёстно проверяют прочие отклики и продолжают исполнение только когда существует достаточное число узлов, которые подчинены предварительно описанным правилам алгоритма.

[Замечание]
Последующее чтение

Если вы желаете узнать больше относительно упоминавшихся в этой главе понятий, вы можете ссылаться на следующие источники:

Атомарное широковещание

Junqueira, Flavio P., Benjamin C. Reed, and Marco Serafini. “Zab: Highperformance broadcast for primary-backup systems.” 2011. In Proceedings of the 2011 IEEE/IFIP 41st International Conference on Dependable Systems & Networks (DSN ’11): 245-256.

Hunt, Patrick, Mahadev Konar, Flavio P. Junqueira, and Benjamin Reed. 2010. “ZooKeeper: wait-free coordination for internet-scale systems.” In Proceedings of the 2010 USENIX conference on USENIX annual technical conference (USENIX-ATC’10): 11.

Oki, Brian M., and Barbara H. Liskov. 1988. “Viewstamped Replication: A New Primary Copy Method to Support Highly-Available Distributed Systems.” In Proceedings of the seventh annual ACM Symposium on Principles of distributed computing (PODC ’88): 8-17ю

Van Renesse, Robbert, Nicolas Schiper, and Fred B. Schneider. 2014. “Vive la Différence: Paxos vs. Viewstamped Replication vs. Zab.”.

Классический Paxos

Lamport, Leslie. 1998. “The part-time parliament.” In ACM Transactions on Computer Systems, no. 2 (May): 133-169.

Lamport, Leslie. 2001. “Paxos made simple.” In ACM SIGACT News 32, no. 4: 51-58.

Lamport, Leslie. 2005. “Generalized Consensus and Paxos.” In Technical Report MSR-TR-2005-33. Microsoft Research, Mountain View, CA.

Primi, Marco. 2009. “Paxos made code: Implementing a high throughput Atomic Broadcast.” (Libpaxos code).

Быстрый Paxos

Lamport, Leslie. 2005. “Fast Paxos.” 14 July 2005. Microsoft Research.

Multi- Paxos

Chandra, Tushar D., Robert Griesemer, and Joshua Redstone. 2007. “Paxos made live: an engineering perspective.” In Proceedings of the twenty-sixth annual ACM symposium on Principles of distributed computing (PODC ’07)): 398-407.

Van Renesse, Robbert and Deniz Altinbuken. 2015. “Paxos Made Moderately Complex.” In ACM Computing Surveys)47, no. 3 (February): Article 42. https://doi.org/10.1145/2673577.

EPaxos

Moraru, Iulian, David G. Andersen, and Michael Kaminsky. 2013. “There is more consensus in Egalitarian parliaments.” In Proceedings of the Twenty-Fourth ACM Symposium on Operating Systems Principles (SOSP ’13)): 358-372.

Moraru, I., D. G. Andersen, and M. Kaminsky. 2013. “A proof of correctness for Egalitarian Paxos.” Technical report, Parallel Data Laboratory, Carnegie Mellon University, Aug. 2013.

Raft

Ongaro, Diego, and John Ousterhout. 2014. “In search of an understandable consensus algorithm.” In Proceedings of the 2014 USENIX conference on USENIX Annual Technical Conference (USENIX ATC’14)), Garth Gibson and Nickolai Zeldovich (Eds.): 305-320.

Howard, H. 2014. “ARC: Analysis of Raft Consensus.” In Technical Report UCAM-CL-TR-857), University of Cambridge, Computer Laboratory, July 2014.

Howard, Heidi, Malte Schwarzkopf, Anil Madhavapeddy, and Jon Crowcroft. 2015. “Raft Refloated: Do We Have Consensus?” In SIGOPS Operating Systems Review) 49, no. 1 (January): 12-21. https://doi.org/10.1145/2723872.2723876.

Последние разрабтки

Howard, Heidi and Richard Mortier. 2019. “A Generalised Solution to Distributed Consensus.” 18 Feb 2019.

Заключение Части II

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

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

Часть II начинается с обсуждения того чем распределённые системы отличаются от приложений для одного узла и какие трудности можно ожидать в подобных средах.

Мы обсудим все основные строительные блоки базовай распределённой системы, различные согласованные моделей, а также различные важные классы распределённых алгоритмов, причём некоторые из них могут применяться для реализации таких непротиворечивых моделей:

Выявление отказов

Точно и действенно определяет отказы удалённых процессов.

Выбор лидера

Быстро и надёжно выбирает единственный процесс для временного обслуживания в роли координатора.

Рассеяние

Надёжно распространяет сведения при помощи однорангового взаимодействия.

Анти- энтропия

Выявляет и восстанавливает состояние расхождения между имеющимися узлами.

Распределённые транзакции

Атомарно выполняет последовательности операций для множества разделов.

Консенсус

Достигает некого согласия между удалёнными участниками, оставаясь безразличным к отказу процессов.

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

[Замечание]
Последующее чтение

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

Системы баз данных

Bernstein, Philip A., Vassco Hadzilacos, and Nathan Goodman. 1987. Concurrency Control and Recovery in Database Systems. Boston: Addison-Wesley Longman.

Korth, Henry F. and Abraham Silberschatz. 1986. Database System Concepts. New York: McGraw-Hill.

Gray, Jim and Andreas Reuter. 1992. Transaction Processing: Concepts and Techniques(1st Ed.). San Francisco: Morgan Kaufmann.

Stonebraker, Michael and Joseph M. Hellerstein (Eds.). 1998. Readings in Database Systems (3rd Ed.). San Francisco: Morgan Kaufmann.

Weikum, Gerhard and Gottfried Vossen. 2001. Transactional Information Systems: Teory, Algorithms, and the Practice of Concurrency Control and Recovery. San Francisco: Morgan Kaufmann.

Ramakrishnan, Raghu and Johannes Gehrke. 2002. Database Management Systems (3 Ed.). New York: McGraw-Hill.

Garcia-Molina, Hector, Jeffrey D. Ullman, and Jennifer Widom. 2008. Database Systems: Te Complete Book (2 Ed.). Upper Saddle River, NJ: Prentice Hall.

Bernstein, Philip A. and Eric Newcomer. 2009. Principles of Transaction Processing (2nd Ed.). San Francisco: Morgan Kaufmann.

Elmasri, Ramez and Shamkant Navathe. 2010. Fundamentals of Database Systems (6th Ed.). Boston: Addison-Wesley.

Lake, Peter and Paul Crowther. 2013. Concise Guide to Databases: A Practical Introduction. New York: Springer.

Härder, Theo, Caetano Sauer, Goetz Graefe, and Wey Guy. 2015. Instant recovery with write-ahead logging. Datenbank-Spektrum.

Распределённые системы

Lynch, Nancy A. Distributed Algorithms. 1996. San Francisco: Morgan Kaufmann.

Attiya, Hagit, and Jennifer Welch. 2004. Distributed Computing: Fundamentals, Simulations and Advanced Topics.. Hoboken, NJ: John Wiley & Sons.

Birman, Kenneth P. 2005. Reliable Distributed Systems: Technologies, Web Services, and Applications. Berlin: Springer-Verlag.

Cachin, Christian, Rachid Guerraoui, and Lus Rodrigues. 2011. Introduction to Reliable and Secure Distributed Programming (2nd Ed.). New York: Springer.

Fokkink, Wan. 2013. Distributed Algorithms: An Intuitive Approach. The MIT Press.

Ghosh, Sukumar. Distributed Systems: An Algorithmic Approach (2nd Ed.). Chapman & Hall/CRC.

Tanenbaum Andrew S. and Maarten van Steen. 2017. Distributed Systems: Principles and Paradigms (3rd Ed.). Boston: Pearson

Работа баз данных

Beyer, Betsy, Chris Jones, Jennifer Petoff, and Niall Richard Murphy. 2016 Site Reliability Engineering: How Google Runs Production Systems (1st Ed.). Boston: O’Reilly Media.

Campbell, Laine and Charity Majors. 2017. Database Reliability Engineering: Designing and Operating Resilient Database Systems (1st Ed.). Boston: O’Reilly Media.

Blank-Edelman, David N. 2018. Seeking SRE. Boston: O’Reilly Media.

Sridharan, Cindy. 2018. Distributed Systems Observability: A Guide to Building Robust Systems. Boston: O’Reilly Media.

Часть 2. Распределённые системы

Распределённая система это такая штука, в которой отказ компьютера, о существовании которого вы даже не знаете, может привести ваш собственный компьютер в недоступное состояние.

- Лесли Лампорт

Без распределённых систем мы не были бы способны делать телефонные звонки, переводить деньги или обмениваться сведениями на больших расстояниях. Мы ежедневно пользуемся распределёнными системами. Порой даже не имея представления о них: любое приложение клиент/ сервер является распределённой системой.

Для многих современных систем вертикальное масштабирование (масштабирование за счёт запуска того же самого программного обеспечения на большей, более быстрой машине с большим числом ЦПУ, ОЗУ или быстрых дисков) не жизнеспособно. Большие машины более дороги, их сложнее менять, а многие требуют особого сопровождения. Некой альтернативой является горизонтальное масштабирование: запуск программного обеспечения на множестве машин, соединённых через их сетевую среду и работающих как единая логическая сущность.

Распределённые системы могут различаться как в размерах, начиная с нескольких штук до сотен машин, так и по характеристикам их участников, от малых наладонных или сенсорных устройств до высокопроизводительных компьютеров.

Те времена, когда системы баз данных в основном запускались на отдельном узле давно ушли, и большинство современных баз данных имеют множество соединённых в кластер узлов для роста ёмкости хранения, улучшения производительности и расширения доступности.

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

  Основные определения

В распределённой системе у нас имеются участники (порой носящими название процессов, узлов или реплик). Каждый участник обладает своим собственным состоянием. Участники взаимодействуют обменом сообщений, применяя соединения между ними.

Процессы могут получать доступ ко времени при помощи часов, которые могут быть логическими или физическими. Логические часы реализуются при помощи некого вида монотонно растущих счётчиков. Физические часы, также носящие название ходиков (wall clock) ограничены представлением времени физического мира и доступ к ним осуществляется через средства локального процесса; например, через некую операционную систему.

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

Большинство исследований на полях распределённых систем связано с тем фактом, что ничто полностью не надёжно: каналы взаимодействия могут иметь задержки, выполняться повторно, либо выдавать отказы в доставке сообщений; процессы способны делать паузу, останавливаться, испытывать крушение, выходить из под контроля или внезапно переставать отвечать.

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

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

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

Распределённые алгоритмы обслуживают множество различных целей:

Координация

Некий процесс, который наблюдает за происходящими действиями и поведением обособленных исполнителей.

Кооперация

Множество участников полагаются друг на друга для завершения своих задач.

Рассеяние

Процессы кооперируют в быстром и надёжном распространении необходимых сведений для всех заинтересованных сторон.

Консенсус

достижение согласия между множеством процессов.

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

Глава 8. Введение и обзор

Содержание

Глава 8. Введение и обзор

Одновременное исполнение

Разделяемое состояние в распределённой системе

Хитрости распределённых вычислений

Обработка

Такты и время

Согласованность состояния

Локальное и удалённое исполнение

Потребность обработки отказов

Сетевые разделы и частичные отказы

Каскадные отказы

Абстракции распределённых систем

Соединения

Справедливо утрачиваемые соединения

Подтверждения сообщений

Повторная передача сообщений

Проблемы с повторной передачей сообщений

Порядок сообщений

В точности одна доставка

Проблема двух генералов

Невозможность FLP

Синхронность систем

Модели отказов

Отказы крушения

Отказы пропуска

Произвольные отказы

Обработка отказов

Выводы

Что делает распределённые системы внутренне отличающимися от систем с единственным узлом? Давайте рассмотрим некий простой пример и попытаемся увидеть. В некой программе единственного узла мы определяем переменные и сам процесс исполнения (некий набор шагов).

Допустим, мы можем задать некую переменную и выполнить с ней простые арифметические операции:

int i = 1;
i += 2;
i *= 2;

У нас имеется единственная история исполнения: мы определили некую переменную, увеличили её на два, затем умножили её на два и получили соответствующий результат: 6. Давайте допустим, что вместо наличия одного потока исполнения этих операций, у нас имеются два потока, которые должны иметь доступ на чтение и запись к переменной x.

Одновременное исполнение

Как только двум потокам исполнения дозволено выполнять доступ к имеющейся переменной, точный выход имеющегося одновременного пошагового исполенения непредсказуем, пока эти шаги не синхронизованны между данными потоками. Вместо единственного возможного вывода мы приходим к четырём, что отображает Рисунок 8-1 (Перекрытие, при котором наш умножитель выполняет считывпие перед сумматором, опущено для краткости, так как оно приводит к тому же результату что и a)).

Рисунок 8-1

Возможные перекрытия одновременных исполнений

x = 2, если оба потока считали некое начальное значение, сумматор записал своё значение, но его переписал результат умножения.

x = 3, если оба потока считали некое начальное значение, умножитель записал своё значение, но его переписал результат сложения.

x = 4, если умножитель может считать имеющееся начальное значение и выполнить свою операцию, причём прежде чем начнёт сумматор.

x = 6, если сумматор может считать имеющееся начальное значение и выполнить свою операцию, причём прежде чем начнёт умножитель.

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

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

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

[Замечание]
Одновременное и параллельное

Мы часто взаимозаменяемо применяем вычислительные термины одновременно и параллельно, однако эти понятия обладают небольшим семантическим отличием. Когда две последовательности шагов выполняются одновременно, обе они пребывают в своём развитии, но только одна из них выполняется в некий момент времени. Когда две последовательности выполняются параллельно, их шаги выполняются совместно. Одновременные операции перекрываются по времени, в то время как параллельные операции исполняются на множестве процессоров [WEIKUM01].

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

Разделяемое состояние в распределённой системе

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

Для доступа к этой базе данных, процессам придётся следовать через установленную среду взаимодействия отправкой и получением сообщений для запросов или изменения имеющегося состояния. Тем не менее, что произойдёт если если один из процессов не поличит некого отклика от этой базы данных за продолжительное время? Чтобы ответить на этот вопрос, мы должны вначале определить что в точности означает продолжительно. Для этого наша система должна описываться в терминах синхронности: будет ли наше взаимодействие полностью асинхронным, или же будут некие предположнения относительно времени. Такие предположения о времени позволяют нам вводить операции таймаутов и повторов.

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

Некое свойство, которое описывет надёжность системы и то, способна или нет она продолжать правильно работать в присутствии отказов носит название отказоустойчивости (fault tolerance). Отказы неизбежны, поэтому нам необходимо строить системы с надёжными компонентами и исключать некую единую точку отказа в том виде, как уже упоминавшаяся ранее база данных с единственным узлом, может стать самым первым шагом в этом направлении. Мы можем делать это вводя некую избыточность и добавляя какую- то резервную копию базы данных. Однако теперь мы сталкиваемся с другой проблемой: как нам сохранять множество копий разделяемого состояния в процессе синхронизации?

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

Хитрости распределённых вычислений

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

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

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

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

Мишель Льюис в своей книге Flash Boys (Simon and Schuster) повествуют историю о компании, которая потратила миллионы долларов на снижение задержки в несколько миллисекунд чтобы быть способными к более быстрому доступу к рынку чем их конкуренты. Это прекрасный пример применения задержки в качестве конкурентного преимущества, однако стоит отметить, что согласно некоторым исследованиям, таким как [BARTLETT16], сама возможность скупки и продажи устаревающих котировок (возможности получения прибыли от способности обладания ценами и исполнения заказов быстрее своих конкурентов) не даёт возможностям трейдерам эксплуатировать рынки.

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

[Замечание]
Замечание

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

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

Обработка

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

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

Узлы могут располагаться ближе или дальше друг от друга, иметь различные ЦПУ, объёмы ОЗУ, отличающиеся диски или же запускать различные версии и конфигурации программного обеспечения. Мы не можем ожидать от них обработки запросов с одной и той же скоростью. Когда нам приходится дожидаться пока отдельные удалённые серверы работают параллельно над откликом всей нашей задачи, само исполнение в целом настолько же медленно, насколько не быстр наш удалённый сервер.

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

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

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

Разъединение

Приём и обработка разделены по времени и происходят независимо.

Конвейеризация

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

Поглощение кратковременных всплесков

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

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

Имеющийся удалённый сервер может работать над запросами быстро, но это не означает, что мы всегда получаем от него положительный отклик. Он может отвечать и отказом: он не способен выполнить запись,искомое значени отсутствует или же он может столкнуться с некой ошибкой. Суммируя, даже наиболее предпочтительный сценарий всё ещё требует некого внимания с нашей стороны.

Такты и время

Время призрачно. Обеденное время вдвойне.

- Форд Префект, Руководство Хитчхикера по Галактике

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

Существенно всегда исчислять возможные временные отличия между имеющимися процессами и то время, которое необходимо для доставки и обработки самих сообщений. Например, Spanner (см. раздел Распределённые транзакции при помощи Spanner) применяет специальный API времени, который возвращает некую временную отметку и несомненные границы для выставления строгогопорядка транзакции. Некоторые алгоритмы выявления отказов полагаются на разделяемое представлени времени и гарантии того, что расхождение часов всегда в допустимых для верности границах [GUPTA01].

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

Также помогает понимание того, что ваш источник часов монолитен (т.е. что он никогда не движется в обратном направлении) и то, насколько способны расходиться ваши запланированные по времени операции, связанные со временем.

Согласованность состояния

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

Распределённые алгоритмы не всегда гарнтируют толчное соответсивие состояния. Некоторые подходы обладают обладают утрачиваемыми ограничениями и допускают расхождение между репликами, полагаясь на разрешение конфликтов (некую возможность выявления и разрешения расхождения состояний внутри данной системы) и восстановления данных в реальном времени (приведение реплик обратно в синхронное состояние в процессе считываний при ситуациях, когда они отвечают с различными результатами). Дополнительные сведения об этом понятии вы можете найти в Главе 12. Предположение что данное состояние полностью согласовано по всем узлам может приводить к едва уловимым ошибкам.

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

Например, в Apache Cassandra имелась некая ошибка, вызываемая тем, что изменения схемы распротсранялись в серверы в различное время. Когда вы пытаетесь выполнять чтение из этой базы данных во время распространения данной схемы, имелась вероятность разрушения, так как один сервер кодировал результаты в предположении одной схемы, а другой декодировал их применяя иную схему.

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

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

Локальное и удалённое исполнение

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

Простое сокрытие их обоих за одним и тем же интерфейсом, хотя и полезно, может сбивать с толку. Для отладки, настройки и наглядности могут потребоваться дополнительные параметры API. Нам всегда следует держать в уме что локальное и удалённое исполнение это не одно и то же [WALDO96].

Наиболее важной видимй проблемой для сокрытия удалённых вызовов являются задержки: удалённые вызовы во много раз более дороги нежели локальные, поскольку они содержат транспортировку в обе стороны, сериализацию/ распаковку, а также множество иных шагов. Чередование локальных и блокирование удалённых вызовов могут приводить к деградации производительности и непреднамеренным сторонним эффектам [VINOSKI08].

Потребность обработки отказов

Это отлично начинать работать в некой системе в пердположении что все узлы подняты и работают нормально, однако представление о том, что это имеет место всегда чревато опасностями. В длительно работающих системах узлы могут отключаться на обслуживание (которое обычно происходит через деликатное отключение) или же испытывать крушение по различным причинам: проблемы с программным обеспеченеим, уничтожения нехваткой памяти [KERRISK10], ошибки времени исполнения, проблемы с оборудованием и т.п. Процессы реально падают, и лучшее что вы можете предпринять, это подготовиться к таким отказам для их обработки.

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

Сетевые разделы и частичные отказы

Когда два или более серверов не способны взаимодействовать друг с другом, мы называем такое положение дел сетевым разделением. В "Перспективах теоремы CAP" [GILBERT12], Сет Гилберт и Нэнси Линч провели различие между случаем когда два участника не могут взаимодействовать друг с другом и когда различные группы участников изолированы друг от друга, не способны обмениваться сообщениями и следовать своему алгоритму.

Общая ненадёжность сетевой среды (потеря пакетов, повторные передачи, трудно предсказуемые задержки) являются досадными но терпимыми, в то время как сетевые разделы могут вызывать намного больше проблем, поскольку независимые группы могут продолжать своё выполнение и производить вступающие в конфликт результаты. Сетевые соединения к тому же способны отказывать ассиметрично: сообщения могут всё ещё доставляться от одного процесса к другому, но не наоборот.

Для построения некой устойчивой к наличию отказов одного или множества процессов системы, нам придётся рассматривать варианты частичных отказов [TANENBAUM06] и того как эта система может продолжать работу даже несмотря на то что её часть недоступна или функционирует неверно.

Отказы сложно выявлять и они не всегда видятся одними и теми же из различных частей одной системы. При проектировании некой системы с высокой доступностью, всегда следует задумываться о граничных случаях: что если мы реплицируем свои данные и не получаем подтверждения? Требуется ли нам повторять? Стоит ли эти данные всё ещё рассматривать как доступные для чтения в тех узлах, которые отправили подтверждения?

Закон Мёрфи (являющийся неким афоризмом, который можно суммировать следующим образом: "Нечто, что может пойти не так, пойдёт не так", которое получило популярность и часто используется как устойчивое выражение в народной культуре) говорит нам что такие отказы происходят на самом деле. Фольклор программистов добавляет, что тот отказ, который произойдёт, случится в наихудшем варианте, поэтому наше задание как инженеров распределённых систем состоит в гарантированном снижении общего числа ситуаций, при которых дела идут плохо и подготовиться к отказам в том случае, который содержит разрушения, способные провоцироваться ими.

Не возможно предотвращать все отказы, но мы всё ещё способны строить эластичную систему, которая правильно работает при их наличии. Наилучшим способом подготовки к отказам является их проверка. Почти невозможно представить себе все возможные ситуации отказов и предсказать устанавливаемое поведение множества процессов. Настройка проверки снаряжения, которое создаёт разделы, эмуляция битовой деградации [GRAY05], увеличение задержек, расхождение часов и увеличение относительных скоростей обработки это лучший способ их прохода. Установки распределённых систем реальной практики могут быть достаточно состязательными, недружественными и "креативными" (однако черезчур неприятельским способом), поэтому следует предпринять все свои усилия по проверке настолько большого числа ситуаций, насколько это возможно.

[Совет]
Совет

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

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

Каскадные отказы

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

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

[Совет]
Совет

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

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

Откат применяется для увеличения промежутков времени между запросами от отдельного клиента. Тем не менее, использование различными клиентами одной и той же стратегии отката также способно производить солидную нагрузку. Чтобы воспрепятствовать различным клиентам всем одновременно повторять попытку после соответствующего промежутка отката, мы можем вводить некое дрожжание (jitter). Дрожжание добавляет небольшие произвольные промежутки времени в откат и снижают значение вероятности просыпания клиентов в одно и то же время для повторных попыток.

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

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

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

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

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

Абстракции распределённых систем

При обсуждении языков программирования мы применяем общую терминологию и определяем свои программы в терминах функций, операторов, классов, переменных и указателей.

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

Прежде чем мы перейдём к алгоритмам, мы вначале рассмотрим словарь распределённых систем: определения, с которыми вы часто сталкиваетесь в разговорах, книгах и статьях.

Соединения

Сетевые среды не надёжны: сообщения могут теряться, задерживаться и повторяться. Теперь, имея это на уме, мы попытаемся построить отдельные протоколы взаимодействия. Мы начнём с наименее надёжных и устойчивых, указывая те состояния, в которых они способны пребывать и выводя необходимые возможные добавления для этих протоколов, которые способны предоставлять лучшие гарантии.

Справедливо утрачиваемые соединения

Мы можем начать с двух процессов, подключённых неким соединением (link). Процессы могут отправлять сообщения друг другу, как это отображено на Рисунке 8-2. Все среды взаимодействия не идеальны и сообщения могут теряться или задерживться.

Рисунок 8-2

Проостейшая, ненадёжная форма взаимодействия

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

Ещё пока не доставлено процессу B (но будет лоставлено в некий момент времени)

Безвозвратно утрачено в процессе транспортировки

Успешно доставлено удалённому процессу

Отметим, что наш отправитель не имеет никакой возможности определить что его сообщение уже доставлено. В терминологии распределённых систем такой вид соединений носит название справедливо утрачиваемых (fair- loss). Основные свойства этого вида соединений таковы:

Справедливая утрата

Если и отправитель и получатель верны, и этот отправитель сохраняет повторную передачу определённого сообщения неограниченно много раз, оно в конечном итоге будет доставлено (более точное определение состоит в том, что если верный процесс A отправляет сообщение верному процессу B неограниченно часто, оно будет доставлено неограниченно часто [CACHIN11]).

Имеющее предел дублирование

Отправленные сообщения не будут доставляться неограниченно много раз.

Никакого создания

Никакое соединение не может появиться с сообщениями; иными словами, оно не способно доставить никогда не отправленное сообщение.

Некое справедливо утрачиваемое соединение является полезной абстракцией и неким первичным строительным блоком для протоколов взаимодействия со строгими гарантиями. Мы можем предположить, что такое соединение не теряет сообщения между взаимодействующими частями систематически и не создаёт никаких новых сообщений. Но, в то же самое время мы не можем целиком полагаться на это. Это может напоминать вам протокол UDP (User Datagram Protocol), который позволяет нам отправлять сообщения от одного процесса к другому, однако не имеет сематики достоверной доставки на уровне самого протокола.

Подтверждения сообщений

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

[Замечание]
Замечание

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

Теперь процесс A может отправить некое сообщение M(n), где n это монотонно растущий счётчик сообщений. Как только B получает данное сообщение, он отправляет подтверждение ACK(n) обратно к A. Рисунок 8-3 отображает данную форму взаимодействия.

Рисунок 8-3

Отправка сообщения с неким подтверждением

Такое подтверждение, также как и само первоначальное сообщение, способно быть утраченным на своём пути. Общее число состояний данного сообщения может слегка измениться. Пока A не получит некого подтверждения, это сообщение всё ещё пребывет в одном из трёх упомянутых ранее состояний, однако как только A получает соответствующее подтверждение, он может быть уверен что это сообщение доставлено B.

Повторная передача сообщений

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

После того как процесс A отправил сообщение M, он ожидает пока не включится таймаут T и пытается отправить то же самое сообщение снова. В предположении что данное соединение между процессами всё ещё остаётся невредимым, сетевые разделы между этими процессами не являются безграничными и не все пакеты теряются, мы можем постулировать, что с точки зрения отправителя, это сообщение либо ещё пока не доставлено процессу B, либо оно успешно доставлено процессу B. Так как A пытается отправить это сообщение, мы можем сказать, что это не может стать невосполнимой утратой в процессе транспортировки.

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

Проблемы с повторной передачей сообщений

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

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

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

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

Порядок сообщений

Ненадёжные сети являют нам две задачи: сообщения могут приходить не в установленном порядке и, по причине повторов, некоторые сообщения могут возникать более одного раза. Мы уже ввели последовательные номера и мы можем пользоваться этими идентификаторами сообщений на принимающей стороне для проверки порядка первый- пришедший- обрабатывается- первым (FIFO - first-in, first-out). Поскольку каждое сообщение обладает неким последовательным номером, наш получатель способен отслеживать:

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

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

Когда получаемое нами сообщение имеет не идущий следом последовательный номер, наш получатель помещает его в свой буфер упорядочения. Например, он получает сообщение с последовательным номером 5 после получения сообщения с номером 3 и мы знаем что 4 пока пропущено, поэтому нам отложить 5 в сторону пока не придёт 4 и мы сможем реконструировать первичный порядок сообщений. Поскольку мы выполняем построение поверх соединения со справедливой утратой, мы полагаем, что сообщения между nпоследовательный и nмакс_увиденный в конечном итоге будут доставлены.

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

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

В терминологии распределённых систем этот тип подключения носит название совершенного соединения (perfect link), которое предоставляет следующие гарантии [CACHIN11]:

Надёжная доставка

Всякое единожды отправленное сообщение правильным процессом A верному процессу B будет в конечном итоге доставлено.

Отсутствие дублирования

Никакое сообщение не доставляется более одного раза.

Никакого создания

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

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

В точности одна доставка

В распределённых системах имеются всего лишь две проблемы: 2. В точности одна доставка 1. Гарантированный порядок сообщений 2. В точности одна доставка

- Матиас Вираис

Существовало множество обсуждений на предмет того, возможна или нет в точности одна доставка. Здесь имеет значение семантика и точность формулировок. Поскольку может иметься некий отказ соединения, препятствующий доставке сообщения с самой первой попытки, большинство систем реального мира используют по крайней мере одну доставку, что обеспечивает повторы отправителя пока он не получит некое подтверждение,в противном случае данное сообщение не рассматривается как полученное. Другая семантика доставки состоит в не более одного раза: наш отправитель посылает своё сообщение и не дожидается никаких подтверждений о доставке.

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

Это означает, что здесь нас интересует именно в точности одна обработка, так как дублированные доставки (или передачи пакетов) не имеют сторонних эффектов и всего лишь выступают в качестве неких артефактов наилучших усилий данного соединения. Например, когда узел базы данных только получил необходимую запись, но не сохранил его, доставка произошла, однако он не будет никакого его применения пока данная запись не будет повторена (иными словами, пока она не будет и доставлена, и обработана).

Для поддержки гарантии только одного раза, узлы обязаны обладать общим знанием [HALPERN90]: все знают некий факт и каждый знает что все кроме него также знают об этом факте. Упрощённо говоря, узлы должны прийти к согласию относительно состояния данной записи: оба узла согласны с тем, что она либо была, либо нет сохранена. Как вы обнаружите позднее в данной главе, это теоретически невозможно, однако на практике мы всё же пользуемся такой нотацией ослабляя требования координации.

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

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

Проблема двух генералов

Одной из наиболее рельефных описаний согласия в распределённых системах является мысленный эксперимент, широко известный как проблемы двух генералов.

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

Представми себе две армии, ведомые двумя генералами, готовящимися к атаке на укреплённое поселение. Эти армии расположены с двух стороно от данного поселения и способны достичь учпеха в своей осаде только когда они атакуют одновременно.

Их генералы могут взаимодействовать, отправляя посыльных и уже разработали некий план атаеи. Единственный момент, который им осталось согласовать это приводить или нет даннй план в исполнение. Вариантами этой задачи являются случаи, когда у одного из этих генералов более высокий ранг, но ему требуется быть уверенным, что данная атака скоординирована; или же что этим генералам требуется согласовать её точное время. Такие детали не меняют основного определения задачи: генералам требуется достичь согласия.

Генералам этих армий требуется лишь иметь договорённость по тому факту, что они оба произведут данную атаку. В противном случае эта атака не будет успешной. Генерал A отправляет некое сообщение MSG(N), утверждающее намерение выполнить данную атаку в какое- то определённое время, если другая сторона также согласна на это.

После того как A отправляетданное послание, он не знает, было ли его донесение доставлено или нет: посыльный мог быть перехвачен, или не выполнил доставку этого донесения. Когда генерал B получает данное донесение, он должен отправить уведомление о его получении ACK(MSG(N)). Рисунок 8-4 отображает что некое сообщение было отправлено одним из путей и другой стороной была подтверждена его доставка.

Рисунок 8-4

Иллюстрация проблемы двух генералов

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

Чтобы иметь уверенность в этом, B приходится дожидаться ACK(ACK(MSG(N))), подтверждающего состояния второго порядка о том, что A плучил подтверждение подтверждения.

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

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

Невозможность FLP

В статье Фишера, Линча и Патерсона, её авторы описали некую задачу, широко известную как Проблема невозможности FLP [FISCHER85] (по заглавным буквам фамилий авторов), в которой они обсудили некий вид согласия, в котором процессы стартовали с неким начальным значением и пытаются договориться о неком новом значении. После выполнения этого алгоритма, данное новое значение должно быть одним и тем же для всех продолжающих работу процессов.

Достигнуть некого соласия по определённому значению очень просто когда сетевая среда полностью надёжна; но в действительности, системы склонны ко многим различного рода отказам, таким как потеря сообщений, дублирование, разбиение сетей на разделы, а также медленные или испытывающие крушения процессы.

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

Согласие

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

Обоснованность

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

Прекращение

Некой согласование завершено только когда нет никакого процесса, который не достиг состояния данного решения.

[FISCHER85] предполагает, что обработка полностью асинхронная: ет никакого совместного представления о временах между этими процессами. Алгоритмы в такой системе не могут основываться на таймаутах, и нет никакого способа для процессов определять претерпели ли прочие процессы крушение или просто очень медленно работают. Данная статья показыает, что при заданных предположениях, не существует никакого протокола, который был бы способен обеспечить согласование в ограниченный срок. Никакой полностью асинхронный алгоритм не может быть толерантным к падениям без предупреждений даже некого отдельного удалённого процесса.

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

Тем не менее, невозможность FLP не означает что нам придётся упаковать свои шмотки и идти домой, так как достижение согласия невозможно. Она лишь означает, что мы не можем всегда достигать согласия в асинхронных системах в ограниченное время. На практике системы демонстрируют по крайней мере какую- то степень синхронности, а получаемое решение данной задачи требует дополнительно уточнённой модели.

Синхронность систем

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

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

Не всегда возможно разрешить задачу согласования в некой асинхронной модели [FISCHER85]. Более того, не всегда достижимо проектирование некого действенного синхронного алгоритма и более вероятно быть зависимым от времени [ARJOMANDI83].

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

Некая синхронная система также может представляться в терминах синхронизуемых локальных для процесса часов: существует некая верхняя граница разницы во времени между конкретными двумя локальными для процесса источниками времени [CACHIN11]..

Проектирование систем в рамках некой синхронной модели позволяет нам применять таймауты. Мы способны строить более сложные абстракции, такие как выбор лидера, консенсус, выявление отказов, и много чего ещё поверх них. Это делает рекомендуемые сценарии более надёжными, однако приводят к отказам в случае несоблюдения предположений о времени. Нпример, в алгоритме согласования Raft мы можем в итоге получить множество процессов, полагающих что они являются лидерами, что разрешается принудительным процессом торможения для принятия иного процесса в качестве лидера. Алгоритмы выявления сбоев (см. Главу 9) могут ошибочно идентифицировать некий процесс как отказавший и наоборот. При проектировании своих систем нам необходимо проверять что мы рассмотрели такие возможности.

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

Синхронность это некое существенное свойство рассматриваемой распределённой системы: оно оказывает воздействие на производительность, масштабируемость и общую разрешимость, а также обладает множеством факторов, которые требуются для корректной работы наших систем. Некоторые из обсуждаемых нами в данной книге алгоритмов работают в предположениях синхронных систем.

Модели отказов

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

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

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

Отказы крушения

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

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

Отказавшие процессы не способны продолжать своё участие в данном текущем раунде переговоров,в процессе которого они испытали отказ. Назначение данному восстановленному процессу некого нового, иного логического элемента не делает эту модель эквивалентной восстановлению после крушения (обсуждаемой следом), так как большинство алгоритмов применяют предварительно заданный список процессов и чётко определяют семантику отказов в терминах того, какому числу отказов они могут быть равнодушны [CACHIN11].

Восстановление после отказа (Crush- recovery) это иная абстракция процесса, при которой данный процесс остановил выполнение тех шагов, которые требуются его алгоритмом, но восстановился в некий более поздний момент и пытается выполнять последующие шаги. Такая возможность восстановления требует введения в рассматриваемую систему некого надёжного состояния и протокола восстановления [SKEEN83]. Допускающим восстановление после крушения алгоритмам необходимо учитывать все возможные состояния восстановления, так как их процесс восстановления может попытаться продолжить выполнение с самого последнего известного ему шага.

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

Отказы пропуска

Другой моделью отказа является отказ пропуска (omission fault). Данная модель предполагает, что наш алгоритм опустил некоторые шаги своего алгоритма, или был не в состоянии выполнить их, либо это исполнение не видно прочими участниками, или же он не способен отправлять или принимать сообщения прочих участников. Отказ пропуска захватывает разбиение на сетевые разделы между имеющимися процессами, вызываемые отказами сетевых соединений, сбоями коммутаторов или сетевыми переполнениями. Разбиения на сетевые разделы может представляться как пропуски сообщений между индивидуальными процессами или группами процессов. Некое крушение может имитироваться полным пропуском всех сообщений к процессу или от него.

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

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

Произвольные отказы

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

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

Возможно вы слышали о Византийской отказоустойчивости в аэрокосмической индустрии: системы самолётов и космических кораблей не получают ответы от своих составных компонентов с непосредственными значениями и проводят перекрёстную проверку их результатов.

Обработка отказов

Мы можем маскировать отказы формируя группы процессов и вводя в свои алгоритмы избыточность: даже когда один из процессов отказывает, его пользователь не замечет такого отказа [CHRISTIAN91].

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

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

Выводы

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

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

[Замечание]
Последующее чтение

Если вы желаете узнать больше относительно упоминавшихся в этой главе понятий, вы можете ссылаться на следующие источники:

Абстракции распределённых систем, модели отказов и предположения об исчислении времени

Lynch, Nancy A. 1996. in Distributed Algorithms, San Francisco: Morgan Kaufmann. .

Tanenbaum, Andrew S. and Maarten van Steen. 2006. in Distributed Systems: Principles and Paradigms, (2nd Ed). Boston: Pearson.

Cachin, Christian, Rachid Guerraoui, and Lus Rodrigues. 2011. in Introduction to Reliable and Secure Distributed Programming, (2nd Ed). New York: Springer.

Глава 9. Выявление отказов

Содержание

Глава 9. Выявление отказов

Сигналы сердцебиения и Ping

Свободный от таймаутов определитель отказа

Независимые сигналы сердцебиения

Определитель отказа Phi- начисления

Gossip и определитель отказа

Постановка задачи обратного выявления отказа

Выводы

Когда в лесу падает дерево и никто вокруг не слышит этого, издаёт ли оно звук?

- Неизвестный автор

Чтобы система надлежащим образом реагировала на отказы, отказы должны выявляться своевременно. С отказавшим процессом можно связываться даже если он не способен отвечать, что увеличивает задержки и снижает общую доступность системы.

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

Такие термины как умер, отказал и претерпел крушение обычно применяются для описания некого процесса, который прекратил выполнение своих шагов надлежащим образом. Такие термины как неотвечающий, сбойный и замедлившийся применяются для описания сомнительных (suspected) процессов, которые на самом деле могут быть умершими.

Отказы могут происходить на уровне установленного соединения (сообщения между процессами теряются или работают медленно), или на уровне самого процесса (сам процесс претерпел крушение или он работает медленно), а медленность может не всегда отличаться от отказа. Это означает, что всегда существует некий компромисс между ошибочным подозрением работающего процесса умершим (производящим ошибочные заключения - false- positives), и отсрочкой не отвечающего процесса умершим, что даёт ему преимущество сомнения и ожидания от него в конечном итоге отклика (производя ошибочно отрицательные заключения - false- negatives).

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

Жизнеспособность и безопасность являются свойствами, которые описывают некую способность алгоритма разрешать конкретную проблему и скорректировать свой вывод. Более формально, жизнеспособность это свойство, которое обеспечивает что некое определённое намеченное событие обязано произойти. Например, когда один из процессов отказал, определитель отказа обязан выявить такой отказ. Безопасность гарантирует что нежелательные события не случатся. Например, когда определитель отказов пометил некий процесс как умерший, этот процесс, фактически, должени быть умершим [LAMPORT77] [RAYNAL99] [FREILING11].

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

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

Мы можем судить о качестве алгоритма по его действенности: насколько быстро определитель отказов способен выявлять отказы. Другим способом делать это является рассмотрение точности такого алгоритма: более эффективный алгоритм может быть менее точным, а более точный алгоритм обычно менее действенный. Практически невозможно построить некий определитель отказов, который и точен, и эффективен. В то же самое время, определителям отказов допускается производить ошибочные заключения (т.е. неверно рассматривать рабочие процессы как отказавшие и наоборот) [CHANDRA96].

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

Многие распределённые системы реализуют определители отказов применяя сердцебиения (heartbeats). Такой подход достаточно популярен, так как он прост и строго полон. Обсуждаемые нами здесь алгоритмы предполагают отсутствие Византийских отказов: процессы не пытаются преднамеренно лгать о своём состоянии или состояниях своих соседей.

Сигналы сердцебиения и Ping

Мы можем запрашивать значение состояния удалённых процессов включая один из двух периодических процессов:

Мы можем включать некий ping. который отправляет сообщения удалённым процессам, проверяя что они ещё работают, ожидая некий отклик через определённый временной период.

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

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

Каждый процесс поддерживает некий перечень прочих процессов (рабочих, умерших и подозреваемых в этом) и обновляет его последним по времени откликом для каждого процесса. Когда некий процесс перестаёт откликаться на некий ping продолжительное время, он помечается как подозрительный (suspected) Рисунок 9-1 отображает обычное функционирование системы: процесс P1 запрашивает значение состояния соседнего узла P2, который отвечает неким подтверждающим откликом.

Рисунок 9-1

Ping для выявления отказов: обычная работа, никаких задержек сообщений

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

Рисунок 9-2

Ping для выявления отказов: отклики задерживаются, приходя после отправки следующего сообщения

Многие алгоритмы выявления отказов основываются на сердцебиениях и таймаутах. К примеру, Akka, популярная инфраструктура построения распределённых систем, имеет некую реализацию выявления отказов срока окончания, которая использует сердцебиения и отчёты об отказах некого процесса когда тот отказал в регистрации за фиксированный промежуток времени.

Такой подход обладает рядом потенциальных недостатков: его точность полагается на тщательный выбор частоты ping и времени отклика, к тому же он не отражает видимость процесса с точки зрения прочих процессов. (см. раздел Независимые сигналы сердцебиения)

Свободный от таймаутов определитель отказа

Некоторые алгоритмы при выявлении отказов избегают полагаться на таймауты. Например, Heartbeat, алгоритм выявления отказов без таймаутов [AGUILERA97], является неким алгоритмом, который лишь подсчитывает число сердцебиений и позволяет соответствующим приложениям выявлять процесс отказов на основании значений данных в соответствующих векторах счётчиков сердцебиений. Поскольку данный алгоритм свободен от таймаутов, он работает в предположениях асинхронных систем.

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

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

Когда соответствующий процесс получает некое сообщение сердцебиения, он увеличивает значение счётчика для всех тех участников, которые присутствуют в этом пути и отправляет это сердцебиение одному из тех, который не присутствует там, добавляя себя в данный путь. процесс останавливает распространение сообщений, как только видно, что все известные процессы уже получили его (иначе говоря, идентификаторы процессов присутствуют в этом пути).

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

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

Независимые сигналы сердцебиения

Некий альтернативный, применяемый SWIM (Scalable Weakly Consistent Infection-style Process Group Membership Protocol, Масштабируемым слабо согласованным протоколом участия в группах процессов в стиле инфекции) [GUPTA01] служит для использования независимых сердцебиений (outsourced heartbeats) для улучшения надёжности используемых сведений относительно жизнеспособности соответствующего процесса с точки зрения его соседей. Такой подход не требует от процессов осведомлённости обо всех прочих процессах в его сетевой среде, а лишь о неком подмножестве соединённых однораногвых партнёров.

Как показано на Рисунке 9-3, процесс P₁ отправляет какое- то сообщение ping процессу P₂. P₁ не отвечает на это сообщение, поэтому P₁ продолжает выбирать множество случайных участников (P₃ и P₄). Эти произвольные участники пытаются отправлять сообщения сердцебиений к P₂ и, если он отвечает, преправлять подтверждения обратно к P₁.

Рисунок 9-3

Переданные на внешний подряд сердцебиения

Это позволяет вести учёт как прямой так и косвенной досягаемости. Например, когда у нас имеются процессы P₁, P₂ и P₃, мы можем проверять состояние P₃ с точки зрения как P₁, так и P₂.

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

Определитель отказа Phi- начисления

Вместо того чтобы рассматривать отказ узла как некую двоичную задачу, при которой процесс способен быть только в двух состояниях: рабочем или отключённом, определитель отказов начисления фи (phi-accrual, φ-accrual) [HAYASHIBARA04] обладает непрерывной шкалой, перехватывая значение вероятности крушения соответствующего отслеживаемого процесса. Он работает сопровождая некое смещающееся окно, собирая времена появления наиболее последних сердцебиений от имеющихся одноранговых процессов. Эт сведения используются для оценки возникновения следующего сердцебиения, сравнивает это приближение со значением реального времени его появления и вычисляя значение подозреваемого уровня φ: насколько определённое значение определителя отказов близко к факту отказа на основании текущих условий сетево среды.

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

С точки зрения архитектуры, выявитель отказов начисления фи может представляться как сочетание трёх подсистем:

Отслеживание

Сбор сведений о жизнеспособности через ping, сердцебиения или выборочные запрос- отклик.

Интерпретация

Принятие решения о том следует или нет помечать определённый процесс как подозрительный.

Действие

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

Обозначенный процесс отслеживания собирает и сохраняет выборки данных (которые, как предполагается, следуют нормальному распределению) в некотором окне фиксированного размера времён возникновения сердцебиений. Более новые появляющиеся добавляются в это окно, а более старые отметки сведений о сердцебиениях отвергаются.

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

Этот подход был выработан исследователями из Japan Advanced Institute of Science and Technology и теперь применяется во многих распределённых системах; например, в Cassandra и Akka (помимо уже упомянутого выявителя отказов срока окончания).

Gossip и определитель отказа

Другой подход, который избегает полагаться на представление некого отдельного узла на принятие решения это служба выявления отказов в стиле болтовни [VANRENESSE98], которая применяет болтовню (gossip, см. раздел Рассеивание Gossip) для сбора и распространения состояний соседних процессов.

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

Узлы также периодически проверяют свой список состояний и счётчики сердцебиений. Когда некий узел не обновлял свой счётчик достаточно долго, он рассматривается как отказавший. Данный промежуток таймаута должен выбираться аккуратно для минимизации вероятности ложных включений. Насколько часто участники должны взаимодействовать друг с другом (иначе говоря, полоса пропускания наихудшего случая) ограничивается сверху, причём это значение может расти почти линейно с общим числом процессов в данной системе.

Рисунок 9-4 отображает три взаимодействующих процесса, разделяющих свои счётчики сердцебиений:

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

P3 не способен взаимодействовать с P1, однако его временная отметка t₆ всё ещё способна распространяться через P2.

P3 испытывает крушение. Так как он более не отправляет обновления, он выявляется прочими процессами как отказавший.

Рисунок 9-4

Таблица реплицируемых сердцебиений для принятия решений об отказе

Таким образом мы способны выявлять претерпевшие крушение узлы, а также те узлы, которые недостижимы со стороны прочих участников кластера. Это решение надёжное, так как такое представление кластера агрегируется от множества узлов. Когда имеется отказ в соединении между двумя хостами, сердцебиения всё ещё способны распространяться через прочие процессы. Применение gossip для распространения состояний системы увеличивает общее число сообщений в данной системе, однако позволяет сведениям распространяться более надёжно.

Постановка задачи обратного выявления отказа

Так как распространение сведений об отказах не всегда возможно, а к тому же их распространение для всех участников может быть затратным, один из подходов с названием FUSE (failure notification service, служба уведомления об отказах) [DUNAGAN04], сосредоточена на надёжном и не затратном распространении сведений об отказах, которое работает даже в случае разбиения сетевой среды на разделы.

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

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

Рисунок 9-5 отображает четыре взаимодействующих процесса:

Начальное состояние: все процессы в работе и способны взаимодействовать.

P₂ испытывает крушение и перестаёт отвечать на сообщения ping.

P₄ выявляет отказ P₂ и совсем прекращает отвечать на сообщения ping.

В конце концов P₁ и P₃ замечают что и P₂, и P₄ не отвечают и отказ процессов распространяется на всю данную группу целиком.

Рисунок 9-5

Выявление отказов FUSE

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

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

Выводы

Выявление отказов является существенной частью любой распределённой системы. Как показывает результат невозможности FLP, никакой протокол не способен обеспечивать консенсус в некой асинхронной системе. Определители отказов помогают пополнять данную модель, позволяя нам решать некую задачу консенсуса при помощи компромисса между точностью и завершённостью. Одной из важнейших находок в этой области, доказывающей полезность определителей отказов, было описание в [CHANDRA96], которое показало, что решение консенсуса возможно даже при некотором определителе отказов, который делает бесконечное число ошибок.

Мы рассмотрели несколько алгоритмов выявления отказов, причём каждый из них предпринимал отличительный подход: некоторые сосредотачивались на выявлении отказов при непосредственном взаимодействии, какие- то пользовались широковещанием или gossip для распространения имеющихся сведений вокруг, а некоторые применяли вариант использования неподвижности (иначе говоря, отсутствия взаимодействия) как некого средства распространения. Теперь мы знаем, что мы способны применять сердцебиения или ping, аппаратные сроки окончания или непрерывные шкалы. Всякий из этих подходов обладает своими собственными сильными сторонами: простотой, правильностью или точностью.

[Замечание]
Последующее чтение

Если вы желаете узнать больше относительно упоминавшихся в этой главе понятий, вы можете ссылаться на следующие источники:

Выявление отказов и алгоритмы

Chandra, Tushar Deepak and Sam Toueg. 1996. “Unreliable failure detectors for reliable distributed systems.” in Journal of the ACM, 43, no. 2 (March): 225-267. https://doi.org/10.1145/226643.226647.

Freiling, Felix C., Rachid Guerraoui, and Petr Kuznetsov. 2011. “The failure detector abstraction.” in ACM Computing Surveys, 43, no. 2 (January): Article 9. https://doi.org/10.1145/1883612.1883616.

Phan-Ba, Michael. 2015. “A literature review of failure detection within the context of solving the problem of distributed consensus.” https://www.cs.ubc.ca/~bestchai/theses/michael-phan-ba-msc-essay-2015.pdf.

Часть 1. Механизмы хранения

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

Так как сам термин DBMS (database management system, систем управления базами данных - СУБД) достаточно объёмист, на протяжении этой книги мы будем применять более компактные термины системы базы данных и базы данных для ссылки на то же самое понятие.

Базы данных являются модульными системами и они состоят из многих частей: транспортный уровень принимает запросы, процессор запросов определяет наиболее действенный способ исполнения запросов, механизм исполнения заботится о самих действиях, а механизм хранения отвечает за хранение (см. Архитектура DBMS).

Такой механизм хранения (storage engine, или механизм базы данных) является неким программным компонентом системы управления базой данных, ответственным за хранение, выборку и управление данными в памяти {=оперативной памяти} и на диске, спроектированным для захвата некой постоянной, долговременной памяти каждого из узлов [REED78]. В то время как базы данных способны отвечать на сложные запросы, механизмы хранения рассматривают данные более гранулировано и предлагают некий простой API манипулирования данными, позволяя пользователям создавать, обновлять, удалять и выбирать записи. Одним из способов взглянуть на это состоит в том, что системы управления базой данных являются приложениями, которые строятся поверх механизмов хранения, предлагая некие схему, язык запросов, индексацию, транзакции и множество прочих полезных функциональных возможностей.

Для гибкости и ключи и значения могут быть произвольными последовательностями байт без какой бы то ни было установленной формы. Их семантики сортировки и представления определяются подсистемами верхнего уровня. Например, вы можете в качестве ключа в одной из таблиц можете применять int32 (32- битное целое), и ascii (строку ASCII) для другой; с точки зрения нашего механизма хранения оба ключа это всего лишь упорядоченные записи.

Такие механизмы хранения, как BerkeleyDB, LevelDB и их потомки RocksDB, LMDB, а также их последователи libmdbx, Sophia, HaloDB и многие прочие, все они были разработаны независимо от своих систем управления базами данных, в которые они теперь встроены. Применение подключаемых механизмов хранения позволило разработчикам баз данных раскручивать системы баз данных применяя имеющиеся механизмы хранения и сосредоточиться на прочих подсистемах. {Прим. пер.: есть и обратные примеры использования механизмов хранения без систем управления базами данных, см. использование RocksDB в BlueStore Ceph.}

В то же самое время, чёткое разделение между компонентами системы базы данных открывает для нас некую возможность переключения между различными механизмами, потенциально лучше подходящими для различных вариантов использования. Например, MySQL, популярная система управления базой данных имеет несколько механизмов хранения, включающих InnoDB, MyISAM и RocksDB (в соответствующем дистрибутиве MyRocks). MongoDB позволяет переключаться между механизмами хранения WiredTiger, In-Memory и (теперь считающимся устаревшим) MMAPv1.

Сопоставление баз данных

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

Всякая система базы данных имеет сильные и слабые стороны. Для снижения риска затратной миграции вы можете потратить некое время прежде чем вы решите что некая конкретная база данных для построения заслуживает доверия к своим возможностям отвечать потребностям вашего приложения.

Попытка сравнивать базы данных на основе их компонентов (например, какой механизм хранения они используют, как разделяются, реплицируются и распространяются и т.п. данные), их ранжирования (произвольного значения популярности, присваиваемого консалтинговыми агентствами, такими как ThoughtWorks, либо такими вебсайтами как DB-Engines или Database of Databases), либо языка реализации (C++, Java или Go и тому подобных), могут влечь за собой неверные или преждевременные выводы. Эти методы могут применяться только для некого сравнения на самом верхнем уровне и способны выступать лишь грубой прикидкой в выборе между HBase и SQLite, а потому даже поверхностное понимание того как работает каждая база данных и что у неё внутри может помочь вам приземлиться с более весомым заключением.

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

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

Для сравнения баз данных полезно понимать вариант использования более подробно и определять значения текущих и ожидаемых переменных, таких как:

Схема и размеры записей

Число клиентов

Типы запросов и шаблонов доступа

Частоты запросов чтения и записи

Ожидаемые изменения каждой из этих переменных

Знание этих переменных позволит вас ответить на следующие вопросы:

Поддерживает ли эта база данных все необходимые запросы?

Способна ли рассматриваемая база данных обрабатывать то количество данных, которое мы планируем хранить?

Какое число операций чтения и записи способен обрабатывать некий отдельный узел?

Сколько узлов должна иметь данная система?

Как мы расширяем этот кластер в предположении его роста?

В чём состоит процесс сопровождения?

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

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

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

Одним из популярных инструментов для эталонного тестирования, оценки производительности и сопоставления является Yahoo! Cloud Serving Benchmark (YCSB). YCSB предлагает некую инфраструктуру и общий набор рабочих нагрузок, который можно применять для различных хранилищ данных. Как и всякая прочая вещь общего применения, этот инструмент следует использовать с предосторожностями, так как просто прийти к неверному заключению. Для осуществления справедливого сравнения и принятия выверенного решения, необходимо затратить значительное время на понимание условий реального мира, в которых вашей базе данных придётся работать и подгонять эталонное тестирование надлежащим образом.

[Замечание]
Эталонное тестирование TPC-C

Transaction Processing Performance Council (TPC, Совет по производительности обработки транзакций) имеет некий набор эталонного тестирования, который производители баз данных применяют для сравнения предлагаемой производительности своих продуктов. TPC-C является неким интерактивным эталонным тестом обработки транзакций (OLTP), некую смесь транзакций только на чтение и обновлений, которая имитирует распространённые рабочие нагрузки приложений..

Этот эталонный тест касается производительности и правильности выполнения одновременных транзакций. Основным показателем производительности выступает пропускная способность: общее число транзакций, которое тестируемая база данных способна выполнять за минуту. Выполняемые транзакции требуют сохранения свойств ACID и соответствия набору свойств, задаваемых этим тестом.

Данный эталонный тест не сосредоточен на каком бы то ни было определённом сегменте бизнеса, а вместо этого представляет некий абстрактный набор действий, важных для большинства приложений, которым подходят базы данных OLTP. Он содержит определённые таблицы и записи, такие как хранилища данных, рынки (описи), потребителей и заказы, определения табличных схем, подробности транзакций, которые могут выполняться для таких таблиц, значения минимального числа строк для таблиц и ограничения на долговечность данных.

Не важно, что эталонное тестирование может применяться только для сопоставления баз данных. Эталонные тесты могут оказываться полезными для определения подробностей тестов показателей SLA (service-level agreement, соглашений об уровне обслуживания - обязательстве поставщика услуг относительно качества предоставляемых им услуг. Помимо прочего, SLA может содержать сведения о задержках, пропускной способности, вариабельности, а также о числе отказов), понимании требований к системе, планирования ёмкости и прочего. Чем большими знаниями вы обладаете о базе данных до её применения, тем больше времени вы сохраните при её работе в промышленной реализации.

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

Осознание компромиссов

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

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

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

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

Существует множество механизмов хранения, использующих все виды структур данных, реализуемых на различных языках программирования, причём в диапазоне от низкого уровня, например, C, до верхнего уровня, например, Java. Все механизмы хранения сталкиваются с одними и теми же вызовами и ограничениями. Чтобы провести параллель с городским планированием, можно строить для определённой группы населения и выбирать застройку вверх и застройку вширь. В обоих случаях в этот город поместится одно и то же число жителей, но эти подходы приведут к радикально отличающимся стилям жизни. при строительстве города ввысь, жители обитают в апартаментах, а плотность населения скорее всего приведёт к большему трафику на меньшей площади; в более обширном городе люди скорее всего будут жить в домах, но ежедневные поездки на работу потребуют преодоления больших расстояний.

Аналогично, принимаемые разработчиками механизма хранения решения проектирования сделают их более пригодными для различных вещей: некоторые оптимальны для низких задержек чтения или записи, некоторые пытаются максимизировать плотность (объём хранимых в узле данных), а иные сосредоточены на упрощении работы.

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

Глава 1. Введение и обзор

Содержание

Глава 1. Введение и обзор

Архитектура DBMS

Сопоставление DBMS в памяти и основанных на дисках

Живучесть в основанных на памяти хранилищах

Сопоставление DBMS ориентированных на столбцы и на строки

Схема ориентированных на строки данных

Схема ориентированных на столбцы данных

Различия и оптимизация

Хранилища с широкими столбцами

Файлы данных и индексные файлы

Файлы данных

Файлы индексации

Первичный индекс как некая косвенная адресация

Буферизация, неизменность и упорядоченность

Выводы

Системы управления базами данных могут обслуживать различные цели: некоторые применяются в первую очередь для временных горячих данных, некоторые служат как долговременные холодные хранилища, какие- то допускают сложные аналитические запросы, а какие- то допускают лишь доступ к значениям по ключу, иные оптимизированы под хранение данных временных последовательностей, а есть также эффективные хранилища больших двоичных объектов (blobs). Для осознания различий и проведения разграничений, мы начнём с некой короткой классификации и обзора, так как это поможет нам понять саму сферу для последующего обсуждения.

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

Мы начнём с некого обзора архитектур систем управления базами данных (см. Архитектура DBMS) и обсудим компоненты системы и их зоны ответственности. После этого мы обсудим имеющиеся отличия между системами управления базами данных в терминах среды хранения (см. Сопоставление DBMS в памяти и основанных на дисках), а также схему (см. Сопоставление DBMS ориентированных на столбцы и на строки).

Эти две группы не представят полной систематики систем управления базами данных и имеется множество прочих способов их классификации. К примеру, некоторые источники группируют DBMS на три основные категории:

Базы данных OLTP (Online transaction processing, ориентированных на обработку транзакций в реальном времени)

Они обрабатывают большое число запросов и транзакций со стороны пользователя. Запросы часто определены заранее и имеют короткое время жизни.

Базы данных OLAP (Online analytical processing, оперативной аналитики)

Эти обрабатывают сложные конгломераты. Базы данных OLAP часто применяются для аналитики и организации информационных хранилищ (data warehousing), причём способны обрабатывать сложные, долгоживущие не регламентированные запросы (ad hoc queries).

HTAP (Hybrid transactional and analytical processing, гибридная обработка транзакций и аналитики)

Такие базы данных сочетают свойства и OLTP, и OLAP хранилищ.

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

Так как I Часть данной книги сосредоточена на самих хранилищах и структурах индексации, нам требуется понимать подходы к организации данных на самом верхнем уровне, а также имеющуюся взаимосвязь между самими данными и индексными файлами (см. Файлы данных и индексные файлы.)

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

Архитектура DBMS

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

Описывающие архитектуру систем управления базами данных (например, [HELLERSTEIN07], [WEIKUM01], [ELMASRI11] и [MOLINA08]) определяют компоненты и взаимосвязи между ними по- разному. Та архитектура, которая представлена на Рисунке 1-1, демонстрирует некоторые общие темы этих представителей.

Рисунок 1-1

Архитектура системы управления базой данных

Системы управления базами данных применяют модель клиент/ сервер, в которой экземпляры системы базы данных (узлы) берут на себя роль серверов, а экземпляры приложения выполняют роль клиентов.

Требования клиентов появляются через имеющуюся транспортную подсистему. Требования поступают в виде запросов (queries), наиболее часто выражаемых на неком языке запросов. Имеющаяся транспортная система также отвечает за взаимодействие с прочими узлами в установленном кластере базы данных.

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

Разобранный запрос передаётся в имеющийся оптимизатор запроса, который исключает невозможные и избыточные части полученного запроса, а затем пытается найти наиболее действенный способ его исполнения на основе внутренних статистических данных (кардинальных значений индексов, приблизительный размер разбиения и т.п.) а также размещения данных (какие узлы в имеющемся кластере содержат необходимые данные и оценку стоимостей в связи с их пересылкой). Такой оптимизатор обрабатывает как необходимые для разрешения запроса реляционные операции, обычно представляемые неким деревом зависимостей, а также оптимизации, такие как упорядочение индексов, оценку кардинальных значений и выбор метода доступа.

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

Такой план выполнения обрабатывается механизмом исполнения, который собирает получаемые результаты исполнения локальных и удалённых операций. Удалённое исполнение может содержать запись и считывание данных в- и мз- прочих узлов в общем кластере, а также репликации.

Локальные запросы (поступающие обычно от клиентов или из прочих узлов) исполняются механизмом хранения. Такой механизм хранения обладает рядом компонентов с выделенными зонами ответственности:

Диспетчер транзакций

Данный диспетчер управляет транзакциями и гарантирует что они не могут покидать эту базу данных в логически не согласованном состоянии.

Диспетчер блокировки

Этот диспетчер блокирует в своей базе данных объекты для выполнения транзакций, гарантируя что одновременные действия не разрушат физическую целостность данных.

Методы доступа (структуры хранения)

Они управляют доступом и организацией данных на диске. Методы доступа включают в себя файлы кучи и такие структуры хранения, как B- деревья (см.Вездесущие B- деревья) или Деревья LSM.

Диспетчер буфера

Данный диспетчер кэширует данные страниц в памяти (см.Управление буфером).

Диспетчер восстановления

Этот диспетчер сопровождает имеющийся журнал действий и восстанавливает необходимое состояние системы в случае отказа (см.Восстановление)

Диспетчеры транзакций и блокировки совместно отвечают за контроль одновременности (см.Управление одновременностью): они гарантируют логическую и физическую целостность данных и в то же самое время обеспечивают одновременность исполнения операций настолько эффективно, насколько это возможно.

Сопоставление DBMS в памяти и основанных на дисках

Системы баз данных хранят данные в оперативной памяти {Прим. пер.: далее просто в памяти} и на диске. системы управления базами данных в памяти (In-memory, порой именуемые как DBMS основной памяти), хранят данные преимущественно в памяти и применяют диск для восстановления и ведения журналов. Основанные на дисках DBMS удерживают основные свои данные на диске и используют память для кэширования содержимого диска или в качестве некого временного хранилища. Оба типа систем используют в определённой степени свой диск, однако базы данных основной памяти хранят своё содержимое по большей части исключительно в оперативной памяти (RAM).

доступ к памяти был и остаётся более быстрым на несколько порядков чем доступ к диску (да, вы можете найти визуализацию сравнения латентности диска и памяти, а также множество прочих числовых значений, относящихся к этому вопросу по годам), поэтому имеется потребность в применении памяти в качестве первичного хранилища, причём по мере снижения цен на память это становится экономически всё более целесообразным. Однако, цены на оперативную память всё ещё остаются высокими по сравнению с устройствами постоянного хранения, такими как SSD и HDD.

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

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

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

Такое положение дел имеет вероятность измениться, поскольку растёт доступность и популярность технологии NVM (Non-Volatile Memory, энергонезависимой памяти) [ARULRAJ17]. Хранилища NVM снижают или даже полностью исключают (в зависимости от степени точности технологии) ассиметричность между задержками в чтении и записи, дополнительно улучшают производительнсть чтения и записи и делают возможным доступ с побайтовой адресацией.

Живучесть в основанных на памяти хранилищах

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

Прежде чем соответствующую операцию можно рассматривать как завершённую, её результаты обязаны быть записанными в некий последовательный файл журнала. Более подробно мы рассмотрим журналы с упреждающей записью в разделе Восстановление. Во избежание повторного воспроизведения всего содержимого журнала в процессе запуска или после крушения, хранилища в памяти поддерживают некую копию резервирования (backup copy). Такая копия резервирования сопровождается как некая структура данных, хранимая на диске и вносимые в эту структуры изменения зачастую асинхронны (отсоединены от запросов клиентов), а также для снижения общего числа операций ввода/ вывода применяются пакетно. В процессе восстановления, содержимое базы данных может восстанавливаться из такой резервной копии и журналов.

Записи журналов обычно применяются к резервной копии в пакетном режиме. После обработки такого пакета записей журнала, резервная копия хранит некий моментальный снимок (snapshot) базы данных на определённый момент времени, а содержимое журнала вплоть до этого момента времени может быть отвергнуто. Такой процесс имеет название создания контрольных точек (checkpointing). Он снижает время восстановления за счёт удержания располагающейся на диске базы данных по большей части в актуальном состоянии с записями журнала без блокирования запросов клиентов в ожидании обновления своей резервной копии.

[Замечание]
Замечание

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

Дисковые базы данных используют особые структуры хранения, которые оптимизированы для доступа к диску. При работе с памятью, по указателям можно проходить достаточно быстро и доступ к памяти с произвольной адресацией значительно быстрее произвольного дискового доступа {Прим. пер.: справедливости ради, следует сказать что протоколы NVMe существенно снизили это отличие}. Структуры дискового хранения зачастую имеют вид широких и коротких деревьев (см. раздел Деревья для основанных на дисках хранилищах), в то время как основанные на памяти реализации вольны делать выбор из большего пула структур данных и осуществлять оптимизацию, которая в противном случае была бы невозможна или сложна для реализации на дисках [MOLINA92]. Аналогично, обработка данных переменной длины на дисках требует особого внимания, в то время как в памяти это зачастую просто вопрос обращения к значению по ссылке.

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

Сопоставление DBMS ориентированных на столбцы и на строки

Большинство систем баз данных хранят некий набор записей данных, состоящих из столбцов и строк в таблицах. Поля являются неким пересечением столбцов и строк: отдельное значение некого типа. Относящиеся к одному и тому же столбцу поля, обычно, обладают одним и тем же типом данных. Например, когда мы определяем какую- то таблицу, содержащую записи пользователей, все имена имели бы один и тот же тип и относились бы к одному и тому же столбцу. Некое собрание значений, которое относились бы логически к одной и той же записи (как правило, идентифицируемое соответствующим ключом) составляет некую строку.

Один из способов классификации баз данных состоит в том как такая база данных хранится на диске: строками или столбцами. Таблицы могут разбиваться на разделы либо горизонтально (сохраняя совместно значения, относящиеся к одной и той же строке), либо вертикально (совместно храня значения, относящиеся к одному и тому же столбцу). Рисунок 1-2 выделяет это отличие (a) показывает значения, разбиваемые на разделы столбцами, а (b) отражает значения, делящиеся на разделы по строкам.

Рисунок 1-2

Схема хранения, ориентированная на столбцы и на строки

Примеров систем управления базами данных, ориентированных на хранение строк предостаточно: MySQL, PostgreSQL и большая часть всех традиционных реляционных баз данных. Двумя пионерами ориентированных на столбцы хранилищ являются MonetDB и C-Store (C-Store это предшественник Vertica, имеющий открытый исходный код).

Схема ориентированных на строки данных

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

| ID | Name  | Birth Date  | Phone Number   |
| 10 | John  | 01 Aug 1981 | +1 111 222 333 |
| 20 | Sam   | 14 Sep 1988 | +1 555 888 999 |
| 30 | Keith | 07 Jan 1984 | +1 333 444 555 |

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

Так как ориентированные на строки хранилища наиболее полезны в тех ситуациях, когда нам приходится выполнять доступ к данным по строкам, совместное хранение всей строки улучшает пространственную локальность [DENNING68] (пространственная локальность является одним из основных Принципов локальности, утверждающим что при обращении к некому местоположению в памяти, в ближайшем будущем будет выполнен доступ к расположенной рядом памяти).

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

Схема ориентированных на столбцы данных

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

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

С логической точки зрения, те данные, которые представляют цены рыночных котировок всё ещё могут представляться в виде таблицы:

| ID | Symbol | Date        | Price     |
| 1  | DOW    | 08 Aug 2018 | 24,314.65 |
| 2  | DOW    | 09 Aug 2018 | 24,136.16 |
| 3  | S&P    | 08 Aug 2018 | 2,414.45  |
| 4  | S&P    | 09 Aug 2018 | 2,232.32  |

Однако физическая схема базы данных на основе столбцов выглядит совершенно иначе. Значения, относящиеся к одной и той же строке внизу хранятся близко друг к другу:

Symbol: 1:DOW; 2:DOW; 3:S&P; 4:S&P
Date:   1:08 Aug 2018; 2:09 Aug 2018; 3:08 Aug 2018; 4:09 Aug 2018
Price:  1:24,314.65; 2:24,136.16; 3:2,414.45; 4:2,232.32

Для реконструкции кортежей данных, что может быть полезно для соединений, фильтрации и агрегации множества строк, нам потребуется сберегать некие метаданные на уровне определённого столбца для идентификации какой именно точке из другого столбца они соответствуют. Когда вы делаете это в явном виде, каждое значение будет иметь некий ключ для удержания, что увеличивает дублирование и приводит росту объёма хранимых данных. Некторые хранилища столбцов применяют неявные идентификаторы (виртуальные ID) вместо использования конкретного значения позиции (иными словами, его смещение) для указания обратного соответствия для связанных с ним значений [ABADI13].

На протяжении последних нескольких лет, вероятно по причине роста спроса на выполнение сложных аналитических запросов по всё растущим наборам данных, мы наблюдаем множество новых ориентированных на столбцы форматов файлов, таких как Apache Parquet, Apache ORC, RCFile, а также ориентированные на столбцы хранилища, например, Apache Kudu, ClickHouse и многие иные [ROY12].

Различия и оптимизация

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

Считывание множества значений для одного и того же столбца за один проход значительно улучшает действенность применения кэширования и вычислений. В современных ЦПУ для обработки множества точек данных могут использоваться векторные инструкции в отдельной операции ЦПУ [DECANDIA07]. (Векторные инструкции, такие как Single Instruction Multiple Data (SIMD), описываются как некий класс инструкций ЦПУ, которые выполняют одно и то же действие для множества пунктов данных.)

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

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

Хранилища с широкими столбцами

Ориентированные на столбцы базы данных не стоит путать с хранилищами широких столбцов, например BigTable или HBase, где данные представляются в виде некой многомерной карты, а колонки группируются на семейства столбцов (обычно хранящих данные одного и того же типа), а внутри каждого семейства столбцов данные хранятся строками. Такая схема наилучшая для хранения данных, выбираемых по ключу или некой последовательности ключей.

Неким каноническим примером из основополагающей статьи Bigtable [CHANG06] является Webtable. Webtable хранит моментальные снимки содержимого веб страниц, их атрибуты и имеющиеся между ними связи для определённой временной отметки. Страницы идентифицируются значениями приводимых в обратном порядке URL, а все атрибуты (например, content и anchors представляют связи между страницами) идентифицируются значениями временных отметок в которые были взяты эти моментальные снимки. Для простоты это можно представить как некое встраиваемое соответствие, как это показано на Рисунке 1-3.

Рисунок 1-3

Концептуальная структура Webtable

Данные хранятся в неком многомерном сортированном соответствии с иерархической индексацией: мы можем определять местоположение данных в соответствии с некой определённой веб страницей по её реверсивному URL и её содержимое или ссылки по значению временной метки. Каждая строка индексируется собственным ключом строки. Соответствующие колонки группируются совместно в семейства столбцов - в данном примере это contents и anchor - которые хранятся на диске по отдельности. Каждая колонка внутри семейства столбцов идентифицируется ключом колонки, который в сочетании с названием семейства столбцов и некого квалификатора (в данном примере html, cnnsi.com, my.look.ca). Семейства столбцов хранят множество данных по временной метке. Такая схема делает для нас возможным быстрое определение местоположения записей верхнего уровня (в данном случае веб страниц) и их параметров (версий содержимого и ссылок на прочие соответствующие страницы).

Хотя полезно понимать такое концептуальное представление хранилищ широких столбцов, их физическая схема совершенно иная. Схематическое представление имеющейся схемы данных в семействах столбцов показано на Рисунке 1-4: семейства столбцов хранятся раздельно, однако в каждом семействе столбцов все данные относящиеся к одному и тому же ключу сведения хранятся совместно.

Рисунок 1-4

Физическая структура Webtable

Файлы данных и индексные файлы

Самая первая цель системы базы данных состоит в хранении данных и создании возможности быстрого доступа к ним. Но как эти данные организованы? Зачем вам требуется система управления базой данных и почему бы просто не пользоваться пачкой файлов? Как организация файлов улучшает эффективность?

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

Эффективность хранения

Файлы организованы неим обарзом, чтобы минимизировать накладные расходы хранения, исчисляемые на запись хранимых данных.

Эффективность доступа

Записи могут быть расположены за самое минимальное количество шагов.

Эффективность обновлений

Обновления записи выполняются каким- то методом, который минимизирует общее число изменений на диске.

Системы баз данных хранят записи данных, состоящие из множества полей, причём в таблицах, при этом каждая таблица обычно представляется отдельным файлом. Каждая запись в такой таблице может быть найдена при помощи ключа поиска. Для определения местоположения некой записи системы баз данных используют индексы: вспомогательные структуры данных, которые позволяют им определять местоположение записей данных без сканирования таблиц целиком при каждом доступе. Индексы строятся на основании подмножеств идентифицирующих эту запись полей.

Система базы данных обычно разделяет файлы данных и индексные файлы: файлы данных хранят данные записей, в то время как индексные файлы хранят записи метаданных и используют их для определения местоположения записей в файлах данных. Индексные файлы обычно меньше своих файлов данных. Файлы разбиваются на разделы страницами, которые обычно имеют размер одного или нескольких дисковых блоков. Страницы могут быть организованы как последовательности страниц или как перекидные страницы (см. раздел Перекидные страницы).

Теперь записи (вставляемые) и обновления уже имеющихся записей представлены парами ключ/ значение. Большинство современных систем хранения не удаляют данные на страницах непосредственно. Вместо этого они применяют маркеры удаления (также имеющие название надгробий - tombstones), которые содержат метаданные удаления, такие как некий ключ и метку времени (timestamp). Занимаемое такой записью пространство экранируется их обновлениями или маркерами удаления и возвращаются в процессе сборки мусора, которая считывает данную страницу, записывает здравствующие (то есть не экранированные) записи в их новое место и отбрасывают те, которые экранированы.

Файлы данных

Файлы индексации

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

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

Вторичные индексы могут указывать непосредственно на определённые записи данных, либо просто хранить их первичный ключ. Некий указатель на запись данных способен содержать некое смещение относительно какого- то файла кучи или некой таблицы, упорядоченной индексом. Множество вторичных индексов может указывать на одну и ту же запись, что делает возможным отдельной записи данных идентифицироваться различными полями и располагаться в различных индексах. В то время как файлы первичного индекса содержат некую уникальную запись для каждого ключа поиска, вторичные индексы могут содержать несколько записей для одного и того же ключа поиска [MOLINA08].

Когда порядок записей данных соответствует порядку значений ключа поиска, такой индекс именуется кластеризованным (что также имеет синоним кластеризации). Записи данных в случае кластеризации обычно хранятся в одном и том же файле, или в неком файле кластеризации, в котором сохраняется порядок ключей. Когда имеющиеся данные хранятся в неком обособленном файле, а порядок в нём не соответствует порядку ключей, такой индекс имеет название некластеризованного (порой именуемого как не имеющего кластеризации).

Рисунок 1-5 показывает разницу между двумя подходами:

Два индекса ссылаются на записи данных непосредственно из файлов вторичных индексов.

Некий вторичный индекс проходит через косвенный уровень какого- то первичного индекса для определения местоположения искомых записей данных.

Рисунок 1-5

Хранение записей данных в неком индексном файле в сопоставлении с хранением смещений в самом файле данных (сегменты индексов показаны белым, удерживающие данные сегменты записей данных показаны серыми)

[Замечание]
Замечание

Таблицы с индексной организацией сохраняют информацию в соответствии с порядком индекса и кластеризованы по умолчанию. Первичные индексы чаще всего кластеризованы. Вторичные индексы не кластеризованы по определению, ибо они применяются для удобства доступа по ключам, отличающихся от первичных. Кластеризованные индексы могут быть как имеющими индексную организацию, так и иметь раздельные файлы индексов и данных.

Многие системы баз данных имеют встроенные первичные ключи в явном виде, некое множество столбцов, которые уникально идентифицируют такую запись базы данных. В том случае, когда такой первичный ключ не определён, сам механизм хранения может создать некий не явный первичный ключ (к примеру, MySQL и InnoDB добавляют некий новый столбец с автоматическим инкрементальным приращением и автоматически заполняют его значение).

Данная терминология используется в различных видах систем баз данных: реляционных систем (таких как MySQL и PostgreSQL), основанных на Dynamo хранилищ NoSQL (например, Apache Cassandra и Riak), а также в хранилищах документах (подобных MongoDB). Иогут иметься некие особенные для проекта названия, но наиболее часто имеется чистое соответствие данной терминологии.

Первичный индекс как некая косвенная адресация

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

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

Обновление всего лишь пары индексов способно работать когда наша рабочая нагрузка в основном состоит из считываний, однако этот подход не работает достойным образом при интенсивных рабочих нагрузках записи для большого числа индексов. Для снижения стоимости обновлений индексов, вместо полезной нагрузки смещений некоторые реализации пользуются первичными ключами для косвенной адресации. Например, MySQL, InnoDB при выполнении некого запроса применяют некий первичный индекс и осуществляют два поиска: один по значению своего вторичного индекса и один в первичном индексе [TARIQ11]. Это добавляет некие накладные расходы поиска по первичному ключу вместо непосредственному следованию по смещению из значения вторичного индекса.

Рисунок 1-6 показывает чем отличаются два этих подхода:

Два индекса ссылаются на записи данных напрямую из файлов вторичных индексов.

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

Рисунок 1-6

Непосредственное отражение кортежа данных (a) в сопоставлении с с косвенным использованием первичного индекса (b)

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

Буферизация, неизменность и упорядоченность

Механизм хранения основывается на некой структуре данных. Однако эти структуры не описывают имеющуюся семантику кэширования, восстановления способности к транзакциям и прочим моментам, которые такие механизмы хранения добавляют поверх неё.

В своих последующих главах мы приступим к такому обсуждению с B- деревьев (см. Вездесущие B- деревья) и попытаемся понять почему имеется так много вариантов B- деревьев и почему выходят в свет новые структуры хранения баз данных.

Структуры хранения обладают тремя общими переменными: они пользуются буферизацией (либо избегают её использование), применяют неизменные (или изменяемые) файлы, а также хранят значения упорядоченными (или не прибегая к упорядочиванию). Большинство различий и оптимизаций в обсуждаемых этой книгой структурах хранения связаны с одним из этих трёх понятий.

Буферизация

Она определяет будет или нет конкретная структура хранения выбирать сбор определённого объёма данных в памяти перед их помещением на диск. Конечно, всякой дисковой структуре приходится применять буферизацию в определённой степени, так как наименьшим элементом данных для обмена с диском является некий блок, и всегда желательно записывать заполненные блоки. Здесь мы говорим об избежании буферизации, которой порой придерживаются разработчики механизма хранения. Одной из самых первых обсуждаемых в данной книги оптимизаций выступает добавление буферов памяти в узлы B- дерева для амортизации стоимостей ввода/ вывода (см. Ленивые B- деревья). Однако не единственный способ, которым мы можем использовать буферизацию. Например, двухкомпонентные LSM деревья (см. Двухкомпонентные LSM деревья), вопреки своему сходству с B- деревьями, пользуются буферизацией совершенно иным образом и сочетают буферизацию с неизменностью.

Изменяемость (или неизменность)

Это определяет будет или нет данная структура хранения считывать части в данном файле, обновлять их и записывать результаты обновления в том же самом местоположении этого файла. Неизменные структуры являются дописываемыми исключительно в конец: после своей записи единожды, содержимое файла не изменяется. Вместо этого изменения добавляются в самый конец файла. Существуют и иные способы реализации неизменности. Один из них это копирование- записью (copy-on-write, см. Копирование записью), при котором значение изменяемой страницы, содержащее изменяемую версию рассматриваемой записи, записывается в новое место данного файла вместо его первоначального расположения. Часто само обсуждение относительно LSM и B- деревьев рисуется как сопоставление неизменности с обновлением хранилища на месте, однако существуют структуры (например, Bw- деревья), которые инспирированы B- деревьями, но при этом неизменны.

Упорядочение

Определяет будут или нет записи данных храниться в порядке своего ключа в имеющихся на диске страницах. Иными словами, те ключи, которые сортируются близко, сохраняются в непрерывных сегментах диска. Упорядочение часто определяет будет или нет возможным действенно сканировать определённый диапазон записей, а не просто определять местоположение определённых записей данных. Хранение данных неупорядоченным образом (чаще всего в порядке вставки) открывает некие оптимизации времени записи. Например, Bitcask (см. Bitcask) и WiscKey (см. WiscKey) хранят записи данных непосредственно в файлах с исключительным добавлением в конец.

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

Выводы

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

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

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

Наконец, мы ввели три центральных понятия: буферизацию, неизменность и упорядочение. Мы будем пользоваться ими на протяжении данной книги для высвечивания свойств конкретных использующих их механизмов хранения.

[Замечание]
Последующее чтение

Если вы желаете узнать больше относительно упоминавшихся в этой главе понятий, вы можете ссылаться на следующие источники:

Архитеткура баз данных

Hellerstein, Joseph M., Michael Stonebraker, and James Hamilton. 2007. “Architecture of a Database System.” Foundations and Trends in Databases 1, no. 2 (February): 141-259. https://doi.org/10.1561/1900000002.

Ориентированные на столбцы СУБД

Abadi, Daniel, Peter Boncz, Stavros Harizopoulos, Stratos Idreaos, and Samuel Madden. 2013. The Design and Implementation of Modern Column-Oriented Database Systems. Hanover, MA: Now Publishers Inc. {Прим. пер.: http://dx.doi.org/10.1561/1900000024; DownloadSummary }

СУБД в памяти

Faerber, Frans, Alfons Kemper, and Per-Åke Alfons. 2017. Main Memory Database Systems. Hanover, MA: Now Publishers Inc. {Прим. пер.: http://dx.doi.org/10.1561/1900000058; DownloadSummary }

Глава 2. Основы B- деревьев

Содержание

Глава 2. Основы B- деревьев

Деревья двоичного поиска

Балансировка деревьев

Деревья для основанных на дисках хранилищах

Основанные на дисках структуры

Устройства жёстких дисков

Устройства твердотельных дисков

Структуры на дисках

Вездесущие B- деревья

Иерархия В- деревьев

Ключи разделители

Сложность поиска в В- деревьях

Алгоритм поиска в В- деревьях

Ключи счётчика

Расщепление узла В- дерева

Слияние узла В- дерева

Выводы

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

Механизмы хранения часто допускают множество версий одной и той же записи данных для её представления в самой базе данных: например, при использовании контроля одновременности множества версий (см. Управление одновременностью множества версий) или при организации перекидных страниц (см. Перекидные страницы). С целью упрощения на данный момент мы предположим, что каждый ключ связан лишь с одной записью данных, которая имеет некое уникальное место.

Одной из наиболее популярных структур является B- дерево {произносится: Бэ- дерево}. Многие системы баз данных с открытым исходным кодом являются основанными на B- деревьях и на протяжении многих лет они доказали охват основной массы вариантов использования.

B- деревья не являются недавним изобретением: они были введены Рудольфом Байером и Эдвардом М. макКрейтом ещё в 1971 и с годами обрели популярность. К 1979 году имелось уже достаточно много вариантов B- деревьев. Дуглас Коммер собрал и систематизировал некоторые из них [COMER79].

Прежде чем мы окунёмся в B-деревья, давайте вначале обсудим зачем нам стоит рассматривать альтернативы традиционным деревьям поиска, таким как, например, двоичные деревья поиска, деревья 2-3 и деревья AVL [KNUTH98]. Для этого давайте напомним что такое деревья двоичного поиска.

Деревья двоичного поиска

Дерево двоичного поиска (BST, binary search tree) это сортированная в памяти структура данных, используемая для действенного поиска ключ- значение. BST состоит из множества узлов. Каждый узел дерева представлен неким ключом, связанным с этим ключом значением и двумя дочерними указателями (отсюда и название двоичного). BST стартует с одного узла, имеющего название корневого узла. В определённом дереве может иметься лишь один корень. Рисунок 2-1 отображает некий пример дерева двоичного поиска.

Рисунок 2-1

Дерево двоичного поиска

Каждый узел расщепляет общее пространство поиска на левое и правое поддеревья, как это показывает Рисунок 2-2: некий ключ узла больше чем все хранящиеся в левом поддереве ключи и меньше чем все ключи хранящиеся в его правом поддереве [DECANDIA07].

Рисунок 2-2

Инварианты дерева двоичного поиска

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

Балансировка деревьев

Операция вставки не следует никакому определённому шаблону и вставка элемента может приводить к такой ситуации, когда ваше дерево не сбалансировано (т.е. одна из его ветвей длиннее всех прочих). Самый худший сценарий показан на Рисунке 2-3 (b), при котором мы в конечном счёте получаем патологическое дерево, которое больше похоже на некий связанный список и вместо логарифмической сложности мы получаем линейную, как это проиллюстрировано Рисунке 2-3 (a).

Рисунок 2-3

Примеры сбалансированного (a) и несбалансированного или патологического (b) деревьев

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

Сбалансированное дерево определяется как имеющее высоту в log₂ N, где N это общее число элементов данного дерева и значение разницы в высоте между любыми двумя поддеревьями не превышает единицы [KNUTH98]. (Данное требование накладывается деревьями AVL и некими прочими структурами данных. В более общем случае деревья двоичного поиска удерживают численную разницу ы высотах между поддеревьями в пределах небольшого постоянного фактора.) Без сбалансированности мы утрачиваем преимущества производительности своего двоичного поиска в структуре дерева и допускаем порядку вставок и удалений определять форму дерева.

В сбалансированном дереве, следуя за указателем влево или вправо, пространство поиска в среднем сокращается вдвое, а потому сложность поиска является логарифмической: O(log₂ N). Когда наше дерево не сбалансировано, в наихудшем случае сложность достигает O(N), так как мы можем получить в конечном счёте ситуацию, при которой все элементы выстраиваются по одну сторону данного дерева.

Вместо добавления новых элементов в одну из ветвей своего дерева и удлиняя её, в то время как прочие остаются пустыми (как это показано на Рисунке 2-3 (b)), хорошее дерево является сбалансированным после каждой операции. Балансировка осуществляется за счёт реорганизации узлов таким образом, чтобы минимизировать высоту дерева и сохранять значение числа узлов с каждой стороны в границах.

Один из способов сохранения своего дерева сбалансированным является выполнение шага разворота после добавления или удаления узлов. Когда операция оставляет некую ветвь несбалансированной (два последующих узла в этой ветви обладают лишь одним потомком), мы можем развернуть узлы вокруг их середины. В показанном на Рисунке 2-4 примере, в процессе разворота нашего среднего узла (3), имеющего названия оси поворота (rotation pivot), он передвигается на один уровень вверх, а его родитель становится его правым потомком

Рисунок 2-4

Пример шага разворота

Деревья для основанных на дисках хранилищах

Как уже упоминалось ранее, несбалансированные деревья имеют наихудшую сложность в O(N). Сбалансированные деревья дают нам в среднем O(log₂ N). В то же самое время, вследствие низкого ветвления (fanout, ветвление это значение максимально допустимого для узла числа потомков), нам приходится достаточно часто выполнять балансировку, перемещение узлов и обновление указателей. Рост стоимости сопровождения делает BST непрактичным для дисковых структур данных [NIEVERGELT74].

Если мы хотим поддерживать на диске некий BST, мы сталкиваемся с рядом проблем. Одна из задач состоит в локальности: так как элементы добавляются случайным образом, нет никакой гарантии того, что вновь создаваемый узел записывается поблизости от своего предка, что означает, что указатели дочернего узла могут расширяться по нескольким страницам диска. До некоторой степени мы можем улучшить это положение, изменяя схему своего дерева и применяя страничные двоичные деревья (см. Страничные двоичные деревья).

Другая задача, тесно связанная со значением стоимости последующих уникальных указателей это высота дерева. Так как двоичные деревья имеют ветвление равное всего лишь двум, высота равна двоичному логарифму от общего числа всех элементов в данном дереве и нам приходится выполнять O(log₂ N) поисков для определения места элемента и вследствие этого, выполнение того же самого числа обменов с дисками. 2-3- Деревья и прочие деревья с низким ветвлением имеют аналогичное ограничение: в то время как они полезны в качестве размещаемых в памяти структур, малый размер узла делает их непрактичными для внешних хранилищ [COMER79].

Безыскусная дисковая реализация BST потребует того же самого числа поисков на диске, сколько имеется сравнений, так как нет никакой концепции локальности. Это заставляет нас следовать поиском некой структуры данных, которая будет демонстрировать это свойство.

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

Высокой степенью ветвления для улучшения локализации значений соседних ключей.

Низкой высоты для снижения общего числа поисков в процессе просмотра- обхода.

[Замечание]
Замечание

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

Основанные на дисках структуры

В общих чертах мы уже обсуждали хранилища в памяти и на диске (см. Главе 2,). Мы можем провести то же самое отличие для специальных структур данных: некоторые лучше приспособлены для использования на диске, а некоторые лучше в памяти.

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

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

Устройства жёстких дисков

Большинство традиционных алгоритмов были разработаны когда шпиндельные диски были наиболее распространённым постоянным носителем хранения, что существенно повлияло на их проектирование. Позднее, новые разработки, такие как флеш устройства, вдохновили новые алгоритмы и изменения в уже имеющихся, эксплуатируя появившиеся возможности такого нового оборудования. В эти дни нарождаются новые типы структур данных, которые оптимизированы для работы с энергонезависимыми хранилищами с байтовой адресацией (например, [XIA17], [KANNAN18]).

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

Самым малым элементом обмена некого шпиндельного устройства является сектор, а потому когда выполняется некая операция, должен быть считан или записан по крайней мере весь сектор целиком. Размер сектора обычно находится в диапазоне от 512 байт до 4 кБ. {Прим. пер.: подробнее о проблемах размера сектора на диске в Обзоре технологии Расширенного формата жёстких дисков Ильи Крутова, IBM.}.

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

Устройства твердотельных дисков

Твердотельные устройства (SSD) не имеют перемещающихся частей: нет никаких вращающихся дисков или головок, которые приходится позиционировать для такого чтения. Типичный SSD строится из ячеек памяти (memory cells), соединённых в цепочки (струны, strings, обычно от 32 до 64 на цепочку), цепочки комбинируются в массивы, массивы собираются в страницы, а страницы сочетаются в блоки [LARRIVEE15].

В зависимости от конкретно применяемой технологии, ячейка может содержать один или множество бит данных. Страницы варьируются в размере для устройств, но обычно их размеры находятся в диапазоне от 2 до 16 кБ. Блоки обычно содержат от 64 до 512 страниц. Блоки организованы в плоскости (planes) и, наконец, плоскости помещаются в чип (кристалл, die). SSD могут иметь один или более чипов. Рисунок 2-5 отображает данную иерархию.

Рисунок 2-5

Схематическая организация SSD

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

Основная часть некого контроллера флеш памяти, отвечающая за соответствие идентификаторов страниц их физическим положениям, отслеживание пустых, записанных и отвергнутых страниц, имеет название FTL (Flash Translation Layer, см. Слой трансляции флеш- устройства для получения дополнительных сведений об FTL). Он также отвечает за сборку мусора, в процессе которого FTL выискивает блоки, которые он способен безопасно удалить. Некоторые блоки всё ещё могут содержать живые страницы. В таком случае он перемещает живые страницы с таких блоков в новые расположения и устанавливает новое соответствие идентификатора страницы для указания сюда. После этого он уничтожает такие неиспользуемые блоки, делая их доступными для записей.

Поскольку в обоих типах устройств (HDD и SSD) мы выполняем адресацию к фрагментам памяти вместо индивидуальных байт (то есть выполняем доступ к блоку данных целиком), большинство операционных систем имеют некую абстракцию блочного устройства [CESATI05]. Она скрывает внутреннюю структуру диска и внутренним образом буферирует операции ввода/ вывода, а потому когда мы считываем отдельное слово из блочного устройства, считывается все содержимое целого блока. Такое обязательство мы не можем игнорировать и всегда обязаны учитывать при работе с располагающимися на диске структурами данных.

В SSD мы не выделяем строго сопоставление произвольного и последовательного ввода/ вывода, как это имеет место для HDD, так как различие в задержках между произвольными и последовательными чтениями не велико. Всё ещё имеется различие, вызываемое упреждением, считыванием непрерывных страниц и внутренней параллельностью [GOOSSAERT14].

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

Запись только полных блоков и комбинирование последовательных записей в одном и том же блоке может способствовать снижению общего числа требуемых операций ввода/ вывода. В последующих главах мы обсудим буферизацию и неизменность как пути достижения этого.

Структуры на дисках

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

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

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

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

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

[Замечание]
Страничные двоичные деревья

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

Рисунок 2-6

Страничные двоичные деревья

Вездесущие B- деревья

Мы отважней чем пчёлы, и ух... длиннее чем деревья...

Винни Пух

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

Как мы уже обсуждали в Деревьях двоичного поиска, B- деревья строятся поверх основания сбалансированных деревьев поиска и отличаются тем, что имеют более высокий порядок ветвления (обладают большим числом дочерних узлов) и меньшей высотой.

В большинстве литературных источников узлы двоичных деревьев рисуются окружностями. Так как каждый узел отвечает только за один ключ и расщепляет свой диапазон на две части, такого уровня подробности достаточно и он интуитивно ясен. В то же самое время, узлы B- дерева часто изображаются прямоугольниками, а блоки указателей также отражаются в явном виде для выделения имеющейся взаимосвязи между дочерними узлами и ключами разделителя. Рисунок 2-7 показывает двоичное дерево, дерево 2-3 и узлы B- дерева бок о бок, что помогает понять сходства и различия между ними.

Рисунок 2-7

Двоичное дерево, дерево 2-3 и узлв B- дерева бок о бок

Ничто не мешает нам перерисовать двоичное дерево точно так же. Обе структуры имеют аналогичную семантику следования указателям, а отличия начинают проявляться в том как сопровождается их балансировка. Рисунок 2-8 показывает это наводит на мысль о сходстве между BST и B- деревьями: в обоих случаях ключи расщепляют само дерево на поддеревья и применяются для навигации по самом у дереву и поиска искомых ключей. Вы можете сравнить его с Рисунком 2-1.

Рисунок 2-8

Альтернативное представление двоичного дерева

B- деревья сортированы: ключи внутри самого узла B- дерева находятся в порядке сортировки. Благодаря этому, для определения места некого искомого ключа мы можем применять некий алгоритм, подобный двоичному поиску. Это также подразумевает, что поиск по B- деревьям обладает логарифмической сложностью. Например, поиск искомого ключа среди 4 миллиардов (4 × 10⁹) элементов занимает около 32 сравнений (см. Сложность поиска в В- деревьях для дополнительных сведений по этому вопросу). Когда нам приходится выполнять дисковое позиционирование для каждого из таких сравнений, это значительно замедлило бы нас, но так как узлы B- дерева хранят десятки и даже сотни элементов, нам всего лишь придётся выполнят по одному позиционированию диска для прыжка на следующий уровень. Позднее в этой главе мы обсудим алгоритм поиска более подробно.

При помощи B- деревьев мы можем действенно выполнять как запросы указания, так и запросы диапазона. Запросы указания, выражаемые в большинстве языков запросов знаком предиката равенства (=) определяют место единственного элемента. С другой стороны, запросы диапазона, выражаемые символами предикатов сравнения (<, >, ≤ и ≥) используются для запроса множества элементов данных в распоряжении.

Иерархия В- деревьев

B- состоят из множества узлов. Каждый узел содержит до N ключей и N + 1 указателей на его дочерние узлы. Эти узлы логически разбиваются на три группы:

Узел корня

Не имеет предков и является основной вершиной этого дерева.

Узлы листьев

Это самые узлы самого нижнего уровня, которые не имеют дочерних узлов {терминальные узлы}.

Внутренние узлы

Это все прочие узлы, соединяющие корень с листьями. Обычно имеется более одного уровня внутренних узлов {нетерминальные узлы}.

Эта иерархия показана на Рисунке 2-9.

Рисунок 2-9

Иерархия узлов И- дерева

Так как B- деревья являют технику организации страниц (то есть они используются для организации страниц с фиксированным размером и навигации по ним), мы часто будем взаимозаменяемо использовать термины узел и страница.

Значение соотношения между ёмкостью такого узла и общим числом ключей, которое он способен содержать именуется заполнением (occupancy).

B- деревья характеризуются их ветвлением (fanout): значением числа ключей, хранимых в каждом узле. Более высокая степень ветвления позволяет гасить общую стоимость структурных изменений, требуемых для удержания дерева в сбалансированном состоянии снижения числа позиционирований за счёт хранения ключей и указателей на дочерние узлы в неком отдельном блоке или во множестве последовательных блоков. Операции балансировки (а именно, расщепления и слияния) включаются когда эти узлы заполнены или почти пусты.

[Замечание]
B⁺- деревья

Мы применяем термин B- дерева в качестве некого зонтика для структур данных, которые совместно применяют все или часть упомянутых свойств. Более точным названием для описываемой структуры данных является B⁺- деревья. [KNUTH98] ссылается на деревья с высоким порядком ветвления как на многоканальные (multiway) деревья.

B- деревья позволяют хранить значения на любом уровне: в корневом, в промежуточных и листовых узлах. B⁺- деревья хранят значения только в узлах листьев. Внутренние узлы хранят лишь ключи разделения, применяемые как руководство для самого алгоритма поиска связанного значения, хранимого в соответствующем уровне листьев.

Поскольку значения в B⁺- деревьях хранятся лишь на уровне листьев, все операции (вставка, обновление, удаление и выборка записей данных) воздействуют лишь на узлы листьев и распространяются на верхние уровни только в процессе расщеплений и слияний.

B⁺- деревья приобрели широкое распространение и мы ссылаемся на них как на B- деревья, аналогично прочим литературным источникам по данной теме. Например, в [GRAEFE11] B⁺- деревья именуются как некое решение по умолчанию, а MySQL и InnoDB именуют свои реализации B⁺- деревьев B- деревом.

Ключи разделители

Хранимые в B- деревьях ключи имеют название записей индекса, ключей разделителя или ячеек делителя. Они расщепляют само дерево на поддеревья (также именуемые ветвями или поддиапазонами), содержащими соответствующие диапазоны ключей. Ключи хранятся в неком сортированном порядке чтобы допускать двоичный поиск. Некое поддерево отыскивается по месту некого ключа и следованию соответствующему указателю с самого верхнего до самого нижнего уровней.

Самый первый указатель в таком узле указывает на то поддерево, которое содержит элементы меньше чем значение первого ключа, а самый последний указатель в этом узле указывает на то поддерево, которое содержит элементы больше чем или равные значению для самого последнего ключа. Прочие указатели ссылаются на поддеревья между значениями двух ключей: Ki-1 ≤ Ks < Ki, где K это множество ключей, а Ks это относящийся к определённому поддереву ключ. Рисунок 2-10 отображает такие инварианты.

Рисунок 2-10

Как ключ разделителя расщепляет некое дерево на поддеревья

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

Что отличает B- деревья, так это то, что они не создаются сверху вниз (в виде бинарных деревьев поиска), а наоборот - снизу вверх. Общее число листовых узлов растёт, что увеличивает общее число внутренних узлов и высоту дерева.

Поскольку B- деревья резервируют дополнительное пространство внутри узлов для последующих вставок и обновлений, использование хранилища дерева может достигать 50%, но обычно наблюдаются более высокие значения. Более высокая загруженность не оказывает влияние на производительность B- деревьев.

Сложность поиска в В- деревьях

Сложность поиска в B- дереве может быть представлен с двух точек зрения: общего числа блочного обмена и общего числа сравнений, осуществляемых в процессе такого поиска.

В терминах числа обменов, значением основания логарифма выступает N (числа ключей в узле). На каждом новом уровне имеется в K раз больше узлов, а следуя за неким дочерним указателем снижает пространство поиска на значение множителя N. В процессе поиска по крайней мере logK M (где M это общее число элементов в этом B- дереве) страниц для решения поиска искомого ключа. Общее число дочерних указателей, которым придётся следовать при проходе от вершины к листьям также равно общему числу уровней, иными словами, значению высоты h данного дерева.

С точки зрения числа сравнений, значением основания логарифма является 2, так как поиск ключа внутри каждого узла осуществляется при помощи двоичного поиска. Каждое сравнение делит пополам пространство поиска, поэтому сложность равна log₂ M.

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

В учебных руководствах и статьях (например, [KNUTH98]), сложность поиска в B- дереве обычно приводится как log M. В анализе сложности обычно не используется основание логарифма, так как изменение значения основания просто добавляет некий постоянный множитель, а умножение на некий постоянный множитель не изменяет сложность. Например, имея ненулевое значение постоянного множителя c, O(|c| × n) == O(n) [KNUTH97].

Алгоритм поиска в В- деревьях

Теперь, когда мы рассмотрели саму структуру и внутреннюю организацию B- деревьев, мы можем определить алгоритмы для поиска, вставки и удалений. Для поиска некой записи в B- дереве нам приходится выполнять отдельный просмотр от корня до листа. Предметом такого поиска является обнаружение искомого ключа или его предка. Обнаружение точного соответствия применяется для запросов указания, обновлений и удалений; поиск его предка полезен для сканирований и вставок диапазона.

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

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

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

Ключи счётчика

В относящейся к данной теме литературе вы можете обнаружить различные способы описания счётчиков ключа и дочерних смещений. [BAYER72] ссылается на определённое зависящее от устройства натуральное число k, которое представляет некий оптимальный размер страницы. В этом случае страницы способны содержать от k до 2k ключей, однако может быть частично заполненной от по крайней мере k + 1 и максимально 2k + 1 указателями на дочерние узлы. Узел корня может содержать от 1 до 2k ключей. Далее вводится некое число l и утверждается, что любая нетерминальная страница может иметь l + 1 ключей.

Другие источники, к примеру, [GRAEFE11], описывают узлы как способные содержать до N ключей разделителей и N + 1 указателей, в остальном семантика и инварианты аналогичны.

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

Расщепление узла В- дерева

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

Когда наш целевой узел не обладает достаточным доступным пространством, мы говорим что этот узел переполнен [NICHOLS66] и приходится расщеплять его на два для помещения таких новых данных. Более точно, этот узел расщепляется при соблюдении следующих условий:

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

Для не являющихся листьями {нетерминальных} узлов: если этот узел способен содержать до N + 1 указателей и вставка ещё одного указателя приводит его к превышению его максимальной ёмкости N + 1.

Расщепление осуществляется посредством выделения необходимого нового узла, переноса в него половины его элементов из подвергающегося расщеплению узла и добавления его самого первого ключа и указателя в его родительский узел. В таком случае мы говорим что продвигается (promoted) новый ключ. Значение индекса, в котором выполняется данное расщепление имеет название точкой расщепления (также именующейся его средней точкой). Все элементы после такой точки расщепления (включая точку расщепления в случае деления нетерминального узла) перемещаются в такой вновь создаваемый братский {того же самого родственного уровня} узел, а все остальные элементы остаются в самом делимом узле.

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

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

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

Рисунок 2-11

Расщепление терминального узла в процессе вставки 11. Новый ключ и продвигаемый ключ показаны серым.

Рисунок 2-12 показывает соответствующий процесс расщепления полностью заполненного нетерминального (то есть корневого или внутреннего) узла в процессе вставки нашего нового элемента 11. Для выполнения расщепления мы вначале создаём какой- то новый узел и перемещаем в него элементы, начинающиеся с индекса N/2 + 1. Сама точка расщепления продвигается в своего родителя.

Рисунок 2-12

Расщепление нетерминального узла в процессе вставки 11. Новый элемент и продвигаемый ключ показаны серым.

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

Не имеет значения расщепляется ли терминальный или нетерминальный узел (то есть содержит ли этот узел ключи и значения или только сами ключи). В случае расщепления листа, ключи перемещаются совместно со связанными с ними значениями.

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

Чтобы подвести итог, расщепление узла выполняется четырьмя шагами:

Выделение нового узла.

Копирование половины элементов из расщепляемого узла во вновь созданный.

Помещение нового элемента в надлежащий узел.

Добавление ключа разделителя и указателя на вновб созданный узел в родителя расщепляемого узла.

Слияние узла В- дерева

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

Если соседние узлы имеют слишком мало значений (то есть когда их занятость падает ниже порогового значения), такие находящиеся на одном родственном уровне узлы сливаются. Подобный случай имеет название потерей значимости (underflow, антипереполнением). [BAYER72] определяет два сценария потери значимости: если два смежных узла имеют общего предка и их содержимые помещаются в один узел, их содержимое следует слить (выполнить конкатенацию); когда их содержимое не умещается в одном узле, ключи перераспределяются между ними для восстановления баланса (см. Повторная балансировка). Более точно, два узла сливаются когда имеют место следующие обстоятельства:

Для терминальных узлов: когда этот узел способен содержать до N и сочетание числа пар ключ- значение в двух соседних узлах меньше чем или равно N.

Для нетерминальных узлов: если этот узел способен содержать до N + 1 и сочетание числа пар ключ- значение в двух соседних узлах меньше чем или равно N + 1.

Рисунок 2-13 показывает слияние при удалении элемента 15. Для этого мы перемещаем элементы из одного из братьев в другой. Как правило, элементы из первого брата перемещаются в левого, но это может быть выполнено и иначе когда фиксирована соответствующий порядок ключа.

Рисунок 2-13

Слияние терминальных узлов

Рисунок 2-14 показывает два нетерминальных узла одного родственного уровня, которые подлежат слиянию в процессе удаления элемента 10. Когда мы соединяем их элементы, они помещаются в одном узле, поэтому мы можем иметь один узел вместо двух. В процессе слияния нетерминальных узлов, нам приходится вытолкнуть соответствующий ключ разделителя из их родителей (то есть понизить его). Общее число указателей уменьшается на один, так как подобное слияние является продвижением удаления соответствующего указателя с нижнего уровня, вызывающего удаления надлежащей страницы. В точности как и при расщеплении, слияние способно продвигаться по всему пути до уровня корня.

Рисунок 2-14

Слияние нетерминальных узлов

Итак, чтобы суммировать, слияние узлов выполняется в три шага, в предположении что соответствующий элемент уже удалён:

Копируем все элементы из своего правого узла в левый.

Удаляем в их предке указатель на правый узел (либо понижаем его в случае слияния нетерминальных).

Удаляем свой правый узел.

Одной из технологий, часто реализуемой в B- деревьях для снижения общего числа расщеплений и слияний является ребалансировка, которая обсуждается в разделе Повторная балансировка.

Выводы

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

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

Мы можем применять эти знания для создания B- деревьев в памяти. Для создания дисковой реализации нам потребуется углубиться в подробности того как выкладывать узлы B- дерева на диск и составить схему диска при помощи форматов кодирования данных.

[Замечание]
Последующее чтение

Если вы желаете узнать больше относительно упоминавшихся в этой главе понятий, вы можете ссылаться на следующие источники:

Деревья двоичного поиска

Sedgewick, Robert and Kevin Wayne. 2011. Algorithms (4th Ed.) Boston: Pearson. {Прим. пер.: https://www.pearson.com/store/p/algorithms/P100000009629 }

{Прим. пер.: Роберт Седжвик, Кевин Уэйн Алгоритмы на Java 4-е издание. http://www.williamspublishing.com/Books/978-5-8459-1781-2.html }

Knuth, Donald E. 1997. The Design and Implementation of Modern Column-Oriented Database Systems. Boston: Addison-Wesley Longman. {Прим. пер.: перевод на русский язык, Искусство программирования, том 2. Получисленные алгоритмы, 3-е издание. Дональд Э. Кнут http://www.williamspublishing.com/Books/sci_Knuth2.html }

Алгоритмы расщепления и слияния B- деревьев

Elmasri Ramez and Shamkant Navathe. 2011. Fundamentals of Database Systems (6th Ed.). Boston: Pearson.

{Прим. пер.: Elmasri Ramez and Shamkant Navathe. 2016. Fundamentals of Database Systems (7th Ed.). Boston: Pearson. https://www.pearson.com/us/higher-education/program/Elmasri-Fundamentals-of-Database-Systems-7th-Edition/PGM189052.html }

Silberschatz, Abraham, Henry F. Korth, and S. Sudarshan. 2010. Database Systems Concepts (6th Ed.). New York: McGraw-Hill. {Прим. пер.: https://www.mheducation.com/highered/product.0073523321.html }

Глава 3. Форматы файлов

Содержание

Глава 3. Форматы файлов

Мотивация

Двоичное кодирование

Типы примитивов

Строки и данные переменной длины

Двоично упакованные данные: Булевы, перечисления и флаги

Общие принципы

Структура страницы

Перекидные страницы

Схема ячейки

Комбинирование ячеек в страницы с прорезями

Управление данными переменной длины

Сопровождение версий

Подсчёт контрольных сумм

Выводы

Обсудив основы семантики B- деревьев, мы теперь готовы к изучению того как именно реализовать на диске B- деревья и прочие структуры. Мы получаем доступ к диску другим способом по сравнению с тем как мы выполняем доступ в основной памяти: с точки зрения разработчика приложения, доступ в памяти в целом прозрачен. Благодаря виртуальной памяти [BHATTACHARJEE17], нам не требуется управлять вручную смещениями. Доступ к дискам выполняется при помощи системных вызовов (см. https://databass.dev/links/54). Обычно нам приходится определять значение смещения внутри своего целевого файла, а затем интерпретировать представление на диске в некий вид, подходящий для основной памяти.

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

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

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

Поскольку основная сложность B- деревьев проистекает из изменчивости, мы обсудим подробности выкладки страниц, их расщепления, перемещения и прочие понятия, применяемые к изменяемым структурам данных. Позднее, при обсуждении Деревьев LSM (см. раздел Деревья LSM), мы сосредоточимся на сортировке и сопровождении, так как именно отсюда проистекает основная сложность LSM.

Мотивация

Создание некого формата файла во многом аналогично тому как мы создаём структуры данных в языках программирования при некой неуправляемой модели памяти. Мы выделяем некий блок данных и нарезаем его любым способом, которым пожелаем, применяя примитивы и структуры фиксированного размера. Когда мы желаем ссылаться на больший фрагмент памяти или на некую структуру переменной длины, мы пользуемся указателями.

Языки программирования с неуправляемой моделью памяти позволяют нам выделять дополнительную память в любой момент времени, когда она нам потребуется (в рамках разумных ограничений) без того чтобы задумываться или беспокоиться относительно того будет или нет доступен непрерывный сегмент памяти, будет ли он фрагментирован или нет, или что произойдёт после того как мы освободим его. На диске нам приходится заботиться о сборке мусора и фрагментации самостоятельно.

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

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

При хранении данных в основной памяти большинства проблем со схемой памяти не существует, их проще решать или они могут быть решены при помощи библиотек сторонних производителей. Например, обработка полей переменной длины и имеющих большой размер данных намного более непосредственная, так как мы можем применять выделение в памяти и указатели и нам не требуется выкладывать их неким особым образом. Всё ещё имеются ситуации, при которых разработчики проектируют особые схемы данных основной памяти для получения преимущества линий кэширования ЦПУ, предварительной выборки и прочих относящихся к оборудованию особенностей, но в целом это делается для целей оптимизации [FOWLER11].

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

Двоичное кодирование

Для действенного сохранения данных на диск их необходимо кодировать при помощи некого формата, который является компактным и прост для сериализации и обратной процедуры. При обсуждении двоичных форматов вы достаточно слышите слова схема размещения (layout). Поскольку у нас нет подобных malloc и free примитивов, а лишь read и write, нам приходится думать о доступе иначе и готовить данные соответствующим образом.

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

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

Типы примитивов

Ключи и значения обладают неким типом, таким как integer, date или string, и могут быть представлены в(последовательно или параллельно) в виде своих сырых двоичных значений.

Большинство численных типов данных представляются в виде значений фиксированной длины. При работе с численными данными со множеством байт важно применять один и то же порядок байт (endianness), причём как для кодирования, так и для декодирования. Порядок следования байт определяет саму последовательность байт:

Big-endian

Этот порядок начинается с наиболее значимого байта (MSB, most-significant byte), за которым следуют байты в порядке убывания значимости. Иными словами, MSB имеет наинизший адрес.

Little-endian

Данный порядок стартует с наименее значимого байта (LSB, least-significant byte), за которым байты в возрастающем порядке значимости.

Рисунок 3-1 иллюстрирует это. Наше шестнадцатеричное 32- битное целое 0xAABBCCDD, где AA это SB, показано как с применением порядка байт big-, так и с little- endian.

Рисунок 3-1

Big- и little- endian порядки байт. Наиболее значимый байт показан серым. Адресация, обозначаемая a, растёт слева направо.

Например, для реконструкции некого 64- битного целого в соответствующем порядке байт RocksDB имеет специфичное для платформы определение, которое помогает указывать цель порядка байт платформы (В зависимости от выбранной платформы - macOS, Solaris, AIX или одно из предпочтений BSD, либо Windows - значение переменной kLittleEndian) устанавливается в зависимости от того будет или нет эта платформа поддерживать little-endian. Когда данная целевая платформа не соответствует значению порядка следования байт (EncodeFixed64WithEndian просматривает значение kLittleEndian и сравнивает его с порядком следования байт), он меняет местами порядок байт при помощи EndianTransform, который способен считывать байтовые значения и изменять на обратный их порядок, добавляя их в конец получаемого результата.

Записи состоят из таких примитивов как числа, строки, булевы значения и их сочетания. Однако, при передаче данных через сетевую среду или при сохранении на диск, мы можем применять только последовательности байт. Это означает, что для отправки или сохранения записи нам приходится выполнять её сериализацию (преобразование в некую непрерывную последовательность байт) и, прежде чем мы сможем использовать её после получения и считывания, нам придётся выполнить обратную процедуру (deserialize) с ней (перевести в последовательность байт, обратно в первоначальный порядок записи).

В двоичных форматах данных мы всегда начинаем с примитивов, которые служат в качестве строительных блоков для более сложных структур. Различные типы могут меняться по размеру. Значение byte состоит из 8 бит, short из 2 байт (16 бит), int из 4 байт (32 бит), а long из 8 байт (64 бит).

Числа с плавающей запятой (например, float и double) представлены своими знаком, Italic и дробной частью и показателем степени. Стандарт IEEE Standard for Binary Floating-Point Arithmetic (IEEE 754) описывает широко распространённые представления чисел с плавающей запятой. Некое 32- битное float представляет значение с одинарной точностью. Например, число с плавающей запятой 0.15652 обладает двоичным представлением, показанном на Рисунке 3-2. Самые первые 23 бита представляют дробную часть, последующие 8 бит представляют показатель степени, а 1 бит представляет знак (является или нет это число отрицательным).

Рисунок 3-2

Двоичное представление числа с плавающей запятой одинарной точности.

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

Величина double представляет значение с плавающей запятой двойной точности [SAVARD05]. Большинство языков программирования обладают средствами для кодирования и декодирования значений с плавающей запятой в их двоичное представление и обратно в своих стандартных библиотеках.

Строки и данные переменной длины

Все типы числовых примитивов имеют некий фиксированный размер. Составление более сложных значений воедино во многом схоже со struct в C (Будет не лишним отметить, что компиляторы могут добавлять в структуры заполнение. Это может нарушать наше предположение о точных значениях смещений байт и их места. Вы можете ознакомиться со структурами упаковки дополнительно здесь). Вы можете сочетать значения примитивов в структуры и применять массивы фиксированного размера или указатели на прочие области в памяти.

Строки и прочие типы данных переменной длины (такие как массивы данных с фиксированным размером) могут подвергаться сериализации как некие числа, представляя значение длины такого массива или строки с последующим size байт: реальными данными. Для строк такое представление часто именуется как Строки UCSD или Строки Pascal, носящие название в честь популярной реализации языка программирования Pascal. Мы можем выразить это в псевдокоде следующим образом:

String
{
    size    uint_16
    data    byte[size]
}

Некой альтернативой для строк Pascal являются завершающиеся нулём строки, в которых считывание потребляет такую строку байт пока не будет достигнут символ конца строки. Подход строк Pascal обладает определёнными преимуществами: он позволяет определять длину строки за постоянное время вместо выполнения итераций по содержимому строки, а некая особая для языка программирования строка может составляться разделением size байт в памяти и пересылкой полученного массива байт в некий построитель строки.

Двоично упакованные данные: Булевы, перечисления и флаги

Булевы значения могут представляться либо при помощи отдельного байта, либо кодированием true и false как значений 1 и 0. Поскольку булевы имеют лишь два значения, применение целого байта для его представления расточительно и разработчики часто собирают булевы значения вместе в группы по восемь, причём каждое булево занимает всего лишь один бит. Мы говорим что каждый бит 1 это установка (set), а каждый бит 0 это сброс (unset) или пусто (empty).

Enums, сокращение от enumerated types (перечислимых типов), могут представляться как целые и часто применяются в двоичных форматах и протоколах взаимодействия. Enums используются для представления часто повторяющихся значений с низкой кардинальностью (мощностью). Например, мы можем закодировать тип некого узла B- дерева при помощи enum:

enum NodeType {
   ROOT,     // 0x00h
   INTERNAL, // 0x01h
   LEAF      // 0x02h
};

Другим тесно связанным понятием являются флаги, некий вид сочетания упакованных булевых и enums. Флаги могут представлять неизменные исключительные именованные булевы параметры. Например, мы можем применять флаги для обозначения того будет или нет данная страница содержать значения ячеек, будут ди такие значения иметь фиксированных размер или переменный размер и будут ли иметься или нет страницы переполнения, связанные с этим узлом. Так как каждый бит представляет некое значение флага, мы можем применять для масок лишь степень двух (так как степень двух в двоичном всегда имеет отдельный бит установки; например, 2³ == 8 == 1000b, 2⁴ == 16 == 0001 0000b и т.п.):

int IS_LEAF_MASK         = 0x01h; // bit #1
int VARIABLE_SIZE_VALUES = 0x02h; // bit #2
int HAS_OVERFLOW_PAGES   = 0x04h; // bit #3

Также как и булевы, значения флагов могут считываться из своего упакованного значения при помощи битовых масок и побитовых операторов. Например, для установки бита для одного определённого флага мы можем воспользоваться побитовым OR (|) и некой битовой маской. Вместо битовой маски мы можем применить битовый сдвиг (<<) и некий индекс бита. Для сброса значения бита мы можем воспользоваться побитовым AND (& и побитовым оператором отрицания (~). Для проверки того установлено или нет значение бита n, мы можем сравнить результат побитового AND с 0:

// Установка значения бита
flags |= HAS_OVERFLOW_PAGES;
flags |= (1 << 2);

// Сброс значения бита
flags &= ~HAS_OVERFLOW_PAGES;
flags &= ~(1 << 2);

// Проверка того установлен или нет бит
is_set = (flags & HAS_OVERFLOW_PAGES) != 0;
is_set = (flags & (1 << 2)) != 0;

Общие принципы

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

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

Рисунок 3-3

Организация файла

Многие хранилища данных обладают некой фиксированной схемой, определяющей общее число, порядок и тип полей, которые способна содержать данная таблица. Обладание некой фиксированной схемой способствует снижению хранимых на диске данных: вместо повторения записи названий полей мы можем применять их позиционные идентификаторы.

Когда вы желаете спроектировать некий формат для каталога своей компании, хранимых имён, дат рождения, налоговых номеров и пола для каждого сотрудника, мы можем применять различные подходы. Мы можем хранить все поля фиксированного размера (такие как дата рождения и налоговый номер) в самом заголовке данной структуры, за которым следуют поля с изменяемым размером:

Fixed-size fields:
| (4 bytes) employee_id                |
| (4 bytes) tax_number                 |
| (3 bytes) date                       |
| (1 byte)  gender                     |
| (2 bytes) first_name_length          |
| (2 bytes) last_name_length           |

Variable-size fields:
| (first_name_length bytes) first_name |
| (last_name_length bytes) last_name   |

Теперь, для доступа к first_name мы можем отрезать first_name_length байт после его области физического размера. Для доступа к last_name мы можем определить его начальное положение проверив значение размера предшествующих ему полей переменного размера. Во избежание вычислений, включающих в себя множество полей, мы можем закодировать и смещение и длину, оба значения, на область фиксированного размера. В таком случае мы можем определять местоположение любого поля переменной длины по отдельности.

Построение более сложных структур обычно вовлекает построение иерархий: поля составляются из примитивов, ячейки составляются из полей, страницы составляются из ячеек, разделы составляются из страниц, области составляются из разделов и так далее. Не существует строгих правил, которым вам тут надлежит следовать, причём все они зависят от того вида данных, для которого вам следует создавать формат.

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

Структура страницы

Системы баз данных хранят записи данных в файлах данных и индексации. Эти файлы разделяются на элементы фиксированного размера, имеющие название страниц, которые часто имеют размер произведения блоков файловой системы. Обычно страницы находятся в диапазоне от 4 до 16 кБ.

Давайте взглянем на соответствующий образец размещаемого на диске узла B- дерева. С точки зрения некой структуры, в B- деревьях мы имеем отличия между терминальными узлами, которые содержат ключи и записи данных и нетерминальными узлами, которые содержат ключи и указатели на прочие узлы. Каждый узел B- дерева занимает одну страницу или множество связанных воедино страниц, поэтому в контексте B- деревьев сами термины узел и страница (а порой даже блок) часто взаимозаменяемы.

Первоначальная статья B- дерева [BAYER72] описывает простую организацию страниц для записей данных фиксированного размера, в которых каждая страница это всего лишь конкатенация триплетов, как это показано на рисунке 3-4: ключи обозначаются как k, связанные с ними значения как v, а указатели на дочерние страницы обозначаются как p.

Рисунок 3-4

Организация страницы для записей фиксированного размера

Такой подход прост для его реализации, однако он обладает рядом тёмных сторон:

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

Он не допускает управления или действенного доступа к записям переменного размера и работает лишь с записями фиксированного размера.

Перекидные страницы

При хранении записей переменной длины основная проблема состоит в управлении свободным пространством: восстановлении того пространства, которое занято удаляемыми записями. Если мы попытаемся поместить запись с размером n в некое пространство, которое изначально было занято записью с размером m, пока не выполняется m == n, или мы не можем отыскать другую запись, которая в точности имеет размер m - n, это пространство остаётся незанятым. Аналогично, некий сегмент с размером m не может применяться для хранения записей с размером k, когда k больше чем m, поэтому последняя будет вставляться без возврата имеющегося неиспользуемым пространства.

Для упрощения управления пространством под записи переменного размера мы можем расщеплять свою страницу на сегменты с фиксированным размером. Однако мы в конце концов когда мы следуем этим путём, мы также получаем трату пространства впустую. Например, когда мы применяем размер сегмента в 64 байта, пока размер соответствующей записи не является произведением с 64, мы теряем 64 - (n modulo 64) байт, где n является размером вставляемой записи. Иными словами, пока запись не кратна 64, один из её блоков будет заполнен лишь частично.

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

Чтобы суммировать, нам необходим некий формат страницы, который позволит нам:

Сохранять записи переменного размера с минимальными накладными расходами.

Возвращать занимаемое удаляемыми записями пространство.

Ссылаться на записи из этой страницы без необходимости заботы о их точном местоположении.

Для эффективного хранения записей переменной длины, таких как строки, BLOB (binary large objects) и т.п., мы можем применять технологию организации с названием Перекидных страниц (то есть страниц с прорезями, slotted page) [SILBERSCHATZ10] или Перекидного каталога [RAMAKRISHNAN03]. Такой подход используется многими базами данных, например, PostgreSQL.

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

Некая перекидная страница обладает заголовком фиксированного размера, который содержит важные сведения относительно самой страницы и её ячеек (см. Заголовок страницы). Ячейки могут отличаться в размере и способны содержать произвольные данные: ключи, указатели, записи данных и т.п.. Рисунок 3-5 отображает организацию Перекидных страниц, при которой каждая страница имеет некую область сопровождения (заголовок), ячейки и указатели на них.

Рисунок 3-5

Перекидные страницы

Давайте рассмотрим как этот подход решает обозначенные нами в предыдущем разделе задачи:

Минимальные накладные расходы: единственными накладными расходами, вносимыми Перекидными страницами являются связанные с массивом указателей для хранения смещений для точного позиционирования мест нахождения хранимых записей.

Возврат пространства: пространство может возвращаться путём деврагментации и перезаписи такой страницы.

Динамическая схема: извне данной страницы на такие раскладушки можно ссылаться лишь по их идентификаторам, поэтому знание точного места является внутренним для такой страницы.

Схема ячейки

Применяя флаги, перечислимые (enums) и примитивные значения, мы можем начинать проектирование схемы ячейки, затем сочетать ячейки в страницу и составлять из таких страниц некое дерево. На уровне некой ячейки у нас имеется разница между ячейками ключей и ключ- значение. Ячейки с ключом содержат некий разделительный ключ и указатель на страницу между двумя соседними указателями. Ячейки ключ- значение содержат ключи и связанные с ними записи данных.

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

Для составления некой ячейки ключей нам требуется знать:

Тип ячейки (может подразумеваться по самой странице метаданных)

Размер ключа

Идентификатор соответствующей дочерней страницы, на которую указывает эта ячейка

Байты ключа

Схема ячейки ключа переменной длины может выглядеть подобно этому (ячейка фиксированного размера не будет иметь описателя размера на самом уровне ячейки):

0                4               8
+----------------+---------------+-------------+
| [int] key_size | [int] page_id | [bytes] key |
+----------------+---------------+-------------+

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

Ячейки ключ- значение содержат записи данных вместо значений идентификаторов дочерних страниц. Во всём остальном их структура аналогична:

Тип ячейки (может подразумеваться по самой странице метаданных)

Размер ключа

Размер значения

Байты ключа

Байты записи данных

0              1                5 ...
+--------------+----------------+
| [byte] flags | [int] key_size |
+--------------+----------------+

5                  9                    .. + key_size
+------------------+--------------------+----------------------+
| [int] value_size |     [bytes] key    | [bytes] data_record  |
+------------------+--------------------+----------------------+

Здесь вы могли заметить основное отличие между значениями смещения и идентификатора страницы. Поскольку страницы обладают неким фиксированным размером и управляются имеющимся кэшем страниц (см. Управление буфером), нам требуется хранить лишь значение идентификатора страницы, который позднее транслируется в реальное смещение в самом файле при помощи таблицы поиска. Смещения ячейки локальны для страницы и отсчитываются относительно начального смещения самой страницы: таким образом мы можем применять меньшую кардинальность целого для удержания представления в более компактном виде.

[Замечание]
Данные переменного размера

Для ключа и значения нет необходимости в фиксированном размере. Как ключ, так и значие могут обладать переменной длиной. Их местоположение может вычисляться по заголовку ячейки постоянного размера с применением смещений.

Для определения местоположения ключа мы пропускаем установленный заголовок и считываем байты key_size. Аналогично, для локализации искомого значения мы можем пропустить сам заголовок полюс дополнительно key_size байт и считать байты value_size.

Существуют различные способы выполнения одного и того же; например, сохраняя общий размер и вычисляя величину размера значения вычитанием. Всё сводится к тому чтобы иметь достаточно сведений для нарезки самой ячейки на составные части и восстановления её закодированных данных.

Комбинирование ячеек в страницы с прорезями

Для организации ячеек в страницы, мы можем пользоваться той техникой Перекидных страниц, которую мы обсудили в разделе Перекидные страницы. Мы добавляем ячейки в правую сторону такой страницы (со стороны её конца) и сохраняем смещения/ указатели в левой стороне данной страницы, как это показано на Рисунке 3-6.

Рисунок 3-6

Направления роста смещений и ячеек

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

Давайте рассмотрим некий пример страницы, содержащей имена. В данную страницу добавляются два имени, причём порядок их вставки таков: Tom и Leslie. Как вы можете видеть на Рисунке 3-7, их логический порядок (в данном случае алфавитный), не соответствует порядку вставки (тому порядку, в котором они вставлялись в конец этой страницы). Ячейки располагаются в порядке вставки, но смещения сортируются повторно делая для наз возможным двоичный поиск.

Рисунок 3-7

Добавляемые в конец в произвольном порядке записи: Tom, Leslie

Теперь мы бы хотели добавить в эту страницу ещё одно имя: Ron. Новые данные добавляются к верхней границе свободного пространства этой страницы, однако смещениям ячеек приходится предохранять установленный лексикографический порядок ключей: Leslie, Ron, Tom. Для этого нам приходится повторно упорядочить смещения ячеек: указатели после такой точки вставки сдвигаются вправо для предоставления пространства под ячейку Ron, как это можно видеть на Рисунке 3-8.

Рисунок 3-8

Добавляемые в конец ещё одной записи: Ron

Управление данными переменной длины

Удаление некого элемента из определённой страницы не удаляет реальную ячейку и не сдвигает прочие ячейки для перераспределения высвобождающегося свободного пространства. Вместо этого ячейка может быть помечена как удалённая, а расположенный в памяти список доступности может быть обновлён этим значением освобождаемой памяти с освобождением данного значения указателя. Такой список доступности хранит смещения освобождённых сегментов и их размеров. При вставке новой ячейки мы вначале проверяем список доступности для обнаружения того имеется ли некий сегмент, в котором она могла бы уместиться. На Рисунке 3-9 вы можете видеть некий образец фрагментированной памяти с доступными сегментами.

Рисунок 3-9

Фрагментированная страница и список доступности. Занятые страницы показаны серым. Линии с точуой представляют собой указатели на незанятые области из имеющегося списка доступности.

SQLite именует не занятые сегменты свободными блоками (freeblocks) и хранит некий указатель на самый первый свободный блок в заголовке самой страницы. Кроме того, он хранит общее число доступных байт внутри самой страницы для быстрой проверки будем ли мы иметь возможность размещать некий новый элемент в этой странице после её дефрагментации.

Подгонка вычисляется на основании определённой стратегии:

Подгоняется первый

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

Укладывается лучше всего подходящий

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

Когда мы не можем найти некого сегмента с последовательно идущими байтами для размещения своей новой ячейки, но имеется достаточно доступных байт, живые ячейки считываются и перезаписываются, выполняя дефрагментацию имеющейся страницы и возвращённого пространства под новые записи. Когда остающегося свободным пространства недостаточно даже после его дефрагментации, нам приходится создавать некую страницу переполнения (см. раздел Страницы переполнения).

[Замечание]
Замечание

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

Чтобы суммировать, для упрощения схемы B- дерева мы предполагаем что каждый узел занимает некую отдельную страницу. Страница состоит из заголовка с фиксированным размером, блок указателей ячеек и ячейки. Ячейки содержат ключи и указатели на те страницы, которые отвечают за дочерние узлы или связанные записи данных. B- деревья используют простую иерархию указателей: указатели страниц задают место своих дочерних узлов в имеющемся файле дерева, а смещения ячеек на расположение ячеек внутри такой страницы.

Сопровождение версий

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

Это можно выполнять различными способами. Например, Apache Cassandra применяет в названиях файлов префиксы используемых версий. Таким образом, вы можете сказать какая версия у данного файла даже не открывая его. Когда версия 4.0, некое название файла имеет префикс названия файла na, скажем, na-1-big-Data.db. Более старые файлы имеют иные префиксы: записанные с версией 3.0 файлы имеют префикс ma.

В качестве альтернативы, номер версии может храниться в неком отдельном файле. Например, PostgreSQL хранит номера версий в своём файле PG_VERSION.

Номер версии также может храниться непосредственно в самом заголовке файла индексов. В таком случае часть такого заголовка (или даже заголовок целиком) приходится кодировать в неком формате, который не меняется между версиями. После отыскания того с какой версией кодируется данный файл, мы можем создать некий особый для версии считыватель чтобы интерпретировать его содержимое. Некие форматы идентифицируют номер версии при помощи магических чисел, которые мы обсуждаем подробнее в разделе Магические числа.

Подсчёт контрольных сумм

Файлы на диске могут разрушаться или искажаться за счёт ошибок программного обеспечения и отказов оборудования. Для предварительного выявления таких проблем и чтобы избегать распространения нарушенных данных в прочие системы или даже узлы мы можем применять контрольные суммы и циклические проверки избыточности (CRC, cyclic redundancy checks).

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

Контрольные суммы предоставляют самый слабый вид гарантии и не способны выявлять нарушения во множестве бит. Обычно они вычисляются при помощи XOR с частичными проверками или суммированиями [KOOPMAN15].

CRC способны способствовать выявлению ошибок разрушений (например, когда получены нарушения во множестве) [STONE98]. Критически важно выявлять ошибки во множестве бит, так как значительное процентное соотношение отказов в сетевых средах взаимодействия и устройствах хранения выявляется именно так.

[Предостережение]
Предостережение

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

Перед записью своих данных на диск, мы вычисляем их контрольные суммы и записываем их вместе с этими данными. При обратном считывании мы вычисляем их контрольную сумму снова и сопоставляем её с записанным значением. Если имеется некое расхождение в контрольных суммах, мы знаем что произошло разрушение и нам не следует пользоваться считанными данными.

Поскольку вычисления некой контрольной суммы всего файла часто не является практичным, и скорее всего придётся считывать его содержимое целиком при каждом к нему обращений, обычно вычисляются контрольные суммы страниц и помещаются в заголовок самой страницы. Тем самым контрольные суммы могут быть более прочными (ибо выполняются для некого небольшого подмножества данных), а файл целиком не будет разрушен когда нарушение содержится в единственной странице.

Выводы

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

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

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

Эти принципы могут применяться для составления двоичных форматов дисковых структур и сетевых протоколов.

[Замечание]
Последующее чтение

Если вы желаете узнать больше относительно упоминавшихся в этой главе понятий, вы можете ссылаться на следующие источники:

Технологии организации файла

Folk, Michael J., Greg Riccardi, and Bill Zoellick. 1997. File Structures: An ObjectOriented Approach with C++ (3rd Ed.) Boston: Addison-Wesley Longman. https://www.amazon.com/File-Structures-Object-Oriented-Approach-C/dp/0201874016.

Giampaolo, Dominic. 1998. Practical File System Design with the Be File System (1st Ed.). San Francisco: Morgan Kaufmann. https://www.amazon.com/Practical-System-Design-Dominic-Giampaolo/dp/1558604979.

Vitter, Jeffrey Scott. 2008. “Algorithms and data structures for external memory.” Foundations and Trends in Teoretical Computer Science 2, no. 4 (January): 305-474. https://doi.org/10.1561/0400000014.

Глава 7. Хранилище структурированного журнала

Содержание

Глава 7. Хранилище структурированного журнала

Деревья LSM

Структура деревьев LSM

Двухкомпонентные LSM деревья

Многокомпонентные LSM деревья

Таблицы в памяти

Обновления и удаления

Поиск по дереву LSM

Слияние итераций

Улаживание

Сопровождение в деревьях LSM

Упаковка по уровням

Стягиваемая по размеру упаковка

Чтение, запись и увеличение пространства

Предположение RUM

Подробности реализации

Таблицы сортированных строк

Фильтры Блюма

Список пропусков

Доступ к дискам

Сжатие

Неупорядоченное хранилище LSM

Bitcask

WiscKey

Одновременность в деревьях LSM

Составление журналов в стек

Слой трансляции флеш- устройства

Журналирование файловой системы

Составление стека LLAMA и Mindful

SSD Open-Channel

Выводы

Заключение Части I

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

- Пэт Хелланд

Когда бухгалтерам приходится изменять имеющуюся запись, вместо того чтобы стирать ластиком уже имеющееся значение, они создают некую новую запись с исправлением. При публикации соответствующего квартального отчёта он может содержать незначительные изменения, исправления и корректировку результатов их предыдущего квартала. Для вывода самой нижней строки вам придётся пройти по всем записям и вычислять подитоги [HELLAND15].

Аналогично, неизменные структуры хранения не допускают изменений в своих имеющихся файлах: таблицы записываются один раз и никогда не изменяются снова. Вместо этого в конец соответствующего нового файла добавляются новые записи, а чтобы обнаружить окончательный результат (или заключить об его отсутствии), записи приходится восстанавливать из множества файлов. В противоположность этому, изменяемые структуры хранения модифицируют записи на диски по месту хранения.

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

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

Как это делают и прочие книги и статьи по данному предмету, мы применяем B- деревья в качестве обычного примера изменяемой структуры и Деревья слияния структурированного журнала (Log-Structured Merge Trees, LSM- деревья) как некий образец неизменной структуры. Неизменные LSM- деревья применяют хранилище с добавлением исколючительно в конец и согласование слияний, а B- деревья размещают записи данных на диске и обновляют страницы по их первоначальным смещениям в своём файле.

Структуры хранения с обновлением по месту размещения оптимальны для производительности чтения [GRAEFE04]: после определения их места на диске, данная запись может быть возвращена запросившему её клиенту. Это достигается за счёт затрат в производительности записи: для обновления имеющейся записи данных по её месту, вначале требуется её найти. С другой стороны, хранилище с исключительной записью в конец оптимизировано под производительность записи. Операциям записи не приходится отыскивать место на диске для его перезаписи. Тем не менее, это делается за счёт затрат на чтения, которым приходится выполнять выборку множества версий записей данных и согласовывать их.

До сих пор мы в основном обсуждали изменяемые структуры хранения. Мы затронули такую тему неизменности при обсуждении B- деревьев копирования- записью (см. раздел Копирование записью), FD- деревья (см. раздел FD- деревья) и Bw- деревья (см. раздел Bw- деревья). Однако имеются и прочие способы реализации неизменных структур.

По причине своей структуры и подхода к построению, принятого в изменяемых B- деревьях, большинство операций ввода/ вывода в процессе чтения, записи и сопровождения являются случайными. Всякой операции записи вначале требуется определить некую страницу, которая содержит запись данных и лишь затем может изменять её. B- деревья требуют расщепления и слияния узлов, которые перемещают уже сохранённые записи. По истечению некоторого промежутка времени, страницы B- дерева могут потребовать работ по сопровождению. Страницы фиксированы в размере и некоторое пространство зарезервировано под последующие записи. Другая проблема состоит в том, что даже когда изменяется лишь одна ячейка в определённой странице, приходится переписывать всю страницу целиком.

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

Деревья LSM

При обсуждении B- деревьев мы заключили что накладные расходы пространства и прирост записей могут улучшаться за счёт буферизации. В целом существуют два способа буферизации, которые можно применять к различным структурам хранения: затягивание записи на расположенные на диске страницы (что мы наблюдали для FD- деревьев и для WiredTiger) или превращать операции записи в последовательные.

Одно из наиболее популярных не изменяемых дисковых структур хранения, LSM дерео, для достижения последовательных записей пользуется буферизацией и хранилищем с исключительным добавлением в конец. Такое LSM дерево выступает неким вариантом располагающейся на диске структуры, аналогичной B- дереву, в которой узлы заняты целиком, являясь оптимизированными для последовательного дискового доступа. Это понятие впервые было ведено в статье Патрика О'Нийла и Эварда Ченга [ONEIL96]. Деревья слияния структурированных журналов (Log-structured merge trees) получили своё название от файлсистем со структурированными журналами, которые записывают на диск все изменения в неком подобном журналу файле[ROSENBLUM92].

[Замечание]
Замечание

LSM деревья записывают неизменяемые файлы и сливают их со временем воедино. Эти файлы обычно содержат некую собственную индексацию для содействия читателям в эффективном определении местоположения данных. Даже хотя LSM деревья часто представляются в виде некой альтернативы B- дервьям, распространённым является использование B- деревьев в качестве собственной внутренней структуры индексации неких неизменяемых файлов LSM деревьев.

Слово "слияние" (merge) в LSM дереве указывает, что по причине своей неизменности, содержимое деревьев сливается при помощи некого подхода, аналогичного сортировке слиянием. Это происходит в процессе сопровождения для высвобождения пространства, занимаемого имеющимися избыточными копиями, а также в процессе считываний перед тес как содержимое может быть возвращено своему пользователю.

LSM деревья откладывают записи файла данных и буферируют изменения в расположенной в памяти таблице. Эти изменения затем распространяются записью их содержимого прочь в неизменяемые дисковые файлы. Все записи данных остаются доступными в памяти пока такие файлы не окажутся целиком сохранёнными.

Сохранение файлов данных неизменными способствует последовательным записям: данные записываются на диск единым проходом и файлы являются дописываемыми исключительно в конец. Изменяемые структуры могут предварительно выделять блоки в неком отдельном проходе (например, индексно- последовательный метод доступа - ISAM, indexed sequential access method [RAMAKRISHNAN03] [LARSON81]), однако последовательным доступам всё ещё требуются произвольные чтения и записи. неизменные структуры позволяют нам располагать записи данных последовательно во избежание фрагментации. Кроме того, неизменные файлы обладают более высокой плотностью: мы не резервируем никакое дополнительное пространство для записей данных, которые мы намерены записывать в последствии, либо для ситуаций, при которых обновления записей потребуют большего пространства нежели изначально записанные.

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

В соответствии с архитектурой, чтения и записи не пересекаются, поэтому данные на диске могут считываться и записываться без блокирования сегментов,то значительно упрощает одновременный доступ. В противоположность этому, изменяемые структуры пользуются иерархическими блокировками и защёлками (дополнительные сведения о блокировках и защёлках вы можете найти в разделе Управление одновременностью) для обеспечения целостности дисковых структур данных и допуска множество одновременных читателей, но требуя исключительного обладания поддеревом для писателей. Механизмы хранения на основе LSM применяют линеаризуемые в памяти представления данных и индексных файлов и им приходится лишь ограждать одновременные доступ к своим структурам для управления ими.

И B- деревья и LSM деревья для оптимизации производительности требуют некого ухода, однако по различным причинам. Так как общее число выделяемых файлов неуклонно растёт, LSM деревьям приходится сливать и перезаписывать файлы чтобы быть уверенным что в процессе чтения доступ осуществляется к наименьшему возможному числу файлов, ибо запрашиваемые записи данных могут быть разбросаны по множеству файлов. С другой стороны, изменяемые файлы могут частично или целиком перезаписываться для снижения фрагментирования и возврата пространства, занимаемого обновлениями или удалёнными записями. Конечно, точный объём выполняемых процессом уборки работ в сильной степени зависит от особенностей конкретной реализации.

Структура деревьев LSM

Мы начинаем с упорядоченных LSM деревьев [ONEIL96], в которых поля хранят сортированные записи данных. Позднее, в разделе Неупорядоченное хранилище LSM, мы также обсудим структуры, которые позволяют хранить данные в порядке их вставки, что имеет некие очевидные преимущества по пути их записи.

Как мы только что обсуждали, LSM деревья состоят из компонентов меньшего размера, располагающихся в памяти и на диске. Для записи неизменного содержимого н диск, вначале необходимо их буферировать в памяти и сортировать их содержимое.

Расположенный в памяти компонент (часто именуемый memtable, таблицей в памяти) изменяемый: он буферирует записи данных и служат целью операций чтения и записи. Содержимое таблицы в памяти сохраняется на диск пр достижении его размером настроенного порогового значения. Обновления таблицы в памяти не влечёт за собой дискового доступа и не требует связанных с вводом/ выводом затрат. Для обеспечения долговечности записей данных отдельного файла журнала упреждающей записи, аналогичного обсуждавшемуся нами в разделе Восстановление. Записи данных добавляются в конец этого журнала и фиксируются в памяти перед отправкой подтверждения своему клиенту.

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

Располагающиеся на диске компоненты строятся путём сброса (flushing) буферированного в памяти содержимого на диск. Расположенные на диске компоненты используются исключительно для чтения: буферированное содержимое сохраняется и файлы никогда не изменяются. Это позволяет нам мыслить в терминах простых операций: записей в некую таблицу в памяти и чтения с диска и из таблицы в памяти, слияний и удалений файлов.

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

Двухкомпонентные LSM деревья

Мы различаем двухкомпонентные и многокомпонентные LSM деревья. Двухкомпонентные LSM деревья имеют лишь один дисковый компонент, составляемый из неизменяемых сегментов. Дисковый компонент организован как B- дерево со 100% занятостью узлов и доступными только для чтения страницами.

Расположенное в памяти содержимое дерева сбрасывается на диск частями. В процессе сброса, для каждого сбрасываемого поддерева из памяти мы определяем соответствующее поддерево на диске и записываем содержимое слияния располагаемого в памяти сегмента с дисковым поддеревом в некий новый сегмент на диске Рисунок 7-1 отображает деревья в памяти и на диске перед слиянием.

Рисунок 7-1

Двухкомпонентное LSM дерево перед сбросом. Сбрасываемые сегменты, расположенные в памяти и на диске показаны серым.

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

Рисунок 7-2

Двухкомпонентное LSM дерево после сброса. Слитое содержимое показано серым. Показанные пунтиром блоки отображают отброшенные дисковые сегменты.

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

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

При реализации слияний и сброса поддеревьев, нам приходится гарантировать три момента:

Каак только стартует процес сброса, всем новым записям приходится переходить в новую таблицу в памяти.

На протяжении сброса данного поддерева и расположенное на диске и сбрасываемое из памяти поддеревья должны оставаться доступными на чтение.

После выполнения сброса, публикация слитого содержимого и отбрасывания не подвергшегося слиянию располагаемого на диске и в памяти содержимого должны выполняться атомарно.

Даже несмотря на то, что двухкомпонентные LSM деревья могут быть полезными для поддержки индексных файлов, на момент написания книги автору не известно ни об одной реализации. Это можно объяснить свойствами прироста записей для такого подхода: слияния относительно частые, поскольку они включаются сбросами таблиц в памяти.

Многокомпонентные LSM деревья

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

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

Наличие потребности чтения множества источников вместо всего одного может быть достаточно затратным. Для смягчения этой проблемы и удержания числа таблиц минимальным, периодически включается процесс слияния, носящий название упаковки (compaction, см. раздел Сопровождение в деревьях LSM). Упаковка выхватывает некоторые таблицы, считывает их содержимое, сливает его и записывает результаты слияния в новый комбинированный файл. Одновременно с появлением таких новых таблиц слияния отвергаются старые таблицы.

Рисунок 7-3 отображает жизненный цикл данных многокомпонентного LSM дерева. Когда оно становится слишком большим,его содержимое сбрасывается на диск для создания располагающихся на диске таблиц. Затем множество таблиц сливаются воедино для создания таблиц большего размера.

Рисунок 7-3

Жизненный цикл данных многокомпонентного LSM дерева

Остальная часть данной главы посвящена многокомпонентным LSM деревьям, болкам построения и процессу их сопровождения.

Таблицы в памяти

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

На Рисунке 7-4 вы можете видеть компоненты определённого LSM дерева, взаимосвязи между ними и операции, которые совершают переход между ними:

Текущая таблица в памяти (Current memtable)

Получает записи и обслуживает чтения.

Сбрасываемая таблица в памяти (Flushing memtable)

Доступна для чтения.

Цель сброса на диске (On-disk flush target)

Не принимает участия в операциях чтения, ибо её содержимое не завершено.

Сброшенные таблицы (Flushed tables)

Доступны для чтения как только отвергнута соответствующая сбрасываемая таблица в памяти.

Упаковываемые таблицы (Compacting tables)

Сливаемые в данный момент и располагаемые на диске таблицы.

Упакованные таблицы (Compacted tables)

Созданные из сброшенных проих упаковываемых таблицы.

Рисунок 7-4

Структура компонентов LSM.

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

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

Обновления и удаления

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

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

Давайте рассмотрим некий пример. Сброшенная на диск таблица содержит запись данных v1? связанную с ключом k1, а соответствующая таблица в памяти содержит значение v2:

Disk Table        Memtable
| k1 | v1 |       | k1 | v2 |

Если мы только удалим v2 из таблицы в памяти и сбросим её, мы в действительности воскресим v1, так как это будет единственное значение, связанное с данным ключом:

Disk Table        Memtable
| k1 | v1 |       ∅

По этой причине удаления необходимо записывать в явном виде. Это можно осуществлять некой специальной удаляющей записью (порой носящей название надгробия - tombstone - или некого дремлющего сертификата - dormant certificate), указывающего удаление этой записи данных со связанным с ней определённым ключом:

Disk Table        Memtable
| k1 | v1 |       | k1 | <tombstone> |

Наш процесс урегулирования прихватывает надгробия и отфильтровывает теневые значения.

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

Предикаты могут иметь форму DELETE FROM table WHERE key ≥ "k2" AND key < "k4" и могут получать некий диапазон соответствия. Apache Cassandra реализует этот подход и именует его диапазоном надгробий (range tombstones). Некий диапазон надгробий охватывает какой- то диапазон ключей вместо единственного ключа.

При применении диапазона надгробий, по причине перекрывающихся диапазонов и границ располагаемых на диске таблиц, разрешающие правила надлежит рассматривать аккуратно. Например, наша следующая комбинация скроет свзанные с k2 и k3 записи данных из нашего окончательного результата:

Disk Table 1      Disk Table 2
| k1 | v1 |       | k2 | <start_tombstone_inclusive> |
| k2 | v2 |       | k4 | <end_tombstone_exclusive>   |
| k3 | v3 |
| k4 | v4 |

Поиск по дереву LSM

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

Слияние итераций

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

Некая многоканальная сортировка слиянием применяет очереди с приоритетами, например, min-heap [SEDGEWICK11], которые содержат до N элементов (где N выступает общим числом итераторов), которые сортируют своё содержимое и подготавливают свой следующий в строке подлежащий возврату наименьший элемент. Головное значение каждого итератора помещается в соответствующую очередь. Некий элемент из такой головы данной очереди устанавливается далее как минимальный из всех итераторов.

[Замечание]
Замечание

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

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

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

Рисунок 7-5 отображает схематическое представление только что описанного процесса слияния: головные элементы (светло серые элементы в таблицах источников) помещаются в очередь с приоритетами. Элементы из этой очереди с приоритетами возвращаются в соответствующий итератор вывода. Получаемый результат отсортирован.

Рисунок 7-5

Механика слияния LSM.

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

Давайте пошагово пройдём по одному примеру. В качестве данных итераторов у нас имеются итераторы двух располагаемых на диске таблиц:

Iterator 1:         Iterator 2:
{k2: v1} {k4: v2}   {k1: v3} {k2: v4} {k3: v5}

Очередь с приоритетами заполняется головными элементами этих итераторов:

Iterator 1:         Iterator 2:         Priority queue:
{k4: v2}            {k2: v4} {k3: v5}   {k1: v3} {k2: v1}

Ключ k1 является наименьшим ключом в этой очереди и он добавляется в конец получаемого результата. Поскольку он получен из Iterator 2, мы пополняем свою очередь из него:

Iterator 1:         Iterator 2:         Priority queue:      Merged Result:
{k4: v2}            {k3: v5}            {k2: v1} {k2: v4}    {k1: v3}

Теперь у нас имеются две записи для значения ключа k2 в этой очереди. Мы можем быть уверенными, что нет никаких иных записей с этим же ключом в прочих итераторах по причине вышеуказанных инвариантов. Записи с одним и тем же ключом сливаются и в конец очереди добавляется результат такого слияния.

Наша очередь пополняется данными из обоих итераторов:

Iterator 1:         Iterator 2:         Priority queue:      Merged Result:
{}                  {}                  {k3: v5} {k4: v2}    {k1: v3} {k2: v4}

Поскольку все итераторы теперь пустые, мы добавляем в конец своего вывода всё остающееся в очереди содержимое:

Merged Result:
  {k1: v3} {k2: v4} {k3: v5} {k4: v2}

Суммарно, для создания некой комбинации итераторов следует повторять следующие шаги:

Первоначально заполняем свою очередь самыми первыми элементами из каждого итератора.

Из этой очереди получаем самый наименьший элемент (головной).

Заполняем свою очередь из соответствующего итератора до тех пор, пока этот итератор не будет исчерпан.

В терминах сложности, слияние итераторов это то же самое что и слияние сортированных наборов. Оно обладает накладными расходами по памяти O(N), где N это общее число итераторов. Сортированная коллекция заголовков итераторов препровождается с O(log N) (в среднем) [KNUTH98].

Улаживание

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

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

[Замечание]
Замечание

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

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

Скрываемые записями с более верхними временными отметками записи не возвращаются своему клиенту и не записываются в процессе уплотнения.

Сопровождение в деревьях LSM

Аналогично изменяемым B- деревьям, LSM деревья требуют сопровождения. Сама природа этих процессов сильно подвержена воздействию тех инвариантов, которые сохраняют эти алгоритмы.

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

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

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

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

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

[Замечание]
Надгробия и упаковка.

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

В процессе уплотнения надгробия не отбрасываются просто так. Они представляются до тех пор, пока имеющийся механизм хранения сможет получить уверенность в том, что никакие записи данных для одного и того же ключа с меньшей временной отметкой не представлены ни в какой иной таблице. RocksDB оставляет надгробия вплоть до достижения самого нижнего уровня. Apache Cassandra оставляет надгробия вплоть до достижения периода отсрочки GC (garbage collection, сборки мусора) по причине в конечном итоге непротиворечивой природы самой базы данных, гарантирующей что прочие узлы видят эти надгробия. Сохранение надгробий во время уплотнения важно во избежание воскрешения данных.

Упаковка по уровням

Уплотнение открывает для оптимизации множество возможностей и имеется множество различных стратегий уплотнения. Одной из часто принимаемых реализаций стратегий уплотнения имеет название упаковки по уровням. Например она применяется в RocksDB.

Выстраиваемое по уровням уплотнение разделяет размещаемые на диске таблицы по уровням. Таблицы на каждом уровне имеют целевые размеры, причём каждый уровень обладает соответствующим номером индекса (идентификатора). Что несколько нелогично, уровень с самым высоким уровнем имеет название самого нижнего уровня. Для ясности в этом разделе мы избегаем применения терминов верхний и нижний уровни и используем те же самые квалификации для индекса уровня. А именно, поскольку 2 больше чем 1, уровень 2 имеет более высокий индекс чем уровень 1. Наши термины предыдущий и следующий имеют тот же самый порядок семантики, что и индексы уровня.

Таблицы уровня 0 создаются сбросом содержимого таблиц в памяти. Таблицы из уровня 0 могут содержать перекрывающиеся диапазоны ключей. По мере того, как число таблиц уровня 0 достигает некого порогового значения, их содержимое сливается, создавая новые таблицы для уровня 1.

Диапазоны ключей для всех таблиц на уровне 1 и все уровни с более высоким индексом не перекрываются, поэтому таблицы уровня 0 должны быть разбиты на разделы в процессе уплотнения, расщепляясь по диапазонам и слиты с таблицами, содержащими соответствующие диапазоны ключей. В качестве альтернативы, уплотнение включает в себя все таблицы уровня 0 и уровня 1 и выводит разбитые по разделам таблицы уровня 1.

Уплотнения по уровням для более высоких индексов прихватывает таблицы двух соприкасающихся уровней с перекрывающимися диапазонами и производит некую новую таблицу на более высоком уровне. Рисунок 7-6 схематично отображает как наш процесс уплотнения выполняет миграцию данных между своими уровнями. Данный процесс уплотнения таблиц уровня 1 и уровня 2 произведёт некую новую таблицу на уровне 2. В зависимости от того как таблицы разбиты на части, для уплотнения могут быть захвачено множество таблиц с одного уровня.

Рисунок 7-6

Процесс упаковки. Помеченные серым блоки в окружении пунктирных линий представляют пакуемые в данный момент таблицы. Объединяющие по уровню блоки представляют установленный предел размера цели для данного уровня. Уровень 1 превысил свой предел.

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

Каждый уровень имеет некий предел для размера своих таблиц и максимального числа таблиц. Как только число таблиц на уровне 1 или любом ином уровне с более высоким индексом достигает порогового значения, таблица с данного текущего уровня сливается с таблицами следующего уровня сохраняя имеющийся диапазон перекрывающихся ключей.

Между этими уровнями размеры растут экспоненциально: таблицы на следующем уровне экспоненциально больше таблиц предыдущего уровня. Таким образом, самые свежие данные всегда находятся на самом высоком уровне с наименьшим индексом, а более старые данные постепенно мигрируют на более верхние уровни.

Стягиваемая по размеру упаковка

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

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

[Предостережение]
Предостережение

Одной из проблем при стягиваемом размером уплотнении является именуемая табличного голода: если уплотнённые таблицы всё ещё достаточно малы после упаковки (т.е. записи скрыты своими надгробиями и на выполняют необходимого слияния таблиц), более верхние уровни могут начинать испытывать голод уплотнения и их надгробия не будут приниматься во внимание, увеличивая стоимость чтения. В таком случае уплотнение для некого уровня должно запускаться принудительно, даже когда тот содержит достаточно таблиц.

Существуют и иные широко реализуемые стратегии уплотнения, которые способны оптимизировать различные рабочие нагрузки. Например, Apache Cassandra также реализует некую стратегию уплотнения окна времени, которая, в частности, полезна для рабочих нагрузок временных последовательностей с записями для какого времени жизни они установлены (иными словами, элементы имеют срок истечения своего действия после заданного периода времени).

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

Чтение, запись и увеличение пространства

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

[Замечание]
Замечание

Одной из самых больших дискуссий в сообществе баз данных состоит в том, должны ли B- деревья и LSM деревья обладать меньшим ростом записей. Чрезвычайно важно понимать сам источник роста записей в обоих случаях. В B- деревьях он поступает за счёт операций обратной записи и последовательных обновлений в одном и том же узле. В LSM деревьях рост записей вызывается миграцией данных из одного файла в другой в процессе уплотнения. Сравнение их обоих напрямую может приводить к неверным предположениям.

Суммируя, при сохранении данных на диск неким неизменяемым образом, мы сталкиваемся с тремя задачами:

Рост чтений

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

Рост записей

Вызываемый непрерывными перезаписями необходимым процессом уплотнения.

Рост пространства

Возникающим из- за хранения множества записей, связанных с одним и тем же ключом.

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

Предположение RUM

Одна из наиболее популярных моделей стоимости для структур хранения принимает во внимание три фактора: накладные расходы Read, Update и Memory. Она носит название предположения RUN [ATHANASSOULIS16].

Предположение RUM постулирует, что снижение двух из этих накладных расходов неминуемо приводит к изменению в худшую сторону третьего и что оптимизация может быть выполнена лишь за счёт затрат одного из трёх параметров. Мы можем сопоставлять различные механизмы хранения в терминах этих трёх параметров для понимания того, какие из них оптимизируются, а какие могут приноситься в угоду компромиссу.

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

B- деревья оптимизированы для чтения. Записи в B- дереве требуют определения места на диске для обновления множества раз соответствующей страницы на диске. Резервирование дополнительного пространства для обновлений и удалений увеличивает накладные расходы пространства.

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

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

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

Подробности реализации

Мы рассмотрели основную динамику LSM деревьев: как данные считываются, записываются и уплотняются. Тем не менее, существует ещё прочие моменты, которыми обычно обладают многие реализации LSM деревьев и которые неплохо обсудить: как реализуются располагаемые в памяти и на диске таблицы, как работают вторичные индексы, как снижать общее число доступов к располагаемых на дисках таблицах при чтении и, наконец, новые относящиеся к хранилищам структурированных журналов идеи.

Таблицы сортированных строк

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

Располагаемые на диске таблицы часто реализуются при помощи Сортированных таблиц строк (SSTables, Sorted String Tables). Как и предполагает их название, записи данных в SSTables отсортированы и следуют в порядке их ключей. SSTables обычно состоят из двух компонентов: файлов индексов и файлов данных. Индексные файлы реализуются при помощи некой структуры, допускающей логарифмический поиск, таких как B- деревья, либо с поиском с постоянным временем, таких как хэштаблицы.

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

Соответствующий компонент индексации содержит записи ключа и данных (смещений в соответствующем файле данных где и расположены реальные сведения). Его компонент данных состоит из объединённых пар ключ- значение. Те архитектуры ячеек и форматы записей данных, которые мы обсуждали в Главе 3,, в основном применимы и для SSTables. Основное отличие здесь состоит в том, что ячейки записываются последовательно и не могут изменяться в процессе своего жизненного цикла в этой SSTable. Поскольку наши индексные файлы содержат указатели на соответствующие записи данных, хранимые в своём файле данных, их смещения должны быть известны на момент создания самого индекса.

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

[Замечание]
Подключаемые к SSTable вторичные индексы

Одной из интересных разработок в области индексации LSM деревьев являются Подключаемые к SSTable вторичные индексы (SASI, SSTable- Attached Secondary Indexes). Чтобы сделать возможной индексацию сотдержимого таблицы не только по её первичному ключу, но также и по прочим полям, структуры индексации и их жизненные циклы сочетаются с жизненным циклом SSTable и для SSTable создаётся некий индекс. При сбросе соответствующей таблицы в памяти её содержимое записывается на диск, а файлы вторичной индексации создаются совместно со значением первичного индексного ключа SSTable.

Так как буферированные в памяти данные и индексы LSM дерева должны работать как для располагаемого в памяти содержимого, так и для размещаемого на диске, SASI сопровождает некую отдельную идексирующую содержимое таблиц в памяти структуру оперативной памяти.

В процессе чтения первичные ключи искомых записей определяются поиском и слиянием содержимого индексов и согласовываются аналогично тому как обычно работает просмотр в LSM деревьях.

Одним из основных преимуществ прихвата прицепом жизненного цикла самой SSTable является то, что индексы могут создаваться в процессе сброса или уплотнения таблиц в памяти.

Фильтры Блюма

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

Одним из способов предотвращения просмотра таблицы состоит в хранении его диапазона ключей (наименьшего и наибольшего хранимых в этой конкретной таблице ключей) в метаданных и проверять относятся ли искомые ключи к диапазону данной таблицы. Эти сведения не точны и могут лишь нам сообщить что запись данных может присутствовать в данной таблице. Для улучшения данной ситуации многие реализации, включая Apache Cassandra и RocksDB применяют структуру данных, носящую название Фильтра Блюма (Bloom filter).

[Замечание]
Замечание

Вероятностные структуры данных обычно более эффективны в отношении пространства чем их "регулярная" противоположность. Например, чтобы проверить вхождение в множество, кардинальная мощность (определения общего числа отличающихся элементов в неком наборе) или частоты (определения того сколько раз был встречен некий определённый элемент), нам придётся хранить весь набор элементов и для обнаружения данных пройтись по всему набору данных. Вероятностные структуры позволяют нам хранить приблизительные сведения и выполнять запросы, которые несомненно дают результаты с неким элементом. Некими широко известными примерами являются Фильтры Блюма (для участия в множестве), HyperLogLog (для оценки кардинального значения) [FLAJOLET12] и Эскиз минимального подсчёта (Count-Min Sketch) [CORMODE12].

Фильтр Блюма, представленный Бартоном Ховардом Блюмом в 1970 [BLOOM70] является эффективной в отношении пространства вероятностной структурой данных, которая может применяться для проверки того, будет ли данный элемент участником определённого множества или нет. Они может производить ошибочные включения соответствий (сообщать, что данный элемент является участником рассматриваемого множества, когда он таковым не является), однако не способен производить ложные промахи (когда возвращается некое отрицание соответствия, данный элемент гарантировано не будет участником этого множества).

Иными словами, фильтр Блума может применяться для того чтобы сообщать нам что данный ключ может иметься в таблице, либо определённо отсутствует в этой таблице. Файлы, для которых фильтр Блюма возвращает некое отрицание соответствия, пропускаются в процессе данного запроса. Ко всем прочим файлам выполняется доступ для обнаружения того что искомая запись данных действительно присутствует. Использование связанных с располагаемыми на диске таблицами фильтров Блюма способствует значительному снижению общего числа таблиц для доступа в процессе чтения.

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

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

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

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

Давайте рассмотрим некий простой пример, представленный на Рисунке 7-7. У нас имеется 16- канальный массив битов и 3 хэширующих функции, которые для key1 выдают значения 3, 5 и 10. Теперь мы устанавливаем биты в этих позициях. Добавляется наш следующий ключ и хэш функции дают для key2 значения 5, 8 и 14, для которых мы также устанавливаем биты.

Рисунок 7-7

Фильтр Блюма

Теперь мы пытаемся определить будет или нет key3 присутствовать в этом наборе и хэш функции дают 3, 10 и 14. Так как все три бита были установлены при добавлении key1 и key2, мы получаем ситуацию, при которой наш фильтр Блюма возвращает некое ложной вхождение: key3 здесь никогда не будет прикреплён, даже хотя установлены все его вычисляемые биты. Однако, поскольку наш фильтр Блюма лишь утверждает что такой элемент может присутствовать в рассматриваемой таблице, такой результат допустим.

Когда мы попытаемся выполнить поиск для key4 и получим значения 5, 9 и 15, мы обнаружим, что установлен лишь бит 5, а все прочие биты сброшены. Даже когда только один бит сброшен, мы определённо знаем, что этот элемент никогда не был прикреплён к данному фильтру.

Список пропусков

Существует множество различных структур данных для сохранения в памяти сортированных данных,и одним из таких, который приобретает всё большую популярность в последнее время по причине его простоты, является список пропусков (skiplist) [PUGH90b]. Широко реализуемый, список пропуска не намного сложнее чем односвязный список, а его гарантированная вероятностная сложность близка к деревьям поиска.

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

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

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

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

Например, поиск ключа 7 в нашем списке пропусков, отображаемом на Рисунке 7-8 может быть выполнен следующим образом:

Следует за указателем самого верхнего уровня к узлу, содержащему ключ 10.

Так как искомый ключ 7 меньше чем 10, мы следуем по ссылке на следующий уровень из его головного узла, определяющей узел, содержащий ключ 5.

Мы следуем по указателю самого верхнего уровня в данном узле, определяющим узел, снова содержащем ключ 10.

Искомый ключ 7 меньше чем 10 и мы следуем по указателю следующего уровня, который содержит ключ 5, определяющий место узла, содержащего искомый ключ 7.

Рисунок 7-8

Список пропусков

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

В процессе удаления, следующие далее указатели удаляемого узла помещаются в предыдущий узел на соответствующие уровни.

МЫ можем создавать некую одновременную версию списка пропусков, реализуя некую линеаризуемую схему, которая пользуется дополнитеоьным флагом fully_linked, , который определяет будут или нет указатели соответствующего узла обновляться полностью. Этот флаг может быть установлен при помощи compare- and- swap [HERLIHY10]. Он необходим по той причине, что указатели рассматриваемого узла следует обновлять на множестве уровней для полного восстановления структуры данного списка пропусков.

В языках с неуправляемой моделью памяти для обеспечения того, что имеющие ссылки в настоящее время узлы не освобождаются при одновременном доступе к ним, могут применяться счётчики ссылок или рискованные указатели (hazard pointers) [RUSSEL12]. Этот алгоритм свободен от взаимных блокировок, так как всегда имеется доступ к узлам с верхнего уровня.

Apache Cassandra применяет списки пропусков для реализации таблиц в памяти своих вторичных индексов. WiredTiger использует списки пропусков для некоторых операций в оперативной памяти.

Доступ к дискам

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

Наиболее примечательное отличие состоит в том, что содержимое в памяти неизменно, а следовательно не требует дополнительных блокировок или защёлок для одновременного доступа. Для гарантии того, что имеющие в настоящее время страницы не будут вытеснены из памяти, используются счётчики ссылок, а выполненные ранее на лету запросы к лежащим в основе файлам удаляются в процессе уплотнения.

Другое отличие заключается в том, что записи данных в LSM деревьях не обязательно выравнивать по границе страниц, а указатели для адресации могут реализовываться с применением абсолютных смещений вместо идентификаторов страниц. На Рисунке 7-9 вы можете наблюдать записи с содержимым, которое не выравнено по дисковым блокам. Некоторые записи пересекают установленные границы страниц и требуют загрузки нескольких страниц в память.

Рисунок 7-9

Не выравненные записи данных

Сжатие

В контексте B- деревьев мы уже обсуждали уплотнение (см. раздел Сжатие). Аналогичные мысли также применимы и к LSM деревьям. Основное отличие для них состоит в том, что таблицы LSM деревьев неизменны, и обычно записываются одним проходом. При упаковке данных по множеству страниц уплотняемые страницы не выровнены по границам страниц, а их размеры меньше чем у не уплотнённых.

Чтобы иметь возможность адресации к уплотнённым страницам, нам требуется отслеживать значения адресов границ при записи их содержимого. Мы можем заполнять уплотнённые страницы нулями, выравнивая их на границу страницы, однако мы утратим основные преимущества уплотнения.

Для того чтобы сделать уплотнённые страницы адресуемыми, нам требуется некий обходной путь, сохраняющий смещения и размеры уплотнённых страниц. Рисунок 7-10 отображает устанавливаемое соответствие между уплотнёнными страницами и страницами без уплотнения. Уплотнённые страницы всегда меньше своих оригиналов, ибо иначе не было бы смысла в их упаковке.

Рисунок 7-10

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

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

Неупорядоченное хранилище LSM

Большинство до сих пор обсуждавшихся структур хранения хранили данные упорядоченно. Изменяемые и неизменяемые страницы B-деревьев, сортированные прогоны в FD деревьях, а также SSLTables в LSM деревьях хранят данные в порядке своих ключей. Сам порядок в этих структурных сохраняется различным образом: страницы B- дерева обновляются по месту хранения, прогоны FD- дерева создаются слиянием двух прогонов, а SSLTables создаются буферизацией и сортировкой записей данных в памяти.

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

Bitcask

Bitcask,, один из механизмов хранения, применяемых в Riak, является неким механизмом хранения структурированного журнала без упорядочения [SHEEHY10b]. В отличии от обсуждавшихся до сих пор реализаций хранилищ структурированных журналов, он не применяет таблицы в памяти для буферизации и сохраняет записи данных напрямую в файлах журналов.

Для тгбо чтобы сделать возможным поиск значений, Bitcask применяет некую структуру с названием keydir, которая хранит ссылки на самые последие записи данных для значений соответствующих ключей. Старые записи данных всё ещё могут присутствовать на диске, однако на них нет ссылок из keydir и они подвержены сборке мусора в процессе уплотнения. Keydir реализуется как некое соответсвие хэш- значений в памяти и должно повторно строиться из имеющихся файлов журналов в процессе запуска.

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

Считывание проверяет keydir на предмет определения места искомого ключа и следует по полученному связанному с ним указателю в соответствующий файл журнала, определяя местоположение записи данных. Поскольку в определённый момент времени в данном keydir может иметься лишь одно связанное с искомым ключом значение, запросы точки не должны сливать данные из множества источников.

Рисунок 7-11 показывает соответствие между самими ключами и записями в файлах данных Bitcask. Файлы журнала содержат записи данных, а keydir указывает на самую последнюю живую запись данных, связанную с каждым ключом. Экранированные записи в файлах данных (те, которые были замещены более поздними записями или удалениями) отображены серым.

Рисунок 7-11

Соответствие между keydir и файлами данных в Bitcask. Сплошные линии представляют указатели от самого ключа к самому последнему связанному с ним значению. Скрытые пары ключ/ значение показаны светло серыми.

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

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

Преимуществами данного подхода выступают простота и великолепная производительность точечных запросов. Даже когда существует множество версий записей данных, через keydir адресуется только самая последняя. Тем не менее, необходимость отслеживания в памяти всех ключей и перестроение keydir при запуске являются ограничениями, которые способны выступать препятствующими условиями для некоторых вариантов применения. Хотя данный подход и великолепен для точечных запросов, он не предлагает никакого сопровождения для запросов диапазонов.

WiscKey

Запросы диапазонов важны для многих приложений и было бы великолепно иметь некую структуру хранения, которая бы обладала преимуществами записи и пространства не сортированных хранилищ, и в то же время позволяла бы нам сканирование диапазонов.

WiscKey [LU16] отвязывает сортировку от сборки мусора, сохраняя значения ключей сортированными в LSM деревьях и оставляя записи данных в не сортированных файлах с добавлением в конец, именуемые как vLogs (value logs, журналы значений). Такой подход способен разрешить две проблемы, упомянутые при обсуждении Bitcask: необходимость удержания всех ключей в памяти и перестроения некой таблицы хэширования при запуске.

Рисунок 7-12 отображает ключевые компоненты WiscKey и соответствие между ключами и файлами журналов. Файлы vLogs хранят не сортированные записи данных. Ключи хранятся в сортированных LSM деревьях, указывая на самые последние записи данных в соответствующих файлах журналов.

Рисунок 7-12

Ключевые компоненты WiscKey: индекс LSM деревьев и файлы vLog, а также взаимоотношения между ними. Скрытые записи в файлах данных (которые мы впоследствии делим на записываемые и удаляемые) показаны серым. Сплошные линии представляют указатели от соответствующего ключа в самом LSM дереве к самому последнему значению в его файле журнала.

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

Самый основной вызов здесь состоит в том, что данные vLog не сортированы, сканирования чтения требуют произвольного ввода/ вывода. WiscKey применяет внутренний парралелизм SSD для предварительной параллельной выборки блоков на протяжении сканирований диапазона и снижает стоимости ввода/ вывода. В терминах блочного обмена, эти стоимости всё ещё высоки: для выборки некой отдельной записи данных в процессе необходимого сканирования диапазона, подлежит считыванию вся страница, в которой она содержится.

В процессе уплотнения, содержимое файла vLog последовательно считывается, сливается и записывается по новому месту. Указатели (значения ключа в LSM дереве) обновляются на точку для этого нового местоположения. Во избежание сканирования всего vLog, WiscKey пользуется указателями head и tail, содержащими сведениями относительно сегментов vLogs, которые хранят живые ключи.

Поскольку данные в vLogs не сортированы и не содержат сведений о жизнеспособности, приходится сканировать само дерево ключей для определения того какие из значений всё ещё живы. Выполнение таких проверок на протяжении сборки мусора вводит дополнительную сложность: традиционные LSM деревья могут разрешать содержимое файла в процессе уплотнения без адресации значения индекса ключа.

Одновременность в деревьях LSM

Основные проблемы с одновременностью в LSM деревьях связаны с переключением на табличное представление (table view, собрание располагаемых в памяти и на диске таблиц, которые изменились в процессе сброса и упаковки) и синхронизации журнала. Доступ к таблицам в памяти также, как правило, выполняется одновременно (за исключением хранилищ с разбитым на разделы ядром, таких как ScyllaDB), однако одновременные в памяти структуры данных выходят за рамки данной книги.

В процессе сброса необходимо следовать следующим правилам:

Сами новые таблицы в памяти должны стать доступными для чтений и записей.

Старые (сбрасываемые) таблицы в памяти должны оставаться видимыми для чтений.

Сбрасываемые таблицы в памяти должны быть записаны на диск.

Отбрасывание сброшенных таблиц в памяти и превращение сброшенных страниц в располагаемые на диске необходимо выполнять некой атомарной операцией.

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

К примеру, Apache Cassandra решает эти задачи применяя операции упорядоченных барьеров: все принятые на запись операции будут ожидать пока прежде не сбросится соответствующая таблица в памяти. Таким образом, сам процесс сброса (обслуживаемый как потребитель) знает какие прочие процессы (выступающие в роли поставщиков) полагаются на него.

В более общем случае у нас имеются следующие точки синхронизации:

Переключение таблиц в памяти

После него все записи происходят только в новой таблице в памяти, которая сделана первичной, в о время как старая всё ещё доступна для чтения.

Завершение сброса

Заменяет в установленном табличном представлении свою старую таблицу в памяти сброшенной располагаемой на диске.

урнал транзакций упреждающей записи

Отбрасывает сегмент журнала со сброшенной располагаемой на диске таблицей в установленном представлении таблицы.

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

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

В B- деревьях усечение журнала следует рассматривать координируемым со сбросом испорченных (dirty) страниц из установленного кэша страниц для обеспечения долговечности. В LSM деревьях у нас имеется аналогичное требование: записи буферируются в таблицах в памяти и их содержимое не долговечно до выполнения соответствующего сброса, а в случае крушения узла содержимое журнала не будет воспроизведено и невозможно будет восстановить данные этого сегмента.

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

Составление журналов в стек

Многие современные файловые системы снабжены структурированным журналом: они буферируют записи в сегменте памяти и сбрасывают его содержимое на диск по мере его заполнения неким образом дописывания в конец. Чтобы иметь дело с небольшими произвольными записями, также применяются хранилища структурированного журнала на SSD, что минимизирует накладные расходы записи, улучшая уровень износа и увеличивая время службы данного устройства.

Системы хранения со структурированным журналом (LSS, log-structured storage) начинают обретать популярность со временем, когда SSD становятся более доступными. LSM деревья и SSD хорошо подходят друг к другу, поскольку последовательные рабочие нагрузки и исключительная запись в конец способствуют снижению роста обновлений по месту хранения, что отрицательно сказывается на производительности SSD.

Когда мы помещаем в стек множество систем структурированного журнала, одна поверх другой, мы можем врезаться в некоторые проблемы, которые мы и пытались решать применяя LSS, включая рост записей, фрагментирование и плохую производительность. По крайней мере, нам следует хранить в памяти соответствующий уровень трансляции флеш SSD и его файловой системы при разработке своих приложений [YANG14].

Слой трансляции флеш- устройства

Применение некоего уровня соответствия структурированного журнала в SSD мотивируется двумя обстоятельствами: небольшие произвольные записи следует совместно объединять в пакеты в некую физическую страницу, а также тот факт, что SSD работают применяя циклы программирования/ уничтожения. Записи могут выполняться только в предварительно уничтоженные (erased) страницы. Это означает, что такая страница не может быть запрограммирована (иными словами, записана), пока она не очищена (иначе говоря, не уничтожена).

Отдельная страница не может быть уничтожена, причём лишь сгруппированные в некий блок (обычно содержащий от 64 до 512 страниц) может быть совместно уничтожен. Рисунок 7-13 отображает некое схематичное представление страниц, группируемых в некий блок. Уровень трансляции сброса (FTL, flash translation layer) транслирует логические адреса страниц в их физическое расположение и отслеживает состояние страниц (рабочие, отброшенные или пустые). Когда FTL исчерпывает свободные страницы, ему приходится выполнять сборку мусора и уничтожать отвергнутые страницы.

Рисунок 7-13

Сгруппированные в блоки страницы SSD

Нет никаких гарантий, что все страницы в том блоке, который готов к уничтожению отвергнуты. Прежде чем этот блок будет очищен, FTL придётся передислоцировать свои рабочие (live) страницы в один из блоков, содержащих пустые страницы. Рисунок 7-14 отображает такой процесс перемещения рабочих страниц из одного блока в новое местоположение.

Рисунок 7-14

Передислокация страниц в процессе сборки мусора

После того как все рабочие страницы передислоцированы, этот блок может быть безопасно очищен (уничтожен), а его свободные страницы становятся доступными для записи. Поскольку FTL осведомлён о состояниях страниц и переходах состояний, а также обладает всеми необходимыми сведениями, он также отвечает за износ (wear leveling) SSD.

[Замечание]
Замечание

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

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

Журналирование файловой системы

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

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

Рисунок 7-15 показывает соответствие между неким журналом верхнего уровня (например, уровня приложения) и журналом нижнего уровня (скажем, его файловой системы), имеющим в результате избыточное протоколирование и различные шаблоны сборки мусора [YANG14]. Записи не совпадающих сегментов могут приводить к ещё худшим обстоятельствам, при которых отбрасывание сегмента журнала верхнего уровня может вызывать фрагментирование и передислокацию имеющихся соседними частей сегментов.

Рисунок 7-15

Смещённые записи и отбрасывание сегмента журнала верхнего уровня

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

Даже хотя мы и говорим, что хранилище структурированного журнала целиком посвящено последовательному вводу/ выводу, мы должны держать на уме, что системы баз данных могут обладать множеством потоков записи (скажем, журналы параллельно сохраняют записи данных) [YANG14]. При рассмотрении на аппаратном уровне, перекрывающиеся потоки последовательных записей могут не транслироваться в те же самые последовательные шаблоны: блоки не обязательно помещаются в порядке их записи. Рисунок 7-16 отображает множество перекрывающихся по времени потоков, сохраняющих записи, обладающие размерами, не выровненными на размер страницы лежащего в основе оборудования.

Рисунок 7-16

Не выровненные записи множества потоков

В результате мы получаем фрагментирование, которого мы пытались избежать. Для снижения перекрытий некоторые производители баз данных рекомендуют хранить сам журнал на отдельном устройстве для изолирования рабочих нагрузок и чтобы получать обоснованные значения их производительности и шаблонов доступа независимым образом. Тем не менее, более важно сохранять выравнивание разделов для лежащего в основе оборудования [INTEL14] и сохранять записи выровненными на размер страниц[KIM12].

Составление стека LLAMA и Mindful

Хорошо, вы никогда не поверите в это, но та лама, которую вы видите когда- то была человеком. и не просто человеком. Этот парень был императором. Богатым, раздутым от божественной силы.

- Кузко из Новой борозды императора

В разделе Bw- деревья мы обсуждали не изменяемую версию B- дерева с названием Bw- дерева. Bw- деревья выстраиваются уровнем поверх подсистемы хранения свободной от защёлок со структурированным журналом и осведомлённой о методе доступа (LLAMA, latch-free, log-structured, access-method aware). Такой уровень позволяет Bw- деревьям динамично расти и сокращаться, в то же время оставляя сборку мусора и управление страницами прозрачными для самого дерева. В данном случае мы больше всего интересуемся в части осведомлённости о методе доступа, демонстрирующем основные преимущества координации между имеющимися программными уровнями.

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

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

Без наличия осведомлённости о методе доступа перекрывающиеся узлы приращений,которые относятся к различным логическим узлам, будут записываться в их порядке вставки. Оседомлённость Bw- деревьев в LLAMA допускает консолидацию различных узлов приращений в неком отдельном непрерывном физическом месте. Когда два обновления в узлах приращения прекращают друг друга (например, за некой вставкой следует удаление), также мжно выполнять их логическую консолидацию, и может оставаться лишь последнее удаление.

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

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

Вы можете видеть, что при аккуратном подходе, стекирование всё ещё может давать множество преимуществ. Нет необходимости всегда строить туго стянутые структуры с единственным уровнем. Хороший API и выставление правильных сведений способно существенно улучшать эффективность.

SSD Open-Channel

Некой альтернативой стекируемым программным уровням является пропуск всех косвенных уровней и использование аппаратных средств напрямую. Например, имеется возможность избегать применения файловой системы и уровня трансляции сброса выполняя разработку в Open- Channel SSD. Тем самым мы можем избежать по крайней мере двух уровней журналов и обладать большим контролем над износом, сборкой мусора размещением данных и планированием. Одной из реализаций, применяющих такой подход, является LOCS (LSM Tree-based KV Store on Open-Channel SSD) [ZHANG13]. Другим примером использования Open-Channel SSD является LightNVM, реализованный в самом ядре Linux [BJØRLING17].

Уровень трансляции сброса обычно обрабатывает размещение данных, сборку мусора и передислокацию страниц. Open-Channel SSD выставляет их внутренне присущие свойства, управление устройством и расписания ввода/ вывода без необходимости проходить через FTL. Хотя это и требует намного большего внимания к подробностям с точки зрения разработчика, такой подход способен давать значительные улучшения производительности. Вы можете провести параллель с применением флага O_DIRECT для обхода имеющегося кэширования страниц в самом ядре, который даёт лучший контроль, но требует ручного управления страницами.

Software Defined Flash (SDF) [OUYANG14], совместно с Open Channel SSD разрабатываемая система аппаратных/ программных средств, выставляет асимметричный интерфейс ввода/ вывода, который учитывает специфические особенности SSD. Размеры элементов чтения и записи различны, причём размер элемента записи соответствует размеру элемента удаления (блоку), что значительно снижает рост записей. Эти установки идеальны для хранилища со структурированным журналом, так как существует лишь единственный программный уровень, который выполняет сборку мусора и размещение страниц. Кроме того, разработчики имеют доступ к внутреннему параллелизму SSD, так как каждый канал в SDF выставляется как некое отдельное блочное устройство, которое может применяться для дальнейшего улучшения производительности.

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

Выводы

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

LSM деревья берут некие идеи из LSS и способствуют построению индексных структур управляемых в стиле структурированного журнала: записи собираются в пакеты в памяти и сбрасываются на диск; экранированные записи данных вычищаются в процессе уплотнения.

Важно запомнить, что многие программные уровни применяют LSS и убедиться что уровни оптимально собраны в стек. В качестве альтернативы, мы можем пропустить вообще уровень файловой системы и осуществлять доступ к аппаратным средствам напрямую.

[Замечание]
Последующее чтение

Если вы желаете узнать больше относительно упоминавшихся в этой главе понятий, вы можете ссылаться на следующие источники:

Обзор

Luo, Chen, and Michael J. Carey. 2019. “LSM-based Storage Techniques: A Survey.” in Te VLDB Journal, https://doi.org/10.1007/s00778-019-00555-y .

LSM деревья

O’Neil, Patrick, Edward Cheng, Dieter Gawlick, and Elizabeth O’Neil. 1996. “The log-structured merge-tree (LSM-tree).” in Acta Informatica, 33, no. 4: 351-385. https://doi.org/10.1007/s002360050048 .

Bitcask

Justin Sheehy, David Smith. “Bitcask: A Log-Structured Hash Table for Fast Key/ Value Data.” 2010 .

WiscKey

Lanyue Lu, Thanumalayan Sankaranarayana Pillai, Hariharan Gopalakrishnan, Andrea C. Arpaci-Dusseau, and Remzi H. Arpaci-Dusseau. 2017. “WiscKey: Separating Keys from Values in SSD-Conscious Storage.” in ACM Trans. Storage 13, 1, Article 5 (March 2017), 28 pages.

LOCS

Peng Wang, Guangyu Sun, Song Jiang, Jian Ouyang, Shiding Lin, Chen Zhang, and Jason Cong. 2014. “An efficient design and implementation of LSM-tree based key-value store on open-channel SSD.” in Proceedings of the Ninth European Conference on Computer Systems (EuroSys ’14). ACM, New York, NY, USA, Article 16, 14 pages .

LLAMA

Justin Levandoski, David Lomet, and Sudipta Sengupta. 2013. “LLAMA: a cache/ storage subsystem for modern hardware.” in Proc. VLDB Endow. 6, 10 (August 2013), 877-888 .

Заключение Части I

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

Я рассмотрел некоторые структуры хранения, начав с B- деревьев. Рассмотренные структуры не представляют все поле целиком, и имеется множество прочих интересных разработок. Тем не менее, данные примеры всё ещё хорошая иллюстрация трёх основных свойств, которые мы выделили в самом начале этой части: буферизации, неизменности и упорядоченности. Эти свойств полезны для описания, запоминания и выражения различных сторон имеющихся структур хранения.

Таблица I-1 суммирует обсуждённые структуры хранения и показывает используют или нет они эти свойства.

Таблица I-1. Свойства буферирования, неизменности и упорядоченности обсуждавшихся структур хранения. (1) WiscKey применяет буферирование только для хранения ключей в сотрированном порядке. (2) Лишь консолидированные узлы в Bw- древьях содержат упорядоченные записи.

Буферируемые
Изменяемые
Упорядоченные

B+ деревья

Нет

Да

Да

WiredTiger

Да

Да

Да

LA- деревья

Да

Да

Да

COW B- деревья

Нет

Нет

Да

2C LSM- деревья

Да

Нет

Да

MC LSM- деревья

Да

Нет

Да

FD- деревья

Да

Нет

Да

BitCack

Нет

Нет

Нет

WiscKey

Да(1)

Нет

Да(1)

BW- деревья

Нет

Нет

Нет(2)

Добавление буферизации в памяти всегда носит положительное воздействие на рост зарисей. При обновлении структур по месту хранения, таких как WiredTiger и LA-Trees, буферизация в памяти способствует амортизации общей стоиомсти множества записей в одной и той же странице через их комбинирование. Иными словами, буферизация помогает снижению роста записей.

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

При использовании неизменности, пока мы также не применяем буферизацию, мы в конце концов приходим к неупорядоченным структурам, таким как BitCack и WiscKey (за исключением копируемых- записью B- деревьев с копированием, вторичной сортировкой и передислокацией их страниц). WiscKey хранит только ключи в сортированных LSM деревьях и позволяет выборку записей в порядке ключей применяя индексацию ключей. В Bw- деревьях некоторые из имеющихся узлов (те, которые были консолидированы) хранят записи данных в порядке ключей, в то время как все прочие имеющиеся логические узлы Bw- дерева могут иметь свои приращения обновлений разбросанными по различным страницам.

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

Применяя эти знания, вы должны быть способны начать ближе понимать сам код большинства современных систем баз данных. Некоторые ссылки на такой код и отправные точки можно найти по всей книге. Знание и понимание приводимой терминологии сделает для вас этот процесс более быстрым.

Многие современные системы баз данных усилены вероятностными структурами данных [FLAJOLET12] [CORMODE04], а также имеются новые исследования, выполненные привнесёнными в системы баз данных идеями из машинного обучения [KRASKA18]. Мы близки к изучению последующих изменений в исследованиях и отрасли, таких как энергонезависимые и байт- адресуемые хранилища, становящиеся всё более распространёнными и более широко доступными [VENKATARAMAN11].

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

Глава 6. Варианты B- деревьев

Содержание

Глава 6. Варианты B- деревьев

Копирование записью

Реализация копирования записью: LMDB

Абстракция обновления узлов

Ленивые B- деревья

WiredTiger

Дерево ленивой адаптации

FD- деревья

Фрагментарное каскадирование

Логарифмические прогоны

Bw- деревья

Цепочки обновлений

Культивирование одновременности при помощи Compare- and- Swap

Операции структурного изменения

Консолидация и сборка мусора

Не замечающие кэш B- деревья

Схема ван Эмде Боаса

Выводы

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

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

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

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

Деревья FD предпринимают иной подход для буферизации, в чём- то схожий с деревьями LSM (см. Деревья LSM). FD- деревья обновляют буфер в небольшом B- дереве. Как только такое дерево заполняется, его содержимое записывается в неком неизменном исполнении. Обновления распространяются между уровнями неизменных исполнений неким каскадным образом, причём от более верхних уровней к нижним.

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

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

Копирование записью

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

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

Рисунок 6-1

B- деревья копирования записью

Очевидной обратной стороной данного подхода является то,что он требует больше пространства (даже несмотря на то, что старые версии сохраняются лишь на протяжении короткого временного периода, поскольку страницы могут быть возвращены немедленно по завершению одновременных операций, использующих старые страницы) и процессорного времени, так как требуется копирование всего содержимого страницы. Благодаря обычно относительно малой глубине B- деревьев, предлагаемые простота и преимущества данного подхода обычно перевешивают имеющиеся отрицательные моменты.

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

Реализация копирования записью: LMDB

Одним из имеющихся механизмов хранения, применяющих копирование- записью, является Облегчённая отображаемая в памяти база данных (LMDB, Lightning Memory-Mapped Database), которая является хранилищем ключ- значение, используемым в проекте OpenLDAP. Благодаря своим проектным возможностям и архитектуре, lDMB не требует некого кэша страниц, журнала упреждающей записи, контрольных точек и сжатия (Для получения дополнительных сведений по LDMB, ознакомьтесь с комментариями к коду и презентацией).

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

LMDB хранит только две версии своего узла корня: самую последнюю версию и ту, которая предназначена для фиксации новых изменений. Этого достаточно, так как всем записи приходится проходить через свой узел корня. После создания нового узла корня, старый становится недоступным для новых чтений и записей. Как только завершены все чтения, обратившиеся к разделам старого дерева, их страницы возвращаются и могут использоваться повторно. Благодаря тому что LMDB спроектирована с исключительным добавлением в конец, она не пользуется ссылками одного уровня и ей приходится восходить обратно к своему родительскому узлу в процессе последовательных сканирований.

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

Абстракция обновления узлов

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

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

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

Третий подход заключается в предоставлении доступа к имеющемуся буферу, поддерживающему конкретный узел через некий обёртывающий объект, который материально представляет изменения в данном B- дереве как только они выполнены. Данный подход наиболее часто используется в языках с управляемой моделью памяти. Для самих изменений в имеющихся буферах поддержки применяются обёртывающие объекты.

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

Ленивые B- деревья

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

WiredTiger

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

Аналогичный подход используется в WiredTiger, теперь устанавливаемом по умолчанию механизму хранения MongoDB. Его реализация хранения строк B- дерева применяет различные форматы для страниц в памяти и на диске. Прежде чем страницы из памяти станут неизменными, им придётся пройти процесс согласования.

На Рисунке 6-2 вы можете видеть схематическое представление страниц WiredTiger и то как они составляют некое B- дерево. Некая чистая страница состоит лишь из какого- то индекса, изначально построенного из самого образа дисковой страницы. Обновления вначале сохраняются в имеющемся буфере обновления.

Рисунок 6-2

WiredTiger: представление верхнего уровне

Доступ к буферам обновления выполняется считываниями: их содержимое сливается с содержимым первоначальной дисковой страницы для возврата самых последних данных. После сброса данной страницы, содержимое буфера обновления согласовывается с содержимым страницы на диске, перезаписывая первоначальную страницу. Когда размер согласуемой страницы больше максимального, она расщепляется на множество страниц. Буферы обновления реализуются при помощи списков пропусков (skiplists), которые обладают сложностью, аналогичной деревьям поиска [PAPADAKIS93], но обладают лучшим профилем одновременности [PUGH90a].

Рисунок 6-3 показывает что и чистые и испорченные страницы в WiredTiger обладают версиями в памяти и ссылкой на базовый дисковый образ. Испорченные страницы имеют дополнительно к этому некий буфер обновления.

Рисунок 6-3

Страницы WiredTiger

Основное преимущество здесь состоит в том, что сами обновления страниц и структурные изменения (расщепления и слияния) выполняются определённым фоновым потоком, а процессам чтения/ записи не приходится ждать их завершения.

Дерево ленивой адаптации

Вместо того чтобы буферировать обновления в индивидуальных узлах, мы можем группировать узлы в некие поддеревья и подключать какой- то буфер обновлений для выполняемых в пакете операций для каждого поддерева. Буферы обновлений в таком случае будут отслеживать все выполняемые для данного поддерева верхнего узла и его потомков действия. Такой алгоритм носит название Дерева ленивой адаптации (LA- дерева, Lazy-Adaptive Tree) [AGRAWAL09].

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

На Рисунке 6-4 вы можете наблюдать некое LA- дерево с выстроенными каскадом буферами для сгруппированных в соответствующие поддеревья узлов. Серые блоки представляют распространяемые из корневого буфера изменения.

Рисунок 6-4

LA- дерево

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

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

FD- деревья

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

Сопровождение некого B- дерева требует большого числа случайных записей - записи уровня листьев, распространяемые на их предков расщепления и слияния - но что если мы сможем избегать случайных записей, а вместе с тем и обновлений узла?

До сих пор мы обсуждали буферы обновления для индивидуальных узлов или групп узлов через создание вспомогательных буферов. Неким альтернативным подходом выступает группирование обновлений воедино имея целью различные узлы с помощью хранилища с добавлением только в конец и процессов слияния, некой идеи, которая также инспирировала деревья LSM (см. раздел Деревья LSM). Это означает, что все выполняемые нами операции записи не требуют для соответствующей записи поиска целевого узла: все обновления просто добавляются в конец. Один из примеров такого применяемого для индексации подхода носит название Дерева флэш- диска (FD- дерева, Flash Disk Tree) [LI10].

Некое FD- дерево состоит из небольшого подверженного изменениям головного дерева и множества неизменных сортированных исполнений. Этот подход ограничивает область поверхности при необходимости случайных операций ввода/ вывода имеющимся головным деревом: неким небольшим B- деревом, буферирующим такие изменения. Как только такое головное дерево заполнено, его содержимое передаётся в соответствующее неизменное исполнение (run). Когда размер вновь записываемого исполнения превосходит установленное пороговое значение, его содержимое сливается с имеющимся следующим уровнем, постепенно распространяя записи данных с верхнего на нижние уровни.

Фрагментарное каскадирование

Для поддержки указателей между имеющимися уровнями FD- деревья применяют некую технологию, имеющую название фрагментарного каскадирования (ractional cascading) [CHAZELLE86]. Такой подход помогает снижению общей стоимости определения места в имеющемся каскаде сортированных массивов: вы выполняете log n шагов для обнаружения искомого элемента в самом первом массиве, но последующие поиски значительно дешевле, так как они начинают свой поиск с самого близкого соответствия от своего предыдущего уровня.

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

Поскольку [CHAZELLE86] решает задачу поиска в вычислительной геометрии, он описывает двусторонние мосты и некий алгоритм восстановления инварианта зазора, который мы не будем здесь рассматривать. Мы опишем лишь те части, которые применимы к хранилищам баз данных и, в частности, к FD- деревьям.

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

Например, когда у нас имеется множество сортированных массивов:

A1 = [12, 24, 32, 34, 39]
A2 = [22, 25, 28, 30, 35]
A3 = [11, 16, 24, 26, 30]

Мы можем сократить имеющийся зазор между элементами вынимая всякий иной элемент из имеющегося массива с более высоким индексом в массив с меньшим индексом для упрощения поисков:

A1 = [12, 24, 25, 30, 32, 34, 39]
A2 = [16, 22, 25, 26, 28, 30, 35]
A3 = [11, 16, 24, 26, 30]

Теперь мы можем воспользоваться этими вытащенными элементами для создания мостов (или как они называются в статье по FD- деревьям, препятствиях - fences): указатели от элементов верхнего уровня к их двойникам на имеющихся нижних уровнях, как это отображено на Рисунке 6-5.

Рисунок 6-5

Фрагментарное каскадирование

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

Логарифмические прогоны

Некое FD- дерево сочетает фрагментарное каскадирование с созданием логарифмически сортируемых сортированных запусков: неизменные сортированные массивы с размерами, увеличивающимися на множитель k, создаваемые слиянием первоначального уровня с текущим.

Самый высокий уровень исполнения (прогона, run) создаётся когда заполняется установленное головное дерево: содержимое его листьев записано на его первом уровне. Как только это головное дерево заполняется нова, его содержимое сливается с элементами его первого уровня. Эти слитые результаты в результате замещают имеющуюся старую версию такого его первого исполнения. Соответствующие прогоны нижнего уровня создаются в случае достижения размерами прогонов верхнего уровня некого порогового значения. Когда исполнение нижнего уровня уже имеется, оно замещается таким результатом слияния его содержимого с получаемым содержимым верхнего уровня. Этот процесс достаточно схож со сжатием в деревьях LSM, при котором содержимое неизменной таблицы сливается для создания таблиц большего размера.

Рисунок 6-6 отображает схематическое представление некого FD- дерева, причём с неким головным B- деревом в его вершине, двумя логарифмическими прогонами L1 и L2 и мостами между ними.

Рисунок 6-6

Схематичное представление FD- дерева {Прим. пер.: подробнее о построении FD- дерева в Дополнении B.}

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

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

Bw- деревья

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

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

Мы можем собирать в пакет обновления к различным узлам применяя хранилище с исключительным добавлением в конец, связывать узлы воедино в цепочки и пользоваться структурами данных в памяти, которые позволяют устанавливать указатели между соответствующими узлами единственной операцией сравнить- и- обменять, делая дерево свободным от блокировок. Этот подход носит название Модного дерева (Buzzword- tree, Bw- дерева) [LEVANDOSKI14].

Цепочки обновлений

Некое Bw- дерево записывает какой- то базовый узел отдельно от его изменений. Изменения узлы приращений формируют цепь: некий связанный список от самых новых изменений через более старые, имея в самом конце свой базовый узел. Каждое обновление может храниться отдельно, причём без необходимости перезаписывать уже имеющийся узел на диск. Узлы приращений могут представлять вставки, обновления (которые неотличимы от вставок) или удаления.

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

Обладание неким узлом как логической, а не физической, сущностью это интересное изменение системы взглядов: нам не требуется предварительно выделять пространство чтобы иметь некий фиксированный размер, либо даже удерживать его в непрерывных сегментах памяти. Несомненно, у этого имеется обратная тёмная сторона: в процессе чтения требуется обходить все приращения и применять их к имеющемуся базовому узлу для перестроения реального состояния узла. Это нечто подобно тому, что делают LA- деревья (см. раздел Дерево ленивой адаптации): хранить обновления отдельно от их основной структуры и воспроизводя их при чтении.

Культивирование одновременности при помощи Compare- and- Swap

Было бы достаточно затратным сопровождать некую дисковую структуру дерева, которая позволяла бы вставлять спереди элементы в дочерние узлы: это потребовало бы от нас постоянного обновления родительских узлов указателями на наиболее свежие приращения. Именно по этой причине узлы Bw- дерева, составляемые из некой цепи приращений и своего базового узла, обладают логическими идентификаторами и применяют некую таблицу соответствий в памяти для соответствующих идентификаторов к их местоположению на диске. Использование такого соответствия также способствует нам в избавлении от защёлок: вместо того чтобы иметь исключительное обладание на протяжении времени записи, наше Bw- дерево применяет операции сравнения- и- обмена (compare-and-swap) по физическим смещениям в своей таблице соответствий.

Рисунок 6-7 отображает образец Bw- дерева. Каждый логический узел состоит из единственного базового узла и множества связанных узлов приращений (дельта- узлов).

Рисунок 6-7

Bw- дерево. Пунктирные линии представляют связи между имеющимися узлами, разрешаемые при помощи установленной таблицы соответствий. Сплошные линии представляют реальные указатели на данные между имеющимися узлами.

Для обновления узла Bw- дерева его алгоритм выполняет следующие шаги:

Искомый целевой логический узел листа определяется обходом данного дерева от корня к листьям. Имеющаяся таблица соответствий содержит виртуальные связи с целевыми базовыми узлами или значение самых последних узлов приращений в установленной цепи обновлений.

Некий новый узел приращения создаётся с указателем на его базовый узел (или на самый последний узел приращения), найденный на шаге 1.

Таблица соответствия обновляется неким указателем на самый новый узел приращения, созданный в процессе шага 2.

Операция обновления на шаге 3 может осуществляться при помощи сравнения- и- перестановки (compare- and- swap), являющейся атомарной операцией, поэтому все чтения, одновременные с обновлением данного указателя, упорядочиваются либо до, либо после самой записи без блокирования как имеющихся читателей, так и самого писателя. Установленные по порядку до считывания следуют старому указателю и не видят получаемого нового узла приращения, так как он ещё не установлен. Чтения, установленные по порядку после, следуют новым указателем и наблюдают полученное обновление. Когда два потока пытаются установить некий новый узел в одном и том же логическом узле, лишь один из них может быть успешным, а другому придётся повторить свою операцию.

Операции структурного изменения

Bw- дерево логически структурировано как некое B- дерево, что означает, что узы всё ещё способны расти и становиться слишком большими (переполняться), либо сжиматься и становиться практически пустыми (недозаполненность) и требовать операций структурного изменения (SMO, structure modification operations), например, расщеплений и слияний. Сама семантика расщеплений и слияний здесь является аналогичной с B- деревьями (см. раздел Расщепление узла В- дерева), однако их реализация различается.

SMO расщепления стартует с консолидации имеющегося логического содержимого соответствующего расщепляемого узла, применяя приращения к его базовому узлу и создавая некую новую страницу, содержащую элементы справа от точки расщепления. После этого этот процесс проистекает в дав этапа [WANG18]:

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

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

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

SMO слияния работает аналогично:

Удаление собрата- Создаётся некий специальный узел приращения удаления и добавляется в конец надлежащего правого собрата, указывая начало данного SMO слияния и помечая своего правого собрата для удаления.

Слияние- Создаётся некий узел приращения слияния в соответствующем левом собрате для указания на содержимое его правого собрата и чтобы сделать его логической частью этого левого собрата.

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

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

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

Консолидация и сборка мусора

Если не предпринимать каких- либо дополнительных действий, цепи приращений могут становиться произвольно длинными. Так как чтения становятся более затратными по мере того как удлиняются такие цепочки приращений, нам необходимо попытаться удерживать длину своих цепей приращения в разумных рамках. Когда они достигают некого настроенного порогового значения, мы перестраиваем данный узел сливая содержимое его базового узла со всеми прочими приращениями, собирая их в один новый базовый узел. Такой новый узел затем записывается в новом месте на диске и в установленной таблице соответствий обновляется соответствующий указатель узла чтобы отмечать его. Более подробно мы обсуждаем этот процесс в разделе Составление стека LLAMA и Mindful, поскольку за сборку мусора, объединение узлов и передислокацию отвечает лежащая в основе хранилище структурированного журнала.

После того как определённый узел объединён, его старое содержимое (сам базовый узел и все его узлы приращений) больше не имеют адресов в установленной таблице соответствий. Тем не менее, мы не можем освобождать занимаемую ими память прямо сразу, ибо некоторые из них могут использоваться в проистекающих операциях. Так как нет никаких удерживаемых читателями защёлок (читателям не приходится для доступа к данному узлу проходить сквозь некий вид барьеров или регистраций), нам необходимо найти прочие средства отслеживания живых страниц.

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

Такое Bw- дерево является неким занимательным вариантов B- дерева, выполняющим улучшения для ряда важным сторон: прироста числа записей, не блокируемого доступа и дружелюбия кэшированию. Некая видоизменённая версия была реализована в Sled, экспериментальном механизме хранения. CMU Database Group разработала некую версию Bw- дерева в оперативной памяти под названием OpenBw-Tree и выпустила некое практическое руководство реализации [WANG18].

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

Не замечающие кэш B- деревья

Размер блока, размер узла, выравнивание линий кэша и прочие параметры настройки влияют на производительность B- дерева. Некий новый класс структур данных с названием не замечающих кэш структур (cache-oblivious structures) [DEMAINE02] предоставляют асимптотически оптимальную производительность вне зависимости от лежащей в основе иерархии памяти и потребности в настройке этих параметров. Это означает, что такой алгоритм не требует знания размеров имеющихся строк кэширования, блоков файловой системы и страниц дисков. Не замечающие кэширование структуры разработаны для надлежащей работы без изменений во множестве машин с различными настройками.

До сих пор мы в основном рассматривали B- деревья с двухуровневой иерархией памяти (за исключением LMDB, описанных в разделе Копирование записью). Узлы B- дерева хранятся в дисковых страницах, а для возможности действенного доступа к ним в оперативной памяти применяется устанавливаемый кэш страниц.

Такими двумя уровнями иерархии выступают кэш страниц (который является более быстрым, но ограничен в размере) и диск (значительно более медленный, но обладающий большей ёмкостью) [AGGARWAL88]. В этом случае у нас имеются лишь два параметра, что упрощает проектирование алгоритмов, ибо нам приходится иметь дело лишь с двумя модулями кода, особенными для уровня, которые и заботятся обо всех относящихся к такому уровню деталях.

Чам диск разбивается на части в блоки и обмен данными между диском и кэшем выполняется блоками: даже когда самому алгоритму приходится определять место отдельного элемента внутри блока, должен быть загружен весь блок целиком. Этот подход является осведомлённым о кэшировании.

при разработке критичного к производительности программного обеспечения, мы зачастую программируем более сложную модель, учитывая кэши ЦПУ, а порой даже иерархию дисков (например, горячее/ холодное хранение, или выстраивание иерархий HDD/ SSD/ NVMe, а также стадий переключения данных с одного уровня на другой). В большинстве случаев такие усилия сложно обобщать. В разделе Сопоставление DBMS в памяти и основанных на дисках мы обсудили тот факт, что доступ к диску на несколько порядков медленнее доступа к основной памяти, что имеет мотивацию реализующих базу данных в оптимизации данной разницы.

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

Схема ван Эмде Боаса

Не замечающие кэширование B- деревья состоят из некого статического B- дерева и структуры упакованного массива [BENDER05]. Статическое B- дерево строится с применением установленной схемы ван Эмде Боаса. Она расщепляет всё дерево на на своём среднем уровне имеющихся граней. Далее каждое поддерево рекурсивно расщепляется до меньшего размера, имея в результате поддеревья с размером в sqr(N). Ключевая мысль этой схемы состоит в том, что любое рекурсивное дерево хранится в каком- то непрерывном блоке памяти.

На Рисунке 6-8 вы можете видеть некий образец схемы ван Эмде Боаса. Логически сгруппированные совместно узлы помещаются поблизости друг от друга. В его верхней части вы можете отслеживать некую логическую схему представления (то есть как узлы формируют некое дерево), а снизу вы можете наблюдать как узлы дерева размечаются в памяти и на диске.

Рисунок 6-8

Схема ван Эмде Боаса

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

Рисунок 6-9

Упакованный массив

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

Само статическое дерево применяется в качестве индексного для своего упакованного массива нижнего уровня и обязано обновляться в соответствии с перемещениями элементов для указания на верные элементы нижнего уровня.

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

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

Выводы

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

Прирост записей может быть снижен при помощи буферирования. Ленивые B- деревья, такие как WiredTiger и LA- деревья, подключают буферы памяти к индивидуальным узлам или группам узлов для снижения общего числа необходимых операций ввода/ вывода буферируя последовательные обновления в страницах в памяти.

Для снижения прироста пространства, FD- деревья пользуются неизменностью: записи данных хранятся в неизменных сортированных прогонах, а размер изменяемого B- дерева ограничен.

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

[Замечание]
Последующее чтение

Если вы желаете узнать больше относительно упоминавшихся в этой главе понятий, вы можете ссылаться на следующие источники:

B- деревья копирования записью

Driscoll, J. R., N. Sarnak, D. D. Sleator, and R. E. Tarjan. 1986. “Making data structures persistent.” in Proceedings of the eighteenth annual ACM symposium on Teory of computing (STOC ’86), 109-121. https://dx.doi.org/10.1016/0022-0000(89)90034-2 .

Деревья ленивой адаптации

Agrawal, Devesh, Deepak Ganesan, Ramesh Sitaraman, Yanlei Diao, and Shashi Singh. 2009. “Lazy-Adaptive Tree: an optimized index structure for flash devices.” in Proceedings of the VLDB Endowment 2, no. 1 (January): 361-372. https://doi.org/10.14778/1687627.1687669 .

FD- деревья

Li, Yinan, Bingsheng He, Robin Jun Yang, Qiong Luo, and Ke Yi. 2010. “Tree Indexing on Solid State Drives.” in Proceedings of the VLDB Endowment 3, no. 1-2 (September): 1195-1206. https://doi.org/10.14778/1920841.1920990 .

{Прим. пер.: http://pages.cs.wisc.edu/~yinan/fdtree.html } .

Bw- деревья

Wang, Ziqi, Andrew Pavlo, Hyeontaek Lim, Viktor Leis, Huanchen Zhang, Michael Kaminsky, and David G. Andersen. 2018. “Building a Bw-Tree Takes More Than Just Buzz Words.” in Proceedings of the 2018 International Conference on Management of Data (SIGMOD ’18), 473–488. https://doi.org/10.1145/3183713.3196895 .

Levandoski, Justin J., David B. Lomet, and Sudipta Sengupta. 2013. “The BwTree: A B-tree for new hardware platforms.” in Proceedings of the 2013 IEEE International Conference on Data Engineering (ICDE ’13), 302-313. IEEE. https://doi.org/10.1109/ICDE.2013.6544834 .

Не замечающие кэширования B- деревья

Bender, Michael A., Erik D. Demaine, and Martin Farach-Colton. 2005. “CacheOblivious B-Trees.” in SIAM Journal on Computing, 35, no. 2 (August): 341-358. https://doi.org/10.1137/S0097539701389956 .

Дополнение B. Дерево индексации в твердотельных устройствах

Yinan Li*, Bingsheng He, Robin Jun Yang, Qiong Luo, Ke Yi

Hong Kong University of Science and Technology

{fyinanli, saven, yjrobin, luo, yikeg}@cse.ust.hk

Содержание

Дополнение B. Дерево индексации в твердотельных устройствах

Краткий обзор

Введение

Подготовка и относящиеся к ней работы

Оптимизация произвольных записей на SSD

Оптимизированное для записи дерево индексации

FD- дерево

Принципы разработки для индексации в SSD

Обзор FD- дерева

Операции в FD- дереве

Возврат отчуждения операций в FD- дереве

Анализ стоимости и модель стоимости

Результаты экспериментов

Настройка эксперимента

Анализ модели

Производительность вставки и удаления

Производительность операции возврата отчуждения

Сравнение производительности флеш SSD

Сравнение производительности шпиндельного диска

Выводы

Ссылки

Приложение A. Начальные сведения по флэш SSD

Приложение B. Псевдокод алгоритма

Приложение С. Анализ стоимости и модель стоимости

С1. Анализ времени поиска

С2. Анализ времени вставки

С3. Модель стоимости для установки параметров

Приложение D. Подробности экспериментальной настройки

Краткий обзор

Введение

Подготовка и относящиеся к ней работы

Оптимизация произвольных записей на SSD

Оптимизированное для записи дерево индексации

FD- дерево

В этом разделе мы представим архитектуру FD- дерева. Наша цель состоит в минимизации общего числа небольших произвольных записей и ограничить эти произвольные записи в пределах небольшой области и в то же время предоставлять высоко эффективный поиск. Для простоты мы предполагаем уникальность всех ключей индексации в неком FD- дереве. Применяемая на протяжении этой статьи нотация сведена в Таблице B-1

Таблица B-1. Применяемые в этой статье параметры

Параметры
Описание

B

размер страницы (байты)

Li

iй уровень FD- дерева

|Li|

ёмкость Li (общее число вхождений)

l

число уровней FD- дерева

k

логарифмическое соотношение размеров между соседними уровнями

n

число записей в данном соотношении индексации

f

число вхождений в странице

Принципы разработки для индексации в SSD

В своём проекте для некой индексации во флэш SSD мы рассмотрим три таких принципа.

П1. Преобразование произвольных записей в последовательные. Нам следует воспользоваться преимуществами последовательных записей и избегать произвольных за счёт проектирования передовых структур данных.

П2. Ограничение произвольных записей внутри ограниченной области. Предыдущие исследования [4, 6] сообщали что произвольные записи на флэш SSD в небольшой области (512кБ - 8МБ) обладают производительностью, сопоставимой с последовательными записями.

П3. Поддержка оптимизации ввода/ вывода множества страниц. Доступ ко множеству страниц в некой операции ввода/ вывода более действенна чем доступ к каждой странице по отдельности.

Обзор FD- дерева

Некое FD- дерево составляется из множества уровней, обозначаемых как L₀ ~ Li-1. Самый верхний уровень, L₀, является небольшим B⁺- деревом, именуемым головным деревом. Размер соответствующего узла этого головного дерева равняется размеру страницы B его флеш SSD. Всякий последующий уровень, Li (1 ≤ i < l) являются отсортированными прогонами (run) в непрерывных страницах. Рисунок B-2 a) иллюстрирует структуру некого FD- дерева. Это FD- дерево имеет три уровня, само головное дерево и два сортированных прогона. Его головное дерево является двухуровневым B⁺- деревом. С помощью технологии фрагментарного каскадирования (fractional cascading), определённые листовые узлы данного головного дерева содержат указатели на сортированный прогон L₁. Все нелистовые уровни в FD- дереве содержат указатели на сортированные прогоны на уровень непосредственно ниже своего.

Рисунок B-2

Пример FD- дерева

Каждый уровень FD- дерева обладает некой ёмкостью (capasity) в терминах вхождений, обозначаемой как |Li|. Следуя логарифмическому методу, мы устанавливаем уровни с некой пошаговой ёмкостью, т.е. |Li+1| = k ∙ |Li| (0 ≤ i ≤ l-2), где k это значение логарифмического соотношения размеров между соседними уровнями. Таким образом, |Li| = ki ∙ |L₀|. Все обновления первоначально выполняются в установленном головном дереве, а затем постепенно мигрируют в сортированные прогоны на нижние уровни пакетно по мере расходования установленной ёмкости уровня. Следуя принципу проектирования П2, величина максимального размера установленного головного дерева устанавливается равным размеру значения области локальности, в рамках которой произвольные записи имеют производительность, аналогичную последовательным. Значение размера области локальности в устройствах наших дней обычно в пределах от 128кБ до 8МБ [4, 6].

Мы разбиваем вхождения в FD- дерево на категории двух видов, индексные вхождения и преграды (fetches - мосты - согласно охватывающей книге). На каждом уровне FD- дерева имеющиеся индексные вхождения и преграды организованы в восходящем порядке своих ключей.

Индексные вхождения (Index Entry). Некое индексное входение содержит три поля: ключ индекса, key, а также некий идентификатор записи, rid, для индексации записи данных и type, указывающий на роль вхождения в установленном логарифмическом удалении FD- дерева. В зависимости от type, мы и далее разбиваем на категории индексные вхождения на два вида, фильтрующие вхождения и нормальные вхождения.

Фильтрующее вхождение (type = Filter). Некое фильтрующее вхождение это отметка удаления. Это фильтрующее вхождение вставляется в FD- дерево при неком удалении для указания того что относящаяся к нему запись была номинально удалена. Она обладает теми же самыми ключом и идентификатором записи, что и удалённое индексное вхождение. Мы именуем такой удалённый индекс как фантомное вхождение (phantom entry), поскольку оно логически было удалено, но физически пока не удалено из данного индекса.

Нормальное вхождение (type = Normal). Все индексные вхождения, отличающиеся от фильтрующих вхождений, носят название нормальных вхождений.

Преграда (fence, мост). Преграда это некое вхождение с тремя полями: каким- то значением ключа, key, неким type и pid, значении идентификатора определённой страницы в идущем непосредственно ниже уровне, в который поиск проследует далее. По- существу, некая преграда {мост} это указатель, ключ которого выбран из индексного вхождения в FD- дереве.

Инвариант 1. Самая первая запись каждой страницы это преграда {мост}.

Инвариант 2. Значение диапазона ключей между некой преградой {мостом} и следующей за ним преградой {мостом} на том же самом уровне содержится в том диапазоне ключей соответствующей страницы, на которую указывает эта преграда {мост}.

В зависимости от того, будет ли значение определённого ключа рассматриваемой преграды на уровне Li выбираться с уровня Li или с уровня Li+1, мы разбиваем преграды на уровне Li на два вида, внутренние преграды и внешние преграды.

Внешняя преграда (type = External). Значение ключа некой внешней преграды {моста} на уровне Li выбирается с уровня Li+1. Для страницы P на уровне Li+1 мы выбираем значение ключа самой первой записи из P в качестве значения ключа этой преграды и устанавливаем значение поля pid для данной преграды в качестве идентификатора P для удовлетворения Инварианта 2.

Внутренняя преграда (type = Internal). Значение ключа некой внутренней преграды {моста} на уровне Li выбирается с самого уровня Li. Если значение самого первого вхождения произвольной страницы P не является внешней преградой, мы добавляем некую внутреннюю преграду в самый первый разъём данной страницы для удовлетворения Инварианта 1. Значением ключа такой внутренней преграды устанавливается значением ключа самого первого индексного вхождения e со страницы P. Значение поля pid такой внутренней преграды устанавливается равным идентификатору соответствующей страницы на следующем уровне, для которой диапазон ключей охавтывает e. Например, на Рисунке B-2 a) запись 88 со страницы g это некая внутренняя преграда, которая указывает на страницу n, причём она та же самая, что и внешняя преграда 71 со страницы f.

В соответствии с установленным определением для внешней преграды, общее число внешних преград на уровне Li равняется общему числу страниц на уровне Li+1, т.е. |Li+1|/f, где f равно общему числу вхождений в некой странице. Общее число внутренних преград на уровне Li, по крайней мере, составляет |Li|/f, так как каждая страница содержит по крайней мере одну внутреннюю преграду. Максимальное значение преград на уровне Li, (|Li| + |Li+1|)/f должно быть меньше общего числа вхождений в Li, достигая k < f - 1.

Операции в FD- дереве

FD- дерево поддерживает пять общих операций запросов: поиск, вставку, слияние, удаление и обновление. За псевдокодом алгоритмов обратитесь к Приложению B.

Поиск. Некий поиск индекса в FD- дереве требует поиска на каждом уровне сверху донизу. Запрос может быть точечным поиском с неким предикатом эквивалентности, или поиском диапазона с предикатом диапазона.

Для выполнения точечного поиска по некому искомому ключу K мы вначале выполняем просмотр в своём головном дереве, точно так же как и в стандартном B⁺- дереве. Далее мы выполняем поиск на каждом уровне вслед за значением pid соответствующей преграды {моста}. Внутри некой страницы P на уровне Li осуществляется двоичный поиск для поиска наилучшего ключа равного или меньшего чем K. Допустим, соответствующее вхождение ei содержит такой ключ. Затем мы сканируем сортированный прогон справа налево пока не обнаружим некую преграду ej. Поскольку все вхождения с ключами между ei.key и ej.key в нашем следующем уровне Li+1 появляются на соответствующей странице ej.pid (Инвариант 2), мы далее следуем по указателю ej.pid на эту страницу для поиска вхождений на своём следующем уровне. Тем самым мы обходим всё дерево сверху вниз, следуя по pid принятых преград.

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

В соответствии с Инвариантом 1 некий поиск по крайней мере может найти соответствующую преграду в самом первом разъёме любой страницы при сканировании такой страницы в обратном порядке. Тем самым, когда нет дубликатов, поиск сталкивается только с одной преградой на странице каждого уровня. Именно по этой причине мы ввели внутренние преграды. Когда данные скошены, соответствующие индексные вхождения между двумя последовательными внешними вхождениями, Fj и Fj+1 могут распространяться на множество страниц. Некому стартующему между Fj и Fj+1 сканированию необходимо пройти по множеству страниц для получения своей предыдущей преграды Fj. При наличии соответствующих внутренних преград такое сканирование останавливается соответствующим внутренними преградами в самом первом разъёме каждой из страниц.

Рисунок B-2 b) иллюстрирует такой путь поиска ключа 81 (сплошная линия) и ключа 91 (пунктирная линия) в нашем примере FD- дерева с Рисунка B-2 a). На каждом уровне он находит соответствующую страницу из следующего уровня сортированных прогонов. При поиске 91 на уровне L₁, установленная на странице g преграда 88 препятствует данному сканированию в выборке страницы f при поиске соответствующей внешней преграды 71.

Поиск диапазона аналогичен точечному поиску за исключением того, что он способен выбирать на каждом уровне множество страниц. Получая значение преграды, удовлетворяющее значению предиката на своём текущем уровне Li, нам становится известно об общем числе страниц, которое будет просканировано на соответствующем следующем уровне, Li+1, прежде чем мы столкнёмся с преградой на этих страницах. Более того, эти страницы хранятся непрерывным образом. Данное свойство предоставляет нам возможность выборки точного числа соответствующих страниц со своего следующего уровня в некой операции ввода/ вывода применяя оптимизацию ввода/ вывода множества страниц (П3).

Вставка. некое новое вхождение изначально вставляется первым в своё головное дерево L₀. Когда общее число вхождений в этом головном дереве L₀ превосходит его ёмкость |L₀|, для L₀ и L₁ выполняется операция слияния с целью осуществления миграции всех вхождений L₀ на L₁. В результате, число произвольных записей ограничено в рамках установленного головного дерева, следуя принципу П2.

Слияние. Процесс слияния выполняется между двумя прилегающими уровнями когда меньший из них достигает предела своей ёмкости. Данная операция слияния последовательно два соответствующих входных потока и сочетает их в один сортированный прогон в непрерывных страницах. Вновь создаваемый уровень Li состоит из всех индексных вхождений из Li-1, а также всех индексных вхождений и внешних преград из Li. Мы оставляем в Li все внешние преграды, так как соответствующий уровень (Li+1), на который указывают эти внешние вхождения не изменился. По мере необходимости конструируются соответствующие внутренние преграды Li. В то же самое время перестраиваются соответствующие новые уровни Lj (0 ≤ j < i) для соответствующих конструируемых с вновь создаваемого уровня Li внешних преград. То есть, получая два прилегающих уровня, Li-1 и Li, наш процесс слияния вырабатывает i + 1 новых сортированных прогонов для обновления всех уровней с L₀ до Li. Если такой получаемый новый уровень Li исчерпывает свою ёмкость, сливаются Li и Li+1. Этот процесс продолжается пока самый большой из двух вновь вырабатываемых уровней не перестанет исчерпывать свою ёмкость.

Данная операция слияния вовлекает в свой процесс только последовательные чтения и записи, тем самым мы успешно преобразуем свои произвольные записи вставки в последовательные считывания и записи, следуя своему принципу проектирования П1. Мы и далее оптимизируем производительнсоть своих операций ввода/ вывода применяя оптимизацию ввода/ вывода множества страниц следуя принципу П3. Так как страницы на каждом уровне FD- дерева хранятся непрерывным образом на соответствующем флэш- дискемы выполняем выборку множества страниц единым запросом ввода/ вывода. Аналогично, так как заново сгенерированные сортированные прогоны записываются последовательно, мы также записываем множество страниц единым запросом. Подходящее число страниц в неком запросе ввода/ вывода устанавливается равным размеру единицы доступа при котором скорость обмена шаблона последовательного доступа достигает максимального значения.

Удаление. Некое удаление в FD- дереве обрабатывается каким- то аналогичным вставке методом: оно сначала выполняется в установленном головном дереве, а затем мигрирует на свои нижние уровни по мере возникновения слияний. Такая схема логарифмического удаления снижает амортизационные расходы. Отметим, что известный метод ленивого удаления, широко используемый в жёстких дисках, на которых основывается индексация, который помечает некое вхождение как неверное, не эффективен для флеш SSD, так как операция пометки это небольшая произвольная запись.

Самый первый шаг состоит в выполнении удаления в имеющемся головном дереве L₀, так как произвольные записи в нашем головном дереве ограничены пространством и очень эффективны. Затем, мы выполняем удаление на всех прочих уровнях вставляя особое вхождение с названием фильтрующего вхождения. Такое подлежащее удалению вхождение далее становится фантомным вхождением и оставляется нетронутым. В частности, мы вначале выполняем в своём FD- дереве поиск примеяя соответствующий предикат для этого удаления. Такой поиск выявляет все подлежащие удалению индексные вхождения. Новые вхождения (фильтрующие вхождения) с тем же самым ключом и значением указателя что и эти вхождения вставляются в наше FD- дерево. Собственно реальное удаление осуществляется при операции слияния когда фильтрующие вхождения встречают свои соответствующие фантомные вхождения.

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

Объём накладных расходов пространства фильтрующих и фантомных вхождений низок. Поскольку самый нижний уровень Li-1 не содержит никаких фильтрующих или фантомных вхождений, эти вхождения в худшем случае занимают все уровни кроме самого нижнего, чей общий размер всего лишь около 1 / k всего индексного пространства. Так как k обычно велико, фильтрующие и фантомные вхождения оказывают низкое воздействие на производительность.

Рисунок B-3 иллюстрирует некий пример такого процесса удаления. Мы помечаем свои фильтрующие вхождения сплошным подчёркиванием. На Рисунке B-3 a) мы удаляем свои индексные вхождения 37 для L₀, 45 для L₁ и 16 для L₂. Поскольку вхождение 37 находится в нашем головном дереве L₀, оно удаляется непосредственно из L₀. Соответствующие фильтрующие вхождения 45 и 16 вставляются в наше головное дерево. По мере выполнения прочих вставок и удалений, наше головное дерево постепенно становится большим. Когда оно заполняется и выполняется слияние L₀ и L₁, как это показано на Рисунке B-3 b), фильтрующее вхождение 45 сталкивается со своим фантомным вхождением и оба вхождения отвергаются. При вставке большего числа вхождений в значения индексов и когда происходит слияние между L₁ и L₂, как это показано на Рисунке B-3 c), отвергаются фильтрующее вхождение 16 и соответствующее ему фантомное вхождение.

Рисунок B-3

Пример процесса логарифмического удаления

Обновление. Операция обновления реализуется как некое удаление для старого значения со следующей за ним вставкой.

Возврат отчуждения операций в FD- дереве

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

Рисунок B-4 демонстрирует основную идею возврата отчуждений вставок в FD- дереве, которая состоит в перекрытии процесса выполнения операций вставок и слияния. В частности, получая под слияние Nmerge вхождений, мы делим эти Nmerge вхождений на |L₀| разделов и со временем комбинируем вхождения в неком разделе после выполнения некой вставки. В результате общая затратная стоимость соответствующей операции слияния возмещается |L₀| операциями вставок. Тем самым наихудшее время на вставки уменьшается примерно на множитель |L₀|, причём средняя стоимость остаётся неизменной.

Рисунок B-4

Вставка с возвратом отчуждения и без него

Чтобы перекрывать время исполнения вставок и слияния мы поддерживаем два головных дерева. Как только некое головное дерево L₀ наполняется, новые вхождения вставляются в другой экземпляр, то есть во временное головное дерево L'₀, в то время как само слияние выполняется для L₀. Этот процесс слияния аналогичен тому, что мы описали в разделе 3.3, за исключением того что внешние преграды {мосты} из соответствующих нижних уровней вставляются в L'₀ одна за другой вместо выполнения пакетом. После того как данное слияние завершено, L'₀ уже был заполнен и все врешние преграды уже были вставлены в L'₀. При возврате отчуждений после завершения слияния мы меняем местми L₀ и L'₀ для последующих вставок и слияний.

С помощью возврата отчуждений запросы поиска индекса могут продолжаться даже в процессе слияния. Так как первоначальное FD- дерево (L₀ ~ Ll-1) содержит все первоначальные вхождения вставленными до самой операции слияния, а временное головное дерево L'₀ хранит все вновь созданные вхождения, мы можем осуществлять просмотр как в своём первоначальном FD- дереве (L₀ ~ Ll-1), так и в нашем временном головном дереве L'₀. Значение размера временного головного дерева (T'₀) настолько мало, что скорее всего умещается в памяти и значение накладных расходов производительности поиска с возвратом отчуждений незначительно.

Анализ стоимости и модель стоимости

Таблица B-2

Сопоставление стоимости ввода/ вывода для четырёх типов индексов

Результаты экспериментов

Настройка эксперимента

Анализ модели

Производительность вставки и удаления

Производительность операции возврата отчуждения

Сравнение производительности флеш SSD

Сравнение производительности шпиндельного диска

Выводы

Ссылки

[1] D. Agrawal, D. Ganesan, R. Sitaraman, Y. Diao, and S. Singh. Lazy-adaptive tree: An optimized index structure for flash devices. PVLDB, 2(1):361–372, 2009 .

[2] L. Arge. The buffer tree: A new technique for optimal i/o-algorithms. In WADS, 1995. .

[3] J. L. Bentley. Decomposable searching problems. Inf. Process. Lett., 8(5), 1979.2009 .

[4] L. Bouganim, B. T. Jonsson, and P. Bonnet. uflip: Understanding flash io patterns. In CIDR, 2009. .

[5] B. Chazelle and L. J. Guibas. Fractional cascading: I. a data structuring technique. Algorithmica, 1(2), 1986. .

[6] F. Chen, D. Koufaty, and X. Zhang. Understanding intrinsic characteristics and system implications of flash memory based solid state drives. In SIGMETRICS, 2009 .

[7] S. Chen. Flashlogging: Exploiting flash devices for synchronous logging performance. In SIGMOD Conference, 2009 .

[8] D. Comer. The ubiquitous b-tree. ACM Comput. Surv., 11(2), 1979. .

[9] E. Gal and S. Toledo. Algorithms and data structures for flash memories. ACM Comput. Surv., 37(2), 2005 .

[10] G. Graefe. Write-optimized b-trees. In VLDB, 2004 .

[11] G. Graefe. The five-minute rule 20 years later: and how flash memory changes the rules. ACM Queue, 6(4):40–52, 2008 .

[12] J. Gray and B. Fitzgerald. Flash disk opportunity for server applications. ACM Queue, 6(4):18–23, 2008 .

[13] J. M. Hellerstein, M. Stonebraker, and J. R. Hamilton. Architecture of a database system. Foundations and Trends in Databases, 1(2), 2007 .

[14] H. V. Jagadish, P. P. S. Narayan, S. Seshadri, S. Sudarshan, and R. Kanneganti. Incremental organization for data recording and warehousing. In VLDB, 1997 .

[15] C. Jermaine, A. Datta, and E. Omiecinski. A novel index supporting high volume data warehouse insertion. In VLDB, 1999 .

[16] K. Kimura and T. Kobayashi. Trends in high-density flash memory technologies. In IEEE Conference on Electron Devices and Solid-State Circuits, 2003 .

[17] S.-W. Lee and B. Moon. Design of flash-based dbms: an in-page logging approach. In SIGMOD Conference, 2007 .

[18] S.-W. Lee, B. Moon, C. Park, J.-M. Kim, and S.-W. Kim. A case for flash memory ssd in enterprise database applications. In SIGMOD Conference, 2008 .

[19] Y. Li, B. He, Q. Luo, and K. Yi. Tree indexing on flash disks. In ICDE, 2009 .

[20] C. Manning. Yaffs: the nand-specific flash file system. 2002 .

[21] P. Muth, P. E. O’Neil, A. Pick, and G. Weikum. Design, implementation, and performance of the lham log-structured history data access method. In VLDB, 1998 .

[22] D. Myers. On the use of nand flash memory in high-performance relational databases. MIT Msc Thesis, 2008 .

[23] S. Nath and A. Kansal. Flashdb: dynamic self-tuning database for nand flash. In IPSN, 2007 .

[24] P. E. O’Neil, E. Cheng, D. Gawlick, and E. J. O’Neil. The log-structured merge-tree (lsm-tree). Acta Inf., 33(4), 1996 .

[25] M. Rosenblum and J. K. Ousterhout. The design and implementation of a log-structured file system. ACM Trans. Comput. Syst., 10(1), 1992 .

[26] D. Tsirogiannis, S. Harizopoulos, M. A. Shah, J. L. Wiener, and G. Graefe. Query processing techniques for solid state drives. In SIGMOD Conference, 2009 .

[27] D. Woodhouse. Jffs: The journalling flash file system. In Ottawa Linux Symposium, 2001 .

[28] C.-H. Wu, T.-W. Kuo, and L. P. Chang. An efficient b-tree layer implementation for flash-memory storage systems. In RTCSA, 2003 .

Приложение A. Начальные сведения по флэш SSD

Приложение B. Псевдокод алгоритма

Приложение С. Анализ стоимости и модель стоимости

С1. Анализ времени поиска

С2. Анализ времени вставки

С3. Модель стоимости для установки параметров

Приложение D. Подробности экспериментальной настройки

Глава 4. Реализация B- деревьев

Содержание

Глава 4. Реализация B- деревьев

Заголовок страницы

Магические числа

Братские ссылки

Крайние правые указатели

Верхние ключи узла

Страницы переполнения

Двоичный поиск

Двоичный поиск по косвенным указателям

Распространение расщеплений и слияний

Хлебные крошки

Повторная балансировка

Исключительно правые добавления в конец

Загрузка в навал

Сжатие

Уборка и сопровождение

Вызванная обновлениями и удалениями фрагментация

Дефрагментация страниц

Выводы

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

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

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

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

Заголовок страницы

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

Например, PostgreSQL хранит в своём заголовке значение размера страницы и версию схемы. В MySQL InnoDB заголовок страницы содержит общее число записей заголовка, уровень и некие иные сведения, особые для реализации. В SQLite заголовок страницы хранит общее число ячеек и самый правый указатель.

Магические числа

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

Магические числа часто используются для проверки подлинности и проверки корректности [GIAMPAOLO98]. Очень важно чтобы эта последовательность байт по некому произвольному смещению в строгости соответствовала данному магическому числу. Когда имеется соответствия, существует хорошая вероятность того что это смещение верное. например, чтобы проверить что данная страница загружена и выровнена как положено, в процессе записи мы можем помещать значение магического числа 50 41 47 45 (шестнадцатеричное значение для PAGE). В процессе этого считывания мы можем подтвердить данную страницу, сопоставив эти четыре байта из считанного заголовка с ожидаемой последовательностью байт.

Братские ссылки

Некоторые реализации хранят прямые и обратные ссылки, указывающие на самую левую и самую правую страницы одного уровня. Эти ссылки помогают определению места соседних узлов без необходимости подъёма вверх к своему родителю. Такой подход добавляет некую сложность в операции расщепления и слияния, поскольку также потребуется обновлять и такие братские смещения. Например, при расщеплении самого правого узла, его правый обратный указатель на собрата (ранее указывавший на тот узел который был расщеплён) должен быть подобран для указания на вновь созданный узел.

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

Рисунок 4-1

Сопоставление определения места собратьев путём следования родительским ссылкам (a) с прохождением по братским ссылкам (b)

Одним из недостатков хранения ссылок собратьев состоит в том, что они обязаны обновляться в процессе расщеплений и слияний. Так как обновления обязаны происходить в неком собратском узле, а не в узлах расщепления/ слияния, это может потребовать дополнительного блокирования. Мы обсудим то, насколько полезными могут быть ссылки собратьев в некой реализации одновременных B- деревьев в разделе Деревья Blink.

Крайние правые указатели

Ключи разделителя B- дерева имеют строгие инварианты: они применяются для расщепления данного дерева на поддеревья и навигации по ним, поэтому всегда имеется на один указатель на дочерние страницы больше, чем имеется ключей. Это именно тот +1, который упоминался ранее в разделе Ключи счётчика.

В разделе Ключи разделители мы описывали инварианты ключи разделителя. Во многих реализациях узлы выглядят во многом похоже с тем как они отображены на Рисунке 4-2: каждый ключ разделителя обладает неким дочерним указателем, в то время как самый последний указатель хранится отдельно, так как у него нет в паре ни одного ключа. Вы можете сравнить это с Рисунком 2-10.

Рисунок 4-2

Самый правый указатель

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

Когда расщепляется крайний правый потомок и получаемый новый потомок добавляется в конец к своему предку, значение самого правого указателя необходимо переназначить. Как показано на Рисунке 4-3, после такого расщепления полученная ячейка добавляется в конец своего предка (показана серым), удерживая значение продвигаемого ключа и указывая на расщеплённый узел. Вместо значения предыдущего крайнего правого указателя присваивается значение указателя на этот новый узел. Аналогичный подход описывается и реализуется в SQLite. (Вы можете отыскать данный алгоритм в функции balance_deeper в репозитории этого проекта.)

Рисунок 4-3

Обновление самого правого указателя в процессе расщепления узла. Продвигаемый ключ показан серым.

Верхние ключи узла

Мы можем предпринять несколько иной подход и сохранять и помимо крайнего правого указателя сохранять в соответствующей ячейке значения верхнего ключа (high key) узла. Это значение верхнего ключа представляет величину наивысшего возможного ключа, который может быть представлен в данном поддереве под текущим узлом. Такой подход применяется в PostgreSQL и имеет название Blink- деревьев (относительно реализаций данного подхода с одновременностью обратитесь к разделу Деревья Blink).

В- обладают N ключами (обозначаемыми как Ki) и N + 1 указателями (обозначаемыми как Pi). В каждом из поддеревьев ключи ограничены Ki ≤ Ks < Ki. Подразумевается, что значение K₀ = -∞ и оно не представлено в этом узле.

Деревья Blink добавляют в каждый узел некий ключ KN+1. Он определяет некую верхнюю границу ключей, которые могут храниться в том поддереве, на который ссылается указатель PN, а тем самым это некая верхняя грань значений, которые могут храниться в данном текущем поддереве. Оба подхода показаны на Рисунке 4-4: (a) отображает некий узел без верхнего ключа, а (b) показывает узел с верхним ключом.

Рисунок 4-4

B- деревья без (a) и с верхним ключом (b)

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

На Рисунке 4-5 вы можете наблюдать схематичную структуру страницы для обоих подходов и то как отличается расщепление самого пространства поиска для таких вариантов проходя до +∞ в самом первом случае и вверх до верхней границы K₃ во втором.

Рисунок 4-5

Применение +∞ как некого виртуального ключа (a) в сопоставлении со значением верхнего ключа (b)

Страницы переполнения

Размер узла и развёртываемые значения дерева фиксированы и не изменятся динамически. Также было бы трудно прийти к такому значению, которое оказалось бы универсально оптимальным: когда в дереве имеются переменные значения размера, причём они достаточно велики, только некоторые из них могут помещаться на имеющейся странице. Когда же значения очень малы, мы в конце концов теряем зарезервированное пространство.

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

Для реализации узлов с переменным размером без копирования данных в новую непрерывную область мы можем выстраивать узлы из множества связанных страниц. Например, установленный по умолчанию размер страницы составляет 4k, а после вставки нескольких значений размер его данных превышает 4k. Вместо того чтобы допускать произвольные размеры, узлы позволяют рост приращениями по 4k, а потому мы выделяем некую страницу расширения в 4k и связываем её с первоначальной. Такие страницы расширения носят название страниц переполнения (overflow pages). В рамках данного раздела для ясности мы будем именовать свою оригинальную страницу первоначальной страницей (primary page).

Большинство реализаций B- дерева делают возможным хранение исключительно фиксированного числа байт полезной нагрузки в своём узле B- дерева напрямую и вытеснять остальное в подобные страницы переполнения. Это значение вычисляется делением установленного размера узла на расширение. Применяя такой подход, мы не сможем прийти к ситуации при которой данная страница не имеет свободного пространства, поскольку она всегда будет иметь по крайней мере max_payload_size байт. Для получения дополнительных сведений по страницам переполнения в SQLite, обратитесь к репозиторию исходного кода SQLite; а также проверьте документацию MySQL InnoDB.

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

На Рисунке 4-6 мы можем видеть некую первичную страницу и страницу переполнения с записями, имеющими указатели со своей первоначальной страницы на страницу переполнения, в которой и продолжаются эта полезная нагрузка.

Рисунок 4-6

Страницы переполнения

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

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

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

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

Двоичный поиск

Мы уже обсуждали алгоритм поиска по B- дереву (см. Алгоритм поиска в В- деревьях) и упоминали, что мы определяем некий искомый ключ внутри данного узла с помощью алгоритма двоичного поиска. Двоичный поиск работает исключительно для сортированных данных. Когда ключи не упорядочены, по ним невозможно выполнять двоичный поиск. Именно по этой причине ключи отслеживают порядок и существенна поддержка неизменной сортировки.

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

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

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

Двоичный поиск по косвенным указателям

Ячейки в странице конкретного B- дерева хранятся в порядке вставки и лишь смещения ячеек удерживают имеющийся логический порядок элемента. Для выполнения двоичного поиска по ячейкам страницы мы берём значение смещения средней ячейки, следуем его указателю для определения места этой ячейки, сравниваем значение ключа из этой ячейки со значением искомого ключа для принятия решения по пути последующего поиска - вправо или налево, а затем рекурсивно продолжаем этот процесс пока не будет найден искомый элемент или необходимое место вставки, как это показано на Рисунке 4-7.

Рисунок 4-7

Двоичный поиск при помощи косвенных ссылок. Сам искомый элемент показан серым. Обозначенные пунктиром стрелки представляют двоичный поиск по указателям ячеек. Сплошные линии представляют доступы, которые следуют указателям этих ячеек, которые требуются для сравнения значений этих ячеек с неким искомым ключом.

Распространение расщеплений и слияний

Как мы уже обсуждали ранее в предыдущих главах, расщепления и слияния B- деревьев могут распространяться на более верхние уровни. Для этого нам требуется иметь возможность выполнять обход по некой цепочке обратно к узлу корня от расщепляемого листа или от сливаемых листьев.

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

Так же как и с указателями на собратьев (см. раздел Братские ссылки), указатели на родителей необходимо обновлять всякий раз когда изменяется такой предок. Это происходит во всех тех случаях, когда соответствующий ключ разделителя со значением идентификатора страницы передаётся от одного узла другому: в процессе расщепления узла его родителя, слияний или повторной балансировки имеющегося узла предка.

Некоторые реализации (к примеру, WiredTiger) применяют указатели родителей для обхода листьев во избежание взаимных блокировок, которые могут происходить при применении указателей собратьев (см. [MILLER78], [LEHMAN81]). Вместо применения братских указателей для обхода терминальных узлов такие алгоритмы держат в своём штате указатели на родителей, во многом подобно тому, что мы видели на Рисунке 4-1.

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

Хлебные крошки

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

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

Хлебные крошки содержат ссылки на те узлы, которым мы следуем от своего корня и применяются для отслеживания пути назад по ним в обратном направлении при распространении расщеплений и слияний. Наиболее естественной структурой для этого выступает некий стек. Например, PostgerSQL хранит хлебные крошки в неком стеке, имеющим внутреннее название BTStack. (Дополнительно о нём можно прочесть в репозитории данного проекта).

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

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

Рисунок 4-8

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

Повторная балансировка

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

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

B*- деревья распространяют данные по близлежащим узлам пока не заполнятся оба собрата [KNUTH98]. Затем, вместо расщепления отдельного узла на два заполненных наполовину, этот алгоритм расщепляет два узла на три узла, причём каждый заполнен на две трети. SQLite применяет именно этот вариант в своей реализации. такой подход улучшает значение средней занятости за счёт откладывания расщеплений, но требует дополнительной логики отслеживания и балансировки. Более высокая заполняемость также означает более действенные поиски, так как значение высоты получаемого дерева меньше и приходится обходить меньшее число страниц по пути к искомому листу.

Рисунок 4-9 показывает распределение элементов между двумя соседними узлами, при котором левый собрат содержит больше элементов чем правый. Элементы из более занятого узла перемещаются в более свободный. Так как балансировка изменяет значения минимума/ максимума инварианта в таких братских узлах, нам приходится обновлять ключи и указатели в соответствующем родительском узле для их сохранения.

Рисунок 4-9

Балансировка B- дерева: Распространение элементов между более заполненными и менее занятыми узлами.

Балансировка нагрузки является полезной технологией, которая применяется в реализациях многих баз данных. Например, SQLite реализует алгоритм балансировки собратьев, который до определённой степени близок к тому, что мы описали в данном разделе. Балансировка может добавлять некую сложность в имеющемся коде, но так как её варианты применения изолированы, она может реализовываться в виде некой оптимизации на более поздней стадии.

Исключительно правые добавления в конец

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

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

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

Загрузка в навал

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

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

Один из подходов для реализации пакетной загрузки состоит в записи предварительно отсортированных данных на имеющемся листовом уровне по всем страницам (вместо вставки индивидуальных элементов). По окончанию записи листовой страницы мы продвигаем её самый первый ключ в её предка и применяем обычный алгоритм для построения верхних уровней B- дерева [RAMAKRISHNAN03]. Так как добавляемые в конец ключи заданы в сортированном порядке, все расщепления в этом случае происходят в крайнем правом узле.

Так как B- деревья всегда строятся с самого нижнего (листового) уровня, можно записать весь уровень листьев целиком прежде чем составлять какие бы то ни было узлы верхнего уровня. Это позволяет получить на руки все дочерние указатели в момент времени построения уровней высшего порядка. Основные преимущества такого подхода состоят в том, что нам не приходится выполнять какие бы то ни было расщепления и слияния на диске и в то же самое время отслеживать в памяти во время построения лишь минимальную часть всего дерева (то есть всех родителей заполняемых в настоящее время листовых узлов).

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

Сжатие

Хранение сырых, несжатых данных способно производить значительные накладные расходы и многие базы данных предлагают способы их сжатия для сохранения пространства. Очевидный компромисс здесь состоит между скоростью доступа и соотношением сжатия: большие значения соотношения сжатия могут улучшать размер данных, позволяя выборку большего объёма данных за один доступ, но может потребовать больше оперативной памяти и циклов ЦПУ для их сжатия и распаковки.

Сжатие может осуществляться на различных уровнях гранулярности. Даже хотя сжатие файлов целиком может выдавать наилучшие соотношения сжатия, оно ограничивает приложение, так как приходится распаковывать весь файл целиком при неком обновлении, а более гранулированное сжатие обычно лучше подходит для наборов данных большего размера. Сжатие всего индексного файла целиком как непрактично, так и тяжело реализовать действенным образом: для поиска определённой страницы приходится выполнять доступ (чтобы определить место в сжатом разделе) весь файл целиком (либо его раздел, содержащий сжатые метаданные), выполнить распаковку и сделать его доступным.

В качестве альтернативы выступает постраничное сжатие. Это хорошо подходит для нашего обсуждения, ибо те алгоритмы, которые мы до сих пор рассматривали, используют страницы с фиксированным размером. Страницы могут сжиматься и распаковываться независимо друг от друга, позволяя нам сочетать сжатие с загрузкой и сбросом страниц. Однако, некая сжатая страница в таком случае может занимать лишь часть некого дискового блока и, поскольку обмены обычно выполняются элементами дисковых блоков, может потребоваться размещать на странице дополнительные байты [RAY95]. На Рисунке 4-10 вы можете видеть некую сжатую страницу (a), занимающую пространство меньшее дискового блока. При загрузке этой страницы мы захватываем ею также некие дополнительные байты, которые относятся к другой странице. при страницах, которые охватывают множество дисковых блоков, как в случае на том же самом изображении, нам приходится считывать некий дополнительный блок.

Рисунок 4-10

Сжатие и набивка блока

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

Большинство баз данных с открытым исходным кодом, рассмотренных при написании этой книги, обладали подключаемыми методами сжатия с применением доступных библиотек, таких как Snappy, zLib, lz4 и многие иные.

Поскольку алгоритмы сжатия дают различные результаты в зависимости от набора данных и потенциальных обстоятельств (т.е. соотношения сжатия, производительности или накладных расходов по памяти), мы не будем вдаваться в этой книге в подробности сравнения и реализации. Существует доступными множество обзоров, которые оценивают разнообразные алгоритмы сжатия для различных размеров блоков (например, Squash Compression Benchmark), обычно сосредоточенных на четырёх измерениях: накладных расходах памяти, производительности сжатия, производительности распаковки и соотношении сжатия. Эти измерения важны при рассмотрении выбора библиотеки сжатия.

Уборка и сопровождение

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

Описанная архитектура перекидных страниц (см. раздел Перекидные страницы) требует осуществления поддержки страниц для поддержки их в хорошей форме. Например, последовательные расщепления и слияния во внутренних узлах, либо вставки, обновления и удаления на уровне листьев могут иметь результатом то, что некая страница имеет достаточное логическое пространство, но не обладает достаточным непрерывным пространством по причине своей фрагментарности. Рисунок 4-11 отображает пример такого случая: эта страница всё ещё обладает доступным некое логическое пространство, однако оно фрагментировано и расщеплено между двумя удалёнными (мусорными) записями и неким остающимся свободным пространством между заголовком/ указателями ячеек и ячейками.

Рисунок 4-11

Образец фрагментированной страницы

Навигация по B- деревьям выполняется с уровня его корня. Записи данных, которые можно достичь, следуя вниз по указателям из имеющегося коневого узла являются живыми (имеющими адреса), Записи данных без адресации именуются мусором: эти записи не имеют ссылок и не могут быть считаны или интерпретированы, а потому их содержимое настолько же полезно как заполненное нулями.

Вы можете наблюдать на Рисунке 4-11 эту разницу: те ячейки, которые всё ещё обладают сылающиеся на них указатели обладают адресами, в отличии от удалённых или перезаписанных. По соображениям производительности зачастую опускается заполнение нулями областей мусора, так как в конечном счёте эти области в любом случае перекрываются новыми данными.

Вызванная обновлениями и удалениями фрагментация

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

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

[Замечание]
Замечание

Некоторые базы данных полагаются на сборку мусора и оставляют удалённые и обновлённые ячейки на месте для контроля одновременности множества версий (см. раздел Управление одновременностью множества версий). Ячейки остаются доступными для одновременно исполняющихся транзакций пока не завершится данное обновление и могут быть собраны как только никакой иной поток не имеет к ним доступа. Некоторые базы данных поддерживают отслеживающие такие записи приведения (ghost) структуры, которые собираются как только завершены все транзакции, которые могли их просматривать [DECANDIA07].

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

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

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

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

Дефрагментация страниц

Тот процесс, который заботится о возврате пространства и перезаписи страниц имеет название уплотнения (compaction), уборки (vacuum) или просто сопровождения. Перезаписи страниц могут выполняться синхронно с записью когда такая страница не обладает достаточным свободным физическим пространством (во избежание создания не нужных страниц переполнения), однако уплотнение чаще применяется для обозначения некого отличающегося, асинхронного, процесса обхода страниц с выполнением сборки мусора и переписываением их содержимого.

Такой процесс возвращает то пространство, которое занято убитыми ячейками и переписывает ячейки в их логическом порядке. При перезаписи страниц также может потребоваться новое положение в самом файле. Не используемые в памяти страницы становятся доступными и возвращаются в страничный кэш. Идентификаторы таких вновь доступных страниц на диске добавляются в список свободных страниц (порой именуемый свободным списком - freelist - например, SQLite поддерживает список страниц, которые ни применяются в его базе данных, в то время как соединительные - trunk - страницы удерживаются в неком связанном списке и содержат адреса высвобожденных страниц). Эти сведения обязаны быть постоянными для выживания при крушениях и повторных стартах и чтобы иметь уверенность что свободное пространство не потеряно и не утекает.

Выводы

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

Заголовок страницы

Какие сведения обычно хранятся в нём.

Крайние правые указатели

То, что они не являются парой для ключей разделителей, а также как их обрабатывать.

Верхние ключи

Определяют максимальное значение ключей, которые допускается хранить в данном узле.

Страницы переполнения

Позволяют вам хранить излишки и записи переменной длины с применением страниц фиксированного размера.

После этого мы прошлись по некоторым подробностям, относящимся к проходам от корня к листу:

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

Как отслеживать иерархии дерева пользуясь родительскими указателями или хлебными крошками

Наконец, мы прошлись по некоторым технологиям оптимизации и сопровождения:

Повторная балансировка

Перемещает элементы между соседними узлами для снижения числа расщеплений и слияний.

Исключительно крайние правые добавления в конец

Добавляют в конец новую самую правую ячейку вместо её расщепления в предположении что она скоро заполнится.

Пакетная загрузка

Технология для эффективного построения B- деревьев с нуля из сортированных данных.

Сборка мусора

Некий процесс, который перезаписывает страницы, помещая ячейки в порядке ключей и возвращая пространство, занимаемое ячейками без адресации.

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

[Замечание]
Последующее чтение

Если вы желаете узнать больше относительно упоминавшихся в этой главе понятий, вы можете ссылаться на следующие источники:

B- деревья на основе дисков

Graefe, Goetz. 2011. “Modern B-Tree Techniques.” Foundations and Trends in Databases 3, no. 4 (April): 203-402. https://doi.org/10.1561/1900000028.

Healey, Christopher G. 2016. Disk-Based Algorithms for Big Data (1st Ed.). Boca Raton: CRC Press. {Прим. пер.: https://www.crcpress.com/Disk-Based-Algorithms-for-Big-Data/Healey/p/book/9781138196186 } .

Глава 10. Выбор лидера

Содержание

Глава 10. Выбор лидера

Алгоритм задиры

Отказоустойчивый следующий- в- строю

Оптимизация соискатель/ ординарный

Алгоритм приглашения

Алгоритм кольца

Выводы

Синхронизация может стоить достаточно дорого: когда каждый шаг алгоритма вовлечён в контакт с каждым иным участником, мы можем завершить значительными накладными расходами взаимодействия. Это в особенности справедливо для крупных и географически распределённых сетевых сред. Для снижения накладных расходов синхронизации и общего числа путешествующих из конца в конец сообщений для достижения некого решения, некоторые алгоритмы полагаются на наличии некого процесса лидера (порой именуемого координатором), ответственного за исполнение или координацию этапов некого распределённого алгоритма.

Как правило, процессы в распределённой системе единообразны и любой процесс может взять на себя роль такого лидера. Процессы предполагают лидерство на протяжении длительного времени, но это не постоянная роль. Как правило, такой процесс остаётся лидером до своего крушения. После такого крушения любой иной процесс способен запустить новый раунд выборов, предложить лидерство и, если он будет избран, продолжить работу отказавшего лидера.

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

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

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

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

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

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

Наличие некого стабильного лидера в рассматриваемой системе помогает избегать состояние синхронизации между удалёнными участниками, снижает общее число обмена сообщениями и направление исполнения с какого- то отдельного процесса вместо требования координации одноранговой сети. Одна из основных потенциальных проблем в системах с представлением о лидерстве состоит в том, что такой лидер способен становиться узким местом. Для преодоления этого многие системы разделяют данные на разделы в не пересекающиеся независимые наборы реплик (см. раздел Создание разделов баз данных). Вместо наличия отдельного лидера по всей системе, каждый набор реплик обладает своего собственного лидера. Одна из тех систем, которые применяют такой подход, является Spinner (см. раздел Распределённые транзакции при помощи Spanner).

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

Некоторые алгоритмы, например, ZAB (см. раздел ZAB), Multi-Paxos (Multi-Paxos) или (Raft) применяют временных лидеров для снижения общего числа сообщений необходимых для достижения некого согласия между имеющимися участниками. Тем не менее, эти алгоритмы применяют свои собственные особенные для алгоритма средства для выбора лидера, выявления отказа и разрешения конфликтов между соперничающими за лидерство процессами.

Алгоритм задиры

Один из алгоритмов выбора лидера, именуемый как алгоритм задиры (bully algorithm), ипользует ранги процессов для выявления своего нового лидера. Каждый процесс получает некий уникальный назначаемый ему ранг. В процессе проведения выборов процесс с наивысшим значением ранга становится лидером [MOLINA82].

Этот алгоритм стал известным благодаря своей простоте. Данный алгоритм носит название задиры по той причине, что узел с наивысшим значением ранга "быкует" прочие узлы чтобы они приняли его в качестве лидера. Он также известен под названием монархического выбора лидера: монархом становится собрат с наивысшим рейтингом после того как прежний перестаёт существовать.

Выборы запускаются когда один из имеющихся процессов замечает что в его системе отсутствует лидер (он не был инициализирован) или предыдущий лидер перестал отвечать на запросы и выполняется в три этапа (данные шаги описывают видоизменённый алгоритм выборов задиры [KORDAFSHARI05], поскольку он более компактный и ясный):

Этот процесс отправляет сообщение о выборах процессам с более высокими идентификаторами.

Данный процесс выполняет ожидание, позволяя откликнуться процессам с более высоким рангом. Если ни один из процессов с более высоким рангом не отвечает, он продолжает с шага 3. В противном случае этот процесс отмечает ответивший ему процесс с наивысшим рангом и позволяет этому процессу выполнять шаг 3.

Процесс этого шага полагает, что больше нет активных процессов с более высоким рангом и уведомляет все процессы с более низким рангом о своём лидерстве.

Рисунок 10-1 иллюстрирует обсуждённый алгоритм задиры выборов лидера:

Процесс 3 замечает, что его предыдущий лидер 6 претерпел крушение и запустил новые выборы отправив сообщение Election (выборы) процессам с более высокими значениями идентификаторов.

4 и 5 отвечают Alive (живём), поскольку у них более высокий ранг чем у 3.

3 обнаруживает, что в процессе этого раунда откликнулся процесс 5 с наивысшим рангом.

5 выбирается в качестве нового лидера. Он широковещательно рассылает сообщения Elected (избран), уведомляя процессы с более низким рангом о результатах этих выборов.

Рисунок 10-1

Алгоритм задиры: предыдущий лидер (6) отказал и процесс 3 запустил новые выборы

Одной из очевидных проблем для данного алгоритма является то, что он нарушает необходимые гарантии безопасности (относительно того, что за раз может быть выбран лишь один лидер) пр наличии разбиения сетевой среды на разделы. Это развитие событий может запросто завершиться ситуацией, при которой узлы разбиваются на два или более независимо работающих подмножества и каждый из наборов выбирает своего собственного лидера. Такое положение дел носит название расщепления сознания.

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

Отказоустойчивый следующий- в- строю

Существует множество вариаций обсуждённого алгоритма задиры, которые улучшают его различные свойства. К примеру, мы можем воспользоваться альтернативными следующими- в- строю процессами в качестве отказоустойчивости для скорейших повторных выборов [GHOLIPOUR09].

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

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

Рисунок 10-2 показывает данный процесс с данной оптимизацией на своём месте:

6 является лидером с обозначенными альтернативами {5,4}, претерпевает крушение, 3 отмечает этот отказ и контактирует с 5, альтернативой из имеющегося списка с наивысшим рангом.

5 отвечает 3 что он работает, чтобы предотвратить тому контакты с прочими узлами из имеющегося перечня альтернатив.

5 уведомляет все прочие узлы что он новый лидер.

Рисунок 10-2

Алгоритм задиры с предпочтением: предыдущий лидер (6) отказал и процесс 3 запустил новые выборы, контактируя с имеющейся альтернативой наивысшего ранга

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

Оптимизация соискатель/ ординарный

Другой алгоритм пытается снизить требования в общем числе сообщений расщепляя все узлы на два подмножества: соискателей и обычные. в которых лишь один из имеющихся узлов кандидатов может в конечном итоге становиться лидером [MURSHED12].

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

Для решения проблемы с множеством одновременных выборов данный алгоритм предлагает пользоваться переменной δ схемы разрешения конфликтов, некой конкретной для процесса задержкой, значительно меняющейся между имеющимися узлами, которая позволяет одному из существующих узлов инициировать необходимые выборы до того как этим займутся прочие. Значение времени схемы разрешения конфликтов обычно больше времени прохода сообщения в оба конца. Узлы с большими приоритетами обладают более низкими δ, и наоборот.

Рисунок 10-3 отображает установленные этапы данного процесса выборов:

Процесс 4 из множества обычных отмечает отказ процесса лидера 6. Он запускает новый раунд выборов контактируя с остающимися в своём наборе кандидатами.

Процессы кандидатов откликаются 4 указывая что они в рабочем состоянии.

4 уведомляет все процессы о новом лидере: 2.

Рисунок 10-3

Модификация кандидат/ обычный алгоритм задиры: предыдущий лидер (6) отказал и процесс 4 запустил новые выборы

Алгоритм приглашения

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

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

Рисунок 10-4 отображает шаги, исполняемые этим алгоритмом приглашения:

Четыре процесса начинают как лидеры групп, каждая из которых содержит единственного участника. 1 приглашает 2 соединиться в группу, а 3 зазывает в объединённую группу 4.

2 соединяется в группу с процессом 1, а 4 объединяется в группу с процессом 3. 1, лидер своей первой группы, выходит на контакт с 3, лидером другой группы. Остающиеся участники группы (в данном случае 4) уведомляются о новом лидере группы.

Две группы сливаются и 1 становится лидером некой расширенной группы.

Рисунок 10-4

Алгоритм приглашения

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

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

Алгоритм кольца

В данном алгоритме кольца [CHANG79] все узлы рассматриваемой системы формируют некое кольцо и осведомлены об установленной топологии кольца (то есть о своих предшественниках и последователях в этом кольце). Когда определённый процесс замечает отказ имевшегося лидера, он запускает соответствующие новые выборы. Такое сообщение о выборах пускается далее по этому кольцу: каждый процесс вступает в контакт со своим последователем (следующим ближайшим к нему в кольце узлом). Когда такой узел недоступен, процесс пропускает недостижимый узел и пытается вступить в контакт со следующим после него узлом в кольце, до тех пор пока один из них не ответит.

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

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

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

3 запускает процесс обхода раунда выборов. На каждом этапе имеется некий набор обойдённых узлов по установленному пути к этому моменту. 5 не способен достичь 6, поэтому он пропускает его и переходит сразу к 1.

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

Рисунок 10-5

Алгоритм кольца: предыдущий лидер (6) отказал и процесс 3 запустил новые выборы

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

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

Как вы можете видеть, для правильной работы систем с лидером нам необходимо знать состояние текущего лидера (жив он или нет), так как для продолжения поддержки организации процессов и самого исполнения такой лидер должен быть рабочим и и достижим чтобы исполнять свои обязанности. Для выявления крушения лидера мы можем применять алгоритмы выявления отказов (см. Главу 9).

Выводы

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

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

Многие алгоритмы консенсуса, включая Multi-Paxos и Raft, полагаются на некого лидера для координации. Тем не менее, являются ли выборы лидера тем же самым что и консенсус? Для выборов некого лидера нам требуется достичь согласия о его идентичности. Если мы способны достичь консенсуса относительно идентичности лидера, мы можем применять то же самое средство для достижения согласия по чему угодно [ABRAHAM13].

Значение идентичности некого лидера может изменяться без знания этого процессами, потому будет справедлив также и вопрос о том знает ли о своём лидере локально процесс. К примеру, алгоритм стабильного выбора лидера (stable leader election) использует раунды с уникальным стабильным лидером и выявление отказов на основании таймаутов для обеспечения того, что это лидер может оставаться в своём положении пока он не испытает крушения и является доступным [AGUILERA01].

Полагающиеся на выборы лидера алгоритмы часто допускают наличие присутствия множества лидеров и пытаются разрешать конфликты между этими лидерами настолько быстро, насколько это возможно. Например, это справедливо для Multi-Paxos (см. раздел Multi-Paxos), в котором способен продолжать лишь один из двух конфликтующих лидеров (доверенное лицо, proposers), а эти конфликты разрешаются сбором некого вторичного кворума, что обеспечивает тот факт, что не будут приняты значения от двух различных доверенных лиц.

В Raft (см. раздел Raft) может обнаружить, что его звание устарело, что подразумевает присутствие в этой системе другого лидера, и ему придётся обновлять это звание на более позднее.

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

Мы обсудим более подробно консенсус и выборы лидера в контексте консенсуса в Главе 14.

[Замечание]
Последующее чтение

Если вы желаете узнать больше относительно упоминавшихся в этой главе понятий, вы можете ссылаться на следующие источники:

Алгоритмы выбора лидера

Lynch, Nancy and Boaz Patt-Shamir. 1993. “Distributed algorithms.” in Lecture notes for 6.852, Cambridge, MA: MIT.

Attiya, Hagit and Jennifer Welch. 2004. Distributed Computing: Fundamentals, Simulations and Advanced Topics, USA: John Wiley & Sons.

Tanenbaum, Andrew S. and Maarten van Steen. 2006. Distributed Systems: Principles and Paradigms, (2nd Ed.). Upper Saddle River, NJ: Prentice-Hall.

Указатель

Содержание

Указатель