분산 데이터 캐 시 에서 의 일치 성 해시 알고리즘

12164 단어 hash분포 식캐 시
일치 성 해시 알고리즘 은 분포 식 캐 시 분야 의 MemCached, 부하 균형 분야 의 Nginx 와 각종 RPC 프레임 워 크 에서 광범 위 하 게 응용 되 고 있 으 며, 전통 적 인 해시 함수 가 해시 표 슬롯 수 를 추가 한 후 키 워드 를 다시 매 핑 하 는 문 제 를 해결 하기 위해 서 입 니 다.
본 고 는 일치 성 해시 알고리즘 의 원리 와 실현 을 소개 하고 서로 다른 해시 함수 가 실현 하 는 성능 데이터 대 비 를 제시 하 며 Redis 군집 의 데이터 분할 실현 등 을 연구 하고 글 끝 에 실현 되 는 구체 적 인 github 주 소 를 제시 할 것 이다.
Memcached 와 클 라 이언 트 분포 식 캐 시
Memcached 는 고성능 분포 식 캐 시 시스템 이지 만 서버 에 분포 식 기능 이 없 으 며 각 서버 는 서로 통신 하지 않 습 니 다.그것 의 분포 식 실현 은 클 라 이언 트 의 라 이브 러 리 에 의존 하 는 것 도 Memcached 의 큰 특징 이다.예 를 들 어 제3자 의 spymemcached 클 라 이언 트 는 일치 성 해시 알고리즘 을 바탕 으로 분포 식 캐 시 기능 을 실현 했다.
그 구체 적 인 절 차 는 다음 과 같다.
  • Memcached 에 데 이 터 를 추가 합 니 다. 먼저 클 라 이언 트 의 알고리즘 은 key 값 에 따라 이 key 에 대응 하 는 서버 를 계산 합 니 다.
  • 서버 가 선택 되면 캐 시 데 이 터 를 저장 합 니 다.
  • 데 이 터 를 가 져 올 때 같은 key 에 대해 클 라 이언 트 의 알고리즘 은 같은 서버 에 위치 하여 데 이 터 를 가 져 올 수 있 습 니 다.

  • 이 과정 에서 클 라 이언 트 의 알고리즘 은 먼저 캐 시 된 데 이 터 를 각 서버 에 고 르 게 분포 하도록 해 야 한다. 그 다음 에 개별 서버 가 오프라인 또는 온라인 일 때 데이터 이전 이 나타 나 므 로 이전 해 야 할 데이터 양 을 최대한 줄 여야 한다.
    클 라 이언 트 알고리즘 은 클 라 이언 트 분포 식 캐 시 성능 의 우열 의 관건 이다.
    일반적인 해시 표 알고리즘 은 일반적으로 해시 값 을 계산 한 후 나머지 작업 을 통 해 키 값 을 서로 다른 서버 에 비 추 지만 서버 수량 이 변 할 때 나머지 작업 의 나 누 기 에 변화 가 생 겨 모든 키 가 비 추 는 서버 가 거의 변 합 니 다. 이것 은 분포 식 캐 시 시스템 에 서 는 받 을 수 없습니다.
    일치 성 해시 알고리즘 은 서버 수량 변화 로 인 한 캐 시 이전 을 최대한 줄 일 수 있 습 니 다.
    해시 알고리즘
    우선 일치 성 해시 알고리즘 은 일반적인 해시 알고리즘 에 의존한다.대부분의 학생 들 이 해시 알고리즘 에 대한 이 해 는 JDK hashCode 함수 에 머 물 러 있 을 수 있 습 니 다.사실 해시 알고리즘 은 여러 가지 실현 이 있 는데 그들 은 서로 다른 측면 에서 우열 을 가지 고 서로 다른 장면 에 대해 서로 다른 해시 알고리즘 을 사용 하여 실현 할 수 있다.
    다음은 흔히 볼 수 있 는 해시 알고리즘 몇 가 지 를 소개 하고 분포 의 균일 성, 해시 충돌 확률 과 성능 등에 서 의 우열 을 알 아 보 겠 습 니 다.
    MD5 알고리즘: 모두 Message - Digest Algorithm 5 라 고 부 르 며 정보 전송 이 완전 하 게 일치 하도록 합 니 다.컴퓨터 가 광범 위 하 게 사용 하 는 컴 퓨 팅 알고리즘 중 하나 로 주류 프로 그래 밍 언어 는 보편적으로 MD5 가 실현 되 었 다.MD5 의 역할 은 대 용량 정 보 를 비밀 형식 으로 압축 하 는 것 이다.일반적인 파일 의 완전 성 검 사 는 MD5 를 사용 하 는 것 입 니 다.
    CRC 알고리즘: 전체 이름 은 CyclicRedundancy Check 이 고 중국어 이름 은 순환 중복 검사 입 니 다.이것 은 중요 한 것 으로 인 코딩 과 디 코딩 방법 이 간단 하고 오류 검사 와 오류 정정 능력 이 강 한 해시 알고리즘 으로 통신 분야 에서 오류 통 제 를 실현 하 는 데 널리 사용 된다.
    MurmurHash 알고리즘: 높 은 연산 성능, 낮은 충돌 율, Austin Appleby 가 2008 년 에 만 들 었 고 현 재 는 Hadoop, libstdc +, nginx, libmemcached 등 오픈 소스 시스템 에 응용 되 었 습 니 다.자바 계 에 서 는 Redis, Memcached, Cassandra, HBase, Lucene, Guava 가 모두 사용 하고 있다.
    FNV 알고리즘: 전체 이름 은 Fowler - Noll - vo 알고리즘 으로 세 명의 발명가 Glenn Fowler, Landon Curt Noll, Phong Vo 의 이름 으로 명명 되 었 으 며 이 르 면 1991 년 에 제기 되 었 습 니 다.FNV 는 대량의 데 이 터 를 빠르게 hash 하고 작은 충돌 율 을 유지 할 수 있 습 니 다. 고도 로 분산 되 어 hash 와 매우 비슷 한 문자열, 예 를 들 어 URL, hostname, 파일 이름, text 와 IP 주소 등 이 적 용 됩 니 다.
    Ketama 알고리즘: 일치 성 해시 알고리즘 의 실현 중 하나 입 니 다. 다른 해시 알고리즘 은 통용 되 는 일치 성 해시 알고리즘 이 실현 되 었 습 니 다. 해시 맵 함 수 를 교체 한 것 에 불과 하지만 Ketama 는 전체 절차 입 니 다. 우 리 는 뒤에서 소개 할 것 입 니 다.
    일치 성 해시 알고리즘
    다음은 분포 식 캐 시 장면 을 예 로 들 어 일치 성 해시 알고리즘 링 의 원 리 를 분석 하 겠 습 니 다.
    먼저 캐 시 서버 (ip + 포트 번호) 를 해시 하여 링 의 한 노드 로 매 핑 하여 캐 시 데이터 key 값 의 hash key 를 계산 하고 링 에 도 매 핑 하 며 가장 가 까 운 서버 노드 를 캐 시 에 저장 할 서버 로 선택 합 니 다.구체 적 으로 실현 하면 후속 적 인 장절 을 볼 수 있다.
    예 를 들 어 A, B, C, D 네 개의 캐 시 서버 가 존재 할 때 키 값 이 1 인 캐 시 데이터 가 일치 성 해시 링 에 있 는 위 치 는 다음 그림 과 같 습 니 다. 최근 서버 노드 를 가 져 오 는 규칙 에 따라 이 캐 시 데 이 터 는 서버 B 에 저장 해 야 합 니 다.
    키 값 이 4 인 캐 시 데 이 터 를 저장 하려 면 일치 성 해시 링 의 위 치 는 다음 과 같 기 때문에 서버 C 에 저장 해 야 합 니 다.
    이와 유사 하 게 키 값 이 5, 6 인 데 이 터 는 서비스 D 에, 키 값 이 7, 8 인 데 이 터 는 서비스 A 에 저장 해 야 한다.
    이때 서버 B 가 다운 되 어 서버 B 에 저 장 된 캐 시 데 이 터 를 이전 해 야 하지만 일치 성 해시 링 이 존재 하기 때문에 key 값 이 1 인 데이터 만 이전 하면 다른 데이터 의 저장 서버 는 변 하지 않 습 니 다.이것 도 일치 성 해시 알고리즘 이 나머지 맵 알고리즘 보다 뛰어난 곳 이다.
    서버 B 오프라인 에서 키 값 이 1 인 데 이 터 는 시계 방향 으로 가장 가 까 운 서버 가 C 이기 때문에 데이터 저장 소 는 서버 C 로 이전 합 니 다.
    현실 상황 에서 서버 가 일치 성 해시 링 에 있 는 위치 가 이렇게 고 르 게 분포 되 지 않 아 각 노드 가 실제 적 으로 링 을 차지 하 는 구간 의 크기 가 다르다.
    이런 상황 에서 허 노드 를 늘 려 해결 할 수 있다.가상 노드 를 증가 시 켜 모든 노드 가 링 에 있 는 '관할' 구역 을 더욱 고 르 게 한다.이렇게 하면 노드 가 변화 할 때 데이터 분포 의 변화 에 최대한 작은 영향 을 주 는 동시에 데이터 분포 의 균일 도 확보 할 수 있다.
    구체 적 실현
    다음은 Memcached 분포 식 캐 시 장면 에서 의 일치 성 해시 알고리즘 을 실현 하고 구체 적 인 테스트 성능 데 이 터 를 제시 합 니 다.이 실현 은 kiritomoe 블 로그 의 실현 과 spymemcached 클 라 이언 트 코드 를 참고 했다.구체 적 인 실현 은 제 github 를 보 세 요. 주 소 는?https://github.com/ztelur/con...。 NodeLocator 분포 식 캐 시 장면 에서 일치 하 는 해시 알고리즘 의 추상 적 인 것 으로 getPrimary 함수 가 있 고 캐 시 데 이 터 를 받 는 key 값 으로 이 캐 시 데 이 터 를 저장 하 는 서버 인 스 턴 스 를 출력 합 니 다.
    public interface NodeLocator {
        MemcachedNode getPrimary(String k);
    }

    다음은 통용 되 는 일치 성 해시 알고리즘 의 실현 입 니 다. TreeMap 을 일치 성 해시 링 의 데이터 구조 로 사용 합 니 다. ceilingEntry 함 수 는 링 의 가장 가 까 운 노드 를 얻 을 수 있 습 니 다.buildConsistentHashRing 함수 에는 일치 성 해시 링 을 구축 하 는 과정 이 포함 되 어 있 고 기본적으로 12 개의 가상 노드 가 추가 되 었 습 니 다.
    public class ConsistentHashNodeLocator implements NodeLocator {
        private final static int VIRTUAL_NODE_SIZE = 12;
        private final static String VIRTUAL_NODE_SUFFIX = "-";
    
        private volatile TreeMap hashRing;
        private final HashAlgorithm hashAlg;
    
        public ConsistentHashNodeLocator(List nodes, HashAlgorithm hashAlg) {
            this.hashAlg = hashAlg;
            this.hashRing = buildConsistentHashRing(hashAlg, nodes);
        }
    
    
        @Override
        public MemcachedNode getPrimary(String k) {
            long hash = hashAlg.hash(k);
            return getNodeForKey(hashRing, hash);
        }
    
        private MemcachedNode getNodeForKey(TreeMap hashRing, long hash) {
            /*        key */
            Map.Entry locatedNode = hashRing.ceilingEntry(hash);
            /*        ,          */
            if (locatedNode == null) {
                locatedNode = hashRing.firstEntry();
            }
            return locatedNode.getValue();
        }
    
        private TreeMap buildConsistentHashRing(HashAlgorithm hashAlgorithm, List nodes) {
            TreeMap virtualNodeRing = new TreeMap<>();
            for (MemcachedNode node : nodes) {
                for (int i = 0; i < VIRTUAL_NODE_SIZE; i++) {
                    //               ,                     
                    virtualNodeRing.put(hashAlgorithm.hash(node.getSocketAddress().toString() + VIRTUAL_NODE_SUFFIX + i), node);
                }
            }
            return virtualNodeRing;
        }
    }
    getPrimary 함수 에서 먼저 HashAlgorithm 키 값 에 대응 하 는 해시 값 을 계산 한 다음 getNodeForKey 함수 로 TreeMap 에 대응 하 는 최근 서버 노드 인 스 턴 스 를 가 져 옵 니 다.HashAlgorithm 은 해시 알고리즘 에 대한 추상 적 이 고 일치 성 해시 알고리즘 은 CRC, MurmurHash 와 FNV 등 여러 가지 일반적인 해시 알고리즘 을 사용 할 수 있다.다음은 각종 해시 알고리즘 이 이 실현 에 가 져 온 성능 차이 점 을 비교 해 보 겠 습 니 다.
    성능 테스트
    테스트 데 이 터 는 알고리즘 의 좋 고 나 쁨 을 평가 하 는 가장 진실 하고 효과 적 인 방법 으로 양 적 사고방식 은 반드시 있어 야 한다. 이것 도 프로그래머 가 진급 하 는 보물 중의 하나 이다.우 리 는 서로 다른 해시 함 수 를 바탕 으로 하 는 일치 성 해시 알고리즘 을 다음 네 가지 계량 화 된 기준 으로 평가 합 니 다.
  • 각 서버 노드 에 저 장 된 캐 시 수량 을 통계 하고 분산 과 표준 차 이 를 계산한다.캐 시 분포 의 균일 한 상황 을 측정 하면 우 리 는 50000 개의 캐 시 데 이 터 를 모 의 하여 100 개의 서버 에 배분 하고 마지막 노드 에 캐 시 데 이 터 를 저장 하 는 분산 과 표준 차 이 를 테스트 할 수 있 습 니 다.
  • 랜 덤 으로 10% 의 서버 를 오프라인 하고 캐 시 를 재배 치 하 며 캐 시 이전 비율 을 통계 합 니 다.노드 상하 선의 상황 을 측정 하면 우 리 는 50000 개의 캐 시 데 이 터 를 모 의하여 100 개의 지 정 된 서버 에 분배 한 다음 에 무 작위 로 10 개의 서버 를 하선 하고 이 50000 개의 데 이 터 를 재분배 할 수 있다. 캐 시가 서로 다른 서버 에 분배 되 는 비율, 즉 이전 비율 을 통계 할 수 있다.
  • JMH 를 사용 하여 서로 다른 해시 알고리즘 의 집행 효율 을 비교한다.

  • 구체 적 인 평가 알고리즘 은 다음 과 같다.
    public class NodeLocatorTest {
    
        /**
         *          
         */
        @Test
        public void testDistribution() {
            List servers = new ArrayList<>();
            for (String ip : ips) {
                servers.add(new MemcachedNode(new InetSocketAddress(ip, 8080)));
            }
            //      DefaultHashAlgorithm    ,       
            NodeLocator nodeLocator = new ConsistentHashNodeLocator(servers, DefaultHashAlgorithm.NATIVE_HASH);
            //    50000     
            List keys = new ArrayList<>();
            for (int i = 0; i < 50000; i++) {
                keys.add(UUID.randomUUID().toString());
            }
            //     
            AtomicLongMap atomicLongMap = AtomicLongMap.create();
            for (MemcachedNode server : servers) {
                atomicLongMap.put(server, 0);
            }
            for (String key : keys) {
                MemcachedNode node = nodeLocator.getPrimary(key);
                atomicLongMap.getAndIncrement(node);
            }
            System.out.println(StatisticsUtil.variance(atomicLongMap.asMap().values().toArray(new Long[]{})));
            System.out.println(StatisticsUtil.standardDeviation(atomicLongMap.asMap().values().toArray(new Long[]{})));
    
        }
    
        /**
         *               
         */
        @Test
        public void testNodeAddAndRemove() {
            List servers = new ArrayList<>();
            for (String ip : ips) {
                servers.add(new MemcachedNode(new InetSocketAddress(ip, 8080)));
            }
            //    10    ,  shuffle,    0 90,        。
            Collections.shuffle(servers);
            List serverChanged = servers.subList(0, 90);
            NodeLocator loadBalance = new ConsistentHashNodeLocator(servers, DefaultHashAlgorithm.NATIVE_HASH);
            NodeLocator changedLoadBalance = new ConsistentHashNodeLocator(serverChanged, DefaultHashAlgorithm.NATIVE_HASH);
    
            //    50000     
            List keys = new ArrayList<>();
            for (int i = 0; i < 50000; i++) {
                keys.add(UUID.randomUUID().toString());
            }
            int count = 0;
            for (String invocation : keys) {
                MemcachedNode origin = loadBalance.getPrimary(invocation);
                MemcachedNode changed = changedLoadBalance.getPrimary(invocation);
               //          
                if (!origin.getSocketAddress().equals(changed.getSocketAddress())) count++;
            }
            System.out.println(count / 50000D);
        }
        static String[] ips = {...};
    }

    JMH 의 테스트 스 크 립 트 는 다음 과 같다.
    @BenchmarkMode(Mode.AverageTime)
    @OutputTimeUnit(TimeUnit.MICROSECONDS)
    @State(Scope.Thread)
    public class JMHBenchmark {
    
        private NodeLocator nodeLocator;
        private List keys;
    
        @Benchmark
        public void test() {
            for (String key : keys) {
                MemcachedNode node = nodeLocator.getPrimary(key);
            }
        }
    
        public static void main(String[] args) throws RunnerException {
            Options opt = new OptionsBuilder()
                    .include(JMHBenchmark.class.getSimpleName())
                    .forks(1)
                    .warmupIterations(5)
                    .measurementIterations(5)
                    .build();
            new Runner(opt).run();
        }
    
        @Setup
        public void prepare() {
            List servers = new ArrayList<>();
            for (String ip : ips) {
                servers.add(new MemcachedNode(new InetSocketAddress(ip, 8080)));
            }
            nodeLocator = new ConsistentHashNodeLocator(servers, DefaultHashAlgorithm.MURMUR_HASH);
            //    50000     
            keys = new ArrayList<>();
            for (int i = 0; i < 50000; i++) {
                keys.add(UUID.randomUUID().toString());
            }
        }
    
        @TearDown
        public void shutdown() {
        }
        static String[] ips = {...};
    }
    

    각각 JDK 해시 알고리즘, FNV 132 알고리즘, CRC 알고리즘, MurmurHash 알고리즘 과 Ketama 알고리즘 을 테스트 하여 각각 DefaultHashAlgorithmNATIVE_HASH, FNV1_32_HASH, CRC_HASH, MURMUR_HASHKETAMA_HASH 에 대응 했다.구체 적 인 데 이 터 는 다음 과 같다.
    가상 슬롯 파 티 션
    레 디 스 클 러 스 터 는 일치 성 해시 알고리즘 을 사용 하지 않 고 가상 슬롯 파 티 션 알고리즘 을 사용 했다 는 글 도 있다.그러나 외부 네트워크 (주 소 는 문 말 참조) 에 서 는 Redis 가 사용 하 는 가상 슬롯 구역 은 일치 성 해시 알고리즘 의 변종 일 뿐 가상 슬롯 은 Redis 동적 확장 을 허용 할 수 있다 고 말 했다.
    레 디 스 의 소스 코드 를 알 아 봐 야 이 문제 에 대해 정확 한 대답 을 할 수 있 을 지도 모른다.알 고 계 신 분 들 은 적극적으로 댓 글로 답 해 주세요. 감사합니다.
    github 주소:https://github.com/ztelur/con...redis분산 토론 의 주소:https://www.reddit.com/r/redi...
    레 퍼 런 스
  • https://jistol.github.io/soft...
  • https://mp.weixin.qq.com/s/oe...
  • 좋은 웹페이지 즐겨찾기