Привет! Меня зовут Василий Копытов, я руковожу группой разработки рекомендаций в Авито. Мы занимается системами, которые предоставляют пользователю персонализированные объявления на сайте и в приложениях. На примере нашего основного сервиса покажу, когда стоит переходить с Python на Go, а когда нужно оставить всё как есть. В конце дам несколько советов по оптимизации сервисов на Python.

Как работают рекомендации на главной Авито 

Любой человек, который зашёл на главную страницу сайта или приложения, видит персональную ленту объявлений — рекомендации. Нагрузка на наш основной сервис рекомендаций representation, который отвечает за формирование бесконечной ленты айтемов на главной, порядка 200 000 запросов в минуту. Весь трафик за рекомендациями — порядка 500 000 запросов в минуту.

Так выглядят рекомендации в приложении и на сайте
Так выглядят рекомендации в приложении и на сайте

Сервис representation выбирает самые подходящие объявления из 100 миллионов активных объявлений (айтемов) под каждого пользователя. Рекомендации формируются на основе всех действий человека за последний месяц.

Representation работает по такому алгоритму:

  1. Сервис обращается к хранилищу истории пользователя и забирает из него агрегированную историю и интересы. 

Интересы — это набор категорий и подкатегорий объявлений, которые в последнее время просматривал человек. Например, детская одежда, товары для рукоделия или домашние животные. 

  1. Затем передаёт историю и интересы, как набор параметров, нескольким ML-моделям первого уровня

ML-модели первого уровня — это нижележащие сервисы. Сейчас у нас 4 таких модели. Они предсказывают айтемы по различным алгоритмам машинного обучения. На выходе от каждого сервиса получаем список id (рекомендованных айтемов).

  1. Фильтруем id на основе истории пользователя. В итоге получается примерно 3000 айтемов на один аккаунт.

  2. И самое интересное — representation внутри себя использует ML-модель второго уровня на основе CatBoost для ранжирования объявлений от ML-моделей первого уровня в realtime.

  3. Из данных готовятся фичи — параметры для ранжирования рекомендаций. Для этого по id айтема идем за данными в хранилище (1 TB шардированный Redis). Данные айтема — title, цена и много еще чего, порядка 50 полей. 

  4. Сервис передает фичи и айтемы в ML-модель второго уровня на основе библиотеки CatBoost. На выходе получаем отранжированную ленту объявлений. 

  5. Далее representation выполняет бизнес-логику. Например, поднимает в ленте те объявления, для которых оплачено премиум-размещение (boost VAS).

  6. Отдаём сформированную ленту рекомендаций пользователю, в ленте около 3000 объявлений.

Алгоритм, по которому формируется лента рекомендаций 
Алгоритм, по которому формируется лента рекомендаций 

Почему мы решили переписать сервис рекомендаций

Representation — один из самых высоконагруженных сервисов в Авито. Он обрабатывает 200 000 запросов в минуту. Сервис стал таким не сразу: мы постоянно внедряли что-то новое и улучшали качество рекомендаций. В какой-то момент он начал потреблять почти столько же ресурсов, сколько и остаток монолита Avito. Нам стало тяжело выкатывать сервис днём, в часы пик, из-за нехватки ресурсов в кластере — в это время большинство разработчиков деплоило свои сервисы. 

Карта взаимодействия сервисов Авито. Размер круга показывает, сколько CPU потребляет сервис
Карта взаимодействия сервисов Авито. Размер круга показывает, сколько CPU потребляет сервис

Одновременно с ростом потребления ресурсов росло и время ответа сервиса. Во время пиковых нагрузок пользователь мог ждать свои рекомендации до 1,6 секунды — это 8-ми кратный рост за последние 2 года. Все это могло заблокировать дальнейшую разработку и улучшение рекомендаций.

Причины всего этого достаточно очевидны: 

  1. Большая IO-bound нагрузка. В representation каждый запрос состоит из примерно 20 корутин — блоков кода, которые работают асинхронно во время обработки сетевых запросов.

  2. CPU-bound нагрузка от realtime вычислений ML-моделью, которые полностью занимает CPU, пока происходит ранжирование объявлений.

  3. GIL - representation изначально был написан на однопоточном Python. На этом языке невозможно совместить IO-bound и CPU-bound нагрузки так, чтобы сервис использовал ресурсы эффективно. 

Как мы решали проблемы с сервисом рекомендаций

Давайте расскажу, что нам помогло жить под нашими нагрузками на Python:

1. ProcessPoolExecutor

ProcessPoolExecutor создает пул из ядер процессора — воркеров. Каждый воркер представляет собой отдельный процесс, который будет выполняться на отдельном ядре. В такой воркер можно передать CPU-bound нагрузку, чтобы она не тормозила другие процессы в сервисе. 

В representation мы изначально использовали ProcessPoolExecutor, чтобы разделить CPU-bound и IO-bound нагрузки. Помимо основного питонячьего процесса, которые обслуживает запросы и ходит по сети (IO-bound) мы выделили три воркера  для ML-модели (CPU-bound).

У нас есть асинхронный сервис на aiohttp, который обслуживает запросы и успешно справляется с IO-bound нагрузкой. ProcessPoolExecutor создает пул из ядер процессора — воркеров. Это отдельные процессы, которые будут выполняться на отдельном ядре. В такой воркер можно передать CPU-bound нагрузку, чтобы она не тормозила корутины в основном процессе сервиса и влияла на latency всего сервиса. 

Выигрыш по времени от использования ProcessPoolExecutor около 35%. Для эксперимента мы решили сделать код синхронным и отключили ProcessPoolExecutor. То есть IO-bound и CPU-bound нагрузка стала выполняться в одном процессе.

Без ProcessPoolExecutor время ответа сервиса выросло на 35%, тут и ниже все графики на 95 perc.
Без ProcessPoolExecutor время ответа сервиса выросло на 35%, тут и ниже все графики на 95 perc.

Как это выглядит в коде:

async def process_request(user_id):
    # I/O task
    async with session.post(feature_service_url,
                            json={'user_id': user_id}) as resp:
        features = await resp.json()

    return features

У нас есть асинхронный хэндлер который обрабатывает запрос. Для тех кто не знаком с async await - служебные слова, которые означают точки переключения корутин. 

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

def predict(features)
    preprocessed_features = processor.preprocess(features)
    return model.infer(preprocessed_features)
  
  
async def process_request(user_id):
    # I/O task
    async with session.post(feature_service_url,
                            json={'user_id': user_id}) as resp:
        features = await resp.json()
    # blocking CPU task
	  return predict(features)

Вдруг нам понадобилось выполнить cpu-bound нагрузку от ml модели. Функция predict. И вот на строчке 12 наша корутина заблокирует питонячий процесс пока не выполнится, соответственно все запросы на сервис встанут в очередь и время ответа сервиса вырастет как мы видели ранее.

executor = concurrent.futures.ProcessPoolExecutor(man_workers=N)
  
  
def predict(features):
    preprocessed_features = processor.preprocess(features)
    return model.infer(preprocessed_features)
    
    
async def process_request(user_id):
    # I/O task
    async with session.post(feature_service_url,
                            json={'user_id': user_id}) as resp:
        features = await resp.json()
    # Non blocking CPU task
	  return await loop.run_in_executor(executor, predict(features))

Тут появляется ProcessPoolExecutor со своим пулом воркеров, который решает эту проблему. На строке 1 мы создаем пул. На строке 15 берем оттуда воркер и перекидываем CPU-bound задачу на отдельное ядро, таким образом функция predict будет исполнятся асинхронно по отношению к родительскому процессу и не блокировать его. Самое приятное, что все это будет обернуто в обычный синтаксис async-await и CPU-bound задачи будут выполняться асинхронно наравне с IO-bound задачами, но под капотом будет дополнительная магия с процессами.

Видео, в котором подробнее рассказывается про CPU-bound задачи в Python.

ProcessPoolExecutor позволил нам уменьшить оверхед от realtime ml модели, но даже с ним в какой-то момент стало плохо. Первым делом начали с самого очевидного - профилирование и поиск узких мест.  

2. Профилирование сервиса и поиск узких мест

Даже если сервис пишут опытные программисты — в нём есть, что улучшать. Чтобы понять, какие участки кода работают медленно, а какие быстро, мы профилировали сервис с помощью профайлера py-spy

Профайлер строит диаграмму, на которой горизонтальные полосы означают, сколько процентов процессорного времени тратит участок кода. Первое, что увидели — это 3 столбика справа. Это как раз наши дочерние процессы для скоринга фичей ml моделью.

По flame графу мы увидели интересные детали: 

  • 7% времени процессор тратит на сериализацию данных между процессами. Сериализация — это перекодирование данных в байты. В Python этот процесс называется pickle, а обратный ему —  unpickle.

  • 3% времени уходит на накладные расходы ProcessPoolExecutor — подготовку пула воркеров и распределение нагрузки между ними.

  • 6,7% времени занимает сериализация данных для сетевых запросов в json.loads и json.dumps.

Помимо процентного распределения мы хотели узнать конкретное время, которое занимают разные участки кода. Для этого снова отключили ProcessPoolExecutor, запустили ML-модель для ранжирования синхронно. 

Без ProcessPoolExecutor ранжирование проходит быстрее, потому что всё процессорное время занимает только подготовка фичей и скоринг ML-моделью, нет оверхеда на pickle/unpickle и IO-wait (время на ожидание между переключением корутин)
Без ProcessPoolExecutor ранжирование проходит быстрее, потому что всё процессорное время занимает только подготовка фичей и скоринг ML-моделью, нет оверхеда на pickle/unpickle и IO-wait (время на ожидание между переключением корутин)

Но выросло время ответа самого сервиса по причинам описанным выше. Конкретный участок кода стал быстрее, но сам сервис медленнее.

После экспериментов выяснили:

  • Накладные расходы ProcessPoolExecutor составляют примерно 100 миллисекунд.

  • IO-bound запросы от корутин ожидают 80 миллисекунд, то есть корутина уснула и EventLoop до нее добирается вновь через 80 ms, чтобы возобновить ее выполнение. В representation три больших группы IO-bound запросов — итого 240 миллисекунд уходит на IO-wait.

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

3. Разделили cpu-bound и io-bound нагрузку на 2 отдельных сервиса

Одно из крупных изменений, которое мы попробовали, — убрать ML-модель в отдельный сервис rec-ranker. То есть остался наш сервис representation в котором  только сетевые запросы, а скоринг ml модели был на отдельном сервисе rec-ranker в который мы передавали все необходимые данные и возвращали скоры для ранжирования. Казалось что чуть снизим latency и будем раздельно масштабировать обе части.

Эксперимент показал: мы экономим время на работе модели, но получаем задержку в 270 миллисекунд при передаче данных по сети и json.loads/json.dumps. На один запрос нужно пересылать примерно 4 Мб, а для очень активных пользователей — до 12 Мб данных для ml модели. После масштабирования реплик rec-ranker стало не намного меньше, чем у старого  representation, а время ответа тоже самое, был mvp не с самой удачной архитектурой для проверки гипотезы. Для нашего кейса разделение на сервисы оказалось неудачным решением, поэтому мы вернулись к предыдущей реализации representation.

4. Оценили Shared Memory

В сервисе representation данные между процессами передаются через pickle/unpickle. Вместо этого в процессах, которые делятся данными, можно указать на общий участок памяти. Так экономится время на сериализации.

По максимальным оценкам можно выиграть примерно 70 миллисекунд на сериализации и еще примерно на такое же время - 70 миллисекунд уменьшается время выполнения запроса, так как pickle/unpickle - CPU-bound нагрузка и она лочила основной python процесс, который обрабатывал запросы от пользователей, то есть всего 140 миллисекунд. Этот вывод мы сделали на основе профайла: pickle/unpickle занимает всего 7% процессорного времени, большого профита от shared memory мы не получили бы.

5. Сделали подготовку фичей на Go

Мы решили проверить эффективность Go сначала на части сервиса. Для эксперимента выбрали самую тяжелую cpu-bound задачу в сервисе — подготовку фичей. 

Фичи в сервисе рекомендация — это данные айтема. Например, название объявления, цена, информация о показах и кликах. Всего около 60 параметров, которые влияют на результат работы ML-модели, то есть мы подготавливаем все эти данные для 3000 айтемов и отправляем в модель, она отдает скор для каждого по которому мы ранжируем ленту.

Чтобы связать код на Go для подготовки фичей с остальным кодом сервиса на Python, мы использовали ctypes.

def get_predictions(
  raw_data: bytes,
  model_ptr: POINTER(c_void_p),
  size: int,
) -> list:
  raw_predictions = lib.GetPredictionsWithModel(
    GoString(raw_data, len(raw_data)),
    model_ptr,
  )
  predictions = [raw_predictions[i] for i in range(size)]
  return predictions

Так выглядит подготовка фичей внутри Python. Модуль lib это скомпилированный гошный пакет в котором есть функция GetPredictionsWithModel в которую мы передаем байты с данными об айтемах и указатель на ML модель. Все фичи подготавливаются для модели гошным кодом.

Результаты нас впечатлили: 

  • фичи на go считаются в 20-30 раз быстрее;

  • весь шаг ранжирования ускорился в 3 раза учитываю лишнюю сериализацию десиарилизацию данных в байты;

  • ответ главной упал на 35 процентов.

Подготовка фичей на Go ускорила загрузку главной страницы сайта с 1060 до 680 миллисекунд
Подготовка фичей на Go ускорила загрузку главной страницы сайта с 1060 до 680 миллисекунд
Время на ранжирование рекомендаций ml моделью с подготовкой фичей. Тут еще нужно еще учитывать что в случае с Go у нас синхронный код и мы не используем ProcessPoolExecutor
Время на ранжирование рекомендаций ml моделью с подготовкой фичей. Тут еще нужно еще учитывать что в случае с Go у нас синхронный код и мы не используем ProcessPoolExecutor

Итоги

После всех экспериментов сделали три вывода: 

  1. Фичи на Go для 3000 айтемов на запрос считаются в 20-30 раз быстрее, экономия 30% времени.

  2. ProcessPoolExecutor тратит около 10% времени;

  3. Три группы io-bound-запросов занимают 25% времени на пустое ожидание.

  4. После перехода на Go сэкономим примерно 65% времени.

Переписали все на Go

В representation-go есть ML-модель. Нативно кажется что ml дружит только с питоном, но в нашем случае ml модель на CatBoost и у нее есть С API, которое можно вызывать из Go. Этим мы и воспользовались. 

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

if !C.CalcModelPrediction(
      model.Handler,
      C.size_t(nSamples),
      floatsC,
      C.size_t(floatFeaturesCount),
      catsC,
      C.size_t(categoryFeaturesCount),
      (*C.double)(&results[0]),
      C.size_t(nSamples),
) {
      return nil, getError()
}

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

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

Representation-go показал отличные результаты: 

  • Ответ главной страницы упал в 3 раза с 1280 до 450 миллисекунд;

  • Потребление CPU упало в 5 раз;

  • Потребление RAM снизилось в 21 раз.

Время ответа сервисов
Время ответа сервисов

Разблокировали дальнейшую разработку рекомендаций - можем дальше внедрять тяжелые фичи.

Когда стоит переписывать сервис с Python на Go

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

  1. в сервисе много CPU-bound-нагрузки; 

  2. при этом также много IO-bound нагрузки;

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

Если у вас есть только IO-bound нагрузка, то лучше остаться на Python. При переходе на Go вы почти не выиграете по времени, только сэкономите ресурсы, что при малых и средних нагрузках не так важно.

Если в сервисе используются обе нагрузки, но по сети передаётся не так много данных как у нас, есть два варианта:

  1. Использовать ProcessPoolExecutor. Накладные расходы времени будут не очень большими, пока сервис не гигант.

  2. Как нагрузка станет большой - разбить на 2 сервиса, для раздельного масштабирования.

Оптимизации сервиса, c чего нужно начать

Профилируйте ваш сервис. Используйте py-spy, как мы, или другой профайлер Python. Скорее всего, в вашем коде нет огромных неоптимальных участков. Но нужно внимательнее посмотреть все небольшие участки, из которых собирается приличный объём для улучшения. Возможно, переписывать весь код вам не понадобится.

Запуск py-spy в не блокирующем режиме: 

record -F -o record.svg -s --nonblocking -p 1

Это первый flame, который мы получили без всяких оптимизаций. Что первое тут бросилось в глаза — заметный кусок времени тратится на json валидацию запроса, которая в нашем случае не очень нужна, поэтому мы её убрали. Еще больше времени тратилось json loads/dumps всех сетевых запросов, заменили на orjson.

Ну и в завершении дам несколько советов:

  1. Используйте request validator с умом.

  2. Для парсинга используйте orjson для Python или jsoniter для Golang.

  3. Уменьшайте нагрузку на сеть — жмите данные(zstd). Оптимизируйте хранение, чтение/запись данных в БД (Protobuf/MessagePack). Иногда быстрее сжать, отправить и разжать, чем отправлять несжатые данные.

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

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


  1. Paskin
    30.08.2022 14:11
    +3

    Не совсем понятно, что же в конце концов изменилось в "подготовке фич". Создается впечатление, что после всех разговоров об IO и workers - задача свелась к оптимизации каких-то однопоточных преобразований массивов/списков...


    1. katletmedown
      30.08.2022 18:40

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


    1. kopytovsv Автор
      30.08.2022 21:25

      После всех разговоров об IO и workers в Python выяснилось, что только подготовка фич на Go из Python кода в лоб, последовательно в 1 процессе без ProcessPoolExecutor - в 20-30 раз быстрее чем чистый Python. Как правильно заметили ниже в комментариях:

      > в 20-30 раз быстрее потому, что код на Go компилируется, а птичий пайтоновский >скрипт интерпретируется виртуальной машиной.

      Если все перевести на Go, то будет еще и более эффективная модель шедулинга рутин, что еще сильнее отражается на latency и CPU, что и показано в статье в числах на примере нашего сервиса.



      1. Paskin
        31.08.2022 11:39

        более эффективная модель шедулинга рутин

        Я как раз и пытался понять, каким образом вышесказанное связано с "последовательно в 1 процессе без ProcessPoolExecutor". Да - работа с нативными массивами/списками в Питоне сравнительно медленная - именно поэтому всякие numpy, PyTorch, OpenCV это обертки к заоптимизированным C/C++ библиотекам. Но это уже вопрос выбора инструмента...


        1. kopytovsv Автор
          31.08.2022 15:15

          Вы правы, что есть различные инструменты повышения производительно Python кода. Работу с массивами/списками оптимизировали и все равно уперлись в возможности Python, к тому же это все не решило проблемы с накладными расходами питона на CPU-bound задачи.


  1. boopiz
    30.08.2022 16:33

    в 20-30 раз быстрее потому, что код на Go компилируется, а птичий пайтоновский скрипт интерпретируется виртуальной машиной.


  1. s_f1
    30.08.2022 17:17
    +7

    Зашёл на главную Авито – размер страницы ~2 МБ, огромная такая кучка картинок нерелевантных объявлений. Делайте сайты нормально, хотя бы главную, и не нужно будет «выгадывать по 70 мс на сериализации».


    1. avdosev
      30.08.2022 17:41
      +3

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


      1. zloddey
        31.08.2022 10:07

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


      1. Ullr_S
        01.09.2022 19:52

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

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


  1. IL_Agent
    30.08.2022 20:54
    +1

    Почему го, а не с++, java(kotlin), .net ?


    1. kopytovsv Автор
      30.08.2022 21:27

      1. java(kotlin), .net - менее эффективны, чем Go.

      2. с++ - сложнее писать и поддерживать код, меньше специалистов на рынке.

      3. Go - основной язык в компании.


      1. asd111
        01.09.2022 15:09

        Зря не смотрите .net. Microsoft последние несколько лет прикладывает серьёзные усилия для повышения производительности кроссплатформенных asp.net core и entity framework core. Их фреймворки уже не уступают golang в производительности хоть и потребляют больше памяти. Также Microsoft развивает открытый ML.net - фреймворк для ml на c#. Очень советую ознакомится с данными новинками.


      1. Ivanhoe
        01.09.2022 17:37

        java(kotlin), .net - менее эффективны, чем Go.

        Citation needed. Я не знаю про .NET, но Java и JVM не "менее эффективны" (чтобы это не значило).


      1. Ivanhoe
        01.09.2022 17:37
        +1

        Go - основной язык в компании.

        В общем, других причин и не надо.

        • Go - основной язык в компании.


    1. kopytovsv Автор
      30.08.2022 23:30

      Ну и Go из коробки идеально подходит для такого рода задач)


  1. zloddey
    31.08.2022 10:00
    +1

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

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


    1. Ztare
      31.08.2022 10:50

      Полностью поддерживаю, главный вопрос "зачем это вообще существует?" так и повис в воздухе. Есть объективные доказательства того что вся эта махина приносит значимую пользу?


      1. kopytovsv Автор
        31.08.2022 11:58

        более половины всех просмотров объявлений идут с ленты рекомендаций на главной, польза есть)


        1. kopytovsv Автор
          31.08.2022 12:11

          и треть контактов


        1. a40
          31.08.2022 14:17
          +1

          Открыл авиту. Все(!) предложения с главной страницы абсолютно нерелеватны.

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


          1. zloddey
            31.08.2022 14:36
            +1

            Может, в глубине что-то полезное есть? Вы уж не поленитесь, пожалуйста, промотайте хотя бы на пару сотен объявлений вниз! /s


          1. kopytovsv Автор
            31.08.2022 14:45

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


    1. kopytovsv Автор
      31.08.2022 12:05

      Юзеры смотрят не все, но чтобы выбрать самый подходящий топ айтемов для пользователя нужно все равно обработать все эти 3000 айтемов, а чтобы получить 3000 - нужно обработать перед этим 100 миллионов. При уменьшении количества кандидатов для ранжирования просаживаются наши рек метрики - проверяли через АБ. Работа тут осталась только нужная.

      На клиент приходит только одна страница - 30 айтемов, есть пагинация.

      Некоторые пользователи залипают в ленту и листают ее до конца


    1. Paskin
      31.08.2022 12:06
      +2

      Честно говоря - у меня статья оставила впечатление джуна на интервью, рассказывающего о проекте в котором ему дали постоять в сторонке и посмотреть как другие работают. "У нас есть какая-то лютая хрень, перемножающая здоровенные массивы. Мы ее уже пихали и туда, и сюда - но CPU она жрет столько же. Потом мы ее переписали на компилируемом языке, включили оптимизации и чудо, все стало работать быстрее. А Гуру, который как раз мимокрокодил - сказал что-то про шедулинг короутин. Что он имел в виду - я не понял, но решил вставить в статью.


    1. kopytovsv Автор
      31.08.2022 15:18

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


  1. Mel
    31.08.2022 11:17

    А где можно почитать как вы используете вон тот 1Тб редис?


    1. kopytovsv Автор
      31.08.2022 12:32

      к сожалению пока нигде, думаем сделать доклад на эту тему


  1. Helltraitor
    31.08.2022 11:41

    Не могу не спросить: Почему не Rust?


    1. kopytovsv Автор
      31.08.2022 12:29

      язык не поддержан в компании на уровне платформы)


  1. xtrime
    31.08.2022 23:06
    +2

    Профилируйте ваш сервис. Используйте py-spy, как мы, или другой профайлер Python. 

    Мы у себя пошли дальше и включили профилирование всех процессов 24/7. Получился крутой и простой selfhosted opensource аналог blackfire/newrelic:

    • Собираем трейсы с помощью с помощью phpspy в неблокирующем режиме с поиском процессов по регулярке.

    • Конвертируем и агрегируем трейсы, навешиваем теги (проект, название машины/контейнера и тд...) с помощью своего адаптера https://github.com/zoonru/pyrospy.

    • Засылаем трейсы в https://github.com/pyroscope-io/pyroscope

    • Анализируем трейсы в интерактивном интерфейсе pyroscope. Там куча возможностей: фильтрация по тегам, поиск по методам, сравнение производительности с течением времени или на разных машинах.

    Подробнее в посте: https://habr.com/ru/post/662349/

    Можно пойти еще дальше: экспортировать метрики из pyroscope в grafana. Потом, например, навешать алертов, что бы мониторить время работы критичных мест :)


  1. titan_pc
    31.08.2022 23:40
    +1

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

    Пошли переписывать на го. Потратили 15 дней, кодили в 7-ром. Цель достигнута. Получили премии с продажи лишнего сервака. И прирост fps по всем направлениям.

    Это точно добавит аргументов в копилку "а надо ли переписывать".

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

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

    А к чему это всё. Да потому что завтра к нам в отдел придёт менеджер и скажет, ну весь питон на го переписывают - вперед. Сколько времени надо - 2 дня хватит?


    1. kopytovsv Автор
      31.08.2022 23:41
      +2

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

      В нашем случае уже не получалось остаться на питоне, так как:

      • железа докинуть сложно, уже начались проблемы при деплое, в кластере часто не хатало ресурсов при выкатке новой версии сервиса;

      • исчерпали почти все возможности питона, заоптимизировали все до чего дотянулись руки, местами в ущерб читаемости кода;

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

      Процесс переписывания такого сервиса действительно дорогостоящий, если говорить о цифрах, то стоит закладывать 40% времени на саму разработку и 60% на тестирование/отладку. Мы сверяли через АБ, часть трафика на старый сервис, часть на новый, АБ на основную ручку сервиса стал зеленый с 7го раза, при том что все было покрыто тестами и была пройдена оффлайн сверка.


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

      Опыт на питоне 8 лет, на go 4 года. Сказать честно, когда начал писать на go, ощущение как будто 2 пальца отрезали с каждой руки, но сейчас такой проблемы нет) Если у команды нет экспертизы на go, то в такое ввязываться не стоит.


  1. bevial
    01.09.2022 16:39

    Feature engineering -- наверное логично было ускорять через go, тем более если в компании много компетенций в языке.

    Но почему сильно распараллеленый ml реализован на python, а не на spark/scala?