Я хочу рассказать о такой штуке как 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:
После этого, можно собрать пример выполнив make в каталоге с ним. Надо только создать переменную окружения RTE_SDK которая указывает на каталог с DPDK.
Тут лежит полный код примера. Он состоит из инициализации, реализации примитивной версии tcp/ip и примитивного http парсера. Начнём с инициализации
В тот момент, когда через dpdk-setup.py мы биндим выбранный сетевой интерфейс к драйверу dpdk, этот сетевой интерфейс перестаёт быть доступным для ядра. После этого, любые пакеты которые придут на этот интерфейс сетевая карта запишет через DMA в очереди, которые мы ей предоставили.
А вот цикл обработки пакетов.
Для чтения пакетов из очереди используется функция rte_eth_rx_burst, если в очереди что-то есть, то она прочитает пакеты и положит их в массив. Если в очереди ничего нет — вернётся 0, в этом случае нужно сразу же вызвать её ещё раз. Да, такой подход «расходует» процессорное время в пустую, если в данный момент данных в сети нет, но если мы уж взяли dpdk, то предполагается, что это не наш случай. *Важно, функция не thread-safe, нельзя читать из одной очереди в разных процессах* После завершения обработки пакета надо вызвать rte_pktmbuf_free. Для отправки пакета можно использовать функцию rte_eth_tx_burst, которая положит rte_mbuf полученный из rte_pktmbuf_alloc в очередь сетевой карты.
После того как разобрали заголовки пакета надо будет построить tcp сессию. tcp протокол изобилует различными частными случаями, специальными ситуациями и опасностями denial of service. Реализация более-менее полноценного tcp отличное упражнение для опытного разработчика, но тем не менее, не входит в рамки описываемого тут. В примере, tcp реализован ровно настолько, чтобы хватило для тестирования. Реализована таблица сессий на базе hash table поставляемого вместе с dpdk, установка и разрыв tcp соединения, передача и приём данных без учёта потерь и переупорядочивания пакетов. Hash table из dpdk имеет важное ограничение о том, что в несколько потоков можно читать, но нельзя писать. Пример сделан однопоточный и эта проблема тут не важна, а в случае обработки трафика на нескольких ядрах можно использовать RSS, пошардить hash table и обойтись без блокировок.
Парсер http будет поддерживать только GET читать оттуда URL и возвращать html с запрошенным URL.
После того как пример готов, можно сравнить производительность с 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 однопоточен и выдаёт искажённую статистику если в сети есть ошибки.
При большом трафике в EC2 можно наблюдать некоторое количество дропов, для обычных приложений это будет незаметно, но в случае с ab любой retransmit затягивает общее время и ab, в результате чего данные о среднем количестве запросов в секунду оказываются непригодными. Причины дропов отдельная загадка с которой надо разбираться, однако, тот факт, что проблемы есть не только при использовании dpdk, но и с nginx, говорит о том, что дело кажется не в том, что примером что-то не так.
Тест я проводил в две стадии, вначале запускал wrk на 1 инстансе, потом на 2-х. Если суммарная производительность с 2-х инстансов равна 1, то значит я не упёрся в производительность самого wrk.
Итого, мы видим, что в обоих примерах получается примерно 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
А вот с checksum на cpu
OK, я могу объяснить падение производительности тем, что сетевая карта тормозит, хотя это странно, она как бы ускорять должна. Но почему с рассчётом checksum на карте latency оказывается почти константной равной 6ms, а если считать на cpu, то плавает от 2,5ms до 817ms? Задачу бы сильно упростил невиртуальный стенд с прямым подключением, но у меня такого нет к сожалению. Сам DPDK работает далеко не на всех сетевых картах и перед его использованием надо сверится со списком.
И напоследок опрос.
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)
{
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;
}
В тот момент, когда через 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 сессию. tcp протокол изобилует различными частными случаями, специальными ситуациями и опасностями denial of service. Реализация более-менее полноценного tcp отличное упражнение для опытного разработчика, но тем не менее, не входит в рамки описываемого тут. В примере, tcp реализован ровно настолько, чтобы хватило для тестирования. Реализована таблица сессий на базе hash table поставляемого вместе с dpdk, установка и разрыв tcp соединения, передача и приём данных без учёта потерь и переупорядочивания пакетов. Hash table из dpdk имеет важное ограничение о том, что в несколько потоков можно читать, но нельзя писать. Пример сделан однопоточный и эта проблема тут не важна, а в случае обработки трафика на нескольких ядрах можно использовать 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, ð_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 однопоточен и выдаёт искажённую статистику если в сети есть ошибки.
При большом трафике в EC2 можно наблюдать некоторое количество дропов, для обычных приложений это будет незаметно, но в случае с ab любой retransmit затягивает общее время и ab, в результате чего данные о среднем количестве запросов в секунду оказываются непригодными. Причины дропов отдельная загадка с которой надо разбираться, однако, тот факт, что проблемы есть не только при использовании dpdk, но и с nginx, говорит о том, что дело кажется не в том, что примером что-то не так.
Тест я проводил в две стадии, вначале запускал wrk на 1 инстансе, потом на 2-х. Если суммарная производительность с 2-х инстансов равна 1, то значит я не упёрся в производительность самого wrk.
Результат тестирования примера dpdk на r4.8xlarge
запуск wrk c 1 инстанса
Запуск wrk c 2-х инстансов одновременно
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
Запуск wrk c 2-х инстансов одновременно
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 дал такие результаты
запуск wrk c 1 инстанса
Запуск wrk c 2-х инстансов одновременно
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
Запуск wrk c 2-х инстансов одновременно
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
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_____________________________________________________________»;
}
}
}
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
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
А вот с checksum на 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, я могу объяснить падение производительности тем, что сетевая карта тормозит, хотя это странно, она как бы ускорять должна. Но почему с рассчётом checksum на карте latency оказывается почти константной равной 6ms, а если считать на cpu, то плавает от 2,5ms до 817ms? Задачу бы сильно упростил невиртуальный стенд с прямым подключением, но у меня такого нет к сожалению. Сам DPDK работает далеко не на всех сетевых картах и перед его использованием надо сверится со списком.
И напоследок опрос.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Нужен ли новый web-сервер для статики, который быстрее nginx
53.27% Нет, всегда тормозят скрипты, а остальное ни кого не волнует114
46.73% Да, если статика будет быстрее, то люди начнут переписывать конфиги для нового сервера100
Проголосовали 214 пользователей. Воздержались 68 пользователей.