Медленная загрузка страниц профилей пользователей

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

Вот дополнительная информация о нашей конфигурации:

Характеристики сервера: 8 ядер, 32 ГБ ОЗУ, хранилище NVMe SSD с ZFS

На форуме около 25 миллионов сообщений. Большинство из них — личные переписки, при этом около 10,5 миллиона являются публичными.
У нас есть несколько мега-тем (мы — сайт для писателей, поэтому отдельные истории развиваются медленно, накапливая множество сообщений), в которых тысячи постов, так что это может быть причиной проблемы, но я не знаю, как это подтвердить.

Вот один из проблемных запросов — я видел его время выполнения более 5000 мс во встроенном анализаторе нагрузки:

SELECT "topic_links"."id" AS t0_r0, "topic_links"."topic_id" AS t0_r1, "topic_links"."post_id" AS t0_r2, "topic_links"."user_id" AS t0_r3, "topic_links"."url" AS t0_r4, "topic_links"."domain" AS t0_r5, "topic_links"."internal" AS t0_r6, "topic_links"."link_topic_id" AS t0_r7, "topic_links"."created_at" AS t0_r8, "topic_links"."updated_at" AS t0_r9, "topic_links"."reflection" AS t0_r10, "topic_links"."clicks" AS t0_r11, "topic_links"."link_post_id" AS t0_r12, "topic_links"."title" AS t0_r13, "topic_links"."crawled_at" AS t0_r14, "topic_links"."quote" AS t0_r15, "topic_links"."extension" AS t0_r16, "topics"."id" AS t1_r0, "topics"."title" AS t1_r1, "topics"."last_posted_at" AS t1_r2, "topics"."created_at" AS t1_r3, "topics"."updated_at" AS t1_r4, "topics"."views" AS t1_r5, "topics"."posts_count" AS t1_r6, "topics"."user_id" AS t1_r7, "topics"."last_post_user_id" AS t1_r8, "topics"."reply_count" AS t1_r9, "topics"."featured_user1_id" AS t1_r10, "topics"."featured_user2_id" AS t1_r11, "topics"."featured_user3_id" AS t1_r12, "topics"."deleted_at" AS t1_r13, "topics"."highest_post_number" AS t1_r14, "topics"."like_count" AS t1_r15, "topics"."incoming_link_count" AS t1_r16, "topics"."category_id" AS t1_r17, "topics"."visible" AS t1_r18, "topics"."moderator_posts_count" AS t1_r19, "topics"."closed" AS t1_r20, "topics"."archived" AS t1_r21, "topics"."bumped_at" AS t1_r22, "topics"."has_summary" AS t1_r23, "topics"."archetype" AS t1_r24, "topics"."featured_user4_id" AS t1_r25, "topics"."notify_moderators_count" AS t1_r26, "topics"."spam_count" AS t1_r27, "topics"."pinned_at" AS t1_r28, "topics"."score" AS t1_r29, "topics"."percent_rank" AS t1_r30, "topics"."subtype" AS t1_r31, "topics"."slug" AS t1_r32, "topics"."deleted_by_id" AS t1_r33, "topics"."participant_count" AS t1_r34, "topics"."word_count" AS t1_r35, "topics"."excerpt" AS t1_r36, "topics"."pinned_globally" AS t1_r37, "topics"."pinned_until" AS t1_r38, "topics"."fancy_title" AS t1_r39, "topics"."highest_staff_post_number" AS t1_r40, "topics"."featured_link" AS t1_r41, "topics"."reviewable_score" AS t1_r42, "topics"."image_upload_id" AS t1_r43, "posts"."id" AS t2_r0, "posts"."user_id" AS t2_r1, "posts"."topic_id" AS t2_r2, "posts"."post_number" AS t2_r3, "posts"."raw" AS t2_r4, "posts"."cooked" AS t2_r5, "posts"."created_at" AS t2_r6, "posts"."updated_at" AS t2_r7, "posts"."reply_to_post_number" AS t2_r8, "posts"."reply_count" AS t2_r9, "posts"."quote_count" AS t2_r10, "posts"."deleted_at" AS t2_r11, "posts"."off_topic_count" AS t2_r12, "posts"."like_count" AS t2_r13, "posts"."incoming_link_count" AS t2_r14, "posts"."bookmark_count" AS t2_r15, "posts"."score" AS t2_r16, "posts"."reads" AS t2_r17, "posts"."post_type" AS t2_r18, "posts"."sort_order" AS t2_r19, "posts"."last_editor_id" AS t2_r20, "posts"."hidden" AS t2_r21, "posts"."hidden_reason_id" AS t2_r22, "posts"."notify_moderators_count" AS t2_r23, "posts"."spam_count" AS t2_r24, "posts"."illegal_count" AS t2_r25, "posts"."inappropriate_count" AS t2_r26, "posts"."last_version_at" AS t2_r27, "posts"."user_deleted" AS t2_r28, "posts"."reply_to_user_id" AS t2_r29, "posts"."percent_rank" AS t2_r30, "posts"."notify_user_count" AS t2_r31, "posts"."like_score" AS t2_r32, "posts"."deleted_by_id" AS t2_r33, "posts"."edit_reason" AS t2_r34, "posts"."word_count" AS t2_r35, "posts"."version" AS t2_r36, "posts"."cook_method" AS t2_r37, "posts"."wiki" AS t2_r38, "posts"."baked_at" AS t2_r39, "posts"."baked_version" AS t2_r40, "posts"."hidden_at" AS t2_r41, "posts"."self_edits" AS t2_r42, "posts"."reply_quoted" AS t2_r43, "posts"."via_email" AS t2_r44, "posts"."raw_email" AS t2_r45, "posts"."public_version" AS t2_r46, "posts"."action_code" AS t2_r47, "posts"."locked_by_id" AS t2_r48, "posts"."image_upload_id" AS t2_r49 FROM "topic_links" INNER JOIN "topics" ON "topics"."id" = "topic_links"."topic_id" AND ("topics"."deleted_at" IS NULL) INNER JOIN "posts" ON "posts"."id" = "topic_links"."post_id" AND ("posts"."deleted_at" IS NULL) WHERE (posts.post_type IN (1,2,3,4)) AND ("topics"."deleted_at" IS NULL) AND (topics.archetype <> 'private_message') AND "topics"."visible" = TRUE AND (topics.category_id IS NULL OR topics.category_id IN (SELECT id FROM categories WHERE NOT read_restricted OR id IN (3,4))) AND "topic_links"."user_id" = 7237 AND "topic_links"."internal" = FALSE AND "topic_links"."reflection" = FALSE AND "topic_links"."quote" = FALSE ORDER BY clicks DESC, topic_links.created_at DESC LIMIT 6

При мониторинге сервера во время загрузки страницы пользователя видно, что процессы PostgreSQL резко потребляют процессорное время во время выполнения запроса. Я не вносил никаких изменений в конфигурацию установки, кроме добавления поддержки MySQL для процесса импорта. Буду признателен за любые советы по дальнейшим шагам!

7 лайков

Добавляю это сюда для справки. Я добавил 4 индекса в базу данных, что значительно улучшило производительность, хотя, думаю, её можно ещё оптимизировать. Вот что я добавил:

CREATE INDEX index_topic_links_on_clicks_and_created ON public.topic_links USING btree (clicks, created_at);
CREATE INDEX index_posts_on_like_count_and_created ON public.posts USING btree (like_count, created_at);
CREATE INDEX index_topic_links_on_clicks_and_created_desc ON public.topic_links USING btree (clicks DESC, created_at DESC);
CREATE INDEX index_posts_on_like_count_and_created_desc ON public.posts USING btree (like_count DESC, created_at DESC, user_id) WHERE deleted_at IS NULL AND post_number > 1 AND (post_type = ANY ('{1,2,3,4}'::integer[]));
6 лайков

Хм, не хватает ли здесь некоторых индексов, @tgxworld?

2 лайка

Привет, @Ghan,

Большое спасибо за эти индексы. У нас уже давно есть такая же проблема, но мы не могли её решить.
После применения этих индексов загрузка профилей пользователей значительно улучшилась. Были профили в «худшем случае», которые загружались до 30 секунд. Теперь они отображаются примерно за 5 секунд.

Ещё раз спасибо, и надеюсь, что есть потенциал для дальнейшего ускорения.

С уважением,
Саша

P.S. Даже страница /about загружается до ~20 секунд. Возможно, и её тоже можно ускорить.

1 лайк

Спрашиваю ещё раз. Мы упустили эти индексы @tgxworld @sam?

2 лайка

Давайте проверю планы выполнения этих запросов в Meta.

Запросы в Meta выполняются довольно быстро, но я понимаю, как они могут работать медленно на слабых базах данных. Мы используем ActiveRecord, и код довольно чистый, но сгенерированный SQL-код получается довольно запутанным.

Если писать SQL вручную, мы можем заставить запрос использовать существующие индексы и значительно оптимизировать его:

До

После

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

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

7 лайков

Можно ли реализовать переписанную версию в коде ActiveRecord, или здесь необходимо передать строку запроса вручную?

Надеюсь, что так, ведь текущий код на Ruby прекрасен. Но если нет, у нас есть очень простой паттерн для работы с SQL в приложении, который уже широко используется.

1 лайк

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

@Falco, я вижу, что вы назначили себя на эту задачу, вы всё ещё хотите её взять? Сложность здесь в том, что эти запросы в основном не протестированы, поэтому нужно быть осторожными, чтобы ничего не сломать.

3 лайка

Я добавил черновой коммит для тестирования вывода сериализатора, чтобы мы не действовали вслепую:

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

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

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

4 лайка

Звучит хорошо… так и сделаем!

1 лайк

Я пушил кэш в ветку своего PR:

При дальнейшей отладке я заметил, что там тоже есть N+1 запросы. Миниатюры и статус «Решено» вызывают N+1 для списков тем. Есть какие-то идеи, @david @tgxworld?

2 лайка

Объединена работа с кэшем.

Продолжаю работу над проблемой N+1.

7 лайков

Какой сериализатор использует страница сводки пользователя для списков тем? Для полного поиска по странице я недавно исправил проблему N+1, убрав атрибуты thumbnails из сериализатора поиска, так как они вообще не используются.

3 лайка

Исправлено в PERF: Preload topic thumbnails for all topic lists by davidtaylorhq · Pull Request #11238 · discourse/discourse · GitHub и PERF: Simplify topic serialization for user summary page by davidtaylorhq · Pull Request #11236 · discourse/discourse · GitHub

4 лайка