Cargas lentas de páginas en perfiles de usuario

Hemos completado la importación de publicaciones de usuarios desde nuestro software actual a Discourse, y hemos notado que algunas páginas de usuario son muy lentas para cargar (varios segundos), lo cual parece deberse a consultas lentas. ¿Cómo podemos solucionar este problema e identificar el cuello de botella?

Aquí hay más información sobre nuestra configuración:

Especificaciones del servidor: 8 núcleos, 32 GB de RAM, almacenamiento NVMe SSD con ZFS

El foro tiene aproximadamente 25 millones de publicaciones. La mayoría son conversaciones privadas, con alrededor de 10,5 millones siendo públicas.
Tenemos varios megatopicos (somos un sitio de escritura, por lo que las historias individuales tienden a evolucionar lentamente con el tiempo, acumulando muchas publicaciones), con miles de publicaciones cada uno, lo cual podría estar contribuyendo, pero no sé cómo confirmarlo.

Aquí hay una consulta que parece estar causando problemas; la he visto ejecutarse en más de 5000 ms en el analizador de carga integrado:

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

Al observar el servidor mientras se carga una página de usuario, parece que solo se dispara el uso de CPU en los procesos de PostgreSQL mientras se ejecuta la consulta. No he realizado ningún cambio de configuración en la instalación, excepto agregar soporte para MySQL durante el proceso de importación. ¡Cualquier consejo sobre los siguientes pasos sería muy apreciado!

7 Me gusta

Lo dejo aquí para referencia. He añadido 4 índices a la base de datos, lo que ha mejorado considerablemente el rendimiento, aunque creo que aún podría ser mejor. Esto es lo que he añadido:

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 Me gusta

Hmm, ¿nos faltan algunos índices aquí, @tgxworld?

2 Me gusta

Hola @Ghan,

muchas gracias por estos índices. Hemos tenido el mismo problema durante un tiempo y no hemos podido resolverlo.
Tras aplicar estos índices, la carga de los perfiles de usuario mejoró drásticamente. Había algunos perfiles en el “peor caso” que tardaban hasta 30 segundos en cargar. Ahora aparecen en unos 5 segundos.

Así que, gracias de nuevo, y espero que haya más potencial para acelerar esto.

Saludos cordiales,
Sascha

P.D. Incluso la página /about tarda hasta unos 20 segundos en cargar. Quizás también se podría acelerar.

1 me gusta

Preguntando de nuevo. ¿Nos faltan estos índices @tgxworld @sam?

2 Me gusta

Déjame revisar los planes para esas consultas en Meta.

Las consultas son bastante rápidas en Meta, pero entiendo cómo pueden tener un rendimiento deficiente en bases de datos con recursos limitados. Estamos utilizando ActiveRecord y el código es bastante limpio, pero el SQL generado es bastante desordenado.

Al escribir el SQL manualmente, podemos hacer que la consulta utilice los índices existentes y optimizarla considerablemente:

Antes

Después

En la consulta optimizada, solo recuperamos publicaciones del usuario, lo que evita que el bucle de publicaciones para la unión se ejecute demasiado tarde.

Por lo tanto, creo que podemos obtener un mejor rendimiento sin añadir sobrecarga por índices.

7 Me gusta

¿Se puede realizar la versión reescrita en el código de ActiveRecord o necesitarías pasar una cadena de consulta manual aquí?

Espero que sí, porque el código Ruby actual es hermoso. Pero si no, tenemos un patrón muy sencillo para hacer SQL en la aplicación que ya se usa ampliamente.

1 me gusta

Parece que nos faltan índices en muchas de estas consultas que se ejecutan en la página de resumen de usuarios. Habrá que optimizar estas consultas una por una.

@Falco veo que te has asignado esto, ¿sigues queriendo ocuparte? La parte complicada aquí es que estas consultas están mayormente sin probar, así que tenemos que tener cuidado de no romper nada.

3 Me gusta

Agregué un commit de borrador para probar la salida del serializador aquí para que no vayamos a ciegas:

Después de eso, intenté volver a aplicar lo que hice aquí, pero resulta que estaba agregando una cláusula extra que lo hacía rápido pero incorrecto.

El hecho de que pasemos todo esto (incluso los más apreciados/apreciados por) a través del guardian significa que la información siempre es muy precisa y adaptada al espectador, pero hace que la caché no ayude contra múltiples usuarios.

Supongo que podemos al menos agregar una caché más larga a este punto final. En mi opinión, una caché de 1 hora sería adecuada, ya que la información en el resumen no cambia con frecuencia.

4 Me gusta

Suena bien… ¡hazlo realidad!

1 me gusta

He subido la caché a mi rama de PR:

Mientras depuraba más a fondo, noté que también tenemos algunos problemas de N+1 allí. Las miniaturas y la etiqueta “Resuelto” están provocando un N+1 en las listas de temas. ¿Alguna idea al respecto, @david @tgxworld?

2 Me gusta

Se ha fusionado el trabajo de caché.

Aún estoy trabajando en el problema N+1.

7 Me gusta

¿Qué serializador utiliza la página de resumen del usuario para las listas de temas? Para la búsqueda completa de la página, recientemente solucioné el N+1 eliminando los atributos thumbnails del serializador de búsqueda, ya que no se utilizan en absoluto.

3 Me gusta

Corregido en PERF: Preload topic thumbnails for all topic lists by davidtaylorhq · Pull Request #11238 · discourse/discourse · GitHub y PERF: Simplify topic serialization for user summary page by davidtaylorhq · Pull Request #11236 · discourse/discourse · GitHub

4 Me gusta