HowTo: Drupal 101
Recientemente estuve trabajando en la migración de algunas aplicaciones construidas en el popular sistema de manejo de contenidos Drupal a AWS (Amazon Web Services). La experiencia puede llegar a ser increíblemente frustrante debido a varios factores, siendo el comportamiento predeterminado de Drupal el más relevante.
No siendo un experto en Drupal, mi impresión es que la documentación esté bastante dispersa, y no existen guías consistentes de buenas prácticas. Los libros disponibles están enfocados principalmente en usuarios finales y no en desarrolladores o sysadmins, lo cual hace que el proceso de configuración sea largo, tedioso y a la larga pueda resultar frustrante.
En este artículo pretendo dar una idea de la configuración básica de una instancia t1.micro
de
Amazon EC2 para obtener un rendimiento aceptable. Estoy seguro que profundizando en algunos aspectos puede
lograrse mucho mejor desempeño, pero mi interés es por lo pronto hacer una receta fácil de seguir.
Se utilizará, además de la instancia EC2, una instancia small
de Amazon RDS y un cluster de dos nodos de Amazon Elasti-Cache, que no es otra cosa que un cluster de Memcached. Con el fin de entender el comportamiento de la aplicación en el servidor se utilizará un agente de monitoreo New Relic (servidor y PHP).
Existen distintas estrategias para realizar esta configuración, mis objetivos son los siguientes :
Correr cada aplicación en un entorno aislado de otras, y mantener la instancia libre de archivos cargados mediante el manejador de contenidos. Esto a razón de que si la instancia se viese comprometida por alguna vulnerabilidad, sea fácil reconstruir el sitio sin problema.
El lanzamiento de cada instancia debe estar automatizado, y debe ser posible replicarlo fácilmente para crear instancias idénticas que puedan existir simultáneamente detrás de un balanceador de carga.
Debe tener un desempeño (performance) decente en una instancia t1.micro (594MB RAM, 1.0-1.2 GHz 2007 Opteron or 2007 Xeon) con el fin de mantener los costos bajos. Mi intención es la de escalar horizontalmente (crear múltiples instancias de este tipo), en lugar de verticalmente (agregar memoria y capacidad de procesamiento).
Para estos objetivos usaré Elastic Beanstalk, un servicio tipo PaaS de AWS, que busca competir contra otros servicios como Heroku y mi personal favorito Google App Engine.
Elastic Beanstalk me permite configurar reglas en un balanceador de carga para aprovisionar automáticamente instancias EC2 en las que corre la aplicación Drupal y adicionalmente permite utilizar el sistema de control de contenidos Git para realizar los deployments, lo cual facilita el proceso de automatización.
De acuerdo con la documentación de AWS, las instancias t1.micro son idóneas para aplicaciones que tienen un perfil de uso de CPU como el siguiente:
Muchas aplicaciones web, encajan perfectamente en este patrón. Una linea base de procesamiento dada por scripts PHP que entregan el contenido de una u otra página, interrumpida ocasionalmente por periodos de procesamiento más intensivo, edición en la administración, ensamblaje de alguna información, consulta de servicios remotos, etc. y su almacenamiento en cache.
Linea Base
Normalmente es difícil definir que tan bien o mal se desempeña una aplicación en cualquier entorno sin tener
un punto de comparación, un "Blanco" como se suele llamar en diseño experimental. Mi "blanco" consiste en una
sencilla aplicación PHP que imprime los primeros 15.000 números primos. Elegí 15.000 porque el tiempo de respuesta está alrededor de los 200ms por cada solicitud. Las solicitudes concurrentes harán que el tiempo comience a aumentar proporcionalmente, dado que la instancia cuenta con un único procesador. Está prueba no es muy útil estimando uso de memoria RAM, pero me da una idea aproximada de la capacidad de la instancia para realizar operaciones con la configuración predeterminada de Apache
y mod_php
.
En la práctica, un CMS con una estrategia de caché bien configurada, debería ser menos demandante que mi prueba con el cálculo de los 15.000 números primos, pero seguramente consumirá mucha más memoria. Según la gráfica, la instancia comienza a ponerse inestable cerca de los 4 minutos, el número de hits/seg
comienza a oscilar, si siguiéramos esta gráfica en el tiempo, la instancia posiblemente comenzaría a responder con errores, o simplemente respondiendo más allá de mi límite de tiempo por solicitud de esta prueba o timeout que en este caso está configurado en 4s. Un estimado a ojo (sin estimar el uso de memoria de forma muy precisa), me sugiere que la instancia debería soportar al menos unos 10-12 usuarios concurrentes (algo cercano a unos 6 hits/seg
) en este caso.
Hay otros factores importantes, por ejemplo, en número de archivos CSS
, JS
e imágenes que se deben servir una vez carga el index.php
. Dado que Drupal concatena y minifica los CSS
y JS
, voy a ignorar el impacto que puedan tener sobre el hit rate, dado que normalmente un web server es muy eficiente sirviendo archivos estáticos. Las imágenes son sin embargo otra historia muy diferente, sobre todo porque pueden ser muchas, y en ocasiones tener un tamaño considerable. Dado que uno de mis objetivos es mantener las imágenes y cualquier otro tipo de archivos cargados mediante el CMS fuera de la instancia, voy a ignorar también el impacto de este factor. Si la configuración que vamos a hacer asume que las imágenes van a ser servidas desde la misma máquina, definitivamente es un aspecto relevante. Una forma sencilla de minimizar el impacto (adicional a enviar los encabezados de cache apropiados), es utilizar un servicio de CDN, como por ejemplo CloudFlare que hace la tarea por nosotros.
Drupal Round I
El primer Round de Drupal consiste en cargar a la instancia una instalación predeterminada del CMS, cargar unos 1000 artículos aleatorios (Muy sencillo usando el módulo Devel) y correr la prueba nuevamente con los mismos parámetros. En mi configuración, la base de datos MySQL
, se encuentra fuera de la instancia, alojada en el servicio administrado de AWS llamado RDS.
Aún cuando suena sencillo, usar la interfaz de administración es dolorosamente lento. Tan solo generar los artículos sin tener ningún tipo de caché activo es imposible sin recibir varias respuestas incompletas y los registros de Watchdog
no muestran mayor cosa y tampoco lo hacen los logs del servidor. La única forma de traer todo de vuelta es reiniciando el servicio HTTP.
Agregar CSS y JavaScript
Una de las configuraciones más simples que podemos hacer para mejorar el desempeño de la instancia, corresponde a la optimización de archivos JS
y CSS
. Con simplemente chequear dos casillas, reemplazamos mas de una decena de sentencias @import
por un par de <link rel="stylesheet">
y varios <script>
quedan agregados en solo un par. Reemplazar las sentencias @import
no solamente mejora el desempeño, también elimina problemas de incompatibilidad de estilos con IE cuando utilizamos media queries y librerías como Respond, las cuales necesitan cargar las hojas de estilo dentro del DOM, para reemplazar los media queries.
Base de datos
El punto débil de casi cualquier aplicación web a la hora de escalar es en el 99% de los casos la base de datos, bien sea porque las consultas no están correctamente optimizadas o porque son demasiadas. Drupal normalmente hará una gran cantidad de consultas a la base de datos para presentar el contenido (cualquier cosa entre 50 y 220 consultas por solicitud es frecuente) y en registrar información mediante el Watchdog
. Por esta razón, las optimizaciones que más impacto van a tener, son todas aquellas relacionadas con la disminución de consultas a la base de datos.
El módulo Devel, permite activar la visualización de todas las consultas a la base de datos que se realizan en cada solicitud.
Cambiar dblog por syslog
A mi juicio, uno de los aspectos más problemáticos de una instalación predeterminada de Drupal es el registro (logging) de errores o alertas a la base de datos con Watchdog
. Esto ocurre cuando se tiene activo el módulo dblog. Usar una base de datos como un registro de errores de una aplicación web de alto tráfico, no es una estrategia muy inteligente, mucho menos escalable. Supongo que la razón principal por la que el equipo core de Drupal la mantiene como predeterminada, es la de facilitar el acceso a los usuarios no expertos a los errores de la aplicación, y asumen que cualquier usuario medianamente experimentado la reemplazará inmediatamente con syslog.
Syslog reemplaza los logs en la base de datos por registros de texto en /var/log/messages
o cualquier otro archivo que decidamos configurar en la configuración de syslog
del sistema operativo.
Desactivar verificación de actualizaciones
Personalmente prefiero desactivar la verificación de actualizaciones. Al ejecutarse esta verificación, Drupal revisa si existen nuevas versiones para cada uno de los módulos instalados, lo cual normalmente toma tiempo. De hecho es probable que esta verificación se interrumpa en ciertas condiciones y sea intentada varias veces, causando que algunos procesos HTTP se atoren y causen problemas.
De cualquier forma, las actualizaciones normalmente deben probarse sobre una instancia de pasarela, incluirse en un repositorio de control de versiones y posteriormente se pasar a producción. Así es que la verificación de actualizaciones en un servidor de producción no es otra cosa que un gasto de ciclos de CPU y tráfico de red. Afortunadamente en Drupal 7 solo es necesario deshabilitar el módulo verificación de actualizaciones.
En este punto podemos activar el cache para usuarios anónimos, _bloques__ en la configuración de desempeño de Drupal y establecer algun valor mínimo y máximo de permanencia en el cache para correr una primera prueba de desempeño sobre la instancia:
Realmente es un poco desastroso. El tiempo mínimo de respuesta está por encima de 1s, y al llegar a una taza de solicitudes de 1 hit/seg
los tiempos de respuesta se disparan por encima de 4s, en este caso un timeout simbolizado por la linea naranja. Del lado del servidor, las cosas se ven mas o menos así :
Las franjas blancas simplemente indican que no se capturaron trazas durante unos lapsos de tiempo, pero en las trazas capturadas, se puede identificar una gran porción grís que representa algo llamado IO Wait, que no es otra cosa que un cuello de botella causado por la espera a la respuesta de la base de datos. La espera se vuelve mas larga conforme más clientes acceden de forma concurrente, finalmente la memoria se llena y todo comienza lentamente a colapsar.
Drupal Round II
Instintivamente, siempre que tenemos una aplicación que consulta datos intensivamente en una base de datos, pensamos en Memcached. Memcached es básicamente un sistema de almacenamiento en memoria de alto rendimiento que permite guardar valores identificados con una llave. Dentro de AWS, se le llama Elasti-cache.
Para Drupal, existe desde luego un módulo para utilizar este cache de alto rendimiento para guardar muchas consultas repetitivas, en lugar de sacarlas cada vez de la base de datos. De esta manera, no solamente las consultas repetitivas y las sesiones, sino que también varios tipos de cache que Drupal normalmente guarda en la base de datos, se van a almacenar alli.
Una configuración sencilla incluye los siguientes cambios en el archivo settings.php
:
<?PHP
$conf['cache_backends'][] = 'sites/all/modules/contrib/memcache/memcache.inc';
// The 'cache_form' bin must be assigned no non-volatile storage.
$conf['cache_class_cache_form'] = 'DrupalDatabaseCache';
$conf['cache_default_class'] = 'MemCacheDrupal';
$conf['memcache_key_prefix'] = 'someprefix_';
$conf['memcache_servers'] = array(
'somecachenode.0002.use1.cache.amazonaws.com:11211' => 'default',
'somecachenode.0001.use1.cache.amazonaws.com:11211' => 'session'
);
// Con varios nodos, es posible asignar cada tipo de cache a un nodo distinto.
// El número de nodos normalmente depende del tamaño de los objetos en el cache,
// el número de objetos y la cantidad de tráfico atendido.
// En este caso simplemente utilizamos dos nodos
$conf['memcache_bins'] = array(
'cache' => 'default',
'cache_filter' => 'default',
'cache_menu' => 'default',
'cache_page' => 'session',
'session' => 'sesssion',
'users' => 'session'
);
Después de realizar este cambio, y asegurarnos que la configuración es exitosa (no aparecen errores en el syslog ni en la pantalla de reportes de Drupal), vamos a notar inmediatamente un cambio considerable en la consola de administración: de repente todo funciona mucho más rápido. Una prueba de carga en este punto se comporta de una manera totalmente diferente:
Nada mal, con activar y configurar el módulo de Memcache contra el cluster de Elasti-Cache, ganamos 10x la capacidad que teníamos en la prueba anterior. En lugar de comenzar a tener problemas al tener 1 hit/seg
, nuestros problemas están alrededor de los 10 hits/seg
, unos 20-25 usuarios concurrentes. Pero mejor que eso, son los tiempos de respuesta, mas del 95% de las solicitudes antes de ese punto, se responden en menos de 500 ms, en lugar de 1, 2 o 3 segundos. Si estuviésemos midiendo un indice como el Apdex, esto sería sin duda una mejora considerable.
El comportamiento de la memoria y la CPU en este último caso, es muy parecido al caso anterior, solo que la curva no crece repentinamente, es mucho más gradual, dado que la maquina no está esperando tanto tiempo por una respuesta de la base de datos.
Temas Pendientes
Algunos temas que me gustaría explorar en el futuro son los siguientes:
Cron de Drupal
El cron
es definitivamente un aspecto importante del funcionamiento de Drupal, dado que es el responsable de la limpieza del cache y varias otras actividades. Si el cron
tarda demasiado, otros procesos comenzarán a encolarse. En general parece una mejor idea configurar un mecanismo externo que corra el cron
cada 10 o 15 minutos, y evitar que se active en momentos de alto tráfico. Sin embargo personalmente me quedan bastantes dudas respecto a las tareas que se ejecutan dentro del cron
. Exploré brevemente el módulo Cron Debug, el cual permite correr cada tarea por separado, y ver exactamente cuanto tiempo tarda cada tarea, pero resta revisar cada una de estas tareas a fondo.
Tiempo de permanencia en cache
El tiempo de permanencia en cache es otro aspecto importante a explorar y depende de la dinámica particular de cada sitio. Un sitio con gran cantidad de contenidos y visitas dispersas entre muchos contenidos, puede fácilmente entrar en un bucle infinito de creación y eliminación de archivos de cache que rápidamente consumen los recursos de la instancia. Sitios con visitas mas localizadas en "Hot spots" de información seguramente son menos susceptibles al problema.
XHPROF
El [Módulo Devel] permite activar la extensión XHPROF para hacer profiling de Drupal. En algunos aspectos New Relic es una herramienta muy útil, pero me gustaría explorar un poco más las optimizaciones realizadas usando XHPROF.
Nginx
Comparar esta configuración con una versión corriendo sobre el Nginx
+ PHP-fpm
, en lugar de Apache
+ mod_php
.
Intermitencia inexplicable
En algunas raras ocasiones, incluso con los caches implementados, la instancia simplemente deja de entregar contenido. Revisando el código, el problema parece estar en algún punto de la carga del bootstrap.inc
, pero es aún un misterio dado que el error ocurre al azar.
blog comments powered by Disqus