Métodos eficientes para armazenar dezenas de milhões de objetos para consulta, com um alto número de inserções por segundo?


15

Este é basicamente um aplicativo de registro / contagem que conta o número de pacotes e o tipo de pacote etc. em uma rede de bate-papo p2p. Isso equivale a cerca de 4-6 milhões de pacotes em um período de 5 minutos. E como eu tiro apenas um "instantâneo" dessas informações, apenas removo pacotes com mais de 5 minutos a cada cinco minutos. Portanto, o máximo de itens que estarão nesta coleção é de 10 a 12 milhões.

Como preciso fazer 300 conexões com diferentes superpeers, é possível que cada pacote esteja tentando ser inserido pelo menos 300 vezes (e é provavelmente por isso que manter esses dados na memória é a única opção razoável).

Atualmente, estou usando um dicionário para armazenar essas informações. Mas, devido à grande quantidade de itens que estou tentando armazenar, encontro problemas com a pilha de objetos grandes e a quantidade de uso de memória aumenta continuamente ao longo do tempo.

Dictionary<ulong, Packet>

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

Eu tentei usar o mysql, mas ele não foi capaz de acompanhar a quantidade de dados que eu preciso inserir (enquanto verificava para garantir que não era uma duplicata), e isso enquanto usava transações.

Eu tentei o mongodb, mas o uso da CPU era insano e não o mantinha.

Meu problema principal surge a cada 5 minutos, porque removo todos os pacotes com mais de 5 minutos e tiro um instantâneo desses dados. Como eu estou usando consultas LINQ para contar o número de pacotes que contêm um determinado tipo de pacote. Também estou chamando uma consulta distinta () nos dados, em que retiro 4 bytes (endereço IP) da chave do keyvaluepair e a combino com o valor requestingport no valor do keyvalupair e o uso para obter um número distinto de pares de todos os pacotes.

Atualmente, o aplicativo mantém em torno de 1,1 GB de uso de memória e, quando um instantâneo é chamado, pode chegar ao ponto de duplicar o uso.

Agora, isso não seria um problema se eu tiver uma quantidade insana de RAM, mas a vm em que estou executando esta limitada a 2 GB de RAM no momento.

Existe alguma solução fácil?


É um cenário com muita memória e, além disso, você está usando uma vm para executar o aplicativo, uau. De qualquer forma, você explorou o memcached para armazenar os pacotes. Basicamente, você pode executar o memcached em uma máquina separada e o aplicativo pode continuar em execução na própria VM.

Como você já tentou o MySQL e o MongoDB, parece que talvez os requisitos do seu aplicativo (se você quiser fazer o certo) ditem que você simplesmente precisa de mais potência. Se seu aplicativo é importante para você, reforce o servidor. Você também pode revisitar seu código de "limpeza". Tenho certeza de que você pode encontrar uma maneira mais otimizada de lidar com isso, na medida em que isso não torne seu aplicativo inutilizável.
Matt Beckman

4
O que o seu profiler lhe diz?
jasonk

Você não receberá nada mais rápido que o heap local. Minha sugestão seria invocar manualmente a coleta de lixo após a limpeza.
Vartec 15/03/12

@vartec - na verdade, contrariamente à crença popular, invocar manualmente o coletor de lixo na verdade não garante imediatamente, bem ... a coleta de lixo. O GC pode adiar a ação para um período posterior, de acordo com o próprio algoritmo gc. Invocá-lo a cada 5 minutos pode até aumentar a tensão, em vez de aliviá-la. Apenas dizendo;)
Jas

Respostas:


12

Em vez de ter um dicionário e pesquisar entradas muito antigas nesse dicionário; tem 10 dicionários. A cada 30 segundos, crie um novo dicionário "atual" e descarte o dicionário mais antigo sem fazer nenhuma pesquisa.

Em seguida, ao descartar o dicionário mais antigo, coloque todos os objetos antigos em uma fila FILO para mais tarde e, em vez de usar "novo" para criar novos objetos, retire um objeto antigo da fila FILO e use um método para reconstruir o antigo objeto (a menos que a fila de objetos antigos esteja vazia). Isso pode evitar muitas alocações e muita sobrecarga de coleta de lixo.


1
Particionando por intervalo de tempo! Apenas o que eu ia sugerir.
James Anderson

O problema disso é que eu precisaria consultar todos os dicionários criados nos últimos cinco minutos. Como existem 300 conexões, o mesmo pacote chegará a cada uma delas pelo menos uma vez. Portanto, para não manipular o mesmo pacote mais de uma vez, devo mantê-los por pelo menos 5 minutos.
Josh

1
Parte do problema com estruturas genéricas é que elas não são personalizadas para uma finalidade específica. Talvez você deva adicionar um campo "nextItemForHash" e um campo "nextItemForTimeBucket" à estrutura do Packet, implementar sua própria tabela de hash e parar de usar o Dictionary. Dessa forma, você pode encontrar rapidamente todos os pacotes que são muito antigos e pesquisar apenas uma vez quando um pacote é inserido (ou seja, pegue seu bolo e coma também). Também ajudaria na sobrecarga do gerenciamento de memória (como o "Dicionário" não alocaria / liberaria estruturas de dados extras para o gerenciamento do Dicionário).
Brendan

@ Josh, a maneira mais rápida de determinar se você já viu algo antes é um hashset . Conjuntos de hash com fatias de tempo seriam rápidos e você ainda não precisaria procurar para remover itens antigos. Se você não o viu antes, pode armazená-lo no seu dictionar (s).
Básico


3

O primeiro pensamento que vem à mente é por que você espera 5 minutos. Você poderia tirar fotos com mais frequência e, assim, reduzir a grande sobrecarga que você vê no limite de 5 minutos?

Em segundo lugar, o LINQ é ótimo para código conciso, mas, na realidade, o LINQ é o açúcar sintático no C # "regular" e não há garantia de que ele gerará o código mais ideal. Como exercício, você pode tentar reescrever os pontos de acesso sem o LINQ, talvez não melhore o desempenho, mas terá uma idéia mais clara do que está fazendo e isso facilitaria o trabalho de criação de perfil.

Outra coisa a olhar é estruturas de dados. Não sei o que você faz com seus dados, mas você poderia simplificar os dados armazenados de alguma forma? Você poderia usar uma matriz de cadeia de caracteres ou bytes e extrair partes relevantes desses itens conforme necessário? Você poderia usar uma struct em vez de uma classe e até fazer algo errado com o stackalloc para reservar a memória e evitar execuções de GC?


1
Não use uma matriz de cadeia de caracteres / bytes, use algo como um BitArray: msdn.microsoft.com/en-us/library/… para evitar ter que modificar manualmente os bits. Caso contrário, essa é uma boa resposta, não há realmente uma opção fácil além de algoritmos melhores, mais hardware ou hardware melhor.
Ed James

1
A coisa de cinco minutos se deve ao fato de que essas 300 conexões podem receber o mesmo pacote. Portanto, eu tenho que acompanhar o que eu já manusei, e 5 minutos é o tempo que leva para os pacotes se propagarem completamente para todos os nós nessa rede específica.
21139 Josh

3

Abordagem simples: tente memcached .

  • É otimizado para executar tarefas como esta.
  • Ele pode reutilizar memória sobressalente em caixas menos ocupadas, não apenas na sua caixa dedicada.
  • Possui mecanismo de expiração de cache embutido, que é preguiçoso, portanto não há soluços.

A desvantagem é que é baseado em memória e não tem nenhuma persistência. Se uma instância estiver inativa, os dados desaparecerão. Se você precisar de persistência, serialize os dados você mesmo.

Abordagem mais complexa: tente Redis .

A desvantagem é que é um pouco mais complexo.


1
O Memcached pode ser dividido entre máquinas para aumentar a quantidade de memória RAM disponível. Você poderia ter um segundo servidor serializando dados no sistema de arquivos para não perder nada se uma caixa de memcache cair. A API do Memcache é muito simples de usar e funciona em qualquer idioma, permitindo que você use pilhas diferentes em lugares diferentes.
Michael Shopsin

1

Você não precisa armazenar todos os pacotes para as consultas que você mencionou. Por exemplo - contador de tipo de pacote:

Você precisa de duas matrizes:

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

A primeira matriz controla quantos pacotes em tipos diferentes. A segunda matriz controla quantos pacotes foram adicionados a cada minuto, de modo que você saiba quantos pacotes precisam ser removidos a cada intervalo de minutos. Espero que você possa dizer que o segundo array é usado como uma fila FIFO redonda.

Portanto, para cada pacote, são executadas as seguintes operações:

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

A qualquer momento, os contadores de pacotes podem ser recuperados pelo índice instantaneamente e não armazenamos todos os pacotes.


O principal motivo para ter que armazenar os dados que eu faço é o fato de que essas 300 conexões podem receber o mesmo pacote exato. Portanto, preciso manter todos os pacotes vistos por pelo menos cinco minutos para garantir que eu não os manipule / conte mais de uma vez. Qual é o significado do ulong da chave do dicionário.
Josh

1

(Eu sei que essa é uma pergunta antiga, mas eu a encontrei enquanto procurava uma solução para um problema semelhante em que a segunda passagem de coleta de lixo gen estava pausando o aplicativo por vários segundos, gravando para outras pessoas em situação semelhante).

Use uma estrutura em vez de uma classe para seus dados (mas lembre-se de que eles são tratados como um valor com semântica de passagem por cópia). Isso remove um nível de pesquisa que o gc precisa fazer a cada passagem de marca.

Use matrizes (se você souber o tamanho dos dados que está armazenando) ou Lista - que usa matrizes internamente. Se você realmente precisa do acesso aleatório rápido, use um dicionário de índices de matriz. Isso remove outros níveis (ou uma dúzia ou mais, se você estiver usando um SortedDictionary) para que o gc precise pesquisar.

Dependendo do que você está fazendo, a pesquisa em uma lista de estruturas pode ser mais rápida que a pesquisa no dicionário (devido à localização da memória) - perfil para seu aplicativo específico.

A combinação de estrutura e lista reduz significativamente o uso da memória e o tamanho da varredura do coletor de lixo.


Eu tenho uma experiência recente, que gera coleções e dicionários em disco mais rápido, usando SQLite github.com/modma/PersistenceCollections
ModMa
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.