Хабрахабр

Как устроен поиск Яндекс.Маркета и что будет, если упадёт один из серверов

Привет, меня зовут Евгений. Я работаю в инфраструктуре поиска Яндекс.Маркета. Хочу рассказать сообществу Хабра о внутренней кухне Маркета – а рассказать есть что. Прежде всего, как устроен поиск Маркета, процессы и архитектура. Как мы справляемся с внештатными ситуациями: что случится, если упадёт один сервер? А если таких серверов будет 100?

И как тестируем сложные сервисы прямо в production, не доставляя пользователям никаких неудобств. А ещё вы узнаете, как мы внедряем новую функциональность на куче серверов сразу. В общем, как устроен поиск Маркета, чтобы всем было хорошо.

Немного о нас: какую задачу мы решаем

Когда вы вводите текст, ищете товар по параметрам или сравниваете цены в разных магазинах, все запросы прилетают на сервис поиска. Поиск – это самый большой сервис в Маркете.

К нам относятся и предложения товаров в поисковой выдаче на yandex.ru. Мы обрабатываем все поисковые запросы: с сайтов market.yandex.ru, beru.ru, сервиса «Суперчек», Яндекс.Советника, мобильных приложений.

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

Что есть что: архитектура Маркета

Кратко опишу текущую архитектуру Маркета. Условно можно описать её схемой ниже:

Допустим, к нам приходит магазин-партнёр. Говорит, хочу продать игрушку: вот этого злобного кота с пищалкой. И ещё злобного кота без пищалки. И просто кота. Тогда магазину нужно подготовить предложения, по которым Маркет осуществляет поиск. Магазин формирует специальный xml с предложениями и сообщает путь к этому xml через партнерский интерфейс. Затем индексатор периодически скачивает этот xml, проверяет на наличие ошибок и сохраняет всю информацию в огромную базу данных.

Из этой базы данных создается поисковый индекс. Таких сохранённых xml много. После создания индекса сервис Раскладки выкладывает его на поисковые серверы. Индекс хранится во внутреннем формате.

В итоге, в базе появляется злобный кот с пищалкой, а на сервере – индекс кота.

О том, как мы ищем кота, расскажу в части об архитектуре поиска.

Архитектура поиска маркета

Мы живем в мире микросервисов: каждый входящий запрос на market.yandex.ru вызывает очень много подзапросов, и в их обработке участвуют десятки сервисов. На схеме изображены только некоторые:


Упрощённая схема обработки запроса

У каждого сервиса есть замечательная штука – свой балансер с уникальным именем:

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

Когда сервис А делает запрос в B, то по умолчанию балансер B перенаправляет запрос в текущий дата-центр. Уникальное имя балансера не зависит от дата-центра. Если же сервис недоступен или отсутствует в текущем дата-центре, то запрос перенаправляется в другие дата-центры.

Его запрос в сервис B всегда будет обработан. Единый FQDN для всех дата-центров позволяет сервису А вообще абстрагироваться от локаций. Исключение составляет случай, когда сервис лежит во всех дата-центрах.

Балансер может работать нестабильно, и эта проблема решается избыточными серверами. Но не всё так радужно с этим балансером: у нас появляется дополнительная промежуточная компонента. Но на практике она меньше 1 мс и для большинства сервисов это некритично. Также происходит дополнительная задержка между сервисами A и В.

Борьба с неожиданностями: балансировка и отказоустойчивость сервиса поиска

Представьте, что случился коллапс: надо найти кота с пищалкой, но падает сервер. Или 100 серверов. Как выкрутиться? Неужели оставим пользователя без кота?

Расскажу по порядку. Ситуация страшная, но мы к ней готовы.

Поисковая инфраструктура находится в нескольких дата-центрах:

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

В каждом дата-центре одинаковая схема работы балансеров: Рассмотрим отдельно взятый дата-центр.

Такая избыточность сделана для надежности.
Один балансер – это как минимум три физических сервера. Балансеры работают на HAProxу.

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

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

Поэтому надо постоянно отслеживать состояния всех серверов. Так и происходит в реальности: серверы падают. Для этого в HAProxy есть встроенный health check. Если сервер перестает отвечать, то его автоматически отключают от трафика. Он раз в секунду ходит на все серверы с HTTP запросом «/ping».

Для этого HAProxy подключается ко всем серверам, а они возвращают свой вес в зависимости от текущей нагрузки от 1 до 100. Другая особенность HAProxy: agent-check позволяет равномерно загружать все сервера. Вес вычисляется на основании количества запросов в очереди на обработку и нагрузки на процессор.

На поиск прилетают запросы вида /search?text=angry+cat. Теперь о поиске кота. Даже чтение из SSD недостаточно быстрое. Чтобы поиск был быстрым, весь индекс кота должен помещаться в оперативную память.

По мере роста базы предложений, все перестало помещаться в эту оперативную память, и данные разделили на две части: shard 1 и shard 2. Давным-давно база предложений была маленькая, и для неё хватало оперативной памяти одного сервера.


Но так всегда бывает: любое решение, даже хорошее, порождает другие проблемы.

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

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

Решением было указать в качестве «соседнего» сервера несколько серверов с разными приоритетами. Проблема возникала, если соседний сервер оказывался недоступен. Если не поступало ответа, запрос отправлялся на все серверы в этом дата-центре. Сперва запрос отправлялся на серверы в текущую стойку. Но и это был не предел. И уже в последнюю очередь запрос шел в другие дата-центры.
По мере роста количества предложений данные разделили на четыре части.

Кроме того, для ещё большей экономии памяти индекс разделили на поисковую часть (по которой происходит поиск) и на сниппетную часть (которая не участвует в поиске). Сейчас используется конфигурация из восьми шардов.

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

Каждый кластер содержит восемь поисковых серверов и один сниппетный. Серверы сгруппированы в кластеры.

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

Ну или что по одному ID будет другой контент. Поскольку ID документов уникальны только в рамках одного индекса, могла возникнуть ситуация, что в «сниппетах» отсутствуют документы. О том, как мы следим за консистентностью, расскажу чуть ниже. Поэтому, чтобы поиск работал и происходила выдача, появилась необходимость в консистентности всего кластера.

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

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

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

Кроме того, между всеми серверами внутри кластера раз в секунду постоянно делаются подзапросы вида: /status. Поисковые запросы внутри кластера имеют вид: /shard1?text=angry+cat.

Запрос /status обнаруживает ситуацию, когда сервер не доступен.

Также он контролирует, чтобы на всех серверах версия поисковика и версия индекса были одинаковы, иначе внутри кластера будут неконсистентные данные.

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

Теперь невозможна ситуация, когда по одному ключу возвращается контент от другого документа. Чтобы перенести данные, мы ввели универсальные ключи для документов.

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

Рассмотрим несколько случаев недоступности серверов. А теперь к страшным историям со счастливым концом.

Случилось ужасное: недоступен один сервер

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

Поэтому, для сохранения полноты, все серверы в кластере на запрос /ping начинают отвечать балансеру, что они тоже недоступны. Через проверку статуса /status соседние серверы понимают, что один недоступен. Это основной недостаток нашей схемы с кластерами – поэтому мы хотим от неё уйти. Получается, умерли все серверы в кластере (что не так).

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

Как только начинают приходить нормальные ответы на пинги от мёртвых серверов, балансеры начинают отправлять туда пользовательский трафик. Когда сервер становится доступен, он начинает отвечать на /ping. Работа кластера восстанавливается, ура.

Ещё хуже: недоступно много серверов

Вырубается значительная часть серверов в дата-центре. Что делать, куда бежать? На помощь опять приходит балансер. Каждый балансер постоянно содержит в памяти текущее количество живых серверов. Он всё время считает максимальное количество трафика, которое может обработать текущий дата-центр.

Когда падает много серверов в дата-центре, балансер понимает, что этот дата-центр не может обработать весь трафик.

Всё работает, все счастливы. Тогда избыточный трафик начинает случайным образом распределятся в другие дата-центры.

Как мы это делаем: публикация релизов

Теперь о том, как мы публикуем внесённые в сервис изменения. Здесь мы пошли по пути упрощения процессов: выкатка нового релиза почти полностью автоматизирована.
Когда набирается определённое количество изменений в проекте, автоматически создается новый релиз и запускается его сборка.

Затем сервис выкатывается в testing, где проверяется стабильность работы.

Им занимается специальный сервис. Одновременно с этим запускается автоматическое тестирование производительности. Не буду о нём сейчас рассказывать – его описание достойно отдельной статьи.

Prestable – это специальный кластер, куда направляется нормальный пользовательский трафик. Если публикация в testing прошла успешно, автоматически запускается публикация релиза в prestable. Если он возвращает ошибку, балансер делает перезапрос в production.

Если всё нормально, то подключается человек: проверяет графики и результаты нагрузочного тестирования и затем запускает выкатку в production. В prestable замеряется время ответов и сравнивается с предыдущим релизом в production.

Всё лучшее – пользователю: A/B-тестирование

Не всегда очевидно, принесут ли изменения в сервисе реальную пользу. Чтобы измерять полезность изменений, люди придумали A/B-тестирование. Я немного расскажу, как это работает в поиске Яндекс.Маркета.

Пусть нашим параметром будет: market_new_functionality=1. Все начинается с добавления нового CGI-параметра, который включает новую функциональность. Затем в коде включаем эту функциональность при наличии флага:

If (cgi.experiments.market_new_functionality) {
// enable new functionality
}

Новая функциональность выкатывается в production.

В сервисе создается экcперимент. Для автоматизации A/B-тестирования есть выделенный сервис, который подробно описан здесь. Проценты задаются не для запросов, а для пользователей. Задается доля трафика, например, 15 %. Также указывается время эксперимента, например, неделя.

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

Также он автоматически считает выбранные метрики. В результате сервис автоматически добавляет аргумент market_new_functionality=1 к 15 % пользователей. На основании выводов принимается решение о выкатке в production или доработке. После окончания эксперимента аналитики смотрят на результаты и делают выводы.

Ловкая рука Маркета: тестирование в production

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

Есть решение: флаги в CGI параметрах можно использовать не только для A/B-тестирования, но и для проверки новой функциональности.

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

Схема работы сервиса представлена ниже:

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

В Стоп-кране можно выставлять два вида значений:

Применяются, когда выполняется одно из значений. 1) Условные выражения. Например:

{ "condition":"IS_DC1", "value":"3",
},

Значение «3» будет применяться, когда запрос будет обрабатываться в локации DC1. А значение «4», когда запрос обрабатывается на втором кластере для сайта beru.ru.

Применяются по умолчанию, если не выполнено ни одно из условий. 2) Безусловные значения. Например:

value, value!

Если значение заканчивается на восклицательный знак, ему присваивается повышенный приоритет.

Затем применяет значения из Стоп-крана. Парсер CGI-параметров разбирает URL.

Применяются значения со следующими приоритетами:

  1. С повышенным приоритетом из Стоп-крана (восклицательный знак).
  2. Значение из запроса.
  3. Значение по умолчанию из Cтоп-крана.
  4. Значение по умолчанию в коде.

Флагов, которые указываются в условных значениях, много – их хватает на все известные нам сценарии:

  • Дата-центр.
  • Окружение: production, testing, shadow.
  • Площадка: market, beru.
  • Номер кластера.

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

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

Мы пытаемся бороться с неправильным использованием. У этого сервиса есть и минусы: разработчики его очень любят и часто пытаются все изменения запихнуть в Стоп-кран.

При этом у вас ещё остаются сомнения, и вы хотите проверить код в «боевых» условиях. Подход со Стоп-краном хорошо работает, когда у вас уже есть стабильный код, готовый к выкатке в production.

Для разработчиков есть отдельный кластер, который называется «теневой кластер». Однако Стоп-кран не подходит для тестирования в процессе разработки.

Тайное тестирование: теневой кластер

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

Туда летит нормальный пользовательский трафик. Мы получаем тестовый кластер, который находится в настоящих «боевых» условиях. Железо в обоих кластерах одинаковое, поэтому можно сравнивать производительность и ошибки.

Поэтому не страшно совершить ошибку. А поскольку балансер полностью игнорирует ответы, то конечные пользователи ответов от теневого кластера не увидят.

Выводы

Итак, как же мы выстроили поиск Маркета?

Так можно и масштабировать только те компоненты, которые нам нужны, и делать компоненты более простыми. Чтобы всё шло гладко, мы выделяем функциональности в отдельные сервисы. И значительная экономия железа при таком подходе – очевидный плюс. Отдельный компонент легко отдать в другую команду и разделить обязанности по работе над ним.

Нам также помогает теневой кластер: можно разрабатывать сервисы, тестировать их в процессе и при этом не беспокоить пользователя.

Нужно изменить конфигурацию на тысяче серверов? Ну и проверка в production, конечно. Так можно сразу выкатить готовое сложное решение и сделать откат на стабильную версию, если возникнут проблемы. Легко, используем Стоп-кран.

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

Теги
Показать больше

Похожие статьи

Добавить комментарий

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

Кнопка «Наверх»
Закрыть