本项目是基于muduo库+Protobuf开发的一个RPC框架,主要是为RPC方法的调用提供数据处理、网络发送以及服务注册及发现功能,以简化分布式系统中的远程过程调用。
主要工作如下:
- 1、使用protobuf实现数据的序列化与反序列化,以及RPC方法的定义
- 2、使用zookeeper作为RPC方法服务注册与发现中心
- 3、使用Muduo库开发了RPC框架的网络,提供网络通信功能
- 4、封装了lockQueue作为日志缓冲队列,实现异步日志
- 5、至此框架的开发基本结束,由于框架没有业务,所以我自己增加了对MySQL的访问业务,并使用连接池提高对数据库的访问性能
- 6、使用Nginx对RPC节点做集群部署,提高并发能力
# 启动三台zk
sudo ./zkServer.sh start ../conf/zoo1.cfg
sudo ./zkServer.sh start ../conf/zoo2.cfg
sudo ./zkServer.sh start ../conf/zoo3.cfg
启动zookeeper和nginx,已下载到vendor,参考以下链接开启即可: nginx zookeeper
./autobuild.sh
我们在nginx里注册了三个节点,这里就开那三个
cd output/
./server -i server.conf -r rpc1
./server -i server.conf -r rpc2
./server -i server.conf -r rpc3
./client -i client.conf
客户端可发起sql查询,如:
select * from person;
update person set age=24 where name='gyl';
insert into person values('gyl', 18, 'M');
delete from person where name='gyl';
https://mp.weixin.qq.com/s/q4USY8xrj04RpowyaZwtfg
什么是分布式? 分布式是一种系统架构,多个节点协作完成任务 单机服务器的缺点:1.由于硬件,并发有限,无法保证保证高可用;2. 修改任何一个模块,整个工程要重新编译部署;3. 资源分配不合理 (不同的模块对资源的需求是不一样的,如业务模块肯定需要更多的硬件资源,而像一些后台管理模块根据不需要高并发) 集群呢:集群能提高并发,提高可用性,它是水平扩展,各个模块还是一台服务器上的,只是进行了复制,后面2点也解决不了 故引入分布式:把一个工程分成多个模块,根据每个模块对资源的需求部署在合适的机器上,所有服务器共同提供服务。每个服务器就是一个节点 好处:修改任何一个模块不影响其他模块;资源合理分配:可根据节点的并发需求再做集群部署
什么是RPC? RPC: 远程方法调用,它可以让一台服务器通过网络调用另一台服务器上的方法。 它屏蔽了网络调用的细节,让调用远程方法像调用本地一样简单。让我们只需要关注业务逻辑。类似socket,屏蔽了底层的各层协议栈细节。 而且RPC是跨语言的,只要双方协议一致即可。本项目就用的是protobuf 比较火的有grpc,brpc,而我这个项目的就是也实现一个rpc框架,叫mprpc 与HTTP区别: 使用场景不同:http主要用于客户端与服务器之间的通信,包含了很多的状态信息,比较冗余,对内部服务器联调没有意义, RPC的消息格式一般用protobuf,消息格式是固定的,比较精炼,一般用于内部联调、微服务之间的通信等
一个RPC框架的组成部分: 对于单机系统上: 1、确定一个已有的传输协议(TCP\UDP\HTTP\Websocket等) 2、一个客户端通信实现模块(即客户端stub) 3、一个服务端通信实现模块(即服务端stub) 4、选择一个RPC内容协议(如:json、xml、protobuf等) 若要变成分布式,也可加上: 服务发现 负载均衡
RPC的工作原理? 1.客户端调用本地代理(其实就是一个框架)去访问一个RPC方法 2. 代理把请求封装好(数据的序列化),发送给服务端 3. 服务端框架解析请求(解析请求),调用本地方法,把响应封装好,发回给客户端代理 4. 客户端代理解析响应,返回给调用者
为什么选择用protobuf? 常用的通信协议有json、xml、protobuf,各有优点,json简单易用,而protobuf的优点在于它把数据序列成2进制传输,数据量更小,传输也就更快。 而且本项目的RPC方法也是通过protobuf来定义的。
protobuf是如何定义RPC方法? 通过它的service模块 假设现在服务端有一个login方法, 客户端想调用它,前提是login方法它得先变成rpc方法,怎么发布成rpc方法?靠rpotobuf 在proto里的service模块里定义好rpc方法的请求和响应(具体就是把形参给定义好),生成代码,就会生成2个类,假设我定义了一个服务类RpcService,里面有一个login()方法,请求和响应的形参都定义好了,则它生成代码后的2个类:RpcService和RpcService_Stub。 客户端使用pcService_Stub类对象填充request的proto, 调RPC方法,等待响应即可。 服务端重写RpcService类的query方法:1.调本地的query方法,传入客户端request,把返回值写到响应里发回去
protobuf的反射机制? 见https://blog.csdn.net/qq_72642900/article/details/136565050?spm=1001.2014.3001.5501
你的proto设置了几个字段:
三个字段:service_name+method_name+args_size,
1、序列化request, 得到形参的arg_str
2、填充这三个字段,序列化,得到rpc_str,合体:rpc_str+ arg_str
3、头部4个字节存一个int,记录rpc_str的长度,至此就完成了请求字符串的组装
4、客户端先读4个字节导一个int,得到rpc字符串的长度,反序列化得到service_name+method_name+args_size, 再通过arg_size向后读取这么长,反序列化得到形参request。
为什么用Zookeeper?
1. Zookeeper是一个分布式协调组件,常用于分布式系统中配置管理、服务注册、领导选举,因为他有服务注册与发现的功能,我这里就让他充当一个服务注册中心。
2. 他内部的数据组织结构就与linux的目录一样都是树状,像我使用的时候只需要把RPC方法的服务名、方法名、ip+port以树状目录的形式注册到zk即可,形成一个临时节点/server_name/method_name/ip+port。
3. 设置临时节点的原因就是防止RPC节点挂了,
4. zk的客户端会向服务器定时发送心跳,若超时,服务器就认为他挂了,如果是临时节点就删掉。我们服务端节点注册到zk都设置为临时节点。
zk有没有做集群部署? zk集群架构:三种角色:leader领导者、follower追随者、observer观察者。 leader可以处理读写请求,同时领导整个集群;follwer只能处理读请求,但是可以参加选举;observer观察者:处理读请求,但不参加选举。 本项目没有开启observer,即只有2种角色。 zk集群也是CP强一致性模型,采用的是ZAB协议,大致原理:一个写请求来了,leader把他当成一个事务提案,原子广播给所有follower,若半数以上同意,就执行这条请求。 zk部署了几台? 三台,若是2台,其中一台挂了,另一台成为不了leader。集群部署提高zk的可用性和并发能力 zk也能负载均衡,为什么还要nginx呢? nginx的负载均衡算法更多一些。
分布式锁的实现? zk:临时节点,客户端注册一个临时节点到zk的指定锁路径,zk分配一个序号给他,若当前它的序号最小,就获得锁,若不是最小,就拿不到锁,当小序号节点被删除后,zk会通知客户端,它再次检查序号大小 redis:SET lock_key value NX PX 3000, 设置一个键的值若键不存在就成功设置,获得锁。
对muduo库的了解? muduo库是一个基于非阻塞io+io多路复用+线程池高性能的网络库,典型的多Reactor多线程模型 什么是reactor和proactor:其实就是对I/O多路复用做了封装,让他面向对象。proactor是异步的,但linux的异步io并不完善,所以用的很少。 Ractor模型的流程:reactor对象通过内部的epoll监听事件,收到事件后通过分发器分发给accetor对象或者handler对象分别对应建立连接事件和处理已连接用户的具体事件。这是单reacor单线程。常用的是多reactor多线程:主线程中主reactor负责用户连接,然后把连接发给子线程,子线程中的子reactor负责具体事件的处理,而且正好映射了那句话:one loop pthread,一个事件循环对应一个线程(一个reactor即一个事件循环对应一个线程)。 muduo的源码我也看过一点,大致有4大组件:event事件、Reactor反应堆、多路录用事件分发器,事件处理器 当一个连接/读写事件来了,先到event事件暂存,再把它注册到Reactor反应堆,反应堆就是一个事件集合,然后反应堆会把这些事件epoll_ctl到多路时间分发器,事件分发器开启epoll_wait,检测完返回发生变化的事件给reactor反应堆,rector反应堆再把这些事件交给事件处理器去处理。
但我们使用它很简单,写好2个回调的逻辑即可,连接回调(但连接断开关闭文件描述符)、读写回调(对于我们这里:先反序列数据,然后调RPC方法,序列化数据,发回响应)
lockQueue,无锁队列有了解吗? 我自己开发了一个日志模块,但日志落盘是需要磁盘io耗时的,最好是异步,不要让他占用rpc调用的事件。 消费者循环查看队列是否为空,若为空,则cond_wait()释放锁,睡眠。生产者push一条日志后,notify_one()唤醒消费者线程,去消费
lockqueue就是把std::queue加了一把锁,使其充当一个线程安全的日志缓冲队列,本项目是一写多读,开一个写线程在后台pop,
这样就磁盘IO时间就不再RPC调用里面。
pop细节:进来加锁,判断队列是否为空则wait(lock)释放锁并等待。
无锁队列有一点了解,是要用原子操作去保证线程安全,但我没有仔细研究过,比较复杂。
连接池设计? 先说一下连接池的原理:大部分池化技术远离都是预分配资源+重复利用,避免资源的反复申请释放,浪费时间 而数据库连接池也是基于此。我们知道一条sql从客户端到服务器的执行流程:tcp三次握手->mysql连接认证->执行sql->关闭mysql连接->tcp4次挥手 而连接池直接把这条连接给缓存下来,后续直接用,5步变1步。 连接池的核心参数:初始连接数、最大连接数、最大空闲时间(一般超过此时间就把空闲连接数恢复到初始连接数,避免浪费)、连接超时时间 运行流程:创建单例、创建初始连接数的连接放入一个队列,并且开一个线程定时检查队列里的连接有无超过最大空闲时间(看队头即可),超过就开始清理, 直至只剩下初始数的连接。且对他们做保活:取出来发起查询,放回队列。为啥要做保活:因为在池子里的连接也可能因为各种原因关闭。
对Ningx的了解? Ningx是一个高性能的HTTP服务器和反向代理服务器(所谓反向代理就是客户端向代理服务器发请求,由他转发给服务器,正向代理就是客户端通过 代理服务器去访问目标服务器,如翻墙梯子)。本项目就让他作为反向代理服务器,客户端的RPC请求本来是由框架查zk,直接发给对应的节点,现在zk里注册的是nginx的ip+port,即发给nginx,nginx再根据它的负载均衡策略分发给各个服务器节点。 负载均衡:把请求根据一定的策略发到各个服务器节点上,避免单个服务器过载 策略:轮询(等权值)、按权值分配(1,2)、随机、一致性哈希 一致性哈希:我们把0-2^32-1这些数在逻辑上形成一个环。把服务器映射到哈希环上,可以用ip地址对2^32取模。然后把请求也映射上去,这个请求顺时针走,发给第一个服务器。好处:增删节点方便,如:abc三个服务器,我要加一个d在ab之间,则只需要把a-d的数据从b转移到d上,其他的数据不动,开销比一般的哈希函数小很多。但是若节点分布不均,会导致负载不均。所以引入虚拟节点,如现在3个节点,一个节点对应5个虚拟节点,就有15个虚拟节点,然后把15个虚拟节点平均分布在环上,当请求到达虚拟节点,就放到实际节点这样,节点多了就相对均匀。也就相对负载均衡了。
若节点都挂了,客户端还是访问的nginx,咋办? 注册的是临时节点,根本没有rpc方法了
rpc节点间有相互调用吗? 这个要看具体业务了,我这里是没有相应的业务的,如果业务需要的的话,就是一个节点调另一个的方法了,具体实现与客户端的调用应该一样。
RPC方法调用过程中若出现问题怎么办? 有一个参数叫controller,从客户端发起的请求可以带上这个参数,过程中任何一步出问题,都可以用这个参数记录下来
运行流程(架构):简历上有,但我们可以说的更细到具体函数
客户端调rpc方法是阻塞的吗? 在socket连接后,send发送请求字符串,使用setsocktopt设置了超时时间。
压测: 客户端开10个线程,都发起100次rpc请求 没开三个连接池:qps/rps: 30几 开了三个连接池(socket连接池、zk连接池、数据库连接池):900qps 也做了连接保活:定期把连接取出来访问一下服务器。
与grpc对比: 基于http/2, 使用protobuf, 且本身就支持负载均衡和服务发现。