kratos中使用etcdWatch介绍

项目地址

https://gitee.com/huoyingwhw/kratos_etcd_watch

实际中的一个问题

如果实际中我们将代码部署到3台机器上做了负载均衡,或者项目在k8s上有多个pod,而项目中又使用了全局变量去优化查询效率,这个全局变量可能会修改,如果使用接口修改的话,由于项目部署到了不同机器上,一个请求只会修改某台机器上的变量,其他几台机器中这个全局变量并不会修改。

此时我们需要借助ETCD,将这个全局变量对应的数据放到ETCD中,同时借助ETCD的watch机制,3台机器同时watch ETCD的同一个key,修改的接口只去修改ETCD中对应key的数据,这个时候3台机器发现ETCD的配置变了的话都会修改自己本地的全局变量的数据。

使用原生的etcdAPI的测试 

package tests

import (
    "context"
    "fmt"
    "github.com/stretchr/testify/require"
    clientv3 "go.etcd.io/etcd/client/v3"
    "testing"
    "time"
)

// go操作etcd参考:https://www.kancloud.cn/golang_programe/golang/1172700#goetcd_2
func TestCURD1(t *testing.T) {

    // 创建一个client
    cli, err := clientv3.New(clientv3.Config{
        Endpoints:   []string{"localhost:2379"},
        DialTimeout: 5 * time.Second,
    })

    require.Equal(t, err, nil)

    defer cli.Close()

    // 带超时时间的上下文
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    // 1、写入数据:key 没有就新增,有就覆盖
    // 设置key="/class/1" 的值为 三年二班
    // Notice 如果你想要知道key在改变之前的数据, 请设置 clientv3.WithPrevKV() 选项
    putResp, err := cli.Put(ctx, "/class/3", "一年五班", clientv3.WithPrevKV())
    require.Equal(t, err, nil)
    // 打印修改之前的值
    if putResp.PrevKv != nil {
        fmt.Printf("之前的值: Key: %v, Value: %v \n", string(putResp.PrevKv.Key), string(putResp.PrevKv.Value))
    }

    // 2-1、查询数据
    resp, err := cli.Get(ctx, "/class/1")
    require.Equal(t, err, nil)
    for _, ev := range resp.Kvs {
        // Notice 注意,返回的是byte类型的!需要转成string!
        fmt.Printf("单独查询的结果: Key: %v, Value: %v \n", string(ev.Key), string(ev.Value))
    }

    // 2-2、前缀匹配去查询数据
    // Notice 如果你需要监视一个key前缀, 请设置 clientv3.WithPrefix() 选项
    // 查询以 /class 为前缀的所有值
    res, err := cli.Get(ctx, "/class", clientv3.WithPrefix())
    require.Equal(t, err, nil)
    for _, ev := range res.Kvs {
        fmt.Printf("基于前缀查询的结果: Key: %v, Value: %v \n", string(ev.Key), string(ev.Value))
    }

    // 3-1、删除数据
    //_, err = cli. Delete(ctx, "/class/1")
    //require.Equal(t, err, nil)

    // 3-2、前缀匹配删除数据
    // 批量删除以 /class 为前缀的值
    //_, err = cli.Delete(ctx, "/class", clientv3.WithPrefix())
    //require.Equal(t, err, nil)

}
原生etcdAPI的增删改查
package main

import (
    "context"
    "fmt"
    "github.com/go-kratos/kratos/v2/log"
    "time"

    "go.etcd.io/etcd/client/v3"
)

func main() {
    // 创建一个client
    cli, err := clientv3.New(clientv3.Config{
        Endpoints:   []string{"localhost:2379"},
        DialTimeout: 5 * time.Second,
    })
    if err != nil {
        log.Fatal(err)
    }

    defer cli.Close()

    // 创建一个watcher
    watcher := clientv3.NewWatcher(cli)
    defer watcher.Close()

    // 设置watch参数,这里设置为监视 以/class为前缀的所有key的变化
    watchStream := watcher.Watch(context.Background(), "/class", clientv3.WithPrefix())

    // 循环监视key的变化
    for resp := range watchStream {
        for _, ev := range resp.Events {
            fmt.Printf("%s %q : %q\n", ev.Type, ev.Kv.Key, ev.Kv.Value)
            switch ev.Type {
            case clientv3.EventTypePut:
                fmt.Printf("Key %s created with value %s\n", ev.Kv.Key, ev.Kv.Value)
            case clientv3.EventTypeDelete:
                fmt.Printf("Key %s deleted\n", ev.Kv.Key)
                // Handle other event types if needed
            }

        }
    }
}
原生etcdAPI实现的watch

使用kratos封装的etcdAPI的测试

package main

import (
    "context"
    "fmt"
    "github.com/go-kratos/kratos/v2/log"
    "time"

    "go.etcd.io/etcd/client/v3"
)

func main() {
    // 创建一个client
    cli, err := clientv3.New(clientv3.Config{
        Endpoints:   []string{"localhost:2379"},
        DialTimeout: 5 * time.Second,
    })
    if err != nil {
        log.Fatal(err)
    }

    defer cli.Close()

    // 创建一个watcher
    watcher := clientv3.NewWatcher(cli)
    defer watcher.Close()

    // 设置watch参数,这里设置为监视 以/class为前缀的所有key的变化
    watchStream := watcher.Watch(context.Background(), "/class", clientv3.WithPrefix())

    // 循环监视key的变化
    for resp := range watchStream {
        for _, ev := range resp.Events {
            fmt.Printf("%s %q : %q\n", ev.Type, ev.Kv.Key, ev.Kv.Value)
            switch ev.Type {
            case clientv3.EventTypePut:
                fmt.Printf("Key %s created with value %s\n", ev.Kv.Key, ev.Kv.Value)
            case clientv3.EventTypeDelete:
                fmt.Printf("Key %s deleted\n", ev.Kv.Key)
                // Handle other event types if needed
            }

        }
    }
}
kratos封装的etcdAPI测试
package main

import (
    "fmt"
    cfg "github.com/go-kratos/kratos/contrib/config/etcd/v2"
    clientv3 "go.etcd.io/etcd/client/v3"
    "google.golang.org/grpc"
    "log"
    "time"
)

const testKey = "/kratos/test/config"

func main() {

    // new client
    client, err := clientv3.New(clientv3.Config{
        Endpoints:   []string{"127.0.0.1:2379"},
        DialTimeout: time.Second,
        DialOptions: []grpc.DialOption{grpc.WithBlock()},
    })
    if err != nil {
        log.Fatal(err)
    }
    defer func() {
        _ = client.Close()
    }()

    // new config
    source, err := cfg.New(client, cfg.WithPath(testKey))
    if err != nil {
        log.Fatal(err)
    }

    // put
    //if _, err = client.Put(context.Background(), testKey, "test config"); err != nil {
    //    log.Fatal(err)
    //}

    // load
    //kvs, err := source.Load()
    //if err != nil {
    //    log.Fatal(err)
    //}

    //if len(kvs) != 1 || kvs[0].Key != testKey || string(kvs[0].Value) != "test config" {
    //    log.Fatal("config error")
    //}

    // Notice watch
    w, err := source.Watch()
    if err != nil {
        log.Fatal(err)
    }
    defer func() {
        _ = w.Stop()
    }()

    // 死循环监听~
    for {
        kvs, err := w.Next()
        if err != nil {
            log.Fatal(err)
        }
        fmt.Println(">>> ", kvs[0].Key, string(kvs[0].Value))
    }

}
kratos封装的etcdAPI实现的watch

注意防止goroutine泄漏

使用kratos封装好的API需要自己手动监听ctx.Done()

  • 在biz/etcdWatchKratos.go中开启新的协程去watch,需要注意如果项目停掉了要cancel掉etcdWatch的ctx不让子协程继续watch了!
  • etcdWatch使用自己的ctx,注意在biz层New的时候使用自己的ctx,并且需要返回一个cleanFunc,当程序结束的时候cancel掉ctx,select语句中return nil ,子协程中的死循环退出,保证不再继续Watch了

实际中项目停止的时候出现这个提示就表示watch的子协程停了:

使用原生的那个API不需要手动监听ctx.Done()

  • 在biz/etcdWatchOrigin.go中,e.watcher.Watch方法内部自己维护了ctx,如果项目停掉的话在封装好的方法内部会自动捕获到ctx.Done(),监听的协程也已经停止了
posted on 2023-06-07 14:18  江湖乄夜雨  阅读(157)  评论(0编辑  收藏  举报