2014-06-24 10 views
2

guava 라이브러리의 CacheBuilder 및 LocalCache를 사용하고 있지만 getAllPresent의 성능 문제는 p99.9 대기 시간이 약 300 ~ 400 밀리 초입니다. 요청 지연은 대략 다음과 같은 구성이 사용되는LocalCache guava, 높은 처리량을위한 최적화

(P99은 약 150 밀리) P99 및 p99.9 사이에 두 배로 refreshAfterWrite위한 120 초 MAXSIZE 24 시간 2e6 및 만료되도록 설정되고, 초기 용량은 1e6. removeListener가 사용되지 않고 expireAfterWrite가 없습니다. ConcurrencyLevel 256 (다른 값을 시도 함). 기계에는 12 개의 코어가 있습니다. 캐시 사용 중에는 8e5에서 1.2e6까지의 항목이 있습니다. 사용 패턴은 p99.9 및 약 100 qps의 약 3k 키에 대해 getAllPresent입니다.

키는 hashCode의 복잡한 객체이며 Objects.hash 메서드는 거기에 제공된 모든 필드와 함께 사용됩니다. 분산이 균일한지 확인하기 위해 다른 해시 함수를 시도했습니다 (murmur3와 유사한 결과 표시). 그래서 문제는 충돌이 아닙니다.

성능을 향상시키는 방법에 대한 지침이 있습니까?

+0

실제 벤치 마크 코드를 알려 주시면 감사하겠습니다. – dimo414

+0

공유하지 않고 간단하게 stopwatch.start(); after cache.getAllPresent 및 stopwatch.elapsed (TimeUnit.MILLISECONDS) 후에. 이 대기 시간이보고 된 시스템에서 통계를 추적하는 별도의 시스템이 있습니다. 나중에 모든 컴퓨터에서 집계됩니다. –

+0

refreshAfterWrite를 의미합니까? CacheLoader를 사용하고 있습니까? getAllPresent()는 캐시 로더를 호출하지 않습니다. – cruftex

답변

3

99 % 타일이 90 % 타일의 두 배가되고 99.9 % 타일이 99 % 타일의 두 배가되는 것이 Java에서 효율적이라고 말하고 싶습니다. 이 패턴이 보이면 대기 시간을 줄이기 위해 전체 운영 비용을 줄여야합니다. 즉, 빠른 도움이 될 가능성은 거의 없습니다.

참고 : 캐시가 크고 스캔 할 때 모든 항목에 적어도 한두 개의 L3 캐시 누락이있을 것으로 예상 할 수 있습니다. 이것은 비싸 질 것입니다. CPU 캐시에 맞는 작은 캐시의 경우이 작업이 여러 번 빨라집니다.

프로필러를 사용하여이 작업의 CPU 및 메모리 할당을 줄이거 나 필요한 것을 수행하기 위해 캐시를 호출하는 방법을 변경하면 99.9 % 타일이 삭제됩니다. 요청 시간 변화에

+0

Peter,이 부분에 대해 자세히 설명해주십시오 :이 패턴이 보이면 조작에 드는 비용을 줄여야합니까? –

+0

필자가 설명하는 패턴은 효율적이고 단순하지 않은 코드 IMHO에서는 정상이며 일부는 빠른 승리가 아님을 나타냅니다. 결과를 향상 시키려면 일반적인 성능을 더 빨리 만들어야하며 모든 백분위 수를 향상시킵니다. 프로필은 일반 또는 평균 대기 시간을 향상시키는 데 도움이됩니다 (특히 높은 백분위 수의 문제를 찾는 데 그리 좋지 않음). –

+0

@RomanDzhabarov이 프레젠테이션보기 esp 슬라이드 8 http://www.slideshare.net/PeterLawrey/writing-and-testing-high-frequency-trading-engines-in-java –

3

은/단순히 getAllPresent의 통화 중 가끔 GC 수 있습니다

"요청 시간은 P99와 p99.9 사이에 두 배로". 이를 실제로 조사하려면 GC 활동 (카운터 만)을 추적하는 벤치 마크를 제거해야합니다.

또 다른 문제의 원인은 잠금 경합 일 수 있습니다. 문제 설명에 정확한 액세스 패턴이 누락되었습니다. 얼마나 많은 요청이 병렬로 처리됩니까? 핵심 공간은 어떻게 겹 칩니 까? Guava는 캐시 해시 테이블을 내부적으로 분할하고 concurrencyLevel을 힌트로 사용합니다. LRU 목록을 업데이트해야하므로 읽기 액세스가 완전히 잠기지 않습니다. 다른 스레드에서 동일한 키에 액세스 할 때, 이것은 잠금 경합의 소스입니다. 다음은이 효과를 나타내는 nitro cache performance에 대한 (구형 인) 평가입니다. (업데이트 : 구아바 캐시는 읽기에 잠금을 피하기 위해 몇 가지 전략이있다, 이것은 추가 조사 필요)하는 방법에 대한

빠른

가장 비용이 많이 드는 일을 당신이 캐시 액세스 (15 회?) 퇴거 알고리즘이 데이터 구조를 업데이트하는 것입니다. 그러나 최대 캐시 크기 (2E6)는 최대 경험 크기 (1.2E6)를 초과합니다. 용량 제한이 결코 도달하지 않기 때문에 퇴거가 발생하지 않습니다. 즉, Guava 캐시의 LRU 목록을 모두 업데이트하는 것은 의미가 없습니다. 나는 Google 구아바, EHCache, infinispan 및 다른 축출 전략에 대한 캐시 런타임을 cache2k benchmarks에서 벤치 마크했습니다. "히트에 대한 런타임 비교"를 참조하십시오. 멀티 스레드 액세스에 대한 벤치 마크가 아직 없습니다. 이것은 8 월에 나타납니다.

제 생각에 구아바 캐시에서 퇴거 전략을 변경하거나 전환 할 수있는 옵션이 없습니다 (다른 사람이 할 수 있습니까?).

cache2k에서 잠금 해제 읽기 액세스를 허용하는 대체 퇴거 전략을 실험합니다. 시나리오 내에서 간단히 "random eviction"을 선택할 수 있으며 약 15 배의 속도 향상을 기대할 수 있습니다. 또한 cache2k 캐시는 hashCode() 구현에 대한 해시 테이블 통계 및 품질 메트릭을 출력합니다. cache2k statistics .

빠른 평가가 가능해야합니다. 여기에 몇 가지 코드 조각에서는 사용자가 빠르게 시작할 수 :

<dependency> 
    <groupId>org.cache2k</groupId> 
    <artifactId>cache2k-core</artifactId> 
    <version>0.19.1</version> 
</dependency> 
<dependency> 
    <groupId>org.cache2k</groupId> 
    <artifactId>cache2k-api</artifactId> 
    <version>0.19.1</version> 
</dependency> 

비고 : 캐시 구현은 API 모듈에 노출되지 않습니다, 우리는 컴파일 범위의 핵심 모듈을 필요로하는 이유가 있습니다. 캐시 초기화 :

// optional data source (similar to CacheLoader) 
CacheSource<Integer, String> source = 
    new CacheSource<Integer, String>() { 
    public String get(Integer o) { 
     return o + "hello"; 
    } 
    }; 
Cache<Integer, String> cache = 
    CacheBuilder.newCache(Integer.class, String.class) 
    .implementation(RandomCache.class) 
    .maxSize(3000000) 
    .expiryMillis(120 * 1000) 
    /* optional, if cache should do the refresh itself 
    .source(source) 
    .backgroundRefresh(true) 
    */ 
    .build(); 

옵션을 변경하여 다른 퇴거 알고리즘을 시험해 볼 수 있습니다. getAllPresent가 cache2k에서 사용할 수 없습니다, 당신은 스스로를 코딩 할 수 있습니다 : cache2k에서

public Map<Integer, String> getAllPresent(Iterator<Integer> it) { 
    HashMap<Integer, String> hash = new HashMap<>(); 
    while(it.hasNext()) { 
    int k = it.next(); 
    String v = cache.peek(k); 
    if (v != null) { 
     hash.put(k, v); 
    } 
    } 
    return hash; 
} 

cache.peek() 캐시 소스를 호출하지 않고 매핑 된 요소를 반환, 즉 정확히 의도 getAllPresent의 의미입니다. 해시 맵을 작성하면 실제로 많은 GC로드가 생성됩니다. getAll 또는 getAllPresent과 같은 대량 작업의 사용은주의 깊게 결정해야합니다. cache2k의 액세스 시간은 해시 테이블 액세스 시간과 비슷하기 때문에 대량 작업으로 인해 작업 속도가 빨라지지는 않을 것입니다.

getAllPresent에 주() 내 cache2k

동일한 목적에 대해 작용하는 JSR107 호환 getAll() 방법이있다. API 설계자 입장에서 볼 때 이러한 방법은 악의적인데, 이는 리소스를 제어하기위한 캐시의 아이디어와 모순되기 때문입니다. 그냥 cache.get() 또는 cache.peek()가 있습니다. CacheSource (aka CacheLoader)가있는 경우 cache.prefetch(keys) "캐시에 말하기"를 사용하여 다음 키와 함께 작업 할 수 있습니다 .... 미안하지만 약간의 문제가 있습니다.

+0

퇴거가 발생하지 않지만 새로 고침 시간은 어떻게됩니까? 열쇠가 다시로드되게합니까? –

+0

100-3k 키의 액세스 패턴이 멀티 패턴입니다. –

+0

"새로 고침 시간은 어떻게됩니까?" 타이머의 경우 캐시는 다른 데이터 구조를 사용합니다. 새로 고침/만기가 발생하려면 캐시 항목이 ** 업데이트 **되면 캐시가 데이터를 업데이트해야합니다. "좋은 퇴거"가 발생하려면 캐시가 모든 ** 읽기 액세스 **에서 데이터 구조를 업데이트해야합니다. 그것은 큰 차이입니다. – cruftex