Красивый последовательный алгоритм хеширования

задняя часть

Если в крупномасштабной системе с высокой степенью параллелизма требуется распределенное хранилище данных, и ожидается, что данные будут равномерно распределены и обладают сильной масштабируемостью, то алгоритм согласованного хэширования может идеально решить эту проблему. Применение согласованного алгоритма хеширования во многих областяхКэширование распределенной базы данных Hadoop ES

Принцип последовательного хэш-алгоритма

Алгоритм согласованного хеширования использует метод по модулю, а алгоритм согласованного хеширования использует модуль от 2 до 32. То есть последовательный алгоритм хеширования организует все хеш-пространство в виртуальное кольцо.Пространство значений хэш-функции равно 0 ~ 2^32 - 1 (32-битное целое число без знака).Все хеш-кольцо выглядит следующим образом: Значение хеш-функции является целым и неотрицательным.Для определенного атрибута кластера, такого как имя узла, берем значение хеш-функции и помещаем его в кольцо, а значение хэш-функции берем для ключа данных и помещаем в кольцо. Найдите ближайший узел по часовой стрелке и наденьте на него. Все кольцо организовано по часовой стрелке, причем первая точка справа от точки 0 представляет сервер n1 и так далее. Мы используем Hash для выполнения хеширования на каждом сервере. В частности, IP-адрес или имя хоста сервера можно выбрать в качестве ключа для хеширования, чтобы каждый сервер определялся как находящийся в кольце хеширования. Например, мы иметь три машины, используя расположение IP-адреса в кольцевом пространстве после хэширования показано на рисунке:

图片.png
Мы используем следующий алгоритм для обнаружения доступа к данным к соответствующему серверу:

Используйте ту же функцию Hash, чтобы вычислить хеш-значение ключа данных и определить положение данных в кольце.От этой позиции выполните поиск по кольцу по часовой стрелке, и обнаруженный сервер — это сервер, который он должен найти.

Как показано на рисунке ниже, после хеширования трех данных O1, O2 и O3 их положение в кольцевом пространстве следующее: O1-->n1 O2-->n2 O3-->n3

图片.png

Отказоустойчивость и масштабируемость последовательных алгоритмов хеширования

Теперь, предполагая, что наш n3 не работает, мы можем видеть из рисунка, что n1, n2 не затрагиваются, только объект O3 перемещается в n1. Итак, мы обнаружили, что в алгоритме согласованного хеширования, если сервер недоступен, затрагиваемые данные — это только данные между этим сервером и предыдущим сервером в его кольцевом пространстве, а другие не будут затронуты. как показано на рисунке:

图片.png

Теперь в нашу систему добавлен сервер n4, как показано на рисунке

图片.png
Из рисунка видно, что после добавления сервера данные O2 и O3 не затрагиваются, затрагивается только O1, который перемещается на новый узел n4.

Алгоритму согласованного хеширования требуется лишь переместить небольшую часть данных в кольцевом пространстве для увеличения или уменьшения количества узлов, и он обладает хорошей отказоустойчивостью и масштабируемостью. В случае слишком малого количества служебных узлов последовательного алгоритма хеширования легко вызвать перекос данных (большая часть кэшируемых объектов кэшируется на определенном сервере) из-за неравномерного распределения узлов, как показано на рисунке:

图片.png

В это время мы обнаружили, что большой объем данных сосредоточен на узле A, в то время как узел B имеет лишь небольшой объем данных. Чтобы решить проблему перекоса данных, алгоритм последовательного хеширования вводит механизм виртуального узла, то есть для каждого узла сервера вычисляется несколько хэшей, а в каждую позицию результата вычисления помещается узел обслуживания, который называется виртуальным узлом. . Конкретную операцию можно реализовать, добавив число после IP-адреса сервера или имени хоста, как показано на рисунке: Например, для узла n1 мы виртуализируем 100 виртуальных узлов, а те, что находятся в кольце, являются виртуальными узлами Аналогично, n2 и n3 также имеют 100 виртуальных узлов.

图片.png

服务器对应多个虚拟节点

Когда данные приходят, как судить, на какой сервер их ставить?

Когда данные поступают в кольцо, сначала найдите соответствующий виртуальный узел, а затем найдите соответствующий сервер через виртуальный узел, чтобы данные можно было равномерно распределить путем добавления виртуальных узлов.

Алгоритм позиционирования данных остается неизменным, нужно добавить только один шаг: сопоставление виртуальных узлов с реальными точками. Поэтому после добавления виртуальных узлов, даже если сервисных узлов немного, данные можно распределить равномерно. Вышеупомянутые ситуации все равномерно распределены в идеальных условиях, На самом деле существует согласованный алгоритм хеширования.проблема искажения данных

Класс интерфейса алгоритма

public interface IHashService {
    Long hash(String key);
}

Класс реализации интерфейса алгоритма

public class HashService implements IHashService {

    /**
     * MurMurHash算法,性能高,碰撞率低
     *
     * @param key String
     * @return Long
     */
    public Long hash(String key) {
        ByteBuffer buf = ByteBuffer.wrap(key.getBytes());
        int seed = 0x1234ABCD;

        ByteOrder byteOrder = buf.order();
        buf.order(ByteOrder.LITTLE_ENDIAN);

        long m = 0xc6a4a7935bd1e995L;
        int r = 47;

        long h = seed ^ (buf.remaining() * m);

        long k;
        while (buf.remaining() >= 8) {
            k = buf.getLong();

            k *= m;
            k ^= k >>> r;
            k *= m;

            h ^= k;
            h *= m;
        }

        if (buf.remaining() > 0) {
            ByteBuffer finish = ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN);
            finish.put(buf).rewind();
            h ^= finish.getLong();
            h *= m;
        }

        h ^= h >>> r;
        h *= m;
        h ^= h >>> r;

        buf.order(byteOrder);
        return h;

    }
}

Имитация машинного узла

public class Node<T> {
    private String ip;
    private String name;

    public Node(String ip, String name) {
        this.ip = ip;
        this.name = name;
    }

    public String getIp() {
        return ip;
    }

    public void setIp(String ip) {
        this.ip = ip;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    /**
     * 使用IP当做hash的Key
     *
     * @return String
     */
    @Override
    public String toString() {
        return ip;
    }
}

Согласованная хэш-операция

public class ConsistentHash<T> {
    // Hash函数接口
    private final IHashService iHashService;
    // 每个机器节点关联的虚拟节点数量
    private final int          numberOfReplicas;
    // 环形虚拟节点
    private final SortedMap<Long, T> circle = new TreeMap<Long, T>();

    public ConsistentHash(IHashService iHashService, int numberOfReplicas, Collection<T> nodes) {
        this.iHashService = iHashService;
        this.numberOfReplicas = numberOfReplicas;
        for (T node : nodes) {
            add(node);
        }
    }

    /**
     * 增加真实机器节点
     *
     * @param node T
     */
    public void add(T node) {
        for (int i = 0; i < this.numberOfReplicas; i++) {
            circle.put(this.iHashService.hash(node.toString() + i), node);
        }
    }

    /**
     * 删除真实机器节点
     *
     * @param node T
     */
    public void remove(T node) {
        for (int i = 0; i < this.numberOfReplicas; i++) {
            circle.remove(this.iHashService.hash(node.toString() + i));
        }
    }

    public T get(String key) {
        if (circle.isEmpty()) return null;

        long hash = iHashService.hash(key);

        // 沿环的顺时针找到一个虚拟节点
        if (!circle.containsKey(hash)) {
            SortedMap<Long, T> tailMap = circle.tailMap(hash);
            hash = tailMap.isEmpty() ? circle.firstKey() : tailMap.firstKey();
        }
        return circle.get(hash);
    }
}

тестовый класс


public class TestHashCircle {
    // 机器节点IP前缀
    private static final String IP_PREFIX = "192.168.0.";

    public static void main(String[] args) {
        // 每台真实机器节点上保存的记录条数
        Map<String, Integer> map = new HashMap<String, Integer>();

        // 真实机器节点, 模拟10台
        List<Node<String>> nodes = new ArrayList<Node<String>>();
        for (int i = 1; i <= 10; i++) {
            map.put(IP_PREFIX + i, 0); // 初始化记录
            Node<String> node = new Node<String>(IP_PREFIX + i, "node" + i);
            nodes.add(node);
        }

        IHashService iHashService = new HashService();
        // 每台真实机器引入100个虚拟节点
        ConsistentHash<Node<String>> consistentHash = new ConsistentHash<Node<String>>(iHashService, 500, nodes);

        // 将5000条记录尽可能均匀的存储到10台机器节点上
        for (int i = 0; i < 5000; i++) {
            // 产生随机一个字符串当做一条记录,可以是其它更复杂的业务对象,比如随机字符串相当于对象的业务唯一标识
            String data = UUID.randomUUID().toString() + i;
            // 通过记录找到真实机器节点
            Node<String> node = consistentHash.get(data);
            // 再这里可以能过其它工具将记录存储真实机器节点上,比如MemoryCache等
            // ...
            // 每台真实机器节点上保存的记录条数加1
            map.put(node.getIp(), map.get(node.getIp()) + 1);
        }

        // 打印每台真实机器节点保存的记录条数
        for (int i = 1; i <= 10; i++) {
            System.out.println(IP_PREFIX + i + "节点记录条数:" + map.get(IP_PREFIX + i));
        }
    }
}

 

Результаты приведены ниже:

图片.png