1M HTTP-RPS auf 1 CPU-Kern. DPDK anstelle von Nginx + Linux Kernel TCP / IP

Ich möchte über so etwas wie DPDK sprechen - dies ist ein Framework für die Arbeit mit einem Netzwerk, das den Kernel umgeht. Das heißt, Sie können direkt aus userland \ schreiben, um die Warteschlange der Netzwerkkarte einzulesen, ohne dass Systemaufrufe erforderlich sind. Dies erspart Ihnen viel Aufwand beim Kopieren und mehr. Als Beispiel schreibe ich eine Anwendung, die eine Testseite über http bereitstellt, und vergleiche ihre Geschwindigkeit mit nginx.

DPDK kann hier heruntergeladen werden . Nehmen Sie nicht Stable - es hat bei EC2 nicht funktioniert, nehmen Sie 18.05 - alles begann damit. Bevor Sie beginnen, müssen Sie große Seiten im System für den normalen Betrieb des Frameworks reservieren . Grundsätzlich können Testanwendungen mit der Option gestartet werden, ohne große Seiten zu arbeiten, aber ich habe sie immer eingeschlossen. * Update-grub nach grub-mkconfig nicht vergessen * Nachdem Sie mit den riesigen Seiten fertig sind, gehen Sie sofort zu ./usertools/dpdk-setup.py - dieses Ding sammelt und konfiguriert alles andere. In Google finden Sie Anweisungen, die empfehlen, etwas zu sammeln und einzurichten, um dpdk-setup.py zu umgehen. Tun Sie dies nicht. Nun, Sie können es nur mit mir tun, obwohl ich dpdk-setup.py nicht verwendet habe, hat nichts funktioniert. Kurz gesagt, die Reihenfolge der Aktionen in dpdk-setup.py:

  • Build x86_x64 Linux
  • Laden Sie das igb uio-Kernelmodul
  • ordne riesige Seiten / mnt / riesig zu
  • binde das gewünschte nic in uio (vergiss nicht, ifconfig ethX vorher zu machen)

Danach können Sie ein Beispiel erstellen, indem Sie make im Verzeichnis damit ausführen. Es muss nur eine Umgebungsvariable RTE_SDK erstellt werden, die auf ein Verzeichnis mit DPDK verweist.

Hier liegt der vollständige Beispielcode. Es besteht aus der Initialisierung, Implementierung der primitiven Version von tcp / ip und dem primitiven http-Parser. Beginnen wir mit der Initialisierung.

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; } 

In diesem Moment, wenn wir über dpdk-setup.py die ausgewählte Netzwerkschnittstelle an den dpdk-Treiber binden, ist diese Netzwerkschnittstelle für den Kernel nicht mehr zugänglich. Danach zeichnet die Netzwerkkarte alle Pakete, die über den DMA an diese Schnittstelle gelangen, in der von uns bereitgestellten Warteschlange auf.

Und hier ist die Paketverarbeitungsschleife.

 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); } } 

Die Funktion rte_eth_rx_burst wird zum Lesen von Paketen aus der Warteschlange verwendet. Wenn sich etwas in der Warteschlange befindet, werden die Pakete gelesen und in ein Array eingefügt. Befindet sich nichts in der Warteschlange, wird 0 zurückgegeben. In diesem Fall müssen Sie es sofort erneut aufrufen. Ja, dieser Ansatz "verbringt" CPU-Zeit mit nichts, wenn derzeit keine Daten im Netzwerk vorhanden sind. Wenn wir jedoch bereits dpdk verwendet haben, wird davon ausgegangen, dass dies nicht unser Fall ist. * Wichtig, die Funktion ist nicht threadsicher und kann in verschiedenen Prozessen nicht aus derselben Warteschlange gelesen werden. * Nach der Verarbeitung des Pakets muss rte_pktmbuf_free aufgerufen werden. Um ein Paket zu senden, können Sie die Funktion rte_eth_tx_burst verwenden, mit der von rte_pktmbuf_alloc empfangenes rte_mbuf in die Netzwerkkartenwarteschlange gestellt wird.

Nachdem die Paket-Header zerlegt wurden, muss eine TCP-Sitzung erstellt werden. Das TCP-Protokoll enthält zahlreiche Sonderfälle, besondere Situationen und Gefahren von Denial-of-Service. Die Implementierung eines mehr oder weniger vollständigen TCP ist eine hervorragende Übung für einen erfahrenen Entwickler, ist jedoch nicht in dem hier beschriebenen Framework enthalten. Im Beispiel ist tcp gerade genug zum Testen implementiert. Implementierung einer Sitzungstabelle basierend auf der mit dpdk gelieferten Hash-Tabelle , Festlegen und Unterbrechen einer TCP-Verbindung, Senden und Empfangen von Daten ohne Berücksichtigung von Verlusten und Neuordnung von Paketen. Die Hash-Tabelle von dpdk weist eine wichtige Einschränkung auf, die Sie lesen, aber nicht in mehrere Threads schreiben können. Das Beispiel ist Single-Threaded und dieses Problem ist hier nicht wichtig. Wenn Sie Datenverkehr auf mehreren Kernen verarbeiten, können Sie RSS verwenden, eine Hash-Tabelle senden und ohne Blockierung auskommen.

TCP-Implementierung
 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); } } } 


Der http-Parser unterstützt nur GET, um URLs von dort zu lesen und HTML mit der angeforderten URL zurückzugeben.

HTTP-Parser
 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; } } 


Nachdem das Beispiel fertig ist, können Sie die Leistung mit nginx vergleichen. Weil Ich kann zu Hause keinen richtigen Stand aufbauen, ich habe amazon EC2 verwendet. EC2 hat seine Korrekturen beim Testen vorgenommen - ich musste Connection aufgeben: Anfragen schließen, weil Irgendwo bei 300.000 U / min begannen SYN-Pakete einige Sekunden nach Beginn des Tests zu fallen. Anscheinend gibt es eine Art Schutz gegen SYN-Flut, so dass die Anfragen am Leben erhalten wurden. Unter EC2 funktioniert dpdk nicht auf allen Instanzen, beispielsweise auf m1.medium nicht. Der Stand verwendete 1 Instanz r4.8xlarge mit der Anwendung und 2 Instanzen r4.8xlarge, um eine Last zu erstellen. Sie kommunizieren über Hotelnetzwerkschnittstellen über ein privates VPC-Subnetz. Ich habe versucht, mit verschiedenen Dienstprogrammen zu laden: ab, wrk, h2load, belagerung. Am bequemsten war wrk, weil ab ist Single-Threaded und erzeugt verzerrte Statistiken, wenn im Netzwerk Fehler auftreten.

Bei viel Verkehr in EC2 kann eine bestimmte Anzahl von Abbrüchen beobachtet werden. Für gewöhnliche Anwendungen ist dies unsichtbar. Im Fall von ab nimmt jede erneute Übertragung die Gesamtzeit und ab in Anspruch, wodurch die Daten zur durchschnittlichen Anzahl von Anforderungen pro Sekunde ungeeignet sind. Die Gründe für die Tropfen sind ein gesondertes Rätsel. Die Tatsache, dass es nicht nur bei der Verwendung von dpdk, sondern auch bei nginx Probleme gibt, legt nahe, dass dies kein Beispiel für etwas Falsches zu sein scheint.

Ich habe den Test in zwei Schritten durchgeführt, zuerst habe ich wrk in einer Instanz ausgeführt, dann in zwei. Wenn die Gesamtleistung von 2 Instanzen 1 ist, bedeutet dies, dass ich nicht auf die Leistung von wrk selbst gestoßen bin.

Testergebnis des dpdk-Beispiels auf r4.8xlarge
Starten Sie die Instanz 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

Ausführen von wrk von 2 Instanzen gleichzeitig
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 gab solche Ergebnisse
Starten Sie die Instanz 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


Ausführen von wrk von 2 Instanzen gleichzeitig

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 config
Benutzer www-Daten;
worker_processes auto;
pid /run/nginx.pid;
worker_rlimit_nofile 50000;
Ereignisse {
worker_connections 10000;
}}
http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;

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

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

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

Insgesamt sehen wir, dass in beiden Beispielen ungefähr 1 Million Anfragen pro Sekunde eingehen, nur nginx alle 32 CPUs dafür verwendet hat und dpdk nur eine. Vielleicht legt EC2 wieder ein Schwein und 1M rps ist eine Netzwerkbeschränkung, aber selbst wenn dies der Fall ist, sind die Ergebnisse nicht stark verzerrt, da ein Beispiel für eine Verzögerung der Form für (int x = 0; x <100; ++ x) http hinzugefügt wird → request_url [0] = 'a' + (http-> request_url [0]% 10) vor dem Senden des Pakets wurden die RPS bereits reduziert, was bedeutet, dass die CPU mit nützlicher Arbeit fast vollständig geladen ist.

Im Verlauf der Experimente wurde ein Rätsel entdeckt, das ich immer noch nicht lösen kann. Wenn Sie das Auslagern von Prüfsummen aktivieren, d. H. Die Berechnung der Prüfsummen für die IP- und TCP-Header durch die Netzwerkkarte selbst, sinkt die Gesamtleistung und die Latenz verbessert sich.
Hier ist der Start mit aktiviertem Offloading

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


Und hier mit Prüfsumme auf 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, ich kann den Leistungsabfall damit erklären, dass die Netzwerkkarte langsamer wird, obwohl dies seltsam ist, sollte sie sich beschleunigen. Aber warum stellt sich bei der Berechnung der Prüfsumme auf der Latenzkarte eine nahezu konstante Summe von 6 ms heraus, und wenn Sie auf die CPU zählen, schwebt sie zwischen 2,5 ms und 817 ms? Die Aufgabe würde durch einen nicht virtuellen Stand mit direkter Verbindung stark vereinfacht, aber das habe ich leider nicht. DPDK selbst funktioniert nicht auf allen Netzwerkkarten. Bevor Sie es verwenden, müssen Sie die Liste überprüfen.

Und zum Schluss noch eine Umfrage.

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


All Articles