
grpc 初学习 --- 分布式数据库内核研发基础知识笔记
grpc 初学习 --- 分布式数据库内核研发基础知识笔记
经过前段时间的推进,我意识到分布式数据库内核的研发,与 PostgreSQL 内核研发相比,有着很不一样的思路(尽管部分概念上有所相同,但是两者的立场与视角,存在着极大的差别)。因此,我必须更加静下心来,推进学习,同时也要继续促进业内的联结与交流,因为自历史上面看,只有一个开放的、坦率的、相互学习的系统,才可以真正产生出值得信赖的成果来。
理解 grpc,我们就必须了解到,rpc 究竟是什么?它解决了什么样的问题?这就需要我们先做出一定的了解,参考下面的内容。
rpc: 远程系统调用
想要理解 rpc,我们就首先要对单体应用程序/微服务等重要的知识,建立一个基本的了解,它们是我们一系列工作的重要基础。
单体应用程序与微服务
单体应用程序,就像传统的 Unix 小工具程序(如罗列文件夹的 ls 应用程序,tree 应用程序等),其所有的功能,以及拓展的指令,都在一个小型的应用程序之中实现,它非常适合于那些中小型的传统应用研发。
但是伴随着数据规模与程序规模的提升,这种单体程序,其开发的成本与维护的成本,会越来越恐怖,援引《微服务:从设计到部署》中的论述,即是:
走向单体地狱
不幸的是,这种简单的方法有很大的局限性。成功的应用有一个趋势,随着时间推移而变得越来越臃肿。
您的开发团队在每个冲刺阶段都要实现更多的用户需求,这意味着需要添加许多行代码。几年之后,小而简单的应用将会逐渐成长成一个庞大的单体 。
为了给出一个极端示例,我最近和一位开发者做了交谈,他正在编写一个工具,该工具用于从他们的数百万行代码(lines of code,LOC)应用中分析出数千个 JAR 之间的依赖。我相信这是大量开发者在多年齐心协力下创造出了这样的野兽。
而解决的办法,就是“微服务”理念的提出,和单体工程开发将各个功能组织成为一个个模块(如 PostgreSQL 将客户端工具组织为 psql、pg_dump 等模块,Redis 将指令一个个写成 json 结合对应函数)一样,面对庞大的应用开发,按照功能将其整合为“微服务”,成为了一种流行的做法,这里我们继续援引《微服务:从设计到部署》的内容,参考如下:
微服务 — 解决复杂问题
许多如 Amazon、eBay 和 Netflix 这样的组织,已经采用现在所谓的微服务架构模式 解决了这个问题,而不是构建一个臃肿的单体应用。它 的思路是将应用程序分解成一套较小的互连服务。
一个服务通常实现了一组不同的特性或功能,例如订单管理、客户管理等。每一个微服务都是一个迷你应用,它自己的六边形架构包括了业务逻辑以及多个适配器。
一些微服务会暴露一个供其他微服务或应用客户端消费的 API。其他微服务可能实现了一个 web UI。
在运行时,每个实例通常是一个云虚拟机(virtual machine,VM)或者一个 Docker 容器。
而在这种背景下,rpc 作为微服务的一个重要组成部分,为我们构建微服务,提供了重要的支撑与指导作用,让我们继续参考相关的一些内容,他们是我们后续一系列工作的重要基础。
客户端与服务端模型
万变不离其宗,理当看到,无论是微服务,还是消息传递,它们都遵循着“客户端-服务端”的模式,也就是一组服务(充当着客户端的角色),用于主动地向另外一组服务,申请某种数据,或者提供某种数据要求其处理,而另外一组服务(充当着客户端的角色),则等待着来自于客户端服务的连接,之后按照情况加以处理。
区别在于,服务形式做了更好的封装,使得这个过程更为容易让人理解,我们使用传统的办法连接 PostgreSQL 的时候,我们就必须想办法拼接“连接字符串”(请参考 https://www.postgresql.org/docs/current/libpq-connect.html 的有关内容),如图所示:
而连接的时候,则使用客户端工具时,如图所示:
(实际的过程,就是 psql 工具拼接出字符串,为 “postgresql://localhost/postgres”,代表连接到本地的 postgres 数据库上面,并将其送达给 postgres 进程,要求其建立连接)
而当 PostgreSQL 以服务形式提供出来时,可以变为如此:
(PostgREST 将 PostgreSQL 的对外交流方式打包为了服务,但实质还是不变,就是将不同的字符串通过 PostgreSQL 应用层协议传输给 PostgreSQL 服务端,处理以后,由充当客户端角色的服务反馈给用户结果)
API
API 代表了某种可能的动作,我们可以用函数去理解它,实际上就是传入一些数据,在这些数据的指导展开某种工作,并且将结果反馈出来。
以 Redis 为案例,参考如下:
(Redis 的服务形式,可以看到,它非常类似于普通的网址访问,感兴趣的读者可以参考 https://redis.io/docs/latest/operate/rc/api/get-started/use-rest-api 做更为深刻的了解)
微服务某种意义上,就是将各种组件,组织为了一个又一个 API,就像微信中提供的一个个小程序一样。
数据的序列化
数据的序列化,便是采用某种数据组织数据与解析数据,方式多种多样,如 Json 文档,如图所示:
[
{
"name": "青年数据库学习会 MOP 社区"
}
]
而可读文本的一个缺陷,就在于数据量不够紧凑,因此另外一种处理办法,就是使用二进制的格式来加以处理,Protocol Buffers 就是一种极好的处理方案,参考如下:
(Go 版本下面的 Protocol Buffer 的做法,就是将要传递的数据,打包为一个又一个消息,并将其以二进制形式加以传输)
(来自于 “CPP 加油站” --- 《Protobuf通信协议详解:代码演示、详细原理介绍等》)
消息
在过去我们开发 Windows GDI 程序的时候,利用消息接口处理各种场景是一项非常基本的技能,参考如下:
/*
援引 https://learn.microsoft.com/zh-cn/windows/win32/gdi/wm-paint 中的案例
感谢微软的工作人员们!
*/
/* hwnd 代指需要绘图的窗口,uMsg 代表来自于外部的消息,wparam 与 lparam 代表消息可能携带的数据 */
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
/* 依据不同的消息展开不同的处理 */
switch (uMsg)
{
case WM_DESTROY: // 窗口销毁消息
PostQuitMessage(0); // 退出消息循环,结束程序运行
return 0;
case WM_PAINT: // 窗口绘制消息
{
// 这段代码的含义是绘制一个矩形
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hwnd, &ps);
// All painting occurs here, between BeginPaint and EndPaint.
FillRect(hdc, &ps.rcPaint, (HBRUSH) (COLOR_WINDOW+1));
EndPaint(hwnd, &ps);
}
return 0;
}
return DefWindowProc(hwnd, uMsg, wParam, lParam); // 非上述消息,采用默认处理模型
}
在这组逻辑之下,可以看到,Windows 程序的工作流程,就是使用一个回调函数用于处理来自于 Windows 发布的各种“消息”(你可以把他理解为一个工单),而每一种消息,都有着各自不同的处理逻辑乃至于数据,这就需要我们展开灵活的处理办法。
而这种理念,也被应用于分布式网络程序的开发之中,实际上在 rpc 的许多服务,都是采用消息展开数据交换乃至于处理的。
rpc 小结
现在,我们对于 rpc 所相关联的几个基本的概念,有了一个基本的理解,现在让我们将目光放到 rpc 本身上面,它的名称叫做“远程过程调用”,其核心便在于简化调用远程服务调用的过程,让我们使用远程的服务,和使用本地的服务一样简单(而中间的复杂细节,许多框架将会帮助我们进行处理)
接下来,让我们把目光放于 grpc 上面,它是我们本篇文章将要阐述的重点。
grpc 简介
grpc 便是 Google 发布的一款通用形式的 rpc 框架,可以参考文章《Introducing gRPC, a new open source HTTP/2 RPC Framework》,在这里非常感谢他们,以及于雨老师 和 @Bert 老师的指导意见,以及陈小伟老师提供的学习机会
而参照着 grpc 提供的 “Quick Start” 章节(具体请参考 https://grpc.io/docs/languages/go/quickstart/),让我们执行下面的指令:
# 假定处于 Fedora Linux 下面
sudo dnf install go
# 安装 Protobuf 库与 Grpc 库
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
并在 .bashrc 文件中写入如下的内容:
# 导入 Go 语言的路径,确保 shell 可以直接调度 go 指令
export GOPATH=$HOME/go
export PATH=$PATH:$GOPATH/bin
之后执行如下的指令:
git clone -b v1.67.0 --depth 1 https://github.com/grpc/grpc-go
cd grpc-go/examples/helloworld
之后,首先搭建服务端的服务:
go run greeter_server/main.go
再使用客户端服务对接服务端服务:
go run greeter_client/main.go
运行结果如下所示:
具体的代码分析:protocol buffer 文件分析
syntax = "proto3";
option go_package = "google.golang.org/grpc/examples/helloworld/helloworld";
option java_multiple_files = true;
option java_package = "io.grpc.examples.helloworld";
option java_outer_classname = "HelloWorldProto";
package helloworld;
// The greeting service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
// The request message containing the user's name.
message HelloRequest {
string name = 1;
}
// The response message containing the greetings
message HelloReply {
string message = 1;
}
可以看到,文件在声明了基本的组织格式信息,乃至于对接的 go 程序信息与 java 程序信息后(后者可以忽略),就着手定义了包的名称,以及自身所提供的服务(rpc 代表是一个 rpc 服务,服务名称为 SayHello,而接收的数据被组织为 “HelloRequest” 的消息形式,而输出的消息则被组织为 “HelloReply” 形式),以及具体的消息格式(这里比较简单,两个消息都是字符串形式)。
具体的代码分析:服务端服务部分
这里,我们摘取关键的代码部分加以阐述
// 定义了服务所绑定的端口信息
var (
port = flag.Int("port", 50051, "The server port")
)
// 实现 helloworld 包中 服务端部分的类型
type server struct {
pb.UnimplementedGreeterServer
}
// 具体的实现代码,in 是对于 Protocol Buffer 中 HelloRequest 消息包的抽象,HelloReply 同理
// 它们即代表输入的消息与输出的消息
func (s *server) SayHello(_ context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
log.Printf("Received: %v", in.GetName())
return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil
}
func main() {
// 这部分代码都是通用的,搭建一个对外服务接口,接收客户端连接
flag.Parse()
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
pb.RegisterGreeterServer(s, &server{})
log.Printf("server listening at %v", lis.Addr())
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
具体的代码分析:客户端服务部分
const (
defaultName = "world"
)
// 连接的端口以及客户端服务的名称
var (
addr = flag.String("addr", "localhost:50051", "the address to connect to")
name = flag.String("name", defaultName, "Name to greet")
)
func main() {
// 通用代码,连接服务端
flag.Parse()
conn, err := grpc.NewClient(*addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
c := pb.NewGreeterClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
// 这里便是调用了服务端中提供的对外服务接口
r, err := c.SayHello(ctx, &pb.HelloRequest{Name: *name})
if err != nil {
log.Fatalf("could not greet: %v", err)
}
// 接收反馈内容
log.Printf("Greeting: %s", r.GetMessage())
}
更为复杂的案例,可以参考 https://grpc.io/docs/languages/go/basics/ ,讨论了如何传输更为复杂的消息包。
写在最后
人的发展总是取决于一次次地意识到自己的不足。
非常感谢应急管理大学的袁国铭老师、开放原子开源基金会的张凯老师与 PingCAP 的陈小伟老师,让我拓宽了自己在数据库内核领域的眼界,意识到了自己的极大不足,这必须通过不断学习才可以弥补起来。
非常感谢中国 PostgreSQL 分会的魏波老师,王其达老师,IvorySQL 的任娇老师与牛世继老师以及王守波老师,他们为我参与 PostgreSQL 领域起到了极好的帮助作用,而在这个途中,我也意识到自己做事过于不严谨的问题,必须想办法加以改正。
非常感谢 OpenTenBase 的符芬菊老师,非常感谢 Kiwi 社区的于雨老师,这两位老师是我开始意识到自己在分布式数据库领域极大不足的关键人物,同时非常感谢@巡山大王,@熊灿灿,@黄宏亮,@Bert,@machinly,@汤瑞麟老师在我学习途中给予的宝贵的指导意见。
最后非常感谢青学会 MOP 社区的吴洋老师,少安事务所的严少安老师,悠然老师,彭冲老师等朋友们的帮助作用。
非常谢谢你们,期待将来能够共同为建设一个更为繁荣的数据库内核生态而努力!