Optimización de un servicio WEB

Situación tras el arreglo

Antecedentes.

Uno de mis ocupaciones es auditoría y optimización de aplicaciones. Realizaba esto ya en el año 95, sobre un 4GL y BDD relacional Multibase. Ahora, casi 20 años más tarde, J2EE+Oracle, pero se encuentran las mismas burradas carencias en el código, y es que yo opino que si no tienes la cabeza amueblada, la tecnología no te hará mejorar. Algo de conocimientos, metodología y experiencia también ayudan… En fin, no nos dispersemos, vamos a lo concreto.
En este caso, la tarea encomendada consiste en optimizar la implementación de una operación de un WS expuesto, por una de nuestras aplicaciones. El bug principal es una query s-a-l-v-a-j-e que llega a “tirar abajo” la instancia del servidor de BDD en la que se ejecuta.
¿Cuando ocurre? si solo hay una invocación al WS mientras la instancia se vuelve a levantar, origina una pérdida de rendimiento, ya que todas las sesiones se concentran en la otra instancia. Si ocurre que hay otra invocación a la operación del WS mientras una instancia del servidor de BDD se encuentra caída, la sentencia “tira abajo” la única instancia de servidor de BDD que está aguantando, lo que revierte en caída total del sistema.
Obviamente, la implementación de esta integración no genera demasiada confianza, dan ganas de “borrarlo todo y empezar de nuevo”. Sin embargo, la acción a tomar debe ser rápida, en este momento no se requiere un rediseño total de la integración, sino un “golpe de efecto” que con un 10% de esfuerzo, consiga solventar el 90% del problema.

Plan de acción.

  • Al tratarse de código legacy, no cuenta con pruebas unitarias, ni otro tipo de pruebas automatizadas. Por tanto, es neceario proteger el código con una batería de test que aseguren que no rompemos nada.
  • Solo aquello que se puede medir es susceptible de ser mejorado(1) , por tanto la medición es el paso previo para cuantificar la situación actual, y las mejoras conseguidas.
  • Hacer los mínimos cambios para obtener los resultados requeridos.
  • Correr los test elaborados en el punto primero. Estos tres últimos puntos, sin excluir otras prácticas que no detallamos para no complicar este post, se repetirán hasta alcanzar los objetivos.

Descripción del arreglo

Consideramos que no es necesario meter profiler, conocemos la sentencia SQL “sospechosa” por haberla visto en los logs de la caída del servidor de BDD. Se encuentra dentro de un método de un DAO (que es llamado desde un servicio, que a su vez es llamado desde la implementación del WS). La firma del método es:

public List getEntidadFechasNotIn(Date fechaInicio, Date fechaFin, List codigosExcluir) { ... }

Esto es, recupera una lista de entidades que cumplan unas determinadas condiciones (a efectos del ejemplo para simplificar, que “su fecha” esté contenida entre la fecha de inicio y fecha de fin), y que el código no esté contenido en una lista de códigos provistos.

Optimización de un servicio WEB (antes)
La implementación está hecha (a groso modo, y en pseudo-código) con una sentencia del tipo

select lo-que-sea from tabla_soporte_entidad
where fechaini >= $fechaInicio
and fechafin <= $fechaFin
and not codigo in ("codigo1", "codigo2", "codigo3")

siendo “codigo1”, “codigo2”, “codigo3” una lista de códigos elaborada a partir de la lista de códigos a excluir, que el método recibe como argumento. De hecho, el programador da a simple vista la impresión de ser del tipo “astuto”, ya que en previsión (a este punto, creo que a casi todos se nos habría ocurrido) de que vinieran muuuchos códigos en la lista de códigos a excluir, la generación de la sentencia, de tipo dinámico, lo prevee, obteniendo realmente una sentencia del tipo

select lo-que-sea from tabla_soporte_entidad
 where fechaini >= $fechaInicio
 and fechafin <= $fechaFin
 and not (codigo in ("codigo1", "codigo2", "codigo3"...) or
 codigo in ("codigo1001","codigo1002","codigo1003"...) or
 codigo in("codigo2001","codigo2002","codigo2003"...)
 )

Lo de astuto lo decía porque ha “conseguido” un workaround a la limitación de Oracle del máximo número de valores permitidos en una claúsula IN.
Para colmo, la sentencia se complica ya que no es una simple tabla la que soporta la persistencia de la entidad, sino que es una entidad polimórfica donde se ha optado por una estrategia de una tabla por subclase, y el código (murphy’s law) no está en la tabla principal, con lo cual la sentencia realmente queda (insisto, simplificando…)

 select lo-que-sea from tabla_principal, tabla_secundaria_1
 where tabla_secundaria_1.id_principal = tabla_principal.id
 and fechaini >= $fechaInicio
 and fechafin <= $fechaFin
 and not (tabla_secundaria_1.codigo in ("codigo1", "codigo2", "codigo3"...) or
 tabla_secundaria_1.codigo in ("codigo1001","codigo1002","codigo1003"...) or
 tabla_secundaria_1.codigo in("codigo2001","codigo2002","codigo2003"...)
 )
 UNION
 select lo-que-sea from tabla_principal, tabla_secundaria_2
 where tabla_secundaria_2.id_principal = tabla_principal.id
 and fechaini >= $fechaInicio
 and fechafin <= $fechaFin
 and not (tabla_secundaria_2.codigo in ("codigo1", "codigo2", "codigo3"...) or
 tabla_secundaria_2.codigo in ("codigo1001","codigo1002","codigo1003"...) or
 tabla_secundaria_2.codigo in("codigo2001","codigo2002","codigo2003"...)
 )
 UNION
 select lo-que-sea from tabla_principal, tabla_secundaria_3
 where tabla_secundaria_3.id_principal = tabla_principal.id
 and fechaini >= $fechaInicio
 and fechafin <= $fechaFin
 and not (tabla_secundaria_3.codigo in ("codigo1", "codigo2", "codigo3"...) or
 tabla_secundaria_3.codigo in ("codigo1001","codigo1002","codigo1003"...) or
 tabla_secundaria_3.codigo in("codigo2001","codigo2002","codigo2003"...)
 )

Lo que mal empieza, suele acabar peor, y para terminar de ilustraros la dimensión de la picia, os comento que la lista de códigos a excluir es de 2444. ¿El resultado? Ahora si lo pilláis, una sentencia faraónica que “tira abajo” el servidor de BDD.

La mínima mejora posible, con el máximo beneficio, la hacemos con 3 líneas de código, dentro del mismo método del DAO (no es necesario tocar firmas de métodos, ni varias clases, ni varios métodos). Para elaborar la sentencia SQL dinámica se utiliza una colección vacia (en vez de la lista de cödigos a excluir), y posteriormente ésta se tiene en cuenta “subfiltrando” el resultset por JAVA, comprobando que el código de cada una de las entidades recuperadas no se encuentra en la lista de códigos a excluir, antes de “poblar” los objetos y devolverlas.

Optimización de un servicio WEB (después)

Mediciones.

Antes.

A los 37 minutos de ejecución se obtenía error Oracle por falta de recursos para servir la sentencia SQL –salvaje- :

Error en la sentencia: ORA-04030: memoria de proceso insuficiente al intentar asignar 203448192 bytes (kxs-heap-c,temporary memory)

Después.

Con los parámetros iniciales, que abarcaban un periodo de 15 días, el WS contesta en 4’33’’, devolviendo 9618 resultados (manejando 12494 entidades).

Ampliando los parámetros, concretamente abarcando datos de 3 meses, el WS contesta en 29’20”, devolviendo 55279 resultados (manejando 67334 entidades).

LECTURA

Aunque el rendimiento es mejorable, la prueba de “volumen de datos a manejar” se supera con holgura, tras realizar un pequeño arreglo que involucra 3 líneas de código dentro de un único método.


(1) Concepto original atribuido al Dr. H. James Harrington


¿Quieres comentar algo? Tus comentarios son bienvenidos, puedes hacerlo en el formulario “Dejar un comentario” en la parte inferior de este post.

¿Quieres publicar algo interesante? Contacta conmigo y estaré encantado de que colaboremos.

Deja un comentario