# geerpc **Repository Path**: lople/geerpc ## Basic Information - **Project Name**: geerpc - **Description**: GeeRPC 选择从零实现 Go 语言官方的标准库 net/rpc,并在此基础上,新增了协议交换(protocol exchange)、 注册中心(registry)、服务发现(service discovery)、负载均衡(load balance)、超时处理(timeout processing)等特性。 - **Primary Language**: Go - **License**: AGPL-3.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 0 - **Created**: 2021-04-30 - **Last Updated**: 2022-06-14 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README 1.RPC 框架 RPC(Remote Procedure Call,远程过程调用)是一种计算机通信协议,允许调用不同进程空间的程序。 RPC 的客户端和服务器可以在一台机器上,也可以在不同的机器上。 程序员使用时,就像调用本地程序一样,无需关注内部的实现细节。 不同的应用程序之间的通信方式有很多,比如浏览器和服务器之间广泛使用的基于 HTTP 协议的 Restful API。 与 RPC 相比,Restful API 有相对统一的标准,因而更通用,兼容性更好,支持不同的语言。 HTTP 协议是基于文本的,一般具备更好的可读性。但是缺点也很明显: Restful 接口需要额外的定义,无论是客户端还是服务端,都需要额外的代码来处理,而 RPC 调用则更接近于直接调用。 基于 HTTP 协议的 Restful 报文冗余,承载了过多的无效信息,而 RPC 通常使用自定义的协议格式,减少冗余报文。 RPC 可以采用更高效的序列化协议,将文本转为二进制传输,获得更高的性能。 因为 RPC 的灵活性,所以更容易扩展和集成诸如注册中心、负载均衡等功能。 2 RPC框架需要解决什么问题 我们可以想象下两台机器上,两个应用程序之间需要通信,那么首先,需要确定采用的传输协议是什么? 如果这个两个应用程序位于不同的机器,那么一般会选择 TCP 协议或者 HTTP 协议;那如果两个应用程序位于相同的机器, 也可以选择 Unix Socket 协议。传输协议确定之后,还需要确定报文的编码格式,比如采用最常用的 JSON 或者 XML, 那如果报文比较大,还可能会选择 protobuf 等其他的编码方式,甚至编码之后,再进行压缩。 接收端获取报文则需要相反的过程,先解压再解码。 解决了传输协议和报文编码的问题,接下来还需要解决一系列的可用性问题,例如,连接超时了怎么办? 是否支持异步请求和并发? 如果服务端的实例很多,客户端并不关心这些实例的地址和部署位置,只关心自己能否获取到期待的结果, 那就引出了注册中心(registry)和负载均衡(load balance)的问题。简单地说,即客户端和服务端互相不感知对方的存在, 服务端启动时将自己注册到注册中心,暴露服务,客户端调用时,从注册中心获取到所有可用的实例,选择一个来调用。 这样服务端和客户端只需要感知注册中心的存在就够了。 注册中心通常还需要实现服务动态添加、删除,使用心跳确保服务处于可用状态等功能。 再进一步,假设服务端是不同的团队提供的,如果没有统一的 RPC 框架,各个团队的服务提供方就需要各自 实现一套消息编解码、连接池、收发线程、超时处理等“业务之外”的重复技术劳动,造成整体的低效。 因此,“业务之外”的这部分公共的能力,即是 RPC 框架所需要具备的能力。 超时处理特性: 超时处理是 RPC 框架一个比较基本的能力,如果缺少超时处理机制,无论是服务端还是客户端都容易因为网络或其他错误导致挂死, 资源耗尽,这些问题的出现大大地降低了服务的可用性。因此,我们需要在 RPC 框架中加入超时处理的能力。 纵观整个远程调用的过程,需要客户端处理超时的地方有: 与服务端建立连接,导致的超时 发送请求到服务端,写报文导致的超时 等待服务端处理时,等待处理导致的超时(比如服务端已挂死,迟迟不响应) 从服务端接收响应时,读报文导致的超时 需要服务端处理超时的地方有: 读取客户端请求报文时,读报文导致的超时 发送响应报文时,写报文导致的超时 调用映射服务的方法时,处理报文导致的超时 GeeRPC 在 3 个地方添加了超时处理机制。分别是: 1)客户端创建连接时 2)客户端 Client.Call() 整个过程导致的超时(包含发送报文,等待处理,接收报文所有阶段) 3)服务端处理报文,即 Server.handleRequest 超时。 支持 HTTP 协议需要做什么?[非规则的流量控制] 使用HTTP协议的CONNECT方法代理客户端和服务端的连接服务。以明文的方式向代理服务器发送连接请求,然后代理服务器返回连接成功。 之后客户端和服务端建立TCP握手并交换加密数据。代理服务器只负责传输彼此的数据包,并不能读取具体数据内容。 事实上,这个过程其实是通过代理服务器将 HTTP 协议转换为 HTTPS 协议的过程。 负载均衡策略 grpc不同于Nginx这类服务端负载均衡,他采用客户端负载均衡策略。对于使用服务端负载均衡的系统,客户端会首先访问负载均衡的域名/IP,再由负载均衡按照策略分发请求到后端具体某个服务节点上。 而对于客户端的负载均衡则是,客户端从可用的后端服务节点列表中根据自己的负载均衡策略选择一个节点直连后端服务器。 假设有多个服务实例,每个实例提供相同的功能,为了提高整个系统的吞吐量,每个实例部署在不同的机器上。 客户端可以选择任意一个实例进行调用,获取想要的结果。那如何选择呢?取决了负载均衡的策略。 对于 RPC 框架来说,我们可以很容易地想到这么几种策略: 随机选择策略 - 从服务列表中随机选择一个。 轮询算法(Round Robin) - 依次调度不同的服务器,每次调度执行 i = (i + 1) mode n。 加权轮询(Weight Round Robin) - 在轮询算法的基础上,为每个服务实例设置一个权重,高性能的机器赋予更高的权重,也可以根据服务实例的当前的负载情况做动态的调整,例如考虑最近5分钟部署服务器的 CPU、内存消耗情况。 哈希/一致性哈希策略 - 依据请求的某些特征,计算一个 hash 值,根据 hash 值将请求发送到对应的机器。一致性 hash 还可以解决服务实例动态添加情况下,调度抖动的问题。一致性哈希的一个典型应用场景是分布式缓存服务。 注册中心:客户端无需知道服务端的存在 注册中心的好处在于,客户端和服务端都只需要感知注册中心的存在,而无需感知对方的存在。更具体一些: 服务端启动后,向注册中心发送注册消息暴露服务,注册中心得知该服务已经启动,处于可用状态。一般来说,服务端还需要定期向注册中心发送心跳,证明自己还活着。 客户端向注册中心询问,当前哪天服务是可用的,注册中心将可用的服务列表返回客户端。 客户端根据注册中心得到的服务列表,选择其中一个发起调用。 如果没有注册中心,客户端需要硬编码服务端的地址,而且没有机制保证服务端是否处于可用状态。当然注册中心的功能还有很多,比如配置的动态同步、通知机制等。比较常用的注册中心有 etcd、zookeeper、consul,一般比较出名的微服务或者 RPC 框架,这些主流的注册中心都是支持的。 注册中心实现方式: 1.常规启动httpserver 2.使用gin框架启动httpserver 3.dockers部署zookeeper集群(3台zookeeper服务器),server在"/rpcRegistry"节点下建立临时节点 client从/rpcRegistry节点获取node信息并进行动态监听watch 服务发现模式:代理Proxy以及代理在架构中所处的位置 模式1.传统集中式代理 consumer ----> Nginx(7层负载均衡器)[服务端负载均衡] -------> servers 缺点:服务消费方、提供方之间增加了一级,有一定性能开销,请求量大时,效率较低。 模式2.客户端嵌入式代理(本grpc采用方式) ,代理(包括服务发现和负载均衡逻辑)以客户库的形式嵌入在应用程序中。 这种模式一般需要独立的服务注册中心组件配合,服务启动时自动注册到注册中心并定期报心跳,客户端代理则发现服务并做负载均衡。 好处:LB和服务发现能力被分散到每一个服务消费者的进程内部,同时服务消费方和服务提供方之间是直接调用,没有额外开销,性能比较好。 模式3.主机独立进程代理 模式1和模式2的折中。代理既不是独立集中部署,也不嵌入在客户应用程序中,而是作为独立进程部署在每一个主机上, 一个主机上的多个消费者应用可以共用这个代理,实现服务发现和负载均衡。 将LB和服务发现功能从进程内移出来,变成主机上的一个独立进程。主机上的一个或者多个服务要访问目标服务时,他们都通过同一主机上的独立LB进程做服务发现和负载均衡。 这个模式一般也需要独立的服务注册中心组件配合,作用同模式二。 protobuf:protobuf(Google Protocol Buffers)是Google提供一个具有高效的协议数据交换格式工具库(类似Json),但相比于Json,Protobuf有更高的转化效率, 时间效率和空间效率都是JSON的3-5倍。后面将会有简单的demo对于这两种格式的数据转化效率的对比. Protobuf和Xml、Json序列化的方式不同,采用了二进制字节的序列化方式,用字段索引和字段类型通过算法计算得到字段之前的关系映射,从而达到更高的时间效率和空间效率,特别适合对数据大小和传输速率比较敏感的场合使用。 Protobuf 提供了C++、java、python语言的支持,提供了windows(proto.exe)和linux平台动态编译生成proto文件对应的源文件。proto文件定义了协议数据中的实体结构(message ,field) 关键字message: 代表了实体结构,由多个消息字段(field)组成。 消息字段(field): 包括数据类型、字段名、字段规则、字段唯一标识、默认值 数据类型:常见的原子类型都支持(在FieldDescriptor::kTypeToName中有定义) 字段规则:(在FieldDescriptor::kLabelToName中定义) required:必须初始化字段,如果没有赋值,在数据序列化时会抛出异常 optional:可选字段,可以不必初始化。 repeated:数据可以重复(相当于java 中的Array或List) 字段唯一标识:序列化和反序列化将会使用到。 默认值:在定义消息字段时可以给出默认值。 Monitor:监控 在geerpc中server向注册中心暴露服务的同时,自身还会开启debughttpServer服务,对于server收到的每一次rpc服务调用都会在调用成功后记录调用次数。 后续如何将Monitor和server分开,单独实现一个Monitor去可观测server的状态。 Docker部署server和client构造伪分布式 docker server目录下docker build 构建server镜像,运行server.go服务端向zk暴露服务 docker run --name s1 --net=container:zk1 -d imageName docker client目录下docker build 构建client镜像,运行client.go作为一个服务请求者 docker run --name c1 --net=container:zk1 imageName 为了server容器、client容器、zk集群容器间的通信使用docker -net=other_container模式,所有容器共用zk1网络空间(只是共享网络空间)