Red de conocimiento informático - Problemas con los teléfonos móviles - Código fuente de comunicación UDP

Código fuente de comunicación UDP

Primero, supongamos:

Medir paquetes por segundo (pps) es más interesante que medir bytes por segundo (Bps). Puede obtener Bps más altos canalizando mejor y enviando paquetes más largos. Por el contrario, es mucho más difícil mejorar los pps.

Como estamos interesados ​​en pps, nuestros experimentos utilizarán mensajes UDP más cortos. Para ser precisos, es una carga útil UDP de 32 bytes, lo que equivale a 74 bytes en la capa Ethernet.

En el experimento utilizaremos dos servidores físicos: "receptor" y "remitente".

Ambos cuentan con dos procesadores Xeon de seis núcleos a 2 GHz. Cada servidor tiene Hyper-Threading (HT) habilitado para 24 procesadores, utilizando la tarjeta de red multicola 10G de Solarflare y una configuración de 11 colas de recepción. Más sobre esto más adelante.

El código fuente del programa de prueba es: udpsender y udpreceiver.

Principio básico

Utilizamos 4321 como puerto para paquetes UDP. Antes de comenzar, debemos asegurarnos de que la transmisión no será interferida por iptables:

shell

receptor $iptables -entramos 1 -p udp -dport 4321 -j aceptar

Receptor $ iptables -t raw -I pre-ruta 1-p UDP-dport 4321-j no track

Para facilitar las pruebas posteriores, definimos claramente la dirección IP:

shell

El receptor $ de I en "seq 1 20"; do

dirección IP agrega 192.168.254. $ I/24 dev eth 2;

Completo

Dirección IP del remitente agregar 192.168.254 30/24 dev eth 3

1. /p>

Al principio, hicimos algunos experimentos sencillos. ¿Cuántos paquetes se transmitirán simplemente enviando y recibiendo?

Pseudocódigo para simular remitente:

Lenguaje de programación informática

fd = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

fd.bind(("0.0.0.0 ", 65400)) #Seleccione el puerto de origen para reducir la incertidumbre

FD connect((("192.168 . 254 . 1", 4321))

Aunque correcto:

FD . send mmsg([" x00 " * 32]* 1024)

Debido a que utilizamos el envío de la llamada general al sistema, la eficiencia no es muy alta. . El cambio de contexto al kernel es costoso, por lo que es mejor evitarlo. Afortunadamente, Linux agregó recientemente una conveniente llamada al sistema sendmmsg que nos permite enviar múltiples paquetes en una sola llamada.

Pseudocódigo. simular el receptor:

Lenguaje de programación informática

fd = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

FD bind(((" 0. 0 . 0 . 0 ", 4321))

Aunque correcto:

Paquete=[Ninguno] * 1024

fd.recvmmsg(Paquete, MSG_WAITFORONE)

Del mismo modo, recvmmsg es una versión más eficiente de la llamada al sistema que recv normal.

Probémoslo:

shell

remitente $./UDP remitente 192.168.254.1:4321

receptor $.

/Receptor UDP 1 0 . p>0.262 MB PPS 7.991 MB/67.033 MB

0.199 MB PPS 6.081 MB/51.01.03 MB

0.195 Megabyte/49.966Mbyte

0.199Mpps 6.060MiBMbyte /50.836Mbyte

0.200Mpps 6.097MiBMbyte/51.147Mbyte

p>

0.197 MB/50.509 MB

Las pruebas muestran que se pueden alcanzar entre 197.000 y 350.000 PPS logrado de la forma más sencilla. Se ve bien, pero lamentablemente es muy inestable. Esto se debe a que el kernel intercambia nuestro programa entre núcleos, por lo que será útil adjuntar el proceso a la CPU.

shell

Remitente $taskset -c 1. /Remitente UDP 192.168.254 1:4321

Receptor $taskset -c 1. /Receptor UDP 1 0 0 0 0:4321

0,362 MB PPS 11,058 MB/92,760 MB

0,374 m PPS 11,411 MIB/95,723 MB 0,369 MB PPS 11,252 MB/94,389 MB

0,370 MB PPS 11,289 MB/94,696 MB

0,365 MB PPS 11,152 MB/ 93,552 MB

0,360 MB PPS 10,971 MB / 92.033 MB

El programador del kernel ahora ejecuta procesos en CPU específicas, mejora el caché del procesador y hace que los datos sean más consistentes, ¡eso es lo que queremos!

2. Envíe más paquetes

Aunque 370k pps es bueno para un programa simple, todavía está lejos de nuestro objetivo de 1Mpps. Para recibir más, primero debemos enviar más paquetes. ¿Qué tal si enviamos dos hilos separados?

Shell

Remitente $ conjunto de tareas -c 1, 2. /udpsender

192.168.254.1:4321 192.168.254.1:4321

Receptor $taskset -c 1.

/Receptor UDP 1 0 . 0 0 0:4321

0.349 MB PPS 10.651 MB/89.343 MB

0.354 MB PPS 10.815 MB/90.724 MB Sección

0,354 MB PPS 10,806 MB/90,646 MB

0,354 MB PPS 10,811 MB/90,690 MB

Los datos en el extremo receptor no se agregan, el comando ethtool –s mostrará dónde los paquetes realmente van:

shell

receiver $ watch ' sudo ethtool -S eth 2 | grep rx '

rx _ nodesc _ drop _ CNT:451.3k /s

rx-0 .rx _ paquetes:8.0/s

rx-1 .rx _ paquetes:0.0/s

p>

rx-2. rx _ paquetes: 0,0/segundo

rx-3 rx _ paquetes: 0,5/segundo

rx-4.rx_packets: 355,2k/s

rx-5 rx _ paquetes:0.0/segundo

rx-6 rx _ paquetes:0.0/segundo

rx_paquetes:0.5/segundo

rx-8 . rx _ paquetes:0.0/segundo

rx-9 .rx _ paquetes:0.0/segundo

rx- 10. rx_packets:0.0/seg<. /p>

Con estas estadísticas, la NIC muestra que la cola RX 4 ha transmitido exitosamente aproximadamente 350 Kpps. Rx_nodesc_drop_cnt es el contador único de Solarflare que indica que la NIC no pudo enviar 450 kpps al núcleo.

A veces, el motivo por el que no se envían estos paquetes no está muy claro, pero en nuestro caso, sí lo está: la cola RX número 4 envía paquetes a la CPU número 4, pero la CPU número 4 está demasiado ocupada porque Solo puede leer 350 kpps en su momento más ocupado. Se muestra en htop como:

Curso intensivo de tarjeta de red de múltiples colas

Históricamente, la tarjeta de red tiene solo una cola RX, que se utiliza para transmitir paquetes entre el hardware y el kernel. Una limitación obvia de este diseño es que no es posible manejar más paquetes de los que puede manejar una sola CPU.

Para aprovechar los sistemas multinúcleo, las tarjetas de red comenzaron a admitir múltiples colas de recepción. El diseño es simple: cada cola RX está conectada a una CPU separada, por lo que la tarjeta de red que envía paquetes a todas las colas RX puede usar todas las CPU. Pero surge otra pregunta: ¿Cómo decide la NIC a qué cola RX enviar el paquete?

El equilibrio por turnos no es aceptable porque puede provocar la reordenación de paquetes dentro de una única conexión. Otro método consiste en utilizar el hash del paquete para determinar el número de RX. El valor hash generalmente se calcula a partir de una tupla (IP de origen, IP de destino, puerto de origen, puerto de destino). Esto garantiza que los paquetes generados a partir de una secuencia terminarán exactamente en la misma cola RX y que no es posible reorganizar los paquetes dentro de una secuencia.

En nuestro ejemplo, el valor hash podría verse así:

shell

1

RX_queue_number = hash(' 192.168.254 30 ', ' 192.168.254 . 1 ', 65400, 4321) % número de colas

Algoritmo hash de múltiples colas

El algoritmo hash está configurado por ethtool, configurado de la siguiente manera. :

shell

receiver $ ethtool -n eth2 rx-flow-hash udp4

Los flujos UDP en IPV4 utilizan estos campos para calcular la clave de flujo hash:

IP SA

IP DA

Para paquetes IPv4 UDP, la NIC realizará un hash de las direcciones (IP de origen, IP de destino). Eso es

shell

1

RX _ cola _ número = hash(' 192.168.254 . 30 ', ' 192.168.254 . 1 ')% número _ de _ colas

Esto es muy limitado ya que ignora los números de puerto. Muchas tarjetas de red permiten hashes personalizados. Asimismo, usando ethtool, podemos seleccionar una tupla (IP de origen, IP de destino, puerto de origen y puerto de destino) para generar un valor hash.

shell

receptor $eth herramienta -N eth 2 rx-flow-hash UDP 4 sdfn

No se puede cambiar la opción de hash de flujo de red RX: no se admite esta operación

Desafortunadamente, nuestra NIC no admite personalización y solo podemos elegir (IP de origen, IP de destino) para generar hash.

Informe de rendimiento de NUMA

Hasta ahora, todos nuestros paquetes fluyen a una cola RX y una CPU. Podemos usar esto como punto de referencia para medir el rendimiento de diferentes CPU. Hay dos procesadores separados en la máquina host que configuramos como receptor, cada uno de los cuales es un nodo NUMA diferente.

En nuestra configuración, conecta un receptor de un solo subproceso a una de las cuatro CPU. Las cuatro opciones son las siguientes:

El receptor se ejecuta en otra CPU, pero se utiliza el mismo nodo NUMA como cola RX. Desde arriba podemos ver que el rendimiento ronda los 360 kpps.

Utilizando la misma CPU que ejecuta el receptor como la cola RX, podemos obtener unos 430 kpps. Sin embargo, esto también tendrá una alta inestabilidad. Si la tarjeta de red está inundada de paquetes, el rendimiento caerá a cero.

Cuando el receptor se ejecuta en la CPU correspondiente al HT que procesa la cola RX, el rendimiento es la mitad de lo habitual, unos 200 kpps.

El receptor está en un nodo NUMA diferente al de la CPU de la cola RX y su rendimiento es de unos 330 kpps. Pero las cifras variarán.

Si bien ejecutar en diferentes nodos NUMA cuesta un 10%, lo que puede no parecer tan malo, el problema solo empeora a medida que aumenta la escala. En algunas pruebas, cada núcleo solo pudo emitir 250 kpps y en todas las pruebas realizadas en la NUMA, la inestabilidad fue muy mala. En el caso de un mayor rendimiento, la pérdida de rendimiento de los nodos NUMA es más obvia. En una prueba, se descubrió que cuando el receptor se ejecutaba en un nodo NUMA desconectado, su rendimiento se reducía en un factor de 4.

3. Recibir más IP.

Debido a las limitaciones del algoritmo hash de nuestra tarjeta de red, la única forma de distribuir paquetes a través de la cola RX es utilizar múltiples direcciones IP. A continuación se explica cómo enviar paquetes a diferentes IP de destino:

1

Remitente $ taskset -c 1, 2.

/Remitente UDP 192.168.254 1:4321 192.168.254 2:4321

Ethtool confirma que el paquete de datos fluye a diferentes colas de recepción:

shell

receptor $ reloj ' sudo ethtool-S eth 2 | grep rx '

rx-0 paquetes:8.0/sec

rx_paquetes:0.0/. seg

rx-2 .rx _ paquetes:0.0/s

rx-3.rx_packets: 355.2k/s

rx_paquetes. :0.5 /seg

rx-5.rx_packets: 297.0k/s

rx-6 rx _ paquetes: 0.0/seg

rx-7. rx _ paquetes:0.5/segundo

rx-8 rx _ paquetes:0.0/segundo

rx_paquetes:0.0/segundo

. rx- 10. rx_packets: 0.0/seg

Parte de recepción:

shell

Receptor $taskset -c 1. /Receptor UDP 1 0 0 0 0:4321

0,609 MB PPS 18,599 MB/156,019 MB

0,657 MB pps 20,039 MiB MB/168,102 MB Bytes

0.649 MB PPS 19.803 MB/166.120 MB

¡Hurra! Con dos núcleos ocupados procesando la cola RX, el tercero puede alcanzar alrededor de 650 kpps al ejecutar la aplicación.

Podemos aumentar este número enviando datos a tres o cuatro colas RX, pero pronto la aplicación tendrá otro cuello de botella. Rx_nodesc_drop_cnt no aumentó este tiempo, pero netstat recibió el siguiente error:

shell

Receptor $ watch 'netstat -s - udp '

Udp: p>

Se recibieron paquetes de 437,0k/s

Se recibieron paquetes de 0,0/s a un puerto desconocido.

Error de recepción de paquetes de 386,9k/s

0,0 paquetes enviados por segundo

RcvbufErrors: 123,8k/s

SndbufErrors: 0

Número de errores del consumidor: 0

Esto significa que aunque la NIC puede enviar paquetes al kernel, el kernel no puede enviar paquetes a la aplicación. En nuestro caso sólo se pueden proporcionar 440 kpps, los 390 kpps restantes + 123 kpps se reducirán porque la aplicación no puede recibirlos lo suficientemente rápido.

4. Recepción multiproceso

Necesitamos ampliar la aplicación del receptor. La forma más sencilla es utilizar la recepción multiproceso, pero no funciona:

shell

sender $taskset -c 1, 2. /Remitente UDP 192.168.254 1:4321 192.168.254 2:4321

Receptor $taskset -c 1,2.

/udpreceiver 1 0 0 0:4321 2

0,495 MB PPS 15,108 MB/126,733 MB

0,480 MB PPS 14,636 MB/122,775 MB

0,461M PPS 14.071 MIB/118.038 MB

0.486M PPS 14.820Mbyte/124.322MB

El rendimiento de recepción es menor que el de un solo subproceso, lo cual se debe a una contención de bloqueo en el UDP. búfer de recepción. Debido a que ambos subprocesos utilizan el mismo descriptor de socket, pasan demasiado tiempo compitiendo por el bloqueo del búfer de recepción UDP. Este artículo describe este problema en detalle.

Parece que usar múltiples hilos para recibir del descriptor no es la mejor solución.

5.SO_REUSEPORT

Afortunadamente, Linux agregó recientemente una solución: el indicador del puerto so_reuse. Cuando este indicador se establece en un descriptor de socket, Linux permitirá que muchos procesos se vinculen al mismo puerto. De hecho, se permitirá vincular cualquier número de procesos y la carga se distribuirá uniformemente.

Con SO_REUSEPORT, cada proceso tiene un descriptor de socket independiente. Entonces cada uno tiene un buffer de recepción UDP dedicado. Esto evita los problemas de competencia encontrados anteriormente:

Shell

1

2

4

Receptor $taskset -c 1, 2, 3, 4. /Receptor UDP 1 0 0 0 0:4321 4 1

1.114M PPS 34.007 MIB/285.271Mb

1.147MB/293.518MB

1.126. Mpps 34.374MiBMBytes/288.354MBytes

Me gusta aún más ahora, ¡el rendimiento es excelente!

Más encuestas muestran que hay margen para seguir mejorando. Incluso si iniciamos cuatro subprocesos de recepción, la carga no se distribuye uniformemente:

Dos procesos reciben todo el trabajo, mientras que los otros dos procesos no reciben ningún paquete. Esto se debe a una colisión de hash, pero esta vez en el nivel SO_REUSEPORT.

Conclusión

Hice más pruebas. Utilizando una cola RX totalmente consistente, el hilo de recepción puede alcanzar 1,4Mpps en un solo nodo NUMA. Ejecutar el receptor en un nodo NUMA diferente hará que este número caiga a 1Mpps.

En resumen, si desea un rendimiento perfecto, debe hacer lo siguiente:

Asegúrese de que el tráfico se distribuya uniformemente entre muchas colas RX y procesos SO_REUSEPORT. En la práctica, siempre que hay muchas conexiones (o flujos), la carga suele estar distribuida.

Requiere suficiente potencia de CPU para obtener paquetes del kernel.

Para hacer las cosas más difíciles, la cola de recepción y el proceso de recepción deben estar en el mismo nodo NUMA.

Para hacer las cosas más estables, la cola RX y el proceso de recepción deben estar en el mismo nodo NUMA.

Aunque hemos demostrado que es técnicamente posible recibir 1Mpps en una máquina Linux, la aplicación no realiza ningún procesamiento real de los paquetes recibidos; ni siquiera mira el tráfico de contenido. No esperes demasiado de esta interpretación, ya que no será de gran utilidad para ninguna aplicación práctica.