你还在手撕微服务?快试试 go-zero 的微服务自动生成

0. 为何说作好微服务很难?

要想作好微服务,咱们须要理解和掌握的知识点很是多,从几个维度上来讲:mysql

  • 基本功能层面git

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

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

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

另外,咱们始终秉承工具大于约定和文档的理念。咱们但愿尽量减小开发人员的心智负担,把精力都投入到产生业务价值的代码上,减小重复代码的编写,因此咱们开发了goctl工具。sql

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

1. 书店服务示例简介

为了教程简单,咱们用书店服务作示例,而且只实现其中的增长书目和检查价格功能。数据库

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

2. 书店微服务架构图

architecture

3. goctl各层代码生成一览

全部绿色背景的功能模块是自动生成的,按需激活,红色模块是须要本身写的,也就是增长下依赖,编写业务特有逻辑,各层示意图分别以下:后端

  • API Gateway

api

  • RPC

rpc

  • model

model

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

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. 微信交流群

添加个人微信:kevwan,请注明go-zero,我拉进go-zero社区群🤝

好将来技术

相关文章
相关标签/搜索