使用 GoFrame V2 高效高质量开发后端项目
gf2-demo 是一个基于 GoFrameV2 用来快速开发后端服务的脚手架, 目标使开发者只需关注业务逻辑的编写, 快速且规范地交付项目.
项目特性
- 优化工程目录结构, 使支持多个可执行命令
- 多环境管理: 开发环境、测试环境、生产环境
- 编译的二进制文件可打印当前应用的版本信息
- 中间件统一拦截响应, 规范响应格式, 规范业务错误码
- 完善 HTTP 服务访问日志、HTTP 服务错误日志、SQL 日志、开发者打印的日志、其他可执行命令的日志配置
- 封装
Redis
常用工具库:rediscache
,redislock
,redismq
,redisdelaymq
,redispubsub
- 通过工具自动生成数据库层、服务接口层、控制器层代码
- 完整的增删改查接口示例和完善的开发流程文档, 帮助开发者快速上手
- 项目部署遵循不可变基础设施原则, 不论是传统单体部署还是容器云部署方式
- 通过
Makefile
管理项目:make run
,make build
,make dao
,make service
等 - 增加
golangci-lint
配置文件.golangci.yml
, 统一团队代码风格, 保障团队代码质量 - 适合个人开发者高质量完成项目, 也适合团队统一后端技术框架, 规范高效管理
快速开始
安装
|
|
请提前安装 Go 环境, 要求 Go 版本:
1.15+
热更新(Live reload)
开发环境下使用.
|
|
默认加载配置文件:
manifest/config/config.yaml
访问测试
|
|
编译二进制文件
|
|
会生成如下二进制文件:
|
|
打印帮助信息
|
|
工程目录
|
|
环境管理
开发环境
配置文件: manifest/config/config.yaml
运行:
make run
或 ./gf2-demo-api
会默认加载配置文件 config.yaml
测试环境
配置文件: manifest/config/config.test.yaml
运行:
- 通过环境变量指定配置文件:
GF_GCFG_FILE=config.test.yaml GF_GERROR_BRIEF=true ./gf2-demo-api
- 通过命令行参数指定配置文件:
./gf2-demo-api -c config.test.yaml
NOTE:
- 通过命令行参数指定配置文件优先于环境变量.
- -c 参数指定的配置文件可以使用绝对路径, 如果不包含路径, 默认依次在如下路径搜索配置文件:
./
(二进制所在的当前目录) >./config/
>./manifest/config/
.GF_GERROR_BRIEF=true
表示 HTTP 服务日志错误堆栈中不包含 gf 框架堆栈.- 配置文件在通过
make build
或make build.cli
编译时已经打包到二进制文件中, 所以在部署时只需部署二进制文件即可.
生产环境
配置文件: manifest/config/config.prod.yaml
运行:
同测试环境, 只不过指定的配置文件不同, 略.
多命令管理
目录设计
举例:
- 命令 1:
cmd/gf2-demo-api/gf2-demo-api.go
->internal/cmd/apiserver/apiserver.go
- 命令 2:
cmd/gf2-demo-cli/gf2-demo-cli.go
->internal/cmd/cli/cli.go
配置文件
默认不同命令在相同环境下使用同一个配置文件, 比如 gf2-demo-api
和 gf2-demo-cli
在开发环境下都使用 manifest/config/config.yaml
作为配置文件.
不过也可以使用各自独立的配置文件, 只需要在运行时通过环境变量或命令行参数指定需要使用的配置文件即可, 比如:
./gf2-demo-cli -c cli_config.yaml
或
GF_GCFG_FILE=cli_config.yaml ./gf2-demo-cli
错误码管理
规范制定
统一响应格式
不论是正确还是错误响应, 响应体都统一使用如下格式:
1 2 3 4 5 6
{ "code": "string", "message": "string", "traceid": "string", "data": null }
💡 响应 header 中已经有了
Trace-Id
了, 为什么响应 json 中还要加一个traceid
?目的是在遇到错误问题进行排查时, 减少不必要的沟通成本, 毕竟很多用户容易忽略响应 header, 在响应体中直接体现
traceid
更直接. 这样在快速拿到用户反馈的traceid
后, 我们就可以很快找到对应的日志从而高效解决问题了.业务码
统一使用字符串表示, 如:"code": "ValidationFailed"
HTTP 状态码
- 正确响应
200
: 成功的响应202
: 部分成功的响应
- 客户端错误
401
: 未通过访问认证403
: 请求的资源未获得授权404
: 请求的资源不存在400
: 其他所有客户端错误, 比如请求参数验证失败等
- 服务端错误
500
: 所有服务端错误
- 正确响应
业务错误码
请在 internal/codes/biz_codes.go
文件中维护业务错误码.
|
|
响应示例
正确响应
1 2 3 4 5 6 7 8 9 10 11 12 13
HTTP/1.1 200 OK Content-Type: application/json Server: GoFrame HTTP Server Trace-Id: 10c9769ce5cf4117c19a595c2d781e94 Date: Wed, 08 Feb 2023 09:38:41 GMT Content-Length: 34 { "code": "OK", "message": "", "traceid": "10c9769ce5cf4117c19a595c2d781e94", "data": null }
401 错误
1 2 3 4 5 6 7 8 9 10 11 12 13
HTTP/1.1 401 Unauthorized Content-Type: application/json Server: GoFrame HTTP Server Trace-Id: a89b7652b1cf41170d6e5233fbb76a21 Date: Wed, 08 Feb 2023 09:34:56 GMT Content-Length: 83 { "code": "AuthFailed", "message": "authentication failed", "traceid": "a89b7652b1cf41170d6e5233fbb76a21", "data": null }
500 错误
1 2 3 4 5 6 7 8 9 10 11 12 13
HTTP/1.1 500 Internal Server Error Content-Type: application/json Server: GoFrame HTTP Server Trace-Id: 70cd58a9d8cf4117376a265eb84137e5 Date: Wed, 08 Feb 2023 09:37:45 GMT Content-Length: 73 { "code": "InternalError", "message": "an error occurred internally", "traceid": "70cd58a9d8cf4117376a265eb84137e5", "data": null }
日志管理
HTTP 服务日志
1. HTTP 服务日志配置
|
|
2. 生成的日志示例
|
|
服务访问日志示例
1 2 3 4 5
# 普通格式 2023-02-08 16:50:51.992 {10fde08349cd4117115968787a401378} {windvalley, windvalley@sre.im} 401 "GET http localhost:9000 /v1/hello HTTP/1.1" 0.004, ::1, "", "PostmanRuntime/7.28.0" # json格式 {"Time":"2023-02-08 16:53:13.118","TraceId":"a8b1bf5f6acd41177931ba72f7411788","CtxStr":"windvalley, windvalley@sre.im","Level":"","Content":"401 \"GET http localhost:9000 /v1/hello HTTP/1.1\" 0.002, ::1, \"\", \"PostmanRuntime/7.28.0\""}
服务错误日志示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
# 普通格式 2023-02-08 16:55:25.984 {2068374f89cd41170d329c50fe5a5fc8} {windvalley, windvalley@sre.im} 401 "GET http localhost:9000 /v1/hello HTTP/1.1" 0.003, ::1, "", "PostmanRuntime/7.28.0", 0, "resource is not authorized", "{Code:NotAuthorized HttpCode:401}" Stack: 1. resource is not authorized: some error 1). gf2-demo/internal/controller.(*cHello).Hello /Users/xg/github/gf2-demo/internal/controller/hello.go:25 2). gf2-demo/internal/logic/middleware.(*sMiddleware).ResponseHandler /Users/xg/github/gf2-demo/internal/logic/middleware/response.go:16 3). gf2-demo/internal/logic/middleware.(*sMiddleware).AccessUser /Users/xg/github/gf2-demo/internal/logic/middleware/accessuser.go:25 4). gf2-demo/internal/logic/middleware.(*sMiddleware).TraceID /Users/xg/github/gf2-demo/internal/logic/middleware/traceid.go:27 2. some error # json格式 {"Time":"2023-02-08 16:54:28.757","TraceId":"18323afc7bcd411710d9f134cc2ec9d5","CtxStr":"windvalley, windvalley@sre.im","Level":"ERRO","Content":"401 \"GET http localhost:9000 /v1/hello HTTP/1.1\" 0.003, ::1, \"\", \"PostmanRuntime/7.28.0\", 0, \"resource is not authorized\", \"{Code:NotAuthorized HttpCode:401}\"\nStack:\n1. resource is not authorized: some error\n 1). gf2-demo/internal/controller.(*cHello).Hello\n /Users/xg/github/gf2-demo/internal/controller/hello.go:25\n 2). gf2-demo/internal/logic/middleware.(*sMiddleware).ResponseHandler\n /Users/xg/github/gf2-demo/internal/logic/middleware/response.go:16\n 3). gf2-demo/internal/logic/middleware.(*sMiddleware).AccessUser\n /Users/xg/github/gf2-demo/internal/logic/middleware/accessuser.go:25\n 4). gf2-demo/internal/logic/middleware.(*sMiddleware).TraceID\n /Users/xg/github/gf2-demo/internal/logic/middleware/traceid.go:27\n2. some error\n"}
SQL 日志
1. SQL 日志配置
|
|
2. 生成的日志示例
|
|
开发者打印的通用日志
1. 通用日志配置
|
|
2. 如何打日志
|
|
3. 生成的日志示例
|
|
链路跟踪
用于链路跟踪的响应 Header 为:
Trace-Id
, 会优先使用客户端传递的请求 HeaderTrace-Id
的值, 如果不存在会自动生成. 为了便于用户查看Trace-Id
, 也在响应 json 中加入了traceid
字段.服务内部如果需要调用其他服务的接口, 请使用
g.Client()
, 因为他会给请求自动注入Trace-Id
, 这样不同 API 服务之间的日志就可以通过Trace-Id
串起来了.
参考: https://goframe.org/pages/viewpage.action?pageId=49745257
版本管理
1. 写版本变更文档
vi CHANGELOG.md
|
|
2. 给项目仓库打 tag
|
|
3. 使用 Makefile 编译
gf 工具配置(
hack/config.yaml
)1 2 3 4 5 6 7 8 9 10 11 12 13 14
gfcli: # doc: https://goframe.org/pages/viewpage.action?pageId=1115788 build: path: "./bin" # 编译生成的二进制文件的存放目录. 生成的二进制名称默认与程序入口go文件同名 arch: "amd64" system: "linux,darwin" packSrc: "manifest/config" # 将项目需要的配置文件打包进二进制, 这样项目部署的时候就可以不用拷贝配置文件了 extra: "" # 编译时的内置变量可以在运行时通过gbuild包获取, 比如: utility/version.go varMap: # NOTE: # 1) `version` was generated by `make build`, Do Not Edit # 2) you should manage versions by `git tag vX.X.X` version: v0.3.0
编译
1 2 3 4 5
# For gf2-demo-api make build # For gf2-demo-cli make build.cli
4. 查看二进制文件版本信息
|
|
Redis
Redis 配置
|
|
Redis 工具库
- Redis 缓存:
internal/pkg/rediscache
- Redis 分布式锁:
internal/pkg/redislock
- Redis 消息队列:
internal/pkg/redismq
- Redis 延迟队列:
internal/pkg/redisdelaymq
- Redis 发布订阅:
internal/pkg/redispubsub
使用方法可参考代码或每个包下面的 test 文件.
开发流程(重点)
1. 设计表结构, 创建物理表
设计表结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
-- manifest/sql/gf2_demo.sql -- Create demo database CREATE DATABASE IF NOT EXISTS `gf2_demo`; USE `gf2_demo`; -- Create demo table DROP TABLE IF EXISTS `demo`; CREATE TABLE `demo` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID', `fielda` varchar(45) NOT NULL COMMENT 'Field demo', `fieldb` varchar(45) NOT NULL COMMENT 'Private field demo', `created_at` datetime DEFAULT NULL COMMENT 'Created Time', `updated_at` datetime DEFAULT NULL COMMENT 'Updated Time', PRIMARY KEY (`id`), UNIQUE KEY `idx_fielda` (`fielda`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
创建物理表
1
$ mysql -uroot -p'123456' < manifest/sql/demo.sql
2. 自动生成数据层相关代码
gf 工具配置
1 2 3 4 5 6 7 8 9 10 11
# hack/config.yaml gfcli: gen: # doc: https://goframe.org/pages/viewpage.action?pageId=3673173 dao: - link: "mysql:root:123456@tcp(127.0.0.1:3306)/gf2_demo" tables: "" # 指定当前数据库中需要执行代码生成的数据表, 多个以逗号分隔. 如果为空, 表示数据库的所有表都会生成. 默认为空 descriptionTag: true # 用于指定是否为数据模型结构体属性增加desription的标签, 内容为对应的数据表字段注释. 默认 false noModelComment: true # 用于指定是否关闭数据模型结构体属性的注释自动生成, 内容为数据表对应字段的注释. 默认 false jsonCase: "snake" # 指定model中生成的数据实体对象中json标签名称规则. 默认 CamelLower clear: true # 自动删除数据库中不存在对应数据表的本地dao/do/entity代码文件, 默认 false. 线上环境应设置为fasle
自动生成
internal/dao
,internal/model/do
,internal/model/entity
1
$ make dao
3. 编写 api 层代码
位置: api/demo/v1/demo.go
定义业务侧数据结构, 提供对外接口的输入/输出数据结构, 定义访问路由 path, 请求方法, 数据校验, api 文档等.
注意: 目录结构必须遵守这个模式规范 api/模块名称/版本号/模块名称.go
示例:
|
|
编写规范请参考文档: https://goframe.org/pages/viewpage.action?pageId=93880327
4. 自动生成 controller 层框架代码
编写完 api 定义代码(api/demo/v1/demo.go
)后, 在项目根目录执行如下命令行:
|
|
该命令行会根据开发者编写的 api/demo/v1/demo.go
api 定义文件自动生成:
api interface 文件
api/demo/demo.go
controller 层代码
1 2 3 4 5 6
├── internal │ ├── controller │ │ └── demo │ │ ├── demo.go │ │ ├── demo_new.go # 不可变更 │ │ ├── demo_v1_create.go # 我们只需要在这里填充controller的具体实现
5. 编写 model 层代码
位置: internal/model/
定义数据侧数据结构,提供对内的数据处理的输入/输出数据结构.
在 GoFrame 框架规范中, 这部分输入输出模型名称以 XxxInput
和 XxxOutput
格式命名, 需要在 internal/model
目录下创建文件.
示例:
|
|
参考: https://goframe.org/pages/viewpage.action?pageId=7295964
6. 编写 service 层代码
a. 编写具体的业务实现(internal/logic/
)
调用数据访问层(internal/dao/
), 编写具体的业务逻辑. 这里是业务逻辑的重心, 绝大部分的业务逻辑都应该在这里编写.
示例:
|
|
b. 自动生成 service 接口代码(internal/service/
)
|
|
c. 将业务实现注入到服务接口(依赖注入)
示例:
|
|
d. 程序启动时自动注册服务
在程序入口文件 cmd/gf2-demo-api/gf2-demo-api.go
中导入 logic 包.
示例:
|
|
参考: https://goframe.org/pages/viewpage.action?pageId=49770772
7. 编写 controller 层代码
位置: internal/controller/
controller 代码文件前面已经通过make ctrl
自动生成了, 我们只需要在适当的位置填充具体实现即可.
具体实现如何编写:
解析 api 层(api/demo/v1/demo.go
)定义的业务侧用户输入数据结构, 组装为 model 层(internal/model/
)定义的数据侧输入数据结构实例, 调用 internal/service/
层的服务, 最后直接将结果或错误 return 即可(响应中间件会统一拦截处理, 按规范响应用户).
示例:
|
|
8. 路由注册
位置: internal/cmd/apiserver/apiserver.go
路由注册, 调用 controller 层(internal/controller/
), 对外暴露接口.
示例:
|
|
9. 接口访问测试
|
|
代码质量
统一团队代码风格, 保障团队代码质量.
Github: https://github.com/golangci/golangci-lint
Documentation: https://golangci-lint.run
安装 golangci-lint
|
|
或
|
|
使用方法
命令行执行
1 2 3 4 5 6 7 8 9 10 11 12 13
# 在仓库根路径执行, 检测仓库内所有Go代码 $ golangci-lint run # 或 $ make lint # 查看所有linters的功能介绍 $ golangci-lint help linters # 查看 .golangci.yml 已启用的 linters $ golangci-lint linters # 只使用某一个linter来检查代码 $ golangci-lint run --no-config --disable-all -E errcheck
集成到编辑器或 IDE
请参考官方文档:
https://golangci-lint.run/usage/integrations/
强烈建议使用此种方式, 可实时提示代码存在的问题, 而不是等到编译的时候才知道哪里出错了, 不但提高代码质量, 还能提高编码效率.
项目部署
Systemctl
相关的配置文件及脚本
- 生产环境 systemctl 服务配置文件:
manifest/deploy/systemctl/gf2-demo-api.service
- 测试环境 systemctl 服务配置文件:
manifest/deploy/systemctl/gf2-demo-api_test.service
- 部署脚本:
manifest/deploy/systemctl/deploy.sh
- 生产环境 systemctl 服务配置文件:
设置目标服务器(修改
deploy.sh
脚本)1 2 3 4 5 6
# 目标服务器, 请提前配置发布机到目标服务器之间的ssh key信任 deploy_server="gf2-demo.sre.im" # 用于连接目标服务器的用户名 deploy_user="vagrant" # 项目部署目录 deploy_dir=/app/$project_name
执行部署
1 2 3 4 5
# 部署测试环境 $ ./manifest/deploy/systemctl/deploy.sh test # 部署生产环境 $ ./manifest/deploy/systemctl/deploy.sh prod
验证
首先登录到目标服务器.
1 2 3 4 5 6 7 8 9 10 11 12 13 14
# 默认项目的所有标准输出和标准错误输出都会在此文件中. $ tail -f /app/gf2-demo/gf2-demo-api.log # 项目常规日志, 包括通过g.Log()打印的日志. $ tail -f /app/gf2-demo/logs/2023-02-15.log # 项目HTTP服务访问日志 $ tail -f /app/gf2-demo/logs/access-20230215.log # 项目HTTP服务错误日志 $ tail -f /app/gf2-demo/logs/error-20230215.log # sql debug 日志 $ tail -f /app/gf2-demo/logs/sql-20230215.log
systemctl 常用命令
1 2 3 4 5 6 7 8 9 10 11 12
# gf2-demo-api.service 配置有变动的时候, 需要重新加载使生效 $ sudo systemctl daemon-reload # 启动 $ sudo systemctl start gf2-demo-api # 关闭: 发送 SIGTERM 信号给主(sh)和子进程(gf2-demo-api), # gf2-demo-api程序可通过捕获SIGTERM信号来实现优雅关闭. $ sudo systemctl stop gf2-demo-api # 重启: 先关闭(SIGTERM), 再启动 $ sudo systemctl restart gf2-demo-api
NOTE:
- 此示例为单台部署, 若部署集群可使用
gossh
、ansible
等工具.- 服务器操作系统:
CentOS7.x
, 其他系统类型未验证.
Supervisor
相关的配置文件及脚本
- 生产环境 supervisor 配置文件:
manifest/deploy/supervisor/gf2-demo-api.ini
- 测试环境 supervisor 服务配置文件:
manifest/deploy/supervisor/gf2-demo-api_test.ini
- 部署脚本:
manifest/deploy/supervisor/deploy.sh
- 生产环境 supervisor 配置文件:
设置目标服务器(修改
deploy.sh
脚本)1 2 3 4 5 6
# 目标服务器, 请提前配置发布机到目标服务器之间的ssh key信任 deploy_server="gf2-demo.sre.im" # 用于连接目标服务器的用户名 deploy_user="vagrant" # 项目部署目录 deploy_dir=/app/$project_name
在目标服务器上提前安装 supervisor
基于 CentOS7 系统演示.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
yum update -y yum install epel-release -y yum install supervisor -y systemctl enable supervisord systemctl start supervisord systemctl status supervisord echo_supervisord_conf > /etc/supervisord.conf cat >> /etc/supervisord.conf <<EOF [include] files = supervisord.d/*.ini EOF mkdir -p /etc/supervisord.d
执行部署
1 2 3 4 5
# 部署测试环境 $ ./manifest/deploy/supervisor/deploy.sh test # 部署生产环境 $ ./manifest/deploy/supervisor/deploy.sh prod
supervisorctl 常用命令
1 2 3 4 5 6 7 8 9 10 11 12 13
# 启动 $ sudo supervisorctl start gf2-demo-api # 关闭(SIGTERM信号), 可捕获SIGTERM信号, 实现优雅关闭 $ sudo supervisorctl stop gf2-demo-api # 重启: 先关闭(SIGTERM信号), 再启动. # NOTE: /etc/supervisord.*相关配置有变动, 重启具体某服务并不会生效 $ sudo supervisorctl restart gf2-demo-api # 重启 supervisor 控制的所有服务. # NOTE: 当 /etc/supervisord.*相关配置有变动, 必须执行此命令才能加载生效 $ sudo supervisorctl reload
Docker
Dockerfile
采用两阶段构建, 镜像体积小; 将依赖库下载剥离出来并且前置, 利用缓存特性提高编译速度.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
# syntax=docker/dockerfile:1 # Step 1: build binary FROM golang:1.17 as builder ENV GOPROXY https://goproxy.cn,direct WORKDIR /src # pre-copy/cache go.mod for pre-downloading dependencies and # only redownloading them in subsequent builds if they change COPY Makefile ./ RUN make cli COPY go.mod go.sum ./ RUN go mod download && go mod verify COPY . . RUN make build OS="linux" # Step 2: copy binary from step 1 FROM loads/alpine:3.8 ENV GF_GERROR_BRIEF=true WORKDIR /app COPY --from=builder /src/bin/linux_amd64/gf2-demo-api . EXPOSE 9000 ENTRYPOINT [ "./gf2-demo-api" ]
制作容器镜像
1 2 3 4 5 6 7 8
$ cd gf2-demo $ make image $ docker image ls REPOSITORY TAG IMAGE ID CREATED SIZE gf2-demo-api 20230221113306.0d26121.dirty 58e6953c2e1b 15 seconds ago 30.1MB
运行容器
1 2 3 4 5 6 7 8
# 开发环境 $ docker run --name gf2-demo -p80:9000 -d gf2-demo-api:20230221113306.0d26121.dirty # 测试环境 $ docker run --name gf2-demo -p80:9000 -e GF_GCFG_FILE=config.test.yaml -d gf2-demo-api:20230221113306.0d26121.dirty # 生产环境 $ docker run --name gf2-demo -p80:9000 -e GF_GCFG_FILE=config.prod.yaml -d gf2-demo-api:20230221113306.0d26121.dirty
验证
查看是否成功运行:
浏览器访问http://localhost/swagger
, 参看 api 文档是否正常展示.查看二进制应用版本信息
1 2 3 4 5 6 7 8
$ docker exec -it gf2-demo ./gf2-demo-api -v # 输出如下: App Version: v0.7.0 Git Commit: 2023-02-17 19:32:05 95390e39485aa29050c2327c263a732267ec3eb3 Build Time: 2023-02-20 06:18:57 Go Version: go1.17.13 GF Version: v2.3.2
查看不同环境下, 程序使用的配置文件是否正确
1 2 3 4 5 6 7 8
# 查看容器输出的日志 $ docker logs gf2-demo # 如果配置了日志保存到文件, 也可登录到容器内部进行查看. $ docker exec -it gf2-demo sh # 输出的部分日志截取: 2023-02-17 18:52:36.568 [DEBU] {7f0f8d5a279744179740f477f49fbd06} /Users/xg/github/gf2-demo/internal/cmd/apiserver/apiserver.go:79: use config file: &{defaultName:config searchPaths:0xc0000bf6e0 jsonMap:0xc000303720 violenceCheck:false}
上面日志中的
defaultName
如果为config
, 代表开发环境; 为config.test.yaml
, 代表测试环境; 为config.prod.yaml
, 代表生产环境.
如何优雅关闭
1 2 3 4 5 6 7 8 9 10 11
# 关闭: 会发送SIGTERM信号, gf2-demo捕获该信号经过处理, 可实现优雅关闭 $ docker stop gf2-demo # 重启: 先关闭(SIGTERM信号), 再启动, 可实现优雅关闭 $ docker restart gf2-demo # 强制关闭(SIGKILL信号), gf2-demo无法捕获到SIGKILL信号, 直接退出 $ docker kill gf2-demo # 强制关闭并删除容器(SIGKILL信号) $ docker rm -f gf2-demo
优雅关闭测试
GoFrame
从 V2.4.0
版本开始已支持捕获SIGTERM
信号来实现优雅关闭服务. 以上三种部署方式(Systemctl
/Supervisor
/Docker
)在关闭或重启服务的时候均是发送SIGTERM
信号给服务进程, 所以都能优雅关闭服务.
开始测试, 准备至少 3 个 terminal 窗口.
模拟接口响应延时 8 秒
测试接口:
GET localhost:9000/v1/demo/:fielda
1 2 3 4 5 6 7 8 9 10
// internal/controller/demo.go func (c *cDemo) Get(ctx context.Context, req *v1.DemoGetReq) (*v1.DemoGetRes, error) { // 加入此行代码, 模拟延迟 time.Sleep(8 * time.Second) demoInfo, err := service.Demo().Get(ctx, req.Fielda) if err != nil { return nil, err }
配置文件中的
server.gracefulShutdownTimeout
调整为 10 秒1 2
server: gracefulShutdownTimeout: 10 # 默认 5秒, 建议根据实际业务情况调整
在窗口 1 启动服务
1
$ make run
在输出日志中找到服务 PID 为 80273, 在步骤 5 会用到:
1
2023-04-25 11:31:16.716 [INFO] pid[80273]: http server started listening on [:9000]
在窗口 2 模拟请求
1
$ curl localhost:9000/v1/demo/windvalley
请求卡住, 在等待响应中.
在窗口 3 模拟优雅关闭服务
执行完第 4 步后, 立即执行如下命令:
1
$ kill -SYSTERM 80273
窗口 1 输出:
1
2023-04-25 11:41:52.004 80273: server gracefully shutting down by signal: terminated
此时, 并没有马上关闭服务, 在等待请求处理完成. 几秒钟后, 请求处理完成, 服务关闭, 窗口 1 继续输出:
1 2 3
2023-04-25 11:41:57.863 [DEBU] {708a5f7a8710591764de0d572ec0fc19} [ 1 ms] [default] [gf2_demo] [rows:1 ] SELECT * FROM `demo` WHERE `fielda`='windvalley' LIMIT 1 2023-04-25 11:41:57.863 {708a5f7a8710591764de0d572ec0fc19} 200 "GET http localhost:9000 /v1/demo/windvalley HTTP/1.1" 8.005, 127.0.0.1, "", "curl/7.79.1" 2023-04-25 11:41:58.275 [INFO] pid[80273]: all servers shutdown
此时窗口 2 输出:
1
{"code":"OK","message":"","traceid":"708a5f7a8710591764de0d572ec0fc19","data":{"id":14,"fielda":"windvalley","created_at":"2023-02-14 17:12:59","updated_at":"2023-02-14 17:12:59"}}
请求成功.
NOTE:
虽然
GoFrame
支持优雅重启特性, 但在生产环境下不建议开启:
Systemctl
或Supervisor
无法很好的接管和控制父子进程. 优雅关闭特性已足够, 可通过部署负载均衡器(LVS 等)来实现不中断服务.对于
Docker
/K8S
等容器化场景, 最佳实践要求进程和容器本身同生命周期, 更不能开启优雅重启特性. 容器管理平台本身支持容器的平滑启停, 实现不中断服务.
使用 Makefile 管理项目
|
|
使用示例:
|
|
NOTE: 如果是 macOS 系统, 需要提前安装
gsed
命令.
变更项目名称
请按如下步骤便捷地将本项目名称 gf2-demo
改成你自己的项目名称 new-project
.
变更项目目录名称
1
$ mv gf2-demo new-project
运行变更脚本
1 2
$ cd new-project $ hack/change_project_name.sh new-project
NOTE: 如果是 macOS 系统, 需要提前安装
gsed
命令.验证
1 2 3 4 5 6 7 8 9
$ make build 输出如下: bin ├── darwin_amd64 │ └── new-project-api └── linux_amd64 └── new-project-api