Eu quero falar sobre algo como
DPDK - essa é uma estrutura para trabalhar com uma rede ignorando o kernel. I.e. Você pode escrever diretamente da terra do usuário \ para ler na fila da placa de rede, sem a necessidade de chamadas do sistema. Isso economiza muito em sobrecarga para copiar e muito mais. Como exemplo, vou escrever um aplicativo que fornece uma página de teste via http e comparar sua velocidade com o nginx.
O DPDK pode ser baixado
aqui . Não tome Stable - não funcionou para mim no EC2, tome 18.05 - tudo começou com ele. Antes de começar, você precisa reservar
páginas enormes no
sistema para a operação normal da estrutura. Em princípio, os aplicativos de teste podem ser iniciados com a opção de trabalhar sem grandes páginas, mas eu sempre os incluí. * Não esqueça o update-grub após o grub-mkconfig * Após terminar as páginas enormes, vá imediatamente para ./usertools/dpdk-setup.py - esse item coletará e configurará todo o resto. No Google, você pode encontrar instruções recomendando a coleta e a configuração de algo para contornar o dpdk-setup.py - não faça isso. Bem, você pode fazê-lo, somente comigo, enquanto eu não usei o dpdk-setup.py, nada funcionou. Resumidamente, a sequência de ações dentro do dpdk-setup.py:
- compilação x86_x64 linux
- carregar o módulo do kernel do igb uio
- mapearpáginas enormes para / mnt / enorme
- vincule o nic desejado no uio (não esqueça de fazer o ifconfig ethX antes)
Depois disso, você pode criar um exemplo executando make no diretório com ele. É necessário apenas criar uma variável de ambiente RTE_SDK que aponte para um diretório com o DPDK.
Aqui está o código de exemplo completo. Consiste em inicialização, implementação da versão primitiva do tcp / ip e analisador http primitivo. Vamos começar com a inicialização.
int main(int argc, char** argv) { int ret;
Nesse momento, quando através do dpdk-setup.py vinculamos a interface de rede selecionada ao driver dpdk, essa interface de rede deixa de estar acessível ao kernel. Depois disso, a placa de rede registrará todos os pacotes que chegarem a essa interface por meio do DMA na fila que fornecemos a ela.
E aqui está o loop de processamento de pacotes.
struct rte_mbuf* packets[MAX_PACKETS]; uint16_t rx_current_queue = 0; while (1) {
A função rte_eth_rx_burst é usada para ler pacotes da fila.Se houver algo na fila, ele lerá os pacotes e os colocará em uma matriz. Se não houver nada na fila, 0 será retornado; nesse caso, você deve chamá-lo imediatamente novamente. Sim, essa abordagem "gasta" o tempo da CPU para nada se não houver dados na rede no momento, mas se já adotamos o dpdk, esse pressuposto não é o nosso caso. * Importante, a
função não é segura para threads , não pode ser lida da mesma fila em diferentes processos * Após o processamento do pacote, rte_pktmbuf_free deve ser chamado. Para enviar um pacote, você pode usar a função rte_eth_tx_burst, que coloca o rte_mbuf recebido de rte_pktmbuf_alloc na fila da placa de rede.
Após a desmontagem dos cabeçalhos dos pacotes, será necessário criar uma sessão tcp. O protocolo tcp está repleto de vários casos especiais, situações especiais e perigos de negação de serviço. A implementação de um TCP mais ou menos completo é um excelente exercício para um desenvolvedor experiente, mas, no entanto, não está incluído na estrutura descrita aqui. No exemplo, o tcp é implementado apenas o suficiente para o teste. Implementou uma tabela de sessões com base na
tabela de hash fornecida com o dpdk , configurando e interrompendo uma conexão tcp, transmitindo e recebendo dados sem levar em consideração as perdas e a reordenação de pacotes. A tabela de hash do dpdk tem uma limitação importante que você pode ler, mas não pode gravar em vários threads. O exemplo é feito em thread único e esse problema não é importante aqui e, no caso de processar tráfego em vários núcleos, você pode usar o RSS, enviar uma tabela de hash e fazer isso sem bloquear.
Implementação de 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)
O analisador http suportará apenas GET para ler URLs a partir daí e retornar html com o URL solicitado.
Analisador 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;
Depois que o exemplo estiver pronto, você poderá comparar o desempenho com o nginx. Porque Não consigo montar um estande de verdade em casa, usei o Amazon EC2. O EC2 fez suas correções nos testes - tive que abandonar o Connection: solicitações fechadas, porque em algum lugar a 300k rps, os pacotes SYN começaram a cair alguns segundos após o início do teste. Aparentemente, existe algum tipo de proteção contra o SYN-flood, portanto, os pedidos foram feitos mantidos vivos. No EC2, o dpdk não funciona em todas as instâncias, por exemplo, no m1.medium, não funciona. O suporte usou 1 instância r4.8xlarge com o aplicativo e 2 instâncias r4.8xlarge para criar uma carga. Eles se comunicam nas interfaces de rede do hotel por meio de uma sub-rede VPC privada. Tentei carregar com diferentes utilitários: ab, wrk, h2load, cerco. O mais conveniente foi o wrk, porque ab é de thread único e produz estatísticas distorcidas se houver erros na rede.
Com muito tráfego no EC2, um certo número de quedas pode ser observado, para aplicativos comuns isso será invisível, mas no caso de ab, qualquer retransmissão ocupa o tempo total e ab, como resultado dos quais os dados sobre o número médio de solicitações por segundo são inadequados. As razões para as descargas são um mistério separado a ser tratado, no entanto, o fato de haver problemas não apenas ao usar o dpdk, mas também com o nginx, sugere que isso não parece ser um exemplo de algo errado.
Realizei o teste em duas etapas, primeiro executei o wrk em 1 instância e depois em 2. Se o desempenho total de 2 instâncias for 1, significa que não encontrei o desempenho do próprio wrk.
Resultado do teste do exemplo dpdk em r4.8xlargeiniciar instância wrk c 1
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
Executando wrk a partir de 2 instâncias ao mesmo tempo
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 deu esses resultadosiniciar instância wrk c 1
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
Executando wrk a partir de 2 instâncias ao mesmo tempo
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
configuração nginxusuário www-data;
worker_processes auto;
pid /run/nginx.pid;
worker_rlimit_nofile 50000;
eventos {
worker_connections 10000;
}
http {
sendfile ativado;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
inclua /etc/nginx/mime.types;
default_type text / plain;
error_log /var/log/nginx/error.log;
access_log off;
servidor {
escute 80 default_server backlog = 10000 reutilização;
local / {
retornar 200 "answer.padding _____________________________________________________________";
}
}
}
No total, vemos que nos dois exemplos recebemos cerca de 1 milhão de solicitações por segundo, apenas o nginx usou todos os 32 cpu para isso e o dpdk apenas um. Talvez o EC2 coloque novamente um porco e 1M rps seja uma limitação da rede, mas, mesmo assim, os resultados não serão muito distorcidos, porque adicionar um exemplo de atraso no formulário
para (int x = 0; x <100; ++ x) http → request_url [0] = 'a' + (http-> request_url [0]% 10) antes de enviar o pacote, ele já reduziu rps, o que significa carregamento quase completo da CPU com trabalho útil.
No decorrer dos experimentos, um mistério foi descoberto, que eu ainda não consigo resolver. Se você habilitar o descarregamento da soma de verificação, ou seja, o cálculo das somas de verificação para os cabeçalhos ip e tcp pela própria placa de rede, o desempenho geral cairá e a latência aumentará.
Aqui está o começo com o descarregamento ativado
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
E aqui com soma de verificação na 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
OK, eu posso explicar a queda no desempenho pelo fato de a placa de rede ficar mais lenta, embora isso seja estranho, ela deve acelerar. Mas por que, com o cálculo da soma de verificação no mapa de latência, acaba sendo quase constante igual a 6ms e, se você contar com a CPU, ela flutua de 2,5ms a 817ms? A tarefa seria muito simplificada por um suporte não virtual com uma conexão direta, mas infelizmente não tenho isso. O DPDK em si não funciona em todas as placas de rede e, antes de usá-lo, é necessário verificar a
lista .
E, finalmente, uma pesquisa.