服务调用
完成微服务之间的分布式调用
一、Ribbon
1. 概述
1.1 是什么
Spring Cloud Ribbon是基于Netflix Ribbon实现的一套客户端负载均衡【消费者侧80】的工具。
简单的说,Ribbon是Netflix发布的开源项目,主要功能是提供客户端的软件负载均衡算法和服务调用。Ribbon客户端组件提供- 系列完善的配置项如连接超时,重试等。简单的说,就是在配置文件中列出Load Balancer (简称LB)后面所有的机器,Ribbon会自动的帮助你基于某种规则(如简单轮询,随机连接,最大权限等)去连接这些机器。我们很容易使用Ribbon实现自定义的负载均衡算法。
1.2 官网资料
官网地址:https://github.com/Netflix/ribbon/wiki/Getting-Started
目前Ribbon已经进入维护了,但Ribbon中的几个模块目前仍在大规模使用中,未来的趋势是Spring Cloud Loadbalancer,但目前主流还是Ribbon
![](https://gitee.com/honourer/picturebed/raw/master/SpringCloud/图像 (10).png)
![](https://gitee.com/honourer/picturebed/raw/master/SpringCloud/图像 (11).png)
1.3 能干嘛
LB(负载均衡)【主要功能】
LB(负载均衡)的分类:集中式LB和进程内LB
LB负载均衡(Load Balance)是什么
简单的说就是将用户的请求平摊的分配到多个服务上,从而达到系统的HA (高可用)。常见的负载均衡有软件Nginx, LVS, 硬件F5等。
Ribbon本地负载均衡客户端VS Nginx服务端负载均衡区别
Nginx是服务器负载均衡【集中式LB,医院大门】,客户端所有请求都会交给nginx,然后由nginx实现转发请求。即负载均衡是由服务端实现的。
Ribbon本地负载均衡【进程内LB,医院科室】,在调用微服务接口时候,会在注册中心上获取注册信息服务列表之后缓存到JVM本地,从而在本地实现RPC远程服务调用技术。
集中式LB
即在服务的消费方和提供方之间使用独立的LB设施(可以是硬件,如F5, 也可以是软件,如nginx), 由该设施负责把访问请求通过某种策略转发至服务的提供方;
进程内LB
将LB逻辑集成到消费方,消费方从服务注册中心获知有哪些地址可用,然后自己再从这些地址中选择出一个合适的服务器。
Ribbon就属于进程内LB,它只是一个类库,集成于消费方进程,消费方通过它来获取到服务提供方的地址。
总结:Ribbon就是客户端的负载均衡工具,并配合RestTemplate实现PRC的远程调用
2. Ribbon负载均衡演示
Ribbon的工作流程
![](https://gitee.com/honourer/picturebed/raw/master/SpringCloud/图像 (12).png)
Ribbon在工作时分成两步
第一步先选择EurekaServer ,它优先选择在同一个区域内负载较少的server.
第二步再根据用户指定的策略,在从server取到的服务注册列表中选择一个地址。
其中Ribbon提供了多种策略:比如轮询、随机和根据响应时间加权。
总结:Ribbon其实就是一个软负载均衡【软件实现负载均衡】的客户端组件,他可以和其他所需请求的客户端结合使用,和eureka结合只是其中的一个实例。
第5章 3.6 结束的集群中使用Ribbon实现了负载均衡,但在POM中我们没有引入Ribbon,但Ribbon依然可以使用
这是因为spring-cloud-starter-netflix-eureka-client中引入了Ribbon,可以直接使用
如果需要单独引入Ribbon,引入方式如下
<dependency>
<groupld>org.springframework.cloud</groupld>
<artifactld>spring-cloud-starter-netflix-ribbon</artifactld>
</dependency>
RestTemplate的使用
RestTemplate是在服务消费者【80】中使用的
RestTemplate中主要的方法及用法
-
getForObject方法/getForEntity方法
getForObject方法
@GetMapping("/consumer/payment/get/{id}") public CommonResult<Payment> getPayment(@PathVariable("id")Long id){ // 返回对象为响应体中数据转化成的对象,基本上可以理解为Json return restTemplate.getForObject(PAYMENT_URL + "/payment/get/" + id, CommonResult.class); }
getForEntity方法
@GetMapping("/consumer/payment/getEntity/{id}") public CommonResult<Payment> getEntityPayment(@PathVariable("id")Long id){ // 返回对象为ResponseEntity对象,该对象中包含了响应中的一些重要信息, 比如响应头.响应状态码、响应体等 ResponseEntity<CommonResult> forEntity = restTemplate.getForEntity(PAYMENT_URL + "/payment/get/" + id, CommonResult.class); // < // 200,CommonResult(code=200, message=查询成功, data={id=1, serial=aaaaa}), // [ // Content-Type:"application/json", // Transfer-Encoding:"chunked", // Date:"Thu, 24 Nov 2022 07:53:33 GMT", // Keep-Alive:"timeout=60", // Connection:"keep-alive" // ] // > log.info(String.valueOf(forEntity)); // ResponseEntity对象中的几个常用方法 // forEntity.getBody():获取响应体 // forEntity.getStatusCode():获取响应状态码 // forEntity.getHeaders():获取响应头 return forEntity.getBody(); }
-
postForObject/postForEntity方法
同getForObject方法/getForEntity方法
3. Ribbon核心组件IRule
IRule:根据特定算法从服务列表中选取一个要访问的服务
IRule接口的类图【落地实现类】
IRule默认自带的负载均衡算法
- com.netflix.loadbalancer.RoundRobinRule:轮询
- com.netflix.loadbalancer.RandomRule:随机
- com.netflix.loadbalancer.RetryRule:先按照RoundRobinRule的策略获取服务,如果获取服务失败则在指定时间内会进行重试
- WeightedResponseTimeRule:对RoundRobinRule的扩展,响应速度越快的实例选择权重越大,越容易被选择
- BestAvailableRule:会先过滤掉由于多次访问故障而处于断路器跳闸状态【熔断、限流】的服务,然后选择一个并发量最小的服务
- AvailabilityFilteringRule:先过滤掉故障实例,再选择并发较小的实例
- ZoneAvoidanceRule:默认规则,复合判断server所在区域的性能和server的可用性选择服务器
不同负载均衡算法之间如何切换
注:默认使用的负载均衡算法是轮询
-
客户端【服务消费者80】下新建包com.atguigu.myrule
官方文档明确给出了警告:
我们想要实现切换就要增加一个配置类,这个自定义配置类不能放在@ComponentScan所扫描的当前包下以及子包下,否则我们自定义的这个配置类就会被所有的Ribbon客户端所共享,达不到特殊化定制的目的了。@ComponentScan所扫描的当前包:@SpringBootApplication注解所在的类所在的包,这是因为 @SpringBootApplication注解中包含了@ComponentScan这个注解,在这里就是我们不能将自定义的配置类放到
com.atguigu.springcloud
包及其子包下我们新建的包如下
-
在新建的包下创建MySelfRule规则类
package com.atguigu.myrule; import com.netflix.loadbalancer.IRule; import com.netflix.loadbalancer.RandomRule; import org.springframework.context.annotation.Configuration; @Configuration public class MySelfRule { public IRule myRule(){ // 切换负载均衡算法为随机算法 return new RandomRule(); } }
-
主启动类添加@RibbonClient
//指定访问CLOUD-PAYMENT-SERVICE服务时使用的负载均衡算法的配置类为MySelfRule @RibbonClient(name = "CLOUD-PAYMENT-SERVICE",configuration = MySelfRule.class)
4. Ribbon负载均衡算法
4.1 轮询算法原理
rest接口第几次请求数%服务器集群总数量=实际调用服务器位置下标,每次服务重启动后rest接口计数从1开始。
List<ServiceInstance>instances = discoveryClient.getInstances("CLOUD PAYMENT- SERVICE");
如: List [0] instances = 127.0.0.1:8002
List [1] instances = 127.0.0.1:8001
8001+ 8002组合成为集群,它们共计2台机器,集群总数为2,按照轮询算法原理:
当总请求数为1时: 1 %2 =1对应下标位置为1,则获得服务地址为127.0.0.1:8001
当总请求数位2时: 2 %2 =0对应下标位置为0,则获得服务地址为127.0.0.1:8002
当总请求数位3时: 3 %2 =1对应下标位置为1,则获得服务地址为1 27.0.0.1:8001
当总请求数位4时: 4 % 2 =0对应下标位置为0,则获得服务地址为1 27.0.0.1:8002
如此类推....
4.2 轮询源码分析
核心源码如下
// 定义一个原子整型变量
private AtomicInteger nextServerCyclicCounter;
public RoundRobinRule() {
// 原子整型变量的值默认为0
nextServerCyclicCounter = new AtomicInteger(0);
}
public Server choose(ILoadBalancer lb, Object key) {
...
// 找到的服务提供者的实例
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)) {
log.warn("No up servers available from load balancer: " + lb);
return null;
}
// 获取到下次要访问的服务实例的下标
int nextServerIndex = incrementAndGetModulo(serverCount);
// 根据服务实例下标获取到指定的服务实例
server = allServers.get(nextServerIndex);
if (server == null) {
/* Transient. */
Thread.yield();
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;
}
// CAS+自旋锁
private int incrementAndGetModulo(int modulo) {
for (;;) {
// 拿到原子整型变量的值,这个值就是当前访问Rest接口次数 - 1,也就是上次的次数
int current = nextServerCyclicCounter.get();
// 计算出此次要访问的服务实例的下标
int next = (current + 1) % modulo;
// 如果没人动过就返回此次要访问的服务实例的下标
if (nextServerCyclicCounter.compareAndSet(current, next))
return next;
}
}
注:调试代码时的一个现象,使用的是负载均衡中的轮询算法,但在RoundRobinRule的choose方法中打了断点代码不会执行断点处,而执行结果却是按照轮询来的。
4.3 手写轮询算法【非并发版本】
这里的轮询算法未考虑并发的情况,后面补充了JUC高并发的知识后可改写为CAS+自旋锁版本的轮询算法
-
8001/8002微服务改造
Controller添加以下接口
@GetMapping(value = "/payment/lb") public String getPaymentLB(){ return port; }
-
80订单微服务改造
-
ApplicationContextBean去掉@LoadBalanced
-
LoadBalancer接口
package com.atguigu.springcloud.lb; import org.springframework.cloud.client.ServiceInstance; import java.util.List; public interface LoadBalancer { /** * 获取一个服务提供者的实例 * @param serviceInstances:服务提供者的实例列表 * @return */ ServiceInstance getServiceInstance(List<ServiceInstance> serviceInstances); }
-
MyLB
package com.atguigu.springcloud.lb; import lombok.extern.slf4j.Slf4j; import org.springframework.cloud.client.ServiceInstance; import org.springframework.stereotype.Component; import java.util.List; @Slf4j @Component public class MyLB implements LoadBalancer { private Integer next = 1; /** * 获取一个服务提供者的实例 * * @param serviceInstances :服务提供者的实例列表 * @return */ @Override public ServiceInstance getServiceInstance(List<ServiceInstance> serviceInstances) { // rest接口第几次请求数%服务器集群总数量=实际调用服务器位置下标,每次服务重启动后rest接口计数从1开始。 // 服务器集群总数量 int size = serviceInstances.size(); log.info("第" + next + "次访问"); // 实际调用服务器位置下标 int index = next++ % size; return serviceInstances.get(index); } }
-
OrderController
// 注入我们自己的LoadBalancer @Resource private LoadBalancer loadBalancer; // 注入DiscoveryClient,用于获取服务实例列表 @Resource DiscoveryClient discoveryClient; @GetMapping(value = "/consumer/payment/lb") public String getPaymentLB(){ // 获取指定微服务下的服务提供者的实例列表 List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE"); // 如果没有指定的微服务或者存在指定的微服务但没有服务提供者的实例就直接返回 if (instances == null || instances.size() <= 0){ return null; } // 调用我们自定义的轮询算法得到此次应该调用的服务提供者实例 ServiceInstance serviceInstance = loadBalancer.getServiceInstance(instances); // 获取该实例的URI,格式如:http://localhost:8081 URI uri = serviceInstance.getUri(); return restTemplate.getForObject(uri + "/payment/lb", String.class); }
-
二、OpenFeign
1. 概述
1.1 是什么
官网:https://cloud.spring.io/spring-cloud-static/Hoxton.SR1/reference/htmlsingle/#spring-cloud-openfeign
Feign是一个声明式WebService客户端【是用在消费者【80】端的】。 使用Feign能让编写Web Service客户端更加简单。
它的使用方法是定义一个服务接口然后在上面添加注解。Feign也支持可拔插式的编码器和解码器。Spring Cloud对Feign进行了封装,使其支持了Spring MVC标准注解和HttpMessageConverters【OpenFeign继承了Feign的基础上做了一些加强】。 Feign可以与Eureka和Ribbon组合使用以支持负载均衡
Feign发送的是Http请求
总结:Feign是一个声明式的web服务客户端,让编写web服务客户端变得非常容易,只需创建一个微服务接口并在接口上添加注解即可实现微服务接口之间的调用。
GitHub:https://github.com/spring-cloud/spring-cloud-openfeign
1.2 能干嘛
已经有Ribbon可以实现负载均衡和服务调用,为什么还需要OpenFeign
-
Feign能干什么
Feign旨在使编写Java Http客户端变得更容易。
前面在使用Ribbon+ RestTemplate时,利用RestTemplate对http请求的封装处理,形成了-套模版化的调用方法【getForObject、getForEntity】。但是在实际开发中,于对服务依赖的调用可能不止一处,往往一个接口会被多处调用,所以通常都会针对每个微服务自行封装一些客户端类来包装这些依赖服务的调用。所以,Feign在此基础上做了进一步封装, 由他来帮助我们定义和实现依赖服务接口的定义。在Feign的实现下我们只需创建一个接口并使用注解的方式来配置它(以前是Dao接口 上面标注Mapper注解现在是一个微服务接口上面标注一个Feign注解即可),即可完成对服务提供方的接口绑定,简化了使用Spring cloud Ribbon时,自动封装服务调用客户端的开发量。 -
Feign集成了Ribbon
利用Ribbon维护了Payment的服务列表信息,并且通过轮询实现了客户端的负载均衡。而与Ribbon不同的是,通过feign只需要定义服务绑定接口且以声明式的方法,优雅而简单的实现了服务调用
服务接口绑定器
1.3 Feign和OpenFeign两者区别
![](https://gitee.com/honourer/picturebed/raw/master/SpringCloud/图像 (13).png)
2. OpenFeign使用步骤
使用方法:接口+注解(微服务调用接口【服务的提供方和调用方相吻合的接口】+@FeignClient【标注了该注解的接口是通过Feign进行调用的】),用在消费侧【80】
项目架构
使用步骤:
-
建Module【cloud-consumer-feign-order80】
-
改POM
<?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"> <parent> <artifactId>cloud2020</artifactId> <groupId>com.atguigu.springcloud</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>cloud-consumer-feign-order80</artifactId> <!--openfeign:其中包含了Ribbon--> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>com.atguigu.springcloud</groupId> <artifactId>cloud-api-commons</artifactId> <version>${project.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> </project>
-
写YML
server: port: 80 # Feign不需要作为微服务注册进Eureka,仅仅是一个客户端工具 eureka: client: register-with-eureka: false service-url: defaultZone: http://eureka7001.com:7001/eureka, http://eureka7002.com:7002/eureka
-
主启动
通常情况下,使用SpringCloud中的组件【OpenFeign等】一般有两步要做:
①使用:比如@FeignClient
②激活:主启动类上标注@EnableXXX相关的注解
package com.atguigu.springcloud; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.openfeign.EnableFeignClients; @SpringBootApplication @EnableFeignClients // 激活并开启OpenFeign public class OrderFeignMain80 { public static void main(String[] args) { SpringApplication.run(OrderFeignMain80.class, args); } }
-
业务类
业务逻辑接口+@FeignClient配置调用provider服务【新建PaymentFeignService接口并标注注解@FeignClient】
Service
package com.atguigu.springcloud.service; import com.atguigu.springcloud.entities.CommonResult; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.stereotype.Component; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @Component @FeignClient(value = "CLOUD-PAYMENT-SERVICE") // 配置调用的微服务名称 public interface PaymentFeignService { /** * 该接口中可以写与服务提供者的Service/Controller层的方法标识【不包括具体实现】,这里写的是Controller层的方法标识 * 用于配置具体调用的服务 * @GetMapping("/payment/get/{id}"):这里/payment/get/{id}的调用地址实际是通过Feign客户端去Eureka中找到CLOUD-PAYMENT-SERVICE这个微服务下的实例中暴露出来的对应的服务端口地址 * @param id * @return */ @GetMapping("/payment/get/{id}") public CommonResult getPaymentById(@PathVariable("id")Long id); }
Controller
package com.atguigu.springcloud.controller; import com.atguigu.springcloud.entities.CommonResult; import com.atguigu.springcloud.service.PaymentFeignService; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; @RestController public class PaymentFeignController { @Resource private PaymentFeignService paymentFeignService; @GetMapping("/consumer/payment/get/{id}") public CommonResult getPaymentById(@PathVariable("id")Long id){ return paymentFeignService.getPaymentById(id); } }
-
测试
先启动2个eureka集群7001/7002
再启动2个微服务8001/8002
启动80客户端
访问http://localhost/consumer/payment/get/31,Feign自带负载均衡配置项
现象:刚启动时并没有负载均衡的效果,稍微等一段时间后出现负载均衡的效果
-
总结
客户端根据OpenFeignClient指定的微服务名称去调用服务端对外暴露提供的服务
![](https://gitee.com/honourer/picturebed/raw/master/SpringCloud/图像 (14).png)
至此,使用OpenFeign实现服务调用就实现了【集群版Eureka、集群版服务提供者】
OpenFeign与Ribbon+RestTemplate实现服务调用的差异
OpenFeign:消费侧调用服务侧的服务也是通过Controller=》Service这种模式实现的,给人的感觉就像消费侧本身就有服务侧的服务。
Ribbon+RestTemplate:消费侧的Controller去调用服务侧的Controller
3. OpenFeign超时控制
超时:不同微服务之间进行调用【消费侧调用服务侧】会出现超时的问题
对于不同的业务进行执行时间约定
3.1 超时情况
-
服务提供方8001故意写暂停程序
@GetMapping(value = "/payment/feign/timeout") public String paymentFeignTimeout(){ // 通过延时模拟复杂业务流程处理 try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } return port; }
-
服务消费方80添加超时方法
PaymentFeignService
@GetMapping(value = "/payment/feign/timeout") public String paymentFeignTimeout();
-
服务消费方80添加超时方法
OrderFeignController
@GetMapping(value = "/consumer/payment/feign/timeout") public String paymentFeignTimeout(){ // OpenFeign是基于Ribbon的,客户端一般默认等待1s return paymentFeignService.paymentFeignTimeout(); }
-
直接请求服务侧的超时方法是可以正常访问的
-
通过消费侧请求超时方法报错
默认OpenFeign客户端只等待一秒钟, 但是服务端处理需要超过1秒钟,导致OpenFeign客户端不想等待了,直接返回报错。为了避免这样的情况,有时候我们需要设置OpenFeign客户端的超时控制。
3.2 开启超时控制
OpenFeign客户端的超时控制是由底层的Ribbon实现的
![](https://gitee.com/honourer/picturebed/raw/master/SpringCloud/图像 (15).png)
在cloud-consumer-feign-order80【消费侧,客户端】的application.yml中开启OpenFeign客户端超时控制
#设OpenFeign客户端超时时间(OpenFeign默认支持ribbon)
ribbon:
#指的是建立连接所用的时间,适用于网络状况正常的情况下,两端连接所用的时间(5s)
ReadTimeout: 5000
#指的是建立连接后从服务器读取到可用资源所用的时间(5s)
ConnectTimeout: 5000
修改后能够正常获取数据
4. OpenFeign日志打印功能
OpenFeign提供了日志打印功能,我们可以通过配置来调整日志级别,从而了解OpenFeign中Http请求的细节。
说白了就是对OpenFeign接口的调用情况进行监控和输出。【客户端通过OpenFeign调用服务端接口时是需要进行接口测试的,日志打印就能让我们更方便的进行测试】
4.1 日志级别
NONE:默认的,不显示任何日志;
BASIC:仅记录请求方法、URL、 响应状态码及执行时间;
HEADERS:除了BASIC 中定义的信息之外,还有请求和响应的头信息; .
FULL:除了HEADERS中定义的信息之外,还有请求和响应的正文及元数据。
4.2 配置日志bean
cloud-consumer-feign-order80【客户端】中创建配置类
package com.atguigu.springcloud.config;
import feign.Logger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FeignConfig {
// 配置OpenFeign的日志级别为FULL
@Bean
Logger.Level feignLoggerLevel(){
return Logger.Level.FULL;
}
}
4.3 开启日志打印功能
cloud-consumer-feign-order80的application.yml中加入如下配置,以Debug的形式打印Full级别的日志
logging:
level:
# 配置OpenFeign日志以什么级别监控哪个接口【这里的接口指的是标注了@FeignClient注解的接口】,监控的接口以全类名给出
com.atguigu.springcloud.service.PaymentFeignService: Debug
客户端打印出如下日志
标签:调用,服务,OpenFeign,SpringCloud,import,系列,com,Ribbon,客户端 From: https://www.cnblogs.com/wzzzj/p/18039252