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


15

Это в основном приложение для регистрации / подсчета, которое подсчитывает количество пакетов, подсчитывает тип пакета и т. Д. В сети чата p2p. Это соответствует примерно 4-6 миллионам пакетов за 5 минут. И поскольку я делаю только «снимок» этой информации, я удаляю только пакеты старше 5 минут каждые пять минут. Таким образом, максимальное количество предметов, которые будут в этой коллекции, составляет от 10 до 12 миллионов.

Поскольку мне нужно сделать 300 подключений к разным супер-суперпользователям, возможно, что каждый пакет пытается быть вставлен как минимум 300 раз (возможно, поэтому хранение этих данных в памяти является единственно разумным вариантом).

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

Dictionary<ulong, Packet>

public class Packet
{
    public ushort RequesterPort;
    public bool IsSearch;
    public string SearchText;
    public bool Flagged;
    public byte PacketType;
    public DateTime TimeStamp;
}

Я пытался использовать mysql, но он не смог справиться с объемом данных, которые мне нужно вставить (при проверке, чтобы убедиться, что это не дубликат), и это было при использовании транзакций.

Я попробовал mongodb, но использование процессора для этого было безумным и не сохранилось.

Моя основная проблема возникает каждые 5 минут, потому что я удаляю все пакеты старше 5 минут и делаю «снимок» этих данных. Поскольку я использую запросы LINQ для подсчета количества пакетов, содержащих определенный тип пакета. Я также вызываю отдельный запрос () для данных, где я удаляю 4 байта (ip-адрес) из ключа keyvaluepair и объединяю его со значением requesttingport в значении keyvalupair и использую его для получения различного числа peers из всех пакетов.

В настоящее время приложение использует около 1,1 ГБ памяти, и при вызове моментального снимка оно может удвоиться.

Теперь это не будет проблемой, если у меня будет безумное количество оперативной памяти, но виртуальный компьютер, на котором я работаю, ограничен 2 ГБ оперативной памяти.

Есть ли простое решение?


Его сценарий очень интенсивно использует память, и вдобавок к этому вы используете виртуальную машину для запуска приложения, вау. В любом случае, вы исследовали memcached для хранения пакетов. По сути, вы можете запускать memcached на отдельной машине, а приложение может продолжать работать на самом vm.

Поскольку вы уже пробовали MySQL и MongoDB, может показаться, что, возможно, требования вашего приложения (если вы хотите сделать это правильно) диктуют, что вам просто нужно больше лошадиных сил. Если ваше приложение важно для вас, улучшите сервер. Вы также можете пересмотреть свой «очищающий» код. Я уверен, что вы могли бы найти более оптимизированный способ справиться с этим, поскольку это не делает ваше приложение непригодным для использования.
Мэтт Бекман

4
Что говорит вам ваш профилировщик?
Джейсон

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

@vartec - на самом деле, вопреки распространенному мнению, ручной вызов сборщика мусора фактически не гарантирует немедленного, ну ... сбора мусора. GC может отложить действие на более поздний период в соответствии с собственным алгоритмом gc. Вызов его каждые 5 минут может даже усилить нагрузку, а не ослабить ее. Просто говорю;)
Jas

Ответы:


12

Вместо того, чтобы иметь один словарь и искать в этом словаре слишком старые записи; есть 10 словарей. Каждые 30 секунд или около того создайте новый «текущий» словарь и отбросьте самый старый словарь без поиска вообще.

Затем, когда вы отбрасываете самый старый словарь, поместите все старые объекты в очередь FILO на потом и вместо использования «нового» для создания новых объектов вытащите старый объект из очереди FILO и используйте метод для восстановления старого объект (если очередь старых объектов не пуста). Это может избежать большого количества выделений и больших затрат на сборку мусора.


1
Разбиение по времени! Как раз то, что я собирался предложить.
Джеймс Андерсон

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

1
Часть проблемы с общими структурами заключается в том, что они не настроены для конкретной цели. Возможно, вам следует добавить поле «nextItemForHash» и поле «nextItemForTimeBucket» в структуру Packet, реализовать собственную хэш-таблицу и прекратить использование словаря. Таким образом, вы можете быстро найти все пакеты, которые являются слишком старыми, и выполнять поиск только один раз, когда пакет вставлен (т.е. есть свой торт и есть его тоже). Это также помогло бы из-за накладных расходов на управление памятью (так как «Словарь» не выделил бы / не освободил дополнительные структуры данных для управления словарем).
Брендан

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


3

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

Во-вторых, LINQ отлично подходит для краткого кода, но на самом деле LINQ является синтаксическим сахаром на «обычном» C #, и нет гарантии, что он сгенерирует наиболее оптимальный код. В качестве упражнения вы можете попытаться переписать «горячие точки» без LINQ, вы не сможете улучшить производительность, но у вас будет более четкое представление о том, что вы делаете, и это облегчит работу по профилированию.

Другая вещь, на которую стоит обратить внимание - это структуры данных. Я не знаю, что вы делаете со своими данными, но не могли бы вы упростить данные, которые вы храните? Не могли бы вы использовать строку или байтовый массив, а затем извлекать соответствующие части из этих элементов по мере необходимости? Не могли бы вы использовать структуру вместо класса и даже сделать что-то плохое с помощью stackalloc, чтобы выделить память и избежать запуска GC?


1
Не используйте строковый / байтовый массив, используйте что-то вроде BitArray: msdn.microsoft.com/en-us/library/… чтобы избежать ручной бит-твидлинга. В противном случае это хороший ответ, на самом деле нет другого простого варианта, кроме более совершенных алгоритмов, большего количества аппаратного обеспечения или лучшего аппаратного обеспечения.
Эд Джеймс

1
Пятиминутная вещь связана с тем, что эти 300 соединений могут принимать один и тот же пакет. Поэтому я должен отслеживать, что я уже обработал, и 5 минут - это время, которое требуется пакетам для полного распространения на все узлы в этой конкретной сети.
Джош

3

Простой подход: попробуйте memcached .

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

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

Более сложный подход: попробуйте Redis .

Недостатком является то, что это немного сложнее.


1
Memcached можно разделить на несколько машин, чтобы увеличить количество оперативной памяти. Вы могли бы иметь второй сервер, сериализующий данные в файловую систему, чтобы вы не потеряли вещи, если окно memcache выйдет из строя. Memcache API очень прост в использовании и работает с любым языком, что позволяет использовать разные стеки в разных местах.
Михаил Шопсин

1

Вам не нужно хранить все пакеты для запросов, которые вы упомянули. Например - счетчик типа упаковки:

Вам нужно два массива:

int[] packageCounters = new int[NumberOfTotalTypes];
int[,] counterDifferencePerMinute = new int[6, NumberOfTotalTypes];

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

Таким образом, для каждого пакета выполняются следующие операции:

packageCounters[packageType] += 1;
counterDifferencePerMinute[current, packageType] += 1;
if (oneMinutePassed) {
  current = (current + 1) % 6;
  for (int i = 0; i < NumberOfTotalTypes; i++) {
    packageCounters[i] -= counterDifferencePerMinute[current, i];
    counterDifferencePerMinute[current, i] = 0;
}

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


Основной причиной необходимости хранить данные, которые я делаю, является тот факт, что эти 300 подключений могут получать один и тот же точный пакет. Поэтому мне нужно хранить каждый увиденный пакет не менее пяти минут, чтобы убедиться, что я не обработал / не посчитал его более одного раза. Для чего и нужен ulong для словарного ключа.
Джош

1

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

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

Используйте массивы (если вы знаете размер данных, которые вы храните) или List - который использует массивы внутри. Если вам действительно нужен быстрый произвольный доступ, используйте словарь индексов массива. Это снимает еще пару уровней (или дюжину и более, если вы используете SortedDictionary) для поиска в gc.

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

Комбинация struct & list значительно уменьшает как использование памяти, так и размер уборщика мусора.


У меня есть недавний эксперимент, который генерирует коллекции и словари на диске так же быстро, используя sqlite github.com/modma/PersistenceCollections
ModMa
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.