Optimización en Drupal 6

You are viewing a wiki page. You are welcome to join the group and then edit it. Be bold!

Introducción

A continuación se expone el guión de la charla llevada a cabo el día 25 de Febrero en Valencia. Disculpad la tardanza en publicarla en el grupo y perdonad que no añada los enlaces expuestos en la reunión con ejemplos de optimización de consultas SQL llevadas a cabo por el Departamento de Desarrollo de Aureka Internet. Espero que aún sin los ejemplos didácticos os sea útil.

Sentíos totalmente libres de modificar y mejorar el contenido o la forma de este post. Como veis no se ha incluído información sobre Boost, Memcache, Varnish y Pressflow. ¡Os agradeceré si contribuís con lo que esté en vuestra mano!

Un saludo.

La optimización prematura. El principio de Pareto.

El objetivo del desarrollo del software es crear aplicaciones que:

  • sean eficaces.
  • sean usables.
  • sean fiables.
  • sean mantenibles.

La optimización consiste en implementar mejoras que aumenten la capacidad de respuesta del software. Sin embargo, la mayoría de las optimizaciones tienen un coste en fiabilidad y mantenibilidad.

¿Cuándo debemos optimizar?
Cuando el balance de pérdidas y ganancias sea positivo.

¿Dónde debemos optimizar?
En el siglo XIX, Pareto observó que tanto en la naturaleza como en las sociedades humanas, casi todo se puede expresar en función de dos percentiles; 20 y 80. Por ejemplo, el 20% de la población mundial consume el 80% de los recursos. Esta formulación ha sido aplicada en muchos campos del conocimiento.

El desarrollo del software no es una excepción. No es desatinado afirmar que, en cualquier programa, el 80% del tiempo de proceso se consume en el 20% del código. Por lo tanto, debe identificarse ese 20% del código sobre el que las optimizaciones tendrán el mayor efecto y serán, por tanto, más rentables. Esto significa que la optimización debe abordarse después del desarrollo. Hay un axioma en la programación que dice: "la optimización prematura es la raíz de todos los males" (Donald Knuth).

Identificar cuellos de botella

Antes de optimizar debemos identificar los cuellos de botella, que son las secciones de la aplicación sobre las que recae el mayor coste de proceso. Disponemos de distintas herramientas para medir el rendimiento del sistema. A continuación se muestran algunas:

Apache benchmarks.

El programa ab permite medir el tiempo de respuesta de una petición HTTP a nuestro servidor web. ab nos ofrece la medición más global y menos detallada, e incluye desde el tiempo de proceso de la petición hasta el coste de la capa de transporte.

Una buena práctica consiste en realizar benchmarks a todas las rutas de nuestro sitio para averiguar en cuáles se está ofreciendo una peor respuesta.

$ ab -c 5 -n 100 http://mysite.com/example

This is ApacheBench, Version 2.3 <$Revision: 655654 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking mysite.com (be patient).....done


Server Software:        Apache/2.2.14
Server Hostname:        mysite.com
Server Port:            80

Document Path:          /example
Document Length:        5762 bytes

Concurrency Level:      5
Time taken for tests:   24.799 seconds
Complete requests:      100
Failed requests:        0
Write errors:           0
Total transferred:      631854 bytes
HTML transferred:       576200 bytes
Requests per second:    4.03 [#/sec] (mean)
Time per request:       1239.939 [ms] (mean)
Time per request:       247.988 [ms] (mean, across all concurrent requests)
Transfer rate:          24.88 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:      187  191   3.5    190     215
Processing:   586 1028 649.7    848    5460
Waiting:      398  839 650.1    660    5272
Total:        776 1219 650.7   1039    5648

Percentage of the requests served within a certain time (ms)
  50%   1039
  66%   1180
  75%   1247
  80%   1438
  90%   1660
  95%   2173
  98%   3647
  99%   5648
100%   5648 (longest request)

PHP Profiling

Hay herramientas como Xdebug y CacheGrind que se integran en el servidor web y que permiten un detalle más pormenorizado de dónde se está consumiendo la mayor parte del tiempo en nuestro programa. Esto nos permitirá descubrir algoritmos mejorables y accesos a bases de datos demasiado costosos.

Xdebug se integra en Apache y permite generar archivos que contienen información detallada sobre el flujo de ejecución de PHP. KCacheGrind ofrece una interfaz para interpretar los datos generados por Xdebug. Debe tenerse en cuenta que Xdebug añade mucha carga de proceso, por lo que no debe activarse en entornos de producción.

Mysqlslap. Slow queries

Si descubrimos que uno de nuestros cuellos de botella se encuentra en una consulta a la base de datos, probablemente tengamos que añadir índices o reescribir la consulta. Cada paso que demos en la mejora del rendimiento debe ser medido para asegurarnos de que la modificación es beneficiosa. Mysqlslap es una aplicación cliente de Mysql que nos ayuda en esta tarea.

$ mysqlslap -i50 -c5 -q"SELECT * FROM users u JOIN users_roles r USING(uid) WHERE u.name LIKE 'a%'" -uroot -p --create-schema=my_db_name --delimiter=";"
Enter password:
Benchmark
    Average number of seconds to run all queries: 0.007 seconds
    Minimum number of seconds to run all queries: 0.006 seconds
    Maximum number of seconds to run all queries: 0.013 seconds
    Number of clients running queries: 5
   Average number of queries per client: 1

El log mysl-slow de MySQL almacena aquellas queries que superan un umbral definido en la configuración. Es útil supervisar este log (incluso incorporando automatismos) para identificar las consultas que están deteriorando el rendimiento de la web.

Para activar el log debe modificarse la configuración de MySQL y reiniciar el servidor.

/etc/mysql/my.cnf

log_slow_queries = /var/log/mysql/mysql-slow.log
long_query_time  = 2

Adicionalmente puede configurarse el parámetro log-queries-not-using-indexes para que quedan registradas todas aquellas queries que están realizando JOINS sin utilizar índices (es decir, leyendo de disco duro). En bases de datos bien normalizadas debe asegurarse el aprovechamiento de la memoria mediante la creación de índices. De este modo evitaremos las lecturas en disco.

Módulo Devel

El módulo Devel aúna tanto el profiling como las mediciones a bases de datos, y proporciona una interfaz sencilla y directa que nos muestra el tiempo consumido en cada llamada y las veces que ésta se ha realizado. Su desventaja es que es menos fiable, ya que no permite iteraciones ni concurrencia, y que el propio módulo puede influir en los resultados.

Optimización PHP

La experiencia y las buenas prácticas de programación pueden ayudar a evitar problemas de rendimiento. A continuación se exponen algunos consejos.

Almacenamiento estático.

En aquellas funciones donde preveamos llamadas recurrentes, almacenaremos los valores de retorno en variables estáticas (sobreviven al desapilamiento de la función).

/**
* Retrieves the number of times a user has voted a content up.
* @param $uid User uid.
* @return Number of votes.
*/
function my_module_get_user_votes($uid) {
  static $votes = array();
  if (!isset($votes[$uid])) {
    // process
    $votes[$uid] = $result;
  }
  return $votes[$uid];
}

En el ejemplo anterior, el almacenamiento estático evitará que procesemos varias veces el mismo código para un uid.

Operaciones con arrays.

En la mayoría de los casos, los problemas de rendimiento están relacionados con operaciones de búsqueda en arrays. Llamadas a funciones como in_array() y bucles de coste N pueden refactorizarse fácilmente con arrays con claves.

/
* Retrieves the number of times a user has voted a content up.
* @todo Low performance O(n).
* @param $uid User uid.
* @return Boolean, TRUE if user is in freak users list.
*/
function my_module_user_is_freak($uid) {
  $users = my_module_get_freak_users();
  foreach ($users as $user) {
    if ($user->uid == $uid) {
      return TRUE;
    }
  }
  return FALSE;
}

/
* Retrieves the number of times a user has voted a content up.
* High performance O(1).
* @param $uid User uid.
* @return Boolean, TRUE if user is in freak users list.
function my_module_user_is_freak_improved($uid) {
  $users = my_module_get_freak_users();
  return isset($users[$uid]);
}

Estructuras de control

De un modo muy parecido a las operaciones con arrays, evaluaciones de múltiples elseif pueden transformarse en switch case, más eficiente.

// O(n)
if ($op == 'insert') {
  // process
}
elseif ($op == 'update') {
  // process
}
elseif ($op == 'delete') {
  // process
}


// O(1)
switch ($op) {
  case 'insert':
    // process
    break;
  case 'update':
    // process
    break;
  case 'delete':
    // process
    break;
}

Paso por referencia

Normalmente no es recomendable el paso por referencia, ya que exponemos los argumentos pudiendo cambiarlos involuntariamente. En estructuras de datos muy grandes, el paso por referencia evitará que se cree una copia en memoria, aligerando el uso de la misma y el tiempo de proceso.

function my_module_foo(&$big_collection) {
  // process
}

Optimización de bases de datos.

En la mayoría de los casos, dado que las páginas web acceden por lo general constantemente a la base de datos, en las consultas encontraremos gran parte de los problemas de optimización. Una sola consulta puede provocar que la aplicación sea completamente inusable. A continuación se exponen algunos conceptos que pueden ayudar a solucionar problemas relacionados con la base de datos. Os dejo también un enlace muy interesante sobre optimización de consultas de Ricardo Galli (Gallir), padre de Menéame.

Se asume para esta sección y para todo el documento que nuestra instalación Drupal funciona sobre una base de datos MySQL.

InnoDB vs MyISAM. Foreign Keys

La instalación por defecto de Drupal en MySQL se realiza con el motor MyISAM. Aunque MyISAM es a priori más rápido en las lecturas que InnoDB, presenta algunos problemas:

  • MyISAM no dispone de bloqueo por filas. Una inserción o modificación sobre una tabla provoca un bloqueo sobre la tabla completa. Cualquier otra escritura tendrá que esperar a que la primera termine. InnoDB permite bloqueos por fila.
  • MyISAM no soporta transacciones.
  • MyISAM no soporta claves ajenas. En InnoDB, un índice es creado automáticamente sobre una clave ajena, por lo que los JOIN serán mucho más rápidos.

Por estas razones, es recomendable cambiar a InnoDB todas las tablas (incluso las de Drupal). No se recomienda añadir claves ajenas entre tablas del core dado que restricciones de procesamiento en cascada pueden interferir con el proceso en PHP.

EXPLAIN

La orden EXPLAIN de MySQL proporciona información valiosa a la hora de estudiar el comportamiento de una query. Conviene por tanto estudiar el significado de las columnas y los valores representados para entender qué está haciendo el optimizador de MySQL.

mysql> EXPLAIN SELECT * FROM users u JOIN comments c USING(uid) ORDER BY c.subject;
+----+-------------+-------+--------+---------------+---------+---------+------------------+--------+----------------+
| id | select_type | table | type   | possible_keys | key     | key_len | ref              | rows   | Extra          |
+----+-------------+-------+--------+---------------+---------+---------+------------------+--------+----------------+
|  1 | SIMPLE      | c     | ALL    | NULL          | NULL    | NULL    | NULL             | 398659 | Using filesort |
|  1 | SIMPLE      | u     | eq_ref | PRIMARY       | PRIMARY | 4       | educapoker.c.uid |      1 | Using where    |
+----+-------------+-------+--------+---------------+---------+---------+------------------+--------+----------------+
2 rows in set (0.00 sec)

Índices

La creación de claves ajenas e índices sobre tablas puede mejorar considerablemente el rendimiento de la consulta. Todo índice se almacena en memoria si hay disponible. Una de las medidas que debemos tomar es asegurarnos de que MySQL dispone de suficiente memoria para los índices InnoDB.

Nota: el siguiente ejemplo es didáctico. No se sugiere de ningún modo que el índice expuesto a continuación sea recomendable en entornos de producción.

mysql> CREATE TABLE tmp SELECT uid, subject FROM comments LIMIT 100;
mysql> EXPLAIN SELECT * FROM users u JOIN tmp c USING(uid) ORDER BY c.subject;
+----+-------------+-------+--------+---------------+---------+---------+------------------+------+----------------+
| id | select_type | table | type   | possible_keys | key     | key_len | ref              | rows | Extra          |
+----+-------------+-------+--------+---------------+---------+---------+------------------+------+----------------+
|  1 | SIMPLE      | c     | ALL    | NULL          | NULL    | NULL    | NULL             |  100 | Using filesort |
|  1 | SIMPLE      | u     | eq_ref | PRIMARY       | PRIMARY | 4       | educapoker.c.uid |    1 |                |
+----+-------------+-------+--------+---------------+---------+---------+------------------+------+----------------+
2 rows in set (0.00 sec)


mysql> ALTER TABLE tmp ADD INDEX idx_sbj (uid, subject);
mysql> EXPLAIN SELECT * FROM users u JOIN tmp c USING(uid) ORDER BY c.subject;
+----+-------------+-------+--------+---------------+---------+---------+------------------+------+-----------------------------+
| id | select_type | table | type   | possible_keys | key     | key_len | ref              | rows | Extra                       |
+----+-------------+-------+--------+---------------+---------+---------+------------------+------+-----------------------------+
|  1 | SIMPLE      | c     | index  | idx_sbj       | idx_sbj | 198     | NULL             |  100 | Using index; Using filesort |
|  1 | SIMPLE      | u     | eq_ref | PRIMARY       | PRIMARY | 4       | educapoker.c.uid |    1 |                             |
+----+-------------+-------+--------+---------------+---------+---------+------------------+------+-----------------------------+
2 rows in set (0.00 sec)

Gracias al mantenimento de índices en memoria conseguiremos evitar, como dijimos anteriormente, costosas operaciones de lectura en disco.

SQL Rewritting

El modo más directo de mejorar el rendimiento de una consulta es reescribiéndola. El dominio de SQL y la práctica son las mejores recetas para adquirir una buena técnica en el SQL Rewritting.

Transformar subconsultas

IN -> JOIN

SELECT t1.uid
FROM t1
     JOIN t2 USING(uid)
WHERE t1.uid IN (SELECT uid
                 FROM t3
                 WHERE something);


SELECT t1.uid
FROM t1
     JOIN t2 USING(uid)
     JOIN t3 ON t1.uid = t3.uid
WHERE something;

NOT IN -> LEFT JOIN

SELECT t1.uid
FROM t1
     JOIN t2 USING(uid)
WHERE t1.uid NOT IN (SELECT uid
                     FROM t3
                     WHERE something);


SELECT t1.uid
FROM t1
     JOIN t2 USING(uid)
     LEFT JOIN t3 ON t1.uid = t3.uid
WHERE t3.uid IS NULL AND
      something;

Acotar muestras en el FROM

El orden de ejecución de una query de MySQL es vertical y descendente. El FROM se evalúa antes que el WHERE, y el WHERE antes que el GROUP BY, etc.
Una manera eficaz de mejorar una query es reduciendo la muestra manipulada lo antes posible, esto es, durante el FROM.

Evitar lecturas innecesarias

SELECT * y SELECT COUNT(*) son por lo general maneras muy poco eficientes de realizar consultas, ya que su uso implica leer todos los datos del disco duro. Si son pocos los datos a recuperar, colocando solo las columnas necesarias en la cláusula SELECT obtendremos mejores resultados (especialmente con el uso de índices).

Cambio de perspectiva

A veces, tras muchos intentos fracasados de optimización, lo mejor es replantearse la query desde el principio y buscar nuevas perspectivas. Consultar el problema con los compañeros o trabajar en otras cosas durante unas horas o hasta el día siguiente es la mejor receta a título personal.

Denormalización

En casos de extrema necesidad, puede ser conveniente denormalizar las tablas duplicando la información. En estos casos (que deberían ser muy pocos), conviene establecer sistemas que prevengan posibles incoherencias en la duplicidad.

Ejemplo: tabla node_comment_statistics

mysql> DESC node_comment_statistics;
+------------------------+------------------+------+-----+---------+-------+
| Field                  | Type             | Null | Key | Default | Extra |
+------------------------+------------------+------+-----+---------+-------+
| nid                    | int(10) unsigned | NO   | PRI | 0       |       |
| last_comment_timestamp | int(11)          | NO   | MUL | 0       |       |
| last_comment_name      | varchar(60)      | YES  |     | NULL    |       |
| last_comment_uid       | int(11)          | NO   |     | 0       |       |
| comment_count          | int(10) unsigned | NO   | MUL | 0       |       |
+------------------------+------------------+------+-----+---------+-------+
5 rows in set (0.00 sec)

Drupal y los problemas de rendimiento.

Drupal, aunque es un excelente gestor de contenidos, presenta algunas dificultades con respecto al rendimiento. El sistema de hooks, aunque muy potente, implica cargar todo el código en cada petición HTTP y averiguar para cada disparador de hook qué módulos están implementando dicho hook.

Activar la caché de Drupal

Si la caché de Drupal está desactivada, se ejecutará el código PHP y realizarán las consultas a la base de datos para cada petición que se realice al sitio.

La caché de Drupal permite almacenar en memoria la salida HTML de cada página para los usuarios anónimos. El tiempo de vida de la caché puede configurarse en la interfaz de administración en Site Configuration -> Performance.

Reducir módulos

Instalar módulos en Drupal es tan fácil que a menudo nos vemos tentados a descargar y activar decenas de módulos contribuídos buscando para nuestra web el mayor conjunto de funcionalidades posible. Sin embargo, añadir una funcionalidad tras otra no tiene por qué mejorar la experiencia de navegación de los visitantes de nuestro site.

Al contrario, no solo podemos empeorar la usabilidad de nuestra página con un exceso de bloques y rutas de menú, sino que de buen seguro estaremos causando una severa pérdida de rendimiento.

Si tenemos más de 100 módulos instalados vamos por mal camino. Con 60 u 80 (si pueden ser menos, tanto mejor) debería bastar para cualquier sitio web.

Reducir carga en BOOTSTRAP. Includes.

Durante el BOOTSTRAP o inicialización de Drupal en una petición HTTP, todo el código en los .module es leído del disco duro y cargado en memoria.

Muchas de las funciones que implementamos son sólo necesarias en determinadas interfaces, por lo que no tiene sentido cargarlas en todas las peticiones que reciba el servidor. En el hook_menu() puede añadirse la clave 'file' en un item para indicar a Drupal qué archivos debe incluir dinámicamente en la carga.

/**
* Implements hook_menu().
*/
function my_module_menu() {
  $items = array();

  // Admin paths
  $items['admin/my_module/whatever'] = array(
    'title' => 'Foo bar baz',
    'description' => 'Administer whatever',
    'page callback' => 'my_module_admin_page',
    'access arguments' => array('administer whatever'),
    'file' => 'my_module.admin.inc'
  );

  //...
  return $items;
}

Reducir funcionalidades

Si añadir una funcionalidad resulta en un quebradero de cabeza en cuanto a rendimiento, debemos plantearnos si en realidad es necesaria o útil. ¿Es rentable?. ¿Merece la pena invertir tiempo en ella?. Muchas de las funcionalidades que implementamos terminan desactivadas o infrautilizadas por los usuarios.

Funciones costosas.

Las facilidades que ofrece Drupal al programador deben tomarse con precauciones. Por ejemplo, la función user_load() no cachea estáticamente los resultados y puede convertirse en una función muy costosa (depende de los módulos que se estén enganchando al hook_user()). En determinadas secciones de la web podemos estar realizando decenas o centenares de consultas a la base de datos que podríamos reducir en una sola.

En especial si hemos instalado PHP 5.3 en nuestro servidor (mucho más restrictivo con los warnings), la función watchdog() es otra de las que pueden deteriorar significativamente el rendimiento de nuestra web. Si hemos activado el módulo Database Logging, que intercepta las llamadas a watchdog() para registrar los mensajes en la base de datos, y la interacción con nuestra base de datos es lenta, conviene desactivar este módulo y dejar activo el módulo syslog que dejará las trazas en un logfile en el disco duro.

drupal_write_record(). Comparativas frente a db_query().

Casi siempre es recomendable utilizar drupal_write_record() en lugar de db_query() por diversos motivos:

  • Portabilidad
  • Abstracción
  • Seguridad
  • Mantenibilidad

En las pruebas realizadas en el Departamento de Desarrollo de Aureka, comprobamos que drupal_write_record() es entre 5 y 10 veces más lenta que db_query(). En algunos casos muy concretos (secciones críticas con escrituras masivas o procesos batch extremadamente largos) puede resultar necesario transformar los drupal_write_record() en db_query().

Spain

Group organizers

Group categories

Región geográfica

Group events

Add to calendar

Group notifications

This group offers an RSS feed. Or subscribe to these personalized, sitewide feeds:

Hot content this week