文章目录
一、前言
0. 之前写过两篇Spring Cloud,但是感觉不够具体,所以重新写了一份。
新 Spring Cloud (一) 之 Eureka 服务注册中心
新 SpringCloud (二) 之 Ribbon 负载均衡
新 Spring Cloud (三) 之 Hystrix熔断保护
新 Spring Cloud (四) 之 Fegin远程调用
新 Spring Cloud (五) 之 Zuul 网关
1. 正文
继续我们上个项目搭建,不过为了方便识别,进行了一些改动。
-
为了方便更改配置,而不是一直在一个文件配置,所以在Eurake注册中心配置了三个yml文件如下:
其中application-10086.yml 自然是10086端口的配置,application-10087.yml 自然是10087端口的配置
application.yml 用来切换环境
如下:application.yml
spring: profiles: active: 10087 # 通过active的值来切换不同的配置文件 原则是加载 application-{active值}.yml
application-10086.yml
server: port: 10086 # Eureka服务的端口 spring: application: name: eureka-registety-server # 应用名称,会在Eureka中显示 eureka: client: service-url: # EurekaServer的地址,现在是自己的地址,如果是集群,需要加上其它Server的地址。 defaultZone: http://127.0.0.1:10087/eureka register-with-eureka: true # 是否把自己注册到eureka服务列表,默认true,会将自己本身注册到Eureka中 fetch-registry: false # 拉取eureka服务信息 指示此客户端是否应从eureka服务器获取eureka注册表信息,注意开启这个时候,在启动Eureka服务的时候会报错,但不影响使用。 instance: # 使用自定义ip ,否则在获取服务的时候ip地址会错误 preferIpAddress: true instance-id: ${spring.application.name}:${server.port} server: eviction-interval-timer-in-ms: 1000 # 每隔1000ms 清除一次失效列表 enable-self-preservation: false # 关闭自我保护模式(缺省为打开)
application-10087.yml
server: port: 10087 # Eureka服务的端口 spring: application: name: eureka-registety-server # 应用名称,会在Eureka中显示 eureka: client: service-url: # EurekaServer的地址,现在是自己的地址,如果是集群,需要加上其它Server的地址。 defaultZone: http://127.0.0.1:10086/eureka register-with-eureka: true # 是否把自己注册到eureka服务列表,默认true,会将自己本身注册到Eureka中 fetch-registry: false # 拉取eureka服务信息 指示此客户端是否应从eureka服务器获取eureka注册表信息,注意开启这个时候,在启动Eureka服务的时候会报错,但不影响使用。 instance: # 使用自定义ip ,否则在获取服务的时候ip地址会错误 preferIpAddress: true instance-id: ${spring.application.name}:${server.port} server: eviction-interval-timer-in-ms: 1000 # 每隔1000ms 清除一次失效列表 enable-self-preservation: false # 关闭自我保护模式(缺省为打开)
-
启动器名称改了一下,方便看懂
二、Ribbon 介绍
Ribbon是Netlix发布的负载均衡器,它有助于控制HTTP和TCP客户端的行为。为Ribbon配置服务提供者地址列表后,Ribbon 就可基于某种负载均衡算法,自动地帮助服务消费者去请求。Ribbon默认为我们提供了很多的负载均衡算法,例如轮询、随机等。当然,我们也可为Ribbon实现自定义的负载均衡算法。
三、Eureka 整合 Ribbon
Eureka中已经帮我们集成了负载均衡组件:Ribbon,简单修改代码即可使用。
对于我们这个例子,我们假设,现在有两个司机服务提供者,一个占用端口10000,一个占用端口10010。那么我们在实际打车时,不可能一直让10000端口的服务接单,也不可能一直让10010端口的司机接单。我们需要一个策略来将订单分派给不同的司机。Ribbon组件就可以完成这项工作。
-
在司机服务端(EurekaServerProvider)添加一个10001端口配置文件和启动器。和上面一样,创建三个yml文件如下。
application.ymlspring: profiles: active: 10000 # 通过更改 active 的值来引入不同的配置文件
application-10000.yml
server: port: 10000 # Eureka服务的端口 spring: application: name: eureka-server-provider # 应用名称,会在Eureka中显示 eureka: client: service-url: # EurekaServer的地址,现在是自己的地址,如果是集群,需要加上其它Server的地址。 defaultZone: http://127.0.0.1:10086/eureka,http://127.0.0.1:10087/eureka register-with-eureka: true # 是否把自己注册到eureka服务列表,默认true,会将自己本身注册到Eureka中 fetch-registry: true # 拉取eureka服务信息 指示此客户端是否应从eureka服务器获取eureka注册表信息,注意开启这个时候,在启动Eureka服务的时候会报错,但不影响使用。 instance: # 使用自定义ip ,否则在获取服务的时候ip地址会错误 preferIpAddress: true instance-id: ${spring.application.name}:${server.port} # 自定义实例id,后面方便获取 lease-expiration-duration-in-seconds: 10 # 服务失效时间 lease-renewal-interval-in-seconds: 5 # 服务续约(心跳)时间
application-10001.yml
server: port: 10001 # Eureka服务的端口 spring: application: name: eureka-server-provider # 应用名称,会在Eureka中显示 eureka: client: service-url: # EurekaServer的地址,现在是自己的地址,如果是集群,需要加上其它Server的地址。 defaultZone: http://127.0.0.1:10086/eureka,http://127.0.0.1:10087/eureka register-with-eureka: true # 是否把自己注册到eureka服务列表,默认true,会将自己本身注册到Eureka中 fetch-registry: true # 拉取eureka服务信息 指示此客户端是否应从eureka服务器获取eureka注册表信息,注意开启这个时候,在启动Eureka服务的时候会报错,但不影响使用。 instance: # 使用自定义ip ,否则在获取服务的时候ip地址会错误 preferIpAddress: true instance-id: ${spring.application.name}:${server.port} # 自定义实例id,后面方便获取 lease-expiration-duration-in-seconds: 10 # 服务失效时间 lease-renewal-interval-in-seconds: 5 # 服务续约(心跳)时间
-
乘客端(EurekaServerConsumer)进行修改:
1. RestTemplate注入时 添加注解 @LoadBalanced ,标志启用负载均衡@Bean @LoadBalanced public RestTemplate restTemplate(){ return new RestTemplate(); }
2.修改 乘客调用司机服务接口部分,如下:
@RequestMapping("callTaxi") public String callTaxi(String msg) { String url = "http://eureka-server-provider/driver/takeOrder"; // 通过 Eureka 服务注册中心的服务名称来调用 // post方式嗲用传递参数 MultiValueMap<String,String> multiValueMap = new LinkedMultiValueMap<String,String>(); multiValueMap.add("msg", msg); // 访问服务 String result = restTemplate.postForObject(url, multiValueMap, String.class); System.out.println("乘客收到信息: " + result); return result; }
-
至此,搭建完毕。启动10000,10001,10086,10087,10010端口的服务,我们开始访问http://localhost:10010/passenger/callTaxi。多次访问,我们发现接单的端口并不一样,也就是说接单的服务司机并不是同一个人,即有了默认的分派功能。
四、负载均衡配置
1. Ribbon的几种负载均衡策略
Ribbon 默认的负载均衡策略是轮询策略。
2. 配置负载均衡策略
1. yml 方式 :
yml配置原则是 : ${服务名}.ribbon.NFLoadBalancerRuleClassName: com.netflix.loadbalancer.IRule的实现类
eureka-server-provider: # 服务提供者的服务名称,而不是服务消费者。
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
2. Bean注入方式:
直接注入IRule的实现类
@Bean
public IRule ribbonRule() {
return new RandomRule();//这里配置策略,和配置文件对应
}
3 . 源码解析:
- 进入
RibbonClientConfiguration
中,如下的name
字段,其上面有个注解@RibbonClientName
, 有道翻译如下:该注释注入在运行时分配的Ribbon客户端名称。提供了一个方便的选择。即可以看到name的值是eureka-server-provider
,是我们“司机服务的服务名”。即可知道@RibbonClientName
将**“司机服务的名称**”注入到name中。
- 继续往下看,可以看到 ribbonRule 方法,注入了一个IRule对象。通过
@ConditionalOnMissingBean
保证用户定制的自由(用户若自己注入,则不注入默认)。其中可以看到如果我们自己配置了IRule实例,则Ribbon会使用我们配置的IRule作为负载均衡实例。同时,因为name的值是服务提供者(eureka-server-provider
),这也就解释了yml文件中我们配置负载均衡策略时为什么要使用服务提供者的服务名。
- 看到
ribbonLoadBalancer
方法,将IRule实例注入进来,并将ILoadBalancer
的实例注入到Spring容器中。这个实例在后面负载均衡的时候会使用上。
- 这也就解释了上面的yml配置和Bean注入IRule实例为什么可以配置Ribbon 负载均衡策略。
- 当我们使用RestTemplate.postForObject 方法时,流程如下: RestTemplate.postForObject --> RestTemplate.execute --> RestTemplate.doExecute -->AbstractClientHttpRequest.execute --> AbstractBufferingClientHttpRequest.executeInternal --> InterceptingClientHttpRequest.executeInternal --> InterceptingClientHttpRequest.execute。发下 迭代器中的类是 : LoadBalancerInterceptor
- 继续跟进 LoadBalancerInterceptor.intercept 。
- 再跟进 RibbonLoadBalancerClient.execute,已经可以看到服务的地址,并且通过getServer方法已经筛选出这次请求的地址了,所以再进入 RibbonLoadBalancerClient.getServer方法
8.RibbonLoadBalancerClient.getServer --> ZoneAwareLoadBalancer.chooseServer -> BaseLoadBalancer.chooseServer。看到rule的值为我们指定的随机策略。ZoneAwareLoadBalancer 是我们在第三步的时候注入的实例,所以IRule也被初始化为对应的负载均衡规则。
- 进入 RandomRule.choose方法。可以看到其内部逻辑就是随机使用一个服务作为结果服务返回。从而呼应了第七步的内容。
5. 自定义负载均衡策略
我们可以按照RandomRule、RoundRobinRule来照葫芦画瓢。
首先创建一个MyRule 继承 AbstractLoadBalancerRule 类,按照RoundRobinRule内容,自定义一个MyRule。规则是每个服务访问四次后切换下一个服务。
package com.lkingfish.springcloud;
import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.AbstractLoadBalancerRule;
import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.Server;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @Data: 2019/10/24
* @Des: 自定义负载均衡规则。
*/
public class MyRule extends AbstractLoadBalancerRule {
private AtomicInteger nextServerCyclicCounter;
private AtomicInteger atomerCounter;
private AtomicInteger currentServerCounter;
public MyRule() {
nextServerCyclicCounter = new AtomicInteger(0);
atomerCounter = new AtomicInteger(3);
currentServerCounter = new AtomicInteger(3);
}
@Override
public void initWithNiwsConfig(IClientConfig clientConfig) {
}
@Override
public Server choose(Object key) {
return choose(getLoadBalancer(), key);
}
/**
* 具体策略实现
*
* @param lb
* @param key
* @return
*/
public Server choose(ILoadBalancer lb, Object key) {
if (lb == null) {
return null;
}
Server server = null;
int count = 0;
while (server == null && count++ < 10) {
List<Server> reachableServers = lb.getReachableServers();
List<Server> allServers = lb.getAllServers();
int upCount = reachableServers.size();
int serverCount = allServers.size();
if ((upCount == 0) || (serverCount == 0)) {
return null;
}
int currCount = atomerCounter.get();
if (currCount < 3) {
atomerCounter.compareAndSet(currCount, currCount+1);
} else {
int nextServerIndex = incrementAndGetModulo(serverCount);
atomerCounter.set(0);
currentServerCounter.set(nextServerIndex);
}
server = allServers.get(currentServerCounter.get());
if (server == null) {
Thread.yield();
continue;
}
if (server.isAlive() && (server.isReadyToServe())) {
return (server);
}
// Next.
server = null;
}
if (count >= 10) {
System.out.println("尝试10次失败");
}
return server;
}
private int incrementAndGetModulo(int modulo) {
for (; ; ) {
int current = nextServerCyclicCounter.get();
int next = (current + 1) % modulo;
if (nextServerCyclicCounter.compareAndSet(current, next))
return next;
}
}
}
随后将MyRule注解即可。
@Bean
public IRule ribbonRule() {
return new MyRule();//这里配置策略,和配置文件对应
}