Что делать – Telegram
Что делать
103 subscribers
209 photos
3 videos
4 files
130 links
Не смешно
Download Telegram
This media is not supported in your browser
VIEW IN TELEGRAM
👎1
Оказывается, WSL не поддерживает флаг SO_REUSEPORT для сокетов. Держу в курсе

Пришлось для этого даже специально проверку делать, а не запускается ли вебсерв случаем под всл, и случаем не установлено ли количество серверных процессов на не-ноль
👎1
А ещё, гит почему-то перманентно игнорирует мою папочку logs. А её даже в гитигноре нет! Вручную git add logs/ ничего не дает. Магия, не иначе!

А мне ведь пришлось костылить пикрелейтед
👎1
rush-2.0.0-py3-none-any.whl
21.3 KB
Ну штош, столько времени спустя - могу себе позволить перекроить файловую иерархию вебсервера так, чтобы получился в итоге питоний пакет
👎1
Установить можно, просто скачав файл куда угодно, и ввести команду pip3 install path/to/package/rush-2.0.0-py3-none-any.whl


Также, в скором времени намерен залить сеё произведение исскуства на PyPI, чтобы прям по красоте
👎1
Ахуеть. У меня, оказывается, имя проекта спиздили
👎1
Это, кстати, первый мат за всё время существования канала. Ну, что поделать. По такому поводу - можно
👎1
А если серьезно, то этому репозиторию уже года 3. И вот на самом деле, мне крайне обидно, ведь и на PyPI он тоже называется rush. Неужели мне придется публиковать репозиторий под именем rush-webserver?..
👎1
Итак, я решил вести версионирование проекта. Буду делать это по старой-доброй схеме - майорный номер, минорный, и патч. Майорный будет отображать итерацию проекта, минорный - крупные изменения/привносения новых фич и возможностей в интерфейсе. Патчи же будут означать номер выпуска с багфиксами. На каждый багфикс/малейшее изменение в коде я, конечно, не буду инкрементировать патч, но это будет некого рода накопительные выпуски. Накапливается пачка багфиксов - выпускаю в релиз
👎1
Что делать
Цель выполнена: выжать 50к РПС. На данный момент, с 4 форками (5 процессами), вебсервер выдает такие результаты: 100к рпс для статичного кешированного файла, 120к рпс для статичного перманентного редиректа (который происходит внутри вебсервера). Следующая…
Итак - цель частично выполнена. Частично - потому что это тестируется лишь эксперементальная, PoC-фича. Я просто реализовал кеширование ответов с файлами (почему нет - они ведь всегда одни и те же). И реализовал я это прямо в объекте rush.core.entities.Request. Это, конечно, круто, но для этих целей я собираюсь (в отдельной ветке, конечно же) имплементировать сессионный движок - на каждого нового пользователя будет создаваться условно свой инстанс объекта Session, с кукисами, локальным кешированием и шлюхами
👎1
А, новая цель. Выжать 250к RPS на том же статичном файле. #roadtothe250kRPS #родтзе250кРПС
👎1
Как conn.getpeername() 10-15к рпс сожрал

Был отличный день для привнесения новых фич. Я и подумал: пора добавить в класс Request новый аттрибут - IP-адрес пользователя. Какого же было мое удивление, когда из-за этого, производительность упала в среднем на уже вышеупомянутые 10-15к рпс. Видимо, просто conn.getpeername() мутит под капотом что-то тёмное, что и дало мне понять: лучше так не делать
👎1
Давно заметил печальный факт, что в погоне за производительностью, я совсем потерял счёт памяти. Всё кэширование у меня работает по принципу "отдельно сам файл, отдельно кэшированный ответ с этим файлом". По сути, я хранил в каждом процессе по 2 дубликата одного и того же файла, просто во втором дубликате были ещё и хттп хедеры. Наконец, руки дошли до поработать над вебсервером, по итогу починиль - теперь пре-рендерятся только хедеры, а само тело файла уже при каждом запросе прихерячивается к заголовкам
👎1
Осталось придумать, как мне сделать нормально кэширование. Поскольку на данный момент, у каждого из процессов свой кэш со своими in-memory закэшированными файлами. Получается, до оптимизации в прошлом посте, расход памяти на кэширование был примерно 2n, где n - количество логических ядер процессора. После оптимизации, расход стал просто n, что всё ещё так-то много. В идеале, должен быть расход 1. Что-то вроде доп. процесса, который занят только кэшированием внутри себя, и отдающим кэшированные версии запросов/файлов процессам. Но учитывая, как работает multiprocessing.Queue (на IPC+Pickle, что опять таки не очень-то и шустро), это неплохо так ухудшит производительность. А этого я допустить не могу. Штош, поставлю себе в TODO, но куда-нибудь далеко
👎1
Что делать
А, новая цель. Выжать 250к RPS на том же статичном файле. #roadtothe250kRPS #родтзе250кРПС
Цель - перевыполнена. Оказалось, что самой необходимой оптимизацией - была смена парсера. http-parser оказался слишком медленным, и сменив его на httptools - производительность выросла на 100к РПС (!!!). Чутка пооптимизировав, я добился ещё 50к рпс. В итоге, имеем +150к рпс. Ну и, соответственно, меньшую задержку
👎1
Новая цель - 400к РПС на статичном файле.
#roadtothe400kRPS #родтзе400кРПС
👎1
Forwarded from Айван
Столкнулся с тем что __pycache__ директории в проекте доставляют неудобства: когда переключаюсь между git ветками, остаются папки которых нет в ветке, из-за того что в них есть незакоммиченные __pycache__.
Решил как-нибудь избавиться от них, нашёл вопрос на stackoverflow.
В комментариях предложили добавить в ~/.bashrc
export PYTHONPYCACHEPREFIX=/tmp
(/tmp папка со временными файлами, будет очищена спустя какое-то время, или при перезапуске компьютера)
👍1👎1
Я вспомнил пароль от канала

Автор перешёл на голанг, теперь буду писать заметки конкретно об этом языке.

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

Чтобы упростить дальнейшее понимание данной штуки - приведу пример на Го:

data := "Hello, world!"

for _, char := range data {
fmt.Println(char)
}

Данный код выведет построчно Hello, world!. При развёртывании же цикла, мы будем иметь нечто вроде:

fmt.Println("H")
fmt.Println("e")
fmt.Println("l")
fmt.Println("l")
fmt.Println("o")
...

И это даст нам несомненный прирост производительности! Однако же, как вы могли бы заметить, данный пример работает ой как не всегда - нужно знать конкретную длину входных данных. Тем не менее, мы можем поступить проще, сделав нечто на подобии

data := "Hello, world!"

i := 0
dataLen := len(data)

for {
fmt.Println(data[i])
fmt.Printlnb(data[i+1])
fmt.Println(data[i+2])
fmt.Println(data[i+3])

i += 4

if dataLen-i < 4 {
break
}
}

for i < dataLen {
fmt.Println(data[i])
i++
}

Что мы здесь видим? Мы совершаем в ~4 раза меньше итераций, поскольку вместо 4 итераций, мы делаем 4 операции в одной. Можно выставить как 4, так и 6, и 8, и вообще 20 как шаг. Единственное - рекомендую выставлять число, равное степени двойки.

Что по поводу выигрыша - у меня он составил около 35% при шаге в 8 и строке длиною 5,000 элементов. Что, я считаю, очень даже весомо, особенно в ситуациях, когда входных данных достаточно много (экспериментальным путём я пока не успел выяснить, при насколько малых объемах выигрыш становится не столь существенным). Да, это усугубляет читаемость. Да, из-за неё у нас появляется копипаста кода. Да, в некоторых случаях это может превратиться в кучу одинакового кода, если результат каждой операции нужно как-то обрабатывать (к примеру - обрабатывать возврат ошибки функции). И тем не менее, быстрый код - зачастую противоположность красоте, посему не рекомендую городить такие штуки в коде, в котором не настолько важна производительность.

Подводя итоги, хочу сказать, что штука действительно ситуативная. Использовать, или нет - дело сугубо каждое. Посему - спешу откланяться и поблагодарить того, кто прочитал всю эту вероятно бесполезную простыню больного шизофренией маньяка оптимизаций
👍1👎1
#ИнтересноЗнать

Возвращаясь к теме о базовых оптимизациях - нужно также знать о такой штуке, как inlining. Простым языком - в блоке кода, где вызывается функция, вместо непосредственного вызова, код самой функции вставляется прямо в наш искомый блок кода, вызывающий её. Казалось бы, в чём прикол? А прикол в том, что это очень даже неплохо так ускоряет работу программы. Правда, если мы говорим о многократном выполнении одного и того же блока кода, в котором компилятор сделал инлайнинг функции, по сравнению с тем блоком кода, где компилятор этого не сделал.

Но что я бы хотел сегодня поведать - так это о том, как это реализуется в голанге. Сразу хочу предупредить: разговор пойдет об эталонном компиляторе, разработанным Google. Важное уточнение, так как существует gccgo, который более сосредоточен на оптимизациях. И хоть эталонный компилятор также делает оптимизации, он делает их не так вдумчиво в угоду скорости компиляции (что есть чуть ли не главной фишкой рекламной компании голанга). Поэтому, например, он до сих пор не делает loop unrolling, о котором говорилось в посте выше. Также поэтому сам инлайнинг ну уж крайне ограничен. Как гласит выдержка об условиях для инлайна из документации по компилятору:

Function Inlining

Only short and simple functions are inlined. To be inlined a function must contain less than ~40 expressions and does not contain complex things like function calls, loops, labels, closures, panic's, recover's, select's, switch'es, etc.

Правда, актуально это только для 1.16, в более новых версиях свитчи и циклы всё-таки инлайнятся. Тем не менее, список до сих пор довольно обширный, что довольно таки неприятно.

Мораль такова: используя голанг, воспевать к оптимизациям компилятора не стоит. Он это не всегда хорошо умеет :)
👎1