LINUX.ORG.RU

Задача по оптимизации сервера // С++

 , , , ,


0

3

Есть сервер, который по запросу клиентов отправляет им определенные данные.

Например, клиент отправляет через TCP сокет последовательно id’шники желаемых блоков данных, сервер их загружает в память и по сути сразу же запускает асинхронную отправку

У меня возник вопрос, как сделать так (архитектура программы), чтобы один и тот же блок данных не грузился 2 раза, если 2 разных клиента попросили один и тот же блок ?

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

У меня есть идея

  1. загружать блок в условную MAP вида, этим гарантируем, что он не загрузится дважды. Если блок уже есть в мапе, то просто обновляем список получателей (id блока) - (char *, список получателей: юзер 1, юзер 2, …)

  2. Запускаем отправку async write

  3. Калбек функция async write вычеркнет юзера из списка получателей и, если он пустой, освободит буффер и удалит элемент из мапы.

Это вообще адекватно ? Я просто не знаток многопоточного программирования, а к этой мапе доступ будут иметь 2 потока : поток, который занимается отправкой сообщений, а также поток, который занимается тем, что ложит туда данные (и он же вызывает функцию отправки у первого по оставленному указателю на соединение)

То есть если я просто сделаю условно класс thread safe map и приделаю туда мьютекс, то это же будет работать ? (почему нет)

и в калбеке async write будет вызываться функция наподобие

{ 
   std::unique_lock<std::mutex> lock(_mtx);
   
   map.myList.erase(...user_id);
   if(myList.empty())
   {

      delete [] buffer;
      map.erase (...)
   }
}

Хотя чисто интуитивно if под мьютексом выглядит как трэш какой-то. В общем помогите, пожалуйста, или накидайте альтернативных идей, с меня как обычно.



Последнее исправление: Unknown_ (всего исправлений: 2)

В оперативную память.

Есть поток 1: В нем существует сервер, который принимает соединения через async accept.

Далее, соединение принимает id желаемых блоков и пушит их в некую очередь в формате пары (id желаемого блока) - (указатель на соединение)

Есть поток 2: Он тоже имеет доступ к той же очереди. Оттуда он достает id желаемого блока и указатель на соединение, достает блок по id, а затем по указателю вызывает функцию async write

И вот речь идет о том, что, если 2 соединения попросят один и тот же блок большой, то он загрузится в память 2 раза.

(А, комментарий, на который я отвечал, уже удалили, но энивей, для ясности запощу)

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

Если я правильно понимаю как это работает, то это про использование лишней оперативной памяти и экономия на лишней загрузке.

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

То есть, если юзеру 1 уже отправляется некий буффер, который должен удалиться из РАМ после отправки, то при запросе юзером 2 того же блока мы хотим каким-то образом использовать этот же самый буффер, то есть как-то обновить стейт или что-то вроде того, чтобы завершение отправки юзеру 1 уже не удаляло буффер, так как он используется уже для отправки юзеру 2

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

то он загрузится в память 2 раза

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

Syncro ★★★★★
()
Последнее исправление: Syncro (всего исправлений: 1)

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

ваш вопрос отчасти решён на уровне ФС. (однажды прочитанный блок будет сидеть в кеше какое-то время)

На уровне любой базы доведён до оптимума. (прочитанный блок с метаданными будет сидеть в памяти приложения и моментально доступен)

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

То есть, если юзеру 1 уже отправляется некий буффер, который должен удалиться из РАМ после отправки, то при запросе юзером 2 того же блока мы хотим каким-то образом использовать этот же самый буффер, то есть как-то обновить стейт или что-то вроде того, чтобы завершение отправки юзеру 1 уже не удаляло буффер, так как он используется уже для отправки юзеру 2

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

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

А можешь, пожалуйста, подробнее пояснить свою идею? Просто не очень понял вот это: «точнее лучше отделить таблицу загруженного в память от очереди на раздачу, что-бы вторую можно было быстро чистить, а первая быстро искала»

Если мы вот так разделяем, то получается это + 2 контейнера, к которым будут иметь доступ оба треда вместо одного или я что-то не так понимаю?

Очередь на раздачу-то тоже придется шарить потоку сервера, который будет калбеками асинк врайта ее чистить или я что-то не понимаю

«а хранить в той же мапе указатель на уже загруженный блок не получится?»

Я так и собирался, да

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

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

блок 1 - адресат 1

блок 1 - адресат 2

блок 2 - адресат 1

или в виде дерева по адресатам

адресат 1 - блок1 блок2

адресат 2 - блок 1

Syncro ★★★★★
()

Если нужно кешировать данные в своем коде, то используй стандартные структуры для этого. Например, splay tree (растопыренное дерево), но лучше выбрать то, что больше подходит к специфике вашей нагрузки.

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

Да, пожалуй, я так и сделаю. Но на всякий случай хочу еще спросить, а std::shared_ptr нельзя сюда как-то прикрутить для нужд задачи?

Много не пользовался ими, но ведь при удалении последнего объекта shared_ptr, обладающего тем или иным ресурсом, данный ресурс освобождается

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

while(..)
{
    std::shared_ptr buffer = ...
    ..
    WriteData(buffer, size);
    ...
}

Еще сказано, что эти смарт поинтеры потокобезопасные в определенном смысле, короче если мыслить в стиле карго-культа, то кажется, что это что-то связанное с задачей - может быть, можно что-то придумать или это вообще не туда и не про это

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

удалится если им никто не станет владеть

для конкаренси наверное лучше взять соответствующие контейнеры

у вас в цикле если он не буде залинкован во вне, то будет пересоздаваться на каждой итерации

Syncro ★★★★★
()

Не мучайте моск изобретениями колес, берите MQ где все эти «муки зву» с гарантированной доставкой, квитированием и кэшированием уже порешаны.

anonymous
()

Вам надо решить несколько задач. кеш должен:

  1. хранить данные определенный промежуток времени.
  2. смотреть за памятью и удалять данные по LRU/MRU.
  3. атомарно добавлять, удалять, обновлять и получать данные.
  4. хеш функция должна давать по возможности мало коллизий.

Вроде все.

alnkapa
()

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

naushniki
()

Думаю, тут можно обойтись простым счётчиком ссылок и хешмапой, для быстрого поиска блока.

На x86 инструкции add и sub могут атомарно выполняться над памятью. Надо только найти как это сделать в плюсах, без оверхеда и лишних локов.

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

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

anonymous
()
Ответ на: комментарий от Syncro

бд и фс это все блокирующие операции, они ему всю асинхронщину закоматозят

бд как-раз таки не запорят. всякие lightingdb и прочие nosql как раз для такого сделаны, их для этого проектировали.

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

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

MKuznetsov ★★★★★
()

Ложишь все данные в один гигантский файл, загружаешь его через mmap в виртуальную память ридонли. Когда требуется блок (id блока) тупо отдаешь нужный кусок памяти. Ядро само разрулит когда данные подсасывать с диска. Тема рабочая зуб даю

anonymous
()