仿写spring cloud负载均衡实现

扫码关注公众号:Java 技术驿站

发送:vip
将链接复制到本浏览器,永久解锁本站全部文章

【公众号:Java 技术驿站】 【加作者微信交流技术,拉技术群】
免费领取 2000+ 道 Java 面试题

负载均衡实现要点

这里讨论的负载均衡指的是客户端负载均衡,仿照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代码路径


来源:http://ddrv.cn/a/88268

赞(0) 打赏
版权归原创作者所有,任何形式的转载请联系博主:daming_90:Java 技术驿站 » 仿写spring cloud负载均衡实现

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏