Ефективні методи зберігання десятків мільйонів об’єктів для запиту, з великою кількістю вставок в секунду?


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-адресу) з ключа ключового значення параметри і поєдную його зі запитомпортового значення у Значенні ключового значення та використовую, щоб отримати чітке число колеги з усіх пакетів.

Зараз у програмі розміщено близько 1,1 ГБ пам’яті, а коли буде зроблено знімок, це може зайняти вдвічі більше.

Тепер це не буде проблемою, якщо у мене є божевільна кількість оперативної пам’яті, але vm, на якому я працюю, наразі обмежений 2 ГБ оперативної пам’яті.

Чи є якесь просте рішення?


Його дуже інтенсивний пам'ять сценарій, і крім того, що ви використовуєте vm для запуску програми, уау. У будь-якому разі, чи вивчали ви складові для зберігання пакетів. В основному, ви можете запускати складені файли на окремій машині, і програма може постійно працювати на vm.

Оскільки ви вже пробували і MySQL, і MongoDB, здається, що, можливо, вимоги вашої програми (якщо ви хочете зробити це правильно) диктують, що вам просто потрібно більше кінських сил. Якщо ваша програма важлива для вас, підключіть сервер. Ви також можете переглянути код "очищення". Я впевнений, що ви можете знайти більш оптимізований спосіб роботи, якщо це не робить вашу програму непридатною.
Метт Бекман

4
Що вам каже ваш профайлер?
жасонька

Ви не отримаєте нічого швидшого, ніж місцева купа. Моя пропозиція полягала б у ручному застосуванні сміття після очищення.
vartec

@vartec - насправді, всупереч поширеній думці, вручну викликати сміттєзбірник насправді не гарантує негайного, ну ... вивезення сміття. GC може відкласти дію на пізніший період відповідно до власного алгоритму gc. Закликання кожні 5 хвилин може навіть посилити напругу, а не знімати її. Просто сказати;)
Jas

Відповіді:


12

Замість того, щоб мати один словник і шукати цей словник для записів, які є занадто старими; мають 10 словників. Кожні 30 секунд або близько того створюйте новий "поточний" словник і відмовтеся від найдавнішого словника, не здійснюючи пошуку.

Далі, коли ви відкидаєте найдавніший словник, покладіть всі старі об’єкти у чергу FILO для подальшого, а замість використання "нових" для створення нових об'єктів витягніть старий об'єкт із черги FILO та використовуйте метод реконструкції старого об'єкт (якщо черга старих об'єктів порожня). Це дозволяє уникнути безлічі виділень та великої кількості сміття накладних витрат.


1
Розбиття за часовим відрізком! Просто те, що я збирався запропонувати.
Джеймс Андерсон

Проблема в цьому полягає в тому, що мені доведеться запитати всі ті словники, які були складені протягом останніх п'яти хвилин. Оскільки існує 300 з'єднань, той самий пакет збирається приїхати до кожного принаймні один раз. Тому, щоб не обробляти один і той же пакет більше, ніж один раз, я повинен зберігати їх принаймні протягом 5 хвилин.
Джош

1
Частина проблеми із загальними структурами полягає в тому, що вони не налаштовані під конкретні цілі. Можливо, вам слід додати поле "nextItemForHash" та поле "nextItemForTimeBucket" до вашої структури пакету та реалізувати власну хеш-таблицю та припинити використання словника. Таким чином, ви можете швидко знайти всі пакети, які є занадто старими, і шукати лише один раз, коли пакет буде вставлено (тобто, майте торт і теж з'їжте його). Це також допоможе керувати накладними витратами (оскільки "Словник" не виділяє / звільнятиме додаткові структури даних для управління словниками).
Брендан

@Josh - найшвидший спосіб визначити, чи ви бачили щось раніше, - це хешсет . Нарізані часом хеш-набори пройдуть швидко, і вам все одно не потрібно буде шукати, щоб виселити старі предмети. Якщо ви ще не бачили цього, ви можете зберігати його у своєму диктанті (y / ies).
Основні


3

Перша думка, яка спадає на думку, - це те, чому ви чекаєте 5 хвилин. Чи можете ви робити знімки частіше і тим самим зменшити велике перевантаження, яке ви бачите на 5-хвилинній межі?

По-друге, LINQ чудово підходить для стислого коду, але насправді LINQ є синтаксичним цукром на "звичайному" C # і немає гарантії, що він генерує найоптимальніший код. Як вправу ви можете спробувати і переписати гарячі точки за допомогою LINQ, ви можете не покращити продуктивність, але у вас буде чіткіше уявлення, що ви робите, і це полегшить роботу з профілювання.

Інша річ, на яку слід звернути увагу, - це структури даних. Я не знаю, що ви робите зі своїми даними, але ви могли б спростити дані, які ви зберігаєте, будь-яким способом? Чи можете ви використовувати рядок або байтовий масив, а потім витягти відповідні частини з тих елементів, як вони вам потрібні? Чи можете ви використовувати структуру замість класу і навіть зробити щось зло зі stackalloc, щоб відкласти пам'ять і уникнути запуску GC?


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

1
П’ятихвилинна річ пов'язана з тим, що ці 300 з'єднань можуть отримати той самий пакет. Тому я повинен відслідковувати те, що я вже обробив, і 5 хвилин - це кількість часу, яке потрібно, щоб пакети повністю розповсюдилися на всі вузли цієї конкретної мережі.
Джош

3

Простий підхід: спробуйте запам'ятовувати .

  • Оптимізовано для виконання таких завдань.
  • Він може повторно використовувати запасну пам’ять на менш зайнятих коробках, а не лише на спеціально виділеній коробці.
  • У нього є вбудований механізм закінчення кешу, який ледачий, тому не виникає гикавки.

Мінус полягає в тому, що він заснований на пам'яті і не має стійкості. Якщо екземпляр не працює, дані не зникають. Якщо вам потрібна наполегливість, серіалізуйте дані самостійно.

Більш складний підхід: спробуйте Redis .

  • Оптимізовано для виконання таких завдань.
  • Він має вбудований механізм закінчення кешу .
  • Він легко масштабує / клаптує.
  • Він має наполегливість.

Мінус у тому, що це трохи складніше.


1
Memcached може бути розбитий на машинах, щоб збільшити кількість наявної оперативної пам’яті. У вас може бути другий сервер, який серіалізує дані до файлової системи, щоб ви не втратили речі, якщо поле пам’яті знизиться. API Memcache дуже простий у використанні і працює з будь-якої мови, що дозволяє використовувати різні стеки в різних місцях.
Майкл Шопсін

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 з'єднань можуть отримувати такий самий точний пакет. Тому мені потрібно зберігати кожен помічений пакет не менше п’яти хвилин, щоб переконатися, що я не обробляю їх / рахую їх більше одного разу. Для чого призначений ключ для словника.
Джош

1

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

Використовуйте структуру, а не клас для своїх даних (але пам’ятайте, що вона трактується як значення із семантикою пропускання копії). Це займає один рівень пошуку, за допомогою якого GC повинен пройти кожен знак.

Використовуйте масиви (якщо ви знаєте розмір даних, які ви зберігаєте) або Список - який використовує масиви внутрішньо. Якщо вам дійсно потрібен швидкий випадковий доступ, використовуйте словник індексів масиву. Це вимагає ще декількох рівнів (або десяток або більше, якщо ви використовуєте SortedDictionary), щоб gc потребував пошуку.

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

Поєднання struct & list значно зменшує споживання пам'яті та розмір сміттєзбірника.


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