viernes, 13 de febrero de 2009

Evitar superar el maxClauseCount en Lucene

Uno de los errores típicos en Lucene es el siguiente:

org.apache.lucene.search.BooleanQuery$TooManyClauses: maxClauseCount is set to 1024

Esto normalmente viene provocado por hacer una consulta con carácteres comodines (* y ?). Cuando hacemos una consulta de este tipo, WildcardQuery, lo que hace internamente Lucene es sustituir nuestra consulta con comodines por una consulta sin ellos. ¿Cómo? Pues mira los campos que cumplan el patrón de la consulta y hace un OR con cada uno de ellos.


Mejor veamos un ejemplo:

Supongamos que tenemos indexado documentos y queremos encontrar todos aquellos cuyo campo título empiece por "cas". Para ello haremos la siguiente consulta:

titulo: cas*

Al tratarse de una WildcardQuery, Lucene miraría en todos los campos "titulo" del índice y tomaría aquellos que empiezan por "cas", por ejemplo supongamos que existen 3: "casa", "casita" y "caserio". Lucene sustituiría la consulta por:

titulo: casa or titulo: casita or titulo: caserio


En este ejemplo, vemos que una simple consulta que para mí tiene únicamente una única clausula, realmente se convierte en una consulta con 3 clausulas. Si en lugar de existir sólo 3 campos "titulo" que empiecen por "cas" existieran más de 1024, nos daría el error que he comentado al principio.

Conclusión: no usar comodines. Por desgracia, esto no es siempre posible, pero si se piensa un poco a la hora de indexar, sí que podemos minimizar los casos de uso de las WildcardQuerys.

Indexar una estructura de carpetas o en árbol

Es muy corriente el uso de Lucene para indexar estructuras en árbol como un sistema de ficheros en un gestor de contenidos. Cuando se tiene una estructura en árbol se usa mucho la consulta por ruta o path. Supongamos que tenemos una carpeta A, que dentro tiene otra, B y ésta a su vez tiene otra, C. A su vez cada carpeta tiene ficheros dentro. La ruta hacia C sería /A/B/C y la de cualquier fichero en C /A/B/C/nombre_fichero.

Es lógico indexar en cada Document de Lucene un campo llamado path que contenga la ruta. De esta forma, podemos buscar un contenido por ruta:

path: /A/B/C/nombre_fichero

o buscar los hijos de C (todos los que su ruta empiece por la de C)

path: /A/B/C/*

Dependiendo de los hijos que tenga C, podremos superar el maxClauseCount. Y lo más seguro es que tarde o temprano lo hagamos.

Para evitar este problema, lo que podemos hacer es:
  1. Seguimos indexando el campo path para hacer consultas por ruta pero sin *.
  2. Indexamos la ruta de la carpeta padre, por ejemplo en el campo parent_path. Esto nos servirá para consultar los hijos de una carpeta.
  3. Indexamos como campo múltiple la ruta de todas las carpetas padres. Por ejemplo, llamemosle ancestor_path. Esto nos sirve para buscar los hijos, nietos, bisnietos,.... de una carpeta.

Dicho esto, para el fichero /A/B/C/nombre_fichero, indexaríamos lo siguiente:
  • path: /A/B/C/nombre_fichero
  • parent_path: /A/B/C
  • ancestor_path: /A
  • ancestor_path: /A/B
  • ancestor_path: /A/B/C
Tened en cuenta que ancestor_path es un campo múltiple.

Si queremos obtener un contenido a partir de su ruta, la consulta seguiría siendo la misma:

path: /A/B/C/nombre_fichero

pero si queremos obtener los hijos de C ya no tenemos que usar comodines:

parent_path: /A/B/C

y si quisieramos obtener todos los hijos de A, incluido nietos tampoco:

ancestor_path: /A

De esta forma, estamos evitando el error del maxClauseCount en este tipo de consultas, que por otro lado suelen ser muy habituales si se trabaja sobre una estructura en árbol.

Si vuestro caso es otro, quizás pensando un poco podáis encontrar una solución alternativa a los comodines. Y si no, no tendréis más remedio que aumentar el maxClauseCount, pero como se suele decir, "eso es pan para hoy, pero hambre para mañana".

Por último, decir que en Lucene no van bien las consultas con "/", por lo que tendréis que transformar el path antes de indexar y antes de consultar, de forma que sustituyáis las "/" por otro/s caráctere/s. Podéis usar las solución que hace Alfresco, que consiste en transformar las "/" por "_x" o algo así. Es una ISO, pero no recuerdo el número. En cuanto lo encuentre lo actualizaré.

Espero que os sea de utilidad.

martes, 10 de febrero de 2009

Configuración del teclado en la consola tty en Linux (Debian)

La configuración del teclado en un entorno X Window, como X.Org, se define en los ficheros de configuración propios del X Window. Pero, ¿qué ocurre cuando no tenemos levantadas las X? Por ejemplo, en la consola tty. ¿Cómo configuramos la distribución del teclado?

Éste ha sido mi problema durante un tiempo. Tenía perfectamente configurado mi sistema en el entorno gráfico: teclado español para todos los usuarios, ... ; pero cuando abría una consola tty, normalmente como root, el teclado tenía la configuración americana. Al principio creía que era cosa de la configuración del usuario root, pero ejecutaba el comando locale y el idioma era el correcto:

LANG=es_ES.UTF-8
LC_CTYPE="es_ES.UTF-8"
LC_NUMERIC="es_ES.UTF-8"
LC_TIME="es_ES.UTF-8"
LC_COLLATE="es_ES.UTF-8"
LC_MONETARY="es_ES.UTF-8"
LC_MESSAGES="es_ES.UTF-8"
LC_PAPER="es_ES.UTF-8"
LC_NAME="es_ES.UTF-8"
LC_ADDRESS="es_ES.UTF-8"
LC_TELEPHONE="es_ES.UTF-8"
LC_MEASUREMENT="es_ES.UTF-8"
LC_IDENTIFICATION="es_ES.UTF-8"
LC_ALL=

El quid de la cuestión estaba en lo que he expuesto en el primer párrafo. En el entorno gráfico la distribución del teclado se configura en los ficheros de las X Windows, X.Org en mi caso:

bash$ dpkg-reconfigure xserver-xorg

pero en la consola tty al no haber X Window, eso no vale. Basta reconfigurar el paquete console-data para solucionar el problema

bash$ dpkg-reconfigure console-data

Si alguien tiene el mismo problema que yo ya sabe cómo solucionarlo.

Que conste que no tengo nada en contra de los teclados americanos, lo que pasa es que cuando tienes un teclado en el que el símbolo de cada tecla no se corresponde con lo que escribe en pantalla te vuelves loco buscando ciertos caracteres tan útiles como "/", "-" o "|". Y no digamos si te pones a editar en vi ¿dónde están los ":"?

En fin, para no discriminar a nadie se puede decir que esto sirve en general para configurar el teclado correctamente en la consola tty. ;-)

domingo, 1 de febrero de 2009

Depurar fugas de memoria en Java (Tomcat)

Hola a todos. Por fin logro sacar algo de tiempo para escribir el primer post de este año. Esta vez está relacionado con la depuración de bugs relacionado con la memoria.

Si tienes problemas de memoria con Tomcat, o en general con aplicaciones Java(OutOfMemoryException), necesitas o simplemente tienes curiosidad de saber la distribución de la memoria de tu aplicación, no sigas buscando, este es tu post.

Las opciones que voy a mostrar son opciones de configuración de la Máquina Virtual Java (JVM), por lo tanto son aplicables tanto a una simple clase con un método main, como a una aplicación sobre Tomcat o cualquier otro servidor de aplicaciones. Sin embargo, en este post voy a tomar como ejemplo Tomcat, ya que es lo que uso en el desarrollo de aplicaciones web.

Más concretamente utilicé estas opciones para investigar un problema que teníamos con Tomcat. Cada cierto intervalo regular de tiempo, el Tomcat se caía debido a un error de OutOfMemory. Por más que aumentamos la memoria, seguía cayéndose aunque a intervalos más grandes, por lo que empezamos a sospechar que había algo en nuestra aplicación que iba consumiendo memoria y no la liberaba. Nuestra aplicación tenía fugas de memoria.

Para resolverlo necesitabamos saber dónde estaba el problema, y al igual que ocurre cuando te rompes un hueso, el mejor diagnóstico se hace si tienes una radiografía de la memoria. Por suerte Java nos permite hacer esto. Para ello sólo hay que usar el comando jmap.

Antes de continuar comentar que todo lo que expongo aquí es para Java 6. Con Java 5 también lo podéis hacer a partir de cierta versión, pero el formato de los comandos cambia un poco. De todas formas, mi recomendación es que paséis a Java 6. El paso es casi inmediato y os ahorraréis muchos quebraderos de cabeza con estos comandos y opciones.

Para hacer un volcado de la memoria del Tomcat, tendréis que ejecutar lo siguiente:

bash$ jmap -dump:file=heap.hprof <pid>

donde <pid> es el id del proceso Tomcat. Podéis sacarlo con el comando ps:

bash$ ps -ef | grep tomcat
usuario 13832 1 79 22:37 pts/0 00:00:14 /usr/lib/jvm/java-6-sun/bin/java -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager -Djava.util.logging.config.file=/opt/tomcat/conf/logging.properties -Djava.endorsed.dirs=/opt/tomcat/common/endorsed -classpath :/opt/tomcat/bin/bootstrap.jar:/opt/tomcat/bin/commons-logging-api.jar -Dcatalina.base=/opt/tomcat -Dcatalina.home=/opt/tomcat -Djava.io.tmpdir=/opt/tomcat/temp org.apache.catalina.startup.Bootstrap start

En mi caso el pid sería 13832 y por tanto la ejecución del comando jmap quedaría así:

bash$ jmap -dump:file=heap.hprof 13832
Dumping heap to /opt/apache-tomcat-5.5.25/heap.hprof ...
Heap dump file created

Esto genera el fichero heap.hprof en el directorio donde ejecutéis el jmap. Este fichero es un volcado de lo que hay en la memoria del Tomcat, y por tanto ocupará tanto como la memoria del Tomcat. Comprobad que tenéis espacio en disco suficiente para generarlo.

Sin embargo, no necesitábamos una radiografía de la memoria en cualquier momento, la necesitábamos justo en el momento en que el hueso se rompía, es decir, cuando el Tomcat lanza la OutOfMemoryException. Por suerte, contamos con las opciones de la JVM. Para que Tomcat -y repito: Tomcat o cualquier aplicación Java- genere un volcado de la memoria al producir una OutOfMemoryException tenéis que poner entre sus JAVA_OPTS las siguientes opciones:

-XX:HeapDumpPath=/ruta/donde/guardar/el/fichero/ -XX:+HeapDumpOnOutOfMemoryError

Observad que es un más(+) lo que hay antes de HeapDumpOn..... Si ponéis un menos(-), no os funcionará. Es un error muy típico ;-)

En el caso del Tomcat, tendremos que editar el fichero catalina.sh y meter las opciones en la variable CATALINA_OPTS. Por ejemplo:

CATALINA_OPTS="-XX:HeapDumpPath=/tmp/ -XX:+HeapDumpOnOutOfMemoryError [...resto de opciones...]"

Un Tomcat funcionando con estas opciones generará un fichero en /tmp llamado heap<pid>.hprof automáticamente cuando se produzca una OutOfMemoryException. Más información sobre esas opciones en Java HotSpot VM Options.

Una vez configurado de esta forma, ya sólo teníamos que esperar a que el Tomcat volviese a fallar. Y de eso estábamos seguros. Una vez con el fichero hprof generado podemos usar dos herramientas para procesarlo.

Una es VisualVM. Es de Sun y viene con el JDK de Java 6. Es el comando jvisualvm que se encuentra en el directorio bin del JDK. Con esta herramienta puedes navegar por los objetos que hay en memoria, ver las clases que más instancias tienen y que más consumen, etc. Te muestra lo que hay en memoria literalmente, pero para mi gusto es demasiada información en bruto y quizás sea complicado ver lo que está pasando.

La otra herramienta es Eclipse Memory Analyzer. Esta herramienta procesa la información y saca gráficas y estadísticas útiles. Incluso te propone posibles errores. Con esta herramienta fue con la que rápidamente descubrimos el error, ya que la gráfica de tarta de la ocupación de la memoria estaba al 75% ocupada por cierta parte de la aplicación. Se trataba de una de nuestras cachés. De ahí, muy hábiles nosotros, dedujimos que el proceso de limpiado de esa caché no estaba funcionando correctamente y por tanto, esta caché era un saco donde se metían cosas, pero nunca salían. :-p

Espero que esto os ayude a resolver este tipo de problemas si es que lo tenéis, y si no tienes problemas de este tipo, ¿nunca te has preguntado cuanto ocupa tu aplicación en memoria y qué objetos ocupan más? Merece la pena probar.