深入理解Ribbon原理并手写一个微服务负载均衡器
负载均衡器
在微服务架构里面,我们的服务消费者请求服务提供者,通常使用RestTemplate发起http请求。
我们可以写死服务提供者的ip地址和端口号,然后通过RestTemplate发起http请求时指定该服务提供者的ip地址和端口号。我们可以写死服务提供者的ip地址端口号,但是一个服务通常有好几个服务提供者节点组成一个集群,这时候服务消费者就要记录所有服务提供者的ip地址端口号,并且要自行决定请求哪一个节点,这是非常不便于维护的。即使只有一个服务提供者,它的ip地址和端口好也是有可能会变的。
在微服务的世界里,负载均衡器是一个重要组成部分。而负载均衡器可以使得服务消费者可以按照某种负载均衡策略请求微服务集群中的不同服务提供者节点。
由于有了负载均衡器,服务消费者请求服务提供者不再需要通过ip地址加端口号的方式,而是可以以服务名作为域名,负载均衡器会通过一定的负载均衡策略,选择服务名对应的微服务集群中的其中一个服务提供者节点,将请求地址中的服务名替换为该节点的ip地址端口号。
理解Ribbon原理
Ribbon是一个经典的微服务负载均衡器,它是微服务客户端的负载均衡器。通过引入Ribbon,我们的服务消费者可以通过Ribbon的负载均衡机制,选择服务提供者集群中的某个节点发起请求。
Ribbon通过在RestTemplate中加入拦截器的方式,扩展了RestTemplate的能力,使得它具备客户端负载均衡的能力。Ribbon会在RestTemplate的拦截器链interceptors中加入一个自己的拦截器LoadBalancerInterceptor,这个LoadBalancerInterceptor会为RestTemplate提供负载均衡的能力。
LoadBalancerInterceptor被添加到RestTemplate之后,每个通过RestTemplate发起的http请求都会经过LoadBalancerInterceptor的处理。LoadBalancerInterceptor会调用LoadBalancerClient负载均衡客户端进行处理,LoadBalancerClient会通过Ribbon的负载均衡器ILoadBalancer根据负载均衡策略从服务提供者列表中选出一个节点,然后LoadBalancerClient根据选取到的负载均衡节点的ip地址和端口号重写请求的url。
这样,RestTemplate拿到重写后的url,就可以请求对应的服务提供者节点了。
那么还剩下一个问题,LoadBalancerInterceptor是什么时候又是如何被添加到RestTemplate的拦截器链的呢?
其实Ribbon利用了Spring的SmartInitializingSingleton这个扩展点,Spring会在完成所有非懒加载单例bean的初始化后,触发SmartInitializingSingleton的调用。Ribbon扩展了Spring的这个SmartInitializingSingleton接口并往Spring容器中注册。
Spring在完成所有非懒加载单例bean的初始化后触发该SmartInitializingSingleton的调用,往RestTemplate的拦截器链中添加LoadBalancerInterceptor。
手写一个微服务负载均衡器
了解了微服务负载均衡器的作用,又理解了Ribbon的原理之后,我们就可以参照Ribbon动手写一个自己的微服务负载均衡器了。
我们大体上还是参照Ribbon增强RestTemplate的方式,但是我们不像Ribbon那样往RestTemplate的拦截器链上加入自己的拦截器,而是使用另外一个接口ClientHttpRequestFactory。
在RestTemplate发起http请求时,会调用ClientHttpRequestFactory的createRequest(URI uri, HttpMethod httpMethod)方法构建一个ClientHttpRequest对象,里面包含了请求的url地址。然后再调用这个request对象的execute()方法发起http请求,返回一个response对象。这一切的逻辑就在RestTemplate的doExecute()方法中。
RestTemplate#doExecute
protected <T> T doExecute(URI url, HttpMethod method, ...) throws RestClientException {
...
ClientHttpResponse response = null;
try {
// 调用ClientHttpRequestFactory的createRequest()方法方法构造ClientHttpRequest
ClientHttpRequest request = createRequest(url, method);
...
// 调用ClientHttpRequest的execute()方法发起http请求,返回response
response = request.execute();
...
}
catch (...) {...}
...
}
总体设计
于是我们的大体设计就是实现一个自己的ClientHttpRequestFactory,在ClientHttpRequestFactory的createRequest方法里面进行负载均衡和重构url的操作。而我们的ClientHttpRequestFactory对象也是通过Spring的扩展点SmartInitializingSingleton接口放入到RestTemplate中。
我们的框架设计大概就是下面那样:
除了ClientHttpRequestFactory以外,我们还要实现LoadBalanceClient负载均衡客户端,ClientHttpRequestFactory会调用LoadBalanceClient。然后LoadBalanceClient里面是一个loadBalancerMap(负载均衡器map),key是服务名,value是对应的LoadBalancer负载均衡器。
那么整体流程如下:
- ClientHttpRequestFactory调用LoadBalanceClient
- LoadBalanceClient从url中取出serviceName,以serviceName为key从loadBalancerMap中取出对应的LoadBalancer
- LoadBalancer进行负载均衡选取一个节点
- LoadBalanceClient获取LoadBalancer返回的节点,根据节点的ip地址和port端口重写url
- ClientHttpRequestFactory利用重写的url构建ClientHttpRequest对象
上图除开灰色部分,其余的部分都是我们要实现的逻辑。
其中LoadBalancer里面还有一个RegistryCenterClient对象和LoadBalanceRule对象。RegistryCenterClient是注册中心客户端,用于从注册中心中根据服务名serviceName查询服务提供者列表的。而LoadBalanceRule则是负载均衡规则。
总体设计就讲述完毕,下面我们就去看一下代码。
LoadBalanceClientHttpRequestFactory
我们实现的ClientHttpRequestFactory名字叫做LoadBalanceClientHttpRequestFactory,它实现了ClientHttpRequestFactory接口。在LoadBalanceClientHttpRequestFactory中调用我们自己实现的负载均衡客户端LoadBalanceClient进行负载均衡和重构url的操作,然后使用重构后的url构建一个Request对象返回。
public class LoadBalanceClientHttpRequestFactory implements ClientHttpRequestFactory {
private ClientHttpRequestFactory parent;
private LoadBalanceClient loadBalanceClient;
...
@Override
public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException {
uri = URI.create(reconstructUri(uri.toString()));
// 使用重构后的url构建request对象
return parent.createRequest(uri, httpMethod);
}
private String reconstructUri(String uri) {
String serviceName = null;
boolean startsWithHttps = uri.startsWith("https");
String temp = uri.replace(startsWithHttps ? "https://" : "http://", "");
serviceName = temp.contains("/") ? temp.substring(0, temp.indexOf("/")) : temp;
// 调用我们自己实现的负载均衡客户端LoadBalanceClient进行负载均衡和重构url的操作
uri = loadBalanceClient.reconstructUrl(serviceName, uri);
return uri;
}
可以看到构建Request对象时,我们调用的是parent.createRequest(uri, httpMethod),这个parent也是一个ClientHttpRequestFactory,它是Spring提供的SimpleClientHttpRequestFactory,通过它就可以使用给定的url构建一个Request对象,无需我们重复造轮子。
SimpleLoadBalanceClient
LoadBalanceClientHttpRequestFactory会调用LoadBalanceClient接口的reconstructUrl(serviceName, uri)方法,LoadBalanceClient是我们定义的负载均衡客户端接口,具体实现类是SimpleLoadBalanceClient。
public class SimpleLoadBalanceClient implements LoadBalanceClient {
...
private Map<String, LoadBalancer> loadBalancerMap;
@Autowired
private RegistryCenterClient registryCenterClient;
@Autowired
private LoadBalanceProperties loadBalanceProperties;
...
public SimpleLoadBalanceClient() {
loadBalancerMap = new ConcurrentHashMap<>();
}
@Override
public String reconstructUrl(String serviceName, String url) {
// 根据服务名serviceName获取负载均衡器LoadBalancer
LoadBalancer loadBalancer = loadBalancerMap.get(serviceName);
// 如果loadBalancerMap中没有对应的LoadBalancer,则创建LoadBalancer
if (loadBalancer == null) {
// 利用Java的SPI机制加载所有的负载均衡策略类LoadBalanceRule
ServiceLoader<LoadBalanceRule> serviceLoader = ServiceLoader.load(LoadBalanceRule.class);
for (LoadBalanceRule loadBalanceRule: serviceLoader) {
// 读取LoadBalanceRule实现类上的@Rule注解
Rule rule = loadBalanceRule.getClass().getAnnotation(Rule.class);
// 判断@Rule注解是否与配置文件指定的负载均衡类型匹配
if (StringUtils.equals(rule.value(), loadBalanceProperties.getType())) {
// 创建LoadBalancer对象,实现类是SimpleLoadBalancer
loadBalancer = new SimpleLoadBalancer(serviceName, registryCenterClient, loadBalanceRule);
// LoadBalancer对象缓存到map中
loadBalancerMap.put(serviceName, loadBalancer);
break;
}
}
}
...
// 根据负载均衡策略选取一个节点
MicroService microService = loadBalancer.chooseMicroService();
if (microService != null) {
// 把url中的服务名替换成选取节点的ip地址和端口号
url = url.replace(serviceName, microService.getIp() + ":" + microService.getPort());
}
return url;
}
}
SimpleLoadBalanceClient根据serviceName从loadBalancerMap取出LoadBalancer,然后调用LoadBalancer的chooseMicroService()方法根据负载均衡策略选取一个服务提供者节点MicroService,然后把url中的服务名替换成选取出的节点的ip地址和端口号。
如果SimpleLoadBalanceClient取不到LoadBalancer,就会创建一个LoadBalancer。创建LoadBalancer前首先通过Java的SPI机制加载所有的负载均衡策略类LoadBalanceRule,再通过@Rule注解与配置文件的配置进行匹配,匹配出一个LoadBalanceRule。再以匹配到的LoadBalanceRule对象以及注册中心客户端RegistryCenterClient 为构造方法参数,创建SimpleLoadBalancer对象,缓存到loadBalancerMap中。
SimpleLoadBalancer
再来看一下SimpleLoadBalancer的代码,
public class SimpleLoadBalancer implements LoadBalancer {
private String serviceName;
private RegistryCenterClient registryCenterClient;
private LoadBalanceRule loadBalanceRule;
...
@Override
public MicroService chooseMicroService() {
// 通过注册中心客户端根据服务名拉取服务实例列表
List<MicroService> microServiceList = registryCenterClient.getMicroServiceList(serviceName);
...
// 根据负载均衡规则选出一个节点
return loadBalanceRule.chooseMicroService(serviceName, microServiceList);
}
}
SimpleLoadBalancer的逻辑很简单,就是调用负载均衡客户端RegistryCenterClient的getMicroServiceList(serviceName)方法根据服务名serviceName从注册中心拉取服务实例列表。RegistryCenterClient里面是有本地缓存的,如果本地已经缓存了服务名对应的服务实例列表,就不会请求注册中心,因此SimpleLoadBalancer里面我就没有再做一次缓存了。当SimpleLoadBalancer通过RegistryCenterClient获取到实例列表后,调用负载均衡规则LoadBalanceRule的chooseMicroService(serviceName, microServiceList)方法根据负载均衡规则从列表中选取一个节点。
LoadBalanceRule
LoadBalanceRule是负载均衡规则的接口,类似与Ribbon的IRule。我们看一个轮询策略的实现类RoundRobinLoadBalanceRule。
@Rule("roundrobin")
public class RoundRobinLoadBalanceRule implements LoadBalanceRule {
// key->服务名,value->下标
private Map<String, AtomicLong> indexMap = new ConcurrentHashMap<>();
@Override
public MicroService chooseMicroService(String serviceName, List<MicroService> microServices) {
AtomicLong index = indexMap.putIfAbsent(serviceName, new AtomicLong());
long num = index.getAndIncrement();
return microServices.get((int) (num % microServices.size()));
}
}
代码一看就懂,indexMap是服务名serviceName与AtomicLong计数器的映射,chooseMicroService方法通过通过serviceName拿到计算器,然后调用AtomicLong的getAndIncrement()进行原子自增操作,然后模上服务列表的size。
我们注意到RoundRobinLoadBalanceRule类上有一个@Rule(“roundrobin”),我们规定每个LoadBalanceRule实现类都必须被@Rule注解修饰,然后@Rule的属性是负载均衡规则名称,用于与配置文件的“loadbalance.rule.type”配置进行匹配的。
spring.factories与LoadBalanceConfig
我们使用SpringBoot的自动装配机制,在spring.factories文件中定义好我们的自动配置类LoadBalanceConfig
spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.huangjunyi1993.simple.microservice.loadbalance.config.LoadBalanceConfig
LoadBalanceConfig就是我们的自动配置类,会往Spring中注册LoadBalanceClientHttpRequestFactory、LoadBalanceClient等核心组件,并通过SmartInitializingSingleton扩展点把LoadBalanceClientHttpRequestFactory设置到RestTemplate中。
@Configuration
@EnableConfigurationProperties({LoadBalanceProperties.class})
public class LoadBalanceConfig {
@Autowired(required = false)
private List<RestTemplate> restTemplates = Collections.emptyList();
@Bean
@ConditionalOnMissingBean(LoadBalanceClient.class)
public LoadBalanceClient loadBalanceClient() {
return new SimpleLoadBalanceClient();
}
@Bean
public LoadBalanceClientHttpRequestFactory loadBalanceClientHttpRequestFactory(LoadBalanceClient loadBalanceClient) {
return new LoadBalanceClientHttpRequestFactory(loadBalanceClient);
}
@Bean
@ConditionalOnMissingBean
public RestTemplateCustomizer restTemplateCustomizer(LoadBalanceClientHttpRequestFactory loadBalanceClientHttpRequestFactory) {
return (restTemplate) -> restTemplate.setRequestFactory(loadBalanceClientHttpRequestFactory);
}
@Bean
public SmartInitializingSingleton loadBalancedRestTemplateInitializer(List<RestTemplateCustomizer> customizers) {
return () -> restTemplates.forEach(restTemplate -> customizers.forEach(customizer -> customizer.customize(restTemplate)));
}
}
大源码图:
代码仓库地址:https://gitee.com/huang_junyi/simple-microservice/tree/master/simple-microservice-loadbalance