Desde hace unos días Linuxito está hospedado en un VPS de RamNode y dado que estoy muy familiarizado con Apache, instalé un clásico esquema LAMP (Linux Apache PHP MySQL). Pero compré un VPS con poca memoria RAM (256 MB, por cuestiones de costo), y el servidor está muy al límite, consumiendo incluso algo de swap. Por ello tomé la decisión de migrar a Nginx con PHP en modo CGI, un esquema mucho más eficiente en lo que a consumo de memoria respecta.

Nginx se vende como un servidor HTTP de alto rendimiento, estable y con muy bajo consumo de recursos (algo que me han resaltado varios colegas). Su alto rendimiento se debe a que, a diferencia de Apache que utiliza threads (o procesos, depende cómo se lo configure), Nginx posee una arquitectura asincrónica mucho más escalable y basada en eventos, lo que permite utilizar pequeñas cantidades de memoria.

Por otro lado, el hecho de correr PHP como un servicio separado (en lugar de un módulo de Apache) utilizando PHP-FPM (FastCGI Process Manager) mejora mucho la eficiencia de memoria. Cuando se incluye a PHP como módulo de Apache, cada proceso worker (trabajado en modo MPM prefork) es una copia del espacio de memoria (fork) del proceso maestro. Esto implica que en cada worker se replica el espacio de memoria necesario para correr PHP, aunque no sea necesario (por ejemplo si se están sirviendo recursos no ejecutables como imágenes, archivos HTML, u otros). El resultado es que el consumo de memoria de PHP se replica en cada worker. En definitiva el footprint de memoria de PHP es mucho más grande.

Al utilizar PHP-FPM, el motor intérprete de PHP corre como un servicio aparte, el cual puede recibir peticiones a través de un socket TCP/IP o UNIX tradicional. De esta forma se tienen dos servicios: Nginx para manejar el protocolo HTTP y PHP-FPM para interpretar código PHP. Lo cual resulta más eficiente, ya que se invoca a PHP sólo cuando es necesario.

Este artículo explica detalladamente como instalar y configurar Nginx con PHP-FPM. Específicamente instalando php5-fpm desde los repositorios, pero compilando Nginx desde sus fuentes. El sistema operativo es Debian 7.



Instalación y configuración de PHP5 FPM

Instalar PHP-FPM desde los repositorios de Debian. Opcionalmente instalar el driver para MySQL (si se van a utilizar bases de datos MySQL):

# apt-get install php5-fpm php5-mysql

Editar el archivo de configuración de php5-fpm:

# nano /etc/php5/fpm/php-fpm.conf

Especificar el socket sobre el que el servidor FPM escuchará peticiones. Puede ser un socket TCP/IP o un socket UNIX. En este caso se utiliza un socket UNIX.

[...]

listen = /var/run/php5-fpm.sock

[...]

Luego, configurar el pool por defecto. La configuración varía de acuerdo a las necesidades del servidor. En este caso se utilizan como mínimo 3, y como máximo 10, procesos servidores spare (listos para procesar solicitudes); y un máximo de 75, trabajando en modo dinámico (los procesos hijos se crean/matan a medida que son necesarios).

Además se debe especificar el usuario y grupo con los que correrán los procesos hijos (utilizar el usuario "www-data" por defecto de sistemas Debian y derivados).

# nano /etc/php5/fpm/pool.d/www.conf

Los parámetros de configuración principales quedan de la siguiente forma:


[...]

user = www-data
group = www-data

[...]

listen.owner = www-data
listen.group = www-data
listen.mode = 0660

[...]

pm = dynamic
pm.max_children = 75
pm.start_servers = 3
pm.min_spare_servers = 3
pm.max_spare_servers = 10

[...]

Para terminar, reiniciar el servicio:

# service php5-fpm restart

Instalación y configuración de Nginx

Instalar las dependencias necesarias para compilar Nginx:

# apt-get install gcc make libpcre3-dev zlib1g-dev git

Descargar los fuentes de Nginx (1.8.0):

# wget http://nginx.org/download/nginx-1.8.0.tar.gz

Descomprimir el paquete:

# tar xf nginx-1.8.0.tar.gz
# cd nginx-1.8.0

Configurar, compilar e instalar Nginx 1.8.0:

# ./configure --with-http_ssl_module
# make
# make install

Descargar e instalar un script de inicio de servicio System V para Nginx:

# git clone https://github.com/Fleshgrinder/nginx-sysvinit-script.git
# cd nginx-sysvinit-script
# make

Crear un enlace simbólico al binario nginx dentro del directorio /sbin:

# ln -s /usr/local/nginx/sbin/nginx /sbin/nginx

Iniciar el servicio:

# service nginx start

Se observa que ha iniciado el proceso maestro y un proceso worker en modo spare (listo para empezar a procesar peticiones). Además, se encuentra abierto el puerto 80, ligado al proceso maestro:

Es posible comprobar el funcionamiento conectándose al puerto 80 con netcat:

Sino directamente utilizando wget:

# less index.html

Aunque lo más simple es acceder desde un navegador:

Creo que eso es suficiente para asegurarme de que funciona.

Ahora que Nginx está funcionando, es posible crear un directorio base de trabajo diferente al que utiliza la configuración por defecto:

# mkdir /var/www
# chown www-data:www-data /var/www/

Crear un archivo index.html para verificar el correcto funcionamiento de Nginx:

# nano /var/www/index.html
<html>
<head
<title>Hola Mundo</title>
</head>
<body>
<h2>Hola Mundo</h2>
<p><i>Hola Mundo!!!</i></p>
</body>
</html>

También crear un archivo index.php para verificar el funcionamiento de Nginx junto con PHP-FPM:

# nano /var/www/index.php
<?php

echo "Hola Mundo!!!\n";

?>

Verificar la correctitud del script PHP:

root@devuan:~# php /var/www/index.php 
Hola Mundo!!!

Cambiar el dueño y grupo de estos nuevos archivos:

# chown www-data:www-data /var/www/index.*

Crear un archivo personalizado para responder ante errores 404:

# nano /var/www/404.html

Responder con el mensaje y formato que se desee (por ejemplo un "NOT FOUND" a secas):

NOT FOUND

En el directorio de instalación de Nginx, dentro del directorio html, crear un archivo info.php para mostrar la salida clásica de phpinfo();:

# nano /usr/local/nginx/html/info.php
<?php phpinfo();

A continuación, configurar el servidor Nginx (la parte más importante):

# nano /usr/local/nginx/conf/nginx.conf

La configuración de Nginx no se parece en nada a la configuración de Apache. Está estructurada de forma totalmente diferente (a simple vista mejor ordenada que la de Apache). Se comienza agrupando por protocolos, donde cada protocolo puede contener varios servidores (los cuales, por ejemplo, escuchan en diferentes puertos). Dentro de cada servidor se definen diferentes ubicaciones (similar a los aliases o VirtualHosts de Apache).

user www-data www-data;
worker_processes auto;
worker_rlimit_nofile 2048;
pcre_jit on;

pid /var/run/nginx.pid;

events {
    worker_connections 1024;
}

http {
    include mime.types;
    default_type application/octet-stream;

    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" '
                    '"$http_user_agent" "$http_x_forwarded_for"';

    sendfile on;
    keepalive_timeout 30;
    gzip on;

    upstream php5-fpm-sock {
        server unix:/var/run/php5-fpm.sock;
    }

    server {
        listen 80;
        server_name localhost;
        server_tokens off;
        root /var/www;

        location / {
            root /var/www;
            index index.html index.htm;

            location ~ \.php {
                root /var/www;
                fastcgi_pass php5-fpm-sock;
                fastcgi_param SCRIPT_FILENAME $request_filename;
                include fastcgi_params;
            }
        }

        error_page 404 /404.html;
        error_page 500 502 503 504 /50x.html;
        location = /50x.html {
            root /usr/local/nginx/html;
        }

        location ~ \..*/.*\.php$ {
            return 403;
        }

        location /info.php {
            root /usr/local/nginx/html;
            fastcgi_pass php5-fpm-sock;
            fastcgi_param SCRIPT_FILENAME $request_filename;
            include fastcgi_params;
        }

        location ~ /\.ht {
            access_log off;
            log_not_found off;
            deny all;
        }

        include sites-enabled/*.conf;
    }
}

Esta configuración simple posee un único protocolo ("http") el cual posee un único servidor (escuchando peticiones en el puerto 80) y diferentes locaciones, las cuales poseen diferentes directorios de trabajo. Pero además, el protocolo "http" esta asociado a un servidor upstream llamado "php5-fpm", que atiende en el socket UNIX /var/run/php5-fpm.sock. De esta forma (junto con la ubicación ~ \.php), se interpretan los archivos con extensión .php utilizando PHP-FPM.

Para configurar Nginx con SSL acceder al artículo Cómo compilar Nginx con soporte para SSL.

A nivel servicio Nginx, se indica que utilice el usuario y grupo "www-data"; que guarde el ID del proceso padre en el archivo /var/run/nginx.pid (para que el script de servicio /etc/init.d/nginx sepa qué proceso debe terminar cuando se envía la orden "stop"); y que utilice compilación just in time de expresiones regulares (PCRE JIT), lo cual acelera el procesamiento de expresiones regulares de forma significativa.

Como la primera vez que se inició Nginx se estaba utilizando la configuración por defecto, el script de gestión del servicio no conoce el PID que debe terminar. Por ello es necesario terminarlo manualmente enviando SIGTERM utilizando el comando kill:

# ps ax | grep nginx
# kill PID

Luego iniciar el servicio utilizando el script:

# service nginx start

Verificación

Con Nginx y PHP-FPM levantados, verificar el funcionamiento. Primero, acceder a la raíz del servidor:

Se observa que tanto el protocolo, como el servidor escuchando en el puerto 80, como la sentencia "index" han funcionado correctamente, pues se ha servido el archivo /var/www/index.html.

Luego, acceder a una URI inválida (un recurso inexistente):

Se redirecciona correctamente a la página de error 404 personalizada.

Verificar el funcionamiento de PHP-FPM accediendo al recurso /info.php:

No sólo funciona correctamente PHP, sino que además se ha accedido correctamente a URI /info.php, la cual tiene como base al directorio /usr/local/nginx/html.

Finalmente, verificar la ejecución del script index.php:

¡Éxito!

¿Vas a utilizar HTTPS? Cómo compilar Nginx con soporte para SSL/TLS. Además puede interesarte: Cómo habilitar soporte para HTTP/2 en Nginx.

Para finalizar este artículo dejo una muestra del uso de memoria actual en el servidor:

root@linuxito:~# date
Thu Aug 20 11:46:19 EDT 2015
root@linuxito:~# ps axl | grep apache
1     0  1857     1  20   0 158864   980 poll_s Ss  ?       0:05 /usr/sbin/apache2 -k start
5    33  8112  1857  20   0 162228 11144 semtim S   ?       0:01 /usr/sbin/apache2 -k start
5    33  8114  1857  20   0 168116 17248 semtim S   ?       0:00 /usr/sbin/apache2 -k start
5    33  8123  1857  20   0 167876 16488 semtim S   ?       0:00 /usr/sbin/apache2 -k start
5    33  8125  1857  20   0 159216  2436 poll_s S   ?       0:00 /usr/sbin/apache2 -k start
5    33  8129  1857  20   0 159184  2396 semtim S   ?       0:00 /usr/sbin/apache2 -k start
5    33  8146  1857  20   0 159184  2192 semtim S   ?       0:00 /usr/sbin/apache2 -k start
5    33  8147  1857  20   0 158896   880 semtim S   ?       0:00 /usr/sbin/apache2 -k start
5    33  8148  1857  20   0 158896   900 inet_c S   ?       0:00 /usr/sbin/apache2 -k start
0     0  8151  8137  20   0   7724   936 pipe_w S+  pts/0   0:00 grep apache

Se observa que Apache utilizaba en ese momento exactamente un total de 54664 KB residentes en memoria física (algo más de 53 MB). En un próximo artículo, y luego de migrar a Nginx con PHP-FPM, haré la comparación correspondiente en lo que a consumo de memoria respecta.

Referencias

PHP-FPM - A simple and robust FastCGI Process Manager for PHP

FastCGI Process Manager (FPM) Configuration

Nginx Core functionality

nginx

nginx: download

nginx-sysvinit-script

ACTUALIZACIÓN (2015-09-01): Nginx VS. Apache ¿Cuál consume menos recursos? (mi experiencia)


Tal vez pueda interesarte


Compartí este artículo