开端

目前主流的负载方案一般为两种:一种是集中式的负载均衡,在消费者和服务提供方中间使用独立的代理方式进行负载,有硬件(F5),也有软件(Nginx)。另一种则是客户端自己做负载均衡。我觉得使用Nginx也是一个不错的方式,但是这样我们还要令开一块专题去学习,既然在学Spring Cloud,我们就来试试使用Netflix开源的Ribbon做负载。

另外ribbon的内容不是特别多,我们在此还会学习另一个知识点,声明式REST客户端Feign。

Ribbon

开始学习Ribbon。

了解

首先还是来了解一下Ribbon的基本模块:

  • ribbon-loadbalancer:负载均衡模块,内置的负载均衡算法都实现在其中。
  • ribbon-eureka:基于Eureka封装的模块,能够快速集成。
  • ribbon-transport:基于Netty实现多协议的支持,比如HTTP、TCP、UDP等。
  • ribbon-httpclient:基于Apache HttpClient封装的REST客户端,集成了负载均衡模块,可以直接在项目中调用接口。
  • ribbon-example:代码示例,供学习。
  • ribbon-core:核心且通用的代码,客户端API的一些配置和其他API的定义。

使用

浅尝

结束初步的了解,咱们直接开始上手。关于我们之前Eureka的项目文件,大家可以放在一起,右键每一个pom文件后使用add as maven project功能构建。如此:

截屏2021-03-17 下午1.23.07

这样一来就方便大家管理项目,以及统一启动和终止。我们首先复制一份eureka-client项目,创建一个消费者集群。启动类、控制层、配置文件、Maven文件的修改这里就不写了,都是些小细节。我将这个项目的端口设置为8084,然后启动,回到注册中心就可以看到集群成功。

截屏2021-03-17 下午2.10.50

现在我们需要右键外面的大项目文件夹,重新创建一个ribbon-native-demo项目,创建方式同client。为pom添加依赖:

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

复制一份client的yml,修改应用名为ribbon-native-demo,端口为8083。然后写一个测试类调用hello接口。

public static void main(String[] args) {
    List<Server> serverList = Lists.newArrayList(
            new Server("localhost", 8081),
            new Server("localhost", 8084)
    );
    ILoadBalancer loadBalancer = LoadBalancerBuilder.newBuilder()
            .buildFixedServerListLoadBalancer(serverList);
    for (int i = 0; i < 6; i++) {
        String result = LoadBalancerCommand.<String>builder()
                .withLoadBalancer(loadBalancer)
                .build()
                .submit(new ServerOperation<String>() {
                    @Override
                    public Observable<String> call(Server server) {
                        try {
                            String addr = "http://" + server.getHost() + ":" +
                                    server.getPort() + "/test/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);
    }
}

跑起来之后我们看到结果:

截屏2021-03-17 下午2.21.35

循环六次,三次分配给了8081端口,另外三次分配给了8084端口,实现了负载均衡。这时候,各位同学有没有想起来在上一篇中我们添加了服务消费者,使用了RestTemplate的那个,还记不记得我们在调用接口时并没有指明端口号,而是直接使用服务的名字来调用,这时候负载的作用就体现出来了。仔细领悟,RestTemplate的具体用法我就不展开了。

值得一提的是,如果想要脱离Eureka使用Ribbon,请加入下列pom,由于我们测试的时候是使用了Eureka的,所以没有配置:

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

辄止

说到底,我们在使用RestTemplate调用接口时,最关键的步骤在于@LoadBalanced这一个注解。多亏Spring Cloud为我们做了大量的底层工作,才得以如此方便的使用。其实它的原理莫过于拦截请求-替换地址-选择均衡策略-调用,这几步。

由于源码像裹脚布一样又臭又长,难以理解,这里就不做分析,大体知道这个逻辑即可。

拓展

接下来从我们方便接触的角度尝试一下Ribbon的一些拓展功能。

Ribbon API

有时候面对一些特殊的需求,可能需要通过Ribbon获取对应的服务信息,可以使用LoadBalancerClient来获取,比如你想获取一个ribbon-eureka-demo服务的服务地址,可以通过LoadBalancerClient的choose方法来实现。

@Autowired
private LoadBalancerClient loadBalancerClient;

@GetMapping("/choose")
public Object chooseUrl() {
    return loadBalancerClient.choose("eureka-client");
}

这样我们就可以获取到eureka-client这一个服务的信息,我们使用Postman测试结果如下:

{
    "metadata": {
        "management.port": "8081"
    },
    "secure": false,
    "scheme": "http",
    "host": "xxx.xxx.xx.xx",
    "port": 8081,
    "uri": "http://xxx.xxx.xx.xx:8081",
    "serviceId": "EUREKA-CLIENT",
    "instanceInfo": {
        "instanceId": "eureka-client:xxx.xxx.xx.xx:8081",
        "app": "EUREKA-CLIENT",
        "appGroupName": null,
        "ipAddr": "192.168.43.87",
        "sid": "na",
        "homePageUrl": "http://xxx.xxx.xx.xx:8081/",
        "statusPageUrl": "http://xxx.xxx.xx.xx:8081/actuator/info",
        "healthCheckUrl": "http://xxx.xxx.xx.xx:8081/actuator/health",
        "secureHealthCheckUrl": null,
        "vipAddress": "eureka-client",
        "secureVipAddress": "eureka-client",
        "countryId": 1,
        "dataCenterInfo": {
            "@class": "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo",
            "name": "MyOwn"
        },
        "hostName": "xxx.xxx.xx.xx",
        "status": "UP",
        "overriddenStatus": "UNKNOWN",
        "leaseInfo": {
            "renewalIntervalInSecs": 5,
            "durationInSecs": 5,
            "registrationTimestamp": 1616034670752,
            "lastRenewalTimestamp": 1616035593582,
            "evictionTimestamp": 0,
            "serviceUpTimestamp": 1616034670754
        },
        "isCoordinatingDiscoveryServer": false,
        "metadata": {
            "management.port": "8081"
        },
        "lastUpdatedTimestamp": 1616034670755,
        "lastDirtyTimestamp": 1616034670568,
        "actionType": "ADDED",
        "asgName": null
    },
    "instanceId": "eureka-client:xxx.xxx.xx.xx:8081"
}

Eager Load

加入网络不好,往往会令Ribbon的第一次调用超时,我们可以将超时时间改长,也可以禁用超时,但是最新版的Spring Cloud中提供了饥饿加载的策略解决这一问题。要开启饥饿加载,往yml中加入如下配置:

ribbon:
  eager-load:
    enabled: true
    clients: ribbon-native-demo

自定义

差不多也算是Ribbon入门了,我们接下来了解一下他最核心的负载均衡策略。Ribbon默认的负载策略是轮询,也就是和我们之前测试中的结果一样,每次调用都依次分配给集群中的服务,谁都能吃到这一口瓜。但是实际的线上高并发状态,并不是这么简单就能解决的,因此Ribbon还提供了如下的几个策略:

  • BestAvailable:选择一个最小并发请求的Server,逐个考察,若被标记为错误则跳过。
  • AvailabilityFilteringRule:检查Status里记录的各个Server的运行状态,过滤掉高并发的后端Server。
  • ZoneAvoidanceRule:判断Zone的运行性能是否可用,剔除掉不可用的并且过滤掉连接数过多的。
  • RandomRule:顾名思义,随机选择,可能有惊喜,可能。
  • RoundRobinRule:即默认的轮询方式。
  • RetryRule:重试机制策略,若选择Server不成功则一直尝试使用subRule的方式选择一个可用Server。
  • WeightedResponseTimeRule:根据响应时间分配一个权重,时间越长权重越小,被选中的概率也越小。
ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule

如此设置策略为随机。

重试

集群环境中,用多个节点提供服务,难免会遇到故障。由于Eureka是基于AP原则构建的,牺牲了数据的一致性,每个Eureka服务都会保存注册的服务信息,当心跳无法接受到时,仍会保存信息,这个时候Ribbon就可能拿到已经挂掉的服务信息,导致请求失败。我们可以为其配置重试机制。

首先在pom中添加依赖:

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

总结

总的来说,Ribbon是一款非常优秀的客户端负载均衡组件,但是在Spring Cloud里结合RestTemplate使用Ribbon还是相对麻烦的,所以接下来我们看看如何优雅的使用Feign去调用服务中的接口。

Feign

开始学习Feign。

了解

Feign是一个声明式的REST客户端,让接口的调用更加简单,它提供了HTTP请求的模板,通过编写简单的接口和插入注解,就可以定义好HTTP请求的参数、格式、地址等信息。

而Feign则会完全代理HTTP请求,只需要像调用方法一样调用它就可以完成服务请求及相关处理。

集成

首先依旧是添加依赖:

<!--Feign-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

在启动来加上注解@EnableFeignClients。如果接口定义和启动类不再同一个包名下,还需要指定扫描的包名@EnableFeignClients(basePackages = "com.ky.kevin")

然后定义一个Feign的客户端,以接口的形式存在。

package com.ky.kevin.feign;

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

@FeignClient(value = "eureka-client")
public interface UserRemoteClient {
    @GetMapping("/test/hello")
    String hello();
}

主要就是一个注解,value填写对应的eureka服务提供者。将提供的所有服务都单独抽出来,使用注解标记它的url即可。接下来回到控制层,之前写的大段大段测试语句可以删除了,创建的RestTemplate也可以删除了,写上下面的内容。

package com.ky.kevin.controller;

import com.ky.kevin.feign.UserRemoteClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/article")
public class HelloController {
    @Autowired
    private UserRemoteClient userRemoteClient;

    @GetMapping("/callHello")
    private String callHello() {
        return userRemoteClient.hello();
    }
}

接下来启动项目,多次访问路径,如果添加了输出信息可以发现访问成功而且自动完成了负载均衡。

截屏2021-03-18 下午1.14.50

如果初步看这些操作,可能会觉得多次一局,多嵌套了一层,到时候修改接口会复杂不少。但是这样调用接口确实非常优雅,相当于我们之前每次调用都需要写上服务的名称(一开始甚至要写主机号和端口号),现在我们把这一块内容抽出来作为接口的注解,极大的简化了调用,并且这样一来也更加方便我们的视觉上的审阅。

自定义

讲讲自定义的配置方式。

日志

好像都是老生常谈的内容了,还是同一个问题,遇到Bug怎么办,那就定义一个配置类,把请求信息输出。

package com.ky.kevin.configuration;

import feign.Logger;
import org.springframework.context.annotation.Bean;

public class FeignConfiguration {
    @Bean
    Logger.Level feignLoggerLevel() {
        return Logger.Level.FULL;
    }
}

接着回到FeignClient,将注解修改为@FeignClient(value = "eureka-client", configuration = FeignConfiguration.class),表示使用指定的配置类。最后在yml中写上日志级别的显示如下:

logging:
  level:
    com.ky.kevin.feign.UserRemoteClient: DEBUG

再次启动程序,调用接口我们可以看到DEBUG信息:

截屏2021-03-18 下午1.58.25

超时

修改连接超时时间和读取超时时间。

@Bean
public Request.Options options() {
    return new Request.Options(5000, 10000);
}

压缩

开启压缩以有效节约网络资源,提升接口性能,我们可以配置GZIP来压缩数据,在yml中添加一下配置:

feign:
  compression:
    request:
      enabled: true
      mime-types:
        - text/xml
        - application/xml
        - application/json
      min-request-size: 2048
    response:
      enabled: true

总结

通过本节,我们已经对Feign有了一个初步的了解,通过Feign简化调用接口的方式,还可以和Eureka何Ribbon轻松集成,所以还是非常值得学习的,相信接下来的项目实战中我们还有很多的机会可以接触这一块内容。

大总结

本文一共学习了Ribbon和Feign两块知识。我们现在已经可以自己创建注册中心集群,创建服务提供者和消费者集群。使用Ribbon实现有效的负载均衡策略,最后再利用Feign优化接口调用。从一开始的迷茫,无从下手,到现在雾霾的驱散,前方的道路越来越清晰。虽然这部分的内容在实战当中肯定还是经不起考验的,但是毕竟我们也只是初步的一个了解,最终肯定是由实践来检验这些真理。

本来想试试Hystrix来保护高并发下的服务安全,但是目前我们手上的小项目好像并不会涉及到这方面的内容,所以放到最后有时间我们再回来,接下来的内容又是一块比较重要比较难的--Zuul网关,敬请期待。

:)