Главная » Хабрахабр » [Перевод] Прощайте, микросервисы: от ста проблемных детей до одной суперзвезды

[Перевод] Прощайте, микросервисы: от ста проблемных детей до одной суперзвезды

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

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

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

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

Почему микросервисы работают работали

Инфраструктура данных клиентов Segment принимает сотни тысяч событий в секунду и перенаправляет их партнерским API, что мы называем направлениями на стороне сервера (server-side destinations). Существует более ста видов этих направлений, таких как Google Analytics, Optimizely или пользовательские веб-хуки.

Был API, который принимал события и отправлял их в очередь распределенных сообщений. Годы назад, когда продукт изначально был запущен, архитектура была простой. Пример полезной нагрузки выглядел следующим образом: Событием в этом случае был JSON-объект сгенерированный веб- или мобильным приложением, содержащий информацию о пользователях и их действиях.

, "userId": "97980cfea0067"
}

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

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

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

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

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

image

Случай индивидуальных репозиториев

Каждый API направлений использует разный формат запросов, требуя дополнительный код для перевода события в соответствии с этим форматом. Простой пример – назначение X требует отправки даты рождения как traits.dob, в то время как наш API принимает traits.birthday. Код преобразования для назначения X будет выглядеть примерно так:

const traits = {}
traits.dob = segmentEvent.birthday

Многие современные конечные точки назначений приняли формат запросов Segment, что делает некоторые преобразования относительно простыми. Однако, эти преобразования могут быть очень сложными в зависимости от структуры API назначения. Например, для некоторых из старых и самых размашистых точек назначения, мы вручную формируем XML со значениями.

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

Эта изоляция позволила команде разработчиков быстро переключаться во время поддержки. Разделение на отдельные репозитории позволило нам легко изолировать тесты направлений.

Масштабирование микросервисов и репозиториев

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

Общая библиотека проверяет событие на наличие свойств name и Name. Например, если нам необходимо имя пользователя из события, event.name() может быть вызвано из кода любого направления. Тоже самое делает для фамилии, проверяя случаи и комбинируя их чтобы сформировать полное имя. Если они не существуют, она проверяет свойства firstName, first_name, и FirstName.

Identify.prototype.name = function() { var name = this.proxy('traits.name'); if (typeof name === 'string') { return trim(name) } var firstName = this.firstName(); var lastName = this.lastName(); if (firstName && lastName) { return trim(firstName + ' ' + lastName) }
}

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

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

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

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

Хотя у нас и было автоматическое масштабирование, каждый сервис имел четкое сочетание требований ресурсов памяти и процессора, что сделало настойку авто-масштабирования больше искусством, чем наукой.

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

Избавление от микросервисов и очередей

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

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

image

Перемещение в монорепозиторий

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

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

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

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

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

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

Создание устойчивых наборов тестов

Исходящие HTTP запросы к конечным точкам направлений во время прогона тестов были основным источником падения. По опыту мы также знали, что некоторые конечные точки были гораздо медленнее других. Некоторые направления требовали до 5 минут для запуска их тестов. С более чем 140 направлениями время выполнения нашего тестового набора могло занять до часа.

Traffic Recorder собран на основе yakbak, и отвечает за запись и сохранение тестового трафика получателей. Для решения этих проблем мы создали Traffic Recorder. При последующих тестовых запусках запрос и ответ воспроизводятся из файла вместо выполнения запроса по назначению. Всякий раз, когда тест выполняется в первый раз, любые запросы и их соответствующие ответы записываются в файл. Теперь, когда тестовый набор больше не зависит от HTTP запросов через Интернет, наши тесты стали существенно устойчивей и обязательными для миграции в единый репозиторий. Эти файлы добавляются в репозиторий, чтобы тесты были согласованны при каждом изменении.

Потребовались миллисекунды, чтобы завершить тесты для всех 140+ наших направлений. Я помню, как проходили тесты для каждого пункта назначения в первый раз после того, как мы интегрировали Traffic Recorder. Это было похоже на магию. Раньше всего один пункт мог занять пару минут.

Почему монолит работает

Когда код для всех направлений находится в одном репозитории, они могут быть объединены в один сервис. Благодаря тому, что каждое направление находится в одном сервисе продуктивность наших разработчиков значительно улучшилась. Нам больше не нужно разворачивать 140+ сервисов при изменении одной из общих библиотек. Один инженер может развернуть сервис за считанные минуты.

В 2016, когда наша микросервисная архитектура еще существовала, мы сделали 32 улучшения общих библиотек. Доказательством стало увеличение скорости. Мы сделали больше улучшений библиотек за последние 6 месяцев, чем за весь 2016 год. Только в этом году мы сделали 46.

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

Компромиссы

Перенос нашей микросервисной архитектуры в монолит в целом было огромным улучшением, однако пришлой пойти на компромиссы:

  1. Отказоустойчивость затруднена. Когда все работает в монолите, если появляется баг в одном направлении, который приводит к падению сервиса, сервис упадет для всех направлений. У нас есть всестороннее автоматическое тестирование, но тесты все равно могут подвести. В настоящее время мы работаем над гораздо более надежным способом не допустить того, чтобы один пункт назначения ломал весь сервис, сохраняя работоспособность остальных направлений в монолите.
  2. Кэширование в памяти менее эффективно. Раньше, с одним сервисом на направление у маршрутов с низким трафиком было всего несколько процессов, что означало, что их кэш в памяти оставался горячим. Теперь кэш тонко распределяется между 3000+ процессов, поэтому шансов попасть в него гораздо меньше. Мы могли бы использовать что-то вроде Redis чтобы решить эту проблему, но это добавило бы еще одну точку масштабирования. В конце концов, мы приняли эту потерю эффективности с учетом существенных эксплуатационных преимуществ.

Заключение

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

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

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

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


Оставить комментарий

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

Орден куколки

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

Трудоголизм — болезненное состояние, о котором не принято говорить

Тему профессионального выгорания не пинал на Хабре только ленивый. Были истории и о том, как с этим бороться, и рассказы людей, которые лично столкнулись с этой проблемой, и статьи вида «как избежать выгорания». На самом деле тема важная и нужная. ...