Привет, Хабр! Меня зовут Игорь Балюк, совсем недавно я работал платформенным инженером в Авито, разрабатывал «магическую коробочку», которая автоматизирует всю рутину, связанную с созданием сервиса. А еще я следил за тем, как эти сервисы друг с другом общаются и как далее они эксплуатируются.
Продуктовые разработчики Авито, как правило, думают именно о том, как написать классный продуктовый код, который отражает бизнес-логику, а всю неинтересную для них инфраструктурную рутину мы забираем на себя. В платформе я занимался двумя вещам: сбором телеметрии (логи, трейсинг и так далее) и Service Mesh.
В этой статье я рассказываю про второй аспект моей работы, Service Mesh, и показываю, что это, скорее, не конечная точка, где можно найти «серебряную пулю» от всех проблем, а путь, который, возможно, никогда не закончится: придется постоянно куда-то бежать и что-то дорабатывать. В статье я также разбираю, как работает Service Mesh в Авито.
Посмотреть выступление с докладом по этой теме также можно посмотреть здесь.

Содержание:
Service Mesh — дорога, а не пункт назначения
Давайте обратимся к названию. В словосочетании Service Mesh есть слово «сервис». Наверняка, истоки восходят к каким-то сервисам.

В какой-то момент в больших компаниях число сервисов бурно возросло, с чем столкнулись и мы. Поэтому потребовался подход, способствующий организации сервисного общения.
Сегодня под Service Mesh подразумевается некоторый фреймворк, который и позволяет наладить это межсервисное общение.
При этом мы хотим сделать так, чтобы межсервисное общение было:
гибким и управляемым, чтобы мы как администраторы могли все быстро подкрутить, чтобы трафик балансировался между разными дата-центрами и так далее;
наблюдаемым: сверху можно посмотреть на всю систему, получить автоматические метрики, лучше реализовать трейсинг — как наши сервисы друг с другом связаны, какие взаимоотношения у них есть;
безопасным. Этот трек особенно популярен в последнее время. Хочется сделать так, чтобы наши сервисы общались друг с другом по безопасным протоколам, которые не позволили бы злоумышленнику при его появлении получить информацию.
Service Mesh в Авито: как мы меняли решения
В Авито мы реализуем Service Mesh более шести лет. Сейчас у нас:
4 500 уникальных сервисов;
2 250 сотрудников в разработке;
более 1 млн запросов в секунду, проходящих через Service Mesh;
есть инсталляции с инфраструктурой как on-prem, так и в облаке.

Платформа Авито начала зарождаться в 2018 году. Уже тогда у нас было довольно много (около 150) сервисов, поэтому захотелось протестировать какой-нибудь Service Mesh. Начинать стоит с чего-то простого — так и появился наш первый велосипед Netra.
В течение года мы опробовали гипотезу, что Service Mesh — это, в принципе, классное направление, куда нам хочется идти. Поэтому один велосипед мы заменили на второй и назвали его Navigator. С ним мы прожили еще несколько лет до конца 2022 года. За это время к нам прилетали все новые и новые требования от бизнеса.
К концу 2022 года нам надоело делать свои велосипеды, мы попробовали пойти в комьюнити и посмотреть, что существует уже в готовом виде, — так мы обратились к Istio.
Давайте начнем сначала.
2018 год. Мы хотим проверить гипотезу и начать с чего-то очень простого, поэтому решаем построить карту сервисного общения. Каких-то строгих требований к тому, какой Service Mesh хочется взять, у нас не было.
Типичный Service Mesh на тот момент выглядел следующим образом:

Есть Control Plane — специальный компонент, который наблюдает за всей системой. Есть специальная sidecar-прокси, которая подселяется в каждый Kubernetes-под и контролирует весь входящий и исходящий трафик, может с ним что-то делать: например, отправлять его в другое место.
При этом на тот момент уже были такие решения, как Istio и linkerd.

Мы решили, что они для нас слишком сырые и неудовлетворительные по перформансу и потреблению ресурсов, ведь мы просто хотим проверить гипотезу. Начинать стоит с чего-то простого, так и появилась та самая netra — реализация нашей собственной proxy, написанная на Go.

Netra представляла собой легковесное решение (<2000 строк Go-кода) без Control Plane. Данная sidecar-прокси прозрачно обрабатывала весь входящий и исходящий трафик из приложения, автоматически собирала метрики и трейсы и обеспечивала базовую маршрутизацию.
Ускорение тестирование за счет sidecar-прокси

Представьте, что у нас много сервисов и разработчиков. Одновременно несколько разработчиков хотят разрабатывать один и тот же сервис — обновить его код, выкатить свои изменения и проверить их в определенном staging окружении. Когда людей становится много, начинается толкание локтями. Нужно либо договариваться о каком-то расписании, мол «я беру сервис с двух до трех часов дня, пожалуйста, не мешайте», либо иметь много изолированных staging окружений, чтобы каждый нашел себе место, где можно что-то протестировать. Данный подход плохо масштабируется с ростом компании.
Поэтому мы пришли к идее так называемых «изолированных веток». Представьте, что мы хотим выкатить новую версию сервиса. Делаем изменения в git, выкатываем изолированную версию сервиса, которая существует параллельно основной версии сервиса. По умолчанию все запросы идут на основную версию сервиса. Но если мы укажем некий специальный HTTP-заголовок X-Route, то proxy увидит этот заголовок и поймет, что запрос нужно отправить не на основную версию сервиса, а на одну из его «веток».
Возможности netra
Благодаря простому решению в 2000 строк кода мы:
получили наблюдаемость системы (метрики, трейсинг);
улучшили тестирование и ускорили TTM.
Но осталось очень много вопросов, которые netra не покрывала:
нет service discovery;
нет гибкого управления: мы никак не можем управлять трафиком, потому что нет Control Plane;
непонятно, как быть с межкластерными походами. Клиенту по-прежнему нужно знать, где именно запущенные зависимости, к которым он хочет обращаться;
не продвинулись в направлении безопасности.
В поисках нового Service Mesh: Navigator
Сама идея Service Mesh нам очень понравилась. В 2019 году мы всё же попробовали запустить Istio в нашей инфраструктуре, но обнаружили проблемы с производительностью: накладные расходы на ресурсы выходили довольно высокими. Это было связано с архитектурой на Istio. На тот момент сайдкары знали про все остальные сайдкары в системе, из-за чего конфиг у envoy-сайдкара сильно распухал и требовал много RAM. При этом Istio не давал хорошего решения для упрощения межкластерных запросов.
Тогда мы решили, что Istio, хотя и многообещающий инструмент, пока все же сыроват, а потому сделали еще один велосипед. На тот момент вдохновились ingress-контроллером Project Contour. Так появился еще один Service Mesh – Navigator.

Navigator представляет из себя тот самый Control Plane, который обращается к API Kubernetes, получает информацию про поды и ресурсы, необходимые для конфигурации, и готовит конфигурацию для envoy. envoy — это уже реализованная прокси, которая будет запущена в каждом поде рядом с настоящим приложением в роли сайдкара.
Имея Control Plane, довольно легко получилось реализовать Service Discovery: navigator явно знает, когда в системе появляются новые поды, когда эти поды становятся недоступными, а также в каких кластерах и дата-центрах они запущены.

За счет этого мы можем сильно упростить логику и код клиента, потому что клиенту теперь достаточно сказать: «я хочу подключиться к какому-то сервису X», а дальше уже envoy проксирует этот запрос и знает, в каких кластерах запущен тот самый сервис X. envoy позволяет настроить «умную» балансировку трафика и будет учитывать локальность, что позволит оптимизировать latency.
Далее мы перешли к идее канареечных релизов (canary rollout / release).

Когда разработчик хочет выпустить новую версию своего приложения, мы запускаем ее параллельно основной и начинаем постепенно подавать трафик на новую версию приложения: сначала 10% трафика, потом 15%, 20% и так далее. Если по метрикам все хорошо, докатываем «канарейку» до 100%. Если по метрикам видим, что появились какие-то новые «пятисотки» либо у новой версии увеличился латенси, «канарейка» отменяется, пока не будет найдена причина. Данные релизы позволяют значительно снизить урон от багов, которые сложно отловить на этапе тестирования и staging окружении.
С точки зрения конфигурации Navigator был довольно простым.
Для конфигурации использовались стандартные ресурсы из Kubernetes — Service и Ingress, которые регистрировали сущность сервиса в нашей системе и управляли внешней подачей трафика.
Наши собственные ресурсы — это Canary, который банально контролировал, какой процент канарейки нужно пустить на новую версию приложения, и Nexus.
apiVersion: v1
items:
- apiVersion: navigator.avito.ru/v1
kind: Nexus
metadata:
...
spec:
appName: cars-service-catalog-2119-staging
cookieAffinity: ""
downstreams:
- name: cars-service-catalog-2119-staging
namespace: cars-service
headerAffinity: ""
inboundPorts:
- name: http
port: 8890
mTLS:
enabled: true
spiffeID: spiffe://company/hs/cars-service/service/non-prod
rateLimit:
enabled: false
port: 15011
serviceName: cars-service
services:
- namespace: toggles
- namespace: geo-locator
- namespace: image-classifier
- namespace: ab-tests
- namespace: delivery-layout
- namespace: cars-storage
- namespace: user-profile
- namespace: cars-service
Nexus — еще одна наша собственная самоделка, которая содержит в себе всю необходимую нам информацию про сервис. Данный ресурс позволял контролировать параметры rate limit, управлять режимом TLS и требовал указывать список зависимостей сервиса. По сравнению с другими решениями (Istio) настраивать Service Mesh благодаря одному типу ресурса было значительно проще.
Давайте поговорим про безопасность
Первое, что хочется сделать — внедрить в систему проверенный протокол HTTPS.
Он относительно старый, но в этом его преимущество. HTTPS очень распространен. Все языки программирования и многие фреймворки уже умеют с ним работать: понятно, как подключить сертификаты в свой сервис, как научить приложение общаться по HTTPS.
Но базовый HTTPS не решает проблему доверия клиенту. Сервер не знает, кто именно к нему подключился. Для решения этой задачи существует расширение протокола TLS, которое называется Mutual TLS. Теперь сертификат будет не только у сервера, но и у клиента тоже.

Таким образом, когда сервер получает запрос от клиента, он может посмотреть на сертификат клиента, проверить, что сертификат подписан корректным сертификационным центром, и в этом сертификате написано, кто является этим клиентом.
Подобная модель, по сути, решает проблему отсутствия аутентификации.
[[policy]]
description = "Ограничить доступ к РТГ данным"
endpoints = [
"грс:get",
"грс:getAll",
"http:put:/user/*/update",
"http:post/user",
"http:get/user/*/payments",
]
clients = [
# сервисы
"atlas",
"admin-api",
# пользователи
"user:ipivanov",
# ЮНИТЫ
"user.unit:paas",
]
Даже если злоумышленник попадет в систему, он не сможет сделать запрос на произвольный сервис бэкенда. Для запроса необходимо получить сертификат — а его получить его могут только авторизованные сотрудники с наличием необходимых прав.
Получив аутентификацию запросов, можно перейти и к авторизации. Например, можно указать, у каких сервисов и сотрудников есть доступ к определенным HTTP-эндпоинтам для каждого сервиса.
Если интересна тема межсервисной авторизации, доклад про это можно послушать здесь.
С точки зрения инфраструктуры mTLS в Авито выглядит подобным образом:

Центральным компонентом является Spire. Это готовый компонент, запущенный вне контура k8s кластера, который занимается регистрацией сущностей в системе и генерацией сертификатов для этих сущностей. При этом на каждом Kubernetes-узле запущен специальный Spire-агент. Агент является некоторым кэшом (или прокси) для основного spire сервера. И когда на Kubernetes-узле появляется очередной под с envoy-сайдкаром, envoy нужно где-то взять сертификат для себя, поэтому он обращается к Spire-агенту.
Если тема безопасности вам тоже интересна, то мы рассказывали про нее здесь.
Скорее всего идея mTLS будет очень по душе вашей Security-команде. Но внедряя данный подход на большом масштабе, можно столкнуться с некоторым сопротивлением. Естественно, разработчики и владельцы сервисов могут отказываться от внедрения mTLS. Раньше можно было сделать curl запрос из терминала, а теперь необходимо думать про сертификаты.
Для упрощения внедрения mTLS мы поддержали переходный режим, в рамках которого сервис может принимать запросы как с сертификатом, так и без. Когда мы хотим переключить определенный сервис на mTLS, мы идем по следующему алгоритму:
разрешаем подключение к нему как с сертификатом, так и без;
всем легитимным клиентам сервиса добавляем клиентский сертификат;
когда все клиенты переведены, запрещаем подключение к сервису без клиентского сертификата.
Наконец-то…
После нескольких лет мы пришли довольно к очевидному выводу, что разрабатывать свои велосипеды — это не очень-то приятно и довольно сложно.
Наша команда из четырёх человек занималась поддержкой Navigator, в то время как требования к системе постоянно росли. Особую сложность представляло тестирование Service Mesh: end-to-end тесты не покрывали всего разнообразия продакшен-сценариев, что делало каждое изменение потенциально опасным — ошибка в Control Plane могла парализовать всё сервисное взаимодействие в организации. Усугубляло ситуацию отсутствие документации, осложнявшее онбординг новых сотрудников.
Вскоре появилась необходимость поддержать возможность использования облачной инфраструктуры для запуска сервисов, что потребовало существенных доработок Navigator. В итоге это привело нас к решению о переходе на Istio.
Istio: финальный босс
Попробовав два собственных решения, в конце концов мы пришли Istio по нескольким ключевым причинам:
Совместимость: в нашей инфраструктуре уже использовались envoy-сайдкары, поэтому мы хорошо понимаем их поведение и накладные расходы.
Зрелость: проект значительно развился — оптимизирована производительность, устранены основные ошибки. Будучи одним из самых популярных Service Mesh решений, Istio проходит проверку в тысячах производственных окружений, что минимизирует риск критических багов в стабильных версиях.
Документация: хотя и не идеальная, она достаточно подробна для open-source проекта и позволяет проводить онбординг новых сотрудников.
Поддержка межкластерных запросов: встроенные инструменты помогают быстро организовать общение между инфраструктурой on-premise и облаком (например, с помощью east-west gateway).
Как выглядела миграция?

Вся наша сервисная инфраструктура работала на Navigator. Рассмотрим пример сервиса A в Kubernetes:
запущен в отдельном поде;
содержит приложение и envoy-контейнер;
имеет связанные Kubernetes-ресурсы для настройки сетевого взаимодействия
Над этой конструкцией располагается Navigator. Control Plane наблюдает за состоянием ресурсов и генерирует специализированную конфигурацию для envoy.
Есть другой сервис B, с которым первый сервис общается через envoy.

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

После подготовки валидных манифестов мы передаем namespace под управление Istio, который начинает мониторить ресурсы и генерировать конфигурацию для envoy.

При этом для владельцев приложения и для владельцев самого сервиса ничего не изменилось — envoy как общались друг с другом, так и общаются.
Как ускорить миграцию?
Не давать разработчикам доступ к манифестам :)
Нам сильно помог принцип, который был заложен в нашу платформу. В Авито у продуктовых разработчиков, как правило, нет возможности изменять k8s манифесты. Генерацией манифестов занимаются несколько специальных инфраструктурных команд. У продуктовых разработчиков есть удобный веб-интерфейс, где они настраивают работу своего сервиса под себя, однако здесь не требуется управления манифестами. Существует специальный инфраструктурный сервис, который преобразует указанные высокоуровневые параметры в YAML-конфигурации. Эту систему как раз и поддерживают несколько специализированных команд.

Istio и трудности
Шишки мы тоже набили.
Так получилось, что в Авито на момент миграции на Istio довольно сильно разнились версии Kubernetes, были продакшен-кластеры на 1.14, на 1.20, на 1.23. Когда появляется такой огромный разброс в версиях, скорее всего, возникают и некоторые несовместимости в API базовых ресурсов Kubernetes. Не получилось найти одну версию Istio, которая бы запустилась во всех кластерах одновременно.
Поэтому мы сразу же прыгаем с места в карьер и форкаем Istio так, чтобы базово научить работать как со старыми версиями ресурсов, так и с новыми.
Получается расхождение идей — мы хотели отказаться от собственных велосипедов, но как только берем готовое решение, нам приходится его копировать и патчить. К сожалению, ничего умнее на тот момент мы не придумали.
Но форкнуть может каждый. Вопрос: как этот форк дальше поддерживать? Тут нам помогает то, что Service Mesh — это не та штука, которую хочется часто обновлять.

Мы не стремимся установить каждое новое обновление Service Mesh. Обычно мы это делаем тогда, когда у нас обновляется версия Kubernetes кластеров, через пересоздание кластеров.
Действительно, каждый раз, когда мы хотим установить новую версию, необходимо пройти по всем патчам и проверить их корректность. Самих патчей не так много, поэтому данный процесс пока что контролируется.
Межкластерные походы
К сожалению, Istio не смог предоставить нам инструментов для удобной организации межкластерных походов «из коробки».
Представим, что есть три кластера, а также некий сервис, который запущен только в центральном кластере (в кластере B).

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

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

Чтобы это все заработало, нужно правильно настроить Istio: создать корректные манифесты и ресурсы, которые у Istio называются VirtualService и DestinationRule. Это базовые ресурсы, управляющие маршрутизацией трафика и передающие, как клиенту подключиться к серверу, какую аутентификацию использовать и так далее.
Ключевая проблема — распространение этих ресурсов в сторонние кластеры, где наш сервис не развернут. В качестве решения мы создали собственный репликатор, который занимается распространением вышеуказанных ресурсов Istio.

Репликатор — это наш собственный оператор, запускаемый в каждом кластере. Каждый экземпляр репликатора наблюдает за всеми остальными кластерами. Если репликатор видит, что в каком-то другом кластере появился новый под сервиса или новые ресурсы DestinationRule, VirtualService, эти ресурсы копируются в локальный кластер, если их еще там нет. Таким образом, после репликации локальный Istio поймет, что есть сущность сервиса в другом кластере и настроит envoy-сайдкары своего кластера таким образом, чтобы при необходимости запросы отправлялись и в другие кластеры в том числе.
Это нас ведет к тому, что если мы начинаем сильно погружаться в Istio, то манифестов становится очень много.


На данный момент на один деплой сервиса в один кластер мы создаем более 10 манифестов. Есть и простые манифесты, например, Peer Authentication. Но манифесты DestinationRule и VirtualService, которые нужны для основного релиза, «канарейки», East-West Gateway, ingress-контроллера и т.п., становятся очень сложными.
Чтобы лучше разобраться в этом «бардаке», у нас есть документация. На онлайн-доске распечатаны наши Kubernetes-манифесты для Istio. Между разными полями в YAML-манифестах прорисованы стрелочки, написано, что эти поля должны совпадать и быть равны друг к другу, иначе все сломается по такой-то причине.
Данная доска и ситуация, в которой мы иногда находимся, очень похожа на следующую картинку:

К сожалению, с ростом сложности Service Mesh при использовании Istio невозможно в голове одного человека уместить весь объем информации — связи становятся очень сложными.
Параллельно с этим пользователи приходят со своими вопросами.
Владельцев сервисов как правило беспокоят две проблемы:
высокая задержка (latency);
неожиданные ошибки (например, «upstream connect error»), которые ранее не встречались.
Как правило, на то есть две причины:
Конфликты в конфигурации. Обычно это связано обычно с тем, что мы как владельцы Service Mesh сварили не до конца правильную конфигурацию и Istio отработал не так, как от него ожидали.
Конфигурация распространяется долго. Если сервисов, подов, кластеров становится много, Istio может притормаживать.
Почему это важно?
Распространение обновлений
Когда мы раскатываем новую версию сервиса, у нас есть какой-то ожидаемый порядок применения наших изменений.
Запуск новой версии приложения: для начала мы хотим дождаться готовности новой версии сервиса, приложение должно запуститься.
Настройка mesh для новой версии: для новой версии сервиса начинаем настраивать Service Mesh, чтобы новые поды умели общаться со своими зависимостями.
Переключение трафика: переключаем подачу трафика со старой версии приложения и на новую.
Удаление старой версии.
Если по какой-то причине мы сначала переключим трафик, а только потом настроим Service Mesh для новой версии сервиса — конфигурация придет поздно, трафик пойдёт в бездну, т.е. в версию, которая еще не готова к эксплуатации. Istio не предоставляет какой-либо хорошей гарантии, что изменения будут применены в определенном порядке, когда речь идет про несколько ресурсов, настраивающих работу сервиса. Пока мы не реализовали хороший механизм проверки применения изменений в Istio. Вместо этого используем простой sleep с длительной задержкой в надежде, что за n секунды очередные изменения точно применятся.
Отладка Istio
Если говорить про инструменты, то для откладки Service Mesh нужно вообще всё, что у вас есть, и всё, что вы умеете:
tcpdump;
istioctl;
envoy onfig_dump;
strace, ebpf, …
Вся экспертиза в старом Linux-стеке также пригодится.
Istio и envoy предоставляют довольно много полезных метрик, но для отладки в первую очередь мы обращаемся к следующим:
istio_requests_total — детально отражает HTTP-взаимодействия в системе и включает исчерпывающую информацию о маршрутизации (источники, назначения и другие параметры). Однако из-за чрезмерной кардинальности некоторых измерений нам пришлось исключить часть полей, чтобы сократить объем хранимых данных;
pilot_xds_… — для диагностики задержек в работе Istio следует мониторить метрики pilot_xds_..., которые показывают время генерации конфигурации envoy и продолжительность её распространения. Высокие значения этих метрик указывают на возможную нехватку ресурсов и необходимость масштабирования Istio.
Если говорить про накладные расходы, то в каждой организации они будут разными.

Накладные расходы:
P99 |
1.0 CPU |
350 MiB RAM |
P75 |
0.2 CPU |
170 MiB RAM |
Мы пришли к тому, что 99% потребления envoy-сайдкара у нас — это одно ядро и порядка 350MiB оперативной памяти. Но дальше можно пойти в анализ latency и посмотреть, как сильно сайдкар влияет на latency. Я не стал приводить конкретные выкладки, потому что это сильно зависит от профиля нагрузки и от того, какой именно трафик и протокол вы пускаете через этот envoy.
Istio: плюсы и минусы
От Istio мы получили следующее:
код уже написан, не нужно слишком сильно велосипедить;
тестируется другими, то есть критичных багов, скорее всего, не будет;
многое есть из коробки, так как Istio пытается быть универсальным решением;
гибкая настройка, из его готовых манифестов можно собрать целого Франкенштейна.
Но в то же время Istio:
местами запутанный. Если наши требования слишком строгие или у владельцев сервисов много запросов, то Istio становится очень запутанным.
реализовано многое, но не все. Несмотря на универсальность, реализовано действительно не всё. На нашем примере мы реализовывали разные репликаторы, которые помогают Istio распространять ресурсы. Есть необходимость в небольших патчах.
напоминает о себе. Istio нельзя запустить и забыть о нем на два года. Рано или поздно что-то сломается.
Заключение
Наш опыт показал, что эксплуатация Service Mesh требует значительных усилий. Это не серебряная пуля — система сложна в настройке, подвержена сбоям и требует постоянного мониторинга определенных команд. При этом универсального решения не существует.
Мы протестировали несколько реализаций Service Mesh и остановились на Istio, хотя понимаем, что со временем выбор может измениться — возможно, появится более совершенное решение вроде Cilium++.
Тем не менее, несмотря на все сложности, Service Mesh остается мощным инструментом для организации сервисного взаимодействия.
Service Mesh — это отличный фреймворк, который нам дает те самые канареечные релизы, улучшает стабильность и отказоустойчивость нашей системы. Мы можем балансировать трафик между кластерами и сервисами, наблюдать за нашей системой автоматически без участия продуктовых разработчиков. Безопасники будут счастливы. Если говорить про подход с сайдкарами, нам неважно, на каком языке написано приложение.
Это действительно помогает организации масштабироваться. По сути, мы независимы от числа сотрудников и сервисов, которые у нас есть. Этим всем управляет лишь несколько платформенных команд.
Знаком ли вам опыт интеграции Service Mesh? Делитесь кейсами в комментариях.
А если хотите вместе с нами помогать людям и бизнесу через технологии — присоединяйтесь к командам. Свежие вакансии есть на нашем карьерном сайте.
Dhwtj
Жаль, что из статьи не ясно зачем столько сервисов и что они делают