栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

还不懂SpringCloud?带你深入了解(未完待续)

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

还不懂SpringCloud?带你深入了解(未完待续)

文章目录
  • 简介
  • 功能介绍
    • 组件概述
    • 组件详情
      • 注册中心
      • 配置中心
      • 服务调用
        • RestTemplate
        • RestTemplate+Ribbon
          • 源码分析
          • 负载策略
          • 重试&超时
        • Fegin
      • 服务路由
      • 熔断降级
  • 代码仓库
  • 总结

通过本文可以给你带来什么?

  1. 熟悉掌握SpringCloud,了解其生态及掌握多个组件的原理。
  2. 上手实践,基于SpringCloud各组件搭建项目。
简介

Spring Cloud是一系列框架的有序集合。它利用Spring Boot的开发便利性巧妙地简化了分布式系统基础设施的开发,如服务发现注册、配置中心、消息总线、负载均衡、断路器、数据监控等,都可以用Spring Boot的开发风格做到一键启动和部署。Spring Cloud并没有重复制造轮子,它只是将各家公司开发的比较成熟、经得起实际考验的服务框架组合起来,通过Spring Boot风格进行再封装屏蔽掉了复杂的配置和实现原理,最终给开发者留出了一套简单易懂、易部署和易维护的分布式系统开发工具包。

Spring Cloud的诞生就是为了做分布式,它是一个生态,定义了一套标准,目前基于其落地实现的有:Spring Cloud Netflix、Spring Cloud Alibaba,二者都可以提供一套分布式解决方案,例如:众多服务统一管理,服务之间的通信,服务熔断降级、链路追踪等。

功能介绍 组件概述

Spring Cloud为我们提供的解决方案中包含很多组件,每个组件能起到什么作用?下面表格列出Spring Cloud生态常用组件以及其应用。

Spring Cloud官方Spring Cloud NetflixSpring Cloud Alibaba
服务发现/注册ConsulEurekaNacos
服务调用OpenFeign/RestTemplateFeignDubbo RPC
负载均衡LoadbalancerRibbonDubbo LB
服务路由Spring Cloud GatewayZuul-
配置中心Spring Cloud Config-Nacos
服务熔断-HystrixSentinel

通过下图可以清晰的了解各个组件在系统中的作用

组件详情 注册中心

Eureka作为注册中心,分为服务端和客户端,服务端提供客户端注册,客户端可以在服务端拉取注册表到本地(所以客户端不会因为注册中心宕机而找不到其他服务,优先在本地找)。通过下图简单了解:

其中几个核心功能如下:

  • Register

服务注册,向Eureka注册信息,以被发现。

#开启服务注册
eureka.client.register-with-eureka = true
#注册中心地址
eureka.client.service-url.defaultZone=http://eureka-server1.com:8001/eureka/
  • Renew

续租,心跳,30s发送一次心跳续租,通知Eureka实例时活动的,Eureka超过90s没发现更新,在注册表中移除。

#续租时间
eureka.instance.lease-renewal-interval-in-seconds=30
#该时间内未收到下次心跳移除实例
eureka.instance.lease-expiration-duration-in-seconds=90
  • Fetch Registry

Eureka客户端从服务器拉取注册表缓存本地,之后客户端使用这些信息查找其他服务,定期(30s)更新。

#在注册中心拉取服务注册列表
eureka.client.fetch-registry=true
#更新注册列表时间间隔
eureka.client.registry-fetch-interval-seconds=30
  • Cancel

Eureka客户端在关闭时向Eureka服务器发送取消请求,服务器将从注册表删除实例。

  • 自我保护机制

默认情况下,Eureka Server在一定时间内接收不到某个服务的续约会将其踢出服务列表。但是在网络故障时,上述行为就非常危险,因为服务正常,不应该被踢出。Eureka Server通过自我保护机制解决这个问题,当短时间内丢失过多服务时(小于85%),会保护注册列表中的服务不被注销,网络恢复后退出保护模式。

#开启自我保护机制
eureka.server.enable-self-preservation=true
#比较实际心跳和预计心跳时间频率
eureka.server.eviction-interval-timer-in-ms=60000
#短时间丢失服务比例,小于该值进入保护模式
eureka.server.renewal-percent-threshold = 0.85
  • 容错机制

在注册表内的服务一段时间内默认可用,每次调用不实时检查是否可用,提高可用,降低一致。

  • 健康检查

server和client通过心跳保持服务状态,心跳正常,服务一直UP。

如果此服务DB连不上无法提供正常服务需要将服务下线,此时需要启动eureka的健康检查,同时需要设置健康状态为DOWN,代码如下:

#启动健康检查
eureka.client.healthcheck.enabled=true
public class HealthStatusEvent implements HealthIndicator {

    private Boolean status=true;

    @Override
    public Health health() {
        if(status)
            return new Health.Builder().up().build();
        return new Health.Builder().down().build();
    }

    public Boolean getStatus() {
        return status;
    }

    public void setStatus(Boolean status) {
        this.status = status;
    }
}

根据实际情况通过调用setStatus设置服务健康状态。

  • 高可用方式

1.eureka server作为客户端向其他eureka server注册。
server1配置:

eureka.instance.hostname=eureka-server1.com
eureka.client.service-url.defaultZone=http://eureka-server2.com:8002/eureka/

server2配置:

eureka.instance.hostname=eureka-server2.com
eureka.client.service-url.defaultZone=http://eureka-server1.com:8001/eureka/

2.多个eureka server作为单节点注册到多个eureka server中
server1配置:

eureka.instance.hostname=eureka-server1.com
eureka.client.service-url.defaultZone=http://eureka-server1.com:8001/eureka/,http://eureka-server2.com:8002/eureka/,http://eureka-server3.com:8003/eureka/

server2配置:

eureka.instance.hostname=eureka-server2.com
eureka.client.service-url.defaultZone=http://eureka-server1.com:8001/eureka/,http://eureka-server2.com:8002/eureka/,http://eureka-server3.com:8003/eureka/

server3配置:

eureka.instance.hostname=eureka-server3.com
eureka.client.service-url.defaultZone=http://eureka-server1.com:8001/eureka/,http://eureka-server2.com:8002/eureka/,http://eureka-server3.com:8003/eureka/

注意:
由于是在一台服务器上搭建,所以需要指定eureka.instance.hostname,向系统的hosts文件添加ip映射

配置中心

共有配置

不在本地读取配置,远程读取配置

手动刷新配置、自动刷新配置

解决的痛点:

服务调用

SpringCloud服务之间的通信方式主要是以restful风格进行服务调用,常见组合RestTemplate+Ribbon、Fegin。其中RestTemplate是Spring提供用于访问Rest服务的客户端,Ribbon主要功能是提供客户端在服务调用时的负载均衡,而Fegin是一个声明式的Http客户端,只需要在接口上添加一个注解,使得服务调用更简单。

service-provider工程提供的接口

package com.bg.serviecprovider.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class MainController {

    @RequestMapping("/hi")
    public String hi() {
        return "hi,im provider";
    }
}

service-consumer工程通过通过以下三种方式进行调用:

RestTemplate
@GetMapping("/hi")
    public String hi() {
        RestTemplate restTemplate = new RestTemplate();
        return restTemplate.getForObject("http://127.0.0.1:8081/hi", String.class);
    }
RestTemplate+Ribbon

注入RestTemplate时加上@LoadBalanced注解就会具有负载均衡的能力,代码如下:

    @Bean
    //开启负载均衡
    @LoadBalanced
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }
    @GetMapping("/hi2")
    public String hi2() {
        return restTemplate.getForObject("http://service-provider/hi", String.class);
    }
源码分析

为什么加上@LoadBalanced,RestTemplate就有负载均衡的能力呢?源码分析:

1.先看RestTemplate类,继承了InterceptingHttpAccessor,代码中有个类型为ClientHttpRequestInterceptor拦截器集合,Ribbon的功能就是在这扩展的。

public class RestTemplate extends InterceptingHttpAccessor implements RestOperations {
  ......
}
public abstract class InterceptingHttpAccessor extends HttpAccessor {
    private final List interceptors = new ArrayList();
  ......
}

2.Interceptor在哪里注入给RestTemplate的?看LoadBalancerAutoConfiguration这个类,该类重点关注这个静态内部类LoadBalancerInterceptorConfig,其往spring注入了LoadBalancerInterceptor这个Bean,实现了上面所讲的ClientHttpRequestInterceptor接口,并赋值给RestTemplate。

@Configuration(
    proxyBeanMethods = false
)
@ConditionalOnClass({RestTemplate.class})
@ConditionalOnBean({LoadBalancerClient.class})
@EnableConfigurationProperties({LoadBalancerRetryProperties.class})
public class LoadBalancerAutoConfiguration {
  ......
    @ConditionalOnMissingClass({"org.springframework.retry.support.RetryTemplate"})
    static class LoadBalancerInterceptorConfig {
        LoadBalancerInterceptorConfig() {
        }

        @Bean
        public LoadBalancerInterceptor ribbonInterceptor(LoadBalancerClient loadBalancerClient, LoadBalancerRequestFactory requestFactory) {
            return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);
        }

        @Bean
        @ConditionalOnMissingBean
        public RestTemplateCustomizer restTemplateCustomizer(final LoadBalancerInterceptor loadBalancerInterceptor) {
            return (restTemplate) -> {
                List list = new ArrayList(restTemplate.getInterceptors());
                list.add(loadBalancerInterceptor);
                restTemplate.setInterceptors(list);
            };
        }
    }
}

3.接着看RestTemplate请求执行的过程,在执行doExecute()时获取一个ClientHttpRequest->InterceptingClientHttpRequest,调用request.execute()时会做两件事(1)遍历intercept,对请求的URL(http://service-provider/hi)进行处理,这里的intercept也就时步骤2的LoadBalancerInterceptor。(2)对处理后的URL进行真正的HTTP请求。

protected  T doExecute(URI url, @Nullable HttpMethod method, @Nullable RequestCallback requestCallback, @Nullable ResponseExtractor responseExtractor) throws RestClientException {
        Assert.notNull(url, "URI is required");
        Assert.notNull(method, "HttpMethod is required");
        ClientHttpResponse response = null;
        Object var14;
        try {
          	//InterceptingClientHttpRequest
            ClientHttpRequest request = this.createRequest(url, method);
            ......
            response = request.execute();
        	  ......
        } catch (IOException var12) {
           ......
        } finally {
           ......
        }
        return var14;
    }
class InterceptingClientHttpRequest extends AbstractBufferingClientHttpRequest {
    ......
    protected final ClientHttpResponse executeInternal(HttpHeaders headers, byte[] bufferedOutput) throws IOException {
        InterceptingClientHttpRequest.InterceptingRequestExecution requestExecution = new InterceptingClientHttpRequest.InterceptingRequestExecution();
        return requestExecution.execute(this, bufferedOutput);
    }
    private class InterceptingRequestExecution implements ClientHttpRequestExecution {
        ......
        public ClientHttpResponse execute(HttpRequest request, byte[] body) throws IOException {
          //这里执行LoadBalancerInterceptor
            if (this.iterator.hasNext()) {
                ClientHttpRequestInterceptor nextInterceptor = (ClientHttpRequestInterceptor)this.iterator.next();
                return nextInterceptor.intercept(request, body, this);
            } else {
              //拦截器处理完后执行真正的HTTP请求
                ......
                ClientHttpRequest delegate = InterceptingClientHttpRequest.this.requestFactory.createRequest(request.getURI(), method);
                ......
                return delegate.execute();
            }
        }
    }
}

4.intercept是如何处理URL的?调用intercept时有个loadBalancer,其实现为RibbonLoadBalancerClient,在执行loadBalancer的execute时将URL中的“service-provider”映射成对应的ip:port,期间包括负载策略、重试策略、服务列表的获取等功能。

//拦截器的处理
public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {
   ......
    public ClientHttpResponse intercept(final HttpRequest request, final byte[] body, final ClientHttpRequestExecution execution) throws IOException {
        ......
        return (ClientHttpResponse)this.loadBalancer.execute(serviceName, this.requestFactory.createRequest(request, body, execution));
    }
}
public class RibbonLoadBalancerClient implements LoadBalancerClient {
  ......
    public  T execute(String serviceId, LoadBalancerRequest request, Object hint) throws IOException {
    //获取“service-provider”的服务列表
        ILoadBalancer loadBalancer = this.getLoadBalancer(serviceId);
    //通过负载策略选择具体的服务
        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);
        }
    }
 	 protected Server getServer(ILoadBalancer loadBalancer, Object hint) {
     //这里是BaseLoadBalancer
        return loadBalancer == null ? null : loadBalancer.chooseServer(hint != null ? hint : "default");
    }
  ......
}
public class BaseLoadBalancer extends AbstractLoadBalancer implements
        PrimeConnections.PrimeConnectionListener, IClientConfigAware {
  //默认的负载策略
    private final static IRule DEFAULT_RULE = new RoundRobinRule();
    protected IRule rule = DEFAULT_RULE;
 
    public BaseLoadBalancer() {
        this.name = DEFAULT_NAME;
        this.ping = null;
        setRule(DEFAULT_RULE);
        setupPingTask();
        lbStats = new LoadBalancerStats(DEFAULT_NAME);
    }
		//添加服务
    public void addServer(Server newServer) {
         ...
    }

  //设置服务列表
    public void setServersList(List lsrv) {
       ......
    }

  	//获取服务列表
    public List getServerList(boolean availableOnly) {
        return (availableOnly ? getReachableServers() : getAllServers());
    } 

  	//根据负载策略选择具体的服务
    public Server chooseServer(Object key) {
        if (counter == null) {
            counter = createCounter();
        }
        counter.increment();
        if (rule == null) {
            return null;
        } else {
            try {
                return rule.choose(key);
            } catch (Exception e) {
                logger.warn("LoadBalancer [{}]:  Error choosing server for key {}", name, key, e);
                return null;
            }
        }
    }
}

简单提一嘴,Ribbon在2019年停止维护,2020年cloud版本删除了Ribbon的依赖由Loadbalancer顶替负载均衡,个人感觉跟Ribbon一样。

负载策略

Ribbon内置了几种负载策略,体现在源码BaseLoadBalancer类中IRule,可以配置其他负载规则或者自定义实现规则。

负载策略说明
RoundRobinRule轮询策略,默认采⽤的策略
RandomRule随机策略,从所有的提供者随机选一个
RetryRule重试策略,先按照RoundRobinRule策略获取,若获取失败,则在指定的时限内重试
BestAvailableRule最可⽤策略,选择并发量最⼩的提供者
WeightedResponseTimeRule权重策略,根据响应时间做权重,时间越快权重越大
@Bean
public IRule getRule(){
    return new RandomRule();
}
重试&超时 Fegin

pom文件中引入依赖

				
            org.springframework.cloud
            spring-cloud-starter-openfeign
        

启动类加入@EnableFeignClients注解

新建FeginService接口并加上@FeignClient注解,value为要调用的服务名称。hi接口url与service-provider提供的接口url一致。

package com.bg.serviceconsumer.service;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;

@FeignClient(value = "service-provider")
public interface FeginService {
    @GetMapping("/hi")
    public String hi();
}

在需要的地方直接注入FeginService进行调用

@Autowired
private FeginService feginService;
@GetMapping("/hi3")
public String hi3() {
  return feginService.hi();
}
服务路由 熔断降级
  • 降级
  • 熔断
  • 隔离
代码仓库 总结
转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/1021289.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号