miércoles, 17 de junio de 2015

TLS/SSL y los problemas con los certificados

Desde hace unos años vengo trabajando con certificados, de SSL/TLS y de firma electrónica para eFactura, ambos funcionan con un esquema similar, solo que uno es para asegurar conexiones y el otro para asegurar la legitimidad de los documentos intercambiados.

Ahora me dispongo a compartir parte de lo que he aprendido, intentando dar una guía rápida que permita entender y solucionar rápidamente los problemas más comunes al trabajar con certificados. Vale la pena dedicarle 5 o 10 minutos para entenderlo porque luego ahorra mucho tiempo y frustraciones.

En mayor o menor medida todos sabemos que cuando vemos el candadito en el navegador, nos indica que es una conexión segura, esto tiene 2 aspectos: la autenticación y la encriptación, ya que no alcanza con encriptar si no sabemos si estamos hablando con quien queremos o un impostor. Siendo la autenticación la primer etapa, es donde surge la mayor parte de los problemas porque requiere tener instalados adecuadamente los certificados.

Esta es una simplificación de como se da la conversación entre un cliente y un servidor:


Los certificados raíz representan Entidades Certificadoras, estas son empresas en las que confiamos que solo emiten certificados a entidades legítimas, por ejemplo confiamos en que no le va a emitir un certificado para example.com a alguien que no sea dueño de ese dominio.

Entonces el servidor responde con su cadena (el certificado raíz no es necesario) y el cliente verifica en su almacén de certificados de confianza si tiene el certificado raíz que completa esa cadena, una vez que lo encuentra usa herramientas criptográficas para asegurarse de que todos los certificados (intermedios y final) son legítimos. La autenticación de cliente es opcional, si el servidor la requiere el cliente le envía su certificado y el servidor hace la validación análoga, pero la mayoría de los servicios web no requieren este tipo de autenticación.

Algunas apreciaciones:
  • El Trust store y el Key store pueden ser el mismo almacén, pero están diferenciados porque tienen funciones distintas. Por ejemplo en Java se configuran por separado y en Windows se manejan todos juntos categorizados según su uso.
  • Un servidor puede actuar como cliente de otro, entonces en el servidor también hay que configurar el Trust store adecuadamente. Esto es muy frecuente en sistemas web que requieren servicios de terceros.
  • En SSL/TLS un servidor debe enviar la cadena con el certificado final y todos los certificados intermedios (puede ser más de uno), el certificado raíz lo puede enviar pero no es necesario porque el cliente igual debe verificar que esté en su Trust store.
  • En firma electrónica es común que los certificados intermedios no se incluyan, por lo que también hay que agregarlos al Trust store. Esto no es lo ideal pero si queremos validar estos documentos no tenemos alternativa.
¿Por qué son necesarios los certificados intermedios?
La razón principal es seguridad. Una entidad certificadora utiliza su clave privada para emitir certificados intermedios, por o que debe ser guardada con altas medidas de seguridad, si se utilizara cada vez que se emite un certificado final significa que tendría un nivel de exposición alto comparado con utilizarla solo cuando se necesita un nuevo certificado intermedio.
Además si se viera comprometido el certificado intermedio, al entidiad certificadora puede revocarlo y emitir un nuevo certificado intermedio, de esta forma, el certificado raíz que ya está instalado en millones de equipos en todo el mundo sigue siendo válido.

Hasta aquí la parte conceptual, ahora algo de información específica de Java.

Troubleshooting en Java

  • java.io.IOException: Keystore was tampered with, or password was incorrect:
    Lo más probable es que tengamos mal cargada la password y por eso no puede leer el Keystore.
  • javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed:
    El error indica que no se pudo construir la cadena desde el certificado final hasta un certificado raíz de confianza. Lo más probable es que no tengamos cargado el certificado raíz o los intermedios en nuestro Trust store. Otra posibilidad es si nos queremos conectar a un servicio con certificado autofirmado, por ejemplo un servidor de mail interno, en ese caso tenemos que importar el certificado autofirmado (final y raíz a la vez).
    Otro escenario común en Uruguay es con los servicios que están certificados por el Correo Urugayo, como los certificados raíz no vienen precargados en Java tenemos que importarlos manualmente.
  • javax.net.ssl.SSLHandshakeException: Received fatal alert: bad_certificate:
    Este error puede aparecer cuando el servidor nos exige autenticación de cliente y nuestro sistema no le envía un certificado adecuado.

Truststore y Keystore en Java

Se configuran con las siguientes propiedades:
  • javax.net.ssl.trustStore: "/path/to/keystore"
  • javax.net.ssl.trustStoreType: "JKS"
  • javax.net.ssl.trustStorePassword: "changeit"
  • javax.net.ssl.keyStore: "/path/to/keystore"
  • javax.net.ssl.keyStoreType: "PKCS12"
  • javax.net.ssl.keyStorePassword: "contraseña"
y se puede configurar de las siguientes formas:
  • Como parámetro de Java: -Djavax.net.ssl.trustStore=/path/to/keystore
    Recomendado.
  • Con código: System.setProperty("javax.net.ssl.trustStore", "/path/to/keystore");
    No recomendado porque si el Truststore ya fue inicializado esta asignación no va a tener ningún efecto.
En Java el Truststore por defecto se llama "cacerts" y viene cargado con entidades certificadoras donde se encuentran las más comunes. La ubicación varía en cada sistema operativo, en Windows está en la carpeta lib\security\cacerts se la instalación de Java, mientras que en linux varía de una distribución a otra, en general el archivo lib/security/cacerts suele apuntar a una ruta centralizada para cualquier instalación de Java, por ejemplo /etc/pki/java/cacerts.

Además tener en cuenta que los servidores web puede utilizar las ubicaciones por defecto o establecer su propia ubicación, por ejemplo Tomcat usa la por defecto de Java mientras que GlassFish asigna su propia ruta.

Como agregar un certificado raíz o autofirmado al Truststore de Java

Se necesita la herramienta keytool que viene con la JDK (no alcanza con la JRE).

Primero hay que encontrar el archivo cacerts que queremos actualizar.

Para verificar los certificados que hay dentro podemos usar el comando:
> keytool -list -keystore cacerts
la contraseña por defecto es: changeit
Para importar un certificado:
> keytool -import -alias "Nombre del Certicado" -file CertificadoRoot.cer -keystore cacerts
el alias es solo un nombre descriptivo con el que quedará guardado, no es necesario que coincida con nada, solo un nombre con el que nos sea fácil volver a encontrarlo.

Conozco el sitio al que me quiero conectar. ¿Cómo puedo obtener su certificado raíz?

Con un navegador es fácil investigar el certificado, pongo un ejemplo con Chrome, pero en todos se hace de forma similar.

Luego usamos Copy to File para guardarlo y poder importarlo a nuestro almacén.

¿Y si el browser no me muestra la cadena?
Significa que el browser tampoco tiene instalado el certificado raíz, en ese caso vamos a la pestaña Details y le buscamos el Issuer, luego se puede buscar la página oficial del Issuer para descargar sus certificados.


Espero que esta guía les haya sido útil, no entra demasiado en profundidad sino que apunta a explicar conceptualmente como funciona el sistema de certificados y a resolver los problemas más comunes con los que me he encontrado yo y otras personas a las que he ayudado.

viernes, 20 de febrero de 2015

Recuperación de un Windows Server 2003

Hace poco tuvimos un problema, dejó de arrancar el Windows Server 2003 que actuaba de Controlador de Dominio. A veces lograba empezar a iniciar Windows pero antes de terminar de iniciar se apagaba sin ni siquiera una pantalla azul (nunca la extrañe tanto).
Al probar con un Live CD pasaba lo mismo con lo cual se confirmó que existía un problema de hardware, lo cual no era sorprendente ya que tenía unos cuantos años.

El plan era sencillo, comprar un servidor nuevo instalar un Windows Server nuevo y restaurar todo de los backups, pero nunca es tan sencillo, no?

Para poder migrar el Active Directory en un nuevo servidor debe hacerse con ambos servidores online, no se puede recuperar desde un archivo. A su vez como el hardware había fallado la única alternativa era iniciar en un hardware nuevo. El primer paso fue respaldar todo lo que había en los discos en otra PC. Luego utilizamos una PC en desuso para iniciar Windows, pero aparentemente su hardware era demasiado nuevo y no logró arrancar, con lo cual tuvimos que pasar a una PC más vieja y ahí empezamos a tener un poquito más de suerte, pero no se lograba completar el inicio de Windows.

Primer paso de riesgo, utilizando el CD de Windows 2003 hacer una reparación del sistema.
La primera vez tampoco logramos iniciar Windows, así que tuvimos que realizar una reinstalación completa de Windows (sin formatear), aquí empezamos a tener un poco más de suerte y el mismo arrancó pero totalmente limpio.
Algo importante es que este paso elimina la carpeta Windows, así que antes de ejecutarlo es impresindible respaldarla, aunque sea con un LiveCD, de lo contrario sería imposible recuperar la instalación original.
Otra cosa, creo que es importante (aunque no lo pude confirmar) es utilizar la misma contraseña que tenía originalmente o la contraseña de recuperación de Active Directory (DSRM), esta puede ser distinta a la contraseña del Administrador del Dominio.

Una vez que logramos iniciar Windows Server, se instaló el Service Pack 2 y se hizo una restauración del Respaldo. El Respaldo solo tenía el System-State, o sea algunas subcarpetas de Windows, Registro, Active Directory, etc. NOTA: Antes de hacer este paso restaurar manualmente System32 como se muestra más abajo.

Aquí empezaron de vuelta los problemas, al restaurar el estado anterior resultó que al System32 le faltaban montones de DLLs que eran necesarias para un inicio normal, incluso pedía reactivar Windows para poder iniciar sesión, pero fallaba en traer el diálogo de activación y se reiniciaba el sistema.
Después de varios intentos logramos iniciar en modo a prueba de fallos con símbolo del sistema y utilizando el comando explorer trajo el escritorio.
Aquí pudimos recuperar los archivos faltantes de System32 y de toda la carpeta Windows con el siguiente comando:
robocopy Windows\ C:\Windows\ /E /COPYALL /XC
Esto podría hacerse también con un LiveCD, lo importante es copiar solo los archivos faltantes y no reemplazar ningún archivo.

Además aprovechamos para instalar los drivers de la nueva tarjeta de red, que era distinta a la del hardware original.

Después de reiniciar Windwos arrancó normalmente, pidiendo reactivación por cambios de hardware, pero esta vez me daba 3 días para activarlo, así que se pudo iniciar sesión por más que aún no se tenía conectividad de red.

Active Directory levantó y pudimos configurar el nuevo servidor para unirse al dominio, luego se dio de baja el servidor viejo y se dejó el nuevo servidor como principal.

Datos y herramientas que conocí durante el proceso:
  • Para conectar un Windows 2012 a Controlador de Dominio 2003 hay que asegurarse que "el bosque" esté en nivel funcional 2003. Esto requiere primero subir el DC a 2003 y luego el bosque.
    Si hay algún DC viejo desconectado no se permitirá subir el nivel funcional, con lo cual deberá eliminarse manualmente de la base de datos de Active Directory.
  • La base de datos de Active Directory se guarda por defecto en C:\Windows\NTDS\ntds.dit
  • ntdsutil: permite verificar integridad de los archivos de Active Directory, también permite hacer alguna corrección de datos. Por ejemplo fue necesaria para eliminar de la base de Active Directory metadatos obsoletos de un servidor viejo que ya no existía pero pretendía seguir replicando contra él.
  • esentutl: permite reparar un ntds.dit dañado, luego de reparar hay que quitar los .log de la carpeta de NTDS y reiniciar.
  • dsamain: permite levantar el ntds.dit como un LDAP, y se puede consultar con la herramienta ldp.exe, esto puede ser muy útil se se quiere consultar la información para crear un dominio desde cero.
  • ProfWiz: Afortunadamente no lo tuve que usar, pero me lo recomendaron para migrar perfiles de Windows de un dominio viejo a uno nuevo (en el caso de crearlo desde cero).
La lección principal aprendida es que conviene siempre tener más de un Controlador de Dominio, tal vez lo más sencillo sea tener una máquina virtual que funcione como controlador principal o de respaldo, esto es bueno ya que una máquina virtual debería ser bastante más fácil de restaurar.

viernes, 28 de noviembre de 2014

Genexus y nulos

Este análisis se centra en como definir nulos en bases de conocimiento Genexus pero es extrapolable a cualquier modelo de datos (RDBMS, SQL, NoSQL).


¿Qué es un NULL?
En Genexus estamos acostumbrados a que es algo parecido a un Cero o a un String vacío según el tipo de datos, ya que Genexus trataba de ocultarnos los nulos utilizando nullvalues de esos tipos. Más adelante el concepto de nulo se desdobló en 2 conceptos: el NULL y el EMPTY que viene a tomar el lugar del nullvalue cuyo uso es preferible evitar por lo confuso que resulta su nombre.

Entonces un NULL es un valor indefinido (información inexistente o faltante) y tiene mucho sentido en las bases de datos relacionales cuando una relación es opcional. Ej. una Factura puede o no estar asociada a Orden de Compra.

Como ejemplo de lo indefinido que es un valor NULL, algunos RDBMS ordenan los nulos al principio (MSSQL y MySQL) y otros al final (Oracle, PostgreSQL).
Lo cierto es que ambos están correctos, porque un Nulo es indefinido por lo tanto no es ni menor ni mayor que otros valores, por eso algunos DBMS agregan cláusulas NULLS FIRST y NULLS LAST para controlar donde queremos que nos ordenen los nulos.


¿Cuando tiene sentido usar Nulos?

En mi experiencia, en un modelo de datos los lugares donde tiene sentido la utilización de Nulos son los siguientes:
  • Claves Foráneas opcionales (FK). Ej: Una Factura puede tener nulo en su atributo Orden de Compra, ya que es opcional.>
  • Fechas opcionales. Ej: Fecha de cobrada una factura, mientras la factura no esté cobrada el valor es indefinido.
  • Textos o Blobs opcionales. Ej. Una descripción vacía.

¿Qué significa Empty as Null?

Esto es una propiedad que Genexus usa en los atributos para controlar si querémos que un valor vacío se almacene como NULL. Según lo expresado antes, solo algunos tipos de atributo deberían tener la propiedad Empty as NULL = Yes.


¿Y la columna Nullable en la estructura de las transacciones?

Define si el atributo admite Nulos en esa transacción. No es lo mismo que Empy as Null pero ambos juegan juntos para dar el comportamiento final.
Ej. El atributo Orden de Compra tiene Empy as null = Yes porque es clave. A su vez, es opcional en la transacción de Facturas, pero obligatorio en la transacción de Remitos.
Entonces el mismo atributo va a llevar Nullable = Yes en Facturas y Nullable = False en Remitos. Es decir, el atributo puede guardar como nulo, pero en la transacción de Remitos no admite Nulos.


Voy entendiendo la idea pero me gustaría algo más concreto

Ahí van mis sugerencias. En un modelo Genexus (extrapolable a otros modelos de datos), tengo que definir los atributos de esta forma:


Tipo de atributoEmpty as nullNullable
Identificador/Clave no importa de que tipo de datos YES YES - solo si es opcional
Fecha, Texto, Blob YES YES - solo si es opcional
Cantidad, Importe, Dimensión, Booleano, Flag NO NO


¿Para qué me sirve todo esto? ¿Por qué no defino todo nullable o todo no nullable?

Las ventajas de usar valores Nulos ya fueron explicadas, nos da una expresividad mayor, ¿Para qué voy a guardar una fecha 01/01/0001 o 31/12/1753? Es mucho mejor gurdar un NULL que deja mucho más claro que el valor está indefinido.
Otro ejemplo son los DBMS que no aceptan textos nulos, por ejemplo un VarChar en Oracle se debe guardar un nulo o al menos un espacio, pero no acepta textos vacíos.

Entonces: ¿Por qué no admitir nulos en todos lados?
La respuesta a esta pregunta tiene 2 puntas:
  • Integridad en las FK: Si yo definí que los Remitos siempre tienen que tener asociada una Orden de Compra, no quiero que se pueda agregar un registro sin Orden de Compra, entonces en ese caso defino que no puede ser Nulo. Esto es a nivel de la base de datos, pero además Genexus utiliza esta información para validar en el Form que el usuario ingrese una Orden de Compra y de lo contrario no lo deja avanzar.
  • Las operaciones aritméticas con nulos están indefinidas. Por lo tanto no es conveniento utilizarlo en atributos quantitativos ni booleanos.
    Por ejemplo: admito nulos en el Cantidad de un Producto. Cuando agregue un Producto nuevo, se va a guardar Cantidad = NULL.
    ¿Que pasa si recibo 5 unidades y las quiero sumar el Inventario?
    Aquí se abren 2 nuevas posibilidades:
    • Genexus lee el valor, suma y guarda el resultado:
      Aquí Genexus nos oculta que existía un nulo y cuando lee el valor lo lee como cero, entonces 0 + 5 = 5
    • Genexus optimiza el update y lo genera en SQL:
      Update Productos set Cantidad = Cantidad + 5 = NULL
      NULL + 5 está indefinido por lo que resulta en NULL

Conclusiones:

Es importante prestar atención a la obligatoriedad de cada atributo y el posible uso de nulos. Utilizando las reglas detalladas en el cuadro anterior se obtiene un modelo de datos y base de conocimiento enriquecidos.
Agregando:
  • Información sobre obligatoriedad de los atributos
  • Atributos sin valores por defecto no representativos (Fechas mínimas, textos con un blanco o Blobs con valor 0x00).
  • Valores cuantitativos siempre válidos y evita errores sobre operaciones aritméticas

viernes, 15 de febrero de 2013

Analizando Leaks

Hace unos años publiqué un artículo donde contaba como había encontrado leaks de una aplicación Genexus Java. Ahora me enfrento otro problema levemente distinto, nuevamente en Tomcat tengo una aplicación que aparentemente no tiene leaks, pero cuando actualizamos la versión de la aplicación (redeploy) la versión anterior queda colgada.

Esto apunta a otro tipo de leak, en el que se crea una referencia entre una clase de sistema (de las cuales es propietario el Tomcat) y un objeto de mi aplicación, con lo cual la memoria utilizada por la versión vieja de la aplicación no puede ser reciclada y a la larga se produce un error del tipo:
java.lang.OutOfMemoryError: PermGen space
  Los pasos que utilicé para detectar el problema son:
  1. Crear un dump y levantarlo en jhat explica mi post anterior.
  2. Buscar en jhat las instancias de org.apache.catalina.loader.WebappClassLoader.
  3. Abrir las instancias una por una y quedarme solo con las que tienen la ruta de mi aplicación
  4. Las instancias que no están referenciadas por la clase org.apache.catalina.loader.WebappLoader son leaks.
  5. Elegir una de las instancias para estudiarla y copiar el Id del objeto.
  6. En el menú de jhat abrir Execute Object Query Language (OQL) query y ejecutar:
    select heap.livepaths(o) from org.apache.catalina.loader.WebappClassLoader o where objectid(o) == "XXX"
    Sustituyendo XXX por el id que se quiere estudiar (ej: 0xa3ed2510).
  7. Copiar el resultado en un editor de texto
  8. Reemplazar ", " por ",\r\n" para obtener una línea por referencia
  9. Buscar referencias problemáticas:
    • Java Local Reference
    • System Class Reference
En mi caso encontré clases referenciadas por java.lang.ThreadLocal, que es uno de los casos clásicos de Leaks en Java.

El problema en particular estaba en las clases del JDBC de Oracle: oracle.sql.AnyDataFactory y oracle.jdbc.driver.OracleDiagnosabilityMBean. Cómo no se que posible arreglo tendrá, decidí mover el driver al lib global de Tomcat y de esa forma puedo hacer redeploy de la aplicación sin que el driver deje la versión vieja colgada.

Nota:
Estoy usando Tomcat 6.0.24, en versiones anteriores de Tomcat no existía la detección de Leaks, a partir de esta versión se detectan automáticamente algunos leaks y dicha información queda en el log de Tomcat. En esta versión también se resuelve de forma automática el caso típico de leak debido a que no se desregistra el driver JDBC, pero otros leaks (como los que me enfrento ahora) no se resuelven automáticamente y Tomcat no es capaz de determinar con certeza cuales son las situaciones que efectivamente se convierten en un leak.

martes, 14 de septiembre de 2010

Cambio de nombre y url

Bueno, cambié el nombre del blog y la url, los anteriores me gustaban tan poco que no voy a decir cuales eran.

Ahora es mi nombre y simplemente refleja que en este blog hay artículos sobre temas de los que me interesan. Por otro lado, el blog solía ser un tanto anónimo, capaz que por eso lo tenía descuidado, de repente de esta forma me comprometo más con el mismo.

viernes, 22 de enero de 2010

Comprimiendo el acceso a base de datos


Problema
Con aplicaciones win, a menudo ocurre que tenemos el servidor de base de datos en otro lugar fisico, conectado a traves de VPN o similar. Para este tipo de aplicaciones en Genexus se incorporó la posibilidad de generar en 3 capas, donde hay un servidor de aplicaciones que hace el trabajo pesado contra la base de datos y al cliente solo llegan los datos que se muestran al usuario (al menos en teoría).

Cuando se tiene una aplicacion 2 capas, pasar a 3 capas puede implicar muchos dolores de cabeza, principalmente por procesos que no se pueden ejecutar en el servidor porque interactuan con el usuario, ademas de pantallas que se quedan bloqueadas y otros problemas.

En más de un cliente fue necesario instalar aplicaciones Java 2 capas que se conectaban a un servidor en VPN, pero fue hace muy poco que me puse a investigar el volumen de datos que pasaba por la VPN para ver si habia forma de optimizar la performance. Ahí encontré que hay volúmenes grandes, por ejemplo reportes que trasmiten más de 1MB de datos o transacciones que dada la cantidad de consultas que hacen a la base de datos generan mucho tráfico.

Solución
Dado que Genexus es quien genera los programas, a simple vista parece que no hay mucho que podamos hacer para optimizar el tráfico en una aplicación win 2 capas, pero dimos con una solución sencilla y con un costo de implementación muy bajo, grandes beneficios y estable. Se trata de entunelar y comprimir la conexión entre la aplicación y la base de datos.

Del lado del cliente se precisa un programa que pueda escuchar una conexión TCP/IP común, comprimirla y trasmitirla a un servidor de compresión/descompresión, el primero a su vez se encarga de descomprimir la respuesta del servidor. En la otra punta de la conexión lenta (VPN) está el servidor de descompresión que hace las operaciones análogas, descomprimir las conexiones entrantes para mandarlas limpias al DBMS y comprimir la respuesta del DBMS para que vuelva al cliente.

Las 2 partes de la solución (servidor y cliente) fueron implementadas en Java, que gracias a su excelente manejo de streams y threads obtuvo una implementación muy sencilla, estable y de buen rendiminto. El algoritmo de compresión fue zlib que es parte de la api de Java, lo cual también ayudo a no agregar dependencias.

Cliente
El cliente está embebido en la aplicación y levanta automáticamente al poner el parámetro compression=true en la url de conexión. Simplemente parsea la url buscando el parámetro y si lo encuentra levanta una instancia del cliente en la PC cliente y cambia la url de conexión para que se conecte a esta instancia en vez de conectarse al servidor real.

Servidor
Se instala en el servidor de DBMS o en otro servidor que esté en la misma LAN. Para que esté siempre disponible se instaló como servicio de Windows utilizando JSL (altamente recomendable). El único requerimiento es tener una JVM instalada.

Resultados
Los resultados fueron muy buenos, en términos generales puede decirse que se bajaron a la tercera parte los tiempos de respuesta y como ya se anticipó, con una solución muy estable y con un costo bajo. Hay que tener en cuenta que en todos los clientes la velocidad no superaba los 256kbps es de suponer que a medida que se mejora la velocidad los beneficios disminuyen.

También hay que notar que estos algoritmos agregan procesamiento, especialmente al servidor que escucha las peticiones de todos los clientes. En nuestro caso no era un problema, en una parte porque los clientes no son muchos y en otra parte porque los servidores tienen buena capacidad de procesamiento.

Links de interés
IPTunnelManager - Una herramienta paga que permite hacer túneles comprimidos y encriptados, instalando uno en cada punta de la conexión lenta se podrían comprimir las conexiones al DBMS.

miércoles, 13 de mayo de 2009

Memory leaks en Java

Hace poco tuve un inconveniente desarrollando una aplicación web con Genexus/Java, la memoria que consumía el tomcat crecía hasta que daba un error java.lang.OutOfMemoryError: Java heap space.

El problema se daba durante la ejecución de un proceso batch muy largo y era claro que una vez que terminaba no liberaba la memoria que había utilizado, entonces ¿Como identificar que es lo que está quedando colgado?

Lo primero que usé fue JConsole, que una vez habilité JMX en el tomcat, me permitió ver el consumo de memoria global, como crecía y decrecía, pero seguía sin ver que objetos eran los que estaban quedando colgados.

Busqué varios profilers para Java y encontré que principalmente apuntan a mostrar los tiempos de ejecución de las operaciones, así que en este caso no me servían y me di cuenta que tenía que buscar alguna herramienta para analizar la memoria. Estoy seguro de que existen muchas y hay mejores soluciones que la que utilicé, pero lo que encontré sirvió a mis propósitos y tiene la ventaja y contra de no tener que estar analizando los datos en tiempo real.

Solución: JConsole + jhat (Java Heap Analysis Tool), ambas son herramientas que vienen con el JDK de SUN, simplemente hay que buscarlos en la carpeta bin.

jhat es un programa que dado un "memory dump" levanta un servidor web que permite navegar la memoria en dicho dump, además de presentar útiles histogramas de instancias y cantidad de memoria por clase. Pero hay que tener en cuenta que para un dump grande 100MB+ hay que pasarle parámetros para agrandar el heap del propio jhat, sino se queda sin memoria y hay que sentarse a esperar porque se toma su tiempo para procesar los datos.

Entonces los pasos a seguir son:
  1. Habilitar JMX en el Tomcat, agregar a la configuración los siguientes parámetros:
    -Dcom.sun.management.jmxremote
    -Dcom.sun.management.jmxremote.port=”9004″
    -Dcom.sun.management.jmxremote.authenticate=”false”
    -Dcom.sun.management.jmxremote.ssl=”false”
  2. Ejecutar los procesos que se sabe que dejan memoria colgada o usar la aplicación hasta que la memoria crezca considerablemente.
  3. Iniciar JConsole y conectarse al servidor en el puerto 9004.
  4. En el tab MBeans ejecutar com.sun.management.HotSoptDiagnostic.dump usando como parámetro "C:\dump.bin" (o la ruta que venga bien).
  5. Ejecutar jhat pasando como parámetro el archivo a procesar, opcionalmente se le puede agrandar la memoria de heap porque con archivos grandes puede caer.
    jhat -J-Xmx400m dump.bin
  6. Entrar a la url http://localhost:7000/ donde debería aparecer la página de bienvenida de jhat, con un resumen de las clases que hay en memoria y al final unos links a consultas preparadas. La que me resultó más útil es el histograma.
Una vez que uno entra al histograma presentado por jhat, seguramente va a encontrar algún tipo básico con mayor cantidad de instancias, por ejemplo int o char[], es lógico ya que cualquier objeto está compuesto por tipos básicos, pero inmediatamente abajo se pueden encontrar otras clases con cantidad un poco menor de instancias y probablemente éstas sean las que nos interesan.

Ahora llegamos al punto en el que identificamos cuales son los objetos que están ocupando más memoria, al hacer click en la clase que nos interesa analizar, podemos ver algunos datos interesantes de la misma y debajo una lista de todas las instancias de dicha clase, así que elegimos una instancia cualquiera y podemos empezar a navegar a través de sus referencias, para encontrar el objeto raíz que está dejando las instancias colgadas.

Una vez identificado el objeto, estamos listos para corregir el error, o como en mi caso quejarnos a soporte y tratar de encontrar un workaround!