Problemas de desempeño mas comunes en Java… Y en cuáles he caído desarrollando en plataformas móviles: El N+1
Recientemente me hice con un entretenido e-book de AppDynamics donde se presentan un conjunto de 1o problemas con los cuales estoy seguro todos nos hemos enfrentado cuando estamos realizando estas maravillosas y extensas aplicaciones en Java que nos toma meses construir y las mismas una vez que las ‘inauguramos’ empiezan a caer, colgarse y molestar a nuestros usuarios.
¡Javier, esto es muy lento! Dime de una vez si me regreso a mi formulario en papel…
Mi mayor experiencia en Java empezó refactorizando una aplicación de ventas que se ejecuta sobre la máquina virtual de Java de IBM (J9) en la plataforma Windows Mobile. Para aquella entonces, los dispositivos basados en procesadores de 600 Mhz y apenas 256 MB de Ram de los AS400 que empleábamos daban la batalla en durabilidad para las manos pesadas y descuidos a los que estaban sometidos por la fuerza de venta; sin embargo, debido a sus limitados recursos, el código tenía que ser mas que eficiente en uso de memoria y el tiempo de respuesta de la aplicación era crítico para perder o ganar una venta.
Posterior a eso, la era de Android. Equipos doble núcleo, gigas de memoria, ¡mucho poder! Pero en un mercado emergente y socialmente peligroso como en el cual se desenvuelve nuestra fuerza de ventas, el vendedor no puede andar si quiera con un Galaxy S5 sin estar expuesto a un asalto y tampoco es viable para la empresa equipar a mas de 900 ‘asesores comerciales’ con un smartphone cada mes por situaciones como estas.
La aplicación debe ser rápida y ejecutarse en cualquier ‘aparatico’ que no pase de 70USD
Muchos de los problemas de desempeño a veces los solucionamos empleando sentido común, pero no descarto también golpes de suerte probando eso o aquello conseguido en Internet.
El e-book que les comparto distribuye en cuatro categorías los problemas de desempeño en desarrollos basados en Java: base de datos, memoria, concurrencia, y ‘descuido’. Este último no se llama así como tal, pero lo he tropicalizado. Los mismos están medidos por la magnitud del impacto que pueden causar en el desempeño de la aplicación, muestra patrones que permiten identificarlos y lineamentos para resolverlos. Son muchos como para estar en un solo post así que habrá mas de una publicación de este tema en este blog.
Problema ‘Muerte por 1000 Cortes’. El problema N+1 de Base de datos
Confieso que el nombre me llamó la atención y lo que leí en Wikipedia me dejó un mal sabor del café que me tomaba mientras les redactaba esto.
Ocurre cuando nuestra aplicación se vuelve lenta por el uso excesivo de la base de datos a través de accesos individuales. Es mas común en procesos transaccionales y tiene lugar cuando utilizas un número elevado de consultas cuando lo mismo lo puedes hacer con solo dos.
Por ejemplo, el vendedor con su dispositivo puede atender alrededor de 60 clientes al mes que tiene asignado. Un vendedor puede tomar en promedio al día cerca de 10 pedidos de unas 25 líneas a clientes distintos. En un mes puede hacer en promedio 5.000 líneas de pedido; y en total, los 900 vendedores de la empresa pueden hacer 4.500.000 líneas al mes.
Obtener las ventas realizadas a un cliente entonces, durante toda su vida pudiese hacerse ‘de mal manera’ así:
select id_pedido from pedido where id_cliente = $el_cliente
select * from pedido_detalle where id_pedido = 1
select * from pedido_detalle where id_pedido = 2
...
select * from pedido_detalle where id_pedido = n
La solución a este problema es sencilla, de hecho, seguramente la mayoría de quienes lean esto preguntarán ¿por qué no se hace así?:
select id_pedido from pedido where id_cliente = $el_cliente
select * from pedido_detalle where id_pedido in (1, 2, ..., n)
¿Cómo identifico que esto está sucediendo?
- La aplicación responde lentamente obteniendo información.
- Si cuentas el número de consultas que haces a la base de datos y lo comparas con el número de ítems del negocio siendo procesados, encontrarás una relación cercana de 1 a 1.
¿Qué me pasó a mi?
Cuando desarrollas para plataformas móviles, siempre debes tener en cuenta también algo: memoria.
Hice que la aplicación que carga lentamente los pedidos muestre un mensaje de «Espere por favor»para que el usuario’pierda un poco menos’ la paciencia mientras se analizaba el problema. El problema N+1 estaba en el código obteniendo todos los pedidos con una consulta a la base de datos pero instanciando las líneas de cada pedido con una consulta a la vez. Se solucionó la lentitud con una sola consulta, Join de pedidos y detalle de pedidos, la respuesta era inmediata pero mas temprano que tarde un java.lang.OutOfMemoryError aparecía de vez en cuando.
Esto no lo entendía puesto que la forma anterior debería ocasionar el mismo problema. Esto no era así porque en otra sección del código, luego de recuperar el pedido deseado, los detalles no utilizados eran descartados (se liberaba memoria) pero vueltos a cargar al volver a la pantalla anterior (lentitud de nuevo… pero funciona).
Cuando trabajas con poca memoria se debe analizar el impacto de la solución. La selección de únicamente los id del pedido que se necesitaban para instanciar los detalles y la consulta de únicamente las columnas necesarias para la instanciación de los detalles solucionó el problema. Eso, y por supuesto, no descartar las líneas de los demás salvo cuando se cambie de cliente.
El problema recién descrito parece ser trivial, pero no se asombren de si de vez en cuando pecan de hacerlo porque fue la solución rápida, la máquina donde se ejecuta tiene el poder para eso, o simplemente llevamos días sin dormir y el trabajo tenía que terminarse como fuese.
En un próximo post seguimos con dos problemas mas de desempeño relacionados con bases de datos y Java.