1. RestTemplate
RestTemplate提供了多种便捷访问远程Http服务的方法,是一种简单便捷的访问restful服务模板类,是Spring提供的用于访问Rest服务的客户端模板工具集;
使用
使用restTemplate访问restful接口非常的简单粗暴无脑。
(url,requestMapResponseBean.class)这三个参数分别代表 REST请求地址、请求参数、HTTP响应转换被转换成的对象类型。
1.1 RestTemplate-Demo
1.1.1 客户端只写controller
@RestController
@Slf4j
public class OrderController {
//需要访问的目标项目地址
public static final String PARAMENT_URL = "http://localhost:8001";
//在注入之前,别忘了在配置类中注入RestTemplate
@Autowired
private RestTemplate restTemplate;
//因为时浏览器客户端,只有get请求
@GetMapping("/consumer/addPay")
public ResponResult<Void> addPay(Pay pay) {
return restTemplate.postForObject(PARAMENT_URL+"/pay/addPay", pay, ResponResult.class);
}
@GetMapping("/consumer/queryPay")
public ResponResult<List<Pay>> queryPay() {
return restTemplate.getForObject(PARAMENT_URL+"/pay/queryPay", ResponResult.class);
}
}
1.1.2 服务端controller
@RestController
@RequestMapping("pay")
@Slf4j
public class PayController {
@Autowired
private PayService payService;
@GetMapping("queryPay")
public ResponResult<List<Pay>> queryAll() {
return payService.queryAll();
}
@PostMapping("addPay")
public ResponResult<Void> addPay(
@RequestBody Pay pay
) {
return payService.addPay(pay);
}
}
1.1.3 负载均衡
@RestController
@Slf4j
public class OrderController {
//如果是负载均衡,端口写死是个Bug,所以我们采用服务名,不对外暴露ip和端口
// public static final String PARAMENT_URL = "http://localhost:8001";
public static final String PARAMENT_URL = "http://CLOUD-PAYMENT-SERVICE";
@Autowired
private RestTemplate restTemplate;
@GetMapping("/consumer/addPay")
public ResponResult<Void> addPay(Pay pay) {
return restTemplate.postForObject(PARAMENT_URL+"/pay/addPay", pay, ResponResult.class);
}
@GetMapping("/consumer/queryPay")
public ResponResult<List<Pay>> queryPay() {
return restTemplate.getForObject(PARAMENT_URL+"/pay/queryPay", ResponResult.class);
}
}
开启负载均衡
@Configuration
public class OrderConfig {
@Bean("restTemplate")
@LoadBalanced //开启负载均衡,默认轮询的方式
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
}
1.1.4. Discover获取微服务所有服务注册信息
@Autowired
private DiscoveryClient discoveryClient;
@GetMapping("/consumer/discovery")
public Object queryDiscovery() {
//获取所的已经注册的服务名
List<String> services = discoveryClient.getServices();
//获取指定服务名下的具体实列,比如获取CLOUD-PAYMENT-SERVICE
List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
for (ServiceInstance instance : instances) {
String host = instance.getHost();
int port = instance.getPort();
URI uri = instance.getUri();
String instanceId = instance.getInstanceId();
String serviceId = instance.getServiceId();
}
HashMap<String, Object> maps = new HashMap<>();
maps.put("services", services);
maps.put("instances", instances);
return maps;
}
List instances = discoveryClient.getInstances(“CLOUD-PAYMENT-SERVICE”);
2. Ribbon
SpringCloudRibbon是基于NetflixRibbon实现的一套客户端 负载均衡的工具。
简单的说,Ribbon是Netflix发布的开源项目,主要功能是提供客户端的软件负载均衡街算法和服务调用。
Ribbon客户端组件提供一系列完善的配置项如连接超时,重试等。
简单的说,就是在配置文件中列出Load Balance er(简称LB)后面所有的机器,Ribbon会自动的帮助你基于某种规则(如简单轮询,随机连接等)去连接这些机器。
我们很容易使用Rib obon实现自定义的负载均衡算法。
什么叫Ribbon: 一句话就是 RestTemplate+负载均衡
官网: https://github.com/Netflix/ribbon/wiki/Getting-Started
未来趋势可能会被 LoadBalancer
2.1 负载均衡
Ribbon 主要负载均衡,它和nginx有什么区别,区别在于,打个比方,nginx是拦在最前面的,比如医院 nginx是医院大门,ribbon是里面某个小科室
LB负载均衡(Load Balance)是什么
简单的说就是将用户的请求平摊的分配到多个服务上,从而达到系统的HA(高可用)
常见的负载均衡有软件Nginx,LVS,硬件F5等。
Ribbon本地负载均衡客户端VS Nginx服务端负载均衡区别
-
Nginx是服务器负载均衡,客户端所有请求都会交给nginx,然后由nginx实现转发请青求。即负载均衡是由服务端实现的。
-
Ribbon本地负载均衡,在调用微服务接口时候,会在注册中心上获取注册信息服务5列表之后缓存到JVM本地,从而在本地实现RPC远程服务调用技术。
2.2 集中式负载均衡
Nginx和F5就属于集中式负载均衡
集中式LB
即在服务的消费方和提供方之间使用独立的LB设施(可以是硬件,如F5也可以是软件 如nginx),由该设施负责把访问请求通过某种策 略转发至服务的提供方;
2.3 进程内负载均衡
Ribbon就属于进程内负载均衡
进程内LB
将LB逻辑集成到消费方,消费方从服务注册中心获知有哪些地址可用,然后自己再从这些地址中选择出一个合适的服务器。
Ribbon就属于进程内LB
,它只是一个类库,集成于消费方进程,消费方通过它来获取到服务提供方的地址。
2.4 核心组件IRule
IRule根据特定算法从服务列表中选取一个要访问的服务
2.5 如何替换Ribbon默认轮询算法
-
负载均衡分类
- 硬件负载均衡:F5,价格昂贵不考略在内
- 服务端负载均衡:nginx、lvs
- 客户端负载均衡:ribbon
-
负载均衡策略
-
1、随机策略——RandomRule
-
2、轮询策略——RoundRobinRule
注:Ribbon默认策略 -
3、重试策略——RetryRule
-
4、最低并发策略——BestAvailableRule
-
5、可用过滤策略——AvailabilityFilteringRule
过滤掉那些因为一直连接失败的被标记为circuit tripped的后端server,并过滤掉那些高并发的的后端server(active connections 超过配置的阈值
性能仅次于最低并发策略。 -
6、响应时间加权策略——WeightedResponseTimeRule
每隔30秒计算一次服务器响应时间,以响应时间作为权重,响应时间越短的服务器被选中的概率越大。 -
7、区域权衡策略——ZoneAvoidanceRule
-
Ribbon的负载均衡策略使用建议
一般情况下,推荐使用最低并发策略,这个性能比默认的轮询策略高很多。
2.5.1 案例演示
- 第一步: 在项目目录下创建一个文件夹,此文件夹不可以被compscan扫描到
- 第二步,IRule接口下包含七种实现类,注入需要更改的负载均衡相关的是实现类即可
- 第三步,在启动类上加注解
注意配置细节
这个自定义配置类不能放在@ComponentScan所扫描的当前包下以及子包下,否则我们自定义的这个配置类就会被所有的Ribbon客户端所共享,达不到特殊化定制的目的了
2.5.2 轮询原理
2.5.3 手写轮询负载均衡
3.4.1自定义一个接口
public interface ILoadBalancer {
ServiceInstance getServiceInstance(List<ServiceInstance> serviceInstances);
}
3.4.2 自定义一个实现类组件
@Component
public class MyILoadBalancer implements ILoadBalancer {
private AtomicInteger atomicInteger = new AtomicInteger(0);
//通过CAS加自旋锁方式保证不被改动
private final int getAndIncrement(){
int current;
int next;
do {
current = this.atomicInteger.get();
next = current >= 2147483647 ? 0 : current + 1;
} while (!this.atomicInteger.compareAndSet(current, next));
System.out.println("***********第几次访问,次数next为:" + next);
return next;
}
@Override
public ServiceInstance getServiceInstance(List<ServiceInstance> serviceInstances) {
if (serviceInstances == null || serviceInstances.size() <= 0) {
return null;
}
int index = getAndIncrement() % serviceInstances.size();
return serviceInstances.get(index);
}
}
3.4.3 controller层
@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 = myILoadBalancer.getServiceInstance(instances);
URI uri = serviceInstance.getUri();
return restTemplate.getForObject(uri+"/pay/lb",String.class);
}
2.5.4 远程调用dubbo和feign区别
远程调用 Dubbo 与 Feign 的区别
- 相同点
- Dubbo 与 Feign 都依赖注册中心、负载均衡。
- 区别
- 协议
- Dubbo:
支持多传输协议(Dubbo、Rmi、http、redis等等),可以根据业务场景选择最佳的方式。非常灵活。
默认的Dubbo协议:利用Netty,TCP传输,单一、异步、长连接,适合数据量小、高并发和服务提供者远远少于消费者的场景。 - Feign:
基于Http传输协议,短连接,不适合高并发的访问。
- Dubbo:
- 2、负载均衡
- Dubbo:
支持4种算法(随机、轮询、活跃度、Hash一致性),而且算法里面引入权重的概念。
配置的形式不仅支持代码配置,还支持Dubbo控制台灵活动态配置。
负载均衡的算法可以精准到某个服务接口的某个方法。 - Feign:
只支持N种策略:轮询、随机、ResponseTime加权。
负载均衡算法是Client级别的。
- Dubbo:
- 容错策略
- Dubbo:
支持多种容错策略:failover、failfast、brodecast、forking等,也引入了retry次数、timeout等配置参数。 - Feign:
利用熔断机制来实现容错的,处理的方式不一样。
- Dubbo:
3. Rpc框架简述
3.1 什么是RPC框架?
RPC,全称为Remote Procedure Call,即远程过程调用,是一种计算机通信协议。
-
单体项目时:一次服务调用发生在同一台机器上的同一个进程内部,也就是说调用发生在本机内部,因此也被叫作本地方法调用。
-
微服务项目时:服务提供者和服务消费者运行在两台不同物理机上的不同进程内,它们之间的调用相比于本地方法调用,可称之为远程方法调用,简称 RPC;
远程调用的过程中会涉及到建立网络链接(http、socket)、进行网络通信(开放协议、私有协议)、进行数据传输(序列化和反序列化)。
- 通信框架解决客户端和服务端如何建立连接、管理连接以及服务端如何处理请求的问题。
- 通信协议解决客户端和服务端采用哪种数据传输协议的问题。
- 序列化和反序列化解决客户端和服务端采用哪种数据编解码的问题。
3.2 Rpc实现原理
3.2.1 建立通信
- 首先要解决通讯的问题
即A机器想要调用B机器,首先得建立起通信连接。
主要是通过在客户端和服务器之间建立TCP连接,远程过程调用的所有相关的数据都在这个连接里面进行传输交换。
通常这个连接可以是按需连接(需要调用的时候就先建立连接,调用结束后就立马断掉),也可以是长连接(客户端和服务器建立起连接之后保持长期持有,不管此时有无数据包的发送,可以配合心跳检测机制定期检测建立的连接是否存活有效),多个远程过程调用共享同一个连接。
需要有非常高效的网络通信,比如一般选择Netty作为网络通信框架
3.2.2 服务寻址
- 解决寻址的问题
即A机器上的应用A要调用B机器上的应用B,那么此时对于A来说如何告知底层的RPC框架所要调用的服务具体在哪里呢?
通常情况下我们需要提供B机器(主机名或IP地址)以及特定的端口,然后指定调用的方法或者函数的名称以及入参出参等信息,这样才能完成服务的一个调用。比如基于Web服务协议栈的RPC,就需要提供一个endpoint URI,或者是从UDDI服务上进行查找。如果是RMI调用的话,还需要一个RMI Registry来注册服务的地址。
可靠的寻址方式(主要是提供服务的发现),比如可以使用Zookeeper来注册服务等等
3.2.3 网络传输
-
序列化
当A机器上的应用发起一个RPC调用时,调用方法和其入参等信息需要通过底层的网络协议如TCP传输到B机器,由于网络协议是基于二进制的,所有我们传输的参数数据都需要先进行序列化(Serialize)或者编组(marshal)成二进制的形式才能在网络中进行传输。然后通过寻址操作和网络传输将序列化或者编组之后的二进制数据发送给B机器。 -
反序列化
当B机器接收到A机器的应用发来的请求之后,又需要对接收到的参数等信息进行反序列化操作(序列化的逆操作),即将二进制信息恢复为内存中的表达方式,然后再找到对应的方法(寻址的一部分)进行本地调用(一般是通过生成代理Proxy去调用, 通常会有JDK动态代理、CGLIB动态代理、Javassist生成字节码技术等),之后得到调用的返回值。
需要有比较高效的序列化框架,比如谷歌的ProtoStuff 序列化框架
3.2.4 服务调用
B机器进行本地调用(通过代理Proxy)之后得到了返回值,此时还需要再把返回值发送回A机器,同样也需要经过序列化操作,然后再经过网络传输将二进制数据发送回A机器,而当A机器接收到这些返回值之后,则再次进行反序列化操作,恢复为内存中的表达方式,最后再交给A机器上的应用进行相关处理(一般是业务逻辑处理操作)。
3.3 Rpc执行流程
Rpc调用包含以下几个部分
- 客户端(Client)
服务调用方(服务消费者) - 客户端存根(Client Stub)
存放服务端地址信息,将客户端的请求参数数据信息打包成网络消息,再通过网络传输发送给服务端; - 服务端存根(Server Stub)
接收客户端发送过来的请求消息并进行解包,然后再调用本地服务进行处理 - 服务端(Server)
服务的真正提供者
- 客户端(Client) 通过本地调用的方式调用服务
- 客户端存根(client stub)接收到调用请求后负责将方法、入参等信息序列化(组装)成能够进行网络传输的消息体
- 客户端存根(client stub)找到远程的服务地址,并且将消息通过网络发送给服务端
- 服务端存根(server stub)收到消息后进行解码(反序列化操作)
- 服务端存根(server stub)根据解码结果调用本地的服务进行相关处理
- 本地服务执行具体业务逻辑并将处理结果返回给服务端存根( server stub)
- 服务端存根( server stub)将返回结果重新打包成消息(序列化)并通过网络发送至消费方
- 客户端存根(client stub)接收到消息,并进行解码(反序列化)
- 客户端(Client) 得到最终结果
而RPC框架是把调用、编码/解码的过程给封装起来,让用户感觉上像调用本地服务一样的调用远程服务。
3.4 问题补充
3.4.1 为什么需要序列化和反序列化
在网络中,所有的数据都将会被转化为字节进行传送,所以为了能够使参数对象在网络中进行传输,需要对这些参数进行序列化和反序列化操作。
序列化:把对象转换为字节序列的过程称为对象的序列化,也就是编码的过程。
反序列化:把字节序列恢复为对象的过程称为对象的反序列化,也就是解码的过程。
- 解决内存中数据结构到字节序列的映射过程
为如何保留各个结构和字段间的关系而生的技术 。 - 解决异构系统的数据传输
比如大小端、远端的持久存储; - 压缩数据,加快网络传输。
网络传输耗时一方面取决于网络带宽大小,另一方面取决于数据传输量。想加快网络传输,要么提高带宽,要么减小数据传输量,而对数据进行编码的主要目的就是减小数据传输量。比如一部高清电影原始大小为 30GB,如果经过特殊编码格式处理,可以减小到 3GB,同样是 100MB/s 的网速,下载时间可以从 300s 减小到 30s
3.4.2 有哪些序列化协议
序列化和反序列化是用于网络传输过程中的操作,那么规定序列化操作的我们可以称之为序列化协议
序列化方式最简单的一种就是直接实现JDK自带的序列化接口Serializable就可以了,但是这种方式不支持跨语言调用,而且性能比较低。现在常用的序列化协议有 Hessian,Kyro,Protostuff。
另外还有JSON和XML这种文本类序列化方式,可读性比较好,但是性能也比较差。
- Kryo
速度快,序列化后体积小,在Twitter,Groupon,Yahoo等多个著名开源项目中都有广泛使用。
GitHub地址 - Hession
是一个轻量级的自定义二进制RPC协议。也支持跨语言操作,是Dubbo默认的序列化方式
地址 - ProtoStuff
基于protobuf, 速度快,需要静态编译
GitHub地址
3.4.3 服务端处理客户端请求常见的处理方式
- 同步阻塞方式(
BIO
):客户端发一次请求,服务端生成一个对应线程去处理。当客户端同时发起的请求很多时,服务端需要创建 多个线程去处理每一个请求,当达到了系统最大的线程数时,新来的请求就无法处理了;
BIO 适用于连接数比较小的业务场景,这样的话不至于系统中没有可用线程去处理请求。这种方式写的程序也比较简单直观,易于理解。 - 同步非阻塞方式 (
NIO
):客户端发一次请求,服务端并不是每次都创建一个新线程来处理,而是通过 I/O 多路复用技术进行处理。就是把多个 I/O 的阻塞复用到同一个 select 的阻塞上,从而使系统在单线程的情况下可以同时处理多个客户端请求。这种方式的优势是开销小,不用为每个请求创建一个线程,可以节省系统开销;
NIO 适用于连接数比较多并且请求消耗比较轻的业务场景,比如聊天服务器。这种方式相比 BIO,相对来说编程比较复杂。
RPC框架选择NIO进行传输
- 异步非阻塞方式(
AIO
):客户端发起一个 I/O 操作然后立即返回,等 I/O 操作真正完成以后,客户端会得到 I/O 操作完成的通知,此时客户端只需要对数据进行处理就好了,不需要进行实际的 I/O 读写操作,因为真正的 I/O 读取或者写入操作已经由内核完成了。这种方式的优势是客户端无需等待,不存在阻塞等待问题;
AIO 适用于连接数比较多而且请求消耗比较重的业务场景,比如涉及 I/O 操作的相册服务器。这种方式相比另外两种,编程难度最大,程序也不易于理解。