Tag Archives: Optimización

Utilizar MongoDB como sistema de caché

Prácticamente cualquier servicio online con un mínimo de tráfico y de complejidad requiere de un sistema de cache para aligerar su carga y reducir el tiempo de respuesta de ciertas peticiones, que deben ser aquellas peticiones más comunes y aquellas más costosas en cuanto a recursos que consumen.

En este punto siempre surge la incógnita sobre que sistema de caché utilizar, y factores como el tamaño y la estructura de los datos, o la velocidad de lectura y escritura son variables que debemos tener en cuenta a la hora de elegir uno.

Una muy buena opción desde mi punto de vista es utilizar Mongo para cachear datos. La idea es que tienes una colección por cada tipo de información que quieres cachear:

db.micache = {  
   _id:String,
   datos: { 
      ... todo lo que queremos cachear ...
   },
   fecha:Date
}

Donde:

  • El _id es de tipo String y es una cadena de caracteres en cuya generación participan todas las variables que influyen en el resultado a cachear. Es decir, si por ejemplo queremos cachear los contactos de un usuario, el _id debe ser generado con el identificador del usuario, algo del estilo “contacts_007”. Otro ejemplo sería si quisiéramos cachear los contactos compartidos por dos usuarios diferentes cualesquiera, en este caso generaríamos algo como “shared_007_x_199”, que almacenaría datos diferentes a otra entrada que podría ser “shared_007_x_203”.
  • En datos guardaremos lo que queramos cachear, sea del tipo que sea, aprovechando la flexibilidad de Mongo que nos va a permitir almacenar cualquier cosa sin tener que definir ningún esquema previo. Además otra de las ventajas de Mongo es que los datos guardados pueden ser del tamaño que queramos, sin importar lo grandes que sean.
  • Por último en fecha se almacenará la fecha de creación del objeto cacheado, y además crearemos un índice con tiempo de expiración, para que sea el propio Mongo automáticamente el que borre de caché los objetos que hayan cumplido el tiempo de expiración. En la siguiente línea decimos que cualquier documento guardado expirará a los 14.400 segundos, es decir, estamos creando una cache de 4 horas.
db.micache.createIndex( { "fecha": 1 }, { expireAfterSeconds: 14400 } )

 

Las ventajas que aporta esta solución son:

  • Es extremadamente flexible en cuanto a la estructura de datos a guardar. Texto, números, mapas, listas, cualquier tipo de estructura puede guardarse directamente en Mongo, y será aquel que lo guarde y recupere el que le sepa como manejarlo.
  • Disponemos de una capacidad de almacenamiento muy alto, no limitado a la memoria RAM que tengamos disponible. El disco es más barato que la memoria, y nos va a permitir cachear sin tener que preocuparnos prácticamente por lo que consumamos.
  • Si estamos utilizando un Replica Set de Mongo, tendremos además un sistema de cache con alta disponibilidad y escalable, ya que podemos distribuir las lecturas entre los nodos secundarios. Además si añadimos un nuevo servidor de aplicaciones, o tenemos que reiniciar alguno, tendrá inmediatamente toda la información cacheada a su disposición, lo que no ocurriría si cada servidor de aplicaciones guarda su cache en su memoria local.

El inconveniente que le podemos atribuir es:

  • Al ser acceso a disco no será tan rápido como es el acceso a memoria, pero como siempre vamos a acceder por _id será suficientemente rápido para la mayoría de los casos requeridos.

Yo lo estoy utilizando en sistemas en producción con un excelente resultado.

Advertisements

Solr, uso de cursorMark para paginaciones profundas (deep paging)

Solr tiene un problema de rendimiento cuando empezamos a pedirle páginas muy profundas, lo conocido como deep paging. Es decir, dada una búsqueda, el tiempo de respuesta en devolver las primeras páginas de resultados y el tiempo en devolver las páginas, pongamos, 5000, 5001, etc es muy diferente, siendo este último caso el más penalizado.

Es este magnífico post explican el funcionamiento del cursorMark, cuya idea principal es que Solr se guarda en memoria un cursor apuntando a la página profunda que le has pedido, e identifica dicho cursor con una clave única. Posteriormente si quieres seguir la paginación en ese punto, solo tienes que pasarle de nuevo esa clave única, y Solr irá directamente a la siguiente página de resultados, ahorrando muchísimo tiempo al tenerlo ya en memoria.

El funcionamiento un poco más detallado es el siguiente:

Query a Solr con parámetro cursorMark=*

Al hacer la primera query, con los criterios de búsqueda que se necesiten, se añade un parámetro adicional cursorMark=*

http://localhost:8983/solr/example?q=*:*&rows=100&cursorMark=*

Con esto le estamos indicando a Solr que cree un nuevo cursor para esta consulta.

 

Solr devolverá la clave del cursorMark.

En el response, Solr añadirá la clave con la que identificar el cursor.

<?xml version="1.0" encoding="UTF-8"?>
<response>
 <lst name="responseHeader">
  ...
  </lst>
 </lst>
<result name="response" numFound="32" start="0">
 <doc>
 ...
 </doc>
 </result>
 <str name="nextCursorMark">AoIIP4AAACgwNTc5QjAwMg==</str>
</response>

 

Siguiente query a Solr con parámetro cursorMark=CLAVE

Con la segunda query le pasamos el cursorMark recibido en el response de la primera query, sin especificarle otro parámetro start. Al recibir este parámetro Solr sabrá que tiene que continuar por donde se había quedado y nos devolverá el siguiente bloque de documentos.

http://localhost:8983/solr/example?q=*:*&rows=100&cursorMark=AoIIP4AAACgwNTc5QjAwMg==

Solr devolverá la siguiente clave del cursorMark. En el response, Solr añadirá la nueva clave con la que identificar el cursor para el siguiente bloque de resultados

<?xml version="1.0" encoding="UTF-8"?>
<response>
 <lst name="responseHeader">
  ...
  </lst>
 </lst>
<result name="response" numFound="32" start="0">
 <doc>
 ...
 </doc>
 </result>
 <str name="nextCursorMark">AoIIP4AAACoxMDAtNDM1ODA1</str>
</response>

Y así podemos continuar mientras se quiera.

Yo intenté utilizar este mecanismo de Solr en un proceso batch en el que lanzaba queries que me devolvían muchos resultados, estoy hablando de cientos de miles, y necesitaba procesarlos todos, del primero al último. Pero la gran mejora en rendimiento que se producía inicialmente se iba degradando con el tiempo hasta dejarme el servidor frito por exceder el uso de memoria que tenía asignado.

La cuestión es que todos estos cursores Solr los va almacenando en memoria, de ahí que en caso de utilizarlos vayan tan rápidos, pero si pedimos demasiados, no tendrá nunca suficiente espacio. Por tanto, el cursorMark es una buena opción cuando vamos a reutilizar mucho los cursores, no cuanto tenemos una variabilidad alta.

¿Y como solucioné en mi caso el problema del deep paging? Pues troceando la query original, que me devolvía muchas páginas de resultados, en N “subqueries” cada una de las cuales me devolvía unas pocas páginas. Algo así:

Mi query original era más o menos así:

http://localhost:8983/solr/example?q=expresion de busqueda&start=0&rows=10000

que de esa forma, me devolvía cientos de miles de documentos y por tanto tenía que ir paginando con el consiguiente incremento del tiempo de respuesta cuanto más profunda era la página pedida.

http://localhost:8983/solr/example?q=expresion de busqueda&start=10000&rows=10000
http://localhost:8983/solr/example?q=expresion de busqueda&start=20000&rows=10000
http://localhost:8983/solr/example?q=expresion de busqueda&start=500000&rows=10000
...etc

Para trocearlo en subqueries busqué otro campo que no influyera en cada búsqueda concreta, del que pudiera conocer los valores mínimos y máximos, que me sirviera para lanzar queries por rangos más o menos del mismo tamaño y que me devolviera pocas páginas. Por suerte tenía mi identificador interno de los datos que indexaba, un autoincremental de tipo Long que cumplía estos criterios. Llamemos a este campo myID.

Manos a la obra, cogí myID y transformé la query original en N subqueries con filtro por rango de myID.

http://localhost:8983/solr/example?q=expresion de busqueda&fq=myID:[0 TO 200000]&start=0&rows=10000
http://localhost:8983/solr/example?q=expresion de busqueda&fq=myID:[0 TO 200000]&start=10000&rows=10000
http://localhost:8983/solr/example?q=expresion de busqueda&fq=myID:[2000000 TO 400000]&start=0&rows=10000
http://localhost:8983/solr/example?q=expresion de busqueda&fq=myID:[2000000 TO 400000]&start=10000&rows=10000

Está claro que no me ahorré por completo la paginación, pero si tener que irme a páginas extremadamente profundas para las que Solr tarda mucho tiempo en responder.

Además, al utilizar Filter Queries (fq), aprovechaba el cacheo que hace Solr de dichos Filter Queries, con lo que otras búsquedas del mismo proceso con diferente expresión en la query, pero con los mismos rangos de filter queries, se beneficiaban del cacheo previo.

http://localhost:8983/solr/example?q=otra busqueda&fq=myID:[0 TO 200000]&start=0&rows=10000

Con este enfoque conseguí leer de Solr enormes cantidades de resultados de búsqueda sin colapsar en ningún caso el servidor por uso excesivo de memoria.

 

Limitar el uso de memoria de scripts en Groovy

Cuando lanzas múltiples procesos Groovy en una misma máquina, debes de tener cuidado con el consumo de memoria que cada proceso hace en particular para garantizar el buen uso global de dicha memoria, de manera que no malgastes innecesariamente.

En este aspecto Groovy no se comporta como yo esperaba, ya que aún sabiendo que mis procesos no requerían más de 128Mb, al lanzarlos y medirlos veía que inicialmente se me iban hasta 700Mb y que solo con el tiempo acababan reduciendo su uso por debajo de los 128Mb. Además, este descenso no era inmediato, sino que necesitaba bastante tiempo para producirse, con lo que por el camino era muy fácil que me “petara” la máquina.

Por suerte la solución es sencilla, solamente hay que establecer el uso de memoria del script Groovy en el momento de lanzarlo.

JAVA_OPTS="-Xmx128m -Xms64m -Dfile.encoding=UTF-8" groovy -c utf8 -Dfile.encoding="UTF-8" my_groovy_script.groovy

Con la instrucción anterior le estoy dando al script una memoria inicial de 64Mb, y una memoria máxima de 128Mb, con lo que me aseguro que siempre fluctúa en ese intervalo.

Como optimizar procesos de inserción masiva en MongoDB

Mongo DB es un document storage NoSql que tiene una capacidad de escritura muy alta, y realmente la tiene, por mi experiencia mucho más que por ejemplo MySql. Pero cuando empiezas a cargarlo de verdad, con procesos de inserción masiva, del orden de millones de documentos, la cosa ya no va sola, y necesitas empezar a desarrollar de una forma determinada para conseguir que tus procesos sigan yendo a toda pastilla. Aquí te cuento algunos de los que he descubierto a base de leer en algunos casos, y de tortas en otros.

No gestionar las transacciones

Ni se te ocurra gestionar las transacciones en este tipo de procesos. Mongo tiene la virtud de poder “insertar y olvidar”, es decir, de enviar los datos a insertar al servidor, y no esperar ningún tipo de respuesta, ni de confirmación por su parte. Esto lo consigues especificando un WriteConcern.UNACKNOWLEDGED, que quiere decir eso, que no te interesa si la inserción ha ido bien o mal. Eso si, para utilizar esto, ya te tienes que haber preocupado previamente de asegurarte que tus inserciones no van a fallar, sino, mal lo tendrás para poder verificar lo que ha ido bien o mal.

Ejecuta un único save para cada documento, nunca update

El método save es mucho más rápido que el método update, ya que directamente escribe, sin verificar si previamente existe el registro o no. Nunca, repito, nunca desarrolles procesos de inserción masiva que quieras que vayan rápido y que no se degraden al crecer las colecciones en los que hagas uno o varios updates. Nuevamente, en este proceso, tendrás que preocuparte de tener a tu alcance toda la información que quieres guardar para cada documento, júntala durante N procesos e inserta una única vez.

Contra colecciones vacías

El tiempo de inserción en colecciones vacías en Mongo es mas bajo que cuando tiene datos. Plantéate en tu proceso si es posible al inicio del mismo borrar completamente la colección y volver a generarla de nuevo con los datos que vas a insertar. Aunque parezca mentira, en muchos casos puede ser más rápido hacer de nuevo una inserción completa que N parciales.

Contra colecciones sin índices

Si tus colecciones tienen índices, bórralos con dropIndex antes de empezar a insertar, ejecutas, y al finalizar vuelves a crear el indice con ensureIndex. El tiempo de creación del índice global es mucho menor que el tiempo que se tiene que invertir en actualizar el mismo índice ya creado en cada inserción de cada documento.

En colecciones más pequeñas

Trocea tus colecciones, tanto la inserción como la recuperación de datos van a ir más rápidas contra diez colecciones de 2 millones de documentos, que contra una única colección de 20 millones. Además, esto te permitirá paralelizar tus procesos y reducir la duración total.

Utiliza diferentes databases

Es incluso preferible utilizar diferentes databases para datos temporales o que vayas a regenerar por completo en ese ciclo, que diferentes colecciones en la misma base de datos. Esto te permitirá incluso hacer dropDatabase que liberará el espacio en disco que Mongo reserva para la db, cosa que no hace si eliminas la colección.

Y hasta aquí mis pequeños trucos para optimizar la inserción masiva en Mongo. Si tienes algún truco que no haya dicho, no dudes en comentármelo.