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

Этот материал будет интересен тем, кто имеет дело с масштабируемыми сервисами, написанными на Go и развернутыми в Kubernetes. Я расскажу об интеграции наших собственных сервисов с Amazon SQS и базами данных - как внутренними, так и сторонними.

С точки зрения бизнес-процессов, мы обеспечивает наших тревел-агентов техническими инструментами и данными для облегчения и ускорения процесса продажи авиабилетов по телефону. За этим стоит не одна система, в процессе только поисков полетов участвует около дюжины систем и сервисов, каждый из которых ответственен за свою маленькую часть в общем деле. Но сердцем всего является система FLST (Flight Search Tool), о которой сегодня я хотел бы рассказать подробнее. 

Пример запроса на поиск перелета
Пример запроса на поиск перелета

Процесс поиска вариантов полетов в FLST 

FLST это набор масштабируемых сервисов, развернутых в Kubernetes, задача которых найти все возможные варианты полетов максимально быстро. Запрос на поиск авиаперелета обычно состоит из базовой информации: даты, точки отправления и назначения, количества пассажиров, класса (бизнес/эконом). Один входящий запрос на поиск перелета (мы называем его логическим поиском) обычно формирует порядка 20 конфигураций высшего уровня (или физических поисков). Количество таких конфигураций зависит от того, где (т.е. в каких booking-провайдерах) и при каких условиях наши сервисы будут производить поиск всех возможных вариантов, но давайте обо всем по порядку. 

Итак, каждый запрос обрабатывается следующим образом:  

Шаг 1. Когда мы получаем запрос на поиск, мы кладем его в очередь (мы используем Amazon SQS). Первый сервис, кто начинает работу с входящим запросом это Validator. Validator проверяет входящий запрос по своим параметрам, и, если все в порядке, запрос, на основании правил, трансформируется в ряд конфигураций высшего порядка. Эти конфигурации, как уже было сказано выше, содержат в себе всю необходимую информацию, на основании которой можно послать запрос к глобальному booking-провайдеру. Для хранения правил мы используем MariaDb. Такие правила формируются через отдельный интерфейс с помощью наших экспертов из бизнес-подразделений и продавцов: они определяют, с какими авиакомпаниями мы работаем и на каких условиях, по каким направлениям можем продавать билеты, где их лучше искать, какие могут быть исключения. 

Шаг 2. Как только Validator создаст эти конфигурации, он передаст их через KeyDB следующему сервису, Throttling service. Основная задача этого сервиса попытаться частично исключить на основании накопленной статистики конфигурации, которые в прошлом показали себя не очень хорошо (например, когда определенные авиакомпании редко предоставляют полеты по данному направлению, или поиск таких вариантов в прошлые разы занял слишком долго времени, и т.п). Для того, чтобы не хранить гигабайты статистики, Throttling использует постоянно обновляемый фильтр Блума. Если определенная конфигурация не проходит через фильтр, то мы ее не исключаем, а добавляем в выдачу в небольшом проценте случаев (примерно в 10%).  

Фильтр Блума - это вероятностная структура данных, которая позволяет определить принадлежность сущности к множеству без необходимости хранить все множество. В нашем случае он позволяет определить, относился ли подобный запрос на авиаперелет в прошлом к неудовлетворительным. Наш фильтр основан на статистике обо всех медленных (более 5 секунд) поисках и поисках, которые не вернули никаких результатов по тем или иным причинам. Мы обновляем его несколько раз в неделю. Применение фильтра Блума позволяет при гораздо меньшем хранимом объеме информации получать практически такой же результат, с небольшим процентом ложноположительных срабатываний, что в нашем случае допустимо. 

Шаг 3. После обработки Throttling сервисом, на выходе мы имеем готовый список физических поисков (или точнее их конфигураций), которые уже можем произвести. Эти конфигурации через  KeyDB забирают специальные программы-обработчики (Search Workers) и отправляют в международную компьютерную систему бронирований (GDS, Global Distribution System или глобальную дистрибьюторскую систему) в совместимом с ней формате. GDS - это автоматические системы, которые выступают в качестве связующего звена между туристическими агентствами и поставщиками, такими как авиакомпании, отели, и другие сервисы, связанные с индустрией путешествий. 

GDS возвращает на каждый запрос разное количество предложений (вариантов полетов), обычно не более 300 (хотя не редко и более 1000). Это финальный этап, на котором те сервисы, которые нам присылали первоначальный запрос, получают результат поиска (FLST кладет их или тоже в Amazon SQS или пересылает через REST запрос). На этом этапе мы также оцениваем скорость обработки каждого запроса на поиск авиаперелетов (по нашим метрикам все поиски, которые обрабатываются более 5 секунд, относятся к медленным) и используем эту информацию для обновления фильтра Блума.  

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

Схема работы Flight Search Tool (FLST)
Схема работы Flight Search Tool (FLST)

Из чего сделан FLST 

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

Validator, Throttling и Search Workers - наши сервисы, написанные на Go и развернутые в кластерах Kubernetes. Сервисная архитектура и взаимодействие через Amazon SQS и KeyDb позволяют нам масштабироваться в гораздо больших объемах, и при необходимости работать над производительностью отдельного звена всей цепочки. 

Все наши сервисы масштабируются как горизонтально (на основании метрик), так и вертикально (добавляя ресурсов, если мы считаем это обоснованным). Например, поисковых сервисов по умолчанию работает чуть менее 20 штук (подов), но их количество увеличивается до почти до сотни в период пиковых нагрузок. Каждый поисковый сервис имеет ограничения по тому, сколько поисков он может делать параллельно, это контролируемый параметр, выставляемый на основании текущих CPU/RAM ресурсов. Соответственно, когда повышается количество входящих запросов на логические поиски, автоматически запускается больше сервисов для их обработки.  

Важным моментом для каждого сервиса является мониторинг - мы собираем огромное количество метрик, представленных в нескольких панелях в Grafana, а также собираем логи процесса поисков ELK. Для компиляции и докеризации мы используем GitLab CI pipelines, а разворачиваем все системы через ArgoCD, который запускает сервисы в Kubernetes.  

Выводы и планы 

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

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

Вадим Мамонтов, Tech Lead, Dynatech
https://github.com/kaytrance

Комментарии (6)


  1. KrasPvP
    25.05.2023 07:33

    Хотелось бы побольше конкретики, тема масштабируемых сервисов, написанных на Go и развернуты в Kubernetes не раскрыта :(
    Например, у вас 20 подов, и потом при нагрузке они увеличиваются - как? Автоскейлинг, либо еще что-то?
    Можно раскрыть было какие-то интересные плюшки при переходе с PHP на Go - что изменили в бизнес-логике, т.п. У вас 3 сервиса, которые тоже можно было раскрыть как-то в статье.
    Обосновать применение MariaDB, KeyDB по сравнению с другими базами данных.


    1. Demacr
      25.05.2023 07:33
      +1

      Я бы ещё добавил к вашему вопросу как выбирался размер самих подов по CPU/RAM.


    1. AlisSha Автор
      25.05.2023 07:33
      +2

      Ответ от Вадима:
      Масштабирование сделано везде на основании CPU/MEM, и только на Search workers - через KEDA (https://keda.sh/) на основании количества поисковых горутин. Это количество на 1 под было выведено скорее экспериментальным путём, скейлинг включается чтобы не допустить более N поисковых горутин на 1 под. Остальные сервисы не такие прожорливые на ресурсы, и отлично себя показали со скейлингом чисто по CPU/MEM.

      Из интересных плюшек наверное только то, что стало сильно проще распараллеливать процессы, масштабировать отдельные компоненты, а так же иметь на выходе скомпилированный бинарник который можно положить в docker image сильно меньшего размера.

      MariaDb у нас стандарт в компании, эту часть не хотелось трогать. А KeyDb заменил Redis поскольку мультипоточна и производительнее Редиса.


  1. unreal_undead2
    25.05.2023 07:33

    GDS возвращает на каждый запрос разное количество предложений (вариантов полетов), обычно не более 300 (хотя не редко и более 1000).

    Мне казалось, что GDS (Amadeus и т.п.) выдаёт только отдельные перелёты, загруженные авиакомпаниями, оптимальные комбинации ищут агрегаторы (используя, скажем QPX от ITA, и там как раз самое интересное - по крайней мере с алгоритмической точки зрения - из происходящего под капотом). Сейчас можно получить разные варианты (в том числе склейку рейсов разных авиакомпаний из разных альянсов) от GDS?


    1. AlisSha Автор
      25.05.2023 07:33

      Ответ от Вадима:
      Всё верно, задача FLST трансформировать запрос, к примеру, вида "Внуково - Адлер для двух пассажиров на такое-то число" в многопоточный параллельный процесс поиска всех возможных вариантов в разных источниках. Формирование из этих потоков оптимальных результатов - задача для других наших сервисов, которыми занимается отдельная команда. Я думаю, если есть интерес, они могли бы оформить это тоже в отдельную статью.


      1. unreal_undead2
        25.05.2023 07:33
        +1

        Да, было бы интересно почитать - интересно, что поменялось со времён выхода той же QPX.