在微服务中,对服务进行拆分之后,必然会带来微服务之间的通信需求,而每个微服务为了保证高可用性,又会去部署集群,那么面对一个集群微服务进行通信的时候,如何进行负载均衡也是必然需要考虑的问题。那么有需求自然就有供给,由此一大批优秀的开源的负载均衡组件应运而生,本文就让我们一起来分析一下Spring Cloud Netflix 套件中的负载均衡组件Ribbon。
首先我们来看一个问题,假如说我们现在有两个微服务,一个user-center,一个user-order,我现在需要在user-center 服务中调用user-order 服务的一个接口。
这时候我们可以使用HttpClient,RestTemplate 等发起http 请求,user-center 服务端口为8001,如下图所示:
@RestController @RequestMapping(value = "/user") public class UserController { @Autowired private RestTemplate restTemplate; @Bean public RestTemplate restTemplate(){ return new RestTemplate(); } @GetMapping("/order") public String queryOrder(){ return restTemplate.getForObject("http://localhost:8002/order/query",String.class); } } 而user-order 服务中只是简单的定义了一个接口,user-order 服务端口为8002:
@RestController @RequestMapping(value = "/order") public class UserOrderController { @GetMapping(value = "/query") public String queryAllOrder(){ return "all orders"; } } 这时候只需要将两个服务启动,访问http://localhost:8001/user/order 就可以获取到所有的订单信息。
可以看到,这样是可以在两个微服务之间进行通讯的,但是,假如说我们的user-order 服务是一个集群呢?这时候怎么访问呢?因为user-order 服务已经是集群,所以必然需要一种算法来决定应该请求到哪个user-order 服务中,最简单的那么自然就是随机或者轮询机制,轮询或者随机其实就是简单的负载均衡算法,而Ribbon 就是用来实现负载均衡的一个组件,其内部支持轮询,等算法。
接下来我们看看Ribbon 的简单使用。
user-order 服务,在user-order 服务中定义一个服务名配置:spring.application.name=user-order-service user-order 服务中的UserOrderController 稍微改造一下,新增一个端口的输出来区分:@RestController @RequestMapping(value = "/order") public class UserOrderController { @Value("${server.port}") private int serverPort; @GetMapping(value = "/query") public String queryAllOrder(){ return "订单来自:" + serverPort; } } 通过 VM 参数-Dserver.port=8002 和-Dserver.port=8003 分别来启动两个user-order 服务。
接下来改造user-center 服务,在user-center 服务中引入Ribbon 的相关依赖:
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-ribbon</artifactId> <version>2.2.3.RELEASE</version> </dependency> user-center 服务中新增一个Ribbon 相关配置,列举出需要访问的所有服务:user-order-service.ribbon.listOfServers=\ localhost:8002,localhost:8003 user-center 服务中的UserController 进行改造:@RestController @RequestMapping(value = "/user") public class UserController { @Autowired private RestTemplate restTemplate; @Autowired private LoadBalancerClient loadBalancerClient; @Bean public RestTemplate restTemplate(){ return new RestTemplate(); } @GetMapping("/order") public String queryOrder(){ //获取一个 user-order 服务 ServiceInstance serviceInstance = loadBalancerClient.choose("user-order-service"); String url = String.format("http://%s:%s",serviceInstance.getHost(),serviceInstance.getPort()) + "/order/query"; return restTemplate.getForObject(url,String.class); } } 这时候我们再次访问http://localhost:8001/user/order 就可以看到请求的user-order 服务会在8002 和8003 之间进行切换。
看了上面Ribbon 的使用示例,会不会觉得有点麻烦,每次还需要自己去获取ip 和端口,然后格式化url,但是其实实际开发过程中我们并不会通过这么原始的方式来编写代码,接下来我们再对上面的示例进行一番改造:
@RestController @RequestMapping(value = "/user") public class UserController3 { @Autowired private RestTemplate restTemplate; @Bean @LoadBalanced public RestTemplate restTemplate(){ return new RestTemplate(); } @GetMapping("/order") public String queryOrder(){ return restTemplate.getForObject("http://user-order-service/order/query",String.class); } } 在这个示例中,主要就是一个关键主键起了作用:@LoadBalanced。
进入@LoadBalanced 注解中,我们可以看到,这个注解其实没有任何逻辑,只是加了一个@Qualifier 注解:

这个注解大家应该很熟悉了,常用语同一个Bean 有多个不同名称注入的场景。
下面我们通过一个例子来演示一下Qualifier注解的用法。
新建一个空的TestDemo 类,并新增一个TestConfiguration 类来创建不同名称的TestDemo:
@Configuration public class TestConfiguration { @Bean("testDemo1") public TestDemo testDemo(){ return new TestDemo(); } @Bean("testDemo2") public TestDemo testDemo2(){ return new TestDemo(); } } 这时候我们如果需要注入TestDemo,那么有很多种办法,具体的使用就需要看业务需要来决定。
@Autowired,并使用List 集合来接收Bean,这样所有TestDemo 类型的Bean 都会被注入。@Resource(name = "testDemo1") 注解来指定名称,这样就可以只注入一个Bean。@Resource 和@Qualifier(value = "testDemo1") 来指定一个Bean,其实这种方式和方法二的效果基本一致。@Autowired 和@Qualifier 注解来注入,不指定任何名称,如下所示:@RestController @RequestMapping(value = "/test") public class TestQualifierController { @Autowired(required = false) @Qualifier private List<TestDemo> testDemo = Collections.emptyList(); @GetMapping("/all") public String allDemo(){ for (TestDemo testDemo : testDemos){ System.out.println(testDemo.toString()); } return "succ"; } } 这时候运行之后我们发现不会有任何Bean 被注入到集合中,这是因为当使用这种方式来注入时,Spring 会认为当前只需要注入被@Qualifier 注解标记的Bean,而我们上面定义的两个TestDemo 都没有被@Qualifier 修饰。
这时候,我们只需要在TestConfiguration 稍微改造,在TestDemo 的定义上加上@Qualifier 修饰即可:
@Configuration public class TestConfiguration { @Bean("testDemo1") @Qualifier public TestDemo testDemo(){ return new TestDemo(); } @Bean("testDemo2") @Qualifier public TestDemo testDemo2(){ return new TestDemo(); } } 这时候再去运行,就会发现,testDemo1 和testDemo2 都会被注入。
SpringCloud 是基于SpringBoot 实现的,所以我们常用的这些分布式组件都会基于SpringBoot 自动装配来实现,我们进入LoadBalancerAutoConfiguration 自动装配类可以看到,RestTemplate 的注入加上了@LoadBalanced,这就是为什么我们前面的例子中加上了@LoadBalanced 就能被自动注入的原因:

上面我们看到,RestTemplate 被包装成为了RestTemplateCustomizer,而RestTemplateCustomizer 的注入如下:

可以看到这里面加入了一个拦截器LoadBalancerInterceptor,事实上即使不看这里,我们也可以猜测到,我们直接使用服务名就可以进行通讯的原因必然是底层有拦截器对其进行转换成ip 形式,并在底层进行负载均衡选择合适的服务进行通讯。
LoadBalancerInterceptor 是Ribbon 中默认的一个拦截器,所以当我们调用RestTemplate 的getObject 方法时,必然会调用拦截器中的方法。
从源码中可以看到,LoadBalancerInterceptor 中只有一个intercept() 方法:

继续跟进execute 方法会进入到RibbonLoadBalancerClient 类(由RibbonAutoConfiguration 自动装配类初始化)中:

这个方法中也比较好理解,首先获取一个负载均衡器,然后再通过getServer 方法获取一个指定的服务,也就是当我们有多个服务时,到这里就会选出一个服务进行通讯。
进入getServer 方法:

我们看到,最终会调用ILoadBalancer 中的chooseServer 方法,而ILoadBalancer 是一个顶层接口,这时候具体会调用哪个实现类那么就需要先来看一下类图:

这里直接看类图也无法看出到底会调用哪一个,但是不论调用哪一个,我们猜测他肯定会有一个地方去初始化这个类,而在Spring 当中一般就是自动装配类中初始化或者Configuration 中初始化,而ILoadBalancer 正是在RibbonClientConfiguration 类中被加载的:

ZoneAwareLoadBalancer 的初始化会调用其父类DynamicServerListLoadBalancer 进行初始化,然后会调用restOfInit 方法进行所有服务的初始化。
使用Ribbon 后,我们通讯时并没有指定某一个ip 和端口,而是通过服务名来调用服务,那么这个服务名就可能对应多个真正的服务,那么我们就必然需要先获取到所有服务的ip 和端口等信息,然后才能进行负载均衡处理。
获取所有服务有两种方式:
Eureka 注册中心获取(需要引入注册中心)。初始化服务的方式是通过启动一个Scheduled 定时任务来实现的,默认就是30s 更新一次,其实在很多源码中都是通过这种方式来定时更新的,因为源码要考虑的使用的简单性所以不太可能引入一个第三方中间件来实现定时器。
具体的源码如下所示:enableAndInitLearnNewServersFeature() 方法启动的定时任务最终仍然你是调用updateListOfServers() 方法来更新服务。

最终在获取到服务之后会调用父类BaseLoadBalancer 中的将所有服务设置到allServerList 集合中(BaseLoadBalancer 类中维护了一些负载均衡需要使用到的服务相关信息)。
当我们获取到配置文件(或者Eureka 注册中心)中的所有服务,那么这时候能直接执行负载均衡策略进行服务分发吗?显然是不能的,因为已经配置好的服务可能会宕机(下线),从而导致服务不可用,所以在BaseLoadBalancer 中除了有一个allServerList 集合来维护所有服务器,还有一个集合upServerList 用来维护可用服务集合,那么如何判断一个服务是否可用呢?答案就是通过心跳检测来判断一个服务是否可用。
在讲心跳检测之前,我们先看一下BaseLoadBalancer 中的setServersList 方法,有一段逻辑比较重要:


这段逻辑我们看到,默认情况下,如果Ping 的策略是DummyPing,那么默认upServerList = allServerList,而实际上,假如我们没有进行进行特殊配置,其实默认的就是DummyPing,这也是在RibbonClientConfiguration 类中被加载的:

在BaseLoadBalancer 初始化过程中,也会启动一个Scheduled 定时任务去定时更新任务,最终和forceQuickPing() 方法一样,调用一个默认策略来触发心跳检测,而默认策略就是DummyPing,也就是默认所有服务都是可用的。

虽然默认不执行真正的心跳检测操作,但是Netflix 中提供了PingUrl 等其他策略,PingUrl 其实就是发起一个http 请求,如果有响应就认为服务可用,没响应就认为服务不可用。
修改心跳检测策略可以通过如下配置切换(user-order-service 为客户端的服务名),既然是可配置的,那么也可以自己实现一个策略,只需要实现IPing 接口即可。
user-order-service.ribbon.NFLoadBalancerPingClassName=com.netflix.loadbalancer.PingUrl 当获取到可用服务之后,那么最后应该选择哪一个服务呢?这就需要使用到负载均衡策略,在Ribbon 中,可以通过配置修改,也可以自定义负载均衡策略(实现IRule 接口)。
deadline 时间内,如果请求不成功,则重新发起请求知道找到一个可用的服务。3 次连接失败)的服务和高并发的服务。负载均衡算法可通过以下配置进行修改:
user-order-service.ribbon.NFLoadBalancerRuleClassName=Rule规则的类名 本文主要讲述了微服务体系下的Spring Cloud Netflix 套件中Ribbon 的使用,并结合部分源码讲述了Ribbon 的底层原理,重点讲述了Ribbon 中是如何获取服务以及如何判定一个服务是否可用,最后也介绍了Ribbon 中默认提供的7 种负载均衡策略。