Владислав Светлаков
АРХИТЕКТУРА
БЭКЕНДА
API для надежных
корпоративнь1х
приложении
Санкт-Петербург
« БХВ-Петербург»
2026
УДК
ББК
004.41
32.973.26-018.2
С24
Свет лаков В. И.
С24
Архитектура бэкенда. APl для надежных корпоративных приложений. -
СПб.: БХВ-Петербург, 2026. -
256 с.: ил.
ISBN 978-5-9775-2102-4
Книга посвящена полному циклу бэкенд-разработки, начиная от сбора требова­
ний и заканчивая мониторингом приложения. Основные темы книги касаются
масштабирования
приложений,
балансировки
нагрузки,
создания
и доработки
REST API, подбора инструментов для анализа данных, технологии SQL и NoSQL,
обеспечения безопасности приложений, архитектурных стилей, применяемых при
проектировании бэкенда. Также рассмотрены вопросы тестирования и разв!!ртыва­
ния приложений, обмен данными в синхронном и асинхронном режиме, разница
между связанностью и связностью. Без жесткой привязки к конкретным техно­
логиям раскрыты основные особенности и сложности, возникающие в процессе
создания новых сервисов, а также даны рекомендации по улучшению имеющихся
сервисов и микросервисов. Структура книги подразумевает сквозное
изучение
материала, раскрывает принципы построения бэкенд-решений в рамках реальных
проектов, где требуется одновременно учитывать технологическую составляющую,
меняющиеся бизнес-требования и человеческий фактор.
Для разработчиков и архитекторов сложных программных систем
УДК 004.41
ББК 32.973.26-018.2
Группа ПОДГОТОВКИ издания:
Руководитель проекта
Олег Сивче11ко
Зав.редакцией
Людмила Гауль
Компьютерная верстка
Ольги Сергие11ко
Дизайн обложки
Зои Канторович
Подписано в печать 01.08.25.
Формат 70х100 1 /,е. Печать офсетная. Усл. печ. л. 20,64.
Тираж 1200 экз. Заказ № 15179.
"БХВ-Петербург'', 191036. Санкт-Петербург, Гончарная ул., 20.
Отпечатано с готового оригинал-макета
ООО "Принт-М", 142300, М.О., г. Чехов, ул. Полиграфистов. д. 1
ISBN 978-5-9775-2102-4
© ООО "БХВ", 2026
©Оформление.ООО "БХВ-Петербург", 2026
Оглавление
https://t.me/it_boooks/2
Отзывы .................................................................................................................................... 7
Предисловие .......................................................................................................................... 10
Как пользоваться этой книгой ............................................................................................................. 11
Благодарности ....................................................................................................................................... 11
ЧАСТЬ 1. ПРОЕКТИРОВАНИЕ И РАЗРАБОТКА СИСТЕМЫ ................................ 13
Глава 1. Что такое бэкенд-разработка ............................................................................. 15
Роль и цель бэкенд-разработки ............................................................................................................ 15
О построении абстракций .................................................................................................................... 18
О технологиях ....................................................................................................................................... 20
Языки программирования и фреймворки ................................................................................... 20
Базы данных .................................................................................................................................. 22
Безопасность ................................................................................................................................. 23
Интеграции .................................................................................................................................... 23
Заключение ............................................................................................................................................ 24
Глава 2. Сбор требований к системе .................................................................................................................. 25
Понятие бизнес-требований и бизнес-логики ..................................................................................... 25
Роль бизнес-аналитика .......................................................................................................................... 27
От идеи к бэклогу .................................................................................................................................. 29
Предметно-ориентированное проектирование (DDD) ....................................................................... 30
Заключение ............................................................................................................................................ 33
Глава 3. Семантика синхронных взаимодействий. REST ........................................... 34
О передаче данных и проблеме интерфейсов ..................................................................................... 34
REST over НТТР .................................................................................................................................... 35
НТТР .............................................................................................................................................. 35
НТТР-методы ....................................................................................................................... 36
URL ....................................................................................................................................... 37
REST .............................................................................................................................................. 38
НТТР-коды ответов ...................................................................................................................... 40
2хх ......................................................................................................................................... 41
3хх ......................................................................................................................................... 41
4хх ......................................................................................................................................... 41
5хх ......................................................................................................................................... 42
4
Оглавление
OpenAPI ......................................................................................................................................... 43
Про другие патrерны .................................................................................................................... 44
gRPC .............................................................................................................................................. 45
GraphQL ......................................................................................................................................... 45
Заключение ............................................................................................................................................ 46
Глава 4. Масштабирование приложений ........................................................................ 48
Вычислительные ресурсы .................................................................................................................... 48
CPU ................................................................................................................................................ 49
RAM ............................................................................................................................................... 50
Сеть ................................................................................................................................................ 51
Хранилище .................................................................................................................................... 51
Виды масштабирования ........................................................................................................................ 53
Вертикальное масштабирование ................................................................................................. 53
Горизонтальное масштабирование .............................................................................................. 54
Триггеры масштабирования ................................................................................................................. 55
Инструменты ......................................................................................................................................... 57
Виртуальные машины .................................................................................................................. 57
Контейнеризация .......................................................................................................................... 59
О бессерверных функциях ........................................................................................................... 62
Заключение ............................................................................................................................................ 65
Глава 5. Асинхронные взаимодействия .......................................................................... 66
Ограничения синхронных взаимодействий ........................................................................................ 66
Асинхронность как патrерн коммуникации ....................................................................................... 68
Очереди сообщений ...................................................................................................................... 69
Примеры применения очередей, сервис отправки уведомлений .............................................. 72
Очереди сообщений и бессерверные функции ........................................................................... 73
Брокеры сообщений ...................................................................................................................... 73
Примеры использования брокеров сообщений .......................................................................... 77
Веб-хуки ........................................................................................................................................ 78
Опрос ............................................................................................................................................. 80
Веб-сокеты .................................................................................................................................... 82
Заключение ............................................................................................................................................ 83
Глава 6. Кеширование ........................................................................................................ 85
Смысл и основные виды кеширования ............................................................................................... 85
Клиентский кеш ............................................................................................................................ 86
Серверный кеш ............................................................................................................................. 87
Кеширование на уровне баз данных ............................................................................................ 90
Инвалидация кеша ................................................................................................................................ 91
TTL ................................................................................................................................................. 92
Инвалидация по событию ............................................................................................................ 92
Глобальное обновление кеш а ...................................................................................................... 94
Управление ............................................................................................................................................ 95
О базах данных для кеша ............................................................................................................. 95
CDN ................................................................................................................................................ 96
Заключение ............................................................................................................................................ 98
Глава 7. Операционные данные приложения ................................................................ 99
Что такое операционные данные ......................................................................................................... 99
Реляционные данные. SQL ................................................................................................................. 1О 1
Первичные и внешние ключи .................................................................................................... 102
Оглавление
5
Чтение данных и агрегации ................................................................................................................ 106
Индексы ....................................................................................................................................... 107
Нормализация данных и объединения типа JO\N .................................................................... 109
AClD и транзакции ..................................................................................................................... 112
Распределенные базы данных и САР-теорема ......................................................................... 116
Consistency + А vailaЬility .................................................................................................. 117
AvailaЬility + Partition tolerance ........................................................................................ 118
Consistency + Partition tolerance ........................................................................................ 118
ОRМ-фреймворки ....................................................................................................................... 119
Когда применять реляционные БД ............................................................................................ 120
NoSQL .................................................................................................................................................. 120
Базы данных «ключ-значение» .................................................................................................. 122
Документные базы данных ........................................................................................................ 124
Колоночные базы данных .......................................................................................................... 126
Графовые базы данных ............................................................................................................... 127
Базы данных для временных рядов ........................................................................................... 128
Заключение .......................................................................................................................................... 130
Глава 8. Сложная логика приложений .......................................................................... 132
Откуда берется сложная логика ......................................................................................................... 132
Бизнес-требования ...................................................................................................................... 132
Операционные данные ............................................................................................................... 134
Интеграционная логика .............................................................................................................. 135
Сложные части. Связанность и связность ........................................................................................ 137
Архитектурные стили ......................................................................................................................... 141
Монолитные приложения .......................................................................................................... 142
Сервис-ориентированная архитектура ...................................................................................... 145
Микросервисы ............................................................................................................................. 148
Почему микросервисы не были популярны раньше ....................................................... 155
Важность документации ..................................................................................................................... 158
Заключение .......................................................................................................................................... 159
Глава 9. Безопасность ....................................................................................................... 161
Безопасное хранение данных и управление секретами .................................................................... 161
Управление секретами ................................................................................................................ 161
Хранение и передача данных ..................................................................................................... 164
Обзор подходов аутентификации ...................................................................................................... 166
АРl-ключи ................................................................................................................................... 166
JWТ-токены ................................................................................................................................. 167
OAuth2 ......................................................................................................................................... 170
\АМ в рамках облака, RBAC и АВАС ...................................................................................... 172
Основные векторы атак на приложения ............................................................................................ 174
SQL-инъекции ............................................................................................................................. 174
XSS ............................................................................................................................................... 175
CSRF ............................................................................................................................................ 176
DDоS-атаки ................................................................................................................................. 177
Заключение .......................................................................................................................................... 179
ЧАСТЬ 11. СИСТЕМНАЯ АРХИТЕКТУРА ................................................................. 181
Глава 10. Этап эксплуатации и подготовка к ней ...................................................... 183
О техническом долге .......................................................................................................................... 183
Про архитектуру и планирование ...................................................................................................... 186
б
Оглавление
Что нужно проверить перед релизом ................................................................................................ 188
Функции ....................................................................................................................................... 189
Производительность и масштабируемость ............................................................................... 189
Наблюдаем ость системы ............................................................................................................ 190
Безопасность ............................................................................................................................... 190
Заключение .......................................................................................................................................... 191
Глава 11. Проверка изменений ....................................................................................... 193
Тестирование ....................................................................................................................................... 193
Модульные тесты ........................................................................................................................ 194
Сервисные тесты ......................................................................................................................... 195
Интеграционное тестирование .................................................................................................. 197
Нагрузочное тестирование ......................................................................................................... 198
Ручное тестирование .................................................................................................................. 199
Автоматические тесты ............................................................................................................... 200
Конвейеры интеграции ....................................................................................................................... 202
Сфера применения ...................................................................................................................... 202
Организация инфраструктуры для CI ........................................................................................ 203
Хранилища артефактов .............................................................................................................. 205
Заключение .......................................................................................................................................... 206
Глава 12. Доставка изменений ........................................................................................ 208
Основные понятия .............................................................................................................................. 208
Непрерывная доставка и развертывание ................................................................................... 209
Разделение сред ........................................................................................................................... 21 О
Автоматизация через GitOps. IaC .............................................................................................. 211
Модели развертывания ....................................................................................................................... 212
Сине-зеленое развертывание ..................................................................................................... 212
Канареечные релизы ................................................................................................................... 214
А/В-тестирование ....................................................................................................................... 215
Теневое развертывание ............................................................................................................... 216
Автоматизация и оркестрация ........................................................................................................... 217
Заключение .......................................................................................................................................... 220
Глава 13. Мониторинг работы системы ........................................................................ 221
Основные понятия и цель ................................................................................................................... 221
Логи .............................................................................................................................................. 222
Метрики ....................................................................................................................................... 226
Распределенная трассировка ...................................................................................................... 232
Соединяем все вместе ................................................................................................................. 23 7
SLA и SLO ........................................................................................................................................... 239
Анализ инцидентов ............................................................................................................................. 242
Определение возможных причин .............................................................................................. 243
Когда что-то случилось .............................................................................................................. 244
Анализ инцидента и шаги к предотвращению ......................................................................... 245
Заключение .......................................................................................................................................... 24 7
Итоги .................................................................................................................................... 249
О важности конечной цели и шагах к ней ........................................................................................ 249
О технологиях ..................................................................................................................................... 251
О развитии ........................................................................................................................................... 253
Предметный указатель ..................................................................................................... 255
Отзывы
Данная книга -
это исчерпывающее руководство, которое охватывает все аспекты
проектирования, разработки и эксплуатации современных веб-сервисов. Автор
предлагает читателям не просто набор инструментов или рецептов, а целостное
видение архитектуры бэкенда, начиная от сбора требований и заканчивая монито­
рингом и анализом инцидентов в продакшене.
Книга разделена на логические части, каждая из которых посвящена конкретному
аспекту разработки веб-сервисов. Начиная с фундаментальных концепций (напри­
мер, роли бэкенда и бизнес-логики), автор постепенно переходит к более сложным
темам, таким как масштабирование, асинхронные взаимодействия, кеширование,
безопасность и системная архитектура.
Также впечатляет и глубина проработки каждой темы. Например, глава о REST не
ограничивается базовыми принципами НТТР и OpenAPI, но также включает обзор
альтернативных подходов, таких как gRPC и GraphQL. Это позволяет читателю не
только понять, как работают эти технологии, но и выбрать наиболее подходящий
инструмент для конкретной задачи.
Глава о масштабировании заслуживает отдельного внимания. Здесь автор подробно
рассматривает как вертикальное, так и горизонтальное масштабирование, объясняя
их преимущества и недостатки. Особый акцент сделан на современных инструмен­
тах, таких как контейнеризация (Docker) и бессерверные функции, что делает мате­
риал актуальным для разработчиков, работающих с облачными технологиями.
Тема асинхронных взаимодействий раскрыта с практической точки зрения. Автор
не только объясняет теорию очередей сообщений и брокеров, но и приводит кон­
кретные примеры их использования, такие как сервис отправки уведомлений. Это
помогает читателю лучше понять, как применять эти паттерны в реальных проек­
тах.
Раздел о сложной логике приложений особенно полезен для тех, кто работает
с микросервисной архитектурой. Автор подробно объясняет, почему микросервисы
стали популярными только в последние годы и какие вызовы они приносят с собой.
Отдельный акцент сделан на важности документации -
работы с микросервисами.
часто упускаемой стороне
Отзывы
в
Книга отличается выраженной практической направленностью. В каждой главе ав­
тор не только объясняет теорию, но и приводит примеры из реальной жизни, раз­
бирает типичные ошибки и предлагает решения. Я нахожу особенно полезными
главы о тестировании, доставке изменений и мониторинге. Эти темы часто остают­
ся «за кадром» в других книгах, но здесь они раскрыты максимально подробно.
Например, в главе о тестировании автор рассматривает не только модульные и ин­
теграционные тесты, но и более сложные аспекты, такие как нагрузочное тестиро­
вание и автоматизация через CI/CD. Раздел о моделях развертывания (сине-зеленое,
канареечное, А/В-тестирование), безусловно, будет полезен командам, которые
стремятся минимизировать риски при выпуске новых версий.
Несмотря на техническую сложность материала, книга написана живым и доступ­
ным языком. Автор успешно балансирует между глубиной анализа и понятностью
изложения, что делает ее подходящей как для начинающих разработчиков, так и
для опытных специалистов. Особенно приятно, что каждая глава завершается лако­
ничным заключением, которое помогает систематизировать информацию. Также
одним из главных преимуществ книги является ее универсальность. Независимо от
того, работаете ли вы с Python, Java, .NET или другими технологиями, принципы,
описанные в книге, применимы ко всем платформам. Это делает ее ценным ресур­
сом для широкого круга специалистов.
«Архитектура бэкенда:
API для надежных корпоративных приложений» -
это не
просто книга, а настоящая энциклопедия современной бэкенд-разработки. Она ох­
ватывает все ключевые аспекты создания веб-сервисов: от проектирования и разра­
ботки до эксплуатации и анализа инцидентов.
Антон Чеплюков,
Global Practice Lead, Head of Intelligent Solutions at Coherent Solutions
Как инженер-разработчик с десятилетним опытом создания программного обеспе­
чения, лидер команд и предприниматель с опытом запуска бизнеса в разных сфе­
рах, я прекрасно понимаю, насколько тесна связь между бизнесом и IТ-инфра­
структурой. Именно поэтому книга, о которой идет речь, сразу нашла во мне от­
клик.
Автор отлично раскрывает мысль, что бэкенд-разработка -
это не просто написа­
ние кода, а искусство видеть целостную картину бизнеса и грамотно воплощать его
задачи в цифровом виде. Главную идею книги очень точно отражает цитата самого
автора: «Из умения "видеть лес за деревьями" нередко складывается успех разра­
ботки. И эта книга об этом».
С позиции своего опыта в разработке программных продуктов для различных биз­
несов могу с уверенностью сказать, что именно умение видеть общую картину и
понимать бизнес-процессы определяет, насколько успешно IТ-решения будут рабо­
тать на практике.
Автор подробно разбирает важность правильного сбора бизнес-требований, выбора
адекватной архитектуры и создания качественных абстракций, которые позволяют
9
Отзывы
легко развивать и масштабировать системы. На практике я не раз убеждался, что
технические сложности зачастую возникают именно из-за непонимания или недос­
таточного внимания к бизнес-задачам на старте проекта.
Кроме того, автор подчеркивает отсутствие универсальных технологических реше­
ний и необходимость осознанно подходить к выбору инструментов и методов ис­
ходя из конкретных задач и условий бизнеса. Эта идея особенно важна для меня
как человека, который реализовал множество проектов с разным технологическим
стеком -
от облачных решений до мобильных приложений.
Отдельного внимания заслуживает акцент на безопасности, мониторинге и асин­
хронных взаимодействиях, которые в моем опыте играют важнейшую роль в обес­
печении стабильной и эффективной работы продуктов на любом рынке.
Эту книгу стоит прочитать всем, кто стремится строить прочную связь между биз­
несом и
IT, понимая, что ключ к успеху разработки -
это способность видеть
целое за множеством деталей и грамотно соединять технологии и бизнес-процессы.
Антон Терешко,
Staff Software Engineer
Предисловие
Сложно переоценить глубину интеграции различных бэкенд-сервисов в нашу
жизнь. Сегодня практически каждый без проблем может отправить сообщение на
другой конец света, хранить свои важные файлы в облаке, листать фотоальбомы
друзей. Миллионы действий, которые совершаются миллионами пользователей
ежесекундно. Но как это работает?
Нам, как разработчикам, открыта особая точка зрения на мир -
часто мы видим
дальше графического интерфейса. Для нас все не заканчивается после кнопки дей­
ствия, для нас это только начало. И настоящее искусство -
это уметь прятать этот
невероятно разносторонний мир, чтобы обеспечить наилучший пользовательский
опыт. И имя этому искусству- абстракция.
Смысл бэкенд-разработки кроется не только в моделировании бизнес-процессов
с помощью программного кода, ее также отличает та самая абстракция, которая
позволяет скрыть внутреннее устройство системы, представив ее функционал в ви­
де удобных для пользователя интерфейсов. Когда мы обращаемся к API провайдера
карт, чтобы составить маршрут из точки А в точку Б, мы не задумываемся о том,
как именно эти данные представлены в конечной системе. Это, казалось бы, оче­
видное свойство является ключом к проектированию и поддержке сложных про­
граммных систем.
Для того чтобы это стало возможным, было создано огромное количество инстру­
ментов: большой выбор языков программирования и фреймворков для них, базы
данных разного назначения, облачные сервисы, способы развертывания, брокеры,
очереди и т. д. Все это лишь малая часть того, что существует, чтобы решать задачи
в условиях постоянно изменяющегося программного ландшафта. Из умения «ви­
деть лес за деревьями» нередко складывается успех разработки. И эта книга об
этом.
На первых этапах самое сложное -
не выучить язык, не понять фреймворк, не изу­
чить принципы работы с базой данных либо брокером сообщений. С практикой
к разработчику приходит осознание, как именно нужно комбинировать сервисы и
их функционал, как те или иные подходы обеспечивают уже известный пользова­
тельский опыт. С каждым реализованным сервисом, казалось бы, несвязанные ме-
Предисловие
11
жду собой технологии начинают выстраивать баланс, отражающий в себе бизнес­
функции, простоту и удобство разработки, масштабируемость системы. Вспоминая
свой путь и собственные шаги, я хочу сделать так, чтобы шаги читателя бьmи наи­
более эффективны и продуктивны.
В данной книге мы затронем путь создания программных систем, которыми мы все
пользуемся каждый день. Мы начнем с разбора бизнес-требований, что является
важной отправной точкой. Далее мы изучим основные подходы и инструменты,
которые позволяют эффективно работать с потоками бизнес-данных, рассмотрим
важность таких архитектурных свойств, как масштабируемость, отказоустойчи­
вость, расширяемость, модульность. Разберем основные архитектурные паттерны,
которые повсеместно встречаются во всех системах бэкенд-приложений. Затронем
тему безопасности: как правильно защищать данные и быть готовыми к потенци­
альным атакам. А также мы поговорим о том, как происходит поддержка и разви­
тие подобных систем.
Как пользоваться этой книгой
Когда я писал эту книгу, то старался избегать большого количества ссылок между
главами для того, чтобы ее можно было читать выборочно, изучая только то, что
интересно читателю. Но, на мой взгляд, лучшим способом прочтения будет после­
довательное изучение, поскольку примерно в таком порядке рассматриваемые темы
поднимаются в рамках реальных проектов и задач. При этом с каждой разобранной
темой ваше представление об остальных также будет углубляться.
Благодарности
В первую очередь я хочу поблагодарить Олега Сивченко за то, что поверил в воз­
можность написания мной подобной книги. Это именно тот человек, который не
просто подтолкнул на создание, но и окружил меня всеми нужными ресурсами для
написания, был терпелив и мудр. Также я безмерно благодарен литературному
редактору Владимиру Красовскому за терпение во время приведения рукописи
в литературный вид.
Я хочу поблагодарить свою любимую супругу Александру (Мурку) за ее бесконеч­
ную поддержку и заботу. Я очень благодарен ей за то, что она позволила мне уйти
в себя на достаточное для написания рукописи время, а также за то, что она являет­
ся неотъемлемой частью моей жизни, позволяя взглянуть на вещи под другим
углом. Хотя эта книга и имеет техническую направленность и в ней сравнительно
немного философских рассуждений, многие идеи, читаемые между строк, могли
появиться в моем подсознании именно благодаря тебе, обретая свою форму во вре­
мя прохождения через твое тепло и ощущение мира.
Также я хочу поблагодарить своих друзей Андрея и Настю за то, что являются
надежной опорой в моей жизни. Спасибо вам за то, что не отказывались сидеть
с моей собакой Йориком, за гавайскую пиццу, наши проекты, за наши бесконечные
Предисловие
12
посиделки с игрой в шахматы, гольф и разговоры о главном. Также должен отме­
тить своих студентов Сергея Кезика и Тимура Кабулова, благодаря которым я смог
углубить
свои
знания,
передавая
их
вам.
Если
читатель
ищет умелых
МL­
разработчиков, которые могут сделать невозможное, то вы знаете, где их искать.
Большое влияние на меня оказали мои коллеги из ISSoft и Coherent Solutions. Каж­
дый человек; которого я встречал на моем профессиональном пути, учил меня
чему-то новому, так или иначе делал меня лучше. Я хочу поблагодарить Илью Пас­
тушкова за интересные архитектурные задачи, что мы решали вместе,
а также за
традиционный утренний офисный кофе. Хочу поблагодарить Катю Минюкович за
абсолютно уникальный взгляд на то, как можно быть разработчиком, за бесконеч­
ное терпение и чуткость в отношении всех вещей, которые она делает. Благодарю
Антона Чеплюкова за то, что всегда делился своим исключительным опытом и за
грамотное руководство, благодаря чему у каждого под его крылом есть возмож­
ность развивать свои таланты, а также спасибо за написание отзыва. Хочу поблаго­
дарить Анну Клименок за вдохновение и интересные мысли в решении проблем
разработки. А также хочу поблагодарить Андрея Скригаловского за интересные
мысли, занятия и разговоры, Ивана Крупского за наши классные совместные про­
екты и бесценный полученный опыт.
Хочу поблагодарить Антона Терешко за то, что мы познакомились на одном из мо­
их митапов. Благодаря твоему глубокому опыту, а также тому, как жизнь и разра­
ботка могут переплетаться воедино, я понимаю, что мы живем в уникальное время.
Говорят, что талантливый человек талантлив во всем, поэтому важно развивать
себя всесторонне, чтобы быть успешным в определенных вещах.
И я обязательно должен отметить своих родителей, брата и дядю, которые все эти
годы были рядом и поддерживали мои начинания. Семья и те, кто вас окружает, это очень важно, они во многом определяют вас, вдохновляют и вкладывают в вас
то, из чего вы будете составлять ваше «Я».
ЧАСТЬ
1
Проектирование и разработка
системы
Глава 1.
Что такое бэкенд-разработка
Глава 2.
Сбор требований к системе
Глава 3.
Семантика синхронных взаимодействий. REST
Глава 4.
Масштабирование приложений
Глава 5.
Асинхронные взаимодействия
Глава 6.
Кеширование
Глава 7.
Операционные данные приложения
Глава 8.
Сложная логика приложений
Глава 9.
Безопасность
1111111
1111111
ГЛАВА 1
111!!
Что такое бэкенд-разработка
https://t.me/it_boooks/2
У любой сферы деятельности есть определенная цель и предмет, которые глубоко
изучаются в рамках этой сферы. Мир современного 1Т достаточно разнообразный:
хотя исторически эта область связана с техническими подходами, с математикой и
информатикой, это не мешает мирно уживаться под одной крышей специалистам
не только технических, но и гуманитарных специальностей. Каждый из них вносит
свой неповторимый вклад в общее дело, развивая те или иные подходы к решению
задач.
Однако что именно делает бэкенд-разработка? В чем заключается суть работы
бэкенд-инженера, какова его роль на проекте? В рамках этой небольшой главы мы
попробуем описать основные особенности работы, отличия от остальных направ­
лений, а также сделаем краткий очерк того, с чем обычно сталкивается бэкенд­
разработчик, а подробнее разберем это в последующих главах.
Роль и цель бэкенд-разработки
Как уже говорилось ранее, существует множество областей, которые хорошо под­
даются
цифровизации.
Множеству бизнесов
выгодно выполнять
свои бизнес­
процессы точнее, быстрее и эффективнее. Возможность масштабирования при этом
позволяет привлекать новых клиентов, предоставляя более удобные интерфейсы
для работы с тем или иным бизнесом. В случае если эти инструменты удобнее, чем
у конкурентов, это может быть явным преимуществом для организации, поскольку
пользователи при прочих равных будут предпочитать те решения, которые работа­
ют быстрее и предоставляют более богатые функции.
Построение таких сервисов -
это результат работы слаженной команды, каждый
из членов которой может быть профессионалом в своем домене. Кто-то способен
грамотно собирать бизнес-требования, чтобы четко понимать, что именно нужно
разработать, кто-то имеет дизайнерские навыки и может построить не только
красивый, но и функциональный интерфейс, который будет максимально удобен
в работе. Другие специалисты могут обеспечить надежность функционирования
работы платформы, кто-то может запрограммировать работу пользовательских ин-
Часть /. Проектирование и разработка системы
16
терфейсов. И нередко, чтобы
полностью оживить этот механизм, нам нужен
бэкенд-разработчик.
Скорость работы того или иного сервиса обусловлена тем, что логика действий
в нем заранее запрограммирована. Те или иные бизнес-сценарии, которые важны
для владельцев бизнеса, должны быть аккуратно положены на программный код,
в достаточной степени протестированы и введены в эксплуатацию. Помимо самих
сценариев, приложение должно обрабатывать большие объемы информации, кото­
рые фактически отражают в себе состояние тех или иных бизнес-процессов. От
того, насколько грамотно реализована система учета запасов и как именно хранится
и обрабатывается информация о количестве денег в банковской системе, напрямую
зависит успех бизнес-процесса, который затрагивает реальность. Ошибки в таких
областях могут стоить очень дорого, поэтому важно, чтобы все работало именно
так, как задумывалось. Поэтому крайне важно выделить это в отдельную область,
для которой можно будет иметь собственные подходы и правила, позволяющие мо­
делировать те или иные процессы корректно.
Суть бэкенд-разработки во многом связана именно с моделированием той или иной
бизнес-логики, но с применением программного кода, чтобы обеспечить скорость
обработки, надежность и повторяемость результатов. Да, множество процессов
могут быть выполнены людьми, например люди могут вести журналы, считать ко­
личество товаров вручную, нотифицировать других людей по электронной почте.
Однако у такого подхода есть несколько ограничений. Во-первых, он недостаточно
масштабируемый, поскольку мы не можем заставить бесконечное количество лю­
дей работать над какими-то простыми, механическими задачами, например считать
миллионы штук товара на складе. Во-вторых, человеческий фактор: если мы пору­
чим какой-либо монотонный труд человеку, значительно возрастает вероятность
ошибки. И, как мы помним, для некоторых доменов подобные ошибки могут сто­
ить действительно дорого. Реализация таких процессов в рамках кода позволяет не
только освободить людские ресурсы, перенаправив их в другие области, которые не
поддаются автоматизации, но и, как правило, увеличить эффективность самого
процесса, сделать его более наблюдаемым и прозрачным, поскольку такая система
не будет ошибаться сама собой, если иное не заложено в ее программном коде.
Поскольку сама бизнес-логика может быть очень сложной, а ее кодовая реализация
может затрагивать
множество
различных технологий
и
подходов,
бэкенд-раз­
работка выделяется в отдельное направление в рамках проекта. При этом сами
команды могут работать над имплементациями различных сервисов параллельно,
чтобы ускорить сам процесс и строить более устойчивые системы.
В первую очередь бэкенд-часть проекта позволяет организовать работу с операци­
онными данными. Например, если нам необходимо собрать определенную инфор­
мацию от пользователей системы либо сделать запрос в сторонние системы, и если
сами данные имеют операционную природу, это будет делать именно бэкенд­
команда. При этом сами потоки данных могут быть двунаправленными, т. е. как
попадать
в систему, так и отправляться
из нее как пользователям, так и
в другие
системы. Именно данные нередко являются ключом к успешному управлению биз­
несом, поэтому их автоматизированный оборот нередко увеличивает эффектив-
Глава 1. Что такое бэкенд-разработка
17
ность. Когда данные в вашей системе отражают реальный мир, например состояние
складов, расписания врачей, занятость ресторанов, это является серьезным пре­
имуществом в бизнес-планировании. И в небольших масштабах именно бэкенд­
разработчик будет отвечать за хранение данных для бизнес-процессов. Стоит также
отметить роль инженеров больших данных, однако они в своей работе делают упор
именно
на аналитику,
а
не
на операционную
часть,
и это точно так же является
важной составляющей цифровизации. Про управление операционными данными
у нас будет отдельная глава.
Вместе с данными бэкенд-разработчик отвечает за имплементацию бизнес-логики.
Разработчику необходимо правильно смоделировать бизнес-процесс с помощью
абстракций кода. Причем самое сложное -
менте, самое сложное -
это не всегда разработать что-то в мо­
это предусмотреть дизайн самого кода таким образом,
чтобы его можно было легко расширить, даже в случае изменения изначальных
бизнес-требований и видения заказчика. Одной отличительной особенностью яв­
ляются альтернативные пути. Когда тот или иной бизнес-процесс выполняет чело­
век, за счет того, что он способен самостоятельно мыслить и выполнять действия, а
также за счет определенного опыта в случае нештатных ситуаций он может без
проблем разобраться и нередко успешно завершить тот или иной сценарий. Напри­
мер, сотрудник занимается ручной обработкой заказов в интернет-магазине. После
поступления нового заказа сотрудник видит, что запрошенная позиция отсутствует
в нужном цвете либо не соответствует по другим признакам. Опытный сотрудник
может связаться с клиентом и попробовать договориться о том, чтобы отправить
другую позицию либо подождать новой доставки. Вариабельность действий позво­
ляет избежать ситуаций, когда сам бизнес-процесс остановился и не может про­
должиться. В случае с бэкенд-приложениями все обстоит иначе.
Есть такая английская фраза happy path is a\ways easy, которая дословно переводит­
ся как «счастливый путь всегда прост». Ее можно трактовать по-разному, но в на­
шем контексте она означает, что обычный способ прохождения того или иного
бизнес-процесса всегда прост. Если мы разберем наш пример с интернет-магази­
ном, автоматизация не выглядит сложной: мы должны хранить информацию о ко­
личестве товаров, интегрировать платежный сервис и настроить доставку. Однако
отличие от людей заключается в том, что если что-то пойдет не так, мы не можем
рассчитывать на то, что оно разрешится само собой. Например, если пользователь
захочет выполнить оплату, но не сможет из-за проблем с платежным сервисом, то
сотрудник прекрасно знает, что нужно делать -
попросить попробовать через не­
которое время либо предложить другой способ оплаты. Однако если никакая логи­
ка на такой случай не прописана на стороне бэкенд-приложения, оно не сможет
«разобраться по ходу» и, скорее всего, упадет с ошибкой. Причем с пользователь­
ской стороны это может выглядеть абсолютно по-разному, вплоть до того, что сама
система будет показывать успешность оплаты, хотя по факту она не прошла.
На мой взгляд, в рамках разработки важно предусмотреть не столько позитивный
сценарий, сколько негативные. Условно, если сказать человеку, что вы хотите зака­
зать минус 1О товаров, то человек поймет, что такое невозможно, и откажет в этом.
Однако программная система может попробовать обработать такой запрос, что
Часть /. Проектирование и разработка системы
18
в лучшем случае приведет к ошибке, а в худшем -
к нарушению целостности опе­
рационных данных. Потому что, если работа системы максимально наивна, она
может попробовать отнять отрицательное число от количества товаров, что по ито­
гу по правилам математики увеличит их количество. И вариантов, где все может
пойти не так, действительно много. Разработчик должен предусмотреть валидации
самих данных, альтернативные сценарии в случае, если что-то идет не так, а также
обеспечить достаточную наблюдаемость системы для того, чтобы было удобнее
исправлять те случаи, где что-то пошло не так. И, что любопытно, такие ситуации
неизбежны -
в любой системе рано или поздно что-то ломается, поскольку их
сложность может быть впечатляющей.
При этом помимо ошибок, связанных с самими бизнес-процессами, могут легко
появляться сугубо программные ошибки. Если мы строим распределенную систе­
му, то важно понимать, что делать в тех ситуациях, когда мы не смогли успешно
запросить те или иные данные. Здесь также успешно применяется парадигма, что
программная система не сможет разрешить все сама собой, решение должно быть
запрограммировано разработчиком. Эта проблема становится более выраженной
в нагруженных системах, где для управления большим количеством запросов могут
применяться разнообразные паттерны и сервисы. Также важно иметь представле­
ние о том, как именно должен работать наш сервис в инфраструктурном плане.
Поэтому, если резюмировать, суть бэкенд-разработки заключается в переносе биз­
нес-процессов в программный код с обеспечением их эффективного выполнения,
альтернативных путей, а также в управлении операционными данными платформы
для того, чтобы предоставлять информацию, логику и результаты в надежной
форме.
О построении абстракций
Залог грамотного управления сложной логикой заключается в умении построения
абстракций. Формально все паттерны разработки, паттерны построения архитекту­
ры -
это общие и понятные для всех заранее выработанные абстракции, которые
позволяют декомпозировать сложность задачи и представить ее в виде небольших
и понятных кусочков, скрывающих сложность в себе.
На самом первом уровне разработчики встречаются с абстракциями кода. Его ди­
зайн, который заключается в правильном разбиении на модули, использовании
функций языка, повышении абстрактности кода, является решающим в расширении
приложения, а также в читаемости и понятности логики. Да, нередко для построе­
ния системы можно полностью не задумываться об этом, реализовав все так, как
быстрее и удобнее на данном этапе, однако со временем поддержка таких проектов
становится все более сложной и затратной.
Поскольку бэкенд-приложение будет взаимодействовать с внешним миром, тут
также необходима определенная абстракция фейсов для пользователей
-
API. Построение удобных интер­
далеко не простая задача, поскольку сама бизнес­
задача может привносить в нее доменную сложность. Для этого бэкенд-разра­
ботчик должен умело выбирать не только между технологиями, чтобы определить,
19
Глава 1. Что такое бэкенд-разработка
какая именно из них позволит решить эту задачу с наибольшей эффективностью,
но и уметь работать с абстракцией чтобы построить правильный интерфейс, кото­
рый бы соответствовал не только корпоративным, но и общепринятым лучшим
практикам. От грамотного построенного API могут зависеть все последующие ин­
теграции, поэтому этот вопрос может быть одним из основных во время разработки
системы.
Причем сами
API могут иметь абсолютно разные паттерны взаимодействия. Это
могут быть синхронные конечные точки (англ. endpoints), которые возвращают ре­
зультат сразу, либо интерфейсом общения с сервисом может быть брокер сообще­
ний или очередь. Нередко сервис может связываться с другими самостоятельно,
используя механику веб-хуков. Комбинирование разных подходов позволяет гибко
подстроиться под задачу, перенеся ее природу на паттерн коммуникации. Помимо
этого, каждый из подходов привносит свои преимущества и недостатки на инже­
нерном уровне,
например внося ограничения на те или иные сценарии и порядки
общения, по-разному влияя на скорость и нагрузку на стороны.
В общем плане умелое построение интерфейсов -
это залог успешной разработки
сервиса. Если бизнес-логика выведена в реальный мир через удобные абстракции,
о построении которых мы также поговорим в рамках этой книги, то это значи­
тельно упрощает интеграционную часть, например фронтенд-разработчик сможет
быстрее разработать интерфейс к нашему приложению, потому что ему будет
удобно получать данные для него. Если наше приложение ориентировано на других
разработчиков, это также будет поводом использовать наш сервис, поскольку, если
он имеет удобный API, а также в принципе выполняет что-то полезное, его инте­
грация будет и приятной, и полезной.
При этом за подобными абстракциями могут скрываться невероятно сложные про­
граммные системы. Например, системы поиска, API карт, аналитические системы
могут агрегировать огромные объемы информации, иметь сложные системы разде­
ления прав доступа, состоять из десятков сервисов под капотом. Однако для других
разработчиков будет существовать удобный интерфейс, который позволяет скрыть
эту реализацию, поскольку для логики самой бизнес-задачи она является несущест­
венной.
Такой же задачи нужно придерживаться в том числе и для разработки внутренних
сервисов. Особенно это актуально для микросервисных архитектур, где интеграции
между сервисами могут происходить повсеместно и соблюдение принятых правил
позволяет организовать грамотный и надежный обмен данными.
Неправильно построенные абстракции могут привести сразу к множеству проблем.
Во-первых, они могут банально замедлять разработку. Когда другим разработчикам
необходимо разбираться со сложной логикой, которая оказалось сложной просто
потому, что так получилось из-за неверного планирования, это негативно влияет на
качество сервисов. Помимо скорости, это банально может приводить к ошибкам,
которые может быть довольно трудно отловить.
Вторая важная часть -
трудности в расширении. Когда проект успешен, он неиз­
бежно будет расширяться, в нем будут появляться все новые функции. Неправиль-
20
Часть /. Проектирование и разработка системы
но построенные, ошибочно усложненные абстракции не способствуют эффектив­
ному расширению, нередко для этого их необходимо полностью перерабатывать и
переписывать. В свою очередь неправильные внешние абстракции также могут
свидетельствовать о том, что внутри сервиса код также упорядочен недостаточно,
что приводит к схожим сложностям и проблемам.
Поэтому крайне важно сохранять системность в этом вопросе, использовать луч­
шие практики и то, что является логичным в рамках текущей бизнес-задачи. Не
существует одного идеального подхода, соблюдение которого приведет к построе­
нию правильных интерфейсов. Бэкенд-разработка -
это постоянный поиск опти­
мальных подходов и компромиссов, диктуемых технологиями и лучшими практи­
ками, которые мы должны применить в рамках контекста, который до этого, скорее
всего, даже не существовал.
О технологиях
Как и в любом IТ-домене, бэкенд-разработка имеет свой собственный набор техно­
логий, подходов и примеров, которые являются общепринятыми для решения
определенных задач. Специфика IT такова, что многие из этих технологий универ­
сальны и могут быть применены к разным областям, причем нередко это приводит
как к хорошим последствиям, так и к плохим. Поэтому сейчас мы проведем крат­
кий экскурс в то, что именно нужно знать для того, чтобы уверенно чувствовать
себя во время построения бэкенд-систем.
Языки программирования и фреймворки
Уже было сказано, что одной из задач бэкенд-разработки является моделирование
определенной бизнес-логики и манипуляции с ней с помощью программного кода.
Это означает, что существует целый класс языков, которые могут быть удобны для
этих задач. Формально практически любой современный язык способен выполнять
и принимать сетевые запросы, делая это с помощью библиотек, и на любом из них
можно построить достаточно неплохое бэкенд-приложение. Однако тренды и тех­
нологии постоянно меняются, и те языки, которые были популярны
1О лет назад,
для некоторых проектов могут уходить на второй план, заменяясь новыми, более
удобными для быстрой разработки.
Формально все языки и их технологии можно разделить на несколько групп.
Первая группа это взрослые технологии, которые используются последние 2030 и более лет. Они продолжают использоваться и сегодня ввиду того, что сущест­
вует множество приложений, которые были написаны ранее, и их необходимо как­
то поддерживать. Например, это могут быть проекты, написанные на РНР, Java, С#.
Многие из таких технологий, особенно если они построены поверх компилируемых
языков, обеспечивают высокую скорость работы сервисов. Взамен они могут быть
сложными для новичков, хотя и иметь достаточно большое количество поклонни­
ков и ресурсов для обучения. Такие технологии выбираются для построения слож­
ных и комплексных систем, поскольку ограничения на уровне языка, а также опре-
Глава 1. Что такое бэкенд-разработка
21
деленные абстракции кода, которые могут увеличить порог входа для новичков,
в комплексном проекте обеспечивают чистоту и порядок, не позволяя выходить за
рамки задуманной логики. Естественно, так бывает не всегда, нередко такие инст­
рументы могут действовать обратным образом, превращая кодовую базу проекта
в нечитаемый ком грязи, однако это можно назвать следствием других вещей, не
связанных с разработкой.
Вторая группа -
новые языки, которые вошли в бэкенд-обиход относительно не­
давно. К ним можно отнести Python и интерпретатор NodeJS для JavaScript. Про­
блемой технологий, описанных в предыдущих абзацах, можно назвать сложность
освоения и написания кода. Когда разработчику нужно прописывать дополнитель­
ные абстракции, которые выполняют сугубо инженерную функцию, он тратит вре­
мя, которое можно было бы потратить на имплементацию бизнес-логики. Посколь­
ку в современном мире существует тренд на ускорение процессов и более быструю
разработку, это является серьезным фактором при выборе технологии. Такие тех­
нологии, как
Python и NodeJS, имеют серьезные бэкенд-библиотеки, на которых
было построено множество сложных и современных проектов. Благодаря более
простому API эти языки легче в освоении новичками, а также сами разработчики
могут разрабатывать на них быстрее, поскольку от них не требуется реализовывать
множество побочных методов -
они уже реализованы под капотом выбранных
технологий. Однако взамен они могут иметь определенные недостатки. Например,
они могут быть менее производительны в сравнении с компилируемыми языками,
иметь большую задержку на исполнение, быть более требовательными к ресурсам.
В проектах, где нам критически важна минимальная задержка, необходимо это
учитывать. Однако если максимально возможная скорость не является нашей це­
лью, а нам необходимо быстро и надежно имплементировать бизнес-логику, то по­
добные технологии могут быть отличным выбором. Стоит также сказать, что суще­
ствуют и другие новые компилируемые языки, например Go и Rust, которые также
можно применять для бэкенда, и они отличаются высокой скоростью и надежно­
стью, хотя и более сложны в разработке.
В общем плане язык должен иметь достаточное количество библиотек для того,
чтобы не реализовывать все с нуля самостоятельно. Например, он должен иметь
удобные бэкенд-библиотеки для работы с НТТР либо gRPC, иметь расширения для
брокеров
сообщений
и
очередей.
Немаловажным
является
поддержка
ОRМ­
фреймворков, которые позволяют с помощью программного кода работать с базами
данных. Хорошая поддержка, а также грамотная документация и сообщество по­
зволят вам эффективнее решать технические сложности, которые неизбежно по­
явятся во время процесса разработки. В зависимости от специфики необходимо
оценить его производительность и требовательность к ресурсам. Не всегда нужно
стремиться к самой высокой скорости исполнения, если ради нее мы пожертвуем
большим временем, которое может потребоваться для самой разработки и тестиро­
вания. Необходимо понимать сценарии, в которых это уместно, поскольку главной
целью является решение бизнес-задачи, а не построение самого быстрого на свете
решения. Если бизнес-задача такое не подразумевает, есть смысл посмотреть в сто­
рону удобства.
Часть /. Проектирование и разработка системы
22
Также при выборе языка необходимо понимать компетенции команды. Например,
если большая часть команды уже реализовывала проекты с применением опреде­
ленной технологии, имеет смысл выбрать именно ее по той причине, что на выходе
получится более качественный сервис, и, вероятнее всего, он будет разработан
быстрее, чем если команда будет работать с новой для нее технологией. А если вы
еще только ищете, какой язык выбрать и какую технологию выучить, помимо акту­
альности, важно прислушиваться к своему ощущению -
что вам нравится. Как
было сказано ранее, решить бизнес-задачу в состоянии в принципе любой язык,
вопрос заключается лишь в удобстве, надежности и времени.
Базы данных
Правильное хранение и обработка данных является важной задачей бэкенд-раз­
работки. Состояние тех или иных бизнес-процессов отражается именно в рамках
данных, поэтому необходимы определенные технологии, которые будут обеспечи­
вать удобные абстракции для работы с данными.
Классическим выбором для работы с операционными данными являются реляци­
онные базы данных. Существует множество различных провайдеров и версий, каж­
дые из которых могут быть оптимизированы под определенные варианты исполь­
зования. Одни могут иметь нужные для работы типы данных, например для хране­
ния
идентификаторов,
геопространственной информации.
Другие могут иметь
большое количество удобных интеграций, систем управления пользователями и
процедурами. Благодаря гибкой модели SQL возможно моделирование множества
бизнес-контекстов, а также возможны множественные модели чтения поверх фак­
тически одной модели записи. Понимание функций и ограничений той или иной
имплементации реляционной БД является важным для успеха проекта, поскольку
к моменту, когда те или иные ограничения могут проявиться, может пройти много
времени, а наша система уже будет содержать в себе большое количество данных,
миграция которых будет проблематична.
Другой, более современный подход связан с хранением данных в нереляционном
формате, в так называемых NoSQL-бaзax данных. Оригинальная SQL-модель мо­
жет быть достаточно удобной для множества ситуаций, однако она имеет свои ог­
раничения,
например недостаточную производительность, сложность моделирова­
ния определенных данных. Поэтому существует множество альтернативных спосо­
бов хранить информацию, с помощью пар «ключ-значение», графов, документов,
коллекций, временных рядов. Благодаря умелому использованию NоSQL-решений
можно обходить ограничения реляционных моделей, добиваться лучшей пропуск­
ной способности и моделировать данные, сохраняя их природу.
Совокупное представление обо всех этих решениях позволяет понимать, какое
именно решение для хранения данных подойдет лучше всего. Не существует одно­
го идеального решения, которое будет полностью лишено недостатков, поэтому
если вам кажется, что оно их не имеет, высок шанс того, что они пока просто не
проявились. При выборе решения важно понимать основные узкие места бизнес­
задачи. Например, нужно понимать, что важнее в той или иной ситуации: согласо­
ванность данных либо пропускная способность и гибкость структуры. Поскольку
Глава 1. Что таков бэквнд-разработка
23
именно операционные данные могут напрямую влиять на бизнес-логику, в них не
должно быть места случайным ошибкам, которые появились из-за неправильного
применения и понимания технологии хранения. Про управление операционными
данными также пойдет речь в этой книге.
Безопасность
Поскольку приложение содержит данные о реальном мире, например состояния
складов, персональные данные, информацию о денежных транзакциях, на плечи
бэкенд-инженера нередко ложится задача обеспечения безопасности приложения.
Безопасность при этом может быть разных уровней. Например, внутри самой раз­
рабатываемой платформы может существовать логика политик доступа и прав, на­
пример роли администраторов и обычных пользователей. В рамках этой ситуации
бэкенд-разработчик должен правильно разрабатывать саму логику, чтобы исклю­
чить ситуации, при которых возможен неправомерный доступ, например когда
один пользователь получает доступ к информации другого пользователя, если это
не задумано изначально.
Другая сторона безопасности -
общая защита от уязвимостей. Важно проверять
библиотеки и инструменты на предмет уязвимостей, поскольку если они использу­
ются в вашем приложении, то сильно увеличивается шанс их наследования в ваш
проект. Также важно правильно понимать основные слабые места используемых
технологий, например не допускать прямого исполнения пользовательского ввода
на сайтах, потенциальных SQL-инъекций, отсутствия ограничений на ввод пользо­
вателя. Все то, что может сломать приложение из-за особенностей работы техноло­
гий, крайне важно учитывать, поскольку может пострадать не только само прило­
жение, но и деловая репутация бизнеса.
Интеграции
В рамках систем, где существует большое количество других сервисов, интеграции
являются важной деталью, которая позволяет расширить функционал платформы и
добиться лучшего пользовательского опыта. Нередко разные сервисы содержат
в себе разную информацию об одной сущности либо имеют другие функции, кото­
рые нужны в рамках определенных контекстов. Поэтому бэкенд-разработчику важ­
но выбрать механизмы интеграции, чтобы общение между сервисами было наибо­
лее эффективным.
Для этого можно использовать прямые вызовы в семантике REST или gRPC либо
использовать для интеграции общие данные, веб-хуки, брокеры и очереди сообще­
ний. Каждый из этих подходов имеет свои плюсы и минусы. Какие-то из них под­
ходят для быстрого общения, какие-то лучше масштабируются для связи со множе­
ством сервисов. Разработчику также важно понимать, как именно работать с дан­
ными технологиями. Если они подразумевают разворачивание дополнительных
сервисов, важно понимать их настройку, а также лучшие практики работы с ними.
Интеграции могут также приводить к большому количеству ошибок, поскольку мы
не всегда можем гарантировать правильное поведение стороны, у которой запра-
Часть /. Проектирование и разработка системы
24
шиваем или обновляем данные. Поэтому важно продумывать логику поведения
системы в тех случаях, когда все идет не по плану. Опытный бэкенд-разработчик
может сразу понимать такие узкие места, а также итеративно дорабатывать поведе­
ние системы в случае появления новых проблем.
Заключение
Описанное в этой главе -
это далеко не все, что может относиться к разработке
бэкенд-систем. Изучение этих областей может потребовать много времени. Вдоба­
вок
к этому
постоянно
появляются
новые
подходы
и
технологии,
которые
могут
изменять привычный порядок вещей.
В общем плане на бэкенд-разработке строится множество сложных процессов, ко­
торые становятся значительно эффективнее за счет цифровизации. Благодаря этому
миллионы людей могут генерировать и потреблять огромные объемы информации,
решая те задачи, которые раньше были невозможны.
Для того чтобы это работало достаточно стабильно и надежно, требуется глубокое
и комплексное понимание множества процессов. Во-первых, необходимо понимать
саму бизнес-задачу: почему она работает именно так, какие варианты развития
событий в ней возможны и т. д. Во-вторых, необходимо понимать, как именно су­
ществующие технологии могут вписаться в данную бизнес-задачу, как они могут
помочь в моделировании. В рамках этой книги мы убедимся, что не существует
подходов, которые полностью лишены недостатков, поэтому разработчикам необ­
ходимо с умом подходить к тем выборам, которые будут совершаться во время ра­
боты над проектом. И третьей частью являются люди, причем тут важно не только
умение правильно работать с фактическим заказчиком, выстроить с ним правиль­
ную коммуникацию и т. д. Нередко нам нужно будет правильно работать с собст­
венным
коллективом,
правильно
распределять
роли
и
задачи,
контролировать
их
выполнение для того, чтобы разработанная нами система не только соответствова­
ла всем требованиям, была удобна для расширения, но и чтобы во время ее разра­
ботки сами люди не выгорали, получали от процесса удовольствие и развивали
свой профессионализм.
111111
111111
ГЛАВА 2
Сбор требований к системе
•
С чего начинается разработка любой системы? Может быть, сразу с разработки?
Очевидно, что такой подход имеет явные недостатки. Не просто так в рамках про­
ектной деятельности существует понятие бизнес-аналитики, которое начинающим
разработчикам может показаться непонятным и в каких-то случаях даже беспо­
лезным.
Часто технари склонны к ненарочному сужению области своей видимости при раз­
работке. Это далеко не всегда плохо. Например, множество действительно слож­
ных технических вещей было бы невозможно получить без упорного и кропотливо­
го труда, направленного исключительно на одну область. В качестве примера могу
привести различные фундаментальные алгоритмы информатики, такие как сжатие,
различные протоколы передачи данных, алгоритмы нейронных сетей -
это лишь
малая часть тех трудов, закончить которые можно только благодаря высокому
уровню фокусирования на предметной области.
С другой стороны, слишком сильное распыление между областями знаний не по­
зволяет достичь значимых
результатов,
поскольку для
их
достижения
все
также
важна глубина. Но как найти этот баланс? Как охватить все и иметь достаточную
глубину одновременно? Именно тут и появляется необходимость в бизнес-анализе,
который должен предшествовать моменту начала полноценной разработки. О том,
как формализовать бизнес-процессы и подготовить их к переносу на абстракцию
кода, пойдет речь в этой главе.
Понятие бизнес-требований и бизнес-логики
Как говорилось ранее, смысл бэкенд-разработки -
это моделирование бизнес-про­
цессов с возможностью управления данными и получения результатов в удобной и
надежной форме. Это значит, что коммерческая бэкенд-разработка не существует
в вакууме, так или иначе есть некая доменная область, которая моделируется по­
средством кода. И вот тут и встает основной вопрос, связанный с различным
«внешним видом» мира бизнеса и мира разработки: как правильно перенести пра­
вила из одного мира в другой?
Часть /. Проектирование и разработка системы
26
В случае простых систем мы можем попробовать перенести все «в лоб» без лишне­
го планирования. Более того, часто это имеет смысл. Например, если мы строим
систему приема онлайн-заказов для одного ресторана, жизненный цикл заявки не
должен быть слишком сложным. Давайте попробуем его разобрать:
□ у нас есть несколько ролей: клиент ресторана, администратор ресторана и служ­
ба доставки;
□ клиент делает запрос на доставку на сайте, после чего она либо одобряется, либо
отклоняется (к примеру, ввиду загруженности), при этом клиенту отправляется
уведомление;
□ в случае одобрения заказ готовится и передается в службу доставки, после чего
передается клиенту.
Выглядит действительно не слишком сложно, возможно, даже очевидно. Для того
чтобы успешно разработать такую систему, нам достаточно описать это в докумен­
тации,
нарисовать
диаграммы
состояния
и
последовательности,
согласовать
это
с заинтересованной стороной и разработать. Однако давайте добавим пару новых
вводных:
□ у ресторана имеются две опции: доставка и самовывоз;
□ существует множество филиалов. Какой из них должен принять заказ, если
пользователь выбрал доставку?
□ у нас появляется новая роль -
главный администратор, который может смот­
реть за статусами заказов всех подконтрольных филиалов и в случае чего вме­
шиваться;
□ для ресторана важно иметь доступ к аналитике заказов для более эффективного
менеджмента;
□ пользователь может выбрать из следующих способов оплаты: наличными, кар­
той и картой онлайн;
□ система должна иметь функционал промокодов, которые автоматически изменяют стоимость заказа при применении.
И уже на этом этапе все становится гораздо интереснее, притом что подобные биз­
нес-требования могут появляться, уточняться и изменяться с течением времени.
Причем проблема тут не в том, чтобы перенести это все в код:
□ моделируемая область может содержать правила, для понимания которых необ­
ходимо иметь определенные знания. В ресторанном бизнесе есть множество
скрытых деталей (предположим, что это графики закупок продуктов, менедж­
мент персонала, предсказание количества посетителей), которые могут быть
неочевидны при взгляде со стороны разработчика, но являются ключевыми для
успешного ведения бизнеса. То же самое можно сказать про любую другую
область: разработчик без формализованных бизнес-требований может просто
разработать не то, что нужно, и это придется переделывать;
□ отсюда вытекает другая проблема -
внесение изменений. Часто не так сложно
разработать что-то с нуля, как переписать уже существующее. Качество кода
Глава 2. Сбор требований к системе
27
можно оценить по тому, насколько просто его расширить, добавить новые инте­
грации, изменить логику работы существующих функций. Это свойство важно
соблюдать, однако это не значит, что разрабатывать нужно с единственной мыс­
лью, что логика точно будет изменена. При разработке нужно учитывать страте­
гию развития, но если эта стратегия постоянно меняется, то шансы на баланс и
понятность кодовой базы резко падают;
□ частые изменения могут быть свидетельством множества факторов, один из
них -
неправильно сформированные бизнес-требования. Например, на этапе
анализа была упущена какая-то деталь (например, возможность оплаты купона­
ми), ввиду чего в дизайне классов либо сервисов для внесения данного функ­
ционала необходимо переделать структуру. Другая, более частая деталь -
из­
менения видения заинтересованных сторон. Бороться с :пим сложно, но в наших
силах вести свою коммуникацию таким образом, чтобы минимиз11ровать трав­
матичность подобных изменений для нашей системы.
Это все малая часть тех вопросов, которые могут появиться при создании продукта.
Было бы неплохо выделить отдельную роль для того, чтобы разработчик мог со­
средоточиться на написании кода, делегировав работу с бизнес-требованиями. Ско­
рее всего, так и появилась идея бизнес-аналитика.
Роль бизнес-аналитика
Конечно, не всегда возможно иметь для этого выделенного специалиста. Чаще все­
го, когда разработчик запускает свой собственный проект (пет-проект), ему прихо­
дится совмещать множество ролей, например бэкенд- и/или фронтенд-разработ­
чика, DеvОрs-специалиста, проектного менеджера и тестировщика. То же самое
можно сказать и про роль бизнес-аналитика, в небольших командах роль ВА
(business analyst) часто кому-то приходится брать на себя. Однако это ни на грамм
не уменьшает важность.
Бизнес-аналитик выступает связующим звеном между видением заказчика и ко­
мандой разработки. Хороший ВА позволяет разработчикам задумываться только
о своей зоне ответственности и не гадать, насколько сильно все поменяется завтра.
Но в чем секрет? Как именно бизнес-анализ решает проблемы, которые мы пере­
числили ранее?
Во-первых, это коммуникация. Большинство проектов выглядит примерно так: есть
некий бизнес, у которого есть процесс, который необходимо цифровизировать,
либо интегрировать новый процесс, включающий в себя программную часть. Сам
заказчик часто не имеет представления о том, как именно происходит разработка.
Он точно не станет задумываться, в чем отличие тех или иных языков программи­
рования, будут это микросервисы или монолит, как именно будут храниться дан­
ные пользователя, какое разбиение логики должно быть. По-хорошему, он и не
должен об этом думать -
наша задача как разработчиков достичь поставленного
видения без лишнего раскрытия деталей. Однако мы понимаем, что для достижения
поставленной цели саму задачу нужно правильно декомпозировать, представить
Часть /. Проектирование и разработка системы
28
в виде диаграмм, дизайна компонентов, построить нужную абстракцию, в конце
концов разработать. Вот rут на сцену и выходит бизнес-аналитик.
На начальных этапах бизнес-аналитик должен выстроить правильное общение
с заказчиком. Именно поэтому для бизнес-аналитика важно иметь развитые soft-
skills, потому что для человека естественно делиться информацией в рамках обще­
ния. ВА должен правильно интерпретировать видение заказчика, задавать правиль­
ные вопросы о системе, чтобы минимизировать белые пятна на картине сервиса,
потому что заполнение их на этом этапе стоит относительно недорого в сравнении
с их «разукрашиванием» на этапе разработки. Чем яснее будет видение на началь­
ном этапе, включая, например, возможности развития сервиса, тем быстрее и де­
шевле можно будет развивать его. Давайте разберем пример.
Заказчик хочет, чтобы сервис имел функционал купонов. С пользовательской сто­
роны все выглядит просто: достаточно ввести купон, после чего происходит либо
применение скидки, либо добавление нового блюда в заказ. Но с точки зрения
управления этим функционалом назревает огромный пул вопросов, на которые
нужно ответить, причем чем раньше, тем лучше:
□ Как именно купон будет влиять на заказ? Должна ли это быть скидка либо новое
блюдо? Какие есть сиrуации, когда скидка не должна применяться (например,
купон только на первый заказ)?
□ Как будет происходить процесс добавления новых купонов? Насколько сильно
будет изменяться логика их работы (например, применение скидки либо добав­
ление бесплатной позиции из меню)?
□ Должны ли купоны быть многоразовыми? Должны ли они иметь привязку
к пользователю?
□ Какая аналитическая информация была бы полезна в рамках данного функцио­
нала?
□ В каких случаях пользователь в принципе должен получать данный купон?
Должна это быть еmаi\-рассылка либо конкретные действия самого пользова­
теля?
Это лишь малая часть вопросов, которые могут появиться. Ответ на них является
важным при разработке, потому что в рамках кода мы не можем задать правила по
типу «не знаю, как это будет работать, давайте как получится», потому что такие
системы будут очень неустойчивы и, скорее всего, в принципе не будут работать.
А теперь представьте, что на все эти вопросы нужно отвечать разработчику прямо
во время написания кода. Чисто физически невозможно держать в голове всю
доменную область, когда пишешь к ней код, особенно когда ты не знаком с ней
досконально. Шанс реализовать функционал неправильно очень высок, поэтому
ясность бизнес-требований стоит чуть ли не на первом месте. В противном случае
может быть потрачено большое количество времени на разработку того, что не ре­
шает поставленную задачу, после чего созданный сервис придется видоизменять
либо переписывать с нуля.
Интересно то, что сам заказчик, скорее всего, никогда не будет задаваться подобно­
го рода вопросами. Да, у него будет видение функционала, но видение само по себе
Глава 2. Сбор требований к системе
29
не наталкивает на вопросы о том, что нужно выяснить, чтобы это было реализова­
но. Для этого требуется иметь представление о самом процессе разработки, как
именно произойдет декомпозиция, что такое дизайн системы и ее архитектура.
И, как я упоминал ранее, заказчик не должен иметь представления об этом. Наша
задача как специалистов выступить проводником от видения до готового сервиса,
решающего бизнес-задачу.
Для этого важна коммуникация, важно говорить с заказчиком, фиксировать его
требования, документировать встречи. Часто может быть такое, что получилось
договориться об одном, но на следующих встречах появляются новые детали, кото­
рые делают невозможными реализацию предыдущих. Либо новые требования мо­
гут появляться уже в процессе самой разработки, когда что-то поменять становится
проблематично.
Как отмечалось ранее, понимание предметной области является крайне важным.
Нам не нужно знать все так же хорошо, как знает владелец бизнеса, но понимание
основных особенностей упрощает дальнейшую коллаборацию. Я встречал бизнес­
аналитиков, которые, помимо понимания методологии разработки, уверенно себя
чувствовали в «бизнес»-делах клиента, понимали основные формы отчетности, по­
казатели, понимали, почему принимаются те или иные решения и почему заказчик
хочет реализовать тот или иной функционал. Ощущение этих вещей приходит
с опытом и помогает выстроить тот самый мостик, по которому будет проходить
перенос бизнес-процесса в мир кода. Любопытно, что описанные навыки также
приходят к sеniоr-разработчикам со временем, потому что становится важным не
просто хорошо знать фреймворк, язык, технологии, а формализовать их примене­
ние с точки зрения самой задачи, в чем и помогает грамотный бизнес-анализ.
От идеи к бэклогу
Очень трудно ответить на вопрос, как именно нужно собирать бизнес-требования.
Не просто так существует множество методологий, материалов, техник, связанных
с моделированием, реализацией и оптимизацией бизнес-процессов. Не существует
одного решения, подходящего всем. Все бизнесы, от небольших компаний на
l О человек до интерпрайс-гигантов, уникальны, все имеют свой собственный набор
практик, проблем и способов их решения. Поэтому, попадая на новый проект, нуж­
но быть всегда готовым, что сами задачи могут только казаться похожими (напри­
мер, построение
REST API), но в действительности это далеко не так. Но в этом
есть и свои плюсы - скучно почти никогда не будет.
Но если серьезно, это не значит, что нам всегда нужно начинать с чистого листа.
Существуют подходы к моделированию, которые помогают направить коллабо­
рацию в нужное русло. К ним относятся DDD (Domain Driven Design), различные
методологии разработки (Agile, Scrum).
Часть /. Проектирование и разработка системы
30
Предметно-ориентированное проектирование
{DDD)
Предметно-ориентированное проектирование (Domain Driven Design, DDD) являет­
ся одним из основных способов борьбы с неразберихой и путаницей на проектах,
в которых сложность бизнес-логики становится слишком большой. Это очень мощ­
ный инструмент, на понимание которого в деталях может уйти не один год, но мы
попробуем изучить основные подходы, которые сможем применить для увеличения
эффективности прямо сейчас. Стоит также отметить, что каждый может интерпре­
тировать эту практику по-своему. Я встречал множество трактовок данной методо­
логии, поэтому, чтобы получать от нее пользу, не обязательно стараться строго ей
соответствовать -
гораздо продуктивнее брать те части, которые подходят вашим
бизнес-процессам.
Начнем с того, какую проблему призвана решить данная методология. Как говори­
лось в начале этой главы, одной из проблем процесса разработки является слож­
ность конверсии бизнес-процессов на язык кода. В большей степени это связано
с тем, что программа не может позволить себе неясные действия, которые не имеют
четкого плана. Вдобавок к этому операционные и аналитические данные приложе­
ния требуют моделирования, которое невозможно сделать без представления о до­
мене в целом. Возникает это либо из-за отсутствия четкой модели бизнес-процесса,
например когда нужно сделать что-то новое, либо если существующая модель
слишком сложна и в ней много узких мест, через которые нельзя протолкнуть
успешную интеграцию. Также важной составляющей является терминология. Биз­
нес уже может иметь устоявшийся набор названий процессов и терминов, подхо­
дящий той или иной области.
Разработка сложных систем требует разделения и организации кода. Невозможно
описать все сценарии в одном файле, по крайней мере так, чтобы с этим было
удобно работать. Одним из первых способов разделения являлось разделение по
функциональным блокам самой программы. Иными словами, в основу разделения
ложилось объединение компонентов не по бизнес-логике, а по особенностям про­
граммной технологии. Например, мы могли хранить вместе схемы данных для всех
бизнес-процессов, создавать файлы, описывающие логику общения с базой данных.
Такая организация может быть удобна на первых этапах разработки, однако позже
она может являться причиной множества проблем, поскольку такой подход может
допускать импорты и зависимости, которые пересекают сразу несколько бизнес­
процессов.
Предположим, что в нашем проекте появляется все больше задач, которые нужно
автоматизировать с помощью кода. Так или иначе он будет начинать перекрывать­
ся с уже существующим кодом, и может показаться заманчивой идеей перетягивать
часть существующих наработок в качестве импортов в новые модули. Новые моду­
ли могут затрагивать другую предметную область, неизбежно начнутся коллизии
терминов, когда, например, термин «клиент» может иметь абсолютно разные смыс­
лы в зависимости от контекста. Если ничего не предпринять, есть риск нарваться на
архитектурный паттерн под названием «большой шар грязи», когда теряется грань
Глава 2. Сбор требований к системе
31
различных бизнес-процессов. Результат -
внесение изменений становится очень
болезненным, поскольку изменение одних модулей (модулей одного бизнес-про­
цесса) может привести к нарушению работы других. Хорошо, когда кодовая база
покрыта тестами, в противном случае ошибки могут остаться незамеченными,
и найти момент, когда все перестало работать, может быть довольно трудно.
Почему так получилось и как можно с этим бороться? Основная проблема кроется
в том, что на этапе разработки может быть сложно «ощущать» бизнес-контекст
процесса. Для выполнения задачи не всегда требуется полное понимание, как
функционирует вся система в целом, какие в ней есть блоки, помимо тех, что мы
модифицируем. Например, для реализации логики аутентификации не обязательно
знать, как работает сервис информации о складских запасах. Исходя из этого, неиз­
вестны границы и зависимости между бизнес-процессами, потому что в рамках ко­
да они не появляются просто так -
их необходимо соблюдать на этапе разработки.
И для того чтобы корректно разделить части по доменному смыслу, бьm создан
DDD.
Краеугольным камнем данной методологии является понятие «ограниченного кон­
текста» (bounded context). Его суть отражает разделение компонентов не столько по
функциональному принципу, сколько по бизнес-смыслу. Логично, что многие биз­
нес-процессы могли появиться еще до идеи об их цифровизации. Данные процессы
исторически объединялись по своему смыслу и цели. Например, хорошо иметь на­
бор процессов, которые относятся к работе со складскими запасами, либо работе
с отзывами, либо служат для управления доставкой. Это имеет смысл, потому что,
как описывалось ранее, разные предметные области могут по-разному интерпре­
тировать одинаковые термины. Поэтому со временем, когда команды разработки
поняли, что невозможно организовывать сложные бизнес-процессы, не учитывая
контекста, данные идеи стали интегрироваться в программное обеспечение.
Так что же такое ограниченный контекст? Как говорилось ранее, сам термин может
интерпретироваться по-разному, но в общем и целом ограниченный контекст объ­
единяет набор правил, сущностей, процессов, которые относятся к некой одной
бизнес-задаче. Например, мы можем иметь ограниченный контекст для работы
с отзывами клиентов, для обработки уведомлений, выставления счетов. Даже толь­
ко в этих трех примерах термин «пользователь» может означать кардинально раз­
ные вещи. Ограниченные контексты помогают не только разделять процессы меж­
ду собой, но и вносить ясность внутри них. Благодаря тому, что нет необходимости
в постоянном внимании к другим сервисам, чтобы избежать их поломки при изме­
нении, мы можем наращивать свои уникальные и сложные механизмы и правила,
улучшающие конкретно наш процесс. На языке программной архитектуры это так­
же можно назвать высокой связностью процессов, а также низкой связанностью.
Мы вернемся к этим двум терминам позже.
Одним из полезных инструментов DDD является uЬiquitous language. Мы часто го­
ворили про коллизии терминологии, и, как ни странно, для этой проблемы выделя­
ется отдельный термин. К этому моменту сама идея, скорее всего, будет понятна важно вырабатывать вместе с командой бизнес-аналитики и заинтересованной сто­
роной однозначно трактующиеся термины, которые будут отражать те или иные
Часть /. Проектирование и разработка системы
32
части бизнес-процесса. А благодаря тому, что мы существуем в рамках ограничен­
ного контекста, проблемы случайной коллизии минимизированы.
Что именно из программной части входит в ограниченный контекст? Помимо биз­
нес-правил и терминов, стоит также выделить структуры данных. Например, мы
можем оперировать данными пользователя. Для сервиса аутентификации важно
иметь такие данные, как последняя точка аутентификации, как давно пользователь
менял пароль, есть ли резервная почта. Напротив, для сервиса рекомендаций эта
информация не нужна, будет достаточно иметь идентификатор пользователя и его
имя, однако уже без информации о просмотренных страницах, товарах или статьях
нам не обойтись. Чаще всего просачивание бизнес-логики между контекстов про­
исходит именно на уровне контрактов обмена данными. У этого есть даже специ­
альное название -
stamp coupling (связывание по образцу). Самый частый пример
такого поведения -
мы имеем одну структуру данных, которая содержит в себе
сразу всю возможную информацию о сущности. Это может быть удобным в неко­
торых
ситуациях,
но
ассоциированные
риски
при
этом
никуда
не
исчезают.
Во-первых, это дорого с точки зрения передаваемого трафика, во-вторых, это не­
безопасно и, в-третьих, может происходить просачивание бизнес-логики между
ограниченными
контекстами
и,
как следствие,
могут появляться
коллизии терми­
нов, раскрываться детали реализации.
Как именно определять ограниченные контексты? В случае небольших систем час­
то никаких специфических подходов не требуется. Выделить ограниченные кон­
тексты помогут владельцы бизнеса (которые знают, какие именно причины приве­
ли к такому разделению логики в бизнесе), а также команда бизнес-аналитики,
архитекторы, старшие разработчики. Этому очень помогают схемы и диаграммы.
Как упоминалось ранее, коммуникация -
это ключ к успеху разработки. Не реко­
мендуется оставлять большое количество пустых мест с намерением на их исправ­
ление в будущем, -
когда этот вопрос снова появится на этапе реализации, решить
его все равно придется. С другой стороны, не нужно стараться продумать все зара­
нее. Вероятнее всего, вы также не сможете угадать все крайние случаи, и их при­
дется итеративно разрешать. В любом деле важен баланс, поэтому концентрируй­
тесь на общем видении и концепции с возможностью ее итеративной доработки.
Стоит также отметить такую мощную технику, как Event Storming. Подробное опи­
сание данной методики выходит за рамки этой книги, но я попытаюсь описать
основные идеи. Event Storming начинается с представления всех процессов в виде
набора неких событий, которые описываются в прошедшем времени. На этом этапе
не обязательно мыслить последовательно, наоборот, желательно взять максимально
широко и покрыть все возможные события, которые могут происходить в вашей
системе. Группам событий присваиваются команды, которые их вызывают. Далее
происходит выравнивание этих сущностей по временной линии. На основании дан­
ных
последовательностей формируются агрегаты, которые объединяют в себе
близкие по логике и смыслу команды и события. Стоит также отметить добавление
правил, которые определяют, в каких ситуациях что происходит. Агрегаты ложатся
в основу ограниченных контекстов. Стоит отметить, что в таких сессиях может ге­
нерироваться большое количество информации, они могут длиться по несколько
Глава 2. Сбор требований к системе
33
часов, и в них может участвовать много людей. При обсуждении системы критиче­
ски важно иметь не только технических специалистов, но и людей бизнеса (как и
наоборот), чтобы избегать непонимания процессов ввиду незнания каких-либо
деталей реализации.
Заключение
Не просто так про составление требований и бизнес-анализ пишутся огромные от­
дельные книги, где разбирается большое количество примеров и подходов, как
именно решаются эти задачи. Чаще всего бэкенд-разработчик не будет сталкивать­
ся с этим напрямую, особенно если команда достаточно большая и содержит в себе
отдельные роли, направленные на спецификацию требований и общение с заказчи­
ком. Однако это является хорошей практикой для всех специалистов -
иметь бо­
лее широкое представление о том, что происходит в рамках проекта, и, поскольку
именно с идеи и требований начинается сама разработка, точно не будет лишним
понимать, на какие вещи стоить обратить внимание.
Одним из самых важных моментов, которые стоит выделить, является организация
правильной коммуникации с заказчиком. Нельзя забывать про документирование
процесса, нужно стараться генерировать определенные артефакты, поскольку с их
помощью можно выводить обсуждение в более конкретное и предметное русло.
Также это упростит работу для самих разработчиков, у которых появится более на­
глядное представление о том, как функционирует тот или иной процесс.
Важным моментом является то, что исправление бизнес-требований на этапе обсу­
ждения является гораздо более простым, чем переписывание системы на уровне
кода. Если окажется, что заказчик был понят неправильно, но команда разработки
уже реализовала определенные функции, их переписывание может потребовать да­
же больше времени чем разработка с нуля ввиду того, что уже могут существовать
определенные части системы, которые не должны быть затронуты, но во время
переписывания они могут так или иначе повредиться. Поэтому для того чтобы со­
хранить нервы разработчиков, важно правильно понимать цель и несколько раз
убедиться, что команда разрабатывает именно то, что нужно.
Сами программные системы при этом не терпят формата «разобраться с ситуацией
по ходу», необходимо прорабатывать не только позитивные сценарии, но и нега­
тивные. Если все возможные пути развития не будут реализованы в рамках кода,
такой процесс рискует завершаться ошибкой, что может привести в генерации не­
верных данных, плохому пользовательскому опыту и множеству других ситуаций,
которые может быть крайне проблематично исправлять вручную.
Поэтому первым шагом перед разработкой любой системы должен быть вопрос
«Почему?», с однозначным пониманием самой задачи, ее цели. Впоследствии вы
должны быть готовы к тому, что сами требования также могут изменяться, при
этом сам процесс разработки должен минимально страдать от этого, что достигает­
ся за счет технических приемов, которые мы разберем с вами позже.
111181
111181
ГЛАВА 3
1111111
https://t.me/it_boooks/2
Семантика
синхронных взаимодействий. REST
В предыдущей главе мы разобрали немаловажный вопрос бизнес-дизайна системы
и сбора бизнес-требований. Теперь, когда общий порядок бизнес-действий понятен,
можно приступать к построению системы. Одной из ключевых частей программной
системы по праву является коммуникация, причем как коммуникация с пользовате­
лем, так и с внутренними частями самой системы. Существует большое количество
способов передачи информации через Интернет: мы можем передавать файлы, де­
лать сетевые вызовы, отправлять сообщения в очереди и брокеры, иметь общие
базы данных. Каждый из этих способов имеет свои случаи применения, и на про­
тяжении этой книги мы будем к ним возвращаться. Но начать стоит с фундамен­
тальных стандартов, которые чаще всего встречаются, достаточно выразительны и
понятны. Речь пойдет о синхронных взаимодействиях.
О передаче данных и проблеме интерфейсов
При использовании Интернета, например через браузер, постоянно в скрытом от
нас режиме происходит сотня, а то и тысяча сетевых вызовов, которые помогают
собрать информацию буквально со всего мира. Большое количество разработчиков
постоянно участвуют в процессе построения новых программных систем и улуч­
шения существующих. Прямо сейчас передаются терабайты данных в секунду. Всё
это существует в динамическом равновесии, где новые технологии и подходы мо­
гут уживаться с уже привычными и старыми, добавляя им новых красок.
Проблема, которая вытекает из большого количества взаимодействий, -
это, как
ни странно, проблема совместимости. Все люди уникальны, и многие из них имеют
собственный взгляд на вещи, собственный взгляд на то, что удобно, а что нет.
И такое видение -
это большой враг программных систем: невозможно построить
грамотную интеграцию при фундаментальном различии видения этой самой ком­
муникации. И, что более критично, даже если получится построить такую систему,
остается вопрос развития взаимодействия, который неразрывно связан с эволюци­
онной природой бизнеса и бизнес-требований.
На заре Интернета, возможно, по такой же причине сформировались такие техно­
логии, как НТТР. Очевидно, что проблема разных взглядов на одни и те же вещи не
Глава 3. Семантика синхронных взаимодействий. REST
35
уникальна только для бизнеса, поэтому подход стандартизации технологий оказал­
ся очень удачным. Тот же НТТР, например, стал очень серьезной базой для множе­
ства паттернов коммуникации между сервисами. Поверх системных свойств этого
протокола можно передавать не только сухую информацию, но и семантику взаи­
модействий.
Все эти свойства вкупе позволяют выработать общие принципы взаимодействия
программных систем для обеспечения их совместимости и поддерживаемости на
долгие годы. Особенно стоит отметить последний пункт, потому что разработка
программного обеспечения -
это не столько про решение задачи здесь и сейчас,
сколько про возможность развития, которое не вынуждает переписывать все с нуля,
а позволяет методично добавлять новое, не сильно увеличивая хаос. Описанное
требует согласованности контрактов передачи данных, единообразия интерфейсов,
договоренностей о стиле. И начнем мы с самого распространенного представителя
таких договоренностей- REST.
REST over НТТР
Если вбить в поисковике
REST и прочитать определение, маловероятно, что вы
что-то поймете сразу. Более того, на разных ресурсах это понятие может тракто­
ваться по-разному, что вносит еще больше непонятности в сам термин. Может даже
сложиться такая ситуация, что будет казаться, что все вокруг понимают
REST,
успешно его применяют и не делятся этим секретным знанием в удобной форме.
Еще больше вопросов вызывает расшифровка аббревиатуры: Representational State
Transfer, или, дословно, трансфер репрезентативного состояния, - в это понятие
действительно можно вложить множество разных смыслов.
В названии этого раздела не просто так упоминается НТТР, поскольку REST не был
бы RESToм без НТТР-методов. Поэтому, чтобы понять, что такое REST, стоит на­
чать изучение с НТТР.
НТТР
Очень трудно переоценить роль НТТР в развитии Интернета. Протокол позволил
стандартизировать сетевые взаимодействия не только между клиентом и сервером,
но и между серверами. НТТР относится к протоколам прикладного уровня, т. е. он
ответствен за передачу некой логики, которая будет интерпретироваться другими
приложениями. Тема этого протокола (и история его развития) -
очень обширная
и выходит за рамки этой книги. Но мы попробуем затронуть основные моменты,
которые могут оказаться полезными при построении бэкенд-приложений и интер­
фейсов к ним.
Первое, о чем стоит упомянуть, -
это модель клиент-серверного взаимодействия.
Простыми словами, в рамках НТТР-вызова участвуют две стороны, одна вызывает
другую. В ответ на вызов вызываемая система может выполнить изменение состоя­
ния на своей стороне и/или вернуть некую информацию. Например, когда мы от­
крываем страницу браузера, мы выступаем в роли клиента, который запрашивает
Часть /. Проектирование и разработка системы
36
у сервера необходимую информацию, в данном случае это может быть веб-стра­
ница. Путем действий с этой веб-страницей, например добавления комментария на
сайте, путем все тех же вызовов мы можем изменить состояние сервера. Эта идея
состояния глубоко прижилась в REST, поэтому стоит рассмотреть ее подробнее.
НТТР-методы
Я не просто так привел два примера, один из которых мации, а второй -
это запрашивание инфор­
ее изменение. Очень нежелательно путать эти два варианта, по­
скольку это может нарушить пользовательский опыт и привести к несогласованно­
сти данных. Один из механизмов, который позволяет разделить НТТР-вызовы по
смыслу, называется НТТР-метод. Если описывать его одним предложением, то это
специальный флаг, который позволяет серверу понять, какую именно семантику
мы подразумеваем при том или ином вызове. Комбинируя эти методы, мы можем
обеспечить нужное поведение, а поскольку эти методы одни для всех разработчи­
ков, при соблюдении правил мы можем делать правильные предположения о сер­
висах других разработчиков, хотя с этим все же стоит быть аккуратным. Всего су­
ществует пять основных НТТР-методов (помимо служебных), которые позволяют
строить выразительные сетевые интерфейсы: GET, POST, PUT, РАТСН, DELETE.
Давайте разберем их все.
GET
Данный метод вызывается по умолчанию при открывании страницы в браузере и
семантически отвечает за запрашивание информации с сервера. Результатом GЕТ­
запроса может быть целая веб-страница, файл, JSОN-документ, ХМL-файл.
POST
Второй по распространенности применения НТТР-метод. Его использование под­
разумевает отправку некой клиентской информации на сервер. Например, с помо­
щью этого метода реализуется аутентификация пользователя. Передаваемая поль­
зователем информация может изменять состояние сервера, например добавлять но­
вые поля в базу данных, модифицировать файлы. Одной из особенностей РОSТ­
запроса является наличие поля body, в котором могут храниться бинарные файлы,
JSОN-документы, веб-формы.
PUT,PATCH
Наличие некой информации на сервере нередко подразумевает возможность ее из­
менения, и для реализации данной семантики существуют методы PUT и РА ТСН.
Формально для модификации некой информации на сервере можно использовать
POST, но явное применение данных методов обозначает, что мы изменяем сущест­
вующий объект, например перезаписывая какую-то его часть. Так же как и POST,
данные методы имеют поле body, которое пригодно для передачи больших объемов
информации в рамках одного запроса.
37
Глава 3. Семантика синхронных взаимодействий. REST
DELETE
Д,Ля полноты взаимодействий мы должны уметь удалять информацию, и для этого
существует метод DELETE.
Важным моментом является то, что с точки зрения НТТР-протокола методы никак
не влияют на сами запросы, иными словами, трактовка методов полностью ложится
на бэкенд-приложение. Более того, все НТТР-методы обладают одинаковым набо­
ром полей. Например, у того же метода GET тоже может быть body, но общие дого­
воренности разработчиков отразились в том, что браузер не может добавить это
поле к запросу, а сервер не будет его обрабатывать. Такие любопытные специфика­
ции позволяют избегать неправильной семантики, когда для исполнения какого-то
действия применяются неподходящие для этого инструменты и способы.
URL
Еще одной важной абстракцией НТТР является URL-aдpec. Я уверен, что каждый
из вас обращал внимание на то, что адрес сайта в адресной строке сопровождается
длинным набором символов, которые меняются при действиях на сайте. Все это дополнительный способ передачи информации на сервер с помощью НТТР. Давай­
те разберем это.
URL состоит из нескольких частей (рис. 3 .1 ).
Hostnarne
Schem
Path
Fragment
i
Port
Userinfo
#:fragment
Qu ~
Рис. 3.1. Основные части URL-aдpeca
Первая часть (на рисунке -
Scheme) -
объявление протокола связи. В контексте
НТТР это может быть как просто НТТР, так и его вариант с шифрованием, HTTPS.
Вторая часть (на рисунке -
Hostname) -
имя ресурса, это может быть как домен­
ное имя, так и IР-адрес.
Третья (на рисунке -
Port) -
сетевой порт, по которому происходит соединение.
Веб-браузер знает, что порт 80 отвечает за НТТР, 443 -
за HTTPS, поэтому любез­
но скрывает данную информацию от нас. Тем не менее мы можем указывать эту
информацию вручную, что бывает полезно при локальной разработке веб-сервисов,
которые имеют другие порты по умолчанию.
Четвертая (на рисунке -
Path) -
раth-параметр, или путь. Имеет много схожего
с путем на жестком диске и может использоваться сервером как инструкция, к ка-
Часть /. Проектирование и разработка системы
38
кому именно ресурсу мы хотим получить доступ. Например, в рамках веб-сервисов
по пути /api/v2 может находиться вторая версия API, по /ЫoЬ/videos могут нахо­
диться объекты видео и т. д. Грамотное разделение ресурсов и логики с помощью
раth-параметров -
это залог прозрачности и читаемости API.
Пятая (на рисунке -
Query) -
quеrу-параметры. Для более точного указания за­
прашиваемых ресурсов, а также передачи значимой информации необходим более
тонкий, чем раth-параметр, способ. Для этого используются quеrу-параметры. Фор­
мально, это набор пар «ключ-значение», которые разделяются между собой знаком
амперсанда (&). Данные параметры могут быть как прочитаны сервером, так и счи­
таны самим браузером. Например, фронтенд может считать эти данные и передать
их в другом НТТР-запросе к серверу.
REST
Теперь, когда мы разобрались с НТТР, можно приступать к построению новой се­
мантики коммуникации. Совокупность НТТР-методов и URL может показаться не
слишком выразительной, но, как и многое в IT, при правильном использовании она
покрывает большинство потребностей в коммуникации между сервисами. Есть
смысл определить эти потребности -
что именно мы хотим уметь делать с удален­
ной системой.
Существует несколько основных операций, производимых над информацией, создание, чтение, обновление и удаление. Вместе они складываются в термин
CRUD, (Create, Read, Update, Delete). Организация этих действий над объектом по­
средством некого внешнего интерфейса важна, поскольку она позволит обеспечить
единообразную точку входа для множества бэкенд-сценариев.
Не просто так ранее мы разбирали с вами основные НТТР-методы
POST, GET,
РАТСН/РUТ и DELETE, поскольку они отлично накладываются на необходимую
нам СRUD-семантику. Добавив к этому понятие ресурса, определение которого
происходит через
URL, возможность передавать дополнительную информацию
через query и body, мы с вами вплотную приблизимся к тому, что такое REST.
REST -
это договоренность (конвенция) об организации API поверх НТТР-про­
токола, подразумевающая определенное использование НТТР-методов и URL для
обеспечения
CRUD над информацией. В рамках REST есть четкое разделение,
к каким именно результатам должны приводить определенные НТТР-методы для
одного и того же URL. На первых этапах это может показаться не совсем прозрач­
ным, однако со временем вы будете все больше и больше понимать удобство такого
подхода к разработке контрактов обмена данными. Давайте разберем пару при­
меров.
Один из классических, я бы даже сказал книжных, RЕSТ-интерфейсов, -
это API
по работе с книгами. Представим ситуацию, что у нас есть библиотека, в которую
можно добавлять книги, удалять книги, модифицировать метаданные о них и т. д.
Можем ли мы реализовать все описанные действия с помощью одного волшебного
запроса POST, который в body будет содержать JSON с нужными нам действиями
над нашей виртуальной библиотекой? Конечно да! Однако в этом случае мы можем
Глава 3. Семантика синхронных взаимодействий. REST
39
не надеяться на то, что наш API будет интуитивно понятен для окружающих. К то­
му же документирование интерфейса будет требовать много времени, поскольку
нужно
отразить
множество
сценариев,
которые,
скорее
всего,
не
встречаются
на
других проектах. А что если попробовать использовать REST?
Первое действие, которое нужно сделать при создании библиотеки, -
это добавле­
ние нашей первой книги. Как именно реализовать такой интерфейс? Предположим,
что у нас есть ресурс:
https://mybooks.com/api/books
И книга, метаданные которой можно поместить в JSON:
{book_name: "New Book", author: "New Author", date: 20241
Как будет выглядеть запрос на добавление новой книги? Как мы помним, семанти­
чески за создание в НТТР отвечает метод POST. Поэтому мы должны поместить
в поле body наши метаданные и сделать НТТР-запрос POST на указанный URL. То
же самое можно сделать для множества других книг:
POST https://mybooks.com/api/books/
Как говорилось ранее, трактовка этих методов -
задача серверной части. Именно
поэтому наш бэкенд-фреймворк должен корректно обрабатывать разные НТТР­
методы. Например, в случае с запросом POST мы ожидаем добавления новой запи­
си в базу данных с книгами.
Теперь представим, что мы хотим получить доступ ко всем книгам в библиотеке.
Вероятнее всего, читатель уже догадался, что для этого семантически корректно
использовать метод GET:
GET https://mybooks.com/api/books/
Если наш API поддается семантике REST, в ответе сервера мы будем ожидать спи­
сок из
JSON, каждый элемент которого отражает информацию о книге. Но что
если мы хотим получить информацию только об одной книге? Теперь на сцену вы­
ходят раth-параметры:
GET https://mybooks.com/api/books/24
24 в этом случае -
ID книги, и в качестве ответа мы ожидаем один JSON с мета­
данными одной книги.
Идем дальше. А что если
мы хотим
получить книги только
одного автора?
Для передачи инструкции такого рода хорошо подходят quеrу-параметры:
GET https://mybooks.com/api/books?author=MyFavoriteAuthor
Как мы помним, сервер может получить эту информацию из URL, произвести по­
иск в базе данных и вернуть нам только релевантную информацию. Грамотное
комбинирование подобных запросов позволяет не перегружать сервер и получать
только ту информацию, которая нам нужна.
Неотьемлемая часть управления информацией -
возможность ее удаления. Для
этого в семантике REST необходимо использовать НТТР-метод DELETE:
DELETE https://mybooks.com/api/books/24
40
Часть /. Проектирование и разработка системы
Таким образом, мы можем сообщить серверу, что нам необходимо удалить книгу
с 1D 24.
Для изменения информации подходят НТТР-методы
PUT и РА ТСН. Эти методы
схожи семантически: подразумевается, что PUT изменяет весь объект, а РА ТСН -
только его часть, но сама логика обработки ложится на веб-сервер, и разработчик
сам может определить поведение. Допустим, мы хотим изменить имя автора книги.
Для этого мы можем отправить JSON, который содержит только одно поле с име­
нем автора:
{author: "New Author"}
Применив метод РАТСН:
РАТСН
https://mybooks.com/api/books/24
Часто для упрощения фронтенд-логики допустима отправка полноценной структу­
ры данных, в которой изменяется только одно или несколько полей. В таком случае
происходит перезаписывание информации в базе данных, вместе с этим происхо­
дит обновление нужных нам полей.
НТТР-коды ответов
Описанная нами семантика позволяет удобно создавать, читать, изменять и удалять
сущности бэкенд-системы. Однако очевидно, что не все операции могут проходить
так, как того ожидает вызывающая сторона. Что, например, произойдет, если книга,
которую мы хотим прочитать или удалить, отсутствует на сервере? Либо мы не
можем изменять метаданные книги, потому что наша роль в системе не подразуме­
вает возможности изменения информации. Либо сама бэкенд-система в рамках
пользовательского запроса столкнулась с необработанной ошибкой. Как именно
отразить все эти действия, чтобы пользователь мог скорректировать то, что он
делает.
На этот счет в НТТР есть мощная система кодов ответа (НТТР status codes), интер­
претация которых позволяет определить, что произведенное действие не возымело
нужного эффекта, и, более того, дать дополнительную информацию, почему это
произошло.
Коды ответов НТТР трехзначные и делятся на пять групп. Принадлежность к каж­
дой группе можно определить, посмотрев на первую цифру:
□
1-
информационные коды (например, информация о смене протокола);
□
2-
успешные запросы;
□
3-
запросы на перенаправление;
□
4-
клиентские ошибки;
□
5-
ошибки сервера.
Глава 3. Семантика синхронных взаимодействий. REST
41
2хх
Начнем наш разбор с кодов, которые уведомляют нас об успешности операции.
В рамках ответа от НТТР-сервера код 200 означает, что сервер смог корректно об­
работать наш запрос и сделать желаемое действие. Для расширения этой семантики
код 201 означает успешность создания, что может отсылать нас к РОSТ-запросам.
Однако в большинстве случаев код 200 используется для всех операций, где поль­
зователя необходимо уведомить об успехе. Например, если мы отправили запрос
DELETE по одной из конечных точек REST, код 2хх означает успешность уда­
ления.
Зхх
Коды, которые начинаются на тройку, относятся к кодам перенаправления. Напри­
мер, веб-сервер хочет указать, что запрашиваемый ресурс находится на другом
адресе. Реализовать это можно с помощью кодов 307 или 308. Важным нюансом
является то, что интерпретация и действие редиректа ложится на вызывающую
сторону. Например, веб-браузер, получивший такой код при вызове ресурса, может
выполнить
редирект
автоматически,
тогда
как
иные
программные
средства
для
совершения НТТР-вызовов могут не последовать цепочке перенаправлений.
4хх
Один из самых интересных классов кодов ответа. Чаще всего реализовывать на
стороне веб-сервера вы будете именно эту группу кодов. Коды, которые начинают­
ся на четверку, обозначают ошибки клиентской стороны. Но что это значит? Вари­
антов того, что может сделать клиент с нашим веб-сервером, великое множество,
и только небольшой процент из них может привести к правильным результатам.
Например, пользователь может указать неправильный адрес ресурса, не указать
нужные поля в запросе, пользователь может банально не иметь прав для выполне­
ния определенных действий. Всё это и даже больше можно выразить с помощью
семантики статусных кодов. Хорошей практикой также является добавление пояс­
няющих сообщений в ответ, чтобы сделать поведение вашей системы более про­
зрачным.
□
400 -
общий код ответа, говорящий о той или иной клиентской ошибке, напри­
мер о неправильно сформированном НТТР-пакете.
□
40 l Unauthorized -
данный код означает некорректность аутентификации. Мы
вернемся к теме безопасности позже, но в общем смысле данный код говорит
о том, что сервер не может идентифицировать (аутентифицировать) пользовате­
ля ввиду, например, некорректного или истекшего токена доступа.
□
403 ForЬidden -
в отличие от предыдущего кода, данный код говорит пользо­
вательской стороне о нехватке прав на выполняемое действие. Например, мы
можем пытаться прочитать ресурс, который не относится к области видимости
нашего пользователя, например запросить список книг не из нашей библиотеки
или запросить сообщения чужого пользователя. С помощью данного кода мы
можем передать информацию. Важно разделять семантику кодов 40 l и 403, по-
Часть /. Проектирование и разработка системы
42
тому что, хотя они и похожи на первый взгляд, они отвечают за два разных сце­
нария.
□
404 Not Found -
на мой взгляд, этот НТТР-код может быть знаком даже тем,
кто не знает о существовании кодов ответа веб-сервера. Как понятно из назва­
ния, этот код уведомляет о том, что запрашиваемый ресурс не существует. На­
пример, если мы хотим запросить несуществующую книгу по ее идентификато­
ру, веб-сервер после поиска в базе данных и обнаружения, что данной книги не
существует, должен вернуть код 404.
□
405 Method Not Allowed -
данный код нередко автоматически генерируется
веб-сервером, когда вызываемая конечная точка существует, но к ней применя­
ется неправильный метод. Например, мы хотим удалить книгу, применив метод
DELETE, но функционал удаления не заложен в саму систему (что, конечно,
бывает редко, но это просто пример). В данном случае можно вернуть код 405,
дав таким образом понять, что конечная точка принимает другие методы.
□
422 UnprocessaЫe Entity -
данный код означает, что наш запрос сформирован
синтаксически правильно, но в нем могут отсутствовать какие-либо поля, или
присутствующие поля не отвечают определенным критериям. Многие бэкенд­
фреймворки (например,
FastAPI для Python) имеют встроенную валидацию
входных значений НТТР конечной точки. Например, мы можем специфициро­
вать, что количество страниц в книге не может быть меньше одной, и в случае,
если клиент попробует добавить книгу с отрицательным количеством страниц,
сервер вернет данный код, специфицировав, какое именно поле не прошло вали­
дацию. Это очень мощный код ошибки, и его грамотное документирование по­
зволит избежать множества случаев некорректной интерпретации ответов сер­
вера пользователем.
5хх
Осталось обсудить коды ошибок сервера. Когда именно их нужно применять? На­
пример, если наше приложение столкнулось с непредвиденной ошибкой (например,
ошибка во время исполнения, когда мы вызвали несуществующий метод, выполни­
ли невозможную операцию, поделили на О и т. д.), бэкенд-фреймворк вернет клиен­
ту код 500 Intemal Server Error. Данный код позволяет клиенту понять, что сервер
сейчас испытывает временные трудности, которые будут исправлены впоследст­
вии. В случае если наш сервис недоступен, например он в принципе не смог запус­
титься, балансировщик нагрузки может вернуть
502 Bad Gateway, 503 Service
UnavailaЫe или 504 Gateway Тimeout.
Таким образом, мы рассмотрели основные коды ответов НТТР. Более подробную
информацию о перечисленных, а также обо всех остальных кодах можно получить
в Интернете. Поскольку назначение данных кодов одинаково для всех пользовате­
лей сети, в случае если ваша система подчиняется данным правилам, это может
значительно упростить взаимодействие пользователей и сделать их опыт похожим
на предыдущий опыт взаимодействия. Равно как и наоборот, если в случае внут­
ренних ошибок сервера всегда возвращать код 200, пользователи не будут знать,
Глава 3. Семантика синхронных взаимодействий. REST
43
что действие на самом деле было завершено неуспешно, что приведет к путанице и
трудностям интеграции.
OpenAPI
Исходя из разнообразия способов передачи информации в рамках REST, очевидно,
что многие серверы могут иметь большое количество конечных точек, отвечающих
за разные сценарии взаимодействия. Одно дело стандартизировать взаимодействие,
однако совсем другое -
правильно и единообразно его задокументировать. По­
скольку в рамках REST не существует явно строгих правил по разработке конечных
точек, особенности взаимодействия необходимо отражать в документации.
Хорошо, когда документация в принципе есть. Часто под нее могут выделять от­
дельные страницы на Wiki проекта, это может быть обычный документ или тексто­
вый файл. Однако такие подходы плохо масштабируются. Более того, заполнять
эти данные каждая команда разработчиков будет по-разному, что сильно усложняет
идею, которая заложена в REST, -
единообразие интерфейсов, которые были раз­
работаны разными командами разработки и для разных целей. При этом часто, осо­
бенно на старых проектах, документация не сериализована в текстовой форме, а
представляет собой коллегу, который знает, как работает система, либо не видит
смысла в документации, потому что «все и так понятно».
И как решить эту проблему, когда красивая семантика рушится из-за неправильно­
го документирования?
Идея достаточно проста: нам необходимо разработать формат, который будет опи­
сывать конечную точку определенным образом, и впоследствии этот формат можно
будет визуализировать посредством программных пакетов. Сама идея имеет гораз­
до более глубокие корни и отсылает нас к идее WSDL в SOAP (про SOAP мы пого­
ворим позднее), где мы можем использовать документирование не только как удоб­
ство для человека, но и как способ клиентской кодогенерации. Поскольку в рамках
REST нет встроенного способа документирования, сообщество разработало соб­
ственные инструменты, которые можно рассматривать как расширения. Один из
таких инструментов -
OpenAPI spec.
В общем представлении документирование конечной точки происходит в У АМL­
формате и выглядит примерно так:
openapi: 3.0.0
info:
version: 1.0.0
title: Sample API
description: А sample API to illustrate OpenAPI concepts
paths:
/list:
get:
description: Returns а list of stuff
responses:
'200 1 :
description: Successful response
Часть /. Проектирование и разработка системы
44
Описывая конечные точки в таком формате, мы можем обеспечить согласованную
документацию
между
множеством
других
проектов.
Часто
для
визуализации
OpenAPI используются инструменты SwaggerUI или Postman. Данный формат по­
зволяет описывать схемы данных, значения по умолчанию, лимиты значений, коды
ответа и их значения.
Возможно, на этом этапе мог появиться вопрос: неужели мне придется вручную
описывать данный файл? Хорошая новость заключается в том, что множество
бэкенд-библиотек, которые поддерживают типы и схемы данных, на основе напи­
санного кода могут автоматически генерировать данную спецификацию. Самый
близкий пример -
библиотека
FastAPI языка Python, похожий функционал есть
в Java Spring, .NET и других. Однако так бывает не всегда, и иногда действительно
нужно садиться и писать данный файл ручками, но оно определенно того стоит.
Одна неочевидная вещь, которую позволяет сделать автоматическая документа­
ция, -
это кодоrенерация. В большинстве языков сетевые вызовы имеют опреде­
ленную типичность,
благодаря
чему схемы
конечных точек, описанные через
OpenAPI, могут быть автоматически конвертированы в код, минимизируя челове­
ческий фактор. Благодаря этому обеспечивается единообразие схем данных между
разными приложениями. Этот подход часто используется на фронтенде, где нужно
правильно перенести логику получения и управления информацией для того, чтобы
правильно ее отобразить впоследствии.
Про другие паперны
Среди синхронных паттернов взаимодействия существует множество альтернатив
REST. Связано это с тем, что, как и все в программной архитектуре, REST имеет
свои ограничения и сферы применения. Например, хотя REST достаточно прост
в освоении и понимании, факт того, что он построен поверх функционала базового
НТТР, делает его не таким быстрым, как другие бинарные протоколы передачи ин­
формации. Да, современные версии НТТР поддерживают бинарную передачу дан­
ных, но для их правильного использования необходимы определенные оптимиза­
ции. Это может быть большой проблемой в системах, где миллисекундная задерж­
ка играет роль, либо где количество запросов настолько большое, что обрабатывать
их нужно самым эффективным образом.
С другой стороны, если мы не имеем проблем со скоростью, удобство использова­
ния API упирается в грамотность дизайна конечной точки. Если REST API не мо­
жет предоставить информацию в нужной и удобной форме, например когда опре­
деленные данные разбросаны по разным конечным точкам, возможность получить
эти данные одним запросом не появится сама собой -
необходима программная
реализация.
Для того чтобы закрыть подобные потребности, существуют другие протоколы.
Естественно, они имеют собственный набор компромиссов и ограничений, но не
упомянуть их в рамках раздела про синхронные взаимодействия было нельзя.
Глава 3. Семантика синхронных взаимодействий. REST
45
gRPC
Как уже говорилось ранее, REST использует для передачи данных протокол НТТР
версии 1.1. В большинстве своем это не проблема, однако данный протокол до­
вольно старый, имеющий свои собственные узкие места, которые были обусловле­
ны развитием технологий на момент его релиза. Одно из таких -
передача данных
в текстовом виде, что уменьшает эффективность обработки программами. Плюс
к этому данные в таком виде занимают больше места, что соответственно увеличи­
вает объемы передаваемой информации.
Перечисление нововведений НТТР/2 выходит за рамки этой книги, но если подчер­
кивать основные особенности, протокол передает данные в бинарном формате,
позволяет передавать множество пакетов в рамках одного соединения, при этом не
теряя обратной совместимости с НТТР/1.1.
Очевидно, что поверх данного стандарта должен был появиться паттерн коммуни­
кации, полноценно использующий все преимущества нового протокола. Таким
стандартом является gRPC.
В первую очередь gRPC передает данные в бинарном формате, тогда как REST (за
счет НТТР/1.1) использует простой текст. В gRPC используется специальный фор­
мат Protocol Buffers для сериализации запросов, что значительно увеличивает ско­
рость передачи и обработки сообщений.
Одной из других особенностей gRPC является поддержка двунаправленного стри­
минга данных. Тогда как REST построен поверх модели «запрос-ответ», в рамках
gRPC возможна одновременная отправка данных между клиентом и сервером.
Однако у таких улучшений есть последствия. В первую очередь конечные точки
gRPC более сложны в понимании с клиентской точки зрения. Принцип построения
API сложнее с точки зрения механизмов и работы со структурами данных. Требует­
ся больше усилий для построения потребляющих сервисов. Усложнены механизмы
обработки ошибок взаимодействия. Одной из особенностью является необходи­
мость в клиентских библиотеках для работы с API, тогда как в рамках REST доста­
точно обычного набора, в который включены простые НТТР-вызовы
Таким образом, gRPC отлично подходит для построения внутренних сервисов, для
которых скорость взаимодействия важнее, чем понятность интерфейса для больше­
го числа разработчиков. Однако, если мы строим API, который будет использовать­
ся большим количеством внешних пользователей,
REST является предпочтитель­
ным из-за простоты взаимодействия.
GraphQL
Несколько абзацев ранее упоминался пример, когда не все построенные RЕSТ­
интерфейсы предоставляют информацию в удобной для потребителя форме, на­
пример если для получения полной информации об одной сущности необходимо
выполнить множество вызовов. Другая особенность
Например,
нам
нужно
получить
всех
-
пользователей
это фильтрация данных.
определенного
возраста.
46
Часть /. Проектирование и разработка системы
Фильтрация на уровне REST должна быть реализована на уровне сервера, и если ее
там нет, то ее придется делать потребителю.
REST имеет определенные контракты работы с данными, и мы не можем выйти за
их рамки. Но что если клиент сможет сам динамически определять, какие именно
данные он хочет получить от сервера? Скорее всего, именно так и родилась идея
GraphQL.
В то время как в рамках
REST мы ограничены контрактами передачи данных,
в GraphQL мы можем создавать гибкие запросы на чтение, определяя то, что мы
хотим получить. Как это работает на серверной стороне? Как правило, существует
специальный шлюз, который знает про расположение нужных данных. Часто это
может быть группа микросервисов, каждый из которых отвечает за что-то свое.
GraphQL-шлюз способен интерпретировать пользовательский запрос на чтение,
обратиться ко всем нижестоящим сервисам, агрегировать информацию от них и
вернуть ее ровно в том формате, который запросил пользователь.
Данная особенность особенно полезна при фронтенд-разработке, где с помощью
грамотно разработанной конечной точки
GraphQL можно запрашивать нужные
объемы и форматы информации без необходимости обращаться к бэкенд-команде,
чтобы реализовать дополнительные конечные точки для получения нужной инфор­
мации.
Естественно, у такой технологии есть свои сложности. В первую очередь разработ­
ка такого API требует больше времени со стороны бэкенд-команды. Прописывание
логики получения данных из множества источников, их правильная регистрация
это нетривиальная задача. Существует еще одна важная деталь -
-
скорость. Во­
первых, получение данных из множества мест требует времени. Во-вторых, сами
промежуточные запросы могут быть неоптимальными, что может приводить к по­
вышенной нагрузке на сеть, нагрузке на нижестоящие базы данных и сервисы, ко­
торые ими управляют. Поэтому применение данной технологии требует грамотного
планирования. Если вы имеете дело с простыми API, нет большой нужны исполь­
зовать GraphQL в рамках запросов, которые можно сделать на REST. Однако если
данные сложные и есть возможность инвестировать время в разработку и интегра­
цию, правильное применение может окупить все приложенные усилия.
Заключение
Синхронные взаимодействия остаются и будут оставаться одним из самых попу­
лярных способов взаимодействия между сервисами. Понятная природа данных
взаимодействий позволяет строить сервисы быстро без потери выразительности.
При выборе технологии необходимо грамотно описать саму бизнес-задачу, понять,
что именно важно. Например, необходимо понимать требования по задержке, по­
тенциальную возможность интеграции с другими сервисами, безопасность прило­
жения.
Чаще всего выбор будет падать на REST, ввиду его распространенности, удобства
разработки, удобочитаемости, неплохой скорости. С точки зрения опыта я реко-
Глава 3. Семантика синхронных взаимодействий. REST
47
мендую начинать именно с него. Далее, в процессе развития проекта, могут начать
появляться узкие места. Например, если появляются проблемы с производитель­
ностью, помимо масштабирования их можно решить с помощью более совершен­
ных протоколов связи, например gRPC. Если контракты получения данных недос­
таточно гибкие, возможна интеграция GraphQL. Стоит отметить, что необязательно
выбирать только что-то одно.
Например, для простых сценариев либо API, ориентированного на пользователей,
REST. Для наиболее нагруженных частей системы можно
gRPC. Для систем сложной визуализации, где необходимо собирать
разнообразные запросы, отлично подойдет GraphQL. Но не стоит забывать, что
можно использовать
применить
большое количество разных технологий усложняет поддержку продукта и поиск
новых специалистов в команду, поскольку будет требоваться больший набор зна­
ний с самого начала.
ГЛАВА 4
Масштабирование приложений
Одной из любопытных деталей бэкенд-разработки является тот факт, что проблемы
и трудности в ней распределены неравномерно. Например, их может быть довольно
много в самом начале пути, когда многое неизвестно, много новых технологий и
концепций. Может быть не до конца понятно, как именно применять данные инст­
рументы, в конце концов, как правильно писать код. Далее, после освоения данных
деталей, может показаться, что все становится относительно просто. Мы можем
успешно обрабатывать запросы, работать с базой данных, делать простые интер­
фейсы, действительно приносить пользу с помощью нашего программного продук­
та. Однако потом, с ростом сложности приложения, с ростом количества пользова­
телей, те детали, которые в начале казались незначительными, начинают разгорать­
ся с новой силой, прожигая в архитектуре проекта такие дыры, какие невозможно
исправить чем-то иным, кроме переписывания всего проекта.
Любопытно то, что вероятность таких событий в случае успеха продукта не просто
есть, -
они неизбежны. Рано или поздно разработчикам, архитекторам, владельцам
бизнеса придется столкнуться с вопросом масштабирования и роста, и нередко для
того, чтобы это прошло хорошо, необходимо продумывать это в самом начале,
когда успех неочевиден.
В этой главе мы затронем основные вопросы, связанные с масштабированием при­
ложений. Обсудим то, как дизайн системы помогает справляться с нагрузкой, раз­
берем классические понятия горизонтального и вертикального масштабирования,
рассмотрим лучшие практики.
Вычислительные ресурсы
В первую очередь для понимания причин, которые приводят к необходимости
масштабирования, нужно обозначить основные расходные материалы. Ни для кого
не секрет, что компьютеры имеют определенный набор ресурсов, который исполь­
зуется при решении задач. К ним относятся: процессор, оперативная память, хра­
нилище, скорость сети.
Глава 4. Масштабирование приложений
49
CPU
Практически всем известно, что мощность процессора напрямую влияет на ско­
рость работы системы. Обсуждение принципов работы CPU выходит за рамки дан­
ной книги, но упомянуть основные особенности необходимо.
В первую очередь процессор отвечает за исполнение инструкций программ. Будь
это бэкенд-приложение, либо фронтенд, либо настольное приложение,
вся эта
сложная логика трансформируется в инструкции, которые выполняет именно про­
цессор. Результатами данных инструкций могут быть математические выводы,
проверка условий взаимодействия с памятью. Все это так или иначе складывается
в одну большую картину, которая позволяет нам, как простым пользователям, не
задумываться о низкоуровневых деталях,
а существовать в рамках своего уровня
абстракции, развиваясь именно в нем.
Однако, когда дело доходит до оптимизации, понимание принципов работы CPU
становится более важным. Какие основные характеристики есть у процессора? Для
удобства мы не будем затрагивать понятия, характерные для настольных систем,
а углубимся в понимание облачных CPU, которые предоставляет провайдер.
В рамках настройки серверов одним из основных параметров является не столько
семейство процессора, сколько количество ядер. Нередко, например в А WS, приня­
то говорить vCPU, где v -
virtual, т. е. виртуальный. Это означает, что провайдер
не всегда выдает нам физические ядра процессора, а за счет технологий виртуали­
зации способен выдать только дробную часть, которая соответствует некой мощно­
сти, определенной провайдером.
В общем смысле количество ядер процессора влияет на количество одновременно
проводимых операций. Например, с большим количеством CPU мы можем обраба­
тывать больше информации одновременно, обрабатывать больше запросов. Важное
уточнение: большее количество ядер не означает большую скорость процессинrа -
эффективность распоряжения ресурсами зависит от самой программы. Например,
если программа однопоточная (выполняется в одном процессорном потоке), добав­
ление большего количества ядер не увеличит скорость исполнения, потому что
фактически операции выполняются последовательно. Для полноценного раскрытия
мноrопоточноrо потенциала программа должна уметь работать с множеством пото­
ков, либо возможен запуск множества экземпляров одной программы, что будет
раскрыто чуть позже.
Второй важный параметр для CPU потоков
отвечает
за
максимальное
это тактовая частота. Если количество ядер и
количество
параллельно
выполняемых
опера­
ций, то тактовая частота является той самой скоростью, или количеством операций,
которые процессор может выполнить в секунду. Часто облачные провайдеры пре­
доставляют множество семейств виртуальных машин, которые оперируют привыч­
ными
нам
количествами
ядер,
однако
предоставляют
разные
процессоры,
подхо­
дящие под разные задачи. Например, для простых приложений мы можем выбрать
не самые мощные виртуальные машины с обычными значениями тактовой частоты
процессоров. Однако для высоконаrруженных частей системы, например для баз
50
Часть /. Проектирование и разработка системы
данных, которые выполняют сложные агрегации, семейства процессоров с большей
тактовой частотой могут быть ключевыми в обеспечении надежности работы.
У CPU есть набор параметров, которые позволяют определять степень загруженно­
сти, а именно близость к пределу операций, который может обработать процессор
за единицу времени. В рамках облачных систем есть возможность удобного мони­
торинга данного параметра и динамического добавления новых процессорных ядер,
нередко даже без выключения виртуальной машины.
В случае нехватки мощности CPU операции начинают выполняться медленнее, по­
тому что перед этапом исполнения они попадают в очередь, поскольку процессор
занят и не может выполнить инструкцию прямо сейчас. В рамках приложения это
приводит к зависанию не только одного приложения, но и остальных компонентов
системы. Поэтому грамотный выбор процессорных мощностей не только увеличи­
вает надежность системы, но и позволяет сэкономить: мы можем подобрать такое
количество CPU, которое не будет приводить к простою мощностей, но, с другой
стороны, у приложения будет иметься запас на случай незапланированной повы­
шенной нагрузки.
RAM
Второй столп, на котором стоит современная разработка, -
это оперативная па­
мять. Особенностью оперативной памяти в сравнении с обычным хранилищем
является на порядок превосходящая скорость работы с ней. Приложение может вы­
гружать в оперативную память данные, которые ему необходимо получить быстро,
и использовать их в своих целях. Например, в оперативной памяти можно хранить
результаты сложных вычислительных операций, структуры данных, историю.
В рамках бэкенд-разработки это особенно важный параметр, поскольку промежу­
точные результаты обработки запросов так или иначе проходят через оперативную
память, причем как на уровне самого бэкенд-приложения, так и на уровне сервиса
базы данных либо брокера сообщений.
Равно как с CPU, облачный провайдер предоставляет нам возможность управления
не столько семействами памяти, сколько ее объемом. Больший объем оперативной
памяти означает возможность хранения больших объемов информации в памяти
одновременно, что позволяет расширить количество сценариев, где эти данные мо­
гут быть полезны. Другой параметр, похожий на процессорный, -
это тактовая
частота памяти. Чаще всего она определяется семейством виртуальных машин. На­
пример, для приложений, нуждающихся в хорошей оперативной памяти, таких как
распределенный кеш или брокеры сообщений, виртуальные машины с оптимизаци­
ей в этом направлении могут увеличить пропускную способность.
В случае нехватки оперативной памяти приложение, как правило, прекращает свою
работу. Это имеет под собой основания, потому что данные в оперативной памяти
могли быть критическими для исполнения программы, и никакого альтернативного
плана в программной логике на этот счет может не быть. Исключением является
swар-память, т. е. возможность выгрузки оперативной памяти на жесткий диск
устройства. Данный подход позволяет избежать закрытия приложения, однако, как
Глава 4. Масштабирование приложений
51
говорилось ранее, скорость доступа к хранилищу в сотни раз меньше скорости дос­
тупа к данным в оперативной памяти, ввиду чего приложение может также зави­
сать и тормозить. Любопытный факт, что в рамках платформ оркестрации контей­
неров, например у Kubemetes, одним из требований работы кластера является от­
ключение swар-раздела для избежания проблем со скоростью доступа к памяти.
Сеть
Если названные до этого вычислительные ресурсы относились к одиночным узлам,
то сеть, очевидно, позволяет связывать между собой разные устройства и програм­
мы. Сеть позволяет передавать большие объемы информации, уведомлять о собы­
тиях, поддерживать актуальность данных. Часто сетью пренебрегают, поскольку ее
скорость почти во всех сценариях достаточно высока, особенно если мы говорим
про внутренний облачный периметр. Однако этот ресурс тоже исчерпаем, и нужно
понимать, как грамотно им пользоваться.
Основных параметров у сети два -
максимальная скорость входящего и исходяще­
го трафика. Часто она измеряется в мегабитах в секунду, хотя в рамках облака вир­
туальные машины могут иметь скорости до десятков rигабит. Это действительно
большой объем, который может позволить держать множество открытых соедине­
ний и взаимодействовать с множеством систем.
Важно понимать, что сетевой канал может быть один не только для одной вирту­
альной машины, но и для группы машин, если мы говорим про сценарии организа­
ции связи в рамках кластера, когда они соединены через один сетевой узел. Это
значит, что если одна программа будет заниматься скачиванием большого объема
информации, это может забить сетевой канал, ввиду чего другие приложения, на­
пример чувствительные к задержке, могут это почувствовать. Поэтому при работе
с сетью важно уметь ограничивать потребление сетевого канала, наблюдать за его
насыщением.
С другой стороны, приложения могут отправлять много лишней информации, так­
же забивая канал. Например, мы хотим запросить у сервиса информацию о пользо­
вателе, при этом нам интересно только имя. Ввиду особенностей работы сервер,
помимо имени, может вернуть множество дополнительных полей, например исто­
рию последних покупок, изображение пользователя и т. д. На нашей стороне не
составит труда просто не использовать лишние поля, однако этот вызов уже занял
процент сетевого канала, больший, чем было необходимо. Это явление называется
stamp coupling (связанность по образцу) и в какой-то момент может стать антипат­
терном, если количество неиспользуемой информации достигнет больших зна­
чений.
Хранилище
В современном мире хранилище не является проблемой, мы можем легко хранить
сотни терабайт информации, что, например, было очень проблематично всего ка­
ких-то
30-40 лет назад. Сегодня хранилище стоит достаточно дешево, но даже
Часть /. Проектирование и разработка системы
52
в рамках небольшой цены существуют разделения под разные задачи, которые не­
обходимо учитывать.
В первую очередь существует разделение по типу самого жесткого диска: HDD,
SSD либо даже накопители на магнитных лентах. Разные типы хранилища под­
ходят под разные сценарии использования. Например, для работы высоконагру­
женной транзакционной базы данных, в которой скорость случайного доступа
определяет скорость получения и обновления информации, отлично подходят твер­
дотельные накопители (SSD, NVMe). Однако такие накопители стоят дороже клас­
сических жестких дисков (HDD) и не совсем подходят под сценарии редкого дос­
тупа либо просто файлового хранилища, под которые классические жесткие диски
ПОДХОДЯТ лучше.
Иными словами, типы хранилища, помимо объема, влияют на скорость одновре­
менных операций чтения, их одновременное количество, задержку отклика. Поэто­
му важно оценивать свою задачу и выбирать подходящее решение для хранения
информации с точки зрения одновременной экономии и эффективности работы.
Для большего удобства понимания эту информацию можно свести в таблицу
(табл. 4. 1).
Таблица 4.1. Вычислительные ресурсы
Компонент
Основные особенности
Цель
Характеристики
CPU
Выполняет основные
Обработка данных,
Тактовая частота (ГГц).
вычисления и инструкции.
выполнение программ,
Многоядерность для
управление задачами
параллельной обработки
RAM
Быстрый доступ к данным.
Временное хранение
данных для работы
Количество ядер
и потоков.
Кеш-память
Ускорение работы системы
за счет быстрого доступа
Объем (Гб).
Частота (МГц)
к данным
программ
Дисковое
Постоянное хранение
Хранение ОС, программ,
Объем (ГбfТб).
простран-
данных.
пользовательских данных
Скорость чтения/записи.
ство
Разные типы носителей
Тип (HDD, SSD, NVMe)
(HDD, SSD, NVMe)
Сеть
Передача данных между
Обеспечение связи между
Скорость (Мбит/с, Гбит/с).
устройствами
устройствами, доступ
Тип (Ethernet, Wi-Fi, 5G).
к Интернету, обмен
данными
Задержка (ping)
После того как мы вкратце разобрали основные ресурсы, которыми будем управ­
лять, можно окунуться в премудрости масштабирования программных систем.
Глава 4. Масштабирование приложений
53
Виды масштабирования
Какую основную задачу должно решить масштабирование? К этому вопросу можно
подойти с разных сторон.
С одной стороны, нам интересно обеспечить надежный доступ к приложению, оди­
наковое время обработки запросов вне зависимости от их количества, одинаковую
скорость работы приложения в случае разной нагрузки.
С другой стороны, эффективное масштабирование помогает нам сэкономить на
оплате облачной инфраструктуры. Например, если мы видим слабое использование
наших серверов, мы можем отключить какую-то часть, поскольку она в любом слу­
чае не используется. Возможность быстро вернуть все назад является ключевой
особенностью облачных систем, где увеличение и уменьшение количества серверов
является нормой, в отличие, например, от систем on-premises ( систем, развертывае­
мых бизнесом в своем периметре), где вопрос покупки серверов, изменения их ко­
личества может подниматься нечасто ввиду того, что это требует планирования
бюджета, процесса закупки и наладки.
Начнем с первого случая. Допустим, мы заметили уменьшение производительности
нашей базы данных и хотим это исправить. С помощью мониторинга мы обна­
ружили высокую нагрузку на процессор. Какая именно методика придет нам на
помощь?
Вертикальное масштабирование
В начале этой главы мы упоминали о том, что вычислительные ресурсы можно
увеличить количественно в рамках одной виртуальной машины. Например, мы мо­
жем увеличить количество ядер процессора либо оперативную память.
Масштабирование ресурсов, которое затрагивает изменение параметров отдельно
взятой виртуальной машины, называется вертикальным масштабированием.
В нашем случае с базой данных это может быть хорошим решением -
нам нет не­
обходимости менять конфигурацию работающей программы, мы можем просто
добавить вычислительные ресурсы и дать возможность программе самостоятельно
их использовать.
У такого подхода есть своего рода недостаток: он, к сожалению, не бесконечный.
Иными словами, рано или поздно мы упремся в потолок, например, максимального
количества ядер процессора или максимальной оперативной памяти. В этом случае
необходимо продумать возможность разделения приложения, что в случае моно­
литной базы данных может быть большой проблемой, о чем мы поговорим в разде­
ле, посвященном операционным данным приложения.
Вертикальное масштабирование также хорошо показывает себя в ситуациях с клас­
сическими приложениями. Поскольку мы можем просто увеличить количество ис­
пользуемых ресурсов, это не будет требовать от нас более сложной оркестрации
приложения. Формально мы можем просто выполнить миграцию на другой сервер
либо изменить конфигурацию в рамках текущего, что не изменит логику управле­
ния, но тем не менее повысит производительность.
Часть /. Проектирование и разработка системы
54
Также важно не забывать момент, который уже был назван: если приложение не
способно грамотно утилизировать ресурсы, вертикальное масштабирование не по­
зволит на ровном месте получить прирост мощности. Такие ситуации могут слу­
чаться ввиду особенностей работы технологии или языка программирования, когда
параллельное исполнение потоков невозможно и, как следствие, невозможно реа­
лизовать потенциал многоядерности.
Вертикальное масштабирование -
это хороший подход, но он имеет явный пото­
лок. Однако в случае с обычным бэкенд-приложением, которое не хранит состоя­
ние само в себе, есть возможность прыгнуть дальше лимитов виртуальной машины,
и часто это происходит раньше того, как эти самые лимиты будут заметны.
Горизонтальное масштабирование
Заманчивой идеей является возможность использования не одной, а сразу несколь­
ких виртуальных машин для хостинга нашего приложения или приложений. Что
если бы мы могли разделить нагрузку между множеством узлов, при этом сохранив
согласованность обработки? Именно так и звучит идея горизонтального масшта­
бирования: вместо того чтобы увеличивать мощность одной конкретной виртуаль­
ной машины, мы можем добавить несколько узлов, содержащих в себе такое же
программное обеспечение, распределив нагрузку между ними.
Данный подход является крайне популярным в приложениях разного размера, но
особенно он применим к приложениям, которые не имеют состояния. Например,
хорошими кандидатами на горизонтальное масштабирование являются простые
бэкенд-приложения, которые не имеют сложной логики работы с базой, но делают
сложные преобразования запроса сами в себе. Другим хорошим примером являют­
ся сервисы, предоставляющие статические файлы, например сервисы доставки
фронтенд-кода либо медиафайлов.
Как именно обеспечить согласованность доступа? В отличие от приложений, кото­
рые работают на одном сервере, у нас появляется множество точек входа, каждая
из которых по сути выполняет одинаковую логику. Нам необходимо обеспечить
единообразие доступа в систему, чтобы оградить пользователя от деталей реализа­
ции нашего уровня исполнения.
Таким механизмом является балансировщик нагрузки (load balancer), который за­
нимается перенаправлением запросов на экземпляры группы горизонтального мас­
штабирования. Иными словами, пользователь отправляет запрос не участникам
группы напрямую, а специальному сервису балансировки нагрузки, который реша­
ет, какой именно из участников должен выполнить запрос.
Способов перенаправления запросов существует много. От банального случайного,
от менее банального «по очереди» (round roЬin) до более интересных, например на
основе IР-адреса пользователя, наличия определенных заголовков в запросе. Не­
редко балансировщик запросов может обеспечивать так называемые липкие сессии,
при которых один и тот же пользователь будет связываться всегда с одним и тем же
приложением из группы горизонтального масштабирования, позволяя не нарушать
парадигму «состояния» в бэкенд-приложениях, если приложение сериализует его
в себе.
Глава 4. Масштабирование приложений
55
Однако и у горизонтального масштабирования есть лимиты. Мы не просто так ра­
зобрали примеры с приложениями, которые не имеют ярко выраженного состояния.
Например,
ных, -
горизонтальное
масштабирование
баз данных,
особенно
реляцион­
это крайне непростая задача. Если приложение не подразумевает разделе­
ние своих операционных данных, мы не можем применить горизонтальное масшта­
бирование
к
нему
ввиду
возможности
потери
целостности
данных. Этот момент также будет разобран в главе
и
согласованности
7 про операционные данные
приложений.
В остальном горизонтальное масштабирование -
это отличный инструмент, по­
зволяющий приложениям при соблюдении определенных правил обрабатывать
большие объемы нагрузок. Помимо больших нагрузок, горизонтальное масштаби­
рование работает и в обратную сторону. Например, в ночное время суток, когда
количество запросов к нашему сервису не является большим, мы можем отключить
какой-то процент экземпляров группы масштабирования и включить их только
тогда, когда этого потребует нагрузка. Автоматизация данного процесса -
ключ
к надежности и экономической составляющей приложения.
Триггеры масштабирования
Одной из важных задач масштабирования является его автоматизация. Да, в рамках
каких-то ситуаций масштабирование может быть ручным, если мы, например,
решаем вопрос с производительностью базы данных. Однако, если мы говорим про
бэкенд-приложения, частым подходом является полная автоматизация этого про­
цесса.
Для корректной автоматизации необходимо иметь точки данных, привязка к кото­
рым
позволит ассоциировать с ними количество запущенных экземпляров прило­
жения. Самая простая метрика приложение
используется
не
так
время. Например, мы знаем, что ночью наше
активно,
как
утром
или
днем,
поэтому
имеет
смысл уменьшить количество работающих экземпляров приложения в ночное вре­
мя суток, тогда как днем есть смысл его увеличить. Стоит также отметить, что по­
добный вывод нельзя делать, основываясь на чистом ощущении, необходимо точно
так же собирать метрики использования приложения, такие как количество запро­
сов, утилизация процессора и оперативной памяти.
Второй вариант, развивающий идею предыдущего, -
это прямое использование
метрик утилизации ресурсов, а также количества запросов для действий масштаби­
рования. На практике это может выглядеть так: разработчики определяют некий
порог (например, средняя утилизация процессора за последние пять минут), при
превышении которого инициируется действие автомасштабирования, такое как до­
бавление новой виртуальной машины или контейнера. При· этом данное действие
может повторяться спустя какое-то время, если целевая метрика не пришла в нор­
му. Выстраивание подобного равновесия -
важная задача, которую необходимо
решать исходя из специфики приложения. Однако стоит сказать, что данный под­
ход не гарантирует отказоустойчивости во всех ситуациях. Как мы разберем впо­
следствии, масштабирование приложений -
довольно инертный процесс, это зна-
Часть /. Проектирование и разработка системы
56
чит,
что на поднятие
новых экземпляров может затрачиваться
какое-то
время,
от
десятков секунд до нескольких минут. Плюс сбор метрик приложения также про­
исходит не моментально, и мы можем начать действие масштабирования несколько
позже начала повышенной пользовательской активности.
Третий вариант, один из самых интересных, -
это предиктивный подход. Он объ­
единяет в себе то, что вручную может определить человек, и то, на что может среа­
гировать автоматика нашего приложения. Предположим, у нас есть большой набор
исторических данных нагрузки на наше приложение, в котором отражены количе­
ство запросов, утилизация ресурсов, время задержки. Используя данную информа­
цию, а также модели машинного обучения, можно попробовать предсказать на­
грузку, с которой может столкнуться приложение в будущем. Например, в рамках
исторических данных модель может понимать, что нагрузка на приложение в пят­
ничный вечер выше, чем в ночь вторника, и за счет этого рекомендовать коррект­
ную стратегию распределения ресурсов. Красота данного подхода заключается
в возможности выражения достаточно сложных зависимостей данных. Например,
перед повышенным спросом на приложение мы можем наблюдать какую-то не­
большую
предшествующую активность.
Модель машинного обучения, которая
встречала такие ситуации раньше, может ее запомнить и заблаговременно уведо­
мить о необходимости повышения количества вычислительных мощностей.
Описанные подходы требуют определенного уровня технической зрелости от раз­
работчиков. Благо в последнее время множество облачных провайдеров предостав­
ляют свои собственные решения для удобства масштабирования. Такие решения
включают в себя не только удобные способы масштабирования по клику, но и
углубленный мониторинг метрик с возможностью определения допустимых границ
значений, а также предиктивную аналитику. Дополнительно подобные сервисы мо­
гут выявлять аномалии, например аномально большой трафик в необычный для
этого период, и уведомлять об этом разработчиков.
Использование готовых платформ, как правило, надежнее, чем настройка своих
собственных, однако в обмен на это провайдер может брать большую стоимость.
С другой стороны, готовые платформы управления масштабированием могут не
иметь достаточной гибкости, хотя для большинства задач они отлично подходят.
Ниже приведена таблица основных триггеров масштабирования (табл. 4.2).
Таблица 4.2. Триггеры масштабирования
Название
Особенности
На основе расписания
Заданные вручную правила, которые приводят к изменению
количества запущенных экземпляров приложения
На основе значения
Масштабирование на основе заранее заданных рамок метрик, выход
метрик
за которые приводит к запуску новых экземпляров ввиду повышенной
нагрузки на приложение
С помощью машинного
Использование МL-моделей для определения паттернов использо-
обучения
вания с целью точного выделения мощности
Глава 4. Масштабирование приложений
57
Инструменты
Как говорилось ранее, обеспечение масштабирования приложения требует доста­
точной технической зрелости в различных инструментах. При этом для эффектив­
ного
масштабирования
само
приложение
должно
поддерживать
определенные
парадигмы, о которых пойдет речь дальше.
Первый вопрос, который необходимо решить, -
это скорость развертывания при­
ложения. В эпоху монолитных приложений этот процесс часто происходил вруч­
ную, он требовал внимания разработчика, который должен был с помощью интер­
фейса командной строки запускать или перезапускать приложения. Помимо того
что в этом случае тратится дополнительное время на человеческий ресурс, само
приложение может быть не оптимизировано для быстрого запуска. В рамках моно­
литных приложений это не всегда может быть проблемой, поскольку часто вопрос
производительности решался как раз вертикальным масштабированием, и решался
не так часто. Однако в современном мире, где приложения могут быть разбиты на
свои зоны ответственности и быть не такого большого размера, горизонтальное
масштабирование является одним из самых популярных способов повышения про­
пускной способности.
А теперь представьте, что для подобных приложений необходимо выполнять дей­
ствия масштабирования вручную для десятка виртуальных серверов. Даже если это
можно решить с помощью скриптовых сценариев, вопрос скорости развертывания
самого приложения остается открытым.
Еще одна проблема -
это единообразие среды. Это означает, что операционная
система, ее пакеты, зависимости должны соответствовать требованиям программ­
ного обеспечения и быть единообразными при каждом запуске. В противном слу­
чае приложение может в принципе не запускаться или, что еще хуже, запускаться,
но иметь неконтролируемое поведение, которое может вылиться в проблемы с опе­
рационными данными либо логикой работы. Для решения этих проблем сообщест­
во разработчиков использует несколько подходов, которые развивают идею вир­
туализации ния
отделения физических ресурсов сервера от логики работы приложе­
посредством
нового
слоя
абстракции.
Данный
подход
позволяет
решать
проблемы зависимостей приложения и скорости развертывания.
Виртуальные машины
Виртуализация -
это сильный инструмент, благодаря которому сама концепция
облачных провайдеров стала возможной. Вместо того чтобы покупать под каждого
клиента отдельный физический сервер, появилась возможность «нарезать» физиче­
ские ресурсы на удобные кусочки с возможностью изоляции друг от друга.
Данные кусочки могут иметь разное назначение, от возможности прямого выделе­
ния клиенту виртуальной машины до использования в управляемых сервисах, на­
пример, контейнерных оркестраторов либо бессерверных функций.
Как именно виртуализация помогает в обеспечении масштабирования? В первую
очередь в рамках облачного развертывания это возможность быстро получить
Часть /. Проектирование и разработка системы
58
определенную серверную конфигурацию без необходимости ждать покупки и дос­
тавки физического сервера. Скорость создания виртуальной машины может быть
около пяти минут. А в рамках сценариев, когда мы сами является владельцами фи­
зических серверов, виртуализация позволяет гибко распределить ресурсы между
разными частями приложения, и делать это динамически.
Например, мы хотим запустить создание некого отчета для аналитики, который
требует сбора данных из разных мест системы, а после выполняет вычислительно
затратные агрегации. Вместо того чтобы постоянно иметь сервер, который выпол­
няет данное действие (и соответственно постоянно платить за него), мы можем
поднимать его по требованию (например, раз в неделю), что возможно благодаря
облачным провайдерам. Либо, в рамках нашей собственной системы, мы можем
перераспределить ресурсы, выделив в нужный момент большие нагрузки на анали­
тику без необходимости физической перекомпоновки серверов.
Второе полезное свойство виртуальных машин -
это управление зависимостями.
Не секрет, что запуск программных продуктов требует определенных пакетов, от
интерпретаторов и библиотек до веб-серверов. Все это так или иначе ставит себе на
компьютер разработчик в самом начале разработки, но как доставить это на сервер?
Раньше этот процесс также был ручным и мог проходить не так гладко, поскольку
разные серверы могли иметь разные версии программного обеспечения внутри,
из-за чего сам конвейер установки зависимостей мог варьироваться. Виртуальные
машины
также
частично
решают
этот
вопрос
с
помощью
подготовленных,
или
«золотых», образов системы.
Если кто-то из читателей самостоятельно занимался установкой операционных сис­
тем, то он знает, что операционная система перед установкой хранится в виде фай­
ла-образа, который развертывается на установочный носитель, после чего устанав­
ливается на сам компьютер. Похожая идея присутствует в облачных виртуальных
машинах за тем исключением, что мы можем не просто ставить чистую операцион­
ную систему, но и устанавливать образы, которые уже содержат нужные нам зави­
симости. Например, наше приложение требует некого программного пакета, сборка
которого занимает 30 минут. Вместо того чтобы тратить эти 30 минут перед каж­
дым развертыванием экземпляра виртуальной машины, мы можем установить этот
пакет заранее и сделать так называемый снимок виртуальной машины, который
можно сериализовать в образ системы, который уже содержит нужные зависимо­
сти. Как правило, развертывание такого образа происходит быстрее, чем установка
всех пакетов при старте. Применение такого подхода помимо ускорения разверты­
вания позволяет минимизировать проблемы совместимости, которые могут появ­
ляться при ручной установке зависимостей на новых системах. Мы можем хранить
такой золотой образ в облаке и использовать его при новом развертывании. Неред­
ко провайдер уже имеет набор таких образов, который уже содержит нужные зави­
симости, и нам не нужно тратить время на создание своих собственных. Например,
такие образы могут содержать базовые конфигурации веб-серверов, драйверы ви­
деокарты, агенты мониторинга.
Другой вариант -
это применение установочных скриптов, которые выполняются
при первом запуске машины. Хотя такой вариант может быть дольше в плане уста-
59
Глава 4. Масштабирование приложений
новки пакетов, он проще и не требует от нас хранения и сериализации образа сис­
темы. Часто в таких скрипта"1( содержится некая конфигурационная информация,
например адреса серверов, метаданные, что позволяет кастомизировать поведение
сервера при его первом запуске.
Виртуальные машины тем не менее имеют свои ограничения. В первую очередь
виртуальные машины все также требуют времени на развертывание, которое может
быть больше пяти минут, что не всегда допустимо в рамках облачных систем, тре­
бующих быстрого масштабирования. Вторая особенность симостей. Поскольку виртуальная машина -
это количество зави­
это полноценная операционная сис­
тема, она может содержать пакеты, которые напрямую не будут нужны нашему
приложению. Это не только увеличивает вес самого образа, но и увеличивает время
холодного старта. Неправильное составление образа ведет к увеличению данных
характеристик, что в конечном итоге ведет к меньшей производительности систе­
мы, а также к увеличению облачной стоимости.
Третья проблема -
это, как ни странно, все та же проблема зависимостей. Вирту­
альные машины действительно позволяют решить вопрос для одного приложения,
однако если мы в рамках одной виртуальной машины хотим запустить сразу не­
сколько, появляется шанс коллизии зависимостей, когда одно приложение требует
определенных версий одного пакета и при этом другое приложение также требует
этот пакет, но версии ниже. Такие ситуации могут происходить нечасто, однако их
необходимо держать в голове при разработке стратегии развертывания.
Четвертая особенность -
это безопасность. Виртуализация обеспечивает хороший
уровень изоляции между разными приложениями, однако так или иначе он не абсо­
лютный, потому что приложения могут запускаться на одном физическом сервере.
Часто для некоторых стандартов безопасности требуется использование физиче­
ских серверов, на которых не происходит запуска чужого программного обеспе­
чения.
Стоит также сказать, что ввиду разделения физических ресурсов может наблюдать­
ся влияние одного приложения на другое. Например, если одну физическую маши­
ну делят высоконагруженная база данных и приложение, высокая нагрузка прило­
жения может забирать часть общих ресурсов, что понижает производительность
базы данных.
Многие ситуации требуют строгого использования только физических серверов
или виртуальных машин. Однако человечество не стоит на месте, и многие из этих
особенностей были элегантно обыграны в логическом развитии виртуализации
приложений -
контейнеризации.
Контейнеризация
Если суммировать основные проблемы виртуальных машин, то это их размер, ско­
рость
развертывания
и
неудобства
в
развертывании
множества
приложений.
С помощью технологий контейнеризации можно успешно обойти данные особен­
ности, но, естественно, с некоторыми ограничениями.
Часть /. Проектирование и разработка системы
60
Что же такое контейнеры? Если отвечать одним предложением, контейнеры -
это
упрощенные виртуальные машины, которые полностью теряют свое состояние при
выключении. При первом знакомстве это может показаться странным, однако при
более глубоком использовании данные ограничения имеют смысл и могут быть
преодолены встроенными инструментами.
Контейнеры значительно превосходят виртуальные машины по скорости запуска,
поскольку внутри себя они содержат минимальный набор пакетов, который обес­
печивает работу с жестким диском и минимальное функционирование системы.
Виртуальные машины требуют для запуска несколько минут, тогда как контейнеру
достаточно нескольких секунд в зависимости от сложности программного обеспе­
чения внутри.
Как правило, контейнеры используются для запуска одного приложения, в отличие
от виртуальных машин, которые могут одновременно содержать несколько прило­
жений, однако ввиду того что контейнер -
это такая же виртуальная машина, есть
возможность запуска побочных процессов. При этом, если завершается главный
процесс, сам контейнер также останавливается.
Одной отличительной особенностью контейнеров является унифицированный про­
цесс описания содержимого контейнера, иначе говоря, его сборки.
Например,
в рамках такого программного пакета, как Docker, описание содержимого происхо­
дит в
Dockerfile, который содержит в себе последовательные инструкции, какие
файлы необходимо скопировать, какие команды выполнить. После выполнения
этих команд все установленные артефакты сохраняются в так называемый имидж
(образ), который используется для запуска контейнеров. При каждом повторном
запуске контейнера все зависимости находятся в таком же состоянии, как после
окончания процесса сборки, что позволяет гарантировать одинаковый запуск кон­
тейнера в любом окружении, которое поддерживает запуск контейнеров.
Такой подход позволяет изолировать приложение от той среды, в которой оно за­
пускается, будь то компьютер разработчика либо облачная среда, сохраняя при
этом одинаковые зависимости и логику запуска.
Одним из ключевых отличий виртуальной машины и контейнера является отсутст­
вие состояния у последнего. Если выключить виртуальную машину и включить ее
спустя какое-то время, мы будем иметь доступ ко всем тем файлам, которые появи­
лись во время работы витуальной машины. Однако в случае с контейнером все, что
было сохранено внутри него, будет удаляться при выключении. Связано это с тем,
что при старте контейнер всегда использует тот самый собранный имидж, который
по определению не будет отражать новое, приобретенное состояние системы. Од­
нако как тогда быть в случае с приложениями, которым нужно сохранять свое со­
стояние между запусками, например в случае с базами данных? На помощь прихо­
дит механизм проброса папок с локальной машины. Иными словами, контейнер
может сохранять свое состояние на внешние папки, которые после могут быть про­
брошены при запуске самого контейнера, восстановив его состояние. Это доста­
точно удобный механизм, поскольку в рамках облачной среды у нас создаются
определенные точки данных, которые удобно резервировать и динамически под­
ключать в зависимости от окружения. При этом эти папки без проблем могут быть
Глава 4. Масштабирование приложений
61
сетевыми, т. е. не привязываться к конкретной машине, а пробрасываться прямо по
сети из определенного разработчиками хранилища.
Также контейнеры имеют удобные сетевые возможности. Контейнеры могут запус­
каться в собственной виртуальной сети, за счет чего контейнеры могут коммуници­
ровать с другими контейнерами (например, когда контейнер бэкенда взаимодейст­
вует с контейнером базы данных) с помощью системы внутренних доменных имен,
определяемых контейнерным
оркестратором.
При
этом данное
взаимодействие
происходит в виртуальной сети, и без явных изменений в конфигурации со стороны
пользователя в нее невозможно получить доступ извне, что увеличивает безопас­
ность.
Контейнеры представляют особый интерес с точки зрения масштабирования и рас­
пределения ресурсов. Во-первых, на одном виртуальном или физическом сервере,
на котором установлено ПО для запуска контейнеров, может быть сразу запущено
несколько контейнеров, при этом они не будут мешать друг другу в плане зависи­
мостей, поскольку каждый из них запускается в изолированной оболочке. Также
данная парадигма распространяется на кластеры, где мы можем запускать контей­
неры и не думать, на какой именно узел они попадут. Вкупе с удобными сетевыми
инструментами контейнерные оркестраторы способны самостоятельно перенаправ­
лять запросы к нужным контейнерам. Достигается это за счет механизмов баланси­
ровки нагрузки, которые выступают единой точкой входа в контейнерную группу.
В рамках облачных провайдеров существуют отдельные сервисы, направленные на
удобный запуск контейнеров без необходимости управления серверной инфра­
структурой. Это означает, что провайдер самостоятельно и за нас управляет ниже­
стоящими серверами, на которых запущено ПО для оркестрации контейнеров, бла­
годаря чему мы можем сконцентрироваться именно на уровне контейнерной абст­
ракции.
Также популярны
сервисы управляемых контейнерных оркестраторов,
например managed Kubemetes, в котором провайдер также управляет нижестоящи­
ми серверами, давая нам возможность управлять именно оркестрацией, не тратя
время на управление ресурсами.
Практически любая технология сейчас имеет контейнерный имидж. Данные имид­
жи хранятся в специальных хранилищах (registry), которые бывают как публичные,
например для образов баз данных, так и приватные, в которых разработчики хранят
код своих приложений. Облачные провайдеры также предлагают managed-вepcии
таких сервисов, позволяя разработчикам сконцентрироваться на создании кода.
С помощью данных реестров имиджей можно удобно разворачивать разнообразные
сервисы, при этом они будут разворачиваться всегда одинаково вне зависимости от
того, запускаются они локально или в серверной среде. Это открывает новый уро­
вень надежности программного обеспечения, где запуск кода единообразен на всех
платформах, позволяя избегать классической проблемы, когда все работает локаль­
но у разработчика, но не работает на сервере или у остальных.
Одной любопытной особенностью является эффективное хранение и сборка имид­
жей. Как было сказано, инструкция по сборке контейнера представляет собой
последовательные команды, которые выполняет сборщик. Каждая из этих команд
сохраняется в отдельном слое, и далее, при запуске контейнера, все слои собирают-
Часть /. Проектирование и разработка системы
62
ся в один, после чего происходит сам запуск. Сами имиджи могут быть многослой­
ные и иметь одинаковые слои. Нам не обязательно начинать сборку нашего прило­
жения с чистого листа, мы можем выбрать уже готовые образы, содержащие нуж­
ные зависимости, и просто добавить в них свой код. При этом, если данные слои
одинаковые для всех имиджей, они не будут храниться множеством копий -
у каж­
дого слоя есть свой хеш, который позволяет оптимизировать хранение, для всех
новых имиджей базовые слои будут одинаковыми и будут храниться в одном эк­
земпляре. Это значительно увеличивает эффективность использования хранилища,
в отличие от виртуальных машин, где, даже если разница между образами мини­
мальна, они все равно будут занимать полный вес.
Естественно, контейнеры также имеют ограничения. В первую очередь это необхо­
димость использования контейнерного оркестратора разной степени сложности.
В случае с системами с ограниченными ресурсами, например одноплатных ком­
пьютеров либо слабых серверов, оркестратор может тратить полезные ресурсы.
Второе ограничение -
это безопасность. Поскольку контейнеры -
это упрощен­
ные виртуальные машины, они частично наследуют проблемы с изоляцией. Соот­
ветственно, наследуется проблема совместного использования ресурсов, что, одна­
ко, нивелируется путем разделения контейнеров между серверами либо политикой
самого контейнерного оркестратора, который может контролировать использова­
ние вычислительных ресурсов контейнером.
В остальном контейнеры -
это современный и удобный способ развертывания
приложений любого уровня сложности, который позволяет обеспечить единообра­
зие запуска и зависимостей вне зависимости от среды исполнения. Контейнеры по­
зволяют унифицировать работу с данными, логирование и мониторинг метрик, что
можно использовать для повышения наблюдаемости системы, а также для реализа­
ции сложных сценариев автомасштабирования и восстановления системы.
О бессерверных функциях
Одной из современных тенденций программного обеспечения является разделение
ответственности.
Раньше,
ввиду особенностей разработки,
таких как наличие
лицензий ПО, например для баз данных, разработчики стремились к сближению
программной логики, иными словами, к построению монолитных систем. Именно
благодаря этому старые программные системы могут иметь такой причудливый
вид, который не похож на новые разрабатываемые системы.
Повсеместное использование контейнеров и виртуальных машин, а также появле­
ние на рынке большого количества зрелых технологий с открытым исходным
кодом, которые готовы к коммерческому использованию, подкрепляют это направ­
ление.
С развитием облачных технологий вопрос стоимости арендуемых ресурсов всегда
являлся актуальным. Обычно оплата за виртуальные машины является почасовой,
также провайдер может включать в стоимость объем передаваемого трафика.
Что если наше программное обеспечение не такое сложное и при этом количество
запросов к нему не такое большое? Очевидно, что в этом случае виртуальная
машина большее время будет простаивать, однако мы не можем отключить ее, по-
Глава 4. Масштабирование приложений
63
скольку запрос может поступить в любой момент. На помощь приходит парадигма
бессерверных (англ. serverless) функций.
Для начала нужно определить, что значит бессерверный. Это не значит, что про­
граммное обеспечение запускается не на сервере, вычислительные мощности все
также присутствуют, однако они полностью управляются провайдером, от управ­
ления патчами операционной системы до масштабирования. Примеров таких бес­
серверных сервисов
много
-
сервисы
аналитики,
которые
могут автоматически
масштабтироваться в зависимости от ваших нагрузок, сервисы объектного хране­
ния данных, которые скрывают невероятно сложную распределенную систему хра­
нения петабайтов данных за удобным пользовательским API. К бессерверным сер­
висам можно также отнести ситуации, когда облачный провайдер предоставляет
нам некую определенную вычислительную мощность без необхоимости управле­
ния ее дробными единицами применения, -
серверами. Но одна из самых интересных сфер
это бессерверный запуск кода.
Что касается оплаты, в бессерверных сервисах она основана не на часах использо­
вания, а на фактически затраченных ресурсах. Например, в рамках сервиса анали­
тики мы платим только за то время, за которое выполняется аналитический расчет,
либо, в случае с объектным хранилищем, только за используемое пространство и за
обращения к файлам. Было бы очень привлекательно, если бы мы могли реализо­
вать такое же, но в отношении кода, т. е. платить только тогда, когда происходит
его исполнение. И именно так и родилась идея бессерверных функций.
Бессерверные функции объединяют в себе две идеи -
разделение ответственности
с возможностью создания небольших сервисов, ответственных за собственный не­
большой контекст, а также систему оплаты, основанную на количестве запросов и
фактическом времени исполнения кода. Практически все современные облачные
провайдеры имеют такой сервис, который может исполнять определенные куски
кода в нужное для запрашивающей стороны время.
Как это работает? Предположим, у нас есть некий запрос в базу данных, который
исполняется редко, однако мы всегда должны иметь возможность его исполнить.
В рамках классической бэкенд-разработки мы могли создать API, который инкап­
сулирует в себе логику работы с базой, однако проблема заключается в том, что
нам нужно 24 на 7 держать включенными виртуальную машину либо контейнер,
что может быть дорого в сравнении с тем небольшим количеством запросов, кото­
рые потенциально может обработать такой сервис.
В рамках парадигмы бессерверных функций мы можем перенести такой код в спе­
циальную среду, в которой он будет запускаться при определенных триггерах (на­
пример, при НТТР-запросе ), выполнять логику и выключаться. При этом запуск
данного кода будет происходить практически моментально за счет особенностей
данной бессерверной среды. Таким образом, мы можем иметь определенные ко­
нечные точки, которые могут выполнять конкретную логику с достаточно удобной
системой оплаты. Множество облачных провайдеров предоставляют квоты бес­
платного использования ресурсов в месяц, и для какого-то типа проектов стоимость
такого решения будет в принципе бесплатной.
64
Часть /. Проектирование и разработка системы
Еще одна интересная особенность -
это масштабирование. Подобные сервисы мо­
гут быть использованы далеко не только в сценариях с малой нагрузкой, но и, на­
оборот, с достаточно большой. За счет быстрого запуска, а также особенностей по­
строения таких сервисов бессерверные функции могут обслуживать множество за­
просов одновременно, горизонтально масштабируясь с особой эффективностью.
Это особенно полезно при непредсказуемых нагрузках, когда классические подхо­
ды
автоматического
масштабирования
могут быть
неэффективны
из-за
своей
инертности.
Помимо работы с прямыми запросами, триггерами бессерверных функций могут
быть множество облачных сервисов. В зависимости от облачного провайдера бес­
серверные функции могут запускаться при появлении сообщений в очереди, при
добавлении новых файлов в облачное хранилище, при срабатывании мониторов,
которые наблюдают за метриками, и множестве других сценариев. Это делает ис­
пользование бессерверных функций гибким и удобным во множестве ситуаций,
сохраняя при этом потенциально большую пропускную способность, которая под­
страивается автоматически под наблюдаемую нагрузку.
Однако, как и у всего в сфере разработки программного обеспечения, у бессервер­
ных функций имеются определенные минусы. Во-первых, как ни странно, это
стоимость. В ситуациях с небольшим использованием бессерверные функции вы­
игрывают у классических виртуальных машин с почасовой оплатой, тогда как
в случае с большой нагрузкой, особенно при необходимости параллельной обра­
ботки множества запросов, стоимость может значительно превышать стоимость
варианта с виртуальными машинами. Необходимо тщательно следить за этим,
поскольку масштабирование происходит автоматически. Как минимум можно за­
давать
лимиты
на
одновременное
исполнение
и
настроить
мониторинг
вызовов,
чтобы быть в курсе использования сервиса.
Вторая проблема гораздо более глубокая и спорная, связанная с программной архи­
тектурой. Сама идея разделения логики неплохая -
мы можем разделить эти зада­
чи между командами разработки, благодаря чему можно в среднем увеличить ско­
рость создания ПО, а также снизить нагрузку на разработчиков. Однако есть и другая
сторона
-
слишком
маленькое разделение усложняет архитектуру, создает много
узких мест, которые может быть трудно исправить ввиду того, что система, со­
стоящая из множества частей, как правило, более хрупкая -
в ней больше вещей,
которые можно сломать. А в рамках бессерверных функций уровень разбиения по
умолчанию практически самый маленький, что при неправильном подходе к разра­
ботке может ее значительно усложнить. История знает множество примеров, когда
ввиду новомодного веяния
предпринимались попытки декомпозиции монолитного
приложения на бессерверные функции с целью уменьшения связанности, повыше­
ния способности к масштабированию, наблюдаемости. Но такие попытки ввиду
неправильного дизайна системы либо неправильной стратегии декомпозиции при
которой функции монолита просто зеркально переносились в облачную среду, не­
редко заканчивались провалом, кучей потраченного времени и разочарованием от
бессерверных функций. Поэтому их важно применять с умом и четко оценивать
сферу их применения, насколько она подходит в каждом конкретном случае.
Глава 4. Масштабирование приложений
65
Современные реализации таких функций позволяют переносить на подобную логи­
ку работы все более сложные фреймворки, предоставляя разработчикам возмож­
ность иметь привычные интерфейсы и парадигмы для работы с добавлением бес­
серверного функционала, отличающегося эффективным менеджментом ресурсов
в распределенной облачной среде.
В табл. 4.3 сведены основные технологии и подходы для организации вычисли­
тельного уровня
вашего
приложения,
а также
приведены
основные
плюсы
и
ми­
нусы.
Таблица 4.3. Основные технологии организации вычислительного уровня приложения
Плюсы
Минусы
Физические
Высокая производительность
Трудность управления множественными
(Ьаге metal)
и отсутствие дополнительных про-
приложениями, настройкой окружения
серверы
слоек между ПО и сервером
Технология
и зависимостей, время развертывания
новых экземпляров
Виртуальные
Удобство и гибкость выделения,
Низкая скорость развертывания,
машины
неплохая производительность,
неэффективное использование ресур-
неплохая безопасность
сов в случае множественного развертывания для одного приложения,
трудности управления
Контейнеры
Быстрое развертывание, хорошая
Определенные ограничения в хранении
изоляция зависимостей, унифициро-
состояния, более низкая безопасность
ванный интерфейс развертывания
в сравнении с виртуальными машинами,
в разных средах, удобная сборка,
меньшая производительность из-за
эффективное использование памяти
наличия прослоек оркестрации
Бессерверные
Крайне высокая скорость запуска,
Сложности в построении комплексных
функции
выгодная ценовая политика в случае
приложений и оркестрации, высокая
нерегулярного использования,
цена в случае высокой нагрузки, огра-
масштабируемость
ничения на вычислительные ресурсы
Заключение
Эффективное масштабирование -
одно из важных требований устойчивой и высо­
копроизводительной системы. Описанные подходы имеют свои особенности и не­
достатки, поэтому критически важно знать особенности именно своей системы,
чтобы понимать, что может быть подходящим в конкретной ситуации. Не стоит
забывать, что помимо простых вычислений приложения имеют свои операционные
данные, которые также нужно масштабировать ( об этом пойдет речь в главе 7 про
операционные данные приложения).
Как грамотное масштабирование может помочь сэкономить деньги и обеспечить
бесперебойную работу приложения, так и неэффективные и неподходящие инстру­
менты могут не только привести к повышенной стоимости, но и усложнить жизнь
разработчикам, которым придется такую систему поддерживать. Благо современ­
ные облачные провайдеры имеют хорошую документацию и понятные примеры
применения, которые можно использовать как вдохновение для того, чтобы сделать
вашу систему не только работающей, но и красивой.
ГЛАВА 5
Асинхронные взаимодействия
Способы взаимодействия между приложениями не ограничиваются синхронными
вызовами. Существует множество ситуаций, при которых применение того же
REST или gRPC не является оптимальным методом решения коммуникационной
задачи. Для таких случаев существует отдельный вид коммуникации - асинхрон­
ный.
Природа множества бизнес-процессов асинхронна. Например, когда мы заказываем
еду в ресторане, мы не стоим перед стойкой выдачи в ожидании заказа. Вместо это­
го мы можем по крайней мере сесть за стол, поговорить о чем-то, и потом, когда
еда будет готова, она будет доставлена к нам без необходимости нашего прямого
участия.
Отличительной особенностью асинхронных вызовов, как в рамках сетевых взаимо­
действий, так и в рамках абстракции языков программирования, является отсутст­
вие необходимости ожидания завершения активного бизнес-процесса. Если го­
ворить вкратце, нам не нужно дожидаться завершения действия, вместо этого мы
можем узнать о его завершении позже, используя выбранный канал связи.
Подобный паттерн может быть реализован множеством
подходов и сервисов.
К ним относятся очереди, брокеры сообщений, веб-хуки, веб-сокеты. Все они при
грамотном
подходе
позволяют
строить
высокопроизводительные
системы
и
ис­
пользовать все плюсы асинхронности.
Перед этим стоит пояснить, почему синхронные взаимодействия -
это хороший
старт, но в каких-то ситуациях их может быть недостаточно.
Ограничения синхронных взаимодействий
При построении любой системы необходимо провести анализ самой бизнес-задачи,
поскольку ее грамотное моделирование -
это залог грамотной кодовой реализа­
ции. Часто такие системы построены вокруг управления неким набором данных,
например управления запасами, управления доступом, визуализации, возможности
быстро обрабатывать большой объем информации. И при рассмотрении множества
задач мы можем без проблем выразить подобные взаимодействия с помощью син-
Глава 5. Асинхронные взаимодействия
67
хронных взаимодействий. Например, с помощью синхронного вызова мы можем
изменять количество товаров на складе, изменять метаданные. Более того, при син­
хронной реализации данные действия приобретают АСID-свойства, если база дан­
ных их поддерживает. Точно таким же синхронным образом мы можем запраши­
вать какие-то наборы данных, создавать отчеты.
Но давайте рассмотрим эту же ситуацию с другой стороны. Предположим, что на­
ша система резко усложнилась, и, помимо простого управления данными, нам не­
обходимо поддерживать историчность, т. е. хранить историю изменений, а также
нотифицировать какие-то части системы о том, что пользователь что-то сделал
в системе. Например, пользователь изменил имя товара, и нам нужно оповестить
сервис кеша о том, что имя файла поменялось, чтобы информация в системе оста­
валась актуальной. Либо мы хотим использовать информацию о смене имени или
смене цены в аналитических целях, и мы должны нотифицировать сервис аналити­
ки, предоставив ему актуальные данные. Подобных ситуаций, когда одно действие
должно привести к изменению состояния во множестве мест, количество которых
заранее неизвестно, с ростом сложности сервиса может быть довольно много.
В случае с синхронными взаимодействиями, когда большинство соединений пред­
ставляют собой ситуации «точка-точка», изначальный сервис, который является
источником события, должен будет заранее знать, какие именно сервисы необхо­
димо нотифицировать, буквально выполнив количество запросов, равное количест­
ву нижестоящих сервисов. С увеличением количества потребителей уведомления
сложность кодовой поддержки растет, а надежность такой системы падает, потому
что в ситуациях, когда один из сервисов не подтвердил получение, может быть не­
очевидно, что делать с уже отправленными (или неотправленными) запросами.
Рассмотрим другой пример. Предположим, мы хотим запросить некий аналитиче­
ский отчет. Нередки ситуации, когда SQL-зaпpoc может выполняться несколько
десятков секунд, а иногда бывает так, что на его выполнение (либо на другую агре­
гацию данных) может уходить не один час, а иногда и день. В случае с простой
аналитикой пользователь может подождать несколько десятков секунд, держа от­
крытым соединение. Однако, если это время ожидания становится слишком боль­
шим, держать открытое соединение может быть неэффективно, по крайней мере,
потому что пользователю может надоесть, либо сетевое соединение может пре­
рваться, полностью сбросив успех формирования отчета.
Третьим примером может быть отложенный запуск. Например, мы хотим сформи­
ровать аналитический отчет, однако сами вычислительные ресурсы могут быть вы­
делены значительно позже. Для того чтобы реализовать такой сценарий, мы долж­
ны иметь определенный буфер, который сохранит в себе все сообщения до момента
их потребления нижестоящими сервисами.
Описанные ситуации -
это лишь малая часть тех проблем, которые не имеют на­
тивного решения в рамках парадигмы синхронных взаимодействий. Как и все
в программном мире, этот подход имеет свои достоинства и компромиссы, но опи­
санные ситуации требуют других подходов, поскольку сама логика синхронных
взаимодействий в каком-то месте может противоречить бизнес-задаче. Это не зна­
чит, что синхронные взаимодействия плохие и нам везде необходимо использовать
Часть /. Проектирование и разработка системы
68
асинхронные решения. Множество бизнес-задач невозможно решить, применяя
асинхронные подходы. Главное -
правильно оценить сферу применения, без при­
вязки к популярным технологиям.
Если суммировать основные преимущества синхронных взаимодействий, то можно
прийти к следующим пунктам:
□ понятность взаимодействия. Взаимодействия по типу «запрос-ответ» прозрачны
для пользователя, у нас есть явные схемы запросов и явные схемы
возвращае­
мых данных (если это реализовано на уровне паттерна коммуникации);
□ удобство тестирования. Благодаря удобству протоколов взаимодействия и от­
сутствию большого количества двигающихся частей такие системы проще тес­
тировать;
□ согласованность данных. Поскольку часто мы существуем в рамках одного со­
единения, мы можем обеспечить согласованное взаимодействие с базой данных
без потери данных. Исключением являются сложные вызовы, которые затраги­
вают несколько сервисов и оркестрацию.
В свою очередь, к минусам можно отнести:
□ сложность нотификации о событиях на серверной стороне ввиду того, что со­
единение не держится открытым и сервер не может вызвать клиента напрямую,
за исключением если клиент является таким же сервисом;
□ сложность «мультиплексирования», когда одно событие должно быть отражено
во множестве сервисов;
□ неудобства при обработке запросов, которые требуют большого времени на за­
вершение. Мы не можем держать открытое соединение слишком долго, что тре­
бует переработки механизмов обработки таких ситуаций;
□ трудности при реализации отложенной обработки, где потребитель события мо­
жет быть недоступен в данный момент, но рано или поздно он сможет присту­
пить к обработке.
Теперь,
когда мы определились с основными ограничениями и вопросами,
можем посмотреть на них с другой стороны -
мы
со стороны асинхронных взаимо­
действий.
Асинхронность как паперн коммуникации
В самом начале главы было сказано, что отличительная особенность асинхронно­
сти -
это возможность не дожидаться конца действия. Например, не дожидаться
подтверждения получения нашего запроса нижестоящим сервисом, не дожидаться
окончания обработки данных. Часто паттерны асинхронности позволяют не только
не дожидаться ответа, но и в принципе не знать, кто получатель события, но об
этом позже. Для начала необходимо разобраться в механизме работы основных ин­
струментов, после чего мы сможем применить данное понимание для построения
сер висов.
Глава 5. Асинхронные взаимодействия
69
Очереди сообщений
Классическим примером асинхронного взаимодействия является взаимодействие
через очередь. Помимо асинхронности, очередь имеет еще одно полезное свойст­
во -
это некий буфер, который может помогать в отделении сервисов друг от друга.
Основная идея очереди, как следует из названия, -
это очередь сообщений, с кото­
рой можно сделать несколько фундаментальных действий: помещение в очередь,
чтение и удаление сообщений из очереди. На основании этого сервисы, взаимодей­
ствующие с очередью, делятся на производителей сообщений (producers) и потре­
бителей (consumers). Первая группа публикует сообщения, которые впоследствии
будут потреблены второй группой, с дальнейшим удалением обработанных сооб­
щений из очереди.
Очередь способна сглаживать скачки активности, например, если пользователи
слишком часто запрашивают создание определенного отчета, нижестоящий сервис
в случае синхронного вызова мог перестать справляться с нагрузкой, что привело
бы к ошибкам. Наличие очереди позволяет нижестоящему сервису обрабатывать
ровно столько, сколько он может, добавляя некий механизм обратного давления.
Другой пример -
это возможная недоступность нижестоящего сервиса. В рамках
синхронного взаимодействия в случае недоступности сервиса мы ставим вызы­
вающую сторону в не совсем ясное положение, если результат этого вызова чрез­
вычайно важен для продолжения работ. Например, вызывающий сервис может
продолжить вызывать нижестоящий, либо мы также можем бросить сообщение об
ошибке. Такое поведение может возникать как при плановой недоступности серви­
са (при использовании устаревших методов развертывания), так и при внеплановой,
ввиду внутренней ошибки. Очередь в рамках данного сценария помогает сгладить
и это. Пока сервис-потребитель не выйдет на связь, сообщения будут храниться
в очереди.
Третья
особенность
очереди
-
это
возможность
параллельного
потребления.
Предположим, что у нас есть задача обработки множества изображений. Например,
пользователи загружают архивы картинок, и для каждой из них мы должны сделать
миниатюру, чтобы показывать на пользовательском интерфейсе. Если делать это
последовательно, это может занять много времени, поскольку модификация изо­
бражений -
это относительно затратная вычислительная операция. Поэтому нам
необходимо распараллелить это, и в этом нам может помочь очередь. Для обычных
очередей, которые не поддерживают обязательное сохранение порядка событий,
возможно
одновременное
потребление
целой
группой
потребителей.
Поэтому
в рамках нашей задачи мы можем поместить в очередь сообщения с информацией
о картинке, например пути к ней в облачном хранилище, после чего написать при­
ложение-потребитель, которое будет скачивать эту картинку, проводить нужную
операцию и удалять сообщение. Для большей эффективности мы можем объеди­
нять в рамках одного сообщения несколько картинок либо потреблять несколько
сообщений за раз.
На этом этапе важно ответить на вопрос: а что именно представляет собой сообще­
ние? Можем ли мы добавлять какие-то большие файлы в сообщение? Ответ на этот
70
Часть /. Проектирование и разработка системы
вопрос не отличается от множества других в рамках программной архитектуры -
это зависит от обстоятельств.
Но в рамках большинства ситуаций сообщение
-
это JSОN-объект либо иная
структурированная текстовая информация, которая может быть обработана про­
граммой. Добавление больших объектов в сообщение считается антипаттерном и
увеличивает время обработки сообщений, нагружает очередь, а в рамках облачных
реализаций увеличивает стоимость услуг самой очереди. Многие очереди имеют
программные ограничения размера, часто это пара сотен килобайт (как в случае
с SQS).
Важно понимать, что очередь -
это прежде всего способ нотификации, а не способ
передачи данных. Иными словами, сообщения должны содержать в себе только не­
обходимые данные, что является реалистичной задачей, поскольку основной спо­
соб использования очередей -
это взаимодействия типа «точка-точка». Однако
паттерн сцепления по образцу (stamp coupling) тоже встречается, поскольку неко­
торые поля данных могут пригодиться в будущем, либо требуется их перенаправ­
ление другим сервисам.
Как было сказано ранее, очередь способна обеспечить параллельное потребление,
но как обеспечивается защита от коллизий? А именно как не происходит ситуаций,
при которых одно сообщение может быть потреблено несколько раз разными
потребителями? Для этого существует очень элегантный механизм невидимости
сообщений. При потреблении потребителем сообщение не удаляется из очереди
сразу, а становится невидимым для остальных потребителей на определенный
в настройках очереди период времени (как правило, это несколько десятков се­
кунд). За это время приложение-потребитель должно закончить свои действия, на­
пример отправку уведомления, добавление записи в базу, перенаправление, после
чего оно должно удалить сообщение из очереди. Таким образом обеспечивается
семантика обработки сообщения «как минимум один раз».
Стоит сказать пару слов про семантики обработки. Почему нельзя удалить сообще­
ние из очереди сразу при прочтении? Подобный подход допустим, но может при­
вести к потере сообщения. Это может произойти, если в промежутке между целе­
вым действием и удалением сообщения произойдет непредвиденная ошибка, кото­
рая приведет к выключению приложения. В таком случае мы не сможем передать
нужную информацию, например добавить запрос в базу данных или отправить уве­
домление. Сообщение в этом случае будет потеряно, но целевое действие не будет
выполнено до конца. Такая логика работы не гарантирует доставку сообщения,
и она нередко имеет смысл, если мы говорим о задачах с необходимостью обработ­
ки большого количества сообщений, при которых некритична частичная их потеря,
например при обработке кликстримов пользователей.
С другой стороны, удаление сообщения после целевого действия тоже не лишено
потенциальных проблем. Например, мы добавили запись в базу или отправили уве­
домление, но при этом не смогли удалить сообщение ввиду непредвиденной ошиб­
ки. Что произойдет дальше? Как мы помним, сообщение при потреблении имеет
тайм-аут невидимости, в течение которого никакие другие потребители не могут
его потребить повторно. По истечении этого тайм-аута сообщение вновь становит-
71
Глава 5. Асинхронные взаимодействия
ся доступно для других потребителей. В конечном итоге сообщение будет потреб­
лено еще один раз, целевое действие также будет выполнено повторно, приведя
либо к дупликации данных в базе данных, либо к повторной отправке уведомления,
как в нашем примере.
Существует также золотая середина- это семантика обработки «строго один раз».
Однако стоит отметить, что подобная семантика -
это не свойство самой очереди,
ее нельзя включить в настройках, она основывается на сложной логике самого при­
ложения. Дrlя того чтобы избежать дублирования сообщений, нижестоящий по­
требитель должен поддерживать такое свойство, как идемпотентность. Этот слож­
ный термин означает ситуации, при которых многократное выполнение какого­
либо действия эквивалентно его однократному выполнению. Например, если мы
добавляем запись в базу данных о денежной транзакции, повторное добавление
этой записи должно либо привести нас к ошибке, либо никак не отразиться на
состоянии базы данных. Такое поведение достигается с помощью ключей идемпо­
тентности
-
механизма определения уникальности транзакции, а также статуса ее
выполнения.
Как уже было сказано, асинхронная связь не требует от вышестоящей стороны
ожидания окончания действия, что приносит одно ограничение -
усложненный
мониторинг ошибок. Например, в случае синхронного вызова вызывающая сторона
может быть уведомлена о неуспешности операции посредством кодов ответа. Но
как уведомить в случае с очередью?
Не существует идеального механизма, который бы мог подойти во всех ситуациях,
поэтому для правильного управления ошибками вам необходимо оценивать свою
архитектуру самостоятельно. Однако есть некоторые инструменты, которые вы
можете использовать, чтобы добавить прозрачности в асинхронную обработку.
Самый главный из них -
очередь мертвых сообщений (dead
letter queue, DLQ).
Предположим, что приложение понимает, что не может обработать определенное
сообщение, например оно вызывает какое-либо непредвиденное исключение. По­
скольку данное сообщение может бесконечно курсировать между потребителями
в перерывах между тайм-аутами невидимости, мы должны иметь возможность уда­
лить это сообщение из очереди, при этом сохранить его в некой буферной зоне,
к которой впоследствии разработчик может вернуться и разобраться, в чем проб­
лема. Именно для этого и существуют очереди мертвых сообщений. Приложение
может автоматически перенаправить то сообщение, которое оно не может обрабо­
тать до конца, в эту очередь с возможностью повторной его обработки после ис­
правления ошибки. Либо сообщения могут попадать в DLQ после определенного
количества попыток доставки, что позволяет явно не прописывать логику перена­
правления в коде, хотя последнее я нахожу более предпочтительным.
Впоследствии, когда ошибка будет исправлена, мы можем перенаправить все со­
общения из DLQ в нашу обычную очередь, выполнив целевое действие. Стоит ска­
зать, что редко все бывает настолько просто. Например, если мы исправим ошибку
не сразу, сообщения, которые мы сохранили в DLQ, могут потерять актуальность,
более того, их слишком поздняя отправка может навредить системе, поскольку за
время, пока они лежали в DLQ, состояние данных могло сильно поменяться. По-
Часть /. Проектирование и разработка системы
72
этому важно оценивать компромиссы и инструменты и избегать бездумного вне­
дрения тех или иных подходов, минуя стадию оценки их актуальности для проекта.
Примеры применения очередей,
сервис отправки уведомлений
Как говорилось ранее, чаще всего очереди применяются при соединениях типа
«точка-точка». Предположим, что у нас есть необходимость добавить функционал
уведомлений в наше приложение. Нам необходимо поддерживать отправку с по­
мощью электронной почты, с помощью ботов в мессенджерах либо даже добавить
SМS-сообщения или рush-нотификации на устройства пользователей. Вносить дан­
ный функционал в каждый из сервисов, которым он может понадобиться, бывает
довольно сложно ввиду потенциально разного программного стека, а также ввиду
сложности параллельной поддержки при добавлении нового функционала.
Поэтому мы можем вынести эти функции в специальный, отдельный сервис. Одна­
ко какой паттерн коммуникации выбрать? Те способы отправки уведомлений, что
мы выбрали, могут иметь ограничения использования. Например, мы не можем от­
правлять больше
100 SMS в минуту либо больше 100 сообщений в секунду для
ботов мессенджера, более точные числа зависят от реализации, но основная суть
остается такой же. Такие лимиты, связанные с неподвластными нам вещами, могут
сыграть против нас, если мы выберем синхронные паттерны коммуникации. На­
пример, вызывающий сервис хочет отправить письмо на определенный адрес элек­
тронной почты, и он делает синхронный вызов, который будет держаться откры­
тым, пока письмо не будет отправлено. Однако что делать, если почтовый сервер
в этот момент недоступен? Сервис уведомлений может просто вернуть ошибку, но
что делать вызывающему сервису? Повторить вызов? А если повторить, то когда?
Такие ситуации трудноразрешимы и должны быть решены другим путем.
Дпя начала стоит ответить на вопрос: точно ли вызывающему сервису стоит знать,
что уведомление действительно отправлено? Вместо этого мы можем иметь другую
модель по типу «согласованности
в
конечном
итоге», т. е. рано или
поздно наше
уведомление будет доставлено, в разумных рамках, естественно. Это отсылает нас
к тому, что нам не обязательно дожидаться конца операции, мы можем просто соз­
дать запрос на выполнение и продолжить выполнять другие задания. Такой под­
ход -
отличный повод использовать очереди.
Вместо того чтобы синхронно отправлять запросы к сервису уведомлений, послед­
ний может слушать очередь, в которую вышестоящие сервисы смогут отправлять
запросы на уведомления. Например, если сервис столкнулся с ограничениями час­
тоты отправки уведомлений, благодаря наличию очереди сам запрос на уведомле­
ния со стороны других сервисов не потеряется. Сервис уведомления может сохра­
нить это событие в логах и попробовать построить попытку отправки позже. За
счет этого
вне зависимости
от количества запросов
на уведомления
мы
не столк­
немся с ситуацией, когда мы не можем обработать их все сразу и нам нужно воз­
вращать ошибки. Вместо этого мы можем достаточно органично обработать их, как
только появится такая возможность, не потеряв при этом самих запросов. Да, это не
Глава 5. Асинхронные взаимодействия
73
решает вопрос с тем, что в случае большой нагрузки у нас так или иначе возрастет
задержка обработки и отправки уведомления, однако она возросла бы в любом слу­
чае, только в нашем сценарии мы не столкнемся с потерей данных.
Очереди сообщений и бессерверные функции
Одним из красивых паттернов обработки сообщений является связка очередей и
бессерверных функций. Как вы помните из предыдущей главы про масштабирова­
ние, бессерверные функции позволяют выполнять небольшие объемы кода строго
по требованию, с моделью оплаты только за фактическое время исполнения. Как
правило, триггером исполнения таких функций являются НТТР-вызовы, однако
практически
все современные облачные
способы запуска, один из них -
провайдеры предоставляют различные
очереди сообщений (рис. 5.1 ). Например, при по­
явлении нового сообщения в очереди может запускаться бессерверная функция,
которая принимает это сообщение на вход, делает определенные преобразования и
далее удаляет его из очереди. За счет этого можно обрабатывать сообщения, с од­
ной стороны, достаточно быстро, а с другой -
эффективно с точки зрения денеж­
ных ресурсов. Однако не стоит забывать, что в каких-то ситуациях запуск бессер­
верных функций может стать более дорогим, чем запуск полноценного серверного
приложения.
/с/с/ с/ Сообщение/
Производитель
Отправка
Очередь
сообщений
- - - - Потреблен•е - - - •
по эапросу
Потребитель
Рис. 5.1. Принцип работы очереди
Чаще всего такую связку можно использовать при необходимости добавления не­
ких данных в базу данных, при этом источником событий является очередь, и нам
необходимо добиться, с одной стороны, минимальной задержки, а с другой денежной эффективности. Использование популярных языков программирования
для бессерверных функций, а также гибкость структур данных, которые можно
отправлять в рамках сообщения, позволяют быстро настроить нужные конвейеры
обработки данных.
Брокеры сообщений
В рамках программных технологий чем-то похожим на очереди особенно выделя­
ются брокеры сообщений. Хотя какие-то концепции между ними похожи, например
и там и там есть такие понятия, как производители и потребители, случаи исполь­
зования отличаются, и для определенных типов задач следует подбирать разные
технологии.
Часть /. Проектирование и разработка системы
74
В чем основное отличие брокеров сообщений и очередей (рис. 5.2)? Во-первых,
большинство облачных реализаций очередей, такие как А WS
SQS либо Azure
Service Bus, имеют набор функций, оптимизированный для соединения «точка­
точка». Да, некие паттерны использования позволяют расширить эту парадигму для
некого
мультиплексирования
между
сервисами,
но
основная
задумка
все
же
на­
правлена на соединения «точка-точка». Второй момент, связанный с очередями, -
это слабое развитие встроенных механизмов маршрутизации сообщений, например
разделение сообщений на разделы. Третье, на мой взгляд одно из самых веских, -
это отсутствие возможности повторять сообщения с определенного момента, по­
скольку после прочтения приложением сообщение обычно удаляется. Это может
быть критично, если, например, мы обнаружили ошибку в логике обработки собы­
тий, и после ее исправления нам нужно применить это изменение к старым дан­
ным -
как правило, в рамках очереди такое невозможно, поскольку все сообще­
ния, связанные с событием, были удалены еще при первой обработке. Четвертое трудности при множественном потреблении разными приложениями. Например,
мы хотим использовать одно и то же событие для триггера в нескольких приложе­
ниях. Поскольку простые очереди хранят состояния сообщений для целой очереди,
в случае если из нее будут читать несколько приложений, эти сообщения будут
разделяться между потребителями. Иными словами, ни одно из приложений в дан­
ном контексте не получит полного набора событий, т. е. будет происходить колли­
зия. Данная проблема решается созданием отдельных очередей для каждого серви­
са потребителей. Такой подход называется Fаn-Оut-паттерн, и он достаточно не­
плох для небольшого количества потребителей. Однако, если их становится много,
либо мы хотим иметь возможность динамически подключаться к очереди без соз­
дания новых ресурсов, это может быть проблематично, поскольку очередь на такое
не рассчитана.
Описанные особенности ни в коем случае не являются недостатками очередей, они
вполне перекрываются простотой использования и надежностью в рамках тех слу-
г------•
1
Сервис
аналитики
1
Производитель
Отправка ~
сообщений
Б
б
•
рокер соо щении
Сервис
доставки
Сервис
уведомлений
Рис. 5.2. Принцип работы брокера сообщений
Глава 5. Асинхронные взаимодействия
75
чаев использования, где очереди обычно применяются. Однако для ситуаций, где
этого недостаточно, разработчикам есть смысл присмотреться к брокерам сообще­
ний, которые предоставляют более глубокий функционал в обмен на большую
сложность. Поэтому давайте разберем его, а в качестве примера будем использо­
вать один из самых популярных и мощных брокеров сообщений -
Apache Kafka.
В первую очередь брокеры сообщений предоставляют большую гибкость работы.
Это выражается в возможности подключения множества потребителей события,
при этом без необходимости фактического дублирования сообщения. Например,
в рамках Kafka производитель указывает топик для отправки своего сообщения,
при этом из данного топика одновременно может выполнять чтение множество по­
требителей. Достигается это за счет того, что, хотя сообщение и хранится в единст­
венном экземпляре (без учета репликации), информация о потребленных сообще­
ниях сохраняется с помощью так называемых смеще11ий (offsets). Понятие смеще­
ния
полностью отражает свой
смысл
в своем
названии
вместо
-
механизма
очередей, где формально все видимые сообщения доступны для потребления, и
именно за счет их наличия определяется, были ли они обработаны ранее, в Apache
Kafka все работает иначе. Брокер сохраняет все сообщения как последовательность
и также сохраняет смещение относительно начала последовательности. За счет это­
го мы можем хранить множественные смещения для абсолютно разных потребите­
лей, которые могут динамически добавляться и отключаться. Одним из фундамен­
тальных понятий построения систем, основанных на событиях, является необяза­
тельное знание о принимающей стороне, отраженное в отправляющем сервисе.
Простыми словами, мы не всегда знаем, кому именно должно быть адресовано то
или иное событие, произошедшее в сервисе, но мы делимся им. Такой подход
уменьшает связанность между сервисами, и благодаря такому подходу, который
присутствует в брокерах сообщений, удается достичь большей гибкости в построе­
нии асинхронных систем.
Второй особенностью брокеров является маршрутизация сообщений. И хотя в рам­
ках
Kafka хитрые агрегации сообщений -
это не функционал самого брокера,
в ней все же присутствуют механизмы, позволяющие распределять сообщения. Как
мы уже знаем, брокер хранит информацию об обработанных сообщениях с помо­
щью смещений. Однако я специально не уточнил, к чему именно относятся эти
смещения. Во-первых, топик Kafka, как правило, разделен на партиции (partition),
между которыми распределяются сообщения. Их количество может быть довольно
большим, вплоть до нескольких сотен тысяч. Во-вторых, распределение между раз­
делами происходит далеко не случайно, в правильно настроенных системах это
происходит с помощью
ключа раздела,
на основании
которого
происходит опре­
деление раздела. Например, мы хотим, чтобы все сообщения, ассоциированные
с событиями пользователя, находились в одном разделе. В рамках работы брокера
сообщений мы должны указать ключ раздела равным уникальному идентификатору
пользователя, и за счет этого все одинаковые ключи будут направляться в одинако­
вые разделы. Для чего это может быть полезно?
Таким образом мы можем подойти к третьей особенности -
сохранению порядка
событий. Если в рамках обычных (не FIFO) очередей сохранение порядка чтения не
76
Часть /. Проектирование и разработка системы
гарантируется, т. е. мы можем потерять порядок событий, даже если они были дос­
тавлены в правильном порядке, брокер сообщений, а именно Apache Kafka, гаран­
тирует сохранение порядка сообщений в рамках одного раздела. Это особенно
удобно при обработке транзакционных данных, где сохранение порядка -
это
крайне важный момент, и за счет наличия множества разделов мы можем сохра­
нить производительность процесса, поскольку одновременное потребление из то­
пика может проводить количество потребителей, равное либо меньшее количеству
разделов.
За счет этого также обеспечивается четвертая особенность тельность. Такой брокер сообщений, как Apache
высокая производи­
Kafka, спроектирован для обра­
ботки потенциально любых объемов информации. А именно за счет механизмов
топиков, разделов, механики смещений, возможности запуска в распределенной
среде, а также механизма репликации данных между отдельными серверами и даже
центрами обработки данных (ЦОД) может быть обеспечена беспрецедентная про­
пускная способность кластера брокера сообщений. Эта особенность особенно вы­
ражена в приложениях потоковой обработки данных, которые, например, могут
считать самые разные потоковые метрики и агрегации, уведомляя пользователей
о необходимых событиях практически сразу, как они произошли. Системы стри­
минговой обработки данных имеют огромную популярность, поскольку позволяют
эффективно использовать преимущества брокеров сообщений и доставлять бизнес­
ценность. Так, в таких стримах могут обрабатываться клики пользователей на сай­
те, за счет этого можно не только считать обычные метрики, но и делать потоковые
предсказания,
например
стоит
ли
предложить
данному
пользователю
купон
на
скидку, чтобы увеличить вероятность покупки товара.
Одной из важных особенностей брокеров сообщений являются гарантии доставки
сообщений. Во-первых, Apache Kafka гарантирует запись сообщения без его дуб­
лирования, в противном случае брокер просто не даст прочитать это сообщение.
Во-вторых, брокеры сообщений поддерживают такие семантики чтения, как «как
минимум один раз», «строго один раз». Ключевой функцией для системы, в кото­
рой критически важно поддерживать согласованность данных, является поддержка
транзакций, когда мы можем совершить атомарную запись в несколько разных то­
пиков. Это особенно важно, например, в банковских приложениях либо приложе­
ниях обработки билетов.
Одним из популярных способов применения брокеров сообщений является переда­
ча данных. Существуют программные пакеты, такие, например, как Kafka Connect,
которые позволяют использовать брокер сообщений как промежуточный буфер
между двумя системами, когда необходимо обеспечить быструю передачу инфор­
мации из одной системы в другую. Допустим, мы хотим передавать новые записи,
которые появляются в базе данных, в несколько разных мест, например в объектное
хранилище, озеро данных и на дашборд, в котором будут располагаться самые
актуальные значения. Все это возможно с использованием брокеров сообщений.
Это выглядит примерно так:
□ к оригинальной базе данных подключается специальный экспортер, который
слушает лог транзакций базы данных и передает данные из него в топик;
Глава 5. Асинхронные взаимодействия
77
□ далее пул потребителей слушает этот топик и переносит информацию из него,
выполняя агрегации по требованию в целевые системы. Например, в случае ар­
хивной системы коннектор может сохранять не единичные транзакции, а окна
по одному часу или дню, что увеличивает эффективность алгоритмов сжатия и
ускоряет передачу файлов. Что касается дашборда с актуальными значениями,
приложение может хранить актуальные данные в оперативной памяти, предос­
тавляя их нижестоящим приложениям с минимальной задержкой, перед этим
восстановив свое состояние из топика брокера сообщений.
Данный подход даже имеет специальное название
- захват изменения данных
(Change Data Capture, СОС) и активно применяется в приложениях, в том числе и
монолитных, где необходимо добавить гибкости в вопросе уведомления о неких
событиях, произошедших в том или ином источнике данных.
Резюмируя: в каких случаях есть смысл использовать брокеры сообщений?
□ Мы хотим обеспечить большую пропускную способность и иметь возможность
обрабатывать много сообщений большим количеством потребителей. Благодаря
распределенной природе брокеров сообщений и их правильной настройке мы
лимитированы только количеством серверов и пропускной способностью сети.
□ Нам необходимо иметь возможность гибкого подключения новых потребителей
для наших событий.
□ Нам интересна потоковая аналитика, такая как агрегации на временных рядах,
поиск аномалий, быстрое реагирование на события, связанные с действиями
пользователей либо с самой системой.
□ Помимо сохранения порядка событий, нам необходимо сохранить возможность
параллельной обработки.
Однако в ситуациях, где нам достаточно просто иметь буфер, в котором будут хра­
ниться некие события, и количество потребителей ограничено (либо он в принципе
один), имеет смысл воспользоваться очередями, а не брокерами. Связано это в пер­
вую очередь с тем, что брокер сообщений может требовать большее количество
ресурсов, его настройка, как правило, сложнее, что вкупе выливается в большую
стоимость владения технологией.
Примеры использования брокеров сообщений
Предположим, что у нас существует система, которая способна выполнять фикса­
цию всех действий пользователя на платформе. Например, мы можем сохранять
информацию о времени нахождения, посещенных страницах, нажатых кнопках.
Подобный формат данных принято называть кликстримом (от англ.
c\ickstream),
т. е. потоком нажатий. Такая информация может быть очень полезна с точки зрения
аналитики, мы можем выполнять расчет воронок продаж, глубже изучать поведе­
ние пользователя на сайтах. Также мы можем выполнять предиктивную аналитику,
например пытаться предсказать, уйдет ли пользователь с сайта сейчас, и если да, то
мы
можем
вовремя предложить ему скидку
и
повысить шанс его
вовлеченности.
Третьим сценарием использования может быть умное кеширование. Например,
78
Часть /. Проектирование и разработка системы
в зависимости от посещенных страниц мы можем выполнять динамическое обнов­
ление кеша, забирая информацию из истории кликов пользователя, также делая это
заранее, в том числе для тех разделов, которые он мог еще не загружать. И для под­
держания историчности было бы неплохо выполнять сохранение подобной актив­
ности, чтобы иметь возможность вернуться к ней в будущем.
Применение брокеров сообщений может помочь нам решить все эти задачи, при
этом используя фактически один интерфейс, который подойдет и для множества
новых интеграций. Производителем событий в рамках этой системы будет клиент­
ское приложение, которое напрямую либо посредством API (что предпочтительнее)
будет отправлять сообщения о событиях в топик брокера. За счет использования
ключей раздела мы можем выполнять группировки по разделам, например исходя
из уникальных пользователей, что в случае аналитики будет хорошим решением.
Благодаря тому, что брокеры сообщений поддерживают мультиплексирование, мы
можем иметь множественных потребителей для одного источника событий. На­
пример, один из них может быть аналитическим сервисом, который будет читать
топик и выполнять оконные агрегации, вычисляя такие метрики, как средняя кон­
версия, количество уникальных пользователей, среднее время сессии, и множество
других метрик, которые могут быть полезны бизнесу. Второй сервис может зани­
маться обновлением кеша в зависимости от посещенных пользователями страниц
платформы. А третий сервис -
это сервис машинного обучения, который будет
получать на вход информацию о действиях пользователя и с помощью потокового
обучения моделей будет предсказывать, какова вероятность ухода пользователя
с платформы, являясь триггером компенсирующих действий, повышающих кон­
версию платформы. И под конец мы можем выполнять потоковое архивирование
кликов пользователей. Например, раз в сутки мы можем запускать специальный
скрипт, который будет выполнять чтение всех действий за сутки, сжимать их и со­
хранять в объектное хранилище для того, чтобы впоследствии у аналитиков была
возможность выполнить исторический анализ.
При этом подобные системы могут работать полностью независимо друг от друга,
запускаться в разное время и иметь свои собственные циклы разработки. Также
такая система может быть легко расширена за счет унифицированности самого
брокера. Если кому из других сервисов может понадобиться информация о потоке
кликов пользователя, он может просто подключиться к потоку и извлечь нужную
информацию из него.
Веб-хуки
Данная категория отличается от перечисленных выше, причем она не требует от
нас каких-то специфических внешних сервисов для реализации. Какие еще трудно­
сти, помимо буферизации событий, сопутствуют асинхронным системам? Как пра­
вило, это связано с необходимостью нотификации вызывающих систем. В рамках
синхронных взаимодействий это не является проблемой: как правило, мы держим
соединение открытым и просто передаем результат обработки вместе с ответом.
В случае с очередями мы можем иметь выходную очередь, в которую записывают­
ся результаты обработки, которые впоследствии могут быть прочитаны вызываю-
79
Глава 5. Асинхронные взаимодействия
щими системами. Все это так или иначе требует специфических программных
средств, а именно развернутых и настроенных очередей или брокеров, но есть ли
другой способ? И, как ни странно, он есть.
Подход веб-хуков невероятно прост и гибок и основывается, как ни странно, на
парадигме синхронных вызовов. Как правило, он реализуется с помощью НТТР. Но
как синхронные вызовы
при
правильном
подходе
обеспечивают асинхронные
взаимодействия?
Веб-хук -
это, по сути, синхронный НТТР-вызов, который выполняется не клиен­
том, а самим сервером (рис. 5 .3 ). Например, на стороне сервера произошло некое
событие, о котором необходимо уведомить какую-либо систему, например кто-то
подписался на группу пользователя в социальной сети, либо пользователю пришла
посылка. Для того чтобы выполнить это самое уведомление, сервер инициирует
НТТР-запрос по заранее определенному пользователем адресу, на стороне которого
развернут веб-сервер, который будет способен принять и обработать этот НТТР­
запрос. Таким образом, веб-сервер практически моментально может уведомить
заинтересованную сторону, и все, что требуется от этой стороны, это наличие ко­
нечной точки НТТР, которую можно будет передать серверу в качестве конечной
точки веб-хука.
Пользователь
,___
_, ,,ервичный запрос-
Вебсервер
Запуск фоновой задачи
,_____
Пользователь
в:~::::;:е~ам_ _ __,<Вебсереер)...,,.1-----З-а-ве_р_w_е~ниlе процесса
запрос
.
.
~-----~
Рис. 5.3. Принцип работы веб-хуков
Многие современные сервисы имеют свои собственные API для веб-хуков. Напри­
мер, такая платформа, как GitHub, имеет в своем арсенале глубоко настраиваемые
механизмы веб-хуков, которые сам GitHub исполняет при наступлении определен­
ных событий на платформе. Одним из самых частых сценариев является веб-хук
о создании запроса на слияние или добавление новых коммитов в целевую ветку.
В случае наступления этих событий GitHub выполняет НТТР-запрос с необходимой
информацией на наш CI- или СО-сервер, который может скачать актуальную вер­
сию репозитория, собрать новую версию программного обеспечения, протестиро­
вать ее и развернуть.
Механизм веб-хуков можно использовать не только в публичных
API, но и для
внутренних нужд вашего проекта. Например, вместо использования выходной оче-
80
Часть /. Проектирование и разработка системы
реди для результатов обработки вы можете передавать результаты напрямую с по­
мощью НТТР, используя конечные точки той стороны, которая изначально по­
местила сообщение в очередь. Такой подход значительно проще и подходит для
простых систем, однако нельзя забывать, что он может сопутствовать потере сооб­
щений, если, например, отправляющая веб-хук сторона не смогла достучаться до
принимающей. В таком случае нужно определить, что делать с сообщениями, кото­
рые не удалось до конца передать, -
отправлять их в DLQ либо пытаться отправ­
лять их, пока принимающая сторона не будет доступна. Стоит отметить, что такие
настройки, как тайм-аут и количество попыток отправки, должны быть явно из­
вестны и понятны, чтобы понимать поведение веб-хук-системы, если принимающая
сторона не может принять запрос в данный момент. Достаточно часто после опре­
деленного количества попыток при явной недоступности хоста веб-хук-система
может в принципе перестать отправлять запросы для эффективного использования
ресурсов. Все это необходимо определить перед разработкой, чтобы создавать ус­
тойчивые и прозрачные системы.
Стоит повторно отметить, что для реализации веб-хуков мы обязаны иметь стати­
ческую внешнюю конечную точку НТТР, которая будет доступна для вызывающей
стороны. В общем плане это может быть конечная точка сервера с публичным IР­
адресом, в более узком мы можем передавать веб-хуки в рамках приватных сетей,
однако оба эти случая объединяет наличие прямого доступа к конечной точке.
В случае, если достичь такое не получается, например когда у нас есть клиентское
приложение, развернутое на устройстве пользователя, не имеющее внешней конеч­
ной точки, нам необходимы другие механизмы, о которых пойдет речь дальше.
Опрос
Нельзя сказать, что данных подход напрямую относится к асинхронным взаимо­
действиям, однако он позволяет эмулировать асинхронные взаимодействия в си­
туациях, где проблематично разворачивать дополнительные механизмы, например
когда у нас нет доступа к очередям либо доступного извне интерфейса связи.
Идея опроса (polling), на мой взгляд, еще более простая и гениальная, чем веб­
хуков. Сам подход заключается в том, что мы можем синхронными путями опра­
шивать веб-сервер, например с помощью GЕТ-запросов в рамках REST, пока нуж­
ная информация не появится (рис. 5.4).
Например, мы отправили текстовый промпт на генерацию изображения с помощью
нейронной сети. Как правило, это требует времени, может потребоваться около
1О секунд. С одной стороны, мы можем иметь синхронное соединение и просто
держать его открытым, пока веб-сервер не сгенерирует изображение. А с другой,
что будет, если мы захотим перезагрузить страницу или сделать несколько запро­
сов одновременно? При таких сценариях появляется шанс разрыва нашего соеди­
нения, что нарушит пользовательскую историю. Вместо этого можно пойти другим
путем.
При отправке запроса на генерацию картинки веб-сервер может создать некую аб­
стракцию, вроде задания на обработку, и ассоциировать с ним идентификатор.
81
Глава 5. Асинхронные взаимодействия
Данный идентификатор ассоциирован с полем в базе данных, которое отражает его
статус, например что изображение еще генерируется, либо оно уже сгенерировано,
либо что произошла непредвиденная ошибка. Используя этот идентификатор, наше
клиентское
приложение
может
выполнять
периодический
опрос
приложения,
например раз в несколько секунд, и ждать определенного статуса. После того как
генерация готова, веб-сервер меняет статус на «успешно», после чего клиентское
приложение может запросить его результаты. С помощью такого трюка можно соз­
давать ощущение динамического обновления данных, если интервал запроса доста­
точно маленький.
Зanpocc-------tf...-Веб-сероер)
Пользователь
Пустой ответ
\
.
t--------.Зanpoc-------+-/
Веб-сервер
Пользователь
Пустой ответ
Фоновое
действие
выполнено
1~---.дзаанnнроы:е------..(Веб-сероер)
Пользователь, 4
_
~
.
Рис. 5.4. Принцип работы опроса
Одним из частых случаев опроса является так называемый длинный опрос (long
polling). Данный механизм встречается не только в обычных приложениях, он так­
же активно применяется при опросе брокеров сообщений и очередей на предмет
новых сообщений для потребления. Он основывается на том, что принимающая
сторона не сразу возвращает информацию о статусе. Вместо этого мы можем как
бы «придержать» соединение и ждать обновления информации на стороне сервера.
Как только эта информация обновится, например обновится запись в базе данных,
мы можем вернуть пользователю ответ в рамках уже открытой НТТР-сессии, сни­
зив то
время,
когда клиентская
сторона простаивает между
интервалами
опроса.
Это крайне эффективный механизм, который позволяет минимизировать время за­
держки,
поскольку
в рамках задержки
присутствуют не только интервалы
между
опросами, но и время на открытие нового НТТР-соединения, опросов базы данных
ит. д.
Как и всегда, подход опроса не лишен недостатков. Во-первых, это лишние НТТР­
вызовы, которые часто могут возвращаться пустыми. Вторая проблема -
это про­
блема задержки: мы лимитированы интервалами опроса, и даже в случае длинного
опроса, если какое-то событие выпадет на этот интервал ожидания с клиентской
Часть /. Проектирование и разработка системы
82
стороны, она будет уведомлена о нем не сразу. В каких-то случаях это не проблема,
однако в некоторых приложениях задержка особенно критична, например в прило­
жениях аукционов. И для компенсации этих особенностей существуют еще два
подхода: веб-сокеты и серверный стриминг.
Веб-сокеты
Большинство взаимодействий между сервисами в Интернете построены по прин­
ципу «запрос-ответ», т. е. у нас есть сторона, которая запрашивает информацию,
и сторона, которая может эту информацию предоставить. Это отлично работает для
клиент-серверных приложений, например дЛЯ браузера и веб-сервера, где фронтенд
определяет, какую именно информацию необходимо запросить у бэкенд-приложе­
ния, и бэкенд может ее предоставить. Однако что делать, если сам бэкенд хочет
«протолкнуть» какую-то информацию в клиентское приложение, т. е. вызвать его
самостоятельно? Ввиду особенностей работы Интернета с применением, например,
парадигмы REST, это невозможно, т. к. дЛЯ получения некой информации именно
клиентская сторона должна вызвать веб-сервер. Поэтому существуют расширения,
которые обеспечивают двунаправленную связь, и называется этот подход «веб­
сокет» (от англ. WebSocket).
Веб-сокет существует в рамках открытого НТТР-соединения, и за счет того, что
оно держится открытым, возможен двунаправленный канал связи между клиентом
и сервером (рис. 5.5). Такая парадигма открывает большую гибкость в том, как сер­
верные события могут быть переданы пользователю:
□ представим, что у нас есть мессенджер. С помощью веб-сокетов сервер может
моментально передавать пользователю новые сообщения, которые поступили
пользователю,
и,
используя то
же соединение,
сам
пользователь
может также
отправлять уже свои собственные сообщения;
□ сервер может передавать метрики бэкенд-приложения с минимальной задерж­
кой, уведомлять о каких-либо событиях на серверной стороне;
□ при этом пользовательскому приложению не требуется постоянно отправлять
новые запросы, ему достаточно слушать веб-сокет-соединение (отправка сигна­
лов
проверки работоспособности,
как
правило,
происходит
автоматически)
и реагировать на то, что передал сервер.
Стоит отметить, что похожий функционал имеется у gRPC, а именно: данное со­
единение двунаправленное. Однако в случае работы в браузере пользователя веб­
сокет -
это популярный выбор множества разработчиков, если необходимо обес­
печить быструю и двунаправленную связь.
Веб-сокеты, как правило, более требовательны к ресурсам и стабильности Интер­
нета, и это также необходимо учитывать при построении систем.
Также существует еще одна альтернатива это потоковая обработка (Streaming
Responses ). В ситуациях, в которых у нас нет необходимости отправлять что-то на
сервер, а нам необходимо именно слушать, при этом иметь минимальную задерж­
ку, отлично подходит серверная потоковая обработка. Она заключается в том, что
83
Глава 5. Асинхронные взаимодействия
вместо конечного ответа сервер возвращает нам поток, который может периодиче­
ски возвращать полезные для нас события. Их обработка позволяет также быстро
получить доступ к актуальным серверным данным без необходимости делать до­
полнительные запросы с клиентской стороны. Этот подход популярен в современ­
ных GеnАI-приложениях, связанных с большими текстовыми моделями, которые
возвращают генерацию кусочками (если быть точным, то по токенам), и с помощью
потоковой обработки мы можем получить плавную генерацию кусочками на кли­
ентской стороне без необходимости ждать полной генерации на стороне сервера,
передав пользователям полезную информацию практически моментально.
Пользователь
Веб-сервер
о----...,,нициациясоединения-----4~
----Деунаправnенная свя~ь-----м
Рис. 5.5. Принцип работы веб-сокетов
Заключение
При выборе паттернов коммуникации важно в первую очередь оценивать саму
задачу. Не существует идеально подходящих под все решений, так или иначе суще­
ствуют ограничения. И в тех ситуациях, где синхронные взаимодействия не позво­
ляют полноценно перенести семантику бизнес-процесса, асинхронные подходы
могут успешно прийти на помощь.
Они являются основой для разделения приложений, уменьшения связанности про­
цессов, а также увеличения стабильности работы систем.
Как уже говорилось
ранее, лучше всего они применяются в тех случаях, где сама бизнес-логика имеет
похожий принцип, например отложенные отправки уведомлений, фоновая обра­
ботка данных, аналитика.
Но при этом нельзя забывать, что подобный способ накладывает определенные
ограничения. Во-первых, он может требовать участия дополнительных сервисов,
84
Часть /. Проектирование и разработка системы
которые будут брать на себя роль посредника, и для правильной работы с ними
может потребоваться определенная квалификация у команды. Вторым моментом
является большая сложность таких взаимодействий, например трудности в отлав­
ливании ошибок и их обработке, а также потенциальные трудности при локальной
разработке, где нам необходимо воссоздавать сложное окружение. Такие же труд­
ности будут связаны и с более сложными тестовыми сценариями.
Умелое комбинирование синхронных и асинхронных подходов может быть крайне
полезным в рамках проекта. Например, в тех местах, где нам необходима мини­
мальная задержка, мы можем использовать классические синхронные интерфейсы,
а в ситуациях, в которых нам не обязательно моментальное получение данных и
нужна фоновая обработка, есть смысл рассмотреть использование асинхронных
архитектур.
11111
111111
ГЛАВА 6
11!1
Кеширование
Есть такая любопытная цитата: «В
темы -
Computer science есть только две сложные
инвалидация кеша и именование вещей». И если с последним все понятно
(это правило распространяется, на мой взгляд, не только на П), то первая проблема
гораздо более глубокая и в своем роде уникальная.
Современные программные системы могут сталкиваться с огромным объемом за­
просов и трафика, который обрабатывается сложной бизнес-логикой множеством
сервисов. И нередко наступает момент, когда мы упираемся в потолок произво­
дительности. Привычные нам способы оптимизации запросов могут не работать
для нас, и приложение в лучшем случае может работать медленно, а в худшем падать из-за нагрузки.
Пользователям, как правило, не интересно, сколько людей параллельно пользуется
платформой сейчас,
-
им важно получить свой собственный пользовательский
опыт, решить свою конкретную задачу. Все это подводит к тому, что необходимо
придумывать различные механизмы, которые помогут ускорить работу системы и
оптимизировать ее работу, чтобы беспрепятственно работать с большим количест­
вом пользователей и серьезной нагрузкой.
Одним из таких механизмов является кеширование. Вероятнее всего, эта концепция
уже знакома читателям, однако мы попробуем разобрать ее с разных сторон и изу­
чить, как она применяется, реализуется, и как именно кеширование может помочь
увеличить пропускную способность нашего приложения на ответственных участ­
ках.
Смысл и основные виды кеширования
Поверхностный смысл кеширования понятен -
это про ускорение работы системы
посредством сохранения определенных промежуточных данных в близких для
пользователя местах. Однако важно понимать, какие процессы занимают время
в момент клиент-серверного взаимодействия и как они поддаются оптимизации.
Для начала нужно разобраться, какие именно места поддаются оптимизации с по­
мощью кеширования. Выделяют клиентское кеширование, кеширование на уровне
сервера, а также стоит выделить кеширование на уровне базы данных.
86
Часть /. Проектирование и разработка системы
Клиентский кеш
Большая часть современных платформ имеет клиентскую часть, чаще всего это
может быть UI, мобильное либо десктопное приложение. UI - это механизм, обес­
печивающий представление информации в удобной для пользователя форме и по­
зволяющий управлять этими данными с помощью привычных пользователю кана­
лов управления. В большинстве своем данные попадают в приложение с сервера,
и во время работы с приложением может происходить периодическое обновление,
новые запросы на изменение данных и т. д.
На начальных этапах эксплуатации, когда количество пользователей невелико либо
ими выступают сами разработчики, веб-сервер может достаточно эффективно
справляться как с предоставлением данных по запросам, так и с раздачей статиче­
ского контента, такого как изображения и стили. Однако стоит помнить, что каж­
дый запрос, пусть даже и небольшой, -
это трафик, который будет поступать из
локальной сети сервера, это оперативная память, которая будет занята у сервера во
время обработки, это процессорное время, которое затратит сервер на обработку
нашего запроса. И со временем, если мы неэффективно организовываем передачу
данных между клиентом
и сервером, когда количество пользователей нашего
приложения станет расти,
мы сможем заметить замедление
производительности.
Да, возможно, это очевидные вещи, однако о том, что мы ограничены вычис­
лительными и сетевыми ресурсами, нередко забывают, потому что современные
серверы даже в относительно слабых конфигурациях могут обрабатывать сотни
запросов.
Один из вариантов, который стоит продумывать при разработке, это общая
оптимизация запросов, мы не должны включать избыточные данные при передаче
с намеком на то, что они могут пригодиться нам в будущем. Такой подход не толь­
ко уменьшит количество расходуемого трафика, но и сократит нагрузку на сервер,
поскольку ему будет необходимо собирать меньше данных. Однако это общий ва­
риант -
как тут может пригодиться кеширование?
В первую очередь стоит поговорить о статическом контенте. Приложение может
иметь функционал по отображению изображений, сохранять стили и клиентский
код для фронтенд-библиотек, если мы говорим о веб-приложениях. Отличитель­
ной особенностью является то, что подобные данные меняются нечасто. Если мы
не стриминrовая платформа, а, например, интернет-магазин, мы определенно мо­
жем обновлять изображения товаров, однако это гораздо более редкое событие,
чем, например, добавление новых товаров, изменение их цены и т. д. То же самое
касается файлов стилей, JavaScript-кoдa, который может не изменяться довольно
ДОЛГО.
Когда пользователь заходит первый раз в само приложение, веб-сервер предоставит
ему большое количество данных, в которые будет входить статический контент.
Ключевая особенность заключается в том, что его можно переиспользовать для
разных сценариев работы вместо того, чтобы запрашивать каждый раз снова. На­
пример,
когда
мы
загружаем
страницу
товара
и
делаем
перезагрузку
страницы,
имеет смысл каждый раз обновлять стоимость товара, потому что это важная и от-
Глава б. Кеширование
87
носительно часто изменяющаяся информация. Однако само изображение можно
сохранить на клиентской стороне для того, чтобы при повторном рендере страницы
вытянуть ее из внутреннего хранилища, а не скачивать повторно с веб-сервера.
Такой подход имеет множество плюсов. Во-первых, повторные запросы не будут
нагружать сервер, поскольку клиент будет запрашивать значительно меньше ин­
формации, особенно с точки зрения трафика, потому что статический контент мо­
жет весить много. Во-вторых, само приложение также будет работать быстрее вви­
ду того, что нужный контент уже находится на самом устройстве и нет необходи­
мости ждать, пока он будет загружен.
Подход клиентского кеширования особенно популярен в мобильных приложениях.
Например, если мы создаем приложение навигации, есть смысл добавить для поль­
зователей возможность кешировать участки карт. Это не только снизит нагрузку на
сервер, но и даст возможность использовать карты без покрытия сети, что очень
хорошо вписывается в пользовательские истории путешественников.
Однако стоит помнить и про обратные стороны. Во-первых, такой кеш может за­
нимать место на клиентском устройстве, из-за чего может падать производитель­
ность приложения. Во-вторых, это общая проблема для кеша -
его устаревание и
инвалидация. Как работать с этим, мы поговорим отдельно. Также важно. помнить
про безопасность: если мы кешируем пользовательские данные, важно быть уве­
ренным, что к ним не получит доступ сторонний человек.
Серверный кеш
Часто не получается интегрировать сложную логику кеширования на сторону кли­
ентского приложения, например из-за лимитов на хранение либо особенностей
безопасности. Поэтому мы можем подняться на уровень выше и кешировать дан­
ные на уровне сервера и приложения. Данный вид кеширования достаточно рас­
пространен в высоконагруженных системах, которые обрабатывают тысячи за­
просов в секунду. Его реализация может быть разнообразной, может затрагивать
разные аспекты приложения, однако общая логика следующая
-
хранить для
быстрого доступа те данные, которые нельзя получить быстро, но которые крити­
чески важны для работы приложения.
В первую очередь стоит сказать, что именно кешируется на стороне сервера. Во­
первых, если мы занимаемся разработкой веб-приложений, которые используют
рендеринг страниц на стороне сервера, хорошей практикой может считаться кеши­
рование этих страниц. Да, нередко они наполняются динамически с помощью кон­
тента из базы данных, однако если он изменяется нечасто, и у нас нет возможности
кешировать данные на пользовательских устройствах, мы можем выполнять кеши­
рование уже собранных страниц вместо их повторной сборки, поскольку сама
сборка может требовать определенных вычислительных ресурсов. Взять уже гото­
вое в этом случае будет быстрее. Однако такой сценарий не самый распространен­
ный, но про него все равно стоит знать.
В свою очередь, когда речь заходит о кешировании, среднестатистический разра­
ботчик скорее представляет так называемое кеширование данных и объектов.
Часть /. Проектирование и разработка системы
88
До этого мы уже упоминали, что операционные данные приложения хранятся в ба­
зах данных, и с помощью различных запросов мы можем выполнять поиск, агрега­
ции и проверки, если речь идет о чтении. В системах, которые не имеют много дан­
ных, причем как с точки зрения структуры, так и с точки зрения их общего количе­
ства, обычно скорость чтения может быть достаточно высокой. Для современных
серверов не проблема работать с сотнями тысяч строк и делать агрегации на них.
Однако со временем, например с ростом количества пользователей, усложнением
структуры данных и увеличением
их количества,
эта ситуация
может начать
ме­
няться в худшую сторону.
Операции чтения и агрегации не бесплатны для базы данных, она затрачивает на
них определенные ресурсы, и, если этих ресурсов не хватает, база данных начнет
медленнее обрабатывать поступающие запросы. Но даже если запросов немного, на
больших объемах данных сами запросы могут выполняться долго. Например, если
мы хотим предоставить пользователям аналитику за последние пару лет, то такой
запрос
при
самом лучшем
раскладе может занять от нескольких до десятков
се­
кунд. На практике я видел случаи, где один SQL-зaпpoc мог выполняться до не­
скольких часов. Естественно, для операционных данных это значительно более
редкий сценарий, однако даже ожидание в 4-1 О секунд для пользователей может
быть ощутимым, особенно если это ожидание происходит при каждой загрузке но­
вой страницы.
Соответственно, у нас есть две проблемы: потенциальный потолок в производи­
тельности, связанный с тем, что база не всегда может быстро найти нужную ин­
формацию, и длительные агрегации, при этом эти проблемы взаимно усиливают
друг друга. Естественно, есть и другие варианты, как можно бороться с этим, не
прибегая к кешированию. Никто не отменял оптимизацию запросов, материализа­
цию таблиц, увеличение мощности базы данных, выбор других семейств баз дан­
ных. Однако нередко в какой-то момент база данных уже может быть самой мощ­
ной, запросы достаточно оптимизированы, а миграция данных на новое решение
может нас не устраивать по причине привязки к определенным функциям БД либо
структуре данных. И тут на помощь как раз и может прийти кеширование.
Идея заключается в том, чтобы временно хранить результаты запросов и агрегаций
в некой специальной БД, которая будет оптимизирована для быстрого взятия дан­
ных. Примерный порядок действий может быть таким:
□ пользователь запросил определенный аналитический отчет, на сборку которого
база данных может потратить до пяти секунд;
□ после того как отчет сформирован, вместе с передачей пользователю мы можем
сохранить результаты в специальной, отдельной базе, которая будет хранить
в себе идентификатор этого отчета;
□ при последующих запросах пользователя вместо того, чтобы каждый раз агреги­
ровать новые данные, мы можем ходить в хранилище кеша и извлекать резуль­
таты из него;
□ в случае если в кеше нужных данных не нашлось, мы можем обратиться к хра­
нилищу
операционных
данных,
выполнить
в хранилище кеша, и так по кругу.
агрегацию
и
поместить
результат
Глава б. Кеширование
89
Подобный паттерн особенно полезен в случаях, если разные пользователи могут
запрашивать одни и те же данные. Например, сотрудники одной организации могут
просматривать одинаковые аналитические данные, причем либо в одно, либо в раз­
ное время. Множественные запросы могут негативно повлиять на производитель­
ность нашей базы данных, поскольку, несмотря на то, что сами запросы могут быть
одинаковыми, база данных все равно может выполнить их независимо. Дпя того
чтобы оптимизировать эту часть, часто используемые отчеты можно сохранять
в кеше по уникальному идентификатору, и с помощью этого идентификатора мы
можем передавать уже посчитанную информацию для пользователей.
Стоит пару слов сказать о том, какие именно базы данных подходят для этого. Во­
первых, такие решения должны иметь простую и понятную структуру данных, ча­
ще всего это могут быть базы данных «ключ-значение», поскольку это соотносится
с логикой работы кеша: у нас есть некая сущность, которую можно однозначно
идентифицировать, и мы можем сохранять и запрашивать ее по этому идентифика­
тору, иными словами, ключом будет идентификатор, а значением -
полезная на­
грузка. Во-вторых, важным является обеспечение скорости доступа. И эту часть
можно разобрать подробнее.
Базы данных для работы с кешем, как правило, не хранят данные на жестком диске,
а сохраняют их в оперативной памяти, выполняя только периодические синхрони­
зации. Связано это со скоростью работы -
даже самый быстрый SSD-диск будет
работать медленнее, чем средняя оперативная память, причем эта разница может
быть на порядок. Связано это с особенностями работы самой оперативной памяти,
но главным тут является то, что объекты, которые хранятся в ней, можно достать
значительно быстрее. Эта особенность активно используется в работе с кешем.
Благодаря этому можно достичь микро- либо миллисекундной задержки доступа
к данным. Такой подход также оптимизируется с помощью структур данных, кото­
рые лежат в основе баз данных «ключ-значение», например используются хеш­
таблицы.
При этом для реализации такого сценария может быть необязательным использова­
ние баз данных. Например, кеширование можно выполнять прямо на стороне при­
ложения, в его памяти. Такой функционал можно реализовать как самостоятельно,
пользуясь встроенными средствами языка, так и с применением фреймворков, ко­
торые обеспечивают более удобный интерфейс для работы с кешем.
Плюсы данного подхода достаточно понятны: увеличение скорости работы_ прило­
жения, снижение нагрузки на базу, большая потенциальная пропускная способ­
ность. Однако, естественно, присутствуют и минусы. Во-первых, никуда не пропа­
дает проблема, связанная с устареванием кеша. Например, если в рамках отчета
необходимо отразить те данные, которые поступили после формирования кеша,
такой отчет будет считаться устаревшим и может не подходить под требования ор­
ганизации. То же самое может касаться множества других сущностей, которые
обычно кешируют: пользовательские сессии, иные запросы в базу данных, резуль­
таты вычислений самого приложения. Как именно с этим разбираться, мы погово­
рим в отдельной главе, поскольку эта проблема, свойственная всем кешам. Вторая
проблема связана со сложностью реализации. Во-первых, кеширование требует из-
Часть /. Проектирование и разработка системы
90
менений в коде, которые будут отражать логику работы с кешем, например это мо­
гут быть запросы к базам данных либо к внутреннему кешу. В случае с комплекс­
ными приложениями нам необходимо будет не только реализовывать операцион­
ную логику, но и логику кеширования в том числе. Помимо простой записи и чте­
ния,
нам
также
нужно
продумать
логику
инвалидации,
т. е.
устаревания
кеша,
поскольку далеко не во всех случаях мы можем обойтись какими-либо простыми
решениями. Вдобавок к этому, если мы используем внешние базы данных для ке­
ша, на наши плечи ложится задача управления и поддержки, что, к слову, частично
решается, если использовать решения, которые предоставляются облачными про­
вайдерами. И третьим пунктом можно назвать то, что подобные системы могут по­
требовать определенных мощностей, которые оптимизированы с точки зрения опе­
ративной памяти. Например, если мы используем кеш на стороне нашего приложе­
ния, нужно быть готовыми расширить объемы используемой памяти для того,
чтобы этот функционал принес пользу. Это может выливаться в дополнительную
стоимость содержания инфраструктуры, что тоже нужно учитывать при проектиро­
вании систем.
Если резюмировать, серверный кеш -
это мощный инструмент, который позволяет
увеличить пропускную способность приложения. Его реализация может потребо­
вать определенных усилий в случаях, если кеш затрагивает логику приложений,
однако серверное кеширование -
это хороший способ ускорить, например, дос­
тавку часто используемого статического контента либо данных аналитики. Сами
приложения могут использовать кеш для хранения горячих операционных данных,
однако нельзя забывать про аннулирование, чтобы не столкнуться с проблемой
устаревших данных.
Кеширование на уровне баз данных
Вкратце поговорим про этот способ кеширования. Кеширование на стороне серве­
ра, особенно если это связано с программной логикой, приводит к необходимости
реализации через код, управлению дополнительной инфраструктурой, прочими ак­
тивностями, которые нельзя пропустить. Однако мы можем спуститься на уровень
данных и попробовать реализовать кеширование там, что может быть неплохой
идеей в определенных ситуациях.
Очевидно, что сами данные, которые хранятся в базе, используются неравномерно.
Если это общая пользовательская информация, она будет запрашиваться чаще, если
же данные нужны для редких административных целей, они будут просто спокойно
храниться. Ввиду этого может показаться логичным либо хранить часто запраши­
ваемые данные в удобной форме, либо кешировать популярные запросы.
Идея при этом крайне похожа на кеширование на стороне сервера, за тем лишь ис­
ключением, что она происходит на стороне базы данных, а сами приложения могут
даже не знать о том, что получают кешированные данные. За счет этого наследует­
ся множество преимуществ, например увеличивается пропускная способность, ско­
рость доступа к информации, ввиду отсутствия повторения сложных агрегаций
уменьшается нагрузка на базу данных. Однако есть и обратная сторона.
Глава 6. Кеширование
91
Классическая проблема инвалидации кеша в случае кеширования на стороне БД
приобретает другой поворот. Во-первых, проблема устаревших данных напрямую
влияет на АСID-свойства, нарушая согласованность данных. Когда у нас появляет­
ся еще одна копия данных, она может не успеть обновиться до момента повторного
обращения к ним, из-за чего могут произойти несогласованные чтения. Если эту
проблему можно было решать на уровне кода, который мог быть готов к несогласо­
ванным чтениям, то, если приложение либо, точнее сказать, разработчики, не зна­
ют, что база данных имеет внутренние механизмы кеширования, это может привес­
ти к ошибкам на стороне платформы.
Второй момент, который вытекает из этого, -
это сложность настройки и понима­
ния. База данных может иметь глубокие механизмы управления кешем, сложные
правила его инвалидации, и все это может вьmиваться в сложные настройки, кото­
рые нельзя просто оставить по умолчанию. Каждая из таких настроек может влиять
на стабильность работы приложения, поэтому перед тем, как интегрировать кеши­
рование на уровне базы данных, важно хорошо разбираться в том, как эта база дан­
ных работает и как именно механизм кеширования влияет на ее работу. Нередко
поэтому многие базы данных, особенно соблюдающие ACID, могут вовсе не иметь
механизмов встроенного кеширования, ограничиваясь более эффективным исполь­
зованием структур данных и их агрегацией на диске.
Поэтому кеширование на стороне уровня хранения может быть хорошим решением
для аналитики, где нам не так важно получать самые свежие и актуальные данные,
при этом мы не хотим усложнять серверный уровень, реализуя кеширование на его
стороне. Главное -
учитывать сложность настройки, а также последствия, которые
могут появиться при добавлении кеша на уровень базы данных.
Инвалидация кеша
Во время краткого обзора основных типов кеширования среди проблем для каждо­
го из этих типов упоминалась проблема устаревания кеша. Старые данные могут
вести к несогласованной работе приложения и неправильным выводам, которые
пользователи делают на основе этих данных, поэтому процесс обновления кеша
является важной задачей. Однако помимо важности она также является очень не­
очевидной и в какой-то степени противоречивой, поскольку тут не бывает одного
явного подхода, который будет идеален для всех.
С одной стороны, слишком частое обновление кеша либо короткий период его
жизни могут нарушить один из смыслов его добавления -
ускорения системы.
Если наш кеш невозможно использовать достаточно долго в зависимости от биз­
нес-задачи приложения, мы не сможем полноценно извлечь его пользу, поскольку
сама система все равно будет обращаться в места, где хранятся данные, для выпол­
нения поиска и агрегаций. С другой стороны, если кеш будет слишком старым, это
может вести к несогласованности и неправильным выводам. Получается, что у нас
есть две точки, между которыми нужно соблюдать баланс, однако где он находит­
ся
-
каждый проект определяет сам. Для того чтобы искать это равновесие
с большей точностью, существуют различные подходы, о которых мы поговорим
дальше.
Часть /. Проектирование и разработка системы
92
TTL
Самый простой и интуитивно понятный способ аннулирования кеша -
время жиз­
ни (time to live, TTL). Он заключается в том, что сами записи кеша имеют опреде­
ленное время, в течение которого они считаются действительными. Например, если
мы задаем TTL, равное 60 минутам, то спустя это время с момента создания записи
она автоматически удалится. Этот механизм является достаточно распространен­
ным. Например, DNS-кeш имеет параметр TTL, и спустя это время браузер либо
другое сетевое ПО будет повторно запрашивать IР-адрес целевого сервера.
Такой подход имеет определенные преимущества. Во-первых, он достаточно про­
стой и понятный. Такой механизм несложно реализовать самостоятельно, вдобавок
к этому многие базы данных для хранения кеша имеют встроенные функции для
автоудаления записей спустя определенный период времени. Ввиду того что мы
имеем понятный параметр для настройки, мы можем настраивать его под нужды
проекта. Например, если мы заранее знаем, что данные наших аналитических отче­
тов обновляются раз в сутки, мы можем смело использовать большой TTL, напри­
мер в 20-24 часа. В свою очередь, если нам важна актуальность данных, мы можем
использовать кеш с меньшим периодом действительности. Нередко это значение
можно подстраивать динамически в зависимости от того, как именно пользователи
используют наше приложение.
Метод TTL хотя и отличается простотой, но все также требует внимательной на­
стройки. Неправильно подобранные значения могут привести к частым промахам
в
кеш,
например
когда они слишком маленькие,
и устаревшим данным, если они
слишком большие. Также данный метод не отличается особой гибкостью. Мы все­
гда можем управлять значениями
TTL либо сразу инвалидировать все значения,
однако, если нам нужна большая отзывчивость на события в системе, стоит по­
смотреть на другие решения. В любом случае подобный подход обеспечивает
оптимальный прирост производительности в системе, и как первый шаг для опти­
мизации можно использовать именно его.
Инвалидация по событию
Главной проблемой TTL является то, что прямо из коробки этот подход не отража­
ет изменяющуюся природу данных. Система непрерывно может изменять те или
иные сущности, они могут добавляться и удаляться. Поэтому логическим продол­
жением идеи кеширования было бы добавить большую гибкость, связанную имен­
но с событиями в системе.
Подход может быть реализован следующим образом. Предположим, что у нас есть
серверный кеш, в котором присутствуют пользовательские сессии. Например, мы
можем хранить в них информацию о роли пользователя, количестве его баллов, его
имени и т. д. -
всю ту информацию, которую пользователь будет запрашивать при
каждом обращении к системе. Когда пользователь обновляет свое имя, либо меня­
ется его роль, приложение будет обновлять определенные поля в базе данных для
Глава б. Кеширование
93
того, чтобы зафиксировать эту информацию. В случае с TTL информация об этом
в кеше была бы удалена спустя время жизни, и маловероятно, что это обновление
совпало бы с обновлением записи в таблице. Вместо этого после записи в базу при­
ложение может выполнить обновление кеша, но сугубо для той сущности, в кото­
рой произошло изменение. Для реализации этого нужна специальная логика на
уровне кода, которая будет выполнять дополнительное обновление кеша для соот­
ветствующих записей. Стоит отметить, что этот подход может быть исполь'3ован
вместе с TTL, благодаря этому наш кеш не будет расти бесконечно, освобождая
место для более часто запрашиваемых данных.
Различным может быть способ, которым будет выполняться обновление кеша. Мы
разобрали способ прямого вызова приложения. Однако что делать, если нам необ­
ходимо обновить кеш в разных местах системы? Для этого могут использоваться
брокеры сообщений. Когда в системе происходит определенное событие, которое
должно привести к обновлению кеша, например создание, удаление либо измене­
ние записей,
управляющее данными
приложение
может отправить
сообщение
в брокер. Благодаря природе брокеров сообщений на их рассылку могут подписать­
ся сразу несколько других приложений, и, если они находятся в списке активных
слушателей, они могут сразу узнать об этом событии. Само сообщение может со­
держать идентификаторы сущности, с помощью которых приложения могут обно­
вить соответствующие записи кеша, воспользовавшись новыми значениями,
кото­
рые также обычно передаются в сообщении. Такой подход позволяет строить гиб­
кие архитектуры, в которых вне зависимости от количества сервисов обновление
кеша будет достаточно быстрым и органичным.
Если мы по каким-то причинам не можем или не хотим использовать брокеры. но­
тификации об устаревании кеша можно рассылать, используя любые другие меха­
низмы, например веб-хуки. Хотя такой подход имеет более высокую связанность
с другими сервисами и требует реализации на уровне кода для того, чтобы доба­
вить новый сервис в рассылку, он может быть более быстрым и удобным в опреде­
ленных ситуациях.
Теперь поговорим о сложностях. Во-первых, такое управление системой требует
больше ресурсов со стороны разработчиков. Если раньше было достаточно управ­
лять операционными данными, теперь необходимо задумываться о том, как именно
будет происходить обновление кеша. Необходимо предусмотреть хранение нужных
идентификаторов, правила обновления, а также предусмотреть управление инфра­
структурой, которая будет ответственна за хранение кеша и передачу сообщений
о его обновлении. Однако такая дополнительная нагрузка на разработчиков может
окупиться, если она приведет к увеличению производительности системы.
Другим важным моментом является шанс потенциальной потери сообщения об об­
новлении. Поскольку мы имеем дело с распределенной системой, а также несколь­
кими частями, мы не можем гарантировать атомарность операций. В случае если
мы обновили запись в базе данных, но не обновили кеш, это приведет к несогласо­
ванности данных. Поэтому важно предусмотреть механизмы, которые будут мини­
мизировать потерю сообщений. В крайнем случае, как говорилось ранее, хорошей
94
Часть /. Проектирование и разработка системы
практикой является добавление механизма TTL, поскольку, даже если мы потеряем
сообщение об обновлении кеша, он все равно рано или поздно истечет.
В общем плане обновление кеша сущностей с помощью событий, связанных с ни­
ми, позволяет поддерживать свежесть кеша. Взамен на разработчиков ложится не­
обходимость в кодовой реализации и усложнении логики приложения, однако
в системах, где необходимо поддерживать близкую к абсолютной согласованность
данных, это может быть эффективным решением.
Глобальное обновление кеша
Помимо обновления определенных сущностей, в рамках системы могут происхо­
дить события, которые требуют глобального обновления всего кеша системы. Та­
кие ситуации могут быть уникальными для проекта, но сейчас мы разберем основ­
ные из них.
Как мы помним, сами сервисы постоянно обновляются: мы добавляем новые функ­
ции, исправляем ошибки и повышаем производительность. Для того чтобы избе­
жать потенциальных ошибок, а также позволить приложению работать с чистого
кеша, очистка всего кеша приложения может быть привязана к обновлению серви­
са. Такое обновление можно делать как вручную, так и добавлять этот шаг в кон­
вейер развертывания приложения. Эту идею можно развить дальше и обновлять
кеш во всех сопутствующих системах, пользуясь механизмом рассылки сообщений
через брокеров либо веб-хуками. Благодаря этому можно избежать ошибок, связан­
ных с тем, что в кеше могут находиться некорректные структуры данных, которые
не соответствуют текущим.
Обновления сервисов нередко несут в себе процедуры миграции баз данных, что
может приводить к изменению схемы последних. Поэтому важно в этом случае
также выполнять глобальную очистку кеша ввиду тех же проблем с совместимо­
стью и актуальностью данных. Такие миграции могут вносить новые атрибуты
в данные, и в случае, если они будут устаревшими либо их вовсе не будет, прило­
жение может некорректно обрабатывать кеш. Также глобальное обновление есть
смысл выполнять при обновлениях безопасности, миграции данных из других сис­
тем, изменениях в конфигурации приложения.
Ранее упоминалась возможность очистки кеша приложения вручную разработчи­
ками. Нельзя сказать, что это является лучшей практикой, однако это может быть
ключевым во время исправления ошибок приложения. Например, новая версия
приложения может некорректно работать со структурами данных, ввиду чего в кеш
также попадают неверные данные. Хорошим подходом перед развертыванием но­
вого обновления, которое содержит исправление, является очистка кеша системы,
чтобы избежать проблем совместимости. Даже если ошибка в приложении не свя­
зана с кешем напрямую и не должна повлиять на его работу, все равно есть смысл
обновлять кеш приложения, поскольку неизвестно, как далеко распространяются
последствия ошибок в коде.
Глава б. Кеширование
95
Управление
Описанные техники требуют определенного совершенства в построении платфор­
мы. Существуют уже готовые решения, а также базы данных, которые могут по­
мочь в имплементации задуманного. Поэтому нередко стоит заранее знать о них,
когда мы собираемся интегрировать кеширование в нашу платформу, для этого мы
разберем основные из них.
О базах данных для кеша
Как говорилось ранее, существуют специализированные решения, которые позво­
ляют применять удобные структуры данных, а также обладают большой скоро­
стью, что в сумме делает их удачным выбором для построения систем кеша. Они
обладают как рядом особенностей, так и недостатками, о которых и пойдет речь.
Во-первых, подобные базы данных поддерживают распределенное развертывание.
Это значит, что мы фактически не ограничены вычислительными способностями
одного сервера и можем использовать сразу множество. В таких сценариях кеш
может как реплицироваться между нодами, фактически обеспечивая большую про­
пускную способность на чтение, так и храниться шардами, позволяя сохранять
больший объем.
Распределенное развертывание позволяет, помимо кеширования, также реализовать
другой любопытный паттерн коммуникации -
через общий кеш. Если ранее мы
разбирали, что приложения могут общаться между собой через сетевые вызовы
либо через брокеров сообщений, то существует также возможность использовать
общие базы данных. И если использование, например, одной реляционной базы
данных сразу для множества приложений может сулить определенные проблемы
эволюции этих сервисов, паттерн общения через общий кеш ввиду того, что сами
структуры данных там могут быть значительно проще, имеет смысл. Например,
в рамках кеша могут храниться общие для разных сервисов сущности, и каждый из
этих сервисов может выполнять обновление и изменение. За счет этого другие сер­
висы будут автоматически получать новые версии данных. Притом что такие дан­
ные обычно хранятся в рамках оперативной памяти, их обновление и доступ к ним
могут быть достаточно быстрыми.
Использование оперативной памяти как хранилища данных также накладывает
определенные ограничения. Да, работа с ними будет невероятно быстрой, однако
в случае аварийного завершения работы базы данных всё, что было в оперативной
памяти, будет удалено ввиду специфики работы ОЗУ. Для того чтобы компенсиро­
вать это, существуют механизмы синхронизации того, что находится в оперативной
памяти и на жестком диске. Например, Redis способен выгружать свои данные на
диск, фактически делая дампы, и при перезагрузке восстановиться с них. В свою
очередь, Memcached такой возможности не имеет, хотя он и имеет другие преиму­
щества. Поэтому важно учитывать, что в случае перезагрузки базы данных мы мо­
жем потерять то, что хранится в кеше на данный момент, причем либо полностью,
либо до момента последнего резервного копирования на диск.
Часть /. Проектирование и разработка системы
96
При планировании мощностей для базы данных кеша важно учитывать то, на­
сколько серверы оптимизированы с точки зрения оперативной памяти. Часто об­
лачные
провайдеры
предоставляют
специальные
семейства
серверов,
которые
имеют расширенные объемы оперативной памяти с большей частотой, что делает
их хорошим выбором для систем кеширования. Также важно помнить про про­
пускную способность сети, поскольку какой бы быстрой ни была оперативная па­
мять, доступ к ней на одном из этапов происходит через сеть, которая в какой-то
момент может стать бутылочным горлышком системы. Для того чтобы компенси­
ровать это, помимо выбора более быстрой сети, можно использовать репликацию,
чтобы разделить сетевую нагрузку между отдельными нодами базы данных.
И традиционно, если мы не готовы настраивать базу данных вручную, существует
большое количество управляемых провайдером решений. В таком случае гипер­
скейлер может взять на себя вышеописанные вещи, связанные с оптимизацией сер­
веров и сети, оставляя разработчикам API самой базы данных для настроек. Поми­
мо привычной модели, где мы сами можем определить количество серверов и их
мощность, существует и альтернативная, связанная с бессерверными вычисления­
ми. При последнем подходе провайдер как бы скрывает от нас сами серверы, по­
зволяя пользоваться кешем по API, и разработчики могут не задумываться о таких
вещах, как масштабирование кластера, обновления, сетевые ограничения, объем.
При такой модели оплата происходит за фактическое использование ресурсов, на­
пример за количество обращений, объем одновременно находящихся данных и за
объем проходящего трафика. Это может быть удобно в случаях, если нам необхо­
димо кешировать небольшие объемы информации либо мы не готовы настраивать
автоматическое масштабирование самостоятельно, но нам важно иметь возмож­
ность в любой момент увеличить объем нашего кеша до нужного значения. Однако
в случае постоянного и интенсивного использования стоит применять выделенные
серверы, поскольку это может оказаться дешевле.
CDN
Любопытным примером кеширования можно назвать сети доставки контента, или
CDN (Content Delivery Network). При построении приложения нередко бизнес­
задача
может
подразумевать
его
использование
из
разных
стран
и
континентов.
Да, Интернет позволяет нам получить доступ к любому ресурсу вне зависимости от
того, где он находится физически, однако всегда встает вопрос задержки. Если це­
левой ресурс находится в соседнем здании и нас отделяет всего пару сетевых
прыжков, то мы можем получить к нему доступ практически моментально, однако
если для этого нужно будет переплыть океан и сделать десятки таких прыжков, то
задержка может стать значительной.
Помимо самой задержки, проблема может появиться и в отношении пропускной
способности. Промежуточные узлы на пути к пользователю могут работать неста­
бильно либо сталкиваться со слишком большой нагрузкой от сторонних пользова­
телей, что негативно отражается на наших собственных запросах. Поэтому было бы
кстати разработать некий подход, который бы позволил избежать этого.
Глава б. Кеширование
97
Формально, даже если наше приложение имеет самый быстрый и оптимизирован­
ный кеш, который находится в периметрах нашей инфраструктуры, сами данные
могут довольно долго бороздить просторы Интернета, пока не попадут к пользова­
телям. И самому пользователю будет, скорее всего, безразлично, вызвано это про­
блемами с нашим приложением либо проблемами с интернет-узлами, для него это
будет одинаково выглядящая задержка, которая будет ухудшать его опыт исполь­
зования приложения.
Идея CDN решает сразу две проблемы. Во-первых, ввиду того что наше приложе­
ние может быть развернуто только в одной локации и доступ к нему может быть
замедлен, CDN имеют свои собственные, приватные сети, которые позволяют ми­
нимизировать влияние публичного Интернета на задержку. Такие сети имеют свои
точки присутствия в каждом регионе, от которых уже происходит доставка трафика
к пользователям. Такие сети отличаются большей надежностью и пропускной спо­
собностью. Также подобная идея может быть масштабирована до нескольких раз­
вернутых версий нашего приложения в разных регионах. При этом CDN в зависи­
мости от географии пользователя либо других правил будет автоматически выби­
рать, на какой именно экземпляр и на какой именно ЦОД нужно отправить запрос.
Это позволяет еще больше минимизировать задержку, а также увеличить надеж­
ность нашего приложения -
если один из регионов будет недоступен, CDN смо­
жет автоматически перенаправить запросы на здоровые реплики.
Вторая особенность -
это кеширование. Для того чтобы обеспечить максималь­
ную скорость доступа к данным, мы можем выполнять кеширование прямо в точ­
ках доступности
CDN, которые находятся максимально близко к пользователю.
Например, мы можем выполнять кеширование статического контента, часто ис­
пользуемых данных и т. д. При этом каждая из точек доступности может иметь
свой собственный кеш, который запрашивается в определенном регионе. Это еще
больше повышает скорость и эффективность работы нашей системы.
Также использование CDN повышает безопасность вашего приложения. Нередко
точки входа в CDN имеют встроенные DDоsS-защиты, файерволы, а также вы мо­
жете настраивать определенные правила блокировки запросов. Фактически
CDN
является определенными щитом в этой ситуации, защищая ваше приложение.
Однако
них -
CDN имеют и обратные стороны, о которых стоит помнить. Первая из
устаревание данных, абсолютно все особенности кеша, которые мы обсуж­
дали, имеются и тут, а именно -
нам необходимо придумывать определенные пра­
вила инвалидации нашего кеша для того, чтобы пользователи получали наиболее
актуальные данные. Вторая проблема, хотя, возможно, это и не совсем пробле­
ма, -
это стоимость. Подобные решения взимают плату за использованные ресур­
сы, например за количество запросов, количество попаданий в кеш и его объем,
объем прошедшего трафика. Однако если это позволяет обеспечить большую на­
дежность и пропускную способность для вашего сервиса, то эти операционные
расходы могут стоить того.
Чаще всего CDN применяются для доставки статического контента, например фото
и видео, ввиду того, что на конечных точках CDN может быть сложно настраивать
сложные правила кеширования контента и можно обойтись TTL либо запросами на
Часть /. Проектирование и разработка системы
98
очистку кеша. Также через CDN часто доставляются веб-сайты, составные части
библиотек для отображения в браузерах и прочие сервисные данные, не касающие­
ся самих операционных данных приложения. В свою очередь, операционные дан­
ные также можно кешировать с помощью CDN, однако важно учитывать политики
их актуальности и влияние на бизнес-случа_и, если пользователь получит доступ
к старым данным.
Заключение
На мой взгляд, это может быть отдельной гордостью для разработчиков и бизнеса,
что созданная платформа обрабатывает большое количество запросов пользовате­
лей, причем это число настолько большое, что требуются дополнительные меха­
низмы, обеспечивающие работу платформы. Кеширование -
это один из таких
способов, который позволяет расширить пропускную способность приложения,
хотя и привнеся в нее дополнительную сложность.
При мысли о внедрении кеша необходимо ответить на несколько вопросов. Воз­
можно, текущее приложение может быть оптимизировано посредством программ­
ной логики, например мы можем выполнить лучшую оптимизацию базы данных
приложения, либо выполнять какие-либо из действий параллельно, либо мы можем
выполнить масштабирование приложения. Если же подобные решения нам не по­
могают, это может быть поводом к изменению логики работы и интеграции кеша
на разных уровнях системы.
Например, мы можем использовать СDN-решения для того, чтобы ускорить дос­
тавку
как
самих
запросов,
так
и
статического
контента,
что
в
конечном
итоге
позволит ускорить работу приложения с клиентской стороны. Кеширование в ко­
нечных точках позволяет еще сильнее усилить этот эффект.
Использование специальных баз данных для хранения результатов сложных агре­
гаций из базы данных позволяет не только ускорить время их получения, но и сни­
жает нагрузку на основную базу данных, позволяя также ускорить и смежные сце­
нарии, которые не имеют прямого отношения к кешированию. А реализация по­
добного функционала на уровне самой базы данных позволяет не подвергать
изменениям саму логику приложения.
При этом крайне важно не забывать про согласованность данных и какое влияние
кеширование оказывает на то, насколько актуальные данные могут получить наши
пользователи. В случаях, где нам критически важно иметь самые актуальные дан­
ные, применение кеширования может быть затруднено, либо может потребоваться
применение более сложных политик обновления кеша.
В том числе и в обычных ситуациях инвалидация кеша является важной задачей.
Мы описали несколько подходов, каждый из которых имеет свои плюсы и минусы,
поэтому их применение напрямую зависит от специфики решаемой бизнес-задачи.
При этом не стоит упираться только в одно решение, стоит брать лучшее из каждо­
го из подходов для того, чтобы максимизировать пользу и минимизировать нега­
тивные эффекты, вызванные той или иной технологией.
ГЛАВА 7
Операционные данные
приложения
Одной из основных особенностей программных систем является возможность хра­
нения и обработки огромных объемов информации. Иногда, размышляя о том, как
дела с обработкой информации обстояли раньше, например когда необходимо было
вести многочисленные журналы,
вручную
рассчитывать какие-то показатели,
ис­
кать информацию в архивах, я понимаю, насколько человечество ушло далеко впе­
ред, ведь сейчас поиск поверх невозможного для восприятия одним человеком объ­
ема данных происходит буквально за несколько секунд. Но, естественно, за этим
стоит не магия, а глубокий и сложный труд множества людей, без которого огром­
ные объемы ручной работы над информацией оставались бы для нас данностью.
Не просто так говорят, что за успехом современного бизнеса стоят не столько
чутье, опыт, сколько данные. Те, кто способны делать свои управленческие реше­
ния основываясь не только на собственной интуиции, но и на конкретных фактах,
собранных с помощью программных систем, в современном мире, как правило,
идут впереди тех, кто этого не делает.
И с другой стороны, те проекты и бизнесы, которые позволяют людям удобно
управлять информацией, цифровизировать те или иные области, точно так же поль­
зуются успехом, принося не только денежные ресурсы, но и, на мой взгляд, просто
делают мир лучше.
В этой главе мы разберем, что именно скрывается за этими красивыми эпитетами и
описаниями. Каким образом, казалось бы, узкие технологии хранения и обработки
данных могут складываться в подобные абстракции. Мы узнаем, какие основные
подходы существуют, как именно бэкенд-приложение с ними работает, как обеспе­
чить надежную обработку операционных данных, что такое аналитика данных
и как это все может помочь людям.
Что такое операционные данные
Как уже было сказано ранее, сила любого приложения скрывается не только в биз­
нес-логике,
что оно выполняет,
но и в данных,
что
аккумулируются
во время
ра­
боты платформы. Большинство бэкенд-приложений невозможно представить без
Часть /. Проектирование и разработка системы
100
уровня хранения информации, бизнес-логика практически никогда не существует
оторванной от них. Будь это сервис по заказу билетов или обычная СRМ-система,
либо социальная сеть -
все современные бэкенд-приложения генерируют инфор­
мацию, которая необходима для ее работы. Эта информация генерируется от раз­
ных событий: действий пользователей, каких-либо действий в реальном мире,
периодических событий в рамках самой системы. Все эти события объединяет то,
что они приводят к созданию новых либо модификации существующих объектов
данных. При этом все эти изменения должны происходить согласованным и логич­
ным для бизнес-задачи образом.
Очевидно, что все эти изменения над данными не происходят просто так, необхо­
дим некий уровень управления этими данными. И, как может быть понятно, этим
уровнем и является само бэкенд-приложение. На самом деле, при близком рассмот­
рении большинство бэкенд-приложений представляют собой интерфейсы для при­
нятия информации из внешней среды, например с помощью НТТР либо очередей
и брокеров сообщений, с проверкой этих данных на соответствие неким правилам,
правильности их схем, после чего происходит применение бизнес-логики с после­
дующим отражением результатов в уровне хранения, коим и является база дан­
ных.
При этом последующие вызовы, ввиду бизнес-логики, могут быть затронуты уже
изменившимися данными, поскольку к ним будут применяться уже другие бизнес­
правила, связанные с данными. Давайте разберем на примере.
Допустим, у нас есть простая система, в которой реализован функционал регистра­
ции пользователя. У нас есть пользовательский интерфейс, который отображает
форму, в которую можно внести свои личные данные, а также логин и пароль,
иными словами, форма регистрации. При отправке этой формы происходит НТТР­
запрос на наше бэкенд-приложение, которое делает целый ряд проверок. Во-пер­
вых, оно выполняет поиск по базе существующих пользователей для проверки того,
не присутствует ли уже логин или почта человека в системе. Во-вторых, приложе­
ние может проверить корректность введенных личных данных (предположим, что
имя и фамилия не могут содержать чисел и быть длиннее 100 символов). Если все в
порядке, мы можем добавить нового пользователя в нашу базу данных, выслать ему
транзакционное письмо для подтверждения почты и ждать, когда пользователь пе­
рейдет по ссылке, чтобы в конечном итоге обновить статус этой новой записи
о пользователе на «Активен».
Подобных сценариев в современном бэкенд-приложении может быть очень много.
Аутентификация пользователя, изменение личной информации, сброс пароля, до­
бавление записей, связанных с логикой самого приложения, их изменение и удале­
ние. Это все -
малая часть того, чего может требовать бизнес-логика для решения
поставленной задачи, и практически каждое из этих действий должно приводить
к изменению данных, ассоциированных с системой.
Пользуясь опытом поколений, мы знаем, что для удобного управления любой ин­
формацией она должна быть структурирована. Информация в архивах и библиоте­
ках не просто так находится в алфавитном порядке, это позволяет значительно
Глава 7. Операционные данные приложения
101
упростить к ней доступ как на добавление, так и на изменение. Помимо алфавита,
данные можно группировать различными способами, создавать связи между ними.
Эти подходы нашли свое отражение в тех программных средствах, на которых
строится хранение данных в современном мире. Естественно, мы можем хранить
все в куче (к слову, мы к этому тоже вернемся), но структура данных позволяет не
только эффективно изменять их, но и помогает человеку разобраться в них, осо­
бенно если сама природа этих данных как-то связана с их структурой.
Исходя из всего сказанного, что же такое операционные данные приложения? Опе­
рационными
можно
назвать те данные,
которые отражают состояние бизнес­
процессов, которые также необходимы для правильного функционирования бизнес­
логики. Для того чтобы не только работать с ними, но и иметь возможность для их
эволюции и расширения без необходимости полного удаления, существуют раз­
личные подходы структурирования данных, используемые в той или иной ситуа­
ции.
Помимо того что данные должны надежно храниться и должна иметься возмож­
ность их модификации, нельзя забывать, что запросы на их изменение могут при­
ходить
параллельно.
Системы
управления
данными
должны
уметь
разрешать
подобные ситуации, сохраняя максимально возможную производительность. Это
особенно критично для приложений, в которых необходимо обеспечить согласо­
ванность данных, поскольку это может быть очень важно для бизнеса, например
при построении системы обработки платежей.
Для каких-то случаев система может пренебречь согласованностью данных, но при
этом она должна обеспечить сохранение огромных объемов данных либо обеспе­
чить минимальную задержку доступа к ним. Или в каких-то случаях требуется
определенная
структура данных,
которая
позволит
полноценно
отразить
природу
задачи.
При перечислении таких свойств может набраться много, и на их основе сформи­
ровался целый зоопарк программных решений. Но начать стоит, на мой взгляд,
с самого распространенного способа моделирования данных -
реляционной моде­
ли, или баз данных SQL.
Реляционные данные. SQL
Основной смысл моделирования данных -
представление их в такой структуре,
которая, с одной стороны, была бы понятна разработчику, а с другой -
отражала
бы бизнес-задачу. Исторически человечество много времени проводило с таблица­
ми данных. Информация, обрамленная в колонки, значительно лучше подходит для
анализа, чем собранные в кучу объекты, -
строки отражают какие-то события,
факты, тогда как колонки помогают уточнить их свойства.
Даже с помощью одной таблицы можно сохранить достаточно много информации
о, казалось бы, разных вещах, но настоящая красота появляется, когда мы строим
сразу несколько таблиц, причем таким образом, чтобы между колонками появля­
лись связи.
Часть /. Проектирование и разработка системы
102
Я думаю, многие бэкенд-разработчики знакомы с концепцией реляционной модели
данных. У нас есть таблицы, которые соединены между собой с помощью связей
между колонками. Я считаю, что есть смысл напомнить некоторые основные поня­
тия, чтобы в рамках этой главы говорить на одном языке.
Первичные и внешние ключи
Одной из основ реляционной модели данных является правильное использование
такого механизма, как первичные и вторичные ключи. Когда в предыдущем абзаце
речь шла о связях между колонками, речь во многом была про первичные и вто­
ричные ключи. Но зачем они нужны?
База данных, которая содержит операционные данные, в определенных своих мес­
тах должна содержать только уникальные значения. Например, таблица, содержа­
щая информацию о зарегистрированных пользователях, должна иметь уникальные
идентификаторы для каждого из них, поскольку в противном случае могут возни­
кать коллизии, а использовать для идентификации какую-то пользовательскую ин­
формацию, например ник или фамилию и имя, точно так же неэффективно, ввиду
все тех же коллизий. Именно поэтому подобные таблицы первой колонкой имеют
поле, которое содержит уникальное для всей таблицы значение
-
этот самый
идентификатор. Традиционно им может быть число, которое автоинкрементирует­
ся, либо это может быть какое-то более сложное значение, например UUID. Именно
это поле, как правило, называется первичным ключом и служит для однозначной
идентификации строки в рамках таблицы. С помощью этого идентификатора мож­
но быстро запрашивать необходимую информацию о конкретной сущности, изме­
нять связанные с ней колонки, и, как говорилось ранее, уникальность уберегает от
коллизий.
С другой стороны, далеко не все колонки в рамках базы данных должны быть стро­
го уникальными. Например, колонки с личной информацией не обязаны быть уни­
кальными, потому что на нашей платформе могут регистрироваться люди одного
возраста либо с одинаковыми именами. Но что происходит, если часть информации
из одной таблицы начинает отражаться в другой? Например, как именно в рамках
реляционной модели нам одновременно иметь справочную информацию о какой-то
группе и информацию о том, какие пользователи к ней принадлежат? Вот тут на
сцену выходят так называемые внешние ключи.
Давайте смоделируем предыдущую ситуацию на уровне модели данных. У нас есть
платформа, в которой пользователи могут объединяться в группы. Мы можем смо­
делировать это разными путями, в том числе иметь одну большую таблицу, в кото­
рой будут записаны все пользователи, а также в соседних колонках будет нахо­
диться информация о каждой группе, например ее имя. Очевидно, что такой подход
может быть неудобен, если мы заходим изменить какую-то информацию, связан­
ную с группой, поскольку это приведет к изменению той части таблицы, пользова­
тели которой состоят в группе, -
нам нужно будет обновить все эти записи. К то­
му же у нас становится очевидной проблема дублирования данных, что может быть
неэффективным с точки зрения занимаемого места на диске. Поэтому нам необхо-
Глава 7. Операционные данные приложения
103
дим другой подход, который как раз и применяется в рамках реляционной модели
данных.
Вместо одной большой таблицы мы можем иметь две -
таблицу с пользователями
и таблицу доступных групп. В первой из них будет храниться информация о самих
пользователях, а во второй, соответственно, мы можем хранить данные о названии
группы, дате ее создания и т. д. Каким образом мы можем связать эти таблицы?
Как мы помним, таблица должна содержать в себе колонку с первичным ключом.
К слову, многие реляционные базы данных не позволяют создавать таблицы, в ко­
торых первичный ключ не уникален. Дr~я создания связи между двумя таблицами
мы должны создать в одной из них колонку, которая будет содержать первичный
ключ другой таблицы. Например, чтобы отразить принадлежность пользователя
к группе, мы должны добавить в таблице пользователей еще одну колонку, кото­
рую можно назвать, к примеру, group_id, и которая будет содержать в себе первич­
ный ключ из таблицы групп. Соответственно, для того чтобы добавить пользовате­
лей в группу, нам достаточно просто добавить соответствующий
1D в колонку
group_id. Теперь, в рамках нашей задачи, у нас пропадает дублирование данных
в сравнении с реализацией в виде одной таблицы, а также теперь мы можем легко
изменять данные самой группы. Колонка group_ id в таблице пользователей высту­
пает вторичным ключом, помогая связать информацию о группе и о конкретном
пользователе.
Давайте разовьем эту идею. Например, мы хотим хранить информацию о том, кто
является администратором группы. Дr~я этого мы можем модифицировать таблицу
групп, добавив в нее колонку adrnin _ id, которая будет ссылаться на таблицу пользо­
вателей. В данном контексте, колонка admin_ id выступает внешним ключом, тогда
как 1D пользователя является первичным ключом.
Как вы уже догадались, на таких простых примерах мы разобрали два типа связей
между данными в реляционной модели. Наш первый пример с группами пользова­
телей отсылает нас к связи «один ко многим» (One to Many), и, на мой взгляд, этот
вид связи является одним из самых распространенных на практике. Почему? Пото­
му что в рамках нашей жизни или в рамках разных бизнес-процессов данные не­
плохо ложатся в эту парадигму. Один департамент
в нем, одна роль, например инженер,
-
-
множество сотрудников
множество сотрудников, которые находят­
ся в этой роли. Одна марка машин, но множество разных моделей. Мне кажется,
подобный ряд можно продолжать бесконечно, и это очень здорово, что такая жиз­
ненная особенность так красиво отражена в реляционной модели хранения данных.
Наверное, вам интересно, для чего вообще нужна эта условность, это явное выде­
ление первичных и вторичных ключей в рамках базы данных. Разве нельзя делать
все, как получается, закидывая данные в кучу, условливаясь об их структуре просто
на словах? Благо современные базы данных устроены иначе.
При определении схемы базы данных и описании ее с помощью синтаксиса базы
данных (DDL, к этому мы вернемся немного позже) мы накладываем специфиче­
ские ограничения на то, как именно будут добавляться, видоизменяться либо уда­
ляться данные. Например, что будет, если мы добавим в колонку group_id несуще-
Часть /. Проектирование и разработка системы
104
ствующее 1D группы? Без ограничений, на уровне бэкенд-приложения, нам не со­
ставит труда создать подобный запрос, однако он не будет иметь смысла с точки
зрения дальнейшей эксплуатации -
информация о группе с данным идентифика­
тором просто отсутствует в нашей системе. Именно от такого рода ошибок нас мо­
жет защитить база данных -
когда мы попытаемся добавить строку с некоррект­
ным первичным либо внешним ключом, база данных сама отклонит наш запрос на
запись, поскольку появится риск нарушения целостности данных. С другой сторо­
ны, что будет делать база данных, если мы попробуем удалить группу, в которой
содержатся активные пользователи? База данных по умолчанию также не допустит
этого, поскольку тогда после операции удаления у нас останется ссылка на несуще­
ствующую группу, что также нарушает целостность наших данных. Вместо этого
база данных может удалить всех связанных с группой пользователей (что на самом
деле довольно спорное поведение) либо автоматически заменить group_ id на NULL,
что в рамках нашего примера допустимо, поскольку пользователь может не состо­
ять ни в одной из групп. Именно поэтому грамотное моделирование первичных и
внешних ключей позволит сохранить целостность данных, спасая от потенциаль­
ных «выстрелов в ногу», вызванных неправильной логикой приложения или не­
ожиданными сбоями.
Также есть смысл разобрать оставшиеся два вида связей--'-- «один к одному» (Опе
to One) и самый необычный -
«многие ко многим» (Many to Many).
Первый вид применяется в случае, когда мы хотим логически разделить данные
о какой-то сущности. Предположим, мы хотим хранить почтовые данные наших
пользователей. Для этого мы можем создать отдельную таблицу
UsersPostData,
в которой будут храниться их адрес, индекс и т. д. Почему бы нам не добавить эту
информацию в саму таблицу users? На самом деле так тоже можно сделать, однако
если мы хотим структурировать наши данные в явном виде, избегая засорения таб­
лиц информацией из разных доменных областей, то можем без каких-либо проблем
вынести их в отдельную таблицу. При связях «один к одному», навешивая ограни­
чение на уникальность внешнего ключа (который, к слову, может являться одно­
временно и первичным ключом), мы гарантируем невозможность дублирования
данных в дочерних таблицах, например мы можем гарантировать, что у пользова­
теля не будет больше одного адреса, применяя именно такую технику.
Другой пример использования отношения «один к одному» связан с безопасно­
стью. Например, мы хотим вынести какие-то чувствительные данные определенно­
го пользователя в отдельную таблицу (например, его медицинские данные). Поль­
зуясь механизмами безопасности базы данных, мы можем ограничить доступ к этой
таблице, оставив его только для самого приложения, при этом не нарушая какие-то
аналитические запросы, которые затрагивают безопасные для просмотра данные.
Ну и напоследок разберем самый интересный вид связей -
«многие ко многим».
Давайте вернемся к нашему примеру с пользователями и с группами. В рамках
текущей реализации пользователь не может иметь больше одной группы. Но пред­
положим, что заказчику требуется, чтобы пользователь имел возможность нахож­
дения одновременно в нескольких группах, что в рамках бизнеса логично. Как мы
можем реализовать данный функционал?
Глава 7. Операционные данные приложения
105
Очевидно, что связь «один к одному» тут не подходит, поскольку у нас может фи­
гурировать несколько групп. Но и «один ко многим» тоже не подходит, потому что
сейчас информация об 1D группы находится в колонке таблицы Users, которая мо­
жет одномоментно содержать только один идентификатор. Именно в таких ситуа­
циях на помощь приходит связь «многие ко многим».
Для того чтобы отразить принадлежность пользователя к множеству групп, нам
необходима еще одна таблица. Можно назвать ее UserMemЬership. И в самой про­
стой реализации она будет содержать всего две колонки -
user _ id и group_ id. Для
того чтобы отразить принадлежность определенного пользователя к группе, нам
нужно добавить всего одну строку, которая будет содержать пользовательский
идентификатор и идентификатор группы в соответствующих колонках. Следова­
тельно, если мы захотим добавить еще групп, мы можем добавить больше записей,
меняя только group id. Для остальных пользователей процесс будет таким же.
Мы можем развить эту идею еще дальше. Подобная структура позволяет нам доба­
вить
некоторые
метаданные,
соответствующие
конкретному
членству
в
группе.
Например, мы можем добавить в данную интеграционную таблицу дополнитель­
ную колонку
is _ adrnin, которая отражает, является ли пользователь администрато­
ром. Либо мы можем хранить дату вступления в группу, роль пользователя (кото­
рая является внешним ключом для таблицы ролей) и т. д. На рис. 7.1 приведены
примеры рассмотренных видов связей.
Скорее всего, на этом этапе мог появиться вопрос: как база данных разрешает нам
иметь повторяющиеся значения в колонках, когда обязательным условием может
быть уникальность первичного ключа? Ответ заключается в том, что первичным
Опе to Опе
carcompany
CarModel
Oneto Мапу
Doctor
Manyto Мапу
Student
1
1
Patient
r
Subject
Рис. 7.1. Основные виды связей в SQL
106
Часть /. Проектирование и разработка системы
ключом может быть больше чем одна колонка, это называется составным первич­
ным ключом. Так, в примере с группами колонки с идентификатором пользователя
и идентификатором группы являются вместе составным первичным ключом, и как
раз их комбинации в рамках таблицы невозможны ( один и тот же пользователь не
может быть дважды участником одной группы), что достаточно удобно.
Умелое комбинирование эти подходов -
залог надежных и прозрачных данных
приложения.
Чтение данных и агрегации
Мы с вами практически не касались того, как именно происходит чтение и поиск
данных в рамках реляционных систем, однако эта тема напрямую влияет на произ­
водительность приложения. Можно сказать, что сама операция чтения в рамках
операционных данных будет происходить значительно чаще, чем операции записи
или модификации. Поэтому существуют механизмы, которые обеспечивают не
только эффективное хранение, но и быстрый поиск, фильтрацию поверх этого на­
бора данных.
В первую очередь стоит поговорить о том, как именно реализован поиск в реляци­
онных базах данных. В рамках языка
SQL существует отдельная инструкция
SELECT, которая позволяет определить те части базы данных, которые нам интерес­
ны. С помощью этой инструкции мы можем выбирать те или иные столбцы,
выполнять фильтрации, применять эти фильтрации к другим столбцам. Также
с помощью этой операции можно выполнять агрегации, вычислять значения и ди­
намически использовать их для вложенных запросов на чтение из базы данных.
Самые частые случае применения -
это извлечение конкретных колонок с приме­
нением фильтраций. Например, мы хотим вернуть идентификаторы всех сотрудни­
ков, которые относятся к конкретному департаменту:
SELECT id FROM ernployees WHERE departrnent = 'HR';
Мы также можем развить эту идею и вернуть их фамилии и имена:
SELECT first_narne, last_narne FROM ernployees WHERE departrnent = 'HR';
С помощью инструкции WHERE мы можем создавать различные условия, например
мы
можем
делать
сравнения
различного
вида,
например
проверять,
является
ли
значение подстрокой либо подэлементом, выполнять поиск по датам и многое
другое. Конкретный функционал зависит от реализации самой базы данных, но, как
правило, он включает перечисленные вещи.
Стоит также обратить внимание на одну интересную инструкцию, которая называ­
ется GROUP ВУ. Например, нам хочется посчитать количество сотрудников в каждом
департаменте. Нам вовсе не обязательно выгружать всю таблицу, мы можем при­
менить для этого группировку подобным образом:
SELECT departrnent, COUNT(*) AS ernployee_count
FROM ernployees
GROUP ВУ departrnent;
Глава 7. Операционные данные приложения
107
Существует большое количество агрегационных функций, например в этом контек­
сте мы можем просто посчитать количество в рамках категории, однако мы можем
посчитать среднее, максимальное, минимальное значения и многое другое. Такой
функционал может быть полезен при построении встроенной в приложение анали­
тики, где для пользователей платформы могут быть подготовлены различного рода
отчеты, которые опираются на операционные данные.
И также для полноты картины стоит упомянуть возможность сортировки данных.
Для этого применяется инструкция ORDER ВУ, которой необходимо передать стол­
бец, по которому будет выполняться сортировка.
Индексы
Поиск данных -
не такая простая операция, как может показаться на первый
взгляд. Да, мы можем легко доставать данные из таблиц, как было описано в пре­
дыдущем разделе, однако то, что происходит под капотом системы, необходимо
хорошо понимать, чтобы построить эффективное и быстрое приложение.
Как вообще в нашей обычной жизни происходит поиск чего-либо? Например, если
наши вещи лежат в случайном порядке, неплохой стратегией будет просто переби­
рать их по очереди либо брать все вещи случайно, перекладывая их в другую стоп­
ку, пока мы не найдем нужную. Очевидно, что этот подход может занимать много
времени и плохо масштабируется, если вещей станет слишком много. Поэтому лю­
ди придумали сортировку вещей, для более удобного их поиска. Например, в биб­
лиотеке книги, как правило, расположены в алфавитном порядке, либо они сгруп­
пированы по авторам, поэтому мы, зная первую букву того, что мы хотим найти,
можем сразу перейти к нужному разделу и сделать поиск перебором там.
Сама идея агрегации данных в какие-то структуры присутствует и в рамках баз
данных, только, на мой взгляд, она там устроена сложнее в математическом плане.
Правильная организация наших данных в осмысленные структуры, без потери ло­
гики и природы самих данных, позволит обеспечить более эффективный доступ
к ним. В общем и целом в этом и заключается идея так называемых индексов базы
данных, и мы можем разобрать подробнее, зачем они нужны.
Почему поиск информации может занимать много времени? Потому что системе
базы данных, очевидно, может потребоваться много времени на ее поиск, потому
что сам поиск может заключаться буквально в переборе всех данных в таблице. Эта
ситуация, которая называется полнотабличным сканированием (full tаЫе scan), хотя
и позволяет найти нужные данные, но это, скорее всего, единственный ее плюс. Во­
первых, чем больше данных содержит таблица, тем больше времени будет тратить­
ся на поиск по ней. Вторая проблема связана с накладными расходами, поскольку
сканирование целой таблицы -
это недешевая в вычислительном плане операция.
Вкупе это делает такой подход неэффективным, хотя на первых этапах, когда таб­
лица относительно небольшая, он имеет место быть.
Поэтому для увеличения эффективности поиска были придуманы индексы базы
данных. По сути это развитие идеи агрегации данных в некие структуры, которые
108
Часть /. Проектирование и разработка системы
позволяют увеличить эффективность поиска. Например, если из знания алгоритмов
нам известна идея бинарного поиска, мы знаем, что проводить поиск по отсортиро­
ванным данным можно значительно быстрее, если понимать, как делать это пра­
вильно. Существует большое количество разных индексов, подходящих под разные
задачи, поэтому важно выбирать их специфично для каждого сценария использова­
ния. Давайте поговорим о преимуществах индексов.
Как правило, индексы применяются к конкретным колонкам, по которым планиру­
ется проводить поиск. Например, если мы хотим быстрее делать поиск по возрасту
человека либо по его месту работы, мы можем добавить соответствующий индекс
в колонку с его возрастом и местом работы.
В этой ситуации база данных создаст структуру данных (которая зависит от типа
индекса), которая будет изменяться при каждой модификации данных, обеспечивая
лучшую скорость поиска. Но, самое главное, поисковые индексы позволяют нам
избежать проблемы полнотабличноrо сканирования, не нагружая нашу базу данных
с ростом размеров таблицы.
Еще одним полезным применением индексов является обеспечение поиска по ком­
плексным данным. Например, у нас существует колонка, в которой хранятся ком­
ментарии пользователей либо некие текстовые сообщения. И мы хотим обеспечить
поиск по этому массиву текста. Мы можем попробовать выполнить эту задачу
с помощью оператора поиска подстроки, однако мы возвращаемся к проблеме пол­
нотабличноrо сканирования, которая еще усложняется тем, что сами текстовые
данные могут быть большими. Выходом является добавление специального индек­
са для выполнения полнотекстового поиска. Современные реляционные базы дан­
ных имеют такой функционал. За счет этого мы можем проводить эффективный
поиск по не совсем структурированным данным, потому что они были правильно
индексированы самой базой данных.
Теперь стоит поговорить о недостатках, вернее сказать, компромиссах, которые
появляются при использовании индексов. Во-первых, добавление новых структур
в таблицу может требовать дополнительного места на жестком диске. То есть, если
наша таблица содержит миллионы строк, мы должны будем также хранить на диске
индекс, который будет занимать некий процент от оригинального размера таблицы.
Если индексов в таблице несколько, размер тоже не будет стоять на месте. Не ска­
жу, что в современных реалиях это может быть сильно выраженной проблемой,
однако об этом стоит помнить.
Вторая особенность заключается в том, что после добавления индекса операции
добавления и удаления, а также изменения, если они ассоциированы с индексируе­
мой колонкой, требуют дополнительное время на завершение. Это чревато не толь­
ко большим временем, которое требуется на модификацию данных, но и большей
нагрузкой, которую будет испытывать база данных. Связано это с тем, что при до­
бавлении новых данных старые индексы, по сути, могут стать бесполезными, пото­
му что они не помогут в поиске новых данных
-
данные в них просто не отраже­
ны. Поэтому перед открытием доступа на чтение во время записи происходит пере­
стройка индекса, т. е. индексирование новых записей.
Глава 7. Операционные данные приложения
109
Последний пункт, который свойствен всем усложнениям поверх чего-то просто­
го, -
это умение правильного пользования технологией. Неправильно выбранный
индекс либо добавление индекса в неправильные места могут не только не уско­
рить работу базы данных, но и замедлить ее, увеличив занимаемое пространство, а
также увеличив стоимость модификации данных, поскольку теперь необходимо
обновить не только данные, но и индекс.
Нормализация данных и объединения типа JOIN
Описанные свойства первичных и вторичных ключей ложатся в более глубокую
парадигму. На самом деле, нам ничего не мешает не пользоваться ими и хранить
данные с дубликатами в одной таблице. Когда мы вернемся к рассмотрению баз
данных NoSQL, мы затронем эту тему, потому что она действительно имеет смысл
в определенных ситуациях.
Процесс балансирования между дублированием данных и усложнением структуры
базы называется нормштзацией данных. Он заключается в определении того, на­
сколько глубоко парадигма первичных и внешних ключей будет проникать в нашу
схему базы данных.
Выделяют разное количество нормальных форм базы данных. Например, первая
нормш,ьная форма подразумевает только то, что у строки в таблице должен быть
уникальный идентификатор, т. е. тот самый первичный ключ. Да, не все базы дан­
ных требуют обязательного его наличия. Вторая нормшtьная форма требует, чтобы
таблица находилась в первой нормальной форме и все атрибуты данных зависели
от первичного ключа целиком. А третья нормшtьная форма подразумевает отсут­
ствие так называемых транзитивных зависимостей, это когда отсутствуют зави­
симости между неключевыми атрибутами (например, зависимость между именем
человека и его должностью, которые повторяются в рамках таблицы несколько раз,
в таком случае мы должны вынести эту информацию в отдельную таблицу). Нор­
мализация данных -
это довольно сложная тема, существует гораздо большее ко­
личество нормальных форм (почитать про это можно в книге Fundamentals of Data
Engineering). Но чем по итогу она может быть полезна?
Как уже говорилось ранее, нормализация данных помогает избежать ненужного
дублирования данных. Благодаря этому сама схема становится более простой для
понимания, а также упрощается запись в саму базу. Обусловлено это тем, что бла­
годаря отсутствию дублирования нам достаточно внести изменение только в одном
месте, а за счет связей между таблицами обеспечивается их актуальность в разных
частях системы.
Второе преимущество связано с удобством разработки. Поскольку ограничения и
правила нормализации общие для всех, в случае если разработчики им следуют,
новые разработчики либо аналитики, которым предстоит работать с базой, могут
делать правильные предположения о структуре данных, что в конечном итоге будет
ускорять их работу.
И третье, касающееся напрямую самой базы данных, -
это целостность данных,
которая выражается невозможностью добавления записей с недостоверными внеш-
Часть /. Проектирование и разработка системы
110
ними
или
первичными
ключами,
что также
положительно
влияет на
надежность
работы приложения.
Это далеко не весь список преимуществ, но одно из них хочется разобрать попод­
робнее. Реляционная модель позволяет реализовать не просто эффективное хране­
ние с точки зрения структуры, но и очень мощную модель чтения. Не просто так
говорят, что базы данных SQL обеспечивают множественную модель чтения по­
верх одной модели записи.
За счет нормализации данных наша информация хранится в виде множества таб­
лиц. Но что делать, если для нашего приложения требуется информация сразу из
нескольких? Допустим, в нашем примере с группами группы могут иметь направ­
ленность, например технические группы, группы по живописи и т. д. Поскольку
информация об этом находится в разных таблицах, мы, конечно, можем вручную
запрашивать сначала 1D всех групп, которые относятся к живописи, после запра­
шивать всех пользователей, которые относятся к этим группам, и комбинировать
эти таблицы на уровне нашего приложения. Однако очевидно, что это не самый
удобный вариант. Что если мы можем сделать это буквально с помощью одного
запроса? И в этом нам помогут объединения типа JOIN.
Наличие связей между таблицами позволяет не только сохранить целостность дан­
ных,
но
и
добавляет
возможность
к
комбинированию
таблиц
между
собой.
В рамках реляционных данных за это отвечает операция JOIN, которая позволяет
использовать механизм первичных и внешних ключей для создания практически
любых комбинаций таблиц, которые не нарушают логику структуры базы данных.
Если описывать вкратце, данная инструкция позволяет «подтянуть» нужные кусоч­
ки из других таблиц для того, чтобы образовать одну составную, при этом «подтя­
гивание» происходит как раз с помощью ключей. Например, у нас есть две табли­
цы, все те же таблица пользователей и таблица групп. Для простоты мы вернем
ограничение, что пользователь может одновременно находиться только в одной
группе, т. е. информация об идентификаторе этой группы находится напрямую
в таблице пользователей. Например, мы хотим запросить всех пользователей, кото­
рые были зарегистрированы в группах, которые были созданы за последний год.
Для того чтобы это сделать, мы объединяем таблицы между собой, воспользовав­
шись связью -
первичным и внешним ключом. Для того чтобы отфильтровать
нужных пользователей, мы делаем выделение, основываясь на дате создания груп­
пы. В конечном итоге мы можем запросить только нужные колонки из получив­
шейся составной таблицы.
Особенность языка SQL заключается в том, что мы можем делать множественные
соединения таблиц. В какой-то момент синтаксис может показаться непонятным и
сложным, но за счет структуры базы и языка запросов мы можем получить прак­
тически любые данные и любые агрегации, которые могут потребоваться в прило­
жении.
Но что будет, если, к примеру, какие-то первичные ключи в одной таблице не на­
ходят связи с внешними, например они просто отсутствуют? Это отсылает нас
к тому, что существует несколько вариантов объединений типа JOIN (рис. 7.2).
111
Глава 7. Операционные данные приложения
Left
Rlght
Left
Right
(]) (])
INNERJOIN
FULLOUTER
JOIN
Left
Left
Rlght
Right
(]) (])
RIGHTOUTER
JOIN
LEFTOUTER
JOIN
Рис. 7.2. Виды объединений типа JOIN
В случае если мы хотим сохранить только те данные, которые имеют идентифика­
торы в обеих таблицах, применяется
INNER JOIN. Этот подход позволяет не иметь
NULL-значений в результирующей таблице. Однако он может потенциально привес­
ти к потере записей из обеих таблиц, для которых отсутствуют общие ключи.
В ситуациях, где мы хотим избежать потери данных одной из таблиц, можно при­
менять LEFГ
JOIN и RIGHT JOIN соответственно. Смысл заключается в том, что для
LEFГ сохраняются все значения из левой таблицы, а те значения, которые не имеют
ключей в правой таблице, заменяются на NULL. То же самое происходит при RIGHT
JOIN, только наоборот. Это может быть удобно, когда мы хотим насытить таблицу
новой информацией, при этом не потерять данные из нее, и при этом мы терпимы
к пустым значениям из другой таблицы.
В развитие этой идеи существует также ru11
JOIN, который объединяет данные из
обеих таблиц, при этом заменяя на NULL отсутствующие общие ключи в обеих таб­
лицах. Это полезно в случаях, когда нам может быть нужно одновременно изучить
две таблицы.
Также есть более экзотические варианты, например
SELF JOIN -
когда таблица
объединяется сама с собой. Это может быть полезно, если в рамках нашей таблицы
существует некая иерархия, либо данные могут ссылаться сами на себя, например
когда у нас существует информация об
1D менеджера, который также является
сотрудником в этой таблице.
Существует также CROSS
JOIN, который позволяет построить декартово произведе­
ние между данными двух таблиц. Это может быть полезно для аналитических
задач.
Хотя этих вариантов и много, чаще всего вы будете пользоваться объединениями
INNER и LEFTIRIGHT. Однако этот гибкий механизм всегда присутствует на уровне
базы данных, и поэтому перед тем, как выполнять какие-то агрегации данных на
уровне приложения, есть большой смысл изучить, возможно ли сделать это на
уровне самой базы данных, получив в приложение уже готовые данные.
Стоит сказать, что операция слияния таблиц не бесплатна в плане скорости выпол­
нения. Для того чтобы выполнить эту операцию, система управления базой данных
Часть /. Проектирование и разработка системы
112
должна провести поиски по дисковому пространству с последующей выгрузкой
данных и их сопоставлением между собой. В случае очень больших таблиц либо их
множества в рамках одного запроса некоторые SQL-запросы могут становиться
слишком долгими. Особенно это может быть заметно на аналитических данных.
Я был свидетелем SQL-запросов, на выполнение которых требовалось несколько
часов, притом что они выполнялись на достаточно мощном сервере. Мы поговорим
про оптимизацию баз данных чуть позже в этой главе.
ACID и транзакции
Если до этого мы затрагивали особенности реляционных баз данных с точки зрения
их структуры, агрегации, чтения, то сейчас мы разберем их с другой стороны -
со
стороны надежности операций изменения.
Если в рамках аналитических данных, когда мы имеем потенциально огромные по­
токи информации, нам простительно потерять маленькую их часть, то в рамках
операционных данных приложения такие ситуации должны быть сведены к мини­
муму.
Именно поэтому существуют особые механизмы
и парадигмы,
которые
обеспечивают согласованность данных, устойчивость от сбоев приложения и защи­
ту от различных спорных ситуаций, которые могут возникнуть при работе над дан­
ными системы.
Давайте разберем пару примеров, которые могли бы произойти в мире, в котором
все вышеперечисленное не бралось бы в расчет.
Представим, что мы строим банковскую систему, в которой у пользователей есть
возможность обмениваться деньгами между собой. Мы реализовали удобный НТТР
API, который позволяет пользователям системы переводить денежные средства со
своего счета
на чужие счета.
При
поступлении такого запроса наше бэкенд­
приложение идет в базу данных и проверяет наличие достаточной суммы у отпра­
вителя платежа. А далее нам нужно выполнить две операции: списание денег с од­
ного счета и добавление такой же суммы на другой счет. Но представим себе си­
туацию, что в момент,
когда мы уже списали деньги,
но не успели выполнить их
добавление на другой счет, наша база данных резко выключилась либо наше
бэкенд-приложение не смогло отправить второй запрос. В такой ситуации деньги
буквально потерялись в воздухе, и улаживать эту ситуацию придется человеку, на­
пример сотруднику банка.
Вторая ситуация, которая может возникнуть в таком мире, может выглядеть при­
мерно так. Наше приложение обслуживает предприятие, которое занимается закуп­
ками оборудования. В один момент два менеджера могут посмотреть, сколько де­
нежных средств находится на балансе. Однако из-за особенностей работы базы
данных они могут увидеть разные данные, поскольку операция по пополнению сче­
та произошла, условно, «не до конца». Такие различия в разных частях системы
могут быть критичны, в том числе и в нашем примере с переводом денег -
одна
часть системы может считать, что деньги уже дошли, однако другая сторона может
просто не видеть их из-за того, что данные были обновлены не полностью.
Глава 7. Операционные данные приложения
113
Развивая эту идею: из-за того, что какие-то операции могут быть завершены не
полностью, мы можем попасть в ловушку неправильной интерпретации состояния
системы. Например, мы строим систему бронирования билетов. В случае если дан­
ные обновляются неодновременно, может произойти ситуация, когда место уже
забронировано, однако система не видит этого и выполняет повторную бронь.
Все эти примеры могут иметь место в реальном мире, и, как правило, исправление
ошибок подобного рода -
непростая задача, потому что модификация данных
вручную человеком сложна, к тому же отследить такие ситуации может быть до­
вольно сложно, кроме как в ситуациях, где человек сам может заметить, что что-то
не так.
Возможно, вы уже догадались, что примеры отсылают нас к легендарной аббревиа­
туре ACID, которая расшифровывается как atomicity (атомарность), consistency (со­
гласованность), isolation (изоляция) и duraЬility (долговечность). И каждое из этих
слов имеет огромное значение в построении бэкенд-систем. Поэтому давайте раз­
берем их подробнее.
Про что вообще эта аббревиатура? Более того, при буквальном ее переводе с анг­
лийского языка она означает «кислота, кислотный». Как именно это относится
к базам данных? Главная цель предыдущих примеров -
показать важность сохра­
нения согласованности и целостности данных, умение бороться с ситуациями, ко­
гда все идет не по плану, и это нарушает ту логику записи, которую мы продумы­
вали изначально. И для реализации этого многие базы данных имеют большой на­
бор инструментов и механизмов, которые определенно усложняют ее понимание,
но увеличивают надежность работы. Теперь давайте разберем основные свойства,
которые делают это возможным.
Буква А -
atomicity, или атомарность. Возможно, для незнакомого с темой чело­
века либо знакомого, но с другой стороны это слово может навеять какие-то знания
о физике, молекулярной и атомной динамике. Однако в рамках баз данных значе­
ние немного другое. Как мы помним, слово «атом» происходит от греческого слова
a:r6µo<; (atomos), что означает «неделимый». А атомарный (либо атомарность) по­
зволяет растянуть это понятие на системы баз данных. Первая проблема, которая
была описана, связана с тем, что мы можем не до конца выполнить задуманные
нами операции. Например, в случае с переводом денег между счетами нам очень не
хотелось бы потерять их сразу после момента снятия. Мы могли бы попробовать
обыграть это иначе и сразу добавить деньги на целевой счет, но что будет, если мы
не сможем снять их с счета отправителя? Как ни крути, ошибки в рамках этого
процесса приведут к нарушению целостности данных и логики работы, что в слу­
чае оперирования реальными средствами может быть крайне болезненным.
Атомарность в этом контексте подразумевает, что операция будет либо выполнена
полностью, либо не выполнена вовсе. То есть, если в рамках процесса перевода де­
нег наше приложение или база данных упадут, операция автоматически откатится
до момента старта.
Для того чтобы подобный функционал был удобен в реализации, популярные базы
данных имеют понятие «транзакции». В рамках одной транзакции разработчики
Часть /. Проектирование и разработка системы
114
объединяют те действия, которые должны выполниться атомарно. Зная условные
границы транзакции, база данных может запомнить состояние таблиц и данных на
момент старта и в случае чего откатить данные до стартовой точки. Поэтому все
операции, которые должны быть выполнены строго вместе, есть смысл объединять
в транзакции, а остальное сможет выполнить база данных.
Теперь разберем следующую часть аббревиатуры, С -
consistency, или согласо­
ванность. Вернемся к примеру, в котором пользователи нашей платформы могли
получать разные данные при чтении об одной сущности. Согласованность в этом
контексте означает невозможность такой ситуации -
все клиенты базы данных
сразу получают актуальные данные, причем сразу, как они были записаны в базу
данных. Звучит как что-то понятное и очевидное, но, к примеру, в рамках распре­
деленных баз данных это правило может не всегда соблюдаться.
Согласованность данных крайне важна при построении финансовых систем либо
систем управления запасами, где множество пользователей могут запрашивать ин­
формацию и делать на ее основе действия, например решать, стоит ли покупать эти
ценные бумаги либо заказывать эти товары со склада.
Третья буква, 1 -
isolation, или изоляция. Это свойство напрямую касается не­
давно изученного понятия «транзакция». Изоляция означает, что пользователи базы
данных (либо сами транзакции) не будут видеть результатов других транзакций,
пока они не будут зафиксированы. Это очень важно, поскольку в рамках нашей
системы одновременно может происходить сразу несколько транзакций, которые
могут затрагивать одинаковые таблицы или даже сущности.
Тут стоит оговориться, что существуют разные степени изоляции транзакций.
В некоторых ситуациях, например, видеть изменения данных другой транзакцией
до ее фиксации может быть вполне допустимым. Давайте кратко охарактеризуем
каждую из них.
□
READ UNCOMMITTED. Этот уровень изоляции также называют грязным чтени­
ем. Транзакция с этим уровнем изоляции полностью ,видна для остальных поль­
зователей базы данных до ее завершения. Иными словами, если в рамках этой
транзакции происходит изменение данных,
эти изменения сразу доступны для
чтения другими транзакциями. Из плюсов -
этот уровень обеспечивает более
высокую производительность базы данных, поскольку нет необходимости бло­
кировать таблицу, чтобы зафиксировать изменения.
□
READ COMMITTED. Как правило, этот уровень установлен по умолчанию и по­
зволяет транзакциям видеть только те данные, которые зафиксированы другими
транзакциями. Этот вид изоляции исключает «грязные чтения», однако имеет
свои особенности. Например, он не защищает от так называемых неповторяе­
мых чтений. К примеру, если во время нашей транзакции будут места, где мы
обращаемся к одной и той же таблице для чтения данных два раза, и в проме­
жутке между этими чтениями другая транзакция зафиксирует свои данные, наша
транзакция может увидеть разные данные при обоих чтениях. Эту особенность
важно
учитывать
при
построении
систем,
однако
этот
уровень
изоляции,
как
правило, считается балансом между производительностью и целостностью дан­
ных.
115
Глава 7. Операционные данные приложения
□
REPEATABLE READ. Данный вид изоляции позволяет избежать тех самых «не­
повторяемых чтений». А именно при старте транзакции, при обращении
к одним и тем же полям вне зависимости от прохождения других транзакций
будут получены одинаковые значения. Однако этот подход, как и все описанные
выше, не защищает от так называемых фантомных чтений, когда транзакции
могут видеть добавленные или удаленные строки, которые были добавлены дру­
гими транзакциями. Этот подход обеспечивает высокую согласованность дан­
ных, однако он медленнее ввиду того, что таблицу требуется блокировать чаще.
□
SERIALIZABLE. Самый строгий уровень изоляции. Он позволяет избегать всех
артефактов при работе с транзакциями: «грязные чтения», «неповторяемые чте­
нию>, «фантомные чтения». Достигается это за счет того, что все транзакции, по
сути, выполняются
последовательно. Это,
как не трудно догадаться, сильно
замедляет скорость работы базы данных, делая
недоступным
параллельные
записи. Однако в системах, где целостность данных важнее, -
этот уровень
изоляции может являться отличным выбором.
Выбор уровня изоляции транзакции зависит от решаемой задачи. Если мы строим
систему, которая в большей степени собирает аналитические данные, для которых
допустимы небольшие потери в целостности, и нам важно обеспечить высокую
пропускную способность на запись, есть смысл выбрать
READ UNCOMMIТED.
Для большинства приложений вполне логично будет оставить настройки по умол­
чанию и выбрать READ COMMIТED, где нам важно избежать «грязные чтения»,
но в общем плане для нас не является проблемой изменение данных между чтения­
ми в одной транзакции. В финансовых приложениях важно, чтобы данные не изме­
нялись в рамках одной транзакции, поэтому можно рассмотреть применение уровня
REPEA Т ABLE READ, не забывая при этом о том, что это может привести к ухуд­
шению производительности базы данных. И в самых ответственных системах, где
крайне важно иметь целостные данные, например в банковских системах или сис­
темах управления запасами товаров, можно использовать уровень SERIALIZABLE
как самый надежный вариант. Однако нельзя забывать, что из всех перечисленных
он является самым медленным ввиду необходимости в блокировках таблицы.
Мы не касались деталей того, как именно база данных реализует эти уровни изоля­
ции. Мы говорили о блокировках, но что это значит? Для того чтобы обеспечить
чтение и запись данных для нескольких транзакций, база данных может блокиро­
вать те или иные ресурсы. Например, база данных может заблокировать строки,
если транзакция намеревается их изменить. При этом другие транзакции могут не
иметь доступа к чтению либо записи в эти строки. Также база данных может бло­
кировать целые блоки данных либо целые таблицы.
Однако наличие блокировок может вызывать специфические ситуации. Что будет,
если одна транзакция заблокирует данные, которые необходимы другой транзак­
ции, при этом эта вторая транзакция уже успела заблокировать данные, необходи­
мые первой?
Такая ситуация называется взаимная блокировка (deadlock) и приводит к тому, что
обе транзакции будут оставаться заблокированными и не смогут выполниться
116
Часть /. Проектирование и разработка системы
дальше. Благо база данных может автоматически детектировать подобные ситуации
и откатывать одну из транзакций для того, чтобы другая могла завершиться, после
чего перезапустить откатанную транзакцию.
Существует много способов избегания взаимных блокировок. Во-первых, разра­
ботчики могут договориться об определенном порядке применения
изменений
к таблицам в рамках транзакций. Чаще всего взаимные блокировки возникают из-за
перекрестного доступа к таблицам, который выполняется в двух транзакциях в раз­
ном порядке. Одинаковый порядок доступа может решить эту проблему без вреда
для производительности. Другой подход -
минимизация размера транзакций, что­
бы максимально уменьшить время, на которое ресурс становится заблокирован­
ным. Использование тайм-аутов также позволяет увеличить надежность системы,
предотвращая ее зависание. Наконец, мы можем использовать более строгий уро­
вень изоляции, такой как SERIALIZABLE, за счет чего транзакции будут выпол­
няться последовательно, не вызывая взаимных блокировок, однако при этом пони­
жая производительность базы данных.
Также важным аспектом является мониторинг базы данных, поскольку все ситуа­
ции и системы разные, и то, что подошло одним, может не подойти другим, поэто­
му достаточное количество вводной информации позволит грамотно разрешить
проблемы с производительностью и согласованностью.
И нам осталось разобрать последнее свойство, D -
durabllity, или долговечность.
В общем плане это означает, что данные, которые были записаны в базу, не исчез­
нут сами по себе, т. е. база данных должна обеспечить их надежное хранение после
фиксации транзакции. То есть, если транзакция была зафиксирована, но произошел
непредвиденный сбой, база данных гарантирует запись на диск после повторной
загрузки. Для этого существуют журналы транзакций, которые могут использо­
ваться базой данных для восстановления после непредвиденного отключения.
Как правило, почти все реляционные базы данных поддерживают ACID. Однако
стоит помнить, что эффективность применения зависит от разработчика. При не­
правильной настройке самой базы данных либо неправильном применении тран­
закций можно нарушить эти свойства, при этом заметить это только уже после
того, как случится какая-то непредвиденная ошибка, которая считалась невозмож­
ной из-за свойств ACID. Однако часто эти свойства можно отключить специально,
о чем пойдет речь в следующем разделе.
Распределенные базы данных и САР-теорема
Современные реляционные базы данных, которые запускаются на мощных серве­
рах, обеспечивают достаточно высокую пропускную способность вместе с сохра­
нением целостности данных. Как правило, вопрос производительности решается
с помощью вертикального масштабирования, т. е. мы
можем просто добавить
больше оперативной памяти и ядер процессора либо поставить более быстрый на­
копитель. Однако, как мы помним из главы про вертикальное масштабирование,
оно, увы, не бесконечно. Так или иначе мы становимся заложниками ограничения
мощности серверов нашей эпохи, и может настать тот момент (хотя он наступает
далеко не сразу), когда мы не сможем добавить вычислительные ресурсы. Тогда на
Глава 7. Операционные данные приложения
117
помощь приходит горизонтальное масштабирование, разве нет? Однако можем ли
мы применить его к нашей базе данных без последствий? Давайте разбираться.
Идея горизонтального масштабирования баз данных не нова, и существует множе­
ство подходов, чтобы обеспечить большую пропускную способность. Один из са­
мых частых подходов
создание реплик чтения. Это означает, что мы будем
-
иметь несколько экземпляров нашей базы данных, при этом запись данных факти­
чески будет происходить только на один экземпляр, после чего данные будут асин­
хронно реплицироваться на другие экземпляры чтения. И в случае запросов на чте­
ние запрос может быть перенаправлен на любой из экземпляров базы данных, та­
ким образом мы можем распределить нагрузку на чтение. Однако недостатком
этого
подхода является
то,
что
на
репликацию данных
между
репликами
чтения
может уйти время, -
да, оно будет быстрым, но не моментальным.
Другой вариант
шардирование базы данных. Часто для выполнения бизнес­
-
операции нам не нужно иметь доступ сразу ко всем данным таблицы. Если мы мо­
дифицируем данные, связанные только с одним пользователем, нам достаточно по­
лучить
доступ
только
к
определенному
кусочку
данных,
поскольку
остальные
пользователи в рамках данной транзакции нам не нужны. Эта идея позволяет раз­
делять данные между экземплярами базы данных -
шардами, помещая на каждый
из экземпляров определенные наборы данных, которые связаны между собой ка­
кой-то логикой. Чаще всего при шардировании определяется так называемый ключ
шарда, с помощью которого происходит определение того экземпляра, на котором
находятся данные и на котором нужно выполнить их модификацию. Таким обра­
зом, мы можем, с одной стороны, распределить нагрузку, а с другой стороны, со­
хранить согласованность данных. Однако есть и минус: если нам нужно выполнить
запрос, которому для выполнения нужны данные из нескольких шардов, это может
потребовать больше времени, что приведет к большей нагрузке на кластер и точно
так же лишит нас согласованности данных, если необходимо обеспечить атомар­
ную и одновременную запись в разные шарды.
Подобные взаимодействия и компромиссы повсеместны в мире баз данных. И для
их формализации было создано специальное правило, которое называется САР-тео­
рема.
Теорема складывается из трех букв, которые означают
consistency (согласован­
ность), availaЬility (доступность) и partition tolerance (устойчивость к разделению).
По очередности эти понятия могут быть понятны, но как они могут существовать
вместе? В этом и есть суть теоремы. Перечисленные понятия -
это конкретные
свойства, которыми может обладать база данных, и теорема говорит, что в рамках
определенной системы одновременно соблюдаться могут только два из них. Это
отсылает нас к трем возможным комбинациям этих трех свойств, и давайте разбе­
рем их на примерах.
Consistency + Availabllity
Разберем первую комбинацию -
согласованность данных и доступность базы дан­
ных. Это означает, что наша база данных будет сохранять согласованность, т. е., по
Часть /. Проектирование и разработка системы
118
сути, сохранять АСID-свойства, и при этом она будет всегда доступна, т. е. у нее не
будет ситуаций, при которых она будет заблокирована. Эта комбинация является
самой распространенной, потому что она является актуальной для ситуаций с од­
ним экземпляром базы данных, в которую ходят приложения. То есть у этой базы
данных отсутствует репликация, все данные хранятся в единственном экземпляре.
Как мы помним, у нас пропала буква Р, partition tolerance, неужели мы не сможем
никуда ее прикрепить? Что значит устойчивость к разделению? В общем плане это
значит, что наша база данных может быть распределенной, например иметь репли­
ки чтения либо шарды. Но, как мы помним из примеров, если мы используем реп­
лики чтения, у нас появляется проблема с задержкой при обновлении данных на
этих самых репликах. Иными словами, между моментом фактической записи и ее
репликацией на реплики при обращении пользователей на чтение запрос может по­
пасть на одну из реплик, которая не содержит актуальные данные. Иными словами,
мы потеряли consistency, согласованность наших данных. Получается, в этом кон­
тексте три свойства сохранить не получится. Давайте посмотрим на другую комби­
нацию.
Availabllity + Partition tolerance
Данная комбинация -
это сохранение доступности базы данных при сохранении
возможности ее разделения. В этом контексте чаще всего существуют базы данных,
в которых включены реплики чтения, шардирование либо другие опции распреде­
ленного развертывания. Этот паттерн используется для того, чтобы увеличить про­
пускную способность базы данных, опустив при этом согласованность данных. Это
может иметь смысл в системах аналитики, где необходимо обработать большой
объем данных, при этом мы толерантны к тому, чтобы получать актуальные данные
не сразу после момента их фактической записи. При этом, как уже понятно, мы не
можем гарантировать согласованность данных ввиду задержек репликации либо
размытия границы транзакции между шардами базы данных. При этом наша база
данных не теряет доступность для операций.
Consistency + Partition tolerance
Данная комбинация практически не встречается в рамках программных систем
ввиду одного любопытного противоречия. Для того чтобы обеспечить согласован­
ность данных между различными частями системы, мы должны вносить блокиров­
ки. Например, у нас произошла запись данных на главную реплику записи. Каким
образом мы можем предотвратить рассогласованное чтение данных? Мы можем
внести блокировку, т. е. потерять одно из свойств -
availabllity. Однако это проти­
воречит самой идее распределенных баз данных -
обеспечение больших пропуск­
ных способностей. Внесение этой блокировки фактически нарушает эту идею, по­
скольку мы не можем обрабатывать запросы, пока происходит репликация, -
база
данных просто-напросто заблокирована.
Однако я не просто так сказал, что это «почти» не встречается. Существует один
любопытный пример таких блокировок, и он существует даже не в рамках базы
данных, а в рамках распределенного брокера сообщений -
Apache Kafka. В рамках
Глава 7. Операционные данные приложения
119
него существует такое понятие, как репликация топика, иными словами, все сооб­
щения, которые попадают в топик, могут быть реплицированы между брокерами,
при этом самой единицей репликации является раздел. Это поведение можно ука­
зать в настройках. При этом для сохранения согласованности смещений в рамках
раздела применяется хитрый трюк -
сообщение недоступно для потребления до
момента, пока оно не будет реплицировано между всеми репликами этого раздела.
Таким образом, путем по сути нарушения доступности сохраняется та самая согла­
сованность данных, и при этом данные могут быть разделены между разными вы­
числительными узлами.
Понимание этой теоремы является крайне важным при построении практически
любых систем. Если мы понимаем, что в рамках бизнес-задачи нам критически
важно сохранить согласованность данных,
мы не сможем
использовать
распреде­
ленные варианты развертывания базы данных без определенных ухищрений и
усложнений самой системы, без потери согласованности, и нам придется использо­
вать один экземпляр базы данных. И наоборот, если согласованность данных не
является для нас приоритетом, мы можем заметно увеличить пропускную способ­
ность нашей системы путем использования распределенного развертывания, не за­
бывая при этом о накладных расходах на обслуживание системы.
ОRМ-фреймворки
Взаимодействие с самой базой данных происходит с помощью языка SQL. В какие­
то моменты запросы могут становиться комплексными и трудными для понимания.
К тому же в случае с программной логикой может быть трудно редактировать и
модифицировать подобные строки. Поэтому для создания некой прослойки между
языком программирования и запросами к базе данных существует целый класс
решений, называемый ОRМ -
object relational mapping.
По сути, это специальные библиотеки, которые используют функциональные осо­
бенности языка, такие как создание классов, методов для определения сущностей
базы данных. Например, такие структуры языка, как классы, могут быть использо­
ваны ОRМ-библиотекой для определения таблиц, определения типов данных, соз­
дания связей между таблицами, работой с индексами.
Далее, созданные классы могут быть использованы для удобного поиска по дан­
ным, их модификации. При этом ключевая особенность ORM -
это незаметная
конверсия взаимодействия с объектами в язык SQL, который исполняется в фоне.
Таким образом, разработчики могут значительно увеличить удобство работы с ба­
зой данных.
Помимо маппинга, ORM предоставляют своего рода синтаксический сахар для ра­
боты с базой данных. Как мы помним, для составления сложных запросов, как пра­
вило, применяются операции слияния таблиц. При правильном определении струк­
туры классов в ORM можно создавать виртуальные поля, которые при обращении
к ним во время исполнения программы будут преобразовываться в JОIN-запросы,
при этом разработчикам не придется прописывать эту логику явно и самостоятель­
но. Возвращаясь к примеру с пользователям и группами. Конкретика зависит от
Часть /. Проектирование и разработка системы
120
реализации, но в общем плане мы можем создать новое поле типа relationship, ука­
зав в нем детали связи. И когда мы будем работать с объектом группы, который мы
можем получить с помощью запроса к базе данных через ORM, то также можем
получить список пользователей, просто обратившись к этому виртуальному полю.
То, что сделает под капотом ORM, зависит от настроек, но она может как сделать
еще один запрос в базу данных, так и перед обращением сделать JOIN для того, что­
бы сразу иметь нужные данные. К слову, поведение по умолчанию -
это важная
деталь, которую нужно учитывать при разработке, например не забывать про ле­
гендарную проблему
N+ 1, которая могла поставить в замешательство не одного
разработчика.
Когда применять реляционные БД
Для большинства программных проектов реляционные базы данных будут являться
выбором по умолчанию. Связано это во многом с тем, что сама модель реляцион­
ных данных достаточно гибкая, чтобы подойти под многие бизнес-задачи. Гра­
мотное
моделирование данных с
использованием
правильных парадигм
позволит
смоделировать множество бизнес-процессов, сохранив при этом достаточную про­
зрачность и читаемость.
Одним
из
важных плюсов
реляционных баз данных является
наличие АСID­
свойств, благодаря чему мы можем обеспечить согласованность и целостность дан­
ных, используя такие механизмы, как транзакции.
Использование операций слияния таблиц позволяет гибко адаптироваться к новым
потребностям бизнеса, практически бесконечно комбинируя таблицы, составляя
все новые и новые формы чтения данных. Грамотное использование поисковых
индексов также сделает эти формы быстрыми для чтения.
Популярность реляционных баз данных также является поводом для хорошей под­
держки ОRМ-библиотек для множества языков программирования, что позволяет
работать с одними базами данных с помощью разных программных средств, полу­
чая сопоставимый по удобству опыт.
Однако нельзя забывать про некоторые компромиссы таких баз, например что мы
не всегда сможем добиться идеальной производительности в таких системах. Нуж­
но 1юмн111ъ про САР-теорему, про те лимиты, которые она подсвечивает. Понима­
ние
этих
механизмов
является
ключом
к
построению
мощных
и
надежных
про­
граммных систем.
NoSQL
Мы с вами изучили один из самых популярных способов хранения операционных
данных -
реляционные БД. Однако, как мы помним, этот подход ассоциирован
с некоторыми ограничениями.
Во-первых, это вопрос производительности. Да, для большинства случаев нам дей­
ствительно будет хватать скорости работы базы данных SQL, однако что если нам
нужно быстрее? Что делать, если мы хотим миллисекундную задержку на запись и
Глава 7. Операционные данные приложения
121
чтение? Такие числа, как правило, недостижимы с помощью реляционных баз дан­
ных, которые оперируют большими объемами информации.
На удивление другой момент -
это модель данных. Да, я верю, что с помощью ре­
ляционных моделей можно моделировать данные любой сложности, вопрос просто
в том, насколько это будет читаемо. К примеру, если мы собираемся реализовать
графовую структуру, это может быть не такой очевидной задачей для реляционной
БД. Либо наоборот, данные часто могут быть слишком простыми, например для их
моделирования достаточно простых пар «ключ-значение», и большая часть функ­
ционала будет просто не использована.
Существует понятие схемы данных, в особенности ее эволюции. В рамках реляци­
онных БД мы имеем возможность создавать новые таблицы, добавлять или удалять
колонки, однако это также требует определенной сноровки, поскольку, как прави­
ло, от разработчика требуется написание целых миграционных сценариев, чтобы
изменить схему базы данных и не повредить при этом сами данные. Но приложение
редко само по себе может изменять схему базы, если ему требуется, например, до­
бавить новое поле о сущности.
Описанные особенности работы привели к созданию целого класса систем хране­
ния данных, которые позволяют разработчикам взглянуть по-новому на привычные
способы организации операционных данных приложения. Как правило, их называ­
ют базы данных NoSQL, где No может переводиться как отрицание («нет») либо
Not only («не только»).
В чем же заключаются основные отличия SQL и NoSQL?
Во-первых, это схемы данных и подход к моделированию. Мы с вами разобрали
основные парадигмы для построения реляционных моделей, однако в мире NoSQL
моделирование данных может сильно отличаться. Данные в NoSQL могут пред­
ставлять собой пары «ключ-значение», коллекции JSОN-документов, денормализо­
ванные таблицы, графы и множество других вариантов, реализуемых тем или иным
NоSQL-решением. Благодаря такой гибкости можно добиться более натурального
переноса данных предметной области, тем самым сделав систему более нативной.
Плюс к этому такие структуры лучше поддаются изменению и эволюции схемы,
мы не привязаны к какой-то конкретной модели. Например, когда наша база дан­
ных представляет собой коллекции JSОN-документов, формально мы можем хра­
нить в каждом из этих элементов любые поля. Да, мы можем задавать определен­
ные ограничения, но такой подход позволяет хранить разную информацию об объ­
екте
в
зависимости
от
контекста.
Например,
если
мы
храним
информацию
обо всех животных, то можем иметь специфические поля для каждого вида, не пе­
регружая остальные записи NULL-значениями.
Вторая особенность связана с более высокой производительностью баз данных
NoSQL. За счет использования более гибких структур, а также частого отсутствия
АСID-свойств обеспечивается большая пропускная способность базы данных. Если
вспомнить САР-теорему, то, если нам не нужно обеспечивать согласованность дан­
ных, мы можем попробовать разделить базу данных без нарушения ее доступности.
Именно такая стратегия используется во множестве баз данных NoSQL, где мы
Часть /. Проектирование и разработка системы
122
можем разделять наши наборы данных между экземплярами. Также за счет иных
структур данных мы
можем хранить данные не только
на диске,
но
и,
например,
в оперативной памяти, заметно снижая задержку доступа к данным.
Благодаря другому подходу к хранению данных именно NoSQL-бaзы являются
движителями BigData. Такой вариант применения может быть более подходящим
с точки зрения хранения больших объемов информации не только за счет возмож­
ности распределенного развертывания, но и за счет структур данных, которые луч­
ше подходят для аналитики. Например, колоночные базы данных лучше справля­
ются с обработкой сложных аналитических запросов ввиду особенностей хранения
данных на диске.
Для того чтобы лучше разобраться в многообразии баз данных NoSQL, давайте
кратко пройдемся по основным типам, случаям использования, преимуществам и
недостаткам.
Базы данных «ключ-значение))
Начнем наш обзор с относительно простых структур данных, таких как пары
«ключ-значение», либо еще их называют хеш-таблицы. Самыми популярными
примерами можно назвать такие решения, как Redis, Memcached, DynamoDB,
CosmosDB NoSQL. По сути, если достаточно долго денормализовывать какие-либо
данные, мы в конечном итоге сможем представить их в виде таких пар, где ключом
будет являться некий идентификатор, который будет позволять однозначно опре­
делять некую запись, а значением будет ее полезная нагрузка.
Сама полезная нагрузка может иметь различный формат, например это могут быть
как просто строки и числа, так и какие-то более сложные структуры, например
JSОN-объекты или даже бинарные данные. Это позволяет разработчикам не привя­
зываться к каким-то конкретным типам данных, которые, в случае с реляционными
данными, как правило, жестко заданы для той или иной колонки.
Как говорилось ранее, такие базы данных достаточно просты в плане структуры
данных, что упрошает их понимание и моделирование. Особенно это подходит для
случаев, где природа наших данных проста, и мы можем использовать подобные
решения для их хранения и обработки.
Простая структура также позволяет выделить еще одно, я бы даже сказал, ключевое
преимущество -
высокую производительность. Простота структуры данных по­
зволяет не хранить
на диске сложные индексы
поиска, а ограничиться,
например,
хеш-таблицами, которые позволяют моментально находить нужные частички ин­
формации. Однако это не самый главный козырь повышения производительности
за счет такой структуры, данные можно хранить не только на жестком диске, но и
переносить в оперативную память, которая работает на порядок быстрее самого
быстрого SSD.
Большая скорость работы позволяет использовать подобные решения не только для
хранения операционных данных, но и для более узких случаев -
кеширования.
И, как было отмечено ранее, структура данных «ключ-значение» удачно подходит
Глава 7. Операционные данные приложения
123
для задач кеширования, поскольку у нас есть, как правило, уникальный идентифи­
катор закешированного объекта и собственно сам объект, который хранится в на­
шей базе данных.
Помимо высокой скорости, базы данных типа «ключ-значение» хорошо поддаются
масштабированию. Связано это с тем, что сами записи между собой, как правило,
связаны не сильно, поэтому мы можем разделить их между серверами. А для того
чтобы обеспечить эффективный поиск, мы можем применять стратегии шардиро­
вания либо автоматической маршрутизации между нодами. Однако основную роль
тут все же играет структура, которая хорошо подходит для масштабирования и раз­
деления данных. Для увеличения пропускной способности на чтение также может
применяться
репликация данных,
например
между разными регионами для
того,
чтобы обеспечить одинаковую задержку доступа к ресурсам вне зависимости от
локации вызывающего.
Распределенная природа таких баз данных также обеспечивает высокую надеж­
ность, -
даже если какой-то узел будет недоступен, остальные узлы продолжат
свою работу и смогут предоставить свои данные.
Теперь поговорим об ограничениях подобных решений. В первую очередь стоит
отметить сложности для поисковых запросов. Такие запросы, как фильтрация либо
агрегирование (наши любимые объединения типа JOIN), недоступны ввиду того,
что подобная структура данных не вносит ограничения на сами данные и база дан­
ных не сможет грамотно собрать эти данные без полного сканирования вообще
всех данных. Поэтому, если наш сценарий использования подразумевает агрегации
и поиск, следует найти другие решения.
Еще одна особенность, которая вытекает из структуры данных, -
возможность
дублирования данных. Сами базы данных «ключ-значение» позволяют создавать
множественные коллекции, а также, как было сказано ранее, не ограничивают дан­
ные посредством первичных и внешних ключей, из-за чего появляется шанс созда­
ния дубликатов и несогласованности данных между разными коллекциями. Шанс
дублирования увеличивается еще больше ввиду того, что сама природа хранения
данных в подобных хранилищах -
это сильная денормализация по той причине,
что у нас полностью отсутствует функционал агрегации, поэтому мы должны дуб­
лировать множество записей для возможности чтения при разных бизнес-сцена­
риях. Хотя это и увеличивает скорость работы, важно понимать, можем ли мы хоть
немного забыть про согласованность во время разработки или нет.
И последнее, что будет свойственно многим NоSQL-решениям, -
это отсутствие
АСID-свойств. То есть, например, мы не можем гарантировать, что если нам нужно
выполнить множественную вставку или удаление записей, она не будет происхо­
дить атомарно, либо, если наша база данных распределенная, стоит помнить, что
мы теряем согласованность данных. Одним интересным примером базы данных
ACID типа «ключ-значение» является etcd, которая используется таким проектом,
как Kubemetes, для хранения состояния кластера.
Поэтому основными сценариями применения являются ситуации, где нам необхо­
димо обеспечить высокую производительность, при этом структура наших данных
Часть /. Проектирование и разработка системы
124
достаточно простая, и мы не собираемся запускать сложные аналитические запро­
сы. Идеальные варианты: системы кеширования, где мы хотим сохранить в памяти
результаты каких-то других запросов, которые требуют большого количества вре­
мени на исполнение, или системы хранения пользовательских сессий для обеспече­
ния быстрого доступа к ним для чтения и обновления.
Документные базы данных
Другим классом NоSQL-решений являются базы данных для хранения документов.
Но что такое документ? Как правило, под этим понимается некий JSОN-объект,
который содержит в себе информацию, причем нередко во вложенном формате.
При этом эти документы хранятся в виде коллекций, т. е. группы подобных доку­
ментов.
Самой популярной документной базой данных NoSQL считается MongoDB, однако
парадигмы, которые приняты в ней, нередко встречаются и в других решениях.
Идея таких баз данных простая -
иметь возможность хранить некие комплексные
структуры данных без строго заданной сферы с возможностью индексации данных
объектов для быстрого поиска. Формально даже реляционные базы данных можно
представить
в
подобном
ключе,
каждую
из строк
можно
представить JSОN­
объектом, который содержит в себе определенные поля. Ключевое отличие здесь
заключается в том, что в документных базах данных разработчики могут без огра­
ничений добавлять новые поля без необходимости переделывать схему. Даже
в рамках одной коллекции могут находиться документы с абсолютно разными по­
лями, это допустимо, если это имеет бизнес-смысл. Такая возможность добавляет
гибкость при разработке, позволяя нашим данным проще эволюционировать, но
к этому стоит относиться аккуратно.
В отличие от баз данных «ключ-значение», наши документы могут содержать
большее количество разнообразных структур. Помимо вложенности данных, база
данных может автоматически определять типы данных, например правильно рабо­
тать с полями-метками времени и эффективно хранить их в памяти. Это позволяет
моделировать более сложные бизнес-процессы без каких-то явных усложнений,
которые не задумывались создателями базы данных. Плюс, база данных может на­
кладывать ограничения на данные. Так,
похожие
на
первичные
ключи,
не
MongoDB умеет создавать ограничения,
позволяя
создавать
документы
с
одинаковыми
значениями в определенных пользователем полях, например не позволит добавить
нового пользователя с идентификатором, который уже существует в базе.
И за счет более сложных структур данных возможно их эффективное индексирова­
ние. Например, мы можем хранить документы, каждый из которых хранит инфор­
мацию об отдельном пользователе, и этот документ содержит информацию о коли­
честве купленных товаров в виде отдельного поля. Мы можем создать индекс для
этого случая, обеспечив повышенную эффективность поиска по тому, сколько то­
варов купил человек. Очевидно, что мы можем индексировать разные поля доку­
мента, делая доступ к нашим данным не только гибким, но и эффективным.
Глава 7. Операционные данные приложения
125
Документные базы данных частично наследуют плюсы баз данных «ключ-значе­
ние» в области производительности. Ввиду особенностей структуры данных доку­
менты можно удобно разделять и шардировать, обеспечивая высокую пропускную
способность и устойчивость самой базы данных. Подобные базы данных хорошо
масштабируются горизонтально, что делает их хорошим выбором в ситуациях, ко­
гда нам нужно иметь возможность расширять хранимые нами данные со временем
без сильных потерь в производительности. Это особенно хорошо коррелирует
с гибкой схемой данных, которая позволяет добавлять все больше и больше ин­
формации в документы. Также подобные документы могут кешироваться прямо
в оперативной памяти, обеспечивая высокую скорость доступа к данным.
Что касается ограничений, они также схожи. Во-первых, хотя структура данных
более сложная и позволяет хранить больше данных, подобные решения не имеют
функционала слияния таблиц, т. е. объединений типа JOIN. Для того чтобы объ­
единить
данные
из
нескольких
коллекций,
придется,
как
правило,
выполнить
несколько запросов к базе данных и потом сопоставить данные на уровне прило­
жения.
Отсутствие объединений типа JOIN вынуждает разработчика применять денорма­
лизацию данных, которая может привести к созданию дубликатов данных между
коллекциями. Это усложняет одновременное обновление данных и может создать
проблемы с согласованностью данных в разных частях базы данных.
Также подобные базы данных редко поддерживают АСID-свойства, что еще боль­
ше затрудняет обеспечение согласованности данных и работу с транзакциями.
Можно считать, что это является платой за высокую производительность базы дан­
ных.
Свободное изменение схемы базы данных может быть как плюсом, так и минусом.
Если подойти к этому вопросу небрежно, а именно создавать документы с разными
полями нелогичным образом, ввиду отсутствия структуры другим разработчикам
может быть трудно разобраться в том, как именно данные хранятся в системе.
Слишком большое количество ненужных полей, которые могут появиться в доку­
менте ввиду неправильного моделирования данных, не только усложняет понима­
ние другими членами команды, но и может приводить к замедлению работы самой
базы данных и неэффективному выполнению запросов. Поэтому необходимо гра­
мотно пользоваться такой свободой, чтобы не усложнить разработку в будущем.
Основными сценариями использования документных БД можно считать системы
для хранения неструктурированного контента, например статей, которые генери­
руются самим пользователем. Такие базы хорошо подходят, например, для интер­
нет-магазинов, где нам необходимо отразить разнообразные атрибуты товаров, ко­
торые могут быть уникальны для категорий. За счет гибкости схемы разработчики
могут свободно добавлять новые поля в товар, добавлять к ним поисковые индек­
сы. А за счет того, что база данных хорошо поддается разделению, мы можем хра­
нить большое количество информации и выдерживать большую пользовательскую
нагрузку. Стоит также отметить, что существуют семейства документных баз дан­
ных, оптимизированные под сценарии поиска, поэтому в сценариях сложной ин­
дексации и полнотекстового поиска такие решения имеют большой смысл.
Часть /. Проектирование и разработка системы
126
Однако если вы ищете строгую согласованность данных и возможность высокой
нормализации, то подобный способ хранения информации вам не подойдет. Огра­
ничения, связанные со структурой работы, позволяют достичь преимуществ в дру­
гих сферах, но не позволяют обеспечить АСID-свойства, что во множестве ситуа­
ций может быть критически важным.
Колоночные базы данных
Колоночные (или column oriented) базы данных не относятся напрямую к обработке
операционных данных, чаще они используются для аналитики. Однако в случаях,
когда мы хотим предоставить пользователю встроенную аналитику, которая стро­
ится, например, на основе каких-то операционных действий (частоты запросов или
изменения элементов, частоты входов на платформу), колоночные базы являются
ярким представителем NoSQL, направленных на эффективное хранение и обработ­
ку информации.
Формально колоночные базы оперируют похожей структурой данных, которая ис­
пользуется в реляционных системах, -
таблицами. Однако ключевое отличие за­
ключается в механизме хранения на диске. Для реляционных данных, как правило,
важно иметь быстрый доступ к отдельным записям, т. е. строкам. Именно поэтому
для увеличения эффективности записи и чтения базы данных SQL хранят таблицы
на диске построчно. Однако это усложняет аналитические запросы, где нам необ­
ходимо получить информацию в рамках целого столбца, потому что в сценарии
с построчным хранением базе необходимо извлечь информацию из каждой строч­
ки, потом агрегировать ее и выдать результат пользователю.
В колоночных базах данных применяется другой подход. Информация на диске
в этом случае хранится по колонкам, т. е. данные из одной колонки находятся на
жестком диске рядом друг с другом. Поэтому в случае аналитических запросов база
данных может эффективно извлечь информацию из одной колонки, поскольку она,
скажем так, лежит на диске одним куском. Также за счет такого типа хранения
можно использовать более эффективные алгоритмы сжатия и хранить большие
объемы информации, тратя при этом меньше ресурсов. Подобные свойства опреде­
ляют использование колоночных баз в аналитике и хранении больших данных.
Информация в колоночных базах данных, как правило, разрежена и денормализо­
вана, это значит, что в рамках каждой записи многие колонки могут быть пустыми.
Это является одним из полезных свойств -
легкая эволюция схемы данных. Фор­
мально, это может быть похоже на документные базы данных, только тут мы дви­
жемся лишь в одном направлении
-
в направлении колонок, не имея встроенных
инструментов для создания вложенности. К слову, если вложенность нам все-таки
нужна, колоночные базы данных могут хранить различные данные, в том числе
и объекты JSON. Также для удобства работы колонки могут объединяться в семей­
ства, чтобы понятнее обрабатывать связанные между собой сущности.
Одним из дополнительных поводов к использованию колоночных баз данных для
хранения больших объемов информации является возможность разделения. Во­
первых, многие колоночные базы данных поддерживают репликацию данных, что
обеспечивает большую надежность в случае проблем с одним из узлов. Во-вторых,
Глава 7. Операционные данные приложения
127
они поддерживают партиционирование и шардирование, что позволяет разработчи­
кам определить механизмы разделения данных в рамках кластера базы данных для
того, чтобы обеспечить эффективное выполнение запросов.
Как и многие NоSQL-решения, колоночные базы данных могут иметь ограничения
в АСID-свойствах. Ввиду этого в случаях, где требуется строгая согласованность,
подобные базы данных могут не подойти.
Другой проблемой является потенциальное дублирование данных, которое возни­
кает ввиду денормализации. Это усугубляется отсутствием АСID-свойств в ситуа­
циях, где нам нужно гарантированно и одновременно удалить информацию из не­
скольких таблиц либо колонок.
И третье свойство вытекает из особенностей хранения на диске. За счет того что
данные хранятся колонками, усложняется случайное чтение и случайная вставка
строк, потому что это требует реорганизации большего объема данных на диске,
чтобы, условно, втиснуть информацию в базу, и такая операция занимает больше
времени, чем аналогичная на строчно-ориентированных решениях.
Поэтому основными сценариями применения таких баз данных является аналитика,
хранение и обработка больших данных, а также построение распределенных реше­
ний для хранения информации с большой пропускной способностью. Ввиду огра­
ничений в АСID-свойствах они могут не подходить для обработки транзакций,
хотя современные решения позволяют заметно снизить проблему потенциальной
несогласованности данных.
Графовые базы данных
Одной из примечательных структур данных является граф. Если описывать его
вкратце, он представляет собой набор узлов, называемых вершинами, соединенных
между собой с помощью связей, называемых ребрами.
Граф является уникальной структурой данных, которая неплохо описывает наш
реальный мир. Например, города и дороги между ними можно представить в виде
графа, где города -
это узлы, а дороги -
это ребра. Таким же образом можно
представить пользователей социальной сети, где с помощью ребер можно выразить
то, кто для кого является другом.
Очевидно, что для управления такой структурой должны существовать специали­
зированные базы данных, которые позволят хранить подобную информацию, и из­
за экзотичности структуры они попали в категорию NoSQL.
В чем основные преимущества таких баз данных? Во-первых, они способны моде­
лировать информацию в абсолютно другом стиле, который мы до этого не рассмат­
ривали. Многие данные в мире ложатся в парадигму графов, и их может быть
неудобно или неэффективно моделировать, используя, например, реляционные мо­
дели или пары «ключ-значение». Использование нативной структуры графа позво­
ляет избежать каких-то проблем с моделированием, когда мы пытаемся подстроить
одну
парадигму
хранения
данных
под
ситуации,
где
эти
данные
не
так
просто
уложить в эту самую парадигму. Структура графа может быть более понятной для
разработчиков, чем представление того же графа в виде таблицы с дальнейшей его
сериализацией и десериализацией.
Часть /. Проектирование и разработка системы
128
Нативная структура позволяет обеспечить более интересные способы обращения
к данным. Например, запрос в рамках реляционной базы данных «получить инфор­
мацию обо всех друзьях моих друзей, которые являются членами группы» опреде­
ленно возможен, но при усложнении глубины он может становиться все менее и
менее понятным. Как правило, в рамках графовых баз данных подобные запросы
формируются легко и естественно. Что еще важно, добавление информации в rра­
фовую базу данных в случае сложных связей, как правило, проще, чем для схожих
случаев при использовании реляционных моделей. Дополнительно такие запросы
оптимизируются
с
помощью
специальных индексов,
которые ускоряют поиск по
связанным между собой элементам.
Естественно, граф позволяет нам иметь гибкую схему данных. Мы можем ассоции­
ровать с вершинами и ребрами метаданные, которые будут использоваться в нашем
приложении.
Одним из недостатков таких баз данных я вижу сложность их использования для
начинающих разработчиков. Некоторые понятия, которые присутствуют в таких
базах данных, могут не иметь никаких аналогов среди других технологий, что де­
лает кривую обучения довольно крутой. Для правильного моделирования данных и
использования графовых БД в реальных ситуациях может потребоваться практика
для того, чтобы не допускать ошибки и использовать потенциал максимально эф­
фективно.
Что касается согласованности данных, некоторые решения поддерживают ACID,
однако следует помнить, что эти четыре буквы не будут гарантировать согласован­
ность ваших данных прямо из коробки, и нужно правильно оценить возможности
решения и ваши сценарии использования.
Резюмируя: графовые БД отлично подходят для моделирования данных, в которых
присутствуют связи, например для построения социальных сетей. При этом в слу­
чаях, где связи между данными не такие сложные, есть смысл использовать класси­
ческие решения и эмулировать эти связи другими путями.
Базы данных для временных рядов
Существует одно любопытное правило: чем более узким является решение, тем
лучше оно справляется со своей задачей. Условно, у швейцарского ножа есть мно­
жество применений, но я не думаю, что в ситуации, где нам нужна отвертка, мы
захотим использовать его, а не полноразмерную отвертку. То же самое распростра­
няется и на программные инструменты.
Нередко в рамках бизнес-задачи есть необходимость сохранять значения, которые
изменяются со временем. Например, изменения курсов валют, температура, коли­
чество прошедших через систему байтов. Данные, которые мы записываем через
какие-то кратные промежутки, называются временными ряда.ми, и их существует
огромное множество.
Формально мы можем записать временной ряд в разных формах: в таблицах, доку­
ментах, даже как ключ и значение. Однако тут мы возвращаемся к парадоксу швей­
царского ножа: все эти решения имеют свои недостатки и не будут выполнять свою
Глава 7. Операционные данные приложения
129
работу так же эффективно, как что-то такое, что было создано для использования
в этой ситуации.
Поэтому, поскольку задача сохранения временных рядов достаточно специфична
и
в рамках нее можно
использовать разные оптимизации,
существуют отдельные
базы данных для решения этой задачи.
Во-первых, такие базы имеют целый набор инструментов для поиска в рамках вре­
менных диапазонов. Да, подобный функционал есть и у реляционных баз, но сами
данные
при
этом
автоматически
ассоциируются
с
подходящими
поисковыми
индексами, чтобы обеспечить эффективность поиска.
Второе преимущество -
эффективность хранения временных рядов. За счет того
что значения могут изменяться несильно, существует большое количество вариан­
тов оптимизации и сжатия данных.
Нередко базы данных временных рядов имеют функционал расширенных запросов,
например,
выполняющихся
периодически,
предоставляя
пользователям
агрегации
практически в реальном времени. Это может быть особенно полезно для построе­
ния систем финансовой аналитики или обработки lоТ-данных.
Стоит отметить, что, исходя из природы данных, подобные базы данных хорошо
поддаются распределенному развертыванию, поскольку мы
можем разделять дан­
ные как минимум по принадлежности к одному временному ряду или делать разде­
ление на основе промежутков времени.
Думаю, не стоит говорить о недостатках таких баз данных, потому что они вполне
логичны. Да, мы не сможем хранить в них информацию, которая не является вре­
менным рядом, либо просто случайные значения. Мы также имеем ограничения по
тому, как именно выполнять поиск по нашим данным. Зато в обмен на эти особен­
ности мы получаем эффективный инструмент для хранения специфической инфор­
мации, который справится с этой задачей лучше, чем общее решение.
Для удобства все перечисленные решения можно свести в одну таблицу, которая
будет включать в себя плюсы, минусы и сценарии использования (табл. 7.1).
Таблица 7.1. Основные типы NoSQL баз данных
Плюсы
Ключ-
Высокая производительность.
Ограничения схемы
Простая схема данных.
данных.
к простым данным,
Отсутствие СЛОЖНЫХ
хранение пользова-
значение
Минусы
Сценарии
ТипБД
Удобства масштабирования
использования
Кеш, быстрый доступ
агрегаций и запросов
тельских сессий
Управление интернет-
---
~ -
Документ-
Гибкая структура данных.
Сложность в агрега-
ные
Возможность хранения и ин-
ции данных.
магазинами.
дексации JSОN-структур.
Отсутствие
Поиск документов_
Вложенность.
ACI О-свойств.
Хранение данных
Производительность
Сложности в управле-
с непостоянными
нии множественными
полями
структурами
Часть /. Проектирование и разработка системы
130
Таблица 7.1 (окончание)
Тип БД
Плюсы
Минусь1
Сценарии
использования
Колоноч-
Эффективная работа
Неэффективны для
Аналитика, большие
ные
с большими данными.
операционных данных
данные
Аналитические запросы
Графовые
Гибкая структура данных.
Высокий порог входа.
Социальные сети.
Возможность прямого моде-
Сложности в настрой-
Хранение данных
лирования определенной
ке и развертывании
с большим количест-
бизнес-логики.
вом связей между
Возможность естественного
собой
выполнения сложных запросов
Временных
Удачный инструмент для
Не подходит для дру-
Системы аналитики
рядов
хранения временных рядов.
гих данных.
метрик.
Удобный интерфейс запро-
Может требовать
lоТ.
сов.
определенного пони-
Системы мониторинга
мания принципов ра-
приложений
Возможность периодического
выполнения запросов
боты
Заключение
Данные являются силой любого бизнеса и системы. С помощью них люди могут
принимать эффективные решения, делать правильные выводы и развивать свой
сервис и продукт. Помимо этого, правильная работа с данными приложения может
быть ключевой для обычных пользователей системы, которые могут даже не заду­
мываться о том, как именно это работает под капотом, поскольку всю самую слож­
ную часть от них смогла скрыть команда разработки, и пользователи могут пользо­
ваться этим для решения своих задач.
При выборе подхода для работы с операционными данными важно оценивать ком­
промиссы того или иного решения в разрезе именно вашей задачи. Не существует
решения, которое может подойти для всех задач. Если минусы определенного под­
хода могут быть не видны, скорее всего, это значит, что время для их проявления
просто еще не настало. Стоит также помнить, что миграция с неподходящего реше­
ния на подходящее может быть трудной задачей, если вашим приложением уже
пользуются люди. Миграции такого рода всегда являются потенциальным рассад­
ником ошибок, которые в случае реальных данных могут вылиться в плохой поль­
зовательский опыт.
В любом случае, как хорошее начало, есть смысл рассмотреть реляционную модель
для того, чтобы моделировать ваши данные. В большинстве случаев это обеспечит
не только надежную структуру, но и возможность развития схемы данных, которая
неизбежно потребуется с развитием приложения и видения бизнеса. Гибкие запро­
сы на чтение позволяют адаптироваться к различным бизнес-сценариям, миними-
Глава 7. Операционные данные приложения
131
зируя ситуации, при которых все нужно переделывать полностью. При этом пра­
вильные оптимизации самой базы позволят добиться неплохой скорости для боль­
шинства сценариев.
Если же по каким-то причинам нам не подходит реляционная модель, существует
большое количество NоSQL-решений, которые могут подойти для решения задачи.
Рассматривать их стоит в случае, если схема данных NоSQL-решений лучше под­
ходит для вас. Другим плюсом также является большая производительность в срав­
нении с реляционными решениями. Однако нельзя забывать про надежность и со­
гласованность данных, поскольку в большинстве случаев NоSQL-решения не пред­
лагают привычных для разработчиков АСID-свойств. В приложениях, где нам
критически
важна
целостность,
есть
смысл
использовать
классические
решения.
Кроме того, необходимо учитывать опыт команды разработки, скорее всего, она
имеет больше опыта в мире SQL, чем NoSQL.
Поэтому не стоит гнаться за одним идеальным решением. Хорошей практикой
будет проведение определенных экспериментов для выявления лучших решений
в вашем бизнес-контексте, а также применение лучших практик, которые приняты
в сообществе разработчиков. Стоит также учитывать экспертизу и поддерживае­
мость решения, а также изучить его историю и количество других сервисов, в кото­
рых оно уже применялось. Итеративный подход в этом вопросе, как и во множестве
других, связанных с разработкой, помогает строить надежные и эффективные сис­
темы, успешно решающие поставленные задачи.
ГЛАВА 8
Сложная логика приложений
Бизнес-задачи, решаемые с помощью программного обеспечения, бывают совер­
шенно разными. Некоторые из них могут быть довольно простыми, например со­
хранять входящие заявки, другие же системы могут быть сложны не только тем,
что содержат в себе сложные бизнес-правила, но и, например, требуют большой
пропускной способности.
Одним из важных критериев программных систем является простота поддержки и
простота развития. Как уже говорилось ранее, часто большую трудность составляет
не столько разработка в моменте, сколько возможность изменения в соответствии
с меняющимися требованиями. Ввиду требований бизнеса, которые действительно
могут меняться, потому что меняется сама среда существования бизнеса, успех
этих самых изменений нередко упирается в совершенство программных систем.
Но, как и множество других вещей, программные системы делают люди. Да, ввиду
последних трендов в мире LLM есть шанс, что код будут писать машины, а не раз­
работчики, но пока это остается лишь концептом, причем далеким от реальной
замены. Поэтому на разработку пока накладываются такие же ограничения управ­
ления людьми и трудности их эффективной организации.
В этой главе мы затронем тему того, как сложная логика существует не только
в рамках
приложения,
распространяется
где она
дальше,
на
выражается
неосязаемые
множеством строк кода,
слои,
такие
как
но
и как она
архитектура,
векторы
развития проекта, бизнес-ландшафт и данные.
Откуда берется сложная логика
Бизнес-требования
Нередко при построении новой бэкенд-системы берется сравнительно небольшой
объем того, что можно цифровизировать. Например, вместо того чтобы сразу на­
чать разработку полномасштабной системы, которая должна будет выдерживать
миллионы пользователей, хранить тысячи бизнес-правил и еще иметь внутри себя
искусственный интеллект, нередко начинают с чего-то простого. Например, могут
оптимизировать внутренние процессы, интегрировать какие-то практики из
ERP-
Глава 8. Сложная логика приложений
133
систем. Такой подход позволяет лучше понять особенности работы, причем как
системы, так и людей в ней. Если новый функционал приживается, это может быть
важным сигналом для команды, поскольку похожие функции могут быть также
востребованы пользователями. В рамках системы может появиться все больше и
больше правил, которые напрямую выходят из особенностей бизнеса, например
специфические жизненные циклы заявок, правила доступа, роли и т. д.
Поэтому одно из первых мест, в котором, как правило, зарождается сложная логика
приложений, -
бизнес-требования. В одной из предыдущих глав подчеркивалась
важность такой фигуры, как бизнес-аналитик, -
связующего звена между миром
бизнеса и миром разработки. Даже бизнес-правила сами по себе, нарисованные
в виде схем и сценариев, могут быть очень сложными. Еще больше сложностей
может появиться на этапе разработки, на котором, помимо простого пути, необхо­
димо заложить ситуации, когда все идет не по плану, например когда что-то слома­
лось во время прохождения очередного процесса. Для того чтобы упростить задачу,
важна максимальная прозрачность и понятность протекания процессов. Важно не
допустить ситуаций, когда команда разработки и заказчик понимают бизнес-задачу
по-разному.
Одной из неизменных особенностей бизнес-требований является их неизбежное
изменение. Причем изменяться они могут как со стороны заказчика, так и со сторо­
ны самой команды разработки. Если в первом случае все относительно понятно:
заказчик захотел реализовать что-то иначе, то что можно сказать про второй? Это
одна из ключевых особенностей разработки, это некий уровень неопределенности.
До того как система будет полностью реализована, невозможно точно предсказать
затрачиваемое время, а также все
подводные камни,
которые могут появиться
во
время прохождения пути разработки продукта. Именно поэтому для организации
работы используются такие методологии, как Agile, Kanban и Scrum, которые не
пытаются
понизить
эту
неопределенность,
-
они
стараются
адаптировать разра­
ботку к ней. Эта неопределенность появляется из-за так называемых неизвестных
неизвестных (things you don't know you don't know), которые проявляются исклю­
чительно во время процесса разработки. Например, во время этапа определения
бизнес-требований мы планировали добавить интеграцию со сторонним сервисом,
предоставляющим для нас некую важную информацию. Но во время реализации
этой интеграции мы узнали, что эта система имеет ограничение на количество за­
просов в день либо имеет слишком большую задержку, из-за чего наша система
может столкнуться с понижением производительности. Либо схема данных этого
сервиса сильно отличается от того, что мы ожидали перед интеграцией, ввиду чего
требуется дополнительное время на добавление уровня конверсии, или может по­
требоваться использование иного сервиса.
Описанные ситуации можно перечислять бесконечно, и их невозможно предуга­
дать. Как правило, основываясь на опыте бизнес-аналитиков и архитекторов, мож­
но предусмотреть потенциально большее количество случаев, но не исключить их
полностью. И именно такие обстоятельства могут заставить разработчиков переде­
лывать бизнес-требования и согласовывать их с заказчиком, потому что то, что бы­
ло согласовано заранее, реализовать уже невозможно. Это абсолютно нормальный
процесс итеративной разработки, который позволяет совершенствоваться и бизне-
Часть /. Проектирование и разработка системы
134
су, и разработчикам, поэтому не стоит стараться с первой попытки угадать сразу
все узкие места, нужно двигаться итеративно, от простого к сложному.
Операционные данные
В одной из прошлых глав мы разобрали основные инструменты для моделирования
самих данных. Их многообразие и большое количество функций и механик ясно
намекают на то, что сложная логика приложений может храниться на уровне дан­
ных.
На мой взгляд, именно данные являются самым ценным активом программных сис­
тем. Благодаря им существующие бизнес-правила становятся прозрачными для на­
блюдателя,
поскольку
мы
можем
визуализировать
промежуточные
состояния,
смотреть за их изменениями для того, чтобы строить полезные системы. Помимо
простой
наблюдаемости, данные
могут напрямую
влиять на бизнес-процессы,
решая, по какому пути все будет развиваться в конкретной ситуации.
Пользователи, их действия, роли, устройства, бизнес-процессы -
все это малень­
кий пример того, что требует моделирования, чтобы это не было бесполезным на­
бором байтов.
Моделирование схем данных похоже на моделирование бизнес-правил в рамках
программного кода, для них мы определяем структуры, модели изменения и спосо­
бы чтения, которые мы планируем использовать в дальнейшем. Однако у данных
есть одна любопытная особенность -
эволюция.
Как правило, когда у нас встает вопрос об изменении схемы данных, на нашей
платформе уже могут содержаться какие-то данные, которые мы не можем просто
так взять и выбросить. Например, это информация самих пользователей, отчет­
ность, записи каких-либо процессов. И что делать, если наше новое видение схемы
не соответствует тому, что у нас есть сейчас?
Создание самой модели данных -
это далеко не самый простой процесс, потому
что в рамках него нужно предусмотреть довольно много, чтобы обеспечить удоб­
ную работу с информацией. А теперь представьте, что со временем на нашей плат­
форме изменяются бизнес-требование либо видение (как мы уже определили, это
неизбежно) и нам нужно аккуратно внести это новое видение без потерь данных и
нарушения их согласованности и ценности. Все это вкупе может привести к услож­
нению логики хранения данных, в которой со временем можно начать путаться.
Новые бизнес-правила либо изменение уже существующих, скорее всего, будут
приводить к изменению не только логики кода, но и схем данных, поэтому вопрос
их эволюции стоит остро. Если описанные в коде бизнес-правила часто не имеют
явного состояния, которое можно поломать при развертывании новой версии, то
в рамках систем операционных данных это может быть проблемой. Мы не можем
просто выбросить какие-то данные, чтобы применить новую схему; нам важно про­
думывать сценарии миграции, чтобы сохранить данные пользователей.
Еще одна сложность связана с тем, что данные могут храниться не только в одном
месте. Для монолитных приложений это может быть не таким актуальным, однако
135
Глава 8. Сложная логика приложений
для микросервисов либо случаев, где мы имеем несколько баз данных, оптимизи­
рованных под конкретные нужды,
это
может создавать определенные трудности.
Например, в случае распределенных систем нам важно сохранять целостность дан­
ных, чтобы, предположим, данные в одной системе не ссылались на несуществую­
щие объекты другой. Согласованность данных -
отдельный вопрос, который ста­
новится особенно острым при моделировании потоков информации.
Стоит особо упомянуть то, что данные могут иметь разные уровни чувствительно­
сти. Это означает, что наш сервис может работать, например, с медицинской ин­
формацией либо мы должны подчиняться определенным законам по обращению
с персональными данными. Так, в случае с последним примером, в рамках законо­
дательной базы мы обязаны предоставлять пользователю всю собранную информа­
цию о нем либо иметь возможность полностью удалить ее. Это не слишком сложно,
если у нас всего одна база данных, но если их несколько либо мы даже имеем спе­
циальный архив, в котором информация этого пользователя может содержаться,
либо у нас есть брокер событий, в котором сообщения могут относиться к конкрет­
ному пользователю, то всё это необходимо учитывать.
Что касается медицинских данных, тут все может быть еще сложнее. Во-первых,
они не должны храниться в открытом виде -
обязательно применение шифрова­
ния.Во-вторых, доступ к таким данным нередко не могут иметь даже сами разра­
ботчики, чтобы не нарушать конфиденциальность пользователей. В-третьих, мы не
можем удалить эти данные просто так, мы можем быть обязаны хранить их и архи­
вировать в течение определенного времени.
Подобные ограничения и особенности довольно разнообразны и зависят напрямую
от бизнес-домена и законодательной базы. Ни то ни другое не уменьшает слож­
ность логики, поэтому важно помнить об этом.
Интеграционная логика
Эта тема уже частично затрагивалась, но хочется раскрыть ее подробнее. Интегра­
ция часто бывает неотъемлемой частью разработки, потому что существует боль­
шой шанс, что до момента создания новых сервисов уже существует целый пласт
программ, интегрированных в бизнес-процесс. К таким программам могут отно­
ситься не только разработанные с нуля сервисы, но и, например, какие-то SааS­
решения, СRМ- или ЕRР-системы.
Использование подобных готовых решений может помочь бизнесу в цифровизации
без привлечения большого количества сторонних разработчиков. Ввиду того что
типовые задачи могут быть похожими, например ведение базы клиентов, управле­
ние заказами, отправка сообщений, существует большое количество сервисов, по­
зволяющих цифровизировать некоторые части процессов.
Однако с ростом бизнеса готовые решения могут либо не подходить по функциона­
лу, поскольку бизнесу требуется что-то специфическое, либо не подходить по
условиям использования, к примеру быть слишком дорогими или недостаточно
безопасными. Но что делать, если какой-то функционал из тех систем все-таки ва-
Часть /. Проектирование и разработка системы
136
жен и важны данные, которые там генерируются? Вот тут и встает вопрос инте­
граций.
Интеграции фактически -
это ситуации, при которых наши бизнес-правила могут
растекаться на несколько различных систем либо даже команд. Интеграции могут
иметь
разные
направления,
например
мы
можем
выгружать
данные
из
системы,
с которой интегрируемся, либо мы сами можем загружать в нее информацию, либо
связь может быть двунаправленной.
Начнем с ситуаций, когда мы должны выгружать какие-то данные. Казалось бы,
звучит просто, но, как и всегда, тут скрыто много неочевидных моментов. Напри­
мер, нам крайне необходимо получать данные максимально быстро, как только они
появились в целевой платформе. Если нам повезло, наша целевая система может
уже содержать в себе функционал для реализации веб-хуков, который мы можем
использовать уже на своей стороне для реализации моментальных нотификаций.
С другой стороны, если веб-хуков нет, нам придется организовать опрос, чтобы
минимизировать расхождения
между данными. Но тогда мы можем упереться
в ограничение количества запросов. Также нам может понадобиться хранить со­
стояние данных, чтобы понимать, что является новой порцией данных, а что мы
уже имеем в рамках своих систем, и тем самым избежать дублирования. Это про­
стой пример, но даже в нем появляются некие ограничения, которые важно учи­
тывать.
Теперь перейдем к другой ситуации, когда нам необходимо отправить запрос в сис­
тему. Представим, что у нас есть внешний сервис нотификации клиентов. Это до­
вольно популярный вариант интеграций, когда мы выносим какую-то конкретную
логику, например в нашем случае это отправка электронной почты или SMS. По­
добные сервисы нередко имеют удобные API либо даже SDK для того, чтобы упро­
стить интеграцию с ними. Однако так может быть тоже не всегда. Как и в преды­
дущем примере, мы должны учитывать возможные неправильные сценарии работы.
Что нужно делать нашему приложению, если, например, наш сервис отправки со­
общений в принципе недоступен? Как мы будем действовать в этом случае, как мы
отметим в рамках нашей системы, что мы не смогли завершить запрос? Причем
недоступность тоже бывает разных видов: помимо простого отсутствия соединения
с сервисом, мы можем превзойти лимит обращений, либо наши сообщения могут
не проходить по внутренним требованиям данного сервиса. Все это так или иначе
нужно учитывать, потому что одна из особенностей программных систем -
они не
умеют «разбираться по ходу дела», если что-то идет не так.
Другая особенность интеграции связана с людьми. Часто нам нужно будет прово­
дить интеграцию не только с внешними сервисами, которые продают свои услуги.
Такие сервисы, как правило, имеют хорошую документацию и службу поддержки,
поскольку этот сервис буквально продает свои услуги, поэтому важно обеспечить
качество. Однако что касается внутренних интеграций, то тут ситуация может от­
личаться.
Сервисы не существуют сами по себе, их всегда создают люди. И в случае когда
наша интеграция требует более тесного взаимодействия, нам неизбежно придется
137
Глава 8. Сложная логика приложений
взаимодействовать с людьми. В этом случае к технической сложности может доба­
виться сложность организационная.
Во-первых, другие команды разработки имеют собственные задачи и собственные
проблемы, которые напрямую связаны с их системой. У них могут быть свои ка­
лендари релизов, свои планы, функционал и в соответствии с этим они могут рас­
считывать свои ресурсы и силы на разработку. Когда появляется необходимость
выполнить интеграцию, это может требовать от команды разработки сервиса инте­
грации новых функций либо углубленного тестирования существующих. Когда это
требуется вставить в плотное расписание, нередко пожертвовав какими-то другими
задачами, это может вызывать накладки в интеграционном процессе.
Вторая проблема связана с ограниченными контекстами. Например, когда мы раз­
рабатываем что-то, со временем мы можем начать неплохо разбираться во внут­
ренней кухне процессов. Те решения, которые принимаются на проекте, связанные
с дизайном системы, функциями, особенностями, могут быть кристально понятны
для разработчиков. Однако это любопытная ловушка -
для стороннего наблюда­
теля наша система может быть максимально непонятной. Часто интеграции с дру­
гими системами проходят труднее, чем ожидается, ввиду того, что одна из сторон
может не раскрывать достаточное количество деталей, ожидая от другой стороны,
что «тут и так все понятно». На самом деле это может быть далеко не так. При ин­
теграции вашей системы с системами других команд разработки лучше не срезать
углы, а стараться пояснять максимальное количество деталей для того, чтобы у
других команд не оставалось белых пятен. То же самое работает и в обратную сто­
рону: спросить какие-то вещи, которые могут быть вам неочевидны, гораздо де­
шевле, чем начать строить систему на представлениях о работе другой системы,
которые могут оказаться ошибочными.
В соответствии с этим также лучше закладывать на разработку интеграций некий
запас по времени,
поскольку могут появиться непредвиденные моменты, которые
были описаны выше.
Очевидно, что чем больше у нас таких интеграций, тем больше сложности появля­
ется в нашей системе, притом что сложность не будет расти линейно, она, скорее,
будет стремиться к экспоненциальной -
чем больше у нас узких мест и сложных
сценариев, тем сложнее их поддерживать все вместе и обеспечить их надежную
работу.
Сложные части. Связанность и связность
Если до этого мы разбирали некие абстрактные понятия, то теперь стоит посмот­
реть на уровень глубже сы -
на саму реализацию. Как уже говорилось ранее, серви­
это не магия, они реализуются с помощью разработчиков и программного
кода. Ввиду того, что моделируемые области могут быть сложными, они приводят
к более сложным кодовым реализациям, которые сложны не только объемом, но и
внутренними связями между компонентами системы.
Есть одна прекрасная фраза: «Совокупная работа системы -
это не сумма свойств
ее частей, а произведение их взаимодействий». Программные системы вне зависи-
138
Часть /. Проектирование и разработка системы
мости от паттернов проектирования и масштаба так или иначе имеют некую струк­
турную организацию. На то есть явная причина: неясная структурная реализация
системы усложняет одно из важнейших свойств -
внесение изменений. Как уже
упоминалось ранее, самое сложное в рамках разработки -
это не столько разрабо­
тать систему в моменте, сколько сделать возможным добавление в нее нового и
уметь предусматривать ситуации и места, в которых могут произойти изменения,
чтобы безболезненно их внести. С другой стороны, такие размышления о возмож­
ных изменениях вынуждают добавить в рамках текущей разработки некоторые ус­
ложнения, которые хотя и не ускорят достижение конкретной цели, но облегчат
жизнь при последующей эволюции системы. Для того чтобы точнее сформулиро­
вать
описанное
выше,
стоит
выделить
два
легендарных
понятия:
связанность
(coupling) и связность (cohesion).
В рамках русского языка эти понятия звучат почти одинаково, но они имеют раз­
ные смыслы. За счет этих двух понятий можно описать не только равновесие взаи­
модействий в системе, но и структурную организацию, ограниченные контексты
и совершать анализ компромиссов для тех или иных архитектурных решений.
Начнем с того, что такое связанность. Как уже говорилось ранее, моделируемая
область, как правило, имеет связи между компонентами, что напрямую влияет на
сложность разработки. При этом, что любопытно, программная реализация может
вовсе не отражать эти связи в явном виде, скрывая их за абстракциями программ­
ного кода, которые были бы удобны для разработчиков. Такое «упрощение» может
иметь последствия, поскольку, если нам будет необходимо изменить что-то в связи
с изменением бизнес-требований, эти изменения могут просто не вписаться в орга­
низацию кодовой базы. С другой стороны, если мы поддерживаем логичные абст­
ракции кода, объединяем модули в соответствии с некой логикой либо вовсе имеем
отдельные сервисы для отдельных бизнес-задач, внесение изменений в такую
структуру может быть значительно более гармоничным процессом. И теперь, когда
мы имеем эти два примера, мы можем дать определение связанности. Я не уверен,
что существует книжное определение, поэтому попробуем сформулировать его са­
мостоятельно.
Связанность (coupling) -
свойство программных систем, определяющее степень
глубины взаимодействия компонентов, которая выражается в том, насколько они
влияют друг на друга и насколько сильно нам могут потребоваться изменения
в других компонентах при изменении нашего целевого компонента. Если объяснять
простыми словами, связанность занность, как правило,
это мера внешней зависимости. Высокую свя­
имеют системы, в которых не закладывали определенные
правила при разработке либо не контролировали их в достаточной степени и до­
пускали просачивание бизнес-логики из разных доменных областей. Например, мы
начинали строить систему управления складскими запасами, и со временем система
стала разрастаться, и в ней появился функционал уведомлений, управления пользо­
вателями, логика отправки и доставки. По сути, все эти области относятся к разным
доменам, но в рамках кода они могут, скажем так, храниться в одном месте. Другой
момент связан с тем, что сами модули могут активно использовать код друг друга.
Например, логике, которая отвечает за уведомления, полезно иметь логику взаимо-
Глава 8. Сложная логика приложений
139
действия с пользователями, например запрашивать их данные. Мы можем просто
импортировать
эту
логику,
как
пакет,
в
наш
модуль,
поскольку
мы
существуем
в рамках одного приложения. Однако проблема заключается в том, что мы только
что повысили связанность нашего приложения. Заключается это в том, что если
сервису взаимодействий с пользователями потребуется поменять логику, которую
импортировал другой сервис, то это может привести к ошибкам, связанным с тем,
что переделанный модуль может иметь другой интерфейс или логику, которая со­
ответствует ограниченному контексту управления
пользователями,
но
не сервису
уведомлений. На первых этапах подобные случаи могут быть единичными, однако
со временем связи между компонентами могут становиться все более запутанными,
нередко они в принципе могут не иметь логики, потому что единственное, чему они
могу служить,
-
получение функционала, поскольку он уже реализован где-то
еще. Это приводит к паттерну «Большой шар грязи», который вызван тем, что связи
между компонентами становятся слишком сложными, и если визуализировать их,
то получится что-то, похожее на шар грязи. Очевидно, что внесение изменений
в такую структуру -
крайне сложная задача, потому что шанс сломать что-то при
изменении становится практически стопроцентным.
Подобные ситуации нередко происходят в монолитных приложениях ввиду того,
что нам необходимо иметь большое количество сложной логики «под одной кры­
шей», а со временем эта логика может начать перемешиваться между собой. Сис­
темы с высокой связанностью трудно поддерживать, потому что подобные меж­
компонентные взаимодействия затрудняют внесение изменений, а также затрудня­
ют отладку. С другой стороны, если мы стремимся к максимальной скорости
достижения нашей конечной цели, мы можем позволить себе срезать углы, не за­
думываться о зависимостях и дальнейших удобствах для поддержки и делать так,
как быстрее. Однако такие ситуации бывают редко, хотя они и случаются, -
я не
рекомендую так делать без явной на то причины.
Программные системы можно характеризовать еще одним понятием -
связностью.
Вернемся к нашему примеру с монолитным приложением. Внутри него находится
логика из разных областей, при этом на программном уровне она сильно перепле­
тается из-за особенностей в организации кодовой базы. Это значит, что мы не мо­
жем выделить какие-то явные ограниченные контексты, разделить все по тому, ка­
кие функции оно выполняет, ввиду того, что зависимости между частями системы
слишком запутанные и нелогичные. С другой стороны, если мы имеем несколько
сервисов, каждый из которых отвечает за одну конкретную доменную область и
ведется отдельной командой разработки, ввиду конкретности решаемой бизнес­
задачи в нем маловероятно присутствие логики, которая выходит за рамки ограни­
ченного контекста сервиса. Именно такие формы и описывает термин «связность».
Связность (cohesion) -
это свойство программного компонента, которое характе­
ризует отношение его функций к конкретной области, домену без перекрытия
с другими доменными областями. Опять же, если упрощать, высокая связность
компонента означает, что он содержит в себе необходимую логику, которая отно­
сится к конкретной доменной области, и отсутствует размазывание ограниченного
контекста в рамках компонента. Компоненты с высокой связностью, как правило,
140
Часть /. Проектирование и разработка системы
более стабильны, потому что их изменение не приведет к необходимости измене­
ния в других компонентах по той простой причине, что другие компоненты просто
не отвечают за то, что выполняет наш изначальный сервис. И наоборот, компонен­
ты с низкой связностью хотя и могут выполнять бизнес-задачу, но понятие бизнес­
контекста в них размыто, и они могут отвечать за фактически несвязанные между
собой вещи.
Такие понятия, как связанность и связность ложатся в основу построения архитек­
туры программных систем. Сама идея такого подхода находит свое отражение
в DDD, в котором ограниченные контексты и домены помогают в моделировании
программных систем. Системы, которые обладают высокой связностью и низкой
связанностью, как правило, проще поддаются изменению и модификации, над ни­
ми одновременно может работать больше разработчиков, потому что шанс что-то
случайно нарушить в другом сервисе при работе над своим значительно меньше.
Как правило, такое наблюдается в микросервисных архитектурах, хотя это и не
обязательно (мы рассмотрим с вами это позже). И с другой стороны, сервисы с низ­
кой связностью и высокой связанностью имеют размытые ограниченные контек­
сты, сильные зависимости друг с другом, которые вытекают в неизбежность изме­
нений в других сервисах, если нам необходимо поменять функционал в одном из
сервисов.
Основной оптимизационной задачей является разработка сервисов с высокой связ­
ностью и низкой связанностью. Однако это имеет некоторые последствия, о кото­
рых мы сейчас поговорим.
Во-первых, ничего не дается просто так. Если мы хотим иметь высокую связность и
низкую связанность сервисов, нам необходимо правильно проектировать архитек­
туру системы. Мы должны правильно применить
DDD, выделить ограниченные
контексты и на их основе определить, как именно мы будем разрабатывать наши
сервисы.
Помимо трудностей в проектировании, появляются определенные трудности при
разработке. Большее количество сервисов, пусть с высокой связностью и низкой
связанностью, добавляет операционные расходы и усложняет управление системы.
Во-вторых, никуда не исчезла координация между сервисами, и хотя сейчас они
отвечают каждый за свою область, нельзя забывать, что именно произведение их
взаимодействий позволяет реализовать бизнес-функционал.
Третий момент связан с самим написанием кода. Грамотный код, который имеет
высокую связность, достаточную абстрактность и предусматривает расширение,
писать гораздо сложнее, чем что-то такое, что сразу может решить нашу задачу, но
без возможности ее удобного расширения. Во всем должен быть баланс, и это не
значит, что везде надо стремиться к переусложнению. Но поскольку на определен­
ном уровне абстракции сервисы упираются в код, то писать его грамотно может
быть сложнее, чем писать неграмотно. Другой момент заключается в том, что для
написания такого качественного кода необходимо достаточно хорошо знать про­
граммный стек, и в случае современных реалий, когда разработчики могут знать
несколько технологий одновременно, далеко не все могут знать идеально именно
Глава В. Сложная логика приложений
141
ту технологию, на которой планируется реализация. Поэтому важно оценивать на­
выки разработчиков, понимать, насколько возможна дальнейшая поддержка про­
дукта в рамках текущей технологии и насколько она удобна для разработчиков.
Резюмируя: высокая связанность означает, что модули сервиса связаны с собой
слишком сильно, ввиду чего изменение в одном неизбежно потребует изменения
в другом. Низкая связанность, в свою очередь, означает, что модули приложения
отвечают за свои ограниченные контексты, не забирая чужие зоны ответственно­
сти, за счет чего внесение изменений происходит проще -
меньше шанс задеть
стороннюю логику. С другой стороны, низкая связанность требует определенных
архитектурных практик, ревью, знания технологий, поэтому разработка приложе­
ний в этом случае будет занимать больше времени для решения самой бизнес­
задачи, зато гораздо проще будет даваться развитие проекта в случае изменения
бизнес-требований.
Связность -
это мера, касающаяся конкретного модуля. Высокосвязные системы
имеют разграничение зон ответственности, которые, как правило, вырисовываются
с помощью DDD и итеративных подходов разработки. Как и в случае со связанно­
стью, подобный подход позволяет упростить развитие системы, добавление в нее
новых функций, однако это усложняет текущую разработку и вносит тру дн ости
в управление платформой.
В этих вопросах также важна итеративность. Невозможно с первой попытки пра­
вильно
ру, -
угадать
все
ограниченные
контексты
и
построить
идеальную
архитекту­
скорее всего, у вас это не получится. Вместо этого нужно направить свои
усилия на получение достаточной обратной связи при разработке, чтобы после­
дующие шаги в развитии вашей системы приводили к ее планомерному улучше­
нию, повышению прозрачности и понятности. Те команды, которые полноценно
используют этот принцип, в конечном итоге строят более эффективные системы.
Теперь, когда мы имеем представление о том, в чем отличие между связанностью и
связностью сервисов, мы можем рассмотреть частные случаи. Очевидно, что ввиду
некой схожести бизнес-задач существуют лучшие практики и паттерны, которые
позволяют адаптировать разработку без необходимости изобретать велосипед. По­
этому мы поговорим про паттерны проектирования и архитектуры.
Архитектурные стили
Лучшие практики и паттерны проектирования -
это крайне любопытная вещь.
С одной стороны, основная цель такого подхода -
это стандартизация и классифи­
кация сервисов для того, чтобы иметь набор методов и практик, позволяющих бы­
стрее решать задачи. С другой стороны, любая новая программная система не будет
похожа ни на одну из существующих. Хотя она и 'может решать похожие бизнес­
задачи,
каждая из них все равно уникальна, что
ботку. Поэтому одно из главных напутствий -
накладывает отпечаток на разра­
не стараться втиснуть в ваш про­
цесс разработки паттерны, которые были выявлены при разработке хотя и множе­
ства систем, имеющих много общего с вашей системой, но все-таки чужих систем.
Процесс оценки компромиссов и выбора пути разработки лежит именно на вас.
Часть /. Проектирование и разработка системы
142
С другой стороны, общие принципы есть, и они невероятно полезны, но как стар­
товая точка и пища для размышлений. Знания о том, как похожие задачи решались
ранее, какие проблемы при этом возникали, помогут вам лучше разобраться со
своими собственными вопросами, возникающими в процессе разработки.
Принимая во внимание связанность и связность, мы можем выделить три основных
типа бэкенд-систем, которые вы, вероятнее всего, будете встречать в рамках прак­
тики. Вы можете без проблем выбирать один из этих стилей для решения своих
задач в соответствии с ограничениями и бизнес-правилами.
Монолитные приложения
Начнем с хорошего исторического примера, который ранее встречался повсемест­
но, -
монолитных приложений. Возможно, у многих разработчиков возникает во­
прос: почему, имея такое количество минусов (которые мы рассмотрим впоследст­
вии), монолитная архитектура была настолько распространена раньше? На это есть
несколько любопытных причин. Но перед этим давайте вкратце опишем, что такое
монолитная архитектура.
Как уже говорилось ранее,
моделируемая предметная
область не обязательно
должна находить свое полное отражение в программной реализации. Иными сло­
вами, мы можем реализовать всю логику с помощью одного приложения. Такой
подход достаточно удобен, когда количество разработчиков в нашей команде не­
большое и мы хотим уменьшить трудности, связанные с оркестрацией программ­
ных частей.
Как правило, монолитное приложение следует именно такому пути -
вся бизнес­
задача решается с помощью одного приложения и одного программного стека. Та­
кое приложение имеет одну базу данных, традиционно -
реляционную, в которой
происходит моделирование различных ограниченных контекстов для одной общей
бизнес-задачи. Для удобства само приложение разбивается на модули, которые мо­
гут отвечать за собственные бизнес-контексты, однако, как правило, данные всего
приложения хранятся в одном месте.
Такой подход имеет определенные плюсы. Во-первых, ввиду того, что приложение
одно, нам удобнее его разрабатывать и разворачивать, потому что все части, необ­
ходимые для тестирования и разработки, уже присутствуют в приложении -
оно
разворачивается одним целым. За счет того, что используется единый программный
стек, разработчики имеют возможность импорта модулей, которые содержат уже
реализованный функционал. Таким образом, разработчики могут иметь общие мо­
дули, которые, например, реализуют логирование событий, и, следовательно, раз­
работчикам не придется каждый раз реализовывать свою логику с нуля. Третий
плюс связан больше с людскими ресурсами: за счет единообразия программного
стека увеличивается глубина знаний у работающих с ним людей, что положительно
влияет на качество разрабатываемого кода. Также проще проводить ревью кода.
Монолитное приложение традиционно использует одну базу данных, которая при­
меняется сразу для нескольких бизнес-задач (рис. 8.1 ). Использование единой базы
данных позволяет обеспечить АСID-свойства для приложения. То есть, даже если
Глава 8. Сложная логика приложений
143
наша транзакция затрагивает разные доменные области, мы можем обеспечить пра­
вильное ее протекание в соответствии с АСID-свойствами базы данных. Это может
быть чрезвычайно полезно, если наши ограниченные контексты требуют строгой
согласованности между собой: ввиду наличия одной базы данных реализация это­
го
-
дело техники.
Интерфейс
Сервис
База
данны х
Рис. 8.1. Монолитное приложение
Однако у всего этого есть последствия. Во-первых, очень трудно уместить в рамках
одного приложения много разной логики. Это, несомненно, возможно, но это тре­
бует определенных усилий по организации кода. Если разрешить беспорядочный
импорт библиотек либо бездумное использование чужих модулей, кодовая база
может превратиться в один «большой шар грязи», внесение изменений в который
будет практически невозможным.
Когда кодовая база становится большой и отвечает за множество различных функ­
ций, внесение изменений становится непростой задачей, потому что увеличивается
шанс поломать другой функционал. Одной из похожих проблем является трудность
определения места в коде, куда необходимо внести новую логику, тия
ограниченных
контекстов
все
труднее
становится
понять,
из-за размы­
какая
часть
кода
отвечает за конкретную бизнес-задачу. В конечном итоге, когда мы занимаемся
развитием нашего проекта, это замедляет этот процесс, причем со временем с до­
бавлением новых функций он становится все более и более медленным.
Одной из других проблем, которая также влияет на скорость разработки, является
сложность параллельной работы множества разработчиков. Из-за той же проблемы
связанности модулей одни разработчики могут работать над функционалом, кото­
рый требует определенную зависимость, и в то же время другая команда может
сильно видоизменять импортируемый модуль. Избегание таких ситуаций требует
144
Часть /. Проектирование и разработка системы
координации разработки, но, как правило, даже если она высокая, изменения при­
ходится вносить последовательно для того, чтобы не создать большее количество
ошибок. В рамках бизнеса это замедляет развитие проекта, что может выливаться
в упущение конкурентного преимущества и уменьшение удовлетворенности поль­
зователей.
Монолитные архитектуры традиционно имеют проблемы с надежностью и масшта­
бируемостью. Ввиду того что все операционные данные приложения хранятся
в одном месте, а именно в одной базе данных, она является узким местом серви­
са, -
если база становится недоступной, все приложение также становится недос­
тупным. Другая проблема ассоциирована с производительностью. Как мы помним
из главы про операционные данные, монолитная база данных поддается только
вертикальному масштабированию, без потери АСID-свойств. Поэтому, если нам
необходимо обрабатывать огромное количество запросов, это может стать для нас
проблемой. Да, мы можем заниматься горизонтальным масштабированием нашего
приложения, монолитная архитектура этого не запрещает, однако если много логи­
ки у нас заложено на уровне SQL-запросов, то это может не дать достаточного при­
роста производительности.
Часто бывает, что запросы к нашей системе происходят несимметрично ее функ­
ционалу, т. е. какие-то наборы функций используются чаще остальных. В случае
монолитных архитектур у нас нет возможности выделить больше ресурсов под
конкретную часть приложения -
потому что оно является одним целым. Поэтому
горизонтальное масштабирование не является столь эффективным, как в случае,
например, микросервисов, потому что мы используем ресурсы не так эффективно
ввиду того, что логика, которая не нуждается в масштабировании, занимает наши
вычислительные ресурсы.
Если смотреть на монолитную архитектуру с точки зрения связанности и связно­
сти, то традиционно величина связанности является большой, а связности -
низ­
кой. Высокая связанность объясняется тем, что бизнес-логика в рамках одного при­
ложения структурно
находится в одном месте, допуская импорты из разных огра­
ниченных контекстов. Подобные удобства при неправильном использовании могут
создавать большое количество проблем впоследствии, поскольку внесение измене­
ний может быть сопряжено с шансом нарушить функционал в другой предметной
области.
Низкая связность обусловлена тем, что приложение отвечает за всю бизнес-задачу
сразу, не имея снаружи себя разделения на основе ограниченных контекстов. Да,
мы можем иметь подобное разделение на уровне API, но это не гарантирует гра­
мотного разделения ответственности на уровне кода. Ситуация осложняется тем,
что само приложение
может иметь
низкую связность на уровне данных, где ради
достижения нормализации ограниченные контексты могли перемешиваться между
собой. Такой вид связанности, а именно связанности по данным, является одним из
самых крепких и трудно исправимых.
Однако сказанное не означает, что эти проблемы неизбежны. При правильном под­
ходе, моделировании контекстов и ревью кода возможно создание стабильных
монолитных архитектур. Благодаря удобству разработки появляется возможность
Глава В. Сложная логика приложений
145
быстро и эффективно решить бизнес-задачу, удобно протестировать функционал и
доставить саму систему одним куском без излишнего дробления функций.
Монолитная архитектура является хорошим выбором для небольших приложений
либо для быстрой проверки концепций. Я не могу рекомендовать паттерн монолит­
ного приложения для крупных и новых проектов ввиду названных проблем, свя­
занных с недостаточной масштабируемостью, надежностью, возможностью парал­
лельной работы над платформой множества разработчиков. Это ни в коем случае
не делает подобную архитектуру плохой или хорошей, все имеет свои плюсы и ми­
нусы, а стоимость их влияния на конкретную бизнес-задачу -
вопрос сугубо инди­
видуальный. Также монолитная архитектура встречается на старых, так называе­
мых lеgасу-проектах. На вопрос, почему данный паттерн при всех его, казалось бы,
очевидных минусах, выбирался раньше, мы ответим чуть позже, когда рассмотрим
остальные варианты.
Сервис-ориентированная архитектура
Некоторым логическим продолжением монолитной архитектуры является подход
сервис-ориентированной архитектуры, или service oriented architecture (SOA). В чем
заключаются основные отличия?
Как мы помним, монолитная архитектура подразумевает наличие одного приложе­
ния, которое отвечает за моделирование всей бизнес-логики, присутствующей на
проекте. Данное
приложение
имеет одну
реляционную базу данных,
которая
хранит в себе все операционные данные платформы. SOA позволяет решить сразу
несколько узких мест монолитной архитектуры.
По сути, основным отличием SOA от монолита является множество сервисов, ко­
торые продолжают иметь одно место хранения операционных данных То есть у нас появляется новый уровень разделения бизнес-логики -
одну БД.
не отдельные
модули или библиотеки, а отдельные сервисы.
Данные сервисы развертываются
независимо друг от друга,
могут запускаться
в том числе в распределенном окружении. При этом подчеркну ключевую деталь:
эти сервисы имеют общую базу данных (рис. 8.2).
Само разделение на сервисы, как правило, напрямую связано с ограниченными
контекстами. Например, если у нас есть контексты управления пользователями,
доставками и складскими запасами, то в парадигме SOA это могут быть отдельные
сервисы, имеющие общую базу данных. Такое архитектурное решение имеет не­
сколько преимуществ.
Во-первых, данные сервисы могут масштабироваться независимо друг от друга. То
есть если нам необходимо обрабатывать больше запросов, связанных с конкретной
бизнес-областью, и при этом большая часть вычислений ложится не на базу дан­
ных, то мы можем оказаться в выигрыше от этого, не расходуя ресурсы на те части
приложения, которые в масштабируемости не нуждаются.
Во-вторых,
моделирование
на
основе
ограниченных
контекстов
положительно
влияет на удобство и скорость разработки. Выделение отдельных сервисов способ-
Часть /. Проектирование и разработка системы
146
ствует тому, что бизнес-логика не будет просачиваться в другие сервисы на уровне
кода, потому что теперь это могут быть отдельные сервисы. Это не исключает того,
что они могут иметь общие библиотеки, однако такой подход так или иначе пони­
жает связанность сервисов
и
повышает связность,
потому
что теперь
у нас
есть
более строгие границы между сервисами, выраженные на уровне архитектуры. Да,
подобное разделение не принесет плоды, если ограниченные контексты будут вы­
браны неправильно, однако такое ограничение на импорты, вызванное дизайном
системы, я считаю положительным для увеличения связности логики в каждом от­
дельном сервисе. Когда мы имеем несколько сервисов, каждый из которых отвеча­
ет за один либо несколько ограниченных контекстов, которые не имеют сильных
переплетений между собой, мы можем вовлечь большее количество разработчиков
в параллельную разработку, потому что ввиду организации уменьшается количест­
во точек сцепления, из-за которых можно что-то случайно поломать.
Интерфейс
Сервис
Сервис
Сервис
заказов
уведомлений
пользователей
База
данных
Рис. 8.2. Сервис-ориентированная архитектура
Третьим плюсом является общая база данных. Такой подход позволяет сохранить
АСID-свойства даже в том случае, если транзакция затрагивает разные ограничен­
ные контексты. Однако не стоит злоупотреблять такими возможностями, когда
один из сервисов может слишком активно изменять данные в другом домене. Если
такое поведение в вашей платформе встречается регулярно, это верный признак
того,
что стоит пересмотреть ограниченные контексты,
проверить их на коррект­
ность и, возможно, переписать перекрывающиеся части. Но возможность обеспече­
ния АСID-свойств для разных доменов приложения, даже если за них отвечает от-
147
Глава 8. Сложная логика приложений
дельный сервис, можно считать полезной, а в каких-то бизнес-контекстах -
необ­
ходимой, и это хорошо, когда такой функционал доступен прямо из коробки.
Теперь поговорим об основных недостатках такого подхода. Хотя мы и имеем не­
сколько сервисов, которые могут независимо масштабироваться, все они имеют
общий уровень
-
базу данных.
Это означает, что, если запросы происходят
в большей степени на уровне базы данных, мы все еще имеем узкое место, которое
не сможем также эффективно масштабировать. То же самое относится и к общей
надежности приложения -
если наша база данных недоступна, то недоступными
окажутся сразу все сервисы.
За счет того, что наше приложение начинает состоять из нескольких частей, услож­
няется общий менеджмент платформы. Нам нужно обеспечить развертывание этих
сервисов, настроить их мониторинг и сбор логов. Нельзя сказать, что это явный
минус такого подхода, однако не стоит забывать о сложности, которая появляется
в этом месте.
Из-за того, что в рамках такой архитектуры присутствует несколько сервисов, уве­
личивается сложность общего тестирования. Если в рамках монолитной архитекту­
ры мы имели единое приложение, которое можно запустить и протестировать кон­
кретным набором тестов, то теперь, поскольку у нас присутствует сервисное разде­
ление, нам необходимо продумывать больше сценариев тестирования, потому что
не все из них будут подразумевать запуск всего приложения. Да, разделение на ос­
новании ограниченных контекстов упрощает тестирование, поскольку нам необхо­
димо описать тесты только для конкретного контекста, но
в том случае, если
мы
хотим протестировать всю платформу целиком или оценить работу сценариев, ко­
торые затрагивают сразу несколько областей, на этом этапе могут появиться труд­
ности.
Внедрение
SOA требует уделения большего внимания управлению процессами,
чем того может требовать монолитная архитектура. Для того чтобы на практике
получить преимущества данного подхода, необходимо обеспечить надежный под­
ход к управлению зависимостями приложения. Особое внимание стоит уделить
безопасности, поскольку теперь мы имеем несколько сервисов, что потенциально
увеличивает площадь для атаки.
Если резюмировать, то часто
SOA является переходным звеном между монолит­
ными архитектурами и микросервисами. Декомпозиция старого монолитного при­
ложения является трудной задачей, которую невозможно сделать зараз, поэтому,
как правило, она выполнятся итеративно. Благодаря логике SOA, где мы имеем од­
ну общую базу данных, мы можем постепенно выделять логику монолитного при­
ложения в отдельные сервисы. Такой подход позволяет не только сохранить общую
стабильность приложения, не отрывая его от выполнения его функций, но и по­
высить опыт команды разработки, поскольку он дает возможность решать задачу
«по кусочкам», что обеспечивает минимальное количество неожиданных ошибок.
Вместе с построением новых сервисов также растет зрелость команды в DеvОрs­
части, поскольку появляется возможность настраивать мониторинг для новых сер­
висов неким стандартизированным путем, который вырабатывается каждой органи­
зацией самостоятельно в зависимости от требований и пожеланий.
148
Часть /. Проектирование и разработка системы
Микросервисы
Одним из самых новых веяний программной архитектуры является микросервисная
архитектура. Как мы разобрали на примере монолитной и сервис-ориентированной
архитектур,
в
рамках
организации
сервисов
существуют определенные
ограниче­
ния, которые не позволяют, с одной стороны, ускорить разработку, а с другой сохранить при этом достаточную надежность и обеспечить возможность удобного
внесения изменений. Хотя предыдущие варианты архитектур имеют свои неоспо­
римые плюсы, для бизнеса существуют и другие критерии, которые необходимо
соблюдать. Именно поэтому требовался совершенно новый подход, который по­
зволит вывести разработку в другую плоскость.
В чем заключается идея микросервисов? Если исходить из названия, слово «микро»
отсылает нас к небольшому размеру, и в нашем контексте это относится к размеру
сервисов. Если применить к этому идею
DDD, в которой присутствует понятие
ограниченных контекстов, все становится понятнее. Подход построения микросер­
висов гласит, что сама программная система делится на ограниченные контексты,
которые строго разделяются между отдельными сервисами. При этом каждый из
этих сервисов может иметь отдельные циклы разработки, отдельные базы данных,
API и иметь разные языки программирования. В свою очередь, достижение бизнес­
задачи обеспечивается за счет взаимодействия и оркестрации этих сервисов. У каж­
дого из этих сервисов, как правило, имеется своя собственная команда разработки,
которая нередко совмещает в себе специалистов разных ролей, способных вместе
строить сервисы под ключ. Но теперь давайте по порядку.
Основная проблема монолитной архитектуры, а также SOA -
это скорость и удоб­
ство разработки на длинной дистанции. Когда наша система обрастает большим
объемом функций, которые существуют в рамках одного сервиса, может появиться
проблема внесения новых изменений, потому что сама кодовая база может пере­
стать быть понятной для разработчиков. Когда все находится под «одной крышей»,
усложняется масштабирование процесса разработки -
мы не можем просто при­
влечь новых людей, поскольку их координация потребует больше времени, чем
время, затрачиваемое на фактическую разработку.
Есть даже любопытный анекдот: «За сколько времени ты сделаешь этот функцио­
нал? За 40 часов. А если вас будет двое? Тогда за 80». Как говорится, в любой шут­
ке есть доля правды, но в рамках этого примера этот процент значительно превы­
шает обычные значения. Координация
-
это действительно сложный процесс,
особенно когда он касается разработки.
Если мы не можем решить проблему простым увеличением количества работаю­
щих над ней лиц, нам нужно менять подход. И как нам в этом могут помочь микро­
сервисы?
Возвращаясь к теме DDD, поскольку DDD и микросервисы, на мой взгляд, это не­
разрывные вещи, мы имеем такое понятие, как ограниченные контексты. В рамках
каждого контекста существуют определенные правила и законы, а сами взаимодей­
ствия между контекстами происходят не так часто, как взаимодействия внутри кон-
Глава В. Сложная логика приложений
149
текста. Это является неплохой почвой для того, чтобы попробовать сильнее огоро­
дить контексты на программном уровне.
Возвращаясь к нашему примеру, где мы имеем задачу управления складскими за­
пасами, пользователями и доставками. Хотя это и абстрактный пример, мы можем
выделить как минимум три ограниченных контекста, в рамках которых происходит
большее количество взаимодействий, чем между ними. Микросервисы подразуме­
вает, что под каждый этот контекст мы можем иметь отдельный сервис, который
ответствен за моделирование конкретно этой бизнес-области. При этом в зависимо­
сти от самой задачи этот сервис может выбирать свои собственные технологии
и подходы безотносительно к тому, что выбрали остальные.
Выделяя ограниченные контексты в отдельные сервисы, мы решаем несколько
фундаментальных проблем монолитной архитектуры. Поскольку у нас появилось
разделение по бизнес-логике, весь код, который находится в рамках одного серви­
са, ответствен сугубо за одну область. Это означает, что у нас не будет возникать
проблем, связанных с разрешением проблемы имен, когда одно и то же слово могло
означать разные вещи в зависимости от контекста употребления. Сервисы, которые
моделируют конкретную область, проще поддерживать, потому что они реализуют
конкретные правила, мы можем удобно переиспользовать инструменты для кон­
кретной области без опасности поломать что-то в другом ограниченном контексте.
Выделение в отдельные сервисы, моделирующие свои области, позволяет ускорить
процесс разработки за счет добавления большего количество разработчиков. По­
скольку одним из важных условий ограниченного контекста является его изолиро­
ванность, логика наших сервисов тоже может быть изолирована, благодаря чему их
могут разрабатывать разные команды без необходимости общей строгой синхрони­
зации разрабатываемых функций, как это может быть в рамках монолитной архи­
тектуры.
Стоит подробнее рассмотреть самую идею разделения. Самым главным отличием
от стилей, которые мы разбирали раньше, можно считать способ хранения опера­
ционных данных. Каждый микросервис имеет свою собственную базу данных,
в которой он хранит необходимые для работы ограниченного контекста данные
(рис. 8.3). При этом, поскольку нам необязательно сохранять единообразие техни­
ческого стека в рамках всей платформы, для моделирования ограниченного контек­
ста мы можем выбирать технологии, которые справляются с этим лучше всего. На­
пример, если в рамках контекста нам важно обеспечить наименьшую задержку для
обращения и изменения данных и нам не так критична их согласованность, мы мо­
жем выбрать базу данных типа «ключ-значение». Если нам важна согласованность,
мы вольны выбрать классические реляционные БД. Такой подход позволяет не
ограничиваться одной технологией хранения данных для решения сразу всех биз­
нес-задач, а выбирать те, что не только позволят эффективно моделировать данные
и обеспечивать достаточную производительность, но и будут удобными для разра­
ботчиков.
Но если каждый сервис имеет свои собственные базы данных, то каким образом
обеспечивается обмен информацией и оркестрация вызовов? Тут появляется еще
Часть /. Проектирование и разработка системы
150
одна ключевая деталь микросервисов -
абстракция на уровне API. Предположим,
что нам необходимо получить какие-то данные либо обновить информацию о не­
кой сущности. В рамках монолитной архитектуры мы можем сразу изменить эти
данные в базе данных, потому что приложение отвечает за все ограниченные кон­
тексты. Однако в рамках монолитных архитектур доступ к базе данных имеет толь­
ко один микросервис. Это значит, что другие микросервисы не имеют права прямо­
го чтения и записи в базу данных другого сервиса. Вместо этого микросервис стро­
ит уровень
API, обращение к которому позволяют произвести изменения либо
получить информацию из целевой базы. Но в чем заключается смысл? Разве не
проще иметь возможность сразу модифицировать целевую базу данных? Тут и по­
является особый смысл API микросервиса.
Интерфейс
АРl-шлюзы,
оркестраторы
Сервис
Сервис
Сервис
заказов
уведомлений
ользователе
БД
БД
уведомлений
БД
ПОЛЬ3088ТеnеЙ
заказов
Рис. 8.3. МикросервиснаА архитектура
Глава В. Сложная логика приложений
151
Одной из самых сильных форм связанности является связанность по данным. Мо­
дификация схем данных неизбежно затрагивает те модули, которые работают с ни­
ми. Если мы позволим другим микросервисам работать с базой данных определен­
ного микросервиса, то изменения схемы затронут все сервисы, что сильно повыша­
ет связанность сервисов.
Для того чтобы изолировать изменение данных, микросервисы должны предостав­
лять возможность изменения и получения данных, используя АР!. То есть для того,
чтобы получить данные, другие сервисы должны обращаться именно к АР!. Такой
подход имеет несколько преимуществ. Во-первых, при необходимости изменения
схемы базы данных мы все равно можем сохранить структуру АР!. Это значит, что
даже в случае изменения внутренней логики мы можем не изменять интерфейс,
таким образом потребители не будут затронуты нашим изменением схемы данных.
Уровень API создает некий буффер, который фиксирует в себе интерфейсы взаи­
модействия, изолируя их от уровня хранения данных. Это очень важное правило:
избегать взаимодействия микросервисов через общие базы данных и стараться
перевести их в плоскость АР!.
Сам
API при этом может быть любым, он может быть как синхронным (REST,
SOAP, gRPC), так и асинхронным (очереди, брокеры, веб-хуки). Создание таких
АР! еще сильнее развивает идею низкой связанности микросервисов, потому что
мы можем делать любые изменения внутри сервиса при условии, что внешний ин­
терфейс остается стабильным.
В случае если нам необходимо провести большие изменения, связанные с API мик­
росервиса, хорошей практикой является поддержка нескольких версий. Например,
мы можем выпустить в свет новый API, при этом не удалять предыдущий, но поме­
тить его как устаревший. И после того, как все наши потребители начнут пользо­
ваться новым API, мы можем прекратить поддержку старого интерфейса и удалить
его.
За счет того, что правильно спроектированные микросервисы имеют низкую свя­
занность, открываются новые подходы к масштабированию. Во-первых, по ана­
логии с
SOA мы можем выполнять горизонтальное масштабирование сервисов.
Однако т. к. микросервисы имеют собственные базы данных для моделирования
ограниченного контекста, то это положительно влияет на распределение нагрузки.
Нередко сами микросервисы могут использовать базы данных, которые также спо­
собны к горизонтальному масштабированию. Подобная гибкость позволяет изба­
виться от проблемы масштабирования, характерной для монолитных приложений,
когда для масштабирования какой-то части функционала нам требовалось больше
ресурсов для того, чтобы одновременно держать запущенной ту часть приложения,
которой не требуется масштабирование, -
потому что она его неразрывная часть.
Особенно хорошо независимое масштабирование сочетается с автоматизацией, в зависимости от целевых метрик мы можем автоматически выделять новые ресур­
сы для микросервиса и забирать их тогда, когда они не используются в достаточной
мере.
Микросервисы -
это мощный паттерн для увеличения эффективности разработки,
однако он имеет некоторую цену, о которой мы сейчас поговорим. Важно отметить,
Часть /. Проектирование и разработка системы
152
что микросервисы проблемы
и
не волшебный подход, который способен исправить сразу все
привести
организацию
к
цифровому совершенству
быстрее
всех
остальных подходов. Как и все в рамках архитектуры программного обеспечения,
микросервисы имеют свои компромиссы и трудности.
В первую очередь стоит поговорить про DеvОрs-часть. Большое количество неза­
висимых конвейеров разработки, тестирования и развертывания требуют наличия
грамотной оркестрации и управления ресурсами. Если каждый из этих сервисов
будет требовать
полностью ручного управления, это самое управление станет
узким местом и точно не ускорит разработку. Поэтому первое, что стоит учитывать
в начале разработки микросервисов, -
это грамотное управление, особенно авто­
матическое.
Сюда входят мониторинг, сбор логов и метрик, в идеале забывать про стандартизацию управления вать похожие механизмы
трассировка. Стоит не
если разные команды будут использо­
развертывания их сервисов, это
положительно скажется
на удобстве и скорости разработки. История знает много примеров, когда органи­
зации
начинали
внедрение
микросервисов,
срезая
при
этом
углы
автоматизации,
что становилось большой проблемой при увеличении количества микросервисов.
Все это требует некой цифровой зрелости, поэтому стоит закладывать ее развитие
при разработке. Благо чем больше сервисов получилось разработать и внедрить,
тем проще будет со временем, потому что много подводных камней к тому момен­
ту уже может быть найдено.
Вторая проблема микросервисов -
это согласованность данных, а именно распре­
деленные транзакции. Хотя мы и разделяем наше приложение на ограниченные
контексты, так или иначе возникают ситуации, при которых действие должно быть
выполнено сразу в нескольких из них. Одним из важных условий таких взаимодей­
ствий является согласованность данных. Согласованность транзакции может быть
достигнута в рамках одного сервиса (если база данных поддерживает АСID­
свойства), однако когда речь заходит о нескольких сервисах, то тут возникают не­
которые вопросы.
Если вы помните САР-теорему, то вы помните, что согласованность данных невоз­
можна в распределенной системе, кроме как если мы пожертвуем доступностью
ради этого. А поскольку микросервисы -
это как раз распределенная система, мы
не можем рассчитывать на абсолютную согласованность, максимум на согласован­
ность в конечном итоге (eventual consistency). Главным правилом мира микросер­
висов является взаимодействие через API. Это значит, что изменение данных в раз­
ных сервисах будет требовать нескольких вызовов. И что делать в ситуации, если
один из сервисов выполнил действие изменения данных, а второй вернул ошибку
либо вовсе оказался недоступен? В такой ситуации наши данные стали несогласо­
ванными: например, мы изменили количество товара на складе, но не передали его
в доставку. Если бы мы имели монолитную базу данных, такая ситуация легко раз­
решилась бы с помощью отката транзакции, но что делать в рамках микросер­
висов?
Не существует простого ответа на этот вопрос. Одним из вариантов действий явля­
ется так называемая компе11сацион11ая транзакция. В нашем примере со складом и
Глава 8. Сложная логика приложений
153
доставками в случае, если сервис доставки оказался недоступен, мы можем сделать
дополнительный вызов на сервис склада и восстановить предыдущее количество
товара. Казалось бы, пользуясь механизмом компенсационных транзакций, мы мо­
жем обеспечить атомарность нашей операции. Однако это не так, главная проблема
заключается в том, что компенсационная транзакция имеет такие же проблемы, как
и обычная транзакция, -
она может быть неуспешной. Если таких компенсацион­
ных транзакций много, то какие-то из них могут быть успешными, а какие-то нет, и
в таком случае ситуация становится еще любопытнее -
нужно ли нам придумы­
вать компенсационные транзакции на компенсационные транзакции? Все описан­
ное требует программной реализации, причем самая сложная часть кроется не
столько
в
правильном
краевых случаев
и
пути
прохождения
их компенсации,
транзакции,
потому что,
как
сколько
мы уже
в
предсказывании
помним,
граммная система не может разобраться с проблемой «по ходу» -
наша про­
если в нее не
заложено явное решение сложившейся ситуации, то, скорее всего, она завершится
ошибкой.
Если нам нужна строгая согласованность данных, стоит избегать распределенных
транзакций и стараться выполнять подобные бизнес-операции в рамках одного ог­
раниченного контекста. Если вы сталкиваетесь с острой необходимостью во мно­
жестве распределенных транзакций для обеспечения работы вашей бизнес-логики,
возможно, стоит еще раз посмотреть на моделирование ограниченных контекстов,
поскольку данные границы могут быть расставлены не самым оптимальным обра­
зом, и в случае повторного моделирования ситуация может улучшиться. Адекват­
ной практикой является интегрирование в этот вопрос итеративного подхода. Если
мы столкнулись с распределенной транзакцией, ее реализация в согласованном
стиле может требовать изменения ограниченных контекстов, которые мы выдели­
ли. Изменение в этом случае может положительно повлиять на общую читаемость
архитектуры, увеличивая ее стабильность.
Взаимодействие между микросервисами происходит по сети, что также создает не­
которые трудности. Взаимодействие по сети происходит гораздо медленнее, чем,
например, обращение к оперативной памяти, поэтому те операции, которые могли
произойти путем вызова библиотеки, будут происходить медленнее, поскольку те­
перь это вызов
API. Сейчас благодаря развитию интернет-сетей может создаться
впечатление, что сама сеть имеет бесконечную пропускную способность, однако
это не так. Помимо возросшей задержки вызовов мы можем столкнуться с ограни­
чением пропускной способности сети, поскольку эта сеть делится множеством
служб и сервисов. Это не будет проблемой для одиночных и нечастых вызовов,
однако если количество вызовов становится большим, то главным узким местом
нашей системы может стать именно сеть.
Другой проблемой сети является безопасность. Часто микросервисы развертывают­
ся в закрытом периметре, например в виртуальной сети, доступ в которую невоз­
можен извне. Однако часто сами вызовы между сервисами не имеют шифрования,
поэтому в случае, если они идут по публичной сети, крайне важно учитывать это и
добавлять шифрование, желательно общепринятым образом, например используя
TLS. Второй момент связан с самими вызовами: поскольку сервисы могут содер-
Часть /. Проектирование и разработка системы
154
жать чувствительную и приватную информацию, важно обеспечить аутентифика­
цию и авторизацию. Чаще всего доступ к определенным микросервисам происхо­
дит с помощью АРI-ключей либо временных токенов. Крайне не рекомендуется
оставлять доступ к микросервису открытым даже в приватной сети, поскольку
если кто-то получит доступ в эту сеть, то перед ним откроется доступ сразу ко всем
сервисам, которые есть в рамках сети, с возможностью чтения и модификации ин­
формации.
Многие из описанных трудностей покрываются использованием правильных плат­
форм. Например, платформы контейнерной оркестрации позволяют управлять раз­
вертываниями сервисов, АРI-ключами и жизненными циклами. Также такие плат­
формы с помощью расширений могут обеспечивать безопасное соединение между
сервисами, заниматься журналированием событий.
Если смотреть на микросервисы с точки зрения связанности и связности, то, как
правило, значение связанности будет ниже, чем у монолитной архитектуры, а связ­
ности -
выше. Обусловлено это разделением на сервисы, а также подходами в ор­
ганизации взаимодействий между сервисами. Однако достичь этого можно исклю­
чительно при правильном проектировании -
сам факт выбора такого подхода не
гарантирует низкую связанность и высокую связность. При неправильном модели­
ровании можно нарваться на патгерн, который называется «распределенный моно­
лит».
Такое
нередко
происходит
при декомпозиции
монолитной
архитектуры,
когда выбирается стратегия выбора границ сервисов не на основании ограниченных
контекстов, а на основании дизайна модулей кода в рамках приложения. Поскольку
такие связи могут не отражать ограниченный контекст, сами границы между мик­
росервисами могут размываться настолько, что формально сами сервисы не могут
разворачиваться
независимо,
потому
что
они
имеют
слишком
высокую
связан­
ность. Любопытно то, что при таком подходе теряются все плюсы монолитной
архитектуры и приобретаются новые минусы, связанные с архитектурой микросер­
висов.
Резюмируя: микросервисная архитектура -
это мощный инструмент, который ак­
тивно использует современные достижения и технологии, позволяющие обеспечить
автоматизацию, управление и мониторинг на новом уровне. Стремление к высокой
связности внутри сервиса и низкой связанности позволяет сделать саму систему
более прозрачной, что ведет к увеличению скорости разработки, что положительно
влияет на цифровизацию бизнеса. Но нельзя забывать, что микросервисы вносят
определенный объем усложнений, при этом попытка срезать углы в этой области
неизбежно приведет к путанице, ошибкам и большому количеству ручного труда.
В микросервисах ключевую роль играет автоматизация
-
чем более умело она
применяется, тем стабильнее и понятнее становится система.
Мне очень нравится одна мысль: «любая программная система имеет определен­
ный уровень сложности, который невозможно понизить с помощью архитектурных
или программных решений, однако мы очень легко можем увеличить эту слож­
ность, если будут применяться неправильные решения». Это означает, что если вы
не видите простого пути решения бизнес-задачи, то это может быть связано именно
со сложностью самой задачи, и проще ее сделать нельзя. В рамках микросервисов
Глава 8. Сложная логика приложений
155
эта фраза получает свое продолжение. Микросервисы, как правило, увеличивают
техническую сложность проекта, добавляя новые технологии и логику. Однако в
рамках разработки все живет в балансе, и добавляя сложность в этом месте, мы по­
зволяем упростить другие аспекты разработки. Благодаря этому плоскость, услож­
ненная подходами микросервисной архитектуры, может управляться сама с помо­
щью автоматизации, не требуя вмешательства человека. В свою очередь, другие
части проекта, такие как архитектура, технические и бизнес-решения, невозможно
автоматизировать, но теперь у людей есть больше времени и энергии на то, чтобы
творить именно в этой области, в которой программные технологии сами по себе
бессильны.
Почему микросервись1 не были популярны раньше
Наверное, у читателей мог возникнуть вопрос: почему при всех своих плюсах мик­
росервисная архитектура стала развиваться относительно недавно. Это крайне лю­
бопытный вопрос «со звездочкой», и мы попробуем дать на него ответ.
Во-первых, во времена популярности монолитных архитектур выбор технологий
у разработчиков был невелик. В сравнении с сегодняшними днями наблюдалась
большая нехватка технологий с открытым исходным кодом, которые были бы дос­
таточно надежны для использования в продакшен-среде. Это касалось баз данных,
операционных систем, систем тестирования, развертывания, сборки, мониторинга.
Однако существовало большое количество проприетарных решений, которые были
вполне пригодны, но они стоили денег. Часто такие решения поставлялись в фор­
мате подписок на год и стоили дорого. Это касалось многих проприетарных баз
данных, поставлявшихся с лицензией, которая позволяла разворачивать базу на
ограниченное количество серверов либо операционных систем с похожими ограни­
чениями.
Подобные особенности требовали от разработчиков адекватных решений, чтобы
программное обеспечение не только выполняло поставленную бизнес-задачу, но и
делало это максимально эффективно. Таким образом, сформировался тренд на пе­
реиспользование. Например, если у нас есть лицензия базы данных и необходимо
смоделировать какой-то новый ограниченный контекст, мы могли бы разработать
данный функционал в рамках существующего приложения и не иметь проблем
с дополнительной стоимостью владения.
Другая особенность была связана с вычислительными ресурсами. Раньше ввиду
слабого развития облачных вычислений и большой стоимости аренды серверов
многие компании закупали собственные вычислительные кластеры. Такая практика
сохраняется и по сей день, однако она стала выглядеть немного иначе. Микросер­
висная архитектура требует гибкого вьщеления ресурсов и определенного уровня
цифровой зрелости в их оркестрации. В те времена выделение ресурсов, как прави­
ло, происходило вручную с помощью виртуальных машин, а процесс добавления
новых мощностей буквально требовал закупки новых серверов, что может занимать
неопределенное время. Поэтому большой уровень дробления мог быть невыгоден
ввиду того, что это добавляет сложность в управлении, которую тогда бьmо труд-
Часть /. Проектирование и разработка системы
156
нее автоматизировать. Все это усугублялось тем, что проблема лицензий никуда не
исчезла. На каждую виртуальную машину нередко ставилась проприетарная ОС,
и само ПО для управления виртуальными машинами тоже было платным. Добавле­
ние сложности в этой области без явной на то причины не могло окупить себя.
В современном мире ситуация сильно поменялась. Во-первых, появилось большое
количество решений с открытым исходным кодом, которые прямо сейчас исполь­
зуются на больших и серьезных проектах. Часто подобные решения -
это некое
продолжение внутренних инструментов, которые разрабатывались компаниями для
своих собственных нужд, но в какой-то момент они стали публичными для того,
чтобы поддержать сообщество и сделать разработку ПО более стабильной и удоб­
ной. Да, проприетарные решения остались, однако они поменяли свою модель по­
ведения, о чем мы поговорим чуть позже. Большое распространение решений с от­
крытым исходным кодом позволяет экономить на лицензиях и выпускать больше
различных сервисов без проблем, связанных с тратами на эти лицензии, что благо­
творно повлияло на развитие микросервисов. Помимо отсутствия лицензий, данные
открытые проекты имеют богатый функционал, позволяющий разработчикам мик­
росервисов быстрее достигать цели разработки и делать при этом сами сервисы
более стабильными и прозрачными.
Вторая важная деталь -
развитие облачных вычислений. В последнее время ак­
тивно развиваются гиперскейлеры, такие как А WS, Azure, Google Cloud, Yandex
Cloud, которые позволяют организациям не задумываться о построении своих соб­
ственных ЦОД, а получить доступ к таким же мощностям практически по клику
мышкой. Построение своего вычислительного кластера -
сложная задача, а его
масштабирование еще сложнее, и гиперскейлеры берут этот вопрос на себя. Если
мы собираемся запустить новый сервис либо увеличить количество вычислитель­
ных мощностей для существующего, то, пользуясь облачными вычислениями, мо­
жем не задумываться о физической части и просто получить эти ресурсы в наше
пользование. При этом чаще всего модель оплаты подразумевает оплату по часам,
что позволит нам проводить различные эксперименты с конфигурациями без необ­
ходимости больших вложений. Гиперскейлер имеет большое количество вариантов
предоставления вычислительных ресурсов: с почасовой оплатой, с предоплатой за
1-3 года, прерываемые серверы, различные гибкие планы. Благодаря этому можно
неплохо сэкономить на оплате инфрасруктуры, подобрав тот план, что подходит
под вашу задачу. Например, если мы знаем, что наше приложение будет запущено
в ближайшие несколько лет, хорошим решением является покупка по предоплате,
поскольку в такой ситуации гиперскейлер может дать скидку до 40 %. И наоборот,
если наша необходимость в вычислительных ресурсах непостоянна, мы получим
максимальную выгоду за счет почасовой оплаты либо пользуясь прерываемыми
серверами, поскольку мы не будем платить за простаивающие мощности.
Другое преимущество гиперскейлеров напрямую связано с проприетарным про­
граммным обеспечением. Вместе с возможностью выделения вычислительных ре­
сурсов облачные провайдеры обладают глубокой инфраструктурой внутренних
сервисов, которые решают те или иные вопросы в рамках разработки. Например,
провайдер может предоставлять сервисы для хранения данных, сервисы управле-
Глава 8. Сложная логика приложений
157
ния базами данных, сервисы аналитики и обработки, лоrирования, трассировки,
безопасности и аудита. При этом данные сервисы имеют большое количество инте­
граций друг с другом, за счет чего достигается их эффективная синергия в рамках
вашего проекта. Иными словами, теперь то, что требовало покупки лицензии, пре­
доставляется в рамках облачной инфраструктуры как удобные сервисы.
Вместо того чтобы разрабатывать собственные решения с нуля, например для более
удобного управления задачами аналитики данных либо управления базами данных,
мы можем использовать готовые решения, которые предоставляет облачный про­
вайдер. При этом механика оплаты может быть совершенно разной, но, как прави­
ло, современные rиперскейлеры делают упор на гибкие планы оплаты, например за
объем обработанных данных, за количество прошедшего трафика, за количество
вызовов, за секунды или часы работы сервиса. Подобные тарифные планы особен­
но подходят для экспериментов и несимметричных нагрузок.
С учетом подобных подходов и преимуществ, которые появились в последнее вре­
мя, можно дать ответ, почему микросервисы не получили своего развития раньше.
В те давние времена ввиду стремления к переиспользованию, выраженного как на
программном, так и на аппаратном уровне, разработчики создавали архитектуры,
которые подходили в сложившийся IТ-ландшафт. В то время предложения по уве­
личению
количества сервисов,
данных,
по
независимым
наличию у
конвейерам
каждого
сервиса собственной
развертывания
определенно
базы
разбивались
о реальность, в которой на это требовались бы отдельные лицензии и большое ко­
личество ручной работы, автоматизировать которую не представлялось возможным
ввиду слабого распространения автоматизации процессов разработки.
В свою очередь, сегодня, когда подобные проблемы выражены не так сильно и су­
ществует множество удобных способов делегировать определенные части проекта
на эффективные технологии, подход микросервисов позволяет обеспечить разра­
ботку в масштабе, делая системы более надежными.
Подобное сравнение наводит на одну философскую мысль. Нередко, когда разра­
ботчики попадают на существующий проект, они могут начинать осуждать какие­
либо архитектурные решения, которые были приняты несколько лет назад. Важным
моментом в рамках принятия решений является контекст, который со временем
может теряться. Как в примере с монолитными архитектурами -
в современных
реалиях их реализация может иметь явные минусы, однако в то время, когда проект
только начинался, ввиду особенностей, которые были названы выше, требовалось
соответствовать именно им, что и привело к такому архитектурному решению. По­
этому, видя какие-то неэффективные моменты, важно понимать, были ли они не­
эффективны с самого начала, или в те времена, когда они создавались, это было
с чем-то связано. Глубокий анализ подобных решений позволит глубже понимать
специфику проекта и делать более выверенные архитектурные решения в будущем.
К тому же изучение вопроса, почему было сделано именно так, нередко может раз­
вить
ваш
кругозор,
наводя
взгляд
на
технологии
встречаться просто так в каждом проекте.
и
подходы,
которые
могут
не
158
Часть /. Проектирование и разработка системы
Важность документации
Сложная логика приложений становится всё более распространенной в современ­
ном мире, поскольку все больше бизнесов хотят цифровизировать свои процессы
либо предоставить пользователям новый опыт, который позволит отличаться от
конкурентов. Сложность требует определенного совершенства в рамках управле­
ния, поскольку, если пустить все на самотек, даже относительно простые проекты
могут стать непонятными и неподдерживаемыми. Ставшая классической фраза «за­
чем нам документация, тут же и так все понятно» нередко встречается на проектах
в разных областях, поэтому в рамках этого раздела мы пройдемся по основным це­
лям документации и поймем, как она помогает управлять сложной логикой.
Важная вещь, которая встречается почти у всех проектов, -
это то, что они дела­
ются не в одиночку. Даже если вы работаете на фрилансе один, у вас существует
заказчик, которому могут быть интересны детали реализации, принципы работы,
схемы и ограничения. В случае же большой команды то же самое может быть еще
более интересным для ваших коллег, поскольку от понимания этих концепций на­
прямую изменяется скорость разработки. Документация позволяет сгладить этот
момент.
Во-первых, документация помогает всем людям однозначно понимать те или иные
архитектурные и бизнес-решения, которые встречаются в проекте. Это полезно как
людям, которые давно работают на проекте, поскольку многое может со временем
забываться, так и новым разработчикам, которые только присоединяются к проек­
ту. Вместо того чтобы расспрашивать опытных разработчиков об очевидных для
них вещах (потому что они работают с этим достаточно давно), гораздо эффектив­
нее получить эти знания через изучение внутренней документации и впоследствии
задать те вопросы, которые остались.
Особенно стоит остановиться на ситуациях, которые полезны во время длительной
разработки. Во время работы над проектом принимается огромное количество ре­
шений, проводится большое количество экспериментов, и это все напрямую влияет
на то, как проект будет выглядеть в будущем. Нередко какие-то из данных контек­
стов забываются, и именно в такие моменты может пригодиться документация.
В рамках данной ситуации ее можно использовать как журнал, в котором в удобной
форме содержатся все решения и контексты. Это особенно полезно в спорных си­
туациях, при которых не совсем понятно, почему обстоятельства сложились имен­
но так, и в этом случае документация может предоставить полезные референсы для
разрешения вопроса.
Подобный подход может быть очень полезен в рамках общения с заказчиком. Не­
редко при разработке могут быть некие договоренности о том, как именно нужно
реализовывать тот или иной функционал. Однако со временем видение заказчика
может поменяться, и может возникнуть ситуация, при которой заказчик скажет, что
такое видение было с самого начала, а команда разработки сделала все неправиль­
но. Документация может помочь и в этой ситуации -
четко записанные решения и
договоренности позволяют иметь прозрачное видение у обеих сторон. Документа­
цию можно рассматривать как некий буфер между командой разработки и заказчи-
Глава 8. Сложная логика приложений
159
ком, через который можно явно передавать факты и решения. А грамотное ведение
этого буфера позволяет минимизировать недопонимание и увеличить уверенность
в том, что команда делает действительно то, что нужно заказчику.
Второй момент -
это обмен опытом. С помощью документации можно отражать
лучшие практики, которые используются на проекте. Если явно выразить, напри­
мер, принятый стиль названия объектов либо стиль коммуникации между сервиса­
ми, а также выработать лучшие практики использования этого, разработчики будут
иметь явный ориентир в рамках своей работы, что увеличит общее качество кода на
проекте. Документация позволяет добиться согласованности действий между раз­
работчиками и дает возможность, не теряя личное видение каждого, направить всех
в русло некого единого подхода, сохраняя единообразие продукта на выходе.
Однако важно, чтобы разработчики не забывали об этом, поскольку изложенные
практики не всегда являются обязательным поводом к исполнению. Тут необходи­
мо сохранять баланс в обеспечении гибкости во время разработки и в определен­
ных политиках, которые нужно при этом соблюдать.
На проектах, в которых присутствуют определенные регуляторные ограничения,
документация становится особенно важной. Нередко, если проект имеет медицин­
скую направленность либо в нем фигурируют персональные данные пользователей,
могут применяться
определенные
правила и политики,
которые
важны
в
рамках
разработки. Когда таких факторов становится много, они могут забываться, однако
соблюдать необходимо их все. Грамотное документирование и, что может быть
даже более важным, регулярное обращение к этим правилам и подходам является
залогом успешной реализации сервиса с соблюдением специфических правил.
Что касается технической точки зрения, существует большое количество сервисов
для ведения документации. Чаще всего для этого подходят различные Wiki-сис­
темы либо сервисы, которые поставляются вместе с другим ПО для разработки,
например вместе с таск-трекерами. Главное, что должен поддерживать данный, сер­
вис, -
это возможность коллективной работы над документацией, возможность
добавления изображений и схем, а также поддержку rich text (markdown-фopмaтa).
Нередко, если платформа для ведения документации поставляется в рамках про­
граммного пакета, она может иметь удобные интеграции с другими сервисами. При
изменении на страницах могут отправляться нотификации на почту либо в мес­
сенджеры. Также в рамках такой системы можно ссылаться на определенные части
кодовой базы либо на определенные задачи, строить хронологию проекта. Данные
инструменты сильно увеличивают прозрачность и понятность проекта как для за­
казчика, так и для команды разработки.
Заключение
Сложная логика приложений, на мой взгляд, это не всегда плохо. Неочевидные
технические задачи нередко интересны,
-
решая их, мы приобретаем ценный
опыт. Однако они требуют от нас правильных подходов, соблюдение которых яв­
ляется ключом к успешной реализации проекта.
160
Часть /. Проектирование и разработка системы
Сложная логика может скрываться в разных местах, от бизнес-логики до дизайна
сервисов, поэтому активно применяются разные подходы для ее разрешения. Не
существует простого ответа, как правильно управлять сложной логикой, однако
есть целый ряд подходов, который был описан в рамках этой главы, позволяющий
взглянуть на разработку под другим углом, декомпозировать задачу и решить ее
с большей эффективностью, а не решать ее прямо в лоб.
Одним из советов я могу назвать не пытаться вписать ваш проект в какие-то кон­
кретные рамки, которые очерчены паттернами. Мир разработки слишком ком­
плексный. Если бы все можно было решить с помощью одного или двух подходов,
не существовало бы такой дисциплины, как архитектура программного обеспече­
ния, в которой фактически каждый раз нужно заново проводить анализ компромис­
сов для каждого конкретного решения. Ваш проект уникален, и лучшее, что можно
сделать, -
это собрать лучшие подходы из разных практик и паттернов и попробо­
вать итеративно реализовать их. Важным тут является именно итерация -
невоз­
можно угадать все с первого раза, поэтому важно строить постепенно, добавляя
новые технологии, при этом учась и не забывая о главной цели задачи.
решении бизнес­
ГЛАВА 9
1181
1181
88!!
Безопасность
Информация, которая относится к моделируемым контекстам, нередко бывает при­
ватной. Так или иначе, когда мы строим бэкенд-систему, мы оперируем некими
данными пользователей, например их именами, паролями, адресами электронной
почты. Ситуация становится еще более серьезной, если мы, например, обрабатыва­
ем медицинские данные либо финансовую информацию. При этом сама по себе эта
информация может быть относительно безвредной, гораздо опаснее, когда ввиду
неавторизованного доступа может произойти проникновение в бизнес-процесс, при
котором будет нанесен как физический, так и репутационный ущерб. Поэтому
управление информацией с точки зрения безопасности -
это отдельная сложная
тема, которой мы теперь коснемся.
На плечи разработчиков нередко ложится организация безопасности приложения.
Да, во множестве организаций за это может отвечать отдельная группа специали­
стов, которые занимаются интернет-безопасностью, однако о реализации таких ба­
зовых вещей, как аутентификация в вашем приложении, авторизация, организация
ролей, правильное управление секретами приложения, должен помнить каждый
разработчик на проекте.
В рамках данной главы мы разберем три основные темы: безопасное хранение дан­
ных и секретов (secrets) приложения, организация аутентификации и авторизации
пользователей, основные векторы атак. Данный обзор ни в коем случае не является
исчерпывающим, но большинство концептов, которые будут упомянуты, являются
эффективными в рамках организации безопасности бэкенд-приложений.
Безопасное хранение данных
и управление секретами
Управление секретами
В первую очередь стоит сказать, что мы понимаем под секретами (secrets). Наше
приложение так или иначе будет получать доступ к ресурсам, содержащим чувст­
вительную информацию, например к базам данных, другим сервисам. Для этого
Часть /. Проектирование и разработка системы
162
доступа мы должны иметь данные для аутентификации (credentials), которые могут
представлять собой пары логин-пароль, ключи доступа. Поскольку эти данные мо­
гут быть использованы кем угодно, если они будут общедоступны, необходимо
обеспечить безопасный доступ приложения к ним, а также обеспечить безопасную
ротацию в случае изменения секретов.
Самое первое правило, которое необходимо соблюдать,
-
не хранить секреты
в открытом виде. Грубейшая ошибка, которую нередко совершают по незнанию
или
невнимательности,
-
это хранение
секретов
в
репозитории
исходного
кода.
Это особенно критично, если вы работаете н'ад каким-то публичным проектом, на­
пример над популярной библиотекой, и случайно оставили чувствительные данные
в репозитории.
Благо существуют подходы, которые позволяют минимизировать подобные случаи.
Самым частым способом передачи секретов в код являются переменные окруже­
ния. По сути переменные окружения -
это словарь, который содержит в себе оп­
ределенные системные значения (например, имя пользователя, путь к домашней
директории), но также может содержать и пользовательские значения, например
ключи, логины и пароли, токены. При этом сами переменные хранятся в оператив­
ной памяти и могут быть доступны только в рамках системы либо процесса. Для
того чтобы получить доступ к переменным окружения из кода, используется назва­
ние переменной окружения, с помощью которого получается значение. Референсы
при этом оставлять в коде вполне можно, поскольку сами по себе они бесполез­
ны -
необходимы значения.
Переменные окружения могут создаваться разными способами. Во-первых, во вре­
мя локальной разработки можно создавать еnv-файл ( файл с переменными окруже­
ния) и настроить систему контроля версий так, чтобы он не записывался в историю
репозитория. В рамках Git, как вы уже это знаете, это можно сделать с помощью
gitignore-фaйлa. Еще правилом хорошего тона является создание файла example.env,
в котором можно указать пример заполнения еnv-файла, естественно, избегая ре­
альных значений. Далее, используя специальные библиотеки либо встроенный
функционал языка программирования, можно прочитать данный файл для того,
чтобы заполнить переменные окружения процесса, после чего они будут получены
с помощью кода по именам переменных окружения.
Многие облачные платформы управления исходным кодом имеют расширения, ко­
торые автоматически сканируют репозиторий на предмет случайно добавленных
ключей. Поскольку многие ключи имеют определенную типичность (как правило,
это неразборчивый набор символов), появляется возможность находить их автома­
тически и, например, отказывать в добавлении коммитов на удаленный репозито­
рий в случае, если новые файлы содержат чувствительные данные.
Таким образом можно передавать АРI-ключи, данные для доступа к БД и прочую
чувствительную информацию. Это удобно в рамках локальной разработки, где мы,
например, можем менять данные для доступа к базе данных без необходимости
изменять сам код. Например, мы можем развернуть локальную базу данных и тес­
тировать наш код на ней, а потом развернуть наше приложение в облаке, где оно
Глава 9. Безопасность
163
будет получать уже новые переменные окружения, которые соответствуют облач­
ной базе данных.
Данная идея получила свое продолжение в виде специальных хранилищ секретов.
Поскольку еnv-файл не обязательно может содержать чувствительную информа­
цию, например в него также могут записываться различные конфигурации, имена,
варианты запуска, то для более безопасного подхода необходимо иметь отдельные
жизненные циклы для секретов приложения.
Хранилища секретов -
это, как правило, отдельный сервис, который имеет инте­
грации со службами контроля доступа (IAM) и хранит в себе чувствительную ин­
формацию, которую можно получить, не только зная имя переменной, но и если
у приложения имеется достаточно прав на ее получение. Разберем этот пример
в рамках облака, поскольку для локальной разработки может быть достаточно и
еnv-файла, и локальной Бд, поскольку редко разработчик может иметь доступ к
облачной базе данных прямо со своей рабочей машины. Хранилища секретов, как
правило, используются именно в облаке, хотя мы приведем некоторые исключения.
Когда сервис запускается в облаке либо на другой оркестрационной платформе (та­
кой как Kubemetes), его запуск ассоциирован с определенной конфигурацией, в ко­
торой перечисляются основные настройки, например какой образ нужно использо­
вать, какие основные переменные окружения нужны приложению либо сколько
ресурсов необходимо для выполнения. И когда дело касается секретов, то тут под­
ключается специальное хранилище. Платформа развертывания приложения может
получить доступ к хранилищу секретов, используя специфические для платформы
механизмы, например роли запуска сервиса. Используя эту роль, оркестрационная
платформа получает эти значения и добавляет их в переменные окружения прило­
жения. Таким образом приложение может безопасно получить доступ к паролям и
ключам, используя все тот же механизм переменных окружения.
Ключевым пунктом в управлении хранилища секретами является организация пра­
вильного доступа. Например, роли доступа к нему не должны давать больше при­
вилегий, чем нужно. Этот подход называется принципом минимальных привW1егий
(principle of least privilege) и означает, что приложение либо аккаунт должны иметь
доступ только к тем сервисам и ресурсам, которые будут использоваться в рамках
работы. Например, если мы должны получить доступ к нашей базе данных, наша
роль доступа не должна иметь доступ к соседним секретам, например к аналитиче­
скому хранилищу. Основная причина, почему этот принцип должен соблюдаться,
связана с несанкционированным доступом либо ввиду ошибок в приложении. Если
секреты окажутся в чужих руках, это значительно уменьшит «радиус поражения»,
поскольку эти данные будут подходить только к одному специфическому ресурсу.
Что касается ошибок в приложении (хотя такое происходит сравнительно редко),
если приложение имеет доступ к реальным базам данных, разработчики могут по
ошибке добавить в них сценарии, которые, например, удаляют всю базу данных
программным путем либо неправильно модифицируют данные, делая их непригод­
ными. Поэтому не должно быть одного универсального ключа, который открывает
все двери, потому что приложение ввиду ошибок конфигурации может зайти не
в ту, нарушив работу платформы.
Часть /. Проектирование и разработка системы
164
Использование хранилища секретов позволяет проводить ротацию ключей для всех
приложений одновременно, а также иметь возможность отзывать и пересоздавать
секреты в случае, если произошла утечка. Доступ в хранилище секретов может
быть не только у сервисов, но и у пользователей. Например, если при локальной
разработке нам нужно дать ключ доступа к определенной платформе, разработчики
могут использовать свои облачные аккаунты для доступа в хранилище секретов.
Используя такой подход, мы также можем выполнять ротацию секретов без необ­
ходимости их ручной замены, а также отзывать доступ к секретам по необходимо­
сти. Также вариант удобен для аудита, поскольку информация о каждом запросе
того или иного секрета лоrируется, что может быть полезно в случае, если мы
хотим сразу предотвратить несанкционированный доступ.
Если резюмировать основные правила управления секретами, стоит избегать их
прямой передачи и хранения. В рамках хранилища секретов сами данные нередко
зашифрованы и предоставляются сугубо при наличии определенных доступов. Не
стоит забывать удалять старые и неиспользуемые секреты, которые тем не менее
могут подходить к работающим базам данных, -
в этом вам также может помочь
хранилище секретов. Для удобной работы с секретами со стороны приложения
можно использовать переменные окружения, что позволит обеспечить работу как
при локальной разработке, так и при развертывании в облаке, сохранив логику по­
лучения секретов одинаковой.
Хранение и передача данных
Данные, которые хранятся на диске, могут быть мишенью для всяческих атак. Дан­
ные, которые лежат в открытом виде, могут быть прочитаны кем угодно, поэтому
в рамках разных политик хранения персональных данных (HIPAA, GDPR) такой
подход не допускается. Но каким образом можно обеспечить безопасный доступ
к самим данным?
Специфическая реализация зависит от платформы, в рамках которой происходит
работа, но концепции будут общими почти для всех. Первым уровнем защиты яв­
ляется организация правильного доступа, что было частично покрыто в разделе про
управление секретами. Современные гиперскейлеры имеют собственные сервисы
управления доступом, которые позволяют организовать так называемый контроль
доступа на основе ролей (RBAC, role based access control). Подобные механизмы
распространяются как
на объектные хранилища, так и на сами базы данных.
С помощью ролей можно не только разрешать либо запрещать доступ, но и специ­
фицировать степень, например разрешить только чтение либо разрешить только
запись без возможности чтения других объектов. Например, в А WS SЗ все объекты
по умолчанию недоступны для чтения и записи, если запрашивающий не имеет по­
литики доступа к данным. Тем не менее подобные хранилища дают возможность
настроить публичный доступ к объектам, что может быть полезно при управлении
статическим контентом, например картинками для публичных веб-сайтов.
Второй уровень защиты представляет собой шифрование данных. Многие облач­
ные сервисы предоставляют услугу шифрования в месте хранения (at rest encryp-
tion). Это означает, что данные не хранятся на жестком диске в открытом виде, а
Глава 9. Безопасность
165
перед этим принудительно зашифровываются. Выглядит это примерно так: сущест­
вует ключ шифрования, который хранится в специлизированном хранилище секре­
тов. При каждой записи файла этот ключ используется для шифровки файлов;
в свою очередь, при запросе на чтение этот же ключ используется для дешифровки
и передачи пользователям. За счет этого, даже если сам ЦОД будет физически
взломан, файлы в нем будут зашифрованы. Сам же ключ, как правило, хранится
в более безопасных местах, и к нему применяются более серьезные меры защиты.
Плюс сами сотрудники ЦОДа не смогут просмотреть файлы без этого ключа, что
может быть ключевым при работе с медицинскими данными. Нередко облачный
провайдер позволяет создавать и использовать свои собственные ключи вместо
ключей, управляемых облачным провайдером.
Если информация слишком чувствительна, существует и другой, более прямоли­
нейный способ. Мы можем выполнять шифрование самостоятельно, передавая
в облако только зашифрованные данные. Используя данный подход, мы можем
в принципе не хранить ключ в облаке, а, например, иметь его внутри периметра
своего приложения. Такой подход хотя и имеет определенные накладные расходы,
но может гарантировать, что облачный провайдер не получит доступ к информа­
ции, которую мы сохраняем.
Теперь поговорим про передачу данных. Нередко информация передается через
публичный Интернет. Это означает, что любой из узлов сети во время передачи
может прослушивать трафик и получать доступ к информации, которая передается.
Это крайне опасно, если мы работаем, например, с логинами и паролями, потому
что их перехват означает возможность доступа к нашей системе.
Благо современный Интернет все больше и больше поддерживает стандарты шиф­
рования трафика, такие как TLS. Современные браузеры активно помечают сайты,
которые не поддерживают HTTPS, как небезопасные, уведомляя пользователей, что
вся информация, которая относится к этому сайту, передается в незашифрованном
виде, что потенциально может быть небезопасно, если она содержит персональную
информацию.
Используя TLS и сертификаты, возможна организация защищенных каналов связи
между клиентом и сервером. Это относится как к обычному использованию Интер­
нета через браузер, так и к различным вызовам к АР!. Например, все вызовы к АР!
гиперскейлера благодаря тому, что сами серверы имеют настроенные сертификаты
и TLS, по умолчанию зашифрованы, что обеспечивает безопасность передачи на­
ших данных.
Поэтому, если вы строите собственные сервисы либо активно используете внеш­
ние, крайне важно убедиться, что все сетевые взаимодействия, проходящие через
публичный Интернет, зашифрованы. В противном случае клиенты данных сервисов
являются потенциальными жертвами прослушивания трафика, ведущего к утечке
персональных данных, таких как токены, логины и пароли либо любая другая лич­
ная информация.
Есть и другие варианты обеспечения безопасной связи между сервисами. Напри­
мер, если мы говорим про облачное развертывание, провайдеры нередко предос-
Часть /. Проектирование и разработка системы
166
тавляют услугу виртуальных сетей. Вместо того чтобы организовывать общение
сервисов через публичный Интернет, мы можем организовать закрытые, приватные
сети, которые будут недоступны извне, но будут доступны для внутренних серви­
сов, которые будут использовать ее для сетевых взаимодействий. Если сеть дей­
ствительно недоступна извне, сами коммуникации могут быть не зашифрованы.
Однако в периметрах, где требуется повышенная безопасность, все равно имеет
смысл добавить дополнительный уровень защиты, обеспечив шифрование трафика.
В таком случае внутри периметра создается свой собственный центр выпуска сер­
тификатов безопасности, которые рассылаются сервисам для того, чтобы организо­
вать защищенную связь.
Также не стоит забывать, что существует еще один подход. Если наши сервисы
находятся в разных сетях, но нам необходимо организовать их коммуникацию, мы
можем воспользоваться VPN. С помощью VPN-cepвepa мы можем объединить на­
ши удаленные сети в общую виртуальную подсеть. При этом вся коммуникация
будет проходить через публичный Интернет, однако VPN-cepвep будет обеспечи­
вать шифрование трафика, и даже если сами взаимодействия между сервисами
происходят без HTTPS, VPN-cepвep все равно будет шифровать проходящий тра­
фик, обеспечивая нужную защиту.
Обзор подходов аутентификации
Описанные подходы для безопасного хранения и передачи информации являются
основой, которая позволяет обеспечить безопасное взаимодействие. Однако поверх
этого существует большое количество сценариев, которые требуют аутентифика­
ции пользователя.
Существуют два понятия, которые звучат похоже, но имеют разные смыслы. Аутен­
тификация -
это процесс, при котором происходит определение, что пользователь
системы действительно является тем, за кого он себя выдает. То есть аутентифи­
кация в первую очередь направлена на подтверждение личности (причем как поль­
зователя, так, например, и сервиса). Авторизация же направлена на то, чтобы опре­
делить, имеет ли пользователь (либо сервис) доступ к ресурсам, которые он за­
прашивает. То есть пользователь может быть аутентифицирован успешно, но не
пройти авторизацию, потому что у него может быть недостаточно прав на доступ
к определенным ресурсам.
Как правило, авторизация выполняется на уровне логики сервиса, в свою очередь
аутентификация всегда предшествует ей, поскольку мы не можем авторизовать то
или иное действие, если не уверены, кто на самом деле его выполняет. Поэтому
сейчас мы поговорим про основные подходы, которые позволяют аутентифициро­
вать пользователей.
АРl-ключи
На мой взгляд, самым логичным и простым способом организации аутентификации
является АРI-ключ. Он применяется во множестве ситуаций и во множестве пуб-
Глава 9. Безопасность
167
личных сервисов как удобный способ идентификации, позволяющий идентифици­
ровать себя и получить доступ к приватной информации.
Для его организации мы должны предоставить пользователям ключ, который фор­
мально представляет собой строку длиной более
30 символов, чтобы исключить
возможность перебора ключа, а также случайных коллизий. На стороне нашего
бэкенд-приложения мы должны сохранить этот ключ в базе данных, а также ассо­
циировать его с некими метаданными (например, сроком действия ключа, 1D поль­
зователя, который создал ключ, и т. д.). При каждом запросе пользователя к нашей
системе наше приложение будет проверять ключ на действительность. Чаще всего
ключ помещается в заголовки запросов либо передается как один из параметров.
В случае если ключ действителен, наше приложение может разрешить запраши­
ваемое пользователем действие.
У такого подхода есть определенные сценарии, для которых он отлично применим.
Например, мы строим сервис, у которого есть API, который предполагается к про­
граммному использованию. В рамках веб-интерфейса вы можете дать возможность
пользователям создавать АРI-ключи, специфицировать те части приложения, кото­
рые становятся доступными с помощью этого ключа. После этого уже другие раз­
работчики смогут добавить этот ключ в свое приложение и таким образом полу­
чить доступ к вашему сервису. Такой подход активно используется, например, для
сервисов карт, сервисов поиска, доступа к LLM и т. д.
Однако у такого подхода есть и минус. Если нам необходимо аутентифицировать
действия обычного пользователя, АРI-ключи годятся не полностью. Во-первых,
сами по себе ключи лишены полезных метаданных, которые могли бы быть ис­
пользованы приложением, -
нам необходимо хранить их на стороне нашего при­
ложения. Во-вторых, сами ключи, как правило, имеют длительный срок действия,
что не подходит для ситуаций, где нам необходимо периодически обновлять поль­
зовательские секреты. Плюс, нам необходимо унифицировать сам процесс получе­
ния АРI-ключа, что невозможно в рамках нашего примера, однако получило свое
продолжение в формате Oauth2 и JWТ-токенах.
Резюмируя, АРI-ключи -
это отличный выбор для аутентификации других серви­
сов и организации программного доступа, однако для аутентификации пользовате­
лей нам нужна большая гибкость, о которой мы сейчас поговорим.
JWТ-токены
Если обобщать, то для аутентификации пользователей сам пользователь должен
предоставить некое удостоверение (ключ, токен, сертификат), которое сервер смо­
жет провалидировать, чтобы удостовериться, что данный пользователь действи­
тельно тот, за кого себя выдает, т. е. аутентифицировать его.
Однако постоянно передавать ключ может быть неудобным, если мы говорим про
ситуации использования, где у нас присутствует обычный пользователь. Я думаю,
всем знакома аутентификация по логину и паролю. Такой набор секретов проще
запомнить человеку, чем, например, длинный ключ. В свою очередь, первая часть
этих секретов, а именно логин, который обычно представляет собой адрес элек-
168
Часть /. Проектирование и разработка системы
тронной почты, номер телефона или ник пользователя, может быть удобно исполь­
зован для восстановления доступа, поскольку вероятность его забывания значи­
тельно ниже.
Казалось бы, мы можем использовать эти данные для аутентификации запросов,
просто прикладывая их к каждому из наших вызовов. Однако такой подход не со­
всем применим, потому что нам будет необходимо хранить сам логин и пароль
в открытом виде где-то на клиентской стороне, что небезопасно. К тому же, если
каждый из запросов нашего приложения будет содержать логин и пароль, в случае
если произойдет нарушение шифрования трафика, злоумышленники смогут полу­
чить эти данные и сразу применить для неправомерного доступа. Третья проблема
заключается в том, что сами логин и пароль не содержат дополнительной полезной
информации, которая могла бы быть использована приложением для своих нужд,
например управления временем сессии, авторизацией и т. д. Подобные особенности
требуют разработки отдельного механизма, который позволит их компенсировать,
и такой подход есть.
JSON web token (JWT) является логичным продолжением АРI-ключа и позволяет
аутентифицировать запросы пользователя и при этом не требует постоянной пере­
дачи логина и пароля. Идея заключается в том, что пользователь может передать
свои логин и пароль только один раз и получить взамен от сервера некий токен,
который будет использоваться для всех последующих вызовов сервиса для того,
чтобы аутентифицировать эти запросы. Формально мы можем вернуть любую
строку, которую сможем проверить на действительность на серверной стороне (по
логике АРI-ключа), однако существует более гибкий подход, о котором мы сейчас
поговорим.
Сам JWT, помимо функций аутентификации пользователя, как правило, содержит
в себе полезную нагрузку, которую может интерпретировать как клиентская, так и
серверная сторона. Однако самое интересное связано с тем, что такой ключ имеет
собственную цифровую подпись. Давайте разберем основные части JWT.
Сам токен состоит из трех частей, которые представляют собой закодированные
в Base64 JSОN-документы, разделенные между собой точкой. Первая часть токена
называется заголовком (header) и содержит в себе системную информацию, напри­
мер тип токена, алгоритм шифрования. Вторая часть это полезная нагрузка
(payload), которая представляет собой такой же JSОN-документ, однако содержа­
щий некую полезную информацию, например имя пользователя, его роль, срок
действия токена. И третья часть -
это цифровая подпись (signature), о которой мы
поговорим подробнее.
Во время запроса на аутентификацию клиент передает на сервер свои логин и па­
роль в виде РОSТ-запроса. Получив их, сервер выполняет запрос в базу данных для
того, чтобы сверить имя пользователя и хеш пароля. Если данные действительны,
сервер создает новый JWТ-токен, в полезную нагрузку которого он помещает нуж­
ные ему поля (в предыдущем примере это были роль, срок действия токена, а также
имя пользователя). Также вся информация кодируется в формат Base64 для обеспе­
чения строкового вида. После формирования данной части сервер выполняет циф­
ровую подпись токена и добавляет ее в конец. Но что такое цифровая подпись?
Глава 9. Безопасность
169
Сам сервер имеет собственный, закрытый ключ, который используется для шифро­
вания данных. Зашифровав этим ключом общую часть токена, мы фактически фик­
сируем содержимое, исключая возможность дальнейшней модификации. Притом
что мы можем свободно читать содержимое полезной нагрузки, потому что оно не
зашифровано, мы не можем изменять его, потому что сервер добавил цифровую
подпись к содержимому ключа, и он будет проверять действительность этой под­
писи при каждом запросе. Давайте разберем это на примере.
Предположим, мы строим сервис для управления складом. В сервисе у нас есть не­
сколько ролей, например управляющий складом и обычный складской работник.
Оба используют одинаковый интерфейс аутентификации, представляющий собой
форму логина и пароля. Когда пользователь входит в систему, сервер после про­
верки действительности данных создает JWТ-токен, который содержит информа­
цию о роли пользователя, времени действия токена, имени пользователя. После
выполнения цифровой подписи токен возвращается пользователю, после чего каж­
дый запрос к сервису в заголовке, в поле Authorization, содержит этот токен. Когда
сервер получает такой запрос, он в первую очередь выполняет чтение этого токена,
после чего проверяется действительность цифровой подписи. Для этого сервер ис­
пользует тот же ключ, что и для шифрования токена, выполняет повторное шифро­
вание полезной части токена и сверяет получившийся шифр с цифровой подписью.
В случае если эти подписи одинаковы, токен считается действительным. Также
сервер может проверить время действия токена (для этого есть зарезервированное
поле ехр, в котором хранится метка времени, до которого токен является действи­
тельным) и в соответствии с тем, действителен токен или нет, разрешить или запре­
тить его использование. Сам сервер при этом может использовать полезную на­
грузку и этого токена, чтобы, к примеру, определить, обратившись к полю полез­
ной нагрузки, имеет ли данный пользователь административные привилегии.
А что если, к примеру, на клиентской стороне злоумышленник, получив доступ
к токену обычного пользователя и изменив в нем поле role на admin либо вовсе
сгенерировав свой собственный токен, отправит его с запросом? Именно для этого
и нужна цифровая подпись. Поскольку у злоумышленника отсутствует секретный
ключ для подписи токенов (который имеется у сервера), он не сможет сгенериро­
вать правильную цифровую подпись. В случае если подпись будет неверной, сер­
вер должен отклонить подобный запрос, а в идеале -
записать данное событие
в лог, поскольку отправка недействительных токенов может быть одной из попы­
ток поиска слабых мест в приложении.
Сама идея JWT так же проста, как и гениальна. Мы можем подписывать опреде­
ленные кусочки информации, которые могут быть интерпретированы разными час­
тями системы, при этом иметь явное подтверждение, что они действительны и бы­
ли сгенерированы именно нашей системой. Изменение токена на клиентской сто­
роне
при этом невозможно,
поскольку это
неизбежно приведет к изменению
цифровой подписи, что будет обнаружено сервером. Однако стоит помнить, что
данный токен все так же может быть перехвачен, если используется небезопасное
соединение. В таком случае данный токен может быть использован для вредонос­
ных запросов к системе. Для этого нередко токены могут быть ассоциированы
Часть /. Проектирование и разработка системы
170
с другой полезной информацией, например с IР-адресом аутентификации либо
метаданными браузера пользователя. В случае если данная информация у новых
запросов
стала резко отличаться,
есть
повод
выполнить отзыв токена,
поскольку
существует шанс, что он был скомпрометирован.
Одной из важных тем является ротация токенов. Вместо того чтобы иметь один
вечный токен (который может быть перехвачен и также вечно использоваться зло­
умышленником), мы можем передавать пользователю сразу два токена, токен для
запросов и так называемый токен для обновления (refresh token). Когда наш основ­
ной токен истекает, вместо того, чтобы заставлять пользователя еще раз аутенти­
фицироваться, мы можем на уровне фронтенд-приложения выполнить запрос на
обновление нашего токена, приложив к запросу токен для обновления. Если этот
токен действителен, сервер вернет нам новую пару -
токен для запросов и токен
для обновления. Таким образом мы обеспечим ротацию токенов, не вынуждая
пользователя каждый раз вводить логин и пароль.
Стоит помнить, что данные токены также имеют определенный размер, и если вы
поместите слишком много информации в полезную нагрузку, то это может замедлить
ваши запросы, а также увеличить нагрузку на сеть. История знает примеры, когда
из-за злоупотребления данным механизмом размер токена мог быть около сотни
килобайт, что при большой плотности запросов может сильно нагружать сеть.
Использование токенов позволяет также реализовать механизм их отзыва. Для того
чтобы реализовать это, мы можем генерировать уникальный идентификатор сессии
и помещать его как в полезную нагрузку токена, так и в базу данных. В случае если
мы хотим отозвать этот токен, помимо классической проверки цифровой подписи,
мы также можем выполнить запрос к базе данных (для этого отлично подходят ба­
зы данных «ключ-значение», хранящие данные в оперативной памяти, например
Redis или Memcached, хотя мы вполне можем использовать и базы данных SQL)
для того, чтобы проверить, не находится ли данная сессия в черном списке. Если
да, мы можем отклонить запрос ввиду того, что сама сессия недействительна.
Чаще всего JWТ-токены
используются для
аутентификации пользователя для
фронтенд-приложений. Современные браузеры имеют удобные API, которые по­
зволяют сохранять данные во внутреннее хранилище браузера для того, чтобы ис­
пользовать его впоследствии. Также таким механизмом являются файлы
cookie,
однако для приложений, которые способны видоизменять НТТР-запросы, например
добавляя к ним нужные заголовки, самым частым подходом к хранению является
именно внутреннее хранилище браузера.
0Auth2
Существуют ситуации, когда нам необходимо получить доступ к ресурсам, кото­
рые не относятся к нашему приложению напрямую, однако они являются частями
других приложений, которые могут содержать полезную информацию. При этом
наш пользователь может также иметь аккаунт на этой платформе, пользуясь ею по
ее назначению. Иногда нам нужно получить доступ к этим данным со стороны на­
шей платформы, выполняя запросы как бы от имени пользователя, который там
Глава 9. Безопасность
171
уже зарегистрирован. Такой тип взаимодействий может позволить легко объеди­
нять данные и сделать взаимодействия более бесшовными, добавляя возможность,
например, синхронизировать данные между платформами либо, например, выпол­
нять действия сразу в нескольких приложениях, имея один главный триггер. Оче­
видно, что такие взаимодействия можно реализовать разными путями, что постави­
ло бы крест на удобстве интеграции между приложениями. Поэтому был разрабо­
тан
стандарт,
который
называется
0Auth2,
определяющий
именно
такой тип
взаимодействия между приложениями.
Например, мы построили сервис удобной аннотации документов, который поддер­
живает добавление своих собственных файлов. Множество пользователей пользу­
ются облачными хранилищами, и хорошей функцией была бы возможность син­
хронизации этих облачных хранилищ с нашим сервисом. То есть пользователь, ко­
торый уже зарегистрирован в облачном хранилище, может разрешить нашему
приложению аннотации читать и видоизменять файлы, которые находятся в облач­
ном хранилище, выполняя действия от имени пользователя, выдавшего доступ. Ка­
ким образом мы можем реализовать это, используя 0Auth2?
Первым шагом будет регистрация нашего приложения на платформе, с которой мы
хотим совершить интеграцию. Чаще всего мы должны зарегистрироваться в каби­
нете разработчика и получить две важные точки данных -
c\iend_id и client_secret.
Данные секреты будут использоваться нашим приложением для того, чтобы вы­
полнять запрос на авторизацию для пользователей нашей платформы.
Вторым шагом является добавление в наше приложение возможности авторизовать
синхронизацию. На стороне приложения мы должны перенаправить пользователя
на страницу сервера авторизации стороннего ресурса, приложив к запросу (как
правило, используя quеrу-параметры) такие данные, как client_id (полученный при
регистрации), redirect_uri (адрес, на который сервер авторизации выполнит перена­
правление пользователя в случае, если он разрешит доступ), информацию о том, на
какие именно ресурсы планируется осуществлять доступ (scope), а также сервис­
ный атрибут response_type, который в нашем контексте будет являться кодом авто­
ризации.
После
перенаправления
на страницу сервера авторизации пользователь может
аутентифицироваться на ней, используя личные данные именно целевой платфор­
мы, для которой он собирается передать доступ. После разрешения доступа сервер
авторизации выполняет перенаправление, используя
redirect_uri, передавая при
этом код авторизации. Redirect_uri при этом является уже частью нашего приложе­
ния, и мы можем получить доступ к коду авторизации, и, выполнив еще один за­
прос
к серверу авторизации,
приложив к нему вместе с кодом информацию
о client_id и client_secret, мы наконец получаем access_token, который можем ис­
пользовать для доступа к защищенным ресурсам.
Используя данный токен, мы можем получить доступ от имени другого пользова­
теля к
API облачного хранилища и, например, запрашивать и изменять файлы.
Множество популярных сервисов имеют такой функционал, позволяя расширить
пользовательские истории на множество других сервисов.
Часть /. Проектирование и разработка системы
172
Помимо простого обращения к ресурсам, мы можем использовать личную инфор­
мацию пользователя в рамках своей платформы. Я думаю, вы часто видели, когда
на сайтах либо форумах есть кнопка аутентификации с помощью аккаунтов в соци­
альных сетях. Данный подход является частным случаем использования формата
0Auth2 и называется OpenID Connect, или сокращенно OIDC. Сам механизм крайне
похож на вышеописанный процесс получения access token для 0Auth2, однако вме­
сте с ним наше приложение получает 1D Token. Последний представляет собой
JWТ-токен, который содержит в себе какую-то часть личной информации пользо­
вателя с сервиса, на котором мы только что получили доступ, например это могут
быть имя, почта, ник, а также информация, которая может быть использована для
получения дополнительных деталей о пользователе. В свою очередь, мы можем
сохранить эту информацию в нашей системе и использовать ее для собственных
нужд, например мы можем заполнить личные данные пользователя в нашей собст­
венной базе данных.
Этот раздел не претендует на исключительное описание данных стандартов. Хотя
они и имеют определенную стандартизацию, настройка 0Auth2 имеет свои особен­
ности для каждого из сервисов, поэтому стоит обратиться к документации для того,
чтобы выполнить правильную интеграцию. Однако общий принцип для всех серви­
сов остается общим, и важно знать, что ваше приложение может иметь возмож­
ность запрашивать информацию из других сервисов, даже если для этого необхо­
дима регистрация пользователя на них.
IAM в рамках облака, RBAC и АВАС
Как уже говорилось ранее, облачные провайдеры предоставляют не только вычис­
лительные
мощности,
но и определенные инструменты,
которые
позволяют упро­
стить управление теми или иными частями разработки. Поскольку безопасность
в облаке играет далеко не последнюю роль, такие же инструменты присутствуют и
для управления доступами. Ранее в этой главе мелькала такая аббревиатура, как
IAM, identity and access management, и сейчас есть смысл разобрать ее подробнее.
Стоит сказать, что реализация может отличаться от провайдера к провайдеру, но
общий принцип будет оставаться похожим. Большая часть примеров и терминоло­
гии будет связана с гиперскейлером А WS.
IAM привносит в облако сразу несколько абстракций, которые позволяют управ­
лять доступами. Во-первых, это сама абстракция пользователя. Для управления об­
лачной инфраструктурой мы, естественно, можем создавать отдельные аккаунты,
чтобы каждый человек мог управлять своими ресурсами. Для большего удобства
данные аккаунты можно объединять в группы, например объединяя департаменты.
Вторая абстракция -
это политики доступа. Существует большое количество ре­
сурсов, к которым можно получить доступ в рамках облака, это и облачные храни­
лища, и сервисы аналитики, и сервисы создания виртуальных машин и т. д. Фор­
мально данная политика представляет собой JSОN-документ, который содержит
в себе описание того, с какими именно ресурсами может взаимодействовать носи­
тель этой политики, а также то, что именно носитель может делать на этих ресур-
173
Глава 9. Безопасность
сах. Например, мы можем разрешать доступ только на чтение для определенной
области объектного хранилища. Как правило, по умолчанию пользователи не име­
ют доступа ко всем ресурсам, поэтому подобные политики относятся к разреши­
тельным. Политики могут быть применены не только к пользователям отдельно, но
и к группам, что позволяет удобнее управлять групповыми политиками доступа.
Помимо пользователей, доступ к другим сервисам может потребоваться и у запу­
щенных внутри сервисов. Например, наше приложение может складывать файлы на
объектное хранилище, и мы можем формализовать доступ туда с помощью таких
же политик доступа, поскольку по умолчанию доступ к внутренним ресурсам огра­
ничен. Для того чтобы обеспечить доступ не для пользователей, а для сервисов,
существует еще одна абстракция -
роль запуска. По своей сути она похожа на аб­
стракцию с пользовательскими политиками, за тем исключением, что она применя­
ется к выполняющемуся сервису. Используя данную роль, мы можем определить,
какие именно ресурсы должны быть доступны во время выполнения. Удобным мо­
ментом является то, что нам не нужно вручную управлять секретами для доступа
в облако, облачный провайдер самостоятельно создает их, а также пробрасывает их
внутрь работающего приложения, используя переменные окружения либо специ­
ального демона, запущенного вместе с сервисом.
Сами политики доступа могут иметь множество полезных функций. Помимо про­
стого разрешения доступа,
можно определять условия, при
которых эта политика
будет действительна. Например, мы можем ограничить использование этой поли­
тики только для определенного региона либо только в рамках определенного сер­
виса, что повышает безопасность. Мы также можем прикреплять к пользователям
и ролям сразу несколько политик.
Данное краткое описание является ярким примером организации доступа на основе
ролей, или сокращенно RBAC, role based access control. Данный подход является
достаточно распространенным, поскольку прост для понимания
-
у нас есть роли,
которые имеют определенный доступ, например роль обычного пользователя или
роль администратора. С каждой ролью ассоциированы определенные разрешения,
которые может использовать носитель данной роли. Такой подход отлично соотно­
сится с некой общей логикой, где руководитель, к примеру, может видеть большую
часть предприятия, чем обычный работник. Однако такой подход не позволяет
обеспечить удобства в ситуациях, когда необходимо обеспечить множество разных
доступов, поскольку это будет связано с управлением большим количеством ролей,
чтобы реализовать все комбинации прав.
Другим подходом является организация доступа на основе атрибутов, сокращенно
АВАС, attribute based access control. Вместо того чтобы назначать роли, мы можем
управлять другой абстракцией -
атрибутами. Наличие либо отсутствие атрибутов
у того или иного пользователя позволяет определить, можно ли разрешать доступ
к запрашиваемым ресурсам или нет. Иными словами, мы можем выдавать пользо­
вателям атрибуты на чтение из хранилища, на запись в него, а далее механизм
обеспечения доступа будет заниматься интерпретацией данных атрибутов. Такой
подход позволяет реализовать более тонкий контроль доступа для каждого отдель­
ного пользователя. Однако в сравнении с доступом на основе ролей он имеет боль-
174
Часть /. Проектирование и разработка системы
шее количество накладных расходов, более сложный, к тому же он не совсем под­
ходит в ситуациях, где разнообразие прав доступа у нас невелико. Поэтому, как
правило, для простых проектов выбирается подход на основе RВАС, а там, где
нужно обеспечить более тонкий доступ, используется АВАС.
Основные векторы атак на приложения
Описанные выше концепты касаются организации безопасности в тех местах, в ко­
торых она нужна по определению. Разграничение прав, шифрование секретов это подходы, которые уже известны и реализуются, однако есть целый класс ситуа­
ций, в которых происходит эксплуатация функционала, который мог быть даже не­
известен разработчикам либо владельцам платформы.
Ввиду того что современные приложения могут быть довольно сложными с боль­
шим количеством функций и логики, всегда существует шанс, что какие-то из этих
функций при определенных условиях могут работать неправильно, заставляя при­
ложение выполнять вредоносную логику. Это может привести не только к недос­
тупности приложения, но и к утечке личных данных либо к краже аккаунтов, в том
числе с административным доступом со всеми вытекающими последствиями.
Бэкенд-приложения особенно подвержены этому, поскольку они, как правило, дос­
тупны через публичный Интернет, что означает очень большую площадь атаки.
Поэтому важно знать об основных опасностях, которые хорошо известны, и для
них существуют способы, которые позволяют защитить приложение.
SQL-инъекции
Одним из типов уязвимостей" который позволяет злоумышленникам выполнять
SQL-запросы в вашей базе данных, при этом не имея к ней прямого доступа, явля­
ется SQL-инъекция. При этой уязвимости может произойти утечка данных прило­
жения, таких как личные данные пользователей, информация об их действиях формально это может быть все, что лежит в базе данных.
Основная идея проста: у нас есть приложение, которое имеет определенную форму
для ввода информации, например для поиска по сайту. Когда пользователь вводит
туда ключевые слова для поиска, информация сразу попадает в SQL-зaпpoc, после
чего приложение получает результат поиска и возвращает его пользователю. Одна­
ко проблема заключается в том, что приложение может не валидировать входящие
запросы из строки, что может позволить злоумышленнику вместо ожидаемых клю­
чевых слов отправить SQL-кoд, который выполнится в рамках базы данных и будет
возвращен с результатами поиска.
Иными словами, уязвимость подобного рода может позволить выполнять произ­
вольный SQL-кoд, фактически получая доступ к базе данных продакшена. Подоб­
ные уязвимости могут встречаться не только в вашем приложении, но и в устарев­
ших библиотеках, особенно если вы используете какие-либо готовые движки для
управления веб-приложениями.
Глава 9. Безопасность
175
Благо для того, чтобы защитить приложение от подобных атак, существует множе­
ство подходов. Если вы пользуетесь сторонними библиотеками и движками, стоит
проверять их на предмет уязвимостей, а также держать их в обновленном состоя­
нии.
Если вы реализуете свои собственные интерфейсы (что происходит довольно часто,
когда вы строите свои собственные бэкенд-приложения), стоит внимательнее изу­
чать пользовательский ввод. Вам необходимо иметь строгие контракты на обмен
данными, например не разрешать пользователям отправлять специальные символы
либо запросы слишком большой длины. Таким образом, если злоумышленник
попробует вставить в форму длинный SQL-зaпpoc, ваша система сможет его от­
фильтровать. Частым подходом также является использование файерволов, кото­
рые способны сканировать запросы со стороны пользователей и детектировать
в
них
вредоносную
начинку,
например
понимать,
что
запрос
на поиск
содержит
в себе часть SQL-кoдa, и автоматически блокировать его. Подобный функционал
можно реализовать также и в самом бэкенд-приложении.
От подобной уязвимости также защищает правильное использование ОRМ-биб­
лиотек. Поскольку прямое исполнение SQL-кoдa как бы скрыто за фасадом биб­
лиотеки, и также мы имеем более строгий формат поиска, который зафиксирован
именно
на уровне
кода,
где
мы
указываем,
какие
именно колонки
мы
хотим
от­
фильтровать и какого они типа, ОRМ-библиотека просто не разрешит выполнение
кода, который не соответствует критериям поиска из-за своего типа.
Еще одним подходом, который относится не только к SQL-инъекциям, но и случа­
ям, когда к запущенному приложению был получен полный доступ, является огра­
ничение прав. Если вы помните принцип наименьших привилегий, в рамках нашего
примера это означает, что наше приложение не должно иметь доступ к тем данным,
которые не нужны для его непосредственной работы. Это, как говорилось ранее,
позволяет уменьшить «радиус взрыва», минимизировав масштабы утечки.
Резюмируя: для того чтобы данные вашего приложения были в безопасности, стоит
выполнять фильтрацию пользовательского ввода, использовать ОRМ-библиотеки,
проверять актуальность используемых вами модулей и стараться проводить тести­
рование на безопасность регулярно.
xss
Данная уязвимость больше относится к фронтенд-части приложения, однако бэкенд
в ней может играть не последнюю роль. Cross-Site Scripting (XSS) -
это подход,
при котором в фронтенд-часть веб-приложения может быть интегрирован вредо­
носный
код,
выполняющий
заданные
злоумышленником
действия.
Поскольку
браузер выполняет JavaScript, имеющий большое количество функций, интеграция
вредоносного кода может привести к краже токенов, фишинrу, интеграции сторон­
него контента и т. д. Поэтому важно понимать, как защищать ваши приложения от
него.
Простой пример: наше приложение имеет функционал добавления комментариев,
которые видны всем пользователям платформы. Однако в какой-то момент, когда
Часть /. Проектирование и разработка системы
176
мы заходим на страницу с комментариями, появляется странное окно, которое мы
явно не добавляли при разработке. Все это связано с ХSS-уязвимостью в нашем
приложении.
Как и в случае с SQL-инъекцией, это связано с недостаточной проверкой пользова­
тельского ввода. Когда пользователь добавляет комментарий, содержимое коммен­
тария сохраняется в нашей базе данных, после чего при запросе на чтение коммен­
тариев мы создаем веб-страницу, наполняем ее этим текстом и возвращаем его
пользователю.
Однако что будет,
если
пользователь интегрирует вредоносный
JavaScript-кoд в текст комментария? Поскольку этот код будет возвращаться для
каждого запроса на чтение, он будет исполняться при каждом просмотре страницы
у всех пользователей, поскольку браузер пользователя будет выполнять весь код,
который возвращает сервер.
Подобная ситуация также может случиться и без бэкенд-части. Например, если веб­
приложение может интерпретировать параметры URL, то можно добавить вредо­
носную часть именно в нее, после чего отправить эту ссылку пользователям. Сайт
успешно откроется, а веб-приложение вместо ожидаемого параметра может запус­
тить JavaScript-кoд, что может привести к краже токенов, сессии и личных данных.
Защититься от этого можно похожими подходами, как в случае с SQL-инъекциями.
Во-первых, нужно фильтровать пользовательский ввод на предмет отправляемого
кода; если он там присутствует, важно записывать в лог данные события и, естест­
венно, отклонять подобные запросы. На стороне веб-приложения нужно стараться
избегать исполнения всего, что возвращается с бэкенда, пользуясь специальными
библиотеками, фильтрующими исполнение зловредного кода. То же самое отно­
сится и к интерпретации URL-параметров. Это особенно важно при использовании
сторонних библиотек для фронтенда, которые работают с пользовательским вво­
дом, поскольку всегда может существовать ситуация, когда сторонняя библиотека
имеет данную уязвимость,
что автоматически ставит под угрозу
и ваше приложе­
ние.
CSRF
Данная уязвимость не является такой очевидной, но последствия от нее могут быть
не менее серьезными. Cross site request forgery (CSRF) -
это уязвимость, при кото­
рой стороннее приложение может выполнить запрос в приложении, в котором
аутентифицирован пользователь, без его согласия. Связано это с тем, что веб­
сервер по умолчанию не знает, с какой именно страницы проводится запрос, и сам
браузер может включить в него сооkiе-файлы, которые могут содержать токен
аутентификации. Рассмотрим примерный сценарий работы.
У нас есть банковское приложение, и мы храним токен аутентификации в файлах
cookie. Злоумышленник создает подставное приложение и отправляет ссылку на
него клиентам нашего банка. После перехода по этой ссылке вредоносное прило­
жение совершает запрос к бэкенду нашего банка, например запрашивая перевод
денег. Поскольку данный запрос будет включать сооkiе-файлы нашего приложения,
данный запрос успешно пройдет аутентификацию и может быть выполнен, несмот-
177
Глава 9. Безопасность
ря на то, что мы выполняем запрос с другой страницы. К слову, советы перехода по
неизвестным ссылкам, которые приходят нам на почту, во многом могут быть свя­
заны с таким типом уязвимостей, потому что даже простой заход на сайт может
привести к определенным последствиям для пользователя и платформы, поскольку
выполнит вредоносный код, причем сразу, как только пользователь перейдет по
такой ссылке.
Каким образом можно защититься от этого? Самым распространенным является
добавление дополнительных токенов для того, чтобы определить, был ли запрос
выполнен именно из нашего приложения. Поскольку
CSRF не позволяет читать
содержимое других приложений, мы можем добавлять определенные токены как
невидимые поля в наши веб-формы. Для того чтобы запрос был успешный, прило­
жение должно отправить не только токен аутентификации, но и anti-csrf-тoкeн, ко­
торый оно может получить только из веб-страницы оригинального приложения.
Веб-сервер может хранить такие токены в базе данных и сверять их. В случае если
такой токен отсутствует либо неверный, это означает, что запрос был скомпроме­
тирован, и такое событие определенно стоит записывать в журнал и анализировать.
Также
подобные
атаки
можно
предотвратить,
если
устанавливать
атрибуты
SameSite для файлов cookie. Также активно применяется проверка заголовков за­
проса для того, чтобы определить, откуда производился запрос.
Для особенно важных операций, например перевода денежных средств или изме­
нения чувствительных данных, стоит добавлять дополнительные шаги, которые
должен выполнить пользователь. Например, это может быть двухфакторная аутен­
тификация, при которой дополнительно требуется ввести код из специального при­
ложения или отправленный на мобильный телефон (электронную почту). Подоб­
ные действия не только затрудняют СSRF-атаки, но и уберегают пользователя от
ситуаций, в которых он мог случайно выполнить определенные действия.
DDоS-атаки
Нередко получается, что простота подхода ничуть не затрагивает его эффектив­
ность. Часто какие-то вещи можно попробовать решить в лоб, и именно эта идеоло­
гия присуща целому классу атак, призванных положить приложение. Речь пойдет
о DDoS-aтaкax (distribute denial of service, распределенный отказ в обслуживании).
Когда мы строим бэкенд-систему, мы можем предполагать определенные лимиты
с точки зрения пропускной способности. Это вызвано множеством факторов, таких
как скорость базы данных, количество экземпляров приложения, скорость сети.
И нередко даже в рамках обычного использования в случае большого количества
поступающих запросов платформа может столкнуться с пониженной производи­
тельностью. Это выражается в увеличенной задержке, недоступности ресурсов, не­
срабатывании определенных сценариев работы. Все это плохо для бизнес-части,
поскольку когда приложение не работает, оно не выполняет свою роль, что может
выливаться в проблемы финансового характера, а также недовольство пользова­
телей.
DDоS-атаки спроектированы именно для этого -
вызвать недоступность приложе­
ния, для того чтобы поставить под удар функционирование бизнеса. Представьте,
178
Часть /. Проектирование и разработка системы
что вы являетесь владельцем крупного интернет-магазина. Единственный источник
пользователей -
это Интернет, и единственный способ заказать товары -
это ва­
ша платформа. В случае если она недоступна, вы можете потерять большое количе­
ство потенциальных покупателей, которые могут купить нужный им товар на дру­
гих платформах, причем не потому, что там может быть меньшая цена или более
заманчивые условия., а просто потому, что их платформа открылась. Таких приме­
ров можно привести много, поэтому давайте разберемся, как не допускать такие
ситуации.
Во-первых, сама атака заключается в создании искусственной нагрузки на ваше
приложение, как при нагрузочном тестировании. Для этого злоумышленники могут
владеть целыми подсетями захваченных компьютеров, которые могут в один мо­
мент начать отправлять огромное количество запросов на вашу платформу, как
будто они обычные пользователи. Повышенный объем мусорного трафика будет
нагружать базы данных и сервисы, что приведет к понижению эффективности ра­
боты платформы у обычных пользователей. В какой-то момент приложение может
просто зависнуть или отключиться, причем включиться обратно ему будет не да­
вать злоумышленник, потому что отправка DDоS-запросов просто так не прекра­
тится. Особенно дорого подобные атаки обходятся в случае бессерверной архитек­
туры, например если мы строим наш сервис на основе бессерверных функций,
в случае которых мы платим за фактическое время работы. Несмотря на то что
функции обрабатывают мусорные запросы, их выполнение все равно стоит денег,
и нередко очень больших, если запросов много, поскольку, как мы помним, бессер­
верная архитектура прекрасно масштабируется, нередко быстрее, чем наш бюджет.
Каким образом мы можем избежать этого? Это комплексный вопрос, который
можно решать разными путями. Во-первых, если наше приложение находится
в группе потенциального риска, стоит обзавестись файерволом, который будет ска­
нировать поступающий трафик на предмет того, является ли он вредоносным или
нет. Подобные решения способны автоматически блокировать вредоносные IР­
адреса, если с них поступает аномальное количество запросов. Помимо простой
блокировки, они способны выполнять троттлинг запросов, т. е. делать замедление,
чтобы наша система бьmа способна обработать ожидаемое количество. К слову,
ограничение максимального количества запросов -
это хорошая практика, которой
стоит придерживаться и в рамках обычной эксплуатации системы на случай ано­
мальной пользовательской активности.
Нередко такие файерволы являются сторонними решениями, которые легко интег­
рировать в ваше решение. В случае облачного развертывания провайдеры нередко
имеют специальные сервисы для DDоS-защиты. Касательно примера с бессервер­
ными функциями, любопытной особенностью того же А WS является то, что если
DDoS-aтaкa будет обнаружена и будет подключен специальный план на DDоS­
защиту (платный, естественно), то провайдер вернет потраченные сервисами день­
ги, которые были израсходованы из-за активности злоумышленников.
Не последнюю роль в защите от DDoS-aтaк играет мониторинг. Например, выстав­
ленные мониторы на метрики, такие как утилизация CPU, использование оператив­
ной памяти, количество запросов, позволят сразу уведомить разработчиков о том,
179
Глава 9. Безопасность
что на платформе происходит что-то аномальное. Благодаря этому вы можете сразу
узнать, что приложение подвергается атаке, и принять меры. Например, если боль­
шая часть запросов идет из какой-то одной страны, вы можете на время заблокиро­
вать определенные подсети адресов для того, чтобы минимизировать влияние на
ваше приложение. Да, подобное можно делать и автоматически, но участие челове­
ка в подобных ситуациях играет большую роль, поскольку позволяет минимизиро­
вать последствия для обычных пользователей.
Для удобства все перечисленные варианты атак сведены в табл. 9.1.
Таблица 9.1. Виды атак на приложение
Название
Принцип
Методы противодействия
SQL-инъекции
Интеграция вредоносной логики в испол-
ОRМ-библиотеки, фильтрация
няемый сервером SQL-зanpoc
пользовательского ввода, аудит
библиотек
~
xss
Исполнение вредоносного кода в брау-
Фильтрация пользовательского
зере пользователя посредством
ввода, аудит библиотек
уязвимостей клиентских библиотек
CSRF
Связано с особенностью работы браузе-
Использование СSRF-токенов,
ра и отправки им запросов, что может
СОRS-политики, аудит
приводить к непреднамеренным вызовам
со стороны вредоносного сайта
DDоS-атаки
Перегрузка сервера посредством
Использование специальных реше-
мусорного трафика
ний для DDoS, файерволов, политик
фильтрации трафика
Заключение
Безопасность является неотъемлемым пунктом любой системы. Есть даже такая
любопытная фраза, которая в адаптации звучит так: «За безопасность нужно пла­
тить, а за ее отсутствие
-
расплачиваться».
Интеграция технических средств, которые обеспечивают управление секретами,
шифрование, контроль доступа, безусловно, требует усилий со стороны команды
разработки. Для того чтобы правильно их использовать, может потребоваться
определенная квалификация, так же как и их дальнейшая поддержка. Однако, если
наша платформа имеет дело с пользовательскими данными, у команды может про­
сто не быть другого выбора, кроме как интегрировать эти практики в работу.
Помимо общих средств, важно не забывать про безопасность на уровне бизнес­
логики. Поскольку она может реализовываться с нуля, велик шанс того, что могут
быть допущены ошибки, которые могут приводить к уязвимостям. Помочь с этим
может как более глубокое тестирование с упором на безопасность, так и лучшие
практики разработки.
180
Часть /. Проектирование и разработка системы
Тестирование вашего приложения с точки зрения популярных уязвимостей и век­
торов атак -
это важная работа. Если вы заранее знаете план действий либо потен­
циальные узкие места, то в случае реального инцидента у вас будет дополнитель­
ное преимущество. В свою очередь, использование журналирования событий также
положительно влияет на безопасность системы при регулярном аудите.
При этом важно сохранять баланс. Условно, если вы строите простой интернет­
магазин, который не хранит в себе критические пользовательские данные, возмож­
но, использование тройного шифрования внутри закрытого периметра может быть
лишним и будет только отнимать время разработчиков на поддержку. Однако если
специфика вашей задачи требует повышенной безопасности, то к этому вопросу
нужно
подходить
максимально
ответственно,
не
оставляя
злоумышленникам
ни
единого шанса, поскольку от этих действий зависит не только репутация бизнеса,
но и личные данные чужих людей, которые доверили их вашей платформе.
ЧАСТЬ
11
Системная архитектура
Глава 10.
Этап эксплуатации и подготовка к ней
Глава 11.
Проверка изменений
Глава 12.
Доставка изменений
Глава 13.
Мониторинг работы системы
11111
11111
ГЛАВА 10
Этап эксплуатации
•
и подготовка к ней
Разработка любой системы имеет определенную цель. Как правило, это решение
поставленной бизнес-задачи: будь то разработка платформы с нуля или выполнение
специфической интеграции -
любая коммерческая разработка должна иметь опре­
деленные задачи, решение которых будет значимым для бизнеса. Поэтому в разра­
ботке наступает момент, когда все то, что мы разработали, тестировали, оценивали,
увидит свет, и кто-то будет этим пользоваться. И к этому моменту нужно хорошо
подготовиться.
С другой стороны, тут присутствует определенный парадокс. Скорость разработки
всегда является важным параметром, и нередко для ее достижения придется срезать
определенные углы, например упрощая технические подходы реализации, накапли­
вая технический долг в обмен на более быструю реализацию функций. Либо наобо­
рот, разработчики могут стараться предусмотреть большое количество вариантов
развития
событий для того,
чтобы дальнейшее развитие системы было более
полноценным либо чтобы текущее приложение могло обрабатывать значительно
большую нагрузку, чем предполагается, с возможностью расширения до других
бизнес-ситуаций.
Подобный баланс приходится соблюдать в любой системе, которая призвана ре­
шать задачи мира бизнеса, и не существует определенного свода пунктов, пройдя
которые, мы сможем получить точку на прямой, к которой мы должны стремиться.
Но нельзя забывать один важный момент -
настоящую пользу ПО начинает при­
носить только тогда, когда оно работает, а не находится в репозитории кода
в ожидании своего часа.
О техническом долге
Как уже говорилось ранее, разработка сервисов это про баланс разных сил. С одной
стороны, мы имеем бизнес, которому необходимо быстрое и качественное решение
бизнес-задачи. Формально бизнесу не важно, насколько система, решающая эту
задачу, будет сложна технически, какое количество сервисов в ней задействовано.
Важен сам результат, а также надежность его получения. С другой стороны, мы
имеем команды разработки и архитекторов ПО, которые понимают, что для надеж-
Часть 11. Системная архитектура
184
ной реализации, а также возможности подстраивания под новые бизнес-требования
мы не можем реализовывать систему по принципу наименьшей сложности. Если
мы будем срезать углы, обеспечивать низкую абстрактность системы, со временем
она станет трудноизменяемой, и если опустить детали, связанные с масштабируе­
мостью, лучшими практиками, контролем за качеством кода, то она может в прин­
ципе перестать справляться с бизнес-задачей, по крайней мере в удобной для поль­
зователей форме.
В рамках парадигмы управления техническим долгом мне нравится одна аналогия.
Предполож·им, перед нами стоит задача собрать яблоки с яблони. Когда мы только
начинаем этот процесс, большой смысл имеет начать собирать низко висящие
фрукты. Однако со временем, когда они закончатся, мы не сможем собрать более
высокие яблоки, поскольку то, что у нас есть для сбора (фактически наши руки), не
позволяет нам дотянуться до них. Нам нужно иметь лестницу, которая позволит
нам расширить ту область, в которой мы можем собрать фрукты. Плюс заключает­
ся
в том,
что эту лестницу
впоследствии
мы
можем
переиспользовать,
например
собрать другие фрукты либо достать другие высоколежащие предметы. Однако пе­
ред этим нам нужно потратить время на получение лестницы, например собрать ее
из досок.
Ключевым моментом тут является то, что сама лестница без бизнес-задачи беспо­
лезна. Да, мы можем потратить время на ее создание, однако если у нас нет задач, в
которых она может пригодиться, то оно, скорее всего, будет потрачено впустую.
А также во время разработки, если прибегать к аналогии с яблоками, чаще всего мы
имеем временнь1е ограничения. Если у нас не окажется лестницы в нужный мо­
мент, мы можем не успеть собрать все яблоки и выполнить бизнес-задачу качест­
венно и в срок.
Применим эту упрощенную аналогию к миру разработки. Бизнесу выгодно требо­
вать более быструю реализацию решений, поскольку это позволит быстрее полу­
чить требуемую функцию, а если бизнес прибегает к наемной разработке, это
может уменьшить расходы на самих разработчиков (это грязный прием, но это биз­
нес). В свою очередь, разработчикам нередко может потребоваться дополнительное
время на реализацию технических решений, которые хотя и не решают саму биз­
нес-·~адачу напрямую, но являются той самой лестницей, которая в перспективе
может сильно упростить внесение новых функций и решение новых задач. При
этом если слишком сильно сконцентрировать свое внимание на технической части,
на производстве этих самых лестниц, то можно потерять из виду саму бизнес­
задачу, не оставив на ее реализацию достаточного количества ресурсов.
И у этого есть свой термин. Когда во время разработки происходит срезание углов
для того, чтобы быстрее решить саму задачу в моменте, при этом появляются неоп­
тимальные участки в коде, это называется техническим долгом. Его нельзя как-то
конкретно измерить; код, который имеет статус технического долга, визуально мо­
жет выглядеть очень похоже на обычный, стабильный код, однако он вносит в про­
ект определенные технические риски, которые нужно учитывать.
Формально сам по себе технический долг в моменте неопасен. Например, нам нуж­
но было внести новую кнопку в интерфейс или интегрировать новую функцию на
Глава 1О. Этап эксплуатации и подготовка к ней
185
бэкенде. И для того, чтобы это сделать быстро, мы могли, как часто говорят, «за­
хардкодить это» (от слова hardcode, т. е. напрямую прописать какую-то логику, из­
бегая абстракций). Данный кусок функционала может отлично выполнять свои
функции на данный момент, однако все начинает усложняться, когда нам нужно
добавлять новые функции со временем. Если не соблюдать качество написания ко­
да, добавлять функции быстро, не задумываясь о логике расширения, со временем
код нашего проекта может стать нестабильным и нечитаемым. Сложность внесения
новых функций будет возрастать, со временем всё труднее будет разобраться, как
именно нужно расширять код. Скорее всего, таким образом и приходят к архитек­
турам типа «большого шара грязи».
Такой подход, когда мы нацеливаемся только на скорость разработки, без оглядки
на правильность, нарушает один из важных принципов хорошего кода
-
расши­
ряемость. Если код приложения нельзя будет расширить, добавить в него новый
функционал и сделать эффективнее, это в конечном итоге приведет к замедлению
развития самого бизнеса, поскольку он станет заложником программного обеспе­
чения, которое не может развиваться достаточно быстро. Причем любопытно, что
причиной этого как раз и является желание ускорить получение новых функций.
Для того чтобы этого избежать, необходимо задумываться о том, что именно нужно
делать с техническим долгом и как правильно им управлять. Во-первых, ключевым
тут является опыт архитектора и разработчиков. Когда ты реализовал множество
проектов, в том числе и пет-проектов, у тебя появляется
видеть узкие места той
или иной реализации и просчитывать последствия от них. Например, дизайн кода
может быть построен таким образом, что внесение изменений в него, а также его
расширение не будут представлять серьезную проблему, хотя на изначальное его
написание может потребоваться больше времени, чем на написание кода, который
просто решает бизнес-задачу. Во-вторых, важным является этап ревью кода. Более
опытные коллеги могут поделиться своим видением реализации того или иногда
функционала, а также оценить текущий подход. Благодаря этому можно добиться
повышения качества кода, он будет иметь больше возможностей для расширения и
развития.
Понятие технического долга выходит далеко за рамки самого кода. Нередко для
более быстрой разработки можно начать с монолитной архитектуры, что позволит
сэкономить время на управлении системой и потратить его на фактическую разра­
ботку. Либо мы можем использовать определенные технологии, которые могут по­
зволить нам быстрее смоделировать процесс, например nо-соdе-платформы. Одна­
ко со временем могут начать выскакивать компромиссы таких решений. Например,
если мы захотим реализовать что-то более сложное, мы можем быть ограничены
выбранными технологиями, либо, если наша система перестанет справляться с на­
грузкой, нам будет требоваться переписать большой процент системы, что в опре­
деленных реалиях невозможно. Иными словами, сам феномен технического долга
свойствен всему проекту целиком, при этом вне зависимости от локализации его
влияние остается одинаково критичным.
Рассмотрим другую ситуацию. Мы можем потратить все время на исправление
либо предотвращение появления технического долга. Например, мы можем потра-
Часть 11. Системная архитектура
186
тить всё время на написание идеального кода, пользуясь всевозможными патrерна­
ми, сразу начать разработку с микросервисной архитектуры для того, чтобы преду­
смотреть расширение системы, автоматизировать все процессы, обеспечить мас­
штабирование. Однако если среди всех этих безусловно хороших подходов не най­
дется место для бизнес-логики, то такая система будет бесполезной. Если она не
будет решать конкретную бизнес-задачу, которая поставлена заказчиком, она не
может быть использована в реальных условиях. И, как говорилось раньше, бизнесу
в общем и целом без разницы, насколько комплексной является система, важно то,
чтобы она работала и приносила ценность.
Баланс между этими крайними точками всегда зависит от проекта. Если мы стре­
мимся построить продукт максимально быстро, чтобы обогнать конкурентов, то
пара срезанных углов и неоптимальности программного кода того стоят. Но если
мы строим систему, в которой надежность поставлена выше всего, и мы уверены,
что расширение -
это не такой уж дальний вопрос, есть смысл двинуться в сторо­
ну построения более совершенных с точки зрения архитектуры сервисов.
Но
в большинстве случаев требуется как раз-таки скорость разработки. Если вы упре­
тесь в то, что не сможете развивать систему дальше из-за слишком большого коли­
чества технических проблем, вполне вероятно, что к тому моменту у вас будет дос­
таточно ресурсов, чтобы задуматься о переписывании каких-то частей проекта,
чтобы обеспечить развитие бизнеса вместе с ним.
Про архитектуру и планирование
Когда речь шла про технический долг, мы вкратце затронули то, что архитектурные
решения тоже попадают в эту категорию, поэтому стоит посмотреть на архитектуру
с точки зрения развития и планирования. И, уже традиционно, не существует опре­
деленного свода правил, которые будут описывать, как именно те или иные подхо­
ды будут влиять на ваш проект, но по крайней мере можно выделить направления,
в которых будет двигаться техническая сложность.
Когда мы только начинаем проект, конечное видение технической реализации сис­
темы может быть неясным. Да, разработка не терпит идей типа «оно само сложит­
ся», поскольку это чревато накоплением все того же технического долга и ошибок.
Однако виной тому является не только несовершенство планирования, но и так на­
зываемые вещи, которые мы не знаем, что мы их не знаем (things you don 't know
you don't know). Суть этого понятия заключается в невероятно глубокой техниче­
ской сложности современных технологий, из-за чего ни один, даже самый опытный
архитектор не может быть заранее уверен, как пойдет тот или иной новый проект.
Каждый новый старт -
это уникальная комбинация, которая ранее не встречалась
во Вселенной, даже если это очевидно несложный бэкенд-проект на десяток конеч­
ных точек. Поэтому предугадать, как поведут себя вместе те или иные технологии,
может быть сложно. Вдобавок к этому, сюда примешивается сама бизнес-логика,
которая кратно увеличивает количество потенциально неизвестных нам проблем.
Исходя из этого, единственным способом прояснить их является фактическая раз­
работка. Когда нам нужно реализовать поставленные задачи, мы фактически сами
Глава 1О. Этап эксплуатации и подготовка к ней
187
нарываемся на неизвестные нам проблемы, после чего они становятся известными,
и мы можем успешно их решить. Итеративно продвигаясь, мы не только узнаем
больше, наша команда тоже, как правило, становится опытнее, что в будущем по­
зволит уменьшить количество тех самых проблем, о которых мы еще не знаем.
Что эта информация может нам сказать об этапе эксплуатации и подготовке к не­
му? На мой взгляд, не будет момента, когда система будет идеально готова к старту
и будет полностью лишена ошибок. Да, мы можем полностью покрыть всю систему
тестами, проверить кучу сценариев, написать идеальную кодовую базу, однако по­
ка наша система не столкнется с реальным пользовательским трафиком, говорить
о ее безошибочности рано.
Поэтому большинство методологий разработки отражают это свойство программ­
ных систем. Во-первых, сама разработка плохо поддается идее, что мы можем
заранее все спланировать, выработать набор простейших шажков и инструкций,
взаимное выполнение которых приведет нас к достижению цели. Да, такое может
хорошо работать в простых ситуациях, где мы имеет четко зафиксированные биз­
нес-требования, однако в рамках 1Т такое встречается редко. Вместо того чтобы
сделать бизнес-требования неизменными, что невозможно из-за особенностей ра­
боты с заказчиком и динамически меняющегося рынка, стоит посмотреть в сторону
адаптации. Именно поэтому популярны гибкие методологии разработки. Рассказ
о них выходит за рамки этой книги, однако мы можем выделить основные особен­
ности, которые можно попробовать применять в своей работе.
Как уже говорилось ранее, разработка живет в рамках динамически изменяющегося
ландшафта, среди которого постоянно всплывают новые подходы, лучшие практи­
ки и бизнес-требования. Поэтому важно оставаться достаточно гибким, чтобы про­
дукт, который мы делаем, подходил под текущие, а в идеале и под будущие реалии.
Одним из важных принципов является открытость к обратной связи. Мы можем
получать ее от пользователей платформы, причем как в автоматическом, так и
в ручном режиме путем опросов и экспериментов. В рамках заказной разработки
главной фигурой, предоставляющей обратную связь, будет сам заказчик. В зависи­
мости
от того,
в
какую сторону сам
заказчик видит
выгодным
развивать
проект,
в такую же сторону будут меняться и сами бизнес-требования. Например, вы раз­
работали новый функционал и продемонстрировали его заказчику. После демонст­
рации заказчик выявил определенные моменты, которые нужно исправить. Важным
тут является то, что прямой вины команды разработки в этом нет, поскольку далеко
не все могут знать всю специфику предметной области, хотя такие члены команды,
как бизнес-аналитики, должны минимизировать подобные ситуации, стараясь быть
с заказчиком на одной волне. После этой обратной связи команда может исправить
текущий функционал и после его утверждения приступить к следующим задачам.
Интеграция подобных циклов обратной связи позволяет разрабатывать сервисы,
которые эффективнее решают поставленные задачи, поскольку регулярность по­
зволяет двигаться небольшими шажками. Иногда я наблюдал ситуации, когда ко­
манды разработки не показывали прогресс заказчику в течение длительного време­
ни, хотя разработка при этом продолжалась. Нередко это приводило к тому, что на
выходе разрабатывалось совсем не то, что изначально предполагалось, например
сами функции или сценарии могли быть реализованы неправильно. Вероятнее все-
Часть 11. Системная архитектура
188
го, регулярная обратная связь могла бы исправить подобную ситуацию либо, по
крайней мере, ее сгладить.
Хотя сами бизнес-требования могут быть гибкими, это не добавляет гибкости
к программному коду, а особенно к архитектуре. Поэтому возможность внесения
изменений требует определенного программного совершенства, а также небольшо­
го количества технического долга. Дпя обеспечения этого нужно задумываться
о достаточном уровне модульности, грамотном переиспользовании кода, общих
библиотеках и ограниченных контекстах.
Выбор архитектурного подхода при этом нередко рассматривается как что-то неиз­
менное. Да, раньше часто так и было, что определенный паттерн поддерживался
в рамках проекта долгое время, однако с развитием технологий все постепенно ме­
няется. Различные архитектурные подходы по-разному влияют на скорость дости­
жения задачи, а также на последующую надежность проекта. При этом на их выбор
распространяется тот же парадокс, что и на всю разработку, -
существует большое
количество вещей, о которых мы не знаем, что их не знаем. Поэтому постепенное
развитие архитектуры проекта, например добавление либо удаление сервисов, вне­
сение новых коммуникационных паттернов, продвинутая аналитика и мониторинг,
становится нормой в наши дни. Равно как и с кодом, архитекторы не ставят себе
задачу сразу угадать, как сделать правильно, вероятнее всего, никто не сможет уга­
дать это с первого раза для нового проекта. Однако со временем, с накоплением
опыта,
появляется
понимание
протекания
процессов,
потенциальных
узких
мест
предметной области, которые получится удачно перенести на знакомые техниче­
ские рельсы, чтобы обеспечить наиболее понятное и эффективное решение про­
блемы. Тут применима такая же идея с обратной связью, где после наших измене­
ний мы должны быть открыты к отзывам и новому, для того чтобы итеративно и
инкрементно решить поставленные задачи.
Поэтому важно понимать, что после выхода нашей системы в свет ничего не закан­
чивается, вероятнее всего, это только начало. Пользователи могут начать требовать
новые функции, сама система будет требовать развития для того, чтобы выдержи­
вать новую нагрузку. Поэтому момента, когда она будет полностью готова к это­
му, -
нет. Вместо слепого стремления к абстрактному совершенству есть смысл
посмотреть в сторону итеративных улучшений и циклов обратной связи. Если по­
строенная платформа уже решает поставленные задачи, это означает, что есть
смысл выводить ее в свет, чтобы получить больше обратной связи, и исправлять те
места, на которые команда разработки может и не посмотреть, т. к. они могли про­
явиться только на этапе самой эксплуатации. Важно быть открытым к изменениям
и быть достаточно гибким, чтобы их внесение не было проблемой, решение кото­
рой будет доставлять только дискомфорт команде.
Что нужно проверить перед релизом
Выпуск нашего сервиса в продакшен -
это ответственный момент, однако не су­
ществует определенного флага, который позволит понять, что мы полностью гото­
вы. Да, мы можем опираться на бизнес-требования, а именно на то, что они реали-
Глава 10. Этап эксплуатации и подготовка к ней
189
зованы, однако полноценно проверить их функционал можно только на реальном
пользовательском трафике. Для того чтобы это прошло максимально гладко, есть
смысл выделить основные пункты, которые нужно проверить перед выходом при­
ложения.
Функции
Первое, с чего стоит начать, -
это функциональное тестирование. Вероятнее всего,
ваша платформа способна решать большое количество задач, характерных для
предметной области, поэтому критически важным является проверка правильности
их функционирования. Покрывать это можно как с помощью тестов кода, так и
с помощью полномасштабного тестирования сервиса тестировщиками, которые
будут использовать пользовательский интерфейс.
Неправильное функционирование бизнес-логики в продакшен-среде может привес­
ти не только к тому, что сами пользователи будут получать неверные результаты,
но и к тому, что на платформе будут собираться неверные операционные данные.
В перспективе, если проблема была связана с небольшой частью функционала, это
может значительно усложнить откат данных, а также фильтрацию неправильно
сгенерированной информации. Чаще всего именно проблемы с данными признают
самыми сложными для решения, поскольку нам нужно не только исправить их, но
и не
повредить существующие данные на платформе.
Глубокое тестирование
функционала позволяет минимизировать подобные проблемы, поэтому важно не
пропускать этот шаг, особенно для сложных и комплексных приложений.
Если ресурсы команды ограничены, например она не может позволить себе напи­
сание полноценных интеграционных тестов, в крайнем случае нужно самостоя­
тельно эмулировать поведение пользователя для того, чтобы отследить те ошибки,
которые он может найти в рамках использования.
Если функции затрагивают определенную интеграционную логику, ее также стоит
проверить, нередко отдельно. Важно понимать, не изменились ли схемы данных на
стороне, с которой происходит интеграция, не затрагивают ли ограничения той
платформы наш сервис. Это касается не только внешних, но и внутренних интегра­
ций.
Производительность и масштабируемость
Нередко бывает такое, что в первые дни запуска того или иного приложения на­
грузка на него может значительно превышать ожидания. Поэтому перед выходом
в свет важно проверить те лимиты, которые приложение имеет в рамках текущей
конфигурации.
Ответить на этот вопрос можно с помощью нагрузочного тестирования, о чем мы
подробнее поговорим в последующих главах. Зная примерное число запросов, ко­
торые сервис может обработать без потерь в производительности, можно заранее
представлять, в какой именно момент нам нужно масштабировать приложение.
Что касается самого масштабирования, лучше заранее продумать сценарии, как
именно оно будет выполняться, поскольку в тот момент, когда приложение будет
Часть 11. Системная архитектура
190
лежать из-за нагрузки, времени на размышления может и не быть. Например, мы
можем изучить, как горизонтальное масштабирование экземпляров приложения
влияет на производительность нашей системы либо как вертикальное масштабиро­
вание может увеличить производительность нашей базы данных. Можно проводить
множество таких тестов, изучая, как изменение тех или иных конфигураций систе­
мы влияет на ее производительность.
Также есть смысл задуматься о планах долгосрочного развития в задаче обработки
большой нагрузки. Например, база данных может начать хранить в себе очень
большие объемы информации. В рамках таких сценариев мы должны понимать, как
мы будем поддерживать производительность системы на достаточном уровне с ее
ростом. Да, такой момент может настать не сразу, но его необходимо держать в го­
лове, поскольку те или иные архитектурные решения могут ухудшить гибкость
в этом направлении, чего в случае реальной необходимости масштабирования до­
пускать нельзя.
Наблюдаемость системы
На самом старте системы нам, возможно, понадобится быстро исправлять неточно­
сти и ошибки. Для того чтобы сделать это было проще, стоит задуматься о наличии
и правильной настройке инструментов мониторинга приложения. Также стоит про­
тестировать их заранее, постаравшись эмулировать настоящий пользовательский
трафик. Теме наблюдаемости будет отведена отдельная глава, поэтому давайте
опишем вкратце, почему это важно.
В случае если система не обладает достаточной наблюдаемостью, например в ней
в недостаточно удобной форме собираются логи, мы рискуем решать проблемы
практически вслепую. Часто бывает трудно повторить те или иные сценарии, кото­
рые приводят к ошибкам, и нередко единственным способом понять, почему это
происходит, являются исторические данные работы приложения, например логи и
метрики. Причем ошибки могут быть не только в коде, например приложение
банально не справляется с нагрузкой и начинает выдавать разнообразные ошибки.
Без четкого мониторинга нельзя будет определить главную причину, потому что
будет невозможно свести все крупицы информации в одну картинку быстро и эф­
фективно.
Хотя внедрение наблюдаемости, а также ее тестирование требуют определенных
ресурсов от разработчиков, в любой системе она значительно упрощает процесс
поиска и исправления неисправностей. Поэтому эти ресурсы стоит заложить в из­
начальный план разработки.
Безопасность
Поскольку ваша система с большой долей вероятности будет работать с реальными
пользовательскими данными, безопасность -
стоит задуматься. Утечки данных -
это вопрос, над которым обязательно
это точно не то, что может пожелать себе биз­
нес, поскольку в современном мире это чревато не только штрафами, но и потерей
деловой репутации.
Глава 1О. Этап эксплуатации и подготовка к ней
191
Во-первых, необходимо проверить на безопасность собственную логику приложе­
ния. Например, удостовериться, что пользователи не могут читать или видоизме­
нять данные других пользователей, если иное не подразумевается сценариями.
Важно закрыть для публичного доступа все интерфейсы, которые служат только
для удобства разработки, например такие, которые выводят метрики приложения
либо предоставляют иную информацию, которая может нарушать конфиденциаль­
ность данных. Также важно проверить все остальные бизнес-функции на предмет
правильности валидации и отсутствия лазеек и уязвимостей.
Во-вторых, важно провести аудит безопасности для используемых библиотек и
технологий. Чаще всего уязвимости в приложениях могут быть связаны именно
с популярными библиотеками или сервисами, поэтому перед их использованием
важно проверить, не находится ли текущая версия приложения в списке ненадеж­
ных и уязвимых, для чего существуют специальные реестры. Также важным явля­
ется проверка сроков поддержки библиотеки, например если срок вашей версии
истек, даже если в ней будет найдена уязвимость, она вряд ли будет исправлена
владельцем библиотеки или технологии. А для того чтобы успеть вовремя среаги­
ровать в случае атаки, нам также поможет грамотно настроенная наблюдаемость
приложения.
Заключение
Разработка программного обеспечения всегда являлась уникальной дисциплиной,
в которой переплетаются сразу множество различных областей. С одной стороны,
мы имеем бизнес со сложными и во многом уникальными бизнес-задачами, кото­
рые должны быть цифровизированы и эффективно решены. С другой стороны,
у нас имеется сложная программная логика, целый зоопарк различных технологий
и способов решить ту или иную проблему. Вместе с этим решением задач занима­
ются люди, взаимодействие которых добавляет множество уникальных красок
в палитру проекта.
Важно понимать, что далеко не все программные системы существуют по принци­
пу «выпустили и забыли». На мой взгляд, самым необычным примером является
история космического аппарата «Вояджер-1 », который был создан для того, чтобы
вылететь за пределы Солнечной системы и собрать максимальное количество по­
лезных научных данных. История заключается в том, что сам зонд смог прорабо­
тать дольше ожидаемого, и для того чтобы увеличить срок его работы, разработчи­
ки периодически отправляли на него программные обновления, которые модифи­
цировали
поведение
его
систем,
поскольку
со
временем
они
могли
начинать
работать неправильно или становиться ненужными. Иными словами, даже в тех
ситуациях, где не будут ожидаться какие-то серьезные обновления, они все равно
возможны, и в данном примере разработчики исправляли их, отправляя обновления
ПО за миллиарды километров. Любопытным являлось также то, что для этой зада­
чи потребовались специалисты, знавшие старые, «вымершие» языки программиро­
вания, которые в нашей современности не так распространены, а те немногие, кто
их знали, нередко бьши уже на пенсии.
Часть 11. Системная архитектура
192
Возвращаясь на Землю, похожую ситуацию можно наблюдать и на ней, причем
даже с большим размахом. Изменение бизнес-требований вынуждает разработчи­
ков изменять поведение системы, причем от того, насколько это было предусмот­
рено заранее, зависит простота таких обновлений. Технический долг при этом
никто не отменял, и более быстрая реализация функционала в текущем моменте
может быть причиной медленных изменений в будущем.
Поэтому самым важным в рамках эксплуатации системы является баланс между
скоростью доставки нового функционала и надежностью кода. Выбирать нужную
комбинацию этих двух признаков важно исходя из специфики проекта, но нельзя
забывать про самую главную задачу системы -
приносить пользу. Поэтому, когда
ваша система блестит с точки зрения архитектуры, в ней используются самые по­
следние технологии
и
самые
навороченные
и
надежные
коммуникационные
пат­
терны, это может ничего не значить, если она банально не используется людьми.
Поэтому всегда есть смысл начинать с чего-то простого, и сразу дать людям воз­
можность использовать это, и благодаря их обратной связи у вас будет уникальная
информация, которую вы не прочтете ни в одной документации или книге и кото­
рая позволит сделать вашу систему по-настоящему лучше.
11111
11111
ГЛАВА 11
1111!!1
Проверка изменений
Разработка программного обеспечения -
это динамический процесс, во время ко­
торого происходит изменение большого количества частей приложения. Каждая
новая функция не только расширяет список того, на что способна система, но и до­
бавляет большее количество мест, в которых что-то может пойти не по плану, что
приводит к нарушению целостности данных, ошибкам и в конечном итоге к нега­
тивному пользовательскому опыту, если никак с этим не бороться.
Человеку свойственно ошибаться, и вопрос даже не столько в том, как вообще не
ошибаться, а что делать, если ошибки произошли. К тестированию программного
обеспечения желательно сразу подходить с мыслью, что ошибки неизбежны, но
в наших силах их найти и устранить, пока они не стали большей проблемой.
Для того чтобы увеличить стабильность кода в случае инкрементных изменений,
было придумано покрытие автоматизированными тестами. Такие тесты могут про­
верять разные части приложения, начиная от отдельных функций и классов и за­
канчивая целыми сервисами, включая сценарии взаимодействия между ними. Их
запуск
может
как
происходить
локально,
так
и
выполняться
в
изолированных
окружениях, которые повторяют реальную эксплуатационную среду.
В рамках этой главы мы подробнее разберем такую важную часть, как тестирова­
ние сервисов, мы разберем основные подходы и ограничения, которые необходимо
помнить, чтобы тестирование приносило пользу, а не замедляло разработку.
Тестирование
Как говорилось ранее, сами тесты могут тестировать абсолютно разные части при­
ложения. Мне нравится одна любопытная мысль, связанная с тестами: «Успешно
работающие тесты не означают ничего кроме того, что сами тесты работают». Эта
фраза отлично передает идею, которую ни в коем случае нельзя забывать. Тесты
позволяют нам отловить какие-либо явные ошибки, однако это не будет работать
для всех ситуаций сразу. Это означает, что нельзя основываться только на самих
тестах для того, чтобы делать вывод о работоспособности приложения. Однако они
позволяют сильно уменьшить шансы того, что приложение не будет работать из-за
каких-то явных ошибок, вызванных человеческим фактором.
Часть 11. Системная архитектура
194
Разделение тестов на уровни позволяет дифференцировать детализацию и полез­
ную информацию, которую могут дать тесты. Также разные уровни помогают по­
степенно сузить ту часть приложения, которая может функционировать неправиль­
но, и уменьшить время, которое требуется для нахождения этого места. Иными
словами, если наши тесты покрывают отдельные функции и методы, то в случае их
неудачи мы будем сразу понимать, в каком месте появилась проблема. Однако если
тест покрывает целый сервис, например его API, ошибку найти труднее, хотя такой
подход может привести к результату быстрее и быть более целостным. Поэтому
сейчас мы разберем основные типы тестов, начиная с уровня отдельных модулей
кода и заканчивая сервисами.
Модульные тесты
Нередко одними из первых тестов, которые пишут разработчики в рамках проекта,
являются модульные, или, как их чаще называют, юнит-тесты. Исходя из названия,
они касаются сугубо модулей приложения, например функций, методов, библиотек,
их интерфейсов и т. д. Такие тесты, как правило, являются самыми многочислен­
ными, поскольку их написание
-
вполне адекватная по сложности задача, и моду­
лей у приложения может быть много.
Сами юнит-тесты позволяют защитить приложение от случайных ошибок, вызван­
ных изменениями функциональности, которая могла использоваться в других час­
тях проекта, но стала непригодна после данного изменения. Часто подобное может
касаться библиотек, которые крайне важно покрывать тестами, поскольку ошибки
в выполнении таких часто используемых модулей могут привести к проблемам во
множестве других частей.
Подобные тесты проверяют, выполняет ли конкретный модуль приложения свою
функцию. Например, мы можем проверить, выполняет ли данный модуль опреде­
ленное преобразование. Зная входные параметры и то, что должно получиться на
выходе, мы можем проверить, выполняет ли тестируемый модуль эту операцию
корректно или нет.
С помощью юнит-тестов можно проверять, как модули взаимодействуют с непра­
вильными значениями, которые в них передаются. Например, мы можем передать
строку в функцию сложения чисел и ожидать, что она поднимет исключение, гла­
сящее, что функция принимает только числа. Покрытие юнит-тестами будет прове­
рять эту механику на правильность ее работы.
Важным пунктом множества приложений является состояние, что также можно
проверить с помощью юнит-тестов. Например, ваш тест может выполнять создание
объектов, взаимодействие с ними, а также проверять получившиеся объекты на
предмет того,
как именно
поменялось
их состояние,
и
на основании этого делать
вывод о том, не нарушена ли логика приложения. Например, можно строить тесты
взаимодействия с базой данных -
действительно ли вызов данного метода, во-пер­
вых, закончился без ошибок и, во-вторых, привел к нужному изменению состояния
данных.
Глава 11. Проверка изменений
195
Сценарии использования юнит-тестов всецело зависят от приложения, однако су­
ществуют общие правила, которых следует придерживаться, чтобы ваши юнит­
тесты приносили пользу.
Во-первых, исходя из названия, тесты должны тестировать определенные модули и
не стараться захватить сразу все сценарии использования. Благодаря этому упро­
щается читаемость тестового покрытия, а также упрощается их поддержка и напи­
сание. Простые тестовые сценарии позволяют быстро найти модуль, в котором
произошла ошибка, и понять, почему именно это произошло.
Во-вторых, для юнит-тестов важна автоматизация. Например, сам набор тестов
должен запускаться автоматически при попытке сборки приложения. Нередко та­
кие тесты запускаются перед каждой фиксацией кода (коммитом) и не разрешают
разработчику зафиксировать изменения на удаленном репозитории, если та версия
кода, которую он пытается добавить, не проходит юнит-тесты. Все перечисленные
сценарии должны выполняться автоматически при определенных условиях (про
конвейеры интеграции кода мы поговорим с вами позже).
В общем и целом юнит-тестирование, особенно в крупных приложениях, является
важной частью. Возможно, в небольших проектах, либо в проектах, где логика про­
стая, либо в ситуациях, где требуется максимальная скорость разработки, можно
пропустить покрытие юнит-тестами. Однако в случае сложной логики и множества
модулей юнит-тестирование позволит упростить решение массы проблем и поиск
ошибок, которые, как мы уже знаем, неизбежно появятся, это всего лишь вопрос
времени.
И самое главное -
не забывать основное правило: «успешное прохождение тестов
означает только успешное прохождение тестов». Также стоит помнить, что хотя
юнит-тесты и нацелены на модули, они не всегда способны проверить комплексные
взаимодействия между модулями, в рамках которых также могут происходить
ошибки. Важно помнить это при покрытии вашего кода тестами.
Сервисные тесты
Следующим уровнем абстракции, который поддается тестированию, является сер­
висный уровень. Тестирование на уровне целого сервиса позволяет сформировать
более полную картину функционирования, поскольку подобные тесты могут вклю­
чать сразу множество внутренних модулей, проверяя не только то, что сами модули
работают правильно, но и то, что их взаимодействия также не имеют ошибок.
Чаще всего сервисные тесты направлены на тестирование внешних интерфейсов.
Это особенно удобно, если наш сервис представляет собой
API, который имеет
определенный набор внешних конечных точек, приводящих к изменению состоя­
ния сервиса либо запуску определенных сценариев работы.
Сервисные тесты, как правило, требуют запуска всего приложения.
В случае
с REST API приложение может разворачиваться в изолированном контуре, после
чего к нему может быть применен целый набор автоматизированных тестов. Такие
тесты будут проверять правильность работы конечных точек, отправлять неверные
данные и запросы, чтобы проверить, как сервис обрабатывает такие ситуации.
Часть 11. Системная архитектура
196
Ранее мы говорили, что синхронные сервисы лучше поддаются тестированию, чем
асинхронные, которые, например, читают данные из очередей либо брокеров собы­
тий. Теперь мы можем рассмотреть этот момент подробнее. В зависимости от типа
сервиса может потребоваться разная инфраструктура, чтобы написанные тесты
были приближены к реальным условиям эксплуатации. Когда речь идет о синхрон­
ных сервисах, которые, например, имеют собственную реляционную базу данных,
их локальное развертывание не представляет большого труда, формально мы
должны поднять локальный экземпляр базы данных, заполнить ее некими операци­
онными данными, имитирующими реальную платформу, после чего запустить эк­
земпляр приложения и
проверить корректность прохождения тестовых сценариев.
Это можно сделать как локально, так и, например, в конвейерах интеграции прило­
жения. Однако что если наше приложение представляет собой асинхронное прило­
жение, которое читает сообщения из очередей и брокеров? В таком случае сама
инфраструктура может усложняться, нам нужно повторять больше элементов из
нашего продакшен-развертывания для того, чтобы протестировать наше приложе­
ние на сервисном уровне. В случае с бессерверной архитектурой в ситуациях, когда
запуск одних функций порождает запуск других, локальное тестирование тоже мо­
жет быть неочевидной задачей, поскольку оркестрация подобных взаимодействий и
повторение всех нужных триггеров требует владения определенными технологиями.
Сервисные тесты позволяют быстро получить сведения о работоспособности сер­
виса,
поскольку
их,
как
правило,
меньше,
чем
юнит-тестов,
и
на их
выполнение
требуется меньше времени. Однако, в отличие от юнит-тестов, ошибки в сервисных
тестах не дают такой полной картины, что именно пошло не так. Как правило, ко­
нечные точки приложения могут затрагивать множество библиотек и модулей,
и в случае неудачного прохождения сервисного теста ошибка может быть в любой
из них. Благо с использованием логов можно достаточно быстро определить, в чем
именно была проблема, и, изучив историю изменений, ее исправить.
Сервисные тесты являются неотъемлемой частью микросервисов. Поскольку каж­
дый из микросервисов имеет свой собственный жизненный цикл, команда разра­
ботки может покрыть его подходящим набором сервисных тестов, отражающих
специфику развертывания и эксплуатации. Поскольку микросервисы могут активно
использоваться другими микросервисами, важно проверять целостность контрактов
данных, фиксируя их с помощью сервисного тестирования. Ошибки на уровне ин­
терфейсов, будь то REST API, или самих схем данных, отправляемых или прини­
маемых в очередях, могут привести к ошибкам в других частях системы, в которых
наш сервис может участвовать. Так же как и в случае с юнит-тестами, важна авто­
матизация. Запуск сервисных тестов должен происходить в рамках автоматизиро­
ванных конвейеров, в идеале с таким же автоматическим разворачиванием и свора­
чиванием тестового окружения. За счет автоматизации такие тесты будут запус­
каться
регулярно,
предоставляя
разработчикам
определенную
картину
о том,
в каком состоянии находится покрытая тестами функциональность. Частой практи­
кой является интеграция тестов на уровне репозитория. Например, каждый новый
коммит в ветке либо новый запрос на слияние будет приводить к запуску тестов.
Сама платформа тестирования будет уведомлять репозиторий об успешности либо
неуспешности прохождения тестов, визуализируя это на платформе репозитория. •
197
Глава 11. Проверка изменений
Важно не забывать, что даже в случае покрытия всех конечных точек приложения
тестами мы не можем гарантировать правильную работу приложения, поскольку
так или иначе могут остаться точки взаимодействия, которые не были покрыты.
Поэтому важно целостно смотреть на работу сервиса и не полагаться только на од­
ни сервисные тесты.
Интеграционное тестирование
Одним из важных этапов любого приложения является интеграция. И, по опыту,
именно на этапе интеграции может закрасться наибольшее количество ошибок. Это
связано с тем, что интегрирующиеся части могут быть разработаны разными
командами разработки либо в разное время, что неизбежно может привести к раз­
ному пониманию определенных концептов,
разному видению
целей и задачи.
И когда дело до ходит до кода, это может приводить к несостыковкам, влияющим
на количество ошибок во всей системе.
Как ясно из названия, интеграционное тестирование направлено на проверку инте­
грационной логики, причем как между отдельными сервисами, так и между моду­
лями. Подобный подход позволяет проверить еще больший объем системы, при
этом затрагивая самую опасную с точки зрения количества потенциальных ошибок
логику. Такие тесты могут проверять правильность прохождения распределенных
транзакций, корректность запуска сценариев в разных сервисах в ответ на опреде­
ленные действия.
Однако данный вид тестирования можно отнести к сложным с точки зрения орга­
низации. Очевидно, что для интеграции нам может потребоваться запуск сразу
нескольких сервисов либо модулей. Это не проблема, когда нам нужно запустить
всего два сервиса, однако если наша система состоит из большого числа компонен­
тов либо нам требуется разворачивание специфических дополнительных сервисов,
таких как разные брокеры и очереди, разные базы данных, то подобное тестирова­
ние будет хорошей проверкой навыков DеvОрs-команды, ответственной за автома­
тизацию подобных вещей.
Интеграционные тесты позволяют эмулировать различные сценарии, которые про­
исходят в рамках работы системы. Это хорошо, когда они прописаны в рамках кода
и протестированы по отдельности, однако для того, чтобы минимизировать ошиб­
ки, которые могут привести к несогласованности данных, что особенно критично
при распределенных транзакциях, стоит проводить совместное тестирование мно­
жества сервисов.
Данный
вид тестирования
позволяет получить высокоуровневое представление
о функционировании всей системы. Хотя ошибки в подобных тестах не позволяют
сразу определить, какая именно строчка в коде связана с этой ошибкой, они в лю­
бом случае дают достаточно наглядное представление о работе системы, проверяя
основные сценарии, которые посредством взаимодействий реализуют ту или иную
бизнес-логику.
Стоит повторно отметить то, что сам запуск и написание подобного рода тестов
требуют определенных навыков в организации и оркестрации ресурсов. Однако на
198
Часть 11. Системная архитектура
первых этапах внедрения этой практики не обязательно покрывать все сразу. Есть
смысл начать с относительно простых интеграционных сценариев, отработав при
этом подходы автоматизации данных тестов, и далее, с приобретением опыта (ко­
торый для каждой платформы будет уникальны!"), можно будет автоматизировать
все более сложные сценарии тестирования.
В общем плане юнит-, сервисные и интеграционные тесты должны существовать на
проекте в определенной синергии. Быстрые и точные юнит-тесты позволяют пра­
вильно определить поломки в модулях, тогда как интеграционные и сервисные тес­
ты имеют более комплексный вид на систему, который хотя и лишен деталей, но
вкупе позволяет поддерживать систему в стабильном состоянии, несмотря на мно­
жественные изменения со стороны команды разработки.
Нагрузочное тестирование
В отдельную категорию тестов стоит вынести нагрузочное тестирование. Наше
приложение, как правило, проектируется с учетом, что оно сможет обрабатывать
запросы, которые поступают параллельно и от множества пользователей. В свою
очередь, нагрузка, которую мы ожидаем обрабатывать, напрямую влияет на архи­
тектурные подходы, а также на количество используемых вычислительных ресур­
сов. Для более точного планирования мы должны понимать лимиты нашего прило­
жения с точки зрения производительности, поскольку, основываясь на конкретных
числах и метриках, мы сможем подобрать грамотную стратегию развертывания,
которая приведет не только к обработке всех запросов без лишней задержки, но и
будет эффективной с точки зрения облачной экономики, поскольку мы не будем
иметь простаивающих ресурсов.
Нагрузочное тестирование, как правило, распространяется на систему в целом, хотя
мы можем тестировать и отдельные сервисы. Для подобных тестов существуют
специальные фреймворки, которые эмулируют пользовательскую нагрузку с по­
мощью выполнения множества параллельных запросов к нашей системе.
Как правило, нагрузочное тестирование работает по следующему сценарию. Сам
фреймворк начинает с некоторого определенного числа параллельных запросов,
фиксируя задержку исполнения. В случае если сервис показывает стабильные
числа, нагрузочный тест начинает постепенное увеличение количества запросов,
повышая, их, например, на 5 % каждую минуту. Таким образом, фреймворк будет
повышать запросы либо до определенного потолка, заданного пользователем, либо
до момента, пока сервис не начнет возвращать ошибку. Как правило, с увеличением
количества запросов наблюдается увеличение задержки обработки. Важно учиты­
вать, что нагрузочное тестирование лучше проводить с использованием нескольких
экземпляров нагрузочного фреймворка, расположенного на разных серверах, чтобы
компенсировать забивание сетевого канала у каждого из экземпляров.
Результатом нагрузочного тестирования является график, по которому можно изу­
чить зависимость задержки сервиса от количества параллельных запросов, а также
информацию о том, после какого момента наш сервис перестал справляться с на­
грузкой. Благодаря этим данным мы можем точнее определить правила мониторин-
Глава 11. Проверка изменений
199
га для нашего сервиса и настроить автоматическое масштабирование либо изме­
нить текущую конфигурацию вручную. Важным моментом является проведение
нагрузочного тестирования в той конфигурации, которая используется для продак­
шен-окружения либо планируется для использования.
Например, мы обнаружили, что в рамках той конфигурации, что используется сей­
час, наш сервис может обрабатывать до 30 запросов в секунду, после чего появля­
ется значительная задержка, а также ошибки обработки запросов. Тогда как, по ста­
тистике, даже в моменты пиковой нагрузки к сервису обращаются не чаще 1О раз
в секунду. К слову, существует специальная величина,
RPS (requests per second),
которая позволяет оценить уровень нагрузки на сервис и то, как он с ней справля­
ется. С учетом этого, а также изучив информацию об использовании процессора и
о количестве используемой оперативной памяти и сопоставив ее со статистикой
использования сети, мы можем оптимизировать текущий вариант развертывания,
уменьшив количество активных экземпляров приложения либо уменьшив мощ­
ность базы данных. Таким образом мы можем сэкономить ресурсы, платя за мень­
шее количество облачных ресурсов, при этом наши пользователи не заметят разни­
цы, поскольку приложение даже в менее производительной конфигурации успешно
может обрабатывать пользовательские запросы.
И наоборот, предположим, что наше приложение все также может обрабатывать
только 30 запросов в секунду. И, по статистике, в моменты активного использова­
ния, например во время распродаж, приложению приходилось обрабатывать до
50 запросов в секунду, что приводило к увеличению задержки у всех пользовате­
лей. С учетом наших нагрузочных тестов мы можем изучить разные варианты кон­
фигурации, при которых могут быть задействованы разные варианты облачного
развертывания, представляющие собой разные количества одновременно запущен­
ных экземпляров приложения, а также разные варианты основного сервера базы
данных. После того как мы смогли подобрать оптимальный вариант, который, на­
пример, способен обрабатывать до 60 запросов в секунду, мы можем развернуть его
на время, в которое ожидается повышенный спрос на приложение. Таким образом,
мы сможем минимизировать задержку для пользователей, обеспечив более прият­
ный пользовательский опыт. Поэтому являющееся важным этапом нагрузочное
тестирование хотя и можно проводить не так часто, потому что, как правило, про­
изводительность
приложения
со
временем
остается
примерно
в
одних
рамках,
однако важно про него не забывать, особенно при добавлении нового функционала.
Ручное тестирование
В конечном итоге цель разработки приложения -
это интеграция в некий процесс,
в котором будут задействованы люди. Это могут быть как другие разработчики,
которые будут использовать в своих целях наш сервис, так и обычные пользовате­
ли, которые, например, будут использовать в своих целях наше приложение.
Разные тесты, направленные на разные уровни приложения, позволяют получить
картину о том, какие части приложения функционируют правильно. Однако даже
их успешное прохождение не гарантирует, что приложение будет работать пра-
Часть 1/. Системная архитектура
200
вильно на пользовательском уровне. И, как ни странно, самым действенным спосо­
бом проверки этого является ручная проверка, которая эмулирует реальные дейст­
вия, которые может выполнять пользователь в рамках платформы.
Для подобных проверок существуют отдельные специалисты, которые акцентиру­
ют свое внимание на полноценной проверке приложения, причем как в рамках
ожидаемых сценариев работы, так и вне их, например эмулируя невозможные и
неожиданные для платформы ситуации, и имя им -
QА-инженеры (от англ. quality
assurance), т. е. тестировщики.
Сама работа во время ручного тестирования может быть довольно разнообразной.
Во-первых, такое тестирование подразумевает широкое документирование процес­
са, поскольку ни один тестировщик, особенно если он новый в команде, не сможет
проверить все тестовые сценарии, основываясь на своей памяти. Также за счет до­
кументирования
и
ведения
журналов тестирования
появляется
возможность веде­
ния статистики, определяющей, как именно изменялось количество ошибок на
платформе и как быстро удавалось их исправлять.
Во-вторых, сама платформа может иметь разные способы взаимодействия с поль­
зователем. Например, если нашим продуктом является специальный REST API, ко­
манда тестирования может эмулировать поведение разработчика, используя схожие
инструменты разработки для того, чтобы определить, в каких потенциальных мес­
тах рабочего процесса могут появляться ошибки.
Для ситуаций, в которых наше приложение имеет графический интерфейс, работа
тестировщика расширяется. Команда тестирования будет проверять все уголки
приложения, нажимать все кнопки, проверять на допустимые значения все формы
ввода. Благодаря этому мы минимизируем шанс, что какие-либо из ошибок сможет
найти обычный пользователь во время своей работы, поскольку подобные ошибки
могут привести к сбоям на всей платформе, например попаданию некорректных
данных в систему, и даже к проблемам с безопасностью.
Часто важно, чтобы тестировщик не просто проходил по всем сценариям, которые
ожидаются от платформы, но и постоянно развивал идею о том, что еще может
сделать пользователь в рамках нашего сервиса, что может привести к поломке. Та­
кой взгляд на задачу обеспечения надежности решения действительно позволяет
исключить множество ошибок, вызванных человеческим фактором, поскольку, как
мы уже выяснили, тесты не могут дать полноценной гарантии того, что ошибок
в рамках работы платформы не будет. Да, шанс пропустить ошибку в какой-то сте­
пени присутствует и у QА-специалиста, поскольку он тоже человек, с таким же
человеческим фактором, однако комбинация всех подходов позволяет покрыть
большую поверхность приложения, чем концентрация на одном из подходов, плюс
каждый из этих подходов дает разную обратную связь, полезную в определенных
ситуациях.
Автоматические тесты
Все перечисленные подходы, кроме ручного тестирования, можно отнести к авто­
матическим. Так или иначе их можно запускать в рамках конвейеров интеграции
Глава 11. Проверка изменений
201
приложений. Однако существует отдельный подход, про который стоит расска­
зать, -
это эмуляция действий реального пользователя с помощью заранее опреде­
ленных действий, которые выполняет не настоящий человек, а компьютер. Речь
пойдет про AQA -
Auto QA.
Как мы уже говорили ранее, ручная проверка приложения является важной частью,
поскольку позволяет нам напрямую убедиться, что запрограммированное нами по­
ведение выполняется так, как мы ожидаем. Однако ключевая проблема заключается
в том, что для такого тестирования требуется много времени и людей. Представьте,
что вам необходимо проверить тысячу тест-сценариев, на каждый из которых при
хорошем раскладе может уйти до одной-двух минут. Соответственно, нам может
потребоваться больше
15-25 часов, чтобы проверить их все. Да, подобные тесты
можно распараллелить на несколько человек, однако такие тесты трудно выполнять
часто, например с каждым новым коммитом, поскольку это требует большого ко­
личества человеко-часов. Но выход есть.
Поскольку многие из этих тестов выполняются в рамках браузера либо другого
графического интерфейса и представляют собой некие повторяющиеся действия,
можно воспользоваться другим подходом. Существует целый класс эмуляторов,
которые могут выполнять действия в рамках графического интерфейса, как будто
их делает человек. То есть такие программы могут нажимать кнопки в браузере,
заполнять и отправлять формы, проверять граничные условия, делая это таким же
образом, как если бы это делал человек. При этом мы можем запрограммировать
определенные
условия,
которые
должны
выполняться
при
запрограммированных
действиях. Разберем подобный сценарий.
Мы являемся разработчиком автоматических тестов, и нам необходимо проверить
следующий набор действий: аутентификация в приложении, добавление нового
товара, удаление товара, изменение количества существующих товаров. Все это
можно представить как набор определенных кликов мышкой, а также ввода с кла­
виатуры, если мы предполагаем, что действие происходит через веб-браузер.
Сначала приложение выполняет аутентификацию, используя существующие дан­
ные для ввода. Также мы можем проверить поведение приложения в случае, если
мы ввели заведомо неверные данные, к примеру проверить, появилась ли форма,
уведомляющая пользователя о том, что пароль и лоrин неверны. Далее, после
аутентификации, эмулятор выполнит создание новых товаров, их удаление, прове­
ряя при этом графический интерфейс на предмет того, действительно ли эти дейст­
вия привели к изменению состояния. Можно придумать множество подобных сце­
нариев, покрывающих как позитивные варианты развития событий, так и отрица­
тельные, проверяя наше приложение на прочность.
Большой плюс таких тестов заключается в том, что их можно запускать автомати­
чески без необходимости ручного прокликивания человеком. Таким образом, мы
можем встроить автотесты в наш конвейер интеграции, обеспечивая покрытие по­
добной функциональности.
Автотесты ни в коем случае не являются заменой QА-инженерам. Напротив, они
позволяют снять с них большое количество рутинной и монотонной работы, во
202
Часть 11. Системная архитектура
время которой увеличивается шанс допустить ошибку, связанную с человеческим
фактором. Вместо этого в освободившееся время тестировщики могут делать то,
что пока программы не умеют, а именно стараться придумывать новые тестовые
сценарии и искать новые места, в которых могут быть ошибки. Например, тести­
ровщик может найти определенную часть приложения, в которой были найдены
новые ошибки. После находки мы можем покрыть эту часть автоматическими тес­
тами, и нам не придется монотонно прорабатывать ее с помощью людей -
эта
часть будет тестироваться автоматически. Да, подобные тесты также не лишены
того, что они могут пропустить потенциальные ошибки. Однако они позволяют оп­
тимизировать время людей, увеличив шанс того, что тестировщики смогут найти
что-то такое, чего не может найти автоматика, благодаря тому, что у них появилось
больше времени на изучение тех мест приложения, в которые раньше заходить
просто не успевали.
Конвейеры интеграции
Сфера применения
Одной из ключевых современных практик разработки программного обеспечения
является автоматизация рутинных процессов. Ранее множество действий и прове­
рок, связанных с выпускаемым ПО, проводились вручную, например выполнялась
ручная сборка приложения, ручной запуск тестов, перенос данных, создание окру­
жений. Всё это -
плохо масштабируемые вещи, которые ввиду активной цифрови­
зации могут стать бутылочным горлышком ее успешного прохождения.
В рамках этой главы мы уже называли такой термин, как конвейеры интеграции,
также их могут называть конвейерами непрерывной интеграции, или CI (от англ.
continuous integration). Но что это значит на практике?
В рамках циклов разработки существует определенное количество рутинных дей­
ствий, которые необходимо выполнить для того, чтобы получить работающую вер­
сию приложения, готовую к развертыванию на окружение. При этом эти действия
должны быть привязаны к определенным тригерам и технологиям. Например, мы
хотим делать некоторые проверки при каждом новом коммите в ветке, разворачи­
вая при этом специальное тестовое ПО либо тестовые экземпляры базы данных.
Все это требуется проводить множество раз при каждом изменении, поэтому по­
явилась идея оркестрации этого процесса и выделения отдельной дисциплины, ко­
торая называется непрерывная интеграция.
Сама практика активно применяется в микросервисных архитектурах ввиду того,
что каждый из микросервисов может требовать собственных условий и действий,
производимых в рамках конвейера интеграции, однако для того, чтобы обеспечить
единообразие управления, были разработаны инструменты и подходы, позволяю­
щие оркестрировать разные действия через единые интерфейсы. Непрерывной она
называется потому, что выполняется для каждого изменения в рамках кода, факти­
чески не прерываясь.
Такие конвейеры позволяют обеспечить автоматическое получение обратной связи
о разрабатываемом программном обеспечении. Чаще всего в конвейеры непрерыв-
203
Глава 11. Проверка изменений
ной интеграции встраиваются различные виды тестирования, от юнит-тестов до
автоматических
тестов,
нагрузочные
тесты,
тесты
проверки
качества
синтаксиса
кода (линтеры). Архитекторы ПО могут встраивать специальные проверки, кото­
рые проверяют, не совершают ли разработчики импорты из неразрешенных биб­
лиотек либо не нарушают ли они ограниченные контексты приложения, выполняя
импорты из других ограниченных контекстов. Формально в контейнер непрерыв­
ной интеграции можно добавить практически любые проверки, главная цель -
об­
ратная связь о качестве кода, а также раннее обнаружение аномалий, которые могут
привести к ошибкам во время реальной эксплуатации.
При этом такой подход не только повышает прозрачность кода, увеличивает его
качество, но и освобождает большое количество времени для разработчиков, кото­
рым больше не требуется вручную запускать подобные инструменты. Как уже было
сказано ранее, освободившееся время разработчики могут использовать в тех об­
ластях, которые пока не поддаются автоматизации, а именно
-
в архитектуре, ди­
зайне, самой разработке.
Организация инфраструктуры для CI
В эпоху облачных вычислений, когда развертывание множества программных ком­
плексов происходит на мощностях гиперскейлера, похожая логика наблюдается
и для организации контейнеров непрерывной интеграции. Существует несколько
основных вещей, которые должны быть обеспечены с инфраструктурной точки
зрения для того, чтобы удовлетворить потребностям конвейеров автоматизирован­
ного тестирования и проверки программного обеспечения, и сейчас мы о них пого­
ворим.
В первую очередь необходимо выбрать технологию, которая будет заниматься ор­
кестрацией конвейера. Многие гиперскейлеры, а также сервисы удаленных репози­
ториев предоставляют свои собственные решения. В таком случае облачный про­
вайдер полностью берет на себя управление инфраструктурой, позволяя разработ­
чикам определять конвейеры развертывания с помощью файлов конфигурации, как
правило, это У АМL-файлы, а также определять триггеры, при которых будут про­
исходить те или иные проверки. Выглядит это следующим образом: разработчики
добавили новое изменение в кодовую базу, что послужило триггером для запуска
конвейера непрерывной интеграции. В свою очередь, сервис управления конвейе­
рами автоматически выделил ресурсы для того, чтобы выполнить последователь­
ность действий, необходимых для проверки. В новых выделенных мощностях про­
исходит клонирование репозитория, а также выполнение действий, прописанных
в конфигурации. Например, могут запускаться тесты, проверяться качество кода,
выполняться подсчет определенных метрик. В случае если все прошло успешно,
сам конвейер в конце нотифицирует репозиторий о том, что ошибок не было обна­
ружено, что визуализируется для разработчиков с помощью элементов графическо­
го интерфейса на странице репозитория. В свою очередь, если что-то пошло не так,
например один или несколько тестов не прошли, конвейер также нотифицирует
репозиторий, однако на это действие, например, могут быть добавлены нотифика-
Часть 11. Системная архитектура
204
ции по электронной почте либо в мессенджер для того, чтобы привлечь внимание
разработчиков.
Главным плюсом такого подхода, при котором сам облачный провайдер берет
управление над СI-процессом, является также форма оплаты. Как правило, в рамках
таких сервисов оплата происходит за количество секунд фактической работы кон­
вейера. То есть, если никаких новых действий в репозитории не происходит, разра­
ботчики не платят за использование конвейеров. Многие из таких сервисов также
предоставляют бесплатные минуты работы сервиса в месяц, и, если наше приложе­
ние
не слишком крупное, существует шанс, что нам в принципе не придется
пла­
тить за работу данных конвейеров.
Также подобные платформы имеют большое количество расширений и предна­
строенных конвейеров. Например, если вы разрабатываете свой сервис на популяр­
ном языке программирования, облачные платформы могут предоставить вам готовые
для использования сканеры уязвимостей библиотек, которые вам будет достаточно
просто включить в настройках платформы без необходимости программировать их
самостоятельно. То же самое касается механизмов нотификации -
вам вряд ли
придется вручную изучать API вашего мессенджера для того, чтобы оправить со­
общение, скорее всего, подобный модуль уже присутствует в рамках платформы,
и вы можете просто вызвать его, указав нужные секреты для доступа в аккаунт.
Такие платформы отлично подходят для запуска процедур, связанных с определен­
ным сервисом. Чаще всего мы не можем развернуть сразу несколько экземпляров
приложения внутри такого конвейера, поскольку ограничены с точки зрения вы­
числительных ресурсов и свободы действий -
мы не можем выйти за рамки кон­
фигурационного скрипта. Дпя таких случаев существуют более тяжеловесные ре­
шения, которые обладают достаточной гибкостью, чтобы формально реализовать
любой из сценариев.
Дпя крупных проектов, в которых частота запуска конвейера CI, а также время его
работы превышают определенный порог, может стать более выгодным иметь по­
стоянно работающий сервер, который будет выполнять работы по сборке и тести­
рованию программного обеспечения. Подобные решения позволяют не просто
иметь один сервер, но и, например, развертывать целый кластер из рабочих узлов,
каждый из которых может использовать свои вычислительные ресурсы для выпол­
нения интеграции и сборки программного обеспечения.
Управление самим процессом со стороны разработчика при этом является довольно
похожим на то, как это работает в рамках более простых СI-решений, -
мы также
имеем определенный конфигурационный файл, который описывает шаги, которые
нам необходимо выполнить для интеграции нашего решения, например собрать
образ, запустить тесты и т. д. Однако за счет того, что тут мы можем иметь гораздо
большие вычислительные мощности, а также за счет прямого доступа мы можем
исполнять более сложные варианты тестирования.
Например, мы можем выполнять распараллеливание автоматических тестов, когда
вместо того, чтобы выполнять тест в одном экземпляре виртуального браузера, мы
можем
открыть сразу множество
виртуальных окон, ускорив прохождение теста.
205
Глава 11. Проверка изменений
Более сложные конвейеры CI также позволяют разворачивать экземпляры других
сервисов, проводить нагрузочное тестирование, комбинировать все эти шаги в лю­
бом порядке для того, чтобы обеспечить прозрачность программного обеспечения,
а также четкую обратную связь.
Да, подобные подходы могут быть труднее в реализации, поскольку управление
такой платформой -
непростая задача, а также написание таких сценариев может
потребовать определенного мастерства, однако главный плюс заключается в том,
что в случае автоматизации этого процесса, а также его стандартизации все новые
обновления будут проверяться автоматически. Кроме того, со временем и ростом
платформы благодаря тому, что опыт в управлении автоматизированными конвей­
ерами интеграции будет только увеличиваться, скорость и качество использования
подобных подходов для новых решений также не будут стоять на месте.
Хранилища артефактов
Любой из конвейеров должен иметь результат, причем это относится не только
к программному обеспечению, но и к реальной жизни. Конвейер по сборке автомо­
билей должен в своем конце выпускать автомобиль, конвейер самолетов -
самолет
и т. д. То же самое можно сказать и про программное обеспечение, однако сами
артефакты могут при этом отличаться.
Под артефактом можно понимать определенный программный продукт, который
готов к тому, чтобы быть доставленным в место его запуска либо развертывания.
Сама форма артефактов может быть максимально разной, и мы попробуем разо­
брать основные из них.
Например, если мы собираем настольное приложение, которое будет запускаться
под операционной системой Windows, мы ожидаем, что данный файл будет иметь
расширение ехе. Наши конвейеры сборки после процесса тестирования будут вы­
водить в конце этот файл. По похожей логике нашими артефактами могут быть
иные скомпилированные объекты, в зависимости от языка программирования. Мы
можем также разрабатывать свои библиотеки, и форматом нашего артефакта будет
тот формат модулей, который принят для определенной технологии.
Однако, когда дело касается бэкенд-приложений, вариантов артефактов становится
больше. Да, множество бэкенд-приложений можно скомпилировать и запускать на
общих окружениях, однако тут возникает неявная трудность. Множеству приложе­
ния требуется настроенное окружение для того, чтобы нормально функциониро­
вать, например могут требоваться специфические системные зависимости, библио­
теки, установленные пакеты веб-сервера. Без них приложение просто не запустит­
ся, и часто развертывание в таких ситуациях требует предварительного разрешения
этих требований. В эпоху облачных вычислений, когда серверы можно создавать
и удалять по клику мыши, такие накладные расходы сильно усложняют доставку
программного обеспечения, поэтому был придуман другой подход.
В рамках главы про масштабирование мы разобрали с вами такой термин, как кон­
тейнеризация.
Практически
все
современные
конвейеры
интеграции собирают
артефакт определенного типа, который впоследствии может быть запущен на лю-
206
Часть 11. Системная архитектура
бой контейнерной оркестрационной платформе без проблемы, связанной с зависи­
мостями приложения. И название этого артефакта -
container image, или образ
контейнера.
Пользуясь инструментами сборки образов, конвейеры после всех тестов собирают
определенный артефакт, который отправляется в промежуточное хранилище -
container registry, или реестр образов контейнеров. В рамках этого сервиса проис­
ходит хранение, обновление и управление образами контейнеров, при этом сам
артефакт может быть скачан из данного хранилища для последующего развертыва­
ния, причем как вручную, так и во время конвейеров развертывания. Само храни­
лище обеспечивает целостность артефакта, что минимизирует ошибки зависимо­
стей.
Иными словами, сборка контейнерного образа является популярной и надежной
практикой, которая позволяет избежать множества проблем на этапах непрерывной
доставки и развертывания, которые мы разберем в следующей главе. Помимо сбор­
ки контейнеров, артефактами могут являться собранные библиотеки, файлы фрон­
тенда, исполняемые файлы.
Стоит отметить, что подобные сервисы, позволяющие хранить образы контейнеров,
нередко предоставляются самими облачными провайдерами с уже привычной для
них моделью оплаты, например за количество занимаемой памяти и за объем тра­
фика, который понадобился для скачивания образа контейнера. Такие системы
также могут иметь встроенные сканеры уязвимостей, проверяя образ на предмет
устаревших библиотек и прочих уязвимостей.
Заключение
Во время реальной эксплуатации приложения так или иначе могут потребоваться
изменения. Это может быть связано с добавлением нового функционала, который
вписывается в новые бизнес-требования, либо с исправлением существующей ло­
гики ввиду найденных ошибок, либо с изменением видения процесса. Когда при­
ложение не изменяется, его работа может быть относительно стабильна, и чаще
всего большинство ошибок происходят именно во время изменений, поскольку не­
возможно полностью исключить вероятность наличия ошибок, пока приложение не
будет обрабатывать реальный пользовательский трафик.
Поэтому необходимо системно подходить к этой проблеме для того, чтобы добав­
ление обновлений не становилось болью как для пользователей, так и для разра­
ботчиков. В первую очередь необходимо задуматься о правильном тестировании
приложения. Во время разработки сами разработчики могут покрывать свой код
юнит-тестами, чтобы уменьшить шанс непреднамеренной поломки тех или иных
модулей либо логики их взаимодействия. В случае если юнит-тесты написаны гра­
мотно и полноценно, минимизируется шанс того, что при внесении новых функций
произошла поломка уже существующих.
Следующим уровнем является написание сервисных и интеграционных тестов.
Благодаря такому подходу мы можем закрыть большее количество межкомпонент-
207
Глава 11. Проверка изменений
ных взаимодействий, чем мы можем с помощью только одних юнит-тестов. Однако
нельзя забывать, что написание таких тестов может быть более сложной задачей,
особенно если подразумевается множественный запуск разных сервисов.
Для того чтобы протестировать приложение с точки зрения реального использова­
ния, нельзя забывать про применение ручного и автоматизированного тестирова­
ния. Пройдя задуманный пользовательский путь, мы можем убедиться, что прило­
жение работает правильно, и заметить несостыковки такого рода, которые сразу
заметит пользователь, что, скорее всего, негативно скажется
на пользовательском
опыте. При этом грамотное комбинирование автоматизации для рутинных задач и
ручного тестирования для исследования новых тестовых сценариев является зало­
гом обеспечения высокого качества приложения и эффективного использования
человеческих ресурсов.
Поверх всего этого необходима правильная оркестрация. Современные тренды на
непрерывную интеграцию возникли не просто так, поскольку они действительно
могут ускорить выход новых версий приложения без ущерба для качества. Стоит
еще раз подчеркнуть важность автоматизации, поскольку ручной подход для запус­
ка всех тестовых сценариев и проверок никак не приблизит команду к увеличению
эффективности работы. И для того чтобы это все работало, необходима определен­
ная экспертиза в управлении платформами непрерывной интеграции, что, как и всё
остальное, приходит с опытом.
ГЛАВА 12
Доставка изменений
Одним из самых ответственных моментов в разработке является этап, когда наше
приложение
попадает
в
продакшен
и становится доступно для
пользователей.
Я уверен, что многие разработчики помнят тот первый момент, когда функционал,
который они реализовывали, проходя длительное ревью более опытных коллег,
наконец-то попадал к пользователям в руки (либо этот момент был упрощенным,
когда коллег не было и вы делали свой проект самостоятельно).
Во время момента доставки изменений есть множество ситуаций, когда что-то мо­
жет пойти не так. Начиная с того, что приложение может просто не запуститься
в производственной среде, и заканчивая тем, что оно может содержать в себе не­
найденные ошибки, которые нужно будет срочно исправлять. Все эти вещи имеют
проектную специфику, иными словами, везде они имеют разное влияние, однако
знание общих понятий и техник позволит реализовать ваше развертывание макси­
мально эффективно без задержек и недоступности приложения.
Основные понятия
Доставка изменений в современных приложениях строится на определенных прин­
ципах, соблюдение которых не только позволяет разворачивать приложения авто­
матически и быстро, но и также обеспечивает обратную связь в случае, если что-то
пошло не так. Развивая идею непрерывной интеграции приложений, где такие дей­
ствия, как тестирование, проверка качества кода и поиск уязвимостей, происходят
автоматически, мы можем применить похожий подход к развертыванию прило­
жений.
Ранее, ввиду отсутствия достаточного количества технологий для управления раз­
вертываниями, само развертывание приложения происходило вручную. Например,
появлялся определенный человек, который собирал приложение и нередко по про­
токолу FTP либо SFTP заливал приложение на продакшен-сервер, после чего вы­
полнял перезагрузку приложения. Именно таким образом происходила доставка
изменений. Думаю, не стоит говорить, что если приложений много либо если у нас
множество серверов, что подобные ручные действия крайне неэффективны. К тому
же каждый из этих серверов может иметь проблемы разных зависимостей. Напри-
Глава 12. Доставка изменений
209
мер, разработчики во время процесса локального тестирования могли иметь вари­
анты пакетов, которые отличаются от тех, что находятся в данный момент на рабо­
тающих серверах. Подобные несостыковки трудно отследить, и проявляются они,
как правило, в самый ответственный момент -
во время развертывания.
Помимо явных неудобств и неэффективности, подобный подход имеет иные скры­
тые проблемы. Ввиду того что выключение старой версии приложения и включе­
ние новой спустя какое-то время приводит к недоступности приложения для обыч­
ных пользователей, сама сложность процесса косвенно влияет на частоту измене­
ний в приложении. Когда само развертывание требует таких накладных расходов,
гораздо проще собирать изменения пачками, после чего разворачивать их одним
днем. Помимо того что сами пользователи будут дольше ждать реализации нового
функционала, это сулит дополнительное количество ошибок, вызванных тем, что
одномоментно в приложении
появилось большое количество новой логики,
и
в случае непредвиденных ошибок в работе локализация проблемы может происхо­
дить медленнее.
Всё это в совокупности привело к созданию других подходов, о которых мы пого­
ворим далее.
Непрерывная доставка и развертывание
Как уже говорилось ранее, идеи непрерывной интеграции тесно связаны с идеями
непрерывной доставки и развертывания (continuous delivery, continuous deployment).
Главный тренд -
это автоматизация цикла разработки программного обеспечения,
начиная от базовых проверок кода и заканчивая развертыванием на работающие
окружения. Все это собирается в один крупный конвейер, который способен транс­
формировать любое изменение в коде репозитория в работающий сервис, убедив­
шись в том, что само приложение не имеет ошибок и способно правильно работать.
Так что же такое непрерывная доставка и развертывание?
На этапе непрерывной интеграции мы получили определенный артефакт, готовый
к запуску. Цель непрерывного развертывания -
организация вычислительной сре­
ды таким образом, чтобы ее обновление можно было выполнить автоматически
с появлением нового артефакта. Мы обсудим варианты того, как именно можно
выполнять развертывание, позже, пока просто условимся, что мы можем запустить
наше приложение автоматически, и оно станет доступным для пользователей.
Когда у разработчиков появляется удобный интерфейс для развертывания прило­
жений, это стимулирует инкрементные обновления, при которых даже небольшие
изменения в кодовой базе, например общие оптимизации, не влияющие на бизнес­
лоrику, быстрее попадают в продакшен. Это, в свою очередь, минимизирует коли­
чество ошибок, связанных с большим количеством новых функций, которые попа­
ли к пользователям единовременно.
Важной деталью идеи непрерывной доставки является единообразие. Да, сервисы
могут быть построены на разных технологиях, могут реализовывать разные паттер­
ны коммуникации, выполнять разную логику. Однако, если они имеют общий
принцип развертывания, особенно если этот принцип сохраняется одинаковым для
Часть 11. Системная архитектура
210
разработчиков, подобное единообразие позволяет увеличить стабильность выпус­
каемых сервисов, поскольку даже новые разработчики, которые ранее не работали
с сервисом, могут понимать принципы попадания сервиса на продакшен. Другим
плюсом унификации является то, что команда управления платформой с каждым
новым сервисом будет увеличивать свой опыт в развертывании и управлении, что
позитивно влияет на новые инициативы. Последнее ни в коем случае нельзя недо­
оценивать, поскольку нередко эффективность бизнеса напрямую зависит от скоро­
сти разработки программного обеспечения, и в случае если большая часть моно­
тонных и повторяющихся действий автоматизирована, разработчики смогут сфоку­
сироваться именно на самой разработке.
Разделение сред
Унификация, которая позволяет автоматизировать большую часть процессов, от­
крывает новый подход, который невозможно бьто бы поддерживать вручную.
Вспоминая пример, в котором разработчикам приходилось вручную разворачивать
изменения,
можно понять, что поддержание множества сред с разными версиями
кода является непосильной задачей, если выполнять ее вручную. Такой подход свя­
зан с большим количеством ошибок, вызванных человеческим фактором, посколь­
ку он требует большого количества почти одинаковых действий. Из-за этого у раз­
работчиков не всегда могла быть возможность достаточной проверки функций,
потому что каждое развертывание на окружение требовало бы большого объема
монотонной работы.
Непрерывная интеграция и доставка, как правило, напрямую привязаны к репози­
торию исходного кода. Одной из хороших практик ведения репозитория является
ветвление кода. Иными словами, мы можем одновременно в рамках одного репози­
тория иметь разные ветки, которые содержат в себе, например, нестабильную вер­
сию для разработки, стабильную версию для продакшена, а также что-то среднее
для тестирования новых функций. За счет автоматизации мы можем привязать
к каждой ветке свои собственные конвейеры и окружения.
Например, мы собираемся начать разработку нового функционала. Очевидно, что
не стоит сразу вводить его в продакшен, стоит начать с разработки в отдельной
ветке, которая, как правило, выделена специально под разработку (ветка dev). От
нее разработчик создает так называемую ветку feature, в которой реализуется кон­
кретная функция. После всех изменений в коде разработчик создает запрос на
слияние с веткой dev. В это время конвейеры интеграции запускаются, поскольку
создание запросов на слияние является триггером к их запуску. Код проверяется на
предмет ошибок, запускаются тесты и т. д. После этого, когда код прошел доста­
точное ревью, например его изучили старшие разработчики, дается разрешение на
слияние. После того как оно произошло, срабатывает триггер для конвейеров сбор­
ки, которые могут также включать в себя определенные тесты, и в конечном итоге
происходит сборка артефакта, который загружается в хранилище артефактов.
Далее также автоматически либо с разрешения разработчиков происходит процесс
автоматического развертывания. Артефакт скачивается из хранилища, после чего
211
Глава 12. Доставка изменений
происходит
сам
процесс
развертывания,
и
приложение
становится
доступным
на
окружении.
Подобный процесс ввиду его унификации можно расширить на множество других
окружений. Выполняя слияние веток, мы можем инкрементно добавлять новые
функции, тестируя их в средах, которые могут быть максимально похожими на наш
продакшен. После того как окружение протестировано, оно похожим образом через
запросы на слияние попадает в продакшен, запуская такие же конвейеры интегра­
ции, сборки и развертывания.
Для небольших проектов может быть достаточно и двух сред, dev и prod, однако
в крупных проектах их может быть гораздо больше, поскольку на каждой из сред
может быть развернута разная версия приложения, содержащая разный функцио­
нал, при этом далеко не каждая из функций дойдет до продакшена, поскольку на
этих окружениях могут проходить глубокие тесты. Количество окружений, которое
нужно именно вам, стоит выбирать исходя из задач проекта, но начать лучше с че­
го-то простого, чтобы выполнить первичную настройку и найти все подводные
камни, связанные со спецификой проекта, а впоследствии выполнить масштабиро­
вание на большее количество окружений.
Автоматизация через GitOps. laC
Благодаря привязке к репозиториям исходного кода возможно достаточно гибкое
управление самой средой. Такой подход называется GitOps и означает, что состоя­
ние среды должно полностью браться из репозитория. Иными словами, не должно
быть чего-то такого, что настраивается руками, все параметры, инфраструктура,
сервисы должны быть описаны и развернуты с помощью кода, который зафиксиро­
ван в системе контроля версий. Исключением является управление секретами -
они, как правило, в репозиторий не попадают из соображений безопасности.
Если в случае с исходным кодом сервисов все понятно: мы собираем артефакты,
которые могут развертываться на определенных окружениях, то как дела обстоят
с самой инфрастуктурой? Применим ли к ней GitOps-пoдxoд?
Как уже говорилось ранее, множественные окружения требуют унификации, чтобы
минимизировать количество ручного труда. Однако если речь заходит о создании
виртуальных машин, окружений, сетей, побочных сервисов, как мы можем автома­
тизировать этот процесс, не выполняя его каждый раз руками? Для этого была при­
думана крайне мощная DеvОрs-практика, которая называется инфраструктура как
код, или IaC (lnfrastructure as а Code). Сама тема IaC очень обширная, она может
быть применена к разным оркестрационным платформам и облачным провайдерам,
однако общие принципы похожи.
Точно так же как разработчики описывают логику приложения с помощью кода,
DеvОрs-инженеры описывают инфраструктуру, используя определенные языки.
К таким относятся, например,
Terraform, CloudFormation и прочие, специфичные
для конкретного облачного провайдера. Подобные конфигурационные файлы, ко­
торые нередко занимают не одну сотню строк кода, могут быть интерпретированы
технологией IaC и конвертированы в наборы АРl-вызовов к облачному провайдеру,
212
Часть //. Системная архитектура
которые приводят к созданию, изменению либо удалению облачных ресурсов.
К таким ресурсам относятся виртуальные машины, сети, вычислительные класте­
ры, базы данных, хранилища артефактов и секретов и даже целые конвейеры раз­
вертывания и интеграции.
Обязательной практикой является использование репозитория для хранения подоб­
ных конфигурационных файлов. И так же как и в случае с обычными приложения­
ми, с ними должны быть ассоциированы конвейеры интеграции и развертывания.
Во время непрерывной интеграции точно так же может выполняться проверка кода
на соответствие правилам, его ограниченное тестирование. В свою очередь, во вре­
мя доставки сам конвейер сборки будет являться точкой вызова для множества
АРI-запросов к облачному провайдеру, выполняя уточнение статуса новых ресур­
сов, а также изменение их состояния и настроек.
При этом сам DеvОрs-инженер фактически использует классические практики раз­
работки, выполняя фиксацию кода, ветвление, чтобы обеспечить стабильные вер­
сии в главной ветке, из которой происходит сборка всей инфраструктуры проекта.
За счет практик GitOps, которые касаются инфраструктуры, легко обеспечить раз­
вертывание множества окружений. Формально, чтобы создать новое окружение,
нам достаточно взять шаблон, который мы создали для нашего изначального окру­
жения,
и
реплицировать
его,
изменив
нужные
нам
значения,
например
названия
окружений. Для того чтобы это сработало, нам необходимо просто добавить новый
конфигурационный файл и применить его в конвейере развертывания. То же самое
касается и остальных приложений: если внедрять данную практику, то ваши окру­
жения будут находиться в согласованном состоянии и вы сможете изменять их
с помощью изменений в репозитории. Например, если мы захотим отменить по­
следнее изменение, нам будет достаточно выполнить отмену изменений на уровне
репозитория (например, сделав откат последнего коммита), что вернет окружение
в предыдущее состояние.
Модели развертывания
До этого мы специально не касались такой темы, как модели развертывания. Оче­
видно, что в рамках конвейера может быть множество вариантов, как развернуть
наше
приложение,
но
начиная
от
простого
«выключить-включить»
и
заканчивая
более комплексными подходами, обеспечивающими нулевое время недоступности
(zero downtime), а также более глубокий мониторинг ошибок и автоматические от­
каты.
Сине-зеленое развертывание
В чем главная проблема подхода «выключить-включить»? Одним из важных пока­
зателей приложения является его доступность. Все мы хотим, чтобы веб-сервисы
работали стабильно и постоянно. Однако, если мы устраиваем технические работы
для того, чтобы выпустить минорное обновление, которое приводит хоть к пятими­
нутной, но недоступности, это может негативно повлиять на пользовательский
Глава 12. Доставка изменений
213
опыт. К слову, по этой причине множество обновлений могут выпускать именно
ночью ввиду того, что пользовательский трафик минимален, как и количество лю­
дей, которых может задеть наше отключение. Однако представьте ситуацию, что
пользователь в ночи заполняет форму на нашей платформе, уже готов отправить ее,
но из-за того, что мы перезагружаем приложение для применения обновления, от­
правка формы заканчивается неуспешно, и сама платформа также перестает рабо­
тать. Создания таких ситуаций понапрасну стоит избегать, поэтому существуют
подходы, которые обеспечивают высокий уровень доступности приложения.
Один из самых популярных -
сине-зеленое развертываиuе. Само название (от
англ. Ыue-green deployment) было взято не просто так и отсылает к наличию не­
скольких окружений, которые постепенно замещают друг друга во время разверты­
вания. Давайте разберем на примере.
Предположим, что у нас есть один экземпляр приложения, который содержит в се­
бе старую версию нашего приложения. Мы уже собрали артефакт новой версии и
готовы выполнить его развертывание. Вместо того чтобы сразу выключать старое
приложение и после чего запускать новое, мы прямо во время работы старого па­
раллельно запускаем новое приложение. На запуск новой версии требуется опреде­
ленное время, и после его запуска частой практикой является исполнение так назы­
ваемого сигнала жизни (health check), например это может быть GЕТ-запрос на од­
ну из специальных конечных точек приложения. Успешное прохождение проверки
работоспособности означает, что приложение было развернуто без явных ошибок и
может быть использовано как основное. Платформа оркестрации обнаруживает это
и выполняет перенаправление всех новых запросов на новый экземпляр. К слову,
приложение, которое уже было запущено, принято называть синим, а новое прило­
жение
-
зеленым.
Спустя какое-то время, если новая версия приложения не упала, платформа оркест­
рации автоматически выполняет выключение старой версии, освобождая вычисли­
тельные ресурсы. Таким образом, новая версия приложения приобретает статус
«синей» и становится основной.
Если говорить простыми словами, отсутствие периода недоступности обеспечива­
ется посредством одновременного развертывания двух версий приложения, старой
и новой, с незаметным переключением одной версии на другую. За счет этого все
запросы, которые поступают в приложение, успешно обрабатываются и не сбрасы­
ваются,
поскольку
на
момент
перенаправления
наше
«зеленое»
приложение
уже
прошло нужные проверки и готово обрабатывать запросы.
Существуют различные вариации такого подхода. Например, вместо того чтобы
сразу переключать все мощности приложения, мы можем делать это постепенно.
Это обусловлено ограниченными вычислительными ресурсами нашей системы, ко­
торые не смогут одновременно держать сразу два полноценно запущенных прило­
жения, например если уже запущено несколько новых экземпляров. То есть, имея
пять экземпляров старого приложения, мы можем по очереди выключать один из
экземпляров и заменять его на новый. Такой подход называется rolling update, т. е.
«катящееся обновление», когда наше новое приложение, как колесо, постепенно
Часть 1/. Системная архитектура
214
замещает старую версию. Как говорилось ранее, такой подход крайне полезен, если
наше приложение развернуто
в ограниченных ресурсах, но
нам все равно нужно
обеспечить минимальное время недоступности приложения.
Канареечные релизы
Помимо самого факта развертывания, непрерывное развертывание должно обеспе­
чивать определенную обратную связь о том, как именно оно прошло, не наблюда­
лись ли ошибки, не уменьшилась ли производительность. Тему мониторинга и на­
блюдаемости приложения мы подробно разберем в следующей главе, а в рамках
этой мы поговорим об обеспечении, скажем так, материала для исследования, а
именно -
о развертывании приложений таким образом, чтобы эту информацию
можно было собрать.
Предыдущий пример с сине-зеленым развертыванием хотя и позволял избежать
недоступности приложения,
однако новая версия практически одновременно ста­
новилась доступной для всех. Прохождение проверки работоспособности (health
check) не является показателем отсутствия каких-то скрытых ошибок, ввиду чего
после развертывания все пользователи могут столкнуться с неправильно работаю­
щим приложением, которое будет не способно выполнить их запросы. Да, мы смо­
жем без проблем откатить приложение на предыдущую версию, однако это может
иметь
последствия
в
виде
сломанных
данных
и
негативного
пользовательского
опыта.
Нам необходим более тонкий способ управления развертываниями, в идеале с воз­
можностью развертывания приложения для небольшой части пользователей для
того, чтобы убедиться в правильности работы приложения. Именно эта идея иле­
жит в основе так называемых канареечных релизов, или canary releases.
Само название «канареечный релиз» имеет любопытные корни. В давние времена
при работах в подземных шахтах не исключалась вероятность, что сама шахта мо­
жет быть заполнена токсичным газом, из-за чего сами работники могут пострадать.
Ввиду того, что тогда не существовало точного измерительного газового оборудо­
вания, работники заносили с собой канарейку в клетке, которая постоянно щебета­
ла. Если шахта была наполнена газом, то канарейка теряла сознание, и ее пение
прекращалось, что сигнализировало о наличии газа и необходимости покинуть
шахту. Похожий принцип применяется и в развертывании программного обеспече­
ния. Кстати, за время написания этой книги ни одна канарейка не пострадала.
Вместо того чтобы пускать всех пользователей сразу в одну шахту или давать им
доступ сразу ко всему приложению, мы можем выделить небольшую группу поль­
зователей, например случайно, и перенаправлять все их запросы к новой версии
приложения. В случае если мы не будем наблюдать ошибок, то можем постепенно
увеличивать число пользователей, которым доступна новая версия, и со временем
все пользователи будут использовать последнее обновление. В случае если мы все
же столкнемся с ошибками, то сможем переключить эту маленькую группу пользо­
вателей на старую версию приложения. При этом все остальные пользователи, ко­
торые не участвовали в этом развертывании, даже не заметят разницы. Однако мы,
Глава 12. Доставка изменений
215
как разработчики, сможем получить бесценную обратную связь, что поможет нам в
исправлении ошибок приложения, после чего мы можем выполнить такой же цикл
еще раз. Канареечные релизы являются мощным инструментом, который позволяет
сохранить баланс между получением обратной связи на реальных пользовательских
данных, минимизировать количество ошибок для пользователей и поддерживать
при этом стабильность приложения.
А/В-тестирование
Ранее мы говорили о том, что новая версия приложения может содержать ошибки.
Под ними принято понимать ошибки в коде, невозможность завершения процедур
либо неправильное их прохождение. Однако существует другой вариант поведения,
который не совсем относится к категории ошибок, но тоже негативно влияет на
пользовательский опыт, -
неудобный либо непродуманный функционал.
Например, мы внесли новое изменение, которое меняет привычный для пользова­
телей порядок действий. Сама функция при этом работает без явных проблем, в ней
нет ошибок, связанных с кодом. Однако со временем мы начинаем замечать, что
количество пользователей, которые взаимодействуют с данной функцией, умень­
шилось, при этом явных ошибок в приложении нет. В чем может быть причина?
Сама разработка -
это сугубо итеративный процесс, который состоит из экспери­
ментов, хороших и плохих функций, которые либо подходят, либо не подходят
пользователям. Можно полагаться только на собственное чутье бизнеса, однако
в эпоху больших данных реальное преимущество появляется на стороне того, кто
использует мощность технологий для беспристрастного принятия решений, осно­
вываясь на фактах и статистике и тем не менее приправляя все это тем самым биз­
нес-чутьем.
Такое желание относится к зоне ответственности непрерывного развертывания и
позволяет интегрировать подобные эксперименты в продакшен-среду. А/В-тестиро­
вание является частой практикой во множестве сфер жизни, позволяя проводить
эксперименты, при которых мы можем проверить, как именно определенные изме­
нения влияют на целевые метрики, например на среднее время использования, про­
цент конверсии и т. д.
В рамках развертывания А/В-тестирование может выглядеть следующим образом.
Мы вносим новую функцию, однако мы не делаем ее доступной сразу для всех
пользователей. Вместо этого мы определяем группу пользователей, которые увидят
новый функционал, и группу, которая будет пользоваться старой функцией. Мы
делаем развертывание сразу двух версий приложения, однако делаем более хитрую
-
первая группа перенаправляется на новую версию приложения,
а вторая группа -
на старую. Реализовать это можно разными путями, например
маршрутизацию
с помощью идентификаторов пользователей. Если в случае с канареечными рели­
зами нашей целью было определить скрытые ошибки, то тут мы будем заниматься
более глубоким анализом пользователей. Например, мы будем замерять среднее
время работы на платформе, отслеживать целевые события, которые являются зна­
чимыми для нашего бизнеса. И тут есть три потенциальных варианта развития со-
216
Часть 11. Системная архитектура
бытий. Если мы не обнаружили статистической разницы между действиями поль­
зователей из разных групп, то это может означать, что функционал оказался ней­
тральным, что в принципе допускает его использование в рамках основной версии.
Если целевые метрики стали лучше, хотя и косвенно, это может говорить о том, что
новый функционал является более удобным и предпочтительным, поскольку он
привел к увеличению пользовательской вовлеченности. Это значит, что мы можем
попробовать заместить старый функционал новым, сделав его доступным для всех
пользователей. И третий вариант -
новые функции ухудшили пользовательские
метрики. Каждый из этих вариантов требует детального изучения, поскольку всегда
существует шанс неправильной интерпретации и неучтенных факторов, из-за чего
развертывание новых функций сразу для всех функций может обернуться снижени­
ем целевых метрик.
Подобная практика является достаточно мощной и сложной, и в случае глубокой
настройки такое тестирование можно проводить для каждого нового релиза. Чаще
всего А/В-тесты касаются фронтенд-части, поскольку множество пользователей
могут даже не догадываться, что за графическим интерфейсом скрывается сложная
программная система. Однако и бэкенд-часть тоже может успешно поддаваться
А/В-тестам, особенно если это касается изменений производительности системы
либо изменения бизнес-логики.
Теневое развертывание
Одной из самых любопытных техник развертывания являются так называемые
теневые развертывания, или shаdоw-релизы. По своей сути это крайне похоже на
сине-зеленое
развертывание,
однако
с
одним
ключевым
отличием:
старая
версия
приложения при этом не выключается. Представьте, что все поступающие на ваше
приложение запросы приходят на специальный шлюз. В обычное время он перена­
правляет их на ваше приложение, которое успешно обрабатывает все запросы.
Однако во время теневого развертывания этот шлюз, по сути, отправляет копию
всех запросов на теневое приложение, фактически проводя проверки на реальных
запросах пользователей, позволяя имитировать реальное использование. При этом
действия теневой системы не должны приводить к изменению реальных данных
пользователей, поскольку новая версия может содержать потенциальные ошибки,
и цель -
это как раз-таки найти их раньше, чем они смогут поломать сами данные.
В случае если приложение работает успешно, можно выполнить переключение
теневой версии на основную. На мой взгляд, подобный подход является одним из
самых интересных с точки зрения обратной связи, поскольку ни один тестовый
сценарий не позволит разработчикам эмулировать пользовательские запросы так
же точно, как сами пользовательские запросы. При этом мы не имеем риска нару­
шить реальные данные приложения и можем запускать практически любые экспе­
рименты без боязни что-то случайно сломать. Однако он ассоциирован с опреде­
ленными управленческими трудностями, о которых мы сейчас поговорим.
217
Глава 12. Доставка изменений
Автоматизация и оркестрация
Вообще, все перечисленные модели развертывания новых версий приложения тре­
буют от команды определенных навыков в управлении платформой. Ключевым
отличием от ручного развертывания является
именно автоматизация, а поскольку
автоматическое развертывание не способно самостоятельно разобраться (в отличие
от человека) с ошибками, которые могут возникнуть, требуется соблюдение опре­
деленных практик для того, чтобы процесс автоматического развертывания по ито­
гу не стал более трудоемким, чем ручной.
В первую очередь требуется грамотная оркестрация развернутых приложений. Та­
кая платформа должна уметь запускать и сворачивать приложения в максимально
удобной форме, уметь управлять их жизненными циклами, проверять их состояние,
управлять трафиком. В одной из предыдущих глав мы разобрали контейнеризацию,
которая в рамках непрерывного развертывания играет ключевую роль.
Для названных вещей само приложение должно находиться в максимально оптими­
зированной для запуска форме. Если мы говорим о простом запуске прямо поверх
операционной системы, для множества приложений могут понадобиться нужные
библиотеки либо иные программные зависимости, из-за чего мы не сможем запус­
тить это приложение на абсолютно каждом сервере. И исходя из последних трен­
дов, самым популярным артефактом является контейнерный образ, который инкап­
сулирует в себе все программные зависимости.
Благодаря использованию контейнеров мы можем изолироваться от аппаратного
уровня, фактически не задумываясь, как именно запускается наш контейнер ввиду
того, что эта логика скрывается за программной реализацией платформ управления
контейнерами. Именно эта идея легла в основу множества контейнерных оркестра­
торов, таких как Kubemetes, А WS ECS, Azure Container Apps.
Как уже говорилось ранее, оркестрационная платформа должна уметь одновремен­
но запускать разные приложения, управлять их жизненными циклами, трафиком,
заниматься базовым мониторингом их состояния. Основной трудностью в ручном
развертывании, что особенно критично для крупных приложений, является управ­
ление множественными серверами. Балансировка запущенных приложений вруч­
ную между серверами -
это неочевидная задача, которая подвержена большому
количеству ошибок и практически не позволяет прикрепить автоматическую об­
ратную связь на события в приложении. Контейнерные оркестраторы успешно
решают эту задачу, организовывая управление кластером либо кластерами из сер­
веров, позволяя разработчикам думать о другом уровне абстракции. Когда rазра­
ботчик планирует запустить свое приложение, ему не нужно вручную указывать, на
каком из серверов будет запущено это приложение, -
платформа сделает это сама.
Подобные ситуации особенно актуальны в случаях множественных окружений.
Если наше приложение запущено всего в одном экземпляре, то оно, вероятнее все­
го, не нуждается в сложной оркестрации. Однако если у нас есть три окружения
(для разработки, тестирования и продакшена), которые требуется запускать на
определенном наборе физических или облачных серверов, то для организации дос­
таточной изоляции требуется использование оркестрации.
Часть 11. Системная архитектура
218
Как правило, такие платформы покрывают базовые потребности запущенных при­
ложений. К примеру,
Kubernetes имеет встроенные механизмы сине-зеленого раз­
вертывания, которые позволяют реализовать развертывания без периода недоступ­
ности приложения, а также этот оркестратор позволяет выполнить откат разверты­
вания в случае, если что-то пошло не так. Кроме того, данная платформа позволяет
управлять распределением приложений по вычислительным мощностям, берет на
себя управление доступом в кластер, собирает логи приложений.
Однако для более сложных сценариев управления требуются специализированные
сервисы, которые, по сути, расширяют функционал контейнерного оркестратора,
умело пользуясь функциями, которые он предоставляет. Хорошей новостью явля­
ется то, что благодаря простоте запуска, а также прозрачным API существует мно­
жество хороших программных пакетов, которые легко разворачиваются и работают
стабильно.
Например, мы хотим выполнять сборку наших приложений автоматически и не­
прерывно выполнять их тестирование и развертывание. Как правило, сам контей­
нерный оркестратор не имеет таких функций, он просто управляет запущенными
приложениями. Дrlя этого мы можем воспользоваться таким сервисом, как Jenkins,
который будет заниматься мониторингом нашего репозитория и запуском конвейе­
ров интеграции. При этом мы можем выполнить его развертывание прямо в нашем
кластере, в котором у нас запущены основные приложения, для более эффективно­
го использования ресурсов.
Дrlя более глубокого управления развертываниями, например с использованием
практик GitOps, отлично подходит ArgoCD, который позволяет управлять состоя­
нием приложения через так называемые Не\m-чарты, которые хранятся в репозито­
рии исходного кода. Это отличный сервис, который позволяет выполнять непре­
рывное развертывание, а также мониторинг приложений, по сути, грамотно пользу­
ясь функциями контейнерного оркестратора Kubemetes.
Для организации более сложных экспериментов, таких как канареечные релизы или
теневые развертывания, требуются более сложные сервисы. Чаще всего они опи­
раются
на
такую
важную
часть
кластера,
как
сервисная
сеть,
которая
позволяет
управлять сетевой составляющей и разрешает имена между сервисами, позволяя
сервисам коммуницировать между собой. Управляя сервисной сетью, можно вы­
полнять управление трафиком, перенаправление запросов и изменять множество
низкоуровневых вещей, которые позволяют реализовать сложные сценарии управ­
ления развертыванием.
Описанное выше является очень краткой выжимкой того, что мы можем использо­
вать для управления нашими развертываниями в полной мере. Дrlя управления
подобными платформами действительно требуется большой опыт, а адаптация
подобных решений под нужды приложения может занять определенное время. Не­
редко бывает такое, что решения не позволяют реализовать специфические сцена­
рии развертывания,
например если
нам
необходимо обеспечить определенные
условия для А/В-тестирования. В таких случаях решения для развертывания могут
разрабатываться самой командой разработки с использованием низкоуровневых
Глава 12. Доставка изменений
219
API оркестрационной платформы. Такой подход действительно стоит потраченного
времени, если полученный результат позволит увеличить качество и улучшить
опыт разработки, что в конечном итоге может привести к более качественной реа­
лизации бизнес-функций.
Ввиду определенной сложности развертывания активно развиваются проприетар­
ные решения, которые нередко предоставляются облачными провайдерами. Сами
сервисы в облаке, как правило, не существуют независимо друг от друга и имеют
большое количество интеrраций между собой. Например, облачный провайдер
может одновременно предоставлять
нам услуги удаленного
репозитория,
сервиса
запуска конвейеров непрерывной интеграции, сервиса для оркестрации контейнер­
ных мощностей, хранения образа контейнеров и даже средства развертывания, экс­
периментов и мониторинга. Подобная готовая экосистема может быть отличным
решением, если наши нужды не выходят за рамки функционала таких платформ,
поскольку мы можем перенести трудность сопровождения на плечи провайдера, а
сэкономленные силы направить на доработку собственного приложения. Однако не
стоит забывать, что подобные сервисы не бесплатны, и стоит грамотно рассчитать,
что выйдет дешевле -
разработать самостоятельно или купить.
Если резюмировать, то первое, над чем стоит задуматься в рамках интеграции луч­
ших практик непрерывной интеграции, доставки и развертывания, -
это грамотная
автоматизация и управление. Постоянно ломающийся и не работающий конвейер
никак не приближает нас к увеличению скорости выпуска новых версий нашего
проекта. Для того чтобы это не происходило, команда платформы должна понимать
основные
ограничения
решения,
подходы
и
лучшие
практики,
которых
должны
придерживаться разработчики для получения пользы от построенных решений.
В одной из глав мы разбирали автоматическое масштабирование приложений,
и этот подход также является неотъемлемым для непрерывного развертывания и
доставки. Если наши конвейеры и оркестратор платформы будут способны само­
стоятельно управлять запущенными серверами и ресурсами в зависимости от того,
какие приложения сейчас развернуты и какой объем пользовательского трафика мы
обрабатываем, то в случае облачного развертывания мы сможем сэкономить день­
ги, а в случае развертывания на наших собственных серверах мы сэкономим вы­
числительные ресурсы, которые можно будет использовать в других целях.
Как и в случае с конвейерами непрерывной интеграции, не стоит сразу покрыть все
приложение автоматическим развертыванием. Это особенно важно, если ваше при­
ложение уже имеет какой-то механизм развертывания, который хотя и требует руч­
ного вмешательства, но может работать стабильно и хорошо. Для автоматизации
стоит выбрать небольшую часть приложения, на которой можно, скажем так, по­
тренироваться, исправить очевидные ошибки, а потом расширить свой успех на
остальную часть приложения. В случае если вы начинаете разработку с нуля и ви­
дите наличие автоматических конвейеров сильным преимуществом, есть смысл
начинать развертывание сервисов сразу с использованием современных подходов
для того, чтобы обеспечить надежную и быструю разработку и быстрее прийти
к специфике, которая будет характерна именно для вашего проекта.
Часть 11. Системная архитектура
220
Заключение
Развертывание новой версии приложения в разные времена отличалось. Сначала
это был ручной процесс, при котором сами разработчики могли вручную заходить
на сервер и выполнять процесс обновления. С развитием технологий это действие
стало поддаваться большей автоматизации. Помимо того что для развертывания
новых версий может не требоваться ручное вмешательство, сам процесс может
быть куда более интеллектуальным, не только позволяя избежать периодов недос­
тупности приложения, но и получить определенную обратную связь, которая может
помочь в раннем выявлении ошибок.
Автоматизация процесса развертывания является важной практикой в современной
разработке. Часто она может быть доступна прямо из коробки как часть платформы
оркестрации
приложений. Однако
грамотное применение этих техник требует
определенных инвестиций в вашу платформу, причем как со стороны инфраструк­
туры,
так
и
со
стороны
команды,
которая
должна
иметь
полное
представление
о том, как именно нужно применять данные техники.
Нередко происходят ситуации, при которых команды могут попробовать использо­
вать такие подходы, как сине-зеленое развертывание, канареечные релизы, однако
не вкладываются достаточно в автоматизацию этих процессов. Например, для за­
пуска развертывания все равно может потребоваться ручное вмешательство либо
сборка каких-либо артефактов. В таких ситуациях эффект от интеграции практик
развертывания может быть противоположным -
разработчики могут тратить даже
больше времени на развертывание, чем они тратили раньше. Поэтому перед тем как
начать использовать подобные подходы, необходимо определить, какие именно
части
ваших процессов могут быть оптимизированы и
что стоит реализовать
в первую очередь.
Отдельно стоит обратить внимание на команду. С каждым новым настроенным
конвейером развертывания она будет глубже понимать технологию и принципы
работы. Поэтому работа над новыми сервисами будет происходить быстрее ввиду
расширения опыта. В свою очередь, первые шаги могут быть не такими эффектив­
ными, однако это не
повод не делать
их,
-
со временем, когда команда получит
большее представление о подводных камнях той или иной технологии, применение
технологий автоматизированного развертывания станет неотъемлемой частью ва­
шей работы, освободив для разработчиков дополнительные ресурсы, которые они
смогут использовать для комплексного улучшения платформы.
11181
11181
ГЛАВА 13
11!!1
Мониторинг работы системы
Возможно, сейчас прозвучит очевидная вещь, но программные системы обычно не
уведомляют о чем-то самостоятельно. Возможность анализировать ошибки и пове­
дение сервиса -
это не что-то такое, что появляется само собой прямо из коробки.
Однако такая опция является очень важной, поскольку позволяет быстрее находить
потенциальные узкие места в приложении, а также улучшать пользовательский
опыт.
Для того чтобы приложение перестало выглядеть для разработчиков как черный
ящик, необходимо провести определенные действия.
ний -
Наблюдаемость приложе­
это свойство, которое можно развивать в рамках платформы, применяя раз­
личные подходы, каждый из которых покрывает специфические области монито­
ринга приложения.
Не существует одного подхода, который способен покрыть сразу все. При этом
в рамках приложения нередко появляются новые либо глубоко адаптируются суще­
ствующие решения, позволяя покрывать специфический функционал приложения.
Поэтому важно иметь полное представление о том, как именно применение тех или
иных подходов позволяет решить вопрос недостаточной прозрачности приложения.
Основные понятия и цель
В первую очередь стоит подробнее рассмотреть вопрос, зачем нам нужно вклады­
вать ресурсы в развитие мониторинга нашего приложения. Формально приложение
может работать корректно и без них, однако такой сценарий покрывает только один
вариант развития событий, при котором на нашей платформе не происходит ниче­
го, кроме заранее ожидаемых сценариев.
Современные программные системы, как правило, не стоят на месте, в них посто­
янно добавляются обновления, реализуются новые функции, появляются новые за­
дачи. Одно дело, когда это происходит в рамках закрытого тестового периметра,
однако конечной целью приложения являются реальные пользователи, которые
применяют
Если
приложение для
приложение
не
своих
нужд и
будет работать,
решают с
помощью
разработчикам
него свои
необходимо
задачи.
разобраться,
Часть 11. Системная архитектура
222
почему так произошло. И если при этом не имеется достаточной информации, то
разобраться, в чем именно была проблема, может быть крайне сложно.
Сам факт наличия человеческого фактора, причем как со стороны разработчиков,
так и со стороны пользователей, означает, что появление нештатных ситуаций на
платформе не просто вероятно, оно неизбежно. Если тестирование кода, разделение
окружений позволяет минимизировать количество ошибок, то важно также пони­
мать, что нужно иметь план действий на случай, если что-то нештатное все же про­
изойдет.
Другим фактором является сложность приложений. Моделируемые бизнес-про­
цессы могут быть довольно сложными, и эта сложность умножается на степень
программной реализации, которая может содержать в себе сразу несколько техно­
логий и сторон взаимодействия. В случае если каждая из этих систем имеет свои
механизмы сохранения информации, которые не соединены друг с другом, это
многократно усложняет анализ в нештатных ситуациях. Распутывание клубка логов
может занять много времени, причем это хорошо, если такой клубок хотя бы есть,
поскольку нередко сами логи приложения могут банально не сохраняться.
Мониторинг системы может быть полезен не только для анализа экстренных си­
туаций и ошибок, но и для более общих сценариев. Анализ метрик использования
и задержки позволяет делать грамотные выводы о том, в какую сторону развивать
платформу с технической точки зрения. Благодаря анализу подобных метрик появ­
ляется возможность изучать пользовательское поведение на более высоком уровне,
например узнавать среднее время загрузки сервиса, среднее время, проведенное на
нем, и т. д.
Теперь стоит поговорить о конкретике: какие именно параметры нам интересно
собирать, какую форму они имеют и в каких ситуациях могут помочь. Речь пойдет
о метриках, логах и трейсах.
Логи
Текстовая информация является одной из основных форм передачи знания. Даже
читая эту книгу, мы формально подтверждаем это. Как говорилось ранее, сами
приложения просто так не будут доносить информацию до нас и записывать что­
либо полезное, связанное с их работой, однако полностью в наших силах этому по­
способствовать.
Количество событий, которые могут произойти во время работы системы, может
быть большим, при этом они могут быть весьма разнообразными. Поэтому самым
естественным способом их сохранения является текстовый формат, который впо­
следствии поможет разобраться в той или иной ситуации.
Назначение текстовых логов, скорее всего, может быть понятно практически сразу:
ввиду того что само исполнение программы довольно сложно декомпозировать со
стороны
( особенно если оно уже скомпилировано), нам необходимо обеспечить
текстовый отклик на прохождение определенных событий в рамках приложения.
Множество сервисов, таких как базы данных, оркестраторы, шлюзы, прямо из ко-
Глава 13. Мониторинг работы системы
223
робки имеют хорошее покрытие логов, что достигается за счет упорного труда раз­
работчиков, которые занимаются поддержкой этих решений. И для того чтобы
поддерживать единообразие, нам необходимо реализовывать что-то похожее на
нашей стороне.
Важным вопросом является то, какие именно события стоит логировать. Естест­
венно, он напрямую зависит от специфики проекта, однако общие черты все-таки
есть. Первыми кандидатами на логирование можно назвать сложные и комплекс­
ные процессы, которые происходят в приложении в рамках одного запроса. Если
подобный вызов приводит к множеству действий с разными системами, необходи­
мо отображать статусы и значения, которые всплывают во время взаимодействия.
Одним из практических правил можно назвать то, что запись в логе должна содер­
жать в себе достаточно информации, чтобы быть полезной в случае поиска ошибок.
То есть не обязательно выводить все состояния переменных либо полное тело отве­
та или внутреннего вызова, если это не будет полезно при поиске потенциальных
ошибок. Естественно, не всегда можно угадать, что именно может быть полезно,
поэтому хорошей практикой является небольшая избыточность в рамках вывода, но
в пределах разумного. Для логирования также отлично подходят и обычные, про­
стые вызовы, поскольку с помощью анализа их частоты можно определить то, на­
сколько часто используется приложение либо какие из частей приложения исполь­
зуются наиболее активно.
Из сказанного становится понятно, что логи могут также отличаться по важности.
Например, нам вряд ли будет интересно постоянно читать записи об успешном
прохождении проверки работоспособности либо каких-либо других базовых опера­
циях. Для этого был придуман механизм так называемой важности логов (severity).
Пользуясь специальными библиотеками, мы можем динамически определять, какие
именно логи должно выводить наше приложение.
Например, мы занимаемся локальной разработкой, и нам важно видеть больше де­
талей о работе приложения для того, чтобы правильно реализовать запрашиваемую
функцию. Такая подробная информация действительно может включать в себя
большое количество промежуточных значений, внутренних конфигураций и т. д.
Однако в рамках реального использования приложения она может быть избыточ­
ной и просто занимать место на диске. Для этого мы можем настроить важность
логов и выводить соответствующие категории тогда, когда это необходимо.
Выделяют следующие группы:
□
DEBUG -
максимально подробная информация о состоянии процессов прило­
жения;
□
INFO -
информация общего характера, информирующая о нормально прохо­
дящих вызовах;
□
WARNING -
информация, которая не сигнализирует о прямой ошибке, однако
заостряет внимание на том, что то или иное поведение может привести к ошибке
в перспективе;
□
ERROR -
ошибка выполнения в приложении. В рамках таких событий можно
отображать как ожидаемые ошибки, например один из сервисов в рамках тран-
Часть 11. Системная архитектура
224
закции не смог ответить, так и неожиданные, например если не удалось выпол­
нить определенный блок кода;
□ CRIТICAL -
критические ошибки, на которые стоит незамедлительно обратить
внимание.
Это общие группы, которые можно использовать, однако важно понимать, что как
использование, так и интерпретация -
это тема для обсуждения в рамках проекта,
поскольку те или иные события можно трактовать по-разному, важно, чтобы разра­
ботчики сходились в одном мнении. С помощью данных групп логов можно вы­
полнять эффективный поиск и фильтрацию. Например, если мы хотим сразу перей­
ти к ошибкам, то можем анализировать только те логи, которые содержат в своем
теле это ключевое слово. Да, поскольку сами логи -
это текст, для выражения сте­
пени лога также используется текст. Например, применяя ключевые слова [INFO]
или [ERROR], мы можем упростить себе задачу последующего поиска подобных
записей с фильтрацией по категории. Многие программы для лоrирования также
позволяют
выделять такие
группы
с
помощью
цвета,
что определенно улучшает
представление и наглядность.
Многие приложения имеют хороший встроенный функционал, чтобы отображать,
почему произошла та или иная ошибка. Поэтому в рамках логов важно сохранять
вызов стека (stack trace) для того, чтобы можно было отследить среди множества
модулей, где именно произошла ошибка.
Как именно выполняется сбор логов? В этом нам может помочь сама платформа
оркестрации. Множество таких сервисов позволяют напрямую получить доступ
к логам, которые генерируются приложениями. Однако просто смотреть на них
недостаточно, для более эффективной работы необходимо развить эту идею еще
дальше.
Во-первых, сами платформы, как правило, не имеют сложных политик по хране­
нию логов, они просто позволяют получить доступ к программным интерфейсам,
через которые эти логи можно прочитать в моменте для конкретного приложения.
Для более эффективной работы необходимо иметь специальные агрегаторы логов,
которые будут способны собирать логи по всей платформе. Например, в рамках
платформы для запуска приложений может быть развернут специальный агент, ко­
торый будет слушать логи приложений, после чего выполнять их перенаправление
на специальный сервис агрегации.
Во-вторых, после их сохранения необходимо выполнить их индексацию. Посколь­
ку логи -
это текстовая информация, для хранения нередко используются базы
данных, оптимизированные для полнотекстового поиска, например
ElasticSearch.
Как мы помним из главы про операционные данные, индексация позволит нам вы­
полнять более быстрый и эффективный поиск поверх больших массивов данных,
что в случае логов может быть довольно актуально -
сами логи со временем могут
занимать действительно много места, если приложение используется активно.
За счет интеграции единого arperaтopa мы можем объединить под одной крышей
логи из разных систем, например из базы данных, разных сервисов, сервисов ауди­
та и безопасности. За счет такой унификации у нас появляется возможность делать
Глава 13. Мониторинг работы системы
225
сложные запросы, которые будут агрегировать информацию сразу из нескольких
сервисов. Представьте, что вы проводите сложную операцию, которая занимает
много времени, выполняется в фоне и затрагивает сразу несколько сервисов. Если
информация об уникальном идентификаторе операции присутствует в логах каждо­
го
из сервисов, то,
воспользовавшись
им,
мы
можем
получить данные
из
разных
систем буквально одним запросом. Для этого важно соблюдать единообразие фор­
мата логов между разными сервисами, что может быть трудно, однако это стоит
того, когда дело доходит до изучения тех или иных событий в рамках системы.
Крайне важным при анализе логов является время события. Информация о метке
времени должна быть отражена в рамках события в той или иной форме, например
как атрибут либо внутри самого тела сообщения, поскольку именно с помощью
информации о времени события можно впоследствии воспроизвести полную кар­
тину. Нередко за время события принимается время, когда лог попал в систему
агрегации,
однако
не
стоит
пользоваться
таким
подходом,
поскольку
с
момента
самого события и до его попадания в систему может пройти какое-то время, свя­
занное с задержками на стороне лог-агента либо сети.
Одним из вопросов является время, на которое мы держим наши логи в сохранен­
ном состоянии. Важно отметить, что мы можем сохранять логи нашего приложения
навсегда, однако стоит помнить, что база данных, в которой они хранятся, также
имеет свой лимит, и храня большой объем старых данных, к которым мы никогда
не обращаемся, мы можем уменьшать производительность запросов к новым дан­
ным, при этом стоимость инфраструктуры для такой БД также будет высокой.
Поэтому частым подходом является так называемый период хранения (retention
period). Мы можем автоматически удалять все логи, которые старше 30 дней, что
для большинства ситуаций является достаточным. Однако этот период напрямую
зависит от специфики проекта. Например, где-то может быть достаточно и 7 дней,
а где-то нам необходимо иметь большую историчность. Такой подход позволяет
хранить заранее ожидаемый объем данных, обеспечивая надежность системы обра­
ботки логов.
Есть и другой подход. Например, если нам важно сохранять все логи приложения
на срок до нескольких лет, но при этом
нам
не
нужно иметь возможность момен­
тально к ним обращаться, мы можем сгружать сами логи окнами в облачные объ­
ектные хранилища. Там их можно хранить в плоском либо сжатом формате, при
этом многие объектные хранилища имеют разные варианты хранения. Например,
выбрав «архивный» тариф, стоимость хранения будет невероятно низкой, однако
для получения данных может потребоваться несколько часов или даже дней. Гра­
мотное использование таких возможностей позволит как сэкономить деньги, так и
хранить все необходимые данные.
Приведенные сценарии так или иначе подразумевали вмешательство человека, ко­
гда сам человек должен был обращаться с запросами к логам для ответа на свои
вопросы. Однако такие системы можно использовать и в более автоматическом
стиле. Поскольку текстовая информация также может быть обработана с помощью
машины, например с помощью регулярных выражений либо моделей машинного
Часть 11. Системная архитектура
226
обучения, направленных на анализ текста, появляется возможность потоковой об­
работки подобных логов с целью находить аномальные события и заблаговременно
уведомлять разработчиков об ошибках. Например, подобные системы могут посто­
янно анализировать новые поступающие сообщения и искать в них специфические
слова, такие как
error, и в случае если их становится слишком много, мы можем
уведомить разработчиков о том, что с платформой происходит что-то не то. При
этом сами разработчики могут создавать любое количество подобных правил, зада­
вая свои собственные ключевые слова, которые будут специфичны сугубо для их
платформы.
Либо, возвращаясь к моделям машинного обучения, анализ логов может быть не­
много более интеллектуальным. Если наблюдать за логами стабильного приложе­
ния достаточно долго, то можно обнаружить определенные паттерны и историч­
ность, которые повторяются из раза в раз. Однако в случае если что-то идет не так,
то паттерн логов также изменится, появятся новые сообщения, которые могут хотя
и прямо не говорить об ошибках, но свидетельствовать о потенциальных пробле­
мах либо о неправомерном поведении в рамках системы, на которое стоит отреаги­
ровать. Подобный подход называется поиском аномш~ий, и современные модели
машинного обучения способны решать подобные задачи. Они могут анализировать
миллионы сообщений и искать паттерны в них, которые характерны именно для
вашего приложения, и если этот паттерн изменится, они уведомят разработчиков, а
те, в свою очередь, смогут посмотреть уже более профессиональным взглядом и
разобраться, действительно ли появилась проблема, о которой они не подозревали.
Теперь можно поговорить о самих сервисах. Приведенные примеры разбирали бо­
лее суровый вариант, когда мы полностью берем ответственность за управление
нашей инфраструктурой. Однако, как мы много раз видели ранее, множество об­
лачных провайдеров предоставляют свои собственные сервисы для сбора, обработ­
ки, хранения и анализа логов. Традиционно они имеют удобные модели оплаты, где
мы
платим за фактическое
количество логов,
за количество запросов
и т. д.
В случае если мы разворачиваем приложение в рамках облака, хорошим вариантом
является использование подобных систем, поскольку в них найдена и решена
большая часть интеграционных проблем и они работают достаточно стабильно для
использования на продакшене. Помимо облачных провайдеров, существуют от­
дельные сервисы, которые берут на себя эту часть работы, причем они также имеют
интеграции с облачными провайдерами, что упрощает первичную настройку. По­
этому благодаря наличию таких сервисов команды могут не задумываться о труд­
ностях управления такой системой и сразу получить от нее пользу.
Метрики
Помимо
текстовой
информации,
большинство
сервисов
имеют
определенные
числовые значения, которые характеризуют их работу. Эти значения принято назы­
вать метриками, и они могут охватывать большой объем проекта, описывая мно­
жество частей системы, и давать быструю обратную связь в ответ на различные со­
бытия.
Глава 13. Мониторинг работы системы
227
Метрики представляют собой такой тип данных, как временные ряды. Это значит,
что у нас есть некое значение, которое изменяется со временем, и именно в его из­
менении кроется определенный смысл, который может быть использован для ана­
лиза и интерпретации приложения. Например, мониторинг таких значений позво­
ляет отражать историчность, предсказывать дальнейшее развитие событий и пол­
ноценно изучать то, как именно наше приложение существует. Как уже говорилось,
метрики могут быть абсолютно разными, поэтому стоит начать с их классификации
и разобрать сферы применения каждой из них.
Поскольку все приложения так или иначе запускаются на серверах, как больших,
так и маленьких, множество системных метрик,
скажем так,
наследуются нашим
приложением, поскольку оно использует ресурсы системы для того, чтобы выпол­
нять запрограммированную нами логику. Крайне полезными метриками являются
уровень использования процессора, а также загрузка оперативной памяти. С по­
мощью первой можно понимать, насколько хорошо выбранный нами сервер справ­
ляется с нагрузкой. Например, если мы видим, что даже в пиковые часы утилизация
процессора не превышает 30 %, можно сделать вывод, что определенный процент
ресурсов сервера простаивает и есть смысл сменить сервер на менее мощный. Рав­
но как и наоборот -
если утилизация высока, это может говорить о недостаточно­
сти ресурсов. Что касается оперативной памяти, логика тут похожая -
во время
работы, особенно при параллельной обработке множества запросов, в оперативной
памяти могут находиться различные данные, и они также занимают место. В какой­
то момент, если места будет не хватать, приложение просто экстренно завершится.
Поэтому этот параметр крайне важно отслеживать, чтобы делать правильные выво­
ды о потреблении вычислительных ресурсов. Также к подобным метрикам можно
отнести статистику использования хранилища, например количество операций,
объем свободного места и т. д. Также можно анализировать метрики использования
сети, чтобы понимать, насколько сильно производительность нашего приложения
может упираться в сеть.
Важно понимать, что подобные метрики есть смысл собирать не только для серви­
сов. Например, они могут быть крайне важны для баз данных, поскольку нередко
именно на них ложится основная нагрузка платформы. Если в рамках нашей систе­
мы используются другие технологии, например брокеры сообщений, очереди, веб­
серверы, шлюзы, то для каждого из них важно получать информацию о том, как
именно они используют системные ресурсы, чтобы избегать ошибок, связанных
с банальной нехваткой мощности. Такие ошибки, как правило, наиболее коварны,
потому что в рамках анализа логов они могут приводить к абсолютно разным
ошибкам, которые могут даже не относиться к проблеме, и это сбивает разработчи­
ка с поиска правильного ответа.
Следующая группа метрик относится напрямую к самому приложению. Набор того,
что может быть полезно анализировать, напрямую связан со спецификой и архи­
тектурой приложения. Например, если мы строим синхронный API, используя под­
ход REST, нам может быть интересно изучить количество успешных запросов, об­
щее количество обращений, количество ошибок, время выполнения запросов. Если
мы
строим
асинхронное
приложение,
которое
полностью
опирается
на
очередь
Часть 11. Системная архитектура
228
либо брокер сообщений, важными метриками являются количество активных со­
общений в очереди, скорость их обработки, количество ошибок. Главная цель этих
метрик
-
дать
представление
о
производительности
приложения,
но
не
с точки
зрения потребляемых ресурсов, а с точки зрения опыта пользователей. Благодаря
таким метрикам мы можем оценить, насколько наше приложение хорошо и быстро
справляется с нагрузкой, что также может лечь в основу планирования инфраструк­
туры, а также самого дизайна сервиса и дизайна коммуникации.
Поскольку метрики данного класса имеют определенные общие черты, была при­
думана аббревиатура RED, которая кодирует в себе три основных свойства таких
метрик: R -
Rate, или частота, - как правило, это количество запросов в секунду;
Errors, или ошибки, - это информация о том, какой процент запросов закон­
чился неудачно; D - Duration, информация о времени исполнения запроса.
Е-
Первый параметр позволяет определить то, насколько интенсивно используется
наше приложение. Нередко данный параметр имеет цикличность и историчность,
например пользователи обычно активнее используют нашу платформу днем, чем
ночью, что можно наблюдать на колоколообразных графиках, отражающих коли­
чество запросов в секунду.
Информация об ошибках также важна, например она может быть ключевой при
развертывании новых обновлений. Если мы видим, что количество неудачных за­
просов возросло сразу после выкладывания новой версии, это может быть явным
признаком ошибки в приложении, и такие метрики позволяют максимально быстро
заметить это.
С точки зрения индивидуального пользователя время выполнения запросов напря­
мую влияет на скорость пользования приложением по
если нам необходимо ждать
1О секунд, чтобы добавить новый пост или загрузить новую страницу, такой
опыт вряд ли можно назвать хорошим. Анализ метрики продолжительности запро­
са позволяет нам, как разработчикам, посмотреть на использование приложения
с пользовательской стороны. Важным моментом в этой метрике являются ее разные
представления. Поскольку у разных пользователей может быть разная задержка,
принято не считать средние значения, а искать выбивающиеся ситуации, например
считать перцентили распределения времени ожидания. Например, обычно считают
55, 75
и 95-й перцентили, чтобы попытаться оценить, какое наибольшее либо
усредненное время требовалось на то, чтобы выполнить тот или иной запрос. Такой
подход позволяет сконцентрироваться на крайних случаях, поскольку обычно при­
ложение обрабатывает большинство запросов достаточно хорошо, и нам важно
определить, почему какие-то из них выбились из общего распределения.
Третья группа метрик связана с более усложненным анализом пользовательского
опыта. Когда мы приводили примеры задержек и RЕD-метрик, мы рассматривали
их в разрезе отдельных конечных точек и инструментов. Однако, как правило,
пользовательский опыт складывается сразу из множества таких вызовов, и для того,
чтобы иметь полную картину, а также зацепить некоторые другие детали, важно
иметь отдельный набор метрик. Например, мы можем замерять метрики первой
значимой отрисовки, замеряя время, которое проходит с момента того, как пользо­
ватель зашел на сайт, и до момента первого отображения информации. Мы можем
Глава 13. Мониторинг работы системы
229
оценивать среднее время пользовательской сессии, количество посещенных стра­
ниц, среднее количество действий пользователя. Также мы можем выполнять под­
счет того, как именно пользователь попал на нашу платформу, какой версией при­
ложения он пользуется, какими устройствами пользуется. Такие метрики позволя­
ют нам составить более
полный
портрет использования
приложения,
который
отражает комплексное взаимодействие компонентов системы в той форме, в кото­
рой они доходят до пользователя. Да, как правило, такие метрики требуют сложной
реализации, особенно если требуемая для сбора метрика уникальна и не существует
готовых решений, которые могут помочь в ее сборе, однако выводы, которые
можно делать на основе таких данных, могут окупить разработку во много раз
(рис. 13.1 ).
Рис. 13.1. Пример метрик приложения
Сбор метрик без явной цели не имел бы смысла, и настоящая красота начинается во
время их последующего анализа. Первая вещь, которую стоит учитывать, комплексный
подход.
Каждая
из метрик системы
это
описывает ее определенную
часть, однако одной метрики никогда не будет достаточно, чтобы построить вывод
о том, как работает наше приложение. Самые интересные выводы можно сделать,
изучая комбинации и корреляции метрик. Давайте разберем пример.
Мы собираем информацию об утилизации ресурсов нашего приложения, информа­
цию об утилизации базы данных, а также метрики задержки и количества запросов
к нашему приложению. Наша платформа успешно работает, однако иногда после
пиковых часов мы получаем сообщения от пользователей, что все начинает рабо­
тать очень медленно. Нам нужно разобраться, почему так происходит, поскольку
подобная ситуация может со временем появляться у большего количества пользо­
вателей, что негативно влияет на общий пользовательский опыт.
В первую очередь мы можем проверить метрики задержки и количества запросов.
Скорее всего, когда мы будем наблюдать повышение метрики запросов в секунду,
это будет также приводить к увеличению задержки, поскольку мы можем упирать-
Часть 11. Системная архитектура
230
ся в максимальный объем запросов, которые мы можем выполнить, и тогда запросы
будут выполняться конкурентно, что будет приводить к увеличению времени обра­
ботки. Предположим, что подобная ситуация случилась и у нас.
Далее мы можем изучить метрики утилизации процессора у приложения. Мы обна­
ружили, что даже в моменты, когда приложение обрабатывает пиковую нагрузку,
использование процессора возрастает незначительно. Поэтому нам нужно изучить
другую часть системы -
базу данных. И в ее метриках мы уже нашли кое-что ин­
тересное: в моменты пиковой нагрузки базе данных не хватает процессорных мощ­
ностей, а также скорости работы накопителя. Это может являться поводом для того,
чтобы изменить сервер базы данных на более мощный. После этого можно будет
вернуться к анализу метрик, чтобы понять, ушла ли проблема. Либо, если проблема
была не в типе сервера, это может быть поводом к изучению того, как происходят
запросы. Возможно, сами запросы выполняются неэффективно, например отсутст­
вуют индексы для нужных полей, и сами запросы в базу можно сделать более про­
работанными.
К слову, метрики можно нередко привязывать к текстовым логам. Например, у нас
может быть монитор, который считает среднее количество логов, которые содержат
в себе ключевые слова, например «ошибка». Впоследствии такие метрики можно
выводить наряду с традиционными, что позволяет еще больше расширить пред­
ставление о работе системы.
Исторический анализ метрик позволяет понять, как именно менялось ваше прило­
жение. Например, с выходом нового обновления, если мы не сохраняем никаких
метрик, возможно, никто и не заметит изменений в производительности. Однако
если мы сохраняем эти данные, то у нас появляется возможность практически сразу
получать обратную связь. Например, после выхода нового обновления определен­
ной библиотеки значимо возросло использование ресурсов. Если мы замечаем это
заранее, у нас есть возможность принять меры, поскольку повышенное использова­
ние может нарушить наше равновесие, что приведет к тому, что приложение не об­
работает все запросы в пиковые часы работы платформы. Анализ метрик в этом
вопросе позволяет быть беспристрастным и анализировать чистые факты, а не
субъективные ощущения.
То же самое касается и пользовательских метрик. Данные метрики являются клю­
чевыми при А/В-тестах, поэтому их сбор в этом случае обязательный. При подоб­
ного рода тестировании ни в
коем
случае нельзя делать выводы только исходя
из
своего ощущения. Как уже было сказано ранее, важно смотреть на числа и стати­
стику (рис. 13.2).
Неотъемлемой частью метрик являются автоматические нотификации пользовате­
ля. Поскольку метрики -
это числа, мы можем определять границы, в рамках ко­
торых они должны находиться. Если какая-то из метрик выходит за эти границы, то
мы можем настроить уведомления, например на электронную почту либо в мес­
сенджер. Например, если наше приложение расходует слишком много оперативной
памяти, это явно попадает под случай, когда разработчики должны знать об этом,
чтобы принять меры. Либо, если метрика доступности нашего приложения также
упала, об этом тоже можно уведомить, причем в автоматическом стиле.
Глава 13. Мониторинг работы системы
231
q
сtюоиммсе
~
....
-~-~------
Operettona metrtce under payment o..-1N1•2"°""
_,
11,.... , _ :
-
- - - - 10,~
- " - ' ,_
...11.-JL.--10,13f!lq/t;
- 017rм,/1
Рис. 13.2. Пример визуализации RЕD-метрик на платформе Jaeger
Автоматизация и метрики -
это две близкие друг к другу темы. Например, в зави­
симости от того, насколько сильно используется наше приложение в конкретный
момент времени, мы можем настроить автомасштабирование инфраструктуры, что­
бы это не сказывалось на задержке запросов. И наоборот, если запросов мало, а
серверов много, мы можем свернуть часть мощностей для экономии денежных ре­
сурсов в облаке. Самым современным подходом является анализ пользовательской
активности и поиск аномалий с помощью машинного обучения, во время которого
модель анализирует метрику и определяет, происходит ли что-то необычное либо
что-то такое, что требует определенных действий, например увеличение количества
активных
экземпляров
приложения.
Такой
подход
позволяет
адаптироваться
к изменяющимся со временем привычкам пользователей и не принуждает разра­
ботчиков к ручной подстройке под них.
Теперь поговорим об инженерной части: как именно мы можем обеспечить сбор
метрик. Если мы пользуемся облачными провайдерами, то, как правило, там уже
существует встроенный мониторинг системных метрик, которые автоматически
сохраняются в уже существующий сервис управления метриками. Множество сер­
висов, которые предоставляют балансировщики нагрузки, шлюзы API, также име­
ют достаточно широкий набор метрик, которые прямо из коробки доступны к ис­
пользованию в нашем приложении. Такие сервисы анализа метрик, как правило,
имеют встроенные средства для аналитики, нотификации и сравнения, за счет чего
мы можем настроить автоматическую обратную связь для нашего приложения. Та­
кие сервисы имеют богатый набор интеграций, благодаря чему можно выполнять
автоматическое масштабирование ресурсов, делать любые другие вызовы в рамках
облака, уведомлять пользователей.
В случае если мы выполняем развертывание на собственных мощностях, мы можем
выполнять развертывание агентов, которые позволяют выполнять сбор системных
метрик,
а также
развертывать
специальные
хранилища
и
средства
визуализации.
Нередко такие решения поддаются глубокой модификации, поэтому мы можем
адаптировать их под реалии своего проекта, получив на выходе продукт даже луч-
Часть 11. Системная архитектура
232
ше, чем есть у облачного провайдера. Однако нельзя забывать про сложности и по­
тенциальные ошибки, которые могут возникнуть при самостоятельном разверты­
вании.
Метрики являются неотъемлемой частью мониторинга приложений, они позволяют
получить достаточно наглядное представление о том, как работает система в мо­
менте, либо вернуться на определенный момент в прошлое, чтобы сравнить, как то
или иное поведение отрабатывало раньше. Поэтому их сбор и анализ являются
важным требованием для обеспечения наблюдаемости системы. И для того чтобы
получить полное представление о работе системы, мы должны рассказать о по­
следнем участнике трио
-
трассировке.
Распределенная трассировка
Текстовые логи позволяют нам изучить, что происходит в рамках конкретного при­
ложения, а также делать агрегации в рамках разных сервисов. Однако такой подход
не имеет достаточной структуры, поскольку он связан с текстом, который может
иметь тот формат, что определили разработчики конкретного сервиса (и он может
быть разным). Метрики, в свою очередь, хотя и имеют стандартную форму, тем не
менее позволяют оценить состояние системы только в общем, и, как правило, изу­
чение конкретных вызовов через них затруднено. И для того чтобы закрыть этот
пробел, были придуманы трейсы.
Современные системы могут состоять из множества частей и сервисов, которые
могут иметь довольно сложные взаимодействия между собой. Даже простые вызо­
вы к базе данных формально также являются внешним вызовом, и важно понимать,
сколько времени тот или иной запрос или транзакция находятся в рамках сервиса.
Каждый из них может иметь различную реализацию, языки программирования, что
делает интеграцию на уровне логов трудной задачей. И для того чтобы иметь воз­
можность наблюдать за каждым этапом распределенного процесса, необходима
определенная стандартизация и подход.· Поэтому был разработан особый способ,
который позволяет вывести визуализацию распределенных процессов на новый
уровень.
В первую очередь какой формат визуализации для распределенной транзакции
будет наиболее подходящим? Фактически каждый из сервисов выполняет некие
атомарные действия, которые начинаются и заканчиваются в определенное время.
Поэтому эти процессы можно представить в виде диаграммы, очень похожей на
диаграмму Ганта и формально имеющую похожую цель -
выразить прохождение
процессов во времени, отразив переходы от одного процесса к другому.
Каждый из сервисов выполняет внутри себя определенные действия, например
парсинг структур, вычисления. Эти небольшие действия принято называть интер­
валами
(span), и из них складываются сами трейсы. Интервалы содержат в себе
информацию о том, когда действие началось, когда оно закончилось, а также какие­
либо полезные атрибуты, например результаты вычислений, информацию о верси­
ях, коды ошибок, а также информацию о том, в рамках какого трейса они выпол­
нялись.
Глава 13. Мониторинг работы системы
233
Иными словами, мы представляем трейс набором интервалов, которые могут при­
надлежать разным сервисам. У нас появляется возможность отслеживать переходы
процесса между сервисами, а также все его внутренние стадии. Для того чтобы со­
хранять полезную информацию для последующего анализа, хорошей практикой
является добавление атрибутов в интервал, которые, например, отражают версии,
результаты либо иные значения, нередко такие данные добавляются в интервал ав­
томатически. В случае если какой-то из процессов закончится ошибкой, эта ин­
формация отразится в параметрах трейса, и мы сможем отследить ее впоследствии.
Сам принцип работы достаточно понятный, поэтому есть смысл разобрать его на
примере.
Предположим, у нас есть интернет-магазин, и покупка товара включает в себя сра­
зу несколько действий и запросов к разным сервисам. Для простоты мы предполо­
жим,
что
в
рамках
нашего
сервиса
присутствует своя
реализация
кошелька,
т. е.
информация о том, сколько у пользователя есть денег, уже хранится в нашей сис­
теме, и мы можем ее модифицировать.
Когда пользователь выбирает для покупки товар, в первую очередь наш сервис
должен проверить, имеется ли данный товар на складе в нужном количестве, и если
есть, то забронировать его на период оплаты. В этот же момент мы можем выпол­
нить вызов к сервису кошелька и проверить, достаточно ли средств у пользователя.
После того как мы проверим наличие товара и средств, мы можем выполнить спи­
сание денежных средств, а также оформление товара к доставке. Фактически дан­
ный путь может быть выполнен за несколько секунд и с помощью одного клика
мышкой со стороны пользователя, однако внутри системы он затрагивает сразу не­
сколько сервисов и взаимодействий, каждый из которых может пойти не по плану,
а также иметь свои собственные задержки. Если мы смотрим на это со стороны
пользователя, то все задержки суммируются, и нам, как разработчикам, без наличия
специальных инструментов может быть сложно ответить на вопрос, какое место
в рамках этого примера является узким для того, чтобы его в перспективе оптими­
зировать. Да, мы можем отражать информацию о каждом конкретном событии
в рамках логов и использовать отметки о времени, чтобы свести всю информацию
в единый формат, однако, как говорилось ранее, сами логи недостаточно структу­
рированы для такого. А метрики, хотя и могут позволить нам оценить задержки,
дают слишком общее представление, а часто для отладки нам нужно иметь более
конкретные данные (рис. 13 .3 ).
Это именно такой пример, в котором мы можем применить весь арсенал распреде­
ленной трассировки. В первую очередь мы можем настроить сам поток запросов
таким образом, чтобы все то, что касается этого процесса, относилось к одному
трейсу. Иными словами, все генерируемые сервисами интервалы будут иметь об­
щий идентификатор трейса, за счет чего мы сможем свести информацию воедино.
Ключевым
тут
является
механизм
распространения
(propagation)
информации
о том, что конкретный запрос принадлежит к определенному трейсу. Существует
множество стандартизированных способов сделать это, например в рамках НТТР­
запросов сервисы могут автоматически добавлять НТТР-заголовки, которые будут
содержать в себе информацию о родительских интервалах и идентификатор трейса.
Часть 11. Системная архитектура
234
В случае если мы общаемся с помощью асинхронных механизмов, например через
очереди, как правило, сервисы очередей и брокеры позволяют нам добавлять заго­
ловки сообщений по аналогии с НТТР. Таким образом, другие сервисы вне зависи­
мости от механизма общения могут получить нужную информацию.
v frontend: НТТР GЕТ /dispatch
°""'
hr
"1WI .....,to.ao1a,:ui-
-
пr.н-
!..,.
t
•
"
,,-::., _____
J
,..,._ ,,,..., .. __
• l"""'- "'"ЖOIIS
1-
.............
1-
-
,,1•~
__ __ ,_ _
.,-,,......
_ -_
1-
'
.
.........
,_ _
•Nllll ·- _ ...__
,Il IUI
.. 1"'°"""""' "''~"'·· 1 ~ '1':'•""'
.. 1 - ...,,,.у...,
•1 - ""'
1- "''"""'' · "'
,, 1...,_ ..,1"1' ...
--1- ,,.,-.. ...
1 - •<m'Cif•-
• lhtllW\d ,,,
-
... 1-- ~,.....,
Рис. 13.3. Пример трейса
Добавление трассировки в наш пример позволяет закрыть сразу несколько вопро­
сов. Во-первых, если во время данной транзакции произойдет ошибка, у нас будет
удобный интерфейс и информация о том, на каком именно этапе она произошла.
Благодаря наличию атрибутов данный трейс будет помечен как неудачный, и мы
сможем увидеть это с помощью инструментов мониторинга. Во-вторых, у нас по­
является возможность анализировать эффективность выполнения каждого конкрет­
ного шага как на уровне межсервисного взаимодействия, так и внутри их самих.
Благодаря тому, что мы знаем время начала и конца каждого действия, мы можем
визуализировать в виде диаграммы все интервалы и обнаруживать таким образом
узкие места в нашем процессе.
Третьим плюсом является построение так называемого графа зависимостей. По­
скольку множество бэкенд-фреймворков поддерживают автоматическое инстру­
ментирование, за счет чего они будут поддерживать запись и отправку трейсов, мы
можем увидеть то, как именно наши сервисы общаются друг с другом. То есть те­
перь каждый запрос, который происходит между сервисами, а также целые цепочки
запросов могут быть записаны и визуализированы, что позволяет нам понимать,
какие именно части системы используются,
что именно может сломаться, напри­
мер, если выйдет из строя определенная часть системы. Такие графы зависимостей
можно строить и вручную, основываясь на том, как именно логика работы была
определена разработчиками, однако такой автоматический подход позволяет гораз-
Глава 13. Мониторинг работы системы
235
до быстрее обновлять информацию такого рода, показывая разработчикам текущее
состояние дел. Это может быть особенно полезно на проектах, в которых количест­
во сервисов может достигать более десятка и в которых разработчики имеют опре­
деленную автономность и для решения своих задач могут запрашивать информа­
цию из других сервисов, например как в рамках микросервисной архитектуры.
Стоит также отметить, что множество других сервисов, таких как базы данных,
шлюзы, прокси, балансировщики нагрузки, хранилища, брокеры и очереди, также
имеют поддержку распределенной трассировки, что позволяет вам интегрировать
ваши приложения в эту экосистему наиболее эффективно. За счет того, что разра­
ботчики этих сервисов добавили эту поддержку, у вас появляется возможность на­
блюдать не только за процессами в рамках вашего приложения, но и анализировать
события в других сервисах, которые вы сами напрямую не разрабатывали.
В высоконагруженной системе количество сохраненных трейсов может быть впе­
чатляющим. И очевидно, что разработчики не смогут вручную просмотреть их всех
и извлечь из них какую-либо обобщающую информацию. Дгtя этого существует
другой, не менее элегантный подход. Поскольку каждый из интервалов хранит
в себе информацию о продолжительности, а также уникальный идентификатор, ко­
торый позволяет отличать его в рамках разных трейсов, мы можем выполнить рас­
чет метрик сразу для множества интервалов. Например, мы можем посчитать уже
известные нам RЕD-метрики для того, чтобы понимать, как часто исполняется та
или иная часть системы, сколько ошибок в ней, а главное, сколько времени затра­
чивается на выполнение. Такие метрики могут считаться автоматически и выво­
диться вместе с остальными метриками приложения, позволяя нам не просто смот­
реть на задержку цельного вызова приложения, но и смотреть на метрики состав­
ных частей. Такая информация невероятно полезна во время раундов оптимизации
сервисов, поскольку одно дело понимать, что проблема скрывается в рамках серви­
са, а другое -
локализовать ту часть, в которой проблема наиболее выражена. На­
пример, с помощью такого подхода можно прийти к определенным функциям
в приложении, которые начинают медленно работать в моменты повышенной на­
грузки. Либо наоборот, если мы видим, что сами сервисы отрабатывают достаточно
быстро, но на отправку запросов между сервисами уходит много времени, мы мо­
жем сделать вывод, что проблема связана с самой сетью либо с объемом данных,
который передается по сети.
Подобные метрики отдельных интервалов и трейсов может быть особенно полезно
визуализировать с системными метриками, например изучать, как нагрузка на про­
цессор влияет на скорость прохождения трейса либо как количество активных
пользователей изменяют задержку между сервисами. Данная информация ложится
в основу последующих улучшений и оптимизаций платформы.
Как уже говорилось ранее, распределенная трассировка является полезным средст­
вом для анализа и поиска ошибок. Данную информацию также можно представить
в виде метрик, которые могут выводиться для разработчиков в удобном формате.
Если количество ошибок резко увеличилось, у разработчиков появляется возмож­
ность изучить конкретные трейсы, которые закончились неудачно, и, пользуясь
атрибутами интервалов, сделать выводы о том, что именно могло привести к ошиб­
кам, а также в каком именно месте.
Часть 11. Системная архитектура
236
Инженерная часть для внедрения распределенной трассировки в сравнении с лога­
ми и метриками является более сложной. Однако развитие специальных сервисов, а
также технологий с открытым исходным кодом с каждым годом упрощают этот
процесс, предоставляя при этом более высокий уровень стандартизации и функций.
Сбор самих интервалов может происходить разными путями. Нередко вместе с са­
мим приложением разворачивается специальный агент, который выступает неким
коллектором информации о трассировке. Приложение при этом может связываться
с этим агентом по разным протоколам, начиная от специальных и бинарных, на­
пример используя НТТР или gRPC. Данные агрегаторы отправляют информацию
об интервалах на специальные коллекторы, которые сохраняют их в специальные
базы данных, оптимизированные для поиска среди большого количества докумен­
тов. Помимо сохранения, такие сервисы также способны рассчитывать оконные
метрики, например информацию о средней задержке, количестве запросов и про­
центе ошибок.
Далее сервисы, которые управляют сохраненными трейсами, позволяют разработ­
чикам выполнять запросы для того, чтобы изучить прохождение того или иного
процесса. Поскольку информация о трейсе может приходить неодновременно, на­
пример разные сервисы могут отправлять интервалы с задержкой либо пакетами,
сервисы трассировки также ответственны за сборку и восстановление последова­
тельности для того, чтобы показать процесс именно так, как он проходил на самом
деле. На основе этой информации впоследствии строятся графы зависимостей меж­
ду сервисами.
Как и в случае с другими параметрами для мониторинга, облачные провайдеры
предоставляют готовые решения, которые могут использоваться в вашем приложе­
нии. Такие решения имеют совместимость с открытыми стандартами трассировки,
благодаря чему, если вы уже использовали другие решения, интеграция должна
пройти безболезненно. Такие сервисы берут на себя хранение, расчет метрик, мо­
ниторинг данных. И, как во множестве других сценариев, в которых используются
облачные сервисы, у разработчиков появляется возможность использовать много­
численные интеграции -
от сервисов автоматического масштабирования до встро­
енной аналитики.
Стоит также затронуть тему глубокого анализа данных распределенной трассиров­
ки. Поскольку для крупной системы объем самих данных может быть довольно
большим, ввиду большой нагрузки ручной анализ либо даже анализ с помощью
метрик может быть затруднен. В самом мягком раскладе это могут быть сотни мет­
рик, и каждая из них важна по-своему, поэтому мы не можем просто выбросить
информацию об этом. На помощь тут может прийти умная аналитика, а именно
аналитика на основе машинного обучения. Как и в примере с анализом метрик, МL­
модели
могут принимать
историю всех запросов в рамках системы
за определен­
ный период, закодированную в понятной для модели форме. После этого модель
может автоматически изучать новые запросы
и анализировать их на предмет ано­
малий. Например, если какая-то из метрик либо какая-то часть трейса начинает
вести себя медленно или приводит к большему, чем обычно, количеству ошибок,
такая система может автоматически уведомить разработчиков об этом. Такой под-
Глава 13. Мониторинг работы системы
237
ход позволяет динамически получать обратную связь о работе платформы, при
этом не привязываясь к каким-то явным границам поведения
-
эти границы опре­
деляются автоматически и нередко нелинейно, с помощью МL-алгоритмов.
Резюмируя: использование распределенной трассировки позволяет вывести мони­
торинг работы системы на новый уровень. Как уже говорилось ранее, сама систе­
ма -
это не сумма ее частей, а произведение их взаимодействий, и именно на
мониторинг взаимодействий направлен данный подход. За счет него мы можем
держать руку на пульсе для каждого из маленьких шажков в приложении, которые
между собой делают возможным прохождение сложных распределенных процессов
и транзакций. И для того чтобы получить полную пользу от трассировки, важно
использовать все механизмы вместе.
Соединяем все вместе
На вопрос, что именно нужно выбрать: логи, метрики или трассировку, нельзя от­
ветить однозначно. В рамках разработки программного обеспечения практически
никогда не бывает «серебряных пуль», которые способны сразу решить все про­
блемы, при этом не принеся на проект определенный набор компромиссов, с кото­
рым придется мириться. Вместо этого мы можем адаптировать лучшие практики из
каждого из подходов под конкретно нашу систему. И сейчас мы попробуем опи­
сать,
как
именно
совместное
использование
всех
трех
инструментов
позволяет
обеспечить наблюдаемость нашего приложения.
В первую очередь важно обеспечить минимальное логирование. Приложение мо­
жет банально не запускаться в продакшен-среде, и нам важно понимать, почему это
происходит. Для этого можно воспользоваться встроенными средствами платфор­
мы оркестрации, поскольку они способны хранить логи определенное время, что
может быть достаточно для базовой проверки ошибок. Однако в перспективе дан­
ную систему стоит расширить, поскольку встроенные механизмы не обеспечивают
эффективный поиск, индексацию и агрегацию, которая нам может пригодиться
в будущем.
Вторым этапом будет настройка метрик. Нельзя забывать про системные метрики,
поскольку именно благодаря им мы сможем примерно понимать, насколько эффек­
тивно наше приложение использует ресурсы сервера, и обнаружить нужный мо­
мент, когда этих ресурсов может не хватать. Также с помощью метрик приложения
мы можем оценивать фактическую производительность приложения, выраженную
в частоте запросов, проценте ошибок и задержке. Сами метрики также могут рас­
считываться для трейсов, что позволяет получить нам более полный обзор произ­
водительности разных частей системы, причем как на уровне композиции сервисов,
так и на уровне отдельных функций и вызовов.
Предположим, что мы получили уведомление о том, что наше приложение имеет
повышенный процент ошибок. Какими будут наши действия? В первую очередь
нужно изучить, как давно и с чего это началось. Например, причиной для ошибок
может быть новая версия приложения либо более активное использование новой
функциональности. Для того чтобы определить, в каких именно местах произошла
Часть 11. Системная архитектура
238
ошибка, мы можем обратиться к данным трассировки. Нередко сами трейсы имеют
в себе ссылки на записи логов, которые приложение записывало в момент выпол­
нения той или иной транзакции. Если на проекте не используется трассировка, что
вполне допустимо, мы можем воспользоваться нашим хранилищем логов, отфильт­
ровать записи и выполнить поиск по ключевым словам или по идентификаторам
операций, которые закончились неуспешно. Получив эту информацию, мы можем
отсеять
95 % всех логов, которые не относятся к текущей проблеме, и изучать
только то, что приводит к ошибкам. Посредством анализа других трейсов, а также
других записей, например информации о стеке вызовов в логах, мы можем точнее
определить локализацию ошибки. В этом процессе важно сверяться с репозиторием
исходного кода и ответить на вопрос, когда именно и при каких обстоятельствах
изменялся тот функционал, который не работает.
Причины, по которым наше приложение может сталкиваться с ошибками, на самом
деле могут быть абсолютно разными, однако вне зависимости от их природы пра­
вильно настроенный мониторинг приложения позволяет легче разобраться в при­
чинах. Например, если наше приложение банально падает из-за нагрузки, мы смо­
жем наблюдать это посредствам метрик и трейсов, которые перед моментом паде­
ния будут показывать повышенную нагрузку, которую не смогло обработать наше
приложение. Метрики при этом будут показывать это на общем уровне, а впослед­
ствии можно будет определить конкретные места приложения, которые не позво­
ляют пропускать большое количество запросов. Если ошибки в приложении связа­
ны с обновлениями, то за счет грамотного лоrирования мы не только сможем найти
место, где произошла ошибка, но и сделать это своевременно, если наше приложе­
ние имеет мониторы, которые следят за логами на предмет сообщений об ошибках.
Если наше приложение даже в моменты обычной нагрузки может испытывать
трудности с обработкой запросов, то с помощью мониторинга можно определить,
связано это с проблемами с сетью или с неправильной конфигурацией базы дан­
ных, а также дизайном приложения.
Мониторинг особенно полезен в случаях, когда наш проект имеет развитие. На­
пример, мы вносим новые функции, изменяем поведение модулей, меняем архитек­
туру. И ответить на вопрос, как именно это повлияло на эффективность приложе­
ния, можно с помощью уже известных нам инструментов мониторинга. Например,
если после нового обновления мы заметили, что задержка в среднем стала выше, то
это может быть поводом к более глубокой инспекции приложения в поисках при­
чины этого. И, как говорилось ранее, если мы активно интегрируем подходы ма­
шинного обучения для анализа работы приложения, подобные изменения могут
находиться автоматически. Однако в любом случае нельзя забывать самостоятель­
но изучать то, как менялись метрики со временем, и определять границы, в которых
они должны находиться, для того, чтобы обеспечивать хороший пользовательский
опыт.
Еще раз хочу обратить внимание на момент, что нельзя решить все задачи одним
способом. Каждый проект обладает своими собственными, уникальными трудно­
стями, которыми можно управлять посредством разных подходов и технологий.
Если иметь широкое представление о том, как эти инструменты справляются со
Глава 13. Мониторинг работы системы
239
своими задачами, можно подобрать оптимальную комбинацию, которая будет не
слишком сложна в обслуживании, но при этом приносить постоянную пользу, по­
зволяя разработчикам видеть систему глубже и подробнее.
SLA и SLO
В рамках этой главы мы обсуждали, как именно можем расширить наблюдаемость
нашей системы. Эти подходы позволя!()Т нам быть более подготовленными на слу­
чай ситуаций, когда что-то идет не так, и как мы уже знаем, такие моменты в рам­
ках программной системы неизбежны. Однако важно понимать, что наше приложе­
ние
используют пользователи,
которые имеют определенное ожидание о том,
на­
сколько быстро будет работать наш сервис, какова вероятность того, что он может
быть недоступен, и т. д. Для того чтобы формализовать это, были придуманы спе­
циальные метрики и подходы, которые позволяют выразить подобные представле­
ния в численном формате.
Начнем с немного общего представления. Когда мы строим систему, мы предос­
тавляем пользователям определенные функции. Например, пользователи могут до­
бавлять товары, переписываться, запрашивать текущее состояние процессов и т. д.
Помимо самих функций, также можно выделить некие нефункциональные метрики,
например скорость работы, доступность, максимальное количество запросов и про­
пускная способность. В этот список также могут входить другие определенные
услуги, которые платформа может предоставлять, например время ответа техниче­
ской поддержки.
Собирая все вместе, сервис формирует так называемое соглашение об уровне об­
служивания, или
SLA (service level agreement}, в котором прописываются четкие
условия, которые владелец программной системы обязуется соблюдать перед поль­
зователем. Подобные соглашения играют большую роль в рамках доверия к по­
ставщику.
Например, мы можем зафиксировать определенное количество запросов, которые
одновременно может обработать наша система в рамках одного пользовательского
аккаунта. Также мы можем определять максимальное количество отправляемого
трафика, одновременных соединений, количество использований системы в месяц
и т. д. Подобные договоренности играют большую роль в планировании системы.
Зная, сколько максимально может использовать один пользователь, мы можем гра­
мотнее планировать стратегии развертывания и ресурсы системы, чтобы в пиковые
моменты мы могли обеспечить потребности всех.
Очень часто в SLA фигурируют договоренности о доступности сервиса. Чаще всего
она фиксируется в процентах доступности. Например, мы можем зафиксировать,
что наша система будет доступна 99,9 % времени. Если переводить это в привыч­
ное нам время, то это не более 1 минуты 26 секунд в день, 43 с половиной минуты
в месяц или почти 9 часов в год. Эта идея с девятками в процентах довольно попу­
лярна, например системы с четырьмя девятками могут быть недоступны не более
52 минут в году, а системы с шестью -
всего лишь 31 секунду за целый год. Как
правило, разработчики, когда оценивают доступность своей системы, обычно опе-
Часть 11. Системная архитектура
240
рируют именно информацией о процентах доступности и этих «девятках», по­
скольку этот подход оценки является общепринятым.
Безусловно, доступность сервиса важна для обычных пользователей, которые поль­
зуются нашей платформой, никто не хочет заходить в банковское приложение, что­
бы срочно выполнить перевод, и обнаружить, что оно недоступно. Однако она так­
же важна в случае, если пользователями нашего сервиса являются другие разработ­
чики. Например, если нашим сервисом является API, который используют другие
разработчики для своих проектов, то параметры доступности так или иначе будут
применяться к нижестоящим сервисам. Например, если на стороне пользователь­
ского сервиса не существует каких-либо альтернативных вызовов, которые способ­
ны заменить вызов к нашему сервису, доступность нашего сервиса будет унаследо­
вана этим дочерним сервисом. Это особенно заметно в случае с облачными про­
вайдерами.
Если
девятки,
не
мы
доступность
сможем
серверов
построить
такого
приложение
провайдера
с
шестью
не
превышает три
девятками,
сама инфраструктура не обладает такой надежностью, хотя
потому
что
наше приложение,
безусловно, имеет все шансы ею обладать. Поэтому, если нам важно соблюдать
доступность нашей системы, стоит внимательнее присмотреться к тем продуктам,
которые
мы
планируем
использовать
при
построении
системы,
потому
что
есть
шанс, что доступность в их SLA не соответствует нашей.
Сами проценты доступности могут быть одной из целей, поставленных перед нача­
лом разработки со стороны бизнеса. И нередко для того, чтобы обеспечить такую
доступность, особенно начиная с четырех или пяти девяток, архитектура сервиса, а
также весь выбор технологий, начинает служить сугубо этой цели. Доступность
приложения в этом случае будет определять сами типы и подход к разработке, по­
тому что каждая новая девятка -
это не просто дополнительно запущенный экзем­
пляр приложения либо сине-зеленое развертывание, это невероятно сложный труд,
который подразумевает репликации между целыми ЦОДами на случай, если один
из них выйдет из строя, с возможностью моментального перехода пользователей на
другие мощности, при этом сами пользователи даже не будут подозревать об этом.
Поэтому подходить к определению того, какая доступность нам нужна, стоит от­
ветственно, поскольку нередко в приложении может не требоваться такая абсолют­
ная доступность.
Одним из важных параметров для SLA являются те последствия, которые должны
наступить при их нарушении. Варианты того, что можно потребовать в случае не­
предоставления качественной услуги, ограничены, как правило, правовым полем,
но стоит отметить, что последствия определяет именно поставщик. Чаще всего, на­
пример,
если
процентах,
приложение
поставщих
не
может
было доступно
предоставить
в
скидку
рамках определенных в
на
использованные
SLА­
в этот
мо­
мент ресурсы. Например, если недоступность была незначительна, провайдер мо­
жет сбросить
10-20 % стоимости услуг. Однако если проблемы были серьезными,
например сервис был доступен меньше чем 95 % всего времени (а это, к слову, уже
более часа в день, полтора дня в месяц и более 18 дней в году), то провайдер может
вовсе не взять денег за использование. Похожий подход вы можете ввести в рамках
своего сервиса.
241
Глава 13. Мониторинг работы системы
Благодаря SLA мы можем формализовать надежность нашего сервиса перед наши­
ми клиентами, что особенно полезно, если нашими клиентами являются другие
разработчики. Однако важным моментом является то, чтобы мы могли замерить
параметры, которые будут говорить нам о том, что мы смогли соблюсти опреде­
ленное нами SLA. А также нам важно поставить цели, достижение которых позво­
лит нам говорить о том, что мы смогли доставить такой уровень обслуживания, ко­
торый прописан в рамках нашего договора.
Для этого был придуман целевой уровень сервиса, или SLO (service level objective).
Основная идея заключается в том, чтобы мы могли формализовать наши догово­
ренности,
но уже
на уровне
конкретных системных
метрик,
которые
мы
сможем
замерить и использовать для аналитики.
SLO хотя и неразрывно связан с SLA, но имеет большее значение для разработчи­
ков самой платформы, чем для конечных пользователей. Предположим, что разра­
ботчикам была поставлена задача обеспечения высокой доступности системы, а
также возможности обработки множества параллельных запросов от одного поль­
зователя. Главная проблема таких требований -
отсутствие конкретики, поэтому
мы можем преобразовать их в доступность системы в три девятки, а также возмож­
ности обработки до
I О запросов в секунду для одного пользователя. И для того
чтобы сделать это возможным, разработчики могут поставить себе определенные
цели, которые и попадут в SLO, достижение которых позволит ответить на вопрос,
соответствует ли платформа соглашению об уровне использования. Для того чтобы
иметь пространство для маневров и непредвиденных обстоятельств, разработчики
в SLO прописывают себе более строгие цели, чем в SLA. Например, если мы хотим
обеспечить доступность в три девятки, разработчики могут стремиться к доступно­
сти в 99,95 % времени. Если в SLA прописано, что мы должны иметь возможность
обрабатывать до
I О запросов от пользователя параллельно, хорошей практикой
12-14 запросов. Поскольку ошибки и че­
будет стараться обеспечивать обработку
ловеческий фактор неизбежны в системе любого масштаба с любыми средствами
защиты от них, такой подход позволяет разработчикам строить более надежную
систему, которая фактически будет работать лучше, чем описано в SLA, и обеспе­
чить себе дополнительное время на случай непредвиденных ошибок.
Интересным подходом тут является так называемый баланс допустимых ошибок.
Формально в SLO мы можем указывать любые цели, например количество ошибок
в коде во время работы сервиса не должно превышать 30 в месяц. Это означает, что
наше приложение может выдавать непредвиденные ошибки не чаще одного раза
в день. Интересный момент с балансом ошибок заключается в том, что мы можем
заниматься его мониторингом и предсказывать его. Например, мы видим, что вме­
сто допустимой одной ошибки в день наше приложение начинает выдавать ошибку
I О дней мы израсходуем наш баланс
уже три раза в день. Это означает, что спустя
ошибок, что потенциально может привести к нарушению
применить
и
к доступности
приложения:
если
мы
видим,
SLA. Похожее можно
что
наше
приложение
стабильно недоступно в день больше определенного нами лимита, мы можем уже
заранее предсказать, что такими темпами мы не сможем соблюдать наше SLA, что
может привести к оттоку пользователей и финансовым потерям. Поэтому подоб-
242
Часть 11. Системная архитектура
ный подход позволяет смотреть на ошибки приложения стратегически, давая воз­
можность
не
просто концентрировать
внимание на том,
что
какая-то
конкретная
часть сломалась, но и предсказывать, как сохранение подобного поведения может
повлиять на качество работы платформы в дальнейшем.
Естественно, подобные инициативы требуют определенного совершенства в рамках
управления платформой. Для того чтобы понимать, что наша платформа доступна,
должны быть настроены уже известные нам проверки работоспособности, причем
это могут быть системы, которые эмулируют действие пользователя, а не просто
опрашивают приложение с помощью GЕТ-запросов. Для мониторинга баланса
ошибок должны быть настроены специальные правила и фильтры, которые будут
агрегировать логи, метрики и трейсы в одном месте, чтобы иметь полное представ­
ление о том, насколько наша система стабильна и в какой из ее частей могут на­
блюдаться ошибки. Вишенкой на торте становятся автоматические нотификации
разработчиков через мессенджеры либо электронную почту для того, чтобы мини­
мизировать потребность в ручном мониторинге метрик. Да, это не исключает роль
человека в ответе на подобные инциденты (нередко команды могут по очереди
круглосуточно дежурить на проекте, чтобы иметь возможность быстро среагиро­
вать на ситуацию), но тут важно вынести в автоматизацию те моменты, в которых
это возможно, чтобы у человека появилось больше времени для занятия теми дела­
ми, которые может делать только человек.
Резюмируя: SLA и SLO являются неотъемлемыми, если мы хотим не просто обес­
печить стабильность и надежность работы нашего сервиса, но и формализовать это.
Надежное SLA может быть ключом к тому, чтобы завоевать доверие клиентов, по­
скольку если наше приложение не только предоставляет уникальные функции, но и
его надежность закреплена документально, это точно повышает привлекательность
нашего решения, особенно если у него есть конкуренты. В свою очередь, методоло­
гия
SLA и SLO направлена не только на пользователей, но и на разработчиков
в том числе. Благодаря постановке целей, которые должны соблюдаться внутри
приложения, разработчики могут иметь вполне конкретные метрики, оптимизацией
которых можно заниматься. Важно при этом не забывать про важное правило «почему» важнее, чем «как». Это означает, что не каждому приложению могут по­
надобиться шесть девяток доступности либо миллисекундная задержка для любого
из вызовов, важно в первую очередь исходить из задач бизнеса, поскольку каждое
из таких дополнительных требований может увеличить нагрузку на разработчиков
и уменьшить скорость выпуска основных функций.
Анализ инцидентов
Ошибки неизбежны, их невозможно исключить в любой из сфер жизни, то же са­
мое касается и разработки программного обеспечения. Самое важное при этом это не только стараться избегать их, но и стараться на них учиться, а также извле­
кать из них полезную информацию, которая в перспективе сможет улучшить нашу
платформу.
243
Глава 13. Мониторинг работы системы
Разработка сложна тем, что в ней существует большое количество неявных и не­
очевидных вещей. Связано это с тем, что каждый проект уникален, он представляет
собой не только уникальные бизнес-требования, вместе с ними также тесно пере­
плетаются сложные программные технологии, которые, возможно, до этого могли
никогда не работать вместе. Все это умножается на то, что такие системы делают
люди, командами с большим количеством разноплановых специалистов, оперируя
сложными абстракциями. Вся эта комбинаторика складывается в большую площадь
возможных ошибок, которые могут привести к негативному пользовательскому
опыту либо и вовсе подставить под удар важные бизнес-процессы, которые находят
отражение в реальном мире. Поэтому важно смотреть на это с практической точки
зрения -
как именно допускаемые ошибки могут делать наш продукт лучше.
Очевидно, что не существует одного идеального плана, как правильно выстроить
процессы анализа инцидентов, чтобы он подошел сразу всем. Это правило в прин­
ципе универсально для всего, что связано с разработкой,
-
подходящих всем
решений нет. Однако существует определенный набор практик, которые могут ис­
пользоваться командами для того, чтобы адаптировать их под свои процессы и сде­
лать этот процесс наиболее удобным и эффективным.
Определение возможных причин
События в работе системы, которые нарушают ее нормальную работу, могут быть
связаны с абсолютно разными абстракциями. Это могут быть как проблемы с са­
мим кодом приложений, который не в состоянии обработать те или иные бизнес­
сценарии корректно, так и инфраструктурные проблемы, связанные, например,
с ошибками в работе сети, настройке серверов, их банальном отключении из-за то­
го, что появились проблемы с электричеством. Да, маловероятно, что в один день
на ЦОД упадет метеорит, из-за чего наше приложение перестанет работать, но
только потому, что такой шанс есть, мы обязательно должны предусмотреть это.
Такой подход не позволит нам покрыть все возможные сценарии -
это невозмож­
но с физической точки зрения, поэтому нам нужно определиться с теми местами,
в которых ошибки могут появиться с большей вероятностью, поскольку мы будем
заранее представлять, откуда нам их ждать.
В этом
вопросе важна последовательность и методичность.
начать с общей для всех проектов части -
Например,
можно
инфраструктуры. Стоит ответить на
вопрос: достаточно ли надежна наша система с инфраструктурной точки зрения?
Например, мы можем использовать готовые облачные решения, поскольку облач­
ные провайдеры могут предоставлять достаточно надежный сервис и разнообраз­
ную поддержку по настройке и конфигурации. Если мы строим свою собственную
инфраструктуру, важно понимать, как именно будет выполняться управление. Для
обеспечения надежной работы нам может понадобиться выделенная команда для
того, чтобы управлять инфраструктурой, причем на разных уровнях, от физической
сборки самих серверов до управления программным обеспечением, реализующим
виртуализацию и оркестрацию приложений.
Частью, в которой неизбежны постоянные изменения, является программный код.
Каждый новый коммит разработчиков может привести к ошибкам в работе плат-
Часть 11. Системная архитектура
244
формы, поэтому важно использовать лучшие практики, чтобы это минимизировать.
Мы уже затрагивали с вами циклы непрерывной интеграции, доставки и разверты­
вания, а также специальные подходы по тестированию приложений. Организация
этих подходов позволяет снизить шанс появления ошибок и благотворно влияет на
качество кода. К этому можно добавить интеграцию практик ревью кода, сессий
обмена опытом и митапами для того, чтобы большее количество разработчиков по­
нимало, какие действия можно предпринять, чтобы разрабатываемое ПО было
удобно развивать и чтобы его надежность также повышалась.
Третий пункт -
это пользовательская нагрузка. Когда мы выпускаем наше прило­
жение в свет, мы открываем большое количество потенциально новых сценариев,
которые не отрабатывались на этапе разработки. Да, ручное и автоматическое тес­
тирование позволяет снизить количество таких случаев, однако никогда не сможет
сделать его равным нулю. Помимо того что пользователи могут некорректно ис­
пользовать функционал, что может приводить к ошибкам, важным моментом явля­
ется пользовательская нагрузка, которая должна быть обработана нашим приложе­
нием. Особенно стоит отметить возможности ошибок, связанных с неправомерным
доступом и уязвимостями. Злоумышленники могут не только приводить систему
в нерабочее состояние, но и красть операционные данные, что является инцидентом
особого класса.
Четвертым вектором является развитие и масштабирование приложения. Со време­
нем количество активных пользователей платформы может сильно вырасти, из-за
чего текущие
подходы
могут давать большое
количество ошибок.
Например,
выбранная мощность базы данных не позволяет обрабатывать такое количество
запросов, и это требует вмешательства со стороны команды разработки. Такие со­
бытия могут приводить не только к масштабированию, но и переосмыслению архи­
тектуры приложения для того, чтобы обеспечивать такой же надежный сервис, как
и на старте приложения.
Естественно, это далеко не весь список тех мест приложения, на которые стоит об­
ратить внимание. Они зависят напрямую от бизнеса и от решаемой задачи, поэтому
важно учитывать специфику каждого из проектов, чтобы инвестировать время
именно в те места, в которых это может дать наибольшую выгоду и надежность.
Когда что-то случилось
Узнать, о том, что в приложении пошло что-то не так, можно разными путями. Са­
мый, на мой взгляд, удачный из них, -
это автоматические нотификации из систем
мониторинга, которые одновременно изучают логи, метрики и трейсы в поисках
аномального поведения. Иногда информацию о том, что платформа не работает,
можно узнать от пользователей платформы, а нередко и от собственников бизнеса.
Первое, что стоит сделать, -
это определить серьезность ситуации. Если пользова­
тели в формах обратной связи говорят, что какая-то из кнопок смещена на один
пиксель от золотого сечения, возможно, это не самая большая проблема, которую
нужно исправить в первые же секунды. Но если наша платформа недоступна либо
не позволяет выполнить задуманные действия, это определенно относится к тому,
на что стоит незамедлительно реагировать.
245
Глава 13. Мониторинг работы системы
Причины такого поведения могут быть совершенно разными. Как описывалось
выше, площадь потенциальных ошибок в современных программных системах до­
вольно велика. Поэтому стоит подключить свой опыт и понять, как те или иные
сигналы, поступающие от наших мониторинговых систем и от пользователей, мо­
гут соотноситься с частями системы. Если приложение не открывается, логичным
будет посмотреть, не упало ли оно. Если приложение имеет некорректный функ­
ционал, стоит посмотреть на изменения в кодовой базе. Так или иначе, подобные
ситуации должны отсекаться на этапе тестирования, однако никогда нельзя полно­
стью гарантировать безошибочность работы системы.
Когда потенциальная ошибка найдена, необходимо предпринять шаги для исправ­
ления. Если это инфраструктурная проблема, необходимо выполнить перезапуск
сервисов; если проблема связана с кодом, есть смысл выполнить откат к стабиль­
ной версии приложения, чтобы уже в спокойной обстановке иметь возможность
разобраться, почему приложение не работает.
Однако далеко не всегда происходит так, что откаты ошибок никак не влияют на
последующую работу системы. Например, из-за ошибок могли появиться непра­
вильные операционные данные, которые нельзя использовать. В таком случае мы
не можем просто запустить старую версию приложения. Поэтому хорошей практи­
кой является создание так называемых снапшотов базы данных, которые сохраня­
ют в себе ее состояние на определенный момент времени с возможностью после­
дующего отката.
Вообще, в рамках практики управления инцидентами существуют два полезных
термина -
RTO и RPO. RTO (recovery time objective), или целевое время восста­
новления -
это то время, которое происходит между самим инцидентом и восста­
новлением работы платформы. Например, если после инцидента наше приложение
смогло подняться за 1О минут, то RTO в этом случае будет 1О минут. RPO (recovery
point objective ), или целевая точка восстановления -
это та точка состояния сис­
темы, до которой можно откатиться в случае инцидента. Например, мы делаем
снапшоты базы данных каждый час. Случился инцидент, который привел к нару­
шению целостности операционных данных. Соответственно, при самом худшем
раскладе мы сможем откатиться до данных, которые были в нашей системе час на­
зад, поэтому наш RPO -
это один час. Существуют системы, которые делают не­
прерывную репликацию данных, поэтому данная метрика может быть практически
любой. Также существует другая аббревиатура,
MTD (maximum toleraЫe down-
time ), или максимш~ыю допустимое время недоступиости. Эта метрика определя­
ет, какое время недоступности сервиса может быть критичным с точки зрения биз­
неса и процессов. Например, секундная недоступность может быть не такой кри­
тичной для множества процессов, однако конкретные значения должен определять
сам бизнес.
Анализ инцидента и шаги к предотвращению
Важным моментом является извлечение выводов из того или иного инцидента. На
самом деле, именно благодаря таким ошибкам у команды появляется возможность
Часть 11. Системная архитектура
246
улучшить приложение, причем эффект от этого может быть даже больше, чем мно­
гократное тестирование в среде разработки либо обкатки.
В первую очередь важно понимать, какие именно несовершенства привели к слу­
чившемуся. Например, если система не смогла справиться с пользовательской на­
грузкой, необходимо задуматься о более гибких политиках масштабирования при­
ложения
либо других архитектурных подходах.
Если
проблема была связана
с ошибкой в коде, это может быть следствием недостаточной проверки, отсутствия
должного тестирования и ревью. Одно дело, если подобные проблемы возникают
раз в год, однако если они регулярны, то стоит присмотреться к различным подхо­
дам оценки качества программного обеспечения.
Хорошей практикой является подход, при котором заранее вырабатывается поря­
док действий в случае инцидентов. Например, могут определяться дежурные разра­
ботчики, а также цепочки контактов, которые должны быть активированы в случае
нештатных ситуаций в приложении. Помимо этого, могут быть разработаны поли­
тики действий, которые определяют порядок изоляции проблемы для того, чтобы
минимизировать влияние ошибки на пользовательский опыт. Когда в приложении
происходит какая-то проблема, команде может быть сложно скоординировать свои
действия, нередко может наблюдаться такая ситуация, при которой вместо факти­
ческого исправления ошибки производится поиск виноватых в ней. Ответить на
последний вопрос явно можно будет потом, поэтому важно заранее знать приори­
теты действий. Например, такие действия могут включать в себя анализ логов и
метрик, определенные действия с приложением, перезапуск сервисов, откаты дан­
ных и т. д. Когда разработчики заранее знают механизмы и политики, которые
можно использовать в той или иной ситуации, это уменьшает стресс и панику и
позволяет эффективнее разрешить проблему. Примером из реальной жизни, кото­
рый часто приводится, являются чек-листы пилотов, описывающие действия в той
или иной нештатной ситуации. Да, далеко не все такие ситуации можно покрыть
каким-то явным алгоритмом, нередко решающим может быть именно опыт, однако
такая документация не будет лишней в важный момент.
В эффективной работе с инцидентами важен опыт. Одно дело иметь множество
подробных планов действий, однако совершенно другое
-
использовать их на
практике. Естественно, не стоит нарочно ошибаться, чтобы потренироваться, одна­
ко есть несколько подходов, которые позволяют получить полезный опыт. Во­
первых, это симуляции. Например, мы можем тренироваться исправлять инциден­
ты на демостендах для того, чтобы отработать действия и в нужный момент, но уже
на реальном окружении, чувствовать себя увереннее. Например, можно раз в месяц
придумывать определенные сценарии ошибок в приложении, например выключать
базы данных, серверы, вносить сетевую задержку. Все это будет нарушать работу
нашего приложения, после чего группа разработчиков, которая не знает, что имен­
но случилось, будет заниматься анализом ошибок для того, чтобы их исправить.
Такая практика может быть не только интересной, но и крайне полезной, потому
что это реальные знания и действия, которые могут сохранить бизнесу огромные
суммы и репутацию в ответственный момент.
247
Глава 13. Мониторинг работы системы
Некоторые компании могут пойти еще дальше. Классический пример с так назы­
ваемым хаос-инжинирингом заключается в том, что нарушение работы системы
фактически автоматизируется и вносится в обычный процесс. Например, в рамках
системы становится нормой отключение определенных узлов в любые моменты
времени для того, чтобы проверить, насколько надежна наша система. Высшим
пилотажем является запуск таких систем
бильность разработанной
системы
не
на продакшен-окружении, при этом ста­
позволяет пользователям даже заметить
ошибки, хотя где-то в ЦОДе половина системы может быть выключена системой
хаос-инжиниринга.
Также важна итеративность. Система должна становиться лучше с каждым разом,
это касается как выпускаемых функций, так и нештатных ситуаций. С каждым
разом мы можем не только исправлять ошибки, но и делать процессы лучше, про­
думывать большее количество сценариев, которые в ответственный момент будут
применяться по назначению. Мы будем узнавать больше вещей, которые мы не
знаем о приложении, которое мы разрабатываем, и о домене, который мы модели­
руем. Важно не скатываться в полемику в случае, если ошибки произошли, гораздо
ценнее делать из них выводы и интегрировать эти выводы в процессы разработки.
Заключение
Наблюдаемость системы -
это крайне важный этап, польза которого может про­
явиться не сразу. Во время реальной работы системы, если это не было заложено
изначально, мы не сможем получить полноценное представление о ее работе про­
сто так. Это может быть критично даже в случае, если ничего плохого не происхо­
дит, поскольку это мешает нам работать над оптимизацией. Однако если наши
пользователи будут жаловаться на плохую работу либо мы сами будем наблюдать
за тем, что наши приложения падают с непонятными ошибками, то исправить это
без специальных средств может быть крайне проблематично.
Грамотно построенная наблюдаемость позволяет чувствовать себя увереннее во
время решения проблем эксплуатации. Да, сама по себе она не поможет в их пре­
дотвращении, однако
интерпретация человеком
позволит делать правильные тех­
нические выводы, которые будут также усилены опытом разработчиков. Помимо
негативных сценариев, с помощью таких данных можно глубже задумываться об
оптимизации
системы,
что
может
привести
не
только
к
увеличению
производи­
тельности, но и к уменьшению стоимости владения системой.
Нельзя забывать про инженерную сторону. Существует большое количество гото­
вых сервисов, которые предоставляют удобные и готовые интерфейсы для хране­
ния и анализа логов, метрик и трейсов. Есть смысл присмотреться к тем решениям,
которые вам предлагает облачный провайдер, поскольку благодаря гибким инте­
грациям у вас появляется возможность покрыть большую площадь системы, вклю­
чая сторонние сервисы. Однако если такие решения по каким-то причинам вам не
подходят, существует большое количество сервисов с открытым исходным кодом.
Но нельзя забывать, что их развертывание и управление -
это задача, которая ло-
Часть 11. Системная архитектура
248
жится на плечи именно вашей команды, например команды платформы, и на
управление нужно закладывать дополнительные риски и время.
Системы
мониторинга являются
неотьемлемой
частью
цикла разработки
про­
граммного обеспечения. У нас не всегда будет возможность просто опросить поль­
зователей о том, как именно работает наша система, и одним из средств обратной
связи как раз могут стать результаты наших собственных наблюдений за системой.
Поэтому в случае если наша система планируется к длительной эксплуатации, а
также к определенному развитию для того, чтобы доставлять лучший пользова­
тельский опыт, нам необходимо иметь полное представление не только об инже­
нерных метриках, но и о метриках использования, поскольку именно на их основе
можно будет строить сильные технические решения, которые будут вести плат­
форму к успеху.
Итоги
Мир разработки программного обеспечения всегда выделялся на фоне остальных
видов деятельности. Он характеризуется не только определенной доменной слож­
ностью, которая может быть присуща множеству наук и сфер жизни, но и особым
переплетением социальных факторов, в которые входят взаимодействия внутри
коллектива, понимание других людей, умение слышать и быть услышанным. К то­
му же все это перемножается на постоянно меняющийся ландшафт, например на
новые технологии, подходы и изменяющиеся бизнес-требования. Все это требует
комплексного
подхода,
поскольку
ограничение
своего
круга
зрения
не
позволит
добиться поставленной задачи. С одной стороны, мы не можем полностью концен­
трироваться на технологиях, потому что сами по себе они не приносят никакой
пользы, это всего лишь инструмент, который в умелых руках может эффективно
решить поставленную задачу. С другой стороны, обращая внимание только на биз­
нес-часть, мы
можем упереться
в различные ограничения,
вызванные именно не­
грамотным выбором технологий. Поэтому в рамках заключения, чтобы собрать всю
картину воедино, мы вкратце пройдемся по моментам, которые мы разобрали
в этой книге.
О важности конечной цели и шагах к ней
Как уже говорилось ранее, коммерческая разработка не существует без цели, т. е.
без конкретной бизнес-задачи, которую необходимо решить. Да, существуют ис­
следовательские задачи, но так или иначе все это должно начать приносить пользу
бизнесу. И хотя это никак не касается технологий, языков программирования,
именно цель и бизнес-задача могут являться первым препятствием во время разра­
ботки.
Самым частым сценарием будет взаимодействие с теми, кто владеет бизнесом и
хочет улучшить определенные процессы. Такие владельцы уже могут иметь опре­
деленное представление о разработке, но им необходимо расширить штат для того,
чтобы ускорить выход продукта. В этой ситуации первым шагом является правиль­
ное определение бизнес-требований, а также налаживание правильных отношений
с заказчиком.
Итоги
250
Как уже говорилось ранее, разработка ПО -
это не только про технические детали.
С опытом многие разработчики начинают понимать, что в какой-то степени техно­
логии -
это далеко не самое важное. Отсутствие правильного представления биз­
нес-задачи может приводить к плачевным результатам. Например, разработчики
могут сделать продукт, который не решает поставленную задачу просто потому,
что изначально у команды сложилось представление, которое отлично от представ­
ления заказчика. Далее, сам процесс разработки может быть не таким мягким и
приятным, если он сопутствует постоянному изменению требований к системе, из­
за чего много времени тратится на переделывание существующих частей, на кото­
рые уже и так было потрачено много времени.
Перед тем как начинать что-то разрабатывать, крайне важно определить функцио­
нал системы и задачи, которые она должна решать. Делать это нужно вместе с за­
казчиком, будь это люди из бизнеса или даже другая команда разработки. В случае
с бизнесом сам клиент может не иметь полного представления о том, как именно
это будет реализовано технически, что создает определенные трудности. Про­
граммные системы, в отличие от людей, не могут разобраться с проблемой по ходу,
в особенности если она не была оговорена ранее, поэтому создание четких требо­
ваний в самом начале необходимо. Причем в такие требования должны входить не
только
позитивные сценарии,
но
и
негативные,
например
что делать,
если что-то
пошло не так. В случае если требования будут нечеткими, разработчики могут на­
чать заполнять пробелы на свое усмотрение, поскольку иначе это сделать нельзя,
из-за чего могут получиться системы, которые не решают задачи, действительно
важные для бизнеса.
Благо, что существует множество подходов, которые позволяют минимизировать
неясность с точки зрения бизнес-требований и соединить в себе как разработку, так
и бизнес. Во-первых, важно применять современные подходы бизнес-анализа, биз­
нес-моделирования. Во-вторых, важно документировать процесс разработки для
того, чтобы увеличить прозрачность как для разработчиков, так и для заказчика.
В-третьих, нельзя забывать про правильные отношения с заказчиком. Их нужно
строить в дружелюбном и профессиональном ключе, не закрываться от заказчика,
поскольку именно на вас лежит ответственность за имплементацию нужной ему
системы. Да, заказчик может быть далек от тех деталей, что вам уже давно извест­
ны,
но именно в
ваших силах представить ему процесс и вопросы
в таком свете,
чтобы ему все стало понятно и прозрачно. Такие отношения не только могут сни­
зить процент напряжения в коллективе, но и по итогу привести к разработке более
качественной и мощной платформы.
Отдельно хочется сказать про итеративность. Каждый новый проект уникален, да­
же если он максимально типичен и несложен с технической точки зрения. Это
выражено в том, что большое количество разнообразных технологий переплетаются
с не меньшим количеством бизнес-требований, что в итоге приводит к новой, уни­
кальной комбинации. Поэтому даже те подходы, которые мы уже использовали, не
всегда могут быть применены на другом проекте вслепую -
они банально могут
не решить задачу. Это называется «вещи, которые мы не знаем, что мы их не зна-
251
Итоги
ем» (things you don't know you don't know), и именно они являются причиной того,
что разработка плохо решается подходом идеального планирования каждого шага с
их последующим исполнением. Связано это с тем, что во время самой разработки
могут вскрываться детали, которые невозможно было предсказать заранее, но они
сильно влияют на все наши предположения, которые мы планировали использовать
в будущем, из-за чего их придется изменять. Так при чем тут итеративность?
Многие методологии разработки адаптируются к этой особенности, активно ис­
пользуя обратную связь. С каждой решенной задачей мы становимся более опыт­
ными в рамках проекта, благодаря чему мы можем эффективнее решить (либо по­
ставить) новые задачи. Дг~я того чтобы итеративность работала правильно, после
каждой реализованной функции либо после определенных промежутков времени
мы должны получать определенную обратную связь о том, что мы сделали. Напри­
мер, мы можем получать ее от заказчика, от пользователей нашей платформы, от
членов нашей команды. Благодаря этому у нас появляется возможность постоянно
улучшать свое представление о том, какую именно задачу мы решаем, какие есть
ограничения и особенности в домене, что мы моделируем, как не допускать оши­
бок, которые были допущены. Не стоит стремиться за один идеальный раз закрыть
все сразу, лучше сделать несколько разных попыток и изменять подход в зависимо­
сти от обратной связи. В таком случае получается строить достаточно надежные
и сильные системы, которые могут решать поставленные бизнес-задачи быстро и
эффективно, принося выгоду пользователям.
О технологиях
Вторым моментом, который вносит огромный вклад в то, как именно будет выгля­
деть ваша платформа, являются технологии. Их выбор -
это неочевидная задача
ввиду той же уникальности бизнес-требований. При этом сам процесс определения,
какую именно технологию нам нужно использовать, не представляет собой опрос­
ник формата «если вы встретились с таким, вам нужно использовать вот это».
Главное, что нужно понимать, когда мы сталкиваемся с выбором технического сте­
ка, -
это то, что не существует идеальных решений, которые полностью лишены
недостатков. Вероятнее всего, если в данный момент эти недостатки отсутствуют,
их проявление -
это вопрос времени. К каждой выбранной технологии нужно от­
носиться беспристрастно. История знает множество примеров, когда проектные
команды выбирали те или иные решения не из-за реальной необходимости, а из-за
того, что та или иная технология бьта популярна и была на слуху, после чего она
могла не подойти, что выливалось в негативный пользовательский опыт, а также
в большие дополнительные ресурсы, которые нужно было потратить на миграцию.
На мой взгляд, выбор технологий определяется несколькими факторами. Во-пер­
вых, это насмотренность и опыт. Хотя все бизнес-задачи и уникальны, они имеют
определенную типичность. Например, если нам критически важна согласованность
данных, т. е. эта особенность продиктована бизнес-задачей, мы можем посмотреть
в сторону решений, которые предоставляют это прямо из коробки без необходимо-
Итоги
252
сти городить какие-то альтернативные решения на нашей стороне. Так же как и на­
оборот, если согласованность нам не сильно важна, но важна производительность,
мы можем выбирать другие технологии, в которых упор сделан именно на это. При
этом проект очень редко состоит из одной технологии, подобных комбинаций мо­
жет быть большое множество, поэтому выбирать их необходимо с умом -
слож­
ность разработки в случае нескольких неправильных выборов может расти экспо­
ненциально.
Во-вторых, нельзя забывать, что ваше решение будут разрабатывать именно люди,
поэтому важно учитывать их опыт в данных технологиях. Если команда уже реали­
зовала несколько проектов в рамках данного стека, шанс того, что она сможет так­
же
успешно
и
быстро
разработать
еще
один,
становится
значительно
выше.
И наоборот, если команда не знакома с данной технологией, ей может потребовать­
ся дополнительное время на изучение, -
все из-за тех самых вещей, которые мы не
знаем, что мы не знаем. И тут выстраивается любопытный баланс между тем, на­
сколько технология подходит к нашей задаче сама по себе и насколько команда хо­
рошо ее знает. Идеально, когда оба этих пункта высоки, однако такое бывает не
всегда, а что именно представляется тут более важным, является уникальным для
каждого проекта. Например, если команда имеет большой опыт с NоSQL-реше­
ниями, но на проекте крайне важна согласованность данных и реляционная модель,
возможно, есть смысл применять реляционные БД, несмотря на то, что команда не
имеет достаточного опыта, поскольку данный проект может быть стратегическим,
и если заложить в него неправильные технологии на начальном этапе, то это мо­
жет привести к трудностям в будущем. И наоборот, если команда имеет боль­
шой опыт с реляционными БД, но на этапе планирования видно, что сама платфор­
ма может столкнуться с большим объемом данных, есть смысл по крайней мере
начать со знакомых технологий и впоследствии мигрировать на что-то более под­
ходящее.
Последний пример связан с любопытным феноменом преждевременной оптимиза­
ции, который нередко выражается в выборе слишком сложных технологий. Для
большинства задач действительно можно выбрать что-то классическое, с чем рабо­
тало большинство разработчиков, что имеет достаточную поддержку в сообществе.
Однако многие могут на начальных этапах заложить в платформу возможность
роста, хотя явных поводов для этого может и не быть. Из-за этого команда может
потратить много времени и сил на интеграцию технологий, которые не смогут пол­
ноценно раскрыться на проекте просто потому, что он не будет настолько успешен,
как это ожидалось в начале. Поэтому тут стоит исходить из того, что важно в рам­
ках бизнес-задачи. Если нам необходимо быстрее разработать наш проект для того,
чтобы обойти конкурентов, действительно, есть смысл выбрать что-то простое,
с чем уже все работали, и быстрее выпустить продукт, который сможет приносить
пользу, с возможностью его расширения в будущем. С другой стороны, если вы
работаете над стратегическим сервисом, который будет требовать идеальной рабо­
ты и точно будет сталкиваться с большой нагрузкой, есть смысл потратить время
на изучение и разработку. Поэтому подобные ситуации требуют определенных вы-
253
Итоги
боров, которые могут быть неочевидны с первого взгляда, возможно, даже казаться
неправильными, однако узнать это можно будет только тогда, когда система станет
эксплуатироваться по-настоящему, а не в тестовом окружении.
И помочь в этом может та самая итеративность. Не стоит бояться пробовать новые
подходы, поскольку в рамках вашей задачи вряд ли будут выборы, которые заранее
будут помечены как правильные и неправильные. Самым важным является то, как
именно вы используете обратную связь и как вы стимулируете ее появление. На­
пример, вы можете активно
использовать различные виды пользовательского тес­
тирования, интегрировать в вашу платформу мониторинг, собирать любую допол­
нительную информацию. Благодаря подобным данным можно добавить дополни­
тельное осознание к управленческим решениям, которые вы собираетесь сделать,
что
в
конечном
итоге
приведет
к
лучшему
пользовательскому
опыту
и
успеху
вашей платформы.
О развитии
Как уже говорилось ранее, самая сложная задача разработки -
не столько разрабо­
тать систему в моменте, сколько разработать ее таким образом, чтобы ее можно
было безболезненно расширить и улучшить. Это особенно актуально ввиду изме­
няющихся требований, видения и технологий. Часто именно из-за ошибок во время
проектирования и разработки у команд не получается улучшать свою платформу,
поскольку на уровне написанного кода это может быть очень сложно, из-за чего
многие системы приходится переписывать с нуля. Возможность развития нередко
нужно закладывать еще до начала самой разработки, хотя она и может требовать
определенного усложнения, которое, скорее всего, замедлит саму разработку, но
зато облегчит расширение в будущем.
Это сложный вопрос баланса между преждевременной оптимизацией и накоплени­
ем технического долга. Уже традиционно -
не существует идеальной точки, кото­
рой можно придерживаться, необходимо определять ее положение самостоятельно.
Но в большинстве случаев вы будете работать над системами, которые будут ис­
пользоваться не один раз, и если они будут успешными, то так или иначе будут
расширяться и улучшаться. Поэтому для того чтобы развитие проекта не приносило
вам боль, можно придерживаться определенных лучших практик, которые повысят
шансы на успех.
Во-первых, как уже говорилось много раз, нельзя забывать про итеративный под­
ход. Важно пробовать новое, получать обратную связь, прислушиваться к ней и
делать соответствующие шаги. Во-вторых, важно прислушиваться к сообществу,
к подходам и технологиями, которые сейчас принято использовать. Существует
большой шанс того, что ваша проблема может быть похожа на проблемы других
людей, и они уже смогли успешно интегрировать те или иные подходы, за счет чего
развитие платформы не становится такой большой проблемой, какой бы она могла
быть в противном случае.
254
И третий момент, который непрямую влияет на развитие платформы, -
Итоги
это разви­
тие себя. Так или иначе, систему разрабатывают люди, и именно от их решений
зависит то, какой результат получится на выходе. Ввиду постоянно изменяющихся
технологий и трендов не существует определенной точки, дойдя то которой можно
сказать, что ты все знаешь. Профессионализм разработчика заключается в том, что
вне зависимости от занимаемой им позиции, опыта, пройденных проектов, он не
перестает изучать новое, ставить под сомнение те истины, которые он знает, про­
должает вдохновлять людей вокруг себя на похожий подход к работе. И благодаря
тому, что задач, как и путей их решений, огромное множество, этот процесс можно
по-настоящему считать постоянным. В его рамках излишне рисовать себе конкрет­
ную цель, гораздо большую роль в нем играет именно путь.
Предметный указатель
А
АВАС 173
ACID 67, 91, 113, 116, 118, 128, 142, 144, 146, 152
API 18, 19, 38, 44, 63, 78, 79, 96, 112, 136, 144; 148,
150,152,162,165,167,170,194, 195,211,21~219,
227,240
AQA 201
AWS49
в
Go 21
gRPC 21, 23, 45, 82, 151,236
н
НDD52
Helm 218
НТТР 21, 34, 35, 40, 73, 79,100,234,236
НТТР/1.1 45
НТТР/2 45
НТТРS 37, 165, 166
ВА27
с
САР-теорема 117, 152
CDC 77
CDN 96, 97
CI 202,204
CPU49
СRМ 135
CRUD 38
D
DDD30,31, 140,141,148
DDosS 97
DevOps 27,147,152,197,211
DLQ 71, 80
DNS92
Е
ERP 132, 135
F
FТР208
G
GitHub 79
GitOps 211
IAM 163, 172
IР-адрес 54, 80, 92, 170, 178
J
JavaScript 21, 86
JOIN 110
JSON 70
JWT 168, 169
L
LLM 167
м
ML236
МТD 245
N
NodeJS 21
NoSQL 22, 121, 126, 127
NVMe 52
о
OIDC 172
ОRМ-фреймворк 21
256
Предметный указатель
SQL-зanpoc 67
р
SQS 70
SSD 52, 89
Swap 50
POST 168
Python 21
Q
т
QA 200,201
TLS 153, 165
ТТL 92, 93, 97
R
RBAC 173
RED 228,235
REST 23, 35, 38, 80, 82. 151. 227
RPO 245
RPS 199
RTO 245
Rust 21
u
Ш86
URL 37, 38, 176
UUID 102
V
vCPU 49
VPN 166
s
SaaS 135
SDK 136
SFТP 208
SMS 72
SOA 145, 147
SOAP 151
SQL 22, 88. 101,106,110,119, 144
в
w
WSDL43
у
YAML43, 203
Перцентиль 228
Пет-проект 27
Веб-хук 19, 23
с
г
Смещение 75
Гиперскейлер 96
Граф 127
т
д
Токен 154, 168, 172
Тран1акция 113, 114, 152
Диаграмма Ганта 232
◊
и
х
Идемпотентность 71
Хеш 62, 89, 122
к
ц
Кликстрим 70
ЦОД76
Конечная точка 19
ш
п
Паттерн проектирования 141
Переменная окружения
компенсационная 152
162
Шард 95,117
0
You can add this document to your study collection(s)
Sign in Available only to authorized usersYou can add this document to your saved list
Sign in Available only to authorized users(For complaints, use another form )