Я думаю уже многие пользователи хабра знают что на прошлой недели закончился 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. Жду футболочку :)
Код (вдруг кому-то станет интересно) можно посмотреть в моём репозитории.
Автор: Александр