In the realm of distributed systems, efficient data distribution is crucial for optimizing performance and ensuring scalability. One fundamental approach to distributing data across multiple servers is through hashing. This blog post will explore a straightforward method for distributing string keys across a distributed cache using simple hashing, discuss the problems with simple hashing, and how to resolve these problems with Consistent Hashing.
Simple Hashing to distribute the data
Assume, we are going to build a distributed cache application, where we have keys of type String. When dealing with string keys, the hash function needs to convert the string into a numerical value. We then use the modulo operation to determine the server where the data should be stored. The basic formula is:
getServer(key)=hashFunction(key)%n
Here,
a. key is the string to be hashed, and
b. n is the number of servers.
getServer(key) method return the server to store this key.
public int getServer(String key) {
int hashValue = hash(key);
return Math.abs(hashValue) % noOfServers;
}
Following diagram demonstrate the same.
When a key needs to be added to a server, its server location is determined using the getServer(key) method.
The distribution of keys is as follows:
· KEY_0 is stored on Server_3
· KEY_1 is stored on Server_3
· KEY_2 is stored on Server_1
· KEY_3 is stored on Server_2
· KEY_4 is stored on Server_4
· KEY_5 is stored on Server_3
· KEY_6 is stored on Server_3
· KEY_7 is stored on Server_0
· KEY_8 is stored on Server_4
· KEY_9 is stored on Server_3
· KEY_10 is stored on Server_1
· KEY_11 is stored on Server_0
· KEY_12 is stored on Server_3
· KEY_13 is stored on Server_4
· KEY_14 is stored on Server_1
· KEY_15 is stored on Server_0
· KEY_16 is stored on Server_2
· KEY_17 is stored on Server_2
· KEY_18 is stored on Server_3
· KEY_19 is stored on Server_0
Distribution Statistics
· Server_0 contains 4 keys
· Server_1 contains 3 keys
· Server_2 contains 3 keys
· Server_3 contains 7 keys
· Server_4 contains 3 keys
Demo application for the same.
SimpleHashing.java
package com.sample.app.hashing;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class SimpleHashing {
private static MessageDigest MESSAGE_DIGEST = null;
static {
try {
MESSAGE_DIGEST = MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
}
private int noOfServers;
public SimpleHashing(int noOfServers) {
this.noOfServers = noOfServers;
}
private int hash(String key) {
byte[] hashBytes = MESSAGE_DIGEST.digest(key.getBytes(StandardCharsets.UTF_8));
BigInteger hashValue = new BigInteger(1, hashBytes);
return hashValue.intValue();
}
public int getServer(String key) {
int hashValue = hash(key);
return Math.abs(hashValue) % noOfServers;
}
public void printKeysDistrribution(List<String> keys) {
Map<Integer, Integer> stats = new HashMap<>();
for (String key : keys) {
int serverIndex = getServer(key);
stats.putIfAbsent(serverIndex, 0);
int count = stats.get(serverIndex);
stats.put(serverIndex, count + 1);
System.out.println(key + " is stored in Server_" + serverIndex);
}
System.out.println("\nDistribution statistics");
for (int i = 0; i < noOfServers; i++) {
System.out.println("Server_" + i + " contain " + stats.get(i) + " keys");
}
}
}
SimpleHashingDemo.java
package com.sample.app.hashing;
import java.util.ArrayList;
import java.util.List;
public class SimpleHashingDemo {
public static void main(String args[]) {
List<String> keys = new ArrayList<>();
String keyPrefix = "KEY_";
int noOfKeys = 20;
int noOfServers = 5;
for(int i=0; i < noOfKeys; i++) {
keys.add(keyPrefix+i);
}
SimpleHashing simpleHashing = new SimpleHashing(noOfServers);
simpleHashing.printKeysDistrribution(keys);
}
}
Output
KEY_0 is stored in Server_3 KEY_1 is stored in Server_3 KEY_2 is stored in Server_1 KEY_3 is stored in Server_2 KEY_4 is stored in Server_4 KEY_5 is stored in Server_3 KEY_6 is stored in Server_3 KEY_7 is stored in Server_0 KEY_8 is stored in Server_4 KEY_9 is stored in Server_3 KEY_10 is stored in Server_1 KEY_11 is stored in Server_0 KEY_12 is stored in Server_3 KEY_13 is stored in Server_4 KEY_14 is stored in Server_1 KEY_15 is stored in Server_0 KEY_16 is stored in Server_2 KEY_17 is stored in Server_2 KEY_18 is stored in Server_3 KEY_19 is stored in Server_0 Distribution statistics Server_0 contain 4 keys Server_1 contain 3 keys Server_2 contain 3 keys Server_3 contain 7 keys Server_4 contain 3 keys
Problems with Simple Hashing
While simple hashing is an effective and straightforward method for distributing data across multiple servers, it struggles with a notable drawback, particularly when handling node failures and when new nodes are added to the cluster.
Redistribution of Data on Node Failure
One significant drawback of simple hashing is the need to redistribute data when a node goes down. Here's how it works and why it becomes a problem:
Initial Distribution: Initially, data is distributed across the servers based on the hash function. For example, using hash(key) = hashFunction(key) % n, where n is the number of servers.
Node Failure: Suppose one of the servers fails. This reduces the total number of servers (n). For instance, if Server_4 goes down in a system with 5 servers, n becomes 4.
Recalculation: When a node fails, all keys need to be rehashed and redistributed according to the new number of servers. This means recalculating hash(key) % 4 for all keys.
Data Movement: As a result of recalculating the hash values, most of the data will need to be moved to different servers. This is because the hash values modulo 4 will likely differ from the original values modulo 5, causing keys to map to different servers.
The distribution of keys is as follows:
· KEY_0 is stored in Server_3
· KEY_1 is stored in Server_1
· KEY_2 is stored in Server_0
· KEY_3 is stored in Server_1
· KEY_4 is stored in Server_2
· KEY_5 is stored in Server_0
· KEY_6 is stored in Server_1
· KEY_7 is stored in Server_3
· KEY_8 is stored in Server_3
· KEY_9 is stored in Server_1
· KEY_10 is stored in Server_2
· KEY_11 is stored in Server_0
· KEY_12 is stored in Server_3
· KEY_13 is stored in Server_1
· KEY_14 is stored in Server_1
· KEY_15 is stored in Server_3
· KEY_16 is stored in Server_2
· KEY_17 is stored in Server_1
· KEY_18 is stored in Server_0
· KEY_19 is stored in Server_0
Distribution statistics
· Server_0 contain 5 keys
· Server_1 contain 7 keys
· Server_2 contain 3 keys
· Server_3 contain 5 keys
Following table summarizes the keys distribution, when the number of servers are 4 and 5.
Key |
Number of Servers 4 |
Number of Servers 5 |
KEY_0 |
Server_3 |
Server_3 |
KEY_1 |
Server_1 |
Server_3 |
KEY_2 |
Server_0 |
Server_1 |
KEY_3 |
Server_1 |
Server_2 |
KEY_4 |
Server_2 |
Server_4 |
KEY_5 |
Server_0 |
Server_3 |
KEY_6 |
Server_1 |
Server_3 |
KEY_7 |
Server_3 |
Server_0 |
KEY_8 |
Server_3 |
Server_4 |
KEY_9 |
Server_1 |
Server_3 |
KEY_10 |
Server_2 |
Server_1 |
KEY_11 |
Server_0 |
Server_0 |
KEY_12 |
Server_3 |
Server_3 |
KEY_13 |
Server_1 |
Server_4 |
KEY_14 |
Server_1 |
Server_1 |
KEY_15 |
Server_3 |
Server_0 |
KEY_16 |
Server_2 |
Server_2 |
KEY_17 |
Server_1 |
Server_2 |
KEY_18 |
Server_0 |
Server_3 |
KEY_19 |
Server_0 |
Server_0 |
Same is the case when we add new servers to existing clusters. This problem is addressed with consistent Hashing.
Consistent Hashing
As per Wikipedia, In computer science, consistent hashing is a special kind of hashing technique such that when a hash table is resized, only 𝑛/𝑚 keys need to be remapped on average where
a. 𝑛 is the number of keys and
b. 𝑚 is the number of slots.
In contrast, in most traditional hash tables, a change in the number of array slots causes nearly all keys to be remapped because the mapping between the keys and the slots is defined by a modular operation.
Let me explain this with an example.
Suppose you have 4 servers (Server_1, Server_2, Server_3, and Server_4) and you want to distribute all the keys among these four servers.
Step 1: Calculate the hash codes for all these servers. The hash code is generated using the server name, IP address, or any other unique identifier for each server.
Following table summarizes the hashCodes.
Server |
HashCode |
Server_1 |
-24244935 |
Server_2 |
813726879 |
Server_3 |
-937748046 |
Server_4 |
397230681 |
Let’s arrange these hash codes in a circle shape.
Arrange Hash Codes in Ascending Order
-937748046 (Server_3)
-24244935 (Server_1)
397230681 (Server_4)
813726879 (Server_2)
The arrangement in the circular ring.
Server_3 (-937748046) --> Server_1 (-24244935) --> Server_4 (397230681) --> Server_2 (813726879)
Suppose we want to distribute keys KEY_1 to KEY_9 among the servers using consistent hashing. We must compute the hash codes for these keys and compare them with the hash codes of our servers to determine the appropriate server for each key.
Following table summarizes the hashcodes of the keys.
Key |
HashCode |
KEY_0 |
145963125 |
KEY_1 |
680568006 |
KEY_2 |
560725153 |
KEY_3 |
-2048590292 |
KEY_4 |
481448715 |
KEY_5 |
-707126076 |
KEY_6 |
634733290 |
KEY_7 |
-684746024 |
KEY_8 |
-976564317 |
KEY_9 |
158285436 |
Let's find the server each key will be assigned to:
KEY_0 (145963125)
· 145963125 is greater than -24244935 (Server_1) and less than 397230681 (Server_4).
· So, KEY_0 is assigned to Server_4.
KEY_1 (680568006)
· 680568006 is greater than 397230681 (Server_4) and less than 813726879 (Server_2).
· So, KEY_1 is assigned to Server_2.
KEY_2 (560725153)
· 560725153 is greater than 397230681 (Server_4) and less than 813726879 (Server_2).
· So, KEY_2 is assigned to Server_2.
KEY_3 (-2048590292)
· -2048590292 is less than -937748046 (Server_3), which means it should be placed before the first server, wrapping around the ring.
· So, KEY_3 is assigned to Server_3.
KEY_4 (481448715)
· 481448715 is greater than 397230681 (Server_4) and less than 813726879 (Server_2).
· So, KEY_4 is assigned to Server_2.
KEY_5 (-707126076)
· -707126076 is greater than -937748046 (Server_3) and less than -24244935 (Server_1).
· So, KEY_5 is assigned to Server_1.
KEY_6 (634733290)
· 634733290 is greater than 397230681 (Server_4) and less than 813726879 (Server_2).
· So, KEY_6 is assigned to Server_2.
KEY_7 (-684746024)
· -684746024 is greater than -937748046 (Server_3) and less than -24244935 (Server_1).
· So, KEY_7 is assigned to Server_1.
KEY_8 (-976564317)
· -976564317 is less than -937748046 (Server_3), which means it should be placed before the first server, wrapping around the ring.
· So, KEY_8 is assigned to Server_3.
KEY_9 (158285436)
· 158285436 is greater than -24244935 (Server_1) and less than 397230681 (Server_4).
· So, KEY_9 is assigned to Server_4.
Summary of Key Assignments:
Server_3:
· KEY_3 (-2048590292)
· KEY_8 (-976564317)
Server_1:
· KEY_5 (-707126076)
· KEY_7 (-684746024)
Server_4:
· KEY_0 (145963125)
· KEY_9 (158285436)
Server_2:
· KEY_1 (680568006)
· KEY_2 (560725153)
· KEY_4 (481448715)
· KEY_6 (634733290)
This arrangement ensures that each key is assigned to the nearest server in the clockwise direction on the hash ring, minimizing the amount of key movement required when servers are added or removed.
How to identify the server for given key?
To identify the server where a specific key (in this case, KEY_5) persists in a consistent hashing setup, you follow these steps:
1. Calculate the Hash Code of the Key: First, you need the hash code of the key. For KEY_5, this is given as -707126076.
2. Locate the Position in the Hash Ring: Next, you find the position of this hash code in the sorted order of the server hash codes on the hash ring.
3. Find the Nearest Server Clockwise: Starting from the calculated hash code of the key, move clockwise on the ring to find the first server with a hash code greater than or equal to the key’s hash code. If no such server exists, wrap around to the first server in the sorted order.
Steps to Identify the Server for KEY_5:
1. Arrange Server Hash Codes in Ascending Order:
· -937748046 (Server_3)
· -24244935 (Server_1)
· 397230681 (Server_4)
· 813726879 (Server_2)
2. Determine Position of KEY_5 Hash Code:
· The hash code for KEY_5 is -707126076.
· Locate the position of -707126076 in the sorted list of server hash codes.
3. Find the Nearest Server Clockwise:
· -707126076 is greater than -937748046 (Server_3) and less than -24244935 (Server_1).
· Thus, the nearest server in the clockwise direction is Server_1, with a hash code of -24244935.
Therefore, KEY_5 with hash code -707126076 will persist in Server_1.
Find the below working application.
ConsistentHashing.java
package com.sample.app.hashing;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.SortedMap;
import java.util.TreeMap;
public class ConsistentHashing {
private final SortedMap<Integer, String> circle = new TreeMap<>();
public ConsistentHashing(String[] servers) {
for (String server : servers) {
addServer(server);
}
}
private int hash(String key) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] hashBytes = md.digest(key.getBytes(StandardCharsets.UTF_8));
return new String(hashBytes, StandardCharsets.UTF_8).hashCode();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
private void addServer(String server) {
circle.put(hash(server), server);
}
public void printServer(String key) {
if (circle.isEmpty()) {
return;
}
int hash = hash(key);
int keyHash = hash;
if (!circle.containsKey(hash)) {
SortedMap<Integer, String> tailMap = circle.tailMap(hash);
hash = tailMap.isEmpty() ? circle.firstKey() : tailMap.firstKey();
}
String server = circle.get(hash);
System.out.println("Key " + key + " and hashCode : " + keyHash + " is assigned to " + server);
}
public void printRing() {
for(Integer key: circle.keySet()) {
System.out.println(key + " " + circle.get(key));
}
}
}
ConsistentHashingDemo.java
package com.sample.app.hashing;
public class ConsistentHashingDemo {
public static void main(String[] args) {
String[] servers = { "Server_1", "Server_2", "Server_3", "Server_4" };
ConsistentHashing consistentHashing = new ConsistentHashing(servers);
consistentHashing.printRing();
System.out.println();
String keyPrefix = "KEY_";
for (int i = 0; i < 10; i++) {
consistentHashing.printServer(keyPrefix + i);
}
}
}
Output
-937748046 Server_3 -24244935 Server_1 397230681 Server_4 813726879 Server_2 Key KEY_0 and hashCode : 145963125 is assigned to Server_4 Key KEY_1 and hashCode : 680568006 is assigned to Server_2 Key KEY_2 and hashCode : 560725153 is assigned to Server_2 Key KEY_3 and hashCode : -2048590292 is assigned to Server_3 Key KEY_4 and hashCode : 481448715 is assigned to Server_2 Key KEY_5 and hashCode : -707126076 is assigned to Server_1 Key KEY_6 and hashCode : 634733290 is assigned to Server_2 Key KEY_7 and hashCode : -684746024 is assigned to Server_1 Key KEY_8 and hashCode : -976564317 is assigned to Server_3 Key KEY_9 and hashCode : 158285436 is assigned to Server_4
Advantage of Consistent Hashing When a Server Goes Down
Consistent hashing provides significant advantages in distributed systems, particularly when servers are dynamically added or removed. Let's discuss the specific scenario where Server_1 goes down and how consistent hashing handles this situation effectively:
From our previous assignment, we know, Server_1: Stores KEY_5 and KEY_7.
Consistent Hashing Behaviour when Server_1 goes down
When Server_1 goes down, consistent hashing ensures that only a minimal number of keys need to be reassigned to the remaining servers. This minimizes disruption and the need for data transfer, unlike other hashing schemes like simple modulo-based hashing where all keys might need to be redistributed.
1. Redistribution Process: Identify Keys on Server_1: Identify the keys that were assigned to Server_1, which are KEY_5 and KEY_7.
2. Reassign Affected Keys: Reassign these keys to the next available server in the clockwise direction.
Key Reassignment
KEY_5 (-707126076)
· Previously mapped to Server_1 (-24244935)
· Now, find the next server in the clockwise direction:
· Nearest server hash code greater than -707126076: Server_4 (397230681)
· Reassigned to Server_4
KEY_7 (-684746024)
· Previously mapped to Server_1 (-24244935)
· Now, find the next server in the clockwise direction:
· Nearest server hash code greater than -684746024: Server_4 (397230681)
· Reassigned to Server_4
Updated Key Distribution
After Server_1 goes down and the affected keys are reassigned, the new distribution is as follows:
Server_3:
· KEY_3 (-2048590292)
· KEY_8 (-976564317)
Server_4:
· KEY_0 (145963125)
· KEY_5 (-707126076) (Reassigned)
· KEY_7 (-684746024) (Reassigned)
· KEY_9 (158285436)
Server_2:
· KEY_1 (680568006)
· KEY_2 (560725153)
· KEY_4 (481448715)
· KEY_6 (634733290)
Advantage of Consistent Hashing When a new server added
When a new server, "New_Server" with hash code -53574582, is added to the consistent hashing ring, consistent hashing ensures that only a minimal number of keys need to be reassigned. This minimizes disruption and ensures an even distribution of keys across the servers.
Given Servers and Hash Codes:
· Server_3: -937748046
· Server_1: -24244935
· Server_4: 397230681
· Server_2: 813726879
New Server Added:
· New_Server: -53574582
Any key with a hash code between -53574582 (New_Server) and -24244935 (Server_1) will be reassigned to New_Server. As per this logic, KEY_5 (-707126076) and KEY_7 (-684746024) will be reassigned to New_Server.
New Distribution:
Server_3:
· KEY_3 (-2048590292)
· KEY_8 (-976564317)
Server_1:
· Previously had KEY_5 and KEY_7, now re-assigned to New_Server
Server_4:
· KEY_0 (145963125)
· KEY_9 (158285436)
Server_2:
· KEY_1 (680568006)
· KEY_2 (560725153)
· KEY_4 (481448715)
· KEY_6 (634733290)
New_Server:
· KEY_5 (-707126076)
· KEY_7 (-684746024)
Virtual Servers in Consistent Hashing
Virtual servers help achieve more even distribution of keys and better load balancing. Without virtual servers, the hash values might cluster around certain servers, causing them to handle a disproportionate amount of traffic. By assigning multiple virtual servers to each physical server, we can smooth out these imbalances.
If Server_2 goes down, all keys that were supposed to go to Server_2 will now go to Server_3, as it's the next server in the clockwise direction. This causes Server_3 to suddenly take on the load that was previously handled by Server_2.
Scenario with Virtual Servers
Let's assume each server is represented by three virtual servers.
Server_1
· Server_1.1
· Server_1.2
· Server_1.3
Server_2
· Server_2.1
· Server_2.2
· Server_2.3
Server_3
· Server_3.1
· Server_3.2
· Server_3.3
Server_4
· Server_4.1
· Server_4.2
· Server_4.3
Handling Large Gaps and Load Balancing:
Virtual servers ensure that the load is more evenly distributed. In this case, even if Server_2 goes down, its load is not transferred to a single server but rather distributed to multiple servers (Server_1.2, Server_3.2, and Server_4.1). This prevents any single server from becoming a bottleneck.
Benefits of Virtual Servers:
1. More Even Load Distribution: Virtual servers spread the load across multiple points on the hash ring, reducing the likelihood of any single server being overwhelmed.
2. Better Fault Tolerance: When a server goes down, the load it was handling is distributed among multiple servers, maintaining system performance and reliability.
3. Scalability: Adding or removing servers can be done smoothly with minimal disruption, as the impact is distributed across multiple virtual nodes.
In summary, virtual servers in consistent hashing help achieve a more balanced distribution of keys, improving load balancing and fault tolerance in the system. When a server goes down, the load is evenly distributed among the remaining servers, ensuring the system continues to operate efficiently.
System Design Questions
No comments:
Post a Comment