¿Cuánto tiempo dura una dirección de socket local TCP que se ha vinculado no disponible después del cierre?

13

En Linux (mis servidores en vivo están en RHEL 5.5; los enlaces LXR a continuación corresponden a la versión del kernel), man 7 ipdice:

Una dirección de socket local TCP que se ha vinculado no estará disponible durante algún tiempo después del cierre, a menos que se haya establecido el indicador SO_REUSEADDR.

No estoy usando SO_REUSEADDR. ¿Cuánto dura "algún tiempo"? ¿Cómo puedo saber cuánto dura y cómo puedo cambiarlo?

He estado buscando en esto, y he encontrado algunos bocados de información, ninguno de los cuales realmente explica esto desde la perspectiva de un programador de aplicaciones. Esto es:

  • TCP_TIMEWAIT_LEN en net/tcp.hes "cuánto tiempo esperar para destruir el estado TIME-WAIT", y se fija en "aproximadamente 60 segundos"
  • / proc / sys / net / ipv4 / tcp_fin_timeout es "Hora de mantener el socket en el estado FIN-WAIT-2, si fue cerrado por nuestro lado", y "El valor predeterminado es 60 segundos"

Donde me tropiezo es para cerrar la brecha entre el modelo del núcleo del ciclo de vida TCP y el modelo de puertos del programador que no están disponibles, es decir, para comprender cómo estos estados se relacionan con "algún tiempo".

Tom Anderson
fuente
@Caleb: ¡En cuanto a las etiquetas, bind también es una llamada al sistema! Intenta man 2 bindsi no me crees. Es cierto que probablemente no es lo primero que piensan las personas de Unix cuando alguien dice "atar", por lo que es bastante justo.
Tom Anderson
Conocía bien los usos alternativos de bind, pero la etiqueta aquí se aplica específicamente al servidor DNS. No tenemos etiquetas para cada posible llamada al sistema.
Caleb

Respuestas:

14

Creo que la idea de que el socket no esté disponible para un programa es permitir que lleguen los segmentos de datos TCP aún en tránsito y que el núcleo los descarte. Es decir, es posible que una aplicación llame close(2)a un zócalo, pero los retrasos en el enrutamiento o los percances para controlar los paquetes o lo que tenga puede permitir que el otro lado de una conexión TCP envíe datos por un tiempo. La aplicación ha indicado que ya no quiere tratar con segmentos de datos TCP, por lo que el núcleo debería descartarlos a medida que entran.

He pirateado un pequeño programa en C que puedes compilar y usar para ver cuánto dura el tiempo de espera:

#include <stdio.h>        /* fprintf() */
#include <string.h>       /* strerror() */
#include <errno.h>        /* errno */
#include <stdlib.h>       /* strtol() */
#include <signal.h>       /* signal() */
#include <sys/time.h>     /* struct timeval */
#include <unistd.h>       /* read(), write(), close(), gettimeofday() */
#include <sys/types.h>    /* socket() */
#include <sys/socket.h>   /* socket-related stuff */
#include <netinet/in.h>
#include <arpa/inet.h>    /* inet_ntoa() */
float elapsed_time(struct timeval before, struct timeval after);
int
main(int ac, char **av)
{
        int opt;
        int listen_fd = -1;
        unsigned short port = 0;
        struct sockaddr_in  serv_addr;
        struct timeval before_bind;
        struct timeval after_bind;

        while (-1 != (opt = getopt(ac, av, "p:"))) {
                switch (opt) {
                case 'p':
                        port = (unsigned short)atoi(optarg);
                        break;
                }
        }

        if (0 == port) {
                fprintf(stderr, "Need a port to listen on\n");
                return 2;
        }

        if (0 > (listen_fd = socket(AF_INET, SOCK_STREAM, 0))) {
                fprintf(stderr, "Opening socket: %s\n", strerror(errno));
                return 1;
        }

        memset(&serv_addr, '\0', sizeof(serv_addr));
        serv_addr.sin_family      = AF_INET;
        serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
        serv_addr.sin_port        = htons(port);

        gettimeofday(&before_bind, NULL);
        while (0 > bind(listen_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr))) {
                fprintf(stderr, "binding socket to port %d: %s\n",
                        ntohs(serv_addr.sin_port),
                        strerror(errno));

                sleep(1);
        }
        gettimeofday(&after_bind, NULL);
        printf("bind took %.5f seconds\n", elapsed_time(before_bind, after_bind));

        printf("# Listening on port %d\n", ntohs(serv_addr.sin_port));
        if (0 > listen(listen_fd, 100)) {
                fprintf(stderr, "listen() on fd %d: %s\n",
                        listen_fd,
                        strerror(errno));
                return 1;
        }

        {
                struct sockaddr_in  cli_addr;
                struct timeval before;
                int newfd;
                socklen_t clilen;

                clilen = sizeof(cli_addr);

                if (0 > (newfd = accept(listen_fd, (struct sockaddr *)&cli_addr, &clilen))) {
                        fprintf(stderr, "accept() on fd %d: %s\n", listen_fd, strerror(errno));
                        exit(2);
                }
                gettimeofday(&before, NULL);
                printf("At %ld.%06ld\tconnected to: %s\n",
                        before.tv_sec, before.tv_usec,
                        inet_ntoa(cli_addr.sin_addr)
                );
                fflush(stdout);

                while (close(newfd) == EINTR) ;
        }

        if (0 > close(listen_fd))
                fprintf(stderr, "Closing socket: %s\n", strerror(errno));

        return 0;
}
float
elapsed_time(struct timeval before, struct timeval after)
{
        float r = 0.0;

        if (before.tv_usec > after.tv_usec) {
                after.tv_usec += 1000000;
                --after.tv_sec;
        }

        r = (float)(after.tv_sec - before.tv_sec)
                + (1.0E-6)*(float)(after.tv_usec - before.tv_usec);

        return r;
}

Probé este programa en 3 máquinas diferentes, y obtengo un tiempo variable, entre 55 y 59 segundos, cuando el núcleo se niega a permitir que un usuario no root vuelva a abrir un socket. Compilé el código anterior en un ejecutable llamado "abridor", y lo ejecuté así:

./opener -p 7896; ./opener -p 7896

Abrí otra ventana e hice esto:

telnet otherhost 7896

Eso hace que la primera instancia de "abridor" acepte una conexión y luego la cierre. La segunda instancia de "abridor" intenta al bind(2)puerto TCP 7896 cada segundo. "abridor" reporta 55 a 59 segundos de retraso.

Buscando en Google, encuentro que la gente recomienda hacer esto:

echo 30 > /proc/sys/net/ipv4/tcp_fin_timeout

para reducir ese intervalo. No me funcionó. De las 4 máquinas Linux a las que tenía acceso, dos tenían 30 y dos tenían 60. También establecí ese valor tan bajo como 10. No hay diferencia con el programa "abridor".

Haciendo esto:

echo 1 > /proc/sys/net/ipv4/tcp_tw_recycle

Cambió las cosas. El segundo "abridor" solo tardó unos 3 segundos en obtener su nuevo zócalo.

Bruce Ediger
fuente
3
Entiendo (aproximadamente) cuál es el propósito del período de indisponibilidad. Lo que me gustaría saber es exactamente cuánto tiempo dura ese período en Linux y cómo se puede cambiar. El problema con un número de una página de Wikipedia sobre TCP es que es necesariamente un valor generalizado, y no algo que sea definitivamente cierto en mi plataforma específica.
Tom Anderson
¡Tus especulaciones fueron interesantes! simplemente márquelos como eso con un título en lugar de eliminarlos, ¡le da formas de buscar el motivo!
Philippe Gachoud