Что делать
119 subscribers
209 photos
3 videos
4 files
133 links
Не смешно
Download Telegram
context.WithValue медленный

длиннопоста не будет. Он под капотом рефлексию трогает. Придется в индиге ещё и свои контексты пилить
🔥2😁1
Сделал для тела запроса структуру, имплементирующую io.Reader. Теперь можно request.Reader(), и получить свой io.Reader. Красиво.

Правда, из-за некоторых особенностей моей потоковой работы с телом, пришлось чутка закостылить с тем, чтобы сообщать ядру о завершении обработки кусочка тела только при следующем вызове. Но работает стабильно

* Требование сообщать ядру о завершении обработки кусочка тела связана с тем, что этот кусочек тела в виде слайса байт тянется прямиком из tcp сервера, а точнее - буфера для чтения. А значит, если убрать эту синхронизацию, то получаем UB - слайс может быть перезаписан новыми данными
Итак, до беты остаётся лишь имплементировать динамический роутинг. Есть идея ставить в пути маркер {name}, где name - либо пустота, либо имя значения в контексте, которое появится в случае, если путь запроса соответствует шаблону. Поскольку маркер может стоять только между двумя слэшами, нам остаётся лишь пройтись по статичным участкам, и при наступлении маркера - схоронить значение в контекст, где концом значения является слэш (или конец строки)

Почему не регекспы? Ну, они медленные. У меня лично сейчас процесс роутинга укладывается в 80нс, при условии, что у меня получение по ключу из двух хэшмап. Регулярки же укладываются в 700-800нс
🔥2
От канала отписался один человек. И он не узнает, насколько быстро мой алгоритм матчит пути по шаблону…
🤡2
В общем, сравнение пути по шаблону я сделал, и вроде шустрое (само по себе - 80-90нс в худшем случае выдаёт). До 245нс жиреет из-за контекстов (без пропатченных - там до 700-800нс вырастает, прям как регулярки). Сейчас буду пилить префиксное дерево, потому что одним только сравнением сыт не будешь
Что делать
В общем, сравнение пути по шаблону я сделал, и вроде шустрое (само по себе - 80-90нс в худшем случае выдаёт). До 245нс жиреет из-за контекстов (без пропатченных - там до 700-800нс вырастает, прям как регулярки). Сейчас буду пилить префиксное дерево, потому…
Кстати, по поводу пропатченных контекстов - мне нужен конкретно WithValue. О нём я уже писал. Он всё ещё был медленным из-за того, что использовал any (просто any, а не как дженерик), а это интерфейс. А интерфейс под капотом хранит указатель. А указатель утекает на кучу. А утекание на кучу приводит к аллокациям

Переделал под дженерики. С 635нс производительность улучшилась до 245нс, количество аллокаций с 6-10 уменьшились до 3-5, т.е. в два раза. Пока что перемога, посмотрим, насколько префиксное дерево будет влиять на итоговый перфоманс
Я не я, если нет оптимизаций. Для роутинга я решил сделать три уровня поведения:
- Пути все статичные. В таком случае, динамический роутинг нам не нужен, мы его отключаем и фоллбекаемся к текущей реализации
- Пути динамические, но least unique string (самый короткий уникальный префикс) не включает в себя динамическую часть. Тогда мы берём из мапы по этому least unique string нужный нам шаблон, и сверяем
- Пути динамические, и least unique string вмещает в себе динамическую часть. В таком случае, берём это самое наше префиксное дерево, о реализации которого я отпишусь позже
Зашли-вышли, приключение на 20 минут
😁1
Что делать
Я не я, если нет оптимизаций. Для роутинга я решил сделать три уровня поведения: - Пути все статичные. В таком случае, динамический роутинг нам не нужен, мы его отключаем и фоллбекаемся к текущей реализации - Пути динамические, но least unique string (самый…
В общем, с least unique string я решил забить, ибо ну его нафиг. А вот по поводу префиксного дерева, я сделал следующую штуку:

Делим путь на сегменты. Сегмент - это кусочек между слэшами. Каждый сегмент может быть как статическим, так и динамическим. Если статический - мы берём из хэшмапы следующий узел, и идём дальше. А вот если динамический - кладём этот самый сегмент в контекст, и точно также идём дальше

получилось не слишком медленно, что-то около 390нс в самом длинном шаблоне, представленным у меня в тестах. Производительность всего роутера с фоллбеком к префиксному дереву я пока не мерял
А вот интереснее у меня устроен сам роутинг. Ведь если у нас только статические пути, то зачем, спрашивается, использовать префиксное дерево, если оно медленней обычной хешмапы?

Вот и я так подумал. И сделал такую штуку, что у нас динамически, при старте сервера, выбирается имплементация роутинга. Это - просто функция, которая лежит полем в структуре, и мы её вызываем, чтобы получить обработчик для запроса
https://github.com/golang/go/discussions/56010


var all []*Item
for _, item := range items {
all = append(all, &item)
}


Здесь, в all будет len(item) одинаковых указателей, равных указателю на последний элемент в item, потому что item - переменная цикла - является не per-iteration, а per-loop. То есть, переменная item у нас одна-единственная на все итерации, и при каждой итерации её значение затирается.

Это, кстати, как раз та штука, почему надо быть аккуратным с замыканиями в циклах

Кстати, чинится добавлением в теле цикла item := item

А вот ишью, кстати, советую глянуть. Интересная штука
👍1
Голанд хороший, голанд умный

Написал бенчмарки, и можно тут же запустить с профилировщиком. Полезно, теперь я знаю, насколько херовый у меня код
🥰1
Я тут заголовки переделал

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

Это также нам дало возможность подстроить не просто максимальную длину значения заголовка, а выделить конкретное количество пространства под значения. А значит, возможен более гибкий контроль над количеством используемой памяти

Ну и ещё изменений по мелочи, благодаря которым сервер стал в два раза меньше памяти на запрос тратить
Что делать
Я тут заголовки переделал В первую очередь, все значения заголовков теперь хранятся в одном большом слайсе байт. Это уменьшает нагрузку на гц за счёт того, что теперь меньше указателей и разрозненных участков в куче (не забываем, что слайс держит указатель…
Я ошибся. Моё решение не снижает количество указателей, ведь слайс всё равно указывает на память. Но теперь она хотя бы менее фрагментирована, тут большой слайс для всех всё же хорош
Вот что называется приятно
👍2
Вот значения заголовков у меня лежат в одном-большом пространстве, все вместе. Это решает проблему с уменьшением аллокаций, и убирает надобность в обжект пуле (реализация которого всё равно была бы сложной, либо сам обжект пул был бы тупым). Ну и можно указывать максимальное пространство, доступное для них, прикольно

Но вот с ключами я, почему-то, того же самого сделать не додумался. Я просто шёл по запросу до двоеточия, добавляя каждый символ индивидуально. Потом аллоцировал новую строку под ключ. Зачем? Не знаю

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

А ещё, индюшка теперь в бете🎉
Раз уж заговорили за бету, то держите ликбез о семвере

Semver - semantic versioning. Суть данного подхода к версионированию состоит в том, что у нас есть три разряда - major.minor.patches.

Инкрементируем patches, когда сделали изменения, не добавляющие ничего нового для пользователя - внутренние изменения, не влияющие на использование. Например, оптимизация, или фикс баги

Инкрементируем minor, когда добавили новую фичу, которая НЕ ЛОМАЕТ обратную совместимость - изменения, влияющие на удобство пользования, но не ломающие существующий пользовательский код. Например, добавили новый метод для класса, делающий какую-нибудь полезную работу

Инкрементируем major, когда добавили новую фичу, которая ЛОМАЕТ обратную совместимость - изменения, ломающие существующий пользовательский код. Изменение поведения, которое может сломать существующий код, также должно инкрементировать major-версию

К семверу иногда также добавляют build number (обычно это то самое страшное пятизначное число), добавляется оно, как ещё один разряд версии - major.minor.patches.build-number

Но не номером билда едины, так ведь? А мы докинем ещё tag!. Фактически, это те самые alpha, beta, prerelease, release, и как их там только не называют. Докидывают их через слэш, major.minor.patches[.build-number]-tag

Стоит упомянуть, что при инкрементации старшего разряда, принято обнулять младшие. Но учитывая страшные пятизначные build number, не все любят обнулять все младшие разряды

А, ну и любят докидывать префиксом v - v1.0.0, например.

ПыСы: гитхаб не любит вариант v.1.0.0, лучше пишите без точки между v и мажорной версией, предпочитая v1.0.0
У меня тут память текла. И текла очень ощутимо

Текла она от подключений. А точнее даже - после их закрытия, она не освобождается. Первым делом начал грешить на таймеры в tcp сервере - я их там на каждое чтение создавал. Починил, просто обнуляя один таймер на клиента каждый раз. Сервер стал выдавать на ~10к рпс больше, но проблему это не решило

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

И - да! Две горутины нормально себе завершали выполнение, кроме одной - httpServer.requestProcessor. Она ответственена за то, чтобы когда ядро уведомляет её о готовности запроса, она вызвала обработчик, и при окончании работы которого оповестила обратно ядро

При отключении клиента, ожидалось, что в ядро прилетит nil-слайс, и, соответственно, проверка у нас была if data == nil {}, которая слала сигнал об отключении нашей httpServer.requestProcessor. Посмотрев в tcp сервер, оказалось, что при дисконнекте, он передаёт просто пустой слайс 😃

Поменяв буквально одну строчку кода, проблема утечки памяти решилась. А вот профайлером пользоваться стоит
//go:nosplit

Думаю, все мы знаем про safe- и unsafe-участки функций. Вкратце - unsafe-участком функции считается участок, находясь в котором, гц не может очищать мусор (иначе получим кровь кишки). Такие места также называют "атомарными"

Также все мы знаем, что находясь в unsafe-участке, шедулер не может переключать горутину - даже при условии, что время пришло. И всё, что делает nosplit - указывает компилятору, что функцию не следует прерывать по таймеру. Каким образом? Тело функци целиком помечается, как unsafe-участок