乐趣区

关于golang:还在手撸Go微服务快来试试gozero超乎你的想象

Github

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

疾速构建高并发微服务

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

要想做好微服务,咱们须要了解和把握的知识点十分多,从几个维度上来说:

  • 基本功能层面

    1. 并发管制 & 限流,防止服务被突发流量击垮
    2. 服务注册与服务发现,确保可能动静侦测增减的节点
    3. 负载平衡,须要依据节点承受能力散发流量
    4. 超时管制,防止对已超时申请做无用功
    5. 熔断设计,疾速失败,保障故障节点的恢复能力
  • 高阶性能层面

    1. 申请认证,确保每个用户只能拜访本人的数据
    2. 链路追踪,用于了解整个零碎和疾速定位特定申请的问题
    3. 日志,用于数据收集和问题定位
    4. 可观测性,没有度量就没有优化

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

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

上面我通过短链微服务来演示通过 go-zero 疾速的创立微服务的流程,走完一遍,你就会发现:原来编写微服务如此简略!

1. 什么是短链服务?

短链服务就是将长的 URL 网址,通过程序计算等形式,转换为简短的网址字符串。

写此短链服务是为了从整体上演示 go-zero 构建残缺微服务的过程,算法和实现细节尽可能简化了,所以这不是一个高阶的短链服务。

2. 短链微服务架构图

  • 这里只用了 Transform RPC 一个微服务,并不是说 API Gateway 只能调用一个微服务,只是为了最简演示 API Gateway 如何调用 RPC 微服务而已
  • 在真正我的项目里要尽可能每个微服务应用本人的数据库,数据边界要清晰

3. goctl 各层代码生成一览

所有绿色背景的功能模块是主动生成的,按需激活,红色模块是须要本人写的,也就是减少下依赖,编写业务特有逻辑,各层示意图别离如下:

  • API Gateway

  • RPC

  • 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
  • 创立工作目录shorturl
  • shorturl 目录下执行 go mod init shorturl 初始化go.mod

5. 编写 API Gateway 代码

  • 通过 goctl 生成 api/shorturl.api 并编辑,为了简洁,去除了文件结尾的info,代码如下:

    type (
        expandReq struct {shorten string `form:"shorten"`}
    
        expandResp struct {url string `json:"url"`}
    )
    
    type (
        shortenReq struct {url string `form:"url"`}
    
        shortenResp struct {shorten string `json:"shorten"`}
    )
    
    service shorturl-api {
        @server(handler: ShortenHandler)
        get /shorten(shortenReq) returns(shortenResp)
    
        @server(handler: ExpandHandler)
        get /expand(expandReq) returns(expandResp)
    }

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

    • service shorturl-api {这一行定义了 service 名字
    • @server局部用来定义 server 端用到的属性
    • handler定义了服务端 handler 名字
    • get /shorten(shortenReq) returns(shortenResp)定义了 get 办法的路由、申请参数、返回参数等
  • 应用 goctl 生成 API Gateway 代码

    goctl api go -api shorturl.api -dir .

    生成的文件构造如下:

    .
    ├── api
    │   ├── etc
    │   │   └── shorturl-api.yaml         // 配置文件
    │   ├── internal
    │   │   ├── config
    │   │   │   └── config.go             // 定义配置
    │   │   ├── handler
    │   │   │   ├── expandhandler.go      // 实现 expandHandler
    │   │   │   ├── routes.go             // 定义路由解决
    │   │   │   └── shortenhandler.go     // 实现 shortenHandler
    │   │   ├── logic
    │   │   │   ├── expandlogic.go        // 实现 ExpandLogic
    │   │   │   └── shortenlogic.go       // 实现 ShortenLogic
    │   │   ├── svc
    │   │   │   └── servicecontext.go     // 定义 ServiceContext
    │   │   └── types
    │   │       └── types.go              // 定义申请、返回构造体
    │   ├── shorturl.api
    │   └── shorturl.go                   // main 入口定义
    ├── go.mod
    └── go.sum
  • 启动 API Gateway 服务,默认侦听在 8888 端口

    go run shorturl.go -f etc/shorturl-api.yaml
  • 测试 API Gateway 服务

    curl -i "http://localhost:8888/shorten?url=http://www.xiaoheiban.cn"

    返回如下:

    HTTP/1.1 200 OK
    Content-Type: application/json
    Date: Thu, 27 Aug 2020 14:31:39 GMT
    Content-Length: 15
    
    {"shortUrl":""}

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

  • 能够批改 internal/svc/servicecontext.go 来传递服务依赖(如果须要)
  • 实现逻辑能够批改 internal/logic 下的对应文件
  • 能够通过 goctl 生成各种客户端语言的 api 调用代码
  • 到这里,你曾经能够通过 goctl 生成客户端代码给客户端同学并行开发了,反对多种语言,详见文档

6. 编写 transform rpc 服务

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

    能够通过命令生成 proto 文件模板

    goctl rpc template -o transform.proto

    批改后文件内容如下:

    syntax = "proto3";
    
    package transform;
    
    message expandReq {string shorten = 1;}
    
    message expandResp {string url = 1;}
    
    message shortenReq {string url = 1;}
    
    message shortenResp {string shorten = 1;}
    
    service transformer {rpc expand(expandReq) returns(expandResp);
        rpc shorten(shortenReq) returns(shortenResp);
    }
  • goctl 生成 rpc 代码,在 rpc/transform 目录下执行命令

    goctl rpc proto -src transform.proto

    文件构造如下:

    rpc/transform
    ├── etc
    │   └── transform.yaml              // 配置文件
    ├── internal
    │   ├── config
    │   │   └── config.go               // 配置定义
    │   ├── logic
    │   │   ├── expandlogic.go          // expand 业务逻辑在这里实现
    │   │   └── shortenlogic.go         // shorten 业务逻辑在这里实现
    │   ├── server
    │   │   └── transformerserver.go    // 调用入口, 不须要批改
    │   └── svc
    │       └── servicecontext.go       // 定义 ServiceContext,传递依赖
    ├── pb
    │   └── transform.pb.go
    ├── transform.go                    // rpc 服务 main 函数
    ├── transform.proto
    └── transformer
        ├── transformer.go              // 提供了内部调用办法,无需批改
        ├── transformer_mock.go         // mock 办法,测试用
        └── types.go                    // request/response 构造体定义

    间接能够运行,如下:

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

    etc/transform.yaml文件里能够批改侦听端口等配置

7. 批改 API Gateway 代码调用 transform rpc 服务

  • 批改配置文件shorturl-api.yaml,减少如下内容

    Transform:
      Etcd:
        Hosts:
          localhost:2379
        Key: transform.rpc

    留神:这个网站 md 反对不敌对,localhost:2379 原文为- localhost:2379

  • 批改 internal/config/config.go 如下,减少 transform 服务依赖

    type Config struct {
        rest.RestConf
        Transform rpcx.RpcClientConf     // 手动代码
    }
  • 批改internal/svc/servicecontext.go,如下:

    type ServiceContext struct {
        Config    config.Config
        Transformer transformer.Transformer                                          // 手动代码
    }
    
    func NewServiceContext(c config.Config) *ServiceContext {
        return &ServiceContext{
            Config:    c,
        Transformer: transformer.NewTransformer(rpcx.MustNewClient(c.Transform)),  // 手动代码
        }
    }

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

  • 批改 internal/logic/expandlogic.go 里的 Expand 办法,如下:

    func (l *ExpandLogic) Expand(req types.ExpandReq) (*types.ExpandResp, error) {
      // 手动代码开始
        resp, err := l.svcCtx.Transformer.Expand(l.ctx, &transformer.ExpandReq{Shorten: req.Shorten,})
        if err != nil {return nil, err}
    
        return &types.ExpandResp{Url: resp.Url,}, nil
      // 手动代码完结
    }

通过调用 transformerExpand办法实现短链复原到 url

  • 批改internal/logic/shortenlogic.go,如下:

    func (l *ShortenLogic) Shorten(req types.ShortenReq) (*types.ShortenResp, error) {
      // 手动代码开始
        resp, err := l.svcCtx.Transformer.Shorten(l.ctx, &transformer.ShortenReq{Url: req.Url,})
        if err != nil {return nil, err}
    
        return &types.ShortenResp{Shorten: resp.Shorten,}, nil
      // 手动代码完结
    }

通过调用 transformerShorten办法实现 url 到短链的变换

至此,API Gateway 批改实现,尽管贴的代码多,然而期中批改的是很少的一部分,为了不便了解上下文,我贴了残缺代码,接下来解决 CRUD+cache

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

  • shorturl 下创立 rpc/transform/model 目录:mkdir -p rpc/transform/model
  • 在 rpc/transform/model 目录下编写创立 shorturl 表的 sql 文件shorturl.sql,如下:

    CREATE TABLE `shorturl`
    (`shorten` varchar(255) NOT NULL COMMENT 'shorten key',
      `url` varchar(255) NOT NULL COMMENT 'original url',
      PRIMARY KEY(`shorten`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
  • 创立 DB 和 table

    create database gozero;
    source shorturl.sql;
  • rpc/transform/model 目录下执行如下命令生成 CRUD+cache 代码,-c示意应用redis cache

    goctl model mysql ddl -c -src shorturl.sql -dir .

    也能够用 datasource 命令代替 ddl 来指定数据库链接间接从 schema 生成

    生成后的文件构造如下:

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

9. 批改 shorten/expand rpc 代码调用 crud+cache 代码

  • 批改rpc/transform/etc/transform.yaml,减少如下内容:

    DataSource: root:@tcp(localhost:3306)/gozero
    Table: shorturl
    Cache:
      Host: localhost:6379

    留神:Host: localhost:6379原文为 - Host: localhost:6379, 为了防止 md 反对不敌对问题
    能够应用多个 redis 作为 cache,反对 redis 单点或者 redis 集群

  • 批改rpc/transform/internal/config.go,如下:

    type Config struct {
        rpcx.RpcServerConf
        DataSource string             // 手动代码
        Table      string             // 手动代码
        Cache      cache.CacheConf    // 手动代码
    }

    减少了 mysql 和 redis cache 配置

  • 批改rpc/transform/internal/svc/servicecontext.go,如下:

    type ServiceContext struct {
        c     config.Config
      Model *model.ShorturlModel   // 手动代码
    }
    
    func NewServiceContext(c config.Config) *ServiceContext {
        return &ServiceContext{
            c:             c,
            Model: model.NewShorturlModel(sqlx.NewMysql(c.DataSource), c.Cache, c.Table), // 手动代码
        }
    }
  • 批改rpc/transform/internal/logic/expandlogic.go,如下:

    func (l *ExpandLogic) Expand(in *expand.ExpandReq) (*expand.ExpandResp, error) {
        // 手动代码开始
        res, err := l.svcCtx.Model.FindOne(in.Shorten)
        if err != nil {return nil, err}
    
        return &transform.ExpandResp{Url: res.Url,}, nil
        // 手动代码完结
    }
  • 批改rpc/shorten/internal/logic/shortenlogic.go,如下:

    func (l *ShortenLogic) Shorten(in *shorten.ShortenReq) (*shorten.ShortenResp, error) {
      // 手动代码开始,生成短链接
        key := hash.Md5Hex([]byte(in.Url))[:6]
        _, err := l.svcCtx.Model.Insert(model.Shorturl{
            Shorten: key,
            Url:     in.Url,
        })
        if err != nil {return nil, err}
    
        return &transform.ShortenResp{Shorten: key,}, nil
      // 手动代码完结
    }

    至此代码批改实现,凡事手动批改的代码我加了标注

10. 残缺调用演示

  • shorten api 调用

    curl -i "http://localhost:8888/shorten?url=http://www.xiaoheiban.cn"

    返回如下:

    HTTP/1.1 200 OK
    Content-Type: application/json
    Date: Sat, 29 Aug 2020 10:49:49 GMT
    Content-Length: 21
    
    {"shorten":"f35b2a"}
  • expand api 调用

    curl -i "http://localhost:8888/expand?shorten=f35b2a"

    返回如下:

    HTTP/1.1 200 OK
    Content-Type: application/json
    Date: Sat, 29 Aug 2020 10:51:53 GMT
    Content-Length: 34
    
    {"url":"http://www.xiaoheiban.cn"}

11. Benchmark

因为写入依赖于 mysql 的写入速度,就相当于压 mysql 了,所以压测只测试了 expand 接口,相当于从 mysql 里读取并利用缓存,shorten.lua 里随机从 db 里获取了 100 个热 key 来生成压测申请

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

12. 残缺代码

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

12. 总结

咱们始终强调 工具大于约定和文档

go-zero 不只是一个框架,更是一个建设在框架 + 工具根底上的,简化和标准了整个微服务构建的技术体系。

咱们在放弃简略的同时也尽可能把微服务治理的复杂度封装到了框架外部,极大的升高了开发人员的心智累赘,使得业务开发得以疾速推动。

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

有任何好的晋升工程效率的想法,随时欢迎交换!????

退出移动版