Что делать
Собственно, вот пилю я вебсервер на С. Пришлось мне свой ивентлуп возводить, сделал простенькую схему и обернул это дело в еполл. Потом думаю ещё io_uring в эту схему добавить. А мне вот что интересно стало: чисто технически, у меня, при ебанутой нагрузке…
В общем, когда я пытался заснуть, в полусне мне пришло осознание, что я в любом случае не смогу память адекватно дефрагментировать. Мне же нужно будет где-то аллоцировать промежуточный буфер, чтобы данные скопировать. А он тоже должен быть достаточно большим. А коль память фрагментирована, то достаточно большим сделать не получится, да и на стэк особо не поскладируешь. В общем, наверное, я просто обрублю все подключения и деаллоцирую всё, что понавыделять успел.
Да, не получится так же гладко, как если бы я просто временно перестал их обслуживать, но единственный способ. Авось так хоть немного на плаву получится остаться. А сисадмины пусть сами уже разбираются, почему у них сервер захлёбывается.
Да, не получится так же гладко, как если бы я просто временно перестал их обслуживать, но единственный способ. Авось так хоть немного на плаву получится остаться. А сисадмины пусть сами уже разбираются, почему у них сервер захлёбывается.
Что делать
Почему побитовая логика вся выглядит так страшно
Суть вообще вот в чём. Поскольку я делаю сервер асинхронным, то мне нужен ивентлуп. Пока что он работает на еполле. Еполл я взял в edge-triggered режиме. Иными словами - ивент прилетает лишь раз, пока я не дойду до конечной (не получу при чтении либо записи ошибку EAGAIN), после чего он возникнет снова после того, как станет доступным. Поэтому у меня есть массив со всеми тасками, и массив поменбше, в котором хранятся только те таски, с которыми нужно что-то делать. У каждой таски, в свою очередь, есть состояние - READ, WRITE, INACTIVE.
Чтобы не мудрить всякого говна с массивом активных тасок, я просто сделал его самопополняющимся. Как это работает: обёртка (в данном случае, еполл) вызывает ивентлуп (ev_invoke()), и передаёт в неё все новые ивенты. Мы в ивентлупе пробегаемся по всем текущим активным таскам, если состояние READ - читаем из сокета данные, кладём в замыкание. Если write - вызываем соответствующее замыкание, и возвращенный массив байт кладём в сокет. Если при чтении либо записи возникает ошибка EAGAIN, мы просто сохраняем где-то недочитанные/недописанные байтики, ставим состояние INACTIVE. При этом, если у нас есть какие-то новые ивенты, мы такую неактивную таску заменяем новой, активной, и обрабатываем уже её. Если новых ивентов нет, просто оставляем в таком состоянии, она тоже будет заменена на активную, только при следующей итерации.
А вот побитовая логика понадобилась вот зачем. Сверху есть логическая ошибка - если состояние таски в инактив устанавливается, как тогда понять, на каком моменте мы в предыдущий раз остановились - на чтении, или на записи? Вот и получается так, что как только таска становится неактивной, мы больше никогда её не обработаем. Будем постоянно скипать либо заменять другими активными.
Поэтому я решил сделать состояние в виде флагов. READ = 0b001, WRITE = 0b010, и INACTIVE, соответственно, 0b100. Таким образом, если сокет истощился (EAGAIN, иными словами), мы просто добавляем флаг инактива (нижняя строчка). При этом, когда приходит пора заменить таску другой активной, мы этот флаг снимаем (та страшная штука на второй строчке). Таким образом, даже если таска заменяется самой собою, то логика остаётся прежней
Чтобы не мудрить всякого говна с массивом активных тасок, я просто сделал его самопополняющимся. Как это работает: обёртка (в данном случае, еполл) вызывает ивентлуп (ev_invoke()), и передаёт в неё все новые ивенты. Мы в ивентлупе пробегаемся по всем текущим активным таскам, если состояние READ - читаем из сокета данные, кладём в замыкание. Если write - вызываем соответствующее замыкание, и возвращенный массив байт кладём в сокет. Если при чтении либо записи возникает ошибка EAGAIN, мы просто сохраняем где-то недочитанные/недописанные байтики, ставим состояние INACTIVE. При этом, если у нас есть какие-то новые ивенты, мы такую неактивную таску заменяем новой, активной, и обрабатываем уже её. Если новых ивентов нет, просто оставляем в таком состоянии, она тоже будет заменена на активную, только при следующей итерации.
А вот побитовая логика понадобилась вот зачем. Сверху есть логическая ошибка - если состояние таски в инактив устанавливается, как тогда понять, на каком моменте мы в предыдущий раз остановились - на чтении, или на записи? Вот и получается так, что как только таска становится неактивной, мы больше никогда её не обработаем. Будем постоянно скипать либо заменять другими активными.
Поэтому я решил сделать состояние в виде флагов. READ = 0b001, WRITE = 0b010, и INACTIVE, соответственно, 0b100. Таким образом, если сокет истощился (EAGAIN, иными словами), мы просто добавляем флаг инактива (нижняя строчка). При этом, когда приходит пора заменить таску другой активной, мы этот флаг снимаем (та страшная штука на второй строчке). Таким образом, даже если таска заменяется самой собою, то логика остаётся прежней
Какие есть тулзы, чтобы интерактивно данными по голому тсп перекидываться? А то мне надо ивентлуп как-то тестировать, для начала - ручками смотреть
Так. Вот у меня таски лежат в одном пуле. Пул - арены, свободные ячейки в которых лежат в очереди. Получается О(1) вставка и удаление, шикардос. А вот у меня есть net_client, интерфейс для сетевого подключения. В интерфейсе лежит void* env, по сути - указатель на контекст для имплементации, который будет передаваться в методы (лежат как указатели на функции). Есть tcp клиент как имплементация net клиента. А теперь непосредственно проблема: инстанс tcp клиента у меня аллоцируется на хипе. Ну, то есть, это никак не амортизируется, хотя спокойно может. И теперь мне надо думать, как мне так по-умному либо аллокатор прокидывать, либо ещё как-то выкручиваться. Пиздец, мемори менеджмент - сложна.
Ещё и раньше у меня таски лежали просто в динамическом массиве. Ну, обёртка (источник ивентов, пока что - epoll) спавнила новую таску, а я возвращал её индекс, которым и оперировал в дальнейшем. Когда я начал складывать таски в пул, то оказалось проще отдавать указатель. Единственная проблема - индексом таски являлся int64, чтобы отрицательные значения себе загребала обёртка, дабы, например, идентифицировать серверные сокеты, и принимать самостоятельно подключения из них. Если я буду возвращать указатель, то я так сделать не смогу. Самое интуитивное решение - вынести эту логику в ивентлуп, и добавить специальный стейт ACCEPT, как я и сделал.
Но тут я понял, что если взаимодействие с сокетами у меня абстрагировано, то приём подключений - нихуя, а это тоже важно. А добавлять ещё один метод в интерфейс net клиента не хотелось - и без того структурка тяжелая, а тут ещё и метод, который будет использован только на паре сокетов. Поэтому делаем отдельный интерфейс для приёма подключений. Полез имплементировать в tcp. Вижу, надо аллоцировать tcp клиента, чтобы обернуть его в интерфейс. Понимаю - вот тут-то я и в жопе.
Пока идея - склеить интерфейс с имплементацией. Оставить в самом интерфейсе место свободное, куда я могу своё говно положить. Другой вопрос - а сколько именно-то надо? У меня уже сейчас как минимум две реализации (заглушка для тестов, и непосредственно tcp), а потом ещё и tls к этому делу прибавится. Подумываю, нарушить немного несвязанность, и захуярить в интерфейсе union со всеми известными имплементациями вместо void* окружения. Да, должно быть неплохо
Но тут я понял, что если взаимодействие с сокетами у меня абстрагировано, то приём подключений - нихуя, а это тоже важно. А добавлять ещё один метод в интерфейс net клиента не хотелось - и без того структурка тяжелая, а тут ещё и метод, который будет использован только на паре сокетов. Поэтому делаем отдельный интерфейс для приёма подключений. Полез имплементировать в tcp. Вижу, надо аллоцировать tcp клиента, чтобы обернуть его в интерфейс. Понимаю - вот тут-то я и в жопе.
Пока идея - склеить интерфейс с имплементацией. Оставить в самом интерфейсе место свободное, куда я могу своё говно положить. Другой вопрос - а сколько именно-то надо? У меня уже сейчас как минимум две реализации (заглушка для тестов, и непосредственно tcp), а потом ещё и tls к этому делу прибавится. Подумываю, нарушить немного несвязанность, и захуярить в интерфейсе union со всеми известными имплементациями вместо void* окружения. Да, должно быть неплохо
Я докатился по итогу до своего аллокатора. В glibc, конечно, и маллок умный, и юзает под капотом арены, но у меня чуть более специализированный кейс, поэтому могу себе позволить. Всё равно потом забенчмаркаю оба варианта, может, и выкину нахуй
Но тут встала такая проблема: у меня сейчас указатели на все аллоцированные арены лежат в динмассиве. Я могу сделать из арен связанный список, где в начале каждой арены будет лежать указатель на следующую. Но тогда размер арен перестанет быть кратным размеру страницы (как минимум, на х86 - а это 4096 байт), если я буду аллоцировать размер арены + 8 байт (похуй на 32-х разрядные системы, 4 байта непринципиально). Можно просто от обычного размера арены откусить 8 байт под собственные нужды. Но в таком случае, когда мне нужны объекты как раз размером со страницу - например, буфер для чтения из сокета, 4096 байт - тогда у меня ещё и 4088 байт будут "висеть" мёртвым грузом. Эх.
Может, отдельный динмассив где-то там и будет лучшим выбором. Грустно.
Но тут встала такая проблема: у меня сейчас указатели на все аллоцированные арены лежат в динмассиве. Я могу сделать из арен связанный список, где в начале каждой арены будет лежать указатель на следующую. Но тогда размер арен перестанет быть кратным размеру страницы (как минимум, на х86 - а это 4096 байт), если я буду аллоцировать размер арены + 8 байт (похуй на 32-х разрядные системы, 4 байта непринципиально). Можно просто от обычного размера арены откусить 8 байт под собственные нужды. Но в таком случае, когда мне нужны объекты как раз размером со страницу - например, буфер для чтения из сокета, 4096 байт - тогда у меня ещё и 4088 байт будут "висеть" мёртвым грузом. Эх.
Может, отдельный динмассив где-то там и будет лучшим выбором. Грустно.
Что делать
Ещё и раньше у меня таски лежали просто в динамическом массиве. Ну, обёртка (источник ивентов, пока что - epoll) спавнила новую таску, а я возвращал её индекс, которым и оперировал в дальнейшем. Когда я начал складывать таски в пул, то оказалось проще отдавать…
Ага. Хуй. Ну вот я принял новое подключение, а как мне тогда сказать обёртке, чтобы она добавила его в еполл? Сука
Forwarded from dontuto (dontu bruh🥟)
This media is not supported in your browser
VIEW IN TELEGRAM
Что делать
Ага. Хуй. Ну вот я принял новое подключение, а как мне тогда сказать обёртке, чтобы она добавила его в еполл? Сука
По итогу, я осознал, что просто запутался в собственных абстракциях. Добавил новый стейт таски EV_SERVER, который отлавливает конкретно обёртка еполла, и сама принимает подключение. Плюс вынес туда же арены с тасками, склеив таски вместе с собственно клиентами. Получилось стройненько
Что делать
Я докатился по итогу до своего аллокатора. В glibc, конечно, и маллок умный, и юзает под капотом арены, но у меня чуть более специализированный кейс, поэтому могу себе позволить. Всё равно потом забенчмаркаю оба варианта, может, и выкину нахуй Но тут встала…
С аренами решил оставить отдельно список, кстати. Это уже больше мелкие твики, которые буду пробовать тогда, когда я смогу оценить, какие преимущества либо проблемы это создаст. А то решение проблемы в вакууме какое-то