在分布式的时代,服务必定是多个实例的,系统再进行服务间通信时,必定需要根据当前服务实例集合,选择一个实例进行通信。而已负载均衡(Load Balance)就是基于这个产生的。
Ribbon 是一个基于 HTTP 和 TCP 的客户端负载均衡工具,它基于Netflix Ribbon实现。通过Spring Cloud的封装,可以让我们轻松的将面向服务的REST模版请求自动转换成客户端负载均衡的服务调用。
Spring Cloud Ribbon 虽然只是一个工具类框架,它不像服务注册中心、配置中心、API网关那样需要独立部署,但是它几乎存在于每一个Spring Cloud构建的微服务和基础设施中。因为微服务间的调用,API网关的请求转发等内容,实际上都是通过Ribbon来实现的。
本实例基于独立的SpringCloud Ribbon实现了一个简单的客户端负载均衡案例。
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starterartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
<version>2.4.3version>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-ribbonartifactId>
<version>2.2.9.RELEASEversion>
dependency>
<dependency>
<groupId>com.squareup.okhttp3groupId>
<artifactId>okhttpartifactId>
<version>4.9.1version>
dependency>
@RestController
@RequestMapping("/user")
public class UserService implements IUserService {
private static final Map<Long, UserBean> USER_BEAN_MAP = new ConcurrentHashMap<>();
@Value("${server.port}")
private String port;
static {
UserBean userBean = new UserBean();
userBean.setId(111L);
userBean.setName("test");
userBean.setSex("男生");
USER_BEAN_MAP.put(111L, userBean);
}
@Override
@GetMapping("/get")
public UserBean get(Long id) {
System.out.println("----------------port=" + port);
return UserService.USER_BEAN_MAP.get(id);
}
}
服务调用方就是进行负载均衡的一分,通过对服务提供方的服务列表,利用 Ribbon 进行负载调用服务。
@Configuration
public class RibbonConfiguration {
/**
* 实例化ribbon使用的RestTemplate
* @return
*/
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
/*使用OkHttp进行服务通信*/
return new RestTemplate(new OkHttp3ClientHttpRequestFactory());
}
/**
* 默认 RestTemplate
* @return
*/
@Bean
public RestTemplate defaultRestTemplate() {
return new RestTemplate(new OkHttp3ClientHttpRequestFactory());
}
}
@RestController
@RequestMapping("/finpc")
public class FinpcController {
@Autowired
private LoadBalancerClient loadBalancerClient;
@Autowired
private RestTemplate restTemplate;
@Resource(name = "defaultRestTemplate")
private RestTemplate defaultRestTemplate;
@GetMapping("/getUser/{id}")
public UserBean getUser(@PathVariable() Long id) {
UserBean forObject = restTemplate.getForObject("http://lizq-sys/user/get?id={id}", UserBean.class, id);
return forObject;
}
@GetMapping("/getUser1/{id}")
public UserBean getUser1(@PathVariable() Long id) {
// 直接通过Ribbon客户端进行服务选择,然后进行Http通信
ServiceInstance instance = loadBalancerClient.choose("lizq-sys");
URI storesUri = URI.create(String.format("http://%s:%s/user/get?id=%s", instance.getHost(), instance.getPort(), id));
UserBean forObject = defaultRestTemplate.getForObject(storesUri, UserBean.class);
return forObject;
}
}
server:
port: 8080
spring:
application:
name: lizq-finpc
# lizq-sys为需要负载的服务名
lizq-sys:
ribbon:
listOfServers: http://localhost:8888,http://localhost:8889
上面就是一个简单使用 Ribbon 的例子,Ribbon 作为一个独立的组件,可以不依赖与任何第三方进行使用。
Ribbon内置了7种负载均衡算法,并且保留了扩展,用户可通过实现 IRule
接口,实现自定义负载均衡算法。
package com.netflix.loadbalancer;
public interface IRule {
Server choose(Object key);
void setLoadBalancer(ILoadBalancer loadBalancer);
ILoadBalancer getLoadBalancer();
}
IRule
是负载均衡策略的抽象,ILoadBalancer 通过调用 IRule
的 choose(Object key)
方法返回 Server。
IPHash 算法: 根据调用者的ip hash后进行服务选择。
在 application.yaml 中添加配置:
# lizq-sys为需要负载的服务名
lizq-sys:
ribbon:
listOfServers: http://localhost:8888,http://localhost:8889
NFLoadBalancerRuleClassName: com.lizq.finpc.rule.IpHashRule
com.lizq.finpc.rule.IpHashRule 实现
import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.AbstractLoadBalancerRule;
import com.netflix.loadbalancer.Server;
import org.springframework.util.CollectionUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.util.List;
public class IpHashRule extends AbstractLoadBalancerRule {
@Override
public void initWithNiwsConfig(IClientConfig iClientConfig) {
}
@Override
public Server choose(Object key) {
if (getLoadBalancer() == null) {
return null;
}
Server server = null;
while (server == null) {
// 表示可用的服务列表.(默认情况下单纯只用Ribbon时,不会对目标服务做心跳检测)
List<Server> upList = getLoadBalancer().getReachableServers();
// List allList = getLoadBalancer().getAllServers();
int serverCount = upList.size();
if (CollectionUtils.isEmpty(upList)) {
return null;
}
int index = ipAddressHash(serverCount);
server = upList.get(index);
}
return server;
}
/**
* @param serverCount
* @return
*/
private int ipAddressHash(int serverCount) {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
String remoteAddr = requestAttributes.getRequest().getRemoteAddr();
int code = Math.abs(remoteAddr.hashCode());
return code % serverCount;
}
}
Ribbon 实现的关键点是利用了 RestTemplate 的拦截器机制,在拦截器中实现 Ribbon 的负载均衡。负载均衡的基本实现就是利用applicationName 从配置文件或服务注册中心获取可用的服务地址列表,然后通过一定算法负载,返回服务地址后进行 Http 调用。
RestTemplate 拦截器机制
RestTemplate 中有一个属性是 List
,如果 interceptors 里面的拦截器数据不为空,在RestTemplate 进行 Http 请求时,这个请求就会被拦截器拦截。
拦截器需要实现 ClientHttpRequestInterceptor
接口
package org.springframework.http.client;
import java.io.IOException;
import org.springframework.http.HttpRequest;
@FunctionalInterface
public interface ClientHttpRequestInterceptor {
ClientHttpResponse intercept(HttpRequest httpRequest, byte[] body, ClientHttpRequestExecution execution) throws IOException;
}
Ribbon 中的 RestTemplate 拦截器:LoadBalancerInterceptor
public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {
private LoadBalancerClient loadBalancer;
private LoadBalancerRequestFactory requestFactory;
public LoadBalancerInterceptor(LoadBalancerClient loadBalancer, LoadBalancerRequestFactory requestFactory) {
this.loadBalancer = loadBalancer;
this.requestFactory = requestFactory;
}
public LoadBalancerInterceptor(LoadBalancerClient loadBalancer) {
this(loadBalancer, new LoadBalancerRequestFactory(loadBalancer));
}
public ClientHttpResponse intercept(final HttpRequest request, final byte[] body, final ClientHttpRequestExecution execution) throws IOException {
URI originalUri = request.getURI();
String serviceName = originalUri.getHost();
Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);
// 通过LoadBalance算法选择serviceName对应的所有的可用服务
return (ClientHttpResponse)this.loadBalancer.execute(serviceName, this.requestFactory.createRequest(request, body, execution));
}
}
/**
* 拦截请求执行
*/
public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
return this.execute(serviceId, (LoadBalancerRequest)request, (Object)null);
}
public <T> T execute(String serviceId, LoadBalancerRequest<T> request, Object hint) throws IOException {
// 获取当前serviceId对应的LoadBalance
ILoadBalancer loadBalancer = this.getLoadBalancer(serviceId);
// 通过loadBalance选择 Server
Server server = this.getServer(loadBalancer, hint);
if (server == null) {
throw new IllegalStateException("No instances available for " + serviceId);
} else {
RibbonLoadBalancerClient.RibbonServer ribbonServer = new RibbonLoadBalancerClient.RibbonServer(serviceId, server, this.isSecure(server, serviceId), this.serverIntrospector(serviceId).getMetadata(server));
// 执行请求
return this.execute(serviceId, (ServiceInstance)ribbonServer, (LoadBalancerRequest)request);
}
}
/**
* 通过loadBalance选择 Server
*/
protected Server getServer(ILoadBalancer loadBalancer, Object hint) {
// 执行LoadBalance中的choose(Object key) 方法,返回对应的Server
return loadBalancer == null ? null : loadBalancer.chooseServer(hint != null ? hint : "default");
}
定义注入器、拦截器
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnMissingClass({"org.springframework.retry.support.RetryTemplate"})
static class LoadBalancerInterceptorConfig {
LoadBalancerInterceptorConfig() {
}
@Bean
public LoadBalancerInterceptor loadBalancerInterceptor(LoadBalancerClient loadBalancerClient, LoadBalancerRequestFactory requestFactory) {
return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);
}
@Bean
@ConditionalOnMissingBean
public RestTemplateCustomizer restTemplateCustomizer(final LoadBalancerInterceptor loadBalancerInterceptor) {
// 定义注入器,用来将拦截器注入到RestTemplate中
return (restTemplate) -> {
List<ClientHttpRequestInterceptor> list = new ArrayList(restTemplate.getInterceptors());
list.add(loadBalancerInterceptor);
restTemplate.setInterceptors(list);
};
}
}
将 Ribbon 拦截器注入到 RestTemplate
@Configuration
@ConditionalOnClass(RestTemplate.class)
@ConditionalOnBean(LoadBalancerClient.class)
@EnableConfigurationProperties(LoadBalancerRetryProperties.class)
public class LoadBalancerAutoConfiguration {
/**
* 获取所有的 @LoadBalanced 标注的RestTemplate 对象
*/
@LoadBalanced
@Autowired(required = false)
private List<RestTemplate> restTemplates = Collections.emptyList();
@Bean
public SmartInitializingSingleton loadBalancedRestTemplateInitializerDeprecated(
final ObjectProvider<List<RestTemplateCustomizer>> restTemplateCustomizers) {
// 遍历context中的注入器,调用注入方法。
return () -> restTemplateCustomizers.ifAvailable(customizers -> {
for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) {
for (RestTemplateCustomizer customizer : customizers) {
customizer.customize(restTemplate);
}
}
});
}
........
}
遍历 context 中的注入器(RestTemplateCustomizer
),调用注入方法,为目标 RestTemplate 注入拦截器。
还有关键的一点是:需要注入拦截器的目标 RestTemplates 到底是哪一些?因为RestTemplate实例在context中可能存在多个,不可能所有的都注入拦截器,这里就是 @LoadBalanced 注解发挥作用的时候了。
@LoadBalanced 注解
这个注解是 Spring Cloud 实现的,不是 Ribbon 中的,它的作用是在依赖注入时,只注入被 @LoadBalanced 修饰的实例。
@Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Qualifier
public @interface LoadBalanced {
}
那么 @LoadBalanced
是如何实现这个功能的呢?其实都是 Spring 的原生操作。@LoadBalanced
继承了 @Qualifier
注解,@Qualifier
注解用来指定想要依赖某些特征的实例,这里的注解就是类似的实现,RestTemplates 通过 @Autowired
注入,同时被@LoadBalanced
修饰,所以只会注入 @LoadBalanced
修饰的 RestTemplate,也就是我们的目标 RestTemplate。