Cómo optimizar el rendimiento de tu app con New Relic y PostgreSQL

Por

La mejora del rendimiento puede ser una tarea desalentadora pero gratificante en el desarrollo de software. Embarcarse en la mejora de una parte de una aplicación web o una funcionalidad puede resultar en una disminución de los costes operativos y de servidor y en una mejora de la experiencia del usuario, pero la cantidad de tiempo, conocimientos y, a veces, experimentación que hay que invertir en ello debe tenerse en cuenta antes de gastar el tiempo del desarrollador que se puede utilizar en otra cosa; esto es, por supuesto, a menos que un problema de rendimiento haya llegado al punto de romper una funcionalidad o un proceso importante por completo.

Este artículo tratará de dar algunos consejos principalmente en el lado back-end de las cosas y principalmente con el uso de ruby on rails y New Relic; pero algunos de los conceptos se aplican a cualquier stack y herramienta de monitoreo.


Elige tu aventura

Es importante tener en cuenta que aunque el back-end es importante y es el foco de este artículo, la decisión de embarcarse en el rendimiento tiene que tener en cuenta todo lo que ocurre cuando el usuario carga una página; y eso significa el tamaño de las imágenes, la cantidad de javascript descargado, la respuesta del back-end y las manipulaciones del front-end que se hacen con ella, etc.

Por ejemplo, ¿compensaría invertir un par de días de tiempo de los desarrolladores en optimizar un endpoint de 2000 ms a 500 ms si el usuario tiene que pasar 6 segundos esperando a que se cargue una página por culpa de unas imágenes mal dimensionadas? Lo dudo.

Darth Vader intentando purificar el agua de mar podría tener más éxito que nosotros embarcándonos en una mejora del rendimiento equivocada.
Echemos un vistazo a New Relic ahora. En primer lugar, asegúrate de hacer clic en el botón superior derecho para seleccionar un período de tiempo de varios días, de esa manera New Relic te dará un mejor promedio o representación ya que mirar el último día o 30 minutos podría no ser una instantánea exacta del uso y el tráfico de tu aplicación. Yo estoy usando 7 días como puedes ver en la parte resaltada de mi dashboard de New Relic:

Selecciona el plazo aplicable en New Relic antes de hacer cualquier cosa.
Accede a la vista de "transacciones" y la misma te mostrará los puntos finales que consumen más tiempo y que han sido rastreados por New Relic en ese período. Echemos un vistazo a las 20 transacciones más lentas:
Por defecto muestra las transacciones más lentas.
Application server#error no nos dice mucho y probablemente se trate de timeouts de varios endpoints agregados; el siguiente webpros#index parece un buen candidato para optimizar ¿no? No tan rápido, primero hagamos clic en el desplegable 'ordenar por' y seleccionemos 'Rendimiento', que son las llamadas por minuto.
Selecciona "throughput" en el desplegable para considerar el tráfico

Ignorando los dos primeros que son llamados por varios endpoints en rails, podemos ver que webpros#index no está ni cerca de la parte superior de los endpoints más utilizados, porque es un endpoint sólo para administradores que no tiene mucho tráfico.

Aquí es cuando el uso de algún tipo de marco de priorización ayudaría a elegir qué optimizar, a mí me gusta pensar siempre en un cuadrante de valor vs esfuerzo a la hora de decidir qué hacer:

Cuadrante Valor vs Esfuerzo
Al igual que con cualquier otra tarea a elegir, queremos aumentar el rendimiento allí donde tenga más valor y requiera menos esfuerzo.

Ambas cosas son muy subjetivas, pero el valor se puede correlacionar normalmente con el uso o el tráfico que recibe un punto final; el esfuerzo, obviamente, con el tiempo que llevará optimizarlo. Por lo general, los puntos finales de menor rendimiento que nunca han tenido el rendimiento mirado tendrán correcciones más fáciles, pero su conocimiento de la aplicación, el lenguaje, y la cantidad de veces que has hecho esta tarea tendrá que ser parte de su decisión. 

En este caso vamos con un endpoint que percibimos que tiene un valor alto y un esfuerzo medio que es el Jobs#show, y como podrás adivinar es extremadamente importante para nuestro SEO y nuestros usuarios mostrar la página de empleos lo más rápido posible.


Ojos en el objetivo: Visualización y análisis de las trazas de transacciones

Hagamos clic en la transacción Jobs#Show y examinémosla.

Resumen de una transacción
A primera vista no parece que haya mucho que hacer aquí con sólo una respuesta de 245 ms, ¿verdad? Pero podemos ver en el gráfico y la tabla de desglose 2 cosas: 

  • La mayor parte del tiempo es empleado por la base de datos (Postgres)
  • El Postgres Job find es llamado más de dos veces en promedio (!)

Tanto el % de tiempo como la media de llamadas/Txn son importantes para saber qué hay que mirar en este endpoint. Pero podemos obtener más detalles si nos desplazamos hacia abajo y vemos si hay alguna 'transacción lenta' marcada por New Relic:

Muchas transacciones lentas o valores atípicos superiores a 10000 ms.
Y allí hay varios trabajos que tardaron más de 10 segundos en cargarse. Echemos un vistazo a algunos de ellos, por ejemplo este, primero entrando en su resumen y luego en sus detalles de rastreo:

Un perfil de rastreo, que muestra sobre todo las actividades que tardan en cargarse
Este perfil nos muestra en qué se empleó el tiempo en los 22,74 segundos que duró esta petición, y lo desglosa por partes. Podemos ver que el 95% fue una sola consulta en la tabla de actividades para averiguar la última vez que se comprobó el trabajo y, aunque es alarmante, es difícil saber si se trata de un caso aislado. Veamos otro:

Podemos ver aquí las dos consultas de búsqueda de empleo que mencionábamos antes, y cómo una de ellas se lleva la mayor parte de la petición.
En este caso, esto es más similar a nuestra transacción media de lo que vimos en nuestro resumen. Podemos ver dos llamadas a Job.Find, y una de ellas, la más lenta, es llamada desde el parcial de trabajos relacionados para encontrar trabajos relacionados. Hemos encontrado nuestro objetivo.

Postula a empleos de programación aquí


Atacando nuestro problema de rendimiento

Veamos el código ruby haml para esa vista parcial:

- related_jobs = @job.related_jobs
- if related_jobs.present? && !team_member_signed_in?
  %h3.w900.size4.mb2=t("jobs_related", country: t("country"))
  .browse-widget--small.space-between.mb4
    - related_jobs.each do |j|
      - cache(j) do
        = render partial: "related", locals: {job: j}

La consulta en sí se produjo en este parcial y no el parcial para cada uno de los trabajos relacionados, además de que uno se almacena en caché. Vamos a ejecturar esta consulta producida por el método en la base de datos de seed con condiciones similares a las de producción (idealmente) y llamar a to_sql sobre ella  con la consola de rails

puts @job.related_jobs.to_sql
=> SELECT DISTINCT "jobs".* FROM "jobs" .... LIMIT $;

Ahora entraremos en PostgreSQL con algo como `rails db` y meteremos esto ahí, anteponiendo una instrucción de EXPLAIN ANALYZE para que nos de información para analizar esta y cualquier consulta. 

El resultado del plan de consultas y el análisis es un montón de texto muy largo para publicar aquí; Para un administrador de bases de datos experimentado esto podría ser legible, pero como soy un humilde desarrollador web recomiendo pegar esto en algo como https://explain.dalibo.com/ o cualquier sitio web de análisis de planes de consultas para que tenga más sentido (Sólo asegúrate de comprobar las políticas de retención de datos de lo que usas y no subir ningún dato confidencial). 

De todos modos, pegar esto en el sitio web que acabo de mencionar nos da un resultado como este:

Plan de consulta global analizado en un formato legible
Mucho más fácil de leer, ¿verdad? En general, para consultas de uso frecuente con Active Record el sospechoso habitual son las partes de la consulta que no están utilizando un índice, y este parece ser el caso en esta consulta, ya que tiene que hacer un escaneo secuencial de la tabla de puestos de trabajo que es muy caro. Al abrir ese escaneo secuencial se muestra más información:
El escaneo secuencial de la tabla de trabajos no es un buen augurio.
Esta consulta está filtrando alrededor del 99% de sus filas, y un intento de optimizar esto será añadir un índice que podría ayudar con esta consulta:

CREATE INDEX index_test ON jobs(category_id, tenant_city_id, hidden, confidential, state);

Recomiendo hacer esto en la consola psql directamente para tu entorno de desarrollo en lugar de migraciones, ya que tendrás que crear y tirar varios índices hasta que encuentres uno que sea usado por postgres y mejore tu situación, ya que dependiendo de los datos a postgres le puede resultar más fácil simplemente escanear secuencialmente nuestra tabla y entonces no estamos haciendo gran cosa. De todas formas, añadiendo este índice y comprobando el plan nos da un resultado diferente:


Nuestro nuevo plan de consulta después de ese índice.


Nuestro escaneo secuencial se ha convertido ahora en un escaneo de índice de mapa de bits, y leyendo un poco sobre él encontramos que es un término medio entre un escaneo secuencial y un escaneo de índice, obteniendo ventajas de ambos; Los tiempos se ven mejor y ahora el analizador del plan de consulta está realmente mirando el escaneo de índice de la tabla weight_things como la parte lenta, pero siendo ya un escaneo de índice probablemente vamos a tener que pasar mucho tiempo allí para obtener menos resultados ( recuerda nuestro marco de priorización)


Deploya, pero no olvides...

El siguiente paso es, obviamente, hacer una migración con esto y deployarlo; pero no acaba ahí, necesitamos comprobar el tiempo medio de transacción de New Relic y compararlo con el período anterior para confirmar que esto está dando los resultados previstos, ya que estábamos probando esto principalmente en una transacción específica. 

Así es como se ve comparando el tiempo de este endpoint antes/después del despliegue, esperando una semana para promediar más datos:

La comparación del rendimiento del punto final muestra una reducción de más de 100 ms en el tiempo de respuesta (46%)

Gráfico que compara los tiempos de esta semana para jobs#show con los de la semana pasada.

New Relic puede comparar el rendimiento de esta semana con el de la anterior, y podemos ver cómo a nuestro endpoint le va mucho mejor; recuerda, un 46% de tiempo para nuestro servidor no se traduce en un 46% de carga de página para un usuario, ya que está el frontend y otras piezas en juego. Pero tendrá un impacto en eso y en la carga y las facturas de nuestro servidor.

Es importante entender también que tenemos que considerar que cada índice añadido ralentiza las escrituras en esta tabla, pero esta tabla es un modelo de baja frecuencia de escritura a diferencia de otras tablas que tenemos. Otras mejoras que podemos hacer es almacenar en caché el resultado de esta consulta para cada trabajo, pero entonces puede que no aparezcan algunos trabajos nuevos durante el período de actualización de la caché. 

Esperamos que esta guía te haya ayudado a priorizar y enfocar las mejoras de rendimiento.



***************



Tu próximo empleo remoto te espera en: getonbrd.com







Foto de portada:  Tim Mossholder en Unsplash

Lo más reciente en Blog