Очередное решение HighLoadCup на Go

в 11:43, , рубрики: Go, highloadcup, высокая производительность

Я думаю уже многие пользователи хабра знают что на прошлой недели закончился HighLoadCup от Mail.ru (из-за обилия количества статей от участников). Я хотел бы также поделиться своим решением с сообществом.

Описание задачи

Существует три вида сущностей: User, Location, Visit. Необходимо написать REST-API для доступа к ним, т.е. получается необходимо обработка 6 запросов.

  • {GET, POST} /user/:id — получение или изменение пользователя
  • {GET, POST} /location/:id — получение или изменение локации
  • {GET, POST} /visit/:id — получение или изменение посещения локации пользователем
  • POST /user/new — добавление нового пользователя
  • POST /location/new — добавление новой локации
  • POST /visit/new — добавление нового посещения локации пользователем

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

Начало

Изначально я начал писать на С++, но до релиза так и не дошло. Увидев большое количество решений в топе на Go я решил тоже попробовать его. Этот язык мне показался значительно более подходящим для разработки серверных приложения, он имеет из коробки весь необходимый функционал причём в очень качественном исполнении. Впрочем, уже после первых тестов стало понятно, что ни net/http, ни encoding/json не подходят для данного конкурса в связи с большим количеством мусора, который генерируется внутри них.

Раунд первый

Изначально данных было всего 200 мб в распакованном виде, поэтому я подумал что возможно даже хранить готовые JSON-строки для каждой сущность. В качестве http-сервера по советам гошников конкурса (спасибо им большое за оказанную помощь на старте знакомства с языком) я выбрал fasthttp, для парсинга JSON buger/jsonparser (позволяет парсинг без аллокаций и работать только с нужной информацией, игнорируя остальную), а генерацию производил руками, по-скольку никакой обработки русскоязычных строк не требовалось.

type User struct {
	id  uint
	email string
	first_name  string
	last_name string
	gender bool
	birth_date int64

	age   int
	visits Visits
	json []byte
}

type Location struct {
	id uint
	place string
	country string
	city string
	distance int64

	visits Visits
	json []byte
}

type Visit struct {
	id uint
	location *Location
	user *User
	visited_at int64
	mark int64

	json []byte
}

Такие сущности у меня вышли на старте, в посещении для ускорения я решил хранить сразу указатели на соответствующих пользователя и локацию. Итог получился весьма удовлетворительным, все данные прекрасно помещались и в итоге я даже смог попасть в 10-ку (не на долго правда).

Возраст

Отдельное внимание я хочу уделить вопросу расчёта возраста посетителя. Этот вопрос остро стоял для многих участников не смотря на наличие примера FAQ, меня тоже не миновала данная участь и в первые дни много ошибок было именно из-за неверного подсчёта. Также несколько раз в сгенерированных данных возникали пользователи у которых день рождения именно в день тестов, что тоже принесло некоторые трудности.

Итогом стал такой код:

func countAge(timestamp *int64) int {
	now := time.Now()
	t := time.Unix(*timestamp, 0)

	years := now.Year() - t.Year()
	if now.Month() > t.Month() || now.Month() == t.Month() && now.Day() >= t.Day() {
		years += 1
	}

	return years
}

Увеличение нагрузки

За несколько дней до финала после демократического и открытого голосования создатели конкурса в 10 раз увеличили количество данных и в 2 раза максимальный RPS. После этого моё решение перестало влезать в память и потребовало изменений. Пришлось убрать из структур заранее сгенерированный JSON и создавать его на лету при запросе, что увеличило, конечно же, реализм. В тоже время я подумал, а почему бы не добавить в структуры посетителя и локации сразу ссылки на посещения, которые с ними связаны. Это значительно увеличило скорость программы, так как не пришлось проходить каждый раз весь массив посещений (который теперь содержал 10 миллионов записей).

Решение после этого заработало, но по скорости стало уступать другим решениям и я рисковал не пройти в финал. Не долго думая я выкинул fasthttp и перешёл на tcp-сокеты и epoll. Размер окна в нашей системе был в районе 65кб и пакеты точно приходили и отправлялись полностью и это дало большое поле для костылей, которые в продакшене точно работать не будут.

Финал

К финалу я подошёл на 39 месте, чему я был несомненно очень рад. Это первое моё участие в подобном конкурсе, первое знакомство с Go и highload (хотя я бы не назвал это highload). Финал начался плохо, сервер вылетел из-за ошибки одновременного доступа на чтение и запись (до финала таких проблем не возникало и локеры были вырезаны), тем не менее на одной из волн удалось показать лучший результат за все запуски что дало возможность занять 28 место.

В целом это был очень интересный и познавательный (для меня, по крайней мере) конкурс. Я хотел бы выразить огромную благодарность организаторам и жду следующего учтя все ошибки и «фичи» нынешнего (busy-polling, например). Правда в будущем обещан был больший упор именно на логику, а не на сетевой стек, что будет более интересным.

P.S. Жду футболочку :)

Код (вдруг кому-то станет интересно) можно посмотреть в моём репозитории.

Автор: Александр

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js