如何使用原生的Ribbon

什么是Ribbon

之前分析了如何使用原生的Feign,今天我们来研究 Netflix 团队开发的另外一个类库–Ribbon。
Ribbon 和 Feign 有很多相似的地方,首先,它们本质上都是 HTTP client,其次,它们都具备重试、集成断路器等功能。最大的区别在于,Ribbon 内置了一个负载均衡器,而 Feign 没有。

本文将介绍如何使用原生的 Ribbon,注意是原生的,而不是被 Spring 层层封装的 Ribbon。

为什么要使用Ribbon

这里我们需要回答两个问题:

  1. 为什么要使用 HTTP client?
  2. 为什么要在 HTTP client 里内置负载均衡器?

其中,第一个问题在如何使用原生的Feign中已经讲过,这里就不啰嗦了,我们直接看第二个问题。

我们知道,Apache HTTP client、Feign 并没有内置负载均衡器,也就是说,HTTP client 并不一定要内置负载均衡器,那为什么 Ribbon 要搞特殊呢?

其实,我们可以想想, Ribbon 更多地被用在内部调用,而这种场景有一个比较大的特点–目标服务为集群部署。通常情况下,在调用目标服务时,我们希望请求尽可能平均地分发到每个实例。通过内置的负载均衡器,Ribbon 可以很好地满足要求,而 Apache HTTP client、Feign 就无法做到。

所以,在 HTTP client 里内置负载均衡器是为了能够在目标服务为集群部署时提供负载均衡支持。

如何使用原生的Ribbon

有的人可能会说,你单独部署一台负载均衡器就行了嘛,搞那么复杂干嘛。当然,你可以这么做。但是你要考虑很重要的一点,mid-tier services 的请求量要远大于 edge services,所以你需要一台性能极高的负载均衡器。从这个角度来说,Ribbon 的方案帮你省下了独立部署负载均衡器的开销。

如何使用原生的Ribbon

如何使用Ribbon

项目中我用 RxNettty 写了一个简单的 HTTP 接口(见 cn.zzs.ribbon.RxUserServer)供后面的例子调用,这个接口运行在本机的 8080、8081、8082 接口,用来模拟三台不同的实例。所以,如果你想要测试项目中的例子,要先把这三台实例先启动好。

http://127.0.0.1:8080/user/getUserById?userId={userId}
request:userId=1
response:User [id=1, name=zzs001, age=18]

这里提醒一下,Ribbon 的 API 用到了很多 RxJava 代码,如果之前没接触过,最好先了解下。

项目环境

os:win 10

jdk:1.8.0_231

maven:3.6.3

IDE:Spring Tool Suite 4.6.1.RELEASE

Ribbon:2.7.17

作为HTTP client的用法

和 Feign 一样,Ribbon 支持使用注解方式定义 HTTP 接口,除此之外,Ribbon 还支持使用 HttpRequestTemplateHttpClientRequest等方式定义,这部分的例子我也提供了,感兴趣可以移步项目源码。

服务实例的列表通过 ConfigurationManager设置。当你看到 ConfigurationManager时,会不会觉得很熟悉呢?我们之前在Eureka详解系列(三)–探索Eureka强大的配置体系中详细介绍过,没错,Ribbon 用的还是这套配置体系。需要强调下,Netflix 团队开发的这套配置体系提供了动态配置支持(当然,你要会用才行),就拿本项目为例,如果运行过程中你通过 ConfigurationManager更改了 listOfServers,那么,Ribbon 会感知到这种变化,使用最新的 listOfServers 来负载均衡,只是会有一点点延迟。

// 使用注解定义HTTP API
@ClientProperties(properties = {
        @Property(name="ReadTimeout", value="2000"),
        @Property(name="ConnectTimeout", value="1000"),
        @Property(name="MaxAutoRetries", value="1"),
        @Property(name="MaxAutoRetriesNextServer", value="2")
}, exportToArchaius = true)
interface UserService {
    @TemplateName("getUserById")
    @Http(
            method = HttpMethod.GET,
            uri = "/user/getUserById?userId={userId}",
            headers = {
                    @Header(name = "X-Platform-Version", value = "xyz"),
                    @Header(name = "X-Auth-Token", value = "abc")
            })
    RibbonRequest getUserById(@Var("userId") String userId);
}

public class RxUserProxyTest {
    @Test
    public void testBase() throws InterruptedException {
        // 指定服务实例的地址
        // key:服务+".ribbon."+配置项名称(见com.netflix.client.config.CommonClientConfigKey)
        ConfigurationManager.getConfigInstance().setProperty(
                "UserService.ribbon.listOfServers", "127.0.0.1:8080,127.0.0.1:8081,127.0.0.1:8082");

        UserService userService = Ribbon.from(UserService.class);

        userService.getUserById("1")
            .toObservable()
            .subscribe(new Subscriber() {
                @Override
                public void onCompleted() {
                    LOG.info("onCompleted");
                }

                @Override
                public void onError(Throwable e) {
                    e.printStackTrace();
                }

                @Override
                public void onNext(Object t) {
                    LOG.info("onNext:{}", t);
                    if(t != null && t instanceof ByteBuf) {
                        LOG.info(ByteBuf.class.cast(t).toString(Charset.defaultCharset()));
                    }
                }
            });
        // 因为请求HTTP接口是异步的,这里要让测试主线程先睡一会
        Thread.sleep(10000);
    }
}

默认的负载均衡规则

为了观察多次请求在三台实例的分配情况,现在我们更改下代码,试着发起 6 次请求。

    @Test
    public void test01() throws InterruptedException {
        ConfigurationManager.getConfigInstance().setProperty(
                "UserService.ribbon.listOfServers", "127.0.0.1:8080,127.0.0.1:8081,127.0.0.1:8082");

        UserService userService = Ribbon.from(UserService.class);
        // 发起多次请求
        Observable[] requestList = new Observable[]{
                userService.getUserById("1").toObservable(),
                userService.getUserById("2").toObservable(),
                userService.getUserById("3").toObservable(),
                userService.getUserById("4").toObservable(),
                userService.getUserById("5").toObservable(),
                userService.getUserById("6").toObservable()
        };
        Observable.concat(Observable.from(requestList))
                .subscribe(subscriber);
        Thread.sleep(10000);
    }

运行测试,可以看到,6 次请求被平均地分配到了 3 台实例。

如何使用原生的Ribbon

在日志中,可以看到了默认的负载均衡规则。

如何使用原生的Ribbon

通过源码可以看到,这个默认的规则本质上采用的是轮询策略 RoundRobinRule。除此之外,Ribbon 还定义了 RandomRuleRetryRule等规则供我们选择。

public class AvailabilityFilteringRule {
    RoundRobinRule roundRobinRule = new RoundRobinRule();
}

自定义负载均衡规则

自定义负载均衡规则需要继承 com.netflix.loadbalancer.AbstractLoadBalancerRule,并实现 choose 方法。这里我定义的规则是:不管有多少实例,默认访问第一台。

public class MyLoadBalancerRule extends AbstractLoadBalancerRule {
    @Override
    public Server choose(Object key) {

        ILoadBalancer lb = getLoadBalancer();

        List allServers = lb.getAllServers();

        return allServers.stream().findFirst().orElse(null);
    }
}

接着,只需要通过 ConfigurationManager配置自定义规则就行。

    @Test
    public void test01() throws InterruptedException {
        ConfigurationManager.getConfigInstance().setProperty(
                "UserService.ribbon.listOfServers", "127.0.0.1:8080,127.0.0.1:8081,127.0.0.1:8082");
        // 配置自定义规则
        ConfigurationManager.getConfigInstance().setProperty(
                "UserService.ribbon.NFLoadBalancerRuleClassName", "cn.zzs.ribbon.MyLoadBalancerRule");

        UserService userService = Ribbon.from(UserService.class);

        Observable[] requestList = new Observable[]{
                userService.getUserById("1").toObservable(),
                userService.getUserById("2").toObservable(),
                userService.getUserById("3").toObservable(),
                userService.getUserById("1").toObservable(),
                userService.getUserById("2").toObservable(),
                userService.getUserById("3").toObservable()
        };
        Observable.concat(Observable.from(requestList))
                .subscribe(subscriber);
        Thread.sleep(10000);
    }

运行测试,可以看到,所有请求都被分配到了第一台实例。自定义负载均衡规则生效。

如何使用原生的Ribbon

动态刷新listOfServers

在前面的例子中,我们的服务实例地址 listOfServers 都是写死的,然而,在实际项目中,目标服务的实例数量、地址都是变化的,所以,我们需要动态地更新 listOfServers,而不能写死。

上面说过 Netflix 开发的这套配置体系是支持动态配置的,所以,我能想到的最简方案就是,开一个任务,定时地从注册中心等地方拉取最新的实例列表,再把这个新的列表塞进 ConfigurationManager就行了。

针对以 eureka server 为注册中心的项目,官方提供了一个实现方案。对比我上面说的方案,这个方案逻辑虽简单,但实现更复杂,源码可读性挺高的,感兴趣的朋友可以看看。

    @Test
    public void testEureka() throws InterruptedException {
        // 指定实例列表从eureka获取
        ConfigurationManager.getConfigInstance().setProperty(
                "UserService.ribbon.NIWSServerListClassName", "com.netflix.niws.loadbalancer.DiscoveryEnabledNIWSServerList");
        ConfigurationManager.getConfigInstance().setProperty(
                "UserService.ribbon.DeploymentContextBasedVipAddresses", "UserService");

        // 初始化EurekaClient
        DiscoveryManager.getInstance().initComponent(new MyDataCenterInstanceConfig(), new DefaultEurekaClientConfig());

        UserService userService = Ribbon.from(UserService.class);
        userService.getUserById("1")
            .toObservable()
            .subscribe(subscriber);

        Thread.sleep(10000);
    }
}

结语

以上,基本讲完 Ribbon 的使用方法,其实 Ribbon 还有其他可以扩展的东西,例如,断路器、重试等等。感兴趣的话,可以自行分析。

最后,感谢阅读。

参考资料

ribbon github

相关源码请移步:https://github.com/ZhangZiSheng001/ribbon-demo

本文为原创文章,转载请附上原文出处链接:https://www.cnblogs.com/ZhangZiSheng001/p/15484505.html

Original: https://www.cnblogs.com/ZhangZiSheng001/p/15484505.html
Author: 子月生
Title: 如何使用原生的Ribbon

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/575780/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • MySQL索引:B+树索引

    MySQL索引:B+树索引 B+树索引是传统意义上的索引,这是目前关系型数据库系统中查找最为常用和最为有效的索引。B+树索引的构造类似于二叉树,根据键值快速找到数据 B树 B+树是…

    数据库 2023年5月24日
    088
  • Resilience4j 实践

    微服务设计模式 – circuit breaker circuit breaker 熔断器,在很多不同的领域都有这个定义,例如电路里面的熔断器,股票行业里面的熔断,当然…

    数据库 2023年6月11日
    0104
  • MyBatis(二)-CURD (ResultMap 一对一,一对多)

    1、insert 标签 1.1 获取SqlSessionFactory 对象的通用方法 方便后面分测试; //获取SqlSessionFactory 对象的通用方法 public …

    数据库 2023年6月16日
    084
  • Pisa-Proxy 之 SQL 解析实践

    SQL 语句解析是一个重要且复杂的技术,数据库流量相关的 SQL 审计、读写分离、分片等功能都依赖于 SQL 解析,而 Pisa-Proxy 作为 Database Mesh 理念…

    数据库 2023年6月16日
    0126
  • 大连交通大学课程共享

    如本页面访问适配不佳,阅读体验不好可访问公众号页面(适配更好)。公众号页面:https://mp.weixin.qq.com/s/5g2-Izrygm6WhKiT3z1yow 设立…

    数据库 2023年6月11日
    082
  • Mysql终端Terminal操作

    datebase管理 1.创建数据库-create 语法:create database 数据库名 character set 编码 注意:默认会存在四个数据库,其数据库中存储的是…

    数据库 2023年5月24日
    0104
  • Atlas快速入门

    之前的公司在数据中台的项目上调研决定启用了Atlas作为我们数据血缘管理的工具,让我给大家写了一份Atlas快速入门的文档,所以在这里我将这篇文档以一个纯新手视角的方式再一次优化,…

    数据库 2023年6月11日
    099
  • Win10系统链接蓝牙设备

    进入设备界面,删除已有蓝牙,如果蓝牙耳机已经链接其他设备,先断开链接 点击添加蓝牙或其他设备 Original: https://www.cnblogs.com/itcaimeng…

    数据库 2023年6月11日
    098
  • Vuex 简单使用

    官网:https://vuex.vuejs.org/zh/ 参考文章:https://www.cnblogs.com/chinabin1993/p/9848720.html Vue…

    数据库 2023年6月16日
    099
  • MySQL中varchar(1)的解读(辟谣)

    MySQL中varchar(1)的解读(辟谣) 网上有如下错误解读: 在mysql中, varchar(n)和char(n)表示n个字符。不管是中文还是英文,MySQL都可以存储n…

    数据库 2023年6月14日
    0117
  • SNMP windows OIDs

    Windows OID’ for CPU, Memory, Disk Utilization2007-10-31 11:03Windows CPU Utilizatio…

    数据库 2023年6月11日
    099
  • 事务的本质和死锁的原理・改

    由于一些错误操作和被爬的原因,我重新整理了 上一篇文章https://…

    数据库 2023年6月9日
    091
  • Python 垃圾回收总结

    前言 最近在阅读《垃圾回收的算法与实现》,里面将讲到了一些常用的垃圾回收(Garbage Collect)算法,如:标记-清除、引用计数、分代回收等等。后面讲到了 Python 的…

    数据库 2023年6月6日
    0110
  • Sql的字符串匹配 like

    患者信息表: Patients +————–+———+ | Column Name | Type | +————–+——–…

    数据库 2023年6月14日
    082
  • [spring]spring和mybatis的整合与事务处理

    1.导包 junit junit 4.13.1 mysql mysql-connector-java 8.0.29 org.mybatis mybatis 3.5.3 org.sp…

    数据库 2023年6月16日
    096
  • 23种设计模式之访问者模式(Visitor Pattern)

    文章目录 概述 访问者模式的优缺点 访问者模式的使用场景 访问者模式的结构和实现 * 模式结构 模式实现 总结 概述 访问者模式把数据结构和作用于结构上的操作解耦合,使得操作集合可…

    数据库 2023年6月6日
    093
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球