go-zero(七) RPC服务和ETCD使用

go zero RPC和ETCD

在实际的开发中,微服务通常是分布式的,每个服务运行在独立的进程中,甚至可能在不同的服务器上。RPC允许这些服务以透明的方式相互调用方法,就像在本地调用一样,从而简化了服务间的通信。

RPC使调用者不需要关注服务的实现细节,只需知道接口,更方便协作编程 ,多数RPC框架能够根据服务定义自动生成客户端代码,减少了人为错误并加速了开发过程。

RPC可以比传统的HTTP/REST更快地进行数据交换,这在需要高性能和低延迟的微服务系统中尤为重要

一、API和RPC

1.API 和 RPC 的关系

API和RPC是两种不同的服务交互模式,分别用于处理HTTP请求和服务间的远程调用。

  1. API(应用程序编程接口):

    • API通常用于暴露微服务的HTTP接口,允许客户端(如前端应用)通过HTTP请求与微服务进行交互。
    • API主要处理用户请求,进行输入验证、路由、HTTP状态码处理等。
  2. RPC(远程过程调用):

    • RPC用于微服务之间的直接调用,通常通过协议(如gRPC)进行通信。
    • RPC更适合于内部服务之间的高效交互,特别是在需要低延迟和高吞吐量的场景中。

2.API 文件与 Proto 文件的约定

在项目开发中,API定义和RPC定义应保持一定的约定,以确保服务的一致性和可维护性:

  1. 接口定义一致性

    • API层和RPC层应尽量保持一致的接口,比如在API层定义的请求和响应结构体,RPC层也应使用相同或相似的结构体。这有助于降低服务侵入和理解成本。
  2. 数据结构的映射

    • 当API请求到达时,通常需要将HTTP请求参数映射到RPC调用的参数。此时,可以定义一个公共的请求和响应数据结构,以减少重复定义和维护成本。
  3. 错误处理一致性

    • 在API和RPC之间应保持类似的错误处理逻辑,以便在发生错误时能够提供一致的错误响应。

3.示例约定

  • API 文件(.api)示例:
    # 文件: user.api
    service user-api {
        @server(
            rest: {
                path: /users,
                method: POST
            }
        )
        register(req RegisterRequest) returns (RegisterResponse);
    
        @server(
            rest: {
                path: /users/{id},
                method: GET
            }
        )
        getUser(id int64) returns (GetUserResponse);
    }
    
    message RegisterRequest {
        string name = 1;
        int32 age = 2;
    }
    
    message RegisterResponse {
        int64 id = 1;
    }
    
    message GetUserResponse {
        string name = 1;
        int32 age = 2;
    }
    
    


 **Proto 文件(.proto)**示例:
  ```protobuf
  syntax = "proto3";

  package user;

  service UserService {
      rpc Register(RegisterRequest) returns (RegisterResponse);
      rpc GetUser(GetUserRequest) returns (GetUserResponse);
  }

  message RegisterRequest {
      string name = 1;
      int32 age = 2;
  }

  message RegisterResponse {
      int64 id = 1;
  }

  message GetUserRequest {
      int64 id = 1;  // 请求者根据ID获取用户信息
  }

  message GetUserResponse {
      string name = 1;
      int32 age = 2;
  }

下面我将通过一个简单的示例,说明如何使用go zero框架和 Protocol Buffers 定义 RPC 服务。

二、生成 RPC代码

在这个教程中,我们根据user.api文件,来编写一个user.proto 文件,创建一个用户管理的 RPC 服务.

1.回顾user.api

go zero的 API 文件通常定义 HTTP 接口,包括请求和响应的格式。它主要用于 RESTful API 的定义。

我们先来回顾下之前的user.api文件

type (
    RegisterRequest {
        Username string `json:"username" validate:"required"` // 注册请求,用户名
        Password string `json:"password" validate:"required"` // 注册请求,密码
    }
    RegisterResponse {
        Message string `json:"message"` // 响应消息
    }
)

type (
    LoginRequest {
        Username string `json:"username" validate:"required"` // 登录请求,用户名
        Password string `json:"password" validate:"required"` // 登录请求,密码
    }
    LoginResponse {
        Token string `json:"token"` // 登录响应,JWT token
    }
)

@server (
    group:  user // 代表当前 service 代码块下的路由生成代码时都会被放到 user 目录下
    prefix: /v1 //定义路由前缀为 "/v1"
)
// 微服务名称为 user-ap,生成的代码目录和配置文件将和 user 值相关
service user-api {
    //用户注册
    @handler RegisterHandler
    //提交post请求   RegisterRequest为请求体  RegisterResponse为响应体
    post /register (RegisterRequest) returns (RegisterResponse)

    //用户登录
    @handler LoginHandler
    post /login (LoginRequest) returns (LoginResponse)
}

//因为我们想要通过jwt来传递数据,所以我们不需求请求信息
type (
    GetInfoResponse {
        Username string `json:"username" `
        Password string `json:"password" `
    }
)

//更新数据我们就简单修改下密码
type (
    UpdataRequest {
        Password string `json:"password" ` // 更新请求,新的密码
    }
    UpdataResponse {
        Message string `json:"message"` // 更新响应消息
    }
)

@server (
    group:  user
    prefix: /v1
    jwt:    Auth //开启jwt
)
service user-api {
    @handler GetInfoHandler
    // 不需要请求信息
    post /getinfo returns (GetInfoResponse)

    @handler UpdataHandler
    post /updata (UpdataRequest) returns (UpdataResponse)
}

2. 创建user.proto 文件

Proto 文件,用于定义 gRPC 服务和消息格式。它主要用于远程过程调用(RPC),使用 Protocol Buffers 进行数据序列化。

根据user.api文件创建user.proto文件:

syntax = "proto3";  //声明使用 Protocol Buffers 的版本为 3

//proto 包名
package pb;
//生成 golang 代码后的包名
option go_package ="./pb";  //必须要有“./” 这样的路径
//指定生成的 Go 代码的包名为 pb,在同一目录下创建一个名为 pb 的包

//定义用户消息结构
message Users {
  int64 id = 1; // 用户 ID
  string username = 2; // 用户名
  string password = 3; // 密码
  int64 createdAt = 4; // 创建时间
}


//注册请求消息
message RegisterReq {
  string username = 1; // 用户名
  string password = 2; // 密码
  int64 createdAt = 3; // 创建时间
}

//注册响应消息
message RegisterResp {
  string message =1; // 响应消息
}
//登录请求消息
message LoginReq {
  string username = 1; // 用户名
  string password = 2; // 密码
}
//登录响应消息
message LoginResp {
  string token = 1; // JWT token
}

//更新数据请求消息
message UpdateReq {
  string password = 1; //password
}

//更新数据响应消息
message UpdateResp {
  string message =1;
}


// 通过username查询 请求消息
message GetInfoReq {
  string username = 1; //id
}

// 通过username查询 响应消息
message GetInfoResp {
  Users users = 1; //users
}

//定义了一个名为 user 的服务,提供了四个 RPC(Remote Procedure Call)方法,
// 分别对应用户的注册、登录、更新信息和查询信息操作:
service user{
  rpc Register(RegisterReq) returns (RegisterResp);
  rpc Login(LoginReq) returns (LoginResp);
  rpc Update(UpdateReq) returns (UpdateResp);
  rpc Getinfo(GetInfoReq) returns (GetInfoResp);
}

可以看到user.proto文件 和 user.api结构都差不多

3.生成RPC项目

goctl rpc 是 goctl 中的核心模块之一,其可以通过 .proto 文件一键快速生成一个 rpc 服务,在项目根目录下新建一个RPC 目录,并执行以下命令生成 RPC 服务:

 goctl rpc protoc user.proto  --go_out=. --go-grpc_out=. --zrpc_out=. --client=true 

  • goctl rpc protoc 是根据 protobufer 文件生成 rpc 服务
  • go_out 是proto生成的go代码所在的目录,proto本身的命令参数
  • go-grpc_out 是proto生成的grpc代码所在的目录,proto本身的命令参数,和go_out必须同一个目录
  • zrpc_out 是 goctl rpc自带的命令,go zero生成的代码所在的目录
  • client=true 是否生成客户端代码

三、实现RPC服务端

1. 配置说明

在使用gRPC服务时,我们会用到go zero内置RpcServerConf配置

RpcSeverConf

  • ServiceConf: 基础服务配置
  • ListenOn: 监听地址
  • Etcd: etcd 配置项
  • Auth: 是否开启 Auth
  • Redis: rpc 认证,仅当 Auth 为 true 生效
  • Timeout: 超时时间
  • Middlewares: 启用中间件
  • Health: 是否开启健康检查

我们现在只关注EtcdEndpoints 这两个参数说明,rpc服务端和客户端直接是支持etcd和直连的。

我们暂时使用直连模式,稍后在介绍ETCD

2. 初始化配置

在实际开发中,都是使用RPC来调用model,

配置yml
user.yaml配置中添加数据库连接和 JWT 配置,并去掉etcd配置,采用直连模式:

Name: user.rpc
ListenOn: 0.0.0.0:8080
#我们暂时使用直连模式,稍后在介绍ETCD
#取消Endpoints直连,使用etcd
#UserRpcConf:
 # Etcd:
 #   Hosts:
 #     - 127.0.0.1:2379
#   Key: user.rpc  #和server的key要一致
 # Endpoints:
  #  - 127.0.0.1:8080

#为RPC添加 auth和数据连接
JwtAuth:
  AccessSecret: "sss12345678" # AccessSecret的值要是8位以上的字符串
  AccessExpire: 10000 #AccessExpire是过期时间,单位是秒

MysqlDB:
    # 数据库需要替换成你实际的地址、账号、密码和数据库
  DbSource: "root:root@tcp(127.0.0.1:33069)/test_zero?charset=utf8mb4&parseTime=True&loc=Local"

config.go映射配置
接着在config.go文件中添加字段:

type Config struct {
    zrpc.RpcServerConf

    JwtAuth struct {
        AccessSecret string
        AccessExpire int64
    }

    MysqlDB struct {
        DbSource string `json:"dbSource"`
    }
}

svc注入
servicecontext.go文件中注册数据模型,

type ServiceContext struct {
    Config config.Config
    UserModel model.UsersModel   //配置model
}

func NewServiceContext(c config.Config) *ServiceContext {
    return &ServiceContext{
        Config:    c,
        // 用数据库连接初始化model
        UserModel: model.NewUsersModel(sqlx.NewMysql(c.MysqlDB.DbSource)),
    }
}

到这你会发现,rpc用法基本上和原来的api服务一样。

3. 实现业务逻辑

现在我们来实现业务逻辑,这里我只演示登录业务,因为代码基本上和之前的API项目没啥区别,打开loginlogic.go:

func (l *LoginLogic) Login(in *pb.LoginReq) (*pb.LoginResp, error) {
    // todo: add your logic here and delete this line

    userModel := l.svcCtx.UserModel
    //在api中 是req.Username  现在修改成 in.Username
    user, err := userModel.FindOneByUsername(l.ctx, in.Username)
    if err != nil && err != model.ErrNotFound {
        return nil, errors.New(0, "数据库连接失败")
    }

    //从配置文件中获取secret 、secret
    secret := l.svcCtx.Config.JwtAuth.AccessSecret
    expire := l.svcCtx.Config.JwtAuth.AccessExpire
    //生成jwt token
    token, err := getJwtToken(secret, time.Now().Unix(), expire, in.Username)
    if err != nil {
        return nil, errors.New(4, "token生成失败")
    }
    //查询username判断是否有数据
    if user != nil {
        //如果有数据,密码是否和数据库匹配
        //在api中 是req.Password  现在修改成 in.Password 
        if in.Password == user.Password {
            
            
            return &pb.LoginResp{
                Token: "Bearer " + token, //开头添加Bearer
            }, nil

        } else {
            return nil, errors.New(5, "密码错误")
        }
    } else {
        return nil, errors.New(6, "用户未注册")

    }
}

四、使用grpcurl工具测试RPC服务

由于 RPC 不能像 REST API 那样直接使用 Postman的http 或 cURL 进行测试,我们需要使用 grpcurl 工具。

1.启动反射服务

在使用工具调用之前,我们需要在代码中,开启反射服务。

我们先看下RPC的main函数:

func main() {
    /*
    .....
    */
    
    s := zrpc.MustNewServer(c.RpcServerConf, func(grpcServer *grpc.Server) {
        pb.RegisterUserServer(grpcServer, server.NewUserServer(ctx))
        //如果模式是 开发模式(dev)或者是测试模式(test)则启动反射服务。
        if c.Mode == service.DevMode || c.Mode == service.TestMode {
            reflection.Register(grpcServer)
        }
    })

    /*
    .....
    */
    
}

所以我们需要在yaml中添加字段

Mode: test

2.安装grpcurl

先去网站中下载好工具,下载完成后,需要配置环境变量或者直接放在GOPATH的bin目录下即可。

下载地址:https://github.com/fullstorydev/grpcurl

2.使用grpcurl

查看所有服务

grpcurl -plaintext 127.0.0.1:8080 list

输出结果:

grpc.health.v1.Health
grpc.reflection.v1.ServerReflection
grpc.reflection.v1alpha.ServerReflection
pb.user

可以看到我们的pb.user服务

查看服务方法列表

查看我们的pb.user服务的方法列表:

grpcurl -plaintext 127.0.0.1:8080 list pb.user

输出结果:

pb.user.Getinfo
pb.user.Login
pb.user.Register
pb.user.Update

可以看到我们有四个方法

调用方法

我们演示下调用login方法,使用 -d 传入json参数:


grpcurl -plaintext -d "{\"username\": \"admin\", \"password\": \"123456\"}" 127.0.0.1:8080 pb.user.Login

前面的示例使用 \ 对 " 字符进行转义。 转义 " 在 PowerShell 控制台中是必需的,但在某些控制台中不得使用。 例如,macOS 控制台

grpcurl的使用方法你们可以去看下官方文档,我就不过多介绍了。

3.使用postman测试

如果你觉得使用命令很繁琐,可以使用postman更加方便

我不确定postman能不能直接使用,如果不能直接使用还是先去安装下grpcurl

首先新建一个请求,然后把http 切换成gRPC

image.png

我们先选择对应的proto文件 ,然后输入服务地址

image.png

选好proto文件后,会自动弹出服务的方法,我们选择Login

image.png

最后输入服务地址和请求参数,invoke 测试服务。:

image.png

五、实现RPC客户端(在API中使用)

现在我们需要实现RPC的客户端,也就是在API中实现RPC的调用。

1. 配置说明

在使用客户端调用RPC时,我们会用到go zero内置RpcClientConf配置

RpcClientConf

  • Etcd: 服务发现配置,当需要使用 etcd 做服务发现时配置
  • Endpoints: RPC Server 地址列表,用于直连,当需要直连 rpc server 集群时配置
  • Target: 域名解析地址,名称规则请参考
  • App: rpc 认证的 app 名称,仅当 rpc server 开启认证时配置
  • Token: rpc 认证的 token,仅当 rpc server 开启认证时配置
  • NonBlock: 是否阻塞模式,当值为 true 时,不会阻塞 rpc 链接
  • Timeout: 超时时间
  • KeepaliveTime: 保活时间
  • Middlewares: 是否启用中间件

2.初始化配置

首先我们需要在API中配置RPC的链接 ,打开user-api.yaml ,添加字段:

Name: user-api
Host: 0.0.0.0
Port: 8889

#使用直连模式
UserRpcConf:
  Endpoints:
    - 127.0.0.1:8080

服务端使用的直连,所以客户端我们也使用Endpoints进行直连,因为我们现在使用rpc对model进行操作,所以需要删除关于数据库Auth 认证相关的字段.

接着修改,config.go文件:


type Config struct {
    rest.RestConf
    
    // 映射rpc Client配置
    UserRpcConf zrpc.RpcClientConf
}

最后修改servicecontext.go文件,把rpc服务注册到上下文:

type ServiceContext struct {
    Config    config.Config
    UserRpc   user.User  //添加user服务
}

func NewServiceContext(c config.Config) *ServiceContext {
    return &ServiceContext{
        Config:    c,
        // 初始化user服务
        UserRpc:   user.NewUser(zrpc.MustNewClient(c.UserRpcConf)),
    }
}

2. 调用RPC服务

我们在loginlogic.go中调用 RPC 登录服务:

import (
    /*
    ....省略其他包
    */
    userRpc "newTest/newrpc/user"  //导入userRPC服务,设置别名避免冲突
    // 为了避免服务名冲突,应该在proto文件就考虑好服务名的设置
)

func (l *LoginLogic) Login(req *types.LoginRequest) (resp *types.LoginResponse, err error) {
    // todo: add your logic here and delete this line
     //从服务上下文获取UserRpc的login服务
    loginResp, err := l.svcCtx.UserRpc.Login(l.ctx, &userRpc.LoginReq{
        Username: req.Username,  //从请求中获取username
        Password: req.Password, //从请求中获取Password
    })
    if err != nil {
        return nil, errors.New(13, "登录rpc链接错误")
    }
    return &types.LoginResponse{
        Token: loginResp.Token,
    }, nil
}

通过代码你可以发现,go zero调用RPC服务就是这么简单,像调用本地服务一样。

3.测试api

启动 user RPC 和 user API 的服务,进行测试:
[图片上传失败...(image-4bda73-1748189081494)]

六、ETCD简单介绍

如何让服务快速并透明地加入计算集群,并确保集群中的所有机器都能迅速找到共享的配置信息,同时构建一个高可用、安全、易于部署且响应迅速的服务集群,都是待解决的挑战。etcd 作为一种工具,为解决这些问题提供了有效的解决方案。

1.什么是 etcd?

etcd 是一个开源的分布式键值存储系统,主要用于配置共享服务发现数据管理,以确保各个服务之间能够高效、可靠地协调和通信。

etcd 通常使用 Raft 算法来确保强一致性和高可用性,这使它非常适合存储和管理配置信息、服务注册信息以及其他关键数据。

[图片上传失败...(image-27ec37-1748189081495)]

etcd 的主要特点

  1. 强一致性

    • etcd 使用 Raft 共识算法,确保数据的一致性和高可靠性,特别是在节点故障或网络分区的情况下。
  2. 高可用性

    • etcd 能够根据选定的副本数进行扩展,并能够承受部分节点的故障,而不会影响整体服务的可用性。
  3. 简洁的 API

    • etcd 提供简单的 HTTP/gRPC 接口,允许用户轻松存取和管理数据。
  4. 支持 Watch 功能

    • 用户可以订阅/观察特定键的更改,这使得应用程序能够根据配置变化做出实时反应。
  5. 版本控制

    • etcd 自动为每个键生成版本号,用户可以方便地访问和回滚到特定版本的配置。
  6. 轻松集成

    • etcd 在云原生应用程序和微服务架构中广泛使用,特别是 Kubernetes 作为其核心组件。

2.为什么使用 etcd?

使用 etcd 的原因主要包括以下几点:

  1. 配置管理

    • 在分布式系统中,服务的配置通常需要在多个节点之间共享和一致。etcd 允许你集中存储所有配置,并在节点之间保持一致性。
  2. 服务发现

    • 微服务架构中,应该能够快速发现和连接到可用的服务实例。etcd 提供服务注册和发现功能,使得服务可以动态注册和注销。
  3. 高可用性和容错性

    • etcd 设计上考虑了高可用性和节点故障容错。当某些节点发生故障时,系统可以通过其他节点继续提供服务,从而避免单点故障。
  4. 强一致性保障

    • 尽管分布式系统的拓扑结构可能变化,但是 etcd 确保了一致性,使得在任何时间点读取的数据都是最新的,适合管理关键配置数据。
  5. 实时监控与通知

    • 使用 etcd 的 Watch 功能,可以在配置更改时获得实时通知,这对于动态更新配置非常有用。

3. 在docker中部署etcd

我们使用docke部署etcd ,创建etcd.yaml,把下面的内容保存到改文件上:

ersion: "3.5"
services:
  Etcd:
    container_name: etcd3-go zero
    image: bitnami/etcd:3.5.6
    deploy:
      replicas: 1
      restart_policy:
        condition: on-failure
    environment:
      - ALLOW_NONE_AUTHENTICATION=yes
      - ETCD_SNAPSHOT_COUNT=10000
      - ETCD_QUOTA_BACKEND_BYTES=6442450944
    privileged: true
    volumes:
      - ./volumes/etcd/data:/bitnami/etcd/data
    ports:
      - 2379:2379
      - 2380:2380

然后使用docker-compose拉取仓库:

docker-compose -f docker-compose-etcd.yml up -d

七、在项目中使用etcd

只需要把原来的直连,修改成ETCD 即可
server端
在 user.yaml 中取消 etсd 的注释并配置:

Name: user.rpc
ListenOn: 0.0.0.0:8080
# 取消etcd的注释,重新启用
Etcd:
  Hosts:
  - 127.0.0.1:2379
  Key: user.rpc

client端
在 user-api.yaml 中修改 RPC 配置,使用 etcd 进行服务发现:

Name: user-api
Host: 0.0.0.0
Port: 8889

#取消Endpoints直连,使用etcd
UserRpcConf:
  #Endpoints:
  #  - 127.0.0.1:8080
  Etcd:
    Hosts:
      - 127.0.0.1:2379
    Key: user.rpc  #和server的key要一致

注意 server和client的 key要一致

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容