Главная » Хабрахабр » 1M HTTP rps на 1 cpu core. DPDK вместо nginx+linux kernel TCP/IP

1M HTTP rps на 1 cpu core. DPDK вместо nginx+linux kernel TCP/IP

Я хочу рассказать о такой штуке как DPDK — это фреймворк для работы с сетью в обход ядра. Т.е. можно прямо из userland писать\читать в очереди сетевой карты, без необходимости в каких либо системных вызовах. Это позволяет экономить много накладных расходов на копирования и прочее. В качестве примера я напишу приложение, отдающее по http тестовую страницу и сравню по скорости с nginx.
DPDK можно скачать тут. Stable не берите — он у меня не заработал на EC2, берите 18.05 — с ним все завелось. Перед началом работы надо зарезервировать в системе hugepages, для нормальной работы фреймворка. По-идее, тестовые приложения можно запускать с параметром для работы без hugepages, но я всегда их включал. *Не забудте update-grub после grub-mkconfig* После того как с hugepages закончено, сразу идите в ./usertools/dpdk-setup.py – эта штука соберёт и настроит все остальное. В гугле можно найти инструкции рекомендующие собирать и настраивать что-то в обход dpdk-setup.py – не делайте так. Ну, можете делать, только у меня, пока я dpdk-setup.py не использовал, так ничего и не заработало. Кратко, последовательность действий внутри dpdk-setup.py:

  • собрать х86_х64 linux
  • загрузить модуль ядра igb uio
  • замапить hugepages в /mnt/huge
  • забиндить нужный nic в uio (не забудте перед этим сделать ifconfig ethX down)

После этого, можно собрать пример выполнив make в каталоге с ним. Надо только создать переменную окружения RTE_SDK которая указывает на каталог с DPDK.

Он состоит из инициализации, реализации примитивной версии tcp/ip и примитивного http парсера. Тут лежит полный код примера. Начнём с инициализации

int main(int argc, char** argv)
// Создаём 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;
}

В тот момент, когда через dpdk-setup.py мы биндим выбранный сетевой интерфейс к драйверу dpdk, этот сетевой интерфейс перестаёт быть доступным для ядра. После этого, любые пакеты которые придут на этот интерфейс сетевая карта запишет через DMA в очереди, которые мы ей предоставили.

А вот цикл обработки пакетов.

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

Для чтения пакетов из очереди используется функция rte_eth_rx_burst, если в очереди что-то есть, то она прочитает пакеты и положит их в массив. Если в очереди ничего нет — вернётся 0, в этом случае нужно сразу же вызвать её ещё раз. Да, такой подход «расходует» процессорное время в пустую, если в данный момент данных в сети нет, но если мы уж взяли dpdk, то предполагается, что это не наш случай. *Важно, функция не thread-safe, нельзя читать из одной очереди в разных процессах* После завершения обработки пакета надо вызвать rte_pktmbuf_free. Для отправки пакета можно использовать функцию rte_eth_tx_burst, которая положит rte_mbuf полученный из rte_pktmbuf_alloc в очередь сетевой карты.

tcp протокол изобилует различными частными случаями, специальными ситуациями и опасностями denial of service. После того как разобрали заголовки пакета надо будет построить tcp сессию. В примере, tcp реализован ровно настолько, чтобы хватило для тестирования. Реализация более-менее полноценного tcp отличное упражнение для опытного разработчика, но тем не менее, не входит в рамки описываемого тут. Hash table из dpdk имеет важное ограничение о том, что в несколько потоков можно читать, но нельзя писать. Реализована таблица сессий на базе hash table поставляемого вместе с dpdk, установка и разрыв tcp соединения, передача и приём данных без учёта потерь и переупорядочивания пакетов. Пример сделан однопоточный и эта проблема тут не важна, а в случае обработки трафика на нескольких ядрах можно использовать RSS, пошардить hash table и обойтись без блокировок.

Реализация 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); } }
}

Парсер http будет поддерживать только GET читать оттуда URL и возвращать html с запрошенным URL.

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

После того как пример готов, можно сравнить производительность с nginx. Т.к. реального стенда я у себя дома собрать не могу, я воспользовался amazon EC2. EC2 внёс свои корректировки в тестирования — пришлось отказаться от Connection: close запросов, т.к. где-то на 300k rps SYN пакеты начинали дропаться через несколько секунд после начала теста. Видимо, там какая-то защита от SYN-flood, поэтому запросы делались keep-alive. На EC2 dpdk работает не на всех инстансах, например на m1.medium не работал. В стенде использовался 1 инстанс r4.8xlarge с приложением и 2 инстанса r4.8xlarge для создания нагрузки. Общаются они по отельным сетевым интерфейсам через приватную подсеть VPC. Нагружать я пробовал разными утилитами: ab, wrk, h2load, siege. Наиболее удобным оказался wrk, т.к. ab однопоточен и выдаёт искажённую статистику если в сети есть ошибки.

Причины дропов отдельная загадка с которой надо разбираться, однако, тот факт, что проблемы есть не только при использовании dpdk, но и с nginx, говорит о том, что дело кажется не в том, что примером что-то не так. При большом трафике в EC2 можно наблюдать некоторое количество дропов, для обычных приложений это будет незаметно, но в случае с ab любой retransmit затягивает общее время и ab, в результате чего данные о среднем количестве запросов в секунду оказываются непригодными.

Если суммарная производительность с 2-х инстансов равна 1, то значит я не упёрся в производительность самого wrk. Тест я проводил в две стадии, вначале запускал wrk на 1 инстансе, потом на 2-х.

Результат тестирования примера dpdk на r4.8xlarge

запуск wrk c 1 инстанса

30. root@ip-172-30-0-127:~# wrk -t64 -d10s -c4000 --latency http://172. 10/testu
Running 10s test @ http://172. 4. 4. 30. 19ms 63. 10/testu
64 threads and 4000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 32. 26ms 85. 43ms 817. 97k 4. 01%
Req/Sec 15. 97k 93. 04k 113. 58ms
75% 17. 47%
Latency Distribution
50% 2. 94ms
99% 206. 57ms
90% 134. 10s, 1. 03ms
10278064 requests in 10. 11
Transfer/sec: 172. 70GB read
Socket errors: connect 0, read 17, write 0, timeout 0
Requests/sec: 1017645. 30. 75MB

Запуск wrk c 2-х инстансов одновременно
root@ip-172-30-0-127:~# wrk -t64 -d10s -c4000 --latency http://172. 10/testu
Running 10s test @ http://172. 4. 4. 30. 28ms 119. 10/testu
64 threads and 4000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 67. 64s 88. 20ms 1. 99k 4. 90%
Req/Sec 7. 62k 96. 58k 132. 31ms
75% 103. 67%
Latency Distribution
50% 2. 51ms
99% 563. 45ms
90% 191. 10s, 0. 56ms
5160076 requests in 10. 92
Transfer/sec: 86. 86GB read
Socket errors: connect 0, read 2364, write 0, timeout 1
Requests/sec: 510894. 73MB

30. root@ip-172-30-0-225:~# wrk -t64 -d10s -c4000 --latency http://172. 10/testu
Running 10s test @ http://172. 4. 4. 30. 87ms 148. 10/testu
64 threads and 4000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 74. 64s 93. 64ms 1. 22k 2. 45%
Req/Sec 8. 51k 81. 59k 42. 41ms
75% 110. 21%
Latency Distribution
50% 2. 66ms
99% 739. 42ms
90% 190. 10s, 0. 67ms
5298083 requests in 10. 67
Transfer/sec: 89. 88GB read
Socket errors: connect 0, read 0, write 0, timeout 148
Requests/sec: 524543. 04MB

Nginx дал такие результаты

запуск wrk c 1 инстанса

30. root@ip-172-30-0-127:~# wrk -t64 -d10s -c4000 --latency http://172. 10/testu
Running 10s test @ http://172. 4. 4. 30. 36ms 56. 10/testu
64 threads and 4000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 14. 92s 95. 41ms 1. 27k 3. 26%
Req/Sec 15. 23k 83. 30k 72. 38ms
75% 6. 53%
Latency Distribution
50% 3. 95ms
99% 234. 82ms
90% 10. 10s, 2. 99ms
9813464 requests in 10. 79
Transfer/sec: 214. 12GB read
Socket errors: connect 0, read 1, write 0, timeout 3
Requests/sec: 971665. 94MB

Запуск wrk c 2-х инстансов одновременно

30. root@ip-172-30-0-127:~# wrk -t64 -d10s -c4000 --latency http://172. 10/testu
Running 10s test @ http://172. 4. 4. 30. 91ms 82. 10/testu
64 threads and 4000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 52. 04s 82. 19ms 1. 05k 3. 93%
Req/Sec 8. 62k 89. 09k 55. 66ms
75% 94. 11%
Latency Distribution
50% 3. 83ms
99% 354. 87ms
90% 171. 10s, 1. 26ms
5179253 requests in 10. 10
Transfer/sec: 113. 12GB read
Socket errors: connect 0, read 134, write 0, timeout 0
Requests/sec: 512799. 43MB

30. root@ip-172-30-0-225:~# wrk -t64 -d10s -c4000 --latency http://172. 10/testu
Running 10s test @ http://172. 4. 4. 30. 38ms 121. 10/testu
64 threads and 4000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 64. 67s 90. 56ms 1. 30k 2. 32%
Req/Sec 7. 94k 82. 54k 34. 68ms
75% 103. 10%
Latency Distribution
50% 3. 05ms
99% 561. 32ms
90% 184. 10s, 1. 31ms
4692290 requests in 10. 93
Transfer/sec: 102. 01GB read
Socket errors: connect 0, read 2, write 0, timeout 21
Requests/sec: 464566. 77MB

конфиг nginx

user www-data;
worker_processes auto;
pid /run/nginx.pid;
worker_rlimit_nofile 50000;
events {
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;
location / {
return 200 «answer.padding_____________________________________________________________»;
}
}
}

Итого, мы видим, что в обоих примерах получается примерно 1М запросов в секунду, только nginx использовал для этого все 32 cpu, а dpdk только одно. Возможно, EC2 опять подкладывает свинью и 1М rps это ограничение сети, но даже если это так, то результаты не сильно искажены, т. к. добавление в пример задержки вида for(int x=0;x<100;++x) http→request_url[0] = ‘a’ + (http->request_url[0] % 10) перед отправкой пакета уже снижало rps, что означает почти полную загрузку cpu полезной работой.

Если включить checksum offloading, т. В процессе экспериментов обнаружилась одна загадка, которую я пока разрешить немогу. рассчёт контрольных сумм для ip и tcp заголовка самой сетевой картой, то общая производительность падает, а latency улучшается.
Вот запуск с включенным offloading е.

30. root@ip-172-30-0-127:~# wrk -t64 -d10s -c4000 --latency http://172. 10/testu
Running 10s test @ http://172. 4. 4. 30. 91ms 614. 10/testu
64 threads and 4000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 5. 35ms 96. 33us 28. 48k 1. 17%
Req/Sec 10. 89k 98. 51k 69. 91ms
75% 6. 78%
Latency Distribution
50% 5. 19ms
99% 6. 01ms
90% 6. 10s, 1. 99ms
6738296 requests in 10. 71
Transfer/sec: 113. 12GB read
Requests/sec: 667140. 25MB

А вот с checksum на cpu

30. root@ip-172-30-0-127:~# wrk -t64 -d10s -c4000 --latency http://172. 10/testu
Running 10s test @ http://172. 4. 4. 30. 19ms 63. 10/testu
64 threads and 4000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 32. 26ms 85. 43ms 817. 97k 4. 01%
Req/Sec 15. 97k 93. 04k 113. 58ms
75% 17. 47%
Latency Distribution
50% 2. 94ms
99% 206. 57ms
90% 134. 10s, 1. 03ms
10278064 requests in 10. 11
Transfer/sec: 172. 70GB read
Socket errors: connect 0, read 17, write 0, timeout 0
Requests/sec: 1017645. 75MB

Но почему с рассчётом checksum на карте latency оказывается почти константной равной 6ms, а если считать на cpu, то плавает от 2,5ms до 817ms? OK, я могу объяснить падение производительности тем, что сетевая карта тормозит, хотя это странно, она как бы ускорять должна. Сам DPDK работает далеко не на всех сетевых картах и перед его использованием надо сверится со списком. Задачу бы сильно упростил невиртуальный стенд с прямым подключением, но у меня такого нет к сожалению.

И напоследок опрос.


Оставить комментарий

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

В Германии разработали требования к домашним маршрутизаторам

Продолжительное время в Интернете регулярно появляются статьи об уязвимости маршрутизаторов для SOHO сегмента. Я тоже публиковал статью как обнаружить, что Ваш Микротик взломан. Резкий рост участников нашего канала в Телеграм (@router_os) показал, что проблема крайне остра. Но проблема стоит глобальней. ...

[Перевод] Архитектуры нейросетей

Перевод Neural Network Architectures Давайте рассмотрим историю их развития за последние несколько лет. Алгоритмы глубоких нейросетей сегодня обрели большую популярность, которая во многом обеспечивается продуманностью архитектур. Если вас интересует более глубокий анализ, обратитесь к этой работе. Подробнее здесь. Сравнение популярных ...