共计 8253 个字符,预计需要花费 21 分钟才能阅读完成。
0. 为什么说做好微服务很难?
要想做好微服务,咱们须要了解和把握的知识点十分多,从几个维度上来说:
-
基本功能层面
- 并发管制 & 限流,防止服务被突发流量击垮
- 服务注册与服务发现,确保可能动静侦测增减的节点
- 负载平衡,须要依据节点承受能力散发流量
- 超时管制,防止对已超时申请做无用功
- 熔断设计,疾速失败,保障故障节点的恢复能力
-
高阶性能层面
- 申请认证,确保每个用户只能拜访本人的数据
- 链路追踪,用于了解整个零碎和疾速定位特定申请的问题
- 日志,用于数据收集和问题定位
- 可观测性,没有度量就没有优化
对于其中每一点,咱们都须要用很长的篇幅来讲述其原理和实现,那么对咱们后端开发者来说,要想把这些知识点都把握并落实到业务零碎里,难度是十分大的,不过咱们能够依赖曾经被大流量验证过的框架体系。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:
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
// 手动代码完结
}
通过调用 `adder` 的 `Add` 办法实现增加图书到 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
// 手动代码完结
}
通过调用 `checker` 的 `Check` 办法实现从 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.yaml` 和 `rpc/check/etc/check.yaml`,减少如下内容:
DataSource: root:@tcp(localhost:3306)/gozero
Table: book
Cache:
- Host: localhost:6379
能够应用多个 redis 作为 cache,反对 redis 单点或者 redis 集群
* 批改 `rpc/add/internal/config.go` 和 `rpc/check/internal/config.go`,如下:
type Config struct {
rpcx.RpcServerConf
DataSource string // 手动代码
Table string // 手动代码
Cache cache.CacheConf // 手动代码
}
减少了 mysql 和 redis cache 配置
* 批改 `rpc/add/internal/svc/servicecontext.go` 和 `rpc/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](/img/bVcHmZI)
能够看出在我的 MacBook Pro 上能达到 3 万 + 的 qps。## 13. 残缺代码
[https://github.com/tal-tech/go-zero/tree/master/example/bookstore](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](https://github.com/tal-tech/go-zero)
## 16. 微信交换群
增加我的微信:kevwan,请注明 go-zero,我拉进 go-zero 社区群????