【微服务 + RPC | 青训营笔记】

129 阅读6分钟

image.png

这是我参与【第五届青训营】伴学笔记创作活动的第九天。 今天就讲一下微服务的相关知识及微服务的架构,以及一些 RPC编程 有关的知识。

微服务 + RPC

互联网技术飞速发展的今天,微服务备受关注。微服务架构模式具有非常明显的优势—-特别是在实施敏捷开发和复杂的企业应用迭代开发方面。

微服务—单体应用

传统的一个集成的应用,如一个打车系统。

特点

  1. 比较容易开发,能看到整个项目
  2. 能在本地进行调试测试
  3. 容易部署

常见的互联网公司静态逻辑架构图:

1.png

问题和限制

  1. 随着业务量增长,维护单体应用系统的性能和维护成本都会受到限制。
  2. 应用程序复杂,对于任何一个开发人员都显得过于庞大。
  3. 持续部署受挫,更新的话需要重新部署整个应用。
  4. 应用难以扩展。
  5. 技术升级困难。
  6. 可靠性低,bug 影响大。

根据以上特性,单体应用适合业务量小,功能简单的小体量应用。

微服务—架构模式

微服务解决方案

微服务架构

一些微服务会向外暴露一组供其他模块访问和使用的 APl 。其他微服务实现了自己的业务逻辑,在必要时,可以通过 API 进行业务逻辑访问。如下图一个案例:

2.png

微服务与数据库

每一个服务都有自己的数据库模式,这种做法与企业级数据库数据模型做法相背。

单一数据库 -> 多个数据库

特点:

  1. 系统性能相对较高。
  2. 每个模块独立性增强,更有利于分工合作。

微服务的优缺点

优点

  1. 解决复杂问题
  2. 团队分工协作容易
  3. 独立部署
  4. 程序扩展能力强

缺点

  1. 规模难以确定
  2. 增加系统复杂度
  3. 分区数据库构造难题
  4. 项目测试难度增加
  5. 多服务修改更加困难
  6. 需要进行多次部署

微服务面临的问题

客户端如何访问这些服务(API Gateway 网关)

  • 提供统一服务中心,让服务对前台透明。
  • 聚合后台的服务,节省流量,提升性能。
  • 提供安全,过滤,流控等 API 管理功能。
3.png

服务之间如何通信

微服务与微服务之间的通信就是进程间的通信(IPC)

  1. 同步调用:比较简单,一致性强,性能相对较差

    • 服务之间(REST):基于HTTP, 实现更容易,各种语言支持,且能跨客户端。
    • 服务内部(RPC):传输效率高,安全性可控。
  2. 异步消息调用:分布式系统中应用广泛,必须引入 Broker 作为中间代理池,多用于不需要及时反馈时。

多服务管理

可采用服务管理框架(如 Zookeeper),管理服务的上下线以及更新等动作。

1675326555482.png

服务宕机异常处理

  • 重试机制
  • 限流
  • 负载均衡
  • 本地缓存

RPC 简介及原理

特点

这种调用的过程跨越了物理服务器的限制,是在网络中完成的,在调用远端服务器上程序的过程中,本地程序等待返回调用结果,直到远端程序执行完毕,将结果进行返回到本地,最终完成一次完整的调用。

RPC技术架构

  • 客户端:服务调用发起方。
  • 服务器:远端计算机运行的程序,其中包含客户端要调用和访问的方法。
  • 客户端存根:存放服务端的地址,端口信息。将客户端的请求打包成网络信息发送到服务方。接收服务方返回的数据包。
  • 服务端存根:接收客户端发送的数据包,解析数据包,调用具体的服务方法。将调用结果打包发送给客户端一方。
5.png

RPC设计的相关技术

  1. 动态代理技术:client stub 和 server stub 一般都是使用动态代理自动生成的一段程序。

  2. 序列化和反序列化

    • (序列化)编码:把对象转换成字节序列的过程
    • (反序列化)解码:把字节序列恢复为对象的过程

RPC编程实践

net/rpc 库实现 RPC 编程

  1. 服务的定义及暴露

    在编程实现过程中,服务器端需要注册结构体对象,然后通过对象所属的方法暴露给调用者,从而提供服务,该方法称之为输出方法。

    func(t *T) MethodName(request T1,response *T2) error
    

    规则:

    • 有且只有两个参数,这两个参数只能试输出类型或内建类型
    • 方法的类型是可输出的,方法本身也是可输出的
  2. 简单示例测试

    此方式服务端常驻,客户端执行后结束。

    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)
    }
    
  3. 多参数调用:用结构体存储

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 {
        // 输出查询
    }
}