Переход к микросервисной архитектуре требует пересмотра подхода к разработке, тестированию, сопровождению, проектированию – иными словами, ко всем аспектам жизненного цикла программных компонентов. В этом посте мы расскажем о практиках, к которым пришла команда архитекторов Acronis на пути к лучшим API компонентов. Рассказ будет включать как постановку задачи, так и анализ ее решений. Возможно, кому-то этот пост покажется “капитанским”, кому-то будет неясно почему упустили супер-решение Х, но надеемся, что вам он будет интересен и полезен. Строителей микросервисов приглашаем под кат – почитать и оставить свои комментарии.
Если вы подписаны на наш блог, то уже читали про контракты микросервисов. О них мы говорили в постах, посвященных выбору Swagger или RAML, а также статическим проверкам, которые можно проводить на базе созданных ранее аннотаций. Поводом для сегодняшнего поста стал доклад на конференции HighLoad. Нам нужно было в целом рассказать о том пути, который мы прошли для формализации взаимоотношений между микросервисами. И сегодня хочется поделиться с Хабром нашими выводами, а также проверить, согласны ли с нами другие архитекторы.
Микросервисы – это «кирпичики», из которых разработчики создают современные приложения. Каждый такой сервис взаимодействует с внешним миром через API. Обычно микросервисы разрабатываются отдельными командами, иногда разнесенными географически, так что для эффективной работы необходимо поддерживать консистентность и целостность их публичных интерфейсов. В больших компаниях с сотнями сервисов необходимо иметь аннотацию каждого компонента: формализовать входные данные и подробно описать результаты его работы. Если вы работаете c HTTP REST, то для этого существует два распространенных формата аннотаций: RAML и Open API Specification (aka Swagger). Но вопросы, на которых мы останавливаемся сегодня, не привязаны ни к какому конкретному протоколу. Поэтому сказанное ниже будет актуально даже для gRPC.
Предыстория
Компания Acronis существует уже более 15 лет. За это время продукты и кодовая база значительно эволюционировали. От сложного desktop-приложения мы пришли к enterprise модели с централизованными консолями управления, разграничениями прав и аудит-логами. Следующим шагом стало преобразование enterprise-приложения в открытую платформу, где накопившийся опыт применялся для интеграции с внешними сервисами.
Если раньше API был важен, то теперь он стал критическим компонентом продукта. И процессы, этот API обеспечивающие, повзрослели.
Основные проблемы
Проблемы вокруг построения API знакомы, кажется, всем. Опишем их в гипертрофированном виде: как боли, которые мучают гипотетического программиста Васю и его не менее гипотетического менеджера Колю. Все имена вымышлены, а любые совпадения не случайны :)
1. Описание устарело
Пусть программист Вася разрабатывает компонент А, который использует API компонента Б. У последнего есть аннотация, но она невалидна. Васе приходится лезть в чужой код, искать людей, задавать вопросы. Сроки съезжают, а его менеджер Коля должен разбираться с переносами дедлайнов.
2. API неконсистентен
Программист Вася закончил задачу и переключился на следующую, связанную с работой компонента B. Но и у разработчиков Б, и у разработчиков В разное чувство прекрасного, так что одни и те же вещи в API сделаны по-разному. Вася опять ушел разбираться с кодом, а Коля опять страдает от срыва сроков.
3. API не документирован
Менеджер Коля решает опубликовать API компонента А, чтобы интеграторы могли делать чудесные интеграции. Интеграторы сталкиваются с проблемами, служба поддержки перегружена, у менеджера Коли все горит, а Вася чувствует, что скоро настанет его черед.
4. API несовместима со старой версией
Интеграции внедрены, все пожары потушены. Но Вася вдруг решает, что API его компонента далек от совершенства и погружается в доработку. Разумеется, при этом нарушается обратная совместимость и все интеграции разваливаются. Эта ситуация приводит к тратам со стороны интеграторов и потере денег компанией-разработчиком.
Методы лечения
Все эти проблемы возникают, когда у программистов нет представления о хорошем REST API или это представление фрагментировано. В реальности далеко не все разработчики имеют опыт работы с REST. И потому основные методы “лечения” направлены на просвещение. Когда в голове каждого разработчика начинает брезжить видение правильного API, скоординированного с видением других разработчиков, архитекторов и документаторов, API становится идеальным. Процесс формирования этого видения требует усилий и специализированных средств, про которые мы как раз сейчас будем рассказывать.
Боль 1. Аннотация не соответствует имплементации
Аннотация может отличаться от актуального состояния сервиса не только потому, что это API “темного прошлого”, до которого никак не дойдут руки. Это может быть также API светлого будущего, которое пока так и не наступило.
Причина таких состояний – отсутствие понимания, зачем нужны аннотации. Без террора со стороны архитекторов разработчики склонны считать аннотацию внутренним вспомогательным инструментом и не подразумевают, что ее будет использовать кто-то во вне.
Вылечить эту боль можно, проводя:
Архитектурное ревью. Очень полезная штука для компаний любых масштабов, в которых есть хотя бы один программист, который “знает, как правильно”. При изменении сервиса архитектор или ответственное лицо должны отслеживать состояние аннотаций и напоминать программистам, что нужно обновлять не только сервис, но и его описание. Побочные эффекты – узкое место в лице архитектора
Генерацию кода из аннотаций. Это так называемый подход API-first. Он подразумевает, что вы изначально делаете аннотацию, потом генерируете первичный код (инструментов для этого хватает, например [go-swagger] (https://github.com/go-swagger/go-swagger)), а затем заполняете сервис бизнес-логикой. Такая схема позволяет избежать несоответствий. Она хорошо работает, когда область решаемых сервисом задач четко очерчена.
Тестирование аннотации против имплементации. Для этого мы генерируем из аннотации (RAML/swagger) клиента, который бомбит сервис запросами. Если ответы будут соответствовать аннотации, а сам сервис не будет падать, значит все хорошо.
Тестирование аннотация vs имплементация
Остановимся подробнее на тестировании. Подобная полностью автоматическая генерация запросов – сложная задача. Имея данные из аннотаций API, можно создавать отдельные запросы. Однако любой API подразумевает зависимости, например, перед вызовом GET /clients/{cliend_id} нужно этот объект сначала создать, а затем получить его id. Иногда зависимости бывают менее явными – создание объекта Х требует передать идентификатор связанного объекта Y, и это не sub-collection. Ни RAML, ни Swagger не позволяют описывать такие зависимости в явном виде. Поэтому тут возможны несколько подходов:
Ожидать от разработчиков формализованных комментариев в аннотации, указывающих на зависимости.
Запросить описание ожидаемой последовательности у разработчиков (существует довольно много способов описать запросы используя YAML, специализированный DSL или через красивый GUI, как это делал ныне заброшенный apigee.
Взять реальные данные (например, используя OpenResty для логгирования всех запросов и ответов сервера)
Извлечь зависимости из аннотации с помощью (почти что) искусственного интеллекта (например, RESTler)
В любом случае задача тестирования оказывается весьма трудоемкой.
Лично мы пришли к тому, чтобы готовить тестовые последовательности вручную. Разработчикам в любом случае нужно писать тесты, так что мы можем предоставить им удобный инструмент, который, возможно, найдет пару дополнительных багов.
Наша утилита использует для описания последовательностей запросов вот такой yaml:
В фигурных скобках объявлены переменные, которые подставляются в ходе тестирования. Переменная address передается как CLI параметр, а random генерирует произвольную строчку. Наибольший интерес тут представляет поле response-to-var: оно содержит переменную, в которую будет записан json с ответом сервера. Таким образом, в последней строке можно получить id созданного объекта с помощью task.id.
Боль 2. API неконсистентен
Что такое консистентность? Не будем вводить какого-то формального определения, но, упрощая, это внутренняя непротиворечивость. Например, в изначальном проекте Васи нужно было агрегировать данные по докладам на HighLoad, и API предоставляет фильтрацию данных по годам. После того, как проект был почти закончен, к Васе пришел менеджер Коля с просьбой добавить в анализ статистику по докладчикам, причем сделать новый метод “GET speakers” тоже с фильтрацией по годам. В итоге Вася за пару часов дорабатывает код, но в процессе тестирования оказывается, что метод не работает. Причина в том, что в одном случае “год” – это число, в другом – строка. Но это, конечно, не очевидно с первого взгляда и требует постоянной внимательности при работе с API. Конститеность API — это когда такая чрезмерная внимательность не требуется.
Примеров неконсистентости множество:
использование разных форматов одних и тех же данных. Например, формат времени, тип идентификатора(число или строка UUID),
применение разного синтаксиса фильтрации или паджинации,
разные схемы авторизации на сервисах. Мало того, что различия пудрят мозг программистам, они также отражаются на тестах, которые должны будут поддерживать разные схемы.
Лечение:
Архитектурное ревью. Если есть архитектор-тиран, он (при отсутствии шизофрении) обеспечит консистентность. Побочные эффекты: bus factor и тирания :)
Создание API Guideline. Это единый стандарт, который нужно разработать (или взять готовый), но самое главное – внедрить. Для этого требуется и пропаганда, и кнут, и пряник.
Внедрение статических проверок на предмет соответствия аннотации API Guideline (об этом читайте здесь).
Пример — предметы статических проверок
Каждая компания делает свой выбор, каким Guideline пользоваться. И, наверное, нет универсального подхода, что там должно быть, а чего – нет. Ведь чем больше положений в стандарте, тем строже вы подходите к контролю и тем сильнее ограничиваете свободу творчества. И главное, что мало кто дочитает до конца документ из “всего-то 100 страниц”.
В нашей компании мы включили в гайдлайн следующие моменты:
Существование аннотации – необходимое, но не достаточное условие для простоты работы с API. Можно написать аннотацию так, что она не реализует весь свой потенциал. Это случается когда:
не хватает описаний (для параметров, хэдеров, ошибок и т.д.);
не хватает примеров использования, ведь example могут быть использованы не только для улучшения документации (больше контекста для разработчика и возможность прямо с портала интерактивно поиграться с API), но и для тестирования (как отправная точка fuzzing-а));
имеются недокументированные функции.
Обычно такое случается, когда у разработчиков нет четкого понимания, зачем нужные аннотации, когда отсутствует коммуникация между техническими писателями и программистами, а также если никто не посчитал, сколько компании стоит работа с плохой документацией. А если бы к программисту приходили и дергали после каждого запроса в саппорт, все аннотации заполнялись бы очень быстро.
Лечение:
Доступность средств генерации API reference программисту. Если разработчик будет видеть, как выглядит описание его API для коллег и пользователей, он будет стараться сделать аннотацию лучше. Побочные эффекты: конфигурирование этих средств потребует дополнительных рабочих рук.
Настройка взаимодействия между всеми причастными: программистами, евангелистами, сотрудниками саппорта. Побочные эффекты: совещания всех со всеми, усложнение процессов.
Использование тестов на основе аннотации API. Внедрение описанных выше статических проверок в CI репозиториев с аннотациями.
В Acronis на базе аннотаций генерируется API reference с SDK client-ами и Try-It секциями. Вместе с сэмплами кода и описаниями use case-ов, они формируют полный спектр необходимых и удобных дополнений для программистов. Посмотреть на наш портал можно на developer.acronis.com
Надо сказать, что существует целый класс инструментов для генерации API reference. Некоторые компании сами разрабатывают подобный инструментарий под собственные нужды. Другие используют такие достаточно простые и бесплатные инструменты, как Swagger Editor. Мы в Acronis после долгих (действительно долгих) изысканий остановились на Apimatic.io, предпочтя его REST United, Mulesoft AnyPoint и другим.
Боль 4. Проблемы с обратной совместимостью
Обратная совместимость может быть нарушена из-за любой мелочи. Например, программист Вася каждый раз пишет слово compatibility с опечаткой: compatibility. Эта опечатка встречается и в коде, и в комментариях, и в одном query parameter. Заметив ошибку, Вася делает замену этого слова по всему проекту и не глядя отправляет изменения в продакшн. Разумеется, обратная совместимость будет нарушена и сервис упадет на несколько часов.
Почему такие события вообще могут происходить? Основная причина заключается в непонимании жизненного цикла API, который может проявляться и в ломающихся интеграциях, и в непредсказуемых политиках EOL(End Of Life), и в непонятных релизах API.
Лечение:
Архитектурное ревью. Как и всегда, твердая рука архитектора способна предотвратить нарушение обратной совместимости. Однако главной его задачей является объяснение стоимости поддержки нескольких версий и поиск вариантов внесения изменения без поломки существующего API.
Проверка на обратную совместимость. Если аннотация API содержит актуальное описание, то можно проверять нарушения обратной совместимости на этапе CI;
Своевременное обновление документации. API reference и описание API должны обновляться одновременно с изменением кода сервиса. Для этого можно хоть checklist-ы стандартизованные заводить, хоть нотификации на изменения настраивать, хоть тренировать супер-способности по генерации всего из всего… Важно! Отдел документации должен быть в курсе всех планируемых изменений, чтобы у них была возможность запланировать ресурсы на обновление документации и написания upgrade guide-ов. Upgrade guide, протестированный и подтвержденный, — печально значимый атрибут любого переименования, которое вы затеяли в API.
Change Management
Правила, описывающие активности, связанные с жизненным циклом API, называются политиками управления изменениями — change management
Если у вас есть две версии аннотации “текущая” и “новая”, технически проверка на обратную совместимость реализуется просто: распарсив обе аннотации, нужно проверить существование необходимых полей
Мы написали специальный инструмент, который позволяет сравнить все критичные для обратной совместимости параметры в CI. Например, при изменении тела ответа в запросе GET /healthcheck будет выдано сообщение следующего вида:
Заключение
Избавиться от проблем с API мечтает каждый архитектор. Не знать о проблемах API мечтает каждый менеджер. :). Есть много лекарств, но каждое имеет и свою цену и свои побочные эффекты. Мы поделились своими вариантами лечения самых простых детских болезней с API, а дальше встают уже более серьезные проблемы. Выводы из нашей статьи “капитанские”: проблемы API начинаются с головы и обучение людей хорошим практикам есть главный залог успеха. Все остальное лишь дело техники. А с какими проблемами сталкивались вы и какие средства для решения вы избрали в своей организации?
Лекарства от плохого API.
Будем рады любым мыслям, оценкам, замечаниям, мнениям и вопросам!