/
Автор: Марц Н. Уоррен Дж.
Теги: издательский дом вильямс парадигма масштабирование модель данных система обработки
Год: 2016
Текст
Принципы
и практика построении
масштабируемых систем
обработки ранник
I! реальном бремени
Натан Марії
и Джеймс Уоррен
ЕН
Большие данные
Принципы и практика построения
масштабируемых систем обработки
данных в реальном времени
Большие данные
Принципы и практика
построения масштабируемых
систем обработки данных
в реальном времени
Натан Марц
Джеймс Уоррен
Издательский дом "Вильямс"
Москва • Санкт-Петербург • Киев
2016
Оглавление
Благодарности 18
Об этой книге 21
Глава 1. Новая парадигма для больших данных 25
ЧАСТЬ I. УРОВЕНЬ ПАКЕТНОЙ ОБРАБОТКИ 53
Гпава 2. Модель данных для больших данных 55
Глава 3. Иллюстрация модели данных для больших данных 77
Глава 4. Хранение данных на уровне пакетной обработки 85
Глава 5. Иллюстрация хранения данных
на уровне пакетной обработки 99
Глава 6. Уровень пакетной обработки 119
Глава 7. Иллюстрация уровня пакетной обработки 151
Глава 8. Пример построения уровня пакетной обработки:
архитектура и алгоритмы 181
Глава 9. Пример реализации уровня пакетной обработки 199
ЧАСТЬ II. УРОВЕНЬ ОБСЛУЖИВАНИЯ 221
Глава 10. Организация уровня обслуживания 223
Глава 11. Иллюстрация уровня обслуживания 243
ЧАСТЬ III. УРОВЕНЬ УСКОРЕНИЯ 253
Глава 12. Представления в реальном времени 255
Глава 13. Иллюстрация представлений в реальном времени 271
Глава 14. Организация очередей и обработка потоков 277
Глава 15. Иллюстрация организации очередей
и обработки потоков 297
Глава 16. Микропакетная обработка потоков 309
Глава 17. Иллюстрация микропакетной обработки потоков 327
Глава 18. Лямбда-архитектура в деталях 343
Предметный указатель 363
Содержание
Предисловие 15
Благодарности 18
Об этой книге 21
Краткое содержание книги 21
Загружаемый код и условные обозначения 22
Об иллюстрации на обложке книги 22
От издательства 23
Глава 1. Новая парадигма для больших данных 25
1.1. Структура книги 26
1.2. Масштабирование в традиционных базах данных 27
1.2.1. Масштабирование с помощью очереди 28
1.2.2. Масштабирование путем фрагментации базы данных 28
1.2.3. Проявление вопросов отказоустойчивости 29
1.2.4. Проявление вопросов искажения данных 30
1.2.5. Что пошло не так 30
1.2.6. Чем могут помочь технологии организации больших данных 30
1.3. Базы данных типа N080,1, не являются панацеей 31
1.4. Основные принципы 31
1.5. Желательные свойства системы больших данных 32
1.5.1. Надежность и отказоустойчивость 33
1.5.2. Малые задержки чтения и записи данных 33
1.5.3. Масштабируемость 33
1.5.4. Обобщение 33
1.5.5. Расширяемость 34
1.5.6. Запросы с произвольным доступом 34
1.5.7. Минимальное сопровождение 34
1.5.8. Отлаживаемость 35
1.6. Недостатки полностью инкрементных архитектур
1.6.1. Эксплуатационная сложность
35
36
Содержание
7
1.6.2. Крайняя сложность достижения окончательной
согласованности 36
1.6.3. Неустойчивость к ошибкам, связанным
с человеческим фактором 38
1.6.4. Полностью инкрементная архитектура
в сравнении с лямбда-архитектурой 39
1.7. Лямбда-архитектура 40
1.7.1. Уровень пакетной обработки 42
1.7.2. Уровень обслуживания 43
1.7.3. Уровни пакетной обработки и обслуживания обеспечивают
почти все свойства информационной системы 44
1.7.4. Уровень укорения 45
1.8. Современные тенденции в развитии технологий 48
1.8.1. Быстродействие ЦП не повышается 48
1.8.2. Эластичные облака 49
1.8.3. Эффектная экосистема с открытым кодом
для больших данных 49
1.9. Пример приложения SuperWebAnalytics.com 50
Резюме 51
ЧАСТЬ I. УРОВЕНЬ ПАКЕТНОЙ ОБРАБОТКИ 53
Глава 2. Модель данных для больших данных 55
2.1. Свойства данных 57
2.1.1. Необработанность данных 59
2.1.2. Неизменяемость данных 63
2.1.3. Вечная истинность данных 65
2.2. Модель, основанная на фактах, для представления данных 66
2.2.1. Примеры фактов и их свойства 67
2.2.2. Преимущества модели, основанной на фактах 69
2.3. Граф-схемы 72
2.3.1. Элементы граф-схемы 73
2.3.2. Потребность в осуществимой схеме 74
2.4. Полная модель данных для приложения SuperWebAnalytics.com 75
Резюме ^
Глава 3. Иллюстрация модели данных
для больших данных 77
3.1 Причины применения каркаса сериализации 77
3.2. Каркас сериализации Apache Thrift 78
8
Содержание
3.2.1. Узлы
3.2.2. Ребра
3.2.3. Свойства
3.2.4. Связывание всего вместе в объекты данных
3.2.5. Развитие осуществимой схемы
3.3. Ограничения, присущие каркасам сериализации
Резюме
Глава 4. Хранение данных на уровне пакетной обработки 85
4.1. Требования к хранению главного массива данных 86
4.2. Выбор решения для хранения данных
на уровне пакетной обработки 87
4.2.1. Применение хранилища пар “ключ-значение”
для главного массива данных 88
4.2.2. Распределенные файловые системы 88
4.3. Принцип действия распределенных файловых систем 89
4.4. Сохранение главного массива данных
в распределенной файловой системе 91
4.5. Вертикальное разделение 93
4.6. Низкоуровневый характер распределенных файловых систем 94
4.7. Хранение главного массива данных из приложения
SuperWebAnalytics.com в распределенной файловой системе 96
Резюме 97
79
79
80
81
81
83
84
Глава 5. Иллюстрация хранения данных
на уровне пакетной обработки
5.1. Применение распределенной файловой системы HDFS
5.1.1. Недостатки небольших файлов
5.1.2. Переход на более высокий уровень абстракции
5.2. Хранение данных на уровне пакетной обработки
с помощью библиотеки Pail
5.2.1. Основные операции в Pail
5.2.2. Сериализация объектов в “ведрах”
5.2.3. Выполнение пакетных операций средствами Pail
5.2.4. Вертикальное разделение средствами Pail
5.2.5. Форматы и уплотнение “ведерных” файлов
5.2.6. Преимущества библиотеки Pail
5.3. Хранение главного массива данных для приложения
SuperWebAnalytics.com
5.3.1. Структурированное “ведро” для храпения
объектов Apache Thrift
99
100
101
102
103
104
105
106
107
108
109
ПО
112
Содержание
9
d. .2. С сновное “ведро” для приложения SuperWebAnalytics.com 113
.3.3. Расчлененное “ведро” для вертикального
разделения массива данных 113
Резюме jig
Глава 6. Уровень пакетной обработки 119
6.1. Мотивирующие примеры 120
6.1.1. Количество просмотров страницы во времени 120
6.1.2. Заключение о гендерной принадлежности 121
6.1.3. Фактор влияния 121
6.2. Вычисления на уровне пакетной обработки 122
6.3. Сравнение алгоритмов повторных
и инкрементных вычислений 124
6.3.1. Производительность 126
6.3.2. Устойчивость к отказам, обусловленным
человеческим фактором 127
6.3.3. Универсальность алгоритмов 128
6.3.4. Выбор разновидности алгоритма 128
6.4. Масштабируемость на уровне пакетной обработки 129
6.5. MapReduce — парадигма распределенных вычислений
для больших данных 130
6.5.1. Масштабируемость 131
6.5.2. Отказоустойчивость 134
6.5.3. Универсальность MapReduce 134
6.6. Низкоуровневый характер MapReduce 137
6.6.1. Многоэтапные вычисления неестественны 137
6.6.2. Соединения очень трудно реализуются вручную 137
6.6.3. Логическое исполнение тесно связано с физическим 139
6.7. Конвейерные схемы для рассмотрения пакетных
вычислений на более высоком уровне 140
6.7.1. Принципы построения конвейерных схем 140
6.7.2. Выполнение конвейерных схем средствами MapReduce 145
6.7.3. Объединяющие агрегаторы 147
6.7.4. Примеры конвейерных схем 147
Резюме 149
Глава 7. Иллюстрация уровня пакетной обработки 151
7.1. Иллюстративный пример 152
7.2. Типичные ограничения, скрывающиеся
в инструментальных средствах обработки данных 154
7.2.1 Специальные языки программирования 154
7.2.2. Неудачно составляемые абстракции 155
10
Содержание
7.3. Введение в^аясак^ 156
7.3.1. Модель данньп^Савсак^ 156
7.3.2. Структура запросов в^авсак^ 157
7.3.3. Запрашивание нескольких массивов данных 159
7.3.4. Группировка и агрегирование 162
7.3.5. Пошаговое выполнение примера запроса 163
7.3.6. Специальные предикатные операции 166
7.4. Композиция 171
7.4.1. Объединение подзапросов 171
7.4.2. Динамическое создание подзапросов 172
7.4.3. Предикатные макрокоманды 175
7.4.4. Динамическое создание предикатов макрокоманд 177
Резюме 179
Глава 8. Пример построения уровня пакетной обработки:
архитектура и алгоритмы 181
8.1. Разработка уровня пакетной обработки приложения
SuperWebAnalytics.com 182
8.1.1. Поддерживаемые запросы 182
8.1.2. Пакетные представления 183
8.2. Краткий обзор процесса пакетной обработки данных 186
8.3. Ввод данных 188
8.4. Нормализация иКЬ 188
8.5. Нормализация идентификаторов пользователей 189
8.6. Удаление дубликатов событий просмотра страниц 194
8.7. Вычисление пакетных представлений 194
8.7.1. Количество просмотров страниц во времени 194
8.7.2. Подсчет индивидуальных посетителей страниц во времени 196
8.7.3. Анализ показателя отказов от просмотра 196
Резюме 198
Глава 9. Пример реализации уровня пакетной обработки 199
9.1. Отправная точка 200
9.2. Подготовка процесса пакетной обработки данных 200
9.3. Ввод новых данных 201
9.4. Нормализация иКЬ 205
9.5. Нормализация идентификаторов пользователей 206
9.6. Исключение дубликатов событий просмотра страниц 212
Содержание.
11
9.7. Вычисление пакетных представлений 212
9.7.1. Количество просмотров страниц во времени 212
9.7.2. Количество индивидуальных посетителей во времени 215
9.7.3. Анализа показателя отказов от просмотра 217
Резюме 219
ЧАСТЬ II. УРОВЕНЬ ОБСЛУЖИВАНИЯ 221
Глава 10. Организация уровня обслуживания 223
10.1. Количественные показатели производительности
на уровне обслуживания 225
10.2. Уровень обслуживания как решение проблемы выбора
между нормализацией или денормализацией данных 228
10.3. Требования к базе данных на уровне обслуживания 229
10.4. Проектирование уровня обслуживания для приложения
SuperWebAnalytics.com 231
10.4.1. Количество просмотров страниц во времени 232
10.4.2. Количество индивидуальных посетителей
страниц во времени 232
10.4.3. Анализ показателя отказов от просмотра страниц 233
10.5. Сравнение лямбда-архитектуры с полностью
инкрементным решением 234
10.5.1. Полностью инкрементное решение задачи подсчета
количества индивидуальных посетителей
страниц во времени 234
10.5.2. Сравнение с решением на основе лямбда-архитектуры 241
Резюме 242
Глава 11. Иллюстрация уровня обслуживания
11.1. Основные положения о базе данных Е1ерЬапсПВ
11.1.1. Создание представления в базе данных ЕкрйатВВ
11.1.2. Обслуживание представления в базе данных Е1ерЬатБВ
11.1.3. Применение базы данных ЕкрЬашБВ
11.2. Построение уровня обслуживания для приложения
SuperWebAnalytics.com
11.2.1. Просмотры страниц во времени
11.2.2. Индивидуальные посещения страниц во времени
11.2.3. Анализ показателя отказов от просмотра страниц
Резюме
243
244
244
245
246
248
248
250
251
252
12
Содержание
ЧАСТЬ III. УРОВЕНЬ УСКОРЕНИЯ
Глава 12. Представления в реальном времени
12.1. Вычисление представлений в реальном времени
12.2. Хранение представлений в реальном времени
12.2.1. Достижимая точность
12.2.2. Сохранение состояния на уровне ускорения
12.3. Трудности инкрементных вычислений
12.3.1. Достоверность теоремы САР
12.3.2. Сложная взаимосвязь теоремы САР с алгоритмами
инкрементных вычислений
12.4. Асинхронные обновления в сравнении с синхронными
12.5. Истечение срока действия представлений
в реальном времени
Резюме
Глава 13. Иллюстрация представлений
в реальном времени
13.1. Модель данных базы данных Cassandra
13.2. Применение базы данных Cassandra
13.2.1. Расширенные возможности базы данных Cassandra
Резюме
Глава 14. Организация очередей
и обработка потоков
14.1. Организация очередей
14.1.1. Серверы однопользовательских очередей
14.1.2. Многопользовательские очереди
14.2. Обработка потоков
14.2.1. Очереди и рабочие процессы
14.2.2. Препятствия, скрытые в модели очередей
и рабочих процессов
14.3. Высокоуровневая обработка потоков
14.3.1. Модель Storm
14.3.2. Гарантирование обработки сообщений
14.4. Уровень ускорения приложения SuperWebAnalytics.com
14.4.1. Структура топологии
Резюме
253
255
257
258
259
260
261
261
263
265
267
269
271
271
273
275
276
277
278
278
280
281
282
283
284
284
289
291
293
294
Содержание
13
Глава 15. Иллюстрация организации очередей
и обработки потоков 297
15.1. Составление топологий средствами Apache Storm 297
15.2. Кластеры Apache Storm и развертывание топологии 300
15.3. Гарантирование обработки сообщений 302
15.4. Реализация подсчета индивидуальных посещений
страниц во времени на уровне ускорения приложения
SuperWebAnalytics.com 304
Резюме 308
Глава 16. Микропакетная обработка потоков
16.1. Достижение семантики “только однажды”
16.1.1. Строго упорядоченная обработка
16.1.2. Микропакетная обработка потоков
16.1.3. Топологии микропакетной обработки потоков
16.2. Основные понятия микропакетной
обработки потоков
16.3. Расширение конвейерных схем для микропакетной
обработки потоков
16.4. Завершение построения уровня ускорения
в приложении SuperWebAnalylics.com
16.4.1. Просмотры страниц во времени
16.4.2. Анализ показателя отказов от просмотра страниц
16.5. Другой подход к анализу показателя отказов
от просмотра страниц
Резюме
Глава 17. Иллюстрация микропакетной
обработки потоков
17.1. Применение Trident
17.2. Завершение построения уровня ускорения
в приложении SuperWebAnalytics.com
17.2.1. Просмотры страниц во времени
17.2.2. Анализ показателя отказов от просмотра страниц
17.3. Полностью отказоустойчивая микропакетная
обработка с сохранением состояния в памяти
Резюме
309
310
310
311
312
315
316
318
318
319
324
325
327
328
331
331
334
340
342
14
Содержание
Глава 18. Лямбда-архитектура в деталях 343
18.1. Определение информационных систем 343
18.2. Уровни пакетной обработки и обслуживания 345.
18.2.1. Инкрементная пакетная обработка 346
18.2.2. Измерение и оптимизация использования
ресурсов на уровне пакетной обработки 353
18.3. Уровень ускорения 359
18.4. Уровень запросов 359
Резюме 361
Предметный указатель 363
Предисловие
огда я впервые окунулся в область действия больших данных, то почувство¬
вал себя, как на Диком Западе в отношении разработки программного обеспе¬
чения. Многие разработчики переходили от реляционных баз данных с их при¬
вычными удобствами к нереляционным базам данных типа Ыо8(^Ь с их ограни¬
ченными моделями данных, предназначенными для распределения по тысячам
машин. С тех пор число баз данных типа ЫоБЦЬ заметно возросло, а их отличия
стали едва различимы. Затем возник новый многообещающий проект под на¬
званием На^ор, дававший возможность выполнять глубокий анализ огромных
объемов данных. И не так-то просто было разобраться, как рационально пользо¬
ваться всеми этими новыми инструментальными средствами.
В то время я пытался разрешать затруднения, которые возникали в связи
с масштабированием информационных систем в тех организациях, где мне при¬
ходилось работать. Архитектура этих систем была угрожающе сложна, она состо¬
яла из целой паутины совместно используемых реляционных баз данных, очере¬
дей, рабочих, главных и подчиненных компонентов. Информация в таких базах
данных была подвержена искажению, и для противодействия этому явлению
в приложениях существовал специальный код. И в этом отношении ведомые
компоненты системы всегда отставали. Поэтому я решил изучить альтернатив¬
ные технологии больших данных, чтобы найти более совершенное проектное
решение для имевшейся архитектуры информационной системы.
Один печальный опыт на ранней стадии моей карьеры разработчика про¬
граммного обеспечения глубоко сформировал мое представление о том, как сле¬
дует строить архитектуру информационных систем. Один из моих сотрудников
потратил несколько недель на сбор данных из Интернета в совместно использу¬
емой файловой системе. Он предполагал собрать достаточный объем данных,
чтобы проанализировать их. Однажды, выполняя плановое сопровождение си¬
стемы, я случайно стер все данные своего сотрудника, сведя на нет все его мно¬
гонедельные труды.
Я, конечно, понимал, что совершил непоправимую ошибку, но как начинаю¬
щий разработчик программного обеспечения еще не осознавал всех ее послед¬
ствий. Следовало ли меня уволить за подобную халатность? Я отправил по элек¬
тронной почте сообщение команде разработчиков, принеся в нем глубочайшие
извинения за свою оплошность, и, к большому удивлению, все члены команды
отнеслись ко мне с большим сочувствием. Я никогда нс забуду, как мой сотрудник
подошел к моему рабочему столу, похлопал меня по спине и сказал: “Поздравляю.
Теперь ты стал профессиональным разработчиком программного обеспечения”.
Предисловие
В его шутливом заявлении скрывалась следующая прописная истина разработ¬
ки программного обеспечения: мы не знаем, как создавать идеальное программ¬
ное обеспечение. Ошибки вполне могут проникать в программное обеспечение,
развертываемое для эксплуатации. Если приложение может делать запись в базе
данных, то такая запись может быть сделана ошибочно. И когда я приступил
к переделке архитектуры имевшейся информационной системы, то опирался
на этот опыт. Мне было известно, что новая архитектура должна быть масштаби¬
руемой и устойчивой не только к аппаратным отказам, но и в какой-то степени
к ошибкам, связанным с человеческим фактором.
В ходе переделки архитектуры упомянутой выше системы мне пришлось пой¬
ти по пути переосмысления всего, что мне казалось истинным в отношении баз
данных и управления ими. В итоге я построил архитектуру, основанную на непо¬
стоянных данных и их пакетной обработке. И меня просто поразило, насколько
простой оказалась новая система в сравнении с той, что была основана только
на инкрементной (т.е. пошаговой) обработке данных. Все стало проще, вклю¬
чая операции, новые средства поддержки системы, восстановление после оши¬
бок, связанных с человеческим фактором, и оптимизацию производительности.
Такой подход оказался настолько общим, что его, казалось, можно было бы при¬
менить к любой информационной системе.
Но меня все же кое-что смущало. Проанализировав положение дел в осталь¬
ной отрасли, я обнаружил, что почти никто не работает по аналогичным методи¬
кам. Напротив, архитектуры информационных систем отличались непомерной
сложностью и наличием огромных кластеров баз данных, обновлявшихся в по¬
шаговом режиме. А разработанный мною подход позволял устранить или значи¬
тельно упростить многие сложности в подобных архитектурах.
В течение нескольких последующих лет я расширил свою методику, форма¬
лизовав ее в то, что теперь называется лямбда-архитектурой. Когда я работал
в начинающей компании ВаскТуре, наша команда из пяти разработчиков созда¬
ла программный продукт для анализа социальных сетей, предоставлявший в ре¬
альном времени разнообразные наборы аналитических данных объемом свыше
100 Тбайт. Наша небольшая команда вела также развертывание, эксплуатацию
и мониторинг на кластере системы, состоящей из сотней машин. Когда мы про¬
демонстрировали свой продукт пользователям, их поразило, что он был создан
всего пятью людьми. Они часто задавали вопрос: “Как столь малым числом лю¬
дей можно было сделать так много?” Мой ответ был прост: “Главное — не то, что
мы делаем, а то, что мы не делаем”. С помощью лямбда-архитектуры нам удалось
избежать сложностей, которыми страдали традиционные архитектуры. Избежав
этих сложностей, мы сумели значительно повысить производительность своего
труда.
Движение, называемое большими данными, только усугубило сложности, деся¬
тилетиями существовавшие в архитектурах данных. Любая архитектура, осно¬
ванная в основном на крупных постепенно обновляемых базах данных, будет
страдать подобными сложностями, вызывая программные ошибки, тяжеловес¬
ные операции и снижение производительности. И хотя базы данных типа SQL
и >1о8С)Ь нередко изображаются как совершенно противоположные или ду¬
блирующие друг друга, на самом основополагающем уровне они, по существу,
Предисловие
17
одинаковы. Они стимулируют применение одной и той же архитектуры с ее не¬
избежными сложностями. Сложность — зловредное существо, которое все равно
укусит вас, признаете ли вы его или нет.
Эта книга была написана в результате моего желания поделиться своими
знаниями лямбда-архитектуры и способами избежать сложностей, присущих
традиционным архитектурам информационных систем. Мне бы хотелось иметь
такую книгу под рукой, когда я только начинал работать с большими данными.
Надеюсь, что вы, читатель, отнесетесь к этой книге как к приключению, в ходе
которого вам предстоит пересмотреть то, что вам было до сих пор известно
об информационных системах, а также обнаружить, что с большими массивами
данных можно работать изящно, просто и занимательно.
Натан Марц
Благодарности
Эта книга не появилась бы без помощи и поддержки многих неравнодушных
людей. Начну со своих родителей, которые привили мне с юных лет любовь
к изучению и исследованию окружающего мира. Они всегда поощряли меня во
всех моих карьерных начинаниях.
Мой брат Иорав также поощрял во мне с юных лет интерес к интеллектуаль¬
ному труду. Я до сих пор помню, как он учил меня алгебре, когда я ходил в на¬
чальную школу. Он был одним из тех, кто впервые познакомил меня с програм¬
мированием, обучая меня языку Visual Basic, который он преподавал в средней
школе. Эти уроки зародили во мне страсть к программированию и предопреде¬
лили мою карьеру.
Я безмерно благодарен Майклу Монтано (Michael Montano) и Кристоферу
Голде (Christopher Golda) — основателям компании BackType. С того момента,
как они взяли меня на работу в свою компанию, я получил немалую свободу
в принятии решений. И эта свобода дала мне возможность в полной мере иссле¬
довать и использовать лямбда-архитектуру. Они никогда не ставили под сомне¬
ние ценность открытого кода, позволив мне сделать нашу технологию свободно
доступной для всех желающих воспользоваться ею. Возможность разрабатывать
программное обеспечение с открытым исходным кодом стала одной из главных
привилегий в моей жизни.
Особой признательности заслуживают многие мои преподаватели из
Стендфордского университета. В частности, Тим Рафгарден (Tim Roughgarden) —
самый лучший из моих преподавателей — коренным образом улучшил мои спо¬
собности строго анализировать, разбирать и решать трудные задачи. Пройти
как можно больше курсов, которые он вел, было одним из самых удачных реше¬
ний в моей жизни. Выражаю также признательность Монике Лам (Monica Lam)
за то, что она привила мне способность оценить по достоинству язык Datalog.
Многие годы спустя я сочетал язык Datalog с моделью распределенных вычис¬
лений MapReduce в своем первом значительном проекте Cascalog с открытым
исходным кодом.
Крис Уэнсел (Chris Wensel) первым показал мне, что обработка данных в мас¬
штабе может быть изящной и производительной. Его библиотека Cascading из¬
менила мое отношение к обработке больших данных.
Ни одна их моих работ нс состоялась бы без первопроходцев в области
больших данных. Особая благодарность в этой связи выражается Джеффри
Дину (Jeffrey Dean) и Саиджаю Гсмавату (Sanjay Ghemawat) - за оригиналь¬
ную статью по MapReduce; Джузеппе Декаидиа (Giuseppe DcCandia). Денизу
Благодарности
19
астор у emz astorun), Мадану Джампани (Madan Jampani), Гуиавардхану
акулапати ( unavar han Kakulapati), Авинашу Лакшману (Avinash Lakshman),
ексу илчину ( ex Pilchin), Сваминатану Сивасубраманьяну (Swaminathan
ivasu ramaman), итеру Воссхоллу (Peter Vosshall) и Вернеру Фогельсу (Werner
oge s) за оригинальную статью по Dynamo; а также Майклу Кафарелле (Michael
are а) и Д)оу Каттингу (Doug Cutting) — за основание проекта Apache Hadoop.
ич ки (Rich Hickey) был одним из тех, кто больше всех вдохновлял мои
первые начинания в программировании. Clojure стал одним из самых любимых
моих языков программирования. Изучив его, я стал лучше программировать,
оценив его практичность и простоту. Философский подход Рича к состоянию
и сложности в программировании оказал на меня большое влияние.
Когда я приступал к работе над этой книгой, у меня практически не было
для этого никакого опыта. Поэтому особой благодарности заслуживает Рени
Грегуар (Renae Grégoire), одна из ответственных редакторов в издательстве
Manning Publications, за оказанную помощь в моих литературных начинаниях.
Она прочно вбила мне в голову важность употребления примеров для разъясне¬
ния общих понятий и пролила немало света на эффективные приемы написания
технической литературы. Навыки, которым она меня обучила, можно применять
не только к написанию технической литературы, но и к ведению блогов, дискус¬
сий и общению вообще. И я всегда буду благодарен ей за приобретение столь
важного для меня жизненного опыта.
Качество этой книги оказалось бы несравнимо ниже без усилий моего соавто¬
ра Джеймса Уоррена. Он проделал титаническую работу по разъя|снению теоре¬
тических положений и поиску лучших способов подачи материала. Своей ясно¬
стью эта книга обязана большим навыкам Джеймса в общении.
Мне было приятно работать с сотрудниками издательства Manning Publications.
Они проявили немало терпения и пониманием, что на поиски правильного спо¬
соба подачи материала на столь обширную тему требовалось время. В течение
всей работы над книгой они оказывали мне всяческую помощь и поддержку, пре¬
доставляя необходимые ресурсы. Поэтому хотелось бы поблагодарить Марьяна
Бейса (Marjan Васе) и Майкла Стивенса (Michael Stephens) и всех остальных
сотрудников издательства за то, что они всячески помогали и направляли мою
работу над книгой.
Я стараюсь извлечь для себя как можно больше пользы, изучая труды других
авторов. Особое влияние оказали на меня книги Бредфорда Кросса (Bradford
Cross), Клейтона Кристенсена (Clayton Christensen), Пола Грехэма (Paul Graham),
Карла’Сагана (Cari Sagan) и Дерека Сиверза (Derek Sivers).
И наконец, я не могу не выразить благодарность многим людям, которые про¬
сматривали, комментировали данную книгу и давали отзывы о ней по мере ее
написания. Благодаря ответной реакции этих людей мы, авторы книги, неодно¬
кратно пересматривали, переписывали и перестраивали ее структуру до тех пор,
пока не нашли эффективные способы подачи материала. Особая благодарность
в этой связи выражается Аарону Колкорду (Aaron Colcord), Аарону Кроу (Aaron
Crow) Алексу Холмсу (Alex Holmes), Аруну Джекобу (Arun Jacob), Азифу Джану
(Asif Tan) Айону Синхе (Ayon Sinha), Биллу Грсхэму (Bill Graham), Чарльзу Брофи
(Charles Brophy), Дэвиду Бекуиту (David Beckwith), Деррику Бернсу (Derrick
Благодарности
Bums), Дугласу Дункану (Douglas Duncan), Хьюго Гарца (Hugo Garza), Джейсону
Куркуксу (Jason Courcoux), Джонатану Эстерхази (Jonathan Esterhazy), Карлу
Кунцу (Karl Kuntz), Кевину Мартину (Kevin Martin), Лео Половцу (Leo Polovets),
Марку Фишеру (Mark Fisher), Массимо Иларио (Massimo Ilario), Майклу Фогусу
(Michael Fogus), Майклу Г. Ноллу (Michael G. Noll), Патрику Деннису (Patrick
Dennis), Педро Феррере Бертрану (Pedro Ferrera Bertran), Филиппу Джанерту
(Philipp Janert), Родриго Абреу (Rodrigo Abreu), Руди Бонефасу (Rudy Bonefas),
Сэму Ритчи (Sam Ritchie), Сиве Калагарле (Siva Kalagarla), Сорену Макбету (Soren
Macbeth), Тимоти Шкловски (Timothy Chklovski), Валиду Фариду (Walid Farid),
а также Женьхуа Iÿo (Zhenhua Guo).
Натай Марц
Я был поставлен в затруднительное положение, когда передо мной встала за¬
дача упомянуть с благодарностью всех, кто внес свой посильный вклад в работу
над этой книгой. К сожалению, здесь нет места упомянуть всех этих людей. Тем
не менее мне хотелось бы выразить особую благодарность следующим людям.
■ Моей жене Вен-Йинг Фенг (Wen-Ying Feng) — за ее любовь и моральную
поддержку не только в работе над этой книгой, но и во всем, что мы дела¬
ем вместе.
■ Моим родителям Джеймсу и Гретте Уоррен — за бесконечную веру в меня
и жертвы, на которые они готовы пойти ради меня при всякой возможности.
■ Моей сестре Джулии Уоррен-Уланч (Julia Warren-Ulanch) — за блестящий
пример, которым она мне служила и которому я мог следовать.
■ Моим учительницам и наставницам Эллен Тоби (Ellen Toby) и Сью Геллер (Sue
Geller) — за готовность всегда ответить на любой вопрос и продемонстриро¬
вать радость поделиться своими знаниями, а не только приобрести их.
■ Чаку Ламу (Chuck Lam) — за то, что он обратился когда-то ко мне со следу¬
ющим вопросом: “А ты когда-нибудь слышал о чем-то вроде Hadoop?”
■ Моим друзьям и коллегам из компаний RockYou!, Storm8 и Bina, — за об¬
щий приобретенный опыт и возможность воплотить теорию на практике.
■ Марьяну Бейсу, Майклу Стивенсу, Дженифер Стаут (Jennifer Stout), Рени
Грегуар и всем сотрудникам издательства Manning Publications — за настав¬
ничество и терпение, проявленное в отношении сроков завершения этой
книги.
■ Рецензентам и первым читателям этой книги — за комментарии и крити¬
ческие замечания в отношении ясности подачи материала, что в конечном
итоге послужило заметному улучшению качества книги.
И наконец, мне хотелось бы выразить особую признательность моему соав¬
тору, Натану Марцу, за то, что он предложил принять участие в данном проек¬
те. Я и до этого был в восхищении от его трудов, и поэтому совместная работа
с ним только умножила мое уважение к его идеям, принципам и подходам. Я по¬
чел за честь быть его соавтором.
Джеймс Уоррен
Об этой книге
Для таких веб-служб, как социальные сети, веб-аналитика и интеллектуальная
электронная коммерция, нередко требуется управление данными в таких мас-
шта^ ах, которые слишком велики для традиционной базы данных. Сложность
этой задачи возрастает по мере увеличения масштабов и требований, и поэтому
большие данные — это не просто дубликат реляционных баз данных или раз¬
вертывание новомодной технологии. Правда, масштабируемость и простота не
исключают друг друга, а лишь требуют разных подходов. Для хранения и обра¬
ботки данных в системах больших данных используется много работающих па¬
раллельно машин, и это представляет главные трудности, незнакомые большин¬
ству разработчиков.
В этой книге поясняется, как строить подобные системы на основе архитек¬
туры, в которой выгодно используется кластерное оборудование наряду с инстру¬
ментальными средствами, специально предназначенными для фиксации и анали¬
за данных в масштабе веб. В ней описывается масштабируемый и легко понятный
подход к системам больших данных, которые может построить и сопровождать
небольшая команда специалистов. Теория построения систем больших данных
подается в этой книге на реальных примерах их реализации на практике.
Большие данные не требуют предварительного раскрытия для анализа круп¬
номасштабных данных или инструментальных средств типа N08(^1^ Знакомство
с традиционными базами данных полезно, хотя и не обязательно. Цель данной
книги — помочь читателю научиться анализировать информационные системы,
разделяя сложные задачи на простые решения. Мы начнем изложение материала
данной книги с рассмотрения основных принципов, а затем перейдем к необхо¬
димым свойствам каждого компонента архитектуры.
Краткое содержание книги
Рассмотрим вкратце содержание 18 глав дайной книги. В главе 1 представ¬
лены принципы построения информационных систем и вкратце описывается
лямбда-архитектура: обобщенный подход к построению любых информацион¬
ных систем. В главах 2-17 обсуждаются все составляющие лямбда-архитектуры
с попеременным изложением теории и демонстрацией примеров ее применения
на практике. В главах, посвященных теории, демонстрируются понятия, которые
остаются в силе независимо от существующих инструментальных средств, тог¬
да как в главах, посвященных демонстрации примеров, применение этих поня¬
тий на практике иллюстрируется с употреблением реальных инструментальных
Об этой книге
22
средств. Но пусть вас не смущают названия глав — все они опираются на конкрет^
ные примеры. г
В главах 2-9 основное внимание уделяется уровню пакетной обработки
лямбда-архитектуры. Из этих глав вы узнаете о моделировании главного массива
данных, употреблении пакетной обработки для создания произвольных пред¬
ставлений данных, а также о компромиссах между инкрементной и пакетной
обработкой данных.
Главы 10-11 посвящены уровню обслуживания, предоставляющему доступ с ма¬
лой задержкой к представлениям, получаемым на уровне пакетной обработки.
Из этих глав вы узнаете о специализированных базах данных, в которые инфор¬
мация записывается лишь в массовом порядке, и при этом обнаружите, что та¬
кие базы данных намного проще, чем традиционные, а следовательно, они об¬
ладают превосходной производительностью, просты и надежны в эксплуатации.
Главы 12-17 посвящены уровню ускорения, компенсирующему большую задерж¬
ку, возникающую на уровне пакетной обработки, для получения актуальных ре¬
зультатов всех запросов. Из этих глав вы узнаете о базах данных типа NoSQL,
потоковой обработке данных и преодолении сложностей инкрементной обра¬
ботки данных.
В главе 18 вам предоставляется возможность пересмотреть лямбда-архитек¬
туру, чтобы восполнить оставшиеся пробелы в своих знаниях, приобретенных
в предыдущих главах. Из этой главы вы узнаете о инкрементной пакетной обра¬
ботке данных, разных вариантах основной лямбда-архитектуры и способах из¬
влечения наибольшей пользы из ресурсов, имеющихся в вашем распоряжении.
Загружаемый код и условные обозначения
Исходный код, загружаемый в дополнение к данной книге, можно найти
по адресу https://github.com/Big-Data-Manning. Мы предоставили этот исход¬
ный код для выполнения примера службы веб-аналитики SuperWebAnalytics.com.
Большая часть этого исходного кода представлена на страницах книги в про¬
нумерованных по порядку листингах. Некоторые листинги аннотированы
для разъяснения отдельных фрагментов кода. А в других местах книги фрагмен¬
ты кода употребляются по мере надобности. Исходный код выделяется моноши¬
ринным шрифтом, как это обычно принято в технической литературе. А полужир¬
ным моноширинным шрифтом в листингах и фрагментах кода выделяются те части
исходного кода, которые поясняются в тексте книги.
Об иллюстрации на обложке книги
На обложке данной книги изображена фигура французского починщика кера¬
мики (Le Raccommodeur de Fiance) начала XIX века. Его особое ремесло состоя¬
ло в починке побитых или расколотых горшков, чашек, мисок и прочей керами¬
ческой и глиняной утвари. Он ходил по деревням, городам и поселкам по всей
Франции, предлагая свои ремесленные услуги.
Эта иллюстрация взята из четырехтомного издания Сильвеиа Марешаля
(Sylvain Maréchal) сборника региональных обычаев в одежде, существовавших во
Франции в начале XIX века. Каждая иллюстрация в этом сборнике нарисована
вручную и художественно раскрашена в цвете. Большое разнообразие коллекции
Об этой книге
23
одежды из с орника Марешаля ярко напоминает о том, насколько город и дерев¬
ня были культурно разобщены почти 200 лет назад. Встречая людей на улицах
городов и в деревнях, по их одежде нетрудно было понять, где они живут, каким
ремеслом занимаются и каково их социальное положение.
С тех пор стиль, форма и региональные отличия в одежде претерпели суще¬
ственные изменения, а ее богатство и разнообразие поблекло. Теперь жителей
разных континентов трудно отличить по одежде, не говоря уже о горожанах
и селянах. Возможно, люди променяли культурные различия на более разноо¬
бразную личную жизнь, особенно под влиянием идущего быстрыми темпами тех¬
нического прогресса.
Ныне, когда трудно отличить одну книгу на компьютерную тематику от дру¬
гой, издательство Manning Publications славится своей изобретательностью
и инициативностью в издании технической литературы с обложками книг, на¬
глядно иллюстрирующими богатство и разнообразие региональной жизни два
столетия назад, возвращая к жизни с помощью иллюстраций одежды из сборни¬
ка Марешаля.
От издательства
Вы, читатель этой книги, и есть главный ее критик и комментатор. Мы ценим
ваше мнение и хотим знать, что было сделано нами правильно, что можно было
сделать лучше и что еще вы хотели бы увидеть изданным нами. Нам интересно ус¬
лышать и любые другие замечания, которые вам хотелось бы высказать в наш адрес.
Мы ждем ваших комментариев и надеемся на них. Вы можете прислать нам
бумажное или электронное письмо, либо просто посетить наш веб-сайт и оста¬
вить свои замечания там. Одним словом, любым удобным для вас способом дай¬
те нам знать, нравится или нет вам эта книга, а также выскажите свое мнение
о том, как сделать наши книги более интересными для вас.
Посылая письмо или сообщение, не забудьте указать название книги и ее авто¬
ров, а также ваш обратный адрес. Мы внимательно ознакомимся с вашим мнением
и обязательно учтем его при отборе и подготовке к изданию последующих книг.
Наши электронные адреса:
E-mail: inf o@williamspublishing. com
WWW: http: //www. williamspublishing. com
Наши почтовые адреса:
в России: 127055, г. Москва, ул. Лесная, д.43, стр. 1
в Украине: 03150, Киев, а/я 152
Новая парадигма
для больших данных
В ЭТОЙ главе...
■ Типичные затруднения, возникающие при
масштабировании традиционных баз данных.
■ Причины, по которым базы данных типа
МоБОЬ не являются панацеей.
■ Основные принципы организации систем
больших данных.
■ Арсенал инструментальных средств
для больших данных.
■ Представление службы веб-аналитики
SuperWebAnalytics.com
За последнее десятилетие объем создаваемых данных стремительно увеличил¬
ся. Ныне каждую секунду формируется более 30 тысяч гигабайт данных, и ско¬
рость их создания только увеличивается.
Нам приходится иметь дело с разнообразными данными. Пользователи соз¬
дают контент вроде сообщений в блогах и сети Tweeter, взаимодействий в со¬
циальных сетях и фотографий. Серверы постоянно регистрируют сообщения
о выполняемых операциях. Ученые составляют подробные отчеты об исследова¬
ниях окружающего нас мира. Интернет окончательно стал основным и необъят¬
но большим источником данных.
Столь поразительный рост объемов данных оказал глубокое влияние на раз¬
ные сферы деятельности. Такие традиционные системы баз данных, как реляци¬
онные базы данных, достигли предела своих возможностей. Все чаще подобные
системы не выдерживают под давлением больших данных. Традиционные систе¬
мы и связанные с ними методики управления данными достигли своего предела
и так и не смогли дорасти до масштабов больших данных.
26
Глава l. Новая парадигма для больших данных
Для решения задач, связанных с обработкой больших данных, возникли но¬
вые технологии. Многие из этих технологий сгруппированы под одним общим
термином NoSQL, обозначающим нереляционные распределенные базы данных.
В какой-то одной мере новые технологии сложнее традиционных баз данных,
а в другой - проще. Такие информационные системы способны к масштаби¬
рованию до чрезвычайно больших массивов данных, но для их эффективного
применения требуются совершенно новые технологии. И они не подразумевают
единое решение, пригодное на все случаи жизни.
Многие из систем больших данных были впервые созданы компанией Google,
включая распределенные файловые системы, каркас распределенных вычисле¬
ний MapReduce и службы распределенной блокировки. Еще одним первопроход¬
цем в данной области стала компания Amazon, создавшая новаторское распреде¬
ленное хранилище пар “ключ-значение” под названием Dynamo. Свою лепту за
последние годы внесло и сообщество разработчиков программного обеспечения
с открытым исходным кодом, реализовав Hadoop, HBase, MongoDB, Cassandra,
RabbitMQ и много других проектов.
Эта книга посвящена вопросам масштабируемости информационных систем
для преодоления присущей им сложности. Чтобы удовлетворить требованиям
больших данных, необходимо основательно переосмыслить информационные
системы. В ходе такого переосмысления обнаруживается, что самые элементар¬
ные способы управления данными в таких традиционных системах, как систе¬
мы управления реляционными базами данных (СУРБД), оказываются слишком
сложными для систем больших данных. Поэтому более простой альтернативный
подход состоит в выборе новой парадигмы для больших данных. И такой подход
мы назвали лямбда-архитектурой.
В этой первой главе данной книги исследуется проблема больших данных и по¬
ясняются причины, по которым для них требуется новая парадигма. В ней раскры¬
ваются опасности, которые несут в себе некоторые традиционные методики мас¬
штабирования, а также глубокие изъяны в традиционном подходе к построению
информационных систем. Начав с основных принципов организации информа¬
ционных систем, мы сформулируем другой способ их построения без сложности,
присущей традиционным методикам. Затем мы рассмотрим последние технологи¬
ческие тенденции, стимулирующие применение новых видов систем, а в заверше¬
ние главы — пример системы больших данных, которую нам предстоит построить
на протяжении всей данной книги для иллюстрации ключевых понятий.
1.1. Структура книги
Эту книгу следует рассматривать главным образом как теоретическую, где
основное внимание уделяется подходу к решению задачи организации любых
больших данных. Принципы, рассматриваемые в данной книге, остаются в силе
независимо от применяемых инструментальных средств в конкретной области.
Эти принципы позволяют строго отобрать те инструментальные средства, кото¬
рые больше всего подходят для конкретного приложения.
В этой книге не делается обзор баз данных, вычислений и других связанных
с ними технологий. И хотя в ней поясняется, как пользоваться многими ин-
/'-mvMPH'rarrbHbiMH соелствами вроде Hadoop, Cassandra, Storm и Thrift, она не
Глава 1.Новая парадигма для больших данных
27
предназначена для изучения этих инструментальных средств. Напротив, они лишь
помогают в изучении основных принципов построения надежных и масштабиру¬
емых информационных систем. Подробное сравнение достоинств и недостатков
разных инструментальных средств вряд ли принесет пользу, поскольку оно лишь
отвлекает внимание от изучения основных принципов. Иными словами, вам пред¬
лагается научиться ловить рыбу, а не освоить конкретную удочку.
В этой связи мы организовали структуру книги, разделив ее на теоретические
и иллюстративные главы. Прочитав только теоретические главы, вы составите
ясное представление о том, как строятся системы больших данных. Но, на наш
взгляд, процесс увязки теории с конкретными инструментальными средствами
в иллюстративных главах сделает понимание материала книги более глубоким,
дополнив его важными подробностями.
Несмотря на названия теоретических глав, они опираются на конкретные при¬
меры. В качестве всеобъемлющего примера в теоретических и иллюстративных
главах книги рассматривается веб-служба SuperWebAnalytics.com. В теоретических
главах представлены алгоритмы, индексные структуры и архитектура веб-службы
SuperWebAnalytics.com. А в иллюстративных главах эти структуры преобразуются
конкретными инструментальными средствами в функционирующий код.
1.2. Масштабирование в традиционных базах данных
Итак, начнем исследование больших данных с того, с чего обычно начина¬
ют многие разработчики, а именно: с границ, которых достигли возможности
традиционных технологий баз данных. Допустим, ваш начальник поставил пе¬
ред вами задачу написать простое приложение веб-аналитики. Это приложение
должно отслеживать количество просмотров страниц по любому 1Л1Ь, указанно¬
му пользователем. Всякий раз, когда клиент просматривает страницу, с веб-стра¬
ницы пользователя посылается запрос на веб-сервер данного приложения с ука¬
занием ее иШ-,. Кроме того, приложение должно в любой момент сообщать ТЖЬ
ста наиболее посещаемых веб-сайтов по количеству просмотров страниц.
Сначала вы создаете для просмотров
страниц схему традиционной реляционной
базы данных, аналогичную приведенной
на рис. 1.1. Ваше серверное приложение со¬
стоит из СУРБД с таблицей, составленной
по данной схеме, и веб-сервера. Всякий раз,
когда кто-нибудь загружает веб-страницу, от¬
слеживаемую вашим приложением, с этой
веб-страницы посылается запрос на ваш
веб-сервер с указанием просмотра страни¬
цы, а на веб-сервере увеличивается значе¬
ние счетчика соответствующей строки в та¬
блице базы данных.
Рассмотрим те трудности, которые возникают в процессе развития данного
приложения. В частности, речь пойдет о трудностях, связанных с масштаби-
руемостью и сложностью.
Имя столбца
Тип
id
integer
user_i d
integer
url
varchar(255)
pageview s
bigint
Рис. 1.1. Схема реляционной базы данных
для простого приложения веб-аналитики
28
Глава 1. Новая парадигма для больших данных
1.2.1. Масштабирование с помощью очереди
Итак, созданное вами приложение для веб-аналитики пользуется большим
успехом, а сетевой трафик к нему стремительно растет. Ваша компания устра¬
ивает по этому поводу большой прием, но пока вы празднуете свой успех, ваше
приложение начинает получать по электронной почте множество сообщений
от системы мониторинга. И все они извещают об одном и том же: “Ошибка из-
за превышения времени ожидания при вводе информации в базу данных”.
Просмотрев журналы регистрации, вы выясняете причину возникшего за¬
труднения. А она, очевидно, состоит в том, что база данных не справляется с на¬
грузкой, и поэтому запросы на запись для приращения количества просмотров
страниц блокируются по времени ожидания.
Итак, вам нужно каким-то образом устранить возникшее затруднение, и сде¬
лать это быстро. Вы осознаете, что было бы расточительно выполнять одиноч¬
ные приращения в базе данных по очереди. Ведь намного эффективнее сгруппи¬
ровать многие приращения в одном запросе. И для этого вам придется переде¬
лать архитектуру своего серверного приложения.
Вместо того чтобы заставлять веб-сервер
обращаться к базе данных непосредственно,
вы вводите очередь между веб-сервером и ба¬
зой данных. Всякий раз, когда получается но¬
вый результат просмотра страницы, это собы¬
тие вводится в очередь. Затем вы организуете
рабочий процесс для одновременного чтения
100 событий из очереди, группируя их в еди¬
ный запрос на обновление базы данных, как
показано на рис. 1.2.
Такая схема вполне работоспособна и раз¬
решает затруднения, возникавшие ранее в свя¬
зи с блокировкой по времени ожидания. Она даже дает следующее преимущество
в случае перегрузки базы данных: очередь просто увеличивается вместо блоки¬
ровки веб-сервера из-за времени ожидания и возможной потери данных.
1.2.2. Масштабирование путем фрагментации базы данных
К сожалению, ввод очереди и пакетное обновление оказались лишь времен¬
ной мерой для разрешения затруднений, возникающих в связи с масштабирова¬
нием. По мере роста популярности вашего приложения база данных снова пере¬
гружается. Рабочий процесс не справляется с записями, и поэтому вы пытаетесь
ввести дополнительные рабочие процессы с целью распараллелить операции об¬
новления. Но, к сожалению, это не помогает. Очевидным узким местом по-преж¬
нему остается база данных.
Далее вы пробуете найти в поисковом механизме Google способы масштаби¬
рования реляционной базы данных с интенсивными операциями записи. В итоге
вы обнаруживаете, что самый лучший способ сделать эю распределить табл и
цу среди многих серверов базы данных. На каждом сервере должно находиться
подмножество данных из таблицы. Такая методика называется горизонтальным
одновременно
Рис. 1.2. Группировка обновлений
базы данных с помощью очереди
и рабочего процесса
Глава /. Новая парадигма для больших данных
29
разделением (позиционированиемили фрагментацией базы данных, и позволяет рас¬
пределить нагрузку на запись данных среди многих машин.
Методика фрагментации базы данных состоит в том, чтобы выбрать фрагмент
для каждого ключа, взяв хеш-код ключа и получив хеш-значение по модулю коли¬
чества фрагментов. Преобразование ключей во фрагменты с помощью хеш-функ¬
ции приводит к тому, что ключи равномерно распределяются по фрагментам.
Для преобразования всех строк в едином экземпляре базы данных и разделения
данных на четыре фрагмента вам придется написать отдельный сценарий. Для
его выполнения требуется время, и поэтому вы отключаете рабочий процесс,
инкрементирующий число просмотров страниц, чтобы благополучно завершить
сценарий. В противном случае вы потеряете приращения в течение перехода.
И наконец, во всем вашем прикладном коде требуется знать, как найти фраг¬
мент для каждого ключа. С этой целью вы заключаете библиотеку в оболочку
кода для чтения количества фрагментов из файла конфигурации базы данных
и повторно развертываете весь код своего приложения. Вам придется видоиз¬
менить запрос на ста наиболее посещаемых веб-сайтов, чтобы получить из
каждого фрагмента иЯЬ ста наиболее посещаемых веб-сайтов и объединить их
вместе с целью составить глобальный перечень из ста подобных веб-сайтов.
По мере роста популярности вашего приложения вы продолжаете разделять
базу данных на дополнительные фрагменты, чтобы она могла справляться с на¬
грузкой на запись данных. Но делать это всякий раз становится все труднее, по¬
скольку приходится все больше координировать работу отдельных частей при¬
ложения. И теперь для повторной фрагментации уже нельзя обойтись только
одним сценарием, поскольку это было бы слишком медленно. Всю повторную
фрагментацию вам придется делать параллельно, одновременно управляя мно¬
гими активными рабочими сценариями по очереди. При этом вы забываете об¬
новить прикладной код новым числом фрагментов. А это приводит к тому, что
многие приращения будут записаны не в те фрагменты. Таким образом, вам при¬
дется написать одноразовый сценарий для ручного перебора данных и переме¬
щения части из них на свои законные места.
1.2.3. Проявление вопросов отказоустойчивости
В конечном счете вам приходится иметь дело с таким количеством фрагмен¬
тов, что все чаще начинают возникать сбои дисков на каком-нибудь из серверов
базы данных. Соответствующая часть данных становится недоступной на время
выхода сервера базы данных из строя. В качестве выхода из этого затруднитель¬
ного положения вы предпринимаете следующие действия.
■ Обновление системы, состоящей из очереди и рабочего процесса, чтобы
перенести приращения для недоступных фрагментов в отдельную “ожида¬
ющую” очередь, а затем пытаться извлекать ее содержимое через каждые
пять минут.
■ Дополнение каждого фрагмента подчиненным фрагментом путем репликации
базы данных, чтобы иметь резерв на случай выхода из строя главного фраг¬
мента Данные в подчиненный фрагмент не записываются, но, по крайней
мере пользователи смогут просматривать статистику в вашем приложении.
зо
Глава І. Новая парадигма для больших данных
Размышляя над сложившейся ситуацией, вы рассуждаете следующим образом:
Раньше я занимался в основном построением новых функциональных средств
для пользователей. А теперь мне приходится тратить все свое рабочее время
только на устранение затруднений, связанных с чтением и записью данных”.
1.2.4. Проявление вопросов искажения данных
Работая над кодом обращения с очередью и рабочим процессом, вы случайно
вносите в него ошибку, увеличивающую число просмотров страниц по каждому
иЯЬ на два, а не на единицу. Эту ошибку вы замечаете лишь сутки спустя, когда
ущерб уже нанесен. Еженедельные резервные копии вам не помогут, посколь¬
ку вы не знаете, какие именно данные были искажены. После стольких трудов
в попытках сделать систему масштабируемой и отказоустойчивой она оказыва¬
ется неспособной восстанавливаться после ошибок, связанных с человеческим
фактором. В своем программном обеспечении вы можете гарантировать лишь
неизбежное проникновение программных ошибок в выходной код, как бы вы ни
старались этому воспрепятствовать.
1.2.5. Что пошло не так
По мере дальнейшего развития рассматриваемого здесь простого приложе¬
ния веб-аналитики система становилась все сложнее: сначала в ней появились
очереди, затем фрагменты, далее репликация, сценарии повторной фрагмента¬
ции и т.д. Для разработки приложений, предназначенных для обработки данных,
требуется нечто намного большее, чем только знание схемы базы данных. В коде
приложения должно быть известно, как обращаться с верными фрагментами.
И если вы допустите ошибку, то ничто не должно помешать вам читать и запи¬
сывать данные в неверный фрагмент.
Дело, в частности, в том, что вашей базе данных незнаком распределенный ха¬
рактер, и поэтому она ничем не может помочь вам справляться с фрагментацией,
репликацией и обработкой распределенных запросов. Все эти сложности возникли
у вас как при обращении к базе данных, так и при разработке прикладного кода.
Но еще хуже, что рассматриваемая здесь система разработана без учета
ошибок, связанных с человеческим фактором. Напротив, система становится
все сложнее, а следовательно, увеличивается вероятность совершения ошибок.
Ошибки в программном обеспечении неизбежны, но если оно проектируется
без учета вероятных ошибок, то вполне возможным оказывается написание сце¬
нариев, случайно искажающих данные. Для восстановления после ошибок недо¬
статочно одного лишь резервирования. Система должна быть тщательно проду¬
мана вплоть до ущерба, который способны нанести ошибки, связанные с чело¬
веческим фактором. Устойчивость к подобным ошибкам является обязательным,
а не дополнительным свойством системы. Это особенно справедливо для боль¬
ших данных, вносящих намного больше сложностей в разработку приложений.
1.2.6. Чем могут помочь технологии организации больших данных
Технологии организации больших данных, которые будут рассматриваться
далее в этой книге, в значительной степени разрешают затруднения, связанные
с масштабируемостью и сложностью информационных систем. Прежде всего,
Глаза L Новая парадигма для больших данных
31
базам данных и вычислительным системам, применяемым для организации боль-
ши\даННЫХ’ знаком их Разделенный характер, и поэтому такие операции,
как фрагментация и репликация, выполняются автоматически. И маловероятной
оказывается ситуация, когда делается запрос не того фрагмента базы данных,
поскольку логика подобных запросов встроена в саму базу данных. Что же ка¬
сается масштабирования, то достаточно ввести узлы, а система автоматически
перестроит свою структуру с учетом новых узлов.
Еще одна базовая методика, о которой следует знать, состоит в том, чтобы еде-
лать данные неизменяемыми. Вместо того чтобы хранить подсчеты просмотров
страниц в главном массиве данных, который постоянно изменяется по мере по¬
ступления новых просмотров страниц, сохраняется необработанная информация
о них, которая вообще не видоизменяется. И если будет совершена ошибка (воз¬
можно, записаны неверные данные), то, по крайней мере, это не нарушит верные
данные. Это намного более строгая гарантия устойчивости к ошибкам, связанным
с человеческим фактором, чем традиционная система, основанная на видоизме¬
нении данных. В традиционных базах данных изменяемые данные будут исполь¬
зоваться осторожно из-за быстрого разрастания подобного массива данных. Но
поскольку методики хранения больших данных допускают масштабирование таких
объемов данных, то имеется возможность по-разному проектировать информаци¬
онные системы.
1.3. Базы данных типа NoSQL не являются панацеей
За последние десять лет было внедрено немало нововведений в масштаби¬
руемые информационные системы. К их числу относятся системы крупномас¬
штабных вычислений вроде Hadoop и базы данных типа Cassandra и Riak. Такие
системы способны обрабатывать очень большие объемы данных, хотя и с серьез¬
ными компромиссами.
Например, система Hadoop позволяет распараллелить крупномасштабные па¬
кетные вычисления, производимые над очень большими объемами данных, но
такие вычисления производятся с большой задержкой. Поэтому система Hadoop
непригодна там, где требуется получение результатов с малой задержкой.
Масштабируемость в базах данных типа NoSQL вроде Cassandra достигается
благодаря тому, что они предоставляют намного более ограниченную модель
данных, чем в базах данных SQL. Втиснуть приложение в столь ограниченные
модели данных может быть очень сложно. А поскольку базы данных изменяемы,
то они не устойчивы к отказам, связанным с человеческим фактором.
Подобные инструментальные средства не являются панацеей. Но если они
применяются благоразумно и в определенном сочетании, то с их помощью мож¬
но создать масштабируемые системы для решения произвольных информацион¬
ных задач с минимальной сложностью и устойчивостью к отказам, связанным
с человеческим фактором. Все это позволяет сделать лямбда-архшекгура, рас¬
сматриваемая в данной книге.
1.4. Основные принципы
Чтобы выяснить, как правильно строить информационные системы, сле¬
дует вернуться к основным принципам. На самом основополагающем уровне
32
Глава 1. Новая парадигма для больших данных
необходимо найти ответ на следующий вопрос: каково назначение информаци¬
онной системы? Начнем со следующего интуитивного определения: информацион¬
ная система отвечает на вопросы, опираясь на информацию, зафиксированную в прошв-
дее время и вплоть до текущего момента. Так, в социальной сети подобная система
отвечает на следующие вопросы: каково имя человека и сколько у него друзей?
А веб-страница банковского счета отвечает на такие вопросы: какой остаток
на моем счете и какие транзакции произошли на моем счете за последнее время?
Информационные системы не только запоминают и регистрируют информа¬
цию. Они объединяют отдельные ее фрагменты для получения ответов на постав¬
ленные вопросы. Например, ответ об остатке на банковском счете основывается
на объединении информации обо всех транзакциях, совершенных на этом счете.
Еще одно важное наблюдение состоит в том, что не все фрагменты инфор¬
мации одинаковы. Одни фрагменты информации получаются из других. Так,
остаток на банковском счете получается из предыстории транзакций, учетная
запись друга — из списка друзей, а тот — в результате ввода и удаления друзей из
пользовательского профиля.
Прослеживая источники получения информации, можно в конечном итоге
добраться до исходной информации, которая не получена из ничего. Это самая
первоначальная информация, которая считается истинной просто потому, что
она существует. Назовем эту информацию данными.
Термин данные может иметь разные значения. Его нередко употребляют по¬
переменно с термином информация. Но в остальной части данной книги термин
данные употребляется для обозначения специальной информации, из которой
получается все остальное.
Если информационная система отвечает на вопросы, анализируя прошлые
данные, то большинство информационных систем общего назначения анализи¬
руют весь массив данных, чтобы ответить на поставленные вопросы. Самое уни¬
версальное определение, которое можно дать информационной системе, выгля¬
дит следующим образом:
запрос = функция(все данные)
Все, что только позволяет воображение сделать с данными, можно выразить
в виде функции, принимающей все данные как входные. Запомните приведенное
выше уравнение, поскольку оно составляет суть всего, что вам придется изучить
далее в данной книге. Мы еще не раз будем обращаться к этому уравнению.
Лямбда-архитектура предоставляет универсальный подход к реализации для про¬
извольного массива данных произвольной функции, которая возвращает свои ре¬
зультаты с малой задержкой. Впрочем, это совсем не означает, что для реализации
информационной системы всякий раз применяются одни и те же технологии.
Употребляемые технологии могут меняться в зависимости от конкретных требова¬
ний. Но лямбда-архитектура определяет согласованный подход к выбору этих тех¬
нологий и их сочетанию для удовлетворения конкретных требований. А теперь об¬
судим те свойства, которыми должна обладать информационная система.
1.5. Желательные свойства системы больших данных
Свойства, которыми должны обладать системы больших данных, связаны со
сложностью системы в такой же степени, как и с ее масштабируемостью. Система
Глава /» Ноем парадигма для больших данных
33
больших данных должна не только надежно работать и эффективно потреблять
ресурсы, но и должна быть простой в обращении. Рассмотрим свойства такой
системы по порядку.
1.5.1. Надежность и отказоустойчивость
Построить систему, которая делает именно то, что нужно, не так-то про¬
сто, принимая во внимание трудности организации распределенных систем.
Подобные системы должны действовать правильно независимо от случайных ап¬
паратных отказов, сложной семантики согласованности в распределенных базах
данных, дублирования данных, распараллеливания операций и прочих факто¬
ров. Вследствие подобных трудностей оказывается нелегко даже выяснить, что
же система делает. Отчасти добиться надежности системы больших данных озна¬
чает избежать этих осложнений, чтобы можно было легко рассуждать о системе.
Как обсуждалось ранее, очень важно, чтобы системы были устойчивы к отка¬
зам, связанным с человеческим, фактором. Это свойство информационных систем
нередко упускается из виду, но им нельзя пренебрегать. При эксплуатации си¬
стемы неизбежны ошибки, совершаемые кем-нибудь, например, в результате раз¬
вертывания неверного кода, портящего значения в базе данных. Если положить
неизменяемость и повторность вычислений в основу системы больших данных,
то такая система будет естественно устойчива к ошибкам, связанным с челове¬
ческим фактором, предоставляя ясный и простой механизм для восстановления
после подобных ошибок. Подробнее об этом речь пойдет в главах 2-7.
1.5.2. Малые задержки чтения и записи данных
В подавляющем большинстве приложений требуется читать данные с удовлет¬
ворительно малой задержкой (как правило, в пределах от пяти до нескольких
сотен миллисекунд). С другой стороны, требования к задержке обновлений мо¬
гут заметно отличаться в разных приложениях. В одних приложениях требует¬
ся, чтобы обновления распространялись немедленно, тогда как в других вполне
допустима задержка на несколько часов. Как бы там ни было, необходимо до¬
биться малой задержки обновлений именно тогда, когда они требуются в систе¬
мах больших данных. Но еще важнее добиться малой задержки чтения и записи
данных, не нарушая надежность системы. О том, как добиться малой задержки
обновлений, речь пойдет при обсуждении уровня ускорения, начиная с главы 12.
1.5.3. Масштабируемость
Масштабируемость — это способность поддерживать производительность с уче¬
том увеличения объемов данных или нагрузки на систему по мере ввода в нее
ресурсов. Лямбда-архитектура масштабируется по горизонтали на всех уровнях
системного стека. Такое масштабирование осуществляется путем добавления до¬
полнительных машин.
1.5.4. Обобщение
Общая система способна поддерживать обширный ряд приложений. Разумеется,
эта книга была бы не очень полезной, если бы в ней не был обобщен обширный
34
Глава /. Новая парадигма для больших данных
ряд приложений! Лямбда-архитектура основана на функциях всех данных, и поэ¬
тому в ней обобщены все приложения, будь то системы ведения финансовых опе¬
раций, приложения для аналитики социальных сетей, научные приложения и т.п.
1.5.5. Расширяемость
Вряд ли стоит изобретать колесо всякий раз, когда в систему внедряется со¬
ответствующее функциональное средство или вносятся изменения в порядок ее
работы. Расширяемые системы позволяют внедрять функциональные возможно¬
сти при минимальных затратах на разработку.
Нередко новые функциональные средства или изменения в существующих
функциональных средствах требуют переноса прежних данных в новый формат.
Отчасти расширяемость системы означает упрощение крупномасштабных пере¬
носов данных. Возможность произвести такой перенос просто и быстро состав¬
ляет основу подхода, рассматриваемого в данной книге.
1.5.6. Запросы с произвольным доступом
Возможность делать запросы с произвольным доступом к данным имеет чрез¬
вычайно большое значение. Почти каждый массив данных содержит непред¬
усмотренное значение. А возможность произвольного доступа к массиву данных
способствует оптимизации коммерческой деятельности и внедрению новых при¬
ложений. В конечном итоге вы не сможете обнаружить ничего интересного в
данных, не обратившись к ним с запросами с произвольным доступом. О том,
как делаются такие запросы, речь пойдет в главах 6 и 7, когда будут обсуждаться
вопросы пакетной обработки.
1.5.7. Минимальное сопровождение
Сопровождение ложится на разработчиков дополнительным бременем, по¬
добно налогам. Это вид работ, которые требуется выполнять для поддержания
системы в нормальном состоянии во время ее эксплуатации. К их числу относит¬
ся предварительное планирование ввода в эксплуатацию новых машин для мас¬
штабирования системы, поддержание процессов в рабочем состоянии, а также
устранение любых неполадок, возникающих при эксплуатации системы.
Чтобы свести к минимуму сопровождение системы важно выбрать компонен¬
ты, обладающие как можно меньшими сложностями реализации. Для этого следует
предпочесть компоненты, в основу которых положены простые механизмы. В
частности, распределенные базы данных обычно имеют очень сложные внутрен¬
ние механизмы. Чем сложнее система, тем вероятнее сбои в ее работе и тем луч¬
ше нужно знать систему, чтобы отлаживать и настраивать ее.
Чтобы бороться со сложностями реализации, следует применять простые ал¬
горитмы и компоненты. Так, в лямбда-архитектуре сложности специально пере¬
несены из базовых компонентов в те части системы, выходные данные которых
аннулируются через несколько часов. Самые сложные из применяемых компо¬
нентов вроде распределенных баз данных чтения-записи находятся на том уров¬
не, где выходные данные в конечном итоге аннулируются. Подобная методика
подробно рассматривается в главе 12 при обсуждении уровня ускорения.
35
Глава 1. Новая парадигма для больших данных
1.5.8. Отлаживаемость
Система больших данных должна предоставлять информацию, необходимую
для отладки этой системы в том случае, когда в ней возникают неполадки. Для
этого в системе должен отслеживаться каждый параметр, чтобы выяснить при¬
чины, по которым он появился.
Отлаживаемость в лямбда-архитектуре достигается благодаря функциональ¬
ному характеру уровня пакетной обработки и предпочтительному применению
алгоритмов повторных вычислений по мере возможности. На первый взгляд, до¬
стичь всех этих свойств в одной системе очень трудно. Но если начать с основ¬
ных принципов, как это делается в лямбда-архитектуре, то подобные свойства
возникнут естественным образом из структуры системы.
Прежде чем перейти к углубленному исследованию лямбда-архитектуры, рас¬
смотрим более традиционные архитектуры, полагающиеся на пошаговые (или
иначе инкрементные) вычисления. Попробуем также выяснить причины, по ко¬
торым они неспособны удовлетворить многим из перечисленных выше свойств
системы больших данных.
(^П^ложение^^
База данных
Рис. 1.3. Полностью инкрементная
архитектура
1.6. Недостатки полностью инкрементных архитектур
На самом верхнем уровне традиционные архитектуры выглядят так, как пока¬
зано на рис. 1.3. Такие архитектуры отличаются тем, что в них применяются
базы данных чтения-записи, состояние которых поддерживается инкрементно
(т.е. шаг за шагом) по мере поступления новых данных. Например, инкремент¬
ный подход к подсчету просмотров страниц состоит в обработке нового просмо¬
тра страницы приращением на единицу
счетчика для ее иЯЬ. Эта особенность по¬
добных архитектур имеет намного более ос¬
новополагающий, чем только реляционный,
характер в сравнении с нереляционными
архитектурами. На самом деле подавляющее
большинство реляционных и нереляционных баз данных развертывается в виде
полностью инкрементных архитектур. И так происходило многие десятилетия.
Следует особо подчеркнуть, что полностью инкрементные архитектуры на¬
столько широко распространены, что многие даже не осознают, что от недостат¬
ков подобных архитектур можно избавиться, выбрав другую архитектуру. Такие
архитектуры служат ярким примером легкоузнаваемой сложности, которая настоль¬
ко укоренилась, что никто даже не пытается искать способы избежать ее.
Недостатки полностью инкрементных архитектур существенны. Поэтому начнем
исследование этого вопроса с рассмотрения общих сложностей, которые привно¬
сит любая полностью инкрементная архитектура. Затем мы рассмотрим два про¬
тивоположных решения одной и той же задачи: одно - с использованием самой
лучшей из возможных полностью инкрементной архитектуры, а другое - с помо¬
щью лямбда-архитектуры. Из сравнения этих решений станег ясно, что полностью
инкрементный вариант оказывается значительно хуже во многих отношениях.
36
Глава 1. Новая парадигма для больших данных
1.6.1. Эксплуатационная сложность
Полностью инкрементным архитектурам присущи многие сложности, затруд-
няющие эксплуатацию производственной инфраструктуры. Рассмотрим одну из
них: потребность выполнять в оперативном режиме уплотнение в базах данных
чтения-записи, а также те эксплуатационные меры, которые требуется предпри¬
нять для нормальной работы системы.
По мере пошагового увеличения и модификации индекса диска в базе данных
чтения-записи части этого индекса перестают использоваться. Эти неиспользу¬
емые части занимают место на диске, и поэтому от них нужно избавиться во
избежание переполнения диска. Но восстановление дискового пространства,
как только оно перестает использоваться, обходится слишком дорого. Поэтому
восстановление такого пространства происходит периодически и массово в ходе
процесса, называемого уплотнением.
Операция уплотнения интенсивно потребляет вычислительные ресурсы.
Сервер предъявляет немалые требования к ЦП и дискам во время уплотнения
данных, что значительно снижает производительность машины на период вы¬
полнения данной операции. Как известно, базы данных вроде HBase и Cassandra
требуют тщательной конфигурации и управления во избежание осложнений или
блокировок серверов во время уплотнения. Потери производительности во время
уплотнения представляют сложность, которая может даже вызвать каскадные от-
казы. Если уплотнение данных выполняется одновременно на слишком большом
числе машин, нагрузка от них перейдет на другие машины в кластере. Это может
привести к перегрузке в остальной части кластера, а следовательно, и к полному
отказу. Такой режим отказа нам приходилось наблюдать неоднократно.
Чтобы правильно управлять операциями уплотнения, их необходимо заплани¬
ровать в каждом узле, чтобы они не оказывали воздействия на слишком большое
число узлов. Для этого нужно ясно понимать, как долго происходит уплотнение
данных и с каким временным разбросом, чтобы не подвергать уплотнению боль¬
ше узлов, чем требуется. Необходимо также убедиться, что в отдельных узлах
имеется достаточная емкость дисков, чтобы они могли нормально функциони¬
ровать в промежутках между операциями уплотнения. Кроме того, необходимо
убедиться в достаточной емкости дисков в кластере, чтобы избежать перегрузки
при потере ресурсов во время операций уплотнения.
Все эти вопросы могут быть разрешены компетентным эксплуатационным пер¬
соналом, но, на наш взгляд, самый лучший способ справиться с любого рода слож¬
ностью — вообще исключить ее. Чем меньше случаев отказа имеется в системе, тем
менее вероятен выход системы из строя в самые неподходящие моменты времени.
Действия с уплотнением данных в оперативном режиме представляет сложность,
присущую полностью инкрементным архитектурам, но в лямбда-архитектуре пер¬
вичным базам данных вообще не требуется уплотнение в оперативном режиме.
1.6.2. Крайняя сложность достижения
окончательной согласованности
Еще одна сложность возникает в полностью инкрементных архитектурах при по¬
пытке сделать систему доступной в высокой степени. В такой системе допускаются
запросы и обновления даже при аппаратном отказе или частичном сбое в сети.
Глава L Новая парадигма для больших данных
37
Оказывается, чю достижение высокой доступности системы входит в пря¬
мое противоречие с другим важным свойством, называемым согласованностью.
Согласованная система возвращает результаты, в которых обращают внимание
на все предыдущие записи. Как следует из теоремы CAP (Consistency, Availability,
Partition tolerance — Согласованность, Доступность, Устойчивость к разделению),
невозможно достичь высокой доступности и согласованности в одной и той же си¬
стеме при разделении сети. Таким образом, доступные в высокой степени системы
иногда возвращают устаревшие результаты во время разделения сети.
Более подробно теорема САР рассматривается в главе 12, а до тех пор сдела¬
ем акцент на том, что невозможность достичь полной совместимости и высокой
доступности постоянно оказывает влияние на способность разработчиков стро¬
ить системы. Оказывается, что если в требованиях предметной области высокая
доступность системы преобладает над полной согласованностью, то сложность
разработки такой системы заметно возрастет.
Чтобы вернуть доступную в высокой степени систему к согласованности, как
только разделен ie сети завершится (это так называемая окончательная согласован¬
ность), потребуется немалая помощь со стороны приложения. Рассмотрим в ка¬
честве элементарного примера ведение подсчета в базе данных. Очевидно, что
для этого необходимо хранить число в базе данных, инкрементируя его всякий
раз, когда принимается событие, требующее увеличения подсчета. Как ни стран¬
но, если принять именно такой подход, то во время разделения сети может про¬
изойти массовая потеря данных.
Дело в том, что в распределенных базах данных высокая доступность достига¬
ется сохранением многих реплик (т.е. тиражируемых копий) всей информации.
Если хранится много копий одной и той же информации, она остается по-преж¬
нему доступной при выходе машины из строя или разделении сети, как показано
на рис. 1.4. Во время разделения сети система, которая должна быть доступной
в высокой степени, принуждает своих клиентов к обновлению тех реплик, кото¬
рые им доступны. В итоге реплики не совпадают, получая разные обновления.
И только после того, как разделение сети пройдет, реплики можно объединить
в некоторое общее значение.
Допустим, имеются две реплики с подсчетом, равным 10, когда начинается
разделение сети. Допустим также, что в первой реплике этот подсчет инкремен¬
тируется дважды, а во второй — однажды. Когда наступает момент объединить
эти реплики вместе, какое общее значение подсчета получится в результате объе¬
динения значений 12 и 11, инкрементированных в обеих репликах? Правильным
ответом на этот вопрос должно быть значение 13, но это трудно сказать, глядя
только на значения 12 и 11. Они могут отличаться на 11, и тогда ответом будет
значение 12, или на нуль, и тогда ответом будет значение 23.
Для правильного подсчета с высокой степенью доступности недостаточно
лишь хранить подсчет. Для этого требуется создать структуру данных, поддаю¬
щуюся объединению, когда значения отличаются, а также реализовать код, кото¬
рый поправит значения по окончании разделения сети. Поразительно, насколь¬
ко возрастает сложность построения системы даже в таком простом примере
ведения подсчета.
38
Глава 1. Новая парадигма для больших данных
/ "
'Ч
I
I
I
/
...
х -> 10
I
I
х -> 10
у-> 12
I
У -> 12
Реплика 1
I
I
I
1
Реплика 2
_ . )
1
1
/ ^ <
Запрос I Запрос
Рис. 1.4. Повышение доступности системы с помощью репликации
В общем, достижение окончательной согласованности в инкрементных си¬
стемах с высокой степенью доступности происходит интуитивно и не лишено
ошибок. Подобная сложность присуща всем полностью инкрементным системам
с высокой степенью доступности. Как будет показано далее, структурирование
лямбда-архитектуры происходит иначе, значительно облегчая задачу достижения
окончательно согласованных систем.
1.6.3. Неустойчивость к ошибкам,
связанным с человеческим фактором
И последний недостаток, который присущ полностью инкрементным архи¬
тектурам и на который следует обратить внимание, состоит в отсутствии у них
характерной устойчивости к ошибкам, связанным с человеческим фактором.
Инкрементная система постоянно модифицирует состояние, сохраняемое в базе
данных. Это означает, что состояние может быть изменено в базе данных по ошиб¬
ке. А поскольку ошибки неизбежны, то база данных в полностью инкрементной
архитектуре совершенно не гарантирует, что данные не будут искажены.
Следует заметить, что это одна из тех нескольких сложностей полностью
инкрементных архитектур, которые можно разрешить, не переделывая пол¬
ностью архитектуру. Рассмотрим в качестве примера две архитектуры, при¬
веденные на рис. 1.5: синхронную, где приложение делает обновления непо¬
средственно в базе данных; а также асинхронную, где события направляются
в очередь перед обновлением базы данных в фоновом режиме. В обоих случа¬
ях каждое событие неизменно регистрируется в информационном хранилище
событий. Сохраняя каждое событие на тот случай, если произойдет искаже¬
ние базы данных по ошибке, обусловленной человеческим фактором, можно
40
Глава L Новая парадигма для больших данных
иметь эквивалент с именем пользователя sally. Если же адрес sallyögmail.
com регистрируется по имени пользователя sally2, то он будет иметь экви¬
валент с именем пользователя sally2. По правилам транзитивности имена
пользователей sally и sally2 обозначают одно и то же лицо.
Назначение данного запроса — рассчитать число однозначных посетителей
по указанному URL в течение заданного промежутка времени. Запросы должны
быть актуальными для всех данных с минимальной задержкой реакции (менее
100 мс). Интерфейс такого запроса выглядит следующим образом:
long uniquesOverTime(String url, int startHour, int endHour)
Реализацию такого запроса усложняют эквиваленты. Если посетитель обраща¬
ется по одному и тому же URL в промежутке времени, когда два идентификатора
пользователя связаны через эквиваленты (даже траизитивно), то такое посещение
следует считать как одно. Вновь поступивший эквивалент может изменить результа¬
ты любого запроса в течение любого промежутка времени по какому угодно URL.
Мы не будем пока что вдаваться в подробности обеих сравниваемых здесь
архитектур, поскольку для их понимания еще не рассмотрели многие понятия,
в том числе индексацию, распределенные базы данных, пакетную обработку,
алгоритм HyperLogLog и пр. Пояснять все эти понятия сразу было бы нецеле¬
сообразно. Вместо этого мы рассмотрим характеристики обеих сравниваемых
архитектур и их явные отличия. Самая лучшая из возможных полностью инкре¬
ментная архитектура подробно обсуждается в главе 10, а лямбда-архитектура —
в главах 8, 9, 14 и 15.
Обе архитектуры можно сравнить по трем критериям: безошибочность, за¬
держка и быстродействие. Во всех этих отношениях лямбда-архитектура намного
лучше. В обеих архитектурах необходимо производить аппроксимации, но в пол¬
ностью инкрементной архитектуре приходится делать более грубую аппроксима¬
цию с более высоким (в 3-5 раз) коэффициентом ошибок. Выполнение запросов
в полностью инкрементной архитектуре обходится намного дороже, что, в свою
очередь, сказывается на задержке и быстродействии. Но самое разительное от¬
личие обеих архитектур состоит в том, что в полностью инкрементной архитек¬
туре приходится применять специальное оборудование для достижения хотя бы
приемлемого быстродействия. В такой архитектуре необходимо запускать много
операций поиска с произвольным доступом для выполнения запросов, и поэтому
на практике во избежание заторов при поиске информации на диске приходится
применять твердотельные накопители.
Лямбда-архитектура позволяет получать решения с более высокой во всех от¬
ношениях производительностью, исключая сложности, которыми страдает полно¬
стью инкрементная архитектура. Ее главное преимущество проявляется в возмож¬
ности избежать ограничений, присущих полностью инкрементным вычислениям,
применяя совершенно другие методики. Рассмотрим далее, как этого добиться.
1.7. Лямбда-архитектура
Организовать выполнение произвольных функций над произвольным масси¬
вом данных в реальном времени - непростая задача. Полное решение этой зада¬
чи не дает ни одно из инструментальных средств в отдельности. Вместо этого
Глава 1. Новая парадигма для больших данных
41
для построения системы больших данных приходится пользоваться самыми раз¬
ными инструментальными средствами.
Основной замысел лямбда-архитектуры состоит
в построении систем больших данных в виде после¬
довательности уровней, как показано на рис. 1.6.
Каждый уровень удовлетворяет подмножеству свойств
и опирается на функциональные возможности, пре¬
доставляемые нижележащими уровнями. Разработке,
реализации и развертыванию каждого уровня можно
оыло бы посвятить отдельную книгу, но понять, каким Рис. 1.6. Лямбда-архитектура
образом на их основании строится система, нетрудно,
если рассматривать ее на уровне самых общих идей.
Все начинается с упоминавшегося ранее уравнения запрос = функция (все
данные). В идеальном случае функции можно вычислять оперативно для полу¬
чения результатов. Но даже если бы это и было возможно, то, к сожалению,
потребовало бы огромного количества ресурсов, а следовательно, обошлось бы
неоправданно дорого.
Допустим, что всякий раз, когда требуется ответить на чей-то запрос о его
текущем местоположении, необходимо прочитать петабайтовый (250 байт) мас¬
сив данных. Самым очевидным решением этой задачи является предварительное
вычисление функции запроса. Назовем предварительно вычисляемую функцию
пакетным представлением. Вместо оперативного вычисления запроса результаты
считываются из предварительно вычисленного представления. Такое представ¬
ление индексируется, чтобы сделать его доступным для произвольного чтения.
Такая система описывается следующими уравнениями:
пакетное представление = функция (все данные)
запрос = функция (пакетное представление)
Сначала в этой системе для всех данных выполняется функция с целью полу¬
чить пакетное представление. Затем требуется выяснить значение для запроса,
и поэтому для данного пакетного представления выполняется еще одна функция.
Пакетное представление дает возможность очень быстро получить из него тре¬
бующиеся значения, не просматривая его содержимое полностью.
Проиллюстрируем эти несколько абстрактные рассуждения на конкретном
примере. Допустим, требуется (снова) построить приложение для веб-аналити¬
ки, чтобы запрашивать количество просмотров страницы по указанному 1ЖЬ за
любой промежуток времени в днях. Если вычислять запрос в виде функции всех
данных, то пришлось бы просмотреть весь массив данных в поисках просмотров
страницы по указанному в заданном промежутке времени и возвратить под¬
счет результатов этих просмотров.
Вместо этого метод пакетного представления позволяет выполнить функ¬
цию для всех просмотров страницы, чтобы предварительно вычислить индекс
по ключу [иг1, день] и подсчитать количество просмотров страницы по ука¬
занному 1ЖЬ в течение заданного дня. Затем для выполнения запроса из этого
представления извлекаются все значения по всем дням из заданного промежутка
времени и суммируются подсчеты для получения конечного результата. Такой
метод показан на рис. 1.7.
Уровень
ускорения
Уровень
обслуживания
Уровень пакетной
обработки
42
Глава 1. Новая парадигма для больших данных
Рис. 1.7. Архитектура уровня пакетной обработки
Очевидно, что в приведенном выше описании данного метода чего-то не
хватает. Ясно также, что создание пакетного представления должно быть опе¬
рацией, завершаемой с большой задержкой, поскольку она подразумевает вы¬
полнение функции для всех имеющихся данных. К моменту ее завершения будет
накоплено немало новых данных, отсутствующих в пакетных представлениях,
а запросы окажутся устаревшими на многие часы. Но пренебрежем этим недо¬
статком на время, чтобы устранить его в дальнейшем. Предположим, что нас
вполне устраивают запросы, устаревшие на несколько часов, и продолжим ис¬
следовать дальше идею предварительного вычисления пакетного представления
путем выполнения функции для всего массива данных.
1.7.1. Уровень пакетной обработки
Часть лямбда-архитектуры, реализующая уравнение пакетное представление =
функция (все данные), называется уровнем пакетной обработки. На уровне пакетной
обработки хранится главная копия массива данных и предварительно вычисляют¬
ся пакетные представления для этого главного массива данных (рис. 1.8). Главный
массив данных можно рассматривать как очень большой список записей.
Уровень
ускорения
Уровень
обслуживания
Уровень
пакетной обработки
1. Сохранение главного
массива данных
2. Вычисление произвольных
представлении
У
Рис. 1.8. Уровень пакетной обработки
На уровне пакетной обработки необходимо сделать следующее: сохранить
неизменяемый, постоянно растущий главный массив данных и вычислить про¬
извольные функции для этого массива. Такого рода обработку лучше всего вы¬
полнять, используя системы пакетной обработки. Каноническим примером
Глава 1. Новая парадигма для больших данных
43
подобных систем служит Hadoop. Именно этой системой мы и воспользуемся
в данной книге, чтобы проиллюстрировать принципы организации уровня па-
кетнои обработки.
В простейшей форме уровень пакетной обработки может быть представлен
в виде следующего псевдокода:
function runBatchLayer():
while(true):
recomputeBatchViews()
Уровень пакетной обработки выполняется в цикле while (true) и постоянно
повторяет вычисление пакетных представлений с самого начала. В действитель¬
ности уровень пакетной обработки немного сложнее, но мы еще дойдем до этого
далее в книге. А до тех пор уровень пакетной обработки лучше всего рассматри¬
вать именно таким образом.
Уровень пакетной обработки примечателен тем, что он прост в употреблении.
Пакетные вычисления пишутся подобно однопоточным программам, а параллелизм
достигается автоматически. Написать надежные, масштабируемые в высокой степе¬
ни вычисления на уровне пакетной обработки совсем не трудно. Масштабирование
на уровне пакетной обработки осуществляется добавлением новых машин.
Ниже приведен пример вычислений на уровне пакетной обработки. Понимать
код из этого примера совсем не обязательно. Его назначение — показать, каким
образом выглядит параллельная по своей сути программа.
Api.execute (Api.hfsSeqfile("/tmp/pageview-counts"),
new Subquery("?url", "?count")
.predicate(Api.hfsSeqfile("/data/pageviews"),
"?url", "?user", "? timestamp")
.predicate (new Count(), "?count");
В коде из данного примера вычисляется количество просмотров страницы
по каждому URL из заданного входного массива исходных данных о просмотрах
страниц. Этот код любопытен тем, что все вопросы планирования параллельно¬
го выполнения работы и объединения результатов разрешаются автоматически.
Благодаря именно такому написанию алгоритма его можно произвольно распре¬
делить по всему кластеру MapReduce, но масштабируя на столько узлов, сколько
имеется в наличии. В конце вычисления выходной каталог будет содержать ряд
файлов с полученными результатами. О том, как писать подобные программы,
речь пойдет в главе 7.
1.7.2. Уровень обслуживания
В результате функционирования уровня пакетной обработки формируют¬
ся пакетные представления. Следующий шаг состоит в загрузке представлений
в такое место, где их можно было бы запросить. И для этой цели предназначен
уровень обслуживания. На этом уровне находится специализированная база дан¬
ных, загружающая пакетное представление и допускающая в нем произвольное
чтение информации (рис. 1.9). Как только становятся доступными новые пакет¬
ные представления, они автоматически сменяют на уровне обслуживания преды¬
дущие представления, чтобы сделать доступными более актуальные результаты.
44
Глава 1. Новая парадигма для больших данных
Уровень
ускорения
Уровень
обслуживания
Уровень
пакетной обработки
1 Произвольный ДОС гуп
к пакетным предстанлениям
2 Обновление представлений
на уровне пакетной обработки
у'
Рис. 1.9. Уровень обслуживания
База данных на уровне обслуживания поддерживает обновления и операции
произвольного чтения информации, а самое главное — она не требует поддерж¬
ки операций произвольной записи. И это очень важно, поскольку именно опе¬
рации произвольной записи информации обусловливают в основном сложность
баз данных. Базы данных на этом уровне оказываются чрезвычайно простыми,
потому что они не поддерживают произвольной записи. Благодаря своей просто¬
те они надежны, предсказуемы, просты в настройке и эксплуатации. Так, для ор¬
ганизации на уровне обслуживания базы данных Е1ерЬатБВ, рассматриваемой
в данной книге, требуется лишь несколько тысяч строк кода.
1.7.3. Уровни пакетной обработки и обслуживания обеспечивают
почти все свойства информационной системы
На уровнях пакетной обработки и обслуживания поддерживаются произволь¬
ные запросы произвольных массивов данных, хотя и за счет того, что такие запро¬
сы потеряют свою актуальность через несколько часов. Новому фрагменту данных
потребуется несколько часов для распространения через уровень пакетной обра¬
ботки на уровень обслуживания, где они могут потребоваться. Следует, однако,
иметь в виду, что, помимо малой задержки обновлений, уровни пакетной обработ¬
ки и обслуживания обеспечивают практически все свойства, требующие в системе
больших данных и упоминавшиеся в разделе 1.5. Эти свойства перечислены ниже.
■ Надежность и отказоустойчивость. Система Наскюр обеспечивает вос¬
становление после отказа в случае выхода машины из строя. А на уровне
обслуживания применяется внутренний механизм репликации, чтобы обе¬
спечить доступность информации, когда серверы выходят из строя. Кроме
того, на уровнях пакетной обработки и обслуживания обеспечивается устой¬
чивость к ошибкам, обусловленным человеческим фактором. Когда совер¬
шается подобная ошибка, можно откорректировать применяемый алгоритм
или удалить искаженные данные и вычислить представления заново.
■ Масштабируемость, Легко достигается как на уровне пакетной обработки,
так и на уровне обслуживания. Системы на обоих этих уровнях являются
полностью распределенными, и поэтому для их масштабирования доста¬
точно ввести новые машины.
■ Обобщение. Архитектура описывается как можно более обобщенной. Это
дает возможность вычислять и обновлять произвольные представления
произвольных массивов данных.
Глава Г Новая парадигма для больших данных
45
асширяемость. Чтобы добавить новое представление, достаточно ввести
новую функцию для главного массива данных. А поскольку главный массив
данных может содержать произвольную информацию, то ввести новые
типы данных также не составит особого труда. А если потребуется откор¬
ректировать представление, это можно сделать, не беспокоясь о поддерж¬
ке нескольких версий представления в приложении. Достаточно вычис¬
лить все представление заново.
■ Запросы с произвольным доступом. На уровне пакетной обработки обе¬
спечивается внутренняя поддержка запросов с произвольным доступом.
Все данные удобно доступны в одном месте.
■ Минимальное сопровождение. Главным компонентом для сопровождения
такой системы служит Нас1оор. Для работы с системой Наскюр требуются
знания и навыки администратора, хотя она довольно проста в обращении.
Как пояснялось ранее, базы данных на уровне обслуживания просты, по¬
скольку в них не выполняются операции произвольной записи информации.
Благодаря тому что у серверов баз данных на уровне обслуживания совсем
немного подвижных частей, их отказы намного менее вероятны, а следова¬
тельно, сопровождать базы данных на этом уровне намного проще.
■ Отлаживаемость. На уровне пакетной обработки всегда имеются входные
и выходные данные выполняемых вычислений. В традиционной базе дан¬
ных выходные данные могут заменить первоначальные входные данные,
например, при инкрементировании значения. На уровнях пакетной об¬
работки и обслуживания в качестве входных данных служит главный мас¬
сив, а в качестве выходных данных — представления. Аналогично входные
и выходные данные имеются и на всех промежуточных стадиях. Наличие
входных и выходных данных дает возможность получать информацию, не¬
обходимую для отладки возникающих неполадок.
Уровни пакетной обработки и обслуживания примечательны тем, что обеспе¬
чивают почти все требующиеся свойства информационной системы простым и по¬
нятным способом. Благодаря этому исключается необходимость решать вопросы
распараллеливания операций, а масштабирование осуществляется тривиально.
Единственным недостающим свойством в данном случае оказывается малая задерж¬
ка обновлений. Но этот недостаток восполняется на последнем, уровне ускорения.
1.7.4. Уровень укорення
Всякий раз, когда на уровне пакетной обработки завершается предваритель¬
ное вычисление пакетного представления, на уровне обслуживания происходит
обновление. Это означает, что в пакетном представлении оказываются только те
данные, которые поступили в ходе предварительного вычисления. Для того что¬
бы организовать информационную систему, полностью работающую в реальном
времени, т.е. выполнять произвольные функции над произвольными данными
в реальном времени, достаточно компенсировать задержку данных за несколько
последних часов. В этом и состоит назначение уровня ускорения. Как подразумева¬
ет название этого уровня, он предназначен для того, чтобы обеспечить представ¬
ление новых данных с использованием функций запросов, как только они потре¬
буются в приложении (рис. 1.10).
46
Глава 1. Новая парадигма для больших данных
1, Компенсацияоолыиой ..адержки
обнов пений на уровне обслуживания
2 Быстрые алгоритмы инкрементных
вычислений
3 Уровень пакетной обработки в конечном
итоге замещает уровень ускорения
Рис. 1.10. Уровень ускорения
Уровень ускорения подобен уровню пакетной обработки в том отношении,
что представления на этом уровне получаются, исходя из получаемых данных.
Главное отличие уровня ускорения состоит в том, что на нем рассматриваются
только последние данные, тогда как на уровне пакетной обработки — все дан¬
ные сразу. Еще одно немаловажное отличие состоит в том, что для достижения
как можно меньших задержек новые данные не рассматриваются на уровне уско¬
рения все сразу. Напротив, представления обновляются в реальном времени
по мере поступления новых данных вместо вычисления представлений заново,
как это происходит на уровне пакетной обработки. На уровне ускорения выпол¬
няются инкрементные вычисления вместо повторных вычислений, выполняе¬
мых на уровне пакетной обработки.
Поток данных на уровне ускорения можно формально представить с помо¬
щью следующего уравнения:
преставление в реальном времени = функция (преставление в реальном времени,
новые данные)
Представление в реальном времени обновляется, исходя из новых данных
и существующего представления в реальном времени. Лямбда-архитектуру можно
полностью свести к следующим трем уравнениям:
пакетное представление = функция (все данные)
преставление в реальном времени = функция (преставление в реальном времени,
новые данные)
запрос = функция (пакетное представление.преставление в реальном времени)
Графическое представление идей, положенных в основу лямбда-архитектуры,
приведено на рис. 1.11. Разрешение запросов осуществляется не просто с по¬
мощью функции пакетного представления, а путем анализа как пакетного пред¬
ставления, так и представления в реальном времени и объединения полученных
результатов.
На уровне ускорения применяются базы данных, поддерживающие операции
произвольного чтения и записи информации. А поскольку в этих базах данных
поддерживаются операции произвольной записи, то они на несколько порядков
сложнее, чем базы данных, применяемые на уровне обслуживания, как с точки
зрения реализации, гак и эксплуатации.
Лямбда-архитектура примечательна тем, что как только данные пройдут через
уровень пакетной обработки на уровень обслуживания, соответствующие резуль¬
таты уже будут не нужны для представлений, формируемых в реальном времени.
Это означает, что фрагменты представления в реальном времени могут быть
Уровень
ускорения
Уровень
обслуживания
Уровень
пакетной обработки
Глава /. Новая парадигма для больших данных
47
отвергнуты, поскольку они больше не нужны, И это замечательный результат,
поскольку уровень ускорения намного сложнее, чем уровени пакетной обработ¬
ки и о служивания. Такое свойство лямбда-архитектуры называется изоляцией
сложности, а э го означает, что сложность переносится на уровень, где находятся
только временные результаты. Если же что-нибудь пойдет не так, то можно от¬
клонить состояние всего уровня ускорения, чтобы привести все в норму через
несколько часов.
Уровень ускорения
Представление
^в реальном
времени
Представление
^в реальном
времени
Представление
к в реальном
Г времени
Уровень пакетной обработки
Главный массив данных
с
Уровень обслуживания
Пакетное
Сор пред-
Чг' ставление
-л* Пакетное
Сор пред-
Чл>ставление
Пакетное
Сор пред-
м-г^ставление
ч
/
/
Рис. 1.11. Блок-схема лямбда-архитектуры
Продолжим рассмотрение примера построения приложения для веб-анали¬
тики, где поддерживаются запросы на подсчет количества просмотров страниц
за определенный период в днях. Напомним, что на уровне пакетной обработки
получаются пакетные представления количества просмотров страниц по ключу
[иг1, день].
На уровне ускорения поддерживается отдельное представление количества
просмотров страниц по ключу [иг1, день]. Если на уровне пакетной обработ¬
ки его представления вычисляются заново путем буквального подсчета количе¬
ства просмотров страниц» то на уровне ускорения его представления обновля¬
ются путем инкрементирования их подсчета всякий раз, когда в представление
поступают новые данные. Чтобы разреши 1ь запрос, нужно сначала обратиться
к пакетным представлениям, а также к представлениям, формируемым в реаль¬
ном времени, по мерс необходимости удовлетворить указанному диапазону дат,
48
Глава 1. Новая парадигма для больших данных
а затем просуммировать результаты для получения окончательного подсчета.
Для правильной синхронизации результатов придется немного потрудиться, но
об этом речь пойдет в одной из последующих глав.
Некоторые алгоритмы с трудом поддаются инкрементному вычислению.
Разделение на уровни пакетной обработки и ускорения дает удобную возмож¬
ность применить конкретный алгоритм на уровне пакетной обработки и ап¬
проксимировать алгоритм на уровне ускорения. Уровень пакетной обработки
неоднократно замещает уровень ускорения, чтобы была откорректирована ап¬
проксимация, а в системе проявилось свойство окончательной безошибочности.
Например, вычисление однозначных подсчетов может быть затруднено, если
массивы однозначных данных укрупняются. На уровне пакетной обработки
нетрудно сделать однозначный подсчет, поскольку все данные анализируются
сразу, но на уровне ускорения можно воспользоваться для аппроксимации ал¬
горитмом HyperLogLog.
В итоге получается обоюдовыгодное решение, дающее требующуюся произ¬
водительность и надежность. Система, которая выполняет точное вычисление
на уровне пакетной обработки и аппроксимацию на уровне ускорения, прояв¬
ляет окончательную безошибочность, поскольку на уровне пакетной обработки
корректируется то, что вычисляется на уровне ускорения. Обновления по-преж¬
нему происходят с малой задержкой, но поскольку уровень ускорения является
переходным, то сложность достижения такого результата не оказывает влияния
на надежность получаемых результатов. Переходный характер уровня ускорения
дает удобную возможность не идти на компромиссы в отношении производи¬
тельности. Разумеется, система должна обеспечить полную точность вычисле¬
ний, которые могут быть выполнены инкрементным способом.
1.8. Современные тенденции в развитии технологий
Полезно знать технологии, положенные в основу инструментальных средств,
упоминаемых в данной книге. За последнее время наметился ряд тенденций
в развитии технологий, оказавших значительное влияние на способы построе¬
ния систем больших данных.
1.8.1. Быстродействие ЦП не повышается
Мы фактически приблизились к физическим пределам быстродействия от¬
дельно взятого центрального процессора (ЦП). Это означает, что если систему
требуется масштабировать для хранения и обработки большего объема данных,
то вычисления придется распараллеливать.
Это привело к появлению неразделяемых параллельных алгоритмов и соот¬
ветствующих систем вроде MapReduce. Вместо того чтобы пытаться решить зада¬
чу масштабирования информационной системы, приобретя более совершенную
машину, т.е. осуществить так называемое вертикальное масштабирование, достаточ¬
но ввести в систему дополнительные машины, т.е. осуществить так называемое
горизонтальное масштабирование.
49
Глава L Новая парадигма для больших данных
1.8.2s Эластичные облака
Еще одна тенденция в развитии технологии состоит в появлении эластичных
облаков, которые иначе называются инфраструктурой как услугой (Infrastructure as
a Service, IaaS). К числу наиболее примечательных эластичных облаков относятся
веб-службы компании Amazon (AWS — Amazon Web Services). Эластичные обла¬
ка позволяют брать в аренду оборудование по мере необходимости, вместо того
чтобы владеть собственным оборудованием в месте своего расположения. Они
также позволяют практически мгновенно увеличивать или уменьшать размеры
кластера. 1ак, если требуется выполнить крупное задание, для этой цели можно
временно выделить необходимое оборудование.
Эластичные облака значительно упрощают администрирование систем. Они
предоставляют также дополнительные возможности для хранения информации
и выделения оборудования, существенно снижая стоимость инфраструктуры.
Например, веб-службы AWS обладают свойством, называемым спотовым предло¬
жением (аукцион), и позволяют вам предлагать свою цену, а не платить фикси¬
рованную цену за товар. Если кто-нибудь предложит более высокую цену, чем
ваша, вы потеряете возможность приобрести товар. Спотовые предложения
могут исчезнуть в любой момент, поскольку они, как правило, оказываются де¬
шевле, чем обычные товары. Для таких систем распределенных вычислений,
как MapReduce, спотовые предложения предоставляют отличную возможность,
поскольку отказоустойчивость системы обеспечивается на программном уровне.
1.8.3. Эффектная экосистема с открытым кодом для больших данных
Сообщество разработчиков открытого кода создало за последние годы нема¬
ло технологий для больших данных. Все технологии, упоминаемые в данной кни¬
ге, являются открытыми и свободно доступными для применения.
Рассматриваемые здесь проекты с открытым кодом делятся на пять перечис¬
ленных ниже категорий. Напомним, однако, что назначение данной книги — не
только представить различные технологии, но и разъяснить основополагающие
принципы, позволяющие правильно оценить и выбрать подходящие для приме¬
нения инструментальные средства.
■ Системы пакетных вычислений. Обладают высокой производитель¬
ностью, но и большой задержкой. Такие системы позволяют выполнять
практически любые вычисления, хотя для этого может потребоваться не
один час, а то и день. Единственной системой пакетных вычислений, кото¬
рая употребляется в примерах из данной книги, является Iladoop. Проект
Hadoop состоит из двух подчиненных проектов: Hadoop Distributed File
System (HDFS) и Hadoop MapReduce. В частности, HDFS представляет со¬
бой распределенную, отказоустойчивую систему хранения, масштабируе¬
мую до петабайт (250 байт) данных, a MapReduce - масштабируемый по го¬
ризонтали каркас вычислений, интегрируемый с IIDFS.
■ Каркасы сериализации. Предоставляют инструментальные средства и би¬
блиотеки для применения объектов на разных языках программирования.
Они позволяют сериализировать объект в массив байтов из исходного
кода на одном языке, а затем десериализировать этот массив в объект
50
Глава /. Новая парадигма для больших данных
на другом языке. Каркасы сериализации предоставляют язык определения
схемы (Schema Definition Language), позволяющий определять объекты
и их ноля, а также механизмы для безопасного отслеживания версий объ¬
ектов и дальнейшего развития схемы, не делая существующие объекты не¬
достоверными. К числу наиболее примечательных каркасов сериализации
относятся Apache Thrift, Protocol Buffers и Avro.
■ Базы данных типа NoSQL с произвольным доступом. За последние
годы было создано немало распределенных нереляционных баз данных
(NoSQL). К их числу относятся Cassandra, HBase, MongoDB, Voldemort,
Riak, CouchDB и другие базы данных, и все они обладают следующим об¬
щим свойством: в них полностью принесена в жертву выразительность
языка SQL, и вместо этого они специализируются на определенного рода
операциях. Все они имеют разную семантику и предназначены для особых
целей, а не для хранения произвольных данных. Во многих отношениях
выбор базы данных типа NoSQL для употребления аналогичен выбору меж¬
ду хеш-отображением, отсортированным отображением, связным списком
или вектором в качестве структуры данных для применения в программе.
Если вы заранее знаете, что собираетесь делать, то выбрать соответствую¬
щую базу данных вам будет нетрудно. В примере приложения, построение
которого рассматривается в данной книге, будет употреблена база данных
Cassandra.
■ Системы обмена сообщениями и постановки их в очередь.
Предоставляют средства для обмена сообщениями между процессами от¬
казоустойчивым и асинхронным способом. Очередь сообщений является
главным компонентом обработки данных в реальном времени. В примерах
из данной книги употребляется система Apache Kafka.
■ Система вычислений в реальном времени. Такие системы имеют высо¬
кую производительность, малую задержку и выполняют потоковую обра¬
ботку данных. Они не могут выполнять операции, на которые способны
системы пакетной обработки. Но в то лее время такие системы могут очень
быстро обрабатывать сообщения. В примерах из данной книги употребля¬
ется система Apache Storm, топология и масштабирование которой осу¬
ществляется очень просто.
По мере развития перечисленных выше категорий проектов с открытым
кодом вокруг них образовались компании для поддержки на уровне предприя¬
тий. Так, компания Cloudera предоставляет поддержку проекта Hadoop, а ком¬
пания DataStax — поддержку проекта Cassandra. Остальные проекты являются
продукцией отдельных компаний. Например, Riak — это продукт компании
Basho Technologies, MongoDB — продукт компании lOgen, a RabbitMQ— продукт
SpringSource, подразделения компании VMWare.
1.9. Пример приложения SuperWebAnalytics.com
На протяжении данной книги нам предстоит построить приложение, что¬
бы проиллюстрировать на его примере основные принципы построения си¬
стем больших данных. В частности, мы построим уровень управления данными
Глава 1. Новая парадигма для больших данных
51
для службы веб-аналитики, аналогичной Google Analytics. Эта служба должна
быть в состоянии отслеживать миллиарды просмотров страниц за день.
Такая служба должна обеспечивать самые разные числовые показатели.
Каждый такой показатель будет поддерживаться в реальном времени. Числовые
показатели разнятся от простых подсчетов до сложного анализа посещений
веб-сайтов. Ниже перечислены числовые показатели, поддерживаемые в рассма¬
триваемом здесь приложении.
■ Подсчет просмотров страниц по URL во времени. Примером тому слу¬
жат запросы на подсчет количества просмотров страниц каждый день за
прошедший год или за последние 12 часов.
■ Количество индивидуальных посетителей по URL во времени.
Примером тому служат запросы на подсчет количества индивидуальных
посетителей отдельного домена за 2010 год или каждый час за три послед¬
них года.
■ Анализ показателя отказов от просмотра страниц. Примером тому слу¬
жат запросы на определение доли посетителей страницы, не просматри¬
вавших остальные страницы на данном веб-сайте.
Таким образом, нам предстоит построить уровни для хранения, обработки
и обслуживания запросов, направляемых приложению.
Резюме
В этой главе были рассмотрены недостатки масштабирования реляционных
систем традиционными методами вроде фрагментации базы данных. Эти недо¬
статки не ограничиваются только масштабированием по мере усложнения систе¬
мы в отношении управления, расширения и даже ее понимания. По мере рас¬
смотрения особенностей построения систем больших данных в последующих
главах мы уделим в одинаковой степени немало внимания вопросам надежности
и масштабируемости. Как будет показано далее, при правильном подходе к по¬
строению надежность и масштабируемость достигаются в одной и той же ин¬
формационной системе.
Преимущества построения информационной системы с помощью лямбда-ар¬
хитектуры не ограничиваются только масштабированием. Такая система будет
способна обрабатывать намного большие объемы данных, что позволит собирать
еще больше данных, извлекая из них дополнительную пользу. Увеличение объема
и разнообразие типов сохраняемых данных даст больше возможностей для их
проходки, анализа и построения новых приложений.
Еще одно преимущество применения лямбда-архитектуры состоит в обеспе¬
чении надежности приложений. Для этого имеется немало причин. Например,
у разработчиков должна быть возможность выполнять вычисления над всем
массивом данных в целом для переноса данных или устранения неполадок.
Разработчикам придется иметь дело с ситуациями, где одновременно имеется
несколько активных вариантов схемы. Когда они изменяют схему, то имеют
возможность обновлять все данные в новую схему. Аналогично, если в рабо¬
чей системе случайно развернут неверный алгоритм и обслуживаемые данные
52
Глава /. Новая парадигма для больших данных
искажены, эту оплошность нетрудно устранить, вычислив заново искаженные
значения. Как будет показано далее, существует немало других причин для повы¬
шения надежности приложений.
И наконец, более прогнозируемой должна быть производительность.
Несмотря на то что лямбда-архитектура в целом является обобщенной и гибкой,
отдельные компоненты, составляющие систему, имеют специальный характер.
Внутри системы не происходит ничего “волшебного” в сравнении с чем-то вроде
планировщика запросов Это приводит к более прогнозируемой производи¬
тельности.
Если материал этой главы покажется вам не совсем понятным, не отчаивай¬
тесь. Далее вам предстоит изучить еще немало, и по мере изучения материала
книги вы можете возвращаться к этой главе. В частности, из следующей главы вы
узнаете, как построить лямбда-архитектуру. И начнем мы с рассмотрения основы
моделирования и схематизации основной копии массива данных.
Часть I
Уровень пакетной
обработки
и
Хасть I посвящена уровню пакетной обработки лямбда-архитектуры. В ней те¬
оретические главы перемежаются с иллюстративными.
В главе 2 обсуждаются вопросы моделирования и структурирования данных
в главном массиве данных. А в главе 3 иллюстрируются принципы применения
инструментального средства Apache Thrift.
В главе 4 обсуждаются требования к хранению главного массива данных.
В ней будет показано, что многие функциональные средства, которые, как прави¬
ло, предоставляются в решениях баз данных, не требуются для главного массива
данных и в действительности только мешают оптимизации хранилища главного
массива данных. Чем проще и менее функционально насыщеннее решение, тем
лучше оно удовлетворяет поставленным требованиям. А в главе 5 иллюстрирует¬
ся практический пример построения хранилища главного массива данных с по¬
мощью файловой системы Hadoop Distributed Filesystem.
В главе 6 обсуждаются вопросы вычисления произвольных функций в глав¬
ном массиве данных с помощью парадигмы MapReduce. Для вычисления любой
масштабируемой функции парадигма MapReduce оказывается достаточно обоб¬
щенной. Несмотря на всю эффективность MapReduce, в этой главе будет показа¬
но, что чем выше уровень абстракции, тем проще пользоваться данной парадиг¬
мой. А в главе 7 демонстрируется эффективность высокоуровневой абстракции
MapReduce, называемой JCascalog.
Все представленные ранее понятия сводятся вместе в главах 8 и 9, где де¬
монстрируется полная реализация уровня пакетной обработки для приложения
SuperWebAnalytics.com, пример которого рассматривается в данной книге. Так,
в главе 8 будут показаны общая архитектура и алгоритмы, тогда как в главе 9
подробно анализируется реализующий их рабочий код.
Модель данных
для больших данных
В этой главе...
■ Свойства данных.
■ Информационная модель, основанная на фактах.
■ Преимущества информационной модели
для больших данных.
■ Граф-схемы.
В предыдущей главе были рассмотрены недостатки применения традицион¬
ных инструментальных средств для построения информационных систем, а так¬
же основные принципы разработки их более совершенных структур. Кроме
того, в предыдущей главе было показано, что каждую информационную систему
можно представить как функцию вычисления данных. В ней были также рассмо¬
трены основные составляющие лямбда-архитектуры, предоставляющей практи¬
ческий способ реализации произвольной функции для обработки произвольных
данных в реальном времени.
В основу лямбда-архитектуры положен главный массив данных (рис. 2.1), ко¬
торому придается первоочередное значение в лямбда-архитектуре. Даже если
потерять все массивы данных на уровнях обслуживания и ускорения, работоспо¬
собность приложения можно восстановить из главного массива данных. Дело
в том, что пакетные представления, обрабатываемые на уровне обслуживания,
получаются с помощью функций в главном массиве данных, а уровень ускорения
опирается только на самые последние данные, и поэтому он может восстано¬
виться в течение нескольких часов.
56
Часть I. Уровень пакетной обработки
© Данные
представлений
в реальном времени
на уровне ускорения
имеют большую
скорость обмена,
и поэтому ошибки
быст ро исключают са
Уровень ускорения
Представление
ЯГ в реальном
г времени
Представ линии
Ш в реальном
г времени
Представление
ЯГ в реальном
г времени
О Главному массиву
данных н лямбда-
архитектуре
придается первооче¬
редное значение,
однако он не защищен
07 искажения
Уровень пакетной обработки
Главный массив данных
ж
Уровень обслуживания
\
Пакетное
СОО пред*
^члавлвние
Пакетное
505 пред-
Чл->ставлпние
Пакетное
СО? пред-
^-^ставление
I 11
© Любые ошибки, вноси¬
мые на уровнях
обслуживания и пакет¬
ной обработки, пере¬
записываются,
поскольку они продол¬
жаю 1 восстлнавликать -
оя из главного массива
Рис. 2.1. Главному массиву данных в лямбда-архитектуре придается первоочередное значение
в системах больших данных. Ошибки, возникающие на уровнях обслуживания и ускорения,
могут быть исправлены, но восстановить искаженный главный массив данных нельзя
Главный массив данных является единственным компонентом лямбда-архи¬
тектуры, требующим абсолютной защиты от искажения. Перегрузка машин, сбои
дисков и отключения электропитания — все эти факторы могут вызвать ошибки,
не говоря уже о практической неизбежности и риске возникновения ошибок,
обусловленных человеческим фактором и столь характерных для динамических
информационных систем. Поэтому главный массив данных нужно проектиро¬
вать очень тщательно, чтобы предотвратить его искажение во всех подобных
случаях. Ведь отказоустойчивость является важным залогом нормального функ¬
ционирования информационной системы в долгосрочной перспективе.
Главный массив данных состоит из двух компонентов: применяемой модели
данных и средства для физического хранения этого массива данных. Эта глава
посвящена разработке модели данных для главного массива данных и ее свойств.
А о том, как физически хранить этот массив данных, речь пойдет в следующей
главе.
В этой главе вам предстоит сделать следующее.
■ Изучить основные свойства данных.
■ Узнать, каким образом эти свойства поддерживаются в модели, основан¬
ной на фактах.
Глава 2. Модель данных для больших данных
57
■ Рассмотреть преимущества модели, основанной на фактах, для главного
массива данных.
■ Выразить модель, основанную на фактах, с помощью граф-схем.
Итак, начнем с обсуждения довольно общего термина данные.
2.1. Свойства данных
В соответствии с прикладным характером данной книги мы сосредоточим об-
суждение свойств данных вокруг примера конкретного приложения. Допустим,
вы разрабатываете очередную крупную социальную сеть под названием РасеБрасе.
Когда новый пользователь (назовем его Томом) посещает сайт вашей социальной
сети и присоединяется к ней, он приветствует своих родных и друзей. Какую же
информацию следует хранить о связях Тома в данной сети? Для этого имеется
немало вариантов выбора, в том числе следующие:
■ последовательность событий, связанных с включением лиц в список дру¬
зей Тома и исключением их из этого списка;
■ текущий список друзей Тома;
■ текущее количество друзей Тома.
Все эти варианты выбора и взаимосвязи между ними приведены на рис. 2.2.
Изменения
в списке друзей
Рис. 2.2. Три возможных варианта хранения информации о дружеских отношениях
пользователя социальной сети РасеБрасе. Каждый вариант выбора может быть произведен
из находящегося слева от него, но это односторонний процесс
Данный пример иллюстрирует информационную зависимость. Следует заме¬
тить, что каждый последующий уровень информации может быть произведен
из предыдущего (находящегося слева от него), но этот процесс односторонний.
Из последовательности событий, связанных с включением лиц в список друзей
и исключением их из этого списка, можно определить другие величины. Но если
в вашем распоряжении имеется только количество друзей Тома, то определить
точно, кто они, невозможно. Аналогично из списка текущих друзей невозможно
58
Часть /. Уровень пакетной обработки
определить, дружил ли раньше Том с Джерри, или же социальная сеть Тома была
организована совсем недавно.
Понятие информационной зависимости формирует определения следующих
терминов, употребляемых в данной книге.
■ Информация — общая коллекция релевантных знаний, имеющих отношение
к системе больших данных. Служит синонимом общеупотребительного тер¬
мина данные.
■ Данные — обозначают первичную информацию, которую нельзя больше
получить из другого источника. Данные служат аксиомой, от которой про¬
исходит все остальное.
■ Запросы — вопросы, которые ставятся перед данными. Например, запрос
предыстории финансовых транзакций делается с целью определить оста¬
ток на банковском счете.
■ Представления — информация, получаемая из первичных данных. Они
формируются как вспомогательное средство, дополняющее ответы на кон¬
кретные типы запросов.
Информационная зависимость между данными, представлениями и запроса¬
ми в социальной сети РасеЗрасе приведена на 2.3.
О Данные — это
информация, которую
нельзя больше
получить из других
источников
© Представления
вычисляются из данных,
чтобы помочь ответить
на запросы
© Запросы,на которые
следует ответить,
требуют доступа
к информации,
хранящейся
в представлениях
Рис. 2.3. Информационная зависимость между данными, представлениями и запросами
Следует также заметить, что данные об одном человеке могут быть представ¬
лением о другом. Допустим, социальная сеть РасеБрасе становится весьма попу¬
лярной и рекламная фирма создает поисковый робот для формирования демо¬
графического среза из профилей пользователей. В социальной сети ГасеЗрасе
доступна вся информация, предоставленная Томом, включая дату его рождения
13 марта 1984 года. Но Том, желая скрыть свой возраст, указал в качестве даты сво¬
его рождения только 13 марта в своем общедоступном профиле. День его рожде¬
ния является представлением с точки зрения РассЯрасе, поскольку он происходит
от даты его рождения, но для рекламодателя это данные, поскольку они содержат
ограниченную информацию о Томе. Такая взаимосвязь показана на рис. 2.4.
Глава 2. Модель данных для больших данных
59
Социальная сеть РасеБрасе
Рекламодатель
О Том предоставляет подробную
информацию о себе в своем профиле
в социальной сети РасеБрасе
(т.е. данные), но решает ограничить
то, что должно быть общедоступно
(т.е. представления)
© После выскабливания профиля
Тома рекламодатель получает
только общедоступную информацию
об этом пользователе
Рис. 2.4. Классифицирование информации в качестве данных или представления зависит
от конкретной точки зрения. Для социальной сети РасеБрасе день рождения Тома
является представлением, поскольку он происходит от даты рождения этого пользователя.
А для стороннего рекламодателя день рождения Тома считается данными
Имея в своем распоряжении установившийся общедоступный словарь, мы мо¬
жем теперь внедрить следующие основные свойства данных: необработанность,
неизменяемость и бессрочность (или “непреложная истинность данных”). Эти три
свойства и соответствующие понятия служат основанием для правильного пони¬
мания систем больших данных.
Эти понятия могут быть не совсем ясны тем, кто имеет опыт работы с реля¬
ционными информационными системами. Как правило, информация постоянно
обновляется и подытоживается, чтобы отражать текущее состояние окружающе¬
го мира. Следовательно, ее неизменяемость или бессрочность не принимается
во внимание. Но такой подход ограничивает круг вопросов, на которые позво¬
ляют ответить имеющиеся данные, а также не дает возможность надежно исклю¬
чить ошибки и искажение данных. Внедряя упомянутые выше свойства в обла¬
сти больших данных, можно сделать систему более надежной и эффективной
по своим возможностям. Мы рассмотрим этот вопрос более подробно, обсуждая
далее необработанность данных.
2.1.1. Необработанность данных
Информационная система отвечает на вопросы об информации, приобретен¬
ной в прошлом. Разрабатывая систему больших данных, нужно суметь ответить
на как можно больше вопросов. Например, в социальной сети РассБрасе ваши
данные более ценны, чем данные рекламодателя, поскольку они позволяют выве¬
сти дополнительные сведения о Томе. Обычно такое свойство данных называют
необработанностью. Данные желательно хранить в как можно более необработан¬
ном виде. Чем более необработанными оказываются данные, тем больше вопро¬
сов можно им поставить.
Пример социальной сети РасеБрасс помогает проиллюстрировать ценность
свойства необработанности данных, но для его разъяснения мы рассмотрим
DU Часть L Уровень пакетной обработки
другой пример. Торги на фондовой бирже изобилуют разнообразной информа¬
цией о миллионах акций и миллиардах долларов, ежедневно переходящих из рук
в руки. В связи со столь интенсивными торгами цены на акции исторически реги¬
стрировались ежедневно в виде цены при открытии биржи, высокой цены, низ¬
кой цены, а также цены при закрытии биржи. Но эти фрагменты данных зачастую
не дают общего представления о положении дел на бирже и способны исказить
представление о том, что на ней происходит. Рассмотрим в качестве примера ко¬
тировки акций, приведенные на рис. 2.5, где зафиксированы цены на акции ком¬
паний Google, Apple и Amazon в тот день, когда компания Google объявила о вы¬
пуске новых программных продуктов, нацеленных на ее конкурентов.
Компания
Обозначение
Цена при
закрытии
накануне
Цена
при
открытии
Высокая
цена
Низкая
цена
Цена
при
закрытии
/ Чистов N
/ изменение',
/ цены \
Google
GOOG
564.68
567.70
573.99
566.02
569.30
I +4.62 |
Apple
AAPL
572.02
575.00
576.74
571.92
574.50
\ +2.48 j
Amazon
AMZN
225.61
225.01
227.50
223.30
225.62
' +0.01 /
В финансовых отчетах ежедневно фигурирует чистое
изменение цен на акции при закрытии биржи. — "
Какие выводы можно сделать из объявления
компании Google о выпуске новых продуктов?
Рис. 2.5. Итоги одного дня торгов акциями компаний Google, Apple и Amazon
Как следует из данных, приведенных на рис. 2.5, объявление компании Google
о выпуске новых программных продуктов никак не повлияло на курс акций ком¬
пании Amazon, цена которых едва изменилась. А на курс акций компании Apple
оно оказало положительный, хотя и незначительный эффект.
Но если получить доступ к данным, хранящимся при большой детализации
времени, то можно составить более ясное представление о события на фондовой
бирже в тот день и попытаться проследить возможные причинно-следственные
связи. На рис. 2.6 показаны поминутные относительные изменения цен на ак¬
ции всех трех упомянутых выше компаний. Из данных, приведенных на рис. 2.6,
можно сделать вывод, что объявление компании Google о выпуске новых про¬
граммных продуктов действительно повлияло на курс акций обеих компаний,
Amazon и Apple, причем первой из них в большей степени, чем второй.
Следует также заметить, что дополнительные данные могут вызвать новые
идеи, к которым вряд ли побудил бы анализ итоговых цен за день. Например, бо¬
лее подробные данные, приведенные на рис. 2.6, позволяют сделать вывод, что
объявление компании Google о выпуске новых программных продуктов все же
повлияло на курс акций компании Amazon на рынках планшетных компьютеров
и облачных вычислений.
Особая ценность сохранения необработанных данных состоит в том, что
все вопросы, на которые требуется найти ответы, редко известны заранее.
Сохраняя данные в как можно более необработанном виде, можно расширить
до максимального предела возможность получать новые представления, в то же
время подытоживая, перезаписывая или устраняя информационные границы
того, что могут сообщить данные. Но дело в том, что чем в большей степени
Глава 2. Модель данных для больших данных
61
необработанными оказываются данные, тем больше, как правило, а иногда и на¬
много больше, их требуется. Впрочем, технологии больших данных специаль¬
но предназначены для обращения с петабайтами и даже экзабайтами данных.
В частности, они позволяют сохранять данные распределенным, масштабиро¬
ванным способом, поддерживая в то же время возможность непосредственного
запроса данных.
Цены на акции компании Apple
оставались устойчивыми
в течение всего дня
О 00'« •AAPLf043^#GOQGH}82‘4
Цены на акции компании Google
немного выросли в день
объявлении о выпуске
новых продуктов
ч
V,
чу^-ч
Lçvj
\>
Ob'*
ч
О G**
We: J^n27
Цены на акции компании Amazon
немного упали к концу дня
Рис. 2.6. Относительные изменения цен на акции компаний Google, Apple и Amazon
27 июня 2012 года в сравнении с ценами при закрытии биржи 26 июня того же года
(www.google.com/finance). Ежедневные записи не поддерживаются краткосрочным
анализом, но его можно произвести, сохраняя данные чаще во времени
Несмотря на всю простоту рассматриваемого здесь понятия, не всегда ясно,
какую именно информацию следует хранить в виде необработанных данных.
Приведем пару примеров, чтобы направить принятие решения в нужное русло.
НЕСТРУКТУРИРОВАННЫЕ ДАННЫЕ НЕ ОБРАБОТАНЫ
В БОЛЬШЕЙ СТЕПЕНИ, ЧЕМ НОРМАЛИЗОВАННЫЕ ДАННЫЕ
Принимая решение о том, какие именно необработанные данные следует со¬
хранять, приходится различать неясную границу между синтаксическим анализом
и семанти'ческой нормализацией. Семантическая нормализация - это процесс реор¬
ганизации информации из свободной формы в структурированную форму данных.
Например, социальная сеть FaceSpace может запросить место жительства
Тома. Он может ввести какие-то данные в соответствующем поле, например San
Francisco, CA, SF, North Beach и т.п. Алгоритм семантической нормализации
попытается согласовать введенные данные с известным местом, как показано
на рис. 2.7.
Часть L Уровень пакетной обработки
64
Алгоритм нормализации
может и не распознать
Норт-Бич как часть
Сан-Франциско, но эти
данные могут быть уточнены
впоследствии
Рис. 2.7. Семантическая нормализация неструктурированных ответов о месте жительства пользователя
до города, штата и страны. Простой алгоритм нормализует входные данные "North Beach" до пустого
значения null, если он не распознает их как местность, находящуюся по соседству с Сан-Франциско
Если встретится форма неструктурированных данных, подобная упомянутой
выше строке, обозначающей место жительства пользователя, то следует ли со¬
хранять такие данные неструктурированными или приводить их в семантически
нормализованную форму? На наш взгляд, введенные данные лучше сохранить не¬
структурированными, поскольку алгоритм семантической нормализации может
быть со временем усовершенствован. Если сохранить введенные данные неструк¬
турированными, их можно нормализовать в дальнейшем, когда соответствующие
алгоритмы будут усовершенствованы. Если вернуться к предыдущему примеру,
то алгоритм семантической нормализации можно в дальнейшем адаптировать
к распознаванию местности Норт-Бич как части Сан-Франциско или воспользо¬
ваться информацией о соседней местности в других целях.
Когда данные следует сохранять неструктурированными. Как правило, если ал¬
горитм извлечения данных так же прост и точен, как, например, извлечение
информации о возрасте с НТМЕ-страницы, то сохранять следует результаты
выполнения этого алгоритма. Если же алгоритм подвергается изменениям
вследствие усовершенствований или расширения требований, данные следует
сохранять в неструктурированном виде.
БОЛЬШИЙ ОБЪЕМ ИНФОРМАЦИИ СОВСЕМ
НЕ ОЗНАЧАЕТ БОЛЬШУЮ НЕОБРАБОТАННОСТЬ ДАННЫХ
Легко допустить, что чем больше данных, тем больше их необработанность,
но так бывает далеко не всегда. Допустим, что Том ведет свой блог и ему требу¬
ется вводить рассылаемые сообщения в свой профиль БасеЯрасе. Что именно
следует сохранить, как только Том предоставит иЫ. своего блога?
Безусловно, сохранить можно только текст записей в блоге. Но любые фразы,
которые Том специально выделил курсивом, полужирным или крупным шрифтом,
могут оказаться полезными при анализе текста. Этой дополнительной информаци¬
ей можно, например, воспользоваться для индексации, чтобы организовать поиск
в социальной сети БасеЭрасе. Таким образом, аннотированные элементы текста на¬
ходятся в более необработанной форме данных, чем текстовые строки в коде АБСН.
С другой стороны, можно сохранить как данные всю НТМЬ-страницу блога
Тома. И хотя в байтах этой информации окажется значительно больше, цвето¬
вая схема, таблицы стилей и код ^уаБспр! не помогут извлечь дополнительную
информацию о Томе. Они служат лишь в качестве контейнера для содержимого
сайта, и поэтому их нс следует включать в необработанные данные.
63
Глава 2. Модель данных для больших данных
2.1.2. Неизменяемость данных
Неизменяемость может показаться на первый взгляд не совсем обычным
свойством данных тем, кто имеет опыт работы с реляционными базами данных.
Ведь обновление является одной из основополагающих операций в реляционных
и большинстве других баз данных, а для соблюдения неизменяемости данные
нельзя ни обновлять, ни удалять, а можно только дополнять новыми1. Тем не
менее применение неизменяемой схемы в системах больших данных дает следу¬
ющие важные преимущества.
■ Устойчивость к отказам, связанным с человеческим фактором. Это самое
главное преимущество модели неизменяемости данных. Как обсуждалось
в главе 1, устойчивость к отказам, связанным с человеческим фактором, явля¬
ется очень важным свойством информационных систем. Людям свойственно
ошибаться, и поэтому приходится ограничивать влияние подобных ошибок
и предусматривать механизмы для восстановления после них. Если применя¬
ется модель изменяемости данных, то совершенная ошибка может привести
к потере данных, поскольку значения фактически перезаписываются в базе
данных. А если применяется модель неизменяемости данных, то потери дан¬
ных исключаются. Ведь если записываются неверные данные, то предыдущие
(верные) данные по-прежнему остаются. Для устранения ошибок в такой ин¬
формационной системе достаточно удалить блоки неверных данных и вычис¬
лить заново представления, построенные из главного массива данных.
■ Простота. В моделях изменяемости данных подразумевается, что данные
должны каким-то образом индексироваться, чтобы можно было извлекать
и обновлять конкретные объекты данных. А в моделях неизменяемости
данных имеется только возможность присоединять новые блоки к главно¬
му массиву данных. Для этого не требуется индексация данных, что суще¬
ственно упрощает дело. Как поясняется в следующей главе, главный массив
данных сохраняется так же просто, как и плоский файл.
Преимущества сохранения неизменяемости данных становятся очевидными
в сравнении со схемой их изменяемости. Рассмотрим в качестве примера эле¬
ментарную схему изменяемости, приведенную на рис. 2.8. Ее можно применить
к социальной сети ЕасеБрасе.
Если Том переедет в Лос-Анджелес, запись с информацией о нем придется
обновить таким образом, чтобы она отражала его текущее место жительства. Но
в процессе этого обновления потеряются сведения о том, что Том некогда жил
в Сан-Франциско.
Совсем иначе обстоит дело, если применяется система неизменяемости. Вместо
хранения моментального снимка текущего состояния, как это делается в схеме
изменяемости, в данном случае создается отдельная запись всякий раз, когда из-
меняется информация о пользователе. Для этого необходимо внести два измене¬
ния. Во-первых, отследить каждое поле в информации о пользователе в отдельной
таблице. И во-вторых, привязать каждый блок данных к моменту времени, когда
1 В некоторых случаях данные мо1ут быть все же удалены, но это особые случаи, выходящие за
рамки повседневной работы с системой. Мы обсудим эти случаи в разделе 2.1.3.
64
Часть /. Уровень пакетной обработки
становится известно, что информация истинна. На рис. 2.9 приведена соответ¬
ствующая схема неизменяемости для социальной сети РасеБрасе.
Информация о пользователе
id
name
age
gender
employer
location
1
Alice
25
female
Apple
Atlanta, GA
2
Bob
36
male
SAS
Chicago, IL
3
Tom
28
male
Google
San Francisco, CA
4
Charlie
25
male
Microsoft
Washington, DC
Если Том переедет
и другой I ород,
ото значение будет
перезаписано
Рис. 2.8. Схема изменяемости для информации о пользователе в социальной сети РасеБрасе.
Когда изменяются отдельные подробности (например, Том переезжает в Лос-Анджелес),
предыдущие значения перезаписываются и теряются
.
Каждое поле
информации
о пользователе
хранится отдельно
У V
0
Каждая запись
получает отмену
времени. .-.01 да
она сохраняй! ся
Рис. 2.9. Эквивалентная схема неизменяемости для информации о пользователе в социальной
сети ЯасеБрасе. Каждое поле этой информации отслеживается в отдельной таблице, а в каждой ее
строке имеется отметка времени, когда становится известно, что информация истинна.
(Ради простоты гендерные данные и данные о работодателе опущены, хотя они сохраняются
аналогичным образом.)
Впервые Том присоединился к социальной сети ГасеБрасс 4 апреля 2012 года,
предоставив сведения для своего профиля. Момент времени, когда об этом стало
известно, отражен в отметке времени соответствующей записи. Когда же 17 июня
2012 года Том переехал в Лос-Анджелес, в таблицу места жительства была введена
новая запись с отметкой времени изменения его профиля (рис. 2.10).
Данные места жительства
user id
location
timestamp
1
Atlanta, GA
2012/03/29 08:12:24
2
Chicago, IL
2012/04/12 14:47:51
3
San Fr ancisco, CA
2012/04/04 18:31:24
4
Washington, DC
2012/04/09 11:52:30
Данные возраста
user id
age
timestamp
1
25
2012/03/29 08:12:24
2
36
2012/04/12 14:47:51
3
28
2012/04/04 18:31:24
4
25
2012/04/09 11:52:30
Данные имени
user id
name
timestamp
1
Alice
2012/03/29 08:12:24
2
Bob
2012/04/12 14.47:51
3
Tom
2012/04/04 18:31:24
4
Charlie
2012/04/09 11:52:30
Глава 2. Модель данных для больших данных
65
.Овннивмрста жительства
имгЮ
кквЬоп
ИтеМатр
1
АІІата, ЗД
2012/03/29 08:12:24
2
СЫсадо, И
2012/04/12 14:47:51
3
Эап Ргапсівсо, СА
2012/04/04 18:31:24
4
^МавМгфоп, ОС
2012/04/09 11:52:30
3
І-Ов АлдеІеБ, СА
2012/06/17 20:09:48
...
...
0 Первоначальная информация,
1 предоставленная Томом
I (с идентификатором
/ пользователя 3), получила
' отметку времени, когда
он впервые присоединился
к социальной сети РасеБрасе
© А позднее, когда Том переехал
/ на новое место жительства,
' в таблицу была введена
дополнительная запись
с отметкой времени
о получении новых данных
Рис. 2.10. Вместо обновления уже существующих записей в схеме неизменяемости используются
новые записи для представления измененной информации. Таким образом, схема неизменяемости
позволяет хранить несколько записей для одного и того же пользователя.
(Остальные таблицы здесь не показаны, поскольку они не изменились.)
Теперь имеются две записи о месте жительства Тома (с идентификатором поль¬
зователя #3). Обе они могут быть истинны, поскольку блоки данных привязаны
к конкретным моментам времени. Текущее место жительства Тома подразумева¬
ет простой запрос данных на поиск всех мест его жительства и выбор одной из
самых последних отметок времени. Сохраняя каждое поле в отдельной таблице,
достаточно записать информацию, которая изменилась. Для хранения этой ин¬
формации требуется меньше места, и при этом гарантируется, что каждая запись
содержит новую информацию, а не просто переносится из последней записи.
Один из компромиссов такого подхода к неизменяемости данных состоит
в том, что он требует больше места для хранения данных, чем в схеме изменя¬
емости. Прежде всего, идентификатор пользователя указывается для каждого
свойства, а не только построчно, как в схеме неизменяемости. Кроме того, со¬
храняется вся предыстория событий, а не только текущее представление состо¬
яния. Но ведь большие данные называются так недаром. Технологии больших
данных дают возможность сохранять большие объемы данных, чтобы восполь¬
зоваться преимуществами неизменяемости. Значение главного массива данных,
который является простым и строго устойчивым к отказам, связанным с челове¬
ческим фактором, трудно переоценить.
2.1.3. Вечная истинность данных
Главное следствие неизменяемости данных состоит в том, что каждый фраг¬
мент данных оказывается истинным в вечности. Это означает, что, став однажды
истинным, фрагмент данных должен оставаться таковым всегда. Без этого свой¬
ства неизменяемость данных не имеет смысла, и в предыдущем разделе было
показано, как сделать на практике каждый фрагмент данных вечно истинным,
обозначив его отметкой времени.
Аналогичный подход применяется при изучении истории в школе. Тот факт,
что Соединенные Штаты образовались из тринадцати штатов 4 июля 1776 года,
остается всегда истинным в силу конкретной даты. А тот факт, что количество
штатов с тех пор увеличилось, зафиксирован в дополнительных (и также веч¬
ных, т.е. бессрочных) данных.
66
Часть /. Уровень пакетной обработки
В общем, главный массив данных постоянно растет в результате ввода новых
неизменяемых и вечно истинных фрагментов данных. Но имеются особые слу¬
чаи, когда данные приходится удалять. Эти случаи несовместимы с вечной ис¬
тинностью данных и вкратце рассматриваются ниже.
■ Сборка “мусора”. Когда выполняется сборка “мусора”, удаляются все фраг¬
менты данных, имеющие малую ценность. С помощью сборки “мусора”
можно реализовать правила удерживания данных, контролирующие рост
главного массива данных. В частности, можно реализовать правило, пред¬
усматривающее сохранение только одного места жительства на каждого
человека в год вместо всей предыстории всякий раз, когда пользователь
меняет место своего жительства.
■ Нормативы — государственные нормативные документы, которые требу¬
ют очистки содержимого баз данных при определенных условиях.
В обоих случаях удаление данных не является утверждением об истинности
данных. Напротив, это утверждение о ценности данных. Несмотря на то что дан¬
ные вечно истинны, можно предпочесть вообще “забыть” об информации, пото¬
му что это придется сделать или потому что она не представляет достаточной
ценности с точки зрения затрат на ее хранение. Продолжим внедрение модели
данных, в которой применяются рассмотренные выше свойства данных.
Как удалить неизменяемые данные
В самом деле, как же удалить неизменяемые данные? На первый взгляд в этом вопросе
кроется противоречие. В этой связи очень важно понять, что удаление данных — это
особый и редкий случай. В обычных условиях эксплуатации данные неизменяемы, и для
соблюдения этого свойства данных предпринимаются такие действия, как установка со¬
ответствующих разрешений. А поскольку данные удаляются редко, необходимо принять
крайние меры предосторожности, чтобы обеспечить безопасность подобной операции.
Мы считаем, что удаление данных безопаснее всего выполнять следующим образом:
получить вторую копию главного массива данных с отфильтрованными, ненужными дан¬
ными, провести аналитические работы для проверки правильности оставшихся после
отсеивания данных и только тогда заменить старую копию главного массива данных.
2.2. Модель, основанная на фактах,
для представления данных
Данные — это совокупность информации, которая не может быть получена из
других источников, хотя для их представления в главном массиве данных можно
воспользоваться многими способами. Помимо традиционных реляционных та¬
блиц, структурированных ХМЬ-документов и слабоструктурированных докумен¬
тов формата |50Х, имеются и другие возможности для сохранения данных. Но
мы рекомендуем применять для этой цели модель, основанную на фактах, кото¬
рая позволяет вскрыть противоречия в данных, и данные можно представить на
основе единицы, называемой, как ни странно, фактом.
Обсуждая неизменяемость данных, мы вскользь упомянули модель, осно¬
ванную на фактах, в том отношении, что главный массив данных постоянно
Глава 2. Модель данных для больших данных
67
растет в результате ввода неизменяемых данных, обозначаемых отметкой вре¬
мени. пираясь на то, чго уже обсуждалось ранее, рассмотрим модель, основан¬
ную на фактах, подробнее. С этой целью введем сначала модель, основанную на
фактах, в контекст примера социальной сети БасеБрасе и обсудим ее основные
свойства, а затем продолжим обсуждать, как и почему факты должны стать распоз¬
наваемыми. И в завершение поясним преимущества применения модели, основан¬
ной на фактах, а также причины, по которым она отлично подходит для главного
массива данных,
2.2.1. Примеры фантов и их свойства
На рис. 2.11 приведены примеры фактов о Томе, полученные из данных в со¬
циальной сети БасеБрасе, а также следующие два свойства фактов: атомарность
и обозначение отметкой времени.
Рис. 2.11. Все необработанные данные, касающиеся Тома, разлагаются
на обозначаемые отметкой времени атомарные единицы, называемые фактами
Факты атомарны, поскольку их нельзя разложить на более мелкие и значи¬
мые составляющие. Такие совокупные данные, как список друзей пользователя
на рис. 2.11, представлены в виде множества независимых фактов. Как следствие
атомарности, отдельные факты не страдают избыточностью информации. Факты,
имеющие отметки времени, не должны быть неожиданными. Как пояснялось ранее
при обсуждении свойств данных, отметки времени делают каждый факт неизменя¬
емым и вечно истинным. Благодаря этим свойствам модель, основанная на фактах,
получается простой и выразительной для главного массива данных, хотя имеется
еще одно свойство распознаваемости,, которое рекомендуется накладывать на факты.
КАК СДЕЛАТЬ ФАКТЫ РАСПОЗНАВАЕМЫМИ
Помимо того что факты атомарны и обозначаются отметками времени, они
должны быть связаны с однозначно распознаваемым фрагментом данных. Это
свойство фактов проще всего пояснить на конкретном примере. Допустим, в соци¬
альной сети КасеБрасе требуется сохранить данные о просмотрах страниц. Первый
подход к решению этой задачи может выглядеть в псевдокоде следующим образом:
68
Часть /. Уровень пакетной обработки
struct PageView:
DateTime timestamp
String url
String ip_address
С помощью этой структуры факты не различают однозначно конкретное со¬
бытие, связанное с просмотром страниц. Если одновременно поступает несколь¬
ко просмотров страницы по одному и тому же иіІЬ и 1Р-адресу, то у каждого
просмотра страницы будет иметься одна и та же запись данных. Следовательно,
если встретятся две одинаковых записи просмотров данной страницы, то трудно
сказать, обозначают ли они два разных события или же в главный массив данных
случайно введен дубликат записи.
Чтобы различать разные просмотры страницы, в схему можно ввести однора¬
зовое число (попсе) — 64-разрядное число, произвольно генерируемое для каждого
просмотра страницы, как показано ниже.
struct PageView:
Date time timestamp
String url
String ip_address
Long nonce
Одноразовое число вместе с другими
полями однозначно обозначает конкретный
просмотр страницы
Благодаря введению одноразового числа появляется возможность различать
события, связанные с просмотрами страниц, и если два блока данных просмотра
страниц окажутся одинаковыми (по всем полям, включая и одноразовое число),
то можно с уверенностью сказать, что они обозначают одно и то же событие.
Способность сделать факты распознаваемыми означает, что один и тот же
факт можно неоднократно записать в главный массив данных, не изменяя се¬
мантику главного массива данных. А в запросах можно отсеять дублирующиеся
факты, когда над ними выполняются вычисления. Как оказывается и поясняется
далее, наличие различаемых фактов намного упрощает реализацию остальной
части лямбда-архитектуры.
Дубликаты не так уж и редки
На первый взгляд не совсем очевидно, почему так важно уделять внимание идентичности
и дубликатам. Ведь во избежание дубликатов было бы достаточно записать событие лишь
один раз. К сожалению, дело с большими массивами данных не всегда обстоит так просто.
По мере роста популярности социальной сети FaceSpace потребуются сначала сотни,
а затем и тысячи веб-серверов. Для построения главного массива данных потребуется
объединение данных из всех этих серверов в центральную систему, что само по себе
является нетривиальной задачей. Для решения этой задачи имеются инструментальные
средства накопления вроде Scribe в Facebook, Apache Flume, syslog-ng и многие другие,
но любое решение должно быть отказоустойчивым.
Одним из общих огрехов, которые следует предвидеть в подобных системах, является
разделение сети, когда целевое информационное хранилище становится недоступным.
В подобных случаях отказоустойчивые системы обычно пытаются повторять неудачные
операции до тех пор, пока они не завершатся удачно. Отправителю данных неизвест¬
но, какие именно данные были получены в последний раз, и поэтому все данные, как
Глава 2. Модель данных для больших данных
69
правило, пересылаются, хотя они и подтверждены получателем. Но если часть первона¬
чальной попытки приведет к появлению метахранилища, то в конечном итоге дубликаты
появятся в главном массиве данных.
Подобного рода операции могут быть сделаны транзакционными, хотя это непросто
и затратно по производительности. Для обеспечения правильности работы информаци¬
онных систем очень важно избегать сложных решений. Используя различаемые факты,
можно устранить потребность в транзакционных присоединениях к главному массиву
данных и упростить анализ правильности всей системы. В конечном счете, зачем брать
на себя тяжкое бремя хлопот, когда достаточно настроить информационную модель
и тем самым избежать всех этих трудностей?
Итак, подводя краткий итог, можно сказать, что модель, основанная на фак¬
тах, позволяет делать следующее.
■ Сохранять необработанные данные в виде атомарных фактов.
■ Поддерживать неизменяемость и вечную истинность фактов с помощью
отметок времени.
■ Обеспечивать распознаваемость каждого факта, чтобы различать дублика¬
ты при обработке запросов.
Рассмотрим далее те преимущества, которые дает выбор модели, основанной
на фактах, для главного массива данных.
2.2.2. Преимущества модели, основанной на фантах
Благодаря модели, основанной на фактах, главный массив данных будет по¬
стоянно наполняться неизменяемыми атомарными фактами. Это не тот образец,
которому обычно следовали разработчики при построении реляционных баз дан¬
ных. Если у вас имеется опыт работы с реляционными базами данных, то от всех
этих перемен у вас может просто пойти голова кругом. Правда, изменяя парадигму
модели данных, вы получаете немало преимуществ. В частности, данные
■ можно запрашивать в любой момент их исторического существования;
■ устойчивы к ошибкам, обусловленным человеческим фактором;
■ допускают обработку неполной информации;
■ обладают преимуществами как нормалированных, так и денормализован-
ных форм.
Рассмотрим каждое из этих преимуществ по очереди.
МАССИВ ДАННЫХ МОЖНО ЗАПРАШИВАТЬ В ЛЮБОЙ
МОМЕНТ ЕГО ИСТОРИЧЕСКОГО СУЩЕСТВОВАНИЯ
Вместо того чтобы сохранять только текущее состояние, как это обычно де¬
лается по изменяемой реляционной схеме, имеется возможность запрашивать
данные в любой момент времени, охваченный главным массивом данных. Это
прямое следствие неизменяемости и обозначения фактов отметками времени.
Обновления и удаления выполняются путем ввода новых фактов с самыми по¬
следними отметками времени, но, поскольку никакие данные фактически не уда¬
ляются, состояние можно восстановить в любой момент, указанный в запросе.
70
Часть 1. Уровень пакетной обработки
ДАННЫЕ УСТОЙЧИВЫ К ОТКАЗАМ, СВЯЗАННЫМ С ЧЕЛОВЕЧЕСКИМ ФАКТОРОМ
Устойчивость к отказам, связанным с человеческим фактором, достигается
простым удалением любых ошибочных фактов. Допустим, что по ошибке были
сохранены данные о том, что Том переехал из Сан-Франциско в Лос-Анджелес,
как показано на рис. 2.12. Удаляя факт переезда в Лос-Анджелес, место житель¬
ства Тома автоматически “восстанавливается”, поскольку факт нахождения в Сан-
Франциско становится самой последней информацией.
Ошибки, обусловленные
человеческим фактором,
легко исправляются
простым удалением
ошибочных фактов.
Запись автоматически
восстанавливается
по прежним отметкам
времени
Рис. 2.12. Чтобы исправить ошибки, обусловленные человеческим фактором,
достаточно удалить неверные факты. В ходе этого процесса автоматически восстанавливается
прежнее состояние путем “раскрытия” любых связанных с ним предыдущих фактов
В МАССИВЕ ДАННЫХ ЛЕГКО ОБРАБАТЫВАЕТСЯ НЕПОЛНАЯ ИНФОРМАЦИЯ
Сохраняя один факт на каждую запись, нетрудно обработать неполную инфор¬
мацию о сущности, не вводя пустые значения NULL в массив данных. Допустим,
что Том указал в своем профиле возраст и пол, но не место жительства или про¬
фессию. Массив данных будет содержать только факты об известной информа¬
ции, а любой отсутствующий факт будет логически равнозначен пустому значе¬
нию NULL. Дополнительные сведения, которые Том предоставит впоследствии,
будут естественным образом введены через новые факты.
УРОВНИ ХРАНЕНИЯ ДАННЫХ И ОБРАБОТКИ ЗАПРОСОВ РАЗДЕЛЕНЫ
Еще одно важное преимущество модели, основанной на фактах, отчасти объ¬
ясняется самой структурой лямбда-архитектуры. Сохраняя информацию на уров¬
нях пакетной обработки и обслуживания, можно извлечь двойную выгоду из хра¬
нения данных как в нормализованной, так и в денормализованной форме.
Нормализация — перегруженный термин. Нормализация данных совершен¬
но не связана с употреблявшимся ранее термином семантическая нормализа¬
ция. В данном случае нормализация данных обозначает сохранение данных
в структурированном виде, чтобы свести к минимуму избыточность и обеспе¬
чить согласованность данных.
Подготовим почву, рассмотрев в качестве примера реляционные таблицы,
задающие контекст, в котором чаще всего происходит нормализация данных.
Реляционные таблицы вынуждают выбирать между нормализованными и денор-
мализованными схемами в зависимости от того, что важнее: эффективность обра¬
ботки запросов или согласованность данных. Допустим, что требуется сохранить
информацию о занятости различных лиц, к которым проявляется определенный
Данные места жительства
user id
location
timestamp
1
Atianta, GA
2012/03/29 08:12:24
2
Chicago, IL
2012/04/12 14:47:51
3
San Francisco, CA
2012/04/04 18:31:24
4
Washington, DC
2012/04/09 11:52:30
з
ПЛ-ІО/ЛГ/ІТ ЛЛ.ЛЛ.іП
i-03 Angeles, C/-i
- 2812/86/17-20:03:48 —
Глава 2. Модель данных для больших данных
71
иа герес. На рис. 2.13 приведена простая денормализованная схема, пригодная
для этой цели.
Данные п атом таблице
денормалмзованы потому что одна
и гаже информация хранится
избыIочно (в данном случае — ото
название компании, которое может
ПОЙТООЯТЬСй)
Эи таблица позволяет быстро
определить количество работников
п каждой компании, хотя когда
в ней происходят изменения (в данном
случае - при смене названия компании
с BackRub на Google), в ной нужно обновить
множество строк
Занятость
row Id
name
company
1
Bill
Microsoft
2
Larry
^ _ BackRub
3
Sergey
BackRub
4
Steve
Apple
Рис. 2.13. Простая денормализованная схема для хранения информации о занятости
В денормализованной схеме название той же самой компании может быть со¬
хранено в нескольких строках таблицы. Это позволит быстро определить коли¬
чество работников в каждой компании. Но если название компании изменится,
то придется обновить много строк таблицы. Если информация хранится во мно¬
гих местах, то повышается риск, что она станет несогласованной. Для сравнения
рассмотрим нормализованную схему, приведенную на рис. 2.14.
Пользователь
user id
name
company Id
1
Bill
3
2
Larry
2
3
Sergey
2
4
Steve
1
Компания
company id
name
1
Apple
2
BackRub
3
Microsoft
4
IBM
Для нормализации данных каждый факт хранится только в одном месте,
а для ответов на запросы служат взаимосвязи между массивами данных.
Благодаря этому упрощается соблюдение согласованности данных,
хотя соединение таблиц может быть затратным
Рис. 2.14. Две нормализованные таблицы для хранения одной и той же информации о занятости
Данные в нормализованной схеме хранятся только в одном месте. Если же по¬
требуется сменить название компании с BackRub на Google, для этого придется
внести изменения только в одной строке таблицы. Благодаря этому устраняется
риск несогласованности данных, но для ответов на запросы придется соединить
таблицы, а это потенциально затратная по вычислениям операция.
Взаимоисключающий выбор между нормализованными и денормализованны-
ми схемами приходится делать потому, что в реляционных базах данных запросы
делаются непосредственно на уровне хранения данных. Следовательно, необхо¬
димо решить, что важнее: эффективность запросов или согласованность данных,
а затем выбрать наиболее подходящую схему.
С другой стороны, цели обработки запросов и хранения данных отчетливо
разделены в лямбда-архитектуре. Рассмотрим в качестве примера уровни пакет¬
ной обработки и обслуживания, приведенные на рис. 2.15.
78
Часть /. Уровень пакетной обработки
О Данные нормализуются
в главном массиве данных
ради компактности
и согласованности...
© .но хранятся избыIочно
(де н о р м а л и з у ю т ся)
в пакетных
представлениях
ради эффективности
обработки запросов
Рис. 2.15. В лямбда-архитектуре выгодно используются преимущества нормализации
и денормализации путем разделения целей на разных уровнях
В лямбда-архитектуре главный массив данных полностью нормализован. Как
пояснялось при обсуждении модели, основанной на фактах, данные не хранятся с
резервированием. А обновления выполняются легко, поскольку новый факт с те¬
кущей отметкой времени замещает любые предыдущие, связанные с ним факты.
Аналогично пакетные представления действуют подобно денормализованным
таблицам в том отношении, что один фрагмент данных из главного массива мо¬
жет быть проиндексирован во многих пакетных представлениях. А главное отли¬
чие состоит в том, что пакетные представления определяются в виде функций
в главном массиве данных. Соответственно отпадает необходимость обновлять
пакетное представление, поскольку оно будет постоянно восстанавливаться из
главного массива данных. Это дает еще одно преимущество: пакетные представ¬
ления и главный массив данных никогда не выходят из синхронизма. Лямбда-
архитектура дает принципиальные преимущества полной нормализации, а так¬
же преимущества в производительности благодаря разным способам индексации
данных для оптимизации обработки запросов.
Таким образом, благодаря всем упомянутым выше преимуществам модель, ос¬
нованная на фактах, отлично подходит для главного массива данных. А теперь
перейдем от теоретических рассуждений к подробностям реализации модели,
основанной на фактах.
2.3. Граф-схемы
Каждый факт в модели, основанной на фактах, фиксирует один фрагмент ин¬
формации. Но сам факт не передает структуру, скрывающуюся за данными. Это
означает, что отсутствует как описание типов фактов, содержащихся в главном
Глава 2. Модель данных для больших данных
73
массиве данных, так и любое пояснение взаимосвязей между ними. В этом разделе
будут представлены граф-схемы — графы, фиксирующие структуру массива данных,
сохраняемую с помощью модели, основанной на фактах. Мы обсудим элементы
граф-схемы и потребность сделать ее осуществимой. Итак, начнем со структури¬
рования фактов из рассматриваемого здесь примера социальной сети Расе8расе
в виде графа.
2.3.1. Элементы граф-схемы
В предыдущем разделе мы подробно обсуждали факты в социальной сети
Расе8расе. Каждый такой факт представляет фрагмент информации о поль¬
зователе или взаимосвязь между двумя пользователями. На рис. 2.16 показана
граф-схема, представляющая взаимосвязи между фактами в социальной сети
Расе8расе. Она удобна для наглядного представления всех пользователей, их ин¬
дивидуальной информации и дружеских отношений между ними.
Имя:
Пол:
Том
мужской
О Овалами обозначены
узлы графа (в данном
случае — это пользователи
РасеБрасе)
Возраст:
25 лет
Место
жительства:
г. Атланта,
шт. Калифорния
© Пунктирные линии
соелиняют узлы графа
(т е. пользователей)
с их свойствами,
обозначенными
прямоугольниками
© Сплошные линии,
соединяющие узлы,
являются ребрами графа
представляющими связи
в ГасеБрасе
Идентификатор
личности: 4
Место жительства:
г. Вашингтон,
округ Колумбия
Рис. 2.16. Наглядное представление взаимосвязи между фактами в социальной сети РасеБрасе
На рис. 2.16 показаны три главных элемента граф-ехемы: узлы, ребра и свой¬
ства. Ниже дается их краткое описание.
■ Узлы обозначают сущности в системе. В данном примере узлы обознача¬
ют пользователей социальной сети ЕасеБрасе, представленных идентифи¬
каторами. В качестве другого примера узлов могут служить группы поль¬
зователей, если последним разрешено удостоверять себя как часть группы
пользователей ЕасеБрасе.
■ Ребра обозначают взаимосвязи между узлами. Очевидно, что в Еасебрасе
ребра обозначают дружеские отношения между пользователями этой со¬
циальной сети. Для обозначения взаимосвязей между сотрудниками, род¬
ственниками или соучениками в дальнейшем можно ввести дополнитель¬
ные типы ребер.
Часть /. Уровень пакетной обработки
■ Свойства предоставляют информацию о сущностях. В данном примере
возраст, пол, местоположение и все остальные фрагменты индивидуаль¬
ной информации являются свойствами.
■ Ребра проводятся строго между узлами графа. Несмотря на то что свой¬
ства и узлы соединены на рис. 2.16 пунктирными линиями, эти линии не
являются ребрами графа. Они лишь помогают нагляднее показать связь
между пользователями и их личной информацией. Чтобы провести это от¬
личие, ребра на рис. 2.16 обозначены сплошными линиями, а связи узлов
со свойствами — пунктирными.
Граф-схема дает полное описание всех данных, содержащихся в массиве.
Обсудим далее потребность обеспечить жесткую привязку к схеме всех фактов
в массиве данных.
2.3.2. Потребность в осуществимой схеме
На данном этапе информация сохраняется в виде фактов, а граф-схема опи¬
сывает типы фактов, содержащихся в массиве данных. Итак, все готово? Не со¬
всем. Нужно еще решить, в каком именно формате следует хранить факты.
Первой мыслью может стать выбор слабоструктурированного текстового
формата вроде JSON. Такой выбор обеспечит простоту и удобство, позволяя за¬
писывать, по существу, все, что угодно, в главный массив данных. Но в данном
случае это даже чрезмерно для наших потребностей.
Чтобы проиллюстрировать суть данного затруднения, допустим, что возраст
Тома решено представить в формате JSON следующим образом:
{"id": 3, "field":"age", "value":28, "timestamp": 1333589484}
Представление этого единственного факта в формате JSON не вызывает ника¬
ких вопросов, но нет никакой гарантии, что все последующие факты последуют
этому формату. В результате ошибки, обусловленной человеческим фактором,
в массиве данных могут также оказаться следующие факты:
{"name":"Alice", "field":"age", "value":25,
"timestamp":"2012/03/29 08:12:24"}
{"id":2, "field":"age", "value":36}
В обоих приведенных выше примерах данные верно представлены в формате
JSON, но они несовместимы по формату или составу данных. Так, в предыдущем
разделе подчеркивалось особое значение отметки времени для каждого факта,
но в текстовом формате это требование нельзя соблюсти. Для того чтобы эф¬
фективно пользоваться данными, необходимо предоставить какие-то гарантии
относительно содержимого массива данных.
С другой стороны, можно воспользоваться осуществимой схемой, где стро¬
го определяется структура фактов. Осуществимые схемы требуют больше под¬
готовительной работы, но в то же время они гарантируют наличие всех полей
и предполагаемые типы всех значений. При таких гарантиях разработчик может
быть уверен, что получит именно тс данные, которые и предполагались, а имен¬
но: каждый факт будет иметь отметку времени, имя пользователя будет всегда
представлено символьной строкой и т.д. А самое главное, что если при созда¬
нии фрагмента данных будет допущена ошибка, то осуществимая схема выдаст
76
Часть /. Уровень пакетной обработки
С ребрами в данной граф-схеме все очень просто. В частности, ребро просмо¬
тра страницы проводится между узлом лица и страницы для каждого просмотра
в отдельности, тогда как ребро эквивалента — между узлами отдельного лица, ког-
да они представляют одно и то же лицо. Последнее происходит, когда лицо пер¬
воначально распознается только по содержимому соо1йе-файла, а полностью —
в дальнейшем.
Свойства также не требуют особых пояснений. У страниц имеются подсчеты
их просмотров, а у людей — основная демографическая информация: имя, пол
и место жительства.
Модель, основанная на фактах, и граф-схемы примечательны тем, что они мо¬
гут развиваться по мере появления других типов данных. В частности, граф-схе¬
ма предоставляет согласованный интерфейс для самых разнообразных данных,
что упрощает внедрение новых типов информации. Дополнения схемы произво¬
дятся путем определения новых типов узлов, ребер и свойств. Благодаря атомар¬
ности фактов эти дополнения не оказывают влияния на типы уже существующих
фактов.
Резюме
Порядок моделирования главного массива данных закладывает основание
для системы больших данных. Решения, принимаемые по поводу главного мас¬
сива данных, определяют вид аналитического анализа, который можно выпол¬
нить над данными, а также порядок их потребления. Структура главного массива
данных должна поддерживать развитие видов сохраняемых данных, поскольку
типы данных в организации могут претерпевать со временем значительные из¬
менения.
Модель, основанная на фактах, обеспечивает простое, но выразительное
представление данных, естественно сохраняя всю предысторию каждой сущ¬
ности во времени. По своему характеру она служит только для присоединения,
что упрощает ее реализацию в распределенной системе и дальнейшее развитие
по мере появления потребности в изменении данных. Это дает возможность не
только реализовать реляционную систему с большей масштабируемостью, но
и вводить в нее совершенно новые свойства.
модели данных
для больших данных
В этой главе...
■ Каркас сериализации Apache Thrift.
■ Реализация граф-схемы средствами Apache Thrift.
■ Ограничения, присущие каркасам сериализации.
В предыдущей главе были рассмотрены принципы формирования модели дан¬
ных, основанной на фактах, в том числе ценность данных, семантическая норма¬
лизация и особое значение неизменяемости данных. В ней было также показано,
как строится граф-схема, способная удовлетворить всем свойствам данных, и как
выглядит такая граф-схема для приложения SuperWebAnalytics.com, пример кото¬
рого рассматривается в данной книге.
Эта глава является первой из иллюстративных глав книги, где понятия, пред¬
ставленные в предыдущей главе, демонстрируются на примере применения
реальных инструментальных средств. Чтобы изучить лямбда-архитектуру, вам
достаточно прочитать лишь теоретические главы данной книги, но в ее иллю¬
стративных главах наглядно показываются особенности воплощения теории
в реальном коде. В этой главе нам предстоит реализовать модель данных при¬
ложения SuperWebAnalytics.com с помощью каркаса сериализации Apache Thrift.
В ней будет показано, что даже в решении такой простой задачи, как состав¬
ление схемы, возникает противоречие между идеализированной теорией и гем,
чего можно добиться на практике.
3.1. Причины применения каркаса сериализации
Многие разработчики идут по пути представления необработанных данных
в таком бессхемном формате, как JSON. Этот путь привлекателен гой про¬
стотой, с которой он начинается, но очень быстро он приводит к серьезным
78
Часть J. Уровень пакетной обработки
затруднениям. Искажение данных неизбежно происходит или по причине не¬
доразумений между разными разработчиками, или из-за программных ошибок.
Как показывает наш опыт, ошибки, приводящие к искажению данных, отнимают
больше всего времени на их исправление.
Проблемы искажения данных трудноразрешимы из-за очень скудного контек¬
ста, в котором произошло искажение. Как правило, проблема становится замет¬
ной, когда ошибка возникает по ходу обработки данных, т.е. спустя много време¬
ни после записи искаженных данных. Например, исключение в связи с пустым
указателем может возникнуть из-за отсутствия обязательного поля. И тогда сразу
же становится ясно, что все дело в пропущенном поле, но при этом совершенно
отсутствует информация, каким образом данные оказались в таком состоянии.
При создании осуществимой схемы ошибки возникают во время записи дан¬
ных. Они предоставляют полный контекст о том, как и почему данные стали
недостоверными, как и при трассировке стека. Кроме того, ошибка препятствует
программе испортить главный массив данных при записи этих данных.
Каркасы сериализации предоставляют простой подход к построению осуще¬
ствимой схемы. Если у вас имеется опыт программирования на объектно-ориен¬
тированном, статически типизированном языке, вам сразу же станет ясно, как
пользоваться каркасом сериализации. Каркасы сериализации генерируют код
на любых языках программирования, которыми вы желаете пользоваться для чте¬
ния, записи и проверки достоверности объектов, соответствующих составленной
вами схеме.
Но возможности каркасов сериализации ограничены для построения полно¬
стью строгой схемы. Обсудив сначала применение каркаса сериализации для по¬
строения модели данных приложения SuperWebAnalytics.com, мы рассмотрим
ограничения, присущие каркасам сериализации, а также способы их преодоления.
3.2. Каркас сериализации Apache Thrift
Каркас сериализации Apache Thrift (http://thrift.apache.org/) представля¬
ет собой инструментальное средство, предназначенное для определения стати¬
чески типизированных осуществимых схем. Он предоставляет язык определения
интерфейсов для описания схемы с точки зрения обобщенных типов данных,
и такое описание может быть в дальнейшем использовано для конкретной реа¬
лизации схемы на многих языках программирования.
О применении Apache Thrift. Инструментальное средство Apache Thrift было
первоначально разработано для создания межъязыковых служб в социальной
сети Facebook. Им можно пользоваться в самых разных целях, но мы ограни¬
чимся его применением в качестве каркаса сериализации.
В основу Apache Thrift положены определения типов struct и union. Они,
в свою очередь, состоят из полей других типов, включая следующие.
■ Примитивные типы данных (символьные строки, целые числа, длинные
целые числа, а также числа с плавающей точкой двойной точности).
■ Коллекции других типов (списки, отображения и множества).
■ Прочие структуры и объединения.
79
Глава 3. Иллюстрация модели данных для больших данных
В общем, объединения удобны для представления узлов, структуры — для есте¬
ственного представления ребер, а совместно и те и другие — для представле¬
ния свойств. Это станет далее очевидным из определений типов, требующихся
для представления составляющих граф-схемы приложения SuperWebAnalytics.com.
Другие каркасы сериализации
Имеются и другие инструментальные средства, аналогичные Apache Thrift, в том числе
Protocol Buffers и Avro. Напомним, что назначение данной книги состоит не в том, что¬
бы рассмотреть все возможные инструментальные средства, подходящие для каждой
ситуации, а в том, чтобы проиллюстрировать основополагающие принципы построения
информационных систем на примере применения подходящего для этой цели инстру¬
ментального средства. В частности, Apache Thrift широко применяется в качестве кар¬
каса сериализации, поскольку это инструментальное средство тщательно проверено
на практике.
3.2.1. Узлы
В узлах пользователей граф-схемы приложения SuperWebAnalytics.com отдель¬
ный пользователь обозначается своим идентификатором или cookie-файлом из
браузера, но не тем и другим вместе. Такой шаблон является типичным для узлов
и точно соответствует типу данных union с единственным значением, которое
может иметь самые разные представления.
В каркасе Apache Thrift объединения определяются перечислением всех воз¬
можных представлений. В следующем фрагменте кода показано, каким образом
узлы граф-схемы приложения SuperWebAnalytics.com определяются с помощью
объединений в Apache Thrift:
union PersonID {
1: string cookie;
2: i64 user_id;
}
union PagelD {
1: string url;
)
Следует иметь в виду, что объединения применяются также для определения
узлов с единственным представлением. Объединения дают возможность разви¬
ваться осуществимой схеме вместе с данными. Более подробно об этом речь по¬
йдет в следующем разделе.
3.2.2. Ребра
Каждое ребро может быть представлено в виде структуры, содержащей два
узла. Имя структуры ребра обозначает взаимосвязь, которую она представляет,
а поля в этой структуре содержат сущности, вовлеченные во взаимосвязь.
Осуществимая схема для ребер определяются очень просто, как показано
ниже.
80
Часть /. Уровень пакетной обработки
struct EquivEdge {
1: required PersonID idl;
2: required PersonID id2;
}
struct PageViewEdge {
1: required PersonID person;
2: required PagelD page;
3: required i64 nonce;
}
Поля структуры в Apache Thrift могут быть обозначены как required или
optional. Если поле определяется как required, то для него требуется предоста¬
вить значение, иначе Apache Thrift выдаст ошибку по завершении сериализации
или десериализации. Каждое ребро в граф-схеме должно соединять два узла,
и поэтому в данном примере они представлены обязательными полями.
3.2.3. Свойства
И наконец, определим свойства. Каждое свойство содержит узел и значение
этого свойства. Значение может относиться к одному из многих типов, поэтому
его лучше всего представить с помощью структуры объединения.
Итак, начнем с определения осуществимой схемы для свойств страницы. Для
страниц имеется только одно свойство, и поэтому его схема определяется очень
просто:
union PagePropertyValue {
1: i32 page_views;
struct PageProperty {
1: required PagelD id;
2: required PagePropertyValue property;
}
Далее определим свойства для людей. Как показано ниже, свойство места жи¬
тельства оказывается более сложным и требует определения другой структуры.
struct Location {
1: optional string city;
2: optional string state;
3: optional string country;
}
enum GenderType {
MALE = 1,
FEMALE = 2
union PersonPropertyValue {
1: string full_name;
2: GenderType gender;
3: Location location;
f
Глава 3» Иллюстрация модели данных для больших данных
81
struct PersonProperty {
1: required PersonID id;
2: required PersonPropertyValue property;
}
Структура места жительства Location интересна тем, что поля city, state
и country могут быть сохранены в виде отдельных фрагментов данных. В данном
случае они настолько тесно связаны, что их имеет смысл разместить в одной
структуре в виде необязательных полей типа optional. Все эти поля практически
всегда требуются для потребления информации о месте жительства.
3.2.4. Связывание всего вместе в объекты данных
На данном этапе ребра и свойства граф-схемы определяются как отдельные
типы. В идеальном случае все данные требуется сохранять вместе, чтобы пре¬
доставить единый интерфейс для доступа к информации. Это также упрощает
управление данными, если они сохраняются в одном массиве. С этой целью тип
каждого свойства и ребра заключается в оболочку объединения DataUnit, как
демонстрируется в приведенном ниже листинге.
Листинг 3.1. Завершение построения осуществимой схемы
для приложения SuperWebAnalytics.com
union DataUnit {
1: PersonProperty person_property;
2: PageProperty pagejproperty;
3: EquivEdge equiv;
4: PageViewEdge page_view;
struct Pedigree {
1: required i32 true_as_of_secs;
}
struct Data {
1: required Pedigree pedigree;
2: required DataUnit dataunit;
}
Каждому объединению DataUnit сопутствуют метаданные, хранящиеся
в структуре Pedigree. В этой структуре содержится отметка времени информа¬
ции, хотя в ней может вполне находиться отладочная информация или источник
данных. В окончательном виде структура Data соответствует факту из модели,
основанной на фактах.
3.2.5. Развитие осуществимой схемы
Каркас сериализации Apache Thrift разработай таким образом, чтобы осуще¬
ствимые схемы могли развиваться со временем. И это очень важное свойство, ведь
новые данные приходится вводить по мере изменения требований к предметной
области, и поэтому делать это желательно как можно более непринужденно.
Главным компонентом Apache Thrift для дальнейшего развития осуществи¬
мых схем являются числовые идентификаторы, связанные с каждым полем. Эти
82
Часть I, Уровень пакетной обработки
идентификаторы служат для распознавания полей в их сериализированной фор¬
ме. Если требуется изменить осуществимую схему, но в то же время обеспечить
обратную совместимость с существующими данными, необходимо соблюдать сле¬
дующие правила.
■ Поля могут быть переименованы. Дело в том, что для распознавания по¬
лей в сериализированной форме объекта используются идентификаторы
полей, а не имена.
■ Поле может быть удалено, но идентификатор этого поля нельзя упо¬
треблять снова. При десериализации существующих данных Apache Thrift
проигнорирует все ноля с идентификаторами, не включенными в схему.
Если попытаться воспользоваться идентификатором удаленного ранее
поля, Apache Thrift попробует десериализировать эти прежние данные
в новом поле, что сделает данные недостоверными или неверными.
■ В существующие структуры могут быть введены только необязатель¬
ные поля. Вводить обязательные поля нельзя, поскольку существующие
данные не будут содержать эти поля, а следовательно, они не подлежат
десериализации. (Следует, однако, иметь в виду, что данное правило не
распространяется на объединения, поскольку в них вообще отсутствует
понятие обязательных и необязательных полей.)
В качестве примера внесите в свой файл определения осуществимой схе¬
мы в Apache Thrift изменения, выделенные полужирным в приведенном
ниже листинге, если потребуется изменить осуществимую схему приложения
SuperWebAnalytics.com для хранения возраста отдельного лица и взаимных ссы¬
лок на веб-страницы. Обратите внимание на то, что новое свойство аде вводится
в структуру соответствующего объединения, а новое ребро внедряется путем его
добавления в объединение DataUnit.
Листинг 3.2. Расширение осуществимой схемы приложения SuperWebAnalytics.com
union PersonPropertyValue {
1: string full_name;
2: GenderType gender;
3: Location location;
4: 116 age;
}
struct LinkedEdge {
1: required PagelD source ;
2: required PagelD target;
}
union DataUnit {
1: PersonProperty person_property;
2: PageProperty pagejproperty;
3: EquivEdge equiv;
4: PageViewEdge page_view;
5: LinkedEdge page_link ;
}
Глава i. Иллюстрация модели данных для больших данных
83
3.3. Ограничения, присущие каркасам сериализации
Каркасы сериализации лишь проверяют наличие в осуществимой схеме всех
обязательных полей и их соответствие предполагаемым типам. Они неспособны
производить более сложные проверки свойств вроде “неотрицательных величин
возраста или ‘употребления в будущем отметок времени об истинности данных”
в настоящем. Данные, не соответствующие этим свойствам, могут указывать на за¬
труднение в системе, и вряд ли их стоит записывать в главный массив данных.
На первый взгляд это нельзя назвать ограничением, поскольку каркасы сери¬
ализации действуют в какой-то степени подобно схемам в реляционных базах
данных. В действительности работать со схемами реляционных баз данных не¬
легко, но еще труднее сделать их более строгими. Не следует, однако, путать вто¬
ростепенные сложности работы со схемами реляционных баз данных с ценно¬
стью самих схем. Трудности представления вложенных объектов и переноса схем
в реляционных базах данных вообще не возникают, когда каркасы сериализации
применяются для представления неизменяемых объектов с помощью граф-схем.
Осуществимую схему лучше всего рассматривать в качестве функции, принима¬
ющей фрагмент данных и возвращающей логическое значение, обозначающее, яв¬
ляются ли эти данные достоверными или нет. Язык осуществимых схем для Apache
Thrift позволяет представить подмножество этих функций там, где проверяется
только наличие полей и их типов. Идеальное инструментальное средство должно
позволять реализовывать какую угодно функцию осуществимой схемы.
Разумеется, такого идеального свойства, особенно не зависящего от конкрет¬
ного языка программирования, не существует, но для преодоления упомянутых
выше ограничений таких каркасов сериализации, как Apache Thrift, можно вы¬
брать один из двух следующих подходов.
■ Заключение генерируемого кода в оболочку другого кода, проверяюще¬
го дополнительные интересующие свойства вроде возраста, имеюще¬
го неотрицательную величину. Такой подход вполне пригоден при усло¬
вии, что операции чтения и записи данных выполняются на одном языке.
А если используется несколько языков, то логику этих операций придется
дублировать на всех этих языках.
■ Проверка дополнительных свойств в самом начале процесса пакетной
обработки данных. На первоначальном этапе массив данных разделяется
на достоверные и недостоверные данные, и если будут обнаружены недо¬
стоверные данные, то посылается соответствующее уведомление. При та¬
ком подходе упрощается реализация остальной части процесса, поскольку
все, что проходит проверку достоверности, можно считать имеющим ин¬
тересующие нас более строгие свойства. Но такой подход не препятству¬
ет записи недостоверных данных в главный массив данных и нс помогает
определить контекст, в котором произошло искажение данных.
Ни один из этих подходов не является идеальным, но вряд ли можно предло¬
жить что-нибудь более совершенное, если операции чтения и записи данных в ва¬
шей организации выполняются на нескольких языках программирования. Вам
придется решать, следует ли сопровождать одну и ту же логику на нескольких
84
Часть /. Уровень пакетной обработки
языках, чтобы не утратить контекст, в котором возникло искажение данных.
Самым идеальным был бы такой подход, при котором каркас сериализации реа¬
лизован на универсальном языке программирования, транслирующем код на лю¬
бой целевой язык. И хотя такого инструментального средства не существует, те¬
оретически оно возможно.
Резюме
Реализация осуществимой граф-схемы для приложения SuperWebAnalytics.
com оказалась по большей части простой. Когда каркас сериализации приме¬
няется для данной цели, возникает следующее противоречие: неспособность
осуществить каждое интересующее свойство. Инструментальные средства ред¬
ко удовлетворяют полностью требованиям пользователя, поэтому очень важно
знать, чего можно добиться с помощью идеальных инструментальных средств.
Это поможет выяснить компромиссы, на которые приходится идти, попытаться
найти более совершенные инструментальные средства или создать собственные.
Эта общая тема проходит красной нитью через все теоретические и иллю¬
стративные главы данной книги. В следующей главе будет показано, каким об¬
разом осуществляется физическое хранение главного массива данных на уровне
пакетной обработки, чтобы сделать проще и эффективнее его обработку.
Хранение данных
на уровне пакетной
обработки
В этой главе...
■ Требования к хранению главного массива данных.
■ Распределенные файловые системы.
■ Повышение эффективности с помощью верти¬
кального разделения.
В двух предыдущих главах рассматривалась модель данных для главного мас¬
сива данных, а также порядок ее преобразования в граф-схему. При этом было
подчеркнуто особое значение, которое придается обеспечению неизменяемости
и вечности данных. А в этой главе речь пойдет о том, как сохранять физически
эти данные на уровне пакетной обработки. На рис. 4.1 в качестве напоминания
показана лямбда-архитектура в том виде, в каком мы рассмотрели ее до сих пор.
Как и две предыдущие главы, эта глава посвящена главному массиву данных.
Как правило, главный массив данных слишком велик, чтобы размещаться на од¬
ном сервере, поэтому приходится решать, каким образом распределить данные
по нескольким машинам. От способа хранения главного массива данных на не¬
скольких машинах зависит, каким образом он будет употребляться. Поэтому
очень важно выработать разумную стратегию хранения с учетом особенностей
использования этого массива данных.
В этой главе вам предстоит сделать следующее.
■ Определить требования к хранению главного массива данных.
■ Выяснить причины, по которым распределенные файловые системы есте¬
ственным образом подходят для хранения главного массива данных.
■ Узнать, каким образом хранилище для приложения SuperWebAnalytics.com
согласуется на уровне пакетной обработки с распределенными файловыми
системами.
86
Часть I, Уровень пакетной обработки
Итак, начнем с рассмотрения роли, которую уровень пакетной обработки игра¬
ет и лямбда-архитектуре, а также ее влияния па выбор способа хранения данных.
Рис. 4.1. Уровень пакетной обработки должен иметь крупную структуру, постоянно наращивая
массивы данных таким образом, чтобы поддерживать незначительное сопровождение
и эффективное создание пакетных представлений
4.1. Требования к хранению главного массива данных
Чтобы определить требования к хранению данных, необходимо рассмотреть
порядок, в котором они будут' записываться и читаться. На этот порядок оказы¬
вает влияние роль уровня пакетной обработки в лямбда-архитектуре. Поэтом)
рассмотрим сначала порядок записи и чтения на высоком уровне, а затем пол¬
ный список требований к хранению данных.
Как подчеркивалось в главе 2, двумя главными свойствами данных являются
неизменяемость и вечная истинность. Следовательно, каждый фрагмент данных
должен записываться один и только один раз. Изменять данные вообще не нужно,
поэтому единственной операцией записи должен быть ввод нового блока данных
в главный массив. Таким образом, решение задачи хранения данных должно быт ь
оп тимизировано для обращения с крупным, постоянно растущим массивом данных.
Уровень пакетной обработки отвечает также за вычисление функций в мас¬
сиве данных для получения пакетных представлений. Это означает, что снеге
ма хранения данных на уровне пакетной обработки должна вполне справляться
с одновременным чтением больших объемов данных. Это, в частности, озна¬
чает, что произвольный доступ к отдельным фрагментам данных лс требуется.
87
Глава 4. Хранение данных на уровне пакетной обработки
Принимая во внимание этот принцип однократной записи и многократного мас¬
сового чтения, мы можем составить перечень требований к хранению данных,
как показано в табл. 4.1.
Таблица 4.1. Перечень требований к хранению данных в главном массиве
Операция Требование Описание
Запись Эффективность присоединения
новых данных
Масштабируемость хранилища
Чтение Поддержка параллельной
обработки
И то и другое Коррекция затрат на хранение
и обработку
Соблюдение неизменяемости
Единственной операцией записи является ввод
новых фрагментов данных, поэтому эта операция
должна обеспечивать простое и эффективное
присоединение нового ряда объектов данных
к главному массиву данных
На уровне пакетной обработки хранится весь
главный массив данных — потенциально
объемом от терабайт до петабайт. Следовательно,
хранилище должно легко масштабироваться
по мере роста массива данных
Для построения пакетных представлений требуется
вычисление функций во всем главном массиве
данных в целом. Следовательно, пакетное
хранилище должно поддерживать параллельную
обработку, чтобы справляться с крупными
объемами данных масштабируемым образом
Чтобы свести к минимуму затраты на хранение,
можно прибегнуть к их уплотнению, но
разуплотнение данных во время вычислений
может сказаться на производительности.
Поэтому на уровне пакетной обработки должна
быть предоставлена удобная возможность
выбрать способ хранения и уплотнения данных
в соответствии с конкретными потребностями
Очень важно соблюсти свойство неизменяемости
в главном массиве данных. Безусловно,
компьютеры по характеру своей работы склонны
всегда изменять сохраняемые данные. Поэтому
самое лучшее, что можно сделать, — это ввести
проверки, запрещающие операции, изменяющие
данные. Такие проверки должны предотвращать
программные и прочие случайные ошибки,
способные испортить существующие данные
А теперь рассмотрим ряд технологий, удовлетворяющих этим требованиям.
4.2. Выбор решения для хранения данных
на уровне пакетной обработки
Имея в своем распоряжении перечень требований, можно теперь рассмо¬
треть варианты реализации хранилища на уровне пакетной обработки. При та-
нестрогих требованиях, когда не нужен даже произвольный доступ к дан-
ным, для главного массива данных можно было бы выбрать практически любую
распределенную базу данных. Поэтому рассмотрим в первую очередь возмож¬
ность использования для главного массива данных хранилища пар “ключ-значе¬
ние” в качестве наиболее распространенного типа распределенной базы данных.
Часть /. Уровень пакетной обработки
8*
4.2.1. Применение хранилища пар “ключ-значение”
для главного массива данных
И хотя мы еще не обсуждали распределенные хранилища пар “ключ-значе¬
ние”, их можно рассматривать в качестве гигантских постоянно сохраняемых
хеш-отображений, распределенных среди многих машин. Если главный массив
данных предполагается хранить в хранилище пар “ключ-значение”, то прежде
всего нужно выяснить, какими должны быть ключи и их значения.
Очевидно, что значением должен быть фрагмент сохраняемых данных, но каким
должен быть ключ? Естественный для этой цели ключ в модели данных отсутствует,
да он и не нужен, поскольку данные предполагается потреблять в массовом порядке.
Таким образом, сразу же возникает' разногласие между моделью данных и принци¬
пом действия хранилищ пар “ключ-значение”. Единственная жизнеспособная идея
состоит в том, чтобы сформировать универсальный уникальный идентификатор
(ииГО) и воспользоваться им в качестве ключа.
Но это лишь начало тех трудностей, которые скрываются за применением
хранилищ пар “ключ-значение” для главного массива данных. Таким хранили¬
щам требуется мелкоструктурный доступ к парам “ключ-значение” для выполне¬
ния операций произвольного чтения и записи, и поэтому несколько пар “ключ-
значение” нельзя уплотнить вместе. Это накладывает серьезное ограничение
на выбор компромисса между затратами на хранение и обработку данных.
Хранилища пар “ключ-значение” предназначены для применения в качестве из¬
меняемых, а это служит немалым препятствием для соблюдения свойства неизме¬
няемости, столь важного для главного массива данных. Если не модифицировать
код, реализующий используемое хранилище пар “ключ-значение”, то отменить воз¬
можность видоизменить существующие пары “ключ-значение”, как правило, нельзя.
Но самая большая трудность состоит в том, что хранилище пар “ключ-значе¬
ние” обладает многими ненужными функциями, включая произвольное чтение
и запись, а также все механизмы, которые их поддерживают. В действительности
большая часть реализации хранилища пар “ключ-значение” посвящена средствам,
которые в данном случае вообще не нужны. Это означает, что данное инструмен¬
тальное средство оказывается намного более сложным, чем требуется для удовлет¬
ворения рассматриваемых здесь требований, а следовательно, обращение с ним
будет, скорее всего, затруднено. Кроме того, хранилище пар “ключ-значение” ин¬
дексирует данные и предоставляет ненужные услуги, что может увеличить затраты
на хранение и снизить производительность при чтении и записи данных.
4.2.2. Распределенные файловые системы
Как оказывается, имеется вид технологии, с которой вы уже, вероятно, зна¬
комы и которая идеально подходит для организации хранилища на уровне па¬
кетной обработки. Речь идет о файловых системах. Файлы представляют собой
последовательности байтов, и наиболее эффективным способом потребления
файлов является их просмотр. Они хранятся на диске в последовательном виде
(иногда они разделяются на блоки, но их чтение и запись, по существу, выпол¬
няются последовательно). У вас имеется полный контроль над байтами в фай¬
ле и полная свобода действий, чтобы уплотнить его содержимое как угодно.
89
Глава 4. Хранение данных на уровне пакетной обработки
В отличие от хранилища пар ключ-значение”, файловая система дает вам имен¬
но то, что нужно, и ничего больше. Но в то же время она не ограничивает ваши
возможности корректировать соотношение затрат на хранение и обработку дан¬
ных. Более того, файловые системы реализуют системы мелкоструктурных раз¬
решений, идеально подходящие для соблюдения свойства неизменяемости.
Трудности применения обычной файловой системы заключаются в том, что она
существует только на одной машине, и поэтому масштабировать ее хранилище мож¬
но лишь до определенных пределов и вычислительных возможностей одной маши¬
ны. Но существует технология, которая называется распределенная файловая система,
очень похожая на обычную файловую систему, за исключением того, что ее храни¬
лище распределено в кластере машин. Для целей масштабирования в кластер добав¬
ляется просто больше машин. Распределенные файловые системы конструктивно
обладают определенной отказоустойчивостью, и даже если какая-нибудь машина
выходит из строя, то все файлы и данные по-прежнему остаются доступными.
У распределенных и обычных файловых систем имеются некоторые отличия.
Операции, которые можно выполнять в распределенных файловых системах, не¬
редко более ограничены, чем в обычных файловых системах. Например, после
создания файла запись данных в него или даже его модификация может быть
запрещена. Зачастую хранить в таких системах мелкие файлы неэффективно,
и поэтому размеры файлов желательно поддерживать относительно крупными,
чтобы пользоваться распределенной файловой системой надлежащим образом
(в качестве удобного эмпирического правила можно руководствоваться размером
файла порядка 64 Мбайт, хотя это зависит от конкретной файловой системы).
4.3. Принцип действия распределенных
файловых систем
Абстрактно описать принцип действия любой распределенной файловой си¬
стемы нелегко, поэтому дальнейшие пояснения будут основываться на примере
конкретной распределенной файловой системы под названием Hadoop Distributed
File System (HDFS). На наш взгляд, структура HDFS в достаточной степени пред¬
ставляет принцип действия распределенных файловых систем, чтобы продемон¬
стрировать возможности их применения на уровне пакетной обработки.
HDFS и Hadoop MapReduce являются двумя ответвлениями проекта Hadoop, ре¬
ализующего библиотеку Java для распределенного хранения и обработки больших
объемов данных. Система Hadoop развертывается на группе серверов, обычно на¬
зываемой кластером, a HDFS служит в качестве распределенной и масштабируемой
файловой системы, управляющей хранением данных в кластере. Hadoop - довольно
крупный и сложный проект, поэтому мы опишем его лишь в самых общих чертах.
У кластера HDFS имеются два типа узлов: единственный узел имен и несколь¬
ко узлов данных. Когда файл выгружается в систему IIDFS, он разбивается снача¬
ла на блоки фиксированного размера - как правило, от 64 до 256 Мбайт. Затем
происходит репликация каждого блока по нескольким (как правило, гром) узлам
данных, выбираемым произвольно. В узле имен отслеживается разбиение фай¬
ла на блоки и местоположение каждого узла. Такая с труктура храпения данных
приведена на рис. 4.2.
98
Часть J. Уровень пакетной обработки
Файл данных:
loge.txt
£>
О Все (обычно большие) файлы
разбиваются на блоки
от 63 до 256 Мбайт
Узел данных 1
Узел данных 2
Узел данных 3
пи
Узел данных 4
□□
Узел данных 5
Узел данных 6
© Эти блоки реплицируются
(обычно тремя копиями)
по серверам HDFS
(узлам данных)
© В узле имен предоставляются
услуги поиска для клиентов,
получающих доступ к данным,
а также обеспечивается
правильная репликация
блоков в кластере
Рис. 4.2. Файлы разбиваются на блоки, распределяемые по узлам данных в кластере
Такое распределение файла по многим узлам упрощает его параллельную
обработку. Когда программе требуется доступ к файлу, хранящемуся в системе
НБР8, она обращается к узлу имен, чтобы определить те узлы данных, где раз¬
мещается содержимое этого файла. Этот процесс иллюстрируется на рис. 4.3.
О Когда приложение
обрабатывает файл,
хранящийся в системе
НОРЭ, оно запрашивает /
сначала в узле имен /
местоположение блоков /
этого файла /
/
Клиентское
приложение
/
/ \ 4
\
\
\
\
\
\
\
\
\
© Как только становится
известным местоположение
блоков, приложение
ч\обращается к узлам данных
Для непосредственного
доступа к содержимому файла
Узел данных 1
Узел данных 2
□□
□і
Рис. 4.3. Клиенты связываются с узлом имен, чтобы определить те узлы данных,
где хранятся блоки требующегося файла
Кроме того, каждый блок реплицируется по нескольким узлам, и поэтому
данные остаются доступными даже в том случае, если отдельные узлы работа¬
ют в автономном режиме. Разумеется, такой отказоустойчивости присущи свои
ограничения. Так, если коэффициент репликации равен трем и одновременно
91
Глава 4. Хранение данных на уровне пакетной обработки
выходят из строя три узла, где хранятся миллионы байтов данных, то некоторые
блоки, хранящиеся в этих трех узлах, окажутся недоступными.
Реализовать распределенную файловую систему совсем не просто, но, по край¬
ней мере, вам теперь должно быть ясно, что в ней важнее всего для пользовате¬
лей. Таким образом, о распределенной файловой системе нужно знать следующее.
■ Файлы распределяются по многим машинам для целей масштабируемости
и параллельной обработки данных.
■ Блоки файлов реплицируются по нескольким узлам для достижения отка¬
зоустойчивости.
А теперь исследуем возможности сохранения главного массива данных с по¬
мощью распределенной файловой системы.
4.4. Сохранение главного массива данных
в распределенной файловой системе
Распределенные файловые системы отличаются теми видами операций, кото¬
рые в них разрешается выполнять. В одних распределенных файловых системах
допускается модифицировать существующие файлы, а в других — запрещается.
Если в одних файловых системам разрешается присоединять данные к уже суще¬
ствующим данным, то в других такая возможность отсутствует. В этом разделе
мы выясним, каким образом главный массив данных сохраняется в распределен¬
ной файловой системе самыми элементарными средствами, не допускающими
модификацию файла после его создания.
Очевидно, что если файл немодифицируемый, то в нем нельзя сохранить весь
главный массив данных. Вместо этого главный массив данных можно распреде¬
лить по многим файлам, сохранив их в одной папке. Каждый такой файл будет
содержать многие объекты сериализированных данных, как показано на рис. 4.4.
Объект
Объект
сериализированных данных
сериализированных данных
Объект
Объект-
сериализированных данных
сериал изированных данных
Объект
Объект
сериализированных д анных
сериализированных д анных
Файл: /баІаДіІеІ
Объект
сериализированных данных
Объект
сериализированных д анных
Файл: /ба1а/А1е2
Папка: /data/
Рис. 4.4. Распределение главного массива данных по многим файлам
Чтобы присоединить данные к главному массиву, достаточно ввести в папку
главного массива данных новый файл, содержащий новые объекты данных, как
показано на рис. 4.5.
92
Часть I, Уровень пакетной обработки
Объект
сериалиаированных данных
Объект
сериалиаированных данных
Объект
сериалиаированных данных
Файл: /data/file3
Выгрузить
Объект
сериалиаированных данных
Объект
сериалиэированных д анных
Объект
сериалиэированных данных
Файл: /data/file1
Объект
сериалиаированных данных
Объект
сериал изированных данных
Объект
сериалиаированных данных
Объект
сериал изированных данных
Объект
сериалиэированных данных
Файл:/сіаІаДІІе2
Папка: /data/
Рис. 4.5. Присоединение данных к главному массиву путем выгрузки
нового файла с новыми объектами данных
А теперь вернемся к требованиям, предъявляемым к хранению главного мас¬
сива данных, и проверим, насколько распределенные файловые системы удов¬
летворяют им (табл. 4.2).
Таблица 4.2. Перечень требований к хранению главного массива данных,
и насколько им удовлетворяют распределенные файловые системы
Операция
Требование
Описание
Запись
Эффективность присоединения
новых данных
Чтобы присоединить новые данные, достаточно
ввести новый файл в папку, содержащую главный
массив данных
Масштабируемость хранилища
В распределенных файловых системах хранилище
равномерно распределяется по всему кластеру
машин. Для расширения пространства хранения
данных и повышения производительности
операций ввода-вывода достаточно ввести
дополнительные машины
Чтение
Поддержка параллельной
обработки
В распределенных файловых системах все
данные распределяются по многим машинам,
что позволяет распараллелить обработку
данных на многих машинах. Как правило,
распределенные системы интегрированы
с каркасами распределенных вычислений вроде
MapReduce для упрощения обработки данных,
как поясняется в главе 6
Глава 4. Хранение данных на уровне пакетной обработки
93
Окончание табл. 4.2.
Операция Требование Описание
И то, и другое Коррекция затрат на хранение Как и в обычных файловых системах, порядок
и обработку сохранения блоков данных в файлах полностью
контролируется. В частности, можно выбрать
формат и степень уплотнения данных на уровне
отдельных записей и блоков или вообще
отказаться от уплотнения данных
Соблюдение неизменяемости Как правило, в распределенных файловых
системах употребляются такие же системы
разрешений, как и в обычных файловых
системах. Для соблюдения неизменяемости
можно запретить пользователю приложения
модификацию или удаление файлов в папке
главного массива данных. Такая избыточная
проверка защищает уже существующие данные
от программных и прочих ошибок, связанных
с человеческим фактором
На самом верхнем уровне распределенные файловые системы выглядят очень
простыми и вполне пригодными для хранения главного массива данных. Хотя,
как и любому другому инструментальному средству, им, безусловно, присущи свои
особенности, обсуждаемые в следующей иллюстративной главе. Но абстрактное
представление файлов и папок можно исследовать дальше, чтобы усовершен¬
ствовать хранение главного массива данных. Поэтому рассмотрим далее возмож¬
ности использования папок для вертикального разделения.
4.5. Вертикальное разделение
Несмотря на то что уровень пакетной обработки служит для выполнения
функций над целым массивом данных, многие вычисления не требуют анализа
всех данных. Например, возможны такие вычисления, для выполнения которых
требуется только информация, накопленная за последние две недели. Поэтому
хранилище на уровне пакетной обработки должно допускать разделение данных,
чтобы функции были доступны только те данные, которые требуются для ее вы¬
числения. Этот процесс называется вертикальным разделением и в значительной
степени способствует повышению эффективности уровня пакетной обработки.
И хотя вертикальное разделение совсем не обязательно на уровне пакетной об¬
работки, тем не менее, оно дает существенный выигрыш в производительности
при анализе всех данных и отсеивании всего лишнего на уровне пакетной обра¬
ботки. Поэтому очень важно знать и уметь пользоваться этим методом.
Вертикальное разделение данных в распределенной файловой системе мо¬
жет быть осуществлено сортировкой данных в отдельных папках. Допустим, что
информация о регистрации сохраняется в распределенной файловой системе.
При каждой регистрации эта информация содержит имя пользователя, 1Р-адрес
и отметку времени. Чтобы вертикально разделить данные по дням, достаточно
создать отдельную папку для хранения данных каждый день. Папка каждого дня
будет содержать много файлов с данными регистрации за день, как показано
на рис. 4.6. Если теперь потребуется рассмотреть конкретное подмножество из
94
Часть L Уровень пакетной обработки
массива данных, для этого достаточно просмотреть только нужные файлы в от¬
дельных папках, не обращая внимания на все остальные файлы.
Папка: /logins
Папка:/logins/2012-10-25
Файл: /logins/2012-10-25/loglns-2012-10-25.txt
alex 192.168.12.125 Thu Oct 25 22:33 - 22:46 (00:12)
bob 192.168.8.251 Thu Oct 25 21:04 - 21:28 (00:24)
Папка: /logins/2012-10-26
Файл: /logins/2012-10-26/logins-2012-10-26-part 1 .txt
Файл: /logins/2012-10-26/logins-2012-10-26-part2.txt
Рис. 4.6. Схема вертикального разделения данных регистрации. Благодаря тому что информация
по каждой дате отсортирована в отдельных папках, функция может выбрать только те папки,
которые содержат данные, требующиеся для вычисления этой функции
4.6. Низкоуровневый характер
распределенных файловых систем
Несмотря на то что распределенные файловые системы обладают свойства¬
ми запоминания и отказоустойчивости, требующимися для хранения главного
массива данных, непосредственное обращение к их прикладным программным
интерфейсам (API) носит слишком низкоуровневый характер для задач, которые
приходится решать. Проиллюстрируем это положение на примере операций
в обычной файловой системе Unix, чтобы показать трудности, которые могут
возникнуть при решении таких задач, как присоединение данных к главному мас¬
сиву или вертикальное разделение главного массива данных.
Начнем с присоединения данных к главному массиву. Допустим, главный мас¬
сив данных находится в папке /master, а данные, которые требуется разместить
в главном массиве, — в папке /new-data. Допустим также, что в папках данные
содержатся в файлах, как показано на рис. 4.7.
Файл: /new-data/file2
Файл: /new-data/file3
Файл: /new-data/file9
Папка: /new-data/
Файл: /master/filel
Файл: /master/file2
Файл: /master/file8
Папка: /master/
Рис. 4.7. Пример размещения в папках данных, которые требуется
присоединить к главному массиву. Имена файлов могут перекрываться
95
Глава 4. Хранение данных на уровне пакетной обработки
Очевиднее всего, попробовать воспользоваться псевдокодом вроде следующего:
foreach file : "/new-data" <j.
mv file "/master/" ^
Перебрать все файлы
в папке /new-data
Переместить файл
в папку /master
К сожалению, этот код страдает серьезными недостатками. Если в папке глав¬
ного массива данных содержатся любые файлы с одинаковым именем, то опе¬
рация mv завершится неудачно. Для правильного ее выполнения следует непре¬
менно переименовать файл, присвоив ему произвольное имя, чтобы избежать
конфликтов.
Кроме того, одно из основных требований к хранению главного массива дан¬
ных состоит в возможности найти компромисс между затратами на хранение
и обработку данных. При хранении главного массива данных в распределенной
файловой системе выбирается формат файла и уплотнения для достижения нуж¬
ного компромисса. Но что, если файлы в папке /new-data имеют не такой фор¬
мат, как файлы в папке /master? В таком случае операция mv вообще не будет
выполнена. Вместо этого придется скопировать записи из папки /new-data в со¬
вершенно новый файл, используя формат из папки /master.
А теперь рассмотрим выполнение той же самой операции, но с вертикально
разделенным главным массивом данных. Допустим, что папки /new-data и /master
выглядят так, как показано на рис. 4.8.
Рис. 4.8. Если целевой главный массив данных разделен вертикально, то для присоединения
к нему данных уже недостаточно ввести файлы в папку этого массива
Файл: /new-data/file2
Файл: /new-data/file3
Файл: /new-data/file9
Папка: /new-data/
Файл: /maste r/age/f 11е 1
Файл: /master/ageAile2
Папка: /master/age/
Файл: /master/bday/flle 1
Файл: /master/bday/file2
Папка: /master/bday/
Папка: /master/
Было бы неверно переместить только файлы из папки /new-data в корень
папки /master, поскольку это было бы сделано без учета вертикального разделе¬
ния в папке /master. В любом случае операция присоединения данных была бы
запрещена из-за неверного вертикального разделения данных в папке /new-data,
иначе такое разделение данных в этой папке должно было бы стать частью опе¬
рации присоединения. Но если для операции с файлами и нанками обратиться
непосредственно к прикладному программному интерфейсу, то можно легко до¬
пустить ошибку, нарушив ограничения, накладываемые на вертикальное разделе¬
ние в главном массиве данных.
96
Часть /. Уровень пакетной обработки
Все действия и проверки, которые должны быть произведены для обеспече¬
ния правильности выполнения подобных операций, явно указывают на то, что
файлы и папки имеют слишком низкий уровень абстракции для манипулирова¬
ния массивами данных. В следующей иллюстративной главе будет продемонстри¬
рован пример библиотеки, автоматизирующей подобные операции.
4.7. Хранение главного массива данных
из приложения SuperWebAnalytics.com
в распределенной файловой системе
А теперь воспользуемся распределенной файловой системой для хранения
главного массива данных из приложения SuperWebAnalytics.com. Работу над этим
проектом мы остановили на создании граф-схемы для представления массива
данных. Каждое ребро и свойство в этой граф-схеме представлено собственным
независимым блоком данных СаЬаиплТ. На рис. 4.9 еще раз показано, как выгля¬
дит эта граф-схема.
О Граф-схема состоит
из двух типов узлов:
людей и просматриваемых
ними страниц
Пол:
Имя:
женский
Том
—
Эквивалент
Лицо (идентификатор
пользователя):
200
) Ребра, проведенные
между узлами людей,
обозначают одного и
того же пользователя,
распознаваемого другими
средствами. А ребра,
проведенные между узлами
лиц и страниц, обозначают
отдельный просмотр страницы
Место жительства:
г Сан-Франциско,
шт. Калифорния
' Лицо (идентификатор
пользователя):
123
Просмотр страницы
Страница:
http://mysite.сот/
Просмотр
страницы Просмотр страницы
Страница:
\http: //mysite. com/blo^
Всего
Всего
просмотров:
просмотров:
452
25
© Свойства представляют
подсчеты просмотров страниц
и демографическую информацию
отдельных лиц
Рис. 4.9. Граф-схема для приложения SuperWebAnalytics.com
Как видите, эта граф-схема обеспечивает естественное вертикальное разделе¬
ние данных. Все типы ребер и свойств можно хранить в отдельных панках. Такое
вертикальное разделение данных позволяет эффективно выполнять вычисления,
в ходе которых анализируются только определенные свойства и ребра.
97
Глава 4. Хранение данных на уровне пакетной обработки
Резюме
Высокоуровневые требования к хранению данных на уровне пакетной обра¬
ботки лямбда-архитектуры довольно просты. Как было показано в этой главе, по¬
добные требования могут быть сведены в обязательный перечень для решения
задачи хранения, и для этой цели естественным образом подходит распределен¬
ная файловая система. Ее применение не должно вызывать особых трудностей
у тех, кто привык обращаться с файловыми системами.
В следующей главе будут подробно рассмотрены особенности применения
распределениых файловых систем на практике, а также показано, как обращать¬
ся с низкоуровневыми по своему характеру файлами и папками на высоком уров¬
не абстракции.
Иллюстрация
хранения данных
на уровне пакетной
обработки
В этой главе...
■ Применение распределенной файловой
системы HDFS.
■ Высокоуровневая абстракция для манипулиро¬
вания массивами данных в библиотеке Pail.
В предыдущей главе были рассмотрены требования к хранению главного
массива данных, а также показано, что этим требованиям вполне удовлетворяет
распределенная файловая система. По в то же время в ней пояснялось, что не¬
посредственное обращение к АР1 такой файловой системы носит слишком низ¬
коуровневый характер для всех видов операций, которые требуется выполнить
над главным массивом данных. А в этой главе будет показано, как пользоваться
конкретной распределенной файловой системой МОРЯ на практике и как авто¬
матизировать решаемые задачи с помощью программного интерфейса более вы¬
сокого уровня.
Как и во всех остальных иллюстративных главах данной книги, мы уделим
основное внимание конкретным инструментальным средствам и рассмотрим
особенности применения высокоуровневых понятий, представленных в преды¬
дущей главе. Наша цель, как всегда, — не сравнивать и противопоставлять вес
возможные инструментальные средства, но подкрепить конкретными примера¬
ми применение высокоуровневых понятий па практике.
100
Часть /. Уровень пакетной обработки
5.1. Применение распределенной
файловой системы HDFS
Основы работы распределенной файловой системы HDFS были рассмотрены
в предыдущей главе. Они еще раз приводятся ниже для краткого напоминания.
■ Файлы разделяются на блоки, распределяемые но многим узлам в кластере.
■ Блоки реплицируются по многим узлам, чтобы обеспечить постоянную до¬
ступность данных при выходе машин из строя.
■ В узле имен отслеживаются блоки из каждого файла, а также места их хра¬
нения.
Знакомство с системой Hadoop
Настройка системы Hadoop может оказаться непростой задачей. В системе Hadoop име¬
ются многочисленные параметры конфигурации, которые должны быть настроены на
оптимальный режим работы оборудования. Чтобы не увязнуть в подробностях, рекомен¬
дуем загрузить предварительно сконфигурированную виртуальную машину для первого
знакомства с Hadoop. Эта виртуальная машина ускорит процесс изучения системы HDFS
и каркаса MapReduce, а также даст возможность лучше понять, когда следует устанав¬
ливать кластер.
На момент написания данной книги все поставщики Hadoop (компании Cloudera,
Hortonworks и MapR) предоставляли общедоступный учебный материал для изучения
данной системы. Рекомендуем получить доступ к Hadoop, чтобы проработать примеры,
приведенные в этой и последующих главах.
Рассмотрим применение интерфейса API системы HDFS для манипулирова¬
ния файлами и папками. Допустим, что требуется сохранить все данные реги¬
страции на сервере. Ниже приведены некоторые примеры подобных данных.
$ cat logins-2012-10-25.txt
alex 192.168.12.125 Thu Oct 25 22:33 - 22:46 (00:12)
bob 192.168.8.251 Thu Oct 25 21:04 - 21:28 (00:24)
Charlie 192.168.12.82 Thu Oct 25 21:02 - 23:14 (02:12)
doug 192.168.8.13 Thu Oct 25 20:30 - 21:03 (00:33)
Чтобы сохранить эти данные в системе РШГй, можно создать каталог для хра¬
нения массива данных и выгрузки файла следующим образом:
Команды hadoop f s выполняются в оболочке Hadoop
для непосредственного взаимодействия с системой HDFS.
Их полный перечень приведен по адресу:
http://hadoop.apache.org/
$ hadoop fs -mkdir /logins о
$ hadoop fs -put logins-2012-10-25.txt /logins
При автоматической выгрузке файл
разбивается на блоки, которые
распределяются по узлам данных
101
Глава 5. Иллюстрация хранения данных на уровне пакетной обработки
Вывести содержимое каталога списком можно следующим образом:
$ hadoop fs -Is “R /logins <
-rw-r~r~ 3 hdfs hadoop 175802352 2012-10-26 01:38
/logins/logins-2012-10-25.txt
Команда is подобна
одноименной команде
в OC Unix
А проверить содержимое файла можно так:
$ hadoop fs -cat /logins/logins-2012-10-25.txt
alex 192.168.12.125 Thu Oct 25 22:33 - 22:46 (00:12)
bob 192.168.8.251 Thu Oct 25 21:04 - 21:28 (00:24)
Как упоминалось ранее, файл автоматически разбивается на блоки и распре¬
деляется по узлам данных, когда он выгружается. Блоки и их местоположение
можно распознать по следующей команде:
$ hadoop fsck /logins/logins-2012-10-25.txt -files -blocks -locations
/logins/logins-2012-10-25.txt 175802352 bytes, 2 block(s):
OK
0. blk_-1821909382043065392_1523 len=134217728 <
repl=3 [10.100.0.249:50010, 10.100.1.4:50010, 10.100.0.252:50010]
1. blk_2733341693279525583_1524 len=41584624
repl=3 [10.100.0.255:50010, 10.100.1.2:50010, 10.100.1.5:50010]
Файл хранится
в двух блоках
1Р-адреса и номера портов в узлах данных,
где располагается каждый блок
5.1.1. Недостатки небольших файлов
Компоненты HDFS и MapReduce системы Hadoop тесно связаны, образуя
инфраструктуру для хранения и обработки больших объемов данных. Каркас
MapReduce будет подробнее рассмотрен в последующих главах, а до тех пор сле¬
дует сказать, что вычислительные возможности системы Hadoop заметно сокра¬
щаются, когда данные хранятся во множестве небольших файлов, находящихся в
системе HDFS. В частности, выполнение задания MapReduce может замедлиться
на порядок величины, если оно обрабатывает данные объемом 10 Гбайт из мно¬
жества небольших файлов, а не из нескольких больших файлов.
Дело в том, что задание MapReduce запускает целый ряд задач, каждая из ко¬
торых предназначена для обработки одного блока из входного массива данных.
И каждая задача несет определенные издержки на планирование и согласование
своего выполнения, что влечет за собой постоянные затраты, поскольку для обра¬
ботки каждого небольшого файла требуется отдельная задача. Такая особенность
выполнения заданий MapReduce требует объединения данных из небольших фай¬
лов в массиве данных. С этой целью можно было бы написать код, в котором
применяется интерфейс HDFS API, или же сформировать специальное задание
MapReduce, но для реализации обоих подходов потребуются основательные зна¬
ния внутреннего механизма работы MapReduce и немало труда.
102
Часть I. Уровень пакетной обработки
5.1.2. Переход на более высокий уровень абстракции
Следует особо подчеркнуть, что в данной книге рассматриваются решения, ко-
торые являются не только масштабируемыми, отказоустойчивыми и производи¬
тельными, но также изящными. Отчасти изящество решения определяется тем, что
оно должно быть в состоянии лаконично выразить производимые вычисления.
Что же касается манипулирования главным массивом данных, то в предыду¬
щей главе были представлены следующие две важные операции.
■ Присоединение данных к массиву.
■ Вертикальное разделение массива данных и принятие мер против наруше¬
ния уже существующего разделения данных.
Эти требования дополняются следующим характерным для HDFS требованием:
небольшие файлы должны эффективно объединяться в большие файлы. Как было
показано в предыдущей главе, решение подобных задач непосредственно в фай¬
лах и папках оказывается трудоемким и чреватым ошибками. Поэтому для более
изящного их решения мы воспользуемся специальной библиотекой. В отличие от
кода, в котором применяется интерфейс HDFS API, в приведенном ниже листинге
демонстрируется применение библиотеки Pail.
Листинг 5.1. Абстракции задач сопровождения в системе HDFS
import java.io.IOException;
import backtype.hadoop.pail.Pail;
public class PailMove {
public static void mergeData (String masterDir, String updateDir)
throws IOException
“Ведра” служат оболочками вокруг
папок в системе HDFS
Pail target = new Pail (masterDir);
Pail source = new Pail (updateDir);
target.absorb(source);
target.consolidate(); <—
С помощью библиотеки Pail присоединения
превращаются в однострочные операции
Небольшие файлы данных в "ведре" можно
также объединить в одном вызове функции
Библиотека Pail позволяет присоединять папки в одной строке кода и объеди¬
нять вместе небольшие файлы. Если при объединении данные в целевой папке
оказываются в разных форматах файлов, библиотека Pail автоматически приве¬
дет новые данные к нужному формату. Если же в целевой папке употребляется
другая схема вертикального разделения, библиотека Pail сгенерирует соответ¬
ствующее исключение. А самое главное, что такая абстракция на более высоком
уровне, как в Pail, позволяет обрабатывать данные непосредственно, а не поль¬
зоваться низкоуровневыми контейнерами вроде файлов и каталогов.
КРАТКОЕ ПОДВЕДЕНИЕ ИТОГОВ
Прежде чем рассматривать возможности библиотеки Pail дальше, умест¬
но сделать небольшое отступление, чтобы восстановить более общую картину.
Напомним, что главный массив данных является источником истины в лямбда-ар¬
хитектуре, а следовательно, большие, постоянно разрастающиеся массивы
103
Глава 5. Иллюстрация хранения данных на уровне пакетной обработки
данных должны непременно обрабатываться на уровне пакетной обработки.
Кроме того, для ответов на конкретные запросы должны быть простые и эффек¬
тивные средства преобразования данных в пакетные представления. Материал
этой главы носит более технический характер, чем в предыдущих главах, но всег¬
да следует иметь в виду порядок интеграции компонентов в лямбда-архитектуре.
5.2. Хранение данных на уровне пакетной
обработки с помощью библиотеки Pail
Библиотека Pail обеспечивает тонкое абстрагирование файлов и папок от би¬
блиотеки dfs-datastores (http://github.com/nathanmarz/dfs--datastores). Такое
абстрагирование значительно упрощает управление коллекцией записей для па¬
кетной обработки. Как подразумевает название библиотеки Pail, в ней применя¬
ются так называемые “ведра” — папки, в которых хранятся метаданные о массиве
данных. Применяя эти метаданные, библиотека Pail позволяет безопасно действо¬
вать на уровне пакетной обработки, не беспокоясь о нарушении его целостности.
Назначение библиотеки Pail — сделать важные операции (присоединения данных
к массиву, вертикального разделения и объединения) безопасными, простыми
и производительными.
Внутренне Pail является обычной библиотекой Java, где применяются интер¬
фейсы Hadoop API. Она обеспечивает взаимодействие с файловой системой на
низком уровне, предоставляя API, изолирующий пользователя этой библиотеки
от сложностей внутреннего механизма Hadoop. Самое главное для библиотеки
Pail — позволить ее пользователю сосредоточить основное внимание на самих
данных, а не заботиться о том, как их хранить и сопровождать.
О значении библиотеки Pail
Как и многие другие инструментальные средства, рассматриваемые в данной книге, би¬
блиотека Pail была написана Натаном Марцем в процессе разработки лямбда-архитекту¬
ры. Мы представляем здесь эти технологии не для того, чтобы пропагандировать их, а для
того, чтобы обсуждать контекст их происхождения и решаемые ими проблемы. А посколь¬
ку библиотека Pail была разработана Натаном Марцем, то она идеально соответствует
рассмотренным до сих пор требованиям, предъявляемым к главному массиву данных,
и эти требования естественно вытекают из основных принципов формирования запро¬
сов как функции всех данных. Впрочем, вы вольны пользоваться другими библиотеками
или разрабатывать свои, а наша цель — предложить особый путь связывания принципов
построения систем больших данных с имеющимися инструментальными средствами.
Ранее уже были рассмотрены характеристики системы HDFS, благодаря кото¬
рым она является вполне жизнеспособным вариантом выбора для хранения глав¬
ного массива данных на уровне пакетной обработки. Применяя библиотеку Pail,
имейте в виду, что она сохраняет преимущества системы HDFS и в то же время
оптимизирует операции над данными. Итак, рассмотрев основные операции Pail,
подведем краткий итог общим ценностям данной библиотеки. В частности, рассмо¬
трим действие этой библиотеки па примерах создания и записи данных в ведро .
104
Часть I. Уровень пакетной обработки
5.2.1. Основные операции в Pall
Понять принцип действия библиотеки РаП лучше всего на примере написа-
ния и выполнения кода на компьютере. С этой целью вам придется загрузить ис¬
ходный код библиотеки с^8-с1а1а81:оге8 из хранилища данных СиНиЬ и построить
ее. Если в вашем распоряжении нет кластера Наскюр или виртуальной машины,
в рассматриваемых далее примерах ваша локальная система будет трактоваться
как ЬШЕ8. В таком случае вы сможете увидеть результаты выполнения вводимых
команд, исследуя соответствующие каталоги в своей файловой системе.
Итак, начнем с создания нового “ведра” и сохранения в нем некоторых данных:
Создает“ведро"
по умолчанию
в указанном
каталоге
Закрывает
текущий файл
public static void simplelOO throws IOException {
Pail pail = Pail.create("/tmp/mypail"); <
—1>TypedRecordOutputStream os = pail.openWrite ();
Предоставляет
поток вывода
в новый файл
в библиотеке Pail
os.writeObject(new byte[] {1, 2, 3}); <]
os.writeObject (new byte [] {1, 2, 3, 4});
os.writeObject (new byte [] {1, 2, 3, 4, 5})
os.close();
В“ведре"
без метаданных
допускается хранить
только массивы байтов
Проверяя свою файловую систему, вы непременно обнаружите, что в ней
была создана папка /Ьтр/тураіі, содержащая следующие файлы:
root:/ $ Is /tmp/mypail
f2fa3af0-5592-43e0~a29c-fb6b056af8a0.pailfile <ъ
pail.meta < I Метаданные описывают содержимое
I и структуру “ведра"
Записи хранятся в профилях
“Ведерный” файл содержит сохраненные вами записи. Этот файл создается
автоматически, и поэтому все созданные вами записи появляются сразу, т.е. при¬
ложение, читающее записи из “ведра”, не заметит файл до тех пор, пока он не
закроется в потоке записи. Кроме того, в “ведерных” файлах употребляются гло¬
бально однозначные имена, чтобы они назывались иначе в файловой системе.
Эти однозначные имена позволяют параллельно и без всяких конфликтов запи¬
сывать данные из многих источников в одно и то же “ведро”.
Другой файл в данном каталоге содержит метаданные “ведра”. Эти метадан¬
ные описывают тип данных, а также порядок их хранения в “ведре”. В рассма¬
триваемом здесь примере метаданные вообще не были указаны при построении
“ведра”, и поэтому в следующем файле содержатся настройки по умолчанию:
root:/ $ cat /tmp/mypail/pail.meta
format: SequenceFile <-
Формат файлов в "ведре"; по умолчанию
в файлах формата БедиепсеРНе, находящихся
в “ведре” системы Набоор, данные сохраняются
в виде пар “ключ-значение"
Аргументы описывают содержимое “ведра";
пустое отображение предписывает Pali трактовать
данные как неуплотненные массивы байтов
Далее в этой главе будет рассмотрен другой файл pail.meta, содержащий бо¬
лее основательные данные, но общая его структура остается прежней. А теперь
выясним, как средствами библиотеки Pail сохранять в “ведрах” реальные объек¬
ты, а не просто двоичных записи.
105
Глава 5, Иллюстрация хранения данных на уровне пакетной обработки
5.2.2. Сериализация объектов в “ведрах”
Чтобы сохранить объекты в “ведре”, необходимо снабдить библиотеку Pail
инструкциями по сериализации объектов в двоичные данные и их десериализа¬
ции в обратном порядке. Вернемся к примеру с данными регистрации на серве¬
ре, чтобы показать, как это делается. В приведенном ниже листинге демонстри¬
руется упрощенный код класса, представляющего процесс регистрации.
Листинг 5.2. Класс для регистрации без всяких излишеств
public class Login {
public String userName;
public long loginUnixTime;
public Login(String _user, long _login) {
userName = _user;
loginUnixTime = _login;
}
Чтобы сохранить объекты типа Login в “ведре”, необходимо создать класс,
реализующий интерфейс PailStructure. В приведенном ниже листинге опреде¬
ляется класс LoginPailStructure, описывающий порядок выполнения сериали¬
зации.
Листинг 5.3. Реализация интерфейса PailStructure
public class LoginPailStructure implements PailStructure<Login>{
"Ведро" со
структурой
для хранения
только
объектов
типа Login
к> public Class getType () {
return Login.class;
}
Объекты типа Login
должны быть сериализированы
при сохранении в “ведерных"
файлах
public byte[] serialize(Login login) { <—
ByteArrayOutputStream byteOut = new ByteArrayOutputStreamO ;
DataOutputStream dataOut = new DataOutputStream (byteOut) ;
byte [ ] userBytes = login. userName. getBytes ();
try {
dataOut.writelnt(userBytes.length);
dataOut.write(userBytes);
dataOut.writeLong(login.loginUnixTime);
dataOut.close();
} catch(IOException e) {
throw new RuntimeException (e);
}
return byteOut.toByteArray0;
}
Объекты типа Login
восстанавливаются
при последующем чтении
из “ведерных" файлов
public Login deserialize(byte[] serialized) { <i—
DatalnputStream dataln =
new DatalnputStream (new ByteArraylnputStream(serialized))
try {
bytel] userBytes = new byte[dataln.readlntO];
dataln.read(userBytes);
return new Login(new String (userBytes), dataln.readLong());
106
Часть /, Уровень пакетной обработки
} catch(IOException е) {
throw new RuntimeException (e) ;
)
}
В методе
деЪТагдеЬ()
определяется
схема
вертикального
разделения,
хотя она не
используется
в данном
примере
-t>public List<String> getTarget (Login object)
return Collections.EMPTY JjIST;
}
public boolean isValidTarget(String... dirs)
return true;
}
}
В методе 1аУа^ТагдеЬ ()
определяется соответствие
заданного пути
схеме вертикального
разделения,
но и он не используется
в данном примере
Если передать объект типа LoginPailStructure функции created из библи¬
отеки Pail, как показано ниже, то инструкции для сериализации будут использо¬
ваны в результирующем “ведре”. После этого достаточно передать ему объекты
типа Login, чтобы они были автоматически сериализированы средствами Pail.
public static void writeLogins () throws IOException {
Pail<Login> loginPail = Pail.create("/tmp/logins",
new LoginPailStructure()); <
TypedRecordOutputStream out = loginPail.openWrite();
out. writeObject (new Login ("alex", 1352679231)); Создает "ведро"
out. writeObject (new Login ("bob", 1352674216)); с новой структурой
out.close();
}
Аналогично при чтении данных записи автоматически десериализируются
средствами Pail. Ниже показано, как перебрать все только что записанные объ¬
екты. Как только данные будут сохранены в “ведре”, их можно благополучно об¬
работать с помощью встроенных в Pail операций.
public static void readLoginsO throws IOException {
Pail<Login> loginPail = new Pail<Login>("/tmp/logins");
for(Login 1 : loginPail) { <
System, out .print In (l.userName + " " + l.loginUnixTime); Для типа объектов
. в "ведре” поддерживается
' интерфейс Iter able
5.2.3. Выполнение пакетных операций средствами Pail
В библиотеку Pail встроена поддержка целого ряда типичных операций.
Именно в этих операциях проявляются преимущества управления записями
не вручную, а средствами Pail. Все операции реализованы с помощью каркаса
MapReduce и поэтому допускают масштабирование независимо от объема дан¬
ных в “ведре”, будь то в гига- или терабайтах. Мы еще вернемся к более под¬
робному рассмотрению каркаса MapReduce в последующих главах, а пока лишь
заметим, что операции автоматически распараллеливаются и выполняются на
всех рабочих машинах в кластере.
107
Глава 5. Иллюстрация хранения данных на уровне пакетной обработки
В предыдущем разделе обсуждалось, насколько важны операции присоедине¬
ния и объединения. Как и следовало ожидать, оба эти вида операций поддер¬
живаются в библиотеке Pail. Особенно изящна операция присоединения. Она
проверяет, насколько допустимо присоединять “ведра” друг к другу. В частности,
она не позволит присоединить “ведро”, содержащее символьные строки, к “ве¬
дру , содержащему целые числа. Если в “ведрах” хранятся записи одинакового
типа, но в разных форматах файлов, то операция присоединения приведет дан¬
ные в соответствие с форматом целевого “ведра”. Это означает, что для данного
“ведра будет соблюден выбранный компромисс между затратами на хранение
и обработку данных.
По умолчанию операция объединения осуществляет слияние небольших фай¬
лов в новые файлы, размеры которых как можно более точно соответствуют
128 Мбайт, т.е. стандартному размеру блока в системе HDFS. Эта операция рас¬
параллеливается средствами MapReduce.
Допустим, что в рассматриваемом здесь примере дополнительные данные ре¬
гистрации находятся в отдельном “ведре” и требуется соединить их с исходным
“ведром”. Обе операции присоединения и объединения выполняются в следую¬
щем фрагменте кода:
public static void appendDataO throws IOException {
Pail<Login> loginPail = new Pail<Login>("/tmp/loginsn) ;
Pail<Login> updatePail = new Pail<Login>( Vtmp/updates") ;
loginPail.absorb(updatePail) ;
loginPail.consolidate();
}
Из всего изложенного выше можно сделать следующий главный вывод: встро¬
енные функции позволяют сосредоточить основное внимание на обработке дан¬
ных, не заботясь о правильности манипулирования файлами.
5.2.4. Вертикальное разделение средствами Pail
Как упоминалось ранее, вертикальное разделение данных в системе HDFS
можно произвести, используя несколько папок. Если попытаться управлять
вертикальным разделением вручную, то можно легко забыть, что два массива
данных разделяются по-разному, соединив их по ошибке. Аналогично не соста¬
вит большого труда случайно нарушить структуру разделения при объединении
данных. Правда, библиотека Pail обладает достаточно развитой логикой, чтобы
соблюсти структуру “ведра” и уберечь от подобного рода ошибок.
Чтобы создать структуру разделенных каталогов для “ведра”, необходимо реа¬
лизовать два дополнительных метода из интерфейса PailStructure. Эти методы
перечислены ниже.
■ Метод getTarget о . Получает запись, определяет структуру каталогов, где
эта запись должна храниться, и возвращает путь в виде списка объектов
типа String.
■ Метод isValidTarget (). Получает массив объектов типа String, составля¬
ет путь к каталогу и определяет, согласуется ли он со схемой вертикально¬
го разделения.
108
Часть /. Уровень пакетной обработки
Эти методы используются в библиотеке Pail для соблюдения структуры катало¬
гов и автоматического распределения записей по соответствующим подкаталогам.
В приведенном ниже листинге демонстрируется вертикальное разделение объек¬
тов типа Login таким образом, чтобы сгруппировать записи по дате регистрации.
Листинг 5.4, Схема вертикального разделения записей типа Login
public class PartitionedLogiriPailStructure extends LoginPailStructure {
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");
Данные
регистрации
вертикально
разделяются
по папкам в
соответствии
с датой
регистрации
public List<String> getTarget(Login object)
ArrayList<String> directoryPath = new ArrayList<String>();
Date date = new Date(object.loginUnixTime * 1000L); <j
directoryPath.add(formatter.format(date));
return directoryPath; Отметка времени объекта типа Login
} преобразуется в понятную форму
public boolean isValidTarget(String... strings) {
if (strings.length != 1) return false;
try {
return (formatter.parse(strings[0]) !=null);
}
catch(ParseException e) {
return false;
Метод ІвУаІісІТагдаЬ ()
проверяет, имеет ли
структура каталогов
единичную глубину,
а имя папки — дату
}
}
Исходя из новой структуры “ведра”, библиотека Pail выбирает подходящий
подкаталог всякий раз, когда записывается новый объект типа Login:
public static void partitionData() throws IOException {
Pail<Login> pail = Pail.create("/tmp/partitioned_logins",
new PartitionedLoginPailStructure());
TypedRecordOutputStream os = pail.openWrite();
os.writeObject (new Login("chris", 1352702020)); <-
os.writeObject (new Login("david", 1352788472)); o-
os.close ();
' 1352788472 - ОТМвТКЭ времени
1 2012-11-12, 22:34:32 PST
Проверив этот новый “ведерный” каталог, можно убедиться, что данные были
разделены правильно:
1352702020 — ОТМвТКЭ времени
2012-11-11, 2:33:40 PST
root:/ $ Is -R /tmp/partitioned_logins
2012-11-11 2012-11-12 pail.meta
/tmp/partitioned_logins/2012-ll-ll: <
d8c0822b-6caf-4516-9c74-24bf805d565c.pailfile
В "ведре" были созданы
папки для разных
дат регистрации
/tmp/partitioned_logins/2012-ll-12:
d8c0822b-6caf-4516-9c74-24bf805d565c.pailfile
5.2.5. Форматы и уплотнение *ведерных" файлов
В структуре каталогов “ведра” данные сохраняются во многих файлах. Чтобы
контролировать порядок сохранения записей в этих файлах, достаточно указать
109
Глпви 5. Иллюстрация хранения данных на уровне пакетной обработки
используемый формат файлов. Это дает возможность выбрать компромисс меж¬
ду величиной пространства для хранения данных и производительностью чте¬
ния записей из ведра . Как пояснялось ранее в главе, это основной контроль,
необходимый для удовлетворения потребностей приложения в настройке.
Несмотря на возможность реализовать собственный формат файлов, в библи¬
отеке Pail по умолчанию используются файлы формата Sequence File на платфор¬
ме Hadoop. Этот широко употребляемый формат позволяет параллельно обра¬
батывать отдельные файлы средствами MapReduce. Он также имеет встроенную
поддержку уплотнения записей в файлах.
В качестве примера ниже показано, как создать “ведро”, в котором использу¬
ется формат SequenceFile и алгоритм блочного уплотнения gzip.
Содержимое
“ведра” будет
уплотнено по
алгоритму gzlp
public static void createCompressedPail() throws IOException {
Map<String, 0bject> options = new HashMap<String, 0bject>();
options .put (SequenceFileFormat.C0DEC_ARG,
> SequenceFileFormat.C0DEC_ARG_GZIP);
options.put(SequenceFileFormat.TYPE_ARG,
SequenceFileFormat.TYPE_ARG_BL0CK);
Блоки записи будут
сжаты вместе,
а не отдельно
по строкам
LoginPailStructure struct = new LoginPailStructure ();
Pail compressed = Pail.create("/tmp/compressed",
new PailSpec("SequenceFile", options, struct));
Создает новое "ведро" для хранения
объектов типа Login в нужном формате
Все эти свойства можно затем наблюдать в метаданных “ведра”, как показано
ниже.
root:/ $ cat /tmp/compressed/pail.meta
format: SequenceFile
structure : manning.LoginPailStructure
Полное имя класса
LoginPailStructure
args:
compressionCodec: gzip <ь
compressionType: block
Параметры уплотнения
“ведерных” файлов
Всякий раз, когда записи вводятся в это “ведро”, они автоматически уплотня¬
ются. Это “ведро” занимает намного меньше места, но требует больших затрат
вычислительных ресурсов ЦП на чтение и запись данных.
5.2.6. Преимущества библиотеки Pail
Потратив время на изучение внутреннего механизма работы библиотеки Pail,
очень важно уяснить преимущества, которые она дает по сравнению с исход¬
ными возможностями системы HDFS. В табл. 5.1 приведены выгоды, которые
приносит библиотека Pail по отношению к упоминавшемуся ранее перечню тре¬
бований к хранению данных в главном массиве на уровне пакетной обработки.
На этом краткий экскурс в возможности библиотеки Pail завершается. Она
служит удобной и эффективной абстракцией для взаимодействия с данными на
уровне пакетной обработки, избавляя от необходимости вникать в подробности
функционирования базовой файловой системы.
114)
Часть /. Уровень пакетной обработки
Таблица 5.1. Преимущества библиотеки Pail для хранения данных в главном массиве
Операция
Требование
Описание
Запись
Эффективность
присоединения
новых данных
Библиотека Pail предоставляет первоклассный интерфейс
для присоединения данных и уберегает от выполнения
недопустимых операций, на что неспособен исходный
интерфейс HDFS API
Масштабируемость
хранилища
Узел имен содержит все пространство имен HDFS
в оперативной памяти, что может оказаться
обременительным, если в файловой системе находится
большое количество небольших файлов. Операция
объединения в Pail сокращает общее количество блоков
HDFS и облегчает бремя, накладываемое на узел имен
Чтение
Поддержка
параллельной
обработки
Количество задач в задании MapReduce определяется
количеством блоков в массиве данных. Объединение
содержимого "ведра” позволяет сократить количество
требуемых задач и повысить эффективность обработки
данных
Возможность
вертикального
разделения данных
Выводимые данные, записываемые в "ведре",
автоматически разделяются, причем каждый факт хранится
в соответствующем каталоге. Подобная структура каталогов
строго соблюдается для всех операций в Pail
И то и другое
Коррекция затрат
на хранение и
обработку
В библиотеку Pail встроена поддержка приведения данных
в формат, указанный в структуре "ведра”. Такое приведение
происходит автоматически при выполнении операций над
содержимым "ведра"
Соблюдение
неизменяемости
Библиотека Pail служит лишь тонкой оболочкой вокруг
файлов и папок, и поэтому она позволяет соблюсти свойство
неизменяемости установкой соответствующих разрешений,
как это делается непосредственно в системе HDFS
5.3. Хранение главного массива данных
для приложения SuperWebAnalytics.com
Как было показано в предыдущей главе, высокоуровневые принципы для
хранения данных в приложении SuperWebAnalytics.com довольно просты. Они
состоят в том, чтобы воспользоваться распределенной файловой системой и
вертикальным разделением, сохраняя свойства и ребра граф-схемы в разных
подкаталогах. А теперь воспользуемся уже известными инструментальными сред¬
ствами, чтобы воплотить эти принципы в жизнь.
Напомним исполняемую схему, разработанную средствами Apache Thrift для
приложения SuperWebAnalytics.com. Ниже приведен фрагмент этой схемы.
struct Data { -i
1: required Pedigree pedigree;
2: required DataUnit dataunit;
Все факты в массиве данных представлены
отметкой времени базовым блоком данных
union DataUnit { о —
1: PersonProperty pers
I Базовый блок данных описывает ребра
| и свойства граф-схемы массива данных
оп_ргорегТу;
Глава 5. Иллюстрация хранения данных
на уровне пакетной обработки
111
2: PageProperty page_j>roperty;
3: EquivEdge equiv;
4: PageViewEdge page_view;
I Значение свойства может
union PersonPropertyValue { <j I коситься к разным типам
1: string full_name;
2: GenderType gender;
3: Location location;
На рис. 5.1 показано, каким образом эта схема воплощается в папках.
і DataUnit {
РегзопРгоре^у регзоп^горе^у;
РадеРгоре^у раде_ргоре^у;
EquivEdge едиіу;
PageViewEdge раде_7іе;/;
О Ряд возможных значений для
объединений естественным образом
разделяет массив данных. Каталоги
верхнего уровня соответствуют
разным типам фактов в объединении
Оаіаипі с
/data/ union PersonPropertyValue {
1/
1; string full пягпр:
1/
2: GenderType gender;
2/
3: Location location;
21
}
1 1/
union PagePropertyValue {
3/
1: i32 page views;
4/
}
0 Свойства также являются
объединениями, и поэтому их
каталоги дополнительно
разделяются по типу каждого
значения
Рис. 5.1. Объединения в граф-схеме обеспечивают естественную схему
для вертикального разделения массива данных
/data/
union
1:
2:
3:
4:
Чтобы воспользоваться системой HDFS и библиотекой Pail для разработки
приложения SuperWebAnalytics.com, необходимо определить структурированное
“ведро”, предназначенное для хранения объектов типа Data и также соблюдаю¬
щее упомянутую выше схему вертикального разделения. Код реализации такой
возможности непростой, и поэтому представим его поэтапно.
1. Прежде всего создается абстрактная структура “ведра” для хранения объек¬
тов Apache Thrift. Сериализация этих объектов не зависит от типа сохраня¬
емых данных, а код получается более ясным благодаря разделению логики.
2. Далее структура “ведра” получается из абстрактного класса для хранения
объектов типа Data в приложении SuperWebAnalytics.com.
3. И наконец, определяется дополнительный подкласс, реализующий требую¬
щуюся схему вертикального разделения.
112
Часть I. Уровень пакетной обработки
Читая этот раздел, не обращайте особого внимания на код. Самое главное,
что этот код пригоден для реализации любой граф-схемы. Даже если граф-схема
будет развиваться дальше, этот код все равно останется работоспособным.
5.3.1. Структурированное “ведро”
для хранения объектов Apache Thrift
Создать структуру “ведра” для хранения объектов Apache Thrift необычайно
просто, поскольку основные хлопоты Apache Thrift берет на себя. В приведен¬
ном ниже листинге демонстрируется применение утилит Apache Thrift для сери¬
ализации и десериализации данных.
Листинг 5.5. Обобщенная абстрактная структура “ведра” для сериализации объектов Apache Thrift
Благодаря
обобщениям в Java
public abstract class ThriftPailStructure<T extends Comparable>
іmn 1 отпоггі-о Don 1
структура “ведра"
оказывается
пригодной для
хранения любых
объектов
Apache Thrift
{
private transient TSerializer ser; <a—
private transient TDeserializer des;
TSerializer и Tdeserializer являются
утилитами Apache Thrift для
сериализации объектов
в двоичные массивы и обратно
н> private TSerializer getSerializer() {
Утилиты Apache Thrift
строятся только
по требованию
if (ser==null) ser = new TSerializer();
return ser;
private TDeserializer getDeserializer () (
if(des==null) des = new TDeserializer ();
return des;
}
public byte[] serialize (T obj) {
try {
return getSerializer().serialize((TBase)obj); <h
} catch (TException e) {
throw new RuntimeException(e) ;
}
Объект
приводится
к типу объекта
Apache Thrift для
сериализации
Новый объект
Apache Thrift
создается перед
десериализацией
г-о
public T deserialize (byte [] record) {
T ret = createThriftObject ();
try {
getDeserializer().deserialize((TBase)ret, record);
} catch (TException e) {
throw new Runt imeExcept ion (e);
}
return ret; Конструктор объектов
, Apache Thrift должен быть
' реализован
в порожденном классе
protected abstract Т createThriftObject (); *>н
113
Глава 5. Иллюстрация храпения данных на уровне пакетной обработки
5.3.2. Основное "ведро” для приложения SuperWebAnalytlcs.com
Далее можно определить основной класс для хранения объектов типа Data
в приложении SuperWebAnalytics.com. С этой целью создается конкретный под¬
класс, производный от класса ThriftPailStructure, как показано в приведенном
ниже листинге.
Листинг 5.6. Конкретная реализация для объектов типа Data
Обозначает,
что в “ведре"
хранятся объекты
типа Data
public class DataPailStructure extends ThriftPailStructure<Data> {
—1> public Class getType() {
return Data.class;
}
protected Data createThriftObject()
return new Data();
Требуется в классе
ТЪгИГЬРаИЗЬгисЬигв
для создания объекта
и с целью сериализации
}
public List<String> getTarget (Data object) {
return Collections.EMPTY_LIST;
public boolean isValidTarget(String... dirs)
return true;
}
}
В структуре этого
“ведра”
вертикальное
разделение
не применяется
5.3.3. Расчлененное “ведро” для вертикального
разделения массива данных
И наконец, нужно создать структуру “ведра”, реализующего стратегию вер¬
тикального разделения данных по граф-схеме. Это самая трудная стадия описы¬
ваемого здесь процесса. Все приведенные ниже фрагменты кода извлечены из
класса SplitDataPailStructure, выполняющего эту задачу.
На самом верхнем уровне в коде из класса SplitDataPailStructure проверяет¬
ся класс DataUnit с целью создать отображение идентификаторов Apache Thrift
на классы для обработки данных соответствующего типа. Подобное отображе¬
ние для приложения SuperWebAnalytics.com приведено на рис. 5.2.
Рис. 5.2. Отображение полей для класса РаГаипИ
из приложения SuperWebAnalytics.com в классе ЗрШОаГаРаПБ^исгиге
В приведенном ниже листинге демонстрируется код, формирующий отобра¬
жение полей. Он пригоден для любой граф-схемы, а не только для той, что рас-
сматривается в данном примере.
114
Часть /. Уровень пакетной обработки
Листинг 5.7. Код, формирующий отображение полей для граф-схемы
Код Apache
Thrift для
проверки
и обхода
объекта
типа
DataUnit
public class SplitDataPailStructure extends DataPailStructure {
public static HashMap<Short, FieldStructure> validFieldMap *=
new HashMap<Short, FieldStructure>(); ^ FieldStructure — это интерфейс
как для ребер, так и для свойств
static { граф-схемы
-г> for(DataUnit._Fields k: DataUnit.metaDataMap.keyset()) {
FieldValueMetaData md = DataUnit.metaDataMap.get(k).valueMetaData;
FieldStructure fieldStruct;
if(md instanceof StructMetaData &&
((StructMetaData) md).structClass
.getNameO .endsWith("Property")) <ь
Свойства определяются по имени
класса проверяемого объекта
Если имя
класса не
оканчивается
на слово
"Property",
то оно
обозначает
ребро
н> fieldStruct = new PropertyStructure (
((StructMetaData) md).structClass);
} else {
fieldStruct = new EdgeStructure ();
}
validFieldMap.put(k.getThriftFieldldO , fieldStruct);
}
// остальная часть класса опущена
Как упоминается в комментариях к приведенному выше коду, FieldStructure —
это интерфейс, общий для классов PropertyStructure и EdgeStructure. Ниже
приведено определение этого интерфейса.
protected static interface FieldStructure {
public boolean isValidTarget(String[] dirs);
public void fillTarget(List<String> ret, Object val);
В дальнейшем мы рассмотрим классы РгорегЬуЗ^ис^ге и EdgeStructure бо¬
лее подробно. А до тех пор ниже показано, каким образом с помощью этого ин¬
терфейса осуществляется вертикальное разделение таблицы.
// Приведенные ниже методы взяты из класса SplitDataPailStructure
Каталог
верхнего
уровня
определяется
при проверке
класса
DataUnit
public List<String> getTarget(Data object) {
List<String> ret = new ArrayList<String>();
DataUnit du = object.get_dataunit();
short id = du.getSetField().getThriftFieldldO;
t> ret.add("" + id);
Любое дальнейшее
разделение
передается интерфейсу
FieldStructure
validFieldMap.get(id).fillTarget(ret, du.getFieldValue());
При проверке
достоверности
сначала
выясняется,
находится ли
идентификатор
поля из класса
DataUnit
в отображении
полей
return ret;
public boolean isValidTarget(String[] dirs) {
if(dirs.length==0) return false;
try (
short id = Short.parseShort (dirs [0]) ;
FieldStructure s = validFieldMap.get(id);
-t-' if(s==null)
return false;
115
Глава 5. Иллюстрация хранения данных на уровне пакетной обработки
}
else
return s. isValidTarget (dirs) ;<i
catch(NumberFormatException e) {
return false;
Любые дополнительные проверки
передаются интерфейсу Fieldstructure
Класс SplitDataPailStructure отвечает за каталог верхнего уровня при верти¬
кальном разделении. А ответственность за любые дополнительные подкаталоги
он передает классам, реализующим интерфейс FieldStructure. Следовательно,
как только классы EdgeStructure и PropertyStructure будут определены, дело
можно считать сделанным.
Ребра являются структурами, а следовательно, они не подлежат дальнейшему
разделению. Благодаря этому класс EdgeStructure определяется довольно про¬
сто, как показано ниже.
protected static class EdgeStructure implements FieldStructure {
public boolean isValidTarget (String [] dirs) { return true; }
public void fillTarget (List<String> ret, Object val) ( }
Но свойства являются объединениями подобно классу DataUnit. Поэтому в
реализующем их коде выполняется проверка с целью создать множество досто¬
верных идентификаторов полей Apache Thrift для класса заданного свойства.
Ради полноты изложения в приведенном ниже листинге класс свойства пред¬
ставлен полностью, но особое внимание следует обратить на построение множе¬
ства и его применение для выполнения контракта на интерфейс FieldStructure.
Листинг 5.8. Класс PropertyStructure
Множество
идентификаторов
Apache Thrift
для типов
значений свойств
protected static class PropertyStructure implements FieldStructure {
private TFieldldEnum valueld; < Свойство является структурой Apache Thrift,
-о private HashSet<Short> validlds; содержащей поле со значением свойства;
служит идентификатором этого поля
public PropertyStructure (Class prop) {
try {
MapcTFieldldEnum, FieldMetaData> propMeta = getMetadataMap(prop) ;
Class valClass = Class.forName(prop.getName() + "Value");
Выполняет
синтаксический
анализ метаданных
Apache Thrift,
чтобы получить
идентификатор
поля со значением
свойства
valueld = getldForClass (propMeta, valClass);
validlds = new HashSet<Short> () ;
Map<TFieldIdEnum, FieldMetaData> valMeta = getMetadataMap (valClass) ;
for(TFieldldEnum valid: valMeta.keyset()) {
validlds.add(valid.getThriftFieldldO);
}
catch(Exception e) {
throw new RuntimeException(e),
Выполняет синтаксический
анализ метаданных Apache
Thrift, чтобы получить все
достоверные идентификаторы
полей со значением свойства
public boolean isValidTarget (String [] dirs) (
if(dirs.length < 2) return false; «
try ( 1((check))
short s = Short.parseShort(dirs[1]);
return validlds.contains(s);
Вертикальное разделение
значения свойства на глубину
не меньше двух
116
Часть /. Уровень пакетной обработки
} catch (NmberFomatException е) {
return false;
}
}
public void fillTarget(List<String> ret
ret.add("" + ((TUnion) ((TBase)val)
.getFieldValue(valueld))
.getSetFieldO
.getThriftFieldidO ) ;
ujjj csJ l vax)
Использует идентификаторы
Apache Thrift, чтобы составить путь
к каталогу для текущего факта
private static MapcTFieldldEnum, FieldMetaData>
getMetadataMap(Class c) <}
I GetMetadataMap ( ) и getldForClasa ( )
try {
Object o = c.newlnstance();
return (Map) c.getField("metaDataMap") .get (o);
} catch (Exception e) {
throw new Runt imeException (e);
}
являются вспомогательными методами
для проверки объектов Apache Thrift
}
private static TFieldldEnum getldForClass (
MapcTFieldldEnum, FieldMetaData> meta, Class toFind)
{
for(TFieldldEnum k: meta.keyset()) {
FieldValueMetaData md = meta.get (k) .valueMetaData;
if(md instanceof StructMetaData) {
if(toFind.equals(((StructMetaData) md).structClass)) {
return k;
}
}
throw new RuntimeException("Could not find " + toFind.toString() +
" in " + meta.toString());
}
Проанализировав приведенный выше код, сделайте перерыв — вы его заслу¬
жили. Этот код примечателен тем, что он требует одноразовых затрат. Как толь¬
ко будет определена структура “ведра” для главного массива данных, дальнейшее
взаимодействие на уровне пакетной обработки не составит особого труда. Более
того, этот код можно применить в любом проекте, где составлена граф-схема
средствами Apache Thrift.
Резюме
В этой главе было показано, что для хранения массива данных в системе HDFS
требуется решить типичные задачи присоединения новых данных к главному
массиву, вертикального разделения данных на многие папки и объединения не¬
больших файлов. У вас была возможность убедиться, что выполнение этих задач
117
Глава 5. Иллюстрация хранения данных на уровне пакетной обработки
непосредственным обращением к интерфейсу HDFS API - дело нелегкое и чрева¬
тое ошибками, обусловленными человеческим фактором.
Далее в этой главе была представлена библиотека Pail, обеспечивающая тре¬
бующийся уровень абстракции, избавляя вас от необходимости разбираться
в форматах файлов и структуре каталогов системы HDFS и тем самым упрощая
задачи надежного вертикального разделения и выполнения типичных операций
над массивом данных. Для подобной абстракции в конечном счете требуется со¬
всем немного строк кода. Вертикальное разделение происходит автоматически,
а задачи вроде присоединения и объединения упрощаются до однострочных
операций. Это означает, что вы можете уделить основное внимание обработке
записей, не вдаваясь в подробности их хранения.
С помощью системы IIDFS и библиотеки Pail в этой главе был представлен
способ хранения главного массива данных, удовлетворяющий всем требовани¬
ям и довольно изящный в употреблении. Независимо от того, воспользуетесь ли
вы этими инструментальными средствами или нет, мы показали на конкретном
примере, насколько изящной может быть эта часть архитектуры, и надеемся, что
этот пример вдохновит и нацелит вас на достижение хотя бы такого же уровня
изящества.
В следующей главе речь пойдет об эффективном использовании хранения за¬
писей для выполнения следующего важного шага в реализации лямбда-архитек¬
туры: вычисления пакетных представлений.
Уровень пакетной
обработки
В этой главе...
■ Вычисление функций на уровне пакетной обработки.
■ Разбиение запроса на предварительно
и динамически вычисляемые составляющие.
■ Сравнение алгоритмов повторных
и инкрементных вычислений.
■ Значение масштабируемости.
■ Парадигма MapReduce.
■ Рассмотрение MapReduce на высоком уровне.
Назначение информационной системы — отвечать на произвольные запросы, ка¬
сающиеся хранящихся в ней данных. Любой запрос к массиву данных может быть
реализован в виде функции, принимающей все запрашиваемые данные в качестве
входных параметров. В идеальном случае подобные функции можно выполнять
в ходе запроса к массиву данных. Но, к сожалению, выполнение функции, где в ка¬
честве входных данных используется целый массив, потребует очень много време¬
ни. Поэтому для быстрой обработки запросов нужна совсем другая стратегия.
В лямбда-архитектуре на уровне пакетной обработки происходит предвари¬
тельное вычисление главного массива данных в пакетные представления, чтобы
обрабатывать запросы с малой задержкой. Для этого требуется достичь равно¬
весия между тем, что было вычислено заранее, и тем, что будет вычислено во
время выполнения для завершения запроса. Выполняя чуть больше вычислений
по ходу обработки запроса, можно избежать необходимости предварительно
вычислять неоправданно большие пакетные представления. И здесь самое глав-
ное — предварительно обработать лишь столько данных, сколько окажется доста¬
точно для быстрого завершения запроса.
120
Часть /. Уровень пакетной обработки
В двух предыдущих главах было показано, каким образом формируется модель
данных для массива данных и как они сохраняются на уровне пакетной обработки
путем масштабирования. А в этой главе далее будет показано, как вычислять про¬
извольные функции, выполняемые над этими данными. Сначала мы представим
ряд мотивирующих примеров, чтобы продемонстрировать принципы вычисления
на уровне пакетной обработки. Затем подробно рассмотрим порядок вычисления
индексов главного массива данных, используемых на прикладном уровне для завер¬
шения запросов. Попутно будут исследованы компромиссы между алгоритмами по¬
вторных вычислений, характерными для уровня пакегной обработки, а также алгорит-
мами инкрементных вычислений, зачасгую применяемыми в реляционных базах дан¬
ных. После этого будет разъяснено значение масштабируемости уровня пакегной
обработки, а также парадигмы MapReduce для масштабируемых и практически про¬
извольных пакетных вычислений. Из этих пояснений станет ясно, что MapReduce
обеспечивает очень простой, хотя и довольно низкий уровень абстракции. И в за¬
вершение этой главы мы рассмотрим высокоуровневую парадигму, которую можно
реализовать средствами MapReduce.
6.1. Мотивирующие примеры
Рассмотрим некоторые примеры запросов в качестве мотивации для обсужде¬
ния теоретических вопросов в этой главе. Эти запросы иллюстрируют принци¬
пы пакетных вычислений, и в каждом примере показано, как вычислять запрос
в виде функции, которой передается весь главный массив данных в качестве па¬
раметра. А в дальнейшем мы видоизменим эти примеры, чтобы воспользовать¬
ся предварительными вычислениями вместо того, чтобы делать это полностью
по ходу обработки запроса.
6.1.1. Количество просмотров страницы во времени
В первом примере запрос делается к массиву данных о просмотрах страниц,
где запись каждого просмотра страницы содержит ее URL и отметку времени.
Назначение такого запроса — определить общее количество просмотров страни¬
цы по указанному URL в течение заданного периода времени в часах.
Такой запрос может быть написан в псевдокоде следующим образом:
function pageviewsOverTime(masterDataset, url, startHour, endHour) {
pageviews = 0
for(record in masterDataset) {
if (record.url == url
record.time >= startHour &&
record.time <= endHour) {
pageviews += 1
}
}
return pageviews
}
Чтобы обработать такой запрос, используя функцию для всего массива дан¬
ных, достаточно перебрать каждую запись, подсчитывая все просмотры страни¬
цы по указанному URL в заданном промежутке времени. После перебора всех
записей возвращается конечное значение подсчета.
Глава 6. Уровень пакетной обработки
121
б.1«2. Заключение о гендерной принадлежности
В следующем примере запрашивается массив данных с записями имен и на ос¬
новании полученного ответа делается заключение о гендерной принадлежности
конкретною лица. В алгоритме обработки запроса сначала выполняется семан¬
тическая нормализация имен, в ходе которой происходит преобразование имен
вроде Bob и Bill в имена Robert и William соответственно. А затем используется
модель, позволяющая определить вероятность принадлежности каждого имени
к соответствующему полу.
В конечном итоге алгоритм заключения о гендерной принадлежности выгля¬
дит следующим образом:
все имена,вязанные function genderlnference (masterDataset, personld) {
с конкретным лицом names - new Set ()
for (record in masterDataset) {
if (record.personld == personld) {
names .add (normalizeName (record.name) )
}
maleProbSum = 0.0 I Усредняет вероятность
for (name in names) { < I каждого имени быть мужским
maleProbSum += maleProbabilityOfName (name)
1
maleProb = maleProbSum / names.size()
if (maleProb > 0.5) { <
return "male" Возвращает пол
} else { с наибольшей вероятностью
return "female"
}
Этот запрос примечателен тем, что результаты его обработки могут меняться
по мере усовершенствования алгоритма нормализации имен и модели, опреде¬
ляющей гендерную принадлежность имен во времени, а не только по мере полу¬
чения новых данных.
6.1.3. Фактор влияния
И в последнем примере запрашивается массив данных с записями реакции
на сообщения в социальной сети Twitter. Каждая запись реакции состоит из полей
sourceld и responderld, обозначающих, что получатель (responderld) обратил
внимание и процитировал или ответил на сообщение отправителя (sourceld).
В данном запросе определяется фактор влияния на каждого человека, уча¬
ствующего в социальной сети. Этот фактор вычисляется в два этапа. Сначала
для каждого человека выбирается наиболее влиятельное лицо в зависимости
от количества реакций, вызванных им у этого человека. Затем фактор чьего-то
влияния определяется по количеству людей, для которых он оказался наиболее
влиятельным лицом.
Алгоритм определения фактора чьего-то влияния выглядит следующим об¬
разом:
122
Часть I. Уровень пакетной обработки
function influence_score(masterDataset, personld) {
influence = new Map()
for (record in masterDataset) { < —
curr * influence.get(record.responderld)
curr[record.sourceld] += 1
influence.set(record.sourceld, curr)
}
score = 0
for (entry in influence) { <1
if(topKey(entry.value) == personld) {
score += 1
Вычисляет влияние
среди всех пар людей
I I new Map(default=0)
Подсчитывает количество людей,
для которых человек с идентификатором регвОПЫ
оказался наиболее влиятельным лицом
}
return score
В приведенном выше коде функция ^рКеу () имитируется, поскольку ее реали¬
зация довольно проста. В противном случае алгоритм упрощается и сначала под¬
считывает количество реакций в каждой паре людей, а затем количество людей,
для которых запрашиваемый пользователь оказался наиболее влиятельным лицом.
6.2. Вычисления на уровне пакетной обработки
Сделаем небольшое отступление, чтобы посмотреть, каким образом лямбда-ар¬
хитектура действует на самом верхнем уровне. При обработке запросов каждый
уровень лямбда-архитектуры играет свою вспомогательную роль, как показано
на рис. 6.1.
На уровне пакетной обработки выполняются функции над главным масси¬
вом данных для предварительного вычисления промежуточных данных, на¬
зываемых пакетными представлениями. Пакетные представления загружаются
на уровне обслуживания, где они индексируются для быстрого доступа к этим
данным. А на уровне ускорения устраняется большая задержка в обработке дан¬
ных на уровне пакетной обработки. С этой целью предоставляются с малой за¬
держкой обновления, в которых используются данные, которые еще предстоит
предварительно вычислить в пакетном представлении. Таким образом, запросы
удовлетворяются путем обработки данных из представлений на уровнях обслужи¬
вания и ускорения и объединения полученных результатов.
Вся лямбда-архитектура основывается на том, что по любому запросу мож¬
но предварительно вычислить данные на уровне пакетной обработки, чтобы
ускорить их обработку на уровне обслуживания. Подобные предварительные
вычисления в главном массиве данных требуют времени, но большую задерж¬
ку этих вычислений на уровне пакетной обработки следует рассматривать как
возможность для углубленного анализа данных и связывания вместе их разроз¬
ненных частей. Напомним, что обслуживание запросов с малой задержкой до¬
стигается в других частях лямбда-архитектуры.
В качестве наивной стратегии организации вычислений па уровне пакетной
обработки можно было бы предварительно вычислить все возможные запросы
и кешировать результаты на уровне обслуживания. Подобная стратегия показана
на рис. 6.2.
Глава 6. Уровень пакетной обработки
123
Уровень ускорения
Представление
Ж в реальном
' времени
Представление
Ж в реальном
г времени
Представление
Ж в реальном
' времени
© На уровне ускорения
задержка устраняется
путем запрашивания
недавно полученных
данных
Уровень пакетной обработки
Главный массив данных
О На уровне пакетной обработки пред¬
варительно вычисляются функции,
выполняемые над главным массивом
данных. Обработка всего массива
данных вносит большую задержку
У|
ровень обслуживания
ггь\ Пакетное
С О Ь пред¬
оставление
Гг\ Пакетное
С О Ь пред¬
оставление
Пакетное
Сор пред-
о-Ставление
© На уровне обслуживания
предварительно вычисленные
результаты обрабатываются
с малой задержкой чтения
Рис. 6.1. Роли отдельных уровней лямбда-архитектуры в обслуживании запросов к массиву данных
К сожалению, далеко не все можно
вычислить заранее. Рассмотрим в ка¬
честве примера запрос на подсчет
количества просмотров страниц во
времени. Если потребуется предва¬
рительно вычислить практически
каждый запрос, то для этого придет¬
ся сформировать ответ для каждого
возможного часового промежутка
времени по каждому 1Л1Ё. Но ведь
таких часовых промежутков времени
в заданных времени ых рамках может
быть очень много. Так, в течение года насчитывается около 380 млн. отдельных
часовых промежутков времени. Для предварительного вычисления такого запро¬
са придется заранее рассчитать и проиндексировать 380 млн. значений по каждо¬
му ТЛИ,. Очевидно, что это совершенно неосуществимое и непригодное решение.
Вместо этого можно заранее вычислить промежуточные результаты, а затем
воспользоваться ими для завершения запросов в динамическом режиме, как по¬
казано на рис. б.З.
Рис. 6.2. Выполнение функции непосредственно
в главном массиве данных для предварительного
вычисления запроса
124
Часть I. Уровень пакетной обработки
Предварительное вычисление Запрос с малой задержкой
Рис. 6.3. Разбиение запроса на предварительно и динамически вычисляемые составляющие
Так, для обработки запроса на подсчет количества просмотров страниц во
времени можно предварительно вычислить количество просмотров страниц за
каждый час по каждому ШИ., как показано на рис. 6.4.
Функция:
суммиро-
, вание
Рис. 6.4. Запрашивание индексированного пакетного представления для вычисления
количества просмотров страниц
Чтобы завершить запрос, следует извлечь по индексу количество просмо¬
тров страниц за каждый час в заданном промежутке времени и просуммировать
полученные результаты. Так, для периода в один год (т.е. за 24 часа в течение
365 дней) нужно заранее вычислить и проиндексировать лишь 8760 значений
по каждому иКТ. Это, безусловно, более осуществимое решение.
6.3. Сравнение алгоритмов повторных
и инкрементных вычислений
Главный массив данных непрерывно растет, и поэтому требуется какая-то
стратегия для обновления пакетных представлений по мере поступления новых
данных. Для этого можно было бы, с одной стороны, выбрать алгоритм повтор¬
ных вычислений, отказавшись от прежних пакетных представлений и повторно
вычисляя функции, выполняемые над всем главным массивом данных, а с другой
С \
Результаты:
2930
ч )
ига.
Час
Количество
просмотров
страниц
foo.com/blog
2012/12/0815:00
876
foo.com/blog
2012/12/0816:00
987
foo.com/blog
2012/12/0817:00
762
foo.com/blog
2012/12/0818:00
413
foo.com/blog
2012/12/0819:00
1098
foo.com/blog
2012/12/08 20:00
657
foo.com/blog
2012/12/08 21:00
101
Глаза 6. Уровень пакетной обработки
125
сторо алгоритм инкрементных вычислений, обновляющий представления не¬
посредственно, когда поступают новые данные.
В а I стве элементарного примера рассмотрим пакетное представление,
содержащее о щее количество записей в главном массиве данных. Алгоритм
повторных вычислений обновит подсчет, присоединив сначала новые данные
к главному массиву, а затем подсчитав заново все записи. Подобная стратегия
представлена на рис. 6.5.
N
Предварительно
вычисленное
■ представление:
20612788
записей
Рис. 6.5. Алгоритм повторных вычислений, обновляющий количество
записей в главном массиве данных. Сначала к главному массиву
присоединяются новые данные, а затем все записи подсчитываются заново
С другой стороны, алгоритм инкрементных вычислений позволяет подсчи¬
тать количество новых записей данных и добавить полученный результат к уже
имеющемуся подсчету (рис. 6.6).
Рис. 6.6. Алгоритм инкрементных вычислений, обновляющий количество записей
в главном массиве данных. Записи подсчитываются только в новом массиве данных,
а общий их подсчет служит для непосредственного обновления пакетного представления
В связи с изложенным выше невольно возникает вопрос: зачем вообще поль¬
зоваться алгоритмом повторных вычислений, если имеется более эффективный
алгоритм инкрементных вычислений? Дело в том, что эффективность — далеко
не единственный фактор, который требуется принимать во внимание. К числу
главных критериев выбора между двумя рассматриваемыми здесь алгоритмам
относятся производительность, устойчивость к ошибкам, обусловленным чело¬
веческим фактором, а также универсальность алгоритма. Оба алгоритма оудут
рассматриваться далее по каждому из этих критериев. И в конечном итш е вы¬
яснится, что на практике придется все же пользоваться вариантами алторигмов
с повторными вычислениями, несмотря на то, что алгоритмы инкрементных вы¬
числений способны обеспечить дополнительную эффективность.
126
Часть /, Уровень пакетной обработки
6.3.1т Производительность
В отношении производительности у любого алгоритма вычислений на уровне
пакетной обработки имеются две особенности: количество ресурсов, требующих¬
ся для обновления пакетного представления новыми данными, а также размеры
получаемых пакетных представлений.
Алгоритму инкрементных вычислений практически всегда требуется намного
меньше ресурсов для обновления пакетного представления, поскольку для этой
цели он использует новые данные и текущее состояние пакетного представле¬
ния. Хотя для решения таких задач, как подсчет количества просмотров страниц
во времени, требуется намного меньшее представление, чем главный массив дан¬
ных, благодаря агрегированию. В то же время алгоритм повторных вычислений
просматривает весь главный массив данных, и поэтому количество ресурсов,
требующихся для обновления, может быть на несколько порядков больше, чем
для алгоритма инкрементного вычисления. Но размер пакетного представления
для алгоритма инкрементных вычислений может оказаться значительно больше,
чем у соответствующего представления для алгоритма повторных вычислений,
поскольку представление должно быть сформировано таким образом, чтобы об¬
новляться постепенно. Продемонстрируем все сказанное выше на двух примерах.
Допустим сначала, что требуется вычислить среднее количество просмотров
страниц по каждому 1ЖЬ в отдельном домене. Пакетное представление, сформи¬
рованное алгоритмом повторных вычислений, будет содержать результат преоб¬
разования просмотров страниц по каждому 1Л1Ь в их среднее количество. Но это
совершенно неприемлемо для алгоритма инкрементных вычислений, поскольку
для инкрементного обновления среднего значения требуется также знать коли¬
чество записей, использованных для вычисления предыдущего среднего значе¬
ния. Следовательно, в инкрементном представлении сохранится как среднее, так
и общее количество просмотров страниц по каждому иЯЬ, что приведет к увели¬
чению на постоянную величину размера инкрементного представления по срав¬
нению с аналогичным представлением, основанным на повторном вычислении.
В других случаях увеличение размера пакетного представления для алгоритма
инкрементных вычислений оказывается еще более значительным. Рассмотрим в ка¬
честве еще одного примера запрос на подсчет количества индивидуальных посети¬
телей страниц по каждому иЯЬ. На рис. 6.7 показаны отличия в пакетных представ¬
лениях при использовании алгоритмов повторных и инкрементных вычислений.
С одной стороны, для представления при повторном вычислении требуется
лишь преобразование посещений страниц по каждому иЯЬ в подсчет индивиду¬
альных посетителей. С другой стороны, в алгоритме инкрементных вычислений
во внимание принимаются только новые просмотры страниц, и поэтому его пред¬
ставление должно содержать все множество посетителей страниц по каждому
иКЦ чтобы определить в новых данных те записи, которые соответствуют по¬
вторным посещениям. Следовательно, представление при инкрементном вычисле¬
нии может достичь размеров главного массива данных! И хотя пакетное представ¬
ление, формируемое алгоритмом инкрементных вычислений, далеко не всегда
оказывается таким крупным, тем не менее, оно может быть намного крупнее, чем
соответствующее представление, основанное на повторных вычислениях.
Глава 6. Уровень пакетной обработки
127
ияі
Количество
индивидуальных
посетителей
foo.com
2217
foo.com/blog
1899
foo.com/about
524
foo.com/careers
413
foo.com/faq
1212
Пакетное представление
при повторном вычислении
ияі.
Количество
индивидуальных
посетителей
Идентификаторы
посетителей
foo.com
2217
1,4,5,7,10,12,14,....
foo.com/blog
1899
2,3,5,17,22,23,27,...
foo.com/about
524
3,6,7,19,24,42,51,...
foo.com/careers
413
12,17,19,29,40,42,...
foo.com/faq
1212
8,10,21,37,39,46,55,...
Пакетное представление
при инкрементном вычислении
Рис. 6.7. Сравнение пакетных представлений при использовании
алгоритмов повторных и инкрементных вычислений для определения
количества индивидуальных посетителей страниц по каждому ІІЯІ-
6.3.2. Устойчивость к отказам, обусловленным
человеческим фактором
Срок эксплуатации информационной системы чрезвычайно долгий, и в течение
этого периода времени программные ошибки могут и, скорее всего, будут появлять¬
ся в работающей системе. Поэтому необходимо рассмотреть степень устойчивости
алгоритмов обновления пакетного представления к подобным ошибкам. В этом от¬
ношении алгоритмы повторных вычислений внутренне устойчивы к отказам, обу¬
словленным человеческим фактором, тогда как алгоритмы инкрементных вычисле¬
ний не избавляют от подобных ошибок, способных вызвать серьезные осложнения.
Рассмотрим в качестве примера алгоритм, подсчитывающий на уровне пакет¬
ной обработки общее количество записей в главном массиве данных. Допустим,
что такой алгоритм развернут в системе, но в нем допущена следующая ошибка:
при подсчете каждой записи общее количество записей увеличивается не на еди¬
ницу, а на два. Если это алгоритм повторных вычислений, то достаточно испра¬
вить ошибку и снова развернуть его код, чтобы пакетное представление пра¬
вильно отражало общее количество записей при последующем запуске уровня
пакетной обработки. Такая отказоустойчивость достигается благодаря тому, что
алгоритм повторных вычислений заново формирует пакетное представление.
Но если алгоритм выполняет инкрементные вычисления, то исправить сфор¬
мированное представление нелегко. Единственная возможность — распознать те
записи, которые были подсчитаны лишний раз, определить, сколько раз каждая
такая запись была подсчитана излишне, а затем исправить подсчет каждой по¬
добной записи. Но сделать это с достаточно высокой степенью достоверности
удается далеко не всегда. Для этой цели может пригодиться подробная регистра¬
ция действий, выполняемых в системе, хотя журналы регистрации не всегда со¬
держат требующуюся информацию, поскольку нельзя заранее предугадать, како¬
го рода ошибка будет совершена в будущем. Поэтому в пакетное представление
зачастую приходится вносить изменения, носящие произвольный характер и ос¬
нованные на самых вероятных предположениях, но делать это нужно так, чтобы
ничего не испортить.
128
Часть /. Уровень пакетной обработки
Безусловно, надежду на полноту фиксации информации в журналах регистра¬
ции для исправления ошибок вряд ли можно считать удачной нормой инженер¬
ной практики. Приходится еще раз напоминать о неизбежности ошибок, связан¬
ных с человеческим фактором. И, как пояснялось выше, алгоритмы повторных
вычислений обладают намного большей устойчивостью к подобного рода ошиб¬
кам, чем алгоритмы инкрементных вычислений.
6.3.3. Универсальность алгоритмов
Несмотря на то что алгоритмы инкрементных вычислений могут выпол¬
няться быстрее, они зачастую должны быть приспособлены к разрешению воз¬
никающих затруднений. Как было показано в одном из предыдущих примеров,
алгоритм для подсчета количества индивидуальных посетителей страниц мо¬
жет формировать неоправданно большие пакетные представления. Подобный
недостаток позволяют устранить алгоритмы вероятностных вычислений вроде
НурегЬод1л^, сохраняющие промежуточные статистические данные для оценки
общего подсчета индивидуальных посещений. Это дает возможность сократить
затраты на хранение пакетного представления, хотя и за счет приближенного,
а не точного характера применяемого алгоритма.
Пример запроса для определения фактора влияния, приведенный в начале
этой главы, иллюстрирует еще один насущный вопрос: сложность алгоритмов
инкрементных вычислений смещается в сторону динамических вычислений. По
мере усовершенствования алгоритма семантической нормализации внедренные
усовершенствования должны отражаться на результатах запросов. Но если норма¬
лизация выполняется как часть предварительного вычисления, то пакетное пред¬
ставление окажется неактуальным всякий раз, когда усовершенствуется алгоритм
нормализации. Поэтому нормализация должна происходить по ходу обработки
запроса, когда применяется алгоритм инкрементных вычислений. В данном при¬
мере пакетное представление должно будет содержать каждое имя, обнаруженное
для каждого лица, а в динамически выполняемом коде придется снова нормали¬
зовать каждое имя всякий раз, когда выполняется запрос. В итоге увеличивается
задержка в динамически вычисляемой составляющей запроса, причем она может
оказаться слишком долгой, чтобы удовлетворять требованиям приложения.
Алгоритм повторных вычислений непрерывно перестраивает все пакетное
представление в целом, и поэтому структура этого представления и динамически
вычисляемая составляющая запроса упрощаются. В итоге алгоритм становится
более универсальным.
6.3.4. Выбор разновидности алгоритма
Все, что обсуждалось ранее в этом разделе, подытожено в табл. 6.1, где срав¬
ниваются алгоритмы повторных и инкрементных вычислений. Из сравнения
этих алгоритмов можно сделать следующий главный вывод: предпочтение всегда
следует отдавать алгоритмам повторных вычислений. Это единственный способ
обеспечить устойчивость системы к отказам, обусловленным человеческим фак¬
тором, а ведь подобная устойчивость является требованием к надежности систе¬
мы, которым нельзя пренебрегать.
129
Глава 6. Уровень пакетной обработки
Таблица 6.1. Сравнение алгоритмов повторных и инкрементных вычислений
Алгоритмы повторных вычислений Алгоритмы инкрементных
вычислений
Производительность
Требуют больше вычислительных
ресурсов для обработки всего
главного массива данных в целом
Требуют меньше вычислительных
ресурсов, но могут формировать
намного более крупные пакетные
представления
Устойчивость к отказам,
обусловленным
человеческим фактором
Особенно устойчивы к ошибкам,
обусловленным человеческим
фактором, поскольку пакетные
представления постоянно
перестраиваются
Не упрощают исправление ошибок
в пакетных представлениях;
исправления носят произвольный
характер и могут потребовать
дополнительной оценки
Универсальность
Сложность алгоритма
преодолевается во время
предварительного вычисления,
в результате чего упрощаются
пакетные представления
и сокращается задержка при
динамической обработке запросов
Требуют специальной настройки;
способны перенести всю
свою сложность на стадию
динамической обработки запросов
Заключение
Незаменимы для поддержки
надежной системы обработки
данных
Могут повысить эффективность
системы, но лишь в качестве
дополнения алгоритмов повторных
вычислений
Имеется также возможность ввести в алгоритмы варианты инкрементных вы¬
числений, чтобы повысить в них эффективность использования вычислитель¬
ных ресурсов. Далее в главе будут рассматриваться в основном алгоритмы по¬
вторных вычислений, хотя в главе 18 мы еще вернемся к вопросу применимости
инкрементных вычислений на уровне пакетной обработки.
6.4. Масштабируемость на уровне пакетной обработки
Словом масштабируемость слишком часто разбрасываются, поэтому выяс¬
ним тщательно, что же оно обозначает в контексте информационных систем.
Масштабируемость — это способность системы поддерживать свою производи¬
тельность при увеличении нагрузки вводом дополнительных ресурсов. Нагрузка
в контексте больших данных обозначает сочетание общего объема имеющихся
данных с объемом ежедневно получаемых новых данных, количеством запросов,
обслуживаемых приложением каждую секунду, и т.д. и т.п.
Еще важнее, что масштабируемая система является линейно масштабируемой.
В частности, линейно масштабируемая система способна поддерживать свою
производительность при увеличении нагрузки за счет ввода дополнительных
ресурсов пропорционально растущей нагрузке. И хотя нелинейно масштабиру¬
емая система также считается масштабируемой, пользы от нее не очень много.
Допустим, количество требующихся машин находится в квадратической зависи¬
мости от нагрузки на систему, как показано на рис. 6.8. Затраты на эксплуата¬
цию такой системы могут со временем значительно возрасти. Так, если нагрузка
на систему увеличится в 10 раз, то затраты на ее эксплуатацию - в 100 раз. Такая
система неосуществима с точки зрения затрат.
130
Часть I. Уровень пакетной обработки
Рис. 6.8. Нелинейная масштабируемость
Если система является линейно масштабируемой, затраты на ее эксплуатацию
возрастают пропорционально нагрузке. И это очень важное свойство информа¬
ционной системы.
Что масштабируемость не означает
Как ни странно, масштабируемая система совсем не обязательно должна быть в со¬
стоянии повышать свою производительность вводом дополнительных машин. Допустим,
имеется веб-сайт, обслуживающий статическую HTML-страницу. Допустим также, что
каждый веб-сервер способен обслужить 1000 запросов в секунду с допустимой задерж¬
кой 100 миллисекунд. Сократить эту задержку обслуживания веб-страницы не удастся
вводом дополнительных машин, поскольку отдельный запрос не распараллеливается
и должен быть обслужен одной машиной. Но при увеличении количества запросов в се¬
кунду можно масштабировать веб-сайт, введя дополнительные веб-серверы, чтобы рас¬
пределить нагрузку на обслуживание HTML-страницы.
С помощью алгоритмов, допускающих распараллеливание, можно практически повы¬
сить производительность, введя дополнительные машины, но усовершенствования этих
алгоритмов способны свести на нет эффект от ввода дополнительных машин. Дело
в том, что ввод дополнительных машин влечет за собой увеличение расходов на экс¬
плуатацию и связь.
Мы обсудили понятие масштабируемости для того, чтобы подготовить почв\
для представления MapReduce — парадигмы распределенных вычислений, с по¬
мощью которой можно реализовать уровень пакетной обработки. При дальней¬
шем рассмотрении этой парадигмы следует иметь в виду ее линейную масшта¬
бируемость. Так, если размер главного массива данных увеличивается вдвое, то
в той же степени возрастает и количество серверов, на которых можно строить
пакетные представления при той же самой задержке обработки запросов.
6.5. MapReduce — парадигма распределенных
вычислений для больших данных
MapReduce — это парадигма распределенных вычислений, первона¬
чально предложенная компанией Google. Она предоставляет примитивы
Глава 6. Уровень пакетной обработки
131
для масштабируемых и отказоустойчивых пакетных вычислений. С помощью
MapReduce вычисления пишутся в виде функций предварительной обработки
и консолидации, манипулирующих парами “ключ-значение”. Эти примитивы
достаточно выразительны для реализации практически любой функции, а кар¬
кас MapReduce выполняет их над главным массивом данных распределенным
и надежным способом. Благодаря подобным свойствам MapReduce оказывается
не только превосходной парадигмой для предварительных вычислений, требу¬
ющихся на уровне пакетной обработки, но и для низкоуровневой абстракции,
когда выражение вычислений может потребовать немалых трудов.
Каноническим примером применения MapReduce служит подсчет слов. С этой
целью берется массив текстовых данных и определяется, сколько раз каждое
слово встречается в тексте. Функция предварительной обработки в MapReduce
выполняется один раз на каждую строку текста и порождает любое количество
пар “ключ-значение”. При подсчете слов функция предварительной обработки
порождает пару “ключ-значение” для каждого слова в тексте, устанавливая ключ
равным найденному слову, а значение — единице, как показано ниже. А затем
MapReduce организует результат, возвращаемый функцией предварительной об¬
работки таким образом, чтобы все значения по одному и тому же ключу были
сгруппированы вместе.
function word_count_map (sentence) {
for (word in sentence.split (" ")) {
emit (word, 1)
}
}
Функция консолидации данных берет весь список значений по одному и тому
же общему ключу и порождает новые пары “ключ-значение” в качестве окон¬
чательного результата. При подсчете слов в качестве входных данных функции
консолидации служит список значений, равных 1 для каждого слова, а сама функ¬
ция просто суммирует эти значения, чтобы произвести подсчет этого слова, как
показано ниже.
function word_count_reduce(word, values) {
sum = 0
for(val in values) {
sum += val
}
emit (word, sum)
}
Для выполнения программы вроде подсчета слов на кластере машин должно
произойти немало подспудных действий, но каркас MapReduce скрывает от вас
большую часть всех этих подробностей. Его назначение - дать вам возможность
сосредоточить свое внимание на том, что именно требуется вычислить, не забо¬
тясь о том, как это будет сделано.
6.5.1. Масштабируемость
Эффективность парадигмы ftÆapReduce объясняется гем, что написанные
по ней программы внутренне масштабируемы. Так, программа, обрабатывающая
132
Часть L Уровень пакетной обработки
10 гигабайт данных, может обработать и 10 петабайт данных. MapReduce автома¬
тически распараллеливает вычисления в кластере машин независимо от объема
входных данных. Все подробности распараллеливания, переноса данных между
машинами и планирования исполнения абстрагируются, скрываясь за каркасом
MapReduce.
Проанализируем, каким образом программа вроде подсчета слов выполняется
в кластере MapReduce. Входные данные прикладной программы MapReduce хра¬
нятся в распределенной файловой системе наподобие упоминавшейся ранее си¬
стемы HDFS. Прежде чем обработать данные, программа определяет в кластере те
машины, на которых размещаются блоки, содержащие входные данные (рис. 6.9).
Местополо¬
жение блоков
файла
□
1.3
2.3
Распределенная файловая система
Сервері
Сервер 2
ґ
Сервер 3
□□
□и.
иш.
г 41
Сервер 4
Г '
Сервер 5
Сервер 6
□□
□□
Прежде чем начать обработку данных, программа MapReduce
определяет местоположение блоков в распределенной файловой системе
Рис. 6.9. Обнаружение серверов, на которых размещаются
входные файлы для прикладной программы MapReduce
После определения местоположения входных данных MapReduce запускает
ряд задач предварительной обработки пропорционально объему входных дан¬
ных. Для каждой из этих задач назначается подмножество входных данных,
и над ними выполняется функция предварительной обработки. Как правило,
объем кода намного меньше объема данных, и поэтому MapReduce пытается на¬
значить задачи для тех серверов, где размещаются обрабатываемые данные. Как
показано на рис. 6.10, перемещение кода к данным позволяет избежать переноса
всех этих данных по сети.
Задача
предварительной
обработки: г і
сервер 1 I I
<to,
1>,
<Ье,1>,
<ог,
1>,
<not,1>,
<to,
1>,
<Ье,1>,
Задача
<brevity,l>/ <is,l>,
предварительной
<the,l>, <soul,l>
обработки:
сервер 3
<of,l>, <wit,l>,
О Код перемещается на те серверы,
где размещаются входные файлы,
чтобы ограничить сетевой трафик
в кластере
© Задачи предварительной об раб о
формируют промежуточные пары
"ключ-значение ', которые затем
направляются задачам консолида
Рис. 6.10. MapReduce способствует локализации данных,
выполняя задачи на тех серверах, где размещаются входные данные
Глава 6, Уровень пакетной обработки
133
Как и задачи предварительной обработки, задачи консолидации распределя¬
ются по всему кластеру. Каждая из этих задач отвечает за вычисление функции
консолидации для подмножества ключей, сформированных задачами предвари¬
тельной обработки. Функции консолидации требуются все значения, связанные
с заданным ключом, и поэтому задача консолидации не может быть начата до тех
пор, пока не завершатся все задачи предварительной обработки.
По завершении задач предварительной обработки каждая порожденная пара
“ключ-значение” направляется задаче консолидации, отвечающей за обработ¬
ку значений по данному ключу. Следовательно, каждая задача предварительной
обработки распределяет свои выходные данные по всем задачам консолидации.
Такой перенос промежуточных пар “ключ-значение” называется перетасовкой
и иллюстрируется на рис. 6.11.
На стадии перетасовки все пары “ключ-значение , сформированные задачами предварительной
доработки, распределяются по задачам консолидации В ходе этого процесса все пары с одинаковым
ключом посылаются одной и той же задаче консолидации
Рис. 6.11. На стадии перетасовки выходные данные задач
предварительной обработки распределяются по задачам консолидации
Как только задача консолидации получит все пары “ключ-значение” из ка¬
ждой задачи предварительной обработки, она отсортирует их по ключу. В итоге
все значения будут сгруппированы вместе по заданному ключу. Затем по каждому
ключу и группе его значений вызывается функция консолидации, как показано
на рис. 6.12.
<to, 1>
<and, 1>
<from, 1>
<to, 1>
Сортировка
cand, 1>
cand, 1>
<from, 1>
cfrom, 1>
Консолидация
<and, 2>
cfrom, 2>
chere, 1>
cto, 2>
chere, 1>
cfrom, 1>
cand, 1>
chere, 1>
<to, 1>
<to, 1>
Рис. 6.12. Задача консолидации сначала сортирует поступающие данные по заданному ключу,
а 'затем выполняет функцию консолидации над результирующими группами значений
Как видите, в программе MapReduce происходит немало операций над дан¬
ными. Из приведенного выше краткого обзора MapReduce можно сделать следу¬
ющие выводы.
Часть /. Уровень пакетной обработки
■ Программы MapReduce выполняются полностью распределенно и без об¬
щего камня преткновения.
■ MapReduce поддерживает масштабируемость: предоставляемые функции
предварительной обработки и консолидации выполняются параллельно
по всему кластеру.
* Трудности распараллеливания и назначения задач для отдельных машин
разрешаются автоматически.
6.5.2. Отказоустойчивость
Распределенные системы известны своей привередливостью. Разделение се¬
тей, аварийные сбои и отказы дисков относительно редки на одном сервере, но
их вероятность возрастает, когда приходится согласовывать вычисления на боль¬
шом числе машин. Правда, распределенные вычисления средствами MapReduce
оказываются не только легко распараллеливаемыми и внутренне масштабируе¬
мыми, но и отказоустойчивыми.
Выполнение программы может завершиться неудачно по самым разным при¬
чинам, включая переполнение жесткого диска, исчерпание процессом доступной
оперативной памяти или выход оборудования из строя. MapReduce отслежива¬
ет подобные отказы и сбои, пытаясь повторить ошибочную часть вычислений
в другом узле. Все приложение, обычно называемое заданием, завершается неу¬
дачно лишь в том случае, если задачу не удается выполнить больше заданного
при конфигурации числа раз — как правило, четыре раза. Принцип такой от¬
казоустойчивости состоит в том, что одиночный отказ может возникнут из-за
сбоя на одном сервере, но повторяющийся отказ вряд ли обусловлен ошибкой
в прикладном коде.
Задачи можно повторять, и поэтому MapReduce требует, чтобы функции пред¬
варительной обработки и консолидации были детерминированы. Это означает,
что при одних и тех же исходных данных функции должны всегда выдавать оди¬
наковые результаты. Это относительно легкое ограничение, но оно имеет боль¬
шое значение для правильного функционирования MapReduce. Характерным
примером недетерминированной функции служит генерирование случайных чи¬
сел. Если в задании MapReduce требуется воспользоваться случайными числами,
то с этой целью придется явным образом задать начальное значение для генера¬
тора случайных чисел, чтобы он всегда выдавал одинаковые результаты.
6.5.3. Универсальность MapReduce
Совсем не очевидно, что модель распределенных вычислений, которая под¬
держивается в MapReduce, достаточно выразительна для вычисления любых
функций, выполняемых над данными. Чтобы проиллюстрировать это положе¬
ние, покажем, как воспользоваться MapReduce для реализации функций форми¬
рования пакетных представлений по запросам, примеры которых были пред¬
ставлены в начале главы.
Глава 6, Уровень пакетной обработки
135
РЕАЛИЗАЦИЯ ПОДСЧЕТА КОЛИЧЕСТВА ПРОСМОТРОВ СТРАНИЦ ВО ВРЕМЕНИ
В следующем коде MapReduce получается пакетное представление для подсче¬
та количества просмотров страниц во времени:
function map (record) {
key = [record.url# toHour(record.timestamp)]
emit(key, 1)
function reduce(key, vals) (
emit(new HourPageviews(key[0], key[l], sum(vals)))
}
Этот код очень похож на код подсчета слов, но в качестве ключа, порождае¬
мого функцией предварительной обработки, служит структура, содержащая URL
и отметку времени, обозначающую час просмотра страницы. А результатом вы¬
полнения функции консолидации является требующееся пакетное представление,
содержащее преобразование структуры [url, hour] в количество просмотров
страницы за данный час.
РЕАЛИЗАЦИЯ ЗАКЛЮЧЕНИЯ О ГЕНДЕРНОЙ ПРИНАДЛЕЖНОСТИ
В следующем коде MapReduce заключение о гендерной принадлежности дела¬
ется из предоставляемых имен:
function map (record) {
emit(record.userid, normalizeName(record.name))
Семантическая нормализация
происходит на стадии предварительной
обработки данных
function reduce(userid, vals) {
allNames = new Set()
for(normalizedName in vals) {
allNames. add (normalizedName) <*.
}
maleProbSum =0.0
Множество, предназначенное
для удаления любых возможных
дубликатов
for(name in allNames) {
maleProbSum += maleProbabilityOfName (name) <—
1
maleProb = maleProbSum / allNames.size()
if (maleProb > 0.5) { <
gender = "male" Возвращает наиболее
} else { вероятный пол
gender = "female"
Усредняет вероятность
принадлежности
к мужскому полу
emit(new InferredGender(userid, gender))
Заключение о гендерной принадлежности делается так же просто. Функция
предварительной обработки выполняет семантическую нормализацию имен,
а функция консолидации определяет предполагаемый пол каждого пользователя.
РЕАЛИЗАЦИЯ ФАКТОРА ВЛИЯНИЯ
Предварительно вычислить фактор влияния сложнее, чем в двух предыдущих
примерах. Чтобы реализовать логику такого вычисления, два задания MapReduce
придется связать в цепочку. Идея состоит в том, чтобы результат выполнения
136
Часть I. Уровень пакетной обработки
первого задания MapReduce передать в качестве входных данных второму зада¬
нию MapReduce. В следующем коде показано, как это делается:
function mapl (record) {
emit(record.responderld, record.sourceld)
}
В первом задании для каждого
function reducel (userid, sourcelds) { пользователя определяется
influence = new Map (default=0) наиболее влиятельное лицо
for(sourceld in sourcelds) {
influence[sourceld] += 1
}
emit(topKey(influence))
function map2 (record) {
emit(record, 1)
function reduce2(influencer, vais) {
emit (new Inf luenceScore (inf luencer, sum(vals))) <]-
Наиболее влиятельное лицо
затем служит для определения
числа тех людей, на которых
оказывает влияние каждый
пользователь
Для вычислений нередко требуется несколько заданий MapReduce. Это лишь
означает потребность в нескольких уровнях группировки. В данном случае при
выполнении первого задания требуется сгруппировать все реакции по каждому
пользователю, чтобы определить наиболее влиятельное лицо для этого пользо¬
вателя. А во втором задании записи группируются по наиболее влиятельному
лицу; чтобы определить факторы влияния.
Сделаем небольшое отступление, чтобы выяснить, что же MapReduce делает
на самом основном уровне.
■ Произвольно разделяет данные по ключу, порождаемому на стадии пред¬
варительной обработки. Произвольное разделение позволяет связать дан¬
ные вместе для последующей обработки, которая по-прежнему выполняет¬
ся параллельно.
■ Произвольно преобразовать данные с помощью кода, предоставляемого
на стадиях предварительной обработки и консолидации.
Трудно себе представить что-нибудь более универсальное, что могло бы
по-прежнему оставаться масштабируемой распределенной системой.
Сравнение MapReduce и Spark
Spark является относительно новой системой распределенных вычислений, привлекшей
немало внимания. В ней применяется модель вычислений в виде “устойчивых распреде¬
ленных массивов данных". Система Spark не более универсальна, чем MapReduce, но ее
модель позволяет достичь намного большей производительности алгоритмов, в которых
приходится неоднократно обходить один и то же массив данных, поскольку система Spark
способна кешировать данные в оперативной памяти, а не читать их всякий раз с диска. Во
многих алгоритмах машинного обучения неоднократно перебираются одни и те же данные,
и поэтому для них особенно пригодной оказывается система Spark.
Глава 6. Уровень пакетной обработки
137
6.6. Низкоуровневый характер MapReduce
К сожалению, MapReduce не очень годится для написания изящного кода,
хотя и является отличным примитивом для пакетных вычислений, предоставляя
универсальный, масштабируемый и отказоустойчивый способ вычисления функ¬
ций в крупных массивах данных. Написанные вручную программы MapReduce
оказываются длинными, неуклюжими и трудно понимаемыми. Рассмотрим неко¬
торые причины, по которым это происходит.
6.6.1. Многоэтапные вычисления неестественны
В приведенном выше примере определения фактора влияния было проде¬
монстрировано вычисление, требующее двух заданий MapReduce. В этом приме¬
ре отсутствует код, связывающий оба задания вместе. Для выполнения задания
MapReduce требуется не только иметь в своем распоряжении функции предва¬
рительной обработки и консолидации, но и знать, откуда именно следует читать
входные данные и куда выводить выходные данные (или результаты). Но в том-
то и дело, что для нормального функционирования кода требуется место для раз¬
мещения промежуточных результатов между первой и второй стадией вычисле¬
ний. А в дальнейшем промежуточные результаты необходимо удалить, чтобы они
занимали ценное место на диске не дольше, чем это требуется для вычислений.
Это обстоятельство должно сразу же вызвать у вас тревогу, поскольку оно
явно свидетельствует о том, что вы работаете на низком уровне абстракции.
Ведь абстракция требуется там, где вычисление может быть полностью представ¬
лено как одна принципиальная единица, а такие подробности, как управление
временными путями, разрешаются автоматически.
6.6.2. Соединения очень трудно реализуются вручную
Рассмотрим более сложный пример реализации соединения средствами
MapReduce. Допустим, имеются два отдельных массива данных: один из них со¬
держит записи с полями id и аде, а другой — записи с полями user_id, gender
и location. Для каждого поля с идентификатором (id и user_id) в обоих мас¬
сивах данных требуется вычислить возраст (аде), пол (gender) и место житель¬
ства (location) пользователя. Такая операция называется внутренним соединением
и иллюстрируется на рис. 6.13. Соединения являются весьма распространенны¬
ми операциями, и вам они могут быть знакомы из опыта составления запросов
к реляционным базам данных на языке SQL.
Чтобы осуществить соединение средствами MapReduce, нужно прочитать
записи из двух независимых массивов данных в одном задании MapReduce.
Следовательно, в этом задании должны различаться записи из двух разных мас¬
сивов данных. Как правило, каркас MapReduce предоставляет контекст источни¬
ка поступления записи, хотя это и не было продемонстрировано ранее в псевдо¬
коде. Поэтому расширим псевдокод, включив в него этот контекст. Ниже приве¬
ден код, в котором реализуется внутреннее соединение.
138
Часть /. Уровень пакетной обработки
Рис. 6.13. Пример двустороннего внутреннего соединения
Использовать исходный каталог, откуда поступила
запись, чтобы выяснить местоположение записи
с левой или с правой стороны соединения
Здесь значения,
предназначенные
для размещения
в выходной записи,
вводятся в список.
В дальнейшем
они будут связаны
с записями
по любую сторону
соединения
для получения
результата
function join_map(sourcedir, record) {
if(sourcedir=="/data/age") {
emit (record.id, {"side" = "1", <—
—1> "values" = [record.age]})
} else {
emit(record.user_id, "side" = "r",
"values" = [record.gender, record.location]
Задать в ключе MapReduce
идентификатор id или user_id
соответственно. В итоге все записи
с этими идентификаторами
по любую сторону соединения
будут переданы одной и той же
вызываемой функции консолидации.
При соединении сразу
по нескольким ключам пришлось
бы задать коллекцию
в ключе MapReduce
function join_reduce(id, records)
side_l = []
side_r = []
for(record : records) { <
values = record.get ("values")
if(record.get("side") == "1") I
При консолидации записи по любую сторону
соединения сначала разделяются
на "левый” и "правый” списки
side_l.add(values)
} else {
side r.add(values)
Связать каждую
запись по одну
сторону соединения
с каждой записью
по другую сторону
соединения, чтобы
достичь нужной
семантики
соединения
-£>
}
for(l : side_l) {
for (г : side_r) {
emit(concat([id], 1, r), null)
}
}
Идентификатор добавляется
к связанным значениям
для получения конечного результата.
Следует иметь в виду, что MapReduce
всегда оперирует парами
"ключ-значение”, поэтому в данном
примере получается результат
с заданным ключом и пустым
значением, хотя это можно было бы
сделать и по-другому
И хотя приведенный выше код не такой уж и громоздкий, разобраться в его
функционировании совсем не просто. Главная трудность состоит в том, чтобы опре¬
делить связь записей по каждую сторону соединения с конкретными каталогами,
Глава 6. Уровень пакетной обработки
139
чтобы настроить код на соединение записей из разных каталогов. Кроме того,
MapReduce вынуждает выражать все в виде пар “ключ-значение”, тогда как в ре¬
зультате выполнения данного задания должен быть получен лишь список значений.
А ведь это всего лишь простое двухстороннее внутреннее соединение по од¬
ному полю. Представьте себе соединение по нескольким полям (по пяти с ка¬
ждой его стороны), причем одни соединения должны быть внешними, а дру¬
гие — внутренними. Очевидно, что вам вряд ли захочется писать код всякий раз,
когда потребуется соединение. Следовательно, у вас должна быть возможность
установить соединение на более высоком уровне абстракции.
6.6.3- Логическое исполнение тесно связано с физическим
Рассмотрим еще один пример, раскрывающий причины низкоуровневой аб¬
стракции в MapReduce. С этой целью расширим приведенный ранее пример под¬
счета слов, чтобы отсеивать слова the и а и производить двойной, а не одиноч¬
ный их подсчет. В следующем коде показано, как этого добиться:
EXCLUDE JTORDS = Set("a", "the")
function map(sentence) {
for(word : sentence) {
if(not EXCLUDE_WORDS.contains(word)) {
emit(word, 1)
}
function reduce(word, amounts) {
result = 0
for (amt : amounts) {
result += amt
}
emit (result * 2)
}
Этот код вполне работоспособен, но в нем, по-видимому, несколько задач со¬
вмещаются в одной и той же функции. Надлежащие нормы программирования
предписывают разделение независимых функциональных возможностей в от¬
дельные функции. Порядок вычислений в данном примере приведен на рис. 6.14.
Рис. 6.14. Разложение вычислений на составляющие
в видоизмененном примере подсчета слов
140
Часть /. Уровень пакетной обработки
Рассматриваемый здесь код можно разбить таким образом, чтобы в каждом
задании MapReduce выполнялась лишь одна из функций. Но задание MapReduce
подразумевает конкретное физическое исполнение: сначала ряд процессов пред¬
варительной обработки, затем ввод-вывод в сеть или на диск для передачи про¬
межуточных результатов функции консолидации и далее ряд процессов консоли¬
дации для получения окончательного результата. Но вследствие разбиения кода
на отдельные модули может быть сформировано больше заданий MapReduce,
чем требуется, а следовательно, вычисления окажутся весьма неэффективными.
И это вынуждает идти на трудный компромисс: связать все функциональные
возможности вместе, следуя примерам неудачной практики разработки про¬
граммного обеспечения, или разбить код на модули, что повлечет за собой неэ¬
ффективное использование ресурсов. Но в действительности идти на такой ком¬
промисс совсем не обязательно, а вместо этого можно принять обоюдовыгодное
решение: сочетать полную модульность с компиляцией кода для оптимального
физического исполнения. Далее будет показано, как этого добиться.
6.7. Конвейерные схемы для рассмотрения пакетных
вычислений на более высоком уровне
В этом разделе будет представлен намного более естественный способ рассмо¬
трения пакетных вычислений, называемый конвейерными схемами. Такие схемы
могут быть скомпилированы для исполнения в виде эффективного ряда заданий
MapReduce. Как будет показано далее, каждый рассматриваемый здесь пример,
включая и приложение SuperWebAnalytics.com, может быть кратко представлен
в виде конвейерной схемы.
Конвейерные схемы удобны тем, что они дают возможность обсуждать па¬
кетные вычисления в лямбда-архитектуре, не вдаваясь в подробности написания
псевдокода MapReduce. Они примечательны своей краткостью и интуитивно¬
стью, чего явно недостает MapReduce. Кроме того, конвейерные схемы дают
возможность обсуждать отдельные алгоритмы и преобразования, применяемые
при обработке данных для разрешения затруднений в рассматриваемых здесь
примерах, не прибегая к конкретным инструментальным средствам.
Применение конвейерных схем на практике
Конвейерные схемы являются отнюдь не гипотетическим понятием. Все высокоуровне¬
вые инструментальные средства MapReduce, в том числе Cascading, Pig, Hive и Cascalog,
являются прямым отображением конвейерных схем. В какой-то степени это относится
и к системе Spark, хотя ее модель данных не включает в себя понятие кортежей с про¬
извольным числом именованных полей.
6.7.1. Принципы построения конвейерных схем
В основу конвейерных схем положен принцип рассмотрения обработки данных
с точки зрения кортежей, функций, агрегаторов, соединений и слияний, т.е. тех
понятий, которые уже известны из БС)Ь. Так, на рис. 6.15 приведена конвейерная
Глава 6. Уровень пакетной обработки
141
схема для видоизмененного примера подсчета слов,
рассматривавшегося в разделе 6.6.3 и дополненного
фильтрацией и двойным подсчетом слов.
Вычисление начинается с кортежей, состоящих
из единственного именованного поля sentence,
функция split () преобразует единственный кортеж
предложения во многие кортежи с дополнительным
полем word. Эта функция принимает в качестве вход¬
ных данных поле sentence, создает и возвращает в ка¬
честве выходных данных (т.е. результата) поле word.
В качестве примера на рис. б. 16 показано, что
происходит с массивом кортежей предложения по¬
сле применения к ним функции split (). Как видите,
поле sentence дублируется во всех новых кортежах.
Безусловно, в конвейерных схемах могут быть ото¬
бражены не только предопределенные функции. Это
могут быть любые функции, реализуемые на языке
программирования общего назначения. То же самое
относится к фильтрам и агрегаторам.
Далее применяется фильтр для отсеивания слов а
и the. Его действие показано на рис. 6.17.
Далее весь массив кортежей группируется по полю
word, после чего к каждой группе применяется агрега¬
тор count. Такое преобразование показано на рис. 6.18.
Далее поле count дублируется для создания но¬
вого поля double и удвоения подсчета, как показано
на рис. 6.19.
И наконец, выбираются поля, требующиеся для по¬
лучения конечного результата. Остальные поля отбрасываются.
Как видите, конвейерные диаграммы примечательны, в частности, тем, что
поля не изменяются после их создания. Очевидно, что один из способов опти¬
мизации состоит в том, чтобы отбросить поля, как только они перестанут быть
нужными, и тем самым избежать излишней сериализации и ввода-вывода данных
в сеть. Зачастую инструментальные средства, реализующие конвейерные схемы,
выполняют подобную сериализацию автоматически. Поэтому на практике приве¬
денный выше пример будет выполнен так, как показано на рис. 6.20.
В конвейерных схемах отображаются две другие важные операции, которые
служат для объединения независимых массивов кортежей.
Первой из них является операция соединения, позволяющая устанавливать
внутренние и внешние соединения любого числа массивов кортежей. В разных
инструментальных средствах поля с каждой стороны соединения указываются
по-разному, но, на наш взгляд, самое простое обозначение состоит в том, чтобы
выбрать любые поля, которые являются одинаковыми для всех сторон соеди¬
нения. Для этого все соединяемые поля должны именоваться совершенно оди¬
наково, а каждая сторона соединения обозначается как внутренняя ил внешняя.
Некоторые примеры конвейерных схем соединений приведены па рис. 6.21.
Рис. 6.15. Конвейерная схема
для видоизмененного
примера подсчета слов
142
sentence
the dog
fly to the moon
dog
7
Функция:
Split
(sentence) -> (word)
sentence
word
the dog
the
the dog
dog
fly to the moon
flv
fly to the moon
to
fly to the moon
the
fly to the moon
moon
dog
dog
Рис. 6.16. Конвейерная схема функции
Часть L Уровень пакетной обработки
sentence
word
the dog
the
the dog
dog
fly to the moon
fly
fly to the moon
to
fly to the moon
the
fly to the moon
moon
dog
dog
Фильтр:
FilterAandThe
(word)
sentence
word
the dog
dog
fly to the moon
fly
fly to the moon
to
fly to the moon
moon
dog
dog
Рис. 6.17. Конвейерная схема фильтра
sentence
word
the dog
dog
fly to the moon
fly
fly to the moon
to
fly to the moon
moon
dog
dog
Агрегатор:
count
()-> (count)
word
count
dog
2
fly
1
to
1
moon
1
Группировка:
[word]
sentence
word
the dog
dog
dog
dog
fly to the moon
fly
fly to the moon
to
fly to the moon
moon
Рис. 6.18. Конвейерная схема группировки и агрегирования
Глава 6. Уровень пакетной обработки
143
УУОГб
count
бод
2
Ау
1
1
тооп
1
Функция:
double
(count) -> (double)
\Arord
count
double
бод
2
4
«У
1
2
ю
1
2
тооп
1
2
Рис. 6.19. Конвейерная схема выполнения функции double ()
Рис. 6.20. Поля автоматически отбрасываются, когда они больше не нужны
144
Часть /. Уровень пакетной обработки
пите
ЬоЬ
25
<т1их
71
ЬоЬ
37
БОІІу
21
Внутреннее
name
ponder
bob
m
sally
f
george
m
bob
m
> я
„ Внешнее
Вну грен нее
Внешнее
name
location
maria
USA
bally
Brazil
george
Japan
Соединение
Внутреннее
Внугреннее
JL
Внешнее
ІИНЄЄ
vJL
Соединение
пате
аде
gender
ЬоЬ
25
т
ЬоЬ
25
т
ЬоЬ
37
т
ЬоЬ
37
т
эаНу
21
f
Соединение
пате
gender
location
bob
m
null
sally
f
Brazil
george
m
Japan
bob
m
null
name
аде
gender
location
sally
21
f
Brazil
george
пиН
m
Japan
Рис. 6.21. Примеры конвейерных схем внутренних, внешних и смешанных соединений
Второй является операция слияния, объединяющая независимые массивы
кортежей в единый массив. Для выполнения операции слияния все массивы кор¬
тежей должны иметь одинаковое число полей. В пей также указываются новые
имена кортежей. Пример конвейерной схемы слияния приведен на рис. (5.22.
Рис. 6.22. Пример конвейерной схемы операции слияния
Глава 6. Уровень пакетной обработки
145
А теперь рассмотрим более интересный пример. Допустим, имеется один мас¬
сив данных с полями [person, gender], а другой — с полями [person, follower].
Допустим также, что требуется подсчитать количество лиц мужского пола, под¬
писчиками на сообщения которых является каждый пользователь. Конвейерная
схема такого вычисления показана на рис. 6.23.
Рис. 6.23. Конвейерная схема подсчета подписчиков
6.7.2. Выполнение конвейерных схем средствами MapReduce
Конвейерные схемы дают возможность рассматривать пакетные вычисления
на высоком уровне, но их нетрудно скомпилировать в ряд заданий MapReduce.
Это означает, что их можно выполнять с масштабированием. Каждая операция
в конвейерной схеме преобразуется в задание MapReduce следующим образом.
■ Функции и фильтры. Обрабатывают записи по очереди, и поэтому их
можно выполнять на стадии предварительной обработки или же на стадии
консолидации после соединения или агрегирования.
■ 1^уппировка. Эта операция легко преобразуется в задание MapReduce
по ключу, порождаемому на стадии предварительной обработки. Если груп¬
пировка осуществляется по нескольким значениям, то в качестве ключа
будет служить список этих значений.
■ Агрегирование. Происходит на стадии консолидации, поскольку оно про¬
веряет все кортежи для группы.
146
Часть L Уровень пакетной обработки
■ Соединение. При рассмотрении основ реализации соединений ранее было
выяснено, что на стадии предварительной обработки и консолидации им
требуется некоторый код. В разделе 6.6.2 был представлен код для двухсто¬
роннего внутреннего соединения на стадии консолидации. Этот код мож¬
но расширить для обработки произвольного числа сторон соединения,
а также внутренних и внешних соединений в любом сочетании.
■ Слияние. Эта операция просто означает, что тот же самый код будет вы¬
полняться над несколькими массивами данных.
Еще важнее, что компилятор, обладающий развитой логикой, разместит на од¬
ной и той же стадии предварительной обработки или консолидации как можно
больше операций, чтобы свести к минимуму число стадий процесса MapReduce
и обеспечить максимальную эффективность. Это даст возможность разделить про¬
цесс вычисления на независимые стадии, не принося в жертву производительность.
На рис. 6.24 приведена сокращенная конвейерная схема, где обрамленными пункти¬
ром прямоугольниками обозначен порядок компиляции заданий MapReduce. Одна
стадия консолидации, следующая после других стадий консолидации, подразумевает
промежуточную стадию предварительной обработки для установки соединения.
Рис. 6.24. Компиляция конвейерной схемы в задания MapReduce
Глава 6. Уровень пакетной обработки 147
6.7.3. Объединяющие агрегаторы
Имеется специальный вид агрегаторов, называемых объе¬
диняющими и допускающих более эффективное выполнение,
чем обычные агрегаторы. В некоторых случаях применение
объединяющих агрегаторов играет решающую роль для обе¬
спечения масштабируемости. А поскольку такие случаи воз¬
никают очень часто, то нужно знать, как пользоваться объе¬
диняющими агрегаторами.
Допустим, требуется подсчитать все записи в массиве
данных. Конвейерная схема для этой цели будет выглядеть
так, как показано на рис. 6.25.
Стадия глобальной группировки обозначает, что каждый
кортеж должен быть направлен в одну и ту же группу, тогда
как агрегатор — выполнен над каждым отдельным кортежем
в массиве данных. Обычный способ состоит в том, чтобы
направить сначала каждый кортеж на одну и ту же машину,
а затем выполнить код агрегатора на этой машине. Но та¬
кой способ не подразумевает масштабирование, поскольку
в этом случае теряется всякая видимость параллелизма.
Тем не менее подсчет может быть произведен более эф¬
фективно. Вместо того чтобы направлять каждый кортеж на одну и ту же машину,
можно произвести сначала частичный подсчет на каждой машине, где находится
часть массива данных, а затем отправить результаты частичного подсчета на одну
машину и подытожить их, получив глобальный подсчет. Количество частичных
подсчетов будет равно количеству машин в кластере, и поэтому глобальная часть
вычисления потребует очень малого объема работы.
Все объединяющие агрегаторы действуют следующим образом: сначала вы¬
полняется частичное агрегирование, а затем объединение частичных результа¬
тов для получения желаемого результата. Поведение далеко не всякого агрега¬
тора удается выразить подобным образом, но если такая возможность все же
имеется, то она позволяет добиться заметного повышения производительности
и усиления масштабируемости как при глобальном агрегировании, так и при
агрегировании только нескольких групп. Подсчет и суммирование, являющиеся
типичными операциями для большинства агрегаторов, могут быть реализованы
в виде объединяющих агрегаторов.
Рис. 6.25. Глобальное
агрегирование
6.7.4. Примеры конвейерных схем
В начале этой главы были представлены три примера затруднений, возника¬
ющих при пакетных вычислениях. А теперь посмотрим, каким образом можно
разрешить эти затруднения практическим и масштабируемым способом с помо¬
щью конвейерных схем.
Подсчитать количество просмотров страниц во времени совсем не трудно,
как показано на рис. 6.26. Для этого достаточно преобразовать сначала каждую
отметку времени в учетный период, а затем подсчитать количество просмотров
страниц по каждому иЯЬ или за учетный период.
148
Часть /. Уровень пакетной обработки
Заключение о гендерной принадлежности также нетрудно сделать, как по¬
казано на рис. 6.27. Для этого достаточно нормализовать каждое имя, вызвать
функцию та1еРгоЬаЬ:11:^уО£Ыате (), чтобы получить вероятность гендерной при¬
надлежности для каждого имени, а затем вычислить среднюю вероятность при¬
надлежности каждого пользователя к мужскому полу. И наконец, остается выпол¬
нить функцию, относящую пользователей со средней вероятностью больше 0,5
к мужскому полу, а с меньшей средней вероятностью — к женскому.
Рис. 6.26. Конвейерная схема для подсчета Рис. 6.27. Конвейерная схема
количества просмотров страниц во времени для заключения о гендерной принадлежности
И наконец, покажем, как разрешить затруднение, связанное с определени¬
ем фактора влияния. Конвейерная схема для этой цели приведена на рис. 6.28.
Сначала наиболее влиятельное лицо выбирается для каждого пользователя пу¬
тем группировки по идентификатору реагирующего пользователя и выбора вли¬
ятельного лица, на сообщения которого этот пользователь реагировал чаще все¬
го. А затем просто подсчитывается, сколько раз каждое влиятельное лицо оказы¬
вается для кого-то наиболее влиятельным.
Как видите, все эти примеры затруднений довольно изящно разлагаются
на конвейерные схемы, которые наглядно показывают, каким образом следует
рассматривать преобразования данных. Когда в главе 8 речь пойдет о построе¬
нии уровня пакетной обработки для приложения SuperWebAnalytics.com, а для
этого потребуются намного более сложные вычисления, у вас будет возможность
убедиться, сколько времени и труда удается сэкономить, пользуясь такого рода
высокоуровневой абстракцией.
Глава 6. Уровень пакетной обработки
149
Рис. 6.28. Конвейерная схема для определения фактора влияния
Резюме
Уровень пакетной обработки образует сердцевину лямбда-архитектуры. Для
уровня пакетной обработки характерна большая задержка, и этим обстоятель¬
ством можно воспользоваться для глубокого анализа затратных вычислений, ко¬
торые нельзя выполнять в реальном времени. В этой главе было показано, что
при разработке пакетных представлений приходится идти на компромиссы меж¬
ду размером формируемого представления и объемом работы, которая потребу¬
ется во время обработки запроса для его завершения.
Парадигма MapReduce предоставляет универсальные примитивы для пред¬
варительного вычисления функций запросов по всем данным масштабируемым
способом. Но подобные вычисления трудно рассматривать на низком уровне
абстракции в MapReduce. Несмотря на то что MapReduce обеспечивает отказо¬
устойчивость, распараллеливание и планирование заданий, очевидно, что рабо¬
тать на исходном уровне MapReduce неудобно и трудно. Поэтому в этой главе
были представлены конвейерные схемы в качестве более краткого и естествен¬
ного способа рассмотрения пакетных вычислений. А в следующей главе будет
представлена высокоуровневая абстракция, называемая JCascalog и реализующая
конвейерные схемы.
пакетной обработки
В этой главе...
■ Источники сложности в коде обработки данных.
■ Практическая реализация конвейерных схем
средствами .Юазса1о&
■ Применение методов абстракции и композиции
к обработке данных.
В предыдущей главе было показано, насколько кратко и естественно
конвейерные схемы позволяют определить вычисления, оперирующие большими
объемами данных. В ней было также показано, что конвейерные схемы
можно выполнять в виде последовательности заданий MapReduce, соблюдая
параллелизм и масштабируемость.
А в этой иллюстративной главе будет рассмотрено инструментальное средство
JCascalog, непосредственно реализующее конвейерные схемы. Для пояснения
JCascalog придется изложить немало материала, и поэтому эта глава обширнее
предыдущих иллюстративных глав. Как всегда, теоретические вопросы лямбда-
архитектуры можно изучать, не читая иллюстративные главы, но эта иллюстративная
глава особенная, поскольку в ней преследуется цель наглядно показать, чего можно
добиться инструментальными средствами обработки данных. И самое главное, что
код обработки данных ничем не отличается от любого другого прикладного кода,
а следовательно, он требует достаточной абстракции, допускающей повторное
использование и композицию. Абстракция и композиция являются краеугольными
камнями качественной разработки программного обеспечения.
Вместо того чтобы ограничиваться обсуждением того, каким образом
в JCascalog реализуются конвейерные схемы, мы рассмотрим возможности
JCascalog для применения целого ряда методов абстракции и композиции, чего
нельзя добиться другими инструментальными средствами. Как показывает наш
опыт, большинство разработчиков информационных систем мыслят категориями
152
Часть I. Уровень пакетной обработки
языка SQL, ставшего золотым стандартом для инструментальных средств
манипулирования данными, и, на наш взгляд, такое мировоззрение страдает
серьезными ограничениями. Многим инструментальным средствам обработки
данных присущи сложности, обусловленные не характером решаемой задачи,
а построением самого инструментального средства. Сначала мы обсудим эти
сложности, а затем покажем, как обойти подобные классические скрытые
ограничения с помощью JCascalog. У вас будет возможность убедиться, что
JCascalog допускает применение таких методик программирования, которые
позволяют писать очень краткий и изящный код.
7.1. Иллюстративный пример
Как упоминалось ранее, подсчет слов служит каноническим примером
применения MapReduce, поэтому рассмотрим его реализацию средствами
JCascalog. Ради простоты будем хранить входной массив данных (текст
Геттисбергской речи президента Авраама Линкольна) списком в оперативной
памяти, где каждая фраза хранится отдельно:
Сокращено
ради краткости
List SENTENCE = Arrays.asList (
Arrays.asList ("Four score and seven years ago our fathers"),
Arrays.asList ("brought forth on this continent a new nation"),
Arrays .asList ("conceived in Liberty and dedicated to"),
Arrays.asList ("the proposition that all men are created equal"),
В следующем фрагменте кода представлена полная реализация в ^авса^
подсчета слов в упомянутом выше массиве данных:
Результаты
обработки запросов
выводятся на консоль
Обозначает типы выводимых данных,
возвращаемых по запросу
Api.execute(new StdoutTap(),
new Subquery("?word", "?count")
.predicate (SENTENCE, "?sentence") <—
Читает каждое
предложение
из входных данных
.predicate(new Split(), "?sentence").out("?word")
Разбивает каждое
предложение
на отдельные слова,
выделяя их в лексемы
.predicate(new Count(), "?count"));
1
Определяет подсчет
каждого слова
Прежде всего обратите внимание на краткость приведенного выше кода!
Высокоуровневый характер JCascalog с трудом позволяет поверить, что это
интерфейс MapReduce, но данный код выполняется как задание MapReduce.
После его выполнения полученный результат выводится на консоль в виде
списка, часть которого ради краткости показана ниже.
RESULTS
But 1
Four 1
God 1
It 3
Liberty 1
Now 1
The 2
We 2
153
Глава 7. Иллюстрация уровня пакетной обработки
Проанализируем данный код подсчета слов построчно, чтобы разобраться
вею назначении. Если в этом анализе вам не все будет ясно, не отчаивайтесь.
Мы рас смотрим JCascalog более подробно далее в этой главе.
В JCascalog входные и выходные данные определяются посредством абстрак¬
ции, называемой отводом. Абстракция отвода позволяет отображать результа¬
ты на консоли, сохранять их в системе HDFS или записывать в базе данных.
Приведенная ниже первая строка анализируемого здесь кода означает следую¬
щее: выполнить следующее вычисление и направить результаты на консоль.
Api.execute(new StdoutTap(), ...
Вторая строка анализируемого кода начинается с определения самого вычис¬
ления. Вычисления представлены экземплярами класса Subquery, обозначающе¬
го подзапрос, который порождает массив кортежей, содержащих два поля, ?word
и ?count, как показано ниже.
new Subquery("?word", "?count")
В следующей строке анализируемого кода указан источник входных данных
для запроса, как показано ниже. Эти данные читаются из массива SENTENCE,
а затем порождаются кортежи, содержащие одно поле ?sentence. Аналогично
выходным данным, абстракция отвода допускает получение данных из разных
источников, в том числе значений, хранящихся в оперативной памяти, файлов,
находящихся в системе HDFS, или результатов, получаемых из других запросов.
.predicate(SENTENCE, "?sentence")
В четвертой строке анализируемого кода каждое предложение разбивается
на отдельные слова, затем в качестве входных данных функции Split () предо¬
ставляется поле ?sentence, а выходные данные (т.е. результаты) сохраняются
в новом поле ?word:
.predicate (new SplitO, "?sentence") .out ("?word")
Функция SplitO не входит в состав прикладного программного интерфейса
JCascalog API, но она наглядно демонстрирует, каким образом новые определяе¬
мые пользователем функции могут быть интегрированы в запросы. Ее действие
определяется приведенным ниже одноименным классом. Определение этого
класса не требует особых пояснений. Он принимает предложения в качестве
входных данных и порождает новый кортеж для каждого слова в предложении.
public static class Split extends CascalogFunction {
public void operate (FIowProcess process, FunctionCall call) {
String sentence = call, get Arguments () .getString (0);
for (String word: sentence.split(" ")) { < '
call. getOutputCollector () . add (new Tuple (word)); « i ПорождаеТ отдельный
} I кортеж для каждого слова
)
И наконец, в последней строке анализируемого здесь кода подсчитывается,
сколько раз каждое слово встречается в тексте, а полученный результат сохраня¬
ется в переменной ?соипі, как показано ниже. А теперь, когда у вас сложилось
154
Часть I. Уровень пакетной обработки
некоторое представление о JCascalog, рассмотрим некоторые из типичных скры¬
тых ограничений, способных привести к излишней сложности инструменталь¬
ных средств обработки данных.
.predicate(new Count(), и?count"));
7.2. Типичные ограничения, скрывающиеся
в инструментальных средствах обработки данных
Как и любой другой код, код обработки данных следует сохранять как можно
более простым, чтобы иметь возможность анализировать разрабатываемую си¬
стему и обеспечивать правильность ее функционирования. Сложность кода про¬
является в следующих двух формах: принципиальная сложность, присущая решае¬
мой задаче, а также второстепенная сложность, возникающая только из подхода
к решению задачи. Сводя к минимуму второстепенную сложность, можно упро¬
стить сопровождение кода и приобрести большую уверенность в его правильно¬
сти. В этом разделе мы рассмотрим два источника второстепенной сложности
в коде обработки данных: специальные языки программирования и неудачно
составляемые абстракции.
7.2.1. Специальные языки программирования
Типичным источником сложности в инструментальных средствах обработ¬
ки данных служит употребление специальных языков программирования. К их
числу относятся SQL для реляционных баз данных или Pig и Hive для Hadoop.
Несмотря на искушение воспользоваться специальным языком для обработки
данных, такое решение чревато серьезными осложнениями.
Применение специальных языков чинит языковое ограничение, требующее
наличия интерфейса для взаимодействия с остальными частями прикладного
кода. Такой интерфейс является типичным источником ошибок и неизбежной
сложности. Например, в атаках умышленного внесения запросов SQL выгодно
используется некорректно определенный интерфейс между пользовательской
частью кода и операторами SQL для составления запросов реляционной базы
данных. Необходимость пользоваться таким интерфейсом вынуждает разработ¬
чика быть постоянно начеку, чтобы не допустить никаких ошибок.
Языковое ограничение служит также причиной всех остальных сложностей.
Разделение на модули может вызвать трудности, если в специальном языке под¬
держиваются пространства имен и функции, которые в конечном счете уступают
своим аналогам в языках общего назначения. А если требуется внедрить в запро¬
сы собственную бизнес-логику, то придется самостоятельно создать определяе¬
мые пользователем функции и зарегистрировать их в специальном языке.
Наконец, придется каким-то образом согласовывать переход с языка общего
назначения на специальный язык обработки данных, и наоборот. Например, за¬
прос можно составить на специальном языке, а затем воспользоваться классом
Pail, упоминавшимся в главе 5, чтобы присоединить данные результатов к суще¬
ствующему хранилищу. Класс Pail вызывается стандартным для Java способом,
и поэтому придется написать сценарии командного процессора для выполнения
заданий в нужном порядке. Вследствие того что программировать приходится
155
Глава 7. Иллюстрация уровня пакетной обработки
на нескольких языках, связанных вместе посредством сценариев, такие механиз¬
мы, как исключения и их обработка, перестают действовать, и тогда приходится
проверять возвращаемые коды ошибок, чтобы не перейти случайно к следующей
стадии обработки данных, если предыдущая стадия завершилась неудачно.
Все это примеры второстепенной сложности, которой можно полностью из¬
бежать, если разработать инструментальное средство обработки данных в виде
библиотеки на языке общего назначения. Это даст возможность свободно соче¬
тать обыкновенный код с кодом обработки данных, пользоваться обычными ме¬
ханизмами разделения на модули и обеспечить надлежащее действие механизма
исключений. Как будет показано далее, обычную библиотеку можно сделать крат¬
кой и такой же удобной в обращении, как и специальный язык обработки данных.
7.2.2. Неудачно составляемые абстракции
Еще одним типичным источником второстепенной сложности может стать
совместное применение нескольких абстракций. Очень важно, чтобы из абстрак¬
ций можно было составлять композиции, образующие новые и более крупные
абстракции. В противном случае нельзя будет повторно воспользоваться кодом,
изобретая всякий раз колесо, хотя и по-разному.
Характерный тому пример — агрегатор Average, написанный на языке Apache
Pig в качестве еще одной абстракции для MapReduce. На момент написания
данной книги реализация этого агрегатора насчитывала свыше 300 строк кода
и 15 определений отдельных методов. Сложность этого агрегатора объясняется
оптимизацией кода для повышения производительности путем согласования ра¬
боты на обеих стадиях предварительной обработки и консолидации.
Недостатки реализации на языке Apache Pig состоят в том, что в ней повтор¬
но реализуются функциональные возможности агрегаторов Count и Sum, но по¬
вторно воспользоваться кодом, написанным для этих агрегаторов, нельзя. И это
прискорбно, потому что требует сопровождения большего объема кода и необ¬
ходимости внедрять всякий раз в агрегатор Average усовершенствования, кото¬
рые вносятся в агрегаторы Count и Sum. Ведь намного лучше определить агре¬
гатор Average в виде композиции агрегирования подсчета, агрегирования суммы
и функции деления.
К сожалению, абстракции на языке Apache Pig не позволяют определить агре¬
гатор Average подобным образом. А в JCascalog агрегатор Average можно опреде¬
лить следующим образом:
PredicateMacroTemplate Average = PredicateMacroTemplate.build("?val")
.out("Tavg")
.predicate(new Count (), "?count")
.predicate(new Sum(), "?val").out("?sum")
.predicate(new Div(), "?sum", "?count").out("?avg");
Помимо явной простоты, такое определение агрегатора Average оказывается
столь же эффективным, как и его реализация на языке Apache Pig, поскольку
в нем повторно используются ранее оптимизированные агрегаторы Count и Sum.
Причина, по которой подобного рода композиция допускается в JCascalog, но
совершенно недопустима в Apache Pig, кроется в фундаментальных отличиях
выражения вычислений в JCascalog и Apache Pig. Эти и другие функциональные
156
Часть L Уровень пакетной обработки
возможности JCascalog мы еще рассмотрим более подробно, а до тех пор отме¬
тим особое значение, которое приобретает способность абстракций поддавать¬
ся композиции. Далее в этой главе будет представлено немало других примеров
композиции. Итак, рассмотрев основные источники сложности инструменталь¬
ных средств обработки данных, перейдем к исследованию JCascalog.
7.3. Введение в JCascalog
JCascalog представляет собой библиотеку, написанную на Java и обеспечи¬
вающую композицию абстракций для выражения вычислений, выполняемых
на уровне MapReduce. Напомним, что назначение данной книги — проиллю¬
стрировать принципы организации больших данных, используя конкретные ин¬
струментальные средства, чтобы обосновать эти принципы. Имеются и другие
инструментальные средства, предоставляющие высокоуровневые интерфейсы
для MapReduce, в том числе Hive, Pig и Cascading как самые распространенные,
но многим из них все еще присущи ограничения на абстракцию и композицию
кода обработки данных. Мы выбрали JCascalog потому, что это инструментальное
средство специально предназначено для применения новых методов абстракции
и композиции с целью уменьшить сложность пакетной обработки данных.
JCascalog служит для декларативной абстракции, где вычисления выражают¬
ся через логические ограничения. Вместо того чтобы предоставлять явные ин¬
струкции для получения требующихся выходных данных (т.е. результатов), по¬
следние можно описать с точки зрения входных данных. На основании такого
описания в JCascalog определяется наиболее эффективный способ выполнения
вычислений в виде последовательности заданий MapReduce.
Если у вас имеется опыт работы с реляционными базами данных, JCascalog
покажется вам необычным и в то же время знакомым инструментальным сред¬
ством. В частности, вы непременно узнаете такие знакомые вам понятия, как
декларативное программирование, соединения и агрегирование. Но эти поня¬
тия представлены совсем иначе, чем в SQL, поскольку они реализованы в виде
прикладного программного интерфейса API, основанного на логическом про¬
граммировании.
7.3.1. Модель данных JCascalog
Модель данных JCascalog такая же, как и у конвейерных схем, представленных
в предыдущей главе. JCascalog манипулирует и преобразует кортежи — именован¬
ные списки значений, где каждое значение может иметь любой тип объекта. У
массива кортежей имеется общая схема, устанавливающая количество полей в ка¬
ждом кортеже, а также имя каждого поля. На рис. 7.1 в качестве примера приве¬
ден массив кортежей с общей схемой. При выполнении запроса JCascalog пред¬
ставляет первоначальные данные в виде одних кортежей и преобразует входные
данные в последовательность других кортежей на каждой стадии вычислений.
Возможности JCascalog лучше всего представить на разнообразных приме¬
рах. Помимо упомянутого выше массива данных SENTENCE, мы воспользуемся
рядом других массивов данных, размещаемых в оперативной памяти, чтобы
продемонстрировать особенности JCascalog. Примеры из этих массивов данных
157
Глава 7. Иллюстрация уровня пакетной обработки
приведены на рис. 7.2, а полный их ряд можно найти в исходном коде, прила¬
гаемом к данной книге. Библиотека ^авсак^ выгодно отличается простым син¬
таксисом, способным выразить сложные запросы. Поэтому рассмотрим далее
структуру запросов в ^авсак^.
О Общая схема определяет имя
каждого поля в кортеже
© Каждый кортеж соответствует
отдельной записи и может содержать
разнотипные данные
Рис. 7.1. Пример массива кортежей с общей схемой, описывающей их содержимое
?name
?age
?gender
"allce"
28
T
"jim"
48
"m"
"emily"
21
T
"david"
25
"m"
Избыток знаков препинания
После ознакомления с некоторыми примерами применения JCascalog у вас может воз¬
никнуть естественный вопрос: “Что означают все эти знаки препинания?" Поля, имена
которых начинаются со знака вопроса (?), не допускают пустые значения null. Если
JCascalog встретится кортеж с пустым значением в поле, где оно не допускается, это
поле будет сразу же исключено из рабочего массива данных. А имена полей, начинаю¬
щиеся с восклицательного знака (!), могут содержать пустые значения.
Поля, имена которых начинаются с двойного восклицательного знака (!!), также до¬
пускают пустые значения и требуются для установления соединений между массивами
данных. В соединениях по такого рода полям записи, не удовлетворяющие условию со¬
единения массивов данных, все равно включаются в результирующий массив, но с пу¬
стыми значениями для тех полей, где данные отсутствуют.
FOLLOWS
?person
?follows
"alice"
"david"
"allce"
"bob"
"bob"
"david"
"emily"
"gary"
GENDER
?person
?gender
"allce”
"bob"
"m"
"Chris"
"m"
"emily”
T
AGE
?person
?age
"allce"
28
"bob"
33
"Chris"
40
"david"
25
Рис. 7.2. Примеры массивов данных, применяемых для демонстрирования прикладного
программного интерфейса JCascalog API: массив людей разного возраста,
отдельный массив для определения пола, массив отношений подписчиков
(в социальной сети Twitter), а также массив целых значений
7.3.2. Структура запросов в JCascalog
Запросы, формируемые средствами JCascalog, обладают однородной структу¬
рой, состоящей из целевого отвода и подзапроса, определяющего конкретное
вычисление. Так, в следующем примере кода приведен запрос па обнаружение
всех людей из массива данных AGE в возрасте моложе 30 лет:
15© Часть /. Уровень пакетной обработки
. . , I Целевой отвод
Api. execute (new StdoutTap (), i Выходные поля
new Subquery("?person") < '
.predicate(AGE, "?person", "?age") о—, „
.pr,aic«e,„„ -7W, 30)) ; 1 (XSSSSr
Обратите внимание на то, что в JCascalog употребляются предикаты, описы¬
вающие требующийся результат, вместо того чтобы выражать порядок выполне¬
ния вычисления. Эти предикаты позволяют выразить все возможные операции
над массивами кортежей, в том числе преобразования, фильтры, соединения
и прочее. Предикаты можно разделить на следующие основные типы.
■ Предикат функции. Устанавливает взаимосвязь между рядом входных и вы¬
ходных полей. К этому типу относятся математические функции, напри¬
мер, сложения и умножения, хотя функция может также порождать не¬
сколько кортежей из одних входных данных.
■ Предикат фильтра. Накладывает ограничение на ряд входных полей и уда¬
ляет все кортежи, не удовлетворяющие этому ограничению. К этому типу
относятся логические операции “больше” и “меньше”.
■ Предикат агрегатора. Функция, выполняемая над группой кортежей.
Например, агрегатор может вычислить среднее, выдав единый результат
для всей группы.
■ Предикат генератора. Простое конечное множество кортежей. Генератор
может служить конкретным источником данных (например, структурой
данных, размещаемой в оперативной памяти), файлом, находящимся в си¬
стеме HDFS, или результатом другого подзапроса.
Некоторые примеры предикатов приведены на рис. 7.3.
Тип
Пример
Описание
Генератор
.predicate(SENTENCE, "? sentence")
Генератор, создающий
кортежи из массива данных
SENTENCE, Причем КЭЖДЫЙ
кортеж состоит из одного
поля ?sentence
Функция
.predicate(new Multiply(),
2, "?x").out("?z")
Эта функция удваивает
значение в поле ?х и сохра¬
няет результат в поле ? z
Фильтр
.predicate(new LT(), "?y",
50 )
Этот фильтр удаляет все
кортежи, кроме тех, где
значение в поле ?у меньше 50
Рис. 7.3. Примеры предикатов генератора, функции и фильтра. Предикаты агрегаторов
мы обсудим далее в этой главе, хотя они разделяют ту же самую общую структуру запросов
Главное проектное решение для JCascalog состояло в том, чтобы создать об¬
щую структуру, разделяемую всеми предикатами. В качестве первого аргумента
предиката служит предикатная операция, а в качестве остальных аргументов — па¬
раметры этой операции. В предикатах функции и агрегатора метки выходных
данных указываются с помощью метода out ().
159
Глава 7. Иллюстрация уровня пакетной обработки
Возможность представить каждую часть вычисления, используя единый про¬
стой согласованный механизм, — главное условие для создания абстракций с вы¬
сокой степенью композиции. Несмотря на свою простую структуру, предикаты
обеспечивают чрезвычайно богатую семантику. Эту их особенность наглядно де¬
монстрируют конкретные примеры, приведенные на рис. 7.4.
Тип
Пример
Описание
Функция
в качестве
фильтра
.predicate(new Plus(), 2, "?x'
') .out(6)
Несмотря на то что Plus ()
является функцией, этот
предикат отбирает все
кортежи, где значение
поля ?х*4
Составной
фильтр
.predicate(new Multiply(), 2,
.predicate(new Multiply()r 3,
"?a").out("?z")
"?b").out(H ?z M)
Совместно эти предикаты
отбирают все кортежи,
где2(?а)*3(?Ь)
Рис. 7.4. Простая структура предикатов позволяет выразить глубокие семантические
взаимосвязи для описания результата, требующегося в запросе
Как упоминалось ранее, соединения между массивами данных выражаются
также через предикаты. Подробнее об этом речь пойдет в следующем разделе.
7.3.3. Запрашивание нескольких массивов данных
При составлении многих запросов требуется объединить несколько массивов
данных. В реляционных базах данных это чаще всего делается с помощью опе¬
рации соединения, которая поддерживается и в JCascalog. Допустим, требуется
объединить массивы данных AGE and GENDER для создания нового массива кор¬
тежей с информацией о возрасте и поле всех людей, записи которых хранятся
в обоих массивах. Это стандартное внутреннее соединение по полю ?person, как
показано на рис. 7.5.
Рис. 7.5. Внутреннее соединение массивов данных age and gender осуществляет
слияние данных в кортежи по значениям в поле ?person. присутствующем в обоих массивах
160
Часть I, Уровень пакетной обработки
В специальных языках вроде соединения выражаются явно, а в^азсак^ -
неявно и на основании имен переменных. Эти отличия приведены на рис. 7.6.
Язык
Запрос
Описание
SQL
SELECT AGE.person, AGE.age, GENDER.gender
FROM AG E
INNER JOIN GENDER
ON AGE. per son = GENDER, person
В этом операторе явно указы-
вается условие соединения
JCascalog
new Subquery("?person", "?age", "?gender")
.predicate(AGE, "?person", "?age")
.predicate(GENDER, "?person", "Tgender);
Указывая имени поля ?регзоп
[ для обоих массивов данных,
] иСавсаІод неявно осуществ¬
ляет их соединение по общему
имени
Рис. 7.6. Сравнение синтаксиса SQL и JCascalog для внутреннего
соединения массивов данных age и gender
Соединения осуществляются в JCascalog таким же образом, как и в конвей¬
ерных схемах: массивы кортежей соединяются по именам общих полей, исполь¬
зуемых в качестве ключа соединения. Так, в запросе, приведенном на рис. 7.6,
имя общего поля ?person служит в качестве выходных данных для двух разных
предикатов генераторов AGE и GENDER. Каждый экземпляр переменной должен
иметь одинаковое значение для любых результирующих кортежей, и благодаря
этому JCascalog становится известно, что для правильного разрешения запроса
следует осуществить внутреннее соединение массивов данных AGE и GENDER.
Внутренние соединения лишь порождают кортежи по полям, имеющимся по обе
стороны соединения. Но иногда требуется получить результаты для записей, отсут¬
ствующих в одном из массивов данных, а это может привести к появлению пустого
значения для несуществующих данных. Такие операции называют внешними соедине-
ниямиу и выполняются они в JCascalog так же просто, как и внутренние соединения.
Некоторые примеры внешних соединений приведены на рис. 7.7.
Как упоминалось ранее, для внешних соединений в JCascalog служат поля,
имена которых начинаются со знаков !!, чтобы сформировать пустые значения
для несуществующих данных. В результирующий массив левого внешнего соеди¬
нения должен быть включен возраст лица, а для отсутствующих данных о поле —
пустые значения. А в результаты полного внешнего соединения включаются все
лица, присутствующие в обоих соединяемых массивах, причем пустые значения
заполняют любые отсутствующие данные о возрасте или поле.
Помимо соединений, имеются и некоторые другие способы объединения мас¬
сивов данных. Иногда требуется слияние в одно целое двух массивов, содержа¬
щих однотипные данные. Для этой цели в JCascalog предоставляются функции
combine () и union (). В частности, функция combine () соединяет вместе массивы
данных, тогда как функция union () удаляет любые дубликаты записей в процессе
объединения. Отличия в обеих функциях иллюстрируются на рис. 7.8.
Глава 7. Иллюстрация уровня пакетной обработки
161
Вид
соединения
Запрос
Результаты
Полное new Subquery(n?person", "!!age", "!(gender”)
внешнее .predicate(AGE, "?person", "Mage")
соединение .predicate(GENDER, "?person", "!(gender);
Левое
внешнее
new Subquery("?personn, ”?age", "!(gender")
.predicate(AGE, "?person", ”?age")
?name
?age
?gender
соединение
.predicate(GENDER, n?personM, ”!(gender);
"bob"
33
"m"
"chrls"
40
"m"
"davkT
25
null
"Jim”
32
null
?name
?age
?gender
"alice"
null
T
"bob"
33
"m"
"Chris"
40
”m"
"david"
25
null
"emily"
null
T
"Jim"
32
null
Рис. 7.7. Запросы, составленные в Jcascalog/для реализации двух видов внешних соединений
между массивами данных age и gender
Рис. 7.8. Для слияния сходных массивов данных в JCascalog предоставляются две разные функции,
combine () и union (). Первая из них выполняет простое агрегирование двух массивов,
тогда как вторая удаляет любые дублирующиеся кортежи
162
Часть /. Уровень пакетной обработки
До сих пор мы рассматривали преобразования, по очереди воздействующие
на кортежи или соединяющие вместе массивы данных. А далее будут представле¬
ны операции, предназначенные для обработки групп кортежей.
7.3.4. Группировка и агрегирование
Имеется немало типов запросов, где требуется агрегирование информации
из конкретных групп, чтобы ответить, например, на следующие вопросы: какова
средняя зарплата представителей разных профессий или какая возрастная груп¬
па пользователей чаще всего публикует сообщения в социальной сети Tweeter?
На языке SQL можно явно указать, каким образом должны быть сгруппированы
записи и выполнены операции над результирующими массивами данных.
В JCascalog отсутствует команда GROUP BY, позволяющая обозначить поря¬
док разделения кортежей для целей агрегирования. Вместо этого группировка,
как и в соединениях, неявно основывается на требующемся результате запроса.
Чтобы продемонстрировать эту особенность JCascalog, рассмотрим пару приме¬
ров. В первом примере агрегатор Count используется для выяснения количества
людей, подписчиком на сообщения которых является каждый пользователь:
Знак подчеркивания
предписывает JCascalog
проигнорировать
данное поле
new Subquery("?person", "?count") <
—о .predicate(FOLLOWS, "?person",
.predicate(new Count(), "?count"); <ь
Имена выходных
полей определяют
все потенциальные
группировки
При агрегировании
_ выходных полей
подразумевается,
что кортежи должны
быть сгруппированы
по полю ?регзоп
При выполнении предиката count из объявленного результата запроса
JCascalog заключает, что сначала должна быть выполнена группировка по полю
?person. Второй пример запроса похож на первый, но в нем выполняются дру¬
гие операции перед применением агрегатора, как показано ниже.
В этом запросе
кортежи группируются
по полю ?gender
Затем кортежи
фильтруются
по полю ?аде
new Subquery("?gendern, "?count")
.predicate(GENDER, "?person", "?gender")
.predicate(AGE, "?person", "?age") <
Перед агрегированием
массивы данных age
И GENDER СОеДИНЯЮТСЯ
.predicate(new LT(), "?age", 30) <
.predicate(new Count(), "?count");
Несмотря на то что поля
?person и ?age используются
в предыдущих предикатах,
они отвергаются агрегатором,
так как они не включены
в указанный результат
После соединения массивов данных AGE и GENDER в JCascalog отбираются все
пользователи от 30 лет и старше. В этот момент кортежи группируются по полу
и к ним применяется агрегатор Count.
Фактически в JCascalog поддерживаются три типа агрегаторов: собственно
агрегаторы, буферы и параллельные агрегаторы. Здесь они лишь упоминаются, а бо¬
лее подробно их отличия будут обсуждаться в разделе 7.3.6, при рассмотрении
особенностей реализации специальных предикатов.
163
Глава 7. Иллюстрация уровня пакетной обработки
Итак, мы подробно остановились на разных типах предикатов в JCascalog. А те¬
перь перейдем к выполнению запросов, чтобы показать, каким образом осуществля¬
ется манипулирование массивами кортежей на разных стадиях вычисления запроса.
7.3.5. Пошаговое выполнение примера запроса
Начнем рассмотрение данного примера с двух тестовых массивов данных,
приведенных на рис. 7.9.
VAL1
?а
?ь
V
1
"Ь"
2
V
5
"(Г
12
*d”
1
VAL2
?а
?с
V
4
"Ь"
6
V
3
"d"
15
Рис. 7.9. Тестовые массивы данных
для пошагового выполнения примера запроса
Воспользуемся следующим запросом, чтобы пояснить особенности выполне¬
ния запросов в 5Савса1од, наблюдая за изменениями в массивах кортежей на ка¬
ждой стадии процесса выполнения:
Генераторы
для тестовых
массивов
данных
Несколько
агрегаторов
.predicate(new Multiply(), 2, "?avg").out("?double-avg") <
.predicate (new LT(), "?double-avg", 50); Предикаты,следующие
после агрегатора
new Subquery ("?a", "?avg")
—1> .predicate (VAL1, "7a", "?b")
.predicate(VAL2, "?a", "?c")
.predicate (new Multiply(), 2, ”?b”).out("?double-b")
.predicate(new LT(), "?b\ "?c")
—> .predicate(new Count(), "?count")
.predicate(new Sum(), "?double-b").out("?sum")
.predicate(new Div(), "?sum", "?count").out("?avg")
Функция и фильтр,
предшествующие
агрегатору
В начале каждого запроса в ^авсак^ массивы данных для генераторов на*
ходятся в независимых ветвях вычисления. На первой стадии вычисления
в ^азсак^ применяются функции, фильтруются кортежи и соединяются масси¬
вы данных до тех пор, пока это возможно. Функцию или фильтр можно приме¬
нить, если все входные переменные доступны для операции. Эта стадия выпол¬
нения запроса показана на рис. 7.10.
Обратите внимание на то, что для применения одних предикатов требуется
сначала применить другие предикаты. В данном примере фильтр по критерию
“меньше” нельзя применить до тех пор, пока не будет осуществлено соединение.
В конечном итоге эта стадия достигает такого момента, когда для примене¬
ния больше не остается ни одного предиката, поскольку остальные предикаты
являются агрегаторами или же требуют переменных, которые еще недоступны.
И в этот момент]Сыса\о% переходит к стадии агрегирования в процессе выпол¬
нения запроса.
164
Часть /. Уровень пакетной обработки
Рис. 7.10. Первая стадия выполнения запроса охватывает применение
всех функций, фильтров и соединений там, где доступны входные переменные
Сначала кортежи группируются в ^аэсак^ по любым доступным перемен¬
ным, объявленным в качестве выходных для запроса, а затем к каждой группе
кортежей применяются агрегаторы. Эта стадия процесса выполнения запроса
иллюстрируется на рис. 7.11.
После стадии агрегирования выполняются все оставшиеся функции. В кон¬
це этой стадии из кортежей исключаются любые переменные, не объявленные
в выходных полях для запроса.
Глава 7. Иллюстрация уровня пакетной обработки
165
Рис. 7.11. Стадии агрегирования и после агрегирования в процессе выполнения запроса.
Сначала кортежи группируются по требующимся выходным переменным,
а затем применяются все агрегаторы. После этого выполняются
все остальные предикаты и возвращается требующийся результат
Итак, мы показали, как пользоваться предикатами для составления запросов
произвольной сложности, где данные фильтруются, соединяются, преобразуются
и агрегируются. Мы также пояснили, каким образом в^авсак^ реализуется каждая
операция из конвейерных схем и обеспечивается краткий способ обозначения кон¬
вейерной схемы. Далее мы продемонстрируем, как специальные фильтры, функции
и агрегаторы реализуются для применения в качестве предикатов а|Са5са1о£
166
Часть /. Уровень пакетной обработки
Многословное пояснение
Возможно, вы обратили внимание на то, что в рассматриваемом здесь примере сред¬
нее значение вычисляется путем подсчета, суммирования и деления. Это сделано специ¬
ально в целях иллюстрации. Подобные операции могут быть абстрагированы в агрега¬
тор Average, как было показано ранее в этой главе.
Вероятно, вы обратили внимание и на то, что некоторые переменные, указанные после
точки, вообще не используются, хотя они и остаются в результирующих массивах кор¬
тежей. Например, переменная ?Ъ не употребляется после применения предиката LT, но
по-прежнему группируется с остальными переменными. В действительности JCascalog
исключит любые переменные, как только в них отпадет необходимость, чтобы не сери-
ализировать и не пересылать их по сети. Как упоминалось в предыдущей главе, подоб¬
ную оптимизацию можно применить к любой конвейерной схеме.
7.3.6. Специальные предикатные операции
Нередко возникает потребность в создании дополнительных типов предика-
тов для реализации специальной бизнес-логики. С этой целью в JCascalog пре¬
доставляются простые интерфейсы для определения новых фильтров, функций
и агрегаторов. Но еще важнее, что все это делается в обычном коде Java, реали¬
зующем соответствующие интерфейсы.
ФИЛЬТРЫ
Начнем с фильтров. Для предиката фильтра требуется единственный метод
isKeepO, возвращающий логическое значение true, если входной кортеж дол¬
жен быть сохранен, или же логическое значение false, если он должен быть
отсеян. В следующем примере кода демонстрируется фильтр, сохраняющий все
кортежи, где входное значение больше 10:
public static class GreaterThanTenFilter extends CascalogFilter
public boolean isKeep(FlowProcess process, FilterCall call) {
return call.getArguments().getlnteger(0) >10; <
}
Получает первый элемент входного
кортежа, интерпретируя
его значение как целое
ФУНКЦИИ
Далее следует черед функций. Аналогично фильтрам, предикат функции реа¬
лизует единственный метод — в данном случае он называется operate (). Функция
принимает ряд входных данных и порождает от нуля и больше кортежей в каче¬
стве выходных данных. Ниже приведен пример простой функции, увеличиваю¬
щей свое входное значение на единицу.
Получает
значение
из входного
кортежа
public static class IncrementFunction extends CascalogFunction {
public void operate(FlowProcess process, FunctionCall call) {
}
int v = call.getArguments().getlnteger(0);
call.getOutputCollector () .add(new Tuple (v + 1)) ;
Порождает новый
кортеж
с приращенным
значением
167
Глава 7. Иллюстрация уровня пакетной обработки
На рис. 7.12 демонстрируется процесс применения данной функции к массиву
кортежей.
predicate(new IncrementFunction() "’b"
.out ("Тс")
?а
?Ь
?с
"а"
1
2
"Ь"
4
5
"а"
1
2
Рис. 7.12. Применение предиката функции іпсгетепіГипсііопО
к некоторым избранным кортежам
Как упоминалось ранее, функция может действовать в качестве фильтра, если
она не порождает ни одного кортежа для заданного кортежа. В качестве приме¬
ра ниже приведена функция, в которой предпринимается попытка извлечь из
символьной строки целое число путем синтаксического анализа или отсеять кор¬
теж при неудачном исходе такого анализа.
Интерпретирует
входное значение
как символьную
строку
public static class TryParselnteger extends CascalogFunction {
public void operate(FlowProcess process, FunctionCall call) (
—> String s = call.getArguments().getString(0);
try {
int i = Integer .parselnt (s) ;
call.getOutputCollector () .add(new Tuple (i)); <
Порождает
целое значение
при удачном исходе
синтаксического
анализа
catch(NumberFormatException e) {} <y
}
Ничего не порождает
при неудачном исходе
синтаксического анализа
На рис. 7.13 демонстрируется процесс применения данной функции к массиву
кортежей. Как видите, в ходе этого процесса отбирается один кортеж.
I.predicate(new TryParselnteger()
?а
?Ь
?с
"2"
4
2
"3"
1
3
Рис. 7.13. Функция ТгуРагэеШ:едег () отсеивает строки,
где содержимое поля ?а нельзя преобразовать в целое значение
И наконец, если функция порождает несколько выходных кортежей, то каж
дый такой кортеж присоединяется к собственной копии входных аргументов.
Ниже приведена функция Split () из примера подсчета слов.
Порождает
каждое слово
в виде отдельного
кортежа
public static class Split extends CascalogFunction (
public void operate(FlowProcess process, FunctionCall call)
String sentence = call.getArguments () .getString (0);
for(String word: sentence.split (" ")) f o
}
call.getOutputCollector () .add(new Tuple (word)) ;
Ради простоты
слова отделяются
единственным
пробелом
168
Часть L Уровень пакетной обработки
На рис. 7.14 демонстрируется процесс применения данной функции к ряду
предложений. Как видите, каждое входное предложение дублируется для каждо¬
го слова, которое оно содержит.
?а
?b
?doub le-b
?c -
"Ьи
2
4
4
"Ь"
2
4
6
"d"
12
24
15
"d"
1
2
15
?a
?b
?double-b
?c
Г V
1
2
4
4 !
2
4
e !
О На стадии агреги¬
рования JCascalocj
группирует кортежи
по выходным пере¬
менным объявлен
ным с запросе
Рис. 7.14. Функция Split () способна породить несколько кортежей из одного входного кортежа
АГРЕГАТОРЫ
К последней категории специально настраиваемых предикатных операций
относятся агрегаторы. Как упоминалось ранее, имеются три типа агрегаторов,
каждый из которых обладает разными свойствами в отношении композиции
и производительности.
Вполне очевидно, что первый тип агрегаторов так и называется — агрегато¬
ром. Агрегатор просматривает по очереди каждый кортеж в группе, корректируя
свое внутреннее состояние под каждый наблюдаемый кортеж. В качестве приме¬
ра ниже приведена реализация операции суммирования в виде агрегатора.
public static class SumAggregator extends CascalogAggregator {
public void start(FlowProcess process, AggregatorCall call) {
> call.setContext(0);
}
Инициализирует
внутреннее
состояние
агрегатора
Вызывается
для каждого
кортежа;
обновляет свое
внутреннее
состояние для
хранения текущей
суммы
public void aggregate(FlowProcess process, AggregatorCall call) {
int total = (Integer) call.getContext();
> call.setContext (total + call.getArguments().getlnteger(0));
}
public void complete(FlowProcess process, AggregatorCall call) {
int total = (Integer) call.getContext ();
call.getOutputCollector().add(new Tuple(total)); <
) Порождает кортеж с конечным
результатом, как только все
кортежи будут обработаны
Следующий тип агрегаторов называется буфером. Буфер получает итератор
для обхода всего массива кортежей в группе. В качестве примера ниже приведе¬
на реализация операции суммирования в виде буфера.
public static class SumBuffer extends CascalogBuffer {
public void operate(FlowProcess process, BufferCall call) {
Iterator<TupleEntry> it = call.getArgumentsIterator(); vH
int total = 0;
while(it.hasNext()) {
TupleEntry t = it.next();
total+=t.getlnteger(0);
}
Массив кортежей
доступен через
итератор
Глава 7. Иллюстрация уровня пакетной обработки 169
call.getOutputCollector () .add(new Tuple (total)); <j
) Единственная функция обходит
) все кортежи в массиве
и порождает выходной кортеж
Написать буфера проще, чем агрегаторы, поскольку для этого достаточно ре¬
ализовать один, а не три метода. Но, в отличие от буферов, агрегаторы можно
связывать в цепочку при составлении запроса. Связывание в цепочку означает, что
над одной и гой же группой можно выполнить сразу несколько операций агреги¬
рования. Буфера нельзя применять вместе с другими типами агрегаторов, тогда
как одни агреіаторьі допускается применять вместе с другими агрегаторами.
В контексте каркаса MapReduce буфера и агрегаторы опираются на функции
консолидации для выполнения конкретных вычислений в соответствующих пре¬
дикатных операциях. На рис. 7.15 наглядно показано, как это происходит в дей¬
ствительности.
Рис. 7.15. Выполнение агрегатора и буфера суммирования на уровне MapReduce
JCascalog укладывает как можно больше операций в задачи предварительной
обработки и консолидации, но операции агрегирования выполняются только
функциями консолидации. А это требует интенсивного обмена данными по сети,
поскольку все данные для вычисления должны направляться от функций пред¬
варительной обработки к функциям консолидации. А если имеется лишь одна
группа (например, при подсчете количества кортежей в массиве данных), то все
кортежи придется пересылать одной функции консолидации для агрегирования,
полностью нивелируя назначение системы параллельных вычислений.
Правда, последний тип операций агрегирования допускает более масштаби¬
руемое и эффективное их выполнение. Такие агрегаторы аналогичны объединяю¬
щим агрегаторам из конвейерных схем, хотя в JCascalog они называются параллель¬
ными агрегаторами. Параллельный агрегатор выполняет агрегирование по частям
в задачах предварительной обработки.
На рис. 7.16 демонстрируется разделение задания по суммированию, если
оно реализовано в виде параллельного агрегатора. Далеко не всякий агрегатор
может быть реализован как параллельный, но если такая возможность все же
имеется, то она позволяет добиться заметного повышения производительности,
избавляя от ввода-вывода данных в сеть.
170
Часть I. Уровень пакетной обработки
Рис. 7.16. Выполнение параллельного суммирующего агрегатора на уровне MapReduce
Чтобы написать собственный параллельный агрегатор, необходимо реализо¬
вать две функции.
■ Функцию init (), предварительно выполняющую частичное агрегирование
аргументов одного кортежа.
■ Функцию combine (), определяющую порядок объединения двух результа¬
тов частичного агрегирования в единое агрегированное значение.
В следующем фрагменте кода операция суммирования реализуется в виде па¬
раллельного агрегатора:
При суммировании
результатом
частичного
агрегирования
является всего
лишь значение
аргумента
public static class SumParallel implements ParallelAgg {
public void prepare(FlowProcess process, OperationCall call) {}
public List<Object> init(List<Object> input) {
-t> return input;
public List<Object> combine(List<Object> inputl,
List<Object> input2) {
int vail = (Integer) inputl.get(0);
int val2 = (Integer) input2.get(0);
return Arrays .asList ((Object) (vail + val2));
Чтобы объединить
два результата
частичного
агрегирования,
достаточно
просуммировать
значения
Параллельные агрегаторы можно связывать в цепочку с друг'ими параллель¬
ными или обычными агрегаторами. Но если связать параллельные агрегаторы
в цепочку с обычными агрегаторами, то они не смогут выполнять частичное
агрегирование в задачах предварительной обработки и будут действовать как
обычные агрегаторы.
Итак, мы рассмотрели все абстракции, составляющие подзапросы в5Са$са1о§\
в том числе предикаты, функции, фильтры и агрегаторы. Истинный потенциал
этих абстракций кроется в том, как они поддаются повторному использованию
и композиции. А теперь перейдем к рассмотрению различных методов компози¬
ции, допускаемых в ^аяса^.
171
Глава 7. Иллюстрация уровня пакетной обработки
7.4. Композиция
Обсуждая вопросы консолидации к минимуму второстепенной сложности
в коде обрабоїки данных, мы подчеркнули, что абстракции должны поддавать¬
ся композиции для внедрения новых и лучших функциональных возможностей.
Именно такой подход и преобладает в ЗСавсаЦг.
В этом разделе мы рассмотрим методы композиции абстракций путем объе¬
динения подзапросов, предикатных макрокоманд и функций для динамическо¬
го составления как подзапросов, так и макрокоманд. В этих методах выгодно
используется отсутствие видимых ограничений между инструментальным сред¬
ством составления запросов и языком программирования общего назначения,
позволяя очень точно манипулировать запросами. В них также используются
преимущества невероятно однородной структуры ^азса^, где все представле¬
но в виде одинаково указываемых предикатов. Это свойство открывает возмож¬
ности для применения эффективных методов композиции, характерных только
для Л^авсак^.
7.4.1. Объединение подзапросов
Подзапросы являются основными единицами абстракции в JCascalog, по¬
скольку они дают произвольное представление любого количества источников
данных. Одно из наиболее примечательных свойств подзапросов состоит в том,
что к ним можно обращаться как к источникам других запросов. Аналогично раз¬
биению крупной программы на многие функции, это дает возможность раскла¬
дывать крупные запросы на составляющие.
Рассмотрим следующий пример поиска всех записей в массиве данных
FOLLOWS, где каждый зарегистрированный в записи человек является подписчи¬
ком на сообщения больше чем двух других людей:
В первом подзапросе определяются
все люди, являющиеся подписчиками
на сообщения больше чем двух
других людей
Учитывает
только
подписчика,
а не источник
-о
Подсчитывает
количество людей,
являющихся
подписчиками
на сообщения
каждого
пользователя,
и сохраняет тех,
у кого подсчет
больше 2
Subquery many Follows = new Subquery ("?person")
.predicate(FOLLOWS, "?person",
.predicate(new Count(), "?count")
o .predicate (new GT(), M?count", 2);
Api.execute(new StdoutTap(),
new Subquery("?personl", "?person2”)
.predicate(manyFollows, "?personl")
.predicate(manyFollows, "?person2")
.predicate(FOLLOWS, "?personl\ "?person2"));
Использует результаты
первого подзапроса
в качестве источника
для этого подзапроса
Подзапросы выполняются по требованию — ничего не вычисляется до тех
пор, пока не будет вызван метод Api. execute (). В предыдущем примере ни одно
из заданий MapReduce не запускалось на выполнение до тех пор, пока не вызы¬
вался метод Api • execute (), несмотря на то, что первым был определен нодза-
прос manyFollows.
172
Часть L Уровень пакетной обработки
Ниже приведен еще один пример запроса, где требуется несколько подзапро¬
сов. Этот запрос расширяет подсчет слов для определения количества слов, су¬
ществующих для каждого вычисленного значения подсчета слов.
I Основной подзапрос
Subquery wordCount = new Subquery ("?word", "? count") < * на подсчет слов
.predicate(SENTENCE, "?sentence")
.predicate(new Split(), "?sentence").out("?word")
.predicate(new Count(), "?count");
Api.execute (new StdoutTap(),
new Subquery("?count", "?num-words")
.predicate(wordCount, "?count") <3
.predicate (new Count (), "?num-words")); <-i
Второй запрос, требующий
только подсчет каждого слова
Определяет количество
слов для каждого
значения подсчета
Объединение подзапросов является эффективным методом выражения слож¬
ных операций с помощью простых составляющих. И такая эффективность стала
возможной благодаря тому, что функции способны составлять подзапросы непо¬
средственно, как поясняется далее.
7.4.2. Динамическое создание подзапросов
Один из наиболее распространенных методов, пригодных для применения
в JCascalog, состоит в написании функций, динамически создающих подзапро¬
сы. Это означает, что для составления подзапроса в соответствии с некоторыми
параметрами достаточно написать обычный код на Java. Ранее были продемон¬
стрированы преимущества применения одних подзапросов в качестве источни¬
ков данных для других подзапросов, а их составление в динамическом режиме
делает эти преимущества еще более доступными.
Допустим, в системе HDFS имеются файлы с данными транзакций: идентифи¬
катором покупателя, идентификатором продавца, отметкой времени и денежной
суммой. Эти данные представлены в формате JSON следующим образом:
{"buyer": 123, "seller": 456, "amt": 50, "timestamp": 1322401523}
{"buyer": 1009, "seller": 12, "amt": 987, "timestamp": 1341401523}
{"buyer": 2, "seller": 98, "amt": 12, "timestamp": 1343401523}
Над этими данными можно выполнить самые разные вычисления, но для об¬
работки каждого из запросов потребуется выполнить синтаксический анализ
данных, вводимых из текстовых файлов. С этой целью полезно создать служеб¬
ную функцию, принимающую путь к файлу в системе НБР8 и возвращающую под¬
запрос на синтаксический анализ данных в данном файле, как показано ниже.
Внешняя
библиотека
преобразует
данные
формата
в отображение
Требующиеся
значения
переносятся
из отображения
в единый кортеж
В подзапросе требуется функция Cascalog
для выполнения синтаксического анализа
public static class ParseTransactionRecord extends CascalogFunction {
public void operate(FlowProcess process, FunctionCall call) {
String line = call, get Arguments () .getString (0);
> Map parsed = (Map) JSONValue.parse(line);
-ч> call.getOutputCollector().add(new Tuple(parsed.get("buyer"),
parsed.get("seller"),
parsed.get("amt"),
parsed.get("timestamp")));
173
Глава 7. Иллюстрация уровня пакетной обработки
)
) Обычная функция Java составляет
запрос в динамическом режиме
public static Subquery parseTransactionData (String path) {<
return new Subquery ("?buyer", "?seller", "?amt", M?timestamp")
1> .predicate(Api.hfsTextline(path), "?line")
.predicate(new ParseTransactionRecord(), "?line") <
^ .out("?buyer", "?seller", "?amt", "?timestamp");
Вызывает специальную функцию
для синтаксического анализа
данных в формате JSON
Как только эта абстракция будет определена, ею можно воспользоваться
для составления любого запроса к массиву данных. В качестве примера ниже
приведен запрос на подсчет количества транзакций каждого покупателя.
public static Subquery buyerNumTrans act ions (String path) {
return new Subquery ("?buyer", "?count")
.predicate (parseTransactionData(path), "?buyer", "_") <—
.predicate (new Count(), "?count"); ~ Отвергает все поля,
^ кроме поля buyer
Это очень простой пример создания подзапросов в динамическом режиме,
но он наглядно показывает, каким образом их можно составить вместе, чтобы
абстрагировать части более сложных вычислений. Рассмотрим еще один при¬
мер, в котором количество предикатов в подзапросе динамически определяется
числом аргументов.
Допустим, имеется массив данных с процитированными сообщениями в со¬
циальной сети Tweeter, где каждая запись обозначает цитирование какого-то
другого сообщения, и требуется найти все цепочки цитирований определенной
длины. Это означает, что для цепочки длиной 4 требуется выяснить все три по¬
следовательные цитирования исходного сообщения.
Исходный массив данных состоит из пар идентификаторов сообщений
в Tweeter. Следует иметь в виду, что эти пары можно преобразовать в цепочки
длиной 3, соединив массив данных с самим собой. Аналогично можно найти
цепочки длиной 4, соединив цепочки длиной 3 с парами исходных сообщений.
В качестве примера ниже приведен запрос, возвращающий цепочки длиной 3,
если задан входной генератор пар.
public static Subquery chainsLength3(Object pairs) {
return new Subquery ("7a", "?b", "?c")
.predicate(pairs, "?a", "?b")
.predicate(pairs, "?b", "?c");
формирует отвод из
предоставляемого
пути к файлу
в системе HDFS
Дополнительное соединение позволяет обнаружить все цепочки длиной 4
следующим образом:
public static Subquery chainsLength4(Object pairs) {
return new Subquery("?a", "?b", "?c", "?d")
.predicate(pairs, "?a", "?b")
.predicate(pairs, "?b", "?c")
.predicate(pairs, "?c", "?d");
174
Часть I. Уровень пакетной обработки
Чтобы обобщить этот процесс для обнаружения цепочек любой ДЛИНЫ, По¬
требуется функция, составляющая подзапрос с нужным количеством предикатов
и переменных. Такую функцию можно реализовать, написав на Java следующий
довольно простой код:
public static Subquery chainsLengthN(Object pairs, int n)
List<String> genVars = new ArrayList<String>();
for(int i=0; i<n; i++) {
genVars. add (Api. genNullableVar ()); <]
}
Subquery ret = new Subquery (genVars);
for(int i=0; i<n-l; i++) {
Генерирует однозначные
выходные переменные,
допускающие пустые значения
ret - ret .predicate(pairs, genVars.get(i), genVars.get(i+1));
return ret;
}
Определяет
в цикле требующееся
количество соединений
Любопытно отметить, что данная функция пригодна не только для обработки
данных цитируемых сообщений. Она может принимать любой подзапрос или источ¬
ник данных, содержащий пары, и возвращать подзапрос на вычисление цепочек.
Рассмотрим еще один пример подзапроса, создаваемого в динамическом ре¬
жиме. Допустим, что из массива данных неизвестного размера требуется извлечь
произвольную выборку N элементов. Это проще всего сделать распределенным
и масштабируемым способом, применив следующий алгоритм.
1. Сгенерировать случайное число для каждого элемента.
2. Выявить N элементов с наименьшими случайными числами.
В JCascalog имеется встроенный агрегатор Limit для выполнения второго эта¬
па данного алгоритма. В агрегаторе Limit применяется такая же стратегия, как
и в параллельных агрегаторах, где в каждой задаче предварительной обработки
сначала обнаруживаются наименьшие N элементов, а затем результаты, получен¬
ные из всех задач предварительной обработки, объединяются для выявления об¬
щего количества наименьших N элементов. Эта стратегия реализуется в следую¬
щем фрагменте кода для извлечения произвольной выборки из массива данных:
Просматривает входной массив данных, чтобы
определить нужное количество входных и выходных полей
Формирует
отдельные
поля для входных
и выходных
переменных
public static Subquery fixedRandomSample (Object data, int n) {
List<String> inputVars = new ArrayList<String>();
List<String> outputVars = new ArrayList<String>() ;
for (int i=0; i < Api.numOutFields (data); i++) { <
£> inputVars. add (Api. genNullableVar ()) ;
outputVars.add(Api.genNullableVar());
Использует функцию
ВапбЬопдО
из .1Са8са1о£
для присоединения
каждого входного
кортежа со случайным
значением
}
String randVar = Арі.genNullableVar () ;
Создает отдельное поле
для хранения случайных
значений
return new Subquery (outputVars)
.predicate(data, inputVars)
—[>.predicate (new RandLongO, randVar)
.predicate(Option.SORT, randVar)
Выполняет вторичную
сортировку по случайным
значениям
.predicate(new Limit(n), inputVars).out(outputVars),
і
Использует функцию ИапбЬопд () из ЗСаБса1об для присоединения
каждого входного кортежа со случайным значением
175
Глава 7. Иллюстрация уровня пакетной обработки
Этот алгоритм довольно прост. Он распараллеливает вычисление фиксирован¬
ной выборки, не требуя сосредоточения всех записей в одном центральном месте.
При составлении запросов в ДОадсак^ можно заметить, что некоторые пре¬
дикаты часто употребляются вместе. В подобных случаях проще и эффективнее
выразить совокупные функциональные возможности в одной операции. Поэтому
далее мы поясним, каким образом такая возможность поддерживается в^авсак^
с помощью предикатных макрокоманд.
7.4.3. Предикатные макрокоманды
Предикатная макрокоманда является операцией, которая расширяется
eJCascalog до другого ряда предикатов. Все операции представлены в JCascalog
в виде предикатов, и поэтому предикатные макрокоманды позволяют создавать
эффективные абстракции из составляемых вместе предикатов, будь то агрегато¬
ры, фильтры или функции.
В начале этой главы уже рассматривался пример предикатной макрокоманды
при определении агрегатора Average. Приведем это определение еще раз:
Предикатная
макрокоманда
возвращает
единственный
результат
Во временных
переменных
хранятся
результаты
агрегирования
Использует шаблон для определения предикатной
макрокоманды с входной переменной
PredicateMacroTemplate Average =
PredicateMacroTemplate.build("?val") <
1> .out("?avg")
.predicate(new Count(), "?count") <
Макрокоманда Average
расширяется до трех
предикатов: count,
sum и div
-о .predicate (new Sum(), "?val") .out ("?sum")
.predicate(new Div(), "?sum", "?count").out("?avg"); <—
Делит результаты агрегирования
для вычисления окончательного результата
Макрокоманда Average состоит из следующих трех предикатов: агрегирова¬
ния подсчета, агрегирования суммы и функции деления. На рис. 7.17 показано,
каким образом вызывается и в итоге расширяется макрокоманда Average.
new Subquery("?result")
.predicate(INTEGER, "?n")
.predicate(Average, ”?n).out("?result");
I
new Subquery("?result")
.predicate(INTEGER, "?nu)
.predicate(new Count(), "?count_genl")
.predicate(new Sum(), "?n).out("?sum_gen2")
.predicate(new Div(), "?sum_gen2", "?count_genl")
.out("?result");
Пример исходного кода,
в котором используется
предикатная макро¬
команда Average
ХаБса1од внутренним
образом расширяет
макрокоманду на состав¬
ляющие ее предикаты,
используя отливающиеся
имена полей, чтобы
избежать конфликта
с окружающим подзапросом
Рис. 7.17. Предикатные макрокоманды обеспечивают эффективные абстракции для составления
простых запросов, автоматически расширяемых в Хаэса^ до составляющих предикатов
В определении макрокоманды Average применяется шаблон JCascalog для ука¬
зания тех предикатов, до которых эта конкретная предикатная макрокоманда
176
Часть I. Уровень пакетной обработки
должна быть расширена. Но не все можно указать с помощью шаблона. Допустим,
требуется создать предикатную макрокоманду, вычисляющую количество отдель¬
ных значений для заданного ряда переменных:
new Subquery ("?distinct-followers-countn)
.predicate(FOLLOWS, "?person", " ")
.predicate(new DistinctCount(), "?person")
.out("?distinct-followers-count");
Требующаяся макрокоманда
подсчитывает количество
отдельных подписчиков
В этом подзапросе определяется количество отдельных пользователей, под¬
писавшихся на сообщения хотя бы одного лица. В отличие от вычисления
среднего значения отдельной переменной, молено сделать отдельные подсчеты
для массива переменных любого размера. Но для этого нельзя воспользоваться
шаблонами, поскольку в них поддерживаются только фиксированные массивы
входных и выходных переменных. Поэтому рассмотрим другой способ опреде¬
ления предикатных макрокоманд для достижения подобных функциональных
возможностей.
Прежде всего необходимо определить агрегатор, выполняющий конкретное
вычисление. Этот агрегатор должен действовать даже в том случае, если количе¬
ство кортежей настолько велико для группы, что не может содержаться в опера¬
тивной памяти. Чтобы разрешить это затруднение, можно воспользоваться сред¬
ством, называемым вторичной сортировкой и позволяющим отсортировать группу
кортежей перед их обработкой агрегатором. После сортировки агрегатор лишь
инкрементирует отдельный подсчет, если текущий кортеж отличается от своего
предшественника.
Код для выполнения агрегирования выглядит следующим образом:
Инициализирует
отслеживаемое
состояние для
каждой группы
public static class DistinctCountAgg extends CascalogAggregator
static class State { <-
int count = 0;
Tuple last = null;
Внутреннее состояние для
отслеживания текущего подсчета
и рассмотренного ранее кортежа
{
}
public void start(FlowProcess process, AggregatorCall call) {
-t> call. setContext (new State ());
}
Увеличивает
отдельный
подсчет
в текущем
состоянии только
в том случае,
если текущий
кортеж
отличается
от предыдущего
public void aggregate(FlowProcess process, AggregatorCall call) {
State s = (State) call.getContext (); <1 - i получает текущее состояние
Tuple t = call .getArguments () .getTupleCopy (); | при обработке кортежа
if(s.last==null II !s.last.equals (t)) (
—1> s. count++;
}
s.last = t; <ь
}
Всегда обновляет
последний просмотренный
кортеж в состоянии
public void complete(FlowProcess process, AggregatorCall call) {
State s = (State) call.getContext ();
call.getOutputCollector () .add(new Tuple (s.count)); vi 1
^ Порождает отдельный подсчет, как только |
) будут обработаны все кортежи в группе |
177
Глава 7. Иллюстрация уровня пакетной обработки
Класс DistinctCountAgg содержит логику для отдельного подсчета на основа¬
нии отсортированных входных данных. И не удивительно, что в JCascalog име¬
ется предикат Option.SORT для указания порядка сортировки кортежей в каждой
группе. В следующем фрагменте кода демонстрируется, каким образом определя¬
ется сортировка и производится отдельный подсчет вручную:
public static Subquery distinctCountManual() {
return new Subquery("?distinct-followers-count")
.predicate (FOLLOWS, "?person", " ") Сортирует кортеж
.predicate(Option.SORT, "?person") <j по полю ?person
.predicate(new DistinctCountAgg(), "?person")
.out ("?distinct-followers-count");
Разумеется, более предпочтительной в данном случае была бы предикатная
макрокоманда, чтобы не указывать сортировку и агрератор всякий раз, когда
потребуется отдельный подсчет. Наиболее общей формой предикатной макро¬
команды является функция, принимающая списки входных и выходных полей
и возвращающая ряд предикатов. В качестве примера такой функции ниже при¬
ведено объявление класса 01зЬ1пс1:СоипЬ — обычной реализации интерфейса
РгесисаЬеМасго, определяющего предикатную макрокоманду.
public static class DistinctCount implements PredicateMacro {
public List<Predicate> getPredicates(Fields inFields,
Fields outFields) { <
Входные и выходные ПОЛЯ
определяются,
когда макрокоманда
используется в подзапросе
List<Predicate> ret = new ArrayList<Predicate> О ;
}
ret.add(new Predicate(Option.SORT, inFields)); <h
ret.add(new Predicate(new DistinctCountAgg(),
inFields,
Группы сортируются
по предоставляемым
входным ПОЛЯМ
return ret;
outFields)); <ъ
Для этой макрокоманды отдельный подсчет порождает
одно выходное поле, но в общей форме макрокоманды
поддерживается несколько выходных полей
7-4.4- Динамическое создание предикатов макрокоманд
Как было показано ранее, обычные функции, написанные на Java, позволяют
создавать подзапросы в динамическом режиме, поэтому и не удивительно, что то
же самое можно сделать и с предикатными макрокомандами. Это чрезвычайно
эффективный метод, наглядно демонстрирующий преимущества, которые дает
для обработки данных отдельная библиотека, написанная на языке программи¬
рования общего назначения.
Рассмотрим в качестве примера следующий запрос:
,, „ ,, „0 „ч I Читает массив данных,
new Subquery ( ?х , ?у / -z ) содержащий кортежи чисел
.predicate(TRIPLETS, "?а", "?b", и?с") <з
.predicate(new IncrementFunction(), "?a").out( ?x )
.predicate(new IncrementFunction(), "?b").out("?y")
.predicate(new IncrementFunction(), "?c").out( ?z ),
Возвращает новый кортеж, где
каждое поле инкрементируется
178
Часть I. Уровень пакетной обработки
И хотя это простой запрос, в нем немало повторений, поскольку в нем нуж¬
но явным образом применять функцию IncrementFunctionO к каждому полю из
входных данных. Поэтому было бы неплохо исключить такое повторение следу¬
ющим образом:
new Subquery("?х", "?у", "?z")
.predicate(TRIPLETS, "?a", "?b", "?c")
.predicate (new Each(new IncrementFunctionO), "?a", "?b", "?c")
.out("?x", "?y", "?z");
Вместо неоднократных вызовов функции IncrementFunctionO в предикатной
макрокоманде Each эта функция применяется лишь один раз к указанным вход¬
ным полям и формирует требующийся результат. Расширение этой предикатной
макрокоманды соответствует трем отдельным предикатам в первоначальном за¬
просе. Ниже приведено определение предикатной макрокоманды Each.
public static class Each implements PredicateMacro {
Object _op;
public Each(Object op) {
_op = op; о
}
В качестве параметра макрокоманды Each
служит выполняемая предикатная операция
public List<Predicate> getPredicates(Fields inFields,
Fields outFields) {
List<Predicate> ret = new ArrayList<Predicate> ();
for(int i=0; i<inFields.size(); i++) {
Object in = inFields.get (i);
Object out = outFields .get (i);
ret.add(new Predicate(_op,
Arrays.asList(in),
Arrays.asList(out))); <
Предикатная макрокоманда создает
предикат для каждой отдельной пары
входных и выходных полей
return ret;
}
Рассмотрим еще один пример динамически создаваемой предикатной макро¬
команды. Ранее мы определили собственную функцию IncrementFunctionO,
инкрементирующую свой аргумент, но на самом деле это всего лишь функция
Plus () с одним аргументом, имеющим заданное значение 1. Поэтому было бы
полезнее иметь в своем распоряжении предикатную макрокоманду, абстрагирую¬
щую частичное применение предикатной операции. И тогда операцию Increment
можно было бы определить следующим образом:
Object Increment = new Partial(new Plus(), 1) ;
Как видите, предикатная макрокоманда Partial заполняет некоторые из вход¬
ных полей, что дает возможность переписать запрос на инкрементирование сра¬
зу трех полей, аналогично приведенному ниже.
new Subquery("?х", "Ту", "?z")
.predicate(TRIPLETS, "?а", "?b", "?с")
.predicate (new Each (new Partial(new PlusO, 1)), "?a", "?b", "?c")
.out("?x", "?y", "?z");
179
Глава 7. Иллюстрация уровня пакетной обработки
После расширения
следующий вид:
всех предикатных макрокоманд этот запрос приобретает
new Subquery ("?x", "?y", "Tz")
.predicate(TRIPLETS, "7a", "7b", "?c")
.predicate(new Plus(), 1, "7a").out("?x")
.predicate(new Plus(), 1, "?b").out("?y")
.predicate(new Plus(), 1, "?c").out("?z");
Определение предикатной макрокоманды Partial выглядит довольно просто:
public static class Partial implements PredicateMacro {
Object _op;
List<Object> _args;
public Partial(Object op, Object... args) {
_op = op;
_args = Arrays.asList(args);
}
public List<Predicate> getPredicates(Fields inFields,
Fields outFields) {
List<Predicate> ret = new ArrayList<Predicate>() ;
List<Object> input = new ArrayList<Object>() ;
input.addAll(_args);
input.addAl1(inFields);
ret.add(new Predicate(_op, input, outFields));
return ret;
Эта предикатная макрокоманда просто вставляет любые предоставляемые
входные поля перед теми входными полями, которые указываются при создании
подзапроса. Как видите, динамически создаваемые макрокоманды открывают не¬
малые возможности для манипулирования процессом построения подзапросов.
Резюме
Способ представления вычислений особенно важен, если требуется избежать
сложности, предотвратить программные ошибки и повысить производитель¬
ность. К самым главным методам борьбы со сложностью относятся абстракция
и композиция, и поэтому очень важно, чтобы избранное вами инструментальное
средство обработки данных поддерживало эти методы, а не затрудняло их при¬
менение.
В двух последующих главах мы подкрепим принципы организации уровня па¬
кетной обработки его построением для приложения SuperWebAnalytics.com. Это
приложение послужит более сложным, но и правдоподобным примером, позво¬
ляющим продемонстрировать на практике сложные особенности пакетных вы¬
числений с точки зрения архитектуры, алгоритмов и реализации.
Пример построения уровня
пакетной обработки:
архитектура и алгоритмы
В этой главе...
■ Построение уровня пакетной обработки
от начала и до конца.
■ Практические примеры предварительных
вычислений.
■ Итеративные алгоритмы обхода графов.
■ Применение алгоритма Нуреги^Ь^
для выполнения эффективных операций
над мощностью множества.
Итак, мы рассмотрели все части уровня пакетной обработки: построение
схемы для хранения данных, сохранение главного массива данных и выполне¬
ние вычислений в масштабе и с минимальной сложностью. В этой главе все
эти части будут связаны вместе в согласованный уровень пакетной обработки.
Никаких новых теоретических положений в этой главе не вводится, поскольку
наша цель — подкрепить понятия, разъясненные в предыдущих главах, рассмо¬
трев построение уровня пакетной обработки от начала до конца. Это послужит
ценным примером воплощения теории на практике.
В частности, из этой главы вы узнаете, как создавать уровень пакетной об¬
работки на конкретном примере приложения SuperWebAnalytics.com. Этому до¬
статочно сложному приложению требуется довольно развитый, хотя и не такой
сложный уровень пакетной обработки, чтобы увязнуть в подробностях. У вас
будет возможность убедиться, что различные абстракции уровня пакетной обра¬
ботки аккуратно сходятся вместе, образуя довольно изящный уровень пакетной
обработки для приложения SuperWebAnalylics.com.
После краткого изложения требований к приложению SuperWebAnalytics.com
мы подробно рассмотрим, что должно быть осуществлено на уровне пакетной
182
Часть I. Уровень пакетной обработки
обработки и что следует предварительно вычислять для каждого пакетного пред¬
ставления. В этой главе будут рассмотрены архитектура и алгоритмы для уровня
пакетной обработки с использованием конвейерных схем, а в следующей гла¬
ве — их реализация в коде с помощью конкретных инструментальных средств.
Лейтмотивом всей этой главы является гибкость уровня пакетной обработки.
Мы рассмотрим процесс обработки данных лишь на трех примерах пакетных
представлений, хотя уровень пакетной обработки совсем не трудно расширить
для вычисления новых представлений. Это означает, что уровень пакетной об¬
работки внутренне готов адаптироваться к изменениям в требованиях заказчика
к приложению.
8.1. Разработка уровня пакетной обработки
приложения SuperWebAnalytics.com
Нам предстоит создать уровень пакетной обработки приложения
SuperWebAnalytics.com для поддержки обработки трех видов запросов. Напомним,
что назначение уровня пакетной обработки состоит в предварительном вычисле¬
нии представлений, чтобы обрабатывать указанные запросы с малой задержкой.
Представив запросы, поддерживаемые в приложении SuperWebAnalytics.com, мы
обсудим пакетные представления, требующиеся для ответа на эти запросы.
8.1.1. Поддерживаемые запросы
В приложении SuperWebAnalytics.com поддерживаются следующие три разно¬
видности запросов.
■ Подсчет просмотров страниц по иКЬ во времени. Примером тому слу¬
жат запросы вроде количества просмотров страниц каждый день за про¬
шедший год или за последние 12 часов.
■ Количество индивидуальных посетителей по иКЬ во времени. При¬
мером тому служат запросы наподобие количества индивидуальных посети¬
телей отдельного домена за 2010 год или каждый час за три последних года.
■ Анализ показателя отказов от просмотра страниц. Примером тому слу¬
жат запросы вроде процента посетителей страницы, не просматривавших
остальные страницы на данном веб-сайте.
Склад характера людей делает вторую разновидность запросов наиболее
трудной для обработки. Напомним, что в исполнительной схеме приложения
SuperWebAnalylics.com отдельное лицо представлено идентификатором зареги¬
стрированного пользователя или идентификатором сооЫе-файла, полученного
из браузера. Следовательно, отдельное лицо может посетить один и тот же сайт
под разными идентификаторами. С одной стороны, соо1ие-файл может изме¬
ниться, если пользователь очистит этот файл. А с другой стороны, пользователь
может зарегистрироваться с разными идентификаторами.
Подобное затруднение разрешается в схеме определением ребер эквивален¬
тов, обозначающих моменты, когда два разных идентификатора фактически
представляют одно и то же лицо. Граф эквивалентов для отдельного лица может
быть произвольно сложным, как показано на рис. 8.1. Для точной обработки
Глава 8. Пример построения уровня пакетной обработки: архитектура и алгоритмы 183
этой второй разновидности запросов требуется проанализировать данные, что-
ы определить те просмотры страниц, которые относятся к одному лицу, поль¬
зующемуся разными идентификаторами.
Лицо
(идентификатор
пользователя):
200
© У пользователей с единствен¬
ным идентификатором ребра
эквивалентов вообще нет
Просмотр
страницы
Просмотр
страницы
Лицо
(идентификатор
пользователя):
123 ^
Просмотр Просмотр
страницы страницы
Лицо
(идентификатор
пользователя):
87
Просмотр
страницы
О Лицо может получить доступ
к странице по одному и тому
же иль, пользуясь несколь¬
кими идентификаторами
Страница:
Страница:
http://foo.com/blog
http://foo.com/about
Рис. 8.1. Примеры просмотра разных страниц одним и тем же лицом,
зарегистрированным с разными идентификаторами
8.1.2. Пакетные представления
Рассмотрим далее пакетные представления, требующиеся для обработки за¬
просов. Для каждого пакетного представления очень важно соблюсти равнове¬
сие между размерами предварительно вычисляемых представлений и объемом
вычислений, динамически выполняемых во время запроса.
ПРОСМОТРЫ СТРАНИЦ ВО ВРЕМЕНИ
В рассматриваемом здесь примере приложения требуется извлекать количество
просмотров страницы по заданному иЯЬ за любой период времени со степенью
детализации до часа. Как упоминалось в главе 6, предварительно подсчитать про¬
смотры страниц за каждый возможный часовой промежуток времени в течение
года практически невозможно, поскольку для этого потребуется около 380 мил¬
лионов отдельно вычисленных значений по каждому иКЦ охваченному массивом
данных. Вместо этого можно предварительно вычислить меньшее количество про¬
смотров страниц, а остальные вычисления выполнять во время запроса.
Простейший подход заключается в том, чтобы предварительно подсчитать
количество просмотров страниц по каждому иКЬ за каждый часовой промежу¬
ток времени. В итоге получится пакетное представление, аналогичное приведен¬
ному на рис. 8.2. Чтобы разрешить запрос, достаточно извлечь предварительно
подсчитанное значение за каждый часовой промежуток в течение заданного пе¬
риода времени и просуммировать эти значения.
Но у такого подхода имеется следующий недостаток: обработка запроса замед¬
ляется по мере увеличения времени ь/х рамок. Так, для подсчета количества про¬
смотров страниц за один год потребуется извлечь из пакетного представления
около 8760 значений и сложить их вместе. А поскольку многие из этих значений
должны извлекаться с диска, то задержка обработки запросов за длинные периоды
времени может оказаться намного больше, чем за малые периоды времени.
184
Часть /. Уровень пакетной обработки
1Ж1-
Час
Количество
просмотров
страниц
foo.com/blog
2012/12/08 15:00
876
foo.com/blog
2012/12/0816:00
987
foo.com/blog
2012/12/0817:00
762
foo.com/blog
2012/12/08 18:00
413
foo.com/blog
2012/12/0819:00
1098
foo.com/blog
2012/12/08 20:00
657
foo.com/blog
2012/12/08 21:00
101
Рис. 8.2. Предварительный подсчет просмотров страниц за время с точностью до часа
Правда, этот недостаток легко устраняется. Вместо предварительного вычис¬
ления значений только с точностью до часа их можно предварительно вычис¬
лить с меньшей точностью, например, с интервалом в 1 день, неделю или месяц.
Подобное сокращение задержки в обработке запросов лучше всего продемон¬
стрировать на конкретном примере.
Допустим, требуется подсчитать количество просмотров страниц от 3 часов
утра 3 марта до 8 часов утра 17 сентября. Если, с одной стороны, произвести под¬
счет с точностью до часа, то для обработки такого запроса потребуется извлечь
и просуммировать значения для 4805 часовых промежутков времени. А если,
с другой стороны, произвести подсчет с меньшей точностью, то можно значи¬
тельно уменьшить количество извлекаемых значений. В данном случае значения
сначала извлекаются за каждый месяц в промежутке от 3 марта до 17 сентября,
а затем складываются или вычитаются за более точные промежутки времени
для получения нужного временного диапазона (рис. 8.3).
Группировка значений
по разной степени детали¬
зации по времени позволяет
сократить число их выборок
для запросов за длинные
периоды времени
Данная стратегия состоит
в том чтобы извлечь сначала
значения за длинные прома¬
жу гки времени а зат о г и ело
жить (показано горизонталь
ной штриховкой) ИЛИ ВЫЧъСТЬ
(показано наклонной штрп
хоекой) значения за более
короткие промежутки врвмеиі
и тем самыгі покрыть еесь
запрашиваемый период
времени
Рис. 8.3. Оптимизация подсчета количества просмотров страниц за длинные
запрашиваемые периоды времени благодаря вычислениям с меньшей точностью
Для обработки данного запроса потребуется извлечь лишь 26 значений, а это
означает 200-кратное улучшение! Но возникает вопрос: во что обойдется предва¬
рительное вычисление значений за интервалы в 1 день, неделю и месяц, поми¬
мо часовых промежутков? Как ни странно, это едва ли потребует каких-нибудь
Глава 8. Пример построения уровня пакетной обработки: архитектура и алгоритмы 185
затрат. На рис. 8.4 показано, сколь¬
ко промежутков времени требуется
для каждой степени детализации
по времени за период в один год.
Если сложить количество проме¬
жутков времени за 1 день, неделю
и месяц, то получится 430 допол¬
нительных значений, которые при¬
дется предварительно вычислить
по каждому URL за период в один
год. Это не только увеличит объем
предварительных вычислений всего лишь на 5%, но и сократит в 200 раз время,
требующееся на обработку запроса. Такой компромисс оказывается более чем
приемлемым!
КОЛИЧЕСТВО ИНДИВИДУАЛЬНЫХ ПОСЕТИТЕЛЕЙ ВО ВРЕМЕНИ
В следующем типе запросов определяется количество индивидуальных страниц
посетителей за указанный промежуток времени. На первый взгляд это похоже
на подсчет количества просмотров страниц во времени, но здесь имеется следу¬
ющее существенное отличие: подсчет индивидуальных посетителей не носит ад¬
дитивный характер. Если общее количество просмотров страниц за двухчасовый
период времени можно получить, сложив значения за отдельные часовые проме¬
жутки, то сделать то же самое по данному типу запросов нельзя. Дело в том, что
подсчет индивидуальных посетителей представляет размер множества элементов,
а множества для каждого часового промежутка могут перекрываться. Если просто
сложить такие подсчеты за два часа, то количество людей, посетивших страницу
по заданному URL в течение обоих часовых промежутков времени, удвоится.
Единственный способ вычислить количество индивидуальных посетителей
с идеальной точностью за любой период времени состоит в том, чтобы подсчи¬
тывать их динамически. Для этого потребуется произвольный доступ к массиву
посетителей страниц по каждому URL за каждый часовой промежуток времени.
И это вполне осуществимо, хотя и затратно, поскольку весь главный массив дан¬
ных должен быть проиндексирован. С другой стороны, можно воспользоваться
алгоритмом аппроксимации, где в жертву приносится точность ради значитель¬
ного сокращения объема данных, которые требуется проиндексировать в пакет¬
ном представлении. Характерным примером для индивидуального подсчитыва¬
ния служит алгоритм HyperLogLog. По каждому URL за часовой промежуток вре¬
мени алгоритму HyperLogLog требуется объем информации лишь около 1 Кбайт,
чтобы оценить мощности множеств (т.е. количество элементов в них) до 1 мил¬
лиарда с максимальной вероятностью ошибки 2%1.
Несмотря на всю занимательность алгоритма HyperLogLog, мы не будем от¬
влекаться на подробное изложение принципа его действия. Вместо этого станем
Степень
детализации
Количество промежутков
времени в году
Ежечасно
8760
Ежедневно
-365
Еженедельно
-52
Ежемесячно
-13
Рис. 8.4. Количество промежутков времени
за период в один год для каждой степени
детализации по времени
1 Алгоритм HyperLogLog описан в статье “Анализ алгоритма IlypcrLogLog для близкой к опти¬
мальной оценки мощности множества” (HyperLogLog: the analysis of a nearoptiinal cardinality
estimation algorithm), опубликованной коллективом авторов по адресу http://algo.inria.
fr/fla;jolet/Publications/FlFuGaMe07 .pdf.
186
Часть I. Уровень пакетной обработки
рассматривать его как некий “черный ящик”, уделив основное внимание его ин¬
терфейсу, определение которого приведено ниже.
interface HyperLogLog {
long size ();
void add(Object o);
HyperLogLog merge(HyperLogLog... otherSets);
}
Каждый объект типа HyperLogLog представляет множество элементов и под¬
держивает ввод новых элементов в множество, слияние с другими множествами
по алгоритму HyperLogLog, а также извлечение размера множества. Благодаря
применению алгоритма HyperLogLog запрос количества индивидуальных посе¬
тителей становится очень похожим на запрос количества просмотров страниц.
Главные отличия заключаются в вычислении относительно большого значения
по каждому URL за промежуток времени, а также в применении функции слия¬
ния по алгоритму HyperLogLog для объединения промежутков времени вместо
суммирования отдельных подсчетов. Как и при подсчете количества просмотров
страниц во времени, множества создаются по алгоритму HyperLogLog для про¬
межутков времени в 1 день, неделю и месяц, чтобы сократить объем работы,
которую требуется выполнить во время запроса.
АНАЛИЗ ПОКАЗАТЕЛЯ ОТКАЗОВ ОТ ПРОСМОТРА СТРАНИЦ
И завершающий тип запросов служит для определения показателя отказов
от просмотра в каждом домене. Пакетное представление для такого запроса ока¬
зывается довольно простым и содержит отображение каждого домена на коли¬
чество отказов от просмотра при посещении страниц и общее количество посе¬
щений. Показатель отказов от просмотра определяется просто как отношение
этих двух значений.
Самое главное для предварительного вычисления этих значений — опреде¬
лить, что именно составляет посещение. Так, два просмотра страницы считают¬
ся частью одного и того же посещения, если они совершены одним и тем же
пользователем в одном и том же домене и отделены промежутком времени мень¬
ше получаса. Посещение считается отказом от просмотра, если оно содержит
только один просмотр страницы.
8.2. Краткий обзор процесса пакетной обработки данных
А теперь, когда стали понятны конкретные требования к пакетным представ¬
лениям, можно определить в самых общих чертах последовательность опера¬
ций, выполняемых в процессе обработки данных на уровне пакетной обработки.
Основная последовательность этих операций приведена на рис. 8.5.
В начале процесса обработки данных на уровне пакетной обработки в распре¬
деленной файловой системе имеется единственная папка, содержащая главный
массив данных. Первая стадия рассматриваемого здесь процесса состоит просто
в том, чтобы взять любые новые данные, накопившиеся с момента последнего
выполнения на уровне пакетной обработки, и присоединить их к главному мас¬
сиву данных.
Глава 8 Пример построения уровня пакетной обработки: архитектура и алгоритмы 187
© Как только данные будут подготовлены,
пакетные представления могут
быть вычислены параллельно
Рис. 8.5. Основная последовательность операций, выполняемых в процессе анализа
данных на уровне пакетной обработки в приложении SuperWebAnalytics.com
На двух последующих стадиях происходит нормализация данных для под¬
готовки к вычислению пакетных представлений. На первой стадии нормали¬
зации во внимание принимается тот факт, что разные иЯЬ могут обозначать
один и тот же веб-ресурс. Например, отдельные иЯЬ вроде www.mysite.com/
Ь1од/1?и1^1=1 и http://mysite.eom/blog/l обозначают один и тот же веб-адрес.
Поэтому на первой стадии нормализации все 1ЖЬ приводятся к стандартному
формату, чтобы правильно агрегировать данные при последующих вычислениях.
Вторая стадия нормализации требуется потому, что данные одного и того же
лица могуч' существовать под разными идентификаторами. Чтобы поддерживать
обработку запросов о посещениях страниц и их посетителях, необходимо вы¬
брать единственный идентификатор для каждого лица. Для решения этой задачи
на второй стадии нормализации обрабатывается граф эквивалентов. В пакетных
представлениях используются только данные о просмотрах страниц, и поэтому'
преобразованы будут только ребра просмотров страниц, чтобы воспользоваться
выбранными идентификаторами.
На следующей стадии устраняются дубликаты событий просмотра страниц. Как
упоминалось в главе 2, преимущества блоков данных, содержащих достаточно ин¬
формации, заключаются в том, что они однозначно распознаваемы. В таких слож¬
ных случаях, как разделение сети, принято регистрировать один и тот же про¬
смотр страницы несколько раз, чтобы зафиксировать связанное с ним событие.
188
Часть /. Уровень пакетной обработки
А устранение дубликатов событий требуется для правильного вычисления пакетных
представлений, поскольку они зависят от отдельных событий в массиве данных.
И на завершающей стадии рассматриваемого здесь процесса нормализованные
данные используются для вычисления пакетных представлений, описанных в пре¬
дыдущем разделе. Следует, однако, иметь в виду, что данный процесс повторяется
всякий раз, когда вводятся новые данные, а следовательно, пакетные представле¬
ния вычисляются заново. Как пояснялось в предыдущей главе, обработку на уров¬
не пакетной обработки можно сделать инкрементной, чтобы не всегда требова¬
лось выполнять повторное вычисление с использованием всего главного массива
данных. Но в то же время совершенно необходим вполне определенный, повторя¬
емый снова процесс пакетной обработки данных, поскольку пакетные представле¬
ния придется вычислять с самого начала, если они испортятся.
А теперь рассмотрим структуру каждой стадии данного процесса более под¬
робно. Основное внимание уделим архитектуре и алгоритмам, демонстрируя ка¬
ждую стадию обработки данных на конвейерных схемах, отложив особенности
их воплощения в коде до следующей главы.
8.3. Ввод данных
Для ввода данных в главный массив можно, в частности, размещать файлы
в папке главного массива по мере поступления новых данных. Но у такого под¬
хода имеется существенный недостаток. Допустим, в процессе обработки данных
на уровне пакетной обработки требуется выполнить в главном массиве данных
несколько вычислений, например, вычислить несколько пакетных представле¬
ний. Такие вычисления могут начаться в разное время, а это означает, что ка¬
ждое представление будет отображать главный массив данных в разные моменты
времени. И хотя это может и не стать камнем преткновения, на наш взгляд, на¬
много проще рассуждать о представлениях, когда известно, что все они основы¬
ваются на одном и том же главном массиве данных.
Этот недостаток можно устранить, записывая данные в папку newdata. И тог¬
да на первой стадии процесса пакетной обработки необходимо переместить все
данные из папки newdata в папку главного массива данных, возможно, осуще¬
ствив их вертикальное разделение по ходу дела. Как только данные будут пере¬
мещены, соответствующие файлы могут быть удалены из папки newdata. Таким
образом, в процессе пакетной обработки осуществляется полный контроль
над вводом данных в главный массив и тем самым гарантируется, что каждое
пакетное представление основывается на том же самом главном массиве данных.
8.4. Нормализация URL
На следующей стадии рассматриваемого здесь процесса нормализуются все
URL, имеющиеся в главном массиве данных. Для запроса на выполнение этой за¬
дачи требуется специальная функция, реализующая логику нормализации. В ло¬
гику нормализации может входить извлечение параметров из URL, присоеди¬
нение префикса http:// в начале URL, удаление конечных знаков косой черты
и пр. Но самое главное, что вся логика нормализации должна вмещаться в теле
одной функции, способной оперировать каждым URL в отдельности.
Глава 8. Пример построения уровня пакетной обработки: архитектура и алгоритмы 189
Конвейерная схема такого вычисления очень проста и приведена па рис. 8.6.
Как видите, нужно лишь выполнить одну функцию над каждым блоком данных.
Рис. 8.6. Конвейерная схема нормализации URL
Извлечение полей из объектов в конвейерных схемах
Как правило, данные укладываются в объекты, содержащие нужные для обработки
поля. Например, объект типа Pageview. содержит поля url, timestamp и userid. Если
используется настоящая реализация конвейерных схем, то вычисление начинается
с единственного поля, содержащего объект, и далее над этим объектом выполняется
функция для извлечения из него полей, которыми требуется манипулировать, соединяя,
группируя, используя их в качестве аргументов функций и т.д. Ради краткости изложе¬
ния в этой главе пропускается этап извлечения полей, и поэтому конвейерные схемы,
как правило, начинаются с обозначения входных данных, представляющих собой поля
объекта, которыми требуется манипулировать.
8.5. Нормализация идентификаторов пользователей
На следующей стадии для каждого пользователя выбирается единственный
идентификатор. Это самая сложная стадия процесса пакетной обработки дан¬
ных, поскольку на ней применяется полностью распределенный итеративный
алгоритм обхода графа. Но несмотря на всю сложность этой стадии, для ее ре¬
ализации требуется лишь несколько небольших конвейерных схем. С помощью
подходящих инструментальных средств эту стадию можно реализовать, написав
не больше 100 строк кода, как будет показано в следующей главе.
Идентификаторы пользователя обозначаются как принадлежащие одному и тому
же лицу с помощью ребер эквивалентов. Если наглядно представить эти идентифи¬
каторы из массива данных в виде ребер эквивалентов, то в конечном итоге получат-
ся многочисленные независимые подграфы, как показано на рис. 8.7.
Каждый подграф обозначает отдельного пользователя. Для каждого лица нуж¬
но выбрать единственный идентификатор и организовать приведение к нему
остальных идентификаторов, как показано на рис. 8.8.
С этой целью исходный граф эквивалентов преобразуется в форму, приведен¬
ную на рис. 8.9, где каждый идентификатор пользователя связан с единственным
выбираемым для него идентификатором, к которому он приводится по таблице,
представленной на рис. 8.8.
190
Часть I. Уровень пакетной обработки
О Если у пользователя имеется
несколько идентификаторов,
лишние идентификаторы
должны быть приведены
к минимальному значению
идентификатора пользователя
© Сохранить нужно только
приводимые идентификаторы,
поэтому выбранные
идентификаторы (1, 6)
опускаются
Рис. 8.8. Приведение всех идентификаторов пользователя
к единому идентификатору в каждом массиве данных
Исходный
Приведенный
идентифи¬
идентифи¬
катор
катор
пользователя
пользователя
2
1
3
1
4
1
5
1
11
1
7
6
9
6
Рис. 8.9. Исходный граф эквивалентов преобразуется таким образом,
чтобы все его узлы были направлены к единому узлу
Этот замысел должен быть воплощен в конкретный алгоритм, допускающий мас¬
штабирование с помощью пакетных вычислений. Все предыдущие примеры пакет¬
ных вычислений предполагали одновременное исполнение единственной конвейе])-
ной схемы для получения требуемого результата. Но с помощью данного алгорит¬
ма невозможно получить требуемый результат за один раз. Вместо этого придется
выбрать итеративный алгоритм, где на каждом шаге граф переходит в состояние,
близкое в требующейся структуре, приведенной на рис. 8.9. Определив шаг итера¬
ции, можно выполнять его повторно до тех пор, пока достичь дальнейшего про¬
гресса больше не удастся. В итоге достигается так называемая точка фиксации, где
результирующие выходные данные аналогичны входным данным. Когда достигается
точка фиксации, граф доходит до требуемого состояния.
Глава 8. Пример построения уровня пакетной обработки: архитектура и алгоритмы 191
На каждом шаге итерации в этом алгоритме проверяются узлы, находящиеся
по соседству с каждым узлом графа. Сначала определяется наименьший иденти¬
фикатор среди всех связанных вместе узлов, а затем каждое ребро графа направ¬
ляется к узлу с минимальным значением идентификатора. Этот процесс для од¬
ного узла иллюстрируется на рис. 8.10.
По мере анализа
Рис. 8.10. Пример видоизменения ребер графа вокруг
одного узла на одном шаге итерации
Как видите, данный алгоритм применяется к графу эквивалентов, приведен¬
ному на рис. 8.7. Преобразования, происходящие в этом графе до тех пор, пока
не будет достигнута точка фиксации, представлены на рис. 8.11.
Исходный граф
Шаг итерации 1
Шаг итерации ЗДочка фиксации
Следует иметь Б виду
что когда ребро видо¬
изменяется, оно совсем
не обязательно удаляется
Нп шаге итерации 1 при
обработке узлов, соседних
с узлом 4. алгоритм заме¬
няет ребро [3,4] на ребро
[3.1] Но при обработке
узлов соседних с узлом 3
единственным соседни? 1
с ним узлом оказывает
узел 4, и поэтому ребро
[3 4] остается в графе
Та хе самая погика
соблюдается на послед¬
нем шаге итерации где
новые ребра не вводятся
но удаляются мної ие
имеющиеся в і рафе ребра
Рис. 8.11. Итеративное выполнение алгоритма до тех пор, пока
не будет достигнута тонка фиксации
А теперь составим конвейерную схему, реализующую рассмотренный выше
итеративный алгоритм. В качестве входных данных для этого алгоритма служит
массив из двухэлементных кортежей с идентификаторами лица, представленным
ребрами эквивалентов на графе.
Первым требованием к итеративному алгоритму является определение проме¬
жуточных узлов по соседству с каждым узлом. Кортежи можно попытаться сгруп¬
пировать по их первому элементу, ПО из этой группировки будут исключены те
ребра графа, где данный узел хранится в последнем элементе. Поэтому нужно
сначала распространить каждое ребро в обоих направлениях, используя функцию
192
Часть /. Уровень пакетной обработки
ВісіігесііопаІЕсІдез (). Таким образом, группируя кортежи по первому элемен¬
ту, можно получить каждое ребро графа, где существует узел. Действие функции
ісіігесІїіопаІЕсідез () показано на рис. 8.12. Двунаправленность ребер графа гаран¬
тирует, что при группировке будут учтены все ребра, содержащие узел, независимо
от местоположения узла в первом или во втором элементе кортежа.
Рис. 8.12. Функция, распространяющая каждое ребро графа в обоих направлениях. Когда кортежи
группируются по элементу ±<11 или ±<12, все соседние с данным узлы включаются в его группу
Теперь для каждой группы ребер будет построен новый ряд ребер, содержа¬
щих все узлы из этой группы и направленных в сторону наименьшего узла в груп¬
пе. Это можно сделать с помощью простого агрегатора, как показано в следую¬
щем фрагменте псевдокода:
Все узлы из всех
ребер собираются
в множество
Выбирается
наименьший узел
function userid-step-aggregator(grouped-node, edges) {
nodes = new SortedSetO
for (e in edges) {
nodes.add(e.first)
nodes.add(e.second)
}
target = nodes.smallest()
isNewEdges = grouped != target
for(n in nodes) { <
if (n != target) {
emit(n, target, isNewEdges)
}
Если условие удовлетворяется,
то порождается хотя бы одно ребро.
Эта информация будет использована
в дальнейшем для определения
момента, когда следует остановить
выполнение шага итерации
nodes .size О > 2 <i
Для каждого узла, кроме
выбранного, порождается новое
ребро, направляемое в сторону
выбранного узла
}
Если группируемый узел оказывается наименьшим среди своих соседей, то по¬
рождаемое ребро не изменяется. А если по соседству с узлом находится не один
узел, то некоторые ребра видоизменяются, направляясь к узлу с наименьшим
идентификатором пользователя. При наличии функции В1с11гес^опа1Ес1дез ()
и готового агрегатора конвейерная схема рассматриваемого здесь шага итерации
оказывается довольно простой, как показано на рис. 8.13.
Шаг итерации должен повторяться до тех пор, пока не останется больше ребер
для изменения. Одним из результатов шага итерации является повое множество
ребер, а другим результатом - множество ребер, отсутствующих во входных дан¬
ных этого шага итерации. Смысл его в том, что как только перестанут порождать¬
ся новые ребра, алгоритм достигнет точки фиксации, и каждый узел из множества
Г ав 8 Пример построения уровня пакетной обработки: архитектура и алгоритмы 193
связанных вместе узлов будет направлен в сторону наименьшего узла в данном
МНПЖРГТШ» 1 ' ] ^
Рис. 8.13. Шаг итерации для нормализации
идентификаторов пользователя
Чтобы завершить рассматриваемый здесь алгоритм, нужно заключить шаг
итерации в цикл, выполняя его до тех пор, пока не будет достигнута точка фик¬
сации, как показано в следующем фрагменте псевдокода:
Параметр зЪагЫпдЕс1деэ обозначает
множество ребер, хранящихся в кластере
распределенных вычислений. Оно не
представлено явно в данном примере кода
Выполняется до тех
пор, пока не перестанут
порождаться новые ребра
function userid-normalization(startingEdges) { <
isNewEdges = true
ч> while(isNewEdges) {
[nextEdges, newEdges] = runNormalizationStep(startingEdges) <!—
isNewEdges = ! newEdges. isEmpty () функция runNormalizationStep ()
} заключает в оболочку предыдущую
I конвейерную схему и возвращает
' новое множество с совершенно
новыми ребрами >
На практике подобный код должен выполняться на одной машине, тогда как
функция гипНоппаИгаЪд-ОпЗЪерО вызовет задание на параллельное выполнение
в кластере распределенных вычислении. Если для хранения входных и выходных
данных используется распределенная файловая система, то для согласования путей
к файлам потребуется немного больше кода, как будет показано в следующей главе.
Хотя в приведенном выше псевдокоде схвачена сама суть рассматриваемого здесь
алгоритма.
194
Часть /. Уровень пакетной обработки
Рис. 8.14. Последняя стадия процесса нормализации
идентификаторов пользователя
И последнее требование для за¬
вершения рассматриваемой здесь
стадии процесса обработки состо¬
ит в том, чтобы изменить иденти¬
фикаторы лица в данных о просмо¬
трах страниц и пользоваться далее
только выбранными идентифи¬
каторами. Такое преобразование
можно выполнить, соединив дан¬
ные о просмотрах страниц с по¬
следним шагом итерации в графе
эквивалентов. Соответствующая
конвейерная схема приведена
на рис. 8.14.
Следует иметь в виду, 410 суще¬
ствование идентификатора лица
вполне допустимо в данных о про¬
смотрах страниц, но не в любом
из ребер эквивалентов. Нечто по¬
добное происходит, когда зарегистрирован только один идентификатор пользовате¬
ля. В подобных случаях внешнее соединение гарантирует, что такие идентификато¬
ры лица не отсеиваются из результатов, а данные о просмотрах страниц соединяют¬
ся с пустым значением. Выбранный идентификатор лица становится соединенным
идентификатором, если он существует, а иначе — исходным идентификатором лица.
Вот, собственно, и все, что касается нормализации идентификаторов пользо¬
вателей. И хотя алгоритм такой нормализации представляет больше трудностей
для вычисления, для его выражения требуется совсем немного конвейерных
схем и псевдокода.
8.6. Удаление дубликатов событий просмотра страниц
Последней стадией подготовки к вычислению пакетных представлений яв¬
ляется удаление дубликатов событий просмотра страниц. Конвейерная схема
для этой стадии довольно проста и приведена на рис. 8.15.
8.7. Вычисление пакетных представлений
Итак, данные подготовлены к вычислению пакетных представлений, как и было
определено в начале этой главы. На стадии вычислений создаются неиндексируе¬
мые записи. Далее в этой главе буцет показано, каким образом пакетные представле¬
ния индексируются, чтобы запрашивать их в режиме произвольного доступа.
8.7.1. Количество просмотров страниц во времени
Как пояснялось ранее, пакетное представление количества просмотров страниц
во времени должно агрегировать просмотры страниц по каждому и ЛЬ с ежечасной,
еженедельной и ежемесячной степенью детализации. Поэтом)' сначала следует агре¬
гировать просмотры страниц с ежечасной степенью детализации. Благодаря этому
Гл€ша 8. Пример построения уровня пакетной обработки: архитектура и алгоритмы 195
удается сократить объем данных на несколько порядков. А в дальнейшем значения,
вычисленные с ежечасной степенью детализации, можно накопить, чтобы получите
подсчеты за более длинные промежутки времени. В последнем случае операции бу¬
дут выполняться намного быстрее благодаря меньшему объему входных данных.
Начнем с конвейерной схемы для подсчета количества просмотров страниц
с ежечасной степенью детализации. Сначала необходимо преобразовать отметку
времени в каждом просмотре страницы в часовой промежуток времени, а затем
подсчитать количество просмотров страниц по каждому иЛЬ за групповой про¬
межуток времени. Эта конвейерная схема приведена на рис. 8.16.
Рис. 8.15. Конвейерная схема для удаления
дубликатов событий просмотра страниц
Рис. 8.16. Подсчет количества просмотров
страниц во времени с ежечасной степенью
детализации
Рассмотрим далее конвейерную схему для формирования подсчетов просмо¬
тров страниц со всеми степенями детализации по времени, исходя из ежечас¬
ной степени детализации. Эта конвейерная схема основывается на функции,
порождающей промежуток времени для каждой степени детализации по време¬
ни и подсчета просмотров страниц за каждый час, как показано ниже. И тог¬
да в конвейерной схеме остается лишь просуммировать подсчеты просмотров
страниц по каждому ШЬ, за каждый промежуток времени и при любой степени
детализации (рис. 8.17).
Гипс^іоп етітСгапиІагіТіез (ИоигВискеи { <•
сіауВискеї = hourBucket / 24;
ъ/еекВискеТ = сіауВискет / 7;
топіІіВискеІ = бауВискеТ / 28;
Эта функция порождает четыре
двухэлементных кортежа
для каждых входных данных
196
Часть /. Уровень пакетной обработки
ети ("11",
етП ("у",
ети ("ш",
}
ЬоигВиске!:)
dayBucket)
\^еекВиске!:)
шonthBucket)
Первый элемент кортежа содержит значение
"Ь", "с!”, "к" или "т", обозначающее степень
детализации в 1 час, день, неделю или месяц
соответственно, а второй элемент — числовое
значение промежутка времени
Рис. 8.17. Конвейерная схема для подсчета просмотров страниц
во времени со всеми степенями детализации по времени
8.7.2. Подсчет индивидуальных посетителей страниц во времени
Пакетное представление для подсчета индивидуальных посетителей страниц
во времени содержит множество по алгоритму Нурег1^1х^ для каждого проме¬
жутка времени, отслеживаемого по каждому иЯЬ. Вычисления в данном случае
отличаются от подсчета количества просмотров страниц лишь тем, что вместо
подсчетов агрегируются множества по алгоритму НурегГл^!^.
Сводная конвейерная схема для подсчета индивидуальных посетителей стра¬
ниц из множеств по алгоритму Нурег1х^1х^ за каждый и час и с меньшей степе¬
нью детализации по времени приведена на рис. 8.18. Как видите, для этой цели
требуются лишь агрегаторы Сопз1:гис1:НурегЪодЪод и МегдеНурегЬодЪод, реализо¬
вать которые не составит большого труда.
8.7.3. Анализ показателя отказов от просмотра
И в последнем пакетном представлении вычисляется показатель отказов
от просмотра страниц по каждому иЯЬ. Как пояснялось в начале этой главы,
с этой целью для каждого домена вычисляются два значения: общее количество
посещений и количество отказов от просмотра при посещении страниц.
Главную часть обработки этого типа запросов составляет отслеживание каж¬
дого посещения, совершаемого пользователем при просмотре Интернета. Для
этого проще всего проанализировать все просмотры страниц, совершенные
Глаоа 8. Пример построения уровня пакетной обработки: архитектура и алгоритмы 197
пользователем в конкретном домене, отсортировав их в хронологическом поряд¬
ке. А затем можно воспользоваться разницей во времени между последователь¬
ными просмотрами страниц, чтобы выяснить, относятся ли они к одному и тому
хсе посещению. Если посещение содержит только один просмотр страницы, оно
считается посещением с отказом от просмотра.
Агрегатор, выполняющий эти операции, можно назвать Апа1у2еУ1злЛз.
Проанализировав все просмотры страниц, совершенные пользователем в кон¬
кретном домене, агрегатор Апа1у2еУ1за^з порождает два поля, содержащие об¬
щее количество посещений, совершенных пользователем в этом домене, а также
количество посещений с отказом от просмотра.
Конвейерная схема для анализа показателя отказов от просмотра приведена
на рис. 8.19. Как видите, она требует более сложных вычислений из-за несколь¬
ких операций агрегирования. Тем не менее их нетрудно представить в виде кон¬
вейерной схемы.
Рис. 8.18. Конвейерная схема для подсчета рИс. 8.19. Конвейерная схема для анализа
индивидуальных посетителей страниц показателя отказов от просмотра
во времени
198
Часть /. Уровень пакетной обработки
Вот, собственно, и все! На этом рассмотрение процесса обработки дан¬
ных и алгоритмов, применяемых на уровне пакетной обработки приложения
SuperWebAnalytics.com, завершается.
Резюме
Уровень пакетной обработки для приложения SuperWebAnalytics.com содер¬
жит сложную логику, хотя реализовать ее нетрудно. Это объясняется в основном
характером вычисления функций над всеми данными, имеющимися на уровне
пакетной обработки. Если все данные можно просматривать сразу и без огра¬
ничений, накладываемых алгоритмами инкрементных вычислений, то построе¬
ние информационных систем оказывается простым и нетрудным делом. Кроме
того, пакетные вычисления предоставляют немало удобств. Так, для вычисления
новых представлений можно легко расширить уровень пакетной обработки, по¬
скольку на каждой стадии процесса обработки допускается свободно выполнять
произвольные функции над всеми данными.
Как уже не раз отмечалось, в этой главе был разработан повторно выполняе¬
мый процесс обработки данных, где пакетные представления всегда вычисляют¬
ся заново. Имеется целая категория задач, для решения которых можно сделать
обработку на уровне пакетной обработки инкрементной, повысив эффектив¬
ность использования на нем ресурсов, но не слишком усложняя дело. О тОхМ, как
это делается, речь пойдет в главе 18.
Следует особо подчеркнуть, что ни один из методов, представленных в этой
главе, не является характерным для каких-нибудь инструментальных средств.
Поэтому, независимо от выполняемых пакетных вычислений или хранилищ, ис¬
пользуемых на уровне пакетной обработки, процесс и алгоритмы обработки дан¬
ных, рассмотренные в этой главе, остаются неизменными. И на практике этот
процесс и алгоритмы очень просто воплощаются в реальном коде. У вас будет
возможность убедиться в этом в следующей главе, где представлена полная рабо¬
чая реализация уровня пакетной обработки в приложении SuperWebAnalytics.com.
Пример реализации
уровня пакетной
обработки
В этой главе...
■ Ввод данных в главный массив.
■ Подробная реализация процесса пакетной
обработки данных.
■ Интеграция граф-схем, построенных средствами
Apache Thrift, с библиотеками Pail и JCascalog.
В предыдущей главе обсуждалась архитектура и алгоритмы для уровня пакет¬
ной обработки приложения SuperWebAnalytics.com. А в этой главе нам предсто¬
ит воплотить их полностью, используя рассмотренные ранее инструментальные
средства вроде Apache Thrift, Pail и JCascalog. По ходу дела у вас будет возмож¬
ность убедиться, насколько точно прикладной код соответствует составленным
ранее конвейерным схемам и последовательностям операций. Это явно свиде¬
тельствует о доброкачественности использовавшихся абстракций, поскольку они
позволяют писать код в том же ключе, в каком осмысляются решаемые задачи.
Как это часто случается с настоящими инструментальными средствами, при
их использовании возникают трения в связи с искусственными сложностями, ко¬
торые они привносят в процесс разработки. В данном случае некоторые слож¬
ности возникают из-за ограничений, которые накладываются системой Hadoop
на файлы небольшого размера и которые приходится преодолевать. Поэтому
очень важно ясно представлять не только идеальный процесс обработки данных
и алгоритмы, но и особенности их реализации на практике.
Процесс обработки данных, разработанный в предыдущей главе, еще раз
представлен на рис. 9.1. Рекомендуем обратиться к главе 8, чтобы освежить в па¬
мяти все, что происходит на каждой стадии этого процесса.
200
Часть I. Уровень пакетной обработки
© Как только данные будут подготовлены,
пакетные представления могут быть
вычислены параллельно
Рис. 9.1. Процесс панетной обработки данных в приложении SuperWebAnalytics.com
9.1. Отправная точка
Прежде чем реализовать процесс обработки данных, сделаем краткий обзор
того, что было до сих пор разработано для приложения SuperWebAnalytics.com.
Итак, мы реализовали самую сердцевину уровня пакетной обработки: строгую
граф-схему, представляющую информационную модель и позволяющую хранить
данные в распределенной файловой системе. Для составления этой граф-схемы, где
пользователи и страницы представлены узлами, просмотры страниц — ребрами, со¬
единяющими узлы пользователей и страниц, а прочая информация хранится в виде
свойств узлов, было использовано инструментальное средство Apache Thrift.
Для сопряжения с системой HDFS была использована библиотека Pail, кото¬
рая предоставляет простой интерфейс для вертикального разделения данных,
выбора формата файлов и управления основными операциями вроде присоеди¬
нения массивов данных. А для хранения в приложении SuperWebAnalytics.com
объектов типа Data в “ведрах” с дополнительным разделением по типу ребер или
свойств был создан интерфейс PailStructure.
9.2. Подготовка процесса пакетной обработки данных
Прежде чем начать реализацию самого процесса обработки данных, требует¬
ся быстро подготовиться к нему. На многих стадиях этого процесса происходит
201
Глави 9. Пример реализации уровня пакетной обработки
манипулирование объектами, определенными в граф-схеме, в том числе объекта¬
ми типа Data, PageViewEdge, PagelD и PersonID. Системе Hadoop должно быть из¬
вестно, каким образом следует сериализировать и дессриализировать эти объек¬
ты, что ы переносить их между машинами при выполнении заданий MapReduce.
С этой целью необходимо зарегистрировать сериализатор объектов.
Необходимая для этой цели реализация данного сериализатора предоставляет¬
ся в проекте cascadingthrift (https://github.com/cascading/cascading-thrift).
В следующем фрагменте кода демонстрируется порядок регистрации сериализа¬
тора для процесса пакетной обработки данных:
Сериализатор Apache Thrift
объектов в приложении
SuperWebAnalytics.com
public static void setApplicationConf () {
Map conf = new HashMap ( ) ;
String sers = "backtype.hadoop.ThriftSerialization," +
”org•apache.hadoop.io.serializer.WritableSerialization" ; <
conf. put ("io.serializations", sera); Стандартный сериализатор
Api. setApplicationConf (conf) ; для объектов, записываемых
в Hadoop
В этом фрагменте кода системе Hadoop дается команда пользоваться как се-
риализатором Apache Thrift, так и стандартным для Hadoop сериализатором.
При регистрации нескольких сериализаторов система Hadoop автоматически
определит соответствующий сериализатор, когда в ней потребуется сериали¬
зировать объект. В этом коде конфигурация устанавливается глобально и далее
используется в каждом задании, выполняемом в процессе пакетной обработки
данных. Принимая все это во внимание, приступим к реализации процесса па¬
кетной обработки данных, начиная с ввода новых данных.
9.3. Ввод новых данных
При разработке процесса пакетной обработки данных было важно отделить
главный массив данных от новых поступающих данных. Это препятствует вводу
новых данных в главный массив по ходу процесса пакетной обработки данных,
а следовательно, не дает возможности отдельным представлениям отображать
несколько отличающие главные массивы данных.
Поэтому первая стадия процесса пакетной обработки данных состоит в том,
чтобы расположить данные в папке new-data, ввести их в “ведро” главного
массива данных и удалить эти данные из папки new-data. И хотя это простой
принцип, он может вызвать трудности, связанные с синхронизацией. Опуская
на время подробности фактического присоединения данных, попробуем сделать
следующее:
//Не для использования!
public static void badNewDataAppend (
Pail masterPail, Pail newDataPail) throws IOException {
appendNewDataToMasterDataPail (masterPail, newDataPail) ;
newDataPail.clear();
}
Этот код нажегся достаточно простым, но в нем кроется условие гонок. В ходе
присоединения дополнительные данные могут быть записаны в “ведро” с новыми
данными Если же очистить “ведро” с новыми данными по окончании задания
202
Часть I. Уровень пакетной обработки
на присоединение, то попутно удалятся любые новые данные, записанные при
выполнении задания на присоединение.
Правда, это затруднение легко разрешить, воспользовавшись метода¬
ми snapshot () и deleteSnapshot () из библиотеки Pail. В частности, метод
snapshot () сохраняет моментальный снимок “ведра” в новом месте, тогда как ме¬
тод deleteSnapshot () удаляет из исходного “ведра” только данные, существующие
в моментальном снимке. С помощью этих методов в следующем фрагменте кода
гарантируется, что удаляются только те данные, которые были успешно присоеди¬
нены к “ведру” в главном массиве данных:
Папка /Ьпр/вна
служит в качестве
временного
места для
хранения данных
в процессе
их обработки
Присоединяет
данные из
моментального
снимка к
главному массиву
данных
public static void ingest(Pail masterPail, Pail newDataPail)
throws IOException (
FileSystem fs = FileSystem.get (new Configuration());
fs.delete(new Path("/tmp/swa"), true); _
Lfe n 4-vwi. /*. / ,.v x Принимает моментальный
L>fs.mkdirs(new Path("/tmp/swa")); снимок "ведра” с новыми данными
Pail snapshotPail = newDataPail. snapshot ("/tmp/swa/newDataSnapshot"); <
h> appendNewDataToMasterDataPail(masterPail, snapshotPail);
newDataPail.deleteSnapshot(snapshotPail); «
} После присоединения удаляются
только те данные, которые
существуют в моментальном снимке
Следует заметить, что в этом коде создается временное место для хранения
данных в папке /tmp/swa. На многих стадиях процесса обработки данных потре¬
буется место для временного хранения данных, что дает возможность инициали¬
зировать это место перед выполнением первой стадии.
Но это еще не все, поскольку мы должны рассмотреть подробности реализа¬
ции функции appendNewDataToMasterDataPail (). Отличие “ведер” в папке new-
data и главном массиве данных заключается, в частности, в том, что главный
массив данных вертикально разделяется по типу свойств или ребер. “Ведро”
в папке new-data служит лишь в качестве места для хранения новых данных,
и поэтому каждый файл в ней может содержать блоки данных со всеми типами
свойств и ребер. Прежде чем присоединить эти данные к главному массиву дан¬
ных, их нужно реорганизовать в структуру, согласующуюся с “ведром” в главном
массиве данных. Такой процесс реорганизации "ведра” с целью придать ему но¬
вую структуру данных называется дроблением.
Чтобы раздробить данные в “ведре”, нужно иметь возможность записывать
и читать данные из “ведра” по запросам JCascalog. Прежде чем реализовывать
дробление, рассмотрим порядок интеграции библиотеки JCascalog и “ведер”.
Напомним, что в JCascalog абстракция для чтения и записи данных называ¬
ется отводом. В проекте dfs-datastores (https://github.com/nathanmarz/dfs-
datastores) предоставляется реализация PailTap “ведерного” отвода, благодаря
которой “ведра” можно употреблять в качестве входных и выходных данных
для запросов JCascalog. Если отвод типа PailTap служит источником данных, то
в нем проверяется “ведро” и автоматически десериализируются содержащиеся
в нем записи.
203
Глава 9. Пример реализации уровня пакетной обработки
В следующем фрагменте кода создается отвод для чтения всех данных из “ве¬
дра в качестве источника для запроса:
public static void pailTapüsage ()
Моментальный снимок содержит “ведро”
из приложения SuperWebAnalytics.com,
чтобы порождать в отводе
объекты Apache Thrift типа Data
о—
Тар source - new PailTap ("/tmp/swa/snapshot") ;
new Subquery("?data") .predicate(source, " ", "'’data");
1
В отводе порождается файл,
содержащий запись и сами данные;
в процессе обработки данных имя
файла не требуется, поэтому им
можно пренебречь
В отводе тина РаПТар поддерживается также чтение подмножества данных
из ведра . Так, для “ведер”, пользующихся классом SplitDataPailStructure, упо¬
минавшимся в главе 5, можно построить отвод типа РаПТар, читающий данные
только из ребер эквивалентов, содержащихся в “ведре”:
Атрибуты представлены массивом списков;
каждый список содержит путь к подкаталогу,
который служит в качестве входных данных
PailTapOptions opts = new PailTapOptions ();
>opts.attrs = new List[] {
Передает специальные
конфигурации отводу
типа PailTap
Создает список,
содержащий
относительный
путь к ребрам
эквивалентов
new ArrayList<String>.() {{
add("" + DataUnit._Fields.EQUIV.getThriftFieldId());
П
};
Тар equivs = new PailTap("/tmp/swa/snapshot", opts);
Создает отвод
с указанными
параметрами
Такие функциональные возможности требуются довольно часто, и поэтому их
следует заключить в тело функции для последующего применения, как показано
ниже.
public static PailTap attributeTap(String path,
final DataUnit._Fields... fields) {
PailTapOptions opts = new PailTapOptionsO;
opts.attrs = new List [] {
new ArrayList<String>() {{
for(DataUnit._Fields field: fields) {
add("" + field.getThriftFieldld()); <^~
}
}}
В качестве входных данных
для отвода может быть указано
несколько подпапок
return new PailTap(path, opts);
}
При вводе данных из запросов в совершенное новое “ведро” необходимо
объявить тип записей, направляемых в отвод типа PailTap. С этой целью уста¬
навливается специальный параметр spec, который должен содержать подходя¬
щий объект типа PailStructure. Чтобы создать ведро , где все блоки данных
дробятся по атрибуту, можно воспользоваться классом SplitDataPailStructure
следующим образом:
public static PailTap splitDataTap(String path) {
PailTapOptions opts = new PailTapOptionsO;
opts.spec =
204
Часть I. Уровень пакетной обработки
new PailSpec((PailStructure) new SplitDataPailStructure());
return new PailTap(path, opts);
}
Л теперь можно воспользоваться отводом типа Pail Тар и библиотекой
JCascalog, чтобы реализовать дробление в виде отдельной стадии процесса об¬
работки данных. Первая попытка дробления может выглядеть так, как показано
ниже.
// Не для использования!
public static void badShredO {
PailTap source = new PailTap("/tmp/swa/snapshot");
PailTap sink = splitDataTap("/tmp/swa/shredded");
Api.execute(sink,
new Subquery("?data").predicate(source, "?data"));
}
Логически этот запрос составлен правильно. Но при попытке выполнить его
над большим массивом входных данных в системе IIDFS возникнет ряд затруд¬
нений вроде ошибок в именах узлов и ограничений на обработку файлов. Эти
ограничения накладываются самой системой Hadoop. Недостаток этого запроса
заключается в том, что он приводит к созданию бесконечного числа файлов не¬
большого размера, а ведь система Hadoop плохо справляется с непомерно боль¬
шим числом мелких файлов (см. главу 7).
Чтобы понять, почему это происходит, нужно выяснить, каким образом вы¬
полняется запрос. В этот запрос не входят операции агрегирования и соедине¬
ния, и поэтому он выполняется в виде задания только на стадии предваритель¬
ной обработки, тогда как стадия консолидации пропускается. И, как правило,
это весьма желательно, поскольку стадия консолидации оказывается намного бо¬
лее затратной. Но допустим, что граф-схема состоит из 10 разных типов ребер
и свойств. Следовательно, в единственной задаче на стадии предварительной об¬
работки можно создать 100 отдельных выходных файлов — по одному на каждый
тип записи. Если для обработки входных данных (около 1,5 Тбайт, хранящихся
блоками по 128 Мбайт) потребуется 10 тыс. операций предварительной обра¬
ботки, то выходные данные будут состоять приблизительно из 1 млн файлов,
которых окажется слишком много для обработки в Hadoop.
Это затруднение можно разрешить, искусственно введя стадию консолидации
в вычисление. В отличие от операций предварительной обработки, можно яв¬
ным образом контролировать количество операций консолидации, настраивая
задание. Так, если выполнить гипотетическое задание над данными объемом 1,5
Тбайт в течение 100 операций консолидации, можно сформировать 10 тыс. на¬
много более управляемых файлов. В следующий фрагмент кода включен “агрега¬
тор идентичности”, вынуждающий выполнять запрос на стадии консолидации:
public static Pail shred() throws IOException {
PailTap source = new PailTap("/tmp/swa/snapshot");
PailTap sink = splitDataTap("/tmp/swa/shredded");
Subquery reduced = new Subquery ("?rand", "?data")
.predicate(source, "?data-in")
.predicate (new RandLongO)
205
Глава 9. Пример реализации уровня пакетной обработки
Назначает
произвольное
число для
каждой записи
После стадии
консолидации
выдает
случайное число
о .out("?rand")
.predicate (new IdentityBuffer(), "?data-in")<H
> .out("?data");
Api.execute(sink,
Использует агрегатор
идентичности, чтобы получить
каждую запись данных
для стадии консолидации
new Subquery("?data").predicate(reduced, "?data"));
}
Pail shreddedPail = new Pail (
shreddedPail.consolidate() ;
return shreddedPail;
vtmp/swa/shredded") ;
Соединяет опорожненное “ведро” для
дальнейшей консолидации количества файлов
А теперь, когда данные раздроблены и количество файлов сведено к мини¬
муму, их можно в конечном итоге присоединить к “ведру” в главном массиве
данных, как показано ниже. Как только новые данные будут введены в главный
массив данных, можно приступить к их нормализации.
public static void appendNewData (Pail masterPail,
Pail snapshotPail) throws IOException {
Pail shreddedPail = shred();
masterPail.absorb(shreddedPail) ;
}
9.4. Нормализация URL
Следующая стадия состоит в нормализации всех URL в главном массиве данных,
чтобы привести их к канонической форме. Несмотря на то что нормализация мо¬
жет иметь немало особенностей, включая извлечение параметров из URL, добав¬
ление префикса http:// в начале URL и удаление конечных знаков косой черты,
ниже в целях демонстрации приведена лишь самая элементарная ее реализация.
Входной объект
клонируется
таким образом,
чтобы его можно
было надежно
модифицировать
Функция принимает объект типа Data
и порождает нормализованный объект типа Data
public static class NormalizeURL extends CascalogFunction { <
public void operate(FlowProcess process, FunctionCall call) {
-t> Data data = ((Data) call.getArguments().getObject(0)).deepCopy();
DataUnit du = data.get_dataunit();
if(du.getSetField() == DataUnit._Fields.PAGE_VIEW) { <ь
normalize (du.getj?age_view() .getjpageO ) ;
call.getOutputCollector () .add (new Tuple (data) ) ;
private void normalize(PagelD page) {
if(page.getSetField() == PageID._Fields.URL) {
String urlStr = page.get_url() ;
Для поддерживаемых
пакетных представлений
должны быть
реализованы только ребра
просмотров страниц
try {
URL url = new URL (urlStr) ;
page.set_url(url.getProtocol()
ur1.getPath()); «
) catch(MalformedURLException e)
+ + url.getHost () +
I, Просмотры страниц нормализуются
1 путем извлечения стандартных
составляющих из иЯ1
}
206
Часть /. Уровень пакетной обработки
Приведенной выше функцией можно воспользоваться для создания нормали¬
зованной версии главного массива данных. Напомним, что конвейерная схема
для нормализации иКЬ выглядит так, как показано на рис. 9.2.
Рис. 9.2. Конвейерная схема для нормализации URL
Эта конвейерная схема преобразуется в запрос JCascalog с помощью следую¬
щего кода:
public static void normalizeURLs () {
Tap masterDataset = new PailTap("/data/master");
Tap outTap = splitDataTap("/tmp/swa/normalized^rls") ;
Api.execute(outTap,
new Subquery("?normalized")
.predicate(masterDataset, "?raw")
.predicate(new NormalizeURL(), "?raw")
.out("?normalized"));
9.5. Нормализация идентификаторов пользователей
А теперь реализуем самую сложную стадию процесса обработки данных: нор¬
мализацию идентификаторов пользователей. В качестве напоминания на рис. 9.3
приведен итеративный алгоритм обхода графа, применяемый на данной стадии.
Упорядочение типов данных в Apache Thrift
Напомним, что вместо целых значений идентификаторы типа PersonID фактически
моделируются в Apache Thrift как объединения следующим образом:
union PersonID {
1: string cookie;
2: і64 user_id;
}
Правда, в Apache Thrift обеспечивается естественное упорядочение всех структур, которые
могут использоваться для определения "минимального” идентификатора. Это средство
Apache Thrift выгодно используется в рассматриваемом здесь алгоритме нормализации.
207
Глава 9. Пример реализации уровня пакетной обработки
Исходный граф Шаг итерации!
Следует иметь в виду,
что когда ребро видо¬
изменяется, оно сов¬
сем не обязательно
удаляется
На шаге итерации 1
при обработке узлов,
соседних с узлом 4,
алгоритм заменяет
ребро [3,4] на ребро
[3,1]. Но при обработ¬
ке узлов, соседних
с узлом 3, единствен¬
ным соседним с ним
узлом оказывает
узел 4, и поэтому реб¬
ро [3,4] остается
в графе
Та же самая логика
соблюдается на по¬
следнем шаге итера¬
ции, где новые ребра
не вводятся, но уда¬
ляются многие имею¬
щиеся в графе ребра
Рис. 9.3. Повторение алгоритма до тех пор, пока не будет достигнута точка фиксации
Итак, приступим к реализации итеративного алгоритма нормализации.
Результаты нормализации будут сохраняться в новой папке по пути /tmp/swa/
equivs{число итераций} в распределенной файловой системе. Эти результаты
будут состоять из двухэлементных кортежей с идентификаторами типа PersonID.
В следующем фрагменте кода исходный массив данных создается путем преоб¬
разования объектов ребер эквивалентов, хранящихся в главном массиве данных:
public static class EdgifyEquiv extends CascalogFunction { <b
public void operate (FlowProcess process, FunctionCall call) {
Data data = (Data) call.getArguments().getObject(0);
EquivEdge equiv = data.get_dataunit () .get_equiv() ;
call.getOutputCollector()
.add(new Tuple(equiv.get_idl(), equiv.get_id2()));
}
Специальная функция
для извлечения
идентификаторов из ребер
эквивалентов
public static void initializeUserldNormalizationO throws IOException
Tap equivs = attributeTap('7tmp/swa/normalized_urls'\
DataUnit._Fields.EQUIV);
Api.execute(Api.hfsSeqfile("/tmp/swa/equivsO"), <3
new Subquery("?nodel", "?node2")
.predicate(equivs, "?data")
.predicate(new EdgifyEquivO , "?nodel", "?node2"));
Инициализированные данные
сохраняются как результат
выполнения нулевого шага итерации
В качестве напоминания конвейерная схема рассматриваемой здесь итератив¬
ной стадии еще раз приводится на рис. 9.4.
208
Часть /. Уровень пакетной обработки
Рис. 9.4. Конвейерная схема итеративной стадии
нормализации идентификаторов пользователей
Напомним, что ребра должны распространяться в обоих направлениях, что¬
бы все узлы, находящиеся по соседству с данным узлом, можно было сгруппиро¬
вать вместе. Следующая функция распространяет ребра в обоих направлениях:
Отбирает любые ребра,
соединяющие узел
с самим собой
public static class BidirectionalEdge extends CascalogFunction {
public void operate(FlowProcess process, FunctionCall call) {
Object nodel = call.getArguments () .getObject (0) ;
Object node2 = call.getArguments().getObject(1);
—> if(!nodel.equals(node2)) {
call.getOutputCollector().add(new Tuple(nodel, node2)); <}—
call.getOutputCollector().add(new Tuple(node2, nodel));
Распространяет ребра, используя
упорядочения [а, b] и [Ь, а]
Как только узлы будут сгруппированы, потребуется специальный агрегатор,
чтобы реализовать логику рассматриваемого здесь алгоритма и обозначить но¬
вые ребра, как показано ниже.
public static class IterateEdges extends CascalogBuffer {
public void operate(FlowProcess process, BufferCall call) {
PersonID grouped = (PersonID) call.getGroup().getObject (0);
TreeSet<PersonID> alllds = new TreeSet<PersonID>(); о—
Получает узел,
используемый
для группирования
alllds.add(grouped)
Множество типа ТгееЭеЬ содержит
узел и все соседние с ним узлы
209
Глава 9. Промер реализации уровня пакетной обработки
Iterat°r<TupleEntry> it = call .getArguments Iterator () ,•
while(it.hasNext()) { w
alllds.add{(PersonID) it.next().getObject(0));
}
Iterator<PersonID> allldslt = alllds. iterator () ;
PersonID smallest = allldslt.next(); <
boolean progress =
alllds.size() > 2 && !grouped.equals(smallest);
while(allldslt.hasNext()) {
PersonID id = allldslt.next();
call.getOutputCollector () .add(new Tuple (smallest,
Множество типа ТгееЗеЪ сортируется,
чтобы наименьший его элемент
оказался первым
Если сгруппированный узел не
является наименьшим и связан хотя
бы с двумя другими узлами, то будет
создано новое ребро
id, progress)); о
Распространяет ребра,
сформированные
на данном шаге итерации
Наконец, в следующем фрагменте кода реализуется первая часть конвейерной
схемы:
public static Subquery iterationQuery(Тар source) {
Subquery iterate = new Subquery("?bl", ”?nodel", "?node2", "?is-new")
Отвод источника данных
порождает кортежи
идентификаторов типа
РегаопП) из предыдущего
шага итерации
h>.predicate(source, ,"?nl", "?n2")
.predicate(new BidirectionalEdge(), "?nl",
.out("?bl", "?b2")
.predicate (new IterateEdges(), "?b2") <—
.out("?nodel", "?node2", "?is-new");
iterate = Арі.selectFields(iterate,
"?n2")
Группирует кортежи
по полю ?Ы
из объявленного
результата запроса
JCascalog
new Fields("?nodel", "?node2", "?is-new");
return (Subquery) iterate;
}
В приведенном ниже подзапросе реализуется логика рассматриваемого здесь
алгоритма. Чтобы завершить шаг итерации, придется сначала ввести подходящие
отводы источника и получателя данных, а затем выполнить запрос.
Вое ребра
направляются
к результату
шага итерации
public static Тар userIdNormalizationIteration(int і) (
Тар source = (Тар) Api.hfsSeqfile("/tmp/swa/equivs" + (і - 1)) ;
-> Tap sink = (Tap) Api.hfsSeqfile ("/tmp/swa/equivs" + i) ;
Tap progressSink = (Tap) Api.hfsSeqfile("/tmp/swa/equivs" + "-new");
Исключает запись
дубликатов ребер
в получатель
данных
Выполняет
подзапросы
newEdgeSetи
progreaeEdges
параллельно
Subquery iteration = iterationQuery(source);
Subquery newEdgeSet = new Subquery("?nodel", "?node2")
.predicate(iteration, "?nodel", "?node2", "_")
-t> .predicate(Option.DISTINCT, true);
Новые ребра
дополнительно
сохраняются
по отдельному пути
Subquery progressEdges = new Subquery (?nodel", "?node2"
.predicate(iteration, "?nodel", "?node2", true)
Арі.execute(Arrays.asList(sink, progressSink),
t, Arrays.asList(newEdgeSet, progressEdges));
return progressEdgesSink;
На этом шаге
итерации
в текущем
получателе
данных
сохраняются
только новые
ребра
210
Часть /. Уровень пакетной обработки
Помимо хранения всех ребер в качестве входных данных для следующего шага
итерации, все новые ребра сохраняются также в отдельной папке. Благодаря это¬
му легче выяснить, были ли на текущем шаге итерации сформированы новые
ребра, а иначе — посчитать, что в итеративном алгоритме достигнута точка фик¬
сации. В следующем коде реализуется цикл итерации и подобная логика завер¬
шения данного алгоритма:
Отслеживает
текущий
шаг итерации
public static int userldNormalizationiterationLoop() {
> int iter = 1;
while(true) {
На последнем
шаге итерации
определяется путь
для сохранения
окончательного
результата
Тар progressEdgesSink = userldNormalizationlteration(iter) ;
FlowProcess flowProcess = new HadoopFlowProcess (new JobConfO)
if(! flowProcess.openTapForRead(progressEdgesSink).hasNext())
return iter;
iter++;
Если сформированы новые ребра,
счетчик цикла увеличивается
Цикл прекращается,
если на данном шаге
итерации сформированы
новые ребра
Далее необходимо обновить данные о просмотрах страниц новыми иденти¬
фикаторами типа РегзопЮ. На рис. 9.5 еще раз приведена конвейерная схема,
предназначенная для этой цели и включающая в себя соединение.
Рис. 9.5. Конвейерная схема завершающей
стадии нормализации идентификаторов пользователей
Прежде чем осуществить такое соединение, потребуется пара функций. Во-
первых, нужно выявить объекты Apache Thrift для просмотров страниц, чтобы
извлечь необходимые поля:
Извлекает
нужные параметры
из объектов типа
PageViewEdge
public static class ExtractPageViewFields extends CascalogFunction {
j public void operate(FlowProcess process, FunctionCall call) {
| Data data = (Data) call.getArguments().getObject(0);
211
Глава Я Пример реализации уровня пакетной обработки
Порождает №1.,
идентификатор
типа РагаогШЭ
и отметку времени
просмотра страниц
д ewEdge pageview = data.get__dataunit ().getjpage_view() ;
it(pageview.get j?age().getSetFieldO == PagelD. Fields.URL) {
call.getOutputCollector().add(
l> new Tuple (pageview. get jpage (). get_url (),
pageview.get_person(),
^ data.get_pedigree () .get_true_as_of_secs ())); <
} И хотя отметка времени непосредственно не требуется,
} эта функция будет повторно использована на других
стадиях процесса обработки
Во-вторых, требующаяся функция принимает объект типа Data и новый иден¬
тификатор типа PersonID, а возвращает новый объект типа Data с обновленным
идентификатором типа PersonID, как показано ниже.
Клонирует
объект типа Data
для его надежной
модификации
И хотя отметка времени непосредственно не требуется,
эта функция будет повторно использована на других
стадиях процесса обработки
public static class MakeNormalizedPageview extends CascalogFunction {
public void operate(FlowProcess process, FunctionCall call) {
PersonID newld = (PersonID) call.getArguments().getObject(0); <*
—> Data data = ((Data) call.getArguments () .getObject (1)) .deepCopyO ;
if(newld != null) {
data. get_dataunit () . getj?age_view (). set jperson (newld) ;
}
call.getOutputCollector () .add(new Tuple (data)); <
. ^ Порождает потенциально видоизмененный
' объект типа Data для просмотра страниц
С помощью этих двух функций теперь можно осуществить соединение, чтобы
сначала модифицировать просмотры страниц, а затем воспользоваться норма¬
лизованными идентификаторами типа PersonID. Напомним, что внешнее соеди¬
нение требуется для просмотров страниц с идентификаторами типа PersonID,
которые содержатся в графе эквивалентов, как показано ниже.
public static void modifyPageViews(int iter) throws IOException {
Tap pageviews = attributeTap("/tmp/swa/normalized_urls",
DataUnit._Fields.PAGE_VIEW);
> Tap newlds = (Tap) Api.hfsSeqfile("/tmp/swa/equivs" + iter);
Tap result = splitDataTap("/tmp/swa/normalized_pageview_users");
Использует
конечный
результат
выполнения
цикла
итерации
Осуществляет
соединение по
идентификатору
пользователя
в просмотре
страницы
Api.execute (result, new Subquery ("?normalized-pageview")
.predicate(newlds, "!!newld", "?person") «—
.predicate(pageviews, "?data")
.predicate(new ExtractPageViewFields("?data"
-t> . out (M_", "?person", "_")
Осуществляет соединение
по полю ?рег8оп; префикс
! !пеиМ обозначает, что это
внешнее соединение
.predicate (new MakeNormalizedPageview (), "ünewld", "?data")
.out("?normalized-pageview"));
Создает и порождает новый
нормализованный просмотр
станины
И последняя задача состоит в определении функции-оболочки для выполне¬
ния отдельных фаз на данной стадии процесса обработки данных. Ниже показа¬
но, каким образом эта задача реализуется в коде.
Часть /. Уровень пакетной обработки
218
public static void normalizeUserlds() throws IOException {
initializeUserldNormalizationO;
int numlterations = userldNormalizationiterationLoop();
modifyPageViews(numlterations);
}
На этом реализация стадии нормализации рассматриваемого здесь процесса
пакетной обработки данных завершается. Она служит характерным примером
тех преимуществ, которые дает указание вычислений MapReduce с помощью
библиотеки языка программирования общего назначения. Значительная часть
логики, включая итерацию и проверку точки фиксации, реализована в обычном
коде Java. Обратите также внимание, насколько точно прикладной код следует
конвейерным схемам и псевдокоду, представленным в предыдущей главе. И это
явный признак того, что вы работаете на верном уровне абстракции.
9.6. Исключение дубликатов
событий просмотра страниц
На следующей стадии исключаются дубликаты событий просмотра страниц
для подготовки к вычислению пакетных представлений. Это делается настолько
просто, что мы опустим конвейерную схему и перейдем непосредственно к при¬
веденному ниже коду, где предикат Option. DISTINCT из библиотеки JCascalog слу¬
жит для ввода операций группирования и агрегирования, требующихся для раз¬
личения кортежей.
Ограничивает отвод
источника данных
только чтением
просмотров страниц
из“ведра"
public static void deduplicatePageviews() {
Tap source = attributeTap('7tmp/swa/normalizedj?ageview_users",
> DataUnit._Fields.PAGE_VIEW);
Tap outTap = splitDataTap("/tmp/swa/unique_pageviews");
Api.execute(outTap, new Subquery("?data")
.predicate(source, "?data")
.predicate(Option.DISTINCT, true));
<H
Отдельный предикат
для удаления всех
дублирующих объектов
просмотра страниц
9.7. Вычисление пакетных представлений
Итак, просмотры страниц нормализованы, а их дубликаты устранены. Теперь
можно перейти к написанию кода для вычисления пакетных представлений.
9.7.1. Количество просмотров страниц во времени
Подсчет количества просмотров страниц во времени разделяется на две ста¬
дии: сначала просмотры страниц подсчитываются со степенью детализации
до часа, а затем ежечасные их подсчеты накапливаются со всеми требующимися
степенями детализации по времени. Конвейерная схема первой стадии вычисле¬
ний приводится еще раз на рис. 9.6.
Глава 9. Пример реализации уровня пакетной обработки
Рис. 9.6. Подсчет количества просмотров страниц
во времени со степенью детализации до часа
Прежде всего определим функцию, в которой часовой промежуток времени
определяется по отметке времени:
public static class ToHourBucket extends CascalogFunction {
private static final int HOUR_IN_SECS = 60 * 60;
public void operate(FlowProcess process, FunctionCall call) {
int timestamp = call.getArguments().getlnteger(0);
int hourBucket = timestamp / HOUR_IN_SECS;
call. getOutputCollector () . add (new Tuple (hourBucket)) ;
}
}
Эта функция позволяет самым обыкновенным способом составить следующий
запрос ^аБса^ на определение ежечасных подсчетов:
public static Subquery hourlyRoHup () {
Tap source = new PailTap("/tmp/swa/unique_pageviews") ;
return new Subquery("Turl", "Thour-bucket", ”?count")
.predicate(source, "?pageview")
.predicate (new ExtractPageViewFields(), "?pageview") <h
.out("?url", "Ttimestamp")
Группирует I .predicate(new ToHourBucket (), "?timestamp")
по полям ?url i .out("?hour-bucket")
и ?hour-bucJcet L> .predicate (new Count (), "?count") ;
Повторно использует
приведенный ранее
код для извлечения
просмотров страниц
Как обычно, код запроса]Са.8сак^ довольно точно соответствует конвейерной
схеме. Конвейерная схема второй стадии вычислений приведена на рис. 9.7.
214
Часть I. Уровень пакетной обработки
Рис. 9.7. Подсчет количества просмотров страниц
во времени для всех степеней детализации по времени
Начнем с функции, получающей все степени детализации по заданному
часовому промежутку времени:
public static class EmitGranularities extends CascalogFunction {
public void operate(FlowProcess process, FunctionCall call) {
int hourBucket = call, get Arguments () .getlnteger (0) ;
int dayBucket = hourBucket /24;
int weekBucket = dayBucket / 7;
int monthBucket = dayBucket / 28;
Функция, порождающая
двухэлементные
кортежи для каждых
входных данных
call.getOutputCollector().add(new Tuple ("h", hourBucket)); <-
call.getOutputCollector().add(new Tuple ("d", dayBucket));
call.getOutputCollector().add(new Tuple("w", weekBucket));
call.getOutputCollector().add(new Tuple ("m", monthBucket));
}
Первый элемент кортежа
обозначает степень
детализации до часа (Ы, дня
Щ), недели (м) или месяца (ш),
а второй элемент — числовое
значение соответствующего
промежутка времени
Эта функция упрощает совокупный подсчет количества просмотров страниц
для всех степеней детализации, как демонстрируется в приведенном ниже коде.
public static Subquery pageviewBatchView() {
Subquery pageviews =
new Subquery("?url", U?granularity", "?bucket", "?total-pageviews"
Выполняет подзапрос
на ежечасные подсчеты
Порождает промежутки
времени для всех
степеней детализации
.predicate(hourlyRollup(), "?url", "?hour-bucket", "?count")
.predicate(new EmitGranularities(), "?hour-bucket")
—t' .out("?granularity", "?bucket")
.predicate(new Sum(), "?count").out("?total-pageviews"); ■>-
return pageviews;
Суммирует подсчеты количества просмотров
страниц по заданному URL, промежутку
времени и степени детализации
215
Глава 9. Пример реализации уровня пакетной обработки
9.7.2. Количество индивидуальных посетителей во времени
Количество индивидуальных посетителей во времени определяется таким же
образом, как и количество просмотров страниц, но вместо подсчета в данном
случае нужно создать множества HyperLogLog. Для вычисления этого пакетного
представления потребуются две новые специальные операции.
Первую операцию выполняет агрегатор, создающий множество НурегЬс^Ьс^
из последовательности идентификаторов пользователей следующим образом:
Функция генерирует множество
HyperLogLog для ряда посетителей
public static class ConstructHyperLogLog extends CascalogBuffer { о
public void operate(FlowProcess process, BufferCall call) {
HyperLogLog hll = new HyperLogLog (8192); <ь
Iterator<TupleEntry> it = call.getArgumentsIterator();
while(it.hasNext()) {
TupleEntry tuple = it.next();
hll.offer (tuple.getObject (0)); <i ,
} Вводит все объекты
try { 1
Создает множество HyperLogLog,
занимающее 1 Кбайт памяти
в множество
call. getOutputCollector () . add (new Tuple (hll. getBytes ()));
catch (IOException e) {
throw new RuntimeException(e) ;
Порождает
сохраняемые байты
объекта HyperLogLog
}
Следующая функция выполняет вторую операцию и представляет собой еще
один специальный агрегатор, объединяющий множества Нурег1л^Ьс^, получа¬
емые с ежечасной степенью детализации, во множества НурегЬс^Ьс^ за более
длительные промежутки времени:
public static class MergeHyperLogLog extends CascalogBuffer {
public void operate (FlowProcess process, BufferCall call) {
Iterator<TupleEntry> it = call.getArgumentsIterator();
HyperLogLog merged = null; <
try {
while(it.hasNext()) {
Создает новое множество
HyperLogLog для хранения
объединенных результатов
TupleEntry tuple = it.next();
byte[] serialized = (byte[]) tuple.getObject(O);
HyperLogLog hll = HyperLogLog.Builder.build(serialized); o-
if (merged— null) <j i объединяет текущее
merged - nil; { | множество в результаты
1 else {
merged = (HyperLogLog) merged.merge(hll); {
Восстанавливает
множество
HyperLogLog из
сохраняемых байтов
call.getOutputCollector () .add(new Tuple (merged.getBytes ())),
} catch (Exception e) {
throw new RuntimeException (e) ;
}
}
Порождает
сохраняемые байты
для объединенного
множества
}
216
Часть I. Уровень пакетной обработки
В приведенном ниже фрагменте кода обе упомянутые выше операции
выполняются для вычисления пакетного представления. Обратите внимание
на сходство этого кода с запросом на подсчет количества просмотров страниц
во времени.
public static void uniquesView () {
Tap source = new PailTapC'/tmp/swa/uniquejpageviews");
По первому подзапросу
определяются множества
Hypert.ogl.og по каждому
№1. с ежечасной
степенью детализации
Subquery hourlyUniques =
new Subquery("?url", "?hour-bucket", "?hyper-log-log")
.predicate(source, "?pageview")
.predicate(new ExtractPageViewFields(), "?pageview")
.out("?url", "?user", "?timestamp")
.predicate(new ToHourBucket(), "?timestamp")
.out("?hour-bucket")
.predicate(new ConstructHyperLogLog(), "?user")
—.out ("?hyper-log“log");
Subquery uniques =
new Subquery("?url", "?granularity", "?bucket", "?aggregate-hll")
.predicate(hourlyUniques,"?url", "?hour-bucket", "?hourly-hll")
.predicate(new EmitGranularities(), "?hour-bucket")
.out ('^granularity", "?bucket")
.predicate(new MergeHyperLogLog(), "Thourly-hll")
.out("Taggregate-hll");
return unioues • По второму подзапросу определяются
uniques, множества HyperLogLog для всех
степеней детализации по времени
Имеется также возможность создать функцию, абстрагирующую части, яв¬
ляющиеся общими для запросов на количество просмотров страниц и индиви¬
дуальных посетителей. Оставляем реализовать эту возможность читателям в ка¬
честве упражнения.
Дополнительная оптимизация пакетного представления,
вычисляемого по алгоритму НурегЬовЬов
В рассмотренной выше реализации для каждого множества НурегО^О^ выбирается
одинаковый размер 1 Кбайт. Такой размер множества Нурег1_о|и^ требуется для по¬
лучения приемлемо точного ответа на запросы по 1Ш1_ тех страниц, которые могут на¬
считывать миллионы и даже сотни миллионов посещений. Но большинство веб-сайтов,
анализируемых приложением SuperWebAnalytics.com, и близко не насчитают такого ко¬
личества просмотров страниц, и поэтому было бы расточительно использовать для них
столь большие множества НурегО^и^.
Для дополнительной оптимизации можно было бы выяснить общее количество просмо¬
тров страниц по отдельным ит в конкретном домене и соответственно откорректиро¬
вать размер множества Нуреги^1_о& Благодаря этому можно значительно сократить
пространство, требующееся в оперативной памяти для хранения пакетного представле¬
ния, хотя и за счет усложнения кода, формирующего это представление.
217
Глава 9. Пример реализации уровня пакетной обработки
9*7.3. Анализа показателя отказов от просмотра
И последним вычисляется пакетное представление для анализа показателя
отказов от просмотра страниц по каждому иИ!,. Рассмотрим еще раз конвейерную
схему этого представления, приведенную на рис. 9.8.
Рис. 9.8. Конвейерная схема для анализа показателя отказов от просмотра страниц
Главная роль в этом пакетном представлении принадлежит агрегатору Analyze
Visits, анализирующему все просмотры страниц, совершенные пользователем
в отдельном домене, и подсчитывающему как общее количество посещений, так
и количество посещений с отказом от просмотра страниц. Для этого проще все¬
го проанализировать просмотры страниц в отсортированном порядке. Так, если
между последовательными просмотрами страницы прошло больше 30 минут, это
означает, что началось новое ее посещение. А если посещение состоит лишь из
одного просмотра страницы, то оно считается с отказом от просмотра.
218
Часть /. Уровень пакетной обработки
Такая логика работы агрегатора реализована в приведенном ниже коде.
Агрегатор заключен в запрос, предоставляющий ему входные данные в отсорти¬
рованном порядке.
Два последовательных просмотра страницы относятся
к одному и тому же посещению, если они разделены
промежутком времени меньше 30 минут
public static class AnalyzeVisits extends CascalogBuffer {
private static final int VISIT LENGTH SECS = 60 * 30; <J
public void operate(FlowProcess process, BufferCall call)
Iterator<TupleEntry> it = call.getArgumentsIterator();
int bounces = 0;
int visits = 0;
Integer lastTime = null;
int numlnCurrVisit = 0;
Отслеживает
время предыдущего
просмотра страницы
{
<ъ
Допускает,
что просмотры
страниц
отсортированы
в хронологическом
порядке
Регистрирует
начало нового
посещения
while(it.hasNext()) {
TupleEntry tuple = it.next ();
int timeSecs = tuple.getlnteger(0);
if (lastTime == null || (timeSecs-lastTime) > VISIT_LENGTH_SECS) {
—о visits++;
if(numlnCurrVisit == 1)
bounces++;
Выясняет, было ли предыдущее
посещение с отказом от просмотра
numlnCurrVisit = 0;
}
numlnCurrVis it++ ;
}
if (numInCurrVisit==l) { <i
bounces++ • Выясняет, было ли последнее
посещение с отказом от просмотра
call.getOutputCollector().add(new Tuple(visits, bounces)); <
^ Порождает подсчеты
} посещений и отказов от просмотра
Прежде чем составлять подзапрос, реализуем требующуюся для него специ¬
альную функцию. Эта функция извлекает домен из заданного иКЦ как показано
ниже.
Использует
платформенно¬
ориентированные
библиотеки Java для
извлечения домена
public static class ExtractDomain extends CascalogFunction {
public void operate(FlowProcess process, FunctionCall call) {
String urlStr = call, get Arguments () .getString (0);
try {
—1> URL url = new URL (urlStr) ;
call.getOutputCollector().add(new Tuple(url.getAuthority()));
} catch(MalformedURLException e) {}
}
А теперь соберем все вместе, чтобы произвести анализ показателя отказов
от просмотра страниц, как демонстрируется в следующем коде:
Глоеа 9, Пример реализации уровня пакетной обработки 219
public static Subquery bounces View () {
Tap source = new PailTap("/tmp/swa/unique_j>ageviews") ;
Subquery userVisits = new Subquery("?domain", H?user",
"?num-user-visits",
"?num-user-bounces")
.predicate(source, n?pageview")
.predicate(new ExtractPageViewFields(), "?pageview")
.out(H?urln, "?user", "?timestamp")
.predicate(new ExtractDomain(), "?url")
.out("?domain")
.predicate(Option.SORT, "?timestamp") <
.predicate (new AnalyzeVisits (), "?timestamp")
.out("?num-user-visits", "Tnum-user-bounces"); ^
Subquery bounces = new Subquery("?domain", "?num-visits",
"?num-bounces")
.predicate(userVisits, "?domain",
Сортирует просмотры страниц
в хронологическом порядке
для анализа посещений.
Предикат Option. sort
позволяет контролировать
порядок сортировки каждой
группы, прежде чем передать
ее операциям агрегирования
Посещения страниц
и отказы от их просмотра
определяются по каждому
пользователю
"?num-user-visits", "?num-user-bounces")
.predicate(new Sum(), "?num-user-visits")
.out("?num-visits")
.predicate(new Sum(), "?num-user-bounces")
.out("?num-bounces")
return bounces; ,
}
Суммирует посещения страниц и отказы от просмотра
всех пользователей для вычисления пакетного
представления
Сделайте небольшой перерыв, чтобы перевести дух. После стольких трудов
вы успешно завершили реализацию повторных вычислений на уровне пакетной
обработки приложения SuperWebAnalytics.com!
Резюме
Для реализации уровня пакетной обработки приложения 8ире^еЬАпа1у^С8.
сот потребовалось не больше нескольких сотен строк кода, несмотря на всю
сложность его бизнес-логики. Различные абстракции вполне согласуются на этом
уровне, обеспечивая прямое соответствие реализации тем требованиям, кото¬
рые предъявлялись к каждой стадии процесса обработки данных. И хотя по ходу
дела, конечно, возникали некоторые трудности, обусловленные характером при¬
меняемых инструментальных средств, например, при обработке большого коли¬
чества мелких файлов в системе Набоор, их нетрудно было преодолеть.
В этой главе довольно подробно были рассмотрены особенности примене¬
ния отдельных инструментальных средств. Тем не менее сделаем небольшое
отступление, чтобы еще раз напомнить основные причины для построения
уровня пакетной обработки. Неизменяемость и возможность повторных вычис¬
лений на этом уровне обеспечивают требуемую отказоустойчивость — свойства,
обязательные для любой информационной системы. В этой главе было пока¬
зано, насколько просто написать код для получения пакетных представлений,
а такие трудно реализуемые свойства, как отказоустойчивость и параллельная
обработка, автоматически обеспечивает инфраструктура вычислений. Уровень
220
Часть /. Уровень пакетной обработки
пакетной обработки значительно упрощает задачу получения представлений
в реальном времени, поскольку такие представления требуется принимать во
внимание только для очень небольшой части всего массива данных. В дальней¬
шем, когда речь пойдет о сложностях, присущих вычислениям в реальном вре¬
мени, у вас будет возможность оценить по достоинству, насколько нестрогие
требования к задержке обработки данных на уровне пакетной обработки упро¬
щают оперирование его отдельными частями.
На этом часть I данной книги завершается. А в следующей ее части мы пере¬
йдем к рассмотрению уровня обслуживания , предназначенного для ускорения
операций чтения пакетных представлений в режиме произвольного доступа.
Часть II
Уровень обслуживания
ч
Аасть 2 данной книги посвящена уровню обслуживания лямбда-архитектуры.
Уровень обслуживания состоит из баз данных, где индексируются и обсуживают¬
ся результаты, получаемые нд уровне пакетной обработки. Эта часть коротка, по¬
тому что базы данных, не требующие записи с произвольным доступом, чрезвы¬
чайно просты. Так, в главе 10 обсуждаются самые общие принципы организации
уровня обслуживания, тогда как в главе 11 демонстрируется пример реализации
базы данных под названием ЕкрЬаШБВ на уровне обслуживания.
обслуживания
В этой главе...
■ Приспосабливание пакетных представлений
к тем запросам, которые они обслуживают.
■ Новый ответ на полемику по поводу
нормализации и денормализации данных.
■ Преимущества баз данных с пакетным
непроизвольным способом записи, но с
произвольным способом чтения.
■ Сравнение лямбда-архитектуры с полностью
инкрементным решением.
Мы рассмотрели особенности предварительного вычисления произвольных
представлений любого массива данных на уровне пакетной обработки. Чтобы
сделать представления полезными, необходимо иметь доступ к их содержимому
с малой задержкой, и эта роль, как показано на рис. 10.1, принадлежит уровню
обслуживания. На этом уровне индексируются представления и предоставляются
интерфейсы для быстрого запрашивания предварительно вычисленных данных.
Уровень обслуживания является последней составляющей пакетной части
лямбда-архитектуры. Он тесно связан с уровнем пакетной обработки, посколь¬
ку последний отвечает за непрерывное обновление представлений на уровне
обслуживания. Эти представления всегда будут неактуальными в силу большой
задержки, присущей пакетным вычислениям. Но этот недостаток устраняется
на уровне ускорения, отвечающем за любые данные, еще недоступные на уровне
обслуживания.
224
Часть II. Уровень обслуживания
( Уровень укоренив
Представление
я ареальном
г времени
Представление
я в реальном
Т времени
Представление
я в реальном
г времени
Уровень пакетной обработки
Главный массив данных
О Время, требующееся для
обработки главного массива
данных, служит причиной того,
что пакетные представления
всегда остаются неактуальными
Уровень обслуживания
ч
сГЛ\ Пакетное
3 пред-
^-^ставление
Пакетное
С ОЬ пред-
^-пставление
сгу\ Пакетное
С О Э Пред-
^-^ставление
ч
±
У
> На уровне ускорения
обслуживаются последние
данные, не включенные
в пакетные представления
© На уровне обслуживания пред¬
варительно вычисленные
результаты обрабатываются
в реальном времени
Рис.10.1. В лямбда-архитектуре на уровне обслуживания предоставляется доступ с малой
задержкой к результатам пакетных вычислений, выполненных над главным массивом данных.
Представления на уровне обслуживания становятся не совсем актуальными из-за времени,
требующегося для выполнения пакетных вычислений
К сожалению, уровень обслуживания является именно тем местом, где прак¬
тика не поспевает за теорией. Реализовать уровень обслуживания общего назна¬
чения совсем не трудно. В действительности сделать это было бы значительно
проще, чем построить любую из ныне существующих нереляционных распреде¬
ленных баз данных типа >1о8С)Ь. Поэтому рассмотрим сначала теорию, положен¬
ную в основу создания простого, масштабируемого, отказоустойчивого уровня
обслуживания общего назначения, а затем воспользуемся наилучшими из имею¬
щихся в нашем распоряжении инструментальных средств, чтобы продемонстри¬
ровать основные понятия и принципы.
Исследуя уровень обслуживания, мы рассмотрим следующие вопросы.
■ Стратегии индексирования для сведения к минимуму задержки, используе¬
мых ресурсов и расхождений.
■ Требования к уровню обслуживания в лямбда-архитектуре.
■ Особенности разрешения на уровне обслуживания проблемы выбора меж¬
ду нормализацией или денормализацией данных, уже давно вызывающей
немалые споры.
Глава 10. Организация уровня обслуживания
225
Итак, начнем с исследования главных вопросов, которые придется решать,
структурируя представление на уровне обслуживания.
10.1. Количественные показатели
производительности на уровне обслуживания
Как и уровень пакетной обработки, уровень обслуживания распределен сре¬
ди многих машин для целей масштабирования. Индексы создаются, загружаются
и обрабатываются на уровне обслуживания распределенным способом.
При построении этих индексов необходимо принимать во внимание следую¬
щие главные количественные показатели: производительность и задержка. В дан¬
ном контексте задержка, — это время, требующееся для ответа на один запрос,
тогда как производительность — количество запросов, которые могут быть обслу¬
жены в течение заданного периода времени. Взаимосвязь между структурой ин¬
дексов на уровне обслуживания и их количественными показателями лучше все¬
го пояснить на конкретном примере.
Вернемся на время к примеру приложения SuperWebAnalyLics.com, рассма¬
триваемого на протяжении всей данной книги. В частности, рассмотрим запрос
на подсчет количества просмотров страниц во времени. Назначение этого за¬
проса — обслужить количество просмотров страниц за каждый час по заданному
иЯЬ в конкретном часовом промежутке времени. Чтобы еще больше упростить
обсуждение, допустим, что подсчеты количества просмотров страниц произво¬
дятся со степенью детализации только до часа. Получающееся в итоге представ¬
ление будет выглядеть так, как показано на рис. 10.2.
ит
Промежуток времени
Количество
просмотров страниц
foo.eom/blog/1
0
10
foo.eom/blog/1
1
21
foo.eom/blog/1
2
7
foo.eom/blog/1
3
38
foo.eom/blog/1
4
29
bar.eom/post/a
0
178
bar.eom/post/a
1
91
bar.eom/post/a
2
568
Рис. 10.2. Пакетное представление количества просмотров страниц
во времени со степенью детализации до часа
Простой способ индексирования этого представления состоит в применении
пар “ключ-значение”, где ключами служат пары [1ЖЬ, час], а значениями - про¬
смотры страниц. В таком случае индекс может быть разделен по ключу, чтобы
разместить подсчеты количества просмотров страницы по одному и тому же 1т1и,
в разных разделах. А поскольку разные разделы существуют на отдельных серве¬
рах, то для выборки отдельного промежутка времени в часах по заданному
придется извлечь значения из нескольких серверов на уровне обслуживания.
226
Часть II, Уровень обслуживания
Несмотря на то что такой подход в принципе работоспособен, он страдает
серьезными недостатками как с точки зрения задержки, так и производитель¬
ности. Прежде всего, задержка неизменно велика. Значения по отдельному
распределяются по всему кластеру машин, и поэтому потребуются запросы цело¬
го ряда серверов, чтобы получить подсчеты количества просмотров страниц за
большой промежуток времени в часах. Но, как показывает опыт, время реакции
серверов разнится. Например, один сервер может быть загружен немного боль¬
ше, чем остальные, а другой — выполнять в это же время сборку “мусора”. И даже
если распараллелить запросы на выборку, то общее время реакции на запросы
будет ограничено быстродействием самого медленного сервера.
Чтобы проиллюстрировать это положение, допустим, что по запросу требует¬
ся извлечь данные из трех серверов. Характерный пример распределения време¬
ни реакции на этот вопрос приведен на рис. 10.3.
Время реакции
отдельных серверов
-О
-О—О
Задержка
Общее время
реакции на запрос
Рис. 10.3. При распределении задачи по нескольким серверам общая задержка
определяется временем реакции медленнее всего работающего сервера
Для сравнения допустим, что запрос направляется 20 серверам. На рис. 10.4
приведено типичное распределение задержек реакции серверов на этот запрос.
Время реакции
отдельных серверов
о сзэсгххххосю-оо-о—о—о-
Задержка
Общее время
реакции на запрос
Рис. 10.4. Если увеличить количество серверов, участвующих в решении распределенной задачи,
то одновременно увеличится вероятность замедления реакции, по крайней мере, одного сервера
В общем, чем больше серверов обрабатывают запрос, тем больше общая
задержка его обработки. Это объясняется тем простым фактом, что при уве¬
личении количества задействованных серверов возрастает вероятность того,
что, по крайней мере, один из них будет реагировать на запрос медленно.
Следовательно, разброс времени реакции серверов приводит к тому, что произ¬
водительность самого медленного сервера становится общей их производитель¬
ностью при обработке запросов. Это становится серьезным препятствием в до¬
стижении приемлемой задержки при обработке запроса на подсчет количества
просмотров страниц.
Еще одно затруднение, возникающее при применении пар “ключ-значение”,
связано со слабой производительностью, особенно если на серверах применяют¬
ся дисковые, а не твердотельные накопители. Для извлечения значения по одно¬
му ключу требуется поиск информации па дисках, а ведь для обработки одного
Глава 10. Организация уровня обслуживания
227
запроса могут быть извлечены значения по десяткам и даже большему количе¬
ству ключей. Поиск информации на традиционных дисковых накопителях явля¬
ется затратной операцией, а поскольку в кластере машин имеется ограниченное
количество дисковых накопителей, то на число операций поиска информации,
совершаемых в секунду, накладываются определенные ограничения. Допустим,
в среднем на каждый запрос приходятся выборки по 20 ключам, в кластере име¬
ется 100 дисков и на каждом диске допускается выполнять 500 операций поиска
информации в секунду. В данном случае кластер способен обслуживать 2500 за¬
просов в секунду — необычно малое количество, принимая во внимание количе¬
ство дисковых накопителей в кластере.
Но не все еще потеряно. Если применить другой способ индексирования,
то можно существенно улучшить показатели задержки и производительности.
Этот способ состоит в том, чтобы размещать информацию о просмотрах стра¬
ниц по каждому иЯЬ в одном и том же разделе, сохраняя ее последовательно.
И тогда для извлечения информации о просмотрах страниц потребуется един¬
ственная операция поиска и просмотра вместо многочисленных операций по¬
добного рода. Операции просмотра информации намного менее затратные, чем
операции поиска, а следовательно, они значительно эффективнее потребляют
ресурсы. Кроме того, в обработке каждого запроса может быть задействован
лишь один сервер, а следовательно, устраняются недостатки, связанные с раз¬
ным временем реакции отдельных серверов. Распределение индекса по данному
способу показано на рис. 10.5.
Подсчеты количества
просмотров страниц
по каждому иРЯ
хранятся вместе
и сортируются
в хронологическом,
порядке
Рис. 10.5. Отсортированный индекс способствует преодолению ограничений,
накладываемых на поиск и просмотр информации на дисках,
благодаря чему улучшаются показатели задержки и производительности
Оба рассмотренных выше примера наглядно показывают, что способ струк¬
турирования индексов па уровне обслуживания оказывает существенное вли¬
яние на производительность при обработке запросов. Важное преимущество
лямбда-архитектуры заключается в том, что она позволяет приспосабливать
foo.eom/blog/1
bar.com/post/abc
baz.net/pageZa
Час
Количество
просмотров
страниц
0
123
1
101
2
278
3
176
4
133
0
123
1
101
2
278
0
176
1
133
2
97
228
Часть 11. Уровень обслуживания
уровень обслуживания к обрабатываемым запросам, чтобы оптимизировать эф¬
фективность ее применения.
10.2. Уровень обслуживания как решение
проблемы выбора между нормализацией
или денормализацией данных
Уровень обслуживания позволяет решить проблему, издавна существовавшую
в области реляционных баз данных: выбор между нормализацией или денорма¬
лизацией данных. Чтобы принять правильное решение и оценить последствия
его принятия, необходимо сначала выяснить основные вопросы, которые оно
призвано решить.
Проблема выбора между нормализацией или денормализацией данных
в конечном итоге сводится к выбору между неприемлемыми компромиссами.
Информацию в реляционных базах данных требуется хранить полностью нор¬
мализованной. Это подразумевает определение взаимосвязи между независимы¬
ми массивами данных для сведения избыточности к минимуму. К сожалению, за¬
прашивание нормализованных данных может происходить медленно, и поэтому
некоторую информацию, возможно, придется хранить избыточно ради сокраще¬
ния времени реакции. Такой процесс денормализации способствует повышению
производительности, хотя и за счет осложнений, связанных с необходимостью
поддерживать согласованность избыточных данных.
Чтобы проиллюстрировать суть данной проблемы, допустим, что информа¬
ция о месте жительства пользователей хранится в таблицах реляционной базы
данных, как показано на рис. 10.6. У каждого места жительства имеется свой
идентификатор, а у каждого пользователя — один из этих идентификаторов
для обозначения их места жительства. Для обработки запроса на извлечение ин¬
формации о месте жительства отдельного пользователя придется соединить две
таблицы. Это пример полностью нормализованной схемы, где информация нс
хранится избыточно.
Location ID
City
State
Population
1
New York
NY
8.2M
2
San Diego
CA
1.3M
3
Chicago
IL
2.7M
User ID
Name
Location ID
1
Sally
3
2
George
1
3
Bob
3
Рис. 10.6. Нормализованная схема нескольких независимых массивов данных
с незначительной или полностью отсутствующей их избыточностью
А теперь допустим, что извлечение информации о городе и штате для поль¬
зователя оказывается операцией, очень часто выполняемой в приложении. Но
поскольку соединения затратны, то принимается решение повысить каким-то
образом эффективность данной операции. Избежать соединения можно лишь
в том случае, если хранить информацию о городе и штате избыточно в таблице
с данными пользователя. Такой способ избыточного хранения информации с це¬
лью избежать соединений называется денормализацит, а подходящая для этого
случая схема приведена на рис. 10.7.
Глава 10. Организация уровня обслуживания
User ID
Name
Location ID
City
State
1
Sally
3
Chicago
IL
2
George
1
New York
NY
3
Bob
3
Chicago
IL
Location ID
City
State
Population
1
New York
NY
8.2M
2
San Diego
CA
1.3M
3
Chicago
IL
2.7M
Рис. 10.7. В денормализированных таблицах данные хранятся избыточно
ради повышения эффективности обработки запросов
Денормализация является далеко не идеальным решением, поскольку разра¬
ботчик приложения отвечает за согласованность всех избыточных данных. В свя¬
зи с этим возникают следующие неудобные вопросы: что произойдет, если раз¬
ные копии поля окажутся несовместимыми, и какова семантика данных в этом
случае? Напомним, что в долгосрочных системах ошибки неизбежны, поэтому
в них со временем так или иначе возникают несогласованности.
К счастью, отделение главного массива данных от уровня обслуживания
в лямбда-архитектуре разрешает проблему выбора между нормализацией и де¬
нормализацией. Главный массив данных можно нормализовать на уровне па¬
кетной обработки по своему усмотрению. При вычислении на уровне пакетной
обработки чтение из главного массива данных происходит в массовом порядке,
что избавляет от необходимости составлять схему для оптимизации операций
чтения с произвольным доступом. Кроме того, уровень обслуживания полностью
приспособлен к обрабатываемым запросам, что дает возможность оптимизиро¬
вать его функционирование для достижения максимальной производительности.
Подобные виды оптимизации на уровне обслуживания могут выходить далеко за
рамки денормализации. Помимо предварительного соединения данных, можно
выполнить дополнительное агрегирование и преобразование для еще большего
повышения эффективности.
Что же касается вопроса совместимости данных в лямбда-архитектуре, то со¬
вершенно справедливо, чтобы информация избыточно сохранялась между уров¬
нями пакетной обработки и обслуживания. Главное отличие состоит в том, что
уровень обслуживания определяется как функция главного массива данных. Если
ошибка вносит несогласованность в данные, последнюю нетрудно устранить, вы¬
полнив вычисление заново на уровне обслуживания.
10.3. Требования к базе данных на уровне обслуживания
Лямбда-архитектура выдвигает ряд требований к базе данных уровня обслужи¬
вания. Но намного интереснее выяснить то, что н*? требуется от этой базы данных.
Прежде всего, к ней предъявляются следующие требования.
■ Пакетная запись данных. Пакетные представления получаются для уров¬
ня обслуживания с самого начала. Как только станет доступной новая
™ Часть //. Уровень обслуживании
версия представления, должна быть возможность полностью заменить ею
прежнюю версию.
■ Масштабируемость. База данных уровня обслуживания должна быть способ¬
на обрабатывать представления произвольного размера. Для этого она долж¬
на быть распределена среди многих машин аналогично рассматривавшимся
ранее распределенным файловым системам и каркасу пакетных вычислений.
■ Чтение с произвольным доступом. База данных уровня обслуживания
должна поддерживать чтение с произвольным доступом, а индексы — пре¬
доставлять прямой доступ к небольшим частям представления. Это требо¬
вание обусловлено необходимостью иметь малую задержку при обработке
запросов.
■ Отказоустойчивость. В силу того что база данных уровня обслуживания яв¬
ляется распределенной, она должна быть устойчива к аппаратным отказам.
Как видите, в этих требованиях нет ничего особенного. Но в них отсутству¬
ет традиционное требование, являющееся стандартным для всех известных баз
данных и состоящее в поддержке записи данных с произвольным доступом. Такие
функциональные возможности не имеют никакого отношения к уровню об¬
служивания, поскольку представления получаются только в массовом порядке.
Откровенно говоря, операции записи с произвольным доступом все же имеют
место в лямбда-архитектуре, но они обособлено выполняются на уровне уско¬
рения для достижения малой задержки в ходе обновления. А при обновлении
уровня обслуживания формируются совершенно новые представления, и поэто¬
му от базы данных уровня обслуживания не требуется способность модифициро¬
вать небольшие части текущего представления.
И это замечательный результат, поскольку операции записи с произвольным
доступом являются причиной большинства осложнений, возникающих в базах дан¬
ных, а тем более — в распределенных базах данных. Рассмотрим в качестве примера
одну из неприятных и обсуждавшихся в главе 1 особенностей работы баз данных
при записи информации с произвольным доступом: необходимость уплотнения дан¬
ных для высвобождения неиспользуемого места. Операция уплотнения выполняет¬
ся интенсивно, иногда потребляя немало машинных ресурсов. Если она проводит¬
ся неверно, то машины оказываются перегруженными, что может стать причиной
каскадных отказов по мере переноса нагрузки с одной машины на другую.
На уровне обслуживания операции записи с произвольным доступом не тре¬
буются, а следовательно, не требуется и оперативное уплотнение данных. Таким
образом, данное осложнение и связанное с ним эксплуатационное бремя полно¬
стью устраняются на уровне обслуживания. И это тем более важно, если при¬
нять во внимание относительные размеры кластеров на уровнях обслуживания и
ускорения. На уровне обслуживания отображаются представления для большей
части (возможно, даже свыше 99%) главного массива данных, что отнимает боль¬
шинство ресурсов базы данных. Это означает, что большая часть серверов базы
данных не страдает от эксплуатационного бремени, налагаемого на проведение
оперативного уплотнения данных.
Оперативное уплотнение данных представляет собой лишь одно из многих ос¬
ложнений, возникающих в связи с необходимостью поддерживать в базе данных
Глава 10. Организация уровня обслуживания
231
операции записи с произвольным доступом. Еще одно осложнение связано с не¬
обходимостью синхронизировать операции чтения и записи, чтобы исключить
чтение наполовину записанных значений. Если операции записи с произволь¬
ным доступом в базе данных не выполняются, она может оптимизировать путь
чтения данных и тем самым повысить свою производительность по сравнению
с базой данных, поддерживающей чтение и запись с произвольным доступом.
Характерным, хотя и не совсем точным признаком сложности может служить
размер кодовой базы. Так, для построения базы данных ElephantDB, специально
предназначенной для уровня обслуживания, потребовалось всего лишь несколь¬
ко тысяч строк кода, тогда как для построения двух популярных распределен¬
ных баз данных чтения-записи IIBase и Cassandra — сотни тысяч строк кода.
Количество строк кода обычно не служит удобным количественным показате¬
лем, но в данном случае оно явно свидетельствует о разительном отличии.
Чем проще база данных, тем более она предсказуема, поскольку выполняет
меньше функций. Соответственно в ней менее вероятны программные ошибки,
и она оказывается более простой в эксплуатации, как следует из приведенного
выше примера уплотнения данных. Представления на уровне обслуживания со¬
держат самые разнообразные запрашиваемые данные, и поэтому принципиаль¬
ная простота уровня обслуживания является сущим благом для обеспечения на¬
дежности архитектуры в целом.
10.4. Проектирование уровня обслуживания
для приложения SuperWebAnalytics.com
А теперь обратимся к примеру приложения SuperWebAnalytics.com, что¬
бы разработать для него уровень обслуживания. Мы оставили этот пример
на том, что разработали процесс пакетной обработки данных для приложения
SuperWebAnalytics.com, создав пакетные представления для следующих трех
запросов: количества просмотров страниц во времени, количества индивиду¬
альных посетителей во времени и анализа показателя отказов от просмотра
страниц. Выходные данные на уровне пакетной обработки не индексированы,
поскольку это задача уровня обслуживания, где упомянутые представления ин¬
дексируются и обрабатываются с малой задержкой.
Наша цель — спроектировать уровень обслуживания, идеально подходящий
для приложения SuperWebAnalytics.com. Именно на уровне обслуживания боль¬
ше, чем где-либо еще в лямбда-архитектуре, настоящие инструментальные сред¬
ства явно не поспевают за идеальными. По иронии судьбы базы данных уровня
обслуживания относятся к самым простым из всех инструментальных средств,
требующихся для лямбда-архитектуры. На наш взгляд, это ооъясняется историче¬
ской инерцией. Ведь большинство разработчиков создают приложения, которые
обслуживаемые единственным монолитным кластером базы данных, который
постепенно обновляется в реальном времени. Но очень важно выяснить, чего
же в идеале можно достичь на пути к инструментальным средствам будущего. А
на практике традиционные базы данных обычно переориентируются для уровня
обслуживания. Итак, рассмотрим идеальные типы индексов для каждого пред¬
ставления в приложении SuperWebAnalytics.com.
Часть Ч. Уровень обслуживания
гм
10.4.1. Количество просмотров страниц во времени
По запросу на подсчет количества просмотров страниц во времени извлекают-
ся и суммируются подсчеты просмотров страниц по заданному иКЬ за указанный
период времени в часах. Как обсуждалось ранее, идеальным индексом для такого
запроса может служить ключ к отсортированному отображению (см. рис. 10.5).
Напомним, что пакетное представление для количества просмотров страниц
во времени производит подсчеты за определенный период времени со степенью
детализации не только до часа, но и до одного дня, недели, месяца и года. Это
было сделано с целью свести к минимуму общее количество значений, которые
должны быть извлечены для разрешения запроса. Так, для подсчетов за период
в один год потребуется извлечь тысячи часовых промежутков времени, но лишь
с десяток промежутков времени при меньшей степени детализации по времени.
А если воспользоваться ключом к отсортированному отображению в качестве
типа индекса, то окажется, что меньшая степень детализации по времени вооб¬
ще не нужна. Ведь если все значения из заданного периода времени хранятся
последовательно, то прочитать их все сразу будет совсем не затратно.
Допустим, для размещения каждой записи в отсортированном отображении
промежутков времени на подсчеты количества просмотров страниц требуется
12 байт (4 байта для номера промежутка времени и 8 байт для значения). Чтобы
извлечь такие подсчеты за двухлетний период времени, потребуется около 17500
значений. Если сложить все эти подсчеты вместе, то потребуется извлечь объем
данных порядка 205 Кбайт. Это небольшой объем данных, и поэтому лучше оп¬
тимизировать данный процесс, чтобы обойтись лишь одной операцией поиска,
даже если в целом потребуется прочитать больше информации.
Разумеется, этот анализ делается с учетом характеристик современных нако¬
пителей на жестких дисках. Если же произвести анализ с учетом твердотельных
накопителей или других более быстродействующих запоминающих устройств,
то он может привести к другому выводу: в индекс предпочтительнее включать
степень детализации по времени.
10ш4.2. Количество индивидуальных посетителей страниц во времени
А теперь обсудим идеальный индекс для подсчета количества индивидуальных
посетителей страниц во времени (рис. 10.8). Запросы на количество индивиду¬
альных посетителей страниц и просмотров страниц во времени очень похожи
тем, что единственное объединенное значение извлекается, исходя из диапа¬
зона значений. А одно из главных отличий заключается в том, что множества
НурегЬод!х^, применяемые для подсчета индивидуальных посетителей страниц,
оказываются значительно более крупными, чем массивы значений, хранящиеся
для подсчета количества просмотров страниц за определенные промежутки вре¬
мени. Так, если составить отсортированный индекс, содержащий степень дета¬
лизации только до часа, а размер множества НурегЬх^Ь^ окажется равным 1024
байт, то в конечно итоге придется извлечь около 17 Мбайт информации по за¬
просу за двухлетний период времени. Если накопитель па жестких дисках позво¬
ляет считывать данные со скоростью 300 Мбайт/с, то для чтения такого объема
информации потребуется 60 мс (и это в совершенно идеальных условиях). Кроме
Ело** Ю. Организация уровня обслуживания
233
того, слияние множеств Нурег1х^1^ оказывается более затратной операцией,
чем простое суммирование чисел, потенциально увеличивая задержку обработки
запросов. А поскольку обработке запросов на количество индивидуальных посе¬
тителей во времени присущи большие затраты ресурсов, чем обработке запросов
на количество просмотров страниц во времени, то в данном случае лучше произ¬
водить вычисления с меньшей степенью детализации по времени.
Рис. 10.8. Составление индекса для подсчета количества индивидуальных посетителей
страниц во времени. Несмотря на то что ключи этого индекса состоят из 1)РЬ
и степени детализации по времени, они распределяются среди серверов только по 1)РЬ
В данном случае индекс, аналогичный представленному на рис. 10.8, кажется
оптимальным. Он похож на индекс в виде ключа к отсортированному отображе¬
нию, применяемый для подсчета количества просмотров страниц, кроме следу¬
ющих отличий.
■ Ключ состоит из иЛЬ и степени детализации по времени.
■ Индексы разделяются только по иЛЬ, а не по иЛЬ и степени детализации
по времени. Чтобы извлечь диапазон значений по и ЛЬ и степени детали¬
зации по времени, сначала следует воспользоваться и ЛЬ с целью найти
сервер, содержащий нужную информацию, а затем воспользоваться как
1Л1Ь, так и степенью детализации по времени для поиска интересующих
значений. Разделение только по ЬЛЬ гарантирует, что все промежутки
времени для данного и ЛЬ размещаются на одном и том же сервере, исклю¬
чая любые отклонения, связанные с необходимостью взаимодействовать
со многими серверами при обработке одного запроса.
10.4.3. Анализ показателя отказов от просмотра страниц
Представление для анализа показателя отказов от просмотра страниц обе¬
спечивает преобразование из домена в количество посещений и количество
отказов от просмотра страниц в этом домене. Организовать поддержку такого
Часть II. Уровень обслуживания
представления проще всего, поскольку для этого требуется только индекс В виде
пары “ключ-значение”, как показано на рис. 10.9.
foo.com
[Число посетителей,
Число отказов от просмотра]
bar.com
[5067,3212]
[100, 11]
baz.com
[7283736,3849930]
cha.com
[8812,6123]
Рис. 10.9. Реализация представления для анализа показателя отказов
от просмотра страниц с помощью индекса в виде пары "ключ-значение'’
10.5. Сравнение лямбда-архитентуры
с полностью инкрементным решением
В ряде предыдущих глав было показано, каким образом строятся уровни пакет¬
ной обработки и обслуживания для приложения SuperWebAnalytics.com. С помощью
такой модели вычисления представлений в виде функции всех данных сделать все
необходимое было нетрудно. Чтобы оценить по достоинству превосходные свой¬
ства такой архитектуры, целесообразно сравнить ее с традиционной архитектурой,
построенной на основе полностью инкрементных вычислений. В традиционной
архитектуре применяется большая база данных чтения-записи для хранения и под¬
держания текущего состояния по мере поступления новых данных.
Представляя лямбда-архитектуру в главе 1, мы сравнивали ее с традиционным
решением на примере подсчета количества индивидуальных посетителей стра¬
ниц во времени. А теперь, когда разъяснено немало понятий, мы можем прове¬
сти это сравнение более подробно. Сначала мы представим хорошо известное,
полностью инкрементное решение задачи подсчета количества индивидуальных
посетителей страниц во времени, а затем покажем, что такое решение оказыва¬
ется более сложным для реализации, значительно менее точным, ухудшающим
показатели задержки и пропускной способности и требующим специального обо¬
рудования для своего осуществления.
10.5.1. Полностью инкрементное решение задачи подсчета
количества индивидуальных посетителей страниц во времени
Попробуем выработать самое лучшее из возможных полностью инкремент¬
ных решений данной задачи, действуя поэтапно. Чтобы начать этот процесс,
упростим задачу, полностью проигнорировав эквивалентами в первоначальном
решении. Это даст возможность получить ценные сведения о намного более
трудной задаче определения количества индивидуальных посетителей с учетом
эквивалентов.
Следует иметь в виду, что в решении дайной задачи мы не ограничиваемся
инструментальными средствами, имеющимися в нашем распоряжении, допуская
любые отклонения от них. Прежде всего нас интересует, насколько удачным или
Глава М. Организация уровня обслуживания
235
неудачным окажется инкрементное решение, выработанное с помощью самых
лучших инструментальных средств.
Рассматриваемое здесь решение является полностью инкрементным, и поэ¬
тому самое главное для решения поставленной задачи — определить тип исполь¬
зуемой базы данных и порядок поддержания текущего состояния в ней. С этой
целью попробуем сначала воспользоваться базой данных ключей к множеству.
Эта база данных реализует следующий интерфейс:
interface KeyToSetDatabase {
Set getSet(Object key);
void addToSet (Object key, Object val) ;
}
Такая база данных вполне может существовать, а также стать распределенной
и отказоустойчивой. Предпочтение такому типу базы данных над базой данных
пар “ключ-значение” отдано для того, чтобы сделать эффективной операцию
addToSet (). Из базы данных пар “ключ-значение” пришлось бы извлекать все
множество, вводить в него элемент, а затем записывать все множество обратно.
А при наличии базы данных, которой присуще распознавать сохраняемую струк¬
туру данных, подобную операцию можно выполнить намного более эффективно,
передав ей только элемент, который требуется ввести в множество.
Любой полностью инкрементный подход разделяется на две части: выясне¬
ние происходящего, когда получается информация о новом просмотре страницы
(на стадии записи), а также определение вычисления, необходимого для разре¬
шения запроса (на стадии чтения). Так, на стадии записи в базе данных будет
установлен ключ к паре [URL, часовой промежуток времени], а значением будет
множество всех идентификаторов пользователей, посетивших страницу по дан¬
ному URL за указанный часовой промежуток времени. Всякий раз, когда получа¬
ется информация о новом просмотре страницы, идентификатор пользователя
добавляется к соответствующему промежутку времени в базе данных. А на ста¬
дии чтения запросы разрешаются извлечением всех промежутков времени в пре¬
делах, указанных в запросе, слиянием множеств и подсчетом индивидуальных
посетителей в результирующем множестве.
Несмотря на всю простоту такого подхода, он таит в себе немало трудностей,
в том числе следующие.
■ База данных получается довольно большой, поскольку практически каж¬
дый просмотр страницы приходится хранить в базе данных.
■ Для обработки запроса в больших пределах придется выполнить очень
много операций поиска в базе данных. Например, период времени в один
год состоит из 8760 часовых промежутков времени. Извлечение 8760 часо¬
вых промежутков времени вряд ли благоприятствует быстрой обработке
запросов.
■ Для популярных веб-сайтов даже отдельные промежутки времени могут
состоять из миллионов, а то и более элементов. И это вряд ли благоприят¬
ствует быстрой обработке запросов.
Попробуем применить другой подход, чтобы существенно сократить объ¬
ем работы, которую необходимо выполнить при обработке запросов. С этой
236
Чисть Ц, Уровень обслуживания
целью воспользуемся алгоритмом HyperLogLog, чтобы аппроксимировать уста-
новленный подсчет и тем самым значительно сократить требующуюся емкость
хранилища. При таком походе применяется база данных ключей к множеству
HyperLogLog, которая совсем не обязательно должна быть распределенной и от¬
казоустойчивой. На самом деле она будет мало чем отличаться от базы данных
вроде Apache Cassandra.
Как и прежде, ключом будет пара [URL, часовой промежуток времени], а зна¬
чением — множество HyperLogLog, представляющее идентификаторы всех поль¬
зователей, посетивших страницу по данному URL за указанный час. На стадии
записи идентификатор пользователя просто вводится в множество HyperLogLog
соответствующего промежутка времени, а на стадии чтения сначала извлекают¬
ся все множества HyperLogLog в заданных пределах, затем они объединяются,
и далее производится подсчет.
Благодаря существенной экономии места, которую обеспечивает алгоритм
HyperLogLog, такой подход оказывается более эффективным. Отдельные про¬
межутки времени теперь получаются гарантированно короткими, а база данных
в целом намного экономнее расходует свободное место. И все это достигается за
счет весьма умеренного снижения точности при обработке запросов.
Но такой подход по-прежнему вызывает трудности при обработке запросов
в больших пределах, когда приходится выполнять неприемлемо большое коли¬
чество операций поиска в базе данных. А ведь такие запросы желательно выпол¬
нять так же быстро, как и запросы в небольших пределах.
Правда, устранить подобное затруднение совсем не трудно. Как и в послед¬
нем случае, воспользуемся для этой цели базой данных ключей к множеству
HyperLogLog, но на этот раз изменим ключ, сделав его триадой [URL, часовой
промежуток времени, степень детализации по времени]. Это означает, что вме¬
сто вычисления множеств HyperLogLog со степенью детализации только до часа
такое вычисление производится с меньшей степенью детализации до дня, неде¬
ли, месяца или года.
Всякий раз, когда поступают новые данные о просмотре страниц, на стадии
записи идентификатор пользователя вводится в множество HyperLogLog для ча¬
сового, дневного, недельного и годового промежутка времени соответственно.
А на стадии чтения для получения результата считывается минимальное ко¬
личество промежутков времени. Например, по запросу за период с 1 декабря
2013 года по 4 февраля 2015 года потребуются следующие промежутки времени.
■ Декабрь месяц 2013 года.
■ 2014 год.
■ Январь месяц 2015г.
■ Дни с 1 до 3 февраля 2014 г.
Это значительное усовершенствование по сравнению с тысячами промежут¬
ков времени, которые требовалось прочитать в больших пределах при преды¬
дущем подходе. Такой подход сродни тем, что приняты па уровне пакетной об¬
работки для представлений в приложении SuperWebAnalytics.com. Как было по¬
казано ранее, затраты на хранение при дополнительных степенях детализации
Гл*** 10. Организация уровня обслуживания
237
но времени минимальны, и поэтому вполне допустимо пойти на столь умеренное
увеличение объема хранилища ради ускорения обработки всех запросов.
В целом такой подход к решению рассматриваемой здесь задачи можно счи¬
тать вполне удовлетворительным, поскольку он обеспечивает быстроту обработ¬
ки всех запросов, эффективное использование свободного места, простоту и яс¬
ность реализации. А теперь усложним задачу, введя эквиваленты, чтобы увидеть,
насколько радикально это меняет дело. Решить эту задачу в полностью инкре¬
ментной архитектуре намного труднее, а полученное в итоге решение оказыва¬
ется неудовлетворительным.
Как упоминалось ранее, обращение с эквивалентами усложняется тем, что
новый эквивалент может изменить результат обработки практически любого за¬
проса. Вернемся, например, к первому подходу, где множество идентификаторов
пользователя сохранялось для каждой пары [1ЖЬ, часовой промежуток вре¬
мени]. Допустим, что во всей базе данных требуется хранить лишь по одному
идентификатору на каждого пользователя, а при каждом появлении нового экви¬
валента — принимать меры к тому, чтобы во всей базе данных хранился только
один идентификатор данного пользователя. Пример того, как будет в этом случае
выглядеть база данных, приведен на рис. 10.10. Допустим далее, что в промежут¬
ке между идентификаторами пользователя А и С появляется новый эквивалент.
В данном примере это потребует модификации до 75% всех промежутков време¬
ни, имеющихся в базе данных! И поскольку заранее неизвестно, на какие именно
промежутки времени окажет влияние появление нового эквивалента, для обра¬
ботки эквивалентов при таком подходе останется только одно: обойти всю базу
данных по каждому эквиваленту. Очевидно, что такой подход неприемлем.
Ключ (ДОи Часовой
промежуток времени)
Множество
идентификаторов
пользователя
“foo.com/pager, 0
А, В, С
“foo.com/page1", 1
А, й
“foo.com/pager, 2
КО,?
“foo.com/page1”, 102
А, В, С, в
Рис. 10.10. Эквиваленты способны повлиять
на любой промежуток времени в базе данных
Чтобы оптимизировать данный подход, можно, в частности, попьпаться под¬
держивать второй индекс от идентификатора пользователя ко множеству всех
промежутков времени, где имеется этот идентификатор пользователя. Если
пользователь посещал страницу в двух промежутках времени, то при появлении
Часть 11. Уровень обслуживании
эквивалента достаточно согласовать его идентификаторы в этих двух промежут-
ках времени, а не обходить всю базу данных.
К сожалению, такой подход не лишен недостатков. Что, если имеется робот
поискового механизма, который ежечасно посещает страницы по каждому URL?
Список промежутков времени для идентификатора пользователя будет содер¬
жать каждый промежуток времени в базе данных, что совершенно непрактич¬
но. Имеется немало вполне приемлемых массивов данных, где производитель¬
ность страдает из-за наличия непомерно больших списков промежутков времени
для отдельных идентификаторов пользователя или периодической потребности
обходить всю базу данных. Помимо ухудшения производительности, имеются
и другие недостатки. Информация о принадлежности отдельного идентифика¬
тора пользователя конкретному промежутку времени хранится в разных местах,
что создает предпосылки к несогласованности базы данных.
Должно быть также очевидно, что, обращаясь с эквивалентами, нельзя приме¬
нять алгоритм HyperLogLog. Множеству HyperLogLog неизвестно, какие имен¬
но элементы в нем находятся, а следовательно, нельзя применить эквиваленты
для удаления лишних идентификаторов пользователя. И это весьма прискорбно,
поскольку алгоритм HyperLogLog обеспечивает существенную оптимизацию.
До сих пор мы рассматривали задачу анализа эквивалентов для выбора одно¬
го идентификатора, представляющего пользователя. Эта задача довольно сложна
сама по себе, но воплотить ее на практике еще сложнее. Впрочем, для разъяс¬
нения сложностей полностью инкрементной архитектуры совершенно не обя¬
зательно знать, каким образом эта задача воплощается на практике, и поэтому
просто допустим, что она решена полностью. А результатом такого решения ока¬
зывается индекс от userid к personid, где personid — идентификатор, выбран¬
ный для представления всех идентификаторов userid, принадлежащих одному
и тому же пользователю.
Сложность рассматриваемой здесь задачи до сих пор состояла в том, что
на стадии записи предпринималась попытка внести коррективы в базу данных,
чтобы в ней отсутствовали одновременно два идентификатора пользователя,
связанных эквивалентами. Поэтому применим другой подход, перенеся всю об¬
работку эквивалентов на стадию чтения в процессе выполнения запроса.
Для первой попытки обработать эквиваленты на стадии чтения выбрана база
данных ключей от пары [URL, часовой промежуток времени] к множеству всех
идентификаторов пользователей, посетивших страницу по заданному URL за
указанный час (рис. 10.11). На этот раз в базе данных допускается существование
нескольких идентификаторов одного и того же пользователя, поскольку их об¬
работка переносится на стадию чтения, где выполняются следующие действия.
1. Извлечь каждый из идентификаторов пользователя за каждый час в задан¬
ных пределах и объединить их.
2. Преобразовать множество идентификаторов пользователя в идентифика¬
торы персоны, используя индекс от идентификатора пользователя к иден¬
тификатору персоны.
3. Возвратить подсчет из множества идентификаторов персоны.
Глеи» 10. Организация уровня обслуживания
239
База данных База данных
Стадия записи | Стадия чтения
I
Рис. 10.11. Обработка эквивалентов на стадии чтения
К сожалению, такой подход не вполне практичен, поскольку он обходится
слишком дорого. Допустим, требуется обработать запрос на 100 миллионов ин¬
дивидуальных посещений страниц. Это означает, что сначала придется извлечь
немало гигабайт информации, чтобы получить множество идентификаторов
пользователей, а затем выполнить 100 миллионов операций поиска по индексу
от идентификатора пользователя к идентификатору персоны. Такой объем рабо¬
ты вряд ли удастся выполнить за несколько миллисекунд.
Предыдущий подход можно немного видоизменить, чтобы сделать его бо¬
лее практичным, прибегнув к аппроксимации с целью значительно сократить
затраты на хранение и вычисление. В частности, вместо того чтобы хранить
все множество идентификаторов пользователей для каждого промежутка вре¬
мени, достаточно хранить их выборку из каждого промежутка времени. Так,
если хранить только 5% всех идентификаторов пользователей, то объем работы
по извлечению множество идентификаторов пользователей и преобразованию
идентификаторов пользователей в идентификаторы персон сократится на 95%.
Разделив подсчет из выбранного множества идентификаторов персон на частоту
выборки, можно получить оценку подсчета из невыбранного множества.
Последовательность операций на стадиях записи и чтения при подходе с вы¬
боркой приведена на рис. 10.12. Первая попытка сделать выборку может состо¬
ять в том, чтобы сгенерировать случайное число в пределах от 0 до 1 и ввести
идентификатор пользователя только в том случае, если это число меньше ча¬
стоты выборки. К сожалению, такая попытка не пройдет, как можно увидеть
248
Часть П. Уровень обслуживания
на простом примере. Допустим, имеется 100 просмотров страниц, совершенных
каждым из пользователей А, В, С и О, а требующаяся частота выборки состав¬
ляет 50%. Вероятнее всего, будут выбраны все четыре пользователя, поскольку
на каждого из них приходится по 100 просмотров страниц. Но это неверно, по¬
тому что при правильном методе выборки должно быть в среднем выбраны два
пользователя.
База данных База данных
і
Стадия записи
Стадия чтения
Рис. 10.12. Внедрение выборки на стадии записи
Правильную выборку обеспечивает другой метод, называемый выборкой с хеши¬
рованием. Вместо того чтобы выбирать случайное число с целью выяснить, следу¬
ет ли вводить идентификатор пользователя в промежуток времени, этот иденти¬
фикатор хешируется с помощью такой хеш-функции, как 5НА-256. Хеш-функции
обладают свойством равномерного распределения входных данных по всем\
ряду выходных чисел. Кроме того, они детерминированы, а следовательно,
одни и те же входные данные хешируются в те же самые выходные данные.
Так, если требуется выбрать лишь 25% всех идентификаторов пользователей, то
благодаря этим двум свойствам можно просто сохранить все идентификаторы
пользователей, хеш-значение которых составляет меньше 25% от максимального
выходного значения хеш-функции. И благодаря тому что хеш-функции детерми¬
нированы, однажды выбранный идентификатор пользователя будет выбираться
всегда, а если он не выбирается, то не будет выбран никогда. Таким образом,
при частоте выборки 50% в множестве сохраняется половина значений, неза¬
висимо от того, сколько раз появляется каждый идентификатор пользователя.
Глава 10. Организация уровня обслуживания
241
Применяя выборку с хешированием, можно существенно сократить размеры
множеств, сохраняемых для каждого промежутка времени, и чем выше заданная
частота выборки, тем точнее будут результаты обработки запросов.
С одной стороны, мы наконец-то получили эффективный подход к реализа¬
ции рассматриваемого здесь запроса. А с другой стороны, его можно применять
лишь с некоторыми оговорками. Во-первых, степень точности выборки с хеши¬
рованием не идет ни в какое сравнение с точностью алгоритма НурегЬод!х^.
Для одного и того же используемого пространства значений средняя ошибка
выборки с хешированием будет в 3-5 больше, чем у алгоритма НурегЬодГл^,
в зависимости от величины идентификаторов пользователей. Во-вторых, чтобы
достичь приемлемой производительности при таком подходе, требуется специ¬
альное оборудование для составления индекса от идентификатора пользователя
к идентификатору персоны. А для того чтобы достичь приемлемых частот вы¬
борки, множества идентификаторов пользователей должны по-прежнему иметь
не меньше 100 элементов. Это означает, что при обработке запросов придется
выполнять не менее 100 операций поиска по индексу от идентификатора поль¬
зователя к идентификатору персоны. И хотя это значительное усовершенство¬
вание по сравнению с потенциальными миллионами операций поиска, требую¬
щихся при подходе без выборки, оно все же не дает повода для оптимизма. Так,
если для хранения индекса от'идентификатора пользователя к идентификатору
персоны применяются накопители на жестких дисках, то для каждого поиска
по этому индексу потребуется хотя бы один поиск на дисках. Как было показано
ранее, операции поиска информации на дисках обходятся дорого, а необходи¬
мость выполнять большое число таких операций по каждому запросу приведет
к значительному снижению производительности.
Устранить это препятствие можно двумя способами. С одной стороны, можно
обеспечить хранение индекса от идентификатора пользователя к идентификато¬
ру персоны полностью в оперативной памяти, вообще не обращаясь к дискам.
Осуществимость такого способа зависит от размера индекса. А с другой стороны,
можно воспользоваться твердотельными накопителями, чтобы избежать поиска
на дисках и повысить производительность. Но главной оговоркой к такому под¬
ходу остается потребность в специальном оборудовании, позволяющем достичь
приемлемой производительности.
10.5.2. Сравнение с решением на основе лямбда-архитектуры
Полностью инкрементное решение задачи подсчета индивидуальных посети¬
телей страниц с учетом эквивалентов оказывается хуже решения, основанного
на лямбда-архитектуре, во всех отношениях. В этом решении приходится при¬
менять метод аппроксимации со значительно более высокой частотой появле¬
ния ошибок, большей задержкой и потребностью в специальном оборудовании
для достижения приемлемой производительности. В гаком случае уместно выяс¬
нить причины, по которым решение на основе лямбда-архитектуры оказывается
намного более простым и эффективным.
Все отличие такого решения заключается в том, что на уровне пакетной об¬
работки можно анализировать сразу все данные. В полностью инкрементном ре¬
шении приходится обрабатывать эквиваленты по мере их появления, но именно
242
Часть II, Уровень обслуживании
это препятствует применению алгоритма НурегЬл^Ьо^ А на уровне пакетной
обработки сначала обрабатываются эквиваленты путем нормализации, приво¬
дящей идентификаторы пользователей к идентификаторам персон, а затем без
всяких помех создаются представления индивидуальных посещений во времени.
Позаботившись сначала об эквивалентах, можно затем выбрать намного более
эффективную стратегию для формирования представлений индивидуальных по¬
сещений во времени. В дальнейшем эквиваленты все равно придется обрабаты¬
вать на уровне обслуживания, но благодаря наличию уровня пакетной обработки
справиться с этой задачей будет намного проще.
Резюме
В этой главе были рассмотрены следующие принципы, составляющие основу
уровня обслуживания в лямбда-архитектуре.
■ Способность приспосабливать представления для оптимизации характери¬
стик задержки и производительности.
■ Простота, с которой исключается поддержка операций записи с произ¬
вольным доступом.
■ Способность хранить данные нормализованными на уровне пакетной об¬
работки и выполнять их денормализацию на уровне обслуживания.
■ Внутренне присущая отказоустойчивость и корректность уровня обслужи¬
вания, обусловленная возможностью вычислить его содержимое заново из
главного массива данных.
Гибкая возможность полностью приспосабливать представления на уровне
обслуживания к обрабатываемым запросам служит ярким примером простоты
в действии. В традиционных архитектурах информационных систем в качестве
главного массива данных, исторического хранилища и хранилища реального
времени применяется единственная база данных. Необходимость выполнять все
эти роли сразу вынуждает разработчика приложения идти на неприемлемые ком¬
промиссы вроде выбора степени нормализации или денормализации схем, а так¬
же принимать на себя основное эксплуатационное бремя вроде уплотнения дан¬
ных. А в лямбда-архитектуре все эти роли выполняют отдельные компоненты.
Поэтому каждая роль может быть оптимизирована в намного большей степени,
а система в целом — стать более надежной. В следующей главе будет рассмотрен
пример практической реализации базы данных на уровне обслуживания.
оослуживания
И Е1ерИагийВ в качестве примера базы данных
для уровня обслуживания.
■ Архитектура базы данных Е1ер1папФВ.
■ Недостатки базы данных Е1ерЬагФВ.
■ Применение базы данных Е1ерИапЮВ
в приложении SuperWebAnalytics.com.
Рассмотрев требования к уровню обслуживания, мы можем перейти к при¬
меру построения базы данных, предназначенной для применения на уровне об¬
служивания. Как и во всех остальных иллюстративных главах, в этой главе не
рассматриваются новые теоретические вопросы, а демонстрируется воплощение
на практике теоретических понятий и принципов вплоть до подробностей при¬
менения конкретных инструментальных средств.
Как упоминалось в предыдущей главе, инструментальные средства, доступные
для организации уровня обслуживания, явно отстают от идеальных возможно¬
стей, и этот факт станет очевидным по мере построения уровня обслуживания
для приложения SuperWebAnalytics.com. В этой главе будет показано, как поль¬
зоваться базой данных ЕкрИатБВ для обработки пар “ключ-значение” на уров¬
не обслуживания. В этой базе данных поддерживается только тип индекса пар
“ключ-значение”, и поэтому нам придется отойти от идеальных типов индексов,
описанных в предыдущей главе.
Сначала мы исследуем в этой главе основную архитектуру базы данных
Е1ерЬатБВ, чтобы выяснить, насколько она соответствует требованиям уровня
обслуживания, а затем сделаем краткий обзор возможностей ее интерфейса АР1
для извлечения содержимого пакетного представления. И наконец, будет показа¬
но, как пользоваться базой данных Е1ерЬап10В для индексирования и обслужива¬
ния пакетных представлений в приложении SupcrWcbAnalytics.com.
244
Часть П. Уровень обслуживания
11.1. Основные положения о базе данных ElephantDB
База данных ElephantDB служит для хранения пар “ключ-значение” в виде от¬
дельных массивов байтов для ключей и значений. База данных ElephantDB разделя¬
ет пакетные представления на фиксированный ряд фрагментов, а каждый сервер
базы данных ElephantDB отвечает за некоторое подмножество этих фрагментов.
Функция, назначающая ключи для фрагментов, является подключаемой и на¬
зывается схемой фрагментации. В одной из наиболее распространенных схем
целевой фрагмент определяется взятием остатка от деления хеш-значения клю¬
ча на общее количество фрагментов (это, по существу, операция по модулю).
Назовем этот метод неформально получением хеш-значения по модулю. Этот метод
обеспечивает равномерное распределение ключей по всем фрагментам и предо¬
ставляет простые средства, позволяющие определить, в каком именно фрагменте
хранится данный ключ. Зачастую этот метод является самым лучшим выбором,
хотя далее будут рассмотрены случаи, где требуется специальная настройка схе¬
мы фрагментации. Как только пара “ключ-значение” будет назначена для фраг¬
мента, она сохраняется в локальном механизме индексации. По умолчанию в ка¬
честве такого механизма служит база данных BerkeleyDB, но это может быть
любой настраиваемый механизм индексации, выполняемый на одной машине.
Действие базы данных ElephantDB можно разделить на две аспекта: создание
представления и обслуживание представления. Создание представления происходит
при выполнении задания MapReduce в конце процесса обработки данных на уров¬
не пакетной обработки, где формируются разделы, сохраняемые в распределенной
файловой системе. Затем представления обслуживаются в кластере, который специ¬
ально выделен для базы данных ElephantDB, загружает фрагменты из распределен¬
ной файловой системы и взаимодействует с клиентами, поддерживающими запросы
на произвольное чтение данных. Мы обсудим вкратце оба эти аспекта, прежде чем
окончательно перейти к рассмотрению базы данных ElephantDB.
11.1.1. Создание представления в базе данных ElephantDB
Фрагменты базы данных ElephantDB создаются при выполнении задания
MapReduce, входными данными для которого являются пары “ключ-значение”.
Количество операций консолидации настраивается по количеству фрагментов
базы данных ElephantDB, а ключи распределяются для операций консолидации
по указанной схеме фрагментации. Следовательно, каждая операция консолида¬
ции отвечает за получение лишь одного фрагмента представления ElephantDB.
Затем каждый фрагмент индексируется (в частности, по индексу базы данных
BerkeleyDB) и выгружается обратно в распределенную файловую систему.
Следует иметь в виду, что фрагменты в процессе создания представления нс
направляются серверам базы данных ElephantDB. Такое проектное решение было
бы неудачным, поскольку машины, взаимодействующие с клиентами, не смогли
быть контролировать свою нагрузку, от чего бы пострадала производительность
при обработке запросов. Вместо этого серверы базы данных ElephantDB извлека¬
ют фрагменты из распределенной файловой системы с регулируемой частотой,
что дает им возможность поддерживать производительность, гарантируемую
для клиентов.
245
/Ходе //. Иллюстрация уровня обслуживания
Обслуживание представления в базе данных Е1ерЬаМ0В
Кластер базы данных ЕкрИатИВ состоит из ряда машин, среди которых рас¬
пределяется работа по обслуживанию фрагментов. Для точного разделения на¬
грузки фрагменты равномерно распределяются среди серверов.
В базе данных Е1ерЬатОВ поддерживается также репликация, когда каждый
фрагмент дополнительно размещается на предопределенном числе серверов.
Так, если имеется 40 фрагментов и 8 серверов, а коэффициент репликации ра¬
вен 3, то на каждом сервере будет размещено 15 фрагментов, т.е. каждый фраг¬
мент будет присутствовать на трех разных серверах. Благодаря этому кластер
становится устойчивым к аппаратным отказам, предоставляя полный подступ ко
всему представлению даже при выходе из строя отдельных машин. Разумеется,
из строя могут выйти только три машины, прежде чем все части представления
окажутся недоступными, но благодаря репликации такое событие становится
намного менее вероятным. Порядок репликации наглядно показан на рис. 11.1.
Благодаря репликации каждый фрагмент
присутствует на нескольких серверах
Рис. 11.1. При репликации фрагменты сохраняются в нескольких местах,
чтобы повысить устойчивость к отказам отдельных машин
Серверы базы данных Е1ерЬатВВ отвечают за извлечение назначенных
для них фрагментов из распределенной файловой системы. Когда сервер об¬
наруживает, что имеется новая версия фрагмента, он выполняет регулируемую
загрузку нового раздела. Загрузка происходит под контролем, чтобы не пере¬
грузить машину операциями ввода-вывода данных и не повлиять отрицательно
на операции непосредственного чтения. По окончании загрузки сервер перехо¬
дит к новому разделу и удаляет прежний раздел.
Как только сервер базы данных Е1ерЬатВВ загрузит все ее фрагменты, со¬
держимое пакетных представлений будет доступно через основной интерфейс
АР1. Как упоминалось ранее, для уровня обслуживания отсутствует база данных
общего назначения. Именно здесь и проявляются ограничения, присущие базе
данных Е1ерйатПВ. В этой базе данных применяется модель индексирования
246
Часть IL Уровень обслуживания
по ключу, и поэтому АР1 позволяет лишь извлекать значения по указанным клю¬
чам. А база данных общего назначения должна предоставлять на серверном уров¬
не более развитый АР1, в том числе возможность выполнять просмотр по клю¬
чам в заданных пределах.
11.1.3. Применение базы данных Е1врЬапЮВ
Простота базы данных Е1ерЬагНПВ облегчает ее применение. В частности,
ее применение можно разделить на три составляющие: создание фрагментов,
установка кластера для обслуживания запросов и применение клиентского АР1
для запрашивания пакетных представлений. Рассмотрим эти составляющие
по очереди.
СОЗДАНИЕ ФРАГМЕНТОВ БАЗЫ ДАННЫХ ELEPHANTDB
Абстракция отвода упрощает создание множества фрагментов базы данных
Е1ерЬатБВ средствами ^азсак^. В базе данных Е1ерЬатПВ предоставляется
отвод для автоматизации процесса создания фрагментов. Так, если имеется под¬
запрос на формирование пар “ключ-значение”, то для создания представления
в базе данных Е1ер11атОВ достаточно выполнить этот подзапрос в отводе, как
показано ниже.
Применяет
разделение
по модулю
хеш-значения
согласно схеме
фрагментации
public static elephantDbTapExample (Subquery subquery) {
DomainSpec spec = new DomainSpec (new JavaBerkDBO , <
о new HashModScheme ()) ;
Применяет
базу данных
BerkeleyDB
в качестве
локального
механизма
Направляет
результат
обработки
подзапроса
в построенный
отвод
Object tap = EDB.makeKeyValTap("/output/path/on/dfs", spec, 32);
Api. execute (tap, subquery); Создает 32 фрагмента
по заданному пути
в распределенной
файловой системе
Подспудно в сконфигурированном отводе задание MapReduce автоматически
настраивается на правильное разделение ключей. Здесь же составляется каждый
индекс, который затем выгружается в распределенную файловую систему.
УСТАНОВКА КЛАСТЕРА БАЗЫ ДАННЫХ ELEPHANTDB
Для установки кластера базы данных ЕІерЬатОВ требуются две конфигура¬
ции: локальная и глобальная. Локальная конфигурация содержит характерные
для сервера свойства, а также адреса, где находятся конкретные фрагменты
и глобальная конфигурация. Основная локальная конфигурация находится на ка¬
ждом отдельном сервере и напоминает следующую:
Локальный каталог для хранения
загруженных фрагментов
{:local-root "/data/elephantdb" ^
:hdfs-conf {"fs.default.паше" "hdfs://namenode.domain.com:8020"}
:blob-conf {"fs.default.name" "hdfs://namenode.domain.com:8020"})
Адрес распределенной
файловой системы,
где хранятся фрагменты
■ V
Адрес распределенной файловой
системы, где размещается
глобальная конфигурация
Глава II, Иллюстрация уровня обслуживания
247
Глобальная конфигурация содержит информацию, необходимую для каждого
сервера в кластере. К ней относится коэффициент репликации, ТСР-порт, кото¬
рым серверы должны пользоваться для приема запросов, а также представления,
обслуживаемые данным кластером. Один кластер может обслужить несколько до¬
менов, и поэтому его конфигурация содержит отображение имен доменов на их
местоположение в системе ГГОЕБ. Основная глобальная конфигурация будет вы¬
глядеть так, как показано в следующем фрагменте кода:
ТСР-порт,
которым
сервер будет
пользоваться
для приема
запросов
Коэффициент репликации
всех представлений
для всех серверов
Имена хостов
всех серверов
в кластере
{:replication 1 <ь-
:hosts ["edbl.domain.com" "edb2.domain.com" "edb3.domain.com"]
r> :port 3578
:domains {"tweet-counts" "/data/output/tweet-counts-edb"
"influenced-by" "/data/output/influenced-by-edb"
"influencer-of" "/data/output/influencer-of-edb"}} <3-
Идентификаторы всех
представлений и их местоположение
в распределенной файловой системе
Эти конфигурации настолько просты, что почти все они оказываются непол¬
ными. Например, в них отсутствует явное назначение конкретных фрагментов
для размещения на отдельных серверах. В данном конкретном случае серверы
пользуются своим положением в списке хостов в качестве входных данных
для детерминированной функции, чтобы определить те фрагменты, которые
они должны загрузить. Простота конфигураций отражает простоту использова¬
ния базы данных ЕкрЬамБВ.
Как запустить сервер базы данных ElephantDB
Процесс запуска сервера базы данных ElephantDB следует стандартным нормам прак¬
тики, принятым в программировании на Java. Он включает в себя построение проекта
uberjar и передачу местоположения конфигураций через параметры командной строки.
Не вдаваясь в подробности этого процесса, которые могут быстро устареть, отсылаем
вас за справкой на веб-сайт данного проекта по адресу http://github.com/nathanmarz/
elephantdb.
ЗАПРАШИВАНИЕ КЛАСТЕРА БАЗЫ ДАННЫХ ELEPHANTDB
База данных ElephantDB предоставляет доступ к простому интерфейсу
Apache Thrift API для выдачи запросов. Подключившись к серверу базы данных
ElephantDB, можно выдавать запросы так, как показано ниже. Если запрашивае¬
мые ключи не хранятся локально на подключенном сервере, он свяжется с дру¬
гими серверами в своем кластере, чтобы извлечь требующиеся значения.
public static void clientQuery(ElephantDB.Client client,
tring domain,
byte[] key) {
client.get(domain, key);
}
248
Часть Л. Уровень обслуживания
11.2. Построение уровня обслуживания
для приложения SuperWebAnalyttcs.com
После того как мы рассмотрели основные положения о базе данных
Е1ер11ат1ЭВ, можно приступить к созданию оптимизированных представлений
в этой базе данных по каждому запросу в приложении SuperWebAnalytics.com.
И прежде всего, это представление просмотров страниц во времени.
11.2.1. Просмотры страниц во времени
Напомним, что идеальным представлением для просмотров страниц во време-
ни является индекс от ключа к отсортированному отображению, как еще раз пока¬
зано на рис. 11.2. И как пояснялось ранее, детализация по времени свыше отдель¬
ных часов не требуется, а для каждой записи требуется лишь несколько байт, и по¬
этому просмотр информации за период в несколько лет обходится очень дешево.
foo.eom/blog/1
bar.com/post/abc
Ьаг.пеУраде/а
Час
Количество
просмотров
стоаниц
0
123
1
101
2
278
3
176
4
133
0
123
1
101
2
278
0
176
1
133
2
97
Рис. 11.2. Идеальная стратегия индексирования
для представления просмотров страниц во времени
К сожалению, в базе данных Е1ерИатОВ поддерживается только индексиро¬
вание пар “ключ-значение”, и поэтому создать такое представление с ее помо¬
щью нельзя. А поскольку каждый ключ приходится извлекать по отдельности,
то существует настоятельная необходимость свести к минимуму количество клю¬
чей, извлекаемых по каждому запросу. Это подразумевает обязательное индек¬
сирование всех степеней детализации по времени в представлении. Выясним,
как воспользоваться базой данных Е1ерЬап10В, чтобы реализовать эту' стратегию
индексирования.
В конце главы 8 было получено представление, аналогичное приведенно¬
му на рис. 11.3. Напомним, что ключи и значения хранятся в базе Е1ерЬаш1)В
в виде массивов байтов. Для представления просмотров страниц во времени тре¬
буется закодировать 1ЖЬ, степень детализации по времени и промежуток време¬
ни в ключе.
рлова 11 • Иллюстрация уровня обслуживания
249
URL
Степень детализации
Промежуток времени
Количество
просмотров страниц
foo.eom/bloo/1
h
0
10
foo.eom/blofl/1
h
1
21
foo.com/blOfl/1
h
2
7
foo.eom/blog/1
w
0
38
foo.eom/bloo/1
m
0
38
bar.eom/post/a
h
0
213
bar.eom/post/a
h
1
178
bar.eom/post/a
h
2
568
Рис. 11.3. Представление просмотров страниц во времени
Приведенные ниже функции JCascalog реализуют требующиеся операции се¬
риализации для составных ключей и значений просмотра страниц.
public static class ToUrlBucketedKey extends CascalogFunction {
public void operate(FlowProcess process, FunctionCall call) {
String url = call.getArguments () .getString(0);
String gran = call.getArguments () .getString(l) ;
Integer bucket = call.getArguments().getlnteger(2);
String keyStr = url + "/" + gran + + bucket; «-
try {
call.getOutputCollector()
.add(new Tuple(keyStr.getBytes("UTF-8"))); <-
} catch(UnsupportedEncodingException e) {
throw new RuntimeException (e) ;
}
Соединяет
составляющие ключа
Преобразует байты,
используя кодировку UTF-8
public static class ToSerializedLong extends CascalogFunction {
public void operate (FlowProcess process, FunctionCall call) {
long val = call.getArguments () .getLong(O);
ByteBuffer buffer = ByteBuffer.allocate (8); о
buffer.putLong(val);
call.getOutputCollector () .add(new Tuple (buffer, array ())); <»—i Извлекаетмассив
} I байтов из буфера
Настраивает буфер
типа ВуЬеВг^ег
на хранение
единственного
длинного значения
Следующий шаг состоит в создании отвода Е1ерЬапсОВ. Во избежание расхож-
дений во времени реакции серверов можно создать специальную схему фрагмен¬
тации. чтобы гарантировать нахождение всех пар “ключ-значение” по одному
иЯЬ в одном и том же фрагменте. С этой целью в следующем фрагменте кода
хеш-значение получается по модулю только для той части составного ключа, ко¬
торая относится к иШ-:
250
Часть II, Уровень обслуживания
private static String getUrlFromSerializedKey (byte[] ser)
try {
String key = new String (ser, "UTF-8") ;
return key.substring(0, key.lastIndexOf("/")); <—
} catch(UnsupportedEncodingException e) {
throw new RuntimeException (e);
{
Извлекает URL
из составного ключа
)
public static class UrlOnlyScheme implements ShardingScheme {
public int shardlndex(byte[] shardKey, int shardCount) {
String url = getUrlFromSerializedKey (shardKey);
return url.hashCodeO % shardCount; <
} Возвращает модуль
} хеш-значения URL
Собирая все вместе, в следующем подзапросе ^авсак^ представление уров¬
ня пакетной обработки преобразуется в пары “ключ-значение”, подходящие
для базы данных ЕкрИатЭВ:
Из подзапроса
должны
возвращаться
лишь два поля,
соответствующих
ключам
и значениям
Определяет
локальный
механизм
хранения, схему
фрагментации
и общее число
фрагментов
public static void pageviewElephantDB (Subquery batchView) {
Subquery toEdb =
—> new Subquery("?key", "?value")
.predicate(batchView, "?url", "?gran", "?bucket", "?total-views")
.predicate (new ToUrlBucketedKey(), "?url", M?gran", "?bucket")
.out("?key")
. predicate(new ToSerializedLong(), "?total-views")
.out("?value");
Указывает
местоположение
DomainSpec spec = new DomainSpec (new JavaBerkDBO , фрагментов
—> new UrlOnlyScheme (), 32); BCHCTeMeHDFS
Tap tap = EDB.makeKeyValTap("/outputs/edb/pageviews", spec); <
Api.execute(tap, toEdb); < .
} Выполняет преобразование
Следует еще раз подчеркнуть, что представление просмотров страниц во
времени только выиграло бы от применения базы данных общего назначения
на уровне обслуживания, поскольку в ней можно было бы хранить больше проме¬
жутков времени по каждому 1ЖЬ, последовательно располагая их в хронологиче¬
ском порядке. Такая база данных допускала бы просмотр информации на дисках,
сводя к минимуму ее поиск на дисках.
К сожалению, на момент написания книги такая база данных для уровня об¬
служивания отсутствовала, хотя создать ее намного проще, чем большинство
имеющихся в настоящее время нереляционных распределенных баз данных типа
МоБС^Ь. Впрочем, рассматриваемая здесь база данных не намного хуже идеаль¬
ной базы данных для уровня обслуживания. И хотя она способна гарантировать,
что все выборки индексов по единственному индексу затронут лишь один узел,
извлечь придется только с десяток значений по каждому запросу в отдельности.
11.2.2. Индивидуальные посещения страниц во времени
Следующий тип запроса состоит в подсчете количества индивидуальных посе¬
щений страниц во времени. Как для запроса на подсчет количес тва просмотров
Гл<**а Иллюстрация уровня обслуживания
251
страниц во времени, отсутствие на уровне обслуживания базы данных ключей
к отсортированному отображению препятствует реализации идеального индек¬
са, описанного в предыдущей главе. Впрочем, можно воспользоваться стратеги¬
ей, аналогичной описанной выше при рассмотрении представления просмотров
страниц во времени, чтобы выработать работоспособное решение.
Оба упомянутых запроса отличаются лишь тем, что индивидуальные посеще¬
ния страниц во времени хранятся во множествах HyperLogLog. Но и в этом слу¬
чае можно воспользоваться той же самой схемой фрагментации, чтобы избежать
расхождений во времени реакции серверов. Ниже приведен код для получения
представления посещений страниц во времени.
public static void uniquesElephantDB(Subquery uniquesView) {
Subquery toEdb = new Subquery ("?key", "?value")
.predicate(uniquesView,"?urr,/ "?gran", "?bucket
.predicate (new ToUrlBucketedKey(), "?url", "?gran
.out("?key");
?value")
?bucket") о
Сериализировать требуется
только составной ключ, поскольку
множества HyperLogLog уже
сериализированы
DomainSpec spec = new DomainSpec (new JavaBerkDB (),
new UrlOnlyScheme (), 32);
Tap tap = EDB.makeKeyValTap ("/output s/edb/uniques", spec); <
Api. execute (tap, toEdb); Изменяет выходной каталог для
\ размещения отдельных фрагментов
просмотров страниц
Идеальной базе данных на уровне обслуживания должно быть известно, как обра¬
щаться с множествами Нурег1х^1х^ и завершать запросы на сервере. Вместо запро¬
сов к базе данных, возвращающей множества НурегГх^Ьо^ серверу пришлось бы
в таком случае объединить множества и возвратить только количество элементов
структуры Нурег1х^Ьо^ Это позволило бы максимально повысить эффективность,
исключив передачу по сети любых множеств Нурег1л^1^ при обработке запросов.
11.2.3. Анализ показателя отказов от просмотра страниц
Идеальное представление анализа показателя отказов от просмотра страниц
является индексом пар “ключ-значение”, и поэтому такое представление может
быть получено средствами EiephantDB. Представление анализа показателя отка¬
зов от просмотра страниц содержит отображение каждого домена на количество
посещений и количество отказов от просмотра страниц.
И хотя для этой цели можно воспользоваться каркасом из предыдущих за¬
просов, для сериализации строковых ключей и составного значения потребуется
специальный код, как показано ниже.
public static class ToSerializedString extends CascalogFunction { <i .
public void operate (FlowProcess process, FunctionCall call) { Эта функция сериализации, ;
String str = call, get Arguments () .getString (0); по существу, такая же, j
как и для составных ключей !
try {
call .getOutputCollector () .add(new Tuple (str .getBytes ("UTF-8 )));
} catch(UnsupportedEncodingException e) (
throw new RuntimeException (e);
}
)
}
252
Часть IL Уровень обслуживании
public static class ToSerializedLongPair extends CascalogFunction {
public void operate(FlowProcess process, FunctionCall call) {
long 11 = call.getArguments () .getLong(O);
long 12 = cal 1. getArguments () .getLong (1); <] Выделяет место для двух
ByteBuffer buffer » ByteBuffer.allocate (16); длинных значений
buffer.putLong(11);
buffer.putLong(12);
call.getOutputCollector().add(new Tuple(buffer.array()));
}
По запросам относительно этого представления домены будут извлекаться
только по очереди, и поэтому можно не беспокоиться о расхождениях во време¬
ни реакции серверов. И в данном случае вполне пригодной оказывается обычная
фрагментация по модулю хеш-значения, как показано ниже.
public static void bounceRateElephantDB (Subquery bounceView) {
Subquery toEdb = new Subquery ("?key", "?value")
.predicate(bounceView, "?domain", "?bounces", "?total")
.predicate(new ToSerializedString(), "?domain")
.out("?key")
.predicate(new ToSerializedLongPair(),"?bounces", "?total")
.out("?value");
Использует фрагментацию по модулю
DomainSpec spec = new DomainSpec(new JavaBerkDB(), хеш-значения,обеспечиваемую
new HashModScheme (), 32); « 1 средствами ElepharrtDB
Tap tap = EDB.makeKeyValTap("/outputs/edb/bounces", spec);
Api.execute(tap, toEdb);
}
Как видите, интеграция пакетных представлений на уровне обслуживания не
требует особых затрат труда.
Резюме
На уровне обслуживания может быть использована база данных ElephantDB.
И в этой главе было показано, насколько просто пользоваться и оперировать
этой базой данных. Можно надеяться, что в будущем для уровня обслуживания
будут созданы и другие базы данных с иными или более общими моделями ин¬
дексации, поскольку принципиальная простота уровня обслуживания позволяет
легко построить такие базы данных.
Итак, разъяснив особенности организации и построения уровней пакетной
обработки и ослуживания, перейдем к исследованию уровня ускорения - завер¬
шающего компонента лямбда-архитектуры. На уровне ускорения возмещаются
большие задержки обновлений, происходящие на уровне пакетной обработки,
а также разрешается доступ к актуальным данным по запросам.
Часть III
Уровень ускорения
jj
Хасть III посвящена исследованию уровня ускорения лямбда-архитектуры. На
этом уровне возмещаются большие задержки, возникающие на уровне пакетной
обработки при обновлении результатов, получаемых по запросам.
В главе 12 представления в реальном времени обсуждаются в сравнении
с пакетными представлениями. Главное отличие заключается в том, что база
данных представлений в реальном времени должна поддерживать операции
произвольной записи, что приводит к существенному усложнению базы данных.
В этой главе будет пояснено, что существование уровня пакетной обработки
упрощает управление подобными базами данных. В ней будет также показано,
что уровень ускорения может быть реализован синхронно или асинхронно.
А в главе 18 иллюстрируется создание представлений в реальном времени
средствами Apache Cassandra.
Синхронные архитектуры не требуют никаких дополнительных пояснений,
и поэтому в начале главы 14 обсуждаются асинхронные архитектуры для уровня
ускорения, а затем организация инкрементных вычислений с использованием
обработки очередей и потоков. Имеются две основные парадигмы обработки
потоков: поочередная и микропакетная, и у каждой из них имеются свои
компромиссы. В главе 14 исследуется принцип поочередной обработки потоков,
а в главе 15 иллюстрируется модель, в которой применяются системы Apache
Kafka и Apache Storm.
В главе 16 исследуется принцип микропакетной обработки потоков. В ней
будет показано, что принося в жертву задержку, можно получить новые широкие
возможности. А в главе 17 микропакетная обработка потоков иллюстрируется
с использованием прикладного программного интерфейса Trident.
Представления
в реальном времени
■ Теоретическая модель, положенная в основу
уровня ускорения.
я Упрощение обязанностей уровня ускорения
на уровне пакетной обработки.
■ Применение баз данных произвольной записи
для создания представлений в реальном
времени.
■ Теорема САР и ее последствия.
■ Трудности организации инкрементных
вычислений.
■ Истечение срока действия данных на уровне
ускорения.
Обсуждение лямбда-архитектуры до сих пор велось вокруг уровней пакет¬
ной обработки и обслуживания, т.е. тех компонентов, которые предназначены
для вычисления функций над каждым имеющимся фрагментом данных. На этих
уровнях удовлетворяются все требующиеся свойства информационной системы,
кроме одного: малой задержки обновлений. И эго последнее требование удов¬
летворяется на уровне ускорения, в чем и состоит его единственное назначение.
Выполнение функций над всем главным массивом данных (возможно, над це¬
лыми петабайтами данных) является ресурсоемкой операцией. Чтобы сократить
задержку обновлений в как можно большей степени, необходимо применить со¬
вершенно другой подход, чем на уровнях пакетной обработки и обслуживания.
Это означает, что в основу уровня ускорения должны быть положены инкремент¬
ные, а не пакетные вычисления.
Инкрементные вычисления вносят немало новых трудностей и значительно
сложнее, чем пакетные вычисления. Правда, узкие требования к уровню ускоре¬
ния дают два преимущества. Во-первых, уровень ускорения отвечает только за
256
Чисть НІ. Уровень ускорении
те данные, которые еще предстоит включить в представления уровня обслужи¬
вания. Зачастую эти данные имеют срок давности в несколько часов, а их объ¬
ем намного меньше, чем у главного массива данных. Обработка данных в малых
>шсштабах дает бодыпую гибкость в принятии проектных решений. И во-вторых,
представления уровня обслуживания являются переходными. Как только данные
переносятся в представления уровня обслуживания, они могут быть отвергнуты
на уровне ускорения. Несмотря на то что уровень ускорения более сложный,
а следовательно, он в большей степени подвергнут ошибкам, любые ошибки
в нем носят краткосрочный характер и автоматически исправляются на более
простых уровнях пакетной обработки и обслуживания.
Как уже не раз отмечалось, истинный потенциал лямбда-архитектуры кроется
в разделении ролей на разных уровнях, как показано на рис 12.1. В традционных
архитектурах информационных систем, в том числе основанных на реляцион¬
ных базах данных, все существующее относится к уровню ускорения. Такие си¬
стемы обладают ограниченными возможностями для преодоления сложностей,
присущих инкрементным вычислениям.
о
ИММЬ VI.КН -IVНИИ
с печива*.-(обної
ии і малой
вржкой для
НИЧП
ДЭНИЫХВЯЙ Д-
чпмнияорсдт
Уровень ускорения
Представление
ш в реальном
Т времени
Представление
г в реальном
г времени
Уровень пакетной обработки
§В88 Главный массив данных
V
)
/
>
Уровень обслуживания
>чг-иТ1акетное
СОЬ пред¬
оставление
^г-иХ1акетное
{о) пред¬
ставление
Рис. 12.1. Уровень ускорения лямбда-архитектуры позволяет
обслуживать запросы актуальных данных с малой задержкой
У уровня ускорения имеются две грани: сохранение представлений в реальном
времени и обработка входящего потока данных для обновления этих представле¬
ний. Эта глава посвящена структуре и хранению представлений в реальном времени.
257
Глава 12. Представления в реальном времени
Сначала мы сделаем краткий обзор теоретических основ организации уровня уско¬
рения,, а затем уделим внимание различным трудностям» возникающим при инкре¬
ментных вычислениях. А после этого продемонстрируем, каким образом данные
утрачивают силу по истечении срока их действия на уровне ускорения.
12.1. Вычисление представлений в реальном времени
Основное назначение уровня ускорения является таким же, как и уровней па¬
кетной обработки и обслуживания, и состоит в получении представлений, кото¬
рые можно эффективно запрашивать. Главные его отличия заключаются в том,
что представления отображают лишь последние данные, которые должны об¬
новляться сразу же после поступления новых данных. Значение фразы “сразу
же” по-разному трактуется в отдельных приложениях, но, как правило, этот пе¬
риод времени простирается от нескольких миллисекунд до нескольких секунд.
Это требование имеет далеко идущие последствия для вычислительного подхода
к формированию представлений на уровне ускорения.
Чтобы понять эти последствия, рассмотрим простой подход к уровню ускорения.
Подобно тому, как представления получаются на уровнях пакетной обработки и об¬
служивания путем вычисления функции во всем главном массиве данных, на уров¬
не ускорения могут быть произведены представления путем выполнения функции
над всеми последними данными (т.е. такими данными, которые еще предстоит пере¬
нести на уровень ускорения). Такой подход привлекателен своей простотой и согла¬
сованностью с функционированием уровня пакетной обработки (рис. 12.2).
Простая стратегия, зеркально
отражающаяся на уровнях
пакетной обработки и обслужи¬
вания и предназначенная для
вычисления представлений
в реальном времени с исполь¬
зованием всех последних данных
в качестве входных
Рис. 12.2. Простая стратегия: представление в реальном времени = функция(последние данные)
К сожалению, такой подход оказывается непрактичным для многих прило¬
жений, стоит лишь рассмотреть присущую ему задержку и характеристики ис¬
пользования ресурсов. Допустим, информационная система ежедневно получает
новые данные объемом 32 Гбайт, и эти новые данные переносятся на уровень
обслуживания в течение 6 часов после их получения. Уровень ускорения будет
отвечать лишь за данные шестичасовой давности объемом около 8 Гбайт. И хотя
объем данных 8 Гбайт не такой уж и большой, он все же значительный для дости¬
жения задержек меньше секунды. Кроме того, для выполнения функции над дан¬
ными объемом 8 Гбайт всякий раз, когда получается новый фрагмент данных,
потребуется немало ресурсов. Если средний размер блока данных составляет
100 байт, то объем последних данных 8 Гбайт приблизительно составляет 86 мил¬
лионов блоков данных. Следовательно, чтобы сохранить актуальными представ¬
ления в реальном времени, придется обрабатывать неприемлемо большой объем
258
Часть III. Уровень ускорения
данных (86000000 х 8 Гбайт) каждые шесть часов. Чтобы сократить потребление
ценных ресурсов, можно было бы прибегнуть к пакетным обновлениям, но это
привело бы к значительному росту задержки обновлений.
Если для приложения приемлема задержка порядка нескольких минут, то
для него вполне пригоден рассмотренный выше простой подход. Но в общем
представления в реальном времени необходимо получать с задержкой порядка
миллисекунд, эффективно используя ресурсы. Именно такой подход и будет об¬
суждаться далее в этой главе.
В общем, любое работоспособное решение опирается на применение инкре¬
ментных алгоритмов, как демонстрируется на рис. 12.3. Основная идея состоит
в том, чтобы обновлять представления в реальном времени по мере поступления
данных, т.е. повторно пользоваться результатами той работы, которая уже была
выполнена для получения представлений. Для этого потребуются базы данных
с произвольным чтением и записью, что позволит выполнять обновления уже
имеющихся представлений. Мы рассмотрим эти базы данных в следующем раз¬
деле, подробно обсудив хранение представлений на уровне ускорения.
Рис. 12.3. Инкрементная стратегия: представление в реальном времени = функция(новые данные,
предыдущее представление в реальном времени)
12.2. Хранение представлений в реальном времени
Обязательства представлений на уровне ускорения вполне требовательны:
лямбда-архитектуре требуются произвольные операции чтения с малой задерж¬
кой. А применение инкрементных алгоритмов также требует произвольных об¬
новлений с малой задержкой. Следовательно, базовый уровень хранения должен
удовлетворять следующим требованиям.
■ Операции произвольного чтения. Представление в реальном времени
должно поддерживать быстрые операции произвольного чтения, чтобы
быстро отвечать на запросы. Это означает, что данные, содержащиеся
в таком представлении, должны быть индексированы.
259
Глава 12. Представления в реальном времени
■ Операции произвольной записи* Чтобы поддерживать инкрементные ал¬
горитмы, должна быть также возможность для модификации представле¬
ния в реальном времени с малой задержкой.
■ Масштабируемость. Аналогично представлениям на уровне обслужива¬
ния, представления в реальном времени должны быть масштабированы
но объему хранящихся в них данных и скоростям чтения и записи, требую¬
щимся в приложении. Как правило, это подразумевает, что представления
в реальном времени могут быть распределены среди многих машин.
■ Отказоустойчивость. Если выйдет из строя диск или машина, представ¬
ление в реальном времени должно и дальше функционировать нормально.
Отказоустойчивость достигается репликацией данных по машинам, чтобы
иметь резервные копии на случай отказа одной машины.
Эти требования являются общими для категории баз данных, которые полу¬
чили название нереляционных распределенных баз данных типа NoSQL. В таких ба¬
зах данных поддерживаются разные модели данных и типы индексов, и поэтому
можно выбрать одну или более базу данных представлений в реальном време¬
ни, чтобы удовлетворить требованиям индексации. Например, можно выбрать
базу данных Cassandra, чтобы хранить индексы в формате пар “ключ-значение”,
а затем воспользоваться поисковым механизмом ElasticSearch для задействова¬
ния индексов, поддерживающих запросы на поиск. В конечном счете вы вольны
проявить гибкость в выборе баз данных в таком сочетании, которое вполне удов¬
летворяет потребностям уровня ускорения.
12.2.1. Достижимая точность
От выбора баз данных зависит, каким образом представления хранятся в реаль¬
ном времени, но вы вольны с большой гибкостью выбирать, что именно хранить,
для ответа на запросы. Зачастую содержимое представлений в реальном време¬
ни точно отражает содержимое пакетных представлений. Например, для ответа
на запрос, по которому возвращается количество просмотров страниц, точные
подсчеты будут сохранены как на пакетном, так и на уровне ускорения. Но это
совсем не обязательно именно то, что нужно, поскольку нередко функции, ко¬
торые можно легко выполнить в пакетном режиме, трудно выполнить посте¬
пенно. Подобная ситуация уже возникала раньше при подсчете индивидуальных
посещений страниц. Вычисления легко произвести в пакетном режиме потому,
что весь массив данных обрабатывается сразу. Но сделать то же самое в реаль¬
ном времени намного труднее, поскольку придется хранить весь массив данных
для правильного обновления подсчетов.
В подобных случаях можно принять другой подход на уровне ускорения, что¬
бы аппроксимировать правильный ответ. Все данные в конечном итоге отобра¬
жаются в представлениях уровней пакетной обработки и обслуживания, и поэто¬
му любые аппроксимации, совершаемые на уровне ускорения, постоянно коррек¬
тируются. Это означает, что любые аппроксимации являются лишь временными,
а достижимая точность проявляется в самих запросах. Данный подход действи¬
тельно эффективен, поскольку он позволяет добиться наилучших результатов во
всем, что касается пропуспой способности, точности и своевременности. При
260
Часть III. Уровень ускорения
обработке сложных запросов весьма распространены подходы, обеспечиваю¬
щие достижимую точность, в том числе и те, что требуют машинного обучения
в реальном времени. Такие представления должны согласовывать вместе мно¬
гие виды данных и не могут последовательно выдавать точный ответ ни коим
приемлемым образом. В дальнейшем будут представлены примеры выгодного
использования достижимой точности, когда дело дойдет до воплощения уровня
ускорения в приложении SuperWebAnalytics.com.
Следует еще раз подчеркнуть, что достижение заданной точности является
дополнительным методом, с помощью которого можно существенно сократить ис¬
пользование ресурсов на уровне ускорения. И такая возможность имеется лишь
потому, что в лямбда-архитектуре существуют два уровня с разными методами
вычислений. А в традиционной архитектуре, основанной на полностью инкре¬
ментных вычислениях, такой метод не предусмотрен как дополнительный.
12.2.2. Сохранение состояния на уровне ускорения
На уровне ускорения сохраняется относительно малая часть состояния, по¬
скольку на нем представлены только последние данные. И это выгодно, потому
что представления реального времени намного более сложные, чем представле¬
ния на уровне обслуживания.
Рассмотрим вкратце сложности представлений в реальном времени, которые
обойдены на уровне обслуживания.
■ Оперативное уплотнение данных. По мере поступления обновлений
в базу данных чтения-записи части индекса становятся ненужными, зани¬
мая лишнее место на диске. Поэтому в базе данных должно периодически
выполняться уплотнение для высвобождения свободного места. Но уплот¬
нение данных требует интенсивного использования ресурсов и может по¬
тенциально исчерпать все ресурсы машины, требующиеся для быстрого
обслуживания запросов. Неправильное проведение уплотнения данных
способно вызвать каскадные отказы во всем кластере.
■ Распараллеливание. Потенциально база данных чтения-записи может по¬
лучать много запросов на одновременное чтение или запись одного и того
же значения. Следовательно, ей нужно скоординировать эти операции чте¬
ния и записи, чтобы предотвратить возврат устаревших или несогласован¬
ных значений. Общее использование изменяемого состояния в потоках
исполнения является печально известной проблемой, а такие стратегии
управления параллельной обработкой, как блокировка, не менее печально
известны чреватостью ошибками.
Очень важно отметить, что уровень ускорения находится под меньшим давле¬
нием, поскольку на нем хранится намного меньше данных, чем на уровне обслу¬
живания. Разделение ролей и ответственности в лямбда-архитектуре ограничива¬
ется сложностью уровня ускорения. Как правило, на уровне ускорения в расчет
принимаются данные со сроком давности лишь в несколько часов, и поэтом\
кластер базы данных с произвольным чтением и записью может быть в 100 раз
меньше, чем в полностью инкрементной архитектуре. Чем меньше кластеры,
тем легче ими управлять. Итак, представив теоретические основы организации
261
Глава 12• Представления в реальном времени
уровня ускорения, перейдем к рассмотрению тех трудностей, с которыми при¬
ходится сталкиваться при выполнении инкрементных вычислений, в отличие
от пакетных.
12.3. Трудности инкрементных вычислений
В главе 6 обсуждались отличия между алгоритмами повторных и инкремент¬
ных вычислений. В целом алгоритмы инкрементных вычислений являются менее
общими и устойчивыми к отказам, обусловленным человеческим фактором, чем
алгоритмы повторных вычислений, но они обеспечивают намного большую про-
пусную способность. Именно эта более высокая иропусная способность выгод¬
но используется на уровне ускорения. Но дополнительная трудность возникает,
когда алгоритмы инкрементных вычислений применяются в реальном контексте
взаимодействия между алгоритмами инкрементных вычислений и так называе¬
мой теоремой CAP (Consistency, Availability, Partition tolerance — Согласованность,
Доступность, Устойчивость к разделению). Эта трудность может оказаться осо¬
бенно сложной, и поэтому ее очень важно понять.
Теорема САР имеет отношение к принципиальным компромиссам между согла¬
сованностью\ когда гарантируется взаимное соответствие операций чтения и всех
предыдущих операций записи, и доступностью, когда по запросу возвращается от¬
вет вместо ошибки. К сожалению, эта теорема нередко объясняется неточно, вво¬
дя в заблуждение. Как правило, мы стараемся избегать представлений неточных
пояснений каких-нибудь понятий, но данная интерпретация настолько распро¬
странена, что требует обсуждения для прояснения возникающих недоразумений.
Как правило, теорема САР формулируется следующим образом: согласован¬
ность, доступность и устойчивость к разделению может быть по меньшей мере
двоякой. Недостаток такого пояснения заключается в том, что теорема САР име¬
ет непосредственное отношение к тому, что происходит с информационными
системами, когда не все машины могут связываться вместе. Говорить, что инфор¬
мационная система согласована и доступна, но не устойчива к разделению, не
имеет смысла, поскольку данная теорема имеет непосредственное отношение
к тому, что происходит при разделении.
Теорему САР правильнее сформулировать таким образом: если распределен¬
ная информационная система разделена, она может быть согласованной или до¬
ступной, но не той и не другой. Если выбрать доступность, то операции чтения
могут возвратить устаревшие результаты при разделении сети. Самой лучшей
согласованности, которой можно добиться в системе с высокой степенью до¬
ступности, является окончательная согласованность, когда система восстанавливает
согласованность, как только закончится разделение сети.
12.3.1. Достоверность теоремы САР
Нетрудно понять истинность теоремы СЛР. Допустим, имеется простая рас¬
пределенная база данных, хранящая пары “ключ-значение”, где каждый узел кла-
стера отвечает за отдельное множество ключей, что означает отсутствие репли¬
кации. Если требуется прочитать или записать данные по отдельному ключу, то
такой запрос обрабатывает на одной машине.
1
Часть ///, Уровень ускорения
А теперь допустим, что неожиданно пропадает связь с одной из машин в рас¬
пределенной информационной системе. Очевидно, прочитать или записать дан¬
ные в недоступных узлах нельзя.
Повысить отказоустойчивость распределенной информационной системы
можно репликацией данных среди нескольких серверов. Так, при коэффици¬
енте репликации, равном трем, каждый ключ будет реплицирован в трех узлах.
И если теперь одна из реплик окажется недоступной, то нужное значение все
равно может быть извлечено из реплики в другом месте. Но вероятность недо¬
ступности всех реплик по-прежнему остается, хотя она теперь стала намного
меньше.
В связи с изложенным выше возникает следующий вопрос: как выполнять за¬
пись при разделении? Это можно сделать несколькими способами — например,
отказаться от обновления, если не обновлены все реплики. При такой стратегии
по каждому запросу на чтение гарантируется возврат самого последнего значе¬
ния. И такая стратегия оказывается согласованной, поскольку клиенты, отделен¬
ные друг от друга, будут всегда получать одно и то же значение (или ошибку) при
выполнении операций чтения.
С другой стороны, обновление можно выбирать всякий раз, когда реплики
доступны, синхронизируя их с другими репликами, когда разделение благополуч¬
но разрешается. Но дело усложняется в такой ситуации, как показанная на рис.
12.4. В этой ситуации клиенты разделены по-разному и связаны с другими под¬
группами машин. В итоге реплики будут отличаться, если потребуется обновить
подмножество реплик, а произвести слияние сложнее, чем просто выбрать по¬
следнее обновление.
© При разделении Салли
обновляет место своего
жительства на г. Токио
© Если Дэн подключается
к разделенной реплике,
изменения, внесенные Салли,
не будут доступны ему до тех пор,
пока разделение благополучно
не разрешится
Рис. 12.4. Реплики могут отличаться, если при разделении допускаются обновления
При такой стратегии частичных обновлений информационная система ока-
зывается доступной, но согласованной. Как показано на рис. 12.4, пользова¬
тель Салли может обновить место своего жительства иа г. Токио, но реплика,
доступная пользователю Дэну, не может быть обновлена, и поэтому он прочи¬
тает устаревшие данные. Как демонстрирует данный пример, при разделении
Глава 12. Представления в реальном времени 263
невозможно обеспечить одновременно доступность и согласованность, посколь¬
ку отсутствуют средства связи с той репликой, которая содержит самую послед¬
нюю информацию.
Крайняя доступность в размытых кворумах
В некоторых распределенных базах данных применяет метод, называемый размытыми
кворумами и обеспечивающий крайнюю доступность, когда запросы на операции записи
принимаются даже в том случае, если реплики этих данных недоступны. Вместо этого
создается временная реплика, которая затем объединяется с официальной репликой,
как только они становятся доступными. В размытых кворумах потенциальное количество
реплик фрагмента данных равно количеству узлов в кластере, если каждый отдельный
узел отделяется от другого узла. И хотя такой метод может оказаться полезным, следует
иметь в виду, что в этом случае излишне повышается сложность системы.
Уровни пакетной обработки и обслуживания относятся к распределенным ин¬
формационным системам и подпадают под действие теоремы САР, как и любая
другая система. На уровне пакетной обработки разрешено записывать только
новые фрагменты неизменяемых данных. Для такой записи не требуется коор¬
динация между машинами, поскольку каждый фрагмент данных является незави¬
симым. Если данные нельзя записать во входящее информационное хранилище
на уровне пакетной обработки, они могут быть буферизированы локально, а в
дальнейшем попытка записать их может быть возобновлена. Что же касается
уровня обслуживания, то читаемые данные всегда устаревают из-за большой за¬
держки на уровне пакетной обработки. Уровень обслуживания оказывается не
просто, но даже окончательно несогласованным, поскольку он всегда неактуален.
Соответственно на уровнях пакетной обработки и обслуживания предпочтение
отдается доступности над согласованностью.
Как видите, в определении этих свойств нет ничего сложного. Логика работы
уровней пакетной обработки и обслуживания довольно проста и Понятна. Но,
к сожалению, дело состоит совсем иначе, когда приходится бороться с окон¬
чательной согласованностью, применяя алгоритмы инкрементных вычислений
в контексте реального времени.
12.3.2. Сложная взаимосвязь теоремы САР
с алгоритмами инкрементных вычислений
Как обсуждалось выше, если выбрать высокую доступность для распределен¬
ной информационной системы, то при разделении будет создано несколько ре¬
плик одного и того же значения, обновляемых независимо друг от друга. Как
только разделение будет благополучно разрешено, значения должны быть объе¬
динены вместе, чтобы новое значение включало в себя каждое обновление, про¬
изошедшее во время разделения, - ни больше ни меньше. Но дело в том, чго
добиться этого в каждом случае применения не так-то просто, и поэтому разра¬
ботчику приходится самому выбирать работоспособную стратегию. В качестве
примера рассмотрим реализацию окончательно согласованного подсчитывания.
В данном случае предполагается, что подсчет сохраняется лишь как значе¬
ние. Допустим, сеть разделяется, реплики развиваются независимо, а затем
264
Часть III. Уровень ускорения
разделение благополучно разрешается. Когда наступает момент для объединения
значений из реплик, то оказывается, что в одной реплике подсчет равен 110,
а в другой — 105. Каким же должно быть новое значение подсчета? Причина
этого недоразумения заключается в том, что заранее неизвестно, в какой именно
момент эти значения стали расходиться. Если они разошлись на значении 105,
то обновленный подсчет должен быть равен 110. А если они разошлись на зна¬
чение 0, то подсчет должен быть равен 215. Определенно известно лишь то, что
правильный ответ находится в этих двух пределах.
Чтобы правильно реализовать окончательно согласованное подсчитывание,
необходимо воспользоваться структурами, называемыми бесконфликтно реплици¬
руемыми типами дачных, сокращенно называемыми С1ШТ. Имеется целый ряд
типов С1ШТ для разнообразных значений и операций: множества, поддержи¬
вающие только сложение; множества, поддерживающие сложение и удаление;
множества, поддерживающие инкрементирование; числа, поддерживающие ин¬
крементирование и декрементирование, и т.д. Так, к типу С1ШТ относится на¬
растающий счетчик, поддерживающий только инкрементирование. Это именно
то, что нужно для решения задачи организации текущего счетчика. Пример на¬
растающего счетчика приведен на рис. 12.5.
счетчик счетчик
Рис. 12.5. Нарастающий счетчик, где в реплике только инкрементируется присваиваемый ей
счетчик. Общее значение счетчика является суммой подсчетов из реплик
В нарастающем счетчике сохраняются разные значения из каждой реплики,
а не единственное значение. А конкретный подсчет составляет сумму подсчетов
из реплик.
Если же обнаруживается конфликт между репликами, то новый нарастающий
счетчик принимает максимальное значение из каждой реплики. А поскольку под¬
счеты производятся только с нарастанием и только один сервер в системе будет
обновлять подсчет в отдельной реплике, то максимальное значение будет гаран¬
тированно правильным, как показано на рис. 12.6.
Реплики 1 и 3
Реплика 2
при объединении
Рис. 12.6. Объединение нарастающих счетчиков
265
Глава 12. Представления в реальном времени
Как видите, реализовать подсчитывание в окончательно согласованном кон¬
тексте реального времени оказывается намного сложнее. Для этого недостаточно
поддерживать простой подсчет. Нужна также стратегия исправления значений,
расходящихся при разделении. Дополнительную сложность могут также внести
применяемые алгоритмы. Если же разрешить не только инкрементирование, но
и декрементирование, то структура данных и алгоритм их объединения станут
еще более сложными. Такие алгоритмы обычно называются алгоритмами исправ¬
ления прочитанного и служат источниками ошибок, связанных с человеческим
фактором. И это не удивительно, если учесть их сложность.
К сожалению, избежать подобной сложности нельзя, если на уровне уско¬
рения требуется окончательная согласованность. Тем не менее лямбда-архи¬
тектуре присуща способность защитить разработчика от совершения ошибок.
Так, если представление в реальном времени становится искаженным потому,
что разработчик забыл учесть крайний случай или спутал алгоритм объедине¬
ния, совершенная им ошибка в дальнейшем будет автоматически исправлена
в представлениях на уровнях пакетной обработки и обслуживания. Самым
худшим последствием совершенной ошибки является временное искажение
представления. А в архитектурах без резервного копирования, совершаемого
в реальном времени на уровне пакетной обработки, такое искажение было бы
непоправимым.
12.4. Асинхронные обновления
в сравнении с синхронными
Архитектура уровня ускорения отличается в зависимости от того, каким об¬
разом обновляются представления в реальном времени: синхронно или асин¬
хронно. Вам, вероятно, не раз приходилось выполнять синхронное обновление,
когда приложение обращается с запросом на обновление непосредственно к базе
данных и блокируется до тех пор, пока запрос на обновление не будет обрабо¬
тан. Так, если пользователь формирует адрес электронной почты, приложение
может обратиться к базе данных с запросом на обновление, чтобы записать
в нее новую информацию. Синхронные обновления происходят быстро, потому
что они предусматривают непосредственное обращение к базе данных, а кроме
того, они легко согласуются с другими действиями в приложении, в том числе
с вращением курсора до тех пор, пока обновление не завершится.
Архитектура уровня ускорения для синхронных обновлений приведена
на рис. 12.7. И не удивительно, что приложение просто выдает запросы на об¬
новление непосредственно базе данных.
И напротив, запросы на асинхронные обновления ставятся в очередь, а сами
обновления происходят не сразу, а спустя некоторое время. Задержка обновле¬
ний на уровне ускорения может колебаться в пределах от нескольких миллисе¬
кунд до нескольких секунд, а может длиться еще больше, если запросов слиш¬
ком много. Асинхронные обновления происходят медленнее, чем синхронные,
поскольку они требуют дополнительных мер для модификации базы данных,
и согласовать их с другими действиями в приложении нельзя из-за отсутствия
контроля над их исполнением. Тем не менее асинхронные обновления дают не¬
мало преимуществ. Прежде всего, из очереди можно читать многие сообщения
266
Часть III. Уровень ускорении
и выполнять пакетные обновления базы данных, тем самым значительно повы¬
шая пропусную способность. Кроме того, асинхронные обновления ПОЗВОЛЯЮТ
регулировать изменяющуюся нагрузку. Так, если количество запросов на обнов¬
ление резко возрастает, дополнительные запросы буферизуются в очереди до тех
пор, пока не будут произведены все обновления. С другой стороны, резкое увели¬
чение сетевого трафика при синхронных обновлениях может привести к пере¬
грузке базы данных, а следовательно, к пропуску запросов, простоям и прочим
ошибкам, нарушающим нормальную работу приложения.
При синхронных обновлениях клиенты обращаются
непосредственно к базе данных и блокируются
до тех пор, пока обновление не завершится
Рис. 12.7. Простая архитектура уровня ускорения,
использующая синхронные обновления
Архитектура уровня ускорения для асинхронных обновлений приведена
на рис. 12.8. Подробнее об обработке очередей и потоков речь пойдет в гла¬
вах 14 и 15.
и применяет обновления
в массовом порядке
Рис. 12.8. Асинхронные обновления обеспечивают повышение пропусной способности
и оперативное регулирование изменяющейся нагрузки
Синхронные и асинхронные обновления находят свое применение. В част¬
ности, синхронные обновления характерны для систем обработки транзакций,
взаимодействующих с пользователями и требующих согласования с пользова¬
тельским интерфейсом, тогда как асинхронные обновления характерны для ана¬
литических систем, рабочая нагрузка на которые требует или не требует согласо¬
вания. Архитектурные преимущества асинхронных обновлений, в том числе по¬
вышенная пропуская способность и улучшенное управление резко возрастающей
г
Глава 12. Представления в реальном времени 267
нагрузки, предполагают реализацию асинхронных обновлений лишь в том слу¬
чае, если нет веских оснований не делать этого.
12.5. Истечение срока действия
представлений в реальном времени
Алгоритмы инкрементных вычислений и базы данных с произвольной запи-
сью делают уровень ускорения намного более сложным, чем уровни пакетной об-
раоотки и обслуживания, но одно из главных преимуществ лямбда-архитектуры
заключается в переходном характере ее уровня ускорения. Более простые уров¬
ни пакетной обработки и обслуживания непрерывно подменяют собой уровень
ускорения, и поэтому представления на уровне ускорения должны отображать те
данные, которые еще предстоит обработать в процессе пакетных вычислений.
Как только пакетные вычисления завершатся, часть представлений уровня уско¬
рения может быть отвергнута. Часть из них переносится на уровень обслужива¬
ния, но все остальное, очевидно, придется сохранить.
В идеальном случае база данных на уровне ускорения обеспечивает непо¬
средственную поддержку истечения срока действия записей, но это, как прави¬
ло, не годится для текущих баз данных. Такие инструментальные средства, как
МешсасЬес!, обеспечивают аналогичное поведение, чтобы установить задержи¬
ваемое во времени истечение срока действия по парам “ключ-значение”, хотя
они и не совсем пригодны для решения подобной задачи. Например, в качестве
истечения срока действия записи можно установить ожидаемое время, прежде
чем она будет интегрирована в представления уровня обслуживания (возможно,
через некоторое время, дополнительно требующееся для обслуживания буфера).
Но если по непредвиденным причинам для обработки на уровне пакетной об¬
работки потребуется дополнительное время, то истечение срока действия этих
записей на уровне ускорения произойдет преждевременно.
Вместо этого мы рассмотрим обобщенный подход к истечению срока дей¬
ствия представлений на уровне ускорения независимо от типа баз данных,
применяемых на этом уровне. Чтобы уяснить этот подход, нужно сначала ясно
понять, срок чего именно должен истечь, прежде чем уровень обслуживания
обновится. Допустим, имеется полная реализация лямбда-архитектуры, а прило¬
жение запускается на выполнение в первый раз. В системе пока еще отсутствуют
какие-либо данные, и поэтому представления как на уровне ускорения, так и на
уровне обслуживания оказываются пустыми.
Когда вычисления на уровне пакетной обработки выполняются в первый раз,
они вообще не будут оперировать данными. Допустим, вычисления на уровне па¬
кетной обработки отнимают 10 минут из-за издержек на выполнение заданий,
создавая пустые индексы и т.д. По окончании этих же 10 минут представления
на уровне обслуживания останутся пустыми, но представления на уровне ускоре-
ния теперь отображают данные сроком действия 10 минут. Эта ситуация нагляд¬
но показана на рис. 12.9.
Выполнение вычислений на уровне пакетной обработки сразу же начнется
во второй раз для обработки данных, накопившихся при первом выполнении
в течение 10 минут. Ради примера допустим, что для выполнения вычислений
Часть III. Уровень ускорения
на уровне пакетной обработки во второй раз потребуется 15 минут По окон¬
чании этих вычислений представления на уровне обслуживания будут отобра¬
жать данные, накопленные в течение первых 10 минут, тогда как представления
на уровне ускорения - жданные, накопленные в течение 25 минут. Ткким обра¬
зом, срок действия данных, накопленных в течение первых 10 минут в представ¬
лениях на уровне ускорения, может теперь истечь, как показано на рис. 12.10,
Уровень обслуживания
10 минут
Уровень ускорения
Время
выполнения
в системе
По окончании первых пакетных вычислений
уровень обслуживания остается пустым,
но на уровне ускорения оказываются
обработанными недавние данные
Рис. 12.9. Состояние представлений на уровнях обслуживания
и ускоренияпо окончании первых пакетных вычислений
Уровень обслуживания
Н— ■ И
Н 10 минут - 15 минут
Срок действия Уровень ускорения
истек
Время
выполнения
в системе
По окончании пакетных вычислений во второй раз
первый фрагмент данных переносится на
уровень обслуживания, а срок действия представлений
на уровне ускорения может истечь
Рис. 12.10. Срок действия части представлений в реальном
времени может истечь по окончании выполнения во второй раз
И наконец, допустим, что для выполнения пакетных вычислений на уровне па¬
кетной обработки в третий раз требуется 18 минут. Рассмотрим момент, предшеству¬
ющий окончанию вычислений в третий раз, как показано на рис. 12.11.
В этот момент на уровне обслуживания представлены лишь данные, накоплен¬
ные за 10 минут, тогда как на уровне ускорения обрабатываются данные, нако¬
пленные за оставшиеся 33 минуты. Как показано на рис. 12.11, представления
на уровне ускорения возмещают отсутствие данных в промежутках между вычис¬
лениями на уровне пакетной обработки в первый и во второй раз, в зависимости
от того, насколько продвинулся процесс обработки данных на уровне пакетной
обработки. Как только вычисления на уровне пакетной обработки завершатся,
данные для этих вычислений за три прошедших раза могут быть благополучно
удалены из уровня ускорения.
Простейший способ решения такой задачи состоит в том, чтобы поддержи¬
вать два множества представлений в реальном времени и поочередно очищать
их после выполнения вычислений на уровне пакетной обработки (рис. 12.12).
Одно из этих множеств представлений в реальном времени точно отображает
данные, необходимые для возмещения представлений на уровне обслуживания.
269
Г***112' Проставления в реальном времени
а % каждого вычисления на уровне пакетной обработки приложе-
П нт гг е^е ТИ К Чтению из представления в реальном времени дополни-
тель анных, кроме тех, которые были только что очищены.
Уровень облуживания
■ 10 минут
15 минут
18 минут
н
Уровень ускорения
Время
выполнения
в системе
л 1 «чмым окончанием вычислений на уровне пакетной
оораоогки уровень ускорения отвечает заданные накопленные
при выполнении вычислений за два предыдущих раза
Рис. 12.11. Состояние представлений уровней обслуживания и ускорения
непосредственно перед окончанием пакетных вычислений в третий раз
Выполнение
Выполнение
Выполнение
Выполнение
Выполнение
пакетных
пакетных
пакетных
пакетных
пакетных
вычислений
вычислений
вычислений
вычислений
вычислений
в первый раз
во второй раз
в третий раз
в четвертый раз
в пятый раз
Первые
представления И
/
Вторые
представления н НИ Н
в реальном времени
Рис. 12.12. Поочередная очистка двух разных множеств представлений в реальном уровне
гарантирует, что одно множество всегда содержит данные, подходящие для уровня ускорения
На первый взгляд, поддерживать два представления в реальном времени
затратно, поскольку это, по существу, означает удвоение затрат на хранение
данных на уровне ускорения. Самое главное, что представления уровня уско¬
рения отображают лишь мизерную часть данных давностью не менее нескольких
часов. В сравнении с данными, имеющими потенциально срок давности в не¬
сколько лет и представленными на уровне ускорения, эти данные составляют
меньше 0,1% от всех данных, имеющихся в системе. Такой подход вносит из¬
быточность, но это вполне приемлемая цена, которую приходится платить за
общее решение задачи, связанной с истечением срока действия представлений
в реальном времени.
Резюме
Уровень ускорения заметно отличается от уровня пакетной обработки.
Вместо выполнения функций над всем главным массивом данных производится
вычисление усложненных алгоритмов над более сложными формами хранения.
Но лямбда-архитектура позволяет сохранить уровень ускорения компактным,
а следовательно, и более управляемым.
В этой главе были рассмотрены основные принципы организации уровня
ускорения и особенности управления представлениями в реальном времени.
В ней были представлены два способа обновления представлений в реальном
2Т0
Часть III. Уровень ускорения
Бремени: синхронно и асинхронно. Синхронное обновление на уровне ускоре¬
ния не требует особых пояснений, поскольку для этого достаточно обратиться
к базе данных с запросом на обновление, но совсем другое дело — асинхронное
обновление на уровне ускорения. Поэтому прежде чем перейти к подробному
обсуждению особенностей этого процесса, рассмотрим в качестве примера базу
данных Cassandra, которую можно использовать для представления в реальном
времени на уровне ускорения.
представлений
в реальном времени
В этой главе...
■ Модель данных базы данных Cassandra.
■ База данных Cassandra в качестве
представления в реальном времени.
■ Контроль над разделением и упорядочением
для поддержки самых разных типов
представлений.
Итак, изложив основы организации уровня ускорения и представлений в ре¬
альном времени, перейдем к рассмотрению базы данных Cassandra, которой
можно воспользоваться в качестве представления в реальном времени. База дан¬
ных Cassandra ни в коем случае не является представлением общего назначения
в реальном времени. Ведь для удовлетворения потребностей многих информаци¬
онных систем в индексировании, согласованности и производительности требу¬
ется несколько баз данных. Тем не менее база данных Cassandra вполне подходит
для иллюстрации принципов организации представлений в реальном времени,
и поэтому именно она и будет использована на уровне ускорения приложения
далее в книге. Имеется немало общедоступных ресурсов, помогающих лучше по¬
нять внутренний механизм работы базы данных Cassandra, поэтому мы уделим
основное внимание ее свойствам с точки зрения пользователей.
13.1. Модель данных базы данных Cassandra
Несмотря на то что многие считают Cassandra столбчатой базой данных,
на наш взгляд, такое се обозначение несколько запутано. Вместо этого мо¬
дель данных базы данных Cassandra лучше рассматривать в виде отображение,
272
Часть III. Уровень ускорен^
состоящее из отсортированных отображений в виде значений. В базе данных
Cassandra допускаются стандартные операции над вложенными отображениями
в том числе ввод пар "ключ-значение”, поиск по ключу и получение значений
в заданных пределах.
Для наглядного представления терминологии на рис. 13.1 показаны разные
составляющие модели данных базы данных Cassandra. А связанная с ней терми¬
нология вкратце разъясняется ниже.
■ Семейства столбцов. Аналогичны таблицам в реляционных базах данных.
В каждом семействе столбцов хранится полностью независимое множе¬
ство информации.
■ Ключи. Если рассматривать семейство столбцов в качестве гигантского
отображения, то ключи являются записями верхнего уровня в этом множе¬
стве. В базе данных Cassandra ключи служат для распределения семейства
столбцов по всему кластеру.
■ Столбцы. Каждый ключ указывает на другие пары имен и значений, на¬
зываемых столбцами. Все столбцы физически хранятся по заданному клю¬
чу вместе, и благодаря этому доступ к ним обходится недорого. У разных
ключей могут быть разные множества столбцов, и по одному конкретному
ключу допускается наличие тысяч, а то и миллионов столбцов.
Семейство столбцов:
пользователи **
/
age -> 25
1 Alice 1
gender -> female
location -> CA
| Bob |
gender-* male
nickname -»• chuck
Charlie 1
posts -> 25
birthday -»06/12/86
f
О В семействе столбцов как и
в таблице реляционной базы
данных, хранится независимое
множество данных
© Семейства столбцов содержат
пары '‘ключ-значение4, где
сами значения являются
отображениями
© У каждого ключа имеются мно! о-
чисденные пары "ключ-значение
называемые столбцами. Все
столбцы хранятся вместе и могут
отличаться в зависимости
от конкретного ключа
Ключи Столбцы
Рис. 13.1. Информационная модель базы данных Cassandra состоит
из семейств столбцов, ключей и столбцов
Чтобы полностью уяснить эту модель данных, вернемся к примеру прило¬
жения SuperWebAnalytics.com. В частности, рассмотрим, как смоделировать
Глава 13. Иллюстрация представлений в реальном времени
япНгя Пп РосмотРов страниц во времени с помощью базы данных
’ Я кого представления требуется извлечь количество просмотров
стран во времени по отдельному URL и с конкретной степенью детализации
(за iac, день, неделю или месяц) в заданном диапазоне промежутков времени.
Для сохранения информации в базе данных Cassandra служит пара [URL, степень
детализации], а в качестве столбцов — пары имен и значений, обозначающие
промежутки времени и просмотры страниц. Пример данных для такого пред¬
ставления приведен на рис. 13.2.
Семейство столбцов: просмотры страниц
Рис. 13.2. Количество просмотров страниц во времени,
представленное в базе данных Cassandra
Такой способ хранения просмотров страниц эффективен потому, что столб¬
цы отсортированы (в данном случае по временному промежутку) и физически
хранятся вместе. В качестве альтернативы может служить ключ к базе данных
Cassandra, представляющий собой триаду, состоящую из URL, степени детализа¬
ции и промежутка времени, а также единственный столбец с просмотрами стра¬
ниц. Для запрашивания диапазонов просмотров страниц подобным способом по¬
требуются операции поиска по ключам к базе данных Cassandra. Каждый ключ
может потенциально находиться на другом сервере, и поэтому обработка таких
запросов будет происходить с большой задержкой из-за отличий во времени ре¬
акции отдельных серверов (см. главу 7).
13.2. Применение базы данных Cassandra
Итак, рассмотрев модель данных базы данных Cassandra, можно перей ти к ре¬
ализации способа подсчета просмотров страниц во времени с помощью ключей
в виде пары [URL, степень детализации] и столбцов, приходящихся на каждый
Часть III. Уровень ускорени*
промежуток времени. Такая реализация будет продемонстрирована с помощью
Java-клиента Hector для базы данных Cassandra.
Прежде всего, нам потребуется клиент, способный выдавать запросы на вы¬
полнение операций в конкретном семействе столбцов. Схемы приходится со¬
ставлять только по отдельным семействам столбцов, и поэтому в приведенном
ниже фрагменте кода определение схемы опущено, чтобы не усложнять сам код.
Подробнее об этом можно узнать из документации на Hector или Cassandra.
Кеуарасе —
это контейнер
для всех
семейств
столбцов
в приложении
Создает объект cluster для связи с кластером
распределенной базы данных Cassandra
Cluster cluster = HFactory.getOrCreateCluster("mycluster",”127.0.0.1") ;<!
-•> Keyspace keyspace =
HFactory.createKeyspace("superwebanalytics", cluster);
*> ColumnFamilyTemplate<String, Long> template =
new ThriftColumnFamilyTemplate<String, Long> (
keyspace, "pageviews",
StringSerializer.get(), LongSerializer.get());
Шаблон Со1итпРатИуТешр1аЪе служит для выдачи
запросов на выполнение операций в заданном семействе
столбцов и в указанном ключевом пространстве
Как только клиент свяжется с кластером, просмотры страниц можно из¬
влечь по заданному иЯЬ и в указанном диапазоне промежутков времени. В сле¬
дующем примере кода подсчитывается общее количество просмотров страниц
по веб-адресу foo.com/blog с ежедневной степенью детализации и в диапазоне
промежутков времени от 22 до 55:
SliceQuery<String, Long, Long> slice =
HFactory.createSliceQuery(keyspace,
Создает шаблон
диапазона для запроса
базы данных Cassandra
Присваивает
семейство столбцов
и ключ для запроса
StringSerializer.get(), <
LongSerializer.get(),
LongSerializer.get());
slice.setColumnFamily("pageviews");
slice.setKey("foo.com/blog-d");
Сериализаторы ключей
(№1, степень детализации),
имен (промежутков времени)
и значений (просмотров страниц)
соответственно
Получает
итератор столбцов
для заданного
диапазона
ColumnSliceIterator<String, Long, Long> it =
new ColumnSliceIterator<String, Long, Long>(slice,
1> 20L, 55L, false) ;
long total = 0;
while(it.hasNext()) {
total += it.next () .getValueO;
Выполняет итерацию
и суммирует просмотры страниц
для получения итоговой суммы
}
Итератор типа Со1итпЗНсе1Ьега1:ог осуществляет обход всех столбцов в за¬
данном диапазоне. Если этот диапазон непомерный (например, содержит десят¬
ки столбцов), то столбцы буферизуются и извлекаются из сервера в пакетном
режиме, чтобы не исчерпать оперативную память. Нс следует, однако, забывать,
что столбцы упорядочиваются и хранятся вместе, поэтому операции нарезки
в диапазоне столбцов оказываются очень эффективными.
Обновления реализуются так же просто. В следующем фрагменте кода к коли¬
честву просмотров страниц по веб-адресу foo.com/blog с ежедневной степенью
детализации для промежутка времени 7 прибавляется значение 25:
Гми* l3‘ ИллюетРаМия представлении в реальном времени
275
Если кя одно
из значений
не записано,
то никаких
просмотров
страниц
не было
long currVal;
HColumncLong, Long> col *
template. querySingleColumn ("foo. com/blog-d", 7L, <-
if (col»null)
-o currVal = 0;
else
LongSerializer.get ()); <з-
Сериализатор значения
просмотра страницы
currVal = col.getValue ();
Извлекает
текущее значение
по указанному иКЦ
с заданной степенью
детализации
и в конкретном
промежутке времени
ColumnFamilyUpdater<String, Long> updater =
template.createUpdater("foo.com/blog-d")
updater.setLong(7L, currVal + 25L);
r> template.update (updater);
Инкрементирует количество просмотров
страниц и выполняет обновление
Создает шаблон
обновлений
по заданному ключу
ассмотреиный выше пример подсчета количества просмотров страниц рас¬
пространяется и на веб-аналитику. Ради повышения эффективности пакетные
обновления обычно выполняются в пакетном режиме для нескольких промежут¬
ков времени. Мы еще вернемся к этому вопросу в главе 15, когда будем рассма¬
тривать обработку потоков.
13.2.1. Расширенные возможности базы данных Cassandra
Следует упомянуть о некоторых расширенных возможностях базы данных
Cassandra, делающих ее пригодной для более обширного ряда типов представле¬
ний в реальном времени. Первая возможность связана с распределением ключей
среди узлов в кластере. С этой целью можно выбрать один из следующих двух
типов разделителей: RandomPartitioner или OrderPreservingPartitioner.
С одной стороны, разделитель RandomPartitioner определяет режим ра¬
боты базы данных Cassandra в качестве хеш-множества и присваивает клю¬
чи разделам, используя хеш-значение ключа. В итоге ключи равномерно
распределяются по всем узлам в кластере. А с другой стороны, разделитель
OrderPreservingPartitioner сохраняет ключи в определенном порядке, вы¬
нуждая базу данных Cassandra вести себя как отсортированное отображение.
Хранение ключей в отсортированном порядке позволяет делать эффективные
запросы в заданном диапазоне ключей.
Несмотря на все преимущества хранения ключей в отсортированном поряд¬
ке, применение разделителя типа OrderPreservingPartitioner требует опреде¬
ленных затрат. Для хранения ключей в отсортированном порядке база данных
Cassandra пытается разделить ключи таким образом, чтобы каждый раздел со¬
держал приблизительно одинаковое количество ключей. К сожалению, для опе¬
ративного определения уравновешенных диапазонов ключей не существует
подходящего алгоритма. Так, при инкрементных вычислениях нагрузка на кла¬
стеры может стать неравномерной, когда одни серверы перегружены, тогда как
на других практически отсутствуют данные. И это еще один пример трудностей,
с которыми приходится иметь дело при выполнении инкрементных вычислений
в реальном времени, которых следует избегать при пакетной обработке данных.
В контексте пакетных вычислений все ключи известны заранее, и поэтому их
можно равномерно распределить по разделам в ходе вычислений.
!
276 Часть III. Уровень ускорения
У базы данных Cassandra имеется еще одно средство, называемое составны,
ми столбцами и расширяющее дальше принцип отсортированного отображения.
Составные столбцы позволяют вкладывать отображения друг в друга на произ-
вольную глубину. Например, индекс можно проиндексировать как отображение,
состоящее из отсортированных отображений, которые, в свою очередь, состоят
из отсортированных отображений и т.д. Это дает немало удобств для выбора мо¬
дели индексирования.
Резюме
В этой главе были рассмотрены основы применения базы данных Cassandra
на уровне ускорения. У нес имеются и другие возможности вроде поддержки
окончательной согласованности, которые не были рассмотрены в этой главе,
поскольку они не требуются для примера приложения SuperWebAnalytics.com.
Имеется немало других ресурсов, которыми вы можете воспользоваться для до¬
полнительного изучения данного вопроса.
В качестве базы данных с произвольной записью и чтением оперировать ба¬
зой данных Cassandra значительно труднее, чем базой данных на уровне обслужи¬
вания. Уплотнение данных в оперативном режиме и потребность в восстановле¬
нии уравнвешенности диапазонов ключей, если пользоваться разделителем типа
OrderPreservingPartitioner, может вызвать значительные трудности. Правда,
переходный характер уровня ускорения в лямбда-архитектуре в значительной
степени защищает разработчика. Так, если что-нибудь пойдет совсем не так, то
уровень ускорения можно полностью перестроить, отказавшись от его прежнего
состояния.
Организация очередей
и обработка потоков
В этой главе...
■ Однопользовательские очереди
в сравнении с многопользовательскими.
■ Поочередная обработка потоков.
■ Ограничения, присущие" подходам к обработке
потоков с помощью очередей и рабочих
процессов
Мы рассмотрели две разновидности архитектур для организации уровня
ускорения: синхронную и асинхронную. С одной стороны, в синхронных архи¬
тектурах приложения посылают запросы на обновления непосредственно базе
данных и блокируются до тех пор, пока не получат ответ. Такие приложения
требуют согласования разных задач, но к этому, по существу, нечего добавить
с архитектурной точки зрения. А с другой стороны, в асинхронных архитектурах
базы данных уровня ускорения обновляются независимо от приложения, кото¬
рое сформировало данные. Решение о сохранении и обработке запросов на об¬
новление непосредственно зависит от масштабируемости и отказоустойчивости
системы в целом.
В этой главе излагаются основы организации очередей и обработки потоков,
образующих основу асинхронных архитектур. Как было показано ранее, глав¬
ным условием для организации пакетной обработки является способность про¬
тивостоять отказам и повторять вычисления по мере надобности. Те же самые
принципы переносятся и на уровень ускорения, поскольку отказоустойчивость
и повторные вычисления имеют особое значение в системах обработки потоков.
Как обычно, инкрементный характер уровня ускорения усложняет дело, а кроме
того, при разработке приложений следует иметь в виду намного больше компро¬
миссов.
278
Часть III. Уровень ускорения
После обсуждения потребности в организации постоянных очередей мы сде¬
лаем краткий обзор простейшей поочередной обработки потоков. Как будет по¬
казано далее, отказоустойчивость и повторность вычислений теряют свою силу
для такого рода обработки.
14.1. Организация очередей
Чтобы стала понятнее потребность в организации постоянных очередей
для асинхронных систем, сначала нужно рассмотреть их архитектуру без очере¬
ди. В такой системе события обрабатываются непосредственно в рабочих про¬
цессах независимо друг от друга. Подобный подход иллюстрируется на рис. 14.1.
Рис. 14.1. Чтобы реализовать асинхронную обработку без очередей,
клиент передает событие на обработку, не контролируя удачность ее исхода
Такой метод действует автономно по принципу “запустил и забыл” и не мо¬
жет гарантировать удачный исход обработки. Так, если рабочий процесс пре¬
кращается до завершения назначенной для него задачи, то для обнаружения или
исправления ошибки отсутствует подходящий механизм. Рассматриваемая здесь
архитектура подвержена также резкому росту сетевого трафика, превышающего
ресурсы кластера, где обрабатываются данные. В подобных случаях кластер ис¬
пытывает перегрузку, что может стать причиной потери сообщений.
Упомянутые выше затруднения позволяют преодолеть запись событий в по¬
стоянную очередь. В частности, очереди дают системе возможность повторять
события для их буферизации, когда нисходящие рабочие процессы достигают
своих пределов в обработке данных.
И хотя теперь очевидно, что асинхронной архитектуре требуется очередь
для постоянного хранения потока событий, семантика подходящей структуры
очереди требует дополнительного обсуждения. И начать его лучше всего с таких
известных интерфейсов очередей, как Queue в Java. Ниже приведено объявление
этого интерфейса вместе с методами, уместными для данного обсуждения.
Удаляет элемент interface Queue { I Вводит новый
из головы очереди void add (Object item); <, I элемент в очередь
—1> Object poll();
Object peek(); < 1
1 Проверяет элемент в голове
' I очереди, не удаляя его
14.1.1. Серверы однопользовательских очередей
Упомянугый выше интерфейс Queue служит естественной отправной точкой
при разработке постоянной очереди для уровня ускорения. В действительно¬
сти аналогичный интерфейс применяется в реализациях очередей вроде Kestrel
и RabbitMQ. У них общая однопользовательская структура, как показано ниже.
Лмм 14. Организация очередей и обработка потоков
гп
Извлекает элемент
типа Item для
обработки
Сообщает о неудачном
исходе обработки
элемента типа Item
struct Item { <J
long id;
byte[] item;
1
interface Queue {
Item get ();
void ack(long id) ;
> void fail(long id);
1
Обобщенная структура состоит
из идентификатора и полезной
двоичной информации
Подтверждает удачный исход
обработки элемента типа item
Структура однопользовательской очереди основывается на следующем прин¬
ципе: когда событие читается из очереди, оно не удаляется из нее сразу, а воз¬
вращается функцией get () как элемент очереди, содержащий идентификатор,
используемый в дальнейшем для подтверждения удачного (аск) или неудачного
(fail) исхода обработки события. Событие удаляется из очереди лишь в том
случае, если подтвержден удачный исход его обработки. А если он окажется
неудачным или истечет время ожидания, сервер очередей разрешит другому
клиенту извлечь то же самое событие путем отдельного вызова функции get ().
Следовательно, при таком подходе событие может быть обработано несколько
раз (например, когда клиент прекратит свою работу, так и не завершив обработ¬
ку события), но обработка каждого события гарантируется хотя бы один раз.
Все это, конечно, замечательно, но у такой структуры очереди имеется сле¬
дующий изъян: что, если пользователями одного и того же потока окажутся не¬
сколько приложений? А ведь такое случается очень часто. Так, если имеется по¬
ток просмотров страниц, то одно приложение может построить представление
количества просмотров страниц во времени, тогда как другое — представление
количества индивидуальных посетителей страниц во времени. Одно из возмож¬
ных решений этой проблемы состоит в том, чтобы заключить все приложения
в оболочку одного и того же пользователя, как показано на рис. 14.2.
Рис. 14.2. Несколько приложений совместно используют один клиент очереди
В таком случае все приложения будут находиться в одной и той же кодовой
базе выполняя те же самые рабочие процессы. Но такое проектное решение
неудачно потому, что оно исключает любую обособленность среди независи¬
мых приложений. И если в отсутствие подобной обособленности в одном из
280
Часть III. Уровень ускорения
приложений обнаружится программная ошибка» то она может отрицательно
сказаться на нормальном функционировании всех остальных приложений» дей¬
ствующих в пределах того же самого пользователя. Данная проблема еще боль¬
ше обостряется в крупных организациях, где нескольким группам пользователей
требуется совместно пользоваться одним и тем же потоком. Без сомнения, на¬
много лучше (и благоразумнее) обособить независимые приложения таким об¬
разом, чтобы выход из строя одного приложения не сказывался отрицательно
на нормальном функционировании других приложений.
Если очередь имеет однопользовательскую структуру, то добиться независи¬
мости приложений друг от друга можно только в том случае, если поддерживать
отдельную очередь для каждого пользовательского приложения. Очевидный не¬
достаток такого подхода заключается в том, что он приводит к значительному
увеличению нагрузки на сервер очередей. В частности, нагрузка теперь оказы¬
вается пропорциональной количеству приложений, умноженному на количество
входящих событий, а не просто количеству входящих событий. Ведь ничто не
помешает построить десятки представлений с помощью одного и того же по¬
тока, что может в конечном итоге привести к выходу из строя сервера очереди
из-за чрезмерной нагрузки на него.
Обсуждение недостатков однопользовательской очереди помогает выявить
требующиеся свойства системы организации очередей. На самом деле требуется
единственная очередь, которой смогут пользоваться многие пользователи, а до¬
бавление пользователя делается просто и приводит к минимальному увеличению
нагрузки. После подробного анализа выясняется следующий главный недостаток
однопользовательской очереди: в ее обязанности входит слежение за тем, что
именно потребляется. В силу ограничительного условия, когда элемент очере¬
ди “потребляется” или “не потребляется”, такая очередь неспособна изящно об¬
служивать несколько клиентов, которым требуется употребить один и тот же ее
элемент.
14.1.2. Многопользовательские очереди
Правда, имеется альтернативная структура очереди, не страдающая недо¬
статками, присущими однопользовательским очередям. Такая очередь основана
на том принципе, что обязанность следить за состоянием употребленных или не
употребленных событий из очереди возлагается на сами приложения. Так, если
в приложении отслеживаются употребленные события, оно может запросить
воспроизведение потока событий, начиная с любого момента его предыстории.
Такая структура очереди приведена на рис. 14.3.
Серверу очередей неизвестно, когда именно все пользователи обработали
любой заданный элемент очереди, и поэтому он предоставляет соглашение
об уровне обслуживания (SLA) имеющихся событий. Таким образом, он га¬
рантирует доступность некоторой части потока, например, всех событий, на¬
ступивших за последние 12 часов, или последних 50 Гбайт информации о со¬
бытиях. Характерным примером реализации многопользовательской очереди
служит система Apache Kafka, предоставляющая интерфейс, аналогичный
описанному выше.
Й«Ю 14. организация очередей и обработка потоков
281
“Отправить 3 элемента,
Рис. 14.3. При наличии многопользовательской очереди приложения запрашивают
конкретные элементы из очереди и берут на себя обязанность
следить за удачным исходом обработки каждого события
У однопользовательских и многопользовательских очередей имеется еще
одно примечательное отличие. С одной стороны, сообщение удаляется из од¬
нопользовательской очереди, как только подтвердится его успешная обработка,
после чего оно больше не воспроизводится. В итоге событие, которое не уда¬
лось обработать, может стать причиной обработки потока событий не по поряд¬
ку. Так, если поток потребляется параллельно, а событие не удалось обработать,
то остальные события могут быть обработаны удачно, прежде чем будет сделана
повторная попытка. А с другой стороны, многопользовательская очередь позво¬
ляет вернуться назад и воспроизвести поток с того момента, когда не удалось
обработать событие. Тем самым гарантируется, что события будут обрабатывать¬
ся в том порядке, в каком они наступали. Возможность воспроизводить поток
событий является большим преимуществом многопользовательских очередей,
у которых отсутствуют какие-нибудь заметные недостатки в сравнении с одно¬
пользовательскими очередями. Именно поэтому и рекомендуется пользоваться
многопользовательскими очередями вроде Apache Kafka.
14.2. Обработка потоков
Как только входящие события будут поставлены в многопользовательскую
очередь, их нужно каким-то образом обработать, обновив представления в реаль¬
ном времени. Такая норма практики называется обработкой потоков и показана
на рис. 14.4.
Рис. 14.4. Обработка потоков
За последние годы появились две модели обработки потоков: поочередная и ми-
кропакетпая. Сравним эти модели, поскольку каждой из них присущи свои достоин¬
ства и недостатки. Обе модели вполне дополняют друг друга, поскольку одни при¬
ложения в большей степени пригодны для поочередной обработки потоков, тогда
как друтие - для микропакетной. Достоинства обеих моделей сведены на рис. 14.5.
В этой главе мы уделим основное внимание поочередной обработке потоков,
а к обсуждению микропакетной обработки по токов вернемся в следующей главе.
282
Часть III. Уровень ускорения
Поочередная
обработка потоков
Микропакетная
обработка потоков
Малая задержка
Повышенная
производительность
Семантика "хотя бы однажды”
Семантика "только однажды”
Иногда
Более простая модель
программирования
Рис. 14.5. Сравнение двух моделей обработки потоков
Немалое преимущество поочередной обработки потоков заключается в том,
что потоки можно обрабатывать с меньшей задержкой, чем при микропакетной
обработке. К примерам приложений, выгодно пользующихся этим преимуще¬
ством, относятся оповещение и торговля на финансовых рынках.
Начнем построение общей модели поочередной обработки потоков, соблюдая
сначала устарелый подход в виде модели очередей и рабочих процессов. Недостатки,
которыми страдает такой подход, побуждают к поиску более общего способа ре¬
шения данной задачи.
14.2.1. Очереди и рабочие процессы
Модель очередей и рабочих процессов служит распространенным средством
для достижения поочередной обработки потоков. В основу этой модели положе¬
но разделение конвейера обработки на рабочие процессы и размещение между
ними очередей. Если при такой структуре рабочий процесс завершится неудачно
или начнется заново, он может быть просто продолжен там, где был оставлен,
чтением из его очереди (рис. 14.6).
Рис. 14.6. Система, в которой применяется архитектура очередей и рабочих процессов. На этой
блок-схеме очереди могли бы быть потенциально распределенными очередями
Допустим, представление количества просмотров страниц во времени ре¬
ализуется с помощью модели очередей и рабочих процессов, как показано на
Глаш 14. Организация очередей и обработка потоков
283
^Огп-я ич пяля ГГМ ^>^Де Ра®очих процессов события просмотров страниц чита-
недостоверных а’ КаЖДЫЙ пРосмотР страницы проверяется для отсеивания
14 папрр пппг * затем передает события второму ряду рабочих процессов.
пит кпв еТЫ КОЛИ1*ества просмотров страниц обновляются по достоверным
ияь во втором ряде рабочих процессов.
Рис. 14.7. Подсчет количества просмотров страниц во времени
в архитектуре очередей и рабочих процессов
14.2.2. Препятствия, скрытые в модели
очередей и рабочих процессов
Модель очередей и рабочих процессов не сложная, но не обязательно простая.
Один из недостатков такой модели заключается в необходимости гарантировать,
чтобы в нескольких рабочих процессах не предпринимались попытки обновить
подсчет количества просмотров страницы одновременно по одному и тому же
иЛЬ. В противном случае может возникнуть условие гонок, когда операции записи
выполняются в базе данных. Чтобы не допустить состояния гонок, исходящий по¬
ток разделяется в первом ряде рабочих процессов по ІЛ^Ь. При таком разделении
весь ряд ЕШЬ будет по-прежнему распределен по очередям, но события просмо¬
тров страницы по любому заданному иЯЬ будут всегда направляться в одну и ту же
очередь. Чтобы реализовать это, достаточно выбрать целевую очередь, получив
хеш-значение ІЛЇЬ по модулю количества целевых очередей. К сожалению, след¬
ствием неудачного распределения по очередям является слабая отказоустойчи¬
вость. Так, если прекратится рабочий процесс, в котором обновляются подсчеты
количества просмотров страниц, то ни в одном из других рабочих процессов не
удастся обновить базу данных для этой части потока. И тогда неудачно завершив¬
шийся рабочий процесс придется снова запустить вручную в каком-нибудь другом
месте или же построить специальную систему, чтобы делать это автоматически.
Еще один недостаток заключается в том, что наличие очередей между отдель¬
ными рядами рабочих процессов накладывает эксплуатационное бремя на си¬
стему. Если потребуется изменить конфигурацию обработки, для этого придется
согласовать действия таким образом, чтобы промежуточные очереди заново очи¬
щались перед развертыванием.
Кроме того, очереди вносят задержку и снижают пропускную способность
системы, поскольку каждое событие, передаваемое из одного рабочего процесса
в другой, вынуждено проходить через промежуточную очередь, где оно должно
284
Часть III, Уровень ускорения
храниться на диске. Помимо всего прочего, каждая промежуточная очередь тре¬
бует управления и контроля, добавляя еще один уровень, где требуется масштаби¬
рование.
Но самым большим недостатком модели очередей и рабочих процессов, ве¬
роятно, является трудоемкость ее построения. Большая часть кода специально
предназначена для сериализации и десериализации с целью передавать объект
через очереди, реализации логики маршрутизации, соединяющей рабочие про¬
цессы, а также инструкций по развертыванию рабочих процессов в кластере
серверов. И когда все это будет оговорено и сделано, то на бизнес-логику фак¬
тически придется лишь очень малая доля кодовой базы. Тот факт, что рабочие
процессы действуют совместно для достижения некоторой более высокой цели,
а следовательно, им требуется тщательное согласование, явно свидетельствует
о необходимости абстракции на более высоком уровне.
14.3. Высокоуровневая обработка потоков
Рассматриваемый здесь подход к высокоуровневой обработке потоков состо¬
ит в обобщении упомянутой выше модели очередей и рабочих процессов, но без
ее сложностей. Как и в этой модели, в данном случае кортежи обрабатываются
по очереди, но код выполняется параллельно по всему кластеру, а следователь¬
но, система масштабируется для достижения высокой пропускной способности.
Напомним, что назначение уровня ускорения состоит в том, чтобы обработать
поток и обновить представления в реальном времени. Поэтому наша цель — до¬
биться этого с минимальными хлопотами и полной гарантией обработки данных.
Ранее в данной книге был представлен каркас MapReduce в качестве модели
для масштабируемых пакетных вычислений с конкретной реализацией в системе
Hadoop. Аналогичная модель существует и для поочередной обработки запросов,
хотя у нее и нет краткого, привлекательного названия. Поэтому далее будем на¬
зывать ее моделью Storm по наименованию проекта, в котором впервые были вне¬
дрены рассматриваемые здесь методы. Итак, сделаем краткий обзор этой модели,
чтобы выяснить, насколько она облегчает трудности, присущие модели очередей
и рабочих процессов.
14.3.1. Модель Storm
В модели Storm весь конвейер обработки потоков представлен в виде графа
вычислений, называемого топологией. Вместо того чтобы писать отдельные про¬
граммы для каждого узла этой топологии и связывать их вручную, как это пред¬
усмотрено в модели очередей и процессов, для модели Storm требуется един¬
ственная программа, которая развертывается по всему кластеру. Столь гибкий
подход позволяет отфильтровать в едином исполняемом коде данные, находя¬
щиеся в одном узле, вычислить агрегаты в другом узле и обновить базы данных
представлений в реальном времени в третьем узле. Сериализацию, передачу со¬
общений, обнаружение задач и отказоустойчивость можно обеспечить автома¬
тически с помощью абстракций. И все это можно сделать с малой задержкой не
более 10 мс. Если раньше каждую из этих функциональных возможностей при¬
ходилось разрабатывать и программировать в отдельности, то теперь основное
внимание можно уделить бизнес-логике.
Глава 14. Организация очередей и обработка потоков
285
остроим модель Storm с самого начала. Сердцевину модели Storm составляют
потоки, показано на рис. 14.8, поток состоит из бесконечной последовательно¬
сти кортежей, где каждый кортеж представляет собой именованный список значе¬
ний. о существу, назначение модели Storm состоит в преобразовании существую¬
щих потоков в новые потоки с возможным обновлением баз данных по ходу дела.
Кортеж
Кортеж
Кортеж
Кортеж
Кортеж
Кортеж
Кортеж
Поток ►
Рио. 14.8. Поток состоит из бесконечной последовательности кортежей
Следующей абстракцией в модели Storm является сток — источник потоков
в топологии (рис. 14.9). Например, обычный сток позволяет читать данные из
очереди Kestrel или Kafka, превращая их в поток кортежей, тогда как хронирую¬
щий сток — порождать кортеж, направляя его в выходной поток каждые 10 секунд.
Рис. 14.9. Сток является источником потоков в топологии модели Storm
Если стоки являются источниками потоков, то абстракция затворов выполняет
действия над потоками. Затвор принимает любое количество потоков в качестве
входных данных и производит любое количество потоков в качестве выходных
данных (рис. 14.10). Затворы реализуют большинство логики в рассматриваемой
здесь топологии, выполняя функции, фильтруя данные, вычисляя агрегаты, осу¬
ществляя соединения потоков, обновляя базы данных и т.д.
Рис 14 10 Затворы обрабатывают данные, поступающие из одного
или нескольких входных потоков, и производят любое количество выходных потоков
286
Часть ///. Уровень ускорения
Рис. 14.11. Топология связывает
стоки и затворы, а также определяет
порядок прохождения кортежей
через приложение, построенное
по модели Storm
Как только будут определены упомянутые
выше абстракции, топология превратится в сеть
стоков и затворов, где каждый узел представляет
затвор, обрабатывающий выходной поток, посту¬
пающий из другого стока или затвора (рис* 14.11).
Каждый экземпляр стока или затвора называ¬
ется задачей. Главная особенность модели Storm
заключается в том, то ее задачам присуще распа¬
раллеливание подобно тому, как задачам предва¬
рительной обработки и сведения присуще распа¬
раллеливание в MapReduce. Распараллеливание
прохождения кортежей через топологию нагляд¬
но демонстрируется на рис. 14.12.
Стоки и затворы состоят
из многих параллельно
выполняемых задач. Задача
для затвора получает кортежи
из всех задач, формирующих
входной поток для затвора
Рис. 14.12. В топологии стоки и затворы имеют
многие экземпляры, выполняемые параллельно
Разумеется, все задачи для отдельного стока или затвора совсем не обязатель¬
но выполнять на одной машине. Вместо этого они распределяются по разным
рабочим процессам в кластере. В отличие от предыдущего рисунка, на рис. 14.18
показана топология, сгруппированная по физическим машинам.
В связи с распараллеливанием стоков и затворов возникает следующий во¬
прос: если задача порождает кортеж, то какая клиентская задача должна его по¬
лучить? В модели Storm требуется группировка потоков, чтобы указать порядок
распределения кортежей среди клиентских задач. В простейшем случае группи¬
ровка потоков принимает вид перетасованной группировки, где кортежи произволь¬
но распределяются по циклическому алгоритму. Такая группировка позволяет
равномерно распределить нагрузку при обработке благодаря тому, что кортежи
Глава 14. Организация очередей и обработка потоков
287
КпиГодним<типии!.И Произвольно распределяются среди всех пользователей,
оагппелеп ЫМ ВИДОМ гРУппиР°вки является группировка палей, где корте-
ТГтятГГ ХеШИР°ва“ием подмножества „оГей кортежей и получением
Р^ ° модУлю количества клиентских задач. Так, если группировка полей
ocyi яегся по полю word, то все кортежи с одинаковым словом будут предо-
ставлены одной и той же задаче. 77 ^
Рис. 14.13. Физическое представление
распределения задач в топологии из трех серверов
Таким образом, можно составить окончательную блок-схему топологии, обо¬
значив каждое ребро подписки соответствующей группировкой потоков, как по¬
казано на рис. 14.14.
Подкрепим все сказанное выше элементарным примером топологии. Подобно
тому, как подсчет слов служит примером, представляющим MapReduce, выясним,
каким образом должен выглядеть потоковый вариант подсчета слов в модели
Storm. Соответствующая топология для подсчета слов приведена на рис. 14.15.
В затворе разделителя слов поток предложений преобразуется в поток слов, а в
затворе подсчета слов употребляются слова для их подсчитывания. Самое глав¬
ное в данной топологии, что группировка полей осуществляется между затвора¬
ми разделителя и подсчета слов. Этим гарантируется, что каждой задаче подсче¬
та слов доступен экземпляр каждого получаемого слова, что дает возможность
правильно подсчитывать слова.
288
Часть III. Уровень ускорения
Рис. 14.14. Топология с группировкой потоков
Рис. 14.15. Топология для подсчета слов
А теперь рассмотрим псевдокод для реализации затворов. Приведем сначала
следующий код, реализующий затвор разделителя слов:
Затворы получают
кортежи. В данном
случае затвор
получает кортеж
с одним полем
г—О
class SplitterBolt {
function execute(sentence) { <■
Затворы определяются как объекты,
т.к. они могут хранить состояние
}
for(word in sentence.split(" ")) {
emit(word)
}
Направляет слово в выходной поток.
Любые подписчики на этот затвор
получат данное слово
Теперь приведем следующий код, реализующий затвор подсчета слов:
class WordCountBolt {
counts = Map (default=0)
function execute (word) { Подсчеты слов
counts [word] ++ хранятся в памяти
emit(word, counts[word])
)
i
Глава 14. Организация очередей и обработка потоков
289
Как видите, модели Storm не требуется никакой логики дня отправки или се¬
риализации кортежей. Все это может быть сделано ниже абстракций.
14.3.2. Гарантирование обработки сообщений
Представляя модель очередей и рабочих процессов, мы подробно обсудили
вопросы организации очередей между отдельными стадиями обработки данных.
А модель Storm примечательна, в частности, тем, что ее можно реализовать без
всяких промежуточных очередей.
При организации промежуточных очередей обработка сообщений гаранти¬
руется, потому что сообщения не удаляются из очереди до тех пор, пока они не
будут успешно обработаны в рабочем процессе. Если же рабочий процесс пре¬
кратится или произойдет сбой иного рода, то попытка обработать сообщение
будет повторена. Таким образом, организация промежуточных очередей дает га¬
рантию обработать сообщение хотя бы один раз.
Но оказывается, что гарантировать обработку сообщений хотя бы один раз
можно и без промежуточных очередей. Разумеется, это должно быть сделано
иначе, когда повторные попытки происходят не из места, где случается сбой,
а из корня топологии. Поясним это на примере обработки кортежа в топологии
подсчета слов, как показано на рис. 14.16.
Кортежи в стоке
Кортежи
в разделителе
слов
( [“the"] )( [“cow1'] ) ([“jumped"]) ( [“over1
Кортежи
в счетчике
слов
( [“the",1] ) ( [“coW,1] j ([“jumped",ц) ( [“over",1l ) ( [“the", 2] ) ( [“moon",1] )
Рис. 14.16. ОАГ для одного кортежа, порождаемого из стока.
Размер ОАГ быстро растет по мере увеличения объема обрабатываемых данных
Когда в стоке формируется кортеж предложения, он посылается в любые за¬
творы, подписавшиеся на этот сток. В этом случае в затворе разделителя слов
создается шесть новых кортежей на основании этого кортежа из стока. Затем
кортежи слов поступают в затвор подсчета слов, где создается один кортеж
для каждого из этих кортежей слов. Все кортежи, созданные в ходе обработки
единственного кортежа в стоке, можно наглядно представить в виде ориентиро¬
ванного ациклического графа (ОАГ), назвав его ОАГ кортежей. Для более слож¬
ных топологий можно составить более крупные ОАГ кортежей.
Для отслеживания ОАГ кортежей имеется эффективный масштабируемый
алгоритм повторяющий попытки извлечь кортежи из стока в том случае, если
в ходе обработки потока произойдет сбой. Л повторная попытка извлечь кортеж
из стока приведет к тому, что ОАГ кортежей будет составлен заново.
290
Часть III. Уровень ускорения
Эффективное и масштабированное отслеживание ОАГ кортежей
Каким же образом осуществляется эффективное и масштабированное отслежива¬
ние ОАГ кортежей? Ведь ОАГ может состоять из миллионов, а то и больше кортежей,
и поэтому для отслеживания состояния каждого кортежа в стоке может потребоваться
слишком много оперативной памяти. Но оказывается, что ОАГ кортежей можно отсле¬
живать, фактически не следя за ОАГ. Для этого достаточно выделить 20 байт свобод¬
ного пространства в памяти на каждый кортеж в стоке, независимо от размера ОАГ.
Даже если ОАГ состоит из триллионов кортежей, для его отслеживания будет достаточно
и 20 байт. Не вдаваясь в детали алгоритма отслеживания ОАГ кортежей (он подробно
описан в оперативно доступной документации на Apache Storm), отметим лишь, что этот
алгоритм очень эффективно отслеживает сбои, инициируя повторные попытки в ходе
обработки потоков.
На первый взгляд повторная попытка извлечь кортеж из стока кажется ша¬
гом назад, поскольку промежуточные стадии, которые были завершены успеш¬
но, придется повторить снова. Но при более тщательном рассмотрении такая
модель, по существу, ничем не отличается от прежней. В модели очередей и ра¬
бочих процессов отдельная стадия обработки сообщения могла быть успешно
завершена, а перед самым подтверждением этого факта мог произойти сбой, что
дало бы право удалить сообщение из очереди, а затем повторить попытку обра¬
ботать его. В обеих моделях обработка сообщения гарантируется хотя бы один
раз. Более того, как станет ясно в дальнейшем при рассмотрении микропакет-
ной обработки потоков, одной и той же семантики можно добиться, опираясь
на гарантию обработки данных хотя бы один раз, но не прибегая к промежуточ¬
ным очередям. А теперь выясним, как воспользоваться моделью Storm, чтобы
реализовать часть уровня ускорения в приложении SuperWebAnalytics.com.
Соблюдение гарантии обработки данных хотя бы один раз
Как правило, повторная обработка кортежей не дает практически никакого эффекта.
Если операции в топологии идемпотентны, т.е. если повторное выполнение операции не
меняет результат, топология будет иметь семантику "только однажды". Примером идем-
потентной операции может служить ввод элемента в множество. Сколько бы раз эта
операция ни выполнялась, результат будет одним и тем же.
Следует также иметь в виду, что при выполнении операций, не являющихся идемпотент-
ными, можно пренебречь некоторой неточностью. Сбои происходят относительно редко,
и поэтому любые возникающие неточности незначительны. Уровень обслуживания так
или иначе подменяет собой уровень ускорения, и поэтому любая неточность в конечном
итоге может быть исправлена. Кроме того, добиться семантики "только однажды" мож¬
но, принеся в жертву некоторую задержку. Но если малая задержка все-таки важнее,
чем временная неточность, то выполнение операций, не являющихся идемпотентными,
в модели Storm может стать удачным компромиссом.
Глава 14. Организация очередей и обработка потоков
291
14.4. Уровень ускорения приложения
SuperWebAnalytics.com
Напомним, что в приложении SuperWebAnalytics.com реализуются три отдель-
ных запроса на следующее.
■ Количество просмотров страниц за определенный ряд часов.
■ Количество индивидуальных посетителей страниц за определенный ряд
часов.
■ Показатель отказа от просмотров страниц в домене.
В этом разделе мы рассмотрим реализацию запроса на подсчет количества инди¬
видуальных посетителей страниц, а два других запроса — в главе 16. Цель этого за¬
проса — получить количество индивидуальных посетителей страницы по заданному
URL за определенный ряд часов. Алгоритм HyperLogLog производит компактное
представление множества, которое может быть объединено с другими множества¬
ми для подсчета количества индивидуальных посетителей страниц за определенное
количество часов, не сохраняя множество посетителей за каждый час. Компромисс
в данном случае состоит в том, что алгоритм HyperLogLog является аппроксимиру¬
ющим, а следовательно, подсчеты будут неточными на небольшую долю в процен¬
тах. Но такой компромисс вполне приемлем, поскольку он позволяет значительно
сэкономить свободное пространство, тогда как идеальная точность для приложения
SuperWebAnalytics.com не требуется. Аналогичные компромиссы существуют и на
уровне ускорения, что дает возможность пользоваться алгоритмом HyperLogLog
для подсчета количества индивидуальных посетителей страниц на этом уровне.
Напомним также, что в приложении SuperWebAnalytics.com можно отслежи¬
вать посетителей, используя IP-адреса и сведения о регистрации по учетной за¬
писи. Если пользователь зарегистрировался и пользуется мобильным телефоном
и компьютером для посещения одной и той же веб-страницы приблизительно
в одно и то же время, его действия должны быть зарегистрированы как одно по¬
сещение. На уровне пакетной обработки это условие соблюдалось с помощью вза¬
имосвязей между ребрами эквивалентов сначала для отслеживания тех идентифи¬
каторов, которые обозначают одно и то же лицо, а затем для нормализации всех
идентификаторов одного лица с приведением их к единому идентификатору. В
частности, полный анализ ребер эквивалентов можно было выполнить перед тем,
как приступить к подсчету количества индивидуальных посещений по времени.
Справиться с несколькими идентификаторами на уровне ускорения намного
труднее. Главное затруднение состоит в том, что взаимосвязь между нескольки¬
ми идентификаторами может быть установлена после обновления представлений
на уровне ускорения. Рассмотрим в качестве примера следующую последователь¬
ность событий.
■ Пользователь по IP-адресу 11.11.11.111 посещает веб-страницу foo.com/
about в 13:30.
■ Пользователь Салли посещает веб-страницу foo.com/about в 13:40.
■ Ребро эквивалента между пользователями по IP-адресу 11.11.11.111
и Салли обнаруживается в 14:00.
»93
Часть III. Уровень ускорения
Прежде чем в приложении будет выяснена взаимосвязь между эквивалентами,
посещения будут отнесены к двум отдельным посетителям. Поэтому подсчет ко¬
личества индивидуальных посетителей придется уменьшить на единицу по клю¬
чу [иль, час], чтобы получить наиболее точную статистику посещений данной
веб-страницы.
Рассмотрим, что следует для этого сделать в реальном времени. Во-первых,
необходимо отследить граф эквивалентов в реальном времени, т.е. произвести
постепенный анализ всего графа, как пояснялось в главе 8. Во-вторых, нужно
постараться выяснить, был ли один и тот же посетитель подсчитан несколько
раз. Для этого придется хранить все множество посетителей за каждый час.
Алгоритм Нурег1х^1х^ обеспечивает компактное представление такого множе¬
ства и не может нам в этом помочь, поэтому трудности обращения с эквивален¬
тами не удастся разрешить, прибегнув к алгоритму Нурег1л^Ьо^ Помимо всего
прочего, алгоритм инкрементных вычислений слишком сложен для анализа все¬
го графа эквивалентов и коррекции произведенных ранее подсчетов количества
индивидуальных посетителей страниц.
Вместо того чтобы пытаться идеально подсчитать количество индивиду¬
альных посетителей страниц во времени, можно пойти на некоторый компро¬
мисс в отношении точности подсчетов. Напомним, что одно из преимуществ
лямбда-архитектуры заключается в том, что компромисс в отношении точности
вычислений на уровне ускорения не делается безвозвратно, поскольку уровень
обслуживания постоянно подменяет собой уровень ускорения, а следовательно,
любые неточности могут быть исправлены для достижения окончательной точ¬
ности в системе. Таким образом, нужно рассмотреть альтернативные подходы,
трезво взвесив присущие им неточности в сравнении с теми преимуществами,
которые они дают для упрощения вычислений.
Первый альтернативный подход состоит в том, чтобы не выполнять анализ
эквивалентов в реальном времени. Вместо этого результаты пакетного анали¬
за эквивалентов могут быть доступны на уровне ускорения из базы данных пар
“ключ-значение” на уровне обслуживания. Идентификаторы лиц нормализуют¬
ся на уровне ускорения посредством базы данных, прежде чем произвести под¬
счет количества индивидуальных посетителей во времени. Преимущество дан¬
ного подхода заключается в возможности выгодно воспользоваться алгоритмом
Нурег1л^1л^, поскольку в этом случае не приходится иметь дело с эквивален¬
тами в реальном времени. Реализовать такой подход намного проще, затратив
значительно меньше ресурсов.
А теперь выясним, в чем именно состоит неточность данного подхода.
Результаты анализа предварительно подсчитываемых эквивалентов устаревают
через несколько часов, и поэтому любые вновь обнаруженные взаимосвязи меж¬
ду эквивалентами не реализуются. Таким образом, данный подход оказывается
неточным в тех случаях, когда пользователь перемещается по странице, реги¬
стрируется по своему идентификатору, после чего ребро эквивалента фикси¬
руется, а затем пользователь возвращается к той же самой странице в течение
одного и того же часа, но по другому 1Р-адресу. Следует, однако, иметь в виду
что неточность возникает в отношении лишь совершенно новых пользователей,
т.е. любое посещение, происходящее после того, как регистрация пользователя
Глава 14. Организация очередей и обработка потоков
293
будет обработана в результате пакетного анализа эквивалентов, будет зарегистри¬
ровано надлежащим образом. В целом такая неточность довольно незначитель¬
на, чтобы пойти на нее ради большой экономии.
Потенциально можно даже пойти на еще большую неточность. Поэтому вто¬
рой альтернативный подход состоит в том, чтобы полностью проигнорировать
эквиваленты и подсчитать количество индивидуальных посетителей в реальном
времени, исходя только из того идентификатора лица, который имелся в про¬
смотре страницы. Если в таком случае эквивалент был зарегистрирован между
идентификатором пользователя и 1Р-адресом много месяцев назад и если этот
пользователь посетил страницу, зарегистрировался и снова посетил ту же самую
страницу, то этот факт будет зарегистрирован неоднократно при подсчете коли¬
чества посетителей страницы за данный час.
Самое правильное в данном случае — выполнить пакетный анализ для количе¬
ственного определения неточности, возникающей при каждом подходе, чтобы
принять обоснованное решение при выборе наиболее подходящей стратегии.
Чутье нам подсказывает, что полное пренебрежение эквивалентами на уровне
ускорения не должно внести слишком большую неточность в вычисления, и по¬
этому мы выберем именно такой подход ради простоты демонстрируемых далее
примеров. Но прежде подчеркнем еще раз, что любая неточность вычислений
на уровне ускорения является временной, а система в целом оказывается в ко¬
нечном итоге точной.
14.4.1. Структура топологии
А теперь перейдем непосредственно к реализации подсчета количества инди¬
видуальных посетителей страниц на уровне ускорения, пренебрегая эквивален¬
тами. Этот процесс можно разделить на три стадии.
1. Употребление потока событий просмотров страниц, содержащих иденти¬
фикатор пользователя, ТЛи, и отметку времени.
2. Нормализация 1ЖЬ.
3. Обновление базы данных, содержащей вложенное отображение 1ЖЬ
на множество НурегЬс^Ьс^ по часам.
На рис. 14.17 приведена структура топологии для реализации такого подхода.
Рассмотрим подробнее следующие ее составляющие.
■ Сток просмотров страниц. В этом стоке сначала читаются данные из оче¬
реди, а затем порождаются события просмотров страниц по мере их по¬
ступления.
■ Нормализация иКЬ в затворе. В этом затворе происходит нормализация
тех 1114., которые приводятся к своей канонической форме. Алгоритм
этой нормализации должен быть таким же, как и алгоритм, применяю-
щийся на уровне пакетной обработки, и поэтому его целесообразно реали¬
зовать в виде библиотеки, общей для обоих уровней. Кроме того, в этом
затворе отсеиваются любые недостоверные 1Ш..
■ Обновление базы данных в затворе. В этом затворе употребляется по¬
ток из предыдущего затвора, в котором используется группировка полей
294
Часть Уровень ускорения
по ияь для исключения условия гонок при обновлении состояния по лю¬
бому иыь. В этом затворе поддерживаются множества Нурег1л^1х^, хра¬
нящиеся в базе данных, реализующей структуру данных ключей к отсорти¬
рованному множеству Ключом в ней является 1ЖЬ, вложенным ключом -
промежуток времени, а вложенным значением — множество Нурег1л^1х^
В идеальном случае такой базе данных должна быть свойственна поддерж¬
ка множества НурегГх^Гл^, чтобы не извлекать множества Нурег1л^1^
из базы данных и не записывать их обратно в нее.
Рис. 14.17. Топология для подсчета количества индивидуальных посетителей страниц во времени
Вот, собственно, и все. Если аппроксимация производится на уровне ускоре¬
ния путем игнорирования эквивалентов, можно значительно упростить логику
функционирования уровня ускорения.
Очень важно подчеркнуть, что столь сильная аппроксимация может быть
произведена на уровне ускорения только благодаря наличию надежной поддерж¬
ки на нем уровня пакетной обработки. В главе 10 было продемонстрировано
полностью инкрементное решение задачи подсчета количества индивидуальных
посетителей страниц и показано, насколько добавление эквивалентов услож¬
няет решение этой задачи. В полностью инкрементном ее решении просто не
предполагается пренебрежение эквивалентами, поскольку это означало бы их
пренебрежение во всем представлении в целом. И, как было показано ранее,
лямбда-архитектура в этом отношении оказывается намного более гибкой.
Резюме
В этой главе было показано, насколько инкрементная обработка данных
с присущими ей очень строгими ограничения на задержку оказывается сложнее,
чем пакетная обработка. Это объясняется тем, что все данные нельзя просматри¬
вать сразу, а также сложностями баз данных с произвольной записью, например,
при уплотнении данных. Тем не менее в большинстве традиционных архитек¬
тур для представления главного массива данных, а также индексов исторически
и в реальном времени применяется единственная база данных. В этом и состоит
главная сложность, поскольку всем этим свойствам должна быть присуща способ¬
ность к оптимизации, но их переплетение в одной системе не позволяет этого
сделать.
Пример запроса на подсчет количества индивидуальных посетителей стра¬
ниц в приложении SuperWebAnalyrics.com идеально подходит для иллюстрации
подобной трихотомии. Главный массив данных состоит из просмотров стра¬
ниц и эквивалентов, а следовательно, их можно сохранять в массовом порядке
Глава 14. Организация очередей и обработка потоков
295
з распределенной файловой системе, выбрав формат файлов, оптимальный
по затратам на хранение и чтение данных. Распределенная файловая система
не вынуждает разработчика прибегать к ненужным средствам вроде операций
произвольной записи, индексации или уплотнения, предоставляя более простое
и надежное решение.
Исторические представления вычисляются с помощью системы пакетной об¬
работки, где можно выполнять функции сразу над всеми данными. Это дает воз¬
можность анализировать граф эквивалентов, полностью сопоставляя просмотры
страниц с одними и теми же людьми, даже если просмотры страниц осуществля¬
лись под разными идентификаторами лиц. Представление размещается в базе
данных, не поддерживающей операции произвольной записи, что опять же
позволяет избежать излишних сложностей, вносимых ненужными средствами.
А поскольку запись в базу данных при чтении из нее не допускается, то можно
и не беспокоиться об эксплуатационном бремени, которое накладывается таки¬
ми процессами, как уплотнение данных.
И наконец, главными свойствами, которые требуются для представлений в ре¬
альном времени, являются эффективность и малая задержка обновлений. И того
и другого можно добиться на уровне ускорения инкрементными вычислениями
представлений в реальном времени, производя аппроксимацию и пренебрегая
эквивалентами, чтобы ускорить вычисления и упростить реализацию. Базы дан¬
ных с произвольной записью служат для достижения малой задержки, которая
требуется на уровне ускорения, но их сложность в значительной степени ниве¬
лируется тем обстоятельством, что представления в реальном времени довольно
малы, а большая часть данных отображается в пакетных представлениях. В сле¬
дующей главе будет показано, каким образом принципы организации очередей
и обработки потоков, рассмотренные в этой главе, реализуются с помощью на¬
стоящих инструментальных средств.
Иллюстрация
организации очередей
и обработки потоков
В этой главе...
■ Применение системы Apache Storm.
■ Гарантирование обработки сообщений.
■ Интеграция Apache Kafka, Apache Storm
и Apache Cassandra.
■ Реализация обработки запросов на подсчет
количества индивидуальных посетителей
страниц во времени на уровне ускорения
приложения SuperWebAnalytics.com.
В предыдущей главе были рассмотрены многопотребительские очереди и модель
Storm в качестве общего подхода к поочередной обработке потоков. А в этой главе
мы выясним, как воплотить эти принципы на практике, используя настоящие
инструментальные средства Apache Storm и Apache Kafka. И в заключение
реализуем обработку запросов на подсчет количества индивидуальных посетителей
страниц во времени на уровне ускорения приложения SuperWebAnalytics.com.
15.1. Составление топологий средствами Apache Storm
Apache Storm — это проект с открытым кодом, реализующий (и породивший)
модель Storm. Как было показано в предыдущей главе, основу модели Storm
составляют такие понятия, как кортежи, потоки, стоки, затворы и топологии.
А теперь покажем, как реализуется подсчет слов в потоковом режиме с помощью
интерфейса Apache Storm API. Для справки топология для подсчета слов еще раз
приведена на рис. 15.1.
298
Часть III. Уровень ускорения
Рис. 15.1. Топология для подсчета слов
Прежде всего следует получить экземпляр объекта типа TopologyBuilder, что¬
бы определить топологию приложения, как показано ниже. Этот объект обеспе¬
чивает доступ к API для обозначения топологий по модели Storm.
TopologyBuilder builder = new TopologyBuilder ();
Далее нужно ввести сток, порождающий поток предложений. Этот сток на¬
зывается sentencespout, и ему присваивается коэффициент распараллеливания,
равный 8, как показано ниже. Это означает порождение 8 потоков в кластере
для исполнения стока.
builder. setSpout ("sentence-spout", new RandomSentenceSpout (), 8) ;
А теперь, когда имеется поток предложений, требуется затвор, употребляющий
этот поток и преобразующий его в поток слов. Такой затвор называется splitter,
и ему присваивается коэффициент распараллеливания, равный 12, как показано
ниже. Но поскольку отсутствуют какие-либо требования к порядку потребления
предложений, то для равномерного распределения нагрузки при обработке дан¬
ных среди всех 12 задач используется перетасованное группирование.
builder.setBolt("splitter", new SplitSentence(), 12)
.shuffleGrouping("sentence-spout");
И последний затвор употребляет поток слов и производит требующийся по¬
ток подсчетов слов. Он соответственно называется counter, и ему присваивает¬
ся коэффициент распараллеливания, равный 12, как показано ниже. Обратите
внимание на применение в данном случае группирования полей, чтобы возло¬
жить ответственность за итоговый подсчет любого слова только на одну задачу.
builder.setBolt("count", new WordCount(), 12)
.fieldsGrouping("splitter", new Fields("word"));
Как только топология для подсчета слов будет определена, можно перейти
к конкретной реализации стока и затворов. В частности, реализовать затвор раз¬
делителя слов совсем не трудно, как показано в приведенном ниже фрагменте
кода. Он извлекает предложение из первого поля в поступающем кортеже и по¬
рождает новый кортеж для каждого слова в предложении.
Разделяет
предложение
на слова
и направляет
каждое слово
в выходной
поток кортежей
public static class SplitSentence extends BaseBasicBolt {
public void execute(Tuple tuple, BasicOutputCollector collector
String sentence = tuple.getString(0); <i
for(String word: sentence.split (" ")) {
o collector.emit (new Values(word));
Входящий кортеж содержит
единственное предложение
Глава 15. Иллюстрация организации очередей и обработки потоков
299
public void declareOutputFields (OutputFieldsDeclarer declarer) (
declarer.declare (new Fields ("word")); объявляет,
что исходящие потоки
состоят из одного
значения с меткой "word"
Логика функционирования затвора подсчета слов так же проста, как демон¬
стрируется в приведенном ниже фрагменте кода. В этой конкретной реализации
ведется подсчет слов в хеш-отображении, которое находится в оперативной па¬
мяти, хотя ее можно было бы с тем же успехом связать и с базой данных.
Извлекает
из входящего
кортежа
Инициализирует
подсчет, если
слово ранее
не наблюдалось
public static class WordCount extends BaseBasicBolt {
Map<String, Integer> counts =
new HashMap<String, Integer>() ; <j
В отображении,
находящемся
в оперативной памяти,
подсчитываются все слова,
полученные в затворе
public void execute(Tuple tuple, BasicOutputCollector collector)
> String word = tuple.getString(O) ;
Integer count = counts.get (word); «-
n>if (count=null) count = 0;
count++;
counts.put(word, count); o—
collector .emit (new Values (word
Сохраняет
обновленный
подсчет
r count)); <н
Извлекает подсчет
текущего слова
Порождает обновленный
подсчет данного слова
public void declareOutputFields (OutputFieldsDeclarer declarer) {
declarer.declare(new Fields("word", "count"));
}
}
Объявляет исходящие
кортежи, состоящие
из слова и текущего
подсчета
Осталось только реализовать сток. В модели Storm предоставляется целый
ряд предопределенных стоков вроде Kafka или Kestrel для чтения данных из
внешних очередей, но в приведенном ниже фрагменте кода демонстрируется по¬
строение специального стока. Этот сток произвольно порождает одно из своих
предложений каждые 100 мс, формируя бесконечный поток предложений.
public static class RandomSentenceSpout extends BaseRichSpout {
SpoutOutputCollector _collector;
Random rand;
public void open(Map conf, TopologyContext context,
SpoutOutputCollector collector) (
collector = collector;
В Apache Storm
метод nextTuple()
вызывается в цикле
,-P
Массив предложений,
порождаемый стоком I
rand = new Random ( ) ;
jublic void nextTuple ()
Utils.sleep(100);
String[] sentences = new String!]
Ожидает в текущем потоке
исполнения в течение 100 мс
{
"the cow jumped over the moon",
"an apple a day keeps the doctor away",
"four score and seven years ago",
"snow white and the seven dwarfs",
"i am at two with nature");
so«
Часть UL Уровень ускорения
Произвольно
порождает одно
из заданных
предложений
String sentence = sentences [_rand.nextInt (sentences.length) ];
_collector.emit(new Values(sentence));
}
public void declar'eOutputFields (OutputFieldsDeclarer declarer) {
declarer.declare(new Fields("sentence"));
}
Объявляет,
что исходящие кортежи
содержат единственное
предложение
Вот, собственно, и все. А теперь выясним, каким образом действуют кластеры
Apache Storm и как они развертываются.
15.2. Кластеры Apache Storm
и развертывание топологии
Архитектура кластера Apache Storm приведена на рис. 15.2. В системе Apache
Storm имеется главный узел, который называется Nimbus и управляет текущими
топологиями. Узел Nimbus принимает запросы на развертывание топологии
по модели Storm и назначает их для рабочих процессов в кластере для испол¬
нения данной топологии. Кроме того, узел Nimbus отвечает за обнаружение мо¬
ментов, когда прекращаются рабочие процессы и их перераспределение среди
других машин по мере необходимости.
Nimbus
О Nimbus является главным
узлом системы Apache
Storm. В нем назначаются
и контролируются рабочие
процессы для обеспечения
правильного исполнения
топологии
Кластер ZooKeeper
ZooKeeper
ZooKeeper
ZooKeeper
© Механизм Apache
ZooKeeper обеспечивает
весьма надежное согла¬
сование распределенных
вычислений. ZooKeeper
применяется в системе
Apache Storm для отсле¬
живания конфигурацион¬
ной информации
о топологии
Кластер Storm
Supervisor
Рабочий
процесс
Рабочий
процесс
Рабочий
процесс
Узел рабочего
процесса
Supervisor
Рабочий
процесс
Узел рабочего
процесса
Supervisor
Рабочий
процесс
Рабочий
процесс
Узел рабочего
процесса
© У каждого рабочего
процесса в кластере Storni
имеется демон Supervisor
выполняющий задачи,
направляемые ему узлом
Nimbus Рабочие процессы
направляют ZooKeeper
запрос на определение
местоположения задач
и дру| их рабочих
процессах
Рис. 15.2. Архитектура Apache Storm
Глава 15. Иллюстрация организации очередей и обработки потоков 301
В центре архитектуры Apache Storni находится механизм Zookeeper — еще один
проект рас ie, предназначенный для хранения небольших частей состояния
и о еспечивающий семантику, идеально подходящую для согласования работы
кластера. архитектуре Apache Storni механизм Zookeeper отслеживает назна¬
чение задач для рабочих процессов и прочую конфигурационную информацию
о топологии. Типичный кластер Zookeeper в архитектуре Apache Storm состоит
из трех или пяти узлов.
Последнюю группу в архитектуре Apache Storm составляют узлы рабочих про¬
цессов. Каждый узел рабочего процесса выполняет демон, который называется
Supervisor и связывается с узлом Nimbus через механизм Zookeeper, чтобы опреде¬
лить, что именно следует выполнять на машине. Затем демон Supervisor начина¬
ет или останавливает рабочие процессы по мере необходимости под управлени¬
ем Nimbus. В одних рабочих процессах обнаруживается местоположение других
рабочих процессов посредством Zookeeper и осуществляется обмен сообщения¬
ми непосредственно между ними.
В приведенном ниже фрагменте кода показано, каким образом развертывает¬
ся топология для подсчета слов, построенная в разделе 15.1.
public static void main(String [] args) throws Exception {
TopologyBuilder builder = new TopologyBuilder ();
/
builder.setSpout("sentence-spout", new RandomSentenceSpout () ,8);
builder.setBolt("Splitter", new SplitSentence(), 12)
.shuffleGrouping("sentence-spout") ;
builder.setBolt("count", new WordCount(), 12)
.fieldsGrouping("Splitter", new Fields("word"));
Предоставляет
имя при передаче
топологии на
рассмотрение
Config conf = new ConfigO ;
conf. setNumWorkers (4); <
StormSubmitter.submitTopology(
^ "word-count-topology",
conf, builder.createTopology());
conf.setMaxSpoutPending(1000); о
}
Порождает четыре узла
рабочих процессов среди
серверов Storm
Задает количество
неподтвержденных кортежей,
которые способе породить сток
Конфигурация топологии содержит параметры, настраивающие топологию
в целом. В приведенном выше примере кода демонстрируется конфигурация,
в которой системе Storm предписывается породить в кластере четыре рабочих
процесса для исполнения топологии. Напомним, что ранее при определении
топологии был указан коэффициент распараллеливания для каждого стока и за¬
твора Так, стоку предложения был присвоен коэффициент распараллеливания,
равный 8, а обоим затворам разделителя и счетчика слов - коэффициент распа¬
раллеливания, равный 12. Эти коэффициенты распараллеливания обозначают
количество потоков исполнения, которые должны быть порождены для данного
стока или затвора. Таким образом, система Storm равномерно распределяет ра¬
бочие процессы в кластере и столь же равномерно задачи по рабочим процес¬
сам, но правила их распределения можно изменить, встроив специальный пла¬
нировщик в узел Nimbus.
308
Часть IJL Уровень ускорения
В приведенном выше примере кода демонстрируется еще одна конфигурация,
осуществляемая на уровне топологии при резком увеличении объема входящих
данных. Так, если резко нарастает поток входящих событий, то очень важно,
чтобы потоковый процессор не был перегружен и не вышел из строя из-за по¬
вышенной нагрузки, которая, например, приводит к исчерпанию оперативной
памяти. В системе Storm имеется простой механизм для управления потоком
на основании средств, гарантирующих обработку сообщений. Так, параметр
максимального ожидания в стоке топологии управляет максимальным количеством
кортежей, которые могут быть порождены из стока, но еще не полностью обра¬
ботаны в топологии. Как только этот предел будет достигнут, задачи в стоке пе¬
рестанут порождать кортежи до тех пор, пока они будут или не будут подтверж¬
дены или же не истечет время ожидания. В приведенном выше примере кода си¬
стеме Storm предписывается, что наибольшее количество ожидающих кортежей
для любой задачи в стоке не должно превышать 1000. А поскольку коэффициент
распараллеливания в стоке предложения равен 8, то количество ожидающих
кортежей во всей топологии в целом должно быть не больше 8000.
15.3. Гарантирование обработки сообщений
В предыдущей главе было показано, что модель Storm позволяет гарантиро¬
вать обработку сообщений без промежуточных очередей сообщений. Если ниже
стока в ОАГ кортежей обнаруживается сбой, то может быть осуществлена по¬
вторная попытка породить кортежи из стока. А теперь рассмотрим подробнее,
каким образом все это действует в системе Apache Storm.
В системе Apache Storm кортеж успешно обрабатывается в стоке только в том
случае, если весь ОАГ кортежей исчерпан и каждый узел в нем отмечен как го¬
товый. Кроме того, весь этот процесс должен произойти в заданном промежут¬
ке времени ожидания (по умолчанию 30 секунд). Установкой времени ожидания
гарантируется, что сбои будут обнаружены независимо от того, что произойдет
по ходу дела, будь то зависание рабочего процесса или неожиданный выход ма¬
шины из строя.
На пользователя системы Storm возлагаются две обязанности, чтобы выгодно
воспользоваться гарантией обработки сообщений. Он должен уведомить систем)
Storm всякий раз, когда требуется создать ребро зависимости в ОАГ кортежей
и когда следует завершить обработку кортежа. Обе эти задачи называются привяз¬
кой и подтверждением соответственно. Рассмотрим снова следующий код, разделя¬
ющий предложение на слова для их подсчета с помощью логики ОАГ кортежей:
public static class SplitExplicit extends BaseRichBolt {
OutputCollector _collector;
public void prepare(Map conf, TopologyContext context,
OutputCollector collector) {
collector = collector;
Подклассы,производные
от класса ВавеШсЬВогь,
требуют явной привязки или
подтверждения кортежей
}
public void execute(Tuple tuple) (
String sentence = tuple.getString(0);
for(String word: sentence.split (" ")) {
Глава 15. Иллюстрация организации очередей и обработки потоков
collector .emit (tuple, new Values (word));
collector.ack(tuple); <]
Привязывает исходящее слово
к кортежу входящего предложения
Подтверждает успешную
обработку кортежа предложения
public void declareOutputFields (OutputFieldsDeclarer declarer) (
declarer.declare (new Fields ("word"));
303
Семантика этого затвора, по существу, идентична исходной реализации из пре¬
дыдущего раздела. Когда порождается новый кортеж слова, кортеж предложения
указывается в качестве первого аргумента при вызове метода emit (). В ходе этого
процесса кортеж слова привязывается к кортежу предложения. И как только будут
порождены новые кортежи, подтверждается успешная обработка кортежа пред¬
ложения, поскольку дальнейшая обработка не требуется. Такая привязка всех ис¬
ходящих кортежей к входящему кортежу с последующим подтверждением успеш¬
ной обработки кортежа является весьма распространенным шаблоном. Поэтому
для автоматизации такого поведения в системе Storm предоставляется класс
BaseBasicBolt, берущий на себя все хлопоты по привязке и подтверждению. Этот
класс использован в первой реализации затвора разделителя слов.
Но шаблон типа BaseBasicBolt пригоден не для всех операций, особенно
если требуется агрегирование или соединение потоков. Допустим, требуется об¬
рабатывать по 100 кортежей за один раз. В таком случае все входящие кортежи
можно было бы сохранить в буфере, привязать исходящий кортеж ко всем 100
обрабатываемым кортежам, а затем подтвердить успешную обработку всех корте¬
жей в буфере. Такой подход демонстрируется в следующем фрагменте кода, где
выдается сумма каждых 100 кортежей:
public static class MultiAnchorer extends BaseRichBolt { о
OutputCollector _collector;
public void prepare(Map conf, TopologyContext context,
OutputCollector collector) {
_collector = collector;
} Затвор типа MultiAnchorer складывает значения
из 100 входящих кортежей и выдает их сумму
List<Tuple> _buffer = new ArrayList<Tuple> ();
Суммирует
входящие
значения
int _sum = 0;
public void execute (Tuple tuple)
_sum += tuple.getlnteger(O);
if( buffer.size () < 100) {
Вводит кортежи в буфер
вплоть до его заполнения
Подтверждает
успешную
обработку
кортежей |
в буфере 1_
_buffer.add(tuple); <
else {
_collector.emit(_buffer, new Values(_sum)) ;
for(Tuple _tuple : _buffer) {
collector.ack(_tuple);
Когда буфер заполнен,
порождает кортеж с
суммой, привязанный ко
всем кортежам в буфере
}
buffer.clear (); < i Л _ .
Очищает буфер
, > sum = 0; 1
Обнуляет | -
текущую '
сумму I }
304
Часть III. Уровень ускорении
public void declareOutput Fields (Output Fields Declarer declarer) {
declarer.declare(new Fields("sum"));
}
В данном случае воспользоваться шаблоном типа BaseBasicBolt нельзя, пото¬
му что успешная обработка кортежей не подтверждается сразу же по завершении
функции execute ( ). Ведь они буферизированы, а следовательно, их успешная об¬
работка подтверждается позднее.
Внутренний механизм Storm отслеживает ОАГ кортежей, применяя алгоритм,
требующий лишь около 20 байт свободного пространства на каждый кортеж
в стоке независимо от размера ОАГ. Даже если ОАГ состоит из триллионов кор¬
тежей, для его отслеживания будет достаточно и 20 байт. Не вдаваясь в детали
алгоритма отслеживания ОАГ кортежей (он подробно описан в оперативно до¬
ступной документации на Apache Storm), отметим лишь, что он очень эффектив¬
но отслеживает сбои, инициируя повторные попытки в ходе обработки потоков.
15.4. Реализация подсчета индивидуальных
посещений страниц во времени на уровне
ускорения приложения SuperWebAnalytics.com
В предыдущей главе была рассмотрена структура уровня ускорения для под¬
счета количества индивидуальных посетителей страниц во времени в прило¬
жении SuperWebAnalytics.com. Ее главный замысел состоял в том, чтобы про¬
извести аппроксимацию, проигнорировав эквиваленты с целью значительно
упростить реализацию. На рис. 15.3 для справки еще раз показана структура то¬
пологии для этого уровня ускорения. А теперь нам предстоит реализовать эту
топологию, используя инструментальные средства Apache Storm, Apache Kafka
и Apache Cassandra.
Рис. 15.3. Топология для подсчета количества индивидуальных посетителей страниц во времени
Начнем реализацию этой топологии со стока. В приведенном ниже фрагменте
кода используется сток Kafka для чтения просмотров страниц из серверов Kafka.
При этом предполагается, что просмотры страниц хранятся в системе Kafka в виде
объектов типа Data из каркаса Apache Thrift, определенных в главе 3.
Тема сообщений
в Kafka для стока
TopologyBuilder builder = new TopologyBuilder () ;
SpoutConfig spoutConfig = new SpoutConfig(
new KafkaConfig.ZkHosts("zkserver:1234", "/kafka"),
—ч> "pageviews", Пространство имен
"/kafkastorm", -> Zookeeper, используемое
в системе Storm для стока
Использует адрес
кластера Zookeeper
■ ь и пространство имен
Kafka в механизме
Zookeeper
Глава 15. Иллюстрация организации очередей и обработки потоков
305
Идентификатор
стока
Создает сток
с коэффициентом
распараллеливания,
равным 1б
і—о "uniquesSpeedLayer") ;
І зрі - -
spoutConfig, scheme = new PageviewScheme ( ) ;
builder.setSpout("pageviews",
new KafkaSpout(spoutConfig), 16) ;
Задает схему
для десериализации
двоичных записей в объекты
типа Data в строке
ольшей части приведенного выше кода устанавливается конфигурация,
в том числе подробности настройки кластера Kafka, выбора темы сообщений
и места, где механизму Zookeeper следует записывать то, что было до сих пор
употреблено в стоке. А далее необходимо нормализовать URL в событиях про¬
смотра страниц, как показано ниже.
public static class NormalizeURLBolt extends BaseBasicBolt {
public void execute (Tuple tuple, BasicOutputCollector collector) (
PersonID user = (PersonID) tuple.getValue (0);
String url = tuple.getString(l) ;
int timestamp = tuple.getlnteger (2);
try (
collector .emit (new Values (user, normalizeURL (url), «-
timestamp, user ) ) ; Отсеивает кортеж, ничего
} не порождая при неудачном
catch (MalformedURLException,e) (} <з исходе нормализации URL
Г1ЫТЭ6ТСЯ НОрмаЛИЗОВаТЬ UnLy
используя общую с уровнем
пакетной обработки функцию
}
public void declareOutputFields (OutputFieldsDeclarer declarer) {
declarer, declare (new Fields ("user", "url", "timestamp"));
)
И наконец, необходимо обновить множества HyperLogLog в базе данных
Cassandra. Начнем с простого варианта. В следующем фрагменте кода сначала из¬
влекается множество HyperLogLog, соответствующее заданному просмотру стра¬
ницы, а затем оно обновляется и опять записывается в базу данных Cassandra.
public static class UpdateCassandraBolt extends BaseBasicBolt {
public static final int HOURS_SECS = 60 * 60;
ColumnFamilyTemplate<String, Integer> _template;
public void prepare (Map conf, TopologyContext context) { <i
Cluster cluster =
HFactory.getOrCreateCluster("mycluster", "127.0.0.1");
Инициализирует классы
для извлечения и сохранения
значений в базе данных
Cassandra
Keyspace keyspace - . . .
HFactory.createKeyspace("superwebanalytics , cluster),
new ThriftColuirmFamilyTemplate<String, Integer> (keyspace,
"uniques", StringSerializer.get(), IntegerSerializer.get());
public void execute(Tuple tuple, BasicOutputCollector collector) (
PersonID user = (PersonID) tuple.getValue(O);
306
Часть III. Уровень ускорения
}
String url = tuple.getString(l);
int bucket = tuple, getlntegertf) / H0URSJ3ECS; « , извлекает данные пользователя,
URL, часовой промежуток времени
HColumn<lnteger, byte [ ] > hcol = из входящего кортежа
_template.querySingleColumn(url, bucket,
BytesArraySerializer.get ());
HyperLogLog hll;
try {
if (hcol—null) hll = new HyperLogLog (800) ;
else hll = HyperLogLog. Builder, build (hcol. getValueO ); <ь
hll.offer(user);
Извлекает множество
HyperLogLog из базы данных
Cassandra, а если оно не найдено,
то инициализирует новое
множество
ColumnFamilyUpdater<String, Integer> updater =
_template.createUpdater(url);
updater.setByteArray(bucket, hll.getBytes());
_template.update(updater); <
} catch (IOException e) { throw new RuntimeException (e); }
Вводит данные пользователя
в множество HyperLogLog
и обновляет базу данных Cassandra
public void declareOutputFields (OutputFieldsDeclarer declarer) {
// пусто, t.k. затвор не порождает выходной поток
Ради полноты примера ниже приведен фрагмент кода, где все составляющие
данной топологии связываются вместе.
public static void main(String[] args) {
TopologyBuilder builder = new TopologyBuilder () ;
SpoutConfig spoutConfig = new SpoutConfig(
new KafkaConfig.ZkHosts("zkserver:1234", "/kafka"),
"pageviews", "/kafkastorm", "uniquesSpeedLayer");
spoutConfig.scheme = new PageviewScheme ();
builder.setSpout("pageviews", new KafkaSpout(spoutConfig), 16);
builder.setBolt("extract-filter", new NormalizeURLBolt(), 32)
.shuffleGrouping("pageviews");
builder.setBolt("cassandra", new UpdateCassandraBolt(), 16)
.fieldsGrouping("extract-filter", new Fields("url"));
Следует иметь в виду, что данная топология полностью отказоустойчива.
Кортеж считается успешно обработанным в стоке лишь после того, как будет
обновлена база данных, и поэтому любой сбой может привести к повторному
воспроизведению кортежа в стоке. Сбои и повторные попытки не оказывают
никакого влияния на точность системы, поскольку ввод данных в множество
HyperLogLog является идемпотентной операцией.
Недостаток приведенного выше кода для обновления базы данных Cassandra
заключается в том, что он требует немалых издержек на извлечение множеств из
базы данных Cassandra и обратной их записи в нее. В идеальном случае база дан¬
ных должна внутренне поддерживать множества HyperLogLog, чтобы избежать
подобных издержек, но база данных Cassandra таким свойством не обладает.
Тем не менее намного большей эффективности можно добиться, группи¬
руя вместе обновления, особенно если одно и то же множество может быть
Глава 15. Иллюстрация организации очередей и обработки потоков
307
о несколько раз сразу. В следующем фрагменте кода демонстрируется
пример реализации такого подхода, когда каждая сотня кортежей записывается
в азу данных assandra по мере их накопления или каждую секунду, в зависимо¬
сти от того, какое из этих событий наступит раньше:
public static class UpdateCassandraBoltBatched extends BaseRichBolt (
public static final int HOURS_SECS = 60 * 60;
List<Tuple> _buffer = new ArrayListO ;
OutputCollector _collector;
public void prepare (Map conf, TopologyContext context,
OutputCollector collector) {
_collector = collector;
// установить здесь клиент базы данных Cassandra
}
public Map getComponentConfigurationO {
Config conf = new ConfigO ;
conf .put (Config. TOPOLOGY_TICK_TUPLE_FREQ_SECS, 1) ;
return conf;
}
Порождает тактовый кортеж
каждую секунду, чтобы обновления
происходили хотя бы один раз в секунду
public void execute (Tuple tuple) {
boolean flush = false; '
if(tuple.getSourceStreamld()
.equals (Constants. SYSTEMJTICK_STREAM_ID))
flush = true;
J Очищает буфер, если текущий
кортеж является тактовым
} else {
_buffer.add(tuple); <
if (_buffer.size () >= 100) flush = true;
}
Если кортеж является обычным,
вводит его в буфер, а если буфер
заполнен, то очищает его
if (flush) {
// сгруппировать здесь обновления базы данных Cassandra
for(Tuple t: _buffer) {
collector.ack(t);
}
_buffer.clear (); <■
}
}
Подтверждает успешную обработку
всех кортежей и очищает буфер
public void declareOutputFields (OutputFieldsDeclarer declarer) {
// пусто, т.к. затвор не порождает выходной поток
Главная особенность этого кода заключается в том, что кортежи буферизуют¬
ся а их успешная обработка не подтверждается до тех пор, пока соответствую¬
щие обновления не будут внесены группой в базу данных Cassandra. Этим гаран¬
тируется что кортежи будут воспроизведены только в случае каких-нибудь сбоев.
А ДЛЯ того чтобы обновления происходили хотя бы раз в секунду, применяется
соелство Storm называемое тактовым, кортежем. Такой кортеж конфигурирует¬
ся для затвора каждую секунду. Когда поступает один из тактовых кортежей, то
308
Часть III. Уровень ускорения
все, что в настоящий момент находится в буфере, записывается в базу данных.
В этом коде опущены те строки, которые связаны непосредственно с обращени¬
ем к базе данных, чтобы не отвлекать внимание от демонстрации особенностей
обработки потоков.
Этот код можно сделать еще более эффективным. Ниже перечислены некото¬
рые из возможных вариантов его оптимизации.
■ В ходе пакетных вычислений можно потенциально оценить требующийся
размер множеств HyperLogLog из разных доменов (чем больше индиви¬
дуальных посетителей страниц в домене, тем более крупное множество
HyperLogLog для этого требуется). Для большинства доменов требуются
очень мелкие множества HyperLogLog, и, зная это заранее, можно добить¬
ся значительной экономии.
■ Для кластера Storm можно реализовать специальный планировщик, чтобы
задачи в затворах сочетались с теми разделами базы данных Cassandra, ко¬
торые они обновляют. Благодаря этому исключается обмен данных по сети
между задачами обновления и базой данных Cassandra.
■ Как упоминалось ранее, множества HyperLogLog не должны переноситься
из базы данных Cassandra и обратно, если они внутренне могут быть реа¬
лизованы в этой базе данных.
Реализация всех эти вариантов оптимизации выходит за рамки данной книги.
Поэтому они служат лишь в качестве рекомендаций по улучшению данной кон¬
кретной разновидности уровня ускорения.
Резюме
Теперь у вас должно сложиться ясное представление обо всех составляющих
уровня ускорения, включая очереди, потоковые процессоры и представления
в реальном времени. Уровень ускорения оказывается намного более сложной ча¬
стью лямбда-архитектуры вследствие его инкрементного характера, в чем можно
убедиться, если сравнить код инкрементных вычислений из этой главы с кодом
пакетных вычислений из предыдущих глав.
На уровне ускорения осталось лишь рассмотреть микропакетную разновид¬
ность обработки потоков. Такая обработка потоков вынуждает идти на иные
компромиссы, чем при поочередной обработке потоков, принося, например,
в жертву задержку. Но это сулит и немалые выгоды, в том числе внедрение в об¬
работку потоков семантики “только однажды” для более общего ряда операций.
Микропакетная
обработка потоков
В этой главе...
■ Семантика "только однажды"
для обработки потоков.
■ Микропакетная обработка потоков
и ее компромиссы.
■ Расширение конвейерных схем
для микропакетной обработки потоков.
В четырех предыдущих главах были рассмотрены основные составляющие
уровня ускорения, в том числе представления в реальном времени, алгоритмы ин¬
крементных вычислений, обработка потоков и их сочетание вместе. В этой главе
основное внимание уделено еще одной разновидности обработки потоков, кото¬
рая также вынуждает идти на определенные компромиссы ради достижения таких
преимуществ, как повышение точности и высокой пропускной способности.
Рассмотренная ранее поочередная обработка потоков обеспечивает очень
малую задержку и проста для понимания, но она может гарантировать обработ¬
ку данных хотя бы один раз во время сбоев. И хотя это не оказывает никакого
влияния на точность выполнения некоторых операций вроде ввода элементов
во множество, но в то же время оказывает влияние на другие операции вроде
подсчитывания. Зачастую подобная неточность не особенно важна, поскольку
уровень пакетной обработки подменяет собой уровень ускорения, делая эту не¬
точность временной. Но иногда полная точность требуется постоянно, и тогда
временная неточность неприемлема. В подобных случаях микропакетная обра¬
ботка потоков может обеспечить отказоустойчивую точность, хотя и за счет уве¬
личения задержки до величины от сотен миллисекунд до нескольких секунд.
После рассмотрения принципов, положенных в основу микропакетной обра¬
ботки потоков, в этой главе будут продемонстрированы конвейерные схемы, ко¬
торые могут быть расширены для применения в целях микропакетной обработки
310 Часть III. Уровень ускорения
потоков. С помощью этих схем будет завершено построение уровня ускорения
в приложении SuperWebAnalytics.com.
16.1. Достижение семантики “только однажды”
При поочередной обработке потоков кортежи обрабатываются независимо
друг от друга. Сбои отслеживаются на уровне отдельных кортежей, и на этом же
уровне происходит их повторное воспроизведение.
Совсем иначе дело обстоит при микропакетной обработке потоков.
Небольшие пакеты (или группы) кортежей обрабатываются одновременно,
и если обработка пакета завершится неудачно, то весь пакет будет повторно вос¬
произведен. Кроме того, пакеты кортежей обрабатываются в строгом порядке.
Такой подход позволяет воспользоваться новыми методами для достижения се¬
мантики “только однажды” при обработке данных, вместо того чтобы опираться
на внутренне идемпотентные функции, как это происходит при поочередной
обработке потоков. Далее будет показано, каким образом достигается такая се¬
мантика.
16.1.1. Строго упорядоченная обработка
Допустим, требуется подсчитать все кортежи в реальном времени, причем
сделать это нужно очень точно независимо от количества сбоев, происходящих
во время обработки. Чтобы понять, как это сделать, начнем с поочередной обра¬
ботки потоков и выясним, что нам даст семантика “только однажды”.
Псевдокод для поочередной обработки потоков выглядит следующим образом:
process(tuple) {
counter.increment()
}
Этот код не обладает семантикой “только однажды”. Рассмотрим, что же про¬
исходит во время сбоев. В этом случае будут воспроизведены кортежи, а когда
дело дойдет до приращения подсчета, то нельзя будет выяснить, был ли данный
кортеж уже обработан. Возможно, приращение подсчета уже было произведено,
но затем произошел сбой непосредственно перед подтверждением успешной об¬
работки кортежа. Это можно выяснить лишь в том случае, если сохранять иден¬
тификатор каждого обрабатываемого кортежа. Но тогда придется сохранять
немалую долю состояния вместо одного числа. Следовательно, такое решение
неприемлемо.
Самое главное для достижения семантики “только однажды” — соблюсти стро¬
гий порядок обработки входного потока. Выясним, что же происходит, когда
кортежи из входного потока обрабатываются строго по очереди, т.е. обработка
следующего кортежа начинается только после того, как будет успешно обработан
предыдущий кортеж. Безусловно, такое решение не является масштабируемым,
но оно наглядно показывает принцип, положенный в основу микропакетной
обработки. Допустим также, что у каждого кортежа имеется свой однозначный
идентификатор, который остается неизменным независимо от того, сколько раз
воспроизводится кортеж.
Глава 16, Микропакетная обработка потоков
311
с ттё* пнргп^п^ с^м°е главное хранить не только подсчет, но и идентификатор по¬
ра отанного кортежа. И тогда при обновлении подсчета возникают
следующие ситуации.
Хранимый идентификатор совпадает с текущим идентификатором корте¬
жа. этом случае становится известно, что подсчет уже отражает текущий
кортеж, и поэтому ничего больше делать не нужно.
И Хранимый идентификатор отличается от текущего идентификатора кор-
тежа. В этом случае становится известно, что подсчет не отражает теку¬
щий кортеж, и поэтому нужно прирастить подсчет и обновить хранимый
идентификатор. И это вполне обоснованно, поскольку кортежи обрабаты¬
ваются по очереди, а их текущий подсчет и идентификатор обновляются
автоматически.
Такой подход к обновлению годится на все случаи сбоев. Если обработка за¬
вершится неудачно после обновления подсчета, то кортеж будет воспроизведен,
а обновление во второй раз будет пропущено. А если обработка завершится не¬
удачно перед обновлением подсчета, то обновление произойдет во второй раз.
16.1.2. Микропакетная обработка потоков
Как упоминалось ранее, поочередная обработка кортежей совершенно не эф¬
фективна. Поэтому их лучше обрабатывать отдельными пакетами (или группами),
как показано на рис. 16.1. Такая обработка потоков называется микропакетной.
Входящий поток кортежей
Пакет 5 Пакет 4 Пакет 3 Пакет 2 Пакет 1
Рис. 16.1. Поток кортежей, разделяемый на пакеты
Пакеты обрабатываются по порядку, и каждому пакету присваивается свой
идентификатор, который остается неизменным при всяком воспроизведении.
Многие кортежи обрабатываются в ходе итерации, а не по одному, и поэтому
такую обработку можно масштабировать путем распараллеливания. Прежде чем
перейти к следующему пакету, следует полностью завершить обработку предыду-
щего пакета.
Рассмотрим принцип действия микропакетной
обработки на примере глобального подсчитыва¬
ния. И в этом случае подсчет хранится не отдель¬
но, а вместе с идентификатором последнего па¬
кета, участвующего в обновлении этого подсчета.
Допустим, текущее состояние сохраняемого подсче¬
та выглядит так, как показано на рис. 16.2.
Допустим далее, что поступил пакет 4 с десятью кортежами. Текущее состоя¬
ние сохраняемого подсчета будет обновлено так, как показано на рис. 1Ь.З.
Подсчет
112
Идентификатор
пакета
3
Рис. 16.2. Текущее состояние
сохраняемого подсчета, включая
идентификатор пакета
312
Часть III. Уровень ускорения
А теперь допустим, что после обновления теку¬
щего состояния подсчета, сохраняемого в базе дан¬
ных, в потоковом процессоре произойдет какой-ни-
Рис. 16.3. Результат обновления будь сбой, и поэтому сообщение о завершении
текущего состояния подсчета обработки пакета так и не будет получено. Время
ожидания этого сообщения в потоковом процессо¬
ре истечет, после чего будет предпринята повторная попытка обработать пакет
4. Когда же наступит время обновить текущее состояние подсчета в базе данных,
то выяснится, что оно уже было обновлено с учетом пакета 4. Таким образом,
вместо приращения подсчета снова произойдет переход к следующему пакету.
Рассмотрим далее более сложный пример, чем глобальное подсчитывание, что¬
бы лучше разъяснить принцип действия микропакстной обработки потоков.
16.1.3. Топологии микропакетной обработки потоков
Допустим, требуется построить потоковое приложение, потребляющее боль¬
шой поток слов для подсчета количества трех слов, чаще всего встречающихся
в этом потоке. Эту задачу можно решить с помощью микропакетной обработки по¬
токов, обеспечив ее полное распараллеливание, отказоустойчивость и точность.
Для каждого пакета слов необходимо выполнить две задачи. Во-первых,
хранить состояние подсчета по частоте появления каждого слова. Это можно
сделать с помощью базы данных пар “ключ-значение”. И во-вторых, обновлять
список из трех чаще всего встречающихся слов, если любое из только что обра¬
ботанных слов встречается чаще, чем одно из тех трех слов, которые чаще всего
встречались до сих пор.
Начнем с обновления частот появления слов. Как пояснялось ранее, в кар¬
касе MapReduce и при поочередной обработке потоков данные разделяются,
и каждый раздел обрабатывается параллельно. То же самое происходит и при
микропакетной обработке потоков. На рис. 16.4 показано, каким образом вы¬
глядит обработка пакета слов. Как видите, в один пакет входят кортежи из всех
разделов входящего потока.
Для обновления частот появления слов необходимо разделить слова таким
образом, чтобы одно и то же слово всегда обрабатывалось в той же самой зада¬
че. Этим гарантируется, что при обновлении базы данных информация об этом
слове будет обновлена лишь в одном потоке исполнения, чтобы исключить по¬
явление условия гонок, как показано на рис. 16.5.
Далее нужно выяснить, что же хранить в качестве состояния для каждого сло¬
ва. Хранить один только подсчет каждого слова в данном случае явно недоста¬
точно, как и при глобальном подсчитывании. Если попытка обработать пакет по¬
вторяется, то неизвестно, отражает ли текущий подсчет текущий пакет. Поэтому,
как и при глобальном подсчитывании, решение состоит в том, чтобы сохранять
текущий подсчет каждого слова вместе с идентификатором пакета, как показано
на рис. 16.6.
Подсчет
112
Идентификатор
пакета
4
Глава 16. Микропакетная обработка потоков
313
Раздел 1
Раздел 3
Рис. 16.4. В каждый пакет входят кортежи из всех разделов входящего потока
Рис. 16.5. Топология подсчета слов
Яблоко
Подсчет
15
Идентификатор
пакета—
3
Груша
Подсчет
15
Идентификатор
пакета
18
Банан
Подсчет
15
Идентификатор
пакета
3
База данных
Рис. 16.6. Сохранение подсчетов слов вместе с идентификаторами пакетов
314
Часть III. Уровень ускорения
А теперь рассмотрим случай сбоя. Допустим, что во время обработки пакета
в кластере выходит из строя машина и до этого в базе данных удалось обновить
лишь некоторые разделы. При этом подсчеты одних слов будут отражать теку¬
щий пакет, а подсчеты других слов еще не обновлены. Когда пакет воспроиз¬
водится, слова, имеющие состояние, включая идентификатор текущего пакета,
не обновляются, поскольку они имеют такой же идентификатор пакета, как и у
текущего пакета, тогда как слова, которые не были еще обновлены, будут обнов¬
лены как обычно. Аналогично глобальному подсчитыванию, обработка выполня¬
ется совершенно точно и отказоустойчиво.
Перейдем далее ко второй части вычислений, которая состоит в подсче¬
те трех чаще всего встречающихся слов. Одно из решений этой задачи состо¬
ит в том, чтобы направлять новые подсчеты каждого слова одной задаче, где
они должны объединяться в список из трех чаще всего встречающихся слов.
Недостаток такого решения заключается в том, что он не предусматривает мас¬
штабирование. Количество кортежей, направляемых этой единственной задаче
определения трех чаще всего встречающихся слов, может быть практически та¬
ким же, как и количество слов во всем входном потоке.
Правда, имеется лучшее решение, лишенное упомянутого выше недостатка.
Вместо того чтобы посылать обновленный подсчет каждого слова задаче опре¬
деления трех чаще всего встречающихся слов, каждая задача подсчета слов мо¬
жет сначала определить три слова, чаще всего встречающихся в текущем пакете,
а затем послать список этих слов задаче, ответственной за ведение глобального
списка из трех чаще всего встречающихся слов. И далее эта задача может объе¬
динить все полученные списки в свой глобальный список. В этом случае объем
данных, передаваемых задаче, ответственной за ведение глобального списка, из
задач обработки каждого пакета, будет пропорционален коэффициенту распа¬
раллеливания задач подсчета слов, а не объему всего входного потока.
Выясним далее, каким образом сбои могут оказывать влияние на ту часть вы¬
числений, которые отвечают за определение трех чаще всего встречающихся
слов. Допустим, что в результате сбоя один из промежуточных списков из трех
чаще всего встречающихся слов не посылается задаче, ответственной за ведение
глобального списка этих слов. В таком случае глобальный список не будет обнов¬
лен, а его обновление произойдет как обычно при воспроизведении пакета.
Допустим, что сбой произойдет после обновления промежуточного списка из
трех чаще всего встречающихся слов, т.е. сообщение, уведомляющее об успеш¬
ной обработке пакет, так и не было получено. В таком случае пакет будет вос¬
произведен снова, и те же самые списки из трех чаще всего встречающихся слов
будут переданы задаче, ответственной за ведение глобального списка этих слов.
На этот раз в глобальном списке текущий пакет уже учтен. Но поскольку объеди¬
нение этих списков является идемпотентной операцией, то повторное их объ¬
единение в уже обновленный глобальный список нс изменит результата. Таким
образом, специальный прием, применявшийся до включения идентификатора
пакета в состояние текущего подсчета, в данном случае не требуется, чтобы до¬
стичь полной точности обработки.
Глава 16. Микропакетная обработка потоков
315
16.2. Основные понятия мннропакетной
обработки потоков
В примерах, приведенных в предыдущем разделе, выявляются некоторые ос¬
новные понятия микропакетной обработки потоков. В частности, этой разно¬
видности обработки потоков присущи следующие особенности.
■ Локально-пакетные вычисления. Происходят только в пределах пакета
независимо от сохраняемого состояния. К их числу относится перераспре¬
деление потока слов по полю word и подсчет всех кортежей в пакете.
■ Вычисления с сохранением состояния. При таких вычислениях состоя¬
ние сохраняется во всех пакетах, включая обновление глобального подсче¬
та, обновление отдельных подсчетов слов или сохранение списка из трех
чаще всего встречающихся слов. Но именно здесь следует быть особенно
внимательным, обновляя состояние таким образом, чтобы обработка про¬
исходила идемпотентно при сбоях и повторных попытках. В данном слу¬
чае особенно уместным оказывается сохранение идентификатора пакета
вместе с состоянием, поскольку это привносит идемпотентность в неидем-
потентные операции.
И наконец, микропакетная обработка потоков опирается на источник пото¬
ков, где пакет можно воспроизвести точно так же, как и прежде. Как оказыва¬
ется, у очередей вроде Kafka, рассматривавшихся в предыдущей главе, для этой
цели имеется идеальная семантика. Такие очереди обеспечивают доступ к API,
очень похожему на API файловой системы, и поэтому, когда порождается па¬
кет, клиент может запомнить смещение, с которого данные были считаны из
разделов для конкретного пакета. Если же пакет требуется воспроизвести, то
с этой целью может быть порожден точно такой же пакет. Такие виды источ¬
ников потоков называются транзакционными стоками.
Характеристики задержки и пропускной способности оказывается у микро¬
пакетной обработки потоков иными, чем у поочередной обработки. Задержка
от момента постановки любого отдельного кортежа в исходящую очередь и до
момента завершения его обработки оказывается больше при микропакетной
обработке. И хотя согласование пакетов требует небольших издержек, тем не
менее, оно заметно увеличивает задержку. Вместо того чтобы ожидать завер¬
шения обработки только одного кортежа, приходится обрабатывать намного
больше кортежей. На практике это приводит к задержке от сотен миллисекунд
до нескольких секунд.
Но пропускная способность при микропакетной обработке потоков может
быть выше, чем при поочередной обработке. Если при поочередной обработке
потоков приходится отслеживать сбои на уровне отдельных кортежей, то при
микропакетной обработке - на уровне пакетов. Это означает, что в среднем
на каждый кортеж расходуется меньше ресурсов, а следовательно, пропускная
способность при микропакетной обработке потоков оказывается выше, чем
при поочередной обработке.
$16
Часть III. Уровень ускорения
Другие виды стоков
Достичь семантики “только однажды" можно и без транзакционных стоков. В транзак¬
ционном стоке при каждом воспроизведении должен быть порожден точно такой же
пакет. Но недостаток транзакционных стоков заключается в том, что обработку нельзя
продолжить, поскольку пакет нельзя точно воспроизвести, если он оказывается сбой¬
ным и его раздел становится недоступным.
Менее ограничительным оказывается так называемый непрозрачный сток. Такой вид
стока должен просто обеспечить успешную обработку каждого кортежа только в одном
пакете. Этим допускается следующая последовательность событий.
1. Порождается пакет А с кортежами из разделов 1, 2 и 3.
2. Пакет А не удается обработать.
3. Раздел 3 становится недоступным.
4. Пакет А воспроизводится только с кортежами из разделов 1 и 2.
5. В дальнейшем раздел 3 может быть иногда доступен.
6. Те кортежи, которые не удалось обработать в пакете при предыдущей попытке,
успешно обрабатываются при следующей попытке.
Чтобы достичь семантики “только однажды" с помощью непрозрачных стоков, требуется
более сложная логика при обновлении состояния. И для этого недостаточно сохранять
только идентификатор пакета вместе с обновляемым состоянием. Нужно еще отслежи¬
вать небольшой объем дополнительной информации. Подробнее о применении непро¬
зрачных стоков можно узнать из документации на систему Apache Storm.
16.3. Расширение конвейерных схем
для мнкропакетной обработки потоков
Как было показано ранее, конвейерные схемы предоставляют отличную воз¬
можность кратко выразить порядок вычислений при пакетной обработке, не
геряя обобщенный характер их представления. Оказывается, что с помощью
4ескольких расширений конвейерные схемы можно приспособить к представле-
-шю вычислений и при микропакетной обработке потоков.
Представленные ранее конвейерные схемы применялись для представления
юрядка обработки лишь одного пакета. Но конвейерные схемы нетрудно при-
:пособить к микропакетной обработке потоков, организовав их выполнение
то каждому пакету в отдельности. Таким образом, операции вроде агрегирования
4 соединения происходят только в пределах пакета, а вычисления не пересека-
от границы пакетов.
Разумеется, проку от обработки каждого пакета совершенно независимо
>т другого немного. В этом случае приходится не только выполнять локально-па-
сетные вычисления, но и сохранять состояние между пакетами. И для этой цели
требуются расширения конвейерных схем с логикой обработки идентификато-
юв пакетов, позволяющей достичь семантики “только однажды”.
Представить подобные расширения проще всего с помощью конкретного при¬
зера. С этой целью вернемся снова к примеру реализации подсчета слов во вход-
юм потоке, но на этот раз прибегнем к микропакетной обработке данного потока.
Гмим 16. Мчкропакетная обработка потоков
317
Ее следует интеопР“еГа Конве^еРная схема, реализующая такой подсчет слов,
работке В этой ехем*> ^°ВаТЬ таким же образом, как и при обычной пакетной об-
Сн^еннГ1лГ "ОЛНОС™° обрабатывается пакет кортежей, содержащих
ходит переход к пбг^ 6ПСе' завершении обработки данного пакета проис¬
ходит переход к обработке следующего пакета кортежей.
Рис. 16.7. Конвейерная схема подсчета слов при микропакетной обработке
Как видите, эта конвейерная схема очень похожа на конвейерную схему под¬
счета слов при пакетной обработке, за исключением того, что вместо агрегато¬
ра в ней употребляется обновитель состояния, связанный с состоянием отобра¬
жения. Состояние отображения представляет информационное хранилище пар
“ключ-значение”, которое может быть внешней базой данных вроде Cassandra
или состоянием, сохраняемым в оперативной памяти при выполнении задач
обработки. Функции обновителя состояния отвечают за обновление состояния,
которое происходит идемпотентно при воспроизведении. В данном случае агре¬
гатор Count применяется в функции обновителя состояния для обновления под¬
счета каждого слова в состоянии отображения.
Такой подход примечателен тем, что все подробности хранения и проверки
идентификаторов пакетов для обеспечения идемпотентности могут быть авто¬
матизированы ниже абстракций. Следовательно, при составлении конвейерных
схем обо всем этом можно вообще не думать. Достаточно допустить, что каждый
кортеж обрабатывается лишь один раз, а конвейерные схемы будут подспудно
выполняться таким образом, чтобы обеспечить отказоустойчивость и идемпо¬
тентность при воспроизведении.
В конвейерной схеме можно сохранить любое состояние, а не только состоя¬
ния отображения. Ведь состояние представляет любую индексированную инфор¬
мационную модель, которая удовлетворяет требованиям индексации в реальном
времени. Например, состояние ключей к отсортированному отображению мож¬
но хранить в базе данных вроде Apache Cassandra, поддерживающей такую ин¬
формационную модель.
318
Часть III. Уровень ускорения
Как было показано ранее, в контексте пакетной обработки конвейерные схемы
полностью заменяют каркас MapReduce. Они способны делать все, на что спосо¬
бен каркас MapReduce, не только с той же самой пропускной способностью, но
и с намного более изящной выразительностью. А в контексте обработки потоков
конвейерные схемы — единственный способ представить микропакетные вычис¬
ления и не подменяют собой поочередную обработку потоков. Они подходят дале¬
ко не во всех ситуация, поскольку при микропакетной и поочередной обработке
потоков приходится идти на разные компромиссы между задержкой, пропускной
способностью и гарантированной семантикой обработки сообщений.
16.4. Завершение построения уровня ускорения
в приложении SuperWebAnalytics.com
А теперь завершим построение уровня ускорения в приложении
SuperWebAnalytics.com, воспользовавшись микропакетной обработкой. Начнем
с обработки запросов на подсчет количества просмотров страниц во времени.
16.4.1. Просмотры страниц во времени
Напомним, что цель запроса на подсчет количества просмотров страниц во
времени — получить общее количество просмотров страницы по заданному URL
за любой ряд часов. При обработке такого запроса на уровне обслуживания мы
подсчитывали количество просмотров страницы ежечасно, ежедневно, ежене¬
дельно и ежемесячно, чтобы повысить эффективность вычислений за большие
промежутки времени. Но такая оптимизация вычислений на уровне ускорения
не требуется, поскольку на этом уровне обрабатываются только последние дан¬
ные. Ведь с момента последнего обновления должно было пройти не больше
нескольких часов, и поэтому вести совокупный подсчет количества просмотров
страниц за остальные промежутки времени не имеет никакого смысла. Если цикл
обработки пакетов длительный (например, охватывает больше дня), то целесо¬
образно вести совокупный подсчет количества просмотров страниц ежедневно.
Но ради простоты реализуем на уровне ускорения обработку запросов только
на ежечасный подсчет количества просмотров страниц.
Как и при реализации на уровне ускорения обработки запросов на количество
индивидуальных посетителей страниц в предыдущих главах, в данном случае впол¬
не подойдет представление в реальном времени, реализующее интерфейс ключей
к отсортированному отображению, где в качестве ключа служит URL, в качестве
ключа к отсортированному отображению — часовой промежуток времени, а в ка¬
честве значения в отсортированном отображении — количество просмотров стра¬
ницы по заданному URL за указанный часовой промежуток времени. Конвейерная
схема, реализующая соответствующую логику, приведена на рис. 16.8.
На этой конвейерной схеме применяется состояние ключей к отсортиро¬
ванному отображению, которое обновляется по мерс поступления просмотров
страниц. Обновитель состояния типа UpdatelnnerMap представляет собой пара-
метризированный агрегатор Count, а следовательно, ему известно, как обновлять
внутренние отображения этого состояния, представляющие часовые промежут¬
ки времени для подсчетов количества просмотров страниц.
Глава 16. Микропакетная обработка потоков
Рис. 16.8. Конвейерная схема микропакетной обработки
запросов на подсчет количества просмотров страниц
16,4.2. Анализ показателя отказов от просмотра страниц
А теперь перейдем к реализации на уровне ускорения запросов на вычисле¬
ние показателя отказов от просмотра страниц. Напомним, что посещение стра¬
ницы считается с отказом от просмотра, если посетитель этой страницы не пе¬
решел на другую страницу в течение 30 минут после своего первого, посещения.
Следовательно, отказ от просмотра происходит по событию, которое не насту¬
пает.
Как и при подсчете количества индивидуальных посетителей, в данном слу¬
чае приходится принимать во внимание эквиваленты, чтобы совершенно точно
вычислить отказы от просмотра страниц. Так, если пользователь просматривает
веб-сайт сначала на своем компьютере, а через десять минут просматривает его
со своего мобильного телефона, то такое посещение должно считаться единствен¬
ным без отказа от просмотра. А если заранее неизвестно, что идентификаторы
пользователя обоих устройств обозначают одно и то же лицо, то в таком случае
следует считать два отдельных посещения с отказом от просмотра страниц.
Как и при подсчете количества индивидуальных посетителей, принятие экви-
валентов в массовом порядке усложняет вычисления. Чтобы выяснить причины,
достаточно рассмотреть, что нужно сделать при получении нового эквивалента.
В качестве иллюстрации ниже приведен следующий ряд сооыгий.
1. Просмотр страницы поступает от пользователя компьютера Алисы на ну¬
левой минуте.
120 Часть III. Уровень ускорения
2. Просмотр страницы поступает от пользователя мобильного телефона
Алисы на десятой минуте.
3. Эквивалент поступает на 140-й минуте между обращениями пользователя
компьютера и мобильного телефона Алисы.
До наступления третьего события оба просмотра страниц должны были счи¬
таться как два посещения с двумя отказами от просмотра. При этом не было
известно, были ли они эквивалентны, а следовательно, не нужно было больше
ничего вычислять в реальном времени. Но с наступлением третьего события
все изменилось. Теперь придется вернуться к исправлению показателей отказов
от просмотра, вычисленных два часа назад.
Единственный способ сделать это — извлечь все прошлые посещения из ком¬
пьютера и мобильного телефона пользователя Алисы и вычислить заново соот¬
ветствующие показатели отказов от просмотра страниц. Сравнив их с тем, что
было вычислено в первый раз, необходимо инкрементировать или декременти¬
ровать соответственно подсчеты отказов от просмотра и посещений страниц,
хранящиеся в базе данных. Помимо всего прочего, нужно произвести анализ
графа эквивалентов в реальном времени.
Принятие во внимание эквивалентов не только усложняет дело, но и замет¬
но увеличивает затраты на хранение данных на уровне ускорения. Вместо того
чтобы хранить только подсчеты отказов от просмотра и посещений страниц
в каждом домене, приходится также извлекать все просмотры страниц, совер¬
шенные пользователем с момента последнего обновления уровня обслуживания
. А поскольку эквивалент может поступить в любой момент для любого пользо¬
вателя, то все просмотры страниц придется проиндексировать на уровне ускоре¬
ния. И хотя это вполне осуществимо, но обойдется намного дороже.
Как и при подсчете количества индивидуальных посетителей, упростить дело
можно, просто проигнорировав эквиваленты на уровне ускорения. Это вряд ли
внесет слишком большую неточность в вычисления, а благодаря тому что любые
неточности автоматически исправляются на уровнях пакетной обработки и обслу¬
живания, такой компромисс кажется вполне допустимым. Именно так мы и посту¬
пим. И, как всегда, следует оценить степень неточности, вносимой любым приня¬
тым подходом, измерив ее аналитическими методами в автономном режиме.
Анализ отказов от просмотра страниц в реальном времени выполняется в сле¬
дующие три этапа.
1. Обработать поток событий просмотра страниц, содержащий идентифика¬
тор пользователя, 1Л1Ь и отметку времени.
2. Выявить отказы от просмотра, обнаружив момент, когда пользователь посе¬
тил одну страницу в домене, не посетил другие страницы в течение 30 по¬
следующих минут.
3. Обновить базу данных, содержащую отображение домена на показатель
отказов от просмотра. Сохранить показатель отказов от просмотра в виде
пары значений [количество отказов, количество посещений].
Как и при подсчете количества просмотров страниц, анализ показателя от¬
казов от просмотра начинается с потребления потока просмотров страниц. Но
Глава 16. Микропакетная обработка потоков
321
намного интереснее выявить отказы от просмотра страниц, поскольку это со-
ытие зависит от времени, а не является результатом простого агрегирования,
лю ои заданный момент времени отказ от просмотра страницы может быть
определен исходя из просмотров страниц за последние 30 минут. А поскольку
при о ра огке потоков доступными оказываются только произошедшие собы¬
тия, то для выявления отказов от просмотра нужно хранить состояние о том,
что произошло за последние 30 минут.
Самое главное — отслеживать каждое посещение (пару [домен, пользова¬
тель]) до тех пор, пока оно не завершится. Ранее посещение было определено
как завершенное, если по истечении 30 минут пользователь больше не просма¬
тривал никаких страниц в данном домене. По окончании такого посещения ин¬
формация о показателе отказов от просмотра страниц в данном домене может
быть обновлена. Для каждого завершенного посещения количество посещений
увеличивается на единицу, а количество отказов от просмотра инкрементируется
лишь в том случае, если при данном посещении был совершен только один про¬
смотр страницы. Для отслеживания таких посещений на уровне конвейерной
схемы поддерживается отображение пары [домен, пользователь] на первый
и последний просмотры страницы в течение данного посещения. С целью опре¬
делить завершенные посещения все отображение перебирается каждую минуту.
Завершенные посещения будут удалены из отображения, а затем использованы
для обновления информации о показателе отказов от просмотра в соответству¬
ющих доменах.
Для этого требуется отслеживать все посещения за последние 30 минут. В круп¬
ном масштабе это может составить сразу порядка сотен миллионов и даже милли¬
ардов посещений. Если выделить для отслеживания посещения около 100 байт,
то с учетом домена, идентификатора пользователя, отметки времени и места, вы¬
деляемого для отображения, объем требующейся памяти может составить поряд¬
ка терабайта. И хотя это вполне осуществимо, тем не менее, обойдется дорого.
Поэтому, составив требования к памяти, необходимо найти способ их упрощения
или полного исключения.
Обработка оконных потоков
Вам, возможно, встречался раньше термин оконный поток, который обозначает раз¬
биение входящего потока на временные окна по 30 секунд, 1, 5 или 30 минут. В одних
случаях такое окно “плавает", но всегда обозначает последние х секунд времени, а в
других окна фиксированы и следуют друг за другом.
На первый взгляд обработка оконных потоков вполне подходит для анализа показателя
отказов от просмотра в силу ее временного характера. Но при более пристальном рас¬
смотрении обработка оконных потоков совершенно не годится для подобного анализа.
Ведь любое отдельное посещение может занимать неопределенный период времени.
Так если некто посещает страницу в домене каждые 10 минут в течение 16 часов, та¬
кое посещение должно оставаться в памяти все это время (до тех пор, пока не истекут
зо минп бездействия). При обработке оконных потоков вычисления происходят иначе
и призваны ответить на вопрос вроде следующего: “Сколько просмотров страниц было
получено за последние 15 минут?
322
Часть III. Уровень ускорения
Конвейерная схема для анализа отказов от просмотра страниц приведена
на рис. 16.9. Главным элементом этой конвейерной схемы является обновитель
состояния типа Апа1угеУ1з^з, где по завершении посещения определяется,
были ли они с отказами. А в отображении типа MapState, находящемся в опера¬
тивной памяти, сохраняется состояние после посещений.
Рис. 16.9. Конвейерная схема для анализа отказов от просмотра
страниц при микропакетной обработке
Псевдокод этого обновителя состояния выглядит следующим образом:
Сохраняет пару
отметок времени
[время начала
посещения,
время окончания
посещения]
для каждого
посещения.
Информация о
посещении сохраняется
в отображении
по ключу в виде пары
[домен,
пользователь]
function AnalyzeVisits(mapstate, domain, user, timestamp) {
THIRTY-MINUTES-SECS = 60 * 30 Получает данные о проверке посещений
в последний раз, чтобы выяснить, завершилось
ли какое-нибудь из них (спустя 30 минут
update (mapstate, [domain, user], с момента последнего посещения),
function (visit-info) { Посещения будут
if (visit-info == null) [timestamp, timestamp] просматриваться
else [visit-info [0], timestamp] каждую минуту
^ })
last-sweep-time = get(state, "last-sweep”, 0)
Глава 16. Микропакетнал обработка потоков
323
По завершении
посещения
порождается факт
и прекращается
отслеживание
посещения
в отображении
Просматривает посещения по истечении больше одной
минуты с момента последней проверки. Этот код зависит от
устойчивости входящего потока просмотров страниц, т.к. он
действует только после проверки отметки времени нового
if (timestamp > last-sweep-time + 60)
for (entry in mapstate) {
domain = entry, key [0]
visit-info = entry, value
{
nywmvipa vii/рппцкя
if(timestamp > visit-info[1] + THIRTY-MINUTES-SECS)
emit(domain, visit-info[0] = visit-info[1]) <
remove(mapstate, entry.key)
}
put(mapstate, "last-sweep", timestamp)
Посещение считается с отказом
от просмотра, если моменты его
начала и окончания совпадают
}
Обновитель состояния типа AnalyzeVisits порождает поток информации о по¬
сещении в виде двухэлементного кортежа, содержащего домен и логическое значе¬
ние, обозначающее, было ли посещение с отказом от просмотра. В следующей далее
части конвейерной схемы подсчитывается общее количество посещений и общее
количество отказов от просмотра страниц, и оба результата сохраняются в состоя¬
нии отображения типа MapState. Сначала логическое значение преобразуется в чис¬
ловое значение 0 или 1, в зависимости от того, было ли оно равно false или true
соответственно. Затем подсчитывается количество кортежей, чтобы определить
общее количество посещений, а значения 0 и 1 суммируются, чтобы определить
общее количество отказов от просмотра страниц. Оба эти результата сохраняются
в отображении типа MapState в виде значения по ключу в качестве домена.
Время и неупорядоченность кортежей
В рассмотренной выше конвейерной схеме предполагается, что отметки времени в кор¬
тежах всегда нарастают. Но, что если кортежи поступают не по порядку?
И это отнюдь не гипотетический вопрос. Ведь кортежи формируются во всем кластере
и затем совместно размещаются на серверах очередей. Вполне вероятно, что кортежи
окажутся расположенными не по порядку. Кроме того, при разделении сети, ошибках
или задержках, возникающих при постановке в очередь, кортежи могут располагаться
не по порядку намного дольше — вплоть до нескольких минут.
Для выполнения многих видов вычислений (например, подсчета количества просмотров
страниц во времени) это не имеет особого значения. Но для анализа показателя отказов
от просмотра страниц, при котором время используется для инициирования проверки по¬
сещений на завершенность, это очень важно. Например, просмотр страницы может посту¬
пить после проверки посещений на завершенность, превратив завершенное посещение
во все еще активное.
Чтобы как-то справиться с неупорядоченностью кортежей, можно, например, ввести
в вычисления задержку. Так, в коде, выполняющем анализ показателя отказов от про¬
смотра страниц, можно изменить критерий завершенности посещения на следующий:
с момента последнего посещения должно пройти больше 45 минут, а в течение 30 минут
с момента последнего просмотра страницы никаких дополнительных просмотров стра¬
ниц не должно произойти. Благодаря этому обрабатываются кортежи, поступающие не
по порядку с запаздыванием на 15 минут.
324
Часть ///. Уровень ускорения
Разумеется, идеального способа обращения с неупорядоченностью кортежей не суще¬
ствует. Теоретически можно получить кортеж, сформированный два дня назад, но вряд
ли благоразумно ожидать до бесконечности поступления не по порядку любых кортежей
в коде, выполняющем анализ показателя отказов от просмотра страниц, иначе этот код
не сможет продвинуться ни на шаг вперед. Именно поэтому следует установить предел
на время ожидания поступающих не по порядку кортежей. Как правило, целесообразно
провести измерения, чтобы определить распределение и частоту поступления кортежей
не по порядку.
Как и многое другое на уровне ускорения, рассматриваемый здесь вопрос служит еще
одним примером трудностей, возникающих при обработке данных в реальном времени,
но не при пакетной их обработке. В лямбда-архитектуре имеется уровень пакетной об¬
работки, подменяющий собой уровень ускорения, и поэтому любые неточности, вноси¬
мые неупорядоченностью кортежей, со временем искореняются.
У рассмотренного выше проектного решения имеется следующий недоста¬
ток: все состояние хранится в оперативной памяти на время выполнения задач
по конвейерной схеме. Как упоминалось ранее, для вычислений в довольно круп¬
ных масштабах потребуется объем памяти порядка терабайта. И хотя вполне воз¬
можно организовать кластер с таким объемом оперативной памяти, оказывает¬
ся, что имеется другой подход, вообще не требующий памяти.
16.5. Другой подход к анализу показателя
отказов от просмотра страниц
Выясним, как вообще исключить требование к оперативной памяти для ана¬
лиза показателя отказов от просмотра страниц. Для этого нужно вернуться к рас¬
смотрению данной задачи снова. Посещение считается незавершенным до тех
пор, пока не пройдет 30 минут бездеятельности с момента этого посещения.
Необходимость ждать 30 минут, чтобы определить состояние посещения, означа¬
ет, что анализ показателя отказов от просмотра страниц, по существу, не создает
никаких затруднений в реальном времени. А поскольку задержка не накладывает
никаких особых ограничений, то и пользоваться системой обработки потоков,
ориентированной на оперативную память, совсем не обязательно.
В данном случае для уровня ускорения вполне подойдет и система пакетной об¬
работки. Логика выполнения последовательности операций не меняется: по-преж¬
нему ведется отображение посещений на информацию о посещениях, посещения
отмечаются с отказом от просмотра или без него по истечении 30 минут бездея¬
тельности, а затем информация о посещениях и отказах от просмотра накапли¬
вается в базе данных пар “ключ-значение”. Отличие состоит лишь в изменении
базовых технологий. Так, для организации вычислений можно воспользоваться
системой Нас1оор; а для сохранения представления на уровне ускорения — базой
данных уровня обслуживания наподобие ЕкрИатВВ. И наконец, для сохранения
промежуточного состояния в виде пары “ключ-значение” вполне подойдет база
данных типа ИерИаШОВ. Такой подход наглядно показан на рис. 16.10.
Глава 16. Микропакет,іоя обработка потоков
325
Рис. 16.10. Анализ показателя отказов от просмотра страниц
при инкрементной пакетной обработке
До сих пор мы обсуждали только возможность выполнения пакетной обработ¬
ки для повторных вычислений там, где все данные используются сразу для полу¬
чения нужного представления заново. Совсем иначе действует инкрементная па¬
кетная обработка, при которой используются новые данные, а новые представ¬
ления получаются исходя из последнего варианта имеющихся представлений.
Это не похоже на обработку потоков, при которой представления изменяются
на месте. Представления/ получаемые в процессе инкрементной пакетной обра¬
ботки, оказываются совершенно новыми и вообще не изменяются после созда¬
ния. Подробнее об инкрементной пакетной обработке речь пойдет в последней
главе данной книги.
Резюме
В этой главе было показано, как, принеся в жертву задержку, можно обойтись
без семантики ‘‘хотя бы однажды” при поочередной обработке потоков и в то
же время достичь семантики “только однажды” при микропакетной обработке
потоков. Если семантика “только однажды” присуща лишь идемпотентным опе¬
рациям при поочередной обработке потоков, то при микропакетной обработке
потоков ее можно достичь практически для любых видов вычислений.
Из материала этой главы должно быть также очевидно, что уровень ускоре¬
ния совсем не обязательно должен функционировать в реальном времени и по¬
влечь за собой обработку потоков. На уровне ускорения во внимание должны
приниматься последние данные. Как было показано на примере анализа пока¬
зателя отказов от просмотра страниц, суть проблемы кроется совсем не в ре¬
альном времени. Учет последних данных позволяет применить другие подходы
вроде инкрементной пакетной обработки. Итак, рассмотрев основные понятия
микропакетной обработки потоков, перейдем к ее применению на практике.
Иллюстрация
микропакетной
обработки потоков
В этой главе...
и Интерфейс API Trident для микропакетной
обработки потоков в системе Apache Storm.
■ Интеграция инструментальных средств Kafka,
Trident и Cassandra.
■ Локальное состояние отказоустойчивой задачи.
В предыдущей главе были представлены основные понятия микропакетной об¬
работки потоков. Обрабатывая кортежи последовательно и небольшими пакетами,
можно достичь семантики “только однажды”. Придерживаясь строгого порядка при
обработке пакетов и сохраняя информацию об идентификаторах пакетов в состоя¬
нии, можно выяснить, был ли пакет обработан прежде. Это дает возможность избе¬
жать многократных обновлений и тем самым достичь семантики “только однажды”.
Кроме того, в предыдущей главе было показано, как воспользоваться незна¬
чительными расширениями конвейерных схем для представления вычислений
при микропакетной обработке потоков. Такие конвейерные схемы дают воз¬
можность рассматривать вычисления по принципу разработки каждого корте¬
жа только один раз, и в то же время они компилируются в код, автоматически
обрабатывающий сбои, выполняющий повторные попытки и реализующий всю
логику идентификаторов пакетов.
В этой главе речь пойдет об API Trident, предоставляющем реализацию рас¬
ширенных конвейерных схем для микропакетной обработки потоков в системе
Apache Storm. В ней будет показано, насколько это похоже на обычную пакетную
обработку. И наконец, будут рассмотрены вопросы интеграции Trident с источ¬
никами потоков вроде Kafka и провайдерами состояния типа Cassandra.
328
Часть ///. Уровень ускорения
17.1. Применение Trident
Trident является программным интер¬
фейсом Java API, преобразующим топо¬
логии микропакетной обработки в стоки
и затворы по модели Storm. Trident дей¬
ствует по принципам, очень похожим
на пакетную обработку, выполняя уже из¬
вестные вам операции соединения, агре¬
гирования и группировки, а также опе¬
рируя функциями и фильтрами. Помимо
этого, он вводит абстракции для обработ¬
ки пакетов с сохранением состояния, ис¬
пользуя любую базу данных или постоян¬
ное хранилище.
Напомним, что конвейерная схе¬
ма для потокового подсчета слов вы¬
глядит так, как показано на рис. 17.1.
Рассмотрим ее реализацию с помощью
Trident.
В целях иллюстрации в рассматриваемом здесь примере бесконечный поток
предложений читается из следующего источника:
FixedBatchSpout spout = new FixedBatchSpout (
new Fields("sentence"),
3, // количество кортежей в каждом пакете
new Values ("the cow jumped over the moon"),
new Values ("the man went to the store"),
new Values ("four score and seven years ago"),
new Values ("how many apples can you eat"),
new Values ("to be or not to be the person"));
spout .setCycle (true); // эти кортежи повторяются бесконечно
В приведенном выше стоке порождаются три предложения в каждом пакете,
и этот цикл повторяется до бесконечности. Ниже приведено определение топо¬
логии Trident, реализующей подсчет слов.
TridentTopology topology = new TridentTopology ();
topology.newstream("spout1", spout)
.each(new Fields("sentence"),
new Split(),
new Fields("word"))
.groupBy(new Fields("word"))
.persistentAggregate(
new MemoryMapState.Factory(),
new Count(),
new Fields("count"));
Проанализируем приведенный выше код построчно. Прежде всего, в нем
создается объект типа TridentTopology, предоставляющий доступ к интерфейсу
для организации вычислений средствами Trident. У объекта типа TridentTopology
имеется метод newStreamO, связывающий топологию с источником входящего
Рис. 17.1. Конвейерная схема подсчета слов
при микропакетной обработке
потоков
329
Глава 17. Иллюстрация микропакетной обработки
источншсом является определенный выше сток
га ипгчтт^ к гь С pout‘ ^сли требуется прочитать данные из другого источни-
w ^ а, то с этой целью следует воспользоваться стоком Trident Kafka.
е ьшая доля состояния каждого источника входящего потока (т.е. метадан¬
ные о том, что было употреблено) отслеживается в Trident с помощью меха¬
низма оо еерег, а строковый параметр spout 1 обозначает узел Zookeeper, где
п ent должен хранить метаданные состояния. Эти метаданные содержат ин¬
формацию о содержимом каждого пакета, чтобы порождать точно такой же па¬
кет, как и прежде, всякий раз, когда его требуется воспроизвести.
Сток порождает поток, содержащий одно поле sentence. В следующей строке
рассматриваемого здесь кода определения топологии функция Split () применя¬
ется к каждому кортежу в потоке, принимая иоле sentence в качестве параметра
и разделяя его содержимое на отдельные слова. Из каждого кортежа предложе¬
ния потенциально может быть образовано много кортежей слов. Например, из
кортежа предложения “the cow jumped over the moon” (корова перепрыгнута
луну) создается шесть кортежей слов. Ниже приведено определение функции
Splitt).
public static class Split extends BaseFunction {
public void execute (TridentTuple tuple,
TridentCollector collector) {
String sentence = tuple.getString(0);
for(String word: sentence.split(" ")) {
collector .emit (new Values (word));
}
}
В отличие от затворов Storm, принимающих целые кортежи в качестве вход¬
ных данных и производящих целые кортежи в качестве выходных данных, опе¬
рации в Trident принимают частичные кортежи в качестве входных данных
и присоединяют выходные значения к входным кортежам, поскольку именно
эти операции должны выполняться по конвейерной схеме. Подспудно в Trident
компилируется столько операций, сколько можно объединить в единый затвор.
Как было показано ранее, конвейерные схемы были расширены операцией
обновителя состояния, которая связывается с объектом типа State для посто¬
янного хранения состояния между пакетами. Для этой цели в Trident имеется
интерфейс StateUpdater, который определяется следующим образом:
public interface StateUpdater<S extends State> extends Operation {
void updateState (S state, List<TridentTuple> tuples,
TridentCollector collector) ;
Этот интерфейс принимает пакет кортежей и должен выполнить соответству¬
ющую операцию для обновления текущего состояния. Для ввода операции об-
новителя состояния в топологию в Trident предусмотрены два метода. Первый
ИТ них называется partitionPersist () и принимает реализацию интерфейса
StateUpdater Второй называется persistentAggregate О и принимает агрегатор.
Агрегаторы лишены состояния, и поэтому метод persistentAggregate () авто¬
матически преобразует агрегатор в обновитель состояния. Например, агрегатор
330 Часть UL Уровень ускорения
Count будет преобразован для добавления подсчета текущего пакета к подсчету,
хранящемуся в состоянии, что зачастую очень удобно.
И в завершение примера подсчета слов в остальной части рассматриваемой
здесь топологии ведется подсчет слов, а его результаты постоянно сохраняются.
Сначала поток группируется по полю word, а затем каждая группа постоянно агре¬
гируется с помощью агрегатора Count и метода persistentAggregate (). В данном
примере подсчеты слов хранятся в оперативной памяти, но вместо нее нетрудно
воспользоваться любым постоянным хранилищем вроде Memcached или Cassandra.
Покажем, как реализовать все эти операции, чтобы хранить подсчеты слов
в базе данных Cassandra. Ниже приведен соответствующий код.
CassandraState.Options opts = new CassandraState.Options();
opts.globalCol = "COUNT";
opts.keySerializer = StringSerializer.get();
opts.colSerializer = StringSerializer.get();
stream.groupBy(new Fields("word"))
.persistentAggregate(CassandraState.transactional(
"127.0.0.1",
"mykeyspace",
"mycolumnfamily"),
new Count(),
new Fields("count"));
Приведенная выше реализация состояния типа CassandraState позволяет вы¬
полнять агрегирование групп, состоящих из одно- или двухэлементных кортежей.
Если группы состоят из одноэлементных кортежей, то база данных Cassandra
трактуется как хранилище пар “ключ-значение”. А если группы состоят из двух¬
элементных кортежей, то база данных Cassandra трактуется как хранилище клю¬
чей к отображению. В первом случае значение из одноэлементного кортежа со¬
ответствует ключу к базе данных Cassandra, а в качестве параметра указывается
используемый столбец globalCol, как показано в предыдущем примере. Во втором
случае первым элементом кортежа служит ключ к базе данных Cassandra, тогда как
вторым элементом — столбец в этой базе данных.
Подробнее о реализации состояния типа CassandraState
В исходном коде, сопутствующем данной книге, предоставляется простая реализация
состояния типа CassandraState. Но она не идеальная, поскольку обеспечивает пооче¬
редное, а не пакетное выполнение операций в базе данных. Вследствие этого потен¬
циальная производительность такой реализации состояния типа CassandraState ока¬
зывается намного ниже. В то же время ее код намного проще для понимания. И мы
надеемся, что она послужит вам удобной отправной точкой для сохранения состояний
в любой избранной вами базе данных.
Ниже приведено определение агрегатора Count.
public static class Count implements CombinerAggregator<Long> {
public Long init(TridentTuple tuple) (
return 1L;
331
Глава 17. Иллюстрация микропакетной обработки потоков
public Long combine(Long vail, Long val2) (
return vail + va12;
public Long zero() {
return OL;
}
дите, это довольно простая реализация агрегатора Count, аналогичная
определению агрехаторов в JCascalog. Обратите, в частности, внимание на то, что
во всем этом коде отсутствует сложная логика обработки идентификаторов пакетов
для достижения семантики только однажды”, поскольку это делается в Trident ав¬
томатически. В данном случае идентификатор naicera автоматически сохраняется
вместе с подсчетом, и если обнаружится, что хранящийся идентификатор пакета со¬
впадает с текущим, то никакого обновления постоянного хранилища не происходит.
Не рассмотренные еще части Trident будут поясняться далее по ходу изло¬
жения материала этой главы, а за более подробными сведениями об интерфей¬
се Trident API обращайтесь к оперативно доступной документации на систему
Apache Storm. В этой главе/ преследуется цель показать, каким образом практи¬
ческие задачи решаются с помощью микропакетной обработки, а не вдаваться
в подробности функционирования интерфейсов вроде Trident API.
17.2. Завершение построения уровня
ускорения в приложении SuperWebAnalytics.com
А теперь преобразуем конвейерные схемы из предыдущей главы в работоспо¬
собный код средствами Trident. Нам осталось реализовать обработку запросов
на подсчет количества просмотров страниц во времени и анализ показателя от¬
казов от просмотра страниц.
17.2.1. Просмотры страниц во времени
Ради напоминания конвейерная схема для подсчета количества просмотров
страниц во времени еще раз приводится на рис. 17.2. Что реализовать ее, нужно
решить, какие именно технологии следует применять для обращения с исход¬
ным потоком и состоянием.
Обращаться с исходным потоком удобно с помощью системы Apache Kafka.
Напомним, что одно из главных условий достижения семантики “только однаж¬
ды” во время сбоев состоит в том, чтобы всегда воспроизводить пакет точно та¬
ким же, как и прежде. В модели Storm такое свойство исходной очереди называ¬
ется транзакционной семантикой. Этим свойством обладает система Apache Kafka,
и поэтому она вполне подходит для организации микропакетной обработки.
Что же касается состояния, то для обрахцеиия с ним треоуется иххдекс типа
ключа к отсортированному отображению. Именно такой тип индекса и предо¬
ставляет база данных Apache Cassandra, а следовательно, она вполне подходит
для применения в данном случае.
332
Часть III, Уровень ускорения
Рис. 17.2. Конвейерная схема микропакетной обработки
запросов на подсчет количества просмотров страниц
Чтобы реализовать рассматриваемую здесь топологию, необходимо сначала
определить сток для чтения просмотров страниц из системы Apache Kafka. Это
делается в следующем фрагменте кода:
TridentTopology topology = new TridentTopology();
TridentKafkaConfig kafkaConfig = new TridentKafkaConfig (
new KafkaConfig.ZkHosts ("zkstr", "/kafka")/ "pageviews");
kafkaConfig.scheme = new PageviewScheme();
Конфигурация стока Trident Kafka осуществляется таким же образом, как
и у обычного стока Storm Kafka, как демонстрировалось в предыдущей главе.
Обратите внимание на установку схемы десериализации просмотров страниц
на следующие три поля: url, user и timestamp. Ниже приведен код, реализую¬
щий первую часть топологии, где нормализуются URL, а отметки времени пре¬
образуются в соответствующий промежуток времени.
Stream stream = topology.newStream("pageviewsOverTime",
new TransactionalTridentKafkaSpout(kafkaConfig))
.each(new Fields("url"),
new NormalizeURLO ,
new Fields("normurl"))
.each(new Fields("timestamp"),
new ToHourBucket(),
new Fields("bucket"))
Как видите, в этом коде просто вызываются функции для выполнения каждой
задачи в отдельности. Ниже приведена реализация этих функций.
public static class NormalizeURL extends BaseFunction (
public void execute(TridentTuple tuple,
ГЛЯва i7‘ ИлмостР^ микропаквтной обработки потоков
try {
TridentColiector collector) {
}
)
String urlStr « tuple.getstring(O);
URL url *= new URL (urlStr) ;
collector .emit (new Values (
ur1.getProtocol() + +
url.getHost() + url.getPath()));
catch (MalformedURLException e) {
}
333
public static class ToHourBucket extends BaseFunction {
private static final int HOUR_SECS = 60 * 60;
public void execute (TridentTuple tuple,
TridentColiector collector) {
int secs = t uple. get Integer (0) ;
int hourBucket = secs / HOUR_SECS;
collector .emit (new Values (hourBucket));
Логика в данном случае ничем не отличается о той, что применялась на уровне
пакетной обработки. Поэтому было бы лучше, чтобы реализующий ее код совместно
использовался на разных уровнях, но ради простоты он здесь просто дублируется.
И наконец, осталось лишь свести все подсчеты в базу данных Cassandra, при¬
чем сделать это идемпотентно. Прежде всего сконфигурируем состояние типа
CassandraState следующим образом:
CassandraState. Opt ions opts = new CassandraState. Options () ;
opts.keySerializer = StringSerializer.get();
opts.colSerializer = IntegerSerializer.get () ;
StateFactory state = CassandraState.transactional(
"127.0.0.1",
"superwebanalytics",
"pageviewsOverTime",
opts);
Соответствующие сериализаторы устанавливаются для ключей (URL) и столб-
цов (промежутков времени). Затем состояние конфигурируется таким образом,
чтобы указывать на соответствующий кластер, пространство ключей и семей¬
ство столбцов. Ниже приведено определение остальной части топологии.
stream.groupBy(new Fields("normurl", "bucket"))
.persistentAggregate(state,
new Count(),
new Fields("count"));
В конвейерной схеме обновитель состояния типа UpdatelnnerMap был исполь¬
зован агрегатором Count для выражения требующегося преобразования состоя¬
ния. Но в приведенном выше коде отсутствует ссылка на обновитель состояния
типа UpdatelnnerMap. Это полностью объясняется особенностями функционирова¬
ния состояния типа CassandraState. Как только группировка выполнена но двум
334
Часть III, Уровень ускорения
ключам, второй ключ интерпретируется в состоянии типа CassandraState как
ключ внутреннего отображения. Это означает, что обновитель состояния типа
UpdatelnnerMap неявно присутствует в данном определении топологии.
В данном случае ключ группировки состоит из двух полей: url и bucket.
Метод persistentAggregate() служит для применения встроенного агрегатора
Count с целью свести все подсчеты вместе. Идентификатор пакета автоматиче¬
ски сохраняется средствами Trident вместе с каждым подсчетом, чтобы обра¬
щаться идемпотентно с любыми сбоями и повторными попытками.
На этом завершается реализация микропакетной обработки запросов на под¬
счет количества просмотров страниц во времени на уровне ускорения. Как види¬
те, она очень краткая и довольно простая.
17.2.2. Анализ показателя отказов от просмотра страниц
А теперь покажем, как реализовать анализ показателя отказов от просмотра
страниц средствами Trident. Ради напоминания конвейерная схема для анализа
показателя отказов от просмотра страниц еще раз приводится на рис. 17.3.
Рис. 17.3. Конвейерная схема для анализа отказов
от просмотра при микропакетной обработке
Глава 17. Иллюстрация микропакетной обработки потоков
335
Эта топология намного сложнее, и поэтому рассмотрим ее по частям. Она до¬
вольно точно отражает конвейерную схему. Начало реализации этой топологии
выглядит следующим образом:
topology.newStream(
"bounceRate",
new TransactionalTridentKafkaSpout(kafkaConfig))
.each(new Fields("url"),
new NormalizeURL(),
new Fields("normurl"))
.each(new Fields("normurl"),
new ExtractDomainO ,
new Fields("domain"))
Как видите, в этом коде нет ничего нового. В нем употребляется поток про¬
смотров страниц из системы Kafka и выполняется пара функций для извлече¬
ния домена по указанному URL. Ниже приведена реализация следующей части
рассматриваемой здесь топологии, где анализируются посещения и выясняется,
когда происходят отказы от просмотра страниц.
.partitionBy(new Fields("domain", "user"))
.partitionPersist (
new MemoryMapState.Factory(),
new Fields("domain", "user", "timestamp"),
new AnalyzeVisits(),
new Fields("domain", "isBounce"))
.newValuesStream()
В приведенном выше фрагменте кода обращает на себя внимание операция
разделения типа partitionBy. Чтобы понять, зачем она нужна, напомним, каким
образом действует обновитель состояния типа AnalyzeVisits. Он анализирует
события просмотра страниц по очереди и обновляет состояние, исходя из того,
как долго пользователь посещает домен. Кроме того, обновитель состояния типа
AnalyzeVisits просматривает все посещения каждую минуту, чтобы выяснить,
были ли завершены какие-нибудь посещения, когда пройдет 30 минут без просмо¬
тра страницы пользователем. И хотя обновитель состояния типа AnalyzeVisits
осуществляет обновление состояния по отдельным доменам и конкретным поль¬
зователям, потенциально он анализирует все пары “домен-пользователь” в своем
состоянии при обработке отдельного кортежа.
А теперь вернемся к операции partitionBy. Для разделения кортежей
в Trident предоставляются две операции: partitionBy и groupBy. С одной сто¬
роны, операция groupBy позволяет сгруппировать кортежи вместе по общему
ключу и выполнить агрегирование в этих группах независимо от всех остальных
групп. А с другой стороны, операция partitionBy просто дает возможность ука¬
зать, каким образом кортежи должны быть распределены по отдельным задачам
обработки. Кортежи с одинаковыми ключами разделения будут направлены од¬
ной и той же задаче. Операция partitionBy применяется в данном случае пото¬
му, что обновитель состояния типа AnalyzeVisits не обрабатывает пары “домен-
пользователь” независимо друг от друга. Каждую минуту он анализирует все пары
“домен-пользователь”, хранящиеся в настоящий момент в оперативной памяти.
336
Часть HL Уровень ускорения
Эта топология оказывается правильной и в том случае, если она разделяется
по полю domain. Но это может привести к искажению, если в массиве данных
имеется лишь несколько доменов, где преобладают посещения. А если разделить
топологию по полям user и domain, то распределение окажется, скорее всего,
равномерным, поскольку маловероятно, чтобы в массиве данных преобладали
просмотры страниц одним пользователем.
Рассмотрим далее реализацию обновителя состояния типа AnalyzeVisits.
Он сохраняет все свое состояние в отображении, находящемся в оперативной
памяти, с помощью класса MemoryMapState, предоставляемого в Trident и реа¬
лизующего всю логику обращения с идентификаторами пакетов идемпотентно
при повторных попытках. Так, если произойдет сбой и потребуется еще раз об¬
работать пакет, реализация типа MemoryMapState гарантирует, что обновления
не будут применяться больше одного раза. Обо всем этом не придется вообще
беспокоиться в коде, реализующем обновитель состояния типа AnalyzeVisits.
Прежде чем перейти к реализации обновителя состояния типа AnalyzeVisits,
необходимо рассмотреть ряд вспомогательных классов. Эти классы представля¬
ют ключи и значения, хранящиеся в состоянии, применяемом обновителем со¬
стояния типа AnalyzeVisits для отслеживания посещений пользователями, как
показано ниже.
static class Visit extends ArrayList {
public Visit (String domain, PersonID user) {
super ();
add(domain);
add(user);
}
}
static class Visitlnfo {
public int startTimestamp;
public Integer lastVisitTimestamp;
public Visitlnfo(int startTimestamp) {
this. startTimestamp = startTimestamp;
this. lastVisitTimestamp = startTimestamp;
public Visitlnfo clone() {
Visitlnfo ret = new Visitlnfo(this.startTimestamp);
ret. lastVisitTimestamp = this. lastVisitTimestamp;
return ret;
А вот как реализуется сам обновитель состояния типа AnalyzeVisits:
public static class AnalyzeVisits
extends BaseStateUpdater<MemoryMapState> {
static final String LAST_SWEEP_TIMESTAMP = "lastSweepTs";
static final int THIRTY_MINUTES_SECS = 30 * 60;
public void updateState(
Глава 17. Иллюстрация микропакетной обработки потоков
337
MemoryMapState state,
List<TridentTuple> tuples,
TridentCollector collector) {
for(TridentTuple t: tuples) {
final String domain = t .getString (0);
final PersonID user = (PersonID) t.get(l);
final int timestampSecs = t. get Integer (2);
Visit v = new Visit (domain, user) ;
update (state, v, new ValueUpdater<VisitInfo> () {
public Visitlnfo update (Visitlnfo v) {
if(v==null) {
return new Visitlnfo (timestampSecs);
} else {
Visitlnfo ret = new Visitlnfo (v.startTimestamp);
ret.lastVisitTimestamp = timestampSecs;
return ret;
I
}
});
Integer lastSweep =
(Integer) get(state, LAST_SWEEP_TIMESTAMP);
if(lastSweep==null) lastSweep = 0;
List<Visit> expired = new ArrayListO ;
if (timestampSecs > lastSweep + 60) {
Iterator<List<Object» it = state.getTuples();
while(it.hasNext()) {
List<Object> tuple = it.next();
Visit visit = (Visit) tuple.get(0);
Visitlnfo info = (Visitlnfo) tuple.get(1);
if(info.lastVisitTimestamp >
timestampSecs + THIRTY_MINUTES_SECS) {
expired.add(visit);
if(info.startTimestamp ==
info.lastVisitTimestamp) {
collector.emit(new Values(domain, true));
} else {
collector.emit (new Values(domain, false));
}
}
put(state, LAST_SWEEP_TIMESTAMP, timestampSecs);
for (Visit visit; expired) {
remove(state, visit);
}
Логика этой реализации идентична псевдокоду, представленному в преды¬
дущей главе. Единственное отличие этой реализации заключается в том, что
%ля ее выражения требуется код Java. В коде данной реализации применяются
338
Часть III« Уровень ускорения
вспомогательные функции для взаимодействия с классом MemoryMapState, и по¬
этому ради полноты примера эти вспомогательные функции приведены ниже.
private static Object update(MapState s, Object key,
ValueUpdater updater) {
List keys = new ArrayList ();
List updaters = new ArrayList () ;
keys .add(new Values (key));
updaters.add(updater);
return s.multiUpdate(keys, updaters).get(0);
private static Object get(MapState s, Object key) {
List keys = new ArrayList () ;
keys, add (new Values (key));
return s.multiGet(keys).get(0);
}
private static void put(MapState s, Object key, Object val) (
List keys = new ArrayList () ;
keys.add(new Values (key));
List vals = new ArrayList ();
vals.add(val);
s.multiPut(keys, vals);
private static void remove (MemoryMapState s, Object key) {
List keys = new ArrayList () ;
keys, add (new Values (key)) ;
s.multiRemove (keys) ;
I
А вот как выглядит остальная часть определения рассматриваемой здесь то¬
пологии:
,each(new Fields("isBounce"),
new BooleanToInt(),
new Fields("bint"))
.groupBy(new Fields("domain"))
.persistentAggregate(
CassandraState.transactional(
"127.0.0.1",
"superwebana1уtics",
"bounceRate",
opts),
new Fields("bint"),
new CombinedCombinerAggregator(
new Count(),
new Sum ()),
new Fields("count-sum"));
В этой части топологии просто употребляется поток кортежей ["domain",
"isBounce"] и их агрегирование в базе данных Cassandra, чтобы определить ко¬
личество посещений в каждом домене и количество отказов от просмотра стра¬
ниц. Сначала логическое значение isBounce преобразуется с помощью функции
BooleanToInt () в числовое значение 0, если оно равно false, или в числовое
Глава 17. Иллюстрация микропакетной обработки потоков
339
значение 1, если оно равно true. Затем для обновления базы данных Cassandra
выполняется стандартный метод persistentAggregate().
По существу» нужно выполнить следующие две операции агрегирования: под¬
считать количество посещений и просуммировать целочисленные значения
isBounce, чтобы определить количество отказов от просмотра страниц. С этой
целью агрегаторы Count и Sum объединяются в одно целое с помощью утилиты
типа CombinedCombinerAggregator, которая определяется следующим образом:
public static class CombinedCombinerAggregator
implements CombinerAggregator {
CombinerAggregator[] _aggs;
public CombinedCombinerAggregator(CombinerAggregator... aggs) {
_aggs = aggs;
}
public Object init(TridentTuple tuple) {
List<Object> ret = new ArrayListO ;
for(CombinerAggregator agg: _aggs) {
ret.add(agg.init(tuple));
}
return ret;
public Object combine(Object ol, Object o2) {
List 11 = (List) ol;
List 12 = (List) o2;
List<Object> ret = new ArrayListO ;
for(int i=0; i<_aggs.length; i++) {
ret.add(_aggs[i].combine(
11. get(i),
12. get(i)));
}
return ret;
}
public Object zero() {
List<Object> ret = new ArrayListO ;
for(CombinerAggregator agg: _aggs) {
ret.add(agg.zero());
}
return ret;
}
И на этом реализация анализа отказов от просмотра страниц на уровне уско¬
рения завершается, хотя она далеко не идеальна. В частности, состояние не со¬
храняется постоянно и не реплицируется где-то еще, несмотря на то, что Trident
и класс MemoryMapState гарантируют, что обновления применяются только один
раз. Так, если отслеживание состояния выполнения задачи прекращается, это
состояние теряется.
Чтобы преодолеть этот недостаток, им можно, с одной стороны, просто
пренебречь, приняв небольшую степень неточности, вносимой в вычисления,
340
Часть III. Уровень ускорения
и полагаясь на то, что эта неточность будет исправлена на уровне пакетной об¬
работки, если она возникнет. А с другой стороны, можно выполнить обработку
потока с учетом состояния, сохраняемого в оперативной памяти, устойчивым
к отказам способом, как поясняется далее.
17.3. Полностью отказоустойчивая микропакетная
обработка с сохранением состояния в памяти
Добиться локального сохранения состояния в оперативной памяти, чтобы вос¬
становить его после прекращения рабочего процесса, можно двумя способами.
Первый, стандартный для баз данных способ состоит в том, чтобы вести в реплици¬
руемом хранилище журнал регистрации фиксаций. Для этой цели вполне подойдут
такие системы, как HDFS или Kafka. Всякий раз, когда состояние сохраняется или
обновляется, в журнале регистрации записывается само обновление. На рис. 17.4
показано, каким образом может выглядеть журнал регистрации.
Batch ID: 1
Operation: put
Args: "somekey”, "somevar
Batch ID: 1
Operation: put
Args: “somekey2", “someval2”
Batch ID: 2
Operation: remove
Args: “somekey2”
Batch ID: 2
Operation: put
Args: Hsomekey3n, 24
Batch ID: 2
Operation: put
Args: Hsomekey4n, Hsomeval4"
Batch ID: 3
Operation: remove
Args: н5отекеу4и
Рис. 17.4. Журнал регистрации фиксаций
Когда задача запускается на выполнение, она воспроизводит журнал регистра¬
ции фиксаций, чтобы восстановить внутреннее состояние. Разумеется, журнал
регистрации фиксаций разрастается до бесконечности, и поэтому восстанавли¬
вать состояние из журнала регистрации фиксаций становится с каждым разом
все дороже. Этот недостаток можно преодолеть, периодически уплотняя журнал
регистрации. Уплотнение представляет собой процесс сначала сохранения всего
состояния в целом, а затем удаления из журнала регистрации фиксаций всех эле¬
ментов, связанных с ведением этого состояния. Для сохранения всего состояния
в целом отлично подходит распределенная файловая система. Уплотнять журнал
регистрации достаточно, например, каждую секунду или после того, как он до¬
стигнет определенного размера.
Глава 17. Иллюстрация микропакетной обработки потоков
341
Другой способ постоянного хранения текущего состояния в оперативной
памяти вообще не предполагает применение журнала регистрации фиксаций.
Напомним, что Trident по своему характеру обрабатывает пакеты в строгом
порядке, и благодаря сохранению идентификатора пакета вместе с состоянием
можно обнаружить, был ли пакет обработан прежде, достигнув тем самым семан¬
тики “только однажды”. Но что, если система вычислений попытается повторно
обработать все пакеты, кроме последнего, скажем, за последние пять минут? Это
дает возможность сделать немало интересного и нового.
Самое главное — периодически проверять любое состояние, сохраняемое
в оперативной памяти, записывая его контрольную точку где-нибудь еще (напри¬
мер, в распределенной файловой системе). Такую проверку по контрольным точ¬
кам можно, например, вести каждую минуту. В ходе этой проверки сохраняется
также контрольная точка, достигнутая в исходном потоке.
А теперь допустим, что 45 секунд спустя возникает сбой и прекращается одна
из задач, удерживающая какой-то один раздел текущего состояния. В этот мо¬
мент сбойная задача удерживает состояние, которое оставалось текущим для па¬
кета 45 секунд назад, т.е. в последней контрольной точке. А все остальные задачи
остаются по-прежнему текущими, поскольку в них отсутствуют сбои.
Чтобы восстановить состояние, можно вернуться в исходном потоке на 45 се¬
кунд назад и воспроизвести его. И хотя эта операция зачастую обходится очень
дорого, она может быть очень эффективной, поскольку восстанавливать придет¬
ся только один раздел. Таким образом, при повторных вычислениях можно про¬
пустить те разделы, для которых состояние все еще актуально.
Аналогично первому способу, предполагающему применение журнала реги¬
страции фиксаций, данный способ требует периодической записи состояния
в целом. Но для этого не нужен журнал регистрации фиксаций, а следовательно,
данный способ лучше.
В то же время данный способ требует расширения модели микропакетной обра¬
ботки потоков, что в Storm и Trident в настоящее время не реализовано. Впрочем,
имеется другая система, называемая Spark, а в ней режим работы — Spark Streaming,
где такой подход реализован, как поясняется в приведенной ниже врезке.
Системы Spark и Spark Streaming
О системе Spark упоминалось ранее, когда речь шла о пакетной обработке в качестве
альтернативы MapReduce для эффективного использования оперативной памяти. В си¬
стеме Spark имеется другой режим работы, называемый Spark Streaming и реализую¬
щий микропакетную обработку потоков с периодической проверкой внутреннего состо¬
яния по контрольным точкам. Если в Trident основное внимание уделяется интеграции
с внешними базами данных, то в Spark Streaming — вычислению состояния для его со¬
хранения в оперативной памяти.
Системы вычислений можно разделить на виды вычислений, которые в них поддержи¬
ваются. Основными видами вычислений являются пакетная обработка, поочередная об¬
работка с малой задержкой и микропакетная обработка. В системе Hadoop поддержи¬
вается только пакетная обработка, в системе Storm — поочередная и микропакетная,
а в системе Spark — пакетная и микропакетная обработка.
342
Часть III. Уровень ускорения
Резюме
В этой главе было показано, каким образом микропакетная обработка пото¬
ков реализуется на практике с помощью интерфейса Trident API системы Apache
Storm. В ней было также показано довольно полное соответствие концептуально¬
го способа рассмотрения потока данных в виде конвейерных схем реализующему
его коду. И хотя в Trident отлично реализована поддержка сохранения состо¬
яния во внешних базах данных с семантикой “только однажды”, тем не менее,
поддержка локального сохранения состояния в оперативной памяти не является
полностью отказоустойчивой в Trident.
Итак, мы рассмотрели все части, составляющие основу лямбда-архитектуры:
уровень пакетной обработки, опирающийся на повторные вычисления, уровни об¬
служивания и ускорения. В ходе обсуждения все особенности этих уровней были
продемонстрированы на полноценном примере приложения SuperWebAnalytics.
com, в котором они реализованы. А теперь, опираясь на это основание, перейдем
к более подробному рассмотрению лямбда-архитектуры, проанализировав разноо¬
бразные возможности для повышения эффективности информационных систем.
в деталях
■ Повторный обзор лямбда-архитектуры.
■ Инкрементная пакетная обработка.
■ Эффективное управление ресурсами
в процессах пакетной обработки.
■ Объединение логики пакетных представлений
и представлений в реальном времени.
В главе 1 была представлена лямбда-архитектура и принятый в ней универ¬
сальный подход к реализации любой информационной системы. А во всех после¬
дующих главах были подробно рассмотрены различные составляющие лямбда-ар¬
хитектуры. Как было показано ранее, построение систем больших данных пред¬
полагает, что они должны быть не только масштабируемыми, но и надежными
и простыми для понимания.
А теперь, когда стали известны особенности всех уровней лямбда-архитекту¬
ры, воспользуемся этими знаниями, чтобы сделать обзор лямбда-архитектуры
еще раз и достичь лучшего ее понимания. В ходе этого обзора мы постараемся
восполнить оставшиеся пробелы и исследовать разные варианты рассмотренных
ранее методик.
18.1. Определение информационных систем
Итак, мы начали с постановки следующего вопроса: “Каково назначение ин¬
формационной системы?” Ответ на этот вопрос был прост: информационная си¬
стема отвечает на запросы, основываясь на данных, зафиксированных в прошлом.
Говоря более формально, информационная система обрабатывает запросы, кото¬
рые являются функциями, выполняемыми над всеми зафиксированными ранее
данными. Это интуитивное определение, явным образом инкапсулирующее любую
информационную систему, которую требуется построить, показано ниже.
запрос = функция (все данные)
344
Часть ///. Уровень ускорения
Имеется целый ряд следующих свойств, связанных с этими запросами.
■ Задержка* Время, требующееся для выполнения запроса. В большинстве
случаев требования к задержке означают, что она должна быть порядка не¬
скольких миллисекунд. А в других случаях вполне допустимо, что обработ¬
ка запроса может отнять несколько секунд. Для выполнения специального
анализа требования к задержке нередко оказываются довольно мягкими —
даже порядка нескольких часов.
■ Своевременность. Обозначает актуальность результатов обработки запро¬
са. При совершенно своевременной обработке запросов во внимание при¬
нимаются все данные, представленные в прошлом, тогда как в результаты
менее своевременной обработки запросов могут быть не включены дан¬
ные, полученные за последние несколько минут или часов.
■ Точность. Как правило, чтобы сделать обработку запросов эффективной
или масштабируемой, приходится прибегать к аппроксимации при реали¬
зации запросов.
Значительная часть процесса построения информационных систем связана
с обеспечением их отказоустойчивости. С этой целью приходится заранее пла¬
нировать поведение системы при возникновении аппаратных сбоев. Зачастую
это означает необходимость идти на компромиссы в отношении перечисленных
свойств. Например, между задержкой и своевременностью имеется принципи¬
альное противоречие. Как следует из теоремы САР, при разделении сети система
должна быть согласованной (при обработке запросов принимаются во внимание
все записанные ранее данные) или доступной (ответы на запросы поступают сво¬
евременно). Согласованность является лишь формой своевременности, а доступ¬
ность означает ограничение на задержку запроса. При построении окончательно
согласованной системы предпочтение отдается задержке над своевременностью
(ответы на запросы получаются всегда, но в них могут не приниматься во внима¬
ние все предыдущие данные при возникновении сбоев).
Информационные системы динамичны, они строятся людьми, которые явля¬
ются их неотъемлемой частью, поэтому эти системы постоянно анализируются,
в них все время вносятся изменения и внедряются новые свойства. Но людям
свойственно ошибаться. Они вносят в эксплуатируемые системы самые разные
ошибки. Поэтому так важно, чтобы информационные системы были устойчивы
к ошибкам, связанным с человеческим фактором.
Как было показано ранее, изменчивость и связанные с ней понятия вроде
СКИП принципиально не устойчивы к ошибкам, обусловленным человеческим
фактором. Если человек может изменить данные, то и ошибка может их изме¬
нить. Следовательно, разрешение обновлять и удалять основные данные неиз¬
бежно приводит к их искажению.
Единственный выход - сделать основные данные неизменяемыми, разрешив
операции записи только для присоединения новых данных к постоянно увели¬
чивающемуся массиву. Чтобы запретить удаление и обновление основных дан¬
ных, можно установить соответствующие разрешения. Эта дополнительная мера
гарантирует, что ошибки не смогут испортить данные, а следовательно, инфор¬
мационная система станет намного более надежной.
Глава 18. Лямбда-архитектура в деталях
345
Это приводит нас к следующей основной модели информационных систем.
■ Главный массив данных, состоящий из постоянно увеличивающегося мно¬
жества данных.
■ Запросы в виде функций, принимающих весь главный массив в качестве
входных данных.
По этой модели можно выполнить любую обработку данных, причем в осно¬
ву такой информационной системы положено очень важное свойство устойчи¬
вости к ошибкам, связанным с человеческим фактором. Если бы такую инфор¬
мационную систему можно было реализовать, она стала бы идеальной. Лямбда-
архитектура возникает из некоторых возможных жертв, на которые приходится
идти ради достижения этого идеала, когда запросы служат функциями постоянно
увеличивающегося неизменяемого массива данных.
18.2. Уровни пакетной обработки и обслуживания
Выполнять запросы в виде функций всех данных непрактично, поскольку
вряд ли стоит ожидать возврата ответов на запросы в многотерабайтовом, а тем
более многопетабайтовом массиве данных через несколько миллисекунд. И даже
если бы это было возможно, то запросы непомерно интенсивно потребляли бы
ценные ресурсы. В качестве самой простой модификации такой архитектуры
можно запрашивать предварительно вычисленные представления, а нс обра¬
щаться непосредственно к массиву данных. Предварительно вычисленные пред¬
ставления можно приспособить таким образом, чтобы обрабатывать запросы
как можно быстрее, тогда как сами представления сделать функциями главного
массива данных.
В главах 2-9 были подробно рассмотрены особенности реализации такой архи¬
тектуры. В ее основу положена система пакетной обработки, способная выполнять
функции над всеми данными масштабируемым и отказоустойчивым способом.
Именно поэтому данная часть лямбда-архитектуры и называется уровнем пакетного
обработки.
Назначение уровня пакетной обработки состоит в том, чтобы производить
представления, индексируемые таким образом, чтобы запросы к ним можно
было разрешать с малой задержкой. Индексирование и обслуживание этих пред¬
ставлений выполняется на уровне обслуживания, тесно связанном с уровнем па¬
кетной обработки. При разработке уровней пакетной обработки и обслуживания
необходимо соблюсти равновесие между объемом вычислений, предварительно
выполняемых на уровне пакетной обработки с учетом размеров представле¬
ний, а также объемом вычислений, требующихся во время запроса (подробнее
об этом см. в главе 6).
А теперь попробуем выйти за рамки этой основной модели уровней пакетной
обработки и обслуживания лямбда-архитектуры, чтобы выяснить, какие еще воз¬
можности имеются для разработки этих уровней. Главным показателем произво¬
дительности на этих уровнях служит продолжительность обновления представле¬
ний. А поскольку на уровне ускорения должны быть восстановлены все данные,
отсутствующие на уровне обслуживания, то чем дольше выполняются вычисле¬
ния на уровне пакетной обработки, тем крупнее должны быть представления
346
Часть III. Уровень ускорения
на уровне ускорения. Потребность в крупных кластерах значительно более слож¬
ных баз данных существенно усложняет их эксплуатацию. Кроме того, чем доль¬
ше выполняются вычисления на уровне пакетной обработки, тем дольше проис¬
ходит восстановление работоспособности после программных ошибок, случайно
проникающих в эксплуатируемую систему при развертывании. Чтобы сократить
задержку на уровне пакетной обработки, можно, в частности, сделать обработку
данных инкрементной.
18.2.1. Инкрементная пакетная обработка
В главе 6 сравнивались достоинства и недостатки алгоритмов инкрементных
и повторных вычислений. В ней было показано, что одно из главных преиму¬
ществ уровня пакетной обработки заключается в возможности выгодно исполь¬
зовать алгоритмы повторных вычислений. Поэтому предложение сделать обра¬
ботку данных на уровне пакетной обработки инкрементной может показаться
неуместным. Но, как это принято в разработке вообще, необходимо рассмотреть
все компромиссы, чтобы прийти к наилучшему проектному решению.
Рассмотрим крайний случай, когда производится единственное представле¬
ние глобального подсчета всех записей в главном массиве данных. В этом случае
применение инкрементной обработки на уровне пакетной обработки дает явное
преимущество, поскольку инкрементное представление оказывается не крупнее,
чем повторно вычисляемое представление (лишь одного числа в обоих случаях).
А кроме того, сделать код инкрементным совсем не трудно. Благодаря тому что вы¬
числения не повторяются многократно во всем главном массиве данных в целом,
экономится немало ресурсов. Так, если главный массив содержит 100 терабайт
данных, а каждый новый пакет — 100 гигабайт данных, то эффективность уровня
пакетной обработки возрастает на несколько порядков величины. Ведь на каждом
шаге итерации придется обрабатывать 100 гигабайт, а не 100 терабайт данных.
А теперь рассмотрим в качестве еще одного примера задачу логического вы¬
вода о дне рождения человека, когда труднее сделать выбор между алгоритма¬
ми инкрементных и повторных вычислений. Допустим, требуется написать по¬
исковый робот, собирающий сведения о возрасте людей из их общедоступных
профилей. В таком профиле отсутствуют сведения о дне рождения, но имеется
только возраст человека на момент посещения веб-страницы. На основании этих
исходных данных в виде пары [возраст, отметка времени] требуется сделать
логический вывод о дне рождения каждого человека.
Принцип действия алгоритма логического вывода о дне рождения человека
наглядно показан на рис. 18.1. Допустим, в профиле пользователя Тома от 4 ян¬
варя 2012 года обнаружен его возраст 23 года, а при последующем просмотре
его профиля от 11 января 2012 года выясняется, что ему уже 24 года. Из этих
сведений можно сделать логический вывод, что день рождения Тома находится
в промежутке между этими двумя датами. Аналогично, если в профиле пользо¬
вателя Джилл от 20 октября 2013 года обнаруживается ее возраст 43 года, а при
последующем просмотре се профиля от 4 ноября 2013 года выясняется, что ей
уже 44 года, то день рождения Джилл находится в промежутке между этими дву¬
мя датами. Чем больше выборок возраста имеется, тем точнее можно сделать
логический вывод о дне рождения человека в небольшом диапазоне дат.
Глава 18. Лямбда-архитектура в деталях
347
Возраст 28 лет
на 21.02.2012
22.02.1983
21.02.1984
Возраст 29 лет
на 4.03.2012
5.03.1983
4.03.1984
Возраст 30 лет
на 2.06.2013
3.06.1982
2.06.1983
Диапазон дат
для логического
вывода о дне рождения
5.03.1983
2.06.1983
Рис. 18.1. Элементарный алгоритм логического вывода о дне рождения человека
Разумеется, на практике даты могут быть перепутаны. Кто-нибудь может ввести
неверную дату своего рождения, а в дальнейшем изменить ее. В итоге алгоритм не
сможет сделать логический вывод о дне рождения человека, поскольку из рассмо¬
трения в качестве возможного дня рождения исключен каждый день года. Этот
алгоритм можно усовершенствовать, сузив поиск до минимального количества вы¬
борок возраста, которыми можно пренебречь, чтобы получить наименьший диа¬
пазон дат, где может находиться день рождения. В таком алгоритме предпочтение
может быть отдано последним выборкам возраста над прежними.
Реализовать алгоритм логического вывода о дне рождения человека на уров¬
не пакетной обработки с помощью повторных вычислений совсем не трудно.
В этом случае алгоритм сможет проанализировать сразу все выборки возраста
человека, выполнить необходимые действия над перепутанными датами и вы¬
дать в конечном итоге единственный диапазон дат. Но намного труднее реализо¬
вать алгоритм логического вывода о дне рождения человека на уровне пакетной
обработки с помощью инкрементных вычислений. В этом случае трудно решить,
что делать с перепутанными датами, не имея доступа ко всему диапазону выбо¬
рок возраста. Сделать такой алгоритм полностью инкрементным значительно
труднее, и для этого может потребоваться намного более крупное и сложное
представление.
ЧАСТИЧНЫЕ ПОВТОРНЫЕ ВЫЧИСЛЕНИЯ
Повторно вычислять день рождения каждого человека из выборок возраста
в каждый отдельный момент времени выполнения вычислений на уровне па¬
кетной обработки слишком расточительно. В частности, если отсутствуют но¬
вые выборки возраста человека с момента последнего выполнения вычислений
на уровне пакетной обработки, то логический вывод о дне рождения этого чело¬
века вообще не изменится. Принцип частичных повторных вычислений на уров¬
не пакетной обработки для логического вывода о дне рождения этого человека
состоит в выполнении следующих действий.
348
Часть III. Уровень ускорения
1. Найти в новом пакете данных всех людей, для которых имеется новая вы¬
борка возраста.
2. Извлечь все выборки возраста из главного массива данных для всех людей,
обнаруженных в п.1.
3. Повторно вычислить дни рождения всех людей, обнаруженных в п.1, ис¬
пользуя выборки возраста из п.2 и выборки возраста в новом пакете.
4. Объединить вновь вычисленные дни рождения в представлениях, существу¬
ющих на уровне обслуживания.
Это не полностью инкрементные вычисления, поскольку в них по-прежнему
используется главный массив данных. Тем не менее они позволяют избежать боль¬
шей части затрат, связанных с полным повторением вычислений, отсекая тех лю¬
дей, сведения о которых не изменились в самом последнем массиве данных.
На примере логического вывода о дне рождения человека нетрудно заметить,
что частичные повторные вычисления позволяют разрешить многие затрудне¬
ния. Самое главное — извлечь все соответствующие данные для тех элементов,
которые не изменились, выполнить обычный алгоритм повторных вычислений
над извлеченными и новыми данными, а затем объединить полученные резуль¬
таты в существующие представления. Частичные повторные вычисления приме¬
чательны тем, что их можно сделать очень эффективными. Выполнение самой
затратной их стадии (т.е. просмотр всего главного массива данных в поисках
подходящих данных) может обойтись относительно дешево.
Самое главное для повышения эффективности вычислений — избежать по¬
вторного разделения всего главного массива данных, поскольку это самая затрат¬
ная часть алгоритмов пакетных вычислений. Например, повторное разделение
происходит всякий раз, когда выполняется операция группирования или сое¬
динения. Разделение включает в себя сериализацию и десериализацию, переда¬
чу данных по сети, а, возможно, их буферизацию на диске. С другой стороны,
в операциях, не требующих разделения, можно быстро просмотреть данные,
обработав их по мере выявления. Последним способом может быть извлечение
подходящих данных для частичных повторных вычислений.
На первой стадии извлечения подходящих данных создается множество всех
элементов, для которых требуются эти данные. Затем просматривается главный
массив данных, из которого извлекаются только те данные, которые требуются
для элементов, имеющихся в этом множестве (у каждой задачи будет своя копия
такого множества). В такой системе пакетной обработки, как НасЬор, это соот¬
ветствует заданию только на предварительную обработку данных.
Размер множества ограничивается наличным объемом оперативной памяти.
Но структура данных, называемая фильтром Блума, позволяет создавать намного
более крупные множества элементов. Фильтр Блума является компактной струк¬
турой данных, представляющей множество элементов и позволяющей обращать¬
ся к нему, если оно содержит нужный элемент. Фильтр Блума намного более ком¬
пактный, чем множество, но в качестве компромисса операции запрашивания
множества носят вероятностный характер. Иногда фильтр Блума неправильно
сообщает, что элемент существует во множестве, но он никогда не сообщит, что
элемент, который был введен во множество, отсутствует в нем. Таким образом,
Глава 18. Лямбда-архитектура в деталях
349
применение филнгра Блума дает скорее ложноположительные, чем ложноотри¬
цательные результаты.
Применение фильтра Блума для оптимизации процесса извлечения подходя¬
щих данных показано на рис. 18.2. Если фильтр Блума применяется для извле¬
чения подходящих данных из главного массива, то большая часть содержимого
этого массива данных отсеивается. Но в силу ложноположительных результатов
применения фильтра Блума некоторые извлеченные данные окажутся ненужны¬
ми. В таком случае извлеченные данные можно соединить со списком требую¬
щихся элементов, чтобы отсеять ложноположительные результаты. Для такого
соединения потребуется разделение, но поскольку большая часть содержимого
главного массива данных уже отсеяна, операция избавления от ложноположи¬
тельных результатов обойдется недорого.
Рис. 18.2. Соединение с применением фильтра Блума
350
Часть ///. Уровень ускорения
А теперь оценим, насколько улучшился показатель задержки от внедрения ин¬
крементной обработки на уровне пакетной обработки, сравнив частичные по¬
вторные вычисления с полностью повторяющимися вычислениями. Допустим,
для логического вывода о дне рождения человека требуется полностью выпол¬
нить в MapReduce одно задание с разделением и что о кластере и находящихся
в нем данных известно следующее.
■ Главный массив содержит 100 терабайт данных.
■ При частичных повторных вычислениях каждый новый пакет будет содер¬
жать 50 гигабайт данных.
■ Для выполнения в MapReduce задания с разделением потребуется 8 часов,
чтобы обработать весь главный массив данных.
■ Для выполнения задания только на предварительную обработку данных
(т.е. без какого-либо разделения) потребуется 2 часа, чтобы обработать
весь главный массив данных. Как правило, такое задание выполняется
в четыре раза быстрее в кластерах MapReduce.
■ Для создания совершенно новых представлений на уровне обслуживания
потребуется 2 часа при полностью повторяющихся вычислениях.
■ Для обновления представлений на уровне обслуживания потребуется 1 час
при частичных повторных вычислениях.
При таких исходных данных для повторного вычисления заново всех пред¬
ставлений логического вывода о дне рождения человека потребуется 8 часов
и еще 2 часа для построения представлений на уровне обслуживания, что в ито¬
ге составит 10 часов. А вот каких показателей можно добиться при частичных
повторных вычислениях.
■ Для получения подходящих данных из главного массива данных потребу¬
ется 2 часа.
■ Для вычисления новых дней рождения по элементам в текущем пакете по¬
требуется несколько минут.
■ Для обновления представлений, существующих на уровне обслуживания,
вновь вычисленными днями рождения потребуется 1 час.
Таким образом, при частичных повторных вычислениях для выполнения
задания потребуется около 3 часов, т.е. на 70% меньше времени, чем при пол¬
ностью повторяющихся вычислениях. Безусловно, это всего лишь оценочные
показатели, но они дают ясное представление о тех усовершенствованиях, кото¬
рые следует ожидать от частичных повторных вычислений на уровне пакетной
обработки. А при выполнении более сложных пакетных вычислений, которые
требуют выполнения не одной стадии разделения, экономия времени окажется
еще более ощутимой. Так, если для алгоритма повторных вычислений требуются
четыре стадии разделения, то для выполнения задания при полностью повторя¬
ющихся вычислениях потребуется 34 часа, тогда как при частично повторных
вычислениях — лишь около 3 часов.
Еще одно преимущество частичных повторных вычислений заключает¬
ся в тОхЧ, что они дают возможность в какой-то мере исправлять ошибки,
Глава 18. Лямбда-архитектура в деталях
351
обусловленные человеческим фактором. Так, если записываются неправиль¬
ные данные, способные испортить некоторые элементы, такую ошибку можно
исправить на уровне обслуживания, выполнив частичные повторные вычисле¬
ния только для подверженных искажению элементов. Это дает возможность
намного быстрее внести коррективы в приложение, чем при полностью по¬
вторяющихся вычислениях. Но частичные повторные вычисления помогают
исправить ошибки лишь при условии, что можно выявить все подверженные
искажению элементы. Именно поэтому частичные повторные вычисления ока¬
зываются намного более эффективными для исправления ошибок, связанных
с записью неправильных данных, чем ошибок, связанных с развертыванием
ошибочного кода, портящего представления. По устойчивости к ошибкам,
обусловленным человеческим фактором, частичные повторные вычисления
находятся где-то посредине между полностью повторяющимися и полностью
инкрементными вычислениями.
Когда частичные повторные вычисления уместны, они позволяют намного
сократить задержку при обработке данных на уровне пакетной обработки, не
принося в жертву выгоды от применения алгоритмов, основанных на повторных
вычислениях. Как правило, такие вычисления не годятся для алгоритмов, приме¬
няемых в реальном времени, поскольку для этого, возможно, придется проиндек¬
сировать весь главный массив данных, что обойдется очень дорого. И, очевидно,
что просмотр всего главного массива данных в реальном времени неосуществим.
Но для уровня пакетной обработки частичные повторные вычисления оказыва¬
ются вполне пригодными, и поэтому их следует взять на вооружение.
РЕАЛИЗАЦИЯ ИНКРЕМЕНТНОГО УРОВНЯ ПАКЕТНОЙ ОБРАБОТКИ
Независимо от того, применяются ли алгоритмы полностью повторяющихся
или частичных повторных вычислений на инкрементном уровне пакетной обра¬
ботки, главное его отличие от уровня пакетной обработки, опирающегося на по¬
вторные вычисления, заключается в необходимости обновлять представления
на уровне обслуживания, а не создавать их заново.
Вполне возможно построить инкрементный уровень пакетной обработки,
аналогичный уровню ускорения, реализовав представления с помощью баз дан¬
ных чтения-записи, которые модифицируются на месте. Но в этом случае при¬
дется пренебречь многими (обсуждавшимися в главе 10) преимуществами баз
данных уровня обслуживания , которых можно добиться, если не поддерживать
операции произвольной записи. К этим преимуществам относятся следующие.
■ Надежность. Отсутствие потребности выполнять операции произволь¬
ной записи означает, что кодовая база упрощается и в меньшей степени
подвержена программным ошибкам.
■ Упрощение эксплуатации. Чем проще базы данных, тем проще их настра¬
ивать, вести и эксплуатировать.
■ Более прогнозируемая производительность. Отсутствие потребности
выполнять операции произвольной записи параллельно с операциями
чтения означает также, что можно не беспокоиться о любого рода блоки¬
ровках в самой базе данных. Если в базе данных с произвольным чтением
и записью требуется периодически выполнять уплотнение инсЬормаиии,
352
Часть III. Уровень ускорения
чтобы избавиться от лишних частей индекса, что приводит к заметному
снижению производительности, то в базах данных без произвольной запи¬
си делать этого вообще не нужно.
Итак, уделим внимание сохранению в как можно большей степени этих
свойств баз данных на уровне обслуживания. В главе 11 было рассмотрено одно
проектное решение для организации уровня обслуживания на основе базы дан¬
ных ElephantDB.
Главная особенность применения базы данных ElephantDB состоит в том, что
представление на уровне пакетной обработки индексируется и разделяется в за¬
дании MapReduce, а полученные в итоге индексы сохраняются в распределен¬
ной файловой системе. В кластере базы данных ElephantDB периодически про¬
веряются новые версии представления, которые оперативно заменяют прежние
версии по мере их доступности. А самое главное, что создание и обслуживание
представлений происходит совершенно независимо и согласуется через распре¬
деленную файловую систему.
Такое проектное решение можно расширить для внедрения инкрементной
пакетной обработки таким образом, чтобы передавать последнюю версию пред¬
ставления на уровне пакетной обработки в качестве входных данных для зада¬
ния, создающего новую версию этого представления. В таком случае обновления
применяются к прежней версии, тогда как новая версия записывается в распре¬
деленной базе данных (что и реализуется в базе данных ElephantDB). Так, если
в качестве системы индексации применяется база данных BerkeleyDB, где сохра¬
няются подсчеты слов, то задание на создание новой версии представления бу¬
дет выполнено следующим образом. Задача для заданного раздела представления
сначала загрузит соответствующий раздел из распределенной файловой систе¬
мы, затем откроет его локально, инкрементирует подсчеты слов для его пакета
данных, а далее скопирует обновленное представление в папку для новой версии
в распределенной файловой системе. При таком подходе вся инкрементная об¬
работка происходит на стадии создания представления, а обслуживание новых
версий представления ничем не отличается от прежнего.
Такая стратегия избавляет от необходимости переделывать всю работу по соз¬
данию предыдущей версии представления. Она дает также возможность выгодно
воспользоваться повышенной задержкой на уровне пакетной обработки, чтобы
уплотнить индексы перед их записью в распределенной файловой системе.
Эта стратегия лучше всего подходит для представлений умеренного и неболь¬
шого размера. Если же сами представления оказываются крупными, то затра¬
ты на выполнение заданий могут возрасти из-за преобладания операций чтения
и записи всего представления в распределенной файловой системе. В подобных
случаях внедрение инкрементной обработки может принести мало пользы. В ка¬
честве альтернативы можно воспользоваться структурой баз данных на уровне
обслуживания, предоставляющей “приращения”, которые оперативно объеди¬
няются в этих базах данных. Разумеется, в этом случае придется также выпол¬
нять уплотнение данных во время обслуживания, а следовательно, базы данных
на уровне обслуживания станут в большей степени похожи на базы данных чте¬
ния-записи со всеми их сложностями.
Глава 18. Лямбдогархитектура в деталях
353
Правда, размеры представлений на инкрементном уровне пакетной обработ¬
ки можно свести к минимуму таким образом, чтобы не прибегать к стратегии
“приращений” или базам данных чтения-записи на уровне обслуживания. Вместо
этого можно сохранить преимущества уровня обслуживания , которые заключа¬
ются в совершенно независимом создании и обслуживании представлений. С
этой целью можно организовать несколько пакетных уровней.
НЕСКОЛЬКО ПАКЕТНЫХ УРОВНЕЙ
Вместо одного уровня пакетной обработки и одного уровня ускорения, где
возмещается задержка на уровне пакетной обработки, можно организовать не¬
сколько пакетных уровней. В частности, можно организовать один уровень па¬
кетной обработки, опирающийся на полностью повторяющиеся вычисления
один раз в месяц, а другой уровень пакетной обработки сделать инкрементным,
чтобы оперировать только теми данными, которые не представлены на уровне
пакетной обработки с полностью повторяющимися вычислениями. Вычисления
на инкрементном уровне пакетной обработки могут выполняться каждые шесть
часов. А на уровне ускорения будут восстанавливаться все данные, не представ¬
ленные на двух пакетных уровнях.
В элементарной лямбда-архитектуре уровень пакетной обработки смягчает
требования к уровню ускорения. Аналогично каждый последующий уровень па¬
кетной обработки смягчает требования к предыдущему уровню пакетной обра¬
ботки. В упомянутом выше примере на инкрементном уровне обрабатываются
данные только двухмесячной давности. Это означает, что его представления
могут быть намного более мелкими, чем представления, которые должны посто¬
янно отображать все данные. Таким образом, методы вроде создания совер¬
шенно новых представлений на основе прежних представлений на уровне об¬
служивания вполне осуществимы благодаря тому, что затраты на копирование
представлений не станут преобладающими.
Еще одно преимущество от наличия нескольких пакетных уровней заключается
в том, что оно позволяет извлечь обоюдную выгоду из инкрементной обработки
и повторных вычислений. В частности, процессы инкрементной обработки могут
стать намного более эффективными, хотя им будет недоставать той возможности
восстанавливаться после ошибок, которая имеется у процессов повторных вычис¬
лений. Если повторные вычисления постоянно выполняются в отдельной части
системы, то восстановить ее работоспособность можно после любой ошибки.
Задержка обработки данных на каждом уровне системы оказывает влияние
на требования к производительности на вышестоящем уровне. Именно поэтом)'
так важно иметь ясное представление о влиянии эффективности прикладного
кода и объема выделяемых ресурсов на задержку обработки данных на каждом
уровне. Выясним, как проявляется это влияние.
18.2.2. Измерение и оптимизация использования
ресурсов на уровне пакетной обработки
На производительность процессов пакетной обработки 1юздсйствуют разные
противоречивые динамические факторы. Ниже приведены некоторые реальные
примеры такого воздействия.
354
Часть III. Уровень ускорения
■ После удвоения размера кластера задержка обработки данных на уровне
пакетной обработки сократилась с 30 до б часов, т.е. усовершенствование
произошло на 80%.
■ В результате неверно выполненной повторной конфигурации частота сбо¬
ев при выполнении задач в кластере Наёоор возросла на 10%. В итоге вре¬
мя выполнения процессов пакетной обработки возросло с 8 до 72 часов,
т.е. производительность снизилась в девять раз.
На первый взгляд, трудно понять, как такое вообще возможно, но ос¬
новную динамику обработки данных нетрудно проиллюстрировать на кон¬
кретном примере. Допустим, имеется процесс пакетной обработки данных,
для выполнения которого требуется 12 часов. Следовательно, на каждом шаге
итерации данные в этом процессе обрабатываются в течение 12 часов. А те¬
перь допустим, что процесс усложняется введением некоторого дополнитель¬
ного анализа, выполнение которого оценивается в два часа, добавляемых ко
времени обработки данных в этом процессе. Таким образом, время выполне¬
ния процесса возрастет в следующий раз с 12 до 14 часов. Это означает, что
на следующем шаге итерации будут обрабатываться данные, накопившиеся за
последние 14 часов. А поскольку таких данных окажется больше, то их об¬
работка отнимет еще больше времени. И это означает, что в очередной раз
данных окажется еще больше и т.д.
Прибегнув к несложным математическим расчетам, можно определить, стано¬
вится ли устойчивым время выполнения и когда это происходит. Прежде всего
составим уравнение для времени выполнения одного шага итерации в процессе
пакетной обработки данных. В этом уравнении будут употребляться следующие
переменные.
■ Т — время выполнения процесса обработки данных в часах.
■ О — издержки в процессе обработки данных в часах. Это время, затрачивае¬
мое в процессе независимо от самой обработки данных. Такие издержки мо¬
гут включать в себя установку процессов, копирование кода в кластере и т.д.
■ Н— количество часов, затрачиваемых на обработку данных на шаге итера¬
ции. Это количество часов служит для измерения объем,а данных, поскольку
таким образом значительно упрощаются получаемые в итоге уравнения.
Предполагается, что данные поступают с довольно постоянной частотой,
хотя это и не оказывает никакого влияния на последующие выводы.
■ Р — время динамической обработки. Это количество часов, добавляемых
за каждый час существования данных ко времени обработки данных в про¬
цессе. Так, если за каждый час существования данных ко времени выполне¬
ния процесса добавляется полчаса, то Р = 0,5.
Исходя из приведенных выше определений можно составить следующее есте¬
ственное уравнение для определения времени выполнения одного шага итера¬
ции в процессе обработки данных:
Т= О + Р х //
Глава 18. Лямбда-архитектура в деталях
355
Разумеется, переменная Н в этом уравнении будет меняться на каждом шаге ите¬
рации в процессе обработки данных. Ведь если для выполнения этого процесса
потребуется больше или меньше времени, чем на предыдущем шаге итерации, то
на следующем шаге придется обработать больше или меньше данных соответствен¬
но. Чтобы определить устойчивое время выполнения процесса обработки данных,
нужно выявить момент, когда время выполнения этого процесса окажется равным
количеству часов существования обрабатываемых данных. Для этого достаточно
подставить Т = Н и решить уравнение для переменной Г, как показано ниже.
Т= О + Рх Н
а -р)
Как видите, устойчивое время выполнения процесса обработки данных
прямо пропорционально величине издержек в этом процессе. Так, если со¬
кратить издержки на 25%, то время выполнения процесса убавится на те же
самые 25%. Но, с другой стороны, устойчивое время выполнения процесса
обработки данных обратно пропорционально времени динамической обра¬
ботки Р. Из этого можно, в частности, сделать следующий вывод: убывающий
эффект от повышения производительности достигается при добавлении ка¬
ждой машины в кластер.
Что произойдет, если величина Р окажется равной или больше 1?
Возникает резонный вопрос: что же произойдет, если величина Р окажется равной или
больше 1? В этом случае на каждом шаге итерации процесса обработки окажется больше
данных, чем на предыдущем шаге. Следовательно, обработка данных на уровне пакет¬
ной обработки будет все больше и больше задерживаться с каждым шагом итерации и в
конечном итоге отстанет навсегда. Именно поэтому так важно поддерживать величину
Р меньше 1.
Благодаря приведенному выше уравнению описанные ранее противоречивые
факторы приобретают намного больший смысл. Рассмотрим сначала, что прои¬
зойдет с устойчивым временем выполнения процесса, если удвоить размер кла¬
стера. В этом случае время динамической обработки Р сократится приблизитель¬
но наполовину, поскольку теперь обработку данных можно распараллелить вдвое
(формально увеличатся также издержки на согласование, но не намного, и поэ¬
тому ими можно пренебречь). Если обозначить устойчивое время выполнения
процесса до удвоения размера кластера как 77, а устойчивое время выполнения
процесса после такого удвоения — как Т2, то в конечном итоге можно составить
следующие уравнения:
77 =
О
О -Р)
о
7’2 =
356
Часть III. Уровень ускорения
Решив эти уравнения для соотношения Т2/Т1> можно в итоге получить при¬
веденное ниже уравнение. На основании этого уравнения можно построить гра¬
фик, представленный на рис. 18.3.
72 1 -Р
71 ~ (2 - Р)
Этот график явно показывает, что при очень малой величие Р (например,
около 6 минут на каждый час существования данных) удвоение размера кластера
едва ли окажет заметное влияние на время выполнения процесса. И это вполне
объяснимо, поскольку время выполнения преобладает над издержками, на кото¬
рые удвоение размера кластера не оказывает никакого влияния.
Рис. 18.3. График, демонстрирующий влияние, оказываемое
на производительность при удвоении размера кластера
Но если величина Р довольно большая (например, около 54 минут времени
динамической обработки, приходящегося на каждый час существования данных),
то в результате удвоения размера кластера время выполнения процесса на новом
шаге итерации составит лишь 18% от своей прежней величины, т.е. процесс об¬
работки данных ускорится на 82%! Таким образом, на следующем шаге итерации
процесс обработки завершится намного раньше, а следовательно, на следующем
далее шаге итерации окажется меньше данных, а их обработка завершится еще
раньше. Эта петля положительной обратной связи в конечном итоге установится
на величине ускорения на 82%.
Рассмотрим далее воздействие, которое возрастание частоты сбоев оказыва¬
ет на устойчивое время выполнения процесса обработки. В частности, частота
сбоев задач 10% означает, что нужно выполнить приблизительно на 11% больше
задач, чтобы обработать данные. (Так, если имелось 100 задач и 10 из них дали
сбой, то повторить придется все эти 10 задач. Но в среднем одна из этих задач
также даст сбой, поэтому придется повторить и ее.) Л поскольку выполнение за¬
дач зависит от имеющегося объема данных, то время обработки данных за один
час (Р) увеличится на 11%.
Глава 18. Лямбда-архитектура в деталях
357
Как и при предыдущем анализе, обозначим время выполнения процесса
до сбоев как 77, а время выполнения после сбоев как 72. В итоге можно соста¬
вить следующие уравнения:
71 =
О
(1 -Р)
72 =
О
(1-1.1x7)
Теперь соотношение Т2/Т1 дает приведенное ниже уравнение. На основании
этого уравнения можно построить график, представленный на рис. 18.4.
72 (1 -7)
71 ”(1-1.1x7)
Рис. 18.4. График, демонстрирующий влияние,
оказываемое при возрастании частоты сбоев на 10%
Как видите, чем ближе величина Р к 1, тем более заметным становится влия¬
ние, которое возрастание частоты сбоев оказывает на устойчивое время выпол¬
нения процесса обработки. Так, возрастание частоты сбоев на 10% может при¬
вести к снижению производительности в девять раз. Именно поэтому так важно
удерживать величину Р как можно меньше 1, чтобы время выполнения процес¬
са обработки оставалось устойчивым при естественных отклонениях кластера
от нормального режима работы. Как следует из графика на рис. 18.4, величина
7меньше 0,7 кажется вполне надежным показателем устойчивости.
Оптимизируя прикладной код, можно контролировать величины О и 7. Кроме
того, можно контролировать величину 7 по объему ресурсов (например, коли¬
честву машин), выделяемых процессу пакетной обработки данных. Идеальной
в этом случае оказывается величина 7, равная 0,5. Если эта величина оказы¬
вается больше 0,5, то в результате увеличения числа машин на 1% задержка
358
Часть III. Уровень ускорения
обработки данных сокращается более чем на 1%, а следовательно, такое реше¬
ние будет экономически эффективным. А если величина Р оказывается меньше
0,5, то в результате увеличения числа машин на 1 % задержка обработки данных
сокращается менее чем на 1%, что поставит под сомнение экономическую эф¬
фективность такого решения.
Чтобы измерить величины О и Рдля процесса обработки данных, можно попы¬
таться выполнить этот процесс при нулевом объеме данных. Это приведет к следу¬
ющему уравнению: Т= 0+ Р* 0, которое нетрудно решить для величины О. Этим
значением можно затем воспользоваться, чтобы решить для величины Р следую¬
щее уравнение: Т= О /(1 - Р). Но такой подход кажется неточным. Например,
задание в системе Hadoop, как правило, состоит из намного большего количества
задач, чем количество сегментов задач в кластере. Для выполнения задания и до¬
стижения полной скорости обработки данных может потребоваться несколько
минут, поскольку для этого потребуется задействовать все сегменты задач, имею¬
щиеся в кластере. Время, которое обычно для этого требуется, имеет постоянную
величину, фиксируемую в переменной О. Если же задание выполняется с неболь¬
шим объемом данных, оно завершится еще до того, как будет задействован весь
кластер, что исказит результат измерения величины О.
Более совершенный способ измерения величин О и Р состоит в том, чтобы
искусственно ввести издержки в процесс обработки данных, например, ввести
вызов метода sleep (1 час) в прикладной код. Как только время выполнения про¬
цесса обработки станет устойчивым, в результате измерения будут получены две
величины, 77 и 7!2, т.е. время выполнения процесса обработки до и после ввода
издержек. И в конечном итоге величины О и Р могут быть получены из приве¬
денных ниже уравнений. Нужно только не забыть удалить искусственно введен¬
ные издержки по окончании измерений!
(Г2-Т1)
Р_ а-1)
(Т2-Г1)
При построении и эксплуатации лямбда-архитектуры приведенными выше
уравнениями можно воспользоваться с целью определить, сколько ресурсов
следует выделить для каждого уровня пакетной обработки данной архитектуры.
В частности, величину Р следует поддерживать меньше 1, чтобы адаптировать
устойчивое время выполнения процесса обработки к возрастанию частоты сбоев
или частоты поступления данных. Если же величина Р окажется меньше 0,5, то
употребление машин в кластере будет экономически неэффективным, и поэтому
им следует найти лучшее применение. А если величина О окажется чрезмерно
большой, это может стать явным признаком случайно возникшего узкого места
в процессе обработки данных.
Теперь у вас должно сложиться ясное представление о построении и эксплу¬
атации пакетных уровней в лямбда-архитектуре. С одной стороны, разработка
уровня пакетной обработки может быть упрощена до поддержки повторных
вычислений. А с другой стороны, на уровне пакетной обработки могут быть
Глава 18. Лямбдаг архитектура в деталях
359
выгодно использованы инкрементные вычисления, возможно, даже в сочета¬
нии с уровнем пакетной обработки, опирающимся на повторные вычисления.
Перейдем далее к рассмотрению уровня ускорения лямбда-архитектуры.
18.3. Уровень ускорения
Уровень пакетной обработки обновляется с большой задержкой, и поэто¬
му данные на этом уровне всегда устаревают хотя бы на несколько часов. Но
представления на уровне обслуживания отображают большую часть имеющих¬
ся данных, кроме тех, что поступили после обновления уровня обслуживания .
Поэтому представлениям в реальном времени остается лишь возместить данные
за последние несколько часов. Для этой цели и служит уровень ускорения.
Именно на уровне ускорения разрешаются вопросы, связанные с производи¬
тельной стороной компромиссов, на которые приходится идти, выбирая алго¬
ритмы инкрементных вычислений вместо алгоритмов повторных вычислений
и изменяемые базы данных чтения-записи вместо тех, что более предпочти¬
тельны на уровне обслуживания. Это приходится делать потому, что требуется
добиться малой задержки, не обращая особого внимания на недостаток устой¬
чивости к отказам, связанным с человеческим фактором. Но поскольку уровень
обслуживания постоянно подменяет собой уровень ускорения, то ошибки, совер¬
шаемые на уровне ускорения, легко исправляются.
В традиционных архитектурах, как правило, имеется лишь один уровень, кото¬
рый приблизительно соответствует уровню ускорения в лямбда-архитектуре. Но
поскольку этот уровень не опирается на уровень пакетной обработки, то он уяз¬
вим к ошибкам, приводящим к искажению данных. Кроме того, трудности эксплуа¬
тации баз данных чтения-записи объемом порядка петабайт оказываются чрезмер¬
ными. Все эти трудности отсутствуют на уровне ускорения лямбда-архитектуры,
поскольку уровни пакетной обработки и обслуживания в значительной степени
смягчают требования к нему. На уровне ускорения должны быть представлены
только самые последние данные, и поэтому его представления можно поддержи¬
вать очень мелкими, избегая упомянутых выше трудностей эксплуатации.
Особенности и варианты построения уровня ускорения были подробно рас¬
смотрены в главах 12-17, включая организацию очередей, синхронных и асин¬
хронных уровней ускорения, а также поочередную и микропакетную обработку
потоков. В этих главах было показано, что при возникновении больших труд¬
ностей можно прибегнуть к аппроксимации на уровне ускорения, чтобы упро¬
стить проектное решение, повысить производительность или добиться и того
и другого.
18.4. Уровень запросов
Последним в лямбда-архитектуре является уровень запросов, ответственный
за выдачу ответов на запросы с помощью пакетных представлений и представ¬
лений в реальном времени. На этом уровне должно быть решено, что именно
следует выбрать из каждого представления и как объединить их вместе, что¬
бы добиться нужного результата. Каждый запрос выражается в виде некото¬
рой функции пакетных представлений и представлений в реальном времени.
360
Часть III. Уровень ускорения
Логика, применяемая для объединения этих представлений в запросах, зависит
от конкретного запроса.
В запросах, ориентированных по времени, применяются простые страте¬
гии объединения, как, например, в запросе на подсчет количества просмотров
страниц во времени в приложении SuperWebAnalytics.com. Чтобы выполнить
такой запрос, нужно сначала получить сумму просмотров страниц вплоть
до того часа, когда на уровне пакетной обработки готовы все данные, а затем
извлечь подсчеты количества просмотров страниц из представлений на уров¬
не ускорения за все остальные часы, указанные в запросе, просуммировав их
с подсчетами из представлений на уровне пакетной обработки. Сходная стра¬
тегия объединения применяется в любом запросе, естественно разделяемом
по времени аналогично данному запросу.
К решению задачи логического вывода о дне рождения человека, рассмотрен¬
ной ранее в этой главе, требуется иной подход. В частности, ее можно решить
следующим образом.
■ Выполнить на уровне пакетной обработки алгоритм обработки перепутан¬
ных дат рождения и выбрать в конечном итоге единственный диапазон
дат. Наряду с диапазоном дат этот алгоритм выдает количество выборок
возраста, по которым вычисляется этот диапазон.
■ Выполнить на уровне ускорения инкрементные вычисления, сузив диапа¬
зон дат по каждой выборке возраста. Если из выборки возраста исключа¬
ются все дни как возможные дни рождения, то она игнорируется. Такая
инкрементная стратегия быстра и проста, но она не вполне справляется
с перепутанными датами. Впрочем, ее достаточно, поскольку обработка пе¬
репутанных дат выполняется на уровне пакетной обработки. Кроме того,
на уровне ускорения сохраняется количество выборок возраста, по кото¬
рым вычисляется диапазон дат.
■ Извлечь диапазоны дат из уровней пакетной обработки и ускорения вме¬
сте с соответствующими подсчетами выборок, чтобы ответить на запро¬
сы. Если два диапазона дат объединяются вместе без исключения всех воз¬
можных дней, то в конечном итоге получается наименьший из возможных
диапазонов дат. В противном случае выбирается диапазон дат с большим
количеством выборок возраста.
Такая стратегия логического вывода о дне рождения человека позволяет со¬
хранять представления простыми и учесть все подходящие случаи. Люди, незна¬
комые с такой системой, будут соответственно обслужены алгоритмом инкре¬
ментных вычислений, применяемым на уровне ускорения, где смешанные дан¬
ные не обрабатываются таким же образом, как и на уровне пакетной обработки,
но и этого оказывается достаточно, если на уровне пакетной обработки может
быть произведен более сложный анализ. Эта стратегия позволяет также обраба¬
тывать внезапно появляющиеся порции новых данных. Так, если неожиданно
ввести в систему целый ряд выборок возраста, то будут использованы результа¬
ты, полученные на уровне ускорения, а не па уровне пакетной обработки, по¬
скольку они опираются на более свежие данные. И разумеется, диапазоны дат
рождения всегда вычисляются повторно на уровне пакетной обработки, чтобы
Глава 18. Лямбда-архитектура в дегмыях
361
получить со временем более точные результаты. Безусловно, вы можете выбрать
свой вариант такой реализации логического вывода о дне рождения человека,
но общий ее замысел должен быть вам понятен.
Из приведенных выше примеров должно быть ясно, что структура представ¬
лений должна подразумевать их объединение. Это вполне естественно для об¬
работки таких ориентированных по времени запросов, как подсчет количества
просмотров страниц во времени, но в примере логического вывода о дне рожде¬
ния человека подсчеты выборок возраста специально вводятся в представления,
чтобы помочь объединению. Выбор структуры представлений для их объедине¬
ния является одним из тех проектных решений, которые приходится принимать
при реализации лямбда-архитектуры.
Резюме
Лямбда-архитектура является результатом перехода от простых принципов
(общей постановки задач обработки данных в виде функций всех просмотрен¬
ных данных) к составлению обязательных требований вроде устойчивости
к ошибкам, связанным с человеческим фактором, горизонтальной масштабируе¬
мости, операций чтения и обновления с малой задержкой. Исследуя лямбда-ар¬
хитектуру, мы пользовались многими инструментальными средствами, в том чис¬
ле Hadoop, JCascalog, Kafka, Cassandra и Storm, чтобы представить практические
примеры реализации Основополагающих принципов. Очевидно, что ни одно из
этих инструментальных средств не является составной частью лямбда-архитек¬
туры. Эти инструментальные средства будут изменяться и развиваться со вре¬
менем, но основные принципы организации лямбда-архитектуры всегда будут
оставаться прежними.
Во многих отношениях лямбда-архитектура выходит за рамки имеющихся
в настоящее время инструментальных средств. И хотя реализовать ее можно
уже теперь, что мы и попытались продемонстрировать в данной книге, подроб¬
но рассмотрев особенности построения различных уровней этой архитектуры,
конечно, сделать это можно было бы еще проще. Имеется совсем немного баз
данных, предназначенных для применения на уровне обслуживания, и было бы
совсем неплохо, если бы для уровня ускорения имелись базы данных, способ¬
ные легко справляться с истечением срока действия отдельных частей пред¬
ставления, которые больше не нужны. Правда, создать такие инструментальные
средства намного проще, чем обширный ряд традиционных баз чтения-записи,
и поэтому мы вправе ожидать, что эти пробелы будут восполнены по мере роста
числа людей, взявших лямбда-архитектуру на вооружение. Между тем традицион¬
ные базы данных, возможно, придется переориентировать на выполнение раз¬
личных ролей в лямбда-архитектуре, выполнив самостоятельно определенную
работу, чтобы приспособить их к лямбда-архитектуре.
Впервые сталкиваясь в проблемами больших данных и экосистемой инстру¬
ментальных средств для больших данных, можно легко смутиться и впасть
в отчаяние. И вполне понятно желание обратиться к уже знакомой области ре¬
ляционных баз данных, к которым мы так привыкли за последние несколько
десятилетий. Надеемся, что, изучив лямбда-архитектуру, вы уяснили, что стро¬
ить системы больших данных намного проще, чем информационные системы,
362
Часть III. Уровень ускорения
основанные на традиционных архитектурах. В лямбда-архитектуре полностью
решена проблема выбора между нормализацией и денормализацией, которой так
страдают традиционные архитектуры, а кроме того, ей присуща устойчивость
к ошибкам, связанным с человеческим фактором, т.е. она удовлетворяет такому
требованию к надежности системы, которым нельзя пренебрегать. Помимо это¬
го, в лямбда-архитектуре удается избежать многих сложностей, вносимых архи¬
тектурами, основанными на монолитных базах данных чтения-записи. А посколь¬
ку лямбда-архитектура основывается на функциях всех данных, то она по своему
характеру является универсальной, давая полную уверенность в разрешении лю¬
бых задач обработки данных.
Предметный
указатель
А
Агрегаторы
AnalyzeVisits, применение, 197
Average, реализация, 155
ConstructHyperLogLog, применение, 196
Count, реализация, 155
Limit, применение, 174
MergeHyperLogLog, применение, 196
Sum, реализация, 155
буферы, применение, 168
вторичная сортировка кортежей, 176
объединяющие, применение, 147
обычные, применение, 168
параллельные, применение, 169
связывание в цепочку, 169
типы, 162
.Алгоритмы
HyperLogLog, применение, 185
инкрементных вычислений
особенности, 125
отказоустойчивость, 127
производительность, 126
исправления прочитанного,
сложность, 265
нормализации, итеративные,
реализация, 207
обхода графа
достижение неподвижной точки, 190
итеративные, 190
повторных вычислений
особенности, 125
отказоустойчивость, 127
производительность, 126
сравнение и выбор, 128
универсальность, 128
Архитектуры
полностью инкрементные
в сравнении с лямбда-архитектурой,
39; 234
как пример знакомой сложности, 35
недостатки, 35; 38
традиционные, особенности, 35
уровня ускорения
для асинхронных обновлений, 266
для синхронных обновлений, 265
Б
Базы данных
Cassandra
информационная модель, 271
назначение, 271
применение, 273
расширенные возможности, 275
составляющие, обозначение, 272
составные столбцы, назначение, 276
ElephantDB
назначение, 244
обслуживание представлений, 245
применение, 246
создание представлений, 244
схема фрагментации, 244
на уровне обслуживания
назначение, 44
преимущества, 351
на уровне ускорения, особенности, 46
размытые кворумы, метод, 263
типа NoSQL
масштабируемость, 31
не панацея, 31
определение, 26
требования,259
364
Предметный указатель
традиционные
искажение данных, 30
отказоустойчивость, затруднения, 29
трудности эксплуатации, 30
Бесконфликтно реплицируемые типы
данных, разновидности, 264
Библиотеки
JCascalog
агрегирование, 162
'ведерные' отводы, реализация, 202
группировка, 162
информационная модель, 156
композиция, методы, 171
назначение, 156
отводы, абстракция, 153
предикаты, разновидности, 158
специальные предикатные
операции, 166
структура запросов, 157
РаП
вертикальное разделение,
особенности, 107
назначение, 103
основные операции, 104
пакетные операции, выполнение, 106
предназначение 'ведер', 103
преимущества, 109
сериализация и десериализация
объектов в 'ведре', 105
создание 'ведер', 104
форматы файлов, 109
Большие данные
методы организации, польза, 30
свойства систем, 33
современные технологии, категории, 49
В
Выборка
на стадии записи, способ, 239
с хешированием, способ, 240
Г
Главный массив данных
компоненты, 56
хранение данных
в распределенной файловой системе,
особенности, 91
в хранилище пар 'ключ-значение',
особенности, 88
требования,87
Граф-схемы
определение, 73
основные элементы, 73
д
Данные
вечная истинность, 65
неизменяемость, 63
необработанность, 59
определение, 32; 66
отличие от информации, 32
свойства, 59
уплотнение, операция, 36
Денормализация данных
определение, 228
проблема выбора в сравнении с
нормализацией, 229
Дробление
определение, 202
реализация, 202
3
Запросы
на анализ показателя отказов от
просмотра страниц
индексирование представлений, 234; 251
инкрементная пакетная обработка на
уровне ускорения,324
микропакетная обработка на уровне
ускорения, 319; 334
пакетные представления, вычисление,
196; 217
предварительная обработка, 186
на выявление количества индивидуальных
посетителей во времени
индексирование представлений, 232; 250
пакетные представления, вычисление,
196; 215
полностью инкрементное решение
задачи,234
поочередная обработка на уровне
ускорения, 291; 293
предварительная обработка, 185
на заключение о гендерной
принадлеж! юсти
конвейерная схема, 148
назначение и обработка, 121
реализация, 135
на определение показателя влияния
конвейерная схема, 148
Предметный указатель
365
назначение и обработка, 121
реализация, 135
на подсчет количества просмотров
страниц во времени
индексирование представлений, 232; 248
конвейерная схема, 147
микропакетная обработка на уровне
ускорения, 318; 331
назначение и обработка, 120
пакетные представления, вычисление,
195; 212
предварительная обработка, 184
предварительные вычисления, 124
реализация, 135
И
Информационная зависимость
между данными, представлениями и
запросами, 58
основные понятия, 58
Информационные системы
назначение, 32
окончательная точность, свойство, 48
определение, 32
основное уравнение, 32
принципы организации, основные, 31
свойства, 33; 35
сложности реализации, преодоление, 34
согласованность
окончательная, 37; 261
свойство, 37
Исполняемые схемы
изменение, правила, 82
назначение, 74
построение средствами каркаса
реализации, 78
развитие, 81
реализация, 75
К
Каркас MapReduce
задания
компиляция, 146
назначение, 134
масштабируемость, 132
назначение, 131
низкоуровневый характер абстракции,
причины, 137
особенности, 133
отказоустойчивость, 134
парадигма распределенных
вычислений, 131
перетасовка данных, 133
универсальность, 136
функции
предварительной обработки,
назначение, 131
Каркасы сериализации
Apache Thrift
назначение, 78
применение, 79
составляющие, назначение, 78
назначение, 78
ограничения
особенности, 83
преодоление, 83
применение, 78
Конвейерные схемы
назначение, 140
операции
слияния, 144
соединения, 141
преимущества, 140
преобразование в задания MapReduce, 145
применение, 140
принципы построения, 140
расширение, 316
Л
Лямбда-архитектура
в сравнении с полностью инкрементными
архитектурами, 39; 234; 241
главный массив данных, назначение, 56
изоляция сложности, свойство, 47
как универсальный подход, 32
масштабируемость по горизонтали, 33
обобщение приложений, 34
отлаживаемость, достижение, 35
преодоление сложностей реализации, 34
разделение на уровни, 41
с несколькими пакетными уровнями,
организация и преимущества, 353
уровень запросов
назначение, 359
объединение представлений,
стратегии, 360
уровень обслуживания
базы данных, назначение, 44
366
Предметный указатель
выбор между нормализацией или
денормализацией данных, 228
индексирование представлений,
способы, 227
количественные показатели, 225
назначение, 223
обеспечение свойств системы, 44
уровень пакетной обработки
измерение и оптимизация
использования ресурсов, 354
инкрементная обработка,
реализация, 346
инкрементный, реализация, 351
назначение, 42
обеспечение свойств системы, 44
предварительные вычисления,
организация, 122
частичные повторные вычисления,
принцип и преимущества, 347
уровень ускорения
достижение окончательной
точности, 260
инкрементные вычисления,
особенности выполнения, 255
назначение, 255
обновление, 45
особенности, 46; 257
требования к базе данных, 229
М
Масштабирование
в традиционных базах данных
путем фрагментации, 28
с помощью очередей, 28
горизонтальное и вертикальное,
особенности, 48
Масштабируемость
линейная и нелинейная, отличия, 129
определение, 33; 129
Модели
Storm
группировка
перетасованное, назначение, 286
полей, назначение, 287
потоков, назначение, 286
задачи, назначение, 286
затворы, назначение, 285
идемпотентные операции,
выполнение, 290
назначение, 285
ОАГ кортежей, построение
и отслеживание, 289
построение топологии, 287
потоки кортежей, назначение, 285
стоки, назначение, 285
топология, назначение, 284
транзакционная семантика,
свойство, 331
изменяемости и неизменяемости данных,
применение, 63
информационных систем,
особенности, 345
обработки потоков
Бюпп, особенности, 284
достоинства и недостатки, 281
очередей и рабочих процессов,
особенности, 282
очередей и рабочих процессов
недостатки, 283
структура, 282
фактологические
атомарность фактов, 67
назначение, 69
преимущества, 69
распознаваемость фактов, 67
факты как основные единицы, 66
Н
Нагрузка, определение, 129
Назначение книги, 26
Нормализация
данных
в сравнении с денормализацией, 70
определение, 70
проблема выбора в сравнении с
денормализацией, 228
семантическая
алгоритм, 61
определение, 61
О
Обработка потоков
без очередей, недостатки, 278
микропакетная
назначение, 311
основные понятия, 315
производительность, 315
с сохранением состояния в памяти,
способы, 340
Предметный указатель
367
модели, достоинства и недостатки, 281
оконных, особенности, 321
определение, 281
поочередная
достоинства и недостатки, 282
модели, разновидности, 282; 284
на более высоком уровне, 284
производительность, 315
строго упорядоченная, особенности, 310
Очереди
многопользовательские
преимущества, 281
структура, 280
однопользовательские
недостатки, 280
структура, 279
промежуточные, организация, 289
п
Подзапросы
выполнение по требование, 171
объединение, 171
создание подзапросов в динамическом
режиме, 172
Получение хеш-значения по модулю,
метод, 244
Предикатные макрокоманды
назначение, 175
применение, 176
создание в динамическом режиме, 177
Представления
в реальном времени
вычисление, 257
обновление, 46; 265
сложности, 260
хранение, 258
на уровне обслуживания, индексирование,
225; 248
пакетные
анализ показателя отказов от просмотра
страниц,186
вычисление, 194; 212
количество индивидуальных
посетителей во времени, 185
метод, описание, 41
назначение, 41
просмотры страниц во времени, 183
Приложение SuperWebAnalytics.com
граф-схема
ребра, определение, 79
свойства, определение, 80
составление, 75
узлы, определение, 79
как пример построения систем больших
данных, 50
поддерживаемые числовые показатели, 51
типы обрабатываемых запросов, 182
уровень обслуживания
индексирование представлений, 231
построение по запросам, 248
разработка, 231
уровень пакетной обработки
ввод данных, 188; 201
нормализация URL и идентификаторов
пользователей, 188; 194; 205; 206
подготовка процесса обработки
данных, 201
построение, 182
процесс обработки данных, 186
удаление дубликатов событий просмотра
страниц, 194; 212
уровень ускорения
для микропакетной обработки,
построение, 318; 331
для поочередной обработки,
построение, 291
реализация топологии для поочередной
обработки,304
фактологическая модель, реализация, 76
хранение главного массивов
распределенной файловой системе
HDFS средствами Pail, 110
граф-схема, 96
Р
Разделение
горизонтальное, определение, 29
Распределенные файловые системы
HDFS
применение, 100
принцип действия, 89
вертикальное разделение,
осуществление, 93
назначение, 89
низкоуровневый характер, 94
свойства, 91
С
Системы
Apache Kafka, применение, 331
Предметный указатель
366
Apache Storni
гарантирование обработки
сообщений, 302
демон Supervisor, назначение, 301
интерфейс Trident API,
применение, 328; 331
механизм Zookeeper, назначение, 301
назначение, 297
построение топологий, 297
привязка и подтверждение
кортежей, 302
развертывание топологии в
кластерах, 300
тактовые кортежи, назначение, 307
Hadoop
назначение, 31
настройка, 100
развертывание, 89
Spark
особенности, 136
режим работы Spark Streaming,
назначение, 341
больших данных
неизменяемая схема, преимущества, 63
развитие технологий, современные
тенденции, 48
свойства, 32; 35
вычислений, виды, 341
масштабируемые линейно и нелинейно,
отличия, 129
Сложности кода
абстракции, неудачное составление, 155
второстепенные, проявление, 154
принципиальные, проявление, 154
специальные языки программирования,
применение, 154
Соединения
внешние, назначение, 160
внутренние, назначение, 137
стороны внутренние и внешние, 141
Стоки
назначение, 285
непрозрачные, назначение, 316
транзакционные, назначение, 315
Структура книга, организация, 27
Т
Теорема САР
достоверность, 261
правильная интерпретация, 261
формулировка, 261
Ф
Фильтр Блума
применение, 349
структура данных, 348
Фрагментация базы данных,
определение, 29
Э
Эквиваленты, назначение, 39
Эластичные облака, инфраструктура как
служба, 49
Большие данные
Натан Марк • Джеймс Уоррен
В крупномасштабных веб-приложениях, которые поддер¬
живают работу социальных сетей, выполняют аналитику
в реальном времени или поддерживают электронную тор¬
говлю, приходится обрабатывать большие массивы данных,
объем и скорость обмена которыми превышают возможности
информационных систем, основанных на традиционных ба¬
зах данных. Для подобных приложений требуются архитек¬
туры, в основе которых лежат кластеры машин для хранения
и обработки данных любого объема и с любой скоростью.
Правда, масштабируемость и простота не являются взаимои¬
сключающими свойствами подобных архитектур.
Эта книга поможет читателю научиться строить системы боль¬
ших данных, используя архитектуру, специально предназ¬
наченную для фиксации и анализа данных в масштабе веб.
В ней представлена простая для понимания и масштабируе¬
мая лямбда-архитектура, позволяющая разрабатывать инфор¬
мационные системы усилиями небольших команд. В книге
даются теоретические основы организации систем больших
данных и поясняется, каким образом они воплощаются на
практике. Помимо общей инфраструктуры для обработки
больших данных, читатель может ознакомиться с конкретны¬
ми технологическими и инструментальными средствами вро¬
де Hadoop, Storm и баз данных типа NoSQL.
В 9II)DU KHUZC рассматриваются следующие темы.
• Введение в системы больших данных.
• Описание особенностей обработки данных масштаба веб
в реальном времени.
• Применение инструментальных средств вроде Hadoop,
Cassandra и Storm.
Возможность расширить свои знания и навыки за пределы
традиционных баз данных.
Для чтения этой книги не требуется предварительное зна¬
комство с особенностями анализа крупномасштабных дан¬
ных или баз данных типа NoSQL, хотя полезно знать о тради¬
ционных базах данных.
Ы авторах
Натан Марц — создатель системы Apache Storm и инициа¬
тор применения лямбда-архитектуры для построения систем
больших данных.
Джеймс Уоррен — архитектор-аналитик с квалификацией
в области машинного обучения и научных расчетов.
Издательский дом "Вильямс":
http://www.williamspublishing.com
“Эта книга выходит за рамки
отдельных инструментальных
средств или платформ.
Обязательна к прочтению всем,
кто работает системами больших
данных”.
Джонатан Эстерхази,
компания Groupon
“Эта книга — подробный,
снабженный примерами экскурс
в лямбда-архитектуру под
руководством ее изобретателя ”.
Марк Фишеру компания Pivotal
'Книга содержит мудрость,
которую можно приобрести
только после выполнения многих
проектов с большими данными.
Обязательна для чтения ”.
Педро Феррера Бертрану
компания Datasalt
“Это фактическое руководство
по рационализации обработки
конвейера данных пакетами и
почти в реальном времени”.
Алекс Холмсу
автор книги Hadoop in Practice