-
Notifications
You must be signed in to change notification settings - Fork 9
/
search_plus_index.json
1 lines (1 loc) · 356 KB
/
search_plus_index.json
1
{"./":{"url":"./","title":"简介","keywords":"","body":" go-zero 缩短从需求到上线的距离 go-zero 是一个集成了各种工程实践的 web 和 rpc 框架。通过弹性设计保障了大并发服务端的稳定性,经受了充分的实战检验。 go-zero 包含极简的 API 定义和生成工具 goctl,可以根据定义的 api 文件一键生成 Go, iOS, Android, Kotlin, Dart, TypeScript, JavaScript 代码,并可直接运行。 使用 go-zero 的好处: 轻松获得支撑千万日活服务的稳定性 内建级联超时控制、限流、自适应熔断、自适应降载等微服务治理能力,无需配置和额外代码 微服务治理中间件可无缝集成到其它现有框架使用 极简的 API 描述,一键生成各端代码 自动校验客户端请求参数合法性 大量微服务治理和并发工具包 1. go-zero 框架背景 18 年初,我们决定从 Java+MongoDB 的单体架构迁移到微服务架构,经过仔细思考和对比,我们决定: 基于 Go 语言 高效的性能 简洁的语法 广泛验证的工程效率 极致的部署体验 极低的服务端资源成本 自研微服务框架 有过很多微服务框架自研经验 需要有更快速的问题定位能力 更便捷的增加新特性 2. go-zero 框架设计思考 对于微服务框架的设计,我们期望保障微服务稳定性的同时,也要特别注重研发效率。所以设计之初,我们就有如下一些准则: 保持简单,第一原则 弹性设计,面向故障编程 工具大于约定和文档 高可用 高并发 易扩展 对业务开发友好,封装复杂度 约束做一件事只有一种方式 我们经历不到半年时间,彻底完成了从 Java+MongoDB 到 Golang+MySQL 为主的微服务体系迁移,并于 18 年 8 月底完全上线,稳定保障了业务后续迅速增长,确保了整个服务的高可用。 3. go-zero 项目实现和特点 go-zero 是一个集成了各种工程实践的包含 web 和 rpc 框架,有如下主要特点: 强大的工具支持,尽可能少的代码编写 极简的接口 完全兼容 net/http 支持中间件,方便扩展 高性能 面向故障编程,弹性设计 内建服务发现、负载均衡 内建限流、熔断、降载,且自动触发,自动恢复 API 参数自动校验 超时级联控制 自动缓存控制 链路跟踪、统计报警等 高并发支撑,稳定保障了疫情期间每天的流量洪峰 如下图,我们从多个层面保障了整体服务的高可用: 觉得不错的话,别忘 star 👏 4. Installation 在项目目录下通过如下命令安装: GO111MODULE=on GOPROXY=https://goproxy.cn/,direct go get -u github.com/zeromicro/go-zero 5. Quick Start 完整示例请查看 快速构建高并发微服务 快速构建高并发微服务 - 多 RPC 版 安装 goctl 工具 goctl 读作 go control,不要读成 go C-T-L。goctl 的意思是不要被代码控制,而是要去控制它。其中的 go 不是指 golang。在设计 goctl 之初,我就希望通过 她 来解放我们的双手👈 GO111MODULE=on GOPROXY=https://goproxy.cn/,direct go get -u github.com/zeromicro/go-zero/tools/goctl 如果使用 go1.16 版本, 可以使用 go install 命令安装 GOPROXY=https://goproxy.cn/,direct go install github.com/zeromicro/go-zero/tools/goctl@latest 确保 goctl 可执行 快速生成 api 服务 goctl api new greet cd greet go mod init go mod tidy go run greet.go -f etc/greet-api.yaml 默认侦听在 8888 端口(可以在配置文件里修改),可以通过 curl 请求: curl -i http://localhost:8888/from/you 返回如下: HTTP/1.1 200 OK Content-Type: application/json Date: Thu, 22 Oct 2020 14:03:18 GMT Content-Length: 14 {\"message\":\"\"} 编写业务代码: api 文件定义了服务对外暴露的路由,可参考 api 规范 可以在 servicecontext.go 里面传递依赖给 logic,比如 mysql, redis 等 在 api 定义的 get/post/put/delete 等请求对应的 logic 里增加业务处理逻辑 可以根据 api 文件生成前端需要的 Java, TypeScript, Dart, JavaScript 代码 goctl api java -api greet.api -dir greet goctl api dart -api greet.api -dir greet ... 6. Benchmark 测试代码见这里 7. 文档 API 文档 goctl 使用帮助 awesome 系列(更多文章见『微服务实践』公众号) 快速构建高并发微服务 快速构建高并发微服务 - 多 RPC 版 精选 goctl 插件 插件 用途 goctl-swagger 一键生成 api 的 swagger 文档 goctl-android 生成 java (android) 端 http client 请求代码 goctl-go-compact 合并 api 里同一个 group 里的 handler 到一个 go 文件 8. 微信公众号 go-zero 相关文章都会在 微服务实践 公众号整理呈现,欢迎扫码关注,也可以通过公众号私信我 👏 9. 微信交流群 如果文档中未能覆盖的任何疑问,欢迎您在群里提出,我们会尽快答复。 您可以在群内提出使用中需要改进的地方,我们会考虑合理性并尽快修改。 如果您发现 bug 请及时提 issue,我们会尽快确认并修改。 为了防止广告用户、识别技术同行,请 star 后加我时注明 github 当前 star 数,我再拉进 go-zero 群,感谢! 加我之前有劳点一下 star,一个小小的 star 是作者们回答海量问题的动力🤝 Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:49 "},"about-us.html":{"url":"about-us.html","title":"关于我们","keywords":"","body":"关于我们 go-zero go-zero 是一个集成了各种工程实践的 web 和 rpc 框架。通过弹性设计保障了大并发服务端的稳定性,经受了充分的实战检验。 go-zero 包含极简的 API 定义和生成工具 goctl,可以根据定义的 api 文件一键生成 Go, iOS, Android, Kotlin, Dart, TypeScript, JavaScript 代码,并可直接运行。 go-zero作者 万俊峰,七牛云技术副总裁,拥有14年研发团队管理经验,16年架构设计经验,20年工程实战经验,负责过多个大型项目的架构设计,曾多次合伙创业(被收购),阿里云MVP,ArchSummit全球架构师峰会明星讲师,GopherChina大会主持人 & 金牌讲师,QCon+ Go语言出品人兼讲师,腾讯云开发者大会讲师。 go-zero社区 我们目前拥有7000多人的社区成员,在这里,你可以和大家讨论任何关于go-zero的技术,问题反馈,获取最新的go-zero信息,以及各位大佬每天分享的技术心得。 go-zero社区群 Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:49 "},"join-us.html":{"url":"join-us.html","title":"加入我们","keywords":"","body":"加入我们 概要 go-zero 是一个基于MIT License 的开源项目,大家在使用中发现bug,有新的特性等,均可以参与到go-zero的贡献中来,我们非常欢迎大家的积极参与,也会最快响应大家提出的各种问题,pr等。 贡献形式 Pull Request Issue 贡献须知 go-zero 的Pull request中的代码需要满足一定规范 命名规范,请阅读命名规范 以英文注释为主 pr时备注好功能特性,描述需要清晰,简洁 增加单元测试覆盖率达80%+ 贡献代码(pr) 进入go-zero 项目,fork一份go-zero 项目到自己的github仓库中。 回到自己的github主页,找到xx/go-zero项目,其中xx为你的用户名,如anqiansong/go-zero 克隆代码到本地 开发代码,push到自己的github仓库 进入自己的github中go-zero项目,点击浮层上的的【Pull requests】进入Compare页面。 base repository选择zeromicro/go-zero base:master,head repository选择xx/go-zero compare:$branch ,$branch为你开发的分支,如图: 点击【Create pull request】即可实现pr申请 确认pr是否提交成功,进入go-zero 的Pull requests 查看,应该有自己提交的记录,名称为你的开发时的分支名称 Issue 在我们的社区中,有很多伙伴会积极的反馈一些go-zero使用过程中遇到的问题,由于社区人数较多,我们虽然会实时的关注社区动态,但大家问题反馈过来都是随机的,当我们团队还在解决某一个伙伴提出的问题时,另外的问题也反馈上来,可能会导致团队会很容易忽略掉,为了能够一一的解决大家的问题,我们强烈建议大家通过issue的方式来反馈问题,包括但不限于bug,期望的新功能特性等,我们在实现某一个新特性时也会在issue中体现,大家在这里也能够在这里获取到go-zero的最新动向,也欢迎大家来积极的参与讨论。 怎么提Issue 点击这里 进入go-zero的Issue页面或者直接访问https://github.com/zeromicro/go-zero/issues 地址 点击右上角的【New issue】新建issue 填写issue标题和内容 点击【Submit new issue】提交issue 参考文档 Github Pull request Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:49 "},"concept-introduction.html":{"url":"concept-introduction.html","title":"概念介绍","keywords":"","body":"概念介绍 go-zero 晓黑板golang开源项目,集各种工程实践于一身的web和rpc框架。 goctl 一个旨在为开发人员提高工程效率、降低出错率的辅助工具。 goctl插件 指以goctl为中心的周边二进制资源,能够满足一些个性化的代码生成需求,如路由合并插件goctl-go-compact插件, 生成swagger文档的goctl-swagger插件,生成php调用端的goctl-php插件等。 intellij/vscode插件 在intellij系列产品上配合goctl开发的插件,其将goctl命令行操作使用UI进行替代。 api文件 api文件是指用于定义和描述api服务的文本文件,其以.api后缀结尾,包含api语法描述内容。 goctl环境 goctl环境是使用goctl前的准备环境,包含 golang环境 protoc protoc-gen-go插件 go module | gopath go-zero-demo go-zero-demo里面包含了文档中所有源码的一个大仓库,后续我们在编写演示demo时,我们均在此项目下创建子项目, 因此我们需要提前创建一个大仓库go-zero-demo,我这里把这个仓库放在home目录下。 $ cd ~ $ mkdir go-zero-demo&&cd go-zero-demo $ go mod init go-zero-demo 参考文档 go-zero Goctl 插件中心 工具中心 api语法 Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:49 "},"quick-start.html":{"url":"quick-start.html","title":"快速开发","keywords":"","body":"快速开发 本节主要通过对 api/rpc 等服务快速开始来让大家对使用 go-zero 开发的工程有一个宏观概念,更加详细的介绍我们将在后续一一展开。如果您已经参考 准备工作 做好环境及工具的准备,请跟随以下小节开始体验: 单体服务 微服务 Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:49 "},"monolithic-service.html":{"url":"monolithic-service.html","title":"单体服务","keywords":"","body":"单体服务 前言 由于go-zero集成了web/rpc于一体,社区有部分小伙伴会问我,go-zero的定位是否是一款微服务框架,答案是不止于此, go-zero虽然集众多功能于一身,但你可以将其中任何一个功能独立出来去单独使用,也可以开发单体服务, 不是说每个服务上来就一定要采用微服务的架构的设计,这点大家可以看看作者(kevin)的第四期开源说 ,其中对此有详细的讲解。 创建greet服务 $ mkdir go-zero-demo $ cd go-zero-demo $ go mod init go-zero-demo $ goctl api new greet $ go mod tidy Done. 说明:如无 cd 改变目录的操作,所有操作均在 go-zero-demo 目录执行 查看一下greet服务的目录结构 $ tree greet greet ├── etc │ └── greet-api.yaml ├── greet.api ├── greet.go └── internal ├── config │ └── config.go ├── handler │ ├── greethandler.go │ └── routes.go ├── logic │ └── greetlogic.go ├── svc │ └── servicecontext.go └── types └── types.go 由以上目录结构可以观察到,greet服务虽小,但\"五脏俱全\"。接下来我们就可以在greetlogic.go中编写业务代码了。 编写逻辑 $ vim greet/internal/logic/greetlogic.go func (l *GreetLogic) Greet(req *types.Request) (*types.Response, error) { return &types.Response{ Message: \"Hello go-zero\", }, nil } 启动并访问服务 启动服务 $ cd greet $ go run greet.go -f etc/greet-api.yaml 输出如下,服务启动并侦听在8888端口: Starting server at 0.0.0.0:8888... 访问服务 $ curl -i -X GET http://localhost:8888/from/you 返回如下: HTTP/1.1 200 OK Content-Type: application/json Date: Sun, 07 Feb 2021 04:31:25 GMT Content-Length: 27 {\"message\":\"Hello go-zero\"} 源码 greet源码 猜你想看 goctl使用说明 api目录结构介绍 api语法 api配置文件介绍 api中间件使用 Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:49 "},"micro-service.html":{"url":"micro-service.html","title":"微服务","keywords":"","body":"微服务 在上一篇我们已经演示了怎样快速创建一个单体服务,接下来我们来演示一下如何快速创建微服务, 在本小节中,api部分其实和单体服务的创建逻辑是一样的,只是在单体服务中没有服务间的通讯而已, 且微服务中api服务会多一些rpc调用的配置。 前言 本小节将以一个订单服务调用用户服务来简单演示一下,演示代码仅传递思路,其中有些环节不会一一列举。 情景提要 假设我们在开发一个商城项目,而开发者小明负责用户模块(user)和订单模块(order)的开发,我们姑且将这两个模块拆分成两个微服务① [注意] ①:微服务的拆分也是一门学问,这里我们就不讨论怎么去拆分微服务的细节了。 演示功能目标 订单服务(order)提供一个查询接口 用户服务(user)提供一个方法供订单服务获取用户信息 服务设计分析 根据情景提要我们可以得知,订单是直接面向用户,通过http协议访问数据,而订单内部需要获取用户的一些基础数据,既然我们的服务是采用微服务的架构设计, 那么两个服务(user, order)就必须要进行数据交换,服务间的数据交换即服务间的通讯,到了这里,采用合理的通讯协议也是一个开发人员需要 考虑的事情,可以通过http,rpc等方式来进行通讯,这里我们选择rpc来实现服务间的通讯,相信这里我已经对\"rpc服务存在有什么作用?\"已经作了一个比较好的场景描述。 当然,一个服务开发前远不止这点设计分析,我们这里就不详细描述了。从上文得知,我们需要一个 user rpc order api 两个服务来初步实现这个小demo。 创建mall工程 如果你跑了单体的示例,里面也叫 go-zero-demo,你可能需要换一个父目录。 $ mkdir go-zero-demo $ cd go-zero-demo $ go mod init go-zero-demo 说明:如无 cd 改变目录的操作,所有操作均在 go-zero-demo 目录执行 创建user rpc服务 创建user rpc服务 $ mkdir -p mall/user/rpc 添加user.proto文件,增加getUser方法 $ vim mall/user/rpc/user.proto 增加如下代码: syntax = \"proto3\"; package user; // protoc-gen-go 版本大于1.4.0, proto文件需要加上go_package,否则无法生成 option go_package = \"./user\"; message IdRequest { string id = 1; } message UserResponse { // 用户id string id = 1; // 用户名称 string name = 2; // 用户性别 string gender = 3; } service User { rpc getUser(IdRequest) returns(UserResponse); } 生成代码 如未安装 protoc,protoc-gen-go,protoc-gen-grpc-go 你可以通过如下指令一键安装: $ goctl env check -i -f 注意: 1、每一个 *.proto文件只允许有一个service error: only one service expected $ cd mall/user/rpc $ goctl rpc protoc user.proto --go_out=./types --go-grpc_out=./types --zrpc_out=. Done. 填充业务逻辑 $ vim internal/logic/getuserlogic.go package logic import ( \"context\" \"go-zero-demo/mall/user/rpc/internal/svc\" \"go-zero-demo/mall/user/rpc/types/user\" \"github.com/zeromicro/go-zero/core/logx\" ) type GetUserLogic struct { ctx context.Context svcCtx *svc.ServiceContext logx.Logger } func NewGetUserLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetUserLogic { return &GetUserLogic{ ctx: ctx, svcCtx: svcCtx, Logger: logx.WithContext(ctx), } } func (l *GetUserLogic) GetUser(in *user.IdRequest) (*user.UserResponse, error) { return &user.UserResponse{ Id: \"1\", Name: \"test\", }, nil } 创建order api服务 创建 order api服务 # 回到 go-zero-demo/mall 目录 $ mkdir -p order/api && cd order/api 添加api文件 $ vim order.api type( OrderReq { Id string `path:\"id\"` } OrderReply { Id string `json:\"id\"` Name string `json:\"name\"` } ) service order { @handler getOrder get /api/order/get/:id (OrderReq) returns (OrderReply) } 生成order服务 $ goctl api go -api order.api -dir . Done. 添加user rpc配置 $ vim internal/config/config.go package config import ( \"github.com/zeromicro/go-zero/zrpc\" \"github.com/zeromicro/go-zero/rest\" ) type Config struct { rest.RestConf UserRpc zrpc.RpcClientConf } 添加yaml配置 $ vim etc/order.yaml Name: order Host: 0.0.0.0 Port: 8888 UserRpc: Etcd: Hosts: - 127.0.0.1:2379 Key: user.rpc 完善服务依赖 $ vim internal/svc/servicecontext.go package svc import ( \"go-zero-demo/mall/order/api/internal/config\" \"go-zero-demo/mall/user/rpc/user\" \"github.com/zeromicro/go-zero/zrpc\" ) type ServiceContext struct { Config config.Config UserRpc user.User } func NewServiceContext(c config.Config) *ServiceContext { return &ServiceContext{ Config: c, UserRpc: user.NewUser(zrpc.MustNewClient(c.UserRpc)), } } 添加order演示逻辑 给 getorderlogic 添加业务逻辑 $ vim internal/logic/getorderlogic.go package logic import ( \"context\" \"errors\" \"go-zero-demo/mall/order/api/internal/svc\" \"go-zero-demo/mall/order/api/internal/types\" \"go-zero-demo/mall/user/rpc/types/user\" \"github.com/zeromicro/go-zero/core/logx\" ) type GetOrderLogic struct { logx.Logger ctx context.Context svcCtx *svc.ServiceContext } func NewGetOrderLogic(ctx context.Context, svcCtx *svc.ServiceContext) GetOrderLogic { return GetOrderLogic{ Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx, } } func (l *GetOrderLogic) GetOrder(req *types.OrderReq) (*types.OrderReply, error) { user, err := l.svcCtx.UserRpc.GetUser(l.ctx, &user.IdRequest{ Id: \"1\", }) if err != nil { return nil, err } if user.Name != \"test\" { return nil, errors.New(\"用户不存在\") } return &types.OrderReply{ Id: req.Id, Name: \"test order\", }, nil } 启动服务并验证 启动etcd$ etcd 下载依赖# 在 go-zero-demo 目录下 $ go mod tidy 启动user rpc # 在 mall/user/rpc 目录 $ go run user.go -f etc/user.yaml Starting rpc server at 127.0.0.1:8080... 启动order api # 在 mall/order/api 目录 $ go run order.go -f etc/order.yaml Starting server at 0.0.0.0:8888... 访问order api $ curl -i -X GET http://localhost:8888/api/order/get/1 HTTP/1.1 200 OK Content-Type: application/json Date: Sun, 07 Feb 2021 03:45:05 GMT Content-Length: 30 {\"id\":\"1\",\"name\":\"test order\"} 注意:在演示中的提及的api语法,rpc生成,goctl,goctl环境等怎么使用和安装,快速入门中不作详细概述,我们后续都会有详细的文档进行描述,你也可以点击下文的【猜你想看】快速跳转的对应文档查看。 源码 mall源码 猜你想看 goctl使用说明 api目录结构介绍 api语法 api配置文件介绍 api中间件使用 rpc目录 rpc配置 rpc调用方说明 Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:49 "},"framework-design.html":{"url":"framework-design.html","title":"框架设计","keywords":"","body":"框架设计 本节将从 go-zero 的设计理念,go-zero 服务的最佳实践目录来说明 go-zero 框架的设计,本节将包含以下小节: go-zero设计理念 go-zero特点 api语法介绍 api目录结构 rpc目录结构 Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:49 "},"go-zero-design.html":{"url":"go-zero-design.html","title":"go-zero设计理念","keywords":"","body":"go-zero设计理念 对于微服务框架的设计,我们期望保障微服务稳定性的同时,也要特别注重研发效率。所以设计之初,我们就有如下一些准则: 保持简单,第一原则 弹性设计,面向故障编程 工具大于约定和文档 高可用 高并发 易扩展 对业务开发友好,封装复杂度 约束做一件事只有一种方式 Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:49 "},"go-zero-features.html":{"url":"go-zero-features.html","title":"go-zero特点","keywords":"","body":"go-zero特性 go-zero 是一个集成了各种工程实践的包含 web 和 rpc 框架,有如下主要特点: 强大的工具支持,尽可能少的代码编写 极简的接口 完全兼容 net/http 支持中间件,方便扩展 高性能 面向故障编程,弹性设计 内建服务发现、负载均衡 内建限流、熔断、降载,且自动触发,自动恢复 API 参数自动校验 超时级联控制 自动缓存控制 链路跟踪、统计报警等 高并发支撑,稳定保障了疫情期间每天的流量洪峰 如下图,我们从多个层面保障了整体服务的高可用: Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:49 "},"api-grammar.html":{"url":"api-grammar.html","title":"api语法介绍","keywords":"","body":"api语法介绍 api示例 /** * api语法示例及语法说明 */ // api语法版本 syntax = \"v1\" // import literal import \"foo.api\" // import group import ( \"bar.api\" \"foo/bar.api\" ) info( author: \"songmeizi\" date: \"2020-01-08\" desc: \"api语法示例及语法说明\" ) // type literal type Foo{ Foo int `json:\"foo\"` } // type group type( Bar{ Bar int `json:\"bar\"` } ) // service block @server( jwt: Auth group: foo ) service foo-api{ @doc \"foo\" @handler foo post /foo (Foo) returns (Bar) } api语法结构 syntax语法声明 import语法块 info语法块 type语法块 service语法块 隐藏通道 [!TIP] 在以上语法结构中,各个语法块从语法上来说,按照语法块为单位,可以在.api文件中任意位置声明, 但是为了提高阅读效率,我们建议按照以上顺序进行声明,因为在将来可能会通过严格模式来控制语法块的顺序。 syntax语法声明 syntax是新加入的语法结构,该语法的引入可以解决: 快速针对api版本定位存在问题的语法结构 针对版本做语法解析 防止api语法大版本升级导致前后不能向前兼容 **[!WARNING] 被import的api必须要和main api的syntax版本一致。 语法定义 'syntax'={checkVersion(p)}STRING 语法说明 syntax:固定token,标志一个syntax语法结构的开始 checkVersion:自定义go方法,检测STRING是否为一个合法的版本号,目前检测逻辑为,STRING必须是满足(?m)\"v[1-9][0-9]*\"正则。 STRING:一串英文双引号包裹的字符串,如\"v1\" 一个api语法文件只能有0或者1个syntax语法声明,如果没有syntax,则默认为v1版本 正确语法示例 ✅ eg1:不规范写法 syntax=\"v1\" eg2:规范写法(推荐) syntax = \"v2\" 错误语法示例 ❌ eg1: syntax = \"v0\" eg2: syntax = v1 eg3: syntax = \"V1\" import语法块 随着业务规模增大,api中定义的结构体和服务越来越多,所有的语法描述均为一个api文件,这是多么糟糕的一个问题, 其会大大增加了阅读难度和维护难度,import语法块可以帮助我们解决这个问题,通过拆分api文件, 不同的api文件按照一定规则声明,可以降低阅读难度和维护难度。 **[!WARNING] 这里import不像golang那样包含package声明,仅仅是一个文件路径的引入,最终解析后会把所有的声明都汇聚到一个spec.Spec中。 不能import多个相同路径,否则会解析错误。 语法定义 'import' {checkImportValue(p)}STRING |'import' '(' ({checkImportValue(p)}STRING)+ ')' 语法说明 import:固定token,标志一个import语法的开始 checkImportValue:自定义go方法,检测STRING是否为一个合法的文件路径,目前检测逻辑为,STRING必须是满足(?m)\"(/?[a-zA-Z0-9_#-])+\\.api\"正则。 STRING:一串英文双引号包裹的字符串,如\"foo.api\" 正确语法示例 ✅ eg: import \"foo.api\" import \"foo/bar.api\" import( \"bar.api\" \"foo/bar/foo.api\" ) 错误语法示例 ❌ eg: import foo.api import \"foo.txt\" import ( bar.api bar.api ) info语法块 info语法块是一个包含了多个键值对的语法体,其作用相当于一个api服务的描述,解析器会将其映射到spec.Spec中, 以备用于翻译成其他语言(golang、java等) 时需要携带的meta元素。如果仅仅是对当前api的一个说明,而不考虑其翻译 时传递到其他语言,则使用简单的多行注释或者java风格的文档注释即可,关于注释说明请参考下文的 隐藏通道。 **[!WARNING] 不能使用重复的key,每个api文件只能有0或者1个info语法块 语法定义 'info' '(' (ID {checkKeyValue(p)}VALUE)+ ')' 语法说明 info:固定token,标志一个info语法块的开始 checkKeyValue:自定义go方法,检测VALUE是否为一个合法值。 VALUE:key对应的值,可以为单行的除'\\r','\\n','/'后的任意字符,多行请以\"\"包裹,不过强烈建议所有都以\"\"包裹 正确语法示例 ✅ eg1:不规范写法 info( foo: foo value bar:\"bar value\" desc:\"long long long long long long text\" ) eg2:规范写法(推荐) info( foo: \"foo value\" bar: \"bar value\" desc: \"long long long long long long text\" ) 错误语法示例 ❌ eg1:没有key-value内容 info() eg2:不包含冒号 info( foo value ) eg3:key-value没有换行 info(foo:\"value\") eg4:没有key info( : \"value\" ) eg5:非法的key info( 12: \"value\" ) eg6:移除旧版本多行语法 info( foo: > some text type语法块 在api服务中,我们需要用到一个结构体(类)来作为请求体,响应体的载体,因此我们需要声明一些结构体来完成这件事情, type语法块由golang的type演变而来,当然也保留着一些golang type的特性,沿用golang特性有: 保留了golang内置数据类型bool,int,int8,int16,int32,int64,uint,uint8,uint16,uint32,uint64,uintptr ,float32,float64,complex64,complex128,string,byte,rune, 兼容golang struct风格声明 保留golang关键字 **[!WARNING]️ 不支持alias 不支持time.Time数据类型,用int64表示,因为api支持客户端代码生成,并非所有客户端语言都有time.Time对应的类型 结构体名称、字段名称、不能为golang关键字 语法定义 由于其和golang相似,因此不做详细说明,具体语法定义请在 ApiParser.g4 中查看typeSpec定义。 语法说明 参考golang写法 正确语法示例 ✅ eg1:不规范写法 type Foo struct{ Id int `path:\"id\"` // ① Foo int `json:\"foo\"` } type Bar struct{ // 非导出型字段 bar int `form:\"bar\"` } type( // 非导出型结构体 fooBar struct{ FooBar int `json:\"fooBar\"` } ) eg2:规范写法(推荐) type Foo{ Id int `path:\"id\"` Foo int `json:\"foo\"` } type Bar{ Bar int `form:\"bar\"` } type( FooBar{ FooBar int `json:\"fooBar\"` } ) 错误语法示例 ❌ eg type Gender int // 不支持 // 非struct token type Foo structure{ CreateTime time.Time // 不支持time.Time,且没有声明 tag } // golang关键字 var type var{} type Foo{ // golang关键字 interface Foo interface // 没有声明 tag } type Foo{ foo int // map key必须要golang内置数据类型,且没有声明 tag m map[Bar]string } [!NOTE] ① tag定义和golang中json tag语法一样,除了json tag外,go-zero还提供了另外一些tag来实现对字段的描述, 详情见下表。 tag表 绑定参数时,以下四个tag只能选择其中一个 tag key 描述 提供方有效范围 示例 json json序列化tag golang request、response json:\"fooo\" path 路由path,如/foo/:id go-zero request path:\"id\" form 标志请求体是一个form(POST方法时)或者一个query(GET方法时/search?name=keyword) go-zero request form:\"name\" header HTTP header,如 Name: value go-zero request header:\"name\" tag修饰符 常见参数校验描述 tag key 描述 提供方 有效范围 示例 optional 定义当前字段为可选参数 go-zero request json:\"name,optional\" options 定义当前字段的枚举值,多个以竖线|隔开 go-zero request json:\"gender,options=male\" default 定义当前字段默认值 go-zero request json:\"gender,default=male\" range 定义当前字段数值范围 go-zero request json:\"age,range=[0:120]\" [!TIP] tag修饰符需要在tag value后以英文逗号,隔开 service语法块 service语法块用于定义api服务,包含服务名称,服务metadata,中间件声明,路由,handler等。 **[!WARNING]️ main api和被import的api服务名称必须一致,不能出现服务名称歧义。 handler名称不能重复 路由(请求方法+请求path)名称不能重复 请求体必须声明为普通(非指针)struct,响应体做了一些向前兼容处理,详请见下文说明 语法定义 serviceSpec: atServer? serviceApi; atServer: '@server' lp='(' kvLit+ rp=')'; serviceApi: {match(p,\"service\")}serviceToken=ID serviceName lbrace='{' serviceRoute* rbrace='}'; serviceRoute: atDoc? (atServer|atHandler) route; atDoc: '@doc' lp='('? ((kvLit+)|STRING) rp=')'?; atHandler: '@handler' ID; route: {checkHttpMethod(p)}httpMethod=ID path request=body? returnToken=ID? response=replybody?; body: lp='(' (ID)? rp=')'; replybody: lp='(' dataType? rp=')'; // kv kvLit: key=ID {checkKeyValue(p)}value=LINE_VALUE; serviceName: (ID '-'?)+; path: (('/' (ID ('-' ID)*))|('/:' (ID ('-' ID)?)))+; 语法说明 serviceSpec:包含了一个可选语法块atServer和serviceApi语法块,其遵循序列模式(编写service必须要按照顺序,否则会解析出错) atServer: 可选语法块,定义key-value结构的server metadata,'@server' 表示这一个server语法块的开始,其可以用于描述serviceApi或者route语法块,其用于描述不同语法块时有一些特殊关键key 需要值得注意,见 atServer关键key描述说明。 serviceApi:包含了1到多个serviceRoute语法块 serviceRoute:按照序列模式包含了atDoc,handler和route atDoc:可选语法块,一个路由的key-value描述,其在解析后会传递到spec.Spec结构体,如果不关心传递到spec.Spec, 推荐用单行注释替代。 handler:是对路由的handler层描述,可以通过atServer指定handler key来指定handler名称, 也可以直接用atHandler语法块来定义handler名称 atHandler:'@handler' 固定token,后接一个遵循正则[_a-zA-Z][a-zA-Z_-]*)的值,用于声明一个handler名称 route:路由,有httpMethod、path、可选request、可选response组成,httpMethod是必须是小写。 body:api请求体语法定义,必须要由()包裹的可选的ID值 replyBody:api响应体语法定义,必须由()包裹的struct、array(向前兼容处理,后续可能会废弃,强烈推荐以struct包裹,不要直接用array作为响应体) kvLit: 同info key-value serviceName: 可以有多个'-'join的ID值 path:api请求路径,必须以'/'或者'/:'开头,切不能以'/'结尾,中间可包含ID或者多个以'-'join的ID字符串 atServer关键key描述说明 修饰service时 key描述示例 jwt声明当前service下所有路由需要jwt鉴权,且会自动生成包含jwt逻辑的代码jwt: Auth group声明当前service或者路由文件分组group: login middleware声明当前service需要开启中间件middleware: AuthMiddleware prefix添加路由分组prefix: api 修饰route时 key描述示例 handler声明一个handler- 正确语法示例 ✅ eg1:不规范写法 @server( jwt: Auth group: foo middleware: AuthMiddleware prefix api ) service foo-api{ @doc( summary: foo ) @server( handler: foo ) // 非导出型body post /foo/:id (foo) returns (bar) @doc \"bar\" @handler bar post /bar returns ([]int)// 不推荐数组作为响应体 @handler fooBar post /foo/bar (Foo) returns // 可以省略'returns' } eg2:规范写法(推荐) @server( jwt: Auth group: foo middleware: AuthMiddleware prefix: api ) service foo-api{ @doc \"foo\" @handler foo post /foo/:id (Foo) returns (Bar) } service foo-api{ @handler ping get /ping @doc \"foo\" @handler bar post /bar/:id (Foo) } 错误语法示例 ❌ // 不支持空的server语法块 @server( ) // 不支持空的service语法块 service foo-api{ } service foo-api{ @doc kkkk // 简版doc必须用英文双引号引起来 @handler foo post /foo @handler foo // 重复的handler post /bar @handler fooBar post /bar // 重复的路由 // @handler和@doc顺序错误 @handler someHandler @doc \"some doc\" post /some/path // handler缺失 post /some/path/:id @handler reqTest post /foo/req (*Foo) // 不支持除普通结构体外的其他数据类型作为请求体 @handler replyTest post /foo/reply returns (*Foo) // 不支持除普通结构体、数组(向前兼容,后续考虑废弃)外的其他数据类型作为响应体 } 隐藏通道 隐藏通道目前主要为空白符号、换行符号以及注释,这里我们只说注释,因为空白符号和换行符号我们目前拿来也无用。 单行注释 语法定义 '//' ~[\\r\\n]* 语法说明 由语法定义可知道,单行注释必须要以//开头,内容为不能包含换行符 正确语法示例 ✅ // doc // comment 错误语法示例 ❌ // break line comments java风格文档注释 语法定义 '/*' .*? '*/' 语法说明 由语法定义可知道,单行注释必须要以/*开头,*/结尾的任意字符。 正确语法示例 ✅ /** * java-style doc */ 错误语法示例 ❌ /* * java-style doc */ */ Doc&Comment 如果想获取某一个元素的doc或者comment开发人员需要怎么定义? Doc 我们规定上一个语法块(非隐藏通道内容)的行数line+1到当前语法块第一个元素前的所有注释(单行,或者多行)均为doc, 且保留了//、/*、*/原始标记。 Comment 我们规定当前语法块最后一个元素所在行开始的一个注释块(当行,或者多行)为comment 且保留了//、/*、*/原始标记。 语法块Doc和Comment的支持情况 语法块parent语法块DocComment syntaxLitapi✅✅ kvLitinfoSpec✅✅ importLitimportSpec✅✅ typeLitapi✅❌ typeLittypeBlock✅❌ fieldtypeLit✅✅ key-valueatServer✅✅ atHandlerserviceRoute✅✅ routeserviceRoute✅✅ 以下为对应语法块解析后细带doc和comment的写法 // syntaxLit doc syntax = \"v1\" // syntaxLit commnet info( // kvLit doc author: songmeizi // kvLit comment ) // typeLit doc type Foo {} type( // typeLit doc Bar{} FooBar{ // filed doc Name int // filed comment } ) @server( /** * kvLit doc * 开启jwt鉴权 */ jwt: Auth /**kvLit comment*/ ) service foo-api{ // atHandler doc @handler foo //atHandler comment /* * route doc * post请求 * path为 /foo * 请求体:Foo * 响应体:Foo */ post /foo (Foo) returns (Foo) // route comment } Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:49 "},"api-dir.html":{"url":"api-dir.html","title":"api目录结构","keywords":"","body":"api目录介绍 . ├── etc │ └── greet-api.yaml // 配置文件 ├── go.mod // mod文件 ├── greet.api // api描述文件 ├── greet.go // main函数入口 └── internal ├── config │ └── config.go // 配置声明type ├── handler // 路由及handler转发 │ ├── greethandler.go │ └── routes.go ├── logic // 业务逻辑 │ └── greetlogic.go ├── middleware // 中间件文件 │ └── greetmiddleware.go ├── svc // logic所依赖的资源池 │ └── servicecontext.go └── types // request、response的struct,根据api自动生成,不建议编辑 └── types.go Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:49 "},"rpc-dir.html":{"url":"rpc-dir.html","title":"rpc目录结构","keywords":"","body":"rpc服务目录 proto 文件 greet.proto syntax = \"proto3\"; package stream; option go_package = \"./greet\"; message StreamReq { string name = 1; } message StreamResp { string greet = 1; } service StreamGreeter { rpc greet(StreamReq) returns (StreamResp); } goctl rpc proto $ goctl rpc protoc greet.proto --go_out=. --go-grpc_out=. --zrpc_out=. [goctl-env]: preparing to check env [goctl-env]: looking up \"protoc\" [goctl-env]: \"protoc\" is installed [goctl-env]: looking up \"protoc-gen-go\" [goctl-env]: \"protoc-gen-go\" is installed [goctl-env]: looking up \"protoc-gen-go-grpc\" [goctl-env]: \"protoc-gen-go-grpc\" is installed [goctl-env]: congratulations! your goctl environment is ready! [command]: protoc greet.proto --go_out=. --go-grpc_out=. Done. 生成的目录结构 . ├── etc │ └── greet.yaml ├── go.mod ├── go.sum ├── greet // [1] │ ├── greet.pb.go │ └── greet_grpc.pb.go ├── greet.go ├── greet.proto ├── internal │ ├── config │ │ └── config.go │ ├── logic │ │ └── greetlogic.go │ ├── server │ │ └── streamgreeterserver.go │ └── svc │ └── servicecontext.go └── streamgreeter └── streamgreeter.go [1] pb.go & _grpc.pb.go 文件所在目录并非固定,该目录有 go_opt & go-grpc_opt 与 proto文件中的 go_package 值共同决定,想要了解grpc代码生成目录逻辑请阅读 Go Generated Code Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:50 "},"project-dev.html":{"url":"project-dev.html","title":"项目开发","keywords":"","body":"项目开发 在前面的章节我们已经从一些概念、背景、快速入门等维度介绍了一下go-zero,看到这里,相信你对go-zero已经有了一些了解, 从这里开始,我们将会从环境准备到服务部署整个流程开始进行讲解,为了保证大家能够彻底弄懂go-zero的开发流程,那就准备你的耐心来接着往下走吧。 在章节中,将包含以下小节: 准备工作 golang安装 go modudle配置 goctl安装 protoc & protoc-gen-go安装 其他 开发规范 命名规范 路由规范 编码规范 开发流程 配置介绍 api配置 rpc配置 业务开发 目录拆分 model生成 api文件编写 业务编码 jwt鉴权 中间件使用 rpc服务编写与调用 错误处理 CI/CD 服务部署 日志收集 链路追踪 服务监控 Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:49 "},"prepare.html":{"url":"prepare.html","title":"准备工作","keywords":"","body":"准备工作 在正式进入实际开发之前,我们需要做一些准备工作,比如:Go环境的安装,grpc代码生成使用的工具安装, 必备工具Goctl的安装,Golang环境配置等,本节将包含以下小节: golang安装 go modudle配置 goctl安装 protoc & protoc-gen-go安装 其他 Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:49 "},"golang-install.html":{"url":"golang-install.html","title":"golang安装","keywords":"","body":"Golang环境安装 前言 开发golang程序,必然少不了对其环境的安装,我们这里选择以1.15.1为例。 官方文档 https://golang.google.cn/doc/install mac OS安装Go 下载并安装Go for Mac 验证安装结果 $ go version go version go1.15.1 darwin/amd64 linux 安装Go 下载Go for Linux 解压压缩包至/usr/local $ tar -C /usr/local -xzf go1.15.8.linux-amd64.tar.gz 添加/usr/local/go/bin到环境变量 $ $HOME/.profile export PATH=$PATH:/usr/local/go/bin $ source $HOME/.profile 验证安装结果 $ go version go version go1.15.1 linux/amd64 Windows安装Go 下载并安装Go for Windows 验证安装结果 $ go version go version go1.15.1 windows/amd64 其他 更多操作系统安装见https://golang.org/dl/ Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:49 "},"gomod-config.html":{"url":"gomod-config.html","title":"go module配置","keywords":"","body":"Go Module设置 Go Module介绍 Modules are how Go manages dependencies.[1] 即Go Module是Golang管理依赖性的方式,像Java中的Maven,Android中的Gradle类似。 MODULE配置 查看GO111MODULE开启情况 $ go env GO111MODULE on 开启GO111MODULE,如果已开启(即执行go env GO111MODULE结果为on)请跳过。 $ go env -w GO111MODULE=\"on\" 设置GOPROXY $ go env -w GOPROXY=https://goproxy.cn 设置GOMODCACHE 查看GOMODCACHE $ go env GOMODCACHE 如果目录不为空或者/dev/null,请跳过。 go env -w GOMODCACHE=$GOPATH/pkg/mod 参考文档 [1] Go Modules Reference Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:49 "},"goctl-install.html":{"url":"goctl-install.html","title":"goctl安装","keywords":"","body":"Goctl安装 前言 Goctl在go-zero项目开发着有着很大的作用,其可以有效的帮助开发者大大提高开发效率,减少代码的出错率,缩短业务开发的工作量,更多的Goctl的介绍请阅读Goctl介绍, 在这里我们强烈推荐大家安装,因为后续演示例子中我们大部分都会以goctl进行演示。 安装(mac&linux) download&install # Go 1.15 及之前版本 GO111MODULE=on GOPROXY=https://goproxy.cn/,direct go get -u github.com/zeromicro/go-zero/tools/goctl@latest # Go 1.16 及以后版本 GOPROXY=https://goproxy.cn/,direct go install github.com/zeromicro/go-zero/tools/goctl@latest 环境变量检测 go get下载编译后的二进制文件位于$GOPATH/bin目录下,要确保$GOPATH/bin已经添加到环境变量。 $ sudo vim /etc/paths 在最后一行添加如下内容 $GOPATH/bin [!TIP] $GOPATH为你本机上的文件地址 安装结果验证 $ goctl -v goctl version 1.1.4 darwin/amd64 [!TIP] windows用户添加环境变量请自行google Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:49 "},"protoc-install.html":{"url":"protoc-install.html","title":"protoc & protoc-gen-go安装","keywords":"","body":"protoc & protoc-gen-go安装 前言 protoc是一款用C++编写的工具,其可以将proto文件翻译为指定语言的代码。在go-zero的微服务中,我们采用grpc进行服务间的通信,而grpc的编写就需要用到protoc和翻译成go语言rpc stub代码的插件protoc-gen-go。 mac OS方式一:goctl一键安装 $ goctl env check -i -f --verbose [goctl-env]: preparing to check env [goctl-env]: looking up \"protoc\" [goctl-env]: \"protoc\" is not found in PATH [goctl-env]: preparing to install \"protoc\" \"protoc\" installed from cache [goctl-env]: \"protoc\" is already installed in \"/Users/keson/go/bin/protoc\" [goctl-env]: looking up \"protoc-gen-go\" [goctl-env]: \"protoc-gen-go\" is not found in PATH [goctl-env]: preparing to install \"protoc-gen-go\" \"protoc-gen-go\" installed from cache [goctl-env]: \"protoc-gen-go\" is already installed in \"/Users/keson/go/bin/protoc-gen-go\" [goctl-env]: looking up \"protoc-gen-go-grpc\" [goctl-env]: \"protoc-gen-go-grpc\" is not found in PATH [goctl-env]: preparing to install \"protoc-gen-go-grpc\" \"protoc-gen-go-grpc\" installed from cache [goctl-env]: \"protoc-gen-go-grpc\" is already installed in \"/Users/keson/go/bin/protoc-gen-go-grpc\" [goctl-env]: congratulations! your goctl environment is ready! 方式二: 源文件安装 protoc安装 进入protobuf release 页面,选择适合自己操作系统的压缩包文件 解压protoc-x.x.x-osx-x86_64.zip并进入protoc-x.x.x-osx-x86_64 $ cd protoc-x.x.x-osx-x86_64/bin 将启动的protoc二进制文件移动到被添加到环境变量的任意path下,如$GOPATH/bin,这里不建议直接将其和系统的以下path放在一起。 $ mv protoc $GOPATH/bin [!TIP] $GOPATH为你本机的实际文件夹地址 验证安装结果 $ protoc --version libprotoc x.x.x protoc-gen-go/protoc-gen-go-grpc 安装 下载安装protoc-gen-go $ go install google.golang.org/protobuf/cmd/protoc-gen-go@latest $ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest [!WARNING] protoc-gen-go安装失败请阅读常见错误处理 Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:49 "},"prepare-other.html":{"url":"prepare-other.html","title":"其他","keywords":"","body":"其他 在之前我们已经对Go环境、Go Module配置、Goctl、protoc & protoc-gen-go安装准备就绪,这些是开发人员在开发阶段必须要准备的环境,而接下来的环境你可以选择性的安装, 因为这些环境一般存在于服务器(安装工作运维会替你完成),但是为了后续演示流程能够完整走下去,我建议大家在本地也安装一下,因为我们的演示环境大部分会以本地为主。 以下仅给出了需要的准备工作,不以文档篇幅作详细介绍了。 其他环境 etcd redis mysql Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:49 "},"dev-specification.html":{"url":"dev-specification.html","title":"开发规范","keywords":"","body":"开发规范 在实际业务开发中,除了要提高业务开发效率,缩短业务开发周期,保证线上业务高性能,高可用的指标外,好的编程习惯也是一个开发人员基本素养之一,在本章节, 我们将介绍一下go-zero中的编码规范,本章节为可选章节,内容仅供交流与参考,本章节将从以下小节进行说明: 命名规范 路由规范 编码规范 开发三原则 Clarity(清晰) 作者引用了Hal Abelson and Gerald Sussman的一句话: Programs must be written for people to read, and only incidentally for machines to execute 程序是什么,程序必须是为了开发人员阅读而编写的,只是偶尔给机器去执行,99%的时间程序代码面向的是开发人员,而只有1%的时间可能是机器在执行,这里比例不是重点,从中我们可以看出,清晰的代码是多么的重要,因为所有程序,不仅是Go语言,都是由开发人员编写,供其他人阅读和维护。 Simplicity(简单) Simplicity is prerequisite for reliability Edsger W. Dijkstra认为:可靠的前提条件就是简单,我们在实际开发中都遇到过,这段代码在写什么,想要完成什么事情,开发人员不理解这段代码,因此也不知道如何去维护,这就带来了复杂性,程序越是复杂就越难维护,越难维护就会是程序变得越来越复杂,因此,遇到程序变复杂时首先应该想到的是——重构,重构会重新设计程序,让程序变得简单。 Productivity(生产力) 在go-zero团队中,一直在强调这个话题,开发人员成产力的多少,并不是你写了多少行代码,完成了多少个模块开发,而是我们需要利用各种有效的途径来利用有限的时间完成开发效率最大化,而Goctl的诞生正是为了提高生产力, 因此这个开发原则我是非常认同的。 Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:49 "},"naming-spec.html":{"url":"naming-spec.html","title":"命名规范","keywords":"","body":"命名规范 在任何语言开发中,都有其语言领域的一些命名规范,好的命名可以: 降低代码阅读成本 降低维护难度 降低代码复杂度 规范建议 在我们实际开发中,有很多开发人可能是由某一语言转到另外一个语言领域,在转到另外一门语言后, 我们都会保留着对旧语言的编程习惯,在这里,我建议的是,虽然不同语言之前的某些规范可能是相通的, 但是我们最好能够按照官方的一些demo来熟悉是渐渐适应当前语言的编程规范,而不是直接将原来语言的编程规范也随之迁移过来。 命名准则 当变量名称在定义和最后一次使用之间的距离很短时,简短的名称看起来会更好。 变量命名应尽量描述其内容,而不是类型 常量命名应尽量描述其值,而不是如何使用这个值 在遇到for,if等循环或分支时,推荐单个字母命名来标识参数和返回值 method、interface、type、package推荐使用单词命名 package名称也是命名的一部分,请尽量将其利用起来 使用一致的命名风格 文件命名规范 全部小写 除unit test外避免下划线(_) 文件名称不宜过长 变量命名规范参考 首字母小写 驼峰命名 见名知义,避免拼音替代英文 不建议包含下划线(_) 不建议包含数字 适用范围 局部变量 函数出参、入参 函数、常量命名规范 驼峰式命名 可exported的必须首字母大写 不可exported的必须首字母小写 避免全部大写与下划线(_)组合 [!TIP] 如果是go-zero代码贡献,则必须严格遵循此命名规范 参考文档 Practical Go: Real world advice for writing maintainable Go programs Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:49 "},"route-naming-spec.html":{"url":"route-naming-spec.html","title":"路由规范","keywords":"","body":"路由规范 推荐脊柱式命名 小写单词、横杠(-)组合 见名知义 /user/get-info /user/get/info /user/password/change/:id Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:50 "},"coding-spec.html":{"url":"coding-spec.html","title":"编码规范","keywords":"","body":"编码规范 import 单行import不建议用圆括号包裹 按照官方包,NEW LINE,当前工程包,NEW LINE,第三方依赖包顺序引入 import ( \"context\" \"string\" \"greet/user/internal/config\" \"google.golang.org/grpc\" ) 函数返回 对象避免非指针返回 遵循有正常值返回则一定无error,有error则一定无正常值返回的原则 错误处理 有error必须处理,如果不能处理就必须抛出。 避免下划线(_)接收error 函数体编码 建议一个block结束空一行,如if、for等 func main (){ if x==1{ // do something } fmt.println(\"xxx\") } return前空一行 func getUser(id string)(string,error){ .... return \"xx\",nil } Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:49 "},"dev-flow.html":{"url":"dev-flow.html","title":"开发流程","keywords":"","body":"开发流程 这里的开发流程和我们实际业务开发流程不是一个概念,这里的定义局限于go-zero的使用,即代码层面的开发细节。 开发流程 goctl环境准备[1] 数据库设计 业务开发 新建工程 创建服务目录 创建服务类型(api/rpc/rmq/job/script) 编写api、proto文件 代码生成 生成数据库访问层代码model 配置config,yaml变更 资源依赖填充(ServiceContext) 添加中间件 业务代码填充 错误处理 [!TIP] [1] goctl环境 开发工具 Visual Studio Code Goland(推荐) Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:49 "},"config-introduction.html":{"url":"config-introduction.html","title":"配置介绍","keywords":"","body":"配置介绍 在正式使用go-zero之前,让我们先来了解一下go-zero中不同服务类型的配置定义,看看配置中每个字段分别有什么作用,本节将包含以下小节: api配置 rpc配置 Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:49 "},"api-config.html":{"url":"api-config.html","title":"api配置","keywords":"","body":"api配置 api配置控制着api服务中的各种功能,包含但不限于服务监听地址,端口,环境配置,日志配置等,下面我们从一个简单的配置来看一下api中常用配置分别有什么作用。 配置说明 通过yaml配置我们会发现,有很多参数我们并没有与config对齐,这是因为config定义中,有很多都是带optional或者default 标签的,对于optional可选项,你可以根据自己需求判断是否需要设置,对于default标签,如果你觉得默认值就已经够了,可以不用设置, 一般default中的值基本不用修改,可以认为是最佳实践值。 Config type Config struct{ rest.RestConf // rest api配置 Auth struct { // jwt鉴权配置 AccessSecret string // jwt密钥 AccessExpire int64 // 有效期,单位:秒 } Mysql struct { // 数据库配置,除mysql外,可能还有mongo等其他数据库 DataSource string // mysql链接地址,满足 $user:$password@tcp($ip:$port)/$db?$queries 格式即可 } CacheRedis cache.CacheConf // redis缓存 UserRpc zrpc.RpcClientConf // rpc client配置 } rest.RestConf api服务基础配置,包含监听地址,监听端口,证书配置,限流,熔断参数,超时参数等控制,对其展开我们可以看到: service.ServiceConf // service配置 Host string `json:\",default=0.0.0.0\"` // http监听ip,默认0.0.0.0 Port int // http监听端口,必填 CertFile string `json:\",optional\"` // https证书文件,可选 KeyFile string `json:\",optional\"` // https私钥文件,可选 Verbose bool `json:\",optional\"` // 是否打印详细http请求日志 MaxConns int `json:\",default=10000\"` // http同时可接受最大请求数(限流数),默认10000 MaxBytes int64 `json:\",default=1048576,range=[0:8388608]\"` // http可接受请求的最大ContentLength,默认1048576,被设置值必须在0到8388608之间 // milliseconds Timeout int64 `json:\",default=3000\"` // 超时时长控制,单位:毫秒,默认3000 CpuThreshold int64 `json:\",default=900,range=[0:1000]\"` // cpu降载阈值,默认900,可允许设置范围0到1000 Signature SignatureConf `json:\",optional\"` // 签名配置 service.ServiceConf type ServiceConf struct { Name string // 服务名称 Log logx.LogConf // 日志配置 Mode string `json:\",default=pro,options=dev|test|pre|pro\"` // 服务环境,dev-开发环境,test-测试环境,pre-预发环境,pro-正式环境 MetricsUrl string `json:\",optional\"` // 指标上报接口地址,该地址需要支持post json即可 Prometheus prometheus.Config `json:\",optional\"` // prometheus配置 } logx.LogConf type LogConf struct { ServiceName string `json:\",optional\"` // 服务名称 Mode string `json:\",default=console,options=console|file|volume\"` // 日志模式,console-输出到console,file-输出到当前服务器(容器)文件,,volume-输出docker挂载文件内 Path string `json:\",default=logs\"` // 日志存储路径 Level string `json:\",default=info,options=info|error|severe\"` // 日志级别 Compress bool `json:\",optional\"` // 是否开启gzip压缩 KeepDays int `json:\",optional\"` // 日志保留天数 StackCooldownMillis int `json:\",default=100\"` // 日志write间隔 } prometheus.Config type Config struct { Host string `json:\",optional\"` // prometheus 监听host Port int `json:\",default=9101\"` // prometheus 监听端口 Path string `json:\",default=/metrics\"` // 上报地址 } SignatureConf SignatureConf struct { Strict bool `json:\",default=false\"` // 是否Strict模式,如果是则PrivateKeys必填 Expiry time.Duration `json:\",default=1h\"` // 有效期,默认1小时 PrivateKeys []PrivateKeyConf // 签名密钥相关配置 } PrivateKeyConf PrivateKeyConf struct { Fingerprint string // 指纹配置 KeyFile string // 密钥配置 } cache.CacheConf ClusterConf []NodeConf NodeConf struct { redis.RedisConf Weight int `json:\",default=100\"` // 权重 } redis.RedisConf RedisConf struct { Host string // redis地址 Type string `json:\",default=node,options=node|cluster\"` // redis类型 Pass string `json:\",optional\"` // redis密码 } Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:49 "},"rpc-config.html":{"url":"rpc-config.html","title":"rpc配置","keywords":"","body":"rpc配置 rpc配置控制着一个rpc服务的各种功能,包含但不限于监听地址,etcd配置,超时,熔断配置等,下面我们以一个常见的rpc服务配置来进行说明。 配置说明 Config struct { zrpc.RpcServerConf CacheRedis cache.CacheConf // redis缓存配置,详情见api配置说明,这里不赘述 Mysql struct { // mysql数据库访问配置,详情见api配置说明,这里不赘述 DataSource string } } zrpc.RpcServerConf RpcServerConf struct { service.ServiceConf // 服务配置,详情见api配置说明,这里不赘述 ListenOn string // rpc监听地址和端口,如:127.0.0.1:8888 Etcd discov.EtcdConf `json:\",optional\"` // etcd相关配置 Auth bool `json:\",optional\"` // 是否开启Auth,如果是则Redis为必填 Redis redis.RedisKeyConf `json:\",optional\"` // Auth验证 StrictControl bool `json:\",optional\"` // 是否Strict模式,如果是则遇到错误是Auth失败,否则可以认为成功 // pending forever is not allowed // never set it to 0, if zero, the underlying will set to 2s automatically Timeout int64 `json:\",default=2000\"` // 超时控制,单位:毫秒 CpuThreshold int64 `json:\",default=900,range=[0:1000]\"` cpu降载阈值,默认900,可允许设置范围0到1000 } discov.EtcdConf type EtcdConf struct { Hosts []string // etcd host数组 Key string // rpc注册key } redis.RedisKeyConf RedisConf struct { Host string // redis 主机 Type string `json:\",default=node,options=node|cluster\"` // redis类型 Pass string `json:\",optional\"` // redis密码 } RedisKeyConf struct { RedisConf Key string `json:\",optional\"` // 验证key } Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:50 "},"business-dev.html":{"url":"business-dev.html","title":"业务开发","keywords":"","body":"业务开发 本章节我们用一个简单的示例去演示一下go-zero中的一些基本功能。本节将包含以下小节: 目录拆分 model生成 api文件编写 业务编码 jwt鉴权 中间件使用 rpc服务编写与调用 错误处理 演示工程下载 在正式进入后续文档叙述前,可以先留意一下这里的源码,后续我们会基于这份源码进行功能的递进式演示, 而不是完全从0开始,如果你从快速入门章节过来,这份源码结构对你来说不是问题。 点击这里下载演示工程基础源码 演示工程说明 场景 程序员小明需要借阅一本《西游记》,在没有线上图书管理系统的时候,他每天都要去图书馆前台咨询图书馆管理员, 小明:你好,请问今天《西游记》的图书还有吗? 管理员:没有了,明天再来看看吧。 过了一天,小明又来到图书馆,问: 小明:你好,请问今天《西游记》的图书还有吗? 管理员:没有了,你过两天再来看看吧。 就这样经过多次反复,小明也是徒劳无功,浪费大量时间在来回的路上,于是终于忍受不了落后的图书管理系统, 他决定自己亲手做一个图书查阅系统。 预期实现目标 用户登录 依靠现有学生系统数据进行登录 图书检索 根据图书关键字搜索图书,查询图书剩余数量。 系统分析 服务拆分 user api 提供用户登录协议 rpc 供search服务访问用户数据 search api 提供图书查询协议 [!TIP] 这个微小的图书借阅查询系统虽然小,从实际来讲不太符合业务场景,但是仅上面两个功能,已经满足我们对go-zero api/rpc的场景演示了, 后续为了满足更丰富的go-zero功能演示,会在文档中进行业务插入即相关功能描述。这里仅用一个场景进行引入。 注意:user中的sql语句请自行创建到db中去,更多准备工作见准备工作 添加一些预设的用户数据到数据库,便于后面使用,为了篇幅,演示工程不对插入数据这种操作做详细演示。 参考预设数据 INSERT INTO `user` (number,name,password,gender)values ('666','小明','123456','男'); Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:49 "},"service-design.html":{"url":"service-design.html","title":"目录拆分","keywords":"","body":"目录拆分 目录拆分是指配合go-zero的最佳实践的目录拆分,这和微服务拆分有着关联,在团队内部最佳实践中, 我们按照业务横向拆分,将一个系统拆分成多个子系统,每个子系统应拥有独立的持久化存储,缓存系统。 如一个商城系统需要有用户系统(user),商品管理系统(product),订单系统(order),购物车系统(cart),结算中心系统(pay),售后系统(afterSale)等组成。 系统结构分析 在上文提到的商城系统中,每个系统在对外(http)提供服务的同时,也会提供数据给其他子系统进行数据访问的接口(rpc),因此每个子系统可以拆分成一个服务,而且对外提供了两种访问该系统的方式api和rpc,因此, 以上系统按照目录结构来拆分有如下结构: . ├── afterSale │ ├── api │ └── rpc ├── cart │ ├── api │ └── rpc ├── order │ ├── api │ └── rpc ├── pay │ ├── api │ └── rpc ├── product │ ├── api │ └── rpc └── user ├── api └── rpc rpc调用链建议 在设计系统时,尽量做到服务之间调用链是单向的,而非循环调用,例如:order服务调用了user服务,而user服务反过来也会调用order的服务, 当其中一个服务启动故障,就会相互影响,进入死循环,你order认为是user服务故障导致的,而user认为是order服务导致的,如果有大量服务存在相互调用链, 则需要考虑服务拆分是否合理。 常见服务类型的目录结构 在上述服务中,仅列举了api/rpc服务,除此之外,一个服务下还可能有其他更多服务类型,如rmq(消息处理系统),cron(定时任务系统),script(脚本)等, 因此一个服务下可能包含以下目录结构: user ├── api // http访问服务,业务需求实现 ├── cronjob // 定时任务,定时数据更新业务 ├── rmq // 消息处理系统:mq和dq,处理一些高并发和延时消息业务 ├── rpc // rpc服务,给其他子系统提供基础数据访问 └── script // 脚本,处理一些临时运营需求,临时数据修复 完整工程目录结构示例 mall // 工程名称 ├── common // 通用库 │ ├── randx │ └── stringx ├── go.mod ├── go.sum └── service // 服务存放目录 ├── afterSale │ ├── api │ └── model │ └── rpc ├── cart │ ├── api │ └── model │ └── rpc ├── order │ ├── api │ └── model │ └── rpc ├── pay │ ├── api │ └── model │ └── rpc ├── product │ ├── api │ └── model │ └── rpc └── user ├── api ├── cronjob ├── model ├── rmq ├── rpc └── script 猜你想看 api目录结构介绍 Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:50 "},"model-gen.html":{"url":"model-gen.html","title":"model生成","keywords":"","body":"model生成 首先,下载好演示工程 后,我们以user的model来进行代码生成演示。 前言 model是服务访问持久化数据层的桥梁,业务的持久化数据常存在于mysql,mongo等数据库中,我们都知道,对于一个数据库的操作莫过于CURD, 而这些工作也会占用一部分时间来进行开发,我曾经在编写一个业务时写了40个model文件,根据不同业务需求的复杂性,平均每个model文件差不多需要 10分钟,对于40个文件来说,400分钟的工作时间,差不多一天的工作量,而goctl工具可以在10秒钟来完成这400分钟的工作。 准备工作 进入演示工程book,找到user/model下的user.sql文件,将其在你自己的数据库中执行建表。 代码生成(带缓存) 方式一(ddl) 进入service/user/model目录,执行命令 $ cd service/user/model $ goctl model mysql ddl -src user.sql -dir . -c Done. 方式二(datasource) $ goctl model mysql datasource -url=\"$datasource\" -table=\"user\" -c -dir . Done. [!TIP] $datasource为数据库连接地址 方式三(intellij 插件) 在Goland中,右键user.sql,依次进入并点击New->Go Zero->Model Code即可生成,或者打开user.sql文件, 进入编辑区,使用快捷键Command+N(for mac OS)或者 alt+insert(for windows),选择Mode Code即可 [!TIP] intellij插件生成需要安装goctl插件,详情见intellij插件 验证生成的model文件 查看tree $ tree . ├── user.sql ├── usermodel.go ├── usermodel_gen.go └── vars.go 更多 对于持久化数据,如果需要更灵活的数据库能力,包括事务能力,可以参考 Mysql 如果需要分布式事务的能力,可以参考 分布式事务支持 猜你想看 model命令及其原理 Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:49 "},"api-coding.html":{"url":"api-coding.html","title":"api文件编写","keywords":"","body":"api文件编写 编写user.api文件 $ vim service/user/api/user.api type ( LoginReq { Username string `json:\"username\"` Password string `json:\"password\"` } LoginReply { Id int64 `json:\"id\"` Name string `json:\"name\"` Gender string `json:\"gender\"` AccessToken string `json:\"accessToken\"` AccessExpire int64 `json:\"accessExpire\"` RefreshAfter int64 `json:\"refreshAfter\"` } ) service user-api { @handler login post /user/login (LoginReq) returns (LoginReply) } 生成api服务 方式一 $ cd book/service/user/api $ goctl api go -api user.api -dir . Done. 方式二 在 user.api 文件右键,依次点击进入 New->Go Zero->Api Code ,进入目标目录选择,即api源码的目标存放目录,默认为user.api所在目录,选择好目录后点击OK即可。 方式三 打开user.api,进入编辑区,使用快捷键Command+N(for mac OS)或者 alt+insert(for windows),选择Api Code,同样进入目录选择弹窗,选择好目录后点击OK即可。 猜你想看 api语法 goctl api命令 api目录结构介绍 Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:49 "},"business-coding.html":{"url":"business-coding.html","title":"业务编码","keywords":"","body":"业务编码 前面一节,我们已经根据初步需求编写了user.api来描述user服务对外提供哪些服务访问,在本节我们接着前面的步伐, 通过业务编码来讲述go-zero怎么在实际业务中使用。 添加Mysql配置 $ vim service/user/api/internal/config/config.go package config import ( \"github.com/zeromicro/go-zero/rest\" \"github.com/zeromicro/go-zero/core/stores/cache\" ) type Config struct { rest.RestConf Mysql struct{ DataSource string } CacheRedis cache.CacheConf } 完善yaml配置 $ vim service/user/api/etc/user-api.yaml Name: user-api Host: 0.0.0.0 Port: 8888 Mysql: DataSource: $user:$password@tcp($url)/$db?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai CacheRedis: - Host: $host Pass: $pass Type: node [!TIP] $user: mysql数据库user $password: mysql数据库密码 $url: mysql数据库连接地址 $db: mysql数据库db名称,即user表所在database $host: redis连接地址 格式:ip:port,如:127.0.0.1:6379 $pass: redis密码 更多配置信息,请参考api配置介绍 完善服务依赖 $ vim service/user/api/internal/svc/servicecontext.go type ServiceContext struct { Config config.Config UserModel model.UserModel } func NewServiceContext(c config.Config) *ServiceContext { conn:=sqlx.NewMysql(c.Mysql.DataSource) return &ServiceContext{ Config: c, UserModel: model.NewUserModel(conn,c.CacheRedis), } } 填充登录逻辑 $ vim service/user/api/internal/logic/loginlogic.go func (l *LoginLogic) Login(req types.LoginReq) (*types.LoginReply, error) { if len(strings.TrimSpace(req.Username)) == 0 || len(strings.TrimSpace(req.Password)) == 0 { return nil, errors.New(\"参数错误\") } userInfo, err := l.svcCtx.UserModel.FindOneByNumber(req.Username) switch err { case nil: case model.ErrNotFound: return nil, errors.New(\"用户名不存在\") default: return nil, err } if userInfo.Password != req.Password { return nil, errors.New(\"用户密码不正确\") } // ---start--- now := time.Now().Unix() accessExpire := l.svcCtx.Config.Auth.AccessExpire jwtToken, err := l.getJwtToken(l.svcCtx.Config.Auth.AccessSecret, now, l.svcCtx.Config.Auth.AccessExpire, userInfo.Id) if err != nil { return nil, err } // ---end--- return &types.LoginReply{ Id: userInfo.Id, Name: userInfo.Name, Gender: userInfo.Gender, AccessToken: jwtToken, AccessExpire: now + accessExpire, RefreshAfter: now + accessExpire/2, }, nil } [!TIP] 上述代码中 [start]-[end]的代码实现见jwt鉴权章节 猜你想看 api语法 goctl api命令 api目录结构介绍 jwt鉴权 api配置介绍 Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:49 "},"jwt.html":{"url":"jwt.html","title":"jwt鉴权","keywords":"","body":"jwt鉴权 概述 JSON Web令牌(JWT)是一个开放标准(RFC 7519),它定义了一种紧凑而独立的方法,用于在各方之间安全地将信息作为JSON对象传输。由于此信息是经过数字签名的,因此可以被验证和信任。可以使用秘密(使用HMAC算法)或使用RSA或ECDSA的公钥/私钥对对JWT进行签名。 什么时候应该使用JWT 授权:这是使用JWT的最常见方案。一旦用户登录,每个后续请求将包括JWT,从而允许用户访问该令牌允许的路由,服务和资源。单一登录是当今广泛使用JWT的一项功能,因为它的开销很小并且可以在不同的域中轻松使用。 信息交换:JSON Web令牌是在各方之间安全地传输信息的一种好方法。因为可以对JWT进行签名(例如,使用公钥/私钥对),所以您可以确保发件人是他们所说的人。此外,由于签名是使用标头和有效负载计算的,因此您还可以验证内容是否未被篡改。 为什么要使用JSON Web令牌 由于JSON不如XML冗长,因此在编码时JSON的大小也较小,从而使JWT比SAML更为紧凑。这使得JWT是在HTML和HTTP环境中传递的不错的选择。 在安全方面,只能使用HMAC算法由共享机密对SWT进行对称签名。但是,JWT和SAML令牌可以使用X.509证书形式的公用/专用密钥对进行签名。与签署JSON的简单性相比,使用XML Digital Signature签署XML而不引入模糊的安全漏洞是非常困难的。 JSON解析器在大多数编程语言中都很常见,因为它们直接映射到对象。相反,XML没有自然的文档到对象的映射。与SAML断言相比,这使使用JWT更加容易。 关于用法,JWT是在Internet规模上使用的。这突显了在多个平台(尤其是移动平台)上对JSON Web令牌进行客户端处理的简便性。 [!TIP] 以上内容全部来自jwt官网介绍 go-zero中怎么使用jwt jwt鉴权一般在api层使用,我们这次演示工程中分别在user api登录时生成jwt token,在search api查询图书时验证用户jwt token两步来实现。 user api生成jwt token 接着业务编码章节的内容,我们完善上一节遗留的getJwtToken方法,即生成jwt token逻辑 添加配置定义和yaml配置项 $ vim service/user/api/internal/config/config.go type Config struct { rest.RestConf Mysql struct{ DataSource string } CacheRedis cache.CacheConf Auth struct { AccessSecret string AccessExpire int64 } } $ vim service/user/api/etc/user-api.yaml Name: user-api Host: 0.0.0.0 Port: 8888 Mysql: DataSource: $user:$password@tcp($url)/$db?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai CacheRedis: - Host: $host Pass: $pass Type: node Auth: AccessSecret: $AccessSecret AccessExpire: $AccessExpire [!TIP] $AccessSecret:生成jwt token的密钥,最简单的方式可以使用一个uuid值。 $AccessExpire:jwt token有效期,单位:秒 更多配置信息,请参考api配置介绍 $ vim service/user/api/internal/logic/loginlogic.go func (l *LoginLogic) getJwtToken(secretKey string, iat, seconds, userId int64) (string, error) { claims := make(jwt.MapClaims) claims[\"exp\"] = iat + seconds claims[\"iat\"] = iat claims[\"userId\"] = userId token := jwt.New(jwt.SigningMethodHS256) token.Claims = claims return token.SignedString([]byte(secretKey)) } search api使用jwt token鉴权 编写search.api文件 $ vim service/search/api/search.api type ( SearchReq { // 图书名称 Name string `form:\"name\"` } SearchReply { Name string `json:\"name\"` Count int `json:\"count\"` } ) @server( jwt: Auth ) service search-api { @handler search get /search/do (SearchReq) returns (SearchReply) } service search-api { @handler ping get /search/ping } [!TIP] jwt: Auth:开启jwt鉴权 如果路由需要jwt鉴权,则需要在service上方声明此语法标志,如上文中的/search/do 不需要jwt鉴权的路由就无需声明,如上文中/search/ping 更多语法请阅读api语法介绍 生成代码 前面已经描述过有三种方式去生成代码,这里就不赘述了。 添加yaml配置项 $ vim service/search/api/etc/search-api.yaml Name: search-api Host: 0.0.0.0 Port: 8889 Auth: AccessSecret: $AccessSecret AccessExpire: $AccessExpire [!TIP] $AccessSecret:这个值必须要和user api中声明的一致。 $AccessExpire: 有效期 这里修改一下端口,避免和user api端口8888冲突 验证 jwt token 启动user api服务,登录 $ cd service/user/api $ go run user.go -f etc/user-api.yaml Starting server at 0.0.0.0:8888... $ curl -i -X POST \\ http://127.0.0.1:8888/user/login \\ -H 'Content-Type: application/json' \\ -d '{ \"username\":\"666\", \"password\":\"123456\" }' 如果是在Windows的CMD里运行,命令格式如下: curl -i -X POST http://127.0.0.1:8888/user/login -H \"Content-Type: application/json\" -d \"{ \\\"username\\\":\\\"666\\\", \\\"password\\\":\\\"123456\\\" }\" 访问结果: HTTP/1.1 200 OK Content-Type: application/json Date: Mon, 08 Feb 2021 10:37:54 GMT Content-Length: 251 {\"id\":1,\"name\":\"小明\",\"gender\":\"男\",\"accessToken\":\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MTI4NjcwNzQsImlhdCI6MTYxMjc4MDY3NCwidXNlcklkIjoxfQ.JKa83g9BlEW84IiCXFGwP2aSd0xF3tMnxrOzVebbt80\",\"accessExpire\":1612867074,\"refreshAfter\":1612823874} 启动search api服务,调用/search/do验证jwt鉴权是否通过 $ go run search.go -f etc/search-api.yaml Starting server at 0.0.0.0:8889... 我们先不传jwt token,看看结果 $ curl -i -X GET \\ 'http://127.0.0.1:8889/search/do?name=%E8%A5%BF%E6%B8%B8%E8%AE%B0' HTTP/1.1 401 Unauthorized Date: Mon, 08 Feb 2021 10:41:57 GMT Content-Length: 0 很明显,jwt鉴权失败了,返回401的statusCode,接下来我们带一下jwt token(即用户登录返回的accessToken) $ curl -i -X GET \\ 'http://127.0.0.1:8889/search/do?name=%E8%A5%BF%E6%B8%B8%E8%AE%B0' \\ -H 'Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MTI4NjcwNzQsImlhdCI6MTYxMjc4MDY3NCwidXNlcklkIjoxfQ.JKa83g9BlEW84IiCXFGwP2aSd0xF3tMnxrOzVebbt80' HTTP/1.1 200 OK Content-Type: application/json Date: Mon, 08 Feb 2021 10:44:45 GMT Content-Length: 21 {\"name\":\"\",\"count\":0} [!TIP] 服务启动错误,请查看常见错误处理 至此,jwt从生成到使用就演示完成了,jwt token的鉴权是go-zero内部已经封装了,你只需在api文件中定义服务时简单的声明一下即可。 获取jwt token中携带的信息 go-zero从jwt token解析后会将用户生成token时传入的kv原封不动的放在http.Request的Context中,因此我们可以通过Context就可以拿到你想要的值 $ vim /service/search/api/internal/logic/searchlogic.go 添加一个log来输出从jwt解析出来的userId。 func (l *SearchLogic) Search(req types.SearchReq) (*types.SearchReply, error) { logx.Infof(\"userId: %v\",l.ctx.Value(\"userId\"))// 这里的key和生成jwt token时传入的key一致 return &types.SearchReply{}, nil } 运行结果 {\"@timestamp\":\"2021-02-09T10:29:09.399+08\",\"level\":\"info\",\"content\":\"userId: 1\"} 猜你想看 jwt介绍 api配置介绍 api语法 Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:49 "},"middleware.html":{"url":"middleware.html","title":"中间件使用","keywords":"","body":"中间件使用 在上一节,我们演示了怎么使用jwt鉴权,相信你已经掌握了对jwt的基本使用,本节我们来看一下api服务中间件怎么使用。 中间件分类 在go-zero中,中间件可以分为路由中间件和全局中间件,路由中间件是指某一些特定路由需要实现中间件逻辑,其和jwt类似,没有放在jwt:xxx下的路由不会使用中间件功能, 而全局中间件的服务范围则是整个服务。 中间件使用 这里以search服务为例来演示中间件的使用 路由中间件 重新编写search.api文件,添加middleware声明 $ cd service/search/api $ vim search.api type SearchReq struct {} type SearchReply struct {} @server( jwt: Auth middleware: Example // 路由中间件声明 ) service search-api { @handler search get /search/do (SearchReq) returns (SearchReply) } 重新生成api代码 $ goctl api go -api search.api -dir . etc/search-api.yaml exists, ignored generation internal/config/config.go exists, ignored generation search.go exists, ignored generation internal/svc/servicecontext.go exists, ignored generation internal/handler/searchhandler.go exists, ignored generation internal/handler/pinghandler.go exists, ignored generation internal/logic/searchlogic.go exists, ignored generation internal/logic/pinglogic.go exists, ignored generation Done. 生成完后会在internal目录下多一个middleware的目录,这里即中间件文件,后续中间件的实现逻辑也在这里编写。 完善资源依赖ServiceContext $ vim service/search/api/internal/svc/servicecontext.go type ServiceContext struct { Config config.Config Example rest.Middleware } func NewServiceContext(c config.Config) *ServiceContext { return &ServiceContext{ Config: c, Example: middleware.NewExampleMiddleware().Handle, } } 编写中间件逻辑 这里仅添加一行日志,内容example middle,如果服务运行输出example middle则代表中间件使用起来了。 $ vim service/search/api/internal/middleware/examplemiddleware.go package middleware import \"net/http\" type ExampleMiddleware struct { } func NewExampleMiddleware() *ExampleMiddleware { return &ExampleMiddleware{} } func (m *ExampleMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // TODO generate middleware implement function, delete after code implementation // Passthrough to next handler if need next(w, r) } } 启动服务验证 {\"@timestamp\":\"2021-02-09T11:32:57.931+08\",\"level\":\"info\",\"content\":\"example middle\"} 全局中间件 通过rest.Server提供的Use方法即可 func main() { flag.Parse() var c config.Config conf.MustLoad(*configFile, &c) ctx := svc.NewServiceContext(c) server := rest.MustNewServer(c.RestConf) defer server.Stop() // 全局中间件 server.Use(func(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { logx.Info(\"global middleware\") next(w, r) } }) handler.RegisterHandlers(server, ctx) fmt.Printf(\"Starting server at %s:%d...\\n\", c.Host, c.Port) server.Start() } {\"@timestamp\":\"2021-02-09T11:50:15.388+08\",\"level\":\"info\",\"content\":\"global middleware\"} 在中间件里调用其它服务 通过闭包的方式把其它服务传递给中间件,示例如下: // 模拟的其它服务 type AnotherService struct{} func (s *AnotherService) GetToken() string { return stringx.Rand() } // 常规中间件 func middleware(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { w.Header().Add(\"X-Middleware\", \"static-middleware\") next(w, r) } } // 调用其它服务的中间件 func middlewareWithAnotherService(s *AnotherService) rest.Middleware { return func(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { w.Header().Add(\"X-Middleware\", s.GetToken()) next(w, r) } } } 完整代码参考:https://github.com/zeromicro/zero-examples/tree/main/http/middleware Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:49 "},"rpc-call.html":{"url":"rpc-call.html","title":"rpc服务编写与调用","keywords":"","body":"rpc编写与调用 在一个大的系统中,多个子系统(服务)间必然存在数据传递,有数据传递就需要通信方式,你可以选择最简单的http进行通信,也可以选择rpc服务进行通信, 在go-zero,我们使用zrpc来进行服务间的通信,zrpc是基于grpc。 场景 在前面我们完善了对用户进行登录,用户查询图书等接口协议,但是用户在查询图书时没有做任何用户校验,如果当前用户是一个不存在的用户则我们不允许其查阅图书信息, 从上文信息我们可以得知,需要user服务提供一个方法来获取用户信息供search服务使用,因此我们就需要创建一个user rpc服务,并提供一个getUser方法。 rpc服务编写 编译proto文件 $ vim service/user/rpc/user.proto syntax = \"proto3\"; package user; option go_package = \"./user\"; message IdReq{ int64 id = 1; } message UserInfoReply{ int64 id = 1; string name = 2; string number = 3; string gender = 4; } service user { rpc getUser(IdReq) returns(UserInfoReply); } 生成rpc服务代码$ cd service/user/rpc $ goctl rpc protoc user.proto --go_out=./types --go-grpc_out=./types --zrpc_out=. [!TIPS] 如果安装的 protoc-gen-go 版大于1.4.0, proto文件建议加上go_package 添加配置及完善yaml配置项 $ vim service/user/rpc/internal/config/config.go type Config struct { zrpc.RpcServerConf Mysql struct { DataSource string } CacheRedis cache.CacheConf } $ vim /service/user/rpc/etc/user.yaml Name: user.rpc ListenOn: 127.0.0.1:8080 Etcd: Hosts: - $etcdHost Key: user.rpc Mysql: DataSource: $user:$password@tcp($url)/$db?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai CacheRedis: - Host: $host Pass: $pass Type: node [!TIP] $user: mysql数据库user $password: mysql数据库密码 $url: mysql数据库连接地址 $db: mysql数据库db名称,即user表所在database $host: redis连接地址 格式:ip:port,如:127.0.0.1:6379 $pass: redis密码 $etcdHost: etcd连接地址,格式:ip:port,如: 127.0.0.1:2379 更多配置信息,请参考rpc配置介绍 添加资源依赖 $ vim service/user/rpc/internal/svc/servicecontext.go type ServiceContext struct { Config config.Config UserModel model.UserModel } func NewServiceContext(c config.Config) *ServiceContext { conn := sqlx.NewMysql(c.Mysql.DataSource) return &ServiceContext{ Config: c, UserModel: model.NewUserModel(conn, c.CacheRedis), } } 添加rpc逻辑 $ service/user/rpc/internal/logic/getuserlogic.go func (l *GetUserLogic) GetUser(in *user.IdReq) (*user.UserInfoReply, error) { one, err := l.svcCtx.UserModel.FindOne(in.Id) if err != nil { return nil, err } return &user.UserInfoReply{ Id: one.Id, Name: one.Name, Number: one.Number, Gender: one.Gender, }, nil } 使用rpc 接下来我们在search服务中调用user rpc 添加UserRpc配置及yaml配置项 $ vim service/search/api/internal/config/config.go type Config struct { rest.RestConf Auth struct { AccessSecret string AccessExpire int64 } UserRpc zrpc.RpcClientConf } $ vim service/search/api/etc/search-api.yaml Name: search-api Host: 0.0.0.0 Port: 8889 Auth: AccessSecret: $AccessSecret AccessExpire: $AccessExpire UserRpc: Etcd: Hosts: - $etcdHost Key: user.rpc [!TIP] $AccessSecret:这个值必须要和user api中声明的一致。 $AccessExpire: 有效期 $etcdHost: etcd连接地址 etcd中的Key必须要和user rpc服务配置中Key一致 添加依赖 $ vim service/search/api/internal/svc/servicecontext.go type ServiceContext struct { Config config.Config Example rest.Middleware UserRpc user.User } func NewServiceContext(c config.Config) *ServiceContext { return &ServiceContext{ Config: c, Example: middleware.NewExampleMiddleware().Handle, UserRpc: user.NewUser(zrpc.MustNewClient(c.UserRpc)), } } 补充逻辑 $ vim /service/search/api/internal/logic/searchlogic.go func (l *SearchLogic) Search(req types.SearchReq) (*types.SearchReply, error) { userIdNumber := json.Number(fmt.Sprintf(\"%v\", l.ctx.Value(\"userId\"))) logx.Infof(\"userId: %s\", userIdNumber) userId, err := userIdNumber.Int64() if err != nil { return nil, err } // 使用user rpc _, err = l.svcCtx.UserRpc.GetUser(l.ctx, &user.IdReq{ Id: userId, }) if err != nil { return nil, err } return &types.SearchReply{ Name: req.Name, Count: 100, }, nil } 启动并验证服务 启动etcd、redis、mysql 启动user rpc $ cd service/user/rpc $ go run user.go -f etc/user.yaml Starting rpc server at 127.0.0.1:8080... 启动search api $ cd service/search/api $ go run search.go -f etc/search-api.yaml 验证服务 $ curl -i -X GET \\ 'http://127.0.0.1:8889/search/do?name=%E8%A5%BF%E6%B8%B8%E8%AE%B0' \\ -H 'authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MTI4NjcwNzQsImlhdCI6MTYxMjc4MDY3NCwidXNlcklkIjoxfQ.JKa83g9BlEW84IiCXFGwP2aSd0xF3tMnxrOzVebbt80' HTTP/1.1 200 OK Content -Type: application/json Date: Tue, 09 Feb 2021 06:05:52 GMT Content-Length: 32 {\"name\":\"西游记\",\"count\":100} 猜你想看 rpc配置 rpc服务目录 goctl rpc命令 Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:50 "},"error-handle.html":{"url":"error-handle.html","title":"错误处理","keywords":"","body":"错误处理 错误的处理是一个服务必不可缺的环节。在平时的业务开发中,我们可以认为http状态码不为2xx系列的,都可以认为是http请求错误, 并伴随响应的错误信息,但这些错误信息都是以plain text形式返回的。除此之外,我在业务中还会定义一些业务性错误,常用做法都是通过 code、msg 两个字段来进行业务处理结果描述,并且希望能够以json响应体来进行响应。 业务错误响应格式 业务处理正常 { \"code\": 0, \"msg\": \"successful\", \"data\": { .... } } 业务处理异常 { \"code\": 10001, \"msg\": \"参数错误\" } user api之login 在之前,我们在登录逻辑中处理用户名不存在时,直接返回来一个error。我们来登录并传递一个不存在的用户名看看效果。 curl -X POST \\ http://127.0.0.1:8888/user/login \\ -H 'content-type: application/json' \\ -d '{ \"username\":\"1\", \"password\":\"123456\" }' HTTP/1.1 400 Bad Request Content-Type: text/plain; charset=utf-8 X-Content-Type-Options: nosniff Date: Tue, 09 Feb 2021 06:38:42 GMT Content-Length: 19 用户名不存在 接下来我们将其以json格式进行返回 自定义错误 首先在common中添加一个baseerror.go文件,并填入代码 $ cd common $ mkdir errorx&&cd errorx $ vim baseerror.go package errorx const defaultCode = 1001 type CodeError struct { Code int `json:\"code\"` Msg string `json:\"msg\"` } type CodeErrorResponse struct { Code int `json:\"code\"` Msg string `json:\"msg\"` } func NewCodeError(code int, msg string) error { return &CodeError{Code: code, Msg: msg} } func NewDefaultError(msg string) error { return NewCodeError(defaultCode, msg) } func (e *CodeError) Error() string { return e.Msg } func (e *CodeError) Data() *CodeErrorResponse { return &CodeErrorResponse{ Code: e.Code, Msg: e.Msg, } } 将登录逻辑中错误用CodeError自定义错误替换 if len(strings.TrimSpace(req.Username)) == 0 || len(strings.TrimSpace(req.Password)) == 0 { return nil, errorx.NewDefaultError(\"参数错误\") } userInfo, err := l.svcCtx.UserModel.FindOneByNumber(req.Username) switch err { case nil: case model.ErrNotFound: return nil, errorx.NewDefaultError(\"用户名不存在\") default: return nil, err } if userInfo.Password != req.Password { return nil, errorx.NewDefaultError(\"用户密码不正确\") } now := time.Now().Unix() accessExpire := l.svcCtx.Config.Auth.AccessExpire jwtToken, err := l.getJwtToken(l.svcCtx.Config.Auth.AccessSecret, now, l.svcCtx.Config.Auth.AccessExpire, userInfo.Id) if err != nil { return nil, err } return &types.LoginReply{ Id: userInfo.Id, Name: userInfo.Name, Gender: userInfo.Gender, AccessToken: jwtToken, AccessExpire: now + accessExpire, RefreshAfter: now + accessExpire/2, }, nil 开启自定义错误 $ vim service/user/api/user.go func main() { flag.Parse() var c config.Config conf.MustLoad(*configFile, &c) ctx := svc.NewServiceContext(c) server := rest.MustNewServer(c.RestConf) defer server.Stop() handler.RegisterHandlers(server, ctx) // 自定义错误 httpx.SetErrorHandler(func(err error) (int, interface{}) { switch e := err.(type) { case *errorx.CodeError: return http.StatusOK, e.Data() default: return http.StatusInternalServerError, nil } }) fmt.Printf(\"Starting server at %s:%d...\\n\", c.Host, c.Port) server.Start() } 重启服务验证 $ curl -i -X POST \\ http://127.0.0.1:8888/user/login \\ -H 'content-type: application/json' \\ -d '{ \"username\":\"1\", \"password\":\"123456\" }' HTTP/1.1 200 OK Content-Type: application/json Date: Tue, 09 Feb 2021 06:47:29 GMT Content-Length: 40 {\"code\":1001,\"msg\":\"用户名不存在\"} Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:49 "},"ci-cd.html":{"url":"ci-cd.html","title":"CI/CD","keywords":"","body":"CI/CD 在软件工程中,CI/CD或CICD通常指的是持续集成和持续交付或持续部署的组合实践。 ——引自维基百科 CI可以做什么? 现代应用开发的目标是让多位开发人员同时处理同一应用的不同功能。但是,如果企业安排在一天内将所有分支源代码合并在一起(称为“合并日”),最终可能造成工作繁琐、耗时,而且需要手动完成。这是因为当一位独立工作的开发人员对应用进行更改时,有可能会与其他开发人员同时进行的更改发生冲突。如果每个开发人员都自定义自己的本地集成开发环境(IDE),而不是让团队就一个基于云的 IDE 达成一致,那么就会让问题更加雪上加霜。 持续集成(CI)可以帮助开发人员更加频繁地(有时甚至每天)将代码更改合并到共享分支或“主干”中。一旦开发人员对应用所做的更改被合并,系统就会通过自动构建应用并运行不同级别的自动化测试(通常是单元测试和集成测试)来验证这些更改,确保这些更改没有对应用造成破坏。这意味着测试内容涵盖了从类和函数到构成整个应用的不同模块。如果自动化测试发现新代码和现有代码之间存在冲突,CI 可以更加轻松地快速修复这些错误。 ——引自《CI/CD是什么?如何理解持续集成、持续交付和持续部署》 从概念上来看,CI/CD包含部署过程,我们这里将部署(CD)单独放在一节服务部署, 本节就以gitlab来做简单的CI(Run Unit Test)演示。 gitlab CI Gitlab CI/CD是Gitlab内置的软件开发工具,提供 持续集成(CI) 持续交付(CD) 持续部署(CD) 准备工作 gitlab安装 git安装 gitlab runner安装 开启gitlab CI 上传代码 在gitlab新建一个仓库go-zero-demo 将本地代码上传到go-zero-demo仓库 在项目根目录下创建.gitlab-ci.yaml文件,通过此文件可以创建一个pipeline,其会在代码仓库中有内容变更时运行,pipeline由一个或多个按照顺序运行, 每个阶段可以包含一个或者多个并行运行的job。 添加CI内容(仅供参考) stages: - analysis analysis: stage: analysis image: golang script: - go version && go env - go test -short $(go list ./...) | grep -v \"no test\" [!TIP] 以上CI为简单的演示,详细的gitlab CI请参考gitlab官方文档进行更丰富的CI集成。 参考文档 CI/CD 维基百科 CI/CD是什么?如何理解持续集成、持续交付和持续部署 Gitlab CI Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:49 "},"service-deployment.html":{"url":"service-deployment.html","title":"服务部署","keywords":"","body":"服务部署 本节通过jenkins来进行简单的服务部署到k8s演示。 准备工作 k8s集群安装 gitlab环境安装 jenkins环境安装 redis&mysql&nginx&etcd安装 goctl安装 [!TIP] goctl确保k8s每个node节点上都有 以上环境安装请自行google,这里不做篇幅介绍。 服务部署 1、gitlab代码仓库相关准备 1.1、添加SSH Key 进入gitlab,点击用户中心,找到Settings,在左侧找到SSH Keystab 1、在jenkins所在机器上查看公钥 $ cat ~/.ssh/id_rsa.pub 2、如果没有,则需要生成,如果存在,请跳转到第3步 $ ssh-keygen -t rsa -b 2048 -C \"[email protected]\" \"[email protected]\" 可以替换为自己的邮箱 完成生成后,重复第一步操作 3、将公钥添加到gitlab中 1.2、上传代码到gitlab仓库 新建工程go-zero-demo并上传代码,这里不做细节描述。 2、jenkins 2.1、添加凭据 查看jenkins所在机器的私钥,与前面gitlab公钥对应 $ cat id_rsa 进入jenkins,依次点击Manage Jenkins-> Manage Credentials 进入全局凭据页面,添加凭据,Username是一个标识,后面添加pipeline你知道这个标识是代表gitlab的凭据就行,Private Key`即上面获取的私钥 2.2、 添加全局变量 进入Manage Jenkins->Configure System,滑动到全局属性条目,添加docker私有仓库相关信息,如图为docker用户名、docker用户密码、docker私有仓库地址 [!TIP] docker_user 修改为你的docker用户名 docker_pass 修改为你的docker用户密码 docker_server 修改为你的docker服务器地址 这里我使用的私有仓库,如果没有云厂商提供的私有仓库使用,可以自行搭建一个私有仓库,这里就不赘述了,大家自行google。 2.3、配置git 进入Manage Jenkins->Global Tool Configureation,找到Git条目,填写jenkins所在机器git可执行文件所在path,如果没有的话,需要在jenkins插件管理中下载Git插件。 2.4、 添加一个Pipeline pipeline用于构建项目,从gitlab拉取代码->生成Dockerfile->部署到k8s均在这个步骤去做,这里是演示环境,为了保证部署流程顺利, 需要将jenkins安装在和k8s集群的其中过一个节点所在机器上,我这里安装在master上的。 获取凭据id 进入凭据页面,找到Username为gitlab的凭据id 进入jenkins首页,点击新建Item,名称为user 查看项目git地址 添加服务类型Choice Parameter,在General中勾选This project is parameterized,点击添加参数选择Choice Parameter,按照图中添加选择的值常量(api、rpc)及接收值的变量(type),后续在Pipeline script中会用到。 配置user,在user配置页面,向下滑动找到Pipeline script,填写脚本内容 pipeline { agent any parameters { gitParameter name: 'branch', type: 'PT_BRANCH', branchFilter: 'origin/(.*)', defaultValue: 'master', selectedValue: 'DEFAULT', sortMode: 'ASCENDING_SMART', description: '选择需要构建的分支' } stages { stage('服务信息') { steps { sh 'echo 分支:$branch' sh 'echo 构建服务类型:${JOB_NAME}-$type' } } stage('check out') { steps { checkout([$class: 'GitSCM', branches: [[name: '$branch']], doGenerateSubmoduleConfigurations: false, extensions: [], submoduleCfg: [], userRemoteConfigs: [[credentialsId: '${credentialsId}', url: '${gitUrl}']]]) } } stage('获取commit_id') { steps { echo '获取commit_id' git credentialsId: '${credentialsId}', url: '${gitUrl}' script { env.commit_id = sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim() } } } stage('goctl版本检测') { steps{ sh '/usr/local/bin/goctl -v' } } stage('Dockerfile Build') { steps{ sh '/usr/local/bin/goctl docker -go service/${JOB_NAME}/${type}/${JOB_NAME}.go' script{ env.image = sh(returnStdout: true, script: 'echo ${JOB_NAME}-${type}:${commit_id}').trim() } sh 'echo 镜像名称:${image}' sh 'docker build -t ${image} .' } } stage('上传到镜像仓库') { steps{ sh '/root/dockerlogin.sh' sh 'docker tag ${image} ${dockerServer}/${image}' sh 'docker push ${dockerServer}/${image}' } } stage('部署到k8s') { steps{ script{ env.deployYaml = sh(returnStdout: true, script: 'echo ${JOB_NAME}-${type}-deploy.yaml').trim() env.port=sh(returnStdout: true, script: '/root/port.sh ${JOB_NAME}-${type}').trim() } sh 'echo ${port}' sh 'rm -f ${deployYaml}' sh '/usr/local/bin/goctl kube deploy -secret dockersecret -replicas 2 -nodePort 3${port} -requestCpu 200 -requestMem 50 -limitCpu 300 -limitMem 100 -name ${JOB_NAME}-${type} -namespace hey-go-zero -image ${dockerServer}/${image} -o ${deployYaml} -port ${port}' sh '/usr/bin/kubectl apply -f ${deployYaml}' } } stage('Clean') { steps{ sh 'docker rmi -f ${image}' sh 'docker rmi -f ${dockerServer}/${image}' cleanWs notFailBuild: true } } } } [!TIP] ${credentialsId}要替换为你的具体凭据值,即【添加凭据】模块中的一串字符串,${gitUrl}需要替换为你代码的git仓库地址,其他的${xxx}形式的变量无需修改,保持原样即可。 port.sh参考内容如下 case $1 in \"user-api\") echo 1000 ;; \"user-rpc\") echo 1001 ;; \"course-api\") echo 1002 ;; \"course-rpc\") echo 1003 ;; \"selection-api\") echo 1004 esac 其中dockerlogin.sh内容 #!/bin/bash docker login --username=$docker-user --password=$docker-pass $docker-server $docker-user: docker登录用户名 $docker-pass: docker登录用户密码 $docker-server: docker私有地址 查看pipeline 查看k8s服务 猜你想看 goctl安装 k8s介绍 docker介绍 jenkins安装 jenkins pipeline nginx文档介绍 etcd文档说明 Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:50 "},"log-collection.html":{"url":"log-collection.html","title":"日志收集","keywords":"","body":"日志收集 为了保证业务稳定运行,预测服务不健康风险,日志的收集可以帮助我们很好的观察当前服务的健康状况, 在传统业务开发中,机器部署还不是很多时,我们一般都是直接登录服务器进行日志查看、调试,但随着业务的增大,服务的不断拆分, 服务的维护成本也会随之变得越来越复杂,在分布式系统中,服务器机子增多,服务分布在不同的服务器上,当遇到问题时, 我们不能使用传统做法,登录到服务器进行日志排查和调试,这个复杂度可想而知。 [!TIP] 如果是一个简单的单体服务系统或者服务过于小不建议直接使用,否则会适得其反。 准备工作 kafka elasticsearch kibana filebeat、Log-Pilot(k8s) go-stash filebeat配置 $ vim xx/filebeat.yaml filebeat.inputs: - type: log enabled: true # 开启json解析 json.keys_under_root: true json.add_error_key: true # 日志文件路径 paths: - /var/log/order/*.log setup.template.settings: index.number_of_shards: 1 # 定义kafka topic field fields: log_topic: log-collection # 输出到kafka output.kafka: hosts: [\"127.0.0.1:9092\"] topic: '%{[fields.log_topic]}' partition.round_robin: reachable_only: false required_acks: 1 keep_alive: 10s # ================================= Processors ================================= processors: - decode_json_fields: fields: ['@timestamp','level','content','trace','span','duration'] target: \"\" [!TIP] xx为filebeat.yaml所在路径 go-stash配置 新建config.yaml文件 添加配置内容 $ vim config.yaml Clusters: - Input: Kafka: Name: go-stash Log: Mode: file Brokers: - \"127.0.0.1:9092\" Topics: - log-collection Group: stash Conns: 3 Consumers: 10 Processors: 60 MinBytes: 1048576 MaxBytes: 10485760 Offset: first Filters: - Action: drop Conditions: - Key: status Value: \"503\" Type: contains - Key: type Value: \"app\" Type: match Op: and - Action: remove_field Fields: - source - _score - \"@metadata\" - agent - ecs - input - log - fields Output: ElasticSearch: Hosts: - \"http://127.0.0.1:9200\" Index: \"go-stash-{{yyyy.MM.dd}}\" MaxChunkBytes: 5242880 GracePeriod: 10s Compress: false TimeZone: UTC 启动服务(按顺序启动) 启动kafka 启动elasticsearch 启动kibana 启动go-stash 启动filebeat 启动order-api服务及其依赖服务(go-zero-demo工程中的order-api服务) 访问kibana 进入127.0.0.1:5601 [!TIP] 这里仅演示收集服务中通过logx产生的日志,nginx中日志收集同理。 参考文档 kafka elasticsearch kibana filebeat go-stash filebeat配置 Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:49 "},"trace.html":{"url":"trace.html","title":"链路追踪","keywords":"","body":"go-zero链路追踪 序言 微服务架构中,调用链可能很漫长,从 http 到 rpc ,又从 rpc 到 http 。而开发者想了解每个环节的调用情况及性能,最佳方案就是 全链路跟踪。 追踪的方法就是在一个请求开始时生成一个自己的 spanID ,随着整个请求链路传下去。我们则通过这个 spanID 查看整个链路的情况和性能问题。 下面来看看 go-zero 的链路实现。 代码结构 spancontext :保存链路的上下文信息「traceid,spanid,或者是其他想要传递的内容」 span :链路中的一个操作,存储时间和某些信息 propagator : trace 传播下游的操作「抽取,注入」 noop :实现了空的 tracer 实现 概念 SpanContext 在介绍 span 之前,先引入 context 。SpanContext 保存了分布式追踪的上下文信息,包括 Trace id,Span id 以及其它需要传递到下游的内容。OpenTracing 的实现需要将 SpanContext 通过某种协议 进行传递,以将不同进程中的 Span 关联到同一个 Trace 上。对于 HTTP 请求来说,SpanContext 一般是采用 HTTP header 进行传递的。 下面是 go-zero 默认实现的 spanContext type spanContext struct { traceId string // TraceID 表示tracer的全局唯一ID spanId string // SpanId 标示单个trace中某一个span的唯一ID,在trace中唯一 } 同时开发者也可以实现 SpanContext 提供的接口方法,实现自己的上下文信息传递: type SpanContext interface { TraceId() string // get TraceId SpanId() string // get SpanId Visit(fn func(key, val string) bool) // 自定义操作TraceId,SpanId } Span 一个 REST 调用或者数据库操作等,都可以作为一个 span 。 span 是分布式追踪的最小跟踪单位,一个 Trace 由多段 Span 组成。追踪信息包含如下信息: type Span struct { ctx spanContext // 传递的上下文 serviceName string // 服务名 operationName string // 操作 startTime time.Time // 开始时间戳 flag string // 标记开启trace是 server 还是 client children int // 本 span fork出来的 childsnums } 从 span 的定义结构来看:在微服务中, 这就是一个完整的子调用过程,有调用开始 startTime ,有标记自己唯一属性的上下文结构 spanContext 以及 fork 的子节点数。 实例应用 在 go-zero 中http,rpc中已经作为内置中间件集成。我们以 http ,rpc 中,看看 tracing 是怎么使用的: HTTP func TracingHandler(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // **1** carrier, err := trace.Extract(trace.HttpFormat, r.Header) // ErrInvalidCarrier means no trace id was set in http header if err != nil && err != trace.ErrInvalidCarrier { logx.Error(err) } // **2** ctx, span := trace.StartServerSpan(r.Context(), carrier, sysx.Hostname(), r.RequestURI) defer span.Finish() // **5** r = r.WithContext(ctx) next.ServeHTTP(w, r) }) } func StartServerSpan(ctx context.Context, carrier Carrier, serviceName, operationName string) ( context.Context, tracespec.Trace) { span := newServerSpan(carrier, serviceName, operationName) // **4** return context.WithValue(ctx, tracespec.TracingKey, span), span } func newServerSpan(carrier Carrier, serviceName, operationName string) tracespec.Trace { // **3** traceId := stringx.TakeWithPriority(func() string { if carrier != nil { return carrier.Get(traceIdKey) } return \"\" }, func() string { return stringx.RandId() }) spanId := stringx.TakeWithPriority(func() string { if carrier != nil { return carrier.Get(spanIdKey) } return \"\" }, func() string { return initSpanId }) return &Span{ ctx: spanContext{ traceId: traceId, spanId: spanId, }, serviceName: serviceName, operationName: operationName, startTime: timex.Time(), // 标记为server flag: serverFlag, } } 将 header -> carrier,获取 header 中的traceId等信息 开启一个新的 span,并把「traceId,spanId」封装在context中 从上述的 carrier「也就是header」获取traceId,spanId 看header中是否设置 如果没有设置,则随机生成返回 从 request 中产生新的ctx,并将相应的信息封装在 ctx 中,返回 从上述的 context,拷贝一份到当前的 request 这样就实现了 span 的信息随着 request 传递到下游服务。 RPC 在 rpc 中存在 client, server ,所以从 tracing 上也有 clientTracing, serverTracing 。 serveTracing 的逻辑基本与 http 的一致,来看看 clientTracing 是怎么使用的? func TracingInterceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { // open clientSpan ctx, span := trace.StartClientSpan(ctx, cc.Target(), method) defer span.Finish() var pairs []string span.Visit(func(key, val string) bool { pairs = append(pairs, key, val) return true }) // **3** 将 pair 中的data以map的形式加入 ctx ctx = metadata.AppendToOutgoingContext(ctx, pairs...) return invoker(ctx, method, req, reply, cc, opts...) } func StartClientSpan(ctx context.Context, serviceName, operationName string) (context.Context, tracespec.Trace) { // **1** if span, ok := ctx.Value(tracespec.TracingKey).(*Span); ok { // **2** return span.Fork(ctx, serviceName, operationName) } return ctx, emptyNoopSpan } 获取上游带下来的 span 上下文信息 从获取的 span 中创建新的 ctx,span「继承父span的traceId」 将生成 span 的data加入ctx,传递到下一个中间件,流至下游 总结 go-zero 通过拦截请求获取链路traceID,然后在中间件函数入口会分配一个根Span,然后在后续操作中会分裂出子Span,每个span都有自己的具体的标识,Finsh之后就会汇集在链路追踪系统中。开发者可以通过 ELK 工具追踪 traceID ,看到整个调用链。 同时 go-zero 并没有提供整套 trace 链路方案,开发者可以封装 go-zero 已有的 span 结构,做自己的上报系统,接入 jaeger, zipkin 等链路追踪工具。 参考 go-zero trace Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:50 "},"service-monitor.html":{"url":"service-monitor.html","title":"服务监控","keywords":"","body":"服务监控 在微服务治理中,服务监控也是非常重要的一个环节,监控一个服务是否正常工作,需要从多维度进行,如: mysql指标 mongo指标 redis指标 请求日志 服务指标统计 服务健康检测 ... 监控的工作非常大,本节仅以其中的服务指标监控作为例子进行说明。 基于prometheus的微服务指标监控 服务上线后我们往往需要对服务进行监控,以便能及早发现问题并做针对性的优化,监控又可分为多种形式,比如日志监控,调用链监控,指标监控等等。而通过指标监控能清晰的观察出服务指标的变化趋势,了解服务的运行状态,对于保证服务稳定起着非常重要的作用 prometheus是一个开源的系统监控和告警工具,支持强大的查询语言PromQL允许用户实时选择和汇聚时间序列数据,时间序列数据是服务端通过HTTP协议主动拉取获得,也可以通过中间网关来推送时间序列数据,可以通过静态配置文件或服务发现来获取监控目标 Prometheus 的架构 Prometheus 的整体架构以及生态系统组件如下图所示: Prometheus Server直接从监控目标中或者间接通过推送网关来拉取监控指标,它在本地存储所有抓取到样本数据,并对此数据执行一系列规则,以汇总和记录现有数据的新时间序列或生成告警。可以通过 Grafana 或者其他工具来实现监控数据的可视化 go-zero基于prometheus的服务指标监控 go-zero 框架中集成了基于prometheus的服务指标监控,下面我们通过go-zero官方的示例shorturl来演示是如何对服务指标进行收集监控的: 第一步需要先安装Prometheus,安装步骤请参考官方文档 go-zero默认不开启prometheus监控,开启方式很简单,只需要在shorturl-api.yaml文件中增加配置如下,其中Host为Prometheus Server地址为必填配置,Port端口不填默认9091,Path为用来拉取指标的路径默认为/metrics Prometheus: Host: 127.0.0.1 Port: 9091 Path: /metrics 编辑prometheus的配置文件prometheus.yml,添加如下配置,并创建targets.json - job_name: 'file_ds' file_sd_configs: - files: - targets.json 编辑targets.json文件,其中targets为shorturl配置的目标地址,并添加了几个默认的标签 [ { \"targets\": [\"127.0.0.1:9091\"], \"labels\": { \"job\": \"shorturl-api\", \"app\": \"shorturl-api\", \"env\": \"test\", \"instance\": \"127.0.0.1:8888\" } } ] 启动prometheus服务,默认侦听在9090端口 $ prometheus --config.file=prometheus.yml 在浏览器输入http://127.0.0.1:9090/,然后点击Status -> Targets即可看到状态为Up的Job,并且Lables栏可以看到我们配置的默认的标签 通过以上几个步骤我们完成了prometheus对shorturl服务的指标监控收集的配置工作,为了演示简单我们进行了手动的配置,在实际的生产环境中一般采用定时更新配置文件或者服务发现的方式来配置监控目标,篇幅有限这里不展开讲解,感兴趣的同学请自行查看相关文档 go-zero监控的指标类型 go-zero目前在http的中间件和rpc的拦截器中添加了对请求指标的监控。 主要从请求耗时和请求错误两个维度,请求耗时采用了Histogram指标类型定义了多个Buckets方便进行分位统计,请求错误采用了Counter类型,并在http metric中添加了path标签rpc metric中添加了method标签以便进行细分监控。 接下来演示如何查看监控指标: 首先在命令行多次执行如下命令 $ curl -i \"http://localhost:8888/shorten?url=http://www.xiaoheiban.cn\" 打开Prometheus切换到Graph界面,在输入框中输入{path=\"/shorten\"}指令,即可查看监控指标,如下图 我们通过PromQL语法查询过滤path为/shorten的指标,结果中显示了指标名以及指标数值,其中http_server_requests_code_total指标中code值为http的状态码,200表明请求成功,http_server_requests_duration_ms_bucket中对不同bucket结果分别进行了统计,还可以看到所有的指标中都添加了我们配置的默认指标 Console界面主要展示了查询的指标结果,Graph界面为我们提供了简单的图形化的展示界面,在实际的生产环境中我们一般使用Grafana做图形化的展示 grafana可视化界面 grafana是一款可视化工具,功能强大,支持多种数据来源Prometheus、Elasticsearch、Graphite等,安装比较简单请参考官方文档,grafana默认端口3000,安装好后再浏览器输入http://localhost:3000/,默认账号和密码都为admin 下面演示如何基于以上指标进行可视化界面的绘制: 点击左侧边栏Configuration->Data Source->Add data source进行数据源添加,其中HTTP的URL为数据源的地址 点击左侧边栏添加dashboard,然后添加Variables方便针对不同的标签进行过滤筛选比如添加app变量用来过滤不同的服务 进入dashboard点击右上角Add panel添加面板,以path维度统计接口的qps 最终的效果如下所示,可以通过服务名称过滤不同的服务,面板展示了path为/shorten的qps变化趋势 总结 以上演示了go-zero中基于prometheus+grafana服务指标监控的简单流程,生产环境中可以根据实际的场景做不同维度的监控分析。现在go-zero的监控指标主要还是针对http和rpc,这对于服务的整体监控显然还是不足的,比如容器资源的监控,依赖的mysql、redis等资源的监控,以及自定义的指标监控等等,go-zero在这方面后续还会持续优化。希望这篇文章能够给您带来帮助 Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:50 "},"goctl.html":{"url":"goctl.html","title":"Goctl","keywords":"","body":"Goctl goctl是go-zero微服务框架下的代码生成工具。使用 goctl 可显著提升开发效率,让开发人员将时间重点放在业务开发上,其功能有: api服务生成 rpc服务生成 model代码生成 模板管理 本节将包含以下内容: 自动补全设置 命令大全 api命令 rpc命令 model命令 plugin命令 其他命令 goctl 读音 很多人会把 goctl 读作 go-C-T-L,这种是错误的念法,应参照 go control 读做 ɡō kənˈtrōl。 查看版本信息 $ goctl -v 如果安装了goctl则会输出以下格式的文本信息: goctl version ${version} ${os}/${arch} 例如输出: goctl version 1.1.5 darwin/amd64 版本号说明 version:goctl 版本号 os:当前操作系统名称 arch: 当前系统架构名称 安装 goctl 方式一(go get) # Go 1.15 及之前版本 GO111MODULE=on GOPROXY=https://goproxy.cn/,direct go get -u github.com/zeromicro/go-zero/tools/goctl@latest # Go 1.16 及以后版本 GOPROXY=https://goproxy.cn/,direct go install github.com/zeromicro/go-zero/tools/goctl@latest 通过此命令可以将goctl工具安装到 $GOPATH/bin 目录下 方式二 (fork and build) 从 go-zero代码仓库 [email protected]:zeromicro/go-zero.git 拉取一份源码,进入 tools/goctl/目录下编译一下 goctl 文件,然后将其添加到环境变量中。 安装完成后执行goctl -v,如果输出版本信息则代表安装成功,例如: $ goctl -v goctl version 1.1.4 darwin/amd64 常见问题 command not found: goctl 请确保goctl已经安装,或者goctl是否已经正确添加到当前shell的环境变量中。 Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:49 "},"goctl-completion.html":{"url":"goctl-completion.html","title":"自动补全设置","keywords":"","body":"goctl自动补全 goctl 自动补全仅支持 unix-like 操作系统 用法 $ goctl completion -h NAME: goctl completion - generation completion script, it only works for unix-like OS USAGE: goctl completion [command options] [arguments...] OPTIONS: --name value, -n value the filename of auto complete script, default is [goctl_autocomplete] 生成自动补全文件 $ goctl completion generation auto completion success! executes the following script to setting shell: echo PROG=goctl source /Users/keson/.goctl/.auto_complete/zsh/goctl_autocomplete >> ~/.zshrc && source ~/.zshrc or echo PROG=goctl source /Users/keson/.goctl/.auto_complete/bash/goctl_autocomplete >> ~/.bashrc && source ~/.bashrc shell 配置 zsh$ echo PROG=goctl source /Users/keson/.goctl/.auto_complete/zsh/goctl_autocomplete >> ~/.zshrc && source ~/.zshrc bash$ echo PROG=goctl source /Users/keson/.goctl/.auto_complete/bash/goctl_autocomplete >> ~/.bashrc && source ~/.bashrc 演示效果 使用 tab 键出现自动补全提示 $ goctl api -- generate api related files bug -- report a bug completion -- generation completion script, it only works for unix-like OS docker -- generate Dockerfile help h -- Shows a list of commands or help for one command kube -- generate kubernetes files migrate -- migrate from tal-tech to zeromicro model -- generate model code rpc -- generate rpc code template -- template operation upgrade -- upgrade goctl to latest version Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:49 "},"goctl-commands.html":{"url":"goctl-commands.html","title":"命令大全","keywords":"","body":"goctl命令大全 goctl bug (报告一个错误) upgrade (将goctl升级到最新版本) env (检查或编辑goctl环境) --write, -w: 编辑goctl环境 check (检测goctl环境和依赖性工具) --force, -f: 默许安装不存在的依赖项 --install, -i: 如果没有找到,就安装依赖工具 migrate (从tal-tech迁移到zeromicro) --verbose, -v: verbose可以实现额外的日志记录 --version: 要迁移的github.com/zeromicro/go-zero的目标版本。 api (生成api相关文件) --branch:远程版本库的分支,它与--remote一起工作。 --home:模板的goctl首页路径,--home和--remote不能同时设置,如果它们同时设置,--remote的优先级更高 --remote:模板的远程git repo,--home和--remote不能同时设置,如果同时设置,--remote的优先级更高 git repo目录必须与https://github.com/zeromicro/go-zero-template 目录结构一致 -o:输出api文件 new (快速创建api服务) --branch:远程repo的分支,它与--remote一起工作。 --home: 模板的goctl首页路径,--home和--remote不能同时设置,如果设置了,--remote的优先级更高 --remote:模板的远程git repo,--home和--remote不能同时设置,如果同时设置,--remote的优先级更高 git repo目录必须与https://github.com/zeromicro/go-zero-template 目录结构一致 --style:文件的命名格式,见[https://github.com/zeromicro/go-zero/blob/master/tools/goctl/config/readme.md] format (格式化api文件) --declare:用于跳过检查已经声明的api类型 --dir: 格式目标目录 --iu: 忽略更新 --stdin:使用stdin输入api文件内容,按 \"ctrl + d \"发送EOF。 validate (验证api文件) --api: 验证目标api文件 doc (生成文档文件) --dir: 目标目录 --o: 输出markdown目录 go (提供的api生成go文件) --api: api文件 --branch: 远程 repo 的分支,它与 --remote 一起工作。 --dir: 目标目录 --home: 模板的goctl首页路径,--home和--remote不能同时设置,如果它们同时设置,--remote的优先级更高 --remote:模板的远程git repo,--home和--remote不能同时设置,如果同时设置,--remote的优先级更高 git repo目录必须与https://github.com/zeromicro/go-zero-template 目录结构一致 --style:文件的命名格式,见[https://github.com/zeromicro/go-zero/tree/master/tools/goctl/config/readme.md] java (为api文件中提供的api生成java文件) --api: api文件 --dir: 目标目录 ts (为api文件中提供的api生成ts文件) --api: api文件 --caller: 网络api调用者 --dir: 目标目录 --unwrap: 解除webapi调用器的包装,以便导入 --webapi: web api文件的路径 dart (为api文件中提供的api生成dart文件) --api: api文件 --dir: 目标目录 --hostname: 服务器的主机名 --legacy: 用于flutter v1的传统生成器 kt (为提供的api文件生成kotlin代码) --api: api文件 --dir: 目标目录 --pkg: 定义kotlin文件的包名 plugin (自定义文件生成器) --api: api文件 --dir: 目标目录 --plugin, -p: 插件文件 --style: 文件的命名格式,见[https://github.com/zeromicro/go-zero/tree/master/tools/goctl/config/readme.md] docker (生成Docker文件) --branch:远程版本库的分支,它与--remote一起工作。 --go:包含主函数的文件 --home:模板的goctl首页路径,--home和--remote不能同时设置,如果它们同时设置,--remote的优先级更高 --port:要公开的端口,默认为无(默认:0)。 --remote:模板的远程git repo,--home和--remote不能同时设置,如果它们同时设置,--remote的优先级更高。 git repo目录必须与https://github.com/zeromicro/go-zero-template 目录结构一致 --scratch:使用scratch作为基础docker镜像 --tz:容器的时区(默认:亚洲/上海) --version:goctl builder golang镜像的版本。 kube (生成kubernetes文件) deploy (生成部署yaml文件) --branch:远程repo的分支,它与--remote一起工作。 --home:模板的goctl首页路径,--home和--remote不能同时设置,如果设置了,--remote的优先级更高 --image:部署的docker镜像 --limitCpu:部署的cpu上限(默认为1000)。 --limitMem: 部署的内存上限(默认为1024)。 --maxReplicas: 部署的最大复制数(默认为10)。 --minReplicas: 部署的最小复制量(默认为3)。 --name:部署的名称 --namespace:部署的命名空间 --nodePort: 要公开的部署的nodePort(默认为0)。 --port: 要在pod上监听的部署的端口(默认值:0) --remote:模板的远程git repo,--home和--remote不能同时设置,如果它们同时设置,--remote有更高的优先级。 git repo目录必须与https://github.com/zeromicro/go-zero-template 目录结构一致 --replicas:要部署的副本数量(默认:3个)。 --requestCpu:要部署的请求cpu(默认为500)。 --requestMem: 要部署的请求内存(默认为512)。 --revisions: 限制修订历史的数量(默认为5)。 --secret: 从注册表中提取镜像的秘密。 --serviceAccount:部署的ServiceAccount。 -o: 输出的yaml文件 rpc (生成rpc代码) new (生成rpc演示服务) --branch: 远程版本库的分支,它与--remote一起工作。 --home:模板的goctl首页路径,--home和--remote不能同时设置,如果同时设置,--remote的优先级更高 --idea:命令执行环境是否来自idea插件。[可选] --remote:模板的远程git repo,--home和--remote不能同时设置,如果同时设置,--remote的优先级更高 git repo目录必须与https://github.com/zeromicro/go-zero-template 目录结构一致 --style:文件的命名格式,见[https://github.com/zeromicro/go-zero/tree/master/tools/goctl/config/readme.md] template (生成proto模板) --branch:远程repo的分支,它与--remote一起工作。 --home:模板的goctl主路径,--home和--remote不能同时设置,如果设置了,--remote的优先级更高 --out, -o: proto的目标路径 --remote:模板的远程git repo,--home和--remote不能同时设置,如果有的话,--remote的优先级更高 git repo目录必须与https://github.com/zeromicro/go-zero-template 目录结构一致 protoc (生成grpc代码) --branch:远程repo的分支,它与--remote一起工作。 --home: 模板的goctl主路径 --remote: 模板的远程git repo,--home和--remote不能同时设置,如果它们同时设置,--remote的优先级更高。 git repo目录必须与https://github.com/zeromicro/go-zero-template 目录结构一致 --style:文件的命名格式,见[https://github.com/zeromicro/go-zero/tree/master/tools/goctl/config/readme.md] --zrpc_out:zrpc的输出目录 model (生成model代码) mysql (生成mysql模型) ddl (从ddl生成mysql模型) - --branch:远程 repo 的分支,它与 --remote 一起工作。 - --cache, -c:生成带有缓存的代码[可选] 。 - --database, --db:数据库的名称 [可选] - --dir, -d: 目标目录 - --home:模板的goctl首页路径,--home和--remote不能同时设置,如果设置了,--remote的优先级更高 - --idea:用于理念插件[可选] - --remote:模板的远程git repo,--home和--remote不能同时设置,如果同时设置,--remote的优先级更高 git repo目录必须与https://github.com/zeromicro/go-zero-template 目录结构一致 --src, -s:ddl的路径或路径globbing模式 --style:文件的命名格式,见[https://github.com/zeromicro/go-zero/tree/master/tools/goctl/config/readme.md] datasource (从数据源生成模型) - --branch:远程 repo 的分支,它与 --remote 一起工作。 - --cache, -c: 使用缓存生成代码 [可选] - --dir, -d:目标目录 - --home:模板的goctl首页路径,--home和--remote不能同时设置,如果它们同时设置,--remote的优先级更高 - --idea:用于理念插件[可选] - --remote:模板的远程git repo,--home和--remote不能同时设置,如果同时设置,--remote的优先级更高 git repo目录必须与https://github.com/zeromicro/go-zero-template 目录结构一致 --style:文件的命名格式,见[https://github.com/zeromicro/go-zero/tree/master/tools/goctl/config/readme.md] --table, -t:数据库中的表或表球化模式 --url:数据库的数据源,如 \"root:password@tcp(127.0.0.1:3306)/database\" pg (生成postgresql模型) datasource (从数据源生成模型) - --branch:远程 repo 的分支,它与 --remote 一起工作。 - --cache, -c:生成带有缓存的代码[可选] 。 - --dir, -d:目标目录 - --home:模板的goctl首页路径,--home和--remote不能同时设置,如果它们同时设置,--remote的优先级更高 - --idea:用于理念插件[可选] - --remote:模板的远程git repo,--home和--remote不能同时设置,如果同时设置,--remote的优先级更高 git repo目录必须与https://github.com/zeromicro/go-zero-template 目录结构一致 --schema, -s:表的模式,默认为[public] --style:文件的命名格式,见[https://github.com/zeromicro/go-zero/tree/master/tools/goctl/config/readme.md] --table, -t: 数据库中的表或表球化模式 --url:数据库的数据源,如 \"postgres://root:[email protected]:5432/database?sslmode=disable\" mongo (生成mongo模型) --branch:远程repo的分支,它与--remote一起工作。 --cache, -c: 使用缓存生成代码 [可选] --dir, -d:目标目录 --home:模板的goctl首页路径,--home和--remote不能同时设置,如果它们同时设置,--remote的优先级更高 --remote:模板的远程git repo,--home和--remote不能同时设置,如果同时设置,--remote的优先级更高 git repo目录必须与https://github.com/zeromicro/go-zero-template 目录结构一致 --style:文件的命名格式,见[https://github.com/zeromicro/go-zero/tree/master/tools/goctl/config/readme.md] --type, -t:指定的模型类型名称 template (模板操作) init (初始化所有模板(强制更新)) --home: 模板的goctl主路径 clean (清理所有缓存的模板) --home: 模板的goctl主路径 update (将目标类别的模板更新为最新的) --category, -c: 模板的类别,枚举[api,rpc,model,docker,kube] --home: 模板的goctl主页路径 revert (将目标模板恢复到最新版本) --category, -c: 模板的类别,枚举[api,rpc,model,docker,kube] 。 --home:模板的goctl主路径 --name, -n: 模板的目标文件名 completion (生成自动补全脚本,它只适用于类unix操作系统) --name, -n:自动完成脚本的文件名,默认为[goctl_autocomplete] Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:49 "},"goctl-api.html":{"url":"goctl-api.html","title":"api命令","keywords":"","body":"api命令 goctl api是goctl中的核心模块之一,其可以通过.api文件一键快速生成一个api服务,如果仅仅是启动一个go-zero的api演示项目, 你甚至都不用编码,就可以完成一个api服务开发及正常运行。在传统的api项目中,我们要创建各级目录,编写结构体, 定义路由,添加logic文件,这一系列操作,如果按照一条协议的业务需求计算,整个编码下来大概需要5~6分钟才能真正进入业务逻辑的编写, 这还不考虑编写过程中可能产生的各种错误,而随着服务的增多,随着协议的增多,这部分准备工作的时间将成正比上升, 而goctl api则可以完全替代你去做这一部分工作,不管你的协议要定多少个,最终来说,只需要花费10秒不到即可完成。 [!TIP] 其中的结构体编写,路由定义用api进行替代,因此总的来说,省去的是你创建文件夹、添加各种文件及资源依赖的过程的时间。 api命令说明 $ goctl api -h NAME: goctl api - generate api related files USAGE: goctl api command [command options] [arguments...] COMMANDS: new fast create api service format format api files validate validate api file doc generate doc files go generate go files for provided api in yaml file java generate java files for provided api in api file ts generate ts files for provided api in api file dart generate dart files for provided api in api file kt generate kotlin code for provided api file plugin custom file generator OPTIONS: -o value the output api file --help, -h show help 从上文中可以看到,根据功能的不同,api包含了很多的自命令和flag,我们这里重点说明一下 go子命令,其功能是生成golang api服务,我们通过goctl api go -h看一下使用帮助: $ goctl api go -h NAME: goctl api go - generate go files for provided api in yaml file USAGE: goctl api go [command options] [arguments...] OPTIONS: --dir value the target dir --api value the api file --style value the file naming format, see [https://github.com/zeromicro/go-zero/tree/master/tools/goctl/config/readme.md] --dir 代码输出目录 --api 指定api源文件 --style 指定生成代码文件的文件名称风格,详情见文件名称命名style说明 使用示例 $ goctl api go -api user.api -dir . -style gozero 猜你想看 api语法 api目录 Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:49 "},"goctl-rpc.html":{"url":"goctl-rpc.html","title":"rpc命令","keywords":"","body":"rpc命令 Goctl Rpc是goctl脚手架下的一个rpc服务代码生成模块,支持proto模板生成和rpc服务代码生成,通过此工具生成代码你只需要关注业务逻辑编写而不用去编写一些重复性的代码。这使得我们把精力重心放在业务上,从而加快了开发效率且降低了代码出错率。 特性 简单易用 快速提升开发效率 出错率低 贴近protoc 快速开始 方式一:快速生成greet服务 通过命令 goctl rpc new ${servieName}生成 如生成greet rpc服务: goctl rpc new greet 执行后代码结构如下: . ├── etc │ └── greet.yaml ├── go.mod ├── go.sum ├── greet │ ├── greet.go │ ├── greet.pb.go │ └── greet_grpc.pb.go ├── greet.go ├── greet.proto └── internal ├── config │ └── config.go ├── logic │ └── pinglogic.go ├── server │ └── greetserver.go └── svc └── servicecontext.go [!TIP] 新版本目录详见 rpc目录 方式二:通过指定proto生成rpc服务 生成proto模板 goctl rpc template -o=user.proto syntax = \"proto3\"; package user; option go_package=\"./user\"; message Request { string ping = 1; } message Response { string pong = 1; } service User { rpc Ping(Request) returns(Response); } 生成rpc服务代码 $ goctl rpc protoc user.proto --go_out=. --go-grpc_out=. --zrpc_out=. 准备工作 安装了go环境 安装了protoc & protoc-gen-go,并且已经设置环境变量 更多问题请见 注意事项 用法 rpc服务生成用法 goctl rpc protoc -h NAME: goctl rpc protoc - generate grpc code USAGE: example: goctl rpc protoc xx.proto --go_out=./pb --go-grpc_out=./pb --zrpc_out=. DESCRIPTION: for details, see https://go-zero.dev/cn/goctl-rpc.html OPTIONS: --zrpc_out value the zrpc output directory --style value the file naming format, see [https://github.com/zeromicro/go-zero/tree/master/tools/goctl/config/readme.md] --home value the goctl home path of the template --remote value the remote git repo of the template, --home and --remote cannot be set at the same time, if they are, --remote has higher priority The git repo directory must be consistent with the https://github.com/zeromicro/go-zero-template directory structure --branch value the branch of the remote repo, it does work with --remote 参数说明 --zrpc_out 可选,默认为proto文件所在目录,生成代码的目标目录 --style 可选,输出目录的文件命名风格,详情见https://github.com/zeromicro/go-zero/tree/master/tools/goctl/config/readme.md --home 可选,指定模板路径 --remote 可选,指定模板远程仓库 --branch 可选,指定模板远程仓库分支,与 --remote 配合使用 你可以理解为 zrpc 代码生成是用 goctl rpc $protoc_command --zrpc_out=${output} 模板,如原来生成 grpc 代码指令为 $ protoc user.proto --go_out=. --go-grpc_out=. 则生成 zrpc 代码指令就为 $ goctl rpc protoc user.proto --go_out=. --go-grpc_out=. --zrpc_out=. [!TIP] --go_out 与 --go-grpc_out 生成的最终目录必须一致 --go_out & --go-grpc_out 和 --zrpc_out 的生成的最终目录必须不为同一目录,否则pb.go和_grpc.pb.go就与main函数同级了,这是不允许的。 --go_out 与 --go-grpc_out 生产的目录会受 --go_opt 和 --grpc-go_opt 和proto源文件中 go_package值的影响,要想理解这里的生成逻辑,建议阅读 官方文文档:Go Generated Code 开发人员需要做什么 关注业务代码编写,将重复性、与业务无关的工作交给goctl,生成好rpc服务代码后,开发人员仅需要修改 服务中的配置文件编写(etc/xx.json、internal/config/config.go) 服务中业务逻辑编写(internal/logic/xxlogic.go) 服务中资源上下文的编写(internal/svc/servicecontext.go) 注意事项 proto暂不支持多文件同时生成 proto不支持外部依赖包引入,message不支持inline 目前main文件、shared文件、handler文件会被强制覆盖,而和开发人员手动需要编写的则不会覆盖生成,这一类在代码头部均有 // Code generated by goctl. DO NOT EDIT! // Source: xxx.proto 的标识,请注意不要在里面写业务性代码;也不要将它写在业务性代码里面。 proto import 对于rpc中的requestType和returnType必须在main proto文件定义,对于proto中的message可以像protoc一样import其他proto文件。 proto示例: 错误import syntax = \"proto3\"; package greet; option go_package = \"./greet\"; import \"base/common.proto\"; message Request { string ping = 1; } message Response { string pong = 1; } service Greet { rpc Ping(base.In) returns(base.Out);// request和return 不支持import } 正确import syntax = \"proto3\"; package greet; option go_package = \"./greet\"; import \"base/common.proto\"; message Request { base.In in = 1;// 支持import } message Response { base.Out out = 2;// 支持import } service Greet { rpc Ping(Request) returns(Response); } 猜你想看 rpc目录 rpc配置 rpc调用 Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:49 "},"goctl-model.html":{"url":"goctl-model.html","title":"model命令","keywords":"","body":"model命令 goctl model 为go-zero下的工具模块中的组件之一,目前支持识别mysql ddl进行model层代码生成,通过命令行或者idea插件(即将支持)可以有选择地生成带redis cache或者不带redis cache的代码逻辑。 快速开始 通过ddl生成 $ goctl model mysql ddl -src=\"./*.sql\" -dir=\"./sql/model\" -c 执行上述命令后即可快速生成CURD代码。 model ├── usermodel.go ├── usermodel_gen.go └── vars.go 通过datasource生成 $ goctl model mysql datasource -url=\"user:password@tcp(127.0.0.1:3306)/database\" -table=\"*\" -dir=\"./model\" usermodel_gen.go // Code generated by goctl. DO NOT EDIT! package model import ( \"context\" \"database/sql\" \"fmt\" \"strings\" \"time\" \"github.com/zeromicro/go-zero/core/stores/builder\" \"github.com/zeromicro/go-zero/core/stores/cache\" \"github.com/zeromicro/go-zero/core/stores/sqlc\" \"github.com/zeromicro/go-zero/core/stores/sqlx\" \"github.com/zeromicro/go-zero/core/stringx\" ) var ( userFieldNames = builder.RawFieldNames(&User{}) userRows = strings.Join(userFieldNames, \",\") userRowsExpectAutoSet = strings.Join(stringx.Remove(userFieldNames, \"`id`\", \"`create_time`\", \"`update_time`\"), \",\") userRowsWithPlaceHolder = strings.Join(stringx.Remove(userFieldNames, \"`id`\", \"`create_time`\", \"`update_time`\"), \"=?,\") + \"=?\" cacheUserIdPrefix = \"cache:user:id:\" cacheUserNumberPrefix = \"cache:user:number:\" ) type ( userModel interface { Insert(ctx context.Context, data *User) (sql.Result, error) FindOne(ctx context.Context, id int64) (*User, error) FindOneByNumber(ctx context.Context, number string) (*User, error) Update(ctx context.Context, data *User) error Delete(ctx context.Context, id int64) error } defaultUserModel struct { sqlc.CachedConn table string } User struct { Id int64 `db:\"id\"` Number string `db:\"number\"` // 学号 Name string `db:\"name\"` // 用户名称 Password string `db:\"password\"` // 用户密码 Gender string `db:\"gender\"` // 男|女|未公开 CreateTime time.Time `db:\"create_time\"` UpdateTime time.Time `db:\"update_time\"` } ) func newUserModel(conn sqlx.SqlConn, c cache.CacheConf) *defaultUserModel { return &defaultUserModel{ CachedConn: sqlc.NewConn(conn, c), table: \"`user`\", } } func (m *defaultUserModel) Insert(ctx context.Context, data *User) (sql.Result, error) { userIdKey := fmt.Sprintf(\"%s%v\", cacheUserIdPrefix, data.Id) userNumberKey := fmt.Sprintf(\"%s%v\", cacheUserNumberPrefix, data.Number) ret, err := m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { query := fmt.Sprintf(\"insert into %s (%s) values (?, ?, ?, ?)\", m.table, userRowsExpectAutoSet) return conn.ExecCtx(ctx, query, data.Number, data.Name, data.Password, data.Gender) }, userIdKey, userNumberKey) return ret, err } func (m *defaultUserModel) FindOne(ctx context.Context, id int64) (*User, error) { userIdKey := fmt.Sprintf(\"%s%v\", cacheUserIdPrefix, id) var resp User err := m.QueryRowCtx(ctx, &resp, userIdKey, func(ctx context.Context, conn sqlx.SqlConn, v interface{}) error { query := fmt.Sprintf(\"select %s from %s where `id` = ? limit 1\", userRows, m.table) return conn.QueryRowCtx(ctx, v, query, id) }) switch err { case nil: return &resp, nil case sqlc.ErrNotFound: return nil, ErrNotFound default: return nil, err } } func (m *defaultUserModel) FindOneByNumber(ctx context.Context, number string) (*User, error) { userNumberKey := fmt.Sprintf(\"%s%v\", cacheUserNumberPrefix, number) var resp User err := m.QueryRowIndexCtx(ctx, &resp, userNumberKey, m.formatPrimary, func(ctx context.Context, conn sqlx.SqlConn, v interface{}) (i interface{}, e error) { query := fmt.Sprintf(\"select %s from %s where `number` = ? limit 1\", userRows, m.table) if err := conn.QueryRowCtx(ctx, &resp, query, number); err != nil { return nil, err } return resp.Id, nil }, m.queryPrimary) switch err { case nil: return &resp, nil case sqlc.ErrNotFound: return nil, ErrNotFound default: return nil, err } } func (m *defaultUserModel) Update(ctx context.Context, data *User) error { userIdKey := fmt.Sprintf(\"%s%v\", cacheUserIdPrefix, data.Id) userNumberKey := fmt.Sprintf(\"%s%v\", cacheUserNumberPrefix, data.Number) _, err := m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { query := fmt.Sprintf(\"update %s set %s where `id` = ?\", m.table, userRowsWithPlaceHolder) return conn.ExecCtx(ctx, query, data.Number, data.Name, data.Password, data.Gender, data.Id) }, userIdKey, userNumberKey) return err } func (m *defaultUserModel) Delete(ctx context.Context, id int64) error { data, err := m.FindOne(ctx, id) if err != nil { return err } userIdKey := fmt.Sprintf(\"%s%v\", cacheUserIdPrefix, id) userNumberKey := fmt.Sprintf(\"%s%v\", cacheUserNumberPrefix, data.Number) _, err = m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { query := fmt.Sprintf(\"delete from %s where `id` = ?\", m.table) return conn.ExecCtx(ctx, query, id) }, userIdKey, userNumberKey) return err } func (m *defaultUserModel) formatPrimary(primary interface{}) string { return fmt.Sprintf(\"%s%v\", cacheUserIdPrefix, primary) } func (m *defaultUserModel) queryPrimary(ctx context.Context, conn sqlx.SqlConn, v, primary interface{}) error { query := fmt.Sprintf(\"select %s from %s where `id` = ? limit 1\", userRows, m.table) return conn.QueryRowCtx(ctx, v, query, primary) } func (m *defaultUserModel) tableName() string { return m.table } usermodel.go package model import ( \"github.com/zeromicro/go-zero/core/stores/cache\" \"github.com/zeromicro/go-zero/core/stores/sqlx\" ) var _ UserModel = (*customUserModel)(nil) type ( // UserModel is an interface to be customized, add more methods here, // and implement the added methods in customUserModel. UserModel interface { userModel } customUserModel struct { *defaultUserModel } ) // NewUserModel returns a model for the database table. func NewUserModel(conn sqlx.SqlConn, c cache.CacheConf) UserModel { return &customUserModel{ defaultUserModel: newUserModel(conn, c), } } 用法 $ goctl model mysql -h NAME: goctl model mysql - generate mysql model\" USAGE: goctl model mysql command [command options] [arguments...] COMMANDS: ddl generate mysql model from ddl\" datasource generate model from datasource\" OPTIONS: --help, -h show help 生成规则 默认规则 我们默认用户在建表时会创建createTime、updateTime字段(忽略大小写、下划线命名风格)且默认值均为CURRENT_TIMESTAMP,而updateTime支持ON UPDATE CURRENT_TIMESTAMP,对于这两个字段生成insert、update时会被移除,不在赋值范畴内,当然,如果你不需要这两个字段那也无大碍。 ddl NAME: goctl model mysql ddl - generate mysql model from ddl USAGE: goctl model mysql ddl [command options] [arguments...] OPTIONS: --src value, -s value the path or path globbing patterns of the ddl --dir value, -d value the target dir --style value the file naming format, see [https://github.com/zeromicro/go-zero/tree/master/tools/goctl/config/readme.md] --cache, -c generate code with cache [optional] --idea for idea plugin [optional] --database value, --db value the name of database [optional] --home value the goctl home path of the template, --home and --remote cannot be set at the same time, if they are, --remote has higher priority --remote value the remote git repo of the template, --home and --remote cannot be set at the same time, if they are, --remote has higher priority The git repo directory must be consistent with the https://github.com/zeromicro/go-zero-template directory structure --branch value the branch of the remote repo, it does work with --remote datasource $ goctl model mysql datasource -h 13:40:46 羽106ms NAME: goctl model mysql datasource - generate model from datasource USAGE: goctl model mysql datasource [command options] [arguments...] OPTIONS: --url value the data source of database,like \"root:password@tcp(127.0.0.1:3306)/database\" --table value, -t value the table or table globbing patterns in the database --cache, -c generate code with cache [optional] --dir value, -d value the target dir --style value the file naming format, see [https://github.com/zeromicro/go-zero/tree/master/tools/goctl/config/readme.md] --idea for idea plugin [optional] --home value the goctl home path of the template, --home and --remote cannot be set at the same time, if they are, --remote has higher priority --remote value the remote git repo of the template, --home and --remote cannot be set at the same time, if they are, --remote has higher priority The git repo directory must be consistent with the https://github.com/zeromicro/go-zero-template directory structure --branch value the branch of the remote repo, it does work with --remote 生成代码仅基本的CURD结构。 缓存 对于缓存这一块我选择用一问一答的形式进行罗列。我想这样能够更清晰的描述model中缓存的功能。 缓存会缓存哪些信息? 对于主键字段缓存,会缓存整个结构体信息,而对于单索引字段(除全文索引)则缓存主键字段值。 数据有更新(update)操作会清空缓存吗? 会,但仅清空主键缓存的信息,why?这里就不做详细赘述了。 为什么不按照单索引字段生成updateByXxx和deleteByXxx的代码? 理论上是没任何问题,但是我们认为,对于model层的数据操作均是以整个结构体为单位,包括查询,我不建议只查询某部分字段(不反对),否则我们的缓存就没有意义了。 为什么不支持findPageLimit、findAll这种模式代码生成? 目前,我认为除了基本的CURD外,其他的代码均属于业务型代码,这个我觉得开发人员根据业务需要进行编写更好。 类型转换规则 mysql dataType golang dataType golang dataType(if null&&default null) bool int64 sql.NullInt64 boolean int64 sql.NullInt64 tinyint int64 sql.NullInt64 smallint int64 sql.NullInt64 mediumint int64 sql.NullInt64 int int64 sql.NullInt64 integer int64 sql.NullInt64 bigint int64 sql.NullInt64 float float64 sql.NullFloat64 double float64 sql.NullFloat64 decimal float64 sql.NullFloat64 date time.Time sql.NullTime datetime time.Time sql.NullTime timestamp time.Time sql.NullTime time string sql.NullString year time.Time sql.NullInt64 char string sql.NullString varchar string sql.NullString binary string sql.NullString varbinary string sql.NullString tinytext string sql.NullString text string sql.NullString mediumtext string sql.NullString longtext string sql.NullString enum string sql.NullString set string sql.NullString json string sql.NullString Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:49 "},"goctl-plugin.html":{"url":"goctl-plugin.html","title":"plugin命令","keywords":"","body":"plugin命令 goctl支持针对api自定义插件,那我怎么来自定义一个插件了?来看看下面最终怎么使用的一个例子。 $ goctl api plugin -p goctl-android=\"android -package com.tal\" -api user.api -dir . 上面这个命令可以分解成如下几步: goctl 解析api文件 goctl 将解析后的结构 ApiSpec 和参数传递给goctl-android可执行文件 goctl-android 根据 ApiSpec 结构体自定义生成逻辑。 此命令前面部分 goctl api plugin -p 是固定参数,goctl-android=\"android -package com.tal\" 是plugin参数,其中goctl-android是插件二进制文件,android -package com.tal是插件的自定义参数,-api user.api -dir .是goctl通用自定义参数。 怎么编写自定义插件? go-zero框架中包含了一个很简单的自定义插件 demo,代码如下: package main import ( \"fmt\" \"github.com/zeromicro/go-zero/tools/goctl/plugin\" ) func main() { plugin, err := plugin.NewPlugin() if err != nil { panic(err) } if plugin.Api != nil { fmt.Printf(\"api: %+v \\n\", plugin.Api) } fmt.Printf(\"dir: %s \\n\", plugin.Dir) fmt.Println(\"Enjoy anything you want.\") } plugin, err := plugin.NewPlugin() 这行代码作用是解析从goctl传递过来的数据,里面包含如下部分内容: type Plugin struct { Api *spec.ApiSpec Style string Dir string } [!TIP] Api:定义了api文件的结构数据 Style:可选参数,可以用来控制文件命名规范 Dir:工作目录 完整的基于plugin实现的android plugin演示项目 https://github.com/zeromicro/goctl-android 猜你想看 api目录 api语法 api配置 api命令介绍 Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:49 "},"goctl-other.html":{"url":"goctl-other.html","title":"其他命令","keywords":"","body":"其他命令 goctl docker goctl kube goctl docker goctl docker 可以极速生成一个 Dockerfile,帮助开发/运维人员加快部署节奏,降低部署复杂度。 准备工作 docker安装 Dockerfile 额外注意点 选择最简单的镜像:比如alpine,整个镜像5M左右 设置镜像时区RUN apk add --no-cache tzdata ENV TZ Asia/Shanghai 多阶段构建 第一阶段构建出可执行文件,确保构建过程独立于宿主机 第二阶段将第一阶段的输出作为输入,构建出最终的极简镜像 Dockerfile编写过程 首先安装 goctl 工具 $ GO111MODULE=on GOPROXY=https://goproxy.cn/,direct go get -u github.com/zeromicro/go-zero/tools/goctl 在 greet 项目下创建一个 hello 服务 $ goctl api new hello 文件结构如下: greet ├── go.mod ├── go.sum └── service └── hello ├── Dockerfile ├── etc │ └── hello-api.yaml ├── hello.api ├── hello.go └── internal ├── config │ └── config.go ├── handler │ ├── hellohandler.go │ └── routes.go ├── logic │ └── hellologic.go ├── svc │ └── servicecontext.go └── types └── types.go 在 hello 目录下一键生成 Dockerfile$ goctl docker -go hello.go Dockerfile 内容如下: FROM golang:alpine AS builder LABEL stage=gobuilder ENV CGO_ENABLED 0 ENV GOOS linux ENV GOPROXY https://goproxy.cn,direct WORKDIR /build/zero ADD go.mod . ADD go.sum . RUN go mod download COPY . . COPY service/hello/etc /app/etc RUN go build -ldflags=\"-s -w\" -o /app/hello service/hello/hello.go FROM alpine RUN apk update --no-cache RUN apk add --no-cache ca-certificates RUN apk add --no-cache tzdata ENV TZ Asia/Shanghai WORKDIR /app COPY --from=builder /app/hello /app/hello COPY --from=builder /app/etc /app/etc CMD [\"./hello\", \"-f\", \"etc/hello-api.yaml\"] 在 hello 目录下 build 镜像 $ docker build -t hello:v1 -f service/hello/Dockerfile . 查看镜像 hello v1 5455f2eaea6b 7 minutes ago 18.1MB 可以看出镜像大小约为18M。 启动服务$ docker run --rm -it -p 8888:8888 hello:v1 测试服务$ curl -i http://localhost:8888/from/you HTTP/1.1 200 OK Content-Type: application/json Date: Thu, 10 Dec 2020 06:03:02 GMT Content-Length: 14 {\"message\":\"\"} goctl docker总结 goctl 工具极大简化了 Dockerfile 文件的编写,提供了开箱即用的最佳实践,并且支持了模板自定义。 goctl kube goctl kube提供了快速生成一个 k8s 部署文件的功能,可以加快开发/运维人员的部署进度,减少部署复杂度。 头疼编写 K8S 部署文件? K8S yaml 参数很多,需要边写边查? 保留回滚版本数怎么设? 如何探测启动成功,如何探活? 如何分配和限制资源? 如何设置时区?否则打印日志是 GMT 标准时间 如何暴露服务供其它服务调用? 如何根据 CPU 和内存使用率来配置水平伸缩? 首先,你需要知道有这些知识点,其次要把这些知识点都搞明白也不容易,再次,每次编写依然容易出错! 创建服务镜像 为了演示,这里我们以 redis:6-alpine 镜像为例。 完整 K8S 部署文件编写过程 首先安装 goctl 工具 $ GO111MODULE=on GOPROXY=https://goproxy.cn/,direct go get -u github.com/zeromicro/go-zero/tools/goctl 一键生成 K8S 部署文件 $ goctl kube deploy -name redis -namespace adhoc -image redis:6-alpine -o redis.yaml -port 6379 生成的 yaml 文件如下: apiVersion: apps/v1 kind: Deployment metadata: name: redis namespace: adhoc labels: app: redis spec: replicas: 3 revisionHistoryLimit: 5 selector: matchLabels: app: redis template: metadata: labels: app: redis spec: containers: - name: redis image: redis:6-alpine lifecycle: preStop: exec: command: [\"sh\",\"-c\",\"sleep 5\"] ports: - containerPort: 6379 readinessProbe: tcpSocket: port: 6379 initialDelaySeconds: 5 periodSeconds: 10 livenessProbe: tcpSocket: port: 6379 initialDelaySeconds: 15 periodSeconds: 20 resources: requests: cpu: 500m memory: 512Mi limits: cpu: 1000m memory: 1024Mi volumeMounts: - name: timezone mountPath: /etc/localtime volumes: - name: timezone hostPath: path: /usr/share/zoneinfo/Asia/Shanghai --- apiVersion: v1 kind: Service metadata: name: redis-svc namespace: adhoc spec: ports: - port: 6379 selector: app: redis --- apiVersion: autoscaling/v2beta1 kind: HorizontalPodAutoscaler metadata: name: redis-hpa-c namespace: adhoc labels: app: redis-hpa-c spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: redis minReplicas: 3 maxReplicas: 10 metrics: - type: Resource resource: name: cpu targetAverageUtilization: 80 --- apiVersion: autoscaling/v2beta1 kind: HorizontalPodAutoscaler metadata: name: redis-hpa-m namespace: adhoc labels: app: redis-hpa-m spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: redis minReplicas: 3 maxReplicas: 10 metrics: - type: Resource resource: name: memory targetAverageUtilization: 80 部署服务,如果 adhoc namespace 不存在的话,请先通过 kubectl create namespace adhoc 创建 $ kubectl apply -f redis.yaml deployment.apps/redis created service/redis-svc created horizontalpodautoscaler.autoscaling/redis-hpa-c created horizontalpodautoscaler.autoscaling/redis-hpa-m created 查看服务允许状态 $ kubectl get all -n adhoc NAME READY STATUS RESTARTS AGE pod/redis-585bc66876-5ph26 1/1 Running 0 6m5s pod/redis-585bc66876-bfqxz 1/1 Running 0 6m5s pod/redis-585bc66876-vvfc9 1/1 Running 0 6m5s NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/redis-svc ClusterIP 172.24.15.8 6379/TCP 6m5s NAME READY UP-TO-DATE AVAILABLE AGE deployment.apps/redis 3/3 3 3 6m6s NAME DESIRED CURRENT READY AGE replicaset.apps/redis-585bc66876 3 3 3 6m6s NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE horizontalpodautoscaler.autoscaling/redis-hpa-c Deployment/redis 0%/80% 3 10 3 6m6s horizontalpodautoscaler.autoscaling/redis-hpa-m Deployment/redis 0%/80% 3 10 3 6m6s 测试服务$ kubectl run -i --tty --rm cli --image=redis:6-alpine -n adhoc -- sh /data # redis-cli -h redis-svc redis-svc:6379> set go-zero great OK redis-svc:6379> get go-zero \"great\" goctl kube 总结 goctl 工具极大简化了 K8S yaml 文件的编写,提供了开箱即用的最佳实践,并且支持了模板自定义。 猜你想看 准备工作 api目录 api语法 api配置 api命令介绍 docker介绍 k8s介绍 Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:49 "},"template-manage.html":{"url":"template-manage.html","title":"模板管理","keywords":"","body":"模板管理 模板操作 自定义模板 Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:50 "},"template-cmd.html":{"url":"template-cmd.html","title":"模板操作","keywords":"","body":"模板操作 模板(Template)是数据驱动生成的基础,所有的代码(rest api、rpc、model、docker、kube)生成都会依赖模板, 默认情况下,模板生成器会选择内存中的模板进行生成,而对于有模板修改需求的开发者来讲,则需要将模板进行落盘, 从而进行模板修改,在下次代码生成时会加载指定路径下的模板进行生成。 使用帮助 NAME: goctl template - template operation USAGE: goctl template command [command options] [arguments...] COMMANDS: init initialize the all templates(force update) clean clean the all cache templates update update template of the target category to the latest revert revert the target template to the latest OPTIONS: --help, -h show help 模板初始化 NAME: goctl template init - initialize the all templates(force update) USAGE: goctl template init [command options] [arguments...] OPTIONS: --home value the goctl home path of the template 清除模板 NAME: goctl template clean - clean the all cache templates USAGE: goctl template clean [command options] [arguments...] OPTIONS: --home value the goctl home path of the template 回滚指定分类模板 NAME: goctl template update - update template of the target category to the latest USAGE: goctl template update [command options] [arguments...] OPTIONS: --category value, -c value the category of template, enum [api,rpc,model,docker,kube] --home value the goctl home path of the template 回滚模板 NAME: goctl template revert - revert the target template to the latest USAGE: goctl template revert [command options] [arguments...] OPTIONS: --category value, -c value the category of template, enum [api,rpc,model,docker,kube] --name value, -n value the target file name of template --home value the goctl home path of the template [!TIP] --home 指定模板存储路径 模板加载 在代码生成时可以通过--home来指定模板所在文件夹,目前已支持指定模板目录的命令有: goctl api go 详情可以通过goctl api go --help查看帮助 goctl docker 详情可以通过goctl docker --help查看帮助 goctl kube 详情可以通过goctl kube --help查看帮助 goctl rpc new 详情可以通过goctl rpc new --help查看帮助 goctl rpc proto 详情可以通过goctl rpc proto --help查看帮助 goctl model mysql ddl 详情可以通过goctl model mysql ddl --help查看帮助 goctl model mysql datasource 详情可以通过goctl model mysql datasource --help查看帮助 goctl model pg datasource 详情可以通过goctl model pg datasource --help查看帮助 goctl model mongo 详情可以通过goctl model mongo --help查看帮助 默认情况(在不指定--home)会从$HOME/.goctl目录下读取。 使用示例 初始化模板到指定$HOME/template目录下$ goctl template init --home $HOME/template Templates are generated in /Users/anqiansong/template, edit on your risk! 使用$HOME/template模板进行greet rpc生成$ goctl rpc new greet --home $HOME/template Done Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:50 "},"template.html":{"url":"template.html","title":"自定义模板","keywords":"","body":"模板修改 场景 实现统一格式的body响应,格式如下: { \"code\": 0, \"msg\": \"OK\", \"data\": {} // ① } ① 实际响应数据 [!TIP] go-zero生成的代码没有对其进行处理 准备工作 我们提前在module为greet的工程下的response包中写一个Response方法,目录树类似如下: greet ├── response │ └── response.go └── xxx... 代码如下 package response import ( \"net/http\" \"github.com/zeromicro/go-zero/rest/httpx\" ) type Body struct { Code int `json:\"code\"` Msg string `json:\"msg\"` Data interface{} `json:\"data,omitempty\"` } func Response(w http.ResponseWriter, resp interface{}, err error) { var body Body if err != nil { body.Code = -1 body.Msg = err.Error() } else { body.Msg = \"OK\" body.Data = resp } httpx.OkJson(w, body) } 修改handler模板 $ vim ~/.goctl/api/handler.tpl 将模板替换为以下内容 package handler import ( \"net/http\" \"greet/response\"// ① {% raw %} {{.ImportPackages}} {% endraw %} ) {% raw %} func {{.HandlerName}}(ctx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { {{if .HasRequest}}var req types.{{.RequestType}} if err := httpx.Parse(r, &req); err != nil { httpx.Error(w, err) return }{{end}} l := logic.New{{.LogicType}}(r.Context(), ctx) {{if .HasResp}}resp, {{end}}err := l.{{.Call}}({{if .HasRequest}}req{{end}}) {{if .HasResp}}response.Response(w, resp, err){{else}}response.Response(w, nil, err){{end}}//② } } {% endraw %} ① 替换为你真实的response包名,仅供参考 ② 自定义模板内容 [!TIP] 1.如果本地没有~/.goctl/api/handler.tpl文件,可以通过模板初始化命令goctl template init进行初始化 修改模板前后对比 修改前 func GreetHandler(ctx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req types.Request if err := httpx.Parse(r, &req); err != nil { httpx.Error(w, err) return } l := logic.NewGreetLogic(r.Context(), ctx) resp, err := l.Greet(req) // 以下内容将被自定义模板替换 if err != nil { httpx.Error(w, err) } else { httpx.OkJson(w, resp) } } } 修改后 func GreetHandler(ctx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req types.Request if err := httpx.Parse(r, &req); err != nil { httpx.Error(w, err) return } l := logic.NewGreetLogic(r.Context(), ctx) resp, err := l.Greet(req) response.Response(w, resp, err) } } 修改模板前后响应体对比 修改前 { \"message\": \"Hello go-zero!\" } 修改后 { \"code\": 0, \"msg\": \"OK\", \"data\": { \"message\": \"Hello go-zero!\" } } 总结 本文档仅对http相应为例讲述了自定义模板的流程,除此之外,自定义模板的场景还有: model 层添加kmq model 层生成待有效期option的model实例 http自定义相应格式 ... Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:50 "},"extended-reading.html":{"url":"extended-reading.html","title":"扩展阅读","keywords":"","body":"扩展阅读 扩展阅读是对go-zero 中的最佳实现和组件的介绍, 因此会比较庞大,而此资源将会持续更新,也欢迎大家来进行文档贡献,本节将包含以下目录(按照文档更新时间排序): 快速构建高并发微服务 日志组件介绍 布隆过滤器 executors 流处理组件 fx go-zero mysql使用介绍 redis锁 periodlimit限流 令牌桶限流 时间轮介绍 熔断原理与实现 进程内缓存组件 collection.Cache 高效的关键词替换和敏感词过滤工具 服务自适应降载保护设计 文本序列化和反序列化 并发处理工具 MapReduce 基于prometheus的微服务指标监控 防止缓存击穿之进程内共享调用 DB缓存机制 zrpc 使用介绍 go-zero缓存设计之持久层缓存 go-zero缓存设计之业务层缓存 go-zero分布式定时任务 我是如何用go-zero 实现一个中台系统 流数据处理利器 10月3日线上交流问题汇总 Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:49 "},"logx.html":{"url":"logx.html","title":"日志组件介绍","keywords":"","body":"logx 使用示例 var c logx.LogConf // 从 yaml 文件中 初始化配置 conf.MustLoad(\"config.yaml\", &c) // logx 根据配置初始化 logx.MustSetup(c) logx.Info(\"This is info!\") logx.Infof(\"This is %s!\", \"info\") logx.Error(\"This is error!\") logx.Errorf(\"this is %s!\", \"error\") logx.Close() 初始化 logx 有很多可以配置项,可以参考 logx.LogConf 中的定义。目前可以使用 logx.MustSetUp(c) 进行初始化配置,如果没有进行初始化配置,所有的配置将使用默认配置。 Level logx 支持的打印日志级别有: alert info error severe fatal slow stat 可以使用对应的方法打印出对应级别的日志。 同时为了方便调试,线上使用,可以动态调整日志打印级别,其中可以通过 logx.SetLevel(uint32) 进行级别设置,也可以通过配置初始化进行设置。目前支持的参数为: const ( // 打印所有级别的日志 InfoLevel = iota // 打印 errors, slows, stacks 日志 ErrorLevel // 仅打印 severe 级别日志 SevereLevel ) 日志模式 目前日志打印模式主要分为2种,一种文件输出,一种控制台输出。推荐方式,当采用 k8s,docker 等部署方式的时候,可以将日志输出到控制台,使用日志收集器收集导入至 es 进行日志分析。如果是直接部署方式,可以采用文件输出方式,logx 会自动在指定文件目录创建对应 5 个对应级别的的日志文件保存日志。 . ├── access.log ├── error.log ├── severe.log ├── slow.log └── stat.log 同时会按照自然日进行文件分割,当超过指定配置天数,会对日志文件进行自动删除,打包等操作。 禁用日志 如果不需要日志打印,可以使用 logx.Close() 关闭日志输出。注意,当禁用日志输出,将无法在次打开,具体可以参考 logx.RotateLogger 和 logx.DailyRotateRule 的实现。 关闭日志 因为 logx 采用异步进行日志输出,如果没有正常关闭日志,可能会造成部分日志丢失的情况。必须在程序退出的地方关闭日志输出: logx.Close() 框架中 rest 和 zrpc 等大部分地方已经做好了日志配置和关闭相关操作,用户可以不用关心。 同时注意,当关闭日志输出之后,将无法在次打印日志了。 推荐写法: import \"github.com/zeromicro/go-zero/core/proc\" // grace close log proc.AddShutdownListener(func() { logx.Close() }) Duration 我们打印日志的时候可能需要打印耗时情况,可以使用 logx.WithDuration(time.Duration), 参考如下示例: startTime := timex.Now() // 数据库查询 rows, err := conn.Query(q, args...) duration := timex.Since(startTime) if duration > slowThreshold { logx.WithDuration(duration).Slowf(\"[SQL] query: slowcall - %s\", stmt) } else { logx.WithDuration(duration).Infof(\"sql query: %s\", stmt) } 会输出如下格式 {\"@timestamp\":\"2020-09-12T01:22:55.552+08\",\"level\":\"info\",\"duration\":\"3.0ms\",\"content\":\"sql query:...\"} {\"@timestamp\":\"2020-09-12T01:22:55.552+08\",\"level\":\"slow\",\"duration\":\"500ms\",\"content\":\"[SQL] query: slowcall - ...\"} 这样就可以很容易统计出慢 sql 相关信息。 TraceLog tracingEntry 是为了链路追踪日志输出定制的。可以打印 context 中的 traceId 和 spanId 信息,配合我们的 rest 和 zrpc 很容易完成链路日志的相关打印。示例如下 logx.WithContext(context.Context).Info(\"This is info!\") SysLog 应用中可能有部分采用系统 log 进行日志打印,logx 同样封装方法,很容易将 log 相关的日志收集到 logx 中来。 logx.CollectSysLog() 日志配置相关 LogConf 定义日志系统所需的基本配置 完整定义如下: type LogConf struct { ServiceName string `json:\",optional\"` Mode string `json:\",default=console,options=console|file|volume\"` Path string `json:\",default=logs\"` Level string `json:\",default=info,options=info|error|severe\"` Compress bool `json:\",optional\"` KeepDays int `json:\",optional\"` StackCooldownMillis int `json:\",default=100\"` } Mode Mode 定义了日志打印的方式。默认的模式是 console, 打印到控制台上面。 目前支持的模式如下: console 打印到控制台 file 打印到指定路径下的access.log, error.log, stat.log等文件里 volume 为了在k8s内打印到mount进来的存储上,因为多个pod可能会覆盖相同的文件,volume模式自动识别pod并按照pod分开写各自的日志文件 Path Path 定义了文件日志的输出路径,默认值为 logs。 Level Level 定义了日志打印级别,默认值为 info。 目前支持的级别如下: info error severe Compress Compress 定义了日志是否需要压缩,默认值为 false。在 Mode 为 file 模式下面,文件最后会进行打包压缩成 .gz 文件。 KeepDays KeepDays 定义日志最大保留天数,默认值为 0,表示不会删除旧的日志。在 Mode 为 file 模式下面,如果超过了最大保留天数,旧的日志文件将会被删除。 StackCooldownMillis StackCooldownMillis 定义了日志输出间隔,默认为 100 毫秒。 Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:49 "},"bloom.html":{"url":"bloom.html","title":"布隆过滤器","keywords":"","body":"bloom go-zero微服务框架中提供了许多开箱即用的工具,好的工具不仅能提升服务的性能而且还能提升代码的鲁棒性避免出错,实现代码风格的统一方便他人阅读等等,本系列文章将分别介绍go-zero框架中工具的使用及其实现原理 布隆过滤器bloom 在做服务器开发的时候,相信大家有听过布隆过滤器,可以判断某元素在不在集合里面,因为存在一定的误判和删除复杂问题,一般的使用场景是:防止缓存击穿(防止恶意攻击)、 垃圾邮箱过滤、cache digests 、模型检测器等、判断是否存在某行数据,用以减少对磁盘访问,提高服务的访问性能。 go-zero 提供的简单的缓存封装 bloom.bloom,简单使用方式如下 // 初始化 redisBitSet store := redis.New(\"redis 地址\", func(r *redis.Redis) { r.Type = redis.NodeType }) // 声明一个bitSet, key=\"test_key\"名且bits是1024位 bitSet := newRedisBitSet(store, \"test_key\", 1024) // 判断第0位bit存不存在 isSetBefore, err := bitSet.check([]uint{0}) // 对第512位设置为1 err = bitSet.set([]uint{512}) // 3600秒后过期 err = bitSet.expire(3600) // 删除该bitSet err = bitSet.del() bloom 简单介绍了最基本的redis bitset 的使用。下面是真正的bloom实现。 对元素hash 定位 // 对元素进行hash 14次(const maps=14),每次都在元素后追加byte(0-13),然后进行hash. // 将locations[0-13] 进行取模,最终返回locations. func (f *BloomFilter) getLocations(data []byte) []uint { locations := make([]uint, maps) for i := uint(0); i 向bloom里面add 元素 // 我们可以发现 add方法使用了getLocations和bitSet的set方法。 // 我们将元素进行hash成长度14的uint切片,然后进行set操作存到redis的bitSet里面。 func (f *BloomFilter) Add(data []byte) error { locations := f.getLocations(data) err := f.bitSet.set(locations) if err != nil { return err } return nil } 检查bloom里面是否有某元素 // 我们可以发现 Exists方法使用了getLocations和bitSet的check方法 // 我们将元素进行hash成长度14的uint切片,然后进行bitSet的check验证,存在返回true,不存在或者check失败返回false func (f *BloomFilter) Exists(data []byte) (bool, error) { locations := f.getLocations(data) isSet, err := f.bitSet.check(locations) if err != nil { return false, err } if !isSet { return false, nil } return true, nil } 本节主要介绍了go-zero框架中的 core.bloom 工具,在实际的项目中非常实用。用好工具对于提升服务性能和开发效率都有很大的帮助,希望本篇文章能给大家带来一些收获。 Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:49 "},"executors.html":{"url":"executors.html","title":"executors","keywords":"","body":"executors 在 go-zero 中,executors 充当任务池,做多任务缓冲,适用于做批量处理的任务。如:clickhouse 大批量 insert,sql batch insert。同时也可以在 go-queue 中看到 executors 【在 queue 里面使用的是 ChunkExecutor ,限定任务提交字节大小】。 所以当你存在以下需求,都可以使用这个组件: 批量提交任务 缓冲一部分任务,惰性提交 延迟任务提交 具体解释之前,先给一个大致的概览图: 接口设计 在 executors 包下,有如下几个 executor : Name Margin value bulkexecutor 达到 maxTasks 【最大任务数】 提交 chunkexecutor 达到 maxChunkSize【最大字节数】提交 periodicalexecutor basic executor delayexecutor 延迟执行传入的 fn() lessexecutor 你会看到除了有特殊功能的 delay,less ,其余 3 个都是 executor + container 的组合设计: func NewBulkExecutor(execute Execute, opts ...BulkOption) *BulkExecutor { // 选项模式:在 go-zero 中多处出现。在多配置下,比较好的设计思路 // https://halls-of-valhalla.org/beta/articles/functional-options-pattern-in-go,54/ options := newBulkOptions() for _, opt := range opts { opt(&options) } // 1. task container: [execute 真正做执行的函数] [maxTasks 执行临界点] container := &bulkContainer{ execute: execute, maxTasks: options.cachedTasks, } // 2. 可以看出 bulkexecutor 底层依赖 periodicalexecutor executor := &BulkExecutor{ executor: NewPeriodicalExecutor(options.flushInterval, container), container: container, } return executor } 而这个 container是个 interface: TaskContainer interface { // 把 task 加入 container AddTask(task interface{}) bool // 实际上是去执行传入的 execute func() Execute(tasks interface{}) // 达到临界值,移除 container 中全部的 task,通过 channel 传递到 execute func() 执行 RemoveAll() interface{} } 由此可见之间的依赖关系: bulkexecutor:periodicalexecutor + bulkContainer chunkexecutor:periodicalexecutor + chunkContainer [!TIP] 所以你想完成自己的 executor,可以实现 container 的这 3 个接口,再结合 periodicalexecutor 就行 所以回到👆那张图,我们的重点就放在 periodicalexecutor,看看它是怎么设计的? 如何使用 首先看看如何在业务中使用这个组件: 现有一个定时服务,每天固定时间去执行从 mysql 到 clickhouse 的数据同步: type DailyTask struct { ckGroup *clickhousex.Cluster insertExecutor *executors.BulkExecutor mysqlConn sqlx.SqlConn } 初始化 bulkExecutor: func (dts *DailyTask) Init() { // insertIntoCk() 是真正insert执行函数【需要开发者自己编写具体业务逻辑】 dts.insertExecutor = executors.NewBulkExecutor( dts.insertIntoCk, executors.WithBulkInterval(time.Second*3), // 3s会自动刷一次container中task去执行 executors.WithBulkTasks(10240), // container最大task数。一般设为2的幂次 ) } [!TIP] 额外介绍一下:clickhouse 适合大批量的插入,因为 insert 速度很快,大批量 insert 更能充分利用 clickhouse 主体业务逻辑编写: func (dts *DailyTask) insertNewData(ch chan interface{}, sqlFromDb *model.Task) error { for item := range ch { if r, vok := item.(*model.Task); !vok { continue } err := dts.insertExecutor.Add(r) if err != nil { r.Tag = sqlFromDb.Tag r.TagId = sqlFromDb.Id r.InsertId = genInsertId() r.ToRedis = toRedis == constant.INCACHED r.UpdateWay = sqlFromDb.UpdateWay // 1. Add Task err := dts.insertExecutor.Add(r) if err != nil { logx.Error(err) } } } // 2. Flush Task container dts.insertExecutor.Flush() // 3. Wait All Task Finish dts.insertExecutor.Wait() } [!TIP] 可能会疑惑为什么要 Flush(), Wait() ,后面会通过源码解析一下 使用上总体分为 3 步: Add():加入 task Flush():刷新 container 中的 task Wait():等待全部 task 执行完成 源码分析 [!TIP] 此处主要分析 periodicalexecutor,因为其他两个常用的 executor 都依赖它 初始化 func New...(interval time.Duration, container TaskContainer) *PeriodicalExecutor { executor := &PeriodicalExecutor{ commander: make(chan interface{}, 1), interval: interval, container: container, confirmChan: make(chan lang.PlaceholderType), newTicker: func(d time.Duration) timex.Ticker { return timex.NewTicker(interval) }, } ... return executor } commander:传递 tasks 的 channel container:暂存 Add() 的 task confirmChan:阻塞 Add() ,在开始本次的 executeTasks() 会放开阻塞 ticker:定时器,防止 Add() 阻塞时,会有一个定时执行的机会,及时释放暂存的 task Add() 初始化完,在业务逻辑的第一步就是把 task 加入 executor: func (pe *PeriodicalExecutor) Add(task interface{}) { if vals, ok := pe.addAndCheck(task); ok { pe.commander =maxTask 将container中tasks pop, return if pe.container.AddTask(task) { return pe.container.RemoveAll(), true } return nil, false } addAndCheck() 中 AddTask() 就是在控制最大 tasks 数,如果超过就执行 RemoveAll() ,将暂存 container 的 tasks pop,传递给 commander ,后面有 goroutine 循环读取,然后去执行 tasks。 backgroundFlush() 开启一个后台协程,对 container 中的 task,不断刷新: func (pe *PeriodicalExecutor) backgroundFlush() { // 封装 go func(){} threading.GoSafe(func() { ticker := pe.newTicker(pe.interval) defer ticker.Stop() var commanded bool last := timex.Now() for { select { // 从channel拿到 []tasks case vals := pe.interval*idleRound { // 既没到maxTask,Flush() err,并且 last->now 时间过长,会再次触发 Flush() // 只有这置反,才会开启一个新的 backgroundFlush() 后台协程 pe.guarded = false // 再次刷新,防止漏掉 pe.Flush() return } } } }) } 总体两个过程: commander 接收到 RemoveAll() 传递来的 tasks,然后执行,并放开 Add() 的阻塞,得以继续 Add() ticker 到时间了,如果第一步没有执行,则自动 Flush() ,也会去做 task 的执行 Wait() 在 backgroundFlush() ,提到一个函数:enterExecution(): func (pe *PeriodicalExecutor) enterExecution() { pe.wgBarrier.Guard(func() { pe.waitGroup.Add(1) }) } func (pe *PeriodicalExecutor) Wait() { pe.wgBarrier.Guard(func() { pe.waitGroup.Wait() }) } 这样列举就知道为什么之前在最后要带上 dts.insertExecutor.Wait(),当然要等待全部的 goroutine task 完成。 思考 在看源码中,思考了一些其他设计上的思路,大家是否也有类似的问题: 在分析 executors 中,会发现很多地方都有 lock [!TIP] go test 存在竞态,使用加锁来避免这种情况 在分析 confirmChan 时发现,confirmChan 在此次提交才出现,为什么会这么设计? 之前是:wg.Add(1) 是写在 executeTasks() ;现在是:先wg.Add(1),再放开 confirmChan 阻塞 如果 executor func 执行阻塞,Add task 还在进行,因为没有阻塞,可能很快执行到 Executor.Wait(),这时就会出现 wg.Wait() 在 wg.Add() 前执行,这会 panic 具体可以看最新版本的TestPeriodicalExecutor_WaitFast() ,不妨跑在此版本上,就可以重现 总结 剩余还有几个 executors 的分析,就留给大家去看看源码。 总之,整体设计上: 遵循面向接口设计 灵活使用 channel ,waitgroup 等并发工具 执行单元+存储单元的搭配使用 在 go-zero 中还有很多实用的组件工具,用好工具对于提升服务性能和开发效率都有很大的帮助,希望本篇文章能给大家带来一些收获。 Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:49 "},"fx.html":{"url":"fx.html","title":"流处理组件 fx","keywords":"","body":"数据的流处理利器 流处理(Stream processing)是一种计算机编程范式,其允许给定一个数据序列(流处理数据源),一系列数据操作(函数)被应用到流中的每个元素。同时流处理工具可以显著提高程序员的开发效率,允许他们编写有效、干净和简洁的代码。 流数据处理在我们的日常工作中非常常见,举个例子,我们在业务开发中往往会记录许多业务日志,这些日志一般是先发送到Kafka,然后再由Job消费Kafaka写到elasticsearch,在进行日志流处理的过程中,往往还会对日志做一些处理,比如过滤无效的日志,做一些计算以及重新组合日志等等,示意图如下: 流处理工具fx gozero是一个功能完备的微服务框架,框架中内置了很多非常实用的工具,其中就包含流数据处理工具fx,下面我们通过一个简单的例子来认识下该工具: package main import ( \"fmt\" \"os\" \"os/signal\" \"syscall\" \"time\" \"github.com/zeromicro/go-zero/core/fx\" ) func main() { ch := make(chan int) go inputStream(ch) go outputStream(ch) c := make(chan os.Signal, 1) signal.Notify(c, syscall.SIGTERM, syscall.SIGINT) inputStream函数模拟了流数据的产生,outputStream函数模拟了流数据的处理过程,其中From函数为流的输入,Walk函数并发的作用在每一个item上,Filter函数对item进行过滤为true保留为false不保留,ForEach函数遍历输出每一个item元素。 流数据处理中间操作 一个流的数据处理可能存在许多的中间操作,每个中间操作都可以作用在流上。就像流水线上的工人一样,每个工人操作完零件后都会返回处理完成的新零件,同理流处理中间操作完成后也会返回一个新的流。 fx的流处理中间操作: 操作函数 功能 输入 Distinct 去除重复的item KeyFunc,返回需要去重的key Filter 过滤不满足条件的item FilterFunc,Option控制并发量 Group 对item进行分组 KeyFunc,以key进行分组 Head 取出前n个item,返回新stream int64保留数量 Map 对象转换 MapFunc,Option控制并发量 Merge 合并item到slice并生成新stream Reverse 反转item Sort 对item进行排序 LessFunc实现排序算法 Tail 与Head功能类似,取出后n个item组成新stream int64保留数量 Walk 作用在每个item上 WalkFunc,Option控制并发量 下图展示了每个步骤和每个步骤的结果: 用法与原理分析 From 通过From函数构建流并返回Stream,流数据通过channel进行存储: // 例子 s := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 0} fx.From(func(source chan Filter Filter函数提供过滤item的功能,FilterFunc定义过滤逻辑true保留item,false则不保留: // 例子 保留偶数 s := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 0} fx.From(func(source chan Group Group对流数据进行分组,需定义分组的key,数据分组后以slice存入channel: // 例子 按照首字符\"g\"或者\"p\"分组,没有则分到另一组 ss := []string{\"golang\", \"google\", \"php\", \"python\", \"java\", \"c++\"} fx.From(func(source chan Reverse reverse可以对流中元素进行反转处理: // 例子 fx.Just(1, 2, 3, 4, 5).Reverse().ForEach(func(item interface{}) { fmt.Println(item) }) // 源码 func (p Stream) Reverse() Stream { var items []interface{} // 获取流中数据 for item := range p.source { items = append(items, item) } // 反转算法 for i := len(items)/2 - 1; i >= 0; i-- { opp := len(items) - 1 - i items[i], items[opp] = items[opp], items[i] } // 写入流 return Just(items...) } Distinct distinct对流中元素进行去重,去重在业务开发中比较常用,经常需要对用户id等做去重操作: // 例子 fx.Just(1, 2, 2, 2, 3, 3, 4, 5, 6).Distinct(func(item interface{}) interface{} { return item }).ForEach(func(item interface{}) { fmt.Println(item) }) // 结果为 1,2,3,4,5,6 // 源码 func (p Stream) Distinct(fn KeyFunc) Stream { source := make(chan interface{}) threading.GoSafe(func() { defer close(source) // 通过key进行去重,相同key只保留一个 keys := make(map[interface{}]lang.PlaceholderType) for item := range p.source { key := fn(item) // key存在则不保留 if _, ok := keys[key]; !ok { source Walk Walk函数并发的作用在流中每一个item上,可以通过WithWorkers设置并发数,默认并发数为16,最小并发数为1,如设置unlimitedWorkers为true则并发数无限制,但并发写入流中的数据由defaultWorkers限制,WalkFunc中用户可以自定义后续写入流中的元素,可以不写入也可以写入多个元素: // 例子 fx.Just(\"aaa\", \"bbb\", \"ccc\").Walk(func(item interface{}, pipe chan 并发处理 fx工具除了进行流数据处理以外还提供了函数并发功能,在微服务中实现某个功能往往需要依赖多个服务,并发的处理依赖可以有效的降低依赖耗时,提升服务的性能。 fx.Parallel(func() { userRPC() // 依赖1 }, func() { accountRPC() // 依赖2 }, func() { orderRPC() // 依赖3 }) 注意fx.Parallel进行依赖并行处理的时候不会有error返回,如需有error返回或者有一个依赖报错需要立马结束依赖请求请使用MapReduce工具进行处理。 总结 本篇文章介绍了流处理的基本概念和gozero中的流处理工具fx,在实际的生产中流处理场景应用也非常多,希望本篇文章能给大家带来一定的启发,更好的应对工作中的流处理场景。 Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:49 "},"mysql.html":{"url":"mysql.html","title":"go-zero mysql使用介绍","keywords":"","body":"mysql go-zero 提供更易于操作的 mysql API。 [!TIP] 但是 stores/mysql 定位不是一个 orm 框架,如果你需要通过 sql/scheme -> model/struct 逆向生成 model 层代码,可以使用「goctl model」,这个是极好的功能。 Feature 相比原生,提供对开发者更友好的 API 完成 queryField -> struct 的自动赋值 批量插入「bulkinserter」 自带熔断 API 经过若干个服务的不断考验 提供 partial assignment 特性,不强制 struct 的严格赋值 Connection 下面用一个例子简单说明一下如何创建一个 mysql 连接的 model: // 1. 快速连接一个 mysql // datasource: mysql dsn heraMysql := sqlx.NewMysql(datasource) // 2. 在 servicecontext 中调用,懂model上层的logic层调用 model.NewMysqlModel(heraMysql, tablename), // 3. model层 mysql operation func NewMysqlModel(conn sqlx.SqlConn, table string) *MysqlModel { defer func() { recover() }() // 4. 创建一个批量insert的 [mysql executor] // conn: mysql connection; insertsql: mysql insert sql bulkInserter , err := sqlx.NewBulkInserter(conn, insertsql) if err != nil { logx.Error(\"Init bulkInsert Faild\") panic(\"Init bulkInsert Faild\") return nil } return &MysqlModel{conn: conn, table: table, Bulk: bulkInserter} } CRUD 准备一个 User model var userBuilderQueryRows = strings.Join(builder.FieldNames(&User{}), \",\") type User struct { Avatar string `db:\"avatar\"` // 头像 UserName string `db:\"user_name\"` // 姓名 Sex int `db:\"sex\"` // 1男,2女 MobilePhone string `db:\"mobile_phone\"` // 手机号 } 其中 userBuilderQueryRows : go-zero 中提供 struct -> [field...] 的转化,开发者可以将此当成模版直接使用。 insert // 一个实际的insert model层操作 func (um *UserModel) Insert(user *User) (int64, error) { const insertsql = `insert into `+um.table+` (`+userBuilderQueryRows+`) values(?, ?, ?)` // insert op res, err := um.conn.Exec(insertsql, user.Avatar, user.UserName, user.Sex, user.MobilePhone) if err != nil { logx.Errorf(\"insert User Position Model Model err, err=%v\", err) return -1, err } id, err := res.LastInsertId() if err != nil { logx.Errorf(\"insert User Model to Id parse id err,err=%v\", err) return -1, err } return id, nil } 拼接 insertsql 将 insertsql 以及占位符对应的 struct field 传入 -> con.Exex(insertsql, field...) [!WARNING] conn.Exec(sql, args...) : args... 需对应 sql 中的占位符。不然会出现赋值异常的问题。 go-zero 将涉及 mysql 修改的操作统一抽象为 Exec() 。所以 insert/update/delete 操作本质上是一致的。其余两个操作,开发者按照上述 insert 流程尝试即可。 query 只需要传入 querysql 和 model 结构体,就可以获取到被赋值好的 model 。无需开发者手动赋值。 func (um *UserModel) FindOne(uid int64) (*User, error) { var user User const querysql = `select `+userBuilderQueryRows+` from `+um.table+` where id=? limit 1` err := um.conn.QueryRow(&user, querysql, uid) if err != nil { logx.Errorf(\"userId.findOne error, id=%d, err=%s\", uid, err.Error()) if err == sqlx.ErrNotFound { return nil, ErrNotFound } return nil, err } return &user, nil } 声明 model struct ,拼接 querysql conn.QueryRow(&model, querysql, args...) : args... 与 querysql 中的占位符对应。 [!WARNING] QueryRow() 中第一个参数需要传入 Ptr 「底层需要反射对 struct 进行赋值」 上述是查询一条记录,如果需要查询多条记录时,可以使用 conn.QueryRows() func (um *UserModel) FindOne(sex int) ([]*User, error) { users := make([]*User, 0) const querysql = `select `+userBuilderQueryRows+` from `+um.table+` where sex=?` err := um.conn.QueryRows(&users, querysql, sex) if err != nil { logx.Errorf(\"usersSex.findOne error, sex=%d, err=%s\", uid, err.Error()) if err == sqlx.ErrNotFound { return nil, ErrNotFound } return nil, err } return users, nil } 与 QueryRow() 不同的地方在于: model 需要设置成 Slice ,因为是查询多行,需要对多个 model 赋值。但同时需要注意️:第一个参数需要传入 Ptr querypartial 从使用上,与上述的 QueryRow() 无异「这正体现了 go-zero 高度的抽象设计」。 区别: QueryRow() : len(querysql fields) == len(struct) ,且一一对应 QueryRowPartial() :len(querysql fields) numA:数据库字段数;numB:定义的 struct 属性数。 如果 numA ,但是你恰恰又需要统一多处的查询时「定义了多个 struct 返回不同的用途,恰恰都可以使用相同的 querysql 」,就可以使用 QueryRowPartial() 事务 要在事务中执行一系列操作,一般流程如下: var insertsql = `insert into User(uid, username, mobilephone) values (?, ?, ?)` err := usermodel.conn.Transact(func(session sqlx.Session) error { stmt, err := session.Prepare(insertsql) if err != nil { return err } defer stmt.Close() // 返回任何错误都会回滚事务 if _, err := stmt.Exec(uid, username, mobilephone); err != nil { logx.Errorf(\"insert userinfo stmt exec: %s\", err) return err } // 还可以继续执行 insert/update/delete 相关操作 return nil }) 如同上述例子,开发者只需将 事务 中的操作都包装在一个函数 func(session sqlx.Session) error {} 中即可,如果事务中的操作返回任何错误, Transact() 都会自动回滚事务。 分布式事务 go-zero 与 dtm 深度合作,原生的支持了分布式事务,详情参见 分布式事务支持 Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:49 "},"redis-lock.html":{"url":"redis-lock.html","title":"redis锁","keywords":"","body":"redis-lock redis lock 既然是锁,首先想到的一个作用就是:防重复点击,在一个时间点只有一个请求产生效果。 而既然是 redis,就得具有排他性,同时也具有锁的一些共性: 高性能 不能出现死锁 不能出现节点down掉后加锁失败 go-zero 中利用 redis set key nx 可以保证key不存在时写入成功,px 可以让key超时后自动删除「最坏情况也就是超时自动删除key,从而也不会出现死锁」 example redisLockKey := fmt.Sprintf(\"%v%v\", redisTpl, headId) // 1. New redislock redisLock := redis.NewRedisLock(redisConn, redisLockKey) // 2. 可选操作,设置 redislock 过期时间 redisLock.SetExpire(redisLockExpireSeconds) if ok, err := redisLock.Acquire(); !ok || err != nil { return nil, errors.New(\"当前有其他用户正在进行操作,请稍后重试\") } defer func() { recover() // 3. 释放锁 redisLock.Release() }() 和你在使用 sync.Mutex 的方式时一致的。加锁解锁,执行你的业务操作。 获取锁 lockCommand = `if redis.call(\"GET\", KEYS[1]) == ARGV[1] then redis.call(\"SET\", KEYS[1], ARGV[1], \"PX\", ARGV[2]) return \"OK\" else return redis.call(\"SET\", KEYS[1], ARGV[1], \"NX\", \"PX\", ARGV[2]) end` func (rl *RedisLock) Acquire() (bool, error) { seconds := atomic.LoadUint32(&rl.seconds) // execute luascript resp, err := rl.store.Eval(lockCommand, []string{rl.key}, []string{ rl.id, strconv.Itoa(int(seconds)*millisPerSecond + tolerance)}) if err == red.Nil { return false, nil } else if err != nil { logx.Errorf(\"Error on acquiring lock for %s, %s\", rl.key, err.Error()) return false, err } else if resp == nil { return false, nil } reply, ok := resp.(string) if ok && reply == \"OK\" { return true, nil } else { logx.Errorf(\"Unknown reply when acquiring lock for %s: %v\", rl.key, resp) return false, nil } } 先介绍几个 redis 的命令选项,以下是为 set 命令增加的选项: ex seconds :设置key过期时间,单位s px milliseconds :设置key过期时间,单位毫秒 nx:key不存在时,设置key的值 xx:key存在时,才会去设置key的值 其中 lua script 涉及的入参: args 示例 含义 KEYS[1] key$20201026 redis key ARGV[1] lmnopqrstuvwxyzABCD 唯一标识:随机字符串 ARGV[2] 30000 设置锁的过期时间 然后来说说代码特性: Lua 脚本保证原子性「当然,把多个操作在 Redis 中实现成一个操作,也就是单命令操作」 使用了 set key value px milliseconds nx value 具有唯一性 加锁时首先判断 key 的 value 是否和之前设置的一致,一致则修改过期时间 释放锁 delCommand = `if redis.call(\"GET\", KEYS[1]) == ARGV[1] then return redis.call(\"DEL\", KEYS[1]) else return 0 end` func (rl *RedisLock) Release() (bool, error) { resp, err := rl.store.Eval(delCommand, []string{rl.key}, []string{rl.id}) if err != nil { return false, err } if reply, ok := resp.(int64); !ok { return false, nil } else { return reply == 1, nil } } 释放锁的时候只需要关注一点: 不能释放别人的锁,不能释放别人的锁,不能释放别人的锁 所以需要先 get(key) == value「key」,为 true 才会去 delete Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:49 "},"periodlimit.html":{"url":"periodlimit.html","title":"periodlimit限流","keywords":"","body":"periodlimit 不管是在单体服务中还是在微服务中,开发者为前端提供的API接口都是有访问上限的,当访问频率或者并发量超过其承受范围时候,我们就必须考虑限流来保证接口的可用性或者降级可用性。即接口也需要安装上保险丝,以防止非预期的请求对系统压力过大而引起的系统瘫痪。 本文就来介绍一下 periodlimit 。 使用 const ( seconds = 1 total = 100 quota = 5 ) // New limiter l := NewPeriodLimit(seconds, quota, redis.NewRedis(s.Addr(), redis.NodeType), \"periodlimit\") // take source code, err := l.Take(\"first\") if err != nil { logx.Error(err) return true } // switch val => process request switch code { case limit.OverQuota: logx.Errorf(\"OverQuota key: %v\", key) return false case limit.Allowed: logx.Infof(\"AllowedQuota key: %v\", key) return true case limit.HitQuota: logx.Errorf(\"HitQuota key: %v\", key) // todo: maybe we need to let users know they hit the quota return false default: logx.Errorf(\"DefaultQuota key: %v\", key) // unknown response, we just let the sms go return true } periodlimit go-zero 采取 滑动窗口 计数的方式,计算一段时间内对同一个资源的访问次数,如果超过指定的 limit ,则拒绝访问。当然如果你是在一段时间内访问不同的资源,每一个资源访问量都不超过 limit ,此种情况是允许大量请求进来的。 而在一个分布式系统中,存在多个微服务提供服务。所以当瞬间的流量同时访问同一个资源,如何让计数器在分布式系统中正常计数? 同时在计算资源访问时,可能会涉及多个计算,如何保证计算的原子性? go-zero 借助 redis 的 incrby 做资源访问计数 采用 lua script 做整个窗口计算,保证计算的原子性 下面来看看 lua script 控制的几个关键属性: argument mean key[1] 访问资源的标示 ARGV[1] limit => 请求总数,超过则限速。可设置为 QPS ARGV[2] window大小 => 滑动窗口,用 ttl 模拟出滑动的效果 -- to be compatible with aliyun redis, -- we cannot use `local key = KEYS[1]` to reuse thekey local limit = tonumber(ARGV[1]) local window = tonumber(ARGV[2]) -- incrbt key 1 => key visis++ local current = redis.call(\"INCRBY\", KEYS[1], 1) -- 如果是第一次访问,设置过期时间 => TTL = window size -- 因为是只限制一段时间的访问次数 if current == 1 then redis.call(\"expire\", KEYS[1], window) return 1 elseif current 至于上述的 return code ,返回给调用方。由调用方来决定请求后续的操作: return code tag call code mean 0 OverQuota 3 over limit 1 Allowed 1 in limit 2 HitQuota 2 hit limit 下面这张图描述了请求进入的过程,以及请求触发 limit 时后续发生的情况: 后续处理 如果在服务某个时间点,请求大批量打进来,periodlimit 短期时间内达到 limit 阈值,而且设置的时间范围还远远没有到达。后续请求的处理就成为问题。 periodlimit 中并没有处理,而是返回 code 。把后续请求的处理交给了开发者自己处理。 如果不做处理,那就是简单的将请求拒绝 如果需要处理这些请求,开发者可以借助 mq 将请求缓冲,减缓请求的压力 采用 tokenlimit,允许暂时的流量冲击 所以下一篇我们就来聊聊 tokenlimit 总结 go-zero 中的 periodlimit 限流方案是基于 redis 计数器,通过调用 redis lua script ,保证计数过程的原子性,同时保证在分布式的情况下计数是正常的。但是这种方案存在缺点,因为它要记录时间窗口内的所有行为记录,如果这个量特别大的时候,内存消耗会变得非常严重。 参考 go-zero periodlimit 分布式服务限流实战,已经为你排好坑了 tokenlimit Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:49 "},"tokenlimit.html":{"url":"tokenlimit.html","title":"令牌桶限流","keywords":"","body":"tokenlimit 本节将通过令牌桶限流(tokenlimit)来介绍其基本使用。 使用 const ( burst = 100 rate = 100 seconds = 5 ) store := redis.NewRedis(\"localhost:6379\", \"node\", \"\") fmt.Println(store.Ping()) // New tokenLimiter limiter := limit.NewTokenLimiter(rate, burst, store, \"rate-test\") timer := time.NewTimer(time.Second * seconds) quit := make(chan struct{}) defer timer.Stop() go func() { tokenlimit 从整体上令牌桶生产token逻辑如下: 用户配置的平均发送速率为r,则每隔1/r秒一个令牌被加入到桶中; 假设桶中最多可以存放b个令牌。如果令牌到达时令牌桶已经满了,那么这个令牌会被丢弃; 当流量以速率v进入,从桶中以速率v取令牌,拿到令牌的流量通过,拿不到令牌流量不通过,执行熔断逻辑; go-zero 在两类限流器下都采取 lua script 的方式,依赖redis可以做到分布式限流,lua script同时可以做到对 token 生产读取操作的原子性。 下面来看看 lua script 控制的几个关键属性: argument mean ARGV[1] rate 「每秒生成几个令牌」 ARGV[2] burst 「令牌桶最大值」 ARGV[3] now_time「当前时间戳」 ARGV[4] get token nums 「开发者需要获取的token数」 KEYS[1] 表示资源的tokenkey KEYS[2] 表示刷新时间的key -- 返回是否可以活获得预期的token local rate = tonumber(ARGV[1]) local capacity = tonumber(ARGV[2]) local now = tonumber(ARGV[3]) local requested = tonumber(ARGV[4]) -- fill_time:需要填满 token_bucket 需要多久 local fill_time = capacity/rate -- 将填充时间向下取整 local ttl = math.floor(fill_time*2) -- 获取目前 token_bucket 中剩余 token 数 -- 如果是第一次进入,则设置 token_bucket 数量为 令牌桶最大值 local last_tokens = tonumber(redis.call(\"get\", KEYS[1])) if last_tokens == nil then last_tokens = capacity end -- 上一次更新 token_bucket 的时间 local last_refreshed = tonumber(redis.call(\"get\", KEYS[2])) if last_refreshed == nil then last_refreshed = 0 end local delta = math.max(0, now-last_refreshed) -- 通过当前时间与上一次更新时间的跨度,以及生产token的速率,计算出新的token数 -- 如果超过 max_burst,多余生产的token会被丢弃 local filled_tokens = math.min(capacity, last_tokens+(delta*rate)) local allowed = filled_tokens >= requested local new_tokens = filled_tokens if allowed then new_tokens = filled_tokens - requested end -- 更新新的token数,以及更新时间 redis.call(\"setex\", KEYS[1], ttl, new_tokens) redis.call(\"setex\", KEYS[2], ttl, now) return allowed 上述可以看出 lua script :只涉及对 token 操作,保证 token 生产合理和读取合理。 函数分析 从上述流程中看出: 有多重保障机制,保证限流一定会完成。 如果redis limiter失效,至少在进程内rate limiter兜底。 重试 redis limiter 机制保证尽可能地正常运行。 总结 go-zero 中的 tokenlimit 限流方案适用于瞬时流量冲击,现实请求场景并不以恒定的速率。令牌桶相当预请求,当真实的请求到达不至于瞬间被打垮。当流量冲击到一定程度,则才会按照预定速率进行消费。 但是生产token上,不能按照当时的流量情况作出动态调整,不够灵活,还可以进行进一步优化。此外可以参考Token bucket WIKI 中提到分层令牌桶,根据不同的流量带宽,分至不同排队中。 参考 go-zero tokenlimit Go-Redis 提供的分布式限流库 Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:50 "},"timing-wheel.html":{"url":"timing-wheel.html","title":"时间轮介绍","keywords":"","body":"TimingWheel 本文来介绍 go-zero 中 延迟操作。延迟操作,可以采用两个方案: Timer:定时器维护一个优先队列,到时间点执行,然后把需要执行的 task 存储在 map 中 collection 中的 timingWheel ,维护一个存放任务组的数组,每一个槽都维护一个存储task的双向链表。开始执行时,计时器每隔指定时间执行一个槽里面的tasks。 方案2把维护task从 优先队列 O(nlog(n)) 降到 双向链表 O(1),而执行task也只要轮询一个时间点的tasks O(N),不需要像优先队列,放入和删除元素 O(nlog(n))。 cache 中的 timingWheel 首先我们先来在 collection 的 cache 中关于 timingWheel 的使用: timingWheel, err := NewTimingWheel(time.Second, slots, func(k, v interface{}) { key, ok := k.(string) if !ok { return } cache.Del(key) }) if err != nil { return nil, err } cache.timingWheel = timingWheel 这是 cache 初始化中也同时初始化 timingWheel 做key的过期处理,参数依次代表: interval:时间划分刻度 numSlots:时间槽 execute:时间点执行函数 在 cache 中执行函数则是 删除过期key,而这个过期则由 timingWheel 来控制推进时间。 接下来,就通过 cache 对 timingWheel 的使用来认识。 初始化 // 真正做初始化 func newTimingWheelWithClock(interval time.Duration, numSlots int, execute Execute, ticker timex.Ticker) ( *TimingWheel, error) { tw := &TimingWheel{ interval: interval, // 单个时间格时间间隔 ticker: ticker, // 定时器,做时间推动,以interval为单位推进 slots: make([]*list.List, numSlots), // 时间轮 timers: NewSafeMap(), // 存储task{key, value}的map [执行execute所需要的参数] tickedPos: numSlots - 1, // at previous virtual circle execute: execute, // 执行函数 numSlots: numSlots, // 初始化 slots num setChannel: make(chan timingEntry), // 以下几个channel是做task传递的 moveChannel: make(chan baseEntry), removeChannel: make(chan interface{}), drainChannel: make(chan func(key, value interface{})), stopChannel: make(chan lang.PlaceholderType), } // 把 slot 中存储的 list 全部准备好 tw.initSlots() // 开启异步协程,使用 channel 来做task通信和传递 go tw.run() return tw, nil } 以上比较直观展示 timingWheel 的 “时间轮”,后面会围绕这张图解释其中推进的细节。 go tw.run() 开一个协程做时间推动: func (tw *TimingWheel) run() { for { select { // 定时器做时间推动 -> scanAndRunTasks() case 可以看出,在初始化的时候就开始了 timer 执行,并以interval时间段转动,然后底层不停的获取来自 slot 中的 list 的task,交给 execute 执行。 Task Operation 紧接着就是设置 cache key : func (c *Cache) Set(key string, value interface{}) { c.lock.Lock() _, ok := c.data[key] c.data[key] = value c.lruCache.add(key) c.lock.Unlock() expiry := c.unstableExpiry.AroundDuration(c.expire) if ok { c.timingWheel.MoveTimer(key, expiry) } else { c.timingWheel.SetTimer(key, value, expiry) } } 先看在 data map 中有没有存在这个key 存在,则更新 expire -> MoveTimer() 第一次设置key -> SetTimer() 所以对于 timingWheel 的使用上就清晰了,开发者根据需求可以 add 或是 update。 同时我们跟源码进去会发现:SetTimer() MoveTimer() 都是将task输送到channel,由 run() 中开启的协程不断取出 channel 的task操作。 SetTimer() -> setTask(): not exist task:getPostion -> pushBack to list -> setPosition exist task:get from timers -> moveTask() MoveTimer() -> moveTask() 由上面的调用链,有一个都会调用的函数:moveTask() func (tw *TimingWheel) moveTask(task baseEntry) { // timers: Map => 通过key获取 [positionEntry「pos, task」] val, ok := tw.timers.Get(task.key) if !ok { return } timer := val.(*positionEntry) // {delay 延迟时间比一个时间格间隔还小,没有更小的刻度,说明任务应该立即执行 if task.delay interval,则通过 延迟时间delay 计算其出时间轮中的 new pos, circle pos, circle := tw.getPositionAndCircle(task.delay) if pos >= timer.pos { timer.item.circle = circle // 记录前后的移动offset。为了后面过程重新入队 timer.item.diff = pos - timer.pos } else if circle > 0 { // 转移到下一层,将 circle 转换为 diff 一部分 circle-- timer.item.circle = circle // 因为是一个数组,要加上 numSlots [也就是相当于要走到下一层] timer.item.diff = tw.numSlots + pos - timer.pos } else { // 如果 offset 提前了,此时 task 也还在第一层 // 标记删除老的 task,并重新入队,等待被执行 timer.item.removed = true newItem := &timingEntry{ baseEntry: task, value: timer.item.value, } tw.slots[pos].PushBack(newItem) tw.setTimerPosition(pos, newItem) } } 以上过程有以下几种情况: delay :因为 针对改变的 delay: new >= old: newCircle > 0:计算diff,并将 circle 转换为 下一层,故diff + numslots 如果只是单纯延迟时间缩短,则将老的task标记删除,重新加入list,等待下一轮loop被execute Execute 之前在初始化中,run() 中定时器的不断推进,推进的过程主要就是把 list中的 task 传给执行的 execute func。我们从定时器的执行开始看: // 定时器 「每隔 interval 会执行一次」 func (tw *TimingWheel) onTick() { // 每次执行更新一下当前执行 tick 位置 tw.tickedPos = (tw.tickedPos + 1) % tw.numSlots // 获取此时 tick位置 中的存储task的双向链表 l := tw.slots[tw.tickedPos] tw.scanAndRunTasks(l) } 紧接着是如何去执行 execute: func (tw *TimingWheel) scanAndRunTasks(l *list.List) { // 存储目前需要执行的task{key, value} [execute所需要的参数,依次传递给execute执行] var tasks []timingTask for e := l.Front(); e != nil; { task := e.Value.(*timingEntry) // 标记删除,在 scan 中做真正的删除 「删除map的data」 if task.removed { next := e.Next() l.Remove(e) tw.timers.Del(task.key) e = next continue } else if task.circle > 0 { // 当前执行点已经过期,但是同时不在第一层,所以当前层即然已经完成了,就会降到下一层 // 但是并没有修改 pos task.circle-- e = e.Next() continue } else if task.diff > 0 { // 因为之前已经标注了diff,需要再进入队列 next := e.Next() l.Remove(e) pos := (tw.tickedPos + task.diff) % tw.numSlots tw.slots[pos].PushBack(task) tw.setTimerPosition(pos, task) task.diff = 0 e = next continue } // 以上的情况都是不能执行的情况,能够执行的会被加入tasks中 tasks = append(tasks, timingTask{ key: task.key, value: task.value, }) next := e.Next() l.Remove(e) tw.timers.Del(task.key) e = next } // for range tasks,然后把每个 task->execute 执行即可 tw.runTasks(tasks) } 具体的分支情况在注释中说明了,在看的时候可以和前面的 moveTask() 结合起来,其中 circle 下降,diff 的计算是关联两个函数的重点。 至于 diff 计算就涉及到 pos, circle 的计算: // interval: 4min, d: 60min, numSlots: 16, tickedPos = 15 // step = 15, pos = 14, circle = 0 func (tw *TimingWheel) getPositionAndCircle(d time.Duration) (pos int, circle int) { steps := int(d / tw.interval) pos = (tw.tickedPos + steps) % tw.numSlots circle = (steps - 1) / tw.numSlots return } 上面的过程可以简化成下面: steps = d / interval pos = step % numSlots - 1 circle = (step - 1) / numSlots 总结 timingWheel 靠定时器推动,时间前进的同时会取出当前时间格中 list「双向链表」的task,传递到 execute 中执行。 而时间分隔上,时间轮有 circle 分层,这样就可以不断复用原有的 numSlots ,因为定时器在不断 loop,而执行可以把上层的 slot 下降到下层,在不断 loop 中就可以执行到上层的task。 在 go-zero 中还有很多实用的组件工具,用好工具对于提升服务性能和开发效率都有很大的帮助,希望本篇文章能给大家带来一些收获。 Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:50 "},"eco.html":{"url":"eco.html","title":"go-zero 生态","keywords":"","body":"go-zero 生态 工具中心 intellij插件 vscode插件 分布式事务支持 插件中心 Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:49 "},"tool-center.html":{"url":"tool-center.html","title":"工具中心","keywords":"","body":"工具中心 在go-zero中,提供了很多提高工程效率的工具,如api,rpc生成,在此基础之上,api文件的编写就显得那么的无力, 因为缺少了高亮,代码提示,模板生成等,本节将带你了解go-zero是怎么解决这些难题的,本节包含以下小节: intellij插件 vscode插件 Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:50 "},"intellij.html":{"url":"intellij.html","title":"intellij插件","keywords":"","body":"intellij插件 Go-Zero Plugin 介绍 一款支持go-zero api语言结构语法高亮、检测以及api、rpc、model快捷生成的插件工具。 idea版本要求 IntelliJ 2019.3+ (Ultimate or Community) Goland 2019.3+ WebStorm 2019.3+ PhpStorm 2019.3+ PyCharm 2019.3+ RubyMine 2019.3+ CLion 2019.3+ 版本特性 api语法高亮 api语法、语义检测 struct、route、handler重复定义检测 type跳转到类型声明位置 上下文菜单中支持api、rpc、mode相关menu选项 代码格式化(option+command+L) 代码提示 安装方式 方式一 在github的release中找到最新的zip包,下载本地安装即可。(无需解压) 方式二 在plugin商店中,搜索Goctl安装即可 预览 新建 Api(Proto) file 在工程区域目标文件夹右键->New-> New Api(Proto) File ->Empty File/Api(Proto) Template,如图: 快速生成api/rpc服务 在目标文件夹右键->New->Go Zero -> Api Greet Service/Rpc Greet Service Api/Rpc/Model Code生成 方法一(工程区域) 对应文件(api、proto、sql)右键->New->Go Zero-> Api/Rpc/Model Code,如图: 方法二(编辑区域) 对应文件(api、proto、sql)右键-> Generate-> Api/Rpc/Model Code 错误提示 Live Template Live Template可以加快我们对api文件的编写,比如我们在go文件中输入main关键字根据tip回车后会插入一段模板代码 func main(){ } 或者说看到下图你会更加熟悉,曾几何时你还在这里定义过template 下面就进入今天api语法中的模板使用说明吧,我们先来看看service模板的效果 首先上一张图了解一下api文件中几个模板生效区域(psiTree元素区域) 预设模板及生效区域 模板关键字 psiTree生效区域 描述 @doc ApiService doc注释模板 doc ApiService doc注释模板 struct Struct struct声明模板 info ApiFile info block模板 type ApiFile type group模板 handler ApiService handler文件名模板 get ApiService get方法路由模板 head ApiService head方法路由模板 post ApiService post方法路由模板 put ApiService put方法路由模板 delete ApiService delete方法路由模板 connect ApiService connect方法路由模板 options ApiService options方法路由模板 trace ApiService trace方法路由模板 service ApiFile service服务block模板 json Tag、Tag literal tag模板 xml Tag、Tag literal tag模板 path Tag、Tag literal tag模板 form Tag、Tag literal tag模板 关于每个模板对应内容可在Goland(mac Os)->Preference->Editor->Live Templates-> Api|Api Tags中查看详细模板内容,如json tag模板内容为 json:\"$FIELD_NAME$\" Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:49 "},"vscode.html":{"url":"vscode.html","title":"vscode插件","keywords":"","body":"vs code 插件 该插件可以安装在 1.46.0+ 版本的 Visual Studio Code 上,首先请确保你的 Visual Studio Code 版本符合要求,并已安装 goctl 命令行工具。如果尚未安装 Visual Studio Code,请安装并打开 Visual Studio Code。 导航到“扩展”窗格,搜索 goctl 并安装此扩展(发布者ID为 “xiaoxin-technology.goctl”)。 Visual Studio Code 扩展使用请参考这里。 功能列表 已实现功能 语法高亮 跳转到定义/引用 代码格式化 代码块提示 未实现功能: 语法错误检查 跨文件代码跳转 goctl 命令行调用 语法高亮 代码跳转 代码格式化 调用 goctl 命令行格式化工具,使用前请确认 goctl 已加入 $PATH 且有可执行权限 代码块提示 info 代码块 type 代码块 service 代码块 handler 代码块 Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:50 "},"distributed-transaction.html":{"url":"distributed-transaction.html","title":"分布式事务支持","keywords":"","body":"分布式事务支持 需求场景 在微服务架构中,当我们需要跨服务保证数据一致性时,原先的数据库事务力不从心,无法将跨库、跨服务的多个操作放在一个事务中。这样的应用场景非常多,我们可以列举出很多: 订单系统:需要保证创建订单和扣减库存要么同时成功,要么同时回滚 跨行转账场景:数据不在一个数据库,但需要保证余额扣减和余额增加要么同时成功,要么同时失败 积分兑换场景:需要保证积分扣减和权益增加同时成功,或者同时失败 出行订票场景:需要在第三方系统同时定几张票,要么同时成功,要么全部取消 面对这些本地事务无法解决的场景,我们需要分布式事务的解决方案,保证跨服务、跨数据库更新数据的一致性。 解决方案 go-zero与dtm强强联合,推出了在go-zero中无缝接入dtm的极简方案,是go生态中首家提供分布式事务能力的微服务框架。该方案具备以下特征: dtm服务可以通过配置,直接注册到go-zero的注册中心 go-zero能够以内建的target格式访问dtm服务器 dtm能够识别go-zero的target格式,动态访问go-zero中的服务 详细的接入方式,参见dtm文档:go-zero支持 更多应用场景 dtm不仅可以解决上述的分布式事务场景,还可以解决更多的与数据一致性相关的场景,包括: 数据库与缓存一致性: dtm 的二阶段消息,能够保证数据库更新操作,和缓存更新/删除操作的原子性 秒杀系统: dtm 能够保证秒杀场景下,创建的订单量与库存扣减数量完全一样,无需后续的人工校准 多种存储组合: dtm 已支持数据库、Redis、Mongo等多种存储,可以将它们组合为一个全局事务,保证数据的一致性 更多 dtm 的能力和介绍,参见dtm Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:49 "},"plugin-center.html":{"url":"plugin-center.html","title":"插件中心","keywords":"","body":"插件中心 goctl api提供了对plugin命令来支持对api进行功能扩展,当goctl api中的功能不满足你的使用, 或者需要对goctl api进行功能自定义的扩展,那么插件功能将非常适合开发人员进行自给自足,详情见 goctl plugin 插件资源 goctl-go-compact goctl默认的一个路由一个文件合并成一个文件 goctl-swagger 通过api文件生成swagger文档 goctl-php goctl-php是一款基于goctl的插件,用于生成 php 调用端(服务端) http server请求代码 猜你想看 goctl插件 api语法介绍 Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:49 "},"learning-resource.html":{"url":"learning-resource.html","title":"学习资源","keywords":"","body":"学习资源 这里将不定期更新go-zero的最新学习资源通道,目前包含通道有: 公众号 Go夜读 Go开源说 Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:49 "},"wechat.html":{"url":"wechat.html","title":"公众号","keywords":"","body":"公众号 微服务实践是go-zero的官方公众号,在这里会发布最新的go-zero最佳实践,同步go夜读、go开源说、GopherChina、腾讯云开发者大会等多渠道关于go-zero的最新技术和资讯。 公众号名称 公众号作者 公众号二维码 微服务实践 kevwan 推荐主题 《带你十天轻松搞定 Go 微服务》 该系列将带你在本机利用 go-zero 快速开发一个商城系统,向大家详细展示了基于 go-zero 框架构建微服务的过程,整个系列分十篇文章,目录结构如下: 第一天:环境搭建 第二天:服务拆分 第三天:用户服务 第四天:产品服务 第五天:订单服务 第六天:支付服务 第七天:RPC服务Auth验证 第八天:服务监控 第九天:链路追踪 第十天:分布式事务 干货 这里列举一些干货,想要收获更多go-zero最佳实践干货,可以关注公众号获取最新动态。 《一文读懂云原生 go-zero 微服务框架》 《你还在手撕微服务?快试试 go-zero 的微服务自动生成》 《最简单的Go Dockerfile编写姿势,没有之一!》 《通过MapReduce降低服务响应时间》 《微服务过载保护原理与实战 《最简单的 K8S 部署文件编写姿势,没有之一!》 《go-zero 如何应对海量定时/延迟任务?》 《go-zero 如何扛住流量冲击(一)》 《服务自适应降载保护设计》 Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:50 "},"goreading.html":{"url":"goreading.html","title":"Go夜读","keywords":"","body":"Go夜读 2020-08-16 晓黑板 go-zero 微服务框架的架构设计 2020-10-03 go-zero 微服务框架和线上交流 防止缓存击穿之进程内共享调用 基于go-zero实现JWT认证 再见go-micro!企业项目迁移go-zero全攻略(一) Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:49 "},"gotalk.html":{"url":"gotalk.html","title":"Go开源说","keywords":"","body":"Go开源说 Go 开源说第四期 - Go-Zero Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:49 "},"practise.html":{"url":"practise.html","title":"User Practise","keywords":"","body":"User Practise [!TIP] This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please PR Persistent layer cache Business layer cache Queue Middle Ground System Stream Handler Online Exchange Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:50 "},"redis-cache.html":{"url":"redis-cache.html","title":"go-zero缓存设计之持久层缓存","keywords":"","body":"go-zero缓存设计之持久层缓存 缓存设计原理 我们对缓存是只删除,不做更新,一旦DB里数据出现修改,我们就会直接删除对应的缓存,而不是去更新。 我们看看删除缓存的顺序怎样才是正确的。 先删除缓存,再更新DB 我们看两个并发请求的情况,A请求需要更新数据,先删除了缓存,然后B请求来读取数据,此时缓存没有数据,就会从DB加载数据并写回缓存,然后A更新了DB,那么此时缓存内的数据就会一直是脏数据,直到缓存过期或者有新的更新数据的请求。如图 先更新DB,再删除缓存 A请求先更新DB,然后B请求来读取数据,此时返回的是老数据,此时可以认为是A请求还没更新完,最终一致性,可以接受,然后A删除了缓存,后续请求都会拿到最新数据,如图 让我们再来看一下正常的请求流程: 第一个请求更新DB,并删除了缓存 第二个请求读取缓存,没有数据,就从DB读取数据,并回写到缓存里 后续读请求都可以直接从缓存读取 我们再看一下DB查询有哪些情况,假设行记录里有ABCDEFG七列数据: 只查询部分列数据的请求,比如请求其中的ABC,CDE或者EFG等,如图 查询单条完整行记录,如图 查询多条行记录的部分或全部列,如图 对于上面三种情况,首先,我们不用部分查询,因为部分查询没法缓存,一旦缓存了,数据有更新,没法定位到有哪些数据需要删除;其次,对于多行的查询,根据实际场景和需要,我们会在业务层建立对应的从查询条件到主键的映射;而对于单行完整记录的查询,go-zero 内置了完整的缓存管理方式。所以核心原则是:go-zero 缓存的一定是完整的行记录。 下面我们来详细介绍 go-zero 内置的三种场景的缓存处理方式: 基于主键的缓存PRIMARY KEY (`id`) 这种相对来讲是最容易处理的缓存,只需要在 redis 里用 primary key 作为 key 来缓存行记录即可。 基于唯一索引的缓存 在做基于索引的缓存设计的时候我借鉴了 database 索引的设计方法,在 database 设计里,如果通过索引去查数据时,引擎会先在 索引->主键 的 tree 里面查找到主键,然后再通过主键去查询行记录,就是引入了一个间接层去解决索引到行记录的对应问题。在 go-zero 的缓存设计里也是同样的原理。 基于索引的缓存又分为单列唯一索引和多列唯一索引: 但是对于 go-zero 来说,单列和多列只是生成缓存 key 的方式不同而已,背后的控制逻辑是一样的。然后 go-zero 内置的缓存管理就比较好的控制了数据一致性问题,同时也内置防止了缓存的击穿、穿透、雪崩问题(这些在 gopherchina 大会上分享的时候仔细讲过,见后续 gopherchina 分享视频)。 另外,go-zero 内置了缓存访问量、访问命中率统计,如下所示: dbcache(sqlc) - qpm: 5057, hit_ratio: 99.7%, hit: 5044, miss: 13, db_fails: 0 可以看到比较详细的统计信息,便于我们来分析缓存的使用情况,对于缓存命中率极低或者请求量极小的情况,我们就可以去掉缓存了,这样也可以降低成本。 单列唯一索引如下: UNIQUE KEY `product_idx` (`product`) 多列唯一索引如下: UNIQUE KEY `vendor_product_idx` (`vendor`, `product`) 缓存代码解读 1.基于主键的缓存逻辑 具体实现代码如下: func (cc CachedConn) QueryRow(v interface{}, key string, query QueryFn) error { return cc.cache.Take(v, key, func(v interface{}) error { return query(cc.db, v) }) } 这里的 Take 方法是先从缓存里去通过 key 拿数据,如果拿到就直接返回,如果拿不到,那么就通过 query 方法去 DB 读取完整行记录并写回缓存,然后再返回数据。整个逻辑还是比较简单易懂的。 我们详细看看 Take 的实现: func (c cacheNode) Take(v interface{}, key string, query func(v interface{}) error) error { return c.doTake(v, key, query, func(v interface{}) error { return c.SetCache(key, v) }) } Take 的逻辑如下: 用 key 从缓存里查找数据 如果找到,则返回数据 如果找不到,用 query 方法去读取数据 读到后调用 c.SetCache(key, v) 设置缓存 其中的 doTake 代码和解释如下: // v - 需要读取的数据对象 // key - 缓存key // query - 用来从DB读取完整数据的方法 // cacheVal - 用来写缓存的方法 func (c cacheNode) doTake(v interface{}, key string, query func(v interface{}) error, cacheVal func(v interface{}) error) error { // 用barrier来防止缓存击穿,确保一个进程内只有一个请求去加载key对应的数据 val, fresh, err := c.barrier.DoEx(key, func() (interface{}, error) { // 从cache里读取数据 if err := c.doGetCache(key, v); err != nil { // 如果是预先放进来的placeholder(用来防止缓存穿透)的,那么就返回预设的errNotFound // 如果是未知错误,那么就直接返回,因为我们不能放弃缓存出错而直接把所有请求去请求DB, // 这样在高并发的场景下会把DB打挂掉的 if err == errPlaceholder { return nil, c.errNotFound } else if err != c.errNotFound { // why we just return the error instead of query from db, // because we don't allow the disaster pass to the DBs. // fail fast, in case we bring down the dbs. return nil, err } // 请求DB // 如果返回的error是errNotFound,那么我们就需要在缓存里设置placeholder,防止缓存穿透 if err = query(v); err == c.errNotFound { if err = c.setCacheWithNotFound(key); err != nil { logx.Error(err) } return nil, c.errNotFound } else if err != nil { // 统计DB失败 c.stat.IncrementDbFails() return nil, err } // 把数据写入缓存 if err = cacheVal(v); err != nil { logx.Error(err) } } // 返回json序列化的数据 return jsonx.Marshal(v) }) if err != nil { return err } if fresh { return nil } // got the result from previous ongoing query c.stat.IncrementTotal() c.stat.IncrementHit() // 把数据写入到传入的v对象里 return jsonx.Unmarshal(val.([]byte), v) } 2. 基于唯一索引的缓存逻辑 因为这块比较复杂,所以我用不同颜色标识出来了响应的代码块和逻辑,block 2 其实跟基于主键的缓存是一样的,这里主要讲 block 1 的逻辑。 代码块的 block 1 部分分为两种情况: 通过索引能够从缓存里找到主键,此时就直接用主键走 block 2 的逻辑了,后续同上面基于主键的缓存逻辑 通过索引无法从缓存里找到主键 通过索引从DB里查询完整行记录,如有 error,返回 查到完整行记录后,会把主键到完整行记录的缓存和索引到主键的缓存同时写到 redis 里 返回所需的行记录数据 // v - 需要读取的数据对象 // key - 通过索引生成的缓存key // keyer - 用主键生成基于主键缓存的key的方法 // indexQuery - 用索引从DB读取完整数据的方法,需要返回主键 // primaryQuery - 用主键从DB获取完整数据的方法 func (cc CachedConn) QueryRowIndex(v interface{}, key string, keyer func(primary interface{}) string, indexQuery IndexQueryFn, primaryQuery PrimaryQueryFn) error { var primaryKey interface{} var found bool // 先通过索引查询缓存,看是否有索引到主键的缓存 if err := cc.cache.TakeWithExpire(&primaryKey, key, func(val interface{}, expire time.Duration) (err error) { // 如果没有索引到主键的缓存,那么就通过索引查询完整数据 primaryKey, err = indexQuery(cc.db, v) if err != nil { return } // 通过索引查询到了完整数据,设置found,后面直接使用,不需要再从缓存读取数据了 found = true // 将主键到完整数据的映射保存到缓存里,TakeWithExpire方法已经将索引到主键的映射保存到缓存了 return cc.cache.SetCacheWithExpire(keyer(primaryKey), v, expire+cacheSafeGapBetweenIndexAndPrimary) }); err != nil { return err } // 已经通过索引找到了数据,直接返回即可 if found { return nil } // 通过主键从缓存读取数据,如果缓存没有,通过primaryQuery方法从DB读取并回写缓存再返回数据 return cc.cache.Take(v, keyer(primaryKey), func(v interface{}) error { return primaryQuery(cc.db, v, primaryKey) }) } 我们来看一个实际的例子 func (m *defaultUserModel) FindOneByUser(user string) (*User, error) { var resp User // 生成基于索引的key indexKey := fmt.Sprintf(\"%s%v\", cacheUserPrefix, user) err := m.QueryRowIndex(&resp, indexKey, // 基于主键生成完整数据缓存的key func(primary interface{}) string { return fmt.Sprintf(\"user#%v\", primary) }, // 基于索引的DB查询方法 func(conn sqlx.SqlConn, v interface{}) (i interface{}, e error) { query := fmt.Sprintf(\"select %s from %s where user = ? limit 1\", userRows, m.table) if err := conn.QueryRow(&resp, query, user); err != nil { return nil, err } return resp.Id, nil }, // 基于主键的DB查询方法 func(conn sqlx.SqlConn, v, primary interface{}) error { query := fmt.Sprintf(\"select %s from %s where id = ?\", userRows, m.table) return conn.QueryRow(&resp, query, primary) }) // 错误处理,需要判断是否返回的是sqlc.ErrNotFound,如果是,我们用本package定义的ErrNotFound返回 // 避免使用者感知到有没有使用缓存,同时也是对底层依赖的隔离 switch err { case nil: return &resp, nil case sqlc.ErrNotFound: return nil, ErrNotFound default: return nil, err } } 所有上面这些缓存的自动管理代码都是可以通过 goctl 自动生成的,我们团队内部 CRUD 和缓存基本都是通过 goctl 自动生成的,可以节省大量开发时间,并且缓存代码本身也是非常容易出错的,即使有很好的代码经验,也很难每次完全写对,所以我们推荐尽可能使用自动的缓存代码生成工具去避免错误。 猜你想看 Go开源说第四期-go-zero缓存如何设计 Goctl Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:49 "},"buiness-cache.html":{"url":"buiness-cache.html","title":"go-zero缓存设计之业务层缓存","keywords":"","body":"go-zero缓存设计之业务层缓存 在上一篇go-zero缓存设计之持久层缓存介绍了db层缓存,回顾一下,db层缓存主要设计可以总结为: 缓存只删除不更新 行记录始终只存储一份,即主键对应行记录 唯一索引仅缓存主键值,不直接缓存行记录(参考mysql索引思想) 防缓存穿透设计,默认一分钟 不缓存多行记录 前言 在大型业务系统中,通过对持久层添加缓存,对于大多数单行记录查询,相信缓存能够帮持久层减轻很大的访问压力,但在实际业务中,数据读取不仅仅只是单行记录, 面对大量多行记录的查询,这对持久层也会造成不小的访问压力,除此之外,像秒杀系统、选课系统这种高并发的场景,单纯靠持久层的缓存是不现实的,本节我们来 介绍go-zero实践中的缓存设计——biz缓存。 适用场景举例 选课系统 内容社交系统 秒杀 ... 像这些系统,我们可以在业务层再增加一层缓存来存储系统中的关键信息,如选课系统中学生选课信息,课程剩余名额;内容社交系统中某一段时间之间的内容信息等。 接下来,我们一内容社交系统来进行举例说明。 在内容社交系统中,我们一般是先查询一批内容列表,然后点击某条内容查看详情, 在没有添加biz缓存前,内容信息的查询流程图应该为: 从图以及上一篇文章go-zero缓存设计之持久层缓存中我们可以知道,内容列表的获取是没办法依赖缓存的, 如果我们在业务层添加一层缓存用来存储列表中的关键信息(甚至完整信息),那么多行记录的访问不在是一个问题,这就是biz redis要做的事情。 接下来我们来看一下设计方案,假设内容系统中单行记录包含以下字段 字段名称 字段类型 备注 id string 内容id title string 标题 content string 详细内容 createTime time.Time 创建时间 我们的目标是获取一批内容列表,而尽量避免内容列表走db造成访问压力,首先我们采用redis的sort set数据结构来存储,根需要存储的字段信息量,有两种redis存储方案: 缓存局部信息 对其关键字段信息(如:id等)按照一定规则压缩,并存储,score我们用createTime毫秒值(时间值相等这里不讨论),这种存储方案的好处是节约redis存储空间, 那另一方面,缺点就是需要对列表详细内容进行二次回查(但这次回查是会利用到持久层的行记录缓存的) 缓存完整信息 对发布的所有内容按照一定规则压缩后均进行存储,同样score我们还是用createTime毫秒值,这种存储方案的好处是业务的增、删、查、改均走redis,而db层这时候 就可以不用考虑行记录缓存了,持久层仅提供数据备份和恢复使用,从另一方面来看,其缺点也很明显,需要的存储空间、配置要求更高,费用也会随之增大。 示例代码: type Content struct { Id string `json:\"id\"` Title string `json:\"title\"` Content string `json:\"content\"` CreateTime time.Time `json:\"create_time\"` } const bizContentCacheKey = `biz#content#cache` // AddContent 提供内容存储 func AddContent(r redis.Redis, c *Content) error { v := compress(c) _, err := r.Zadd(bizContentCacheKey, c.CreateTime.UnixNano()/1e6, v) return err } // DelContent 提供内容删除 func DelContent(r redis.Redis, c *Content) error { v := compress(c) _, err := r.Zrem(bizContentCacheKey, v) return err } // 内容压缩 func compress(c *Content) string { // todo: do it yourself var ret string return ret } // 内容解压 func unCompress(v string) *Content { // todo: do it yourself var ret Content return &ret } // ListByRangeTime提供根据时间段进行数据查询 func ListByRangeTime(r redis.Redis, start, end time.Time) ([]*Content, error) { kvs, err := r.ZrangebyscoreWithScores(bizContentCacheKey, start.UnixNano()/1e6, end.UnixNano()/1e6) if err != nil { return nil, err } var list []*Content for _, kv := range kvs { data:=unCompress(kv.Key) list = append(list, data) } return list, nil } 在以上例子中,redis是没有设置过期时间的,我们将增、删、改、查操作均同步到redis,我们认为内容社交系统的列表访问请求是比较高的情况下才做这样的方案设计, 除此之外,还有一些数据访问,没有想内容设计系统这么频繁的访问, 可能是某一时间段内访问量突如其来的增加,之后可能很长一段时间才会再访问一次,以此间隔, 或者说不会再访问了,面对这种场景,如果我又该如何考虑缓存的设计呢?在go-zero内容实践中,有两种方案可以解决这种问题: 增加内存缓存:通过内存缓存来存储当前可能突发访问量比较大的数据,常用的存储方案采用map数据结构来存储,map数据存储实现比较简单,但缓存过期处理则需要增加 定时器来出来,另一宗方案是通过go-zero库中的 Cache ,其是专门 用于内存管理. 采用biz redis,并设置合理的过期时间 总结 以上两个场景可以包含大部分的多行记录缓存,对于多行记录查询量不大的场景,暂时没必要直接把biz redis放进去,可以先尝试让db来承担,开发人员可以根据持久层监控及服务 监控来衡量时候需要引入biz。 Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:49 "},"go-queue.html":{"url":"go-queue.html","title":"go-zero分布式定时任务","keywords":"","body":" go-zero 分布式定时任务 日常任务开发中,我们会有很多异步、批量、定时、延迟任务要处理,go-zero中有go-queue,推荐使用go-queue去处理,go-queue本身也是基于go-zero开发的,其本身是有两种模式 dq : 依赖于beanstalkd,分布式,可存储,延迟、定时设置,关机重启可以重新执行,消息不会丢失,使用非常简单,go-queue中使用了redis setnx保证了每条消息只被消费一次,使用场景主要是用来做日常任务使用 kq:依赖于kafka,这个就不多介绍啦,大名鼎鼎的kafka,使用场景主要是做消息队列 我们主要说一下dq,kq使用也一样的,只是依赖底层不同,如果没使用过beanstalkd,没接触过beanstalkd的可以先google一下,使用起来还是挺容易的。 etc/job.yaml : 配置文件 Name: job Log: ServiceName: job Level: info #dq依赖Beanstalks、redis ,Beanstalks配置、redis配置 DqConf: Beanstalks: - Endpoint: 127.0.0.1:7771 Tube: tube1 - Endpoint: 127.0.0.1:7772 Tube: tube2 Redis: Host: 127.0.0.1:6379 Type: node Internal/config/config.go :解析dq对应etc/*.yaml配置 /** * @Description 配置文件 * @Author Mikael * @Email [email protected] * @Date 2021/1/18 12:05 * @Version 1.0 **/ package config import ( \"github.com/zeromicro/go-queue/dq\" \"github.com/zeromicro/go-zero/core/service\" ) type Config struct { service.ServiceConf DqConf dq.DqConf } Handler/router.go : 负责注册多任务 /** * @Description 注册job * @Author Mikael * @Email [email protected] * @Date 2021/1/18 12:05 * @Version 1.0 **/ package handler import ( \"context\" \"github.com/zeromicro/go-zero/core/service\" \"job/internal/logic\" \"job/internal/svc\" ) func RegisterJob(serverCtx *svc.ServiceContext,group *service.ServiceGroup) { group.Add(logic.NewProducerLogic(context.Background(),serverCtx)) group.Add(logic.NewConsumerLogic(context.Background(),serverCtx)) } ProducerLogic: 其中一个job业务逻辑 /** * @Description 生产者任务 * @Author Mikael * @Email [email protected] * @Date 2021/1/18 12:05 * @Version 1.0 **/ package logic import ( \"context\" \"github.com/zeromicro/go-queue/dq\" \"github.com/zeromicro/go-zero/core/logx\" \"github.com/zeromicro/go-zero/core/threading\" \"job/internal/svc\" \"strconv\" \"time\" ) type Producer struct { ctx context.Context svcCtx *svc.ServiceContext logx.Logger } func NewProducerLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Producer { return &Producer{ ctx: ctx, svcCtx: svcCtx, Logger: logx.WithContext(ctx), } } func (l *Producer)Start() { logx.Infof(\"start Producer \\n\") threading.GoSafe(func() { producer := dq.NewProducer([]dq.Beanstalk{ { Endpoint: \"localhost:7771\", Tube: \"tube1\", }, { Endpoint: \"localhost:7772\", Tube: \"tube2\", }, }) for i := 1000; i 另外一个Job业务逻辑 /** * @Description 消费者任务 * @Author Mikael * @Email [email protected] * @Date 2021/1/18 12:05 * @Version 1.0 **/ package logic import ( \"context\" \"github.com/zeromicro/go-zero/core/logx\" \"github.com/zeromicro/go-zero/core/threading\" \"job/internal/svc\" ) type Consumer struct { ctx context.Context svcCtx *svc.ServiceContext logx.Logger } func NewConsumerLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Consumer { return &Consumer{ ctx: ctx, svcCtx: svcCtx, Logger: logx.WithContext(ctx), } } func (l *Consumer)Start() { logx.Infof(\"start consumer \\n\") threading.GoSafe(func() { l.svcCtx.Consumer.Consume(func(body []byte) { logx.Infof(\"consumer job %s \\n\" ,string(body)) }) }) } func (l *Consumer)Stop() { logx.Infof(\"stop consumer \\n\") } svc/servicecontext.go /** * @Description 配置 * @Author Mikael * @Email [email protected] * @Date 2021/1/18 12:05 * @Version 1.0 **/ package svc import ( \"job/internal/config\" \"github.com/zeromicro/go-queue/dq\" ) type ServiceContext struct { Config config.Config Consumer dq.Consumer } func NewServiceContext(c config.Config) *ServiceContext { return &ServiceContext{ Config: c, Consumer: dq.NewConsumer(c.DqConf), } } main.go启动文件 /** * @Description 启动文件 * @Author Mikael * @Email [email protected] * @Date 2021/1/18 12:05 * @Version 1.0 **/ package main import ( \"flag\" \"fmt\" \"github.com/zeromicro/go-zero/core/conf\" \"github.com/zeromicro/go-zero/core/logx\" \"github.com/zeromicro/go-zero/core/service\" \"job/internal/config\" \"job/internal/handler\" \"job/internal/svc\" \"os\" \"os/signal\" \"syscall\" \"time\" ) var configFile = flag.String(\"f\", \"etc/job.yaml\", \"the config file\") func main() { flag.Parse() //配置 var c config.Config conf.MustLoad(*configFile, &c) ctx := svc.NewServiceContext(c) //注册job group := service.NewServiceGroup() handler.RegisterJob(ctx,group) //捕捉信号 ch := make(chan os.Signal) signal.Notify(ch, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT) go func() { for { s := 常见问题: 为什么使用dq,需要使用redis? 因为beanstalk是单点服务,无法保证高可用。dq可以使用多个单点beanstalk服务,互相备份 & 保证高可用。使用redis解决重复消费问题。 Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:49 "},"datacenter.html":{"url":"datacenter.html","title":"我是如何用go-zero 实现一个中台系统","keywords":"","body":"我是如何用 go-zero 实现一个中台系统 作者:Jack Luo 原文连接:https://www.cnblogs.com/jackluo/p/14148518.html [TOC] 最近发现golang社区里出了一个新星的微服务框架,来自好未来,光看这个名字,就很有奔头,之前,也只是玩过go-micro,其实真正的还没有在项目中运用过,只是觉得 微服务,grpc 这些很高大尚,还没有在项目中,真正的玩过,我看了一下官方提供的工具真的很好用,只需要定义好,舒适文件jia结构 都生成了,只需要关心业务,加上最近 有个投票的活动,加上最近这几年中台也比较火,所以决定玩一下, 开源地址: https://github.com/jackluo2012/datacenter 先聊聊中台架构思路吧: 中台的概念大概就是把一个一个的app 统一起来,反正我是这样理解的。 先聊用户服务吧,现在一个公司有很多的公众号、小程序、微信的、支付宝的,还有 xxx xxx,很多的平台,每次开发的时候,我们总是需要做用户登陆的服务,不停的复制代码,然后我们就在思考能不能有一套独立的用户服务,只需要告诉我你需要传个你要登陆的平台(比如微信),微信登陆,需要的是客户端返回给服务端一个code ,然后服务端拿着这个code去微信获取用户信息,反正大家都明白。 我们决定,将所有的信息弄到配置公共服务中去,里面再存微信、支付宝以及其它平台的appid、appkey、还有支付的appid、appkey,这样就写一套。 最后说说实现吧,整个就一个repo: 网关,我们用的是: go-zero的Api服务 其它它的是服务,我们就是用的go-zero的rpc服务 看下目录结构 整个项目完成,我一个人操刀,写了1个来星期,我就实现了上面的中台系统。 datacenter-api服务 先看官方文档 https://go-zero.dev/cn/ 我们先把网关搭建起来: ➜ blogs mkdir datacenter && cd datacenter ➜ datacenter go mod init datacenter go: creating new go.mod: module datacenter ➜ datacenter 查看book目录: ➜ datacenter tree . └── go.mod 0 directories, 1 file 创建api文件 ➜ datacenter goctl api -o datacenter.api Done. ➜ datacenter tree . ├── datacenter.api ├── user.api #用户 ├── votes.api #投票 ├── search.api #搜索 ├── questions.api #问答 └── go.mod 定义api服务 分别包含了上面的 公共服务,用户服务,投票活动服务 datacenter.api的内容: info( title: \"中台系统\"// TODO: add title desc: \"中台系统\"// TODO: add description author: \"jackluo\" email: \"[email protected]\" ) import \"user.api\" import \"votes.api\" import \"search.api\" import \"questions.api\" //获取 应用信息 type Beid { Beid int64 `json:\"beid\"` } type Token { Token string `json:\"token\"` } type WxTicket { Ticket string `json:\"ticket\"` } type Application { Sname string `json:\"Sname\"` //名称 Logo string `json:\"logo\"` // login Isclose int64 `json:\"isclose\"` //是否关闭 Fullwebsite string `json:\"fullwebsite\"` // 全站名称 } type SnsReq { Beid Ptyid int64 `json:\"ptyid\"` //对应平台 BackUrl string `json:\"back_url\"` //登陆返回的地址 } type SnsResp { Beid Ptyid int64 `json:\"ptyid\"` //对应平台 Appid string `json:\"appid\"` //sns 平台的id Title string `json:\"title\"` //名称 LoginUrl string `json:\"login_url\"` //微信登陆的地址 } type WxShareResp { Appid string `json:\"appid\"` Timestamp int64 `json:\"timestamp\"` Noncestr string `json:\"noncestr\"` Signature string `json:\"signature\"` } @server( group: common ) service datacenter-api { @doc( summary: \"获取站点的信息\" ) @handler appInfo get /common/appinfo (Beid) returns (Application) @doc( summary: \"获取站点的社交属性信息\" ) @handler snsInfo post /common/snsinfo (SnsReq) returns (SnsResp) //获取分享的 @handler wxTicket post /common/wx/ticket (SnsReq) returns (WxShareResp) } //上传需要登陆 @server( jwt: Auth group: common ) service datacenter-api { @doc( summary: \"七牛上传凭证\" ) @handler qiuniuToken post /common/qiuniu/token (Beid) returns (Token) } user.api内容 //注册请求 type RegisterReq struct { // TODO: add members here and delete this comment Mobile string `json:\"mobile\"` //基本一个手机号码就完事 Password string `json:\"password\"` Smscode string `json:\"smscode\"` //短信码 } //登陆请求 type LoginReq struct{ Mobile string `json:\"mobile\"` Type int64 `json:\"type\"` //1.密码登陆,2.短信登陆 Password string `json:\"password\"` } //微信登陆 type WxLoginReq struct { Beid int64 `json:\"beid\"` //应用id Code string `json:\"code\"` //微信登陆密钥 Ptyid int64 `json:\"ptyid\"` //对应平台 } //返回用户信息 type UserReply struct { Auid int64 `json:\"auid\"` Uid int64 `json:\"uid\"` Beid int64 `json:\"beid\"` //应用id Ptyid int64 `json:\"ptyid\"` //对应平台 Username string `json:\"username\"` Mobile string `json:\"mobile\"` Nickname string `json:\"nickname\"` Openid string `json:\"openid\"` Avator string `json:\"avator\"` JwtToken } //返回APPUser type AppUser struct{ Uid int64 `json:\"uid\"` Auid int64 `json:\"auid\"` Beid int64 `json:\"beid\"` //应用id Ptyid int64 `json:\"ptyid\"` //对应平台 Nickname string `json:\"nickname\"` Openid string `json:\"openid\"` Avator string `json:\"avator\"` } type LoginAppUser struct{ Uid int64 `json:\"uid\"` Auid int64 `json:\"auid\"` Beid int64 `json:\"beid\"` //应用id Ptyid int64 `json:\"ptyid\"` //对应平台 Nickname string `json:\"nickname\"` Openid string `json:\"openid\"` Avator string `json:\"avator\"` JwtToken } type JwtToken struct { AccessToken string `json:\"access_token,omitempty\"` AccessExpire int64 `json:\"access_expire,omitempty\"` RefreshAfter int64 `json:\"refresh_after,omitempty\"` } type UserReq struct{ Auid int64 `json:\"auid\"` Uid int64 `json:\"uid\"` Beid int64 `json:\"beid\"` //应用id Ptyid int64 `json:\"ptyid\"` //对应平台 } type Request { Name string `path:\"name,options=you|me\"` } type Response { Message string `json:\"message\"` } @server( group: user ) service datacenter-api { @handler ping post /user/ping () @handler register post /user/register (RegisterReq) returns (UserReply) @handler login post /user/login (LoginReq) returns (UserReply) @handler wxlogin post /user/wx/login (WxLoginReq) returns (LoginAppUser) @handler code2Session get /user/wx/login () returns (LoginAppUser) } @server( jwt: Auth group: user middleware: Usercheck ) service datacenter-api { @handler userInfo get /user/dc/info (UserReq) returns (UserReply) } votes.api 投票内容 // 投票活动api type Actid struct { Actid int64 `json:\"actid\"` //活动id } type VoteReq struct { Aeid int64 `json:\"aeid\"` // 作品id Actid } type VoteResp struct { VoteReq Votecount int64 `json:\"votecount\"` //投票票数 Viewcount int64 `json:\"viewcount\"` //浏览数 } // 活动返回的参数 type ActivityResp struct { Actid int64 `json:\"actid\"` Title string `json:\"title\"` //活动名称 Descr string `json:\"descr\"` //活动描述 StartDate int64 `json:\"start_date\"` //活动时间 EnrollDate int64 `json:\"enroll_date\"` //投票时间 EndDate int64 `json:\"end_date\"` //活动结束时间 Votecount int64 `json:\"votecount\"` //当前活动的总票数 Viewcount int64 `json:\"viewcount\"` //当前活动的总浏览数 Type int64 `json:\"type\"` //投票方式 Num int64 `json:\"num\"` //投票几票 } //报名 type EnrollReq struct { Actid Name string `json:\"name\"` // 名称 Address string `json:\"address\"` //地址 Images []string `json:\"images\"` //作品图片 Descr string `json:\"descr\"` // 作品描述 } // 作品返回 type EnrollResp struct { Actid Aeid int64 `json:\"aeid\"` // 作品id Name string `json:\"name\"` // 名称 Address string `json:\"address\"` //地址 Images []string `json:\"images\"` //作品图片 Descr string `json:\"descr\"` // 作品描述 Votecount int64 `json:\"votecount\"` //当前活动的总票数 Viewcount int64 `json:\"viewcount\"` //当前活动的总浏览数 } @server( group: votes ) service datacenter-api { @doc( summary: \"获取活动的信息\" ) @handler activityInfo get /votes/activity/info (Actid) returns (ActivityResp) @doc( summary: \"活动访问+1\" ) @handler activityIcrView get /votes/activity/view (Actid) returns (ActivityResp) @doc( summary: \"获取报名的投票作品信息\" ) @handler enrollInfo get /votes/enroll/info (VoteReq) returns (EnrollResp) @doc( summary: \"获取报名的投票作品列表\" ) @handler enrollLists get /votes/enroll/lists (Actid) returns(EnrollResp) } @server( jwt: Auth group: votes middleware: Usercheck ) service datacenter-api { @doc( summary: \"投票\" ) @handler vote post /votes/vote (VoteReq) returns (VoteResp) @handler enroll post /votes/enroll (EnrollReq) returns (EnrollResp) } questions.api 问答内容: // 问答 抽奖 开始 @server( group: questions ) service datacenter-api { @doc( summary: \"获取活动的信息\" ) @handler activitiesInfo get /questions/activities/info (Actid) returns (ActivityResp) @doc( summary: \"获取奖品信息\" ) @handler awardInfo get /questions/award/info (Actid) returns (ActivityResp) @handler awardList get /questions/award/list (Actid) returns (ActivityResp) } type AnswerReq struct { ActivityId int64 `json:\"actid\"` Answers string `json:\"answers\"` Score string `json:\"score\"` } type QuestionsAwardReq struct { ActivityId int64 `json:\"actid\"` AnswerId int64 `json:\"answerid\"` } type AnswerResp struct { Answers string `json:\"answers\"` Score string `json:\"score\"` } type AwardConvertReq struct { UserName string `json:\"username\"` Phone string `json:\"phone\"` LotteryId int64 `json:\"lotteryid\"` } @server( jwt: Auth group: questions middleware: Usercheck ) service datacenter-api { @doc( summary: \"获取题目\" ) @handler lists get /questions/lists (VoteReq) returns (AnswerResp) @doc( summary: \"提交答案\" ) @handler change post /questions/change (AnswerReq) returns (VoteResp) @doc( summary: \"获取分数\" ) @handler grade get /questions/grade (VoteReq) returns (VoteResp) @doc( summary: \"开始转盘\" ) @handler turntable post /questions/lottery/turntable (EnrollReq) returns (EnrollResp) @doc( summary: \"填写中奖信息人\" ) @handler lottery post /questions/lottery/convert (AwardConvertReq) returns (EnrollResp) } // 问答 抽奖 结束 search.api 搜索 type SearchReq struct { Keyword string `json:\"keyword\"` Page string `json:\"page\"` Size string `json:\"size\"` } type SearchResp struct { Data []ArticleReq `json:\"data\"` } type ArticleReq struct{ NewsId string `json:\"NewsId\"` NewsTitle string `json:\"NewsTitle\"` ImageUrl string `json:\"ImageUrl\"` } @server( group: search middleware: Admincheck ) service datacenter-api { @doc( summary: \"搜索\" ) @handler article get /search/article (SearchReq) returns (SearchResp) @handler articleInit get /search/articel/init (SearchReq) returns (SearchResp) @handler articleStore post /search/articel/store (ArticleReq) returns (ArticleReq) } 上面基本上写就写的API及文档的思路 生成datacenter api服务 ➜ datacenter goctl api go -api datacenter.api -dir . Done. ➜ datacenter treer . ├── datacenter.api ├── etc │ └── datacenter-api.yaml ├── go.mod ├── internal │ ├── config │ │ └── config.go │ ├── handler │ │ ├── common │ │ │ ├── appinfohandler.go │ │ │ ├── qiuniutokenhandler.go │ │ │ ├── snsinfohandler.go │ │ │ ├── votesverificationhandler.go │ │ │ └── wxtickethandler.go │ │ ├── routes.go │ │ ├── user │ │ │ ├── code2sessionhandler.go │ │ │ ├── loginhandler.go │ │ │ ├── pinghandler.go │ │ │ ├── registerhandler.go │ │ │ ├── userinfohandler.go │ │ │ └── wxloginhandler.go │ │ └── votes │ │ ├── activityicrviewhandler.go │ │ ├── activityinfohandler.go │ │ ├── enrollhandler.go │ │ ├── enrollinfohandler.go │ │ ├── enrolllistshandler.go │ │ └── votehandler.go │ ├── logic │ │ ├── common │ │ │ ├── appinfologic.go │ │ │ ├── qiuniutokenlogic.go │ │ │ ├── snsinfologic.go │ │ │ ├── votesverificationlogic.go │ │ │ └── wxticketlogic.go │ │ ├── user │ │ │ ├── code2sessionlogic.go │ │ │ ├── loginlogic.go │ │ │ ├── pinglogic.go │ │ │ ├── registerlogic.go │ │ │ ├── userinfologic.go │ │ │ └── wxloginlogic.go │ │ └── votes │ │ ├── activityicrviewlogic.go │ │ ├── activityinfologic.go │ │ ├── enrollinfologic.go │ │ ├── enrolllistslogic.go │ │ ├── enrolllogic.go │ │ └── votelogic.go │ ├── middleware │ │ └── usercheckmiddleware.go │ ├── svc │ │ └── servicecontext.go │ └── types │ └── types.go └── datacenter.go 14 directories, 43 files 我们打开 etc/datacenter-api.yaml 把必要的配置信息加上 Name: datacenter-api Log: Mode: console Host: 0.0.0.0 Port: 8857 Auth: AccessSecret: 你的jwtwon Secret AccessExpire: 86400 CacheRedis: - Host: 127.0.0.1:6379 Pass: 密码 Type: node UserRpc: Etcd: Hosts: - 127.0.0.1:2379 Key: user.rpc CommonRpc: Etcd: Hosts: - 127.0.0.1:2379 Key: common.rpc VotesRpc: Etcd: Hosts: - 127.0.0.1:2379 Key: votes.rpc 上面的 UserRpc, CommonRpc ,还有 VotesRpc 这些我先写上,后面再来慢慢加。 我们先来写 CommonRpc 服务。 CommonRpc服务 新建项目目录 ➜ datacenter mkdir -p common/rpc && cd common/rpc 直接就新建在了,datacenter目录中,因为common 里面,可能以后会不只会提供rpc服务,可能还有api的服务,所以又加了rpc目录 goctl创建模板 ➜ rpc goctl rpc template -o=common.proto ➜ rpc ls common.proto 往里面填入内容: ➜ rpc cat common.proto syntax = \"proto3\"; option go_package = \"common\"; package common; message BaseAppReq{ int64 beid=1; } message BaseAppResp{ int64 beid=1; string logo=2; string sname=3; int64 isclose=4; string fullwebsite=5; } // 请求的api message AppConfigReq { int64 beid=1; int64 ptyid=2; } // 返回的值 message AppConfigResp { int64 id=1; int64 beid=2; int64 ptyid=3; string appid=4; string appsecret=5; string title=6; } service Common { rpc GetAppConfig(AppConfigReq) returns(AppConfigResp); rpc GetBaseApp(BaseAppReq) returns(BaseAppResp); } gotcl生成rpc服务 ➜ rpc goctl rpc proto -src common.proto -dir . protoc -I=/Users/jackluo/works/blogs/datacenter/common/rpc common.proto --go_out=plugins=grpc:/Users/jackluo/works/blogs/datacenter/common/rpc/common Done. ➜ rpc tree . ├── common │ └── common.pb.go ├── common.go ├── common.proto ├── commonclient │ └── common.go ├── etc │ └── common.yaml └── internal ├── config │ └── config.go ├── logic │ ├── getappconfiglogic.go │ └── getbaseapplogic.go ├── server │ └── commonserver.go └── svc └── servicecontext.go 8 directories, 10 files 基本上,就把所有的目录规范和结构的东西都生成了,就不用纠结项目目录了,怎么放了,怎么组织了。 看一下,配置信息,里面可以写入mysql和其它redis的信息: Name: common.rpc ListenOn: 127.0.0.1:8081 Mysql: DataSource: root:admin@tcp(127.0.0.1:3306)/datacenter?charset=utf8&parseTime=true&loc=Asia%2FShanghai CacheRedis: - Host: 127.0.0.1:6379 Pass: Type: node Etcd: Hosts: - 127.0.0.1:2379 Key: common.rpc 我们再来加上数据库服务: ➜ rpc cd .. ➜ common ls rpc ➜ common pwd /Users/jackluo/works/blogs/datacenter/common ➜ common goctl model mysql datasource -url=\"root:admin@tcp(127.0.0.1:3306)/datacenter\" -table=\"base_app\" -dir ./model -c Done. ➜ common tree . ├── model │ ├── baseappmodel.go │ └── vars.go └── rpc ├── common │ └── common.pb.go ├── common.go ├── common.proto ├── commonclient │ └── common.go ├── etc │ └── common.yaml └── internal ├── config │ └── config.go ├── logic │ ├── getappconfiglogic.go │ └── getbaseapplogic.go ├── server │ └── commonserver.go └── svc └── servicecontext.go 10 directories, 12 files 这样基本的一个 rpc 就写完了,然后我们将rpc 和model 还有api串连起来,这个官方的文档已经很详细了,这里就只是贴一下代码: ➜ common cat rpc/internal/config/config.go package config import ( \"github.com/zeromicro/go-zero/core/stores/cache\" \"github.com/zeromicro/go-zero/zrpc\" ) type Config struct { zrpc.RpcServerConf Mysql struct { DataSource string } CacheRedis cache.ClusterConf } 再在svc中修改: ➜ common cat rpc/internal/svc/servicecontext.go package svc import ( \"datacenter/common/model\" \"datacenter/common/rpc/internal/config\" \"github.com/zeromicro/go-zero/core/stores/sqlx\" ) type ServiceContext struct { c config.Config AppConfigModel model.AppConfigModel BaseAppModel model.BaseAppModel } func NewServiceContext(c config.Config) *ServiceContext { conn := sqlx.NewMysql(c.Mysql.DataSource) apm := model.NewAppConfigModel(conn, c.CacheRedis) bam := model.NewBaseAppModel(conn, c.CacheRedis) return &ServiceContext{ c: c, AppConfigModel: apm, BaseAppModel: bam, } } 上面的代码已经将 rpc 和 model 数据库关联起来了,我们现在再将 rpc 和 api 关联起来: ➜ datacenter cat internal/config/config.go package config import ( \"github.com/zeromicro/go-zero/core/stores/cache\" \"github.com/zeromicro/go-zero/rest\" \"github.com/zeromicro/go-zero/zrpc\" ) type Config struct { rest.RestConf Auth struct { AccessSecret string AccessExpire int64 } UserRpc zrpc.RpcClientConf CommonRpc zrpc.RpcClientConf VotesRpc zrpc.RpcClientConf CacheRedis cache.ClusterConf } 加入 svc 服务中: ➜ datacenter cat internal/svc/servicecontext.go package svc import ( \"context\" \"datacenter/common/rpc/commonclient\" \"datacenter/internal/config\" \"datacenter/internal/middleware\" \"datacenter/shared\" \"datacenter/user/rpc/userclient\" \"datacenter/votes/rpc/votesclient\" \"fmt\" \"net/http\" \"time\" \"github.com/zeromicro/go-zero/core/logx\" \"github.com/zeromicro/go-zero/core/stores/cache\" \"github.com/zeromicro/go-zero/core/stores/redis\" \"github.com/zeromicro/go-zero/core/syncx\" \"github.com/zeromicro/go-zero/rest\" \"github.com/zeromicro/go-zero/zrpc\" \"google.golang.org/grpc\" ) type ServiceContext struct { Config config.Config GreetMiddleware1 rest.Middleware GreetMiddleware2 rest.Middleware Usercheck rest.Middleware UserRpc userclient.User //用户 CommonRpc commonclient.Common VotesRpc votesclient.Votes Cache cache.Cache RedisConn *redis.Redis } func timeInterceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { stime := time.Now() err := invoker(ctx, method, req, reply, cc, opts...) if err != nil { return err } fmt.Printf(\"调用 %s 方法 耗时: %v\\n\", method, time.Now().Sub(stime)) return nil } func NewServiceContext(c config.Config) *ServiceContext { ur := userclient.NewUser(zrpc.MustNewClient(c.UserRpc, zrpc.WithUnaryClientInterceptor(timeInterceptor))) cr := commonclient.NewCommon(zrpc.MustNewClient(c.CommonRpc, zrpc.WithUnaryClientInterceptor(timeInterceptor))) vr := votesclient.NewVotes(zrpc.MustNewClient(c.VotesRpc, zrpc.WithUnaryClientInterceptor(timeInterceptor))) //缓存 ca := cache.NewCache(c.CacheRedis, syncx.NewSharedCalls(), cache.NewCacheStat(\"dc\"), shared.ErrNotFound) rcon := redis.NewRedis(c.CacheRedis[0].Host, c.CacheRedis[0].Type, c.CacheRedis[0].Pass) return &ServiceContext{ Config: c, GreetMiddleware1: greetMiddleware1, GreetMiddleware2: greetMiddleware2, Usercheck: middleware.NewUserCheckMiddleware().Handle, UserRpc: ur, CommonRpc: cr, VotesRpc: vr, Cache: ca, RedisConn: rcon, } } 这样基本上,我们就可以在 logic 的文件目录中调用了: cat internal/logic/common/appinfologic.go package logic import ( \"context\" \"datacenter/internal/svc\" \"datacenter/internal/types\" \"datacenter/shared\" \"datacenter/common/model\" \"datacenter/common/rpc/common\" \"github.com/zeromicro/go-zero/core/logx\" ) type AppInfoLogic struct { logx.Logger ctx context.Context svcCtx *svc.ServiceContext } func NewAppInfoLogic(ctx context.Context, svcCtx *svc.ServiceContext) AppInfoLogic { return AppInfoLogic{ Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx, } } func (l *AppInfoLogic) AppInfo(req types.Beid) (appconfig *common.BaseAppResp, err error) { //检查 缓存中是否有值 err = l.svcCtx.Cache.GetCache(model.GetcacheBaseAppIdPrefix(req.Beid), appconfig) if err != nil && err == shared.ErrNotFound { appconfig, err = l.svcCtx.CommonRpc.GetBaseApp(l.ctx, &common.BaseAppReq{ Beid: req.Beid, }) if err != nil { return } err = l.svcCtx.Cache.SetCache(model.GetcacheBaseAppIdPrefix(req.Beid), appconfig) } return } 这样,基本就连接起来了,其它基本上就不用改了,UserRPC, VotesRPC 类似,这里就不在写了。 使用心得 go-zero 的确香,因为它有一个 goctl 的工具,他可以自动的把代码结构全部的生成好,我们就不再去纠结,目录结构 ,怎么组织,没有个好几年的架构能力是不好实现的,有什么规范那些,并发,熔断,完全不用,考虑其它的,专心的实现业务就好,像微服务,还要有服务发现,一系列的东西,都不用关心,因为 go-zero 内部已经实现了。 我写代码也写了有10多年了,之前一直用的 php,比较出名的就 laravel,thinkphp,基本上就是模块化的,像微服务那些实现真的有成本,但是你用上go-zero,你就像调api接口一样简单的开发,其它什么服务发现,那些根本就不用关注了,只需要关注业务。 一个好的语言,框架,他们的底层思维,永远都是效率高,不加班的思想,我相信go-zero会提高你和你团队或是公司的效率。go-zero的作者说,他们有个团队专门整理go-zero框架,目的也应该很明显,那就是提高,他们自己的开发效率,流程化,标准化,是提高工作效率的准则,像我们平时遇到了问题,或是遇到了bug,我第一个想到的不是怎么去解决我的bug,而是在想我的流程是不是有问题,我的哪个流程会导致bug,最后我相信 go-zero 能成为 微服务开发 的首选框架。 最后说说遇到的坑吧: grpc grpc 本人第一次用,然后就遇到了,有些字符为空时,字段值不显示的问题: 通过 grpc 官方库中的 jsonpb 来实现,官方在它的设定中有一个结构体用来实现 protoc buffer 转换为JSON结构,并可以根据字段来配置转换的要求。 跨域问题 go-zero 中设置了,感觉没有效果,大佬说通过nginx 设置,后面发现还是不行,最近强行弄到了一个域名下,后面有时间再解决。 sqlx go-zero 的 sqlx 问题,这个真的费了很长的时间: time.Time 这个数据结构,数据库中用的是 timestamp 这个 比如我的字段 是delete_at 默认数库设置的是null ,结果插入的时候,就报了 Incorrect datetime value: '0000-00-00' for column 'deleted_at' at row 1\"} 这个错,查询的时候报 deleted_at\\\": unsupported Scan, storing driver.Value type \\u003cnil\\u003e into type *time.Time\" 后面果断去掉了这个字段,字段上面加上 .omitempty 这个标签,好像也有用,db:\".omitempty\" 其次就是这个 Conversion from collation utf8_general_ci into utf8mb4_unicode_ci,这个导致的大概原因是,现在都喜欢用emj表情了,mysql数据识别不了。 数据连接 mysql 这边照样按照原始的方式,将配置文件修改编码格式,重新创建数据库,并且设置数据库编码为utf8mb4,排序规则为 utf8mb4_unicode_ci。 这样的话,所有的表还有string字段都是这个编码格式,如果不想所有的都是,可以单独设置,这个不是重点.因为在navicat上都好设置,手动点一下就行了。 重点来了:golang中使用的是 github.com/go-sql-driver/mysql 驱动,将连接 mysql的 dsn(因为我这使用的是gorm,所以dsn可能跟原生的格式不太一样,不过没关系, 只需要关注 charset 和 collation 就行了) root:password@/name?parseTime=True&loc=Local&charset=utf8 修改为: root:password@/name?parseTime=True&loc=Local&charset=utf8mb4&collation=utf8mb4_unicode_ci Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:49 "},"stream.html":{"url":"stream.html","title":"流数据处理利器","keywords":"","body":"流数据处理利器 流处理 (Stream processing) 是一种计算机编程范式,其允许给定一个数据序列 (流处理数据源),一系列数据操作 (函数) 被应用到流中的每个元素。同时流处理工具可以显著提高程序员的开发效率,允许他们编写有效、干净和简洁的代码。 流数据处理在我们的日常工作中非常常见,举个例子,我们在业务开发中往往会记录许多业务日志,这些日志一般是先发送到 Kafka,然后再由 Job 消费 Kafaka 写到 elasticsearch,在进行日志流处理的过程中,往往还会对日志做一些处理,比如过滤无效的日志,做一些计算以及重新组合日志等等,示意图如下: 流处理工具fx go-zero 是一个功能完备的微服务框架,框架中内置了很多非常实用的工具,其中就包含流数据处理工具fx ,下面我们通过一个简单的例子来认识下该工具: package main import ( \"fmt\" \"os\" \"os/signal\" \"syscall\" \"time\" \"github.com/zeromicro/go-zero/core/fx\" ) func main() { ch := make(chan int) go inputStream(ch) go outputStream(ch) c := make(chan os.Signal, 1) signal.Notify(c, syscall.SIGTERM, syscall.SIGINT) inputStream函数模拟了流数据的产生,outputStream函数模拟了流数据的处理过程,其中From函数为流的输入,Walk函数并发的作用在每一个item上,Filter函数对item进行过滤为true保留为false不保留,ForEach函数遍历输出每一个item元素。 流数据处理中间操作 一个流的数据处理可能存在许多的中间操作,每个中间操作都可以作用在流上。就像流水线上的工人一样,每个工人操作完零件后都会返回处理完成的新零件,同理流处理中间操作完成后也会返回一个新的流。 fx的流处理中间操作: 操作函数 功能 输入 Distinct 去除重复的item KeyFunc,返回需要去重的key Filter 过滤不满足条件的item FilterFunc,Option控制并发量 Group 对item进行分组 KeyFunc,以key进行分组 Head 取出前n个item,返回新stream int64保留数量 Map 对象转换 MapFunc,Option控制并发量 Merge 合并item到slice并生成新stream Reverse 反转item Sort 对item进行排序 LessFunc实现排序算法 Tail 与Head功能类似,取出后n个item组成新stream int64保留数量 Walk 作用在每个item上 WalkFunc,Option控制并发量 下图展示了每个步骤和每个步骤的结果: 用法与原理分析 From 通过From函数构建流并返回Stream,流数据通过channel进行存储: // 例子 s := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 0} fx.From(func(source chan Filter Filter函数提供过滤item的功能,FilterFunc定义过滤逻辑true保留item,false则不保留: // 例子 保留偶数 s := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 0} fx.From(func(source chan Group Group对流数据进行分组,需定义分组的key,数据分组后以slice存入channel: // 例子 按照首字符\"g\"或者\"p\"分组,没有则分到另一组 ss := []string{\"golang\", \"google\", \"php\", \"python\", \"java\", \"c++\"} fx.From(func(source chan Reverse reverse可以对流中元素进行反转处理: // 例子 fx.Just(1, 2, 3, 4, 5).Reverse().ForEach(func(item interface{}) { fmt.Println(item) }) // 源码 func (p Stream) Reverse() Stream { var items []interface{} // 获取流中数据 for item := range p.source { items = append(items, item) } // 反转算法 for i := len(items)/2 - 1; i >= 0; i-- { opp := len(items) - 1 - i items[i], items[opp] = items[opp], items[i] } // 写入流 return Just(items...) } Distinct distinct对流中元素进行去重,去重在业务开发中比较常用,经常需要对用户id等做去重操作: // 例子 fx.Just(1, 2, 2, 2, 3, 3, 4, 5, 6).Distinct(func(item interface{}) interface{} { return item }).ForEach(func(item interface{}) { fmt.Println(item) }) // 结果为 1,2,3,4,5,6 // 源码 func (p Stream) Distinct(fn KeyFunc) Stream { source := make(chan interface{}) threading.GoSafe(func() { defer close(source) // 通过key进行去重,相同key只保留一个 keys := make(map[interface{}]lang.PlaceholderType) for item := range p.source { key := fn(item) // key存在则不保留 if _, ok := keys[key]; !ok { source Walk Walk函数并发的作用在流中每一个item上,可以通过WithWorkers设置并发数,默认并发数为16,最小并发数为1,如设置unlimitedWorkers为true则并发数无限制,但并发写入流中的数据由defaultWorkers限制,WalkFunc中用户可以自定义后续写入流中的元素,可以不写入也可以写入多个元素: // 例子 fx.Just(\"aaa\", \"bbb\", \"ccc\").Walk(func(item interface{}, pipe chan 并发处理 fx工具除了进行流数据处理以外还提供了函数并发功能,在微服务中实现某个功能往往需要依赖多个服务,并发的处理依赖可以有效的降低依赖耗时,提升服务的性能。 fx.Parallel(func() { userRPC() // 依赖1 }, func() { accountRPC() // 依赖2 }, func() { orderRPC() // 依赖3 }) 注意fx.Parallel进行依赖并行处理的时候不会有error返回,如需有error返回或者有一个依赖报错需要立马结束依赖请求请使用MapReduce 工具进行处理。 总结 本篇文章介绍了流处理的基本概念和go-zero中的流处理工具fx,在实际的生产中流处理场景应用也非常多,希望本篇文章能给大家带来一定的启发,更好的应对工作中的流处理场景。 Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:50 "},"online-exchange.html":{"url":"online-exchange.html","title":"10月3日线上交流问题汇总","keywords":"","body":"10月3日线上交流问题汇总 go-zero适用场景 希望说说应用场景,各个场景下的优势 高并发的微服务系统 支撑千万级日活,百万级QPS 完整的微服务治理能力 支持自定义中间件 很好的管理了数据库和缓存 有效隔离故障 低并发的单体系统 这种系统直接使用api层即可,无需rpc服务 各个功能的使用场景以及使用案例 限流 熔断 降载 超时 可观测性 go-zero的实际体验 服务很稳 前后端接口一致性,一个api文件即可生成前后端代码 规范、代码量少,意味着bug少 免除api文档,极大降低沟通成本 代码结构完全一致,便于维护和接手 微服务的项目结构, monorepo的 CICD 处理 bookstore ├── api │ ├── etc │ └── internal │ ├── config │ ├── handler │ ├── logic │ ├── svc │ └── types └── rpc ├── add │ ├── adder │ ├── etc │ ├── internal │ │ ├── config │ │ ├── logic │ │ ├── server │ │ └── svc │ └── pb ├── check │ ├── checker │ ├── etc │ ├── internal │ │ ├── config │ │ ├── logic │ │ ├── server │ │ └── svc │ └── pb └── model mono repo的CI我们是通过gitlab做的,CD使用jenkins CI尽可能更严格的模式,比如-race,使用sonar等工具 CD有开发、测试、预发、灰度和正式集群 晚6点上灰度、无故障的话第二天10点自动同步到正式集群 正式集群分为多个k8s集群,有效的防止单集群故障,直接摘除即可,集群升级更有好 如何部署,如何监控? 全量K8S,通过jenkins自动打包成docker镜像,按照时间打包tag,这样可以一眼看出哪一天的镜像 上面已经讲了,预发->灰度->正式 Prometheus+自建dashboard服务 基于日志检测服务和请求异常 如果打算换go-zero框架重构业务,如何做好线上业务稳定安全用户无感切换?另外咨询下如何进行服务划分? 逐步替换,从外到内,加个proxy来校对,校对一周后可以切换 如有数据库重构,则需要做好新老同步 服务划分按照业务来,遵循从粗到细的原则,避免一个api一个微服务 数据拆分对于微服务来讲尤为重要,上层好拆,数据难拆,尽可能保证按照业务拆分数据 服务发现 服务发现 etcd 的 key 的设计 服务key+时间戳,服务进程数存在时间戳冲突的概率极低,忽略 etcd服务发现与治理, 异常捕获与处理异常 为啥k8s还使用etcd做服务发现,因为dns的刷新有延迟,导致滚动更新会有大量失败,而etcd可以做到完全无损更新 etcd集群直接部署在k8s集群内,因为多个正式集群,集群单点和注册避免混乱 针对etcd异常或者leader切换,自动侦测并刷新,当etcd有异常不能恢复时,不会刷新服务列表,保障服务依然可用 缓存的设计与使用案例 分布式多redis集群,线上最大几十个集群为同一个服务提供缓存服务 无缝扩缩容 不存在没有过期时间的缓存,避免大量不常使用的数据占用资源,默认一周 缓存穿透,没有的数据会短暂缓存一分钟,避免刷接口或大量不存在的数据请求带垮系统 缓存击穿,一个进程只会刷新一次同一个数据,避免热点数据被大量同时加载 缓存雪崩,对缓存过期时间自动做了jitter,5%的标准变差,使得一周的过期时间分布在16小时内,有效防止了雪崩 我们线上数据库都有缓存,否则无法支撑海量并发 自动缓存管理已经内置于go-zero,并可以通过goctl自动生成代码 能否讲解下, 中间件,拦截器的设计思想 洋葱模型 本中间件处理,比如限流,熔断等,然后决定是否调用next next调用 对next调用返回结果做处理 微服务的事务处理怎么实现好,gozero分布式事务设计和实现,有什么好中间件推荐 2PC,两阶段提交 TCC,Try-Confirm-Cancel 消息队列,最大尝试 人工补偿 多级 goroutine 的异常捕获 ,怎么设计比较好 微服务系统请求异常应该隔离,不能让单个异常请求带崩整个进程 go-zero自带了RunSafe/GoSafe,用来防止单个异常请求导致进程崩溃 监控需要跟上,防止异常过量而不自知 fail fast和故障隔离的矛盾点 k8s配置的生成与使用(gateway, service, slb) 内部自动生成k8s的yaml文件,过于依赖配置而未开源 打算在bookstore的示例里加上k8s配置样板 slb->nginx->nodeport->api gateway->rpc service gateway限流、熔断和降载 限流分为两种:并发控制和分布式限流 并发控制用来防止瞬间过量请求,保护系统不被打垮 分布式限流用来给不同服务配置不同的quota 熔断是为了对依赖的服务进行保护,当一个服务出现大量异常的时候,调用者应该给予保护,使其有机会恢复正常,同时也达到fail fast的效果 降载是为了保护当前进程资源耗尽而陷入彻底不可用,确保尽可能服务好能承载的最大请求量 降载配合k8s,可以有效保护k8s扩容,k8s扩容分钟级,go-zero降载秒级 介绍core中好用的组件,如timingwheel等,讲讲设计思路 布隆过滤器 进程内cache RollingWindow TimingWheel 各种executors fx包,map/reduce/filter/sort/group/distinct/head/tail... 一致性hash实现 分布式限流实现 mapreduce,带cancel能力 syncx包里有大量的并发工具 如何快速增加一种rpc协议支持,將跨机发现改为调本机节点,并关闭复杂filter和负载均衡功能 go-zero跟grpc关系还是比较紧密的,设计之初没有考虑支持grpc以外的协议 如果要增加的话,那就只能fork出来魔改了 调本机直接用direct的scheme即可 为啥要去掉filter和负载均衡?如果要去的话,fork了改,但没必要 日志和监控和链路追踪的设计和实现思路,最好有大概图解 日志和监控我们使用prometheus, 自定义dashboard服务,捆绑提交数据(每分钟) 链路追踪可以看出调用关系,自动记录trace日志 go-zero框架有用到什么池化技术吗?如果有,在哪些core代码里面可以参考 一般不需要提前优化,过度优化是大忌 core/syncx/pool.go里面定义了带过期时间的通用池化技术 go-zero用到了那些性能测试方法框架,有代码参考吗?可以说说思路和经验 go benchmark 压测可以通过现有业务日志样本,来按照预估等比放大 压测一定要压到系统扛不住,看第一个瓶颈在哪里,改完再压,循环 说一下代码的抽象经验和心得 Don’t repeat yourself 你未必需要它,之前经常有业务开发人员问我可不可以增加这个功能或那个功能,我一般都会仔细询问深层次目的,很多时候会发现其实这个功能是多余的,不需要才是最佳实践 Martin Fowler提出出现三次再抽象的原则,有时有些同事会找我往框架里增加一个功能,我思考后经常会回答这个你先在业务层写,其它地方也有需要了你再告诉我,三次出现我会考虑集成到框架里 一个文件应该尽量只做一件事,每个文件尽可能控制在200行以内,一个函数尽可能控制在50行以内,这样不需要滚动就可以看到整个函数 需要抽象和提炼的能力,多去思考,经常回头思考之前的架构或实现 你会就go-zero 框架从设计到实践出书吗?框架以后的发展规划是什么? 暂无出书计划,做好框架是最重要的 继续着眼于工程效率 提升服务治理能力 帮助业务开发尽可能快速落地 Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:49 "},"contributor.html":{"url":"contributor.html","title":"贡献人员","keywords":"","body":"社区贡献 作者 kevwan go-zero参与人员 kevwan dependabot[bot] kesonan kingxt chenquan MarkJoyMa zcong1993 fynxiu testwill stevenzack zhoushuguang szpnygo miaogaolin dfang bittoy Suyghur heyanfu wubenqi fondoger shenbaise9527 taobig sjatsh Mikaelemmmm foliet POABOB reatang Code-Fight xiaowei520 wsx864321 chowyu12 chensylz phibe2017 anyoptional AlexLast soasurs LeeDF czyt zjbztianya kurimi1 pig-peppa re-dylan jaronnie guonaihong fearlessfei fanlongteng ch3nnn xiaoyuzdy knight0zh veezhang jiang4869 yangjinheng voidint lizhichao supermario1990 sado0823 ronething-bot mywaystay reneleonhardt cjf8134 smithyj showurl weicut zzzfwww HarryWang29 workman-Lu codeErrorSleep wuqinqiang pipi-lv Howie59 appleboy ShyunnY almas1992 SnakeHacker sohamtembhurne toby1991 kscooo bensonfx cuishuang fyyang wangzeping722 heyehang lchjczw mongobaba lucaq mlr3000 masonchen2014 ahmczsy moyrne mlboy me-cs yangwenmai magickeha lowang-bh lovelly lord63 fishJack01 lvillis imzhongqi r00mz mamil safeoy shyandsy skyoct sniperwzq SpectatorNan gq-tang RivenChan fisnone foyon genewoo godLei6 gongluck hanxuanliang tfzxyinhao lhcGinv hexiaoen iyyzh Janetyu demoManito jichangyun jiz4oh jursonmo Kangkeizai kevin0527 byops louyuexing kunyu lascyb linden-in-China liumin-go zeromake zzhaolei zzZZzzz888 r27153733 sixwaaaay liuqing6767 linganmin citizen233 u2nyakim wenj91 congim 600ML seth-shi AaronCXZ lppgo wanghaha-dev happy-v587 peasfarmer DestinyOfLove qwxingzhe SeigeC jsonMark suyhuai toventang tsinghuacoder vankillua shssen rcyw weibobo windk wojiukankan wuleiming2009 wwek wxc421 xiang-xx xt-inking TonoT xybingbing doptime runtu666 yedf2 yiGmMk liyiwu yonwoo9 2822132073 nianiaJR JiChenSSG richardJiang joshq00 Julian-Chu 0xkookoo wanjunfeng Kimjin-gd tnothy lyuangg WangLeonard letian-jiang fzdwx liamhao mervin0502 JasonMing vfmh notrynosuccess ofey404 oraoto ivalue2333 WqyJh 7134g lqlspace alonexy amorist 0Armaan025 tvermaashutosh AtlanCI Awadabang BYT0723 bhargavshirin changkun chrislee87 CrazyZard defp EinfachePhy qiujiafei gokure Hkesd eltociear RyanTokManMokMTM l306287405 wjiec zlx362211854 a0v0 xiongqq345 aimuz ak5w Ouyangan ansoda anstns benyingY bigrocs jiangbohhh accaolei x1nchen mycatone charliecen cuisongliu dahaihu dylanNew edieruby elza2 fffreedom linyihai qwernser metaRobin ren544735689 chenrui333 Jancd SgtDaJim SleeplessBot 1067088037 suravshresth zhouyusd cgx027 wangyi12358 tim1116 tonywangcn patche-v cch123 ChengXavier wwwangxc cubxxw lxy1992 fulldog brickzzhang 文档贡献人员 kevwan loocor koulerz citizen233 Mikaelemmmm avtion helloshaohua wuyang910217 wuqinqiang zcong1993 jackluo2012 zoulux karnin keehao linganmin ronething-bot topfanfan belm ice-waves hwb2017 hbinr ha-ni-cc gggwvg chensylz auula Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:51 "},"doc-contibute.html":{"url":"doc-contibute.html","title":"文档贡献","keywords":"","body":"文档贡献 怎么贡献文档? 点击顶部\"编辑此页\"按钮即可进入源码仓库对应的文件,开发人员将修改(添加)的文档通过pr形式提交, 我们收到pr后会进行文档审核,一旦审核通过即可更新文档。 可以贡献哪些文档? 文档编写错误 文档不规范、不完整 go-zero应用实践、心得 组件中心 文档pr通过后文档多久会更新? 在pr接受后,github action会自动build gitbook并发布,因此在github action成功后1-2分钟即可查看更新后的文档。 文档贡献注意事项 纠错、完善源文档可以直接编写原来的md文件 新增组件文档需要保证文档排版、易读,且组件文档需要放在组件中心子目录中 go-zero应用实践分享可以直接放在开发实践子目录下 目录结构规范 目录结构不宜过深,最好不要超过3层 组件文档需要在归属到组件中心,如* [开发实践](practise.md) * [logx](logx.md) * [bloom](bloom.md) * [executors](executors.md) * 你的文档目录名称 应用实践需要归属到开发实践,如* [开发实践](practise.md) * [我是如何用go-zero 实现一个中台系统](datacenter.md) * [流数据处理利器](stream.md) * [10月3日线上交流问题汇总](online-exchange.md * 你的文档目录名称 开发实践文档模板 # 标题 > 作者:填入作者名称 > > 原文连接: 原文连接 some markdown content 猜你想看 怎么参与贡献 Github Pull request Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:49 "},"error.html":{"url":"error.html","title":"常见错误处理","keywords":"","body":"常见错误处理 Windows上报错 A required privilege is not held by the client. 解决方法:\"以管理员身份运行\" goctl 即可。 grpc引起错误 错误一 protoc-gen-go: unable to determine Go import path for \"greet.proto\" Please specify either: • a \"go_package\" option in the .proto source file, or • a \"M\" argument on the command line. See https://developers.google.com/protocol-buffers/docs/reference/go-generated#package for more information. --go_out: protoc-gen-go: Plugin failed with status code 1. 解决方法: go get -u github.com/golang/protobuf/[email protected] protoc-gen-go安装失败 go get github.com/golang/protobuf/protoc-gen-go: module github.com/golang/protobuf/protoc-gen-go: Get \"https://proxy.golang.org/github.com/golang/protobuf/protoc-gen-go/@v/list\": dial tcp 216.58.200.49:443: i/o timeout 请确认GOPROXY已经设置,GOPROXY设置见go module配置 api服务启动失败 error: config file etc/user-api.yaml, error: type mismatch for field xx 请确认user-api.yaml配置文件中配置项是否已经配置,如果有值,检查一下yaml配置文件是否符合yaml格式。 goctl找不到 command not found: goctl 请确保goctl已经安装或者goctl是否已经添加到环境变量 Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:49 "},"source.html":{"url":"source.html","title":"相关源码","keywords":"","body":"相关源码 demo源码 Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:50 "},"tips.html":{"url":"tips.html","title":"阅读须知","keywords":"","body":"阅读须知 本文档从快速入门,详细项目开发流程,go-zero服务设计思想,goctl工具的使用等维度进行了介绍, 对于刚刚接触go或go-zero的同学需要把这些篇幅都看完才能有所了解,因此有些费力,这里建议大家阅读的方法。 保持耐心跟着文档目录进行,文档是按照从简单到深入的渐进式过程编写的。 在遇到问题或错误时,请一定记住多查FAQ。 Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:50 "},"shorturl.html":{"url":"shorturl.html","title":"快速构建高并发微服务","keywords":"","body":"快速构建高并发微服务 English | 简体中文 0. 为什么说做好微服务很难 要想做好微服务,我们需要理解和掌握的知识点非常多,从几个维度上来说: 基本功能层面 并发控制 & 限流,避免服务被突发流量击垮 服务注册与服务发现,确保能够动态侦测增减的节点 负载均衡,需要根据节点承受能力分发流量 超时控制,避免对已超时请求做无用功 熔断设计,快速失败,保障故障节点的恢复能力 高阶功能层面 请求认证,确保每个用户只能访问自己的数据 链路追踪,用于理解整个系统和快速定位特定请求的问题 日志,用于数据收集和问题定位 可观测性,没有度量就没有优化 对于其中每一点,我们都需要用很长的篇幅来讲述其原理和实现,那么对我们后端开发者来说,要想把这些知识点都掌握并落实到业务系统里,难度是非常大的,不过我们可以依赖已经被大流量验证过的框架体系。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 安装 protoc-gen-go $ go get -u github.com/golang/protobuf/[email protected] 安装 protoc $ wget https://github.com/protocolbuffers/protobuf/releases/download/v3.14.0/protoc-3.14.0-linux-x86_64.zip $ unzip protoc-3.14.0-linux-x86_64.zip $ mv bin/protoc /usr/local/bin/ 安装 goctl 工具 $ GO111MODULE=on GOPROXY=https://goproxy.cn/,direct go get -u github.com/zeromicro/go-zero/tools/goctl 创建工作目录 shorturl 和 shorturl/api mkdir -p shorturl/api 在 shorturl 目录下执行 go mod init shorturl 初始化 go.mod module shorturl go 1.15 require ( github.com/golang/mock v1.4.3 github.com/golang/protobuf v1.4.2 github.com/zeromicro/go-zero v1.3.0 golang.org/x/net v0.0.0-20200707034311-ab3426394381 google.golang.org/grpc v1.29.1 ) 注意:这里可能存在 grpc 版本依赖的问题,可以用以上配置 5. 编写 API Gateway 代码 在 shorturl/api 目录下通过 goctl 生成 api/shorturl.api: $ goctl api -o shorturl.api 编辑 api/shorturl.api,为了简洁,去除了文件开头的 info,代码如下: type ( expandReq { shorten string `form:\"shorten\"` } expandResp { url string `json:\"url\"` } ) type ( shortenReq { url string `form:\"url\"` } shortenResp { 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 {\"shorten\":\"\"} 可以看到我们 API Gateway 其实啥也没干,就返回了个空值,接下来我们会在 rpc 服务里实现业务逻辑 可以修改 internal/svc/servicecontext.go 来传递服务依赖(如果需要) 实现逻辑可以修改 internal/logic 下的对应文件 可以通过 goctl 生成各种客户端语言的 api 调用代码 到这里,你已经可以通过 goctl 生成客户端代码给客户端同学并行开发了,支持多种语言,详见文档 6. 编写 transform rpc 服务 在 shorturl 目录下创建 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 -dir . 注意:不能在 GOPATH 目录下执行以上命令 文件结构如下: 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... 查看服务是否注册 $ ETCDCTL_API=3 etcdctl get transform.rpc --prefix transform.rpc/7587851893787585061 127.0.0.1:8080 etc/transform.yaml 文件里可以修改侦听端口等配置 7. 修改 API Gateway 代码调用 transform rpc 服务 修改配置文件 shorturl-api.yaml,增加如下内容 Transform: Etcd: Hosts: - localhost:2379 Key: transform.rpc 通过 etcd 自动去发现可用的 transform 服务 修改 internal/config/config.go 如下,增加 transform 服务依赖 type Config struct { rest.RestConf Transform zrpc.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(zrpc.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 types.ExpandResp{}, err } return types.ExpandResp{ Url: resp.Url, }, nil // 手动代码结束 } 通过调用 transformer 的 Expand 方法实现短链恢复到 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 types.ShortenResp{}, err } return types.ShortenResp{ Shorten: resp.Shorten, }, nil // 手动代码结束 } 有的版本生成返回值可能是指针类型,需要自己调整下 通过调用 transformer 的 Shorten 方法实现 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 生成 生成后的文件结构如下: Plain Text rpc/transform/model ├── shorturl.sql ├── shorturlmodel.go // CRUD+cache 代码 └── vars.go // 定义常量和变量 9. 修改 shorten/expand rpc 代码调用 crud+cache 代码 修改 rpc/transform/etc/transform.yaml,增加如下内容: DataSource: root:password@tcp(localhost:3306)/gozero Table: shorturl Cache: - Host: localhost:6379 可以使用多个 redis 作为 cache,支持 redis 单点或者 redis 集群 修改 rpc/transform/internal/config/config.go,如下: type Config struct { zrpc.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), // 手动代码 } } 修改 rpc/transform/internal/logic/expandlogic.go,如下: func (l *ExpandLogic) Expand(in *transform.ExpandReq) (*transform.ExpandResp, error) { // 手动代码开始 res, err := l.svcCtx.Model.FindOne(l.ctx, in.Shorten) if err != nil { return nil, err } return &transform.ExpandResp{ Url: res.Url, }, nil // 手动代码结束 } 修改 rpc/transform/internal/logic/shortenlogic.go,如下: func (l *ShortenLogic) Shorten(in *transform.ShortenReq) (*transform.ShortenResp, error) { // 手动代码开始,生成短链接 key := hash.Md5Hex([]byte(in.Url))[:6] object, _ := l.svcCtx.Model.FindOne(l.ctx, key) if object != nil { return &transform.ShortenResp{ Shorten: key, }, nil } _, err := l.svcCtx.Model.Insert(l.ctx, &model.Shorturl{ Shorten: key, Url: in.Url, }) if err != nil { return nil, err } return &transform.ShortenResp{ Shorten: key, }, nil // 手动代码结束 } 至此代码修改完成,凡是手动修改的代码我加了标注 注意: undefined cache,你需要 import \"github.com/zeromicro/go-zero/core/stores/cache\" undefined model, sqlx, hash 等,你需要在文件中 import \"shorturl/rpc/transform/model\" import \"github.com/zeromicro/go-zero/core/stores/sqlx\" 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/zeromicro/zero-examples/tree/main/shorturl 12. 总结 我们一直强调 工具大于约定和文档。 go-zero 不只是一个框架,更是一个建立在框架 + 工具基础上的,简化和规范了整个微服务构建的技术体系。 我们在保持简单的同时也尽可能把微服务治理的复杂度封装到了框架内部,极大的降低了开发人员的心智负担,使得业务开发得以快速推进。 通过 go-zero+goctl 生成的代码,包含了微服务治理的各种组件,包括:并发控制、自适应熔断、自适应降载、自动缓存控制等,可以轻松部署以承载巨大访问量。 有任何好的提升工程效率的想法,随时欢迎交流!👏 Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:50 "},"breaker-algorithms.html":{"url":"breaker-algorithms.html","title":"熔断原理与实现","keywords":"","body":"熔断原理与实现 在微服务中服务间依赖非常常见,比如评论服务依赖审核服务而审核服务又依赖反垃圾服务,当评论服务调用审核服务时,审核服务又调用反垃圾服务,而这时反垃圾服务超时了,由于审核服务依赖反垃圾服务,反垃圾服务超时导致审核服务逻辑一直等待,而这个时候评论服务又在一直调用审核服务,审核服务就有可能因为堆积了大量请求而导致服务宕机 由此可见,在整个调用链中,中间的某一个环节出现异常就会引起上游调用服务出现一些列的问题,甚至导致整个调用链的服务都宕机,这是非常可怕的。因此一个服务作为调用方调用另一个服务时,为了防止被调用服务出现问题进而导致调用服务出现问题,所以调用服务需要进行自我保护,而保护的常用手段就是熔断 熔断器原理 熔断机制其实是参考了我们日常生活中的保险丝的保护机制,当电路超负荷运行时,保险丝会自动的断开,从而保证电路中的电器不受损害。而服务治理中的熔断机制,指的是在发起服务调用的时候,如果被调用方返回的错误率超过一定的阈值,那么后续的请求将不会真正发起请求,而是在调用方直接返回错误 在这种模式下,服务调用方为每一个调用服务(调用路径)维护一个状态机,在这个状态机中有三个状态: 关闭(Closed):在这种状态下,我们需要一个计数器来记录调用失败的次数和总的请求次数,如果在某个时间窗口内,失败的失败率达到预设的阈值,则切换到断开状态,此时开启一个超时时间,当到达该时间则切换到半关闭状态,该超时时间是给了系统一次机会来修正导致调用失败的错误,以回到正常的工作状态。在关闭状态下,调用错误是基于时间的,在特定的时间间隔内会重置,这能够防止偶然错误导致熔断器进去断开状态 打开(Open):在该状态下,发起请求时会立即返回错误,一般会启动一个超时计时器,当计时器超时后,状态切换到半打开状态,也可以设置一个定时器,定期的探测服务是否恢复 半打开(Half-Open):在该状态下,允许应用程序一定数量的请求发往被调用服务,如果这些调用正常,那么可以认为被调用服务已经恢复正常,此时熔断器切换到关闭状态,同时需要重置计数。如果这部分仍有调用失败的情况,则认为被调用方仍然没有恢复,熔断器会切换到打开状态,然后重置计数器,半打开状态能够有效防止正在恢复中的服务被突然大量请求再次打垮 服务治理中引入熔断机制,使得系统更加稳定和有弹性,在系统从错误中恢复的时候提供稳定性,并且减少了错误对系统性能的影响,可以快速拒绝可能导致错误的服务调用,而不需要等待真正的错误返回 熔断器引入 上面介绍了熔断器的原理,在了解完原理后,你是否有思考我们如何引入熔断器呢?一种方案是在业务逻辑中可以加入熔断器,但显然是不够优雅也不够通用的,因此我们需要把熔断器集成在框架内,在zRPC框架内就内置了熔断器 我们知道,熔断器主要是用来保护调用端,调用端在发起请求的时候需要先经过熔断器,而客户端拦截器正好兼具了这个这个功能,所以在zRPC框架内熔断器是实现在客户端拦截器内,拦截器的原理如下图: 对应的代码为: func BreakerInterceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { // 基于请求方法进行熔断 breakerName := path.Join(cc.Target(), method) return breaker.DoWithAcceptable(breakerName, func() error { // 真正发起调用 return invoker(ctx, method, req, reply, cc, opts...) // codes.Acceptable判断哪种错误需要加入熔断错误计数 }, codes.Acceptable) } 熔断器实现 zRPC中熔断器的实现参考了Google Sre过载保护算法,该算法的原理如下: 请求数量(requests):调用方发起请求的数量总和 请求接受数量(accepts):被调用方正常处理的请求数量 在正常情况下,这两个值是相等的,随着被调用方服务出现异常开始拒绝请求,请求接受数量(accepts)的值开始逐渐小于请求数量(requests),这个时候调用方可以继续发送请求,直到requests = K * accepts,一旦超过这个限制,熔断器就回打开,新的请求会在本地以一定的概率被抛弃直接返回错误,概率的计算公式如下: 通过修改算法中的K(倍值),可以调节熔断器的敏感度,当降低该倍值会使自适应熔断算法更敏感,当增加该倍值会使得自适应熔断算法降低敏感度,举例来说,假设将调用方的请求上限从 requests = 2 acceptst 调整为 requests = 1.1 accepts 那么就意味着调用方每十个请求之中就有一个请求会触发熔断 代码路径为go-zero/core/breaker type googleBreaker struct { k float64 // 倍值 默认1.5 stat *collection.RollingWindow // 滑动时间窗口,用来对请求失败和成功计数 proba *mathx.Proba // 动态概率 } 自适应熔断算法实现 func (b *googleBreaker) accept() error { accepts, total := b.history() // 请求接受数量和请求总量 weightedAccepts := b.k * float64(accepts) // 计算丢弃请求概率 dropRatio := math.Max(0, (float64(total-protection)-weightedAccepts)/float64(total+1)) if dropRatio 每次发起请求会调用doReq方法,在这个方法中首先通过accept效验是否触发熔断,acceptable用来判断哪些error会计入失败计数,定义如下: func Acceptable(err error) bool { switch status.Code(err) { case codes.DeadlineExceeded, codes.Internal, codes.Unavailable, codes.DataLoss: // 异常请求错误 return false default: return true } } 如果请求正常则通过markSuccess把请求数量和请求接受数量都加一,如果请求不正常则只有请求数量会加一 func (b *googleBreaker) doReq(req func() error, fallback func(err error) error, acceptable Acceptable) error { // 判断是否触发熔断 if err := b.accept(); err != nil { if fallback != nil { return fallback(err) } else { return err } } defer func() { if e := recover(); e != nil { b.markFailure() panic(e) } }() // 执行真正的调用 err := req() // 正常请求计数 if acceptable(err) { b.markSuccess() } else { // 异常请求计数 b.markFailure() } return err } 总结 调用端可以通过熔断机制进行自我保护,防止调用下游服务出现异常,或者耗时过长影响调用端的业务逻辑,很多功能完整的微服务框架都会内置熔断器。其实,不仅微服务调用之间需要熔断器,在调用依赖资源的时候,比如mysql、redis等也可以引入熔断器的机制。 Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:49 "},"collection.html":{"url":"collection.html","title":"进程内缓存组件 collection.Cache","keywords":"","body":"通过 collection.Cache 进行缓存 go-zero微服务框架中提供了许多开箱即用的工具,好的工具不仅能提升服务的性能而且还能提升代码的鲁棒性避免出错,实现代码风格的统一方便他人阅读等等,本系列文章将分别介绍go-zero框架中工具的使用及其实现原理 进程内缓存工具collection.Cache 在做服务器开发的时候,相信都会遇到使用缓存的情况,go-zero 提供的简单的缓存封装 collection.Cache,简单使用方式如下 // 初始化 cache,其中 WithLimit 可以指定最大缓存的数量 c, err := collection.NewCache(time.Minute, collection.WithLimit(10000)) if err != nil { panic(err) } // 设置缓存 c.Set(\"key\", user) // 获取缓存,ok:是否存在 v, ok := c.Get(\"key\") // 删除缓存 c.Del(\"key\") // 获取缓存,如果 key 不存在的,则会调用 func 去生成缓存 v, err := c.Take(\"key\", func() (interface{}, error) { return user, nil }) cache 实现的建的功能包括 缓存自动失效,可以指定过期时间 缓存大小限制,可以指定缓存个数 缓存增删改 缓存命中率统计 并发安全 缓存击穿 实现原理: Cache 自动失效,是采用 TimingWheel(https://github.com/zeromicro/zeromicro/blob/master/core/collection/timingwheel.go) 进行管理的 timingWheel, err := NewTimingWheel(time.Second, slots, func(k, v interface{}) { key, ok := k.(string) if !ok { return } cache.Del(key) }) Cache 大小限制,是采用 LRU 淘汰策略,在新增缓存的时候会去检查是否已经超出过限制,具体代码在 keyLru 中实现 func (klru *keyLru) add(key string) { if elem, ok := klru.elements[key]; ok { klru.evicts.MoveToFront(elem) return } // Add new item elem := klru.evicts.PushFront(key) klru.elements[key] = elem // Verify size not exceeded if klru.evicts.Len() > klru.limit { klru.removeOldest() } } Cache 的命中率统计,是在代码中实现 cacheStat,在缓存命中丢失的时候自动统计,并且会定时打印使用的命中率, qps 等状态. 打印的具体效果如下 cache(proc) - qpm: 2, hit_ratio: 50.0%, elements: 0, hit: 1, miss: 1 缓存击穿包含是使用 syncx.SingleFlight(https://github.com/zeromicro/zeromicro/blob/master/core/syncx/singleflight.go) 进行实现的,就是将同时请求同一个 key 的请求, 关于 SingleFlight 后续会继续补充。 相关具体实现是在: func (c *Cache) Take(key string, fetch func() (interface{}, error)) (interface{}, error) { val, fresh, err := c.barrier.DoEx(key, func() (interface{}, error) { v, e := fetch() if e != nil { return nil, e } c.Set(key, v) return v, nil }) if err != nil { return nil, err } if fresh { c.stats.IncrementMiss() return val, nil } else { // got the result from previous ongoing query c.stats.IncrementHit() } return val, nil } 本文主要介绍了go-zero框架中的 Cache 工具,在实际的项目中非常实用。用好工具对于提升服务性能和开发效率都有很大的帮助,希望本篇文章能给大家带来一些收获。 Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:49 "},"keywords.html":{"url":"keywords.html","title":"高效的关键词替换和敏感词过滤工具","keywords":"","body":"高效的关键词替换和敏感词过滤工具 1. 算法介绍 利用高效的Trie树建立关键词树,如下图所示,然后依次查找字符串中的相连字符是否形成树的一条路径 发现掘金上这篇文章写的比较详细,可以一读,具体原理在此不详述。 2. 关键词替换 支持关键词重叠,自动选用最短的关键词,并且只会对原始字符串只会遍历一次进行匹配替换,如果替换结果中又出现的关键词不会被二次替换(如果业务上有这种可能性,请自行对上一次的替换结果再次执行替换操作),代码示例如下: replacer := stringx.NewReplacer(map[string]string{ \"日本\": \"法国\", \"日本的首都\": \"东京\", \"东京\": \"日本的首都\", }) fmt.Println(replacer.Replace(\"日本的首都是东京\")) 可以得到: ```Plain Text 法国的首都是日本的首都 示例代码见`stringx/replace/replace.go` ## 3. 查找敏感词 代码示例如下: ```go filter := stringx.NewTrie([]string{ \"AV演员\", \"苍井空\", \"AV\", \"日本AV女优\", \"AV演员色情\", }) keywords := filter.FindKeywords(\"日本AV演员兼电视、电影演员。苍井空AV女优是xx出道, 日本AV女优们最精彩的表演是AV演员色情表演\") fmt.Println(keywords) 可以得到: ```Plain Text [苍井空 日本AV女优 AV演员色情 AV AV演员] ## 4. 敏感词过滤 代码示例如下: ```go filter := stringx.NewTrie([]string{ \"AV演员\", \"苍井空\", \"AV\", \"日本AV女优\", \"AV演员色情\", }, stringx.WithMask('?')) // 默认替换为* safe, keywords, found := filter.Filter(\"日本AV演员兼电视、电影演员。苍井空AV女优是xx出道, 日本AV女优们最精彩的表演是AV演员色情表演\") fmt.Println(safe) fmt.Println(keywords) fmt.Println(found) 可以得到: Plain Text 日本????兼电视、电影演员。?????女优是xx出道, ??????们最精彩的表演是??????表演 [苍井空 日本AV女优 AV演员色情 AV AV演员] true 示例代码见stringx/filter/filter.go 5. Benchmark Sentences Keywords Regex go-zero 10000 10000 16min10s 27.2ms Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:49 "},"loadshedding.html":{"url":"loadshedding.html","title":"服务自适应降载保护设计","keywords":"","body":"服务自适应降载保护设计 设计目的 保证系统不被过量请求拖垮 在保证系统稳定的前提下,尽可能提供更高的吞吐量 设计考虑因素 如何衡量系统负载 是否处于虚机或容器内,需要读取cgroup相关负载 用1000m表示100%CPU,推荐使用800m表示系统高负载 尽可能小的Overhead,不显著增加RT 不考虑服务本身所依赖的DB或者缓存系统问题,这类问题通过熔断机制来解决 机制设计 计算CPU负载时使用滑动平均来降低CPU负载抖动带来的不稳定,关于滑动平均见参考资料 滑动平均就是取之前连续N次值的近似平均,N取值可以通过超参beta来决定 当CPU负载大于指定值时触发降载保护机制 时间窗口机制,用滑动窗口机制来记录之前时间窗口内的QPS和RT(response time) 滑动窗口使用5秒钟50个桶的方式,每个桶保存100ms时间内的请求,循环利用,最新的覆盖最老的 计算maxQPS和minRT时需要过滤掉最新的时间没有用完的桶,防止此桶内只有极少数请求,并且RT处于低概率的极小值,所以计算maxQPS和minRT时按照上面的50个桶的参数只会算49个 满足以下所有条件则拒绝该请求 当前CPU负载超过预设阈值,或者上次拒绝时间到现在不超过1秒(冷却期)。冷却期是为了不能让负载刚下来就马上增加压力导致立马又上去的来回抖动 averageFlying > max(1, QPS*minRT/1e3) averageFlying = MovingAverage(flying) 在算MovingAverage(flying)的时候,超参beta默认取值为0.9,表示计算前十次的平均flying值 取flying值的时候,有三种做法: 请求增加后更新一次averageFlying,见图中橙色曲线 请求结束后更新一次averageFlying,见图中绿色曲线 请求增加后更新一次averageFlying,请求结束后更新一次averageFlying 我们使用的是第二种,这样可以更好的防止抖动,如图: QPS = maxPass * bucketsPerSecond maxPass表示每个有效桶里的成功的requests bucketsPerSecond表示每秒有多少个桶 1e3表示1000毫秒,minRT单位也是毫秒,QPS*minRT/1e3得到的就是平均每个时间点有多少并发请求 降载的使用 已经在rest和zrpc框架里增加了可选激活配置 CpuThreshold,如果把值设置为大于0的值,则激活该服务的自动降载机制 如果请求被drop,那么错误日志里会有dropreq关键字 参考资料 滑动平均 Sentinel自适应限流 Kratos自适应限流保护 Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:49 "},"mapping.html":{"url":"mapping.html","title":"文本序列化和反序列化","keywords":"","body":"文本序列化和反序列化 go-zero针对文本的序列化和反序列化主要在三个地方使用 http api请求体的反序列化 http api返回体的序列化 配置文件的反序列化 本文假定读者已经定义过api文件以及修改过配置文件,如不熟悉,可参照 快速构建高并发微服务 快速构建高并发微服务 1. http api请求体的反序列化 在反序列化的过程中的针对请求数据的数据格式以及数据校验需求,go-zero实现了自己的一套反序列化机制 1.1 数据格式以订单order.api文件为例 type ( createOrderReq struct { token string `path:\"token\"` // 用户token productId string `json:\"productId\"` // 商品ID num int `json:\"num\"` // 商品数量 } createOrderRes struct { success bool `json:\"success\"` // 是否成功 } findOrderReq struct { token string `path:\"token\"` // 用户token page int `form:\"page\"` // 页数 pageSize int8 `form:\"pageSize\"` // 页大小 } findOrderRes struct { orderInfo []orderInfo `json:\"orderInfo\"` // 商品ID } orderInfo struct { productId string `json:\"productId\"` // 商品ID productName string `json:\"productName\"` // 商品名称 num int `json:\"num\"` // 商品数量 } deleteOrderReq struct { id string `path:\"id\"` } deleteOrderRes struct { success bool `json:\"success\"` // 是否成功 } ) service order { @doc( summary: 创建订单 ) @handler CreateOrderHandler post /order/add/:token(createOrderReq) returns(createOrderRes) @doc( summary: 获取订单 ) @handler FindOrderHandler get /order/find/:token(findOrderReq) returns(findOrderRes) @doc( summary: 删除订单 ) @handler: DeleteOrderHandler delete /order/:id(deleteOrderReq) returns(deleteOrderRes) } http api请求体的反序列化的tag有三种: path:http url 路径中参数反序列化 /order/add/1234567会解析出来token为1234567 form:http form表单反序列化,需要 header头添加 Content-Type: multipart/form-data /order/find/1234567?page=1&pageSize=20会解析出来token为1234567,page为1,pageSize为20 json:http request json body反序列化,需要 header头添加 Content-Type: application/json {\"productId\":\"321\",\"num\":1}会解析出来productId为321,num为1 1.2 数据校验以用户user.api文件为例 type ( createUserReq struct { age int8 `json:\"age,default=20,range=(12:100]\"` // 年龄 name string `json:\"name\"` // 名字 alias string `json:\"alias,optional\"` // 别名 sex string `json:\"sex,options=male|female\"` // 性别 avatar string `json:\"avatar,default=default.png\"` // 头像 } createUserRes struct { success bool `json:\"success\"` // 是否成功 } ) service user { @doc( summary: 创建订单 ) @handler CreateUserHandler post /user/add(createUserReq) returns(createUserRes) } 数据校验有很多种方式,包括以下但不限: age:默认不输入为20,输入则取值范围为(12:100],前开后闭 name:必填,不可为空 alias:选填,可为空 sex:必填,取值为male或female avatar:选填,默认为default.png 更多详情参见unmarshaler_test.go 2. http api返回体的序列化 使用官方默认的encoding/json包序列化,在此不再累赘 3. 配置文件的反序列化 配置文件的反序列化和http api请求体的反序列化使用同一套解析规则,可参照http api请求体的反序列化 Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:49 "},"mapreduce.html":{"url":"mapreduce.html","title":"并发处理工具 MapReduce","keywords":"","body":"通过MapReduce降低服务响应时间 在微服务中开发中,api网关扮演对外提供restful api的角色,而api的数据往往会依赖其他服务,复杂的api更是会依赖多个甚至数十个服务。虽然单个被依赖服务的耗时一般都比较低,但如果多个服务串行依赖的话那么整个api的耗时将会大大增加。 那么通过什么手段来优化呢?我们首先想到的是通过并发来的方式来处理依赖,这样就能降低整个依赖的耗时,Go基础库中为我们提供了 WaitGroup 工具用来进行并发控制,但实际业务场景中多个依赖如果有一个出错我们期望能立即返回而不是等所有依赖都执行完再返回结果,而且WaitGroup中对变量的赋值往往需要加锁,每个依赖函数都需要添加Add和Done对于新手来说比较容易出错 基于以上的背景,go-zero框架中为我们提供了并发处理工具MapReduce,该工具开箱即用,不需要做什么初始化,我们通过下图看下使用MapReduce和没使用的耗时对比: 相同的依赖,串行处理的话需要200ms,使用MapReduce后的耗时等于所有依赖中最大的耗时为100ms,可见MapReduce可以大大降低服务耗时,而且随着依赖的增加效果就会越明显,减少处理耗时的同时并不会增加服务器压力 并发处理工具MapReduce MapReduce是Google提出的一个软件架构,用于大规模数据集的并行运算,go-zero中的MapReduce工具正是借鉴了这种架构思想 go-zero框架中的MapReduce工具主要用来对批量数据进行并发的处理,以此来提升服务的性能 我们通过几个示例来演示MapReduce的用法 MapReduce主要有三个参数,第一个参数为generate用以生产数据,第二个参数为mapper用以对数据进行处理,第三个参数为reducer用以对mapper后的数据做聚合返回,还可以通过opts选项设置并发处理的线程数量 场景一: 某些功能的结果往往需要依赖多个服务,比如商品详情的结果往往会依赖用户服务、库存服务、订单服务等等,一般被依赖的服务都是以rpc的形式对外提供,为了降低依赖的耗时我们往往需要对依赖做并行处理 func productDetail(uid, pid int64) (*ProductDetail, error) { var pd ProductDetail err := mr.Finish(func() (err error) { pd.User, err = userRpc.User(uid) return }, func() (err error) { pd.Store, err = storeRpc.Store(pid) return }, func() (err error) { pd.Order, err = orderRpc.Order(pid) return }) if err != nil { log.Printf(\"product detail error: %v\", err) return nil, err } return &pd, nil } 该示例中返回商品详情依赖了多个服务获取数据,因此做并发的依赖处理,对接口的性能有很大的提升 场景二: 很多时候我们需要对一批数据进行处理,比如对一批用户id,效验每个用户的合法性并且效验过程中有一个出错就认为效验失败,返回的结果为效验合法的用户id func checkLegal(uids []int64) ([]int64, error) { r, err := mr.MapReduce(func(source chan 该示例中,如果check过程出现错误则通过cancel方法结束效验过程,并返回error整个效验过程结束,如果某个uid效验结果为false则最终结果不返回该uid MapReduce使用注意事项 mapper和reducer中都可以调用cancel,参数为error,调用后立即返回,返回结果为nil, error mapper中如果不调用writer.Write则item最终不会被reducer聚合 reducer中如果不调用writer.Wirte则返回结果为nil, ErrReduceNoOutput reducer为单线程,所有mapper出来的结果在这里串行聚合 实现原理分析: MapReduce中首先通过buildSource方法通过执行generate(参数为无缓冲channel)产生数据,并返回无缓冲的channel,mapper会从该channel中读取数据 func buildSource(generate GenerateFunc) chan interface{} { source := make(chan interface{}) go func() { defer close(source) generate(source) }() return source } 在MapReduceWithSource方法中定义了cancel方法,mapper和reducer中都可以调用该方法,调用后主线程收到close信号会立马返回 cancel := once(func(err error) { if err != nil { retErr.Set(err) } else { // 默认的error retErr.Set(ErrCancelWithNil) } drain(source) // 调用close(ouput)主线程收到Done信号,立马返回 finish() }) 在mapperDispatcher方法中调用了executeMappers,executeMappers消费buildSource产生的数据,每一个item都会起一个goroutine单独处理,默认最大并发数为16,可以通过WithWorkers进行设置 var wg sync.WaitGroup defer func() { wg.Wait() // 保证所有的item都处理完成 close(collector) }() pool := make(chan lang.PlaceholderType, workers) writer := newGuardedWriter(collector, done) // 将mapper处理完的数据写入collector for { select { case reducer单goroutine对数mapper写入collector的数据进行处理,如果reducer中没有手动调用writer.Write则最终会执行finish方法对output进行close避免死锁 go func() { defer func() { if r := recover(); r != nil { cancel(fmt.Errorf(\"%v\", r)) } else { finish() } }() reducer(collector, writer, cancel) }() 在该工具包中还提供了许多针对不同业务场景的方法,实现原理与MapReduce大同小异,感兴趣的同学可以查看源码学习 MapReduceVoid 功能和MapReduce类似但没有结果返回只返回error Finish 处理固定数量的依赖,返回error,有一个error立即返回 FinishVoid 和Finish方法功能类似,没有返回值 Map 只做generate和mapper处理,返回channel MapVoid 和Map功能类似,无返回 本文主要介绍了go-zero框架中的MapReduce工具,在实际的项目中非常实用。用好工具对于提升服务性能和开发效率都有很大的帮助,希望本篇文章能给大家带来一些收获。 Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:49 "},"metric.html":{"url":"metric.html","title":"基于prometheus的微服务指标监控","keywords":"","body":"基于prometheus的微服务指标监控 服务上线后我们往往需要对服务进行监控,以便能及早发现问题并做针对性的优化,监控又可分为多种形式,比如日志监控,调用链监控,指标监控等等。而通过指标监控能清晰的观察出服务指标的变化趋势,了解服务的运行状态,对于保证服务稳定起着非常重要的作用 prometheus是一个开源的系统监控和告警工具,支持强大的查询语言PromQL允许用户实时选择和汇聚时间序列数据,时间序列数据是服务端通过HTTP协议主动拉取获得,也可以通过中间网关来推送时间序列数据,可以通过静态配置文件或服务发现来获取监控目标 Prometheus 的架构 Prometheus 的整体架构以及生态系统组件如下图所示: Prometheus Server直接从监控目标中或者间接通过推送网关来拉取监控指标,它在本地存储所有抓取到样本数据,并对此数据执行一系列规则,以汇总和记录现有数据的新时间序列或生成告警。可以通过 Grafana 或者其他工具来实现监控数据的可视化 go-zero基于prometheus的服务指标监控 go-zero 框架中集成了基于prometheus的服务指标监控,下面我们通过go-zero官方的示例shorturl来演示是如何对服务指标进行收集监控的: 第一步需要先安装Prometheus,安装步骤请参考官方文档 go-zero默认不开启prometheus监控,开启方式很简单,只需要在shorturl-api.yaml文件中增加配置如下,其中Host为Prometheus Server地址为必填配置,Port端口不填默认9091,Path为用来拉取指标的路径默认为/metrics Prometheus: Host: 127.0.0.1 Port: 9091 Path: /metrics 编辑prometheus的配置文件prometheus.yml,添加如下配置,并创建targets.json - job_name: 'file_ds' file_sd_configs: - files: - targets.json 编辑targets.json文件,其中targets为shorturl配置的目标地址,并添加了几个默认的标签 [ { \"targets\": [\"127.0.0.1:9091\"], \"labels\": { \"job\": \"shorturl-api\", \"app\": \"shorturl-api\", \"env\": \"test\", \"instance\": \"127.0.0.1:8888\" } } ] 启动prometheus服务,默认侦听在9090端口 prometheus --config.file=prometheus.yml 在浏览器输入http://127.0.0.1:9090/,然后点击Status -> Targets即可看到状态为Up的Job,并且Lables栏可以看到我们配置的默认的标签 通过以上几个步骤我们完成了prometheus对shorturl服务的指标监控收集的配置工作,为了演示简单我们进行了手动的配置,在实际的生产环境中一般采用定时更新配置文件或者服务发现的方式来配置监控目标,篇幅有限这里不展开讲解,感兴趣的同学请自行查看相关文档 go-zero监控的指标类型 go-zero中目前在http的中间件和rpc的拦截器中添加了对请求指标的监控。 主要从请求耗时和请求错误两个维度,请求耗时采用了Histogram指标类型定义了多个Buckets方便进行分位统计,请求错误采用了Counter类型,并在http metric中添加了path标签rpc metric中添加了method标签以便进行细分监控。 接下来演示如何查看监控指标: 首先在命令行多次执行如下命令 curl -i \"http://localhost:8888/shorten?url=http://www.xiaoheiban.cn\" 打开Prometheus切换到Graph界面,在输入框中输入{path=\"/shorten\"}指令,即可查看监控指标,如下图 我们通过PromQL语法查询过滤path为/shorten的指标,结果中显示了指标名以及指标数值,其中http_server_requests_code_total指标中code值为http的状态码,200表明请求成功,http_server_requests_duration_ms_bucket中对不同bucket结果分别进行了统计,还可以看到所有的指标中都添加了我们配置的默认指标 Console界面主要展示了查询的指标结果,Graph界面为我们提供了简单的图形化的展示界面,在实际的生产环境中我们一般使用Grafana做图形化的展示 grafana可视化界面 grafana是一款可视化工具,功能强大,支持多种数据来源Prometheus、Elasticsearch、Graphite等,安装比较简单请参考官方文档,grafana默认端口3000,安装好后再浏览器输入http://localhost:3000/,默认账号和密码都为admin 下面演示如何基于以上指标进行可视化界面的绘制: 点击左侧边栏Configuration->Data Source->Add data source进行数据源添加,其中HTTP的URL为数据源的地址 点击左侧边栏添加dashboard,然后添加Variables方便针对不同的标签进行过滤筛选比如添加app变量用来过滤不同的服务 进入dashboard点击右上角Add panel添加面板,以path维度统计接口的qps 最终的效果如下所示,可以通过服务名称过滤不同的服务,面板展示了path为/shorten的qps变化趋势 总结 以上演示了go-zero中基于prometheus+grafana服务指标监控的简单流程,生产环境中可以根据实际的场景做不同维度的监控分析。现在go-zero的监控指标主要还是针对http和rpc,这对于服务的整体监控显然还是不足的,比如容器资源的监控,依赖的mysql、redis等资源的监控,以及自定义的指标监控等等,go-zero在这方面后续还会持续优化。希望这篇文章能够给您带来帮助 Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:49 "},"sharedcalls.html":{"url":"sharedcalls.html","title":"防止缓存击穿之进程内共享调用","keywords":"","body":"防止缓存击穿之进程内共享调用 go-zero微服务框架中提供了许多开箱即用的工具,好的工具不仅能提升服务的性能而且还能提升代码的鲁棒性避免出错,实现代码风格的统一方便他人阅读等等。 本文主要讲述进程内共享调用神器SharedCalls(v1.2.0改名为SingleFlight ) 使用场景 并发场景下,可能会有多个线程(协程)同时请求同一份资源,如果每个请求都要走一遍资源的请求过程,除了比较低效之外,还会对资源服务造成并发的压力。举一个具体例子,比如缓存失效,多个请求同时到达某服务请求某资源,该资源在缓存中已经失效,此时这些请求会继续访问DB做查询,会引起数据库压力瞬间增大。而使用SharedCalls可以使得同时多个请求只需要发起一次拿结果的调用,其他请求\"坐享其成\",这种设计有效减少了资源服务的并发压力,可以有效防止缓存击穿。 高并发场景下,当某个热点key缓存失效后,多个请求会同时从数据库加载该资源,并保存到缓存,如果不做防范,可能会导致数据库被直接打死。针对这种场景,go-zero框架中已经提供了实现,具体可参看sqlc和mongoc等实现代码。 为了简化演示代码,我们通过多个线程同时去获取一个id来模拟缓存的场景。如下: func main() { const round = 5 var wg sync.WaitGroup barrier := syncx.NewSharedCalls() wg.Add(round) for i := 0; i 运行,打印结果为: 837c577b1008a0db 837c577b1008a0db 837c577b1008a0db 837c577b1008a0db 837c577b1008a0db 可以看出,只要是同一个key上的同时发起的请求,都会共享同一个结果,对获取DB数据进缓存等场景特别有用,可以有效防止缓存击穿。 关键源码分析 SharedCalls interface提供了Do和DoEx两种方法的抽象 // SharedCalls接口提供了Do和DoEx两种方法 type SharedCalls interface { Do(key string, fn func() (interface{}, error)) (interface{}, error) DoEx(key string, fn func() (interface{}, error)) (interface{}, bool, error) } SharedCalls interface的具体实现sharedGroup // call代表对指定资源的一次请求 type call struct { wg sync.WaitGroup // 用于协调各个请求goroutine之间的资源共享 val interface{} // 用于保存请求的返回值 err error // 用于保存请求过程中发生的错误 } type sharedGroup struct { calls map[string]*call lock sync.Mutex } sharedGroup的Do方法 key参数:可以理解为资源的唯一标识。 fn参数:真正获取资源的方法。 处理过程分析: // 当多个请求同时使用Do方法请求资源时 func (g *sharedGroup) Do(key string, fn func() (interface{}, error)) (interface{}, error) { // 先申请加锁 g.lock.Lock() // 根据key,获取对应的call结果,并用变量c保存 if c, ok := g.calls[key]; ok { // 拿到call以后,释放锁,此处call可能还没有实际数据,只是一个空的内存占位 g.lock.Unlock() // 调用wg.Wait,判断是否有其他goroutine正在申请资源,如果阻塞,说明有其他goroutine正在获取资源 c.wg.Wait() // 当wg.Wait不再阻塞,表示资源获取已经结束,可以直接返回结果 return c.val, c.err } // 没有拿到结果,则调用makeCall方法去获取资源,注意此处仍然是锁住的,可以保证只有一个goroutine可以调用makecall c := g.makeCall(key, fn) // 返回调用结果 return c.val, c.err } sharedGroup的DoEx方法 和Do方法类似,只是返回值中增加了布尔值表示值是调用makeCall方法直接获取的,还是取的共享成果 func (g *sharedGroup) DoEx(key string, fn func() (interface{}, error)) (val interface{}, fresh bool, err error) { g.lock.Lock() if c, ok := g.calls[key]; ok { g.lock.Unlock() c.wg.Wait() return c.val, false, c.err } c := g.makeCall(key, fn) return c.val, true, c.err } sharedGroup的makeCall方法 该方法由Do和DoEx方法调用,是真正发起资源请求的方法。 // 进入makeCall的一定只有一个goroutine,因为要拿锁锁住的 func (g *sharedGroup) makeCall(key string, fn func() (interface{}, error)) *call { // 创建call结构,用于保存本次请求的结果 c := new(call) // wg加1,用于通知其他请求资源的goroutine等待本次资源获取的结束 c.wg.Add(1) // 将用于保存结果的call放入map中,以供其他goroutine获取 g.calls[key] = c // 释放锁,这样其他请求的goroutine才能获取call的内存占位 g.lock.Unlock() defer func() { // delete key first, done later. can't reverse the order, because if reverse, // another Do call might wg.Wait() without get notified with wg.Done() g.lock.Lock() delete(g.calls, key) g.lock.Unlock() // 调用wg.Done,通知其他goroutine可以返回结果,这样本批次所有请求完成结果的共享 c.wg.Done() }() // 调用fn方法,将结果填入变量c中 c.val, c.err = fn() return c } 最后 本文主要介绍了go-zero框架中的 SharedCalls工具,对其应用场景和关键代码做了简单的梳理,希望本篇文章能给大家带来一些收获。 Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:50 "},"sql-cache.html":{"url":"sql-cache.html","title":"DB缓存机制","keywords":"","body":"DB缓存机制 QueryRowIndex 没有查询条件到Primary映射的缓存 通过查询条件到DB去查询行记录,然后 把Primary到行记录的缓存写到redis里 把查询条件到Primary的映射保存到redis里,框架的Take方法自动做了 可能的过期顺序 查询条件到Primary的映射缓存未过期 Primary到行记录的缓存未过期 直接返回缓存行记录 Primary到行记录的缓存已过期 通过Primary到DB获取行记录,并写入缓存 此时存在的问题是,查询条件到Primary的缓存可能已经快要过期了,短时间内的查询又会触发一次数据库查询 要避免这个问题,可以让上面粗体部分第一个过期时间略长于第二个,比如5秒 查询条件到Primary的映射缓存已过期,不管Primary到行记录的缓存是否过期 查询条件到Primary的映射会被重新获取,获取过程中会自动写入新的Primary到行记录的缓存,这样两种缓存的过期时间都是刚刚设置 有查询条件到Primary映射的缓存 没有Primary到行记录的缓存 通过Primary到DB查询行记录,并写入缓存 有Primary到行记录的缓存 直接返回缓存结果 Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:50 "},"zrpc.html":{"url":"zrpc.html","title":"zrpc 使用介绍","keywords":"","body":"企业级RPC框架zRPC 近期比较火的开源项目go-zero是一个集成了各种工程实践的包含了Web和RPC协议的功能完善的微服务框架,今天我们就一起来分析一下其中的RPC部分zRPC。 zRPC底层依赖gRPC,内置了服务注册、负载均衡、拦截器等模块,其中还包括自适应降载,自适应熔断,限流等微服务治理方案,是一个简单易用的可直接用于生产的企业级RPC框架。 zRPC初探 zRPC支持直连和基于etcd服务发现两种方式,我们以基于etcd做服务发现为例演示zRPC的基本使用: 配置 创建hello.yaml配置文件,配置如下: Name: hello.rpc // 服务名 ListenOn: 127.0.0.1:9090 // 服务监听地址 Etcd: Hosts: - 127.0.0.1:2379 // etcd服务地址 Key: hello.rpc // 服务注册key 创建proto文件 创建hello.proto文件,并生成对应的go代码 syntax = \"proto3\"; package pb; service Greeter { rpc SayHello (HelloRequest) returns (HelloReply) {} } message HelloRequest { string name = 1; } message HelloReply { string message = 1; } 生成go代码 protoc --go_out=plugins=grpc:. hello.proto Server端 package main import ( \"context\" \"flag\" \"log\" \"example/zrpc/pb\" \"github.com/zeromicro/go-zero/core/conf\" \"github.com/zeromicro/go-zero/zrpc\" \"google.golang.org/grpc\" ) type Config struct { zrpc.RpcServerConf } var cfgFile = flag.String(\"f\", \"./hello.yaml\", \"cfg file\") func main() { flag.Parse() var cfg Config conf.MustLoad(*cfgFile, &cfg) srv, err := zrpc.NewServer(cfg.RpcServerConf, func(s *grpc.Server) { pb.RegisterGreeterServer(s, &Hello{}) }) if err != nil { log.Fatal(err) } srv.Start() } type Hello struct{} func (h *Hello) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) { return &pb.HelloReply{Message: \"hello \" + in.Name}, nil } Client端 package main import ( \"context\" \"log\" \"example/zrpc/pb\" \"github.com/zeromicro/go-zero/core/discov\" \"github.com/zeromicro/go-zero/zrpc\" ) func main() { client := zrpc.MustNewClient(zrpc.RpcClientConf{ Etcd: discov.EtcdConf{ Hosts: []string{\"127.0.0.1:2379\"}, Key: \"hello.rpc\", }, }) conn := client.Conn() hello := pb.NewGreeterClient(conn) reply, err := hello.SayHello(context.Background(), &pb.HelloRequest{Name: \"go-zero\"}) if err != nil { log.Fatal(err) } log.Println(reply.Message) } 启动服务,查看服务是否注册: ETCDCTL_API=3 etcdctl get hello.rpc --prefix 显示服务已经注册: hello.rpc/7587849401504590084 127.0.0.1:9090 运行客户端即可看到输出: hello go-zero 这个例子演示了zRPC的基本使用,可以看到通过zRPC构建RPC服务非常简单,只需要很少的几行代码,接下来我们继续进行探索 zRPC原理分析 下图展示zRPC的架构图和主要组成部分 zRPC主要有以下几个模块组成: discov: 服务发现模块,基于etcd实现服务发现功能 resolver: 服务注册模块,实现了gRPC的resolver.Builder接口并注册到gRPC interceptor: 拦截器,对请求和响应进行拦截处理 balancer: 负载均衡模块,实现了p2c负载均衡算法,并注册到gRPC client: zRPC客户端,负责发起请求 server: zRPC服务端,负责处理请求 这里介绍了zRPC的主要组成模块和每个模块的主要功能,其中resolver和balancer模块实现了gRPC开放的接口,实现了自定义的resolver和balancer,拦截器模块是整个zRPC的功能重点,自适应降载、自适应熔断、prometheus服务指标收集等功能都在这里实现 Interceptor模块 gRPC提供了拦截器功能,主要是对请求前后进行额外处理的拦截操作,其中拦截器包含客户端拦截器和服务端拦截器,又分为一元(Unary)拦截器和流(Stream)拦截器,这里我们主要讲解一元拦截器,流拦截器同理。 客户端拦截器定义如下: type UnaryClientInterceptor func(ctx context.Context, method string, req, reply interface{}, cc *ClientConn, invoker UnaryInvoker, opts ...CallOption) error 其中method为方法名,req,reply分别为请求和响应参数,cc为客户端连接对象,invoker参数是真正执行rpc方法的handler其实在拦截器中被调用执行 服务端拦截器定义如下: type UnaryServerInterceptor func(ctx context.Context, req interface{}, info *UnaryServerInfo, handler UnaryHandler) (resp interface{}, err error) 其中req为请求参数,info中包含了请求方法属性,handler为对server端方法的包装,也是在拦截器中被调用执行 zRPC中内置了丰富的拦截器,其中包括自适应降载、自适应熔断、权限验证、prometheus指标收集等等,由于拦截器较多,篇幅有限没法所有的拦截器给大家一一解析,这里我们主要分析两个,自适应熔断和prometheus服务监控指标收集: 内置拦截器分析 自适应熔断(breaker) 当客户端向服务端发起请求,客户端会记录服务端返回的错误,当错误达到一定的比例,客户端会自行的进行熔断处理,丢弃掉一定比例的请求以保护下游依赖,且可以自动恢复。zRPC中自适应熔断遵循《Google SRE》中过载保护策略,算法如下: requests: 总请求数量 accepts: 正常请求数量 K: 倍值 (Google SRE推荐值为2) 可以通过修改K的值来修改熔断发生的激进程度,降低K的值会使得自适应熔断算法更加激进,增加K的值则自适应熔断算法变得不再那么激进 熔断拦截器定义如下: func BreakerInterceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { // target + 方法名 breakerName := path.Join(cc.Target(), method) return breaker.DoWithAcceptable(breakerName, func() error { // 真正执行调用 return invoker(ctx, method, req, reply, cc, opts...) }, codes.Acceptable) } accept方法实现了Google SRE过载保护算法,判断否进行熔断 func (b *googleBreaker) accept() error { // accepts为正常请求数,total为总请求数 accepts, total := b.history() weightedAccepts := b.k * float64(accepts) // 算法实现 dropRatio := math.Max(0, (float64(total-protection)-weightedAccepts)/float64(total+1)) if dropRatio doReq方法首先判断是否熔断,满足条件直接返回error(circuit breaker is open),不满足条件则对请求数进行累加 func (b *googleBreaker) doReq(req func() error, fallback func(err error) error, acceptable Acceptable) error { if err := b.accept(); err != nil { if fallback != nil { return fallback(err) } else { return err } } defer func() { if e := recover(); e != nil { b.markFailure() panic(e) } }() // 此处执行RPC请求 err := req() // 正常请求total和accepts都会加1 if acceptable(err) { b.markSuccess() } else { // 请求失败只有total会加1 b.markFailure() } return err } prometheus指标收集 服务监控是了解服务当前运行状态以及变化趋势的重要手段,监控依赖于服务指标的收集,通过prometheus进行监控指标的收集是业界主流方案,zRPC中也采用了prometheus来进行指标的收集 prometheus拦截器定义如下: 这个拦截器主要是对服务的监控指标进行收集,这里主要是对RPC方法的耗时和调用错误进行收集,这里主要使用了Prometheus的Histogram和Counter数据类型 func UnaryPrometheusInterceptor() grpc.UnaryServerInterceptor { return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) ( interface{}, error) { // 执行前记录一个时间 startTime := timex.Now() resp, err := handler(ctx, req) // 执行后通过Since算出执行该调用的耗时 metricServerReqDur.Observe(int64(timex.Since(startTime)/time.Millisecond), info.FullMethod) // 方法对应的错误码 metricServerReqCodeTotal.Inc(info.FullMethod, strconv.Itoa(int(status.Code(err)))) return resp, err } } 添加自定义拦截器 除了内置了丰富的拦截器之外,zRPC同时支持添加自定义拦截器 Client端通过AddInterceptor方法添加一元拦截器: func (rc *RpcClient) AddInterceptor(interceptor grpc.UnaryClientInterceptor) { rc.client.AddInterceptor(interceptor) } Server端通过AddUnaryInterceptors方法添加一元拦截器: func (rs *RpcServer) AddUnaryInterceptors(interceptors ...grpc.UnaryServerInterceptor) { rs.server.AddUnaryInterceptors(interceptors...) } resolver模块 zRPC服务注册架构图: zRPC中自定义了resolver模块,用来实现服务的注册功能。zRPC底层依赖gRPC,在gRPC中要想自定义resolver需要实现resolver.Builder接口: type Builder interface { Build(target Target, cc ClientConn, opts BuildOptions) (Resolver, error) Scheme() string } 其中Build方法返回Resolver,Resolver定义如下: type Resolver interface { ResolveNow(ResolveNowOptions) Close() } 在zRPC中定义了两种resolver,direct和discov,这里我们主要分析基于etcd做服务发现的discov,自定义的resolver需要通过gRPC提供了Register方法进行注册代码如下: func RegisterResolver() { resolver.Register(&dirBuilder) resolver.Register(&disBuilder) } 当我们启动我们的zRPC Server的时候,调用Start方法,会像etcd中注册对应的服务地址: func (ags keepAliveServer) Start(fn RegisterFn) error { // 注册服务地址 if err := ags.registerEtcd(); err != nil { return err } // 启动服务 return ags.Server.Start(fn) } 当我们启动zRPC客户端的时候,在gRPC内部会调用我们自定义resolver的Build方法,zRPC通过在Build方法内调用执行了resolver.ClientConn的UpdateState方法,该方法会把服务地址注册到gRPC客户端内部: func (d *discovBuilder) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) ( resolver.Resolver, error) { hosts := strings.FieldsFunc(target.Authority, func(r rune) bool { return r == EndpointSepChar }) // 服务发现 sub, err := discov.NewSubscriber(hosts, target.Endpoint) if err != nil { return nil, err } update := func() { var addrs []resolver.Address for _, val := range subset(sub.Values(), subsetSize) { addrs = append(addrs, resolver.Address{ Addr: val, }) } // 向gRPC注册服务地址 cc.UpdateState(resolver.State{ Addresses: addrs, }) } // 监听 sub.AddListener(update) update() // 返回自定义的resolver.Resolver return &nopResolver{cc: cc}, nil } 在discov中,通过调用load方法从etcd中获取指定服务的所有地址: func (c *cluster) load(cli EtcdClient, key string) { var resp *clientv3.GetResponse for { var err error ctx, cancel := context.WithTimeout(c.context(cli), RequestTimeout) // 从etcd中获取指定服务的所有地址 resp, err = cli.Get(ctx, makeKeyPrefix(key), clientv3.WithPrefix()) cancel() if err == nil { break } logx.Error(err) time.Sleep(coolDownInterval) } var kvs []KV c.lock.Lock() for _, ev := range resp.Kvs { kvs = append(kvs, KV{ Key: string(ev.Key), Val: string(ev.Value), }) } c.lock.Unlock() c.handleChanges(key, kvs) } 并通过watch监听服务地址的变化: func (c *cluster) watch(cli EtcdClient, key string) { rch := cli.Watch(clientv3.WithRequireLeader(c.context(cli)), makeKeyPrefix(key), clientv3.WithPrefix()) for { select { case wresp, ok := 这部分主要介绍了zRPC中是如何自定义的resolver,以及基于etcd的服务发现原理,通过这部分的介绍大家可以了解到zRPC内部服务注册发现的原理,源代码比较多只是粗略的从整个流程上进行了分析,如果大家对zRPC的源码比较感兴趣可以自行进行学习 balancer模块 负载均衡原理图: 避免过载是负载均衡策略的一个重要指标,好的负载均衡算法能很好的平衡服务端资源。常用的负载均衡算法有轮训、随机、Hash、加权轮训等。但为了应对各种复杂的场景,简单的负载均衡算法往往表现的不够好,比如轮训算法当服务响应时间变长就很容易导致负载不再平衡, 因此zRPC中自定义了默认负载均衡算法P2C(Power of Two Choices),和resolver类似,要想自定义balancer也需要实现gRPC定义的balancer.Builder接口,由于和resolver类似这里不再带大家一起分析如何自定义balancer,感兴趣的朋友可以查看gRPC相关的文档来进行学习 注意,zRPC是在客户端进行负载均衡,常见的还有通过nginx中间代理的方式 zRPC框架中默认的负载均衡算法为P2C,该算法的主要思想是: 从可用节点列表中做两次随机选择操作,得到节点A、B 比较A、B两个节点,选出负载最低的节点作为被选中的节点 伪代码如下: 主要算法逻辑在Pick方法中实现: func (p *p2cPicker) Pick(ctx context.Context, info balancer.PickInfo) ( conn balancer.SubConn, done func(balancer.DoneInfo), err error) { p.lock.Lock() defer p.lock.Unlock() var chosen *subConn switch len(p.conns) { case 0: return nil, nil, balancer.ErrNoSubConnAvailable case 1: chosen = p.choose(p.conns[0], nil) case 2: chosen = p.choose(p.conns[0], p.conns[1]) default: var node1, node2 *subConn for i := 0; i = a { b++ } // 随机获取所有节点中的两个节点 node1 = p.conns[a] node2 = p.conns[b] // 效验节点是否健康 if node1.healthy() && node2.healthy() { break } } // 选择其中一个节点 chosen = p.choose(node1, node2) } atomic.AddInt64(&chosen.inflight, 1) atomic.AddInt64(&chosen.requests, 1) return chosen.conn, p.buildDoneFunc(chosen), nil } choose方法对随机选择出来的节点进行负载比较从而最终确定选择哪个节点 func (p *p2cPicker) choose(c1, c2 *subConn) *subConn { start := int64(timex.Now()) if c2 == nil { atomic.StoreInt64(&c1.pick, start) return c1 } if c1.load() > c2.load() { c1, c2 = c2, c1 } pick := atomic.LoadInt64(&c2.pick) if start-pick > forcePick && atomic.CompareAndSwapInt64(&c2.pick, pick, start) { return c2 } else { atomic.StoreInt64(&c1.pick, start) return c1 } } 上面主要介绍了zRPC默认负载均衡算法的设计思想和代码实现,那自定义的balancer是如何注册到gRPC的呢,resolver提供了Register方法来进行注册,同样balancer也提供了Register方法来进行注册: func init() { balancer.Register(newBuilder()) } func newBuilder() balancer.Builder { return base.NewBalancerBuilder(Name, new(p2cPickerBuilder)) } 注册balancer之后gRPC怎么知道使用哪个balancer呢?这里我们需要使用配置项进行配置,在NewClient的时候通过grpc.WithBalancerName方法进行配置: func NewClient(target string, opts ...ClientOption) (*client, error) { var cli client opts = append(opts, WithDialOption(grpc.WithBalancerName(p2c.Name))) if err := cli.dial(target, opts...); err != nil { return nil, err } return &cli, nil } 这部分主要介绍了zRPC中内中的负载均衡算法的实现原理以及具体的实现方式,之后介绍了zRPC是如何注册自定义的balancer以及如何选择自定义的balancer,通过这部分大家应该对负载均衡有了更进一步的认识 总结 首先,介绍了zRPC的基本使用方法,可以看到zRPC使用非常简单,只需要少数几行代码就可以构建高性能和自带服务治理能力的RPC服务,当然这里没有面面俱到的介绍zRPC的基本使用,大家可以查看相关文档进行学习 接着,介绍了zRPC的几个重要组成模块以及其实现原理,并分析了部分源码。拦截器模块是整个zRPC的重点,其中内置了丰富的功能,像熔断、监控、降载等等也是构建高可用微服务必不可少的。resolver和balancer模块自定义了gRPC的resolver和balancer,通过该部分可以了解到整个服务注册与发现的原理以及如何构建自己的服务发现系统,同时自定义负载均衡算法也变得不再神秘 最后,zRPC是一个经历过各种工程实践的RPC框架,不论是想要用于生产还是学习其中的设计模式都是一个不可多得的开源项目。希望通过这篇文章的介绍大家能够进一步了解zRPC Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:50 "},"go-zero-looklook.html":{"url":"go-zero-looklook.html","title":"使用go-zero开发一个旅游系统go-zero-looklook","keywords":"","body":"使用go-zero开发一个旅游系统go-zero-looklook 因为大家都在说目前go-zero没有一个完整的项目例子,本人接触go-zero可能比较早,go-zero在大约1000start左右我就在用了,后来跟go-zero作者加了微信也熟悉了,go-zero作者非常热心以及耐心的帮我解答了很多问题,我也想积极帮助go-zero推广社区,基本是在社区群内回答大家相关的问题,因为在这个过程中发现很多人觉得go-zero没有一个完整的项目例子,作为想推动社区的一员,索性我就把内部项目删减了一些关键东西,就搞了个可用版本开源出来,主要技术栈包含如下: go-zero nginx网关 filebeat kafka go-stash elasticsearch kibana prometheus grafana jaeger go-queue asynq asynqmon dtm docker docker-compose mysql redis 项目地址 https://github.com/Mikaelemmmm/go-zero-looklook 项目文档 https://github.com/Mikaelemmmm/go-zero-looklook/tree/main/doc 项目简介 整个项目使用了go-zero开发的微服务,基本包含了go-zero以及相关go-zero作者开发的一些中间件,所用到的技术栈基本是go-zero项目组的自研组件,基本是go-zero全家桶了 另外,前端是小程序,本项目已经对接好了小程序授权登录 以及 微信支付了 ,前端看看后面是否能开源吧 项目目录结构如下: app:所有业务代码包含api、rpc以及mq(消息队列、延迟队列、定时任务) common:通用组件 error、middleware、interceptor、tool、ctxdata等 data:该项目包含该目录依赖所有中间件(mysql、es、redis、grafana等)产生的数据,此目录下的所有内容应该在git忽略文件中,不需要提交。 deploy: filebeat: docker部署filebeat配置 go-stash:go-stash配置 nginx: nginx网关配置 prometheus : prometheus配置 script: gencode:生成api、rpc,以及创建kafka语句,复制粘贴使用 mysql:生成model的sh工具 goctl: 该项目goctl的template,goctl生成自定义代码模版,tempalte用法可参考go-zero文档,复制到家目录下.goctl即可, 该项目用到goctl版本是v1.2.3 doc : 该项目系列文档 系统架构图 业务架构图 网关 nginx做网关,使用nginx的auth模块,调用后端的identity服务统一鉴权,业务内部不鉴权,如果涉及到业务资金比较多也可以在业务中进行二次鉴权,为了安全嘛。 另外,很多同学觉得nginx做网关不太好,这块原理基本一样,可以自行替换成apisix、kong等 开发模式 本项目使用的是微服务开发,api (http) + rpc(grpc) , api充当聚合服务,复杂、涉及到其他业务调用的统一写在rpc中,如果一些不会被其他服务依赖使用的简单业务,可以直接写在api的logic中 日志 关于日志,统一使用filebeat收集,上报到kafka中,由于logstash懂得都懂,资源占用太夸张了,这里使用了go-stash替换了logstash 链接:https://github.com/kevwan/go-stash , go-stash是由go-zero开发团队开发的,性能很高不占资源,主要代码量没多少,只需要配置就可以使用,很简单。它是吧kafka数据源同步到elasticsearch中,默认不支持elasticsearch账号密码,我fork了一份修改了一下,很简单支持了账号、密码 监控 监控采用prometheus,这个go-zero原生支持,只需要配置就可以了,这里可以看项目中的配置 链路追踪 go-zero默认jaeger、zipkin支持,只需要配置就可以了,可以看配置 消息队列 消息队列使用的是go-zero开发团队开发的go-queue,链接:https://github.com/zeromicro/go-queue 这里使用可kq,kq是基于kafka做的高性能消息队列 通用go-queue中也有dq,是延迟队列,不过当前项目没有使用dq 延迟队列、定时任务 延迟队列、定时任务本项目使用的是asynq , google团队给予redis开发的简单中间件, 当然了asynq也支持消息队列,你也可也把kq消息队列替换成这个,毕竟只需要redis不需要在去维护一个kafka也是不错的 链接:https://github.com/hibiken/asynq 分布式事务 分布式事务准备使用的是dtm, 嗯 ,很舒服,之前我写过一篇 \"go-zero对接分布式事务dtm保姆式教程\" 链接地址:https://github.com/Mikaelemmmm/gozerodtm , 本项目目前还未使用到,后续准备直接集成就好了,如果读者使用直接去看那个源码就行了 部署 部署的话,目前这个直接使用docker可以部署整套技术栈,如果上k8s的话 ,最简单直接用阿里云的吧 我说下思路,这个后续会出一个基于阿里云效的部署到k8s文档教程,自己搭建一个gitlab、jenkins、harbor去做的话太费时间了 1、将代码放在阿里云效(当然你整到gitlab也行) 2、在阿里云效创建流水线,基本是一个服务一个流水线了 3、流水线步骤 : 拉取代码--->ci检测(这里可以省略哈,自己看着办)--->构建镜像(go-zero官方有Dockerfile还有教程,别告诉我不会)-->推送到阿里云镜像服务--->使用kubectl去阿里云k8s拉取镜像(ack、ask都行,ask无法使用daemonset 不能用filebeat)---->ok了 另外, 如果你想自己基于gitlab、jenkins、harbor去做的话,嗯 自己去找运维弄吧,我之前也写过一个教程,有空在整吧老哥们!! Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:49 "},"faq.html":{"url":"faq.html","title":"FAQ","keywords":"","body":"常见问题集合 goctl安装了执行命令却提示 command not found: goctl 字样。 如果你通过 go get 方式安装,那么 goctl 应该位于 $GOPATH 中, 你可以通过 go env GOPATH 查看完整路径,不管你的 goctl 是在 $GOPATH中, 还是在其他目录,出现上述问题的原因就是 goctl 所在目录不在 PATH (环境变量)中所致。 rpc怎么调用 该问题可以参考快速开始中的rpc编写与调用介绍,其中有rpc调用的使用逻辑。 proto使用了import,goctl命令需要怎么写。 goctl 对于import的proto指定 BasePath 提供了 protoc 的flag映射,即 --proto_path, -I, goctl 会将此flag值传递给 protoc. 假设 base.proto 的被main proto 引入了,为什么不生能生成base.pb.go。 对于 base.proto 这种类型的文件,一般都是开发者有message复用的需求,他的来源不止有开发者自己编写的proto文件, 还有可能来源于 google.golang.org/grpc 中提供的一些基本的proto,比如 google/protobuf/any.proto, 如果由 goctl 来生成,那么就失去了集中管理这些proto的意义。 model怎么控制缓存时间 在 sqlc.NewNodeConn 的时候可以通过可选参数 cache.WithExpiry 传递,如缓存时间控制为1天,代码如下: sqlc.NewNodeConn(conn,redis,cache.WithExpiry(24*time.Hour)) jwt鉴权怎么实现 请参考jwt鉴权 api中间件怎么使用 请参考中间件 怎么关闭输出的统计日志(stat)? logx.DisableStat() rpc直连与服务发现连接模式写法 // mode1: 集群直连 // conf:=zrpc.NewDirectClientConf([]string{\"ip:port\"},\"app\",\"token\") // mode2: etcd 服务发现 // conf:=zrpc.NewEtcdClientConf([]string{\"ip:port\"},\"key\",\"app\",\"token\") // client, _ := zrpc.NewClient(conf) // mode3: ip直连mode // client, _ := zrpc.NewClientWithTarget(\"127.0.0.1:8888\") grpc 客户端设置消息大小限制 修改grpc消息大小限制,需要 服务端 和 客户端 都设置 // 需要 grpc 方法里面添加配置项:grpc.MaxCallRecvMsgSize(bytes int)、grpc.MaxCallSendMsgSize(bytes int) // 示例如下:设置 UserRpc.List 列表消息大小限制为 8MB // l.svcCtx.UserRpc.List(l.ctx, &listReq, grpc.MaxCallRecvMsgSize(1024*1024*8), grpc.MaxCallSendMsgSize(1024*1024*8)) faq会不定期更新大家遇到的问题,也欢迎大家把常见问题通过pr写在这里。 Copyright © 2019-2021 go-zero all right reserved,powered by GitbookLast UpdateTime: 2024-12-29 13:52:49 "}}