欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Spring Cloud Ribbon--Spring Cloud负载均衡组件

程序员文章站 2022-06-13 10:45:28
...

扫码关注公众号,领取更多资源
Spring Cloud Ribbon--Spring Cloud负载均衡组件

Spring Cloud Ribbon负载均衡器

        Spring Cloud Ribbon是一个基于HTTP和TCP的客户端负载均衡工具,基于NetFlix Ribbon实现。在经过Spring Cloud封装后,可以快速结合Rest模板请求自动完成客户端的负载均衡调用。
        Spring Cloud Ribbon是一个工具类框架,但是与服务注册中心,配置中心,Api网关需要独立部署有所区别,他几乎存在于每一个SpringCloud的微服务中,因为服务之间的调用,Api网关的请求转发等操作,实际上都是用过Ribbon来实现的。

Ribbon的使用

        接下来使用Ribbon实现一个简单的负载均衡的功能,这里需要使用到之前Eureka笔记中创建的两个项目。
        Eureka学习笔记

        然后创建一个新的Maven项目ribbon-native-demo,在项目中继承Ribbon,pom.xml添加如下依赖:

<dependency>
    <groupId>com.netflix.ribbon</groupId>
    <artifactId>ribbon</artifactId>
    <version>2.2.2</version>
</dependency>
<dependency>
    <groupId>com.netflix.ribbon</groupId>
    <artifactId>ribbon-core</artifactId>
    <version>2.2.2</version>
</dependency>
<dependency>
    <groupId>com.netflix.ribbon</groupId>
    <artifactId>ribbon-loadbalancer</artifactId>
    <version>2.2.2</version>
</dependency>
<dependency>
    <groupId>io.reactivex</groupId>
    <artifactId>rxjava</artifactId>
    <version>1.0.10</version>
</dependency>

        接下来就是创建一个客户端来调用接口并实现负载均衡:

// 服务列表
List<Server> serverList = Lists.newArrayList(new Server("localhost", 8081), new Server("localhost", 8083));
// 构建负载实例
ILoadBalancer loadBalancer = LoadBalancerBuilder.newBuilder().buildFixedServerListLoadBalancer(serverList);
// 调用 5 次来测试效果
for (int i = 0; i < 5; i++) {
    String result = LoadBalancerCommand.<String>builder().withLoadBalancer(loadBalancer).build()
            .submit(new ServerOperation<String>() {
                public Observable<String> call(Server server) {
                    try {
                        String addr = "http://" + server.getHost() + ":" + server.getPort() + "/user/hello";
                        System.out.println(" 调用地址:" + addr);
                        URL url = new URL(addr);
                        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
                        conn.setRequestMethod("GET");
                        conn.connect();
                        InputStream in = conn.getInputStream();
                        byte[] data = new byte[in.available()];
                        in.read(data);
                        return Observable.just(new String(data));
                    } catch (Exception e) {
                        return Observable.error(e);
                    }
                }
            }).toBlocking().first();
    System.out.println(" 调用结果:" + result);
}

        上述例子中使用HttpURLConnection,当然也可以直接使用RibbonClient进行请求,执行程序后输出如下:

Spring Cloud Ribbon--Spring Cloud负载均衡组件

        从以上结果可以看到,负载均衡器还是起到作用的,两个服务都能接收到请求。

Spring Cloud Ribbon结合RestTemplate实现负载均衡

        在上述例子中,我们单独使用了Ribbon进行了负载均衡的调用,SpringCloud在原来的Ribbon上进行了一层封装,使得Ribbon的使用更加简单。

RestTemplate整合Ribbon

        首先尝试一下Get的请求方式,创建一个微服务spring-rest-template,并配置RestTemplate:

@Configuration
public class BeanConfiguration {
    @Bean
    public RestTemplate getRestTemplate() {
        return new RestTemplate();
    }
}

        新建一个HouseController,创建两个请求接口,一个通过@RequestParam传递参数,返回一个对象,另一个通过@PathVariable传递参数,返回字符串,代码如下:

@GetMapping("/house/data")
public HouseInfo getData(@RequestParam("name") String name) {
    return new HouseInfo(1L, "上海" "虹口" "东体小区");
}

@GetMapping("/house/data/{name}")
public String getData2(@PathVariable("name") String name) {
    return name;
}

        新建一个 HouseClientController 用于测试,使用 RestTemplate 来调用我们刚刚定义的两个接口,代码如下:

@GetMapping("/call/data")
public HouseInfo getData(@RequestParam("name") String name) {
    return restTemplate.getForObject( "http://localhost:8081/house/data?name="+ name, HouseInfo.class);
}

@GetMapping("/call/data/{name}")
public String getData2(@PathVariable("name") String name) {
    return restTemplate.getForObject( "http://localhost:8081/house/data/{name}", String.class, name);
}

        除了 getForObject,我们还可以使用 getForEntity 来获取数据,代码如下所示:

@GetMapping("/call/dataEntity")
public HouseInfo getData(@RequestParam("name") String name) {
    ResponseEntity<HouseInfo> responseEntity = restTemplate.getForEntity("http://localhost:8081/house/data?name=" + name, HouseInfo.class);
    if (responseEntity.getStatusCodeValue() == 200) {
        return responseEntity.getBody();
    }
    return null;
}

        getForEntity 中可以获取返回的状态码、请求头等信息,通过 getBody 获取响应的内容。

        接下来看看怎么使用 POST 方式调用接口。在 HouseController 中增加一个 save 方法用来接收 HouseInfo 数据,代码如下:

@PostMapping("/house/save")
public Long addData(@RequestBody HouseInfo houseInfo) {
    System.out.println(houseInfo.getName());
    return 1001L;
}

        接着写调用代码,用 postForObject 来调用,代码如下:

@GetMapping("/call/save")
public Long add() {
    HouseInfo houseInfo = new HouseInfo();
    houseInfo.setCity("上海");
    houseInfo.setRegion("虹口");
    houseInfo.setName("×××");
    Long id = restTemplate.postForObject("http://localhost:8081/house/save", houseInfo, Long.class);
    return id;
}

        除了 getpost 对应的方法之外,RestTemplate 还提供了 putdelete 等操作方法,还有一个比较实用的就是 exchange 方法。exchange 可以执行 getpostputdelete 这 4 种请求方式。
        在 Spring Cloud 项目中集成 Ribbon 只需要在 pom.xml 中加入下面的依赖即可,其实也可以不用配置,因为 Eureka 中已经引用了 Ribbon,代码如下:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>

RestTemplate 负载均衡示例

        前面我们调用接口都是通过具体的接口地址来进行调用,RestTemplate 可以结合 Eureka 来动态发现服务并进行负载均衡的调用。修改 RestTemplate 的配置,增加能够让 RestTemplate 具备负载均衡能力的注解 @LoadBalanced。代码如下:

@Configuration
public class BeanConfiguration {
    @Bean
    @LoadBalanced
    public RestTemplate getRestTemplate() {
        return new RestTemplate();
    }
}

        修改接口调用的代码,将 IP+PORT 改成服务名称,也就是注册到 Eureka 中的名称,代码如下:

@GetMapping("/call/data")
public HouseInfo getData(@RequestParam("name") String name) {
    return restTemplate.getForObject("http://ribbon-eureka-demo/house/data?name=" + name, HouseInfo.class);
}

        接口调用的时候,框架内部会将服务名称替换成具体的服务 IP 信息,然后进行调用。

@LoadBalanced 注解原理

        @LoadBalanced原理就是给RestTemplate新增一个拦截器,在请求之前对请求的地址进行替换,或者根据具体的负载策略选择服务器地址,然后再去调用。
        下面我们来实现一个简单的拦截器,看看在调用接口之前会不会进入这个拦截器。代码如下:

public class MyLoadBalancerInterceptor implements ClientHttpRequestInterceptor {

    private LoadBalancerClient loadBalancer;
    private LoadBalancerRequestFactory requestFactory;

    public MyLoadBalancerInterceptor(LoadBalancerClient loadBalancer, LoadBalancerRequestFactory requestFactory) {
        this.loadBalancer = loadBalancer;
        this.requestFactory = requestFactory;
    }

    public MyLoadBalancerInterceptor(LoadBalancerClient loadBalancer) {
        this(loadBalancer, new LoadBalancerRequestFactory(loadBalancer));
    }

    @Override
    public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
            final ClientHttpRequestExecution execution) throws IOException {
        final URI originalUri = request.getURI();
        String serviceName = originalUri.getHost();
        System.out.println("进入自定义的请求拦截器中" + serviceName);
        Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);

        return this.loadBalancer.execute(serviceName, requestFactory.createRequest(request, body, execution));
    }
}

        拦截器设置好了之后,我们再定义一个注解,并复制@LoadBalanced的代码,改个名称就可以了,代码如下:

@Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Qualifier
public @interface MyLoadBalanced {
}

        然后定义一个配置类,给 RestTemplate 注入拦截器,代码如下:

@Configuration
public class MyLoadBalancerAutoConfiguration {
    @MyLoadBalanced
    @Autowired(required = false)
    private List<RestTemplate> restTemplates = Collections.emptyList();

    @Bean
    public MyLoadBalancerInterceptor myLoadBalancerInterceptor() {
        return new MyLoadBalancerInterceptor();
    }

    @Bean
    public SmartInitializingSingleton myLoadBalancedRestTemplateInitializer() {
        return new SmartInitializingSingleton() {
          @Override
          public void afterSingletonsInstantiated() {
            for (RestTemplate restTemplate : MyLoadBalancerAutoConfiguration.this.restTemplates){
                List<ClientHttpRequestInterceptor> list = new ArrayList<>(restTemplate.getInterceptors());
                list.add(myLoad BalancerInterceptor());
                restTemplate.setInterceptors(list);
            }
          }
        };
    }
}

        维护一个 @MyLoadBalancedRestTemplate 列表,在 SmartInitializingSingleton 中对 RestTemplate 进行拦截器设置。然后改造我们之前的 RestTemplate 配置,将 @LoadBalanced 改成我们自定义的 @MyLoadBalanced,代码如下:

@Bean
//@LoadBalanced
@MyLoadBalanced
public RestTemplate getRestTemplate() {
    return new RestTemplate();
}

        重启服务,访问服务中的接口就可以看到控制台的输出了,这证明在接口调用的时候会进入该拦截器,输出如下:

进入自定义的请求拦截器中 ribbon-eureka-demo

Ribbon API 使用

        当你有一些特殊的需求,想通过 Ribbon 获取对应的服务信息时,可以使用 Load-Balancer Client 来获取,比如你想获取一个 ribbon-eureka-demo 服务的服务地址,可以通过 LoadBalancerClientchoose 方法来选择一个。

@Autowired
private LoadBalancerClient loadBalancer;

@GetMapping("/choose")
public Object chooseUrl() {
    ServiceInstance instance = loadBalancer.choose("ribbon-eureka-demo");
    return instance;
}

        访问接口,可以看到返回的信息如下:

{
    serviceId: "ribbon-eureka-demo",
    server: {
        host: "localhost",
        port: 8081,
        id: "localhost:8081",
        zone: "UNKNOWN",
        readyToServe: true,
        alive: true,
        hostPort: "localhost:8081",
        metaInfo: {
            serverGroup: null,
            serviceIdForDiscovery: null, instanceId: "localhost:8081",
            appName: null
        }
    },
    secure: false, metadata: { }, host: "localhost", port: 8081,
    uri: "http://localhost:8081"
}

Ribbon 饥饿加载

        Ribbon 的客户端是在第一次请求的时候初始化的,如果超时时间比较短的话,初始化 Client 的时间再加上请求接口的时间,就会导致第一次请求超时。
        针对这种情况可以通过配置 eager-load来提前初始化客户端就可以解决这个问题。

ribbon.eager-load.enabled=true
ribbon.eager-load.clients=ribbon-eureka-demo
  • ribbon.eager-load.enabled:开启 Ribbon 的饥饿加载模式。
  • ribbon.eager-load.clients:指定需要饥饿加载的服务名,也就是你需要调用的服务,若有多个则用逗号隔开。

        怎么进行验证呢?网络情况确实不太好模拟,不过通过调试源码的方式即可验证,在 org.springframework.cloud.netflix.ribbon.RibbonAutoConfiguration 中找到对应的代码,代码如下所示:

@Bean
@ConditionalOnProperty(value = "ribbon.eager-load.enabled")
public RibbonApplicationContextInitializer ribbonApplicationContextInitializer() {
    return new RibbonApplicationContextInitializer(springClientFactory(),ribbonEagerLoadProperties.getClients());
}

        在 return 这行设置一个断点,然后以调试的模式启动服务,如果能进入到这个断点的代码这里,就证明配置生效了。

Spring Cloud Ribbon负载均衡策略

        Ribbon作为一款负载均衡框架,默认的负载均衡策略是轮询,但是同时也提供了很多其他的策略,可以提供使用者根据不同的业务自行配置。
        Ribbon中实现的策略代码结构如下图所示:

Spring Cloud Ribbon--Spring Cloud负载均衡组件

  • BestAvailable
            选择一个最小的并发请求的服务,如果服务宕机或者发生错误则跳过,然后重新选择一个ActiveRequestCount最小的服务。
  • AvailabilityFilteringRule
            过滤掉那些一直连接失败的且被标记为circuit tripped的后端 服务,并过滤掉那些高并发的后端 服务 或者使用一个AvailabilityPredicate来包含过滤 服务 的逻辑。其实就是检查Status里记录的各个服务的运行状态。
  • ZoneAvoidanceRule
            使用 ZoneAvoidancePredicateAvailabilityPredicate 来判断是否选择某个服务,前一个判断判定一个Zone的运行性能是否可用,剔除不可用的Zone的所有服务,AvailabilityPredicate 用于过滤掉连接数过多的服务。
  • RandomRule
            随机选择一个服务。
  • RoundRobinRule
            轮询选择,轮询index,选择index对应位置的服务。
  • RetryRule
            在选定的负载均衡策略机上重试机制,也就是说当选定了某个策略进行请求负载时在一个配置时间段内若选择服务不成功,则一直尝试使用subRule的方式选择一个可用的服务。
  • ResponseTimeWeightedRule/WeightedResponseTimeRule
            这两个策略功能相同,只是名字做了修改。根据响应时间分配一个Weight(权重),响应时间越长,Weight 越小,被选中的可能性越低。

自定义负载均衡策略

        通过实现 IRule 接口可以自定义负载策略,主要的选择服务逻辑在 choose 方法中。我们这边只是演示怎么自定义负载策略,所以没写选择的逻辑,直接返回服务列表中第一个服务。具体代码如下:

public class MyRule implements IRule {

    private ILoadBalancer lb;

    @Override
    public Server choose(Object key) {
        List<Server> servers = lb.getAllServers();
        for (Server server : servers) {
            System.out.println(server.getHostPort());
        }
        return servers.get(0);
    }

    @Override
    public void setLoadBalancer(ILoadBalancer lb) {
        this.lb = lb;
    }

    @Override
    public ILoadBalancer getLoadBalancer() {
        return lb;
    }
}

        在Spring Cloud中,可通过配置的方式使用自定义的负载策略,ribbon-config-demo 是调用的服务名称。


ribbon-config-demo.ribbon.NFLoadBalancerRuleClassName=net.biancheng.ribbon_eureka_demo.rule.MyRule

        重启服务,访问调用了其他服务的接口,可以看到控制台的输出信息中已经有了我们自定义策略中输出的服务信息,并且每次都是调用第一个服务。这跟我们的逻辑是相匹配的。

Spring Cloud Ribbon配置

常用配置

  • 禁用Eureka
            当我们在RestTemplate上面注解了@LoadBalanced注解后,就可以使用服务名字来调用接口了,当有多个服务的时候,还能做负载均衡。因为在Eureka中的服务信息已经被拉取到了客户端本地,如果我们不想和Eureka集成,可以通过下面的配置将其禁用:
# 禁用 Eureka
ribbon.eureka.enabled=false

        当我们禁用了 Eureka 之后,就不能使用服务名称去调用接口了,必须指定服务地址。

  • 配置接口地址列表
            上面我们讲了可以禁用 Eureka,禁用之后就需要手动配置调用的服务地址了,配置如下:
# 禁用 Eureka 后手动配置服务地址
ribbon-config-demo.ribbon.listOfServers=localhost:8081,localhost:8083

        这个配置是针对具体服务的,前缀就是服务名称,配置完之后就可以和之前一样使用服务名称来调用接口了。

  • 配置超时时间
            Ribbon 中有两种和时间相关的设置,分别是请求连接的超时时间和请求处理的超时时间,设置规则如下:
# 请求连接的超时时间
ribbon.ConnectTimeout=2000
# 请求处理的超时时间
ribbon.ReadTimeout=5000
# 也可以为每个Ribbon客户端设置不同的超时时间, 通过服务名称进行指定:
ribbon-config-demo.ribbon.ConnectTimeout=2000
ribbon-config-demo.ribbon.ReadTimeout=5000
  • 并发参数
# 最大连接数
ribbon.MaxTotalConnections=500
# 每个host最大连接数
ribbon.MaxConnectionsPerHost=500

代码配置Ribbon

        通过代码的方式配置Ribbon,首先需要创建一个配置类,初始化自定义策略,代码如下:

@Configuration
public class BeanConfiguration {
    @Bean
    public MyRule rule() {
        return new MyRule();
    }
}

        创建一个Ribbon客户端的配置类,关联BeanConfiguration,用name来制定调用的服务名称,代码如下:

@RibbonClient(name = "ribbon-config-demo", configuration = BeanConfiguration.class)
public class RibbonClientConfig {
}

        去掉配置文件中之前配置的策略配置,重启服务后调用,发现调用结果与MyRule之中的配置一样。

配置文件方式配置Ribbon

        我们还可以通过如下方式对Ribbon进行配置:

<clientName>.ribbon.NFLoadBalancerClassName: Should implement ILoadBalancer(负载均衡器操作接口)
<clientName>.ribbon.NFLoadBalancerRuleClassName: Should implement IRule(负载均衡算法)
<clientName>.ribbon.NFLoadBalancerPingClassName: Should implement IPing(服务可用性检查)
<clientName>.ribbon.NIWSServerListClassName: Should implement ServerList(服务列表获取)
<clientName>.ribbon.NIWSServerListFilterClassName: Should implement ServerList­Filter(服务列表的过滤)

重试机制

        在集群环境中,用多个节点来提供服务,难免会出现宕机等服务故障。使用nginx多负载均衡的时候,Nginx在转发请求失败后会重新将请求转发到其他服务实例上去,这样对用户影响也比较小。
        由于Eureka是基于AP原则构建的,牺牲了数据的一致性,每个Eureka服务都会保存注册的服务信息,当注册的服务客户端与Eureka心跳无法保持时,可能是网络原因,也可能是服务器挂掉了。
        因为Eureka会在一段时间内保存注册信息,所以客户端拿到的可能是有问题的服务信息,所以Ribbon可能发送一个导致失败的请求。
        这种情况下可以使用重试机制来避免,就是当Ribbon发现请求不可达时,重新请求另外的服务。

RetryRule重试

        最简单的重试方法就是利用Ribbon自带的重试机制策略,只需要指定某个服务的负载均衡策略为重试策略即可,配置如下:

ribbon-config-demo.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RetryRule

Spring Retry重试

        我们还可以通过集成Spring Retry来进行重试操作。在pom.xml中添加Spring Retry依赖,如下:

<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
</dependency>

        在配置文件中配置相关信息:

# 对当前实例的重试次数
ribbon.maxAutoRetries=1
# 切换实例的重试次数
ribbon.maxAutoRetriesNextServer=3
# 对所有操作请求都进行重试
ribbon.okToRetryOnAllOperations=true
# 对Http响应码进行重试
ribbon.retryableStatusCodes=500,404,502

Spring Cloud Ribbon负载均衡器

        Spring Cloud Ribbon是一个基于HTTP和TCP的客户端负载均衡工具,基于NetFlix Ribbon实现。在经过Spring Cloud封装后,可以快速结合Rest模板请求自动完成客户端的负载均衡调用。
        Spring Cloud Ribbon是一个工具类框架,但是与服务注册中心,配置中心,Api网关需要独立部署有所区别,他几乎存在于每一个SpringCloud的微服务中,因为服务之间的调用,Api网关的请求转发等操作,实际上都是用过Ribbon来实现的。

Ribbon的使用

        接下来使用Ribbon实现一个简单的负载均衡的功能,这里需要使用到之前Eureka笔记中创建的两个项目。
        Eureka学习笔记

        然后创建一个新的Maven项目ribbon-native-demo,在项目中继承Ribbon,pom.xml添加如下依赖:

<dependency>
    <groupId>com.netflix.ribbon</groupId>
    <artifactId>ribbon</artifactId>
    <version>2.2.2</version>
</dependency>
<dependency>
    <groupId>com.netflix.ribbon</groupId>
    <artifactId>ribbon-core</artifactId>
    <version>2.2.2</version>
</dependency>
<dependency>
    <groupId>com.netflix.ribbon</groupId>
    <artifactId>ribbon-loadbalancer</artifactId>
    <version>2.2.2</version>
</dependency>
<dependency>
    <groupId>io.reactivex</groupId>
    <artifactId>rxjava</artifactId>
    <version>1.0.10</version>
</dependency>

        接下来就是创建一个客户端来调用接口并实现负载均衡:

// 服务列表
List<Server> serverList = Lists.newArrayList(new Server("localhost", 8081), new Server("localhost", 8083));
// 构建负载实例
ILoadBalancer loadBalancer = LoadBalancerBuilder.newBuilder().buildFixedServerListLoadBalancer(serverList);
// 调用 5 次来测试效果
for (int i = 0; i < 5; i++) {
    String result = LoadBalancerCommand.<String>builder().withLoadBalancer(loadBalancer).build()
            .submit(new ServerOperation<String>() {
                public Observable<String> call(Server server) {
                    try {
                        String addr = "http://" + server.getHost() + ":" + server.getPort() + "/user/hello";
                        System.out.println(" 调用地址:" + addr);
                        URL url = new URL(addr);
                        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
                        conn.setRequestMethod("GET");
                        conn.connect();
                        InputStream in = conn.getInputStream();
                        byte[] data = new byte[in.available()];
                        in.read(data);
                        return Observable.just(new String(data));
                    } catch (Exception e) {
                        return Observable.error(e);
                    }
                }
            }).toBlocking().first();
    System.out.println(" 调用结果:" + result);
}

        上述例子中使用HttpURLConnection,当然也可以直接使用RibbonClient进行请求,执行程序后输出如下:

Spring Cloud Ribbon--Spring Cloud负载均衡组件

        从以上结果可以看到,负载均衡器还是起到作用的,两个服务都能接收到请求。

Spring Cloud Ribbon结合RestTemplate实现负载均衡

        在上述例子中,我们单独使用了Ribbon进行了负载均衡的调用,SpringCloud在原来的Ribbon上进行了一层封装,使得Ribbon的使用更加简单。

RestTemplate整合Ribbon

        首先尝试一下Get的请求方式,创建一个微服务spring-rest-template,并配置RestTemplate:

@Configuration
public class BeanConfiguration {
    @Bean
    public RestTemplate getRestTemplate() {
        return new RestTemplate();
    }
}

        新建一个HouseController,创建两个请求接口,一个通过@RequestParam传递参数,返回一个对象,另一个通过@PathVariable传递参数,返回字符串,代码如下:

@GetMapping("/house/data")
public HouseInfo getData(@RequestParam("name") String name) {
    return new HouseInfo(1L, "上海" "虹口" "东体小区");
}

@GetMapping("/house/data/{name}")
public String getData2(@PathVariable("name") String name) {
    return name;
}

        新建一个 HouseClientController 用于测试,使用 RestTemplate 来调用我们刚刚定义的两个接口,代码如下:

@GetMapping("/call/data")
public HouseInfo getData(@RequestParam("name") String name) {
    return restTemplate.getForObject( "http://localhost:8081/house/data?name="+ name, HouseInfo.class);
}

@GetMapping("/call/data/{name}")
public String getData2(@PathVariable("name") String name) {
    return restTemplate.getForObject( "http://localhost:8081/house/data/{name}", String.class, name);
}

        除了 getForObject,我们还可以使用 getForEntity 来获取数据,代码如下所示:

@GetMapping("/call/dataEntity")
public HouseInfo getData(@RequestParam("name") String name) {
    ResponseEntity<HouseInfo> responseEntity = restTemplate.getForEntity("http://localhost:8081/house/data?name=" + name, HouseInfo.class);
    if (responseEntity.getStatusCodeValue() == 200) {
        return responseEntity.getBody();
    }
    return null;
}

        getForEntity 中可以获取返回的状态码、请求头等信息,通过 getBody 获取响应的内容。

        接下来看看怎么使用 POST 方式调用接口。在 HouseController 中增加一个 save 方法用来接收 HouseInfo 数据,代码如下:

@PostMapping("/house/save")
public Long addData(@RequestBody HouseInfo houseInfo) {
    System.out.println(houseInfo.getName());
    return 1001L;
}

        接着写调用代码,用 postForObject 来调用,代码如下:

@GetMapping("/call/save")
public Long add() {
    HouseInfo houseInfo = new HouseInfo();
    houseInfo.setCity("上海");
    houseInfo.setRegion("虹口");
    houseInfo.setName("×××");
    Long id = restTemplate.postForObject("http://localhost:8081/house/save", houseInfo, Long.class);
    return id;
}

        除了 getpost 对应的方法之外,RestTemplate 还提供了 putdelete 等操作方法,还有一个比较实用的就是 exchange 方法。exchange 可以执行 getpostputdelete 这 4 种请求方式。
        在 Spring Cloud 项目中集成 Ribbon 只需要在 pom.xml 中加入下面的依赖即可,其实也可以不用配置,因为 Eureka 中已经引用了 Ribbon,代码如下:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>

RestTemplate 负载均衡示例

        前面我们调用接口都是通过具体的接口地址来进行调用,RestTemplate 可以结合 Eureka 来动态发现服务并进行负载均衡的调用。修改 RestTemplate 的配置,增加能够让 RestTemplate 具备负载均衡能力的注解 @LoadBalanced。代码如下:

@Configuration
public class BeanConfiguration {
    @Bean
    @LoadBalanced
    public RestTemplate getRestTemplate() {
        return new RestTemplate();
    }
}

        修改接口调用的代码,将 IP+PORT 改成服务名称,也就是注册到 Eureka 中的名称,代码如下:

@GetMapping("/call/data")
public HouseInfo getData(@RequestParam("name") String name) {
    return restTemplate.getForObject("http://ribbon-eureka-demo/house/data?name=" + name, HouseInfo.class);
}

        接口调用的时候,框架内部会将服务名称替换成具体的服务 IP 信息,然后进行调用。

@LoadBalanced 注解原理

        @LoadBalanced原理就是给RestTemplate新增一个拦截器,在请求之前对请求的地址进行替换,或者根据具体的负载策略选择服务器地址,然后再去调用。
        下面我们来实现一个简单的拦截器,看看在调用接口之前会不会进入这个拦截器。代码如下:

public class MyLoadBalancerInterceptor implements ClientHttpRequestInterceptor {

    private LoadBalancerClient loadBalancer;
    private LoadBalancerRequestFactory requestFactory;

    public MyLoadBalancerInterceptor(LoadBalancerClient loadBalancer, LoadBalancerRequestFactory requestFactory) {
        this.loadBalancer = loadBalancer;
        this.requestFactory = requestFactory;
    }

    public MyLoadBalancerInterceptor(LoadBalancerClient loadBalancer) {
        this(loadBalancer, new LoadBalancerRequestFactory(loadBalancer));
    }

    @Override
    public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
            final ClientHttpRequestExecution execution) throws IOException {
        final URI originalUri = request.getURI();
        String serviceName = originalUri.getHost();
        System.out.println("进入自定义的请求拦截器中" + serviceName);
        Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);

        return this.loadBalancer.execute(serviceName, requestFactory.createRequest(request, body, execution));
    }
}

        拦截器设置好了之后,我们再定义一个注解,并复制@LoadBalanced的代码,改个名称就可以了,代码如下:

@Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Qualifier
public @interface MyLoadBalanced {
}

        然后定义一个配置类,给 RestTemplate 注入拦截器,代码如下:

@Configuration
public class MyLoadBalancerAutoConfiguration {
    @MyLoadBalanced
    @Autowired(required = false)
    private List<RestTemplate> restTemplates = Collections.emptyList();

    @Bean
    public MyLoadBalancerInterceptor myLoadBalancerInterceptor() {
        return new MyLoadBalancerInterceptor();
    }

    @Bean
    public SmartInitializingSingleton myLoadBalancedRestTemplateInitializer() {
        return new SmartInitializingSingleton() {
          @Override
          public void afterSingletonsInstantiated() {
            for (RestTemplate restTemplate : MyLoadBalancerAutoConfiguration.this.restTemplates){
                List<ClientHttpRequestInterceptor> list = new ArrayList<>(restTemplate.getInterceptors());
                list.add(myLoad BalancerInterceptor());
                restTemplate.setInterceptors(list);
            }
          }
        };
    }
}

        维护一个 @MyLoadBalancedRestTemplate 列表,在 SmartInitializingSingleton 中对 RestTemplate 进行拦截器设置。然后改造我们之前的 RestTemplate 配置,将 @LoadBalanced 改成我们自定义的 @MyLoadBalanced,代码如下:

@Bean
//@LoadBalanced
@MyLoadBalanced
public RestTemplate getRestTemplate() {
    return new RestTemplate();
}

        重启服务,访问服务中的接口就可以看到控制台的输出了,这证明在接口调用的时候会进入该拦截器,输出如下:

进入自定义的请求拦截器中 ribbon-eureka-demo

Ribbon API 使用

        当你有一些特殊的需求,想通过 Ribbon 获取对应的服务信息时,可以使用 Load-Balancer Client 来获取,比如你想获取一个 ribbon-eureka-demo 服务的服务地址,可以通过 LoadBalancerClientchoose 方法来选择一个。

@Autowired
private LoadBalancerClient loadBalancer;

@GetMapping("/choose")
public Object chooseUrl() {
    ServiceInstance instance = loadBalancer.choose("ribbon-eureka-demo");
    return instance;
}

        访问接口,可以看到返回的信息如下:

{
    serviceId: "ribbon-eureka-demo",
    server: {
        host: "localhost",
        port: 8081,
        id: "localhost:8081",
        zone: "UNKNOWN",
        readyToServe: true,
        alive: true,
        hostPort: "localhost:8081",
        metaInfo: {
            serverGroup: null,
            serviceIdForDiscovery: null, instanceId: "localhost:8081",
            appName: null
        }
    },
    secure: false, metadata: { }, host: "localhost", port: 8081,
    uri: "http://localhost:8081"
}

Ribbon 饥饿加载

        Ribbon 的客户端是在第一次请求的时候初始化的,如果超时时间比较短的话,初始化 Client 的时间再加上请求接口的时间,就会导致第一次请求超时。
        针对这种情况可以通过配置 eager-load来提前初始化客户端就可以解决这个问题。

ribbon.eager-load.enabled=true
ribbon.eager-load.clients=ribbon-eureka-demo
  • ribbon.eager-load.enabled:开启 Ribbon 的饥饿加载模式。
  • ribbon.eager-load.clients:指定需要饥饿加载的服务名,也就是你需要调用的服务,若有多个则用逗号隔开。

        怎么进行验证呢?网络情况确实不太好模拟,不过通过调试源码的方式即可验证,在 org.springframework.cloud.netflix.ribbon.RibbonAutoConfiguration 中找到对应的代码,代码如下所示:

@Bean
@ConditionalOnProperty(value = "ribbon.eager-load.enabled")
public RibbonApplicationContextInitializer ribbonApplicationContextInitializer() {
    return new RibbonApplicationContextInitializer(springClientFactory(),ribbonEagerLoadProperties.getClients());
}

        在 return 这行设置一个断点,然后以调试的模式启动服务,如果能进入到这个断点的代码这里,就证明配置生效了。

Spring Cloud Ribbon负载均衡策略

        Ribbon作为一款负载均衡框架,默认的负载均衡策略是轮询,但是同时也提供了很多其他的策略,可以提供使用者根据不同的业务自行配置。
        Ribbon中实现的策略代码结构如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dr9Eijdy-1606194193364)(SpringCloud.assets/5-1ZR1155050B0.png)]

  • BestAvailable
            选择一个最小的并发请求的服务,如果服务宕机或者发生错误则跳过,然后重新选择一个ActiveRequestCount最小的服务。
  • AvailabilityFilteringRule
            过滤掉那些一直连接失败的且被标记为circuit tripped的后端 服务,并过滤掉那些高并发的后端 服务 或者使用一个AvailabilityPredicate来包含过滤 服务 的逻辑。其实就是检查Status里记录的各个服务的运行状态。
  • ZoneAvoidanceRule
            使用 ZoneAvoidancePredicateAvailabilityPredicate 来判断是否选择某个服务,前一个判断判定一个Zone的运行性能是否可用,剔除不可用的Zone的所有服务,AvailabilityPredicate 用于过滤掉连接数过多的服务。
  • RandomRule
            随机选择一个服务。
  • RoundRobinRule
            轮询选择,轮询index,选择index对应位置的服务。
  • RetryRule
            在选定的负载均衡策略机上重试机制,也就是说当选定了某个策略进行请求负载时在一个配置时间段内若选择服务不成功,则一直尝试使用subRule的方式选择一个可用的服务。
  • ResponseTimeWeightedRule/WeightedResponseTimeRule
            这两个策略功能相同,只是名字做了修改。根据响应时间分配一个Weight(权重),响应时间越长,Weight 越小,被选中的可能性越低。

自定义负载均衡策略

        通过实现 IRule 接口可以自定义负载策略,主要的选择服务逻辑在 choose 方法中。我们这边只是演示怎么自定义负载策略,所以没写选择的逻辑,直接返回服务列表中第一个服务。具体代码如下:

public class MyRule implements IRule {

    private ILoadBalancer lb;

    @Override
    public Server choose(Object key) {
        List<Server> servers = lb.getAllServers();
        for (Server server : servers) {
            System.out.println(server.getHostPort());
        }
        return servers.get(0);
    }

    @Override
    public void setLoadBalancer(ILoadBalancer lb) {
        this.lb = lb;
    }

    @Override
    public ILoadBalancer getLoadBalancer() {
        return lb;
    }
}

        在Spring Cloud中,可通过配置的方式使用自定义的负载策略,ribbon-config-demo 是调用的服务名称。


ribbon-config-demo.ribbon.NFLoadBalancerRuleClassName=net.biancheng.ribbon_eureka_demo.rule.MyRule

        重启服务,访问调用了其他服务的接口,可以看到控制台的输出信息中已经有了我们自定义策略中输出的服务信息,并且每次都是调用第一个服务。这跟我们的逻辑是相匹配的。

Spring Cloud Ribbon配置

常用配置

  • 禁用Eureka
            当我们在RestTemplate上面注解了@LoadBalanced注解后,就可以使用服务名字来调用接口了,当有多个服务的时候,还能做负载均衡。因为在Eureka中的服务信息已经被拉取到了客户端本地,如果我们不想和Eureka集成,可以通过下面的配置将其禁用:
# 禁用 Eureka
ribbon.eureka.enabled=false

        当我们禁用了 Eureka 之后,就不能使用服务名称去调用接口了,必须指定服务地址。

  • 配置接口地址列表
            上面我们讲了可以禁用 Eureka,禁用之后就需要手动配置调用的服务地址了,配置如下:
# 禁用 Eureka 后手动配置服务地址
ribbon-config-demo.ribbon.listOfServers=localhost:8081,localhost:8083

        这个配置是针对具体服务的,前缀就是服务名称,配置完之后就可以和之前一样使用服务名称来调用接口了。

  • 配置超时时间
            Ribbon 中有两种和时间相关的设置,分别是请求连接的超时时间和请求处理的超时时间,设置规则如下:
# 请求连接的超时时间
ribbon.ConnectTimeout=2000
# 请求处理的超时时间
ribbon.ReadTimeout=5000
# 也可以为每个Ribbon客户端设置不同的超时时间, 通过服务名称进行指定:
ribbon-config-demo.ribbon.ConnectTimeout=2000
ribbon-config-demo.ribbon.ReadTimeout=5000
  • 并发参数
# 最大连接数
ribbon.MaxTotalConnections=500
# 每个host最大连接数
ribbon.MaxConnectionsPerHost=500

代码配置Ribbon

        通过代码的方式配置Ribbon,首先需要创建一个配置类,初始化自定义策略,代码如下:

@Configuration
public class BeanConfiguration {
    @Bean
    public MyRule rule() {
        return new MyRule();
    }
}

        创建一个Ribbon客户端的配置类,关联BeanConfiguration,用name来制定调用的服务名称,代码如下:

@RibbonClient(name = "ribbon-config-demo", configuration = BeanConfiguration.class)
public class RibbonClientConfig {
}

        去掉配置文件中之前配置的策略配置,重启服务后调用,发现调用结果与MyRule之中的配置一样。

配置文件方式配置Ribbon

        我们还可以通过如下方式对Ribbon进行配置:

<clientName>.ribbon.NFLoadBalancerClassName: Should implement ILoadBalancer(负载均衡器操作接口)
<clientName>.ribbon.NFLoadBalancerRuleClassName: Should implement IRule(负载均衡算法)
<clientName>.ribbon.NFLoadBalancerPingClassName: Should implement IPing(服务可用性检查)
<clientName>.ribbon.NIWSServerListClassName: Should implement ServerList(服务列表获取)
<clientName>.ribbon.NIWSServerListFilterClassName: Should implement ServerList­Filter(服务列表的过滤)

重试机制

        在集群环境中,用多个节点来提供服务,难免会出现宕机等服务故障。使用nginx多负载均衡的时候,Nginx在转发请求失败后会重新将请求转发到其他服务实例上去,这样对用户影响也比较小。
        由于Eureka是基于AP原则构建的,牺牲了数据的一致性,每个Eureka服务都会保存注册的服务信息,当注册的服务客户端与Eureka心跳无法保持时,可能是网络原因,也可能是服务器挂掉了。
        因为Eureka会在一段时间内保存注册信息,所以客户端拿到的可能是有问题的服务信息,所以Ribbon可能发送一个导致失败的请求。
        这种情况下可以使用重试机制来避免,就是当Ribbon发现请求不可达时,重新请求另外的服务。

RetryRule重试

        最简单的重试方法就是利用Ribbon自带的重试机制策略,只需要指定某个服务的负载均衡策略为重试策略即可,配置如下:

ribbon-config-demo.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RetryRule

Spring Retry重试

        我们还可以通过集成Spring Retry来进行重试操作。在pom.xml中添加Spring Retry依赖,如下:

<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
</dependency>

        在配置文件中配置相关信息:

# 对当前实例的重试次数
ribbon.maxAutoRetries=1
# 切换实例的重试次数
ribbon.maxAutoRetriesNextServer=3
# 对所有操作请求都进行重试
ribbon.okToRetryOnAllOperations=true
# 对Http响应码进行重试
ribbon.retryableStatusCodes=500,404,502