Appearance
多级分片缓存架构
1. 系统架构图
从客户端发起请求到服务器响应的整个流程如下:
App 启动时解析 DNS(可结合 Geo DNS),访问 Nginx 集群;
Nginx 集群统一处理 SSL 终止(SSL Termination),并根据
X-User-ID请求头(Header)的值进行一致性哈希负载均衡,将请求转发至微服务网关;微服务网关统一处理认证与授权,从注册中心获取所有服务实例,使用一致性哈希负载均衡,将请求转发至对应的微服务实例;
微服务先尝试从本地缓存(Caffeine)拿数据,如果没有再从 Redis 分片集群获取数据,若 Redis 也没有最终再从数据库取数据,并写入 Redis 和本地缓存。
可以看到整个链路调用过程,不论是从外部请求到微服务,还是微服务间的相互调用,都需要基于一致性哈希算法进行负载均衡。
2. 一致性哈希负载均衡算法介绍
常用的一致性哈希负载均衡算法有:
Ketama 哈希环
其核心思想是使用哈希函数将每个服务器(节点)标识哈希成一个 32 位整数,映射到哈希环上的多个虚拟点(通常 100 ~ 200 个点),些点均匀分布在
到 的环上,通过添加权重,服务器可以有更多虚拟点,从而获得更高负载份额。对于一个键,使用相同哈希函数计算其哈希值。在哈希环上顺时针找到最近的虚拟点,该点所属的服务器即为目标节点。
添加/移除节点时只影响环上新节点的邻域部分键(约 1/N,其中 N 为节点数)
Rendezvous Hashing(也称 Highest Random Weight, HRW)
是一种无环一致性哈希算法。HRW 使用均匀哈希函数(如 MurmurHash 或 Jenkins Hash)对于给定键遍历所有节点,计算每个
hash(key, node)作为 “分数” 或 “权重”,选择分数最高的节点作为主节点。添加/移除节点时,只需重新计算受影响键的分数,无需全局重映射。
| 方面 | Ketama 风格哈希环 | Rendezvous (HRW) Hashing |
|---|---|---|
| 核心结构 | 哈希环 + 虚拟节点 | 无结构,基于最高权重 |
| 存储开销 | 高(需存储虚拟点数组) | 低(仅需节点列表) |
| 查找时间 | ||
| 节点变化影响 | 最小重映射(1/N 键迁移) | 均匀重分布,无需预计算 |
| 负载均衡 | 好(虚拟节点均匀) | 优秀(天然均匀) |
| 适用场景 | 大规模动态缓存(如 Memcached) | 小型无状态负载均衡(如 Kafka) |
| 实现复杂度 | 中等(需构建环) | 低(只需哈希函数) |
3. 一致性哈希负载均衡实现
3.1. Nginx 集群到 Gateway 集群
以下是一个根据 X-User-ID 请求头进行一致性哈希的示例:
Nginx
map $http_x_user_id $hash_key {
"" $remote_addr; # 如果没有 X-User-ID,用客户端 IP
default $http_x_user_id; # 否则用该 header
}
upstream backend {
hash $hash_key consistent;
server backend1.example.com;
server backend2.example.com;
}
server {
location / {
proxy_pass http://backend;
}
}3.2. Gateway 及微服务集群
信息
以下组件在 Gateway 与微服务中通用,用法相同。
3.2.1. 核心组件
负载均衡器实现类
ReactorRendezvousHashingLoadBalancer:SCLB(Spring Cloud LoadBalancer)默认不支持一致性哈希负载均衡策略,我们可以通过创建
ReactorServiceInstanceLoadBalancer接口的实现类来实现一致性哈希负载均衡器。以下是一个基于 HRW 算法的实现示例:
Javaimport com.google.common.hash.Hashing; import lombok.Getter; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.ObjectProvider; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.loadbalancer.DefaultResponse; import org.springframework.cloud.client.loadbalancer.EmptyResponse; import org.springframework.cloud.client.loadbalancer.Request; import org.springframework.cloud.client.loadbalancer.Response; import org.springframework.cloud.loadbalancer.core.ReactorServiceInstanceLoadBalancer; import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import per.tenormis.utils.RandomUtil; import per.tenormis.utils.annotation.NonNullByDefault; import per.tenormis.utils.loadbalancer.LoadBalancerKeyExtractor; import reactor.core.publisher.Mono; import java.nio.ByteBuffer; import java.util.Comparator; import java.util.List; /** * 注意:当 keyExtractor 未能提取到有效 key 时,将会退化到<b>随机</b>负载均衡 */ @RequiredArgsConstructor @NonNullByDefault public class ReactorRendezvousHashingLoadBalancer implements ReactorServiceInstanceLoadBalancer { @Getter private final String serviceId; private final ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider; private final LoadBalancerKeyExtractor keyExtractor; protected String extractKeyFallback(Request<?> request) { return String.valueOf(RandomUtil.SECURE_RANDOM.nextLong()); } @Override public Mono<Response<ServiceInstance>> choose(Request request) { ServiceInstanceListSupplier supplier = this.serviceInstanceListSupplierProvider.getIfAvailable(); if (supplier == null) return Mono.just(new EmptyResponse()); return supplier.get(request).next().map( instances -> !CollectionUtils.isEmpty(instances) ? new DefaultResponse( instances.size() > 1 ? pickByRendezvous(instances, keyExtractor.extract(request).orElseGet(() -> extractKeyFallback(request))) : instances.get(0)) : new EmptyResponse()); } private static long hashing(String key, String instanceId) { String combined = key + ":" + instanceId; byte[] hash = Hashing.murmur3_128().hashUnencodedChars(combined).asBytes(); return ByteBuffer.wrap(hash, 0, 8).getLong(); } private static ServiceInstance pickByRendezvous(List<ServiceInstance> instances, String key) { return instances.stream() .max(Comparator.comparingLong(inst -> { String instanceId = inst.getInstanceId(); if (!StringUtils.hasText(instanceId)) instanceId = inst.getUri().toString(); return hashing(key, instanceId); })) .orElse(instances.get(0)); } }提取哈希键值的接口
LoadBalancerKeyExtractor:Javaimport org.springframework.cloud.client.loadbalancer.Request; import per.tenormis.utils.annotation.NonNullByDefault; import java.util.Optional; @NonNullByDefault public interface LoadBalancerKeyExtractor { Optional<String> extract(Request<?> request); }从客户端请求的指定 HTTP 头部中提取哈希键值
HeaderLoadBalancerKeyExtractor:Javaimport lombok.RequiredArgsConstructor; import org.springframework.cloud.client.loadbalancer.Request; import org.springframework.cloud.client.loadbalancer.RequestData; import org.springframework.cloud.client.loadbalancer.RequestDataContext; import org.springframework.http.HttpHeaders; import org.springframework.util.StringUtils; import per.tenormis.utils.annotation.NonNullByDefault; import java.util.Optional; @RequiredArgsConstructor @NonNullByDefault public class HeaderLoadBalancerKeyExtractor implements LoadBalancerKeyExtractor { private final String headerName; @Override public Optional<String> extract(Request<?> request) { Object context = request.getContext(); if (context instanceof RequestDataContext) { RequestData data = ((RequestDataContext) context).getClientRequest(); if (data != null) { HttpHeaders headers = data.getHeaders(); if (headers != null) { String key = headers.getFirst(headerName); if (StringUtils.hasLength(key)) { return Optional.of(key); } } } } return Optional.empty(); } }
3.2.2. 使用示例
创建负载均衡器配置类,需要注意的是该配置类不需要加
@Configuration注解,后续我们将通过@LoadBalancerClient或@LoadBalancerClients引用它;Javaimport org.springframework.beans.factory.ObjectProvider; import org.springframework.cloud.loadbalancer.core.ReactorServiceInstanceLoadBalancer; import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier; import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory; import org.springframework.context.annotation.Bean; import org.springframework.core.env.Environment; import per.tenormis.utils.loadbalancer.HeaderLoadBalancerKeyExtractor; import per.tenormis.utils.loadbalancer.reactive.ReactorRendezvousHashingLoadBalancer; public class RendezvousLoadBalancerConfiguration { @Bean public ReactorServiceInstanceLoadBalancer serviceInstanceLoadBalancer(Environment environment, LoadBalancerClientFactory clientFactory) { String serviceId = LoadBalancerClientFactory.getName(environment); ObjectProvider<ServiceInstanceListSupplier> supplierProvider = clientFactory.getLazyProvider(serviceId, ServiceInstanceListSupplier.class); return new ReactorRendezvousHashingLoadBalancer(serviceId, supplierProvider, new HeaderLoadBalancerKeyExtractor("X-User-ID")); } }在 Spring Boot 启动类或任意
@Configuration类上绑定:Java@SpringBootApplication @LoadBalancerClient(name = "order-service", configuration = RendezvousLoadBalancerConfiguration.class) public class GatewayApplication { public static void main(String[] args) { SpringApplication.run(GatewayApplication.class, args); } }
4. 与其它缓存架构对比
单缓存架构:
性能更好,多了一级本地缓存,响应速度更快,Redis 承担的压力更小;
基于一致性算法(如 Raft)实现各节点间数据/缓存同步的多缓存架构:
实现更简单:相比于每个微服务都需要实现自定义通讯协议来保持数据/缓存的一致性,多级分片缓存架构的实现更为简单;
性能更好:缓存同步架构为了维护数据的一致性,增加了各服务之间的通讯压力;
硬件成本更低:缓存同步架构的每个服务都需要维护全量的数据,内存压力大;
基于 Nginx 实现路由分片的多缓存架构:
注册中心被架空,系统灵活性降低,运维难度增加(每次增减实例都需要修改 Nginx 配置),调用链路延长,硬件成本增加(需要更多的机器来部署 Nginx 集群)。
综上,通过对请求进行分片,不仅规避了各实例之间的数据同步问题,降低了系统的开发难度,同时也降低了单点实例的内存压力,还避免了调用链路的延长,可谓一举多得。