HighLoad.org – блог о высоких нагрузках
HighLoad.org > Быстрый поллинг средствами C, memcached, nginx и libevent

Быстрый поллинг средствами C, memcached, nginx и libevent
2010-02-26 19:18 my_fess 
В этом посте я покажу вам как реализовать действительно быстрый поллинг средствами C, и libevent, memcached и nginx. Производительность сервера (неоптимизированный Mac Book) более 2400 запросов в секунду на . Это 144,000 запросов в секунду.
Мы используем поллинг на Plurk и у нас тысячи пользователей онлайн, которые долбают сервис своими запросами. Каждый пользователь делает запрос после апдейтов каждые несколько секунд, поэтому у нас тысячи запросов. Обработка этих запросов начинает становиться все дороже, поэтому я поставил себе цель — оптимизировать это. Сейчас мы используем данный подход на продакшн серверах, и это все использует всего лишь около 2% CPU и совсем немного памяти.

Выбор инструментов

Я мог выбрать различные наборы, но я выбрал следующие компоненты:
  • C и libevent: C низкоуровневый насколько возможно (если конечно вы не хотите программировать на ассемблере) и libevent реализует асинхронное IO для C. libevent также предоставляет достаточно искусную библиотеку HTTP для построения масштабируемых HTTP серверов. libevent используется в memcached.
  • memcached: Проверена уже тысячами стартапов, например таким как Facebook. memcached — легковесный, масштабируемый и чрезвычайно оптимизирован.
  • nginx: Достаточно сказать, что является эталоном российской разработки.
Я предпочитаю использовать memcached, а не Varnish или другой кэширующий инструмент, потому что memcached предоставляет мне больше опций (удаление множества ключей за раз, согласованное хеширование и простое проверенное масштабирование терабайтов кэша). Varnish хороший инструмент для кэша, но он не очень подходит для решения моих задач. Например, как мне переделать решение на Varnish для поддержки долгого поллинга? Я могу добавить эту опцию просто понимая основы построения произвольного сервера с хорошей производительностью, который может быть настроен под мои нужды. Эти все технологии являются неблокирующими, и их производительность и масштабируемость проверена и не раз.

Архитектура быстрого поллинга

Вот архитектура быстрого поллинга: Важно заметить, что в случае, когда необходимых данных нет в кэше, то мы задействуем только ядро написанное на Python. Это означает, что мы пополняем кэш пустыми значениями, если у нас нет ничего нового для клиента.

Самая тяжелая часть: конфигурирование nginx

Поллинг должен поддерживать и POST и GET запросы, но кажется nginx больше предпочитает GET запросы. Вот проблема с которой я столкнулся: обработчик error_page пропускает POST данные, которые посланы с POST запросом. Это была главная проблема, и для нахождения решения потребовалось немного хакерства и чтения листа рассылки nginx:
location /Poll/
{
    proxy_pass http://localhost:8080/;
    error_page 501 404 502 = @fallback;
}

location @fallback
{
    proxy_pass http://localhost:14002;
}
Это означает, что мы сначала все отправим в localhost:8080, если там фэйл, то используем @fallback. Если описывать более детально: в случае если данные в кэше, мы просто перенаправим запрос на сервер и вернем результат в случае если данных нет в кэше, мы направим запрос на ядро, написанное на Python Проблема решена! Я не использовал модуль memcached для nginx, потому что он поддерживает только один сервер memcached и только GET запросы.

Бенчмарк

Прежде чем показать C код, я хочу показать вам бенчмарк ab от Apache (около 2400 запросов в секунду на Mac Book):
amixs-macbook:~ amix$ ab -c 50 -n 5000 http://127.0.0.1/Poll/getMessages
This is ApacheBench, Version 2.3 <$Revision: 655654 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking 127.0.0.1 (be patient)
Completed 500 requests
Completed 1000 requests
Completed 1500 requests
Completed 2000 requests
Completed 2500 requests
Completed 3000 requests
Completed 3500 requests
Completed 4000 requests
Completed 4500 requests
Completed 5000 requests
Finished 5000 requests

Server Software:        nginx/0.7.44
Server Hostname:        127.0.0.1
Server Port:            80

Document Path:          /Poll/getMessages
Document Length:        5 bytes

Concurrency Level:      50
Time taken for tests:   2.068 seconds
Complete requests:      5000
Failed requests:        0
Write errors:           0
Total transferred:      735000 bytes
HTML transferred:       25000 bytes
Requests per second:    2417.43 [#/sec] (mean)
Time per request:       20.683 [ms] (mean)
Time per request:       0.414 [ms] (mean, across all concurrent requests)
Transfer rate:          347.03 [Kbytes/sec] received

Connection Times (ms)
min  mean[+/-sd] median   max
Connect:        0    0   0.6      0       7
Processing:     5   20   3.2     20      39
Waiting:        4   20   3.1     19      39
Total:          6   21   3.0     20      39

Percentage of the requests served within a certain time (ms)
50%     20
66%     21
75%     21
80%     22
90%     25
95%     26
98%     29
99%     32
100%     39 (longest request)

Почему не использовать Comet?

Comet — это по сути пуши апдейтов от сервера. Comet может и новое модное слово, но я все еще думаю, что поллинг проще реализовывать и масштабировать. Из всего что я видел, я сделал вывод - все Comet решения слишком уж хакерские, изворотливые и очень тяжело масштабируются. Они плохо масштабируются, потому что web платформы построены на модели запрос-ответ. Это все в будущем может измениться, но до тех пор я думаю, что самым безопасным путем добиться цели является поллинг.

C код

Это кусок моего первоначального C кода, пожалуйста сообщите, если увидите какие-нибудь проблемы в нем или у вас просто будут предложения по улучшению. На самом деле я получу удовольствие мастерить эту C программу вместе!
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

//
// Глобальное 
//

typedef struct
{
	char* host;
	int port;
} MServer;

int NUM_OF_SERVERS = 0;
MServer memcache_servers[4];
memcached_st* tcp_client;

//
// информация о Memcached
//

void add_mserver(char* host, int port)
{
	memcache_servers[NUM_OF_SERVERS].host = host;
	memcache_servers[NUM_OF_SERVERS].port = port;
	NUM_OF_SERVERS++;
}

void init_memcache_servers()
{
	add_mserver("localhost", 11211);
	add_mserver("192.168.0.51", 11211);

	// Добавить клиента к memcached

	tcp_client = memcached_create(NULL);

	int i;

	for(i=0; i < NUM_OF_SERVERS; i++)
	{
      	      	memcached_server_add
		(
			tcp_client, 
			memcache_servers[i].host,
			memcache_servers[i].port
		); 
	}
}


//
// Берет `uri` и удаляет аргументы запроса.
//

char* parse_path(const char* uri)
{
	char c;
	char* ret;

	int i, j, in_query = 0;

	ret = malloc(strlen(uri) + 1);

	for(i = j = 0; uri[i] != '\0'; i++)
	{
		c = uri[i];

		if(c == '?')
		{
			break;
		}
		else
		{
			ret[j++] = c;
		}
	}
	
	ret[j] = '\0';

	return ret;
}

// 
// Проверить находится ли path в memcached, если нет
// то соединение разрывается без ответа
// и тогда nginx перенаправит запрос
//

void memcache_handler(struct evhttp_request* req, void* arg)
{
	struct evbuffer* buf;
	buf = evbuffer_new();

	if(buf == NULL)
	{
		err(1, "failed to create response buffer");
	}

	char key[200];
    
	strcpy(key, "/Poll");

	char* request_uri = parse_path(req->uri);

	// Проверка на переполнение буфера

	if(strlen(request_uri) > 125)
	{
		evhttp_connection_free(req->evcon);
	}
	else
	{
		strcat(key, request_uri);

		// Получить результат от memcached
		
		memcached_return rc;

		char* cached;
		size_t string_length;
		uint32_t flags;

		cached = memcached_get
			(
				tcp_client,
				key,
				strlen(key),
				&string_length,
				&flags,
				&rc
			);

		// Вернуть результат клиенту

		if(cached)
		{
			evbuffer_add_printf(buf, "%s", cached);
			evhttp_send_reply(req, HTTP_OK, "OK", buf);
			free(cached);
		}
		else
		{
			evhttp_connection_free(req->evcon);
		}
	}
	
	free(request_uri);
	evbuffer_free(buf);
}


int main(int argc, char** argv)
{
	init_memcache_servers();
	struct evhttp* httpd;
	event_init();
	httpd = evhttp_start(argv[1], atoi(argv[2]));
	evhttp_set_gencb(httpd, memcache_handler, NULL);
	event_dispatch();
	evhttp_free(httpd);
	memcached_free(tcp_client);
	return 0;
}
Мне было лень делать считывальщик настроек, в основном потому что в Python я бы мог просто написать:
open("server.config").readlines()
но в C все будет более громоздко :-p

Послесловие

plurk@web04:~$ ps aux | grep poll_server
plurk     29835  2.3  0.0   2376  1048 ?
S    Mar24  27:23 /home/plurk/poll_server/poll_server 0.0.0.0 16000
Сервер достаточно стабилен (ни одного падения с того момента как я запустил его). Использует очень мало CPU и очень мало памяти. Если я остановлю сервер, сервер nginx перенаправит все на сервера с Python программой. Я сомневаюсь, что Varnish или пропатченный nginx будут работат ь быстрее или стабильнее, и к тому же они добавили бы большую концептуальную сложность. Лично я бы лучше пофиксил ошибки в программе из 100 строк, чем в программе с тысячами строк. Это разделение задач и разделение сложностей. Конечно, я бы мог выбрать расширенный nginx + memcached или хакнутый Varnish, но я решил не создавать лишних проблем и выбрал простое решение - изучить немного новых для меня вещей и решить проблему в очень необычном виде. Как всегда приятного вам кодинга и я надеюсь пост доставил!

Автор статьи: amix Дата: 24 марта 2009 Оригинал статьи


комментарии [0]  | комментировать

  © 2010-2018 HIGHLOAD