Posts tagged with “mysql”


Tue 26 Jul

Replicación con Multi-Threaded Slaves, sacando uso a nuestros cores

Ahora mismo, todos los servidores que compramos son multi-core o multi-cpu. MySQL ha ido solucionando sus problemas de escalabilidad, sobre todo a nivel del engine InnoDB, y la diferencia es clara entre MySQL 5.0 y MySQL 5.5, donde el rendimiento en entornos multicore es cada vez mayor. Pero aún queda un punto por mejorar, la replicación de MySQL.

En una replicación MySQL Master/Slave el problema se puede ver claramente. Mientras que en el maestro puedes tener cientos de threads modificando datos en paralelo, estos se escriben de forma ordenada en el binlog mientras que el slave, que solo tiene un thread para aplicar los cambios (SQL Thread), tiene que escribir los cambios uno a uno. De esta forma, el rendimiento que ganamos con la paralelización de las consultas, se pierden al llegar al Slave. Razón por la cual en entornos de alta carga siempre vemos que el esclavo va muy por detrás del Master aplicando los cambios (Seconds Behind Master).

Este es un problema que se está intentando solucionar en las versiones de desarrollo de MySQL. En la versión 5.6 se ha introducido un nuevo concepto llamado Workers, que son diferentes SQL Threads para la aplicación en paralelo de los cambios.

Fuente

Podemos descargar una versión de desarrollo con esta funcionalidad desde http://labs.mysql.com/.

Con una serie nueva de parámetros que veremos a continuación podemos decirle a nuestro Slave cuantos Workers debe lanzar. Cada Worker podrá escribir los cambios del relaylog en paralelo, siempre y cuando pertenezcan a diferentes bases de datos. Por lo que de momento, para poder sacar beneficio a esta nueva funcionalidad, deberás tener diferentes bases de datos, cosa que es bastante habitual en entornos complejos donde el Sharding es la norma a seguir.

En primer lugar, debemos decirle a nuestro MySQL que ahora algunos de los estados relacionados con la replicación la queremos guardar en base de datos, concretamente en tablas dentro de la BD mysql:

relay-log-info-repository="TABLE" 
master-info-repository="TABLE" 

relay-log-info-repository: esta variable indica donde se guardará la posición en la que nos encontramos dentro del relay log master-info-repository: esta variable indica donde se guardará la información referente a su master

Todas las opciones se encuentran ya documentadas en http://dev.mysql.com/doc/refman/5.6/en/replication-options-binary-log.html. Es recomendable tener esta URL a mano, ya que al ser algo que se encuentra en desarrollo, las especificaciones y las opciones cambian constantemente.

Una vez hecho, nos conectamos al Slave y le decimos que queremos cuatro Workers.

mysql> STOP SLAVE;
Query OK, 0 rows affected (0.00 sec)
mysql> SET GLOBAL slave_parallel_workers=4;
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT @@slave_parallel_workers;
+------------------------------+
| @@slave_parallel_workers |
+------------------------------+
|                            4 |
+------------------------------+
1 row in set (0.00 sec)
mysql> START SLAVE;
Query OK, 0 rows affected (0.05 sec)

A continuación comprobamos si realmente se han lanzado los 4 procesos:

mysql> show processlist;
+----+-------------+------+---------+------+---------------------------------------------+
| Id | User        | db   | Command | Time | State                                       |
+----+-------------+------+---------+------+---------------------------------------------+
|  1 | root        | NULL | Query   |    0 | init                                        |
|  8 | system user | NULL | Connect |    2 | Waiting for master to send event            |
|  9 | system user | NULL | Connect |    2 | Slave has read all relay log; waiting [...] |
| 10 | system user | NULL | Connect |    2 | Waiting for an event from sql thread        |
| 11 | system user | NULL | Connect |    2 | Waiting for an event from sql thread        |
| 12 | system user | NULL | Connect |    2 | Waiting for an event from sql thread        |
| 13 | system user | NULL | Connect |    2 | Waiting for an event from sql thread        |
+----+-------------+------+---------+------+---------------------------------------------+
7 rows in set (0.00 sec)

Ahora mismo ya tenemos cuatro threads SQL para la aplicación de datos en paralelo. Como hemos comentado anteriormente, es posible aplicar cambios en paralelo siempre que estos sean de bases de datos diferentes. Por lo no tiene sentido que el número de threads que lancemos sea superior al del número de bases de datos que queremos replicar.

Esta es solo una más de las múltiples mejoras que se están implantando en la rama de desarrollo, intentaré manteneros informados de todas las novedades interesantes que sigan surgiendo :)


Sat 16 Jul

Sniffing del protocolo de red de MySQL

En ocasiones es necesario hacer un análisis de que está pasando en un servidor con MySQL, comprobar que queries se están ejecutando, cuanto tardan, donde están los cuellos de botella, etc. Hay diferentes formas de hacerlos:

  • A lo pobre: ejecutar SHOW FULL PROCESSLIST cada pocos segundos, identificar a ojo las querys que pueden ser interesantes y luego analizarlas.
  • A lo basto: habilitar el log general de MySQL y almacenar todas las querys que se ejecutan. Te llevarás una gran cantidad de IOPS y puede que el fichero termine siendo tan grande que analizarlo sea un infierno.

Como casi siempre, las maatkit vienen a ayudarnos en esta tarea. En esta ocasión, mk-query-digest nos va a permitir analizar la ejecución de querys y generarnos un reporte. Esta utilidad es capaz de coger datos de los logs, pero aquí vamos a utilizar un parámetro que nos ayudará a capturar el tráfico directamente de la interfaz de red y hacer un reporte de lo que está pasando ahora mismo.

Para ello solo necesitamos tcpdump y mk-query-digest :)

Iniciamos la captura de tráfico y se la pasamos a mk-query-digest por STDIN:

root: ~ # tcpdump -s 65535 -x -n -q -tttt -i lo0 -c 10000
 port 3306 | ./mk-query-digest --type tcpdump

Ahora mismo, tcpdump estará capturando el tráfico del puerto TCP 3306 y pasándolo por pipe a mk-query-digest. Pasados unos tiempo (10000 paquetes) veremos el reporte:

listening on lo0, link-type NULL (BSD loopback), capture size 65535 bytes
10000 packets captured
17211 packets received by filter
7070 packets dropped by kernel
# 2.2s user time, 30ms system time, 16.28M rss, 2.33G vsz
# Current date: Sat Jul 16 20:50:09 2011
# Hostname: iMac-Punisher.local
# Files: STDIN
# Overall: 2.20k total, 9 unique, 668.40 QPS, 0.38x concurrency __________
# Time range: 2011-07-16 20:50:05.650775 to 20:50:08.940697
# Attribute          total     min     max     avg     95%  stddev  median
# ============     ======= ======= ======= ======= ======= ======= =======
# Exec time             1s       0    95ms   569us     1ms     2ms   287us
# Rows affecte       1.91k       0       1    0.89    0.99    0.31    0.99
# Query size       835.46k      25     433  389.05  420.77  118.11  420.77
# Warning coun           0       0       0       0       0       0       0
# Boolean:
# No index use   0% yes,  99% no
# Profile
# Rank Query ID           Response time Calls R/Call Apdx V/M   Item
# ==== ================== ============= ===== ====== ==== ===== ==========
#    1 0xCAEC22E79B0EFD3B  0.9023 72.1%  1955 0.0005 1.00  0.01 INSERT t?
#    2 0xA21B9C02CA2BF472  0.2649 21.2%   139 0.0019 1.00  0.00 SELECT t?
#    3 0xBCFD2D3AD85C5621  0.0475  3.8%     1 0.0475 1.00  0.00 CREATE TABLE t? `t1`
# MISC 0xMISC              0.0368  2.9%   104 0.0004   NS   0.0 <6 ITEMS>

Podemos mejorarlo aún más, haciendo que las queries pasen por el explain automáticamente. Esto último hay que hacerlo con cuidado, porque al contrario de lo que cree mucha gente, un explain no siempre evita que parte de la query se tenga que ejecutar consumiendo recursos.

tcpdump -s 65535 -x -n -q -tttt -i lo0 -c 10000 port 3306 | ./mk-query-digest 
--type tcpdump --explain h=localhost,u=root

# Query 2: 60.04 QPS, 0.11x concurrency, ID 0xA21B9C02CA2BF472 at byte 269106
# This item is included in the report because it matches --limit.
# Scores: Apdex = 1.00 [1.0], V/M = 0.00
# EXPLAIN sparkline: a
# Query_time sparkline: |  .^_   |
# Time range: 2011-07-16 20:59:46.310625 to 20:59:48.425763
# Attribute    pct   total     min     max     avg     95%  stddev  median
# ============ === ======= ======= ======= ======= ======= ======= =======
# Count          5     127
# Exec time     21   242ms   143us    15ms     2ms     4ms     2ms     2ms
# Rows affecte   0       1       0       1    0.01       0    0.09       0
# Query size     0   7.19k      58      58      58      58       0      58
# Warning coun   0       0       0       0       0       0       0       0
# String:
# Databases    mysqlslap
# Errors       none
# Hosts        127.0.0.1
# Users        root
# Query_time distribution
#   1us
#  10us
# 100us  ###########################
#   1ms  ################################################################
#  10ms  ##
# 100ms
#    1s
#  10s+
# Tables
#    SHOW TABLE STATUS FROM 'mysqlslap' LIKE 't1'\G
#    SHOW CREATE TABLE 'mysqlslap'.'t1'\G
# EXPLAIN /*!50100 PARTITIONS*/
SELECT intcol1,intcol2,charcol1,charcol2,charcol3 FROM t1\G
# *************************** 1. row ***************************
#            id: 1
#   select_type: SIMPLE
#         table: t1
#    partitions: NULL
#          type: ALL
# possible_keys: NULL
#           key: NULL
#       key_len: NULL
#           ref: NULL
#          rows: 34
#         Extra:

No solo puedes analizar las queries de tu MySQL en tiempo real, si no incluso hacer un explain y comprobar sus planes de ejecución :)


Fri 8 Jul

Mantener la consistencia de los datos en la replicación

Las replicaciones necesitan de un chequeo constante en la integridad de los datos. Fallos de disco, corrupción de de logs, mezcla de tablas transaccionales y no transaccionales y otros problemas pueden tumbar la consistencia de nuestros datos. Por lo tanto, podemos tener una replicación funcionando, pero los datos, si no hay una comprobación activa, pueden ser diferentes en las dos máquinas. MySQL no tiene comprobaciones activas de consistencia, por lo que es trabajo nuestro. Para ello, instalamos las herramientas maatkit de Percona:

apt-get install maatkit

Las herramientras que usaremos serán mk-table-checksum y mk-table-sync. El funcionamiento de la herramienta se basa en la replicación en base a sentencias de mysql. mk-table-checksum realiza una comprobación mediante un algoritmo de hashing en las tablas, escribiendo los resultados en la base de datos. Estas sentencias se replicarán en el esclavo y se volverán a ejecutar, realizando por lo tanto el mismo hashing en las tablas del esclavo. De esta forma, unicamente debemos comparar los resultados en el maestro y en el esclavo para comprobar si los datos son exactamente iguales o no.

Tenemos dos MySQL en Master-Slave.

NODO 1:

Puerto TCP 19369

master [localhost] {msandbox} (vida) > select * from producto;
+---+--------+
| i | nombre |
+---+--------+
| 1 | VPS    |
| 2 | Cloud  |
+---+--------+
2 rows in set (0.00 sec)

NODO 2:

Puerto TCP 19370

slave1 [localhost] {msandbox} (vida) > select * from producto;
+---+---------+
| i | nombre  |
+---+---------+
| 1 | VPS     |
| 2 | Cloud   |
| 3 | Storage |
+---+---------+
3 rows in set (0.00 sec)

¿Como comprobamos si los datos son o no consistentes?

Primero, le decimos a mk-table-checksum que haga un checksum de todas las tablas de la base de datos vida y guarde el resultado en test.checksum.

~$ mk-table-checksum u=root,p=msandbox
--socket=/tmp/mysql_sandbox19369.sock --databases=vida
--replicate test.checksum --create-replicate-table
Cannot connect to P=19369,S=/tmp/mysql_sandbox19369.sock,h=SBslave1,p=...,u=root
Cannot connect to P=19369,S=/tmp/mysql_sandbox19369.sock,h=SBslave1,p=...,u=root
DATABASE TABLE    CHUNK HOST        ENGINE      COUNT         CHECKSUM TIME WAIT STAT  LAG
vida     producto     0 soporteit69 InnoDB          2         935b4964    0 NULL NULL NULL

Comprobamos los resultados en Master:

master [localhost] {msandbox} (test) > select * from checksum;
+------+----------+-------+------------+----------+----------+------------+------------+
| db   | tbl      | chunk | boundaries | this_crc | this_cnt | master_crc | master_cnt |
+------+----------+-------+------------+----------+----------+------------+------------+
| vida | producto |     0 | 1=1        | 935b4964 |        2 | 935b4964   |          2 |
+------+----------+-------+------------+----------+----------+------------+------------+
1 row in set (0.00 sec)

Comprobamos los resultados en Slave:

slave1 [localhost] {msandbox} (test) > select * from checksum;
+------+----------+-------+------------+----------+----------+------------+------------+
| db   | tbl      | chunk | boundaries | this_crc | this_cnt | master_crc | master_cnt |
+------+----------+-------+------------+----------+----------+------------+------------+
| vida | producto |     0 | 1=1        | 6120c018 |        3 | 935b4964   |          2 |
+------+----------+-------+------------+----------+----------+------------+------------+
1 row in set (0.00 sec)

Como podemos ver, el crc del slave (this_crc) es diferente al del master (master_crc) por lo tanto, tenemos incosistencias en los datos de la tabla producto de la base de datos vida.

¿Cómo lo arreglamos?

Podriamos hacerlo volviendo a montar la replicación de cero usando un dump de mysqldump. Esto vale si tu BBDD ocupa solo unos cientos de megas, pero si hablamos de gigas, puedes tardar horas y provocar caidas del sistema. Así que haremos uso de mk-table-sync.

~$ mk-table-sync --execute --replicate test.checksum 
--sync-to-master u=root,p=msandbox --socket=/tmp/mysql_sandbox19370.sock

Comprobamos si ahora los datos son consistentes:

master [localhost] {msandbox} (vida) > select * from producto;
+---+--------+
| i | nombre |
+---+--------+
| 1 | VPS    |
| 2 | Cloud  |
+---+--------+
2 rows in set (0.00 sec)
slave1 [localhost] {msandbox} (vida) > select * from producto;
+---+--------+
| i | nombre |
+---+--------+
| 1 | VPS    |
| 2 | Cloud  |
+---+--------+
2 rows in set (0.00 sec)
slave1 [localhost] {msandbox} (test) > select * from checksum;
+------+----------+-------+------------+----------+----------+------------+------------+
| db   | tbl      | chunk | boundaries | this_crc | this_cnt | master_crc | master_cnt |
+------+----------+-------+------------+----------+----------+------------+------------+
| vida | producto |     0 | 1=1        | 935b4964 |        2 | 935b4964   |          2 |
+------+----------+-------+------------+----------+----------+------------+------------+
1 row in set (0.00 sec)

Listo! Replicación consistente y en marcha :)


Mon 20 Jun

Gestión centralizada de MySQL en un DataCenter

Cuando el número de servidores que administras es pequeño, realizar tareas sobre estos no supone un gran problema. Pero cuando se empieza a superar el número de pestañas que entran en el terminal, empiezas a tener un problema. El uso de una administración centralizada te permite tener el control total de las configuraciones, paquetes y servicios que hay en cada servidor desde un punto central, aliviando así la carga de trabajo y manteniendo un entorno lo más homogéneo posible.

Para llevar a cabo esta tarea existen múltiples soluciones en el mundo del Software Libre. Estas semanas he dedicado parte de mi tiempo libre a rellenar esta laguna de conocimientos que tenía como sysadmin y el elegido ha sido Puppet.

Puppet es una herramienta de gestión centralizada de servidores, basado en Ruby y con un lenguaje declarativo muy parecido a este. Existen múltiples módulos para gestionar servicios como Apache, nginx, Postfix, Cron, etc. Y lo más importante, gran parte de estos, por ejemplo el de la gestión de paquetes, es independiente de la distribución final. Usando el lenguaje de Puppet, a un nivel superior, indicando que quieres tener instalado el paquete X, el agente de Puppet en el lado del cliente se encargará de usar yum, apt-get, emerge o lo que sea necesario para cumplir la tarea indicada.

Para poner en práctica mis conocimientos, he desarrollado un módulo de MySQL que ahora comparto con todos vosotros. La razón para elegir son varías:

  • Es un servicio que conozco
  • Los módulos realizaban pequeñas tareas, no había uno que hiciese de todo un poco
  • Porque si

Así que me puse manos a la obra, y aquí está el resultado:

https://github.com/miguelangelnieto/puppet-mysql

Con este módulo de Puppet, podréis gestionar todos los MySQL de vuestro datacenter desde un Puppet Master con una sintaxis sencilla. Las tareas que se podrán realizar son:

  • Instalar el paquete de MySQL Server
  • Gestionar la activación en arranque y la puesta en marcha del servicio
  • Configurar my.cnf
  • Importación de bases de datos
  • Creación de usuarios y dar privilegios

¿Cómo lo hacemos? El ejemplo que vemos a continuación demuestra todo lo que se puede hacer. Configuración de un nodo de bases de datos MySQL:

node 'debian.client.puppet' {
    include mysql
mysql::config_file {
        "/etc/mysql/my.cnf":
        max_connections => "100",
        bind_address => "10.10.0.2",
        key_buffer => "40M",
        query_cache_size => "0",
    innodb_buffer_pool_size => "120M";
        }
mysql::db {
    "test":
    source_file => "test.sql"
    }
mysql::grants {
    "admin_test":
    user => "admin",
    password => "4dm1n",
    db => "test",
    privileges => "all privileges";
    }
}

Como se puede ver la sintaxis es fácil de leer. La configuración, dividida en tres partes, genera un fichero my.cnf a medida, crea la base de datos test usando como origen el dump test.sql y finalmente, crea el usuario admin y le da all privileges a dicha base de datos.

Este ejemplo es para una sola máquina, debian.cliente.puppet, pero si eliminamos el nombre y en su lugar ponemos una expresión regular o simplemente "default" tendremos nuestros cientos de servidores gestionados desde un único punto central. Podrás crear cientos de bases de datos, asignarlo a otros cientos de usuarios y generar una configuración de my.cnf acorde a dichas bases de datos.

Respecto a las posibilidades de parametrización de my.cnf, las opciones que se pueden configurar están reflejados en https://github.com/miguelangelnieto/puppet-mysql/blob/master/modules/mysql/manifests/config_file.pp Si se desean añadir más, lo único que habría que hacer es editar este fichero y reflejar los cambios en el template https://github.com/miguelangelnieto/puppet-mysql/blob/master/modules/mysql/templates/my.erb

Gracias a este módulo podremos gestionar todos los MySQL de nuestro DataCenter de forma fácil y sencilla, meter nuevos servidores en producción en tiempo record y lo más importante, trabajar con un entorno homogéneo y desde un punto único y central.


Fri 27 May

BOTs para Twitter de MySQL

Llevo dos semanas de baja, sin poder tampoco salir de casa. Razón por la cual pensé en aprovechar el tiempo e intentar mejorar mis conocimientos en algo que nunca se me ha dado bien. La programación y más exactamente, la orientada a objetos. Así que me pillé un libro Ruby y Kindle en mano he ido aprendiendo y poniendo en funcionamiento conceptos que siempre me han sonado a chino. Si bien es cierto que sigo teniendo muchísimas deficiencias de conocimientos a nivel de programación (soy sysadmin, fuera de bash scripting casi nunca tengo que hacer nada), creo que he cogido una base que me permitirá ir mejorando poco a poco dentro de mis limitaciones ;)

Para poner en práctica todo lo aprendido, quise hacer algo relacionado con las redes sociales y MySQL. Y entonces recordé que un buen amigo y ex-compañero de trabajo llamado Saúl, también conocido como el chico SIP, programó en su día un BOT que hacía retweet de todos los hashtag #asterisk. Así que me puse manos a la obra, e intentando hacer un código lo menos sucio y guarro posible, me programé mi propio bot. El código está disponible en github, al alcance de todo el mundo que quiera mejorarlo, forkearlo... o imprimirlo y limpiarse el culo con el https://github.com/miguelangelnieto/Twitter-BOT

Ahora mismo ya hay dos bots corriendo, que son @MySQLBot_en y @MySQLBot_es. Como os podéis imaginar, el primero retweetea los mensajes de MySQL en inglés y el segundo en español. Tengo pensado montarlo también en francés y aleman. El problema es que como no entenderé los tweets, por lo que será complicado filtrar y banear usuarios indeseados. Si alguien quiere colaborar, que me avise :)


Wed 25 May

Colección de cheatsheets accesibles desde consola

Los cheatsheets suelen ser unas tablas resumen que siempre conviene tener a mano. Existen miles de CheatSheets por la web, para servicios como Apache, Mysql o aplicaciones como Vim. Por ejemplo, en http://www.cheat-sheets.org/ teneís una colección al alcance de un click. Lo que aquí voy a enseñar es un pequeño truco para que todos ellos sean accesibles desde la consola, ya que no siempre tenemos un navegador a mano :)

Aprovechando que intento aprender Ruby y los conceptos de POO, haremos uso de las Gem de Ruby para dotar de CheatSheets a nuestra shell. La Gem que nos dará la funcionalidad se llama Cheat y su instalación es sencilla:

# gem install cheat
Successfully installed cheat-1.3.0
1 gem installed
Installing ri documentation for cheat-1.3.0...
Installing RDoc documentation for cheat-1.3.0...

Listo! :) Veamos ahora el listado de CheatSheets que disponemos:

# cheat sheets
All Cheat Sheets:
  a2ps
  a2ps_page_size
  ack
  acl9
  acts_as_authenticated
  acts_as_state_machine
  acts_as_taggable_on
  address
  administrateme
  agile
  alias
  amazon_conditions
  ambition
  android
  ansi
  apache
  [..]

He cortado la salida ya que hay cientos. Veamos por ejemplo, los CheatSheets de mysql_insert y rm:

# cheat mysql_insert
mysql_insert:
  Name: 'INSERT'
  Description:
  Syntax:
  INSERT [LOW_PRIORITY | DELAYED | HIGH_PRIORITY] [IGNORE]
      [INTO] tbl_name [(col_name,...)]
      VALUES ({expr | DEFAULT},...),(...),...
      [ ON DUPLICATE KEY UPDATE col_name=expr, ... ]
Or:
  INSERT [LOW_PRIORITY | DELAYED | HIGH_PRIORITY] [IGNORE]
      [INTO] tbl_name
      SET col_name={expr | DEFAULT}, ...
      [ ON DUPLICATE KEY UPDATE col_name=expr, ... ] 
Or: 
  INSERT [LOW_PRIORITY | HIGH_PRIORITY] [IGNORE]
      [INTO] tbl_name [(col_name,...)]
      SELECT ...
      [ ON DUPLICATE KEY UPDATE col_name=expr, ... ]  
  INSERT inserts new rows into an existing table. The INSERT ... VALUES
  and INSERT ... SET forms of the statement insert rows based on
  explicitly specified values. The INSERT ... SELECT form inserts rows
  selected from another table or tables. INSERT ... SELECT is discussed
  further in [HELP INSERT SELECT].
  URL: http://dev.mysql.com/doc/refman/5.0/en/insert.html

# cheat rm
rm:
  rm - Remove files.
          Syntax: rm [] 
          rm main purpose is to remove files.  It will also take flags that will
          allow it to remove directories.  Wild cards are allowed as well.
          Common Flags:
                  -r - remove listed files and directories plus all files in
                       those directories plus all sub-dirs and there files in
                       listed directories, etc..
                  -f - don't ask if you want to remove a file.
                  -i - ask before removing a file.

Receta fácil fácil y para toda la familia :)


Sun 3 Apr

Benchmark de MySQL

La herramienta típica para hacer Benchmark de MySQL es sysbench http://sysbench.sourceforge.net/

Se hace en dos pasos:

  • Preparar la tabla sobre la que se hará el testeo:
sysbench --test=oltp --db-driver=mysql --mysql-db=test --mysql-user=root --mysql-password=xxxxx
 --mysql-table-engine=innodb --oltp-table-size=1000000 prepare

Donde se indica que el driver será mysql, la base de datos test y el engine innodb con un tamaño de 1000000. La tabla tendrá el siguiente formato:

CREATE TABLE `sbtest` (`id` int(10) unsigned NOT NULL auto_increment, 
`k` int(10) unsigned NOT NULL default '0', `c` char(120) NOT NULL default '', 
`pad` char(60) NOT NULL default '', PRIMARY KEY  (`id`), KEY `k` (`k`));

  • Una vez preparada, lanzamos el benchmark:
sysbench --test=oltp --db-driver=mysql --mysql-db=test --mysql-user=root --mysql-password=xxxxx
--max-requests=10000 --num-threads=10 run

Lanzará 10 threads con un máximo de 10000 peticiones.

Si se desea volver a lanzar el benchmark es necesario eliminar la tabla de testo, reiniciar MySQL para vaciar buffers y caches.

Para ver todas las opciones existentes:

http://sysbench.sourceforge.net/docs/#database_mode

Ajustar el valor max_connections

max_connections es un valor que no se puede poner al azar, ya que afecta al funcionamiento de la aplicación y establece el máximo de memoria que MySQL podrá utilizar. La fórmula para calcularlo es:

(read_buffer_size + read_rnd_buffer_size + sort_buffer_size + thread_stack 
+ join_buffer_size) * max_connections

Por lo tanto, si los buffers anteriores suman 70 megas y ponemos 10.000 conexiones máximas, MySQL se creerá que tiene para gestionar 680 GB de RAM.

Imaginemos entonces que la máquina tiene 3gb. Como los 680 no los va a intentar reservar al iniciio no hay problema, pero el aceptará conexiones sin parar y llegará un momento en el que se superen los 3 gigas. Como MySQL cree que tiene 680, seguirá aceptando conexiones y todas empezarán a fallar, las nuevas y las actuales por errores de falta de memoria. Al final, aunque indiques 10.000 conexiones máximas, tu máquina no las gestionará y tirarás a bajo todo el servicio con poco más de 50 conexiones.

Por lo tanto, si MySQL te suelta un error de número máximo de conexiones superadas, no pongas 1 millon, no se soluciona nada. Baja los buffers, aplica la formula e intenta ajustarlo para permitir el mayor número de conexiones sin degradar el rendimiento.

0 Comments · Tags:

Sun 13 Feb

Tunear Apache con cabeza

Voy a cambiar un poco de temática, ya que no solo de MySQL vive el mundo 2.0 ;) Apache, el servidor web estándar en el ecosistema LAMP es uno de los grandes olvidados a la hora de hacer un buen tuning de nuestra plataforma. A continuación voy a explicar una serie de pasos recomendados para lograr el máximo rendimiento de nuestra máquina y al mismo tiempo evitar que esta se caiga bajo un gran pico de visitas. Ni copiar configuraciones de internet ni dejar los valores por defecto son buenas ideas ;)

A la hora de instalar Apache debemos elegir entre dos versiones, aunque generalmente lo elegirá nuestro gestor de paquetes.

  • Apache Worker

Incluido desde Apache 2.0. Utiliza menos memoria y tiene un mejor rendimiento. Se lanzan múltiples procesos y cada uno de estos pueden correr múltiples threads.

  • Apache Prefork

Es el MPM por defecto en Apache 1.3 y Apache 2.0. Es el instalado por defecto si hacemos uso de mod_php, ya que algunos módulos de PHP no son "thread safe". Tiene un rendimiento ligeramente peor que Worker y más consumo de memoria, pero se considera más seguro y estable. En este MPM se lanzan múltiples procesos y se ejecuta un thread por proceso.

Lo lógico sería utilizar Worker, pero vamos a basarnos en el típico ejemplo LAMP que incluye el soporte mod_php, aunque esto no sea lo más óptimo. Por dicha razón vamos a ver como deberíamos ajustar la configuración para un Prefork.

El principal parámetro hardware de nuestros frontales Apache será la memoria RAM. Aquí está todo claro, a más memoria RAM, más procesos se podrán lanzar y más peticiones se podrán atender. Por lo tanto, en primer lugar debemos saber el número de procesos que nuestra máquina puede llegar a soportar. Para ello podemos hacer uso de esta fórmula:

MaxClients = (RAM - tamaño_del_resto_de_procesos)/(tamaño_de_procesos_apache)

Para conocer el tamaño de los procesos de Apache podemos ejecutar un ps:

ps -ylC httpd --sort:rss

Es recomendable que los procesos de Apache ya hayan atentido algunas peticiones, de forma que podamos observar no solo el valor mínimo de memoria de los procesos, si no el máximo que alcanzan una vez atendidas las peticiones.

Entonces, pongamos el siguiente ejemplo:

  • El mayor tamaño que alcanza un proceso de Apache son 98 megas
  • El resto de procesos ocupan 250 megas.
  • Tenemos 32 gigas de ram
MaxClients = (32768 - 250) / 98

Por lo tanto, el número máximo de procesos Apache que se pueden arrancar son 331. Hay que tener en cuenta que esto sería un límite teórico antes de empezar a swapear. Para no estar rozando el límite recomiendo poner algo menos, 310. Con esto definimos claramente cuantos procesos podrán lanzarse para atender a las peticiones y será en función de la RAM que tengamos. Si reducimos el tamaño de los procesos de Apache podremos también aumentar el número de procesos que se pueden lanzar. Para ello, lo mejor es desactivar todos los módulos que no se vayan a utilizar.

En caso de recibir más peticiones de las que pueda atender las encolará y evitaremos que comience a swapear tirando la máquina abajo. Esta cola está definida por el parámetro ListenBacklog que tiene como valor por defecto 511. Como bien dice el manual de Apache, no se recomienda cambiar este valor a no ser que estemos sufriendo un ataque de flood SYN.

Asociado a este parámetro también tenemos ServerLimit, que viene a indicar el valor máximo que podremos poner en MaxClients. ServerLimit tiene por defecto 256, por lo que si aumentamos MaxClient a 310 también tendremos que aumentar ServerLimit al mismo número. De lo contrario el cambio en MaxClients no servirá de nada y estaremos limitados.

Además, tenemos que configurar los siguientes valores:

  • StartServers

Indica el número de procesos que arranca al iniciar el servicio. Este valor no se debe poner demasiado elevado o ralentizará mucho la puesta en marcha del servicio. Como modificar este valor no da ninguna mejora de rendimiento, lo dejamos en el valor por defecto 5.

  • MinSpareServers

Son el número de procesos IDLE que deben existir como mínimo en el servidor. Según vayan recibiendo peticiones Apache lanzará nuevos procesos para mantener este número mínimo. De esta forma los nuevos usuarios que se conecten a la web no tendrán que esperar a que Apache lance un proceso para ellos, si no que siempre se garantizará un mínimo, mejorando la respuesta del servicio. Aún así no es recomendado poner un valor exageradamente grande. Con 30 es suficiente.

  • MaxSpareServers

Es el número máximo de procesos IDLE que existirán en el servidor. Si este número se supera, Apache empezará a cerrar procesos. Poner un valor que sea el doble de MinSpareServers suele dar buenos resultados, siempre y cuando MinSpareServers no tenga un valor absurdamente grande. 60 será suficiente.

  • MaxRequestsPerChild

Con el fin de evitar posibles posibles fugas de memoria en la programación, se suele establecer el número de peticiones máximas que atenderá un proceso antes de morir. Poner un valor pequeño reducirá el rendimiento, ya que Apache tendrá que estár continuamente matando y lanzando nuevos procesos. Un valor recomendado por defecto es 100000, el cual es correcto.

Por lo tanto, el tuning quedaría de la siguiente manera:


StartServer 5
MinSpareServer 30
MaxSpareServer 60
MaxClients 310
MaxRequestsPerChild 10000

Además es recomendable bajar los timeous para evitar tener procesos sin hacer nada.

Timeout 30

Timeout esperará 30 segundos antes de cerrar una petición. Esperará para tres cosas (copy/paste del manual):

1- La cantidad de tiempo que tarda en recibir una petición GET.

2- La cantidad de tiempo entre la recepción de paquetes TCP packets en una petición POST o PUT.

3- La cantidad de tiempo entre ACKs en transmisiones de paquetes TCP en las respuestas.

Por defecto el valor es 300, el cual bajo mi experiencia es realmente exágerado.

KeepAliveTimeout 5

Muchos clientes no ven con buenos ojos la bajada del KeepAliveTimeout, pero tiene una buena razón. En prefork, una petición se queda asociada a un proceso durante todo el KeepAlive, por lo que tener un valor muy alto terminará por agotar las conexiones en algunos picos de carga. En algunos casos incluso es posible aumentar el rendimiento deshabilitando por completo el KeepAlive, pero eso ya dependerá de la página y la programación de la misma. El valor por defecto es 15.

Con estos consejos tendrás una configuración mucho mejor que la de defecto y completamente ajustada a tu máquina. Si ves que el número máximo de clientes es muy poco y no puedes atender todas las peticiones, no lo aumentes a lo loco, mete más máquinas en el balanceador o aumenta la RAM. Lo más importante como ya he indicado es poner valores realistas. En sistemas con mucha carga evitar la swap es la diferencia entre poder acceder a la máquina por SSH o tener que ir al centro de datos a dar un botonazo.

Además es importante tunear los frontales teniendo en cuenta el backend. Si tu MySQL no puede gestionar por memoria más de 200 conexiones, es una estupidez poner en apache 310. Todo tiene que estar equilibrado.


Fri 11 Feb

(No) Escalado de escrituras en MySQL

Muchas veces se montan replicaciones Master-Master con el fin de "balancear y escalar escrituras". En MySQL el concepto de escalabilidad existe, pero únicamente en lecturas. En cambio, con las escrituras esto no es posible a no ser que usemos engines especiales como SpiderSQL o modificaciones de replicación como Galera. Las razones por las que no se recomienda usar Master-Master para balanceo de carga son las siguientes:

  • El balanceo de carga es falso, por el simple hecho de tener una replicación master-master. Si lanzas una Update al HostA que tarda 5 minutos en terminar, cuando la query se replique al HostB este tardará también 5 minutos. Aquí no hay balanceo de carga, da igual a que host lances el INSERT o el UPDATE, todos tendrán que ejecutarlo y dedicar recursos. Por lo tanto no balanceas carga, solamente decides quien será el primero en aplicar los cambios. Si activas Row-Based replication si puede existir una pequeña ganancia, pero no evitas el siguiente problema.

  • En MySQL no existen los bloqueos de tabla distribuidos, por lo que el balanceo de queries entre diferentes masters dará como resultado datos inconsistentes.

Pongamos como ejemplo una tabla con estos datos:

NODO1> select \* from producto;
+----+--------+
| ID | Nombre |
+----+--------+
|  2 | VPS    | 
|  4 | cloud  | 
+----+--------+
2 rows in set (0.00 sec)

Replicado master-master en dos nodos mysql, llamados NODO1 y NODO2. Se balancean las escrituras mediante un balanceador.

En el NODO 1 se bloquea la tabla para hacer un cambio de producto (dejamos de ofrecer VPS y ofrecemos STORAGE), de forma que solo la sesión que ha creado el bloqueo pueda escribir:

NODO1> lock tables producto WRITE;
Query OK, 0 rows affected (0.00 sec)
NODO1> update producto SET Nombre="storage" where Nombre="VPS";
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0
NODO1> select \* from producto;
+----+---------+
| ID | Nombre  |
+----+---------+
|  2 | storage | 
|  4 | cloud   | 
+----+---------+
2 rows in set (0.00 sec)

Durante dicha transacción, en el NODO2 entra una petición de eliminar un producto (no ofrecemos VPS). La tabla en realidad está bloqueada en el NODO1, pero no existe bloqueo distribuido, por lo que alguien elimina el producto VPS de la tabla en el NODO2.

NODO2> delete from producto where Nombre="vps";
Query OK, 1 row affected (0.00 sec)
NODO2> select \* from producto;
+----+--------+
| ID | Nombre |
+----+--------+
|  4 | cloud  | 
+----+--------+
1 row in set (0.00 sec)

En este momento acabamos de tirar a la basura la consistencia de los datos, metíendonos en un problemón:

NODO1> select \* from producto;
+----+---------+
| ID | Nombre  |
+----+---------+
|  2 | storage | 
|  4 | cloud   | 
+----+---------+
2 rows in set (0.00 sec)
NODO2> select \* from producto;
+----+--------+
| ID | Nombre |
+----+--------+
|  4 | cloud  | 
+----+--------+
1 row in set (0.00 sec)

Los datos son incosistentes, la replicación sigue funcionando y nadie se ha enterado. Este ejemplo se aplica a master-master de dos nodos y a replicación circular.

← Previous Next → Page 2 of 6