Soporte de control de versiones de Elasticsearch

Uno de los principios clave de Elasticsearch es permitirte aprovechar al máximo los datos. Históricamente, la búsqueda era un proyecto de solo lectura en el que se cargaban datos de una sola fuente en un motor de búsqueda. A medida que el uso aumenta y Elasticsearch se vuelve más esencial para tu aplicación, los datos deben actualizarse desde varios componentes. El uso de distintos componentes lleva a la concurrencia, y la concurrencia lleva a conflictos. El sistema de control de versiones de Elasticsearch está para ayudar a afrontar esos conflictos.

La necesidad del control de versiones, un ejemplo

Para ejemplificar la situación, supongamos que tenemos un sitio web que las personas usan para calificar un diseño de camiseta. El sitio web es simple. Incluye todos los diseños y permite a los usuarios darle un "pulgar arriba" o bajar su calificación con el ícono de "pulgar abajo". Para cada camiseta, el sitio web muestra el promedio actual de votos positivos y negativos.

Un registro para cada motor de búsqueda se ve así:

curl -XPOST 'http://localhost:9200/designs/shirt/1' -d'
{
"name": "elasticsearch",
"votes": 999
}'

Como puedes ver, cada diseño de camiseta tiene un nombre un contador de votos para hacer un seguimiento del promedio actual.

Para mantener la simpleza y escalabilidad, el sitio web es completamente libre de estado. Cuando alguien mira una página y hace clic en el botón de voto positivo, envía una solicitud de AJAX al servidor, que debería indicar a Elasticsearch que actualice el contador. Para hacerlo, una implementación básica tomará el valor de votos actual, lo aumentará en uno y lo enviará a Elasticsearch:

curl -XPOST 'http://localhost:9200/designs/shirt/1' -d'
{
"name": "elasticsearch",
"votes": 1000
}'

Este enfoque tiene un defecto importante; puede perder votos. Supongamos que Adán y Eva están mirando la misma página al mismo tiempo. En ese momento, la página muestra 999 votos. Como ambos son fanáticos, los dos hacen clic en el botón de voto positivo. Ahora, Elasticsearch recibe dos copias idénticas de la solicitud anterior para actualizar el documento, y lo hace con gusto. Eso significa que en lugar de tener un recuento de votos total de 1001, el recuento de votos ahora es 1000. ¡Ups!

Por supuesto, la API de actualización te permite ser más inteligente y comunicar que el voto puede aumentarse en lugar de configurarse con un valor específico:

curl -XPOST 'http://localhost:9200/designs/shirt/1/_update' -d'
{
"script" : "ctx._source.votes += 1"
}'

Hacerlo de esta forma significa que Elasticsearch primero recupera el documento de forma interna, realiza la actualización y lo vuelve a indexar. Si bien esto hace que haya muchas más posibilidades de que se complete correctamente, sigue teniendo el mismo problema potencial que antes. Durante esta pequeña ventana entre la recuperación y la nueva indexación de los documentos, puede haber inconvenientes.

Para enfrentar la situación anterior y ayudar en otras más complejas, Elasticsearch incluye un sistema de control de versiones integrado.

Sistema de control de versiones de Elasticsearch

Cada documento que almacenas en Elasticsearch tiene un número de versión asociado. Ese número de versión es un número positivo entre 1 y 2 63-1 (inclusive). Cuando indexas un documento por primera vez, recibe la versión 1, y puedes verlo en la respuesta que devuelve Elasticsearch. Esto es, por ejemplo, el resultado del primer comando cURL en este blog:

{
"ok": true,
"_index": "designs",
"_type": "shirt",
"_id": "1",
"_version": 1
}

Con cada operación de escritura en este documento, ya sea de indexación, actualización o eliminación, Elasticsearch aumentará la versión en 1. Este aumento es atómico y está garantizado si la operación se completa correctamente.

Elasticsearch también devolverá la versión actual de los documentos con la respuesta de las operaciones get (recuerda, son en tiempo real), y además se le puede indicar que la devuelva con cada resultado de búsqueda.

Bloqueo optimista

Volvamos a nuestro ejemplo. Necesitábamos una solución para una situación en la que potencialmente dos usuarios intentan actualizar el mismo documento al mismo tiempo. Tradicionalmente, esto se resuelve con bloqueo: antes de actualizar un documento, uno lo bloqueará, hará la actualización y liberará el bloqueo. Al bloquear un documento, te aseguras de que nadie pueda modificarlo.

En muchas aplicaciones esto también significa que si alguien está modificando un documento, nadie más podrá leerlo hasta que la modificación se haya completado. Este tipo de bloqueo funciona, pero tiene un costo. En el contexto de sistemas de alto rendimiento, tiene dos desventajas principales:

  • En muchos casos, simplemente no es necesario. Si se hace correctamente, los conflictos no son habituales. Por supuesto, sucederán, pero solo en una pequeña parte de las operaciones que completa el sistema.
  • El bloqueo supone que realmente te importa. Si solo quieres proveer una página web, probablemente no te preocupe tener algunos datos ligeramente desactualizados, pero un valor consistente, incluso si el sistema sabe que cambiará en breve. Las lecturas no siempre deben esperar a que se completen las escrituras en curso.

El sistema de control de versiones de Elasticsearch te permite usar con facilidad otro patrón llamado bloqueo optimista. En lugar de hacer un bloqueo cada vez, le indicas a Elasticsearch la versión del documento que esperas encontrar. Si el documento no cambió en el transcurso de ese lapso, la operación se completa con éxito, sin bloqueo. Si algo cambió en el documento y hay una versión más nueva, Elasticsearch te lo indicará para que puedas resolverlo correctamente.

Si retomamos el ejemplo del voto del motor de búsqueda anterior, sucederá lo siguiente. Cuando proveemos una página sobre un diseño de camiseta, anotamos la versión actual del documento. Esta se devuelve con la respuesta de la solicitud get que hacemos para la página:

curl -XGET 'http://localhost:9200/designs/shirt/1'
{
"_index": "designs",
"_type": "shirt",
"_id": "1",
"_version": 4,
"exists": true,
"_source": {
"name": "elasticsearch",
"votes": 1002
}
}

Una vez que la usuaria emitió su voto, podemos indicar a Elasticsearch que solo indexe el valor nuevo (1003), si nada cambió en ese lapso: (observa el parámetro de texto de búsqueda version adicional).

curl -XPOST 'http://localhost:9200/designs/shirt/1?version=4' -d'
{
"name": "elasticsearch",
"votes": 1003
}'

Internamente, todo lo que Elasticsearch tiene que hacer es comparar los dos números de versión. Esto es mucho más ligero que adquirir y liberar un bloqueo. Si nadie cambió el documento, la operación se completará correctamente con un código de estado 200 OK. Sin embargo, si alguien modificó el documento (y por lo tanto aumentó su número de versión interno), la operación fallará y tendrá un código de estado 409 Conflict. Nuestro sitio web ahora puede responder correctamente. Recuperará el documento nuevo, aumentará el recuento de votos e intentará nuevamente usar el valor de versión nuevo. Lo más probable es que tenga éxito. De lo contrario, solo repetimos el procedimiento.

Este patrón es tan común que el endpoint de actualización de Elasticsearch puede hacerlo por ti. Puedes configurar el parámetro retry_on_conflict para indicarle que reintente la operación en caso de conflicto de versiones. Es útil, en especial, en combinación con una actualización con script. Por ejemplo, este cURL indicará a Elasticsearch que intente actualizar el documento hasta 5 veces antes de fallar:

curl -XPOST 'http://localhost:9200/designs/shirt/1/_update?retry_on_conflict=5' -d'
{
"script" : "ctx._source.votes += 1"
}'

Ten en cuenta que la comprobación de control de versiones es totalmente opcional. Puedes elegir aplicarla mientras actualizas ciertos campos (como votes) e ignorarla cuando actualizas otros (en general, campos de texto, como name). Todo depende de los requisitos de tu aplicación y las compensaciones.

¿Ya tienes un sistema de control de versiones?

Junto a su soporte interno, Elasticsearch funciona bien con las versiones de documentos que mantienen otros sistemas. Por ejemplo, puedes tener los datos almacenados en otra base de datos que mantiene el control de versiones o puedes tener alguna lógica específica de la aplicación que establece cómo deseas que se comporte el control de versiones. En estas situaciones, aún puedes usar el soporte de control de versiones de Elasticsearch, indicándole que use un tipo de versión external. Elasticsearch funcionará con cualquier sistema de control de versiones numérico (en el rango 1:263-1) siempre y cuando esté garantizado que aumente con cada cambio que se haga en el documento.

Para indicar a Elasticsearch que use un control de versiones externo, agrega un parámetro version_type junto con el parámetro version en cada solicitud que cambie los datos. Por ejemplo:

curl -XPOST 'http://localhost:9200/designs/shirt/1?version=526&version_type=external' -d'
{
"name": "elasticsearch",
"votes": 1003
}'

Mantener el control de versiones en otra parte significa que Elasticsearch no tiene que saber necesariamente cada cambio que se haga. Esto tiene implicaciones sutiles respecto a cómo se implementa el control de versiones.

Considera el comando de indexación anterior. Con el control de versiones internal, significa "solo indexa esta actualización de documento si la versión actual es igual a 526". Si la versión coincide, Elasticsearch la aumentará en uno y almacenará el documento. Sin embargo, con un sistema de control de versiones externo, no podemos aplicar este requisito. Quizá ese sistema de control de versiones no aumenta de a uno por vez. Tal vez usa números arbitrarios (como control de versiones basado en la hora). O quizá es difícil comunicar cada cambio de versión a Elasticsearch. Por todos estos motivos, el soporte de control de versiones external se comporta ligeramente diferente.

Con version_type configurado como external, Elasticsearch almacenará el número de versión tal como se proporciona y no lo aumentará. Además, en lugar de comprobar una coincidencia exacta, Elasticsearch solo devolverá un error de conflicto de versiones si la versión almacenada actualmente es igual o mayor que la del comando de indexación. Esto significa efectivamente "solo almacena esta información si nadie más proporcionó la misma versión o una más reciente en este lapso". En concreto, la solicitud anterior tendrá éxito si el número de versión almacenado es menor que 526. Los valores 526 y mayores provocarán la falla de la solicitud.

Importante: Al usar el control de versiones external, asegúrate de agregar siempre la version (y version_type) actual a cualquier llamada de indexación, actualización o eliminación. Si lo olvidas, Elasticsearch usará su sistema interno para procesar la solicitud, lo que hará que la versión se aumente de forma incorrecta.

Unos últimos comentarios sobre eliminaciones.

Eliminar datos es problemático para un sistema de versiones. Una vez que los datos se eliminan, no hay forma de que el sistema sepa correctamente si las solicitudes nuevas están desactualizadas o en realidad contienen información nueva. Por ejemplo, supongamos que ejecutamos lo siguiente para eliminar un registro:

curl -XDELETE 'http://localhost:9200/designs/shirt/1?version=1000&version_type=external'

Esa operación de eliminación fue la versión 1000 del documento. Si nos deshacemos de todo lo que sabemos al respecto, una próxima solicitud que esté desincronizada hará acciones incorrectas:

curl -XPOST 'http://localhost:9200/designs/shirt/1/?version=999&version_type=external' -d'
{
"name": "elasticsearch",
"votes": 3001
}'

Si nos olvidáramos de que alguna vez existió el documento, aceptaríamos esta llamada y crearíamos un documento nuevo. Sin embargo, la versión de la operación (999) nos indica que esto está desactualizado y que el documento debe permanecer eliminado.

Probablemente pienses que la solución es fácil, solo no hay que borrar todo, sino recordar las operaciones de eliminación, las id de los documentos correspondientes y su versión. Si bien eso sí resuelve este problema, tiene un costo. Pronto nos quedaremos sin recursos si las personas indexan documentos y los borran de forma reiterada.

La búsqueda de Elasticsearch logra un equilibrio entre ambas situaciones. Mantiene los registros de las eliminaciones, pero los olvida luego de un minuto. Esto se denomina eliminación de recolección de basura. En los casos de uso más prácticos, 60 segundos es suficiente para que el sistema se ponga al día y que lleguen las solicitudes demoradas. Si no sirve en tu caso, puedes cambiarlo configurando index.gc_deletes en tu índice a otro lapso.