Understanding the consistent hashing algorithm in five minutes

The consistent hash algorithm was proposed in 1997 by Karger et al. of the Massachusetts Institute of Technology in solving distributed Cache. The design goal is to solve the hot spot problem in the Internet. The original intention is very similar to CARP. Consistent hashing corrects the problems caused by the simple hash algorithm used by CARP , so that DHT can be truly applied in the P2P environment.

But now the consistent hash algorithm has also been widely used in distributed systems. Anyone who has studied memcached cache databases knows that the memcached server itself does not provide the consistency of distributed cache, but is provided by the client. The following steps are used to calculate the consistency hash:

  1. First, find the hash value of the memcached server (node) and place it on the continuum of 0-2 32 .

  2. Then use the same method to find the hash value of the key that stores the data, and map it to the same circle.

  3. Then start clockwise search from the location where the data is mapped, and save the data to the first server found. If the server cannot be found after more than 2 32 , it will be saved to the first memcached server.

Add a memcached server from the state above. The remainder distributed algorithm will affect the cache hit rate due to the huge changes in the server that saves the key, but in Consistent Hashing, only the key on the first server in the counterclockwise direction where the server is added on the circle (continuum) will be affected. ,As shown below:

Consistent Hash properties

Considering that each node of a distributed system may fail, and new nodes are likely to dynamically increase in, how to ensure that the system can still provide good services to the outside world when the number of nodes in the system changes, this is worth considering, especially When designing a distributed cache system, if a certain server fails, for the entire system, if a suitable algorithm is not used to ensure consistency, all data cached in the system may become invalid (that is, due to the reduced number of system nodes, The client needs to recalculate its hash value (usually related to the number of nodes in the system) when requesting an object. Since the hash value has changed, it is likely that the server node that saves the object cannot be found), so the consistent hash is It seems very important that the consistent hash algorithm in a good distributed cahce system should meet the following aspects:

  • Balance

Balance means that the result of the hash can be distributed to all buffers as much as possible, so that all buffer space can be used. Many hash algorithms can meet this condition.

  • Monotonicity

Monotonicity means that if some content has been allocated to the corresponding buffer by hash, and a new buffer is added to the system, then the result of the hash should be able to ensure that the original allocated content can be mapped to the new one. To the buffer, and will not be mapped to other buffers in the old buffer set. Simple hash algorithms often cannot meet the monotonicity requirements, such as the simplest linear hash: x = (ax + b) mod (P), in the above formula, P represents the size of the entire buffer. It is not difficult to see that when the buffer size changes (from P1 to P2), all the original hash results will change, which does not meet the monotonicity requirement. The change of the hash result means that when the buffer space changes, all the mapping relationships need to be updated in the system. In the P2P system, the buffer change is equivalent to Peer joining or exiting the system. This situation occurs frequently in the P2P system, which will bring a huge calculation and transmission load. Monotonicity requires the hash algorithm to be able to cope with this situation.

  • Spread

In a distributed environment, the terminal may not see all the buffers, but only a part of them. When the terminal wants to map content to the buffer through the hashing process, the buffer range seen by different terminals may be different, resulting in inconsistent hashing results. The final result is that the same content is mapped to different by different terminals In the buffer. This situation should obviously be avoided, because it causes the same content to be stored in different buffers, reducing the efficiency of system storage. The definition of dispersion is the severity of the occurrence of the above situation. A good hash algorithm should be able to avoid inconsistencies as much as possible, that is, to minimize dispersion.

  • Load

The load problem is actually looking at the dispersion problem from another angle. Since different terminals may map the same content to different buffers, a specific buffer may also be mapped to different contents by different users. As with decentralization, this situation should also be avoided, so a good hash algorithm should be able to reduce the buffer load as much as possible.

  • Smoothness

Smoothness means that the smooth change of the number of cache servers and the smooth change of cache objects are consistent.

principle

basic concepts

Consistent Hashing was first proposed in the paper "Consistent Hashing and Random Trees: Distributed Caching Protocols for Relieving Hot Spots on the World Wide Web". In simple terms, consistent hashing organizes the entire hash value space into a virtual circle. For example, assume that the value space of a hash function H is 0-2^ 32 -1 (that is, the hash value is a 32-bit Symbol shaping), the entire hash space ring is as follows:

image

The entire space is organized in a clockwise direction. 0 and 2 32 -1 coincide in the direction of the zero point.

In the next step, each server uses Hash to perform a hash. Specifically, you can select the server's ip or host name as the key to hash, so that each machine can determine its position on the hash ring. The location of each server in the ring space after using the ip address hash is as follows:

Next, use the following algorithm to locate the data to access the corresponding server: use the same function Hash to calculate the hash value of the data key, and determine the position of the data on the ring, from this position to "walk" clockwise along the ring, the first encounter The reached server is the server it should locate.

For example, we have four data objects: Object A, Object B, Object C, and Object D. After hash calculation, their positions in the ring space are as follows:

 

According to the consistent hashing algorithm, data A will be assigned to Node A, B will be assigned to Node B, C will be assigned to Node C, and D will be assigned to Node D.

The following analyzes the fault tolerance and scalability of the consistent hash algorithm. Now suppose that Node C is down unfortunately. You can see that objects A, B, and D will not be affected at this time, only the C object is relocated to Node D. Generally, in a consistent hash algorithm, if a server is unavailable, the affected data is only the server to the previous server in its ring space (that is, the first server encountered when walking in the counterclockwise direction). Data between servers), others will not be affected.

Consider another situation below. If you add a server Node X to the system, as shown in the following figure:

image

At this time, Object A, B, and D are not affected, only object C needs to be relocated to the new Node X. Generally, in the consistent hashing algorithm, if one server is added, the affected data is only the new server to the previous server in its ring space (that is, the first server encountered when walking in a counterclockwise direction) ), other data will not be affected.

To sum up, the consistent hash algorithm only needs to relocate a small part of the data in the ring space for the increase or decrease of nodes, which has good fault tolerance and scalability.

In addition, when there are too few service nodes, the consistent hash algorithm is likely to cause data skew due to uneven node distribution. For example, there are only two servers in the system, and the ring distribution is as follows,

image

At this time, a large amount of data will inevitably be concentrated on Node A, and only a very small amount will be located on Node B. In order to solve this data skew problem, the consistent hash algorithm introduces a virtual node mechanism, that is, multiple hashes are calculated for each service node, and a service node is placed at each calculation result location, which is called a virtual node. The specific method can be realized by adding a number after the server ip or host name. For example, in the above case, three virtual nodes can be calculated for each server, so "Node A#1", "Node A#2", "Node A#3", "Node B#1", and "Node A#1" can be calculated separately. The hash values ​​of B#2" and "Node B#3" form six virtual nodes:

At the same time, the data positioning algorithm remains unchanged, but there is an extra step of mapping the virtual node to the actual node. For example, the data located to the three virtual nodes of "Node A#1", "Node A#2" and "Node A#3" are all located Go to Node A. This solves the problem of data skew when there are few service nodes. In practical applications, the number of virtual nodes is usually set to 32 or even greater, so even a few service nodes can achieve relatively uniform data distribution.

JAVA code implementation

package org.java.base.hash; 
import java.util.Collection; 
import java.util.HashSet; 
import java.util.Iterator; 
import java.util.Set; 
import java.util.SortedMap; 
import java.util.SortedSet ; 
import java.util.TreeMap; 
import java.util.TreeSet; 

public class ConsistentHash<T> { 
 private final int numberOfReplicas; // The replication factor of the node, the number of actual nodes * numberOfReplicas = 
 // The number of virtual nodes 
 private final SortedMap <Integer, T> circle = new TreeMap<Integer, T>();// stores the mapping from the virtual node's hash value to the real node 

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

 public void add(T node) { 
 for (int i = 0; i <numberOfReplicas; i++){ 
 // For an actual machine node node, corresponding to numberOfReplicas virtual nodes 
 /* 
 * Different virtual nodes (i different) have different hash value, but they all correspond to the same actual machine node 
 * Virtual nodes are generally evenly distributed on the ring, and the data is stored on the clockwise virtual node 
 */ 
 String nodestr = node.toString() + i; 
 int hashcode = nodestr .hashCode(); 
 System.out.println("hashcode:"+hashcode); 
 circle.put(hashcode, node); 
 
 } 
 } 

 public void remove(T node) { 
 for (int i = 0; i <numberOfReplicas; i++ ) 
 circle.remove((node.toString() + i).hashCode()); 
 } 

 /* 
 * Get the nearest clockwise node, take Hash 
 * according to the given key, and then get the nearest clockwise one The actual node corresponding to the virtual node  
 * then obtain the data from the actual node
 */
 public T get(Object key) { 
 if (circle.isEmpty()) 
 return null; 
 int hash = key.hashCode();// node is represented by String to get the hashCode of node in the hash ring 
 System.out.println ("hashcode----->:"+hash); 
 if (!circle.containsKey(hash)) {//The data is mapped between the ring where the two virtual machines are located, so you need to look for the machine in a clockwise direction. 
 SortedMap< Integer, T> tailMap = circle.tailMap(hash); 
 hash = tailMap.isEmpty()? Circle.firstKey(): tailMap.firstKey(); 
 } 
 return circle.get(hash); 
 } 

 public long getSize() { 
 return circle.size(); 
 } 
 
 /* 
 * View the position of each virtual node in the entire hash ring 
 */ 
 public void testBalance(){  
 Set<Integer> sets = circle.keySet();//Get all of the TreeMap Key
 SortedSet<Integer> sortedSets = new TreeSet<Integer>(sets);//Key to be obtained Set sort
 for(Integer hashCode : sortedSets){
 System.out.println(hashCode);
 }
 
 System.out.println("----each location 's distance are follows: ----");
 /*
 * 查看相邻两个hashCode的差值
 */
 Iterator<Integer> it = sortedSets.iterator();
 Iterator<Integer> it2 = sortedSets.iterator();
 if(it2.hasNext())
 it2.next();
 long keyPre, keyAfter;
 while(it.hasNext() && it2.hasNext()){
 keyPre = it.next();
 keyAfter = it2.next();
 System.out.println(keyAfter - keyPre);
 }
 }
 
 public static void main(String[] args) {
 Set<String> nodes = new HashSet<String>();
 nodes.add("A");
 nodes.add("B");
 nodes.add("C");
 
 ConsistentHash<String> consistentHash = new ConsistentHash<String>(2, nodes);
 consistentHash.add("D");
 
 System.out.println("hash circle size: " + consistentHash.getSize());
 System.out.println("location of each node are follows: ");
 consistentHash.testBalance();
 
 String node =consistentHash.get("apple");
 System.out.println("node----------->:"+node);
 }
 
}


Guess you like

Origin blog.51cto.com/15082402/2644368