# blog **Repository Path**: guanzhanyi/blog ## Basic Information - **Project Name**: blog - **Description**: 基于go-kit的微服务demo - **Primary Language**: Go - **License**: Not specified - **Default Branch**: master - **Homepage**: https://gitee.com/guanzhanyi - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 1 - **Created**: 2021-06-08 - **Last Updated**: 2024-08-09 ## Categories & Tags **Categories**: Uncategorized **Tags**: Go语言, 微服务, go-kit ## README # go-kit微服务实践 ## 项目描述 ### ## keypoints * [HTTP服务——go-kit](#go-kit) * [微服务调用——grpc](#grpc) * [身份认证——JWT](#JWT) * [限流——golang/rate](#rate) * [服务发现和注册——consul](#consul) * [服务熔断和降级——hystrix-go](#hystrix) * [配置文件管理——go-ini/ini](#ini) * [表单验证——validation](#validation) * [注释自动生成——Swagger](#Swagger) * [链路追踪——Jaeger](#Jaeger) * [X][网关管理]
go-kit
* 分为三层: service: 定义业务逻辑 endpoint: 调用service处理请求和返回响应 transport: 连接endpoint的request和response
grpc
* 安装 编译安装protobuf 安装go-proto `go get -u github.com/golang/protobuf/proto` 安装protoc `go get -u github.com/golang/protobuf/protoc-gen-go` * 使用 编写protobuf文件 ``` syntax = "proto3"; // package pb; //生成的go文件路径 option go_package = "./"; //函数 service UserService{ rpc CheckPassword(LoginRequest) returns (LoginResponse){} } //结构体 message LoginRequest{ string Username = 1; string Passowrd = 2; } ``` 生成pb.go文件 `$ protoc --go_out=plugins=grpc:. *.proto`
JWT
``` //生成token //初始化与token有关的数据结构: 主要有jwt库提供的标准声明和自定义的数据. 标准声明有过期时间, 创建时间,发行商和主题. 自定义的数据一般有鲜明且唯一的用户特征, 比如用户名 type Claims struct{ Username string `json:"username"` jwt.StandardClaims } claims:=Claims{ username, jwt.StandardClaims{ ExpiresAt: expireTime.Unix(),//过期时间 IssuedAt: time.Now().Unix(), Issuer: "gyp",//发行商 Subject: "user token",//主题 }, } //生成加密后的token tokenClaims := jwt.NewWithClaims(jwt.SigningMethodHS256,claims) //加上签名, 认证数据来源 token,err:=tokenClaims.SignedString(jwtSecret) ``` ``` //验证token //把token解密 tokenClaims, err := jwt.ParseWithClaims(token, &Claims{}, func(token *jwt.Token) (interface{}, error) { return jwtSecret, nil }) //验证解密后的token是否是Claims结构体 if tokenClaims != nil { if claims, ok := tokenClaims.Claims.(*Claims); ok && tokenClaims.Valid { return claims, nil } } ``` 使用了中间件的方式嵌入到gin中, JWT()函数返回gin.HandlerFunc, 在路由中通过Use使用 ``` func JWT() gin.HandlerFunc{ //gin.Context是gin 中最重要的部分, 用于传递变量, 管理流程, 验证json return func(c *gin.Context){ //调用generateToken()和ParseToken() ... } } apiv1:=r.Group("/api/v1") apiv1.Use(jwt.JWT()) ```
rate
``` r := rate.NewLimiter(1, 5) //1表示每次放进筒内的数量,桶内的令牌数是5,最大令牌数也是5,这个筒子是自动补充的,你只要取了令牌不管你取多少个,这里都会在每次取完后自动加1个进来,因为我们设置的是1 ctx := context.Background() r.waitN(ctx,2) //每次消耗两个 wait() Allow() r.AllowN(time.Now(), 2) { //AllowN表示取当前的时间,这里是一次取2个,如果当前不够取两个了,本次就不取,再放一个进去,然后返回false //Allow 无可用token则返回false //Wait无可用token会阻塞住,直到获取一个token,或者超时或取消 Reserve() ReserveN() ```
consul
``` //安装consul $ docker pull consul ``` ``` // 启动consul // -server表示以服务端的方式启动 // -boostrap 指定自己为leader, 而不需要选举 // -ui 启动内置管理web页面 // -client指定客户端的ip, 0.0.0.0表示本地的都可以访问 // 8500是后台UI端口 // -node 节点在集群中的名称,在一个集群中必须是唯一的,默认是该节点的主机名 // -bootstrap-expect 在一个datacenter中期望提供的server节点数目,当该值提供的时候,consul一直等到达到指定sever数目的时候才会引导整个集群,该标记不能和bootstrap共用 $ docker run \ -d \ -p 8500:8500 \ -p 8600:8600/udp \ --name=badger \ consul agent -server -ui -node=server1 -bootstrap-expect=1 -client=0.0.0.0 ``` //注册可以手动通过json注册, 也可以通过函数注册 //健康检测机制是通过定时发送报文并且得到响应完成的 ``` //手动注册服务 $ vim imagecode.json { "ID":"imagecode", "Name":"imagecode", "Tags":{ "iHome" }, "Address":"106.52.19.86", "Port":8081, "Check":{ "HTTP":"http://106.52.19.86/api/v1/imagecode-health", "InterVal":"5s" } } ``` ``` //导入服务 $ curl --request PUT --data @imagecode.json localhost:8500/v1/agent/service/register //反注册服务 $ curl --request PUT --data @imagecode.json loservice/deregister/imagecode ``` ``` 通过函数导入和注册服务 package consul import ( consulapi "github.com/hashicorp/consul/api" "log" ) func RegService() { config := consulapi.DefaultConfig() config.Address = "39.107.76.100:8500" reg := consulapi.AgentServiceRegistration{} reg.Name = "userservice" //注册service的名字 reg.Address = "39.107.76.100" //注册service的ip reg.Port = 8080//注册service的端口 reg.Tags = []string{"primary"} check := consulapi.AgentServiceCheck{} //创建consul的检查器 check.Interval="5s" //设置consul心跳检查时间间隔 check.HTTP = "http://39.107.76.100:8080/health" //设置检查使用的url reg.Check = &check client, err := consulapi.NewClient(config) //创建客户端 if err != nil { log.Fatal(err) } err = client.Agent().ServiceRegister(®) if err != nil { log.Fatal(err) } } ``` ``` //优雅退出consul ``` * 结果 ![consul.png](https://i.loli.net/2021/07/15/urVawnX3d9GQY4K.png) ![consul_service.png](https://i.loli.net/2021/07/15/EjJng9ZF3UoPWKG.png)
hystrix
服务熔断处理 `Do`和`Go`类似, 不过Go是异步方式, Do是同步方式 Do有三个参数: Command 处理正常逻辑的函数 处理异常逻辑的函数 通过ConfigureCommand函数配置Command的参数 ``` hystrix.ConfigureCommand("wuqq", hystrix.CommandConfig{ Timeout: int(3 * time.Second),//超时时间 MaxConcurrentRequests: 10, //最大并发量 SleepWindow: 5000, //当熔断器被打开后,SleepWindow 的时间就是控制过多久后去尝试服务是否可用了 RequestVolumeThreshold: 10, //一个统计窗口10秒内请求数量。达到这个请求数量后才去判断是否要开启熔断 ErrorPercentThreshold: 30, //错误百分比,请求数量大于等于RequestVolumeThreshold并且错误率到达这个百分比后就会启动熔断 }) _ = hystrix.Do("wuqq", func() error { // talk to other services _, err := http.Get("https://www.baidu.com/") if err != nil { fmt.Println("get error:%v",err) return err } return nil }, func(err error) error { fmt.Printf("handle error:%v\n", err) return nil }) ```
ini
`"github.com/go-ini/ini"` //建一个*.ini文件 //文件格式由的领域[] RUN_MODE = debug [app] PAGE_SIZE = 10 JWT_SECRET = 23347$040412 [server] HTTP_PORT = 8000 READ_TIMEOUT = 60 //通过ini.Load()初始化 //通过GetSection()获取领域 //通过Key()获取值 //通过Mustxx()判断值类型和匹配
validation
``` github.com/astaxie/beego/validation valid := validation.Validation{} valid.Required(name,"name").Message("名字不能为空") valid.MaxSize(name, 100, "name").Message("名称最长为100字符") valid.Required(createdBy, "created_by").Message("创建人不能为空") valid.MaxSize(createdBy, 100, "created_by").Message("创建人最长为100字符") valid.Range(state, 0, 1, "state").Message("状态只允许0或1") valid.HasErrors() ```
Jaeger
tracer span 不太明白链路追踪中client的作用
Swagger
* 安装swagger ``` go get -u github.com/swaggo/swag/cmd/swag@v1.6.5 ``` * 安装 gin-swagger ``` $ go get -u github.com/swaggo/gin-swagger@v1.2.0 $ go get -u github.com/swaggo/files $ go get -u github.com/alecthomas/template ``` * 编写 API 注释 ``` // @Summary 新增文章标签 // @Produce json // @Param name query string true "Name" // @Param state query int false "State" // @Param created_by query int false "CreatedBy" // @Success 200 {string} json "{"code":200,"data":{},"msg":"ok"}" // @Router /api/v1/tags [post] func AddTag(c *gin.Context) { ``` * 针对 swagger 新增初始化动作和对应的路由规则 ``` r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) ``` * 生成swag文件 ``` //进入项目根目录 > swag init ``` * 访问网页 ``` http://127.0.0.1:8000/swagger/index.html ``` * 结果 ![swag.png](https://i.loli.net/2021/07/15/JQfk63czerHCG2m.png) ## 理解GO CONTEXT机制 context被译为上下文, 一般理解为程序单元的一个运行状态、现场、快照 每个Goroutine在执行之前,都要先知道程序当前的执行状态,通常将这些执行状态封装在一个Context变量中,传递给要执行的Goroutine中。 在网络编程下,当接收到一个网络请求Request,处理Request时,我们可能需要开启不同的Goroutine来获取数据与逻辑处理,即一个请求Request,会在多个Goroutine中处理。而这些Goroutine可能需要共享Request的一些信息;同时当Request被取消或者超时的时候,所有从这个Request创建的所有Goroutine也应该被结束。 ``` type Context interface { Deadline() (deadline time.Time, ok bool) Done() <-chan struct{} Err() error Value(key interface{}) interface{} } ``` Deadline会返回一个超时时间,Goroutine获得了超时时间后,例如可以对某些io操作设定超时时间。 Done方法返回一个信道(channel),当Context被撤销或过期时,该信道是关闭的,即它是一个表示Context是否已关闭的信号。 当Done信道关闭后,Err方法表明Context被撤的原因。 Value可以让Goroutine共享一些数据,当然获得数据是协程安全的。但使用这些数据的时候要注意同步,比如返回了一个map,而这个map的读写则要加锁。 ### 使用 要创建Context树,第一步就是要得到根节点,context.Background函数的返回值就是根节点: ``` func Background() Context ``` 该函数返回空的Context,该Context一般由接收请求的第一个Goroutine创建,是与进入请求对应的Context根节点,它不能被取消、没有值、也没有过期时间。它常常作为处理Request的顶层context存在。 有了根节点,又该怎么创建其它的子节点,孙节点呢?context包为我们提供了多个函数来创建他们: ``` func WithCancel(parent Context) (ctx Context, cancel CancelFunc) func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) func WithValue(parent Context, key interface{}, val interface{}) Context ``` 函数都接收一个Context类型的参数parent,并返回一个Context类型的值,这样就层层创建出不同的节点。子节点是从复制父节点得到的,并且根据接收参数设定子节点的一些状态值,接着就可以将子节点传递给下层的Goroutine了。 ### 总结 context包通过构建树型关系的Context,来达到上一层Goroutine能对传递给下一层Goroutine的控制。对于处理一个Request请求操作,需要采用context来层层控制Goroutine,以及传递一些变量来共享。 Context对象的生存周期一般仅为一个请求的处理周期。即针对一个请求创建一个Context变量(它为Context树结构的根);在请求处理结束后,撤销此ctx变量,释放资源。 每次创建一个Goroutine,要么将原有的Context传递给Goroutine,要么创建一个子Context并传递给Goroutine。 Context能灵活地存储不同类型、不同数目的值,并且使多个Goroutine安全地读写其中的值。 当通过父Context对象创建子Context对象时,可同时获得子Context的一个撤销函数,这样父Context对象的创建环境就获得了对子Context将要被传递到的Goroutine的撤销权。