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

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

  • 基本功能层面

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

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

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

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

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

1. 书店服务示例简介

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

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

2. 书店微服务架构图

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
  • 创建工作目录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

可以看出在我的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社区群

你还在手撕微服务?快试试 go-zero 的微服务自动生成的更多相关文章

  1. 设计数据库 ER 图太麻烦?不妨试试这两款工具,自动生成数据库 ER 图!!!

    忙,真忙 点赞再看,养成习惯,微信搜索『程序通事』,关注就完事了! 点击查看更多精彩的文章 这两个星期真是巨忙,年前有个项目因为各种莫名原因,一直拖到这个月才开始真正测试.然后上周又接到新需求,马不停 ...

  2. 金融数据分析还能这样做?快试试这个BI工具小白也能学会!

    说起银行.保险.股票投资等这些金融行业,大多数人都认为它们都是依靠数据驱动的企业,毕竟大数据的诞生本来就是为了金融信息流通而服务的,但是事实真的是这样吗? 事实并非如此,真正在金融行业做数据分析的人, ...

  3. WCF服务引用之后自动生成的泛型代理类名称太长的解决方案

    问题:WCF服务引用之后会将原来的泛型类自动生成一个代理类,但是有时候名称太长怎么办? 解决方案: 1.方案一: 调用客户端同样也引用这个泛型类的类库. 2.方案二: 找到这个泛型类,然后在上面的[D ...

  4. Re:从0开始的微服务架构--(二)快速快速体验微服务架构?--转

    原文地址:https://mp.weixin.qq.com/s/QO1QDQWnjHZp8EvGDrxZvw 这是专题的第二篇文章,看看如何搭建一个简单模式的微服务架构. 记得好久之前看到一个大牛说过 ...

  5. 微服务理论之二:面向微服务架构与传统架构、SOA对比,以及云化对比

    一.Monolith 网上对Microservice进行介绍的文章常常以Monolith作为开头,我也不会例外.原因是,知道了Monolith的不便之后才能更容易地理解Microservice架构模式 ...

  6. .NET Core微服务之基于Ocelot实现API网关服务

    Tip: 此篇已加入.NET Core微服务基础系列文章索引 一.啥是API网关? API 网关一般放到微服务的最前端,并且要让API 网关变成由应用所发起的每个请求的入口.这样就可以明显的简化客户端 ...

  7. go微服务框架go-micro深度学习(三) Registry服务的注册和发现

    服务的注册与发现是微服务必不可少的功能,这样系统才能有更高的性能,更高的可用性.go-micro框架的服务发现有自己能用的接口Registry.只要实现这个接口就可以定制自己的服务注册和发现. go- ...

  8. Spring Cloud 微服务中搭建 OAuth2.0 认证授权服务

    在使用 Spring Cloud 体系来构建微服务的过程中,用户请求是通过网关(ZUUL 或 Spring APIGateway)以 HTTP 协议来传输信息,API 网关将自己注册为 Eureka ...

  9. 【微服务】使用spring cloud搭建微服务框架,整理学习资料

    写在前面 使用spring cloud搭建微服务框架,是我最近最主要的工作之一,一开始我使用bubbo加zookeeper制作了一个基于dubbo的微服务框架,然后被架构师否了,架构师曰:此物过时.随 ...

随机推荐

  1. uap设置gradle和jdk

  2. SEDA架构实现

    一.SEDA SEDA全称是:stage event driver architecture,中文直译为“分阶段的事件驱动架构”,它旨在结合事件驱动和多线程模式两者的优点,从而做到易扩展,解耦合,高并 ...

  3. Navicat 闲置时间过长会卡死

    前段时间使用navicat连接线上的数据库,Navicat 闲置时间过长会卡死.解决方案:选中数据库,右键点击 编辑连接,修改保持连接间隔为 20秒.非常 so easy ! 1. 选中数据库,右键点 ...

  4. YOLOv4: Darknet 如何于 Docker 编译,及训练 COCO 子集

    YOLO 算法是非常著名的目标检测算法.从其全称 You Only Look Once: Unified, Real-Time Object Detection ,可以看出它的特性: Look Onc ...

  5. 二、loadrunner参数化连接数据库

    2.连接sqlserver数据库.oracle数据库或mysql数据库(只有mysql数据库驱动需要先手动安装) 2.1.新建一个参数,随便设置file还是table类型之类的 2.2.点击Data ...

  6. 内置函数:循环调用函数map和filter

    1.map:循环调用函数,前面一定一定要加list,要不然不会被调用 map的格式:list(map(函数名,循环体)) #这里的函数只能写函数名,不要加() list(map(os.mkdir,[' ...

  7. golang开发:CSP-WaitGroup Mutex

    CSP 是 Communicating Sequential Process 的简称,中文可以叫做通信顺序进程,是一种并发编程模型,最初于Tony Hoare的1977年的论文中被描述,影响了许多编程 ...

  8. [LeetCode]子串的最大出现次数(字符串)

    题目 给你一个字符串 s ,请你返回满足以下条件且出现次数最大的 任意 子串的出现次数: 子串中不同字母的数目必须小于等于 maxLetters . 子串的长度必须大于等于 minSize 且小于等于 ...

  9. Oracle数据库sqlldr工具的使用

    sqlldr导入文本内容到数据库表时,需要指定一个ctl文件(控制文件),通过该文件来完成数据的导入. 1 首先创建一个表student create table student( stu_id nu ...

  10. 数据库:浅谈DML、DDL、DCL的区别

    简介 SQL是一个标准的数据库语言,是面向集合的描述性非过程化语言.它功能强,效率高,简单易学易维护(迄今为止,我还没见过比它还好学的语言).然而SQL语言由于以上优点,同时也出现了这样一个问题:它是 ...