Quiero hablar sobre algo como
DPDK : este es un marco para trabajar con una red sin pasar por el núcleo. Es decir Puede escribir directamente desde userland \ para leer en la cola de la tarjeta de red, sin necesidad de realizar ninguna llamada al sistema. Esto le ahorra muchos gastos generales para copiar y más. Como ejemplo, escribiré una aplicación que ofrece una página de prueba a través de http y compararé su velocidad con nginx.
DPDK se puede descargar
aquí . No tome Stable: no funcionó para mí en EC2, tome 18.05, todo comenzó con eso. Antes de comenzar, debe reservar
grandes páginas en el
sistema para el funcionamiento normal del marco. En principio, las aplicaciones de prueba se pueden iniciar con la opción de trabajar sin grandes páginas, pero siempre las incluí. * No olvides update-grub después de grub-mkconfig * Una vez que hayas terminado con las páginas enormes, ve inmediatamente a ./usertools/dpdk-setup.py: esta cosa recopilará y configurará todo lo demás. En Google puede encontrar instrucciones que recomiendan recopilar y configurar algo para omitir dpdk-setup.py; no haga esto. Bueno, puedes hacerlo, solo conmigo, aunque no usé dpdk-setup.py, nada funcionó. Brevemente, la secuencia de acciones dentro de dpdk-setup.py:
- construir x86_x64 linux
- cargar el módulo kernel igb uio
- mapear hugepages a / mnt / huge
- enlace el nic deseado en uio (no olvide hacer ifconfig ethX hacia abajo antes)
Después de eso, puede crear un ejemplo ejecutando make en el directorio con él. Solo es necesario crear una variable de entorno RTE_SDK que apunte a un directorio con DPDK.
Aquí yace el código de ejemplo completo. Consiste en la inicialización, la implementación de la versión primitiva de tcp / ip y el analizador http primitivo. Comencemos con la inicialización.
int main(int argc, char** argv) { int ret;
En ese momento, cuando a través de dpdk-setup.py vinculamos la interfaz de red seleccionada al controlador dpdk, esta interfaz de red deja de ser accesible para el núcleo. Después de eso, la tarjeta de red registrará todos los paquetes que lleguen a esta interfaz a través del DMA en la cola que le proporcionamos.
Y aquí está el ciclo de procesamiento de paquetes.
struct rte_mbuf* packets[MAX_PACKETS]; uint16_t rx_current_queue = 0; while (1) {
La función rte_eth_rx_burst se usa para leer paquetes de la cola.Si hay algo en la cola, leerá los paquetes y los colocará en una matriz. Si no hay nada en la cola, se devolverá 0, en este caso, debe volver a llamarlo de inmediato. Sí, este enfoque "gasta" el tiempo de CPU en nada si actualmente no hay datos en la red, pero si ya tomamos dpdk, entonces se supone que este no es nuestro caso. * Importante, la
función no es segura para subprocesos , no se puede leer desde la misma cola en diferentes procesos * Después de procesar el paquete, se debe llamar a rte_pktmbuf_free. Para enviar un paquete, puede usar la función rte_eth_tx_burst, que coloca rte_mbuf recibido de rte_pktmbuf_alloc en la cola de la tarjeta de red.
Después de separar los encabezados del paquete, será necesario crear una sesión tcp. El protocolo tcp está repleto de varios casos especiales, situaciones especiales y peligros de denegación de servicio. La implementación de un tcp más o menos completo es un ejercicio excelente para un desarrollador experimentado, pero no está incluido en el marco descrito aquí. En el ejemplo, tcp se implementa lo suficiente para realizar pruebas. Implementé una tabla de sesión basada en la
tabla hash suministrada con dpdk , configurando y rompiendo una conexión tcp, transmitiendo y recibiendo datos sin tener en cuenta las pérdidas y el reordenamiento de paquetes. La tabla hash de dpdk tiene una limitación importante que puede leer pero no puede escribir en varios subprocesos. El ejemplo está hecho de un solo subproceso y este problema no es importante aquí, y en caso de procesar el tráfico en varios núcleos, puede usar RSS, enviar una tabla hash y hacerlo sin bloquear.
Implementación TCP tatic void process_tcp(struct rte_mbuf* m, struct tcp_hdr* tcp_header, struct tcp_key* key, void* data, size_t data_size) { TRACE; struct tcp_state* state; if (rte_hash_lookup_data(g_clients, key, (void**)&state) < 0)
El analizador http solo admitirá GET para leer las URL desde allí y devolver html con la URL solicitada.
Analizador HTTP static void feed_http(void* data, size_t data_size, struct tcp_state* state) { TRACE; size_t remaining_data = data_size; char* current = (char*)data; struct http_state* http = &state->http; if (http->state == HTTP_BAD_STATE) { TRACE; return; } while (remaining_data > 0) { switch(http->state) { case HTTP_START: { if (*current == 'G') { http->state = HTTP_READ_G; } else { http->state = HTTP_BAD_STATE; } break; } case HTTP_READ_G: { if (*current == 'E') { http->state = HTTP_READ_E; } else { http->state = HTTP_BAD_STATE; } break; } case HTTP_READ_E: { if (*current == 'T') { http->state = HTTP_READ_T; } else { http->state = HTTP_BAD_STATE; } break; } case HTTP_READ_T: { if (*current == ' ') { http->state = HTTP_READ_SPACE; } else { http->state = HTTP_BAD_STATE; } break; } case HTTP_READ_SPACE: { if (*current != ' ') { http->request_url[http->request_url_size] = *current; ++http->request_url_size; if (http->request_url_size > MAX_URL_SIZE) { http->state = HTTP_BAD_STATE; } } else { http->state = HTTP_READ_URL; http->request_url[http->request_url_size] = '\0'; } break; } case HTTP_READ_URL: { if (*current == '\r') { http->state = HTTP_READ_R1; } break; } case HTTP_READ_R1: { if (*current == '\n') { http->state = HTTP_READ_N1; } else if (*current == '\r') { http->state = HTTP_READ_R1; } else { http->state = HTTP_READ_URL; } break; } case HTTP_READ_N1: { if (*current == '\r') { http->state = HTTP_READ_R2; } else { http->state = HTTP_READ_URL; } break; } case HTTP_READ_R2: { if (*current == '\n') { TRACE; char content_length[32]; sprintf(content_length, "%lu", g_http_part2_size - 4 + http->request_url_size + g_http_part3_size); size_t content_length_size = strlen(content_length); size_t total_data_size = g_http_part1_size + g_http_part2_size + g_http_part3_size + http->request_url_size + content_length_size; struct tcp_hdr* tcp_header; struct rte_mbuf* packet = build_packet(state, total_data_size, &tcp_header); if (packet != NULL) { tcp_header->rx_win = TX_WINDOW_SIZE; tcp_header->sent_seq = htonl(state->my_seq_sent); tcp_header->recv_ack = htonl(state->remote_seq + data_size); #ifdef KEEPALIVE state->my_seq_sent += total_data_size; #else state->my_seq_sent += total_data_size + 1;
Una vez que el ejemplo esté listo, puede comparar el rendimiento con nginx. Porque No puedo armar un verdadero stand en casa, utilicé Amazon EC2. EC2 hizo sus correcciones en las pruebas. Tuve que abandonar Connection: cerrar solicitudes, porque En algún lugar a 300k rps, los paquetes SYN comenzaron a caer unos segundos después del inicio de la prueba. Aparentemente, hay algún tipo de protección contra la inundación SYN, por lo que las solicitudes se hicieron para mantener la vida. En EC2, dpdk no funciona en todas las instancias, por ejemplo en m1.medium, no funcionó. El stand utilizó 1 instancia r4.8xlarge con la aplicación y 2 instancias r4.8xlarge para crear una carga. Se comunican en las interfaces de red del hotel a través de una subred VPC privada. Traté de cargar con diferentes utilidades: ab, wrk, h2load, siege. Lo más conveniente era wrk, porque ab es de un solo subproceso y produce estadísticas distorsionadas si hay errores en la red.
Con mucho tráfico en EC2, se puede observar un cierto número de caídas, para aplicaciones ordinarias esto será invisible, pero en el caso de ab, cualquier retransmisión ocupa el tiempo total y ab, como resultado de lo cual los datos sobre el número promedio de solicitudes por segundo son inutilizables. Las razones de las caídas son un misterio por separado, sin embargo, el hecho de que haya problemas no solo cuando se usa dpdk, sino también con nginx, sugiere que esto no parece ser un ejemplo de algo mal.
Realicé la prueba en dos etapas, primero ejecuté wrk en 1 instancia, luego en 2. Si el rendimiento total de 2 instancias es 1, significa que no me encontré con el rendimiento de wrk.
Resultado de la prueba del ejemplo dpdk en r4.8xlargelanzar wrk c 1 instancia
root@ip-172-30-0-127:~# wrk -t64 -d10s -c4000 --latency http://172.30.4.10/testu
Running 10s test @ http://172.30.4.10/testu
64 threads and 4000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 32.19ms 63.43ms 817.26ms 85.01%
Req/Sec 15.97k 4.04k 113.97k 93.47%
Latency Distribution
50% 2.58ms
75% 17.57ms
90% 134.94ms
99% 206.03ms
10278064 requests in 10.10s, 1.70GB read
Socket errors: connect 0, read 17, write 0, timeout 0
Requests/sec: 1017645.11
Transfer/sec: 172.75MB
Ejecutar wrk desde 2 instancias al mismo tiempo
root@ip-172-30-0-127:~# wrk -t64 -d10s -c4000 --latency http://172.30.4.10/testu
Running 10s test @ http://172.30.4.10/testu
64 threads and 4000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 67.28ms 119.20ms 1.64s 88.90%
Req/Sec 7.99k 4.58k 132.62k 96.67%
Latency Distribution
50% 2.31ms
75% 103.45ms
90% 191.51ms
99% 563.56ms
5160076 requests in 10.10s, 0.86GB read
Socket errors: connect 0, read 2364, write 0, timeout 1
Requests/sec: 510894.92
Transfer/sec: 86.73MB
root@ip-172-30-0-225:~# wrk -t64 -d10s -c4000 --latency http://172.30.4.10/testu
Running 10s test @ http://172.30.4.10/testu
64 threads and 4000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 74.87ms 148.64ms 1.64s 93.45%
Req/Sec 8.22k 2.59k 42.51k 81.21%
Latency Distribution
50% 2.41ms
75% 110.42ms
90% 190.66ms
99% 739.67ms
5298083 requests in 10.10s, 0.88GB read
Socket errors: connect 0, read 0, write 0, timeout 148
Requests/sec: 524543.67
Transfer/sec: 89.04MB
Nginx dio tales resultadoslanzar wrk c 1 instancia
root@ip-172-30-0-127:~# wrk -t64 -d10s -c4000 --latency http://172.30.4.10/testu
Running 10s test @ http://172.30.4.10/testu
64 threads and 4000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 14.36ms 56.41ms 1.92s 95.26%
Req/Sec 15.27k 3.30k 72.23k 83.53%
Latency Distribution
50% 3.38ms
75% 6.82ms
90% 10.95ms
99% 234.99ms
9813464 requests in 10.10s, 2.12GB read
Socket errors: connect 0, read 1, write 0, timeout 3
Requests/sec: 971665.79
Transfer/sec: 214.94MB
Ejecutar wrk desde 2 instancias al mismo tiempo
root@ip-172-30-0-127:~# wrk -t64 -d10s -c4000 --latency http://172.30.4.10/testu
Running 10s test @ http://172.30.4.10/testu
64 threads and 4000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 52.91ms 82.19ms 1.04s 82.93%
Req/Sec 8.05k 3.09k 55.62k 89.11%
Latency Distribution
50% 3.66ms
75% 94.87ms
90% 171.83ms
99% 354.26ms
5179253 requests in 10.10s, 1.12GB read
Socket errors: connect 0, read 134, write 0, timeout 0
Requests/sec: 512799.10
Transfer/sec: 113.43MB
root@ip-172-30-0-225:~# wrk -t64 -d10s -c4000 --latency http://172.30.4.10/testu
Running 10s test @ http://172.30.4.10/testu
64 threads and 4000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 64.38ms 121.56ms 1.67s 90.32%
Req/Sec 7.30k 2.54k 34.94k 82.10%
Latency Distribution
50% 3.68ms
75% 103.32ms
90% 184.05ms
99% 561.31ms
4692290 requests in 10.10s, 1.01GB read
Socket errors: connect 0, read 2, write 0, timeout 21
Requests/sec: 464566.93
Transfer/sec: 102.77MB
nginx configusuario www-data;
trabajador_procesos auto;
pid /run/nginx.pid;
worker_rlimit_nofile 50000;
eventos {
trabajador_conexiones 10000;
}
http {
enviar archivo;
tcp_nopush en;
tcp_nodelay on;
keepalive_timeout 65;
tipos_hash_max_size 2048;
incluir /etc/nginx/mime.types;
default_type text / plain;
error_log /var/log/nginx/error.log;
acceso_log desactivado;
servidor {
escuchar 80 default_server backlog = 10000 reuseport;
ubicación / {
devuelva 200 "answer.padding _____________________________________________________________";
}
}
}
En total, vemos que en ambos ejemplos obtenemos aproximadamente 1 millón de solicitudes por segundo, solo nginx usó las 32 CPU para esto y dpdk solo una. Quizás EC2 nuevamente pone un cerdo en él y 1M rps es una limitación de red, pero incluso si lo es, los resultados no están muy distorsionados, porque agregar un retraso como
for (int x = 0; x <100; ++ x) http al ejemplo
→ request_url [0] = 'a' + (http-> request_url [0]% 10) antes de enviar el paquete, ya redujo rps, lo que significa una carga de CPU casi completa con trabajo útil.
En el curso de los experimentos, se descubrió un misterio que todavía no puedo resolver. Si habilita la descarga de la suma de verificación, es decir, el cálculo de las sumas de verificación para los encabezados ip y tcp por la propia tarjeta de red, entonces el rendimiento general disminuye y la latencia mejora.
Aquí está el comienzo con la descarga habilitada
root@ip-172-30-0-127:~# wrk -t64 -d10s -c4000 --latency http://172.30.4.10/testu
Running 10s test @ http://172.30.4.10/testu
64 threads and 4000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 5.91ms 614.33us 28.35ms 96.17%
Req/Sec 10.48k 1.51k 69.89k 98.78%
Latency Distribution
50% 5.91ms
75% 6.01ms
90% 6.19ms
99% 6.99ms
6738296 requests in 10.10s, 1.12GB read
Requests/sec: 667140.71
Transfer/sec: 113.25MB
Y aquí con suma de comprobación en la CPU
root@ip-172-30-0-127:~# wrk -t64 -d10s -c4000 --latency http://172.30.4.10/testu
Running 10s test @ http://172.30.4.10/testu
64 threads and 4000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 32.19ms 63.43ms 817.26ms 85.01%
Req/Sec 15.97k 4.04k 113.97k 93.47%
Latency Distribution
50% 2.58ms
75% 17.57ms
90% 134.94ms
99% 206.03ms
10278064 requests in 10.10s, 1.70GB read
Socket errors: connect 0, read 17, write 0, timeout 0
Requests/sec: 1017645.11
Transfer/sec: 172.75MB
Bien, puedo explicar la caída del rendimiento por el hecho de que la tarjeta de red se ralentiza, aunque esto es extraño, debería acelerarse. Pero, ¿por qué, con el cálculo de la suma de comprobación en el mapa de latencia, resulta ser casi constante igual a 6 ms, y si cuenta con CPU, entonces flota de 2.5 ms a 817 ms? La tarea se simplificaría enormemente con un soporte no virtual con una conexión directa, pero desafortunadamente no tengo esto. El DPDK en sí no funciona en todas las tarjetas de red, y antes de usarlo, debe verificar la
lista .
Y finalmente, una encuesta.