项目地址:https://github.com/go-kratos/kratos
项目致力于提供完整的微服务研发体验,整合相关框架及工具后,微服务治理相关部分可对整体业务开发周期无感,从而更加聚焦于业务交付。对每位开发者而言,整套Kratos框架也是不错的学习仓库,可以了解和参考到bilibili在微服务方面的技术积累和经验。
Requirments
Go version>=1.13
Installation
GO111MODULE=on && go get -u github.com/go-kratos/kratos/tool/kratos
cd $GOPATH/src
kratos new kratos-demo
通过 kratos new 会快速生成基于kratos库的脚手架代码,如生成 kratos-demo
Build & Run
cd kratos-demo/cmd
go build
./cmd -conf ../configs
打开浏览器访问:http://localhost:8000/kratos-demo/start,你会看到输出了Golang 大法好 !!!
项目结构
├── api
│ ├── api.bm.go
│ ├── api.pb.go
│ ├── api.proto
│ └── client.go
├── CHANGELOG.md
├── cmd
│ └── main.go
├── configs
│ ├── application.toml
│ ├── db.toml
│ ├── grpc.toml
│ ├── http.toml
│ ├── memcache.toml
│ └── redis.toml
├── internal
│ ├── dao
│ │ ├── dao.bts.go
│ │ ├── dao.go
│ │ ├── dao_test.go
│ │ ├── db.go
│ │ ├── mc.cache.go
│ │ ├── mc.go
│ │ ├── redis.go
│ │ ├── wire_gen.go
│ │ └── wire.go
│ ├── di
│ │ ├── app.go
│ │ ├── wire_gen.go
│ │ └── wire.go
│ ├── model
│ │ └── model.go
│ ├── server
│ │ ├── grpc
│ │ │ └── server.go
│ │ └── http
│ │ └── server.go
│ └── service
│ └── service.go
├── OWNERS
├── README.md
└── test
├── 0_db.sql
├── 1_data.sql
├── application.toml
├── db.toml
├── docker-compose.yaml
├── grpc.toml
├── http.toml
├── memcache.toml
└── redis.toml
参考:
在test中docker-compose.yaml可在本机创建mysql,redis, memcached 数据库
docker-compose up -d
在像微服务这样的分布式架构中,经常会有一些需求需要你调用多个服务,但是还需要确保服务的安全性、统一化每次的请求日志或者追踪用户完整的行为等等。要实现这些功能,你可能需要在所有服务中都设置一些相同的属性,虽然这个可以通过一些明确的接入文档来描述或者准入规范来界定,但是这么做的话还是有可能会有一些问题:
为了解决这样的问题,你可能需要一个框架来帮助你实现这些功能。比如说帮你在一些关键路径的请求上配置必要的鉴权或超时策略。那样服务间的调用会被多层中间件所过滤并检查,确保整体服务的稳定性
性能优异,不应该掺杂太多业务逻辑的成分
方便开发使用,开发对接的成本应该尽可能地小
后续鉴权、认证等业务逻辑的模块应该可以通过业务模块的开发接入该框架内
默认配置已经是 production ready 的配置,减少开发与线上环境的差异性
参考gin设计整套HTTP框架,去除gin中不需要的部分逻辑
内置一些必要的中间件,便于业务方可以直接上手使用
blademaster由几个非常精简的内部模块组成。其中Router用于根据请求的路径分发请求,Context包含了一个完整的请求信息,Handler则负责处理传入的Context,Handlers为一个列表,一个串一个地执行。
所有的middlerware均以Handler的形式存在,这样可以保证blademaster自身足够精简且扩展性足够强。
blademaster处理请求的模式非常简单,大部分的逻辑都被封装在了各种Handler中。一般而言,业务逻辑作为最后一个Handler。
正常情况下每个Handler按照顺序一个一个串行地执行下去,但是Handler中也可以中断整个处理流程,直接输出Response。这种模式常被用于校验登陆的middleware中:一旦发现请求不合法,直接响应拒绝。
请求处理的流程中也可以使用Render来辅助渲染Response,比如对于不同的请求需要响应不同的数据格式JSON、XML,此时可以使用不同的Render来简化逻辑。
warden模块使用
lag | 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 | - | - | 指定服务运行:grpc.target=demo.service=127.0.0.1:9000 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 | 日志级别: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: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实时更新。
ecode
背景
错误码一般被用来进行异常传递,且需要具有携带message文案信息的能力。
在kratos里,错误码被设计成Codes接口,声明如下代码位置:
// 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结构体实现了该接口。
一个Code错误码可以对应一个message,默认实现会从全局变量_messages中获取,业务可以将自定义Code对应的message通过调用Register方法的方式传递进去,如:
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接口为gRPC预留,gRPC传递异常会将服务端的错误码pb序列化之后赋值给Details,客户端拿到之后反序列化得到,具体可阅读status的实现:
ecode包内的Status结构体实现了Codes接口代码位置
warden/internal/status包内包装了ecode.Status和grpc.Status进行互相转换的方法代码位置
warden的client和server则使用转换方法将gRPC底层返回的error最终转换为ecode.Status 代码位置
转换为ecode
错误码转换有以下两种情况:
因为框架传递错误是靠ecode错误码,比如bm框架返回的code字段默认就是数字,那么客户端接收到如{“code”:-404}的话,可以使用ec := ecode.Int(-404)或ec := ecode.String("-404")来进行转换。
在项目中dao层返回一个错误码,往往返回参数类型建议为error而不是ecode.Codes,因为error更通用,那么上层service就可以使用ec := ecode.Cause(err)进行转换。
判断
错误码判断是否相等:
ecode与ecode判断使用:ecode.Equal(ec1, ec2)
ecode与error判断使用:ecode.EqualError(ec, err)
使用工具生成
使用proto协议定义错误码,格式如下:
// user.proto
syntax = "proto3";
package ecode;
enum UserErrCode {
UserUndefined = 0; // 因protobuf协议限制必须存在!!!无意义的0,工具生成代码时会忽略该参数
UserNotLogin = 123; // 正式错误码
}
需要注意以下几点:
必须是enum类型,且名字规范必须以"ErrCode"结尾,如:UserErrCode
因为protobuf协议限制,第一个enum值必须为无意义的0
使用kratos tool protoc --ecode user.proto进行生成,生成如下代码:
package ecode
import (
"github.com/go-kratos/kratos/pkg/ecode"
)
var _ ecode.Codes
// UserErrCode
var (
UserNotLogin = ecode.New(123);
)
在项目中加入errors文件夹,创建userErrors.go 文件
package cms
import "github.com/go-kratos/kratos/pkg/ecode"
var (
CmsError = ecode.New(10000)
)
func init() {
cms := map[int]string{
10000: "自定义错误",
}
ecode.Register(cms)
}
在server/http/server.go 中加入
g.GET("/code", testcode)
func testcode(c *bm.Context) {
c.JSON(nil, cms.CmsError)
}
访问http://127.0.0.1:9056/kratos-demo/code
{"code":10000,"message":"自定义错误","ttl":1}
基于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 | 过虑敏感信息 |
使用方式
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")
}
kratos 借鉴了 Sentinel 项目的自适应限流系统,通过综合分析服务的 cpu 使用率、请求成功的 qps 和请求成功的 rt 来做自适应限流保护。
自动嗅探负载和 qps,减少人工配置
削顶,保证超载时系统不被拖垮,并能以高水位 qps 继续运行
限流规则
指标介绍
指标名称 指标含义
cpu 最近 1s 的 CPU 使用率均值,使用滑动平均计算,采样周期是 250ms
inflight 当前处理中正在处理的请求数量
pass 请求处理成功的量
rt 请求成功的响应耗时
在自适应限流保护中,采集到的指标的时效性非常强,系统只需要采集最近一小段时间内的 qps、rt 即可,对于较老的数据,会自动丢弃。为了实现这个效果,kratos 使用了滑动窗口来保存采样数据。
如上图,展示了一个具有两个桶(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。