In distributed RPC systems, client-side load balancing ensures predictable request distribution while maintaining stability during topology changes. Apache Dubbo’s ConsistentHashLoadBalance is specifically designed for scenarios where request affinity—routing identical requests to the same provider—is critical. Unlike round-robin or random strategies, it minimizes redistribution impact when providers join or leave the cluster.
Why Consistent Hashing Matters
Naive modulo-based hashing suffers from high churn: adding or removing one node can remap up to 90% of all requests. Consistent hashing reduces this to roughly 1/N (where N is the number of nodes), making it ideal for stateful workloads like session stickiness, local cache utilization, or sharded data access.
Core Mechanism
Dubbo implements consistent hashing using a sorted ring structure backed by a TreeMap<Long, Invoker>, where keys are hash values and values are service endpoints. Two key abstractions enable robustness:
Virtual Node Distribution
Each physical provider (e.g., host:port) is expanded into multiple virtual replicas on the ring. This mitigates skew caused by uneven real-node hashing. The default count is 160, configurable via hash.nodes. Internally, Dubbo computes four distinct hashes per replica index using MD5 and bit-shifting:
private void buildVirtualNodeRing(List<Invoker<T>> invokers) {
TreeMap<Long, Invoker<T>> ring = new TreeMap<>();
int replicas = getUrl().getParameter("hash.nodes", 160);
for (Invoker<T> invoker : invokers) {
String address = invoker.getUrl().getAddress();
for (int i = 0; i < replicas; i++) {
byte[] digest = md5(address + ":" + i);
for (int h = 0; h < 4; h++) {
long hashValue = hash(digest, h);
ring.put(hashValue, invoker);
}
}
}
this.virtualRing = Collections.unmodifiableSortedMap(ring);
}
Request Key Generation
The routing key is derived from method arguments—not the entire invocation. By default, only the first argument is used (hash.arguments=0), but comma-separated indices (e.g., "0,2") allow composite keys. Dubbo normalizes inputs before hashing:
private String extractKey(Object[] args) {
StringBuilder keyBuilder = new StringBuilder();
int[] indices = parseArgumentIndices(); // e.g., [0, 2]
for (int idx : indices) {
if (args != null && idx >= 0 && idx < args.length) {
Object arg = args[idx];
keyBuilder.append(arg == null ? "null" : arg.toString());
}
}
return keyBuilder.toString();
}
Routing Logic
Given a computed request hash h, Dubbo performs a ceiling lookup in the sorted ring. If no entry ≥ h exists, it wraps around to the smallest key:
public Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) {
long requestHash = hash(extractKey(invocation.getArguments()));
Map.Entry<Long, Invoker<T>> candidate = virtualRing.ceilingEntry(requestHash);
if (candidate == null) {
candidate = virtualRing.firstEntry();
}
return candidate.getValue();
}
Configuration Options
| Parameter | Purpose | Default | Scope |
|---|---|---|---|
hash.nodes |
Number of virtual copies per provider | 160 | Provider or consumer URL |
hash.arguments |
Zero-based argument indices for key derivation | "0" |
Consumer or method-level |
Declaration Examples
XML Configuration
<!-- Consumer side: route by user ID and tenant ID -->
<dubbo:reference interface="com.example.UserService"
loadbalance="consistenthash">
<dubbo:parameter key="hash.arguments" value="0,1"/>
</dubbo:reference>
<!-- Provider side: increase virtual nodes for better balance -->
<dubbo:service interface="com.example.UserService"
ref="userServiceImpl"
loadbalance="consistenthash">
<dubbo:parameter key="hash.nodes" value="240"/>
</dubbo:service>
Spring Boot Annotations
@DubboService(loadBalance = "consistenthash", parameters = {
@Parameter(key = "hash.nodes", value = "240")
})
public class UserServiceImpl implements UserService { /* ... */ }
@DubboReference(loadBalance = "consistenthash", parameters = {
@Parameter(key = "hash.arguments", value = "0,1")
})
private UserService userService;
Operational Guidance
- Affinity Stability: Choose immutable, business-meaningful arguments (e.g.,
userId,accountId). Avoid timestamps, UUIDs, or transient values. - Scale Tuning: For clusters under 10 providers, raise
hash.nodesto 200–300. Above 50 nodes, 160 usually suffices. - Fallback Safety: Combine with
Routerrules (e.g., tag-based routing) to exclude unhealthy instances before hashing. - Monitoring: Track per-provider invocation counts via Dubbo Admin or metrics exporters to detect imbalance early.