开源推荐 刀耕火种微服务?何不用 go-zero 自动生成!

kevwan · 2020年09月06日 · 706 次阅读
本帖已被设为精华帖!

0. 为什么说做好微服务很难?

要想做好微服务,我们需要理解和掌握的知识点非常多,从几个维度上来说:

  • 基本功能层面

    1. 并发控制&限流,避免服务被突发流量击垮
    2. 服务注册与服务发现,确保能够动态侦测增减的节点
    3. 负载均衡,需要根据节点承受能力分发流量
    4. 超时控制,避免对已超时请求做无用功
    5. 熔断设计,快速失败,保障故障节点的恢复能力
  • 高阶功能层面

    1. 请求认证,确保每个用户只能访问自己的数据
    2. 链路追踪,用于理解整个系统和快速定位特定请求的问题
    3. 日志,用于数据收集和问题定位
    4. 可观测性,没有度量就没有优化

对于其中每一点,我们都需要用很长的篇幅来讲述其原理和实现,那么对我们后端开发者来说,要想把这些知识点都掌握并落实到业务系统里,难度是非常大的,不过我们可以依赖已经被大流量验证过的框架体系。go-zero 微服务框架就是为此而生。

另外,我们始终秉承工具大于约定和文档的理念。我们希望尽可能减少开发人员的心智负担,把精力都投入到产生业务价值的代码上,减少重复代码的编写,所以我们开发了goctl工具。

下面我通过书店服务来演示通过go-zero快速的创建微服务的流程,走完一遍,你就会发现:原来编写微服务如此简单!

1. 书店服务示例简介

为了教程简单,我们用书店服务做示例,并且只实现其中的增加书目和检查价格功能。

写此书店服务是为了从整体上演示 go-zero 构建完整微服务的过程,实现细节尽可能简化了。

2. 书店微服务架构图

architecture

3. goctl 各层代码生成一览

所有绿色背景的功能模块是自动生成的,按需激活,红色模块是需要自己写的,也就是增加下依赖,编写业务特有逻辑,各层示意图分别如下:

  • API Gateway

api

  • RPC

rpc

  • model

model

下面我们来一起完整走一遍快速构建微服务的流程,Let’s Go!🏃‍♂️

4. 准备工作

  • 安装 etcd, mysql, redis

  • 安装 goctl 工具

GO111MODULE=on GOPROXY=https://goproxy.cn/,direct go get -u github.com/tal-tech/go-zero/tools/goctl
  • 创建工作目录bookstore

  • bookstore目录下执行go mod init bookstore初始化go.mod

5. 编写 API Gateway 代码

  • bookstore/api目录下通过 goctl 生成api/bookstore.api
goctl api -o bookstore.api

编辑bookstore.api,为了简洁,去除了文件开头的info,代码如下:

type (
    addReq struct {
        book  string `form:"book"`
        price int64  `form:"price"`
    }

    addResp struct {
        ok bool `json:"ok"`
    }
)

type (
    checkReq struct {
        book string `form:"book"`
    }

    checkResp struct {
        found bool  `json:"found"`
        price int64 `json:"price"`
    }
)

service bookstore-api {
    @server(
        handler: AddHandler
    )
    get /add(addReq) returns(addResp)

    @server(
        handler: CheckHandler
    )
    get /check(checkReq) returns(checkResp)
}

type 用法和 go 一致,service 用来定义 get/post/head/delete 等 api 请求,解释如下:

  • service bookstore-api {这一行定义了 service 名字
  • @server部分用来定义 server 端用到的属性
  • handler定义了服务端 handler 名字
  • get /add(addReq) returns(addResp)定义了 get 方法的路由、请求参数、返回参数等

    • 使用 goctl 生成 API Gateway 代码
goctl api go -api bookstore.api -dir .

生成的文件结构如下:

api
├── bookstore.api                  // api定义
├── bookstore.go                   // main入口定义
├── etc
│   └── bookstore-api.yaml         // 配置文件
└── internal
    ├── config
    │   └── config.go              // 定义配置
    ├── handler
    │   ├── addhandler.go          // 实现addHandler
    │   ├── checkhandler.go        // 实现checkHandler
    │   └── routes.go              // 定义路由处理
    ├── logic
    │   ├── addlogic.go            // 实现AddLogic
    │   └── checklogic.go          // 实现CheckLogic
    ├── svc
    │   └── servicecontext.go      // 定义ServiceContext
    └── types
        └── types.go               // 定义请求、返回结构体
  • 启动 API Gateway 服务,默认侦听在 8888 端口
go run bookstore.go -f etc/bookstore-api.yaml
  • 测试 API Gateway 服务
curl -i "http://localhost:8888/check?book=go-zero"

返回如下:

HTTP/1.1 200 OK
Content-Type: application/json
Date: Thu, 03 Sep 2020 06:46:18 GMT
Content-Length: 25

{"found":false,"price":0}

可以看到我们 API Gateway 其实啥也没干,就返回了个空值,接下来我们会在 rpc 服务里实现业务逻辑

  • 可以修改internal/svc/servicecontext.go来传递服务依赖(如果需要)

  • 实现逻辑可以修改internal/logic下的对应文件

  • 可以通过goctl生成各种客户端语言的 api 调用代码

  • 到这里,你已经可以通过 goctl 生成客户端代码给客户端同学并行开发了,支持多种语言,详见文档

6. 编写 add rpc 服务

  • rpc/add目录下编写add.proto文件

可以通过命令生成 proto 文件模板

goctl rpc template -o add.proto

修改后文件内容如下:

syntax = "proto3";

package add;

message addReq {
    string book = 1;
    int64 price = 2;
}

message addResp {
    bool ok = 1;
}

service adder {
    rpc add(addReq) returns(addResp);
}
  • goctl生成 rpc 代码,在rpc/add目录下执行命令
goctl rpc proto -src add.proto

文件结构如下:

rpc/add
├── add.go                      // rpc服务main函数
├── add.proto                   // rpc接口定义
├── adder
│   ├── adder.go                // 提供了外部调用方法,无需修改
│   ├── adder_mock.go           // mock方法,测试用
│   └── types.go                // request/response结构体定义
├── etc
│   └── add.yaml                // 配置文件
├── internal
│   ├── config
│   │   └── config.go           // 配置定义
│   ├── logic
│   │   └── addlogic.go         // add业务逻辑在这里实现
│   ├── server
│   │   └── adderserver.go      // 调用入口, 不需要修改
│   └── svc
│       └── servicecontext.go   // 定义ServiceContext,传递依赖
└── pb
    └── add.pb.go

直接可以运行,如下:

$ go run add.go -f etc/add.yaml
Starting rpc server at 127.0.0.1:8080...

etc/add.yaml文件里可以修改侦听端口等配置

7. 编写 check rpc 服务

  • rpc/check目录下编写check.proto文件

可以通过命令生成 proto 文件模板

goctl rpc template -o check.proto

修改后文件内容如下:

syntax = "proto3";

package check;

message checkReq {
    string book = 1;
}

message checkResp {
    bool found = 1;
    int64 price = 2;
}

service checker {
    rpc check(checkReq) returns(checkResp);
}
  • goctl生成 rpc 代码,在rpc/check目录下执行命令
goctl rpc proto -src check.proto

文件结构如下:

rpc/check
├── check.go                    // rpc服务main函数
├── check.proto                 // rpc接口定义
├── checker
│   ├── checker.go              // 提供了外部调用方法,无需修改
│   ├── checker_mock.go         // mock方法,测试用
│   └── types.go                // request/response结构体定义
├── etc
│   └── check.yaml              // 配置文件
├── internal
│   ├── config
│   │   └── config.go           // 配置定义
│   ├── logic
│   │   └── checklogic.go       // check业务逻辑在这里实现
│   ├── server
│   │   └── checkerserver.go    // 调用入口, 不需要修改
│   └── svc
│       └── servicecontext.go   // 定义ServiceContext,传递依赖
└── pb
    └── check.pb.go

etc/check.yaml文件里可以修改侦听端口等配置

需要修改etc/check.yaml的端口为8081,因为8080已经被add服务使用了,直接可以运行,如下:

$ go run check.go -f etc/check.yaml
Starting rpc server at 127.0.0.1:8081...

8. 修改 API Gateway 代码调用 add/check rpc 服务

  • 修改配置文件bookstore-api.yaml,增加如下内容
Add:
  Etcd:
    Hosts:
      - localhost:2379
    Key: add.rpc
Check:
  Etcd:
    Hosts:
      - localhost:2379
    Key: check.rpc

通过 etcd 自动去发现可用的 add/check 服务

  • 修改internal/config/config.go如下,增加 add/check 服务依赖
type Config struct {
    rest.RestConf
    Add   rpcx.RpcClientConf     // 手动代码
    Check rpcx.RpcClientConf     // 手动代码
}
  • 修改internal/svc/servicecontext.go,如下:
type ServiceContext struct {
    Config  config.Config
    Adder   adder.Adder          // 手动代码
    Checker checker.Checker      // 手动代码
}

func NewServiceContext(c config.Config) *ServiceContext {
    return &ServiceContext{
        Config:  c,
        Adder:   adder.NewAdder(rpcx.MustNewClient(c.Add)),         // 手动代码
        Checker: checker.NewChecker(rpcx.MustNewClient(c.Check)),   // 手动代码
    }
}

通过 ServiceContext 在不同业务逻辑之间传递依赖

  • 修改internal/logic/addlogic.go里的Add方法,如下:
func (l *AddLogic) Add(req types.AddReq) (*types.AddResp, error) {
    // 手动代码开始
    resp, err := l.svcCtx.Adder.Add(l.ctx, &adder.AddReq{
        Book:  req.Book,
        Price: req.Price,
    })
    if err != nil {
        return nil, err
    }

    return &types.AddResp{
        Ok: resp.Ok,
    }, nil
    // 手动代码结束
}

通过调用adderAdd方法实现添加图书到 bookstore 系统

  • 修改internal/logic/checklogic.go里的Check方法,如下:
func (l *CheckLogic) Check(req types.CheckReq) (*types.CheckResp, error) {
    // 手动代码开始
    resp, err := l.svcCtx.Checker.Check(l.ctx, &checker.CheckReq{
        Book:  req.Book,
    })
    if err != nil {
        return nil, err
    }

    return &types.CheckResp{
        Found: resp.Found,
        Price: resp.Price,
    }, nil
    // 手动代码结束
}

通过调用checkerCheck方法实现从 bookstore 系统中查询图书的价格

9. 定义数据库表结构,并生成 CRUD+cache 代码

  • bookstore 下创建rpc/model目录:mkdir -p rpc/model

  • 在 rpc/model 目录下编写创建 book 表的 sql 文件book.sql,如下:

CREATE TABLE `book`
(
  `book` varchar(255) NOT NULL COMMENT 'book name',
  `price` int NOT NULL COMMENT 'book price',
  PRIMARY KEY(`book`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
  • 创建 DB 和 table
create database gozero;
source book.sql;
  • rpc/model目录下执行如下命令生成 CRUD+cache 代码,-c表示使用redis cache
goctl model mysql ddl -c -src book.sql -dir .

也可以用datasource命令代替ddl来指定数据库链接直接从 schema 生成

生成后的文件结构如下:

rpc/model
├── bookstore.sql
├── bookstoremodel.go     // CRUD+cache代码
└── vars.go               // 定义常量和变量

10. 修改 add/check rpc 代码调用 crud+cache 代码

  • 修改rpc/add/etc/add.yamlrpc/check/etc/check.yaml,增加如下内容:
DataSource: root:@tcp(localhost:3306)/gozero
Table: book
Cache:
  - Host: localhost:6379

可以使用多个 redis 作为 cache,支持 redis 单点或者 redis 集群

  • 修改rpc/add/internal/config.gorpc/check/internal/config.go,如下:
type Config struct {
    rpcx.RpcServerConf
    DataSource string             // 手动代码
    Table      string             // 手动代码
    Cache      cache.CacheConf    // 手动代码
}

增加了 mysql 和 redis cache 配置

  • 修改rpc/add/internal/svc/servicecontext.gorpc/check/internal/svc/servicecontext.go,如下:
type ServiceContext struct {
    c     config.Config
    Model *model.BookModel   // 手动代码
}

func NewServiceContext(c config.Config) *ServiceContext {
    return &ServiceContext{
        c:             c,
        Model: model.NewBookModel(sqlx.NewMysql(c.DataSource), c.Cache, c.Table), // 手动代码
    }
}
  • 修改rpc/add/internal/logic/addlogic.go,如下:
func (l *AddLogic) Add(in *add.AddReq) (*add.AddResp, error) {
    // 手动代码开始
    _, err := l.svcCtx.Model.Insert(model.Book{
        Book:  in.Book,
        Price: in.Price,
    })
    if err != nil {
        return nil, err
    }

    return &add.AddResp{
        Ok: true,
    }, nil
    // 手动代码结束
}
  • 修改rpc/check/internal/logic/checklogic.go,如下:
func (l *CheckLogic) Check(in *check.CheckReq) (*check.CheckResp, error) {
    // 手动代码开始
    resp, err := l.svcCtx.Model.FindOne(in.Book)
    if err != nil {
        return nil, err
    }

    return &check.CheckResp{
        Found: true,
        Price: resp.Price,
    }, nil
    // 手动代码结束
}

至此代码修改完成,凡事手动修改的代码我加了标注

11. 完整调用演示

  • add api 调用
curl -i "http://localhost:8888/add?book=go-zero&price=10"

返回如下:

HTTP/1.1 200 OK
Content-Type: application/json
Date: Thu, 03 Sep 2020 09:42:13 GMT
Content-Length: 11

{"ok":true}
  • check api 调用
curl -i "http://localhost:8888/check?book=go-zero"

返回如下:

HTTP/1.1 200 OK
Content-Type: application/json
Date: Thu, 03 Sep 2020 09:47:34 GMT
Content-Length: 25

{"found":true,"price":10}

12. Benchmark

因为写入依赖于 mysql 的写入速度,就相当于压 mysql 了,所以压测只测试了 check 接口,相当于从 mysql 里读取并利用缓存,为了方便,直接压这一本书,因为有缓存,多本书也是一样的,对压测结果没有影响。

压测之前,让我们先把打开文件句柄数调大:

ulimit -n 20000

并日志的等级改为error,防止过多的 info 影响压测结果,在每个 yaml 配置文件里加上如下:

Log:
    Level: error

benchmark

可以看出在我的 MacBook Pro 上能达到 3 万 + 的 qps。

13. 完整代码

https://github.com/tal-tech/go-zero/tree/master/example/bookstore

14. 总结

我们一直强调工具大于约定和文档

go-zero 不只是一个框架,更是一个建立在框架 + 工具基础上的,简化和规范了整个微服务构建的技术体系。

我们在保持简单的同时也尽可能把微服务治理的复杂度封装到了框架内部,极大的降低了开发人员的心智负担,使得业务开发得以快速推进。

通过 go-zero+goctl 生成的代码,包含了微服务治理的各种组件,包括:并发控制、自适应熔断、自适应降载、自动缓存控制等,可以轻松部署以承载巨大访问量。

有任何好的提升工程效率的想法,随时欢迎交流!👏

15. 项目地址

https://github.com/tal-tech/go-zero

16. 微信交流群

wechat

更多原创文章干货分享,请关注公众号
  • 加微信实战群请加微信(注明:实战群):gocnio
astaxie 将本帖设为了精华贴 09月08日 20:16
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册