Spring Cloud学习笔记
OpenFeign与Ribbon
概述
OpenFeign简介
- 【原文】Declarative REST Client: Feign creates a dynamic implementation of an interface decorated(装饰) with JAX-RS or Spring MVC annotations.
- 【翻译】声明式 REST 客户端:Feign 通过使用 JAX-RS 或 SpringMVC 注解的装饰方式,生成接口的动态实现。
- 综合说明:Feign,假装、伪装。OpenFeign 可以使消费者将提供者提供的服务名伪装为接口进行消费,消费者只需使用“Feign 接口 + 注解”的方式即可直接调用 Service 接口方法,而无需再使用 RestTemplate 了。
OpenFeign与Feign
- Spring Cloud D版及之前的版本使用的是 Feign,而该项目现已更新为了 OpenFeign,所以后续使用的依赖也发生了变化。
<!-- openfeign 依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
Ribbon与OpenFeign
- 说到 OpenFeign,不得不提的就是 Ribbon。 Ribbon 是 Netflix 公司的一个开源的负载均衡项目,是一个客户端负载均衡器,运行在消费者端。
- OpenFeign 中使用 Ribbon 进行负载均衡,所以 OpenFeign 直接内置了 Ribbon。即在导入OpenFeign 依赖后,无需再专门导入 Ribbon 依赖了。
声明式Rest客户端OpenFeign
创建消费者工程 03-consumer-feign-8080
- 复制 02-consumer-8080 工程,并重命名为 03-consumer-feign-8080,添加 openfeign 依赖即可。
- 定义 Feign Service 接口,并修改 Controller 层接口实现:
@FeignClient("msc-provider-depart") // 指定Service绑定的微服务名称
@RequestMapping("/provider/depart")
public interface DepartService {
@PostMapping("/save")
boolean saveOne(Depart depart);
@DeleteMapping("/del/{id}")
boolean deleteById(@PathVariable("id") int id);
@PutMapping("/update")
boolean updateOne(Depart depart);
@GetMapping("/get/{id}")
Depart getDepartById(@PathVariable("id") int id);
@GetMapping("/list")
List<Depart> listDeparts();
}
- 关于 Feign 的说明:
- Feign 接口名一般是与业务接口名相同的,但不是必须的;
- Feign 接口中的方法名一般也与业务接口方法名相同,但也不是必须的;
- Feign 接口中的方法返回值类型、方法参数要求与业务接口中的相同;
- 接口上与方法上的 Mapping 的参数 URI 要与提供者处理器相应方法上的 Mapping 的 URI 相同。
@RestController
@RequestMapping("/consumer/depart")
public class DepartController {
/**
* 这里的DepartService是FeignClient,并不是真正的业务接口
*/
@Autowired
private DepartService departService;
@PostMapping("/save")
public boolean saveHandle(@RequestBody Depart depart) {
return departService.saveOne(depart);
}
@DeleteMapping("/del/{id}")
public boolean deleteHandle(@PathVariable("id") int id) {
return departService.deleteById(id);
}
@PutMapping("/update")
public boolean updateHandle(@RequestBody Depart depart) {
return departService.updateOne(depart);
}
@GetMapping("/get/{id}")
public Depart getHandle(@PathVariable("id") int id) {
return departService.getDepartById(id);
}
@GetMapping("/list")
public List<Depart> listHandle() {
return departService.listDeparts();
}
}
- 启动类开启Feign客户端:
超时与压缩
- 在 application.yml 中添加如下配置
feign:
client:
config:
default:
# 指定Feign连接提供者的超时时限,决定于网络状况
connectTimeout: 5000
# 指定Feign从请求到获取提供者响应的超时时限,决定于提供者的处理时间
readTimeout: 5000
compression:
request:
# 开启对请求的压缩
enabled: true
# 指定对哪些MIME类型的文件进行压缩
mime-types: ['text/xml', 'application/xml', 'application/json']
min-request-size: 2048
response:
# 开启对客户端响应的压缩
enabled: true
Ribbon负载均衡
系统结构
创建提供者 03-provider-8090
- 复制 02-provider-8090 工程,并重命名为 03-provider-8090,利用该工程在本地启动三个提供者,端口号分别是 8090、8091、8092,IDEA允许并行运行。
- 修改Service实现类:
@Service
public class DepartServiceImpl implements DepartService{
@Autowired
private DepartRepository repository;
// 读取配置文件中的属性值
@Value("${server.port}")
private int port;
// 略...
@Override
public Depart getDepartById(int id) {
// repository.getOne()中指定的id若不存在,则会抛出异常,因此这里要先判断是否存在
if (repository.existsById(id)) {
Depart depart = repository.getOne(id);
depart.setName(rename(depart.getName()));
return depart;
}
return new Depart().setName(port + " No this depart");
}
@Override
public List<Depart> listDeparts() {
List<Depart> departs = repository.findAll();
departs.forEach(v -> v.setName(rename(v.getName())));
return departs;
}
private String rename(String name) {
return name + " : " + port;
}
}
- 启动三个提供者:
- 访问地址:http://localhost:8080/consumer/depart/get/1,测试负载均衡效果,可以看到采用的是轮询方式。
负载均衡策略
内置负载均衡策略
- RoundRobinRule:轮询策略。Ribbon 默认采用的策略。若经过一轮轮询没有找到可用的 provider,其最多轮询 10 轮。若最终还没有找到,则返回 null。
- RandomRule:随机策略,从所有可用的 provider 中随机选择一个。
- RetryRule:重试策略。先按照 RoundRobinRule 策略获取 provider,若获取失败,则在指定的时限内重试。默认的时限为 500 毫秒。
- BestAvailableRule:最可用策略。选择并发量最小的 provider,即连接的消费者数量最少的 provider。
- AvailabilityFilteringRule:可用过滤算法。该算法规则是,先采用轮询方式选择一个 Server,然后判断其是否处于熔断状态,是否已经超过连接极限。若没有,则直接选择。否则再重新按照相同的方式进行再选择。最多重试 10 次。若 10 次后仍没有找到,则重新将所有 Server 进行判断,挑选出所有未熔断,未超过连接极限的 Server,然后再采用轮询方式选择一个。若还没有符合条件的,则返回 null。
- ZoneAvoidanceRule:zone 回避策略。根据 provider 所在 zone 及 provider 的可用性,对 provider 进行选择。
- WeightedResponseTimeRule:“权重响应时间”策略。根据每个 provider 的平均响应时间计算其权重,响应时间越快权重越大,被选中的机率就越高。在刚启动时采用轮询策略。后面就会根据权重进行选择了。
更换内置策略 03-consumer-lb-8080
- Ribbon 默认采用的是 RoundRobinRule,即轮询策略。但通过修改消费者工程的配置文件,或修改消费者的启动类或 JavaConfig 类可以实现更换负载均衡策略的目的。
- 复制 03-consumer-feign-8080 工程,并重命名为 03-consumer-lb-8080。
- 方式一 修改 application.yml 配置文件:在其中添加如下内容
# 修改负载均衡策略
msc-provider-depart: # 要负载均衡的提供者微服务名称
ribbon:
# 指定要使用的负载均衡策略
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
- 方式二 修改 JavaConfig 类:在 JavaConfig 类中添加负载负载 Bean 方法,这种方式的优先级比配置文件中的要高
@Configuration
public class ConsumerConfig {
// 指定Ribbon使用随即算法策略
@Bean
public IRule loadBalanceRule() {
return new RandomRule();
}
}
自定义负载均衡策略
- Ribbon 支持自定义负载均衡策略,自定义实现的负载均衡算法类需要实现 IRule 接口。
- 自定义 CustomRule 类:从所有可用的 provider 中排除掉指定端口号的 provider,剩余 provider 进行随机选择。
public class CustomRule implements IRule {
private ILoadBalancer lb;
// 要排除的提供者的服务端口号
private List<Integer> excludePorts;
public CustomRule() {
}
public CustomRule(List<Integer> excludePorts) {
this.excludePorts = excludePorts;
}
@Override
public void setLoadBalancer(ILoadBalancer lb) {
this.lb = lb;
}
@Override
public ILoadBalancer getLoadBalancer() {
return lb;
}
@Override
public Server choose(Object key) {
// 获取所有可用的提供者主机
List<Server> servers = lb.getReachableServers();
// 获取所有排除了指定端口号的提供者
List<Server> availableServers = this.getAvailableServers(servers);
// 从剩余的提供者中随机获取可用的提供者
return this.getAvailableRandomServers(availableServers);
}
// 获取所有排除了指定端口号的提供者
private List<Server> getAvailableServers(List<Server> servers) {
// 若没有要排除的主机,则返回所有
if(excludePorts == null || excludePorts.size() == 0) {
return servers;
}
return servers.stream()
.filter(server -> excludePorts.stream().noneMatch(port -> server.getPort() == port))
.collect(Collectors.toList());
}
// 从剩余的提供者中随机获取可用的提供者
private Server getAvailableRandomServers(List<Server> availableServers) {
// 获取一个[0,availableServers.size())的随机数
int index = new Random().nextInt(availableServers.size());
return availableServers.get(index);
}
}
- 修改 JavaConfig 类:将原来的负载均衡 Bean 方法注释掉,添加新的负载均衡策略方法。
@Configuration
public class ConsumerConfig {
// ...
// // 指定Ribbon使用随即算法策略
// @Bean
// public IRule loadBalanceRule(){
// return new RandomRule();
// }
// 指定Ribbon使用自定义负载均衡策略
@Bean
public IRule loadBalanceRule(){
List<Integer> ports = new ArrayList<>();
ports.add(8083);
return new CustomRule(ports);
}
}
OpenFeign源码解析
重要的类与接口解析
@EnableFeignClients
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {
String[] value() default {};
String[] basePackages() default {};
Class<?>[] basePackageClasses() default {};
Class<?>[] defaultConfiguration() default {};
Class<?>[] clients() default {};
}
- @EnableXxx 中一般都会包含@Import 注解,该注解用于导入配置类。有三种:
- 直接导入配置类:一般为 Configuration 结尾的类
- 根据条件选择配置类:一般为 ConfigurationSelector 结尾的类
- 动态注册配置类:一般为 Registrar 结尾
- 进入 FeignClientsRegistrar
- 我们找到 registerBeanDefinitions 方法:打断点查看
- 再进入到 registerFeignClients 方法中,找到 registerFeignClient 方法,断点查看
- 执行解析:访问 http://localhost:8080/consumer/depart/get/1,在对应的控制器方法中打断点查看
- 由以上源码分析可以知道,Feign的底层仍然是用的 RestTemplate。
@FeignClient
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface FeignClient {
@AliasFor("name")
String value() default "";
@Deprecated
String serviceId() default "";
String contextId() default "";
@AliasFor("value")
String name() default "";
String qualifier() default "";
String url() default "";
boolean decode404() default false;
Class<?>[] configuration() default {};
Class<?> fallback() default void.class;
Class<?> fallbackFactory() default void.class;
String path() default "";
boolean primary() default true;
}
Ribbon源码解析
跟踪RibbonLoadBalancerClient类
- 分析入口:@LoadBalanced——开启消费者客户端的负载均衡功能
- 再来看一下 LoadBalancerClient 接口的实现类 RibbonLoadBalancerClient:
- 其中 rule:
解析IRule接口
- 查看一下 IRule 接口:
- 该接口的实现类:
Ribbon内置负载均衡算法
RoundRobinRule
- 轮询策略:Ribbon 默认采用的策略。
public class RoundRobinRule extends AbstractLoadBalancerRule {
// 原子整形,用于获取下一个Server的循环计数器
private AtomicInteger nextServerCyclicCounter;
private static final boolean AVAILABLE_ONLY_SERVERS = true;
private static final boolean ALL_SERVERS = false;
private static Logger log = LoggerFactory.getLogger(RoundRobinRule.class);
public RoundRobinRule() {
nextServerCyclicCounter = new AtomicInteger(0);
}
public RoundRobinRule(ILoadBalancer lb) {
this();
setLoadBalancer(lb);
}
public Server choose(ILoadBalancer lb, Object key) {
if (lb == null) {
log.warn("no load balancer");
return null;
}
Server server = null;
int count = 0;
while (server == null && count++ < 10) { // 默认尝试获取10次
List<Server> reachableServers = lb.getReachableServers();
List<Server> allServers = lb.getAllServers();
int upCount = reachableServers.size();
int serverCount = allServers.size();
if ((upCount == 0) || (serverCount == 0)) {
log.warn("No up servers available from load balancer: " + lb);
return null;
}
// 轮询获取
int nextServerIndex = incrementAndGetModulo(serverCount);
// 这里是从allServers获取,所以有可能获取到不可用的server
server = allServers.get(nextServerIndex);
if (server == null) {
/* Transient. */
Thread.yield(); // 线程让步,把CPU的使用权让出去
continue;
}
if (server.isAlive() && (server.isReadyToServe())) {
return (server);
}
// Next.
server = null;
}
if (count >= 10) {
log.warn("No available alive servers after 10 tries from load balancer: " + lb);
}
return server;
}
private int incrementAndGetModulo(int modulo) {
for (;;) {
int current = nextServerCyclicCounter.get();
int next = (current + 1) % modulo;
if (nextServerCyclicCounter.compareAndSet(current, next))
return next;
}
}
@Override
public Server choose(Object key) {
return choose(getLoadBalancer(), key);
}
// 省略 initWithNiwsConfig() ...
}
RandomRule
- 随即策略:从所有可用的 provider 中随机选择一个。
public class RandomRule extends AbstractLoadBalancerRule {
public Server choose(ILoadBalancer lb, Object key) {
if (lb == null) {
return null;
}
Server server = null;
while (server == null) {
if (Thread.interrupted()) {
return null;
}
List<Server> upList = lb.getReachableServers();
List<Server> allList = lb.getAllServers();
int serverCount = allList.size();
if (serverCount == 0) {
return null;
}
// 随机选择
int index = chooseRandomInt(serverCount);
// 这里是一个Bug,很可能就会数组越界
server = upList.get(index);
if (server == null) {
Thread.yield();
continue;
}
if (server.isAlive()) {
return (server);
}
// Shouldn't actually happen.. but must be transient or a bug.
server = null;
Thread.yield();
}
return server;
}
protected int chooseRandomInt(int serverCount) {
return ThreadLocalRandom.current().nextInt(serverCount);
}
@Override
public Server choose(Object key) {
return choose(getLoadBalancer(), key);
}
// 省略 initWithNiwsConfig() ...
}
RetryRule
- 先按照 RoundRobinRule 策略获取 provider,若获取失败,则在指定的时限内重试。默认的时限为 500 毫秒。
public class RetryRule extends AbstractLoadBalancerRule {
IRule subRule = new RoundRobinRule(); // 子策略:轮询
long maxRetryMillis = 500; // 最大重试时间,500ms写死,不过可以修改
public RetryRule() {
}
public RetryRule(IRule subRule) {
this.subRule = (subRule != null) ? subRule : new RoundRobinRule();
}
public RetryRule(IRule subRule, long maxRetryMillis) {
this.subRule = (subRule != null) ? subRule : new RoundRobinRule();
this.maxRetryMillis = (maxRetryMillis > 0) ? maxRetryMillis : 500;
}
public void setRule(IRule subRule) {
this.subRule = (subRule != null) ? subRule : new RoundRobinRule();
}
public IRule getRule() {
return subRule;
}
public void setMaxRetryMillis(long maxRetryMillis) {
if (maxRetryMillis > 0) {
this.maxRetryMillis = maxRetryMillis;
} else {
this.maxRetryMillis = 500;
}
}
public long getMaxRetryMillis() {
return maxRetryMillis;
}
@Override
public void setLoadBalancer(ILoadBalancer lb) {
super.setLoadBalancer(lb);
subRule.setLoadBalancer(lb);
}
public Server choose(ILoadBalancer lb, Object key) {
long requestTime = System.currentTimeMillis();
long deadline = requestTime + maxRetryMillis;
Server answer = null;
answer = subRule.choose(key);
if (((answer == null) || (!answer.isAlive())) && (System.currentTimeMillis() < deadline)) {
InterruptTask task = new InterruptTask(deadline - System.currentTimeMillis());
while (!Thread.interrupted()) {
answer = subRule.choose(key);
if (((answer == null) || (!answer.isAlive()))
&& (System.currentTimeMillis() < deadline)) {
/* pause and retry hoping it's transient */
Thread.yield();
} else {
break;
}
}
task.cancel();
}
if ((answer == null) || (!answer.isAlive())) {
return null;
} else {
return answer;
}
}
@Override
public Server choose(Object key) {
return choose(getLoadBalancer(), key);
}
// 省略 initWithNiwsConfig() ...
}
BestAvailableRule
- 选择并发量最小的 provider,即连接的消费者数量最少的 provider。
public class BestAvailableRule extends ClientConfigEnabledRoundRobinRule {
private LoadBalancerStats loadBalancerStats;
@Override
public Server choose(Object key) {
if (loadBalancerStats == null) {
return super.choose(key);
}
List<Server> serverList = getLoadBalancer().getAllServers();
int minimalConcurrentConnections = Integer.MAX_VALUE;
long currentTime = System.currentTimeMillis();
Server chosen = null;
for (Server server: serverList) {
// 获取服务器状态
ServerStats serverStats = loadBalancerStats.getSingleServerStat(server);
// 判断是否处于熔断状态
if (!serverStats.isCircuitBreakerTripped(currentTime)) {
int concurrentConnections = serverStats.getActiveRequestsCount(currentTime);
// 冒泡获取请求连接数量最少的那个服务器
if (concurrentConnections < minimalConcurrentConnections) {
minimalConcurrentConnections = concurrentConnections;
chosen = server;
}
}
}
if (chosen == null) {
return super.choose(key);
} else {
return chosen;
}
}
@Override
public void setLoadBalancer(ILoadBalancer lb) {
super.setLoadBalancer(lb);
if (lb instanceof AbstractLoadBalancer) {
loadBalancerStats = ((AbstractLoadBalancer) lb).getLoadBalancerStats();
}
}
}
AvailabilityFilteringRule
- 可用过滤策略:该算法规则是过滤掉处于断路器跳闸状态的 provider,或已经超过连接极限的 provider,对剩余 provider 采用轮询策略。
public class AvailabilityFilteringRule extends PredicateBasedRule {
private AbstractServerPredicate predicate;
public AvailabilityFilteringRule() {
super();
predicate = CompositePredicate.withPredicate(new AvailabilityPredicate(this, null))
.addFallbackPredicate(AbstractServerPredicate.alwaysTrue())
.build();
}
@Override
public void initWithNiwsConfig(IClientConfig clientConfig) {
predicate = CompositePredicate.withPredicate(new AvailabilityPredicate(this, clientConfig))
.addFallbackPredicate(AbstractServerPredicate.alwaysTrue())
.build();
}
@Monitor(name="AvailableServersCount", type = DataSourceType.GAUGE)
public int getAvailableServersCount() {
ILoadBalancer lb = getLoadBalancer();
List<Server> servers = lb.getAllServers();
if (servers == null) {
return 0;
}
return Collections2.filter(servers, predicate.getServerOnlyPredicate()).size();
}
@Override
public Server choose(Object key) {
int count = 0;
Server server = roundRobinRule.choose(key);
while (count++ <= 10) {
if (predicate.apply(new PredicateKey(server))) {
return server;
}
server = roundRobinRule.choose(key);
}
return super.choose(key);
}
@Override
public AbstractServerPredicate getPredicate() {
return predicate;
}
}
↓↓↓↓↓
public abstract class PredicateBasedRule extends ClientConfigEnabledRoundRobinRule {
public abstract AbstractServerPredicate getPredicate();
@Override
public Server choose(Object key) {
ILoadBalancer lb = getLoadBalancer();
// 轮询选择
Optional<Server> server = getPredicate().chooseRoundRobinAfterFiltering(lb.getAllServers(), key);
if (server.isPresent()) {
return server.get();
} else {
return null;
}
}
}
public class AvailabilityPredicate extends AbstractServerPredicate {
// 省略...
@Override
public boolean apply(@Nullable PredicateKey input) {
LoadBalancerStats stats = getLBStats();
if (stats == null) {
return true;
}
return !shouldSkipServer(stats.getSingleServerStat(input.getServer()));
}
private boolean shouldSkipServer(ServerStats stats) {
if ((CIRCUIT_BREAKER_FILTERING.get() && stats.isCircuitBreakerTripped())
|| stats.getActiveRequestsCount() >= activeConnectionsLimit.get()) {
return true;
}
return false;
}
}
ZoneAvoidanceRule
- 复合判断 provider 所在区域的性能及 provider 的可用性选择服务器。
WeightedResponseTimeRule
- “权重响应时间”策略。根据每个 provider 的平均响应时间计算其权重,响应时间越快权重越大,被选中的机率就越高。在刚启动时采用轮询策略。后面就会根据权重进行选择了。
- 源码github地址:https://github.com/shouwangyw/springcloud