Pull to refresh

Браузерная стратегия «Пути Истории». Архитектура и эволюция проекта

Reading time6 min
Views12K
В этой статье я расскажу о разработке и эволюции технической части браузерной игры «Пути Истории».
Уделю внимание выбору языка программирования, базы данных, технологии и архитектуры. Расскажу о хостинге.

Пути Истории — это массовая браузерная стратегическая игра. Проект начинался с энтузиазма одного человека и вырос до серьзного проекта с немалой аудиторией.

Для разработки движка был выбран язык C++ по трем причинам:
  1. он быстр, что важно для данного проекта;
  2. гибок, что позволяет реализовывать некоторые возможности оптимально;
  3. я его знаю лучше, чем другие подходящие.

Суть работы движка — это получение запроса, формирование и отдача страницы.
Базу данных выбрал MySQL лишь потому, что она достаточно популярна и подобные проекты часто делаются с использованием MySQL. В тот момент я не имел опыта работы с базами данных.
Сразу встал вопрос архитектуры. Была выбрана следующая модель:
Движок делится на две части (назовем их Д1, Д2).
Д1 получает запрос, передает в один из 8-ми свободных потоков. Поток анализирует запрос, запрашивает необходимые данные из базы, формирует страницу и возвращает её. Д1 не умеет вносить изменения в базу данных. Для уменьшения числа запросов к базе данных многие данные кэшируются на Д1.

В некоторых случаях Д1 получает запрос на изменение состояния мира (заказано строение в городе игрока, отправлены войска и т. п.). В этом случае Д1 передает запрос на Д2 (связь на сокетах). К Д2 может подключиться более одного Д1 (каждый из которых имеет по 8 нитей, и следовательно может одновременно передать 8 инструкций на Д2). Д2 одновременно выполняет только одну инструкцию, остальные ждут в очереди. Выполнение одной инструкции для базы данных осуществляется как одна транзакция. При успешной отработке инструкции в базу вносятся изменения, при недопустимой инструкции транзакция отменяется и все её изменения откатываются. Важно отметить, что недопустимые инструкции отсекаются еще на Д1, но бывает, что инструкция стала недопустимой уже после передачи её на Д2 (например, в предыдущей инструкции город потратил ресурсы, а в данной пытается заказать строение за несуществующие ресурсы, но обе инструкции пришли почти одновременно). Вся система может работать без Д2, но только в режиме «read only» — ничего изменить нельзя, все таймеры событий идут, но по окончании «зависают». Если после этого включить Д2, то работа системы восстановится, словно никакого сбоя не было, все события обработаются в правильном порядке.

Вначале использовался веб сервер Apache. Он был выбран потому, что популярен и имеет сборку под Windows. Д1 подключался к Apache по технологии ISAPI, тоесть как dll библиотека. Apache принимал запросы и передавал их в подключенную к себе библиотеку. Сам по себе Apache работал достаточно медленно. Поэтому в какой-то момент проект был переведен на связку nginx + FastCGI.

Веб сервер nginx очень удобен как в настройке, так и в использовании. Скорость отдачи страниц возросла. К тому же nginx очень быстро раздает статический контент.
Как работает FastCGI? Сам движок из dll библиотеки был переделан в самостоятельное приложение. Приложение принимает запросы от веб сервера через сокеты, обрабатывает их, генерирует страницы и, через те же сокеты, возвращает страницы веб серверу. При этом сокеты остаются открытыми и по ним поступают новые запросы. Узнать больше о разработке на C++ с использованием протокола FastCGI можно здесь.

Теперь о хостинге.
До запуска проекта все работало на обычном домашнем компьютере по обычному домашнему кабельному Интернету.
В то время не было финансовой возможности арендовать сервер в дата центре, поэтому первый игровой мир был запущен все на том же домашнем компьютере. Это создавало ряд неудобств: доступ к сети нестабилен, иногда выключался свет в доме, провайдер не выдает трафик по заявленному тарифу и часто проводит технические работы. Проект начал наполняться игроками, нагрузка росла. На обслуживание 1-го мира был поставлен еще один компьютер с другим подключением к Интернету. Сейчас один мир легко работает на 1-м сервере, но тогда еще не все было оптимизировано, а используемые компьютеры были слабыми.

Вскоре был открыт следующий мир. На него было использовано еще два компьютера и два подключения к сети. Уже готовился к запуску мир 3. Все эти компьютеры распологались у меня дома в жилой комнате. С ростом числа серверов росло число проблем. Я уже не мог покидать дом, так как постоянно что-то падало. Кроме форс-мажорных проблем были еще и обычные баги. При возникновении любой неправильной ситуации приложение сразу падало по assert'у, не пытаясь как-то выйти из сложившейся ситуации. Такое решение было выбрано специально. Это заставляло меня всегда в первую очередь бороться в багами, а не тащить их через весь период разработки.
Проект начал приносить доход, и был арендован сервер в дата центре. Сайт игры и оба мира были перенесены на него. Администрировать систему стало значительно проще, но расходы возросли. Третий мир тоже был запущен на этом сервере, но после DDoS атаки я перенес его на отдельный сервер, чтобы первые два мира были вне угрозы.

Разработка велась и тестировалась на ОС Windows. Но код писался сразу без привязки к данной ОС, и, в дальнейшем, понадобился всего один день, чтобы поправить код и скомпилировать проект под FreeBSD.
Для работы с потоками была выбрана библиотека POSIX. Для создания графических файлов я использовал библиотеку FreeImage.

Мониторинг системы.
Изначально мониторинг системы был при помощи мониторов! Любое «падение» сервера можно было обнаружить в виде окна с ошибкой или отсутствия исходящего трафика на графике. Приходилось даже ночью просыпаться по несколько раз и окидывать взглядом все мониторы.
Так долго продолжаться не могло и был написан специальный php скрипт, который постоянно опрашивает сервера, собирает с них данные о состоянии и, по необходимости, отправляет письмо на почту или SMS сообщение на телефон. Данный скрипт был запущен на бесплатном хостинге, где работает и по сей день. Благодаря ему всегда удается оперативно узнать о проблемах и, по возможности, устранить их немедленно.

В следующих статьях я расскажу про разработку проекта от идеи до релиза, про технические решения в движке и форматы хранения данных в базе, про резервное копирование данных и защиту от атак, про механизм формирования страниц.

Основа Д1:

void* operateRequest(void* listen_socket)
{
	//инициализация
	FCGX_Request request;
	assert(!FCGX_InitRequest(&request, *(int*)listen_socket, 0));
	Session* s = new Session;
	//условно бесконечный цикл по приему запросов
	while(FCGX_Accept_r(&request) == 0) {
		stringstream out;
		stringstream header;
		header << "Content-type: text/html";

		//чтение параметров запроса
		string query;
		string addr;
		string referer;
		string post;
		string cookie;
		string agent;
		int content_lenght = 0;
		for(char** envp = request.envp; *envp; ++envp) {
			string v = *envp;
			string::size_type e = v.find('=');
			string p = v.substr(0, e);
			if(p == "REQUEST_URI") query = v.substr(e + 2, v.length());
			if(p == "REMOTE_ADDR") addr = v.substr(e + 1, v.length());
			if(p == "HTTP_COOKIE") cookie = v.substr(e + 1, v.length());
			if(p == "HTTP_REFERER") referer = v.substr(e + 1, v.length());
			if(p == "CONTENT_LENGTH") content_lenght = toInt(v.substr(e + 1, v.length()));
			if(p == "HTTP_USER_AGENT") agent = v.substr(e + 1, v.length());
		}

		//чтение тела запроса
		maximize(content_lenght, 9999);
		char p[10000];
		FCGX_GetStr(p, content_lenght, request.in);
		p[content_lenght] = 0;
		post = p;

		//основная функция. Генерирует header и страницу
		s->work(header, out, addr, cookie, referer, query, post);

		//сборка и возвращение страницы
		header << "\r\n\r\n" << out.str();
		FCGX_PutStr(header.str().c_str(), int(header.str().length()), request.out);

		FCGX_Finish_r(&request);
	}
	return 0;
}

int main() {
	assert(initSocketSystem());
	assert(!FCGX_Init());
	int listen_socket = FCGX_OpenSocket(":8000", 400);
	assert(listen_socket >= 0);
	//создать потоки
	for(int i = 0; i < threads; ++i) {
		pthread_t thread;
		assert(pthread_create(&thread, 0, operateRequest, (void*)&listen_socket) == 0);
	}
	while(true) sleep(1000);
	return 0;
}


Основа Д2:

void operateCommand(asComType com, Socket& sock)
{
	//критическая секция. В ней работает одновременно только один поток
	pthread_mutex_lock(&ascs);
	bool res;
	//сообщить бд о начале транзакции
	assert(sql.put("BEGIN"));
	switch(com) {
		case ASC_TOWNUPDATE:
		{
			//инструкция на простое обновление состояния города
			int id = sock.readVal<int>();
			res = asUpdateTown(id);
		}
		break;
		//другие инструкции
		//…
		//…
		//…
	}
	//в зависимости от успешности выполнения инструкции заканчиваем или отменяем транзакцию
	assert(sql.put(res ? "COMMIT" : "ROLLBACK"));
	//возвращаем результат
	sock.sendVal(res);
	//критическая секция заканчивается
	pthread_mutex_unlock(&ascs);
}

void* clientThread(void* client_socket)
{
	Socket& sock = *(Socket*)client_socket;
	asComType com;
	int bytes;
	//висеть в ожидании запроса
	while((bytes = sock.readVal(com)) && bytes >= 0) {
		//обработать запрос
		operateCommand(com, sock);
	}
	delete &sock;
	return 0;
}

int main()
{
	while(Socket* client = sock.listen()) {
		//создать поток для чтения и обработки данных сокета
		pthread_t thread;
		assert(pthread_create(&thread, 0, clientThread, (void*)client) == 0);
	}
	return 0;
}

Весь представленный код местами специально упрощен для наглядности. Некоторые классы и фукнции опущены.
Tags:
Hubs:
Total votes 65: ↑60 and ↓5+55
Comments26

Articles