仿写spring cloud负载均衡实现

负载均衡实现要点

这里讨论的负载均衡指的是客户端负载均衡,仿照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调用->返回结果。

工程loadblance代码路径

    原文作者:Spring Cloud
    原文地址: https://blog.csdn.net/qq_20597727/article/details/82635059
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞