Arreglar errores 502 con nginx y PHP-FPM

Hace algún tiempo, un servidor comenzó a generar errores 502: Bad Gateway de manera aleatoria y no reproducible. O sea, en las condiciones ideales para convertirse en un quebradero de cabeza.

Este servidor funciona con nginx + PHP-FPM, pero luego de descartar las razones más usuales de problemas que se pueden encontrar en este tipo de configuraciones, los errores seguían ocurriendo. Lo peor es que el servicio no solamente se interrumpía, sino que no volvía a funcionar hasta reiniciar manualmente nginx.

Finalmente pude dar con la causa: al parecer, habían ciertos procesos de PHP que nunca terminaban adecuadamente. Si bien el valor de max_execution_time estaba correctamente configurado, en ciertas condiciones hay procesos que no terminan de ejecutarse correctamente. La solución, de parte de PHP:

; detener la ejecución que no obedecen a max_execution_timerequest_terminate_timeout = 30s

; loguear peticiones con tiempo de ejecución excesivo
slowlog = /var/log/php5-fpm/$pool.log.slow
request_slowlog_timeout = 30s

Luego, por parte de nginx también es recomendable establecer límites a la conexión que hace el servidor web con el servidor FastCGI (en este caso, PHP-FPM)

http {
    ...
    # fix para "upstream sent too big header"
    fastcgi_buffers 8 16k;
    fastcgi_buffer_size 32k;
    # timeout para conexiones entre nginx y fastcgi
    fastcgi_connect_timeout 30s;
    fastcgi_send_timeout 30s;
    fastcgi_read_timeout 30s;
    ...
}

Algunas notas:

  • Según la documentación de nginx, el valor fasctgi_connect_timeout no debiese exceder los 75 segundos
  • Para fastcgi_send_timeout y fastcgi_read_timeout, el tiempo máximo se mide entre dos operaciones de escritura/lectura simultáneas, respectivamente, y no considera el tiempo de transmisión de datos. Es decir, si tras el lapso de tiempo indicado el servidor FastCGI no recibe/envía ningún dato, se corta la conexión.

Un-breaking lighttpd’s broken mod_access

A client let us know that the server where her company’s site was hosted had an unusually high load.

After checking the access log for the web server, it was clear that the cause was repeated access attempts at a single URL, which was not essential to the site. So I though this should be easy, I’ll just block the request in the web server config. Unfortunately, they were using a very outdated version of lighttpd, so it wasn’t that easy.

It seems that older lighttpd builds had several bugs with mod_access, but the worst in our case was that instead of blocking the request and send a 403 Forbidden, it passed the request on to the 404 error handler, and this loaded the entire app enviroment.

So here’s what I did. The lighttpd config looked like this:

$HTTP["url"] =~ "^/foobar.php" {
    url.access-deny = ("")
    server.error-handler-404 = "/403.php"
}

… so request to foobar.php would be handled by 403.php. And then, 403.php:

<?php header('HTTP/1.0 403 Forbidden'); ?>
<h1>Forbidden</h1>

Very silly, but effective. Just because status codes matter.

Optimización AJAX 4: Caché permanente

En la parte final (por ahora) de esta serie sobre Optimización de respuestas AJAX vamos a revisar cómo utilizar técnicas de caché permanente, o lo que se debería denominar más correctamente caché-que-se-controla-desde-el-servidor — lo que complementa a la parte anterior sobre “caché volátil” o caché-que-se-controla-desde-el-cliente.

Continue reading “Optimización AJAX 4: Caché permanente”

Horizontally scaling PHP applications

One of the most common worries of the enterprise IT world about WordPress and other Open Source apps it’s how you can scale it — which it’s kind of ironic when their enterprise-y web services response times are usually measured in the scale of tens of seconds…

DigitalOcean has published a high-level practical-overview on horizontally scaling PHP apps that’s a good starting point and I guess it could also apply to other kinds of apps as well.

Optimización AJAX 2: compresión GZIP

Habilitar la compresión de la respuesta puede reducir la cantidad de datos enviados alrededor de un 70%. La mayoría de los navegadores soporta recibir contenidos, e informan de esta capacidad al servidor al realizar una petición.

Es posible agregar la indicación de comprimir la respuesta del servidor en su configuración de acuerdo al mime-type de la respuesta: text/html, text/plain, application/json, application/xml (y en general, es conveniente activarlo para cualquier lenguaje basado en texto, como text/html, text/css, application/javascript, application/json).

Habilitar compresión en Apache y nginx

En Apache puedes habilitar la compresión gzip añadiendo las siguientes líneas a la configuración del servidor o el archivo .htaccess relevante:

<ifModule mod_gzip.c>
mod_gzip_on Yes
mod_gzip_dechunk Yes
mod_gzip_item_include file .(html?|txt|css|js|php|pl)$
mod_gzip_item_include handler ^cgi-script$
mod_gzip_item_include mime ^text/.*
mod_gzip_item_include mime ^application/x-javascript.*
mod_gzip_item_exclude mime ^image/.*
mod_gzip_item_exclude rspheader ^Content-Encoding:.*gzip.*
</ifModule>

En el caso de nginx puedes activarlo dentro del bloque http de la configuración de modo que quede disponible para todos los virtual hosts configurados o bien por bloques server o incluso location.

En Ubuntu estos parámetros suelen venir en la configuración por defecto pero comentados.

gzip on;
# deshabilitar para IE 4 a IE6
gzip_disable "msie6";

gzip_vary on;
# des/habilitar compresión para respuestas de las nginx hace de proxy
gzip_proxied any;
# nivel de compresión, de 1 a 9
gzip_comp_level 4;
gzip_buffers 16 8k;
gzip_http_version 1.1;
# los tipos de respuesta que se enviarán comprimidos. siempre incluye text/html
gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;

Habilitar compresión desde PHP

También es posible activar la compresión de la respuesta en la misma aplicación; por ejemplo en PHP esto se puede lograr de forma muy sencilla con ob_gzhandler:

/**
 * Si el navegador no soporta Gzip,
 * la condición retorna falso y se abre un buffer normal
 */
if ( ! ob_start('ob_gzhandler') ) ob_start();
echo $out;
ob_end_flush();
exit;

Cómo comprobar si la respuesta fue comprimida

Puedes comprobar si la respuesta se está enviando efectivamente comprimida con las herramientas de desarrollo de Chrome (debes habilitar la opción use large request rows, junto a la opción para mostra los filtros de contenido).

2b90b5d301En la columna Size, puedes ver dos cifras: la primera corresponde al tamaño del cuerpo de la respuesta, mientras que la segunda a los datos transferidos. Si la cantidad de datos transferidos es menor al tamaño de la respuesta, significa que la compresión está funcionando.

Optimización AJAX 1: elección del método HTTP

Si bien las interacciones en AJAX nos permiten obtener información de forma dinámica desde el servidor de un sitio web o aplicación, existen varias formas de aumentar la velocidad de estas respuestas para mejorar la experiencia de nuestros usuarios al cargar información bajo demanda, obtener datos de búsquedas u otras interacciones.

Más allá de la parafernalia tecnológica que supone la implementación de una respuesta AJAX (por supuesto, reducida al mínimo con el uso de un framework adecuado), ésta sigue siendo una respuesta HTTP, por lo que se pueden aplicar los mismos principios para mejorar su performance.

En éste y los siguientes posts intentaré revisar la teoría y práctica de las técnicas fundamentales para mejorar la performance de respuestas AJAX y la experiencia de tus usuarios.

Continue reading “Optimización AJAX 1: elección del método HTTP”

Cómo agregar un nuevo panel a WordPress Debug Bar

Una de las ventajas del plugin WordPress Debug Bar es que puedes agregar nuevos paneles según tus propias necesidades; por ejemplo, para mostrar datos de respuestas desde APIs externas o conexiones con web services u otras funcionalidades que hayas implementado de forma particualr.

// el plugin utiliza el filtro 'debug_bar_panels'
// que puedes usar para agregar nuevos paneles
add_filter( 'debug_bar_panels', 'my_custom_panel_init' );

/**
 * Inicializar tu panel personalizado
 * @param array $panels Instancias de Paneles
 * @return array
 */
function my_custom_panel_init( $panels ) {
    // Debes extender la clase Debug_Bar_Panel que forma
    // parte del plugin
    class Debug_Bar_Custom_Panel extends Debug_Bar_Panel{
        public function init(){
            // Como mínimo, debes definir el título de tu panel
            // personalizado. Se usará como título de la pestaña
            $this->title( 'Panel Personalizado' );
        }
        public function render(){
            // Construye e imprime el contenido de la pestaña
        }
    }
    // Agrega tu pestaña a la barra de depuración
    $panels[] = new Debug_Bar_Custom_Panel();
    return $panels;
}

Por supuesto, este es un ejemplo simplificado, pero es el punto de partida para tu implementación. Con un par de líneas más ya puedes tener tu propio panel:

Agregar un panel de depuración personalizado a WordPress Debug Bar

Para un ejemplo más completo, puedes revisar el código de alguno de los plugins que extienden la funcionalidad de la barra de depuración, como el repositorio de Debug-Bar-Shortcodes.

Adding a new panel to the WordPress Debug Bar plugin

You can extend the WordPress Debug Bar plugin adding new custom panels to fit your needs; for instance, showing responses from external APIs or webservices, or for other custom features.

// the 'debug_bar_panels' filter it's used by the plugin;
// hook into it to add your custom panel
add_filter( 'debug_bar_panels', 'my_custom_panel_init' );

/**
 * Initialize my custom debug panel
 * @param array $panels Debug bar panels objects
 * @return array Debug bar panels with your custom panels
 */
function my_custom_panel_init( $panels ) {
    // you'll be extending the Debug_Bar_Panel class provided
    // by the plugin
    class Debug_Bar_Custom_Panel extends Debug_Bar_Panel{
        public function init(){
            // you should set at least the title of your custom
            // panel. it'll be used as the tab title
            $this-&gt;title( 'Custom Panel' );
        }
        public function render(){
            // build and echo your relevant output
        }
    }
    // add your custom panel
    $panels[] = new Debug_Bar_Custom_Panel();
    return $panels;
}

That’s an extremely simple example. You can check a more complex and complete one by viewing the source of one of the Debug Bar extender plugins, such as the Debug-Bar-Shortcodes GitHub repo.

Debugging memory usage in PHP apps

As your app gets increasingly more complex, you might run into Memory exhausted errors, and even though you can always increase the allowed memory usage — either by tweaking php.ini or locally with ini_set() — it should be a better option to find out what’s using so much memory in the first place.

Continue reading “Debugging memory usage in PHP apps”