LINUX.ORG.RU

Сетевой движок на Си. Вычитывание данных из сокета в 4кб буферы.

 


2

1

Хотелось построить сетевую подсистему, написанную на крестах, на такой идее: разные протоколы/модули обмениваются «цепочками буферов» (блохчейнами ога), в том числе и сетевая подсистема. Например, если кто-то хочет отправить по вебсокету 12 кб данных юзеру, то этот кто-то просит у центрального менеджера ресурсов 3 блока, наполняет их данными, чейнит их в одну цепь и кидает поинтер на первый блок в «сеть». Epoll-driven «сеть» отправит все данные (когда сможет) из этих блоков последовательно в сокет и отдаст «менеджеру ресурсов» блоки обратно по мере отправки. Блоки переюзываются всеми компонентами, потребляются по мере нужды, а новые коннекты не жрут память только по факту коннекта, тяжелее заддосить, всё красиво. Короче, общая идея в том, что есть некий slab-аллокатор блоков, владеющий ими и выдающий в аренду и блоки, протекая через все подсистемы и задерживаясь в сетевом модуле, если сразу запсать в сокет не удалось (больше чем TCP window или другой прикол), и в конце возвращаются в блокопрародитель обратно. Памяти при таком подходе в целом в любой момент сожрано меньше, чем если бы при наших 5к соединений каждый коннекшен бы держал собственный буфер на пару метров.

Одно но: если такая моя «сеть» решила вычитать из сокета мегабайт, то она сделает 1024/4 = 256 сисколлов read(), чтобы выжрать данные в цепь 4кб-блоков вместо одного сисколла, если бы у неё был буфер на 1 метр. На отправке те же приколы - рост числа сисколлов.

Да, есть оптимизации - можно юзать блоки разных размеров в разных местах, можно смотреть на Content-Length в случае протокола HTTP и готовиться к мегабайту оптимальнее заранее и т.п.

Но вот хотелось бы послушать разных историй про подобные «движки сети» где были похожие идеи и где авторы нашли оптимум.

Про recvmmsg/sendmmsg и io_uring в принципе в курсе.

UPDATE

«4 кб буферы» отошли в прошлое, текущее понимание затеи как «цепочка буферов разной фиксированной длины», например HTTP-ответ юзеру может быть цепочкой из буфера 4КБ (на хидеры) и за ним буфер в мегабайт под body.



Последнее исправление: kilokolyan (всего исправлений: 3)
Ответ на: комментарий от alysnix

не стесняйтесь писать «мне» с заглавной буквы.

У вас комплексы? Но это не ко мне. От слова «совсем».

bugfixer ★★★★★
()
Последнее исправление: bugfixer (всего исправлений: 1)
Ответ на: комментарий от bugfixer

И я теперь ну совсем не понимаю зачем Вы хотите свой memory manager (и тем более работающий с fixed-size blocks)

Потому что брать готовые pointers на 4кб/8кб/1мб куски памяти быстрее в 2…4 раза, чем дёргать malloc() на рандомные размеры. Так мерял:

  int test_malloc() {
  
    auto NUM = 1024 * 1024;
    std::vector<char*> stack_free(NUM, nullptr);
    std::vector<char*> stack_alloc;
    stack_alloc.reserve(NUM);
  
    for (int i = 0; i < NUM; ++i) {
      stack_free[i] = new char[4096];
    }
  
    size_t cnt_allocated = 0;
  
    {
      Measure me("malloc");
      for (int i = 0; i < NUM; ++i) {
        // sz is for malloc argument AND to touch memory at position "sz/2";
        static const auto minimum = 1024;
        // increasing "minimum" gives slower results
        int sz = minimum + (rand() % (4096 - minimum));
        // you can change malloc arg to constant "4096" and measure: you will get SLOWER results
        char *ptr = reinterpret_cast<char*>(malloc(sz));
        stack_alloc.push_back(ptr);
        ptr[sz / 2] = sz; 
      }   
    }


    // prepare alloc-vector for new allocations.
    // memory leak, yes:
    stack_alloc.clear();


    {
      Measure me("slab");
      for (int i = 0; i < NUM; ++i) {
        int sz = 1024 + (rand() % (4096 - 1024));
        char *ptr = stack_free.back();
        stack_free.pop_back();
        stack_alloc.push_back(ptr);
        // touch memory; also for measuring rand() in this loop too
        ptr[sz / 2] = sz; 
      }   
    }
  
    return 0;
  }

kilokolyan
() автор топика
Последнее исправление: kilokolyan (всего исправлений: 2)
Ответ на: комментарий от kilokolyan

Так мерял:

надо просто брать обычные массивы, а не векторы. и ручками считать индекс для скорости. еще непонятно какой код будет сгенерен для этих back, pop_back и все такое.

malloc всегда будет дольше захвата из пула, и не раза в два-три, а побольше(впрочем зависит от реализации хип менеджера)…

в реальной ситуации, когда память уже фрагментирована, а хипменеджер ведет списки свободной памяти, и у него есть своя стратегия отдачи свободного блока, обычно по принципу - наиболее подходит по размеру, там правда хипменеджеры могут вести подсписки памяти, типа - свободные от 4 до 8 кб, свободные от 8 до 16кб и так далее, чтобы быстрее искать.

отдача(free) тоже медленная, поскольку надо еще проверить на слияние отдаваемого куска с предыдушим и следующим, если они свободны, то сливать в большой свободный кусок - чтобы избегать фрагментирования.

то что вы намеряли (2-3 раза) - это сильно похоже, что у хипменеджера еще вообще нет списков свободной памяти, вы же не отдавали ничего туда, и он отдает сразу из большого своего свободного единственного куска.

так что мерять так вообще не стоит. хипменеджер в общем случае работает на порядок медленнее чем из пула заранее аллокированных отдавать.

либо сначала рандомно нахватайте кусков тыщ 10, потом рандомно половину освободите - чтобы там списки свободной памяти появились, а потом пускайте тесты для malloc. то есть у вас должен быть пул памяти фрагментрован, чтобы тесты malloc пускать.

аналогично тесты для free выглядят(на дефрагментации надо). free по идее даже дольше malloc должен быть, поскольку там работы больше.

alysnix ★★★
()
Последнее исправление: alysnix (всего исправлений: 2)
Ответ на: комментарий от kilokolyan

Потому что брать готовые pointers на 4кб/8кб/1мб куски памяти быстрее в 2…4 раза, чем дёргать malloc() на рандомные размеры.

Брать то может и быстрее. Точнее поначалу быстрее. А вот условный КПД по wasted RAM однозначно хуже. И я не понимаю зачем Вы вообще в это полезли. У Вас malloc/free на profiles вылезли? У Вас имеется хорошая статистика по аллокациям которая даёт Вам возможность предположить что Вы обгоните generic allocator? Вы абсолютно не в том направлении копаете. Перефразирую - я бы тратил время оптимизируя совсем другие вещи. И я до сих пор уверен что дизайн с передачей ownership на эту память обёртке вокруг сокета мягко говоря неоптимален. Только вот донести до Вас эту мысль всё никак не удаётся.

bugfixer ★★★★★
()
Ответ на: комментарий от kilokolyan

Потому что брать готовые pointers на 4кб/8кб/1мб куски памяти быстрее в 2…4 раза, чем дёргать malloc() на рандомные размеры.

Брать то может и быстрее. Точнее поначалу быстрее.

И ещё пара наводящих вопросов:

  • Вы вообще планируете память из своих pools хоть когда либо отпускать?
  • Какая стратегия планируется когда, например, preallocated 1Mb блоки заканчиваются?
bugfixer ★★★★★
()
Последнее исправление: bugfixer (всего исправлений: 1)
Ответ на: комментарий от alysnix

Внутри вектора обычные массивы и код будет сгенерён оптимальный, а в тесте важно лишь что одинаковый

kilokolyan
() автор топика
Ответ на: комментарий от bugfixer

И я до сих пор уверен что дизайн с передачей ownership на эту память обёртке вокруг сокета мягко говоря неоптимален.

Неоптимально скопировать 1 поинтер - это сложно.

kilokolyan
() автор топика
Ответ на: комментарий от kilokolyan

А вот условный КПД по wasted RAM однозначно хуже.

Не хуже: я бы и у malloc просил те же куски, равные лимитам на то, с чем работаю. Скажем, лимит размера входящих хидеров http - это 4…16КБ, соответственно я столько и аллокачу маллоком. Сколько юзерский код напишет http-ответа - тоже никто не знает, но знаем лимит - 512Кб, такого размера кусок нам и нужен. Юзер пишет килобайт 100. В сокете затык. Пойти копировать 100 кб - зачем, я просто подарю поинтер на 512 кб. Да, мне после этого нужен новый, но вообще… не нужен никакой. т.к. ещё один ответ формировать не надо, ибо новый запрос будет только после полной отправки ответа от этого. Если не считать pipelining, который всегда у всех сломан. Ну то есть тут да: 400 кб из 512 просрано. У всех кто сейчас пишет ответ клиенту. Ну вот если это парит, то врубаем режим экономии памяти, когда отправлялка маллочит себе рлвно 115 килобайт, копирует туда пейлоад из того 512кб буфера, дарит 512кб обратно и сидит с 115.

kilokolyan
() автор топика
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.