Problemas de diseño: envío de segmentos de datos pequeños a través de TCP con Winsock

Cuando necesite enviar paquetes de datos pequeños a través de TCP, el diseño de la aplicación Winsock es especialmente crítico. Un diseño que no tenga en cuenta la interacción de la confirmación retrasada, el algoritmo Nagle y el almacenamiento en búfer de Winsock puede afectar drásticamente al rendimiento. En este artículo se describen estos problemas mediante un par de estudios de casos. También deriva una serie de recomendaciones para enviar paquetes de datos pequeños de forma eficaz desde una aplicación Winsock.

Versión original del producto: Winsock
Número de KB original: 214397

Información previa

Cuando una pila tcp de Microsoft recibe un paquete de datos, se apaga un temporizador de retraso de 200 ms. Cuando se envía un ACK, se restablece el temporizador de retraso y se iniciará otro retraso de 200 ms cuando se reciba el siguiente paquete de datos. Para aumentar la eficacia tanto en Internet como en las aplicaciones de intranet, la pila TCP usa los criterios siguientes para decidir cuándo enviar un ACK en los paquetes de datos recibidos:

  • Si se recibe el segundo paquete de datos antes de que expire el temporizador de retraso, se envía el ACK.
  • Si hay datos que se enviarán en la misma dirección que el ACK antes de que se reciba el segundo paquete de datos y expire el temporizador de retraso, el ACK se devuelve con el segmento de datos y se envía inmediatamente.
  • Cuando expira el temporizador de retraso, se envía el ACK.

Para evitar que los paquetes de datos pequeños ingieran la red, la pila TCP habilita el algoritmo Nagle de forma predeterminada, que combina un pequeño búfer de datos de varias llamadas de envío y retrasa el envío hasta que se recibe un ACK para el paquete de datos anterior enviado desde el host remoto. Las siguientes son dos excepciones al algoritmo Nagle:

  • Si la pila ha fusionado un búfer de datos mayor que la unidad de transmisión máxima (MTU), se envía inmediatamente un paquete de tamaño completo sin esperar el ACK desde el host remoto. En una red Ethernet, el MTU para TCP/IP es de 1460 bytes.

  • La TCP_NODELAY opción socket se aplica para deshabilitar el algoritmo Nagle para que los paquetes de datos pequeños se entreguen al host remoto sin demora.

Para optimizar el rendimiento en la capa de aplicación, Winsock copia los búferes de datos de las llamadas de envío de la aplicación a un búfer de kernel de Winsock. A continuación, la pila usa su propia heurística (como el algoritmo Nagle) para determinar cuándo poner realmente el paquete en la conexión. Puede cambiar la cantidad de búfer de kernel de Winsock asignado al socket mediante la SO_SNDBUF opción (es 8K de forma predeterminada). Si es necesario, Winsock puede almacenar en búfer más que el tamaño del SO_SNDBUF búfer. En la mayoría de los casos, la finalización del envío en la aplicación solo indica que el búfer de datos de una llamada de envío de aplicación se copia en el búfer del kernel de Winsock y no indica que los datos han alcanzado el medio de red. La única excepción es cuando se deshabilita el almacenamiento en búfer de Winsock estableciendo SO_SNDBUF en 0.

Winsock usa las reglas siguientes para indicar que se ha completado el envío a la aplicación (en función de cómo se invoque el envío, la notificación de finalización podría ser la función que devuelve una llamada de bloqueo, la señalización de un evento o una llamada a una función de notificación, etc.):

  • Si el socket sigue estando dentro de SO_SNDBUF cuota, Winsock copia los datos del envío de la aplicación e indica la finalización del envío a la aplicación.

  • Si el socket supera SO_SNDBUF la cuota y solo hay un envío almacenado en búfer anteriormente todavía en el búfer del kernel de pila, Winsock copia los datos del envío de la aplicación e indica la finalización del envío a la aplicación.

  • Si el socket supera SO_SNDBUF la cuota y hay más de un envío almacenado en búfer anteriormente en el búfer del kernel de pila, Winsock copia los datos del envío de la aplicación. Winsock no indica la finalización del envío a la aplicación hasta que la pila complete los envíos suficientes para devolver el socket dentro de SO_SNDBUF la cuota o solo una condición de envío pendiente.

Caso práctico 1

Un cliente TCP de Winsock debe enviar 10000 registros a un servidor TCP de Winsock para almacenarlos en una base de datos. El tamaño de los registros varía de 20 bytes a 100 bytes de largo. Para simplificar la lógica de la aplicación, el diseño es el siguiente:

  • El cliente solo bloquea el envío. El servidor solo bloquea recv .
  • El socket de cliente establece en SO_SNDBUF 0 para que cada registro salga en un único segmento de datos.
  • El servidor llama a recv en un bucle. El búfer publicado en recv es de 200 bytes para que cada registro se pueda recibir en una recv llamada.

Rendimiento

Durante las pruebas, el desarrollador detecta que el cliente solo puede enviar cinco registros por segundo al servidor. El total de 10000 registros, máximo en 976 kb de datos (10000 * 100 / 1024), tarda más de media hora en enviarse al servidor.

Análisis

Dado que el cliente no establece la TCP_NODELAY opción, el algoritmo Nagle fuerza a la pila TCP a esperar un ACK antes de que pueda enviar otro paquete en la conexión. Sin embargo, el cliente ha deshabilitado el almacenamiento en búfer de Winsock estableciendo la SO_SNDBUF opción en 0. Por lo tanto, las 10000 llamadas de envío deben enviarse y ACK'ed individualmente. Cada ACK se retrasa 200 ms porque se produce lo siguiente en la pila TCP del servidor:

  • Cuando el servidor obtiene un paquete, su temporizador de retraso de 200 ms se apaga.
  • El servidor no necesita devolver nada, por lo que no se puede devolver el ACK.
  • El cliente no enviará otro paquete a menos que se confirme el paquete anterior.
  • El temporizador de retraso en el servidor expira y se devuelve el ACK.

Cómo mejorar

Hay dos problemas con este diseño. En primer lugar, existe el problema del temporizador de retraso. El cliente debe poder enviar dos paquetes al servidor en un plazo de 200 ms. Dado que el cliente usa el algoritmo Nagle de forma predeterminada, solo debe usar el almacenamiento en búfer de Winsock predeterminado y no establecerlo en SO_SNDBUF 0. Una vez que la pila TCP ha fusionado un búfer mayor que la unidad de transmisión máxima (MTU), se envía inmediatamente un paquete de tamaño completo sin esperar el ACK desde el host remoto.

En segundo lugar, este diseño llama a un envío para cada registro de tan pequeño tamaño. El envío de este pequeño tamaño no es eficaz. En este caso, es posible que el desarrollador quiera rellenar cada registro en 100 bytes y enviar 80 registros a la vez desde una llamada de envío de cliente. Para que el servidor sepa cuántos registros se enviarán en total, es posible que el cliente quiera iniciar la comunicación con un encabezado de tamaño fijo que contenga el número de registros que se van a seguir.

Caso práctico 2

Una aplicación cliente TCP de Winsock abre dos conexiones con una aplicación de servidor TCP de Winsock que proporciona el servicio de cotizaciones de cotizaciones. La primera conexión se usa como canal de comandos para enviar el símbolo de stock al servidor. La segunda conexión se usa como canal de datos para recibir la cotización de acciones. Una vez establecidas las dos conexiones, el cliente envía un símbolo de stock al servidor a través del canal de comandos y espera a que la cotización de stock vuelva a través del canal de datos. Envía la siguiente solicitud de símbolo de stock al servidor solo después de que se haya recibido la primera cotización bursátil. El cliente y el servidor no establecen la SO_SNDBUF opción y TCP_NODELAY .

  • Rendimiento

    Durante las pruebas, el desarrollador detecta que el cliente solo puede obtener cinco comillas por segundo.

  • Análisis

    Este diseño solo permite una solicitud de cotización de acciones pendiente a la vez. El primer símbolo de existencias se envía al servidor a través del canal de comandos (conexión) y una respuesta se envía inmediatamente desde el servidor al cliente a través del canal de datos (conexión). A continuación, el cliente envía inmediatamente la segunda solicitud de símbolo de stock y el envío devuelve inmediatamente a medida que el búfer de solicitud de la llamada de envío se copia en el búfer del kernel de Winsock. Sin embargo, la pila TCP de cliente no puede enviar la solicitud desde su búfer de kernel inmediatamente porque aún no se confirma el primer envío a través del canal de comandos. Después de que expire el temporizador de retraso de 200-ms en el canal de comandos del servidor, el ACK para la primera solicitud de símbolo vuelve al cliente. A continuación, la segunda solicitud de presupuesto se envía correctamente al servidor después de retrasarse durante 200 ms. La cotización del segundo símbolo de existencias vuelve inmediatamente a través del canal de datos porque, en este momento, el temporizador de retraso en el canal de datos del cliente ha expirado. El servidor recibe un ACK para la respuesta de presupuesto anterior. (Recuerde que el cliente no pudo enviar una segunda solicitud de cotización de acciones para 200 ms, dando así tiempo para que el temporizador de retraso en el cliente expire y envíe un ACK al servidor). Como resultado, el cliente obtiene la segunda respuesta de comillas y puede emitir otra solicitud de presupuesto, que está sujeta al mismo ciclo.

  • Cómo mejorar

    El diseño de dos conexiones (canal) no es necesario aquí. Si solo usa una conexión para la solicitud y respuesta de cotización de acciones, el ACK para la solicitud de oferta se puede colocar en la respuesta de la oferta y volver inmediatamente. Para mejorar aún más el rendimiento, el cliente podría multiplexar varias solicitudes de cotización en una sola llamada de envío al servidor y el servidor también podría multiplexar varias respuestas de presupuesto en una llamada de envío al cliente. Si el diseño de dos canales unidireccionales es necesario por alguna razón, ambos lados deben establecer la TCP_NODELAY opción para que los paquetes pequeños se puedan enviar inmediatamente sin tener que esperar un ACK para el paquete anterior.

Recomendaciones

Aunque estos dos casos prácticos se fabrican, ayudan a ilustrar algunos de los peores escenarios. Al diseñar una aplicación que implica envíos de segmentos de datos pequeños extensos y recvs, debe tener en cuenta las siguientes directrices:

  • Si los segmentos de datos no son críticos para el tiempo, la aplicación debe fusionarlos en un bloque de datos mayor para pasarlos a una llamada de envío. Dado que es probable que el búfer de envío se copie en el búfer del kernel de Winsock, el búfer no debe ser demasiado grande. Un poco menos de 8K es eficaz. Siempre que el kernel de Winsock obtenga un bloque mayor que el MTU, enviará varios paquetes de tamaño completo y un último paquete con lo que quede. El lado de envío, excepto el último paquete, no será alcanzado por el temporizador de retraso de 200-ms. El último paquete, si resulta ser un paquete con números impares, todavía está sujeto al algoritmo de confirmación retrasada. Si la pila final de envío obtiene otro bloque mayor que el MTU, puede omitir el algoritmo Nagle.

  • Si es posible, evite las conexiones de socket con un flujo de datos unidireccional. Las comunicaciones a través de sockets unidireccionales se ven afectadas más fácilmente por el Nagle y los algoritmos de confirmación retrasada. Si la comunicación sigue una solicitud y un flujo de respuesta, debe usar un único socket para realizar ambos envíos y recvs para que el ACK se pueda colocar en la respuesta.

  • Si todos los segmentos de datos pequeños tienen que enviarse inmediatamente, establezca TCP_NODELAY la opción en el extremo de envío.

  • A menos que desee garantizar que winsock envíe un paquete en la conexión cuando Winsock indique una finalización de envío, no debe establecer en SO_SNDBUF cero. De hecho, el búfer 8K predeterminado se ha determinado heurísticamente para funcionar bien para la mayoría de las situaciones y no debe cambiarlo a menos que haya probado que la nueva configuración de búfer de Winsock le proporciona un mejor rendimiento que el predeterminado. Además, establecer en SO_SNDBUF cero es principalmente beneficioso para las aplicaciones que realizan la transferencia masiva de datos. Incluso entonces, para lograr la máxima eficacia, debe usarlo junto con el almacenamiento en búfer doble (más de un envío pendiente en un momento dado) y la E/S superpuesta.

  • Si no es necesario garantizar la entrega de datos, use UDP.

Referencias

Para obtener más información sobre la confirmación retrasada y el algoritmo Nagle, consulte lo siguiente:

Braden, R.[1989], RFC 1122, Requirements for Internet Hosts--Communication Layers, Internet Engineering Task Force.