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)
    {
    	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, &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 однопоточен и выдаёт искажённую статистику если в сети есть ошибки.

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

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

    Результат тестирования примера dpdk на r4.8xlarge
    запуск 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

    Запуск 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 инстанса

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

    Итого, мы видим, что в обоих примерах получается примерно 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

    • 56.8%Нет, всегда тормозят скрипты, а остальное ни кого не волнует100
    • 43.1%Да, если статика будет быстрее, то люди начнут переписывать конфиги для нового сервера76
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 17

      –5
      Отличная техническая статья. Побольше бы таких на Хабре.
        +9

        Я стал страшно далек от таких низкоуровневых деталей, но разве nginx не использует для раздачи статики sendfile? Тогда тоже не должно быть накладных расходов на копирование в userspace и обратно.


        А так может получиться, что когда реализуешь полноценные TCP и HTTP, поддержку прочих плюшек типа HTTPS, rate-limiting, error pages, и т.д. и т.п., окажется, что все выигрыши где-то потерялись, а стоимость поддержки такого решения чрезвычайно высока.

          0

          Для TCP есть библиотеки под dpdk. А http не так уж и сложно/заумно реализовывать, если надо.
          Понятно, что для раздачи статики лучше взять nginx- его все админы умеют настраивать.
          Ну и использовать http для высокой нагрузки — это глупо. Канал загружает плохо (да, я знаю про http Pipelining), накладных расходов на заголовки много. Бинарные протоколы рулят.

            +4
            А http не так уж и сложно/заумно реализовывать, если надо.

            С этих слов начиналось очень много epic fail'ов. Практически все не так уж сложно реализовать. До тех пор пока не надо укладываться в дедлайны, пока не нужно думать о поддержке всего этого в течении многих лет (особенно будет «приятно», если автор подобного велосипеда на атомном реакторе уйдет в другое место, а поддерживать его придется менее продвинутым коллегам), исправлении уязвимостей безопасности, и так далее и так далее.

              0
              С другой стороны, все равно иногда приятно осозновать, что есть какое-то движение в сторону оптимизации программ. Что производительность достигается не только за счет выпуска более мощных процессоров (которые уже не так, чтобы и более мощные по сравнению с предыдущими версиями). Однонаправленное движение в сторону тормознутости всего и вся (потому что ресурсы же есть) уже напрягает
                0

                Эх… В жизни все направлено на оптимизацию прибыли, а оптимизация быстродействия часто противоположна оптимизации прибыли.


                Как качественно перевести книгу издательству, если качественный перевод требует большего времени и больших затрат на переводчиков (кого попало не возьмешь, а грамотный опытный переводчик будет стоить дороже)? Пока будет готовиться качественный перевод, скажем, книги по Rust, другие издательства напечатают книги по Rust плохого качества, и убьют рынок для качественной книги — затраты на качественный перевод никогда не отобьются.


                Как сделать качественный быстродействующий сайт, если ресурсы надо тратить на захват рынка, исправление неотложных багов, добавление свистелок, которые нравятся тем пользователям, от которых зависит прибыль?


                К сожалению, как-то приходится с этим жить.


                Если любая компания вместо того, чтобы сосредоточиться на своем продукте, будет писать собственные реализации TCP и HTTP, прогресс совсем замрет.

              0
              (да, я знаю про http Pipelining)

              Зато создаётся ощущение, что про HTTP/2 не знаете

              0
              Ну за полноценным TCP лучше действительно использовать готовый ядерный TCP, но если надо быстро, а не универсально то можно писать свой.

              sendfile будет работать только когда на диске файл лежит сразу как надо. Если используется gzip или ssl, то всё будет копироваться в userspace и обратно.

              А ещё файл на диск можно положить сразу разбитым на tcp пакеты, и тогда нужно будет только поправить адреса и checksum, ну если приспичило ну совсем быстро что-то отдать.
                0

                Когда-то был еще HTTP сервер в ядре...

                  +2

                  У всех разный опыт, конечно. Рядом со мной всегда происходят такие истории:
                  1) Можно реализовать это самостоятельно, у нас простой вырожденный случай, мы получим свое быстрое решение
                  2) Через месяц: «Ой, а вот надо бы этот и это случай поддержать»
                  3) Еще через месяц: «Оказывается, существующее ПО рассчитывает на то и это, придется наш велосипед расширить для совместимости с существующими клиентами»
                  4) Еще через месяц: «На production страшный баг, срочно требуется до 6 утра найти и накатить патч, иначе бизнес потеряет кучу денег»


                  И т.д. Свое решение всегда будет расти. Его надо будет расширять, портировать на новые платформы, тестировать, поддерживать. И в большинстве случаев на это потребуется намного больше денег, чем купить более дорогой или еще один сервер.


                  То есть теоретически я красоту и крутость понимаю. Но на практике, скорее всего, выгоду из этого может извлечь только Google, Яндекс и Cloudflare. Остальные, если сунутся, увязнут по шею.


                  Да и у всех разные задачи, конечно. В моей практике есть гораздо более доступные цели для оптимизации. Если я в текущем проекте начну оптимизировать раздачу статики, то даже самый дотошный пользователь не заметит никаких изменений. Зато есть десяток мест, где я могу не только произвести вполне заметное измеримое улучшение, но и при этом получить еще более поддерживаемый читаемый код.


                  Но это, конечно, мой случай. Допускаю, что есть места, где другая ситуация, и единственным узким местом может стать копирование данных из ядра в процесс и обратно. Просто уверен, что таких проектов, единицы проектов в мире.

                +6
                DPDK (https://www.dpdk.org/) — это фреймворк для работы с сетью в обход ядра.


                Ядро ОС выполняет функцию (де)/мультиплексирования при работе приложений с сетевым оборудованием. Тут получается, что данный фреймворк фактически монопольно, предоставляет сеть только одному приложению, из-за этого становится быстрее, но теряется универсальность.

                Реальное применение подобного подхода очень ограниченно, хотя и не нулевое.
                  0
                  Обычно на машине ровно одно нагруженное приложение, а остальное уже не так требовательно. Можно сделать виртуальный интерфейс и бросать туда все пакеты, которые не нужны dpdk приложению. В dpdk вроде что-то даже есть, что как-то позволяет подобное делать — я пока ещё далеко не со всем разобрался.
                    0
                    Не совсем так, зависит от сетевой карты, в частности, на современных сетевых картах поддерживается SR-IOV, DPDK может нормально работать в виртуальной машине на VF, а на хосте или в других виртуальных машинах можно запускать другие приложения. При желании — на новом инстансе DPDK :)
                    +1
                    HFT-шники одобряют этот пост
                      0
                      действительно 1кк рпс на одном ядре? неужели сетевая подсистема ядра дает такой большой оверхед? можно погонять теже тесты на одноядерной машине?
                        0
                        Одноядерной машины, которая поддерживала бы dpdk у меня нет, а на ec2 слабые одноядерные инстансы с dpdk не совместимы. Сам хотел так вначале тестить, чтобы проще сравнивать было, но не нашёл ничего подходящего.
                        0
                        это все начиналось делаться интелом для NFV всяких, когда трафиком клаудов рулит не железки, а обычные сервера, так что веб сервер на dpdk это некоторое извращение)

                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                        Самое читаемое