Case study: Optimizando mi web server en linux pt. 3

Luego de revisar las variables posibles de pobre desempeño a nivel más bajo, me acerco a la optimización del software. Vuelvo a reiterar que «Your mileage may (and will) vary», debido a que mis requerimientos son muy específicos respecto al software servido.

Ver también:

Ahora, Apache

Tras mejorar las variables más comunes de posibles cuellos de botella que sean en parte culpables por el rendimiento, llegamos al servidor mismo.

Elección de MPM

Como ya antes fue enunciado, Apache tiene principalmente tres modelos de funcionamiento, siendo mayoritariamente utilizados los modelos de Prefork y Worker. Prefork es tal cual el modelo antiguo de apache 1.3, es decir un proceso por cliente y sin hilos. Worker es un modelo de multihilos en donde algunos procesos manejan distintos clientes mediante diferentes hebras. Debido al costo de cambio de contexto de los procesos en prefork, worker desde ese punto de vista provee un mayor desempeño. El problema asoma cuando utilizamos software que no tenga un suficientemente elaborado sistema de manejo de seguridad de memoria en el ambito de los hilos (o «Thread Safe»), como es el problema de PHP y su TSRM.

Por lo tanto existe incluso una recomendación desde los desarrolladores a no utilizar Apache MPM Worker con PHP con extensiones Thread Safe. Pese a esto, existe una posible solución para lograr utilizar PHP en un Apache MPM Worker y es obviando mod_php y utilizando FastCGI. De esta forma cada thread engendrado corre bajo su propio contexto protegido por fastcgi, lo que provee la seguridad necesaria que no brinda TSRM.

Lamentablemente su rendimiento (en mis requerimientos) era menos que óptimo y el incremento en el desempeño al servir contenido estático (Planeta GNOME o Planeta Blogs) era fuertemente opacado por el pobre desempeño del contenido dinamico (Dewback, Arale o Noticias GNOME)., luego Worker no constituyó una solución viable para mis requerimientos.

Finalmente la elección de continuar con Apache MPM Prefork fue la solución más factible.

Dynamic Shared Objects

Una pregunta que asoma luego es si utilizar un servidor que utilice DSO o tenga sus funcionalidades compiladas estáticamente. En mi caso, dado el multifacético uso del servidor no me es eficiente desde el punto de vista de la administración el compilar mod_perl, mod_python, mod_php y otros estaticamente. Esta m´inima mejora en el rendimiento comparado con la pérdida de flexibilidad de comportamiento y facilidad de actualización me hacen preferir continuar utilizando DSO’s. Luego se debe cuidar de utilizar no más DSO’s que los estrictamente necesarios.

Los ajustes de procesos

A comenzar los ajustes de configuración entonces, pero primero, algo de fondo. En Apache, al partir, un proceso padre es responsable de gestar los subsiguientes procesos hijos quienes serán los responsables a su vez de servir el contenido, el proceso padre en ningún caso sirve contenido. Este proceso padre es quien engendra en una primera instancia los StartServers, luego engendra nuevos hijos cada vez que llega una petición nueva que supere los hijos vivos y mantiene el numero de servidores disponibles entre MinSpareServers y MaxSpareServers o cuando uno de los hijos llegue a los MaxRequestsPerChild requerimientos servidos realizando el proceso de «ripping» o asesinato de los hijos inútiles.

Apache2 engendra hijos a ritmo exponencial hasta llegar a un máximo de gestar 32 hijos por segundo hasta llegar a los MaxClients hijos, poniendo en cola a los siguientes requerimientos.

Evidentemente todos estos cambios de contexto, entre gestar nuevos hijos y asesinar otros cuantos produce un costo alto.

¿Como elegir la combinación perfecta entonces entre tanta variable?. Gracias al nacimiento exponencial de hijos en Apache2 (lineal en Apache 1.3) ya no provoca tanto problema el ajustarlos. Aun asi, todo reside en la experiencia y análisis del comportamiento del tráfico hacia el servidor web.

La condición perfecta es mantener vivos a un número suficiente de hijos para resistir la carga común y constante de el servidor y mantener un número suficiente de «servidores de respuesto» en caso de generarse repentinas explosiones (o llamados «burst») de tráfico fuera del plano común. La variable crítica entonces en adivinar dichos números. Para esto puedes analizar el promedio de requerimientos por segundo y el tráfico concurrente. Estos serán el número constante de servidores que (en promedio) estarán siempre sirviendo contenido, y por tanto los hijos que estarán siendo utilizados y analizar los posibles «bursts» de tráfico posibles con tal de tener una impresión de los posiblemente necesarios spare servers disponibles en caso de ocurrencia.

Luego MaxClients podra ser el numero máximo de clientes que seran atendidos, el numero máximo de procesos a ser engendrados en un momento de máximo tráfico y por lo tanto cuanto va a ser el máximo de memoria que utilizaría Apache. Por lo tanto se necesita saber la cantidad de memoria que utiliza cada proceso en promedio. Vale decir que sirviendo contenido estático, cada proceso utiliza mucho menos memoria (alrededor de 1MB aproximadamente) que los procesos sirviendo contenido dinámico (entre 15 y 20MB aproximadamente); sabido esto y la cantidad de memoria que se espera utilizar para el servicio web, se puede hacer una estimación del número máximo de procesos que se desea permitir engendrar.

MaxRequestsPerChild es un caso particular. Por omisión tiene un valor de 0, es decir infinito número de requerimientos pueden ser atendidos por cada hijo. Esto es, nuevamente, un arma de doble filo. Al configurarlo en un número determinado de requerimientos máximo, por una parte incurrimos en el costo del cambio de contexto al iinducir al ripping de hijos obsoletos, pero nos estamos ahorrando la oportunidad de posibles memory leaks en las aplicaciones que el servidor este corriendo provocando memory starvation. De no estar seguro de la calidad del software servido, es mejor inducir el ripping cada cierto número de requests, no menor a 10000 en ningún caso normal, de todas formas.


<IfModule prefork.c>
StartServers    TraficoEstandar
MinSpareServers MinimoBurst
MaxSpareServers MaximoBurst
MaxClients      MemoriaTotal/MemoriaPorProceso
MaxRequestsPerChild 100000
</IfModule>

Configuraciones

Un punto que no obstante su simpleza incrementa considerablemente el desempeño del servicio es la utilizacion de Overrides, el uso de los conocidos .htaccess. A modo de ejemplo, de estar activado el uso de .htaccess en / y si el DocumentRoot de un sitio esta ubicado en /var/www/sites/www.dewback.cl/ , esto es:


DocumentRoot /var/www/sites/www.dewback.cl/
<Directory />
AllowOverride All
</Directory>

y es requerido /index.php, esto implica que apache buscara los archivos /.htaccess, /var/.htaccess, /var/www/.htaccess, /var/www/sites/.htaccess y /var/www/sites/www.dewback.cl/.htaccess. Luego la solucion es activar el uso de los AccessFilename (.htaccess) solo donde sea estrictamente necesario.


<Directory />
AllowOverride None
</Directory>
<Directory /var/www/sites/www.dewback.cl/log/>
AllowOverride FileInfo
</Directory>

Por otra parte, otra configuración que puede significar un uso excesivo de llamadas de sistema es cuando una combinación de FollowSymlinks y SymlinksIfOwnerMatch no es utilizada correctamente. De no existir FollowSymlinks y si existir SymlinksIfOwnerMatch, Apache realizará una comprobación de la existencia de enlaces simbólicos mediante lstat(2) a través de toda la ruta hasta alcanzar el archivo objetivo, algo similar a la busqueda de los .htaccess antes explicada. El mayor desempeño se alcanza utilizando FollowSymlinks para todo el sitio y nunca utilizar SymlinksIfOwnerMatch, pero te saltas la comprobación de seguridad. De querer utilizar la comprobación, se debería utilizar algo como lo siguiente:


<Directory />
Options FollowSymLinks
</Directory>
<Directory /var/www/sites/www.dewback.cl/log/>
Options -FollowSymLinks +SymLinksIfOwnerMatch
</Directory>

Siguiendo con el objetivo de eliminar system calls, otro punto a tomar en cuenta es eliminar la resolución reversa de los clientes. Esto se logra con:


HostnameLookups Off

De querer resolver los nombres de las direcciones ip registradas en los log, siempre se puede recurrir a post procesar los logs con alguna aplicación como webazolver de webalizer.

Asimismo una buena idea es desactivar el uso de Negociación de Contenido de ser posible. Y en caso de ser necesario, pensar en la posibilidad de utilizar type-maps en vez de MultiViews.

ExtendedStatus es otra característica a desactivar de no ser estrictamente necesaria. Cada vez que se sirve un requerimiento, Apache realiza varias llamadas de sistema a gettimeofday(2) y a time(2).


<IfModule mod_status.c>
ExtendedStatus off
</IfModule>

MMap

Entrando en terreno mas árido, llegamos a la utilización de recursos de sistema por parte de Apache. En la mayoria de los sistemas operativos, y particularmente en Linux, se puede lograr mayor desempeño mediante mmap(2) que con read(2) cuando se trata de leer el contenido de archivos locales a servir ya que mmap permite tratar los archivos como regiones contiguas de memoria. Hablo de locales, ya que no ocurre lo mismo cuando el contenido es servido a través de NFS.


EnableMMAP On

Sendfile

Otra llamada de sistema util es sendfile(2). Se puede entender como una forma de zerocopy ya que de esta forma, Apache puede leer un archivo sin entender el contenido de este. Tan solo lo sirve sin realizar el proceso de leerlo primero y luego servirlo. Esto es particularmente util sirviendo contenido estatico (esto lo aprendi a las malas el día que tuve 300GB de tráfico en un par de horas al publicar unos videos).
Al igual que mmap, existe un problema con sendfile en sistemas montados con NFS, asimismo que existe un problema de checksum missmatch al utilizar IPV6, por lo que no es posible utilizarlo en estos casos.


EnableSendfile On

Otras optimizaciones

Por último se puede utiizar otros componentes de software que sirvan para optimizar el servicio de archivos. mod_cache es uno muy util al servir contenido estatico. Esto permite que Apache almacene en su propio cache el contenido de un archivo al momento de ser servido por primera vez, de esta forma se puede acelerar la lectura en sistemas lentos.

Dos implementaciones de cache pueden ser implementadas, cache en memoria y cache en disco. Esta de mas decir que la lectura y escritura de cache en memoria es notoriamente mas rapido, y en tiempos en donde el costo por MB de RAM es relativamente bajo es algo conveniente. El cache de disco es mucho menos costoso aun ($) a costo de un desempeño algo menor.


<IfModule mod_cache.c>
CacheDefaultExpire 86400
CacheMaxExpire 604800
<IfModule mod_disk_cache.c>
CacheRoot /var/cache/apache2/
CacheEnable disk /
CacheDirLevels 3
CacheDirLength 2
</IfModule>
</IfModule>

Pronto, y por último, algunos ajustes realizados al software asociado al servicio prestado, PHP y MySQL.

    • http://google cesar

      que bien
      hey

    • http://arale.cl arale

      :O
      estaba buscando informacion sobre servidores y me encontre con esto
      :O
      Saludines
      PD: tiene una novia muy linda ;)