En la anterior parte de este artículo realizamos un recorrido sobre las características de los motores en memoria en general. Además nos centramos en analizar con algo más de detalles las características específicas de uno de los motores en memoria más usados, VoltDB.

En esta segunda parte vamos a centrarnos en la parte más práctica y configuraremos un cluster de VoltDB sobre el cual realizaremos algunas pruebas de rendimiento. Esto nos permitirá tener referencias válidas respecto a qué podemos esperar con este tipo de motores.

¿Cómo instalar un cluster de VoltDB basado en contenedores?

Gracias al uso de contenedores es realmente sencillo levantar un cluster de VoltDB. El primer paso será crear una red específica para VoltDB y de esa forma nos aseguraremos de no tener problemas de comunicación entre los nodos.

docker network create -d bridge voltdb

A continuación solo tendremos que lanzar las siguientes líneas en un entorno que tenga instalado Docker:

docker run -d -P -e HOST_COUNT=3 -e HOSTS=voltdb1,voltdb2,voltdb3 –network=voltdb –name=voltdb1 voltdb/voltdb-community:latest
 
docker run -d -P -e HOST_COUNT=3 -e HOSTS=voltdb1,voltdb2,voltdb3 –network=voltdb –name=voltdb2 voltdb/voltdb-community:latest
 
docker run -d -P -e HOST_COUNT=3 -e HOSTS=voltdb1,voltdb2,voltdb3 –network=voltdb –name=voltdb3 voltdb/voltdb-community:latest
 

Una vez estén los tres contenedores creados veremos que en unos pocos segundos conformarán un cluster. Es importante que observemos los mensajes de arranque para saber en qué tipo de configuración estamos:

initializing voltdb

Es decir, con la configuración por defecto no tenemos alta disponibilidad, ni durabilidad ni seguridad habilitada. Este es un escenario “ideal” desde el punto de vista de rendimiento ya que no vamos a tener que replicar datos entre nodos ni escribir en disco ni validar ningún tipo de permiso pero alejada de lo que suele ser requerido en entornos empresariales.

¿Cómo conectar con la base de datos?

Una vez tenemos el cluster levantado tendremos que conectarnos a éste. En los comandos Docker lanzados hemos especificado con el parámetro -P que se realice un mapeo de los puertos de los contenedores a puertos locales.

Para ver los mapeos podemos usar el comando “Docker port nombrecontenedor”:

docker port voltdb1

docker port voltd

De estos mapeos de puertos nos interesan principalmente el mapeo del puerto 8080 (portal de administración) y el mapeo del puerto 21212 (endpoint de conexión).

Preparando nuestra tabla de pruebas

Para crear nuestra tabla de pruebas comenzaremos accediendo al portal del cluster. Para ello abriremos en un navegador y conectaremos al puerto mapeado al 8080 (127.0.0.1:49184 en mi caso). El portal veremos que nos ofrece capacidades de monitorización, administración e incluso ejecución de consultas:

voltdb

A la hora de crear una tabla en memoria debemos tener en cuenta los tipos de datos que soporta VoltDB, que son bastante reducidos comparados con los motores tradicionales:

table a1

Para las pruebas vamos a crear una tabla que almacene información sobre ciertos sensores y vamos a testear cómo de rápido podemos insertar y cómo de rápido podemos modificar los datos. El particionado lo realizaremos en base al identificador del sensor que será a su vez la clave primaria. La tabla de ejemplo será la siguiente:

query

Preparando la carga

Vamos a realizar una operación sencilla de UPSERT como carga de ejemplo.

UPSERT INTO Sensor (ID, Value, Maximum, Minimum) VALUES (NOW(), 1,2,3);
 

Para lanzar la carga utilizaremos el driver JDBC (el driver ODBC de VoltDB parece abandonado/no disponible) y la herramienta dbstress. Un primer test lo realizamos apuntando únicamente a uno de los nodos del cluster para ver qué comportamiento tenemos:

bases de datos memoria voltdb

Con esta configuración la CPU del cluster de VoltDB se estanca en un 12.5% (1/8 de 8 cores, 1 único core en uso al 100%), con una latencia de unos 60ms y un throughput de unas 1800 transacciones por segundo:

cpu
cluster latency
cluster transactions

Estos números a priori parecen extremadamente pobres por lo que el primer cambio pasa por modificar la cadena de conexión y añadir los tres nodos del cluster para intentar distribuir la carga:

bases de datos memoria voltdb

Pese a realizar dicho cambio, el rendimiento es exactamente el mismo, no hay mejoras. La distribución de los datos entre las particiones es correcta y equilibrada:

data volume by table

El problema parece ser que, en un momento dado, con un valor NOW() concreto, estamos impactando siempre una misma partición y por tanto acaba siendo una partición “caliente” lo cual nos limita el rendimiento. Para probar esta teoría vamos a crear varios tests (10) dentro de la configuración de dbstress y atacando cada uno a valores distintos:

unit name 1

Al eliminar el uso del NOW y generar accesos a filas de distintas particiones vemos que alcanzamos unas 25 mil transacciones por segundo de media con una latencia media de 4-5 milisegundos, siendo todas las operaciones a efectos prácticos UPDATES (testeamos también modificando el UPSERT por UPDATE y obtuvimos resultados similares):

tps
cluster latency 99

¿Y qué me podría ofrecer mi motor relacional tradicional?

El último test configurado, que ejecuta básicamente updates masivamente sobre unas pocas filas, es ideal para mostrar cómo un motor tradicional como SQL Server se comportaría. Crearemos primero la tabla:

 

CREATE TABLE Sensors (
ID BIGINT NOT NULL PRIMARY KEY,
Value DECIMAL,
Maximum DECIMAL,
Minimum DECIMAL)
 

Y vamos a lanzar el mismo test de los updates, con 1000 conexiones concurrentes, sobre SQL Server y observamos cuantas peticiones por segundo podemos soportar:

batch

Podemos ver que nos quedamos en algo menos de 11000 transacciones de media por segundo, lo cual es menos de la mitad que con VoltDB. La razón es que, usando el modelo clásico de almacenamiento en disco, se generan una gran cantidad de bloqueos:

task state

También debemos tener en cuenta que estamos en un modelo donde tenemos asegurada la durabilidad de cada uno de estos updates, algo que en esta configuración con VoltDB tenemos deshabilitado para maximizar el rendimiento.

¿Cómo se comportaría VoltDB con una parametrización más realista?

El primer paso sería configurar un k-factor de 1, de forma que tengamos al menos dos copias de cada dato volátil en memoria. De esta forma podríamos sobrevivir al menos a la caída de un nodo. Para realizar esta configuración deberemos crear un fichero de deployment personalizado:

xml

Si relanzamos la carga con este cambio podemos ver cómo bajamos de unas 25000 transacciones por segundo de media a unas 17000. También vemos como la latencia es algo mayor aunque sigue siendo buena:

cluster trans
cluster latency percentile

Idealmente deberíamos testear la durabilidad estricta, habilitando el commandlog (similar a un log de transacciones) en modo síncrono. Con dicha configuración habilitada muy probablemente tendríamos otra caída adicional en el rendimiento. Desgraciadamente la versión Community (gratuita) no permite esta configuración, únicamente la versión comercial Enterprise, por lo que no podemos testearlo.

¿Qué ocurre si migramos a In-Memory OLTP en SQL Server?

Como comentamos en la primera parte de este artículo, una de las ventajas que tenemos con SQL Server es que podemos tener bases de datos “híbridas” donde parte de la información esté en memoria y otra siga con su formato clásico. Vamos a crear una nueva tabla (sensors_inmem) y vamos a lanzar la misma carga sobre dicha tabla:

CREATE TABLE dbo.Sensors_inmem (
ID BIGINT NOT NULL PRIMARY KEY NONCLUSTERED HASH WITH (BUCKET_COUNT=1000000),
Value DECIMAL,
Maximum DECIMAL,
Minimum DECIMAL
) WITH (MEMORY_OPTIMIZED=ON)

Lanzando 1000 conexiones contra esta tabla optimizada en memoria, que garantiza la durabilidad de cada una de las transacciones, conseguimos llegar a unas 30000 transacciones por segundo:

batch request

Y realmente el mayor problema en este caso es el consumo de recursos que estamos haciendo, ya que solo tenemos 8 cores disponibles. Si optimizamos el consumo de recursos utilizando un procedimiento almacenado en vez de un UPDATE adhoc podemos llegar a más de 50000 transacciones por segundo con estos mismos 8 cores y asegurando la durabilidad (DURABILITY = SCHEMA_AND_DATA):

batch request sec

¿Es SQL Server In-Memory OLTP mejor alternativa que VoltDB?

Como casi todo, la respuesta es “it depends”.

Si nos planteamos un escenario de un desarrollo nuevo, donde queremos tener una base de datos en memoria que sea escalable horizontalmente sobre decenas de nodos pequeños y poder con ello llegar a gestionar 1 millón de transacciones por segundo… utilizar In-Memory OLTP no es una opción razonable. Recordemos también que los motores escalables horizontalmente en memoria, sean VoltDB o SingleStore, etc. sacrifican propiedades de los motores relacionales, como las Foreign Keys, o en la durabilidad/alta disponibilidad o añaden restricciones al modelo de datos para conseguir esta escalabilidad y rendimiento extra.

Si por el contrario lo que buscamos es solucionar un problema ya existente en una base de datos SQL Server (o en otro motor relacional tradicional similar) y no vamos a necesitar esa escalabilidad horizontal masiva (nos vale con escalabilidad vertical) sí podríamos tener un buen resultado con SQL Server In-Memory OLTP. Esta alternativa supone habitualmente muchos menos cambios en la aplicación y ello implicaría que la podríamos tener lista para producción en mucho menos tiempo.

Conclusión

A día de hoy apostar por una base de datos relacional 100% en memoria implica en muchos casos de prescindir de muchas funcionalidades a cambio de una mayor escalabilidad en el rendimiento para escenarios concretos. Si venimos de un motor tradicional y solo requerimos más rendimiento tiene mucho sentido intentar utilizar las funcionalidades in-memory que tengamos disponibles en nuestro motor. Por el contrario si estamos diseñando un sistema nuevo cuyos requisitos encajan perfectamente con bases de datos en memoria escalables horizontalmente como  VoltDB o SingleStore éste debería ser el camino a seguir.

Personalmente creemos que el futuro del OLTP son las bases de datos 100% en memoria y posiblemente su popularización vendrá de la mano del aumento de la disponibilidad de memoria persistente (no volátil). El uso de este tipo de memoria permitirá maximizar las ventajas de las bases de datos en memoria y eliminar o minimizar muchas de sus restricciones en lo que respecta a la durabilidad o a la tolerancia a fallos sin pérdida de datos.

¡Sigue Aprendiendo!

Si quieres que te ayudemos a poner en marcha este tipo de proyecto, no dudes en contactar con nosotros.

También puedes aprovechar las formaciones existentes para ampliar tus conocimientos, como nuestra formación de Kubernetes, con la que podrás dar el salto en tu carrera profesional y sacarle el máximo partido a esta tecnología 👇🏼

>> Más info
0 Shares:
Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

You May Also Like
Leer más

Expresiones, parámetros y funciones en Azure Data Factory

Hay ocasiones, cuando estamos construyendo pipelines con Azure Data Factory, que queremos repetir patrones para extraer y procesar la información cambiando de manera dinámica, en tiempo de ejecución, valores, orígenes/destinos de los datasets, incluso los mismos linked services. Esto es posible mediante el uso de parámetros, expresiones y funciones. Vamos a ver cómo implementarlo con un ejemplo práctico en el que se nos plantea el siguiente supuesto. Se nos ha pedido que extraigamos todos los días los datos del día anterior de distintas tablas del DW a ficheros en un blob storage que además se nombre como la tabla de origen. Si no pudiéramos utilizar contenido dinámico tendríamos que crear dos datasets (uno de origen y otro de destino) y añadir una actividad de copia por cada tabla a exportar.