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) }
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 ( "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 } } } }
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)) } }
注意防止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(),监听的协程也已经停止了