viernes, 17 de febrero de 2012

TCP Sockets: TIME_WAIT y los puertos efímeros son poco amigos

Hoy vamos a explicar un poco qué es TIME_WAIT y cómo puede darte por culo un rato largo.

[INTRODUCCIÓN]
Todos sabemos que las conexiones TCP ( capa 4 en modelo OSI, también llamada capa de transmisió de datos ) tienen estados. Umm... bueno, igual no todos lo sabemos. Refresquemos un poco qué son esto de los estados TCP antes de empezar.

TCP establece la conexión a tres vías[1]. Cuando se intenta establecer la conexión del host A al host B, el host A manda un paquete tipo SYN al host B. La conexión pasa a estar en un estado SYN_SENT. Cuando el host B le contesta con un paquete ACK-SYN, la conexión para a un estado llamado SYN_RECV. Cuando la conexión por fin se establece pasa a estado ESTABLISHED. También hay el estado LISTEN, que es el estado que tienen servicios tales como apache, memcached o sshd, esperando nuevas conexiones. Total, con todo esto nos tenemos que quedar con la copla de que una conexión TCP pasa por diferentes estados desde que se establece hasta que se cierra.

La lista completa de estados se puede ver fácilmente haciendo un man netstat, y buscando el apartado State.

Bien, entonces tenemos que los dos estados mas comunes son: ESTABLISHED conexiones establecidas y LISTEN, en el caso de que estemos en un servidor con varios servicios activos. Para ver una lista de las conexiones abiertas en nuestra máquina podemos hacer un:

$ sudo netstat -pan --tcp | less


[SUPOSICIÓN]
Si ejecuto un script muy simple que haga peticiones HTTP contra mi servidor web local SUPONGO que no habrá ningún problema (jeje, los cojones). Tengo un servidor Apache escuchando en el puero 80 en localhost. Problemos a ejecutar el siguiente comando en 5 consolas diferentes.

$ while true; do curl localhost; done

El comando "curl" abre una conexión TCP, hace una petición HTTP, se le devuelve un resultado (que no nos importa lo mas mínimo, ahora mismo) y CIERRA LA CONEXIÓN TCP. O esto es lo que nosotros esperamos, vamos. Entiendo que tener 5 instancias del script haría que normalmente tuviese 5 conexiones en estado ESTABLISHED, pero esto, nunca debería ocasionar un problema a nivel de sistema.

Hagamos la prueba. Ejecutamos cinco veces el script en consolas separadas, y en otra terminal ejecutamos:

$ sudo watch -n 0.5 'netstat -pan --tcp | grep ESTABLISHED | wc -l'


Pues efectivamente, nunca pasamos de 5 conexiones al mismo tiempo en estado ESTABLISHED pero... Umm.... pero al cabo de un minuto de estar ejecutando los scripts, el comando "curl" empieza a dar un error:

curl: (7) Failed to connect to 127.0.0.1: Cannot assign requested address


[PROBLEMA]
WTF. A ver, tenemos tan solo 5 conexiones abiertas. El error del curl parece decirnos que no puede obtener un puerto de origen para establecer la conexión. Veamos porqué:
Ejecutemos los scripts anteriores y pongamos un filtro diferente en el netstat:

$ sudo netstat -pan --tcp | grep TIME_WAIT | wc -l
5329
$ sudo netstat -pan --tcp | grep TIME_WAIT | wc -l
13440
$ sudo netstat -pan --tcp | grep TIME_WAIT | wc -l
19348
$ sudo netstat -pan --tcp | grep TIME_WAIT | wc -l
21526
$ sudo netstat -pan --tcp | grep TIME_WAIT | wc -l
28214


COJONES! Por algún motivo estan quedando chorrocientos sockets abiertos en estado TIME_WAIT!

[QUÉ ES TIME WAIT]
Ahora que nos hemos puesto un poco en matéria vamos a ver para que se usa el estado TIME_WAIT, y como puede llegar a ser un problema. De la definición del man de nestat sacamos que:

TIME_WAIT: The socket is waiting after close to handle packets still in the network.

Umm... según esto parece que la conexión no se cierra hasta que pase un tiempo para poder capturar los paquetes que se habían "perdido" por la red debido a posibles problemas de enrutamiento. Jummm... explicación algo escueta. Sobre todo porque no dice cuanto vamos a tener que esperar. Vamos a investigar un poquito más sobre el tema...

El RFC 1122 en el apartado "Transport layer, TCP" [2] dice algo así como:
When a connection is closed actively, it MUST linger in TIME-WAIT state for a time 2xMSL (Maximum Segment Lifetime).

Vaaaale. Ahora ya tenemos un poco mas de información. Resulta que tenemos que esperarnos 2xMSL antes de que una conexión pase de TIME_WAIT a CLOSED ( y se libere la conexión ). Genial. Y ahora... ¿Dónde coño puedo consultar el tamaño del Maximum Segment Lifetime? Pues ejecutando este comando:

$ cat /proc/sys/net/ipv4/tcp_fin_timeout
30

Que en mi máquina Debian me da un resultado de 30 segundos. En máquinas con Ubuntu el valor acostumbra a ser 60, en máquinas Solaris acostumbra a ser 120. Entonces parece ser que, en mi máquina, tardaré 2*MSL, o sea 2*30=60 segundos en liberar una conexión. El problema que hemos experimentado antes es que hay muchos sockets (conexiones) abiertas en estado TIME_WAIT. Cuando llegamos al límite de conexiones efímeras del sistema, el kernel nos dá un error diciendo "umm... me sabe mal, pero no me quedan puertos efímeros". Esto se traduce en el error que nos saca el "curl". O sea, no es problema del comando "curl" que vaya dejando conexiones abiertas, ya que si no tendríamos muchas conexiones en estado ESTABLISHED, y no es el caso. Tampoco es problema del kernel, ya que éste implementa el RFC 1122 como dios manda.

[PUERTOS EFÍMEROS]
"Ei. Ei, espera. Cuando has empezado a hablar de puertos efímeros y toda esta mierda me he perdido. A ver si te explicas un poquito más, macho."
Veamos. Cuando establecemos una conexión TCP con nuestro navegador a un servidor Web, necesitamos un puerto de origen [3]. Estos puertos, se conocen como puertos efímeros. El protocolo TCP necesita definir un puerto origen y un puerto destino. En nuestro ejemplo, el puerto destino será el 80 (servidor web), y el puerto origen será un puerto efímero. El Kernel nos asigna un puerto efímero automáticamente cuando establecemos una conexión nueva. Para ver el rango de puertos efímeros que tenemos definido podemos ejecutar:

$ cat /proc/sys/net/ipv4/ip_local_port_range
32768 61000

Si hacemos 61000-32768 nos dá 28232. Si os fijais, es un número muy parecido al número de sockets en estado TIME_WAIT que teníamos cuando ha empezado a petar todo. Esto es debido a que un socket en estado TIME_WAIT todavía es un socket activo. Es lo mismo como si estuviese en estado ESTABLISHED, por lo que está ocupando un puerto efímero. Si acumulamos muchos sockets en estado TIME_WAIT, vamos agotando los puertos efímeros. Cuando llegamos al límite y tenemos reservados *todos* los puertos efímeros, al kernel no le queda otra que devolver error cada vez que se quiere crear un socket nuevo. Bonito, verdad?

[POSIBLES SOLUCIONES]
Bueno, una de ellas pasa por aumentar el número de puertos efímeros del sistema. Esto se puede conseguir simplemente ejecutando:
$ sudo su - root
$ echo "1025 61000" > /proc/sys/net/ipv4/ip_local_port_range

Hecho esto, ahora disponemos de 60K puertos efímeros y no solo 30K. En realidad esto no es una solución de nada. Simplemente tendremos que esperar 2 minutos y no 1 para que los procesos "curl" agoten el número de sockets. Estamos haciendo que el sistema aguante un poquito mas, pero ya está.


Otra solución parece que puede venir modificando el parámetro 2*MSL. Si disminuimos este valor del MSL, los sockets en estado TIME_WAIT deberían estar en este estado durante menos tiempo. El parámetro es ajustable mediante el comando comentado anteriormente:
$ sudo su -c 'echo 5 > /proc/sys/net/ipv4/tcp_fin_timeout'

Si probais de aplicar este cambio vereis que en realidad las conexiones en estado TIME_WAIT no se cierran a los 10 segundos como sería lo esperado (2*MSL; 2*5=10), sino que se siguien cerrando a los 60 segundos. Por qué ocurre esto? Simple. El valor "60 segundos" está hardcoded en el kernel de linux. Así de bonito. Si miramos el fichero /include/net/tcp.h línea 105 del kernel de línux veremos algo tal que así:

#define TCP_TIMEWAIT_LEN (60*HZ) /* how long to wait to destroy TIME-WAIT state, about 60 seconds */

A partir de línea 202 del fichero /net/ipv4/tcp_minisocks.c vereis que hace uso intensivo de la variable TCP_TIMEWAIT_LEN, justo en la sección donde define el estado TCP_TIME_WAIT al socket TCP. Por lo que modificar a través de las sysctls el parámetro "tcp_fin_timeout" no va a servir de un carajo. Ni 2*MSL ni pollas. Harcoded en el código. (Esto vulnera el RFC 1122 y la regla 2MSL? Si alguien lo sabe que por favor comente! Gracias )


La última solución, que dista mucho de ser algo perfecto pasa por activar el tcp_tw_recycle y tcp_tw_reuse. Ambas directivas son bastante peligrosas, y vulneran explícitamente el RFC 1122. No deberían usarse en entornos de producción o sin saber muy bien qué se está haciendo. La documentación que he encontrado al respecto es [4] y [5].

TCP_TW_RECYCLE: It enables fast recycling of TIME_WAIT sockets. The default value is 0 (disabled). The sysctl documentation incorrectly states the default as enabled. It can be changed to 1 (enabled) in many cases. Known to cause some issues with hoststated (load balancing and fail over) if enabled, should be used with caution.
$ echo 1 > /proc/sys/net/ipv4/tcp_tw_recycle

TCP_TW_REUSE: This allows reusing sockets in TIME_WAIT state for new connections when it is safe from protocol viewpoint. Default value is 0 (disabled). It is generally a safer alternative to tcp_tw_recycle
Note: The tcp_tw_reuse setting is particularly useful in environments where numerous short connections are open and left in TIME_WAIT state, such as web servers. Reusing the sockets can be very effective in reducing server load.
$ echo 1 > /proc/sys/net/ipv4/tcp_tw_reuse

O sea, usadlos, pero con mucho cuidado.


La solución definitiva viene en forma de pregunta: ¿Qué coño haceis creando 30K conexiones TCP en menos de un minuto, criaturas del Señor?
Si habeis llegado aquí porque os habeis encontrado con el problema del TIME_WAIT en un servidor de producción cuando recibía bastante tráfico os tengo que decir algo que no os va gustar: algo estáis haciendo mal. En este caso no es suficiente con cerrar las conexiones a la BBDD, caches, o lo que sea cada, vez que se hace una consulta. La solución pasa por hacer pooling de conexiones y demás. REAPROVECHAR conexiones. Pero bueno, esto es otra guerra que se sale del alcance de este post. Aún así para dudas, ruegos y preguntas teneis los comentarios. Intentaré ayudar hasta donde pueda/sepa.


[ÚLTIMAS NOTAS]
Nótese que este problema es provocado por la conjunción de crear muchas conexiones por parte de un CLIENTE (importante este punto) durante un periodo muy corto de tiempo. Esto hace que se acumulen muchos sockets abiertos (bueno, o no-cerrados, para ser mas exactos) en estado TIME_WAIT. Cuando se llega al punto que la suma de todos los sockets en dicho estado es igual al número de puertos efímeros de la máquina, el kernel no es capaz de crear sockets nuevos hasta que no se liberan recursos, devolviendo un error a cualquier proceso que intente crear una conexión nueva (nótese que las conexiones ya establecidas seguirán funcionando normalmente).

Espero que les haya gustado el post. La zona de comentarios está disponible para hacer preguntas, o proponer soluciones alternativas al problema.

Un saludo, Jan.

[1] http://danred.wordpress.com/2009/09/28/establecimiento-de-conexion-tcp-de-3-vias/
[2] http://www.rfc-editor.org/rfc/rfc1122.txt página 87
[3] Recordemos que un socket está formado por un IP Origen - Puerto Origen - IP Destino - Puerto Destino.
[4] http://www.speedguide.net/articles/linux-tweaking-121
[5] http://www.kernel.org/doc/Documentation/networking/ip-sysctl.txt

30 comentarios:

  1. Muchas gracias por este texto! Me sacaste muchísimas dudas que tenía respecto a mis servidores de producción.

    ResponderEliminar
  2. Gracias a tí por el comentario!! Estoy contento de que te haya servido.

    No veas para descubrir todo esto nosotros solos en el trabajo. Los síntomas del problema es que el servidor de producción se caía si le hacías MUCHAS peticiones. Resulta que la capa de servicios de nuestra aplicación hacía unas 100 consultas a MongoDB (que son muy rápidas y esto no está mal) pero sin usar un pool de conexiones. Fail. Por lo que por cada petición en la página web se hacían 100 consultas a la BBDD, lo que significaban 100 sockets en TIME WAIT. Se arregló temporalmente con el TCP_RECICLE y TCP_REUSE pero acabamos modificando la aplicación para que usara Pools de conexiones y problema resuelto.

    Un saludo!

    ResponderEliminar
    Respuestas
    1. Amigo eres un maestro. Así se explican las cosas. Esto es útil de cojones. Y si no es mucho abusar, podrías explicar como utilizar el pool de conexiones (reaprovecharlas).

      Eliminar
    2. Primero de todo, muchas gracias Xavs. Se agradecen los comentarios con buenrrollismo.

      Después, sobre lo del pool de conexiones. Si, claro, te puedo explicar como reaprovechar conexiones, pero antes de ponerme a ello, te explico: Esto de reaprovechar conexiones depende mucho de la tecnología que uses (PHP, Python, CLisp, Erlang, C...) así como el DRIVER que uses para conectarte al servicio concreto: PyMySQL, psycopg, php-memcached, etc...

      Para ponerte un par de ejemplos para que se entienda mejor: el pooling lo haremos desde nuestro lenguaje de programación preferido hasta el servicio que queramos (MySQL, Memcache, Postgres, API-REST...) con un driver concreto.

      EJEMPLO 1: Des de PHP a MySQL con el driver mysqli:
      Unlike the mysql extension, mysqli does not provide a separate function for opening persistent connections. To open a persistent connection you must prepend p: to the hostname when connecting.
      http://www.php.net/manual/en/mysqli.persistconns.php

      EJEMPLO 2: Python con Mongo (con el driver de pymongo):
      Every Connection instance has built-in connection pooling
      http://api.mongodb.org/python/1.7/faq.html#how-does-connection-pooling-work-in-pymongo

      etc... Lo que estoy haciendo básicamente aquí es viendo la documentación del DRIVER que estoy usando y ver si tiene la opción de pooling (búscalo como "connection polling" o simplemente "pooling"). En algunos casos esta ya viene por defecto, en otros casos se tiene que activar pasándole un parámetro a un constructor, en otros se tiene que cambiar un poco el código y en otros, simplemente, no se puede (no está soportado).

      A ver, aún así, si no he acabado de resolver tu duda, dime qué lenguaje de programación estás usando, que driver y a que servicio te quieres conectar, y busco un poco en la documentación qué hay sobre el tema y te digo ;)

      Eliminar
    3. Hola Inedit,

      Muchas gracias por contestar tan rápido!!!

      Es PHP con MYSQL (no se a que te refieres con driver). De golpe nuestra web rechaza algunas conexiones. El servidor web está separado del servidor MYSQL. El error es "Can't connect to MySQL server on 'xx.xx.xx.xx' (99)". Yo creo que es el servidor MySQL que las rechaza. También podría ser el servidor web, si las conexiones pasan de las 28.232 que tu has explicado. Viendo los logs del apache, hemos visto que muchos accesos eran de los bots de Bing y de Google. Parece que hemos solucionado temporalmente el problema bajando la frecuencia de rastreo, pero eso no es la solución definitiva.

      Desde el servidor web, estoy controlando las conexiones abiertas entre los dos servidores, y en algunos momentos hemos sobrepasado la cifra de 28.232. ¿ Que podemos hacer ? Tal y como has comentado, una cosa es subir la cifra de puertos efímeros. Supongo que otra es hacer las conexiones persistentes. ¿ Tu que dominas, como lo ves ? ¿ Cual de los dos servidores está rechazando las conexiones ? ¿ Y solo con poner p delante de la conexión mysqli ya la convertimos en persistente ?

      Tenemos claro que es algún tipo de limitación por algún sitio, porque no es que la web cada vez vaya más lenta hasta que las conexiones son rechazadas, si no que de golpe las empieza a rechazar.

      Perdona por avasallarte a preguntas, pero jefe, eres bueno, conciso y llano a la hora de explicar (cosa rara en el mundo de la informática).

      Muchas gracias por tu ayuda !!!!

      Xavs

      Eliminar
    4. Ei, de nada hombre. Todo sea por ayudar. Yo soy chico OpenSource y mis conocimientos también ;) Pregunta lo que quieras (a 10.000 cucas la hora, eh? ;)

      Vayamos por partes. Como yo no tengo acceso al servidor voy a ir dando ideas y tu pruebas las que te convenzan. Si ves que en alguna de ellas no acierto con alguna suposición que haga, simplemente la descartas y me puedes comentar el "por qué" no aplica esta solución, así vamos delimitando el problema.

      El problema seguramente estará solo en uno de los dos lados, pero seguro que podemos hacer mejoras en ambos lados (servidor web/servidor DDBB). Tu comentas que te parece que el servidor web está rechazando conexiones, esto podría ser debido por que se llegan a las conexiones máximas permitidas por MySQL, que por defecto son 100. Si se reaprovechan las conexiones en la aplicación web NUNCA se debería llegar a 100 conexiones concurrentes (o tienes una página de la ostia, macho), pero aún así quiero que sepas que puedes aumentar el número máximo de conexiones soportadas por MySQL [1]. En caso de que el problema fuese que creas mas de 100 conexiones la mismo tiempo con el servidor de BBDD cuando tienes mucha carga, aumentar este número a 250 por ejemplo, podría ser una solución temporal. Repito, abrir 250 conexiones concurrentes es algo muy bestia que tendríamos que evitar a toda costa. Lo suyo es reaprovechar las conexiones [2].

      Segundo punto: ver que podemos hacer con el servidor web. Si realmente sobrepasais las 28.000 conexiones ahí hay algo chungo. Pero me gustaría ver el comando con el que compruebas el número de conexiones en uso. Mas o menos tendrían que ser así (Lo pongo de memória y sin probarlo en el terminal):

      # Comprobar el número de conexiones TCP existentes
      $ sudo netstat -pan --tcp | wc -l
      # Para saber todas las conexiones que tenemos en TIME_WAIT
      $ sudo netstat -pan --tcp | grep TIME_WAIT | wc -l
      # Para saber las conexiones abiertas con el servidor de BBDD
      $ sudo netstat -pan --tcp | grep LA_IP_DE_TU_SERVIDOR_DE_BBDD | wc -l
      # Para saber las conexiones abiertas con el servidor de BBDD en estado TIME_WAIT
      $ sudo netstat -pan --tcp | grep LA_IP_DE_TU_SERVIDOR_DE_BBDD | grep TIME_WAIT | wc -l

      Obviamente estas consultas las tendrías que hacer cuando haya un poco de tráfico en tu web para que las cosa esté animada. Para hacer un poco de prueba de stress puedes usar la utilidad AB (Apache Benchmark):
      $ ab -c 20 -n 400 http://tuservidorweb.com/

      Cuando tengas las métricas anteriores (tanto el número de conexiones como el resultado de "ab") ponlas por aquí o en un pastebin. Quita toda la información que no quieras que sea pública (como IP's, etc...) ya que no son relevantes

      Y ahora lo mas importante: el tema de reuso de conexiones. Para esto es muy importante saber que driver utilizas para conectarte. Como dices que no lo sabes, me podrías pasar la línea de código de PHP en la que haces la conexión con la BBDD. Obviamente quita la IP (si es pública) y cambia el nombre de usuario y contraseña por unos inventados. Lo que me gustaría ver sería una línea parecida a alguna de las siguiente:

      mysql_connect("localhost","peter","abc123");
      $link = mysql_connect('localhost:/tmp/mysql.sock', 'mysql_user', 'mysql_password');

      Con esto te podré ayudar intentando buscar el modo concreto de usar "connection poling" des de MySQL. Seguramente el cambio sea muy pequeño. Aún así, te recomiendo que pruebes que todo funciona en un servidor de desarollo o en tu local, no vaya a ser que te cargues el servidor de producción durante un rato!

      Ala, con Diox!

      Eliminar
    5. [1] Es el primer link que he pillado de google: http://rackerhacker.com/2007/01/24/increase-mysql-connection-limit/
      [2] A ver, no si tu web es muy grande o cuantas visitas diarias tienes. En el caso de que tengas mucho tráfico y algunas consultas SQL muy pesadas, supongo que sería posible llegar a 250 conexiones y sería algo normal (en mi limitada experiencia nunca lo he visto). En el 95% el límite de max_connections=100 en MySQL es mas que suficiente.

      Eliminar
  3. Hola Inedit,

    No estamos hablando del blog del vecino de al lado !! ;-) No se te calcular exactamente el numero de visitas diarias, porque hay muchas webs, pero calculo que estarán sobre las 30.000 / 40.000 visitas. No es mucho, pero el blog del vecino de al lado no llega a tantas!!

    1) Las conexiones máximas permitidas por el mysql las tenemos en 5000 (y tu hablas de 250 !!). Subir este número es una posibilidad, pero como tu dices, es un parche, no soluciona el problema.

    2) El comando que utilizo para saber cuantas conexiones abiertas hay entre el servidor web y el mysql es “netstat -ntu | grep -c '192.168.82.70' “. 192.168.82.70 es la IP privada del servidor mysql, y esta comanda la lanzo en el servidor web. La tengo puesta en el crontab, y la lanzo cada minuto escribiendo el resultado en un log, de forma que sé cuantas conexiones abiertas hay en cada momento. Puedo controlar los picos de conexiones. Por ejemplo, esta noche, aunque la media esta en unas 600 / 700 conexiones, el pico más alto es de 28.230.


    3) Como te he comentado antes hay varias webs en el servidor web que se conectan al servidor mysql, de forma que te daré 2 o 3 ejemplos de conexiones que utilizamos.

    $db = mysql_connect("192.168.82.70","usuario","password");
    mysql_select_db($basededatos,$db);
    mysql_set_charset('utf8');

    ------------------------------------------

    $connect_params = mysqli_connect(“192.168.82.70”, “usuario”, “password”)
    mysqli_query ($connect_params,"SET NAMES 'utf8'");
    mysqli_select_db($connect_params, $ basededatos);

    Y amigo, tu ayuda no tiene precio !!!!

    Gracias de nuevo,

    Xavs

    ResponderEliminar
  4. Wow. Max_connections en 5K. No está mal. Esto debe ocupar memória que da miedo.

    La media de conexiones son unas 600/700.... flipa. Un error muy común, y sin querer ofender a nadie, es dejarse conexiones si haber hecho un mysql_close(). Sé que es algo obvio, pero depende de cómo tengais la aplicación montada podría ser que haya un sitio en donde no se estén cerrando las conexiones. Lo digo para dar ideas y para que alguien diga "SEGURO QUE ESTO ESTÁ BIEN" y mas adelante se le pueda echar en cara que en una parte de código esto se estaba gestinando mal ;)

    Y volviendo a lo de conection pooling, ahora necesitaría saber qué servidor web estais utilizando (dime que Apache, please! o almenos que no sea IIS!) y si ejecutais el PHP a través de PHP-CGI o si estais usando el MPM prefork o cómo lo tenéis montado, vaya. Esto es importante ya que dependiendo de el modo en que esteis ejecutando los intérpretes de PHP, se podrá (o no) hacer connection pooling. De no poder hacerse se podría optar por cambiar el modelo de ejecución o comernos el coco pensando en otras soluciones.

    Se que la cosa se está liando, pero es lo que hay. Me da la impresión de que o (a) habéis tocado ya con el primer bottleneck importante de la apliación y ya empezais a tener problemas típicos cuando la cosa se sobrecarga o (b) hay una cagada por ahí como alguien que no está cerrando las conexiones y que lo está dejando hecho unos zorros.... o (c) que os estoy llevando por el camino que no es, por falta de información o conocimientos, y que quizás el problema venga por otra parte.

    También otra pregunta. Cuántos processos de PHP tenéis trabajando normalmente? 5, 10, 100? Si este número es elevado me cuadraría bastante con la media de conexiones que teneis.

    Anyway. Yo no soy un experto en eso, ni mucho menos. Simplemente voy dando ideas de sentido común para ir avanzando y delimitando el problema, a ver si damos con una solución. Si te tienes que sentir mas cómodo hablando conmigo por email hazlo sin problemas en inedit00 [at] gmail [] com, aunque si diesemos con una solución a todo esto la publicaría como comentario al final de este hilo, que quizás pueda solucionar la papeleta a otro (me parecería justo).

    Ala, un saludo y suerte.

    ResponderEliminar
  5. Hola Inedit,

    creo que es mejor ir escribiendo aquí los comentarios, así te quedará un post niquelao ... ;-)

    Vamos por partes:

    1) El servidor es apache. Y perdona mi ignorancia, pero ni papa de lo que es PHP-CGI. Piensa en servidores Apache (Debian) “normales” con PHP y MySQL (En los dos servidores).
    2) Tenemos una maquina de procesos separada, por lo que los procesos se ejecutan desde otra maquina. Por lo tanto, no afecta. Es cierto que tenemos algunos trabajos ejecutándose en el servidor web, pero en principio no creo que afecten mucho. Por ahí no van los tiros.
    3) Estoy 100 % seguro de que no siempre se cierran las conexiones. No tengo ninguna duda de eso. De hecho es algo a arreglar. Pero si no lo tengo mal entendido, el servidor apache cierra las conexiones automáticamente después de servir un página. No es así ?

    Déjame escribir un pequeño resumen de las cosas que se pueden hacer:

    - Subir el número de puertos efímeros
    - Subir el número de conexiones permitidas por el mysql.
    - Cerrar correctamente todas las conexiones.
    - Re-utilización de las conexiones (si es posible, a ver como lo hacemos)

    Seguimos !!!

    ResponderEliminar
  6. Veamos. El MPM (Multi processing Modules) de Apache tiene dos modos de funcionar: prefork y actor.

    Hay muchos arítulos sobre el tema, pero básicamente la diferéncia es que el modo Prefork utiliza procesos thread-safe y el modo Actor (no utiliza threads). Como PHP utiliza librerías que NO SON thread safe, no puedes usar hilos, por lo que lo mas seguro es que estés utilizando el modo Prefork. Para comprobarlo tienes que hacer:

    $ /usr/sbin/apachectl -l

    ... y te aparecerá si utilizas prefork o actor.

    Entonces, contando con que utilizas prefork, Apache crea N processos (StartServers=5 mas exactamente[1]). Cuando entre un request, Apache va a hacer que lo sirva uno de estos procesos. En el caso de que llegen mas request que procesos, Apache irá creando nuevos procesos hasta llegar a MaxClients (150 en mi caso). Este valor se tiene que poner a un punto óptimo, porque la creación de un proceso nuevo es costosa y añade latencia a las peticiones HTTP, de modo que no lo podemos poner muy muy alto, pero por otra parte si está muy bajo y tienes muchas peticiones se quedarán encoladas hasta que algún proceso quede libre y las procese, por lo que tampoco combiene. Lo suyo es econtrar un punto medio dependiendo del tráfico que tengas. Nótese que si en un momento que tienes un pico de tráfico se crean 100 procesos, cuando estos dejen de trabajar Apache los matará sin piedad al cabo de un tiempo, y te quedarás con MaxSpareServers(10 en mi caso) procesos en marcha esperando nuevas peticiones. Digamos que apache se ajusta según sus necesidades.

    Hay algo mas, que es interesante que son los MaxRequestsPerChild (que por defecto viene a 0). Esto son el número de peticiones que proceso concreto puede servir antes de que Apache lo mate y cree otro. Por ejemplo, si MaxRequestsPerChild=100, después de peticiones HTTP servidas por un proceso individual, el proceso será brutalmente asesinado por Apache (si es que somos unos violentos en esto de la informática) y se creará otro nuevo para reemplazarlo. La muerte de un proceso y su creación es un proceso LENTO, por lo que es recomendable poner este valor algo alto, pero tampoco no mucho ya que podría ser que un proceso tuviese un leak de memória y empieza a malfuncionar, por lo que on está mal que cada tanto se mate el proceso y se cree otro nuevo. La gracia de esto es REAPROVECHAR un proceso y que sirva para ejecutar varias peticiones. En el caso que este parámetro sea 0 (la configuración por defecto) el proceso NO MORIRÁ NUNCA por el hecho de haber servido muchas peticiones. No morirá. Jamás de los jamases. Vivirá eternamente (o hasta que reinicies el servicio de apache y el kernel los mate sin piedad, claro).

    Bien, dada toda la teoría coñazo (tranqui que todo esto servirá para algo, al final), vayamos al problema que nos ocupa y déjame que repase los puntos que tu bien has enumerado:
    - Subir el número de puertos efímeros: Me parece bien pero es una medida temporal. No soluciona el problema real
    - Subir el número de conexiones permitidas por el mysql: Ya esta alto de cojones, 5000 conexiones son una brutalidad. Con muchas menos debería funcionar ya que tu página recibe 27 consultas por minuto, aproximadamente. Tu servidor debería estar muerto de rista con esta carga. Hay algo que no está bien, pero seguro que aumentando el número de conexiones no se soluciona.
    - Re-utilización de las conexiones (si es posible, a ver como lo hacemos): Interesante opción a meditar cuando se solucione el punto siguiente...
    - Cerrar correctamente todas las conexiones: ..... PUES ESO, ALMA DE CÁNTARO!! Cerrar bien las conexiones es algo absolutamente FUNDAMENTAL!!! Dejar conexiones abiertas es un peligro, hombre!

    ResponderEliminar
  7. --- Continuación ----
    A ver que nos aclaremos (sips, ahora te voy a echar la bulla como me la echaron a mi en su día ;). ¿Estás delegando en que APACHE te cierre las conexiones que tu has abierto en PHP? ¡¿Dónde quedan las buenas prácticas, hombre?! Cualquier conexión que se abra a nivel de aplicación a DONDE SEA (MySQL, Memchached, Postgres, etc...) tiene que ser cerrada por la misma aplicación. No presupongas que Apache lo hará por tí. Por que "¿Apache debería cerrar las conexiones por tí?" Pues la explicación larga es porque Apache manda una señal de matar un proceso y lo deja en manos del Kernel. El Kernel, a su vez, cuando se carga (mata) el proceso, cierra todos los descriptores de fichero que el proceso tuviese abiertos y esto incluye las conexiones TCP/UDP. Entonces cierra las conexiones "a las malas". Esto significa que tu servidor MySQL no se ha coscado de que una conexión se ha ido a tomar por culo, lo que esto podría dar problemas adicionales. Cuando, des de PHP, haces un mysql_close() (o equivalente) seguramente se manda un mensaje al servidor MySQL diciéndole que se cierra la conexión por X motivos, y la conexión TCP se cierra normalmente como dios manda. Pero delegar todo esto trabajo al krenel, a Apache o a la chica de la limpieza es una MALA IDEA. Y después pasa lo que pasa.

    Que Apache haría lo que tu esperabas en caso de tener otra configuración diferente, pero como seguramente veas ya, si tienes MaxRequestsPerChild=0, significa que algunos procesos creados por Apache NO MUEREN, sino que viven eternamente. Entonces tendrás un proceso que irá sirviendo peticiones y creando conexiones a la base de datos (sin cerrarlas) indefinidamente. Y claro, después uno se explica que puedas tener 600/700 conexiones abiertas de media, que es algo exagerado.

    Soluciones... Pues veamos... Para empezar yo he escrito todo este tostón y he dado por supuesto que TU configuración es igual a la mía (la configuración por defecto), me puedo estar equivocando con la solución del problema y de ser así me comeré mis palabras (totitas todas). Puede ser que tengamos configuraciones distintas, pero parece claro que el problema viene por no cerrar conexiones en la aplicación. Esto es algo bàsico y que tienes que solucionar cuanto antes mejor. Mira dónde se hacen conexiones y ciérralas (conexiones no cerradas = caca x'D). De momento y como medida provisional, porque ya se que arrebuscar en el código, y sobretodo si hay una base de código grande, puede ser un coñazo supongo que puedes poner MaxRequestsPerChild a... digamos... 10, o a 20 (ves probando) de modo que el proceso muera al cabo de 20 peticiones. Que, a ver, quizás con una sola petición HTTP concreta se dejan abiertas 4 o 5 conexiones SQL (dependiendo de lo bien o mal programado que esté el código), esto querría decir que habrían 100 conexiones al servidor MySQL antes de que el proceso muera. Me estoy inventando las cifras, pero te recomiendos que pruebes y revises log logs de conexiones por minuto, y ajustes esta variable para que quede algo decente (30-50 conexiones, diría yo). Piensa que esto hará que tu página vaya mas LENTA ya que no estarás reaprovechando procesos como antes, y matar y crear un proceso de nuevo lleva tiempo (nosé, para que te hagas una idea será un valor por debajo del segundo SEGURO y seguro que me esté pasando un güebo y sean solo 20-30ms, no se como mirarlo, la verdad....).

    Pues eso... a arremangarse, ponerse las gafas de buceo y a hacer refáctoring por el código, buscando conexiones abiertas (espero, de verdad, que el código sea tuyo y que no seas un servidor de hosting o alguien que esté hosteando páginas en tu servidor. Si es así, vé con Diox).

    Me parece que con esto queda mas o menos el problema resuelto, aún así si sigues teniendo dudas, problemas o simplemente me he equivocado en algo, dímelo sin problemas y seguimos jugando. Me lo he pasado bien investagando sobre esto ;)

    Un saludo!

    ResponderEliminar
  8. EEiii, no me metas tanta caña hombre !!! ;-) vale ... vale ...

    Había un lío de un par de cojones, no solo con conexiones que no se cerraban, si no que habría más conexiones abierta de las debidas.

    He arreglado todo el batiburrillo, por eso he tardado tanto en contestarte. Ahora esta nikelao ... abro el mínimo de conexiones, siempre con mysqli, y las cierro adecuadamente.

    Y aunque en parte el numero de conexiones abiertas ha disminuido, ha sido mucho menos de lo esperado, sigo teniendo una media bastante alta 300 / 400 con picos aún muy altos. Puede ser que tenga algunas conexiones por ahí no controladas, pero te aseguro que he arreglado bastante el tema. Llegado a este punto ... hablamos de persistentes ? ;-)

    Dios te lo page, hijo, Dios te lo page ...

    ResponderEliminar
  9. Por Dios no! Que Diox me lo pague con una lotería generosa o con mucho sexo. Pero nada de hijos! Además, estarían fuera de matrimonio y esto creo que es tarjeta amarilla con las reglas que Dios impuso.

    A ver, me quedan dos semanas de curso y un par de examenes antes de las "fiestas" de Navidad (aka Estudia Los Examenes Finales que Tienes Después de Fiestas). Seguiremos con el tema de las conexiones persistentes, pero siempre y cuando tenga tiempo ;)

    Un saludo.

    ResponderEliminar
  10. Hola inedit,

    cuanto tiempo sin oírte. Ya he encontrado el problema, jefe. Y aunque las conexiones siguen siendo altas, ya no sobrepaso el límite. Y las seguiré bajando. El problema estaba en que casi para cada consulta, se abría una conexión (en la web más importante). Y amigo, no me eches la bronca, que por una vez de esto soy inocente !!! Increíble tio, no sabes la cantidad de conexiones que abre esta web / aplicación. Es inaudito. Lo raro es que no petara antes. Con paciencia, voy reutilizando una que abro al principio de cada php, y que cierro al final, cargándome todas las conexiones abiertas por el medio. Créeme que esto es mucho más complejo de lo que parece (includes, funciones, ...). Una vez haya rebajado la mega-ultra-supra-cantidad-de-conexiones-abiertas, haré que las conexiones que abro al principio (una mysqli, y otra PDO) sean persistentes, a ver si así remato la cosa. Y para los que se estén leyendo este post, y les hayamos ahorrado tiempo y sudor, para que la conexión sea persistente es tan fácil como poner "p:" en la conexión : mysqli_connect("p:hola.lola.com", "usuario_lolita", "psw_de_usuario_lolita").

    Nada jefe, ahora si que hemos rematado la cosa. Gracias por tu inestimable ayuda. Espero que te hayan ido bien los exámenes.

    Adios !!!!!!!!!!!!

    ResponderEliminar
  11. Xavs, lo siento. Teníamos algo pendiente tu y yo y se me pasó completamente.

    Sin embargo estoy muy contento al oír que has encontrado la raíz del problema, y que ya tienes modo de ir arreglando el tema. Te creo cuando dices que es un trabajazo el ir revisando conexión por conexión y patearse todo el código. Árduo trabajo el que te toca hacer.

    Y yo pregunto solo por saber. Si tienes un código que hace:
    abro_conexión()
    [...] código [...]
    cierro_conexión()
    [...] código [...]
    abro_conexión()
    [...] código [...]
    cierro_conexión()
    [...] código [...]
    etc, etc...

    Siendo "abro_conexión()" el código: mysqli_connect("hola.lola.com", .....,
    podrías probar de poner mysqli_connect("p:hola.lola.com",.... y ver si así funciona y se reducen el número totales de conexiones. No tengo ni idea de si va a funcionar o no, pero es un cambio muy simple de hacer y que te ahorraria trabajo infinito de hacer refactoring en toda tu aplicación.

    Igual lo has probado ya, y no te ha funcionado. Ya de dirás.

    Bueno, estoy muy contento de que nuestra conversación haya servido para algo y te mando un saludo. Para lo que necesites y te pueda ayudar, aquí estoy.

    --Jan

    P.D: Siempre me quedaré con las ganas de saber la página web en cuestión... ;)

    ResponderEliminar
    Respuestas
    1. Ya lo había probado. Y se caía la web. Algunas veces, esta tal y como tu dices: abro_conexión() y con algo de suerte, el cierro_conexión. Otras veces el mysqli_connect a saco paco.

      Lógicamente no podía abrir conexiones persistentes en todos los mysqli_connect, pero si en el que estaba dentro del abro_conexión(). Y la web se iba a tomar por saco más rápido de lo que canta un gallo. Yo me imagino que es por los siguiente : imagínate que cada PHP abre 5 o 6 conexiones persistentes (las llamadas a abro_conexión()), mas 2 o 3 conexiones no persistentes de las conexiones abiertas a pelo. Pues multiplica. Hay vete a saber cuantos usuarios conectados simultáneamente. Sé que en teoría se deberían reaprovechar un montón de ellas, pero el resultado es que se caía la web. Supongo que era por demasiadas conexiones persistentes abiertas simultáneamente. No sé. Ahora vamos depurando código, y cuando todo esté "feten", probaré de hacerlas persistentes. Pero de momento ya hemos bajado a la mitad las conexiones abiertas simultáneas.

      Y si chico, lo siento pero lo de la web quedará en el anonimato !!!! ;-)

      Eliminar
  12. Hola que tal, inedit00
    Tendrás alguna información sobre el acelerador APC de PHP, me gustaría aprender sobre este tema y al ver que eres muy bueno explicando me animo a preguntarte.

    Un Saludo!!!

    ResponderEliminar
    Respuestas
    1. Buenas tardes, Carolina. Pues temo decir que no he conocido APC hasta hace muy poco (no soy gran fan de PHP). Si quieres te comento mis impresiones por mail (mándame un correo a inedit00 [] gmail.com) y estaré encantado de compartir lo poco que sé contigo, pero hace mucho que no escribo en este blog y no es mi intención retomarlo.

      Si crees que te puedo ayudar a aclarar un poco que és y para que sirve APC, lo intento sin problemas. Y si tienes dudas concretas, no dudes en preguntar también. Si no tengo ni idea de por donde van los tiros, ya te lo haré saber.

      Ale, un saludo y hasta pronto.

      P.D: Curro. Y hay veces que tardo en contestar los mails. Tened paciencia.

      Eliminar
  13. Buena tarde Jan,

    Con respecto al tema inicial te hago las siguientes preguntas haber si me aclaras un poco el tema. Resulta que tenemos hospedado con un proveedor de servicios de hosting una base de datos en Mysql en el servidor A, el aplicativo web en php en el servidor B, pero por algun razón tenemos un alto número de intermitencia con las conexiones. Según indica el proveedor se puede deber a un problema con la red nuestra llamese firewall o router. Nos relaciona un log con múltiples conexiones desde la misma ip origen y destino:

    tcp 0 0 ::ffff:10.255.18.26:80 ::ffff:190.25.157.133:2712 TIME_WAIT
    tcp 0 0 ::ffff:10.255.18.26:80 ::ffff:190.25.157.133:4250 TIME_WAIT
    tcp 0 0 ::ffff:10.255.18.26:80 ::ffff:190.25.157.133:3066 TIME_WAIT
    tcp 0 0 ::ffff:10.255.18.26:80 ::ffff:190.25.157.133:3783 TIME_WAIT
    tcp 0 0 ::ffff:10.255.18.26:80 ::ffff:190.25.157.137:45848 TIME_WAIT
    tcp 0 0 ::ffff:10.255.18.26:80 ::ffff:190.25.157.133:1577 TIME_WAIT
    tcp 0 0 ::ffff:10.255.18.26:80 ::ffff:190.25.157.133:3805 TIME_WAIT
    tcp 0 0 ::ffff:10.255.18.26:80 ::ffff:190.25.157.133:2735 TIME_WAIT
    tcp 0 0 ::ffff:10.255.18.26:80 ::ffff:190.25.157.133:4698 TIME_WAIT
    tcp 0 0 ::ffff:10.255.18.26:80 ::ffff:190.25.157.133:4180 TIME_WAIT
    tcp 0 0 ::ffff:10.255.18.26:80 ::ffff:190.25.157.133:3684 TIME_WAIT

    La inquietud que tengo, es si verdaderamente es un tema a nivel de la red LAN o de parámetros de configuración en el codigo php o Apache.

    Gracias y agradezco si tienes alguna informacion.

    ResponderEliminar
  14. Hola Anónimo,

    para comprobar si tenéis problemas de conectividad yo dejaría lanzado un PING entre ambos nodos para ver luego los resultados.En el ping se puede ver si hay un jitter alto (si hay mucha diferencia entre tiempos de respuestas por cada ping - en estadística es la desviación típica) para comprobar si realmente se trata de eso. Cuando terminas el ping puedes ver una estadística de el número de pings fallados, las medias de tiempos, etc.. Lo puedes publicar aqui, si quieres.

    El log que muestras no muestro algo que tenga que ver con el problema. Si cierras la conexión TCP simplemente quedará en estado TIME_WAIT. Me parece perfectamente normal.

    Aún así, no acabo de entender el caso. Entiendo que la IP 10.255.18.26 es un servidor web pero y que lo que viene de la 190.25.157.133 del pfSense no se que significa. Donde está el problema? Que error sale? Como está "petando"?

    Un saludo.

    P.D.: Comentar las cosas por los comentarios me parece genial ya que otra gente puede aprovechar la respuesta pero si quieres podemos comunicarnos vía email a inedit00 [at] gmail [dt] com

    ResponderEliminar
  15. Si el tiempo de vida del estado TIME_WAIT es constante (2*MSL), porqué conexiones en este estado aparecen cuando hay muchas conexiones? basta con una conexión para que pueda apreciarse el estado TIME_WAIT por dos minutos !, sin embargo, en presencia de pocas conexiones no son detectables?.

    Viejo, se agradece tu disposición y claridad para expresar tus conocimientos.

    ResponderEliminar
  16. Hola Anónimo, temo que no acabo de entender bien tu pregunta.

    > porqué conexiones en este estado aparecen cuando hay muchas conexiones?
    Por cada nueva conexión que se abre, una vez cerrada se mandendrá en estado TIME WAIT durante dos minutos.

    > basta con una conexión para que pueda apreciarse el estado TIME_WAIT por dos minutos
    Aqui ya me pierdo un poco. En el resultado de netstat se verán tantos TIME_WAIT como sockets se hayan abiertos. No puede haber dos líneas iguales en el resultado de esta comanda.

    > sin embargo, en presencia de pocas conexiones no son detectables?.
    En presencia de pocas conexiones es detectable igualmente ya que cuando un socket se cierra permanecerá en estado TIME WAIT durante los mencionados 2*MSL.

    Si esto no resuelve tus dudas no dudes en reformularlas. Un saludo!

    ResponderEliminar
  17. Un texto buenisimo, ameno y muy útil. No ,no soy tu abuela, pero te felicito por el post!

    ResponderEliminar
  18. Un texto buenisimo, ameno y muy útil. No ,no soy tu abuela, pero te felicito por el post!

    ResponderEliminar
  19. Buenos dias! Tendras un correo para comunicarnos por esa via?

    ResponderEliminar
  20. Hi Jan,

    I reached here following your article http://krenel.org/tcp-time_wait-and-ephemeral-ports-bad-friends.html and now that I am here I wanted to clear one doubt with you. :-)

    In this example, your curl-client and sever were running on the same machine and it was curl-client that quickly consumed (left in TIME_WAIT state to be precise) all the ephemeral ports right? But in the example you discussed in your blog on krenel.org, there it will be the server that would fail to create the connections with the back-end NoSQL Db right? And thus would eventually fail the incoming requests.

    Is my understanding correct so far?

    If yes I am curious what will be the failure message in this case? Will it be similar to what curl-client received your example in this post.

    Many thanks in advance!!! - Sactiw

    ResponderEliminar
  21. Buenísimo el artículo, estoy desarrollando un protocolo y pensé que había errores hasta que leí esto. Muchas gracias.

    P.D.: Si no te molesta, por favor prestale un poco más de atención a las faltas de ortografía (quedan feas a la vista).

    ResponderEliminar
  22. un año buscando la solución a mi problema y aquí encontré la solución, muchas gracias

    ResponderEliminar