1M HTTP rps sur 1 cƓur de processeur. DPDK au lieu de nginx + noyau Linux TCP / IP

Je veux parler d'une chose comme DPDK - c'est un cadre pour travailler avec un rĂ©seau contournant le noyau. C'est-Ă -dire Vous pouvez Ă©crire directement depuis userland \ pour lire dans la file d'attente de la carte rĂ©seau, sans avoir besoin d'appels systĂšme. Cela vous fait Ă©conomiser beaucoup de frais gĂ©nĂ©raux pour la copie et plus encore. À titre d'exemple, je vais Ă©crire une application qui donne une page de test via http et comparer sa vitesse avec nginx.

DPDK peut ĂȘtre tĂ©lĂ©chargĂ© ici . Ne prenez pas Stable - cela n'a pas fonctionnĂ© pour moi sur EC2, prenez le 18.05 - tout a commencĂ© avec. Avant de commencer, vous devez rĂ©server d' Ă©normes pages dans le systĂšme pour le fonctionnement normal du framework. En principe, les applications de test peuvent ĂȘtre lancĂ©es avec la possibilitĂ© de fonctionner sans pages gĂ©antes, mais je les ai toujours incluses. * N'oubliez pas update-grub aprĂšs grub-mkconfig * Une fois que vous avez terminĂ© avec Ă©norme pages, allez immĂ©diatement sur ./usertools/dpdk-setup.py - cette chose collectera et configurera tout le reste. Dans Google, vous pouvez trouver des instructions recommandant de collecter et de configurer quelque chose pour contourner dpdk-setup.py - ne faites pas cela. Eh bien, vous pouvez le faire, seulement avec moi, alors que je n'ai pas utilisĂ© dpdk-setup.py, rien n'a fonctionnĂ©. En bref, la sĂ©quence d'actions dans dpdk-setup.py:

  • construire x86_x64 linux
  • charger le module du noyau uio igb
  • mapper Ă©normes pages vers / mnt / Ă©norme
  • liez le nic souhaitĂ© dans uio (n'oubliez pas de faire ifconfig ethX avant)

AprÚs cela, vous pouvez créer un exemple en exécutant make dans le répertoire avec lui. Il suffit de créer une variable d'environnement RTE_SDK qui pointe vers un répertoire avec DPDK.

Ici se trouve l'exemple de code complet. Il consiste en l'initialisation, l'implémentation de la version primitive de tcp / ip et de l'analyseur http primitif. Commençons par l'initialisation.

int main(int argc, char** argv) { int ret; //  dpdk ret = rte_eal_init(argc, argv); if (ret < 0) { rte_panic("Cannot init EAL\n"); } //  memory pool g_packet_mbuf_pool = rte_pktmbuf_pool_create("mbuf_pool", 131071, 32, 0, RTE_MBUF_DEFAULT_BUF_SIZE, rte_socket_id()); if (g_packet_mbuf_pool == NULL) { rte_exit(EXIT_FAILURE, "Cannot init mbuf pool\n"); } g_tcp_state_pool = rte_mempool_create("tcp_state_pool", 65535, sizeof(struct tcp_state), 0, 0, NULL, NULL, NULL, NULL, rte_socket_id(), 0); if (g_tcp_state_pool == NULL) { rte_exit(EXIT_FAILURE, "Cannot init tcp_state pool\n"); } //  hash table struct rte_hash_parameters hash_params = { .entries = 64536, .key_len = sizeof(struct tcp_key), .socket_id = rte_socket_id(), .hash_func_init_val = 0, .name = "tcp clients table" }; g_clients = rte_hash_create(&hash_params); if (g_clients == NULL) { rte_exit(EXIT_FAILURE, "No hash table created\n"); } //    1     uint8_t nb_ports = rte_eth_dev_count(); if (nb_ports == 0) { rte_exit(EXIT_FAILURE, "No Ethernet ports - bye\n"); } if (nb_ports > 1) { rte_exit(EXIT_FAILURE, "Not implemented. Too much ports\n"); } //     struct rte_eth_conf port_conf = { .rxmode = { .split_hdr_size = 0, .header_split = 0, /**< Header Split disabled */ .hw_ip_checksum = 0, /**< IP checksum offload disabled */ .hw_vlan_filter = 0, /**< VLAN filtering disabled */ .jumbo_frame = 0, /**< Jumbo Frame Support disabled */ .hw_strip_crc = 0, /**< CRC stripped by hardware */ }, .txmode = { .mq_mode = ETH_MQ_TX_NONE, }, }; //          port_conf.txmode.offloads |= DEV_TX_OFFLOAD_IPV4_CKSUM; port_conf.txmode.offloads |= DEV_TX_OFFLOAD_TCP_CKSUM; ret = rte_eth_dev_configure(0, RX_QUEUE_COUNT, TX_QUEUE_COUNT, &port_conf); if (ret < 0) { rte_exit(EXIT_FAILURE, "Cannot configure device: err=%d\n", ret); } //     for (uint16_t j=0; j<RX_QUEUE_COUNT; ++j) { ret = rte_eth_rx_queue_setup(0, j, 1024, rte_eth_dev_socket_id(0), NULL, g_packet_mbuf_pool); if (ret < 0) { rte_exit(EXIT_FAILURE, "rte_eth_rx_queue_setup:err=%d\n", ret); } } //    struct rte_eth_txconf txconf = { .offloads = port_conf.txmode.offloads, }; for (uint16_t j=0; j<TX_QUEUE_COUNT; ++j) { ret = rte_eth_tx_queue_setup(0, j, 1024, rte_eth_dev_socket_id(0), &txconf); if (ret < 0) { rte_exit(EXIT_FAILURE, "rte_eth_tx_queue_setup:err=%d\n", ret); } } // NIC ret = rte_eth_dev_start(0); if (ret < 0) { rte_exit(EXIT_FAILURE, "rte_eth_dev_start:err=%d\n", ret); } //   lcore_hello(NULL); return 0; } 

À ce moment, lorsque via dpdk-setup.py nous lions l'interface rĂ©seau sĂ©lectionnĂ©e au pilote dpdk, cette interface rĂ©seau cesse d'ĂȘtre accessible au noyau. AprĂšs cela, la carte rĂ©seau enregistrera tous les paquets qui arrivent Ă  cette interface via le DMA dans la file d'attente que nous lui avons fournie.

Et voici la boucle de traitement des paquets.

 struct rte_mbuf* packets[MAX_PACKETS]; uint16_t rx_current_queue = 0; while (1) { //  unsigned packet_count = rte_eth_rx_burst(0, (++rx_current_queue) % RX_QUEUE_COUNT, packets, MAX_PACKETS); for (unsigned j=0; j<packet_count; ++j) { struct rte_mbuf* m = packets[j]; //   ethernet  struct ether_hdr* eth_header = rte_pktmbuf_mtod(m, struct ether_hdr*); //  IP  if (RTE_ETH_IS_IPV4_HDR(m->packet_type)) { do { //   if (rte_pktmbuf_data_len(m) < sizeof(struct ether_hdr) + sizeof(struct ipv4_hdr) + sizeof(struct tcp_hdr)) { TRACE; break; } struct ipv4_hdr* ip_header = (struct ipv4_hdr*)((char*)eth_header + sizeof(struct ether_hdr)); if ((ip_header->next_proto_id != 0x6) || (ip_header->version_ihl != 0x45)) { TRACE; break; } if (ip_header->dst_addr != MY_IP_ADDRESS) { TRACE; break; } if (rte_pktmbuf_data_len(m) < htons(ip_header->total_length) + sizeof(struct ether_hdr)) { TRACE; break; } if (htons(ip_header->total_length) < sizeof(struct ipv4_hdr) + sizeof(struct tcp_hdr)) { TRACE; break; } struct tcp_hdr* tcp_header = (struct tcp_hdr*)((char*)ip_header + sizeof(struct ipv4_hdr)); size_t tcp_header_size = (tcp_header->data_off >> 4) * 4; if (rte_pktmbuf_data_len(m) < sizeof(struct ether_hdr) + sizeof(struct ipv4_hdr) + tcp_header_size) { TRACE; break; } if (tcp_header->dst_port != 0x5000) { TRACE; break; } size_t data_size = htons(ip_header->total_length) - sizeof(struct ipv4_hdr) - tcp_header_size; void* data = (char*)tcp_header + tcp_header_size; //     hash table struct tcp_key key = { .ip = ip_header->src_addr, .port = tcp_header->src_port }; //    tcp process_tcp(m, tcp_header, &key, data, data_size); } while(0); } else if (eth_header->ether_type == 0x0608) // ARP { //     -       ARP- do { if (rte_pktmbuf_data_len(m) < sizeof(struct arp) + sizeof(struct ether_hdr)) { TRACE; break; } struct arp* arp_packet = (struct arp*)((char*)eth_header + sizeof(struct ether_hdr)); if (arp_packet->opcode != 0x100) { TRACE; break; } if (arp_packet->dst_pr_add != MY_IP_ADDRESS) { TRACE; break; } send_arp_response(arp_packet); } while(0); } else { TRACE; } rte_pktmbuf_free(m); } } 

La fonction rte_eth_rx_burst est utilisĂ©e pour lire les paquets de la file d'attente. S'il y a quelque chose dans la file d'attente, elle lira les paquets et les placera dans un tableau. S'il n'y a rien dans la file d'attente, 0 sera retournĂ©, dans ce cas, vous devez immĂ©diatement l'appeler Ă  nouveau. Oui, cette approche «passe» le temps CPU Ă  rien s'il n'y a actuellement aucune donnĂ©e sur le rĂ©seau, mais si nous avons dĂ©jĂ  pris dpdk, alors ce n'est pas notre cas. * Important, la fonction n'est pas thread-safe , ne peut pas ĂȘtre lue Ă  partir de la mĂȘme file d'attente dans diffĂ©rents processus * AprĂšs le traitement du package, rte_pktmbuf_free doit ĂȘtre appelĂ©. Pour envoyer un paquet, vous pouvez utiliser la fonction rte_eth_tx_burst, qui place rte_mbuf reçu de rte_pktmbuf_alloc dans la file d'attente de la carte rĂ©seau.

Une fois les en-tĂȘtes de package dĂ©sassemblĂ©s, il sera nĂ©cessaire de crĂ©er une session TCP. Le protocole TCP est rempli de divers cas spĂ©ciaux, situations spĂ©ciales et dangers de dĂ©ni de service. L'implĂ©mentation d'un tcp plus ou moins complet est un excellent exercice pour un dĂ©veloppeur expĂ©rimentĂ©, mais n'est cependant pas inclus dans le cadre dĂ©crit ici. Dans l'exemple, tcp est implĂ©mentĂ© juste assez pour les tests. ImplĂ©mentation d'une table de session basĂ©e sur la table de hachage fournie avec dpdk , Ă©tablissant et interrompant une connexion TCP, transmettant et recevant des donnĂ©es sans prendre en compte les pertes et la rĂ©organisation des paquets. La table de hachage de dpdk a une limitation importante que vous pouvez lire mais ne pouvez pas Ă©crire sur plusieurs threads. L'exemple est fait en un seul thread et ce problĂšme n'est pas important ici, et en cas de traitement du trafic sur plusieurs cƓurs, vous pouvez utiliser RSS, envoyer une table de hachage et faire sans bloquer.

Implémentation 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) //Documentaion lies!!! { TRACE; if ((tcp_header->tcp_flags & 0x2) != 0) // SYN { TRACE; struct ether_hdr* eth_header = rte_pktmbuf_mtod(m, struct ether_hdr*); if (rte_mempool_get(g_tcp_state_pool, (void**)&state) < 0) { ERROR("tcp state alloc fail"); return; } memcpy(&state->tcp_template, &g_tcp_packet_template, sizeof(g_tcp_packet_template)); memcpy(&state->tcp_template.eth.d_addr, &eth_header->s_addr, 6); state->tcp_template.ip.dst_addr = key->ip; state->tcp_template.tcp.dst_port = key->port; state->remote_seq = htonl(tcp_header->sent_seq); #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wpointer-to-int-cast" state->my_seq_start = (uint32_t)state; // not very secure. #pragma GCC diagnostic pop state->fin_sent = 0; state->http.state = HTTP_START; state->http.request_url_size = 0; //not thread safe! only one core used if (rte_hash_add_key_data(g_clients, key, state) == 0) { struct tcp_hdr* new_tcp_header; struct rte_mbuf* packet = build_packet(state, 12, &new_tcp_header); if (packet != NULL) { new_tcp_header->rx_win = TX_WINDOW_SIZE; new_tcp_header->sent_seq = htonl(state->my_seq_start); state->my_seq_sent = state->my_seq_start+1; ++state->remote_seq; new_tcp_header->recv_ack = htonl(state->remote_seq); new_tcp_header->tcp_flags = 0x12; // mss = 1380, no window scaling uint8_t options[12] = {0x02, 0x04, 0x05, 0x64, 0x03, 0x03, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01}; memcpy((uint8_t*)new_tcp_header + sizeof(struct tcp_hdr), options, 12); new_tcp_header->data_off = 0x80; send_packet(new_tcp_header, packet); } else { ERROR("rte_pktmbuf_alloc, tcp synack"); } } else { ERROR("can't add connection to table"); rte_mempool_put(g_tcp_state_pool, state); } } else { ERROR("lost connection"); } return; } if ((tcp_header->tcp_flags & 0x2) != 0) // SYN retransmit { //not thread safe! only one core used if (rte_hash_del_key(g_clients, key) < 0) { ERROR("can't delete key"); } else { rte_mempool_put(g_tcp_state_pool, state); return process_tcp(m, tcp_header, key, data, data_size); } } if ((tcp_header->tcp_flags & 0x10) != 0) // ACK { TRACE; uint32_t ack_delta = htonl(tcp_header->recv_ack) - state->my_seq_start; uint32_t my_max_ack_delta = state->my_seq_sent - state->my_seq_start; if (ack_delta == 0) { if ((data_size == 0) && (tcp_header->tcp_flags == 0x10)) { ERROR("need to retransmit. not supported"); } } else if (ack_delta <= my_max_ack_delta) { state->my_seq_start += ack_delta; } else { ERROR("ack on unsent seq"); } } if (data_size > 0) { TRACE; uint32_t packet_seq = htonl(tcp_header->sent_seq); if (state->remote_seq == packet_seq) { feed_http(data, data_size, state); state->remote_seq += data_size; } else if (state->remote_seq-1 == packet_seq) // keepalive { struct tcp_hdr* new_tcp_header; struct rte_mbuf* packet = build_packet(state, 0, &new_tcp_header); if (packet != NULL) { new_tcp_header->rx_win = TX_WINDOW_SIZE; new_tcp_header->sent_seq = htonl(state->my_seq_sent); new_tcp_header->recv_ack = htonl(state->remote_seq); send_packet(new_tcp_header, packet); } else { ERROR("rte_pktmbuf_alloc, tcp ack keepalive"); } } else { struct tcp_hdr* new_tcp_header; struct rte_mbuf* packet = build_packet(state, state->http.last_message_size, &new_tcp_header); TRACE; if (packet != NULL) { new_tcp_header->rx_win = TX_WINDOW_SIZE; new_tcp_header->sent_seq = htonl(state->my_seq_sent - state->http.last_message_size); new_tcp_header->recv_ack = htonl(state->remote_seq); memcpy((char*)new_tcp_header+sizeof(struct tcp_hdr), &state->http.last_message, state->http.last_message_size); send_packet(new_tcp_header, packet); } else { ERROR("rte_pktmbuf_alloc, tcp fin ack"); } //ERROR("my bad tcp stack implementation((("); } } if ((tcp_header->tcp_flags & 0x04) != 0) // RST { TRACE; //not thread safe! only one core used if (rte_hash_del_key(g_clients, key) < 0) { ERROR("can't delete key"); } else { rte_mempool_put(g_tcp_state_pool, state); } } else if ((tcp_header->tcp_flags & 0x01) != 0) // FIN { struct tcp_hdr* new_tcp_header; struct rte_mbuf* packet = build_packet(state, 0, &new_tcp_header); TRACE; if (packet != NULL) { new_tcp_header->rx_win = TX_WINDOW_SIZE; new_tcp_header->sent_seq = htonl(state->my_seq_sent); new_tcp_header->recv_ack = htonl(state->remote_seq + 1); if (!state->fin_sent) { TRACE; new_tcp_header->tcp_flags = 0x11; // !@#$ the last ack } send_packet(new_tcp_header, packet); } else { ERROR("rte_pktmbuf_alloc, tcp fin ack"); } //not thread safe! only one core used if (rte_hash_del_key(g_clients, key) < 0) { ERROR("can't delete key"); } else { rte_mempool_put(g_tcp_state_pool, state); } } } 


L'analyseur http ne prendra en charge que GET pour lire les URL à partir de là et retourner du code HTML avec l'URL demandée.

Analyseur 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; //+1 for FIN tcp_header->tcp_flags = 0x11; state->fin_sent = 1; #endif char* new_data = (char*)tcp_header + sizeof(struct tcp_hdr); memcpy(new_data, g_http_part1, g_http_part1_size); new_data += g_http_part1_size; memcpy(new_data, content_length, content_length_size); new_data += content_length_size; memcpy(new_data, g_http_part2, g_http_part2_size); new_data += g_http_part2_size; memcpy(new_data, http->request_url, http->request_url_size); new_data += http->request_url_size; memcpy(new_data, g_http_part3, g_http_part3_size); memcpy(&http->last_message, (char*)tcp_header+sizeof(struct tcp_hdr), total_data_size); http->last_message_size = total_data_size; send_packet(tcp_header, packet); } else { ERROR("rte_pktmbuf_alloc, tcp data"); } http->state = HTTP_START; http->request_url_size = 0; } else if (*current == '\r') { http->state = HTTP_READ_R1; } else { http->state = HTTP_READ_URL; } break; } default: { ERROR("bad http state"); return; } } if (http->state == HTTP_BAD_STATE) { return; } --remaining_data; ++current; } } 


Une fois que l'exemple est prĂȘt, vous pouvez comparer les performances avec nginx. Parce que Je ne peux pas assembler un vrai stand Ă  la maison, j'ai utilisĂ© amazon EC2. EC2 a fait ses corrections lors des tests - j'ai dĂ» abandonner Connection: fermer les requĂȘtes, car quelque part Ă  300k rps, les paquets SYN ont commencĂ© Ă  chuter quelques secondes aprĂšs le dĂ©but du test. Apparemment, il existe une sorte de protection contre les inondations SYN, les demandes ont donc Ă©tĂ© maintenues en vie. Sur EC2, dpdk ne fonctionne pas sur toutes les instances, par exemple sur m1.medium cela ne fonctionne pas. Le stand a utilisĂ© 1 instance r4.8xlarge avec l'application et 2 instances r4.8xlarge pour crĂ©er une charge. Ils communiquent sur les interfaces rĂ©seau de l'hĂŽtel via un sous-rĂ©seau VPC privĂ©. J'ai essayĂ© de charger avec diffĂ©rents utilitaires: ab, wrk, h2load, siege. Le plus pratique Ă©tait wrk, car ab est monothread et produit des statistiques dĂ©formĂ©es s'il y a des erreurs sur le rĂ©seau.

Avec beaucoup de trafic dans EC2, un certain nombre de baisses peuvent ĂȘtre observĂ©es, pour les applications ordinaires, cela sera invisible, mais dans le cas de ab, toute retransmission prend le temps total et ab, ce qui fait que les donnĂ©es sur le nombre moyen de demandes par seconde ne conviennent pas. Les raisons des baisses sont un mystĂšre distinct Ă  rĂ©soudre, cependant, le fait qu'il y ait des problĂšmes non seulement lors de l'utilisation de dpdk, mais aussi avec nginx, suggĂšre que cela ne semble pas ĂȘtre un exemple de quelque chose de mal.

J'ai effectuĂ© le test en deux Ă©tapes, d'abord j'ai exĂ©cutĂ© wrk sur 1 instance, puis sur 2. Si les performances totales de 2 instances sont de 1, cela signifie que je n'ai pas rencontrĂ© les performances de wrk lui-mĂȘme.

Résultat du test de l'exemple dpdk sur r4.8xlarge
lancer l'instance 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

ExĂ©cution de wrk Ă  partir de 2 instances en mĂȘme temps
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 a donné de tels résultats
lancer l'instance 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


ExĂ©cution de wrk Ă  partir de 2 instances en mĂȘme temps

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


config nginx
utilisateur www-data;
worker_processes auto;
pid /run/nginx.pid;
worker_rlimit_nofile 50000;
événements {
travailleurs_connexions 10000;
}
http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;

inclure /etc/nginx/mime.types;
default_type text / plain;

error_log /var/log/nginx/error.log;
access_log off;

serveur {
listen 80 default_server backlog = 10000 reuseport;
emplacement / {
return 200 "answer.padding _____________________________________________________________";
}
}
}

Au total, nous voyons que dans les deux exemples, nous recevons environ 1 million de requĂȘtes par seconde, seul nginx a utilisĂ© les 32 processeurs pour cela, et dpdk un seul. Peut-ĂȘtre que EC2 y met un cochon et 1M rps est une limitation de rĂ©seau, mais mĂȘme si c'est le cas, les rĂ©sultats ne sont pas beaucoup dĂ©formĂ©s, car en ajoutant un dĂ©lai comme pour (int x = 0; x <100; ++ x) http Ă  l'exemple → request_url [0] = 'a' + (http-> request_url [0]% 10) avant d'envoyer le paquet, il a dĂ©jĂ  rĂ©duit le rps, ce qui signifie un chargement de CPU presque complet avec un travail utile.

Au cours des expĂ©riences, un mystĂšre a Ă©tĂ© dĂ©couvert, que je ne peux toujours pas rĂ©soudre. Si vous activez le dĂ©chargement de la somme de contrĂŽle, c'est-Ă -dire le calcul des sommes de contrĂŽle pour les en-tĂȘtes ip et tcp par la carte rĂ©seau elle-mĂȘme, les performances globales diminuent et la latence s'amĂ©liore.
Voici le début avec le déchargement activé

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


Et ici avec une somme de contrĂŽle sur le processeur

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, je peux expliquer la baisse des performances par le fait que la carte rĂ©seau ralentit, bien que ce soit Ă©trange, elle devrait accĂ©lĂ©rer. Mais pourquoi avec le calcul de la somme de contrĂŽle sur la carte de latence est presque constante Ă©gale Ă  6 ms, et si vous comptez sur cpu, alors il flotte de 2,5 ms Ă  817 ms? La tĂąche serait grandement simplifiĂ©e par un stand non virtuel avec une connexion directe, mais je ne l'ai malheureusement pas. DPDK lui-mĂȘme ne fonctionne pas sur toutes les cartes rĂ©seau et avant de l'utiliser, vous devez vĂ©rifier la liste .

Et enfin, une enquĂȘte.

Source: https://habr.com/ru/post/fr416651/


All Articles