¿Métodos eficientes para almacenar decenas de millones de objetos para consultas, con un alto número de inserciones por segundo?

15

Esto es básicamente una aplicación de registro / conteo que cuenta el número de paquetes y el tipo de paquete, etc. en una red de chat p2p. Esto equivale a unos 4-6 millones de paquetes en un período de 5 minutos. Y debido a que solo tomo una "instantánea" de esta información, solo estoy eliminando paquetes de más de 5 minutos cada cinco minutos. Entonces, la cantidad máxima de artículos que estarán en esta colección es de 10 a 12 millones.

Debido a que necesito hacer 300 conexiones a diferentes superpeers, es una posibilidad que cada paquete intente insertarse al menos 300 veces (lo que probablemente sea la razón por la cual mantener estos datos en la memoria es la única opción razonable).

Actualmente, he estado usando un diccionario para almacenar esta información. Pero debido a la gran cantidad de elementos que estoy tratando de almacenar, me encuentro con problemas con el montón de objetos grandes y la cantidad de uso de memoria crece continuamente con el tiempo.

Dictionary<ulong, Packet>

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

Intenté usar mysql, pero no pude mantener la cantidad de datos que necesito insertar (mientras lo comprobaba para asegurarme de que no era un duplicado), y eso fue mientras usaba las transacciones.

Intenté mongodb, pero el uso de la CPU para eso fue una locura y tampoco se mantuvo.

Mi problema principal surge cada 5 minutos, porque elimino todos los paquetes que tienen más de 5 minutos y tomo una "instantánea" de estos datos. Como estoy usando consultas LINQ para contar el número de paquetes que contienen un determinado tipo de paquete. También estoy llamando a una consulta distinta () en los datos, donde elimino 4 bytes (dirección IP) de la clave del keyvaluepair, y lo combino con el valor del puerto solicitante en el Valor del keyvalupair y lo uso para obtener un número distinto de pares de todos los paquetes.

Actualmente, la aplicación oscila alrededor de 1,1 GB de uso de memoria, y cuando se llama una instantánea, puede llegar a duplicar el uso.

Ahora, esto no sería un problema si tengo una cantidad increíble de ram, pero la vm en la que estoy ejecutando está limitada a 2GB de ram en este momento.

¿Hay alguna solución fácil?

Josh
fuente
Es un escenario muy intensivo en memoria y además de eso estás usando un vm para ejecutar la aplicación, wow. De todos modos, ¿exploró Memcached para almacenar los paquetes? Básicamente, puede ejecutar memcached en una máquina separada y la aplicación puede seguir ejecutándose en la máquina virtual.
Como ya ha probado MySQL y MongoDB, parece que quizás los requisitos de su aplicación (si desea hacerlo correctamente) dictan que simplemente necesita más potencia. Si su aplicación es importante para usted, refuerce el servidor. También es posible que desee volver a visitar su código de "purga". Estoy seguro de que podría encontrar una forma más optimizada de manejar eso, en la medida en que no deje su aplicación inutilizable.
Matt Beckman
44
¿Qué te dice tu perfilador?
jasonk
No obtendrá nada más rápido que el montón local. Mi sugerencia sería invocar manualmente la recolección de basura después de la purga.
vartec
@vartec: de hecho, contrariamente a la creencia popular, invocar manualmente al recolector de basura en realidad no garantiza la recolección de basura inmediata, bueno ... El GC puede diferir la acción a un período posterior de acuerdo con el propio algoritmo gc. Invocarlo cada 5 minutos podría incluso aumentar la tensión, en lugar de aliviarlo. Solo digo;)
Jas

Respuestas:

12

En lugar de tener un diccionario y buscar entradas que sean demasiado antiguas en ese diccionario; tener 10 diccionarios. Cada 30 segundos, más o menos, cree un nuevo diccionario "actual" y deseche el diccionario más antiguo sin realizar ninguna búsqueda.

A continuación, cuando descarte el diccionario más antiguo, coloque todos los objetos antiguos en una cola FILO para más adelante y, en lugar de usar "nuevo" para crear nuevos objetos, retire un objeto antiguo de la cola FILO y use un método para reconstruir el antiguo objeto (a menos que la cola de objetos antiguos esté vacía). Esto puede evitar muchas asignaciones y una sobrecarga de recolección de basura.

Brendan
fuente
1
Particionamiento por rebanada de tiempo! Justo lo que iba a sugerir.
James Anderson el
El problema con esto es que tendría que consultar todos los diccionarios que se hicieron en los últimos cinco minutos. Como hay 300 conexiones, el mismo paquete llegará a cada una al menos una vez. Entonces, para no manejar el mismo paquete más de una vez, debo mantenerlos durante al menos el período de 5 minutos.
Josh
1
Parte del problema con las estructuras genéricas es que no están personalizadas para un propósito específico. Tal vez debería agregar un campo "nextItemForHash" y un campo "nextItemForTimeBucket" a su estructura de paquetes e implementar su propia tabla hash, y dejar de usar Dictionary. De esa manera, puede encontrar rápidamente todos los paquetes que son demasiado viejos y solo buscar una vez cuando se inserta un paquete (es decir, tener su pastel y comérselo también). También ayudaría a la sobrecarga de administración de memoria (ya que "Diccionario" no estaría asignando / liberando estructuras de datos adicionales para la administración de Diccionario).
Brendan
@Josh la forma más rápida de determinar si has visto algo antes es un hashset . Los conjuntos de hash cortados en el tiempo serían rápidos y aún no necesitaría buscar para desalojar elementos antiguos. Si no lo ha visto antes, puede almacenarlo en su disco (s / s).
Básico
3

El primer pensamiento que me viene a la mente es por qué esperas 5 minutos. ¿Podría hacer las instantáneas más a menudo y así reducir la gran sobrecarga que ve en el límite de 5 minutos?

En segundo lugar, LINQ es ideal para un código conciso, pero en realidad LINQ es azúcar sintáctica en C # "regular" y no hay garantía de que genere el código más óptimo. Como ejercicio, podría intentar reescribir los puntos calientes sin LINQ, es posible que no mejore el rendimiento, pero tendrá una idea más clara de lo que está haciendo y facilitaría el trabajo de creación de perfiles.

Otra cosa a tener en cuenta son las estructuras de datos. No sé qué haces con tus datos, pero ¿podrías simplificar los datos que almacenas de alguna manera? ¿Podría usar una serie de cadenas o bytes y luego extraer partes relevantes de esos elementos según los necesite? ¿Podría usar una estructura en lugar de una clase e incluso hacer algo malo con stackalloc para reservar memoria y evitar ejecuciones de GC?

Steve
fuente
1
No use una matriz de cadena / byte, use algo como un BitArray: msdn.microsoft.com/en-us/library/… para evitar tener que hacer un bit-twiddle manualmente. De lo contrario, esta es una buena respuesta, no hay realmente una opción fácil que no sea mejores algoritmos, más hardware o mejor hardware.
Ed James
1
Lo de cinco minutos se debe al hecho de que estas 300 conexiones pueden recibir el mismo paquete. Por lo tanto, tengo que hacer un seguimiento de lo que ya he manejado, y 5 minutos es la cantidad de tiempo que tardan los paquetes en propagarse completamente a todos los nodos de esta red en particular.
Josh
3

Enfoque simple: intente memcached .

  • Está optimizado para ejecutar tareas como esta.
  • Puede reutilizar la memoria de reserva en cajas menos ocupadas, no solo en su caja dedicada.
  • Tiene un mecanismo de caducidad de caché incorporado, que es vago, por lo que no tiene problemas.

La desventaja es que está basado en la memoria y no tiene ninguna persistencia. Si una instancia está inactiva, los datos se han ido. Si necesita persistencia, serialice los datos usted mismo.

Enfoque más complejo: pruebe Redis .

  • Está optimizado para ejecutar tareas como esta.
  • Tiene un mecanismo de caducidad de caché incorporado .
  • Se escala / fragmenta fácilmente.
  • Tiene persistencia.

La desventaja es que es un poco más complejo.

9000
fuente
1
Memcached se puede dividir entre máquinas para aumentar la cantidad de ram disponible. Podría tener un segundo servidor serializando datos en el sistema de archivos para que no pierda cosas si un cuadro de memoria caché se cae. La API de Memcache es muy simple de usar y funciona desde cualquier idioma, lo que le permite usar diferentes pilas en diferentes lugares.
Michael Shopsin
1

No tiene que almacenar todos los paquetes para las consultas que ha mencionado. Por ejemplo, contador de tipo de paquete:

Necesitas dos matrices:

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

La primera matriz realiza un seguimiento de cuántos paquetes en diferentes tipos. La segunda matriz realiza un seguimiento de cuántos paquetes más se agregaron en cada minuto, de modo que sepa cuántos paquetes deben eliminarse en cada intervalo de minutos. Espero que sepas que la segunda matriz se usa como una cola FIFO redonda.

Entonces, para cada paquete, se realizan las siguientes operaciones:

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;
}

En cualquier momento, el contador puede recuperar los contadores de paquetes instantáneamente y no podemos almacenar todos los paquetes.

Codismo
fuente
La razón principal para tener que almacenar los datos que hago es el hecho de que estas 300 conexiones pueden recibir el mismo paquete exacto. Por lo tanto, necesito conservar todos los paquetes vistos durante al menos cinco minutos para asegurarme de no manejarlos / contarlos más de una vez. Para eso sirve el ulong para la clave del diccionario.
Josh
1

(Sé que esta es una pregunta antigua, pero la encontré mientras buscaba una solución a un problema similar en el que el pase de recolección de basura de segunda generación estaba pausando la aplicación durante varios segundos, por lo que grabé para otras personas en una situación similar).

Use una estructura en lugar de una clase para sus datos (pero recuerde que se trata como un valor con semántica de paso por copia). Esto elimina un nivel de búsqueda, el gc tiene que hacer cada pase de marca.

Utilice matrices (si conoce el tamaño de los datos que está almacenando) o Lista, que utiliza matrices internamente. Si realmente necesita el acceso aleatorio rápido, use un diccionario de índices de matriz. Esto elimina otro par de niveles (o una docena o más si está usando un SortedDictionary) para que el gc tenga que buscar.

Dependiendo de lo que esté haciendo, buscar una lista de estructuras puede ser más rápido que la búsqueda del diccionario (debido a la localización de la memoria) - perfil para su aplicación particular.

La combinación de struct & list reduce significativamente el uso de memoria y el tamaño del barrido del recolector de basura.

Malcolm
fuente
Tengo un experimento reciente, que genera colecciones y diccionarios en disco tan rápido, usando sqlite github.com/modma/PersistenceCollections
ModMa