Optimizando Apache

En septiembre decidí contratar un servidor dedicado en LiquidWeb, un proveedor norteamericano situado en Illinois. Anteriormente tenía uno de similares características en The Planet, pero debido al crecimiento del MultiForos y Bloogle, se estaba haciendo necesario una segunda máquina para separar lo que es el servidor Web de la base de datos. Dado que la oferta de LiquidWeb era sensiblemente mejor que la del otro proveedor, tomé la decisión de trasladar todos los servicios a su datacenter.

El antiguo servidor corría bajo CentOS 4 y, como recientemente había sido liberada la versión 5 de esta distribución de Linux, pensé que también sería una buena oportunidad para actualizarnos a ella, especialmente para disfrutar de Apache 2.2.3, MySQL 5.0.22 y PHP 5.1.6.

Tras la migración de todo el contenido, en un principio todo parecía ir como la seda. El nivel de carga del servidor había descendido sensiblemente al estar el servicio Web y la base de datos en servidores separados. Las peticiones de los clientes corrían más rápidas que nunca antes.

Sin embargo no todo fue tan bien. En momentos puntuales la carga media del servidor, que por lo general está por debajo de 1 (buena cifra teniendo en cuenta que el servidor tiene 4 procesadores), ascendía a 20 o incluso 50 o más, una cifra que prácticamente hace inaccesible cualquier servicio alojado. El swap, que por lo general está a cero, alcanzaba 2 o 3 GB.

¿Donde estaba el problema?

Evidentemente para identificar la razón de estos picos repentinos necesitaría datos. El comando “top” es uno de los más recurridos para tener una instantánea de los procesos y carga del servidor, pero por otro lado, es difícil diagnosticar un problema como este contando con esta herramienta solamente. Por lo pronto, para tener una mejor idea de lo que iba pasando, y lo que es más importante, una especie de histórico de datos, instalé Cacti, una aplicación escrita en php que muestra gráficos bastante elaborados de los servicios que configuremos. Por ejemplo, este es uno de la carga del servidor en un periodo de 24 horas:

Grafico de carga del servidor

Grafico de carga del servidor

Se puede observar claramente un pico a las 21 horas que alcanza más de 20.

Gráfico de ancho de banda

Gráfico de ancho de banda

En este segundo gráfico se ve claramente, a la misma hora, una caída en picado del caudal de datos salientes del servidor, indicio de que en ese momento la máquina estaba colapsada y no respondía correctamente a las peticiones de los clientes.

La verdad es que estos dos gráficos mostraban claramente que algo grave sucedía en un momento dado, y que ese algo era capaz de “tirar” una máquina de 4 procesadores Opteron a 2 Ghz cada uno y 4 GB de memoria RAM. El problema es que aún no tenía demasiadas pistas sobre qué podría ser ese “algo”.

Pero otra gráfica del momento de la caida, esta vez obtenida por otra aplicación, MRTG, mostraba algo que no estaba en los otros gráficos:

Gráfico de MRTG

En éste último gráfico se puede ver que a la hora de la caída aumenta por encima de los 4 Mbps el tráfico entrante al servidor, cuando lo normal es que éste no llegue ni siquiera a los 0,5 Mbps. Hay que tener en cuenta que en un servidor, al contrario que en una conexión casera, la mayor parte del tráfico se origina hacia Internet, no desde Internet.

Sin tener otras pistas esto podría indicar un pequeño ataque de denegación del servicio, comúnmente llamado por sus siglas en inglés DoS.

Si esto era cierto, ahora hacía falta obtener una instantánea de las conexiones al servidor en el momento de una de estas caídas. El problema una vez más era que no existe una hora o momento fijo para que se produzca este ataque, ya llevaba demasiadas noches en vela trabajando en este problema como para pasarme un número indefinido de ellas a la espera de no se sabe qué.

Por supuesto hay medios para paliar ataques DoS, como mod_evasive o DoS deflate, pero habiendo probado ambos, los resultados no han sido apreciables, por lo que había que seguir una nueva táctica.

Tras mucho pensar, la solución no se demoró en exceso. Había que escribir un script que hiciese la captura necesaria y me la enviase, así que manos a la obra. El resultado, por si a alguien le fuese de utilidad, es el siguiente: load_alert.txt

Para usarlo solo es necesario renombrarlo a load_alert.sh, subirlo al servidor y darle permisos de ejecución: chmod 755 load_alert.sh

Luego solo tenemos que crear un cronjob que ejecute el script cada X tiempo. En mi caso lo he puesto para ser ejecutado cada minuto:

crontab -e

*/1 * * * * /root/scripts/load_alert.sh &>/dev/null

En caso que al ejecutarse el script la carga del servidor esté por encima de un valor (predeterminado 12) que configuraremos en la línea:

if [ $cur -ge 12 ]; then

éste ejecuta una serie de comandos y nos envía por correo electrónico el resultado:

free -m | awk ‘{print $3}’ | sed ’1,3 d’ nos dice la cantidad de datos escritos en la partición swap en el momento de ejecución.

ps aux | grep http | wc -l nos muestra la cantidad de procesos de apache en ejecución.

netstat -ntu | awk ‘{print $5}’ | cut -d: -f1 | sort | uniq -c | sort -n muestra las direcciones IP de clientes con mayor número de peticiones, dato importante para localizar y bloquear a un posible atacante.

netstat -ntu | grep SYN_RECV | awk ‘{print $5}’ | cut -d: -f1 | sort | uniq -c | sort -nr muestra las IP cuyo estado de conexión sea SYS RECV.

netstat -an | grep :80 | sort muestra todas las IP conectadas al puerto 80, es decir, al puerto de Apache.

ps wauxf nos muestra, por último, en detalle todos los procesos corriendo en el servidor en el momento de ejecutarse el script.

Como decía, todos estos datos son enviados por correo electrónico. Por defecto lo hace a root, pero pueden añadirse cuantas direcciones sean necesarias. Incluso es posible, y así lo tengo yo configurado, recibir un SMS en el móvil cuando la carga del servidor supere el valor crítico indicado en el script.

Finalmente se ejecutan las siguientes acciones:

chmod 644 /root/scripts/load_alert.sh

/etc/rc.d/init.d/shorewall stop
/etc/rc.d/init.d/httpd stop
/sbin/swapoff -a

sleep 60

/sbin/swapon -a
/etc/rc.d/init.d/httpd start
/etc/rc.d/init.d/shorewall start

chmod 755 /root/scripts/load_alert.sh

Una explicación breve de estas es la siguiente:

El comando chmod 644 /root/scripts/load_alert.sh hace que el script deje de ser ejecutable temporalmente. Esto es útil para que mientras se ejecutan los siguientes comandos no se vuelva a ejecutar el script, ya que no tendría demasiado sentido.

Con

/etc/rc.d/init.d/shorewall stop
/etc/rc.d/init.d/httpd stop
/sbin/swapoff -a

detenemos el firewall (en mi caso es Shorewall), Apache y desactivamos la partición de intercambio o swap. La desactivación del swap es la única forma que he encontrado para borrar los datos que se hayan escrito en ella. De todos modos, el echo de detener Apache debería conseguir el mismo efecto si éstos han sido escritos por procesos suyos.

Con sleep 60 hacemos que es script espere durante un tiempo determinado a que baje la carga del servidor (o cese el hipotético ataque). Por defecto son 60 segundos, pero puede ponerse la cifra que se quiera, siempre en segundos.

Con

/sbin/swapon -a
/etc/rc.d/init.d/httpd start
/etc/rc.d/init.d/shorewall start

volvemos a activar el swap, Apache y el firewall.

Finalmente hacemos de nuevo que el script sea ejecutable mediante chmod 755 /root/scripts/load_alert.sh para que de este modo se siga corriendo el cronjob que lo ejecuta cada minuto.

Con un poco de suerte este código nos será muy útil en caso de que efectivamente seamos víctima de un DoS o DDoS.

Lamentablemente, o casi mejor, afortunadamente, tras estudiar los datos capturados por este script durante varias caídas de mi servidor, queda bastante claro que un ataque de denegación de servicio no parece ser el motivo del problema.

Entonces, seguimos con el misterio, ¿donde está el problema? ¿como es posible que una máquina con un “load average” de 0,30 – 0,90 durante casi todo el día llegue a picos que superen 50 y la dejen inutilizada?

En mi limitado conocimiento de Apache sé que una de las cosas que matan a un servidor es cuando éste empieza a utilizar el swap. La memória RAM permite un acceso rápido a las páginas Web, pero cuando ésta se llena por completo, Apache tiene que empezar a usar la memoria virtual, es decir el espacio de intercambio del propio disco duro, o swap, infinitamente más lento que la RAM.

Esto crea un circulo vicioso que acaba con cualquier servidor que tenga mucho tráfico: al usar el disco duro como memoria Apache tarda más en atender las peticiones, lo cual provoca que aumente el número de peticiones en cola y que el servidor empiece a responder más lentamente. Esto último provoca que muchos usuarios pulsen el botón de su navegador que refresca la página que están cargando, con lo cual se genera otra petición. El final es tal nivel de estrés en la aplicación que prácticamente deja de responder.

¿Podría ser algo así lo que ha provocado en algunas ocasiones la caída del servidor?

Para ser sincero aun no tengo una respuesta definitiva. Hay piezas de este rompecabezas que no me acaban de encajar. Por ejemplo, una de ellas es que algunas de estas caídas se han producido en horas no punta de tráfico. El proceso que he descrito se entendería en horas punta, pero no sé que pensar cuando sucede en otras ocasiones.

En todo caso he empezado a estudiar la optimización de Apache para tratar de paliar este problema, y, la verdad sea dicha, hasta este momento parece que la cosa va mejor, por lo que posiblemente la causa de estas desagradables caídas sí que tenga que ver con lo dicho.

En el proceso de optimización de Apache lo primero que hay que calcular es el número de procesos del servidor que pueden estar abiertos simultáneamente sin que haya que recurrir a la escritura en el espacio de intercambio del disco. Esto no es muy difícil de saber, al menos en teoría. Basta con ejecutar el comando “top” y obtener una media de la memoria consumida por cada proceso en activo. A mi me da entre un 1,5 y un 2% de la memoria RAM por proceso de Apache activo. Con 4 GB de memoria esto viene a decir que cada proceso consume entre 60 y 80 MB. Una barbaridad. Y lo peor de todo es que no acabo de entender como sucede esto. La teoría dice que una página html normal servida por Apache consume entre 1 y 3 MB de memoria. Un script PHP pasa a una media de 20 MB. Pero es que en mi caso estamos hablando de ¡60 u 80 MB!

Además no acabo de entender esta cifra teniendo en cuenta que mi PHP está configurado para usar un máximo de 40 MB en la ejecución de un script…. Está claro que es un aspecto que tengo que investigar más.

En teoría Apache soporta en la instalación estándar un máximo de 256 procesos corriendo de forma simultánea. Si a cada proceso le tuviese que asignar la cantidad de memoria que “top” me dice que se consume de media, 256 x 60 me obligaría a tener una RAM mínima de 15 GB. Pero esto no me acaba de convencer. He visto ya muchas veces que el comando ps aux | grep http | wc -l me indicaba que se estaban usando los 256 procesos de Apache y no había un solo KB escrito en la partición swap, lo que quiere decir que Apache estaba funcionando correctamente con los 4 GB de memoria que tiene ahora. Hay algo que no me acaba de encajar. No sé si la información que proporciona “top” es correcta del todo.

De todos modos he probado a reducir en la configuración el número de procesos simultáneos. Empecé con 64, pero aunque el servidor iba como la seda, se creaba una cola demasiado grande de clientes esperando su turno y las páginas tardaban en cargar unos segundos. Así que subí a 96, y la cosa fue mejor. La espera para cargar una página se redujo considerablemente. Pero el número de clientes conectados casi siempre rondaba el límite establecido, lo cual en horas punta haría que posiblemente aumentase la cola de espera. Finalmente he dejado el límite en 128, o lo que es lo mismo, a un 50% de la capacidad máxima de Apache. Por el momento parece ser que va muy bien, pero habrá que seguirlo de cerca. Eso sí, hasta ahora no se ha vuelto a producir ninguna caída del servidor. Todo indica que ahora sí voy por buen camino.

Para intentar liberar la memoria usada con la máxima rapidez posible también he puesto el “Timeout” en 15 en vez de la cifra original de 30.

En resumen queda así, por el momento, la configuración de httpd.conf:

Timeout 15
KeepAlive On
MaxKeepAliveRequests 250
KeepAliveTimeout 3


<IfModule prefork.c>
StartServers 40
MinSpareServers 30
MaxSpareServers 50
ServerLimit 256
MaxClients 128
MaxRequestsPerChild 10000
</IfModule>

Siguiendo con el proceso de optimización de Apache, el siguiente paso que he dado es deshabilitar los módulos de éste que no se usan. En la configuración original del servidor vienen bastantes módulos que se cargan en memoria, y por tanto ocupan recursos, que no se llegan a usar nunca. Yo he dejado la mía, por el momento, como sigue:

LoadModule auth_basic_module modules/mod_auth_basic.so
#LoadModule auth_digest_module modules/mod_auth_digest.so
LoadModule authn_file_module modules/mod_authn_file.so
#LoadModule authn_alias_module modules/mod_authn_alias.so
#LoadModule authn_anon_module modules/mod_authn_anon.so
#LoadModule authn_dbm_module modules/mod_authn_dbm.so
LoadModule authn_default_module modules/mod_authn_default.so
LoadModule authz_host_module modules/mod_authz_host.so
LoadModule authz_user_module modules/mod_authz_user.so
#LoadModule authz_owner_module modules/mod_authz_owner.so
LoadModule authz_groupfile_module modules/mod_authz_groupfile.so
#LoadModule authz_dbm_module modules/mod_authz_dbm.so
#LoadModule authz_default_module modules/mod_authz_default.so
#LoadModule ldap_module modules/mod_ldap.so
#LoadModule authnz_ldap_module modules/mod_authnz_ldap.so
#LoadModule include_module modules/mod_include.so
LoadModule log_config_module modules/mod_log_config.so
#LoadModule logio_module modules/mod_logio.so
#LoadModule env_module modules/mod_env.so
#LoadModule ext_filter_module modules/mod_ext_filter.so
#LoadModule mime_magic_module modules/mod_mime_magic.so
#LoadModule expires_module modules/mod_expires.so
#LoadModule deflate_module modules/mod_deflate.so
#LoadModule headers_module modules/mod_headers.so
#LoadModule usertrack_module modules/mod_usertrack.so
LoadModule setenvif_module modules/mod_setenvif.so
LoadModule mime_module modules/mod_mime.so
#LoadModule dav_module modules/mod_dav.so
#LoadModule status_module modules/mod_status.so
LoadModule autoindex_module modules/mod_autoindex.so
#LoadModule info_module modules/mod_info.so
#LoadModule dav_fs_module modules/mod_dav_fs.so
#LoadModule vhost_alias_module modules/mod_vhost_alias.so
LoadModule negotiation_module modules/mod_negotiation.so
LoadModule dir_module modules/mod_dir.so
#LoadModule actions_module modules/mod_actions.so
#LoadModule speling_module modules/mod_speling.so
#LoadModule userdir_module modules/mod_userdir.so
LoadModule alias_module modules/mod_alias.so
LoadModule rewrite_module modules/mod_rewrite.so
LoadModule proxy_module modules/mod_proxy.so
#LoadModule proxy_balancer_module modules/mod_proxy_balancer.so
#LoadModule proxy_ftp_module modules/mod_proxy_ftp.so
#LoadModule proxy_http_module modules/mod_proxy_http.so
#LoadModule proxy_connect_module modules/mod_proxy_connect.so
#LoadModule cache_module modules/mod_cache.so
#LoadModule suexec_module modules/mod_suexec.so
#LoadModule disk_cache_module modules/mod_disk_cache.so
#LoadModule file_cache_module modules/mod_file_cache.so
#LoadModule mem_cache_module modules/mod_mem_cache.so
LoadModule cgi_module modules/mod_cgi.so

Todo lo que está comentado con # está deshabilitado. Para llegar a este esquema lo deshabilité todo, luego intentas iniciar Apache, el cual no inicia quejándose de que echa en falta tal o cual módulo. Los vas habilitando hasta que Apache inicia correctamente. El paso siguiente es empezar a probar el funcionamiento del contenido Web que tienes. Si algo no va, entonces hay que buscar qué módulo hay que habilitar. Y así hasta que todo vaya bien. Por ejemplo, yo he tenido que habilitar, además de lo básico para que funcione Apache, algunos módulos extra para poder ejecutar scripts perl o para poder proteger el acceso a determinados contenidos. Por lo demás quizás me quede estudiar las funciones de algunos de los módulos aún no habilitados y, si lo creo conveniente y beneficioso, habilitar algunos de ellos.

Veremos durante un tiempo como evoluciona la situación con estos ajustes. La otra solución que queda es ampliar la RAM, pero por el momento trataremos de optimizar el uso de la que tenemos.

¡Y aún me queda optimizar MySQL en el otro servidor…!

5 Responses to “Optimizando Apache”


  • Increible. Muchas gracias por la clase.

    Si te apetece charlar sobre esto, ahi tienes mi correo. Fantastico.

  • El artículo es lo mejor que he leído en castellano sobre configuración del Apache. Sólo una cuestión, yo tuve problemas parecidos y el top me daba una información completamente “falseada” acerca de la ocupación de memoria. Lo que hice fue obtenerla con otros programas de medida y a partir de ahí empezar a subir procesos hijo apache con la seguridad de que no me iba a quedar sin memoria.

    Un saludo

  • Estoy realmente sorprendido, despues de un dia completo de estar jugando con los parametros mencionados, no pude encontrar la combinación adecuada de los mimos. los oloque como indican aqui y de repente la aplicación se comportaba mucho mejor. Al menos ahora no se observa tanto tiempo de espera en las respuestas.

  • Gracias por el articulo, por favor cuando optimices tu BD MySLQ nos compartes tu experiencia.
    saludos

    RAFA

  • Gracias por compartir la experiencia, ya estoy aplicando tu load_alert en un server

Leave a Reply