LINUX.ORG.RU

Кто тут по алгоритмам может посоветовать?

 ,


0

5

Всем привет, в общем есть некий абстрактный крупный проект новостного сайта, делаю админку для него, в проекте DDD + CQRS.

И столкнулся с одной проблемой, при поиске новостей мы идем в эластик, получаем ID новостей, потом идем в разные домены и грузим источник новости, картинку, импортированные по RSS новости по этой теме, авторов, инфу о регионах, статистику. Таким образом, чтобы загрузить одну новость, мне нужно выполнить порядка 8 запросов на чтение, для 30 новостей на странице выдачи это 240 запросов, каждый из которых длиться около 10 миллисекунд итого получаем 2.5 секунды, что примерно в 12 раз дольше чем надо.

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

let clusters_ids = search_engine.search(filters, pagination);

for cluster_id in cluster_ids {
    let published_news = news_service.get_news_by_id(cluster_id)?;
    let news_source = news_source_service.get_web_resource_by_id(
published_news.imported_from_web_resource())?;
    let author = authors_service.get_author_by_id(published_news.author_id())?;
    let region = regions_service.get_region_by_id(published_news.region_id())?;
    let image = images_service.get_image_by_id(published_news.image_id())?;
    let statistics = statistics_service.get_statistics_by_news_id(published_news.id())?;
    let parent_news = news_service.get_parent_news_by_id(published_news.id())?;

    result.push(
      DTO::builder()
      .published_news(published_news)
      .news_source(news_source)
      .author(author)
      .region(region)
      .image(image)
      .statistics(statistics)
      .parent_news(parent_news)
      .finish()
    );
}

Собственно, проблема заключается в том, что нам нужно сходить в разные домены, чтобы собрать полный объект. Запросы по одному элементу очень долго выполняются так как из-за CQRS запрос летит по REST API в модель чтения и там читается в Postgres.

Я попробовал запускать в отдельных тасках получение дочерних объектов, ускорилось чуток.

Потом, я сделал функции, которые получают коллекцию объектов по коллекции ID-шников. Эти коллекции объектов запихнул в хешмапы и по ID новости беру все дочерние объекты. Стало выполняться за 600мс. Но это все равно очень долго.

Есть ли какие-то алгоритмы на такой случай? Или советы?

★★★★

Гугли N + 1 query problem.

Решение заключается в том, что тебе нужно, чтобы вместо get_author_by_id был get_authors_by_ids, вместо get_region_by_id был get_regions_by_ids и т. д. То есть в каждом сервисе должен быть метод для массового получения объектов по списку id.

Тогда ты сначала получаешь список новостей, а затем из него через iter().map().collect() делаешь списки id связанных ресурсов, запрашиваешь сразу все обходимые ресурсы каждого типа, потом строишь из каждого такого списка HashMap (в качестве ключа будет id связанного ресурса) и пробегаешься по новостям уже доставая необходимые данные из HashMap вместо запроса к другому сервису на каждую новость, потому что все данные у тебя уже есть.

И теперь у тебя 8 запросов вне зависимости от количества новостей. Но надо, чтобы каждый сервис отдающий дополнительные данные умел искать по списку id вместо одного (не знаю как Эластик, но в том же SQL есть оператор IN позволяющий искать по вхождению элемента в список вместо обычного равенства). Если такой функционал в него добавить нельзя, то задача нерешаема и тебе придётся делать твои 240 запросов (хотя можно попытаться кешировать ответы хотя бы).

KivApple ★★★★★
()
Последнее исправление: KivApple (всего исправлений: 2)

Правило №1 для приличных людей: если данные будут читать миллион пользователей - их надо подготовить заранее.

Поэтому в фоне собираются все эти новостные запросы и формируется готовая выборка, которая живет в памяти.

Из памяти происходит вычитка при отдаче клиентам.

alex0x08 ★★★
()
Ответ на: комментарий от KivApple

Спасибо, в целом похоже на то что у меня получилось:

Потом, я сделал функции, которые получают коллекцию объектов по коллекции ID-шников. Эти коллекции объектов запихнул в хешмапы и по ID новости беру все дочерние объекты. Стало выполняться за 600мс. Но это все равно очень долго.

Но хочется еще ускорить. Надо будет еще посмотреть N+1 query problem, может что-то не учел.

AntonyRF ★★★★
() автор топика
Ответ на: комментарий от alex0x08

Правило №1 для приличных людей: если данные будут читать миллион пользователей - их надо подготовить заранее.

Не, это админка, там редакторов человек 50 от силы.

AntonyRF ★★★★
() автор топика
Ответ на: комментарий от AntonyRF

это админка

Тогда в чём проблема? Вообще если там CQRS, то админка должна отражать такую архитектуру. Т.е. админ сначала натыкивает, что он хочет (даёт команду на сбор инфы), а потом отдельно смотрит что собралось. И то что собралось ясен пень уже закешировано и никуда ходить не надо.

240 запросов, каждый из которых длиться около 10 миллисекунд итого получаем 2.5 секунды

А запросы что, синхронно выполняются?

no-such-file ★★★★★
()
Ответ на: комментарий от KivApple

Если такой функционал в него добавить нельзя, то задача нерешаема и тебе придётся делать твои 240 запросов

Их не обязательно делать последовательно. Если сервис может тащить 100500 запросов параллельно, то все запросы отдаются за 10мс и проблемы нет. Это к тому же проще кэшировать т.к. есть 1:1 отображение запрос-сущность. И проще масштабировать.

no-such-file ★★★★★
()
Ответ на: комментарий от AntonyRF

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

max_lapshin ★★★★★
()
Ответ на: комментарий от Toxo2

Нельзя всё это выбросить и написать в ПГ одну процедуру, которая сама это всё будет собирать и, при возможности, у себя же и кэшировать в unlogged таблице?

Обсуждаю на работе, но вообще если придерживаться DDD, то нельзя

AntonyRF ★★★★
() автор топика
Ответ на: комментарий от no-such-file

А запросы что, синхронно выполняются?

У нас все равно появляется цикл обхода по ID-шникам в том или ином виде, плюс разные DDD-сервисы возвращают каждый свой Entity если убрать DDD, то получиться что запросы выполняются практически последовательно.

AntonyRF ★★★★
() автор топика

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

maxcom ★★★★★
()
Последнее исправление: maxcom (всего исправлений: 1)
Ответ на: комментарий от AntonyRF

Если бы у тебя был не эластик, а SQL СУБД, то можно было бы вытягивать связанные сущности на стороне сервера через JOIN.

Также можно подумать о денормализации. Если данные читаются гораздо чаще, чем пишутся, то мб имеет смысл хранить копии связанных сущностей прямо внутри новости (через какую-нибудь JSON-колонку). И при обновлении сущности перекопировать её во все новости. Тогда обновления будут тяжёлыми, зато выборки очень лёгкими.

KivApple ★★★★★
()
Ответ на: комментарий от AntonyRF

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

Почему получится и как цикл обхода мешает сделать все запросы асинхронно? Есть какая-то проблема сделать вот так (условный код на ноде)

const fetches = [];
for(const cluster_id of cluster_ids) {
    fetches.push(Promise.all([
       news_service.get_news_by_id(cluster_id),
       news_source_service.get_web_resource_by_id(published_news.imported_from_web_resource()),
       authors_service.get_author_by_id(published_news.author_id()),
       regions_service.get_region_by_id(published_news.region_id()),
       images_service.get_image_by_id(published_news.image_id()),
       statistics_service.get_statistics_by_news_id(published_news.id()),
       news_service.get_parent_news_by_id(published_news.id()),
   ]);
}

for(const fetch of fetches) {
    const [published_news, news_source, author, region, image, statistics, parent_news] = await fetch;

    result.push(
      DTO::builder()
      .published_news(published_news)
      .news_source(news_source)
      .author(author)
      .region(region)
      .image(image)
      .statistics(statistics)
      .parent_news(parent_news)
      .finish()
    );
}
no-such-file ★★★★★
()
Ответ на: комментарий от maxcom

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

Вот звучит интересно, но мы боимся того что новости достаточно часто редактируются и придется обновлять данные в эластике, а индексация на 80 ГБ не поставит ли эластик на четвереньки?

AntonyRF ★★★★
() автор топика
Ответ на: комментарий от AntonyRF

Это надо тестировать на ваших данных. В любом случае это tradeoff между скоростью выдачи результатов и ресурсами, требуемыми для поддержания индексов.

maxcom ★★★★★
()