Blog de Israel Viana

Como las fechas de los últimos posts indican, he abandonado el blog. Lo mantengo on-line por si alguien encuentra útil algún artículo, y quizá algún día tenga tiempo y ganas para retomarlo. Gracias a todos, ha sido un placer.

Artículos sobre rendimiento

Mejor rendimiento de Drupal con GraphicsMagick

14 de mayo de 2010 | 0 comentarios

Un pequeño (y espero útil) apunte antes de que penséis que he abandonado completamente el blog :-(

Se trata de un consejo para mejorar el rendimiento de nuestro sitio Drupal. El sistema utiliza por defecto la biblioteca GD para manipular imágenes (por ejemplo, generar miniaturas). El módulo ImageAPI inserta una capa de abstracción en Drupal que permite elegir qué biblioteca gráfica usar, GD o ImageMagick. Es mejor utilizar ImageMagick, ya que es una biblioteca independiente de PHP y, por ello obtendremos mejor rendimiento total en Drupal, y además es más estable (si falla GD puede caer todo el motor de PHP, sin embargo, si falla ImageMagick no caerá, ya que es un binario diferente).

Pero aún mejor: existe un proyecto llamado GraphicsMagick, que es un fork de ImageMagick y que es más estable y, sobre todo, con mejor rendimiento, ya que consume menos memoria y es más rápida. Y lo que es mejor, tiene la misma API que ImageMagick (el comando convert), con lo cual podemos engañar a ImageAPI diciéndole que use ImageMagick cuando en realidad tenemos instalado GraphicsMagick.

Para lograr esto no tienes más que instalar GraphicsMagick y el módulo ImageAPI de Drupal, en dos sencillos pasos. Supongamos que trabajamos con un servidor Debian/Ubuntu (en otros sistemas operativos no debería ser muy diferente):

  1. Instalamos GraphicsMagick: apt-get install graphicsmagick-imagemagick-compat

    Podemos probar que GraphicsMagick funciona escribiendo convert en la consola:

    isra@salon:~$ convert
    GraphicsMagick 1.3.5 2009-01-26 Q8 http://www.GraphicsMagick.org/
    Copyright (C) 2002-2009 GraphicsMagick Group.
    Additional copyrights and licenses apply to this software.
    See http://www.GraphicsMagick.org/www/Copyright.html for details.

    Usage: convert [options ...] file [ [options ...] file ...] [options ...] file

    [más opciones]
  2. Instalamos el módulo ImageAPI y lo activamos en /admin/build/modules (módulos ImageAPI e ImageAPI ImageMagick).
  3. Accedemos a la configuración de ImageAPI en Administración > Configuración del sitio > ImageAPI (/admin/settings/imageapi) y veremos el siguiente mensaje: "The ImageAPI ImageMagick module is the only enabled image toolkit. Drupal will use it for resizing, cropping and other image manipulations".
  4. Si también hemos activado el módulo ImageAPI GD2, nos dará a elegir (seleccionamos ImageAPI ImageMagick como opción por defecto):

¡Y listo! Fácil y rápido. Podemos comprobar que funciona accediendo a Administración > Configuración del sitio > ImageAPI > Configurar (/admin/settings/imageapi/config), lo que nos mostrará información sobre GraphicsMagick (versión, tipos soportados, etc):

¿He hecho benchmarks para comprobar la ganancia de rendimiento con respecto a ImageMagick?

Pues no, lo siento. Pero los chicos de GraphicsMagick sí los han hecho, y son bastante jugosos ;-)

Pero... ¿no es necesaria la extension imagick de PHP?

No, no es necesaria, ya que ImageAPI interactúa con ImageMagick/GraphicsMagick a través del binario convert.

Iré escribiendo algunos artículos más sobre técnicas fáciles para optimizar el rendimiento de Drupal.


Aún más rendimiento con CssDispatcher

25 de octubre de 2009 | 0 comentarios
CssDispatcher

En el artículo "Maneja las CSS como un profesional" veíamos algunas técnicas para insertar variables y funciones en las hojas de estilo, así como posibles mejoras del rendimiento en el cliente a la hora de servir las CSS, y para ello utilizábamos la biblioteca CssDispatcher.

Pues bien, en este post vamos a ver varias soluciones para aumentar el rendimiento en el servidor cuando servimos hojas de estilos, especialmente si son hojas de estilos tratadas con CssDispatcher.

Es importante diferenciar las mejoras de rendimiento en el cliente y el servidor. Mientras los benchmarks nos dicen que el 80% del tiempo de carga de una página lo causa el cliente (transferencias, renderizado, etc), para nosotros también es importante ese otro 20%, porque genera una carga en el servidor (acceso a disco duro, procesador, memoria...) que puede ponernos en aprietos si las visitas aumentan.

Accesos al disco duro

De por sí, servir una hoja de estilos simple no suele ser muy costoso para un servidor web. Si un cliente solicita /main.css, el servidor abre el archivo, lo lee y lo devuelve. Además, si el sistema operativo implementa caché de ficheros, 1000 visitas no generarán 1000 accesos al disco duro, lo cual aumentará la velocidad y reducirá la carga del servidor.

Si queremos ir más allá y acelerar aún más los accesos a disco duro podemos poner los archivos del servidor un sistema de archivos montado sobre la memoria RAM. Es decir, una porción del sistema de archivos que, en lugar de guardarse en el disco duro, lo hace sobre una porción de la memoria RAM. No es una técnica muy recomendable, porque aunque no es difícil montarlo, los datos desaparecen al apagar o reiniciar la máquina, por lo que habría que tener sincronizados los directorios en memoria con otros en el disco duro para que no se perdiesen.

Procesamiento de la petición

Como ya he dicho antes y todo el mundo sabe, para el servidor web devolver un archivo estático no le cuesta más que abrirlo e irlo enviando por el socket. Pero si la hoja de estilos pasa por el compilador de PHP (por introducir códigos PHP en la hoja de estilos) entonces la cosa se complica un poco. Para acelerar el procesado de scripts es recomendable usar:

  • Una caché de salida, que evita tener que ejecutar repetitivamente el mismo código para cada petición.
  • Un opcode cache, que evita tener que compilar el código para cada petición (si hemos implementado caché de salida, para la mayoría de las peticiones no será necesario ni compilar ni ejecutar el código).
Diagramas de flujo para el despacho de peticiones con caché

Para la opcode cache existen herramientas como APC, XCache o eAccelerator, que una vez instaladas y configuradas debidamente funcionan sin que tengamos que modificar el código de nuestra aplicación web. Puedes leer más sobre opcode cache en PHP Accelerators.

En cuanto a la caché de salida, hay principalmente dos caminos a seguir: gestionar la caché desde el servidor web o desde PHP. La ventaja del primero es que, al no ejecutar código PHP es un poco más rápido. Esto se puede lograr con el módulo Apache mod_cache. La otra posibilidad, gestionarlo con PHP, nos da más flexibilidad y control sobre la solución. En concreto, podremos guardar la caché en el disco duro o en memoria, con gestores de caché como Memcached (mi preferido).

Caché con URL propia

Existe una tercera solución, y es utilizar una caché en disco duro sin procesar las peticiones por PHP. El funcionamiento es el siguiente: si tenemos una hoja de estilos generada con CssDispatcher en /estilos.css.php, podemos guardar la salida en un archivo y ponerlo accesible en /estilos.css. Al ser un archivo estático, se servirá tan rápido como una hoja de estilos normal. Este sistema tiene un problema, y es que si tenemos hojas de estilos específicas para navegadores, no resultará fácil servir una u otra de forma estática. La única solución aparente es utilizar redirecciones y estructuras condicionales en los ficheros de configuración de Apache.

Algo de código, por favor

Vamos a implementar tres sistemas de caché: uno con Memcached, otro en disco y otro en disco accesible directamente.

Cache en Memcache

<?php

include 'class.Css.php';

//Nombre del elemento en Memcache
$cache_name = 'moduleName_' . Css::getUserAgent();
//Tiempo de vida. Para archivos que no cambien mucho, poner valores altos
$cache_time = 3600*24; 

//Conexión a Memcache
$mc = new Memcache();
$mc->connect('localhost');

//Si la salida no está en caché, se genera y se almacena
if (!$out = $mc->get($cache_name)) {

    //Incluye la biblioteca CssDispatcher.
    //No se incluye si no es necesaria
    include 'class.CssDispatcher.php';

    $estilos = new CssDispatcher;

    //Crea una nueva hoja de estilos
    $general = new Css('example.css.php');
    //Asigna variables
    $general->background = '#eee';
    $general->border_color = 'red';
    $general->header_size = 2.1;

    //Crea otra hoja de estilos para navegadores WebKit
    $another = new Css('example2.css.php', Css::UA_WEBKIT);
    $another->bold = 'font-weight: bold';

    //Añade las hojas de estilo al dispatcher
    $estilos->add($general);
    $estilos->add($another);

    //Añade un aviso
    $out = '/* Generated by CssDispatcher ' . strftime('%c') . " */\n"
        . $estilos->render(false, true, false, true);

    //Guarda la salida en Memcache
    $mc->set($cache_name, $out, null, $cache_time);
}

header("Content-Type: text/css");
echo $out;
die();

?>

Tiempo de ejecución (renovando la caché): 3.6480ms
Tiempo de ejecución (utilizando la caché): 0.6439ms

Caché en disco duro

Este script tendrá será más lento y pesado para la CPU que el anterior, pero no necesita de servicios adicionales como Memcached, imposibles de instalar en alojamientos compartidos.

<?php

include 'class.Css.php';

//Archivo de caché
$cache_name = 'cssCache/moduleName_' . Css::getUserAgent();
//Tiempo de vida, poner valores altos para archivos que no suelen cambiar
$cache_time = 30; 

if (file_exists($cache_name)) {
    if (date('U') - fileatime($cache_name) > $cache_time) {
        $regenerate = true;
    } else {
        $out = file_get_contents($cache_name);
    }
} else {
    $regenerate = true;
}

//Si la salida no está en caché, se regenera
if ($regenerate) {

    //Carga la biblioteca CssDispatcher
    include 'class.CssDispatcher.php';

    $estilos = new CssDispatcher;

    //Crea una nueva hoja de estilos
    $general = new Css('example.css.php');
    //Asigna variables
    $general->background = '#eee';
    $general->border_color = 'red';
    $general->header_size = 2.1;

    //Añade otra hoja de estilos para navegadores WebKit
    $another = new Css('example2.css.php', Css::UA_WEBKIT);
    $another->bold = 'font-weight: bold';

    //Añade las plantillas al dispatcher
    $estilos->add($general);
    $estilos->add($another);

    $out = '/* Generated by CssDispatcher ' . strftime('%c') . " */\n"
        . $estilos->render(false, true, false, true);

    //Graba la salida en el fichero
    file_put_contents($cache_name, $out);
}

header("Content-Type: text/css");
echo $out;
die();

?>

Tiempo de ejecución (renovando caché): 1.290ms
Tiempo de ejecución (utilizando caché): 0.242ms

El último caso, una caché en ficheros accesibles públicamente, es más rápida y más ligera que la anterior, pero nos añade la tarea de actualizar la caché cuando nos interese, bien con una tarea programada (cron) o mediante algún otro mecanismo.

<?php

include 'class.Css.php';
include 'class.CssDispatcher.php';

//Generamos hojas de estilos para todos los navegadores
$navegadores = array(Css::UA_IE6, Css::UA_IE, Css::UA_GECKO, Css::UA_WEBKIT);

//Para cada navegador generamos la hoja de estilos correspondiente
foreach ($navegadores as $navegador) {
    //Archivo de caché
    $cache_name = 'cssCache/estilos_' . $navegador . '.css';
    //Tiempo de vida. Para archivos que no cambien mucho, poner valores altos
    $cache_time = 30; 

    $estilos = new CssDispatcher;

    //Crea una nueva hoja de estilos
    $general = new Css('example.css.php');
    //Asigna variables
    $general->background = '#eee';
    $general->border_color = 'red';
    $general->header_size = 2.1;

    //Añade las plantilla al dispatcher
    $estilos->add($general);

    //Hoja de estilos para WebKit
    //No vale utilizar la detección de navegador de CssDispatcher
    if ($navegador == Css::UA_WEBKIT) {
        $another = new Css('example2.css.php');
        $another->bold = 'font-weight: bold';
        $estilos->add($another);
    }

    $out = '/* Generated by CssDispatcher ' . strftime('%c') . " */\n"
        . $estilos->render(false, true, false, true);

    //Graba la salida en el fichero
    file_put_contents($cache_name, $out);
}

echo "Caché de CSS actualizada";

?>

Tiempo de ejecución del actualizador de caché: 4.009ms
Tiempo de obtención de la hoja de estilos: 86ms (incluye la transmisión del fichero)

Ya que con este método no tenemos un sólo punto de entrada para las diferentes hojas de estilos, será necesario cargar una u otra CSS en función del navegador, a través de los artificios clásicos: comentarios condicionales o la cabecera User-Agent. Pero tendremos que hacerlo manualmente, ya que al usar ficheros estáticos no se ejecuta CssDispatcher. Por ejemplo, según el ejemplo, si el navegador es Firefox tendremos que invocar cssCache/estilos_20.css.

Por mi parte, es todo por hoy. ¿Conoces alguna otra técnica para aumentar el rendimiento al servir CSS? ¿Has probado a cachear la salida de CssDispatcher?


Plantillas PHP: cuestión de rendimiento

2 de abril de 2009 | 5 comentarios

He leído dos posts bastante viejos (2006 y 2007) del gran Ricardo Galli sobre el rendimiento y escalabilidad de Twitter y los sistemas de plantillas en PHP. En ambos posts las discusiones han sido muy ricas, y me llevan a dos grandes conclusiones: aunque el cache y escalabilidad horizontal son soluciones importantes para la alta disponibilidad de sitios web, es vital escribir buen código. Y en ese sentido, los sistemas de plantillas y los frameworks pueden jugar malas pasadas.

Plantillas PHP, sin pseudo-lenguajes

En posts recientes he hablado de Smarty y de PHPTAL, dos sistemas de plantillas que añaden un lenguaje nuevo a nuestras aplicaciones web, ya cargadas con PHP, SQL, XHTML, CSS, JavaScript y nosecuantas siglas más. Eso significa que, además de la curva de aprendizaje, hay que cargar el proyecto con un parser del pseudo-lenguaje. En el caso de Smarty, dicen ser rápidos, aunque hay un fork del proyecto llamado TemplateLight que dice ser más liviano que su "papá".

Existe un método para utilizar plantillas y no usar sistemas adicionales. Es una sistema sencillo, estúpidamente sencillo, pero eficiente y prácticamente con los mismos beneficios que usar Smarty u otros: plantillas en PHP, embebiendo los códigos en los documentos HTML, pero sin las chapuzas de siempre, separando las capas de lógica y vista.

Podemos crear una pequeña biblioteca de funciones para gestionar plantillas de este tipo y que los nombre de variables no causen conflictos. Voy a hacer algún proyecto con este sistema, a modo de prueba (es lo que tiene ser freelance, puedo usar la tecnología que me dé la gana ;-). Utilizar plantillas en PHP permite separar la presentación de la lógica, pero sin la bajada de rendimiento de los sistemas que parsean plantillas escritas en un pseudo-lenguaje como Smarty, del que ya se ha hablado por aquí. Reconozco que las plantillas PHP no tienen la legibilidad de Smarty o PHPTAL, pero se le acerca bastante.

simpletpl.php (la librería del motor)


<?php

//Directorio donde están las plantillas
define("TPL_DIR", "tpl");

//Array que alojará las variables de las plantillas
$tpl_vars = array();

//Asigna una variable a la plantilla
function tpl_asignar($nombre, $valor) {
    global $tpl_vars;
    $tpl_vars[$nombre] = $valor;
}

//Devuelve una variable de la plantilla
function tpl($variable) {
    global $tpl_vars;
    if (isset($tpl_vars[$variable])) return $tpl_vars[$variable];
    else trigger_error("La variable '$variable' no existe", E_USER_ERROR);
}

//Carga la plantilla especificada
function tpl_cargar($plantilla) {
    global $tpl_vars;
    if (file_exists(TPL_DIR)) {
        if (file_exists(TPL_DIR . "/$plantilla")) {
            include(TPL_DIR . "/$plantilla");
        } else trigger_error("La plantilla '$plantilla' no existe", E_USER_ERROR);
    } else trigger_error("El directorio de plantillas especificado no existe. Verifique la constante TPL_DIR", E_USER_ERROR);
}

//Indenta un texto al $n - ésimo nivel
function tab($texto, $n) {
    return preg_replace('/\n/', "\n" . str_repeat("\t", $n), $texto);
}

?>

inicio.tpl.php (un ejemplo de plantilla cualquiera)


<html>
<head>
    <title><?php echo tpl('titulo') ?></title>
</head>
<body>
    <h1><?php echo tpl('titulo') ?></h1>
    
<?php foreach (tpl('posts') as $p) { ?>
    <h2><a href="<?php echo $p['url'] ?>"><?php echo $p['titulo'] ?></a></h2>
    <div style="color: gray"><?php echo $p['creado'] ?></div>

    <?php echo tab($p['cuerpo'], 2) ?>

<?php } ?>

<?php tpl_cargar("pie.tpl.php") ?>

pie.tpl.php (otra plantilla de ejemplo, para ser incluida)

<div style="background: silver; padding: 10px">2008 © Israel Viana</div>
</body>
</html>

index.php (script que obtiene los datos y carga la plantilla)


<?php
include("simpletpl.php");
$conexion = mysql_connect("localhost", "root", "root");
mysql_select_db("blog");

$q = mysql_query("SELECT * FROM posts ORDER BY id DESC LIMIT 5");

$posts = array();
while ($p = mysql_fetch_assoc($q)) {
    $p['url'] = "http://www.israelviana.es/post.php?id=" . $p['id'];
    $posts[] = $p;
}

tpl_asignar("titulo", "Blog de Israel Viana");
tpl_asignar("posts", $posts);

tpl_cargar("inicio.tpl.php");

?>

israelviana.es es propiedad de Israel Viana, escrito en Murcia (España). Puedes ponerte en contacto conmigo a través de la dirección de e-mail .com.
Información en RDF Metadatos Dublin Core Creative Commons License