Introducción y objetivos
En este artículo presento los resultados obtenidos de un conjunto de tests de inserción de datos y de lectura que he realizado sobre MySQL utilizando particionamiento de tablas
Las pruebas las hemos hecho con la versión 5.5.10 de MySQL y usando MyISAM como motor de almacenamiento. La estructura de la base de datos y el código PHP utilizado como base para la realización de los tests son los mismos que los usados para comparar MongoDB contra MySQL.
El objetivo es comprobar si ganamos o perdemos rendimiento y en qué condiciones dividiendo una tabla con decenas de millones de registros correspondientes a logs de acceso en distintas particiones.
Se han comparado las mismas operaciones realizadas sobre una tabla sin particionar y la misma dividida en 12 particiones (particionamiento por fecha, dividiendo por mes). Las operaciones han sido:
- de escritura simple 10, 20 y 30 millones de registros en bucle uno detrás de otro
- de escritura concurrente de 1 millón de registros con 50 usuarios simultáneos
- de inserción masiva 250 millones de registros a partir de un CSV
- de lectura simple probando varias operaciones: buscar registros por id, contar registros para un mes, contar registros entre entre dos fechas, y obtener totales para un dominio
Para elegir un particionado adecuado debemos entender bien cuales son los patrones de acceso que va a utilizar la aplicación. En nuestro caso serán informes por rangos de fechas, mensuales fundamentalmente, con lo que lo lógico crear particiones para cada mes
Me he basado en los tests realizados por Brooks Johnson y Phillip Tellis, que muestran en las dos siguientes presentaciones
Volumen de los datos
Se ha trabajado con la siguiente cantidad de datos:
- 70.000 usuarios
- 70.000 IP’s
- 1.300.000 dominios
- Una tabla de registros de log con 250 millones de registros
Ésta última tabla es la realmente importante, ya que es la que se ha definido sin particiones y con particiones para comparar distintas operaciones sobre ella. Esta tabla tiene tres índices, un id autonumérico que sirve de clave principal y dos índices para el campo de dominio y el campo de fecha y hora.
En tamaño en disco el espacio ocupado para la tabla de logs es el siguiente:
- Tabla sin particionar: 37.303 Mb (29.067 de datos + 8.236 de índices)
- Tabla particionada: 38.561 Mb (29.067 de datos + 9.494 de índices)
Como podemos observar una tabla particionada ocupa más espacio en disco que la misma sin particionar, en particular los índices (casi un 10% más para los índices que usamos en la tabla de logs)
Parametrización de MySQL
Los tests los haremos en una máquina con 12 Gigas de RAM disponible y en la que la carpeta de archivos temporales (generados a la hora de crear los índices) se ha establecido en una partición aparte para que disponga de espacio.
Siguiendo las recomendaciones de varios blogs y respuestas a preguntas encontradas en Internet (aquí y aquí, por ejemplo) y para aliviar la lentitud de MySQL a la hora de generar los índices cuando el tamaño de éstos supera la memoria RAM de la máquina he establecido en MySQL los siguientes parámetros de configuración:
general_log = 0
tmpdir = /users7/mys55_tmp/
bulk_insert_buffer_size = 2G
myisam_sort_buffer_size = 2G
key_buffer_size = 1G
myisam_max_sort_file_size = 39G
[myisamchk]
key_buffer_size = 1G
tmpdir = /users7/mys55_tmp/
Sintaxis para la creación de las tablas
La estructura de la tabla “NonFTP_Access_log” sin particionar corresponde a la definida en la siguiente consulta SQL CREATE TABLE
`id` INT(11) NOT NULL AUTO_INCREMENT,
`clientip` VARCHAR(15) NOT NULL,
`user` CHAR(7) NOT NULL,
`datetime` datetime NOT NULL,
`method` VARCHAR(10) NOT NULL,
`protocol` VARCHAR(10) NOT NULL,
`domain` VARCHAR(255) NOT NULL,
`uri` VARCHAR(100) NOT NULL,
`return_code` SMALLINT(5) UNSIGNED NOT NULL,
`size` INT(10) UNSIGNED NOT NULL,
PRIMARY KEY (`id`),
INDEX `domain` (`domain`),
INDEX `datetime` (`datetime`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8
Y con particiones, una para cada mes del 2012 a la siguiente
`id` INT(11) NOT NULL AUTO_INCREMENT,
`clientip` VARCHAR(15) NOT NULL,
`user` CHAR(7) NOT NULL,
`datetime` datetime NOT NULL,
`method` VARCHAR(10) NOT NULL,
`protocol` VARCHAR(10) NOT NULL,
`domain` VARCHAR(255) NOT NULL,
`uri` VARCHAR(100) NOT NULL,
`return_code` SMALLINT(5) UNSIGNED NOT NULL,
`size` INT(10) UNSIGNED NOT NULL,
PRIMARY KEY (`id`, `datetime`),
INDEX `domain` (`domain`),
INDEX `datetime` (`datetime`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8
partition BY range COLUMNS (datetime)
(
partition p1 VALUES less than ('2012-02-01 00:00:00') engine = myisam,
partition p2 VALUES less than ('2012-03-01 00:00:00') engine = myisam,
partition p3 VALUES less than ('2012-04-01 00:00:00') engine = myisam,
partition p4 VALUES less than ('2012-05-01 00:00:00') engine = myisam,
partition p5 VALUES less than ('2012-06-01 00:00:00') engine = myisam,
partition p6 VALUES less than ('2012-07-01 00:00:00') engine = myisam,
partition p7 VALUES less than ('2012-08-01 00:00:00') engine = myisam,
partition p8 VALUES less than ('2012-09-01 00:00:00') engine = myisam,
partition p9 VALUES less than ('2012-10-01 00:00:00') engine = myisam,
partition p10 VALUES less than ('2012-11-01 00:00:00') engine = myisam,
partition p11 VALUES less than ('2012-12-01 00:00:00') engine = myisam,
partition p12 VALUES less than ('2013-01-01 00:00:00') engine = myisam
);
Para evitar el problema descrito aquí debemos particionar usando columns
Podemos observar que para poder particionar la tabla por rangos de fechas hemos tenido que añadir el campo utilizado (“datetime”) a la clave primaria.
Por otra parte las clases PHP desarrolladas para generar datos aleatorios han sido modificadas, en particular a la clase MySQLRandomElements se le han añadido funcionalidades.
Resultados de los tests
Los tests se han repetido varias veces para comprobar que los resultados son coherentes, descartando si procede los resultados extremos. Los datos mostrados corresponden a los resultados de uno de ellos.
Tests de escritura simple
La prueba ha consistido en insertar 10 millones de registros, luego añadir 20 millones más y en un tercer paso añadir 30 millones. La fecha y hora de cada uno es un valor aleatorio entre el 1 de enero y el 31 de diciembre del 2012.
Inserción de 10 millones | 20 millones | 30 millones | |
---|---|---|---|
Tabla sin particionar | 2h10m57s | 4h36m16s | 7h0m42s |
Tabla particionada | 2h19m55s | 4h43m23s | 7h15m22s |
El test se ha hecho con un script ejecutado localmente. Las pruebas se han repetido varias veces con resultados similares.
La conclusión es que las inserciones individuales tienen aproximadamente el mismo rendimiento. En tablas particionadas ha habido entre un 2% y un 7% de pérdida, algo normal dado el trabajo extra que supone a nivel gestionar las particiones.
Test de escritura simple 2
En este caso vamos a insertar un millón de registros, añadiéndolos a los 60 millones ya existentes, pero siendo todos de la última semana del año, de forma que todas las inserciones se harán sobre la última partición en el caso de usar la tabla particionada.
El test lo he repetido tres veces, recreando la tabla con los mismos datos y reiniciando el servidor.
Tiro 1 | Tiro 2 | Tiro 3 | |
---|---|---|---|
Tabla sin particionar | 13m55s | 13m51s | 13m48s |
Tabla particionada | 14m49s | 14m40s | 14m19s |
Los resultados son los mismos que en el caso anterior. Las inserciones individuales tienen aproximadamente el mismo rendimiento, con una muy ligera pérdida en el caso de tablas particionadas.
Tests de escritura multiusuario (en todo el rango de fechas)
En este caso hemos utilizado la herramienta Apache JMeter lanzando peticiones en red desde mi puesto al servidor MySQL.
He configurado JMeter para generar 50 usuarios virtuales (hilos) en el que cada uno de ellos hace 20.000 peticiones a un script PHP que añade un registro a la tabla (el registro es siempre el mismo, varía la fecha de forma aleatoria entre el 1 de enero y el 31 de diciembre del 2012). Mostramos las medianas.
Tiro 1 | Tiro 2 | Tiro 3 | |
---|---|---|---|
Tabla sin particionar | 112ms | 108ms | 115ms |
Tabla particionada | 128ms | 103ms | 106ms |
Las inserciones individuales multiusuario tienen aproximadamente el mismo rendimiento
Tests de escritura multiusuario 2 (en la última semana)
Este tests es igual al anterior pero las escrituras se realizan con fecha de la última semana de diciembre, para provocar que todas las escrituras se realicen en la última partición.
Este escenario sería más realista, ya que los logs se van grabando con una fecha, la actual, que va creciendo. Mostramos las medianas.
Tiro 1 | Tiro 2 | Tiro 3 | |
---|---|---|---|
Tabla sin particionar | 138ms | 104ms | 118ms |
Tabla particionada | 126ms | 127ms | 131ms |
No se observa aumento de rendimiento aunque las inserciones afecten a una sola de las particiones
Tests de inserción masiva
La prueba ha consistido en la inserción de 250 millones de registros a partir de un fichero CSV.
El problema fundamental a la hora de hacer este tipo de operaciones es la generación de los índices, que en el caso de superar la memoria RAM disponible se convierte en una operación terriblemente lenta por parte de MySQL (puede llegar a llevar días).
El test se ha hecho con un script ejecutado localmente repitiendo las pruebas varias veces. El script sigue las recomendaciones de la documentación del producto indicadas en “Bulk Data Loading for MyISAM Tables“http://dev.mysql.com/doc/refman/5.5/en/optimizing-myisam-bulk-data-loading.html y hace lo siguiente:
- Desactiva los índices en la tabla
- Carga los datos del fichero CSV
- Vuelve a activar los índices
La desactivación y activación de los índices en tablas MyISAM no particionadas se debe hacer con la herramienta de consola myisamchk, en vez de usar el comando SQL ALTER TABLE, ya que su rendimiento es mucho mayor
Los resultados obtenidos, comparando el tiempo total de inserción y para varias pruebas son:
Tiro 1 | Tiro 2 | Tiro 3 | |
---|---|---|---|
Sin particionar (índices con myisamchk) | 2h26m21s | 2h25m16s | 2h24m41s |
Particionada | 2h40m50s | 2h30m8s | 2h34m21s |
La conclusión es que la generación de índices es mucho más rápida en tablas particionadas, ya que hemos conseguido que quepan en memoria lo que antes provocaba una reconstrucción muy lenta por parte de MySQL.
De todas formas, para tablas MyISAM el uso de la herramienta myisamchk permite solventar este problema, con lo que para la carga de datos masiva no ganamos rendimiento al usar tablas particionadas (al menos con MyISAM).
Tests de lectura
En este caso se han desarrollado 3 scripts que hacen las siguientes operaciones simples de lectura:
- Seleccionar un registro por id, repitiendo la operación 10.000 veces (se ha modificado la estructura de la base de datos para incluir un id autonumérico como clave única)
- Contar el número de registros para cada mes del año 2012
- Contar el número de registros entre dos fechas, con más de un mes entre ellas.
Los tests los hemos repetido 5 veces cada uno, para una tabla particionada y sin particionar, parando y arrancando el servidor antes de la ejecución de cada script.
Tiro 1 | Tiro 2 | Tiro 3 | |
---|---|---|---|
Tabla sin particionar | 2m20s | 2m16s | 2m8s |
Tabla particionada | 4m13s | 3m42s | 3m15s |
Tiro 1 | Tiro 2 | Tiro 3 | |
---|---|---|---|
Tabla sin particionar | 2m44s | 2m40s | 2m34s |
Tabla particionada | 3m11s | 3m2s | 2m58s |
Tiro 1 | Tiro 2 | Tiro 3 | |
---|---|---|---|
Tabla sin particionar | 1m44s | 1m38s | 1m44s |
Tabla particionada | 1m42s | 1m45s | 1m45s |
Sorprendentemente en los tres casos (incluido la operación de contar registros por mes, que a priori debería ir mejor) los tiempos para la tabla no particionada y particionada son casi equivalentes.
Conclusiones
Con lo que, con estos datos, el uso de particiones sería recomendable
- Para gestionar el espacio en disco de volúmenes enormes de datos, pudiendo dividir una tabla entre varios discos.
- Si al dividir las tablas conseguimos que los índices entren en memoria aceleraremos operaciones del tipo ALTER TABLE REBUILD, OPTIMIZE, ANALYZE, REPAIR, CHECK ya que podemos hacerlas sobre una partición únicamente
- Para acelerar el borrado de datos en determinados casos (borrar una partición es mucho más rápido que borrar los registros). En este ejemplo suprimir los registros correspondientes a meses pasados sería más rápido.
Estos resultados CONTRADICEN lo descrito en las presentaciones de Brooks Johnson y Phillip Tellis. El resultado que esperaba era un aumento de rendimiento para:
- inserciones que afecten a una sola de las particiones
- operaciones agregadas o de lectura sobre una sola de las particiones
- inserciones concurrentes (multiusuario)
Enlaces de interés
El trabajo realizado se ha basado fuertemente en los trabajos de Giuseppe Maxia, Brooks Johnson y Phillip Tellis, mostrado en las siguientes presentaciones:
- Partitions Performance with MySQL 5.1 and 5.5, de Giuseppe Maxia
- Divide and Be Conquered?, de Brooks Johnson
- Scaling MySQL writes through Partitioning, de Phillip Tellis:http://bluesmoon.info/
Comentarios
Actualmente no hay comentarios a este artículo.
Añadir comentarios ...