【启程Golang之旅】从零开始构建可扩展的微服务架构
欢迎来到Golang的世界!在当今快节奏的软件开发领域,选择一种高效、简洁的编程语言至关重要。而在这方面,Golang(又称Go)无疑是一个备受瞩目的选择。在本文中,带领您探索Golang的世界,一步步地了解这门语言的基础知识和实用技巧。
近几年微服这个词闯入我们的视线,那么什么是微服务?微服务的概念是怎么产生的?接下来我们开始正式了解一下Go语言与微服务的千丝万缕与来龙去脉!
目录
初识微服务
初识RPC协议
初识ProtoBuf
初识GRPC
初识微服务
微服务是一种软件架构风格,旨在将大型应用程序拆分为一组小的、独立的服务。这些服务围绕特定的业务功能组织,每个服务可以独立开发、测试、部署和扩展。微服务之间通过轻量级的通信机制(如 HTTP/REST 或消息队列)进行交互。
什么是微服务:在介绍微服务时,首先得先理解什么是微服务,顾名思义,微服务得从两个方面去理解,什么是"微"、什么是"服务"?微(micro)狭义来讲就是体积小,著名的"2pizza团队"很好的诠释了这一解释(2pizza团队最早是亚马逊CEOBezos提出来的,意思是说单个服务的设计,所有参与人从设计、开发、测试、运维所有人加起来只需要2个披萨就够了)。服务(service)一定要区别于系统,服务一个或者一组相对较小且独立的功能单元,是用户可以感知最小功能集。那么广义上来讲,微服务是一种分布式系统解决方案,推动细粒度服务的使用,这些服务协同工作。
微服务和单体式架构区别:微服务架构将应用拆分为多个独立的服务,关注特定功能,便于独立部署和扩展;而单体式架构则将所有功能打包在一个应用中,整体开发和部署,扩展和维护较为复杂,具体如下所示:
新功能开发 | 传统单体架构 | 分布式微服务架构 |
---|---|---|
部署 | 不经常而且容易部署 | 经常发布,部署复杂 |
隔离性 | 故障影响范围大 | 故障影响范围小 |
架构设计 | 初期技术选型难度大 | 设计逻辑难度大 |
系统性能 | 相对时间快,吞吐量小 | 相对时间慢,吞吐量大 |
系统运维 | 运维难度简单 | 运维难度复杂 |
新人上手 | 学习曲线大(应用逻辑) | 学习曲线大(架构逻辑) |
技术 | 技术单一而且封闭 | 技术多样而且容易开发 |
测试和差错 | 简单 | 复杂(每个服务都要进行单独测试,还需要集群测试) |
系统扩展性 | 扩展性差 | 扩展性好 |
系统管理 | 重点在于开发成本 | 重点在于服务治理和调度 |
初识RPC协议
简单了解了微服务定义和优缺点之后,在我们正式学习微服务框架之前,需要首先了解一下RPC协议,为什么要了解RPC协议?RPC协议具体是什么呢?
RPC协议概念:RPC(Remote Procedure Call Protocol)也叫 “远程过程调用协议”,是一种使程序能够在网络上调用远程服务器上的函数或服务的通信协议,它允许客户端发送请求到远程服务器并接收执行结果,就像调用本地函数一样,其常用于微服务架构中支持服务之间的高效交互,RPC协议通常包括以下几个主要特点:
1)透明性:调用远程服务看起来就像调用本地服务,隐藏了网络通信的复杂性。
2)协议支持:可以通过多种协议(如 HTTP、TCP、UDP)进行通信。
3)语言无关性:支持不同编程语言之间的交互,常通过序列化格式(如 JSON、Protocol Buffers)传输数据。
RPC协议具体是什么:是一种允许程序在网络中调用另一台计算机上的函数或服务的协议。它使得开发者能够像调用本地函数一样,轻松地请求远程服务。
为什么微服务需要使用RPC:微服务使用RPC的好处就是不限定服务的提供方使用什么技术选型,能够实现公司跨团队的技术解耦,如下图所示:
RPC的使用步骤: 接下来开始简述一下RPC的使用步骤:
----服务端:
1)注册rpc服务对象,给对象绑定方法(1. 定义类、2. 绑定类方法)
rpc.RegisterName("服务名", 回调对象)
2)创建监听器:listener, err := net.Listen()
3)建立连接:conn, err := listener.Accept()
4)将连接绑定rpc服务:rpc.ServeConn(conn)
----客户端:
1)用rpc连接服务器:conn, err := rpc.Dial()
2)调用远程函数:conn.Call("服务名.方法名", 传入参数, 传出参数)
RPC相关函数:接下来开始讲解一下使用RPC函数所涉及到的一些函数:
1)注册rpc服务:
// 参数1:服务名,字符串类型
// 参数2:对应rpc对象,该对象绑定方法需要满足如下条件:
/*
1)方法必须是导出的 —— 包外可见,首字母大写。
2)方法必须有两个参数,都是导出类型、内建类型。
3)方法的第二个参数必须是 “指针”(传出参数)
4)方法只有一个 error 接口类型的 返回值。
*/
func (server *Server) RegisterName(name string, rcvr interface{}) error
// 举例:
type World struct {}
func (this *World) HelloWorld(name string, resp *string) error {}
rpc.RegisterName("服务名", new(World))
2)绑定rpc服务:
// conn: 成功建立好连接的 socket -- conn
func (server *Server) ServeConn(conn io.ReadWriteCloser)
3)调用远程函数:
// serviceMethod: “服务名.方法名”
// args: 传入参数,方法需要数据
// reply: 传出参数,定义 var 变量,&变量名 完成传参
func (client *Client) Call (serviceMethod string, args interface{}, reply interface{}) error
这里我们开始做一个演示,这里我们写一个服务器端:
package main
import (
"fmt"
"net"
"net/rpc"
)
// World 定义类对象
type World struct {
}
// HelloWorld 绑定类方法
func (this *World) HelloWorld(name string, resp *string) error {
*resp = name + "你好!"
return nil
}
func main() {
// 01注册RPC服务,绑定对象方法
err := rpc.RegisterName("hello", new(World))
if err != nil {
fmt.Println("注册 rpc 服务失败", err)
return
}
// 02设置监听
listener, err := net.Listen("tcp", "127.0.0.1:8080")
if err != nil {
fmt.Println("监听失败", err)
return
}
defer listener.Close()
fmt.Println("开始监听...")
// 03建立连接
conn, err := listener.Accept()
if err != nil {
fmt.Println("绑定服务失败", err)
return
}
defer conn.Close()
fmt.Println("连接成功...")
// 04绑定服务
rpc.ServeConn(conn)
}
然后客户端这里我们写通过rpc连接服务器然后调用远程方法:
package main
import (
"fmt"
"net/rpc"
)
func main() {
// 01 用rpc链接服务器 --Dial()
conn, err := rpc.Dial("tcp", "127.0.0.1:8080")
if err != nil {
fmt.Println("Dial error", err)
return
}
defer conn.Close()
// 02 调用远程函数方法
var reply string // 接受返回值 --- 传出参数
err = conn.Call("hello.HelloWorld", "zhangsan", &reply)
if err != nil {
fmt.Println("call error", err)
return
}
fmt.Println(reply)
}
最终达到的效果如下所示:
jsonrpc使用:因为rpc使用了go语言特有的数据序列化gob,其他编程语言不能解析,所以这里我们还需要使用通用的能实现序列化和反序列化的json操作,这里我们使用nc充当服务器,如下:
初识ProtoBuf
Protobuf是ProtocolBuffers的简称,它是Google公司开发的一种数据描述语言,是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化或者说序列化,它很适合做数据存储或RPC数据交换格式,可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式,目前提供了C++、Java、Python三种语言的APl,需要了解的两点是:
1)protobuf是类似与json一样的数据描述语言(数据格式)
2)protobuf非常适合于RPC数据交换格式
protobuf的优势与劣势:
---优点---
1)序列化后体积相比Json和XML很小,适合网络传输
2)支持跨平台语言
3)消息格式升级和兼容性还不错
4)序列化反序列化速度很快,快于json的处理速度
---劣势---
1)应用不够广(相比xml和json)
2)二进制格式导致可读性差
3)缺乏自描述
Protobuf的具体安装可以参考我之前的文章:地址 这里不再赘述,如下我们简单的讲解一下在日常编写protobuf过程中的注意事项有哪些,如下所示:
1)message成员编号可以不从1开始,但是不能重复,不能使用 19000 - 19999
2)可以使用message嵌套
3)可以使用定义数组、切片使用 repeated 关键字
4)可以使用枚举enum
5)可以使用联合体:oneof关键字,成员编号依然不能重复
syntax = "proto3";
// 指定所在包名
package pd;
option go_package = "./";
// 定义枚举类型
enum Week {
Monday = 0; // 枚举值,必须从0开始
Tuesday = 1;
}
// 定义消息体
message Student {
int32 age = 1; // 可以不从1开始,但是不能重复,不能使用19000-20000的数字
string name = 2;
People p = 3;
repeated int32 score = 4; // 定义数组
Week w = 5; // 定义枚举类型
oneof data {
string teacher = 6;
string class = 7;
}
}
// 消息体可以嵌套
message People {
int32 weight = 1;
}
然后我们终端执行如下命令可以看到我们生成了对应的go文件:
protoc --go_out=./ *proto
注意:protobuf编译期间默认不编译服务,例如如下的rpc服务是不会被protobuf编译的:
// 添加rpc服务
service bj38 {
rpc Say(People) returns (Student) {}
}
想要编译服务的话需要使用gRPC,其编译指令可以参考如下的内容:
protoc --go_out=plugins=grpc:./ *.proto
初识GRPC
如果从Protobuf的角度看,GRPC只不过是一个针对service接口生成代码的生成器,如果想详细了解gRPC,可以参考我之前的文章:地址 ,接着我们来学习一下GRPC的用法,这里我们创建一个简单的proto文件,定义一个HelloService接口:
syntax = "proto3";
package pd;
// 消息体 --一个package中,不允许定义同名的消息体
message Teacher {
int32 age = 1;
string name = 2;
}
// 定义服务
service SayName {
rpc SayHello (Teacher) returns (Teacher);
}
然后我们终端执行如下命令进行执行proto文件:
protoc --go-grpc_out=. *.proto
执行完命令之后,我们的文件已经生成出来了:
创建grpc服务端:接下来我们开始创建grpc的服务端,来调用我们proto文件生成的go文件:
package main
import (
pd "chat-simple/src/pd"
"context"
"fmt"
"google.golang.org/grpc"
"net"
)
// Children 定义类
type Children struct {
pd.UnimplementedSayNameServer // 嵌入未实现部分
}
// SayHello 按接口绑定类方法
func (this *Children) SayHello(ctx context.Context, t *pd.Teacher) (*pd.Teacher, error) {
t.Name += "is Sleeping"
return t, nil
}
func main() {
// 01 初始一个grpc对象
grpcServer := grpc.NewServer()
// 02 注册服务
pd.RegisterSayNameServer(grpcServer, new(Children))
// 03 设置监听
listener, err := net.Listen("tcp", ":8800")
if err != nil {
fmt.Println("Listen err", err)
return
}
defer listener.Close()
// 04 启动grpc服务
err = grpcServer.Serve(listener)
if err != nil {
return
}
}
创建客户端:接下来创建客户端,调用服务端的一个函数并把数据进行显示,两者通信的桥梁就是我们proto生成的go文件里面的函数:
package main
import (
pd "chat-simple/src/pd"
"context"
"fmt"
"google.golang.org/grpc"
)
func main() {
// 01连接服务
grpcConn, err := grpc.Dial("localhost:8800", grpc.WithInsecure())
if err != nil {
fmt.Println("grpc连接失败", err)
return
}
defer grpcConn.Close()
// 02初始化grpc客户端
grpcClient := pd.NewSayNameClient(grpcConn)
// 03调用grpc服务
t, err := grpcClient.SayHello(context.TODO(), &pd.Teacher{Name: "张三", Age: 18})
fmt.Println(t, err)
}