负载均衡实现要点
这里讨论的负载均衡指的是客户端负载均衡,仿照ribbon实现一个简单的负载均衡例子,采用一定的算法来决定调用服务的哪一个实例。
客户端负载均衡实现主要包括以下几点。
- 服务实例管理(定时更新)
- 拦截RestTemplate请求,实现负载均衡接入点
- 负载均衡算法逻辑
仿写实现
具体pom依赖如下
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zookeeper-all</artifactId>
<exclusions>
<exclusion>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.12</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
</exclusions>
</dependency>
仿写例子中,利用zookeeper作为注册中心。
已有服务
假如有以下一个服务,定义了一个hello rest接口
@SpringBootApplication
@EnableDiscoveryClient
@RestController
public class DiscoverClient {
@Autowired
private DiscoveryClient discoveryClient;
public static void main(String[] args) {
SpringApplication.run(DiscoverClient.class, args);
}
@RequestMapping("/hello")
public String hello(){
return "hello garine";
}
}
客户端请求代码
@SpringBootApplication(scanBasePackages = "garine.learn.custom.loadblance")
@EnableDiscoveryClient
@EnableScheduling
public class LoadblanceSpringApplication {
public static void main(String[] args) {
SpringApplication.run(LoadblanceSpringApplication.class, args);
}
}
@RestController
public class LoadbalceTestController {
@Autowired
private RestTemplate restTemplate;
@GetMapping("/{applicationName}/hello")
public String hello(@PathVariable String applicationName){
//模拟传入应用名称,请求调用服务端hello方法
return restTemplate.getForObject(applicationName + "/hello", String.class);
}
//......
}
服务实例管理实现
首先客户端要实现负载,必须先获取到所有的服务实例,然后才能决定使用哪一个实例。所以客户端需要维护一个服务实例Map,结构Map
//......
private volatile Map<String, Set<String>> serviveUrlCache = new HashMap<>();
@Autowired
private DiscoveryClient discoveryClient;
//......
/** * 更新地址缓存 */
@Scheduled(fixedRate = 10000)
public void pullServiceUrl(){
Map<String, Set<String>> oldServiveUrlCache = this.serviveUrlCache;
Map<String, Set<String>> newServiveUrlCache = new HashMap<>();
discoveryClient.getServices().forEach(dto -> {
Set<String> ins = discoveryClient.getInstances(dto).stream().map(serviceInstance -> {
return (serviceInstance.isSecure() ? "https://":"http://") + serviceInstance.getHost() + ":" + serviceInstance.getPort();
}).collect(Collectors.toSet());
newServiveUrlCache.put(dto, ins);
});
this.serviveUrlCache = newServiveUrlCache;
}
拦截RestTemplate请求,实现负载均衡接入点
我们都知道spring cloud中服务之间的调用是基于RestTemplate进行扩展实现的,结合Feign,提供一个服务名称,实现ClientHttpRequestInterceptor
接口的拦截器拦截RestTemplate请求。拦截器进行负载均衡并且发起服务调用,返回调用结果。
所以扩展负载均衡接入点,需要:
- 定义实现
ClientHttpRequestInterceptor
的拦截器类 - 实例化拦截器并且加入到为RestTemplate的拦截器列表中
1.定义拦截器实现,第一步的服务实例列表管理可以一起放到这里面一起管理。代码如下。
/** * RequestTemplate执行请求时可以应用的自定义拦截器,拦截器逻辑包含了http请求逻辑 */
public class LoadblanceInterceptor implements ClientHttpRequestInterceptor{
private volatile Map<String, Set<String>> serviveUrlCache = new HashMap<>();
@Autowired
private DiscoveryClient discoveryClient;
/** * 更新地址缓存 */
@Scheduled(fixedRate = 10000)
public void pullServiceUrl(){
Map<String, Set<String>> oldServiveUrlCache = this.serviveUrlCache;
Map<String, Set<String>> newServiveUrlCache = new HashMap<>();
discoveryClient.getServices().forEach(dto -> {
Set<String> ins = discoveryClient.getInstances(dto).stream().map(serviceInstance -> {
return (serviceInstance.isSecure() ? "https://":"http://") + serviceInstance.getHost() + ":" + serviceInstance.getPort();
}).collect(Collectors.toSet());
newServiveUrlCache.put(dto, ins);
});
this.serviveUrlCache = newServiveUrlCache;
}
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
String requestPath = request.getURI().getPath().substring(1);
//获取传入的应用名称
String applicationName = requestPath.split("/")[0];
Set<String> ins = this.serviveUrlCache.get(applicationName);
List<String> insUrls = new ArrayList<>(ins);//重新建立列表,防止并发过程被修改
//负载均衡逻辑-start
int index = new Random().nextInt(ins.size());
String preTargetUrl = insUrls.get(index);
//负载均衡逻辑-end
String targetUrl = preTargetUrl + requestPath.substring(requestPath.indexOf("/"));
URL url = new URL(targetUrl);
URLConnection urlConnection = url.openConnection();
//make a simple response
ClientHttpResponse response = new ClientHttpResponse() {
@Override
public HttpStatus getStatusCode() throws IOException {
return HttpStatus.OK;
}
@Override
public int getRawStatusCode() throws IOException {
return 200;
}
@Override
public String getStatusText() throws IOException {
return "OK";
}
@Override
public void close() {
}
@Override
public InputStream getBody() throws IOException {
return urlConnection.getInputStream();
}
@Override
public HttpHeaders getHeaders() {
return new HttpHeaders();
}
};
return response;
}
}
2.设置拦截器为RestTemplate的拦截器
在RestTemplate初始化时,就需要把拦截器加入到RestTemplate的拦截器中,这样在进行请求时才会拦截。
利用spring注解进行设置,为了方便,直接在定义客户端请求的类中设置。代码如下。
/** * 指定某一种bean,修饰/或者指定注入 */
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Qualifier
public @interface LoadblancedRestTemplate {
}
@RestController
public class LoadbalceTestController {
@Autowired
private RestTemplate restTemplate;
@GetMapping("/{applicationName}/hello")
public String hello(@PathVariable String applicationName){
return restTemplate.getForObject(applicationName + "/hello", String.class);
}
@Bean
@LoadblancedRestTemplate
public RestTemplate restTemplate(){
RestTemplate restTemplate = new RestTemplate();
return restTemplate;
}
@Bean
public Object setAndInitRestTamplates(@LoadblancedRestTemplate Collection<RestTemplate> restTemplates, LoadblanceInterceptor loadblanceInterceptor){
//@LoadblancedRestTemplate指定只能注入被@LoadblancedRestTemplate修饰的RestTemplate,LoadblanceInterceptor默认依赖注入
restTemplates.forEach(val -> {
//添加到restTemplate的拦截器中
val.getInterceptors().add(loadblanceInterceptor);
});
return new Object();
}
@Bean
public LoadblanceInterceptor loadblanceInterceptor(){
LoadblanceInterceptor l1 = new LoadblanceInterceptor();
return l1;
}
}
负载均衡算法
在LoadblanceInterceptor实现中,已经实现了一种最简单的负载方式,随机负载。负载的最终目的是选出一个服务地址,所以只需要再LoadblanceInterceptor中定制负载算法即可。
执行结果
restTemplate.getForObject方法执行时,如果发现RestTemplate具有拦截器,则执行请求逻辑由拦截器进行处理。也就是garine.learn.custom.loadblance.interceptor.LoadblanceInterceptor#intercept方法。根据传递的应用名称参数->查找出服务实例地址集合->负载出一个实例地址->拼接url->http调用->返回结果。