1 Star 1 Fork 0

guanzhanyi / blog

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
该仓库未声明开源许可证文件(LICENSE),使用请关注具体项目描述及其代码上游依赖。
克隆/下载
README.md 12.41 KB
一键复制 编辑 原始数据 按行查看 历史
guanzhanyi 提交于 2021-07-15 14:00 . 修改README.md

go-kit微服务实践

项目描述

keypoints

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(&reg)
    if err != nil {
        log.Fatal(err)
    }
}
//优雅退出consul
  • 结果

consul.png

consul_service.png

hystrix
服务熔断处理

DoGo类似, 不过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

理解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的撤销权。

Go
1
https://gitee.com/guanzhanyi/blog.git
git@gitee.com:guanzhanyi/blog.git
guanzhanyi
blog
blog
master

搜索帮助