Ribbon实战

2023年 7月 13日 70.4k 0

  RibbonNetflix开发的客户端负载均衡器,为Ribbon配置服务提供者地址列表后,Ribbon就可以基于某种负载均衡策略算法让服务消费者去请求提供者。Ribbon默认为我们提供了很多负载均衡算法,例如轮询、随机等,也可以实现自定义负载均衡算法。

一、Ribbon的组成

官网首页:github.com/Netflix/rib…

  • ribbon-core:核心的通用性代码

  • ribbon-eureka:基于eureka封装的模块,能快速集成eureka

  • ribbon-examples:学习示例

  • ribbon-httpclient:基于Apache HttpClient封装的rest客户端,集成了负载均衡模块

  • ribbon-loadbalancer:负载均衡算法模块

  • ribbon-transport:基于netty实现多协议的支持,比如httptcpudp

1. 负载均衡形式

负载均衡的目的就是使集群中服务器的负载保持在稳定高效的状态,从而提供整个系统的请求处理能力。通常分为客户端负载均衡服务端负载均衡

(1)客户端负载均衡

在客户端负载均衡中,所有的客户端节点都有一份自己要访问的服务端地址列表,这些服务端地址列表都是从服务注册中心拉取的

spring-cloud-all.drawio.png

(2)服务端负载均衡

在服务端负载均衡中,在客户端和服务端中间使用代理,比如Nginx。但客户端节点只知道服务代理地址,服务代理则知道所有服务端的地址

spring-cloud-all-服务端负载均衡.drawio.png

总结: 客户端负载均衡和服务端负载均衡最大的区别在于 服务端地址列表的存储位置,以及负载算法在哪里

二、负载均衡算法

1. RoundRobinRule

轮询策略,Ribbon 默认采用的策略。若经过一轮轮询没有找到可用的 provider,其最多轮询 10 轮,若最终还没有找到,则返回 null

2. RandomRule

随机策略,从所有可用的 provider 中随机选择一个

3. RetryRule

重试策略,先按照 RoundRobinRule 策略获取 provider,若获取失败,则在指定的时限内重试。默认的时限为 500 毫秒。

4. BestAvailableRule

最低并发策略,会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务。逐个找服务,如果断路器打开,则忽略

6. AvailabilityFilteringRule

可用过滤策略,会先过滤掉多次访问故障而处于熔断状态的服务、过滤并发的连接数量超过阀值得服务,然后对剩余的服务列表按照轮询策略进行访问

7. ZoneAvoidanceRule

区域权衡策略,复合判断 Server 所在区域的性能和 Server 的可用性,轮询选择服务器

8. WeightedResponseTimeRule

响应时间加权策略,据平均响应时间计算所有的服务的权重,响应时间越快服务权重越大,容易被选中的概率就越高。刚启动时,如果统计信息不中,则使用 RoundRobinRule,等统计的信息足够了会自动的切换到 WeightedResponseTimeRule 。响应时间长,权重低,被选择的概率低

三、Ribbon 负载均衡搭建

3.1 服务提供方

  • pom.xml文件


    4.0.0
    
        org.springframework.boot
        spring-boot-starter-parent
        2.3.10.RELEASE
        
    

    com.example
    user-provider
    0.0.1-SNAPSHOT

    
        8
        Hoxton.SR12
    
    
    
        
            
                org.springframework.cloud
                spring-cloud-dependencies
                ${spring-cloud.version}
                pom
                import
            
        
    

    

        
            org.springframework.boot
            spring-boot-starter-actuator
        

        
            org.springframework.cloud
            spring-cloud-starter-netflix-eureka-client
        

        
            org.springframework.boot
            spring-boot-starter-web
        

    

    
        
            
                org.springframework.boot
                spring-boot-maven-plugin
            
        
    


  • application.properties 配置
server.port=8081
#
# 
eureka.client.service-url.defaultZone=http://admin:123@ek1.com:7901/eureka/,http://admin:123@ek2.com:7902/eureka/
#
# 客户端在注册中心中的名称
eureka.instance.instance-id=user-provider-8081
#
# 设置当前 client 每5秒向 server 发送一次心跳,默认 30s
eureka.instance.lease-renewal-interval-in-seconds=5
#
# 表示eureka server至上一次收到client的心跳之后,等待下一次心跳的超时时间,在这个时间内若没收到下一次心跳,则将移除该instance,默认为90秒
eureka.instance.lease-expiration-duration-in-seconds=90
#
# 当前服务名称
spring.application.name=user-provider
#
# 表示将自己的ip注册到Eureka Server上。不配置,表示将操作系统的 hostname 注册到server
eureka.instance.prefer-ip-address=true
#
# eureka 服务名,默认值 unknown;如果没有配置,则取 spring.application.name
#eureka.instance.appname=user-provider
#
# 实例的虚拟主机名称,默认值 unknown;如果没有配置,则取 spring.application.name
#eureka.instance.virtual-host-name=user-provider
#
# 对外开放所有监控端点
management.endpoints.web.exposure.include=*
#
# 是否将自己注册到其他Eureka Server,默认为true
eureka.client.register-with-eureka=true
#
# 是否从eureka server获取注册信息, 需要
eureka.client.fetch-registry=true
  • 代码实现
public class User implements Serializable {

    private String idCard;
    private String username;

    public User() {
    }

    public User(String idCard, String username) {
        this.idCard = idCard;
        this.username = username;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getIdCard() {
        return idCard;
    }

    public void setIdCard(String idCard) {
        this.idCard = idCard;
    }

    @Override
    public String toString() {
        return "User{" +
                "idCard='" + idCard + ''' +
                ", username='" + username + ''' +
                '}';
    }
}
@SpringBootApplication
@EnableDiscoveryClient
public class UserServiceProviderApplication {

    public static void main(String[] args) {
        SpringApplication.run(UserServiceProviderApplication.class, args);
    }

}
@RestController
@RequestMapping("/user")
public class UserController {
    private final static Log log = LogFactory.getLog(UserController.class);

    @GetMapping("/get")
    public User getUser() {
        User user = new User("110", "大宝");
        log.info("UserProvider result : " + user);
        return user;
    }
}

3.2 服务消费方

  • pom.xml依赖


    4.0.0
    
        org.springframework.boot
        spring-boot-starter-parent
        2.3.10.RELEASE
        
    

    com.example
    web-consumer
    0.0.1-SNAPSHOT


    
        8
        Hoxton.SR12
    

    
        
            
                org.springframework.cloud
                spring-cloud-dependencies
                ${spring-cloud.version}
                pom
                import
            
        
    

    
        
            org.springframework.boot
            spring-boot-starter-web
        

        
            org.springframework.cloud
            spring-cloud-starter-netflix-eureka-client
        

        
            com.alibaba
            fastjson
            1.2.83
        

    

    
        
            
                org.springframework.boot
                spring-boot-maven-plugin
            
        
    


  • application.properties 配置
server.port=7071
# 注册中心地址
eureka.client.service-url.defaultZone=http://admin:123@ek1.com:7901/eureka/,http://admin:123@ek2.com:7902/eureka/
#
# 客户端在注册中心中的名称
eureka.instance.instance-id=web-consumer-7071
#
# 设置当前 client 每5秒向 server 发送一次心跳,默认 30s
eureka.instance.lease-renewal-interval-in-seconds=5
#
# 表示eureka server至上一次收到client的心跳之后,等待下一次心跳的超时时间,在这个时间内若没收到下一次心跳,则将移除该instance,默认为90秒
eureka.instance.lease-expiration-duration-in-seconds=90
#
# 当前服务对外暴露的名称
spring.application.name=web-consumer
#
# 表示将自己的ip注册到Eureka Server上。不配置,表示将操作系统的 hostname 注册到 server
eureka.instance.prefer-ip-address=true
#
# 是否将自己注册到其他Eureka Server
eureka.client.register-with-eureka=true
#
# 是否从eureka server获取注册信息
eureka.client.fetch-registry=true
#
# 表示eureka client间隔多久去拉取服务注册信息,默认为30秒,如果要迅速获取服务注册状态,可以缩小该值,比如5秒
eureka.client.registry-fetch-interval-seconds=5
#
# 设置 ribbon 和服务注册中心Eureka一起工作,ribbon 可以从服务注册中心获取服务端的地址信息
ribbon.eureka.enabled=true
  • 代码实现
public class User implements Serializable {

    private String idCard;
    private String username;

    public User() {
    }

    public User(String idCard, String username) {
        this.idCard = idCard;
        this.username = username;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getIdCard() {
        return idCard;
    }

    public void setIdCard(String idCard) {
        this.idCard = idCard;
    }
}
public class ResultBody implements Serializable {

    private boolean status;

    // 响应码
    private String code;

    // 响应描述信息
    private String message;

    // 响应数据
    private T data;

    public ResultBody() {
    }

    private ResultBody(T data) {
        this.data = data;
    }

    private ResultBody(String code, String msg) {
        this.code = code;
        this.message = msg;
    }

    public static  ResultBody success() {
        ResultBody result = new ResultBody();
        result.setCode("1");
        result.setStatus(Boolean.TRUE);
        result.setMessage("成功");
        return result;
    }

    public static  ResultBody success(T data) {
        ResultBody result = new ResultBody();
        result.setCode("1");
        result.setStatus(Boolean.TRUE);
        result.setMessage("成功");
        result.setData(data);
        return result;
    }

    public static  ResultBody error(String code, String message) {
        ResultBody result = new ResultBody(code, message);
        result.setStatus(Boolean.FALSE);
        return result;
    }

    public boolean isStatus() {
        return status;
    }

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

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }
}
@Configuration
public class RestTemplateConfig {

    // 开启消费者的负载均衡,默认负载均衡策略是:轮询; @LoadBalanced 注解的作用就是标识 RestTemplate 为 LoadBalancerClient 客户端
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        RestTemplateBuilder restTemplateBuilder = new RestTemplateBuilder();
        RestTemplate build = restTemplateBuilder.build();
        return build;
    }

}
@SpringBootApplication
@EnableDiscoveryClient
public class WebConsumerApplication {

    public static void main(String[] args) {
        SpringApplication.run(WebConsumerApplication.class, args);
    }

}
@RestController
@RequestMapping("/web")
public class UserProviderController {

    private final static Logger log = LoggerFactory.getLogger(UserProviderController.class);

    private final static String PROVIDER_SERVICE_APPLICATION_NAME = "user-provider";

    @Resource
    private EurekaClient client;
    @Resource
    private RestTemplate restTemplate;
    @Resource
    private LoadBalancerClient loadBalancerClient;

    /**
     * RestTemplate + LoadBalancerClient 方式实现负载均衡
     */
    @GetMapping("/balancer-client")
    public ResultBody getUserByLoadBalancerClient() {

        ServiceInstance server = loadBalancerClient.choose(PROVIDER_SERVICE_APPLICATION_NAME);
        String url = server.getUri() + "/user/get";

        RestTemplate restTemplate = new RestTemplateBuilder().build();
        User res = restTemplate.getForObject(url, User.class);

        return ResultBody.success(res);
    }

    /**
     * RestTemplate + @LoadBalanced 方式实现负载均衡
     */
    @GetMapping("/get")
    public ResultBody getUserByRestTemplate() {
        // 服务提供者的应用名称,当使用  RestTemplate +  @LoadBalanced 实现客户端的负载均衡时,这的请求路径并不是ip+端口号
        String url = "http://" + PROVIDER_SERVICE_APPLICATION_NAME + "/user/get";

        User res = restTemplate.getForObject(url, User.class);
        return ResultBody.success(res);
    }

    /**
     * EurekaClient + ip + 端口 方式直连请求,这种方式没有走负载均衡
     */
    @GetMapping("/client")
    public Object ekClient() {
        InstanceInfo info = client.getNextServerFromEureka(PROVIDER_SERVICE_APPLICATION_NAME, false);
        if (InstanceInfo.InstanceStatus.UP.equals(info.getStatus())) {
            String url = "http://" + info.getHostName() + ":" + info.getPort() + "/user/get";
            log.info("服务端接口地址url : " + url);

            RestTemplate restTemplate = new RestTemplate();
            User res = restTemplate.getForObject(url, User.class);
            return ResultBody.success(res);
        }
        return ResultBody.error("-1", "失败");
    }

}

总结:

  • 消费者请求服务提供者有多种调用方式
    (1)EurekaClient + ip + 端口 的方式,该种方式相当于直连,没有走ribbon负载均衡
    (2)RestTemplate + @LoadBalanced注解 方式实现负载均衡
     (3)  RestTemplate + LoadBalancerClient 方式实现负载均衡

  • 使用 ribbon 做客户端负载均衡时,使用RestTemplate + @LoadBalanced注解的方式请求服务提供者时url不能使用端口+ip,应该将端口+ip替换成服务提供者的spring.application.name属性值

  • Spring Cloud 2020 版本之后,spring-cloud-starter-netflix-eureka-client 中不再有ribbonjar,如果使用高版本,需要单独引入

  • dependency>
        org.springframework.cloud
        spring-cloud-starter-netflix-ribbon
        2.2.10.RELEASE
    
    

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论