前置知识
单体架构
单体系统,指的是只在一台服务器上面运行的系统。这种架构有很大的局限性,硬件性能上会存在瓶颈,这种硬件上的瓶颈是不可避免的,所以这种系统只适合对性能,并发等指标要求不高的系统。
说起分布式系统,我们就不得不说下分布式系统的祖先——集中式系统。集中式系统跟分布式系统是完全相反的两个概念。集中式系统就是把所有的程序、功能都集中到一台主机上,从而往外提供服务的方式。(前后端不分离)
集群
单机架构的硬件资源有限对于业务量比较大的情况是很难适用的,所以便可以在此基础之上实施集群架构方式。
集群的架构的优势在于我们无需改动任何的项目代码,只需要新增服务器部署相同的应用并配置好负载均衡,就可以很好的减轻随着业务增量带来的系统压力,并且可以直接在单机架构上直接进行调整。
分布式
将一个项目拆分成了多个模块,并将这些模块分开部署,那就算是分布式。
- 按照不同的业务域进行拆分,通过对业务进行梳理,根据业务的特性把应用拆开,不同的业务模块独立部署。例如订单、营销、风控、积分资源等。形成独立的业务领域微服务集群。(主站,直播,会员购,漫画,赛事)
- 按照一个业务功能里的不同模块或者组件进行拆分。例如把公共组件拆分成独立的原子服务,下沉到底层,形成相对独立的原子服务层。这样一纵一横,就可以实现业务的服务化拆分。
(比如登录里使用微信登录,需要调用微信的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。
安装protoc
到protobuf release,选择适合自己操作系统的压缩包文件
将解压后得到的protoc二进制文件移动到$GOPATH/bin里
在终端或者cmd中输入 protoc 看到如下提示信息,说明安装成功
可以看到,protoc 只可以生成c++、c#、Java、kotlin、objective-c、php、python和ruby的代码,并没有Go。所以还需要安装插件。
插件的安装:
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 代码。
step 2 实现服务
可以看见calcu_grpc.pb.go中已经定义好了服务的接口,我们只需要实现具体逻辑就行
在 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,我们拿来直接使用即可。
值得注意的事,在实际的开发中,内容相同 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)
}
点击运行
这是调用方的输出,可以看到四个接口都成功返回
服务端的日志也没有问题:
可以看到,通过使用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