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. 3 path核心模块

    const path = require('path') // require('./static/test/test') { /* 总结: __dirname: 获得当前执行文件所在目录的完整目录名 ...

  2. 17_Python的常用模块

    1.随机数模块 random 1.随机小数 import random # (0,1)随机取浮点数 random.random() # 0.17988578778011 # (1, 3)取指定范围的浮 ...

  3. RGB打水印在YUV图片上

    一. 概述 将RGB图片打在YUV上需要注意的是, 字体之外应该透明, 否则背景也会被覆盖不好看,  所以RGB必须有透明度,  本测试格式为BMP ARGB8888(也即B是最低字节, A是最高字节 ...

  4. Solon详解(六)- Solon的校验扩展框架使用与扩展

    Solon详解系列文章: Solon详解(一)- 快速入门 Solon详解(二)- Solon的核心 Solon详解(三)- Solon的web开发 Solon详解(四)- Solon的事务传播机制 ...

  5. C++STL中vector的初始化

    vector的初始化有很多方式,在N维初始化时还会一些容易出现错误的地方.下面进行总结 以下的总结均以int作为模板参数 一维vector的初始化 vector的构造函数通常来说有五种,如下: vec ...

  6. Java线程本质

    java当中的线程和操作系统的线程是什么关系? 关于操作系统的线程 linux操作系统的线程控制原语 int pthread create(pthread t *thread, const pthre ...

  7. Java实现打开google浏览器

    说明: 博主的Google浏览器版本:75.0.3770.142,如果运行异常,需要自行查找对应版本的驱动(chromedriver.exe) 需要的jar包: https://pan.baidu.c ...

  8. Shader 001 - 函数造型能力

    0x00 从函数出发 Shader 中的很多效果都是由函数计算得出的,如何更好地理解二者的关系呢.不妨先看看函数是什么?函数的定义可以简单地描述为:给定一个集合 A,对于其中的元素施加法则 f,则可以 ...

  9. tomcat源码之概述

    tomcat架构及常用的组件如下: Server Server代表了tomcat服务器,Tomcat启动时即会启动一个server实例,它监听在8005端口以接收shutdown命令,使用 telne ...

  10. 3.ConcurrentMap-并发Map