spring cloud深入学习(四)-----eureka源码解析、ribbon解析、声明式调用feign

基本概念

1、Registe 一一服务注册
当eureka Client向Eureka Server注册时,Eureka Client提供自身的元数据,比如IP地址、端口、运行状况指标的Uri、主页地址等信息。
2、Renew一一服务续约
Eureka lient在默认的情况下会每隔30秒发送一次心跳来进行服务续约,代表自己依然存活。如果eureka server在90秒内没有收到心跳消息,将eureka client从实例注册列表中删除。
3、Fetch Registries一一获取服务注册列表信息
Eureka Client从Eureka Server获取服务注册表信息,井将其缓存在本地。Eureka Client会使用服务注册列表信息查找其他服务的信息,从而进行远程调用,该注册列表每30秒更新一次。Eureka Client
和Eureka Server 可以使用 JSON XML 数据格式进行通信。在默认的情况下, Eureka Client
使用JSON 格式的方式来获取服务注册列表的信息。
4、Cancel-一服务下线
Eureka Client在程序关闭时可以向Eureka Server发送下线请求。不过下线请求不会自动完成,需要在程序关闭时调用如下代码:

DiscoveryManager.getinstance().shutdownComponent(); 

5、Eviction一一服务剔除
当Eureka Client90秒没有向Eureka Server发送服务续约(即心跳)时,Eureka Server会将该服务实例从服务注册列表删除,即服务剔除。

Register服务注册

boolean register() throws Throwable {
logger.info(PREFIX + appPathidentifier + ”: registering service ... ” );
EurekaHttpResponse<Void> httpResponse;
try {
httpResponse = eurekaTransport.registrationClient.register(instanceinfo);
}catch(Exception e) {
logger.warn(”{} - registration failed {} ”, PREFIX + appPathidentifier, e.getMessage(), e};
throw e;
}
if logger.isInfoEnabled()) {
logger.info(”{} - registration status: {} ”, PREFIX + appPathidentifier, httpResponse.getStatusCode());
} return httpResponse.getStatusCode() == 204 ;
}

服务续约

boolean renew () {
EurekaHttpResponse<Instanceinfo> httpResponse;
try {
httpResponse = eurekaTransport.registrationClient.sendHeartBeat(InstanceInfo.getAppName(), instanceinfo.getid() ,instanceinfo , null);
logger.debug (”{) - Heartbeat status: {) ”, PREFIX + appPathidentifier, httpResponse.getStatusCode());
if {httpResponse. getStatusCode {) == 4 04) {
REREGISTER COUNTER.increment() ;
logger.info(” {) - Re-registering apps/{) ”, PREFIX + appPathidentifier, instanceinfo.getAppName{));
return register();
} return httpResponse. getStatusCode () == 200;
)catch(Throwable e) {
logger.error(” {) - was unable to send heartbeat !”, PREFIX + appPathidentifier, e);
return false;

为什么获取服务实例特别慢?

1、Eureka Client的注册延迟

Eureka Client启动之后,不是立即向Eureka Server注册的,而是有一个延迟向服务端注册的时间。通过跟踪源码,可以发现默认的延迟时间为 40 秒。

2、Eureka Server的响应缓存

Eureka Server维护每30秒更新一次响应缓存,可通过更改配置eureka.server.responseCacheUpdatelntervalMs来修改。

3、LoadBalancer的缓存

Ribbon的负载平衡器从本地的Eureka Consumer获取服务注册列表信息。Ribbon本身还维护了缓存,以避免每个请求都需要从Eureka Consumer获取服务注册列表。此缓存每30秒刷新一次。

Ribbon

概念

Spring Cloud Ribbon是基于Netflix Ribbon实现的一套客户端负载均衡的工具。 简单的说,Ribbon是Netflix发布的开源项目,主要功能是提供客户端的软件负载均衡算法,将Netflix的中间层服务连接在一起。Ribbon客户端组件提供一系列完善的配置项如连接超时,重试等。简单的说,就是在配置文件中列出Load Balancer(简称LB)后面所有的机器,Ribbon会自动的帮助你基于某种规则(如简单轮询,随机连接等)去连接这些机器。我们也很容易使用Ribbon实现自定义的负载均衡算法。                    

RestTemplate

RestTemplate是用来消费REST服务的,所以RestTemplate的主要方法都与REST的http协议的一些方法紧密相连,例如HEAD GET POST PUT DELETE OPTIONS等方法,对应的RestTemplate中的方法为headForHeaders()、getForObject()、postForObject()、put()和delete()等。

示例:获取百度的html

@RestController
public class RestTestController {
@GetMapping (”/testRest”)
public String testRest() {
RestTemplate restTemplate=new RestTemplate() ;
//代表发送get请求,并且返回结果为String类型
return restTemplate.getForObject(”https://www.baidu.com/”, String.class) ;
}
}

负载均衡

LB,即负载均衡(Load Balance),在微服务或分布式集群中经常用的一种应用。负载均衡简单的说就是将用户的请求平摊的分配到多个服务上,从而达到系统的HA。常见的负载均衡有软件Nginx,LVS,硬件 F5等。相应的在中间件,例如:dubbo和SpringCloud中均给我们提供了负载均衡,SpringCloud的负载均衡算法可以自定义。分为集中式LB以及进程内LB:

1、集中式LB

集中式LB即在服务的消费方和提供方之间使用独立的LB设施(可以是硬件,如F5, 也可以是软件,如nginx), 由该设施负责把访问请求通过某种策略转发至服务的提供方;

2、进程内LB

进程内LB 将LB逻辑集成到消费方,消费方从服务注册中心获知有哪些地址可用,然后自己再从这些地址中选择出一个合适的服务器。Ribbon就属于进程内LB,它只是一个类库,集成于消费方进程,消费方通过它来获取到服务提供方的地址。

简单来说,一个在服务端做负载均衡,一种则提前在客户端做负载均衡,Ribbon则是后面一种。

Ribbon

Spring Cloud构建的微服务系统中,Ribbon作为服务消费者的负载均衡器,有两种使用方式,一种是和RestTemplate相结合,另一种是和Feign 相结合。

在本人的上一篇博文中有Ribbon的demo:spring cloud深入学习(三)-----服务消费

@Configuration
public class RibbonConfig {
@Bean
@LoadBalanced
RestTemplate restTemplate () {
return new RestTemplate();
}
}

Ribbon实现方式:RestTemplate + @LoadBalanced

LoadBalancerClient

LoadBalancerClient是实现负载均衡的核心类,可以通过如下方式获取到服务实例:

@RestController
public class RibbonController { @Autowired
private LoadBalancerClient loadBalancer; @GetMapping(”/testRibbon”)
public String testRibbon() {
Serviceinstance instance = loadBalancer.choose(”eureka-provider");
return instance.getHost() + ”:” + instance.getPort();
}
}

总结:RestTemplate加上@LoadBalance注解后,维护了一个被@LoadBalance注解的RestTemplate列表;
RestTemplate列表中的对象通过拦截器将调用交给Ribbon的LoadBalancerClient去处理;
LoadBalancerClient具体交给了ILoadBalancer来处理,ILoadBalancer通过配置IRule、IPing等,获取注册列表的信息,默认10秒向EurekaClient 发送一次“ping”,检查是否要更新服务的注册列表信息;
在得到服务注册列表信息后,ILoadBalancer根据IRule策略进行负载均衡。

IRule

Ribbon的负载算法默认分为七种:

RoundRobinRule、RandomRule、AvailabilityFilteringRule、WeightedResponseTimeRule、RetryRule、BestAvailableRule、ZoneAvoidanceRule

RoundRobinRule

轮询,默认就是这种算法,例如provider会有三台服务器,那么顺序可能就是1-->2-->3-->1-->2-->3..

RandomRule

随机,每一次调用都是从可用的provider中随机选取一台

AvailabilityFilteringRule

会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,还有并发的连接数量超过阈值的服务,然后对剩余的服务列表按照轮询策略进行访问

WeightedResponseTimeRule

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

RetryRule

先按照RoundRobinRule的策略获取服务,如果获取服务失败则在指定时间内会进行重试,获取可用的服务

BestAvailableRule

会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务

ZoneAvoidanceRule

默认规则,复合判断server所在区域的性能和server的可用性选择服务器

自定义负载策略

@Configuration
public class ConfigBean //boot -->spring applicationContext.xml --- @Configuration配置 ConfigBean = applicationContext.xml
{
@Bean
@LoadBalanced//Spring Cloud Ribbon是基于Netflix Ribbon实现的一套客户端 负载均衡的工具。
public RestTemplate getRestTemplate()
{
return new RestTemplate();
} @Bean
public IRule myRule()
{
//return new RoundRobinRule();
//return new RandomRule();//达到的目的,用我们重新选择的随机算法替代默认的轮询。
//这地方可以实现自己的负载策略即可
return new RetryRule();
}
}

Feign

官网解释:http://projects.spring.io/spring-cloud/spring-cloud.html#spring-cloud-feign

Feign是一个声明式WebService客户端。使用Feign能让编写Web Service客户端更加简单, 它的使用方法是定义一个接口,然后在上面添加注解,同时也支持JAX-RS标准的注解。Feign也支持可拔插式的编码器和解码器。Spring Cloud对Feign进行了封装,使其支持了Spring MVC标准注解和HttpMessageConverters。Feign可以与Eureka和Ribbon组合使用以支持负载均衡,只需要创建一个接口,然后在上面添加注解即可。前面在使用Ribbon+RestTemplate时,利用RestTemplate对http请求的封装处理,形成了一套模版化的调用方法。但是在实际开发中,由于对服务依赖的调用可能不止一处,往往一个接口会被多处调用,所以通常都会针对每个微服务自行封装一些客户端类来包装这些依赖服务的调用。所以,Feign在此基础上做了进一步封装,由他来帮助我们定义和实现依赖服务接口的定义。在Feign的实现下,我们只需创建一个接口并使用注解的方式来配置它(以前是Dao接口上面标注Mapper注解,现在是一个微服务接口上面标注一个Feign注解即可),即可完成对服务提供方的接口绑定,简化了使用Spring cloud Ribbon时,自动封装服务调用客户端的开发量。  Feign集成了Ribbon利用Ribbon维护了MicroServiceCloud-Dept的服务列表信息,并且通过轮询实现了客户端的负载均衡。而与Ribbon不同的是,通过feign只需要定义服务绑定接口且以声明式的方法,优雅而简单的实现了服务调用。

Spring Cloud Feign是一套基于Netflix Feign实现的声明式服务调用客户端。它使得编写Web服务客户端变得更加简单。我们只需要通过创建接口并用注解来配置它既可完成对Web服务接口的绑定。它具备可插拔的注解支持,包括Feign注解、JAX-RS注解。它也支持可插拔的编码器和解码器。Spring Cloud Feign还扩展了对Spring MVC注解的支持,同时还整合了Ribbon和Eureka来提供均衡负载的HTTP客户端实现。

1、pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.ty</groupId>
<artifactId>eureka-feign-consumer</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>eureka-feign-consumer</name>
<description>Demo project for Spring Boot</description> <properties>
<java.version>1.8</java.version>
</properties> <dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency> <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency> <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency> <dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
<version>2.1.1.RELEASE</version>
</dependency> <dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>2.1.1.RELEASE</version>
</dependency> </dependencies> <dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.RELEASE</version>
<type>pom</type>
</dependency> </dependencies>
</dependencyManagement> <build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build> </project>

2、application.properties

# 服务名称
spring.application.name=eureka-feign-consumer
# 端口号
server.port=4001
# 服务注册中心地址
eureka.client.serviceUrl.defaultZone=http://localhost:1001/eureka/

3、主类

package com.ty.eurekafeignconsumer;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.openfeign.EnableFeignClients; @SpringBootApplication
@EnableEurekaClient
//让程序拥有feign的功能
@EnableFeignClients
public class EurekaFeignConsumerApplication { public static void main(String[] args) {
SpringApplication.run(EurekaFeignConsumerApplication.class, args);
} }

4、controller

package com.ty.eurekafeignconsumer.controller;

import com.ty.eurekafeignconsumer.service.EurekaClientFeign;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; @RestController
public class FeignController { @Autowired
private EurekaClientFeign eurekaClientFeign; @GetMapping("/hi")
public String sayHi(@RequestParam(defaultValue = "马云", required = false) String name) {
return eurekaClientFeign.sayHiFromClientEureka(name);
}
}

5、service

package com.ty.eurekafeignconsumer.service;

import com.ty.eurekafeignconsumer.config.FeignConfig;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam; @FeignClient(value = "eureka-provider", configuration = FeignConfig.class)
public interface EurekaClientFeign { //只需要在接口中定义方法即可。调用eureka-provider服务的firstCall方法,并且feign集成了Ribbon
@GetMapping(value = "/firstCall")
String sayHiFromClientEureka(@RequestParam("name") String name);
}

6、config

package com.ty.eurekafeignconsumer.config;

import feign.Retryer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import static java.util.concurrent.TimeUnit.SECONDS; @Configuration
public class FeignConfig { @Bean
public Retryer feignRetryer() {
//调用远程服务失败后,会进行重试。重试间隔为1s,最大尝试次数为5
return new Retryer.Default(100, SECONDS.toMillis(1), 5);
}
}

运行结果:

spring cloud深入学习(四)-----eureka源码解析、ribbon解析、声明式调用feign

原理:@FeignClient注解用于创建声明式API接口,该接口是RSTful风格的。Feign是个伪Java Http客户端,Feign不做任何的请求处理。Feign通过处理注解生成request模板,从而简化Http API的开发。在发送Http Request 请求之前Feign通过处理注解的方式替换掉Request模板中的参数,生成真正的Request,并交给Java Http客户端去处理。

请求架构图如下:

spring cloud深入学习(四)-----eureka源码解析、ribbon解析、声明式调用feign

总结:feign处理过程如下:

  • 首先通过@EnableFeignClients注解开启FeignClient的功能。只有这个注解存在,才会在程序启动时开启对@FeignClient注解的包扫描;
  • 根据Feign的规则实现接口,井在接口上面加上@FeignClient注解;
  • 程序启动后,会进行包扫描,扫描所有的@FeignClient注解的类,并将这些信息注入IoC容器中;
  • 当接口的方法被调用时,通过JDK的代理来生成具体的RequestTemplate模板对象;
  • 根据RequestTemplate再生成Http请求的Request对象;
  • Request对象交给Client去处理,其中Client的网络请求框架可以是HttpURLConnection、HttpClient和OkHttp;
  • 最后Client被封装到LoadBalanceClient类,这个类结合类Ribbon做到了负载均衡

代码已上传到github,可自行下载,记得给小心心哦!

eureka-feign-consumer地址:https://github.com/ali-mayun/eureka-feign-consumer

上一篇:我的Java开发学习之旅------>Java 格式化类(java.util.Formatter)基本用法


下一篇:《精解Windows 10》