Archive of February 2011


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.


Sun 6 Feb

Almacenar fecha y hora de los picos de conexiones en MySQL

Cuando un cliente te pregunta cual ha sido el mayor pico de conexiones recibido es facil de responder:

mysql> show status like '%max_used%';
+----------------------+-------+
| Variable_name        | Value |
+----------------------+-------+
| Max_used_connections | 341   | 
+----------------------+-------+
1 row in set (0.00 sec)

Pero si el cliente te pregunta cuando, no tienes ningún dato que darle, ya que MySQL no guarda ese dato. Aquí voy a presentaros dos formas de hacerlo, una más elegante pero que os llevará algo más de trabajo y una menos elegante pero más facil de implantar.

La elegante

La primera, la elegante, nos la trae el blog systemadmin.es. Se trata de un parche para la versión 5.1 que le permite a MySQL guardar no solo el pico de conexiones, si no el timestamp en el cual ocurrió. Nos añade un nuevo estado llamado Max_used_connections_ts en el que almacenará el timestamp :)

mysql> show status like 'Max_used%';
+-------------------------+------------+
| Variable_name           | Value      |
+-------------------------+------------+
| Max_used_connections    | 27         |
| Max_used_connections_ts | 1296037707 |
+-------------------------+------------+
2 rows in set (0.00 sec)

El parche, descargable desde systemadmin.es se aplica como cualquier otro:

cd mysql-5.1.53
patch -p1 < ../patch.mysql.51.max_used_connections_ts.patch

Una vez compilado y puesto en marcha tendremos un dato más que ofrecer al cliente sobre el funcionamiento de su plataforma.

La menos elegante

La menos elegante es cosa mia :P Lo bueno que tiene es la rápida implantación. Si tienes que conocer la fecha y hora de los picos en una instalación con 100 MySQL, posiblemente termines antes así:

#!/bin/bash
USER="root"
PASS="xxxx"
LOG="/tmp/mysql_top"
if [ -e $LOG ]; then
        ANTES=\`cat $LOG\`
else
        ANTES=0
fi
AHORA=\`mysql -u$USER -p$PASS -e "show status like 'Max_used_connections';" 
| awk -F '|' '{ print $1 }' | awk '{ print $2 }' | tail -n1\`
if [ $AHORA -gt $ANTES ]; then
        logger "$AHORA conexiones maximas de MySQL"
        echo "Se ha superado el pico de conexiones el" \`date\` "con $AHORA"
        echo $AHORA > $LOG
        exit 1
else
        echo "El pico de conexiones se mantiene"
        exit 0
fi

En resumen, es un script que si detecta una subida en la variable lo logea en syslog y muestra un mensaje por STDOUT. De esta forma, te sirve tanto como tarea programada en CRON para llevar un histórico, como script NRPE de Nagios gracias a los exit codes ;)


Tue 1 Feb

Usando correctamente un esclavo como backup

Muchas de las instalaciones de replicación en MySQL no buscan ni balanceo de escrituras/lecturas ni alta disponibilidad, si no un simple backup en vivo. Montar una replicación es sencillo, no necesita hardware costoso y nos puede salvar de algunas situaciones complicadas. Por ejemplo, corrupción de tablas, errores en la controladora RAID o simplemente borrado de tablas directamente desde el sistema de ficheros. Si alguna de estas cosas pasa, tendremos el esclavo con un backup reciente de nuestros datos.

Pero la gente no tiene en cuenta que un backup usando esclavo no te salva de algunos errores humanos o de programación. Imaginemos que el empleado de recursos humanos se conecta a la base de datos de la empresa y quiere eliminar el salario de un trabajador recientemente despedido. Y ejecuta los siguientes comandos:

master [localhost] {msandbox} (employees) > show tables;
+---------------------+
| Tables_in_employees |
+---------------------+
| departments         |
| dept_emp            |
| dept_manager        |
| employees           |
| salaries            |
| titles              |
+---------------------+
7 rows in set (0.00 sec)

master [localhost] {msandbox} (employees) > delete from salaries;
Query OK, 2844047 rows affected (3 min 33.35 sec)

master [localhost] {msandbox} (employees) > select count(\*) from salaries;
+----------+
| count(*) |
+----------+
|        0 |
+----------+
1 row in set (0.07 sec)

Está claro, nuestro compañero se ha equivocado. Ejecutar un delete olvidando un where es un error muy común aunque no lo parezca. Tu, como administrador, le tranquilizas diciéndole que no pasa nada, tienes un backup en el servidor esclavo. ¿Realmente lo tienes? La verdad es que no, el delete se ha propagado al esclavo. Tu compañero de RRHH se ha cargado la BBDD principal y el backup. Cuando se da un error de este tipo no hay tiempo para reaccionar, pierdes todo.

Como casi siempre, las famosas herramientas de administración maatkit vienen a salvarnos y hacer que nuestro esclavo backup sea aún más seguro. La herramienta que vamos a ver es mk-slave-delay. Como el nombre indica, permitirá añadir un delay a la replicación del esclavo del tiempo que nosotros le digamos.

Options:
  --askpass          Prompt for password for connections
  --[no]continue -c  Continue replication normally on exit (default)
  --daemonize        Fork to background and detach (POSIX only)
  --delay        -d  Slave delay (default 1h).  Optional suffix s=seconds,
                     m=minutes, h=hours, d=days; if no suffix, s is used.
  --help             Show this help message
  --interval     -i  Sleep interval (default 1m).  Optional suffix s=seconds,
                     m=minutes, h=hours, d=days; if no suffix, s is used.
  --quiet        -q  Suppress normal output
  --setvars          Set these MySQL variables (default wait_timeout=10000)
  --time         -t  Time to run before exiting.  Optional suffix s=seconds,
                     m=minutes, h=hours, d=days; if no suffix, s is used.
  --usemaster    -u  Get binlog positions from master, not slave
  --version          Output version information and exit

Así que ejecutamos lo siguiente:

mk-slave-delay --daemonize -d 1h h=localhost,u=msandbox,p=msandbox,S=/tmp/mysql_sandbox13214.sock

Con esto, mk-slave-delay se pasara a background como demonio y se conectará a localhost creando un delay en la replicación de 1 hora. Esto es, desde que tu aplicas un cambio en el master hasta que este llega al esclavo pasará una hora. El funcionamiento interno es muy simple, consiste en parar y arrancar el SQL thread

Gracias a esto, desde el momento en el que el compañero de RRHH comete el error tienes una hora de tiempo de reloj para conectarte al esclavo, parar la replicación y sacar un dump. Y claro está, el valor de tiempo es modificable a gusto del consumidor. De esta forma, además de estar protegido de los típicos fallos que se puedan dar en el maestro, también estarás protegido de los fallos humanos o de programación :) Por lo tanto, si tienes un esclavo como backup, es muy recomendable meterle un delay. Luego, todas las sentencias que has perdido por ese tiempo de retraso puedes recuperarla de los binlogs pero evitando la última sentencia que eliminó todos tus datos.

slave1 [localhost] {msandbox} (employees) > select count(\*) from salaries;
+----------+
| count(*) |
+----------+
|  2844047 |
+----------+
1 row in set (0.70 sec)