Как рекомендательный движок ежегодно экономит 200'000 CPU в инфраструктуре Рекламы
Я отвечаю за инфраструктуру рекламных технологий в Яндексе. Спойлер: про Perl говорить не будем, Perl-а почти не осталась. Меня Леша спросил, я просто не уверен, что там совсем ни одной строчки не осталось, но почти ничего не осталось.
Так вот, инфраструктура рекламных технологий включает в себя база данных, стриминговые процессинги, ML-инфраструктуру и рекомендательный движок. И мы будем говорить именно про рекомендательный движок, за которым я в разных ролях наблюдаю уже больше десяти лет. Он пережил взлеты и падения, так что точно есть чем с вами поделиться. Пусть вас не смущает, что здесь сказано про ежегодную экономию, здесь не будет про какие-то методологии, процессы. Сфокусируемся именно на конкретных идеях, которые на нашем масштабе, а это порядка миллиона CPU, приносят конкретный профит. Поговорим про деградацию, про улучшение локальности данных и кэши. Поговорим про микросервисы и почему оценивание микросервисов может приносить профит не только с точки зрения ускорения разработки, и вообще в целом про рекомендательные системы, как их сделать масштабируемыми и эффективными.
Давайте для начала поймём, как в целом реклама устроена. Есть клиенты, в нашем случае рекламодатели, которые рекламу заливают, приходят в интерфейс и заносят туда рекламные объявления, деньги, какие-то настройки крутят, это всё поподает в базу. В базе оказываются миллиарды объявлений, десятки терабайт данных, поверх которых работает рекомендательный движок. В него приходят больше миллиона запросов в секунду. Соответственно, это самая нагруженная, самая прожорливая часть, поэтому про нее дальше будем говорить в контексте экономии железа. Внутри есть разные события — показы, клики. Они все пишутся в логи и нужны в первую очередь для того, чтобы обучались ML-модели. Возникают там те самые 20 ГБ в секунду, благодаря которым обучаются модели, которые работают часть в офлайне, часть в онлайне, в зависимости от того, зависит от пользователя и запроса или не зависит.
Так вот, как мы теперь будем экономить железы в этой чудесной системе? Реклама — это правда рекомендательная система, я не спросто иногда оговариваюсь. Поэтому, если мы хотим сделать ее более эффективной, мы должны куда-то углубиться — углубиться либо в ML-архитектуру, в сами рекомендаторные кубики, либо в хайлоад бэкенд. И можно пойти туда и туда, я в целом покрою обе части, но сфокусирую именно на чисто бэкендерской, потому что ML вам еще сегодня хватит. При этом надо понимать, что в контексте рекламы, во-первых, любая экономия на нашем масштабе супер важна и ценна, даже один процент экономии в отдельном сервисе — это тысячи CPU-ядер, и в случае рекламы их можно разменивать на деньги, то есть взять более большие модели и получать за это больше профитом.
Так вот, я говорил, что рекомендательный движок – самая прожорливая часть. Давайте в неё зазумимся и посмотрим, как он был устроен ещё 5-6 лет назад. Есть база данных, в которые есть рекламные объявления, обогащенные всем подряд. Эти данные варились в некоторые слепки — гигабайтные файлы, — которые развозились по машинам с движком. Это было довольно больно, приходилось их подпинывать, мне в том числе и по ночам. Ну ладно, к этому вернемся еще.
Что в самом движке происходило? Эти данные не помещаются на одну машину, понятное дело, поэтому довольно давно движок шардирован по данным. На каждом шарде мы берем данные пользователя, которые мы знаем, и дальше хорошо бы взять всю базу объявлений и отранжировать эти объявления под конкретного пользователя на конкретной странице на конкретный поисковый запрос, если он есть. Но в случае миллиардной базы нельзя просто взять и запустить модель на каждом из этих объявлений, нужно что-то придумать. Обычно в мире принята каскадная, или многостадийная архитектура, когда вы в ранжирующую модель (внизу слева) передаёте не все объявления, а какие-то отфильтрованные.
Как мы тогда фильтровали (верхняя стадия, генерация кандидатов)? Мы фильтровали в первую очередь алгоритмически, потому что приходит человек в рекламный интерфейс и говорит, кому он хочет рекламу показывать. Он говорит, что хочет показывать рекламу тем, кто ввел поисковый запрос "купить цветы", или хочет показывать тем, кто в корзину товар положил, но пока не купил. Очень логично, да? Значит, настройки есть, давайте обратными индексами подберем этих кандидатов. В целом оно так и работало. Там есть маленький кубик ML, были небольшие эксперименты тогда, но не особо успешные, и в продукт они особо не выливались. И какая-то бизнес-логика между этим.
Короче, двухстадийное ранжирование. Сгенерировали кандидату для ранжирующей модели, запустили ранжирующую модель. Как мы в этой схеме экономили железо?
  • Во-первых, смотрели на флеймграфы. У нас тогда еще не был Perforator-а, у нас был некоторый более инвазивный способ, который дебагером подключался к продакшн-процессам. Нормально. Не сильно падало, и мы делали много выводов из этого. У нас сидел специальный эксперт, который, залипал во флеймграфы и экономил железо. Классно работало. Помимо этого нам нужно было бороться с потенциальной неэффективностями, которые приводили к таймаутам. Когда у вас есть алгоритмический способ подбора кандидатов, довольно легко представить ситуацию, когда их казалось слишком много по разным причинам. Соответственно, с одной стороны, нужно смотреть на базу, чтобы в базе не было трэша. Если в базе есть куча трэша в виде длинного хвоста, то можно грузить его не в каждое поколение индексов, хотя бы уже будет сильно легче.
  • Во-вторых, хочется как-то ограничить количество кандидатов, которые вылезают из этих обратных индексов. Я тогда сел, это единственный первый и последний раз жизни, когда я обучил ML-модель и выложил его в продакшен, вкомпилив в бинарник просто, три года там лежала. Модель, которая как-то очень просто и легко скорит кандидатов, которые вылезают оттуда, так чтобы их просто было не слишком много. Мозга в ней было немного, просто чтобы таймаутов не было. Казалось бы, смотрите, я вам рассказываю, 2019 год, какие-то две стадии ранжирования, смотрели на флеймграфы — красота! О чем дальше-то будем говорить и почему там что-то вообще крутое может появиться?
  • На самом деле мы довольно сильно продолбались в этом все. Мы вышли в некоторый локальный оптимум и не понимали до конца, как эта система должна дальше масштабироваться. И поэтому сели в лужу. Начало '20-го года для нас было довольно тяжелым. Вы все помните, что был ковид, а у нас параллельно с тем, что нас выгоняли из уютного офиса по домам, которые были неприспособлены под работу совершенно, у нас параллельно еще продакшен регулярно ложился. И лучшие люди вместо того, чтобы задачи конкретные пилить, регулярно дебажили, откуда ж там пики нагрузки.
    Почему так произошло?
  • Потому что, во-первых, это был неповоротливый монолит. Я сказал про шарды, но я показывал вам единый желтый прямоугольник. Его было тяжело понимать и развивать.
  • Во-вторых, эта система плохо деградировала. Буквально все, что мы могли сделать, если нас заваливает нагрузкой, — это ходить не во все шарды. У вас, допустим, 10 шардов, вы не сходили в один из них. Вы сэкономили 10% CPU, но и 10% денег тоже потеряли. Это неловкая ситуация, должно быть как-то по-другому.
  • И, наконец, неожиданная проблема с ранжированием. Вот там был нижний левый кубик про ранжирования. Эту модель нельзя было достаточно качественно увеличить в размерах. Нельзя было туда внедрить более классную модель, потому что тогда в нее нужно подавать меньше кандидатов. А какую ручку покрутишь, чтобы подавать в нее меньше кандидатов? Только вот ту мою ML-модель, которая вкомпилена в бинарник и обучена несчастным бэкендером в 2018 году. Тоже плохо масштабировалось.
  • Поэтому нам пришлось значимо перестроиться и многому научиться. Чему мы научились с точки зрения именно рекомендативной системы и того, как она должна быть устроена так, чтобы классно масштабироваться? Что появилось?
    1
  • Появился сверкающий фиолетовый кубик "прогноз профита". Мы в рекламе на самом деле зажирались в том смысле, что мы можем отдать пользователю 200 OK и пустой ответ, и пользователь не пожалуется никому. Потому что мы деньги, может быть, не заработаем немного, но у пользователя рекламы нет — ну что, красота. А рекламодателю главное, чтобы ему эффективный трафик пришел. Поэтому вообще-то мы можем прямо на старте обработки запроса оценить, этому пользователю вообще реклама интересна или как. Он вообще по ней кликнет или ему плевать на неё. Вот если плевать, то давайте не будем на него CPU тратить. Мы здесь правда в очень выгодном положении. Довольно простая и легкая ML-модель позволила здесь довольно много сэкономить железа
  • 2
  • Я рассказывал с болью в глазах про алгоритмическую генерацию кандидатов и какие-то длинные списки, которые приходилось как-то отсекать. Здесь нам помог ML, который развился настолько, что даже внедрился в продукт. Теперь можно прийти в интерфейс Яндекс.Директа и не покупать никакие слова, не говорить, кому я хочу рекламу показывать, а просто сказать "сделайте мне хорошо". И нейросетка сделает хорошо. Как именно она это делает? Ты буквально говоришь в нейросетке: "смотри, у тебя есть база пользователей, у тебя есть база рекламных объявлений, сопоставь каждому из этих объектов точку в многомерном пространстве. Пускай каждый пользователь станет синим шариком, а каждый рекламное объявление коричневым кубиком. И пускай их близость в этом многомерном пространстве коррелирует с тем, насколько это рекламное объявление релевантно конкретному пользователю". И оно, блин, работает внезапно.Дальше приходит конкретный пользователь, вот этот сверкающий синий шарик посередине. И мы можем уже конкретным алгоритмом подобрать к нему K ближайших рекламных объявлений. И это K ещё можно регулировать. Вот это офигенно работает и постепенно вытесняет алгоритмические способы генерации кандидатов.
  • 3
  • Мы расширились с точки зрения размера ранжирующей модели. На самом деле, если в литературе читать рекомендательные системы, вы там видите, как правило, три стадии — это retrieval, генерация кандидатов, preranking и ranking — предварительное ранжирование и, собственно, ранжирование. Мы добавили дополнительное ранжирование легкой моделью. Кандидатогенерация выпилнула много объявлений — тысячу, десяток тысяч. Их можно отправить в лёгкую модель, качество которой наращивать в принципе не обязательно. Она позволит вам оставить какой-то не слишком большой топ. И дальше, когда это всё смёржиться со всех шардов, можно запустить уже сколь угодно тяжёлую модель и как балансировать между ними. И оно правда классно масштабируется, и мы ещё и железо на этом сэкономили, не только выиграли качество. Красота!
  • 4
  • Осталось как-то раскрыть тему про подпинывание гигабайтных фаликов по ночам, да? Мы теперь стали не принимать данные из базы раз в час, а мы поняли, что на самом деле большая часть наших индексов, которые тогда были сериализованными массивами и деревьями, как-то друг с другом связанными, что на самом деле нам нужно key-value хранилище. Ты хочешь отранжировать рекламное объявление, у тебя есть его идентификатор, достань ты данные по нему, достань те факторы, которые нужно в ранжирование передать. Нужен key-value сторадж, который будет выдерживать достаточно высокие нагрузки. Если вы перемножите миллион RPS и тысячу кандидатов, то получите 10 в 9 степени. Наверное, многовато, но можно кэшами обложиться, эти кэши прогревать, инвалидировать, и будет нормально. Вот прям хорошо оно работает.
  • Это в целом те изменения, которые мы за пять лет повнедряли и получили достаточно хорошую масштабируемую систему. Давайте их суммаризируем.
  • Во-первых, прогнозируем профит в самом начале запроса. Если человеку рекламу не интересна, ничего не делаем.
  • Во-вторых, генерируем кандидатов с помощью метода K соседей и нейросеточек.
  • В-третьих, делаем рекомендательную систему трёхстадийной. Разбиваем на генерацию кандидатов, предварительное ранжирование и, собственно, анжирование.
  • И, наконец, не совсем про рекомендации, но в целом актуально для многих хайлоадов. Схема с подвозом многогигабайтных блобов под серверы и подкладывание в оперативку плохо масштабируется и приносит много боли, например, прешардирование. Не рекомендую.
  • Я закончил с ML, а теперь про бэкенд наконец-то обещанное. Какие конкретно масштабные приёмы позволили нам сэкономить 200 тысяч CPU? Я проверил, мы в течение последних трёх лет каждый год по 200 тысяч CPU отдавали. Какие конкретно методики нам здесь помогают, которые, мне кажется, актуальны для всех, но в разной степени?
    Во-первых, у нас есть супербазовый слой на уровне веб-сервера и шедулера, в который мы закопались и который внедрили во все наши крупные runtime-сервисы, мы сэкономили 10% железа буквально из всех наших runtime-сервисов. И собираемся за несколько месяцев еще 10% сэкономить. Это довольно удивительно, что по истечении более чем десяти лет развития хайлоад-сервисов можно взять и вынуть 20% железа просто так. Понятно, что нужно как-то расширить сознание, чтобы это могло быть возможным. Понятно, что недостаточно будет просто взять Perforator и посмотреть на флеймграфы. 20%, наверное, вы бы давно уже туда зарубились и что-нибудь там соптимизировали.
    На что нужно смотреть, кроме как на CPU такты, когда вы действительно хотите уменьшить затраты на железо? Надо смотреть на утилизацию. Если у вашего сервиса утилизация 30%, то это в каком-то роде стыдно с точки зрения затрат на железо, то есть 70% простаивает. Это мне сейчас легко говорит, но когда я три года назад бегал по плану эвакуации с этой идеей, чтобы как-то проблемки с железом порешать, мне говорили "отстань, ты что не понимаешь, 40% утилизации — это просто верх эволюции бэкенда, ну не может быть больше. Ну вот смотри, мы наливаем RPS, и у нас всё валится. Ну не может быть больше. Какое там железо? Отстань, короче, мы тебе наоптимизируем". Ну вот, спустя три года мы все-таки поняли, что утилизация может быть выше 40%.
    Давайте поймем, почему все-таки такая типичная картинка часто у многих происходит. Нужно понимать, что утилизация шумная и она иногда бывает супер низкой, иногда супер высокой, поэтому если она средняя 70%, то в пиках она будет иногда вылезать за 100. Окей, не бывает утилизации выше 100%, но запрошенный капасити будет превышать тот капасити, который сервер сможет обеспечить. Поэтому, чтобы утилизация могла быть 70%, нужно уметь выживать при более чем 100%. И вопрос в том, как именно.
    Здесь вы можете копнуть, конечно, в шедулер, который по тредпулам распределяет запросы. Вот приходит много запросов в ваш сервис, и есть конкретный тредпул пользовательского бинарника, который обслуживает эти HTTP-запросы. Дальше вопрос, что происходит, когда их становится слишком много? Типичная мисконфигурация, которая была и у нас — стыдно говорить, но она была у нас, — и во многих известных фреймворках тоже такая настройка существует, что когда слишком большая очередь возникает, то деградация происходит по времени обработки каждого запроса. Всем становится одинаково плохо, все хомячки страдают в этой очереди, никому здесь нехорошо. Кроме того, может быть, кто просто ожидает в инвалидной колеске снаружи. Мы, на самом деле, буквально подкрутили наш шедулер так, чтобы деградация происходила по длине очереди, а те запросы, которые уже начали обрабатываться, по ним SLA соблюдался. Это первый важный шаг. Мы хотя бы теперь контролируем происходящее.
    Дальше, если всё плохо, хомячки начинают выстраиваться в длинную-длинную очередь. Если мы хотим уметь выживать в такой ситуации больше, чем несколько миллисекунд, нам нужно уметь эту очередь прореживать. Давайте вспомним, что мы уже научились умно деградировать. У нас есть специальный порог, который говорит, что, если на конкретном запросе профит не слишком большой, можно его не обрабатывать. И это нам здесь пришлось очень кстати, потому что дальше можно соорудить некоторую машинерию следующего вида. Вот есть целевая метрика, load average или размер очереди. Мы смотрим, что с этой метрикой происходит, как она флуктуирует. И в зависимости от этого можно как-то двигать управляющее воздействие, то бишь этот самый порог отсечения запросов. И нужна какая-то штука, которая будет уметь это нормально делать, и SRE команда не умрет от того, что там какая-то супер штука работает.
    Внезапно нам здесь помогла инженерная вещь, не айтишная, называется PID-контроллер. Он основан на дифференциальных уравнениях с небольшим количеством параметров. Он очень лёгкий и как раз умеет эти преобразования осуществлять. Довольно простая формула. Благодаря тому, что у нас есть это умное отсечение, которое позволяет выкинуть 15% запросов за 1% просадки качества, мы стали счастливо жить и подняли нашу утилизацию. Мы, правда, не то чтобы все железо отдали, потому что никто лучше нас не умеет железо утилизировать, реклама же прямо железо в деньги конвертирует. Тем не менее, утилизацию повысили и железо переиспользовали.
    Когда мы говорим про утилизацию, надо думать не только про один конкретный сервер, но и про весь кластер целиком, который может быть довольно гетерогенным. Там могут быть процессоры от разных производителей. И в тот момент, когда вы хотите отправить ваш запрос в какую-то из реплик какого-то шарда, вы можете выбрать в какую реплику, и, соответственно, можете больше запросов наливать в ту машину, которая более производительная. И вот эту часть балансируя, вы получите уменьшение дисперсии в утилизации на масштабах всего кластера. Мы ещё, на самом деле, поэкспериментировали с этой штукой и свели к очень простой формуле, и я удивлен искренне, что SRE команда очень рада этим внедрением и счастливо спит. То есть как будто бы нам накрутили какого-то безумия, залезли в шедулер, но нет, и SRE-шники счастливы, честное слово. Рассказывали мне с улыбкой про все эти штуки.
    Теперь давайте поговорим про перекладывание данных. Мы все любим перекладывать json-ы и не только json-ы, но это часто то место, где как раз можно многое поэкономить. Мы как раз на этих оптимизациях, которые сейчас расскажу, сэкономили 18% CPU от основного движка и где-то 5% в некоторых других местах, но в среднем где-то 10% действительно получается.
    Давайте немножко откачусь в историю, когда у нас только шарды появлялись. Вот когда нету в сервисе шардирования, в него пришел запрос, он отдал ответ. Там какие-то структурки есть, они как-то по памяти бегают, всем плевать. Как только добавляются шарды, нужно эти структурки по сети пересылать. Есть структура Banner, нужно ее отправить и потом принять, как-то сложить, посортировать. Что в этот момент делать харкордный плюсовик в 2010 году? К сожалению, он делает memcpy. Если вы не знаете C++, там есть вектор, это последовательный набор структур в памяти. Если у вас баннер — это плоская структура, то есть просто кусок памяти, в котором они лежат. Давайте возьмем этот кусок памяти, вызовем base64.encode и отправим по сети, а там decode сделаем.
    Что же может пойти не так? Очень много что может пойти не так. Во-первых, тут нет никакой обратной совестимости, вам нужны синхронные релизы. Во-вторых, как только структура становится неплоской, приходится представить её в костомном формате и страдать. Короче, я страдал, очень не рекомендую. Надеюсь, у вас нигде такого нет. Прошлый век. Ужас.
    Мы переехали на Protobuf. Очень было тяжело, но переехали. Очень логичное решение — взять Protobuf, если нужно переслать данные по сети. Особенно, если у вас один язык Вот у вас есть структурки. Пишем message под каждую структурку, сериализуем, десериализум. Что же может пойти не так? Смотрим на флеймграфы, и десятки процентов CPU-тайма тратятся на эту сериализацию и десериализацию. Вспоминаем наши масштабы, сотни тысяч CPU жжётся просто непонятно на что — на перекладывание туда-сюда этих байтиков.
    Окей, упаковка там есть какая-то, она там эффективна по размеру сообщения, но тем не менее у нас больше всего болит именно за CPU-тайм. Казалось бы понятно, что с этим делать дальше. Расскажу про FlatBuffers. Это формат, который более эффективен с точки зрения чтений. Он не требует десериализации, если вам нужен доступ к какому-то отдельному полю. Там просто нет десериализации. И соответственно, и сериализация тоже становится легче. При этом он также поддержит обратную совместимость, но понятно, что, поскольку он не запаковывается суперплотно, сообщения оказываются тяжелее. Но ладно, CPU экономится.
    Мы переехали на FlatBuffers, значит, стало хорошо, вроде подвыдухнули. Но мы же не хотим теперь иметь отдельно структурки, которые гоняются по памяти, и отдельно флэтбафферные сообщения, которые пересылаются по сети, потому что тогда придется перекладывать структурки в флэтбафферные сообщения. Поэтому мы сразу стали работать во всем нашем рантайме с этими флэтбафферными месседжами. Логично, да?
    Это удобно с точки зрения написания кода, но дальше у вас чтение плюсовых, даже сишных структур заменяется на чтение из флэтбаффера, и оно начинает светиться на флеймграфах. Берем конкретную функцию, в которой много чтений параметров рекламных компаний, и видим (справа внизу), что почти 25% занимают флэтбафферные чтения. Это неприятно, что-то как будто мы снова где-то потеряли, и с этим хочется что-то поделать.
    Почему так происходит? FlatBuffers старается быть универсальным форматом, и одно конкретное чтение превращается в худшем случае в 4 чтения, потому что там есть виртуальные таблицы, надо посмотреть на размер, сравниться. Короче, если вы харкорный плюсовик, вы понимаете, что чтение эффективное, кажется, выглядит не совсем так. Не таким его хочется видеть. Особенно, когда там вложенные чтения, совсем грустно становятся, а еще из-за этих виртуальных таблиц небольшие месседжи, которых у нас довольно много, слишком раздуваются и становятся неэффективными уже по сети.
    В этот момент мы вздохнули, поняли, что мы перепробовали все, что могли, и нам нужно писать собственный формат. Мы взяли лучшее из двух миров. Мы из Protobuf взяли формат самой схемы, мы взяли интерфейс, который не возвращает null-поинтеры, соответственно, нельзя их разыменовать. И мы реализовали автоматическое перекладывание из Protobuf в этот новый формат, который мы назвали YaFF. А из FlatBuffers мы взяли сам подход, само отсутствие необходимости в десериализации, сам способ доступа к данным, но мы его соптимизировали, и саму раскладку полей. тоже её соптимизировав.
    За счёт чего эти оптимизации? За счёт того, что нас только C++ и только современные архитектуры процессоров. Не нужно париться с выравниванием. И пошаманили, чтобы кэши лучше использовались, переместили виртуальную таблицу после сообщения, а не до, всякие такие вещи, и вдвое улучшили и CPU-тайм. Окей, ладно, не сишные структуры, но уже достаточно офигенно, что кажется, что можно выдохнуть. И еще по размеру сообщения тоже сэкономили на треть. Есть, если что, конкретные бенчмарки.
    Вообще, довольно универсальная, казалось бы, отделимая штука. Мы в целом даже думали её в опенсорс выложить. Если вы чувствуете, что у вас есть какой-то интерес, потребность у вашего соседа — подойдите, поговорим, потому что нам нужно понимать, это нужно кому-то или нет. Если нужно, то отзадачимся. Собственно, автор этой штуки, Даня Гавриловский, рассказывал год назад на Highload++ про то, как мы переезжали с гигабайтных блобов на вот это key-value хранилищи, и у него ровно в том месте возникла проблема с флэтбафферами. Он как раз там рассказывает, что, кажется, надо сделать свой формат. И вот, спустя год он его внедрил. Так что послушайте, там как раз уже некоторые вот эти штуки про эффективность более подробно раскрыты.
    Последняя история про микросервисы. Казалось бы, кто из нас не распиливал монолит на микросервисы. Но принято считать, что распил монолита — это компромисс между эффективностью и гибкостью и скоростью разработки, простоте онбординга, вот этим всем. Вы потеряете в эффективности, зато чудесный мир с микросервисами. Так вот, у нас нет такой жесткой установки, что мы обязательно должны распиливать наш монолит на маленькие куски и пофигу на железо, потому что не пофигу на железо на таком количестве железа. И мы при этом видим конкретные кейсы, когда мы отпиливаем монолит, и суммарная эффективность улучшается, и суммарное железо уменьшается. Вот, про такой кейс хочется рассказать.
    Небольшая водная. Я говорил, что у нас есть кандидаты, которые попадают в франжирование, и там есть какая-то бизнес-логика. Какая бизнес-логика очень важна в рекламе с точки зрения соблюдения всех наших обещаний рекламодателям, например? Важно, чтобы их деньги в минус не уходили. Поэтому нам нужны максимально актуальные данные о том, сколько денег на счете осталось. Понятно, что в схеме с многогигабайтными блобами, которые я подпинываю по ночам, она будет работать плохо, и эти данные будут неактуальными.
    Поэтому у нас испокон веков был маленький сервисочек — Fast Counters, где был небольшой unordered_map, небольшой словарь "рекламная компания → сколько денег осталось" c максимально актуальными данными. Туда прямо HTTP-запросами эти данные слались. Мы, когда смотрели на флеймграфы, мы увидели, что запрос в этот сервис и парсинг ответа этого сервиса сильно отличаются с точки зрения производительности. Хотя запрос и ответ очень близки друг к другу по размерам, парсинг ответа был в пять раз дольше, чем формирование запроса. И это уже наводит на определенные мысли, что что-то у нас не так, и кажется, пока мы ждали ответа этого сервиса, пока было переключение контекста, у нас кэши все вымылись. Вымылись кэши с данными, вымылись кэши с кодом. Поскольку сервис большой, кэши по коду тоже имеет значение. Еще важно, что этим занимался отдельная команда, которая хотела отдельный алгоритм развивать. Короче, надо брать и выделить в микросервис.
    Обвели голубеньким прямоугольником, отселили в микросервис. Казалось бы логично, да? Вот у нас есть id-шники этих объектов, сходили в базу, получили данные, отправили данные в этот микросервис, мы его назвали Метроном, не спрашивайте почему. Потом получили вердикты и пошли с ними дальше работать. Казалось бы замечательно, да? Обвели, отселили. Красота.
    Запускаем мы это в эксперимент и графичек времени выполнения запросов чтения в этот сервис быстро выходит за секунду и улетает в небеса. Не запустили эксперимент, как видите.
    Что пошло не так? Дело в том, что данных дофига, они кэшировались при запросе в базу, то есть мы не ходили в базу на каждом запросе, потому что данных очень много, мы не могли бы так делать, и этот ответ не могли бы получать на каждом запросе. Поскольку в запросе в Метроном, там дальше-то эти быстрые счётчики, там уже кэшировать не получится. Поэтому на каждом из запросов, которых миллион в секунду, 70 мегабайтов данных радостно отправлялись в этот микросервис. Так не работает, кажется, в этом мире. Фиpика немножко не позволяет пока что.
    Поэтому надо было поумнее к этому подойти. Мы сказали "окей, ладно. Кажется, вот это вот туда-сюда гонять невыгодно. И надо просто вот все, что связано с конкретно этими объектами, всю эту бизнес-логику надо туда переселить. И в базу ходить прямо из этого сервиса". Казалось бы, сейчас очевидно звучит, но почему-то накодили ведь, запустили, обломались, переписали код. Неочевидно. Вот так красиво. Делайте так.
    Это уже все классно заработало, ответы парсятся меньше чем за 1 мс, замечательно. Но это еще не все. Мы упариваемся за эффективность. Мы же говорили, что должно быть выгодно оцелить в отдельный микросерез. Окей, давайте копнем глубже.
    У нас данные не просто рекламная компания, и сколько денег потрачено. Там есть какая-то команда ML-щиков, которая что-то улучшает. Одни модели пробуют, другие модели пробуют. Значит, им нужны эксперименты. Значит, нужно для каждого объекта хранить стейт в разрезе каждого эксперимента в отдельности. Но при этом на каждом конкретном поисковом запросе будет работать только один эксперимент. Поэтому вы пойдете в данные одной рекламной компании, там возьмете маленький кусочек про конкретный эксперимент. Пойдете в другую рекламную компанию, там возьмете маленький кусочек данных про конкретный эксперимент. И это суперплохо с точки зрения использования кэшей процессора. Тому самому несчастному хомячку приходится на каждую полку подняться и оттуда взять одну коробку. Так, понятно, делать не надо, но просто с матрицами так не работают, а это в каком смысле матрица.
    Хочется транспонировать, как говорят ли в линейной алгебре, — сделать первичным ключом эксперимент, а потом уже компанию. Но поскольку мы не влияем на то, как лежат данные в самой базе, и понятно, что там выгодно все-таки это по компаниям складывать, нам нужен кэш, в котором данные уже будут лежать так, как нам удобно. Поэтому, пуская данные про каждый эксперимент лежат рядом, мы их будем сразу доставать и использовать.
    Это уже работает гораздо эффективнее. Когда мы это запустили, самая нагруженная часть значимо ускорилась. И без отдельного микросервиса мы бы такой кэш нормально внедрить не смогли.
    Эта штука эволюционирует. У нас сейчас целевая архитектура, которую мы, я думаю, шипнем в ближайшие несколько месяцев, ещё простая. Нам не нужен отдельный микросервер со счётчиками, это просто часть этого микросервиса. База, на самом деле, конкретно в этом случае, маленькая, там буквально 20 гигабайт, и можно ее просто локально положить в оперативку. И это уже не будут супер-большие блобы, у нас там уже инкрементальность, все дела, там уже легко, и сеть сильно не грузится. Ну и локально конечно все еще нужен, чтобы эффективно использовать память.
    Давайте выводы про микросервисы. Микросервис может повысить суммарную эффективность сервиса и уменьшить суммарные затраты за счёт улучшения локальности данных. Чтобы с этой локальностью данных поработать, надо обращать внимание на то, как у вас происходит чтение. И, если что, можно использовать локальный кэш, который вашу CPU-bound задачу, когда вы ходите по разным местам, превращает в memory-bound задачу. Вот вы зафигачили большой кэш, он уже устроен достаточно эффективно, и теперь можете его использовать, CPU тратится меньше. Вы можете регулировать размен CPU на память. И вообще в целом отселение отдельного микросервиса позволяет накручивать кэши, позволяет добавлять батчевание, позволяет делать что угодно, что важно для этой конкретной доменной области и что поможет здесь повысить эффективность.
    1
  • Микросервис повышает локальность по данным
  • 2
  • Но структура чтений всё ещё важна
  • 3
  • Дополнительный кэш превращает задачу в memory-bound
  • 4
  • Отдельный сервис можно затачивать под конкретные задачи: кэши, батчевание и прочее
  • Я закончил рисовать сложные схемы, здесь можно выдохнуть, и сейчас подведу итоги:
    1
  • Я немножко в начале закидываю удочку, что скажу немножко по процессу. Используйте Perforator, смотрите на флеймграфы, используйте PGO. Мы когда Perforator выкладывали в опенсорс, мы говорили, используйте Profile Guide Optimization, сэкономите в среднем 10% CPU. Мы потом замерили на рекламе, иправда, 10% CPU за счет Profile Guide Optimization экономится. Какой-то там разброс будет, но в целом очень полезная штука.
  • 2
  • Есть инфраструктурная команда, которая что-то оптимизирует, а всегда есть какие-то люди, которые добавляют новую логику в этот сервис. Не позволяйте им испортить ваш замечательный сервис своим неэффективным кодом. Здесь могут быть прогрузки, нагрузочные тесты, A/B-тесты со специальной метриками, мы это всё используем.
  • 3
  • Когда создаёте микросервис, не думайте, что обязательно станет хуже по железу. Может стать лучше, если заморочиться.
  • 4
  • Оптимизируйте перекладывание данных, есть много разных способов, хотя бы из Protobuf в FlatBuffers, если у вас много чтений, а может быть мы и наш YaFF выложим, и тогда можно будет его использовать, если для вас актуально,
  • 5
  • Повышайте утилизацию ваших сервисов и деградируйте с умом.
  • Всем спасибо.
    Вопрос:

    Мне понравился подход с микросервисами, что не микросервисы ради микросервисов, а именно выделено то, что нужно, потому что я постоянно говорю про накладные расходы. Про YaFF: очень хочется его увидеть, но я так понял, что это только для плюсов, да, у вас сделано?

    Ответ:

    Сделано только для плюсов, но как бы опенсорс тем и хорош, что...

    Вопрос:

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

    Ответ:

    Ага, так, обсудим в кулуарах на кофе-брейке, окей.

    --

    Вопрос:

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

    Ответ:

    Мы очень любим проводить эксперименты, и в целом, когда мы видим, что мы взяли какую-то ML-модель, которая как-то управляет параметрами, она почти всегда работает лучше, и в среднем точно работает лучше. Но при этом ты же не можешь в своем продукте сказать: "вот у вас был звездолет, вы могли им управлять как хотите, но там вообще-то сидят специалисты по маркетингу, крутые люди с образованием, как бы". И отнять в них 100 ручек — это довольно стремно. Поэтому там есть некоторые компромиссы всегда.

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

    Вопрос:

    Про фильтр, который по профиту отсеивает, показывать или не показывать. Как мне, как человеку, который формирует рекламную компанию, быть уверенным, что мои объявления не будут отфильтрованы вот по этому критерию, что они не приносят профита?

    Ответ:

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


    Вопрос:

    Как вы поступали с теми сервисами, которые хотят много CPU иногда, и поэтому должны держать на этом хосте или на этой железке какой-то излишний резерв?

    Ответ:

    Простой ответ — у нас таких практически нет. У нас есть сервисы, которые выгоднее залить железом на небольших объёмах, чем в них изобретать какую-то деградацию, потому что там просто будет больно вот это всё. Такое есть, но мы их просто не трогаем.

    Вопрос:

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

    Ответ:

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


    Вопрос:

    Я так понял, вы около 5 лет всей этой истории занимались, да?

    Ответ:

    Лично я?

    Вопрос:

    Вообще команда.

    Ответ:

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

    Вопрос:

    То есть у вас за это время сервера не менялись. Вы говорите так смело, мы сэкономили столько-то процентов, столько-то процентов. А сервера живут 3-4 года, потом...

    Ответ:

    Сервера постоянно меняются. Я скажу хуже, мы стали жрать больше железа за это время. Потому что параллельно растет трафик, бизнес растет, но мы обгоняем эту скорость. Мы растем не на 30% железа каждый год.

    Вопрос:

    А теперь, главный вопрос, а как вы убедили бизнес, что вот эта разработка вам когда-то через год принесет денег?

    Ответ:

    Очень легко. Мы в 20-м году просто лежали кучу времени.

    Вопрос:

    Это само так получилось, получается, да?

    Ответ:

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


    Вопрос:

    Я посмотрел вашу выручку за 2024 год, именно Поиска, а не всего Яндекса. Это 439 миллиардов, плюс 30% год году. Экономия твоих 200 тысяч CPU на год по ценам Яндекс Облака, а у вас стопудово дешевле, где-то 2 миллиарда. Экономия вот этой инженерной бизнес-задачи 0.5% при том, что вы растете 30% год к году. Побочка все-таки экономия 200 CPU решения какой-то инженерной задачи, или это была именно цель? Почему я спрашиваю? Потому что я хочу продать своему бизнесу экономию миллиарда в год. Мне говорят, у нас тут CAPEX на сто миллиардов, чтобы дата-центр построить. Вообще нафиг не надо. Давай твои инженеры будут заниматься другой задачей.

    Ответ:

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

    Вопрос:

    Это из-за 2022-го года и из-за времени поставок железа?

    Ответ:

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

    Вопрос:

    Я могу сказать, что все-таки это была побочка, но которую ты уже превратил в бизнес-историю.

    Ответ:

    Получается, что да.