초당 많은 수의 삽입을 사용하여 쿼리를 위해 수천만 개의 객체를 저장하는 효율적인 방법은 무엇입니까?


15

이것은 기본적으로 p2p 채팅 네트워크에서 패킷 수를 계산하고 패킷 유형 등을 계산하는 로깅 / 카운팅 응용 프로그램입니다. 이는 5 분 동안 약 4-6 백만 패킷에 해당합니다. 이 정보의 "스냅 샷"만 가져 오기 때문에 5 분마다 5 분보다 오래된 패킷 만 제거합니다. 따라서이 컬렉션에 포함될 최대 항목 수는 1 천 1 백만에서 1 천 2 백만입니다.

다른 수퍼 피어에 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을 시도했지만 그 CPU 사용량은 미쳤으며 유지하지 못했습니다.

5 분보다 오래된 모든 패킷을 제거하고이 데이터의 "스냅 샷"을 작성하기 때문에 5 분마다 주요 문제가 발생합니다. LINQ 쿼리를 사용하여 특정 패킷 유형을 포함하는 패킷 수를 계산합니다. 또한 데이터에 대해 distinct () 쿼리를 호출하여 키 값 쌍의 키에서 4 바이트 (ip 주소)를 제거하고 키 값 쌍의 Value에서 요청 포트 값과 결합하여 고유 한 수를 얻습니다. 모든 패킷에서 피어.

응용 프로그램은 현재 약 1.1GB의 메모리 사용량을 차지하며 스냅 샷이 호출되면 사용량을 두 배로 늘릴 수 있습니다.

이제 엄청난 양의 램이 있으면 문제가되지 않지만 현재 실행중인 vm은 현재 2GB의 램으로 제한됩니다.

쉬운 해결책이 있습니까?


메모리를 많이 사용하는 시나리오이며 응용 프로그램을 실행하기 위해 vm을 사용하고 있습니다. 어쨌든, 당신은 패킷을 저장하기 위해 memcached를 탐색 했습니까? 기본적으로 별도의 시스템에서 memcached를 실행할 수 있으며 응용 프로그램은 vm 자체에서 계속 실행될 수 있습니다.

이미 MySQL과 MongoDB를 모두 시도했지만 응용 프로그램의 요구 사항 (바로 원한다면)이 더 많은 마력을 필요로하는 것으로 보입니다. 응용 프로그램이 중요한 경우 서버를 강화하십시오. "퍼지"코드를 다시 방문 할 수도 있습니다. 앱을 사용할 수 없게하지 않는 한 더 최적화 된 처리 방법을 찾을 수 있다고 확신합니다.
Matt Beckman

4
프로파일 러가 무엇을 말합니까?
jasonk

로컬 힙보다 빠른 것은 없습니다. 내 제안은 제거 후 가비지 수집을 수동으로 호출하는 것입니다.
vartec

@vartec-사실, 일반적인 생각과는 달리 수동으로 가비지 수집기를 호출한다고해서 실제로 가비지 수집이 즉시 보장되는 것은 아닙니다. GC는 자체 gc 알고리즘에 따라 나중에 조치를 연기 할 수 있습니다. 5 분마다 호출하면 완화하는 대신 변형을 추가 할 수도 있습니다. ;)
Jas

답변:


12

하나의 사전을 가지고 그 사전에서 너무 오래된 항목을 검색하는 대신; 10 개의 사전이 있습니다. 30 초마다 새로운 "현재"사전을 작성하고 검색하지 않고 가장 오래된 사전을 버립니다.

다음으로 가장 오래된 사전을 버릴 때 나중에 이전의 모든 객체를 FILO 대기열에 넣고 "new"를 사용하여 새 객체를 만드는 대신 기존 객체를 FILO 대기열에서 꺼내고 기존의 객체를 재구성하는 방법을 사용합니다 객체 (오래된 객체의 대기열이 비어 있지 않은 경우). 이것은 많은 할당과 많은 가비지 콜렉션 오버 헤드를 피할 수 있습니다.


1
타임 슬라이스로 파티셔닝! 내가 제안하려고했던 것.
James Anderson

이것의 문제는 지난 5 분 이내에 만들어진 모든 사전을 쿼리해야한다는 것입니다. 300 개의 연결이 있으므로 동일한 패킷이 각 패킷에 적어도 한 번 도착합니다. 따라서 동일한 패킷을 두 번 이상 처리하지 않으려면 최소한 5 분 동안 보관해야합니다.
Josh

1
일반 구조의 문제점 중 일부는 특정 목적에 맞게 사용자 정의되지 않았다는 것입니다. 아마도 "nextItemForHash"필드와 "nextItemForTimeBucket"필드를 패킷 구조에 추가하고 고유 한 해시 테이블을 구현하고 Dictionary 사용을 중지해야합니다. 그렇게하면 너무 오래된 모든 패킷을 빠르게 찾을 수 있고 패킷이 삽입 될 때 한 번만 검색 할 수 있습니다 (예 : 케이크를 가지고 먹습니다). 또한 "사전"이 사전 관리를위한 추가 데이터 구조를 할당 / 해제하지 않기 때문에 메모리 관리 오버 헤드에 도움이됩니다.
Brendan

@Josh는 전에 본 적이 있는지 확인하는 가장 빠른 방법은 해시 셋 입니다. 시간 분할 해시 세트는 빠르며 여전히 오래된 항목을 제거하기 위해 검색 할 필요가 없습니다. 전에 본 적이 없다면, 사전 (y / ies)에 저장할 수 있습니다.
기본


3

떠오르는 첫 번째 생각은 5 분을 기다리는 이유입니다. 스냅 샷을 더 자주 수행하여 5 분 경계에서 볼 수있는 큰 과부하를 줄일 수 있습니까?

둘째, LINQ는 간결한 코드에는 적합하지만 실제로 LINQ는 "일반적인"C #에 대한 구문 설탕이며 가장 최적의 코드를 생성 할 것이라는 보장은 없습니다. 연습으로 LINQ없이 핫스팟을 다시 작성하고 재 작성할 수 있으므로 성능을 향상시킬 수는 없지만 수행중인 작업에 대한 명확한 아이디어를 얻을 수 있으며 프로파일 링 작업이 쉬워집니다.

살펴볼 또 다른 사항은 데이터 구조입니다. 데이터로 무엇을하는지 모르겠지만 어떤 방식 으로든 저장 한 데이터를 단순화 할 수 있습니까? 문자열 또는 바이트 배열을 사용한 다음 필요에 따라 해당 항목에서 관련 부분을 추출 할 수 있습니까? 클래스 대신 구조체를 사용하고 메모리를 따로두고 GC 실행을 피하기 위해 stackalloc으로 악한 일을 할 수 있습니까?


1
문자열 / 바이트 배열을 사용하지 말고 BitArray와 같은 것을 사용 하십시오 : msdn.microsoft.com/en-us/library/… 수동으로 비트 트위스트 하지 않아도됩니다. 그렇지 않으면 좋은 대답입니다. 더 나은 알고리즘, 더 많은 하드웨어 또는 더 나은 하드웨어 이외의 쉬운 옵션은 없습니다.
Ed James

1
5 분은이 300 개의 연결이 동일한 패킷을 수신 할 수 있기 때문입니다. 따라서 이미 처리 한 내용을 추적해야하며 5 분은이 특정 네트워크의 모든 노드에 패킷이 완전히 전파되는 데 걸리는 시간입니다.
Josh

3

간단한 접근법 : memcached 시도하십시오 .

  • 이와 같은 작업을 실행하도록 최적화되어 있습니다.
  • 전용 박스뿐만 아니라 사용량이 적은 박스에서도 예비 메모리를 재사용 할 수 있습니다.
  • 캐시 만료 메커니즘이 내장되어있어 게 으르므로 딸꾹질이 없습니다.

단점은 메모리 기반이며 지속성이 없다는 것입니다. 인스턴스가 다운되면 데이터가 사라집니다. 지속성이 필요한 경우 데이터를 직접 직렬화하십시오.

더 복잡한 접근법 : Redis를 사용해보십시오 .

  • 이와 같은 작업을 실행하도록 최적화되어 있습니다.
  • 캐시 만료 메커니즘 이 내장되어 있습니다.
  • 그것은 쉽게 비늘 / 파편입니다.
  • 끈기가 있습니다.

단점은 약간 더 복잡하다는 것입니다.


1
Memcached를 여러 시스템으로 분할하여 사용 가능한 램의 양을 늘릴 수 있습니다. memcache 상자가 다운 되어도 데이터를 잃지 않도록 파일 시스템에 데이터를 직렬화하는 두 번째 서버가있을 수 있습니다. Memcache API는 사용이 매우 간단하고 모든 언어에서 작동하므로 다른 위치에서 다른 스택을 사용할 수 있습니다.
Michael Shopsin

1

언급 한 쿼리에 대한 모든 패키지를 저장할 필요는 없습니다. 예를 들어-패키지 유형 카운터 :

두 개의 배열이 필요합니다 :

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

첫 번째 배열은 다른 유형의 패키지 수를 추적합니다. 두 번째 배열은 1 분마다 추가 된 패키지 수를 추적하여 매 분마다 제거해야하는 패키지 수를 알 수 있습니다. 두 번째 배열이 라운드 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 개의 연결이 동일한 정확한 패킷을 수신 할 수 있기 때문입니다. 따라서 모든 처리 된 패킷을 5 분 이상 유지하여 두 번 이상 처리 / 계산하지 않도록해야합니다. 사전 키의 ulong은 무엇입니까?
Josh

1

(이것은 오래된 질문이라는 것을 알고 있지만 2 세대 가비지 수집 패스가 몇 초 동안 앱을 일시 중지 한 비슷한 문제에 대한 해결책을 찾고있는 동안 비슷한 상황에있는 다른 사람들을 위해 기록했습니다.)

데이터에 클래스가 아닌 구조체를 사용하십시오 (그러나 패스 바이 시맨틱의 값으로 취급됨을 기억하십시오). 이것은 gc가 각 마크 패스를 수행 해야하는 한 수준의 검색을 수행합니다.

배열 (저장하는 데이터의 크기를 알고있는 경우) 또는 List-배열을 내부적으로 사용하는 목록을 사용하십시오. 빠른 임의 액세스가 실제로 필요한 경우 배열 인덱스 사전을 사용하십시오. 이것은 gc가 검색 해야하는 또 다른 두 가지 레벨 (또는 SortedDictionary를 사용하는 경우 12 개 이상)을 가져옵니다.

수행중인 작업에 따라 구조체 목록을 검색하는 것이 특정 응용 프로그램의 메모리 검색으로 인해 사전 검색보다 빠를 수 있습니다.

struct & list의 조합은 메모리 사용량과 가비지 콜렉터 스윕 크기를 모두 줄입니다.


당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.