Джон Карнелл, Иллари Уайлупо Санчес Микросервисы Spring в действии Ещё больше книг по Java в нашем телеграм канале: https://t.me/javalib Spring Microservices in Action JOHN CARNELL and ILLARY HUAYLUPO SÁNCHEZ Микросервисы Spring в действии Джон Карнелл, Иллари Уайлупо Санчес Москва, 2022 УДК 004.432 ББК 32.972.1 К21 К21 Джон Карнелл, Иллари Уайлупо Санчес Микросервисы Spring в действии / пер. с англ. А. Н. Киселева. – М.: ДМК Пресс, 2022. – 490 с.: ил. ISBN 978-5-97060-971-2 В книге рассказывается о том, как создавать приложения на основе микросервисов с использованием Java и Spring. Описываются особенности управления конфигурацией микросервисов и передовые практики их разработки. Уделено внимание защите потребителей, когда один или несколько экземпляров микросервисов выходят из строя. Начав с создания простых служб, читатель постепенно перейдет к знакомству с приемами эффективного журналирования и мониторинга, научится реструктурировать приложения на Java с помощью инструментов Spring, освоит управление API с помощью Spring Cloud Gateway. Издание предназначено для разработчиков на Java, имеющим опыт создания распределенных приложений и использования Spring, а также всем, кому интересно узнать, что необходимо для развертывания приложения на основе микросервисов в облаке. УДК 004.432 ББК 32.972.1 Copyright Original English language edition published by Manning Publications USA, USA. Copyright (c) 2021 by Manning Publications. Russian-language edition copyright (c) 2021 by DMK Press. All rights reserved. Все права защищены. Любая часть этой книги не может быть воспроизведена в какой бы то ни было форме и какими бы то ни было средствами без письменного разрешения владельцев авторских прав. ISBN (анг.) 978-1-617-29695-6 ISBN (рус.) 978-5-97011-971-2 © 2021 by Manning Publications Co. © Оформление, издание, перевод, ДМК Пресс, 2022 Я посвящаю эту книгу всем женщинам, которые делают карьеру в естественно-научных, инженерно-технических и математических дисциплинах. Нет ничего невозможного, если упорно трудиться. Краткое оглавление 1 Добро пожаловать в Spring Cloud......................................... 27 2 Обзор мира микросервисов через призму Spring Cloud... 63 3 Создание микросервисов с использованием Spring Boot.... 91 4 Добро пожаловать в Docker.................................................. 129 5 Управление конфигурациями с использованием Spring Cloud Configuration Server........................................ 150 6 Обнаружение служб............................................................... 191 7 Когда случаются неприятности: шаблоны устойчивости с использованием Spring Cloud и Resilience4j................... 223 8 Маршрутизация служб с использованием Spring Cloud Gateway..................................................................................... 259 9 Безопасность микросервисов.............................................. 294 10 Событийно-ориентированная архитектура и Spring Cloud Stream............................................................ 332 11 Распределенная трассировка с использованием Spring Cloud Sleuth и Zipkin................................................. 367 12 Развертывание микросервисов........................................... 404 Содержание Предисловие от издательства................................................................ 16 Предисловие................................................................................................... 17 Благодарности. ............................................................................................ 19 Об этой книге................................................................................................ 21 1 Добро пожаловать в Spring Cloud.............................................. 27 1.1. Эволюция архитектуры микросервисов.................................. 28 1.1.1. N-уровневая архитектура..................................................... 28 1.1.2. Что такое монолитная архитектура?................................ 29 1.1.3. Что такое микросервис?....................................................... 30 1.1.4. Зачем менять способ создания приложений?........................ 32 1.2. Микросервисы со Spring............................................................. 34 1.3. Что мы будем создавать?............................................................. 36 1.4. О чем эта книга?........................................................................... 37 1.4.1. Что вы узнаете в этой книге................................................ 37 1.4.2. Почему эта книга актуальна для вас?................................. 38 1.5. Облачные приложения и приложения на основе микросервисов.................................................................................... 39 1.5.1. Создание микросервиса с помощью Spring Boot..................... 39 1.5.2. Что такое облачные вычисления?........................................ 44 1.5.3. В чем преимущества облачных вычислений и микросервисов?............................................................................. 46 1.6. Микросервисы – это больше чем код....................................... 49 1.7. Базовый шаблон разработки микросервисов......................... 50 1.8. Шаблоны маршрутизации.......................................................... 52 1.9. Устойчивость клиентов............................................................... 54 8 Содержание 1.10. Шаблоны безопасности............................................................ 55 1.11. Шаблоны журналирования и трассировки........................... 56 1.12. Шаблон сбора метрик приложения........................................ 58 1.13. Шаблоны сборки/развертывания микросервисов............. 59 Итоги.................................................................................................... 61 2 Обзор мира микросервисов через призму Spring Cloud......... 63 3 Создание микросервисов с использованием Spring Boot........ 91 2.1. Что такое Spring Cloud?.............................................................. 64 2.1.1. Spring Cloud Config................................................................ 65 2.1.2. Spring Cloud Service Discovery................................................. 66 2.1.3. Spring Cloud LoadBalancer и Resilience4j............................... 66 2.1.4. Spring Cloud API Gateway...................................................... 67 2.1.5. Spring Cloud Stream................................................................ 67 2.1.6. Spring Cloud Sleuth................................................................. 67 2.1.7. Spring Cloud Security.............................................................. 68 2.2. Пример использования Spring Cloud....................................... 68 2.3. Приемы создания облачных микросервисов.......................... 71 2.3.1. База кода............................................................................... 74 2.3.2. Зависимости......................................................................... 75 2.3.3. Конфигурация....................................................................... 76 2.3.4. Вспомогательные службы...................................................... 76 2.3.5. Сборка, выпуск, выполнение.................................................. 77 2.3.6. Процессы............................................................................... 78 2.3.7. Привязка портов.................................................................. 78 2.3.8. Масштабируемость.............................................................. 79 2.3.9. Одноразовость....................................................................... 79 2.3.10. Сходство окружений разработки/эксплуатации.............. 80 2.3.11. Журналирование................................................................. 80 2.3.12. Задачи администрирования.............................................. 81 2.4. Актуальность наших примеров.................................................. 81 2.5. Создание микросервиса с использованием Spring Boot и Java...................................................................................82 2.5.1. Подготовка окружения......................................................... 83 2.5.2. Начало создания проекта..................................................... 83 2.5.3. Запуск приложения Spring Boot: класс инициализации....... 88 Итоги.................................................................................................... 90 3.1. Точка зрения архитектора: проектирование микросервисной архитектуры.......................................................... 92 3.1.1. Декомпозиция бизнес-задачи............................................. 92 Содержание 9 3.1.2. Детализация служб.............................................................. 95 3.1.3. Определение интерфейсов служб........................................... 98 3.2. Когда не следует использовать микросервисы....................... 99 3.2.1. Сложность распределенных систем...................................... 99 3.2.2. Беспорядочный рост виртуальных серверов или контейнеров.............................................................................. 99 3.2.3. Тип приложения................................................................. 100 3.2.4. Транзакции и согласованность данных............................... 100 3.3. Точка зрения разработчика: создание микросервиса с использованием Spring Boot и Java.............................................. 100 3.3.1. Встраивание дверного проема в микросервис: контроллер Spring Boot.................................................................. 101 3.3.2. Добавление интернационализации в службу лицензий...... 112 3.3.3. Реализация Spring HATEOAS для отображения связанных ссылок........................................................................... 115 3.4. Точка зрения инженера DevOps: сборка выполняемых артефактов......................................................................................... 118 3.4.1. Сборка службы: упаковка и развертывание микросервисов......120 3.4.2. Инициализация службы: управление конфигурацией микросервисов....................................................... 122 3.4.3. Регистрация и обнаружение службы: взаимодействие клиентов с микросервисами................................ 123 3.4.4. Мониторинг состояния микросервиса................................ 124 3.5. Объединение точек зрения...................................................... 127 Итоги.................................................................................................. 128 4 Добро пожаловать в Docker. ..................................................... 129 4.1. Контейнеры или виртуальные машины?............................... 130 4.2. Что такое Docker?...................................................................... 132 4.3. Файлы Dockerfile........................................................................ 135 4.4. Docker Compose......................................................................... 136 4.5. Интеграция Docker с микросервисами.................................. 138 4.5.1. Создание образа Docker ....................................................... 138 4.5.2. Создание образов Docker со Spring Boot................................ 144 4.5.3. Запуск служб с помощью Docker Compose............................. 147 Итоги.................................................................................................. 148 конфигурациями с использованием 5 Управление Spring Cloud Configuration Server............................................... 150 5.1. Об управлении конфигурациями (и сложностью)............... 151 5.1.1. Архитектура управления конфигурацией.......................... 152 10 Содержание 5.1.2. Варианты реализации....................................................... 154 5.2. Настройка Spring Cloud Configuration Server....................... 156 5.2.1. Настройка класса инициализации Spring Cloud Config.... 161 5.2.2. Использование Spring Cloud Config Server с файловой системой..................................................................... 161 5.2.3. Создание конфигурационных файлов для службы.............. 163 5.3. Интеграция Spring Cloud Config с клиентом Spring Boot...... 168 5.3.1. Настройка зависимостей Spring Cloud Config Service в службе лицензий.......................................................................... 170 5.3.2. Настройка службы лицензий для взаимодействий с Spring Cloud Config...................................................................... 170 5.3.3. Подключение к источнику данных с использованием Spring Cloud Config Server.............................................................. 175 5.3.4. Чтение настроек с использованием @ConfigurationProperties................................................................ 179 5.3.5. Обновление настроек с использованием Spring Cloud Config Server.............................................................. 180 5.3.6. Использование Spring Cloud Configuration Server с Git........ 182 5.3.7. Интеграция Vault со службой Spring Cloud Config.............. 183 5.3.8. Пользовательский интерфейс Vault.................................... 184 5.4. Защита конфиденциальных настроек в конфигурации...... 187 5.4.1. Настройка симметричного шифрования........................... 187 5.4.2. Шифрование и дешифрование настроек............................. 188 5.5. Заключительные мысли............................................................ 190 Итоги.................................................................................................. 190 6 Обнаружение служб...................................................................... 191 6.1. Где моя служба?........................................................................... 193 6.2. Обнаружение служб в облаке................................................... 195 6.2.1. Архитектура механизма обнаружения служб.................... 196 6.2.2. Обнаружение служб с использованием Spring и Netflix Eureka.....................................................................................200 6.3. Создание службы Spring Eureka............................................... 202 6.4. Регистрация служб в Spring Eureka......................................... 207 6.4.1. REST API Eureka................................................................. 211 6.4.2. Панель управления Eureka.................................................. 212 6.5. Использование механизма обнаружения служб................... 214 6.5.1. Поиск экземпляров служб с Spring Discovery Client.............. 216 6.5.2. Вызов служб с использованием шаблона Spring REST с поддержкой Load Balancer.......................................................... 218 6.5.3. Вызов служб с использованием Netflix Feign......................... 220 Итоги.................................................................................................. 222 Содержание 7 11 огда случаются неприятности: шаблоны устойчивости К с использованием Spring Cloud и Resilience4j......................... 223 7.1. Шаблоны устойчивости на стороне клиента........................ 225 7.1.1. Балансировка нагрузки на стороне клиента...................... 226 7.1.2. Размыкатель цепи.............................................................. 226 7.1.3. Резервная реализация.......................................................... 227 7.1.4. Герметичные отсеки........................................................... 227 7.2. Почему устойчивость клиента важна..................................... 228 7.3. Реализация с Resilience4j.......................................................... 232 7.4. Подготовка службы лицензий к использованию Spring Cloud и Resilience4j............................................................... 233 7.5. Реализация размыкателя цепи................................................. 234 7.5.1. Добавление размыкателя цепи для обработки вызовов службы организаций........................................................ 240 7.5.2. Настройка размыкателя цепи........................................... 240 7.6. Использование резервной реализации.................................. 241 7.7. Реализация шаблона герметичных отсеков.......................... 244 7.8. Реализация шаблона повторных попыток............................. 248 7.9. Реализация шаблона ограничителя частоты........................ 249 7.10. ThreadLocal и Resilience4j...................................................... 252 Итоги.................................................................................................. 257 служб с использованием 8Маршрутизация Spring Cloud Gateway. ................................................................... 259 8.1. Что такое сервисный шлюз?.................................................... 260 8.2. Введение в Spring Cloud Gateway............................................. 263 8.2.1. Настройка проекта шлюза Spring Boot.............................. 264 8.2.2. Настройка Spring Cloud Gateway для взаимодействий с Eureka.......................................................................................... 266 8.3. Настройка маршрутов в Spring Cloud Gateway...................... 268 8.3.1. Автоматическое отображение маршрутов с помощью механизма обнаружения служб..................................................... 268 8.3.2. Отображение маршрутов вручную с помощью механизма обнаружения служб..................................................... 270 8.3.3. Динамическая загрузка настроек маршрутизации........... 273 8.4. Настоящая мощь Spring Cloud Gateway: фабрики предикатов и фильтров................................................... 274 8.4.1. Встроенные фабрики предикатов....................................... 275 8.4.2. Встроенные фабрики фильтров.......................................... 276 8.4.3. Добавление своих фильтров................................................ 278 12 Содержание 8.5. Создание предварительного фильтра.................................... 281 8.6. Использование идентификатора корреляции в службах.... 284 8.6.1. UserContextFilter: перехват входящих HTTP-запросов........ 286 8.6.2. UserContext: обеспечение доступности HTTP-заголовков в службах........................................................... 287 8.6.3. RestTemplate и UserContextInterceptor: обеспечение передачи идентификатора корреляции нижестоящим службам................ 289 8.7. Создание заключительного фильтра, добавляющего идентификатор корреляции........................................................... 290 Итоги.................................................................................................. 293 9 Безопасность микросервисов...................................................... 294 9.1. Что такое OAuth2?..................................................................... 295 9.2. Введение в Keycloak................................................................... 297 9.3. Начнем с малого: использование Spring и Keycloak для защиты единственной конечной точки................................. 299 9.3.1. Добавление Keycloak в Docker............................................... 299 9.3.2. Настройка Keycloak............................................................ 300 9.3.3. Регистрация клиентского приложения.............................. 303 9.3.4. Настройка пользователей O-stock....................................... 308 9.3.5. Аутентификация пользователей приложения O-stock....... 310 9.4. Защита службы организаций с использованием Keycloak..... 314 9.4.1. Добавление в службы JAR-файлов Spring Security и Keycloak....................................................................................... 314 9.4.2. Настройка связи службы с сервером Keycloak...................... 315 9.4.3. Определение пользователей, кому разрешено обращаться к службе..................................................................... 315 9.4.4. Передача токена доступа................................................... 320 9.4.5. Анализ нестандартного поля в JWT................................... 326 9.5. Некоторые заключительные рассуждения о безопасности микросервисов...................................................... 328 9.5.1. Используйте HTTPS/Secure Sockets Layer (SSL) для взаимодействий между службами........................................... 329 9.5.2. Используйте шлюз для организации доступа к микросервисам............................................................................ 329 9.5.3. Разделите свои службы на общедоступные и закрытые..... 330 9.5.4. Ограничьте поверхность атаки на ваши микросервисы, заблокировав ненужные сетевые порты........................................ 330 Итоги.................................................................................................. 331 Содержание 13 архитектура 10Событийно-ориентированная и Spring Cloud Stream.................................................................... 332 10.1. Обмен сообщениями, событийно-ориентированная архитектура и микросервисы......................................................... 333 10.1.1. Передача событий об изменении состояния с использованием синхронного подхода запрос/ответ.................. 334 10.1.2. Передача событий об изменении состояния с использованием сообщений.......................................................... 337 10.1.3. Недостатки архитектуры на основе сообщений.............. 339 10.2. Введение в Spring Cloud Stream............................................. 340 10.3. Простые издатель и получатель сообщений....................... 342 10.3.1. Настройка Apache Kafka и Redis в Docker....................... 343 10.3.2. Публикация сообщений в службе организаций................ 344 10.3.3. Получение сообщений в службе лицензий......................... 351 10.3.4. Тестирование передачи сообщений между службами........ 355 10.4. Пример использования Spring Cloud Stream: распределенное кеширование........................................................ 356 10.4.1. Использование Redis в роли кеша...................................... 357 10.4.2. Определение собственных каналов.................................... 363 Итоги.................................................................................................. 366 трассировка с использованием 11 Распределенная Spring Cloud Sleuth и Zipkin......................................................... 367 11.1. Spring Cloud Sleuth и идентификатор корреляции............ 369 11.1.1. Подключение Spring Cloud Sleuth к службам лицензий и организаций............................................................................... 370 11.1.2. Особенности трассировки в Spring Cloud Sleuth............... 370 11.2. Агрегирование журналов и Spring Cloud Sleuth................. 372 11.2.1. Интеграция Spring Cloud Sleuth и стека ELK.................. 374 11.2.2. Настройка Logback в службах.......................................... 376 11.2.3. Определение и запуск приложений ELK в Docker.............. 380 11.2.4. Настройка Kibana............................................................ 383 11.2.5. Поиск идентификаторов трассировки Spring Cloud Sleuth в Kibana.......................................................... 386 11.2.6. Добавление идентификатора корреляции в HTTP-ответ с помощью Spring Cloud Gateway........................... 388 11.3. Распределенная трассировка с использованием Zipkin........ 390 11.3.1. Настройка зависимостей Spring Cloud Sleuth и Zipkin......391 14 Содержание 11.3.2. Настройка в службах ссылки на сервер Zipkin ................ 391 11.3.3. Настройка сервера Zipkin................................................. 392 11.3.4. Настройка уровней трассировки...................................... 393 11.3.5. Использование Zipkin для трассировки транзакций....... 394 11.3.6. Визуализация более сложных транзакций....................... 397 11.3.7. Трассировка операций обмена сообщениями..................... 398 11.3.8. Добавление дополнительных операций............................. 400 Итоги.................................................................................................. 403 12 Развертывание микросервисов.................................................. 404 12.1. Архитектура конвейера сборки/развертывания............... 406 12.2. Настройка базовой инфраструктуры для O-stock в облаке.......................................................................... 410 12.2.1. Создание базы данных PostgreSQL с использованием Amazon RDS...................................................... 413 12.2.2. Создание кластера Redis в Amazon................................... 416 12.3. После подготовки инфраструктуры: развертывание O-stock и ELK......................................................... 418 12.3.1. Создание экземпляра EC2 с помощью ELK....................... 418 12.3.2. Развертывание стека ELK в экземпляре EC2................... 422 12.3.3. Создание кластера EKS..................................................... 423 12.4. Конвейер сборки/развертывания в действии................... 430 12.5. Создание конвейера сборки/развертывания..................... 432 12.5.1. Настройка GitHub............................................................ 433 12.5.2. Сборка наших служб в Jenkins........................................... 434 12.5.3. Создание сценария конвейера............................................ 439 12.5.4. Создание сценариев для конвейера развертывания Kubernetes............................................................. 441 12.6. Заключительные рассуждения о конвейере сборки/развертывания................................................................... 442 Итоги.................................................................................................. 444 Приложение A. ........................................................................................... 445 Модель зрелости Ричардсона........................................................ 446 Spring HATEOAS............................................................................... 448 Внешняя конфигурация.................................................................. 448 Непрерывная интеграция и непрерывная доставка.................. 449 Мониторинг...................................................................................... 450 Журналирование.............................................................................. 451 API-шлюзы......................................................................................... 451 Содержание 15 Приложение B............................................................................................ 453 Тип разрешения: пароль................................................................. 454 Тип разрешения: учетные данные клиента................................. 455 Тип разрешения: код авторизации............................................... 456 Тип разрешения: неявный.............................................................. 459 Как обновляются токены................................................................ 461 Приложение C............................................................................................ 463 C.1. Введение в мониторинг с использованием Spring Boot Actuator.......................................................................... 464 C.1.1. Добавление зависимостей Spring Boot Actuator................... 464 C.1.2. Включение конечных точек Spring Boot Actuator................ 464 C.2. Настройка Micrometer и Prometheus..................................... 465 C.2.1. Введение в Micrometer и Prometheus...................................... 466 C.2.2. Интеграция с Micrometer и Prometheus............................... 467 C.3. Настройка Grafana.................................................................... 469 C.4. Итоги обсуждения..................................................................... 474 Предметный указатель........................................................................... 475 Ещё больше книг по Java в нашем телеграм канале: https://t.me/javalib Предисловие от издательства Отзывы и пожелания Мы всегда рады отзывам наших читателей. Расскажите нам, что вы думаете об этой книге, – что понравилось или, может быть, не понравилось. Отзывы важны для нас, чтобы выпускать книги, которые будут для вас максимально полезны. Вы можете написать отзыв на нашем сайте www.dmkpress.com, зайдя на страницу книги и оставив комментарий в разделе «Отзывы и рецензии». Также можно послать письмо главному редактору по адресу dmkpress@gmail.com; при этом укажите название книги в теме письма. Если вы являетесь экспертом в какой-либо области и заинтересованы в написании новой книги, заполните форму на нашем сайте по адресу http://dmkpress.com/authors/publish_book/ или напишите в издательство по адресу dmkpress@gmail.com. Список опечаток Хотя мы приняли все возможные меры для того, чтобы обеспечить высокое качество наших текстов, ошибки все равно случаются. Если вы найдете ошибку в одной из наших книг – возможно, ошибку в основном тексте или программном коде, – мы будем очень благодарны, если вы сообщите нам о ней. Сделав это, вы избавите других читателей от недопонимания и поможете нам улучшить последующие издания этой книги. Если вы найдете какие-либо ошибки в коде, пожалуйста, сообщите о них главному редактору по адресу dmkpress@gmail.com, и мы исправим это в следующих тиражах. Нарушение авторских прав Пиратство в интернете по-прежнему остается насущной проблемой. Издательство «ДМК Пресс» очень серьезно относится к вопросам защиты авторских прав и лицензирования. Если вы столкнетесь в интернете с незаконной публикацией какой-либо из наших книг, пожалуйста, пришлите нам ссылку на интернет-ресурс, чтобы мы могли применить санкции. Ссылку на подозрительные материалы можно прислать по адресу dmkpress@gmail.com. Мы высоко ценим любую помощь по защите наших авторов, благодаря которой мы можем предоставлять вам качественные материалы. Предисловие Эта книга – часть моей мечты внести свой вклад в развитие той области, которой я больше всего увлекаюсь, – информатики и, в частности, разработки программного обеспечения. Эти дисциплины демонстрируют свою исключительную важность во взаимосвязанном и глобальном настоящем. Мы ежедневно наблюдаем невероятные преобразования, которые они вызывают во всех сферах человеческой деятельности. Но зачем писать об архитектуре микросервисов, если есть много других достойных тем? Слово «микросервисы» имеет множество толкований. В этой книге я подразумеваю под микросервисами распределенные, слабо связанные программные службы, которые выполняют ограниченное количество четко определенных задач. Микросервисы стали появляться как альтернатива монолитным приложениям, помогающая бороться с традиционными проблемами сложности в большой кодовой базе, разбивая ее на мелкие, четко ограниченные части. В течение своей 13-летней карьеры я занималась разработкой программного обеспечения на разных языках и в разных программных архитектурах. Архитектуры, с которых я начинала, в настоящее время практически устарели. Современный мир заставляет нас постоянно двигаться вперед, и инновации в области разработки программного обеспечения развиваются ускоренными темпами. По этой причине, находясь в состоянии постоянного поиска новейших знаний и практик, несколько лет тому назад я решила окунуться в мир микросервисов. С тех пор я использовала эту архитектуру чаще других из-за ее преимуществ (таких как масштабируемость, скорость и удобство сопровождения). Успешный опыт работы в области микросервисов побудил меня взять на себя задачу написать эту книгу, чтобы систематизировать свои знания и поделиться ими с вами. Как разработчик программного обеспечения, я понимаю, насколько важно постоянно приобретать и применять новые знания. Прежде чем взяться за эту книгу, я решила поделиться своими выводами и начала публиковать статьи о микросервисах в блоге коста-риканской компании (на моей родине), занимающейся разработкой программного обеспечения. Когда я писала эти статьи, я поняла, что нашла новую страсть и цель в своей профессиональной карьере. Через несколько месяцев после публикации одной 18 Предисловие из моих статей я получила электронное письмо от издательства Manning Publications с предложением написать второе издание этой, которым и делюсь с вами сегодня. Первое издание этой книги было написано Джоном Карнеллом (John Carnell), непревзойденным профессионалом с многолетним опытом разработки программного обеспечения. Я написала это второе издание на основе той книги, добавив мои собственные видение и понимание. Второе издание «Микросервисов Spring» покажет вам, как реализовать разнообразные шаблоны проектирования, чтобы получить успешную архитектуру микросервисов с помощью Spring – фреймворка, предлагающего готовые решения для многих распространенных задач разработки, с которыми вы неизбежно столкнетесь как разработчик микросервисов. А теперь давайте начнем захватывающее путешествие в мир микросервисов со Spring. Благодарности Я глубоко благодарна за возможность поработать над этой книгой, которая позволила мне поделиться с вами моими знаниями и одновременно поучиться самой. Я благодарна издательству Manning Publications за доверие и за то, что позволили мне поделиться своим опытом со многими людьми. Особое спасибо Майклу Стивенсу (Michael Stephens) за то, что дал мне эту фантастическую возможность; Джону Карнеллу (John Carnell) за его поддержку, опыт и знания; Роберту Веннеру (Robert Wenner), моему научному редактору, за его ценный вклад; и Лесли Трайтс (Lesley Trites), моему редактору, за то, что она помогала и поддерживала меня на протяжении всего процесса. Я хочу сказать спасибо Стефану Пирнбауму (Stephan Pirnbaum) и Джону Гатри (John Guthrie), моим техническим рецензентам, которые проверяли мои рукописи и помогли поднять качество этой книги. Спасибо также моему редактору проекта Дейрдре Хиам (Deirdre Hiam); литературному редактору Френсису Бурану (Frances Buran); корректору Кэти Теннант (Katie Tennant); рецензирующему редактору Алексу Драгосавлевичу (Aleks Dragosavljevic) и всем-всем рецензентам (это: Адитья Кумар (Aditya Kumar), Аль Пезевски (Al Pezewski), Алекс Лукас (Alex Lucas), Арпит Ханделвал (Arpit Khandelwal), Бонни Малек (Bonnie Malec), Кристофер Карделл (Christopher Kardell), Дэвид Морган (David Morgan), Гилберто Таккари (Gilberto Taccari), Харинат Кунтамуккала (Harinath Kuntamukkala), Айан Кэмпбелл (Iain Campbell), Капил Дев С. (Kapil Dev S), Константин Еремин (Konstantin Eremin), Кшиштоф Камичек (Krzysztof Kamyczek), Марко Умек (Marko Umek), Мэтью Грин (Matthew Greene), Филипп Виалатт (Philippe Vialatte), ПьерМишель Ансель (Pierre-Michel Ansel), Рональд Борман (Ronald Borman), Сатей Кумар Саху (Satej Kumar Sahu), Стефан Пирнбаум 20 Благодарности (Stephan Pirnbaum), Тан Ви (Tan Wee), Тодд Кук (Todd Cook) и Виктор Дуран (Víctor Durán)) – ваши предложения помогли улучшить эту книгу. Хочу также поблагодарить моих маму, папу и всю мою семью, которые поддерживали и вдохновляли меня и которые, показывая пример преданности своей работе, помогли мне стать настоящим профессионалом. Я благодарна Илаю (Eli), который всегда был рядом со мной в те долгие дни работы, и моим друзьям, постоянно поддерживавшим меня на протяжении всего этого процесса. И последнее, но не в последнюю очередь: я благодарю каждого из вас за приобретение этой книги и за то, что позволили мне поделиться с вами своими знаниями. Надеюсь, вам понравится читать так же, как мне понравилось писать. Я надеюсь, что эта книга станет ценным вкладом в вашу профессиональную карьеру. Об этой книге Книга «Микросервисы Spring», второе издание написана для разработчиков на Java/Spring, которым нужны практические советы и примеры создания и ввода в эксплуатацию приложений на основе микросервисов. Работая над этой книгой, мы хотели сохранить центральную идею, заложенную в первое издание, – применение основных шаблонов микросервисов, соответствующих новейшим практикам и примерам Spring Boot и Spring Cloud. Практически в каждой главе вы найдете конкретные шаблоны проектирования микросервисов, а также примеры реализации Spring Cloud. Кому адресована эта книга Разработчикам на Java, имеющим некоторый опыт (1–3 года) создания распределенных приложений. Имеющим опыт (более 1 года) использования Spring. Желающим научиться создавать приложения на основе микросервисов. Интересующимся особенностями использования микросервисов для создания облачных приложений. Стремящимся узнать, являются ли Java и Spring подходящими технологиями для создания приложений на основе микросервисов. Всем, кому интересно узнать, что необходимо для развертывания приложения на основе микросервисов в облаке. Об этой книге 22 Структура книги Эта книга состоит из 12 глав и 3 приложений. Глава 1 знакомит с архитектурой микросервисов как важным и актуальным подходом к созданию приложений, особенно облачных. Глава 2 рассказывает о технологиях Spring Cloud, которые мы будем использовать, и описывает порядок создания облачных микросервисов с учетом передовых практик известного руководства «Приложение двенадцати факторов». В этой главе также приводится пример создания простого микросервиса на основе REST с помощью Spring Boot. Глава 3 показывает микросервисы с точки зрения архитектора, прикладного программиста и инженера DevOps, а также демонстрирует реализацию некоторых передовых методов в нашем первом микросервисе на основе REST. Глава 4 знакомит с миром контейнеров, подчеркивая основные различия между контейнерами и виртуальными машинами (ВМ). Она также показывает, как запускать микросервисы в контейнерах, используя плагины Maven и команды Docker. Глава 5 описывает особенности управления конфигурацией микросервисов с помощью Spring Cloud Config. Spring Cloud Config помогает организовать хранение конфигураций микросервисов в едином репозитории с учетом их версий и воспроизводить их во всех экземплярах микросервисов. Глава 6 знакомит с шаблоном маршрутизации обнаружения служб. Здесь вы узнаете, как использовать Spring Cloud и службу Netflix Eureka, чтобы обеспечить независимость местоположения ваших служб от клиентов, которые их используют. Вы также узнаете, как реализовать балансировку нагрузки на стороне клиента с помощью Spring Cloud LoadBalancer и клиента Netflix Feign. Глава 7 посвящена защите потребителей ваших микросервисов, когда один или несколько экземпляров микросервисов выходят из строя или находятся в нерабочем состоянии. В этой главе показано, как с помощью Spring Cloud и Resilience4j реализовать шаблоны размыкателя цепи (circuit breaker), отката к резервной реализации (fallback) и герметичных отсеков (bulkhead). Глава 8 охватывает шаблон шлюза маршрутизации (gateway routing). Здесь, используя Spring Cloud Gateway, мы создадим единую точку входа для всех наших микросервисов и увидим, как использовать фильтры Spring Cloud Gateway для создания политик, которые могут применяться ко всем службам, доступным через шлюз. Об этой книге 23 Глава 9 рассказывает, как реализовать аутентификацию и авторизацию с помощью Keycloak. Здесь рассматриваются некоторые основные принципы OAuth2 и способы использования Spring и Keycloak для защиты архитектуры микросервисов. Глава 10 рассматривает реализацию асинхронного обмена сообщениями между микросервисами с помощью Spring Cloud Stream и Apache Kafka. Она также показывает, как использовать Redis для кеширования запросов. Глава 11 показывает, как реализовать шаблоны журналирования, такие как корреляция журналов (log correlation), агрегирование журналов (log aggregation) и трассировка, с помощью Spring Cloud Sleuth, Zipkin и ELK Stack. Глава 12 является краеугольным камнем этой книги. Здесь мы соберем воедино все службы, создаваемые на протяжении всей книги, и развернем их в Amazon Elastic Kubernetes Service (Amazon EKS). Мы также обсудим автоматизацию процессов сборки и развертывания микросервисов с помощью таких инструментов, как Jenkins. Приложение A перечисляет дополнительные передовые практики строительства микросервисных архитектур и объясняет «Модель зрелости Ричардсона». Приложение B содержит дополнительные материалы по OAuth2. OAuth2 – чрезвычайно гибкая модель аутентификации, и в этом приложении дается краткий обзор различных способов использования OAuth2 для защиты приложений и микросервисов. Приложение C рассказывает, как контролировать микросервисы Spring Boot с помощью таких технологий, как Spring Boot Actuator, Micrometer, Prometheus и Grafana. В целом мы советуем всем разработчикам прочитать главы 1, 2 и 3, где приводится важная информация о передовых методах реализации микросервисов с использованием Spring Boot и Java 11. Если вы плохо знакомы с Docker, то мы настоятельно рекомендуем внимательно прочитать главу 4, которая кратко представляет все ключевые концепции Docker, используемые в книге. В остальной части книги обсуждаются некоторые шаблоны проектирования микросервисов, такие как обнаружение служб, распределенная трассировка, API-шлюз и др. Было бы желательно читать главы по порядку и опробовать примеры кода у себя. Если вам жалко тратить время на ввод кода примеров вручную, то можете загрузить его из репозитория GitHub по адресу https://github.com/ ihuaylupo/manning-smia. Об этой книге 24 О примерах кода Эта книга содержит много примеров исходного кода в виде листингов и фрагментов в обычном тексте. В обоих случаях исходный код оформляется моноширинным шрифтом, чтобы его можно было отличить от обычного текста. Код всех примеров доступен в репозитории GitHub https://github.com/ihuaylupo/manning-smia. Примеры хранятся в отдельных папках, по одной для каждой главы. Также обратите внимание, что весь код в этой книге создан для Java 11 и предполагает использование Maven в качестве основного инструмента сборки и Docker в качестве программной платформы контейнеризации. В каждой папке для каждой главы имеется файл README.md, где вы найдете следующую информацию: краткое введение в главу; перечень необходимых инструментов; раздел «How to Use», описывающий порядок использования; команду для сборки примеров; команду для запуска примеров; контакты и дополнительную информацию. Одна из основных концепций, которых мы старались придерживаться на протяжении всей книги, заключается в том, что примеры кода в каждой главе должны работать полностью независимо от других глав. Что это значит? Вы можете, например, взять код из главы 10 и запустить его, минуя примеры из предыдущих глав. Вы увидите, что для каждой службы, созданной в каждой главе, имеется соответствующий образ Docker. В каждой главе мы используем Docker Compose для запуска образов Docker, чтобы гарантировать воспроизводимость среды выполнения. Во многих случаях оригинальный исходный код был переформатирован; мы добавили переносы строк и изменили ширину отступов, чтобы уместить строки кода по ширине книжной страницы. В редких случаях даже этого оказалось недостаточно, поэтому в листингах вы увидите маркеры продолжения строки (➥). Кроме того, во многих листингах в книге, которые объясняются в тексте, мы убрали комментарии. Также многие листинги сопровождаются дополнительными аннотациями, подчеркивающими наиболее важные идеи. Живое обсуждение книги Приобретая книгу «Микросервисы Spring», второе издание, вы получаете бесплатный доступ к частному веб-форуму, организованному издательством Manning Publications, где можно оставлять коммен- Об этой книге 25 тарии о книге, задавать технические вопросы, а также получать помощь от автора и других пользователей. Чтобы получить доступ к форуму и зарегистрироваться на нем, откройте в веб-браузере страницу https://livebook.manning.com/book/spring-microservicesin-action-second-edition/discussion. Узнать больше о форумах Manning и познакомиться с правилами поведения можно по адресу https://livebook.manning.com/#!/discussion. Издательство Manning обязуется предоставить своим читателям место встречи, где может состояться содержательный диалог между отдельными читателями и между читателями и автором. Но со стороны авторов отсутствуют какие-либо обязательства уделять форуму какое-то определенное внимание – их присутствие на форуме остается добровольным (и неоплачиваемым). Мы предлагаем задавать авторам стимулирующие вопросы, чтобы их интерес не угасал! Форум и архив с предыдущими обсуждениями остается доступным на сайте издательства, пока книга продолжает издаваться. Об авторах Джон Карнелл (John Carnell) – архитектор программного обеспечения, возглавляет группу по взаимодействию с разработчиками в Genesys Cloud. Большую часть своего рабочего времени Джон учит клиентов Genesys Cloud и внутренних разработчиков, как развертывать облачные решения для контакт-центров и телефонии, а также рассказывает о передовых методах разработки на основе облачных технологий. Он занимается созданием микросервисов на базе телефонии с использованием платформы AWS. Его повседневная работа заключается в разработке и создании микросервисов для таких технологических платформ, как Java, Clojure и Go. Джон – одаренный оратор и писатель. Он регулярно выступает в местных группах пользователей и на симпозиуме The No Fluff Just Stuff Software Symposium. За последние 20 лет Джон был автором, соавтором и техническим рецензентом ряда книг по технологиям на основе Java и отраслевых публикаций. Имеет степень бакалавра, полученную в университете Маркетт и степень магистра делового администрирования, полученную в Висконсинском университете в городе Ошкош. Джон – увлеченный технолог, постоянно исследует новые технологии и языки программирования. Он живет в Кэри (Северная Каролина) со своей женой Джанет, тремя детьми (Кристофером, Агатой и Джеком) и – да, с собакой Вейдером. Иллари Уайлупо Санчес (Illary Huaylupo Sánchez) – инженерпрограммист, выпускница университета Сенфотек. Имеет также степень магистра делового администрирования в области управления ИТ, полученную в Латиноамериканском университете науки и технологий (ULACIT) в Коста-Рике. Имеет обширные познания 26 Об этой книге в области разработки программного обеспечения, опыт работы с Java и другими языками программирования, такими как Python, C#, Node.js, а также с другими технологиями, такими как различные базы данных, фреймворки, облачные службы и многое другое. В настоящее время Иллари работает старшим инженером-программистом в Microsoft, Сан-Хосе, Коста-Рика, где проводит большую часть своего времени, исследуя и разрабатывая множество современных проектов. В ее профессиональном портфолио имеется также 12-летний опыт работы в качестве сертифицированного разработчика Oracle и старшего инженера-программиста в крупных компаниях, таких как IBM, Gorilla Logic, Cargill и BAC Credomatic (престижный латиноамериканский банк). Иллари любит сложные задачи и всегда готова изучать новые языки программирования и новые технологии. В свободное время она любит играть на басгитаре и проводить время с семьей и друзьями. Связаться с Иллари можно по адресу illaryhs@gmail.com. Об иллюстрации на обложке На обложке «Микросервисы Spring» изображена иллюстрация, подписанная как «Человек из Хорватии». Она взята из недавнего переиздания книги Бальтазара Хакке (Balthasar Hacquet) «Images and Descriptions of Southwestern and Eastern Wenda, Illyrians, and Slavs», опубликованной этнографическим музеем в городе Сплит (Хорватия) в 2008 году. Хакке (1739–1815) – австрийский врач и ученый, много лет изучавший ботанику, геологию и этнографию Австрийской империи, а также Венето, Юлийских Альп и западных Балкан, населенных в прошлом народами иллирийских племен. Рисованные иллюстрации сопровождают многие научные статьи и книги Хаке. Богатое разнообразие рисунков в публикациях Хаке ярко свидетельствует об уникальности и индивидуальности восточных альпийских и северо-западных балканских регионов всего 200 лет назад. Это было время, когда по одежде легко можно было отличить жителей соседних деревень, разделенных всего несколькими милями, или принадлежность к разным социальным классам или профессиям. С тех пор стиль одежды сильно изменился, и исчезло разнообразие, характеризующее различные области и страны. В настоящее время трудно отличить по одежде даже жителей разных континентов, и жители живописных городков и деревень в словенских Альпах или прибрежных балканских городах почти не отличаются от жителей других частей Европы. Мы в издательстве Manning славим изобретательность, предприимчивость и радость компьютерного бизнеса обложками книг, изображающими богатство региональных различий двухвековой давности, оживших благодаря таким иллюстрациям, как эта. 1 Добро пожаловать в Spring Cloud Эта глава: знакомит с архитектурой микросервисов: рассказывает, почему компании используют микросервисы; демонстрирует приемы использования Spring, Spring Boot и Spring Cloud для создания микросервисов; описывает модели облачных вычислений. Внедрение любой новой архитектуры – непростая задача, сопряженная с множеством проблем, таких как масштабируемость приложений, обнаружение служб, мониторинг, распределенная трассировка, безопасность, управление и многих других. Эта книга познакомит вас с миром микросервисов Spring, научит решать все эти проблемы и покажет, какие компромиссы следует учитывать при реализации бизнес-приложений с использованием архитектуры микросервисов. В этой книге вы узнаете, как создавать приложения на основе микросервисов с использованием таких технологий, как Spring Cloud, Spring Boot, Swagger, Docker, Kubernetes, ELK (Elasticsearch, Logstash и Kibana), Stack, Grafana, Prometheus и др. Если вы разработчик на Java, то эта книга поможет вам плавно перейти от создания традиционных приложений на основе Spring к созданию приложений на основе микросервисов, которые можно развернуть в облаке. Здесь вы найдете практические примеры, диаграммы и подробное описание с дополнительными сведениями о реализации архитектуры микросервисов. 28 Глава 1 Добро пожаловать в Spring Cloud Прочитав эту книгу, вы научитесь применять на практике такие технологии и методы, как балансировка нагрузки на стороне клиентов, динамическое масштабирование, распределенная трассировка и многие другие, и создавать гибкие, современные и автономные бизнес-приложения на основе микросервисов с помощью Spring Boot и Spring Cloud. Вы также научитесь создавать свои собственные конвейеры сборки/развертывания, чтобы обеспечить непрерывную доставку и интеграцию с вашим бизнесом, применяя такие технологии, как Kubernetes, Jenkins и Docker. 1.1. Эволюция архитектуры микросервисов Программная архитектура охватывает все фундаментальные аспекты, определяющие структуру, работу и взаимодействие программных компонентов. Эта книга рассказывает, как создать архитектуру микросервисов, состоящую из слабо связанных программных служб, выполняющих узкий круг четко определенных задач и взаимодействующих посредством передачи сообщений по сети. Для начала рассмотрим различия между микросервисами и некоторыми другими распространенными архитектурами. 1.1.1. N-уровневая архитектура Одним из распространенных типов архитектуры корпоративного программного обеспечения является многоуровневая или n-уровневая архитектура. Приложения с этой архитектурой делятся на несколько уровней, каждый со своими обязанностями и функциями, такими как пользовательский интерфейс, службы, данные, тестирование и т. д. Например, при создании приложения создается отдельный проект или решение для пользовательского интерфейса, затем еще один для служб, еще один для уровня данных и т. д. В итоге объединение нескольких проектов дает целое приложение. В больших корпоративных системах n-уровневые приложения имеют множество преимуществ, в том числе: n-уровневые приложения позволяют четко разделить задачи и рассматривать такие элементы, как пользовательский интерфейс, данные и бизнес-логику по отдельности; команды разработчиков могут работать над различными компонентами независимо друг от друга; корпоративная архитектура хорошо изучена, поэтому относительно легко найти квалифицированных разработчиков для многоуровневых проектов. Но n-уровневые приложения имеют и недостатки: после внесения изменений в код приходится останавливать и повторно запускать все приложение; Эволюция архитектуры микросервисов 29 сообщения, как правило, курсируют вверх и вниз через уровни, что может быть неэффективным; рефакторинг большого многоуровневого приложения после развертывания может оказаться сложной задачей. Некоторые из тем, обсуждаемых в этой книге, относятся непосредственно к многоуровневым приложениям, однако мы в большей степени сосредоточимся на различениях между микросервисами и еще одной распространенной архитектурой, часто называемой монолитом. 1.1.2. Что такое монолитная архитектура? Многие веб-приложения небольшого и среднего размера создаются с использованием монолитной архитектуры . Монолитное приложение доставляется как единственный развертываемый программный артефакт. Все его компоненты – пользовательский интерфейс, бизнес-логика и логика доступа к базе данных – объединены в единое приложение и развертываются на сервере приложений. На рис. 1.1 показана базовая архитектура такого приложения. Каждая группа разрабатывает свою часть приложения со своими требованиями и потребностями. Сервер приложений Java (JBoss, WebSphere, WebLogic, Tomcat) Упаковка WAR-пакета MVC Конвейер непрерывной интеграции Группа разработки управления клиентами Службы Spring Единая база кода Данные Spring Веб-приложение на основе Spring Группа разработки управления финансами Группа разработки хранилища данных Группа разработки пользовательского интерфейса База данных База данных для финансов клиентов Оперативная база данных Все компоненты приложения имеют доступ ко всем источникам данных в приложении. Рис. 1.1. Монолитные приложения вынуждают несколько групп разработчиков синхронизировать дату развертывания, потому что их код необходимо собирать, тестировать и развертывать как единое целое 30 Глава 1 Добро пожаловать в Spring Cloud Конечно, приложение может быть развернуто как единое целое, но часто над одним таким приложением работает несколько групп разработчиков. Каждая группа отвечает за свою часть приложения, обычно ориентированную на конкретных клиентов. Например, представьте такой сценарий: у нас есть внутреннее приложение для управления взаимоотношениями с клиентами (Customer Relations Management, CRM), которое предполагает координацию действий нескольких групп разработки пользовательского интерфейса, логики управления клиентами, хранилища данных, логики управления финансами и, возможно, многих других. Сторонники микросервисной архитектуры часто отзываются о монолитных приложениях с негативным оттенком, но иногда монолитная архитектура является отличным выбором. Монолиты проще создавать и развертывать, чем более сложные многоуровневые или микросервисные архитектуры. Если сценарий использования четко определен и вряд ли изменится в будущем, то часто решение начать с монолита может оказаться не таким плохим. Однако с увеличением размеров и сложности приложения управлять монолитом становится все труднее. Любое изменение в монолитном приложении может иметь каскадный эффект, оказывая влияние на другие части приложения, что может существенно осложнить интеграцию и развертывание в промышленной системе. Наш третий вариант, микросервисная архитектура, предлагает большую гибкости и удобство сопровождения. 1.1.3. Что такое микросервис? Идея микросервисов изначально возникла в сообществе разработчиков программного обеспечения как прямой ответ на многие проблемы (как технические, так и организационные), связанные с попытками масштабирования больших монолитных приложений. Микросервис – это небольшая, слабо связанная распределенная служба. Микросервисы позволяют взять приложение с обширным набором функций и разложить его на простые в управлении компоненты с четко определенными обязанностями. Микросервисы помогают преодолевать традиционные проблемы сложности большой базы кода, разбивая ее на небольшие четко определенные части. Ключевые понятия, о которых следует помнить, рассуждая о микросервисах, – это декомпозиция и развязка (unbunding). Функции приложений должны быть полностью независимы друг от друга. Если взять упомянутое выше приложение CRM и разложить его на микросервисы, то получившийся результат мог бы выглядеть примерно так, как показано на рис. 1.2. Эволюция архитектуры микросервисов Конвейер непрерывной интеграции Микросервис управления клиентами Группа разработки управления финансами База кода управления финансами База кода Группа разработки управления клиентами управления клиентами Микросервис управления данными База кода Группа разработки хранилища данных управления данными Конвейер непрерывной интеграции Группа разработки пользовательского интерфейса База кода пользовательского интерфейса Пользовательский интерфейс веб-приложения Оперативная база данных Конвейер непрерывной интеграции База данных клиентов Микросервис управления финансами База данных для финансов Конвейер непрерывной интеграции 31 Обращается к бизнес-логике как к службам REST Рис. 1.2. При использовании микросервисной архитектуры приложение CRM разбивается на набор независимых микросервисов, что позволяет каждой группе разработчиков двигаться вперед в своем собственном темпе Как показано на рис. 1.2, каждая группа разработчиков единолично владеет кодом и инфраструктурой своей службы. Они могут собирать, развертывать и тестировать свой код независимо друг от друга, потому что репозиторий системы управления версиями и инфраструктура (сервер приложений и база данных) теперь полностью независимы от других частей приложения. Напомним характеристики микросервисной архитектуры: логика приложения разбита на мелкие компоненты с четко определенными согласованными границами ответственности; каждый компонент отвечает за узкий круг задач и развертывается независимо от других; один микросервис отвечает за одну часть предметной области; для обмена данными между собой микросервисы используют облегченные протоколы, такие как HTTP и JSON (JavaScript Object Notation – форма записи объектов JavaScript); приложения на основе микросервисов всегда обмениваются данными с использованием технологически нейтрального формата (чаще всего используется JSON), поэтому техническая реализация службы не имеет значения; это означает, что прило- Глава 1 Добро пожаловать в Spring Cloud 32 жение, состоящее из микросервисов, может быть написано на нескольких языках и с использованием нескольких технологий; микросервисы – благодаря небольшому размеру, независимому и распределенному характеру – позволяют организациям иметь небольшие группы разработчиков с четко определенными сферами ответственности. Эти группы могут работать над достижения единой цели, например над созданием приложения, но каждая несет ответственность только за те службы, над которыми они работают. На рис. 1.3 сравниваются монолитная и микросервисная архитектуры на примере типичного небольшого приложения электронной коммерции. Монолитная архитектура Приложение Службы управления продуктами, заказами и учетными записями находятся в одном приложении. Пользовательский интерфейс Микросервисная архитектура Пользовательский интерфейс API-шлюз Бизнеслогика Уровень доступа к данным База данных Служба управления продуктами Служба управления заказами В этом сценарии службы отделены друг от друга и каждая служба имеет свой уровень доступа к данным. Они могут пользоваться одной общей или отдельными базами данных. Служба управления учетными записями База данных Рис. 1.3. Сравнение монолитной и микросервисной архитектур 1.1.4. Зачем менять способ создания приложений? Компании, раньше обслуживавшие местные рынки, внезапно обнаружили, что могут использовать глобальную базу клиентов. Однако более широкой глобальной клиентской базе сопутствует мировая конкуренция. Усиление конкуренции влияет на подходы к разработке приложений. Например: возросшая сложность. Клиенты ожидают, что все подразделения организации будут знать, кто они есть. Но «изолированные» приложения, взаимодействующие с одной базой данных и не интегрирующиеся с другими приложениями, больше не яв- Эволюция архитектуры микросервисов 33 ляются нормой. Современные приложения должны взаимодействовать с множеством служб и баз данных, находящихся не только внутри компании, но и в других компаниях, оказывающих интернет-услуги; клиенты хотят быстрее получать обновления. Клиенты больше не хотят ждать выхода следующей ежегодной версии программного пакета. Они ожидают, что функции в программном продукте будут разделены и новые версии будут выпускаться быстро, в течение нескольких недель (или даже дней); клиентам также нужны высокая производительность и масштабируемость. Глобальные приложения чрезвычайно затрудняют прогнозирование количества транзакций, с которым сможет справиться приложение, и когда это количество будет достигнуто. Приложения должны быстро масштабироваться в ту или иную сторону, в зависимости от объема трафика; клиенты ожидают, что их приложения всегда будут доступны. Клиенты находятся на расстоянии одного щелчка от конкурента, поэтому приложения компании должны быть очень устойчивыми. Сбои или проблемы в одной части приложения не должны приводить к прекращению работы всего приложения. Чтобы оправдать эти ожидания, мы, как разработчики приложений, должны раскрыть тайну создания легко масштабируемых приложений с высокой степенью избыточности, которая заключается в том, чтобы разбить приложения на небольшие службы, которые можно создавать и развертывать независимо друг от друга. «Разбив» приложения на мелкие службы и переместив их из единого монолитного артефакта в распределенную среду, можно строить системы: гибкие – отдельные службы можно конструировать и реорганизовывать независимо друг от друга и быстро предоставлять новые возможности. Чем меньше единица служба, тем проще ее изменить и тем меньше времени уходит на ее тестирование и развертывание; устойчивые – приложение, разделенное на отдельные службы, больше не представляет собой единый «комок грязи», в котором выход из строя одной части приводит к сбою всего приложения. Сбои можно локализовать в небольшой части приложения и устранить до того, как приложение остановится. В случае неисправимой ошибки приложение может продолжать работать, оказывая более узкий круг услуг; масштабируемые – приложение, разделенное на отдельные службы, легко распределить по горизонтали между несколькими серверами и тем самым масштабировать функции/службы. В монолитном приложении, где вся логика взаимосвязана, 34 Глава 1 Добро пожаловать в Spring Cloud приложение должно масштабироваться целиком, даже если узким местом является лишь небольшая его часть. Масштабирование небольших служб проще и намного рентабельнее. Учитывая вышесказанное, начнем обсуждение микросервисов. И имейте в виду следующее: небольшие, простые и разделенные службы = масштабируемые, устойчивые и гибкие приложения. Важно понимать, что системы и организации могут извлечь выгоду из использования микросервисов. Чтобы организация могла получить преимущества, можем применить закон Конвея в обратном порядке. Этот закон указывает несколько моментов, которые могут улучшить коммуникацию и структуру компании. В законе Конвея (впервые был описан в апреле 1968 года Мелвином Р. Конвеем (Melvin R. Conway) в статье «How Do Committees Invent») говорится, что «организации, проектирующие системы... ограничены дизайном, который копирует структуру коммуникации в этой организации». По сути, это означает, что характер коммуникации внутри команды и между командами напрямую отражается в коде, который они создают. Если применить закон Конвея в обратном порядке (также известный как обратный маневр Конвея) и спроектировать структуру компании, опираясь на микросервисную архитектуру, то коммуникация, стабильность и организационная структура приложений улучшатся, если создать слабосвязанные и автономные группы для реализации микросервисов. 1.2. Микросервисы со Spring Spring стал самым популярным фреймворком разработки приложений на Java. Он основан на идее внедрения зависимостей. Инфраструктура внедрения зависимостей позволяет эффективнее управлять большими Java-проектами за счет оформления отношений между объектами в приложении через соглашения (и аннотации) вместо создания жестких связей. Spring располагается посередине между различными Java-классами в приложении и управляет их зависимостями. По сути, Spring позволяет собирать код подобно набору деталей конструктора лего. Что особенно впечатляет в фреймворке Spring и является свидетельством одаренности сообщества его разработчиков, так это ее способность оставаться актуальным и изобретать себя заново. Разработчики Spring быстро заметили нарастающую тенденцию ухода от монолитных приложений, в которых логика представления приложения, бизнес-логика и логика доступа к данным упакованы вместе и развертываются как единый артефакт, и перехода к рас- Микросервисы со Spring 35 пределенным моделям, в которых небольшие службы можно быстро развернуть в облаке. В ответ на эту тенденцию разработчики Spring запустили два проекта: Spring Boot и Spring Cloud. Spring Boot – это переосмысление фреймворка Spring. Поддерживая основные возможности Spring, Spring Boot лишился многих «корпоративных» функций, имеющихся в Spring, и вместо этого предоставляет возможность создания на Java микросервисов в архитектурном стиле REST (Representational State Transfer – передача репрезентативного состояния). С помощью нескольких простых аннотаций разработчик Java может быстро создать службу REST, упаковать и развернуть ее без использования внешнего приложения в роли контейнера. ПРИМЕЧАНИЕ. Мы более подробно рассмотрим архитектурный стиль REST в главе 3, однако уже сейчас можно отметить, что основная его идея заключается в том, что службы должны использовать HTTP-глаголы (GET, POST, PUT и DELETE) для представления своих основных действий и легковесный вебориентированный протокол сериализации данных, такой как JSON обмена данными. Вот некоторые ключевые особенности Spring Boot: встроенный веб-сервер, помогающий избежать сложностей при развертывании приложения: Tomcat (по умолчанию), Jetty или Undertow. Это один из важнейших компонентов Spring Boot; выбранный веб-сервер включается в развертываемый архив JAR. Единственное, что необходимо развертываемым приложениям Spring Boot, – это установить Java на сервере; предопределенная конфигурация для быстрого начала работы над проектом (для начинающих); автоматическая настройка возможностей Spring – когда это возможно; широкий спектр возможностей, готовых к использованию в промышленном окружении (таких как метрики, безопасность, проверка статуса, хранение конфигурации вовне и т. д.). Использование Spring Boot дает нашим микросервисам следующие преимущества: сокращает время разработки и увеличивает эффективность и производительность; предлагает встроенный HTTP-сервер для запуска вебприложений; позволяет избавиться от большого количества шаблонного кода; Глава 1 Добро пожаловать в Spring Cloud 36 упрощает интеграцию с экосистемой Spring (включая Spring Data, Spring Security, Spring Cloud и др.); предоставляет коллекцию различных плагинов для разработки. Поскольку микросервисы стали одним из наиболее распространенных архитектурных шаблонов создания облачных приложений, сообщество разработчиков Spring создали для нас Spring Cloud – фреймворк, упрощающий развертывание микросервисов в частном или общедоступном облаке. Spring Cloud объединяет несколько популярных фреймворков микросервисов для управления облаком, что позволяет использовать и развертывать эти технологии простым аннотированием кода. Мы рассмотрим различные компоненты Spring Cloud в следующей главе. 1.3. Что мы будем создавать? Эта книга предлагает пошаговое руководство по созданию полноценного приложения с микросервисной архитектурой на основе Spring Boot, Spring Cloud и других полезных и современных технологий. На рис. 1.4 показана общая структура интеграции некоторых служб и технологий, которые мы будем использовать на протяжении всей книги. На рис. 1.4 показан клиентский запрос на обновление и получение информации об организации в микросервисной архитектуре, которую мы создадим. Чтобы послать запрос, клиент сначала должен пройти аутентификацию с помощью Keycloak и получить токен доступа. Затем клиент посылает запрос в API-шлюз Spring Cloud. Служба API-шлюза – это точка входа в нашу архитектуру; она обращается к службе обнаружения Eureka, чтобы получить местоположение служб организации и лицензий, а затем вызывает конкретный микросервис. Получив запрос, служба организации проверяет с помощью Keycloak токен доступа на наличие соответствующих привилегий у пользователя. После проверки обновляет и извлекает информацию из базы данных организации и возвращает ее клиенту в форме HTTP-ответа. Дополнительно после обновления информации об организации служба добавляет сообщение в тему Kafka, чтобы уведомить службу лицензий об изменении. Получив сообщение, служба лицензий Redis сохраняет конкретную информацию в своей базе данных, размещенной в оперативной памяти. На протяжении всего этого процесса архитектура использует распределенную трассировку из Zipkin, Elasticsearch и Logstash для управления журналами и экспортирует и отображает метрики приложения с помощью Spring Boot Actuator, Prometheus и Grafana. О чем эта книга? 37 Репозиторий с конфигурацией службы Контейнер Docker ная лен ка е д в е ро пр Рас расси т База данных организации Обнаружение служб (Eureka) Retrieves data Служба организации Клиент Пе ре да ча Публикация Docker container в Logstash Тема Resilience4j Zipkin Sends to Docker container Ау те н API-шлюз (Spring Cloud Gateway) ти То фи ке ка н ци я Служба лицензий Resilience4j Сервер Keycloak База данных службы лицензий Запрос Подписка Elasticsearch М Docker container пр етри ил ож ки ен ия Извлечение данных Контейнер Docker Kibana Контейнер Docker Запрос Grafana Redis Spring Boot Actuator/ Prometheus Metrics Рис. 1.4. Общая структура сервисов и технологий, которые мы используем в этой книге в качестве примера Двигаясь вперед, мы познакомимся со всеми этими технологиями: Spring Boot, Spring Cloud, Elasticsearch, Logstash, Kibana, Prometheus, Grafana и Kafka и др. Они могут показаться сложными, но я буду постепенно показывать и рассказывать, как создавать и интегрировать различные компоненты, составляющие диаграмму на рис. 1.4. 1.4. О чем эта книга? Эта книга рассматривает широкий круг тем. Она охватывает все, от базовых определений до сложных реализаций микросервисной архитектуры. 1.4.1. Что вы узнаете в этой книге Эта книга посвящена созданию приложений на основе микросервисов с использованием проектов Spring, таких как Spring Boot и Spring Cloud, которые можно развернуть в частном облаке вашей компании или в общедоступном облаке, таком как Amazon, Google или Azure. В этой книге рассматриваются следующие темы: что такое микросервис, приемы и особенности проектирования, которые необходимо применять и учитывать при создании приложений на основе микросервисов; Глава 1 Добро пожаловать в Spring Cloud 38 когда не следует создавать приложение на основе микросервисов; как создавать микросервисы с помощью фреймворка Spring Boot; основные практики поддержки приложений на основе микросервисов и особенно облачных приложений; что такое Docker и как его интегрировать с приложением на основе микросервисов; как можно использовать Spring Cloud для реализации методов эксплуатации, описанных далее в этой главе; как создавать метрики приложения и отображать их в инструментах мониторинга; как организовать распределенную трассировку с помощью Zipkin и Sleuth; как управлять журналами приложений с помощью стека ELK; как, используя полученные знания, построить конвейер развертывания, который можно использовать для развертывания служб в частном или в общедоступном облаке. Прочитав эту книгу, вы обретете знания, необходимые для создания и развертывания микросервиса Spring Boot. Вы также познакомитесь с ключевыми архитектурными решениями, которые пригодятся вам для ввода в действие ваших микросервисов. Вы узнаете, как можно комбинировать управление конфигурацией служб, обнаружение служб, обмен сообщениями, журналирование и трассировку, а также безопасность для создания надежной среды микросервисов. Наконец, вы увидите, как можно развертывать микросервисы с использованием различных технологий. 1.4.2. Почему эта книга актуальна для вас? Я полагаю, что коль скоро вы дошли до этого момента, то это означает, что вы: разработчик на Java или хорошо разбираетесь в Java; имеете опыт использования Spring; желаете научиться создавать приложения на основе микросервисов; интересуетесь возможностью использовать микросервисы для создания облачных приложений; хотите узнать, подходят ли Java и Spring для создания приложений на основе микросервисов; хотите познакомиться с передовыми технологиями создания микросервисной архитектуры; хотите увидеть, как реализуется развертывание приложения на основе микросервисов в облаке. Облачные приложения и приложения на основе микросервисов 39 Эта книга подробно рассказывает о реализации архитектуры микросервисов на Java. Она предоставляет описательную и визуальную информацию и множество практических примеров кода, чтобы вы могли понять, как реализовать эту архитектуру с использованием последних версий различных проектов Spring, таких как Spring Boot и Spring Cloud. Кроме того, эта книга дает краткое введение в шаблоны проектирования микросервисов, передовой опыт и инфраструктурные технологии, которые идут рука об руку с архитектурой этого типа, моделируя реальную среду разработки приложений. Давайте на мгновение переключим наше внимание и рассмотрим создание простого микросервиса с использованием Spring Boot. 1.5.Облачные приложения и приложения на основе микросервисов В этом разделе вы увидите, как создать микросервис с помощью Spring Boot, и узнаете, почему облачные окружения являются наиболее подходящими для приложений на основе микросервисов. 1.5.1. Создание микросервиса с помощью Spring Boot В этом разделе я не буду подробно описывать код микросервиса, а только познакомлю с порядком создания службы, чтобы вы могли убедиться, насколько просто пользоваться Spring Boot. Для этого мы создадим простую службу REST «Hello World» с одной главной конечной точкой, реализующей глагол HTTP GET. Эта конечная точка будет получать параметры запроса и параметры URL (также известные как переменные пути). На рис. 1.5 показано, что будет делать эта служба и общий поток обработки запроса пользователя в микросервисе Spring Boot. Этот пример ни в коем случае не является исчерпывающим, и он не иллюстрирует порядок создания микросервисов промышленного уровня. Его цель состоит лишь в том, чтобы показать, как мало кода требуется для его написания. Мы не будем подробно обсуждать настройку файлов сборки проекта или детали кода – всему этому мы уделим должное внимание в главе 2. Желающие увидеть файл pom.xml для Maven и фактический код смогут найти все это в репозитории книги. ПРИМЕЧАНИЕ. Исходный код всех примеров из главы 1 доступен в репозитории GitHub книги по адресу https://github.com/ ihuaylupo/manning-smia/tree/master/chapter1. Глава 1 Добро пожаловать в Spring Cloud 40 Клиент отправляет HTTP-запрос, используя HTTP-глагол GET, включив имя в pathVariable и фамилию в requestParam HTTP-запрос GET к микросервису http://localhost:8080/hello/illary?lastname=huaylupo Код состояния HTTP: 200 HT Микросервис Hello твет TP-о { "message" : "Hello illary huaylupo" } Клиент получает ответ службы в формате JSON. Признак успеха или неудачи возвращается в виде кода состояния HTTP. Определив маршрут, Spring Boot преобразует параметры, найденные внутри маршрута, в аргументы метода Java, который выполнит необходимую работу. После преобразования всех данных Spring Boot выполняет бизнес-логику. Выбор маршрута Деструктуризация параметров Преобразование JSON -> JAVA Выполнение бизнес-логики Преобразование Java -> JSON Поток выполнения микросервиса Spring Boot Spring Boot анализирует HTTP-запрос и выбирает маршрут, опираясь на глагол HTTP, URL и параметры в URL. Маршрут отображается в метод в классе RestController. Для HTTP-запросов PUT и POST данные JSON передаются в теле HTTP-запроса и отображаются в класс Java. Выполнив бизнес-логику, Spring Boot преобразует объект Java в JSON. Рис. 1.5. Spring Boot абстрагирует общие задачи микросервиса REST (маршрутизация к бизнес-логике, синтаксический анализ параметров в URL, преобразование JSON в объекты Java и обратно) и позволяет разработчику сосредоточиться на бизнес-логике службы. На этом рисунке показаны три разных способа передачи параметров нашему контроллеру В этом примере имеется только один класс Java с именем Application, который находится в файле com/huaylupo/spmia/ch01/ SimpleApplication/Application.java. Мы будем использовать этот класс для предоставления конечной точки REST с именем /hello. Определение класса Application показано в листинге 1.1. Листинг 1.1. Служба Hello World с использованием Spring Boot: (очень) простой микросервис Spring Сообщает Spring Boot, что этот класс является точкой входа для службы Spring Boot. import import import import import import import import import org.springframework.boot.SpringApplication; org.springframework.boot.autoconfigure.SpringBootApplication; org.springframework.web.bind.annotation.GetMapping; org.springframework.web.bind.annotation.PathVariable; org.springframework.web.bind.annotation.PostMapping; org.springframework.web.bind.annotation.RequestBody; org.springframework.web.bind.annotation.RequestMapping; org.springframework.web.bind.annotation.RequestParam; org.springframework.web.bind.annotation.RestController; @SpringBootApplication @RestController @RequestMapping(value="hello") public class Application { Сообщает Spring Boot, что код в этом классе должен экспортироваться как Spring RestController. Все URL-адреса, поддерживаемые этим приложением, начинаются с префикса /hello. Облачные приложения и приложения на основе микросервисов 41 public static void main(String[] args) { SpringApplication.run(Application.class, args); } Экспортирует как конечную точку REST GET, которая принимает два параметра: firstName (через @PathVariable) и lastName (через @RequestParam). @GetMapping(value="/{firstName}") public String helloGET( @PathVariable("firstName") String firstName, @RequestParam("lastName") String lastName) { return String.format( "{\"message\":\"Hello %s %s\"}", firstName, lastName); } } class HelloRequest{ private String firstName; private String lastName; Содержит поля структуры JSON, отправленной пользователем. public String getFirstName() { return firstName; } Параметры firstName и lastName отображаются в две переменные, которые передаются в функцию hello. Возвращает простую строку JSON, которую мы создали вручную (в главе 2 мы не будем создавать данных в формате JSON). public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } } В листинге 1.1 экспортируется единственная конечная точка, обрабатывающая HTTP-запросы GET, которая принимает два параметра (firstName и lastName) в URL: один из переменной пути (@PathVariable), а другой из параметра запроса (@RequestParam). Конечная точка возвращает простую строку в формате JSON, содержащeм сообщение "Hello firstName lastName". Запрос GET к конечной точке /hello/illary?LastName=huaylupo к этой службе вернет: {“message”:”Hello illary huaylupo”} Давайте запустим приложение Spring Boot. Для этого выполним следующую команду в командной строке. Это команда Maven, она использует плагин Spring Boot, как указано в файле pom.xml, для запуска приложения с использованием встроенного сервера Tomcat. Когда вы выполните команду mvn spring-boot:run и приложе- 42 Глава 1 Добро пожаловать в Spring Cloud ние запустится, в окне терминала должны появиться строки, как показано на рис. 1.6. mvn spring-boot:run ПРИМЕЧАНИЕ. Перед запуском команды в терминале перейдите в корневой каталог проекта. Корневой каталог проекта – это каталог, в котором находится файл pom.xml. В противном случае вы столкнетесь с ошибкой: «No plugin found for prefix 'spring-boot' in the current project and in the plugin groups» (Не найден плагин для префикса 'spring-boot' в текущем проекте и в группах плагинов). Java против Groovy и Maven против Gradle Платформа Spring Boot поддерживает языки программирования Java и Groovy. Также Spring Boot поддерживает инструменты сборки Maven и Gradle. Gradle – это предметно-ориентированный язык (Domain Specific Language, DSL) на основе Groovy, и он используется для описания конфигурации проекта вместо XML, как в Maven. Язык Gradle – мощный, гибкий и пользуется большой популярностью, но сообщество разработчиков на Java по-прежнему использует Maven. Поэтому, чтобы не терять нити рассуждений и охватить максимально широкую аудиторию, все примеры в этой книге приводятся только для Maven. Встроенный сервер Версия Tomcat По умолчанию служба принимает HTTP-запросы на порту 8080. Рис. 1.6. Служба Spring Boot сообщает номер поддерживаемого порта в консоли Чтобы вызвать службу, нужен инструмент REST. Для обращения к службам REST существует множество инструментов как с графическим интерфейсом, так и с интерфейсом командной строки. В этой книге мы будем использовать Postman (https://www. getpostman.com/). На рис. 1.7 и 1.8 показаны два примера отправки разных запросов из Postman к конечным точкам и полученные в ответ результаты. Облачные приложения и приложения на основе микросервисов 43 На рис. 1.8 показано, как отправить HTTP-запрос POST. В данном случае этот запрос был отправлен только в демонстрационных целях. В следующих главах вы увидите, что метод POST предпочтительнее, когда служба должна создавать новые записи. Этот простой пример кода не демонстрирует ни всех возможностей Spring Boot, ни передовых методов создания служб. Зато он показывает, что для создания на Java полноценной службы HTTP JSON REST с маршрутизацией по URL и параметрам достаточно написать всего несколько строк кода. Java – мощный язык, но он приобрел репутацию чересчур многословного, по сравнению с другими языками. Однако благодаря Spring желаемого результата можно достичь всего с несколькими строками кода. А теперь давайте рассмотрим, в каких случаях предпочтительнее использовать микросервисы для создания приложений. HTTP запрос GET с URL /hello/illary?lastName=huaylupo Ответ службы в формате JSON Рис. 1.7. HTTP-запрос GET к конечной точке /hello и полученный ответ в формате JSON Глава 1 Добро пожаловать в Spring Cloud 44 HTTP-запрос POST с URL /hello Тело HTTP-запроса в формате JSON Ответ службы в формате JSON Рис. 1.8 HTTP-запрос POST к конечной точке /hello с телом запроса и полученным ответом в формате JSON 1.5.2. Что такое облачные вычисления? Облачные вычисления – это оказание вычислительных и виртуализированных ИТ-услуг – баз данных, сетей, программных продуктов, серверов, аналитики и много другого – через интернет для предоставления гибкого, безопасного и простого в использовании окружения. Облачные вычисления дают значительные выгоды компаниям, такие как низкие начальные инвестиции, простота использования и обслуживания, а также масштабируемость. Модели облачных вычислений позволяют пользователю выбирать уровень контроля над информацией и услугами, которые они предоставляют. Эти модели известны своими аббревиатурами обычно в формате XaaS, обозначающими нечто как услуга (anything as a service). Ниже перечислены наиболее распространенные модели облачных вычислений, а на рис. 1.9 показаны различия между этими моделями. Инфраструктура как услуга (Infrastructure as a Service, IaaS). Поставщик предоставляет инфраструктуру, которая позволяет получать доступ к вычислительным ресурсам, таким как серверы, хранилища и сети. В этой модели пользователь сам отвечает за все, что связано с обслуживанием инфраструктуры и масштабированием приложений. Примерами платформ IaaS могут служить AWS (EC2), Azure Virtual Machines, Google Compute Engine и Kubernetes. Контейнер как услуга (Container as a Service, CaaS). Промежуточная модель между IaaS и PaaS. Относится к форме виртуализации на основе контейнеров. В отличие от модели IaaS, где разработчик имеет полный контроль над виртуальной машиной, где развертывается служба, в модели CaaS вам предоставляется легковесный переносимый виртуальный контейнер (например, Docker). Поставщик облачных услуг запускает вирту- Облачные приложения и приложения на основе микросервисов 45 альный сервер, на котором выполняется контейнер, а также имеются инструменты для создания, развертывания, мониторинга и масштабирования контейнеров. Примерами платформ CaaS могут служить Google Container Engine (GKE) и Amazon Elastic Container Service (ECS). В главе 11 вы увидите, как разворачивать свои микросервисы в Amazon ECS. Платформа как услуга (Platform as a Service, PaaS). Эта модель предоставляет платформу и окружение, которые позволяют пользователям сосредоточиться на разработке, выполнении и обслуживании приложения. Приложения могут создаваться с помощью инструментов, предоставляемых поставщиком услуги (например, операционная система, системы управления базами данных, техническая поддержка, хранилище, хостинг, сеть и т. д.). Пользователям не нужно вкладывать средства в физическую инфраструктуру или тратить время на управление ею, что позволяет им сосредоточиться исключительно на разработке приложений. Примерами платформ PaaS могут служить Google App Engine, Cloud Foundry, Heroku и AWS Elastic Beanstalk. Функция как услуга (Function as a Service, FaaS). Также известна как бессерверная архитектура – несмотря на такое интересное название, эта архитектура не означает, что код выполняется без сервера. Эта модель предлагает возможность выполнения функций в облаке, которым поставщик услуги предоставляет все необходимые серверы. Бессерверная архитектура позволяет сосредоточиться исключительно на разработке служб, не беспокоясь о масштабировании, инициализации и администрировании серверов. То есть мы можем сосредоточиться только на наших функциях, не заботясь о какой-либо административной инфраструктуре. Примерами платформ FaaS могут служить AWS (Lambda), Google Cloud Function и Azure Functions. Программное обеспечение как услуга (Software as a Service, SaaS). Эта модель, также известная как программное обеспечение по запросу, позволяет пользователям использовать конкретное приложение без необходимости развертывать или обслуживать его. В большинстве случаев доступ к таким приложениям осуществляется через веб-браузер. Управление приложением, данными, операционной системой, виртуализацией, серверами, хранилищем и сетью осуществляет поставщик услуг. Пользователь просто приобретает услугу и использует программное обеспечение. Примерами платформ SaaS могут служить Salesforce, SAP и Google Business. Глава 1 Добро пожаловать в Spring Cloud 46 Локальные вычисления IaaS CaaS Функции Функции Функции Приложения Приложения Приложения Среда выполнения и контейнеры Среда выполнения и контейнеры Среда выполнения и контейнеры Операционные системы и инструменты управления Операционные системы и инструменты управления Операционные системы и инструменты управления Сеть, хранилища и серверы Сеть, хранилища и серверы Вычислительный центр Вычислительный центр Вычислительный центр PaaS FaaS SaaS Функции Функции Функции Сеть, хранилища и серверы Приложения Приложения Приложения Среда выполнения и контейнеры Операционные системы и инструменты управления Сеть, хранилища и серверы Среда выполнения и контейнеры Операционные системы и инструменты управления Сеть, хранилища и серверы Среда выполнения и контейнеры Операционные системы и инструменты управления Сеть, хранилища и серверы Вычислительный центр Вычислительный центр Вычислительный центр Сфера ответственности пользователя Сфера ответственности поставщика услуг Рис. 1.9. Различия между моделями облачных вычислений сводятся к разграничению сфер ответственности между пользователем и поставщиком облачных услуг ПРИМЕЧАНИЕ. Без должной осмотрительности при использовании платформы на основе FaaS можно оказаться привязанным к конкретному поставщику услуг облачных вычислений, потому что в таких случаях код развертывается в среде выполнения, зависящей от поставщика. Модель FaaS позволяет писать службы на распространенных языках программирования (Java, Python, JavaScript и т. д.), но вынуждает использовать конкретный базовый API поставщика услуг и движок времени выполнения. 1.5.3.В чем преимущества облачных вычислений и микросервисов? Одна из основных идей микросервисной архитектуры заключается в том, что каждая служба упаковывается и развертывается как отдельный и независимый артефакт. Экземпляры служб должны быстро запускаться, и каждый должен быть неотличим от другого. При разработке микросервиса вам рано или поздно придется решить, в каком из следующих источников будет развернута служба. Облачные приложения и приложения на основе микросервисов 47 Физический сервер. Можно создавать и развертывать микросервисы на физических машинах, но немногие организации делают это из-за ограничений, свойственных физическим серверам. Быстро увеличить емкость физического сервера невозможно, а горизонтальное масштабирование микросервиса на нескольких физических серверах может стать чрезвычайно дорогостоящим делом. Образы виртуальных машин. Одно из главных преимуществ микросервисов – возможность быстро запускать и останавливать экземпляры в ответ на события масштабирования и сбои. Виртуальные машины (ВМ) – это сердце и душа ведущих поставщиков облачных услуг. Виртуальный контейнер. Виртуальные контейнеры являются естественным продолжением образов виртуальных машин. В этом сценарии службы развертываются не в полноценной виртуальной машине, а в контейнерах Docker (или аналогичной контейнерной технологии) в облаке. Виртуальные контейнеры работают внутри виртуальной машины. Контейнерные технологии позволяют разделить одну виртуальную машину на серию автономных процессов, совместно использующих один и тот же образ. Микросервис можно упаковать в образ контейнера, а затем быстро развернуть и запустить несколько его экземпляров в частном или общедоступном облаке IaaS. Преимущество облачных микросервисов заключается в таком важном их свойстве, как эластичность. Поставщики облачных услуг способны быстро запускать новые виртуальные машины и контейнеры за считанные минуты. Если потребности в вычислительной мощности для ваших служб упадут, то вы можете уменьшить количество действующих контейнеров, чтобы избежать дополнительных затрат. Использование услуг облачных вычислений для развертывания микросервисов дает вашим приложениям значительно большую горизонтальную масштабируемость (добавление дополнительных серверов и экземпляров служб). Эластичность сервера также означает увеличение устойчивости ваших приложений. Если в одном из ваших микросервисов возникнут проблемы и произойдет сбой, то запуск новых экземпляров службы может обеспечить поддержание приложения в работоспособном состоянии достаточно долго, чтобы ваша группа разработчиков могла решить проблему. В этой книге все микросервисы и соответствующая инфраструктура будут развертываться в облаке на основе CaaS с использованием контейнеров Docker. Это обычная топология развертывания микросервисов. Вот наиболее типичные характеристики облачных услуг CaaS. Глава 1 Добро пожаловать в Spring Cloud 48 Упрощенное управление инфраструктурой. Поставщики облачных услуг CaaS дают вам возможность контролировать работу своих служб и запускать и останавливать службы с помощью простых вызовов API. Значительная горизонтальная масштабируемость. Поставщики облачных услуг CaaS позволяют быстро запустить один или несколько экземпляров службы. Эта означает, что вы можете быстро масштабировать службы и направлять трафик в обход неисправных серверов. Высокая избыточность за счет географического распределения. Провайдеры CaaS имеют несколько центров обработки данных. Развертывая свои микросервисы на платформе CaaS, вы можете получить более высокий уровень избыточности, чем может дать использование кластеров в одном центре обработки данных. Почему использование PaaS для микросервисов – плохая идея? Выше в этой главе было представлено пять типов облачных платформ: инфраструктура как услуга (IaaS), контейнер как услуга (CaaS), платформа как услуга (PaaS), функция как услуга (FaaS) и программное обеспечение как услуга (SaaS). В этой книге особое внимание уделяется созданию микросервисов с использованием модели CaaS. Некоторые провайдеры облачных услуг позволяют абстрагироваться от инфраструктуры развертывания микросервиса, однако в этой книге мы будем стараться сохранить независимость от выбора поставщика услуг и развертывать все части приложения (включая серверы). Например, Cloud Foundry, AWS Elastic Beanstalk, Google App Engine и Heroku дают возможность развертывать службы, не заботясь о базовом контейнере. Они предоставляют веб-интерфейс и интерфейс командной строки (Command-Line Interface, CLI), позволяющие развернуть приложение из файла WAR или JAR. Об установке и настройке сервера приложений и соответствующего Java-контейнера позаботится сам поставщик услуг. Это удобно, но платформа каждого поставщика облачных услуг имеет свои особенности, связанные с его индивидуальным решением PaaS. Службы, создаваемые в этой книге, будут упаковываться в контейнеры Docker; основная причина такого решения состоит в том, что Docker можно развернуть у всех ведущих поставщиков услуг облачных вычислений. В следующих главах вы узнаете, что такое Docker и как использовать эту технологию для запуска всех служб и инфраструктуры, представленных в этой книге. Микросервисы – это больше чем код 1.6. 49 Микросервисы – это больше чем код Идея создания отдельных микросервисов проста и понятна, но запуск и поддержка отказоустойчивого приложения на основе микросервисов (особенно в облаке) предполагают нечто большее, чем просто написание программного кода службы. На рис. 1.10 показаны некоторые рекомендации, которые следует учитывать при создании микросервиса. Оптимальный размер: выбирайте оптимальный размер для своих микросервисов чтобы не возложить на них слишком широкие обязанности Прозрачность местоположения: управляйте физическим местоположением, чтобы экземпляры службы можно было добавлять и удалять, не оказывая отрицательного влияния на клиентов Рекомендации по созданию микросервиса Устойчивость: гарантируйте быстрое переключение клиентов на другие экземпляры службы при появлении проблем в данном конкретном экземпляре Повторяемость: обеспечьте использование той же версии программного кода и конфигурации при запуске каждого нового экземпляра службы Масштабируемость: обеспечьте быстрое масштабирование приложений с минимальными зависимостями между службами Рис. 1.10. Микросервисы – это не только бизнес-логика. Вам нужно подумать об окружении, в котором будут выполняться службы, и как эти службы будут масштабироваться Чтобы получить надежную службу, необходимо учесть несколько аспектов. Давайте рассмотрим вопросы на рис. 1.10 более подробно. Оптимальный размер. Выбирайте оптимальный размер для своих микросервисов, чтобы не возложить на них слишком широкие обязанности. Помните, что выбор правильного размера служб позволяет быстро вносить изменения и снижает общий риск сбоя всего приложения. Прозрачность местоположения. Контролируйте физические детали вызова службы. В приложении на основе микросервисов несколько экземпляров службы могут быстро запускаться и завершаться. Глава 1 Добро пожаловать в Spring Cloud 50 Устойчивость. Защищайте пользователей ваших микросервисов и приложение в целом, организуя маршрутизацию в обход отказавших служб и используя решения «отказоустойчивости». Повторяемость. Гарантируйте использование одной и той же версии кода и конфигурации при запуске каждого нового экземпляра вашей службы. Масштабируемость. Организуйте взаимодействия так, чтобы свести к минимуму прямые зависимости между вашими службами и гарантировать возможность плавного масштабирования своих микросервисов. Для учета всех перечисленных рекомендаций в этой книге применяется подход, основанный на шаблонах. Используя этот подход, мы рассмотрим общие решения с применением разных технологий. Несмотря на то что для реализации шаблонов в этой книге мы решили использовать Spring Boot и Spring Cloud, ничто не помешает вам взять идеи, представленные здесь, и воплотить их на других технологических платформах. В частности, мы рассмотрим следующие шаблоны проектирования микросервисов: базовый шаблон разработки; шаблоны устойчивости клиентов; шаблоны журналирования и трассировки; шаблон сборки и развертывания; шаблоны маршрутизации; шаблоны безопасности; шаблоны сбора метрик приложений. Важно понимать, что нет формального свода правил создания микросервисов. В следующем разделе мы рассмотрим список общих аспектов, которые следует учитывать при создании микросервиса. 1.7. Базовый шаблон разработки микросервисов Базовый шаблон разработки микросервисов определяет самые основы. На рис. 1.11 перечислены темы, касающиеся проектирования служб, которые мы рассмотрим далее. Базовый шаблон разработки микросервисов 51 Веб-клиент Определяет, насколько широкие обязанности возлагаются на службу Степень детализации службы Протоколы связи Определяет, как будут экспортироваться конечные точки службы Определяет управление конфигурацией конкретного приложения, чтобы код и конфигурация оставались независимыми сущностями Определяют, как клиент и служба будут обмениваться данными Определяет порядок использования событий для передачи между службами информации о состоянии и изменениях в данных Архитектура интерфейса Управление конфигурацией Обработка событий Рис. 1.11. При проектировании микросервиса необходимо подумать о том, как эта служба будет использоваться Следующие шаблоны (перечисленные на рис. 1.11) определяют основы создания микросервисов. Степень детализации службы. Как правильно выполнить декомпозицию предметной области на микросервисы, чтобы каждый имел соответствующий круг обязанностей? Слишком широкий круг обязанностей, включающий разные задачи, затрудняет обслуживание и изменение службы с течением времени. Слишком мелкая детализация увеличивает общую сложность приложения и превращает службу в простой уровень абстракции данных без какой-либо логики, кроме той, которая необходима для доступа к хранилищу. О детализации служб мы поговорим в главе 3. Протоколы связи. Протоколы задают порядок взаимодействий с вашей службой. Первый шаг – выбор характера протокола, синхронный или асинхронный. Для синхронных взаимодействий чаще всего используется REST на основе HTTP с использованием XML (Extensible Markup Language – рас- Глава 1 Добро пожаловать в Spring Cloud 52 1.8. ширяемый язык разметки), JSON (JavaScript Object Notation – форма записи объектов JavaScript) или двоичных форматов, таких как Thrift, для обмена данными. Для асинхронных взаимодействий наиболее популярным выбором является AMQP (Advanced Message Queuing Protocol – расширенный протокол организации очередей сообщений), использующий очереди (для взаимодействий типа точка–точка) или темы (для взаимодействий типа публикация–подписка) с брокерами сообщений, такими как RabbitMQ, Apache Kafka и Amazon Simple Queue Service (SQS). С протоколами связи мы будем знакомиться в последующих главах. Архитектура интерфейса. Как лучше всего организовать интерфейс, который другие разработчики будут использовать для вызова вашей службы? Как структурировать оказываемые услуги? Какие приемы существуют? Подробнее методы и практики организации интерфейса рассматриваются в последующих главах. Управление конфигурацией службы. Как организовать управление конфигурацией микросервиса так, чтобы ее можно было перемещать между разными окружениями в облаке? Управление конфигурацией с использованием внешних служб и профилей мы рассмотрим в главе 5. Обработка событий между службами. Как с помощью событий минимизировать количество жестких зависимостей между службами и повысить отказоустойчивость приложения? Мы реализуем управляемую событиями архитектуру с использованием Spring Cloud Stream в главе 10. Шаблоны маршрутизации Шаблоны маршрутизации микросервисов определяют, как клиентское приложение обнаруживает местонахождение службы и отправляет ей запросы. В облачном приложении могут быть запущены сотни экземпляров микросервиса. Чтобы обеспечить соблюдение политик безопасности и выбора контента, необходимо абстрагировать физический IP-адрес служб и организовать единую точку входа. Решить эту задачу помогают следующие шаблоны. Обнаружение служб. С помощью функции обнаружения служб и ее ключевого компонента – реестра служб – можно сделать микросервис доступным для обнаружения клиентскими приложениями без жесткого определения местоположения службы в их коде. Как? Мы выясним это в главе 6. Помните, что механизм обнаружения служб – это внутренняя служба, а не служба, ориентированная на клиентов. Шаблоны маршрутизации 53 Обратите внимание, что в этой книге мы используем Netflix Eureka Service Discovery, но есть и другие реестры служб, такие как etcd, Consul и Apache Zookeeper. Кроме того, в некоторых системах вообще нет явного реестра служб. Вместо него они используют коммуникационную инфраструктуру, известную как сервисная сетка (service mesh). Маршрутизация служб. С помощью API-шлюза в приложениях на основе микросервисов можно организовать единую точку входа для всех служб и обеспечить единообразное применение политик безопасности и правил маршрутизации к нескольким службам и их экземплярам. Как? С помощью Spring Cloud API Gateway, как будет рассказываться в главе 8. На рис. 1.12 показано, что обнаружение и маршрутизация служб выполняются в четко определенной последовательности (сначала выполняется маршрутизация, а затем обнаружение службы). Однако эти две модели не зависят друг от друга. Например, можно реализовать обнаружение служб без маршрутизации и точно так же можно реализовать маршрутизацию служб без обнаружения (хотя реализация при этом усложнится). Веб-клиент Маршрутизация служб (API-шлюз) Обнаружение службы абстрагирует физическое местоположение службы, позволяя добавлять новые экземпляры микросервисов для масштабирования и предотвращать доступ к неработоспособным экземплярам Служба А (два экземпляра) 172.18.32.100 172.18.32.101 Маршрутизация служб позволяет дать клиенту единый логический URL и действует как точка применения различных правил и политик, таких как авторизация, аутентификация и проверка содержимого Обнаружение служб (Eureka) Служба Б (два экземпляра) 172.18.38.96 172.18.38.97 Рис. 1.12. Обнаружение и маршрутизация служб – ключевые элементы любого крупномасштабного приложения на основе микросервисов 54 1.9. Глава 1 Добро пожаловать в Spring Cloud Устойчивость клиентов Микросервисные архитектуры по определению имеют распределенный характер, поэтому следует очень внимательно относиться к предотвращению каскадного распространения проблем из одной службы (или экземпляра службы) к ее потребителям. С этой целью мы рассмотрим четыре модели организации устойчивости клиентов. Балансировка нагрузки на стороне клиента. Реализуется кешированием местоположений экземпляров службы и равномерным распределением трафика между всеми работоспособными экземплярами. Шаблон размыкателя цепи (Circuit Breaker). Не позволяет клиенту продолжать обращаться к службе, потерпевшей сбой или испытывающей проблемы с производительностью. Медленно работающая служба напрасно потребляет ресурсы вызывающего клиента, поэтому вызовы к такой службе должны быстро завершаться с признаком ошибки, чтобы клиент мог быстро среагировать и предпринять соответствующие действия. Шаблон отката к резервной реализации (fallback). На случай сбоя службы должен иметься «сменный» механизм, который даст возможность клиенту попытаться выполнить свою работу с помощью альтернативных средств, отличных от вызываемого микросервиса. Шаблон герметичных отсеков (Bulkhead). Приложения на основе микросервисов используют несколько распределенных ресурсов для выполнения своей работы. Этот шаблон упорядочивает вызовы к этим ресурсам так, чтобы сбой в одной службе не мог отрицательно сказаться на остальной части приложения. На рис. 1.13 показано, как эти шаблоны защищают клиента от некорректного поведения службы. Эти шаблоны рассматриваются в главе 7. Шаблоны безопасности Веб-клиент Шаблон размыкателя цепи (Circuit Breaker) препятствует повторному вызову неисправной службы. Вместо этого он обеспечивает быстрый отказ в обслуживании, чтобы защитить клиента Упорядочивает вызовы различных служб на стороне клиента так, чтобы сбой в одной службе не мог вызвать напрасное потребление ресурсов клиента Балансировка нагрузки на стороне клиента Размыкатель цепи Резервная реализация Герметичные отсеки Служба А (два экземпляра) 172.18.32.100 172.18.32.101 55 Клиент службы кеширует адреса конечных точек микросервисов, полученные от механизма обнаружения служб, и равномерно распределяет нагрузку между экземплярами На случай, если микросервис потерпит сбой, должен существовать альтернативный путь, воспользовавшись которым клиент сможет получить необходимые данные или предпринять какие-то другие действия Служба Б (два экземпляра) 172.18.38.96 172.18.38.97 Рис. 1.13. В приложениях на основе микросервисов вы должны защитить клиента от некорректного поведения службы. Помните, что медленная или неработающая служба может вызвать сбои, выходящие за рамки самой службы 1.10. Шаблоны безопасности Чтобы гарантировать защиту микросервисов от несанкционированного доступа, можно использовать следующие шаблоны безопасности, разрешающие выполнять запросы только обладателям надлежащих учетных данных. На рис. 1.14 показано, как можно реализовать эти три шаблона для организации службы аутентификации, которая защитит ваши микросервисы. Аутентификация. Помогает определить, что клиент, вызывающий службу, является тем, кем за кого себя выдает. Авторизация. Определяет, какие действия разрешено выполнять клиенту службы. Управление учетными данными и их распространение. Предотвращает необходимость постоянного предоставления клиентом своих учетных данных при обращении к службе. Добиться этого можно с использованием стандартов безопасности на основе токенов, таких как OAuth2 и JSON Web Tokens (JWT), суть которых заключается в получении токена, который можно включать в запросы к службе для аутентификации и авторизации пользователя. 56 Глава 1 Добро пожаловать в Spring Cloud Что такое OAuth 2.0? OAuth2 – это инфраструктура безопасности на основе токенов, которая позволяет пользователю аутентифицировать себя с помощью сторонней службы аутентификации. После успешной аутентификации пользователь получает токен, который должен пересылаться службе с каждым запросом. Основная цель OAuth2 заключается в том, чтобы при вызове нескольких служб для выполнения запроса пользователя каждая из этих служб могла аутентифицировать пользователя без предоставления его учетных данных. Мы будем рассматривать OAuth в главе 9, однако я советую дополнительно ознакомиться с документацией по OAuth 2.0 (https://www.oauth.com/), написанной Аароном Пареки (Aaron Parecki). Маршрутизация служб (API-шлюз) Web Client Когда пользователь пытается получить доступ к защищенной службе, он должен пройти процедуру аутентификации и получить токен Сервер аутентификации проверяет привилегии пользователя и удостоверяет представленные ему токены Обнаружение служб (Eureka) Служба аутентификации Защищенная служба Служба А (два экземпляра) 172.18.32.100 172.18.32.101 Владелец ресурса указывает, какие приложения/пользователи могут обращаться к ресурсу через службу аутентификации Рис. 1.14. Используя схему безопасности на основе токенов, можно реализовать аутентификацию и авторизацию клиента без передачи его учетных данных 1.11. Шаблоны журналирования и трассировки Обратной стороной архитектуры микросервисов является сложность отладки, трассировки и выявления проблем, потому что одно простое действие может породить многочисленные вызовы к микросервисам в приложении. В следующих главах мы расскажем, как реализовать распределенную трассировку с помощью Spring Cloud Sleuth, Zipkin и ELK Stack. При этом мы познакомимся с тремя основными шаблонами журналирования и трассировки. Шаблоны журналирования и трассировки 57 Корреляция журналов. Этот шаблон определяет порядок связывания воедино всех журналов, создаваемых службами для трассировки одной пользовательской транзакции. На примере этого шаблона мы рассмотрим реализацию идентификатора корреляции, который передается во все вызовы служб, участвующие в транзакции, и может использоваться для связывания записей в журналах, созданных службами. Агрегирование журналов. На примере этого шаблона мы посмотрим, как собрать воедино все журналы, созданные микросервисами (и их отдельными экземплярами), в единую базу данных, чтобы потом оценить характеристики производительности служб, вовлеченных в транзакцию. Трассировка микросервисов. На примере этого шаблона мы посмотрим, как визуализировать поток операций в клиентской транзакции, выполняемых всеми задействованными службами, чтобы оценить характеристики их производительности. На рис. 1.15 показано, как эти шаблоны сочетаются друг с другом. Мы рассмотрим шаблоны журналирования и трассировки в главе 11. Трассировка транзакций микросервисов: группы разработки и эксплуатации могут запрашивать данные из журнала, соответствующие отдельным транзакциям, и получать поток вызовов всех служб, задействованных в транзакции Микросервисы Служба A Служба B Агрегирование журналов: механизм агрегирования собирает все журналы из всех экземпляров службы Агрегатор журналов Служба C Служба D Служба E Служба G Корреляция журналов: все записи в журналах служб имеют идентификатор корреляции, который связывает эти записи с конкретной транзакцией База данных Визуальное представление Когда данные поступают в центральное хранилище, они индексируются и сохраняются в доступном для поиска формате Рис. 1.15. Хорошо продуманная стратегия журналирования и трассировки позволяет управлять отладкой транзакций в нескольких службах 58 Глава 1 Добро пожаловать в Spring Cloud 1.12. Шаблон сбора метрик приложения Шаблон сбора метрик описывает порядок извлечения метрик для мониторинга приложения и формирования предупреждений о возможных сбоях. Он определяет, как служба метрик осуществляет получение (извлечение), хранение и запрос бизнес-данных с целью предотвращения потенциальных проблем с производительностью в наших службах. Этот шаблон включает три основных компонента. Метрики. Фрагменты важной информации, описывающие состояние приложения и как эту информацию получить. Служба метрик. Запрашивает метрики у приложения и хранит их. Пакет визуализации метрик. Визуализирует данные, характеризующие состояние приложения и инфраструктуры. Как показано на рис. 1.16, метрики, генерируемые микросервисами, в значительной степени зависят от службы метрик и пакета визуализации. Бессмысленно генерировать метрики, отражающие бесконечный поток информации, если нет возможности отобразить и проанализировать эту информацию. Метрики могут пересылаться микросервисом автоматически или извлекаться по запросу службы метрик: в первом случае экземпляр микросервиса вызывает API службы метрик и передает ему данные о приложении; во втором случае служба метрик сама посылает запросы микросервисам для выборки данных о приложении. Важно понимать, что метрики являются важным элементом микросервисных архитектур, и требования к мониторингу в этих архитектурах обычно выше, чем в монолитных структурах, из-за их распределенного характера. аблоны сборки/развертывания микросервисов Сбор метрик может производиться двумя способами: передаваться микросервисами автоматически или по запросу из службы метрик 59 Обрабатывает предупреждения, отправленные службой метрик Микросервисы Служба A Служба B Диспетчер предупреждений ос е пр ни За луче к по етри м на Извлечение по запросу: служба метрик вызывает API службы, чтобы получить метрики приложения я ка к ес ри ич мет т ма а то ач Ав ред пе Служба D Служба E Служба G Хранилище И т. д. Этот пакет позволяет получать и визуализировать временные ряды для анализа состояния инфраструктуры и приложений. Он поддерживает информационные панели для отображения различных метрик в веб-браузере Служба метрик Служба C Электронная почта Пакет визуализации метрик Получение метрик путем автоматической их передачи: экземпляр службы вызывает API службы метрик и отправляет ему метрики приложения Рис. 1.16. Метрики извлекаются из микросервисов или передаются ими автоматически, собираются и хранятся в службе метрик, откуда потом они могут быть получены пакетом визуализации и инструментами управления предупреждениями 1.13.Шаблоны сборки/развертывания микросервисов Одно из основных требований архитектуры микросервисов заключается в том, что каждый экземпляр микросервиса должен быть идентичен всем другим его экземплярам. Нельзя допускать дрейфа конфигурации (когда что-то меняется в ней после развертывания экземпляра), потому что это может привести к нестабильному поведению приложения. Цель этого шаблона – интегрировать конфигурацию инфраструктуры прямо в процесс сборки/развертывания, чтобы вам больше не приходилось развертывать программные артефакты, такие как файлы WAR или EAR, в уже работающую часть инфраструктуры. Вместо этого процесс сборки должен создавать и компилировать микросервис и образ виртуального сервера для его выполнения. А процесс развертывания микросервиса должен развертывать образ всей машины с запущенным на ней сервером. 60 Глава 1 Добро пожаловать в Spring Cloud Рисунок 1.17 иллюстрирует эту идею. В конце книги мы покажем, как создать конвейер сборки/развертывания. В главе 12 мы рассмотрим следующие шаблоны и темы. Конвейеры сборки и развертывания. Помогает определить повторяемый процесс сборки и развертывания, в котором упор делается на сборку одной кнопкой и развертывание в любом окружении в вашей организации. Инфраструктура как код. Определяет отношение к службам как к коду, который может выполняться и управляться с помощью системы управления версиями. Неизменяемые серверы. Создание образа микросервиса позволяет гарантировать, что он никогда не изменится после развертывания. Серверы Phoenix. Гарантирует регулярное отключение и воссоздание серверов, на которых запущены отдельные контейнеры, из неизменяемого образа. Чем дольше работает сервер, тем вероятней появление отклонений от стандартной конфигурации. Дрейф конфигурации может возникнуть, если специальные изменения в конфигурации не фиксируются в системе управления версиями. Цель этих шаблонов – выявление и безжалостное искоренение дрейфа конфигурации еще до того, как он поразит промышленное окружение. ПРИМЕЧАНИЕ. В примерах кода в этой книге (кроме главы 12) все действия выполняются локально на настольном компьютере. Примеры из первых глав можно запускать непосредственно из командной строки. Но, начиная с главы 3, весь код должен компилироваться и запускаться в контейнерах Docker. Теперь, после краткого знакомства с шаблонами, которые мы будем использовать на протяжении всей книги, продолжим их обсуждение во второй главе. В следующей главе мы расскажем о технологиях Spring Cloud и о некоторых передовых методах разработки приложений, ориентированных на облачные микросервисы, а также сделаем первые шаги на пути к созданию нашего первого микросервиса с использованием Spring Boot и Java. аблоны сборки/развертывания микросервисов 61 Инфраструктура как код: мы создаем код и запускаем тесты для наших микросервисов. Однако к нашей инфраструктуре мы относимся как к коду. Когда микросервис компилируется и упаковывается, мы сразу же создаем виртуальный сервер или образ контейнера с установленным в него микросервисом Механизм сборки и развертывания Репозиторий исходного кода Разработчик Конвейер непрерывной интеграции/доставки Код готов Окружение разработки Окружение тестирования Модульное и интеграционное тестирование Созданы артефакты времени выполнения Образ м ы машины Образ отправлен в репозиторий Запуск платформенных тестов Развертывание нового образа/сервера Запуск платформенных тестов Развертывание нового образа/сервера Запуск платформенных тестов Промышленное окружение Развертывание нового образа/сервера Серверы Phoenix: поскольку фактические серверы постоянно останавливаются в рамках процесса непрерывной интеграции, выполняется запуск и остановка новых серверов. Это значительно снижает вероятность дрейфа конфигурации между окружениями Неизменяемые серверы: после создания и развертывания образа ни один разработчик или системный администратор не имеет права вносить изменения в серверы. При переносе между окружениями весь контейнер или образ запускается со специфическими для среды переменными, которые передаются серверу при первом запуске Рис. 1.17. Микросервис и сервер, на котором он выполняется, должны развертываться атомарно, как единый артефакт во всех окружениях Итоги В монолитных архитектурах все процессы тесно связаны и работают как единая служба. Микросервисы – это очень маленькие программные компоненты, отвечающие за узкий круг задач. Spring Boot позволяет создавать архитектуры обоих типов. Монолитные архитектуры обычно идеально подходят для создания простых и легковесных приложений, а микросервисные архитектуры – для сложных и постоянно развивающихся приложений. В конечном итоге выбор архитектуры, кроме прочих факторов, зависит от размера проекта, времени и требований. Глава 1 Добро пожаловать в Spring Cloud 62 Фреймворк Spring Boot упрощает создание микросервисов на основе REST/JSON. Его цель – дать возможность быстро создавать микросервисы, написав лишь нескольких аннотаций. Писать микросервисы легко, но создание полноценных реализаций, пригодных для использования в промышленном окружении, требует дополнительных усилий. Существует несколько категорий шаблонов разработки микросервисов, включая базовые шаблоны разработки, шаблоны маршрутизации, устойчивости клиентов, безопасности, сбора метрик приложений и сборки/развертывания. Шаблоны маршрутизации определяют, как клиентское приложение обнаружит службу и обратится к ней. Чтобы предотвратить каскадное распространение проблемы в экземпляре службы и за ее пределы, используйте шаблоны устойчивости клиентов. К ним относятся шаблон размыкателя цепи (Circuit Breaker), предотвращающий передачу запросов отказавшей службе; шаблон отката к резервной реализации (Fallback), добавляющий альтернативные пути для получения данных или выполнения определенного действия при сбое основной службы; шаблон балансировки нагрузки на стороне клиента для масштабирования и устранения всех возможных узких мест; а также шаблон герметичных отсеков (Bulkhead) для ограничения количества одновременных вызовов службы, чтобы исключить отрицательное влияние низкопроизводительных запросов на другие службы. OAuth 2.0 – наиболее распространенный протокол авторизации пользователей и отличный выбор для защиты микросервисной архитектуры. Шаблон сборки/развертывания позволяет интегрировать конфигурацию вашей инфраструктуры прямо в процесс сборки/развертывания, чтобы вам больше не приходилось развертывать программные артефакты, такие как файлы WAR или EAR, в уже работающую часть инфраструктуры. 2 Обзор мира микросервисов через призму Spring Cloud Эта глава: знакомит с технологиями Spring Cloud; описывает принципиальные отличия облачных приложений; рассказывает о методологии приложения двенадцати факторов; демонстрирует создание микросервисов с использованием Spring Cloud Проектирование, реализация и сопровождение микросервисов быстро превращаются в проблему, если этими процессами управлять неправильно. В работе с микросервисными решениями важно применять передовые методы, чтобы добиться максимальной производительности, избежать узких мест, не допустить появления проблем во время эксплуатации и чтобы архитектура оставалась максимально эффективной и масштабируемой. Следование передовым методам также помогает новым разработчикам быстрее осваивать наши системы. Продолжая обсуждение архитектур микросервисов, важно иметь в виду следующее: 64 Глава 2 Обзор мира микросервисов через призму Spring Cloud чем более распределена система, тем больше в ней мест, где может произойти сбой. Под этим подразумевается, что в микросервисной архитектуре часто намного больше точек отказа, потому что вместо одного монолитного приложения мы имеем целую экосистему из нескольких отдельных служб, взаимодействующих друг с другом. Это основная причина появления различных сложностей в администрировании и синхронизации, с которыми приходится сталкиваться разработчикам при создании приложений или архитектур на основе микросервисов. Чтобы избежать возможных точек отказа, мы будем использовать Spring Cloud. Фреймворк Spring Cloud предлагает ряд функций (регистрации и обнаружения служб, размыкания цепи, мониторинга и др.), позволяющих быстро создавать микросервисные архитектуры с минимальными конфигурациями. Эта глава дает краткое введение в технологии Spring Cloud, которые мы будем использовать далее. Это лишь общий обзор, но далее в книге по мере применения тех или иных технологий мы будем сообщать вам более подробную информацию о каждой из них. Поскольку в следующих главах мы будем заниматься разработкой микросервисов, очень важно понимать концепцию, преимущества и шаблоны разработки микросервисов. 2.1. Что такое Spring Cloud? Реализация с нуля всех шаблонов, перечисленных в первой главе, потребовала бы от нас огромных усилий. К счастью, команда Spring объединила большое количество протестированных проектов с открытым исходным кодом в один подпроект Spring, известный под общим названием Spring Cloud (https://projects.spring.io/ spring-cloud/). Spring Cloud – это коллекция инструментов, объединяющая разработки с открытым исходным кодом многих компаний, таких как VMware, HashiCorp и Netflix. Spring Cloud упрощает установку и настройку наших проектов и предоставляет реализации шаблонов, особенно часто встречающихся в приложениях на основе Spring. Благодаря этой коллекции мы можем сосредоточиться на разработке своего кода, оставив в стороне детали настройки всей инфраструктуры, которая используется для создания и развертывания микросервисов. На рис. 2.1 показаны шаблоны, перечисленные в предыдущей главе, и их деление на проекты в Spring Cloud. Что такое Spring Cloud? Шаблоны разработки Шаблоны маршрутизации Шаблоны сборки и развертывания Service discovery patterns Spring Cloud Discovery/ Netflix Eureka Управление конфигурацией Spring Cloud Config 65 Шаблоны устойчивости клиентов Балансировка нагрузки на стороне клиента Spring Cloud Load Balancer Шаблон размыкателя цепи Resilience4j Шаблоны маршрутизации служб Spring Cloud API Gateway Шаблон резервной реализации Resilience4j Шаблоны сборки и развертывания Шаблоны журналирования Шаблоны безопасности Шаблоны обнаружения служб Jenkins Агрегирование журналов Spring Cloud Sleuth Авторизация Spring Cloud Security/OAuth2 Инфраструктура как код Docker Трассировка микросервисов Spring Cloud Sleuth (ELK Stack) Аутентификация Spring Cloud Security/OAuth2 Неизменяемые серверы Docker Трассировка микросервисов Spring Cloud Sleuth/Zipkin Управление учетными данными и их распространение Spring Cloud Security/OAuth2/JWT Асинхронный обмен сообщениями Spring Cloud Stream Шаблон герметичных отсеков Resilience4j Рис. 2.1. Spring Cloud предоставляет технологии, реализующие шаблоны, с которыми мы познакомились к настоящему моменту 2.1.1. Spring Cloud Config Spring Cloud Config помогает организовать управление конфигурационными данными приложения через централизованную службу, что позволяет четко отделить конфигурацию приложения (в частности, настройки окружения) от развернутого микросервиса. Благодаря такому подходу все экземпляры микросервиса, независимо от их количества, будут иметь одинаковую конфигурацию. Spring Cloud Config имеет свой репозиторий управления свойствами, а также интегрируется со многими проектами с открытым исходным кодом, такими как: Git (https://git-scm.com/). Система управления версиями с открытым исходным кодом, которая позволяет контролировать изменения в любых текстовых файлах. Spring Cloud Config может интегрироваться с репозиторием Git и извлекать из него конфигурации приложений; Consul (https://www.consul.io/). Система обнаружения служб с открытым исходным кодом, которая позволяет регистрировать в ней экземпляры служб. Клиенты могут обращаться к системе Consul, чтобы узнать местоположение экземпляров нужных им служб. Также Consul имеет свое хранилище пар ключ/значение, в котором Spring Cloud Config может хранить конфигурации приложений; 66 Глава 2 Обзор мира микросервисов через призму Spring Cloud Eureka (https://github.com/Netflix/eureka). Проект Netflix с открытым исходным кодом, который, как и Consul, предлагает аналогичные возможности обнаружения служб. В Eureka тоже есть свое хранилище пар ключ/значение, которое может использоваться Spring Cloud Config. 2.1.2. Spring Cloud Service Discovery Шаблон Spring Cloud Service Discovery помогает организовать обнаружение служб клиентами независимо от физического местоположения (IP-адреса и/или имени) ваших серверов. Клиенты вызывают бизнес-логику, используя логическое имя вместо физического адреса. Spring Cloud Service Discovery также предусматривает регистрацию экземпляров службы при их запуске и отмену регистрации по завершении работы. Реализовать Spring Cloud Service Discovery можно с помощью следующих служб: Consul (https://www.consul.io/); Zookeeper (https://spring.io/projects/spring-cloud-zookeeper); Eureka (https://github.com/Netflix/eureka) в качестве механизма обнаружения служб. ПРИМЕЧАНИЕ. Проекты Consul и Zookeeper обладают весьма широкими возможностями, однако сообщество разработчиков на Java по-прежнему предпочитает использовать Eureka. В примерах в этой книге мы будем использовать Eureka, чтобы не распыляться и охватить как можно большую аудиторию. Тем, кого интересует Consul или Zookeeper, я советую заглянуть в приложения C и D. В приложении C вы найдете пример использования Consul для обнаружения служб, а в приложении D – пример использования Zookeeper. 2.1.3. Spring Cloud LoadBalancer и Resilience4j Spring Cloud прекрасно интегрируется с несколькими проектами с открытым исходным кодом. В частности, Spring Cloud обертывает библиотеку Resilience4j и проект Spring Cloud LoadBalancer, упрощая реализацию шаблонов устойчивости клиентов. Библиотеку Resilience4j можно найти по адресу: https://github.com/ resilience4j/resilience4j. С помощью библиотеки Resilience4j можно быстро реализовать такие шаблоны отказоустойчивости клиента, как размыкатель цепи (Circuit Breaker), повторные попытки (Retries), герметичные отсеки (Bulkhead) и др. Проект Spring Cloud LoadBalancer не только упрощает интеграцию с агентами обнаружения служб, такими как Eureka, но также обеспечивает балансировку нагрузки на стороне клиента. Это позволяет клиенту продолжать обращаться к службе, даже если агент обнаружения временно недоступен. Что такое Spring Cloud? 67 2.1.4. Spring Cloud API Gateway API Gateway предоставляет возможность маршрутизации служб в приложениях на основе микросервисов. Как следует из названия (API Gateway – API-шлюз), это – шлюз, передающий запросы службам и служащий единой точкой входа в ваши микросервисы. Такая централизация позволяет применять стандартные политики, такие как авторизация, аутентификация, фильтрация содержимого и правила маршрутизации. API-шлюз можно реализовать с помощью Spring Cloud Gateway (https://spring.io/projects/spring-cloudgateway). ПРИМЕЧАНИЕ. В этой книге мы используем Spring Cloud API Gateway, построенный на основе проектов Spring Framework 5 Project Reactor (поддерживающего интеграцию со Spring Web Flux) и Spring Boot 2. 2.1.5. Spring Cloud Stream Spring Cloud Stream (https://cloud.spring.io/spring-cloud-stream) – это технология, позволяющая интегрировать легковесную обработку сообщений в микросервисы. С помощью Spring Cloud Stream можно создавать интеллектуальные микросервисы, использующие асинхронные события, которые происходят в вашем приложении. Также Spring Cloud Stream позволяет быстро интегрировать микросервисы с брокерами сообщений, такими как RabbitMQ (https://www.rabbitmq.com) и Kafka (http://kafka.apache.org). 2.1.6. Spring Cloud Sleuth Spring Cloud Sleuth (https://cloud.spring.io/spring-cloud-sleuth/) позволяет интегрировать уникальные идентификаторы трассировки в HTTP-вызовы и каналы сообщений (RabbitMQ, Apache Kafka), используемые в приложении. Идентификаторы трассировки, которые иногда называют идентификаторами корреляции, помогают отслеживать прохождение транзакций через различные службы. Spring Cloud Sleuth автоматически добавляет идентификаторы трассировки в любые операции журналирования, выполняемые в микросервисе. Достоинства Spring Cloud Sleuth особенно ярко проявляются в сочетании с инструментами агрегирования журналов, такими как ELK Stack (https://www.elastic.co/what-is/elk-stack), и трассировки, такими как Zipkin (http://zipkin.io). Open Zipkin принимает данные, созданные Spring Cloud Sleuth, и визуализирует поток вызовов служб, задействованных в данной транзакции. ELK Stack – это аббревиатура, составленная из первых букв названий трех проектов с открытым исходным кодом: 68 Глава 2 Обзор мира микросервисов через призму Spring Cloud Elasticsearch (https://www.elastic.co) – поисковая и аналитическая система; Logstash (https://www.elastic.co/products/logstash) – конвейер обработки данных на стороне сервера, который принимает данные и преобразует их для отправки в «хранилище» (stash); Kibana (https://www.elastic.co/products/kibana) – клиентский интерфейс, с помощью которого пользователи могут запрашивать и визуализировать данные всего стека. 2.1.7. Spring Cloud Security Spring Cloud Security (https://cloud.spring.io/spring-cloudsecurity/) – это фреймворк аутентификации и авторизации, управляющий доступом к вашим службам и операциям. Принцип действия Spring Cloud Security основан на токенах, что позволяет службам взаимодействовать друг с другом, используя токены, выданные сервером аутентификации. Каждая служба, получив HTTP-запрос, может проверить предоставленный токен, чтобы подтвердить личность пользователя и его привилегии. Spring Cloud Security также поддерживает веб-токены JSON Web Tokens (JWT). JWT (https://jwt.io) определяет стандартный формат для создания токена OAuth2 и нормализует цифровые подписи для сгенерированного токена. 2.2. Пример использования Spring Cloud В предыдущем разделе мы кратко описали различные технологии Spring Cloud, которые вы можете использовать для создания своих микросервисов. Каждая из этих технологий является независимой службой, поэтому для объяснения всех тонкостей их использования потребуется не одна глава. И тем не менее мы хотим представить вашему вниманию небольшой пример кода, который наглядно покажет, насколько легко эти технологии интегрируются в микросервисы. В отличие от первого примера в листинге 1.1 вы не сможете запустить этот пример, потому что сначала нужно установить и настроить ряд вспомогательных служб. Но не волнуйтесь; затраты на установку этих служб Spring Cloud являются единовременными. После установки и настройки ваши микросервисы смогут использовать эти возможности снова и снова. Разумеется, невозможно продемонстрировать все достоинства в единственном примере, поэтому следующий листинг показывает, как организовать обнаружение служб и балансировку нагрузки на стороне клиента в нашем примере Hello World. Пример использования Spring Cloud 69 Листинг 2.1. Реализация службы Hello World с использованием Spring Cloud package com.optima.growth.simpleservice; import import import import import import import import import import org.springframework.boot.SpringApplication; org.springframework.boot.autoconfigure.SpringBootApplication; org.springframework.cloud.netflix.eureka.EnableEurekaClient; org.springframework.http.HttpMethod; org.springframework.http.ResponseEntity; org.springframework.web.bind.annotation.PathVariable; org.springframework.web.bind.annotation.RequestMapping; org.springframework.web.bind.annotation.RequestMethod; org.springframework.web.bind.annotation.RestController; org.springframework.web.client.RestTemplate; @SpringBootApplication @RestController @RequestMapping(value="hello") @EnableEurekaClient public class Application { Сообщает службе о необходимости зарегистрироваться в агенте обнаружения служб Eureka public static void main(String[] args) { SpringApplication.run(ContactServerAppApplication.class, args); } public String helloRemoteServiceCall(String firstName,String lastName){ RestTemplate restTemplate = new RestTemplate(); ResponseEntity<String> restExchange = restTemplate.exchange( "http://logical-service-id/name/" + "{firstName}/ {lastName}", HttpMethod.GET, null, String.class, firstName, lastName); Использует декорированный return restExchange.getBody(); класс RestTemplate для полу} чения «логического» идентификатора службы, по которому Eureka определит физическое местоположение @RequestMapping(value="/{firstName}/{lastName}", method = RequestMethod.GET) public String hello(@PathVariable("firstName") String firstName, @PathVariable("lastName") String lastName) { return helloRemoteServiceCall(firstName, lastName); } } В этом коде много интересного, поэтому рассмотрим его подробнее. Имейте в виду, что этот листинг является лишь демонстрацией и его нет в репозитории GitHub. Мы включили его, только чтобы вы могли получить представление о том, что будет дальше в книге. 70 Глава 2 Обзор мира микросервисов через призму Spring Cloud Прежде всего обратите внимание на аннотацию @EnableEurekaClient. Эта аннотация сообщает микросервису о необходимости регистрации в агенте обнаружения служб Eureka, который мы будем использовать для поиска конечных точек удаленной службы REST. Отметьте также, что конфигурация извлекается из файла свойств, в котором определяется адрес и номер порта сервера Eureka, с которым можно связаться. Второе, на что следует обратить внимание, – происходящее внутри метода helloRemoteServiceCall. Аннотация @EnableEurekaClient сообщает Spring Boot о необходимости включить поддержку клиента Eureka. Важно подчеркнуть, что эта аннотация является необязательной, если в файле pom.xml уже определена зависимость springcloud-starter-netflix-eureka-client. Класс RestTemplate позволяет передать логический идентификатор службы, которую вы пытаетесь вызвать, например: ResponseEntity<String> restExchange = restTemplate.exchange (http://logical-service-id/name/{firstName}/{lastName} За кулисами класс RestTemplate соединяется со службой Eureka и с ее помощью определяет адрес одного или нескольких экземпляров искомой службы. Вашему коду как клиенту службы не нужно знать, где находится эта служба. Класс RestTemplate также использует библиотеку Spring Cloud LoadBalancer. Эта библиотека извлекает список всех физических конечных точек, связанных со службой и, каждый раз, когда клиент вызывает службу, выполняет «циклический перебор» и посылает вызов разным экземплярам, что избавляет от необходимости использовать централизованный балансировщик нагрузки. Избавившись от централизованного балансировщика и переместив балансировку нагрузки на сторону клиента, вы устраняете еще одну точку отказа в прикладной инфраструктуре. Мы надеемся, что нам удалось произвести на вас впечатление: мы добавили в микросервис большое количество новых возможностей, вставив лишь несколько аннотаций. В этом настоящая красота Spring Cloud. Вы, как разработчик, можете воспользоваться возможностями, реализованными и проверенными ведущими облачными компаниями, такими как Netflix и Consul. Благодаря Spring Cloud для этого достаточно лишь добавить несколько простых аннотаций и параметров конфигурации. Но, прежде чем начать создавать наш первый микросервис, давайте познакомимся с некоторыми передовыми методами реализации облачных микросервисов. Приемы создания облачных микросервисов 2.3. 71 Приемы создания облачных микросервисов В этом разделе мы рассмотрим некоторые передовые методы разработки облачных микросервисов. В предыдущей главе мы объяснили разницу между моделями облачных вычислений, но что такое облако? Облако – это не конкретное место; это система управления ресурсами, которая позволяет заменить локальные машины и центры обработки данных виртуальной инфраструктурой. Существует несколько уровней или типов облачных приложений, но в этом разделе мы сосредоточимся только на двух из них – пригодных для использования в облаке и изначально облачных. Приложение, пригодное для использования в облаке, – это приложение, которое первоначально предназначалось для выполнения на локальном компьютере или сервере. С появлением облака приложения этого типа переместились из статической среды в динамическую – в облако. Например, приложение, не пригодное для использования в облаке, может быть локальным приложением, содержащим одну конкретную конфигурацию базы данных, которую необходимо настраивать в каждом окружении (разработки, тестирования, промышленной эксплуатации). Чтобы сделать такое приложение пригодным для использования в облаке, нужно вынести его конфигурацию вовне, чтобы адаптировать ее к различным окружениям. В этом случае мы сможем обеспечить работу приложения в нескольких окружениях без изменения исходного кода. Изначально облачное приложение (рис. 2.2) с самого начала проектировалось для работы в облачном окружении и способно использовать все его преимущества. При создании приложений этого типа разработчики разбивают функции на микросервисы и организуют их в масштабируемые компоненты, такие как контейнеры, что позволяет им выполняться на нескольких серверах. Эти службы управляются виртуальными инфраструктурами через процессы DevOps и непрерывной доставки. 72 Глава 2 Обзор мира микросервисов через призму Spring Cloud DevOps Микросервисы Изначально облачные компоненты Непрерывная доставка Контейнеры Рис. 2.2. Изначально облачные приложения организованы в масштабируемые компоненты, такие как контейнеры, развертываются как микросервисы и управляются виртуальными инфраструктурами с помощью процессов DevOps и непрерывной доставки Важно понимать, что приложения, пригодные для использования в облаке, не требуют никаких изменений или преобразований. Они уже предусматривают обработку ситуаций, когда нижестоящие компоненты могут оказаться недоступными. Вот четыре принципа разработки изначально облачных приложений: DevOps – это аббревиатура из начальных букв слов «development» (Dev; разработка) и «operations» (Ops; эксплуатация). Она обозначает методологию разработки программного обеспечения, предполагающую тесное сотрудничество разработчиков и специалистов, осуществляющих эксплуатацию и сопровождение программного обеспечения. Основная цель этой методологии – автоматизировать процессы доставки программного обеспечения и минимизировать затраты на изменение инфраструктуры; микросервисы – это небольшие, слабо связанные распределенные службы. Архитектура микросервисов позволяет взять большое приложение и разложить его на простые в управлении компоненты с узко ограниченными обязанностями, а также помогает бороться с проблемами сложности, характерными для больших баз кода, разбивая их на мелкие, четко определенные части; Приемы создания облачных микросервисов 73 непрерывная доставка – это практика разработки программного обеспечения. Согласно этой практике процесс доставки программного обеспечения автоматизируется, что обеспечивает быструю доставку в промышленное окружение; контейнеры являются естественным продолжением развертывания микросервисов в образе виртуальной машины (ВМ). Вместо полноценной ВМ многие разработчики развертывают свои службы в контейнерах Docker (или в аналогичных системах управления контейнерами) в облаке. В этой книге мы сосредоточимся на создании микросервисов, поэтому вы должны помнить, что они по определению являются изначально облачными. Это означает, что приложение на основе микросервисов может выполняться на ресурсах нескольких облачных провайдерах, получая при этом все преимущества облачных служб. Для решения проблем, сопутствующих созданию облачных микросервисов, мы будем использовать руководство с названием «Приложение двенадцати факторов» (https://12factor.net/ru/), разработанное в компании Heroku. Это приложение помогает создавать и развивать облачные приложения (микросервисы). Мы можем рассматривать эту методологию как набор практик разработки и проектирования, которые сосредоточены на динамическом масштабировании и фундаментальных аспектах, присущих распределенным службам. Эта методология была создана в 2002 году несколькими разработчиками Heroku. Основная цель состояла в том, чтобы определить 12 лучших практик проектирования микросервисов. Мы выбрали эту методологию, потому что это она представляет наиболее полный свод правил, которым нужно следовать при создании облачных приложений. Это руководство не только определяет наиболее распространенные проблемы, встречающиеся при разработке современных приложений, но и предлагает надежные решения. Методы, описанные в руководстве «Приложение двенадцати факторов», показаны на рис. 2.3. ПРИМЕЧАНИЕ. В этой главе мы кратко опишем каждую практику, потому что, как вы увидите сами, мы будем использовать методологию приложения двенадцати факторов на протяжении всей книги, применяя ее к примерам проектов на основе Spring Cloud и других технологий. 74 Глава 2 Обзор мира микросервисов через призму Spring Cloud 2. Зависимости 3. Конфигурация 4. Вспомогательные службы 1. База кода 12. Задачи администрирования 5. Сборка, выпуск, выполнение Приложение двенадцати факторов 11. Журналирование 10. Сходство окружений разработки/эксплуатации 6. Процессы 7. Привязка портов 9. Живучесть 8. Масштабируемость Рис. 2.3. Методы, определяемые руководством «Приложение двенадцати факторов» 2.3.1. База кода В соответствии с этой практикой каждый микросервис должен иметь свою отдельную базу исходного кода. Кроме того, важно подчеркнуть, что информация о конфигурации сервера также должна находиться в системе управления версиями. Помните, что управление версиями – это управление изменениями в файлах или в наборе файлов. База кода может включать несколько экземпляров окружений развертывания (таких как окружение разработки, окружение тестирования, промышленное окружение и т. д.), но не имеет общих компонентов с другими микросервисами. Это важный ориентир, потому что если мы будем использовать общую базу кода для всех микросервисов, то в конечном итоге создадим множество неизменяемых выпусков, принадлежащих разным окружениям. На рис. 2.4 показана отдельная база кода с множеством окружений развертывания. Приемы создания облачных микросервисов 75 Окружения Разработки Тестирования Промежуточное База кода Промышленное Рис. 2.4. Отдельная база кода с множеством окружений развертывания 2.3.2. Зависимости Эта практика требует явно объявлять и изолировать зависимости вашего приложения, используя инструменты сборки, такие как Maven или Gradle (Java). Зависимости от сторонних JAR должны объявляться с указанием конкретных номеров версий этих артефактов. Это позволит создавать микросервисы всегда с одной и той же версией библиотеки. Если вы плохо знакомы с концепцией инструментов сборки, то взгляните на рис. 2.5, который поможет вам понять, как работает инструмент сборки. Сначала Maven читает зависимости из файла pom. xml, а затем отыскивает их в локальном репозитории. Отсутствующие зависимости загружаются из центрального репозитория Maven и добавляются в локальный репозиторий для использования в будущем. Если искомый артефакт отсутствует в локальном репозитории, то Maven ищет его в центральном репозитории Центральный репозиторий Maven или репозиторий Maven компании Maven читает файл pom.xml и выполняет сборку Загруженный артефакт Проект простой службы pom.xml Maven Помещает загруженный артефакт в локальный репозиторий Сгенерированные файлы JAR Целевой каталог Локальный репозиторий .M2 Проверка присутствия артефакта в локальном репозитории Рис. 2.5. Maven читает зависимости из файла pom.xml, а затем отыскивает их в локальном репозитории. Отсутствующие зависимости загружаются из центрального репозитория Maven и добавляются в локальный репозиторий 76 Глава 2 Обзор мира микросервисов через призму Spring Cloud 2.3.3. Конфигурация Эта практика относится к хранению конфигураций приложения (особенно конфигураций, зависящих от окружения). Никогда не добавляйте конфигурации в исходный код! Полностью отделяйте конфигурацию от развертываемого микросервиса. Представьте такой сценарий: вы хотите обновить конфигурацию для микросервиса, 100 экземпляров которого было запущено на сервере. Если конфигурация будет храниться в исходном коде микросервиса, то после внесения изменений в конфигурацию вам придется повторно развернуть все 100 экземпляров. Однако в микросервисе можно реализовать загрузку внешней конфигурации и использовать возможности облака для повторной загрузки конфигурации во время выполнения без перезапуска микросервиса. На рис. 2.6 показан пример, как должно выглядеть ваше окружение. База кода Конфигурация для окружений Окружения Разработки Разработки Тестирования Тестирования Промежуточного Промежуточное Промышленного Промышленное Рис. 2.6. Внешняя конфигурация для каждого окружения 2.3.4. Вспомогательные службы Микросервисы часто обмениваются данными по сети с базами данных, службами RESTful, другими серверами или системами обмена сообщениями. В таких случаях вы должны гарантировать возможность замены локальных служб сторонними без каких-либо изменений в коде приложения. В главе 12 мы покажем, как организовать переключение микросервиса с использования локальной базы данных на базу данных, управляемую Amazon. На рис. 2.7 показан пример некоторых вспомогательных служб, которые могут использовать наши приложения. Приемы создания облачных микросервисов 77 URL Служба SMTP x Локальная база данных Вы должны гарантировать возможность замены локальной базы данных удаленной (в данном случае на AWS RDS) без каких-либо изменений в коде Развертывание приложения URL AWS S3 URL URL Приложение может использовать сколько угодно локальных или сторонних служб AWS RDS Рис. 2.7. Вспомогательная служба – это любая служба, к которой приложение обращается по сети. При развертывании приложения вы должны гарантировать возможность замены локальных служб сторонними без какихлибо изменений в коде 2.3.5. Сборка, выпуск, выполнение Эта практика напоминает о необходимости четкого разделения этапов сборки, выпуска и выполнения приложения. Микросервисы не должны зависеть от окружения, в котором они выполняются. Любые изменения в окружении, произведенные после сборки кода, должны приводить к повторению процессов сборки и развертывания. Скомпилированная служба считается зафиксированной и не может быть изменена. Этап выпуска отвечает за объединение скомпилированной службы с определенной конфигурацией для каждого целевого окружения. Если не разделить разные этапы, то это может привести к проблемам и расхождениям в коде, которые невозможно или – в лучшем случае – трудно отследить. Например, если изменить службу, уже развернутую в промышленном окружении, то изменения не будут зафиксированы в репозитории, и могут возникнуть две ситуации: изменения потеряются в следующих версиях службы или вам придется копировать изменения в новую версию. На рис. 2.8 показан пример архитектуры, соответствующей этой практике. 78 Глава 2 Обзор мира микросервисов через призму Spring Cloud Этап сборки База кода Этап выпуска Конфигурация Рис. 2.8. Разделяйте этапы сборки, выпуска и выполнения микросервиса 2.3.6. Процессы Микросервисы не должны иметь состояния и хранить только информацию, необходимую для выполнения запрошенной транзакции. Микросервисы могут быть остановлены в любой момент, и потеря экземпляра службы не должна приводить к потере данных. Если состояние все же необходимо, то оно должно храниться в кеше, таком как Redis, или во внутренней базе данных. На рис. 2.9 показано, как работают микросервисы без состояния. БД SQL Служба покупательской корзины БД NoSQL Данные, обрабатываемые службой покупательской корзины, можно хранить в базе данных SQL или NoSQL Рис. 2.9. Микросервисы без состояния не хранят никакой информации о сеансе (состояния) на сервере. Для этой цели используются базы данных SQL или NoSQL 2.3.7. Привязка портов Под привязкой портов подразумевается экспортирование служб через определенные порты. В микросервисной архитектуре микросервисы полностью автономны, и каждый имеет свой механизм времени выполнения, упакованный в выполняемый файл вместе с кодом службы. Служба не должна нуждаться в отдельном веб-сервере или сервере приложений и должна поддерживать возможность запуска из командной строки и непосредственного подключения к ней через открытый порт HTTP. Приемы создания облачных микросервисов 79 2.3.8. Масштабируемость Согласно практике масштабируемости облачные приложения должны масштабироваться по горизонтали с использованием модели процессов. Что это значит? Это значит, что должна иметься возможность вместо одного процесса запустить несколько процессов, чтобы потом распределить нагрузку между ними. Под вертикальным масштабированием подразумевается увеличение мощности аппаратной инфраструктуры (процессор, объем ОЗУ), а под горизонтальным – запуск дополнительных экземпляров приложения. В ситуациях, когда возникает потребность в масштабировании, запускайте больше экземпляров микросервисов и выполняйте горизонтальное масштабирование, а не вертикальное. На рис. 2.10 показана разница между этими типами масштабирования. Вертикальное масштабирование (добавление ОЗУ, процессоров и т. д.) 1 CPU/1 GB RAM 2 CPU/2 GB RAM 3 CPU/3 GB RAM Горизонтальное масштабирование (добавление экземпляров) 1 CPU/ 1 GB RAM 1 CPU/ 1 GB RAM 1 CPU/ 1 GB RAM 1 CPU/ 1 GB RAM 4 CPU/4 GB RAM Рис. 2.10. Разница между вертикальным и горизонтальным масштабированием 2.3.9. Одноразовость Микросервисы должны быть одноразовыми и запускаться и останавливаться по запросу, чтобы упростить эластичное масштабирование и быстрое развертывание приложения и изменений в конфигурации. В идеале запуск должен длиться не дольше пары секунд с момента команды запуска до момента, когда процесс будет готов принимать запросы. Под одноразовостью мы подразумеваем возможность удаления отказавших экземпляров и запуск новых без влияния на другие службы. Например, если один из экземпляров микросервиса выйдет из строя из-за сбоя в оборудовании, мы можем остановить этот экземпляр, не затрагивая другие микросервисы, и при необходимости запустить другой экземпляр в другом месте. Глава 2 Обзор мира микросервисов через призму Spring Cloud 80 2.3.10. Сходство окружений разработки/эксплуатации Эта практика требует обеспечить максимальное сходство разных окружений (например, разработки, тестирования, промышленной эксплуатации). Окружения всегда должны содержать похожие версии развернутого кода, инфраструктуры и служб. Этого можно добиться с помощью непрерывного развертывания, которое максимально автоматизирует процесс развертывания, позволяя развертывать микросервисы в нескольких окружениях за короткое время. После отправки в репозиторий код следует протестировать, а затем как можно быстрее передать из окружения разработки в промышленное окружение. Это особенно важно для тех, кто стремится избежать ошибок развертывания. Сходство окружений разработки и промышленной эксплуатации позволяет контролировать все возможные сценарии, которые могут возникнуть при развертывании и выполнении приложения. 2.3.11. Журналирование Журналы – это поток событий. Журналы должны управляться такими инструментами, как Logstash (https://www.elastic.co/logstash) или Fluentd (https://www.fluentd.org/), которые собирают и хранят журналы в централизованном хранилище. Микросервис никогда не должен заботиться о том, как это происходит. От него требуется только выводить журналируемую информацию в стандартный вывод (stdout). В главе 11 мы покажем, как обеспечить отправку журналов в стек ELK (Elasticsearch, Logstash и Kibana). На рис. 2.11 показано, как осуществляется журналирование в микросервисной архитектуре, использующей этот стек. Микросервис Файл журнала Микросервис Файл журнала Logstash Микросервис Файл журнала Микросервис Файл журнала Elasticsearch Kibana Рис. 2.11. Управление журналами микросервисов с помощью стека ELK Актуальность наших примеров 81 2.3.12. Задачи администрирования Разработчикам часто приходится выполнять задачи администрирования своих служб (например, переносить или преобразовывать данные). Такие задачи не должны быть уникальными и всегда выполняться только с помощью сценариев, которые хранятся и обслуживаются через репозиторий исходного кода. Сценарии должны быть повторяемыми и неизменяемыми (т. е. код сценария не должен изменяться) в каждом окружении. Важно определить типы задач, которые должны выполняться при запуске микросервиса, чтобы при наличии нескольких микросервисов с такими сценариями мы могли выполнять все задачи администрирования без любых манипуляций вручную. ПРИМЕЧАНИЕ. Если вы хотите узнать больше о манифесте Heroku «Приложение двенадцати факторов», посетите веб-сайт https://12factor.net/ru/ В главе 8 мы расскажем, как реализовать все перечисленные практики с помощью Spring Cloud API Gateway. А теперь можем перейти к следующему разделу, в котором начнем создавать наш первый микросервис с использованием Spring Boot и Spring Cloud. 2.4. Актуальность наших примеров Мы хотели, чтобы примеры, представленные в этой книге, можно было использовать в повседневной работе. С этой целью мы структурировали главы книги и соответствующие примеры кода вокруг программного продукта вымышленной компании под названием Optima Growth. Optima Growth – компания по разработке программного обеспечения, основной продукт которой – Optima Stock (далее мы будем называть его O-stock); это приложение корпоративного уровня для управления активами. Оно охватывает все критические элементы: инвентаризацию, поставки программного обеспечения, управление лицензиями, соблюдение требований законодательства, затраты и управления ресурсами. Основная цель приложения – дать организациям возможность получить точную картину своих программных активов на определенный момент времени. Компании около 12 лет. Компания хочет обновить свой основной продукт, O-stock. Большая часть бизнес-логики приложения останется прежней, но само приложение, имеющее монолитную архитектуру, будет разбито на множество микросервисов гораздо меньшего размера, которые можно будет разворачивать в облаке независимо друг от друга. Такая масштабная реорганизация головного продукта O-stock может оказаться решающей для компании. 82 Глава 2 Обзор мира микросервисов через призму Spring Cloud ПРИМЕЧАНИЕ. Примеры в этой книге охватывают не все части приложения O-stock. Мы лишь создадим конкретные микросервисы для рассматриваемой предметной области, а затем сформируем инфраструктуру для них. Мы сделаем это с помощью различных технологий Spring Cloud (и некоторых других, отличных от Spring Cloud). Возможность успешного внедрения облачной микросервисной архитектуры повлияет на все подразделения технической организации, включая группы проектирования, разработки и сопровождения. Каждая из них должна будет внести свой вклад, и в конце концов их все, вероятно, придется реорганизовать с учетом переоценки их ролей в этой новой среде. Давайте начнем наше путешествие с Optima Growth и для начала определим несколько микросервисов для O-stock, а затем создадим их с помощью Spring Boot. ПРИМЕЧАНИЕ. Мы понимаем всю сложность архитектуры системы управления активами. Поэтому в этой книге мы используем лишь ограниченное число базовых концепций и сосредоточимся на создании законченной микросервисной архитектуры с простой организацией. Создание полного приложения для управления программными активами выходит за рамки этой книги. 2.5.Создание микросервиса с использованием Spring Boot и Java В этом разделе мы построим каркас микросервиса – службы лицензий – для компании Optima Growth, упомянутой в предыдущем разделе. Все микросервисы мы будем создавать с использованием Spring Boot. Spring Boot, как отмечалось ранее, – это слой абстракции над библиотеками Spring, который позволяет быстро создавать вебприложения и микросервисы на Groovy и Java со значительно меньшими церемониями и настройками, чем обычно. В роли основного языка программирования для создания службы лицензий мы используем Java, а в роли инструмента сборки – Apache Maven. В следующих разделах мы: 1 создадим базовый каркас микросервиса и сценарий Maven для сборки приложения; 2 реализуем класс начальной загрузки Spring, который запустит контейнер Spring для микросервиса и выполнит все необходимое для инициализации класса. Создание микросервиса с использованием Spring Boot и Java 2.5.1. 83 Подготовка окружения Для создания микросервисов нам понадобятся следующие компоненты: Java 11 (http://mng.bz/ZP4m); Maven версии 3.5.4 или выше (https://maven.apache.org/ download.cgi); Spring Tools 4 (https://spring.io/tools); эту библиотеку можно также загрузить в выбранной интегрированной среде разработки (Integrated Development Environment, IDE); IDE, такие как: – Eclipse (https://www.eclipse.org/downloads/); – IntelliJ IDEA (https://www.jetbrains.com/idea/download/); – NetBeans (https://netbeans.org/features/index.html). ПРИМЕЧАНИЕ. С этого момента все примеры кода будут создаваться с использованием Spring Framework 5 и Spring Boot 2. Мы не собираемся объяснять все функции Spring Boot и будем отмечать лишь самое необходимое для создания микросервиса. Еще один важный факт: в этой книге мы будем использовать Java 11, чтобы охватить как можно более широкую аудиторию. 2.5.2. Начало создания проекта Для начала создадим каркас проекта службы лицензий O-stock с использованием Spring Initializr. Служба Spring Initializr (https:// start.spring.io/) позволяет создать новый проект Spring Boot, предоставляя возможность выбора зависимостей из обширного списка. Кроме того, она дает возможность изменять настройки проекта. На рис. 2.12 и 2.13 показано, как должна выглядеть страница Spring Initializr с настройками проекта нашей будущей службы лицензий. 84 Глава 2 Обзор мира микросервисов через призму Spring Cloud Инструмент сборки кода на Java Версия Spring Boot Группа, артефакт и имя проекта Главный пакет проекта (местоположение класса инициализации Spring) Рис. 2.12. Настройка зависимостей службы лицензий в Spring Initializr Рис. 2.13. Конфигурация службы лицензий в Spring Initializr Создание микросервиса с использованием Spring Boot и Java 85 ПРИМЕЧАНИЕ. Исходный код можно также загрузить из репозитория GitHub по адресу https://github.com/ihuaylupo/manningsmia/tree/master/chapter2. После того как вы создадите и импортируете проект Maven в выбранную среду разработки, добавьте следующие пакеты: com.optimagrowth.license.controller com.optimagrowth.license.model com.optimagrowth.license.service На рис. 2.14 показана начальная структура проекта службы лицензий, как она выглядит в IDE, а в листинге 2.2 приводится содержимое файла pom.xml. Licensing-service • src/main/java • com.optimagrowth.license • com.optimagrowth.license.controller • com.optimagrowth.license.model • com.optimagrowth.license.service • src/main/resources • static • templates • application.properties • src/test/java • com.optimagrowth.license • src • target • pom.xml Рис. 2.14. Структура проекта службы лицензий для приложения O-stock с классом инициализации, свойствами приложения, тестами и файлом pom.xml ПРИМЕЧАНИЕ. Подробное обсуждение тестирования микросервисов выходит за рамки этой книги. Но если вы захотите поближе познакомиться с созданием модульных, интеграционных и платформенных тестов, то мы настоятельно рекомендуем книгу Алекса Сото Буэно (Alex Soto Bueno), Энди Гумбрехта (Andy Gumbrecht) и Джейсона Портера (Jason Porter) «Testing Java Microservices» (Manning, 2018). 86 Глава 2 Обзор мира микросервисов через призму Spring Cloud Листинг 2.2. Содержимое файла pom.xml с настройками проекта службы лицензий <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> Подключить зависимости <groupId>org.springframework.boot</groupId> набора инструментов <artifactId> Spring Boot Starter spring-boot-starter-parent </artifactId> <version>2.2.3.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.optimagrowth</groupId> <artifactId>licensing-service</artifactId> <version>0.0.1-SNAPSHOT</version> <name>License Service</name> <description>Ostock Licensing Service</description> <properties> <java.version>11</java.version> </properties> По умолчанию в pom указывается версия Java 6. Чтобы использовать Spring 5, ее нужно заменить на Java 11 <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId> Подключить зависимости spring-boot-starter-actuator Spring Actuator </artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId> Подключить зависимости spring-boot-starter-web Spring Boot Web </artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> <dependency> Создание микросервиса с использованием Spring Boot и Java 87 <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <scope>provided</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId> Подключить плагины для сборки spring-boot-maven-plugin и развертывания приложений </artifactId> Spring Boot </plugin> </plugins> </build> </project> ПРИМЕЧАНИЕ. Проект Spring Boot не требует явно настраивать отдельные зависимости Spring – они автоматически извлекаются из артефакта ядра Spring Boot, как определено в файле pom. Сборки Spring Boot v2.x используют Spring Famework 5. Мы не будем подробно комментировать весь файл и рассмотрим только несколько ключевых аспектов. Фреймворк Spring Boot разбит на множество отдельных проектов. Сделано это для того, чтобы не заставлять вас «рушить мир», если вы не собираетесь использовать какие-то части Spring Boot в своем приложении. Кроме того, такой подход позволяет различным проектам Spring Boot выпускать новые версии кода независимо друг от друга. Чтобы упростить жизнь разработчикам, команда Spring Boot собрала зависимые друг от друга проекты в различные «стартовые» («starter») комплекты. В мы требуем от Maven загрузить определенную версию фреймворка Spring Boot (в данном случае 2.2.3). В указываем версию Java, а в и требуем загрузить стартовые комплекты Spring Actuator и Spring Boot Web. Обратите внимание, что зависимость Spring Actuator в данном проекте не нужна, но в следующих главах мы будем использовать некоторые конечные точки Actuator, поэтому добавляем ее сейчас. Эти два проекта составляют основу практически любых REST-служб на основе Spring Boot. По мере расширения возможностей своих служб вы заметите, что список подобных зависимостей становится длиннее. Spring также предоставляет плагины Maven, упрощающие сборку и развертывание приложений Spring Boot. В мы указываем сценарию сборки Maven, что тот должен установить последнюю версию плагина Spring Boot для Maven. Этот плагин содержит несколько дополнительных задач (например, spring-boot:run), которые упрощают взаимодействие между Maven и Spring Boot. 88 Глава 2 Обзор мира микросервисов через призму Spring Cloud Чтобы проверить зависимости Spring, добавленные фреймворком Spring Boot в нашу службу лицензий, можно взглянуть на цель dependency:tree. На рис. 2.15 показано, как выглядит дерево зависимостей для службы лицензий. Дерево зависимостей Maven содержит все зависимости, используемые в текущем проекте Tomcat версии 9.0.30 Spring Framework версии 5.2.3.RELEASE Рис. 2.15. Дерево зависимостей для службы лицензий в приложении O-stock. Здесь отображаются все зависимости, объявленные и используемые в службе 2.5.3. Запуск приложения Spring Boot: класс инициализации Наша цель в этом разделе – создать и запустить простой микросервис на основе Spring Boot, чтобы затем выполнить следующую итерацию и расширить его функциональные возможности. Для этого в микросервисе службы лицензий нужно определить два класса: класс инициализации Spring, который будет использоваться фреймворком Spring Boot для запуска и инициализации приложения; класс контроллера Spring, который экспортирует конечные точки HTTP, через которые клиенты смогут обращаться к микросервису. Создание микросервиса с использованием Spring Boot и Java 89 Как вы скоро увидите, для упрощения запуска и настройки службы Spring Boot использует аннотации. Взгляните на класс инициализации в следующем листинге. Вы найдете этот класс в файле LicenseServiceApplication.java в папке src/main/java/com/ optimagrowth/license. Листинг 2.3. Знакомство с аннотацией @SpringBootApplication package com.optimagrowth.license; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class LicenseServiceApplication { Сообщает Spring Boot, что этот класс является классом инициализации проекта public static void main(String[] args) { SpringApplication.run( LicenseServiceApplication.class, args); } Запуск службы Spring Boot } Первое, на что следует обратить внимание, – это использование аннотации @SpringBootApplication. Spring Boot использует эту аннотацию, чтобы сообщить контейнеру Spring, что этот класс является источником определений bean-компонентов. В приложениях Spring Boot bean-компоненты можно определить путем: 1 добавления к Java-классу аннотации @Component, @Service или @Repository; 2 добавления к Java-классу аннотации @Configuration и определения фабричного метода для каждого bean-компонента с аннотацией @Bean. ПРИМЕЧАНИЕ. Bean-компонент Spring – это объект, которым фреймворк Spring управляет во время выполнения с помощью контейнера Inversion of Control (IoC). Такие компоненты автоматически создаются и добавляются в «репозиторий объектов», что дает возможность получить их позже. Аннотация @SpringBootApplication отмечает класс приложения в листинге 2.3 как класс конфигурации, а затем он начинает автоматически сканирует все классы, доступные в пути поиска классов Java (classpath), выявляя другие bean-компонентs Spring. Следующий важный момент в листинге 2.3 – это метод main() в классе LicenseServiceApplication. Вызов SpringApplication. run(LicenseServiceApplication.class, args) в методе main() запускает контейнер Spring и возвращает объект ApplicationContext. (Мы не используем ApplicationContext напрямую, поэтому он отсутствует в коде.) 90 Глава 2 Обзор мира микросервисов через призму Spring Cloud Еще одно важное замечание об аннотации @SpringBootApplication: отмеченный ею класс LicenseServiceApplication является классом инициализации (или, как еще говорят, классом начальной загрузки) всего микросервиса. Поэтому вся базовая логика инициализации службы должна находиться в этом классе. Теперь, узнав, как создать каркас и класс инициализации микросервиса, перейдем к следующей главе. Там мы расскажем о некоторых важных ролях, связанных с разработкой микросервисов, и какое участие принимают эти роли в создании приложения O-stock. Кроме того, мы познакомим вас с некоторыми дополнительными технологиями, которые сделают наши микросервисы более гибкими и надежными. Итоги Spring Cloud – это комплекс технологий с открытым исходным кодом, разработанных в таких компаниях, как Netflix и HashiCorp. Внедрение этих технологий осуществляется посредством аннотаций Spring, что значительно упрощает создание и настройку служб, использующих их. Облачные приложения собираются из масштабируемых компонентов, таких как контейнеры, развертываются как микросервисы и управляются в виртуальных инфраструктурах с использованием процессов DevOps и непрерывной доставки. DevOps – это аббревиатура из начальных букв слов development (Dev; разработка) и operations (Ops; эксплуатация). Она обозначает методологию разработки программного обеспечения, предполагающую тесное сотрудничество разработчиков и специалистов, осуществляющих эксплуатацию и сопровождение программного обеспечения. Основная цель этой методологии – автоматизировать процессы доставки программного обеспечения и минимизировать затраты на изменение инфраструктуры. Манифест «Приложение двенадцати факторов», разработанный в Heroku, определяет передовые практики, которые следует применять при создании облачных микросервисов. В число практик, определяемых манифестом «Приложение двенадцати факторов», входят: база кода; зависимости; конфигурация; вспомогательные службы; сборка, выпуск и выполнение; процессы; привязка портов; масштабируемость; одноразовость; сходство окружений разработки и эксплуатации; журналирование и задачи администрирования. Spring Initializr позволяет создать новый проект Spring Boot путем выбора зависимостей из обширного списка. Spring Boot – идеальная основа для создания микросервисов, потому что позволяет создавать REST-службы добавлением нескольких простых аннотаций. 3 Создание микросервисов с использованием Spring Boot Эта глава: определяет место микросервисов в облачной архитектуре; описывает порядок деления предметной области на набор микросервисов; перечисляет перспективы приложений на основе микросервисов; рассказывает, когда нельзя использовать микросервисы; показывает приемы реализации микросервисов. Чтобы добиться успеха в проектировании и создании микросервисов, вы должны подходить к их созданию так, будто вы детектив, допрашивающий свидетелей происшествия. Даже притом что все свидетели видели одно и то же событие, их интерпретация зависит от прошлого опыта, от того, что для них важно (например, что их особенно взволновало), а также от того, какое давление окружения они испытывали в тот момент, когда стали свидетелями происшествия. Причем, у каждого свидетеля своя точка зрения (и предубеждения) в отношении того, что считать важным. Подобно полицейским детективам, пытающимся докопаться до истины, мы должны учесть мнение множества людей в своей организации, занимающейся разработкой программного обеспечения, чтобы преуспеть в создании микросервисной архитектуры. Для создания законченного приложения требуются не только технические специалисты, поэтому мы считаем, что основа успешной раз- 92 Глава 3 Создание микросервисов с использованием Spring Boot работки микросервисов начинается с вовлечения в процесс трех важнейших ролей: архитектора – видит общую картину, разбивает приложение на отдельные микросервисы, а затем определяет, как микросервисы будут взаимодействовать друг с другом для получения решения; программиста – пишет код и определяет, как использовать язык и фреймворки для создания микросервиса; инженера DevOps – определяет порядок развертывания служб и управления ими в промышленном и непромышленном окружениях. Девиз инженера DevOps: «Последовательность и повторяемость в любом окружении». В этой главе мы покажем, как проектировать и создавать наборы микросервисов с точки зрения каждой из этих ролей. Эта глава даст вам основу, необходимую для разграничения потенциальных микросервисов в вашем собственном приложении, и представление о характеристиках окружения, необходимых для развертывания микросервиса. К концу этой главы мы создадим службу, готовую к развертыванию в облаке, взяв за основу каркас проекта, созданный в главе 2. 3.1.Точка зрения архитектора: проектирование микросервисной архитектуры Роль архитектора в проекте программного обеспечения – предложить рабочую модель задачи, которую нужно решить. Архитектор формирует основу, на которой разработчики будут строить свой код и приводить в соответствие все части приложения. При создании микросервисов архитектор проекта фокусируется на трех ключевых задачах: декомпозиции бизнес-задачи; детализации служб; определении интерфейсов. 3.1.1. Декомпозиция бизнес-задачи Большинство людей, столкнувшись со сложностями, пытаются разбить задачу, над которой они работают, на более простые подзадачи, чтобы не забивать голову множеством мелких деталей. Они разбивают задачу на несколько основных частей, а затем определяют отношения, существующие между этими частями. Процесс создания архитектуры микросервисов протекает почти так же. Архитектор разбивает бизнес-задачу на части, представляющие отдельные виды деятельности. Эти части инкапсулируют Точка зрения архитектора: проектирование микросервисной архитектуры 93 бизнес-правила и логику данных, связанную с определенной частью предметной области. Например, архитектор может взглянуть на бизнес-поток, который необходимо реализовать, и понять, что ему нужна информация как о покупателе, так и о продукте. СОВЕТ. Наличие двух отдельных видов данных является хорошим признаком того, что в игре может участвовать несколько микросервисов. Правила взаимодействий двух разных частей бизнес-транзакции обычно становятся служебным интерфейсом микросервиса. Разбиение предметной области – это больше искусство, чем точная наука. Для выявления и выделения кандидатов на создание микросервисов можно использовать следующие рекомендации. Опишите бизнес-задачу и обратите внимание на существительные, которые вы используете. Многократное использование одних и тех же существительных при описании задачи обычно является хорошим признаком некоторой подзадачи, которую можно организовать в виде отдельного микросервиса. Примерами таких существительных для приложения O-stock могут быть контракт, лицензии и активы. Обратите внимание на глаголы. Глаголы выделяют действия и часто отмечают естественные контуры предметной области. Если вы обнаружите, что говорите: «Транзакция X должна получить данные от объектов A и B», – то это обычно указывает на присутствие в игре нескольких служб. Применив этот подход к приложению O-stock, можно заметить такие утверждения, как: «Когда Майк настраивает новый компьютер, он выясняет количество лицензий, доступных для программного обеспечения X, и, если лицензии доступны, устанавливает программное обеспечение. Затем он обновляет количество используемых лицензий в своей электронной таблице отслеживания». Ключевые глаголы здесь – это выясняет и обновляет. Ищите тесно связанные данные. Разбивая бизнес-задачу на отдельные части, ищите фрагменты данных, тесно связанные друг с другом. Если вдруг в процессе обсуждения обнаруживается необходимость чтения или обновления данных, которые радикально отличаются от того, что вы обсуждали, то это может указывать на еще одного кандидата в микросервисы. Микросервисы должны владеть только своими данными. Давайте попробуем применить эти рекомендации к реальной задаче, например к приложению O-stock, которое используется для управления программными активами. (Мы впервые упомянули об этом приложении в главе 2.) 94 Глава 3 Создание микросервисов с использованием Spring Boot Напоминаем, что O-stock – это монолитное веб-приложение вымышленной компании Optima Growth, которое развертывается на сервере приложений Java EE в центре обработки данных клиента. Наша цель – разделить существующее монолитное приложение на набор служб. Чтобы достичь ее, начнем с опроса пользователей и некоторых бизнес-партнеров и попробуем выяснить, как они взаимодействуют с приложением и как используют его. Рисунок 3.1 обобщает и выделяет ряд существительных и глаголов из некоторых собеседований с различными бизнес-клиентами. Эмма (закупки) Макс (финансы) Дженни (обслуживание настольных компьютеров) • Вводит информацию о контракте в приложение O-stock • Определяет типы лицензий на программное обеспечение. • Вводит количество лицензий, приобретаемых при закупке • Создает ежемесячные отчеты о затратах • Анализирует стоимость лицензий по контракту • Определяет нехватку или избыточное количество лицензий • Аннулирует неиспользованные лицензии • Настраивает ПК • Определяет доступность лицензия на ПО для ПК • Обновляет информацию в приложении O-stock, чтобы потом можно было узнать, у какого пользователя какое программное обеспечение установлено Монолитное приложение O-stock Таблица с перечнем подразделений в организации Таблица лицензий Таблица контрактов Таблица активов Модель данных является общедоступной и высоко интегрированной База данных O-stock Рис. 3.1. Обобщенные результаты опроса пользователей O-stock, проводившегося с целью понять, как они выполняют свою повседневную работу и взаимодействуют с приложением Точка зрения архитектора: проектирование микросервисной архитектуры 95 Изучив, как пользователи O-stock взаимодействуют с приложением, и ответив на следующие вопросы, мы можем определить модель данных для приложения. После этого мы сможем разложить предметную область O-stock на кандидаты в микросервисы. Где будет храниться информация о контрактах, которыми управляет Эмма? Где будет храниться информация о лицензиях (стоимость, тип лицензии, владелец лицензии и лицензионный договор), и как будет осуществляться управление ею? Дженни настраивает лицензии на ПК. Где будет храниться информация об активах? Принимая во внимание все вышеупомянутое, можно заметить, что лицензия принадлежит организации, имеющей несколько активов, верно? Итак, будет храниться информация об организации? На рис. 3.2 показана упрощенная модель данных, основанная на результатах опросов клиентов компании Optima Growth. Исходя из опросов и модели данных можно выделить следующих кандидатов на роль микросервисов: организацию, лицензию, контракт и активы. Организация Лицензия Контракт Активы Рис. 3.2. Упрощенная модель данных O-stock. Организация может иметь множество лицензий, лицензии могут применяться к одному или нескольким активам, и каждой лицензии соответствует контракт 3.1.2. Детализация служб Определив упрощенную модель данных, можно начать процесс определения микросервисов, которые будут составлять приложение. Согласно модели данных на рис. 3.2, мы можем выделить четырех потенциальных кандидатов на микросервисы, основанных на следующих элементах: активах; лицензии; контракте; организации. Попробуем взять эти основные функциональные элементы и выделить их в автономные модули, которые можно будет компилировать и развертывать независимо друг от друга. Эти модули могут иметь отдельные или совместно использовать общую базу данных. Глава 3 Создание микросервисов с использованием Spring Boot 96 Однако, чтобы выделить службы, опираясь на модель данных, недостаточно просто переупаковать код в отдельные проекты. Необходимо также выделить таблицы в базе данных, к которым будут обращаться службы, и разрешить каждой службе обращаться только к своим конкретным таблицам. На рис. 3.3 показано, как код приложения и модель данных «разбиваются» на отдельные части. ПРИМЕЧАНИЕ. Мы создали отдельные базы данных для каждой службы, но вы можете организовать совместное использование общей базы данных. После разделения предметной области на отдельные части, часто бывает сложно определить – достигли ли мы нужного уровня детализации. Слишком грубое или слишком мелкое деление имеют свои характерные признаки, которые мы вскоре обсудим. Микросервисная архитектура Монолитная архитектура Монолитное приложение O-stock Служба активов Служба лицензий Таблица активов Таблица лицензий Служба Служба контрактов организации База данных O-stock Таблица с перечнем подразделений в организации Таблица лицензий Таблица контрактов Таблица активов Таблица контрактов Таблица с перечнем подразделений в организации Каждая служба владеет всеми данными в своей области. Это не означает, что у каждой службы своя база данных. Это лишь означает, что доступ к тем или иным данным имеют только службы, владеющие ими Рис. 3.3. Результат разбиения монолитного приложения O-stock на более мелкие автономные службы, которые развертываются независимо друг от друга Вопрос детализации очень важен при создании микросервисной архитектуры. Вот почему мы хотим объяснить следующие концепции, которые помогут подобрать правильную степень детализации. Лучше сначала создать микросервис с широкой областью охвата, а затем выполнить рефакторинг и разбить его на более мелкие службы. Вступая на путь создания микросервисов, легко переборщить и превратить в микросервисы все и вся. Однако разделение предметной области на большое количество мелких служб часто приводит к избыточной сложности, потому что микросервисы превращаются в службы, управляющие мелкими фрагментами данных. Точка зрения архитектора: проектирование микросервисной архитектуры 97 В первую очередь сосредоточьтесь на взаимодействиях служб. Это поможет определить общие интерфейсы в предметной области. Слишком широкие службы проще реорганизовать, чем слишком мелкие. Круг обязанностей служб меняется с развитием нашего понимания предметной области. Часто с появлением необходимости внедрения новых возможностей в приложение круг обязанностей микросервиса расширяется. То, что сначала было небольшим микросервисом, может разделиться на несколько служб, при этом исходный микросервис превращается в уровень оркестровки новых служб и инкапсулирует их функциональные возможности. Какие черты позволяют отличить плохой микросервис? Как узнать, правильный ли размер имеет микросервис? Если микросервис получился слишком большой, то вы, скорее всего, увидите: службу со слишком широким кругом обязанностей. Общий поток бизнес-логики в службе выглядит сложным и требует применения чрезмерно разнообразного набора правил; службу, которая управляет данными в большом количестве таблиц. Микросервис – это представление данных, которыми он управляет. Если вы обнаружите, что микросервис манипулирует данными в нескольких таблицах, или обращаетесь к таблицам за пределами своей базы данных, то это явный признак того, что служба слишком велика. Мы в своей практике пользуемся простым правилом: микросервис должен управлять не более чем тремя-пятью таблицами. Если таблиц больше, то, скорее всего, служба имеет слишком широкий круг обязанностей; службу со слишком большим количеством тестов. Службы имеют свойство разрастаться со временем. Если первоначально для вашей службы имелось небольшое количество модульных и интеграционных тестов, которое постепенно достигло нескольких сотен, то вам может потребоваться выполнить рефакторинг. А что можно сказать о слишком мелких микросервисах? Микросервисы, принадлежащие одной части предметной области, размножились как кролики. При дроблении предметной области на слишком мелкие микросервисы становится очень сложно организовать бизнес-логику. Это связано со стремительным ростом количества служб, необходимых для выполнения работы. Типичный признак – наличие в приложении десятков микросервисов, каждый из которых взаимодействует только с одной таблицей в базе данных. Микросервисы сильно зависят друг от друга. Микросервисы в одной части предметной области часто обращаться друг к другу, чтобы обработать один пользовательский запрос. 98 Глава 3 Создание микросервисов с использованием Spring Boot Микросервисы становятся набором простых служб CRUD (Create, Replace, Update, Delete – создание, замена, обновление, удаление). Микросервисы – это выражение бизнес-логики, а не слой абстракции над вашими источниками данных. Если микросервисы не выполняют ничего, кроме логики, связанной с CRUD, то они, вероятно, слишком мелкие. Архитектура микросервисов должна развиваться эволюционно, и важно понимать, что получить правильную архитектуру с первого раза не получится. Именно поэтому лучше начинать с создания более широких служб. Также важно избегать категоричности при проектировании. При создании служб иногда можно столкнуться с физическими ограничениями. Например, вам может понадобиться создать укрупненную службу, объединяющую данные, потому что две отдельные службы будут слишком часто взаимодействовать друг с другом или потому что между разделами предметной области, которые представляют эти службы, не существует четких границ. В конце концов, будьте прагматичными и не тратьте время на то, чтобы довести архитектуру до совершенства, по прошествии которого вам фактически нечем будет оправдать свои усилия. 3.1.3. Определение интерфейсов служб Последняя задача архитектора – определение правил взаимодействий микросервисов друг с другом. Интерфейсы служб должны быть простыми и очевидными, а разработчики должны представлять общую картину работы всех служб в приложении и полностью понимать работу одной или двух служб. Вот несколько общих рекомендаций по определению интерфейсов служб. Возьмите на вооружение философию REST. Это одна из лучших практик (см. приложение A), наряду с моделью зрелости Ричардсона (см. врезку в разделе 3.3.1 ниже). Для взаимодействий между службами философия REST предлагает использовать протокол HTTP с его стандартными глаголами (GET, PUT, POST и DELETE). Моделируйте базовое поведение своих служб, опираясь на эти HTTP-глаголы. Используйте URI для обозначения намерений. URI, выступающие в роли конечных точек службы, должны описывать различные ресурсы в вашей предметной области и служить механизмом отношений между этими ресурсами. Используйте JSON для оформления запросов и ответов. JSON – это чрезвычайно легковесный протокол сериализации данных, и он гораздо проще в использовании, чем XML. Когда не следует использовать микросервисы 99 Используйте коды состояния HTTP для передачи результатов. Протокол HTTP имеет обширный набор стандартных кодов состояния, указывающих на успех или ошибку. Изучите эти коды и, что особенно важно, используйте их единообразно во всех своих службах. Все эти базовые рекомендации сходятся к одному: сделайте интерфейсы служб простыми для понимания и использования. Интерфейсы должны быть определены так, чтобы разработчик сел, посмотрел на них и тут же начал их использовать. Если интерфейс микросервиса сложен в использовании, то разработчики приложат все силы, чтобы обойти его и разрушить замысел архитектуры. 3.2. Когда не следует использовать микросервисы Выше в этой главе мы уже говорили, почему микросервисы являются мощным архитектурным шаблоном. Но мы ничего не сказали о том, когда не следует использовать микросервисы для создания приложений. Давайте рассмотрим основные препятствия: сложность распределенных систем; беспорядочный рост виртуальных серверов или контейнеров; тип приложения; транзакции и согласованность данных. 3.2.1. Сложность распределенных систем Микросервисы по своей природе являются небольшими и распределенными службами, поэтому они влекут за собой дополнительные сложности, отсутствующие в монолитных приложениях. Микросервисные архитектуры требуют высокой степени технической зрелости. Не следует рассматривать возможность использования микросервисов, если ваша организация не желает вкладывать средства в автоматизацию эксплуатации и сопровождения (мониторинг, масштабирование и т. д.), необходимую для надежной работы распределенного приложения. 3.2.2.Беспорядочный рост виртуальных серверов или контейнеров Одна из наиболее распространенных моделей развертывания микросервисов – один экземпляр микросервиса в одном контейнере. Большое приложение на основе микросервисов может оказаться развернутым на большом количестве (от 50 до 100) серверов или контейнеров (обычно виртуальных), которые необходимо создавать и поддерживать. Даже при невысокой стоимости запуска этих служб в облаке сложность управления и мониторинга всего комплекса микросервисов может быть колоссальной. 100 Глава 3 Создание микросервисов с использованием Spring Boot ПРИМЕЧАНИЕ. Учитывайте не только гибкость микросервисов, но также стоимость эксплуатации всех этих серверов. Часто у вас могут быть на выбор разные альтернативы, такие как разработка лямбда-функций или запуск нескольких экземпляров микросервисов на одном сервере. 3.2.3. Тип приложения Микросервисы ориентированы на многократное использование и чрезвычайно полезны для создания больших приложений, которые должны быть очень устойчивыми и масштабируемыми. Это одна из причин высокой популярности микросервисов. Если вы создаете небольшие приложения для отдельных подразделений своей организации или с небольшой базой пользователей, то сложность, сопутствующая построению распределенной модели на основе микросервисов, может привести к неоправданному увеличению затрат. 3.2.4. Транзакции и согласованность данных Начиная рассматривать возможность внедрения микросервисов, обязательно исследуйте шаблоны использования данных вашими службами и пользователями. Микросервис обертывает небольшое количество таблиц и хорошо работает как механизм выполнения простых задач, таких как создание и добавление данных и выполнение несложных запросов к хранилищу данных. Если приложение должно выполнять сложные преобразования или агрегировать данные из нескольких источников, то распределенная природа микросервисов затруднит эту работу. Ваши микросервисы неизбежно будут брать на себя слишком много ответственности и могут стать уязвимыми для проблем с производительностью. 3.3.Точка зрения разработчика: создание микросервиса с использованием Spring Boot и Java В этом разделе мы взглянем на создание микросервиса лицензий для приложения O-stock с точки зрения разработчика. ПРИМЕЧАНИЕ. В предыдущей главе мы создали каркас службы лицензий. Если вы не следовали за примерами кода в той главе, то можете загрузить исходный код по адресу https://github.com/ ihuaylupo/manning-smia/tree/master/chapter2. В следующих нескольких разделах мы: 1 реализуем класс контроллера Spring Boot, чтобы экспортировать конечные точки службы лицензий; Точка зрения разработчика: создание микросервиса с использованием ... 2 3 101 добавим интернационализацию для вывода сообщений на разных языках; реализуем Spring HATEOAS, чтобы дать пользователям достаточно информации для взаимодействия с сервером. 3.3.1. Встраивание дверного проема в микросервис: контроллер Spring Boot Теперь, разобравшись со сценарием сборки (см. главу 2) и реализовав простой класс инициализации, можно приступать к написанию первого кода, который будет делать что-то полезное. Этим кодом будет наш класс контроллера. В Spring Boot класс контроллера экспортирует конечные точки службы и на основе данных из входящего HTTP-запроса выбирает метод Java, который будет обрабатывать этот запрос. Возьмите на вооружение REST Все микросервисы в этой книге соответствуют модели зрелости Ричардсона (http://mng.bz/JD5Z). Это означает, что все создаваемые нами службы будут обладать следующими характеристиками. Использовать HTTP/HTTPS в качестве протокола взаимодействия – конечная точка HTTP представляет службу, а протокол HTTP передает данные в службу и из нее. Отображать свое поведение в стандартные HTTP-глаголы – поведение служб REST соответствует HTTP-глаголам POST, GET, PUT и DELETE, которые в свою очередь соответствуют операциям CRUD. Использовать JSON в роли формата представления всех данных, поступающих в службу и из нее – это не жесткое требование к микросервисам REST, но JSON давно стал основным форматом сериализации данных, которые передаются в микросервисы и возвращаются ими. Вы можете использовать XML, но многие приложения на основе REST используют JavaScript и JSON. JSON – это «родной» формат сериализации и десериализации данных, используемых веб-службами на основе JavaScript. Использовать коды состояния HTTP для передачи результата вызова службы – протокол HTTP имеет богатую коллекцию кодов состояния, указывающих на успех или ошибку. Службы REST и другие веб-инфраструктуры, такие как обратные прокси-серверы и кеши, широко используют эти коды состояния HTTP. Их можно относительно легко интегрировать в ваши микросервисы. HTTP – это язык интернета. Использование HTTP в качестве философской основы является ключом к созданию облачных служб. 102 Глава 3 Создание микросервисов с использованием Spring Boot Определение первого класса контроллера вы найдете в файле src/ main/java/com/optimagrowth/license/controller/LicenseController. java. Класс LicenseController экспортирует четыре конечные точки HTTP, которые соответствующие глаголам POST, GET, PUT, DELETE. Давайте взглянем на этот класс и посмотрим, какие аннотации предлагает Spring Boot, помогающие свести к минимуму усилия, необходимые для определения конечных точек службы и позволяющие сосредоточиться на бизнес-логике. Начнем с определения базового класса контроллера, не имеющего никаких методов. В листинге 3.1 показан класс контроллера для службы лицензий в приложении O-stock. Листинг 3.1 Marking the LicenseServiceController as a Spring RestController package com.optimagrowth.license.controller; import java.util.Random; import import import import import import import import org.springframework.http.ResponseEntity; org.springframework.web.bind.annotation.PathVariable; org.springframework.web.bind.annotation.PostMapping; org.springframework.web.bind.annotation.PutMapping; org.springframework.web.bind.annotation.RequestBody; org.springframework.web.bind.annotation.RequestMapping; org.springframework.web.bind.annotation.RequestMethod; org.springframework.web.bind.annotation.RestController; import com.optimagrowth.license.model.License; @RestController @RequestMapping(value="v1/organization/ {organizationId}/license") public class LicenseController { } Сообщает Spring Boot, что это – служба REST и она автоматически сериализует/десериализует запросы/ответы в JSON Экспортирует все конечные точки HTTP в этом классе, начинающиеся с /v1/ organization/{organizationId}/license Начнем наше исследование с аннотации @RestController. @RestController – это аннотация Java уровня класса, которая сообщает контейнеру Spring, что класс, к которому она применяется, будет использоваться в роли службы REST. Аннотация включает автоматическое преобразование данных, передаваемых в службу, в формат JSON или XML (по умолчанию используется формат JSON). В отличие от традиционной аннотации @Controller аннотация @RestController не требует возвращать класс ResponseBody из метода в классе контроллера. Все необходимое аннотация @RestController, которая включает аннотацию @ResponseBody, сделает сама. Точка зрения разработчика: создание микросервиса с использованием ... 103 В чем преимущества JSON для микросервисов? Мы можем использовать разные протоколы для обмена данными между микросервисами на основе HTTP. Но JSON стал стандартом де-факто по нескольким причинам. По сравнению с другими протоколами, такими как SOAP на основе XML (Simple Object Access Protocol – простой протокол доступа к объектам), JSON более легковесный. Он позволяет выразить данные без больших накладных расходов. JSON удобочитаем для человека. Это недооцененное качество для протокола сериализации. Когда возникает какая-то проблема, разработчик сможет просмотреть фрагмент JSON и быстро разобраться в его содержимом. Простота протокола делает его невероятно ценным. JSON – это протокол сериализации по умолчанию, используемый в JavaScript. После резкого роста популярности JavaScript как языка программирования и столь же значительного роста популярности одностраничных интернет-приложений (Single Page Internet Applications, SPIA), которые в значительной степени основаны на JavaScript, JSON стал естественным форматом для обмена данными между приложениями REST и клиентами. Следует отметить, однако, что существуют другие механизмы и протоколы обмена данными между службами, более эффективные, чем JSON. Фреймворк Apache Thrift (http://thrift.apache.org) позволяет создавать многоязычные службы, взаимодействующие друг с другом с использованием двоичного протокола, такого как Apache Avro (http://avro.apache.org). Apache Avro – это протокол сериализации данных, который преобразует в двоичный формат данные, передаваемые между клиентом и сервером. Если вам нужно минимизировать размер данных, передаваемых по сети, то мы рекомендуем рассмотреть возможность использования этих протоколов. Но, по нашему опыту, формат JSON обладает достаточной эффективностью и упрощает отладку взаимодействий между службами и клиентами. Аннотацию @RequestMapping (вторая аннотация в листинге 3.1) можно использовать и на уровне класса, и на уровне метода, чтобы сообщить контейнеру Spring, какую конечную точку HTTP служба будет экспортировать. Когда @RequestMapping применяется к классу, вы должны определить корневой путь URL для всех других конечных точек, экспортируемых контроллером. В данном случае аннотация @ RequestMapping(value="v1/organization/{organizationId}/license") использует атрибут value и определяет корневой путь в URL ко всем конечным точкам, экспортируемых классом LicenseController. Все конечные точки службы, объявленные в этом контроллере, будут начинаться с v1/organization/{organizationId}/license Здесь {organizationId} – это заполнитель, который указывает, что URL будет параметризован значением organizationId, передаваемым Глава 3 Создание микросервисов с использованием Spring Boot 104 в каждом вызове. Использование organizationId в URL позволяет различать разных клиентов, которые могут использовать службу. Прежде чем добавить первый метод в контроллер, давайте исследуем модель и класс службы, которые мы будем использовать в создаваемых нами микросервисах. В листинге 3.2 показан класс POJO (Plain Old Java Object – старый добрый Java-объект), инкапсулирующий данные о лицензии. ПРИМЕЧАНИЕ. Инкапсуляция – один из основных принципов объектно-ориентированного программирования. Чтобы добиться инкапсуляции в Java, переменные класса нужно объявить приватными, а затем реализовать общедоступные методы для чтения и изменения значений этих переменных. Листинг 3.2. Модель лицензии package com.optimagrowth.license.model; import lombok.Getter; import lombok.Setter; import lombok.ToString; @Getter @Setter @ToString public class License { private private private private private private POJO (Plain Old Java Object – старый добрый Java-объект), содержащий информацию о лицензии int id; String licenseId; String description; String organizationId; String productName; String licenseType; } Lombok Lombok – это небольшая библиотека, которая позволяет уменьшить объем стандартного кода в классах Java. Lombok автоматически генерирует код, например для методов доступа к приватным полям, конструкторов и т. д. В этой книге мы будем использовать библиотеку Lombok во всех примерах кода, чтобы сделать их более читабельными, но не будем вдаваться в подробности ее использования. Желающим узнать больше о Lombok мы рекомендуем ознакомиться со следующими статьями на Baeldung.com: https://www.baeldung.com/intro-to-project-lombok; https://www.baeldung.com/lombok-ide. Чтобы установить Lombok в Spring Tools Suite 4, вы должны загрузить и запустить Lombok и связать ее с IDE. В следующем листинге показано определение класса службы, который мы будем использовать для разработки логики различных микросервисов в классе контроллера. Точка зрения разработчика: создание микросервиса с использованием ... 105 Листинг 3.3. Класс LicenseService package com.optimagrowth.license.service; import java.util.Random; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import com.optimagrowth.license.model.License; @Service public class LicenseService { public License getLicense(String licenseId, String organizationId){ License license = new License(); license.setId(new Random().nextInt(1000)); license.setLicenseId(licenseId); license.setOrganizationId(organizationId); license.setDescription("Software product"); license.setProductName("Ostock"); license.setLicenseType("full"); return license; } public String createLicense(License license, String organizationId){ String responseMessage = null; if(license != null) { license.setOrganizationId(organizationId); responseMessage = String.format( "This is the post and the object is: %s", license.toString()); } return responseMessage; } public String updateLicense(License license, String organizationId){ String responseMessage = null; if (license != null) { license.setOrganizationId(organizationId); responseMessage = String.format( "This is the put and the object is: %s", license.toString()); } return responseMessage; } public String deleteLicense(String licenseId, String organizationId){ String responseMessage = null; responseMessage = String.format( "Deleting license with id %s for the organization %s", licenseId, organizationId); return responseMessage; } } Глава 3 Создание микросервисов с использованием Spring Boot 106 Этот класс содержит набор фиктивных служб, возвращающих жестко запрограммированные данные. Он позволяет получить представление о том, как должен выглядеть каркас микросервиса. Далее мы продолжим работу над этой службой и подробно рассмотрим, как ее структурировать. А пока давайте добавим в контроллер первый метод, реализующий глагол GET и возвращающий единственный экземпляр класса License, как показано в листинге 3.4. Листинг 3.4. Конечная точка HTTP GET package com.optimagrowth.license.controller; import import import import import import import import org.springframework.http.ResponseEntity; org.springframework.web.bind.annotation.PathVariable; org.springframework.web.bind.annotation.PostMapping; org.springframework.web.bind.annotation.PutMapping; org.springframework.web.bind.annotation.RequestBody; org.springframework.web.bind.annotation.RequestMapping; org.springframework.web.bind.annotation.RequestMethod; org.springframework.web.bind.annotation.RestController; import com.optimagrowth.license.model.License; import com.optimagrowth.license.service.LicenseService; @RestController @RequestMapping(value="v1/organization/{organizationId}/license") public class LicenseController { @Autowired private LicenseService licenseService; Два параметра (organizationId и licenseId) из URL отображаются в параметры метода с аннотацией @GetMapping @GetMapping(value="/{licenseId}") public ResponseEntity<License> getLicense( Метод, соот@PathVariable("organizationId") String organizationId, ветствующий @PathVariable("licenseId") String licenseId) { глаголу Get, который возвращает информацию о лицензии License license = licenseService .getLicense(licenseId,organizationId); return ResponseEntity.ok(license); ResponseEntity представляет весь HTTP-ответ } } Первое, что мы сделали, – снабдили метод getLicense() аннотацией @GetMapping. Вместо нее можно также использовать аннотацию @RequestMapping(value="/{licenseId}", method=RequestMethod.GET) c двумя параметрами (value и method). Применив аннотацию @GetMapping на уровне метода getLicense(), мы создали конечную точку: v1/organization/{organizationId}/license/{licenseId} Точка зрения разработчика: создание микросервиса с использованием ... 107 Почему? Все просто. Перед классом мы добавили аннотацию, определяющую все HTTP-запросы с корневым URL, которые должны передаваться контроллеру: сначала мы добавили аннотацию с корневым путем, а затем аннотацию на уровне метода с расширением этого пути. Второй параметр аннотации, method, определяет HTTP-глагол, соответствующий методу. Метод getLicense() мы связали с HTTP-методом GET, представленным элементом перечисления RequestMethod.GET. Второе, что следует отметить в листинге 3.4, – аннотации @PathVariable в списке параметров метода getLicense(). Эти аннотации отображают значения параметров из URL (обозначенных синтаксисом {parameterName}) в параметры метода. В листинге 3.4 мы отображаем два параметра из URL (organizationId и licenseId) в два одноименных параметра метода: @PathVariable("organizationId") String organizationId @PathVariable("licenseId") String licenseId Наконец, рассмотрим возвращаемый объект ResponseEntity. Он представляет весь HTTP-ответ, включая код состояния, заголовки и тело. В листинге 3.4 мы возвращаем объект License в теле ответа и код состояния 200 (OK). Теперь, получив представление о том, как определить конечную точку для HTTP-глагола GET, продолжим и добавим методы для глаголов POST, PUT и DELETE, которые будут создавать, обновлять и удалять экземпляры класса License соответственно (см. листинг 3.5). Листинг 3.5. Остальные конечные точки HTTP package com.optimagrowth.license.controller; import import import import import import import import import import org.springframework.beans.factory.annotation.Autowired; org.springframework.http.ResponseEntity; org.springframework.web.bind.annotation.DeleteMapping; org.springframework.web.bind.annotation.PathVariable; org.springframework.web.bind.annotation.PostMapping; org.springframework.web.bind.annotation.PutMapping; org.springframework.web.bind.annotation.RequestBody; org.springframework.web.bind.annotation.RequestMapping; org.springframework.web.bind.annotation.RequestMethod; org.springframework.web.bind.annotation.RestController; import com.optimagrowth.license.model.License; import com.optimagrowth.license.service.LicenseService; @RestController @RequestMapping(value="v1/organization/{organizationId}/license") public class LicenseController { @Autowired private LicenseService licenseService; Глава 3 Создание микросервисов с использованием Spring Boot 108 @RequestMapping(value="/{licenseId}",method = RequestMethod.GET) public ResponseEntity<License> getLicense( @PathVariable("organizationId") String organizationId, @PathVariable("licenseId") String licenseId) { License license = licenseService.getLicense(licenseId, organizationId); return ResponseEntity.ok(license); } Метод Put для обновления лицензии @PutMapping public ResponseEntity<String> updateLicense( @PathVariable("organizationId") Отображает тело HTTP-запроса в объект String organizationId, License @RequestBody License request) { return ResponseEntity.ok(licenseService.updateLicense(request, organizationId)); } Метод Post для добавления лицензии @PostMapping public ResponseEntity<String> createLicense( @PathVariable("organizationId") String organizationId, @RequestBody License request) { return ResponseEntity.ok(licenseService.createLicense(request, organizationId)); } Метод Delete для удаления лицензии @DeleteMapping(value="/{licenseId}") public ResponseEntity<String> deleteLicense( @PathVariable("organizationId") String organizationId, @PathVariable("licenseId") String licenseId) { return ResponseEntity.ok(licenseService.deleteLicense(licenseId, organizationId)); } } В листинге 3.5 мы сначала добавили метод updateLicense() с аннотацией @PutMapping. Эта аннотация является сокращенной формой аннотации @RequestMapping(method=RequestMethod.PUT), которую мы еще не использовали. Обратите также внимание на использование аннотаций @PathVariable и @RequestBody в списке параметров метода updateLicense(). Аннотация @RequestBody отображает тело HTTP-запроса в объект (в данном случае в объект License). В методе updateLicense() мы отображаем параметр из URL и тело HTTP-запроса в два параметра самого метода: @PathVariable("organizationId") String organizationId @RequestBody License request И наконец, обратите внимание на аннотации @PostMapping и @ DeleteMapping, которые мы применили к методам createLicense() Точка зрения разработчика: создание микросервиса с использованием ... 109 и deleteLicense() соответственно. Аннотация @PostMapping на уровне метода – это сокращенная форма аннотации: @RequestMapping(method = RequestMethod.POST) Аннотация @DeleteMapping (value="/{licenseId}") на уровне метода тоже действует как сокращенная форма аннотации: @RequestMapping(value="/{licenseId}",method = RequestMethod.DELETE) Имена конечных точек имеют значение Прежде чем двинуться дальше по пути разработки микросервисов, вы (и другие команды в вашей организации) должны определить для себя стандарты оформления конечных точек ваших служб. URL (Uniform Resource Locator – унифицированный указатель ресурса) микросервиса должен четко сообщать назначение службы, какими ресурсами она управляет и какими отношениями связаны эти ресурсы. Мы полагаем, что вам пригодятся следующие рекомендации по выбору имен конечных точек служб. Используйте понятные URL, которые сообщают, какие ресурсы представляет служба. Применение канонического формата для определения адресов URL поможет сделать ваш API более понятным и простым в использовании, а также обеспечит единообразие соглашений об именах. Используйте URL, которые сообщают об отношениях между ресурсами. Часто ресурсы связаны отношением родитель–потомок, когда ресурс–потомок не может существовать вне контекста ресурса– родителя, и по этой причине у вас может не быть отдельного микросервиса для представления потомков. Используйте такие URL, которые выражают эти отношения. Если вы обнаружите, что ваши URL получаются слишком длинными и вложенными, то, возможно, ваш микросервис имеет слишком широкий круг обязанностей. С самого начала позаботьтесь о схеме управления версиями для URL. URL и соответствующие конечные точки определяют контракт между владельцем и потребителем службы. Одна из распространенных схем заключается в добавлении во все конечные точки номера версии. Позаботьтесь о схеме управления версиями как можно раньше и неукоснительно следуйте ей. Чрезвычайно сложно внедрить в URL поддержку версионирования (например, добавить /v1/) после появления нескольких потребителей, использующих старые URL. Итак, теперь у нас есть несколько служб. Теперь откройте окно терминала, перейдите в корневой каталог проекта, где находится файл pom.xml, и выполните следующую команду Maven (на рис. 3.4 показан ожидаемый результат этой команды): mvn spring-boot:run 110 Глава 3 Создание микросервисов с использованием Spring Boot Служба лицензий запущена и прослушивает порт 8080 Рис. 3.4. Как показывает этот вывод, служба лицензий успешно запустилась После запуска службы можно обратиться к ее конечным точкам. Мы настоятельно рекомендуем использовать для этого инструмент на основе Chrome, например Postman или cURL. На рис. 3.5 показано, как вызывать конечные точки GET и DELETE службы. В ответ на вызов конечной точки GET возвращается строка в формате JSON с информацией о лицензии В ответ на вызов конечной точки DELETE возвращается простая строка с сообщением Рис. 3.5. Вызовы конечных точек GET и DELETE службы с использованием Postman Точка зрения разработчика: создание микросервиса с использованием ... 111 На рис. 3.6 показано, как вызывать конечные точки POST и PUT службы с помощью URL http://localhost:8080/v1/organization/optimaGrowth/license. Рис. 3.6. Вызовы конечных точек GET и DELETE службы с использованием Postman После реализации методов для HTTP-глаголов PUT, DELETE, POST и GET мы можем перейти к интернационализации. Глава 3 Создание микросервисов с использованием Spring Boot 112 3.3.2. Добавление интернационализации в службу лицензий Интернационализация – важное требование, позволяющее приложению адаптироваться к различным языкам. Основная цель интернационализации – организовать в приложении возможность возвращать контент в разных форматах и на разных языках. В этом разделе мы покажем, как добавить интернационализацию в созданную нами службу лицензий. Для этого обновим класс инициализации LicenseServiceApplication.java и создадим в нем компоненты LocaleResolver и ResourceBundleMessageSource. В листинге 3.6 показано, как должен выглядеть обновленный класс инициализации. Листинг 3.6. Создание bean-компонентов в классе инициализации package com.optimagrowth.license; import java.util.Locale; import import import import import import org.springframework.boot.SpringApplication; org.springframework.boot.autoconfigure.SpringBootApplication; org.springframework.context.annotation.Bean; org.springframework.context.support.ResourceBundleMessageSource; org.springframework.web.servlet.LocaleResolver; org.springframework.web.servlet.i18n.SessionLocaleResolver; @SpringBootApplication public class LicenseServiceApplication { public static void main(String[] args) { SpringApplication.run(LicenseServiceApplication.class, args); } Устанавливает локаль US как локаль по умолчанию @Bean public LocaleResolver localeResolver() { SessionLocaleResolver localeResolver = new SessionLocaleResolver(); localeResolver.setDefaultLocale(Locale.US); return localeResolver; } @Bean public ResourceBundleMessageSource messageSource() { ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource(); messageSource.setUseCodeAsDefaultMessage(true); messageSource.setBasenames("messages"); return messageSource; } } Не генерирует ошибку, если сообщение не найдено, а возвращает код сообщения вместо этого Задает базовое имя файлов с переводами сообщений на разные языки Точка зрения разработчика: создание микросервиса с использованием ... 113 Первое, что следует отметить в листинге 3.6, – мы установили локаль Locale.US в качестве локали по умолчанию. Если не установить локаль по умолчанию, то messageSource будет использовать локаль по умолчанию, установленную компонентом LocaleResolver. Затем обратите внимание на следующий вызов: messageSource.setUseCodeAsDefaultMessage(true) Когда искомое сообщение не обнаруживается, то этот вызов гарантирует возврат кода сообщения 'license.creates.message' вместо ошибки: "No message found under code 'license.creates.message' for locale 'es' Наконец, вызов messageSource.setBasenames("messages") устанавливает messages как базовое имя для файлов с переводами сообщений. Например, в Италии мы бы использовали Locale.IT и файл с именем messages_it.properties. Если сообщение на выбранном языке не будет найдено, то будет предпринята попытка найти сообщение в файле по умолчанию, который называется messages. properties. Теперь давайте настроим сами сообщения. В этом примере мы будем определим сообщения на английском и испанском языках. Для этого мы создадим следующие файлы в папке /src/main/ resources: messages_en.properties; messages_es.properties; messages.properties. В следующих листингах 3.7 и 3.8 показано содержимое файлов messages_en.properties и messages_es.properties. Листинг 3.7. Содержимое файла messages_en.properties license.create.message = License created %s license.update.message = License %s updated license.delete.message = Deleting license with id %s for the organization %s Листинг 3.8. Содержимое файла messages_es.properties license.create.message = Licencia creada %s license.update.message = Licencia %s creada license.delete.message = Eliminando licencia con id %s para la organization %s license Теперь, когда реализовав сообщения и добавив bean-компоненты, добавим в службу использование ресурса с сообщениями. В листинге 3.9 показано, как это сделать. Глава 3 Создание микросервисов с использованием Spring Boot 114 Листинг 3.9. Поиск сообщений в MessageSource @Autowired MessageSource messages; Получает локаль public String createLicense(License license, через параметр String organizationId, метода Locale locale){ String responseMessage = null; if (license != null) { license.setOrganizationId(organizationId); responseMessage = String.format(messages.getMessage( "license.create.message", null,locale), license.toString()); Использует полу} ченную локаль для return responseMessage; выбора конкретного сообщения } public String updateLicense(License license, String organizationId){ String responseMessage = null; if (license != null) { license.setOrganizationId(organizationId); responseMessage = String.format(messages.getMessage( "license.update.message", null, null), license.toString()); Использует пустую ло} каль для выбора конкретного сообщения return responseMessage; } В листинге 3.9 следует отметить три важных момента. Во-первых, локаль можно получить от самого контроллера. Во-вторых, мы можем вызвать messages.getMessage("license.create.message", null, locale), используя локаль, полученную в параметре, чтобы извлечь требуемое сообщение на заданном языке. И в-третьих, мы можем вызвать messages.getMessage("license.update.message", null, null) с пустой локалью. В последнем случае приложение будет использовать локаль по умолчанию, которую мы задали в классе инициализации. Теперь давайте обновим метод createLicense() контроллера, чтобы он получал локаль из заголовка Accept-Language запроса, как показано ниже: @PostMapping public ResponseEntity<String> createLicense( @PathVariable("organizationId") String organizationId, @RequestBody License request, @RequestHeader(value = "Accept-Language",required = false) Locale locale){ return ResponseEntity.ok(licenseService.createLicense( request, organizationId, locale)); } Точка зрения разработчика: создание микросервиса с использованием ... 115 Здесь мы используем аннотацию @RequestHeader, которая отображает значение заголовка Accept-Language запроса в параметр метода locale. Этот параметр не является обязательным, поэтому если в запросе отсутствует заголовок Accept-Language, то приложение будет использовать локаль по умолчанию. На рис. 3.7 показано, как отправить заголовок запроса Accept-Language из Postman. ПРИМЕЧАНИЕ. Не существует четко определенных правил использования локали, поэтому мы советуем проанализировать вашу архитектуру и выбрать наиболее подходящий вариант. Например, если клиентское приложение поддерживает локали, то лучший вариант – получение локали через параметр метода контроллера. Но если управление локалью осуществляется на сервере, то вы можете использовать локаль по умолчанию. Рис. 3.7. Установка заголовка Accept-Language в запросе POST к службе лицензий 3.3.3.Реализация Spring HATEOAS для отображения связанных ссылок HATEOAS расшифровывается как Hypermedia as the Engine of Application State (Гипермедиа как средство изменения состояния приложения). Spring HATEOAS – это небольшой проект, который позволяет создавать API, следующие принципу HATEOAS отображения связанных ссылок для данного ресурса. Принцип HATEOAS гласит, что API должен помогать клиенту, возвращая с каждым ответом службы информацию о возможных следующих шагах. Это не является основной или обязательной функцией, но если вы хотите организовать для своих клиентов исчерпывающую помощь по всем службам API для данного ресурса, то это отличный вариант. 116 Глава 3 Создание микросервисов с использованием Spring Boot С помощью Spring HATEOAS можно быстро создавать классы моделей со ссылками на ресурсы, предоставляемые этими моделями. В Spring HATEOAS также имеется построитель для создания конкретных ссылок, указывающих на методы контроллера Spring MVC. В следующем фрагменте кода показано, как HATEOAS должен представлять службу лицензий: "_links": { "self" : { "href" : "http://localhost:8080/v1/organization/ optimaGrowth/license/0235431845" }, "createLicense" : { "href" : "http://localhost:8080/v1/organization/ optimaGrowth/license" }, "updateLicense" : { "href" : "http://localhost:8080/v1/organization/ optimaGrowth/license" }, "deleteLicense" : { "href" : "http://localhost:8080/v1/organization/ optimaGrowth/license/0235431845" } } В этом разделе мы покажем, как реализовать принципы Spring HATEOAS в службе лицензий. Первое, что нужно сделать для отправки в ответе связанных ссылок на ресурс, – добавить зависимость HATEOAS в файл pom.xml, как показано ниже: <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-hateoas</artifactId> </dependency> Добавив зависимость, мы должны обновить класс License и добавить наследование RepresentationModel<License>, как показано в листинге 3.10. Листинг 3.10. Наследование класса RepresentationModel package com.optimagrowth.license.model; import org.springframework.hateoas.RepresentationModel; import lombok.Getter; import lombok.Setter; import lombok.ToString; @Getter @Setter @ToString public class License extends RepresentationModel<License> { Точка зрения разработчика: создание микросервиса с использованием ... private private private private private private 117 int id; String licenseId; String description; String organizationId; String productName; String licenseType; } RepresentationModel<License> дает возможность добавлять ссылки на класс модели License. Теперь, выполнив необходимые настройки, создадим конфигурацию HATEOS, чтобы получить ссылки для класса LicenseController. В листинге 3.11 показано, как это делается. В этом примере мы изменим только метод getLicense(). Листинг 3.11. Добавление ссылок для LicenseController @RequestMapping(value="/{licenseId}",method = RequestMethod.GET) public ResponseEntity<License> getLicense( @PathVariable("organizationId") String organizationId, @PathVariable("licenseId") String licenseId) { License license = licenseService.getLicense(licenseId, organizationId); license.add(linkTo(methodOn(LicenseController.class) .getLicense(organizationId, license.getLicenseId())) .withSelfRel(), linkTo(methodOn(LicenseController.class) .createLicense(organizationId, license, null)) .withRel("createLicense"), linkTo(methodOn(LicenseController.class) .updateLicense(organizationId, license)) .withRel("updateLicense"), linkTo(methodOn(LicenseController.class) .deleteLicense(organizationId, license.getLicenseId())) .withRel("deleteLicense")); return ResponseEntity.ok(license); } Метод add() – это метод класса RepresentationModel. Метод linkTo проверяет класс LicenseController и получает корневое представление, а метод methodOn получает представление метода, выполняя фиктивный вызов целевого метода. Оба метода являются статическими методами org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.WebMvcLinkBuilder – это вспомогательный класс, предназначенный для создания ссылок на классы контроллеров. На рис. 3.8 показаны ссылки в теле ответа, возвращаемом методом getLicense(). Чтобы получить их, нужно выполнить HTTP-запрос GET. На данный момент мы имеем действующий каркас службы. Но в действительности эта служба еще не закончена. Хорошая 118 Глава 3 Создание микросервисов с использованием Spring Boot микросервисная архитектура предполагает четкое разделение службы на уровни бизнес-логики и доступа к данным. Далее, в следующих главах мы продолжим дорабатывать эту службу и покажем, как ее структурировать. А пока перейдем к последней точке зрения и посмотрим, как инженер DevOps будет вводить упаковывать эту службу и развертывать ее в облаке. Рис. 3.8. Ссылки HATEOAS в теле ответа, возвращаемого службой лицензий на HTTP-запрос GET 3.4. Точка зрения инженера DevOps: сборка выполняемых артефактов DevOps – это богатая и развивающаяся область ИТ. Задача инженера DevOps при работе с микросервисом заключается в управлении службой после того, как она будет передана в эксплуатацию. Разработка программного кода часто оказывается самой простой частью. Поддерживать этот код в работоспособном состоянии сложнее. Начнем обсуждение вопросов DevOps с перечисления четырех принципов, которые мы будем использовать далее в этой книге. Точка зрения инженера DevOps: сборка выполняемых артефактов 119 Микросервис должен быть автономным. Он должен поддерживать возможность независимого развертывания нескольких экземпляров, которые запускаются и останавливаются как единственный программный артефакт. Микросервис должен быть настраиваемым. В момент запуска экземпляр службы должен читать данные, необходимые для настройки, из центрального хранилища или из переменных окружения. Настройка службы не должна требовать вмешательства человека. Доступ к экземпляру микросервиса должен быть прозрачным для клиента. Клиент не должен знать точное местонахождение службы. Доступ ко всем службам должен осуществляться через агента обнаружения служб. Это позволяет приложению находить экземпляры микросервисов, не зная их физического местоположения. Микросервис должен сообщать о своем состоянии. Это важная черта облачной архитектуры. Экземпляры микросервисов обязательно будут выходить из строя, и агенты обнаружения должны иметь возможность направлять запросы в обход неисправных экземпляров. В этой книге для мониторинга состояния каждого микросервиса мы будем использовать Spring Boot Actuator. Эти четыре принципа раскрывают парадокс разработки микросервисов. Микросервисы меньше по размеру и масштабу, но при их использовании в приложении образуется больше движущихся частей, потому что микросервисы распределяются и запускаются независимо друг от друга в своих собственных контейнерах. Это влечет необходимость иметь более высокий уровень координации, потому что в приложении появляется больше точек отказа. С точки зрения DevOps мы должны заранее удовлетворить требования микросервисов к техническому и технологическому оснащению и преобразовать эти четыре принципа в стандартный набор событий жизненного цикла, начинающегося после создания и развертывания микросервиса. Эти четыре принципа можно представить в виде четырех этапов. На рис. 3.9 показано, как эти четыре этапа сочетаются друг с другом. Сборка службы. На этом этапе осуществляется упаковка и развертывание службы. Этот этап должен гарантировать повторяемость и согласованность процесса, чтобы один и тот же код и среда времени выполнения развертывались одинаково. Инициализация службы. На этом этапе производится настройка службы. Конфигурации приложения и окружения должны отделяться от выполняемого кода, чтобы позволить быстро развернуть и запустить экземпляр микросервиса в любом окружении без вмешательства человека. Глава 3 Создание микросервисов с использованием Spring Boot 120 Регистрация/обнаружение службы. На этом этапе происходит регистрация нового экземпляра микросервиса после развертывания и инициализации, чтобы сделать его доступным для обнаружения другими клиентами приложения. Мониторинг службы. В облачном окружении обычно действует несколько экземпляров одной и той же службы согласно требованиям высокой доступности. С точки зрения DevOps мы должны осуществлять мониторинг экземпляров микросервисов и гарантировать маршрутизацию в обход любых отказавших экземпляров и их остановку. 1. Сборка Механизм Выполняемый файл JAR сборки/ развертывания 2. Инициализация Репозиторий с конфигурацией 3. Обнаружение Агент обнаружения служб 4. Мониторинг Service discovery agent Сбой x Запуск экземпляра службы Репозиторий с исходным кодом Множество экземпляров службы Клиент службы Multiple service instances Рис. 3.9. Жизненный цикл микросервиса делится на несколько этапов 3.4.1. Сборка службы: упаковка и развертывание микросервисов С точки зрения DevOps одним из ключевых требований к микросервисной архитектуре является возможность быстрого развертывания нескольких экземпляров микросервиса в ответ на изменившиеся условия (например, внезапный приток пользователей, проблемы в инфраструктуре и т. д.). Для этого микросервис должен быть упакован в единый артефакт со всеми его зависимостями. В число этих зависимостей также должна входить среда времени выполнения (например, HTTP-сервер или контейнер приложения), в которой размещается микросервис. Процесс компиляции, упаковки и развертывания называется сборкой (assembly; этап 1 на рис. 3.9). На рис. 3.10 показаны дополнительные сведения об этом этапе. Точка зрения инженера DevOps: сборка выполняемых артефактов Механизм сборки/развертывания выполняет компиляцию, используя сценарии Spring Boot для Maven Когда разработчик отправляет свой код в репозиторий, механизм сборки/развертывания компилирует и упаковывает его 1. Сборка Механизм сборки/развертывания Выполняемый файл JAR 121 Результатом компиляции является единственный выполняемый файл JAR, включающий выполняемый код приложения и среду времени выполнения Репозиторий с исходным кодом Рис. 3.10. На этапе сборки (assembly) микросервиса исходный код компилируется и упаковывается в один выполняемый файл вместе со средой времени выполнения К счастью, почти все фреймворки для разработки микросервисов на Java включают среду времени выполнения, которую можно упаковать и развернуть вместе с кодом. Например, как показано на рис. 3.10, Maven и Spring Boot создают выполняемый файл JAR, включающий среду времени выполнения, такую как Tomcat. Команда в следующем примере упаковывает службу лицензий в выполняемый файл JAR, а затем запускает его: mvn clean package && java -jar target/licensing-service-0.0.1-SNAPSHOT.jar Для некоторых инженеров DevOps концепция встраивания среды времени выполнения прямо в файл JAR является серьезным сдвигом в их взглядах на развертывание приложений. Обычные веб-приложения на Java развертываются на сервере приложений. Эта модель подразумевает, что сервер приложений является самостоятельной сущностью и часто управляется группой системных администраторов, контролирующих конфигурацию сервера независимо от приложений, которые на этом сервере развертываются. Такое разделение конфигурации сервера и приложений приводит к появлению точек отказа в процессе развертывания. Это связано с тем, что во многих организациях конфигурация серверов приложений не хранится в системе управления версиями и управляется с помощью комбинации пользовательского интерфейса и отдельных сценариев администрирования. В таких случаях слишком высока вероятность просачивания дрейфа конфигурации в окружение сервера приложений и вызвать случайное на первый взгляд отключение. 122 Глава 3 Создание микросервисов с использованием Spring Boot 3.4.2. Инициализация службы: управление конфигурацией микросервисов Инициализация службы (этап 2 на рис. 3.9) выполняется при запуске микросервиса, когда ему требуется загрузить информацию о конфигурации. На рис. 3.11 представлено более подробное описание процесса инициализации. 2. Инициализация Репозиторий с конфигурацией В идеале хранилище конфигурации должно иметь возможность версионирования изменений в конфигурации и хранить информацию о том, кто последним внес изменения Запуск экземпляра службы При запуске микросервиса все настройки окружения и/или приложения должны: • передаваться в запускаемую службу через переменные окружения; • извлекаться из централизованного репозитория с конфигурацией Если конфигурация службы изменится, то все службы, использующие старую конфигурацию, должны быть остановлены или извещены о необходимости прочитать новую конфигурацию Рис. 3.11. Во время запуска (начальной загрузки) микросервис читает свою конфигурацию из центрального репозитория Любой разработчик знает, что иногда бывает желательно настроить поведение приложения во время выполнения. Обычно этот процесс включает чтение конфигурации из файла свойств, развернутого вместе с приложением, или из хранилища, такого как реляционная база данных. Микросервисы часто сталкиваются с похожими требованиями. Разница в том, что в приложении с микросервисной архитектурой, работающем в облаке, могут быть запущены сотни или даже тысячи экземпляров одного и того же микросервиса. Более того, службы могут быть распределены по всему миру. При большом количестве географически распределенных служб становится невозможно повторно развернуть службы, чтобы они прочитали обновленную конфигурацию. Хранение данных во внешнем хранилище решает эту проблему. Но микросервисы в облаке привносят ряд дополнительных сложностей. Конфигурации обычно имеют простую структуру, часто читаются и редко записываются. В этой ситуации реляционные базы данных избыточны, потому что они предназначены для Точка зрения инженера DevOps: сборка выполняемых артефактов 123 управления гораздо более сложными моделями данных, чем простой набор пар ключ/значение. Данные с информацией о конфигурации часто читаются, но редко меняются, поэтому они должны обеспечивать минимальную задержку для чтения. Хранилище должно быть высокодоступным и находиться рядом со службами, читающими свои конфигурации из этого хранилища. Хранилище с конфигурациями не может выйти из строя полностью, иначе оно станет единственной точкой отказа для вашего приложения. В главе 5 мы покажем, как управлять конфигурациями приложения с микросервисной архитектурой, используя простое хранилище пар ключ/значение. 3.4.3.Регистрация и обнаружение службы: взаимодействие клиентов с микросервисами С точки зрения клиента доступ к микросервису должен быть прозрачным и не зависеть от его физического местоположения, потому что серверы в облачном окружении эфемерны. Эфемерность означает, что серверы, где размещена служба, обычно имеют более короткий жизненный цикл, чем жизненный цикл службы, работающей в корпоративном центре обработки данных. Облачные службы можно останавливать и быстро запускать на серверах с совершенно другими IP-адресами. Настаивая на том, что службы должны рассматриваться как недолговечные одноразовые объекты, микросервисные архитектуры позволяют достичь высокой степени масштабируемости и доступности за счет запуска нескольких экземпляров службы. Масштабом и отказоустойчивостью можно управлять так быстро, как того требует ситуация. Каждой службе назначается уникальный и непостоянный IP-адрес. Обратной стороной эфемерных служб является невозможность вручную управлять большим пулом постоянно появляющихся и исчезающих экземпляров этих служб. Экземпляр микросервиса должен зарегистрироваться в стороннем агенте, который называют агентом обнаружения служб (см. этап 3 на рис. 3.9, а также рис. 3.12, где этот этап описан более подробно). Когда экземпляр микросервиса регистрируется в агенте обнаружения служб, он сообщает агенту свой физический IP-адрес (или доменный адрес) и логическое имя, которое приложение может использовать для поиска службы. Некоторые агенты обнаружения служб также требуют указывать URL, который они смогут затем использовать для выполнения проверок работоспособности зарегистрировавшихся служб. В таком сценарии клиент обращается к агенту обнаружения, чтобы узнать местоположение службы. 124 Глава 3 Создание микросервисов с использованием Spring Boot 3. Обнаружение Агент обнаружения служб Запуск экземпляра службы Когда экземпляр службы запускается, он регистрируется в агенте обнаружения служб Множество экземпляров службы Клиент службы Клиент службы никогда не знает физического местоположения экземпляра службы. Вместо этого он запрашивает у агента обнаружения служб адрес работоспособного экземпляра Рис. 3.12. Агент обнаружения служб абстрагирует физическое местоположение службы 3.4.4. Мониторинг состояния микросервиса Агент обнаружения службы действует не только как регулировщик, направляющий клиентов к тем или иным службам. В облачных приложениях с микросервисной архитектурой часто одновременно действует несколько экземпляров службы. Рано или поздно один из этих экземпляров обязательно потерпит сбой. Агент обнаружения служб наблюдает за работоспособностью каждого зарегистрированного экземпляра и удаляет все отказавшие экземпляры из своих таблиц маршрутизации, чтобы гарантировать, что клиенты не будут отправлены к отказавшему экземпляру. После запуска микросервиса агент обнаружения служб продолжит наблюдать за ним и проверять связь с его интерфейсом проверки работоспособности, чтобы убедиться, что служба доступна. Это этап 4 на рис. 3.9 (рис. 3.13 содержит дополнительную информацию об этом этапе). Точка зрения инженера DevOps: сборка выполняемых артефактов Агент обнаружения служб следит за работоспособностью экземпляра службы. Если экземпляр выходит из строя, то он удаляется из пула доступных экземпляров 125 4. Мониторинг Агент обнаружения служб x Сбой Множество экземпляров службы Большинство экземпляров службы предоставляет URL для проверки работоспособности, который будет использоваться агентом обнаружения служб. Если вызов вернет ошибку HTTP или микросервис не ответит своевременно, то агент обнаружения служб может исключить этот экземпляр из своего пула или просто перестать направлять ему трафик Рис. 3.13. Агент обнаружения служб использует предоставленный ему URL для проверки состояния микросервиса После создания согласованного интерфейса проверки работоспособности можно использовать облачные инструменты мониторинга для обнаружения проблем и реагирования на них. Если агент обнаружения служб выявляет проблему с экземпляром службы, он может, например, остановить отказавший экземпляр или запустить дополнительные экземпляры службы. Самый простой способ создать интерфейс проверки работоспособности в окружении микросервисов – реализовать конечную точку HTTP, возвращающую данные в формате JSON и код состояния HTTP. В микросервисах, не основанных на Spring Boot, разработчик часто обязан написать конечную точку, которая информацию о работоспособности службы. В Spring Boot такие конечные точки реализуются тривиально просто, достаточно лишь изменить файл сборки Maven и подключить модуль Spring Actuator. Spring Actuator предоставляет готовые конечные точки, которые помогут вам проверять работоспособность вашей службы и управлять ею. Чтобы задействовать Spring Actuator, нужно включить следующие зависимости в файл сборки Maven: <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> Если после этого обратиться к конечной точке http:// localhost:8080/actator/health в службе лицензий, она вернет данные, с информацией о работоспособности. На рис. 3.14 представлен пример возвращаемых данных. 126 Глава 3 Создание микросервисов с использованием Spring Boot Это может быть http://localhost:8080/actuator/health или http://localhost:8080/health, в зависимости от конфигурации модуля в application.properties По умолчанию встроенная конечная точка проверки работоспособности в Spring Boot возвращает признак работоспособности службы и некоторую дополнительную информацию, например сколько места на диске осталось на сервере Рис. 3.14. Проверка работоспособности каждого экземпляра службы позволяет инструментам мониторинга определить, продолжает ли работать тот или иной экземпляр Как показано на рис. 3.14, проверка работоспособности может служить не только индикатором доступности службы. Она также может предоставить информацию о состоянии сервера, где запущен экземпляр. Spring Actuator позволяет изменить конфигурацию по умолчанию через файл свойств приложения. Например: management.endpoints.web.base-path=/ management.endpoints.enabled-by-default=false management.endpoint.health.enabled=true management.endpoint.health.show-details=always management.health.db.enabled=false management.health.diskspace.enabled=true Объединение точек зрения 127 Первая строка позволяет настроить базовый путь для всех служб Actuator (например, конечная точка проверки работоспособности с такими настройками будет доступна по URL http://localhost:8080/ health). Остальные настройки позволяют отключить ненужные и включить нужные службы. ПРИМЕЧАНИЕ. Если вам интересно побольше узнать обо всех службах, предоставляемых Spring Actuator, прочитайте документацию Spring по адресу https://docs.spring.io/spring-boot/docs/ current/reference/html/production-ready-endpoints.html. 3.5. Объединение точек зрения Облачные микросервисы кажутся простыми, но эта простота обманчива. Чтобы добиться успеха с ними, нужно иметь целостное представление, объединяющее точки зрения архитектора, разработчика и инженера DevOps. Вот ключевые выводы по каждой из этих точек зрения. Архитектор. Сосредоточьтесь на естественных контурах вашей бизнес-задачи. Опишите предметную область, озвучьте получившееся описание и послушайте самого себя. В ходе этого процесса вы непременно выявите кандидатов на микросервисы. Помните также, что лучше начать с больших микросервисов и позднее выполнить рефакторинг, разбив крупные микросервисы на более мелкие, чем с большого количества небольших микросервисов. Хорошие микросервисные архитектуры не появляются в одночасье и требуют постепенной проработки. Инженер-программист (он же разработчик). Небольшой размер службы не означает, что базовые принципы проектирования можно выбросить в утиль. Сосредоточьтесь на создании многоуровневой службы, каждый уровень которой имеет свой круг обязанностей. Избегайте соблазна создавать фреймворки в своем коде и постарайтесь сделать каждый микросервис полностью независимым. Преждевременное проектирование и внедрение фреймворков может повлечь огромные затраты на обслуживание позднее. Инженер DevOps. Службы существуют не в вакууме. С самого начала определите, как должен выглядеть жизненный цикл ваших служб. Внимание инженера DevOps должно быть сосредоточено не только на автоматизации сборки и развертывания службы, но также на мониторинге ее работоспособности и действиях, которые должны выполняться, когда что-то пойдет не так. Ввод в эксплуатацию службы часто требует больше работы и предусмотрительности, чем написание бизнес-логики. В приложении C мы расскажем, как этого добиться с помощью Prometheus и Grafana. 128 Глава 3 Создание микросервисов с использованием Spring Boot Итоги Чтобы добиться успеха с микросервисами, необходимо объединить три точки зрения: архитектора, разработчика и инженера DevOps. Микросервисы, являясь мощной архитектурной парадигмой, имеют свои преимущества и недостатки. Не все приложения выиграют от переноса в микросервисную архитектуру. С точки зрения архитектора микросервисы – это небольшие, автономные и распределенные службы. Микросервисы должны иметь четкие границы и управлять небольшими наборами данных. С точки зрения разработчика, микросервисы обычно создаются с использованием архитектурного стиля REST и формата JSON для передачи данных. Основная цель интернационализации – получить приложения, предлагающие контент в различных форматах и на разных языках. HATEOAS расшифровывается как Hypermedia as the Engine of Application State (Гипермедиа как средство изменения состояния приложения). Spring HATEOAS – это небольшой проект, который позволяет создавать API, следующие принципу HATEOAS отображения связанных ссылок для данного ресурса. С точки зрения инженера DevOps, решающее значение имеет порядок упаковки, развертывания и мониторинга микросервисов. По умолчанию Spring Boot позволяет упаковать службу в единственный выполняемый файл JAR, вмещающий не только службу, но также среду времени выполнения, такую как сервер Tomcat. Модуль Spring Actuator, входящий в состав фреймворка Spring Boot, предоставляет информацию о работоспособности службы вместе с некоторыми сведениями времени выполнения. 4 Добро пожаловать в Docker Эта глава: рассказывает о важности контейнеров; показывает, как контейнеры вписываются в архитектуру микросервисов; перечисляет различия между виртуальной машиной и контейнером; описывает особенности использования Docker и его основных компонентов; обсуждает вопросы интеграции Docker с микросервисами. Чтобы продолжить создание микросервисов, нам необходимо решить проблему переносимости и определить, как запускать микросервисы в различных технологических окружениях? Переносимость – это возможность использовать программное обеспечение в разных окружениях. В последние годы контейнеры приобретают все большую популярность, превращаясь из «полезной» в «обязательную» программную архитектуру. Контейнеры предлагают гибкий и удобный способ переноса и выполнения любого программного обеспечения с одной платформы на другую (например, с машины разработчика на физический или виртуальный корпоративный сервер). Мы можем заменить традиционные модели веб-серверов меньшими и гораздо более гибкими виртуализированными программными контейнерами, обладающими такими преимуществами, как скорость, переносимость и масштабируемость. 130 Глава 4 Добро пожаловать в Docker В этой главе дается краткое введение в контейнеры Docker – технологию, выбранную нами за то, что ее можно использовать со всеми основными поставщиками облачных услуг. Мы расскажем, что такое Docker, как использовать его основные компоненты и как интегрировать технологию Docker с нашими микросервисами. К концу главы вы научитесь создавать собственные образы контейнеров Docker с помощью Maven и запускать в них свои микросервисы. Также вы узнаете, что вам больше не нужно заботиться об установке всех необходимых компонентов для работы ваших микросервисов; единственное требование – наличие окружения с установленным программным обеспечением Docker. ПРИМЕЧАНИЕ. В этой главе мы объясним только то, что будем использовать на протяжении всей книги. Желающим узнать больше о Docker мы рекомендуем отличную книгу Джеффа Николоффа (Jeff Nickoloff), Стивена Куэнзли (Stephen Kuenzli) и Брета Фишера (Bret Fisher) «Docker in Action», 2nd ed. (Manning, 2019). В ней авторы подробно рассказывают, что такое Docker и как он работает. 4.1. Контейнеры или виртуальные машины? Во многих компаниях виртуальные машины (ВМ) продолжают оставаться стандартом де-факто для развертывания программного обеспечения. В этом разделе мы рассмотрим основные различия между виртуальными машинами и контейнерами. ВМ – это программная среда, имитирующая работу одного компьютера на другом компьютере. Виртуальные машины основаны на гипервизоре, имитирующем всю физическую машину и выделяющем желаемый объем системной памяти, ядер процессора, дискового хранилища и других ресурсов. С другой стороны, контейнер – это пакет, содержащий виртуальную операционную систему (ОС) и позволяющий запускать приложения с их зависимостями в изолированном и независимом окружении. Обе технологии имеют некоторое сходство, например наличие гипервизора или механизма управления контейнерами, обеспечивающего работу технологии, но способ их реализации делает виртуальные машины и контейнеры очень разными. На рис. 4.1 показаны самые основные различия между виртуальными машинами и контейнерами. Контейнеры или виртуальные машины? 131 Виртуальная машина Приложение 1 Приложение 2 Библиотеки Библиотеки Приложение 3 Библиотеки Гостевая ОС Гостевая ОС Гостевая ОС Контейнер Приложение 1 Приложение 2 Библиотеки Библиотеки Приложение 3 Библиотеки Гипервизор Container engine Несущая ОС Несущая ОС Оборудование сервера Оборудование сервера Рис. 4.1. Основные различия между виртуальными машинами и контейнерами. Контейнерам не нуждаются в гостевой операционной системе или в гипервизоре для выделения ресурсов – они выполняются механизмом управления контейнеров После беглого взгляда на рис. 4.1 может показаться, что между виртуальными машинами и контейнерами нет большой разницы. В конце концов, контейнеры на этом рисунке отличаются только отсутствием уровня гостевой ОС, а гипервизор заменил механизм управления контейнерами. Однако различия между виртуальными машинами и контейнерами огромны. При создании виртуальной машины мы должны заранее определить, сколько физических ресурсов понадобится, например количество виртуальных процессоров или объем ОЗУ и дискового пространства. Выяснение этих значений может быть сложной задачей, решая которую следует учитывать следующее: процессоры могут совместно использоваться несколькими виртуальными машинами; дисковое пространство, доступное виртуальной машине, можно ограничить ее потребностями. Вы можете задать максимальный размер диска, но активно будет использоваться только некоторая его часть; память, зарезервированная за одной виртуальной машиной, не может использоваться другими виртуальными машинами. При использовании контейнеров тоже можно ограничить объем памяти и количество процессоров, доступных контейнеру, например, с помощью Kubernetes, но это не обязательно. Если не задать эти ограничения, то механизм управления контейнерами автоматически выделит необходимые ресурсы для правильной работы контейнера. Поскольку контейнерам не нужна отдельная ОС, это снижает нагрузку на физическую машину, а также уменьшает потребность в памяти и сокращает время запуска приложения. То есть контейнеры намного легче виртуальных машин. 132 Глава 4 Добро пожаловать в Docker Наконец, обе технологии имеют свои достоинства и недостатки, и выбор между ними зависит от конкретных потребностей. Например, если требуется использовать несколько операционных систем для запуска разных приложений на одном сервере или если приложение требует функциональных возможностей ОС, то лучшим выбором будут виртуальные машины. Выбор контейнеров в этой книге является естественным следствием выбора облачной архитектуры. Вместо виртуализации оборудования, как в случае с виртуальными машинами, мы будем использовать контейнеры только для виртуализации уровня ОС, что дает нам гораздо более легковесную альтернативу, по сравнению с запуском микросервисов локально или на отдельном экземпляре облачного сервера. В настоящее время производительность и переносимость являются критически важными характеристиками, влияющими на принятие решений в компаниях. Поэтому важно знать преимущества технологий, которые мы собираемся использовать. В нашем случае, используя контейнеры с микросервисами, мы получим следующие преимущества: контейнеры могут работать повсюду, что упрощает разработку и внедрение, а также увеличивает переносимость; контейнеры позволяют создавать предсказуемые окружения, полностью изолированные от других приложений; контейнеры запускаются и останавливаются быстрее, чем виртуальные машины, что делает их более пригодными для использования в облаке; контейнеры масштабируемы и могут активно использоваться для оптимизации потребления ресурсов, увеличения производительности и удобства обслуживания приложения, работающего внутри контейнера; контейнеры позволяют запустить максимальное количество приложений на минимальном количестве серверов. Теперь, определив различия между виртуальными машинами и контейнерами, давайте подробнее рассмотрим Docker. 4.2. Что такое Docker? Docker – это популярный движок контейнеров с открытым исходным кодом на основе Linux, созданный в марте 2013 года Соломоном Хайксом (Solomon Hykes), основателем и генеральным директором dotCloud. Docker создавался как технология, отвечающая за запуск контейнеров с нашими приложениями внутри и управление ими. Эта технология позволяет совместно использовать ресурсы физической машины несколькими контейнерами без дополнительных затрат на создание виртуальных машин. Что такое Docker? 133 Поддержка Docker со стороны крупных компаний, таких как IBM, Microsoft и Google, позволила превратить новую технологию в фундаментальный инструмент для разработчиков программного обеспечения. В настоящее время Docker продолжает развиваться и является одним из наиболее широко используемых инструментов для развертывания программного обеспечения в контейнерах на любых серверах. ОПРЕДЕЛЕНИЕ. Контейнер – это механизм логической упаковки, предоставляющий приложениям все необходимое для работы. Чтобы лучше понять, как работает Docker, важно отметить, что ядром всей системы Docker является движок Docker Engine. Что такое Docker Engine? Это приложение с архитектурой клиент–сервер. Оно устанавливается на компьютер и состоит из трех важных компонентов: сервера, REST API и интерфейса командной строки (Command-Line Interface, CLI). Эти и другие компоненты Docker показаны на рис. 4.2. Клиент Docker Сервер/хост Docker Реестр Docker Демон Docker создает образы Docker и управляет ими Демон Docker Docker API Когда запускается команда Docker, она посылает инструкции демону Docker Контейнеры Контейнер 1 Образы Образ X Контейнер 2 Docker CLI Контейнер 3 Образ Y Docker Hub Образы Docker можно получить из Docker Hub или из частных реестров Частный реестр После создания и запуска образ Docker создает контейнер Рис. 4.2. Архитектура Docker включает клиента Docker, сервер Docker и реестр Docker Docker Engine содержит следующие компоненты. Демон Docker. Сервер с именем dockerd, который позволяет создавать образы Docker и управлять ими. Интерфейс REST API и клиент командной строки передают инструкции этому демону. Клиент Docker. Пользователи взаимодействуют с Docker через клиента. Когда пользователь запускает команду Docker, клиент командной строки передает инструкции демону. Глава 4 Добро пожаловать в Docker 134 Реестр Docker. Место, где хранятся образы Docker. Реестры могут быть общедоступными или частными. По умолчанию общедоступные реестры размещаются на Docker Hub, но также можно создать свой частный реестр. Образы Docker. Шаблоны, доступные только для чтения, которые содержат инструкции по созданию контейнера Docker. Образы можно получить на Docker Hub и использовать их как есть или изменить и добавить дополнительные инструкции. Также можно создавать новые образы с помощью файла Dockerfile. Далее в этой главе мы объясним, как использовать файлы Dockerfile. Контейнеры Docker. После создания и запуска командой docker run образ Docker создает контейнер. Внутри этого контейнера запускаются приложение и его окружение. Запустить, остановить и удалить контейнер Docker можно с помощью Docker API или интерфейса командной строки Docker CLI. Тома Docker. Тома Docker – предпочтительный механизм хранения данных, созданных Docker и используемых контейнерами. Ими можно управлять с помощью Docker API или Docker CLI. Сети Docker. Позволяют подключать контейнеры к сколь угодно большому количеству сетей. Сети можно рассматривать как средство связи с изолированными контейнерами. Docker содержит следующие пять типов сетевых драйверов: bridge, host, overlay, none и macvlan. На рис. 4.3 показана схема работы Docker. Обратите внимание, что демон Docker отвечает за все действия с контейнерами. Как можно видеть на рисунке, демон получает команды от клиента Docker; эти команды могут отправляться через интерфейс командной строки или REST API. На схеме также видно, как образы Docker, найденные в реестрах, создают контейнеры. Хост Клиент Docker Команды Docker docker pull docker run docker ... Демон Docker Реестр Docker Контейнер 1 Образ 1 Контейнер 2 Образ 2 Контейнер N Образ N Рис. 4.3. Клиент Docker отправляет команды демону Docker, а демон Docker создает контейнеры из образов Файлы Dockerfile 135 ПРИМЕЧАНИЕ. В этой книге мы не будем учить вас устанавливать Docker. Если у вас Docker еще не установлен, то мы советуем обратиться за инструкциями по установке и настройке Docker в Windows, macOS или Linux в документации: https:// docs.docker.com/install/. В следующих разделах мы объясним, как работают компоненты Docker и как их интегрировать с нашим микросервисом лицензий. Если вы не следовали за примерами из глав 1 и 3, то все, что мы будем рассказывать далее, вы можете применить к любому другому проекту Java/Maven, который у вас есть. 4.3. Файлы Dockerfile Dockerfile – это простой текстовый файл, содержащий список инструкций и команд, которые клиент Docker должен запустить, чтобы создать образ. Проще говоря, этот файл автоматизирует процесс создания образа. Команды, используемые в Dockerfile, аналогичны командам Linux, что упрощает изучение файлов Dockerfile. В следующем фрагменте кода представлен пример короткого файла Dockerfile. В разделе 4.5.1 мы покажем, как создавать свои файлы Dockerfile для микросервисов. На рис. 4.4 показан процесс создания образа Docker. FROM openjdk:11-slim ARG JAR_FILE=target/*.jar COPY ${JAR_FILE} app.jar ENTRYPOINT ["java","-jar","/app.jar"] run command build команда Файл Dockerfile Образ Docker Контейнер Docker Рис. 4.4. После создания файла Dockerfile можно запустить команду docker build и создать образ Docker. Когда образ будет готов, из него можно создавать контейнеры командой run В табл. 4.1 перечислены наиболее употребимые команды, которые мы будем использовать в наших файлах Dockerfile. Смотрите также листинг 4.1, где приводится пример содержимого файла Dockerfile. Глава 4 Добро пожаловать в Docker 136 Таблица 4.1. Некоторые команды, используемые в файлах Dockerfile Команда Описание FROM Определяет базовый образ, с которого начинается процесс сборки. Проще говоря, команда FROM определяет образ Docker, который будет использоваться во время выполнения LABEL Добавляет метаданные в образ. Это пара ключ/значение ARG Определяет переменные для передачи построителю, который вызывается командой docker build COPY Копирует новые файлы, каталоги или данные из удаленных URL и добавляет их в файловую систему создаваемого образа, например COPY ${JAR_FILE} app.jar) VOLUME Создает точку монтирования в контейнере. Когда создается новый контейнер с использованием того же образа, вместе с ним будет создан новый том файловой системы, изолированный от томов других контейнеров RUN Выполняет указанную команду с указанными аргументами при запуске контейнера из образа. Часто используется для установки дополнительного программного обеспечения CMD Перечисляет аргументы для ENTRYPOINT. Эта команда напоминает docker run, но выполняется после установки контейнера ADD Копирует дополнительные файлы в контейнер ENTRYPOINT Определяет команду, которая будет выполняться после установки контейнера ENV Определяет переменные окружения 4.4. Docker Compose Docker Compose упрощает использование Docker, позволяя создавать сценарии, которые облегчают проектирование и создание служб. С помощью Docker Compose можно запускать несколько контейнеров как одну службу или создавать разные контейнеры одновременно. Чтобы использовать Docker Compose, выполните следующие действия: 1 установите инструмент Docker Compose, если он еще не установлен. Получить его можно по адресу https://docs.docker. com/compose/install/; 2 создайте файл YAML для настройки служб приложения. Сохраните его с именем docker-compose.yml; 3 проверьте отсутствие ошибок в файле командой docker-compose config; 4 запустите службы командой: docker-compose up. В листинге 4.1 показано, как должен выглядеть файл dockercompose.yml, а подробнее о его содержимом мы поговорим далее в этой главе. Docker Compose 137 Листинг 4.1. Пример файла docker-compose.yml version: <docker-compose-version> services: database: image: <database-docker-image-name> ports: - "<databasePort>:<databasePort>" environment: POSTGRES_USER: <databaseUser> POSTGRES_PASSWORD: <databasePassword> POSTGRES_DB:<databaseName> <service-name>: image: <service-docker-image-name> ports: - "<applicationPort>:<applicationPort>" environment: PROFILE: <profile-name> DATABASESERVER_PORT: "<databasePort>" container_name: <container_name> networks: backend: aliases: - "alias" networks: backend: driver: bridge В табл. 4.2 перечислены инструкции, которые мы будем использовать в файле docker-compose.yml, а в табл. 4.3 – команды dockercompose, которые мы будем использовать на протяжении всей книги. Таблица 4.2. Инструкции Docker Compose Инструкция Описание version Определяет версию инструмента Docker Compose service Определяет развертываемую службу. Имя службы превращается в запись в DNS для экземпляра в Docker и упрощает доступ к ней image Определяет имя образа для запуска контейнера port Определяет номер порта в контейнере Docker, который будет открыт для внешнего мира. Отображает внутренние порты во внешние environment Определяет переменные окружения для запускаемого образа Docker network Определяет сеть. Позволяет создавать сложные топологии. Тип по умолчанию – bridge, поэтому если не указать явно другой тип (host, overlay, macvlan или none), то будет создана сеть типа bridge. Сеть bridge обеспечивает подключение контейнера к сети хоста. Обратите внимание, что сеть типа bridge применяется только к контейнерам, выполняющимся под управлением одного и того же демона Docker alias Определяет альтернативное имя хоста для службы в сети Глава 4 Добро пожаловать в Docker 138 Таблица 4.3. Команды Docker Compose Команда docker-compose up -d Описание Создает образы для приложения и запускает определенные вами службы. Эта команда загружает все необходимые образы, затем развертывает их и запускает контейнер. Параметр -d требует запустить Docker в фоновом режиме docker-compose logs Позволяет просмотреть всю информацию о последнем развертывании docker-compose logs <service_id> Позволяет просмотреть журналы для конкретной службы. Например, просмотреть историю развертывания службы лицензий можно с помощью команды docker-compose logs licenseService docker-compose ps Выведет список всех контейнеров, которые вы развернули в своей системе docker-compose stop Останавливает запущенные вами службы. Также останавливает контейнеры docker-compose down Останавливает все службы и удаляет все контейнеры 4.5. Интеграция Docker с микросервисами Теперь, познакомившись с основными компонентами Docker, интегрируем Docker с нашим микросервисом лицензий и создадим переносимый, масштабируемый и управляемый микросервис. Начнем с добавления плагина Docker Maven в службу лицензий, созданную в предыдущей главе. Если вы не следовали за примерами, то можете скачать исходный код, созданный в главе 3, по адресу: https://github.com/ihuaylupo/manning-smia/tree/ master/chapter3. 4.5.1. Создание образа Docker Для начала создадим образ Docker, добавив плагин Docker Maven в pom.xml нашей службы лицензий. Этот плагин позволяет управлять образами и контейнерами Docker из файла pom.xml. В листинге 4.2 показано, как должен выглядеть pom-файл. Листинг 4.2. Добавление dockerfile-maven-plugin в файл pom.xml <build> Раздел сборки в pom.xml <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> <!-- Этот плагин используется для создания образа Docker и публикации его в Docker Hub --> Запускает плагин Dockerfile Maven <plugin> <groupId>com.spotify</groupId> Интеграция Docker с микросервисами 139 <artifactId>dockerfile-maven-plugin</artifactId> <version>1.4.13</version> Задает имя удаленного репозитория. <configuration> Здесь мы использу<repository>${docker.image.prefix}/ ем предопределен${project.artifactId}</repository> ные переменные docker.image.prefix <tag>${project.version}</tag> Задает тег и project.artifactId <buildArgs> с версией <JAR_FILE>target/${project.build.finalName} проекта .jar</JAR_FILE> Задает местоположение </buildArgs> файла JAR с помощью <buildArgs>. Это значение </configuration> используется в Dockerfile <executions> <execution> <id>default</id> <phase>install</phase> <goals> <goal>build</goal> <goal>push</goal> </goals> </execution> </executions> </plugin> </plugins> </build> Теперь, добавив плагин в файл pom.xml, можно продолжить процесс и создать переменную docker.image.prefix, упомянутую в листинге 4.2 . Эта переменная определяет префикс для имени нашего образа. В листинге 4.3 показано, как добавить переменную в pom.xml. Листинг 4.3. Добавление переменной docker.image.prefix <properties> <java.version>11</java.version> <docker.image.prefix>ostock</docker.image.prefix> </properties> Задает значение переменной docker.image.prefix Определить значение переменной docker.image.prefix можно несколькими способами. Один из них показан в листинге 4.3. Другой способ – передать значение в параметре -d в Maven JVM. Обратите внимание, что если не создать эту переменную в разделе <properties> в файле pom.xml, то при выполнении команды упаковки и создания образа Docker возникает следующая ошибка: Ошибка выполнения цели com.spotify:dockerfile-maven-plugin:1.4.0:build (default-cli) в проекте licensing-service: Команда default-cli для цели com.spotify:dockerfile-maven-plugin:1.4.0:build потерпела ошибку: Переменная 'docker.image.prefix' не имеет значения Теперь, импортировав плагин в pom.xml, продолжим и добавим Dockerfile в наш проект. В следующих разделах мы покажем, как 140 Глава 4 Добро пожаловать в Docker создать базовый файл Dockerfile и файл Dockerfile для многоступенчатой сборки. Обратите внимание, что вы можете использовать любой из этих файлов Dockerfile, потому что оба позволяют запустить микросервис. Основное различие между ними заключается в том, что в базовом файле Dockerfile копируются Spring Boot и файл JAR микросервиса, а в файле, определяющем многоступенчатую сборку, – только самое необходимое для приложения. В этой книге мы решили использовать многоступенчатую сборку, чтобы оптимизировать создаваемый образ Docker, но, вообще, вы можете использовать любой вариант, который лучше соответствует вашим потребностям. Базовый Dockerfile В этом файле Dockerfile в образ Docker копируется файл Spring Boot JAR, а затем запускается файл JAR приложения. В листинге 4.4 показано несколько простых шагов, необходимых для этого. Листинг 4.4. A basic Dockerfile # Базовый образ, содержащий среду Java времени выполнения FROM openjdk:11-slim Определяет базовый образ для использования Docker (в данном случае openjdk:11-slim) # Добавить информацию о владельце LABEL maintainer="Illary Huaylupo <illaryhs@gmail.com>" # Файл jar приложения ARG JAR_FILE Определяет переменную JAR_FILE со значением dockerfile-maven-plugin # Добавить файл jar приложения в контейнер COPY ${JAR_FILE} app.jar Копирует файл JAR в файловую систему образа с именем app.jar # запустить приложение ENTRYPOINT ["java","-jar","/app.jar"] Определяет целевое приложение службы лицензий в образе, которое должно быть запущено после развертывания контейнера Файл Dockerfile для многоступенчатой сборки В листинге 4.5 показано содержимое Dockerfile, описывающего многоступенчатую сборку. Почему многоступенчатую? Выполняя сборку в несколько этапов, мы можем отбросить все, что не требуется для приложения. Например, вместо копирования в образ Docker всего целевого каталога с фреймворком Spring Boot мы можем скопировать только то, что необходимо для запуска приложения. Такая практика помогает оптимизировать создаваемые образы Docker. Интеграция Docker с микросервисами 141 Листинг 4.5. Dockerfile, описывающий многоступенчатую сборку # шаг 1 # Базовый образ, содержащий среду Java времени выполнения FROM openjdk:11-slim as build # Добавить информацию о владельце LABEL maintainer="Illary Huaylupo <illaryhs@gmail.com>" # Файл jar приложения ARG JAR_FILE # Добавить файл jar приложения в контейнер COPY ${JAR_FILE} app.jar # распаковать файл jar RUN mkdir -p target/dependency && (cd target/dependency; jar -xf /app.jar) # шаг 2 # Та же среда Java времени выполнения FROM openjdk:11-slim # Добавить том, ссылающийся на каталог /tmp VOLUME /tmp Распаковывает app.jar, скопированный ранее, в файловую систему образа Этот новый образ содержит несколько слоев приложения Spring Boot полного файла JAR Копирует отдельные слои из первого образа с именем build # Скопировать распакованное приложение в новый контейнер ARG DEPENDENCY=/target/dependency COPY --from=build ${DEPENDENCY}/BOOT-INF/lib /app/lib COPY --from=build ${DEPENDENCY}/META-INF /app/META-INF COPY --from=build ${DEPENDENCY}/BOOT-INF/classes /app # запустить приложение Определяет службу для запуска после создания контейнера ENTRYPOINT ["java","-cp","app:app/lib/*","com.optimagrowth.license. LicenseServiceApplication"] Мы не будем подробно рассматривать весь этот файл Dockerfile, а отметим лишь несколько ключевых аспектов. На шаге 1 команда FROM создает образ с именем build, из образа openJDK для приложений Java. Этот образ отвечает за создание и распаковку файла приложения JAR. ПРИМЕЧАНИЕ. В используемом нами образе уже имеется Java 11 JDK. Затем извлекается значение переменной JAR_FILE, которое мы установили в разделе <configuration> <buildArgs> в файле pom.xml. Потом файл JAR копируется в файловую систему образа с именем app.jar и распаковывается, чтобы получить доступ к разным слоям в Spring Boot. После этого создается еще один образ, который бу- Глава 4 Добро пожаловать в Docker 142 дет содержать только нужные слои. Далее, на шаге 2 необходимые слои копируются в новый образ. ПРИМЕЧАНИЕ. Если не изменить зависимости проекта, то папка BOOT-INF/lib не изменится. Эта папка содержит все внутренние и внешние зависимости, необходимые для запуска приложения. Наконец, команда ENTRYPOINT настраивает в новом образе запуск целевого приложения службы лицензий после создании контейнера. Чтобы узнать больше о многоступенчатой сборке, загляните внутрь полного JAR-файла Spring Boot, выполнив следующую коман­ду в целевой папке микросервиса: jar tf jar-file Для службы лицензий эта команда должна выглядеть так: jar tf licensing-service-0.0.1-SNAPSHOT.jar Если у вас в целевой папке нет файла JAR, то добавьте следующую команду Maven в файл pom.xml вашего проекта: mvn clean package Теперь, настроив окружение Maven, создадим образ Docker. Для этого нужно выполнить следующую команду: mvn package dockerfile:build ПРИМЕЧАНИЕ. Убедитесь, что на вашем компьютере установлена версия Docker Engine не ниже 18.06.0, чтобы гарантировать успешное выполнение всех примеров кода Docker. Чтобы узнать версию Docker, установленную у вас, выполните команду docker version. После запуска команды создания образа Docker вы должны увидеть вывод, как показано на рис. 4.5. Интеграция Docker с микросервисами 143 Шаги в Dockerfile Идентификатор образа Имя образа Рис. 4.5. Создание образа Docker с помощью плагина Maven командой mvn package dockerfile:build Теперь, после создания образа Docker, его можно увидеть в списке образов Docker в нашей системе. Чтобы вывести список всех образов Docker, выполните команду docker images. Если все было сделано правильно, то вы увидите примерно такой список: REPOSITORY TAG IMAGE ID CREATED SIZE ostock/licensing-service 0.0.1-SNAPSHOT 231fc4a87903 1 minute ago 149MB Глава 4 Добро пожаловать в Docker 144 Теперь можно развернуть и запустить полученный образ командой: docker run ostock/licensing-service:0.0.1-SNAPSHOT Также команде docker run можно передать параметр -d, чтобы запустить контейнер в фоновом режиме, например: docker run -d ostock/licensing-service:0.0.1-SNAPSHOT Эта команда запустит контейнер Docker. Чтобы увидеть все запущенные контейнеры в вашей системе, выполните команду docker ps. Она выведет список всех запущенных контейнеров с их идентификаторами, именами образов, командами, датами создания, состояниями, открытыми портами и именами. Если вам понадобится остановить контейнер, то выполните следующую команду, указав идентификатор контейнера: docker stop <container_id> 4.5.2. Создание образов Docker со Spring Boot В этом разделе мы кратко расскажем, как создавать образы Docker с использованием новых возможностей, появившихся в Spring Boot v2.3. Обратите внимание, что для этого нужно: установить Docker и Docker Compose; иметь Spring Boot версии 2.3 или выше. Эти новые возможности предлагают улучшенную поддержку Buildpack и многослойных JAR-файлов. Чтобы создать образ Docker с использованием новых возможностей, добавьте следующие строки в pom.xml. Убедитесь при этом, что в нем указана версия spring-boot-starter-parent 2.3 или выше. <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.4.0</version> <relativePath/> <!-- поиск родителя в репозитории --> </parent> Buildpacks Buildpacks – это набор инструментов, добавляющих зависимости, необходимые фреймворку и приложению, для преобразования исходного кода в готовый к запуску образ приложения. Другими словами, инструменты Buildpacks обнаруживают и получают все, что необходимо приложению для запуска. Spring Boot 2.3.0 предлагает поддержку создания образов Docker с использованием Cloud Native Buildpacks. Эта поддержка была добавлена в плагины Maven и Gradle в виде цели spring-boot:build-image для Maven и задачи bootBuildImage для Gradle. За дополнительной информацией обращайтесь по следующим ссылкам: Интеграция Docker с микросервисами 145 плагин Spring Boot Maven: https://docs.spring.io/spring-boot/ docs/2.3.0.M1/maven-plugin/html/; плагин Spring Boot Gradle: https://docs.spring.io/spring-boot/ docs/2.3.0.M1/gradle-plugin/reference/html/. В этой книге мы объясним только, как использовать плагин для Maven. Чтобы создать образ с использованием этой новой возможности, выполните следующую команду в корневом каталоге проекта микросервиса: ./mvnw spring-boot:build-image После запуска команда должна вывести строки, как показано ниже: [INFO] [creator] Setting default process type 'web' [INFO] [creator] *** Images (045116f040d2): [INFO] [creator] docker.io/library/licensing-service:0.0.1-SNAPSHOT [INFO] [INFO] Successfully built image 'docker.io/library/ ➥ licensing-service:0.0.1-SNAPSHOT' Чтобы изменить имя созданного образа, добавьте следующий плагин в файл pom.xml, а затем определите имя в разделе <configuration>: <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <image> <name>${docker.image.prefix}/${project.artifactId}:latest</name> </image> </configuration> </plugin> После успешного создания образа его можно запустить следующей командой Docker: docker run -it -p8080:8080 docker.io/library/ ➥ licensing-service:0.0.1-SNAPSHOT Многослойные файлы JAR Spring Boot поддерживает новый вид файлов JAR, которые называют многослойными файлами JAR. В файлах с этим новым форматом папки /lib и /classes разбиты на слои. Эти слои были созданы для разделения кода, в зависимости от вероятности его изменения между сборками, с сохранением информации, необходимой для сборки. Это отличный выбор для тех, кто хочет использовать Buildpacks. Чтобы извлечь слои для нашего микросервиса, выполним следующие шаги. 1 Добавим поддержку слоев в файл pom.xml. 2 Упакуем приложение. 3 Выполним сборку со значением layertools в параметре jarmode. Глава 4 Добро пожаловать в Docker 146 Создадим Dockerfile. Соберем и запустим образ. На первом шаге мы должны добавить поддержку слоев для плагина Spring Boot Maven в pom.xml, например: 4 5 <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <layers> <enabled>true</enabled> </layers> </configuration> </plugin>yp После этого выполним следующую команду, чтобы собрать наш файл Spring Boot JAR: mvn clean package Затем выполним следующую команду в корневом каталоге проекта приложения, чтобы вывести список слоев и узнать порядок, в котором они должны добавляться в наш Dockerfile: java -Djarmode=layertools -jar target/ ➥ licensing-service-0.0.1-SNAPSHOT.jar list Эта команда должна вывести примерно такие строки: dependencies spring-boot-loader snapshot-dependencies application Теперь у нас есть вся необходимая информация о слоях, и мы можем перейти к шагу 4 – созданию Dockerfile. Содержимое Dockerfile показано в листинге 4.6. Листинг 4.6. Содержимое файла Dockerfile использующего многослойные файлы JAR FROM openjdk:11-slim as build WORKDIR application ARG JAR_FILE=target/*.jar COPY ${JAR_FILE} application.jar RUN java -Djarmode=layertools -jar application.jar extract FROM openjdk:11-slim WORKDIR application COPY --from=build application/dependencies/ ./ COPY --from=build application/spring-boot-loader/ ./ COPY --from=build application/snapshot-dependencies/ ./ COPY --from=build application/application/ ./ Скопировать каждый слой, полученный командой jarmode Интеграция Docker с микросервисами 147 ENTRYPOINT ["java", "org.springframework.boot.loader .JarLauncher"] Использовать org.springframework.boot. loader.JarLauncher для запуска приложения Наконец, мы можем выполнить команды build и run в корневом каталоге проекта микросервиса: docker build . --tag licensing-service docker run -it -p8080:8080 licensing-service:latest 4.5.3. Запуск служб с помощью Docker Compose Docker Compose устанавливается как часть Docker. Это инструмент управления службами, который позволяет объединять службы в группы, а затем запускать их как единое целое. Docker Compose также поддерживает возможность определения переменных окружения для каждой службы. Для определения запускаемых служб в Docker Compose используется файл YAML. Например, в репозитории с исходным кодом в каждой главе имеется файл с именем <<chapter>>/dockercompose.yml. Этот файл содержит определения служб, описанных в данной главе. Давайте создадим наш первый файл docker-compose.yml. В листинге 4.7 показано, как должно выглядеть его содержимое. Листинг 4.7. Файл docker-compose.yml version: '3.7' Добавляет метку к каждой запущенной службе. Превращается в DNS-запись для экземпляра Docker при его запуске, и именно так к ней обраservices: щаются другие службы licensingservice: Docker Compose сначала пытается найти image: ostock/licensing-service:0.0.1-SNAPSHOT целевой образ для запуска в локальном Определяет номера портов ports: репозитории Docker и в контейнере Docker, кототолько потом прове- "8080:8080" рые должны быть доступны ряет центральный ревнешнему миру позиторий Docker Hub environment: (http://hub.docker.com) - "SPRING_PROFILES_ACTIVE=dev" Называет сеть, в которой networks: принадлежит сервис backend: aliases: - "licenseservice" Определяет переменные окружения в образе Docker. В данном случае определяет переменную SPRING_PROFILES_ACTIVE Создает настраиваемую сеть networks: с именем backend и с типом bridge по умолчанию backend: driver: bridge Задает альтернативное имя хоста для службы в сети Теперь у нас есть файл docker-compose.yml, и мы можем запустить наши службы, выполнив команду docker-compose up в каталоге, Глава 4 Добро пожаловать в Docker 148 где находится файл docker-compose.yml. После ее запуска вы должны увидеть результат, аналогичный показанному на рис. 4.6. ПРИМЕЧАНИЕ. Если вы еще не знакомы с переменной SPRING_ PROFILES_ACTIVE, то не беспокойтесь об этом. Мы рассмотрим ее в следующей главе, где будем учиться управлять разными профилями в нашем микросервисе. Переменная окружения SPRING_PROFILES_ACTIVE, указанная в файле docker-compose.yml Рис. 4.6. Вывод в консоли Docker Compose, показывающий, что служба лицензий запущена и работает с переменной SPRING_PROFILES_ACTIVE, указанной в docker-compose.yml После запуска контейнера можно выполнить команду docker ps, чтобы увидеть все запущенные контейнеры. ПРИМЕЧАНИЕ. Все контейнеры Docker, используемые в этой книге, эфемерны (т. е. недолговечны) – они не сохраняют своего состояния между остановкой и следующим запуском. Имейте это в виду, если, экспериментируя с кодом, вы заметите, что ваши данные исчезают после перезапуска контейнеров. Если вам интересно узнать, как сохранить состояние контейнера, загляните в описание команды docker commit. Теперь, научившись создавать контейнеры и интегрировать Docker с нашими микросервисами, перейдем к следующей главе. В ней мы создадим наш сервер конфигурации Spring Cloud. Итоги Контейнеры позволяют нам запускать наше программное обеспечение в любом окружении – на машинах разработчиков или на физических или виртуальных корпоративных серверах. Виртуальная машина (ВМ) – это программная среда, имитирующая работу одного компьютера на другом компьютере. Виртуальные машины основаны на гипервизоре, имитирующем всю физическую машину и выделяющем желаемый объем системной памяти, ядер процессора, дискового хранилища и других ресурсов. Интеграция Docker с микросервисами 149 Контейнеры – это еще один метод виртуализации операционной системы (ОС), позволяющий запускать приложение с зависимостями элементами в изолированном окружении. Использование контейнеров уменьшает общие затраты за счет создания облегченных виртуальных машин с коротким временем запуска и тем самым снижает затраты на каждый проект. Docker – популярный движок контейнеров с открытым исходным кодом, основанный на контейнерах Linux. Он был создан в 2013 году Соломоном Хайксом (Solomon Hykes), основателем и генеральным директором dotCloud. Docker состоит из следующих компонентов: Docker Engines, клиентов, реестров, образов, контейнеров, томов и сетей. Dockerfile – это простой текстовый файл, содержащий список инструкций и команд, которые выполняет клиент Docker, чтобы создать образ. То есть этот файл автоматизирует процесс создания образов. Команды, используемые в Dockerfile, аналогичны командам Linux, что упрощает их запоминание. Docker Compose – это инструмент управления службами, который позволяет объединить службы в группу, а затем запускать их вместе как единое целое. Docker Compose устанавливается вместе с Docker. Плагин Dockerfile Maven интегрирует Maven с Docker. 5 Управление конфигурациями с использованием Spring Cloud Configuration Server Эта глава: рассказывает, как отделить конфигурацию службы от ее кода; демонстрирует возможные настройки Spring Cloud Configuration Server; показывает приемы интеграции микросервисов Spring Boot с сервером конфигурации; описывает возможность шифрования конфиденциальных свойств; показывает приемы интеграции Spring Cloud Configuration Server с HashiCorp Vault. Разработчики программного обеспечения постоянно слышат о важности отделения конфигурации приложения от кода. В большинстве случаев это означает отказ от использования жестко запрограммированных настроек в коде. Несоблюдение этого принципа может усложнить дальнейшее развитие приложения, поскольку при каждом изменении конфигурации приходится повторно компилировать и развертывать приложение. Полное отделение конфигурации от кода приложения позволяет разработчикам и сопровождающим специалистам вносить изменения в конфигурацию без необходимости повторной компиляции. Но это также влечет дополнительные сложности, потому что Об управлении конфигурациями (и сложностью) 151 у разработчиков появляется еще один артефакт, которым нужно управлять и развертывать вместе с приложением. Многие разработчики используют файлы свойств (YAML, JSON или XML) для хранения информации о конфигурации. Хранение настроек приложения в этих файлах – настолько простая задача, что большинство разработчиков не делает ничего, кроме как помещают конфигурационные файлы в систему управления версиями и развертывают эти файлы вместе с приложением. Этот подход более или менее применим для небольшого количества приложений, но быстро становится непригодным при работе с облачными приложениями, которые могут состоять из сотен микросервисов, каждый из которых может иметь несколько запущенных экземпляров. Внезапно простой и понятный процесс становится необычайно сложным, и команде разработчиков приходится бороться со всеми этими конфигурационными файлами. Например, представьте, что у нас есть несколько сотен микросервисов и каждый содержит разные конфигурации для трех окружений. Если не управлять этими файлами отдельно, то при каждом изменении нам придется отыскать нужный файл в репозитории, проследить за процессом интеграции (если он есть) и перезапустить приложение. Чтобы избежать этого катастрофического сценария, при разработке облачных микросервисов следует использовать следующие рекомендации: полностью отделяйте конфигурацию приложения от развертываемого кода; создавайте неизменяемые образы приложений, которые никогда не меняются по мере продвижения между окружениями; передавайте любую конфигурацию через переменные окружения или централизованный репозиторий, к которому микросервисы могут обратиться при запуске. В этой главе мы перечислим основные принципы и шаблоны, знание и использование которых необходимы для управления конфигурациями облачных приложений. Затем мы покажем, как создать сервер конфигурации, интегрировать его с фреймворком Spring и клиентами Spring Boot, а затем расскажем, как защитить конфиденциальные сведения в конфигурациях. 5.1.Об управлении конфигурациями (и сложностью) Управление конфигурацией приложений имеет решающее значение для микросервисов, работающих в облаке, потому что экземпляры микросервисов должны запускаться быстро и с минимальным вмешательством со стороны человека. Когда человеку приходится 152 Глава 5 Управление конфигурациями с использованием Spring Cloud... вручную настраивать службу или развертывать ее, то возникает вероятность дрейфа конфигурации, неожиданного сбоя и задержки в реагировании на проблемы масштабируемости в приложении. Давайте начнем обсуждение управления конфигурацией приложений с определения четырех принципов, которым мы должны следовать. Отделение. Мы должны полностью отделить конфигурацию службы от процесса ее развертывания. Конфигурация не должна развертываться вместе с экземпляром службы. Вместо этого информация о конфигурации должна передаваться в переменных окружения или извлекаться из централизованного репозитория в момент запуска службы. Абстрагирование. Также необходимо абстрагировать доступ к конфигурации с использованием служебного интерфейса. Служба не должна содержать код, который напрямую читает конфигурацию из файлов или базы данных, вместо этого следует использовать REST-службу, возвращающую конфигурацию приложения в формате JSON. Централизация. Облачное приложение может содержать буквально сотни служб, поэтому очень важно свести к минимуму количество различных репозиториев, используемых для хранения конфигураций. Храните конфигурации своих приложений в как можно меньшем количестве репозиториев. Надежность. Информация о конфигурации приложения будет полностью отделена от развернутой службы и централизована, поэтому очень важно, чтобы служба, управляющая конфигурациями, была высокодоступной и избыточной. Важно помнить, что при отделении конфигурации от фактического кода создается внешняя зависимость, которой нужно будет управлять и хранить в системе управления версиями. Невозможно переоценить то обстоятельство, что конфигурация приложения должна управляться надлежащим образом, потому что неправильно организованное управление конфигурацией является благодатной почвой для трудно обнаруживаемых ошибок и незапланированных отключений. 5.1.1. Архитектура управления конфигурацией Как рассказывалось в предыдущих главах, загрузка конфигурации производится микросервисом на этапе инициализации. Давайте вспомним, как выглядит жизненный цикл микросервиса (см. рис. 5.1). Давайте посмотрим, как четыре принципа, перечисленные выше в разделе 5.1 (отделение, абстрагирование, централизация и надежность), применяются на этапе инициализации службы. На рис. 5.2 более подробно изображен процесс инициализации и показано, насколько важную роль играет служба управления конфигурациями на этом этапе. Об управлении конфигурациями (и сложностью) 1. Сборка 2. Инициализация 3. Обнаружение Репозиторий с конфигурацией Механизм Выполняемый файл JAR сборки/развертывания 153 Агент обнаружения служб 4. Мониторинг Агент обнаружения служб Сбой x Репозиторий с исходным кодом Запуск экземпляра службы Множество экземпляров службы Клиент службы Множество экземпляров службы Рис. 5.1. Когда микросервис запускается, он выполняет несколько шагов в начале своего жизненного цикла. Конфигурация приложения извлекается им на этапе инициализации 4. Уведомления об изменении конфигурации отправляются приложениям, которые ее используют Экземпляр Служба управления конфигурацией 2. Фактическая конфигурация хранится в репозитории Экземпляр Репозиторий службы управления конфигурацией Конвейер сборки/развертывания Экземпляр 3. Изменения, выполненные разработчиками, передаются через конвейер сборки и развертывания в репозиторий конфигурации 1. Экземпляры микросервисов запускаются и извлекают свои конфигурации Инженеры DevOps Рис. 5.2. Концептуальная архитектура управления конфигурацией 154 Глава 5 Управление конфигурациями с использованием Spring Cloud... На рис. 5.2 показано несколько выполняемых действий. Вот их краткое описание. 1 Когда запускается экземпляр микросервиса, он обращается к конечной точке службы для получения своей конфигурации, которая зависит от окружения, в котором запускается микросервис. Информация, необходимая для подключения к службе управления конфигурацией (учетные данные, адрес конечной точки службы и т. д.), передается в микросервис в момент запуска. 2 Фактическая конфигурация хранится в репозитории. В зависимости от реализации репозитория конфигурации можно выбрать разные способы хранения. Это могут быть файлы в системе управления версиями, реляционные базы данных или хранилища данных типа ключ/значение. 3 Фактическое управление конфигурацией приложения осуществляется независимо от развертывания приложения. Изменения в конфигурации обычно передаются через конвейер сборки и развертывания, где эти изменения могут быть отмечены информацией о версии и окружениях, в которых они применяются (разработка, подготовка, производство и т. д.). 4 После изменения конфигурации соответствующие приложения, должны быть уведомлены об этом, чтобы иметь возможность обновить свои настройки. Итак, мы нарисовали концептуальную архитектуру, которая иллюстрирует различные части шаблона управления конфигурацией и как эти части сочетаются друг с другом. Теперь рассмотрим некоторые решения управления конфигурацией, а затем перейдем к конкретной реализации. 5.1.2. Варианты реализации К счастью для нас, мы имеем на выбор довольно широкий круг проверенных решений управления конфигурацией, реализованных в разнообразных проектах с открытым исходным кодом. Давайте рассмотрим и сравним некоторые из них. Они перечислены в табл. 5.1. Таблица 5.1. Решения управления конфигурацией, реализованные в проектах с открытым исходным кодом Решение etcd Описание Реализовано на Go. Используется для обнаружения служб и в качестве хранилища пар ключ/ значение. Использует протокол raft (https://raft.github.io/) для своей модели распределенных вычислений Характеристики Высокая скорость Масштабируемость Управление из командной строки Простота в использовании и настройке Об управлении конфигурациями (и сложностью) 155 Окончание табл. 5.1 Решение Описание Характеристики Eureka Написано в компании Netflix. Чрезвычайно надежное. Используется для обнаружения служб и в качестве хранилища пар ключ/значение Распределенное хранилище пар ключ/значение Высокая гибкость, но требует некоторых усилий для настройки Поддерживает динамическое обновление клиентов из коробки Consul Написано в компании HashiCorp. Похлже на etcd и Eureka, но использует иной алгоритм в модели распределенных вычислений Высокая скорость Предлагает свою поддержку обнаружения служб с возможностью интеграции с DNS Не поддерживает динамическое обновление клиентов из коробки Zookeeper Проект Apache. Поддерживает распределенные блокировки. Часто используется в качестве решения для управления конфигурацией и в качестве хранилища пар ключ/значение Давнее и хорошо проверенное решение Высокая сложность использования Может применяться для управления конфигурациями, но только если вы уже используете Zookeeper в своей архитектуре Spring Cloud Configuration Server Решение с открытым исходным кодом. Универсальное решение для управления конфигурациями с несколькими базовыми механизмами хранения Нераспределенное хранилище пар ключ/значение Предлагает тесную интеграцию со службами, использующими и не использующими Spring Позволяет задействовать несколько базовых механизмов для хранения конфигураций, включая общие файловые системы, Eureka, Consul и Git Все решения, перечисленные в табл. 5.1, с успехом можно использовать для организации своей архитектуры управления конфигурацией. Однако на протяжении всей книги мы будем использовать решение Spring Cloud Configuration Server (часто называемое Spring Cloud Config Server или просто Config Server), которое идеально подходит для микросервисной архитектуры Spring. Вот некоторые причины, объясняющие наш выбор: сервер Spring Cloud Configuration Server прост в настройке и использовании; Spring Cloud Config тесно интегрируется с Spring Boot – вы можете прочитать конфигурацию вашего приложения с помощью нескольких простых аннотаций; Config Server поддерживает несколько механизмов для хранения конфигураций; из всех решений, перечисленных в табл. 5.1, только Config Server можно напрямую интегрировать с платформами управления версиями Git и HashiCorp Vault. Мы подробно коснемся этого вопроса далее в этой главе. В оставшейся части этой главы мы сделаем следующее. 1 Настроим Spring Cloud Configuration Server и продемонстрируем использование трех разных механизмов для хранения конфигурационных данных приложения: файловой системы, репозитория Git и HashiCorp Vault. 156 Глава 5 Управление конфигурациями с использованием Spring Cloud... 2 3 5.2. Продолжим создание службы лицензий и реализуем извлечение данных из базы данных. Подключим службу Spring Cloud Config к своей службе лицензий для обслуживания конфигурации приложения. Настройка Spring Cloud Configuration Server Spring Cloud Configuration Server – это REST-приложение, построенное на основе Spring Boot. Config Server не является автономным сервером и должен встраиваться в существующее приложение Spring Boot или оформляться как самостоятельный проект Spring Boot со встроенным сервером. Лучше всего, конечно, создать отдельный проект. Первое, что нужно сделать, чтобы получить сервер конфигурации, – создать проект Spring Boot с помощью Spring Initializr (https://start.spring.io/). Для этого мы заполним поля формы Initializr, как описывается ниже. После заполнения форма Spring Initializr должна выглядеть, как показано на рис. 5.3 и 5.4. 1 Выберите тип проекта Maven Project (Проект Maven). 2 Выберите язык Java. 3 Выберите последнюю или более стабильную версию Spring. 4 Введите com.optimagrowth в поле Group (Группа) и configserver в поле Artifact (Артефакт). 5 Раскройте список Options (Параметры) и введите: a Configuration Server в поле Name (Имя); 2 Configuration server в поле Description (Описание); c com.optimagrowth.configserver в поле Package name (Имя пакета). 4 Выберите в поле Packaging (Упаковка) вариант JAR. 5 Выберите версию Java 11. 6 Добавьте зависимости Config Server и Spring Boot Actuator. Настройка Spring Cloud Configuration Server 157 Инструмент сборки кода на Java Версия Spring Boot Группа, артефакт и имя проекта Главный пакет проекта (местоположение класса инициализации Spring) Java version Рис. 5.3. Настройки проекта Spring Configuration Server в форме Spring Initializr Рис. 5.4. Зависимости Config Server и Spring Boot Actuator в форме Spring Initializr После импортирования созданной формы в виде проекта Maven в IDE будет создан файл pom.xml в корневой папке каталога проекта сервера конфигурации, который выглядит, как показано в листинге 5.1. 158 Глава 5 Управление конфигурациями с использованием Spring Cloud... Листинг 5.1. Файл pom.xml для проекта Spring Configuration Server <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> Версия <artifactId>spring-boot-starter-parent</artifactId> Spring Boot <version>2.2.4.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.optimagrowth</groupId> <artifactId>configserver</artifactId> <version>0.0.1-SNAPSHOT</version> <name>Configuration Server</name> <description>Configuration Server</description> Используе<properties> мая версия Spring <java.version>11</java.version> Cloud <spring-cloud.version> Hoxton.SR1 </spring-cloud.version> Проекты Spring Cloud и дру</properties> гие зависимости, необходимые для запуска ConfigServer <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-config-server</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId> junit-vintage-engine </artifactId> </exclusion> </exclusions> Определение Spring Cloud BOM </dependency> (Bill of Materials – ведомость </dependencies> материалов) <dependencyManagement> <dependencies> <dependency> Настройка Spring Cloud Configuration Server 159 <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project> ПРИМЕЧАНИЕ. Все файлы pom.xml должны содержать зависимости и настройки Docker, но для экономии места не стали включать эти строки в листинг. Если вам интересно увидеть настройки Docker для сервера конфигурации, то загляните в папку с примерами для главы 5 в репозитории GitHub: https://github. com/ihuaylupo/manning-smia/tree/master/chapter5. Мы не будем подробно описывать весь файл pom и отметим лишь несколько ключевых моментов. Файл pom.xml в листинге 5.1 содержит четыре важных определения. Первое – версия Spring Boot, а второе – версия Spring Cloud, которую мы собираемся использовать. В этом примере мы используем версию Hoxton.SR1 Spring Cloud. Третье важное определение – конкретные зависимости, которые мы будем использовать, и последнее – это родительская ведомость материалов Spring Cloud Config BOM (Bill of Materials). Эта родительская ведомость включает все сторонние библиотеки и зависимости, которые используются в облачном проекте, а также номера версий отдельных проектов. В этом примере мы используем версию, которая определена выше в разделе <properties>. Используя определение BOM, мы можем гарантировать использование совместимых версий подпроектов Spring Cloud. Это также означает, что нам не нужно объявлять номера версий для вложенных зависимостей. Прокатимся на поезде, на поезде выпусков В Spring Cloud используется нетрадиционный механизм маркировки проектов Maven. Поскольку Spring Cloud – это набор независимых подпроектов, команда Spring Cloud использует процесс, который они называют «поездом выпусков» (release train). Все подпроекты, составляющие Spring Cloud, упаковываются в одну ведомость Maven BOM и выпускаются как единое целое. 160 Глава 5 Управление конфигурациями с использованием Spring Cloud... В качестве названий своих выпусков команда Spring Cloud использует названия остановок лондонского метро, при этом каждому крупному выпуску дается название остановки, начинающееся со следующей заглавной буквы. Всего было сделано несколько выпусков: от Angel (Ангел), Brixton (Брикстон), Camden (Камден), Dalston (Далстон), Edgware (Эджвер), Finchley (Финчли) и Greenwich (Гринвич) до Hoxton (Хокстон). Hoxton – новейший выпуск, но в нем до сих пор имеется несколько веток-кандидатов на выпуск для подпроектов. Следует отметить, что периодичность выпусков новых версий Spring Boot не зависит от выпусков Spring Cloud. Поэтому некоторые версии Spring Boot могут быть несовместимыми с разными выпусками Spring Cloud. Узнать, какие версии Spring Boot и Spring Cloud совместимы, а также увидеть различные версии подпроектов, содержащихся в поезде выпусков, можно на веб-сайте Spring Cloud (http://projects.spring. io/spring-cloud/). Следующий шаг в создании Spring Cloud Config Server – создание еще одного файла с настройками сервера. Это может быть один из следующих файлов: application.properties, application.yml, bootstrap.properties или bootstrap.yml. Файлы bootstrap – особые в Spring Cloud, они загружаются перед application.properties или application.yml. Файл bootstrap используется для определения имени приложения, местоположения Spring Cloud Configuration Git, настроек шифрования/дешифрования и т. д. В частности, файл bootstrap загружается родительским классом ApplicationContext, и это происходит до загрузки файла application.properties или application.yml. Расширения файлов .yml и .properties – это просто разные форматы данных. Вы можете выбрать тот, который вам больше нравится. В этой книге мы будем использовать bootstrap.yml. Теперь продолжим и создадим наш файл bootstrap.yml в папке /src/main/resources. Этот файл сообщает службе Spring Cloud Config, какой порт прослушивать, имя приложения, профили приложений и место, где будет храниться конфигурация. В листинге 5.2 показано содержимое нашего файла bootstrap. Листинг 5.2. Файл bootstrap.yml Имя приложения Config spring: Server (в данном случае, application: config-server) name: config-server server: Порт сервера port: 8071 Настройка Spring Cloud Configuration Server 161 Отметим два важных момента в листинге 5.2. Первый – это имя приложения. Все службы должны иметь уникальные имена, чтобы потом их можно было обнаруживать (об обнаружении служб мы поговорим в следующей главе). Второй момент, на который следует обратить внимание, – это порт, который Spring Configuration Server будет прослушивать, ожидая запросов на получение конфигурации. 5.2.1. Настройка класса инициализации Spring Cloud Config Следующий шаг в создании службы Spring Cloud Config – настройка класса инициализации. Каждая служба Spring Cloud должна иметь класс инициализации, который используется для запуска службы, как было описано в главах 2 и 3 (где мы создали службу лицензий). Этот класс содержит несколько важных частей: метод main(), действующий как точка входа для запуска службы, и набор аннотаций Spring, сообщающих запускаемой службе, какое поведение будет проявлять Spring. В листинге 5.3 показано определение класса инициализации для нашего Spring Cloud Config Server. Листинг 5.3. Определение класса инициализации package com.optimagrowth.configserver; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.config.server.EnableConfigServer; @SpringBootApplication @EnableConfigServer Наша служба управления конфигурациями – это приложение Spring Boot, поэтому мы должны добавить аннотацию @SpringBootApplication Объявляет данную службу службой Spring Cloud Config public class ConfigurationServerApplication { Метод main() загружает службу и запускает контейнер Spring public static void main(String[] args) { SpringApplication.run(ConfigurationServerApplication.class, args); } } Следующий шаг – определение места поиска конфигурации. Начнем с простейшего случая: файловой системы. 5.2.2.Использование Spring Cloud Config Server с файловой системой Место для поиска конфигурации Spring Cloud Configuration Server определяется в файле bootstrap.yml. Самый простой вариант – создать репозиторий на основе файловой системы. Для этого обновим наш файл bootstrap, как показано в листинге 5.4. 162 Глава 5 Управление конфигурациями с использованием Spring Cloud... Листинг 5.4. Определение репозитория на основе файловой системы в bootstrap.yml spring: application: name: config-server profiles: active: native Профиль Spring, связанный с репозиторием (файловая система) cloud: config: server: # Локальная конфигурация: это может быть # classpath или конкретный каталог в файловой системе. native: # Определение конкретной папки в файловой системе search-locations: file:///{FILE_PATH} Место поиска, где server: port: 8071 хранятся файлы конфигурации Мы собираемся использовать файловую систему для хранения конфигурации приложения, поэтому указываем профиль native. Профиль Spring – это базовая особенность, которую предлагает фреймворк Spring. Она позволяет отображать наши beanкомпоненты в различные окружения, например для разработки, тестирования, промышленной эксплуатации и др. ПРИМЕЧАНИЕ. Имейте в виду, что native – это просто профиль, созданный для Spring Cloud Configuration Server, который сообщает, что файлы конфигурации будут извлекаться из пути поиска классов (classpath) или из файловой системы. Используя репозиторий на основе файловой системы, вы также должны использовать профиль native, так как этот профиль не использует Git или Vault, а загружает конфигурацию непосредственно из локального пути поиска классов (classpath) или файловой системы. Наконец, последняя важная деталь в bootstrap.yml – определение каталога, в котором находятся данные приложения. Например: server: native: search-locations: file:///Users/illary.huaylupo Одним из важных параметров в настройках является search-locations. Этот параметр содержит список каталогов, разделенных запятыми, для каждого приложения, настройками которых будет управлять Config Server. В предыдущем примере мы указали путь в файловой системе (file:///Users/illary.huaylupo), но также можно указать конкретный путь к классам. Вот как это делается: Настройка Spring Cloud Configuration Server 163 server: native: search-locations: classpath:/config ПРИМЕЧАНИЕ. Атрибут classpath заставляет Spring Cloud Config Server искать файлы в папке src/main/resources/config. Теперь, определив настройки для Spring Configuration Server, создадим файлы свойств для службы лицензий. Для простоты мы будем выполнять поиск в classpath, как определено в предыдущем фрагменте. Соответственно, как отмечено выше, мы создадим файлы свойств для службы лицензий в папке /config. 5.2.3. Создание конфигурационных файлов для службы В этом разделе в качестве примера использования Spring Cloud Config мы возьмем службу лицензий, которую начали разрабатывать в первых главах этой книги. ПРИМЕЧАНИЕ. Если вы не следовали за примерами в предыдущей главе, то можете скачать исходный код, созданный в главе 4, по адресу https://github.com/ihuaylupo/manning-smia/tree/ master/chapter4. И снова ради простоты мы определим конфигурации для трех окружений: по умолчанию для запуска службы локально, для разработки и для промышленной эксплуатации. В Spring Cloud Config все основано на иерархиях. Поэтому конфигурация вашего приложения будет определяться именем приложения и названием окружения, в котором эта конфигурация будет использоваться. В каждом из этих окружений мы настроим следующие конфигурации: конфигурацию самой службы лицензий; конфигурацию модуля Spring Actuator, который мы будем использовать в службе лицензий; конфигурацию базы данных для службы лицензий. На рис. 5.5 показано, как будет настраиваться и использоваться служба Spring Cloud Config. Следует отметить один важный момент: служба Config – это еще один микросервис, работающий в вашем окружении. После ее настройки содержимое службы будет доступно через конечную точку REST. В соответствии с соглашениями файлы конфигурации приложения должны иметь имена: appname-env.properties или appname-env.yml. Также, как показано на рис. 5.5, имена окружений транслируются непосредственно в адреса URL конфигурации. Позже, когда мы запустим пример микросервиса лицензий, окружение, в котором 164 Глава 5 Управление конфигурациями с использованием Spring Cloud... запускается служба, будет определяться профилем Spring Boot, передаваемым в командной строке. Если профиль не указан в командной строке, то Spring Boot по умолчанию будет использовать конфигурацию в файле application.properties, упакованном вместе с приложением. Spring Cloud Configuration Server (запускается и работает как микросервис) /licensing-service/default /licensing-service/dev /licensing-service/prod licensing-service.properties licensing-service-dev.properties licensing-service-prod.properties Рис. 5.5. Spring Cloud Config экспортирует конфигурации для разных окружений через конечные точки HTTP Вот пример некоторых настроек в конфигурации службы лицензий. Это данные будут храниться в файле configserver/src/ main/resources/config/licensing-service.properties, упомянутом на рис. 5.5. Вот часть содержимого этого файла: ... example.property= I AM THE DEFAULT spring.jpa.hibernate.ddl-auto=none spring.jpa.database=POSTGRESQL spring.datasource.platform=postgres spring.jpa.show-sql = true spring.jpa.hibernate.naming-strategy = org.hibernate.cfg.ImprovedNamingStrategy spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.PostgreSQLDialect Настройка Spring Cloud Configuration Server 165 spring.database.driverClassName= org.postgresql.Driver spring.datasource.testWhileIdle = true spring.datasource.validationQuery = SELECT 1 management.endpoints.web.exposure.include=* management.endpoints.enabled-by-default=true Подумайте, прежде чем реализовать Мы не рекомендуем использовать решение на основе файловой системы для средних и крупных облачных приложений, потому что для этого придется реализовать общую точку монтирования для экземпляров Configuration Server, которым необходимо иметь доступ к конфигурации приложения. Настроить общую файловую систему в облаке, конечно, можно, но это накладывает на вас дополнительное бремя ответственности за ее поддержку. Мы демонстрируем подход с файловой системой только как самый простой пример, который можно использовать при работе с Spring Cloud Configuration Server. В следующем разделе мы покажем, как настроить в Config Server использование GitHub и HashiCorp Vault для хранения конфигурации приложения. Далее создадим файл licensing-service-dev.properties, содержащий только настройки для окружения разработки. Файл свойств для окружения разработки должен содержать следующие параметры: example.property= I AM DEV spring.datasource.url = jdbc:postgresql://localhost:5432/ostock_dev spring.datasource.username = postgres spring.datasource.password = postgres Теперь у нас есть все необходимое для запуска Configuration Server, так давайте сделаем это, выполнив команду mvn springboot:run или docker-compose up. ПРИМЕЧАНИЕ. С этого момента в папки с примерами исходного кода для каждой главы мы будем помещать файл README. Этот файл содержит раздел «How to Use» («Как использовать»), описывающий порядок запуска всех служб вместе с помощью команды docker-compose. После запуска команды в окне терминала должна появиться заставка Spring Boot. Если теперь открыть в браузере адрес http:// localhost:8071/licensing-service/default, то вы увидите, что служба возвращает данные в формате JSON, содержащие все свойства, что определены в файле licensing-service.properties. На рис. 5.6 показан результат обращения к этой конечной точке. 166 Глава 5 Управление конфигурациями с использованием Spring Cloud... Исходный файл в репозитории, содержащий эти настройки Рис. 5.6. Конечные точки Spring Cloud Config возвращают настройки для заданного окружения Чтобы получить конфигурацию службы лицензий для окружения разработки, выберите конечную точку GET http://localhost:8071/ licensing-service/dev. На рис. 5.7 показан результат обращения к этой конечной точке. ПРИМЕЧАНИЕ. Это тот порт мы установили выше в файле bootstrap.yml. Присмотревшись, можно заметить, что при выборе конечной точки dev Spring Cloud Configuration Server вернул не только конфигурацию для окружения разработки, но и конфигурацию с настройками по умолчанию. Причина такого поведения Spring Cloud Config в том, что фреймворк Spring реализует иерархический механизм решения проблем. То есть фреймворк Spring сначала ищет свойство, определенное в файле свойств по умолчанию, а затем переопределяет значение по умолчанию значением из файла свойств Настройка Spring Cloud Configuration Server 167 для заданного окружения, если оно присутствует. В частности, если определить свойство в файле licensing-service.properties и не переопределить его ни в одной из других конфигураций (например, licensing-service-dev.properties), то фреймворк Spring будет использовать значение по умолчанию. При попытке получить конфигурацию для конкретного окружения возвращаются настройки lzk запрошенного окружения, а также настройки по умолчанию Рис. 5.7. Получение конфигурации службы лицензий для окружения разработки ПРИМЕЧАНИЕ. Однако вы не увидите этого поведения, напрямую вызвав конечную точку REST службы Spring Cloud Config. Конечная точка REST возвращает все значения из конфигурации для заданного окружения и конфигурации по умолчанию. Теперь, покончив с настройкой нашей службы Spring Cloud Config, перейдем к интеграции Spring Cloud Config с нашим микросервисом лицензий. 168 Глава 5 Управление конфигурациями с использованием Spring Cloud... 5.3. Интеграция Spring Cloud Config с клиентом Spring Boot В предыдущих главах мы создали простой каркас нашей службы лицензий, который не делает ничего и только возвращает жестко запрограммированный объект Java, представляющий одну запись в реестре лицензий. В этом разделе мы добавим в службу лицензий поддержку базы данных PostgreSQL для хранения информации о лицензиях. Почему PostgreSQL? PostgreSQL, также известная как Postgres, считается корпоративной и одной из самых интересных и продвинутых систем управления реляционными базами данных (СУБД) с открытым исходным кодом. PostgreSQL имеет много преимуществ по сравнению с другими реляционными базами данных, но самое главное заключается в том, что она предлагает единую лицензию, полностью бесплатную и открытую для использования кем угодно. Второе важное преимущество заключается в том, что с точки зрения возможностей и функциональности она позволяет работать со значительными объемами данных без увеличения сложности запросов. Вот некоторые из основных возможностей Postgres: Postgres использует мультиверсионное управление конкуренцией, добавляя представление состояния базы данных в каждую транзакцию, что обеспечивает высокую согласованность и производительность транзакций; Postgres не использует блокировки чтения при выполнении транзакций; в Postgres есть так называемый горячий резерв (hot standby), позволяющий клиенту выполнять поиск, пока сервер находится в режиме восстановления или пассивного ожидания. Иначе говоря, Postgres обеспечивает обработку запросов без полной блокировки базы данных. Вот некоторые из основных характеристик PostgreSQL: поддерживается такими языками, как C, C++, Java, PHP, Python и др.; способна обслуживать множество клиентов, доставляя при этом одну и ту же информацию из своих таблиц без блокировок; поддерживает работу с представлениями, поэтому пользователи могут запрашивать данные со структурой, отличающейся от структуры хранения; это объектно-реляционная база данных, позволяющая работать с данными, как если бы они были объектами, и, соответственно, предлагает объектно ориентированные механизмы; позволяет хранить и запрашивать данные в формате JSON. Интеграция Spring Cloud Config с клиентом Spring Boot 169 Мы будем взаимодействовать с базой данных с помощью Spring Data и извлекать данные из таблицы с лицензиями в виде обычных Java-объектов (POJO). Информация из базы данных будет извлекаться службой Spring Cloud Configuration Server. На рис. 5.8 показано, как это будет происходить. При запуске службе лицензий будут передаваться три настройки: профиль Spring, имя приложения и конечная точка, которую служба лицензий должна будет использовать для связи со службой Spring Cloud Config. Профиль Spring отображается в настройки для окружения, в котором выполняется служба. 1. Службе лицензий передаются профиль Spring, имя приложения и конечная точка Spring profile = dev Spring application name = licensing-service Spring Cloud config endpoint = http://localhost:8071 2. Служба лицензий взаимодействует со службой Spring Cloud Config 3. Информация для окружения извлекается из репозитория licensingservice.properties http://localhost:8071/ licensing-service/dev Экземпляр службы лицензий Служба Spring Cloud Config spring.jpa.hibernate.ddl-auto=none spring.jpa.database=POSTGRESQL spring.datasource.platform=postgres spring.jpa.show-sql = true spring.jpa.hibernate.naming-strategy = org.hibernate.cfg.ImprovedNamingStrategy spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.PostgreSQLDialect spring.database.driverClassName= org.postgresql.Driver spring.datasource.testWhileIdle = true spring.datasource.validationQuery = SELECT 1 spring.datasource.url = jdbc:postgresql://localhost:5432/ostock_dev spring.datasource.username = postgres spring.datasource.password = postgres Репозиторий службы управления конфигурациями licensingservice-dev.properties licensingservice-prod.properties 4. Настройки, возвращаемые службе лицензий ... Рис. 5.8. Извлечение конфигурации для окружения разработки Сразу после запуска служба лицензий связывается со службой Spring Cloud Config через конечную точку с URI, сконструированным на основе переданного ей профиля Spring. Затем служба Spring Cloud Config использует свой репозиторий (файловую систему, Git или Vault) для получения информации о конфигурации, соответствующей профилю Spring, переданному в URI. После этого соответствующие значения свойств (настройки) возвращаются службе лицензий, а фреймворк Spring Boot внедряет эти настройки в соответствующие части приложения. 170 Глава 5 Управление конфигурациями с использованием Spring Cloud... 5.3.1. Настройка зависимостей Spring Cloud Config Service в службе лицензий Давайте теперь перенесем наше внимание на службу лицензий. Первое, что мы должны сделать, – добавить еще пару записей в файл Maven с настройками проекта службы лицензирования. В листинге 5.5 показаны строки, которые нужно добавить. Листинг 5.5. Добавление зависимостей в настройки проекта службы лицензий // Части pom.xml опущены для краткости <dependency> <groupId>org.springframework.cloud</groupId> <artifactId> Сообщает Spring Boot о необходимости загрузить зависимости для взаимодейspring-cloud-starter-config ствий со службой Spring Cloud Config </artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId> spring-boot-starter-data-jpa Сообщает Spring Boot о необходимости использовать Java Persistence API (JPA) </artifactId> </dependency> <dependency> <groupId>org.postgresql</groupId> Сообщает Spring Boot о необходи<artifactId>postgresql</artifactId> мости загрузить драйверы Postgres </dependency> Первая зависимость – артефакт spring-cloud-starter-config – содержит классы, необходимые для взаимодействий со службой Spring Cloud Config Server. Вторая и третья зависимости, springboot-starter-data-jpa и postgresql, импортируют Spring Data Java Persistence API (JPA) и драйверы JDBC для Postgres. 5.3.2.Настройка службы лицензий для взаимодействий с Spring Cloud Config После определения зависимостей нужно сообщить службе лицензий, как связаться с Spring Cloud Configuration Server. Передать информацию службам Spring Boot, использующим Spring Cloud Config, можно в одном из следующих файлов: bootstrap.yml, bootstrap.properties, application.yml или application.properties. Как упоминалось выше, служба читает файл bootstrap.yml до чтения любой другой информации о конфигурации. Как правило, файл bootstrap.yml содержит имя приложения службы, профиль и URI для подключения к Configuration Server. Любые другие настройки службы, которые желательно хранить локально (а не в Spring Cloud Config), можно поместить в локальный файл application.yml. Интеграция Spring Cloud Config с клиентом Spring Boot 171 Обычно в файл application.yml помещают настройки, которые должны быть доступными службе, даже если служба Spring Cloud Config недоступна. Файлы bootstrap.yml и application.yml хранятся в каталоге src/main/resources проекта. Чтобы служба лицензий могла взаимодействовать со службой Spring Cloud Config, соответствующие настройки можно поместить в файл bootstrap.yml, в файл docker-compose.yml службы лицензий или передать в аргументах JVM при запуске службы. В листинге 5.6 показано, содержимое файла bootstrap.yml для нашего приложения при выборе этого варианта. Листинг 5.6. Конфигурация службы лицензий в файле bootstrap.yml spring: Имя службы лицензий, чтобы клиент application: Spring Cloud Config знал, какую службу искать name: licensing-service profiles: Профиль по умолчанию, с которым должна запуactive: dev скаться служба. Профиль определяет окружение cloud: Местоположение службы config: Spring Cloud Config Server uri: http://localhost:8071 ПРИМЕЧАНИЕ. Приложения Spring Boot поддерживают два механизма определения свойств: YAML (YAML Ain’t Markup Language – YAML – это не язык разметки) и составные имена свойств, разделенные точками. Мы будем использовать YAML. Иерархический формат определения значений свойств в YAML отображается непосредственно в имена: spring.application.name, spring.profiles.active и spring.cloud.config.uri. Свойство spring.application.name – это имя приложения (например, службы лицензий), которое должно напрямую соответствовать имени каталога конфигурации в Spring Cloud Configuration Server. Второе свойство, spring.profiles.active, определяет профиль для запуска приложения. Имейте в виду, что профиль – это средство, помогающее различать конфигурации, используемые приложением Spring Boot. Профиль службы лицензирования определяет конфигурацию окружения, в котором будет запускаться служба. Например, передав профиль dev в качестве профиля, Config Server будет использовать свойства dev. Если не указать профиль, то служба лицензий будет использовать профиль по умолчанию. Третье и последнее свойство, spring.cloud.config.uri, определяет место, где служба лицензий будет искать конечную точку Config Server. В этом примере служба лицензий будет пытаться подключиться к серверу конфигураций по адресу http:// localhost:8071. 172 Глава 5 Управление конфигурациями с использованием Spring Cloud... Далее в этой главе мы покажем, как переопределить различные свойства, объявленные в файлах bootstrap.yml и application.yml. Таким способом можно указать микросервису лицензий, в каком окружении ему предстоит работать. Если теперь запустить службу Spring Cloud Config с соответствующей базой данных Postgres, работающей на вашем локальном компьютере, то мы сможем запустить службу лицензий, указав профиль dev. Для этого нужно перей­ти в каталог службы лицензий и ввести следующую команду: mvn spring-boot:run ПРИМЕЧАНИЕ. Чтобы служба лицензий могла получить свою конфигурацию, сначала нужно запустить Configuration Server. Выполнив эту команду без параметров, служба лицензий автоматически попытается подключиться к Spring Cloud Configuration Server, используя конечную точку (в данном случае http:// localhost:8071) и активный профиль (dev), как определено в файле bootstrap.yml службы лицензий. Чтобы переопределить значения по умолчанию и указать другое окружение, можно скомпилировать проект службы лицензий в файл JAR, а затем запустить этот файл с переопределенными свойствами в параметре -D. Следующий пример демонстрирует, как запустить службу лицензий и передать ей необходимые настройки через аргументы JVM: java -Dspring.cloud.config.uri=http://localhost:8071 \ -Dspring.profiles.active=dev \ -jar target/licensing-service-0.0.1-SNAPSHOT.jar Этот пример показывает, как переопределить свойства по умолчанию в командной строке. В данном случае переопределяются два свойства: spring.cloud.config.uri spring.profiles.active ПРИМЕЧАНИЕ. Если попытаться запустить службу лицензий, загрузив ее из репозитория GitHub (https://github.com/ihuaylupo/ manning-smia/tree/master/chapter5) с помощью предыдущей команды, то вас ждет неудача, потому что, во-первых, у вас не запущен локальный сервер Postgres, а во-вторых, исходный код в репозитории GitHub использует шифрование на Config Server. Мы покажем, как настроить шифрование, далее в этой главе. В примерах мы жестко запрограммировали значения для передачи в параметрах –D. В облаке большая часть настроек приложения будет храниться на вашем сервере конфигурации. Интеграция Spring Cloud Config с клиентом Spring Boot 173 Все примеры кода для каждой главы можно запустить в контейнерах Docker. С помощью Docker можно моделировать разные окружения, определяя их параметры в файлах Docker Compose, которые управляют запуском всех ваших служб. Настройки, зависящие от окружения, передаются в контейнеры в виде переменных окружения. Например, чтобы запустить службу лицензий в окружении разработки, файл dev/docker-compose.yml для службы лицензий должен содержать строки, показанные в листинге 5.7. Листинг 5.7. Файл dev/docker-compose.yml licensingservice: image: ostock/licensing-service:0.0.1-SNAPSHOT ports: Раздел с определениОпределение переменной - "8080:8080" ями переменных окруокружения SPRING_PROFILES_ жения для контейнера службы лицензий environment: SPRING_PROFILES_ACTIVE: "dev" SPRING_CLOUD_CONFIG_URI: http://configserver:8071 ACTIVE для передачи в команду запуска службы Spring Boot и соответствующего профиля Конечная точка службы Config Раздел environment в файле YML содержит определения двух переменных: SPRING_PROFILES_ACTIVE с названием профиля Spring Boot, под которым будет работать служба лицензий, и SPRING_CLOUD_CONFIG_ URI с URI службы Spring Cloud Configuration Server, которую служба лицензий будет использовать для получения своей конфигурации. Настроив файл Docker Compose, можно запускать службы, просто выполнив следующую команду в каталоге, где находится файл Docker Compose: docker-compose up Поскольку мы расширяем все наши службы поддержкой ретроспекции (самоанализа), подключая Spring Boot Actuator, то мы можем убедиться в выборе верного окружения, обратившись к конечной точке http://localhost:8080/actuator/env. Конечная точка /env возвращает полный набор сведений о конфигурации службы, включая свойства и конечные точки, с которыми служба была запущена (см. рис. 5.9). 174 Глава 5 Управление конфигурациями с использованием Spring Cloud... Рис. 5.9. Проверить правильный выбор конфигурации службой лицензий можно, обратившись к конечной точке /actuator/env. В этом примере можете видеть, какие значения получили свойства licensing-service.properties и licensing-service-dev.properties О раскрытии слишком большого количества информации В каждой организации установлены свои правила безопасности для служб. Многие организации считают, что службы не должны транслировать никакой информации о себе и не должны открывать доступ к таким источникам сведений, как конечная точка /env. Это мнение (вполне справедливое) основано на том, что такие конечные точки позволяют потенциальным хакерам получить слишком много информации. Интеграция Spring Cloud Config с клиентом Spring Boot 175 Spring Boot предлагает ряд возможностей для ограничения информации, возвращаемой конечными точками Spring Actuator. Однако обсуждение этого вопроса выходит за рамки данной книги. Прекрасное освещение этой темы вы найдете в книге Крейга Уоллса (Craig Walls) «Spring Boot in Action» (Manning, 2016)1. Мы настоятельно рекомендуем оценить свои корпоративные политики безопасности и прочитать книгу Уоллса, чтобы настроить уровень подробности информации, возвращаемой конечными точками Spring Actuator. 5.3.3. Подключение к источнику данных с использованием Spring Cloud Config Server На данном этапе информация о конфигурации базы данных напрямую передается микросервису. После настройки базы данных настройка микросервиса лицензий для работы с этой базой данных сводится к использованию стандартных компонентов Spring для создания и извлечения сведений из базы данных Postgres. Давайте продолжим пример и реорганизуем службу лицензий, разбив ее на несколько классов, каждый из которых будет иметь свои обязанности. Эти классы перечислены в табл. 5.2. Таблица 5.2. Классы службы лицензий и их местоположение Класс Местоположение License com.optimagrowth.license.model LicenseRepository com.optimagrowth.license.repository LicenseService com.optimagrowth.license.service Класс License – это класс модели, который будет хранить данные, полученные из базы данных лицензий. В листинге 5.8 показано определение этого класса. Листинг 5.8. Определение модели JPA, представляющей единственную запись в таблице лицензий package com.optimagrowth.license.model; import import import import javax.persistence.Column; javax.persistence.Entity; javax.persistence.Id; javax.persistence.Table; import org.springframework.hateoas.RepresentationModel; import lombok.Getter; 1 Уоллс Крейг, Spring в действии, ДМК Пресс, 2015, ISBN: 978-5-97060-171-6, 978-1-93518235-1. – Прим. перев. Глава 5 Управление конфигурациями с использованием Spring Cloud... 176 import lombok.Setter; import lombok.ToString; @Getter @Setter @ToString @Entity @Table(name="licenses") public class License { Сообщает Spring, что это класс JPA Определяет таблицу в базе данных Объявляет это поле первичным ключом @Id @Column(name = "license_id", nullable = false) private String licenseId; private String description; @Column(name = "organization_id", nullable = false) private String organizationId; @Column(name = "product_name", nullable = false) private String productName; @Column(name = "license_type", nullable = false) private String licenseType; @Column(name="comment") private String comment; Определяет столбец в таблице, соответствующий данному свойству public License withComment(String comment){ this.setComment(comment); return this; } } Класс License в листинге 5.8 декорирован несколькими аннотациями JPA, которые помогают фреймворку Spring Data отобразить данные из таблицы лицензий в базе данных Postgres в объект Java. Аннотация @ Entity сообщает, что этот Java-объект (POJO) представляет объекты с данными. Аннотация @Table задает таблицу в базе данных. Аннотация @Id определяет первичный ключ таблицы. Наконец, все столбцы, отображаемые в свойства объекта, отмечены аннотацией @Column. СОВЕТ. Если свойство имеет то же имя, что и столбец в базе данных, то вам не нужно добавлять аннотацию @Column. Фреймворки Spring Data и JPA предоставляют основные методы CRUD (Create, Read, Update, Delete – создание, чтение, обновление, удаление) для доступа к базе данных. Некоторые из них перечислены в табл. 5.3. Таблица 5.3. Методы CRUD, предоставляемые фреймворками Spring Data и JPA Метод Описание count() Возвращает количество доступных сущностей delete(entity) Удаляет указанную сущность entity deleteAll() Удаляет все сущности из хранилища deleteAll(entities) Удаляет все указанные сущности entities Интеграция Spring Cloud Config с клиентом Spring Boot 177 Окончание табл. 5.3 Метод Описание deleteById(id) Удаляет сущность с указанным первичным ключом id existsById(id) Возвращает признак присутствия сущности с указанным первичным ключом id findAll() Возвращает все экземпляры данного типа findAllById(ids) Возвращает все экземпляры данного типа с указанными первичными ключами ids findById(ID id) Возвращает сущность с указанным первичным ключом id save(entity) Сохраняет указанную сущность entity saveAll(entities) Сохраняет все указанные сущности entities Если понадобится создать методы в дополнение к перечисленным, то для этого можно использовать интерфейс Spring Data Repository и базовые соглашения об именах методов. При запуске Spring проанализирует имена методов из интерфейса Repository, преобразует их в оператор SQL в соответствии с именами, а затем (за кулисами) сгенерирует динамический прокси-класс для выполнения работы. Определение интерфейса Repository для службы лицензий показано в листинге 5.9. Листинг 5.9. Интерфейс LicenseRepository, определяющий методы запросов package com.optimagrowth.license.repository; import import import import java.util.List; org.springframework.data.repository.CrudRepository; org.springframework.stereotype.Repository; com.optimagrowth.license.model.License; @Repository Сообщает Spring Boot, что это класс репозитория JPA. Аннотацию можно опустить, если интерфейс явно расширяет CrudRepository public interface LicenseRepository extends CrudRepository<License,String> { public List<License> findByOrganizationId (String organizationId); public License findByOrganizationIdAndLicenseId (String organizationId, String licenseId); } Расширяет интерфейс CrudRepository Методы заппросов преобразуются в запросы SELECT…FROM Интерфейс репозитория LicenseRepository декорирован аннотацией @Repository, которая сообщает фреймворку Spring, что этот интерфейс следует рассматривать как интерфейс репозитория и для него должен динамически генерироваться прокси-класс. В данном случае прокси-класс предоставляет набор полноценных и готовых к использованию объектов. 178 Глава 5 Управление конфигурациями с использованием Spring Cloud... В Spring определены различные типы репозиториев для доступа к данным. В этом примере мы использовали базовый интерфейс CrudRepository, который расширяется нашим интерфейсом LicenseRepository. Интерфейс CrudRepository определяет основные методы CRUD. В дополнение к этим методам CRUD из CrudRepository мы добавили в LicenseRepository два своих метода запросов, которые извлекают данные из таблицы лицензий. Фреймворк Spring Data разбивает имена методов по заглавным буквам и на основе этого разбиения создает запросы к базе данных. ПРИМЕЧАНИЕ. Фреймворк Spring Data обеспечивает дополнительный уровень абстракции над различными платформами баз данных и может работать не только с реляционными базами данных. Он также поддерживает базы данных NoSQL, такие как MongoDB и Cassandra. В отличие от предыдущей инкарнации службы лицензий, которую мы создали в главе 3, в этой версии мы отделили бизнес-логику от логики доступа к данным, выделив последнюю из LicenseController в отдельный класс Service с именем LicenseService. Его определение показано в листинге 5.10. Между этим классом LicenseService и версиями, рассмотренными в предыдущих главах, появилось множество различий, потому что мы добавили связь с базой данных. Вы можете скачать полный файл с кодом по следующей ссылке: https://github.com/ihuaylupo/manning-smia/tree/master/chapter5/ licensing-service/src/main/java/com/optimagrowth/license/service/ LicenseService.java Листинг 5.10. Класс LicenseService, извлекающий информацию о лицензиях из базы данных @Service public class LicenseService { @Autowired MessageSource messages; @Autowired private LicenseRepository licenseRepository; @Autowired ServiceConfig config; public License getLicense(String licenseId, String organizationId){ License license = licenseRepository .findByOrganizationIdAndLicenseId(organizationId, licenseId); if (null == license) { throw new IllegalArgumentException( String.format(messages.getMessage( "license.search.error.message", null, null), licenseId, organizationId)); } Интеграция Spring Cloud Config с клиентом Spring Boot 179 return license.withComment(config.getProperty()); } public License createLicense(License license){ license.setLicenseId(UUID.randomUUID().toString()); licenseRepository.save(license); return license.withComment(config.getProperty()); } public License updateLicense(License license){ licenseRepository.save(license); return license.withComment(config.getProperty()); } public String deleteLicense(String licenseId){ String responseMessage = null; License license = new License(); license.setLicenseId(licenseId); licenseRepository.delete(license); responseMessage = String.format(messages.getMessage( "license.delete.message", null, null),licenseId); return responseMessage; } } Классы контроллера, службы и репозитория связываются вместе с помощью стандартной аннотации @Autowired. Теперь давайте посмотрим, как реализовать в классе LicenseService чтение настроек из конфигурации. 5.3.4.Чтение настроек с использованием @ConfigurationProperties В классе LicenseService, в методе getLicense(), мы возвращаем результат вызова license.withComment() со значением config.getProperty(). Вот этот код: return license.withComment(config.getProperty()); Если заглянуть в класс com.optimagrowth.license.config.ServiceConfig.java, то можно увидеть, что он аннотирован, как показано ниже. (В листинге 5.11 показана аннотация @ConfigurationProperties, которую мы будем использовать.) @ConfigurationProperties(prefix= "example") Листинг 5.11. Использование ServiceConfig для централизованного хранения настроек приложения package com.optimagrowth.license.config; import org.springframework.beans.factory.annotation.Value; 180 Глава 5 Управление конфигурациями с использованием Spring Cloud... import org.springframework.stereotype.Component; @ConfigurationProperties(prefix = "example") public class ServiceConfig{ private String property; public String getProperty(){ return property; } } В отличие от настроек базы данных, которые «автоматически и как по волшебству» Spring Data внедряет в объект соединения с базой данных, все другие настройки могут внедряться с помощью аннотации @ConfigurationProperties. Обратите внимание на строку @ConfigurationProperties(prefix= "example") в листинге 5.11, которая извлекает все свойства example из Spring Cloud Configuration Server и вставляет их в атрибут property класса ServiceConfig. СОВЕТ. Конечно, можно и напрямую внедрять значения настроек в свойства отдельных классов, но мы сочли полезным централизовать всю информацию о конфигурации в одном классе, а затем внедрить этот класс туда, где он необходим. 5.3.5.Обновление настроек с использованием Spring Cloud Config Server Один из первых вопросов, который возникает у разработчиков, начинающих использовать Spring Cloud Configuration Server, – как динамически обновлять настройки приложения при изменении конфигурации. Организовать это просто. Config Server всегда возвращает самую последнюю версию настроек. Изменения, внесенные в настройки через его базовый репозиторий, становятся доступны службам немедленно! Однако приложения Spring Boot читают конфигурацию только в момент запуска, поэтому изменения, выполненные в Config Server, не попадут в приложения автоматически. Для решения этой проблемы Spring Boot Actuator предлагает аннотацию @RefreshScope, которая позволяет получить доступ к конечной точке /refresh и заставить приложение Spring Boot повторно прочитать свою конфигурацию. Листинг 5.12 демонстрирует эту аннотацию в действии. Листинг 5.12. Аннотация @RefreshScope package com.optimagrowth.license; import org.springframework.boot.SpringApplication; Интеграция Spring Cloud Config с клиентом Spring Boot 181 import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.context.config.annotation.RefreshScope; @SpringBootApplication @RefreshScope public class LicenseServiceApplication { public static void main(String[] args) { SpringApplication.run(LicenseServiceApplication.class, args); } } Отметим пару особенностей аннотации @RefreshScope. Она повторно загружает только настройки Spring, присутствующие в конфигурации приложения. Другие настройки, такие как конфигурация базы данных для Spring Data, не загружаются этой аннотацией. Об обновлении конфигурации микросервисов Используя службу Spring Cloud Config с микросервисами для динамического изменения настроек, необходимо учитывать одно обстоятельство: возможное наличие нескольких экземпляров одной и той же службы, каждый из которых должен обновить конфигурацию при ее изменении. Есть несколько способов решить эту проблему. Служба Spring Cloud Config предлагает механизм рассылки pushуведомлений, который называется Spring Cloud Bus и позволяет серверу Spring Cloud Configuration Server посылать уведомления об изменениях всем клиентам, использующим эту службу. Spring Cloud Bus требует применения дополнительного программного обеспечения: RabbitMQ. Это чрезвычайно эффективный способ обнаружения изменений, но не все механизмы Spring Cloud Config поддерживают pushуведомления (например, сервер Consul). В следующей главе мы покажем пример использования Spring Cloud Service Discovery и Eureka для регистрации всех экземпляров службы. Вот один из методов, который мы использовали для обработки событий обновления конфигурации приложения в Spring Cloud Config и обновления настроек в приложении: мы написали простой сценарий, запрашивающий механизм обнаружения служб, отыскивающий все запущенные экземпляры службы и напрямую вызывающий конечную точку /refresh. Также для обновления конфигурации можно перезапустить все серверы или контейнеры. Это тривиальное упражнение, особенно если службы запускаются в контейнерах, таких как Docker. Повторный запуск контейнеров Docker занимает буквально секунды и обеспечивает повторное чтение конфигурации приложениями. Помните, что облачные серверы эфемерны. Не бойтесь запускать новые экземпляры службы после обновления конфигурации и затем направлять трафик новым экземплярам и останавливать старые. 182 Глава 5 Управление конфигурациями с использованием Spring Cloud... 5.3.6. Использование Spring Cloud Configuration Server с Git Как упоминалось выше, использование файловой системы в качестве внутреннего репозитория для Spring Cloud Configuration Server не всегда практично для облачных приложений. Это связано с необходимостью настраивать и управлять общей файловой системой, смонтированной на всех экземплярах Config Server, тем более что Config Server может интегрироваться с различными механизмами хранения конфигурации приложения. Один из подходов, который мы успешно использовали в своей практике, – использование Spring Cloud Configuration Server с репозиторием Git. Используя Git, можно получить все преимущества системы управления версиями и простой механизм интеграции развертывания файлов конфигурации в конвейер сборки и развертывания. Чтобы использовать Git, нужно добавить настройки в файл bootstrap.yml службы Spring Cloud Config. В листинге 5.13 показано, как это сделать. Листинг 5.13. Добавление поддержки Git в файл bootstrap.yml с настройками Spring Cloud spring: application: name: config-server profiles: Все активные профили (список названий профилей, разделенactive: ных запятыми) - native, git Сообщает cloud: Spring config: Cloud Сообщает server: Config Spring Cloud native: адрес серConfig исsearch-locations: classpath:/config вера Git и пользовать репозитоgit: Git в качестве рия uri: https://github.com/ihuaylupo/config.git репозитория searchPaths: licensingservice Сообщает Spring server: Cloud Config путь в Git port: 8071 для поиска конфигурационных файлов Вот четыре ключевых настройки в предыдущем листинге: spring.profiles.active; spring.cloud.config.server.git; spring.cloud.config.server.git.uri; spring.cloud.config.server.git.searchPaths. Свойство spring.profiles.active определяет список активных профилей для службы Spring Config. В этом списке через запятую перечисляются профили в порядке возрастания приоритета. По аналогии с приложениями Spring Boot активные профили имеют приоритет над профилями по умолчанию, и последний профиль в списке считается самым приоритетным. Свойство spring.cloud.config.server.git предпи- Интеграция Spring Cloud Config с клиентом Spring Boot 183 сывает серверу Spring Cloud Config Server использовать репозиторий, не основанный на файловой системе. В листинге 5.13 мы описали настройки подключения к облачному репозиторию Git – GitHub. ПРИМЕЧАНИЕ. Если у вас есть учетная запись GitHub, то укажите свои имя пользователя и пароль (личный токен или конфигурацию SSH) в настройках Git в файле bootstrap.yml сервера конфигурации. Свойство spring.cloud.config.server.git.uri определяет URL репозитория. И наконец, свойство spring.cloud.config.server.git.searchPaths сообщает серверу Config Server относительный путь в репозитории Git, где следует производить поиск на этапе инициализации сервера Cloud Config Server. Так же как в версии конфигурации с настройками для файловой системы, в атрибуте spring.cloud.config.server.git. searchPaths можно перечислить через запятую несколько путей поиска для каждой службы, использующей службу конфигурации. ПРИМЕЧАНИЕ. По умолчанию в Spring Cloud Config используется репозиторий Git. 5.3.7. Интеграция Vault со службой Spring Cloud Config Как упоминалось выше, мы покажем пример использования еще одного репозитория: HashiCorp Vault. Vault – это инструмент, обеспечивающий безопасный доступ к секретам. Мы можем ограничить доступ к секретам, например с помощью паролей, сертификатов, ключей API и т. д. Чтобы интегрировать Vault с нашей службой Spring Config, нужно добавить профиль Vault. Этот профиль обеспечивает интеграцию с Vault и позволяет безопасно хранить конфиденциальные настройки микросервисов. Для реализации этой интеграции мы используем Docker и создадим контейнер Vault с помощью следующей команды: docker run -d -p 8200:8200 --name vault -e 'VAULT_DEV_ROOT_TOKEN_ID=myroot' ➥ -e 'VAULT_DEV_LISTEN_ADDRESS=0.0.0.0:8200' vault Команда docker run содержит следующие параметры: VAULT_DEV_ROOT_TOKEN_ID – определяет идентификатор сгенерированного корневого токена. Корневой токен – это начальный токен доступа для начала настройки Vault. Этот параметр задает значение идентификатора для начального сгенерированного корневого токена; VAULT_DEV_LISTEN_ADDRESS – определяет IP-адрес и порт на сервере разработки; значение по умолчанию: 0.0.0.0:8200. ПРИМЕЧАНИЕ. В этом примере мы запустим Vault локально. Дополнительную информацию о том, как запустить Vault в режиме сервера, вы найдете в официальном описании образа Vault Docker по адресу https://hub.docker.com/_/vault. 184 Глава 5 Управление конфигурациями с использованием Spring Cloud... После загрузки последней версии образа Vault в Docker можно приступать к созданию секретов. Для простоты в этом примере мы будем использовать пользовательский интерфейс Vault, но если вы предпочитаете командную строку, то можете использовать ее. 5.3.8. Пользовательский интерфейс Vault Vault предлагает унифицированный интерфейс, упрощающий процесс создания секретов. Для доступа к этому пользовательскому интерфейсу введите URL: http://0.0.0.0:8200/ui/vault/auth. Этот URL определяется параметром VAULT_DEV_LISTEN_ADDRESS в команде docker run. На рис. 5.10 показана страница входа в пользовательский интерфейс Vault. Следующий шаг – создание секрета. Чтобы создать секрет, после входа в систему перейдите на вкладку Secrets (Секреты) в панели управления в пользовательском интерфейсе Vault. В этом примере мы создадим секрет с именем secret/licensingservice и со свойством license.vault.property и присвоим ему значение "Welcome to vault". Доступ к этой информации должен быть ограничен, а сама она должна шифроваться при передаче. Для этого сначала создадим новый механизм управления секретами, а затем добавим в него этот конкретный секрет. На рис. 5.11 показано, как создать этот механизм с помощью пользовательского интерфейса Vault. После создания нового механизма управления секретами создадим наш секрет, как показано на рис. 5.12. Значение параметра VAULT_DEV_ROOT_TOKEN_ID, указанное в команде docker run Рис. 5.10. Страница входа в пользовательский интерфейс Vault. Для входа введите следующий URL в поле Token (Токен): http://0.0.0.0:8200/ui/vault/auth Интеграция Spring Cloud Config с клиентом Spring Boot 185 1. Панель управления секретами 3. Выбрать универсальный механизм KV 2. Создать новый механизм 4. Заполнить данные для механизма секретов KV 5. Create secret Имя приложения Spring Рис. 5.11. Создание нового механизма управления секретами в пользовательском интерфейсе Vault Профиль Spring Рис. 5.12. Создание нового секрета в пользовательском интерфейсе Vault 186 Глава 5 Управление конфигурациями с использованием Spring Cloud... Теперь, настроив Vault и определив секрет, давайте настроим Spring Cloud Config Server для использования Vault. С этой целью добавим профиль Vault в наш файл bootstrap.yml сервера Config Server. В листинге 5.14 показано, как должен выглядеть этот файл. Листинг 5.14. Добавление поддержки Vault в файл bootstrap.yml с настройками Spring Cloud spring: application: name: config-server profiles: active: - vault cloud: config: Сообщает Spring Cloud Config server: использовать Vault в качестве репозитория vault: Порт Vault port: 8200 host: 127.0.0.1 Хост Vault kvVersion: 2 Версия механизма управления server: секретами KV port: 8071 ПРИМЕЧАНИЕ. Обратите внимание на версию механизма управления секретами KV. По умолчанию параметр spring.cloud.config.server.kv-version получает значение 1. Но вообще, мы рекомендуем выбирать версию 2 при использовании версии Vault 0.10.0 или выше. Теперь, выполнив все необходимые настройки, протестируем Config Server, отправив HTTP-запрос. Это можно сделать с помощью утилиты cURL, как показано ниже, или какого-нибудь клиента REST, например Postman: $ curl -X "GET" "http://localhost:8071/licensing-service/default" -H ➥ "X-Config-Token: myroot" Если настройки были выполнены правильно, то эта команда должна вернуть следующий ответ: { "name": "licensing-service", "profiles": [ "default" ], "label": null, "version": null, "state": null, Защита конфиденциальных настроек в конфигурации 187 "propertySources": [ { "name": "vault:licensing-service", "source": { "license.vault.property": "Welcome to vault" } } ] } 5.4.Защита конфиденциальных настроек в конфигурации По умолчанию Spring Cloud Configuration Server хранит все настройки в файлах конфигурации приложения в обычном текстовом виде. В том же открытом виде хранится конфиденциальная информация, такая как имена пользователей и пароли баз данных и т. д. Хранить учетные данные в открытом виде в репозитории с исходным кодом – крайне плохая практика. К сожалению, это случается гораздо чаще, чем вы думаете. Spring Cloud Config дает возможность зашифровать конфиденциальные настройки с использованием симметричных (общих) и асимметричных (с парами закрытый/открытый) ключей шифрования. Асимметричное шифрование безопаснее симметричного, потому что использует более современные и сложные алгоритмы. Но иногда удобнее использовать симметричное шифрование, потому что для этого достаточно определить только одно значение в файле bootstrap.yml для Config Server. 5.4.1. Настройка симметричного шифрования Ключ симметричного шифрования – это не что иное, как общий секрет, который используется для шифрования и дешифрования значений. В Spring Cloud Configuration Server ключ симметричного шифрования представляет собой строку символов, которую можно определить в файле bootstrap.yml с настройками Config Server или передать службе через переменную окружения ENCRYPT_ KEY. Вы можете выбрать любой вариант, наиболее соответствующий вашим потребностям. ПРИМЕЧАНИЕ. Ключ симметричного шифрования должен состоять из 12 или более символов, в идеале случайных. Рассмотрим для начала пример настройки ключа симметричного шифрования в файле bootstrap для Spring Cloud Configuration Server. В листинге 5.15 показано, как это сделать. 188 Глава 5 Управление конфигурациями с использованием Spring Cloud... Листинг 5.15. Настройка симметричного шифрования в файле bootstrap.yml cloud: config: server: native: search-locations: classpath:/config git: uri: https://github.com/ihuaylupo/config.git searchPaths: licensingservice server: port: 8071 encrypt: key: secretkey Сообщает Config Server использовать это значение в качестве ключа симметричного шифрования Далее в этой книге мы всегда будем устанавливать переменную окружения ENCRYPT_KEY командой export ENCRYPT_KEY=IMSYMMETRIC. Но вы можете использовать файл bootstrap.yml для тестирования на локальном компьютере без использования Docker. Управление ключами шифрования В этой книге мы приняли два решения, которые обычно считаются недопустимыми при развертывании в промышленном окружении: выбрали в качестве ключа шифрования осмысленную фразу. Мы хотели, чтобы ключ был простым для запоминания и хорошо вписывался в текст книги. В реальной жизни мы используем разные ключи шифрования для разных окружений, куда выполняется развертывание, а сами ключи составляем из случайно выбранных символов; мы жестко определили переменную окружения ENCRYPT_KEY в файлах Docker, используемых в примерах для книги. Мы сделали это для того, чтобы вы, наши читатели, могли загружать и запускать файлы, не заботясь об определении переменных окружения. В реальной жизни внутри файлов Dockerfile мы лишь ссылаемся на переменную окружения ENCRYPT_KEY. Помните об этом и не включайте ключи шифрования в файлы Docker. Помните, что ваши файлы Dockerfile должны храниться в системе управления версиями. 5.4.2. Шифрование и дешифрование настроек Теперь мы готовы начать шифрование настроек для использования со Spring Cloud Config. Давайте зашифруем пароль к базе данных Postgres службы лицензий. Пароль задается свойством spring. datasource.password со значением postgres, которое в настоящее время хранится в открытом текстовом виде. Защита конфиденциальных настроек в конфигурации 189 На этапе запуска экземпляр Spring Cloud Config проверяет наличие переменной кружения или параметра ENCRYPT_KEY в файле bootstrap и автоматически добавляет две новые конечные точки, /encrypt и /decrypt в службу Spring Cloud Config. Мы будем использовать конечную точку /encrypt для шифрования значения postgres. На рис. 5.13 показано, как зашифровать значение postgres с помощью конечной точки /encrypt и Postman. ПРИМЕЧАНИЕ. Для обращения к конечным точкам /encrypt и / decrypt следует использовать запросы POST. Новая конечная точка encrypt URI службы The Spring Cloud Config Server Шифруемое значение Результат шифрования Рис. 5.13. Шифрование пароля для доступа к источнику данных с использованием конечной точки /encrypt Чтобы расшифровать значение, нужно обратиться к конечной точке /decrypt и передать ей зашифрованную строку. Теперь зашифрованное значение можно сохранить в репозитории GitHub или в файле конфигурации в файловой системе, используя синтаксис, показанный в листинге 5.16. 190 Глава 5 Управление конфигурациями с использованием Spring Cloud... Листинг 5.16. Добавление зашифрованного значения в файл свойств службы лицензий spring.datasource.url = jdbc:postgresql://localhost:5432/ostock_dev spring.datasource.username = postgres spring.datasource.password = {cipher} ➥ 559ac661a1c93d52b9e093d3833a238a142de7772961d94751883b17c41746a6 Spring Cloud Configuration Server требует, чтобы всем зашифрованным значениям предшествовал атрибут {cipher}. Он сообщает службе Config Server, что значение зашифровано. 5.5. Заключительные мысли Управление конфигурацией приложений может показаться чем-то обыденным и не заслуживающим пристального внимания, однако в облачных окружениях правильная организация управления конфигурацией играет важнейшую роль. Как будет показано в следующих главах, крайне важно, чтобы приложения и серверы, на которых они работают, были неизменяемыми и сам сервер никогда не настраивался вручную при передаче между окружениями. Это противоречит традиционным моделям развертывания, согласно которым артефакт приложения (например, файл JAR или WAR) развертывается вместе с его файлами свойств в «фиксированном» окружении. Однако в облачной модели конфигурация должна полностью отделяться от приложения, и затем разные конфигурации, соответствующие разным окружениям, должны внедряться во время выполнения, чтобы во все окружения передавались одни и те же артефакты сервера/приложения. Итоги Spring Cloud Configuration Server (или просто Config Server) позволяет настраивать свойства приложения разными значениями в разных окружениях. Spring использует профили, чтобы в момент запуска службы определить окружение, для которого следует получить настройки из службы Spring Cloud Config. Служба Spring Cloud Config может хранить конфигурации приложений в файловой системе, в Git или Vault. Служба Spring Cloud Config позволяет шифровать конфиденциальные данные с использованием алгоритмов симметричного и асимметричного шифрования. 6 Обнаружение служб Эта глава: рассказывает, почему обнаружение служб играет важную роль для облачных приложений; сравнивает механизм обнаружения служб с балансировщиком нагрузки; описывает порядок настройки Spring Netflix Eureka Server; демонстрирует приемы регистрации микросервисов Spring Boot в Eureka; показывает, как использовать библиотеку Spring Cloud Load Balancer для балансировки нагрузки на стороне клиента. В любой распределенной архитектуре нужно знать имя или IPадрес хоста, на котором выполняется служба. С самого начала распределенных вычислений для этой цели использовался механизм, официально известный как «обнаружение служб». Механизм обнаружения служб может быть таким же простым, как файлы с адресами всех удаленных служб, используемых приложением, или сложным, как репозиторий универсального описания, обнаружения и интеграции (Universal Description, Discovery, and Integration UDDI). Обнаружение служб имеет решающее значение для облачных микросервисов по двум основным причинам: поддержке горизонтального масштабирования – для этого обычно требуется дополнительная настройка архитектуры приложения, например добавление дополнительных экземпляров службы и контейнеров; 192 Глава 6 Управление конфигурациями с использованием Spring Cloud... устойчивости – способности ослаблять влияние проблем, возникающих в окружении или в службе. Микросервисные архитектуры должны быть бескомпромиссными к предотвращению каскадного распространения проблем за границы отдельного экземпляра службы. Во-первых, обнаружение служб позволяет быстро масштабировать службы по горизонтали за счет увеличения числа экземпляров и обеспечивает независимость клиентов от физического местоположения служб. Поскольку клиенты не зависят от фактического местоположения экземпляров службы, появляется возможность запускать новые или останавливать имеющиеся экземпляры. Способность быстрого масштабирования службы без влияния на ее клиентов является убедительным аргументом. Она может помочь команде разработчиков, привыкших создавать монолитные приложения с одним клиентом, перейти от представления о масштабировании только с точки зрения добавления большего и лучшего оборудования (вертикальное масштабирование) к более надежному подходу с увеличением количества серверов и служб (горизонтальное масштабирование). Монолитные архитектуры вынуждают команды разработчиков следовать по пути резервирования избыточных мощностей, потому что рост потребностей в обслуживании редко бывает плавным и устойчивым процессом. Представьте, например, возрастание количества запросов к сайтам электронной коммерции перед праздниками. Микросервисы позволяют добавлять новые экземпляры служб по мере необходимости. Обнаружение служб помогает абстрагироваться от этих развертываний, чтобы они происходили незаметно для клиентов. Второе преимущество: обнаружение служб помогает повысить устойчивость приложений. Когда экземпляр микросервиса потерпит сбой или окажется недоступным, большинство механизмов обнаружения служб автоматически удалят его из своего внутреннего списка доступных служб. Ущерб, вызванный выходом из строя одного экземпляра, в этом случае минимален, потому что механизм обнаружения служб направит трафик в обход него. Все это может показаться чересчур сложным, и у кого-то даже возникнет вопрос: почему для обнаружения служб нельзя использовать более простые и проверенные методы, такие как DNS (Domain Name Service – службу доменных имен) или балансировщик нагрузки? Давайте разберемся, почему эти подходы не работают с микросервисами, особенно с облачными, а затем посмотрим, как реализовать службу Eureka Discovery в нашей архитектуре. Где моя служба? 6.1. 193 Где моя служба? Приложения, использующие службы Если у вас есть приложение, обращающееся к ресурсам, распределенным по нескольким серверам, ему необходим механизм, помогающий найти физическое местоположение этих ресурсов. В обычном необлачном мире местоположение службы часто определялось с помощью DNS и балансировщика нагрузки (рис. 6.1). В этом традиционном сценарии, когда приложению требовалось вызвать службу, находящуюся в другой части организации, оно использовало обобщенное DNS-имя и путь, однозначно представляющие вызываемую службу. DNS-имя передавалось балансировщику нагрузки, такому как коммерческий балансировщик F5 (http://f5.com) или балансировщик с открытым исходным кодом HAProxy (http://haproxy.org). 1. Приложение использует обобщенное DNS-имя и путь для получения адреса конкретной службы Уровень служб Уровень разрешения служб services.ostock.com/servicea services.ostock.com/serviceb DNS-имя передается балансировщику нагрузки (services.ostock.com) Проверка Таблицы маршрутизации Служба A Первичный балансировщик нагрузки Вторичный балансировщик нагрузки Служба B 2. Балансировщик нагрузки определяет физические адреса серверов, на которых выполняется служба 4. Вторичный балансировщик нагрузки проверяет работу основного балансировщика нагрузки и при необходимости берет его обязанности на себя 3. Службы, развернутые в контейнерах приложения, которое выполняется на постоянном сервере Рис. 6.1. Традиционная модель определения местоположения службы с использованием DNS и балансировщика нагрузки В традиционном сценарии балансировщик нагрузки, получив запрос от клиента службы, отыскивает запись в таблице маршрутизации, содержащую список с одним или несколькими с физическими адресами серверов, где запущена служба. Затем подсистема балан- 194 Глава 6 Управление конфигурациями с использованием Spring Cloud... сировки нагрузки, руководствуясь определенными правилами, выбирает один из серверов и передает запрос ему. В этой устаревшей модели каждый экземпляр службы развертывался на одном или нескольких серверах приложений. Количество серверов приложений обычно оставалось статическим (т. е. не увеличивалось и не уменьшалось) и постоянным (если сервер выходил из строя, то его заменял другой сервер с тем же состоянием, которое имело место до сбоя, т. е. с тем же IP-адресом и конфигурацией). Для достижения высокой доступности вторичный балансировщик нагрузки, находясь в режиме ожидания, периодически проверял связь с первичным балансировщиком, и если тот не отвечал, то вторичный балансировщик переходил в режим активной работы, принимая на себя IP-адрес основного балансировщика, и начинал обслуживать запросы. Модель этого типа хорошо подходит для приложений, которые действуют внутри четырех стен корпоративного центра обработки данных и имеют относительно небольшое количество служб, развернутых на нескольких статических серверах, но она непригодна для облачных приложений на основе микросервисов по следующим причинам: даже притом что балансировщик нагрузки можно сделать высокодоступным, он образует единственную точку отказа для всей инфраструктуры. Если балансировщик нагрузки отключится, то вместе с ним отключатся все приложения, находящиеся за ним. Балансировщик нагрузки можно сделать высокодоступным, но обычно балансировщики нагрузки образуют централизованные узкие места в инфраструктуре приложений; объединение служб в единый кластер с балансировщиками нагрузки ограничивает возможность горизонтального масштабирования на нескольких серверах. Многие коммерческие балансировщики нагрузки накладывают два ограничения: модель избыточности и стоимость лицензирования. Большинство коммерческих балансировщиков нагрузки поддерживают избыточность, следуя модели горячей замены. Поэтому у вас может быть только один сервер для обработки нагрузки, при этом вторичный балансировщик используется только для аварийного переключения, когда основной выйдет из строя. Вы, по сути, ограничены возможностями оборудования. Кроме того, коммерческие балансировщики нагрузки имеют ограничительные модели лицензирования, ориентированные на фиксированную емкость, а не на более гибкую модель; большинство традиционных балансировщиков нагрузки управляется статически. Они не предназначены для быстрой регистрации и дерегистрации служб. Традиционные балансировщики Обнаружение служб в облаке 195 нагрузки используют централизованную базу данных для хранения маршрутов и правил, и часто единственный способ добавить новые маршруты – использовать проприетарный API производителя; балансировщик нагрузки действует подобно прокси-серверу для служб, поэтому запросы клиентов должны отображаться в физические службы. Этот дополнительный уровень трансляции увеличивает общую сложность инфраструктуры служб, потому что правила трансляции должны определяться и развертываться вручную. Кроме того, традиционный сценарий балансировки нагрузки не предусматривает регистрацию новых экземпляров службы при их запуске. Эти четыре причины не являются приговором для балансировщиков нагрузки. Балансировщики хорошо работают в корпоративной среде, где большинство приложений имеют относительно небольшой размер и масштаб и могут обслуживаться посредством централизованной сетевой инфраструктуры. Но балансировщики нагрузки по-прежнему должны играть роль централизованных терминалов SSL и обеспечивать безопасность портов служб. Они могут заблокировать входящий (на запись) и исходящий (на чтение) доступ к портам любых серверов, находящихся за ними. Эта концепция «минимизации доступа к сети» часто оказывается критическим компонентом, когда возникает необходимость соответствовать отраслевым стандартам сертификации, таким как соответствие требованиям индустрии платежных карт (Payment Card Industry, PCI). Однако в облаке, где приходится иметь дело с огромным количеством транзакций и избыточностью, централизация любой части сетевой инфраструктуры ухудшает ее эффективность. Это связано с плохой масштабируемостью и экономичностью. Теперь давайте посмотрим, как можно реализовать надежный механизм обнаружения служб для своих облачных приложений. 6.2. Обнаружение служб в облаке Решением для облачного окружения микросервисов является такой механизм обнаружения служб, который: обладает высокой доступностью. Механизм обнаружения служб должен поддерживать «горячую» кластеризацию, когда поиск служб может производиться несколькими узлами. Если какойто узел становится недоступным, другие узлы должны иметь возможность немедленно взять его обязанности на себя. Кластер можно определить как группу из нескольких экземпляров сервера. Все экземпляры имеют одинаковую конфигурацию и работают вместе, чтобы обеспечить высокую доступность, безотказность и масштабируемость. В сочетании 196 Глава 6 Управление конфигурациями с использованием Spring Cloud... с балансировщиком нагрузки кластер может поддерживать аварийное переключение для предотвращения прерывания обслуживания и репликацию сеансов для их восстановления в случае сбоя; образует одноранговую сеть. Каждый узел в кластере обнаружения служб хранит копию состояния экземпляра службы; балансирует нагрузку. Механизм обнаружения служб должен динамически балансировать нагрузку и гарантировать равномерное распределение запросов между всеми экземплярами службы, которыми управляет. Во многих отношениях обнаружение служб заменяет более статические балансировщики нагрузки с ручным управлением, которые широко использовались в ранних реализациях веб-приложений; устойчив. Клиент механизма обнаружения служб должен кешировать информацию о службах у себя. Это гарантирует постепенность деградации качества обслуживания, т. е. если механизм обнаружения служб станет недоступным, то приложения смогут продолжать работать и обнаруживать службы, используя информацию в локальном кеше; отказоустойчив. Механизм обнаружения служб должен определять, когда экземпляр службы перестал работать, и удалять его из списка доступных служб, которые могут принимать запросы клиентов. Он должен обнаруживать сбои в службах и принимать необходимые меры без вмешательства человека. В следующих разделах мы: представим вашему вниманию концептуальную архитектуру агента обнаружения облачных служб; покажем, как кеширование и балансировка нагрузки на стороне клиента позволяют службе продолжать работать даже после выхода из строя агента обнаружения служб; покажем, как реализовать обнаружение служб с помощью Spring Cloud и агентов обнаружения служб Eureka от компании Netflix. 6.2.1. Архитектура механизма обнаружения служб Перед обсуждением механизмов обнаружения служб мы должны представить вам четыре концепции, которые часто используются в реализациях этих механизмов: регистрацию службы – определяет, как производится регистрация служб в агенте обнаружения служб; поиск адреса службы клиентом – определяет, как происходит поиск сведений о службах; Обнаружение служб в облаке 197 обмен информацией – определяет, как узлы механизма обмениваются информацией о службах; мониторинг работоспособности – определяет, как службы сообщают о своем состоянии агенту обнаружения служб. Основная цель обнаружения служб – организовать такую архитектуру, в которой службы сами будут сообщать о своем физическом местоположении. На рис. 6.2 показано, как добавляются и удаляются экземпляры службы и как они регистрируются в реестре агента обнаружения служб и становятся доступными для обработки запросов пользователей. Также на рис. 6.2 показана последовательность из четырех пунктов в предыдущем списке (регистрация службы, поиск адреса службы клиентом, обмен информацией и мониторинг работоспособности) и что обычно происходит в реализациях шаблона обнаружения служб. На рисунке показано несколько узлов, на которых выполняется механизм обнаружения служб. Перед экземплярами этого механизма обычно нет дополнительных балансировщиков нагрузки. Клиентские приложения никогда не знают заранее IP-адреса службы – они получают его от агента обнаружения служб 3. Узлы механизма обнаружения служб обмениваются между собой информацией о работоспособных экземплярах службы Клиентские приложения Уровень обнаружения служб 1. Местоположение службы можно выяснить у агента обнаружения служб, передав ему логическое имя 2. Когда служба подключается к сети, она регистрирует свой IP-адрес в реестре агента обнаружения служб Узел механизма обнаружения служб 1 Узел механизма обнаружения служб 2 Узел механизма обнаружения служб 3 Контрольный 4. Службы сигнал отправляют Экземпляры службы Служба A x контрольные сигналы агенту обнаружения служб. Если служба выходит из строя, то уровень обнаружения удаляет ее IP из реестра Рис. 6.2. По мере добавления или удаления экземпляров службы узлы механизма обнаружения служб обновляют информацию у себя и обеспечивают доступность или недоступность экземпляров для обработки запросов пользователей 198 Глава 6 Управление конфигурациями с использованием Spring Cloud... На этапе запуска экземпляры службы регистрируют свое физическое местоположение, путь и порт, которые один или несколько экземпляров механизма обнаружения служб смогут использовать для доступа к экземплярам службы. Каждый экземпляр службы имеет уникальный IP-адрес и порт, но все они регистрируются под одним и тем же идентификатором службы. Идентификатор службы – это просто некоторый ключ, однозначно идентифицирующий группу экземпляров одной и той же службы. Регистрация обычно выполняется только в одном экземпляре службы обнаружения служб. Большинство реализаций механизмов обнаружения служб используют одноранговую модель распространения данных, в которой информация о каждом новом экземпляре службы передается всем узлам в кластере. В зависимости от реализации механизм распространения информации может использовать жестко запрограммированный список узлов или протокол широковещательной рассылки, чтобы позволить другим узлам «обнаруживать» изменения в кластере. ПРИМЕЧАНИЕ. Желающим узнать больше о протоколах широковещательной рассылки мы можем порекомендовать статью «Gossip Protocol» на сайте проекта Consul (https://www.consul. io/docs/internals/gossip.html) или статью Брайана Сторти (Brian Storti) «SWIM: The scalable membership protocol» (https://www. brianstorti.com/swim/). Наконец, каждый экземпляр службы отправляет механизму обнаружения служб – сам или по запросу – информацию о своем состоянии. Любые экземпляры, не подтвердившие свою работоспособность, удаляются из пула доступных экземпляров службы. После регистрации в механизме обнаружения служб экземпляр готов к использованию. Существуют разные модели обнаружения служб клиентом. Первый подход основан исключительно на использовании механизма обнаружения служб – при каждом обращении к микросервису клиент посылает запрос механизму обнаружения служб. К сожалению, этот подход слишком хрупкий, потому что клиент целиком и полностью зависит от механизма обнаружения служб. Более надежный подход заключается в использовании так называемой балансировки нагрузки на стороне клиента. Он основан на алгоритме выбора экземпляров службы по признаку принадлежности к зоне или циклического перебора. Когда мы говорим «алгоритм циклической балансировки нагрузки», то имеем в виду способ распределения клиентских запросов между несколькими серверами. Его суть заключается в пересылке клиентских запросов всем серверам по очереди. Преимущество балансировки нагрузки на стороне клиента с Eureka заключается в том, что при остановке экземпляра службы он удаляется из реестра. Сразу после этого балансировщик нагрузки на стороне Обнаружение служб в облаке 199 клиента автоматически обновится благодаря постоянному обмену данными со службой реестра. Этот подход иллюстрирует рис. 6.3. 1. Когда клиенту требуется вызвать службу, он извлекает IP-адрес экземпляра службы из локального кеша. Балансировка нагрузки между экземплярами осуществляется внутри клиента 3. Периодически кеш на стороне клиента обновляется с помощью уровня обнаружения служб Клиентские приложения Кеширование и балансировка нагрузки на стороне клиента Кеширование и балансировка нагрузки на стороне клиента Уровень обнаружения служб Узел механизма обнаружения услуг 1 Узел механизма обнаружения услуг 2 Узел механизма обнаружения услуг 3 Контрольный сигнал Экземпляры службы 2. Если клиент найдет IP-адрес службы в кеше, он использует его, иначе обращается к механизму обнаружения служб Служба A x Рис. 6.3. Балансировщик нагрузки на стороне клиента кеширует адреса экземпляров, поэтому нет необходимости связываться с механизмом обнаружения служб при каждом обращении к службе В этой модели: a клиент связывается с механизмом обнаружения служб, получает от него адреса всех экземпляров нужной ему службы и кеширует их локально; b каждый раз, когда клиенту требуется вызвать службу, он извлекает информацию о ее местоположении из кеша. Обычно при этом используется простой алгоритм балансировки нагрузки, например циклического перебора, который гарантирует равномерное распределение вызовов между несколькими экземплярами; 200 Глава 6 Управление конфигурациями с использованием Spring Cloud... клиент периодически связывается с механизмом обнаружения и обновляет свой кеш. Несмотря на то что кеш на стороне клиента периодически приводится в согласованное состояние, всегда существует риск, что в какой-то момент клиент может отправить запрос неработающему экземпляру службы. Если вызов службы завершится неудачей, то локальный кеш объявляется недействительным и клиент обращается к агенту обнаружения служб, чтобы обновить его. Давайте теперь возьмем за основу общий шаблон обнаружения служб и применим его к нашей задаче реализации приложения O-stock. c 6.2.2.Обнаружение служб с использованием Spring и Netflix Eureka В этом разделе мы реализуем обнаружение служб, настроив соответствующего агента, а затем зарегистрируем в нем две службы. В этой реализации для вызова одной службы из другой мы будем использовать информацию, полученную от агента обнаружения служб. Spring Cloud предлагает несколько методов использования агента обнаружения служб. Мы рассмотрим сильные и слабые стороны каждого из них. Spring Cloud делает реализацию этой модели тривиально простой. Для реализации нашего шаблона мы используем Spring Cloud и движок Netflix Eureka Service Discovery, а для балансировки нагрузки на стороне клиента – Spring Cloud Load Balancer. ПРИМЕЧАНИЕ. В этой главе мы не будем использовать Ribbon – балансировщик нагрузки на стороне клиента, широко используемый для взаимодействий с REST-службами на основе Spring Cloud. Балансировка нагрузки на стороне клиента с использованием Netflix Ribbon прежде считалась стабильным решением, но в настоящее время развитие этого проекта, к сожалению, прекратилось, и осуществляется только его текущая поддержка. В этом разделе мы объясним, как использовать балансировщик Spring Cloud Load Balancer, пришедший на смену Ribbon. В настоящее время Spring Cloud Load Balancer продолжает активно разрабатываться, поэтому его возможности постоянно расширяются. В предыдущих двух главах мы старались сохранить нашу службу лицензий максимально простой и включили в нее управление информацией об организациях и лицензиях. В этой главе мы выделим управление информацией об организации в отдельную службу. На рис. 6.4 показана реализация кеширования микросервисов O-stock на стороне клиента с помощью Eureka. При обращении к службе лицензий та автоматически вызывает службу организаций для получения информации, связанной с указанным идентификатором организации. Обнаружение служб в облаке 201 3. Spring Cloud Load Balancer периодически обновляет свой кеш IP-адресов Служба лицензий Служба организаций Spring Cloud Load Balancer 2. Когда служба лицензий вызывает службу организаций, она использует балансировщик нагрузки, чтобы узнать, имеются ли в локальном кеше IP-адреса экземпляров этой службы Уровень обнаружения служб Eureka Eureka Eureka 1. На этапе запуска экземпляры службы регистрируют свои IP-адреса в Eureka Рис. 6.4. Реализуя кеширование на стороне клиента с использованием Eureka, можно уменьшить нагрузку на серверы Eureka и повысить стабильность клиента, если служба Eureka станет недоступной Фактическое местоположение службы организаций хранится в реестре обнаружения служб. В этом примере мы зарегистрируем два экземпляра этой службы, а затем будем использовать балансировку нагрузки на стороне клиента для кеширования реестра и равномерного использования всех экземпляров службы. Эта архитектура изображена на рис. 6.4. 1 На этапе запуска экземпляры служб лицензий и организаций регистрируются в службе Eureka и сообщают свои физические адреса и номера портов, а также идентификаторы запускаемых служб. 2 Когда службе лицензий требуется вызвать службу организаций, она использует Spring Cloud Load Balancer для балансировки нагрузки на стороне клиента. Балансировщик нагрузки в свою очередь связывается со службой Eureka для получения информации о местоположении искомой службы и помещает ее в локальный кеш. 3 Периодически Spring Cloud Load Balancer будет проверять связь со службой Eureka и обновлять локальный кеш с адресами служб. Информация о любом новом экземпляре службы организаций теперь будет доступна службе лицензий в ее локальном кеше, а ин- 202 Глава 6 Управление конфигурациями с использованием Spring Cloud... формация об экземплярах, прекративших работу, будет удаляться из кеша. Мы реализуем эту архитектуру, настроив нашу службу Spring Cloud Eureka. 6.3. Создание службы Spring Eureka В этом разделе мы создадим службу Eureka с помощью Spring Boot. По аналогии со службой Spring Cloud Config, чтобы получить службу Spring Cloud Eureka, нужно создать новый проект Spring Boot и применить необходимые аннотации и настройки. Давайте создадим проект с помощью Spring Initializr (https://start.spring.io/). Для этого выполните следующие шаги в Spring Initializr. 1 Выберите тип проекта Maven. 2 Выберите язык Java. 3 Выберите последнюю или стабильную версию Spring. 4 Введите в поле Group (Группа) название com.optimagrowth, а в поле Artifact (Артефакт) имя eurekaserver. 5 Раскройте список Options (Параметры) и введите Eureka Server в поле Name (Имя), Eureka server в поле Description (Описание) и com.optimagrowth.eureka в поле Package name (Имя пакета). 6 Выберите в поле Packaging (Упаковка) вариант JAR. 7 Выберите версию Java 11. 8 Добавьте зависимости Eureka Server, Config Client и Spring Boot Actuator, как показано на рис. 6.5. 9 В листинге 6.1 показано содержимое файла pom.xml с настройками Eureka Server. Рис. 6.5. Зависимости Eureka Server в Spring Initializr Создание службы Spring Eureka 203 Листинг 6.1. Файл pom с настройками для проекта Eureka Server <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.2.5.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.optimagrowth</groupId> <artifactId>eurekaserver</artifactId> <version>0.0.1-SNAPSHOT</version> <name>Eureka Server</name> <description>Eureka Server</description> <properties> <java.version>11</java.version> <spring-cloud.version>Hoxton.SR1</spring-cloud.version> </properties> Исключить библиотеки Netflix Ribbon <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> Подключить клиента, кото<dependency> рый юудет соединяться с <groupId>org.springframework.cloud Spring Config Server для получения настроек приложения </groupId> <artifactId>spring-cloud-starter-config</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server </artifactId> Подключить библиотеки Eureka <exclusions> <exclusion> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-ribbon</artifactId> </exclusion> <exclusion> <groupId>com.netflix.ribbon</groupId> <artifactId>ribbon-eureka</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> 204 Глава 6 Управление конфигурациями с использованием Spring Cloud... <artifactId>spring-cloud-starter-loadbalancer </artifactId> Подключить библиотеки Spring </dependency> Cloud Load Balancer <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> </dependencies> <!-- Остальная часть файла pom.xml опущена для краткости ... </project> Следующий шаг: создание файла src/main/resources/bootstrap. yml с настройками, которые понадобятся для получения конфигурации из службы Spring Config Server, созданной в главе 5. Нам также нужно запретить использовать Ribbon в качестве балансировщика нагрузки на стороне клиента по умолчанию. В листинге 6.2 показано, как должен выглядеть файл bootstrap.yml. Листинг 6.2. Файл bootstrap.yml с настройками Eureka spring: Имя службы Eureka, которое клиент application: Spring Cloud Config будет искать name: eureka-server cloud: Местоположение службы config: Spring Cloud Config Server uri: http://localhost:8071 loadbalancer: ribbon: enabled: false Ribbon до сих пор считается балансировщиком нагрузки на стороне клиента по умолчанию, поэтому его нужно отключить с помощью параметра loadbalancer.ribbon.enabled После добавления информации о Spring Configuration Server в файл bootstrap для Eureka Server и отключения Ribbon можно переходить к следующему шагу. На этом шаге мы добавим настройки, необходимые службе Eureka для работы в автономном режиме (когда в кластере нет других узлов со службой Eureka). Для этого создадим файл конфигурации Eureka Server в репозитории службы Spring Config. (Как вы наверняка помните, в роли репозитория можно использовать путь поиска классов, файловую систему, GIT или Vault.) Файл конфигурации должен иметь имя, как указано в свойстве spring.application.name в файле bootstrap.yml службы Eureka. Соответственно, мы создадим файл eureka-server.yml в ката- Создание службы Spring Eureka 205 логе configserver/src/main/resources/config/, находящийся в пути поиска классов. В листинге 6.3 показано содержимое этого файла. ПРИМЕЧАНИЕ. Если вы не следовали за примерами кода в главе 5, то можете загрузить код по ссылке: https://github.com/ ihuaylupo/manning-smia/tree/master/chapter5. Листинг 6.3. Настройки Eureka в Spring Config Server Порт, прослушиваемый server: сервером Eureka Server port: 8070 eureka: instance: Запретить серверу Config hostname: localhost Имя хоста, Server регистрироваться client: где нав службе Eureka ... registerWithEureka: false ходится fetchRegistry: false .… и не кешировать инфорэкземпляр serviceUrl: мацию из реестра Eureka defaultZone: URL http://${eureka.instance.hostname}:${server.port}/eureka/ службы server: Начальное время ожидания, прежде waitTimeInMsWhenSyncEmpty: 5 чем сервер начнет принимать запросы Ключевыми в листинге 6.3 являются следующие свойства: server.port – задает порт по умолчанию; eureka.instance.hostname – имя хоста, где размещается экземпляр службы Eureka; eureka.client.registerWithEureka – запрещает серверу Config Server регистрироваться в Eureka при запуске приложения Spring Boot Eureka; eureka.client.fetchRegistry – значение false запрещает службе Eureka кешировать информацию из реестра. Это значение следует изменить для служб Spring Boot, которые должны регистрироваться в Eureka; eureka.client.serviceUrl.defaultZone – URL службы для любого клиента. Это комбинация атрибутов eureka.instance.hostname и server.port; eureka.server.waitTimeInMsWhenSyncEmpty – время ожидания перед тем, как сервер начнет принимать запросы. Обратите внимание на последний атрибут eureka.server.waitTimeInMsWhenSyncEmpty в листинге 6.3. Он задает время ожидания в миллисекундах перед началом приема запросов. Этот параметр следует использовать при тестировании служб локально, потому что иначе служба Eureka не будет искать какие-либо службы, зарегистрированные в ней сразу после запуска. По умолчанию она ждет 5 мин, чтобы дать возможность всем службам зарегистрироваться, прежде чем возвращать информацию о них. Использование этого 206 Глава 6 Управление конфигурациями с использованием Spring Cloud... параметра для локального тестирования помогает сократить время, необходимое для запуска службы Eureka и отображения служб, зарегистрированных в ней. ПРИМЕЧАНИЕ. Отдельные службы, зарегистрированные в Eureka, отображаются в течение 30 с. Это связано с тем, что Eureka требует, чтобы служба прислала три контрольных сигнала с интервалом в 10 с, и только после этого она будет сообщать клиентам о доступности этой службы. Помните об этом при развертывании и тестировании собственных служб. Наконец, мы должны добавить аннотацию к классу инициализации приложения со службой Eureka. Этот класс – EurekaServerApplication – находится в файле src/main/java/com/optimagrowth/ eureka/EurekaServerApplication.java. В листинге 6.4 показано, куда добавить аннотации. Листинг 6.4. Аннотации поддержки Eureka Server для класса инициализации package com.optimagrowth.eureka; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; Разрешает приложению Eureka @SpringBootApplication Server работать в роли реестра служб Spring @EnableEurekaServer public class EurekaServerApplication { public static void main(String[] args) { SpringApplication.run(EurekaServerApplication.class, args); } } Здесь мы использовали только одну новую аннотацию, @EnableEurekaServer, которая позволяет нашей службе Eureka работать в ка- честве службы реестра. Теперь можно запустить службу Eureka командой mvn spring-boot:run или docker-compose. После этого у вас должна появиться служба Eureka без зарегистрированных в ней других служб. Но первой должна запускаться служба Spring Config, которая содержит конфигурацию приложения Eureka. Если этого не сделать, то вы получите следующее сообщение об ошибке: Connect Timeout Exception on Url - http://localhost:8071. Will be trying the next url if available. com.sun.jersey.api.client.ClientHandlerException: java.net.ConnectException: Connection refused (Connection refused) Чтобы избежать этой проблемы, запускайте службы с помощью Docker Compose. Обновленный файл docker-compose.yml вы найдете Регистрация служб в Spring Eureka 207 в репозитории с примерами исходного кода для этой главы в GitHub. Теперь перейдем к созданию службы организаций, а затем зарегистрируем службы лицензий и организаций в нашей службе Eureka. 6.4. Регистрация служб в Spring Eureka На данный момент у нас имеется действующий сервер Eureka Server. Далее в этом разделе мы настроим службы организаций и лицензирования для регистрации в службе Eureka Server. Это необходимо, чтобы клиенты могли находить их в реестре Eureka. К концу этого раздела вы получите полное представление о том, как зарегистрировать микросервис Spring Boot в Eureka. Регистрация микросервиса в Eureka – несложное упражнение. Мы не будем рассматривать весь код служб на Java (мы намеренно старались минимизировать объем кода) и вместо этого сосредоточимся на регистрации службы в реестре Eureka Server, созданном в предыдущем разделе. В этом разделе мы добавим новую службу – службу организаций. Она будет иметь свои конечные точки CRUD. Скачать код с реализацией служб лицензий и организаций можно по следующей ссылке: https://github.com/ihuaylupo/manning-smia/tree/master/ chapter6/Initial. ПРИМЕЧАНИЕ. Так же можете использовать другие микросервисы, которые могут быть у вас. Просто не забудьте поправить идентификаторы служб, когда будете регистрировать их в службе обнаружения. Первым делом добавим зависимость Spring Eureka в файлы pom. xml наших служб организаций и лицензий. В листинге 6.5 показано, как это сделать. Листинг 6.5. Добавление зависимости Spring Eureka в файл pom.xml службы организаций <dependency> <groupId>org.springframework.cloud</groupId> <artifactId> spring-cloud-starter-netflix-eureka-client </artifactId> </dependency> Подключить библиотеки Eureka, чтобы служба могла зарегистрировать себя в Eureka Артефакт spring-cloud-starter-netflix-eureka-client содержит файлы JAR, которые Spring Cloud использует для взаимодействия со службой Eureka. После изменения настроек в файле pom.xml нужно определить правильное значение в параметре spring.application. name в файле bootstrap.yml службы, которую мы будем регистрировать. В листингах 6.6 и 6.7 показано, как это сделать. 208 Глава 6 Управление конфигурациями с использованием Spring Cloud... Листинг 6.6. Добавление spring.application.name в настройки службы организаций spring: application: name: organization-service profiles: active: dev cloud: config: uri: http://localhost:8071 Логическое имя службы, которое будет зарегистрирован в Eureka Листинг 6.7. Добавление spring.application.name в настройки службы лицензий spring: application: name: licensing-service profiles: active: dev cloud: config: uri: http://localhost:8071 Логическое имя службы, которое будет зарегистрирован в Eureka Каждой службе, зарегистрированной в Eureka, соответствует два идентификатора: идентификатор приложения и идентификатор экземпляра. Идентификатор приложения представляет группу экземпляров службы. В микросервисах Spring Boot идентификатором приложения всегда является значение свойства spring. application.name. Мы проявили недюжинную творческую смекалку и присвоили этому свойству службы организаций значение organization-service, а свойству службы лицензий – licensing-service. Идентификатор экземпляра – это случайное число, генерируемое автоматически и представляющее один экземпляр службы. Далее нужно сообщить фреймворку Spring Boot, что он должен регистрировать экземпляры служб организаций и лицензий в Eureka. Для этого добавим дополнительные настройки в файлы конфигурации, которыми управляет служба Spring Config. В данном примере эти файлы находятся в каталоге проекта Spring Configuration Server. В листинге 6.8 показано, как зарегистрировать службы в Eureka. src/main/resources/config/organization-service.properties. src/main/resources/config/licensing-service.properties. ПРИМЕЧАНИЕ. Помните, что файл конфигурации может иметь формат файла свойств или YAML и находиться в пути поиска классов (classpath), файловой системе, репозитории Git или Vault. Это зависит от конфигурации Spring Config Server. В этом Регистрация служб в Spring Eureka 209 примере мы решили использовать путь поиска классов и формат файлов свойств, но вы можете использовать то, что лучше соответствует вашим потребностям. Листинг 6.8. Изменения в файлах application.properties для Eureka eureka.instance.preferIpAddress = true Регистрировать IP-адрес службы вместо имени сервера eureka.client.registerWithEureka = true Регистрировать службу в Eureka eureka.client.fetchRegistry = true Извлекать локальную копию реестра eureka.client.serviceUrl.defaultZone = http://localhost:8070/eureka/ Местоположение службы Eureka Если вы используете файл application.yml, то он должен выглядеть, как показано ниже. Свойство eureka.instance.preferIpAddress требует регистрировать IP-адрес службы вместо имени хоста. eureka: instance: preferIpAddress: true client: registerWithEureka: true fetchRegistry: true serviceUrl: defaultZone: http://localhost:8070/eureka/ Почему регистрация IP-адреса предпочтительнее? По умолчанию Eureka регистрирует имена хостов служб. Этот подход хорошо работает в окружениях, где службе назначается имя хоста, поддерживаемое системой DNS. Однако в контейнерных окружениях (например, в Docker) контейнеры получают случайно сгенерированные имена хостов, для которых нет соответствующих записей в DNS. Если не присвоить параметру eureka.instance.preferIpAddress значение true, то клиентские приложения не смогут правильно определять местоположение хостов. Значение true в атрибуте preferIpAddress сообщает службе Eureka, что службы должны регистрироваться по IP-адресам. Мы сами всегда присваиваем этому атрибуту значение true. Облачные микросервисы должны быть эфемерными и не иметь состояния. Их можно запускать и останавливать по желанию, поэтому IP-адреса лучше подходят для их обнаружения. Атрибут eureka.client.registerWithEureka сообщает о необходимости регистрации служб организаций и лицензий в Eureka. Атрибут eureka.client.fetchRegistry определяет, будет ли клиент Spring Eureka сохранять локальную копию реестра. Если присвоить ему значение true, то клиент будет кешировать реестр локально и использовать его для вызова других служб вместо обращения к Eureka. 210 Глава 6 Управление конфигурациями с использованием Spring Cloud... Каждые 30 с клиент будет повторно связываться со службой Eureka и обновлять свою копию реестра. ПРИМЕЧАНИЕ. По умолчанию эти два свойства получают значение true. Мы включили их в листинги только для иллюстрации. Код будет работать точно так же и без явной установки этих свойств в значение true. Последний атрибут, eureka.serviceUrl.defaultZone, содержит список экземпляров службы Eureka, разделенных запятыми, которые клиент может использовать для обнаружения служб. В данном примере мы будем использовать только один экземпляр службы Eureka. Также можно объявить все свойства в формате ключ/значение в файле bootstrap каждой службы. Идея состоит в том, чтобы делегировать конфигурацию службе Spring Config. Вот почему мы регистрируем все конфигурации в конфигурационном файле службы репозитория Spring Config. В настоящее время файлы bootstrap этих служб должны содержать только имя приложения, профиль (при необходимости) и URI конфигурации в Spring Cloud. Eureka и высокая доступность Для обеспечения высокой доступности недостаточно настроить несколько URL. Атрибут eureka.serviceUrl.defaultZone предоставляет лишь список служб Eureka, с которыми клиент может взаимодействовать. Вы должны также настроить службы Eureka для обмена содержимым их реестров между собой. Экземпляры службы Eureka обмениваеются данными друг с другом, используя модель одноранговой связи, в которой каждая служба Eureka должна быть настроена так, чтобы она знала, где находятся другие узлы в кластере. Обсуждение настройки кластера Eureka выходит за рамки этой книги. Но если вам интересна эта тема, то мы советуем обращаться за дополнительной информацией на веб-сайт проекта Spring Cloud: https://projects.spring.io/spring-cloud/spring-cloud.html#spring-cloudeureka-server. На данный момент у нас есть две службы, зарегистрированные в службе Eureka. Чтобы убедиться в этом, можно проверить содержимое реестра, использовав REST API или панель управления Eureka. В следующих разделах мы опишем оба способа. Регистрация служб в Spring Eureka 211 6.4.1. REST API Eureka Чтобы с помощью REST API получить список всех зарегистрированных экземпляров служб, выполните запрос GET к следующей конечной точке: http://<eureka service>:8070/eureka/apps/<APPID> Например, чтобы увидеть список экземпляров службы организаций, нужно выполнить запрос к конечной точке: http:// localhost:8070/eureka/apps/organization-service. На рис. 6.6 показано, как выглядит ответ. Ключ для поиска службы IP-адрес экземпляра службы организаций В настоящее время служба запущена и функционирует Рис. 6.6. REST API Eureka возвращает список экземпляров службы организаций. В ответе возвращаются IP-адреса экземпляров, зарегистрированных в Eureka, а также их состояние По умолчанию служба Eureka возвращает ответ в формате XML, однако она поддерживает также формат JSON. Чтобы включить ее, нужно передать в HTTP-заголовке Accept значение application/ json. Пример ответа в формате JSON показан на рис. 6.7. 212 Глава 6 Управление конфигурациями с использованием Spring Cloud... Передать в HTTP-заголовке Accept значение application/json, чтобы получить ответ в формате JSON Рис. 6.7. Ответ REST API Eureka в формате JSON 6.4.2. Панель управления Eureka После запуска службы Eureka можно открыть в браузере страницу с адресом http://localhost:8070 и получить доступ к панели управления Eureka. Панель управления Eureka позволяет увидеть состояние зарегистрированных служб. На рис. 6.8 показано, как выглядит панель управления Eureka. Теперь, когда мы зарегистрировали службы организаций и лицензий, давайте посмотрим, как можно использовать обнаружение служб для взаимодействий с этими службами. Регистрация служб в Spring Eureka Ключ для поиска службы 213 В настоящее время служба запущена и функционирует Рис. 6.8. Панель управления Eureka со списком зарегистрированных экземпляров служб организаций и лицензий Об Eureka и запуске служб: не теряйте терпения После регистрации службы Eureka ждет в течение 30 с получения трех контрольных сигналов, сообщающих о работоспособности, и только потом открывает доступ к службе. Эта особенность часто сбивает с толку начинающих разработчиков. Они думают, что Eureka не зарегистрировала их службу, пытаясь вызвать ее сразу же после запуска. С такой особенностью поведения легко столкнуться в наших примерах кода, работающих в среде Docker, потому что служба Eureka и прикладные службы (лицензий и организаций) запускаются одновременно. Имейте в виду, что сразу после запуска приложения вы можете получить ошибку 404 с сообщением о том, что службы не найдены, даже притом, что сами службы запущены. В этом случае подождите 30 с и повторите попытку. В промышленном окружении службы Eureka уже будут запущены. Если вы развертываете экземпляр существующей службу, то старые экземпляры по-прежнему будут доступны и готовы обрабатывать запросы. 214 Глава 6 Управление конфигурациями с использованием Spring Cloud... 6.5.Использование механизма обнаружения служб В этом разделе мы покажем, как сделать так, чтобы служба лицензий вызывала службу организаций, не зная где та находится. Служба лицензий будет определять физическое местонахождение службы организаций с помощью Eureka. Для полноты обсуждения мы рассмотрим три разные клиентские библиотеки Spring/Netflix, посредством которых клиент службы сможет взаимодействовать с балансировщиком Spring Cloud Load Balancer. Далее мы исследуем применение библиотек с разным уровнем абстракции для взаимодействий с Load Balancer. Вот эти библиотеки: Spring Discovery Client; шаблон REST с поддержкой Spring Discovery Client; клиент Netflix Feign. Мы пройдемся по всем этим клиентам и посмотрим, как их использовать в контексте службы лицензий. Но, прежде чем перейти к обсуждению особенностей клиентов, напишем несколько вспомогательных классов и методов. Они позволят вам поэкспериментировать с разными типами клиентов, используя одну и ту же конечную точку службы. Для начала изменим класс src/main/java/com/optimagrowth/license/ controller/LicenseController.java и добавим новый маршрут к службе лицензий. Этот новый маршрут позволяет выбрать тип клиента для вызова службы. Это вспомогательный маршрут, поэтому в процессе исследования разных методов вызова службы организаций через балансировщик нагрузки вы сможете опробовать каждый механизм, используя один и тот же маршрут. В листинге 6.9 показано определение нового маршрута в классе LicenseController. Листинг 6.9. Вызов службы лицензий с разными клиентами REST @RequestMapping(value="/{licenseId}/{clientType}", Параметр clientType задает тип используеmethod = RequestMethod.GET) мого клиента public License getLicensesWithClient( @PathVariable("organizationId") String organizationId, @PathVariable("licenseId") String licenseId, @PathVariable("clientType") String clientType) { return licenseService.getLicense(organizationId, licenseId, clientType); } В листинге 6.9 параметр clientType, переданный в маршруте, определяет тип клиента, который будет использоваться в примерах кода. Вот конкретные типы, которые можно передать этому маршруту: Использование механизма обнаружения служб 215 Discovery – требует использовать для вызова службы организаций клиента Discovery Client и стандартный класс Spring RestTemplate; Rest – требует использовать для вызова службы Load Balancer расширенный шаблон RestTemplate; Feign – требует использовать для вызова службы через Load Balancer клиентскую библиотеку Netflix Feign. ПРИМЕЧАНИЕ. Поскольку для опробования всех трех клиентов используется один и тот же код, вам могут встретиться в примерах аннотации, характерные для конкретных клиентов, которые могут показаться ненужными. Например, вы встретите аннотации @EnableDiscoveryClient и @EnableFeignClients, даже притом, что в тексте мы будем объяснять только один из типов клиентов. Это сделано для того, чтобы дать возможность использовать одну базу кода для примеров. Мы будем подчеркивать избыточность кода по мере необходимости, чтобы впоследствии вы могли осознанно выбрать тот вариант, который лучше соответствует вашим потребностям. В класс src/main/java/com/optimagrowth/license/service/ LicenseService.java мы добавили простой метод retrieveOrganizationInfo(), который использует параметр clientType, переданный в маршрут, для выбора клиента поиска экземпляра службы организаций. Метод getLicense() класса LicenseService использует retrieveOrganizationInfo() для извлечения информации об организации из базы данных Postgres. В листинге 6.10 показан код getLicense() в классе LicenseService. Листинг 6.10. Функция getLicense() использует разные методы для вызова службы REST public License getLicense(String licenseId, String organizationId, String clientType){ License license = licenseRepository.findByOrganizationIdAndLicenseId (organizationId, licenseId); if (null == license) { throw new IllegalArgumentException(String.format( messages.getMessage("license.search.error.message", null, null), licenseId, organizationId)); } Organization organization = retrieveOrganizationInfo(organizationId, clientType); if (null != organization) { license.setOrganizationName(organization.getName()); license.setContactName(organization.getContactName()); license.setContactEmail(organization.getContactEmail()); license.setContactPhone(organization.getContactPhone()); } return license.withComment(config.getExampleProperty()); } 216 Глава 6 Управление конфигурациями с использованием Spring Cloud... Всех клиентов, созданных нами с помощью Spring Discovery Client, класса RestTemplate или библиотек Feign, в пакете src/main/ java/com/optimagrowth/license/service/client службы лицензий. Чтобы вызвать getLicense() с разными клиентами, следует обратиться к следующей конечной точке GET: http://<licensing service Hostname/IP>:<licensing service Port>/v1/ organization/<organizationID>/license/<licenseID>/<client type( feign, discovery, rest)> 6.5.1. Поиск экземпляров служб с Spring Discovery Client Клиент Spring Discovery предлагает самый низкий уровень абстракции доступа к балансировщику нагрузки и зарегистрированным в нем службам. С его помощью можно извлекать все службы, зарегистрированные в клиенте Spring Cloud Load Balancer, и их URL. Далее мы рассмотрим простой пример использования Discovery Client для получения одного из URL службы организаций из балансировщика нагрузки и вызова службы с помощью стандартного класса RestTemplate. Чтобы использовать Discovery Client, сначала нужно добавить к классу src/main/java/com/optimagrowth/ license/LicenseServiceApplication.java аннотацию @EnableDiscoveryClient, как показано в листинге 6.11. Листинг 6.11. Подготовка класса инициализации для использования Eureka Discovery Client package com.optimagrowth.license; @SpringBootApplication @RefreshScope @EnableDiscoveryClient public class LicenseServiceApplication { Активирует Eureka Discovery Client public static void main(String[] args) { SpringApplication.run(LicenseServiceApplication.class, args); } } @EnableDiscoveryClient включает в поддержку библиотек Discovery Client и Spring Cloud Load Balancer. Теперь посмотрим, как выглядит код, вызывающий службу организаций через Spring Discovery Client. Он показан в листинге 6.12. Вы можете найти этот код в файле src/main/java/com/optimagrowth/license/service/client/ OrganizationDiscoveryClient.java. Использование механизма обнаружения служб 217 Листинг 6.12. Использование Discovery Client для поиска информации @Component public class OrganizationDiscoveryClient { @Autowired private DiscoveryClient discoveryClient; Внедряет Discovery Client в класс public Organization getOrganization(String organizationId) { RestTemplate restTemplate = new RestTemplate(); List<ServiceInstance> instances = discoveryClient.getInstances("organization-service"); Получает список if (instances.size()==0) return null; всех экземпляров службы организаций String serviceUri = String.format ("%s/v1/organization/%s", instances.get(0).getUri().toString(), organizationId); Получает конечную точку службы ResponseEntity<Organization> restExchange = Использует restTemplate.exchange( стандартserviceUri, HttpMethod.GET, ный класс null, Organization.class, organizationId); RestTemplate для вызова return restExchange.getBody(); службы } } Первый интересный момент, который хотелось бы отметить, – класс DiscoveryClient. Он используется для взаимодействий с балансировщиком Spring Cloud Load Balancer. Далее, чтобы получить все экземпляры службы организаций, зарегистрированные в Eureka, вызывается метод getInstances() с ключом искомой службы, который возвращает список объектов ServiceInstance. Каждый объект ServiceInstance содержит информацию о конкретном экземпляре службы, включая имя хоста, порт и URI. В листинге 6.12 мы берем первый экземпляр ServiceInstance из списка и создаем целевой URL для вызова службы. Имея целевой URL, мы можем вызвать службу и получить ответ с помощью стандартного класса RestTemplate. Discovery Client и реальная жизнь Discovery Client следует использовать, только когда службе нужно обратиться к балансировщику нагрузки, чтобы выяснить, какие службы и экземпляры зарегистрированы в нем. Код в листинге 6.12 имеет несколько проблем, в том числе: не использует преимущества балансировки нагрузки на стороне клиента. Вызывая Discovery Client напрямую, вы получаете список служб и должны сами решить, какой экземпляр вызывать; 218 Глава 6 Управление конфигурациями с использованием Spring Cloud... делает слишком много работы. Код должен создать URL, чтобы вызвать службу. Это мелочь, но, избавляясь от таких фрагментов кода, вы уменьшаете объем кода, который придется отлаживать. Наблюдательные разработчики с опытом использования Spring могли также заметить, что мы напрямую создали экземпляр класса RestTemplate. Это очень нетипично, потому что обычно фреймворк Spring позволяет внедрить экземпляр класса RestTemplate, предоставляя аннотацию @Autowired. Мы создали экземпляр класса RestTemplate в листинге 6.12 по той простой причине, что после активации клиента Discovery Client в классе приложения через @EnableDiscoveryClient все экземпляры RestTemplate, внедряемые фреймворком Spring, будут иметь перехватчик с поддержкой Load Balancer. Это меняет способ создания URL. Создание экземпляра RestTemplate напрямую позволяет избежать такого поведения. 6.5.2.Вызов служб с использованием шаблона Spring REST с поддержкой Load Balancer В этом разделе мы рассмотрим пример использования шаблона REST с поддержкой Load Balancer. Это один из наиболее распространенных механизмов взаимодействия с Load Balancer через Spring. Чтобы использовать класс RestTemplate с поддержкой Load Balancer, нужно определить bean-компонент RestTemplate с аннотацией Spring Cloud @LoadBalanced. Способ создания bean-компонента RestTemplate демонстрируется в классе LicenseServiceApplication в src/main/java/com/ optimagrowth/license/LicenseServiceApplication.java. В листинге 6.13 показан метод getRestTemplate(), создающий bean-компонент RestTemplate с поддержкой Load Balancer. Листинг 6.13. Аннотирование и определение метода конструирования RestTemplate // Большинство инструкций импортирования было удалено для краткости import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.context.annotation.Bean; import org.springframework.web.client.RestTemplate; @SpringBootApplication @RefreshScope public class LicenseServiceApplication { public static void main(String[] args) { SpringApplication.run(LicenseServiceApplication.class, args); } @LoadBalanced Получает список всех экземпляров службы организаций Использование механизма обнаружения служб 219 @Bean public RestTemplate getRestTemplate(){ return new RestTemplate(); } } Теперь, имея определение bean-компонента для класса RestTemplate, осталось только подключить его к классу, который будет использовать его для вызова службы. Использование класса RestTemplate во многом похоже на использование стандартного класса RestTemplate, за исключением одного небольшого отличия, заключающегося в определении URL целевой службы. Вместо физического местоположения службы в вызове RestTemplate нужно использовать целевой URL, полученный из идентификатора вызываемой службы, под которым она зарегистрирована в службе Eureka. Листинг 6.14 демонстрирует это. Этот код можно найти в файле src/main/java/com/optimagrowth/ license/service/client/OrganizationRestTemplateClient.java. Листинг 6.14. Использование RestTemplate с поддержкой Load Balancer // Объявление пакета и инструкции импорта опущены для краткости @Component public class OrganizationRestTemplateClient { @Autowired RestTemplate restTemplate; public Organization getOrganization(String organizationId){ ResponseEntity<Organization> restExchange = restTemplate.exchange( При использовании RestTemplate "http://organization-service/v1/ с поддержкой Load organization/{organizationId}", Balancer создает цеHttpMethod.GET, null, левой URL с идентиOrganization.class, organizationId); return restExchange.getBody(); фикатором искомой службы в Eureka } } Этот код выглядит почти так же, как в предыдущем примере, за исключением двух важных отличий. Во-первых, здесь нигде не видно клиента Spring Cloud Discovery, и во-вторых, URL, используемый в вызове restTemplate.exchange(), выглядит несколько необычно. Вот сам вызов: restTemplate.exchange( "http://organization-service/v1/organization/{organizationId}", HttpMethod.GET, null, Organization.class, organizationId); 220 Глава 6 Управление конфигурациями с использованием Spring Cloud... Имя сервера в URL совпадает с идентификатором приложения в ключе службы организаций, который использовался для ее регистрации в Eureka: http://{applicationid}/v1/organization/{organizationId} Класс RestTemplate с поддержкой Load Balancer анализирует переданный ему URL и использует ту его часть, что соответствует имени сервера, как ключ для запроса адреса экземпляра службы у балансировщика нагрузки. Фактическое местоположение и порт службы полностью не зависят от разработчика. Кроме того, класс RestTemplate с балансировщиком Spring Cloud Load Balancer автоматически выполняет циклическую балансировку всех запросов между всеми экземплярами службы. 6.5.3. Вызов служб с использованием Netflix Feign Альтернативой классу RestTemplate с поддержкой Spring Cloud Load Balancer является клиентская библиотека Netflix Feign. В этой библиотеке используется другой подход к вызову службы REST: разработчик определяет интерфейс Java, а затем использует аннотации Spring Cloud, чтобы указать, какую службу должен вызвать Spring Cloud Load Balancer, используя Eureka для определения ее местоположения. Фреймворк Spring Cloud динамически сгенерирует проксикласс для вызова целевой службы REST – разработчику не нужно писать код вызова службы, только определение интерфейса. Чтобы задействовать клиента Feign в нашей службе лицензий, добавим новую аннотацию @EnableFeignClients в файл src/main/ java/com/optimagrowth/license/LicenseServiceApplication.java, как показано в листинге 6.15. Листинг 6.15. Использование клиента Spring Cloud/Netflix Feign в службе лицензий @SpringBootApplication @EnableFeignClients public class LicenseServiceApplication { Эта аннотация необходима для использования клиента Feign public static void main(String[] args) { SpringApplication.run(LicenseServiceApplication.class, args); } } Теперь, добавив поддержку клиента Feign в нашу службу лицензий, посмотрим, как выглядит определение интерфейса клиента Feign, который можно использовать для вызова конечной точки службы организаций (см. листинг 6.16). Код из этого листинга вы найдете в файле класса src/main/java/com/optimagrowth/ license/service/client/OrganizationFeignClient.java. Использование механизма обнаружения служб 221 Листинг 6.16. Определение интерфейса Feign для вызова службы организаций // Объявление пакета и инструкции импорта опущены для краткости @FeignClient("organization-service") Определение службы в Feign public interface OrganizationFeignClient { @RequestMapping( Определение пути к конечmethod= RequestMethod.GET, ной точке и операции с ней value="/v1/organization/{organizationId}", consumes="application/json") Organization getOrganization (@PathVariable("organizationId") Определение параметров String organizationId); для передачи конечной точке } В листинге 6.16 мы использовали аннотацию @FeignClient с идентификатором приложения службы, которую должен представлять интерфейс. Затем определили метод getOrganization() в нашем интерфейсе, который может вызывать клиент для обращения к службе организаций. Определение метода getOrganization() выглядит точно так же, как определение конечной точки в классе контроллера Spring. Вопервых, мы добавили перед методом getOrganization() аннотацию @ RequestMapping, которая отображает HTTP-метод в конечную точку службы организаций. Во-вторых, мы отобразили идентификатор организации, полученный в URL, в параметр organizationId с помощью @PathVariable. Значение, возвращаемое службой организаций, автоматически отображается в класс Organization, который указан как тип значения, возвращаемого методом getOrganization(). Чтобы задействовать класс OrganizationFeignClient, нужно лишь внедрить его и использовать. Все остальное возьмет на себя клиент Feign. Об обработке ошибок При использовании стандартного класса Spring RestTemplate все HTTPкоды состояния возвращаются через метод getStatusCode() класса ResponseEntity. В клиенте Feign HTTP-коды состояния 4xx–5xx отображаются в исключение FeignException, содержащее тело в формате JSON, которое можно проанализировать и извлечь конкретное сообщение об ошибке. Библиотека Feign предоставляет возможность написать свой класс декодера ошибок, отображающий ошибки в пользовательский класс Exception. Мы не будем рассматривать особенности написания такого, но вы можете найти примеры этого в репозитории Feign GitHub: https://github.com/Netflix/feign/wiki/Custom-error-handling. 222 Глава 6 Управление конфигурациями с использованием Spring Cloud... Итоги Для абстрагирования физического местоположения служб используется шаблон обнаружения служб. Механизм обнаружения служб, такой как Eureka, способен добавлять и удалять экземпляры служб из окружения, не влияя на работу их клиентов. Балансировка нагрузки на стороне клиента может обеспечить дополнительную производительность и устойчивость за счет кеширования физического местоположения экземпляров службы. Eureka – это проект компании Netflix, который легко устанавливается и настраивается с использованием Spring Cloud. Для вызова служб можно использовать три разных механизма Spring Cloud и Netflix Eureka: Spring Cloud Discovery Client, RestTemplate с поддержкой Spring Cloud Load Balancer и клиента Feign Netflix. 7 Когда случаются неприятности: шаблоны устойчивости с использованием Spring Cloud и Resilience4j Эта глава: рассказывает о шаблонах размыкателя цепи (circuit breaker), резервной реализации (fallback) и герметичных отсеков (bulkheads); демонстрирует применение шаблона размыкателя цепи для предотвращения непроизводительного расходования ресурсов клиента; описывает обработку выхода из строя удаленных служб с использованием Resilience4j; обсуждает применение шаблона герметичных отсеков в Resilience4j для разделения вызовов к удаленным ресурсам; показывает, как настроить реализации шаблонов размыкателя цепи и герметичных отсеков в Resilience4j; описывает настройку стратегии конкуренции в Resilience4j. Во всех системах возникают сбои, в том числе и в распределенных. Реализация реакции на эти сбои в приложениях является важной частью работы каждого программиста. Однако, когда дело доходит до создания устойчивых систем, большинство инженеров-программистов принимают во внимание только полный отказ части инфраструктуры или критически важной службы и стремятся создать из- 224 Глава 7 Управление конфигурациями с использованием Spring Cloud... быточность на каждом уровне приложения, используя такие методы, как кластеризация ключевых серверов, балансировка нагрузки между службами и пространственное распределение инфраструктуры. Эти подходы помогают преодолеть проблемы, вызванные полной потерей компонента системы, но они относятся только к одной стороне широкой темы построения устойчивых систем. Когда служба терпит сбой, приложение может легко обнаружить ее недоступность и перенаправить трафик в обход нее. Однако, когда служба работает слишком медленно, обнаружить такую проблему падения производительности и перестроить маршруты чрезвычайно сложно. Вот несколько причин, объясняющих это: производительность службы сначала может снижаться незаметно, на короткие периоды времени, а затем моментально обрушиться. Также падение производительности службы может происходить небольшими всплесками. Первыми признаками сбоя могут служить периодические жалобы небольшой группы пользователей, пока внезапно контейнер приложения не исчерпает свой пул потоков и не обрушится полностью; вызовы удаленных служб обычно выполняются синхронно и не прерываются по тайм-ауту. Часто разработчик приложения вызывает службу для выполнения некоторого действия и ожидает ответа от нее, не предусматривая прерывание вызова по таймауту, чтобы не допустить зависания; приложения часто проектируются для преодоления полного отказа удаленных ресурсов, но редко предусматривают реакцию на частичную деградацию. Обычно, пока служба не дойдет до стадии полного отказа, приложение будет продолжать обращаться к деградирующей службе вместо быстрого отказа от ее использования. В этом случае вызывающее приложение или служба само может постепенно деградировать или, что более вероятно, обрушиться из-за исчерпания ресурсов. Исчерпание ресурсов наступает, когда потребление ограниченного ресурса, такого как пул потоков или соединений с базой данных, достигает максимума, и вызывающий клиент должен ждать, пока этот ресурс снова станет доступным. Коварство проблем, вызванных плохой работой удаленных служб состоит в том, что их не только трудно обнаружить, но и может возникнуть каскадный эффект, способствующий распространению проблемы на всю экосистему приложений. Если не принять соответствующих мер безопасности, одна плохо работающая служба может быстро привести к полному отказу нескольких приложений. Облачные приложения на основе микросервисов особенно уязвимы для подобных сбоев, потому что эти приложения состоят из большого количества небольших распределенных служб, участвующих в обработке транзакции пользователя. Шаблоны устойчивости на стороне клиента 225 Шаблоны устойчивости – один из наиболее важных аспектов архитектуры микросервисов. В этой главе мы разберем четыре шаблона устойчивости и посмотрим, как их реализовать с использованием Spring Cloud и Resilience4j в нашей службе лицензий, чтобы она могла быстро выйти из строя, если потребуется. 7.1. Шаблоны устойчивости на стороне клиента Шаблоны программирования, обеспечивающие устойчивость на стороне клиента, сосредоточены на защите клиента от сбоя, когда удаленный ресурс выходит из строя из-за ошибок или страдает низкой производительностью. Эти шаблоны позволяют клиенту быстро потерпеть неудачу и не расходовать понапрасну ценные ресурсы, такие как соединения с базой данных и пулы потоков выполнения. Они также помогают предотвратить распространение проблемы «вверх по течению» – на компоненты, использующие данного клиента. В этой главе мы рассмотрим четыре шаблона устойчивости клиентов. На рис. 7.1 показано их место между потребителем микросервиса и микросервисом. Веб-клиент Клиент службы кеширует конечные точки микросервисов, полученные обращением к механизму обнаружения служб Микросервис Балансировка нагрузки на стороне клиента Размыкатель цепи гарантирует, что клиент не будет повторно вызывать неисправную службу Размыкатель цепи Шаблон герметичных отсеков разделяет разные вызовы служб на клиенте, чтобы гарантировать, что сбой в службе не повлечет исчерпание ресурсов на клиенте Резервная реализация Герметичные отсеки Микросервис A Экземпляр 1 Когда вызов терпит неудачу, шаблон резервной реализации предоставляет альтернативу, которую можно выполнить Экземпляр 2 Микросервис B Экземпляр 1 Экземпляр 2 Каждый экземпляр микросервиса работает на отдельном сервере с собственным IP-адресом Рис. 7.1. Четыре шаблона устойчивости клиентов действуют как защитный буфер между службами и их потребителями 226 Глава 7 Управление конфигурациями с использованием Spring Cloud... Эти шаблоны (балансировка нагрузки на стороне клиента, размыкатель цепи, резервная реализация и герметичные отсеки) реализуются в клиенте (микросервисе), вызывающем удаленный ресурс. Логически реализации этих шаблонов находится между клиентом, потребляющим удаленные ресурсы, и самими ресурсами. Давайте потратим немного времени на более детальное знакомство с каждым шаблоном. 7.1.1. Балансировка нагрузки на стороне клиента Мы уже познакомились с шаблоном балансировки нагрузки на стороне клиента в предыдущей главе, когда говорили об обнаружении служб. Балансировка нагрузки на стороне клиента включает поиск клиентом всех отдельных экземпляров службы с помощью агента обнаружения (например, Netflix Eureka) и кеширование их физических адресов. Когда потребителю службы требуется вызвать экземпляр службы, то подсистема балансировки нагрузки на стороне клиента возвращает адрес из пула. Балансировщик нагрузки на стороне клиента находится между клиентом и службой, поэтому может определить, работает ли экземпляр службы и насколько правильно он работает. Обнаружив проблему, балансировщик нагрузки на стороне клиента может удалить этот экземпляр службы из пула и предотвратить повторное обращение к нему в будущем. Именно такое поведение предлагает библиотека Spring Cloud Load Balancer по умолчанию (без дополнительной настройки). Поскольку в главе 6 мы уже рассмотрели балансировку нагрузки на стороне клиента с помощью Spring Cloud Load Balancer, то не будем больше возвращаться к этой теме здесь. 7.1.2. Размыкатель цепи Шаблон размыкателя цепи (circuit breaker) действует подобно автоматическому выключателю в электрической цепи, который размыкает цепь, если обнаруживает, что через нее течет слишком большой ток. Обнаружив проблему, размыкатель цепи разрывает соединение с остальной электрической цепью и не позволяет «поджарить» потребителей электроэнергии, расположенных за ним. Программный размыкатель цепи наблюдает за отдельными вызовами удаленных служб. Если вызов длится слишком долго, размыкатель вмешивается и прерывает его. Также шаблон размыкателя цепи наблюдает за всеми вызовами к удаленным ресурсам, и если количество неудачных вызовов к определенному ресурсу, следующих подряд, превысит некоторый порог, то реализация шаблона «сработает», заставит клиента быстро потерпеть неудачу и предотвратит вызовы отказавшего ресурса в будущем. Шаблоны устойчивости на стороне клиента 227 7.1.3. Резервная реализация В шаблоне отката к резервной реализации (fallback), когда вызов удаленной службы завершается неудачей, вместо возбуждения исключения потребитель использует альтернативную реализацию и пытается выполнить запрошенное действие другими способами. Обычно это предусматривает поиск в других источниках данных или постановку запроса в очередь для последующей обработки. В ответе, возвращаемом пользователю, не сообщается о возникшем исключении, но его можно уведомить о том, что ответ на запрос следует проверить позже. Например, представьте сайт электронной коммерции, анализирующий поведение пользователей и возвращающий рекомендации по другим товарам, которые они могут пожелать купить. Как правило, для получения результатов анализа поведения пользователя в прошлом и списка рекомендаций для каждого конкретного пользователя вызывается микросервис. Однако если служба анализа предпочтений недоступна, то резервным вариантом может быть получение более общего списка предпочтений, основанного на всех покупках всех пользователей. Эти данные могут предоставляться совершенно другой службой и из другого источника данных. 7.1.4. Герметичные отсеки Шаблон герметичных отсеков (bulkheads) заимствован из практики постройки кораблей. Корабль делится на отсеки водонепроницаемыми и герметичными переборками. Даже если в корпусе кораб­ля образуется пробоина, переборки удержат воду в том отсеке, где находится пробоина, и не дадут всему кораблю наполниться водой и затонуть. Ту же идею можно применить к службе, которая должна взаимодействовать с несколькими удаленными ресурсами. При использовании шаблона герметичных отсеков вызовы удаленных ресурсов «герметизируются» в отдельных пулах потоков, благодаря чему уменьшается риск того, что проблема с вызовом одного медленного удаленного ресурса приведет к остановке всего приложения. Пулы потоков подобны герметичным отсекам вашей службы. Операции с каждым удаленным ресурсом отделяются друг от друга и связываются с разными пулами потоков. Если одна служба отвечает слишком медленно, то взаимодействия с ней могут привести к исчерпанию пула потоков и остановить обработку запросов клиента. Связывание служб с отдельными пулами потоков помогает обойти эту проблему и предотвратить исчерпание пулов потоков, связанных с другими службами. 228 Глава 7 Управление конфигурациями с использованием Spring Cloud... 7.2. Почему устойчивость клиента важна Выше мы коротко познакомились с некоторыми шаблонами устойчивости клиентов, а теперь давайте перейдем к конкретному примеру их применения. Рассмотрим типичный сценарий и постараемся понять, почему шаблоны устойчивости клиентов имеют большое значение для микросервисной архитектуры, работающей в облаке. На рис. 7.2 показан сценарий использования удаленных ресурсов, таких как база данных и удаленная служба. Этот сценарий не содержит никаких шаблонов устойчивости из тех, с которыми мы познакомились выше, и, соответственно, наглядно показывает, как вся архитектура (экосистема) может обрушиться из-за сбоя единственной службы. В сценарии на рис. 7.2 мы имеем три приложения, обменивающихся данными в той или иной форме с тремя разными службами. Приложения A и B напрямую взаимодействуют со службой лицензий. Служба лицензий извлекает данные из базы данных и вызывает службу организаций, чтобы та выполнила некоторую работу. Служба организаций извлекает данные из совершенно другой базы данных и обращается к третьей службе – сторонней облачной службе инвентаризации, которая использует внутреннее сетевое хранилище (Network Attached Storage, NAS) для записи данных в общую файловую систему. Приложение C напрямую вызывает службу инвентаризации. Перед выходными сетевой администратор внес небольшую (по его мнению) поправку в конфигурацию NAS. В тот момент эта поправка дала положительный результат, но в понедельник утром чтение из определенной дисковой подсистемы стало выполняться исключительно медленно. Разработчики, написавшие службу организаций, не ожидали такого замедления работы службы инвентаризации. Они написали свой код так, что запись в их базу данных и чтение из службы инвентаризации выполнялись в рамках одной транзакции. Когда производительность службы инвентаризации ухудшилась, это привело не только к торможению потоков, выполняющих запросы к службе инвентаризации, но и к исчерпанию пула соединений с базой данных в контейнере службы. Соединения оставались открытыми, потому что вызовы службы инвентаризации так и не были завершены. После этого стала ощущаться нехватка ресурсов в службе лицензий, потому что она вызывала службу организаций, которая работала медленно из-за службы инвентаризации. В конце концов все три приложения перестали отвечать на запросы, исчерпав ресурсы в ожидании завершения уже запущенных запросов. Такого развития событий можно было бы избежать, если бы в каждой точке, где используется удаленный ресурс (или база данных, или служба), был реализован шаблон размыкателя цепи. Почему устойчивость клиента важна Приложение C использует службу инвентаризации Приложения A и B используют службу лицензий Приложение A 229 Приложение B Приложение C Служба лицензий вызывает службу организаций Служба лицензий Облако Источник данных о лицензиях Служба организаций Служба инвентаризации Служба организаций вызывает службу инвентаризации Источник данных об организациях Здесь начинается самое интересное. Небольшое изменение в конфигурации NAS вызывает проблемы с производительностью службы инвентаризации. Бум! И все рушится NAS (записывает данные в общую файловую систему) Рис. 7.2. Приложение можно представить как граф взаимосвязанных зависимостей. Если не управлять удаленными вызовами между ними, то один плохо работающий удаленный ресурс может остановить все службы в графе Если бы вызов службы инвентаризации в сценарии на рис. 7.2 был реализован с использованием шаблона размыкателя цепи, тогда размыкатель обнаружил бы медленную работу этой службы и прервал вызов, сэкономив тем самым поток. Если представить, что служба организаций имеет несколько конечных точек, то эта неудача затронула бы только те конечные точки, которые взаимодействуют со службой инвентаризации. Остальные конечные точки службы организаций функционировали бы как обычно и смогли обслуживать запросы пользователей. Помните, что размыкатель цепи действует как посредник между приложением и удаленной службой. В сценарии на рис. 7.2 реализация размыкателя цепи могла бы защитить приложения A, B и C от полного сбоя. 230 Глава 7 Управление конфигурациями с использованием Spring Cloud... На рис. 7.3 представлен другой сценарий, в котором служба лицензий не вызывает напрямую службу организаций. Вместо этого она делегирует вызов размыкателю цепи, который принимает вызов и помещает его в поток (обычно из управляемого пула потоков), который не зависит от вызывающей службы. Теперь клиент больше не должен ждать завершения вызова. А размыкатель цепи, контролирующий поток, сможет прервать вызов, если он будет выполняться слишком долго. Размыкатель цепи до появления проблемы Размыкатель цепи после появления проблемы Приложение A Приложение B Приложение C Служба лицензий Служба лицензий Служба лицензий Успешный вызов Терпит неудачу и использует альтернативную реализацию Быстрый отказ Размыкатель цепи Служба организаций Служба инвентаризации Размыкатель цепи x Размыкатель цепи Частичная деградация. Служба лицензий немедленно получает сообщение об ошибке Автоматическое восстановление. Периодически пытается выполнить один из последующих запросов Служба организаций Служба организаций Служба инвентаризации Служба инвентаризации Рис. 7.3. Размыкатель цепи срабатывает и быстро завершает вызов некорректно работающей службы На рис. 7.3 показаны три сценария. В первом сценарии успешного вызова размыкатель цепи поддерживает таймер, и если вызов удаленной службы завершается до того, как истечет таймер, то служба лицензий продолжит работу как обычно. Во втором сценарии частичной деградации служба лицензий вызывает службу организаций через размыкатель цепи. Однако на этот раз служба организаций работает слишком медленно и размыкатель цепи разрывает соединение с удаленной службой по истечении установленного времени ожидания. После этого размыкатель возвращает службе лицензий ошибку. В этом сценарии ресурсы службы лицензий (поток выполнения или пул соединений) не будут расходоваться на ожидание завершения вызова службы организаций дольше установленного тайм-аута. Если служба организаций перестает успевать отвечать вовремя, то размыкатель цепи начинает подсчитывать количество отказов, Почему устойчивость клиента важна 231 и, когда в течение определенного времени накопится достаточно ошибок, размыкатель «разомкнет» цепь, и все вызовы, посылаемые службе организаций, будут быстро завершаться сбоем без фактического обращения к ней. В третьем сценарии размыкатель цепи знает о проблеме и, не дожидаясь тайм-аута, сразу сообщает службе лицензий об ошибке. Она может просто потерпеть неудачу или использовать альтернативный (резервный) вариант. Служба организаций в свою очередь получает время на восстановление, потому что перестает получать вызовы после срабатывания размыкателя. Такой подход помогает предотвратить каскадный сбой, который может произойти при отказе отдельных служб. Размыкатель цепи время от времени пропускает вызовы в неисправную службу. Если несколько вызовов подряд завершаются успехом, то размыкатель сбрасывается в исходное состояние. Вот основные преимущества, которые дает шаблон размыкателя цепи: быстрый отказ. Когда удаленная служба начинает испытывать проблемы, приложение быстро терпит неудачу, что предотвращает проблему исчерпания ресурсов, которая обычно приводит к полному отказу всего приложения. В большинстве ситуаций лучше потерпеть частичный сбой, чем полный отказ; постепенная деградация. Благодаря тайм-ауту и быстрому отказу шаблон размыкателя цепи дает возможность обработать сбой или использовать альтернативные механизмы для удовлетворения намерений пользователя. Например, если пользователь пытается получить данные из одного источника данных и в этом источнике данных происходит сбой, то наши службы могут попытаться получить необходимые данные из другого места; автоматическое восстановление. Размыкатель цепи, действующий как посредник, может периодически проверять доступность запрашиваемого ресурса и вновь открывать доступ к нему без постороннего вмешательства. В большом облачном приложении с сотнями служб такое автоматическое восстановление имеет решающее значение, потому что может значительно сократить время, необходимое для восстановления работоспособности службы, а также снижает риск того, что «уставший» оператор или инженер вызовет еще больше проблем, пытаясь перезапустить отказавшую службу вручную. До появления Resilience4j мы использовали Hystrix – одну из самых популярных библиотек Java для реализации шаблонов устойчивости в микросервисах. Так как в настоящее время развитие Hystrix прекратилось, то взамен нее рекомендуется использовать библиотеку Resilience4j. Это основная причина, по которой мы выбрали ее для демонстрации приемов реализации шаблонов устой- 232 Глава 7 Управление конфигурациями с использованием Spring Cloud... чивости в этой главе. Resilience4j дает нам аналогичные (и некоторые дополнительные) преимущества. 7.3. Реализация с Resilience4j Resilience4j – это библиотека реализаций шаблонов отказоустойчивости, созданная по образу и подобию Hystrix. Она предлагает следующие шаблоны для использования в наших службах: размыкатель цепи (circuit breaker) – прекращает отправку запросов при сбое вызываемой службы; повторные попытки (retry) – повторяет попытки обратиться к службе, что позволяет определить момент, когда та восстановит работоспособность; герметичные отсеки (bulkhead) – ограничивает количество конкурирующих исходящих запросов, чтобы избежать перегрузки; ограничитель частоты (rate limit) – ограничивает количество принимаемых вызовов; откат к резервной реализации (fallback) – устанавливает альтернативные пути выполнения на случай сбоя запросов. Resilience4j позволяет применить сразу несколько шаблонов к одному и тому же вызову метода, для чего достаточно снабдить этот метод аннотациями. Например, чтобы ограничить количество исходящих вызовов с помощью шаблонов герметичных отсеков и размыкателя цепи, достаточно декорировать метод аннотациями @CircuitBreaker и @Bulkhead. Важно отметить, что шаблон повторных попыток Retry из Resilience4j должен применяться в следующем порядке: Retry ( CircuitBreaker ( RateLimiter ( TimeLimiter ( Bulkhead ( Function ) ) ➥ ) ) ) Шаблон Retry применяется последним. Об этом следует помнить при его использовании в комбинации с другими шаблонами. Шаблоны могут также применяться по отдельности. Для реализации шаблонов размыкателя цепи, повторных попыток, ограничения частоты, отката к резервной реализации и герметичных отсеков требуется глубоко понимать, как действуют потоки выполнения и как управлять ими. Чтобы получить высоко­ качественные реализации этих шаблонов, требуется выполнить колоссальный объем работы. К счастью, мы можем использовать Spring Boot и библиотеку Resilience4j, предлагающие проверенные на практике инструменты, которые ежедневно используются многими микросервисными архитектурами. В следующих нескольких разделах мы расскажем, как: Подготовка службы лицензий к использованию Spring Cloud и Resilience4j 233 настроить файл сборки Maven (pom.xml) для службы лицензий, чтобы подключить к проекту обертки Spring Boot/ Resilience4j; использовать аннотации Spring Boot/Resilience4j для декорирования вызовов удаленных служб реализациями шаблонов размыкателя цепи, повторных попыток, ограничения частоты и герметичных отсеков; настроить отдельные размыкатель цепи, чтобы использовать отдельные тайм-ауты для каждого вызова; организовать использование резервной реализации, когда размыкатель прервет вызов или вызов потерпит неудачу; использовать раздельные пулы потоков, чтобы изолировать вызовы к разным службам и создать герметичные перегородки между разными удаленными ресурсами. 7.4.Подготовка службы лицензий к использованию Spring Cloud и Resilience4j Прежде чем приступить к исследованию возможностей библиотеки Resilience4j, нужно настроить файл сборки проекта pom.xml, добавив в него подключение необходимых зависимостей. Рассмотрим, как это сделать, на примере службу лицензий, изменив ее файл pom.xml и добавив в него зависимости для Resilience4j. В листинге 7.1 показано, как это сделать. Листинг 7.1. Добавление зависимости Resilience4j в файл pom.xml службы лицензий <properties> ... <resilience4j.version>1.5.0</resilience4j.version> </properties> <dependencies> // Часть файла pom.xml опущена для краткости ... <dependency> <groupId>io.github.resilience4j</groupId> <artifactId>resilience4j-spring-boot2</artifactId> <version>${resilience4j.version}</version> </dependency> <dependency> <groupId>io.github.resilience4j</groupId> <artifactId>resilience4j-circuitbreaker</artifactId> <version>${resilience4j.version}</version> </dependency> <dependency> <groupId>io.github.resilience4j</groupId> 234 Глава 7 Управление конфигурациями с использованием Spring Cloud... <artifactId>resilience4j-timelimiter</artifactId> <version>${resilience4j.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> // Остальная часть файла pom.xml опущена для краткости ... </dependencies> Тег <dependency> с артефактом resilience4j-spring-boot2 требует от Maven загрузить библиотеку Resilience4j Spring Boot с реализациями аннотаций шаблонов. Артефакты resilience4j-circuitbreaker и resilience4j-timelimiter содержат логику реализации размыкателя цепи и ограничителя частоты. Последняя зависимость, объявленная нами, – это spring-boot-starter-aop. Эта библиотека нужна нам для запуска аспектов Spring AOP. Аспектно-ориентированное программирование (АОП) – это парадигма, направленная на повышение модульности. Она позволяет отделять части программы, влияющие на другие части системы; иначе говоря, сквозные задачи. АОП добавляет новые варианты поведения в существующий код службы лицензий без изменения самого кода. Теперь, добавив необходимые зависимости, можно приступать к реализации шаблонов устойчивости Resilience4j для служб лицензий и организаций, созданных нами в предыдущих главах. ПРИМЕЧАНИЕ. Если вы не следовали за примерами кода в предыдущей главе, то можете загрузить исходный код по адресу https:// github.com/ihuaylupo/manning-smia/tree/master/chapter6/Final. 7.5. Реализация размыкателя цепи Чтобы понять, как работает размыкатель цепи, представьте электрическую цепь. Что происходит, когда по проводам электрической цепи течет слишком большой ток? Как уже говорилось, автоматический выключатель обнаруживает проблему и разрывает цепь, предотвращая повреждение других компонентов. То же самое происходит и в нашей архитектуре. С помощью размыкателей мы будем наблюдать за вызовами удаленных служб и предотвращать длительное ожидание ответа. В этих сценариях размыкатель цепи отвечает за разрыв соединений и проверяет, не слишком ли много ошибок накопилось. В последнем случае шаблон организует быстрый сбой и препятствует отправке последующих запросов к неисправному удаленному ресурсу. В Resilience4j размыкатель цепи реализован как конечный автомат с тремя нор- Реализация размыкателя цепи 235 мальными состояниями. На рис. 7.4 показаны разные состояния этого автомата и как осуществляются переходы между ними. Частота сбоев выше порога РАЗОМКНУТО в ое сб а та рог сто по Ча иже н Сп ус тя за нек де о рж тор Ча ку ую с ни тот же а с по бое ро в га ЗАМКНУТО ПОЛУОТКРЫТО Рис. 7.4. Состояния размыкателя цепи в Resilience4j: замкнуто, разомкнуто и полуоткрыто Первоначально размыкатель цепи Resilience4j находится в замкнутом состоянии и ожидает запросов от клиентов. В этом состоянии он использует кольцевой битовый буфер для хранения признаков успеха или неудачи запросов. Когда запрос выполняется успешно, в буфер записывается нулевой бит, а если ответ не приходит до истечения тайм-аута, в буфер записывается единичный бит. На рис. 7.5 показан кольцевой буфер с 12 результатами. Чтобы вычислить частоту отказов, кольцевой буфер должен быть заполнен. Например, в предыдущем сценарии необходимо выполнить не менее 12 вызовов, прежде чем можно будет рассчитать частоту отказов. Если оценивать только 11 запросов, то размыкатель не перейдет в разомкнутое состояние, даже если все 11 запросов потерпят неудачу. Обратите внимание, что автоматический выключатель размыкается, только когда частота отказов превышает определенный порог. 1 1 0 0 0 1 Кольцевой битовый буфер 0 1 0 0 1 1 Рис. 7.5. Кольцевой битовый буфер размыкателя цепи в Resilience4j с 12 результатами. Нулевые значения в буфере соответствуют успешно выполненным запросам, а 1 – запросам, прерванным из-за истечения тайм-аута 236 Глава 7 Управление конфигурациями с использованием Spring Cloud... Когда размыкатель находится в разомкнутом состоянии, в течение заданного времени все вызовы отклоняются и вызывающий клиент получает исключение CallNotPermittedException. По истечении установленного времени размыкатель переходит в полуоткрытое состояние и пропускает несколько запросов, чтобы узнать – восстановилась ли работоспособность вызываемой службы. В полуоткрытом состоянии размыкатель использует другой кольцевой битовый буфер для оценки частоты отказов. Если новая частота отказов окажется выше заданного порога, то размыкатель возвращается в разомкнутое состояние; если ниже или равна порогу, то происходит переход в замкнутое состояние. Это описание может несколько сбивать с толку, поэтому просто запомните, что в разомкнутом состоянии размыкатель отклоняет, а в замкнутом состоянии принимает все запросы. Кроме того, в шаблоне размыкателя цепи в Resilience4j можно определить дополнительные состояния, но имейте в виду, что выход из этих состояний возможен только через сброс размыкателя в исходное состояние или принудительный переход в другое состояние: ВЫКЛЮЧЕНО (DISABLED) – размыкатель всегда замкнут и пропускает все запросы; ПРИНУДИТЕЛЬНО РАЗОМКНУТ (FORCED_OPEN) – размыкатель всегда разомкнут и отклоняет все запросы. ПРИМЕЧАНИЕ. Мы не будем вдаваться в детали этих двух дополнительных состояний. Желающим узнать больше об этих состояниях мы рекомендуем обратиться к официальной документации Resilience4j по адресу https://resilience4j.readme.io/v0.17.0/ docs/circuitbreaker. В этом разделе мы рассмотрим два примера применения реализации шаблона размыкателя цепи в Resilience4j. В первом примере будем передавать через размыкатель все обращения к нашей базе данных из служб лицензий и организаций, а во втором добавим размыкатель для обработки вызовов между самими службами. Это две совершенно разные категории вызовов, но, как мы увидим далее, с помощью Resilience4j они обрабатываются совершенно одинаково. На рис. 7.6 показаны удаленные ресурсы, доступом к которым мы будем управлять с помощью размыкателя цепи Resilience4j. Реализация размыкателя цепи Приложение A Первая категория вызовов: все обращения к базе данных 237 Приложение B Служба лицензий Resilience4j чение Извле да нных Вызов другой службы Вторая категория вызовов: вызовы других служб База данных службы лицензий Служба организаций Resilience4j Извлечение данных База данных службы организаций Рис. 7.6. Размыкатель цепи помещается перед всеми удаленными ресурсами и защищает клиента. Тип удаленного ресурса – база данных или служба REST – не имеет значения Начнем обсуждение Resilience4j с демонстрации применения шаблона размыкателя для обработки запросов к базе данных, отправляемых службой лицензий. Служба лицензий извлекает данные в синхронном режиме и ожидает завершения оператора SQL или истечения тайм-аута в размыкателе цепи. Resilience4j и Spring Cloud предлагают аннотацию @CircuitBreaker для декорирования методов Java, управляемых размыкателем цепи Resilience4j. Встретив эту аннотацию, фреймворк Spring динамически генерирует прокси-класс, обертывающий метод и управляю- 238 Глава 7 Управление конфигурациями с использованием Spring Cloud... щий всеми его вызовами через пул потоков, специально выделенный для выполнения удаленных вызовов. Давайте добавим @CircuitBreaker к методу getLicensesByOrganization в файле класса src/main/ java/com/optimagrowth/license/service/LicenseService.java, как показано в листинге 7.2. Листинг 7.2. Обертывание вызовов удаленного ресурса шаблоном размыкателя цепи // Часть файла LicenseService.java опущена для краткости @CircuitBreaker(name = "licenseService") @CircuitBreaker обертывает метод getLicensesByOrganization() реализацией шаблона размыкателя цепи из Resilience4j public List<License> getLicensesByOrganization(String organizationId) { return licenseRepository.findByOrganizationId(organizationId); } ПРИМЕЧАНИЕ. Если вы посмотрите на код в репозитории, то увидите еще несколько параметров в аннотации @CircuitBreaker. Мы рассмотрим эти параметры далее в этой главе. Код в листинге 7.2 использует @CircuitBreaker значения по умолчанию для всех этих параметров. Как видите, нам потребовалось добавить совсем немного кода, но в этой аннотации скрыто множество функциональных возможностей. При использовании аннотации @CircuitBreaker каждый вызов метода getLicensesByOrganization() обертывается вызовом размыкателя цепи в Resilience4j, который автоматически прерывает любые неудачные попытки вызвать метод getLicensesByOrganization(). Этот пример кода мог бы показаться ничем не примечательным, если бы база данных работала безупречно. Но давайте смоделируем задержку обработки запроса, посылаемого методом getLicensesByOrganization(), как показано в листинге 7.3. Листинг 7.3. Намеренное увеличение времени обработки запроса к базе данных лицензий // Часть файла LicenseService.java опущена для краткости private void randomlyRunLong(){ Выполнит задержку вызова базы Random rand = new Random(); данных с вероятностью 1/3 int randomNum = rand.nextInt(3) + 1; if (randomNum==3) sleep(); } private void sleep(){ try { Thread.sleep(5000); Приостанавливает выполнение на 5000 мс (5 с) и затем генерирует исключение TimeoutException Реализация размыкателя цепи 239 throw new java.util.concurrent.TimeoutException(); } catch (InterruptedException e) { logger.error(e.getMessage()); } } @CircuitBreaker(name = "licenseService") public List<License> getLicensesByOrganization(String organizationId) { randomlyRunLong(); return licenseRepository.findByOrganizationId(organizationId); } Если теперь ввести в Postman адрес конечной точки http:// localhost:8080/v1/organization/e6a625cc-718b-48c2-ac761dfdff9a531e/license/ достаточное количество раз, то можно увидеть следующее сообщение об ошибке, возвращаемое службой лицензий: { "timestamp": 1595178498383, "status": 500, "error": "Internal Server Error", "message": "No message available", "path": "/v1/organization/e6a625cc-718b-48c2-ac76-1dfdff9a531e/license/" } Если продолжить вызывать отказавшую службу, то кольцевой битовый буфер в конечном итоге заполнится, и должна появиться ошибка, показанная на рис. 7.7. Рис. 7.7. Сообщение об ошибке указывает, что размыкатель цепи перешел в разомкнутое состояние Теперь, когда мы получили действующий размыкатель цепи, продолжим и добавим этот же шаблон для обработки вызовов микросервиса организаций. 240 Глава 7 Управление конфигурациями с использованием Spring Cloud... 7.5.1.Добавление размыкателя цепи для обработки вызовов службы организаций Вся прелесть аннотаций на уровне методов, добавляющих поведение размыкателя цепи, состоит в том, что это одна и та же аннотация, не зависящая от того, обращаемся ли мы к базе данных или вызываем микросервис. Например, службе лицензий требуется получить название организации, связанной с лицензией. При желании мы можем добавить размыкатель цепи для обработки запросов к службе организаций, для чего достаточно лишь выделить вызов RestTemplate в отдельный метод и декорировать его аннотацией @CircuitBreaker: @CircuitBreaker(name = "organizationService") private Organization getOrganization(String organizationId) { return organizationRestClient.getOrganization(organizationId); } ПРИМЕЧАНИЕ. Несмотря на простоту аннотации @CircuitBreaker, необходимо проявлять осторожность в использовании значений по умолчанию для параметров этой аннотации. Мы настоятельно рекомендуем всегда анализировать и настраивать параметры в соответствии с вашими потребностями. Чтобы просмотреть значения по умолчанию параметров аннотации @CircuitBreaker, обратитесь в Postman по URL http:// localhost:<порт_службы>/actator/health. Служба проверки работоспособности в Spring Boot Actuator вернет параметры по умолчанию размыкателя цепи. 7.5.2. Настройка размыкателя цепи В этом разделе мы ответим на один из наиболее распространенных вопросов, которые задают разработчики при использовании Resilience4j: как настроить размыкатель цепи в Resilience4j? Это легко сделать, добавив параметры в файл application.yml, boostrap. yml или в конфигурационный файл службы в репозитории Spring Config Server. В листинге 7.4 показаны настройки шаблона размыкателя цепи в boostrap.yml для обеих служб – лицензий и организаций. Листинг 7.4. Настройки размыкателя цепи Конфигурация экземпляра службы лицензий (имя, данное размыкателю цепи в аннотации) // Часть файла bootstrap.yml опущена для краткости resilience4j.circuitbreaker: instances: licenseService: registerHealthIndicator: true Следует ли экспортировать конфигурацию через конечную точку health Использование резервной реализации Размер кольцевого буфера для замкнутого состояния Исключения, которые должны расцениваться как сбои 241 ringBufferSizeInClosedState: 5 Размер кольцевого буфера для полуоткрытого состояния ringBufferSizeInHalfOpenState: 3 waitDurationInOpenState: 10s Продолжительность ожидания в разомкнутом состоянии failureRateThreshold: 50 recordExceptions: Порог частоты отказов в процентах - org.springframework.web.client.HttpServerErrorException java.io.IOException java.util.concurrent.TimeoutException org.springframework.web.client.ResourceAccessException organizationService: registerHealthIndicator: true ringBufferSizeInClosedState: 6 ringBufferSizeInHalfOpenState: 4 waitDurationInOpenState: 20s failureRateThreshold: 60 Конфигурация экземпляра службы организаций (имя, данное размыкателю цепи в аннотации) Resilience4j позволяет настраивать поведение размыкателя цепи в файле свойств приложения. Мы можем настроить столько экземпляров, сколько захотим, и каждый экземпляр может иметь свои уникальные настройки. В листинге 7.4 показаны следующие параметры конфигурации: ringBufferSizeInClosedState – определяет размер кольцевого битового буфера для замкнутого состояния размыкателя. Значение по умолчанию – 100; ringBufferSizeInHalfOpenState – определяет размер кольцевого битового буфера для полуоткрытого состояния размыкателя. Значение по умолчанию – 10; waitDurationInOpenState – время, которое размыкатель должен ждать перед переходом из разомкнутого состояния в полуоткрытое. Значение по умолчанию – 60 000 мс; failureRateThreshold – порог частоты отказов в процентах. Когда частота отказов оказывается больше или равна этому порогу, размыкатель переходит в разомкнутое состояние и начинает отвергать вызовы. Значение по умолчанию – 50; recordExceptions – список исключений, которые будут считаться сбоями. По умолчанию сбоями считаются все исключения. В этой книге мы не будем рассматривать все параметры реализации размыкателя цепи в Resilience4j. Желающим узнать больше об имеющихся параметрах конфигурации мы рекомендуем обратиться к документации по ссылке: https://resilience4j.readme.io/docs/circuitbreaker. 7.6. Использование резервной реализации Еще одна интересная возможность, которой обладает размыкатель цепи, обусловлена его промежуточным положением между потребителем удаленного ресурса и самим ресурсом. Эта возмож- 242 Глава 7 Управление конфигурациями с использованием Spring Cloud... ность позволяет перехватить сбой вызова удаленной службы и выполнить альтернативные операции. В Resilience4j это называется стратегией использования резервной реализации (fallback). Давайте посмотрим, как организовать выполнение альтернативной реализации на примере нашей службы лицензий, которая возвращает объект с информацией о лицензии, сообщающий, что в данный момент информация о лицензии недоступна (см. листинг 7.5). Листинг 7.5. Реализация резервной стратегии с использованием Resilience4j // Часть файла LicenseService.java опущена для краткости Определяет, какую @CircuitBreaker(name= "licenseService", функцию вызвать в случае сбоя fallbackMethod= "buildFallbackLicenseList") public List<License> getLicensesByOrganization( String organizationId) throws TimeoutException { logger.debug("getLicensesByOrganization Correlation id: {}", UserContextHolder.getContext().getCorrelationId()); randomlyRunLong(); return licenseRepository.findByOrganizationId(organizationId); } private List<License> buildFallbackLicenseList(String organizationId, ➥ Throwable t){ Возвращает List<License> fallbackList = new ArrayList<>(); жестко заданное License license = new License(); значение license.setLicenseId("0000000-00-00000"); license.setOrganizationId(organizationId); license.setProductName( "Sorry no licensing information currently available"); fallbackList.add(license); return fallbackList; } Для воплощения стратегии использования резервной реализации с Resilience4j прежде всего нужно добавить в @CircuitBreaker или любую другую аннотацию (мы объясним этот момент позже) атрибут fallbackMethod с именем метода, который должен вызываться в случае сбоя. Второе, что нужно сделать, – определить этот метод. Он должен находиться в том же классе, что и метод с аннотацией @CircuitBreaker. Метод с резервной реализацией должен иметь ту же сигнатуру, что и исходный метод, плюс один дополнительный параметр для передачи целевого исключения. Благодаря сходству сигнатур в резервный метод можно передать все параметры, полученные исходным методом. Использование резервной реализации 243 В примере, приведенном в листинге 7.5, резервный метод buildFallbackLicenseList() просто создает объект License с фиктивной информацией. Однако мы могли бы организовать в нем чтение данных из альтернативного источника, но, так как мы лишь демонстрируем возможность, наш метод просто создает объект, который может вернуть исходный метод. О резервных реализациях Вот несколько аспектов, о которых следует помнить, принимая решение об использовании стратегии резервной реализации: резервная реализация должна предусматривать действия для выполнения, когда срок ожидания ответа ресурса истек или он вернул ошибку. Если резервная реализация перехватывает исключение тайм-аута и просто регистрирует ошибки в журнале, то вместо этой стратегии лучше вызывать службу в стандартном блоке try ... catch; учитывайте действия, выполняемые в резервной реализации. Если в резервной реализации вы предусматриваете вызов другой удаленной службы, то вам может потребоваться добавить аннотацию @CircuitBreaker к своему резервному методу. Помните, что тот же сбой, с которым вы столкнулись в основной последовательности действий, может возникнуть и в резервной реализации. Используйте приемы защитного программирования. Теперь, добавив резервный метод, попробуем вновь вызвать нашу конечную точку. На этот раз, многократно отправляя запрос в Postman и сталкиваясь с ошибкой тайм-аута (напомним, что эта ошибка возникает с вероятностью 1/3), мы должны вместо исключения получать объект с фиктивной лицензией, как показано на рис. 7.8. Этот результат вернула резервная реализация Рис. 7.8. Вызов службы завершился вызовом резервной реализации 244 Глава 7 Управление конфигурациями с использованием Spring Cloud... 7.7. Реализация шаблона герметичных отсеков В приложениях на основе микросервисов часто требуется вызывать несколько микросервисов для решения определенной задачи. Без использования шаблона герметичных отсеков эти вызовы будут выполняться с использованием одних и тех же потоков, зарезервированных для обработки запросов во всем контейнере Java. При большом количестве запросов проблемы с производительностью в какой-нибудь одной службе могут привести к исчерпанию пула потоков в контейнере Java. Если при этом постоянно будут поступать все новые и новые запросы, то в конечном итоге контейнер Java выйдет из строя. Шаблон герметичных отсеков разделяет вызовы удаленных ресурсов по отдельным пулам потоков, чтобы изолировать одну некорректно работающую службу и не обрушить весь контейнер. Библиотека Resilience4j предоставляет две реализации шаблона герметичных отсеков, которые можно использовать для ограничения количества одновременно выполняющихся запросов: изоляцию с использованием семафоров – ограничивает количество одновременно выполняющихся запросов к службе. По достижении предела эта реализация начинает отклонять запросы; изоляцию с использованием пула потоков – ограничивает очередь и размер пула потоков. Эта реализация отклоняет запросы только по заполнении пула и очереди. По умолчанию Resilience4j применяет изоляцию с использованием семафоров. Принцип действия этой реализации показан на рис. 7.9. Эта модель отлично работает в случаях, когда имеется небольшое количество удаленных ресурсов или служб и вызовы к ним (относительно) равномерно распределены во времени. Однако если среди этих ресурсов или служб есть такие, которые вызываются намного чаще или обрабатывают запросы намного дольше других, то есть риск исчерпать пул потоков, потому что одна служба будет преобладать перед другими и в какой-то момент для работы с ней будут задействованы все потоки в пуле. К счастью, Resilience4j предлагает простой механизм разделения вызовов различных удаленных ресурсов с использованием пулов потоков. На рис. 7.10 показано, как выглядит такое разделение. Реализация шаблона герметичных отсеков 245 Отклоняет запросы по достижении предела Семафор Ограничивает количество одновременных запросов к службе Службы Рис. 7.9. По умолчанию Resilience4j использует изоляцию с использованием семафоров Вызов ресурса Группа потоков A Служба A Вызов ресурса Группа потоков B База данных B Вызов ресурса Группа потоков C Служба C Вызовы удаленных ресурсов выполняются с использованием изолированных пулов потоков. Каждый пул имеет ограниченное количество потоков, которые можно использовать для обработки запросов Служба с низкой производительностью влияет только на вызовы других служб, которые выполняются с использованием того же пула потоков, что ограничивает ущерб, который может быть нанесен такой службой Рис. 7.10. Вызовы разных ресурсов выполняются с использованием изолированных потоков Чтобы реализовать шаблон герметичных отсеков с Resilience4j, нужно использовать аннотацию @CircuitBreaker с дополнительными настройками. Давайте рассмотрим решение, которое: настраивает отдельный пул потоков для вызова getLicensesByOrganization(); определяет настройки в файле bootstrap.yml; 246 Глава 7 Управление конфигурациями с использованием Spring Cloud... при изоляции с использованием семафоров устанавливает ограничения maxConcurrentCalls и maxWaitDuration; при изоляции с использованием пулов потоков устанавливает ограничения maxThreadPoolSize, coreThreadPoolSize, queueCapacity и keepAliveDuration. В листинге 7.6 показано содержимое файла bootstrap.yml с соответствующими настройками для службы лицензий. Листинг 7.6. Настройки шаблона герметичных отсеков для службы лицензий // Часть файла boostrap.yml опущена для краткости resilience4j.bulkhead: instances: bulkheadLicenseService: maxWaitDuration: 10ms maxConcurrentCalls: 20 Максимальная продолжительность блокировки потока Максимальное количество одновременных вызовов resilience4j.thread-pool-bulkhead: Максимальное количество instances: потоков в пуле bulkheadLicenseService: Размер основного maxThreadPoolSize: 1 пула потоков coreThreadPoolSize: 1 queueCapacity: 1 Вместимость очереди keepAliveDuration: 20ms Максимальное время, в течение которого простаивающие потоки ждут новых заданий перед завершением Библиотека Resilience4j позволяет также настраивать поведение шаблонов герметичных отсеков в файле свойств приложения. Так же как в случае с шаблоном размыкателя цепи, можно создать столько экземпляров герметичных отсеков, сколько понадобится, и каждый экземпляр может иметь свои настройки. В листинге 7.6 определены следующие свойства: maxWaitDuration – максимальное время блокировки потока в герметичном отсеке. Значение по умолчанию – 0; maxConcurrentCalls – максимальное количество одновременных вызовов, разрешенное в герметичном отсеке. Значение по умолчанию – 25; maxThreadPoolSize – максимальный размер пула потоков. Значение по умолчанию – Runtime.getRuntime().AvailableProcessors(); coreThreadPoolSize – базовый размер пула потоков. Значение по умолчанию – Runtime.getRuntime().AvailableProcessors(); queueCapacity – вместимость очереди. Значение по умолчанию – 100; Реализация шаблона герметичных отсеков 247 KeepAliveDuration – максимальное время, в течение которого простаивающие потоки ждут новых заданий перед завершением. Учитывается, когда количество потоков превышает базовый размер пула. Значение по умолчанию – 20 мс. Как правильно выбрать размер пула потоков? Для ответа на этот вопрос можно использовать следующую формулу: (пиковое количество запросов в секунду, когда служба исправна × 99-й процентиль задержки в секундах) + небольшое количество дополнительных потоков для учета накладных расходов Часто характеристики производительности службы остаются неизвестными, пока она не начнет работать с полной нагрузкой. Ключевым признаком, помогающим определить, когда пришла пора скорректировать настройки пула потоков, является время ожидания службы, даже если целевой удаленный ресурс исправен. В листинге 7.7 показаны настройки герметичного отсека, изолирующего все вызовы, связанные с поиском данных о лицензиях в нашей службе лицензий. Листинг 7.7. Создание герметичного отсека для метода getLicensesByOrganization() // Часть файла LicenseService.java опущена для краткости @CircuitBreaker(name= "licenseService", fallbackMethod= "buildFallbackLicenseList") @Bulkhead(name= "bulkheadLicenseService", fallbackMethod= "buildFallbackLicenseList") Имя экземпляра и резервный метод для шаблона герметичных отсеков public List<License> getLicensesByOrganization( String organizationId) throws TimeoutException { logger.debug("getLicensesByOrganization Correlation id: {}", UserContextHolder.getContext().getCorrelationId()); randomlyRunLong(); return licenseRepository.findByOrganizationId(organizationId); } Первое, на что следует обратить внимание, – это новая аннотация @Bulkhead. Она создает экземпляр герметичного отсека. Если не опре- делить настройки явно в свойствах приложения, то Resilience4j будет использовать значения по умолчанию, упомянутые выше при обсуждении приема изоляции с использованием семафоров. Второе, что хотелось бы отметить в листинге 7.7, – это отсутствие настройки типа герметичного отсека. В данном случае используется изоляция на основе семафоров. Чтобы выбрать реализацию с использованием пулов потоков, нужно добавить в аннотацию @Bulkhead параметр type, как показано ниже: @Bulkhead(name = "bulkheadLicenseService", type = Bulkhead.Type.THREADPOOL, ➥ fallbackMethod = "buildFallbackLicenseList") 248 Глава 7 Управление конфигурациями с использованием Spring Cloud... 7.8. Реализация шаблона повторных попыток Как следует из названия, шаблон повторных попыток (retry) реализует повторные попытки связаться со службой, если вызов не увенчался успехом. Ключевая идея, лежащая в основе этого шаблона, – дать возможность получить ответ, попытавшись вызвать службу еще один или несколько раз, несмотря на сбой (например, сбой в сети). При настройке этого шаблона нужно указать количество повторных попыток связаться с данным экземпляром службы и интервал времени между каждой повторной попыткой. По аналогии с шаблоном размыкателя цепи библиотека Resilience4j позволяет указать, после каких исключений можно выполнить повторную попытку, а после каких – нет. В листинге 7.8 показано содержимое файла bootstrap.yml для службы лицензий с настройками шаблона повторных попыток. Листинг 7.8. Настройки шаблона повторных попыток в bootstrap.yml // Часть файла boostrap.yml опущена для краткости resilience4j.retry: Максимальное количество повторных попыток instances: Время ожидания между попытками retryLicenseService: maxRetryAttempts: 5 Список исключений, после которых waitDuration: 10000 можно выполнять повторные попытки retry-exceptions: - java.util.concurrent.TimeoutException Первый параметр, maxRetryAttempts, задает максимальное количество повторных попыток для данной службы. Значение по умолчанию – 3. Второй параметр, waitDuration, задает продолжительность ожидания между попытками. Значение по умолчанию – 500 мс. Третий параметр, retry-exceptions, задает список классов исключений, после которых допускается выполнять повторные попытки. Значение по умолчанию – пустой список. В этой книге мы используем только эти три параметра, но вы также можете настроить: intervalFunction – функция, вычисляющая время ожидания после сбоя; retryOnResultPredicate – предикат, оценивающий по полученному результату необходимость выполнения повторной попытки. Этот предикат должен возвращать true, если необходимо повторить попытку; retryOnExceptionPredicate – предикат, оценивающий по полученному исключению необходимость выполнения повторной попытки. Этот предикат должен возвращать true, если необходимо повторить попытку; Реализация шаблона ограничителя частоты 249 ignoreExceptions – задает список классов исключений, после которых не должны выполняться повторные попытки. Значение по умолчанию – пустой список. В листинге 7.9 показаны настройки шаблона повторных попыток для всех вызовов, связанных с поиском данных о лицензиях в нашей службе лицензий. Листинг 7.9. Добавление поддержки повторных попыток для метода getLicensesByOrganization() // Часть файла LicenseService.java опущена для краткости @CircuitBreaker(name= "licenseService", fallbackMethod="buildFallbackLicenseList") @Retry(name = "retryLicenseService", fallbackMethod="buildFallbackLicenseList") @Bulkhead(name= "bulkheadLicenseService", fallbackMethod="buildFallbackLicenseList") Имя экземпляра и резервный метод для шаблона повторных попыток public List<License> getLicensesByOrganization(String organizationId) throws TimeoutException { logger.debug("getLicensesByOrganization Correlation id: {}", UserContextHolder.getContext().getCorrelationId()); randomlyRunLong(); return licenseRepository.findByOrganizationId(organizationId); } Теперь, узнав, как реализовать шаблоны размыкателя цепи и повторных попыток, перейдем к ограничителю частоты. Как уже говорилось, Resilience4j позволяет применять несколько шаблонов к вызовам одного и того же метода. 7.9. Реализация шаблона ограничителя частоты Шаблон ограничителя частоты (rate limiter) не позволяет послать службе запросов больше, чем она в состоянии обработать за заданный интервал времени. Это императивный метод поддержки высокой доступности и надежности API. ПРИМЕЧАНИЕ. В современных облачных архитектурах рекомендуется включать автоматическое масштабирование, но мы не рассматриваем эту тему в этой книге. Resilience4j предлагает две реализации шаблона ограничителя частоты: AtomicRateLimiter и SemaphoreBasedRateLimiter. По умолчанию используется AtomicRateLimiter. 250 Глава 7 Управление конфигурациями с использованием Spring Cloud... SemaphoreBasedRateLimiter – самая простая реализация, основанная на использовании одного экземпляра java.util.concurrent.Semaphore для хранения текущих разрешений. При использовании этой реализации все пользовательские потоки должны вызывать метод semaphore.tryAcquire, чтобы инициировать вызов дополнительного внутреннего потока, выполняющего semaphore.release в начале нового периода обновления разрешений limitRefreshPeriod. В отличие от SemaphoreBasedRate, реализация AtomicRateLimiter не требует управления потоками, потому что вся логика разрешений выполнятся в пользовательских потоках. AtomicRateLimiter разбивает все наносекунды от начала периода на циклы, и продолжительность каждого цикла принимается за период обновления (в наносекундах). В начале каждого цикла мы должны установить активные разрешения для периода. Чтобы лучше понять суть этого подхода, взгляните на следующие настройки: ActiveCycle – номер цикла, использованный последним вызовом; ActivePermissions – количество доступных разрешений после последнего вызова; NanosToWait – количество наносекунд, потраченных на ожидание разрешения в последнем вызове. Эта реализация содержит хитрую логику. Чтобы разобраться в ней, рассмотрим следующие правила, устанавливаемые этим шаблоном: все циклы равны; если доступных разрешений недостаточно, то есть возможность зарезервировать дополнительные разрешения, уменьшив текущее и вычислив время, когда появится достаточное количество разрешений. В Resilience4j эта возможность поддерживается с использованием параметра с количеством вызовов, разрешенных в течение периода времени (limitForPeriod); периода обновления разрешений (limitRefreshPeriod) и как долго поток может ждать получения разрешений (timeoutDuration). При использовании этого шаблона нужно указать продолжительность тайм-аута, количество разрешений и продолжительность периода. В листинге 7.10 показано содержимое файла bootstrap.yml с соответствующими настройками шаблона ограничителя частоты для службы лицензий. Листинг 7.10. Настройки шаблона ограничителя частоты в bootstrap. yml // Часть файла boostrap.yml опущена для краткости resilience4j.ratelimiter: instances: Реализация шаблона ограничителя частоты Период обновления ограничений licenseService: timeoutDuration: 1000ms limitRefreshPeriod: 5000 limitForPeriod: 5 251 Время, в течение которого поток ожидает разрешения Количество разрешений, доступных в течение периода обновления ограничений Первый параметр, timeoutDuration, задает время, в течение которого поток ожидает разрешения; значение по умолчанию – 5 с (секунд). Второй параметр, limitRefreshPeriod, задает период обновления разрешений. После каждого периода ограничитель скорости сбрасывает счетчик разрешений до значения limitForPeriod. Значение по умолчанию для limitRefreshPeriod – 500 нс (наносекунд). Последний параметр, limitForPeriod, задает количество разрешений, доступных в течение одного периода обновления. Значение по умолчанию – 50. В листинге 7.11 показаны настройки шаблона ограничителя частоты для всех вызовов, связанных с поиском данных о лицензиях в нашей службе лицензий. Листинг 7.11. Добавление поддержки ограничения частоты для метода getLicensesByOrganization() // Часть файла LicenseService.java опущена для краткости Имя экземпляра @CircuitBreaker(name= "licenseService", и резервный меfallbackMethod= "buildFallbackLicenseList") тод для шаблона ограничителя @RateLimiter(name = "licenseService", частоты fallbackMethod = "buildFallbackLicenseList") @Retry(name = "retryLicenseService", fallbackMethod = "buildFallbackLicenseList") @Bulkhead(name= "bulkheadLicenseService", fallbackMethod= "buildFallbackLicenseList") public List<License> getLicensesByOrganization(String organizationId) throws TimeoutException { logger.debug("getLicensesByOrganization Correlation id: {}", UserContextHolder.getContext().getCorrelationId()); randomlyRunLong(); return licenseRepository.findByOrganizationId(organizationId); } Основное различие между шаблонами герметичных отсеков и ограничителя частоты заключается в том, что шаблон герметичных отсеков ограничивает количество одновременных вызовов (т. е. позволяет выполнять одновременно не более X вызовов), тогда как шаблон ограничителя частоты ограничивает общее количество вызовов в заданный период времени (т. е. позволяет выполнять X вызовов каждые Y с). Чтобы выбрать правильный шаблон, еще раз вспомните, что вам нужно. Если вы хотите ограничить конкуренцию, то используйте шаблон герметичных переборок, а если требуется ограничить общее количество вызовов в единицу времени, то используй- 252 Глава 7 Управление конфигурациями с использованием Spring Cloud... те шаблон ограничителя частоты. Если требуется ограничить и то и другое, то используйте комбинацию из этих двух шаблонов. 7.10. ThreadLocal и Resilience4j В этом разделе мы определим некоторые значения в ThreadLocal, чтобы узнать, доступны ли они в методах, декорированных аннотациями Resilience4J. Как вы наверняка помните, ThreadLocal в Java позволяет создавать переменные, доступные для чтения и изменения только в одном потоке. При работе с потоками все потоки конкретного объекта совместно используют его переменные, что делает их небезопасными. Самый распространенный способ добиться безопасности в многопоточном окружении – использовать синхронизацию. Но если желательно избежать накладных расходов на синхронизацию, то можно использовать переменные ThreadLocal. Рассмотрим конкретный пример. Часто в окружениях REST требуется определить некоторую контекстную информацию, например идентификатор корреляции или токен аутентификации для передачи в HTTP-заголовке, которая затем может использоваться во всех последующих вызовах службы. Идентификатор корреляции позволяет задать уникальный признак, который можно отслеживать в нескольких вызовах службы, выполняемых в рамках одной транзакции. Чтобы обеспечить доступность этого значения в любом месте в вызове службы, можно использовать класс Spring Filter и с его помощью перехватывать все вызовы нашей службы REST. Перехватив вызов, можно получить нужную информацию из входящего HTTP-запроса, сохранить ее в объекте UserContext, а затем, когда нашему коду потребуется извлечь эту информацию в вызове службы REST, можно получить UserContext из переменной в хранилище ThreadLocal и прочитать требуемое значение. В листинге 7.12 показан пример фильтра Spring, который можно использовать в нашей службе лицензий. ПРИМЕЧАНИЕ. Этот код доступен в файле /licensing-service/src/ main/java/com/optimagrowth/license/utils/UserContextFilter. java в примерах для главы 7: https://github.com/ihuaylupo/ manning-smia/tree/master/chapter7. Листинг 7.12. Парсинг HTTP-заголовка и извлечение данных в UserContextFilter package com.optimagrowth.license.utils; ... // Инструкции импорта опущены для краткости ThreadLocal и Resilience4j 253 @Component public class UserContextFilter implements Filter { private static final Logger logger = LoggerFactory.getLogger(UserContextFilter.class); @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest; UserContextHolder.getContext().setCorrelationId( httpServletRequest.getHeader( UserContext.CORRELATION_ID)); UserContextHolder.getContext().setUserId( httpServletRequest.getHeader( UserContext.USER_ID)); UserContextHolder.getContext().setAuthToken( httpServletRequest.getHeader( UserContext.AUTH_TOKEN)); UserContextHolder.getContext().setOrganizationId( httpServletRequest.getHeader( UserContext.ORGANIZATION_ID)); Извлекает значения из HTTPзаголовка, записывает их в объект UserContext, который затем сохраняется в UserContextHolder filterChain.doFilter(httpServletRequest, servletResponse); } ... // остальная часть файла UserContextFilter.java опущена для краткости } Класс UserContextHolder сохраняет UserContext в классе ThreadLocal. После этого любой код, выполняемый в процессе запроса, будет использовать объект UserContext, хранящийся в UserContextHolder. В листинге 7.13 показан класс UserContextHolder. Это определение вы найдете в файле класса /licensing-service/src/main/java/com/ optimagrowth/license/utils/UserContextHolder.java. Листинг 7.13. Все данные UserContext управляются классом UserContextHolder ... // Инструкции импорта опущены для краткости Сохраняет UserContext в статической переменной ThreadLocal public class UserContextHolder { private static final ThreadLocal<UserContext> userContext = new ThreadLocal<UserContext>(); public static final UserContext getContext(){ UserContext context = userContext.get(); Извлекает объект UserContext для дальнейшего использования 254 Глава 7 Управление конфигурациями с использованием Spring Cloud... if (context == null) { context = createEmptyContext(); userContext.set(context); } return userContext.get(); } public static final void setContext(UserContext context) { userContext.set(context); } public static final UserContext createEmptyContext(){ return new UserContext(); } } ПРИМЕЧАНИЕ. Будьте внимательны, работая непосредственно с ThreadLocal. Ошибки внутри ThreadLocal могут привести к утечке памяти в приложении. UserContext – это класс POJO, содержащий конкретные данные, которые нужно сохранить в UserContextHolder. В листинге 7.14 по- казано определение этого класса, которое можно найти в файле /licensing-service/src/main/java/com/optimagrowth/license/ utils/UserContext.java. Листинг 7.14. Класс UserContext ... // Инструкции импорта опущены для краткости @Component public class UserContext { public static final String public static final String public static final String public static final String private private private private String String String String CORRELATION_ID AUTH_TOKEN USER_ID ORGANIZATION_ID correlationId authToken userId organizationId = = = = new new new new = = = = "tmx-correlation-id"; "tmx-auth-token"; "tmx-user-id"; "tmx-organization-id"; String(); String(); String(); String(); public String getCorrelationId() { return correlationId;} public void setCorrelationId(String correlationId) { this.correlationId = correlationId; } public String getAuthToken() { return authToken; } ThreadLocal и Resilience4j 255 public void setAuthToken(String authToken) { this.authToken = authToken; } public String getUserId() { return userId; } public void setUserId(String userId) { this.userId = userId; } public String getOrganizationId() { return organizationId; } public void setOrganizationId(String organizationId) { this.organizationId = organizationId; } } Последний шаг, который нужно сделать для завершения примера, – добавить журналирование в класс LicenseController.java, который находится в файле com/optimagrowth/license/controller/ LicenseController.java. В листинге 7.15 показано, как это сделать. Листинг 7.15. Добавление журналирования в метод LicenseController.getLicenses() // Часть кода опущена для краткости import org.slf4j.Logger; import org.slf4j.LoggerFactory; @RestController @RequestMapping(value="v1/organization/{organizationId}/license") public class LicenseController { private static final Logger logger = LoggerFactory.getLogger(LicenseController.class); // Часть кода опущена для краткости @RequestMapping(value="/",method = RequestMethod.GET) public List<License> getLicenses( @PathVariable("organizationId") String organizationId) { logger.debug("LicenseServiceController Correlation id: {}", UserContextHolder.getContext().getCorrelationId()); return licenseService.getLicensesByOrganization(organizationId); } } Теперь в нашей службе лицензий имеется поддержка журналирования. Мы уже добавили журналирование в следующие классы и методы службы лицензий: 256 Глава 7 Управление конфигурациями с использованием Spring Cloud... doFilter() в com/optimagrowth/license/utils/ UserContextFilter.java; getLicenses() в com/optimagrowth/license/controller/ LicenseController.java; getLicensesByOrganization() в com/optimagrowth/license/ service/LicenseService.java. Этот метод декорирован аннотациями @CircuitBreaker, @Retry, @Bulkhead и @RateLimiter. Чтобы опробовать пример, вызовем нашу службу, передав ей идентификатор корреляции в HTTP-заголовке tmx-correlation-id со значением TEST-CORRELATION-ID. На рис. 7.11 показаны настройки HTTP-вызова GET в Postman. Рис. 7.11. Добавление идентификатора корреляции в HTTP-заголовок для передачи службе лицензий После отправки этого запроса в консоли должны появиться три журнальных сообщения, содержащих идентификатор корреляции, передаваемый через классы UserContext, LicenseController и LicenseService: UserContextFilter Correlation id: TEST-CORRELATION-ID LicenseServiceController Correlation id: TEST-CORRELATION-ID LicenseService:getLicensesByOrganization Correlation id: Если у вас в консоли не появились эти сообщения, то добавьте строки, показанные в листинге 7.16, в файл application.yml или application.properties с настройками службы лицензий. Листинг 7.16. Настройки для поддержки журналирования в службе лицензий в application.yml // Часть кода опущена для краткости logging: level: org.springframework.web: WARN com.optimagrowth: DEBUG ThreadLocal и Resilience4j 257 Затем снова соберите и запустите микросервисы. Если вы используете Docker, то просто выполните следующие команды в корневом каталоге проекта, где находится родительский pom.xml: mvn clean package dockerfile:build docker-compose -f docker/docker-compose.yml up Как видите, во всех методах, защищенных шаблонами устойчивости, доступен один и тот же идентификатор корреляции. То есть значения из родительского потока доступны в методах, использующих аннотации Resilience4j. Resilience4j – отличный выбор для реализации шаблонов устойчивости в приложениях. С завершением развития Hystrix библиотека Resilience4j стала выбором номер один в экосистеме Java. Теперь, узнав, чего можно достичь с помощью Resilience4j, можно перейти к нашей следующей теме – Spring Cloud Gateway. Итоги При проектировании высокораспределенных приложений, таких как микросервисы, необходимо обеспечивать устойчивость клиентов. Явные сбои службы (например, крах сервера) легко обнаружить и устранить. Единственная плохо работающая служба может вызвать каскадный эффект исчерпания ресурсов из-за того, что потоки в вызывающем клиенте будут блокироваться в ожидании ответа службы. К трем основным шаблонам устойчивости клиентов относятся: шаблон размыкателя цепи, шаблон резервной реализации и шаблон герметичных отсеков. Шаблон размыкателя цепи (circuit breaker) предназначен для прерывания медленно выполняющихся вызовов, быстрого завершения их с признаком ошибки и предотвращения исчерпания ресурсов. Шаблон отката к резервной реализации (fallback) позволяет определить альтернативные пути на случай сбоя вызова удаленной службы или отказа размыкателя цепи выполнить вызов. Шаблон герметичных отсеков (bulkhead) отделяет вызовы удаленных ресурсов друг от друга, изолируя их в собственных пулах потоков. Сбой одной из служб не должен приводить к исчерпанию всех ресурсов контейнера приложения. Шаблон ограничителя скорости (rate limiter) ограничивает количество вызовов, которое можно выполнить в единицу времени. Resilience4j позволяет составлять произвольные комбинации из шаблонов. Шаблон повторных попыток (retry) обеспечивает выполнение повторных попыток, когда служба по каким-то причинам может оказаться недоступной. Основное различие между шаблонами герметичных отсеков и ограничителя частоты заключается в том, что шаблон герметичных отсеков ограничивает количество одновременных вызовов, тогда как шаблон ограничителя частоты ограничивает общее количество вызовов в заданный период времени. Библиотеки Spring Cloud и Resilince4j предоставляют готовые реализации шаблонов размыкателя цепи, отката к резервной реализации, повторных попыток, ограничителя частоты и герметичных отсеков. Библиотека Resilience4j легко настраивается и может использоваться на глобальном уровне, на уровне класса и на уровне пула потоков. 8 Маршрутизация служб с использованием Spring Cloud Gateway Эта глава: знакомит с приемами маршрутизации микросервисов; демонстрирует реализацию шлюза с использованием Spring Cloud Gateway; показывает, как отображать запросы в маршруты к микросервисам; описывает конструирование фильтров для использования идентификаторов корреляции и трассировки. В распределенных архитектурах, таких как архитектуры микросервисов, рано или поздно наступает момент, когда требуется реализовать такие важные черты поведения, как безопасность, журналирование и трассировка пользователей в последовательностях вызовов служб. Для этого необходимо обеспечить единообразное применение всех требуемых атрибутов во всех службах, чтобы командам разработчиков не приходилось создавать собственные решения. Конечно, с этой целью можно использовать некоторую общую библиотеку или фреймворк в каждой отдельной службе, но такое решение влечет за собой следующие последствия: сложно обеспечить единообразие реализации этих возможностей в каждой службе. Программисты сосредоточены на разработке функциональности и в вихре повседневных хлопот могут легко забыть о внедрении поддержки журналирования или трассировки в свои службы, если только они не работают в сфере с жесткими регулирующими правилами; 260 Глава 8 Управление конфигурациями с использованием Spring Cloud... возложение обязанностей по реализации сквозных задач, таких как безопасность и журналирование, на отдельные группы разработчиков значительно увеличивает вероятность, что кто-то выполнит эти реализации не так, как следует, или вообще забудет их выполнить. Сквозные задачи, затрагивающие все службы в приложении, могут повлиять на другие части приложения. подход с использованием общих библиотек или фреймворков может создать жесткую зависимость во всех наших службах. Чем больше возможностей встраивается в общий фреймворк, используемый всеми нашими службами, тем сложнее будет изменять или добавлять поведение в общий код без необходимости повторной компиляции и развертывания служб. Обновление базовых функций в общей библиотеке внезапно может превратиться в длительный процесс миграции. Чтобы решить эту проблему, нужно отделить подобные сквозные задачи от служб и оформить их в виде службы, которая может работать независимо и действовать как фильтр и маршрутизатор для всех микросервисов в приложении. Такие службы называются шлюзами, или сервисными шлюзами. В такой конфигурации клиенты больше не вызывают микросервисы напрямую, а обращаются к сервисному шлюзу, который действует как единая точка применения политик (Policy Enforcement Point, PEP) и направляет запросы адресатам. В этой главе мы покажем, как использовать Spring Cloud Gateway для реализации шлюза и в частности: как организовать прием вызовов всех служб на одном URL и переадресовывать их фактическим экземплярам с использованием механизма обнаружения служб; как добавлять идентификаторы корреляции во все вызовы, пересекающие шлюз; как извлекать идентификаторы корреляции из ответов HTTP и пересылать их обратно клиенту. Давайте подробнее рассмотрим, как шлюз вписывается в микросервисную архитектуру, которую мы создаем в этой книге. 8.1. Что такое сервисный шлюз? До сих пор в этой книге мы создавали службы, которые вызывались непосредственно через веб-клиента или через механизм обнаружения служб, такой как Eureka. Этот подход иллюстрирует рис. 8.1. Что такое сервисный шлюз? 261 Когда клиент вызывает службы напрямую, невозможно быстро реализовать сквозные задачи, такие как поддержка безопасности или журналирование, без включения логики реализации этих задач в каждую службу Служба организаций http://localhost:8081/v1/organization... Клиент Служба лицензий http://localhost:8080/v1/organization/ 958aa1bf-18dc-405c-b84ab69f04d98d4f/license/ Рис. 8.1. В отсутствие сервисного шлюза клиент вызывает отдельные конечные точки, принадлежащие разным службам Сервисный шлюз действует как посредник между клиентом и вызываемой службой. Клиент обращается только к одному URL, принадлежащему сервисному шлюзу, а шлюз анализирует путь, указанный клиентом, и определяет, какую службу вызвать. На рис. 8.2 показано, как сервисный шлюз переадресует пользователя к целевому микросервису и соответствующему экземпляру, подобно диспетчеру трафика. http://localhost:8081/v1/organization... Служба организаций http://serviceGateway/api/ organizationService/v1/organization/... Клиент Сервисный шлюз Служба лицензий http://localhost:8080/v1/organization/ 958aa1bf-18dc-405c-b84a-b69f04d98d4f/license/ Рис. 8.2. Сервисный шлюз располагается между клиентом и соответствующими экземплярами службы. Все вызовы служб (как внутренние, так и внешние) должны передаваться через сервисный шлюз Сервисный шлюз выступает в роли привратника для входящего трафика и переадресует вызовы конкретным микросервисам. При наличии сервисного шлюза клиенты никогда не вызывают службы непосредственно – все вызовы направляются только сервисному шлюзу. Поскольку сервисный шлюз располагается между клиентами и службами, он также действует как центральная точка применения политик. Это означает, что сквозные задачи могут решаться в одном месте и не требуется поручать их реализацию разработчикам отдельных служб. Вот некоторые примеры сквозных задач, реализуемых в сервисном шлюзе: статическая маршрутизация – сервисный шлюз принимает вызовы ко всем службам на одном URL и переадресует их конкретным микросервисам. Это упрощает разработку клиентов, потому что достаточно знать только одну конечную точку для обращения к любой нашей службе; 262 Глава 8 Управление конфигурациями с использованием Spring Cloud... динамическая маршрутизация – сервисный шлюз может анализировать входящие запросы и на основе этого анализа выполнять интеллектуальную маршрутизацию. Например, вызовы клиентов, участвующих в программе бета-тестирования, могут направляться определенному кластеру служб, выполняющих другую версию кода, отличную от той, что используют все остальные; аутентификация и авторизация – поскольку все вызовы сначала поступают в сервисный шлюз, этот шлюз оказывается естественным местом для проверки подлинности клиентов, вызывающих службы; сбор метрик и журналирование – сервисный шлюз службы может использоваться для сбора метрик и журналирования проходящих через него вызовов. Также сервисный шлюз можно использовать для подтверждения наличия критически важной информации в пользовательских запросах и тем самым обеспечить единообразие журналирования. Это не означает, что можно не собирать метрики в отдельных службах – сервисный шлюз лишь помогает централизовать сбор основных метрик, таких как количество вызовов службы и время, потребовавшееся на обработку запроса. Подождите! Но, разве сервисный шлюз не является единой точкой отказа и потенциальным узким местом? Выше в главе 6, когда мы знакомились с Eureka, мы говорили, что централизованные балансировщики нагрузки могут превратиться в единую точку отказа и узкое место для наших служб. Сервисный шлюз при неправильной реализации может нести такой же риск. При реализации сервисного шлюза имейте в виду следующее: балансировщики нагрузки полезны, когда размещаются перед отдельными группами служб. Соответственно, размещение балансировщика нагрузки перед несколькими экземплярами сервисного шлюза является неплохой конструкцией, гарантирующей масштабирование сервисного шлюза по мере необходимости. Но размещение балансировщика перед всеми экземплярами службы – плохая идея, потому что в этом случае он превращается в узкое место; код, реализующий сервисный шлюз, не должен иметь состояния. Не храните информацию о состоянии сервисного шлюза в памяти, потому что иначе есть риск ограничить масштабируемость шлюза, так как вам потребуется гарантировать репликацию данных во все экземпляры; код сервисного шлюза должен быть максимально прозрачным. Шлюз является «узким местом» для вызовов служб. Сложный код, предусматривающий множественные обращения к базе данных, может стать источником трудно выявляемых проблем с производительностью. Введение в Spring Cloud Gateway 263 Давайте теперь посмотрим, как реализовать сервисный шлюз с помощью Spring Cloud Gateway. Мы будем использовать Spring Cloud Gateway как наиболее предпочтительный API-шлюз, созданный командой Spring Cloud. Эта реализация основана на Spring 5 и является неблокирующим шлюзом, легко интегрирующимся с другими проектами Spring Cloud, которые мы использовали и будем использовать на протяжении всей книги. 8.2. Введение в Spring Cloud Gateway Spring Cloud Gateway – это реализация API-шлюза на основе фреймворка Spring 5, Project Reactor и Spring Boot 2.0. Этот неблокирующий шлюз. Но что означает «неблокирующий»? Неблокирующие приложения написаны так, что основные потоки выполнения никогда не блокируются. Они всегда доступны и обрабатывают поступающие запросы асинхронно, возвращая ответы по завершении обработки. Spring Cloud Gateway предлагает ряд интересных возможностей, в том числе: отображение маршрутов ко всем службам в приложении в один URL. Однако Spring Cloud Gateway не ограничивается одним URL. Фактически с помощью этого фреймворка можно определить несколько точек входа и тем самым обеспечить максимально точное отображение маршрутов (для каждой точки входа определяются свои правила маршрутизации). Но первым и наиболее типичным вариантом использования фреймворка является создание единой точки входа, через которую будут проходить все вызовы служб; создание фильтров для проверки запросов и ответов и выполнения необходимых действий по ее результатам. Поддержка фильтров позволяет внедрять точки принудительного применения политик в наш код и последовательно выполнять широкий спектр действий со всеми вызовами служб. Иначе говоря, фильтры позволяют изменять входящие и исходящие HTTPзапросы и ответы; создание предикатов – объектов, которые позволяют проверять соответствие запросов некоторому набору условий перед их обработкой. Spring Cloud Gateway включает набор встроенных фабрик предикатов маршрутов. Чтобы начать работу с Spring Cloud Gateway, давайте: 1 настроим проект Spring Boot с поддержкой Spring Cloud Gateway и определим соответствующие зависимости в файле сборки Maven; 2 настроим шлюз для взаимодействий с Eureka. 264 Глава 8 Управление конфигурациями с использованием Spring Cloud... 8.2.1. Настройка проекта шлюза Spring Boot В этом разделе мы настроим проект службы Spring Cloud Gateway с использованием Spring Boot. По аналогии со службами Spring Cloud Config и Eureka, которые мы создали в предыдущих главах, настройка службы Spring Cloud Gateway начинается с создания нового проекта Spring Boot и последующего применения аннотаций и конфигураций. Итак, создайте новый проект с помощью Spring Initializr (https://start.spring.io/), как показано на рис. 8.3. Рис. 8.3. Заполненная форма создания нового проекта Spring Cloud Gateway в Spring Initializr Для этого выполните следующие действия. 1 Выберите тип проекта Maven. 2 Выберите язык Java. 3 Выберите последнюю или стабильную версию Spring. 4 Введите в поле Group (Группа) название com.optimagrowth, а в поле Artifact (Артефакт) имя gatewayserver. 5 Введите API Gateway server в поля Name (Имя) и Description (Описание) и com.optimagrowth.gateway в поле Package name (Имя пакета). 6 Выберите в поле Packaging (Упаковка) вариант JAR. 7 Выберите версию Java 11. 8 Добавьте зависимости Eureka Client, Config Client, Gateway и Spring Boot Actuator, как показано на рис. 8.4. Введение в Spring Cloud Gateway 265 Рис. 8.4. Зависимости проекта шлюза в форме Spring Initializr В листинге 8.1 показано, как должен выглядеть файл pom.xml с настройками проекта шлюза. Листинг 8.1. Файл pom.xml с настройками проекта шлюза // Часть файла pom.xml опущена для краткости ... <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-config</artifactId> </dependency> <dependency> Подключить библиотеки <groupId> Spring Cloud Gateway org.springframework.cloud </groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> <exclusions> <exclusion> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-ribbon</artifactId> </exclusion> 266 Глава 8 Управление конфигурациями с использованием Spring Cloud... <exclusion> <groupId>com.netflix.ribbon</groupId> <artifactId>ribbon-eureka</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> </dependencies> // Остальная часть файла pom.xml опущена для краткости ... Следующий шаг – подготовка файла src/main/resources/ bootstrap.yml с настройками подключения к службе Spring Config Server, которую мы создали в главе 5. В листинге 8.2 показано, как должен выглядеть файл bootstrap.yml. Листинг 8.2. Файл bootstrap.yml с настройками для проекта шлюза spring: Имя службы шлюза, чтобы клиент Spring application: Cloud Config знал, какую службу искать name: gateway-server cloud: Местоположение config: Spring Cloud Config Server uri: http://localhost:8071 ПРИМЕЧАНИЕ. Если вы не следовали за примерами кода в предыдущей главе, то можете загрузить исходный код по адресу https:// github.com/ihuaylupo/manning-smia/tree/master/chapter7/Final. 8.2.2.Настройка Spring Cloud Gateway для взаимодействий с Eureka Spring Cloud Gateway можно интегрировать со службой Netflix Eureka Discovery, созданной в главе 6. Для этого нужно добавить в настройки сервера конфигураций Eureka параметры только что созданной службы сервисного шлюза. Это может показаться сложным, но не волнуйтесь. Подобную задачу мы уже решили в предыдущей главе. Чтобы организовать обслуживание шлюза сервером конфигураций, сначала создадим файл с настройками для этой службы Введение в Spring Cloud Gateway 267 в репозитории Spring Configuration Server. (Не забывайте, что репозиторием может быть Vault, Git, файловая система или путь поиска классов classpath.) В этом примере мы создали файл gatewayserver.yml в пути поиска классов проекта. Вы найдете его в папке / configserver/src/main/resources/config. ПРИМЕЧАНИЕ. Имя файла задается с помощью свойства spring.application.name, которое определяется в файле bootstrap.yml службы. Например, для шлюза мы определили свойство spring.application.name как gateway-server, поэтому файл конфигурации также должен называться gateway-server. А расширение определяется форматом файла; вы можете выбрать между .properties и .yml. Следующий шаг – добавление настроек Eureka в только что созданный файл конфигурации. В листинге 8.3 показано, как это сделать. Листинг 8.3. Настройки Eureka в конфигурационном файле Spring Configuration Server server: port: 8072 eureka: instance: preferIpAddress: true client: registerWithEureka: true fetchRegistry: true serviceUrl: defaultZone: http://eurekaserver:8070/eureka/ Наконец, добавим аннотацию @EnableEurekaClient к классу ApiGatewayServerApplication, как показано в листинге 8.4. Вы найдете его в файле /gatewayserver/src/main/java/com/optimagrowth/ gateway/ApiGatewayServerApplication.java. Листинг 8.4. Добавление аннотации @EnableEurekaClient к классу ApiGatewayServerApplication package com.optimagrowth.gateway; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.EnableEurekaClient; @SpringBootApplication @EnableEurekaClient public class ApiGatewayServerApplication { public static void main(String[] args) { SpringApplication.run(ApiGatewayServerApplication.class, args); } } 268 Глава 8 Управление конфигурациями с использованием Spring Cloud... Теперь, создав базовую конфигурацию для Spring Cloud Gateway, перейдем к маршрутизации наших служб. 8.3. Настройка маршрутов в Spring Cloud Gateway По своей сути Spring Cloud Gateway – это обратный прокси-сервер, т. е. промежуточный сервер, располагающийся между клиентом, пытающимся получить доступ к ресурсу, и самим ресурсом. Клиент даже не подозревает, что взаимодействует с промежуточным сервером, а не с ресурсом. Обратный прокси-сервер получает запрос клиента, анализирует его и вызывает удаленный ресурс от имени клиента. В микросервисной архитектуре наш обратный прокси-сервер Spring Cloud Gateway принимает запрос к микросервису и пересылает его вышестоящей службе. Клиент думает, что общается только со шлюзом. Но на самом деле все намного сложнее. Для связи с вышестоящими службами шлюз должен знать, по какому маршруту отправить входящий вызов. В Spring Cloud Gateway есть несколько механизмов для этого, в том числе: автоматическое отображение маршрутов с помощью механизма обнаружения служб; ручное отображение маршрутов с помощью механизма обнаружения служб. 8.3.1.Автоматическое отображение маршрутов с помощью механизма обнаружения служб Вся карта маршрутов для шлюза прописывается в файле /configserver/src/main/resources/config/gateway-server.yml. Однако Spring Cloud Gateway может выполнять маршрутизацию автоматически, используя идентификаторы служб. Для этого нужно добавить настройки в конфигурационный файл gateway-server, как показано в листинге 8.5. Листинг 8.5. Настройки в файле gateway-server.yml для поддержки автоматического поиска служб spring: Разрешает шлюзу определять cloud: маршруты, используя механизм gateway: обнаружения служб discovery.locator: enabled: true lowerCaseServiceId: true Согласно настройкам в листинге 8.5 Spring Cloud Gateway будет автоматически обращаться к службе Eureka и определять местоположение вызываемой службы по ее идентификатору. Например, чтобы вызвать нашу службу организаций с использованием автоматической Введение в Spring Cloud Gateway 269 маршрутизации через Spring Cloud Gateway, клиент должен послать запрос экземпляру сервисного шлюза, используя следующий URL: http://localhost:8072/organization-service/v1/organization/958aa1bf-18dc➥ 405c-b84a-b69f04d98d4f Сервер Gateway доступен по адресу http://localhost:8072. Служба, которую требуется вызвать (служба организаций), представлена первой частью пути в URL. Рисунок 8.5 иллюстрирует, как происходит автоматическая маршрутизация. Service Discovery (Eureka) Служба организаций, экземпляр 1 Служба организаций, экземпляр 2 http://localhost:8072/organizationservice/... Сервисный шлюз (Spring Cloud Gateway) Служба организаций, экземпляр 3 Клиент http://localhost:8072/organization-service/v1/organization Имя службы используется как ключ для поиска фактического местоположения этой службы Остальная часть пути – это фактический URL конечной точки вызываемой службы Рис. 8.5. Spring Cloud Gateway использует имя приложения organization-service для поиска службы, которой нужно передать запрос Вся прелесть поддержки интеграции Spring Cloud Gateway с Eureka заключается в том, что теперь мы не только имеем единую конечную точку, через которую можно вызывать наши службы, но также можем добавлять и удалять экземпляры служб без изменения настроек шлюза. Например, мы можем добавить новую службу в Eureka, и шлюз автоматически будет пересылать ей вызовы, потому что автоматически будет определять ее местоположение с помощью Eureka. Чтобы увидеть маршруты, используемые сервером Gateway, можно обратиться к конечной точке actuator/gateway/routes сервера. Она вернет список всех маршрутов к нашим службам. На рис. 8.6 показан результат вызова конечной точки http://localhost:8072/ actator/gateway/routes. 270 Глава 8 Управление конфигурациями с использованием Spring Cloud... Идентификатор службы лицензий, с которым она зарегистрирована в Eureka Идентификатор службы организаций, с которым она зарегистрирована в Eureka Рис. 8.6. Для каждой службы, зарегистрированной в Eureka, имеется свой маршрут в Spring Cloud Gateway На рис. 8.6 показана карта маршрутов в Spring Cloud Gateway к зарегистрированным службам. Здесь также можно видеть дополнительные данные, такие как предикат, управляющий порт, идентификатор маршрута, фильтры и др. 8.3.2.Отображение маршрутов вручную с помощью механизма обнаружения служб Spring Cloud Gateway позволяет организовать более точное определение маршрутов, не полагаясь исключительно на автоматическое обнаружение служб по их идентификаторам в Eureka. Допустим, мы решили упростить маршрут, сократив название службы organization-service в пути по умолчанию /organization-service/v1/organization/{organization- Введение в Spring Cloud Gateway 271 id} до organization. Мы можем сделать это, определив соответствие для маршрута в файле конфигурации /configserver/src/main/resources/ config/gateway-server.yml, который находится в репозитории Spring Cloud Configuration Server. В листинге 8.6 показано, как это сделать. Листинг 8.6. Отображение маршрутов вручную в файле gatewayserver.yml spring: cloud: gateway: discovery.locator: enabled: true lowerCaseServiceId: true routes: - id: organization-service uri: lb://organization-service Путь, хотя и задается методом load(), является всего лишь одним из вариантов predicates: - Path=/organization/** Необязательный идентификатор маршрута, выбирается произвольно URI пункта назначения маршрута Фильтрует коллекцию Spring web.filters для изменения запроса или ответа до или после отправки ответа filters: - RewritePath=/organization/ (?<path>.*), /$\{path} Переписывает путь запроса, заменяя с / organization/** на /**; принимает в параметрах регулярные выражения искомого пути и замены Добавив эти настройки, мы теперь можем обратиться к службе организаций, указав путь /organization/v1/organization/{organization-id}. Если теперь передать этот путь конечной точке шлюза, то результаты должны выглядеть, как показано на рис. 8.7. Если внимательно посмотреть на рис. 8.7, то можно заметить, что для службы организаций теперь имеются две записи в карте маршрутов. Одна запись определяется настройками в файле gateway-server.yml, т. е. organization/**: organization-service. А другая запись – это маршрут, автоматически созданный шлюзом на основе идентификатора службы организаций, зарегистрированного в Eureka, т. е. /organization-service /**: organization-service. ПРИМЕЧАНИЕ. Если при использовании автоматического отображения, когда шлюз определяет маршруты к службам исключительно на основе идентификаторов, зарегистрированных в Eureka, ни один экземпляр службы не будет запущен, то шлюз не покажет маршрут к ней. Напротив, если настроить отображение маршрутов вручную и в Eureka не будет зарегистрировано ни одного экземпляра службы, то шлюз все равно покажет маршрут. Однако если попытаться вызвать несуществующий экземпляр службы по маршруту, настроенному вручную, то шлюз вернет ошибку HTTP 500. 272 Глава 8 Управление конфигурациями с использованием Spring Cloud... Чтобы исключить маршруты к службам, определяемые автоматически с использованием Eureka, и использовать только маршрут, определенные вручную, нужно удалить свойства spring.cloud.gateway.discovery. locator из файла gateway-server.yml, как показано в листинге 8.7. У нас сохранился маршрут, полученный автоматически с помощью службы Eureka Обратите внимание на маршрут к службе организаций, настроенный вручную Рис. 8.7. Результат вызова конечной точки /actuator/gateway/routes шлюза после определения дополнительного маршрута к службе организаций вручную ПРИМЕЧАНИЕ. Решение об отказе от автоматической маршрутизации следует тщательно взвесить. В стабильном окружении, где новые службы появляются не так часто, ручное определение маршрутов является простой задачей. Однако в обширных окру- Введение в Spring Cloud Gateway 273 жениях, где часто появляются новые службы, делать это становится немного утомительно. Листинг 8.7. Удаление записей в gateway-server.yml, отвечающих за поддержку маршрутов, определяемых автоматически spring: cloud: gateway: routes: - id: organization-service uri: lb://organization-service predicates: - Path=/organization/** filters: - RewritePath=/organization/ (?<path>.*), /$\{path} Если теперь вызвать конечную точку actuator/gateway/routes шлюза, мы должны увидеть в только один маршрут к службе организаций – объявленный нами. На рис. 8.8 показан результат этого вызова. Рис. 8.8. Результат вызова конечной точки gateway/actuator/gateway/routes шлюза содержит только один маршрут к службе организаций – объявленный нами 8.3.3. Динамическая загрузка настроек маршрутизации Следующий аспект настройки маршрутов в Spring Cloud Gateway, который мы рассмотрим, – это динамическое обновление маршрутов. Возможность динамически перезагружать настройки маршрутизации позволяет изменять определения маршрутов без перезапуска самого шлюза. Благодаря ей мы сможем быстро изменять существующие маршруты и добавлять новые. 274 Глава 8 Управление конфигурациями с использованием Spring Cloud... Вызвав конечную точку actuator/gateway/routes, мы должны увидеть текущий маршрут к нашей службе организаций. Чтобы добавить новые маршруты на лету, достаточно внести изменения в файл конфигурации и отправить их в репозиторий Git, откуда Spring Cloud Config извлекает данные конфигурации. После этого изменения можно сохранить в GitHub. Spring Actuator предоставляет конечную точку POST actuator/ gateway/refresh, обратившись к которой можно запустить процедуру перезагрузки конфигурации шлюза. Если после вызова конечной точки actuator/gateway/refresh вновь послать вызов конечной точке /routes, то можно увидеть, что появились два новых маршрута. В ответ конечная точка actuator/gateway/refresh возвращает код состояния HTTP 200 без тела ответа. 8.4. Настоящая мощь Spring Cloud Gateway: фабрики предикатов и фильтров Так как все запросы к службам проходят через шлюз, мы можем упростить вызовы. Но истинная мощь Spring Gateway проявляется, когда возникает необходимость внедрить нестандартную логику, которая должна применяться ко всем вызовам служб, проходящим через шлюз. Чаще всего такая логика используется для применения согласованного набора политик, таких как безопасность, журналирование и трассировка. Подобные политики приложений считаются сквозными задачами, потому что должны применяться ко всем службам в приложении без изменения каждой из них для реализации этих политик. Для реализации таких задач используются фабрики предикатов и фильтров в Spring Cloud Gateway, подобно классам аспектов в Spring. Они могут перехватывать широкий спектр поведений и изменять вызовы без привлечения их разработчиков. В отличие от сервлетных фильтров и аспектов Spring, ориентированных на конкретные службы, шлюз и его фабрик предикатов и фильтров позволяет реализовать сквозные задачи для всех служб, маршрутизируемых через шлюз. Имейте в виду, что предикаты позволяют проверять соответствие запросов некоторому набору условий перед их обработкой. На рис. 8.9 показана архитектура, которую Spring Cloud Gateway использует при применении предикатов и фильтров, когда запрос проходит через шлюз. Настоящая мощь Spring Cloud Gateway: фабрики предикатов и фильтров 1. Клиент (браузер, приложение) посылает запрос в Spring Cloud Gateway Клиент 3. Веб-обработчик шлюза читает фильтры, настроенные для определенного маршрута, и с их помощью обрабатывает запрос Обработчик сопоставлений шлюза 275 2. Обработчик сопоставлений отвечает за проверку соответствия пути в запросе конфигурации маршрута Веб-обработчик шлюза Фильтр (порядковый номер: -1) Фильтр (порядковый номер: 0) Фильтр (порядковый номер: n) Микросервис за шлюзом Фактическая служба Рис. 8.9. Как архитектура Spring Cloud Gateway применяет предикаты и фильтры к запросам Клиент (браузер, приложение и т. д.) посылает запрос в Spring Cloud Gateway. После получения запрос передается обработчику сопоставления, отвечающему за проверку соответствия пути в запросе конфигурации маршрута. Если все в порядке, то затем запрос попадает в веб-обработчик шлюза, который читает фильтры и применяет их к запросу. Если запрос благополучно преодолел все фильтры, то далее он передается микросервису в соответствии с настройками маршрутизации. 8.4.1. Встроенные фабрики предикатов Встроенные предикаты – это объекты, позволяющие проверить соответствие запросов набору условий перед их обработкой. Для каждого маршрута можем настроить несколько фабрик предикатов, которые объединяются посредством логического И. В табл. 8.1 перечислены все встроенные фабрики предикатов Spring Cloud Gateway. Эти предикаты могут применяться программно или через конфигурации, как было показано в предыдущих разделах. В этой книге мы будем применять предикаты только через настройки в разделе predicates в файле конфигурации, например: predicates: - Path=/organization/** 276 Глава 8 Управление конфигурациями с использованием Spring Cloud... Таблица 8.1. Встроенные предикаты в Spring Cloud Gateway Предикат Описание Пример Before Принимает параметр с датой и временем и пропускает только запросы, отправленные до этого момента времени Before=2020-03-11T... After Принимает параметр с датой и временем и пропускает только запросы, отправленные после этого момента времени After=2020-03-11T... Between Принимает два параметра с датой и временем и пропускает только запросы, отправленные между этими двумя моментами времени. Сравнение с первым параметром выполняется как «больше или равно», а со вторым – «строго меньше» Between=2020-03-11T..., 2020-04-11T... Header Принимает два параметра: имя заголовка и регулярное выражение. Пропускает только запросы, имеющие указанный заголовок со значением, совпадающим с регулярным выражением Header=X-Request-Id, \d+ Host Принимает параметр с шаблоном имени хоста в стиле Ant и с точкой в качестве разделителя. Пропускает только запросы с заголовком Host, содержащим имя хоста, соответствующее шаблону Host=**.example.com Method Проверяет HTTP-метод Method=GET Path Проверяет соответствие запроса указанному шаблону пути Path=/organization/{id} Query Принимает два параметра: обязательное и дополнительное регулярные выражения. Пропускает только запросы, параметры которых соответствуют этим регулярным выражениям Query=id, 1 Cookie Принимает два параметра: имя cookie и регулярное выражение. Пропускает только запросы с cookie, содержимое которых соответствует регулярному выражению Cookie=SessionID, abc RemoteAddr Принимает список IP-адресов и пропускает только запросы, адреса отправителей в которых соответствуют списку RemoteAddr=192.168.3.5/24 8.4.2. Встроенные фабрики фильтров Встроенные фабрики фильтров позволяют внедрить точки принудительного применения политик и последовательно выполнять самые разные действия со всеми вызовами. Иначе говоря, фильтры позволяют изменять входящие и исходящие HTTP-запросы и ответы. В табл. 8.2 приводится список всех встроенных фильтров в Spring Cloud Gateway. Настоящая мощь Spring Cloud Gateway: фабрики предикатов и фильтров 277 Таблица 8.2. Встроенные фильтры в Spring Cloud Gateway Фильтр Описание Пример AddRequestHeader Принимает два параметра и добавляет в запрос указанный заголовок с указанным значением AddRequestHeader= X-Organization-ID, F39s2 AddResponseHeader Принимает два параметра и добавляет в ответ указанный заголовок с указанным значением AddResponseHeader= X-Organization-ID, F39s2 AddRequestParameter Принимает два параметра и добавляет параметр запроса с указанным именем и значением AddRequestParameter= Organizationid, F39s2 PrefixPath Добавляет указанный префикс в путь HTTPзапроса PrefixPath=/api RequestRateLimiter Принимает три параметра: количество запросов в секунду, которое пользователь может отправить; допустимое превышение заданного ограничения и имя bean-компонента, реализующего интерфейс KeyResolver RequestRateLimiter= 10, 20, #{@userKeyResolver} RedirectTo Принимает два параметра – код состояния и URL. Код состояния должен относиться к диапазону кодов 300, определяющих переадресацию RedirectTo=302, http://localhost:8072 RemoveNonProxy Удаляет некоторые заголовки, такие как KeepAlive, Proxy-Authenticate и Proxy-Authorization – RemoveRequestHeader Удаляет из запроса заголовки, соответствующие параметру RemoveRequestHeader= X-Request-Foo RemoveResponseHeader Удаляет из ответа заголовки, соответствующие параметру RemoveRespnseHeader= X-Organization-ID RewritePath Принимает путь, и регулярные выражения принимает параметр с шаблонами для замены запрошенного пути RewritePath= /organization/ (?<path>.*), /$\ {path} SecureHeaders Добавляет заголовки безопасности в ответ – SetPath Принимает параметр с шаблоном пути. Изменяет путь запроса, подставляя шаблонные сегменты. При этом используются шаблоны URI из окружения Spring. Допускается наличие нескольких совпадающих сегментов SetPath= /{organization} SetStatus Принимает допустимый код состояния HTTP и заменяет им код состояния HTTP в ответе SetStatus=500 SetResponseHeader Принимает имя и значение и добавляет заголовок с этими именем и значением в ответ SetResponseHeader= X-Response-ID,123 278 Глава 8 Управление конфигурациями с использованием Spring Cloud... 8.4.3. Добавление своих фильтров Так как все запросы к службам проходят через шлюз, мы можем упростить вызовы. Но истинная мощь Spring Gateway проявляется, когда возникает необходимость внедрить нестандартную логику, которая должна применяться ко всем вызовам служб, проходящим через шлюз. Чаще всего такая логика используется для применения согласованного набора политик, таких как безопасность, журналирование и трассировка. Spring Cloud Gateway позволяет определять свою логику фильтров. Как вы наверняка помните, фильтры дают возможность реализовать цепочку бизнес-логики, применяемой к каждому запросу. Spring Cloud Gateway поддерживает два типа фильтров: предварительные фильтры – вызываются перед отправкой запроса целевой службе. Обычно цель предварительного фильтра состоит в том, чтобы, например, гарантировать согласованность формата сообщений (например, наличие ключевых HTTP-заголовков) или аутентификацию пользователей; заключительные фильтры – вызываются после обработки запроса целевой службой и применяются к ответу, который отправляется обратно клиенту. Обычно заключительные фильтры осуществляют журналирование ответа, обрабатывают ошибки или проверяют ответ на предмет присутствия конфиденциальной информации. На рис. 8.10 показано, как эти фильтры сочетаются друг с другом при обработке запроса клиента. Давайте пройдемся по этапам, показанным на рис. 8.10. Все начнется с того, что клиент посылает вызов службе, доступной через шлюз. Затем происходит следующее. 1 К запросу применяются все предварительные фильтры, определенные в шлюзе. Они проверяют и изменяют HTTP-запрос до того, как он попадет в службу. Однако предварительный фильтр не может перенаправить запрос другой конечной точке или службе. 2 После применения к входящему запросу всех имеющихся предварительных фильтров шлюз определяет маршрут (целевую службу). 3 После обработки запроса целевой службой к возвращаемому ответу применяются заключительные фильтры. Они проверяют и изменяют ответ, отправленный службой. Настоящая мощь Spring Cloud Gateway: фабрики предикатов и фильтров 279 Клиент Сервисный шлюз 1. Предварительные фильтры обрабатывают поступивший запрос Предварительный фильтр 2. Шлюз определяет целевой маршрут и отправляет запрос целевой службе Целевой маршрут Заключительный фильтр Целевая служба 3. После вызова целевой службы ответ обрабатывается заключительными фильтрами Рис. 8.10. Предварительные фильтры, целевая служба и заключительные фильтры образуют конвейер обработки клиентского запроса. Когда запрос поступает в шлюз, фильтры могут манипулировать им Лучший способ понять, как реализовать фильтры, – увидеть их в действии. С этой целью в следующих нескольких разделах мы сконструируем предварительный и заключительный фильтры, а затем используем их для обработки клиентских запросов. На рис. 8.11 показано, на каких этапах обработки запросов будут применяться эти фильтры в нашем приложении O-stock. 280 Глава 8 Управление конфигурациями с использованием Spring Cloud... Клиент Сервисный шлюз 1. Наш фильтр трассировки проверяет каждый входящий запрос и создает идентификатор корреляции в заголовке HTTP, если он отсутствует Предварительный фильтр Фильтр трассировки Целевая служба 2. Целевой службе передается запрос с идентификатором корреляции в заголовке HTTP Заключительный фильтр Фильтр ответов 3. Фильтр ответов гарантирует передачу идентификатора корреляции во всех возвращаемых ответах Рис. 8.11. Фильтры обеспечивают централизованную трассировку вызовов служб и их журналирование. Фильтры позволяют применять свои правила и политики к вызовам микросервисов Следуя схеме, показанной на рис. 8.11, мы можем выделить следующие фильтры: фильтр трассировки – это предварительный фильтр, который гарантирует наличие в каждом запросе, поступающий от шлюза, связанного с ним идентификатора корреляции. Идентификатор корреляции – это уникальный идентификатор, который передается всем микросервисам, участвующим в обработке запроса. Идентификатор корреляции позволяет проследить цепочку событий, происходящих при обработке запроса в последовательности микросервисов; Создание предварительного фильтра 8.5. 281 целевую службу – может быть службой организаций или службой лицензий. Обе службы получают идентификатор корреляции в заголовке HTTP-запроса; фильтр ответа – это заключительный фильтр, который добавляет идентификатор корреляции, связанный с вызовом службы, в заголовок ответа HTTP, отправляемого клиенту. Благодаря этому клиент получает доступ к идентификатору корреляции, связанному с запросом. Создание предварительного фильтра Создавать фильтры в Spring Cloud Gateway несложно. Чтобы вы могли понять суть, мы создадим предварительный фильтр с именем TrackingFilter, который будет проверять все входящие запросы и определять присутствие в них HTTP-заголовка tmx-correlation-id. Заголовок tmx-correlation-id, как предполагается, содержит GUID (Globally Universal ID – глобально универсальный идентификатор), который можно использовать для трассировки обработки запросов несколькими микросервисами: если tmx-correlation-id отсутствует в HTTP-заголовке, то наш фильтр TrackingFilter сам сгенерирует и добавит идентификатор корреляции; если идентификатор корреляции уже имеется, то фильтр ничего не будет делать (наличие идентификатора корреляции означает, что этот конкретный вызов является частью цепочки вызовов, выполняемых в процессе обработки запроса пользователя). ПРИМЕЧАНИЕ. Идея идентификатора корреляции обсуждалась в главе 7, а здесь мы более подробно рассмотрим порядок использования Spring Cloud Gateway для создания новых идентификаторов корреляции. Если вы пропустили предыдущую главу, мы настоятельно рекомендуем вернуться к ней и прочитать раздел о контексте потоков выполнения. В нашей реализации идентификаторов корреляции будут реализоваться переменные ThreadLocal, а для этого необходимо проделать дополнительную работу. Рассмотрим реализацию TrackingFilter, представленную в листинге 8.8. Этот код можно также найти в репозитории с примерами для этой книги, в файле класса /gatewayserver/src/main/java/ com/optimagrowth/gateway/filters/TrackingFilter.java. 282 Глава 8 Управление конфигурациями с использованием Spring Cloud... Листинг 8.8. Предварительный фильтр для создания идентификаторов корреляции package com.optimagrowth.gateway.filters; // Другие инструкции импорта опущены для краткости import org.springframework.http.HttpHeaders; import reactor.core.publisher.Mono; @Order(1) @Component public class TrackingFilter implements GlobalFilter { Глобальные фильтры реализуют интерфейс GlobalFilter и должны переопределять метод filter() private static final Logger logger = LoggerFactory.getLogger(TrackingFilter.class); Функции, часто используемые в фильтрах, заключены в класс FilterUtils Извлекает HTTP@Override заголовок из public Mono<Void> filter(ServerWebExchange exchange, запроса с поGatewayFilterChain chain) { мощью объекта ServerWebExchange, HttpHeaders requestHeaders = переданного в метод exchange.getRequest().getHeaders(); filter() в виде параif (isCorrelationIdPresent(requestHeaders)) { метра @Autowired FilterUtils filterUtils; Код, который выполняется каждый раз, когда запрос проходит через фильтр logger.debug( "tmx-correlation-id found in tracking filter: {}. ", filterUtils.getCorrelationId(requestHeaders)); } else { String correlationID = generateCorrelationId(); exchange = filterUtils.setCorrelationId(exchange, correlationID); logger.debug( "tmx-correlation-id generated in tracking filter: {}.", correlationID); } Вспомогательный меreturn chain.filter(exchange); тод, проверяющий на} личие идентификатора корреляции в заголовprivate boolean isCorrelationIdPresent( ке запроса HttpHeaders requestHeaders) { if (filterUtils.getCorrelationId(requestHeaders) != null) { return true; Вспомогательный } else { метод, генериreturn false; рующий новое } значение UUID для использования в } качестве идентифи- катора корреляции private String generateCorrelationId() { return java.util.UUID.randomUUID().toString(); } } Создание предварительного фильтра 283 Чтобы создать глобальный фильтр в Spring Cloud Gateway, нужно реализовать интерфейс GlobalFilter и определить метод filter() с логикой фильтра. Еще один важный момент, который следует отметить: способ получения HTTP-заголовков из объекта ServerWebExchange: HttpHeaders requestHeaders = exchange.getRequest().getHeaders(); Мы внедрили экземпляр класса FilterUtils, который инкапсулирует функции, используемые всеми фильтрами. Определение этого класса находится в файле /gatewayserver/src/main/java/com/ optimagrowth/gateway/filters/FilterUtils.java. Мы не будем рассматривать весь класс FilterUtils и обсудим только несколько ключевых методов: getCorrelationId() и setCorrelationId(). В листинге 8.9 показан код метода getCorrelationId() из класса FilterUtils. Листинг 8.9. Извлечение заголовка tmx-correlation-id с помощью getCorrelationId public String getCorrelationId(HttpHeaders requestHeaders){ if (requestHeaders.get(CORRELATION_ID) !=null) { List<String> header = requestHeaders.get(CORRELATION_ID); return header.stream().findFirst().get(); } else{ return null; } } Обратите внимание, как в листинге 8.9 сначала проверяется наличие заголовка tmx-correlation-ID во входящем запросе. Если заголовок отсутствует, метод должен вернуть null. Возможно, вы помните, что в методе filter() нашего класса TrackingFilter мы обрабатывали ситуацию отсутствия заголовка с помощью следующего кода: } else { String correlationID = generateCorrelationId(); exchange = filterUtils.setCorrelationId(exchange, correlationID); logger.debug("tmx-correlation-id generated in tracking filter: {}.", correlationID); } Чтобы добавить заголовок tmx-correlation-id, мы используем метод FilterUtils.setCorrelationId(), как показано в листинге 8.10. Листинг 8.10. Добавление HTTP-заголовка tmx-correlation-id public ServerWebExchange setRequestHeader(ServerWebExchange exchange, String name, String value) { return exchange.mutate().request( exchange.getRequest().mutate() .header(name, value) .build()) .build(); } 284 Глава 8 Управление конфигурациями с использованием Spring Cloud... public ServerWebExchange setCorrelationId(ServerWebExchange exchange, String correlationId) { return this.setRequestHeader(exchange,CORRELATION_ID,correlationId); } Метод FilterUtils.SetCorrelationId(), как показано в листинге 8.10, добавляет значения в HTTP-заголовки запросов с помощью метода mutate() класса ServerWebExchange.Builder. Этот метод возвращает построитель, позволяющий изменять свойства объекта запроса, заключая его в ServerWebExchangeDecorator и возвращая измененные значения или делегируя изменения этому экземпляру. Для тестирования мы можем послать запрос нашей службе организаций или лицензий. После отправки запроса в консоли должно появиться сообщение с созданным идентификатором: gatewayserver_1 | 2020-04-14 22:31:23.835 DEBUG 1 --- [or-http-epoll-3] c.o.gateway.filters.TrackingFilter : tmx-correlation-id generated in tracking filter: 735d8a31-b4d1-4c13-816d-c31db20afb6a. Если вы не увидите это сообщение в консоли, то просто добавьте строки, показанные в листинге 8.11, в конфигурационный файл bootstrap.yml с настройками сервера шлюза. Затем снова скомпилируйте и запустите микросервисы. Листинг 8.11. Настройки журналирования в конфигурационном файле bootstrap.yml сервера шлюза // Часть кода опущена для краткости logging: level: com.netflix: WARN org.springframework.web: WARN com.optimagrowth: DEBUG Если вы используете Docker, то можете выполнить следующие команды в корневом каталоге, где находится файл pom.xml родительского проекта: mvn clean package dockerfile:build docker-compose -f docker/docker-compose.yml up 8.6.Использование идентификатора корреляции в службах Теперь, обеспечив добавление идентификатора корреляции во все вызовы микросервисов, проходящих через шлюз, мы должны реализовать: возможность извлечения идентификатора корреляции в вызываемом микросервисе; передачу того же идентификатора корреляции любым нижестоящим службам. Использование идентификатора корреляции в службах 285 Для этого мы создадим три класса для каждого из наших микросервисов: UserContextFilter, UserContext и UserContextInterceptor. Эти классы будут взаимодействовать друг с другом и обеспечивать возможность чтения идентификатора корреляции (вместе с другой информацией, которую мы добавим позже) из входящего HTTPзапроса и представления его в виде объекта, который может использоваться бизнес-логикой приложения, а также передачу идентификатора корреляции в вызовах нижестоящих служб. На рис. 8.12 показаны эти компоненты в контексте нашей службы лицензий. 1. Служба лицензий вызывается через маршрут в шлюзе Клиент посылает запрос службе лицензий Сервисный шлюз Служба лицензий UserContextFilter Бизнес-логика службы лицензий RestTemplate UserContextInterceptor 2. UserContextFilter извлекает идентификатор корреляции из HTTP-заголовка и сохраняет его в объекте UserContext 3. Бизнес-логика службы имеет доступ к любым значениям в UserContext. Бизнес-логика службы лицензий должна вызвать службу организаций 4. UserContextInterceptor гарантирует включение идентификатора корреляции из UserContext во все исходящие вызовы REST Служба организаций Рис. 8.12. Использование набора общих классов для чтения идентификатора корреляции и его передачи нижестоящим службам 286 Глава 8 Управление конфигурациями с использованием Spring Cloud... Давайте рассмотрим, как выполняется последовательность действий, изображенная на рис. 8.12. 1 Когда шлюз получает запрос к службе лицензий, TrackingFilter добавляет в него HTTP-заголовок с идентификатором корреляции. 2 Класс UserContextFilter, реализующий ServletFilter, отображает идентификатор корреляции в экземпляр класса UserContext, который хранит значения в потоке выполнения для последующего использования. 3 Бизнес-логика службы лицензий вызывает службу организаций. 4 RestTemplate, вызывающий службу организаций, использует класс перехватчика Spring UserContextInterceptor для вставки заголовка с идентификатором корреляции в исходящий HTTPзапрос. Повторяющийся код и разделяемые библиотеки Вопрос об использовании разделяемых библиотек в микросервисах является серой зоной. Сторонники микросервисов скажут вам, что в микросервисах не следует использовать свои фреймворки, потому что это вводит искусственные зависимости. Изменения в бизнес-логике или ошибки могут потребовать широкомасштабного рефакторинга всех ваших служб. Другие разработчики микросервисов скажут, что пуристский подход непрактичен, потому что иногда возникают ситуации (как, например, в случае с классом UserContextFilter), когда имеет смысл создать общую библиотеку и использовать ее во всех сервисах. Мы считаем, что здесь есть своя золотая середина. Общие библиотеки вполне допустимо использовать для решения инфраструктурных задач. Но если вы начнете передавать между микросервисами бизнес-ориентированные классы, то рано или поздно это приведет к проблемам, потому что такой подход нарушает границы между службами. Однако мы, похоже, нарушаем наш собственный совет. Если вы заглянете в исходный код всех служб в этой главе, то увидите, что все они имеют свои копии классов UserContextFilter, UserContext и UserContextInterceptor. 8.6.1. UserContextFilter: перехват входящих HTTP-запросов Первым мы создадим класс UserContextFilter. Этот класс реализует сервлетный фильтр, который будет перехватывать все входящие HTTP-запросы и отображать идентификатор корреляции (и некоторые другие значения) из HTTP-запроса в экземпляр класса UserContext. В листинге 8.12 показано определение класса UserContext, которое находится в файле класса licensing-service/src/main/ java/com/optimagrowth/license/utils/UserContextFilter.java. Использование идентификатора корреляции в службах 287 Листинг 8.12. Отображение идентификатора корреляции в экземпляр класса UserContext package com.optimagrowth.license.utils; // Инструкции импорта опущены для краткости @Component public class UserContextFilter implements Filter { Аннотация @Component зарегистрирует фильтр, реализующий интерфейс javax. servlet.Filter, в Spring private static final Logger logger = LoggerFactory.getLogger(UserContextFilter.class); @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest httpServletRequest = (HttpServletRequest)servletRequest; UserContextHolder.getContext() .setCorrelationId( Извлекает httpServletRequest.getHeader(UserContext.CORRELATION_ID) ); идентификатор UserContextHolder.getContext().setUserId( корреляции из заголовка httpServletRequest.getHeader(UserContext.USER_ID)); и записывает UserContextHolder.getContext().setAuthToken( его в экземhttpServletRequest.getHeader(UserContext.AUTH_TOKEN)); пляр класса UserContextHolder.getContext().setOrganizationId( UserContext httpServletRequest.getHeader(UserContext.ORGANIZATION_ID)); logger.debug("UserContextFilter Correlation id: {}", UserContextHolder.getContext().getCorrelationId()); filterChain.doFilter(httpServletRequest, servletResponse); } // Пустые методы init и destroy опущены } В конечном итоге UserContextFilter отобразит интересующие нас значения из HTTP-заголовков в экземпляр Java-класса UserContext. 8.6.2.UserContext: обеспечение доступности HTTP-заголовков в службах Класс UserContext хранит значения HTTP-заголовков из запроса, отправленного клиентом. Он включает методы чтения/записи свойств, которые возвращают и сохраняют значения из java.lang. ThreadLocal. В листинге 8.13 показано определение класса UserContext, которое можно найти в /licensing-service/src/main/java/ com/optimagrowth/license/utils/UserContext.java. 288 Глава 8 Управление конфигурациями с использованием Spring Cloud... Листинг 8.13. Сохранение значения HTTP-заголовка внутри экземпляра класса UserContext // Инструкции импорта опущены для краткости @Component public class UserContext { public static final String public static final String public static final String public static final String private private private private String String String String CORRELATION_ID AUTH_TOKEN USER_ID ORGANIZATION_ID correlationId authToken userId organizationId = = = = new new new new = = = = "tmx-correlation-id"; "tmx-auth-token"; "tmx-user-id"; "tmx-organization-id"; String(); String(); String(); String(); } Как видите, класс UserContext – это не что иное, как POJO, хранящий значения, полученные из входящего HTTP-запроса. Далее мы будем использовать класс UserContextHolder из /licensing-service/src/main/ java/com/optimagrowth/license/utils/UserContextHolder.java для сохранения UserContext в переменной ThreadLocal, доступной любым методам, вызываемым в потоке, который обрабатывает запрос пользователя. В листинге 8.14 показано определение класса UserContextHolder. Листинг 8.14. Класс UserContextHolder, сохраняющий UserContext а ThreadLocal public class UserContextHolder { private static final ThreadLocal<UserContext> userContext = new ThreadLocal<UserContext>(); public static final UserContext getContext(){ UserContext context = userContext.get(); if (context == null) { context = createEmptyContext(); userContext.set(context); } return userContext.get(); } public static final void setContext(UserContext context) { Assert.notNull(context, "Only non-null UserContext instances are permitted"); userContext.set(context); } public static final UserContext createEmptyContext(){ return new UserContext(); } } Использование идентификатора корреляции в службах 289 8.6.3.RestTemplate и UserContextInterceptor: обеспечение передачи идентификатора корреляции нижестоящим службам Последний фрагмент кода, который мы рассмотрим, – это класс UserContextInterceptor. Он добавляет идентификатор корреляции в любые исходящие HTTP-запросы к нижестоящим службам, которые отправляются экземпляром RestTemplate. Это необходимо для того, чтобы дать возможность увидеть связь между вызовами служб. Для этого мы будем использовать перехватчик Spring, внедренный в класс RestTemplate. Определение UserContextInterceptor приводится в листинге 8.15. Листинг 8.15. Добавление идентификатора корреляции во все исходящие вызовы микросервисов public class UserContextInterceptor implements ClientHttpRequestInterceptor { Реализует интерфейс ClientHttpRequestInterceptor private static final Logger logger = LoggerFactory.getLogger(UserContextInterceptor.class); @Override public ClientHttpResponse intercept( Вызывает HttpRequest request, byte[] body, intercept() ClientHttpRequestExecution execution) throws IOException { до отправки HttpHeaders headers = request.getHeaders(); HTTP-запроса headers.add(UserContext.CORRELATION_ID, с помощью UserContextHolder.getContext(). RestTemplate getCorrelationId()); Принимает идентификатор headers.add(UserContext.AUTH_TOKEN, корреляции, хранящийся UserContextHolder.getContext(). в UserContext, для добавления в исходящий HTTPgetAuthToken()); запрос return execution.execute(request, body); } } Чтобы задействовать UserContextInterceptor, нужно определить bean-компонент RestTemplate и добавить в него UserContextInterceptor. Для этого мы определим свой bean-компонент RestTemplate в классе LicenseServiceApplication. Определение этого класса вы найдете в /licensing-service/src/main/java/com/optimagrowth/license/. В листинге 8.16 показан метод, добавленный в RestTemplate. 290 Глава 8 Управление конфигурациями с использованием Spring Cloud... Листинг 8.16. Добавление UserContextInterceptor в класс RestTemplate @LoadBalanced Указывает, что этот объект RestTemplate будет использовать балансировщик нагрузки @Bean public RestTemplate getRestTemplate(){ RestTemplate template = new RestTemplate(); List interceptors = template.getInterceptors(); if (interceptors==null){ template.setInterceptors(Collections.singletonList( new UserContextInterceptor())); }else{ interceptors.add(new UserContextInterceptor()); template.setInterceptors(interceptors); } Добавляет return template; } UserContextInterceptor в экземпляр RestTemplate С этим определением bean-компонента мы можем использовать аннотацию @Autowired и внедрять класс RestTemplate, созданный в листинге 8.16, с присоединенным к нему экземпляром UserContextInterceptor. Агрегирование журналов, аутентификация и многое другое Теперь, реализовав передачу идентификаторов корреляции всем службам, мы можем отслеживать выполнение транзакций по мере их прохождения через все службы, участвующие в обработке вызова. Но для этого необходимо также организовать журналирование во всех службах через центральную точку агрегирования журналов, которая получает записи из всех наших служб и сохраняет их в одном месте. Каждая запись, отправленная службе агрегирования журналов, будет иметь связанный с ней идентификатор корреляции. Реализация решения для агрегирования журналов выходит за рамки этой главы, но в главе 10 мы покажем, как использовать Spring Cloud Sleuth для этой цели. Пример с Spring Cloud Sleuth не будет использовать TrackingFilter, созданный здесь, но в нем будут продемонстрированы те же идеи трассировки идентификатора корреляции. 8.7.Создание заключительного фильтра, добавляющего идентификатор корреляции Как вы наверняка помните, Spring Gateway выполняет фактический HTTP-вызов от имени клиента и проверяет ответ, возвращаемый целевой службой. При этом он может изменить ответ или добавить в него дополнительную информацию. В сочетании с захватом информации в предварительном фильтре заключительный Создание заключительного фильтра, добавляющего идентификатор... 291 фильтр шлюза является идеальным местом для сбора метрик и добавления в журнал заключительных записей, связанных с транзакциями пользователя. Мы воспользуемся этим и вернем пользователю идентификатор корреляции, который передавался микросервисам. При этом мы не будем касаться тела ответа. В листинге 8.17 показан код заключительного фильтра. Вы найдете его в файле /gatewayserver/src/main/java/com/optimagrowth/ gateway/filters/ResponseFilter.java. Листинг 8.17. Внедрение идентификатора корреляции в HTTP-ответ @Configuration public class ResponseFilter { final Logger logger =LoggerFactory.getLogger(ResponseFilter.class); @Autowired FilterUtils filterUtils; @Bean public GlobalFilter postGlobalFilter() { return (exchange, chain) -> { return chain.filter(exchange).then(Mono.fromRunnable(() -> { HttpHeaders requestHeaders = exchange.getRequest().getHeaders(); String correlationId = Извлекает идентиfilterUtils. фикатор корреляgetCorrelationId(requestHeaders); ции из исходного HTTP-запроса logger.debug( "Adding the correlation id to the outbound headers. {}", Журналирует URI correlationId); исходящего заДобавляет идентиexchange.getResponse().getHeaders(). проса, чтобы пофикатор коррелятом можно было add(FilterUtils.CORRELATION_ID, ции в ответ найти начало и correlationId); конец обработки logger.debug("Completing outgoing request запроса for {}.", exchange.getRequest().getURI()); })); }; } } После реализации ResponseFilter можно запустить сервисный шлюз и с его помощью вызвать службу лицензий или организаций. После обработки запроса вы увидите заголовок tmx-correlation-id в HTTP-ответе, как показано на рис. 8.13. 292 Глава 8 Маршрутизация служб с использованием Spring Cloud Gateway Идентификатор корреляции в HTTP-ответе Рис. 8.13. В ответ был добавлен заголовок tmx-correlation-id Также можно увидеть сообщения в консоли (рис. 8.14) с идентификатором корреляции e3f6a72b-7d6c-41da-ac12-fb30fcd1e547, которые выводят инструкции журналирования по мере прохождения запроса через фильтры. До этого момента наши примеры фильтров манипулировали запросами клиентов до и после передачи целевой службе. Теперь, когда мы знаем, как создать сервисный шлюз на основе Spring Cloud Gateway, перейдем к следующей главе, где рассказывается, как защитить микросервисы с помощью Keycloak и OAuth2. Идентификатор корреляции, добавленный предварительным фильтром Обработка запроса службой организаций URI запроса Идентификатор корреляции, извлеченный из запроса заключительным фильтром Рис. 8.14. Записи в журнале, созданные предварительным фильтром, службой организаций и заключительным фильтром Создание заключительного фильтра, добавляющего идентификатор... 293 Итоги Spring Cloud упрощает создание сервисного шлюза. Spring Cloud Gateway содержит набор встроенных фабрик предикатов и фильтров. Предикаты – это объекты, позволяющие проверить соответствие запросов набору условий перед их обработкой. Фильтры позволяют изменять входящие и исходящие HTTPзапросы и ответы. Spring Cloud Gateway может интегрироваться с Netflix Eureka Server и автоматически определять маршруты к службам, зарегистрированным в Eureka. Spring Cloud Gateway позволяет также вручную определять маршруты в файлах конфигурации приложения. Spring Cloud Config Server поддерживает динамическую загрузку карты маршрутов без перезапуска шлюза. Spring Cloud Gateway позволяет реализовать нестандартную бизнес-логику обработки запросов с помощью фильтров. Spring Cloud Gateway дает возможность определять предварительные и заключительные фильтры. Предварительные фильтры можно использовать для добавления идентификаторов корреляции во все запросы, поступающие в шлюз. Заключительные фильтры можно использовать для добавления идентификаторов корреляции в исходящие HTTPответы, возвращаемые клиентам. 9 Безопасность микросервисов Эта глава: объясняет, почему важна безопасность микросервисов; знакомит с OAuth2 и OpenID; показывает, как настроить Keycloak; рассказывает о применении Keycloak для аутентификации и авторизации; демонстрирует приемы защиты микросервисов с Keycloak; показывает, как передавать токены доступа между службами. Теперь, когда у нас есть готовая микросервисная архитектура, перед нами встает важная задача устранения уязвимостей безопасности. В этой главе мы займемся обсуждением вопросов безопасности и уязвимости. Мы определим уязвимость как слабое место или недостаток приложения. Конечно, все системы имеют уязвимости, но одно дело – наличие уязвимостей и совсем другое – когда эти уязвимости могут использоваться во вред. Упоминание о безопасности часто вызывает у разработчиков невольный стон. В среде разработчиков часто можно услышать такие комментарии: «Безопасность – сложная тема, ее сложно реализовать и еще сложнее отладить». Тем не менее мы не найдем ни одного разработчика (за исключением, может быть, самых неопытных), который сказал бы, что не беспокоится о безопасности. Защита микросервисов – сложная и трудоемкая задача, которая включает реализацию нескольких уровней защиты, в том числе: Что такое OAuth2? 295 прикладного уровня – обеспечивает наличие надлежащих пользовательских элементов управления, с помощью которых можно проверить, является ли пользователь тем, за кого он себя выдает, и что у него есть все необходимые разрешения для выполнения запрошенных операций; инфраструктурного уровня – поддерживает работу службы, обеспечивает доставку исправлений и обновлений, минимизирующих риск уязвимостей; сетевого уровня – реализует элементы управления доступом к сети, чтобы служба была доступна только через четко определенные порты и только небольшому количеству авторизованных серверов. В этой главе рассказывается только об аутентификации и авторизации пользователей на прикладном уровне (первый пункт в списке). Два других пункта представляют чрезвычайно широкие темы безопасности, выходящие за рамки этой книги. Кроме того, существуют другие инструменты, такие как OWASP DependencyCheck Project, которые могут помочь в выявлении уязвимостей. ПРИМЕЧАНИЕ. OWASP Dependency-Check Project – это инструмент OWASP Software Composition Analysis (SCA), который позволяет выявлять доступные извне уязвимости. Желающим узнать больше мы рекомендуем заглянуть на страницу https:// owasp.org/www-project-dependency-check/. Для реализации авторизации и аутентификации мы используем модуль Spring Cloud Security, а для защиты служб на основе Spring – Keycloak, программный компонент с открытым исходным кодом, реализующий управление идентификацией и доступом для современных приложений и служб. Keycloak написан на Java и поддерживает протоколы SAML v2 (Security Assertion Markup Language) и протоколы федеративной идентификации OpenID Connect (OIDC)/OAuth2. 9.1. Что такое OAuth2? OAuth2 – это фреймворк безопасности на основе токенов, который описывает шаблоны авторизации, но не определяет, как на самом деле должна выполняться аутентификация. Вместо этого он позволяет пользователям аутентифицировать себя с помощью сторонней службы, называемой поставщиком удостоверений (identity provider, IdP). В случае успешной аутентификации пользователю передается токен, который он должен отправлять с каждым запросом. Этот токен может быть проверен с помощью службы аутентификации. 296 Глава 9 Безопасность микросервисов Основная цель OAuth2 – дать возможность аутентифицировать пользователя во всех службах, участвующих в обработке запроса, без передачи им учетных данных. OAuth2 позволяет защищать службы REST с помощью схем аутентификации, называемых грантами, или разрешениями на авторизацию. В спецификации OAuth2 предусмотрено четыре типа разрешений: пароль; учетные данные клиента; код авторизации; неявный. Мы не будем рассматривать все эти типы разрешений или приводить примеры кода для каждого из них, потому что одной главы для всего этого слишком мало. Вместо этого мы поступим следующим образом: обсудим порядок применения OAuth2 в наших микросервисах с использованием разрешения самого простого типа – пароля; посмотрим, как можно использовать веб-токены JSON (JSON Web Tokens, JWT) для реализации более надежного решения с OAuth2 и познакомимся со стандартом кодирования информации в токенах OAuth2; познакомимся с некоторыми соображениями безопасности, которые необходимо учитывать при создании микросервисов. ПРИМЕЧАНИЕ. В приложении B представлен обзор других типов разрешений OAuth2. Желающим детально изучить специ­ фикацию OAuth2 и способы реализации разрешений всех типов, мы настоятельно рекомендуем книгу Джастина Ричера (Justin Richer) и Антонио Сансо (Antonio Sanso) «OAuth2 in Action» (Manning, 2017), содержащую исчерпывающее описание OAuth2. Настоящая мощь OAuth2 заключается в простоте интеграции со сторонними поставщиками облачных услуг, а также аутентификации и авторизации пользователей с помощью облачных служб без необходимости постоянно передавать учетные данные пользователя. OpenID Connect (OIDC) – это слой поверх инфраструктуры OAuth2, который обеспечивает аутентификацию и передачу идентификационной информации (удостоверения) о вошедшем в приложение. Серверы авторизации, поддерживающие OIDC, иногда называют поставщиками удостоверений (identity provider). Но, прежде чем перейти к техническим деталям защиты наших служб, познакомимся с архитектурой Keycloak. Введение в Keycloak 9.2. 297 Введение в Keycloak Keycloak – это решение с открытым исходным кодом для управления идентификацией и доступом для наших служб и приложений. Основная цель Keycloak – упростить защиту служб и приложений. Вот некоторые характеристики Keycloak: централизует аутентификацию и обеспечивает поддержку однократного входа (Single Sign-On, SSO); позволяет разработчикам сосредоточиться на прикладной логике и не отвлекаться на такие аспекты безопасности, как авторизация и аутентификация; поддерживает двухфакторную аутентификацию; совместим с LDAP; предлагает несколько адаптеров для простой защиты приложений и серверов; позволяет настраивать политики паролей. 4. Сервер аутентифицирует пользователя и проверяет передаваемые ему токены 1. Служба, которую требуется защитить Сервер аутентификации/ авторизации Защищенный ресурс Приложение, пытающееся получить доступ к защищенному ресурсу Владелец ресурса 2. Владелец ресурса определяет, какие приложения/пользователи могут получить доступ к ресурсу через сервер авторизации Пользователь 3. Когда пользователь пытается получить доступ к защищенной службе, он должен пройти процедуру аутентификацию и получить токен от сервера аутентификации Рис. 9.1. Keycloak позволяет аутентифицировать пользователя без постоянной передачи учетных данных Keycloak выделяет четыре основных компонента: защищенный ресурс, владельца ресурса, приложение и сервер аутентификации/авторизации. На рис. 9.1 показано, как эти компоненты взаимодействуют друг с другом. Защищенный ресурс – это ресурс (в нашем случае микросервис), который требуется защитить и который должен быть доступен только аутентифицированным пользователям, имеющим надлежащие разрешения. Глава 9 Безопасность микросервисов 298 Владелец ресурса – лицо, определяющее, каким приложениям и пользователям разрешено вызывать службу и какие разрешения имеют разные пользователи. Каждому приложению, зарегистрированному владельцем ресурса, дается идентификационное имя приложения, а также секретный ключ. Комбинация имени приложения и секретного ключа является частью учетных данных, которые передаются с токеном доступа для проверки. Приложение – это приложение, которое будет вызывать службу от имени пользователя. В конце концов, пользователи редко вызывают службы напрямую, вместо этого они полагаются на приложения, которые выполняют рутинную работу. Сервер аутентификации/авторизации – это сервер-посредник между приложением и службами. Сервер аутентификации позволяет пользователям аутентифицировать себя без передачи своих учетных данных всем службам, которые приложение будет вызывать от их имени. Как упоминалось выше, компоненты безопасности Keycloak взаимодействуют друг с другом, обеспечивая аутентификацию пользователей служб. Пользователи проходят аутентификацию на сервере Keycloak, передавая свои учетные данные и информацию о приложении/устройстве, которое используется для доступа к защищенному ресурсу (микросервису). Если учетные данные действительны, сервер Keycloak возвращает токен аутентификации, который можно передавать от службы к службе каждый раз, когда они используются пользователем. Получив такой токен, защищенный ресурс может связаться с сервером Keycloak, чтобы проверить действительность токена и получить список ролей, присвоенных пользователю. Роли используются для группировки пользователей с одинаковыми разрешениями и определения ресурсов, к которым они могут обращаться. В этой главе мы используем роли Keycloak для определения HTTP-команд, которые пользователь может применять для вызова конечных точек службы. Безопасность веб-служб – чрезвычайно сложная тема. Важно понимать, кто будет вызывать наши службы (внутренние или внешние пользователи), как они будут их вызывать (с использованием внутреннего веб-клиента, мобильного устройства или вебприложения) и какие действия они могут выполнять. Начнем с малого: использование Spring и Keycloak для защиты... 299 Об аутентификации и авторизации Разработчики часто смешивают и путают термины «аутентификация» и «авторизация». Аутентификация – это действия пользователя, доказывающие, что он действительно является тем, за кого себя выдает, заключающиеся в передаче учетных данных. Авторизация определяет разрешения на выполнение тех или иных операций. Например, пользователь Illary может подтвердить свою личность, передав имя пользователя и пароль, но может не иметь права просматривать конфиденциальные данные (например, сведения о заработной плате). Далее в нашем обсуждении мы будем предполагать, что пользователь должен аутентифицироваться до авторизации. 9.3.Начнем с малого: использование Spring и Keycloak для защиты единственной конечной точки Давайте попробуем разобраться с настройкой аутентификации и авторизации, для чего: добавим службу Keycloak в Docker; настроим службу Keycloak и зарегистрируем O-stock как авторизованное приложение, которое может аутентифицировать и авторизовывать пользователей; используем Spring Security для защиты служб O-stock. Для проверки аутентификации в Keycloak мы просто сымитируем вход пользователя с помощью Postman; защитим службы лицензий и организаций, чтобы их мог вызывать только аутентифицированный пользователь. 9.3.1. Добавление Keycloak в Docker В этом разделе мы расскажем, как добавить службу Keycloak в окружение Docker. Для начала добавим код из листинга 9.1 в файл dockercompose.yml. ПРИМЕЧАНИЕ. Если вы не следовали за примерами кода в предыдущей главе, то можете загрузить исходный код по адресу https://github.com/ihuaylupo/manning-smia/tree/master/chapter8. 300 Глава 9 Безопасность микросервисов Листинг 9.1. Добавление настроек службы Keycloack в dockercompose.yml // Часть файла docker-compose.yml опущена для краткости ... keycloak: Имя службы Keycloak в Docker image: jboss/keycloak restart: always environment: Имя пользователя для доступа к консоли администратора Keycloak KEYCLOAK_USER: admin KEYCLOAK_PASSWORD: admin Пароль для доступа к консоports: ли администратора Keycloak - "8080:8080" networks: backend: aliases: - "keycloak" ПРИМЕЧАНИЕ. Keycloak можно использовать с несколькими базами данных, такими как H2, PostgreSQL, MySQL, Microsoft SQL Server, Oracle и MariaDB. В этой главе мы будем использовать встроенную базу данных H2. Желающим использовать другую базу данных мы советуем ознакомиться с инструкциями по адресу https://github.com/keycloak/keycloak-containers/tree/master/ docker-compose-examples. Обратите внимание, что в листинге 9.1 мы назначили службе Keycloak порт 8080. Если вернуться на несколько глав назад, то можно заметить, что этот же порт мы назначили службе лицензий. Чтобы избежать конфликтов, назначим службе лицензий другой порт – 8180 вместо 8080 – в файле docker-compose.yml, как показано ниже: licensingservice: image: ostock/licensing-service:0.0.3-SNAPSHOT ports: - "8180:8080" ПРИМЕЧАНИЕ. Чтобы запустить Keycloak в локальном окружении, нужно добавить запись 127.0.0.1 Keycloak в файл hosts. В Windows этот файл находится в папке C:\Windows\System32\drivers\etc, а в Linux – в каталоге /etc. Зачем нужна эта запись? Контейнеры могут взаимодействовать друг с другом, используя сетевые псевдонимы или MAC-адреса, но программе Postman необходимо использовать имя локального хоста localhost для вызова служб. 9.3.2. Настройка Keycloak Теперь, добавив Keycloak в docker-compose.yml, выполним следующую команду в корневой папке нашего проекта: Начнем с малого: использование Spring и Keycloak для защиты... 301 docker-compose -f docker/docker-compose.yml up Когда службы запустятся, введите в адресной строке браузера ссылку http://keycloak:8080/auth/, чтобы открыть консоль администрирования Keycloak. Настройка Keycloak – простой процесс. При первом посещении консоли администратора отображается страница приветствия, как показано на рис. 9.2. На этой странице перечислены различные варианты дальнейших действий, такие как: открыть консоль администрирования, перейти в раздел с документацией, получить отчеты о проблемах и т. д. В данном случае выберите ссылку Administration Console (Консоль администрирования). Щелкните на ссылке Administration Console (Консоль администрирования) Рис. 9.2. Страница приветствия Keycloak Следующий шаг – ввод имени пользователя и пароля, которые мы определили в файле docker-compose.yml. Этот шаг показан на рис. 9.3. Чтобы продолжить настройку, создадим пространство (realm). Пространством в Keycloak называется объект, управляющий набором пользователей, учетных данных, ролей и групп. Чтобы создать объект пространства, щелкните на кнопке Add Realm (Добавить пространство), которая находится в раскрывающемся меню Master (Управление) после входа в Keycloak. Назовем это пространство spmia-realm. 302 Глава 9 Безопасность микросервисов Имя пользователя и пароль, которые определены в файле docker-compose.yml Рис. 9.3. Страница входа в Keycloak На рис. 9.4 показаны настройки пространства spmia-realm в службе Keycloak. 1. Щелкните на кнопке Add Realm (Добавить пространство) 2. После щелчка появится форма Add Realm (Добавить пространство) 3. Введите имя пространства 4. Щелкните на кнопке Create (Создать) Рис. 9.4. На странице добавления пространства Keycloak отображается форма, где можно ввести имя нового пространства После создания пространства откроется главная страница spmiarealm с настройками, как показано на рис. 9.5. Начнем с малого: использование Spring и Keycloak для защиты... 303 Рис. 9.5. Страница с настройками spmia-realm в Keycloak 9.3.3. Регистрация клиентского приложения Следующий шаг – создание клиента. Клиенты в Keycloak – это сущности, которые могут запрашивать аутентификацию пользователя. Клиентами часто являются приложения или службы, которые требуется защитить с использованием решения однократного входа (Single Sign-On, SSO). Чтобы создать клиента, выберите пункт Clients (Клиенты) в меню слева. После этого откроется страница, изображенная на рис. 9.6. Щелкните на кнопке Create (Создать) Рис. 9.6. Страница Clients (Клиенты) с настройками клиента для пространства spmia-realm В странице со списком клиентов щелкните на кнопке Create (Создать) вверху справа, как показано на рис. 9.6. После этого появится форма добавления нового клиента, в которой нужно ввести следующую информацию: Глава 9 Безопасность микросервисов 304 идентификатор клиента; протокол клиента; корневой URL. Заполните поля, как показано на рис. 9.7. Идентификатор клиента O-stock Выберите протокол openid-connect Щелкните на кнопке Save (Сохранить) Рис. 9.7. Информация о клиенте O-stock в настройках Keycloak После сохранения клиента откроется страница с его настройками, как показано на рис. 9.8. На этой странице введите следующие данные: Access Type (Тип доступа): Confidential (Конфиденциальный); Service Accounts Enabled (Учетные записи служб включены): On (Вкл); Authorization Enabled (Авторизация включена): On (Вкл); Valid Redirect URLs (Действительные URL для переадресации): http://localhost:80*; Web Origins (Веб-источники): *. В этом примере мы создали только одного глобального клиента с именем ostock, но, вообще, вы можете настроить столько клиентов, сколько захотите. Следующий шаг – настройка ролей клиентов. Перейдите на вкладку Roles (Роли). Чтобы лучше понять суть ролей клиентов, представьте, что с нашим приложением будут работать клиенты двух типов: администраторы и обычные пользователи. Пользователи-администраторы могут обращаться к любым службам приложения, а обычные пользователи – только к некоторым из них. После загрузки страницы Roles (Роли) вы увидите список предопределенных ролей. Щелкните на кнопку Add Role (Добавить Начнем с малого: использование Spring и Keycloak для защиты... 305 роль) вверху справа. После этого откроется форма добавления новой роли, как показано на рис. 9.9. 1. Выберите конфиденциальный тип доступа 2. Включите авторизацию 3. Допустимые URI запросов 4. Веб-источники 5. Щелкните на кнопке Save (Сохранить), чтобы завершить работу со страницей Рис. 9.8. Дополнительные настройки клиента O-stock в Keycloak Рис. 9.9. Страница Add Role (Добавить роль) в Keycloak 306 Глава 9 Безопасность микросервисов На странице Add Role (Добавить роль) мы должны создать следующие роли: USER; ADMIN. В конечном итоге список ролей должен выглядеть, как показано на рис. 9.10. Рис. 9.10. Роли Keycloak для приложения O-stock: USER и ADMIN Теперь, выполнив базовые настройки клиента, перейдем на страницу Credentials (Учетные данные). На этой странице отображается все необходимое для аутентификации клиента (рис. 9.11). Щелкните на вкладке Credentials (Учетные данные) Ключ клиентского приложения Рис. 9.11. Секретный ключ клиента O-stock на странице Credentials (Учетные данные) Следующий наш шаг – создание ролей пространства. Роли пространства помогают точнее управлять ролями отдельных пользователей. Это необязательный шаг. Вы можете пропустить его и создать пользователей напрямую. Но позже вы рискуете столкнуться со сложностями, когда понадобится определять и поддерживать роли для отдельных пользователей. Чтобы создать роли пространства, выберите пункт Roles (Роли) в меню слева, а затем щелкните на кнопке Add Role (Добавить роль) вверху справа. По аналогии с ролями клиентов создадим две роли пространства: ostock-user и ostock-admin. На рис. 9.12 и 9.13 показано, как создать роль пространства ostock-admin. Начнем с малого: использование Spring и Keycloak для защиты... 307 Рис. 9.12. Создание роли пространства ostock-admin Включите поддержку составных ролей Выберите клиента ostock Выберите роль ADMIN Рис. 9.13. Дополнительные настройки роли пространства ostock-admin Закончив определение роли пространства ostock-admin, повторите те же шаги и создайте роль ostock-user. По окончании у вас должен получиться список, как показано на рис. 9.14. 308 Глава 9 Безопасность микросервисов Рис. 9.14. Список ролей для пространства spmia-realm 9.3.4. Настройка пользователей O-stock Теперь, определив роли, имена и ключи на уровнях приложения и пространства, перейдем к настройке отдельных учетных записей и ролей пользователей. Чтобы создать учетную запись пользователя, выберите пункт Users (Пользователи) в меню слева в консоли администратора Keycloak. Для примеров в этой главе мы определим две учетные записи: illary.huaylupo и john.carnell. Учетной записи john.carnell мы назначим роль ostock-user, а учетной запись illary.huaylupo – роль ostockadmin. На рис. 9.15 показана страница добавления учетной записи пользователя. Здесь нужно ввести имя пользователя и включить флаги User Enabled (Пользователь активен) и Email Verified (Электронная почта проверена). Рис. 9.15. Страница Add User (Добавить пользователя) Начнем с малого: использование Spring и Keycloak для защиты... 309 ПРИМЕЧАНИЕ. Keycloak позволяет добавлять дополнительные атрибуты в учетные записи, такие как имя, фамилия, электронный адрес, почтовый адрес, дата рождения, номер телефона и многие другие. Но в этом примере мы ограничимся только самым необходимым. После сохранения этой формы щелкните на вкладке Credentials (Учетные данные). Здесь нужно ввести пароль пользователя, выключить флажок Temporary (Временный) и щелкнуть на кнопке Set Password (Установить пароль). На рис. 9.16 показано, как должна выглядеть эта страница после выполнения необходимых настроек. Выберите OFF (Выкл.) Рис. 9.16. Настройка пароля пользователя и выключение признака Temporary (Временный) После установки пароля перейдем на вкладку Role Mappings (Назначение ролей) и назначим пользователю конкретную роль. Этот шаг показан на рис. 9.17. 310 Глава 9 Безопасность микросервисов Рис. 9.17. Назначение ролей пространства созданному пользователю Наконец, повторите те же шаги для пользователя john.carnell, назначив ему роль ostock-user. 9.3.5. Аутентификация пользователей приложения O-stock На данный момент мы выполнили все необходимые настройки сервера Keycloak для аутентификации приложения и пользователей с использованием пароля. Давайте запустим службу аутентификации. Для этого выберите пункт Realm Settings (Настройки области) в меню слева, а затем щелкните на ссылке OpenID Endpoint Configuration (Конфигурация конечной точки OpenID), чтобы получить список конечных точек, доступных для нашего пространства. Эти шаги показаны на рис. 9.18 и 9.19. 1. Выберите пункт Realm Settings (Настройки области) 2. Щелкните на ссылке OpenID Endpoint Configuration (Конфигурация конечной точки OpenID) Рис. 9.18. Выбор ссылки OpenID Endpoint Configuration (Конфигурация конечной точки OpenID) Начнем с малого: использование Spring и Keycloak для защиты... 311 Скопируйте адрес конечной точки token Рис. 9.19. Выбор конечной точки Теперь сымитируем подключение пользователя к приложению и получим токен доступа. Для этого мы используем Postman и отправим HTTP-запрос POST конечной точке http://keycloak:8080/ auth/realms/spmia-realm/protocol/openid-connect/token, а затем введем имя приложения, секретный ключ, идентификатор пользователя и пароль. ПРИМЕЧАНИЕ. В этом примере мы используем порт 8080, потому что именно этот порт мы указали в файле docker-compose.yml для Keycloak. Чтобы смоделировать получение пользователем токена аутентификации, нужно настроить в Postman отправку имени приложения и секретного ключа. Для этого используем базовую аутентификацию. На рис. 9.20 показано, как настроить Postman для выполнения вызова базовой аутентификации. Обратите внимание, что мы использовали ранее определенное имя приложения и секретный ключ в качестве пароля: Username: <CLIENT_APPLICATION_NAME> Password: <CLIENT_APPLICATION_SECRET> 1. Конечная точка token 2. Введите идентификатор клиента, который был определен прежде в настройках Keycloak 3. Введите секретный ключ клиента, который можно увидеть на вкладке Credentials (Учетные данные) Рис. 9.20. Настройка запроса базовой аутентификации с использованием идентификатора и секретного ключа приложения 312 Глава 9 Безопасность микросервисов Однако мы еще не готовы выполнить вызов для получения токена. Кроме имени приложения и секретного ключа службе необходимо также передать следующую информацию в параметрах HTTP-формы: grant_type – тип разрешения; в этом примере мы будем использовать пароль; username – имя пользователя, выполняющего вход; password – пароль пользователя, выполняющего вход. На рис. 9.21 показано, как настроить эти параметры в HTTP-форме. 1. Введите password как значение поля grant_type 2. Введите имя пользователя и пароль Рис. 9.21. Для получения токена доступа учетные данные пользователя передаются конечной точке /openid-connect/token в виде параметров HTTP-формы В отличие от других вызовов REST в этой книге эти параметры передаются не в виде тала запроса в формате JSON. Стандарт аутентификации требует, чтобы параметры передавались конечной точке, генерирующей токены, в виде HTTP-формы. На рис. 9.22 показан ответ в формате JSON, возвращаемый вызовом конечной точки /openid-connect/token. Ответ JSON содержит пять атрибутов: access_token – токен доступа, который будет передаваться с каждым вызовом защищенного ресурса; token_type – спецификация авторизации позволяет определить несколько типов токенов. Чаще всего используется токен «bearer» – токен на предъявителя (мы не будем рассматривать другие типы токенов в этой главе); refresh_token – токен обновления можно повторно передать серверу авторизации для повторной выдачи токена доступа после истечения срока его действия; expires_in – количество секунд до истечения срока действия токена доступа. По умолчанию токен авторизации в Spring действует 12 ч; scope – определяет область действия токена доступа. Теперь, получив от сервера авторизации действительный токен доступа, мы можем декодировать JWT с помощью https://jwt.io, Начнем с малого: использование Spring и Keycloak для защиты... 313 чтобы получить всю информацию о токене доступа. На рис. 9.23 показаны результаты декодирования JWT. Это поле ключа. Токен доступа – это токен аутентификации, который передается с каждым вызовом Время в секундах, через которое истечет срок действия токена доступа Тип сгенерированного токена доступа Область действия токена Токен, который должен передаваться по истечении срока действия токена доступа для его обновления Рис. 9.22. Ответ в формате JSON, полученный после успешной проверки учетных данных клиента Роль пространства Роль клиента Имя пользователя Рис. 9.23. Получение информации о пользователе на основе действительного токена доступа 314 Глава 9 Безопасность микросервисов 9.4.Защита службы организаций с использованием Keycloak После регистрации клиента на сервере Keycloak и настройки учетных записей пользователей с ролями можно переходить к изучению особенностей защиты ресурсов с помощью Spring Security и Spring Boot Adapter Keycloak. Сервер Keycloak отвечает за создание токенов доступа и управление ими, а Spring – за определение разрешений для ролей пользователей на уровне отдельных служб. Чтобы настроить защищенный ресурс, нужно выполнить следующие действия: добавить соответствующие JAR-файлы Spring Security и Keycloak в службу, которую требуется защитить; связать службу с сервером Keycloak; определить, кто может обращаться к службе. Начнем с простейшего примера настройки защищенного ресурса. Для этого возьмем службу организаций и обеспечим возможность ее вызова только аутентифицированными пользователями. 9.4.1.Добавление в службы JAR-файлов Spring Security и Keycloak Как это часто бывает с микросервисами Spring, мы должны добавить пару зависимостей в файл конфигурации сборки службы организаций: organization-service/pom.xml, как показано в листинге 9.2. Листинг 9.2. Настройка зависимостей Keycloak и Spring Security // Часть файла pom.xml опущена для краткости <dependency> <groupId>org.keycloak</groupId> Зависимость <artifactId> Keycloak Spring Boot keycloak-spring-boot-starter </artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId> spring-boot-starter-security Зависимость Spring </artifactId> Security Starter </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> Защита службы организаций с использованием Keycloak 315 <scope>import</scope> </dependency> <dependency> <groupId>org.keycloak.bom</groupId> <artifactId> keycloak-adapter-bom Управление зависимостью </artifactId> Keycloak Spring Boot <version>11.0.2</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> 9.4.2. Настройка связи службы с сервером Keycloak После настройки службы организаций в качестве защищенного ресурса каждый раз при обращении к ней вызывающий должен включать HTTP-заголовок Authorization с токеном доступа типа Bearer Token. Получив запрос, защищенный ресурс должен обратиться к серверу Keycloak, чтобы подтвердить действительность токена. В листинге 9.3 показана необходимая конфигурация Keycloak. Добавьте эти настройки в файл свойств приложения службы организаций, находящийся в репозитории сервера конфигурации. Листинг 9.3. Настройки связи с Keycloak в файле organizationservice.properties // Некоторые свойства опущены для краткости Имя пространства keycloak.realm = spmia-realm URL конечной точки Auth сервера Keycloak: keycloak.auth-server-url = http://<keycloak_server_url>/auth http://keycloak:8080/auth keycloak.ssl-required = external Идентификатор клиента keycloak.resource = ostock keycloak.credentials.secret = 5988f899-a5bf-4f76-b15f-f1cd0d2c81ba Секретный ключ клиента keycloak.use-resource-role-mappings = true keycloak.bearer-only = true ПРИМЕЧАНИЕ. Чтобы упростить примеры в этой книге, в качестве репозитория мы используем путь поиска классов (classpath). Полный конфигурационный файл вы найдете в /configserver/ src/main/resources/config/organization-service.properties. 9.4.3.Определение пользователей, кому разрешено обращаться к службе Теперь мы готовы определить правила доступа к службе. Для этого нужно расширить класс KeycloakWebSecurityConfigurerAdapter и переопределить следующие методы: Глава 9 Безопасность микросервисов 316 configure(); configureGlobal(); sessionAuthenticationStrategy(); KeycloakConfigResolver(). Класс SecurityConfig службы организаций определен в файле /organization-service/ src/main/java/com/optimagrowth/ organization/config/SecurityConfig.java. В листинге 9.4 приводится фрагмент определения этого класса. Листинг 9.4. Класс SecurityConfig, расширяющий KeycloakWebSecurit yConfigurerAdapter // Некоторые свойства опущены для краткости Класс должен быть отмечен аннотацией @Configuration Применяет конфигурацию к глобальному WebSecurity Включает аннота@EnableWebSecurity цию @RoleAllowed @EnableGlobalMethodSecurity(jsr250Enabled = true) @Configuration public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter { Расширяет класс KeycloakWebSecurityConfigurerAdapter @Override protected void configure(HttpSecurity http) throws Exception { Регистрирует провайдера аутентификаsuper.configure(http); ции Keycloak http.authorizeRequests() .anyRequest() .permitAll(); http.csrf().disable(); } Определяет страте@Autowired гию аутентификации public void configureGlobal( сеанса AuthenticationManagerBuilder auth) throws Exception { KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider(); keycloakAuthenticationProvider.setGrantedAuthoritiesMapper( new SimpleAuthorityMapper()); auth.authenticationProvider(keycloakAuthenticationProvider); } @Bean @Override Определяет стратегию аутентификации protected SessionAuthenticationStrategy сеанса sessionAuthenticationStrategy() { return new RegisterSessionAuthenticationStrategy( new SessionRegistryImpl()); } Защита службы организаций с использованием Keycloak 317 @Bean По умолчанию Spring Security public KeycloakConfigResolver Adapter ищет файл keycloak.json KeycloakConfigResolver() { return new KeycloakSpringBootConfigResolver(); } } Правила доступа могут иметь разный уровень детализации – от обобщенного (любой аутентифицированный пользователь может получить доступ к службе) до узко специализированного (разрешен доступ только приложению с этой ролью, только к этому URL и только через запросы DELETE). Мы не будем обсуждать все возможные в Spring Security варианты определения правил доступа и рассмотрим лишь несколько наиболее распространенных примеров, которые разрешают доступ к защищенному ресурсу только: аутентифицированным пользователям; пользователям с определенной ролью. Разрешение доступа только аутентифицированным пользователям Первый пример, который мы рассмотрим, – защита службы организаций так, чтобы к ней могли обращаться только аутентифицированные пользователи. В листинге 9.5 показано, как реализовать это правило в классе SecurityConfig.java. Листинг 9.5. Разрешение доступа только аутентифицированным пользователям package com.optimagrowth.organization.security; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import ➥ org.springframework.security.config.annotation.web.builders.HttpSecurity; @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(jsr250Enabled = true) public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { Объект HttpSecurity, который передается super.configure(http); методу, определяет все правила доступа http.authorizeRequests() .anyRequest().authenticated(); } } Все правила доступа определяются внутри метода configure(). Для определения своих правил мы будем использовать класс HttpSecurity, полученный от фреймворка Spring. В этом примере доступ Глава 9 Безопасность микросервисов 318 к любому URL в службе организаций разрешается только аутентифицированным пользователям. Предположим, что мы решили послать службе организаций запрос без токена доступа в HTTP-заголовке. В этом случае мы получим код ответа HTTP 401 и сообщение о том, что для доступа к службе требуется полная аутентификация. На рис. 9.24 показан результат вызова службы организаций без HTTP-заголовка Authorization. Получен код ответа HTTP 401 Рис. 9.24. Результат вызова службы организаций без токена доступа в HTTPзаголовке Теперь попробуем вызвать службу организаций, включив токен доступа в запрос. (Как сгенерировать токен доступа, рассказывалось в разделе 9.2.5.) Мы можем просто скопировать значение access_token из ответа в формате JSON, полученного от конечной точки /openid-connect/token и вставить его в наш вызов службы организаций. Не забывайте, что, вызывая службу организаций, мы вместе со значением access_token должны установить тип авторизации Bearer Token. На рис. 9.25 показан результат вызова службы организаций, на этот раз с токеном доступа. Тип авторизации Bearer Token Токен доступа был передан в заголовке Рис. 9.25. Результат вызова службы организаций с токеном доступа Защита службы организаций с использованием Keycloak 319 Это, вероятно, один из самых простых вариантов защиты конечной точки с помощью JWT. Далее мы возьмем за основу этот пример и разрешим доступ к определенной конечной точке только определенной роли. Разрешение доступа только определенной роли В следующем примере мы разрешим выполнять запросы DELETE к нашей службе организаций только пользователям с ролью ADMIN. Как вы помните, в разделе 9.2.4 мы создали две учетные записи для доступа к службам O-stock: illary.huaylupo и john.carnell. Учетной записи john.carnell была назначена роль USER, а учетной записи illary.huaylupo – роли USER и ADMIN. Используя аннотацию @RolesAllowed в контроллере, мы можем разрешить выполнять некоторые методы только определенным ролям. В листинге 9.6 показано, как это сделать. Листинг 9.6. Использование аннотации @RolesAllowed в OrganizationController.java package com.optimagrowth.organization.security; // Инструкции импорта опущены для краткости @RestController @RequestMapping(value="v1/organization") public class OrganizationController { @Autowired private OrganizationService service; Указывает, что это действие могут выполнять только пользователи с ролями USER и ADMIN @RolesAllowed({ "ADMIN", "USER" }) @RequestMapping(value="/{organizationId}",method = RequestMethod.GET) public ResponseEntity<Organization> getOrganization( @PathVariable("organizationId") String organizationId) { return ResponseEntity.ok(service.findById(organizationId)); } @RolesAllowed({ "ADMIN", "USER" }) @RequestMapping(value="/{organizationId}",method = RequestMethod.PUT) public void updateOrganization( @PathVariable("organizationId") String id, @RequestBody Organization organization) { service.update(organization); } @RolesAllowed({ "ADMIN", "USER" }) @PostMapping public ResponseEntity<Organization> saveOrganization( @RequestBody Organization organization) { return ResponseEntity.ok(service.create(organization)); } Глава 9 Безопасность микросервисов 320 @RolesAllowed("ADMIN") Указывает, что это действие могут выполнять только пользователи с ролью ADMIN @DeleteMapping(value="/{organizationId}") @ResponseStatus(HttpStatus.NO_CONTENT) public void deleteLicense(@PathVariable("organizationId") String organizationId) { service.delete(organizationId); } } Чтобы получить токен для john.carnell (password: password1), нужно снова отправить запрос POST конечной точке openid-connect/token. Теперь, имея новый токен доступа, попробуем отправить запрос DELETE службе организаций: http://localhost:8072/organization/ v1/organization/dfd13002-57c5-47ce-a4c2-a1fda2f51513. В ответ мы получим код HTTP 403 (Forbidden) и сообщение о том, что доступ к этой службе был запрещен. Вот как выглядит полученный обратно ответ в формате JSON: { "timestamp": "2020-10-19T01:19:56.534+0000", "status": 403, "error": "Forbidden", "message": "Forbidden", "path": "/v1/organization/4d10ec24-141a-4980-be34-2ddb5e0458c7" } ПРИМЕЧАНИЕ. Мы используем порт 8072, потому что именно его указали в конфигурации Spring Cloud Gateway в предыдущей главе (в частности, в файле конфигурации gateway-server.yml, находящемся в репозитории службы Spring Cloud Configuration). Если попробовать выполнить тот же вызов, используя учетную запись illary.huaylupo (password: password1) и соответствующий токен доступа, то в ответ мы получим код HTTP 204 (No Content), сообщающий об успешной обработке запроса. Теперь мы знаем, как с помощью Keycloak защитить отдельную службу (в данном случае службу организаций). Однако в микросервисных архитектурах в рамках одной транзакции часто требуется вызвать несколько служб. В таких ситуациях мы должны обеспечить передачу токена доступа между службами. 9.4.4. Передача токена доступа Чтобы показать, как реализуется передача токена между службами, мы дополнительно защитим службу лицензий. Как вы помните, служба лицензий вызывает службу организаций для получения информации об указанной организации. Возникает вопрос: как передать токен от одной службы к другой? Защита службы организаций с использованием Keycloak 321 Мы создадим простой пример, в котором служба лицензий будет вызывать службу организаций. Если вы следовали за примерами в предыдущих главах, то знаете, что обе службы работают за шлюзом. На рис. 9.26 показано, как токен аутентифицированного пользователя будет передаваться через шлюз в службу лицензий и далее в службу организаций. 1. Веб-приложение O-stock вызывает службу лицензий (находящуюся за шлюзом) Пользователи и добавляет токен доступа имеют токен в HTTP-заголовок Authorization доступа 2. Шлюз отыскивает службу лицензий и пересылает ей вызов с заголовком Authorization Сервер Keycloak Служба лицензий Пользователь Веб-клиент O-stock Веб-приложение O-stock Spring Cloud Gateway 3. Служба лицензий проверяет токен с помощью службы аутентификации и передает его службе организаций 4. Служба организаций тоже проверяет токен с помощью службы аутентификации Служба организаций Рис. 9.26. Токен доступа должен передаваться по всей цепочке вызовов Вот краткое описание происходящего на рис. 9.26. Цифры в скобках соответствуют цифрам на рисунке: пользователь уже прошел аутентификацию на сервере Keycloak и вызывает веб-приложение O-stock. Токен доступа хранится в сеансе пользователя. Веб-приложению O-stock необходимо получить некоторые данные о лицензиях и вызвать конечную точку REST службы лицензий (1). Формируя вызов, веб-приложение O-stock добавляет токен доступа в HTTP-заголовок Authorization. Служба лицензий находится за шлюзом Spring Cloud Gateway; шлюз определяет местоположение конечной точки службы лицензий и отправляет вызов одному из серверов службы лицензий (2). Шлюз копирует HTTP-заголовок Authorization из входящего вызова и пересылает его конечной точке службы; служба лицензий принимает входящий запрос. Будучи защищенным ресурсом, служба лицензий проверяет токен на сервере Keycloak (3) и оценивает роли пользователя на наличие соответствующих разрешений. Выполняя свою бизнес-логику, служба лицензий вызывает службу организаций. При этом она должна передать токен доступа пользователя вместе с вызовом; Глава 9 Безопасность микросервисов 322 получив вызов, служба организаций извлекает HTTP-заголовок Authorization и проверяет токен на сервере Keycloak (4). Чтобы реализовать эти шаги, нужно внести несколько изменений в наш код. Если этого не сделать, то при попытке запросить информацию об организации служба лицензий получит следующую ошибку: message": "401 : {[status:401, error: Unauthorized message: Unauthorized, path: /v1/organization/d898a142-de44-466c-8c88-9ceb2c2429d3}] Первый шаг – изменить реализацию шлюза так, чтобы он передавал токен доступа в службу лицензий. По умолчанию шлюз не пересылает конфиденциальные HTTP-заголовки, такие как Cookie, SetCookie и Authorization, в нижестоящие службы. Чтобы разрешить передачу HTTP-заголовка Authorization, нужно в конфигурационном файле gateway-server.yml, находящемся в репозитории Spring Cloud Config, добавить в каждый маршрут следующий фильтр: - RemoveRequestHeader= Cookie,Set-Cookie Это «черный список» конфиденциальных заголовков, которые шлюз не должен передавать нижестоящим службам. Отсутствие заголовка Authorization в списке RemoveRequestHeader означает, что шлюз будет пропускать этот заголовок. Если не определить это свойство явно, то шлюз автоматически заблокирует передачу всех трех заголовков (Cookie, Set-Cookie и Authorization). Затем нужно настроить службу лицензий, подключив к ней зависимости Keycloak и Spring Security, и настроить правила авторизации. Наконец, нужно добавить свойства Keycloak в файл свойств приложения на сервере конфигурации. Настройка службы лицензий Первый шаг, необходимый для получения службой токенов доступа, – добавить зависимости в файл сборки pom.xml службы лицензий, как показано в листинге 9.7. Листинг 9.7. Настройка зависимостей Keycloak и Spring Security // Часть файла pom.xml опущена для краткости <dependency> <groupId>org.keycloak</groupId> <artifactId>keycloak-spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> </dependencies> Защита службы организаций с использованием Keycloak 323 <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>org.keycloak.bom</groupId> <artifactId>keycloak-adapter-bom</artifactId> <version>11.0.2</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> Следующий шаг – защита службы лицензий, т. е. проверка токена доступа пользователя. В листинге 9.8 показан класс SecurityConfig, который находится в файле /licensing-service/src/main/java/ com/optimagrowth/license/config/SecurityConfig.java. Листинг 9.8. Разрешение доступа только аутентифицированным пользователям // Часть определения класса опущена для краткости @Configuration @EnableWebSecurity public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { super.configure(http); http.authorizeRequests() .anyRequest().authenticated(); http.csrf().disable(); } @Override protected void configure(HttpSecurity http) throws Exception { super.configure(http); http.authorizeRequests() .anyRequest().authenticated(); http.csrf().disable(); } @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { KeycloakAuthenticationProvider keycloakAuthenticationProvider = Глава 9 Безопасность микросервисов 324 keycloakAuthenticationProvider(); keycloakAuthenticationProvider.setGrantedAuthoritiesMapper( new SimpleAuthorityMapper()); auth.authenticationProvider(keycloakAuthenticationProvider); } @Bean @Override protected SessionAuthenticationStrategy sessionAuthenticationStrategy() { return new RegisterSessionAuthenticationStrategy( new SessionRegistryImpl()); } @Bean public KeycloakConfigResolver KeycloakConfigResolver() { return new KeycloakSpringBootConfigResolver(); } } Заключительный шаг в настройке службы лицензий – добавление конфигурации Keycloak в файл licensing-service.properties. В листинге 9.9 показано, как должен выглядеть этот файл. Листинг 9.9. Настройка Keycloak в licensing-service.properties // Некоторые свойства опущены для краткости keycloak.realm = spmia-realm keycloak.auth-server-url = http://keycloak:8080/auth keycloak.ssl-required = external keycloak.resource = ostock keycloak.credentials.secret = 5988f899-a5bf-4f76-b15f-f1cd0d2c81ba keycloak.use-resource-role-mappings = true keycloak.bearer-only = true Теперь, настроив шлюз для передачи заголовка Authorization и подготовив службу лицензий, мы должны организовать передачу токена доступа по цепочке. Для этого нужно изменить вызов службы организаций в службе лицензий и вставить в вызов HTTPзаголовок Authorization. Без Spring Security нам пришлось бы написать сервлетный фильтр, чтобы извлечь нужный HTTP-заголовок из входящего вызова, и затем вручную добавлять этот заголовок во все исходящие вызовы. Keycloak предоставляет новый класс шаблона REST, который реализует все это. Класс называется KeycloakRestTemplate. Однако этот класс нужно объявить bean-компонентом, чтобы его можно было можно автоматически внедрить в службу, вызывающую другие защищенные службы. Мы сделаем это в файле /licensing-service/ src/main/java/com/optimagrowth/license/config/SecurityConfig. java, как показано в листинге 9.10. Защита службы организаций с использованием Keycloak 325 Листинг 9.10. Экспортирование KeycloakRestTemplate в SecurityConfig package com.optimagrowth.license.service.client; // Часть определения класса опущена для краткости @ComponentScan(basePackageClasses = KeycloakSecurityComponents.class) public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter { ... @Autowired public KeycloakClientRequestFactory keycloakClientRequestFactory; @Bean @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) public KeycloakRestTemplate keycloakRestTemplate() { return new KeycloakRestTemplate(keycloakClientRequestFactory); } ... } Чтобы увидеть, как действует класс KeycloakRestTemplate, можно заглянуть в класс OrganizationRestTemplateClient, который находится в файле /licensing-service/src/main/java/com/optimagrowth/ license/service/client/OrganizationRestTemplateClient.java. В листинге 9.11 показано, как KeycloakRestTemplate автоматически внедряется в этот класс. Листинг 9.11. Использование KeycloakRestTemplate для передачи токена доступа package com.optimagrowth.license.service.client; // Инструкции импортирования опущены для краткости @Component public class OrganizationRestTemplateClient { @Autowired KeycloakRestTemplate restTemplate; KeycloakRestTemplate – это прямая замена стандартному классу RestTemplate. Реализует отправку токена доступа public Organization getOrganization(String organizationId){ ResponseEntity<Organization> restExchange = restTemplate.exchange("http://gateway:8072/organization/ v1/organization/{organizationId}", HttpMethod.GET, null, Organization.class, organizationId); return restExchange.getBody(); } } Вызов службы организаций выполняется точно так же, как и с использованием стандартного класса RestTemplate. Здесь мы указываем адрес сервера шлюза 326 Глава 9 Безопасность микросервисов Чтобы протестировать этот код, можно послать запрос службе лицензий, которая в свою очередь вызовет службу организаций. Например, следующий вызов к службе лицензий запрашивает информацию об определенной лицензии, а служба лицензий в свою очередь вызывает службу организаций для получения сведений об организации. На рис. 9.27 показан результат этого вызова: http://localhost:8072/license/v1/organization/d898a142-de44-466c-8c88➥ 9ceb2c2429d3/license/f2a9c9d4-d2c0-44fa-97fe-724d77173c62 Рис. 9.27. Передача токена доступа из службы лицензий в службу организаций 9.4.5. Анализ нестандартного поля в JWT В этом разделе мы посмотрим, как наш шлюз анализирует нестандартное поле в JWT. В частности, мы изменим класс TrackingFilter, который был представлен в главе 8, и реализуем декодирование поля preferred_username из JWT, проходящего через шлюз. Для этого мы под- Защита службы организаций с использованием Keycloak 327 ключим библиотеку парсера JWT в файле pom.xml сервера шлюза. Существует несколько парсеров токенов, но мы выбрали кодек Apache Commons Codec и пакет org.json для анализа текста в формате JSON. <dependency> <groupId>commons-codec</groupId> <artifactId>commons-codec</artifactId> </dependency> <dependency> <groupId>org.json</groupId> <artifactId>json</artifactId> <version>20190722</version> </dependency> После подключения библиотек можно добавить новый метод с именем getUsername() в фильтр трассировки. Этот метод показан в листинге 9.12. Полный исходный код этого метода вы найдете в файле /gatewayserver/src/main/java/com/optimagrowth/ gateway/filters/TrackingFilter.java. Листинг 9.12. Анализ поля preferred_username в JWT // Часть кода опущена для краткости private String getUsername(HttpHeaders requestHeaders){ String username = ""; if (filterUtils.getAuthToken(requestHeaders)!=null){ String authToken = Анализирует токен filterUtils.getAuthToken(requestHeaders) из HTTP-заголовка Authorization .replace("Bearer ",""); JSONObject jsonObj = decodeJWT(authToken); try { Извлекает preferred_username из JWT username = jsonObj.getString("preferred_username"); }catch(Exception e) {logger.debug(e.getMessage());} } return username; } Использует кодировку Base64 для анализа токена, передавая ключ, который подписывает токен private JSONObject decodeJWT(String JWTToken) { String[] split_string = JWTToken.split("\\."); String base64EncodedBody = split_string[1]; Base64 base64Url = new Base64(true); String body = new String(base64Url.decode(base64EncodedBody)); JSONObject jsonObj = new JSONObject(body); Преобразует тело JWT return jsonObj; в объект JSON, чтобы получить preferred_ } username Чтобы опробовать этот пример, нужно присвоить переменной AUTH_TOKEN в FilterUtils значение Authorization, как в следующем фрагменте кода. (Полный исходный код вы найдете в /gatewayserver/ src/main/java/com/optimagrowth/gateway/filters/FilterUtils.java.) 328 Глава 9 Безопасность микросервисов public static final String AUTH_TOKEN = "Authorization"; Реализовав функцию getUsername(), мы можем добавить вызов System.out.println в метод filter() в фильтре трассировки, чтобы вывести результат анализа поля preferred_username из нашего JWT, проходящего через шлюз. Теперь, послав запрос шлюзу, мы увидим в консоли значение preferred_username. ПРИМЕЧАНИЕ. Чтобы послать этот вызов, необходимо определить все параметры в форме HTTP, включая HTTP-заголовок Authorization и JWT. Если все прошло успешно, в консоли должны появиться следующие строки: tmx-correlation-id found in tracking filter: 26f2b2b7-51f0-4574-9d84-07e563577641. The authentication name from the token is : illary.huaylupo 9.5.Некоторые заключительные рассуждения о безопасности микросервисов В этой главе вы познакомились с OpenID, OAuth2 и спецификацией Keycloak, а также увидели, как использовать Spring Cloud Security в комплексе с Keycloak для реализации службы аутентификации и авторизации. Однако Keycloak является лишь частью мозаики безопасности микросервисов. По мере подготовки своих микросервисов для промышленной эксплуатации вы также должны: использовать HTTPS/Secure Sockets Layer (SSL) для любых взаимодействий между службами; использовать шлюз для организации доступа ко всем своим службам; определить зоны доступности ваших служб (например, общедоступный API и закрытый API); ограничить поверхность атаки на ваши микросервисы, заблокировав ненужные сетевые порты. На рис. 9.28 показано, как эти практики сочетаются друг с другом. Каждому пункту в этом списке соответствует подпись с номером на рис. 9.28. Каждую из перечисленных практик мы вкратце рассмотрим в следующих разделах. Некоторые заключительные рассуждения о безопасности микросервисов 329 4. Заблокируйте ненужные порты, 3. Разделите службы на чтобы ограничить поверхность общедоступные и закрытые 2. Вызовы должны атаки на микросервисы передаваться службам через шлюз Общедоступный API Закрытый API Служба Служба аутентификации аутентификации Служба лицензий Веб-приложение O-stock Данные приложения HTTPS HTTPS Общедоступный шлюз Общедоступный API Закрытый шлюз Служба Данные организаций приложения 1. Используйте HTTPS/SSL для взаимодействий между службами Рис. 9.28. Безопасность микросервисной архитектуры – это больше, чем просто аутентификация и авторизация 9.5.1.Используйте HTTPS/Secure Sockets Layer (SSL) для взаимодействий между службами Во всех примерах кода в этой книге мы использовали HTTP, потому что HTTP – это простой протокол и его не нужно настраивать для каждой службы перед тем, как начать пользоваться этой службой. Однако в промышленном окружении микросервисы должны взаимодействовать только через зашифрованные каналы, используя HTTPS и SSL. Обратите внимание, что конфигурацию и настройку HTTPS можно автоматизировать с помощью сценариев DevOps. 9.5.2.Используйте шлюз для организации доступа к микросервисам Отдельные серверы, конечные точки служб и порты, на которых выполняются и которые используют ваши службы, никогда не должны быть напрямую доступны клиентам. Вместо этого используйте шлюз в качестве единой точки входа во все ваши службы. Настройте сетевой уровень в операционной системе или контейнере, где работают ваши микросервисы, так, чтобы он принимал трафик только от шлюза. Помните, что шлюз может действовать как точка применения политик (Policy Enforcement Point, PEP) ко всем службам. Размещение служб за шлюзом позволит вам последовательно защищать и контролировать свои службы. Шлюз также позволяет заблокировать прямой доступ к портам и конечным точкам служб, которые вы собираетесь открыть внешнему миру. 330 Глава 9 Безопасность микросервисов 9.5.3. Разделите свои службы на общедоступные и закрытые В общем случае обеспечение безопасности сводится к организации уровней доступа и соблюдению концепции наименьших привилегий. Под наименьшими привилегиями подразумевается, что пользователь должен иметь ровно столько привилегий, сколько необходимо для выполнения повседневной работы. С этой целью вы должны реализовать минимальные привилегии, разделив свои службы на две отдельные зоны: общедоступную и закрытую. Общедоступная зона содержит все общедоступные API, которые будут использоваться вашими клиентами (в этой книге таким общедоступным API, например, является приложение O-stock). Микросервисы, обслуживающие общедоступный API, должны решать узкие задачи, ориентированные на рабочий процесс. Эти микросервисы, как правило, являются агрегаторами служб, извлекающими данные, и использующими несколько нижестоящих служб. Общедоступные микросервисы также должны находиться за собственным шлюзом и иметь собственную службу аутентификации и авторизации. Доступ к общедоступным службам со стороны клиентских приложений должен осуществляться по единому маршруту, защищенному шлюзом. Закрытая зона действует как стена, защищающая основные функции и данные приложения. Службы в этой зоне должны быть доступны только через один хорошо известный порт, и этот порт должен быть закрыт для внешнего сетевого трафика. Частная зона должна иметь свой шлюз и свою службу аутентификации. Общедоступные службы должны проходить аутентификацию в службе аутентификации закрытой зоны. Все данные приложения должны находиться в подсети закрытой зоны и быть доступными только для микросервисов, находящихся в закрытой зоне. 9.5.4.Ограничьте поверхность атаки на ваши микросервисы, заблокировав ненужные сетевые порты Многие разработчики забывают заблокировать порты, ненужные для работы их служб. Настройте операционную систему, в которой работает ваша служба, чтобы для входящего и исходящего трафика были доступны только порты или части инфраструктуры, необходимые службе (мониторинг, агрегирование журналов). Не сосредотачивайтесь исключительно на входящем трафике. Многие разработчики забывают заблокировать исходящие порты. Блокирование исходящих портов может предотвратить утечку данных из вашей службы, если злоумышленник скомпрометирует ее. Кроме того, блокируйте доступ к сетевым портам не только в общедоступной зоне, но и в закрытой. Некоторые заключительные рассуждения о безопасности микросервисов 331 Итоги OAuth2 – это фреймворк безопасности на основе токенов, который предоставляет различные механизмы для защиты вебслужб. Эти механизмы называются грантами, или разрешениями на авторизацию. OpenID Connect (OIDC) – это слой поверх инфраструктуры OAuth2, который обеспечивает аутентификацию и передачу идентификационной информации (удостоверения) о вошедшем в приложение. Keycloak – решение с открытым исходным кодом для управления идентификацией и доступом для микросервисов и приложений. Основная цель Keycloak – упростить защиту служб и приложений. Каждое приложение может иметь свое имя в Keycloak и секретный ключ. Каждая служба должна определять, какие действия может выполнять та или иная роль. Spring Cloud Security поддерживает спецификацию JSON Web Token (JWT). JWT позволяет добавлять свои нестандартные поля в спецификацию. Защита микросервисов – это больше чем просто аутентификация и авторизация. В промышленном окружении следует использовать HTTPS для шифрования всех вызовов между службами. Используйте сервисный шлюз, чтобы сократить количество точек доступа, через которые можно добраться до службы. Ограничьте поверхность атаки на микросервисы, заблокировав входящие и исходящие порты в системе, где работает служба. 10 Событийноориентированная архитектура и Spring Cloud Stream Эта глава: знакомит с особенностями событийно-ориентированных архитектур; рассказывает об использовании Spring Cloud Stream для упрощения обработки событий; описывает порядок настройки Spring Cloud Stream; показывает приемы публикации сообщений с использованием Spring Cloud Stream и Kafka; показывает приемы получения сообщений с использованием Spring Cloud Stream и Kafka; демонстрирует пример реализации распределенного кеша с использованием Spring Cloud Stream, Kafka и Redis. Люди находятся в постоянном движении, взаимодействуя с окружающими. Как правило, их общение не является синхронным, линейным и крайне редко укладывается в модель вопрос–ответ. Оно ближе к модели управления сообщениями, когда мы постоянно посылаем и получаем сообщения. Получая сообщения, мы реагируем на них, часто прерывая выполнение какой-то другой задачи, над которой работали. В этой главе рассказывается о приемах разработки и реализации микросервисов на основе Spring, взаимодействующих с другими микросервисами посредством асинхронных сообщений. Использование асинхронных сообщений не является чем-то новым. Новой является идея использования сообщений для передачи событий, Обмен сообщениями, событийно-ориентированная архитектура... 333 представляющих изменения в состоянии. Эта идея называется событийно-ориентированной архитектурой (Event-Driven Architecture, EDA). Она также известна как архитектура, управляемая сообщениями (Message-Driven Architecture, MDA). Подход на основе EDA позволяет создавать слабо связанные системы, способные реагировать на изменения, но не связанные тесно с конкретными библиотеками или службами. В сочетании с микросервисами событийно-ориентированная архитектура позволяет быстро добавлять новые функции в приложения, просто заставляя службы прослушивать потоки событий (сообщений), курсирующих в приложении. Проект Spring Cloud упрощает создание событийно-ориентированных решений, предлагая подпроект Spring Cloud Stream, который позволяет реализовать публикацию и прием сообщений и защищает наши службы от деталей реализации, связанных с базовой платформой обмена сообщениями. 10.1.Обмен сообщениями, событийноориентированная архитектура и микросервисы Почему обмен сообщениями играет важную роль в микросервисных архитектурах? Чтобы ответить на этот вопрос, рассмотрим простой пример, использовав две службы, которые мы разрабатывали на протяжении всей книги: лицензий и организаций. Представим, что после развертывания этих служб в промышленном окружении мы обнаружили, что запросы к службе лицензий обрабатываются очень долго из-за поиска информации в службе организаций. Исследовав шаблоны использования данных об организациях, мы выяснили, что данные меняются редко и большая часть данных, возвращаемых службой организаций, извлекается с использованием первичного ключа. Кешируя читаемые данные об организациях и избавившись от затрат на доступ к базе данных, мы могли бы значительно сократить время отклика службы лицензий. Чтобы реализовать это решение, необходимо учесть следующие три основных требования. 1 Кешированные данные должны быть согласованными во всех экземплярах службы лицензий. Это означает, что данные не могут кешироваться локально в рамках службы лицензий, потому что все экземпляры службы должны получать одни и те же данные. 2 Данные об организациях не могут кешироваться в памяти контейнера, где находится служба лицензий. Контейнеры со службами часто имеют ограниченный размер и могут получать данные, используя разные шаблоны доступа. Использование локального кеша может добавить сложности из-за необходимости его синхронизации со всеми другими службами в кластере. Глава 10 Событийно-ориентированная архитектура... 334 Когда данные об организациях изменяются или удаляются, служба лицензий должна распознавать изменение состояния в службе организаций. В ответ на это изменение служба лицензий должна объявить недействительными кешированные данные об этой конкретной организации и удалить их из кеша. Рассмотрим два подхода к реализации этих требований. Первый подход – реализация перечисленных выше требований с использованием синхронной модели запрос/ответ. При изменении данных об организации службы лицензий и организаций будут обмениваться данными через свои конечные точки REST. Второй подход – служба организаций будет генерировать асинхронное событие (сообщение), чтобы уведомить другие службы об изменении данных, и публиковать его в очереди, сообщая об изменение состояния – обновлении или удалении записи в хранилище организаций. Служба лицензий будет прослушивать посредника (брокера сообщений или очередь), чтобы проверить появление сообщения от службы организаций и удалить данные из своего кеша. 3 10.1.1.Передача событий об изменении состояния с использованием синхронного подхода запрос/ответ В роли базы данных, кеша и брокера сообщений используем Redis (https://redis.io/) – распределенное хранилище пар ключ/значение. На рис. 10.1 представлен обзор решения кеширования с использованием традиционной модели синхронного программирования запрос/ответ. Redis 2. Служба лицензий сначала проверяет наличие данных об организации в кеше Redis 3. Если данные об организации отсутствуют в кеше Redis, то служба лицензий вызывает службу организаций Служба лицензий Чтение данных Служба организаций Клиент службы организаций Клиент службы лицензий 1. Пользователь службы лицензий посылает запрос, чтобы получить данные о лицензии 5. Когда данные об организации изменяются, служба организаций вызывает конечную точку службы лицензий, чтобы удалить запись из кеша, или напрямую обращается к кешу службы лицензий 4. Данные об организации могут измениться в результате прямого вызова службы организаций Рис. 10.1. В синхронной модели запрос/ответ тесная связь между службами добавляет сложности в архитектуру и делает ее хрупкой Обмен сообщениями, событийно-ориентированная архитектура... 335 Как показано на рис. 10.1, когда пользователь вызывает службу лицензий, она должна получить данные об организации. Для этого служба лицензий сначала пытается получит информацию по идентификатору организации из кластера Redis. В случае неудачи служба лицензий вызовет службу организаций, используя конечную точку REST, и сохранит полученные данные в Redis, после чего вернет ответ пользователю. Если кто-то обновит или удалит запись об организации с помощью конечной точки REST службы организаций, то служба организаций должна будет вызвать конечную точку службы лицензий и сообщить ей о необходимости удалить данные об организации из кеша. Однако, внимательно исследовав решение, в котором служба организаций вызывает службу лицензий, чтобы сообщить о необходимости аннулировать запись в кеше Redis, можно увидеть по крайней мере три проблемы: службы организаций и лицензий оказываются тесно связанными. Эта связь добавляет хрупкости всей системе; если путь к конечной точке службы лицензий, с помощью которой аннулируются записи в кеше, изменится, то службу организаций тоже придется изменить. Теряется гибкость; мы не сможем добавить новых получателей данных об организациях, не изменив код службы организаций, чтобы гарантировать вызов службы лицензий при любых изменениях. Тесная связь между службами Служба лицензий зависит от службы организаций, так как получает от нее необходимые данные. Однако, добавив обратную связь между службой организаций и службой лицензий при изменении или удалении записи об организации, мы ввели зависимость службы организаций от службы лицензий (рис. 10.1). Чтобы аннулировать данные в кеше Redis, служба организаций должна обратиться к конечной точке службы лицензий или напрямую к серверу Redis, принадлежащему службе лицензий. Прямое взаимодействие службы организаций с Redis вводит другую проблему – управление хранилищем данных, принадлежащим другой службе. В микросервисных архитектурах это строжайше запрещено. Конечно, можно утверждать, что данные об организациях фактически принадлежат службе организаций, но служба лицензий использует их в определенном контексте и теоретически может преобразовывать эти данные или строить на их основе бизнес-правила. Если служба организаций будет напрямую обращаться к серверу Redis, то может случайно нарушить правила, установленные разработчиками службы лицензий. 336 Глава 10 Событийно-ориентированная архитектура... Хрупкость служб Тесная связь между службами лицензий и организаций также влечет нестабильность взаимодействий. Если служба лицензий не работает или работает медленно, это может повлиять на службу организаций, так как служба организаций теперь напрямую взаимодействует со службой лицензий. С другой стороны, если служба организаций будет напрямую взаимодействовать с хранилищем данных Redis службы лицензий, то мы создадим зависимость между службой организаций и Redis. В этом сценарии любая проблема с общим сервером Redis может привести к отключению обеих служб. Негибкость добавления новых получателей данных об организациях Если в модель, представленную на рис. 10.1, добавить еще одну службу, заинтересованную в получении уведомлений об изменении данных об организациях, то нам придется добавить еще один вызов из службы организации в эту новую службу. А это означает изменение кода и повторное развертывание службы организаций. Очень негибкое решение. При использовании синхронной модели запрос/ответ для уведомления об изменении состояния возникает сеть зависимостей между основными и вспомогательными службами в приложении. Центры этих сетей становятся основными точками отказа в приложении. Другой вид тесных связей Модель обмена сообщениями добавляет уровень косвенности между службами, однако ее применение тоже может привести к созданию тесной связи между двумя службами. Далее в этой главе мы реализуем обмен сообщениями между службами организаций и лицензий. Эти сообщения – объекты Java – будут преобразовываться в формат JSON перед отправкой и восстанавливаться после получения. Изменения в структуре сообщения JSON могут вызвать проблемы при обратном преобразовании в Java, если две службы не смогут корректно обрабатывать разные версии сообщений одного и того же типа. JSON не имеет встроенной поддержки управление версиями. Однако мы можем использовать Apache Avro (https://avro.apache.org/). Avro – это двоичный протокол со встроенной поддержкой управления версиями. Spring Cloud Stream позволяет использовать Apache Avro для обмена сообщениями. К сожалению, использование Avro выходит за рамки этой книги, но мы хотели, чтобы вы знали, что существует протокол, способный помочь в ситуациях, когда возникает потребность в управлении версиями форматов сообщений. Обмен сообщениями, событийно-ориентированная архитектура... 337 10.1.2.Передача событий об изменении состояния с использованием сообщений В этом разделе мы организуем передачу событий между службами организаций и лицензий, использовав подход на основе обмена сообщениями. Система обмена сообщениями будет использоваться службой организаций только для публикации любых изменений в данных, управляемых ею. Это решение изображено на рис. 10.2. Redis 1. Когда данные об организациях изменяются, служба организаций публикует сообщение в очереди Служба лицензий Служба организаций Очередь Клиент службы лицензий Клиент службы организаций 2. Cлужба лицензий наблюдает за очередью, извлекает любые сообщения, опубликованные службой организаций, и при необходимости аннулирует данные в кеше Redis Рис. 10.2. При изменении данных об организации в очередь сообщений, которая находится между службами лицензий и организаций, записывается уведомление В модели, изображенной на рис. 10.2, при изменении данных служба организаций публикует сообщение в очереди. Служба лицензий наблюдает за очередью, извлекает из нее все сообщения, опубликованные службой организаций, и при необходимости удаляет соответствующую запись из кеша Redis. Очередь сообщений в этой модели играет роль посредника между службами лицензий и организаций. Этот подход предлагает четыре преимущества: слабую связанность, надежность, масштабируемость и гибкость. Давайте рассмотрим каждое из них по очереди. Слабая связанность Приложение с микросервисной архитектурой может состоять из десятков небольших распределенных служб, взаимодействующих друг с другом и использующих данные друг друга. Как было показано выше на примере модели запрос/ответ, синхронный подход создает жесткую зависимость между службами лицензий и организаций. 338 Глава 10 Событийно-ориентированная архитектура... Мы не можем полностью избавиться от зависимостей, но можем попытаться минимизировать их, открыв только конечные точки, которые напрямую управляют данными, принадлежащими службе. Подход на основе обмена сообщений позволяет отделить службы друг от друга, потому что для передачи сообщений об изменении состояния ни одна из служб не должна ничего знать о других службах. Когда в службе организаций возникает необходимость опубликовать сообщение об изменении состояния, она просто записывает сообщение в очередь. Служба лицензий знает только, что должна получить сообщение, но не знает, кто опубликовал его. Надежность Наличие очереди позволяет гарантировать доставку сообщения, даже если получатель не работает. Например, служба организаций сможет продолжать публиковать сообщения, даже если служба лицензий на какое-то время станет недоступной. Сообщения будут храниться в очереди и оставаться там до тех пор, пока служба лицензий не возобновит работу. И наоборот, в решении с кешированием и очередью если служба организации перестанет работать, то служба лицензий сможет плавно ухудшать качество обслуживания, потому что по крайней мере часть данных об организациях будет находиться в ее кеше. Иногда старые данные лучше, чем их отсутствие. Масштабируемость Поскольку сообщения сохраняются в очереди, отправителю не нужно ждать ответа от получателя. Он может продолжать решать свои задачи. Точно так же если получатель обрабатывает сообщения недостаточно быстро, то можно попробовать запустить дополнительные экземпляры получателей и тем самым ускорить обработку сообщений. Такой подход к масштабируемости хорошо вписывается в модель микросервисов. В этой книге мы не раз подчеркивали, что запуск новых экземпляров микросервиса должен быть простой задачей. Дополнительный экземпляр микросервиса может помочь ускорить обработку очереди сообщений. Это пример масштабирования по горизонтали. В числе традиционных механизмов масштабирования чтения сообщений из очереди можно назвать увеличение количества потоков, запускаемых получателем. К сожалению, возможности этого подхода ограничены количеством процессоров, доступных получателю. Модель микросервисов не имеет этого ограничения, потому что дает возможность масштабирования, увеличивая количество компьютеров, на которых размещается служба, потребляющая сообщения. Гибкость Отправитель сообщения не знает, кто его получит. Это означает, что мы можем добавлять новых получателей (и новые возможности), Обмен сообщениями, событийно-ориентированная архитектура... 339 не влияя на исходную службу-отправителя. Это очень удобно, потому что мы можем расширять возможности приложения, не затрагивая существующие службы. Вместо этого новый код может извлекать публикуемые события и соответствующим образом на них реагировать. 10.1.3. Недостатки архитектуры на основе сообщений Как и любая архитектурная модель, архитектура на основе сообщений имеет свои достоинства и недостатки. Архитектура на основе сообщений сложна и требует, чтобы разработчики уделили пристальное внимание нескольким ключевым аспектам, включая семантику обработки сообщений, видимость сообщений и их хореографию. Давайте рассмотрим эти аспекты подробнее. Семантика обработки сообщений Для использования сообщений в приложении на основе микросервисов требуется не только знать, как публиковать и извлекать сообщения, но и понимать, как приложение будет вести себя в зависимости от порядка следования сообщений и что произойдет, если сообщения будут обрабатываться не по порядку. Например, если приложение строго требует обрабатывать все заказы от одного и того же клиента в порядке их получения, то мы должны будем организовать обработку сообщений иначе, чем в случае, когда каждое сообщение можно обрабатывать независимо от других сообщений. Это также означает, что при использовании обмена сообщениями для организации строгих переходов между состояниями мы должны подумать о сценариях, когда сообщение вызывает исключение или требуется обработать ошибку вне очереди. Если сообщение не удалось обработать, то следует ли повторить попытку или попробовать восстановить состояние после ошибки? Как в этом случае обрабатывать будущие сообщения, посылаемые этим клиентом? Это важные вопросы, над которыми нужно подумать. Видимость сообщений Использование сообщений в микросервисах часто означает смешивание синхронных вызовов служб с асинхронной обработкой. Асинхронный характер сообщений означает, что они могут быть получены и обработаны значительно позже момента публикации. Кроме того, наличие таких вещей, как идентификаторы корреляции для трассировки транзакций пользователя между вызовами веб-служб и сообщениями, имеет решающее значение для понимания происходящего в приложении и отладке. Как вы, возможно, помните из главы 8, идентификатор корреляции – это уникальный номер, сгенерированный в начале транзакции, который передается вместе с каждым последующим вызовом службы. Его также следует передавать вместе с каждым опубликованным сообщением. 340 Глава 10 Событийно-ориентированная архитектура... Хореография сообщений Как упоминалось в разделе о видимости сообщений, подход на основе обмена сообщениями затрудняет рассуждение о бизнеслогике приложения, потому что обработка событий выполняется не линейно, как в простой модели запрос/ответ. Отладка приложений на основе сообщений может потребовать тщательного исследования журналов нескольких служб, которые могут обрабатывать пользовательские транзакции не по порядку и в разное время. ПРИМЕЧАНИЕ. Обмен сообщениями – сложная, но мощная концепция. Предыдущие разделы мы написали не для того, чтобы отпугнуть вас от ее использования в ваших приложениях, а чтобы подчеркнуть, что применение этого подхода требует предусмотрительности. Одним из достоинств обмена сообщениями является возможность асинхронной работы бизнес-логики, что в свою очередь требует более тщательного ее проектирования. 10.2. Введение в Spring Cloud Stream Spring Cloud упрощает интеграцию модели обмена сообщениями в микросервисы на основе Spring, предлагая проект Spring Cloud Stream (https://spring.io/projects/spring-cloud-stream) – фреймворк, управляемый аннотациями, который позволяет с легкостью создавать издателей и получателей сообщений в приложениях Spring. Spring Cloud Stream также надежно отделяет нас от деталей реализации используемой платформы обмена сообщениями. Spring Cloud Stream поддерживает несколько платформ сообщений, включая Apache Kafka и RabbitMQ, не позволяя деталям реализации платформы просачиваться в код приложения. Публикация и извлечение сообщений в приложении осуществляется через платформенно независимые интерфейсы Spring. ПРИМЕЧАНИЕ. В этой главе мы используем Kafka (https://kafka. apache.org/) – высокопроизводительную шину сообщений, которая позволяет асинхронно отправлять потоки сообщений из одного приложения одному или нескольким других приложениям. Шина Kafka, написанная на Java, широко используется в облачных приложениях, потому что обладает высокой надежностью и масштабируемостью. Spring Cloud Stream также может использовать шину сообщений RabbitMQ. Чтобы познакомиться с проектом Spring Cloud Stream поближе, давайте для начала обсудим его архитектуру и рассмотрим некоторые термины. Новая терминология может показаться немного сложной, если раньше вам не доводилось работать с платформами обмена сообщениями, поэтому рассмотрим архитектуру Spring Cloud Stream через призму двух служб, обменивающихся сообщениями. Одна служба Введение в Spring Cloud Stream 341 является издателем, а другая – получателем сообщений. На рис. 10.3 показано, как Spring Cloud Stream облегчает передачу сообщений. Служба A Бизнес-логика Клиент службы 1. Клиент вызывает службу, и та изменяет данные, которыми владеет. Это делает бизнес-логика службы Источник Канал Связующий слой 2. Источник – это код фреймворка Spring, вызывая который служба публикует сообщение 3. Сообщение публикуется в канал 4. Связующий слой – это код фреймворка Spring Cloud Stream, который взаимодействует с конкретной системой обмена сообщениями Spring Cloud Stream 5. Брокер сообщений может быть реализован с использованием любых платформ обмена сообщениями, включая Apache Kafka и RabbitMQ Брокер сообщений Очередь сообщений Служба B Spring Cloud Stream 6. Порядок обработки сообщений (связующий слой, канал, приемник) изменяется, когда служба получает сообщение 7. Приемник – это код службы, который проверяет наличие входящих сообщений в канале и обрабатывает их Связующий слой Канал Приемник Бизнес-логика Рис. 10.3. После публикации сообщение проходит через ряд компонентов Spring Cloud Stream, абстрагирующих доступ к базовой платформе обмена сообщениями 342 Глава 10 Событийно-ориентированная архитектура... В публикации и приеме сообщений участвуют четыре компонента Spring Cloud Stream: источник; канал; связующий слой; приемник. Для публикации сообщения служба использует источник. Источник – это аннотированный интерфейс Spring, который принимает простой объект Java (POJO), представляющий публикуемое сообщение, сериализует его (по умолчанию в формат JSON) и публикует в канале. Канал – это абстракция очереди, которая будет хранить сообщение после публикации издателем или перед извлечением получателем. Иначе говоря, канал можно описать как очередь, которая отправляет и принимает сообщения. Имя канала всегда связано с именем целевой очереди, но это имя нигде не упоминается в коде. Вместо этого используется имя канала, т. е. мы можем переключать очереди, к которым подключены каналы, изменяя конфигурацию приложения, а не его код. Связующий слой (binder) является частью фреймворка Spring Cloud Stream. Это код, взаимодействующий с конкретной платформой сообщений. Связующий слой в фреймворке Spring Cloud Stream позволяет работать с сообщениями без использования библиотек и API, характерных для платформы. Служба, использующая Spring Cloud Stream, получает сообщение из очереди через приемник. Приемник проверяет наличие входящих сообщений в канале и десериализует их обратно в объекты POJO. После этого сообщения могут быть обработаны бизнес-логикой службы. 10.3. Простые издатель и получатель сообщений Теперь, перечислив основные компоненты Spring Cloud Stream, давайте рассмотрим простой пример использования Spring Cloud Stream. В первом примере мы реализуем передачу сообщений из службы организаций в службу лицензий, которая в свою очередь будет выводить их в консоль. В этом примере у нас будет только один источник сообщений (издатель) и один приемник сообщений (получатель), поэтому начнем с создания нескольких простых сокращений. Это упростит настройку источника в службе организаций и приемника в службе лицензий. На рис. 10.4 показан издатель сообщений, основанный на обобщенной архитектуре Spring Cloud Stream, представленной на рис. 10.3. Простые издатель и получатель сообщений 343 Служба организаций Бизнес-логика Клиент службы организаций 1. Клиент обращается к конечной точке службы организаций; данные изменились Источник (SimpleSourceBean) 2. Имя bean-компонента, который служба организаций использует для публикации сообщения внутри 3. Имя канала Spring Cloud Stream, который соответствует теме Kafka (в данном случае orgChangeTopic) Канал (Output) Связующий слой (Kafka) Spring Cloud Stream 4. Классы и конфигурация Spring Cloud Stream, которые обеспечивают связь с сервером Kafka Kafka orgChangeTopic Рис. 10.4. Когда служба организаций меняет свои данные, то публикует сообщение в orgChangeTopic 10.3.1. Настройка Apache Kafka и Redis в Docker В этом разделе мы объясним, как добавить службы Kafka и Redis в окружение Docker для использования нашим издателем сообщений. Для начала добавим код, представленный в листинге 10.1, в файл docker-compose.yml. Листинг 10.1. Добавление служб Kafka и Redis в docker-compose.yml // Часть файла docker-compose.yml опущена для краткости ... zookeeper: image: wurstmeister/zookeeper:latest ports: - 2181:2181 networks: backend: aliases: - "zookeeper" kafkaserver: image: wurstmeister/kafka:latest ports: - 9092:9092 344 Глава 10 Событийно-ориентированная архитектура... environment: - KAFKA_ADVERTISED_HOST_NAME=kafka - KAFKA_ADVERTISED_PORT=9092 - KAFKA_ZOOKEEPER_CONNECT=zookeeper:2181 - KAFKA_CREATE_TOPICS=dresses:1:1,ratings:1:1 volumes: - "/var/run/docker.sock:/var/run/docker.sock" depends_on: - zookeeper networks: backend: aliases: - "kafka" redisserver: image: redis:alpine ports: - 6379:6379 networks: backend: aliases: - "redis" 10.3.2. Публикация сообщений в службе организаций Начнем реализацию обмена сообщениями с изменения службы организаций, чтобы каждый раз при добавлении, изменении или удалении данных об организации она публиковала сообщение в теме Kafka о том, что произошло событие изменения данных. Сообщение будет включать идентификатор организации, связанный с событием, и тип события (добавление, изменение или удаление). Первым делом мы должны добавить зависимости в файл сборки pom.xml службы организаций, который находится в корневом каталоге службы. Мы добавим две зависимости: одну для подключения основных библиотек Spring Cloud Stream, а другую – для подключения библиотек Spring Cloud Stream Kafka: <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-stream</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-stream-kafka</artifactId> </dependency> ПРИМЕЧАНИЕ. Для запуска всех примеров мы используем Docker. Если вы решите запустить их локально, то на вашем компьютере должен быть установлен сервер Apache Kafka. Если вы используете Docker, то обновленный файл docker-compose.yml с контейнерами Kafka и Zookeeper вы найдете по адресу https://github. com/ihuaylupo/manning-smia/tree/master/chapter10/docker. Простые издатель и получатель сообщений 345 Не забудьте запустить службы, выполнив следующие команды в корневом каталоге, где находится родительский файл pom.xml: mvn clean package dockerfile:build && docker-compose -f docker/docker-compose.yml up После добавления зависимостей в файл сборки нужно сообщить приложению, что оно будет взаимодействовать с брокером сообщений Spring Cloud Stream. Для этого добавим к классу инициализации службы организаций, OrganizationServiceApplication, аннотацию @EnableBinding. Определение этого класса вы найдете в файле /organization-service/src/main/java/com/optimagrowth/ organization/OrganizationServiceApplication.java. Для удобства определение OrganizationServiceApplication.java показано в листинге 10.2. Листинг 10.2. Аннотированный класс OrganizationServiceApplication.java package com.optimagrowth.organization; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.context.config.annotation.RefreshScope; import org.springframework.cloud.stream.annotation.EnableBinding; import org.springframework.cloud.stream.messaging.Source; import org.springframework.security.oauth2.config.annotation.web.configuration. EnableResourceServer; @SpringBootApplication @RefreshScope Определяет связь приложения @EnableResourceServer с брокером сообщений @EnableBinding(Source.class) public class OrganizationServiceApplication { public static void main(String[] args) { SpringApplication.run(OrganizationServiceApplication.class, args); } } Аннотация @EnableBinding в листинге 10.2 сообщает фреймворку Spring Cloud Stream, что мы хотим связать службу с брокером сообщений. Параметр Source.class в @EnableBinding сообщает, что эта служба будет взаимодействовать с брокером сообщений через набор каналов, определяемых классом Source. Напомню, что каналы располагаются над очередью сообщений. Spring Cloud Stream имеет также набор каналов по умолчанию, которые можно настроить для взаимодействий с брокером сообщений. До сих пор мы ничего не сказали фреймворку Spring Cloud Stream о том, к какому именно брокеру сообщений мы будем привязывать службу организаций, и вскоре мы вернемся к этому вопросу. Теперь Глава 10 Событийно-ориентированная архитектура... 346 пойдем дальше и напишем код, публикующий сообщения. Первый шаг – изменить класс UserContext в /organization-service/src/main/ java/com/optimagrowth/organization/utils/UserContext.java. Это изменение сделает наши переменные локальными для потока выполнения. В листинге 10.3 показаны измененные объявления переменных в классе с использованием ThreadLocal. Листинг 10.3. Преобразование переменных в классе UserContext в локальные для потока выполнения package com.optimagrowth.organization.utils; // Инструкции импорта опущены для краткости @Component public class UserContext { public static final String public static final String public static final String public static final String CORRELATION_ID = "tmx-correlation-id"; AUTH_TOKEN = "Authorization"; USER_ID = "tmx-user-id"; ORG_ID = "tmx-org-id"; private static final ThreadLocal<String> correlationId = new ThreadLocal<String>(); Определение private static final ThreadLocal<String> authToken = переменных как ThreadLocal позвоnew ThreadLocal<String>(); private static final ThreadLocal<String> userId = ляет хранить данные отдельно для кажnew ThreadLocal<String>(); дого потока выполprivate static final ThreadLocal<String> orgId = нения. Информация new ThreadLocal<String>(); в этих переменных доступна только в потоке выполнения, инициализировавшем их public static HttpHeaders getHttpHeaders(){ HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.set(CORRELATION_ID, getCorrelationId()); return httpHeaders; } } Следующий шаг – создание логики публикации сообщения. Код публикации сообщений вы найдете в файле /organization-service/ src/main/java/com/optimagrowth/organization/events/source/ SimpleSourceBean.java. Определение этого класса показано в листинге 10.4. Листинг 10.4. Публикация сообщения в брокере сообщений package com.optimagrowth.organization.events.source; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; Простые издатель и получатель сообщений 347 import org.springframework.cloud.stream.messaging.Source; import org.springframework.messaging.support.MessageBuilder; import org.springframework.stereotype.Component; import com.optimagrowth.organization.events.model.OrganizationChangeModel; import com.optimagrowth.organization.utils.UserContext; @Component public class SimpleSourceBean { private Source source; private static final Logger logger = LoggerFactory.getLogger(SimpleSourceBean.class); public SimpleSourceBean(Source source){ this.source = source; } Сюда будет внедрена реализация интерфейса Source для использования службой public void publishOrganizationChange(ActionEnum action, String organizationId){ logger.debug("Sending Kafka message {} for Organization Id: {}", action, organizationId); OrganizationChangeModel change = new OrganizationChangeModel( OrganizationChangeModel.class.getTypeName(), action.toString(), Публикует сообщение organizationId, Java POJO UserContext.getCorrelationId()); source.output().send(MessageBuilder .withPayload(change) .build()); } Отправляет сообщение из канала, определяемого классом Source } В листинге 10.4 мы внедрили в наш код Source – класс Spring Cloud. Как уже говорилось, все взаимодействия с определенной очередью сообщений происходят через конструкцию Spring Cloud Stream, называемую каналом, которая представлена Java-классом. В листинге 10.4 мы использовали реализацию интерфейса Source, предоставляющую единственный метод с именем output(). Интерфейс Source удобно использовать, когда служба должна публиковать сообщения только в одном канале. Метод output() возвращает класс типа MessageChannel. С его помощью мы будем передавать сообщения брокеру сообщений. (Далее в этой главе мы покажем, как открыть несколько каналов обмена сообщениями с помощью своей реализации интерфейса.) Методу output() передается параметр типа ActionEnum, определяющий одно из следующих действий: Глава 10 Событийно-ориентированная архитектура... 348 public enum ActionEnum { GET, CREATED, UPDATED, DELETED } Фактическая публикация сообщения происходит в методе publishOrganizationChange(). Этот метод создает объект OrganizationChangeModel. Определение этого объекта показано в листинге 10.5. Листинг 10.5. Публикация объекта OrganizationChangeModel package com.optimagrowth.organization.events.model; import lombok.Getter; import lombok.Setter; import lombok.ToString; @Getter @Setter @ToString public class OrganizationChangeModel { private private private private String String String String type; action; organizationId; correlationId; public OrganizationChangeModel(String type, String action, String organizationId, String correlationId) { this.type = type; this.action = action; this.organizationId = organizationId; this.correlationId = correlationId; } } Класс OrganizationChangeModel определяет три элемента данных: action – действие, вызвавшее событие. Мы включили элемент action в сообщение, чтобы дать получателю больше контекстной информации, которая может пригодиться ему при обработке сообщения; organizationId – идентификатор организации, связанный с событием; correlationId – идентификатор корреляции вызова службы, вызвавшего событие. Всегда добавляйте идентификатор корреляции в сообщения, потому что он очень помогает в трассировке и отладке потока сообщений, протекающего через наши службы. Если вернуться к классу SimpleSourceBean, то можно заметить, что после подготовки сообщения к публикации вызывается метод send() класса MessageChannel, возвращаемого методом source.output(): Простые издатель и получатель сообщений 349 source.output().send(MessageBuilder.withPayload(change).build()); Метод send() принимает экземпляр класса Message. Здесь мы используем вспомогательный класс Spring с именем MessageBuilder, чтобы преобразовать содержимое класса OrganizationChangeModel в класс Message. Это весь код, который нужен нам для отправки сообщения. Однако на данном этапе многих из вас может не покидать ощущение волшебства, потому что мы пока не показали, как привязать службу организаций к конкретной очереди сообщений, не говоря уже о фактическом брокере сообщений. Все это делается через конфигурацию. В листинге 10.6 показана конфигурация, связывающая интерфейс Source с брокером сообщений Kafka и очередью. Эту конфигурацию можно сохранить в репозитории Spring Cloud Config для службы организаций. ПРИМЕЧАНИЕ. В этом примере в качестве репозитория мы используем путь поиска классов (classpath). Конфигурация службы организаций находится в файле /configserver/src/main/ resources/config/organization-service.properties. Листинг 10.6. Конфигурация с настройками Spring Cloud Stream для публикации сообщений Имя очереди сообщений (темы), куда будут записываться сообщения # Часть файла опущена для краткости spring.cloud.stream.bindings.output.destination= Определяет (подсказыorgChangeTopic вает) тип сообщения spring.cloud.stream.bindings.output.content-type= (в данном случае JSON) application/json spring.cloud.stream.kafka.binder.zkNodes= localhost Эти свойства определяют spring.cloud.stream.kafka.binder.brokers= местоположение Kafka и Zookeeper в сети localhost ПРИМЕЧАНИЕ. Служба Apache Zookeeper используется для хранения конфигурации и имен. Она также обеспечивает гибкую синхронизацию в распределенных системах. Apache Kafka действует как централизованная служба, которая отслеживает узлы кластера Kafka и конфигурацию очередей. Конфигурация в листинге 10.6 выглядит массивной, но она довольно проста. Spring.cloud.stream.bindings – это начало пути к конфигурационным параметрам с настройками, которые необходимы для публикации событий в брокере сообщений Spring Cloud Stream. Свойство spring.cloud.stream.bindings.output связывает канал source. output() из листинга 10.4 с брокером сообщений orgChangeTopic. Глава 10 Событийно-ориентированная архитектура... 350 Оно также сообщает фреймворку Spring Cloud Stream, что сообщения, отправленные в эту очередь (тему), должны быть сериализованы в формат JSON. Spring Cloud Stream может сериализовать сообщения в несколько форматов, включая JSON, XML и формат Avro Apache Foundation (https://avro.apache.org/). Теперь, когда у нас есть код для публикации сообщений через Spring Cloud Stream и конфигурация, связывающая Spring Cloud Stream с брокером сообщений Kafka, давайте посмотрим, где в действительности происходит публикация сообщений в службе организаций. Публикация выполняется классом OrganizationService в файле /organization-service/src/main/java/com/optimagrowth/ organization/service/OrganizationService.java. Определение этого класса приводится в листинге 10.7. Листинг 10.7. Публикация сообщений в службе организаций package com.optimagrowth.organization.service; // Инструкции импорта опущены для краткости @Service public class OrganizationService { private static final Logger logger = LoggerFactory.getLogger(OrganizationService.class); @Autowired private OrganizationRepository repository; @Autowired SimpleSourceBean simpleSourceBean; Используется автоматическое связывание для внедрения SimpleSourceBean в службу организаций public Organization create(Organization organization){ organization.setId( UUID.randomUUID().toString()); organization = repository.save(organization); simpleSourceBean.publishOrganizationChange( ActionEnum.CREATED, Для каждого метода службы, organization.getId()); изменяющего данные return organization; об организации, вызывается } simpleSourceBean.publishOrgChange() // Остальная часть определения класса опущена для краткости } Какие данные передавать в сообщении? Один из частых вопросов, которые нам задают в командах, впервые начинающих использовать обмен сообщениями, – какие данные передавать в сообщениях? И мы всегда отвечаем, что это зависит от специ­ фики приложения. Простые издатель и получатель сообщений 351 Как вы могли заметить, во всех наших примерах после изменения записи об организации мы возвращаем только идентификатор. Мы никогда не помещаем копию измененных данных в сообщения и используем эти сообщения, только чтобы уведомить другие службы об изменении состояния и заставить их обратиться к первоисточнику (службе, владеющей данными) для получения новой копии данных. Это более затратный подход с точки зрения времени выполнения, но он гарантирует использование самой свежей версии данных. Конечно, есть небольшая вероятность, что данные изменятся сразу после того, как мы прочитаем их из первоисточника. Однако это гораздо менее вероятно, чем если бы мы слепо использовали информацию прямо из очереди. Мы советуем внимательно подумать над тем, какие данные передавать. Рано или поздно вы столкнетесь с ситуацией, когда передаваемые данные будут «устаревать» еще до того, как получатель извлечет их, из-за слишком долгого нахождения в очереди сообщений. Также из-за того, что приложение полагается на данные в сообщении, а не на фактические данные в хранилище, представление данных в приложении может оказаться в несогласованном состоянии, если предыдущее сообщение с данными было получено с ошибкой. Если вы собираетесь передавать состояние в своих сообщениях, обязательно включите отметку времени или номер версии, чтобы служба, получающая данные, могла проверить их и убедиться, что они не старше уже имеющейся копии. 10.3.3. Получение сообщений в службе лицензий На данный момент мы добавили в службу организаций публикацию сообщений в Kafka после каждого изменения данных. Теперь любая служба, использующая эти данные, может реагировать на их изменение без явного ее вызова со стороны службы организаций. Это также означает, что теперь можно добавлять новые возможности для обработки изменений в службе организаций, просто получая сообщения, поступающие из очереди. Давайте теперь сменим направление и посмотрим, как с помощью Spring Cloud Stream организовать прием сообщений в службе-получателе – в данном случае в службе лицензий. Для начала добавим необходимые зависимости Spring Cloud Stream в файл pom.xml, находящийся в корневом каталоге службы лицензий. На рис. 10.5 показано место службы лицензий в архитектуре Spring Cloud, впервые показанной на рис. 10.3. По аналогии с файлом pom.xml службы организаций, который мы рассмотрели выше, добавим следующие две зависимости: 352 Глава 10 Событийно-ориентированная архитектура... <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-stream</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-stream-kafka</artifactId> </dependency> Kafka 1. В Kafka orgChangeTopic появляется сообщение, уведомляющее об изменении данных Очередь сообщений Служба лицензий 2. Классы и конфигурация Spring Cloud Stream 3. Для передачи входящего сообщения будет использоваться как канал по умолчанию, так и пользовательский канал (inboundOrgChanges) 4. Каждое входящее сообщение обрабатывается классом OrganizationChangeHandler Spring Cloud Stream Связующий слой (Kafka) Канал (inboundOrgChanges) Приемник (OrganizationChangeHandler) Бизнес-логика Рис. 10.5. Когда сообщение поступает в очередь orgChangeTopic, служба лицензий извлекает его и обрабатывает Затем нужно связать службу лицензий с брокером сообщений с помощью Spring Cloud Stream. Так же как в случае со службой организаций, добавим аннотацию @EnableBinding к классу инициализации службы LicenseServiceApplication. Определение этого класса находится в файле /licensing-service/src/main/java/com/optimagrowth/ license/LicenseServiceApplication.java. Разница между службами лицензий и организаций заключается в значении, которое передается аннотации @EnableBinding, как показано в листинге 10.8. Простые издатель и получатель сообщений 353 Листинг 10.8. Получение сообщений с использованием Spring Cloud Stream Использовать каналы, определяемые интерфейсом приемника Sink, для получения входящих сообщений package com.optimagrowth.license; // Инструкции импорта опущены для краткости @EnableBinding(Sink.class) public class LicenseServiceApplication { Этот метод выполняется всякий раз, когда во входном канале появляется входящее сообщение @StreamListener(Sink.INPUT) public void loggerSink(OrganizationChangeModel orgChange) { logger.debug("Received an {} event for organization id {}", orgChange.getAction(), orgChange.getOrganizationId()); } // Остальной код опущен для краткости } Так как служба лицензий является получателем сообщений, мы передаем в аннотацию @EnableBinding параметр Sink.class. Он сообщает фреймворку Spring Cloud Stream, что связь с брокером сообщений должна устанавливаться с использованием интерфейса Sink. По аналогии с интерфейсом Source (описанным в разделе 10.3.1) интерфейс Sink в Spring Cloud Stream предоставляет канал по умолчанию. Этот канал называется input и используется для приема входящих сообщений. Указав с помощью аннотации @EnableBinding, что служба будет принимать сообщения, мы можем перейти к реализации их обработки. Для этого воспользуемся аннотацией @StreamListener. Она сообщает фреймворку Spring Cloud Stream, что тот должен вызывать метод loggerSink() для получения сообщения из канала input. Spring Cloud Stream автоматически десериализует входящее сообщение в объект с именем OrganizationChangeModel. И снова связь очереди сообщений с входным каналом определяется конфигурацией службы лицензий. Соответствующие настройки показаны в листинге 10.9; вы найдете их в файле /configserver/ src/main/resources/config/licensing-service.properties. Листинг 10.9. Конфигурация с настройками для приема сообщений из очереди Kafka # Некоторые свойства опущены для краткости spring.cloud.stream.bindings.input.destination= orgChangeTopic spring.cloud.stream.bindings.input.content-type= application/json Связывает канал input с очередью orgChangeTopic 354 Глава 10 Событийно-ориентированная архитектура... spring.cloud.stream.bindings.input.group= licensingGroup spring.cloud.stream.kafka.binder.zkNodes= localhost spring.cloud.stream.kafka.binder.brokers= localhost Определение семантики «передачи сообщения единственному экземпляру» Конфигурация в листинге 10.9 очень похожа на конфигурацию службы организаций. Однако она имеет два ключевых отличия. Вопервых, здесь свойство spring.cloud.stream.bindings связывает входной канал Sink.INPUT, который объявлен в листинге 10.8, с orgChangeTopic. Во-вторых, в этой конфигурации появилось новое свойство с именем spring.cloud.stream.bindings.input.group. Свойство group определяет имя группы получателей сообщений. Идея групп получателей заключается в следующем: в приложении может иметься несколько служб, и каждая служба может иметь несколько экземпляров, принимающих сообщения из одной и той же очереди. Было бы желательно, чтобы одно и то же сообщение передавалось каждой уникальной службе, но только одному ее экземпляру. Свойство group определяет группу получателей, к которой принадлежит служба. Пока все экземпляры службы имеют одно и то же имя группы, Spring Cloud Stream и базовый брокер сообщений гарантируют, что сообщение получит только один из них. В нашем примере мы присвоили свойству group значение licensingGroup. На рис. 10.6 показано, как группировка получателей помогает обеспечить семантику передачи сообщения единственному экземпляру. 2. То же самое сообщение передается другой службе (Служба X), которой назначена другая группа получателей Служба лицензий Служба лицензий, экземпляр A (licensingGroup) 1. В orgChangeTopic поступает сообщение от службы организаций Kafka Служба лицензий, экземпляр B (licensingGroup) Служба лицензий, экземпляр C (licensingGroup) orgChangeTopic Служба X 3. Сообщение передается только одному экземпляром службы лицензий, потому что все они принадлежат одной группе потребителей (licensingGroup) Экземпляр службы X (serviceInstanceXGroup) Рис. 10.6. Организация получателей в группы гарантирует, что сообщение будет обработано только одним экземпляром каждой уникальной службы в группе Простые издатель и получатель сообщений 355 10.3.4. Тестирование передачи сообщений между службами Итак, мы реализовали публикацию службой организаций сообщений в очередь orgChangeTopic при каждом добавлении, обновлении или удалении записи в хранилище и получение этих сообщений в службе лицензий. Теперь давайте посмотрим, как действует этот код, для чего создадим новую запись с помощью службы организаций и понаблюдаем за консолью, где должно появиться соответствующее сообщение службы лицензий. Для создания новой записи мы выполним запрос POST к конечной точке http://localhost:8072/organization/v1/organization/ службы организаций и отправим в нем следующий фрагмент JSON. На рис. 10.7 показан результат этого вызова POST: { "name":"Ostock", "contactName":"Illary Huaylupo", "contactEmail":"illaryhs@gmail.com", "contactPhone":"888888888" } ПРИМЕЧАНИЕ. Как вы наверняка помните, сначала мы реализовали аутентификацию, чтобы получить токен доступа и передать его в заголовке Authorization в качестве токена на предъявителя Bearer Token. Мы рассказывали об этом в предыдущей главе. Если вы не следовали за примерами кода в той главе, то можете загрузить исходный код по адресу http://github.com/ ihuaylupo/manning-smia/tree/master/chapter9. Затем мы добавили поддержку Spring Cloud Gateway. Вот почему конечная точка имеет номер порта 8072 и путь /organization/v1/organization, а не /v1/organization. Рис. 10.7. Создание новой записи с использованием службы организаций 356 Глава 10 Событийно-ориентированная архитектура... Служба организаций сообщает, что она отправила сообщение в очередь Kafka Служба лицензий сообщает, что она получила сообщение о событии SAVE Рис. 10.8. Сообщение в консоли, доказывающее отправку сообщения службой организаций и его получение службой лицензий Сразу после вызова службы организации мы должны увидеть в консоли, где были запущены службы, результат, изображенный на рис. 10.8. Теперь у нас есть две службы, общающиеся друг с другом посредством сообщений. Spring Cloud Stream выступает в роли посредника между этими службами. С точки зрения обмена сообщениями службы ничего не знают друг о друге. Они используют брокера сообщений в качестве посредника и Spring Cloud Stream в качестве уровня абстракции над брокером. 10.4.Пример использования Spring Cloud Stream: распределенное кеширование Итак, у нас есть две службы, обменивающиеся сообщениями, но пока мы никак эти сообщения не обрабатываем. Давайте расширим пример реализации распределенного кеша, обсуждавшийся выше в этой главе. В обновленной реализации служба лицензий всегда будет проверять распределенный кеш Redis на наличие информации об организации, связанной с конкретной лицензией. Если данные об организации присутствуют в кеше, то мы вернем данные из кеша. Если нет, то вызовем службу организаций и сохраним результаты вызова в кеше Redis. При обновлении данных в службе организации она будет отправлять сообщение в очередь Kafka. Служба лицензий получит сообщение и отправит команду DELETE службе Redis, чтобы очистить кеш. Пример использования Spring Cloud Stream: распределенное кеширование 357 Кеширование и обмен сообщениями в облаке Redis хорошо подходит на роль распределенного кеша для использования облачными микросервисами. С помощью Redis можно: повысить производительность поиска часто используемых данных. Применение кеша позволит значительно улучшить производительность некоторых ключевых служб за счет уменьшения обращений к базе данных; уменьшить нагрузку на базу данных, где хранится информация. Запросы к базе данных могут обходиться довольно дорого. Каждая операция чтения из базы данных является оплачиваемым событием. Используя сервер Redis, можно получать данные по первичному ключу, минуя базу данных, и значительно уменьшить расходы; повысить устойчивость и обеспечить плавное снижение качества обслуживания, если в основном хранилище или в базе данных возникнут проблемы с производительностью. В зависимости от объема данных, хранимых в кеше, решение с использованием кеширования может помочь уменьшить количество ошибок, которые могут случаться при использовании только основного хранилища данных. Redis – это гораздо больше чем кеш. Однако он может играть роль кеша, если вам нужен распределенный кеш. 10.4.1. Использование Redis в роли кеша В этом разделе мы начнем с настройки службы лицензий для использования Redis. К счастью, Spring Data упрощает внедрение Redis в службы. Чтобы использовать Redis в службе лицензий, необходимо сделать следующее. 1 Подключить к службе лицензий зависимости Spring Data Redis. 2 Создать подключение к Redis. 3 Определить репозитории Spring Data Redis, которые наш код будет использовать для взаимодействия с кешем Redis. 4 Использовать Redis в службе лицензий для сохранения и извлечения данных об организациях. Настройка зависимостей Spring Data Redis в службе лицензий Первым делом подключим зависимости spring-data-redis и jedis в файле pom.xml службы лицензий, как показано в листинге 10.10. Листинг 10.10. Добавление зависимостей Spring Redis // Некоторые настройки опущены для краткости <dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-redis</artifactId> </dependency> 358 Глава 10 Событийно-ориентированная архитектура... <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <type>jar</type> </dependency> Создание подключения к серверу Redis После добавления зависимостей нужно настроить соединение с сервером Redis. Spring Для взаимодействий с Redis в Spring используется проект с открытым исходным кодом Jedis (https:// github.com/xetorthio/jedis). Для подключения к конкретному экземпляру Redis нужно представить класс JedisConnectionFactory как bean-компонент Spring. Определение этого класса вы найдете в /licensing-service/src/main/java/com/optimagrowth/license/ LicenseServiceApplication.java. После подключения к Redis мы будем использовать это соединение для создания объекта Spring RedisTemplate. Классы Spring Data для работы с репозиторием, которые мы вскоре реализуем, используют объект RedisTemplate для выполнения запросов и сохранения данных в службе Redis. Соответствующий код показан в листинге 10.11. Листинг 10.11. Создание соединения для взаимодействия службы лицензий с Redis package com.optimagrowth.license; import org.springframework.data.redis.connection.RedisPassword; import org.springframework.data.redis.connection. RedisStandaloneConfiguration; import org.springframework.data.redis.connection.jedis. JedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; // Большая часть инструкций импорта и аннотаций опущена для краткости @SpringBootApplication @EnableBinding(Sink.class) public class LicenseServiceApplication { @Autowired private ServiceConfig serviceConfig; Настраивает подключение к серверу Redis // Все остальные методы в этом классе опущены для краткости @Bean JedisConnectionFactory jedisConnectionFactory() { String hostname = serviceConfig.getRedisServer(); int port = Integer.parseInt(serviceConfig.getRedisPort()); RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration(hostname, port); Пример использования Spring Cloud Stream: распределенное кеширование 359 return new JedisConnectionFactory(redisStandaloneConfiguration); } @Bean public RedisTemplate<String, Object> redisTemplate() { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(jedisConnectionFactory()); return template; Создает RedisTemplate для взаимо} действий с сервером Redis // Остальной код опущен для краткости } Основная работа по настройке службы лицензий для взаимодействий с Redis завершена. Теперь перейдем к логике получения, добавления, обновления и удаления данных. ServiceConfig – это простой класс, содержащий логику получения параметров, которые мы определим в файле конфигурации службы лицензий; в этом конкретном примере – имя хоста и номер порта Redis. Определение этого класса показано в листинге 10.12. Листинг 10.12. Определение класса ServiceConfig с настройками сервера Redis package com.optimagrowth.license.config; // Инструкции импорта опущены для краткости @Component @Getter public class ServiceConfig{ // Часть кода опущена для краткости @Value("${redis.server}") private String redisServer=""; @Value("${redis.port}") private String redisPort=""; } В репозитории Spring Cloud Config имя хоста и номер порта сервера Redis определены в файле /configserver/src/main/ resources/config/licensing-service .properties: redis.server = localhost redis.port = 6379 ПРИМЕЧАНИЕ. Для запуска всех примеров в этой книге мы используем Docker. Если вы решите запустить этот пример локально, то вам следует установить Redis на свой компьютер. Иначе вы найдете обновленный файл docker-compose.yml вместе с контейнером Redis по адресу https://github.com/ihuaylupo/manningsmia/tree/master/chapter10/docker. 360 Глава 10 Событийно-ориентированная архитектура... Определение репозиториев Spring Data для доступа к Redis Redis – это хранилище данных типа ключ/значение, которое действует как большой распределенный хеш-массив. В простейшем случае он сохраняет и ищет данные с помощью ключа. У него нет сложного языка запросов для извлечения данных. Его простота – его сила и одна из причин, почему так много разработчиков решили использовать его в своих проектах. Поскольку для доступа к нашему хранилищу Redis мы используем Spring Data, то должны определить класс репозитория. В первых главах мы демонстрировали использование Spring Data и своих классов репозиториев для доступа к базе данных Postgres без необходимости писать низкоуровневые SQL-запросы. В службе лицензий мы пойдем тем же путем и определим два файла с кодом для доступа к нашему репозиторию Redis. Первый файл (/licensingservice/src/main/java/com/optimagrowth/license/repository/ OrganizationRedisRepository.java) показан в листинге 10.13 и содержит определение интерфейса Java, который будет внедряться в любые классы службы лицензий, обращающиеся к Redis. Листинг 10.13. OrganizationRedisRepository определяет методы для обращения к Redis package com.optimagrowth.license.repository; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Repository; import com.optimagrowth.license.model.Organization; @Repository public interface OrganizationRedisRepository extends CrudRepository<Organization,String>{ } Расширяя CrudRepository, интерфейс OrganizationRedisRepository определяет всю логику CRUD (Create, Read, Update, Delete – создание, чтение, обновление, удаление), необходимую для сохранения и извлечения данных из Redis (в данном случае). Второй файл – это модель нашего репозитория, класс POJO, содержащий данные, которые мы будем хранить в кеше Redis. Класс /licensingservice/ src/main/java/com/optimagrowth/license/model/Organization. java показан в листинге 10.14. Листинг 10.14. Модель организации для хранения в хеше Redis package com.optimagrowth.license.model; import org.springframework.data.redis.core.RedisHash; import org.springframework.hateoas.RepresentationModel; import javax.persistence.Id; Пример использования Spring Cloud Stream: распределенное кеширование import lombok.Getter; import lombok.Setter; import lombok.ToString; 361 Устанавливает имя хеша на сервере Redis, где хранятся данные об организациях @Getter @Setter @ToString @RedisHash("organization") public class Organization extends RepresentationModel<Organization> { @Id String String String String String id; name; contactName; contactEmail; contactPhone; } В листинге 10.14 следует отметить одну важную деталь: сервер Redis может хранить несколько хешей и структур данных. Поэтому мы должны сообщать Redis имя структуры данных, с которой собираемся работать. Использование Redis из службы лицензий для сохранения и извлечения данных об организациях Теперь, когда у нас есть код для взаимодействий с Redis, можно расширить нашу службу лицензий, чтобы каждый раз, когда ей требуются данные об организации, она сначала проверяла кеш Redis и только потом обращалась к службе организаций. Соответствующую логику вы найдете в классе OrganizationRestTemplateClient в файле /service/src/main/java/com/optimagrowth/license/service/ client/OrganizationRestTemplateClient.java. Она также показана в листинге 10.15. Листинг 10.15. Реализация логики кеширования в OrganizationRestTemplateClient package com.optimagrowth.license.service.client; // Инструкции импорта опущены для краткости @Component public class OrganizationRestTemplateClient { @Autowired Внедряет RestTemplate restTemplate; OrganizationRedisRepository в @Autowired OrganizationRestTemplateClient OrganizationRedisRepository redisRepository; private static final Logger logger = LoggerFactory.getLogger(OrganizationRestTemplateClient.class); Глава 10 Событийно-ориентированная архитектура... 362 private Organization checkRedisCache(String organizationId) { try { Попытка получить из Redis экreturn redisRepository земпляр Organization с указанным идентификатором органи.findById(organizationId) зации .orElse(null); }catch (Exception ex){ logger.error("Error encountered while trying to retrieve organization{} check Redis Cache. Exception {}", organizationId, ex); return null; } } private void cacheOrganizationObject(Organization organization) { try { Сохраняет информаredisRepository.save(organization); цию об организации в Redis }catch (Exception ex){ logger.error("Unable to cache organization {} in Redis. Exception {}", Если не удалось получить данные organization.getId(), ex); из Redis, то вызывается служба } организаций, чтобы получить информацию из исходной базы дан} ных и потом сохранить ее в Redis public Organization getOrganization(String organizationId){ logger.debug("In Licensing Service.getOrganization: {}", UserContext.getCorrelationId()); Organization organization = checkRedisCache(organizationId); if (organization != null){ logger.debug("I have successfully retrieved an organization {} from the redis cache: {}", organizationId, organization); return organization; } logger.debug("Unable to locate organization from the redis cache: {}.",organizationId); ResponseEntity<Organization> restExchange = restTemplate.exchange( "http://gateway:8072/organization/v1/organization/ {organizationId}", HttpMethod.GET, null, Organization.class, organizationId); organization = restExchange.getBody(); if (organization != null) { cacheOrganizationObject(organization); } return restExchange.getBody(); } } Метод getOrganization() – это место, где происходит вызов службы организаций. Но, прежде чем выполнить фактический вызов Пример использования Spring Cloud Stream: распределенное кеширование 363 REST, метод сначала пытается получить требуемый объект Organization из Redis, вызывая для этого checkRedisCache(). Если искомого объекта нет в Redis, то checkRedisCache() возвращает значение null, и тогда getOrganization() вызывает конечную точку REST службы организаций, чтобы получить нужную информацию. Если служба организаций вернула объект с информацией об организации, то он сохраняется в кеше вызовом метода cacheOrganizationObject(). ПРИМЕЧАНИЕ. Обратите внимание на обработку исключений при взаимодействии с кешем. Для большей устойчивости мы никогда не допускаем сбоя всего вызова, если не можем связаться с сервером Redis. Вместо этого мы регистрируем исключение и обращаемся к службе организаций. В данном конкретном случае кеширование предназначено для повышения производительности, и отсутствие кеширующего сервера не должно влиять на успешность вызова службы лицензий. Теперь давайте воспользуемся утилитой Postman и попробуем вызвать службу лицензий, чтобы увидеть, какие сообщения она выведет в журнал. Если последовательно выполнить два запроса GET к конечной точке службы лицензий http://localhost:8072/license/v1/ organization e839ee96-28de-4f67-bb79-870ca89743a0/license/279709ffe6d5-4a54-8b55-a5c37542025b, то мы увидим в нашем журнале две следующие записи: licensingservice_1 | DEBUG 1 --- [nio-8080-exec-4] c.o.l.s.c.OrganizationRestTemplateClient : Unable to locate organization from the redis cache: e839ee96-28de-4f67-bb79-870ca89743a0. licensingservice_1 | DEBUG 1 --- [nio-8080-exec-7] c.o.l.s.c.OrganizationRestTemplateClient : I have successfully retrieved an organization e839ee96-28de-4f67-bb79-870ca89743a0 from the redis cache: Organization(id=e839ee96-28de-4f67-bb79-870ca89743a0, name=Ostock, contactName=Illary Huaylupo, contactEmail=illaryhs@gmail.com, contactPhone=888888888) Первая запись показывает, что при первом обращении к конечной точке службы лицензий эта служба проверила кеш Redis и, не найдя в нем объект с информацией об организации e839ee9628de-4f67-bb79-870ca89743a0, вызвала службу организаций. Следующая запись показывает, что при втором обращении служба лицензий получила искомый объект из кеша. 10.4.2. Определение собственных каналов Выше в этой главе мы организовали обмен сообщениями между службами лицензий и организаций с использованием входных и выходных каналов по умолчанию, упакованных в интерфейсы 364 Глава 10 Событийно-ориентированная архитектура... Source и Sink в Spring Cloud Stream. Однако если понадобится опре- делить больше каналов для нашего приложения или настроить их имена, то придется определить свои интерфейсы и организовать столько входных и выходных каналов, сколько потребуется. Для создания канала мы вызываем inboundOrgChanges в службе лицензий. Канал можно определить с помощью интерфейса CustomChannels, находящегося в файле /licensingservice/src/main/java/ com/optimagrowth/license/events/CustomChannels.java, как показано в листинге 10.16. Листинг 10.16. Определение своего входного канала для службы лицензий package com.optimagrowth.license.service.client; // Инструкции импорта опущены для краткости package com.optimagrowth.license.events; import org.springframework.cloud.stream.annotation.Input; import org.springframework.messaging.SubscribableChannel; Определяет имя канала public interface CustomChannels { @Input("inboundOrgChanges") SubscribableChannel orgs(); } Возвращает экземпляр класса SubscribableChannel для каждого канала, указанного в аннотации @Input Ключевой вывод, следующий из листинга 10.16, заключается в том, что для каждого создаваемого входного канала нужно определить метод с аннотацией @Input, возвращающий экземпляр класса SubscribableChannel. Также нужно определить метод с аннотацией @OutputChannel, который будет вызываться для создания выходного канала, в который будут помещаться сообщения. Метод должен возвращать экземпляр класса MessageChannel. Вот как выглядит определение метода с аннотацией @OutputChannel: @OutputChannel(“outboundOrg”) MessageChannel outboundOrg(); Теперь, когда у нас есть свой входной канал, внесем еще два изменения, необходимые для использования этого канала в службе лицензий. Во-первых, свяжем имя канала с темой Kafka в файле конфигурации службы лицензий, как показано в листинге 10.17. Листинг 10.17. Изменения в конфигурации службы лицензий для использования нового входного канала // Часть настроек опущена для краткости spring.cloud.stream.bindings.inboundOrgChanges.destination= orgChangeTopic spring.cloud.stream.bindings.inboundOrgChanges.content-type= application/json Пример использования Spring Cloud Stream: распределенное кеширование 365 spring.cloud.stream.bindings.inboundOrgChanges.group= licensingGroup spring.cloud.stream.kafka.binder.zkNodes= localhost spring.cloud.stream.kafka.binder.brokers= localhost И внедрим экземпляр объявленного выше интерфейса CustomChannels в класс, который будет обрабатывать входящие сообщения. В своем примере распределенного кеша мы переместили код обработки входящего сообщения в класс службы лицензий OrganizationChangeHandler. Вы найдете его определение в файле /licensingservice/src/main/java/com/optimagrowth/license/events/ handler/OrganizationChangeHandler.java. В листинге 10.18 показан код обработки сообщений, получаемых из только что созданного канала inboundOrgChanges. Листинг 10.18. Обработка сообщений, получаемых из нового канала inboundOrgChanges package com.optimagrowth.license.events.handler; // Инструкции импорта опущены для краткости @EnableBinding(CustomChannels.class) public class OrganizationChangeHandler { Аннотация @EnableBindings перемещена из Application.java в OrganizationChangeHandler.java. На этот раз аннотации передается CustomChannels.class вместо Sink.class private static final Logger logger = LoggerFactory.getLogger(OrganizationChangeHandler.class); private OrganizationRedisRepository organizationRedisRepository; Внедряет OrganizationRedisRepository в OrganizationChangeHandler для поддержки операций CRUD @StreamListener("inboundOrgChanges") public void loggerSink( OrganizationChangeModel organization) { Вспомогательный класс, извлекающий метаданные logger.debug("Received a message of type " + organization.getType()); Проверяет, какая операция запрошена, и реагирует соответствующим образом logger.debug("Received a message with an event {} from the organization service for the organization id {} ", organization.getType(), organization.getType()); } } Теперь давайте добавим информацию о новой организации, а затем найдем ее. Это можно сделать, используя следующие две конечные точки. (На рис. 10.9 показан вывод в консоли, сообщающий результаты этих вызовов.) Глава 10 Событийно-ориентированная архитектура... 366 http://localhost:8072/organization/v1/organization/ http://localhost:8072/organization/v1/organization/d989f37d-9a59-4b59b276-2c79005ea0d9 Сообщение от службы организаций об отправке в очередь Kafka сообщение SAVE Сообщение от класса OrganizationChangeHandler о получении сообщения от службы организаций Сообщение от службы организаций о получении сообщения GET из очереди Kafka Рис. 10.9. Сообщения от службы организаций в консоли Теперь, узнав, как использовать Spring Cloud Stream и Redis, перейдем к следующей главе, где мы рассмотрим несколько методов и технологий распределенной трассировки с использованием Spring Cloud. Итоги Асинхронный обмен сообщениями – важная часть архитектуры микросервисов. Обмен сообщениями улучшает масштабируемость и устойчивость служб. Spring Cloud Stream упрощает публикацию и прием сообщений, предлагая простые аннотации и абстракции, скрывающие детали взаимодействий с фактическими платформами обмена сообщениями. Источник сообщений в Spring Cloud Stream – это аннотированный метод Java, который вызывается для публикации сообщений в очереди брокера сообщений. Приемник сообщений в Spring Cloud Stream – это аннотированный метод Java, который извлекает сообщения из очереди брокера сообщений. Redis – это хранилище данных в виде пар ключ/значение, которое можно использовать как в качестве базы данных, так и в качестве кеша. 11 Распределенная трассировка с использованием Spring Cloud Sleuth и Zipkin Эта глава: знакомит с использованием Spring Cloud Sleuth для внедрения трассировочной информации в запросы к службам; демонстрирует приемы агрегирования для просмотра журналов распределенных транзакций; описывает способы преобразования, поиска, анализа и визуализации данных из журналов в масштабе реального времени; рассказывает о трассировке пользовательских транзакций, охватывающих несколько служб; рассказывает о настройке трассировки с помощью Spring Cloud Sleuth и Zipkin. Микросервисная архитектура – мощная парадигма проектирования для разделения сложных монолитных программных систем на более мелкие и легко контролируемые части. Эти части могут компилироваться и развертываться независимо друг от друга; однако за такую гибкость приходится расплачиваться увеличенной сложностью. Из-за распределенной природы микросервисов поиск и отладка проблем в них может сводить с ума: вам придется отследить обработку одной или нескольких транзакций в нескольких службах, которые могут выполняться на разных физических машинах и использовать разные хранилища данных, а затем попытаться 368 Глава 11 Распределенная трассировка с использованием Spring ... собрать всю информацию воедино и понять суть происходящего. В этой главе мы опишем несколько приемов и технологий распределенной отладки. В частности, мы рассмотрим: использование идентификаторов корреляции для трассировки транзакций, охватывающих несколько служб; агрегирование данных из журналов различных служб в единый доступный для поиска источник информации; визуализацию потока транзакции через несколько служб для понимания каждого аспекта, характеризующего особенности производительности транзакции; анализ, поиск и визуализацию данных из журналов в масштабе реального времени с использованием стека ELK. Для этого мы используем следующие технологии: Spring Cloud Sleuth (https://cloud.spring.io/spring-cloud-sleuth/ reference/html/) – проект, реализующий внедрение идентификаторов трассировки (их еще называют идентификаторами корреляции) во входящие HTTP-запросы путем добавления фильтров и взаимодействия с другими компонентами Spring для передачи идентификаторов корреляции во все вызовы в системе; Zipkin (https://zipkin.io/) – инструмент визуализации данных с открытым исходным кодом, отображающий поток транзакции, охватывающий несколько служб. Zipkin позволяет разбить транзакцию на составляющие и визуально выявить горячие точки; стек ELK (https://www.elastic.co/what-is/elk-stack) – объединяет три инструмента с открытым исходным кодом: Elasticsearch, Logstash и Kibana, помогающих анализировать, искать и визуализировать журналы в масштабе реального времени; – Elasticsearch – распределенная аналитическая база данных всех типов (структурированных и неструктурированных, числовых, текстовых и т. д.); – Logstash – конвейер обработки данных на стороне сервера, позволяющий добавлять и получать данные из нескольких источников одновременно, а также преобразовывать их до того, как они будут проиндексированы в Elasticsearch; – Kibana – инструмент визуализации и управления данными для Elasticsearch. Поддерживает визуализацию информации в виде диаграмм, карт и гистограмм в масштабе реального времени. В начале этой главы мы познакомимся с простейшим инструментом трассировки: идентификатором корреляции. Spring Cloud Sleuth и идентификатор корреляции 369 ПРИМЕЧАНИЕ. Эта глава в значительной части опирается на сведения, которые рассматривались в главе 8 (в частности, на описание предварительных и заключительных фильтров в Spring Gateway), поэтому мы рекомендуем прочитать сначала главу 8 (если вы этого еще не сделали) и только потом приступать к этой главе. 11.1.Spring Cloud Sleuth и идентификатор корреляции Впервые концепция идентификаторов корреляции была представлена в главах 7 и 8. Идентификатор корреляции – это случайно сгенерированный уникальный номер или строка, присваиваемый транзакции при ее создании. По мере прохождения транзакции через несколько служб вместе с ней передается идентификатор корреляции. В главе 8 мы использовали фильтр Spring Cloud Gateway для проверки всех входящих HTTP-запросов и добавления в них идентификаторов корреляции. Также в каждой из наших служб мы использовали фильтр, отображающий входящую переменную в объект UserContext, используя который мы вручную вставляли идентификатор корреляции в записи в журнале или, приложив немного усилий, непосредственно в ассоциативный диагностический контекст Spring (Mapped Diagnostic Context, MDC). MDC – это ассоциативный массив, хранящий набор пар ключ/значение, предоставляемых приложением, которые вставляются в сообщения в журнале. Там же мы написали перехватчик Spring, гарантирующий включение идентификатора корреляции во все исходящие HTTPзапросы, посылаемые другим службам. К счастью, все сложности управления этой инфраструктурой может взять на себя Spring Cloud Sleuth. Давайте продолжим и добавим Spring Cloud Sleuth в наши службы лицензий и организаций. Добавив Spring Cloud Sleuth в наши микросервисы, мы сможем: прозрачно создавать и вставлять идентификатор корреляции в вызовы служб; обеспечить автоматическое внедрение идентификаторов корреляции в исходящие вызовы, выполняемые в рамках транзакции; добавлять идентификационную информацию в Spring MDC, чтобы сгенерированный идентификатор корреляции автоматически регистрировался в реализациях по умолчанию SL4J и Logback; передавать информацию трассировки распределенной платформе трассировки Zipkin. 370 Глава 11 Распределенная трассировка с использованием Spring ... ПРИМЕЧАНИЕ. Если в Spring Cloud Sleuth использовать реализацию журналирования из Spring Boot, то мы автоматически будем получать идентификаторы корреляции, вставляемые в инструкции журналирования в наших микросервисах. 11.1.1.Подключение Spring Cloud Sleuth к службам лицензий и организаций Чтобы задействовать Spring Cloud Sleuth в наших службах лицензий и организаций, нужно добавить одну зависимость в файлы pom.xml обеих служб. Вот как это сделать: <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-sleuth</artifactId> </dependency> Эти зависимости включают все основные библиотеки, необходимые для использования Spring Cloud Sleuth. Вот и все. После добавления этих зависимостей наши службы будут: проверять все входящие HTTP-запросы от вышестоящих служб на наличие в них трассировочной информации Spring Cloud Sleuth. Если трассировочная информация Spring Cloud Sleuth присутствует во входящем вызове, то она будет извлечена и доступна службе для журналирования и обработки; добавлять трассировочную информацию Spring Cloud Sleuth в ассоциативный контекст Spring MDC, чтобы она записывалась в журнал каждым оператором журналирования в нашем микросервисе; внедрять трассировочную информацию Spring Cloud в каждый исходящий HTTP-вызов и в каждое сообщение, отправляемое в канал Spring. 11.1.2. Особенности трассировки в Spring Cloud Sleuth Если все настроено правильно, то любые инструкции журналирования в наших службах будут включать в записи трассировочную информацию Spring Cloud Sleuth. Например, на рис. 11.1 показано, как будет выглядеть вывод службы, если отправить HTTP GET следующей конечной точке службы организаций: http://localhost:8072/organization/v1/organization/95c0dab4-0a7e-48f8-805a➥ 0ba31c3687b8 Spring Cloud Sleuth и идентификатор корреляции 2. Идентификатор 1. Имя приложения: трассировки: уникальный имя приложения, идентификатор запроса которому принадпользователя, который лежит служба, будет передаваться в выполнившая вызовы всех служб, запись в журнал участвующих в обработке этого запроса 3. Идентификатор операции: уникальный идентификатор одной из операций, выполнявшихся в процессе обработки запроса. Когда в обработке запроса участвует несколько служб, каждой службе будет соответствовать свой идентификатор операции 371 4. Отправить в Zipkin: флаг, указывающий, будут ли данные отправляться на сервер Zipkin для трассировки Рис. 11.1 Spring Cloud Sleuth добавляет трассировочную информацию в каждую запись в журнале, создаваемую нашей службой организаций. Эта информация помогает объединить в цепочку вызовы служб, участвовавших в обработке запроса пользователя Spring Cloud Sleuth добавляет в каждую запись в журнале четыре элемента данных (пронумерованы в соответствии с номерами на рис. 11.1). 1 Имя приложения службы, выполнившей запись в журнал. По умолчанию Spring Cloud Sleuth использует имя приложения, указанное в параметре spring.application.name. 2 3 4 Идентификатор трассировки – эквивалент идентификатора корреляции. Это уникальный номер, представляющий всю транзакцию. Идентификатор операции (span ID) – уникальный идентификатор, представляющий часть общей транзакции. Каждая служба, участвующая в транзакции, получит свой идентификатор операции. Идентификаторы операций особенно важны для визуализации потока транзакции с использованием Zipkin. Флаг экспорта – логический признак, показывающий, отправляются ли данные трассировки в Zipkin. В крупномасштабных службах объем генерируемых данных трассировки может быть огромным и не представлять большой ценности, поэтому Spring Cloud Sleuth позволяет определить, когда и как отправлять транзакции в Zipkin. ПРИМЕЧАНИЕ. По умолчанию любая транзакция в приложении начинается с одинаковых идентификаторов трассировки и операции. До сих пор мы рассматривали только данные в журнале, полученные в результате обращения к единственной службе. Давайте теперь посмотрим, что произойдет при вызове службы лицензий. На рис. 11.2 показаны записи в журнале, зафиксировавшие вызовы двух служб. 372 Глава 11 Распределенная трассировка с использованием Spring ... Вызовы двух служб получают один и тот же идентификатор трассировки Каждая служба получает свой идентификатор операции Рис. 11.2. Когда в транзакцию вовлечено несколько служб, вызов каждой из них получает один и тот же идентификатор трассировки Как показано на рис. 11.2, вызовы служб лицензий и организаций получили один и тот же идентификатор трассировки 85f4c6e4a1738e77. Однако службе организаций был присвоен идентификатор операции 85f4c6e4a1738e77, совпадающий с идентификатором транзакции. Служба лицензий получила свой идентификатор операции 382ce00e427adf7b. Добавив лишь несколько зависимостей в файл .pom, мы заменили всю инфраструктуру идентификаторов корреляции, созданную в главах 7 и 8. 11.2. Агрегирование журналов и Spring Cloud Sleuth В крупномасштабном окружении микросервисов (особенно в облаке) журналирование является важным инструментом отладки. Однако, поскольку функциональность приложения на основе микросервисов разбита на небольшие службы, каждая из которых может быть запущена в нескольких экземплярах, отладка на основе данных в журналах нескольких служб может быть чрезвычайно сложным делом. Разработчикам, желающим отладить проблему на нескольких серверах, часто приходится проделывать следующее: войти на несколько серверов, чтобы исследовать журналы на каждом из них. Это чрезвычайно трудоемкая задача, особенно если рассматриваемые службы участвуют в транзакциях с разной частотой, что приводит к наполнению журналов с разной скоростью; писать свои сценарии для анализа журналов и поиска соответствующих записей в них. Поскольку каждая проблема может быть уникальной, в конечном итоге получается большое количество сценариев для запроса данных из наших журналов; Агрегирование журналов и Spring Cloud Sleuth 373 продлевать процедуру восстановления службы для резервного копирования журналов, находящихся на сервере. Если сервер, на котором находится служба, полностью выходит из строя, то журналы обычно теряются. Все это – реальные проблемы, с которыми мы часто сталкиваемся. Отладка на распределенных серверах – неприятная работа, которая часто увеличивает время, необходимое для выявления и решения проблемы. Намного проще оперативно передавать все журналы из всех экземпляров служб в централизованную точку агрегации, где данные могут быть проиндексированы и доступны для поиска. На рис. 11.3 на показано, как работает такая «унифицированная» архитектура журналирования. Экземпляр службы лицензий Экземпляр службы лицензий Экземпляр службы Экземпляр службы организаций организаций Экземпляр службы инвентаризации Механизм агрегирования собирает все данные и направляет их в общее хранилище Когда данные поступают в центральное хранилище, они индексируется и сохраняются в удобном для поиска формате Группы разработки и эксплуатации могут запрашивать данные из журналов, чтобы найти отдельные транзакции. Идентификаторы трассировки из записей в журнале позволяют связать друг с другом записи, добавленные разными службами Рис. 11.3. Агрегирование журналов и уникальные идентификаторы трассировки упрощают отладку распределенных транзакций К счастью, есть несколько продуктов, коммерческих и с открытым исходным кодом, которые могут помочь реализовать архитектуру журналирования, показанную на рис. 11.3. Кроме того, существует несколько моделей реализации, позволяющих выбирать между локальным решением с локальным управлением и облачным решением. В табл. 11.1 перечислено несколько вариантов организации инфраструктуры журналов. Глава 11 Распределенная трассировка с использованием Spring ... 374 Таблица 11.1. Решения агрегирования журналов, которые можно использовать совместно с фреймворком Spring Boot Название продукта Модели реализации Примечания Elasticsearch, Logstash, Kibana (стек ELK) Коммерческая С открытым исходным кодом https://www.elastic.co/what-is/elk-stack. Универсальный механизм поиска. Агрегирование журналов через стек ELK обычно реализуется локально Graylog Коммерческая С открытым исходным кодом https://www.graylog.org/. Предназначен для локальной установки Splunk Коммерческая https://www.splunk.com/. Самый старый и самый полный из инструментов управления журналами. Изначально локальное решение, в настоящее время имеет поддержку облачных окружений Sumo Logic Коммерческая Бесплатная в базовой комплектации с платными дополнительными возможностями https://www.sumologic.com/. Выполняется только как облачная служба. Для регистрации требуется корпоративная учетная запись (учетные записи Google или Yahoo не могут использоваться) Papertrail Коммерческая Бесплатная в базовой комплектации с платными дополнительными возможностями https://www.papertrail.com/. Выполняется только как облачная служба При таком богатстве вариантов может быть сложно выбрать лучший из них. Каждая организация уникальная, и в каждой имеются свои потребности. В этой главе мы рассмотрим интеграцию журналов в единую платформу с использованием ELK. Мы выбрали стек ELK, потому что: ELK распространяется с открытым исходным кодом; его легко настроить и использовать, и он удобен для пользователей; это полноценный инструмент, позволяющий искать, анализировать и визуализировать журналы, созданные различными службами; позволяет организовать централизованное хранение всех журналов для выявления проблем с сервером и приложением. 11.2.1. Интеграция Spring Cloud Sleuth и стека ELK На рис. 11.3 мы увидели унифицированную архитектуру журналирования. Теперь давайте посмотрим, как ту же архитектуру можно реализовать с использованием Spring Cloud Sleuth и стека ELK. Чтобы настроить ELK для работы в нашем окружении, необходимо выполнить следующие действия. 1 Настроить поддержку Logback в наших службах. 2 Настроить и запустить приложения стека ELK в контейнерах Docker. Агрегирование журналов и Spring Cloud Sleuth 3 4 375 Настроить Kibana. Протестировать реализацию, выполнив запросы с использованием идентификаторов корреляции, сгенерированных фреймворком Spring Cloud Sleuth. На рис. 11.4 показано конечное состояние нашей реализации. Здесь можно видеть, как Spring Cloud Sleuth и ELK интегрируются друг с другом. 2. Logstash принимает, преобразует и отправляет данные в Elasticsearch Контейнер Docker Служба лицензий От пр ав Контейнер Docker ля Служба организаций в Контейнер Docker Отправляет в тв Контейнер Docker Spring Cloud Gateway ет От пр яе авл Logstash 4. Kibana использует шаблонные индексы для извлечения данных из Elasticsearch. Здесь мы можем ввести идентификатор трассировки, сгенерированный фреймворком Spring Cloud Sleuth, и просмотреть все записи в журналах различных служб, которые содержат этот идентификатор трассировки Sends to Контейнер Docker 1. Отдельные контейнеры передают свои журналы приложению Logstash Контейнер Docker Запрос Elasticsearch Kibana 3. Индексирует и сохраняет данные в формате, удобном для поиска, чтобы приложение Kibana могло позже запросить их Рис. 11.4. Стек ELK позволяет быстро реализовать унифицированную архитектуру журналирования Как показано на рис. 11.4, службы лицензий и организаций и шлюз взаимодействуют с приложением Logstash, отправляя ему журналы. Logstash фильтрует, преобразует и передает данные в центральное хранилище (в данном случае Elasticsearch). Elasticsearch индексирует и сохраняет данные в формате, удобном для поиска, благодаря чему приложение Kibana может запросить 376 Глава 11 Распределенная трассировка с использованием Spring ... их позже. Kibana использует шаблонные индексы для извлечения данных из Elasticsearch. На данном этапе мы можем создать конкретный индекс и ввести идентификатор трассировки Spring Cloud Sleuth, чтобы получить из журналов разных служб все записи с этим идентификатором. После сохранения данных мы можем выполнять поиск в журналах в реальном времени, просто открыв Kibana. 11.2.2. Настройка Logback в службах Теперь, узнав, как с помощью ELK организовать архитектуру журналирования, приступим к настройке Logback в наших службах. Для этого сделаем следующее. 1 Добавим зависимость logstash-logback-encoder в файлы pom.xml наших служб. 2 Создадим приложение Logstash, добавляющее данные в Logback. Добавление кодера Logstash Для начала добавим в файлы pom.xml наших служб лицензий, организаций и шлюза зависимость logstash-logback-encoder. Файлы pom. xml можно найти в корневых каталогах проектов с исходным кодом. В каждый из от файлов pom.xml нужно добавить следующие строки: <dependency> <groupId>net.logstash.logback</groupId> <artifactId>logstash-logback-encoder</artifactId> <version>6.3</version> </dependency> Создание приложения Logstash для добавления данных После добавления зависимости в каждую службу нужно сообщить службе лицензий, что она должна взаимодействовать с Logstash для отправки журналов в формате JSON (по умолчанию Logback создает журналы в простом текстовом виде, но для индексирования в Elasticsearch журналы должны передаваться в формате JSON). Сделать это можно тремя способами: использовать класс net.logstash.logback.encoder.LogstashEncoder; использовать класс net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder; организовать преобразование простых текстовых журналов в Logstash. В этом примере мы будем использовать LogstashEncoder. Этот класс был выбран, потому что реализовать его легко и просто, а также потому что в этом примере не нужно добавлять дополни- Агрегирование журналов и Spring Cloud Sleuth 377 тельные поля в журнальные записи. Используя LoggingEventCompositeJsonEncoder, можно добавлять новые шаблоны или поля, отключать провайдеров по умолчанию и многое другое. Если выбрать один из этих двух классов, то за анализ файлов журнала в формате Logstash будет отвечать Logback. В третьем варианте мы можем полностью передать синтаксический анализ в Logstash с использованием фильтра JSON. Все три варианта хороши, но мы советуем выбирать LoggingEventCompositeJsonEncoder, когда требуется добавлять новые поля или удалять поля по умолчанию. Два других варианта полностью зависят от потребностей вашего бизнеса. ПРИМЕЧАНИЕ. Вы можете выбирать, где обрабатывать информацию, – в приложении или в Logstash. Для настройки этого кодировщика создадим конфигурационный файл Logback с именем logback-spring.xml. Этот файл должен находиться в папке resources службы. Конфигурация Logback для службы лицензий показана в листинге 11.1 и находится в файле /licensingservice/src/main/resources/logbackspring.xml. На рис. 11.5 показан журнал, созданный этой конфигурацией. Листинг 11.1. Настройки Logback с Logstash для службы лицензий <?xml version="1.0" encoding="UTF-8"?> <configuration> <include resource="org/springframework/boot/logging/logback/base.xml"/> <springProperty scope="context" name="application_name" source="spring.application.name"/> <appender name="logstash" class="net.logstash.logback.appender. LogstashTcpSocketAppender"> <destination>logstash:5000</destination> <encoder class="net.logstash.logback.encoder.LogstashEncoder"/> </appender> Имя хоста и порт Logstash для TCP-соединения <root level="INFO"> <appender-ref ref="logstash"/> <appender-ref ref="CONSOLE"/> </root> <logger name="org.springframework" level="INFO"/> <logger name="com.optimagrowth" level="DEBUG"/> </configuration> Указывает, что для связи с Logstash используется TcpSocketAppender 378 Глава 11 Распределенная трассировка с использованием Spring ... Рис. 11.5. Журнал приложения, отформатированный с помощью LogstashEncoder На рис. 11.5 можно видеть два важных аспекта. Во-первых, LogstashEncoder по умолчанию включает все значения, хранящиеся в Spring MDC, а во-вторых, поскольку мы добавили в службу зависимость Spring Cloud Sleuth, в журнальных записях появились поля TraceId, X-B3-TraceId, SpanId, X-B3-SpanId и spanExportable. Обратите внимание, что префикс X-B3 представляет заголовок по умолчанию, который Spring Cloud Sleuth передает от службы к службе. Это имя состоит из X, представляющего пользовательский заголовок, не являющийся частью спецификации HTTP, и B3, что означает «BigBrotherBird», прежнее название Zipkin. ПРИМЕЧАНИЕ. Желающим узнать больше о полях MDC мы советуем прочитать документацию для SL4J, доступную по адресу http://www.slf4j.org/manual.html#mdc, и документацию для Spring Cloud Sleuth, доступную по адресу https://cloud.spring. io/spring-cloud-static/spring-cloud-sleuth/2.1.0.RELEASE/single/ spring-cloud-sleuth.html. Также настроить форматирование журналов, как показано на рис. 11.5, можно с помощью LoggingEventCompositeJsonEncoder. Используя этот составной кодировщик, можно отключить всех провайдеров, которые были добавлены в конфигурацию по умолчанию, до- Агрегирование журналов и Spring Cloud Sleuth 379 бавить новые шаблоны для отображения пользовательских полей или полей MDC и многое другое. В листинге 11.2 показан небольшой пример конфигурации logback-spring.xml, которая удаляет некоторые поля и создает новый шаблон с пользовательским полем и другими стандартными полями. Листинг 11.2. Настройка конфигурации Logback для службы лицензий <encoder class="net.logstash.logback.encoder .LoggingEventCompositeJsonEncoder"> <providers> <mdc> <excludeMdcKeyName>X-B3-TraceId</excludeMdcKeyName> <excludeMdcKeyName>X-B3-SpanId</excludeMdcKeyName> <excludeMdcKeyName>X-B3-ParentSpanId</excludeMdcKeyName> </mdc> <context/> <version/> <logLevel/> <loggerName/> <pattern> <pattern> <omitEmptyFields>true</omitEmptyFields> { "application": { version: "1.0" }, "trace": { "trace_id": "%mdc{traceId}", "span_id": "%mdc{spanId}", "parent_span_id": "%mdc{X-B3-ParentSpanId}", "exportable": "%mdc{spanExportable}" } } </pattern> </pattern> <threadName/> <message/> <logstashMarkers/> <arguments/> <stackTrace/> </providers> </encoder> Для этого примера мы выбрали вариант с LogstashEncoder, но вообще вы должны выбирать вариант, который лучше соответствует вашим потребностям. Теперь, имея готовую конфигурацию Logback в службе лицензий, добавьте такую же конфигурацию в другие наши службы: организаций и шлюз. После этого мы можем перейти к определению и запуску приложений ELK в контейнерах Docker. Глава 11 Распределенная трассировка с использованием Spring ... 380 11.2.3. Определение и запуск приложений ELK в Docker Для настройки контейнеров с приложениями ELK нужно выполнить два простых шага. Первый – создать конфигурационный файл Logstash, и второй – определить приложения ELK в конфигурации Docker. Однако, прежде чем приступить к созданию конфигурации, отметим, что конвейер Logstash имеет два обязательных компонента и один необязательный. Обязательными являются ввод и вывод: ввод позволяет Logstash читать данные из разных источников событий. Logstash поддерживает различные плагины ввода, такие как GitHub, Http, TCP, Kafka и др.; вывод отвечает за отправку данных в конкретное место назначения. Logstash поддерживает различные плагины вывода, такие как CSV, Elasticsearch, электронная почта, файл, MongoDB, Redis, stdout и др. Необязательный компонент в конфигурации Logstash – это плагины фильтров. Фильтры отвечают за промежуточную обработку событий, например перевод на другой язык, добавление новой информации, анализ дат, усечение полей и т. д. Помните, что задача Logstash – прием и преобразование данных. Диаграмма на рис. 11.6 иллюстрирует работу процесса Logstash. Плагины ввода (обязательные элементы) принимают данные из источника Плагины вывода (обязательные элементы) записывают данные в место назначения Logstash Служба Elasticsearch Ввод Фильтр Вывод Плагины фильтров (необязательные элементы) изменяют данные Рис. 11.6. Конвейер Logstash содержит два обязательных компонента (ввод и вывод) и один необязательный (фильтр) В этом примере в роли плагина ввода мы используем приложение Logback, которое настроили ранее, а в роли плагина вывода – механизм Elasticsearch. В листинге 11.3 показан файл /docker/ config/logstash.conf. Агрегирование журналов и Spring Cloud Sleuth 381 Листинг 11.3. Конфигурационный файл с настройками Logstash input { tcp { port => 5000 Порт codec => json_lines Logstash } } Плагин ввода TCP для чтения событий из сокета TCP filter { mutate { add_tag => [ "manningPublications" ] } } Фильтр mutate, добавляющий тег в события Плагин вывода Elasticsearch для отправки данных в Elasticsearch output { elasticsearch { hosts => "elasticsearch:9200" } } Порт Elasticsearch В листинге 11.3 можно видеть пять основных элементов. Первый – раздел input. В этом разделе мы настроили параметры плагина tcp для ввода данных, в том числе номер порта 5000, который позже мы укажем в настройках Logstash в файле docker-compose. yml (если еще раз взглянуть на рис. 11.4, то можно заметить, что службы будут посылать данные непосредственно в Logstash). Третий элемент является необязательным и определяет настройки фильтров. В этом конкретном сценарии мы используем фильтр mutate. Этот фильтр добавляет в события тег manningPublications. В действующих приложениях в роли тега можно использовать название окружения, в котором выполняется приложение. Наконец, четвертый и пятый элементы определяют плагин вывода из Logstash, который отправляет обработанные данные в службу Elasticsearch, прослушивающую порт 9200. Желающим узнать больше обо всех плагинах ввода, вывода и фильтрации, поддерживаемых в Elastic, мы рекомендую посетить следующие страницы: https://www.elastic.co/guide/en/logstash/current/input-plugins. html; https://www.elastic.co/guide/en/logstash/current/outputplugins.html; https://www.elastic.co/guide/en/logstash/current/filter-plugins. html. Теперь, закончив настройку Logstash, добавим три записи в файл docker-compose.yml. Как вы наверняка помните, этот файл используется для запуска всех контейнеров Docker с примерами кода в этой и предыдущих главах. В листинге 11.4 показан файл docker/ docker-compose.yml с новыми записями. 382 Глава 11 Распределенная трассировка с использованием Spring ... Листинг 11.4. Настройка запуска стека ELK в Docker # Часть файла docker-compose.yml опущена для краткости # Некоторые дополнительные настройки опущены для краткости elasticsearch: image: docker.elastic.co/elasticsearch/ elasticsearch:7.7.0 Образ контейнера с Elasticsearch container_name: elasticsearch (в данном случае версия 7.7.0) volumes: - esdata1:/usr/share/elasticsearch/data Открывает порт 9300 ports: для связи с кластером - 9300:9300 - 9200:9200 Открывает порт 9200 для запросов REST kibana: image: docker.elastic.co/kibana/kibana:7.7.0 container_name: kibana environment: ELASTICSEARCH_URL: "http://elasticsearch:9300" ports: Открывает порт - 5601:5601 для доступа к вебприложению Kibana Образ контейнера с Kibana URL Elasticsearch, определяющий имя сервера и порт 9300 для доступа к API logstash: image: docker.elastic.co/logstash/logstash:7.7.0 Образ контейнера container_name: logstash с Logstash command: logstash -f /etc/logstash/conf.d/logstash.conf Загружает конфиvolumes: гурацию Logstash - ./config:/etc/logstash/conf.d. из указанного файports: ла или каталога Открывает порт для - "5000:5000" Монтирует каталог с конфигудоступа к Logstash рационным файлом в запущенный контейнер Logstash # Остальная часть файла docker-compose.yml опущена для краткости ПРИМЕЧАНИЕ. В листинге 11.4 представлена лишь часть файла docker-compose.yml для этой главы. Полный файл вы найдете по адресу https://github.com/ihuaylupo/manning-smia/tree/master/ chapter11/docker. Чтобы запустить окружение Docker, нужно выполнить следующие команды в корневом каталоге с родительским pom.xml. Команда mvn создает новый образ с изменениями, внесенными в службы организаций, лицензий и шлюза: mvn clean package dockerfile:build docker-compose -f docker/docker-compose.yml up Агрегирование журналов и Spring Cloud Sleuth 383 ПРИМЕЧАНИЕ. Если вы получили код ошибки 137 при попытке запустить контейнер <container_name> командой docker-compose, то перейдите по следующей ссылке, чтобы узнать, как выделить больше памяти для Docker: https://www.petefreitag.com/ item/848.cfm. Теперь, настроив и запустив окружение Docker, сделаем следующий шаг. 11.2.4. Настройка Kibana Настройка Kibana – простая задача, которую нужно выполнить только один раз. Чтобы получить доступ к Kibana, откройте в браузере страницу http://localhost:5601/. При первом обращении к Kibana вы увидите страницу приветствия. На этой странице предлагается выбрать один из двух вариантов. Первый позволяет поэкспериментировать с некоторыми образцами данных, а второй дает возможность исследовать данные, сгенерированные нашими службами. На рис. 11.7 показана страница приветствия Kibana. Выберите этот вариант Рис. 11.7. Страница приветствия Kibana с двумя вариантами: Try our sample data (Попробовать с нашими примерами данных) и Explore it on your own (Исследовать свои) Для исследования наших данных выберите вариант Explore it on your own (Исследовать свои). После щелчка вы увидите страницу Add Data (Добавить данные), как показано на рис. 11.8. На этой странице щелкните на значок Discover (Обнаружить) в левой части страницы. 384 Глава 11 Распределенная трассировка с использованием Spring ... Значок Discover (Обнаружить) Рис. 11.8. Страница настройки Kibana. Здесь можно видеть набор параметров настройки приложения Kibana. Обратите внимание на меню со значками слева Чтобы продолжить, мы должны создать шаблон индекса. Kibana использует набор шаблонов индексов для извлечения данных из Elasticsearch. С помощью шаблонов мы можем сообщить приложению Kibana, какие индексы Elasticsearch будем исследовать. Например, в нашем примере мы создадим шаблон индекса для получения всей информации из Elasticsearch. Чтобы создать шаблон, щелкните на ссылке Index Patterns (Шаблоны индексов) в разделе Kibana слева. На рис. 11.9 показан первый шаг этого процесса. Шаблон индекса logstash-* После настройки шаблона индекса следует перейти к следующему шагу Рис. 11.9. Настройка шаблона индекса для извлечения данных из Elasticsearch На странице Create Index Pattern (Создать шаблон индекса), изображенной на рис. 11.9, можно видеть, что Logstash уже определил заготовку индекс. Однако этот индекс еще не готов к использованию. Чтобы завершить настройку, нужно добавить шаблонный символ, чтобы получить шаблон индекса logstash- *, и затем щелкнуть на кнопке Next Step (Следующий шаг). Агрегирование журналов и Spring Cloud Sleuth 385 На шаге 2 определим временной фильтр. Для этого выберите параметр @timestamp в раскрывающемся списке Time Filter Field Name (Имя поля для фильтра по времени) и щелкните на кнопке Create Index Pattern (Создать шаблон индекса). Этот процесс изображен на рис. 11.10. @timestamp Рис. 11.10. Настройка фильтра по времени для нашего шаблона индекса. Этот шаблон позволяет отбирать события из определенного диапазона времени Теперь мы можем начать посылать запросы к нашим службам и просматривать журналы в реальном времени в Kibana. На рис. 11.11 показан пример, как должны выглядеть данные, отправленные в ELK. Если вы не видите эту страницу, то еще раз щелкните на значке Discover (Обнаружить). Запросов в секунду Отдельные события из журналов Рис. 11.11. Отдельные события из журналов сохраняются, анализируются и отображаются стеком ELK 386 Глава 11 Распределенная трассировка с использованием Spring ... Мы закончили настраивать Kibana, и можно переходить к следующему и заключительному шагу. 11.2.5.Поиск идентификаторов трассировки Spring Cloud Sleuth в Kibana Теперь, когда наши журналы передаются в ELK, можно заняться исследованием – как Spring Cloud Sleuth добавляет идентификаторы трассировки в наши журнальные записи. Чтобы запросить все записи из журнала, относящиеся к одной транзакции, достаточно взять идентификатор трассировки и ввести его на странице Discover (Обнаружить) Kibana (рис. 11.12). По умолчанию Kibana использует язык Kibana Query Language (KQL), предлагающий упрощенный синтаксис запросов. При вводе запроса вы увидите, что Kibana предоставляет также функцию автодополнения, помогая писать запросы. ПРИМЕЧАНИЕ. Чтобы применить следующий фильтр, нужно выбрать действительный идентификатор трассировки. Идентификатор трассировки, используемый в следующем примере, не будет работать в вашем экземпляре Kibana. На рис. 11.12 показано, как выполнить запрос с идентификатором трассировки Spring Cloud Sleuth. Здесь мы используем идентификатор 3ff985508b1b9365. При желании можно распахнуть каждое событие, чтобы увидеть более подробную информацию. При этом все поля, связанные с конкретным событием, будут отображаться в виде таблицы или в формате JSON. При этом также будет отображена вся дополнительная информация, добавленная или полученная во время обработки в Logstash, как, например, тег, добавленный фильтром mutate (листинг 11.3). На рис. 11.13 показаны все поля для этого события. Агрегирование журналов и Spring Cloud Sleuth 387 Идентификатор трассировки, добавленный фреймворком Spring Cloud Sleuth и который мы собираемся запросить Как показывают результаты, полученные в ответ на запрос, в рамках одной транзакции вызывались разные службы Рис. 11.12. Идентификатор трассировки позволяет извлечь записи из журнала, относящиеся к одной транзакции Шаблон индекса с отметкой времени Имя приложения Spring Logger class Идентификатор операции, добавленный фреймворком Spring Cloud Sleuth Идентификатор трассировки, добавленный фреймворком Spring Cloud Sleuth Тег, добавленный с помощью Logstash Рис. 11.13. Подробное представление всех полей событий в Kibana 388 Глава 11 Распределенная трассировка с использованием Spring ... 11.2.6.Добавление идентификатора корреляции в HTTP-ответ с помощью Spring Cloud Gateway Если исследовать HTTP-ответ любой службы, вызванной с помощью Spring Cloud Sleuth, то можно обнаружить, что они никогда не возвращают идентификатор трассировки в ответе. В документации разработчики Spring Cloud Sleuth указывают, что возврат любых данных трассировки может быть потенциальной угрозой безопасности (но не указывают конкретные причины, почему они так считают). Мы же считаем, что возврат идентификатора корреляции или трассировки в HTTP-ответе может оказать огромную помощь при отладке. Spring Cloud Sleuth позволяет добавить в HTTP-ответ идентификаторы трассировки и операции. Однако для этого необходимо написать три класса и внедрить два пользовательских bean-компонента Spring. Если вы захотите узнать больше об этом способе, то обращайтесь к документации Spring Cloud Sleuth по адресу https://cloud.spring.io/spring-cloud-static/spring-cloudsleuth/1.0.12.RELEASE/. Мы же предпочитаем гораздо более простое решение – написать фильтр для Spring Cloud Gateway, добавляющий идентификатор трассировки в HTTP-ответ. В главе 8, где мы рассказывали о Spring Cloud Gateway, было показано, как создать фильтр для изменения ответов, возвращаемых шлюзом, и добавить в них сгенерированные идентификаторы корреляции. Теперь мы изменим этот фильтр и добавим в ответ заголовок Spring Cloud Sleuth. Для этого нужно подключить зависимости Spring Cloud Sleuth в файле pom.xml с настройками шлюза, как показано ниже: <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-sleuth</artifactId> </dependency> Мы используем зависимость spring-cloud-starter-sleuth, чтобы сообщить фреймворку Spring Cloud Sleuth, что шлюз тоже должен участвовать в трассировке. Далее в этой главе во время знакомства с Zipkin мы увидим, что служба шлюза будет первой в любых обращениях к приложению. После добавления зависимости осталось лишь реализовать фактический фильтр. В листинге 11.5 показан исходный код фильтра. Вы найдете его в файле /gatewayserver/src/main/java/com/optimagrowth/ gateway/filters/ResponseFilter.java. Агрегирование журналов и Spring Cloud Sleuth 389 Листинг 11.5. Добавление идентификатора трассировки в ответ с помощью фильтра package com.optimagrowth.gateway.filters; // Инструкции импорта опущены для краткости. import brave.Tracer; import reactor.core.publisher.Mono; @Configuration public class ResponseFilter { final Logger logger =LoggerFactory.getLogger(ResponseFilter.class); @Autowired Tracer tracer; @Autowired FilterUtils filterUtils; Устанавливает точку входа для доступа к идентификаторам трассировки и операции @Bean public GlobalFilter postGlobalFilter() { return (exchange, chain) -> { return chain.filter(exchange) .then(Mono.fromRunnable(() -> { Добавляет идентификаторы трассировки String traceId = в заголовок Tracer.currentSpan() tmx-correlation-ID .context() HTTP-ответа .traceIdString(); logger.debug("Adding the correlation id to the outbound headers. {}",traceId); exchange.getResponse().getHeaders() .add(FilterUtils.CORRELATION_ID,traceId); logger.debug("Completing outgoing request for {}.", exchange.getRequest().getURI()); })); }; } } Поскольку теперь шлюз поддерживает Spring Cloud Sleuth, мы можем получить доступ к информации о трассировке в нашем фильтре ResponseFilter за счет автоматического внедрения экземпляра класса Tracer. Этот класс позволяет получить доступ к текущей информации трассировки. Метод tracer.currentSpan().Context().TraceIdString() возвращает идентификатор трассировки для текущей транзакции в виде строки. Чтобы добавить идентификатор трассировки в исходящий HTTP-ответ достаточно вызвать следующий метод: 390 Глава 11 Распределенная трассировка с использованием Spring ... exchange.getResponse().getHeaders() .add(FilterUtils.CORRELATION_ID, traceId); Если теперь вызвать микросервис O-stock через наш шлюз, то мы должны получить HTTP-ответ с заголовком tmx-correlation-id, содержащим идентификатор трассировки Spring Cloud Sleuth. На рис. 11.14 показаны результаты, полученные в ответ на следующий вызов: http://localhost:8072/license/v1/organization/4d10ec24-141a-4980-be34➥ 2ddb5e0458c7/license/4af05c3b-a0f3-411d-b5ff-892c62710e14 Идентификатор трассировки, сгенерированный фреймворком Spring Cloud Sleuth Рис. 11.14. Получив идентификатор трассировки Spring Cloud Sleuth в ответе, мы сможем запросить информацию о нем в Kibana 11.3. Распределенная трассировка с использованием Zipkin Унифицированная платформа журналирования с идентификаторами корреляции – мощный инструмент отладки. Однако в оставшейся части этой главы мы оставим трассировку по записям в журнале и рассмотрим приемы визуализации потоков транзакций по мере их продвижения по микросервисам. Яркая и выразительная картинка стоит больше миллиона записей в журнале. Распределенная трассировка включает также создание визуального представления прохождения транзакции через различные микросервисы. Инструменты распределенной трассировки позволяют получить приблизительное представление о времени отклика отдельных микросервисов. Однако не следует путать инструменты распределенной трассировки с полноценными пакетами управления производительностью приложений (Application Performance Management, APM). Пакеты APM поставляют готовые низкоуровневые данные о производительности фактического программного кода, а также такие метрики, как потребление памяти, процессорного времени и ввода/вывода. Распределенная трассировка с использованием Zipkin 391 В этой сфере особенно ярко выделяются проекты Spring Cloud Sleuth и Zipkin (также называется OpenZipkin). Zipkin (http:// zipkin.io/) – это платформа распределенной трассировки, позволяющая наблюдать за выполнением транзакций, которые охватывают несколько служб. Она помогает графически представить время, которое занимает транзакция, и разбить его на интервалы, потраченные в каждом микросервисе, участвовавшем в обработке запроса. Zipkin – ценный инструмент для выявления проблем с производительностью в микросервисной архитектуре. Настройка комбинации Spring Cloud Sleuth и Zipkin включает: включение JAR-файлов Spring Cloud Sleuth и Zipkin в службы, собирающие трассировочные данные; настройку в каждой службе свойства со ссылкой на сервер Zipkin, который будет собирать трассировочные данные; установку и настройку сервера Zipkin для сбора данных; определение стратегии сбора данных, которую каждый клиент будет использовать для отправки информации серверу Zipkin. 11.3.1. Настройка зависимостей Spring Cloud Sleuth и Zipkin Мы уже добавили зависимости Spring Cloud Sleuth в наши службы лицензий и организаций. Эти файлы JAR включают необходимые библиотеки с поддержкой возможностей Spring Cloud Sleuth. Теперь нам нужно добавить новую зависимость – spring-cloud-sleuthzipkin, – обеспечивающую интеграцию с Zipkin. В листинге 11.6 показаны строки, которые следует в файлы служб Spring Cloud Gateway, лицензий и организаций. Листинг 11.6. Клиентские зависимости Spring Cloud Sleuth и Zipkin <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-sleuth-zipkin</artifactId> <dependency> 11.3.2. Настройка в службах ссылки на сервер Zipkin После добавления файлов JAR мы должны настроить все службы, которые будут взаимодействовать с Zipkin, определив свойство spring.zipkin.baseUrl с URL сервера Zipkin. Это свойство должно присутствовать во всех конфигурационных файлах служб, находящихся в репозитории Spring Cloud Config Server (например, в файле службы лицензий / configserver/src/main/resources/config/licensing-service.properties). Для опробования в локальной системе присвойте свойству baseUrl значение localhost:9411. Однако, если вы собираетесь запускать приложение в Docker, то присвойте значение zipkin:9411, как показано ниже: zipkin.baseUrl: zipkin:9411 392 Глава 11 Распределенная трассировка с использованием Spring ... Zipkin, RabbitMQ и Kafka Zipkin позволяет отправлять трассировочные данные на сервер с использованием очередей RabbitMQ или Kafka. С функциональной точки зрения для Zipkin нет никакой разницы, как отправлять данные, – через HTTP, RabbitMQ или Kafka. Для отправки данных по протоколу HTTP клиентская часть Zipkin запускает асинхронный поток выполнения. Основное преимущество использования очередей RabbitMQ или Kafka для сбора данных заключается в том, что если сервер Zipkin не работает, то сообщения, отправленные ему, будут храниться в очереди, пока тот не сможет получить их. Настройка Spring Cloud Sleuth для отправки данных в Zipkin через RabbitMQ и Kafka описана в документации Spring Cloud Sleuth, поэтому мы не будем рассматривать ее здесь. 11.3.3. Настройка сервера Zipkin Есть несколько способов настройки Zipkin. Мы используем контейнер Docker с сервером Zipkin. Этот вариант позволит избежать создания нового проекта в нашей архитектуре. Чтобы настроить сервер Zipkin, добавим следующую запись в файл docker-compose. yml, находящийся в папке Docker проекта: zipkin: image: openzipkin/zipkin container_name: zipkin ports: - 9411: 9411 networks: backend: aliases: - "zipkin" Для запуска сервера Zipkin требуется выполнить еще несколько настроек, в том числе указать внутреннее хранилище данных, которое Zipkin будет использовать для хранения трассировочных данных. Zipkin поддерживает четыре вида хранилищ: данные в памяти; MySQL (http://mysql.com/); Cassandra (https://cassandra.apache.org/); Elasticsearch (http://elastic.co/). По умолчанию данные хранятся в памяти. Однако команда Zipkin не рекомендует использовать оперативную память в качестве хранилища в промышленных системах, потому что объем оперативной памяти ограничен и данные теряются, когда сервер Zipkin выключается или выходит из строя. В этом примере мы покажем, как в роли хранилища использовать Elasticsearch, потому что мы уже настроили его. Единственные допол- Распределенная трассировка с использованием Zipkin 393 нительные настройки, которые нужно добавить, – это переменные STORAGE_TYPE и ES_HOSTS в разделе environment в конфигурационном файле. В следующем листинге показан полный реестр Docker Compose: zipkin: image: openzipkin/zipkin container_name: zipkin depends_on: - elasticsearch environment: - STORAGE_TYPE=elasticsearch - "ES_HOSTS=elasticsearch:9300" ports: - "9411:9411" networks: backend: aliases: - "zipkin" 11.3.4. Настройка уровней трассировки Теперь у нас есть клиенты, подготовленные к взаимодействию с сервером Zipkin, и сервер, готовый к работе. Однако, прежде чем начать использовать Zipkin, мы должны сделать еще один шаг – определить, как часто каждая служба должна отправлять данные в Zipkin. По умолчанию клиентская часть Zipkin отправляет на сервер только 10 % всех транзакций. Значение по умолчанию гарантирует, что наша инфраструктура журналирования и анализа не будет испытывать перегрузки. Доля выбираемых транзакций определяется свойством spring. sleuth.sampler.percentage в настройках службы, отправляющей данные. Это свойство принимает значение от 0 до 1, где: значение 0 означает, что Spring Cloud Sleuth не будет отправлять в Zipkin никаких транзакций; значение .5 означает, что Spring Cloud Sleuth будет отправлять 50 % всех транзакций; значение 1 означает, что Spring Cloud Sleuth будет отправлять все транзакции (100 %). Для целей демонстрации мы будем отправлять все транзакции (100 %). Для этого можно установить значение в свойстве spring. sleuth.sampler.percentage или заменить класс Sampler, используемый в Spring Cloud Sleuth по умолчанию, на AlwaysSampler. Класс AlwaysSampler можно внедрить в приложение как bean-компонент Spring. Но в этом примера мы пойдем более простым путем и используем свойство spring.sleuth.sampler.percentage в конфигурационных файлах служб лицензий, организаций и шлюза: zipkin.baseUrl: zipkin:9411 spring.sleuth.sampler.percentage: 1 394 Глава 11 Распределенная трассировка с использованием Spring ... 11.3.5. Использование Zipkin для трассировки транзакций Начнем этот раздел с описания сценария. Представьте, что вы – один из разработчиков приложения O-stock и на этой неделе вам выпало дежурство. Заступив на дежурство, вы получаете запрос клиента с жалобой на медленную работу одной из страниц приложения. У вас есть подозрение, что в этом виновата служба лицензий, используемая страницей. Но как подтвердить это подозрение? В нашем сценарии служба лицензий вызывает службу организаций, и обе службы обращаются к разным базам данных. Какая служба замедляет работу? Также вы знаете, что эти службы постоянно модифицируются, поэтому кто-то мог добавить в них вызов новой службы. ПРИМЕЧАНИЕ. Знание списка всех служб, участвующих в обработке транзакции пользователя, и времени выполнения каждой из них имеет решающее значение для поддержки распределенной архитектуры, такой как микросервисная архитектура. Чтобы решить эту задачу, используем Zipkin и понаблюдаем за обработкой двух транзакций службой организаций. Служба организаций – очень простая, она обращается к единственной базе данных и больше ничего не делает. Давайте отправим ей с помощью Postman два вызова в следующую конечную точку. Запросы к службе организаций попадут сначала в шлюз, и только потом в экземпляр службы организаций: GET http://localhost:8072/organization/v1/organization/4d10ec24141a-4980➥ be34-2ddb5e0458c6 Посмотрев на рис. 11.15 со снимком экрана, можно заметить, что Zipkin обнаружил две транзакции, и каждая транзакция делится на несколько операций. В Zipkin каждая операция представлена вызовом конкретной службы и фиксирует информацию о времени ее выполнения. Каждая транзакция на рис. 11.15 делится на пять операций: две операции – обработка транзакции в шлюзе, две – обработка в службе организаций и одна – в службе лицензий. Напомним, что шлюз не просто пересылает HTTP-запрос. Он получает входящий HTTP-запрос, завершает его и создает новый для отправки целевой службе (в данном случае службе организаций). Именно так он получает возможность обрабатывать запросы и ответы предварительными и заключительными фильтрами. По этой же причине мы видим на рис. 11.15 две операции, принадлежащие службе шлюза. Распределенная трассировка с использованием Zipkin Вызываемая служба 395 Фильтры выбора по времени Результаты вызовов Рис. 11.15. Страница анализа запросов в Zipkin, где можно выбрать службу для трассировки и настроить некоторые базовые фильтры Два вызова службы организаций через шлюз заняли 1,151 с и 39,152 мс соответственно. Давайте детально рассмотрим самый долгий вызов (длившийся 1,151 с). Более подробную информацию можно получить, щелкнув на транзакции и распахнув ее. На рис. 11.16 показаны детали выполнения службы организаций. Транзакция разбита на отдельные операции. Операция представляет часть измеряемой транзакции. Здесь отображается время выполнения каждой операции в транзакции Распахнув одну из транзакций, вы увидите пять операций: две соответствуют обработке шлюзом, две – службой организаций и одна – службой лицензий Щелкнув на выбранной операции, вы получите дополнительные сведения о ней Рис. 11.16. Zipkin позволяет получить более детальную информацию о времени выполнения каждой операции На рис. 11.16 видно, что с точки зрения шлюза на выполнение транзакции потребовалось примерно 1,151 с. Однако вызов службы организаций занял 524,689 мс из 1,151 с общего времени. Давайте исследуем каждую операцию подробнее. Распахнем операцию organization-service щелчком мыши на ней и исследуем дополнительные сведения о вызове (рис. 11.17). 396 Глава 11 Распределенная трассировка с использованием Spring ... Щелкнув на операции, можно получить более детальную информацию о времени, когда шлюз вызвал службу организаций, когда служба организаций получила запрос и когда клиент получил ответ Щелкнув здесь, можно получить некоторые дополнительные сведения о HTTP-вызове Рис. 11.17. Щелчок на операции открывает страницу с дополнительными сведениями о HTTP-вызове Особую ценность на рис. 11.17 представляет разбивка по времени: когда клиент (шлюза) вызвал службу организаций, когда служба организаций получила вызов и когда она ответила. Эта информация о времени бесценна при поиске проблем, связанных с задержками в сети. Чтобы добавить дополнительную операцию, соответствующую вызову сервера Redis в службе лицензий, используем следующий класс: /licensing-service/src/main/java/com/optimagrowth/license/service/ client/ ➥ OrganizationRestTemplateClient.java В OrganizationRestTemplateClient мы реализуем метод checkRedisCache(), как показано в листинге 11.7. Распределенная трассировка с использованием Zipkin 397 Листинг 11.7. Метод checkRedisCache() в классе OrganizationRestTemplateClient package com.optimagrowth.license.service.client; // Инструкции импорта опущены для краткости @Component public class OrganizationRestTemplateClient { @Autowired RestTemplate restTemplate; @Autowired Tracer tracer; Tracer используется для доступа к трассировочной информации, генерируемой фреймворком Spring Cloud Sleuth @Autowired OrganizationRedisRepository redisRepository; private static final Logger logger = LoggerFactory.getLogger(OrganizationRestTemplateClient.class); private Organization checkRedisCache Реализация метода (String organizationId) { CheckRedisCache try { return redisRepository.findById(organizationId).orElse(null); } catch (Exception ex){ logger.error("Error encountered while trying to retrieve organization {} check Redis Cache. Exception {}", organizationId, ex); return null; } } // Остальная часть определения класса опущена для краткости } 11.3.6. Визуализация более сложных транзакций Что можно сделать, чтобы лучше понять, как взаимосвязаны службы? Можно, например, вызвать службу лицензий через шлюз, используя следующую конечную точку, а затем запросить у Zipkin трассировку вызова GET службы лицензий, как показано на рис. 11.18: http://localhost:8072/license/v1/organization/4d10ec24-141a-4980-be34➥ 2ddb5e0458c8/license/4af05c3b-a0f3-411d-b5ff-892c62710e15 На рис. 11.18 можно видеть, что вызов службы лицензий включает восемь HTTP-вызовов: вызов шлюза клиентом; вызов службы лицензий шлюзом; вызов шлюза службой лицензий; вызов службы организаций шлюзом и, наконец, вызов службы лицензий службой организаций посредством Apache Kafka для обновления кеша Redis. 398 Глава 11 Распределенная трассировка с использованием Spring ... Рис. 11.18. Детали трассировки, показывающие, как протекает вызов службы лицензий от шлюза к службе лицензий и затем к службе организаций 11.3.7. Трассировка операций обмена сообщениями Обмен сообщениями может создавать дополнительные проблемы с производительностью и задержками внутри приложения. Служба может недостаточно быстро обрабатывать сообщения из очереди. Или могут возникать проблемы с задержкой в сети. Мы не раз встречались с этими сценариями в приложениях на основе микросервисов. Spring Cloud Sleuth отправляет в Zipkin данные трассировки о любых каналах входящих или исходящих сообщений, зарегистрированных в службе. Используя Spring Cloud Sleuth и Zipkin, можно определить, когда сообщение было опубликовано в очереди и когда оно было получено. Также можно увидеть, какое поведение имело место при получении сообщения из очереди и его обработке. Как рассказывалось в главе 10, при добавлении, обновлении или удалении записи об организации с помощью Spring Cloud Stream создается сообщение и публикуется в очереди Kafka. Служба лицензий получает сообщение и обновляет хранилище пар ключ/ значение Redis, которое она использует для кеширования данных. Давайте попробуем удалить запись об организации из кеша и посмотрим, как Spring Cloud Sleuth и Zipkin трассируют эту транзакцию. Для этого отправим запрос DELETE следующей конечной точке службы организаций с помощью Postman: http://localhost:8072/organization/v1/organization/4d10ec24-141a-4980-be34➥ 2ddb5e0458c7 Выше в этой главе мы видели, как добавить идентификатор трассировки в HTTP-ответ в виде значения заголовка с именем tmx-correlation-id. В этом примере мы получили в ответе заголовок tmx-correlation-id со значением 054accff01c9ba6b. Теперь найдем в Zipkin эту конкретную транзакцию, введя полученный идентификатор трассировки в поле поиска в правом верхнем углу, как показано на рис. 11.19. Имея идентификатор трассировки, можно запросить в Zipkin информацию о транзакции и просмотреть, как обрабатывалось сообщение DELETE. Вторая операция на рис. 11.20 – это вывод Распределенная трассировка с использованием Zipkin 399 сообщения в канал output, который используется для публикации в очереди Kafka под названием orgChangeTopic. На рис. 11.20 показан вывод сообщения в канал и как эта операция отображается в трассировке Zipkin. Введите сюда идентификатор трассировки и нажмите Enter. В результате вы получите искомую транзакцию Рис. 11.19. Имея идентификатор трассировки, полученный в заголовке tmxcorrelation-id HTTP-ответа, можно найти соответствующую транзакцию Время удаления информации об организации с помощью вызова DELETE; фреймворк Spring Cloud Sleuth зафиксировал публикацию сообщения Рис. 11.20. Spring Cloud Sleuth автоматически трассирует публикацию и получение сообщений. Мы можем просмотреть эту информацию с помощью Zipkin Чтобы увидеть, когда служба лицензий получила сообщение, можно щелкнуть на операции, соответствующей службе лицензий (см. рис. 11.21). До сих пор мы использовали Zipkin для трассировки HTTPвызовов и передачи сообщений в наших службах. Но что, если потребуется выполнить трассировку сторонних служб, которые не взаимодействуют с Zipkin, например получить информацию о времени выполнения вызова Redis или Postgres SQL? К счастью, Spring Cloud Sleuth и Zipkin позволяют добавлять промежуточные операции в наши транзакции для трассировки времени выполнения сторонних служб. 400 Глава 11 Распределенная трассировка с использованием Spring ... Вы можете увидеть, когда было получено сообщение из канала inboundOrgChanges Рис. 11.21. Zipkin позволяет увидеть получение сообщения из очереди Kafka, опубликованное службой организаций 11.3.8. Добавление дополнительных операций Zipkin позволяет добавлять промежуточные операции в трассировки. Давайте добавим такую операцию в нашу службу лицензий, чтобы можно было видеть, сколько времени требуется на получение данных из Redis. Затем мы добавим дополнительную операцию в службу организаций, чтобы узнать, сколько времени требуется для получения информации из базы данных организаций. В листинге 11.8 создается дополнительная операция с именем readLicensingDataFromRedis для службы лицензий. Листинг 11.8. Добавление дополнительной операции readLicensingDataFromRedis package com.optimagrowth.license.service.client; // Часть кода опущена для краткости private Organization checkRedisCache(String organizationId) { ScopedSpan newSpan = Создает дополнительtracer.startScopedSpan ную операцию с именем ("readLicensingDataFromRedis"); readLicensingDataFromRedis try { return redisRepository.findById(organizationId).orElse(null); } catch (Exception ex){ logger.error("Error encountered while trying to retrieve organization {} check Redis Cache. Exception {}", organizationId, ex); return null; } finally { Распределенная трассировка с использованием Zipkin } } newSpan.tag("peer.service", "redis"); newSpan.annotate("Client received"); newSpan.finish(); Закрывает и завершает операцию. Если этого не сделать, то мы получим сообщение об ошибке в журнале о том, что операция осталась открытой 401 Добавляет в операцию тег с информацией о службе и операции, которую зафиксирует Zipkin Затем добавим дополнительную операцию с именем getOrgDbCall в службу организаций, чтобы увидеть, сколько времени требуется для получения информации из базы данных Postgres. Трассировку вызовов базы данных можно увидеть в классе OrganizationService, в файле /organization-service/src/main/java/com/optimagrowth/ organization/service/OrganizationService.java. Метод findById() включает создание дополнительной операции для трассировки. В листинге 11.9 показан исходный код этого метода. Листинг 11.9. Реализация метода findById() package com.optimagrowth.organization.service; // Некоторые инструкции импорта опущены для краткости import brave.ScopedSpan; import brave.Tracer; @Service public class OrganizationService { // Часть кода опущена для краткости @Autowired Tracer tracer; public Organization findById(String organizationId) { Optional<Organization> opt = null; ScopedSpan newSpan = tracer.startScopedSpan("getOrgDBCall"); try { opt = repository.findById(organizationId); simpleSourceBean.publishOrganizationChange("GET", organizationId); if (!opt.isPresent()) { String message = String.format("Unable to find an organization with theOrganization id %s", organizationId); logger.error(message); throw new IllegalArgumentException(message); } logger.debug("Retrieving Organization Info: " + opt.get().toString()); } finally { newSpan.tag("peer.service", "postgres"); newSpan.annotate("Client received"); newSpan.finish(); } Глава 11 Распределенная трассировка с использованием Spring ... 402 return opt.get(); } // Остальная часть определения класса опущена для краткости } Добавив эти две дополнительные операции, перезапустите службы, затем отправьте запрос GET следующей конечной точке: http://localhost:8072/license/v1/organization/4d10ec24-141a-4980-be34➥ 2ddb5e0458c9/license/4af05c3b-a0f3-411d-b5ff-892c62710e16 Если теперь запросить транзакцию в Zipkin, вы должны увидеть в ней две новые операции, связанные с получением информации о лицензии, как показано на рис. 11.22. Теперь наши дополнительные операции отображаются в трассировке транзакции Рис. 11.22. Дополнительные операции в наших службах отображаются в трассировке транзакции На рис. 11.22 также показана дополнительная информация о трассировке и времени выполнения, связанная с вызовом кеша Redis и базы данных Postgres. Раскрыв детализацию, мы видим, что вызов Redis занял 5,503 мс. Поскольку искомый элемент отсутствовал в кеше Redis, был выполнен запрос SQL к базе данных Postgres, занявший 234,888 мс. Теперь, зная, как настроить распределенную трассировку, APIшлюз, обнаружение служб и дополнительные операции для трассировки, перейдем к следующей главе. В ней мы объясним, как развернуть все, что создавали на протяжении всей книги. Распределенная трассировка с использованием Zipkin 403 Итоги Spring Cloud Sleuth позволяет добавлять трассировочную информацию (идентификаторы корреляции) в вызовы микросервисов. Идентификаторы корреляции могут использоваться для связывания записей в журнале, сделанных несколькими службами. Это позволяет наблюдать за обработкой транзакции во всех службах, участвующих в ней. Идентификаторы корреляции – мощный инструмент, эта концепция, в сочетании с платформой агрегирования журналов, позволяет получать журналы из нескольких источников, а затем выполнять поиск в их содержимом. Мы можем интегрировать контейнеры Docker с платформой агрегирования журналов для сбора данных из всех журналов в приложении. Мы интегрировали наши контейнеры Docker со стеком ELK (Elasticsearch, Logstash, Kibana). Это позволило нам преобразовывать, хранить, визуализировать и запрашивать данные из журналов наших служб. Унифицированная платформа журналирования имеет большое значение, как и возможность визуальной трассировки транзакций, охватывающих несколько микросервисов. Zipkin позволяет увидеть зависимости между службами в форме потоков транзакций, и исследовать характеристики производительности каждого микросервиса, участвующего в обработке транзакции. Spring Cloud Sleuth легко интегрируется с Zipkin. Zipkin автоматически извлекает трассировочные данные из HTTPвызовов, которые приводят к передаче сообщений по каналам Spring Cloud Sleuth. Spring Cloud Sleuth поддерживает понятие операций, соответствующее вызовам служб, а Zipkin позволяет увидеть продолжительность выполнения каждой операции. Spring Cloud Sleuth и Zipkin также дают возможность определять свои операции для исследования производительности ресурсов, не связанных с Spring (серверов базы данных, таких как Postgres или Redis). 12 Развертывание микросервисов Эта глава: объясняет, почему методология DevOps так важна для микросервисов; описывает настройку базовой инфраструктуры Amazon для служб O-stock; демонстрирует ручное развертывание служб O-stock на Amazon; иллюстрирует разработку конвейера сборки/развертывания; советует относиться к инфраструктуре как к коду; рассказывает о развертывании приложений в облаке. Мы почти подошли к концу книги, но это еще не конец нашего путешествия по микросервисам. Большая часть этой книги посвящена проектированию, созданию и запуску микросервисов с использованием технологии Spring Cloud, однако до сих пор мы не касались вопросов сборки и развертывания микросервисов. Создание конвейера сборки/развертывания может показаться рутинной задачей, но на самом деле это одна из самых важных составляющих разработки архитектуры микросервисов. Почему? Помните, что одним из ключевых преимуществ архитектуры микросервисов является то, что микросервисы являются небольшими блоками кода, которые можно быстро создавать, изменять и развертывать в промышленном окружении независимо друг от друга. Небольшой размер служб позволяет быстро добавлять новые функции (и исправления критических ошибок). Ключевым сло- Развертывание микросервисов 405 вом здесь является «быстро», потому что высокая скорость означает, что между созданием новой функции или исправлением ошибки и повторным развертыванием службы практически не существует трений. Сроки развертывания должны составлять минуты, а не дни. Для этого механизм сборки и развертывания должен быть: автоматизированным – процесс сборки/развертывания должен протекать без вмешательства человека. Процесс сборки программного обеспечения, подготовки образа машины и развертывания службы должен быть автоматизированным и инициироваться операцией отправки кода в репозиторий; повторяемым – процесс сборки/развертывания должен быть повторяемым; при сборке и развертывании каждый раз должно происходить одно и то же. Изменчивость процесса часто является источником мелких ошибок, которые трудно выявить и устранить; исчерпывающим – результатом развертывания артефакта должен быть образ виртуальной машины или контейнера (например, Docker), содержащий «полную» среду выполнения для службы. Это важный сдвиг в восприятии инфраструктуры. Подготовка образов машин должна быть полностью автоматизирована с помощью сценариев, хранящихся в системе управления версиями вместе с исходным кодом службы. В архитектуре микросервисов эта ответственность обычно перекладывается с операторов на разработчиков, владеющих службой. Один из основных принципов разработки микросервисов – побудить разработчиков взять на себя всю ответственность за эксплуатацию служб; неизменным – после развертывания образа машины, содержащего службу, конфигурацию времени выполнения нельзя трогать или изменять. Если необходимо внести какие-то корректировки, следует изменить сценарии, находящиеся в системе управления версиями, а затем заново выполнить сборку службы и инфраструктуры. Изменения в конфигурации времени выполнения (параметры сборки мусора, используемый профиль Spring и т. д.) должны передаваться в образ через переменные окружения, тогда как конфигурация приложения должна храниться отдельно от контейнера (Spring Cloud Config). Создание надежного и универсального конвейера сборки/развертывания – трудоемкая задача, которая часто должна учитывать особенности среды выполнения. Обычно в ней участвует специализированная группа инженеров DevOps (разработчиков/операторов), единственная цель которых – обобщить процесс сборки, Глава 12 Развертывание микросервисов 406 чтобы каждая команда могла создавать микросервисы, не изобретая заново весь процесс сборки. К сожалению, Spring является фреймворком для разработки и не предлагает сколько-нибудь широких возможностей для реализации конвейера сборки/развертывания. Но это не значит, что такой конвейер нельзя построить. 12.1.Архитектура конвейера сборки/ развертывания Цель этой главы – показать готовые компоненты конвейера сборки/развертывания, чтобы вы могли взять их и адаптировать под свои нужды. Начнем обсуждение с общего обзора архитектуры конвейера сборки/развертывания и некоторых типичных шаблонов. Чтобы обеспечить преемственность примеров, мы выделили некоторые части, чего обычно не делаем в собственном окружении, и будем рассматривать их поочередно. В начале обсуждения развертывания микросервисов рассмотрим диаграмму, обсуждавшуюся еще в главе 1. Рисунок 12.1 повторяет схему, которую детально разбирали в главе 1, и на нем показаны части и этапы создания конвейера сборки/развертывания микросервисов. 1. Разработчик отправляет код службы в репозиторий 4. Создается образ виртуальной машины (контейнера) с установленной службой и механизмом выполнения 2. Механизм сборки/развертывания проверяет код и запускает сценарии сборки Конвейер непрерывной интеграции/доставки Код готов Разработчик Репозиторий исходного кода Механизм сборки и развертывания 3. Механизм компилирует код, запускает тесты и создает выполняемый артефакт (файл JAR с автономным сервером) 5. Перед передачей в следующее окружение запускаются платформенные тесты для проверки образа машины 6. Перенос в следующее окружение вызывает запуск образа машины, используемой в предыдущем окружении Окружение разработки Окружение тестирования Промышленное окружение Модульное Созданы и интегра- артефакты ционное времени тестирова- выполнения ние Образ Образ отправлен машины в репозиторий Запуск платформенных тестов Развертывание нового образа/сервера Запуск платформенных тестов Развертывание нового образа/сервера Запуск платформенных тестов Развертывание нового образа/сервера Рис. 12.1. Каждый компонент конвейера сборки/развертывания автоматизирует некоторую задачу, которую можно было бы выполнить вручную Диаграмма на рис. 12.1 должна показаться вам знакомой, потому что она основана на общем шаблоне реализации непрерывной интеграции (Continuous Integration, CI). Архитектура конвейера сборки/развертывания 407 Разработчик отправляет свой код в репозиторий. Инструмент сборки обнаруживает изменения в репозитории и запускает сборку. 3 Во время сборки запускаются модульные и интеграционные тесты и в случае их успешного выполнения создается развертываемый программный артефакт (JAR, WAR или EAR). 4 Получившийся артефакт JAR, WAR или EAR можно развернуть на сервере приложений, работающем на другом сервере (обычно на сервере разработки). Похожий процесс выполняется конвейером сборки/развертывания, пока код не будет готов к развертыванию. Добавив непрерывную доставку (Continuous Delivery, CD) в процесс сборки/развертывания, изображенный на рис. 12.1, получим следующий перечень шагов. 1 Разработчик отправляет свой код в репозиторий. 2 Механизм сборки/развертывания обнаруживает изменения в репозитории, проверяет код и запускает сценарии сборки. 3 После того как сценарии сборки скомпилируют код и выполнят модульные и интеграционные тесты, создается выполняемый артефакт. Поскольку мы рассказываем о создании микросервисов с использованием Spring Boot, наш процесс сборки создает выполняемый файл JAR, содержащий код службы и автономный сервер Tomcat. Обратите внимание, что, начиная со следующего этапа, наш процесс сборки/развертывания начинает отклоняться от традиционного процесса непрерывной интеграции. 1 2 4 После создания выполняемого файла JAR мы «выпекаем» образ машины с развернутым в нем микросервисом. В результате получается образ виртуальной машины или контейнер (Docker), в который устанавливается наша служба. Вместе с образом виртуальной машины запускается и наша служба, и она сразу готова начать принимать запросы. В отличие от традиционного процесса непрерывной интеграции, согласно которому скомпилированный артефакт JAR или WAR развертывается на сервере приложений, который не зависит от приложения (и часто поддерживается отдельной командой), микросервис развертывается вместе с механизмом времени выполнения и образом машины как единое целое, управляемое командой разработчиков, написавшей программное обеспечение. 5 Перед официальным развертыванием в новом окружении производится запуск образа машины и выполняется серия платформенных тестов, чтобы проверить работу в комплексе. Если платформенные тесты проходят успешно, образ маши- 408 Глава 12 Развертывание микросервисов ны перемещается в новое окружение и становится доступным для использования. 6 Перенос службы в новое окружение включает запуск точного того же образа машины, который использовался в предыдущем окружении. Это основа успеха всего процесса – развертывание полного образа машины. При использовании непрерывной доставки после создания сервера никакие изменения не вносятся ни в какое установленное программное обеспечение (включая операционную систему). Продвигая и используя один и тот же образ машины, вы гарантируете неизменность сервера при переходе из одного окружения в другое. Модульное, интеграционное и платформенное тестирование На рис. 12.1 показано несколько видов тестирования (модульное, интеграционное и платформенное), которые выполняются в ходе сборки и развертывания службы: модульное тестирование – выполняется сразу после компиляции службы, но перед развертыванием в окружении. Модульные тесты выполняются в полной изоляции, каждый из них охватывает небольшой фрагмент кода и является узконаправленным. Модульные тесты не должны зависеть от сторонних баз данных, служб и т. д. Обычно один модульный тест охватывает только один метод или функцию; интеграционное тестирование – выполняется сразу после упаковки кода службы. Интеграционные тесты предназначены для тестирования всего рабочего процесса и проверки всех путей в коде с использованием заглушек или имитаций сторонних служб или компонентов. Во время интеграционного тестирования можно запускать базы данных в памяти для хранения информации, имитировать вызовы сторонних служб и т. д. В интеграционных тестах сторонние зависимости просто имитируются, поэтому обработка любых запросов, посылаемых удаленным службам, также имитируется. Вызовы никогда не покидают сервер сборки; платформенное тестирование – выполняется непосредственно перед развертыванием службы в заданном окружении. Платформенные тесты обычно проверяют весь бизнес-поток, а также используют все сторонние зависимости, участвующие в работе производственной системы. Платформенные тесты выполняются в масштабе реального времени в конкретном окружении и используют реальные сторонние службы. Этот тип тестирования проводится для выявления проблем интеграции со сторонними службами, которые обычно не обнаруживаются при использовании имитаций в интеграционном тестировании. Если вам интересно узнать, как создавать модульные, интеграционные и платформенные тесты, то мы настоятельно рекомендуем приобрести книгу Алекса Сото Буэно (Alex Soto Bueno), Энди Гумбрехта (Andy Gumbrecht) и Джейсона Портера (Jason Porter) «Testing Java Microservices» (Manning, 2018). Архитектура конвейера сборки/развертывания 409 Процесс сборки/развертывания построен на трех основных шаблонах, возникших на основе коллективного опыта, накопленного разработчиками облачных приложений с микросервисной архитектурой. Вот эти шаблоны: непрерывная интеграция/непрерывная доставка (CI/CD) – согласно принципам CI/CD код приложения компилируется и тестируется не только после отправки в репозиторий и развертывания. Развертывание кода должно происходить примерно так: если код успешно проходит модульные, интеграционные и платформенные тесты, он немедленно перемещается в следующее окружение. Единственная конечная цель в большинстве организаций – промышленное окружение; инфраструктура как код – последний программный артефакт, который создается в окружении разработки и продвигается далее, – это образ машины. Образ машины с установленным в нем микросервисом создается сразу после компиляции и тестирования кода микросервиса. Создание образа машины выполняется с помощью последовательности сценариев, запускаемых при каждой сборке. Человеческие руки не должны касаться сервера после его сборки. Сценарии должны храниться в системе управления версиями, как и любой другой фрагмент исходного кода; неизменяемый сервер – после создания образа сервера с микросервисом ни конфигурация сервера, ни сам микросервис в этом образе не должны изменяться. Это гарантирует, что окружение не пострадает от «дрейфа конфигурации» из-за того, что разработчик или системный администратор внес «одно небольшое изменение», которое позже стало причиной сбоя. Если необходимо внести изменение, измените сценарии создания сервера и запустите процесс сборки заново. О неизменяемости и воскрешении, Phoenix Server Согласно концепции неизменяемости сервера мы должны гарантировать точное соответствие конфигурации сервера той, что используется в образе машины. После перезапуска сервера из образа машины поведение службы или микросервиса не должно изменяться. Такой сервер, воскрешающийся после уничтожения в неизменном состоянии, был назван Мартином Фаулером (Martin Fowler) «сервером Феникс» (Phoenix Server), потому что после уничтожения старого сервера из пепла должен восстать новый. За дополнительной информацией обращайтесь по ссылке: http://martinfowler.com/bliki/PhoenixServer.html. Шаблон «Сервер Феникс» (Phoenix Server) предлагает два основных преимущества. Во-первых, он управляет дрейфом конфигурации. Постоянно отключая и настраивая новые серверы, вы рано или поздно обнаружите дрейф конфигурации. Этот шаблон оказывает огромную помощь в обеспечении согласованности. 410 Глава 12 Развертывание микросервисов Во-вторых, шаблон «сервер Феникс» (Phoenix Server) помогает повысить устойчивость, помогая выявлять ситуации, когда сервер или служба не полностью восстанавливаются после остановки и перезапуска. Помните, что в архитектуре микросервисов ваши службы не должны иметь состояния, и остановка сервера должна быть незначительным событием. Случайное завершение работы и перезапуск серверов позволяют быстро выявить ситуации, когда служба или инфраструктура имеют состояние и зависят от него. Такие ситуации и зависимости желательно выявить на ранних этапах процесса развертывания, чтобы потом не пришлось оправдываться перед рассерженным клиентом или компанией. В этой главе мы покажем, как реализовать конвейер сборки/развертывания с помощью нескольких инструментов, не входящих в состав Spring. За основу мы возьмем набор микросервисов, созданный выше в этой книге, и сделаем следующее. 1 Интегрируем наши сценарии сборки Maven в облачный инструмент непрерывной интеграции/развертывания под названием Jenkins. 2 Создадим неизменяемые образы Docker для каждой службы и отправим их в централизованный репозиторий. 3 Развернем весь набор микросервисов в облаке Amazon с помощью службы контейнеров Amazon под названием Elastic Kubernetes Service (EKS). ПРИМЕЧАНИЕ. В этой книге мы не будем подробно объяснять, как работает Kubernetes. Если вы незнакомы с этим фреймворком или хотите узнать больше о том, как он работает, то мы настоятельно рекомендуем прочитать отличную книгу Марко Лукши (Marko Lukša) «Kubernetes in Action»2 (Manning, 2017). Но, прежде чем приступить к созданию конвейера, начнем с настройки базовой инфраструктуры в облаке. 12.2.Настройка базовой инфраструктуры для O-stock в облаке Во всех примерах в этой книге мы запускали наши приложения внутри единственного образа виртуальной машины, при этом каждая служба запускалась в отдельном контейнере Docker. Теперь мы изменим привычный порядок вещей и отделим сервер базы данных (PostgreSQL) и сервер кеширования (Redis) от Docker в облаке Amazon. Все остальные службы будут продолжать работать как контейнеры Docker внутри одноузлового кластера Amazon EKS. 2 Лукша Марко, Kubernetes в действии, М.; ДМК Пресс, 2018, ISBN 978-5-97060-657-5. – Прим. перев. Настройка базовой инфраструктуры для O-stock в облаке 411 На рис. 12.2 показано развертывание служб O-stock в облаке Amazon. 2. База данных и кластеры Redis будут перенесены в службу Amazon База данных PostgreSQL База данных ElastiCache Служба ELK 1. Все основные службы O-stock будут работать в одноузловом кластере EKS Служба организаций Служба лицензий Порт 8072 Spring Cloud Gateway Spring Cloud Config Служба Spring Eureka 3. Настройки группы безопасности контейнера ECS ограничивают входящий трафик так, что снаружи будет доступен только порт 8072. Это означает, что ко всем службам O-stock можно получить доступ только через сервер API-шлюза, прослушивающий порт 8072 5. Все остальные службы будут доступны только из контейнера EKS Сервер Kafka Служба аутентификации OAuth2 4. Службы организаций и лицензий защищены службой аутентификации OAuth2 Рис. 12.2. Используя Docker, мы можем развернуть все наши службы в облаке, таком как Amazon EKS Давайте рассмотрим подробнее инфраструктуру на рис. 12.2. Номера пунктов в списке ниже соответствуют номерам на рисунке. 1 Все службы O-stock (за исключением базы данных и кластера Redis) будут развернуты в контейнерах Docker внутри одноузлового кластера EKS. EKS сам настраивает и запускает все необходимое для работы кластера Docker. EKS также может следить за состоянием контейнеров Docker и перезапускать службы в случае сбоя. 2 При развертывании в облаке Amazon мы откажемся от использования своих базы данных PostgreSQL и сервера Redis и бу- Глава 12 Развертывание микросервисов 412 3 4 5 дем использовать Amazon Relational Database Service (Amazon RDS) и Amazon ElastiCache. Конечно, можно было бы продолжать использовать свои хранилища данных Postgres и Redis в Docker, но мы хотели подчеркнуть, насколько легко выполнить переход от одной инфраструктуры к другой, полностью управляемой поставщиком облачных услуг. В отличие от развертывания на настольном компьютере мы хотим, чтобы весь трафик проходил через API-шлюз. Для этого мы используем группу безопасности Amazon, чтобы открыть для доступа извне только порт 8072 на развернутом кластере EKS. Мы по-прежнему будем использовать сервер Spring OAuth2 для защиты наших служб. Прежде чем получить доступ к службам организаций и лицензий, пользователь должен аутентифицироваться с помощью наших служб аутентификации (подробнее об этом рассказывается в главе 9) и передавать действительный токен OAuth2 в каждом вызове. Все наши серверы, включая сервер Kafka, будут доступны через открытые порты Docker только внутри контейнера EKS. Некоторые предварительные условия для работы с AWS Для настройки инфраструктуры в облаке Amazon вам потребуются: личная учетная запись Amazon Web Services (AWS) – вы должны иметь общее представление о консоли AWS и идеях, лежащих в основе работы в этом окружении; веб-браузер; AWS CLI (интерфейс командной строки) – унифицированный инструмент для управления службами AWS (см. https://docs.aws. amazon.com/cli/latest/userguide/install-cliv2.html); Kubectl – инструмент для взаимодействий с кластером Kubernetes (см. https://kubernetes.io/docs/tasks/tools/installkubectl/#installkubectl); IAM Authenticator – инструмент аутентификации для нашего кластера Kubernetes (см. https://docs.aws.amazon.com/eks/latest/ userguide/install-aws-iamauthenticator.html); Eksctl – простая утилита командной строки для создания и управления кластерами AWS EKS в личной учетной записи AWS (см. https://docs. aws.amazon.com/eks/latest/userguide/getting-started-eksctl.html). Если вы незнакомы с AWS, то мы настоятельно рекомендуем прочитать книгу Майкла Виттига (Michael Wittig) и Андреаса Виттига (Andreas Wittig) «Amazon Web Services in Action» (Manning, 2018). Первая глава этой книги включает подробное руководство о том, как зарегистрироваться и настроить свою учетную запись AWS. Эту главу можно скачать здесь: Настройка базовой инфраструктуры для O-stock в облаке 413 https://www.manning.com/books/amazon-web-services-in-actionsecond-edition. В этой главе мы старались использовать в основном бесплатные услуги, предлагаемые компанией Amazon. Единственное место, где мы не могли этого сделать, – настройка кластера EKS. Мы использовали большой сервер M4, стоимость работы которого составляет примерно 0,10 долл. США в час. Если вы не хотите нести лишние расходы, связанные с использованием этого сервера, обязательно отключите соответствующие службы по завершении работы. Если вы хотите узнать больше о ценах на экземпляры серверов AWS EC2, загляните в официальную документацию Amazon: https://aws.amazon.com/ec2/pricing/on-demand/. Наконец, мы не можем гарантировать, что ресурсы Amazon (Postgres, Redis и EKS), которые используются в этой главе, будут доступны вам, если вы решите опробовать примеры самостоятельно. Если вы соберетесь запускать примеры из этой главы, то вам нужно настроить собственный репозиторий GitHub (для хранения конфигурации приложения), собственное окружение Jenkins, Docker Hub (для ваших образов Docker) и учетную запись Amazon. После этого вам потребуется изменить конфигурацию приложения и указать в ней вашу учетную запись и учетные данные. 12.2.1.Создание базы данных PostgreSQL с использованием Amazon RDS Для начала нужно настроить учетную запись Amazon AWS. После этого мы создадим базу данных PostgreSQL, которую будем использовать в наших службах O-stock. Для этого сначала нужно выполнить вход в консоль управления Amazon AWS (https://aws.amazon. com/console/). При первом входе вам будет предложен список вебслужб Amazon. 1 Найдите ссылку RDS. Щелкните не ней, чтобы открыть панель управления RDS. 2 В панели управления RDS щелкните на большой кнопке Create Database (Создать базу данных). 3 Вы должны увидеть список доступных баз данных. Amazon RDS поддерживает различные механизмы баз данных; выберите PostgreSQL и щелкните на ссылке Option (Параметры). После этого появятся страница с настройками базы данных. Перед вами должна появиться страница с тремя вариантами: Production (Промышленная база данных), Dev/Test (База данных для разработки/тестирования) и Free tier (Бесплатный тариф). Выберите вариант Free tier (Бесплатный тариф). Затем в разделе Settings (Настройки) добавьте необходимую информацию о базе данных PostgreSQL, указав идентификатор суперпользователя и пароль, которые вы будете использовать для входа в базу данных. Эта страница показана на рис. 12.3. 414 Глава 12 Развертывание микросервисов Выберите вариант Free tier (Бесплатный тариф) Запишите свой пароль. В наших примерах вы будете использовать учетную запись суперпользователя для входа в базу данных. В реальной системе желательно создать для приложения отдельную учетную запись и никогда напрямую не использовать в приложениях имя/пароль суперпользователя Рис. 12.3. Выбор шаблона для создания базы данных (промышленный, для разработки или бесплатный) и настройка базовой конфигурации в консоли управления Amazon AWS Настройки в разделах DB Instance Size (Размер экземпляра БД), Storage (Хранилище), Availability & Durability (Доступность и сохранность) и Database Authentication (Аутентификация в БД) оставьте как есть. Последний шаг – установить параметры, перечисленные ниже. В любой момент вы можете щелкнуть на ссылке Info (Информация) рядом с тем или иным параметром и получить справку о вариантах, доступных для выбора. Эта страница показана на рис. 12.4. Virtual Private Cloud (VPC) (Виртуальное частное облако) – выберите Default VPC (Облако по умолчанию); Publicly Accessible (Общедоступный) – выберите Yes (Да); VPC Security Group (Группа безопасности) – выберите Create New (Создать) и добавьте имя группы. В этом примере мы дали группе безопасности название ostock-sg; Database Port (Порт базы данных) – введите 5432. Настройка базовой инфраструктуры для O-stock в облаке 415 После этого начнется процесс создания базы данных (это может занять несколько минут). По завершении нужно задействовать базу данных, настроив свойства службы O-stock в файлах конфигурации, расположенных на сервере конфигурации Spring Configuration Server. Для этого вернитесь в панель управления RDS, чтобы отобразить информацию о базе данных. Эта страница показана на рис. 12.5. Объявите базу данных общедоступной и создайте новую группу безопасности Введите номер порта. Он будет использоваться как часть строки подключения службы Рис. 12.4 Настройка сетевого окружения, группы безопасности и номера порта для базы данных RDS Для примеров этой главы вы можете создать новый профиль приложения с названием aws-dev для каждого микросервиса, которому потребуется доступ к базе данных PostgreSQL в Amazon. Профиль приложения будет содержать информацию о подключении к базе данных Amazon. Теперь наша база данных готова к работе (совсем неплохо – выполнить настройку пятью щелчками мыши), и мы можем перейти Глава 12 Развертывание микросервисов 416 к следующей части инфраструктуры и создать кластер Redis, который будет использовать служба лицензий. Щелкните на имени базы данных, чтобы открыть страницу с ее свойствами Это конечная точка и порт, которые будут использоваться для подключения к базе данных Рис. 12.5. Обзор вновь созданной базы данных Amazon RDS/PostgreSQL и ее настроек 12.2.2. Создание кластера Redis в Amazon Теперь нужно переместить в Amazon ElastiCache сервер Redis, который мы запускали в Docker. ElastiCache (https://aws.amazon. com/elasticache/) позволяет создавать кеши данных в памяти на основе Redis или memcached (https://memcached.org/). Для начала вернитесь на главную страницу консоли управления AWS, затем найдите службу ElastiCache и щелкните на ссылке ElastiCache. В консоли ElastiCache выберите ссылку Redis (слева), а затем щелкните на синей кнопке Create (Создать) в верхней части страницы, чтобы запустить мастер создания кеша ElastiCache/ Redis (рис. 12.6). В дополнительных настройках на этой странице выберите ту же группу безопасности, что была создана для базы данных PostgreSQL, а затем снимите флажок Enable Automatic Backups (Включить автоматическое резервное копирование). После заполнения формы щелкните на кнопке Create (Создать), чтобы запустить процесс создания кластера Redis (это займет несколько минут). Amazon создаст одноузловой сервер Redis, работающий на самом маленьком экземпляре сервера Amazon. Настройка базовой инфраструктуры для O-stock в облаке 417 Имя сервера ElastiCache Выберите наименьший тип экземпляра Поскольку это сервер разработки, нам не нужно создавать копии серверов Redis Рис. 12.6. Всего несколькими щелчками мыши можно настроить кластер Redis, управляемый облачной инфраструктурой Amazon После создания кластера Redis щелкните на его имени, чтобы открыть страницу, где можно найти конечную точку для доступа к кешу в кластере (рис. 12.7). Это конечная точка Redis, которую должны использовать наши службы Рис. 12.7. Конечная точка, необходимая службам для подключения к Redis В нашем приложении кеш Redis использует только служба лицензий. Если вы будете развертывать примеры кода из этой главы на своем собственном экземпляре Amazon, то внесите соответствующие изменения в файлы Spring Cloud Config с настройками службы лицензий. 418 Глава 12 Развертывание микросервисов 12.3.После подготовки инфраструктуры: развертывание O-stock и ELK Во второй части главы мы развернем службы Elasticsearch, Logstash и Kibana (ELK) в экземпляре EC2 и службы O-stock – в контейнере Amazon EKS. Напомним, что экземпляр EC2 – это виртуальный сервер в облаке Amazon Elastic Compute Cloud (Amazon EC2), на котором можно запускать приложения. Кроме того, мы разобьем процесс развертывания O-stock на две части. Сначала мы удовлетворим любопытство самых нетерпеливых (таких же, как мы сами) и покажем, как развернуть O-stock в кластере Amazon EKS вручную. Это поможет понять механику развертывания и увидеть развернутые службы, действующие в контейнере. Развертывать службу вручную – это довольно увлекательное занятие, но действовать так нерационально и не рекомендуется. Во второй части мы автоматизируем весь процесс сборки/развертывания и исключим человека из процесса. Это предпочтительное конечное состояние всего процесса разработки, и оно действительно венчает работу, проделанную выше в этой книге, демонстрирующей проектирование, создание и развертывание микросервисов в облаке. 12.3.1. Создание экземпляра EC2 с помощью ELK Для настройки служб ELK используем экземпляр Amazon EC2. Отделение служб приложения O-stock от тех, что будут развернуты в экземпляре ELK, показывает, что можно иметь несколько разных экземпляров и по-прежнему пользоваться этими службами. Чтобы создать экземпляр EC2, выполним следующие шаги. 1 Выберите образ машины Amazon Machine Image (AMI). 2 Выберите тип экземпляра. 3 Определите конфигурацию по умолчанию, хранилища и теги. 4 Создайте новую группу безопасности. 5 Запустите экземпляр EC2. Итак, вернитесь на главную страницу консоли управления AWS, найдите службу EC2 и щелкните на ссылке. Затем щелкните на синей кнопке Launch Instance (Запустить экземпляр), чтобы начать процесс запуска EC2. Мастер запуска проведет вас через семь шагов, и мы подробно опишем их далее. На рис. 12.8 показан первый шаг в процессе создания EC2. После подготовки инфраструктуры: развертывание O-stock и ELK 419 Выберите Amazon Linux 2 AMI Рис. 12.8. Выберите параметр Amazon Linux 2 AMI, чтобы настроить экземпляр службы ELK Затем выберите тип экземпляра. Для этого примера выберите сервер m4.large, предоставляющий 8 Гб памяти и низкую почасовую оплату (0,10 долл. США в час). Этот второй шаг показан на рис. 12.9. Выберите сервер m4.large с 8 Гб ОЗУ и стоимостью 0,10 долл. за час работы Рис. 12.9. Выбор типа экземпляра для службы ELK Выбрав экземпляр, щелкните на ссылке Next: Configure Instance (Далее: настройка экземпляра). На третьем, четвертом и пятом шагах ничего менять не нужно, поэтому просто щелкайте на ссылке Next (Далее), принимая настройки по умолчанию. На шестом шаге нужно создать новую группу безопасности (или выбрать существующую, если вы ее уже создали) и применить ее к новому экземпляру EC2. В этом примере мы разрешим весь входящий трафик (0.0.0.0/0 – маска сети, соответствующая всей Всемирной паутине). В реальных сценариях следует быть более осторожными с этим параметром, поэтому мы рекомендуем потратить некоторое время на анализ и формирование правил фильтрации входящего трафика, лучше всего соответствующих вашим потребностям. Этот шаг показан на рис. 12.10. 420 Глава 12 Развертывание микросервисов Создайте новую группу безопасности с одним правилом для входящего трафика, которое разрешит весь трафик Рис. 12.10. Создание группы безопасности для экземпляра EC2 После настройки группы безопасности щелкните на кнопке Review and Launch (Обзор и запуск), чтобы проверить конфигурацию экземпляра EC2. Убедившись, что конфигурация верна, щелкните на кнопке Launch Instances (Запустить экземпляры), чтобы перей­ ти к последнему шагу. Последний шаг включает создание пары ключей для подключения к экземпляру EC2. Этот шаг показан на рис. 12.11. Имя файла для пары ключей Прежде чем продолжить, убедитесь, что загрузили и сохранили закрытый ключ в надежном месте Рис. 12.11. Создание пары ключей для группы безопасности экземпляра EC2 Теперь у нас есть вся инфраструктура, необходимая для запуска стека ELK. На рис. 12.12 показано состояние нашего экземпляра EC2. После подготовки инфраструктуры: развертывание O-stock и ELK 421 Убедитесь, что экземпляр запущен Обратите внимание на общедоступный IP-адрес IPv4 Рис. 12.12. Страница панели управления Amazon EC2, показывающая состояние вновь созданного экземпляра EC2 Прежде чем перейти к следующему шагу, обратите внимание на IP-адрес экземпляра. Эта информация понадобится нам для подключения к нашему экземпляру EC2. ПРИМЕЧАНИЕ. Прежде чем перейти к следующей части, мы настоятельно рекомендуем загрузить из репозитория папку AWS из каталога с примерами для главы 12: https://github.com/ ihuaylupo/manning-smia/tree/master/chapter12/AWS. В этой папке находятся созданные нами сценарии в файлах .yaml и dockercompose, которые мы будем использовать для развертывания контейнеров EC2 и EKS. Чтобы продолжить, откройте окно терминала, перейдите в каталог, куда сохранили загруженный файл с парой ключей, и выполните следующие команды: chmod 400 <pemKey>.pem scp -r -i <pemKey>.pem ~/корневая/папка/проекта ec2-user@<IPv4>:~/ Важно подчеркнуть, что мы будем использовать папку проекта, содержащую все файлы конфигурации, необходимые для развертывания наших служб. Если вы скачали папку AWS из репозитория GitHub, то можете использовать ее в качестве папки проекта. После выполнения предыдущих команд вы должны увидеть результат, изображенный на рис. 12.13. 422 Глава 12 Развертывание микросервисов Рис. 12.13. Теперь можно войти в экземпляр EC2, используя ранее созданную пару ключей и ssh Если вывести список файлов и каталогов в корневом каталоге экземпляра EC2, то можно увидеть, что в экземпляр была перемещена вся папка проекта. Теперь, закончив с настройками, продолжим развертывание ELK. 12.3.2. Развертывание стека ELK в экземпляре EC2 После создания экземпляра и входа в систему продолжим развертывание служб ELK. Для этого: 1 обновим экземпляр EC2; 2 установим Docker; 3 установим Docker Compose; 4 запустим Docker; 5 запустим файл docker-compose, содержащий службы ELK. Чтобы выполнить все предыдущие шаги, запустите команды, показанные в листинге 12.1. Листинг 12.1. Развертывание ELK в экземпляре EC2 Обновит приложения, установленные в системе (в экземпляре EC2) sudo yum update Установит Docker sudo yum install docker sudo curl -L https://github.com/docker/compose/releases/ download/1.21.0/docker-compose-`uname -s`-`uname -m` | sudo tee /usr/local/bin/ docker-compose > /dev/null Установит Docker Compose sudo chmod +x /usr/local/bin/docker-compose sudo ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose sudo service docker start Запустит Docker в экземпляре EC2 sudo docker-compose -f AWS/EC2/ Запустит файл docker-compose. Параметр -d docker-compose.yml up -d запускает процесс в фоновом режиме Далее, запустив все службы ELK, убедимся, что они работают. Для этого откройте в браузере URL http://<IPv4>:5601/. Должна После подготовки инфраструктуры: развертывание O-stock и ELK 423 появиться панель управления Kibana. Теперь, когда у нас есть работающие службы ELK, создадим кластер Amazon EKS для развертывания наших микросервисов. 12.3.3. Создание кластера EKS Последний шаг перед развертыванием служб O-stock – настройка кластера Amazon EKS. EKS – это служба Amazon, позволяющая запускать Kubernetes на AWS. Kubernetes – это система с открытым исходным кодом, которая автоматизирует развертывание и запуск, а также создание и удаление контейнеров. Как обсуждалось выше, управление большим количеством контейнеров и микросервисов может стать сложной задачей, а инструменты, подобные Kubernetes, помогают обслуживать контейнеры быстрее и эффективнее. Например, представьте себе следующий сценарий. Нужно обновить микросервис и его образ, который запускается в десятках, а может и в сотнях контейнеров. Можете ли вы представить себя, вручную удаляющего и воссоздающего эти контейнеры? С Kubernetes же вы можете уничтожить и воссоздать их, просто выполнив короткую команду. Это только одно из многочисленных преимуществ Kubernetes. Теперь, кратко познакомившись с некоторыми возможностями Kubernetes, продолжим. Создать кластер EKS можно с помощью консоли управления AWS или интерфейса командной строки AWS CLI. В этом примере мы покажем, как использовать AWS CLI. ПРИМЕЧАНИЕ. Прежде чем продолжить, убедитесь, что у вас есть все инструменты, упомянутые выше во врезке с заголовком «Некоторые предварительные условия для работы с AWS», и был настроен интерфейс командной строки AWS. Если вы этого не сделали, то обратитесь к документации: https://docs.aws. amazon.com/cli/latest/userguide/cli-configure-quickstart.html. После установки и настройки всех необходимых инструментов можно приступать к созданию кластера Kubernetes. Для этого: 1 подготовим кластер Kubernetes; 2 поместим образы микросервисов в репозиторий; 3 развернем микросервисы вручную в кластере Kubernetes. Подготовка кластера Kubernetes Чтобы создать кластер с помощью AWS CLI, выполните команду eksctl, как показано ниже: eksctl create cluster --name=ostock-dev-cluster --nodes=1 --node-type=m4.large Предыдущая команда eksctl создаст кластер Kubernetes (с именем ostock-dev-cluster), использующий единственный экземпляр Глава 12 Развертывание микросервисов 424 m4.large EC2 в качестве рабочего узла. В этом примере используется тот же тип экземпляра, который выше мы выбрали в мастере создания службы EC2 ELK. Этой команде необходимо несколько минут для выполнения. Во время работы она будет выводить разные сведения и по завершении сообщит: [✓] EKS cluster "ostock-dev-cluster" in "region-code" region is ready По завершении можно запустить команду kubectl get svc, чтобы проверить правильность конфигурации Kubectl. Эта команда должна вывести: NAME kubernetes TYPE ClusterIP CLUSTER-IP 10.100.0.1 EXTERNAL-IP <none> PORT(S) 443/TCP AGE 1m Добавление образов микросервисов в репозиторий До сих пор мы создавали службы на локальном компьютере. Чтобы использовать полученные образы в кластере EKS, нужно передать образы контейнеров Docker в репозиторий контейнеров. Репозиторий контейнеров напоминает репозиторий Maven, но предназначен для создаваемых вами образов Docker. Образы Docker снабжать метками и выгружать в репозиторий, после чего загружать и использовать их на серверах. Существует несколько репозиториев, таких как Docker Hub, но давайте продолжим работать с инфраструктурой AWS и воспользуемся Amazon Elastic Container Registry (ECR). Узнать больше о ECR можно в документации: https://docs.aws.amazon.com/AmazonECR/ latest/userguide/what-is-ecr.html. Чтобы отправить образы в реестр контейнеров, они должны находиться в локальном каталоге Docker. Если их там нет, выполните команду в каталоге, где находится родительский файл pom.xml: mvn clean package dockerfile:build После выполнения команды мы должны увидеть наши образы Docker в списке. Для проверки введите команду docker images. На экране должен появиться следующий результат: REPOSITORY ostock/organization-service ostock/gatewayserver ostock/configserver ostock/licensing-service ostock/authentication-service ostock/eurekaserver TAG chapter12 chapter12 chapter12 chapter12 chapter12 chapter12 IMAGE ID b1c7b262926e 61c6fc020dcf 877c9d855d91 6a76bee3e40c 5e5e74f29c2 e6bc59ae1d87 SIZE 485MB 450MB 432MB 490MB 452MB 451MB Следующий шаг – аутентификация нашего клиента Docker в реестре ECR. Для этого выполните следующие команды, чтобы получить пароль и идентификатор нашей учетной записи AWS: aws ecr get-login-password aws sts get-caller-identity --output text --query "Account" После подготовки инфраструктуры: развертывание O-stock и ELK 425 Первая команда вернет пароль, а вторая – идентификатор учетной записи AWS. Оба этих значения понадобятся для аутентификации клиента Docker. Теперь, получив необходимые данные, выполним аутентификацию следующей командой: docker login -u AWS -p [password] ➥ https://[aws_account_id].dkr.ecr.[region].amazonaws.com После аутентификации нужно создать репозитории для хранения наших образов. Чтобы создать их, выполните следующие команды: aws aws aws aws aws aws ecr ecr ecr ecr ecr ecr create-repository --repository-name ostock/configserver create-repository --repository-name ostock/gatewayserver create-repository --repository-name ostock/eurekaserver create-repository --repository-name ostock/authentication-service create-repository --repository-name ostock/licensing-service create-repository --repository-name ostock/organization-service По завершении каждая команда должны вывести примерно такие строки: { "repository": { "repositoryArn": "arn:aws:ecr:us-east-2: 8193XXXXXXX43:repository/ostock/configserver", "registryId": "8193XXXXXXX43", "repositoryName": "ostock/configserver", "repositoryUri": "8193XXXXXXX43.dkr.ecr. us-east-2.amazonaws.com/ostock/configserver", "createdAt": "2020-06-18T11:53:06-06:00", "imageTagMutability": "MUTABLE", "imageScanningConfiguration": { "scanOnPush": false } } } Обязательно выпишите и сохраните все URI репозиториев, потому что они понадобятся для создания тега и отправки образов. Чтобы создать теги для образов, выполните следующие команды: docker tag ostock/configserver:chapter12 [configserver-repositoryuri]:chapter12 docker tag ostock/gatewayserver:chapter12 [gatewayserver-repositoryuri]:chapter12 docker tag ostock/eurekaserver:chapter12 [eurekaserver-repositoryuri]:chapter12 docker tag ostock/authentication-service:chapter12 [authentication-servicerepository-uri]:chapter12 docker tag ostock/licensing-service:chapter12 [licensing-servicerepository-uri]:chapter12 docker tag ostock/organization-service:chapter12 [organization-servicerepository-uri]:chapter12 Глава 12 Развертывание микросервисов 426 Команда docker tag создаст новые теги для наших образов. По выполнении каждая из этих команд должна вывести примерно такие строки, как показано на рис. 12.14. Рис. 12.14. Образы Docker с URI и тегами в репозитории Amazon Elastic Container Registry (ECR) Теперь мы можем отправить образы микросервисов в репозитории реестра ECR. Для этого выполните следующие команды: docker docker docker docker docker docker push push push push push push [configserver-repository-uri]:chapter12 [gatewayserver-repository-uri]:chapter12 [eurekaserver-repository-uri]:chapter12 [authentication-service-repository-uri]:chapter12 [licensing-service-repository-uri]:chapter12 [organization-service-repository-uri]:chapter12 После выполнения всех этих команд можно перейти на страницу службы ECR в консоли управления AWS, где должен появиться список, аналогичный показанному на рис. 12.15. Рис. 12.15. Репозитории ECR для примеров в этой главе Развертывание наших микросервисов в кластере EKS Прежде чем приступать к развертыванию микросервисов, нужно убедиться, что службы лицензий и организаций, а также сервис- После подготовки инфраструктуры: развертывание O-stock и ELK 427 ный шлюз имеют соответствующие файлы в Spring Cloud Config. Как вы наверняка помните, у нас изменились службы PostgreSQL, Redis и стек ELK. ПРИМЕЧАНИЕ. Все изменения, которые мы внесли в файлы на сервере конфигураций, а также конфигурацию для сервера Logstash вы найдете в репозитории GitHub: https://github.com/ ihuaylupo/manningsmia/tree/master/chapter12. Не забывайте, что мы использовали жестко запрограммированное значение при создании конфигурации Logstash. Далее создадим службы Kafka и Zookeeper. Сделать это можно несколькими разными способами. В данном примере мы создадим их с помощью диаграмм Helm. Для тех, кто незнаком с инструментом Helm, отметим, что это диспетчер пакетов для кластеров Kubernetes. Диаграмма Helm – это пакет Helm, содержащий все определения ресурсов, необходимые для запуска какой-то определенной службы, приложения или инструмента в кластере Kubernetes. ПРИМЕЧАНИЕ. Если вы еще не установили Helm, то посетите страницу https://helm.sh/docs/intro/install/, где описываются все возможные способы установки этого пакета. После установки Helm выполните следующие команды, чтобы создать службы Kafka и Zookeeper: helm install zookeeper bitnami/zookeeper \ --set replicaCount=1 \ --set auth.enabled=false \ --set allowAnonymousLogin=true helm install kafka bitnami/kafka \ --set zookeeper.enabled=false \ --set replicaCount=1 \ --set externalZookeeper.servers=zookeeper Эти команды должны вывести список служб с некоторыми деталями. Чтобы убедиться, что все работает, выполните команду kubectl get pods, которая должна вывести список запущенных служб: NAME kafka-0 zookeeper-0 READY 1/1 1/1 STATUS Running Running RESTARTS 0 0 AGE 77s 101s Следующий шаг – преобразование файла docker-compose в формат, совместимый с Kubernetes. Для чего? Дело в том, что Kubernetes не поддерживает файлы Docker Compose, поэтому их нужно преобразовать с помощью инструмента Kompose. Kompose преобразует файлы Docker Compose в формат, поддерживаемый фреймворком Kubernetes. Мы можем с помощью этого инструмента использовать конфигурационные файлы docker- 428 Глава 12 Развертывание микросервисов compose.yaml, преобразуя их командой kompose convert <файл>, запуская командой kompose up и т. д. Желающим узнать больше о Kompose, мы настоятельно рекомендуем заглянуть в документацию: https://kompose.io/. Соответствующие файлы для этого примера вы найдете в папке AWS/EKS в репозитории GitHub: https://github.com/ihuaylupo/ manning-smia/tree/master/chapter12/AWS/EKS. Но, прежде чем двигаться дальше, рассмотрим типы служб в Kubernetes, присваивая которые можно открывать доступ к службам. В Kubernetes есть четыре таких типа: ClusterIP – служба, доступная по внутреннему IP-адресу кластера. Если выбрать этот тип, то наша служба будет доступна только внутри кластера; NodePort – служба, доступная по статическому порту (значение NodePort). Kubernetes выделяет диапазон портов по умолчанию от 3000 до 32767. Этот диапазон можно изменить с помощью флага –-service-node-port-range в разделе spec.containers.commands файла service.yaml; LoadBalancer – служба, доступная извне через балансировщика нагрузки; ExternalName – служба, доступная по внешнему имени. Если заглянуть в конфигурационные файлы, то можно увидеть, что в некоторых файлах <служба>.yml указан тип type=NodePort и атрибут nodePort. В Kubernetes, если не определить тип службы явно, по умолчанию будет использоваться тип ClusterIP. Теперь, узнав о типах служб в Kubernetes, продолжим и создадим службы с использованием преобразованных файлов. Для этого выполните следующие команды в корневом каталоге AWS/ELK: kubectl apply -f <service>.yaml,<deployment>.yaml Мы предпочитаем выполнять эти команды по отдельности, чтобы убедиться, что создание каждой службы завершается успехом, но, вообще, можно создать все службы сразу. Для этого нужно перечислить все файлы YAML в параметре -f, как в предыдущей команде. Например, чтобы создать сервер конфигурации и просмотреть состояние пода и журналы, выполните следующие команды: kubectl apply -f configserver-service.yaml,configserver-deployment.yaml kubectl get pods kubect logs <POD_NAME> --follow Чтобы убедиться, что служба запущена и работает, добавьте несколько правил в группу безопасности, пропускающее весь входящий трафик с портов узла. Для этого выполните следующую команду, чтобы получить идентификатор группы безопасности: После подготовки инфраструктуры: развертывание O-stock и ELK 429 aws ec2 describe-security-groups --filters Name=group-name, Values="*eksctl-ostock-dev-cluster-nodegroup*" --query "SecurityGroups[*].{Name:GroupName,ID:GroupId}" Используя идентификатор группы безопасности, выполним следующую команду, чтобы создать правило для входящего трафика. То же самое можно сделать в консоли управления AWS, перейдя в группу безопасности и создав новое правило, как показано ниже: aws ec2 authorize-security-group-ingress --protocol tcp --port 31000 --group-id [security-group-id] --cidr 0.0.0.0/0 Чтобы получить внешний IP-адрес, можно выполнить команду kubectl get nodes -o wide. Она должна вывести строки, как показано на рис. 12.16. Внешний IP-адрес Рис. 12.16. Получение внешнего IP-адреса для узла Теперь можете открыть в браузере страницу http:<node-externalip>:<NodePort>/actator. Если понадобится удалить под, службу или развертывание, то для этого можно выполнить одну из следующих команд: kubectl delete -f <service>.yaml kubectl delete -f <deployment>.yaml kubectl delete <POD_NAME> Мы почти закончили. Если вы заглядывали в файлы YAML, то могли заметить, что файл postgres.yaml немного отличается от других. В нем мы указали, что служба базы данных будет использоваться с внешним адресом. Чтобы обеспечить возможность соединения, нужно указать конечную точку службы RDS Postgres. В листинге 12.2 показано, как это сделать. Листинг 12.2. Добавление внешней ссылки в службу базы данных apiVersion: v1 kind: Service metadata: labels: app: postgres-service name: postgres-service spec: externalName: ostock-aws.cjuqpwnyahhy.us-east-2 .rds.amazonaws.com Конечная точка RDS PostgreSQL 430 Глава 12 Развертывание микросервисов selector: app: postgres-service type: ExternalName status: loadBalancer: {} Здесь вы должны добавить конечную точку externalName своего экземпляра RDS PostgreSQL. После внесения этого изменения можно выполнить следующую команду: kubectl apply -f postgres.yaml Затем, создав и запустив службу, можно продолжить работу с остальными службами. Работая над этой главой, мы заметили, что нам необходимо создать пиринговое соединение VPC и обновить таблицы маршрутов в RDS и кластере EKS, чтобы обеспечить связь между ними. Мы не будем описывать, как это сделать, но настоятельно рекомендуем прочитать статью «Accessing Amazon RDS From AWS EKS» (https://dev.to/bensooraj/accessing-amazon-rds-from-awseks-2pc3), которая описывает все шаги. Также загляните в официальную документацию AWS, чтобы узнать больше о пиринге VPC (https://docs.aws.amazon.com/vpc/latest/peering/create-vpcpeering-connection.html). На данный момент мы благополучно развернули свой первый набор служб в кластере Amazon EKS. Теперь продолжим и посмотрим, как реализовать конвейер сборки/развертывания, автоматизирующий процесс компиляции, упаковки и развертывания служб в Amazon. 12.4. Конвейер сборки/развертывания в действии В обзорном описании архитектуры в разделе 12.1 вы могли заметить, что за конвейером сборки/развертывания расположено множество движущихся частей. Поскольку цель этой книги – показать все компоненты «в действии», мы рассмотрим порядок реализации конвейера сборки/развертывания для служб O-stock. На рис. 12.17 показаны технологии, которые мы будем использовать для реализации нашего конвейера. Конвейер сборки/развертывания в действии 1. Репозиторий с исходным кодом будет находиться на GitHub 431 2. Для сборки и разверты- 4. Контейнером 5. Контейнер Docker будет вания микросервисов Docker будет образ привязан к AWS ECR O-stock будет использомашины ваться Jenkins Конвейер непрерывной интеграции/доставки Разработчик Репозиторий исходного кода Механизм сборки и развертывания 3. Компилировать код, запускать тесты и создавать выполняемые артефакты будет Maven с подключаемым модулем Docker от Spotify Код готов Модульное Созданы и интегра- артефакты времени ционное тестирова- выполнения ние Образ машины Образ отправлен в репозиторий Окружение разработки Развертывание нового образа/сервера 6. Образ Docker будет развернут в Amazon Elastic Kubernetes Service (EKS) Рис. 12.17. Технологии, используемые в конвейере сборки/развертывания служб O-stock Давайте перечислим технологии, которые будут использоваться в нашем конвейере сборки/развертывания: GitHub (http://github.com) – будет служить репозиторием и системой управления версиями для нашего исходного кода. Весь исходный код примеров из этой книги хранится в GitHub. Мы выбрали GitHub на роль репозитория и системы управления версиями, потому что он предлагает широкий спектр веб-обработчиков и надежный API на основе REST для интеграции в процесс сборки; Jenkins (https://www.jenkins.io/) – это механизм непрерывной интеграции, который мы используем для создания и развертывания микросервисов O-stock и подготовки образа Docker. Jenkins поддерживает возможность настройки через вебинтерфейс и включает несколько встроенных плагинов, облегчающих нашу работу; плагин Maven/Spotify Docker (https://github.com/spotify/ dockerfile-maven) или Spring Boot with Docker (https://spring.io/ guides/gs/spring-boot-docker/) – до настоящего времени для компиляции, тестирования и упаковки кода на Java мы использовали Maven без плагинов. Эти плагины для Maven позволят организовать сборку образов Docker прямо из Maven; Docker (https://www.docker.com/) – мы выбрали Docker на роль контейнерной платформы по двум причинам. Во-первых, технология Docker поддерживается многими поставщиками облачных услуг. Мы можем взять один и тот же контейнер Docker и развернуть его в AWS, Azure или Cloud Foundry с минимальными усилиями. Во-вторых, Docker – легковесная технология. Глава 12 Развертывание микросервисов 432 К концу книги мы собрали и развернули около 10 контейнеров Docker (включая сервер базы данных, платформу обмена сообщениями и поисковую систему). Развернуть такое же количество виртуальных машин на локальном настольном компьютере было бы сложно из-за большого размера и требований к вычислительным ресурсам каждого образа; Amazon Elastic Container Registry (ECR) (https://aws.amazon. com/ecr/) – после сборки службы и создания образа Docker он маркируется уникальным идентификатором и помещается в центральный репозиторий. В роли такого репозитория образов Docker мы решили использовать ECR; Amazon EKS Container Service (https://aws.amazon.com/eks/) – конечным местом назначения наших микросервисов будут экземпляры Docker, развернутые на платформе Docker Amazon. Мы выбрали Amazon в качестве облачной платформы, потому что это самый зрелый из поставщиков облачных услуг, который предлагает наиболее простой способ развертывания служб Docker. 12.5. Создание конвейера сборки/развертывания Для реализации конвейера сборки/развертывания можно использовать десятки разных механизмов управления версиями, сборки и развертывания (как локальных, так и облачных). В этой книге мы выбрали GitHub в качестве репозитория и системы управления версиями и Jenkins – механизма сборки. Система управления версиями Git чрезвычайно популярна, а GitHub – один из крупнейших облачных репозиториев, доступных в настоящее время. Jenkins – это механизм сборки, тесно интегрированный с GitHub. Его простота и самодостаточность позволяют легко создать и запустить несложный конвейер для сборки. До сих пор все примеры кода в этой книге можно было запускать исключительно на настольном компьютере (за исключением подключения к GitHub). В этой главе, если вы решите последовать за примером кода, вам потребуется настроить свои учетные записи GitHub, Jenkins и AWS. Мы не будем подробно рассказывать, как настроить Jenkins, но если вы не знакомы с Jenkins, то загляните в созданный нами файл, где подробно (шаг за шагом) рассказывается, как настроить Jenkins с самого начала в экземпляре EC2. Файл находится в репозитории главы 12: https:// github.com/ihuaylupo/manning-smia/blob/master/chapter12/AWS/ jenkins_Setup.md. Создание конвейера сборки/развертывания 433 12.5.1. Настройка GitHub Чтобы сконструировать наш конвейер, мы должны создать вебобработчик GitHub. Что такое веб-обработчик (webhook)? Вебобработчик также известен как обратный веб-вызов HTTP. Обратные вызовы HTTP предоставляют информацию другим приложениям в масштабе реального времени. Обычно обратный вызов производится, когда процедура или действие выполняется в приложении, содержащем веб-обработчик. Если вы не слышали о веб-обработчиках, то, возможно, вам интересно узнать, зачем они нужны. Для наших целей мы создадим веб-обработчик в GitHub, чтобы он сообщал Jenkins о событиях отправки исходного кода в репозиторий. Благодаря этому Jenkins сможет начать процедуру сборки. Чтобы создать веб-обработчик в GitHub, нужно выполнить следующие шаги (они также показаны на рис. 12.18). 1 Перейдите в репозиторий, который будет поддерживаться конвейером сборки проекта. 2 Щелкните на вкладке Settings (Параметры). 3 Выберите раздел Webhooks (Веб-обработчики). 4 Щелкните на кнопке Add Webhook (Добавить веб-обработчик). 5 В поле Payload URL (URL для вызова) укажите конкретный URL. 6 Выберите параметр Just the Push Event (Только для события отправки). 7 Щелкните на кнопке Update Webhook (Обновить веб-обработчик). URL для вызова конструируется по шаблону: http://<Jenkins_IP_Address>:8080/github-webhook/ Обратный вызов отправляется по событию передачи исходного кода в репозиторий Рис. 12.18. Создание веб-обработчика для уведомления Jenkins Глава 12 Развертывание микросервисов 434 Важно отметить, что в URL для вызова должен быть указан IPадрес Jenkins. Таким образом, GitHub сможет посылать уведомление механизму Jenkins всякий раз, когда выполняется отправка исходного кода в репозиторий. Теперь, настроив веб-обработчик, реализуем конвейер с использованием Jenkins. 12.5.2. Сборка наших служб в Jenkins Сердцем сборки каждой службы в этой книге был файл Maven pom. xml, определяющий порядок сборки службы Spring Boot, ее упаковки в выполняемый файл JAR и создания образа Docker, который можно использовать для запуска службы. Вплоть до этой главы компиляция и запуск служб выполнялись следующим образом. 1 Открывалось окно командной строки на локальном компьютере. 2 Запускался сценарий Maven для главы, выполняющий сборку примеров для главы и упаковывающий выполняемый код в образ Docker, который отправлялся в локальный репозиторий Docker. 3 Вновь созданные образы из локального репозитория Docker запускались командами docker-compose и docker-machine. Но как повторить этот процесс в Jenkins? Для этого нужно создать файл, который называется Jenkinsfile. Jenkinsfile – это сценарий, описывающий действия по сборке. Этот файл хранится в корневом каталоге репозитория микросервисов на GitHub. Когда мы отправляем исходный код в репозиторий GitHub, вебобработчик вызывает Jenkins, используя указанный нами URL, после чего задание Jenkins отыскивает файл Jenkinsfile и запускает процесс сборки. На рис. 12.19 показаны этапы этого процесса. 3. Настраивает базовую конфигурацию, включая 1. Разработчик отправляет 2. Jenkins проверяет код инструменты, которые будут использоваться код микросервиса в GitHub и, используя Jenkinsfile, при сборке запускает процесс сборки и развертывания 4. Собирает проект с помощью Maven и создает целевой JAR-файл приложения Jenkinsfile Разработчик Репозиторий исходного кода Механизм сборки и развертывания 5. Jenkins запускает плагин Maven Dockerfile, чтобы создать новый образ 6. Помечает образы данными репозитория ECR 8. Передает службы в Amazon EKS 7. Помещает образы Docker в репозитории ECR Рис. 12.19. Процесс сборки и развертывания нашего программного обеспечения с помощью Jenkins Создание конвейера сборки/развертывания 435 Как показано на рис. 12.19: a разработчик сохраняет в репозиторий GitHub исходный код одного из микросервисов; b Jenkins получает уведомление от GitHub об изменении исходного кода. Это уведомление отправляется вебобработчиком GitHub. Jenkins начинает процесс, извлекая исходный код из GitHub и используя Jenkinsfile для запуска процедуры сборки и развертывания; c Jenkins настраивает базовую конфигурацию для сборки и устанавливает все зависимости; d Jenkins собирает проект, выполняет модульные и интеграционные тесты и генерирует целевой JAR-файл для приложения; e Jenkins запускает плагин Maven Dockerfile для создания новых образов Docker; f Jenkins маркирует новые образы данными репозитория ECR; g созданные образы отправляются в ECR с теми же тегами, которые были добавлены на шаге 6; h процесс сборки подключается к кластеру EKS и развертывает службы с помощью файлов service.yaml и deployment.yaml. Краткое примечание В этой книге мы создали в репозитории GitHub отдельные папки для каждой главы. Весь исходный код главы можно собрать и развернуть как единое целое. Однако в ваших проектах мы рекомендуем настроить каждый микросервис с его окружением в отдельном репозитории и для каждого организовать независимый процесс сборки. Благодаря этому вы получите возможность развертывать каждую службу независимо. В примере реализации процесса сборки мы развертываем сервер конфигурации как единое целое только потому, что предполагаем отправлять проекты в облако Amazon по отдельности и поддерживать сценарии сборки для каждой службы отдельно. В этом примере мы развертываем только сервер конфигурации, но вы можете использовать те же шаги для развертывания других служб. Теперь, рассмотрев основные этапы процесса сборки/развертывания с Jenkins, разберемся с созданием конвейера Jenkins. ПРИМЕЧАНИЕ. Для работы конвейера необходимо установить несколько плагинов Jenkins, таких как GitHub Integration, GitHub, Maven Invoker, Maven Integration, Docker Pipeline, ECR, Kubernetes Continuous Deploy и Kubernetes CLI. За дополнительной информацией об этих плагинах обращайтесь к официальной документации Jenkins (например, описание Kubernetes CLI доступно по адресу https://plugins.jenkins.io/kubernetes-cli/). 436 Глава 12 Развертывание микросервисов После установки всех плагинов нужно создать учетные данные Kubernetes и ECR. Начнем с учетных данных Kubernetes. Чтобы создать их, нужно перейти в раздел Manage Jenkins > Manage Credentials > Jenkins > Global Credentials (Управление Jenkins > Управление учетными данными > Jenkins > Глобальные учетные данные) и щелкнуть на ссылке Add Credentials (Добавить учетные данные). Выберите вариант Kubernetes Configuration (kubeconfig) (Конфигурация Kubernetes (kubeconfig)) и заполните форму информацией о вашем кластере Kubernetes. Чтобы получить информацию из кластера Kubernetes, подключитесь к кластеру, как объяснялось выше при развертывании служб вручную, а затем выполните команду: kubectl config view Скопируйте данные, которые вернет команда, и вставьте их в текстовое поле Content (Содержимое). Этот шаг показан на рис. 12.20. Теперь создадим учетные данные ECR. Для этого перейдите на страницу IAM в консоли AWS, затем зайдите в раздел Users (Пользователи) и щелкните на ссылке Add User (Добавить пользователя). На странице Add User (Добавить пользователя) укажите имя пользователя, тип доступа и политики. Затем загрузите файл .CSV с учетными данными пользователя и, наконец, сохраните пользователя. Мы поможем последовательно пройти процесс получения учетных данных. Сначала введите следующую информацию на странице IAM: User name: ecr-user Access type: Programmatic access Вывод команды kubectl config view Рис. 12.20. Настройка учетных данных kubeconfig для подключения к кластеру EKS Создание конвейера сборки/развертывания 437 На следующей странице Permissions (Разрешения) выберите параметр Attach Existing Policies Directly (Подключить существующие политики напрямую), найдите политику «AmazonEC2ContainerRe gistryFullAccess» и выберите ее. Наконец, на последней странице щелкните на ссылке Download .csv (Загрузить .csv). Шаг загрузки действительно важен, потому что эти учетные данные вам понадобятся позже. Наконец, щелкните на кнопке Save (Сохранить). Следующий шаг – добавление учетных данных в Jenkins, но перед этим убедитесь, что установили плагин ECR для Jenkins. После установки перейдите в раздел Manage Jenkins > Manage Credentials > Jenkins > Global Credentials (Управление Jenkins > Управление учетными данными > Jenkins > Глобальные учетные данные) и щелкнуть на ссылке Add Credentials (Добавить учетные данные). На открывшейся странице добавьте следующие данные: ID: ecr-user Description: ECR User Access Key ID: <Access_key_id_from_csv> Secret Access Key: <Secret_access_key_from_csv> После с оздания учетных данных можно продолжить создание конвейера. На рис. 12.21 показан процесс его настройки. Чтобы получить доступ к странице, показанной на рис. 12.21, откройте панель управления Jenkins и щелкните на ссылке New Item (Новый элемент) вверху слева. Следующий шаг – добавление URL репозитория GitHub и установка флажка GitHub Hook Trigger for GITScm Polling (Триггер GitHub для опроса GITScm; рис. 12.22). Этот флажок включает прием уведомлений, которые мы настроили в предыдущем разделе. Выберите имя элемента Выберите пункт Pipeline (Конвейер) Рис. 12.21. Создание конвейера Jenkins для сборки сервера конфигурации 438 Глава 12 Развертывание микросервисов URL репозитория проекта Установите флажок GitHub hook trigger for GITScm polling Рис. 12.22. Настройка веб-обработчика GitHub и URL репозитория в конвейере Jenkins, шаг 1 Наконец, в разделе Pipeline (Kонвейер) выберите в раскрывающемся списке Definition (Определение) пункт Pipeline Script from SCM (Сценарий конвейера из SCM). Этот последний шаг позволяет отыскать файл Jenkinsfile в том же репозитории, где хранится исходный код приложения. Этот процесс показан на рис. 12.23. Рис. 12.23. Настройка веб-обработчика GitHub и URL репозитория в конвейере Jenkins, шаг 2 Создание конвейера сборки/развертывания 439 Как показано на рис. 12.23, в раскрывающемся списке SCM выберите пункт Git, а затем добавьте информацию для конвейера, такую как URL репозитория, ветвь и путь к сценарию, и имя файла Jenkinsfile. Наконец, щелкните на кнопке Save (Сохранить), чтобы перейти на главную страницу конвейера. ПРИМЕЧАНИЕ. Если вашему репозиторию требуются учетные данные, то вы увидите сообщение об ошибке. 12.5.3. Создание сценария конвейера Сценарий Jenkinsfile настраивает базовую конфигурацию времени выполнения для процесса сборки. Обычно этот файл делится на несколько разделов. Обратите внимание, что вы можете определить столько разделов или этапов, сколько захотите. В листинге 12.3 показано содержимое Jenkinsfile, управляющего сборкой сервера конфигурации. Листинг 12.3. Содержимое Jenkinsfile node { def mvnHome stage('Preparation') { Настройка URL репозитория GIT Этап подготовки git 'https://github.com/ihuaylupo/spmia.git' mvnHome = tool 'M2_HOME' Конфигурация Maven на сервере Jenkins } stage('Build') { Этап сборки // Запуск сборки с помощью maven withEnv(["MVN_HOME=$mvnHome"]) { if (isUnix()) { sh "'${mvnHome}/bin/mvn' -Dmaven.test.failure.ignore clean package" Вызов Maven} else { цели clean bat(/"%MVN_HOME%\bin\mvn" -Dmaven.test.failure.ignore package clean package/) } } Получает результаты тестирования от JUnit } и создает JAR-файл с приложением для установки в образ Docker stage('Results') { junit '**/target/surefire-reports/TEST-*.xml' archiveArtifacts 'configserver/target/*.jar' } stage('Build image') { Этап сборки образа sh "'${mvnHome}/bin/mvn' -Ddocker.image.prefix=8193XXXXXXX43.dkr.ecr.us-east-2.amazonaws.com/ ostock -Dproject.artifactId=configserver Глава 12 Развертывание микросервисов 440 -Ddocker.image.version=latest dockerfile:build" } Вызов Maven-цели dockerfile:build, которая создает новый образ Docker и маркирует его информацией для репозитория ECR stage('Push image') { docker.withRegistry('https://8193XXXXXX43.dkr.ecr.us-east2.amazonaws.com', 'ecr:us-east-2:ecr-user') { sh "docker push 8193XXXXXX43.dkr.ecr.us-east2.amazonaws.com/ostock/ configserver:latest" Сохраняет образ } в репозиторий ECR } Этап сохранения образа в репозиторий Этап развертывания Kubernetes stage('Kubernetes deploy') { kubernetesDeploy configs: 'configserver-deployment.yaml', kubeConfig: [path: ''], kubeconfigId: 'kubeconfig', secretName: '', ssh: [sshCredentialsId: '*', sshServer: ''], textCredentials: [certificateAuthorityData: '', clientCertificateData: '', clientKeyData: '', serverUrl: 'https://'] } } Этот файл Jenkinsfile описывает все этапы нашего конвейера. Как видите, в нем содержатся команды для выполнения каждого этапа. На первом этапе мы добавляем конфигурацию Git. После этого мы можем удалить определение URL репозитория GitHub в разделе сборки. На втором этапе вторая команда определяет путь установки Maven, чтобы получить возможность использовать команду maven в оставшейся части Jenkinsfile. На третьем этапе определяется команда для упаковки нашего приложения. В данном случае мы используем цель clean package. Для упаковки приложения мы будем использовать переменную окружения, которая хранит путь к корневому каталогу Maven. Четвертый этап выполняет цель dockerfile:build, которая отвечает за создание нового образа Docker. Если вы помните, мы включили плагин Maven spotify-dockerfile в файлах pom.xml наших микросервисов. Если внимательно посмотреть на строку с dockerfile:build, то можно заметить, что мы отправляем некоторые параметры в Maven: sh "'${mvnHome}/bin/mvn' -Ddocker.image.prefix=8193XXXXXXX43.dkr.ecr.useast-2.amazonaws.com/ostock -Dproject.artifactId=configserver -Ddocker.image.version=latest dockerfile:build" В этой команде мы передаем только префикс образа Docker, версию образа и идентификатор артефакта. Но вообще здесь можно передать столько параметров, сколько захотите. Кроме того, это хорошее место для назначения профиля, который будет использоваться при развертывании (например: dev, stage или production). Кроме того, на этом этапе можно создавать и назначать новым об- Создание конвейера сборки/развертывания 441 разам теги с данными репозитория ECR. В этом конкретном случае мы используем последнюю версию, но здесь также можно указать разные переменные для создания разных образов Docker. Пятый этап сохраняет образы в репозитории ECR. Здесь мы добавили только что созданный сервер конфигурации в образ Docker в репозитории 8193XXXXXXX43.dkr.ecr.us-east-2.amazonaws.com/ ostock. Заключительный этап – развертывание Kubernetes в нашем кластере EKS. Эта команда может отличаться для разных конфигураций, поэтому ниже мы дадим вам совет, как автоматически создать сценарий конвейера для этого этапа. 12.5.4.Создание сценариев для конвейера развертывания Kubernetes Jenkins предлагает параметр Pipeline Syntax (синтаксис конвейера), который автоматически создает различные фрагменты конвейера для использования в файле Jenkinsfile. Процесс создания таких фрагментов показан на рис. 12.24. Чтобы использовать этот параметр, нужно выполнить следующие шаги. 1 Перейдите на страницу панели управления Jenkins и выберите свой конвейер. 2 Щелкните на параметре Pipeline Syntax (синтаксис конвейера) в левом столбце. 3 На странице Snippet Generator (Генератор фрагментов) выберите в раскрывающемся списке Sample Step (Пример шага) параметр kubernetesDeploy: Deploy to Kubernetes (kubernetesDeploy: Развертывание в Kubernetes). 4 В раскрывающемся списке Kubeconfig выберите учетные данные Kubernetes, созданные в предыдущем разделе. 5 В поле Config Files (Файлы конфигурации) введите имя файла deployment.yaml. Этот файл находится в корневом каталоге проекта GitHub. 6 Оставьте остальные значения по умолчанию. 7 Щелкните на кнопке Generate Pipeline Script (Сгенерировать сценарий конвейера). ПРИМЕЧАНИЕ. Помните, что для примеров в этой книги мы создали для каждой главы отдельные папки в одном репозитории GitHub. Весь исходный код примеров из этой главы можно собрать и развернуть как единое целое. Однако в ваших проектах мы рекомендуем настроить каждый микросервис с его окружением в отдельном репозитории и для каждого организовать независимый процесс сборки. 442 Глава 12 Развертывание микросервисов Теперь, выполнив все необходимые настройки, вы можете изменять свой код, а ваш конвейер Jenkins будет собирать его автоматически. Рис. 12.24. Использование параметра Pipeline Syntax (Синтаксис конвейера) для автоматического создания команды kubernetesDeploy 12.6.Заключительные рассуждения о конвейере сборки/развертывания Завершая эту главу (и книгу), мы хотим выразить надежду, что вы оценили, сколько труда нужно вложить в создание конвейера сборки/развертывания. С другой стороны, надежный конвейер сборки/развертывания имеет решающее значение для развертывания служб. И успех вашей микросервисной архитектуры зависит не только от кода: вы должны понимать, что представленная здесь реализация конвейера сборки/развертывания намеренно упрощена для целей демонстрации в этой книге. Хороший конвейер сборки/развертывания гораздо универсальнее. Он будет поддерживаться командой DevOps и делиться на серию независимых шагов (компиляция > упаковка > развертывание > тестирование), которые группы разработчиков могут использовать для «включения» своих сценариев сборки микросервисов; Заключительные рассуждения о конвейере сборки/развертывания 443 процесс создания образа виртуальной машины, представленный в этой главе, тоже упрощен. Каждый микросервис создается с использованием файла Dockerfile, в котором определяется – какое программное обеспечение будет установлено в контейнере Docker. В настоящее время существует большое количество инструментов подготовки образов, таких как Ansible (https://github.com/ansible/ansible), Puppet (https://github.com/puppetlabs/puppet) и Chef (https:// github.com/chef/chef), помогающих устанавливать и настраивать операционные системы в виртуальные машины или образы контейнеров; топология развертывания облачного приложения в нашем примере консолидирована на одном сервере. В реальном конвейере сборки/развертывания каждый микросервис будет иметь свои сценарии сборки и развертываться в контейнере кластера EKS независимо от других. Разработка микросервисов – сложная задача. С переходом на микросервисную архитектуру сложность создания приложений никуда не делась; она просто трансформировалась и перераспределилась. Идея построения отдельных микросервисов проста и понятна, но запуск и поддержка надежной микросервисной архитектуры требует большего, чем простое написание кода. Помните, что микросервисная архитектура дает множество вариантов и решений, которые необходимо учитывать при создании приложений. Принимая эти решения, вы должны учитывать, что микросервисы слабо связаны, абстрактны, независимы и ограничены. Надеемся, что к настоящему времени вы получили достаточно информации (и опыта) для создания собственной архитектуры микросервисов. В этой книге вы узнали, что такое микросервисы, и познакомились с важными проектными решениями, необходимыми для их реализации. Вы также узнали, как создать законченную архитектуру микросервисов, следуя рекомендациям из руководства «Приложение двенадцати факторов» (управление конфигурацией, обнаружение служб, обмен сообщениями, журналирование, трассировка, безопасность) и используя различные инструменты и технологии, которые идеально сочетаются с фреймворком Spring. Наконец, вы узнали, как реализовать конвейер сборки/развертывания. Все вышеупомянутые ингредиенты составляют идеальный рецепт для разработки успешных микросервисов. В заключение приведем слова Мартина Фаулера (Martin Fowler): «Создавая плохую архитектуру, мы обманываем наших клиентов, потому что снижаем их конкурентоспособность». Глава 12 Развертывание микросервисов 444 Итоги Конвейер сборки/развертывания является важной частью доставки микросервисов. Надежный конвейер сборки/развертывания должен позволять развертывать новые функции и исправления ошибок за считанные минуты. Конвейер сборки/развертывания должен быть автоматизированным и работать без прямого вмешательства человека. Любая часть процесса, выполняемая вручную, открывает двери перед неудачами. Автоматизация конвейера сборки/развертывания требует большого количества сценариев и тщательной настройки их работы. Не следует недооценивать объем работы, необходимой для постройки такого конвейера. Конвейер сборки/развертывания должен создавать неизменяемый образ виртуальной машины или контейнера. После создания образа сервера его нельзя изменять. Конфигурация сервера для конкретного окружения должна передаваться в виде параметров на этапе настройки сервера. Ещё больше книг по Java в нашем телеграм канале: https://t.me/javalib Приложение A Передовые практики конструирования микросервисной архитектуры В главе 2 мы представили некоторые передовые практики, которые желательно использовать при создании микросервисов. В этом приложении мы хотим подробнее рассказать о них. В следующих разделах мы дадим некоторые рекомендации (и поделимся своим опытом) по созданию успешной микросервисной архитектуры. Важно отметить, что не существует четко определенного набора правил; однако Хусейн Бабал (Hüseyin Babal) в своем выступлении «Ultimate Guide to Microservice Architecture» представил ряд передовых практик, помогающих получить гибкую, эффективную и масштабируемую архитектуру. Мы считаем, что с текстом этого выступления должны ознакомиться все разработчики, потому что в нем не только даются рекомендации по разработке микросервисных архитектур, но также выделяются основные компоненты микросервисов. В этом приложении мы выбрали некоторые из описанных Хусейном практик, которые считаем наиболее важными (и которые использовали на протяжении всей книги, чтобы получить гибкую и успешную архитектуру). ПРИМЕЧАНИЕ. Если вам интересно узнать обо всех передовых практиках, упомянутых в выступлении «Ultimate Guide to Microservice Architecture», перейдите по ссылке к видео с их описанием: https://www.youtube.com/watch?v= CTnMUE06oZI. Приложение A 446 В следующем списке перечислены некоторые из передовых практик, упомянутых в этом выступлении, которые мы рассмотрим далее: Модель зрелости Ричардсона; Распределенная конфигурация; Мониторинг; Управление производительностью приложения; Spring HATEOAS; Непрерывная доставка; Журналирование; API-шлюз. Модель зрелости Ричардсона Эта передовая практика (http://martinfowler.com/articles/ richardsonMaturityModel.html), описанная Мартином Фаулером (Martin Fowler), является основой для понимания базовых принципов архитектуры REST и ее оценки. Важно отметить, что эти уровни следует рассматривать скорее как инструмент, помогающий понять компоненты REST и идеи, лежащие в основе RESTful-мышления, а не как механизм оценки компании. На рис. A.1 показаны различные уровни зрелости, используемые для оценки службы. Уровень 3 Уровень 2 Уровень 1 Уровень 0 Гипертекст как механизм передачи состояния приложения (Hypertext As The Engine Of Application State, HATEOAS) Правильное использование глаголов HTTP Несколько URI – один HTTP-метод «Болото» старого доброго XML (Plain Old XML, POX) Рис. A.1. Модель зрелости Ричардсона Уровень 0 в модели зрелости Ричардсона представляет основные функции, ожидаемые от API. По сути, взаимодействие с API на этом уровне сводится к вызову удаленных процедур (Remote Procedure Call, RPC) через единственный URI. На этом уровне в каждом запросе и ответе пересылается текст в формате XML, который определяет действие, цель и параметры для службы. Например, предположим, что мы хотим купить определенный товар в интернет-магазине. Программное обеспечение магазина долж- Приложение A 447 но сначала убедиться, что товар есть в наличии. На этом уровне интернет-магазин предоставляет конечную точку с единственным URI. На рис. A.2 показано, как выглядят запросы и ответы на уровне 0. POST /shoppingService ProductRequest ProductResponse ShoppingService POST /shoppingService ShoppingRequest ShoppingResponse Рис. A.2 Уровень 0: простой вызов удаленных процедур с передачей текста в старом добром формате XML Основная идея уровня 1 заключается в выполнении действий с отдельными ресурсами, а не в отправке запросов единственной конечной точке. На рис. A.3 показаны отдельные ресурсы и соответствующие им запросы и ответы. POST /product/1234 ProductRequest Product ProductResponse POST /shop/1234 ShoppingRequest Shop ShoppingResponse Рис. A.3. Уровень 1: вызовы отдельных ресурсов для выполнения конкретных действий На уровне 2 службы используют глаголы HTTP для выполнения определенных действий. Глагол HTTP GET используется для извлечения, POST – для создания, PUT – для изменения и DELETE – для удаления. Как упоминалось выше, эти уровни служат скорее инструментом, помогающим понять компоненты REST, и сторонники идеологии REST стремятся использовать все глаголы HTTP. На рис. A.4 показано, как выглядит использование глаголов HTTP в нашем примере с интернет-магазином. GET /product/1234 ProductRequest Product 200 OK ProductResponse POST /shop/1234 ShoppingRequest Shop 201 Created ShoppingResponse Рис. A.4. Глаголы HTTP. В нашем примере глагол GET извлекает информацию о продукте, а POST создает заказ на покупку Приложение A 448 Последний третий уровень вводит гипертекст как механизм передачи состояния приложения (Hypertext as the Engine of Application State, HATEOAS). Реализуя HATEOAS, можно заставить API предоставлять дополнительную информацию, например ссылки на ресурсы, для организации более разнообразных взаимодействий (рис. A.5). GET /product/1234 ProductRequest Product 200 OK ProductResponse ... <link rel=“/linkrels/shop/product” uri=“/shop/1234”> POST /shop/1234 ShoppingRequest Shop 201 Created ShoppingResponse Рис. A.5. HATEOAS участвует в формировании ответа на запрос для получения продукта. В нашем сценарии ссылка несет дополнительную информацию о том, как купить конкретный товар Модель зрелости Ричардсона показывает, что не существует какого-то определенного способа реализации приложений на основе REST. Однако мы можем выбирать тот или иной уровень, который наилучшим образом соответствует потребностям наших проектов. Spring HATEOAS Spring HATEOAS (https://spring.io/projects/spring-hateoas) – это небольшой проект Spring, который позволяет создавать API, следующие принципу HATEOAS, описанному на уровне 3 модели зрелости Ричардсона. С помощью Spring HATEOAS можно быстро создавать классы моделей для ссылок и представлений ресурсов. Он также предоставляет API для конструирования ссылок, указывающих на методы контроллера Spring MVC и многое другое. Внешняя конфигурация Эта передовая практика идет рука об руку с передовой практикой конфигурации, перечисленной в руководстве «Приложение двенадцати факторов», упомянутом во второй главе (раздел 2.3). Конфигурация микросервисов никогда не должна храниться в одном репозитории с исходным кодом. Почему это так важно? Архитектура микросервисов состоит из набора служб (микросервисов), которые выполняются в отдельных процессах. Каждую Приложение A 449 службу можно развертывать и масштабировать независимо, запуская больше экземпляров одного и того же микросервиса. Итак, предположим, что вы, как разработчик, хотите изменить конфигурационный файл определенного микросервиса, который подвергся масштабированию несколько раз. Если не следовать этой передовой практике, то конфигурация будет упаковываться вместе с развертываемым микросервисом и вам придется повторно развернуть каждый экземпляр. Это не только отнимает время, но также может привести к проблемам использования разных конфигураций разными экземплярами микросервиса. Есть много способов реализовать внешнюю конфигурацию в микросервисной архитектуре. Но в этой книге мы использовали Spring Cloud Config, как описано в главе 2. Непрерывная интеграция и непрерывная доставка Непрерывная интеграция (Continuous Integration, CI) – это набор практик разработки программного обеспечения, которые позволяют членам команды обнаруживать ошибки и получать результаты анализа качества программного кода в течение короткого времени после интеграции их изменений. Это достигается с помощью автоматической и непрерывной проверки кода (сборки), которая включает выполнение тестов. С другой стороны, непрерывная доставка (Continuous Delivery, CD) – это практика разработки программного обеспечения, требующая автоматизации процесса доставки программного обеспечения, чтобы обеспечить скорейшее внедрение изменение в промышленное окружение. Применяя эти практики при разработке микросервисной архитектуры, важно помнить, что никогда не должно быть «списка ожидания» для интеграции и выпуска кода в производство. Например, группа, отвечающая за службу X, должна иметь возможность передавать свои изменения в промышленное окружение в любое время, не дожидаясь выпуска других служб. На рис. A.6 показана высокоуровневая диаграмма, изображающая, как должен выглядеть процесс CI/CD, когда в работе участвует несколько команд. Для реализаций этого процесса необходимо привлекать дополнительные технологии, потому что фреймворк Spring предназначен для разработки приложений и не имеет инструментов для создания конвейера сборки/развертывания. Приложение A 450 Служба Product Написание кода Команда A Доставка в промежуточное окружение Развертывание в промышленном окружении Репозиторий исходного кода Модульное/ интеграцион- Репозиторий ное образов тестирование Доставка сборки в тестовое окружение Доставка в промежуточное окружение Развертывание в промышленном окружении Репозиторий исходного кода Модульное/ интеграцион- Репозиторий образов ное тестирование Доставка сборки в тестовое окружение Доставка в промежуточное окружение Развертывание в промышленном окружении Модульное/ интеграцион- Репозиторий образов ное тестирование Доставка сборки в тестовое окружение Доставка в промежуточное окружение Развертывание в промышленном окружении Служба User Написание кода Команда C Доставка сборки в тестовое окружение Служба Account Написание кода Команда B Модульное/ интеграцион- Репозиторий ное образов тестирование Репозиторий исходного кода Служба Authentication Написание кода Репозиторий исходного кода Команда D Рис. A.6. Процессы CI/CD при разработке микросервисов с использованием тестового и промежуточного окружений. Здесь каждая команда имеет свой репозиторий исходного кода, модульные и интеграционные тесты, репозиторий образов и процесс развертывания. Это позволяет каждой команде развертывать и выпускать новые версии, не дожидаясь выхода новых версий других микросервисов Мониторинг Мониторинг имеет решающее значение для микросервисов. Микросервисы являются частью больших и сложных распределенных систем, и чем более распределена система, тем сложнее находить и решать ее проблемы. Представьте, что у нас есть архитектура с сотнями микросервисов, и один из них влияет на работу других служб. Как узнать, какая служба работает нестабильно, не имея надежного процесса мониторинга? В приложении C мы расскажем, как решить проблему создания системы мониторинга с помощью инструментов, перечисленных ниже. На рис. A.7 показано, как взаимодействуют Micrometer, Prometheus и Grafana. Micrometer (https://micrometer.io/) – это библиотека метрик по умолчанию в Spring Boot 2, которая позволяет получать характеристики работы приложения, а также метрики JVM, такие как частота и время сборки мусора, объем пулов памяти и т. д.; Prometheus (https://prometheus.io/) – это система мониторинга и оповещений с открытым исходным кодом, в которой все данные хранятся в виде временных рядов; Grafana (https://grafana.com/) – это платформа для анализа метрик, позволяющая запрашивать, визуализировать и исследовать данные независимо от их местонахождения. Приложение A 451 Использование данных Набор данных Сборщик данных Micrometer Сервер Prometheus Grafana Диспетчер уведомлений Prometheus Рис. A.7. Взаимодействие Micrometer, Prometheus и Grafana Журналирование Каждый раз, думая о журналировании, держите в уме распределенную трассировку. Распределенная трассировка помогает выявить, где в микросервисной архитектуре происходит сбой и отладить его. В главе 11 мы объяснили, как этого добиться с помощью Spring Cloud. Наблюдаемость запросов, курсирующих в нашей архитектуре, – еще одна «новая» технологическая ситуация (Spring Cloud Sleuth), которая всплывает в разговорах о микросервисах. В монолитных архитектурах большинство запросов обрабатывается в одном приложении. Исключением из правил являются запросы, которые порождают обращения к базам данных или другим службам. Однако микросервисная архитектура (например, Spring Cloud) – это окружение, состоящее из нескольких приложений, обслуживающих один клиентский запрос, который может пересекать несколько приложений, пока на него не будет дан ответ. Как проследить путь этого запроса в микросервисной архитектуре? Можно присвоить каждому запросу уникальный идентификатор и затем передавать его во все вызовы, а также организовать централизованный сбор журналов для просмотра всех записей, в которых упоминается конкретный клиентский запрос. API-шлюзы API-шлюз – это интерфейс REST API, предоставляющий центральную точку доступа к группе микросервисов и/или определенным сторонним API. Иначе говоря, Spring Cloud API Gateway, по существу, отделяет интерфейс, который видят клиенты, от реализации микросервиса. Это особенно полезно, когда желательно исключить возможность доступа к внутренним службам извне. Имейте в виду, что эта система действует не только как шлюз, но также позволяет добавлять в нее дополнительные функции, такие как: Приложение A 452 аутентификация и авторизация (OAuth2); защита от угроз (DoS-атаки, инъекция кода и т. д.); анализ и контроль (кто использует API, как и когда); мониторинг входящего и исходящего трафика. Приложение B Типы грантов в OAuth2 После прочтения главы 9 может сложиться впечатление, что OAuth2 – простая штука. В конце концов, у нас есть служба аутентификации, которая проверяет учетные данные пользователя и выдает ему токен. Токен в свою очередь может быть проверен при вызове пользователем службы, защищенной службой OAuth2. Учитывая взаимосвязанный характер облачных и вебприложений, пользователи ожидают, что они смогут безопасно обмениваться своими данными и интегрировать функциональные возможности различных приложений, принадлежащих различным службам. Это представляет собой уникальную проблему с точки зрения безопасности, потому что требуется интегрировать различные приложения, не заставляя пользователей вводить свои учетные данные для каждого приложения, участвующего в работе. OAuth2 – это гибкий фреймворк авторизации, предоставляющий приложениям несколько механизмов для аутентификации и авторизации пользователей и избавляющий их от необходимости вводить свои учетные данные снова и снова. К сожалению, это также одна из причин, почему OAuth2 считается сложным. Эти механизмы называются грантами аутентификации. OAuth2 имеет четыре формы грантов (разрешений на авторизацию), которые клиентские приложения могут использовать для аутентификации пользователей, получения токена доступа, а затем для проверки этого токена: пароль; учетные данные клиента; код авторизации; неявный. В следующих разделах мы покажем, что происходит во время обработки каждого из этих типов разрешений в OAuth2. Мы также говорим о том, когда следует использовать тот или другой тип разрешения. Приложение B 454 Тип разрешения: пароль Пароль, вероятно, является в OAuth2 наиболее простым типом разрешений. Его следует использовать, когда приложение и службы явно доверяют друг другу. Например, веб-приложение O-stock, а также службы лицензий и организаций принадлежат одной и той же компании (Optima Growth), поэтому между ними существуют естественные доверительные отношения. ПРИМЕЧАНИЕ. Говоря о «естественных доверительных отношениях», мы имеем в виду, что одна и та же организация полностью владеет приложением и службами. Они управляются одними и теми же политиками и процедурами. В естественных доверительных отношениях почти не приходится беспокоиться о передаче токена доступа OAuth2 вызывающему приложению. Например, веб-приложение O-stock может использовать пароль для получения учетных данных пользователя и прямой аутентификации в службе OAuth2. На рис. B.1 показано использование пароля для взаимодействий между O-stock и нижестоящими службами. 2. Пользователь входит 3. OAuth2 аутентифицирует в приложение O-stock, пользователя и приложение которой передает учетные и возвращает токен доступа данные пользователя и имя приложения с ключом службе OAuth2 Пользователь Приложение O-stock 4. O-stock добавляет токен доступа во все вызовы служб, выполняемые от имени пользователя 1. Владелец приложения регистрирует приложение в службе OAuth2, которая предоставляет секретный ключ Служба OAuth2 Владелец приложения Служба организаций Служба лицензий 5. Защищенные службы вызывают OAuth2 для проверки токена доступа Рис. B.1. Служба OAuth2 определяет, является ли пользователь, обращающийся к службе, аутентифицированным пользователем Приложение B 455 На рис. B.1 изображена следующая последовательность действий (номера пунктов соответствуют цифрам на рисунке). 1 Прежде чем приложение O-stock сможет использовать защищенный ресурс, оно должно быть однозначно идентифицировано в службе OAuth2. Обычно владелец приложения регистрирует его в службе OAuth2 и задает уникальное имя для своего приложения. Служба OAuth2 в свою очередь предоставляет секретный ключ для зарегистрированного приложения. Имя приложения и секретный ключ однозначно идентифицируют приложение, пытающееся получить доступ к любым защищенным ресурсам. 2 Пользователь входит в O-stock и вводит свои учетные данные. O-stock передает учетные данные пользователя и имя приложения с секретным ключом непосредственно в службу OAuth2. 3 Служба OAuth2 проверяет подлинность приложения и пользователя и возвращает токен доступа OAuth2. 4 Каждый раз, когда приложение O-stock вызывает службу от имени пользователя, оно передает токен доступа, полученный от сервера OAuth2. 5 Когда вызывается защищенная служба (в данном случае служба лицензий или организаций), она обращается к службе OAuth2 для проверки подлинности токена. Если токен подлинный, то вызванная служба обрабатывает запрос. Если токен недействительный, то служба OAuth2 возвращает код состояния HTTP 403, указывая, что токен недействителен. Тип разрешения: учетные данные клиента Тип разрешения «учетные данные клиента» обычно используется, когда приложению требуется получить доступ к ресурсу, защищенному OAuth2, но при этом нет человека, участвующего в транзакции. При использовании этого типа разрешения сервер OAuth2 выполняет аутентификацию только по имени приложения и секретному ключу, предоставленному владельцем ресурса. Этот тип разрешения обычно применяется, когда одна и та же компания владеет обоими приложениями. Разница между типами разрешений «пароль» и «учетные данные клиента» заключается в том, что во втором случае аутентификация производится только по зарегистрированному имени приложения и секретному ключу. Например, предположим, что в приложении O-stock имеется задача, выполняющая анализ данных раз в час. В рамках своей работы она обращается к службам O-stock. Однако разработчики O-stock хотят, чтобы приложение аутентифицировалось и авторизовалось, прежде чем получить доступ к данным в этих службах. Приложение B 456 В таком случае можно использовать тип разрешения «учетные данные клиента». Этот процесс показан на рис. B.2. На рис. B.2 изображена следующая последовательность действий (номера пунктов соответствуют цифрам на рисунке). 1 Владелец ресурса регистрирует приложение анализа данных O-stock в службе OAuth2. Он задает имя приложения и получает обратно секретный ключ. 2 Когда запускается задача анализа данных, она передает имя приложения и секретный ключ, предоставленные владельцем ресурса. 3 Служба OAuth2 аутентифицирует приложение, используя переданные ей имя приложения и секретный ключ, и возвращает токен доступа OAuth2. 4 Каждый раз, когда приложение вызывает одну из служб O-stock, оно передает в вызов токен доступа OAuth2. 2. На запуске задача анализа данных передает имя приложения и ключ в OAuth2 Пользователь 3. OAuth2 аутентифицирует приложение и предоставляет токен доступа Приложение O-stock 4. O-stock добавляет токен доступа во все вызовы служб 1. Владелец приложения регистрирует приложение в службе OAuth2, которая предоставляет секретный ключ Служба OAuth2 Владелец приложения Служба организаций Служба лицензий Рис. B.2. Тип разрешения «учетные данные клиента» предназначен для аутентификации и авторизации приложения, выполняющегося без участия человека Тип разрешения: код авторизации Тип разрешения «код авторизации» – безусловно, самый сложный из всех типов, поддерживаемых OAuth2, но он также является наиболее распространенным, потому что позволяет разным приложениям от разных производителей обмениваться данными и услуга- Приложение B 457 ми без передачи учетных данных пользователя. Он также обеспечивает дополнительный уровень проверки, передавая вызывающему приложению не токен доступа OAuth2, а «предварительный» код авторизации. Рассмотрим этот тип разрешения на следующем примере: допустим, у нас есть пользователь O-stock, который также использует приложение Salesforce.com, и этому приложению нужны данные из O-stock (службы организаций). Взгляните на рис. B.3, где изображено использование типа разрешения «код авторизации», чтобы позволить Salesforce получить доступ к данным из службы организаций, при этом пользователю O-stock не нужно передавать в Salesforce свои учетные данные, действующие в O-stock. 2. Пользователь настраивает приложение Salesforce с именем, ключом и URL для входа через страницу регистрации O-stock OAuth2 3. Потенциальные пользователи приложения Salesforce теперь направляются на страницу входа в O-stock; прошедшие аутентификацию пользователи возвращаются на Salesforce.com через URL обратного вызова (с кодом авторизации) Форма входа в приложение O-stock Пользователь Salesforce.com 1. Пользователь O-stock регистрирует приложение Salesforce с помощью OAuth2 и получает секретный ключ и URL для возврата пользователей из формы входа в O-stock обратно на Salesforce.com Служба OAuth2 Владелец приложения Служба организаций 4. Приложение Salesforce передает код авторизации вместе с секретным ключом в OAuth2 и получает токен доступа 5. Приложение Salesforce добавляет токен доступа во все вызовы служб 6. Защищенные службы вызывают OAuth2 для проверки токена доступа Рис. B.3. Тип разрешения «код авторизации» позволяет приложениям обмениваться данными без передачи учетных данных пользователя На рис. B.3 изображена следующая последовательность действий (номера пунктов соответствуют цифрам на рисунке). Приложение B 458 1 2 Пользователь входит в приложение O-stock и генерирует имя приложения и секретный ключ для приложения Salesforce. В процессе регистрации он также предоставляет URL для возврата в приложение Salesforce. Этот URL возврата в Salesforce вызывается после того, как сервер OAuth2 аутентифицирует учетные данные O-stock. Пользователь настраивает свое приложение Salesforce со следующей информацией: a имя приложения, созданное для Salesforce; b секретный ключ, сгенерированный для Salesforce; c URL страницы входа в O-stock OAuth2. Теперь, когда пользователь попытается использовать приложение Salesforce и получить доступ к данным O-stock через службу организаций, он будет перенаправлен страницу входа в O-stock через URL, описанный в шаге 2. Пользователь передает свои учетные данные приложению O-stock. Если эти учетные данные действительны, сервер OAuth2 O-stock сгенерирует код авторизации и перенаправит пользователя в Salesforce через URL-адрес, указанный на шаге 1. Сервер OAuth2 передает код авторизации в параметре запроса к URL обратного вызова. Приложение Salesforce сохраняет код авторизации. Обратите внимание, что код авторизации не является токеном доступа OAuth2. 4 Затем приложение Salesforce передает секретный ключ, сгенерированный в процессе регистрации, и код авторизации обратно на сервер OAuth2 O-stock. Сервер OAuth2 O-stock проверяет код авторизации, а затем возвращает токен OAuth2, который приложение Salesforce использует каждый раз, когда ему требуется аутентифицировать пользователя и получить токен доступа OAuth2. 5 Приложение Salesforce вызывает службу организаций из приложения O-stock, передавая токен OAuth2 в заголовке. 6 Служба проверяет этот токен и, если он действителен, то обрабатывает запрос пользователя. Фу-у-х! Пожалуй, самое время пойти подышать свежим воздухом. Интеграция приложений в приложения – весьма запутанная тема. Ключевой вывод из описания всего этого процесса заключается в следующем: даже притом, что пользователь использует приложение Salesforce для получения данных из O-stock, его учетные данные в O-stock нигде не передаются напрямую приложению Salesforce. После того как службой OAuth2 был сгенерирован предварительный код авторизации, пользователь больше не должен отправлять свои учетные данные службе O-stock. 3 Приложение B 459 Тип разрешения: неявный Неявный тип разрешения используется, когда веб-приложение запускается в традиционном серверном окружении вебпрограммирования, таком как Java или .NET. Как быть, если клиентское приложение – это мобильное приложение или приложение, написанное на чистом JavaScript, выполняющееся в веб-браузере и не использующее серверных вызовов для доступа к сторонним службам? В таких случаях в игру вступает неявный тип разрешения. На рис. B.4 в общих чертах показано использование неявного типа разрешения. Неявный тип разрешения обычно используется в приложениях на чистом JavaScript, которые выполняются внутри браузера. При использовании разрешений других типов клиент взаимодействует с сервером приложений, который получает запрос пользователя и взаимодействует с любыми нижестоящими службами. При неявном типе разрешения все взаимодействия со службами происходят непосредственно из клиента пользователя (обычно веб-браузера). 2. Пользователи приложения вынуждены аутентифицироваться в службе OAuth2 4. Приложение на JavaScript извлекает и сохраняет токен доступа 3. OAuth2 перенаправляет аутентифицированных пользователей на URL обратного вызова (с токеном доступа в параметре запроса) 1. Владелец приложения на JavaScript регистрирует имя приложения и URL обратного вызова http://javascript/app/callbackuri?token=gt325sdfs Пользователь Мобильное приложение или приложение на JavaScript 5. Приложение на JavaScript передает токен доступа во все вызовы служб Служба OAuth2 Служба организаций Служба лицензий Владелец приложения на JavaScript 6. Защищенные службы вызывают OAuth2 для проверки токена доступа Рис. B.4. Неявный тип разрешения используется в одностраничных приложениях (Single Page Application, SPA) на JavaScript, выполняющихся в браузере На рис. B.4 изображена следующая последовательность действий (номера пунктов соответствуют цифрам на рисунке). 1 Владелец приложения на JavaScript регистрирует его на сервере OAuth2 O-stock, предоставляя имя приложения и URL обратного вызова, на который будет выполнено перенаправление с токеном доступа OAuth2 для пользователя. 2 Приложение на JavaScript вызывает службу OAuth2, передавая предварительно зарегистрированное имя приложения. Затем 460 Приложение B сервер OAuth2 заставляет пользователя пройти аутентификацию. 3 В случае успешной аутентификации служба OAuth2 O-stock не возвращает токен, а перенаправляет пользователя обратно на страницу, зарегистрированную владельцем приложения на шаге 1. В параметре запроса в URL обратного вызова передается токен доступа OAuth2. 4 Приложение принимает входящий запрос и запускает сценарий JavaScript, который извлекает и сохраняет токен доступа OAuth2 (обычно в виде файла cookie). 5 При каждом обращении к защищенному ресурсу ему передается токен доступа OAuth2. 6 Вызываемая служба проверяет токен OAuth2 и наличие разрешений у пользователя для выполнения запрошенных действий. Используя неявный тип разрешения, важно помнить о некоторых его аспектах. Во-первых, неявное разрешение – это единственный тип разрешений, когда токен доступа OAuth2 напрямую передается общедоступному клиенту (веб-браузеру). При использовании типа разрешения «код авторизации» клиентское приложение получает код авторизации, возвращаемый сервером приложений, на котором размещено это приложение, и пользователю предоставляется доступ путем передачи кода авторизации. Полученный токен OAuth2 никогда не передается напрямую браузеру пользователя. При использовании типа разрешения «учетные данные клиента» токен передается между двумя серверными приложениями, а при использовании типа разрешения «пароль» все программные компоненты – приложение, обращающееся к службам, и сами службы являются доверенными и принадлежат одной и той же организации. Токены OAuth2, сгенерированные при использовании неявного типа разрешения, более уязвимы для атак и неправильного использования, потому что доступны браузеру. Любой вредоносный код на JavaScript, запущенный в браузере, может получить доступ к токену доступа и вызвать соответствующие службы от вашего имени, фактически выдавая себя за вас. Следовательно, токены при использовании с неявным типом разрешения должны быть недолговечными (1-2 ч). Так как токен доступа OAuth2 хранится в браузере, спецификация OAuth2 (и Spring Cloud Security) не поддерживает понятие токена обновления, когда токен может обновляться автоматически. Приложение B 461 Как обновляются токены Когда OAuth2 выдает токен доступа, он остается действительным в течение ограниченного времени. Когда срок действия токена истекает, вызывающему приложению (и пользователю) необходимо повторно пройти аутентификацию в службе OAuth2. Однако в большинстве случаев сервер OAuth2 выдает два токена: токен доступа и токен обновления. Клиент может передать токен обновления службе аутентификации OAuth2, и та, проверив его, выдаст новый токен доступа. Давайте рассмотрим процесс обновления токена, изображенный на рис. B.5. На рис. B.5 изображена следующая последовательность действий (номера пунктов соответствуют цифрам на рисунке). 1 Пользователь входит в O-stock, уже пройдя аутентификацию в службе OAuth2, работает какое-то время и, к сожалению, в какой-то момент срок действия токена истекает. 2 После этого, когда пользователь попытается вызвать службу (скажем, службу организаций), приложение O-stock отправит службе организаций просроченный токен. 3 Служба организаций проверит токен с помощью службы OAuth2, которая вернет код состояния HTTP 401 (unauthorized – неавторизован) и текст в формате JSON, сообщающий, что токен больше не действителен. Служба организаций вернет код состояния HTTP 401 вызывающему приложению. 4 Приложение O-stock, получив от службы организаций код состояния HTTP 401 и текст JSON, вызовет службу аутентификации OAuth2 с токеном обновления. Служба аутентификации OAuth2 проверит токен обновления и вернет новый токен доступа. Приложение B 462 1. Пользователь уже вошел в приложение, когда срок действия его токена доступа истек Пользователь 4. Приложение вызывает OAuth2 с токеном обновления и получает новый токен доступа Приложение O-stock 2. Приложение добавляет просроченный токен в следующий вызов службы организаций X Служба OAuth2 X Служба организаций 3. Служба организаций вызывает OAuth2, получает ответ о том, что токен больше не действителен, и передает ответ обратно приложению Рис. B.5 . Процедура обновления токена позволяет приложению получить новый токен доступа без повторной аутентификации пользователя Приложение C Мониторинг микросервисов Микросервисные архитектуры состоят из множества небольших распределенных служб, и по этой причине они привносят дополнительные сложности в наши приложения, которых нет в монолитных приложениях. Для внедрения микросервисной архитектуры необходим высокий уровень операционной зрелости, и мониторинг становится важной частью их администрирования. И многие, если не большинство, согласны с тем, что это действительно фундаментальный процесс, но что такое мониторинг на самом деле? Мы определяем мониторинг, как процесс анализа, сбора и хранения данных, таких как метрики приложений, платформ и системных событий, которые помогают визуализировать шаблоны отказов в вычислительном окружении. Только отказы? Важно отметить, что отказ – одна из наиболее очевидных причин, по которой мониторинг имеет фундаментальное значение, но не единственная. Еще одной причиной, играющей важную роль в наших приложениях считается производительность микросервисов. Производительность нельзя описать как двоичную характеристику, которая либо «есть», либо «нет». Комплекс служб может работать в определенном состоянии деградации, влияющем на производительность одной или нескольких служб. В следующих разделах мы покажем, как осуществлять мониторинг микросервисов Spring Boot с помощью нескольких технологий, таких как Spring Boot Actuator, Micrometer, Prometheus и Grafana. Итак, начнем. 464 Приложение C C.1.Введение в мониторинг с использованием Spring Boot Actuator Spring Boot Actuator – это библиотека инструментов мониторинга и администрирования для REST API. Идея мониторинга с использованием этой библиотеки основана на организации ряда конечных точек REST для доступа к различной информации о состоянии служб и ее проверки. Иначе говоря, Spring Boot Actuator предоставляет готовые конечные точки, которые могут помочь раскрыть состояние службы и управлять им. Чтобы задействовать Spring Actuator, нужно выполнить два простых шага. Первый – добавить зависимости в файл pom.xml, а второй – включить конечные точки, которые будут использоваться в нашем приложении. Давайте рассмотрим подробнее каждый из этих шагов. C.1.1. Добавление зависимостей Spring Boot Actuator Чтобы добавить поддержку Spring Boot Actuator в микросервис, нужно включить следующую зависимость в его файл pom.xml: <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> C.1.2. Включение конечных точек Spring Boot Actuator Настройка Spring Boot Actuator – несложная задача. Просто добавив зависимость в конфигурацию микросервиса, мы автоматически получаем ряд конечных точек, доступных для использования. Каждую конечную точку можно включить или выключить и открыть доступ к ней через HTTP или JMX. На рис. C.1 показаны все конечные точки Actuator, включенные по умолчанию. Чтобы включить конкретную конечную точку, нужно лишь определить примерно такое свойство: management.endpoint.<id>.enabled= true или false Например, чтобы выключить конечную точку beans, нужно добавить следующее свойство: management.endpoint.beans.enabled = false В этой книге мы по умолчанию включаем все конечные точки Actuator для всех микросервисов, но вы можете поступить иначе, как будет лучше для вас. Помните, что вы всегда можете добавить дополнительную защиту конечных точек HTTP с помощью Spring Security. Вот пример настройки Spring Boot Actuator для наших служб лицензий и организаций: Приложение C 465 management.endpoints.web.exposure.include=* management.endpoints.enabled-by-default=true Согласно этой конфигурации, если ввести URL http://localhost:8080/actator в Postman, то в ответ мы получим список, подобный показанному на рис. C.1, со всеми активными конечными точками Spring Boot Actuator. Рис. C.1. Конечные точки Spring Boot Actuator, включенные по умолчанию C.2. Настройка Micrometer и Prometheus Spring Boot Actuator предлагает метрики, позволяющие наблюдать за состоянием приложения. Однако, для получения более точной информации необходимо использовать дополнительные инструменты, такие как Micrometer и Prometheus. 466 Приложение C C.2.1. Введение в Micrometer и Prometheus Micrometer – это библиотека, предоставляющая дополнительные метрики, характеризующие работу приложения, за счет небольшого или даже нулевого увеличения накладных расходов на деятельность по их сбору. Micrometer также позволяет экспортировать метрики в некоторые наиболее популярные системы мониторинга. Используя Micrometer, приложение абстрагирует используемую систему метрик и может изменять ее в будущем, если это потребуется. Еще одной популярной системой мониторинга является Prometheus, которая отвечает за сбор и хранение метрик, предоставляемых приложением. Она поддерживает язык запросов, с помощью которого другие приложения могут получать и визуализировать показатели в виде графиков и таблиц. Grafana – один из инструментов, который позволяет просматривать данные, предоставленные Prometheus, но Micrometer можно также использовать с другими системами мониторинга, такими как Datadog, SignalFx, Influx, New Relic, Google Stackdriver, Wavefront и др. Одно из преимуществ использования Spring Boot для разработки микросервисов состоит в том, что этот фреймворк позволяет выбрать одну или несколько систем мониторинга, предлагающих разные способы представления результатов для анализа и отображения. С помощью Micrometer можно измерять метрики, которые позволят понять, как работает наша система в целом; отдельные компоненты одного приложения или экземпляры кластеров. Вот список с некоторыми метриками, которые можно получить с помощью Micrometer: статистики, связанные со сборкой мусора; загрузка процессора; потребление памяти; использование потоков выполнения; использование источников данных; задержки запросов Spring MVC; фабрики соединений Kafka; кеширование; количество событий, зарегистрированных в Logback; время работы. Для демонстрации в этой книге мы выбрали Prometheus, потому что эта система мониторинга легко интегрируется с Micrometer Приложение C 467 и Spring Boot 2. Prometheus – это база данных временных рядов метрик в памяти, а также система мониторинга и оповещения. Когда мы говорим о хранении данных в форме временных рядов, мы имеем в виду хранение данных в хронологическом порядке и оценку значений переменных во времени. Базы данных, предназначенные для хранения временных рядов, исключительно эффективны при работе с такими данными. Основная модель работы Prometheus – активное извлечение метрик из экземпляров приложения через регулярные интервалы времени. Вот некоторые из основных характеристик Prometheus: гибкий язык запросов – настраиваемый язык запросов, позволяющий напрямую запрашивать данные; эффективное хранилище – обеспечивает эффективное хранение временных рядов в памяти и на локальном диске; многомерные модели данных – все временные ряды идентифицируются по названию метрики и содержат наборы пар ключ/значение; интеграция с множеством технологий – позволяет интегрироваться с другими технологиями, такими как Docker, JMX и т. д. C.2.2. Интеграция с Micrometer и Prometheus Чтобы экспортировать данные в Micrometer и Prometheus с помощью Spring Boot 2, нужно добавить следующие зависимости. (Обратите внимание, что Prometheus поставляется в составе пакета Micrometer.) <dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-registry-prometheus</artifactId> </dependency> <dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-core</artifactId> </dependency> В этой книге мы включили все конечные точки Actuator во всех наших микросервисах. Если вы хотите предоставить доступ только к конечной точке Prometheus, то добавьте следующее свойство в файл свойств службы лицензий и организаций: management.endpoints.web.exposure.include=prometheus Согласно этой конфигурации, если ввести http://localhost:8080/ actator в Postman, то в ответ вы должны получить список конечных точек Actuator, подобный изображенному на рис. C.2. 468 Приложение C Рис. C.2. Конечные точки Spring Boot Actuator, включая конечную точку actuator/prometheus Настроить Prometheus можно несколькими способами. В этой книге для запуска служб Prometheus мы используем контейнер Docker и готовые официальные образы. Вы можете использовать тот, который лучше всего соответствует вашим потребностям. Если вы решите использовать Docker, то не забудьте определить службу в файле Docker Compose, как показано в листинге C.1. Листинг C.1. Настройка Prometheus в файле docker-compose prometheus: image: prom/prometheus:latest ports: - "9090:9090" volumes: - ./prometheus.yml:/etc/prometheus/prometheus.yml container_name: prometheus networks: backend: aliases: - "prometheus" В контейнере Prometheus мы создали том для конфигурационного файла Prometheus, который называется prometheus.yml. Этот файл определяет конечные точки, которые Prometheus будет использовать для получения данных. Cодержимое этого файла показано листинге C.2. Приложение C 469 Листинг C.2. Настройки в файле prometheus.yml Устанавливает частоту сбора данных раз в 5 с global: Устанавливает частоту примеscrape_interval: 5s нения правил оценки раз в 5 с evaluation_interval: 5s scrape_configs: URL конечной точки actuator/ - job_name: 'licensingservice' prometheus, предоставляющей метрики в формате, котоmetrics_path: '/actuator/prometheus' рый поддерживает Prometheus static_configs: - targets: ['licensingservice:8080'] URL службы лицензий - job_name: 'organizationservice' metrics_path: '/actuator/prometheus' static_configs: - targets: ['organizationservice:8081'] URL службы организаций Файл prometheus.yml определяет настройки для службы Prometheus. Например, согласно настройкам в листинге C.2, служба будет опрашивать все конечные точки /Prometheus каждые 5 с и добавлять метрики в свою базу данных временных рядов. Теперь, закончив с конфигурацией, давайте запустим наши службы и убедимся, что сбор данных выполняется успешно. Для этого откройте страницу http://localhost:9090/target, которая должна выглядеть примерно так, как показано на рис. C.3. Рис. C.3. Цели Prometheus, настроенные в файле prometheus.yml C.3. Настройка Grafana Grafana – это инструмент с открытым исходным кодом, отображающий временные ряды. Он предлагает набор панелей для визуализации и исследования данных приложения, а также для добавления предупреждений. Grafana также позволяет создавать и отображать свои панели. Основная причина, почему мы выбрали Grafana 470 Приложение C в этой книге, заключается в том, что это один из лучших инструментов отображения информационных панелей. Он содержит невероятную графику и множество функций, гибок и, что самое главное, прост в использовании. Grafana поддерживает несколько способов настройки. В этой книге мы используем официальный образ Grafana Docker. Но вы можете выбрать любой другой подход, который лучше соответствует вашим потребностям. Если вы решите использовать Docker, то не забудьте определить службу в файле Docker Compose, как показано в листинге C.3. Листинг C.3. Настройки Grafana в файле docker-compose grafana: image: "grafana/grafana:latest" ports: - "3000:3000" container_name: grafana networks: backend: aliases: - "grafana" После сохранения настроек в файле docker-compose выполните следующую команду, чтобы запустить свои службы: docker-compose -f docker/docker-compose.yml up Она запустит Grafana на порту 3000, как указано в листинге C.3. Откройте URL http://localhost:3000/login, чтобы увидеть страницу, изображенную на рис. C.4. Рис. C.4. Страница входа в Grafana отображает имя пользователя и пароль по умолчанию (admin admin) Имя пользователя и пароль по умолчанию, отображаемые на рис. C.4, можно изменить после входа или определить их в файле docker-compose, как показано в листинге C.4. Приложение C 471 Листинг C.4. Настройка имени пользователя и пароля для Grafana grafana: image: "grafana/grafana:latest" ports: - "3000:3000" Имя пользователя environment: для администратора - GF_SECURITY_ADMIN_USER=admin - GF_SECURITY_ADMIN_PASSWORD=password Пароль администратора container_name: grafana ... остальная часть файла опущена для краткости Чтобы завершить настройку Grafana, нужно создать источник данных и задать конфигурацию панели мониторинга. Начнем с источника данных. Чтобы создать источник данных, выберите раздел Data Sources (Источники данных) на главной странице Grafana, щелкните на значке Explore (Исследование) слева и выберите Add Your First Data Source (Добавить первый источник данных), как показано на рис. C.5. Рис. C.5. Страница приветствия Grafana. На этой странице показаны ссылки для начальной настройки На странице добавления источника данных выберите Prometheus в качестве базы данных временных рядов, как показано на рис. C.6. Последний шаг – настройка URL Prometheus (http://localhost:9090 или http://prometheus:9090) для источника данных. На рис. C.7 показано, как это сделать. 472 Приложение C Рис. C.6. Выбор базы данных временных рядов для Prometheus в Grafana ПРИМЕЧАНИЕ. Мы используем localhost, когда запускаем службы локально, но если службы запускаются в Docker, то нужно использовать псевдоним службы Prometheus, который мы определили в файле docker-compose: prometheus. Вы можете увидеть это в листинге C.1. Рис. C.7 Настройка источника данных Prometheus с использованием URL локального или указанного в настройках Docker После заполнения всех полей щелкните на кнопке Save and Test (Сохранить и проверить) внизу страницы. Теперь, когда у нас есть источ- Приложение C 473 ник данных, давайте импортируем информационную панель Grafana для нашего приложения. Для этого в Grafana щелкните на значке Dashboard (Информационная панель) в меню слева, выберите пункт Manage (Управление) и щелкните на кнопке Import (Импортировать). На странице импорта вы увидите следующие варианты: Upload a JSON File (Выгрузить файл JSON); Import Via grafana.com (Импортировать через grafana.com); Import Via Panel JSON (Импортировать через панель JSON). В этом примере мы выберем Import Via grafana.com (Импортировать через grafana.com) и импортируем следующую панель: https:// grafana.com/grafana/dashboards/11378/. Эта панель отображает статистику Spring Boot 2.1 из Micrometer-Prometheus. Чтобы импортировать панель, скопируйте URL или идентификатор в буфер обмена, вставьте его в поле Import Via grafana.com (Импортировать через grafana.com) и щелкните на кнопке Load (Загрузить). После этого откроется страница конфигурации панели, где вы сможете переименовать, переместить в папку или выбрать источник данных Prometheus. Для продолжения выберите источник данных Prometheus и щелкните на кнопке Import (Импортировать). Если конфигурация была выполнена успешно, то вы должны увидеть страницу панели со всеми метриками из Micrometer. На рис. C.8 показана панель мониторинга системы Spring Boot 2.1. Рис. C.8. Панель Grafana для мониторинга системы Spring Boot 2.1 474 Приложение C Желающим узнать больше о Grafana и Prometheus мы рекомендуем заглянуть в официальную документацию, доступную по этим ссылкам: https://grafana.com/docs/grafana/latest/; https://prometheus.io/docs/introduction/overview/. C.4. Итоги обсуждения Микросервисная архитектура нуждается в мониторинге по тем же причинам, что и распределенные системы любого другого типа. Чем сложнее архитектура, тем сложнее понять ее особенности ее работы и устранять возникающие проблемы. Когда мы говорим о мониторинге приложения, то часто подразумеваем выявление сбоев, и – да, сбой – одна из наиболее распространенных причин, по которым необходим адекватный мониторинг. Но это не единственная причина. Производительность – еще один отличный повод для организации мониторинга. Как мы упоминали в первых главах, службы не только работают или не работают; они также могут работать с ухудшенной производительностью, нанося ущерб нашим лучшим намерениям. С помощью надежной системы мониторинга, подобной той, которую мы объяснили в этом приложении, вы сможете предотвратить сбои и снижение производительности и даже визуализировать возможные ошибки в вашей архитектуре. Важно отметить, что этот код мониторинга можно добавить на любом этапе разработки приложения. Единственные требования для выполнения кода примеров из этой книги – это Docker и приложение Spring Boot. Предметный указатель горячий резерв (hot standby) 168 интеграционное тестирование 408 кеширование и обмен сообщениями в облаке 357 модульное тестирование 408 одностраничные интернет-приложения (Single Page Internet Applications, SPIA) 103 платформенное тестирование 408 управление ключами шифрования 188 @Autowired аннотация 179, 218, 290 @Bean аннотация 89 @Bulkhead аннотация 232, 247, 256 CaaS (Container as a Service) 44 @CircuitBreaker аннотация 232, 238, 242, 245, 256 @Column аннотация 176 @Component аннотация 89 @ConfigurationProperties аннотация 179, 180 @Configuration аннотация 89 @Controller аннотация 102 /decrypt конечная точка 189 @DeleteMapping аннотация 108, 109 @EnableBinding аннотация 345, 352 @EnableDiscoveryClient аннотация 215, 216, 218 @EnableEurekaClient аннотация 70, 267 @EnableEurekaServer аннотация 206 @EnableFeignClients аннотация 215, 220 /encrypt конечная точка 189 @Entity аннотация 176 @FeignClient аннотация 221 @GetMapping аннотация 106 @Id аннотация 176 @Input аннотация 364 @LoadBalanced аннотация 218 /openid-connect/token конечная точка 318 @OutputChannel аннотация 364 @PathVariable аннотация 41, 107, 108 @PathVariable анотация 221 @PostMapping аннотация 108 @PutMapping аннотация 108 @RateLimiter аннотация 256 @RefreshScope аннотация 180 /refresh конечная точка 180 @Repository аннотация 89, 177 @RequestBody аннотация 108 @RequestHeader аннотация 115 @RequestMapping аннотация 103, 106, 221 @RequestParam аннотация 41 @ResponseBody аннотация 102 @RestController аннотация 102 @Retry аннотация 256 @RolesAllowed аннотация 319 ServerWebExchange.Builder.mutate() метод 284 @Service аннотация 89 @SpringBootApplication аннотация 89 @StreamListener аннотация 353 @Table аннотация 176 A абстрагирование 152 авторизация 55, 299 агрегирование журналов 57, 290, 372 476 Предметный указатель архитектора, роль 92 декомпозиция бизнес-задачи 92 детализация служб 95 определение интерфейсов служб 98 архитектура микросервисов 30 архитектура управления конфигурацией 152 аспектно-ориентированное программирование (АОП) 234 аутентификация 55, 299 Б база кода 74 базовый шаблон разработки микросервисов 50 балансировка нагрузки в обнаруженим служб 196 балансировка нагрузки на стороне клиента 198, 226 балансировки нагрузки на стороне клиента, шаблон 54 безопасности, шаблоны 55 безопасность Keycloack анализ нестандартного поля в JWT 327 передача токена доступа 321 Keycloak 297 защита единственной конечной точки 299 защита службы организаций 314 настройка связи службы с сервером 315 определение пользователей, кому разрешено обращаться к службе 315 OAuth2 295 используйте шлюз для организации доступа к службам 329 наименьших привилегий, принцип 330 ограничьте поверхность атаки, заблокировав ненужные сетевые порты 330 разделите службы на общедоступные и закрытые 330 варианты реализации 154 веб-токены JSON (JSON Web Tokens, JWT) 296 взаимодействие клиентов с микросервисами 123 виртуальные машины (ВМ) 73 виртуальный контейнер 47 владелец ресурса 298 внешняя конфигурация 448 вспомогательные службы 76 встраивание дверного проема в микросервис 101 герметичные отсеки, шаблон обзор 227 реализация 244 герметичных отсеков (Bulkhead), шаблон 54 гибкий язык запросов 467 гипертекст как механизм передачи состояния приложения (Hypertext as the Engine of Application State, HATEOAS) 448 глобально универсальный идентификатор (Globally Universal ID, GUID) 281 гранты 296 декомпозиция 30 декомпозиция бизнес-задачи 92 демон Docker 133 детализация служб 95 динамическая маршрутизация 262 дрейф конфигурации 60 единая точка применения политик (Policy Enforcement Point, PEP) 260 журналирование 80, 451 журналирования и трассировки, шаблоны 56 зависимости 75 настройка зависимостей Spring Cloud Sleuth и Zipkin 391 настройка зависимостей в службе лицензий 170 задачи администрирования 81 заключительные фильтры 278 закон Конвея 34 запуск служб с помощью Docker Compose 147 защита конфиденциальных настроек в конфигурации 187 настройка симметричного шифрования 187 шифрование и дешифрование настроек 188 защищенный ресурс 297 идентификатор корреляции 280 добавление в HTTP-ответ с помощью Spring Cloud Gateway 388 добавление в ответ в заключительном фильтре 291 и Spring Cloud Sleuth 369 агрегирование журналов 372 Предметный указатель добавление в службы лицензий и организаций 370 особенности трассировки 370 использование в службах 284 RestTemplate и UserContextInterceptor 289 UserContext 287 UserContextFilter 286 передача нижестоящим службам 289 изначально облачные приложения 71 изоляция с использованием пула потоков 244 изоляция с использованием семафоров 244 инициализация службы 122 интеграция с множеством технологий 467 интернационализация, добавление в службу лицензий 112 интерфейсов служб, определение 98 инфраструктура как код 409 инфраструктура как услуга (Infrastructure as a Service, IaaS) 44 инфраструктурный уровень 295 класс инициализации Spring Boot 88 Spring Cloud Configuration Server 161 классы приложений 39 клиент Docker 133 конвейер сборки/развертывания архитектура 406 в действии 430 настройка GitHub 433 сборка служб в Jenkins 434 создание 432 создание сценариев для конвейера развертывания Kubernetes 441 создание сценария конвейера 439 контейнер как услуга (Container as a Service, CaaS) 44 контейнеры 73 беспорядочный рост числа 99 и виртуальные машины 130 контейнеры Docker 132, 134 контроллер Spring Boot 101 конфигурации управление 150 конфигурации дрейф 60 конфигурация 76 корневой токен 183 корреляция журналов 57 локаль 113 477 маршрутизации, шаблоны 52 маршрутизация служб 293 маршрутов, настройка автоматически с обнаружением служб 268 вручную с обнаружением служб 271 динамическая загрузка настроек 273 масштабируемость 79 метрики 58 микросервисная архитектура 30 микросервисы DevOps, инженера роль 118 инициализация службы 122 мониторинг состояния микросервиса 124 регистрация и обнаружение службы 123 сборка службы 120 архитектора, роль декомпозиция бизнес-задачи 92 детализация служб 95 определение интерфейсов служб 98 базовый шаблон разработки 50 безопасность Keycloak 297 используйте шлюз для организации доступа к службам 329 наименьших привилегий, принцип 330 ограничьте поверхность атаки, заблокировав ненужные сетевые порты 330 разделите службы на общедоступные и закрытые 330 интеграция с Docker 138 создание образов Docker со Spring Boot 144 когда не следует использовать 99 беспорядочный рост числа контейнеров 99 сложность распределенных систем 99 тип приложения 100 транзакции и согласованность данных 100 мониторинг 463 Grafana 470 Micrometer и Prometheus 465 Spring Boot Actuator 464 облачные вычисления 39 определение 44 преимущества для развертывания 478 Предметный указатель микросервисов 46 создание с помощью Spring Boot 39 определение 30, 72 передовые практики 445 CI/DI 449 HATEOAS 448 внешняя конфигурация 448 журналирование 451 модель зрелости Ричардсона 446 мониторинг 450 передовые практики,приложение двенадцати факторов база кода 74 вспомогательные службы 76 журналирование 80 зависимости 75 задачи администрирования 81 конфигурация 76 масштабируемость 79 одноразовость 79 привязка портов 78 процессы 78 сборка, выпуск, выполнение 77 сходство окружений разработки/ эксплуатации 80 развертывание настройка базовой инфраструктуры 411 разработчика, роль 100 встраивание дверного проема в микросервис 101 интернационализация, добавление в службу лицензий 112 реализация Spring HATEOAS для отображения связанных ссылок 115 со Spring 34 создание с использованием Spring Boot и Java 82 запуск приложения 88 каркас проекта 83 подготовка окружения 83 шаблоны балансировка нагрузки на стороне клиента 54 безопасности 55 герметичные отсеки (Bulkhead) 54 журналирования и трассировки 56 откат к резервной реализации (fallback) 54 размыкатель цепи (Circuit Breaker) 54 сбора метрик приложения 58 сборки/развертывания микросервисов 59 шаблоны маршрутизации 52 шаблоны устойчивости клиентов 54 многослойные файлы JAR 145 модель зрелости Ричардсона 446 мониторинг 450, 463 Grafana 470 настройка 470 Micrometer и Prometheus 465 интеграция 467 Spring Boot Actuator 464 включение конечных точек 464 добавление 464 мониторинг состояния микросервиса 124 монолитная архитектура 29 надежность 152 наименьших привилегий, принцип 330 настройка маршрутов 268 автоматически с обнаружением служб 268 вручную с обнаружением служб 271 динамическая загрузка настроек 273 настройка службы лицензий для взаимодействий с Spring Cloud Config 170 неизменяемый сервер 409 непрерывная доставка 73, 449 непрерывная интеграция 449 непрерывная интеграция/непрерывная доставка (CI/CD) 409 обзор книги 37 актуальность примеров 81 облачные вычисления 39 настройка базовой инфраструктуры 411 обнаружение служб 195 архитектура 196 использование 214 поиск экземпляров с Spring Discovery Client 216 поиск экземпляров с использованием Netflix Feign 220 регистрация служб в Eureka 207 REST API 211 панель управления 212 с использованием Spring и Netflix Eureka 200 создание службы Eureka 202 определение 44 передовые практики создания Предметный указатель облачных микросервисов 73 база кода 74 вспомогательные службы 76 журналирование 80 зависимости 75 задачи администрирования 81 конфигурация 76 масштабируемость 79 одноразовость 79 привязка портов 78 процессы 78 сборка, выпуск, выполнение 77 сходство окружений разработки/ эксплуатации 80 преимущества для развертывания микросервисов 46 развертывание стека ELK в экземпляре EC2 422 создание базы данных PostgreSQL с использованием Amazon RDS 413 создание кластера EKS 423 создание кластера Redis 416 создание экземпляра EC2 с помощью ELK 418 облачные приложения 39, 71 обмен сообщениями 337 видимость сообщений 339 гибкость 339 масштабируемость 338 надежность 338 недостатки 339 слабая связанность 338 обнаружение служб архитектура 196 в облаке 195 использование 214 обзор 192 поиск экземпляров с Spring Discovery Client 216 поиск экземпляров с использованием Netflix Feign 220 регистрация служб в Eureka 207 REST API 211 панель управления 212 с использованием Spring и Netflix Eureka 200 создание службы Eureka 202 образы Docker 134 создание 138 Dockerfile для многоступенчатой сборки 140 базовый файл Dockerfile 140 479 создание образов Docker со Spring Boot 144 образы виртуальных машин 47 ограничитель частоты, шаблон реализация 249 однократный вход (Single Sign-On, SSO) 297 одноразовость 79 одноранговая сеть обнаружения служб 196 отделение, принцип 152 отказоустойчивость обнаружения служб 196 отката к резервной реализации (fallback), шаблон 54 пакет визуализации метрик 58 параметры метода 107 передача событий с использованием синхронного подхода запросответ 334 негибкость добавления новых получателей изменений в службе организаций 336 тесная связь между службами 335 хрупкость служб 336 передовые практики микросервисной архитектуры 445 CI/DI 449 HATEOAS 448 внешняя конфигурация 448 журналирование 451 модель зрелости Ричардсона 446 мониторинг 450 передовые практики,приложение двенадцати факторов база кода 74 вспомогательные службы 76 журналирование 80 зависимости 75 задачи администрирования 81 конфигурация 76 масштабируемость 79 одноразовость 79 привязка портов 78 процессы 78 сборка, выпуск, выполнение 77 сходство окружений разработки/ эксплуатации 80 переменные пути 39 переносимость 129 платформа как услуга (Platform as a Service, PaaS) 45 повторные попытки, шаблон реализация 248 480 Предметный указатель поезд выпусков 159 поставщик удостоверений 295 предварительные фильтры 278 создание 281 предикаты встроенные фабрики предикатов 275 фабрики предикатов 274 привязка портов 78 пригодные для использования в облаке приложения 71 прикладной уровень 295 приложение двенадцати факторов 73 приложения классы 39 программное обеспечение как услуга (Software as a Service, SaaS) 45 профили 171 процессы 78 развертывание микросервисов Amazon создание базы данных PostgreSQL с использованием Amazon RDS 413 в кластере EKS 427 конвейер сборки/развертывания архитектура 406 в действии 430 настройка GitHub 433 сборка служб в Jenkins 434 создание 432 создание сценариев для конвейера развертывания Kubernetes 441 создание сценария конвейера 439 настройка базовой инфраструктуры 411 развертывание стека ELK в экземпляре EC2 422 создание кластера EKS 423 создание кластера Redis 416 создание экземпляра EC2 с помощью ELK 418 развязка 30 размыкатель цепи, шаблон добавление в службу лицензий 240 настройка 240 обзор 226 реализация 234 размыкателя цепи (Circuit Breaker), шаблон 54 разработчика, роль 100 интернационализация, добавление в службу лицензий 112 реализация Spring HATEOAS для отображения связанных ссылок 115 распределенная трассировка Spring Cloud Sleuth агрегирование журналов 372 и идентификатор корреляции 370 особенности трассировки 370 Zipkin 390 визуализация сложных транзакций 397 добавление дополнительных операций для трассировки 400 настройка зависимостей 391 настройка сервера 391, 392 настройка служб 391 настройка уровней трассировки 393 трассировка операций обмена сообщениями 398 трассировка транзакция 394 распределенное кеширование 356 реализация Spring HATEOAS для отображения связанных ссылок 115 регистрация и обнаружение службы 123 реестр Docker 134 резервная реализация, шаблон обзор 227 реализация 242 репозиторий контейнеров 424 репозиторий универсального описания, обнаружения и интеграции (Universal Description, Discovery, and Integration UDDI) 191 Ричардсона, модель зрелости 446 сбора метрик приложения, шаблоны 58 сборка, выпуск, выполнение 77 сборка службы и развертывание 120 сборки/развертывания микросервисов, шаблоны 59 связующий слой 342 сервер аутентификации/ авторизации 298 сервисный шлюз 260 сетевой уровень 295 сети Docker 134 симметричное шифрование, настройка 187 сквозные задачи 274 служба метрик 58 событийно-ориентированная архитектура 333 Spring Cloud Stream 340 использование Redis в роли кеша 357 использование Redis из службы лицензий для сохранения и извлечения данных 361 Предметный указатель определение репозиториев Spring Data для доступа к Redis 360 определение собственных каналов 364 получение сообщений в службе лицензий 351 распределенное кеширование 356 обмен сообщениями 333, 337 видимость сообщений 339 гибкость 339 масштабируемость 338 надежность 338 недостатки 339 слабая связанность 338 передача событий с использованием синхронного подхода запросответ 334 негибкость добавления новых получателей изменений в службе организаций 336 тесная связь между службами 335 хрупкость служб 336 публикация сообщений в службе организаций 344 согласованность данных 100 создание образов Docker со Spring Boot 144 статическая маршрутизация 261 сходство окружений 80, 90 сходство окружений разработки/ эксплуатации 80 типы грантов (разрешений) код авторизации 457 неявный 459 обновление токенов 461 пароль 454 учетные данные клиента 455 токен доступа передача 321 токены доступа обновление 461 тома Docker 134 транзакции данных 100 трассировка микросервисов 57 трассировка операций обмена сообщениями 398 трассировка транзакций 394 управление конфигурацией микросервисов 122 управление конфигурациями 150 управление учетными данными и их распространение 55 481 устойчивости клиентов, шаблоны 54 устойчивость обнаружения служб 196 фабрики предикатов встроенные 275 обзор 274 фабрики фильтров встроенные 276 обзор 274 предварительные фильтры создание 281 собственные 278 физические серверы 47 фильтр ответа 281 фильтр трассировки 280 фильтры UserContext 287 UserContextFilter 286 встроенные фабрики фильтров 276 добавление идентификатора корреляции в ответ в заключительном фильтре 291 использование идентификатора корреляции в службах 284 использование идентификатора корреляциив службах RestTemplate и UserContextInterceptor 289 предварительные фильтры создание 281 собственные фабрики фильтров 278 фабрики фильтров 274 функция как услуга (Function as a Service, FaaS) 45 целевая служба 281 централизация 152 шаблоны базовый шаблон разработки микросервисов 50 балансировка нагрузки на стороне клиента 54 безопасности 55 герметичные отсеки (Bulkhead) 54 журналирования и трассировки 56 маршрутизации 52 откат к резервной реализации (fallback) 54 размыкатель цепи (Circuit Breaker) 54 сбора метрик приложения 58 сборки/развертывания микросервисов 59 устойчивости клиентов 54 шаблоны устойчивости на стороне клиента 225 482 Предметный указатель Resilience и ThreadLocal 252 Resilience4j подготовка службы лицензий 233 реализация 232 балансировка нагрузки 226 важность 228 герметичные отсеки 227 реализация 244 ограничитель частоты реализация 249 повторные попытки реализация 248 размыкатель цепи 226, 234 добавление в службу лицензий 240 настройка 240 резервная реализация 227 реализация 242 шифрование настройка симметричного шифрования 187 шифрование и дешифрование настроек 188 шлюз 260 эволюция архитектуры микросервисов 28 эластичность 47 эффективное хранилище 467 A Accept-Language заголовок 114 access_token атрибут 312 ActionEnum 347 action элемент 348 ActiveCycle параметр 250 ActivePermissions параметр 250 actuator/gateway/refresh конечная точка 274 actuator/gateway/routes конечная точка 269, 274 add() метод 117 AlwaysSampler класс 393 Amazon развертывание стека ELK в экземпляре EC2 422 создание базы данных PostgreSQL с использованием Amazon RDS 413 создание кластера Redis 416 создание экземпляра EC2 с помощью ELK 418 Amazon EKS Container Service 432 Amazon ElastiCache 412 Amazon Web Services (AWS) 412 Apache Kafka, настройка в Docker 343 ApiGatewayServerApplication класс 267 API-шлюз 451 ApplicationContext 160 Application, класс 40 AtomicRateLimiter 250 Authorization заголовок 321, 324 AUTH_TOKEN переменная 328 AWS CLI 412 B baseUrl свойство 391 bootBuildImage задача 144 bridge тип 134 buildFallbackLicenseList() метод 243 Buildpacks 144 build команда 147 C cacheOrganizationObject() метод 363 CallNotPermittedException 236 checkRedisCache() метод 363 clientType параметр 214 ClusterIP тип службы 428 config.getProperty() метод 179 configure() метод 317 Consul 65 Container as a Service (CaaS) 44 Cookie значение 322 coreThreadPoolSize 246 correlationId элемент 348 createLicense() метод 108, 114 CRUD (Create, Read, Update, Delete создание, чтение, обновление, удаление) 176 CRUD (Create, Replace, Update, Delete создание, замена, обновление, удаление) 98 CrudRepository класс 178 CustomChannels интерфейс 364 D deleteLicense() метод 109 dependency:tree цель 88 DevOps (development разработка; operations эксплуатация) 72 DevOps, инженера роль 118 инициализация службы 122 мониторинг состояния микросервиса 124 регистрация и обнаружение службы 123 сборка службы 120 DiscoveryClient класс 217 Docker 129, 431 Docker Compose 136 Предметный указатель добавление Keycloack в 299 интеграция с микросервисами 138 создание образа Docker 138 создание образов Docker со Spring Boot 144 контейнеры и виртуальные машины 130 настройка Apache Kafka 343 настройка Redis 343 определение 132 определение и запуск приложений ELK в 380 docker commit команда 148 Docker Compose 136 docker-compose config команда 136 docker-compose up команда 136, 165 docker-compose команда 137, 165, 206, 383, 434 Dockerfile 135 базовый 140 для многоступенчатой сборки 140 dockerfile:build команда 142 Dockerfile файлы 135 docker images команда 143 docker-machine команда 434 Docker Pipeline 435 docker ps команда 148 docker run команда 144, 183 docker stop команда 144 docker version команда 142 doFilter() метод 256 E ECR 435 ECS (Elastic Container Service) 45 EKS Container Service 432 Eksctl 412 Elastic Container Registry (ECR) 424, 432 Elastic Container Service (ECS) 45 Elastic Kubernetes Service (EKS) 410 ELK (Elasticsearch, Logstash, and Kibana) стек добавление кодера Logstash 376 настройка Kibana 383 поиск идентификаторов трассировки в Kibana 386 реализация 374 ENCRYPT_KEY переменная окружения 188 ENTRYPOINT команда 141 environment раздел 393 ES_HOSTS переменная окружения 393 Eureka 66 REST API 211 483 настройка Spring Cloud Gateway для взаимодействия с 266 обнаружение служб 200 панель управления 212 регистрация служб 207 создание службы Spring Eureka 202 eureka.client.fetchRegistry атрибут 205, 209 eureka.client.registerWithEureka атрибут 205, 209 eureka.client.serviceUrl.defaultZone атрибут 205 eureka.instance.hostname атрибут 205 eureka.instance.preferIpAddress атрибут 209 eureka.instance.preferIpAddress свойство 209 EurekaServerApplication класс 206 eureka.server. waitTimeInMsWhenSyncEmpty атрибут 205 eureka.serviceUrl.defaultZone атрибут 210 Eureka сервер 202 expires_in атрибут 312 ExternalName тип службы 428 F FaaS (Function as a Service) 45 failureRateThreshold 241 fallbackMethod атрибут 242 FeignException класс 221 FilterUtils 328 FilterUtils.setCorrelationId() метод 283 FilterUtils класс 283 Filter класс 252 filter() метод 283 findById() метод 401 firstName, параметр 41 FROM команда 141 Function as a Service (FaaS) 45 G getCorrelationId() метод 283 getInstances() метод 217 getLicensesByOrganization() метод 238, 245, 256 getLicenses() метод 256 getLicense() метод 106, 117, 215 getOrganization() метод 221, 362 getOrgDbCall 401 getRestTemplate() метод 218 getStatusCode() метод 221 getUsername() метод 327 Git 65, 182 GitHub 431, 435 484 Предметный указатель GitHub Integration 435 GKE (Google Container Engine) 45 GlobalFilter интерфейс 283 Google Container Engine (GKE) 45 Grafana 450 настройка 470 grant_type параметр 312 group свойство 354 GUID (Globally Universal ID глобально универсальный идентификатор) 281 IaaS (Infrastructure as a Service) 44 IAM Authenticator 412 Iava, создание микросервисов на 82 каркас проекта 83 подготовка окружения 83 ignoreExceptions параметр 249 inboundOrgChanges канал 365 Infrastructure as a Service (IaaS) 44 input раздел 381 intervalFunction параметр 248 Keycloak защита единственной конечной точки 299 аутентификация пользователей O-stock 310 добавление Keycloack в Docker 299 настройка Keycloack 300 настройка пользователей O-stock 308 регистрация клиентского приложения 303 защита службы организаций 314 настройка 300 настройка связи службы с сервером 315 обзор 297 определение пользователей, кому разрешено обращаться к службе 315 пространства 301 KeycloakRestTemplate класс 324 KeycloakWebSecurityConfigurerAdapter класс 315 Kibana настройка 383 поиск идентификаторов трассировки 386 Kibana Query Language (KQL) 386 kompose up команда 428 Kubectl 412 kubectl get nodes -o wide команда 429 Kubernetes CLI 435 Kubernetes Continuous Deploy 435 J L H host тип 134 HTTP доступность заголовков в службах 287 используйте HTTPS/SSL для взаимодействий между службами 329 перехват входящих запросов 286 HttpSecurity класс 318 I JAR_FILE переменная 141 JAR многослойные 145 Java Persistence API (JPA) 170 Java, создание микросервисов на 100 встраивание дверного проема в микросервис 101 запуск приложения 88 интернационализация, добавление в службу лицензий 112 реализация Spring HATEOAS для отображения связанных ссылок 115 JedisConnectionFactory класс 358 Jenkins 431, 434 JSON Web Tokens (JWT) 55, 68, 296 JWT (JSON Web Tokens) 55, 296 K keepAliveDuration 246 KeepAliveDuration 247 Keycloack анализ нестандартного поля в JWT 327 передача токена доступа 321 lastName, параметр 41 LicenseController.java класс 255 LicenseController класс 103, 117, 214, 256 licenseId параметр 107 LicenseRepository интерфейс 178 LicenseRepository класс 178 LicenseServiceApplication.java класс 112 LicenseServiceApplication класс 89, 218, 289, 352 LicenseService класс 178, 179, 215, 256 license.withComment() метод 179 License класс 107, 116, 175 License, класс 106 License объект 243 licensingGroup 354 licensing-service 208 limitForPeriod параметр 250, 251 limitRefreshPeriod параметр 250, 251 linkTo метод 117 LoadBalancer тип службы 428 Locale 113 Предметный указатель LocaleResolver 112 loggerSink() метод 353 LoggingEventCompositeJsonEncoder класс 377, 378 Logstash добавление кодера 376 LogstashEncoder класс 376 logstash-logback-encoder зависимость 376 Lombok библиотека 104 M macvlan тип 134 main() метод 89, 161 manningPublications тег 381 Maven Integration 435 Maven Invoker 435 Maven/Spotify Docker плагин 431 maven команда 440 maxConcurrentCalls 246 maxRetryAttempts параметр 248 maxThreadPoolSize 246 maxWaitDuration 246 MessageBuilder класс 349 MessageChannel класс 347, 364 messageSource 113 Message класс 349 method параметр 106 Micrometer 450 интеграция 467 настройка 465 mvn package команда 142 mvn spring-boot:run команда 41, 165, 206 N NanosToWait параметр 250 Netflix Feign клиент, вызов служб 220 net.logstash.logback.encoder.LoggingEvent CompositeJsonEncoder класс 376 net.logstash.logback.encoder. LogstashEncoder класс 376 nodePort атрибут 428 NodePort тип службы 428 none тип 134 n-уровневая архитектура 28 N-уровневая архитектура 28 O OAuth2 обзор 295 типы грантов (разрешений) 453 код авторизации 457 неявный 459 обновление токенов 461 пароль 454 учетные данные клиента 455 485 OpenID Connect (OIDC) 295 OrganizationChangeHandler класс 365 OrganizationChangeModel класс 348, 353 OrganizationFeignClient класс 221 organizationId параметр 107, 221 organizationId элемент 348 OrganizationRedisRepository bynthatqc 360 OrganizationRestTemplateClient класс 325, 361 organization-service 208 OrganizationServiceApplication класс 345 OrganizationService класс 350, 401 Organization класс 221 Organization объект 363 output() метод 347 overlay тип 134 P PaaS (Platform as a Service) 45 password параметр 312 PEP (Policy Enforcement Point единая точка применения политик) 260 Platform as a Service (PaaS) 45 PostgreSQL база данных, создание с использованием Amazon RDS 413 postgresql зависимость 170 Prometheus 450 интеграция 467 настройка 465 property атрибут 180 publishOrganizationChange() метод 348 Q queueCapacity 246 R readLicensingDataFromRedis 400 recordExceptions 241 Redis использование в роли кеша 357 определение репозиториев 360 RedisTemplate класс 358 Redis, настройка в Docker 343 Redis, создание кластера с помощью Amazon 416 refresh_token атрибут 312 RemoveRequestHeader 322 RepresentationModel класс 117 Resilience4j и ThreadLocal 252 подготовка службы лицензий 233 реализация 232 resilience4j-circuitbreaker артефакт 234 resilience4j-spring-boot2 артефакт 234 486 Предметный указатель resilience4j-timelimiter артефакт 234 ResourceBundleMessageSource 112 ResponseBody класс 102 ResponseEntity класс 221 ResponseEntity объект 107 ResponseFilter 291, 389 RestTemplate класс 218 restTemplate.exchange() метод 219 RestTemplate класс 70, 215, 216, 221, 289 REST конечная точка 167 retrieveOrganizationInfo() метод 215 retry-exceptions параметр 248 retryOnExceptionPredicate параметр 248 retryOnResultPredicate параметр 248 ringBufferSizeInClosedState 241 ringBufferSizeInHalfOpenState 241 run команда 147 S SaaS (Software as a Service) 45 SAML (Security Assertion Markup Language) протокол 295 Sampler класс 393 scope атрибут 312 search-locations параметр 162 SecurityConfig.java класс 316, 317 SecurityConfig класс 316, 323 SemaphoreBasedRate 250 send() метод 349 server.port атрибут 205 ServerWebExchange.Builder класс 284 ServerWebExchangeDecorator 284 ServerWebExchange объект 283 ServiceConfig класс 180, 359 ServiceInstance класс 217 ServletFilter 286 Set-Cookie значение 322 setCorrelationId() метод 283 SimpleSourceBean класс 348 Sink.INPUT 354 Sink интерфейс 353 Software as a Service (SaaS) 45 source.output() метод 348 Source класс 345, 347, 353 spanExportable поле 378 SpanId поле 378 SPIA (Single Page Internet Applications одностраничные интернетприложения) 103 Spring Actuator 125 spring.application.name свойство 171, 204, 207, 208, 267 Spring Boot создание микросервисов 82 запуск приложения 88 каркас проекта 83 настройка окружения 83 создание микросервисов c интернационализация, добавление в службу лицензий 112 реализация Spring HATEOAS для отображения связанных ссылок 115 создание микросервисов с 39, 100 встраивание дверного проема в микросервис 101 создание образов Docker c Buildpacks 144 spring-boot-starter-aop зависимость 234 spring-boot-starter-data-jpa зависимость 170 Spring Boot клиент, интеграция с Spring Cloud Configuration Server 168 использование Git 182 использование Vault 184 настройка зависимостей в службе лицензий 170 настройка службы лицензий для взаимодействий с Spring Cloud Config 170 обновление настроек 180 подключение к источнику данных 175 прямое чтение настроек 179 Spring Cloud инструменты Spring Cloud API Gateway 67 Spring Cloud Config 65 Spring Cloud LoadBalancer и Resilience4j 66 Spring Cloud Security 68 Spring Cloud Service Discovery 66 Spring Cloud Sleuth 67 Spring Cloud Stream 67 обнаружение служб 200 использование 214 поиск экземпляров с Spring Discovery Client 216 поиск экземпляров с использованием Netflix Feign 220 регистрация служб в Eureka 207 REST API 211 панель управления 212 создание службы Eureka 202 определение 64 пример Hello World 68 Spring Cloud API Gateway 67, 451 Предметный указатель Spring Cloud Config 65 spring.cloud.config.server.git.searchPaths свойство 183 spring.cloud.config.server.git.uri свойство 183 spring.cloud.config.server.git свойство 182 Spring Cloud Configuration Server интеграция с клиентом Spring Boot 168 использование Git 182 использование Vault 184 настройка зависимостей в службе лицензий 170 настройка службы лицензий для взаимодействий с Spring Cloud Config 170 обновление настроек 180 подключение к источнику данных 175 прямое чтение настроек 179 настройка 156 класс инициализации 161 с использованием файловой системы 161 создание конфигурационных файлов 163 SPRING_CLOUD_CONFIG_URI переменная окружения 173 spring.cloud.config.uri свойство 171 Spring Cloud Gateway 293 введение 263 добавление идентификатора корреляции в HTTP-ответ 388 использование идентификатора корреляции в службах 284 настройка для взаимодействия с Eureka 266 настройка маршрутов 268 автоматически с обнаружением служб 268 вручную с обнаружением служб 271 динамическая загрузка настроек 273 настройка проекта 264 обзор 260 Spring Cloud LoadBalancer 66 Spring Cloud Security 68 Spring Cloud Service Discovery 66 Spring Cloud Sleuth 67 агрегирование журналов 372 и идентификатор корреляции 369 добавление в службы лицензий и организаций 370 особенности трассировки 370 spring-cloud-sleuth-zipkin зависимость 391 487 spring-cloud-starter-config зависимость 170 spring-cloud-starter-netflix-eureka-client артефакт 207 spring-cloud-starter-netflix-eureka-client зависимость 70 spring-cloud-starter-sleuth зависимость 388 Spring Cloud Stream 67 Apache Kafka, настройка в Docker 343 Redis, настройка в Docker 343 введение 340 использование Redis в роли кеша 357 использование Redis из службы лицензий для сохранения и извлечения данных 361 определение репозиториев Spring Data для доступа к Redis 360 определение собственных каналов 364 получение сообщений в службе лицензий 351 публикация сообщений в службе организаций 344 распределенное кеширование 356 spring.datasource.password свойство 188 Spring Eureka регистрация служб 207 создание службы 202 Spring HATEOAS 448 Spring HATEOAS (Hypermedia as the Engine of Application State гипермедиа, как средство изменения состояния приложения) 115 SPRING_PROFILES_ACTIVE переменная окружения 148, 173 spring.profiles.active свойство 171, 182 spring.sleuth.sampler.percentage свойство 393 spring.zipkin.baseUrl свойство 391 SSO (Single Sign-On однократный вход) 297 STORAGE_TYPE переменная окружения 393 SubscribableChannel класс 364 T ThreadLocal 252, 346 ThreadLocal переменные 281 timeoutDuration параметр 250, 251 tmx-correlation-id 256 tmx-correlation-id заголовок 390 tmx-correlation-ID заголовок 283 token_type атрибут 312 TraceId поле 378 488 Предметный указатель tracer.currentSpan().Context(). TraceIdString() метод 389 Tracer класс 389 TrackingFilter класс 283, 286 VAULT_DEV_LISTEN_ADDRESS параметр 183 VAULT_DEV_ROOT_TOKEN_ID параметр 183 U W UDDI (Universal Description, Discovery, and Integration репозиторий универсального описания, обнаружения и интеграции) 191 updateLicense() метод 108 URL (Uniform Resource Locator унифицированный указатель ресурса) 109 UserContextFilter класс 286 UserContextHolder класс 253 UserContextInterceptor класс 286, 289 UserContext класс 286, 346 UserContext объект 252, 253, 256 username параметр 312 waitDurationInOpenState 241 waitDuration параметр 248 V value параметр 106 Vault интеграция с Spring Cloud Configuration Server 184 X X-B3-SpanId поле 378 X-B3-TraceId поле 378 Z Zipkin 390 визуализация сложных транзакций 397 добавление дополнительных операций для трассировки 400 настройка зависимостей 391 настройка сервера 391, 392 настройка служб 391 настройка уровней трассировки 393 трассировка операций обмена сообщениями 398 трассировка транзакций 394 Книги издательства «ДМК Пресс» можно заказать в торгово-издательском холдинге «Планета Альянс» наложенным платежом, выслав открытку или письмо по почтовому адресу: 115487, г. Москва, 2-й Нагатинский пр-д, д. 6А. При оформлении заказа следует указать адрес (полностью), по которому должны быть высланы книги; фамилию, имя и отчество получателя. Желательно также указать свой телефон и электронный адрес. Эти книги вы можете заказать и в интернет-магазине: www.a-planeta.ru. Оптовые закупки: тел. (499) 782-38-89. Электронный адрес: books@alians-kniga.ru. Джон Карнелл, Иллари Уайлупо Санчес МИКРОСЕРВИСЫ SPRING В ДЕЙСТВИИ Главный редактор Мовчан Д. А. dmkpress@gmail.com Зам. главного редактора Перевод Корректор Верстка Дизайн обложки Сенченкова Е. А. Киселев А. Н. Абросимова Л. А. Луценко С. В. Мовчан А. Г. Формат 70×100 1/16. Гарнитура «NewBaskervilleC». Печать цифровая. Усл. печ. л. 39,81. Тираж 200 экз. Веб-сайт издательства: www.dmkpress.com