一次完整的RPC调用流程(同步调用,异步另说)如下:

1)服务消费方(client)调用以本地调用方式调用服务;
2)client stub接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体;
3)client stub找到服务地址,并将消息发送到服务端;
4)server stub收到消息后进行解码;
5)server stub根据解码结果调用本地的服务;
6)本地服务执行并将结果返回给server stub;
7)server stub将返回结果打包成消息并发送至消费方;
8)client stub接收到消息,并进行解码;
9)服务消费方得到最终结果。

手写PPC与dubbo区别

Dubbo admin 服务治理平台
1)多种容错策略
Failover Cluster (默认)
失败自动切换,当出现失败,重试其它服务器。通常用于读操作,但重试会带来更长延迟。可通过 retries=”2” 来设置重试次数(不含第一次)。
Failfast Cluster(防止幂等性)
快速失败,只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录。
Failsafe Cluster
失败安全,出现异常时,直接忽略。通常用于写入审计日志等操作。
Failback Cluster
失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知操作。
Forking Cluster
并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。可通过 forks=”2” 来设置最大并行数。
Broadcast Cluster
广播调用所有提供者,逐个调用,任意一台报错则报错 [2]。通常用于通知所有提供者更新缓存或日志等本地资源信息

5、服务降级

通过服务降级功能临时屏蔽某个出错的非关键服务,并定义降级后的返回策略

1)屏蔽 (直接返回为空)

4、修改权重

Rpc框架手写 - 图1

灰度发布

Rpc框架手写 - 图2
实现
Rpc框架手写 - 图3

一个完整的注册中心需要实现以下功能:

  1. 接收服务端的注册与客户端的引用,即将引用与消费建立关联,并支持多对多。
  2. 当服务正常关闭时能即时清除其状态
  3. 当注册中心重启时,能自动恢复注册数据,以及订阅请求

    如果没有注册中心?会怎样

    在不用服务注册之前,怎么去维护这种复制的关系网络呢?答案就是:写死!。
  • 1
  1. 将其他模块的ip和端口写死在自己的配置文件里,甚至写死在代码里,每次要去新增或者移除1个服务的实例的时候,就得去通知其他所有相关联的服务去修改

服务注册中心的作用就是【服务的注册】和【服务的发现】

服务注册,就是将提供某个服务的模块信息(通常是这个服务的ip和端口)注册到1个公共的组件上去(比如: zookeeper\consul)。
服务发现,就是新注册的这个服务模块能够及时的被其他调用者发现。不管是服务新增和服务删减都能实现自动发现。
Zookeeper记录的是正在工作的服务器节点,消息中间件也就是broker
Zookeeper选举机制
所以id值较大的服务器2胜出,服务器4启动,根据前面的分析,理论上服务器4应该是服务器1、2、3、4中最大的,但是由于前面已经有半数以上的服务器选举了服务器3,所以它只能接收当小弟的命了。
它们的id从1-5,同时它们都是最新启动的,也就是没有历史数据,在存放数据量这一点上,都是一样的,如果有历史数据,优先投历史数据最大的,也就是日志记录最多的。如果相同就投id序号最大的,保证选取的leader永远是最大的。

Zookeeper监控机制

很多名字服务软件都会提供健康检查功能。注册服务的这一组机器,当这个服务组的某台机器,如果出现宕机或者服务死掉的时候,就会标记这个实例的状态为故障,或者干脆剔除掉这台机器。这样一来,就实现了自动监控和管理。
服务通知:当服务提供者因为某种原因宕机或不提供服务之后,Zookeeper服务注册中心的对应服务节点会被删除,因为服务消费者在获取服务信息的时候在对应节点上设置了Watch,因此节点删除之后会触发对应的Watcher,Zookeeper注册中心会异步向服务所关联的所有服务消费者发出节点删除的通知,服务消费者根据收到的通知更新缓存的服务列表。
当被Watch的Znode被删除或者更新之后,Zookeeper服务器会查找Watch Table,找到在Znode上对应的所有Watcher,异步通知对应的客户端,并且删除Watch Table中对应的Key:Value;

Netty快的原因,

类似于epoll的多路复用模型,而tomcat用的是传统的bio模型,而且应用于零拷贝技术
ByteBufByteBuf是一个存储字节的容器,最大特点就是使用方便,它既有自己的读索引和写索引,方便你对整段字节缓存进行读写,也支持get/set,方便你对其中每一个字节进行读写,他的数据结构如下图所示:
【基于 Buffer】:
l 传统的 I/O 是面向字节流或字符流的,以流式的方式顺序地从一个 Stream 中读取一个或多个字节, 因此也就不能随意改变读取指针的位置。

l 在 NIO 中,抛弃了传统的 I/O 流,而是引入了 Channel 和 Buffer 的概念。在 NIO 中,只能从 Channel 中读取数据到 Buffer 中或将数据从 Buffer 中写入到 Channel。

l 基于 Buffer 操作不像传统 IO 的顺序操作,NIO 中可以随意地读取任意位置的数据。

Redis做注册中心

以hash类型存放所有提供者列表, key为服务粒度的前缀信息: /dubbo/xxx.xx.XxxService/providers, hash中每个field->value表示,服务全路径信息->过期时间。
通过redis的 pub/sub 机制,通知其他客户端变化。注册时发布一条消息到提供者路径, publish register 。
总结: 下线处理两步骤: 1. 删除对应的hash key-field; 2. publish 一个下线消息通知其他应用; 3. 针对redis的集群配置决定是删除1次或n次,且反复通知操作;
redis注册中心其实不会主动发现服务变更,只有应用自己发布regiter或unregister消息后,其他应用才能感知到变化。前面在 doRegister() 时,我看到,应用是通过hash添加字段注册自己,并同时发布 REGISTER 消息通知所有订阅者。在 doSubscribe() 时开启另一个服务线程处理subscribe();
但和zk比起来,redis功能实现会相对困难些,甚至看起来有些蹩脚(如其redis集群策略需要自行从外部保证同步,这恐怕不会是件容易的事,现有的主从,集群方案都完全无法cover其场景。既要保证任意写,又要保证全同步(数据一致性),呵呵)。它需要单独去维护一些心跳、过期类的事务。过多的服务会导致这类工作更加繁重。
我觉得最重要的一点是Redis专注于数据本身的存储,而Zookeeper更专注于分布式组件的协调,和配置。
Zookeeper即每次发生更新,强制同步刷盘,保证了分布式系统的可靠性;
Redisappendfsync everysec 为每秒一次**;这意味着极端情况下,Redis会丢失数据。

RPC笔记

RPC就是要像调用本地的函数一样去调远程函数

那为啥不能用HTTP请求,要用RPC调用呢?
首先,RPC是一个完整的远程调用方案,它通常包括通信协议和序列化协议
RPC整个过程是序列化,压缩,基于TCP协议或者HTTP协议的socket编程,收到后调用自己方法返回结果,进行反序列化传输给服务调用方。
大彬:其中,通信协议包含http协议(如gRPC使用http2)、自定义报文的tcp协议(如dubbo)
大彬:序列化协议包含基于文本编码的xml、json,基于二进制编码的(谷歌的)protobuf、hessian等,二进制编码的比Java自带的jdk序列化字节少,是压缩过的,省去对象头,锁信息,hashcode值等杂字节。
大彬:序列化协议包含基于文本编码的xml、json,基于二进制编码的protobuf、hessian等
Dubbo与Http区别
dubbo默认使用socket长连接,即首次访问建立连接以后,后续网络请求使用相同的网络通道,总是调用者(类似消费者)多,而服务提供少。因此最好单一长链接
http1.1协议默认使用短连接,每次请求均需要进行三次握手,而http2.0协议开始将默认socket连接改为了长连接
16字节头部,包括请求数据长度,状态等最需要的信息。
Rpc框架手写 - 图4

为什么有些后端子系统之间是使用自定义tcp协议的rpc来做进程通信?而不是用HTTP协议呢?

首先,http协议2.0是支持连接池复用的,也就是建立一定数量的连接不断开,并不会频繁的创建和销毁连接。http也可以使用protobuf这种二进制编码协议对内容进行编码。也就是说连接建立与断开的开销和序列化协议并不是主要影响因素
二者最大的区别还是在传输协议上。
http的传输协议中header部分有很多冗余的部分,像Content-Type、Last-Modified、cookie安全信息等。即使http body是使用二进制编码协议,header头的键值对却用了文本编码,非常占用字节数。
大彬:而自定义的tcp协议,可以精简传输内容,传输效率更高。比如下面的自定义tcp协议的报文:
大彬:报头占用的字节数也就只有16个byte,大大地减少了传输内容。高并发情况下,少几个字节,乘以巨大的请求数量,能带来庞大的收益
其实使用http协议比较多的还是前后端的通信,原因在于主流网页游览器都支持http协议,而且http在缓存、幂等重试乃至cookie这种浏览器安全相关的方面做了很多功夫

RPC笔记

简单版此RPC的最大痛点:

  1. 只能调用服务端Service唯一确定的方法,如果有两个方法需要调用呢?(Reuest需要抽象)
  2. 返回值只支持User对象,如果需要传一个字符串或者一个Dog,String对象呢(Response需要抽象)
  3. 客户端不够通用,host,port, 与调用的方法都特定(需要抽象)

上个例子中response传输的是User对象,显然在一个应用中我们不可能只传输一种类型的数据 由此我们将传输对象抽象成为Object Rpc需要经过网络传输,有可能失败,类似于http,引入状态码和状态信息表示服务调用成功还是失败

此版本最大痛点

服务端与客户端通信的host与port预先就必须知道的,每一个客户端都必须知道对应服务的ip与端口号, 并且如果服务挂了或者换地址了,就很麻烦。扩展性也不强
zookeeper安装, 基本概念
了解curator开源zookeeper客户端中的使用

本节问题

如何设计一个注册中心

注册中心(如zookeeper)的地址是固定的(为了高可用一般是集群,我们看做黑盒即可), 服务端上线时,在注册中心注册自己的服务与对应的地址,而客户端调用服务时,去注册中心根据服务名找到对应的服务端地址。
zookeeper我们可以近似看作一个树形目录文件系统,是一个分布式协调应用,其它注册中心有EureKa, Nacos等

注册中心(如zookeeper)的地址是固定的(为了高可用一般是集群,我们看做黑盒即可), 服务端上线时,在注册中心注册自己的服务与对应的地址,而客户端调用服务时,去注册中心根据服务名找到对应的服务端地址。
zookeeper我们可以近似看作一个树形目录文件系统,是一个分布式协调应用,其它注册中心有EureKa, Nacos等
此版本中我们加入了注册中心,终于一个完整的RPC框架三个角色都有了:服务提供者,服务消费者,注册中心
Zookeeper的监控机制,时刻监控服务提供方生成URL的ip地址,如果发生变化,回查

序列化与反序列化

所以我们需要提前把它转成可传输的二进制,并且要求转换算法是可逆的,这个过程我们一般叫做“序列化”。这时,服务提供方就可以正确地从二进制数据中分割出不同的请求,同时根据请求类型和序列化类型,把二进制的消息体逆向还原成请求对象,这个过程我们称之为“反序列化”。

此版本最大痛点

根据服务名查询地址时,我们返回的总是第一个IP,导致这个提供者压力巨大,而其它提供者调用不到

总结

这一版本中,我们实现负载均衡的两种策略:随机与轮询。
在这一版本下,一个完整功能的RPC已经出现,当然还有许多性能上与代码质量上的工作需要完成。

随机轮询和轮询负载

随机:调用LoadBalance接口,设置10台机器,取总机器Listsize大小,随机数1到10,
缺点:太随机,可能产生饥饿问题。有的服务器性能很高,但是却一直用不到,解决:加权随机。
轮询:设置choose为-1,每次取模总机器,每次都是从1到10,解决:加权轮询
经过加权后,每台服务器能够得到的请求数比例,接近或等于他们的权重比。比如服务器 A、B、C 权重比为 5:3:2。那么在10次请求中,服务器 A 将收到其中的5次请求,服务器 B 会收到其中的3次请求,服务器 C 则收到其中的2次请求。
Rpc框架手写 - 图5
Dubbo的负载轮询
Rpc框架手写 - 图6
4.1 加权随机算法原理(默认)
下面我们通过随机数生成器在 [0, 10) 这个范围内生成一个随机数,然后计算这个随机数会落到哪个区间中。例如,随机生成 4,就会落到 Provider A 对应的区间中,此时 RandomLoadBalance 就会返回 Provider A 这个节点。
实现简单,可以按自己要求设置权重,但是容易出现积累请求问题

  • 加权随机,按权重设置随机概率。
  • 在一个截面上碰撞的概率高,但调用量越大分布越均匀,而且按概率使用权重后也比较均匀,有利于动态调整提供者权重。
  • 缺点:存在慢的提供者累积请求的问题,比如:第二台机器很慢,但没挂,当请求调到第二台时就卡在那,久而久之,所有请求都卡在调到第二台上。

可以用最快响应解决,但是有设置不了权重

RPC框架各有用版本问题及解决办法

版本0:总结:

此RPC的最大痛点:

  1. 只能调用服务端Service唯一确定的方法,如果有两个方法需要调用呢?(Reuest需要抽象)
    2. 返回值只支持User对象,如果需要传一个字符串或者一个Dog,String对象呢(Response需要抽象)
    3. 客户端不够通用,host,port, 与调用的方法都特定(需要抽象)

    版本1:

    更新1:定义了一个通用的Request的对象
    /
    在上个例子中,我们的Request仅仅只发送了一个id参数过去,这显然是不合理的,
    因为服务端不会只有一个服务一个方法,因此只传递参数不会知道调用那个方法
    因此一个RPC请求中,client发送应该是需要调用的Service接口名,方法名,参数,参数类型
    这样服务端就能根据这些信息根据反射调用相应的方法
    还是使用java自带的序列化方式
    /
    更新2: 定义了一个通用的Response的对象(消息格式)
    /

    上个例子中response传输的是User对象,显然在一个应用中我们不可能只传输一种类型的数据
    由此我们将传输对象抽象成为Object
    Rpc需要经过网络传输,有可能失败,类似于http,引入状态码和状态信息表示服务调用成功还是失败
    /
    更新3: 服务端接受request请求,并调用request中的对应的方法
    更新4: 客户端根据不同的Service进行动态代理:
    代理对象增强的公共行为:把不同的Service方法封装成统一的Request对象格式,并且建立与Server的通信

    问题:

    如果一个服务端要提供多个服务的接口, 例如新增一个BlogService,怎么处理?

    版本2

    // 自然的想到用一个Map来保存,
    更新前的工作: 更新一个新的服务接口样例和pojo类
    更新1: HashMap 添加多个服务的实现类

    此RPC最大的痛点

    传统的BIO与线程池网络传输性能低
    netty高性能网络框架的使用

    1. BIO (Blocking I/O)

    同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。

    2.1 NIO 简介

    NIO是一种同步非阻塞的I/O模型,在Java 1.4 中引入了NIO框架,对应 java.nio 包,提供了 Channel , Selector,Buffer等抽象。

    3. AIO (Asynchronous I/O)

    AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的IO模型。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。
    AIO 是异步IO的缩写,虽然 NIO 在网络操作中,提供了非阻塞的方法,但是 NIO 的 IO 行为还是同步的。对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就由这个线程自行进行 IO 操作,IO操作本身是同步的。(除了 AIO 其他的 IO 类型都是同步的,这一点可以从底层IO线程模型解释
    2. 为什么Netty使用NIO而不是AIO?
    主要原因如下:
    1,Netty不看重Windows上的使用,在Linux系统上,AIO的底层实现仍使用EPOLL,没有很好实现AIO,因此在性能上没有明显的优势,而且被JDK封装了一层不容易深度优化。
    2,Netty整体架构是reactor模型, 而AIO是proactor模型, 混合在一起会非常混乱,把AIO也改造成reactor模型看起来是把epoll绕个弯又绕回来。
    3,AIO还有个缺点是接收数据需要预先分配缓存, 而不是NIO那种需要接收时才需要分配缓存, 所以对连接数量非常大但流量小的情况, 内存浪费很多。
    4,Linux上AIO不够成熟,处理回调结果速度跟不上处理需求,比如外卖员太少,顾客太多,供不应求,造成处理速度有瓶颈(待验证)。

    版本3:

    此RPC最大痛点

    java自带序列化方式**(Java序列化写入不仅是完整的类名,也包含整个类的定义,包含所有被引用的类)**,不够通用,不够高效

    版本4:

    序列化器的接口:0:java 1 fastJSon

    阿里巴巴的FastJson

    Fastjson是一个Java语言编写的高性能的JSON处理器,由阿里巴巴公司开发。无依赖,不需要例外额外的jar,能够直接跑在JDK上。FastJson在复杂类型的Bean转换Json上会出现一些问题,可能会出现引用的类型,导致Json转换出错,需要制定引用。FastJson采用独创的算法,将parse的速度提升到极致,超过所有json库。

    序列后的码流太大。java序列化的大小是二进制编码的5倍多!最好的是二进制序列化谷歌的p开头的,但是google的Protobuf

  2. 结构化数据存储格式(xml,json等)
    2.高性能编解码技术
    3.语言和平台无关,扩展性好
    4.支持java,C++,Python三种语言。

    问题:

    服务端与客户端通信的host与port预先就必须知道的,每一个客户端都必须知道对应服务的ip与端口号, 并且如果服务挂了或者换地址了,就很麻烦。扩展性也不强

    版本5:总结

    更新: 更新客户端得到服务器的方式, 服务端暴露服务时,注册到注册中心
    首先new client不需要传入host与name, 而在发送request时,从注册中心获得
    此版本中我们加入了注册中心,终于一个完整的RPC框架三个角色都有了:服务提供者,服务消费者,注册中心

    此版本最大痛点

    根据服务名查询地址时,我们返回的总是第一个IP,导致这个提供者压力巨大,而其它提供者调用不到

    版本6

    负载均衡 简单实现了
    随机负载均衡和轮询负载均衡
    从0开始的RPC的迭代过程:
    - [version0版本]以不到百行的代码完成一个RPC例子
    - [version1版本]**,客户端的动态代理完成对request消息格式的封装
    - [version2版本:支持服务端暴露多个服务接口, 服务端程序抽象化,规范化
    - [version3版本]:使用高性能网络框架netty的实现网络通信- [version4版本]:自定义消息格式,支持多种序列化方式(java原生, json…)
    - [version5版本] 服务器注册与发现的实现,zookeeper作为注册中心
    - [version6版本] 负载均衡的策略的实现 **

    手写PPC与dubbo区别

    Dubbo admin 服务治理平台

    1)多种容错策略

    Failover Cluster (默认)
    失败自动切换,当出现失败,重试其它服务器。通常用于读操作,但重试会带来更长延迟。可通过 retries=”2” 来设置重试次数(不含第一次)。
    Failfast Cluster(防止幂等性)
    快速失败,只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录。
    Failsafe Cluster
    失败安全,出现异常时,直接忽略。通常用于写入审计日志等操作。
    Failback Cluster
    失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知操作。
    Forking Cluster
    并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。可通过 forks=”2” 来设置最大并行数。
    Broadcast Cluster
    广播调用所有提供者,逐个调用,任意一台报错则报错 [2]。通常用于通知所有提供者更新缓存或日志等本地资源信息

2)服务降级

通过服务降级功能临时屏蔽某个出错的非关键服务,并定义降级后的返回策略
屏蔽:不调用直接返回空
容错:调用一次,失败返回空:

3)修改权重 服务治理中心 admin

Rpc框架手写 - 图7

灰度发布 多版本控制

Rpc框架手写 - 图8
实现
Rpc框架手写 - 图9