这是我参与【第五届青训营】伴学笔记创作活动的第九天。 今天就讲一下微服务的相关知识及微服务的架构,以及一些 RPC编程 有关的知识。
微服务 + RPC
互联网技术飞速发展的今天,微服务备受关注。微服务架构模式具有非常明显的优势—-特别是在实施敏捷开发和复杂的企业应用迭代开发方面。
微服务—单体应用
传统的一个集成的应用,如一个打车系统。
特点
- 比较容易开发,能看到整个项目
- 能在本地进行调试测试
- 容易部署
常见的互联网公司静态逻辑架构图:
问题和限制
- 随着业务量增长,维护单体应用系统的性能和维护成本都会受到限制。
- 应用程序复杂,对于任何一个开发人员都显得过于庞大。
- 持续部署受挫,更新的话需要重新部署整个应用。
- 应用难以扩展。
- 技术升级困难。
- 可靠性低,bug 影响大。
根据以上特性,单体应用适合业务量小,功能简单的小体量应用。
微服务—架构模式
微服务解决方案
微服务架构
一些微服务会向外暴露一组供其他模块访问和使用的 APl 。其他微服务实现了自己的业务逻辑,在必要时,可以通过 API 进行业务逻辑访问。如下图一个案例:
微服务与数据库
每一个服务都有自己的数据库模式,这种做法与企业级数据库数据模型做法相背。
单一数据库 -> 多个数据库
特点:
- 系统性能相对较高。
- 每个模块独立性增强,更有利于分工合作。
微服务的优缺点
优点
- 解决复杂问题
- 团队分工协作容易
- 独立部署
- 程序扩展能力强
缺点
- 规模难以确定
- 增加系统复杂度
- 分区数据库构造难题
- 项目测试难度增加
- 多服务修改更加困难
- 需要进行多次部署
微服务面临的问题
客户端如何访问这些服务(API Gateway 网关)
- 提供统一服务中心,让服务对前台透明。
- 聚合后台的服务,节省流量,提升性能。
- 提供安全,过滤,流控等 API 管理功能。
服务之间如何通信
微服务与微服务之间的通信就是进程间的通信(IPC)
-
同步调用:比较简单,一致性强,性能相对较差
- 服务之间(REST):基于HTTP, 实现更容易,各种语言支持,且能跨客户端。
- 服务内部(RPC):传输效率高,安全性可控。
-
异步消息调用:分布式系统中应用广泛,必须引入 Broker 作为中间代理池,多用于不需要及时反馈时。
多服务管理
可采用服务管理框架(如 Zookeeper),管理服务的上下线以及更新等动作。
服务宕机异常处理
- 重试机制
- 限流
- 负载均衡
- 本地缓存
RPC 简介及原理
特点
这种调用的过程跨越了物理服务器的限制,是在网络中完成的,在调用远端服务器上程序的过程中,本地程序等待返回调用结果,直到远端程序执行完毕,将结果进行返回到本地,最终完成一次完整的调用。
RPC技术架构
- 客户端:服务调用发起方。
- 服务器:远端计算机运行的程序,其中包含客户端要调用和访问的方法。
- 客户端存根:存放服务端的地址,端口信息。将客户端的请求打包成网络信息发送到服务方。接收服务方返回的数据包。
- 服务端存根:接收客户端发送的数据包,解析数据包,调用具体的服务方法。将调用结果打包发送给客户端一方。
RPC设计的相关技术
-
动态代理技术:client stub 和 server stub 一般都是使用动态代理自动生成的一段程序。
-
序列化和反序列化:
- (序列化)编码:把对象转换成字节序列的过程
- (反序列化)解码:把字节序列恢复为对象的过程
RPC编程实践
net/rpc 库实现 RPC 编程
-
服务的定义及暴露
在编程实现过程中,服务器端需要注册结构体对象,然后通过对象所属的方法暴露给调用者,从而提供服务,该方法称之为输出方法。
func(t *T) MethodName(request T1,response *T2) error规则:
- 有且只有两个参数,这两个参数只能试输出类型或内建类型
- 方法的类型是可输出的,方法本身也是可输出的
-
简单示例测试
此方式服务端常驻,客户端执行后结束。
server.go
// 结构体 type MathUtil struct { } // 该方法向外暴露:提供计算圆形面积的服务 func(mu *MathUtil) CalCircleArea(req float32, resp *float32) error { *resp = math.Pi * req * req // 原型的面积 return nil } func main() { mathUtil := new(MathUtil) err := rpc.Register(mathUtil) // 调用 net/rpc 包的功能将对象的服务进行注册 if err != nil { panic(err.Error()) } rpc.HandleHTTP() // 注册到 HTTP 协议上,方便调用者利用 http 方式进行数据的传递 // 在特定的端口进行监听 listen, err := net.Listen("tcp", ":8080") if err != nil { panic(err.Error()) } http.Serve(listen, nil) }client.go
func main() { client, err := rpc.DiaalHTTP("tcp", "localhost:8080") if err != nil { panic(err.Error()) } var req float32 // 请求值 req = 3 var resp *float32 // 返回值 // 同步调用方式 err = client.Call("MathUtil.CalCircleArea", req, &resp) if err != nil { panic(err.Error()) } // 异步调用方式 syncCall := client.Go("MathUtil.CalCircleArea", req, &resp, nil) replayDone := <- syncCall.Done // 感知异步调用是否结束 fmt.Println(replayDone) fmt.Println(*resp) } -
多参数调用:用结构体存储
PRotobuf + RPC
protobuf 协议的格式: 使用该协议进行数据序列化和反序列化操作时,首先定义传输数据的格式,并命名为以".proto"为扩展名的消息定义文件。
获取 PRotobuf
go get -a github.com/golang/protobuf/protoc-gen-go
message 定义一个消息:
// 案例
syntax = "proro3" // 设定版本
package message
message OrderInfo {
required int32 Order_id = 1; // 1 代表字段顺序
required string Order_status = 2;
}
/* required : 必须要设置
optional : 消息格式中该字段可以有0个或1个值
repeated : 可以有多个值,相当于切片
*/
使用命令根据 .proto 文件生成 .go 文件(包含诸多方法)
$ protoc ./message.proto --go_out = ./
使用 protobuf 生成的文件:代替上面 RPC 中的结构体,可以使用内置的多种方法
简单案例
server.go
type OrderService struct {}
// 生成的.go文件存储在 message 包下
func (test *OrderService) GetOrderInfo(request message.OrderRequest, *response message.OrderInfo) error {
orderMap := map[string] message.OrderInfo{
"2001": {Order_id: "2001", Order_status: "已付款"},
"2002": {Order_id: "2002", Order_status: "已付款"},
} // 记录为 map 类型
current := time.Now().Unix()
if request.TimeStamp > current {
*response = message.OrderInfo{"0", "订单信息异常"}
} else {
result := orderMap[request.OrderId] // 根据 id 查找数据
if result.OrderId != "" {
*response = orderMap[request.OrderId]
} else {
return errors.New("server error")
}
}
}
// main 函数同上略
client.go : 基本与上面的简单案例类似
GRPC 框架grpc-go 安装
grpc 安装
go gete -u google.golang.org/grpc
定义服务
.proto 文件
// ......
message OrderInfo {
required int32 Order_id = 1; // 1 代表字段顺序
required string Order_status = 2;
}
// 订单服务 service 定义
service OrderService {
rpc GetOrderInfo(OrderRequest) returns (OrderInfo);
}
编译 .proto 文件
protoc --go_out = plugins=grpc:. message.proto
服务实现
按照生成的 .go 文件中的函数接口来实现函数即可,具体与上述案例差不多。
优点:省略不少内容,如注册相关的
server.go
func (test *OrderService) GetOrderInfo(ctx context.Context, request *message.OrderRequest) (* message.OrderInfo, error) {
orderMap := map[string] message.OrderInfo{
"2001": {Order_id: "2001", Order_status: "已付款"},
"2002": {Order_id: "2002", Order_status: "已付款"},
} // 记录为 map 类型
var response *message.OrderInfo
current := time.Now().Unix()
if request.TimeStamp > current {
*response = message.OrderInfo{"0", "订单信息异常"}
} else {
result := orderMap[request.OrderId] // 根据 id 查找数据
if result.OrderId != "" {
return &result, nil
} else {
return nil, errors.New("server error")
}
}
}
func main() {
server := grpc.NewServer() // 具体见 .go 文件
message.RegisterOrderServiceServer(server, new(OrderServiceImp))
lis, err := net.Listen("tcp", ":8080")
if err != nil {
panic(err.Error())
}
server.Serve(lis)
}
client.go
func main(){
// Dail 连接
conn, err := grpc.Dial("localhost:8080", grpc.WithInsecure())
if err != nil { panic(err.Error()) }
defer conn.Close()
orderServiceClient := message.NewOrderServiceClient(conn)
orderRequest := &message.OrderRequest{"2001", time.Now().Unix()}
orderInfo, err := orderServiceClient.GetOrderInfo(context.Background(), orderRequest)
if orderInfo != nil {
// 输出查询
}
}