GRPC

220 阅读9分钟

前置知识

单体架构

单体系统,指的是只在一台服务器上面运行的系统。这种架构有很大的局限性,硬件性能上会存在瓶颈,这种硬件上的瓶颈是不可避免的,所以这种系统只适合对性能,并发等指标要求不高的系统。

说起分布式系统,我们就不得不说下分布式系统的祖先——集中式系统。集中式系统跟分布式系统是完全相反的两个概念。集中式系统就是把所有的程序、功能都集中到一台主机上,从而往外提供服务的方式。(前后端不分离)

img

集群

单机架构的硬件资源有限对于业务量比较大的情况是很难适用的,所以便可以在此基础之上实施集群架构方式。

集群的架构的优势在于我们无需改动任何的项目代码,只需要新增服务器部署相同的应用并配置好负载均衡,就可以很好的减轻随着业务增量带来的系统压力,并且可以直接在单机架构上直接进行调整。

img

分布式

将一个项目拆分成了多个模块,并将这些模块分开部署,那就算是分布式。

  • 按照不同的业务域进行拆分,通过对业务进行梳理,根据业务的特性把应用拆开,不同的业务模块独立部署。例如订单、营销、风控、积分资源等。形成独立的业务领域微服务集群。(主站,直播,会员购,漫画,赛事)
  • 按照一个业务功能里的不同模块或者组件进行拆分。例如把公共组件拆分成独立的原子服务,下沉到底层,形成相对独立的原子服务层。这样一纵一横,就可以实现业务的服务化拆分。

(比如登录里使用微信登录,需要调用微信的api,这个时候我们就可以把和微信相关的逻辑(扫码登录,获取openid,头像,账号)提取出来,组成“微信中心服务”,其他服务需要使用相关信息直接从微信中心服务里取,而不是每个项目都去写调用微信api的代码)

微服务

微服务架构是采用一组服务的方式来构建一个应用,服务独立部署在不同的服务器或者相同服务器的不同进程中。服务之间使用数据进行通信,比如RPC或者HTTP等。不同的服务之间相互不影响,甚至可以使用不同的编程语言进行开发。

官方给微服务的定义为:

  • 一些独立的服务共同组成系统
  • 每个服务单独部署,跑在自己的进程中
  • 各个服务为独立的业务开发
  • 分布式管理
  • 强隔离性。

微服务相比分布式服务来说,它的粒度更小,服务之间耦合度更低,由于每个微服务都由独立的小团队负责,因此它敏捷性更高,分布式服务最后都会向微服务架构演化,这是一种趋势, 不过服务微服务化后带来的挑战也是显而易见的,例如服务粒度小,数量大,后期运维将会很难。

一般而可以按两种方式拆分微服务:

  • 按照不同的业务域进行拆分: 通过对业务进行梳理,根据业务的特性把应用拆开,不同的业务模块独立部署。例如订单、营销、风控、积分资源等。形成独立的业务领域微服务集群
  • 按照一个业务功能里的不同模块或者组件进行拆分: 例如把公共组件拆分成独立的原子服务,下沉到底层,形成相对独立的原子服务层

RPC

远程过程调用(Remote Procedure Call,RPC)是一个计算机通信协议。该协议允许运行于一台计算机的程序调用另一个地址空间(通常为一个开放网络的一台计算机)的子程序,而程序就像调用本地函数一样,无需额外地为这个交互作用编程(无需关注细节)。

简单来讲,若想在机器A上调用机器B上的函数,那么就得了解RPC

和本地调用面临的问题一样,这个操作需要解决两个问题:调用什么函数?函数参数是什么?

  • 调用什么函数?

本地调用只需要函数名就可以定位到函数,但这在远程调用上却不一定行得通,机器A怎么知道机器B上函数究竟怎么命的名。

解决方案就是,机器A和机器B各自放一张映射表,为每个函数分配独一无二的Id,只要两张表上相同的Id对应的是相同的函数,机器A每次调用函数时,附上函数的Id,机器B就可以通过映射表确认机器A想要调用的函数。

  • 怎么传参?

    • 机器A需要把参数序列化,转成字节流,传给服务端。(序列化)
    • 服务端把字节流转换为自己能读取的格式。(反序列化)

gRPC 就是 Google 基于 Protobuf 开发的跨语言的开源 RPC框架。gRPC 基于 HTTP/2协议 设计,可以基于一个 HTTP/2 链接提供多个服务,对于移动设备更加友好。

Protobuf

Google Protocol Buffer(简称 Protobuf)是一种轻便高效的结构化数据存储格式,平台无关、语言无关、可扩展,可用于通讯协议数据存储等领域。

特点

  • 平台无关,语言无关,可扩展;
  • 提供了友好的动态库,使用简单;
  • 解析速度快,比对应的XML快约20-100倍;
  • 序列化数据非常简洁、紧凑,与XML相比,其序列化之后的数据量约为1/3到1/10。

Protobuf语法入门

安装protoc

protobuf release,选择适合自己操作系统的压缩包文件

将解压后得到的protoc二进制文件移动到$GOPATH/bin

在终端或者cmd中输入 protoc 看到如下提示信息,说明安装成功

image-20221209142527625

可以看到,protoc 只可以生成c++、c#、Java、kotlin、objective-c、php、python和ruby的代码,并没有Go。所以还需要安装插件。

image-20221209142736676

插件的安装:

 go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
 go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

接下来,我们通过实战,来熟悉微服务开发的大致流程。

Demo 远程计算器

Step 1 服务定义

新建项目lean-grpc ,初始目录如下:

 learn-grpc
 ├── calcu
 │   └── proto
 │       └── gen
 ├── go.mod
 └── go.sum
 ​

calcu/proto目录下新建文件calcu.proto,并写入内容:

 syntax = "proto3"; //使用proto3语法
 package calcu; //暂时无需理解,和grpc-gateway有关
 option go_package="learn-grpc/calcu/proto/gen;calcupb"; //“放置生成的go文件的目录;生成go文件的包名”
 ​
 ​
 //定义了 CalcuService 服务,提供了 Add Sub Muti Divi 四个接口
 service CalcuService {
   rpc Add (AddRequest) returns (CalcuResponse);
   rpc Sub (SubRequest) returns (CalcuResponse);
   rpc Muti (MultiRequest) returns (CalcuResponse);
   rpc Divi (DiviRequest) returns (CalcuResponse);
 }
 ​
 message CalcuResponse{
   float result = 1;
 }
 ​
 //每条消息各字段后的Id必须不同
 message AddRequest{
   float num1 = 1;
   float num2 = 2;
 }
 ​
 message SubRequest{
   float subtracted = 1;
   float subtractor = 2;
 }
 ​
 message MultiRequest{
   float num1 = 1;
   float num2 = 2;
 }
 ​
 message DiviRequest{
   float divide_num = 1;
   float divisor = 2;
 }
 ​

cd lean-grpc/proto目录下,执行protoc -I=. --go-grpc_out=paths=source_relative:gen calcu.proto命令。如图,可以看到proto为我们生成了通信所需要的 go 代码。

image-20221209162015847

step 2 实现服务

可以看见calcu_grpc.pb.go中已经定义好了服务的接口,我们只需要实现具体逻辑就行

image-20221209162113912

calcu/handler中创建文件,实现CalcuServieceServer中的所有接口

 package handler
 ​
 import (
     "context"
     "errors"
     "log"
 ​
     calcupb "learn-grpc/calcu/proto/gen"
 )
 ​
 type CalcuServer struct {
     calcupb.UnimplementedCalcuServiceServer
 }
 ​
 func (*CalcuServer) Add(_ context.Context, req *calcupb.AddRequest) (*calcupb.CalcuResponse, error) {
     resp := &calcupb.CalcuResponse{
         Result: req.Num1 + req.Num2,
     }
     log.Printf("[add]: %f + %f\n", req.Num1, req.Num2)
     return resp, nil
 }
 ​
 func (*CalcuServer) Sub(_ context.Context, req *calcupb.SubRequest) (*calcupb.CalcuResponse, error) {
     resp := &calcupb.CalcuResponse{
         Result: req.Subtracted - req.Subtractor,
     }
     log.Printf("[sub]: %f - %f\n", req.Subtracted, req.Subtractor)
     return resp, nil
 }
 ​
 func (*CalcuServer) Muti(_ context.Context, req *calcupb.MultiRequest) (*calcupb.CalcuResponse, error) {
     resp := &calcupb.CalcuResponse{
         Result: req.Num1 * req.Num2,
     }
     log.Printf("[muti]: %f * %f\n", req.Num1, req.Num2)
     return resp, nil
 }
 ​
 func (*CalcuServer) Divi(_ context.Context, req *calcupb.DiviRequest) (*calcupb.CalcuResponse, error) {
     if req.Divisor == 0 {
         return nil, errors.New("the divisor cannot be equal to zero")
     }
     resp := &calcupb.CalcuResponse{
         Result: req.DivideNum * req.Divisor,
     }
     log.Printf("[divi]: %f / %f\n", req.DivideNum, req.Divisor)
     return resp, nil
 ​
 }
 ​

Step 3 启动服务

calcu文件中新建 main.go文件,写入内容:

 package main
 ​
 import (
     "log"
     "net"
     
     "google.golang.org/grpc"
 ​
     "learn-grpc/calcu/handler"
     calcupb "learn-grpc/calcu/proto/gen"
 )
 ​
 func main() {
 ​
     lis, err := net.Listen("tcp", ":5001")
     if err != nil {
         log.Fatalf("failed to listen: %v", err)
     }
 ​
     s := grpc.NewServer()
     calcupb.RegisterCalcuServiceServer(s, &handler.CalcuServer{})
 ​
     log.Fatal(s.Serve(lis))
 }
 ​

点击运行,启动 calcu 服务

Step 4 调用服务

可以看到,proto已经为我们实现好了调用该服务的客户端 CalcuServiceClient,我们拿来直接使用即可。

image-20221209170138690

值得注意的事,在实际的开发中,内容相同 proto 文件,既会存在服务的提供方,也会存在服务的调用方。

比如这里的 calcu 服务作为服务的提供方,会有一份 calcu.proto文件,只管实现生成代码中的CalcuServieceServer

如果另外一个微服务 serviceA 想调用 calcu服务,那么也会保留一个 calcu.proto文件。然后只会使用其中的CalcuServiceClient

只要双方的proto文件是相同的,服务间的调用就是可行的。这样,两个服务间的代码互不影响,甚至两个服务可以是不同的语言开发的。

简便起见,这里不再另外起一个服务调用 calcu 服务中的接口。直接在项目的根目录下创建main.go文件,在主函数中调用即可

 package main
 ​
 import (
     "context"
     "fmt"
     "log"
 ​
     "google.golang.org/grpc"
 ​
     calcupb "learn-grpc/calcu/proto/gen"
 )
 ​
 func main() {
 ​
     //设置日志格式
     log.SetFlags(log.Lshortfile)
 ​
     conn, err := grpc.Dial("localhost:5001", grpc.WithInsecure())
     if err != nil {
         log.Fatalf("cannot connnet server:%v", err)
     }
     Client := calcupb.NewCalcuServiceClient(conn)
 ​
     //调用加法的接口
     r, err := Client.Add(context.Background(), &calcupb.AddRequest{Num1: 5.20, Num2: 13.14})
     if err != nil {
         panic(err.Error())
     }
     fmt.Printf("5.20 + 13.14 = %f\n", r.Result)
 ​
     //调用减法的接口
     r, err = Client.Sub(context.Background(), &calcupb.SubRequest{Subtracted: 1024, Subtractor: 666})
     if err != nil {
         panic(err.Error())
     }
     fmt.Printf("1024 - 666 = %f\n", r.Result)
 ​
     //调用乘法的接口
     r, err = Client.Muti(context.Background(), &calcupb.MultiRequest{Num1: 5.20, Num2: 13.14})
     if err != nil {
         panic(err.Error())
     }
     fmt.Printf("5.20 * 13.14 = %f\n", r.Result)
 ​
     //调用除法的接口
     r, err = Client.Divi(context.Background(), &calcupb.DiviRequest{DivideNum: 1024, Divisor: 666})
     if err != nil {
         panic(err.Error())
     }
     fmt.Printf("1024 / 666 = %f\n", r.Result)
 ​
 }

点击运行

这是调用方的输出,可以看到四个接口都成功返回

image-20221209174617150

服务端的日志也没有问题:

image-20221209174700183

可以看到,通过使用proto 为生成的CalcuServiceClient 。我们可以像调用本地函数一样调用远程服务。至此,grpc的基本用法介绍完毕。

最终结构如下:

 learn-grpc
 ├── calcu
 │   ├── handler
 │   │   └── calcu.go
 │   ├── main.go
 │   └── proto
 │       ├── calcu.proto
 │       └── gen
 │           ├── calcu.pb.go
 │           └── calcu_grpc.pb.go
 ├── go.mod
 ├── go.sum
 └── main.go
 ​