Symfony 1.2, la guía definitiva

18.4. Optimizando la cache

El Capítulo 12 describe cómo guardar en la cache partes de la respuesta o incluso la respuesta completa. Como guardar la respuesta en la cache mejora mucho el rendimiento de la aplicación, esta técnica debería ser una de las primeras a considerar para optimizar las aplicaciones. En esta sección se muestra cómo sacar el máximo partido a la cache e incluye algunos trucos muy interesantes.

18.4.1. Borrando partes de la cache de forma selectiva

Durante el desarrollo de una aplicación, se dan muchas situaciones en las que se debe borrar la cache:

  • Cuando se crea una clase nueva: añadir la clase a un directorio para el que funciona la carga automática de clases (cualquier directorio lib/ del proyecto) no es suficiente para que Symfony sea capaz de encontrarla en los entornos de ejecución que no sean el de desarrollo. En este caso, es preciso borrar la cache de la carga automática para que Symfony recorrar otra vez todos los directorios indicados en el archivo autoload.yml y pueda encontrar las nuevas clases.
  • Cuando se modifica la configuración en el entorno de producción: en producción, la configuración de la aplicación solamente se procesa durante la primera petición. Las siguientes peticiones utilizan la versión guardada en la cache. Por lo tanto, cualquier cambio en la configuración no tiene efecto en el entorno de producción (o en cualquier otro entorno donde la depuración de aplicaciones esté desactivada) hasta que se borre ese archivo de la cache.
  • Cuando se modifica una plantilla en un entorno en el que la cache de plantillas está activada: en producción siempre se utilizan las plantillas guardadas en la cache, por lo que todos los cambios introducidos en las plantillas se ignoran hasta que la plantilla guardada en la cache se borra o caduca.
  • Cuando se actualiza una aplicación mediante el comando project:deploy: este caso normalmente comprende las 3 modificaciones descritas anteriormente.

El problema de borrar la cache entera es que la siguiente petición tarda bastante tiempo en ser procesada, porque se debe regenerar la cache de configuración. Además, también se borran de la cache las plantillas que no han sido modificadas, por lo que se pierde la ventaja de haberlas guardado en la cache.

Por este motivo, es una buena idea borrar de la cache solamente los archivos que hagan falta. Las opciones de la tarea cache:clear pueden definir un subconjunto de archivos a borrar de la cache, como muestra el listado 18-14.

Listado 18-14 - Borrando solamente algunas partes de la cache

// Borrar sólo la cache de la aplicación "frontend"
> php symfony cache:clear frontend

// Borrar sólo la cache HTML de la aplicación "frontend"
> php symfony cache:clear frontend template

// Borrar sólo la cache de configuración de la aplicación "frontend"
> php symfony cache:clear frontend config

También es posible borrar a mano algunos archivos del directorio cache/ o borrar las plantillas guardadas en la cache desde la acción mediante el método $cacheManager->remove(), como se describe en el capítulo 12.

Todas estas técnicas minimizan el impacto negativo sobre el rendimiento de todos los cambios mostrados anteriormente.

Truco Cuando se actualiza Symfony, la cache se borra de forma automática, sin intervención manual (si se establece la opción check_symfony_version a true en el archivo de configuración settings.yml).

18.4.2. Generando páginas para la cache

Cuando se instala una nueva aplicación en producción, la cache de las plantillas está vacía. Para que una página se guarde en la cache, se debe esperar a que algún usuario visite esa página. En algunas aplicaciones críticas, no es admisible el tiempo de procesamiento de esa primera petición, por lo que se debe disponer de la versión de la página en la cache desde la primera petición.

La solución consiste en navegar de forma automática por las páginas de la aplicación en un entorno intermedio que se suele llamar "staging" y que dispone de una configuración similar a la del entorno de producción. De esta forma, se genera la cache completa de páginas y plantillas. Después, se puede transferir la aplicación a producción junto con la cache llena.

Para navegar de forma automática por todas las páginas de la aplicación, una opción consiste en utilizar un script de consola que navegue por una serie de URL mediante un navegador de texto (como por ejemplo "curl"). Otra opción mejor y más rápida consiste en utilizar un script de Symfony que utilice el objeto sfBrowser mostrado en el capítulo 15. Se trata de un navegador interno escrito en PHP y que utiliza el objeto sfTestBrowser para las pruebas funcionales. A partir de una URL externa, devuelve una respuesta, teniendo en cuenta la cache de las plantillas, como haría cualquier otro navegador. Como sólo se inicializa Symfony una vez y no pasa por la capa HTTP, este método es mucho más rápido.

El listado 18-15 muestra un script que genera la cache de plantillas en un entorno de tipo "stagging". Se puede ejecutar mediante php batch/generar_cache.php.

Listado 18-15 - Generando la cache de las plantillas, en batch/generar_cache.php

<?php

require_once(dirname(__FILE__).'/../config/ProjectConfiguration.class.php');
$configuration = ProjectConfiguration::getApplicationConfiguration('frontend', 'staging', false);
sfContext::createInstance($configuration);

// Array de URL a navegar
$uris = array(
  '/foo/index',
  '/foo/bar/id/1',
  '/foo/bar/id/2',
  ...
);

$b = new sfBrowser();
foreach ($uris as $uri)
{
  $b->get($uri);
}

18.4.3. Guardando los datos de la cache en una base de datos

Por defecto, los datos de la cache de plantillas se guardan en el sistema de archivos: los trozos de HTML y los objetos serializados de la respuesta se guardan en el directorio cache/ del proyecto. Symfony también incluye un método de almacenamiento alternativo para la cache: la base de datos SQLite. Este tipo de base de datos consiste en un archivo simple que PHP es capaz de reconocer como base de datos para buscar información en el archivo de forma muy eficiente.

Para indicar a Symfony que debería utilizar el almacenamiento de SQLite en vez del sistema de archivos, se debe modificar la opción view_cache del archivo de configuración factories.yml:

view_cache:
  class: sfSQLiteCache
  param:
    database: %SF_TEMPLATE_CACHE_DIR%/cache.db

La ventaja de utilizar el almacenamiento en SQLite es que la cache de las plantillas es mucho más fácil de leer y de escribir cuando el número de elementos de la cache es muy grande. Si la aplicación hace un uso intensivo de la cache, los archivos almacenados en la cache acaban en una estructura de directorios muy profunda, por lo que utilizar el almacenamiento de SQLite mejora el rendimiento de la aplicación.

Además, borrar una cache almacenada en el sistema de archivos requiere eliminar muchos archivos, por lo que es una operación que puede durar algunos segundos, durante los cuales la aplicación no está disponible. Si se utiliza el almacenamiento de SQLite, el proceso de borrado de la cache consiste en borrar un solo archivo, precisamente el archivo que se utiliza como base de datos SQLite. Independientemente del número de archivos en la cache, el borrado es instantáneo.

18.4.4. Saltándose Symfony

La mejor forma de mejorar el rendimiento de Symfony consiste en saltárselo por completo, aunque sea de forma parcial. Algunas páginas no cambian con cada petición, por lo que no es necesario procesarlas cada vez mediante el framework. Aunque la cache de las plantillas acelera el procesamiento de las páginas, todavía debe hacer uso de Symfony.

El capítulo 12 muestra algunos trucos con los que se puede evitar Symfony por completo para algunas páginas. El primer truco consiste en utilizar las cabeceras HTTP 1.1 para solicitar a los proxies y a los navegadores de los usuarios que guarden la página en sus propias caches y que no la soliciten la próxima vez que el usuario quiera acceder a la página. El segundo truco es la cache super rápida (que se puede automatizar mediante el plugin sfSuperCachePlugin) que consiste en guardar una copia de la respuesta en el directorio web/ y la modificación de las reglas de reescritura de URL para que Apache busque en primer lugar la versión de la página en la cache antes de enviar la petición a Symfony.

Estos dos métodos son muy efectivos, aunque solamente se puedan aplicar a las páginas estáticas, ya que evita que estas páginas sean procesadas por Symfony, permitiendo a los servidores dedicarse al procesamiento de las peticiones complejas.

18.4.5. Guardando en la cache el resultado de una función

Si una función no depende del contexto de ejecución ni de variables aleatorias, al ejecutar 2 veces la misma función con los mismos parámetros, el resultado será el mismo. De esta forma, se podría evitar la segunda ejecución de la función si se ha almacenado el resultado de la primera ejecución. Esto es exactamente lo que permite hacer la clase sfFunctionCache. Esta clase dispone de un método llamado call(), al que se le pasa un elemento de PHP que se pueda ejecutar y un array de parámetros con los argumentos. Cuando se ejecuta, este método crea una huella digital mediante el método MD5 de todos los argumentos que se le han pasado y busca en la cache una clave que coincida con esta huella digital. Si se encuentra la clave, se devuelve el resultado almacenado en la cache. Si no se encuentra, sfFunctionCache ejecuta la función, almacena su respuesta en la cache y devuelve esta respuesta. Por tanto, la segunda ejecución del código del listado 18-16 es más rápida que la primera.

Listado 18-16 - Guardando el resultado de una función en la cache

$cache = new sfFileCache(array('cache_dir' => sfConfig::get('sf_cache_dir').'/function'));
$fc = new sfFunctionCache($cache);
$resultado1 = $fc->call('cos', array(M_PI)
$resultado2 = $fc->call('preg_replace', array('/\s\s+/', ' ', $input));

El constructor de sfFunctionCache espera como argumento un objeto de tipo cache. El primer argumento del método call() debe ser cualquier elemento PHP que se pueda ejecutar, por lo que se puede indicar el nombre de una función, un array con el nombre de una clase y un método estático o un array con el nombre de un objeto y el de un método público. Respecto al otro argumento que se puede pasar al método call(), se trata de un array con todos los argumentos que se pasan al método o función.

Advertencia Si utilizas una cache basada en archivos como en el ejemplo anterior, es mejor indicar como directorio de la cache un directorio que se encuentre dentro de cache/, ya que de esta forma se borrará automáticamente cuando se ejecute la tarea cache:clear. Si guardas la cache de las funciones en otro sitio, no se borra automáticamente cuando borras la cache mediante la línea de comandos.

18.4.6. Guardando datos en la cache del servidor

Los aceleradores de PHP proporcionan unas funciones especiales para almacenar datos en la memoria, de forma que se puedan reutilizar entre diferentes peticiones. El problema es que cada acelerador utiliza su propia sintaxis y cada uno realiza esta tarea de una forma diferente. La cache de Symfony abstrae todas las diferencias en el funcionamiento de los diferentes aceleradores. Su sintaxis se muestra en el listado 18-17.

Listado 18-17 - Utilizando un acelerador de PHP para guardar datos en la cache

$cache = new sfAPCCache();

// Guardando datos en la cache
$cache->set($nombre, $valor, $tiempoDeVida);

// Accediendo a los datos
$valor = $cache->get($nombre);

// Comprobando si un valor existe en la cache
$existe_valor = $cache->has($nombre);

// Borrar la cache
$cache->clear();

El método set() devuelve un valor false si no funciona la cache. El valor guardado en la cache puede ser de cualquier tipo (cadena, array, objeto); la clase sfProcessCache se encarga de la serialización automática. El método get() devuelve un valor null si la variable solicitada no existe en la cache.

Truco Si se quiere profundizar en el uso de la cache en memoria, se debería utilizar la clase sfMemcacheCache. Esta clase dispone de la misma interfaz que el resto de las clases de cache y permite reducir la carga en la base de datos para las aplicaciones en las que se aplica el balanceo de carga.