1
0
mirror of https://github.com/go-kratos/kratos.git synced 2026-05-22 10:15:24 +02:00

fix conflict

This commit is contained in:
lxkaka
2020-09-23 14:02:53 +08:00
63 changed files with 223 additions and 504 deletions
View File
+21
View File
@@ -0,0 +1,21 @@
# 安装失败,提示go mod 错误
执行
```shell
go get -u github.com/go-kratos/kratos/tool/kratos
```
出现以下错误时
```shell
go: github.com/prometheus/client_model@v0.0.0-20190220174349-fd36f4220a90: parsing go.mod: missing module line
go: github.com/remyoudompheng/bigfft@v0.0.0-20190806203942-babf20351dd7e3ac320adedbbe5eb311aec8763c: parsing go.mod: missing module line
```
如果你使用了https://goproxy.io/ 代理,那你要使用其他代理来替换它,然后删除GOPATH目录下的mod缓存文件夹(`go clean --modcache`),然后重新执行安装命令
代理列表
```
export GOPROXY=https://mirrors.aliyun.com/goproxy/
export GOPROXY=https://goproxy.cn/
export GOPROXY=https://goproxy.io/
```
+36
View File
@@ -0,0 +1,36 @@
![kratos](img/kratos3.png)
# Kratos
Kratos是bilibili开源的一套Go微服务框架,包含大量微服务相关框架及工具。
### Goals
我们致力于提供完整的微服务研发体验,整合相关框架及工具后,微服务治理相关部分可对整体业务开发周期无感,从而更加聚焦于业务交付。对每位开发者而言,整套Kratos框架也是不错的学习仓库,可以了解和参考到bilibili在微服务方面的技术积累和经验。
### Principles
* 简单:不过度设计,代码平实简单
* 通用:通用业务开发所需要的基础库的功能
* 高效:提高业务迭代的效率
* 稳定:基础库可测试性高,覆盖率高,有线上实践安全可靠
* 健壮:通过良好的基础库设计,减少错用
* 高性能:性能高,但不特定为了性能做hack优化,引入unsafe
* 扩展性:良好的接口设计,来扩展实现,或者通过新增基础库目录来扩展功能
* 容错性:为失败设计,大量引入对SRE的理解,鲁棒性高
* 工具链:包含大量工具链,比如cache代码生成,lint工具等等
### Features
* HTTP Blademaster:核心基于[gin](https://github.com/gin-gonic/gin)进行模块化设计,简单易用、核心足够轻量;
* GRPC Warden:基于官方gRPC开发,集成[discovery](https://github.com/bilibili/discovery)服务发现,并融合P2C负载均衡;
* Cache:优雅的接口化设计,非常方便的缓存序列化,推荐结合代理模式[overlord](https://github.com/bilibili/overlord);
* Database:集成MySQL/HBase/TiDB,添加熔断保护和统计支持,可快速发现数据层压力;
* Config:方便易用的[paladin sdk](config-paladin.md),可配合远程配置中心,实现配置版本管理和更新;
* Log:类似[zap](https://github.com/uber-go/zap)的field实现高性能日志库,并结合log-agent实现远程日志管理;
* Trace:基于opentracing,集成了全链路trace支持(gRPC/HTTP/MySQL/Redis/Memcached);
* Kratos Tool:工具链,可快速生成标准项目,或者通过Protobuf生成代码,非常便捷使用gRPC、HTTP、swagger文档;
-------------
> 名字来源于:《战神》游戏以希腊神话为背景,讲述由凡人成为战神的奎托斯(Kratos)成为战神并展开弑神屠杀的冒险历程。
+39
View File
@@ -0,0 +1,39 @@
* [介绍](README.md)
* [快速开始 - 项目初始化](quickstart.md)
* [FAQ](FAQ.md)
* [http blademaster](blademaster.md)
* [bm quickstart](blademaster-quickstart.md)
* [bm module](blademaster-mod.md)
* [bm middleware](blademaster-mid.md)
* [bm protobuf](blademaster-pb.md)
* [grpc warden](warden.md)
* [warden quickstart](warden-quickstart.md)
* [warden interceptor](warden-mid.md)
* [warden resolver](warden-resolver.md)
* [warden balancer](warden-balancer.md)
* [warden protobuf](warden-pb.md)
* [config](config.md)
* [paladin](config-paladin.md)
* [ecode](ecode.md)
* [trace](trace.md)
* [log](logger.md)
* [log-agent](log-agent.md)
* [database](database.md)
* [mysql](database-mysql.md)
* [mysql-orm](database-mysql-orm.md)
* [hbase](database-hbase.md)
* [tidb](database-tidb.md)
* [cache](cache.md)
* [memcache](cache-mc.md)
* [redis](cache-redis.md)
* [kratos工具](kratos-tool.md)
* [protoc](kratos-protoc.md)
* [swagger](kratos-swagger.md)
* [genmc](kratos-genmc.md)
* [genbts](kratos-genbts.md)
* [限流bbr](ratelimit.md)
* [熔断breaker](breaker.md)
* [UT单元测试](ut.md)
* [testcli UT运行环境构建工具](ut-testcli.md)
* [testgen UT代码自动生成器](ut-testgen.md)
* [support UT周边辅助工具](ut-support.md)
+177
View File
@@ -0,0 +1,177 @@
# 背景
基于bm的handler机制,可以自定义很多middleware(中间件)进行通用的业务处理,比如用户登录鉴权。接下来就以鉴权为例,说明middleware的写法和用法。
# 写自己的中间件
middleware本质上就是一个handler,接口和方法声明如下代码:
```go
// Handler responds to an HTTP request.
type Handler interface {
ServeHTTP(c *Context)
}
// HandlerFunc http request handler function.
type HandlerFunc func(*Context)
// ServeHTTP calls f(ctx).
func (f HandlerFunc) ServeHTTP(c *Context) {
f(c)
}
```
1. 实现了`Handler`接口,可以作为engine的全局中间件使用:`engine.Use(YourHandler)`
2. 声明为`HandlerFunc`方法,可以作为engine的全局中间件使用:`engine.UseFunc(YourHandlerFunc)`,也可以作为router的局部中间件使用:`e.GET("/path", YourHandlerFunc)`
简单示例代码如下:
```go
type Demo struct {
Key string
Value string
}
// ServeHTTP implements from Handler interface
func (d *Demo) ServeHTTP(ctx *bm.Context) {
ctx.Set(d.Key, d.Value)
}
e := bm.DefaultServer(nil)
d := &Demo{}
// Handler使用如下:
e.Use(d)
// HandlerFunc使用如下:
e.UseFunc(d.ServeHTTP)
e.GET("/path", d.ServeHTTP)
// 或者只有方法
myHandler := func(ctx *bm.Context) {
// some code
}
e.UseFunc(myHandler)
e.GET("/path", myHandler)
```
# 全局中间件
在blademaster的`server.go`代码中,有以下代码:
```go
func DefaultServer(conf *ServerConfig) *Engine {
engine := NewServer(conf)
engine.Use(Recovery(), Trace(), Logger())
return engine
}
```
会默认创建一个`bm engine`,并注册`Recovery(), Trace(), Logger()`三个middlerware用于全局handler处理,优先级从前到后。如果想要将自定义的middleware注册进全局,可以继续调用Use方法如下:
```go
engine.Use(YourMiddleware())
```
此方法会将`YourMiddleware`追加到已有的全局middleware后执行。如果需要全部自定义全局执行的middleware,可以使用`NewServer`方法创建一个无middleware的engine对象,然后使用`engine.Use/UseFunc`进行注册。
# 局部中间件
先来看一段鉴权伪代码示例([auth示例代码位置](https://github.com/go-kratos/kratos/tree/master/example/blademaster/middleware/auth)):
```go
func Example() {
myHandler := func(ctx *bm.Context) {
mid := metadata.Int64(ctx, metadata.Mid)
ctx.JSON(fmt.Sprintf("%d", mid), nil)
}
authn := auth.New(&auth.Config{DisableCSRF: false})
e := bm.DefaultServer(nil)
// "/user"接口必须保证登录用户才能访问,那么我们加入"auth.User"来确保用户鉴权通过,才能进入myHandler进行业务逻辑处理
e.GET("/user", authn.User, myHandler)
// "/guest"接口访客用户就可以访问,但如果登录用户我们需要知道mid,那么我们加入"auth.Guest"来尝试鉴权获取mid,但肯定会继续执行myHandler进行业务逻辑处理
e.GET("/guest", authn.Guest, myHandler)
// "/owner"开头的所有接口,都需要进行登录鉴权才可以被访问,那可以创建一个group并加入"authn.User"
o := e.Group("/owner", authn.User)
o.GET("/info", myHandler) // 该group创建的router不需要再显示的加入"authn.User"
o.POST("/modify", myHandler) // 该group创建的router不需要再显示的加入"authn.User"
e.Start()
}
```
# 内置中间件
## Recovery
代码位于`pkg/net/http/blademaster/recovery.go`内,用于recovery panic。会被`DefaultServer`默认注册,建议使用`NewServer`的话也将其作为首个中间件注册。
## Trace
代码位于`pkg/net/http/blademaster/trace.go`内,用于trace设置,并且实现了`net/http/httptrace`的接口,能够收集官方库内的调用栈详情。会被`DefaultServer`默认注册,建议使用`NewServer`的话也将其作为第二个中间件注册。
## Logger
代码位于`pkg/net/http/blademaster/logger.go`内,用于请求日志记录。会被`DefaultServer`默认注册,建议使用`NewServer`的话也将其作为第三个中间件注册。
## CSRF
代码位于`pkg/net/http/blademaster/csrf.go`内,用于防跨站请求。如要使用如下:
```go
e := bm.DefaultServer(nil)
// 挂载自适应限流中间件到 bm engine,使用默认配置
csrf := bm.CSRF([]string{"bilibili.com"}, []string{"/a/api"})
e.Use(csrf)
// 或者
e.GET("/api", csrf, myHandler)
```
## CORS
代码位于`pkg/net/http/blademaster/cors.go`内,用于跨域允许请求。请注意该:
1. 使用该中间件进行全局注册后,可"省略"单独为`OPTIONS`请求注册路由,如示例一。
2. 使用该中间单独为某路由注册,需要为该路由再注册一个`OPTIONS`方法的同路径路由,如示例二。
示例一:
```go
e := bm.DefaultServer(nil)
// 挂载自适应限流中间件到 bm engine,使用默认配置
cors := bm.CORS([]string{"github.com"})
e.Use(cors)
// 该路由可以默认针对 OPTIONS /api 的跨域请求支持
e.POST("/api", myHandler)
```
示例二:
```go
e := bm.DefaultServer(nil)
// 挂载自适应限流中间件到 bm engine,使用默认配置
cors := bm.CORS([]string{"github.com"})
// e.Use(cors) 不进行全局注册
e.OPTIONS("/api", cors, myHandler) // 需要单独为/api进行OPTIONS方法注册
e.POST("/api", cors, myHandler)
```
## 自适应限流
更多关于自适应限流的信息可参考:[kratos 自适应限流](ratelimit.md)。如要使用如下:
```go
e := bm.DefaultServer(nil)
// 挂载自适应限流中间件到 bm engine,使用默认配置
limiter := bm.NewRateLimiter(nil)
e.Use(limiter.Limit())
// 或者
e.GET("/api", csrf, myHandler)
```
# 扩展阅读
[bm快速开始](blademaster-quickstart.md)
[bm模块说明](blademaster-mod.md)
[bm基于pb生成](blademaster-pb.md)
+88
View File
@@ -0,0 +1,88 @@
# Context
以下是 blademaster 中 Context 对象结构体声明的代码片段:
```go
// Context is the most important part. It allows us to pass variables between
// middleware, manage the flow, validate the JSON of a request and render a
// JSON response for example.
type Context struct {
context.Context
Request *http.Request
Writer http.ResponseWriter
// flow control
index int8
handlers []HandlerFunc
// Keys is a key/value pair exclusively for the context of each request.
Keys map[string]interface{}
Error error
method string
engine *Engine
}
```
* 首先可以看到 blademaster 的 Context 结构体中会 embed 一个标准库中的 Context 实例,bm 中的 Context 也是直接通过该实例来实现标准库中的 Context 接口。
* blademaster 会使用配置的 server timeout (默认1s) 作为一次请求整个过程中的超时时间,使用该context调用dao做数据库、缓存操作查询时均会将该超时时间传递下去,一旦抵达deadline,后续相关操作均会返回`context deadline exceeded`
* Request 和 Writer 字段用于获取当前请求的与输出响应。
* index 和 handlers 用于 handler 的流程控制;handlers 中存储了当前请求需要执行的所有 handler,index 用于标记当前正在执行的 handler 的索引位。
* Keys 用于在 handler 之间传递一些额外的信息。
* Error 用于存储整个请求处理过程中的错误。
* method 用于检查当前请求的 Method 是否与预定义的相匹配。
* engine 字段指向当前 blademaster 的 Engine 实例。
以下为 Context 中所有的公开的方法:
```go
// 用于 Handler 的流程控制
func (c *Context) Abort()
func (c *Context) AbortWithStatus(code int)
func (c *Context) Bytes(code int, contentType string, data ...[]byte)
func (c *Context) IsAborted() bool
func (c *Context) Next()
// 用户获取或者传递请求的额外信息
func (c *Context) RemoteIP() (cip string)
func (c *Context) Set(key string, value interface{})
func (c *Context) Get(key string) (value interface{}, exists bool)
// 用于校验请求的 payload
func (c *Context) Bind(obj interface{}) error
func (c *Context) BindWith(obj interface{}, b binding.Binding) error
// 用于输出响应
func (c *Context) Render(code int, r render.Render)
func (c *Context) Redirect(code int, location string)
func (c *Context) Status(code int)
func (c *Context) String(code int, format string, values ...interface{})
func (c *Context) XML(data interface{}, err error)
func (c *Context) JSON(data interface{}, err error)
func (c *Context) JSONMap(data map[string]interface{}, err error)
func (c *Context) Protobuf(data proto.Message, err error)
```
所有方法基本上可以分为三类:
* 流程控制
* 额外信息传递
* 请求处理
* 响应处理
# Handler
![handler](img/bm-handlers.png)
初次接触`blademaster`的用户可能会对其`Handler`的流程处理产生不小的疑惑,实际上`bm``Handler`对处理非常简单:
*`Router`模块中预先注册的`middleware`与其他`Handler`合并,放入`Context``handlers`字段,并将`index`字段置`0`
* 然后通过`Next()`方法一个个执行下去,部分`middleware`可能想要在过程中中断整个流程,此时可以使用`Abort()`方法提前结束处理
* 有些`middleware`还想在所有`Handler`执行完后再执行部分逻辑,此时可以在自身`Handler`中显式调用`Next()`方法,并将这些逻辑放在调用了`Next()`方法之后
# 扩展阅读
[bm快速开始](blademaster-quickstart.md)
[bm中间件](blademaster-mid.md)
[bm基于pb生成](blademaster-pb.md)
+83
View File
@@ -0,0 +1,83 @@
# 介绍
基于proto文件可以快速生成`bm`框架对应的代码,提前需要准备以下工作:
* 安装`kratos tool protoc`工具,请看[kratos工具](kratos-tool.md)
* 编写`proto`文件,示例可参考[kratos-demo内proto文件](https://github.com/go-kratos/kratos-demo/blob/master/api/api.proto)
### kratos工具说明
`kratos tool protoc`工具可以生成`warden` `bm` `swagger`对应的代码和文档,想要单独生成`bm`代码只需加上`--bm`如:
```shell
# generate BM HTTP
kratos tool protoc --bm api.proto
```
### proto文件说明
请注意想要生成`bm`代码,需要特别在`proto``service`内指定`google.api.http`配置,如下:
```go
service Demo {
rpc SayHello (HelloReq) returns (.google.protobuf.Empty);
rpc SayHelloURL(HelloReq) returns (HelloResp) {
option (google.api.http) = { // 该配置指定SayHelloURL方法对应的url
get:"/kratos-demo/say_hello" // 指定url和请求方式为GET
};
};
}
```
# 使用
建议在项目`api`目录下编写`proto`文件及生成对应的代码,可参考[kratos-demo内的api目录](https://github.com/go-kratos/kratos-demo/tree/master/api)。
执行命令后生成的`api.bm.go`代码,注意其中的`type DemoBMServer interface``RegisterDemoBMServer`,其中:
* `DemoBMServer`接口,包含`proto`文件内配置了`google.api.http`选项的所有方法
* `RegisterDemoBMServer`方法提供注册`DemoBMServer`接口的实现对象,和`bm``Engine`用于注册路由
* `DemoBMServer`接口的实现,一般为`internal/service`内的业务逻辑代码,需要实现`DemoBMServer`接口
使用`RegisterDemoBMServer`示例代码请参考[kratos-demo内的http](https://github.com/go-kratos/kratos-demo/blob/master/internal/server/http/server.go)内的如下代码:
```go
engine = bm.DefaultServer(hc.Server)
pb.RegisterDemoBMServer(engine, svc)
initRouter(engine)
```
`internal/service`内的`Service`结构实现了`DemoBMServer`接口可参考[kratos-demo内的service](https://github.com/go-kratos/kratos-demo/blob/master/internal/service/service.go)内的如下代码:
```go
// SayHelloURL bm demo func.
func (s *Service) SayHelloURL(ctx context.Context, req *pb.HelloReq) (reply *pb.HelloResp, err error) {
reply = &pb.HelloResp{
Content: "hello " + req.Name,
}
fmt.Printf("hello url %s", req.Name)
return
}
```
# 文档
基于同一份`proto`文件还可以生成对应的`swagger`文档,运行命令如下:
```shell
# generate swagger
kratos tool protoc --swagger api.proto
```
该命令将生成对应的`swagger.json`文件,可用于`swagger`工具通过WEBUI的方式打开使用,可运行命令如下:
```shell
kratos tool swagger serve api/api.swagger.json
```
# 扩展阅读
[bm快速开始](blademaster-quickstart.md)
[bm模块说明](blademaster-mod.md)
[bm中间件](blademaster-mid.md)
+142
View File
@@ -0,0 +1,142 @@
# 路由
进入`internal/server/http`目录下,打开`http.go`文件,其中有默认生成的`blademaster`模板。其中:
```go
engine = bm.DefaultServer(hc.Server)
initRouter(engine)
if err := engine.Start(); err != nil {
panic(err)
}
```
是bm默认创建的`engine`及启动代码,我们看`initRouter`初始化路由方法,默认实现了:
```go
func initRouter(e *bm.Engine) {
e.Ping(ping) // engine自带的"/ping"接口,用于负载均衡检测服务健康状态
g := e.Group("/kratos-demo") // e.Group 创建一组 "/kratos-demo" 起始的路由组
{
g.GET("/start", howToStart) // g.GET 创建一个 "kratos-demo/start" 的路由,使用GET方式请求,默认处理Handler为howToStart方法
g.POST("start", howToStart) // g.POST 创建一个 "kratos-demo/start" 的路由,使用POST方式请求,默认处理Handler为howToStart方法
}
}
```
bm的handler方法,结构如下:
```go
func howToStart(c *bm.Context) // handler方法默认传入bm的Context对象
```
### Ping
engine自带Ping方法,用于设置`/ping`路由的handler,该路由统一提供于负载均衡服务做健康检测。服务是否健康,可自定义`ping handler`进行逻辑判断,如检测DB是否正常等。
```go
func ping(c *bm.Context) {
if some DB check not ok {
c.AbortWithStatus(503)
}
}
```
# 默认路由
默认路由有:
* /metrics 用于prometheus信息采集
* /metadata 可以查看所有注册的路由信息
查看加载的所有路由信息:
```shell
curl 'http://127.0.0.1:8000/metadata'
```
输出:
```json
{
"code": 0,
"message": "0",
"ttl": 1,
"data": {
"/kratos-demo/start": {
"method": "GET"
},
"/metadata": {
"method": "GET"
},
"/metrics": {
"method": "GET"
},
"/ping": {
"method": "GET"
}
}
}
```
# 路径参数
使用方式如下:
```go
func initRouter(e *bm.Engine) {
e.Ping(ping)
g := e.Group("/kratos-demo")
{
g.GET("/start", howToStart)
// 路径参数有两个特殊符号":"和"*"
// ":" 跟在"/"后面为参数的key,匹配两个/中间的值 或 一个/到结尾(其中不再包含/)的值
// "*" 跟在"/"后面为参数的key,匹配从 /*开始到结尾的所有值,所有*必须写在最后且无法多个
// NOTE:这是不被允许的,会和 /start 冲突
// g.GET("/:xxx")
// NOTE: 可以拿到一个key为name的参数。注意只能匹配到/param1/felix,无法匹配/param1/felix/hao(该路径会404)
g.GET("/param1/:name", pathParam)
// NOTE: 可以拿到多个key参数。注意只能匹配到/param2/felix/hao/love,无法匹配/param2/felix或/param2/felix/hao
g.GET("/param2/:name/:value/:felid", pathParam)
// NOTE: 可以拿到一个key为name的参数 和 一个key为action的路径。
// NOTE: 如/params3/felix/hello,action的值为"/hello"
// NOTE: 如/params3/felix/hello/hi,action的值为"/hello/hi"
// NOTE: 如/params3/felix/hello/hi/,action的值为"/hello/hi/"
g.GET("/param3/:name/*action", pathParam)
}
}
func pathParam(c *bm.Context) {
name, _ := c.Params.Get("name")
value, _ := c.Params.Get("value")
felid, _ := c.Params.Get("felid")
action, _ := c.Params.Get("action")
path := c.RoutePath // NOTE: 获取注册的路由原始地址,如: /kratos-demo/param1/:name
c.JSONMap(map[string]interface{}{
"name": name,
"value": value,
"felid": felid,
"action": action,
"path": path,
}, nil)
}
```
# 性能分析
启动时默认监听了`2333`端口用于`pprof`信息采集,如:
```shell
go tool pprof http://127.0.0.1:8000/debug/pprof/profile
```
改变端口可以使用flag,如:`-http.perf=tcp://0.0.0.0:12333`
# 扩展阅读
[bm模块说明](blademaster-mod.md)
[bm中间件](blademaster-mid.md)
[bm基于pb生成](blademaster-pb.md)
+43
View File
@@ -0,0 +1,43 @@
# 背景
在像微服务这样的分布式架构中,经常会有一些需求需要你调用多个服务,但是还需要确保服务的安全性、统一化每次的请求日志或者追踪用户完整的行为等等。要实现这些功能,你可能需要在所有服务中都设置一些相同的属性,虽然这个可以通过一些明确的接入文档来描述或者准入规范来界定,但是这么做的话还是有可能会有一些问题:
1. 你很难让每一个服务都实现上述功能。因为对于开发者而言,他们应当注重的是实现功能。很多项目的开发者经常在一些日常开发中遗漏了这些关键点,经常有人会忘记去打日志或者去记录调用链。但是对于一些大流量的互联网服务而言,一个线上服务一旦发生故障时,即使故障时间很小,其影响面会非常大。一旦有人在关键路径上忘记路记录日志,那么故障的排除成本会非常高,那样会导致影响面进一步扩大。
2. 事实上实现之前叙述的这些功能的成本也非常高。比如说对于鉴权(Identify)这个功能,你要是去一个服务一个服务地去实现,那样的成本也是非常高的。如果说把这个确保认证的责任分担在每个开发者身上,那样其实也会增加大家遗忘或者忽略的概率。
为了解决这样的问题,你可能需要一个框架来帮助你实现这些功能。比如说帮你在一些关键路径的请求上配置必要的鉴权或超时策略。那样服务间的调用会被多层中间件所过滤并检查,确保整体服务的稳定性。
# 设计目标
* 性能优异,不应该掺杂太多业务逻辑的成分
* 方便开发使用,开发对接的成本应该尽可能地小
* 后续鉴权、认证等业务逻辑的模块应该可以通过业务模块的开发接入该框架内
* 默认配置已经是 production ready 的配置,减少开发与线上环境的差异性
# 概览
* 参考`gin`设计整套HTTP框架,去除`gin`中不需要的部分逻辑
* 内置一些必要的中间件,便于业务方可以直接上手使用
# blademaster架构
![bm-arch](img/bm-arch-2-2.png)
`blademaster`由几个非常精简的内部模块组成。其中`Router`用于根据请求的路径分发请求,`Context`包含了一个完整的请求信息,`Handler`则负责处理传入的`Context``Handlers`为一个列表,一个串一个地执行。
所有的`middlerware`均以`Handler`的形式存在,这样可以保证`blademaster`自身足够精简且扩展性足够强。
![bm-arch](img/bm-arch-2-3.png)
`blademaster`处理请求的模式非常简单,大部分的逻辑都被封装在了各种`Handler`中。一般而言,业务逻辑作为最后一个`Handler`
正常情况下每个`Handler`按照顺序一个一个串行地执行下去,但是`Handler`中也可以中断整个处理流程,直接输出`Response`。这种模式常被用于校验登陆的`middleware`中:一旦发现请求不合法,直接响应拒绝。
请求处理的流程中也可以使用`Render`来辅助渲染`Response`,比如对于不同的请求需要响应不同的数据格式`JSON``XML`,此时可以使用不同的`Render`来简化逻辑。
# 扩展阅读
[bm快速开始](blademaster-quickstart.md)
[bm模块说明](blademaster-mod.md)
[bm中间件](blademaster-mid.md)
[bm基于pb生成](blademaster-pb.md)
+49
View File
@@ -0,0 +1,49 @@
## 熔断器/Breaker
熔断器是为了当依赖的服务已经出现故障时,主动阻止对依赖服务的请求。保证自身服务的正常运行不受依赖服务影响,防止雪崩效应。
## kratos内置breaker的组件
一般情况下直接使用kratos的组件时都自带了熔断逻辑,并且在提供了对应的breaker配置项。
目前在kratos内集成熔断器的组件有:
- RPC client: pkg/net/rpc/warden/client
- Mysql client:pkg/database/sql
- Tidb client:pkg/database/tidb
- Http client:pkg/net/http/blademaster
## 使用说明
```go
//初始化熔断器组
//一组熔断器公用同一个配置项,可从分组内取出单个熔断器使用。可用在比如mysql主从分离等场景。
brkGroup := breaker.NewGroup(&breaker.Config{})
//为每一个连接指定一个breaker
//此处假设一个客户端连接对象实例为conn
//breakName定义熔断器名称 一般可以使用连接地址
breakName = conn.Addr
conn.breaker = brkGroup.Get(breakName)
//在连接发出请求前判断熔断器状态
if err = conn.breaker.Allow(); err != nil {
return
}
//连接执行成功或失败将结果告知breaker
if(respErr != nil){
conn.breaker.MarkFailed()
}else{
conn.breaker.MarkSuccess()
}
```
## 配置说明
```go
type Config struct {
SwitchOff bool // 熔断器开关,默认关 false.
K float64 //触发熔断的错误率(K = 1 - 1/错误率)
Window xtime.Duration //统计桶窗口时间
Bucket int //统计桶大小
Request int64 //触发熔断的最少请求数量(请求少于该值时不会触发熔断)
}
```
+195
View File
@@ -0,0 +1,195 @@
# 开始使用
## 配置
进入项目中的configs目录,打开memcache.toml,我们可以看到:
```toml
[Client]
name = "demo"
proto = "tcp"
addr = "127.0.0.1:11211"
active = 50
idle = 10
dialTimeout = "100ms"
readTimeout = "200ms"
writeTimeout = "300ms"
idleTimeout = "80s"
```
在该配置文件中我们可以配置memcache的连接方式proto、连接地址addr、连接池的闲置连接数idle、最大连接数active以及各类超时。
## 初始化
进入项目的internal/dao目录,打开mc.go,其中:
```go
var cfg struct {
Client *memcache.Config
}
checkErr(paladin.Get("memcache.toml").UnmarshalTOML(&mc))
```
使用paladin配置管理工具将上文中的memcache.toml中的配置解析为我们需要使用的配置。
```go
// dao dao.
type dao struct {
mc *memcache.Memcache
mcExpire int32
}
```
在dao的主结构提中定义了memcache的连接池对象和过期时间。
```go
d = &dao{
// memcache
mc: memcache.New(mc.Demo),
mcExpire: int32(time.Duration(mc.DemoExpire) / time.Second),
}
```
使用kratos/pkg/cache/memcache包的New方法进行连接池对象的初始化,需要传入上文解析的配置。
## Ping
```go
// Ping ping the resource.
func (d *dao) Ping(ctx context.Context) (err error) {
return d.pingMC(ctx)
}
func (d *dao) pingMC(ctx context.Context) (err error) {
if err = d.mc.Set(ctx, &memcache.Item{Key: "ping", Value: []byte("pong"), Expiration: 0}); err != nil {
log.Error("conn.Set(PING) error(%v)", err)
}
return
}
```
生成的dao层模板中自带了memcache相关的ping方法,用于为负载均衡服务的健康监测提供依据,详见[blademaster](blademaster-quickstart.md)。
## 关闭
```go
// Close close the resource.
func (d *Dao) Close() {
d.mc.Close()
}
```
在关闭dao层时,通过调用memcache连接池对象的Close方法,我们可以关闭该连接池,从而释放相关资源。
# 常用方法
推荐使用[memcache代码生成器](kratos-genmc.md)帮助我们生成memcache操作的相关代码。
以下我们来逐一解析以下kratos/pkg/cache/memcache包中提供的常用方法。
## 单个查询
```go
// CacheDemo get data from mc
func (d *Dao) CacheDemo(c context.Context, id int64) (res *Demo, err error) {
key := demoKey(id)
res = &Demo{}
if err = d.mc.Get(c, key).Scan(res); err != nil {
res = nil
if err == memcache.ErrNotFound {
err = nil
}
}
if err != nil {
prom.BusinessErrCount.Incr("mc:CacheDemo")
log.Errorv(c, log.KV("CacheDemo", fmt.Sprintf("%+v", err)), log.KV("key", key))
return
}
return
}
```
如上为代码生成器生成的进行单个查询的代码,使用到mc.Get(c,key)方法获得返回值,再使用scan方法将memcache的返回值转换为golang中的类型(如string,bool, 结构体等)。
## 批量查询使用
```go
replies, err := d.mc.GetMulti(c, keys)
for _, key := range replies.Keys() {
v := &Demo{}
err = replies.Scan(key, v)
}
```
如上为代码生成器生成的进行批量查询的代码片段,这里使用到mc.GetMulti(c,keys)方法获得返回值,与单个查询类似地,我们需要再使用scan方法将memcache的返回值转换为我们定义的结构体。
## 设置KV
```go
// AddCacheDemo Set data to mc
func (d *Dao) AddCacheDemo(c context.Context, id int64, val *Demo) (err error) {
if val == nil {
return
}
key := demoKey(id)
item := &memcache.Item{Key: key, Object: val, Expiration: d.demoExpire, Flags: memcache.FlagJSON | memcache.FlagGzip}
if err = d.mc.Set(c, item); err != nil {
prom.BusinessErrCount.Incr("mc:AddCacheDemo")
log.Errorv(c, log.KV("AddCacheDemo", fmt.Sprintf("%+v", err)), log.KV("key", key))
return
}
return
}
```
如上为代码生成器生成的添加结构体进入memcache的代码,这里需要使用到的是mc.Set方法进行设置。
这里使用的item为memcache.Item结构体,包含key, value, 超时时间(秒), Flags。
### Flags
上文添加结构体进入memcache中,使用到的flags为:memcache.FlagJSON | memcache.FlagGzip代表着:使用json作为编码方式,gzip作为压缩方式。
Flags的相关常量在kratos/pkg/cache/memcache包中进行定义,包含编码方式如gob, json, protobuf,和压缩方式gzip。
```go
const(
// Flag, 15(encoding) bit+ 17(compress) bit
// FlagRAW default flag.
FlagRAW = uint32(0)
// FlagGOB gob encoding.
FlagGOB = uint32(1) << 0
// FlagJSON json encoding.
FlagJSON = uint32(1) << 1
// FlagProtobuf protobuf
FlagProtobuf = uint32(1) << 2
// FlagGzip gzip compress.
FlagGzip = uint32(1) << 15
)
```
## 删除KV
```go
// DelCacheDemo delete data from mc
func (d *Dao) DelCacheDemo(c context.Context, id int64) (err error) {
key := demoKey(id)
if err = d.mc.Delete(c, key); err != nil {
if err == memcache.ErrNotFound {
err = nil
return
}
prom.BusinessErrCount.Incr("mc:DelCacheDemo")
log.Errorv(c, log.KV("DelCacheDemo", fmt.Sprintf("%+v", err)), log.KV("key", key))
return
}
return
}
```
如上为代码生成器生成的从memcache中删除KV的代码,这里需要使用到的是mc.Delete方法。
和查询时类似地,当memcache中不存在参数中的key时,会返回error为memcache.ErrNotFound。如果不需要处理这种error,可以参考上述代码将返回出去的error置为nil。
# 扩展阅读
[memcache代码生成器](kratos-genmc.md)
[redis模块说明](cache-redis.md)
+181
View File
@@ -0,0 +1,181 @@
# 开始使用
## 配置
进入项目中的configs目录,打开redis.toml,我们可以看到:
```toml
[Client]
name = "kratos-demo"
proto = "tcp"
addr = "127.0.0.1:6389"
idle = 10
active = 10
dialTimeout = "1s"
readTimeout = "1s"
writeTimeout = "1s"
idleTimeout = "10s"
```
在该配置文件中我们可以配置redis的连接方式proto、连接地址addr、连接池的闲置连接数idle、最大连接数active以及各类超时。
## 初始化
进入项目的internal/dao目录,打开redis.go,其中:
```go
var cfg struct {
Client *memcache.Config
}
checkErr(paladin.Get("redis.toml").UnmarshalTOML(&rc))
```
使用paladin配置管理工具将上文中的redis.toml中的配置解析为我们需要使用的配置。
```go
// Dao dao.
type Dao struct {
redis *redis.Pool
redisExpire int32
}
```
在dao的主结构提中定义了redis的连接池对象和过期时间。
```go
d = &dao{
// redis
redis: redis.NewPool(rc.Demo),
redisExpire: int32(time.Duration(rc.DemoExpire) / time.Second),
}
```
使用kratos/pkg/cache/redis包的NewPool方法进行连接池对象的初始化,需要传入上文解析的配置。
## Ping
```go
// Ping ping the resource.
func (d *dao) Ping(ctx context.Context) (err error) {
return d.pingRedis(ctx)
}
func (d *dao) pingRedis(ctx context.Context) (err error) {
conn := d.redis.Get(ctx)
defer conn.Close()
if _, err = conn.Do("SET", "ping", "pong"); err != nil {
log.Error("conn.Set(PING) error(%v)", err)
}
return
}
```
生成的dao层模板中自带了redis相关的ping方法,用于为负载均衡服务的健康监测提供依据,详见[blademaster](blademaster-quickstart.md)。
## 关闭
```go
// Close close the resource.
func (d *Dao) Close() {
d.redis.Close()
}
```
在关闭dao层时,通过调用redis连接池对象的Close方法,我们可以关闭该连接池,从而释放相关资源。
# 常用方法
## 发送单个命令 Do
```go
// DemoIncrby .
func (d *dao) DemoIncrby(c context.Context, pid int) (err error) {
cacheKey := keyDemo(pid)
conn := d.redis.Get(c)
defer conn.Close()
if _, err = conn.Do("INCRBY", cacheKey, 1); err != nil {
log.Error("DemoIncrby conn.Do(INCRBY) key(%s) error(%v)", cacheKey, err)
}
return
}
```
如上为向redis server发送单个命令的用法示意。这里需要使用redis连接池的Get方法获取一个redis连接conn,再使用conn.Do方法即可发送一条指令。
注意,在使用该连接完毕后,需要使用conn.Close方法将该连接关闭。
## 批量发送命令 Pipeline
kratos/pkg/cache/redis包除了支持发送单个命令,也支持批量发送命令(redis pipeline),比如:
```go
// DemoIncrbys .
func (d *dao) DemoIncrbys(c context.Context, pid int) (err error) {
cacheKey := keyDemo(pid)
conn := d.redis.Get(c)
defer conn.Close()
if err = conn.Send("INCRBY", cacheKey, 1); err != nil {
return
}
if err = conn.Send("EXPIRE", cacheKey, d.redisExpire); err != nil {
return
}
if err = conn.Flush(); err != nil {
log.Error("conn.Flush error(%v)", err)
return
}
for i := 0; i < 2; i++ {
if _, err = conn.Receive(); err != nil {
log.Error("conn.Receive error(%v)", err)
return
}
}
return
}
```
和发送单个命令类似地,这里需要使用redis连接池的Get方法获取一个redis连接conn,在使用该连接完毕后,需要使用conn.Close方法将该连接关闭。
这里使用conn.Send方法将命令写入客户端的buffer(缓冲区)中,使用conn.Flush将客户端的缓冲区内的命令打包发送到redis server。redis server按顺序返回的reply可以使用conn.Receive方法进行接收和处理。
## 返回值转换
kratos/pkg/cache/redis包中也提供了Scan方法将redis server的返回值转换为golang类型。
除此之外,kratos/pkg/cache/redis包提供了大量返回值转换的快捷方式:
### 单个查询
单个查询可以使用redis.Uint64/Int64/Float64/Int/String/Bool/Bytes进行返回值的转换,比如:
```go
// GetDemo get
func (d *Dao) GetDemo(ctx context.Context, key string) (string, error) {
conn := d.redis.Get(ctx)
defer conn.Close()
return redis.String(conn.Do("GET", key))
}
```
### 批量查询
批量查询时候,可以使用redis.Int64s,Ints,Strings,ByteSlices方法转换如MGET,HMGET,ZRANGE,SMEMBERS等命令的返回值。
还可以使用StringMap, IntMap, Int64Map方法转换HGETALL命令的返回值,比如:
```go
// HGETALLDemo get
func (d *Dao) HGETALLDemo(c context.Context, pid int64) (res map[string]int64, err error) {
var (
key = keyDemo(pid)
conn = d.redis.Get(c)
)
defer conn.Close()
if res, err = redis.Int64Map(conn.Do("HGETALL", key)); err != nil {
log.Error("HGETALL %v failed error(%v)", key, err)
}
return
}
```
# 扩展阅读
[memcache模块说明](cache-mc.md)
+20
View File
@@ -0,0 +1,20 @@
# 背景
我们需要统一的cache包,用于进行各类缓存操作。
# 概览
* 缓存操作均使用连接池,保证较快的数据读写速度且提高系统的安全可靠性。
# Memcache
提供protobuf,gob,json序列化方式,gzip的memcache接口
[memcache模块说明](cache-mc.md)
# Redis
提供redis操作的各类接口以及各类将redis server返回值转换为golang类型的快捷方法。
[redis模块说明](cache-redis.md)
+118
View File
@@ -0,0 +1,118 @@
# Paladin SDK
## 配置模块化
进行配置的模块化是为了更好地管理配置,尽可能避免由修改配置带来的失误。
在配置种类里,可以看到其实 环境配置 和 应用配置 已经由平台进行管理化。
我们通常业务里只用配置 业务配置 和 在线配置 就可以了,之前我们大部分都是单个文件配置,而为了更好管理我们需要按类型进行拆分配置文件。
例如:
| 名称 | 说明 |
|:------|:------|
| application.toml | 在线配置 |
| mysql.toml | 业务db配置 |
| hbase.toml | 业务hbase配置 |
| memcache.toml | 业务mc配置 |
| redis.toml | 业务redis配置 |
| http.toml | 业务http client/server/auth配置 |
| grpc.toml | 业务grpc client/server配置 |
## 使用方式
paladin 是一个config SDK客户端,包括了remote、file、mock几个抽象功能,方便使用本地文件或者远程配置中心,并且集成了对象自动reload功能。
### 远程配置中心
可以通过环境变量注入,例如:APP_ID/DEPLOY_ENV/ZONE/HOSTNAME,然后通过paladin实现远程配置中心SDK进行配合使用。
### 指定本地文件:
```shell
./cmd -conf=/data/conf/app/demo.toml
# or multi file
./cmd -conf=/data/conf/app/
```
### mock配置文件
```go
func TestMain(t *testing.M) {
mock := make(map[string]string])
mock["application.toml"] = `
demoSwitch = false
demoNum = 100
demoAPI = "xxx"
`
paladin.DefaultClient = paladin.NewMock(mock)
}
```
### example main
```go
// main.go
func main() {
flag.Parse()
// 初始化paladin
if err := paladin.Init(); err != nil {
panic(err)
}
log.Init(nil) // debug flag: log.dir={path}
defer log.Close()
}
```
### example HTTP/gRPC
```toml
# http.toml
[server]
addr = "0.0.0.0:9000"
timeout = "1s"
```
```go
// server.go
func NewServer() {
// 默认配置用nil,这时读取HTTP/gRPC构架中的flag或者环境变量(可能是docker注入的环境变量,默认端口:8000/9000)
engine := bm.DefaultServer(nil)
// 除非自己要替换了配置,用http.toml
var bc struct {
Server *bm.ServerConfig
}
if err := paladin.Get("http.toml").UnmarshalTOML(&bc); err != nil {
// 不存在时,将会为nil使用默认配置
if err != paladin.ErrNotExist {
panic(err)
}
}
engine := bm.DefaultServer(bc.Server)
}
```
### example Service(在线配置热加载配置)
```go
# service.go
type Service struct {
ac *paladin.Map
}
func New() *Service {
// paladin.Map 通过atomic.Value支持自动热加载
var ac = new(paladin.TOML)
if err := paladin.Watch("application.toml", ac); err != nil {
panic(err)
}
s := &Service{
ac: ac,
}
return s
}
func (s *Service) Test() {
sw, err := s.ac.Get("switch").Bool()
if err != nil {
// TODO
}
// or use default value
sw := paladin.Bool(s.ac.Get("switch"), false)
}
```
+48
View File
@@ -0,0 +1,48 @@
# config
## 介绍
初看起来,配置管理可能很简单,但是这其实是不稳定的一个重要来源。
即变更管理导致的故障,我们目前基于配置中心(config-service)的部署方式,二进制文件的发布与配置文件的修改是异步进行的,每次变更配置,需要重新构建发布版。
由此,我们整体对配置文件进行梳理,对配置进行模块化,以及方便易用的paladin config sdk。
## 环境配置
| flag | env | remark |
|:----------|:----------|:------|
| region | REGION | 部署地区,sh-上海、gz-广州、bj-北京 |
| zone | ZONE | 分布区域,sh001-上海核心、sh004-上海嘉定 |
| deploy.env | DEPLOY_ENV | dev-开发、fat1-功能、uat-集成、pre-预发、prod-生产 |
| deploy.color | DEPLOY_COLOR | 服务颜色,blue(测试feature染色请求) |
| - | HOSTNAME | 主机名,xxx-hostname |
全局公用环境变量,通常为部署环境配置,由系统、发布系统或supervisor进行环境变量注入,并不用进行例外配置,如果是开发过程中则可以通过flag注入进行运行测试。
## 应用配置
| flag | env | default | remark |
|:----------|:----------|:-------------|:------|
| appid | APP_ID | - | 应用ID |
| http | HTTP | tcp://0.0.0.0:8000/?timeout=1s | http 监听端口 |
| http.perf | HTTP_PERF | tcp://0.0.0.0:2233/?timeout=1s | http perf 监听端口 |
| grpc | GRPC | tcp://0.0.0.0:9000/?timeout=1s&idle_timeout=60s | grpc 监听端口 |
| grpc.target | - | - | 指定服务运行:<br>-grpc.target=demo.service=127.0.0.1:9000 <br>-grpc.target=demo.service=127.0.0.2:9000 |
| discovery.nodes | DISCOVERY_NODES | - | 服务发现节点:127.0.0.1:7171,127.0.0.2:7171 |
| log.v | LOG_V | 0 | 日志级别:<br>DEBUG:0 INFO:1 WARN:2 ERROR:3 FATAL:4 |
| log.stdout | LOG_STDOUT | false | 是否标准输出:true、false|
| log.dir | LOG_DIR | - | 日志文件目录,如果配置会输出日志到文件,否则不输出日志文件 |
| log.agent | LOG_AGENT | - | 日志采集agent:<br>unixpacket:///var/run/lancer/collector_tcp.sock?timeout=100ms&chan=1024 |
| log.module | LOG_MODULE | - | 指定field信息 format: file=1,file2=2. |
| log.filter | LOG_FILTER | - | 过虑敏感信息 format: field1,field2. |
基本为一些应用相关的配置信息,通常发布系统和supervisor都有对应的部署环境进行配置注入,并不用进行例外配置,如果开发过程中可以通过flag进行注入运行测试。
## 业务配置
Redis、MySQL等业务组件,可以使用静态的配置文件来初始化,根据应用业务集群进行配置。
## 在线配置
需要在线读取、变更的配置信息,比如某个业务开关,可以实现配置reload实时更新。
## 扩展阅读
[paladin配置sdk](config-paladin.md)
+51
View File
@@ -0,0 +1,51 @@
# database/hbase
## 说明
Hbase Client,进行封装加入了链路追踪和统计。
## 配置
需要指定hbase集群的zookeeper地址。
```
config := &hbase.Config{Zookeeper: &hbase.ZKConfig{Addrs: []string{"localhost"}}}
client := hbase.NewClient(config)
```
## 使用方式
```
package main
import (
"context"
"fmt"
"github.com/go-kratos/kratos/pkg/database/hbase"
)
func main() {
config := &hbase.Config{Zookeeper: &hbase.ZKConfig{Addrs: []string{"localhost"}}}
client := hbase.NewClient(config)
//
values := map[string]map[string][]byte{"name": {"firstname": []byte("hello"), "lastname": []byte("world")}}
ctx := context.Background()
// 写入信息
// table: user
// rowkey: user1
// values["family"] = columns
_, err := client.PutStr(ctx, "user", "user1", values)
if err != nil {
panic(err)
}
// 读取信息
// table: user
// rowkey: user1
result, err := client.GetStr(ctx, "user", "user1")
if err != nil {
panic(err)
}
fmt.Printf("%v", result)
}
```
+42
View File
@@ -0,0 +1,42 @@
# 开始使用
## 配置
进入项目中的configs目录,mysql.toml,我们可以看到:
```toml
[demo]
addr = "127.0.0.1:3306"
dsn = "{user}:{password}@tcp(127.0.0.1:3306)/{database}?timeout=1s&readTimeout=1s&writeTimeout=1s&parseTime=true&loc=Local&charset=utf8mb4,utf8"
readDSN = ["{user}:{password}@tcp(127.0.0.2:3306)/{database}?timeout=1s&readTimeout=1s&writeTimeout=1s&parseTime=true&loc=Local&charset=utf8mb4,utf8","{user}:{password}@tcp(127.0.0.3:3306)/{database}?timeout=1s&readTimeout=1s&writeTimeout=1s&parseTime=true&loc=Local&charset=utf8,utf8mb4"]
active = 20
idle = 10
idleTimeout ="4h"
queryTimeout = "200ms"
execTimeout = "300ms"
tranTimeout = "400ms"
```
在该配置文件中我们可以配置mysql的读和写的dsn、连接地址addr、连接池的闲置连接数idle、最大连接数active以及各类超时。
如果配置了readDSN,在进行读操作的时候会优先使用readDSN的连接。
## 初始化
进入项目的internal/dao目录,打开db.go,其中:
```go
var cfg struct {
Client *sql.Config
}
checkErr(paladin.Get("db.toml").UnmarshalTOML(&dc))
```
使用paladin配置管理工具将上文中的db.toml中的配置解析为我们需要使用db的相关配置。
# TODO:补充常用方法
# 扩展阅读
[tidb模块说明](database-tidb.md)
[hbase模块说明](database-hbase.md)
+195
View File
@@ -0,0 +1,195 @@
# 开始使用
## 配置
进入项目中的configs目录,mysql.toml,我们可以看到:
```toml
[demo]
addr = "127.0.0.1:3306"
dsn = "{user}:{password}@tcp(127.0.0.1:3306)/{database}?timeout=1s&readTimeout=1s&writeTimeout=1s&parseTime=true&loc=Local&charset=utf8mb4,utf8"
readDSN = ["{user}:{password}@tcp(127.0.0.2:3306)/{database}?timeout=1s&readTimeout=1s&writeTimeout=1s&parseTime=true&loc=Local&charset=utf8mb4,utf8","{user}:{password}@tcp(127.0.0.3:3306)/{database}?timeout=1s&readTimeout=1s&writeTimeout=1s&parseTime=true&loc=Local&charset=utf8,utf8mb4"]
active = 20
idle = 10
idleTimeout ="4h"
queryTimeout = "200ms"
execTimeout = "300ms"
tranTimeout = "400ms"
```
在该配置文件中我们可以配置mysql的读和写的dsn、连接地址addr、连接池的闲置连接数idle、最大连接数active以及各类超时。
如果配置了readDSN,在进行读操作的时候会优先使用readDSN的连接。
## 初始化
进入项目的internal/dao目录,打开db.go,其中:
```go
var cfg struct {
Client *sql.Config
}
checkErr(paladin.Get("db.toml").UnmarshalTOML(&dc))
```
使用paladin配置管理工具将上文中的db.toml中的配置解析为我们需要使用db的相关配置。
```go
// Dao dao.
type Dao struct {
db *sql.DB
}
```
在dao的主结构提中定义了mysql的连接池对象。
```go
d = &dao{
db: sql.NewMySQL(dc.Demo),
}
```
使用kratos/pkg/database/sql包的NewMySQL方法进行连接池对象的初始化,需要传入上文解析的配置。
## Ping
```go
// Ping ping the resource.
func (d *dao) Ping(ctx context.Context) (err error) {
return d.db.Ping(ctx)
}
```
生成的dao层模板中自带了mysql相关的ping方法,用于为负载均衡服务的健康监测提供依据,详见[blademaster](blademaster-quickstart.md)。
## 关闭
```go
// Close close the resource.
func (d *dao) Close() {
d.db.Close()
}
```
在关闭dao层时,通过调用mysql连接池对象的Close方法,我们可以关闭该连接池,从而释放相关资源。
# 常用方法
## 单个查询
```go
// GetDemo 用户角色
func (d *dao) GetDemo(c context.Context, did int64) (demo int8, err error) {
err = d.db.QueryRow(c, _getDemoSQL, did).Scan(&demo)
if err != nil && err != sql.ErrNoRows {
log.Error("d.GetDemo.Query error(%v)", err)
return
}
return demo, nil
}
```
db.QueryRow方法用于返回最多一条记录的查询,在QueryRow方法后使用Scan方法即可将mysql的返回值转换为Golang的数据类型。
当mysql查询不到对应数据时,会返回sql.ErrNoRows,如果不需处理,可以参考如上代码忽略此error。
## 批量查询
```go
// ResourceLogs ResourceLogs.
func (d *dao) GetDemos(c context.Context, dids []int64) (demos []int8, err error) {
rows, err := d.db.Query(c, _getDemosSQL, dids)
if err != nil {
log.Error("query error(%v)", err)
return
}
defer rows.Close()
for rows.Next() {
var tmpD int8
if err = rows.Scan(&tmpD); err != nil {
log.Error("scan demo log error(%v)", err)
return
}
demos = append(demos, tmpD)
}
return
}
```
db.Query方法一般用于批量查询的场景,返回*sql.Rows和error信息。
我们可以使用rows.Next()方法获得下一行的返回结果,并且配合使用rows.Scan()方法将该结果转换为Golang的数据类型。当没有下一行时,rows.Next方法将返回false,此时循环结束。
注意,在使用完毕rows对象后,需要调用rows.Close方法关闭连接,释放相关资源。
## 执行语句
```go
// DemoExec exec
func (d *Dao) DemoExec(c context.Context, id int64) (rows int64, err error) {
res, err := d.db.Exec(c, _demoUpdateSQL, id)
if err != nil {
log.Error("db.DemoExec.Exec(%s) error(%v)", _demoUpdateSQL, err)
return
}
return res.RowsAffected()
}
```
执行UPDATE/DELETE/INSERT语句时,使用db.Exec方法进行语句执行,返回*sql.Result和error信息:
```go
// A Result summarizes an executed SQL command.
type Result interface {
LastInsertId() (int64, error)
RowsAffected() (int64, error)
}
```
Result接口支持获取影响行数和LastInsertId(一般用于获取Insert语句插入数据库后的主键ID)
## 事务
kratos/pkg/database/sql包支持事务操作,具体操作示例如下:
开启一个事务:
```go
tx := d.db.Begin()
if err = tx.Error; err != nil {
log.Error("db begin transcation failed, err=%+v", err)
return
}
```
在事务中执行语句:
```go
res, err := tx.Exec(_demoSQL, did)
if err != nil {
return
}
rows := res.RowsAffected()
```
提交事务:
```go
if err = tx.Commit().Error; err!=nil{
log.Error("db commit transcation failed, err=%+v", err)
}
```
回滚事务:
```go
if err = tx.Rollback().Error; err!=nil{
log.Error("db rollback failed, err=%+v", rollbackErr)
}
```
# 扩展阅读
[tidb模块说明](database-tidb.md)
[hbase模块说明](database-hbase.md)
View File
+19
View File
@@ -0,0 +1,19 @@
# database/sql
## 背景
数据库驱动,进行封装加入了熔断、链路追踪和统计,以及链路超时。
通常数据模块都写在`internal/dao`目录中,并提供对应的数据访问接口。
## MySQL
MySQL数据库驱动,支持读写分离、context、timeout、trace和统计功能,以及错误熔断防止数据库雪崩。
[mysql client](database-mysql.md)
[mysql client orm](database-mysql-orm.md)
## HBase
HBase客户端,支持trace、slowlog和统计功能。
[hbase client](database-hbase.md)
## TiDB
TiDB客户端,支持服务发现和熔断功能。
[tidb client](database-tidb.md)
+102
View File
@@ -0,0 +1,102 @@
# ecode
## 背景
错误码一般被用来进行异常传递,且需要具有携带`message`文案信息的能力。
## 错误码之Codes
`kratos`里,错误码被设计成`Codes`接口,声明如下[代码位置](https://github.com/go-kratos/kratos/blob/master/pkg/ecode/ecode.go):
```go
// Codes ecode error interface which has a code & message.
type Codes interface {
// sometimes Error return Code in string form
// NOTE: don't use Error in monitor report even it also work for now
Error() string
// Code get error code.
Code() int
// Message get code message.
Message() string
//Detail get error detail,it may be nil.
Details() []interface{}
}
// A Code is an int error code spec.
type Code int
```
可以看到该接口一共有四个方法,且`type Code int`结构体实现了该接口。
### 注册message
一个`Code`错误码可以对应一个`message`,默认实现会从全局变量`_messages`中获取,业务可以将自定义`Code`对应的`message`通过调用`Register`方法的方式传递进去,如:
```go
cms := map[int]string{
0: "很好很强大!",
-304: "啥都没变啊~",
-404: "啥都没有啊~",
}
ecode.Register(cms)
fmt.Println(ecode.OK.Message()) // 输出:很好很强大!
```
注意:`map[int]string`类型并不是绝对,比如有业务要支持多语言的场景就可以扩展为类似`map[int]LangStruct`的结构,因为全局变量`_messages``atomic.Value`类型,只需要修改对应的`Message`方法实现即可。
### Details
`Details`接口为`gRPC`预留,`gRPC`传递异常会将服务端的错误码pb序列化之后赋值给`Details`,客户端拿到之后反序列化得到,具体可阅读`status`的实现:
1. `ecode`包内的`Status`结构体实现了`Codes`接口[代码位置](https://github.com/go-kratos/kratos/blob/master/pkg/ecode/status.go)
2. `warden/internal/status`包内包装了`ecode.Status``grpc.Status`进行互相转换的方法[代码位置](https://github.com/go-kratos/kratos/blob/master/pkg/net/rpc/warden/internal/status/status.go)
3. `warden``client``server`则使用转换方法将`gRPC`底层返回的`error`最终转换为`ecode.Status` [代码位置](https://github.com/go-kratos/kratos/blob/master/pkg/net/rpc/warden/client.go#L162)
## 转换为ecode
错误码转换有以下两种情况:
1. 因为框架传递错误是靠`ecode`错误码,比如bm框架返回的`code`字段默认就是数字,那么客户端接收到如`{"code":-404}`的话,可以使用`ec := ecode.Int(-404)``ec := ecode.String("-404")`来进行转换。
2. 在项目中`dao`层返回一个错误码,往往返回参数类型建议为`error`而不是`ecode.Codes`,因为`error`更通用,那么上层`service`就可以使用`ec := ecode.Cause(err)`进行转换。
## 判断
错误码判断是否相等:
1. `ecode``ecode`判断使用:`ecode.Equal(ec1, ec2)`
2. `ecode``error`判断使用:`ecode.EqualError(ec, err)`
## 使用工具生成
使用proto协议定义错误码,格式如下:
```proto
// user.proto
syntax = "proto3";
package ecode;
enum UserErrCode {
UserUndefined = 0; // 因protobuf协议限制必须存在!!!无意义的0,工具生成代码时会忽略该参数
UserNotLogin = 123; // 正式错误码
}
```
需要注意以下几点:
1. 必须是enum类型,且名字规范必须以"ErrCode"结尾,如:UserErrCode
2. 因为protobuf协议限制,第一个enum值必须为无意义的0
使用`kratos tool protoc --ecode user.proto`进行生成,生成如下代码:
```go
package ecode
import (
"github.com/go-kratos/kratos/pkg/ecode"
)
var _ ecode.Codes
// UserErrCode
var (
UserNotLogin = ecode.New(123);
)
```
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 661 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

+30
View File
@@ -0,0 +1,30 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Kratos Documentation</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="description" content="Description">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<link rel="stylesheet" href="//cdn.jsdelivr.net/npm/docsify/lib/themes/vue.css">
</head>
<body>
<div id="app"></div>
<script src="//cdn.jsdelivr.net/npm/docsify/lib/docsify.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/docsify/lib/plugins/search.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/docsify-edit-on-github"></script>
<script>
window.$docsify = {
loadSidebar: true,
auto2top: true,
autoHeader: true,
name: 'go-kratos/kratos',
repo: 'https://github.com/go-kratos/kratos',
search: 'auto',
plugins: [
EditOnGithubPlugin.create('https://github.com/go-kratos/kratos/blob/master/docs/')
]
}
</script>
</body>
</html>
+66
View File
@@ -0,0 +1,66 @@
# 安装
1.安装protoc二进制文件
```
下载地址:https://github.com/google/protobuf/releases
mv bin/protoc /usr/local/bin/
mv -r include/google /usr/local/include/
```
2.安装protobuf库文件
```
go get -u github.com/golang/protobuf/proto
```
3.安装goprotobuf插件
```
go get github.com/golang/protobuf/protoc-gen-go
```
4.安装gogoprotobuf插件和依赖
```
//gogo
go get github.com/gogo/protobuf/protoc-gen-gogo
//gofast
go get github.com/gogo/protobuf/protoc-gen-gofast
//依赖
go get github.com/gogo/protobuf/proto
go get github.com/gogo/protobuf/gogoproto
```
5.安装框架依赖
```
# grpc (或者git clone https://github.com/grpc/grpc-go 然后复制到google.golang.org/grpc)
go get -u google.golang.org/grpc
# genproto (或者git clone https://github.com/google/go-genproto 然后复制到google.golang.org/genproto)
go get google.golang.org/genproto/...
```
6.安装kratos tool
```
go get -u github.com/go-kratos/kratos/tool/kratos
cd $GOPATH/src
kratos new kratos-demo --proto
```
7.运行
```
cd kratos-demo/cmd
go build
./cmd -conf ../configs
```
打开浏览器访问:[http://localhost:8000/kratos-demo/start](http://localhost:8000/kratos-demo/start),你会看到输出了`Golang 大法好 !!!`
[kratos工具](kratos-tool.md)
+27
View File
@@ -0,0 +1,27 @@
### kratos tool genbts
> 缓存回源代码生成
在internal/dao/dao.go中添加mc缓存interface定义,可以指定对应的[注解参数](../../tool/kratos-gen-bts/README.md);
并且在接口前面添加`go:generate kratos tool genbts`
然后在当前目录执行`go generate`,可以看到自动生成的dao.bts.go代码。
### 回源模板
```go
//go:generate kratos tool genbts
type _bts interface {
// bts: -batch=2 -max_group=20 -batch_err=break -nullcache=&Demo{ID:-1} -check_null_code=$.ID==-1
Demos(c context.Context, keys []int64) (map[int64]*Demo, error)
// bts: -sync=true -nullcache=&Demo{ID:-1} -check_null_code=$.ID==-1
Demo(c context.Context, key int64) (*Demo, error)
// bts: -paging=true
Demo1(c context.Context, key int64, pn int, ps int) (*Demo, error)
// bts: -nullcache=&Demo{ID:-1} -check_null_code=$.ID==-1
None(c context.Context) (*Demo, error)
}
```
### 参考
也可以参考完整的testdata例子:kratos/tool/kratos-gen-bts/testdata
+68
View File
@@ -0,0 +1,68 @@
### kratos tool genmc
> 缓存代码生成
在internal/dao/dao.go中添加mc缓存interface定义,可以指定对应的[注解参数](../../tool/kratos-gen-mc/README.md);
并且在接口前面添加`go:generate kratos tool genmc`
然后在当前目录执行`go generate`,可以看到自动生成的mc.cache.go代码。
### 缓存模板
```go
//go:generate kratos tool genmc
type _mc interface {
// mc: -key=demoKey
CacheDemos(c context.Context, keys []int64) (map[int64]*Demo, error)
// mc: -key=demoKey
CacheDemo(c context.Context, key int64) (*Demo, error)
// mc: -key=keyMid
CacheDemo1(c context.Context, key int64, mid int64) (*Demo, error)
// mc: -key=noneKey
CacheNone(c context.Context) (*Demo, error)
// mc: -key=demoKey
CacheString(c context.Context, key int64) (string, error)
// mc: -key=demoKey -expire=d.demoExpire -encode=json
AddCacheDemos(c context.Context, values map[int64]*Demo) error
// mc: -key=demo2Key -expire=d.demoExpire -encode=json
AddCacheDemos2(c context.Context, values map[int64]*Demo, tp int64) error
// 这里也支持自定义注释 会替换默认的注释
// mc: -key=demoKey -expire=d.demoExpire -encode=json|gzip
AddCacheDemo(c context.Context, key int64, value *Demo) error
// mc: -key=keyMid -expire=d.demoExpire -encode=gob
AddCacheDemo1(c context.Context, key int64, value *Demo, mid int64) error
// mc: -key=noneKey
AddCacheNone(c context.Context, value *Demo) error
// mc: -key=demoKey -expire=d.demoExpire
AddCacheString(c context.Context, key int64, value string) error
// mc: -key=demoKey
DelCacheDemos(c context.Context, keys []int64) error
// mc: -key=demoKey
DelCacheDemo(c context.Context, key int64) error
// mc: -key=keyMid
DelCacheDemo1(c context.Context, key int64, mid int64) error
// mc: -key=noneKey
DelCacheNone(c context.Context) error
}
func demoKey(id int64) string {
return fmt.Sprintf("art_%d", id)
}
func demo2Key(id, tp int64) string {
return fmt.Sprintf("art_%d_%d", id, tp)
}
func keyMid(id, mid int64) string {
return fmt.Sprintf("art_%d_%d", id, mid)
}
func noneKey() string {
return "none"
}
```
### 参考
也可以参考完整的testdata例子:kratos/tool/kratos-gen-mc/testdata
+28
View File
@@ -0,0 +1,28 @@
### kratos tool protoc
```shell
# generate all
kratos tool protoc api.proto
# generate gRPC
kratos tool protoc --grpc api.proto
# generate BM HTTP
kratos tool protoc --bm api.proto
# generate ecode
kratos tool protoc --ecode api.proto
# generate swagger
kratos tool protoc --swagger api.proto
```
执行生成如 `api.pb.go/api.bm.go/api.swagger.json/api.ecode.go` 的对应文件,需要注意的是:`ecode`生成有固定规则,需要首先是`enum`类型,且`enum`名字要以`ErrCode`结尾,如`enum UserErrCode`。详情可见:[example](https://github.com/go-kratos/kratos/tree/master/example/protobuf)
> 该工具在Windows/Linux下运行,需提前安装好 [protobuf](https://github.com/google/protobuf) 工具
`kratos tool protoc`本质上是拼接好了`protoc`命令然后进行执行,在执行时会打印出对应执行的`protoc`命令,如下可见:
```shell
protoc --proto_path=$GOPATH --proto_path=$GOPATH/github.com/go-kratos/kratos/third_party --proto_path=. --bm_out=:. api.proto
protoc --proto_path=$GOPATH --proto_path=$GOPATH/github.com/go-kratos/kratos/third_party --proto_path=. --gofast_out=plugins=grpc:. api.proto
protoc --proto_path=$GOPATH --proto_path=$GOPATH/github.com/go-kratos/kratos/third_party --proto_path=. --bswagger_out=:. api.proto
protoc --proto_path=$GOPATH --proto_path=$GOPATH/github.com/go-kratos/kratos/third_party --proto_path=. --ecode_out=:. api.proto
```
+8
View File
@@ -0,0 +1,8 @@
### kratos tool swagger
```shell
kratos tool swagger serve api/api.swagger.json
```
执行命令后,浏览器会自动打开swagger文档地址。
同时也可以查看更多的 [go-swagger](https://github.com/go-swagger/go-swagger) 官方参数进行使用。
+106
View File
@@ -0,0 +1,106 @@
# 介绍
kratos包含了一批好用的工具集,比如项目一键生成、基于proto生成http&grpc代码,生成缓存回源代码,生成memcache执行代码,生成swagger文档等。
# 获取工具
执行以下命令,即可快速安装好`kratos`工具
```shell
go get -u github.com/go-kratos/kratos/tool/kratos
```
那么接下来让我们快速开始熟悉工具的用法~
# kratos本体
`kratos`是所有工具集的本体,就像`go`一样,拥有执行各种子工具的能力,如`go build``go tool`。先让我们看看`-h`的输出:
```
NAME:
kratos - kratos tool
USAGE:
kratos [global options] command [command options] [arguments...]
VERSION:
0.0.1
COMMANDS:
new, n create new project
build, b kratos build
run, r kratos run
tool, t kratos tool
version, v kratos version
self-upgrade kratos self-upgrade
help, h Shows a list of commands or help for one command
GLOBAL OPTIONS:
--help, -h show help
--version, -v print the version
```
可以看到`kratos`有如:`new` `build` `run` `tool`等在内的COMMANDS,那么接下来一一演示如何使用。
# kratos new
`kratos new`是快速创建一个项目的命令,执行如下:
```shell
kratos new kratos-demo
```
即可快速在当前目录生成一个叫`kratos-demo`的项目。此外还支持指定owner和path,如下:
```shell
kratos new kratos-demo -o YourName -d YourPath
```
注意,`kratos new`默认会生成通过 protobuf 定义的`grpc``bm`示例代码的,如只生成bm请加`--http`,如下:
```shell
kratos new kratos-demo -o YourName -d YourPath --http
```
如只生成grpc请加`--grpc`,如下:
```shell
kratos new kratos-demo -o YourName -d YourPath --grpc
```
> 特别注意,如果不是MacOS系统,需要自己进行手动安装protoc,用于生成的示例项目`api`目录下的`proto`文件并不会自动生成对应的`.pb.go`和`.bm.go`文件。
> 也可以参考以下说明进行生成:[protoc说明](protoc.md)
# kratos build & run
`kratos build``kratos run``go build``go run`的封装,可以在当前项目任意目录进行快速运行进行调试,并无特别用途。
# kratos tool
`kratos tool`是基于proto生成http&grpc代码,生成缓存回源代码,生成memcache执行代码,生成swagger文档等工具集,先看下的执行效果:
```
kratos tool
protoc(已安装): 快速方便生成pb.go的protoc封装,windows、Linux请先安装protoc工具 Author(kratos) [2019/10/31]
genbts(已安装): 缓存回源逻辑代码生成器 Author(kratos) [2019/10/31]
testcli(已安装): 测试代码生成 Author(kratos) [2019/09/09]
genmc(已安装): mc缓存代码生成 Author(kratos) [2019/07/23]
swagger(已安装): swagger api文档 Author(goswagger.io) [2019/05/05]
安装工具: kratos tool install demo
执行工具: kratos tool demo
安装全部工具: kratos tool install all
全部升级: kratos tool upgrade all
```
> 小小说明:如未安装工具,第一次运行也可自动安装,不需要特别执行install
目前已经集成的工具有:
* [kratos](kratos-tool.md) 为本体工具,只用于安装更新使用;
* [protoc](kratos-protoc.md) 用于快速生成gRPC、HTTP、Swagger文件,该命令Windows,Linux用户需要手动安装 protobuf 工具;
* [swagger](kratos-swagger.md) 用于显示自动生成的HTTP API接口文档,通过 `kratos tool swagger serve api/api.swagger.json` 可以查看文档;
* [genmc](kratos-genmc.md) 用于自动生成memcached缓存代码;
* [genbts](kratos-genbts.md) 用于生成缓存回源代码生成,如果miss则调用回源函数从数据源获取,然后塞入缓存;
View File
+34
View File
@@ -0,0 +1,34 @@
# 日志基础库
## 概览
基于[zap](https://github.com/uber-go/zap)的field方式实现的高性能log库,提供Info、Warn、Error日志级别;
并提供了context支持,方便打印环境信息以及日志的链路追踪,在框架中都通过field方式实现,避免format日志带来的性能消耗。
## 配置选项
| flag | env | type | remark |
|:----------|:----------|:-------------:|:------|
| log.v | LOG_V | int | 日志级别:DEBUG:0 INFO:1 WARN:2 ERROR:3 FATAL:4 |
| log.stdout | LOG_STDOUT | bool | 是否标准输出:true、false|
| log.dir | LOG_DIR | string | 日志文件目录,如果配置会输出日志到文件,否则不输出日志文件 |
| log.agent | LOG_AGENT | string | 日志采集agent:unixpacket:///var/run/lancer/collector_tcp.sock?timeout=100ms&chan=1024 |
| log.module | LOG_MODULE | string | 指定field信息 format: file=1,file2=2. |
| log.filter | LOG_FILTER | string | 过虑敏感信息 format: field1,field2. |
## 使用方式
```go
func main() {
// 解析flag
flag.Parse()
// 初始化日志模块
log.Init(nil)
// 打印日志
log.Info("hi:%s", "kratos")
log.Infoc(Context.TODO(), "hi:%s", "kratos")
log.Infov(Context.TODO(), log.KVInt("key1", 100), log.KVString("key2", "test value")
}
```
## 扩展阅读
* [log-agent](log-agent.md)
+26
View File
@@ -0,0 +1,26 @@
# protoc
`protobuf`是Google官方出品的一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化,或者说序列化。它很适合做数据存储或 RPC 数据交换格式。可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。
使用`protobuf`,需要先书写`.proto`文件,然后编译该文件。编译`proto`文件则需要使用到官方的`protoc`工具,安装文档请参看:[google官方protoc工具](https://github.com/protocolbuffers/protobuf#protocol-compiler-installation)。
注意:`protoc`是用于编辑`proto`文件的工具,它并不具备生成对应语言代码的能力,所以正常都是`protoc`配合对应语言的代码生成工具来使用,如Go语言的[gogo protobuf](https://github.com/gogo/protobuf),请先点击按文档说明安装。
安装好对应工具后,我们可以进入`api`目录,执行如下命令:
```shell
export $KRATOS_HOME = kratos路径
export $KRATOS_DEMO = 项目路径
// 生成:api.pb.go
protoc -I$GOPATH/src:$KRATOS_HOME/tool/protobuf/pkg/extensions:$KRATOS_DEMO/api --gogofast_out=plugins=grpc:$KRATOS_DEMO/api $KRATOS_DEMO/api/api.proto
// 生成:api.bm.go
protoc -I$GOPATH/src:$KRATOS_HOME/tool/protobuf/pkg/extensions:$KRATOS_DEMO/api --bm_out=$KRATOS_DEMO/api $KRATOS_DEMO/api/api.proto
// 生成:api.swagger.json
protoc -I$GOPATH/src:$KRATOS_HOME/tool/protobuf/pkg/extensions:$KRATOS_DEMO/api --bswagger_out=$KRATOS_DEMO/api $KRATOS_DEMO/api/api.proto
```
请注意替换`/Users/felix/work/go/src`目录为你本地开发环境对应GOPATH目录,其中`--gogofast_out`意味着告诉`protoc`工具需要使用`gogo protobuf`的工具生成代码。
+69
View File
@@ -0,0 +1,69 @@
# 快速开始
快速使用kratos项目,可以使用`kratos`工具,如下:
```shell
go get -u github.com/go-kratos/kratos/tool/kratos
cd $GOPATH/src
kratos new kratos-demo
```
根据提示可以快速创建项目,如[kratos-demo](https://github.com/go-kratos/kratos-demo)就是通过工具创建生成。目录结构如下:
```
├── CHANGELOG.md
├── OWNERS
├── README.md
├── api # api目录为对外保留的proto文件及生成的pb.go文件
│   ├── api.bm.go
│   ├── api.pb.go # 通过go generate生成的pb.go文件
│   ├── api.proto
│   └── client.go
├── cmd
│   └── main.go # cmd目录为main所在
├── configs # configs为配置文件目录
│   ├── application.toml # 应用的自定义配置文件,可能是一些业务开关如:useABtest = true
│   ├── db.toml # db相关配置
│   ├── grpc.toml # grpc相关配置
│   ├── http.toml # http相关配置
│   ├── memcache.toml # memcache相关配置
│   └── redis.toml # redis相关配置
├── go.mod
├── go.sum
└── internal # internal为项目内部包,包括以下目录:
│   ├── dao # dao层,用于数据库、cache、MQ、依赖某业务grpc|http等资源访问
│   │   ├── dao.bts.go
│   │   ├── dao.go
│   │   ├── db.go
│   │   ├── mc.cache.go
│   │   ├── mc.go
│   │   └── redis.go
│   ├── di # 依赖注入层 采用wire静态分析依赖
│   │   ├── app.go
│   │   ├── wire.go # wire 声明
│   │   └── wire_gen.go # go generate 生成的代码
│   ├── model # model层,用于声明业务结构体
│   │   └── model.go
│   ├── server # server层,用于初始化grpc和http server
│   │   ├── grpc # grpc层,用于初始化grpc server和定义method
│   │   │   └── server.go
│   │   └── http # http层,用于初始化http server和声明handler
│   │   └── server.go
│   └── service # service层,用于业务逻辑处理,且为方便http和grpc共用方法,建议入参和出参保持grpc风格,且使用pb文件生成代码
│   └── service.go
└── test # 测试资源层 用于存放测试相关资源数据 如docker-compose配置 数据库初始化语句等
└── docker-compose.yaml
```
生成后可直接运行如下:
```shell
cd kratos-demo/cmd
go build
./cmd -conf ../configs
```
打开浏览器访问:[http://localhost:8000/kratos-demo/start](http://localhost:8000/kratos-demo/start),你会看到输出了`Golang 大法好 !!!`
[kratos工具](kratos-tool.md)
+57
View File
@@ -0,0 +1,57 @@
# 自适应限流保护
kratos 借鉴了 Sentinel 项目的自适应限流系统,通过综合分析服务的 cpu 使用率、请求成功的 qps 和请求成功的 rt 来做自适应限流保护。
## 核心目标
* 自动嗅探负载和 qps,减少人工配置
* 削顶,保证超载时系统不被拖垮,并能以高水位 qps 继续运行
## 限流规则
### 指标介绍
| 指标名称 | 指标含义 |
| -------- | ------------------------------------------------------------- |
| cpu | 最近 1s 的 CPU 使用率均值,使用滑动平均计算,采样周期是 250ms |
| inflight | 当前处理中正在处理的请求数量 |
| pass | 请求处理成功的量 |
| rt | 请求成功的响应耗时 |
### 滑动窗口
在自适应限流保护中,采集到的指标的时效性非常强,系统只需要采集最近一小段时间内的 qps、rt 即可,对于较老的数据,会自动丢弃。为了实现这个效果,kratos 使用了滑动窗口来保存采样数据。
![ratelimit-rolling-window](img/ratelimit-rolling-window.png)
如上图,展示了一个具有两个桶(bucket)的滑动窗口(rolling window)。整个滑动窗口用来保存最近 1s 的采样数据,每个小的桶用来保存 500ms 的采样数据。
当时间流动之后,过期的桶会自动被新桶的数据覆盖掉,在图中,在 1000-1500ms 时,bucket 1 的数据因为过期而被丢弃,之后 bucket 3 的数据填到了窗口的头部。
### 限流公式
判断是否丢弃当前请求的算法如下:
`cpu > 800 AND (Now - PrevDrop) < 1s AND (MaxPass * MinRt * windows / 1000) < InFlight`
MaxPass 表示最近 5s 内,单个采样窗口中最大的请求数。
MinRt 表示最近 5s 内,单个采样窗口中最小的响应时间。
windows 表示一秒内采样窗口的数量,默认配置中是 5s 50 个采样,那么 windows 的值为 10。
## 压测报告
场景1,请求以每秒增加1个的速度不停上升,压测效果如下:
![ratelimit-benchmark-up-1](img/ratelimit-benchmark-up-1.png)
左测是没有限流的压测效果,右侧是带限流的压测效果。
可以看到,没有限流的场景里,系统在 700qps 时开始抖动,在 1k qps 时被拖垮,几乎没有新的请求能被放行,然而在使用限流之后,系统请求能够稳定在 600 qps 左右,rt 没有暴增,服务也没有被打垮,可见,限流有效的保护了服务。
## 参考资料
[Sentinel 系统自适应限流](https://github.com/alibaba/Sentinel/wiki/%E7%B3%BB%E7%BB%9F%E8%87%AA%E9%80%82%E5%BA%94%E9%99%90%E6%B5%81)
+42
View File
@@ -0,0 +1,42 @@
# 背景
当代的互联网的服务,通常都是用复杂的、大规模分布式集群来实现的。互联网应用构建在不同的软件模块集上,这些软件模块,有可能是由不同的团队开发、可能使用不同的编程语言来实现、有可能布在了几千台服务器,横跨多个不同的数据中心。因此,就需要一些可以帮助理解系统行为、用于分析性能问题的工具。
# 概览
* kratos内部的trace基于opentracing语义
* 使用protobuf协议描述trace结构
* 全链路支持(gRPC/HTTP/MySQL/Redis/Memcached等)
## 参考文档
[opentracing](https://github.com/opentracing-contrib/opentracing-specification-zh/blob/master/specification.md)
[dapper](https://bigbully.github.io/Dapper-translation/)
# 使用
kratos本身不提供整套`trace`数据方案,但在`net/trace/report.go`内声明了`repoter`接口,可以简单的集成现有开源系统,比如:`zipkin``jaeger`
### zipkin使用
可以看[zipkin](https://github.com/go-kratos/kratos/tree/master/pkg/net/trace/zipkin)的协议上报实现,具体使用方式如下:
1. 前提是需要有一套自己搭建的`zipkin`集群
2. 在业务代码的`main`函数内进行初始化,代码如下:
```go
// 忽略其他代码
import "github.com/go-kratos/kratos/pkg/net/trace/zipkin"
// 忽略其他代码
func main(){
// 忽略其他代码
zipkin.Init(&zipkin.Config{
Endpoint: "http://localhost:9411/api/v2/spans",
})
// 忽略其他代码
}
```
### zipkin效果图
![zipkin](img/zipkin.jpg)
+500
View File
@@ -0,0 +1,500 @@
## 单元测试辅助工具
在单元测试中,我们希望每个测试用例都是独立的。这时候就需要Stub, Mock, Fakes等工具来帮助我们进行用例和依赖之间的隔离。
同时通过对错误情况的 Mock 也可以帮我们检查代码多个分支结果,从而提高覆盖率。
以下工具已加入到 Kratos 框架 go modules,可以借助 testgen 代码生成器自动生成部分工具代码,请放心食用。更多使用方法还欢迎大家多多探索。
### GoConvey
GoConvey是一套针对golang语言的BDD类型的测试框架。提供了良好的管理和执行测试用例的方式,包含丰富的断言函数,而且同时有测试执行和报告Web界面的支持。
#### 使用特性
为了更好的使用 GoConvey 来编写和组织测试用例,需要注意以下几点特性:
1. Convey方法和So方法的使用
> - Convey方法声明了一种规格的组织,每个组织内包含一句描述和一个方法。在方法内也可以嵌套其他Convey语句和So语句。
```Go
// 顶层Convey方法,需引入*testing.T对象
Convey(description string, t *testing.T, action func())
// 其他嵌套Convey方法,无需引入*testing.T对象
Convey(description string, action func())
```
注:同一Scope下的Convey语句描述不可以相同!
> - So方法是断言方法,用于对执行结果进行比对。GoConvey官方提供了大量断言,同时也可以自定义自己的断言([戳这里了解官方文档](https://github.com/smartystreets/goconvey/wiki/Assertions))
```Go
// A=B断言
So(A, ShouldEqual, B)
// A不为空断言
So(A, ShouldNotBeNil)
```
2. 执行次序
> 假设有以下Convey伪代码,执行次序将为A1B2A1C3。将Convey方法类比树的结点的话,整体执行类似树的遍历操作。
> 所以Convey A部分可在组织测试用例时,充当“Setup”的方法。用于初始化等一些操作。
```Go
Convey伪代码
Convey A
So 1
Convey B
So 2
Convey C
So 3
```
3. Reset方法
> GoConvey提供了Reset方法来进行“Teardown”的操作。用于执行完测试用例后一些状态的回收,连接关闭等操作。Reset方法不可与顶层Convey语句在同层。
```Go
// Reset
Reset func(action func())
```
假设有以下带有Reset方法的伪代码,同层Convey语句执行完后均会执行同层的Reset方法。执行次序为A1B2C3EA1D4E。
```Go
Convey A
So 1
Convey B
So 2
Convey C
So 3
Convey D
So 4
Reset E
```
4. 自然语言逻辑到测试用例的转换
> 在了解了Convey方法的特性和执行次序后,我们可以通过这些性质把对一个方法的测试用例按照日常逻辑组织起来。尤其建议使用Given-When-Then的形式来组织
> - 比较直观的组织示例
```Go
Convey("Top-level", t, func() {
// Setup 工作,在本层内每个Convey方法执行前都会执行的部分:
db.Open()
db.Initialize()
Convey("Test a query", func() {
db.Query()
// TODO: assertions here
})
Convey("Test inserts", func() {
db.Insert()
// TODO: assertions here
})
Reset(func() {
// Teardown工作,在本层内每个Convey方法执行完后都会执行的部分:
db.Close()
})
})
```
> - 定义单独的包含Setup和Teardown的帮助方法
```Go
package main
import (
"database/sql"
"testing"
_ "github.com/lib/pq"
. "github.com/smartystreets/goconvey/convey"
)
// 帮助方法,将原先所需的处理方法以参数(f)形式传入
func WithTransaction(db *sql.DB, f func(tx *sql.Tx)) func() {
return func() {
// Setup工作
tx, err := db.Begin()
So(err, ShouldBeNil)
Reset(func() {
// Teardown工作
/* Verify that the transaction is alive by executing a command */
_, err := tx.Exec("SELECT 1")
So(err, ShouldBeNil)
tx.Rollback()
})
// 调用传入的闭包做实际的事务处理
f(tx)
}
}
func TestUsers(t *testing.T) {
db, err := sql.Open("postgres", "postgres://localhost?sslmode=disable")
if err != nil {
panic(err)
}
Convey("Given a user in the database", t, WithTransaction(db, func(tx *sql.Tx) {
_, err := tx.Exec(`INSERT INTO "Users" ("id", "name") VALUES (1, 'Test User')`)
So(err, ShouldBeNil)
Convey("Attempting to retrieve the user should return the user", func() {
var name string
data := tx.QueryRow(`SELECT "name" FROM "Users" WHERE "id" = 1`)
err = data.Scan(&name)
So(err, ShouldBeNil)
So(name, ShouldEqual, "Test User")
})
}))
}
```
#### 使用建议
强烈建议使用 [testgen](ut-testgen.md) 进行测试用例的生成,生成后每个方法将包含一个符合以下规范的正向用例。
用例规范:
1. 每个方法至少包含一个测试方法(命名为Test[PackageName][FunctionName])
2. 每个测试方法包含一个顶层Convey语句,仅在此引入admin *testing.T类型的对象,在该层进行变量声明。
3. 每个测试方法不同的用例用Convey方法组织
4. 每个测试用例的一组断言用一个Convey方法组织
5. 使用convey.C保持上下文一致
### MonkeyPatching
#### 特性和使用条件
1. Patch()对任何无接收者的方法均有效
2. PatchInstanceMethod()对有接收者的包内/私有方法无法工作(因使用到了反射机制)。可以采用给私有方法的下一级打补丁,或改为无接收者的方法,或将方法转为公有
#### 适用场景(建议)
项目代码中上层对下层包依赖时,下层包方法Mock(例如service层对dao层方法依赖时)
基础库(MySql, Memcache, Redis)错误Mock
其他标准库,基础库以及第三方包方法Mock
#### 使用示例
1. 上层包对下层包依赖示例
Service层对Dao层依赖:
```GO
// 原方法
func (s *Service) realnameAlipayApply(c context.Context, mid int64) (info *model.RealnameAlipayApply, err error) {
if info, err = s.mbDao.RealnameAlipayApply(c, mid); err != nil {
return
}
...
return
}
// 测试方法
func TestServicerealnameAlipayApply(t *testing.T) {
convey.Convey("realnameAlipayApply", t, func(ctx convey.C) {
...
ctx.Convey("When everything goes positive", func(ctx convey.C) {
guard := monkey.PatchInstanceMethod(reflect.TypeOf(s.mbDao), "RealnameAlipayApply", func(_ *dao.Dao, _ context.Context, _ int64) (*model.RealnameAlipayApply, error) {
return nil, nil
})
defer guard.Unpatch()
info, err := s.realnameAlipayApply(c, mid)
ctx.Convey("Then err should be nil,info should not be nil", func(ctx convey.C) {
ctx.So(info, convey.ShouldNotBeNil)
ctx.So(err, convey.ShouldBeNil)
})
})
})
}
```
2. 基础库错误Mock示例
```Go
// 原方法(部分)
func (d *Dao) BaseInfoCache(c context.Context, mid int64) (info *model.BaseInfo, err error) {
...
conn := d.mc.Get(c)
defer conn.Close()
item, err := conn.Get(key)
if err != nil {
log.Error("conn.Get(%s) error(%v)", key, err)
return
}
...
return
}
// 测试方法(错误Mock部分)
func TestDaoBaseInfoCache(t *testing.T) {
convey.Convey("BaseInfoCache", t, func(ctx convey.C) {
...
Convey("When conn.Get gets error", func(ctx convey.C) {
guard := monkey.PatchInstanceMethod(reflect.TypeOf(d.mc), "Get", func(_ *memcache.Pool, _ context.Context) memcache.Conn {
return memcache.MockWith(memcache.ErrItemObject)
})
defer guard.Unpatch()
_, err := d.BaseInfoCache(c, mid)
ctx.Convey("Error should be equal to memcache.ErrItemObject", func(ctx convey.C) {
ctx.So(err, convey.ShouldEqual, memcache.ErrItemObject)
})
})
})
}
```
#### 注意事项
- Monkey非线程安全
- Monkey无法针对Inline方法打补丁,在测试时可以使用go test -gcflags=-l来关闭inline编译的模式(一些简单的go inline介绍戳这里)
- Monkey在一些面向安全不允许内存页写和执行同时进行的操作系统上无法工作
- 更多详情请戳:https://github.com/bouk/monkey
### Gock——HTTP请求Mock工具
#### 特性和使用条件
#### 工作原理
1. 截获任意通过 http.DefaultTransport或者自定义http.Transport对外的http.Client请求
2. 以“先进先出”原则将对外需求和预定义好的HTTP Mock池中进行匹配
3. 如果至少一个Mock被匹配,将按照2中顺序原则组成Mock的HTTP返回
4. 如果没有Mock被匹配,若实际的网络可用,将进行实际的HTTP请求。否则将返回错误
#### 特性
- 内建帮助工具实现JSON/XML简单Mock
- 支持持久的、易失的和TTL限制的Mock
- 支持HTTP Mock请求完整的正则表达式匹配
- 可通过HTTP方法,URL参数,请求头和请求体匹配
- 可扩展和可插件化的HTTP匹配规则
- 具备在Mock和实际网络模式之间切换的能力
- 具备过滤和映射HTTP请求到正确的Mock匹配的能力
- 支持映射和过滤可以更简单的掌控Mock
- 通过使用http.RoundTripper接口广泛兼容HTTP拦截器
- 可以在任意net/http兼容的Client上工作
- 网络延迟模拟(beta版本)
- 无其他依赖
#### 适用场景(建议)
任何需要进行HTTP请求的操作,建议全部用Gock进行Mock,以减少对环境的依赖。
使用示例:
1. net/http 标准库 HTTP 请求Mock
```Go
import gock "gopkg.in/h2non/gock.v1"
// 原方法
func (d *Dao) Upload(c context.Context, fileName, fileType string, expire int64, body io.Reader) (location string, err error) {
...
resp, err = d.bfsClient.Do(req) //d.bfsClient类型为*http.client
...
if resp.StatusCode != http.StatusOK {
...
}
header = resp.Header
code = header.Get("Code")
if code != strconv.Itoa(http.StatusOK) {
...
}
...
return
}
// 测试方法
func TestDaoUpload(t *testing.T) {
convey.Convey("Upload", t, func(ctx convey.C) {
...
// d.client 类型为 *http.client 根据Gock包描述需要设置http.Client的Transport情况。也可在TestMain中全局设置,则所有的HTTP请求均通过Gock来解决
d.client.Transport = gock.DefaultTransport // !注意:进行httpMock前需要对http 请求进行拦截,否则Mock失败
// HTTP请求状态和Header都正确的Mock
ctx.Convey("When everything is correct", func(ctx convey.C) {
httpMock("PUT", url).Reply(200).SetHeaders(map[string]string{
"Code": "200",
"Location": "SomePlace",
})
location, err := d.Upload(c, fileName, fileType, expire, body)
ctx.Convey("Then err should be nil.location should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(location, convey.ShouldNotBeNil)
})
})
...
// HTTP请求状态错误Mock
ctx.Convey("When http request status != 200", func(ctx convey.C) {
d.client.Transport = gock.DefaultTransport
httpMock("PUT", url).Reply(404)
_, err := d.Upload(c, fileName, fileType, expire, body)
ctx.Convey("Then err should not be nil", func(ctx convey.C) {
ctx.So(err, convey.ShouldNotBeNil)
})
})
// HTTP请求Header中Code值错误Mock
ctx.Convey("When http request Code in header != 200", func(ctx convey.C) {
d.client.Transport = gock.DefaultTransport
httpMock("PUT", url).Reply(404).SetHeaders(map[string]string{
"Code": "404",
"Location": "SomePlace",
})
_, err := d.Upload(c, fileName, fileType, expire, body)
ctx.Convey("Then err should not be nil", func(ctx convey.C) {
ctx.So(err, convey.ShouldNotBeNil)
})
})
// 由于同包内有其他进行实际HTTP请求的测试。所以再每次用例结束后,进行现场恢复(关闭Gock设置默认的Transport)
ctx.Reset(func() {
gock.OffAll()
d.client.Transport = http.DefaultClient.Transport
})
})
}
func httpMock(method, url string) *gock.Request {
r := gock.New(url)
r.Method = strings.ToUpper(method)
return r
}
```
2. blademaster库HTTP请求Mock
```Go
// 原方法
func (d *Dao) SendWechatToGroup(c context.Context, chatid, msg string) (err error) {
...
if err = d.client.Do(c, req, &res); err != nil {
...
}
if res.Code != 0 {
...
}
return
}
// 测试方法
func TestDaoSendWechatToGroup(t *testing.T) {
convey.Convey("SendWechatToGroup", t, func(ctx convey.C) {
...
// 根据Gock包描述需要设置bm.Client的Transport情况。也可在TestMain中全局设置,则所有的HTTP请求均通过Gock来解决。
// d.client 类型为 *bm.client
d.client.SetTransport(gock.DefaultTransport) // !注意:进行httpMock前需要对http 请求进行拦截,否则Mock失败
// HTTP请求状态和返回内容正常Mock
ctx.Convey("When everything gose postive", func(ctx convey.C) {
httpMock("POST", _sagaWechatURL+"/appchat/send").Reply(200).JSON(`{"code":0,"message":"0"}`)
err := d.SendWechatToGroup(c, d.c.WeChat.ChatID, msg)
...
})
// HTTP请求状态错误Mock
ctx.Convey("When http status != 200", func(ctx convey.C) {
httpMock("POST", _sagaWechatURL+"/appchat/send").Reply(404)
err := d.SendWechatToGroup(c, d.c.WeChat.ChatID, msg)
...
})
// HTTP请求返回值错误Mock
ctx.Convey("When http response code != 0", func(ctx convey.C) {
httpMock("POST", _sagaWechatURL+"/appchat/send").Reply(200).JSON(`{"code":-401,"message":"0"}`)
err := d.SendWechatToGroup(c, d.c.WeChat.ChatID, msg)
...
})
// 由于同包内有其他进行实际HTTP请求的测试。所以再每次用例结束后,进行现场恢复(关闭Gock设置默认的Transport)。
ctx.Reset(func() {
gock.OffAll()
d.client.SetTransport(http.DefaultClient.Transport)
})
})
}
func httpMock(method, url string) *gock.Request {
r := gock.New(url)
r.Method = strings.ToUpper(method)
return r
}
```
#### 注意事项
- Gock不是完全线程安全的
- 如果执行并发代码,在配置Gock和解释定制的HTTP clients时,要确保Mock已经事先声明好了来避免不需要的竞争机制
- 更多详情请戳:https://github.com/h2non/gock
### GoMock
#### 使用条件
只能对公有接口(interface)定义的代码进行Mock,并仅能在测试过程中进行
#### 使用方法
- 官方安装使用步骤
```shell
## 获取GoMock包和自动生成Mock代码工具mockgen
go get github.com/golang/mock/gomock
go install github.com/golang/mock/mockgen
## 生成mock文件
## 方法1:生成对应文件下所有interface
mockgen -source=path/to/your/interface/file.go
## 方法2:生成对应包内指定多个interface,并用逗号隔开
mockgen database/sql/driver Conn,Driver
## 示例:
mockgen -destination=$GOPATH/kratos/app/xxx/dao/dao_mock.go -package=dao kratos/app/xxx/dao DaoInterface
```
- testgen 使用步骤(GoMock生成功能已集成在Creater工具中,无需额外安装步骤即可直接使用)
```shell
## 直接给出含有接口类型定义的包路径,生成Mock文件将放在包目录下一级mock/pkgName_mock.go中
./creater --m mock absolute/path/to/your/pkg
```
- 测试代码内使用方法
```Go
// 测试用例内直接使用
// 需引入的包
import (
...
"github.com/otokaze/mock/gomock"
...
)
func TestPkgFoo(t *testing.T) {
convey.Convey("Foo", t, func(ctx convey.C) {
...
ctx.Convey("Mock Interface to test", func(ctx convey.C) {
// 1. 使用gomock.NewController新增一个控制器
mockCtrl := gomock.NewController(t)
// 2. 测试完成后关闭控制器
defer mockCtrl.Finish()
// 3. 以控制器为参数生成Mock对象
yourMock := mock.NewMockYourClient(mockCtrl)
// 4. 使用Mock对象替代原代码中的对象
yourClient = yourMock
// 5. 使用EXPECT().方法名(方法参数).Return(返回值)来构造所需输入/输出
yourMock.EXPECT().YourMethod(gomock.Any()).Return(nil)
res:= Foo(params)
...
})
...
})
}
// 可以利用Convey执行顺序方式适当调整以简化代码
func TestPkgFoo(t *testing.T) {
convey.Convey("Foo", t, func(ctx convey.C) {
...
mockCtrl := gomock.NewController(t)
yourMock := mock.NewMockYourClient(mockCtrl)
ctx.Convey("Mock Interface to test1", func(ctx convey.C) {
yourMock.EXPECT().YourMethod(gomock.Any()).Return(nil)
...
})
ctx.Convey("Mock Interface to test2", func(ctx convey.C) {
yourMock.EXPECT().YourMethod(args).Return(res)
...
})
...
ctx.Reset(func(){
mockCtrl.Finish()
})
})
}
```
#### 适用场景(建议)
1. gRPC中的Client接口
2. 也可改造现有代码构造Interface后使用(具体可配合Creater的功能进行Interface和Mock的生成)
3. 任何对接口中定义方法依赖的场景
#### 注意事项
- 如有Mock文件在包内,在执行单元测试时Mock代码会被识别进行测试。请注意Mock文件的放置。
- 更多详情请戳:https://github.com/golang/mock
+152
View File
@@ -0,0 +1,152 @@
## testcli UT运行环境构建工具
基于 docker-compose 实现跨平台跨语言环境的容器依赖管理方案,以解决运行ut场景下的 (mysql, redis, mc)容器依赖问题。
*这个是testing/lich的二进制工具版本(Go请直接使用库版本:github.com/go-kratos/kratos/pkg/testing/lich)*
### 功能和特性
- 自动读取 test 目录下的 yaml 并启动依赖
- 自动导入 test 目录下的 DB 初始化 SQL
- 提供特定容器内的 healthcheck (mysql, mc, redis)
- 提供一站式解决 UT 服务依赖的工具版本 (testcli)
### 编译安装
*使用本工具/库需要前置安装好 docker & docker-compose@v1.24.1^*
#### Method 1. With go get
```shell
go get -u github.com/go-kratos/kratos/tool/testcli
$GOPATH/bin/testcli -h
```
#### Method 2. Build with Go
```shell
cd github.com/go-kratos/kratos/tool/testcli
go build -o $GOPATH/bin/testcli
$GOPATH/bin/testcli -h
```
#### Method 3. Import with Kratos pkg
```Go
import "github.com/go-kratos/kratos/pkg/testing/lich"
```
### 构建数据
#### Step 1. create docker-compose.yml
创建依赖服务的 docker-compose.yml,并把它放在项目路径下的 test 文件夹下面。例如:
```shell
mkdir -p $YOUR_PROJECT/test
```
```yaml
version: "3.7"
services:
db:
image: mysql:5.6
ports:
- 3306:3306
environment:
- MYSQL_ROOT_PASSWORD=root
volumes:
- .:/docker-entrypoint-initdb.d
command: [
'--character-set-server=utf8',
'--collation-server=utf8_unicode_ci'
]
redis:
image: redis
ports:
- 6379:6379
```
一般来讲,我们推荐在项目根目录创建 test 目录,里面存放描述服务的yml,以及需要初始化的数据(database.sql等)。
同时也需要注意,正确的对容器内服务进行健康检测,testcli会在容器的health状态执行UT,其实我们也内置了针对几个较为通用镜像(mysql mariadb mc redis)的健康检测,也就是不写也没事(^^;;
#### Step 2. export database.sql
构造初始化的数据(database.sql等),当然也把它也在 test 文件夹里。
```sql
CREATE DATABASE IF NOT EXISTS `YOUR_DATABASE_NAME`;
SET NAMES 'utf8';
USE `YOUR_DATABASE_NAME`;
CREATE TABLE IF NOT EXISTS `YOUR_TABLE_NAME` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键'
PRIMARY KEY (`id`),
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='YOUR_TABLE_NAME';
```
这里需要注意,在创建库/表的时候尽量加上 IF NOT EXISTS,以给予一定程度的容错,以及 SET NAMES 'utf8'; 用于解决客户端连接乱码问题。
#### Step 3. change your project mysql config
```toml
[mysql]
addr = "127.0.0.1:3306"
dsn = "root:root@tcp(127.0.0.1:3306)/YOUR_DATABASE?timeout=1s&readTimeout=1s&writeTimeout=1s&parseTime=true&loc=Local&charset=utf8mb4,utf8"
active = 20
idle = 10
idleTimeout ="1s"
queryTimeout = "1s"
execTimeout = "1s"
tranTimeout = "1s"
```
*Step 1* 我们已经指定了服务对外暴露的端口为3306(这当然也可以是你指定的任何值),那理所应当的我们也要修改项目连接数据库的配置~
Great! 至此你已经完成了运行所需要用到的数据配置,接下来就来运行它。
### 运行
开头也说过本工具支持两种运行方式:testcli 二进制工具版本和 go package 源码包,业务方可以根据需求场景进行选择。
#### Method 1. With testcli tool
*已支持的 flag: -f,--nodown,down,run*
- -f,指定 docker-compose.yaml 文件路径,默认为当前目录下。
- --nodown,指定是否在UT执行完成后保留容器,以供下次复用。
- down,teardown 销毁当前项目下这个 compose 文件产生的容器。
- run,运行你当前语言的单测执行命令(如:golang为 go test -v ./)
example:
```shell
testcli -f ../../test/docker-compose.yaml run go test -v ./
```
#### Method 2. Import with Kratos pkg
- Step1. 在 Dao|Service 层中的 TestMain 单测主入口中,import "github.com/go-kratos/kratos/pkg/testing/lich" 引入testcli工具的go库版本。
- Step2. 使用 flag.Set("f", "../../test/docker-compose.yaml") 指定 docker-compose.yaml 文件的路径。
- Step3. 在 flag.Parse() 后即可使用 lich.Setup() 安装依赖&初始化数据(注意测试用例执行结束后 lich.Teardown() 回收下~)
- Step4. 运行 `go test -v ./ `看看效果吧~
example:
```Go
package dao
import (
"flag"
"os"
"strings"
"testing"
"github.com/go-kratos/kratos/pkg/conf/paladin"
"github.com/go-kratos/kratos/pkg/testing/lich"
)
var (
d *Dao
)
func TestMain(m *testing.M) {
flag.Set("conf", "../../configs")
flag.Set("f", "../../test/docker-compose.yaml")
flag.Parse()
if err := paladin.Init(); err != nil {
panic(err)
}
if err := lich.Setup(); err != nil {
panic(err)
}
defer lich.Teardown()
d = New()
if code := m.Run(); code != 0 {
panic(code)
}
}
```
## 注意
因为启动mysql容器较为缓慢,健康检测的机制会重试3次,每次暂留5秒钟,基本在10s内mysql就能从creating到服务正常启动!
当然你也可以在使用 testcli 时加上 --nodown,使其不用每次跑都新建容器,只在第一次跑的时候会初始化容器,后面都进行复用,这样速度会快很多。
+45
View File
@@ -0,0 +1,45 @@
## testgen UT代码自动生成器
解放你的双手,让你的UT一步到位!
### 功能和特性
- 支持生成 Dao|Service 层UT代码功能(每个方法包含一个正向用例)
- 支持生成 Dao|Service 层测试入口文件dao_test.go, service_test.go(用于控制初始化,控制测试流程等)
- 支持生成Mock代码(使用GoMock框架)
- 支持选择不同模式生成不同代码(使用"–m mode"指定)
- 生成单元测试代码时,同时支持传入目录或文件
- 支持指定方法追加生成测试用例(使用"–func funcName"指定)
### 编译安装
#### Method 1. With go get
```shell
go get -u github.com/go-kratos/kratos/tool/testgen
$GOPATH/bin/testgen -h
```
#### Method 2. Build with Go
```shell
cd github.com/go-kratos/kratos/tool/testgen
go build -o $GOPATH/bin/testgen
$GOPATH/bin/testgen -h
```
### 运行
#### 生成Dao/Service层单元UT
```shell
$GOPATH/bin/testgen YOUR_PROJECT/dao # default mode
$GOPATH/bin/testgen --m test path/to/your/pkg
$GOPATH/bin/testgen --func functionName path/to/your/pkg
```
#### 生成接口类型
```shell
$GOPATH/bin/testgen --m interface YOUR_PROJECT/dao #当前仅支持传目录,如目录包含子目录也会做处理
```
#### 生成Mock代码
```shell
$GOPATH/bin/testgen --m mock YOUR_PROJECT/dao #仅传入包路径即可
```
#### 生成Monkey代码
```shell
$GOPATH/bin/testgen --m monkey yourCodeDirPath #仅传入包路径即可
```
+32
View File
@@ -0,0 +1,32 @@
# 背景
单元测试即对最小可测试单元进行检查和验证,它可以很好的让你的代码在上测试环境之前自己就能前置的发现问题,解决问题。当然每个语言都有原生支持的 UT 框架,不过在 Kratos 里面我们需要有一些配套设施以及周边工具来辅助我们构筑整个 UT 生态。
# 工具链
- testgen UT代码自动生成器(README: tool/testgen/README.md)
- testcli UT运行环境构建工具(README: tool/testcli/README.md)
# 测试框架选型
golang 的单元测试,既可以用官方自带的 testing 包,也有开源的如 testify、goconvey 业内知名,使用非常多也很好用的框架。
根据一番调研和内部使用经验,我们确定:
> - testing 作为基础库测试框架(非常精简不过够用)
> - goconvey 作为业务程序的单元测试框架(因为涉及比较多的业务场景和流程控制判断,比如更丰富的res值判断、上下文嵌套支持、还有webUI等)
# 单元测试标准
1. 覆盖率,当前标准:60%(所有包均需达到)
尽量达到70%以上。当然覆盖率并不能完全说明单元测试的质量,开发者需要考虑关键的条件判断和预期的结果。复杂的代码是需要好好设计测试用例的。
2. 通过率,当前标准:100%(所有用例中的断言必须通过)
# 书写建议
1. 结果验证
> - 校验err是否为nil. err是go函数的标配了,也是最基础的判断,如果err不为nil,基本上函数返回值或者处理肯定是有问题了。
> - 检验res值是否正确。res值的校验是非常重要的,也是很容易忽略的地方。比如返回结构体对象,要对结构体的成员进行判断,而有可能里面是0值。goconvey对res值的判断支持是非常友好的。
2. 逻辑验证
> 业务代码经常是流程比较复杂的,而函数的执行结果也是有上下文的,比如有不同条件分支。goconvey就非常优雅的支持了这种情况,可以嵌套执行。单元测试要结合业务代码逻辑,才能尽量的减少线上bug。
3. 如何mock
主要分以下3块:
> - 基础组件,如mc、redis、mysql等,由 testcli(testing/lich) 起基础镜像支持(需要提供建表、INSERT语句)与本地开发环境一致,也保证了结果的一致性。
> - rpc server,如 xxxx-service 需要定义 interface 供业务依赖方使用。所有rpc server 都必须要提供一个interface+mock代码(gomock)。
> - http server则直接写mock代码gock。
+39
View File
@@ -0,0 +1,39 @@
# Warden Balancer
## 介绍
grpc-go内置了round-robin轮询,但由于自带的轮询算法不支持权重,也不支持color筛选等需求,故需要重新实现一个负载均衡算法。
## WRR (Weighted Round Robin)
该算法在加权轮询法基础上增加了动态调节权重值,用户可以在为每一个节点先配置一个初始的权重分,之后算法会根据节点cpu、延迟、服务端错误率、客户端错误率动态打分,在将打分乘用户自定义的初始权重分得到最后的权重值。
## P2C (Pick of two choices)
本算法通过随机选择两个node选择优胜者来避免羊群效应,并通过ewma尽量获取服务端的实时状态。
服务端:
服务端获取最近500ms内的CPU使用率(需要将cgroup设置的限制考虑进去,并除于CPU核心数),并将CPU使用率乘与1000后塞入每次grpc请求中的的Trailer中夹带返回:
cpu_usage
uint64 encoded with string
cpu_usage : 1000
客户端:
主要参数:
* server_cpu:通过每次请求中服务端塞在trailer中的cpu_usage拿到服务端最近500ms内的cpu使用率
* inflight:当前客户端正在发送并等待response的请求数(pending request)
* latency: 加权移动平均算法计算出的接口延迟
* client_success:加权移动平均算法计算出的请求成功率(只记录grpc内部错误,比如context deadline)
目前客户端,已经默认使用p2c负载均衡算法`grpc.WithBalancerName(p2c.Name)`
```go
// NewClient returns a new blank Client instance with a default client interceptor.
// opt can be used to add grpc dial options.
func NewClient(conf *ClientConfig, opt ...grpc.DialOption) *Client {
c := new(Client)
if err := c.SetConfig(conf); err != nil {
panic(err)
}
c.UseOpt(grpc.WithBalancerName(p2c.Name))
c.UseOpt(opt...)
c.Use(c.recovery(), clientLogging(), c.handle())
return c
}
```
+374
View File
@@ -0,0 +1,374 @@
# 说明
gRPC暴露了两个拦截器接口,分别是:
* `grpc.UnaryServerInterceptor`服务端拦截器
* `grpc.UnaryClientInterceptor`客户端拦截器
基于两个拦截器可以针对性的定制公共模块的封装代码,比如`warden/logging.go`是通用日志逻辑。
# 分析
## 服务端拦截器
让我们先看一下`grpc.UnaryServerInterceptor`的声明,[官方代码位置](https://github.com/grpc/grpc-go/blob/master/interceptor.go):
```go
// UnaryServerInfo consists of various information about a unary RPC on
// server side. All per-rpc information may be mutated by the interceptor.
type UnaryServerInfo struct {
// Server is the service implementation the user provides. This is read-only.
Server interface{}
// FullMethod is the full RPC method string, i.e., /package.service/method.
FullMethod string
}
// UnaryHandler defines the handler invoked by UnaryServerInterceptor to complete the normal
// execution of a unary RPC. If a UnaryHandler returns an error, it should be produced by the
// status package, or else gRPC will use codes.Unknown as the status code and err.Error() as
// the status message of the RPC.
type UnaryHandler func(ctx context.Context, req interface{}) (interface{}, error)
// UnaryServerInterceptor provides a hook to intercept the execution of a unary RPC on the server. info
// contains all the information of this RPC the interceptor can operate on. And handler is the wrapper
// of the service method implementation. It is the responsibility of the interceptor to invoke handler
// to complete the RPC.
type UnaryServerInterceptor func(ctx context.Context, req interface{}, info *UnaryServerInfo, handler UnaryHandler) (resp interface{}, err error)
```
看起来很简单包括:
* 一个`UnaryServerInfo`结构体用于`Server``FullMethod`字段传递,`Server``gRPC server`的对象实例,`FullMethod`为请求方法的全名
* 一个`UnaryHandler`方法用于传递`Handler`,就是基于`proto`文件`service`内声明而生成的方法
* 一个`UnaryServerInterceptor`用于拦截`Handler`方法,可在`Handler`执行前后插入拦截代码
为了更形象的说明拦截器的执行过程,请看基于`proto`生成的以下代码[代码位置](https://github.com/go-kratos/kratos-demo/blob/master/api/api.pb.go):
```go
func _Demo_SayHello_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(HelloReq)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DemoServer).SayHello(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/demo.service.v1.Demo/SayHello",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DemoServer).SayHello(ctx, req.(*HelloReq))
}
return interceptor(ctx, in, info, handler)
}
```
这个`_Demo_SayHello_Handler`方法是关键,该方法会被包装为`grpc.ServiceDesc`结构,被注册到gRPC内部,具体可在生成的`pb.go`代码内查找`s.RegisterService(&_Demo_serviceDesc, srv)`
*`gRPC server`收到一次请求时,首先根据请求方法从注册到`server`内的`grpc.ServiceDesc`找到该方法对应的`Handler`如:`_Demo_SayHello_Handler`并执行
* `_Demo_SayHello_Handler`执行过程请看上面具体代码,当`interceptor`不为`nil`时,会将`SayHello`包装为`grpc.UnaryHandler`结构传递给`interceptor`
这样就完成了`UnaryServerInterceptor`的执行过程。那么`_Demo_SayHello_Handler`内的`interceptor`是如何注入到`gRPC server`内,则看下面这段代码[官方代码位置](https://github.com/grpc/grpc-go/blob/master/server.go):
```go
// UnaryInterceptor returns a ServerOption that sets the UnaryServerInterceptor for the
// server. Only one unary interceptor can be installed. The construction of multiple
// interceptors (e.g., chaining) can be implemented at the caller.
func UnaryInterceptor(i UnaryServerInterceptor) ServerOption {
return func(o *options) {
if o.unaryInt != nil {
panic("The unary server interceptor was already set and may not be reset.")
}
o.unaryInt = i
}
}
```
请一定注意这方法的注释!!!
> Only one unary interceptor can be installed. The construction of multiple interceptors (e.g., chaining) can be implemented at the caller.
`gRPC`本身只支持一个`interceptor`,想要多`interceptors`需要自己实现~~所以`warden`基于`grpc.UnaryClientInterceptor`实现了`interceptor chain`,请看下面代码[代码位置](https://github.com/go-kratos/kratos/blob/master/pkg/net/rpc/warden/server.go):
```go
// Use attachs a global inteceptor to the server.
// For example, this is the right place for a rate limiter or error management inteceptor.
func (s *Server) Use(handlers ...grpc.UnaryServerInterceptor) *Server {
finalSize := len(s.handlers) + len(handlers)
if finalSize >= int(_abortIndex) {
panic("warden: server use too many handlers")
}
mergedHandlers := make([]grpc.UnaryServerInterceptor, finalSize)
copy(mergedHandlers, s.handlers)
copy(mergedHandlers[len(s.handlers):], handlers)
s.handlers = mergedHandlers
return s
}
// interceptor is a single interceptor out of a chain of many interceptors.
// Execution is done in left-to-right order, including passing of context.
// For example ChainUnaryServer(one, two, three) will execute one before two before three, and three
// will see context changes of one and two.
func (s *Server) interceptor(ctx context.Context, req interface{}, args *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
var (
i int
chain grpc.UnaryHandler
)
n := len(s.handlers)
if n == 0 {
return handler(ctx, req)
}
chain = func(ic context.Context, ir interface{}) (interface{}, error) {
if i == n-1 {
return handler(ic, ir)
}
i++
return s.handlers[i](ic, ir, args, chain)
}
return s.handlers[0](ctx, req, args, chain)
}
```
很简单的逻辑:
* `warden server`使用`Use`方法进行`grpc.UnaryServerInterceptor`的注入,而`func (s *Server) interceptor`本身就实现了`grpc.UnaryServerInterceptor`
* `func (s *Server) interceptor`可以根据注册的`grpc.UnaryServerInterceptor`顺序从前到后依次执行
`warden`在初始化的时候将该方法本身注册到了`gRPC server`,在`NewServer`方法内可以看到下面代码:
```go
opt = append(opt, keepParam, grpc.UnaryInterceptor(s.interceptor))
s.server = grpc.NewServer(opt...)
```
如此完整的服务端拦截器逻辑就串联完成。
## 客户端拦截器
让我们先看一下`grpc.UnaryClientInterceptor`的声明,[官方代码位置](https://github.com/grpc/grpc-go/blob/master/interceptor.go):
```go
// UnaryInvoker is called by UnaryClientInterceptor to complete RPCs.
type UnaryInvoker func(ctx context.Context, method string, req, reply interface{}, cc *ClientConn, opts ...CallOption) error
// UnaryClientInterceptor intercepts the execution of a unary RPC on the client. invoker is the handler to complete the RPC
// and it is the responsibility of the interceptor to call it.
// This is an EXPERIMENTAL API.
type UnaryClientInterceptor func(ctx context.Context, method string, req, reply interface{}, cc *ClientConn, invoker UnaryInvoker, opts ...CallOption) error
```
看起来和服务端拦截器并没有什么太大的区别,比较简单包括:
* 一个`UnaryInvoker`表示客户端具体要发出的执行方法
* 一个`UnaryClientInterceptor`用于拦截`Invoker`方法,可在`Invoker`执行前后插入拦截代码
具体执行过程,请看基于`proto`生成的下面代码[代码位置](https://github.com/go-kratos/kratos-demo/blob/master/api/api.pb.go):
```go
func (c *demoClient) SayHello(ctx context.Context, in *HelloReq, opts ...grpc.CallOption) (*google_protobuf1.Empty, error) {
out := new(google_protobuf1.Empty)
err := grpc.Invoke(ctx, "/demo.service.v1.Demo/SayHello", in, out, c.cc, opts...)
if err != nil {
return nil, err
}
return out, nil
}
```
当客户端调用`SayHello`时可以看到执行了`grpc.Invoke`方法,并且将`fullMethod`和其他参数传入,最终会执行下面代码[官方代码位置](https://github.com/grpc/grpc-go/blob/master/call.go):
```go
// Invoke sends the RPC request on the wire and returns after response is
// received. This is typically called by generated code.
//
// All errors returned by Invoke are compatible with the status package.
func (cc *ClientConn) Invoke(ctx context.Context, method string, args, reply interface{}, opts ...CallOption) error {
// allow interceptor to see all applicable call options, which means those
// configured as defaults from dial option as well as per-call options
opts = combine(cc.dopts.callOptions, opts)
if cc.dopts.unaryInt != nil {
return cc.dopts.unaryInt(ctx, method, args, reply, cc, invoke, opts...)
}
return invoke(ctx, method, args, reply, cc, opts...)
}
```
其中的`unaryInt`即为客户端连接创建时注册的拦截器,使用下面代码注册[官方代码位置](https://github.com/grpc/grpc-go/blob/master/dialoptions.go):
```go
// WithUnaryInterceptor returns a DialOption that specifies the interceptor for
// unary RPCs.
func WithUnaryInterceptor(f UnaryClientInterceptor) DialOption {
return newFuncDialOption(func(o *dialOptions) {
o.unaryInt = f
})
}
```
需要注意的是客户端的拦截器在官方`gRPC`内也只能支持注册一个,与服务端拦截器`interceptor chain`逻辑类似`warden`在客户端拦截器也做了相同处理,并且在客户端连接时进行注册,请看下面代码[代码位置](https://github.com/go-kratos/kratos/blob/master/pkg/net/rpc/warden/client.go):
```go
// Use attachs a global inteceptor to the Client.
// For example, this is the right place for a circuit breaker or error management inteceptor.
func (c *Client) Use(handlers ...grpc.UnaryClientInterceptor) *Client {
finalSize := len(c.handlers) + len(handlers)
if finalSize >= int(_abortIndex) {
panic("warden: client use too many handlers")
}
mergedHandlers := make([]grpc.UnaryClientInterceptor, finalSize)
copy(mergedHandlers, c.handlers)
copy(mergedHandlers[len(c.handlers):], handlers)
c.handlers = mergedHandlers
return c
}
// chainUnaryClient creates a single interceptor out of a chain of many interceptors.
//
// Execution is done in left-to-right order, including passing of context.
// For example ChainUnaryClient(one, two, three) will execute one before two before three.
func (c *Client) chainUnaryClient() grpc.UnaryClientInterceptor {
n := len(c.handlers)
if n == 0 {
return func(ctx context.Context, method string, req, reply interface{},
cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
return invoker(ctx, method, req, reply, cc, opts...)
}
}
return func(ctx context.Context, method string, req, reply interface{},
cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
var (
i int
chainHandler grpc.UnaryInvoker
)
chainHandler = func(ictx context.Context, imethod string, ireq, ireply interface{}, ic *grpc.ClientConn, iopts ...grpc.CallOption) error {
if i == n-1 {
return invoker(ictx, imethod, ireq, ireply, ic, iopts...)
}
i++
return c.handlers[i](ictx, imethod, ireq, ireply, ic, chainHandler, iopts...)
}
return c.handlers[0](ctx, method, req, reply, cc, chainHandler, opts...)
}
}
```
如此完整的客户端拦截器逻辑就串联完成。
# 实现自己的拦截器
以服务端拦截器`logging`为例:
```go
// serverLogging warden grpc logging
func serverLogging() grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
// NOTE: handler执行之前的拦截代码:主要获取一些关键参数,如耗时计时、ip等
// 如果自定义的拦截器只需要在handler执行后,那么可以直接执行handler
startTime := time.Now()
caller := metadata.String(ctx, metadata.Caller)
if caller == "" {
caller = "no_user"
}
var remoteIP string
if peerInfo, ok := peer.FromContext(ctx); ok {
remoteIP = peerInfo.Addr.String()
}
var quota float64
if deadline, ok := ctx.Deadline(); ok {
quota = time.Until(deadline).Seconds()
}
// call server handler
resp, err := handler(ctx, req) // NOTE: 以具体执行的handler为分界线!!!
// NOTE: handler执行之后的拦截代码:主要进行耗时计算、日志记录
// 如果自定义的拦截器在handler执行后不需要逻辑,这可直接返回
// after server response
code := ecode.Cause(err).Code()
duration := time.Since(startTime)
// monitor
statsServer.Timing(caller, int64(duration/time.Millisecond), info.FullMethod)
statsServer.Incr(caller, info.FullMethod, strconv.Itoa(code))
logFields := []log.D{
log.KVString("user", caller),
log.KVString("ip", remoteIP),
log.KVString("path", info.FullMethod),
log.KVInt("ret", code),
// TODO: it will panic if someone remove String method from protobuf message struct that auto generate from protoc.
log.KVString("args", req.(fmt.Stringer).String()),
log.KVFloat64("ts", duration.Seconds()),
log.KVFloat64("timeout_quota", quota),
log.KVString("source", "grpc-access-log"),
}
if err != nil {
logFields = append(logFields, log.KV("error", err.Error()), log.KV("stack", fmt.Sprintf("%+v", err)))
}
logFn(code, duration)(ctx, logFields...)
return resp, err
}
}
```
# 内置拦截器
## 自适应限流拦截器
更多关于自适应限流的信息,请参考:[kratos 自适应限流](ratelimit.md)
```go
package grpc
import (
pb "kratos-demo/api"
"kratos-demo/internal/service"
"github.com/go-kratos/kratos/pkg/conf/paladin"
"github.com/go-kratos/kratos/pkg/net/rpc/warden"
"github.com/go-kratos/kratos/pkg/net/rpc/warden/ratelimiter"
)
// New new a grpc server.
func New(svc *service.Service) *warden.Server {
var rc struct {
Server *warden.ServerConfig
}
if err := paladin.Get("grpc.toml").UnmarshalTOML(&rc); err != nil {
if err != paladin.ErrNotExist {
panic(err)
}
}
ws := warden.NewServer(rc.Server)
// 挂载自适应限流拦截器到 warden server,使用默认配置
limiter := ratelimiter.New(nil)
ws.Use(limiter.Limit())
// 注意替换这里:
// RegisterDemoServer方法是在"api"目录下代码生成的
// 对应proto文件内自定义的service名字,请使用正确方法名替换
pb.RegisterDemoServer(ws.Server(), svc)
ws, err := ws.Start()
if err != nil {
panic(err)
}
return ws
}
```
# 扩展阅读
[warden快速开始](warden-quickstart.md)
[warden基于pb生成](warden-pb.md)
[warden负载均衡](warden-balancer.md)
[warden服务发现](warden-resolver.md)
+48
View File
@@ -0,0 +1,48 @@
# 介绍
基于proto文件可以快速生成`warden`框架对应的代码,提前需要准备以下工作:
* 安装`kratos tool protoc`工具,请看[kratos工具](kratos-tool.md)
* 编写`proto`文件,示例可参考[kratos-demo内proto文件](https://github.com/go-kratos/kratos-demo/blob/master/api/api.proto)
### kratos工具说明
`kratos tool protoc`工具可以生成`warden` `bm` `swagger`对应的代码和文档,想要单独生成`warden`代码只需加上`--grpc`如:
```shell
# generate gRPC
kratos tool protoc --grpc api.proto
```
# 使用
建议在项目`api`目录下编写`proto`文件及生成对应的代码,可参考[kratos-demo内的api目录](https://github.com/go-kratos/kratos-demo/tree/master/api)。
执行命令后生成的`api.pb.go`代码,注意其中的`DemoClient``DemoServer`,其中:
* `DemoClient`接口为客户端调用接口,相对应的有`demoClient`结构体为其实现
* `DemoServer`接口为服务端接口声明,需要业务自己实现该接口的所有方法,`kratos`建议在`internal/service`目录下使用`Service`结构体实现
`internal/service`内的`Service`结构实现了`DemoServer`接口可参考[kratos-demo内的service](https://github.com/go-kratos/kratos-demo/blob/master/internal/service/service.go)内的如下代码:
```go
// SayHelloURL bm demo func.
func (s *Service) SayHelloURL(ctx context.Context, req *pb.HelloReq) (reply *pb.HelloResp, err error) {
reply = &pb.HelloResp{
Content: "hello " + req.Name,
}
fmt.Printf("hello url %s", req.Name)
return
}
```
更详细的客户端和服务端使用请看[warden快速开始](warden-quickstart.md)
# 扩展阅读
[warden快速开始](warden-quickstart.md)
[warden拦截器](warden-mid.md)
[warden负载均衡](warden-balancer.md)
[warden服务发现](warden-resolver.md)
+171
View File
@@ -0,0 +1,171 @@
# 准备工作
推荐使用[kratos工具](kratos-tool.md)快速生成带`grpc`的项目,如我们生成一个叫`kratos-demo`的项目。
```
kratos new kratos-demo --proto
```
# pb文件
创建项目成功后,进入`api`目录下可以看到`api.proto``api.pb.go``generate.go`文件,其中:
* `api.proto`是gRPC server的描述文件
* `api.pb.go`是基于`api.proto`生成的代码文件
* `generate.go`是用于`kratos tool protoc`执行`go generate`进行代码生成的临时文件
接下来可以将以上三个文件全部删除或者保留`generate.go`,之后编写自己的proto文件,确认proto无误后,进行代码生成:
* 可直接执行`kratos tool protoc`,该命令会调用protoc工具生成`.pb.go`文件
*`generate.go`没删除,也可以执行`go generate`命令,将调用`kratos tool protoc`工具进行代码生成
[kratos工具请看](kratos-tool.md)
### 如没看kprotoc文档,请看下面这段话
`kratos tool protoc`用于快速生成`pb.go`文件,但目前windows和Linux需要先自己安装`protoc`工具,具体请看[protoc说明](protoc.md)。
# 注册server
进入`internal/server/grpc`目录打开`server.go`文件,可以看到以下代码,只需要替换以下注释内容就可以启动一个gRPC服务。
```go
package grpc
import (
pb "kratos-demo/api"
"kratos-demo/internal/service"
"github.com/go-kratos/kratos/pkg/conf/paladin"
"github.com/go-kratos/kratos/pkg/net/rpc/warden"
)
// New new a grpc server.
func New(svc *service.Service) *warden.Server {
var rc struct {
Server *warden.ServerConfig
}
if err := paladin.Get("grpc.toml").UnmarshalTOML(&rc); err != nil {
if err != paladin.ErrNotExist {
panic(err)
}
}
ws := warden.NewServer(rc.Server)
// 注意替换这里:
// RegisterDemoServer方法是在"api"目录下代码生成的
// 对应proto文件内自定义的service名字,请使用正确方法名替换
pb.RegisterDemoServer(ws.Server(), svc)
ws, err := ws.Start()
if err != nil {
panic(err)
}
return ws
}
```
### 注册注意
```go
// SayHello grpc demo func.
func (s *Service) SayHello(ctx context.Context, req *pb.HelloReq) (reply *empty.Empty, err error) {
reply = new(empty.Empty)
fmt.Printf("hello %s", req.Name)
return
}
```
请进入`internal/service`内找到`SayHello`方法,注意方法的入参和出参,都是按照gRPC的方法声明对应的:
* 第一个参数必须是`context.Context`,第二个必须是proto内定义的`message`对应生成的结构体
* 第一个返回值必须是proto内定义的`message`对应生成的结构体,第二个参数必须是`error`
* 在http框架bm中,如果共用proto文件生成bm代码,那么也可以直接使用该service方法
建议service严格按照此格式声明方法使其能够在bm和warden内共用。
# client调用
请进入`internal/dao`方法内,一般对资源的处理都会在这一层封装。
对于`client`端,前提必须有对应`proto`文件生成的代码,那么有两种选择:
* 拷贝proto文件到自己项目下并且执行代码生成
* 直接import服务端的api package
> 这也是业务代码我们加了一层`internal`的关系,服务对外暴露的只有接口
不管哪一种方式,以下初始化gRPC client的代码建议伴随生成的代码存放在统一目录下:
```go
package dao
import (
"context"
"github.com/go-kratos/kratos/pkg/net/rpc/warden"
"google.golang.org/grpc"
)
// target server addrs.
const target = "direct://default/127.0.0.1:9000,127.0.0.1:9091" // NOTE: example
// NewClient new member grpc client
func NewClient(cfg *warden.ClientConfig, opts ...grpc.DialOption) (DemoClient, error) {
client := warden.NewClient(cfg, opts...)
conn, err := client.Dial(context.Background(), target)
if err != nil {
return nil, err
}
// 注意替换这里:
// NewDemoClient方法是在"api"目录下代码生成的
// 对应proto文件内自定义的service名字,请使用正确方法名替换
return NewDemoClient(conn), nil
}
```
其中,`target`为gRPC用于服务发现的目标,使用标准url资源格式提供给resolver用于服务发现。`warden`默认使用`direct`直连方式,直接与`server`端进行连接。如果在使用其他服务发现组件请看[warden服务发现](warden-resolver.md)。
有了初始化`Client`的代码,我们的`Dao`对象即可进行初始化和使用,以下以直接import服务端api包为例:
```go
package dao
import(
demoapi "kratos-demo/api"
grpcempty "github.com/golang/protobuf/ptypes/empty"
"github.com/go-kratos/kratos/pkg/net/rpc/warden"
"github.com/pkg/errors"
)
type Dao struct{
demoClient demoapi.DemoClient
}
// New account dao.
func New() (d *Dao) {
cfg := &warden.ClientConfig{}
paladin.Get("grpc.toml").UnmarshalTOML(cfg)
d = &Dao{}
var err error
if d.demoClient, err = demoapi.NewClient(cfg); err != nil {
panic(err)
}
return
}
// SayHello say hello.
func (d *Dao) SayHello(c context.Context, req *demoapi.HelloReq) (resp *grpcempty.Empty, err error) {
if resp, err = d.demoClient.SayHello(c, req); err != nil {
err = errors.Wrapf(err, "%v", arg)
}
return
}
```
如此在`internal/service`层就可以进行资源的方法调用。
# 扩展阅读
[warden拦截器](warden-mid.md)
[warden基于pb生成](warden-pb.md)
[warden服务发现](warden-resolver.md)
[warden负载均衡](warden-balancer.md)
+254
View File
@@ -0,0 +1,254 @@
# 前提
服务注册与发现最简单的就是`direct`固定服务端地址的直连方式。也就是服务端正常监听端口启动不进行额外操作,客户端使用如下`target`
```url
direct://default/127.0.0.1:9000,127.0.0.1:9091
```
> `target`就是标准的`URL`资源定位符[查看WIKI](https://zh.wikipedia.org/wiki/%E7%BB%9F%E4%B8%80%E8%B5%84%E6%BA%90%E5%AE%9A%E4%BD%8D%E7%AC%A6)
其中`direct`为协议类型,此处表示直接使用该`URL`内提供的地址`127.0.0.1:9000,127.0.0.1:9091`进行连接,而`default`在此处无意义仅当做占位符。
# gRPC Resolver
gRPC暴露了服务发现的接口`resolver.Builder``resolver.ClientConn``resolver.Resolver`,[官方代码位置](https://github.com/grpc/grpc-go/blob/master/resolver/resolver.go):
```go
// Builder creates a resolver that will be used to watch name resolution updates.
type Builder interface {
// Build creates a new resolver for the given target.
//
// gRPC dial calls Build synchronously, and fails if the returned error is
// not nil.
Build(target Target, cc ClientConn, opts BuildOption) (Resolver, error)
// Scheme returns the scheme supported by this resolver.
// Scheme is defined at https://github.com/grpc/grpc/blob/master/doc/naming.md.
Scheme() string
}
// ClientConn contains the callbacks for resolver to notify any updates
// to the gRPC ClientConn.
//
// This interface is to be implemented by gRPC. Users should not need a
// brand new implementation of this interface. For the situations like
// testing, the new implementation should embed this interface. This allows
// gRPC to add new methods to this interface.
type ClientConn interface {
// UpdateState updates the state of the ClientConn appropriately.
UpdateState(State)
// NewAddress is called by resolver to notify ClientConn a new list
// of resolved addresses.
// The address list should be the complete list of resolved addresses.
//
// Deprecated: Use UpdateState instead.
NewAddress(addresses []Address)
// NewServiceConfig is called by resolver to notify ClientConn a new
// service config. The service config should be provided as a json string.
//
// Deprecated: Use UpdateState instead.
NewServiceConfig(serviceConfig string)
}
// Resolver watches for the updates on the specified target.
// Updates include address updates and service config updates.
type Resolver interface {
// ResolveNow will be called by gRPC to try to resolve the target name
// again. It's just a hint, resolver can ignore this if it's not necessary.
//
// It could be called multiple times concurrently.
ResolveNow(ResolveNowOption)
// Close closes the resolver.
Close()
}
```
下面依次分析这三个接口的作用:
* `Builder`用于gRPC内部创建`Resolver`接口的实现,但注意声明的`Build`方法将接口`ClientConn`作为参数传入了
* `ClientConn`接口有两个废弃方法不用管,看`UpdateState`方法需要传入`State`结构,看代码可以发现其中包含了`Addresses []Address // Resolved addresses for the target`,可以看出是需要将服务发现得到的`Address`对象列表告诉`ClientConn`的对象
* `Resolver`提供了`ResolveNow`用于被gRPC尝试重新进行服务发现
看完这三个接口就可以明白gRPC的服务发现实现逻辑,通过`Builder`进行`Reslover`的创建,在`Build`的过程中将服务发现的地址信息丢给`ClientConn`用于内部连接创建等逻辑。主要逻辑可以按下面顺序来看源码理解:
*`client``Dial`时会根据`target`解析的`scheme`获取对应的`Builder`,[官方代码位置](https://github.com/grpc/grpc-go/blob/master/clientconn.go#L242)
*`Dial`成功会创建出结构体`ClientConn`的对象[官方代码位置](https://github.com/grpc/grpc-go/blob/master/clientconn.go#L447)(注意不是上面的`ClientConn`接口),可以看到结构体`ClientConn`内的成员`resolverWrapper`又实现了接口`ClientConn`的方法[官方代码位置](https://github.com/grpc/grpc-go/blob/master/resolver_conn_wrapper.go)
*`resolverWrapper`被初始化时就会调用`Build`方法[官方代码位置](https://github.com/grpc/grpc-go/blob/master/resolver_conn_wrapper.go#L89),其中参数为接口`ClientConn`传入的是`ccResolverWrapper`
* 当用户基于`Builder`的实现进行`UpdateState`调用时,则会触发结构体`ClientConn``updateResolverState`方法[官方代码位置](https://github.com/grpc/grpc-go/blob/master/resolver_conn_wrapper.go#L109),`updateResolverState`则会对传入的`Address`进行初始化等逻辑[官方代码位置](https://github.com/grpc/grpc-go/blob/master/clientconn.go#L553)
如此整个服务发现过程就结束了。从中也可以看出gRPC官方提供的三个接口还是很灵活的,但也正因为灵活要实现稍微麻烦一些,而`Address`[官方代码位置](https://github.com/grpc/grpc-go/blob/master/resolver/resolver.go#L79)如果直接被业务拿来用于服务节点信息的描述结构则显得有些过于简单。
所以`warden`包装了gRPC的整个服务发现实现逻辑,代码分别位于`pkg/naming/naming.go``warden/resolver/resolver.go`,其中:
* `naming.go`内定义了用于描述业务实例的`Instance`结构、用于服务注册的`Registry`接口、用于服务发现的`Resolver`接口
* `resolver.go`内实现了gRPC官方的`resolver.Builder``resolver.Resolver`接口,但也暴露了`naming.go`内的`naming.Builder``naming.Resolver`接口
# warden Resolver
接下来看`naming`内的接口如下:
```go
// Resolver resolve naming service
type Resolver interface {
Fetch(context.Context) (*InstancesInfo, bool)
Watch() <-chan struct{}
Close() error
}
// Builder resolver builder.
type Builder interface {
Build(id string) Resolver
Scheme() string
}
```
可以看到封装方式与gRPC官方的方法一样,通过`Builder`进行`Resolver`的初始化。不同的是通过封装将参数进行了简化:
* `Build`只需要传对应的服务`id`即可:`warden/resolver/resolver.go`在gRPC进行调用后,会根据`Scheme`方法查询对应的`naming.Builder`实现并调用`Build``id`传入,而`naming.Resolver`的实现即可通过`id`去对应的服务发现中间件进行实例信息的查询
*`Resolver`则对方法进行了扩展,除了简单进行`Fetch`操作外还多了`Watch`方法,用于监听服务发现中间件的节点变化情况,从而能够实时的进行服务实例信息的更新
`naming/discovery`内实现了基于[discovery](https://github.com/bilibili/discovery)为中间件的服务注册与发现逻辑。如果要实现其他中间件如`etcd`|`zookeeper`等的逻辑,参考`naming/discovery/discovery.go`内的逻辑,将与`discovery`的交互逻辑替换掉即可(后续会默认将etcd/zk等实现,敬请期待)。
# 使用discovery
因为`warden`内默认使用`direct`的方式,所以要使用[discovery](https://github.com/bilibili/discovery)需要在业务的`NewClient`前进行注册,代码如下:
```go
package dao
import (
"context"
"github.com/go-kratos/kratos/pkg/naming/discovery"
"github.com/go-kratos/kratos/pkg/net/rpc/warden"
"github.com/go-kratos/kratos/pkg/net/rpc/warden/resolver"
"google.golang.org/grpc"
)
// AppID your appid, ensure unique.
const AppID = "demo.service" // NOTE: example
func init(){
// NOTE: 注意这段代码,表示要使用discovery进行服务发现
// NOTE: 还需注意的是,resolver.Register是全局生效的,所以建议该代码放在进程初始化的时候执行
// NOTE: !!!切记不要在一个进程内进行多个不同中间件的Register!!!
// NOTE: 在启动应用时,可以通过flag(-discovery.nodes) 或者 环境配置(DISCOVERY_NODES)指定discovery节点
resolver.Register(discovery.Builder())
}
// NewClient new member grpc client
func NewClient(cfg *warden.ClientConfig, opts ...grpc.DialOption) (DemoClient, error) {
client := warden.NewClient(cfg, opts...)
conn, err := client.Dial(context.Background(), "discovery://default/"+AppID)
if err != nil {
return nil, err
}
// 注意替换这里:
// NewDemoClient方法是在"api"目录下代码生成的
// 对应proto文件内自定义的service名字,请使用正确方法名替换
return NewDemoClient(conn), nil
}
```
> 注意:`resolver.Register`是全局行为,建议放在包加载阶段或main方法开始时执行,该方法执行后会在gRPC内注册构造方法
`target``discovery://default/${appid}`,当gRPC内进行解析后会得到`scheme`=`discovery``appid`,然后进行以下逻辑:
1. `warden/resolver.Builder`会通过`scheme`获取到`naming/discovery.Builder`对象(靠`resolver.Register`注册过的)
2. 拿到`naming/discovery.Builder`后执行`Build(appid)`构造`naming/discovery.Discovery`
3. `naming/discovery.Discovery`对象基于`appid`就知道要获取哪个服务的实例信息
# 服务注册
客户端既然使用了[discovery](https://github.com/bilibili/discovery)进行服务发现,也就意味着服务端启动后必须将自己注册给[discovery](https://github.com/bilibili/discovery)知道。
相对服务发现来讲,服务注册则简单很多,看`naming/discovery/discovery.go`内的代码实现了`naming/naming.go`内的`Registry`接口,服务端启动时可以参考下面代码进行注册:
```go
// 该代码可放在main.go,当warden server进行初始化之后
// 省略...
ip := "" // NOTE: 必须拿到您实例节点的真实IP,
port := "" // NOTE: 必须拿到您实例grpc监听的真实端口,warden默认监听9000
hn, _ := os.Hostname()
dis := discovery.New(nil)
ins := &naming.Instance{
Zone: env.Zone,
Env: env.DeployEnv,
AppID: "your app id",
Hostname: hn,
Addrs: []string{
"grpc://" + ip + ":" + port,
},
}
cancel, err := dis.Register(context.Background(), ins)
if err != nil {
panic(err)
}
// 省略...
// 特别注意!!!
// cancel必须在进程退出时执行!!!
cancel()
```
# 使用ETCD
和使用discovery类似,只需要在注册时使用etcd naming即可。
```go
package dao
import (
"context"
"github.com/go-kratos/kratos/pkg/naming/etcd"
"github.com/go-kratos/kratos/pkg/net/rpc/warden"
"github.com/go-kratos/kratos/pkg/net/rpc/warden/resolver"
"google.golang.org/grpc"
)
// AppID your appid, ensure unique.
const AppID = "demo.service" // NOTE: example
func init(){
// NOTE: 注意这段代码,表示要使用etcd进行服务发现 ,其他事项参考discovery的说明
// NOTE: 在启动应用时,可以通过flag(-etcd.endpoints) 或者 环境配置(ETCD_ENDPOINTS)指定etcd节点
// NOTE: 如果需要自己指定配置时 需要同时设置DialTimeout 与 DialOptions: []grpc.DialOption{grpc.WithBlock()}
resolver.Register(etcd.Builder(nil))
}
// NewClient new member grpc client
func NewClient(cfg *warden.ClientConfig, opts ...grpc.DialOption) (DemoClient, error) {
client := warden.NewClient(cfg, opts...)
// 这里使用etcd scheme
conn, err := client.Dial(context.Background(), "etcd://default/"+AppID)
if err != nil {
return nil, err
}
// 注意替换这里:
// NewDemoClient方法是在"api"目录下代码生成的
// 对应proto文件内自定义的service名字,请使用正确方法名替换
return NewDemoClient(conn), nil
}
```
etcd的服务注册与discovery基本相同,可以传入详细的etcd配置项, 或者传入nil后通过flag(-etcd.endpoints)/环境配置(ETCD_ENDPOINTS)来指定etcd节点。
### 其他配置项
etcd默认的全局keyPrefix为kratos_etcd,当该keyPrefix与项目中其他keyPrefix冲突时可以通过flag(-etcd.prefix)或者环境配置(ETCD_PREFIX)来指定keyPrefix。
# 扩展阅读
[warden快速开始](warden-quickstart.md)
[warden拦截器](warden-mid.md)
[warden基于pb生成](warden-pb.md)
[warden负载均衡](warden-balancer.md)
+41
View File
@@ -0,0 +1,41 @@
# 背景
我们需要统一的rpc服务,经过选型讨论决定直接使用成熟的跨语言的gRPC。
# 概览
* 不改gRPC源码,基于接口进行包装集成trace、log、prom等组件
* 打通自有服务注册发现系统[discovery](https://github.com/bilibili/discovery)
* 实现更平滑可靠的负载均衡算法
# 拦截器
gRPC暴露了两个拦截器接口,分别是:
* `grpc.UnaryServerInterceptor`服务端拦截器
* `grpc.UnaryClientInterceptor`客户端拦截器
基于两个拦截器可以针对性的定制公共模块的封装代码,比如`warden/logging.go`是通用日志逻辑。
[warden拦截器](warden-mid.md)
# 服务发现
`warden`默认使用`direct`方式直连,正常线上都会使用第三方服务注册与发现中间件,`warden`内包含了[discovery](https://github.com/bilibili/discovery)的逻辑实现,想使用如`etcd``zookeeper`等也可以,都请看下面文档。
[warden服务发现](warden-resolver.md)
# 负载均衡
实现了`wrr``p2c`两种算法,默认使用`p2c`
[warden负载均衡](warden-balancer.md)
# 扩展阅读
[warden快速开始](warden-quickstart.md)
[warden拦截器](warden-mid.md)
[warden负载均衡](warden-balancer.md)
[warden基于pb生成](warden-pb.md)
[warden服务发现](warden-resolver.md)