使用etcd作服务发现前,你需要必读的一篇文章

前言

如果你想使用etcd作服务发现,千万不要上手就莽,一定要看下面几点:

0.

clientv3 不等于 etcd tag v3.0!

看到别人的示例,都是用的clientv3,就去傻愣愣的git clone github.com/etcd-io/etcd tag v3.0,然后发现,根本没有naming包的我只能傻了。

1. grpc版本

etcd强依赖grpc版本,不下对grpc的版本不行。

https://etcd.io/docs/v3.4.0/learning/design-client/ 可能需要梯子

中文简读:
https://blog.csdn.net/fwhezfwhez/article/details/109823449

2. 常变的api

etcd-client 里的常用api,经常被标记为弃用。弃用意味着逐渐放弃兼容和维护,使用错误的api,可能就达不到意向的效果。(grd的官方客户端包经常删改,各种找不到,标签都是坑人的)

We plan to improve these features based on the early feedback from the community, or abandon them if there is little interest, in the next few releases. Please do not rely on any experimental features or APIs in production environment.
请注意,任何你使用的被标记为Experimental的方法,都可能被丢弃,删除,不兼容,brabrabra, 笔者甚至遇见了整个包都不见了,直呼卧槽依赖不兼容。

参考:
https://etcd.io/docs/v3.4.0/dev-guide/experimental_apis/ 可能要梯子

3. 服务发现均衡不需要自己实现

如果你的内部服务网络,是grpc通信的,那么很高兴地告诉你,etcd,提供了官方的均衡器。参考链接:
https://etcd.io/docs/v3.3.13/dev-guide/grpc_naming/ 可能要梯子

import (
	"go.etcd.io/etcd/clientv3"
	etcdnaming "go.etcd.io/etcd/clientv3/naming"

	"google.golang.org/grpc"
)

...

cli, cerr := clientv3.NewFromURL("http://localhost:2379")
r := &etcdnaming.GRPCResolver{
    
    Client: cli}
b := grpc.RoundRobin(r)
conn, gerr := grpc.Dial("my-service", grpc.WithBalancer(b), grpc.WithBlock(), ...)

从这里获取到的conn连接,内部会自动包含活着的服务池,服务池里的连接,会通过etcd的key watch机制,保持变化。这种变化有一个特点:
续约周期内不灵敏,但是只要保持续约的服务节点够多,他就是高可用的。 怎么理解呢,假设app服务每10秒续约一次。a服务节点挂了,则10秒内,所有访达到这个a节点的请求,都会一直挂,但是,这时候他会自动轮训使用下个b节点,使得这个请求直到成功。(前提: grpc版本是1.23以上)

扫描二维码关注公众号,回复: 12762476 查看本文章

服务端需要需要定时续约:

go etcd.Register("x.x.x.x:port", "app_key", "y.y.y.y:port", 5)
package etcd

import (
	"context"
	"encoding/json"
	"go.uber.org/zap"
	"log"
	"strings"
	"time"

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

var cli *clientv3.Client

// Register register service with name as prefix to etcd, multi etcd addr should use ; to split
func Register(etcdAddr, name string, addr string, ttl int64) error {
    
    
	var err error

	if cli == nil {
    
    
		cli, err = clientv3.New(clientv3.Config{
    
    
			Endpoints:   strings.Split(etcdAddr, ";"),
			DialTimeout: 15 * time.Second,
			LogConfig: &zap.Config{
    
    
				Level:       zap.NewAtomicLevelAt(zap.ErrorLevel),
				Development: false,
				Sampling: &zap.SamplingConfig{
    
    
					Initial:    100,
					Thereafter: 100,
				},
				Encoding:      "json",
				EncoderConfig: zap.NewProductionEncoderConfig(),
				// Use "/dev/null" to discard all
				OutputPaths:      []string{
    
    "stderr"},
				ErrorOutputPaths: []string{
    
    "stderr"},
			},
		})
		if err != nil {
    
    
			return err
		}
	}

	service := Service{
    
    
		Addr: addr,
	}
	bts, err := json.Marshal(service)
	if err != nil {
    
    
		return err
	}

	serviceValue := string(bts)
	serviceKey := fmt.Sprintf("%s/%s", name, serviceValue)

	ticker := time.NewTicker(time.Second * time.Duration(ttl))

	go func() {
    
    
		for {
    
    
			getResp, err := cli.Get(context.Background(), serviceKey)
			if err != nil {
    
    
				log.Println(err)
			} else if getResp.Count == 0 {
    
    
				err = withAlive(serviceKey, serviceValue, ttl)
				if err != nil {
    
    
					log.Println(err)
				}
			} else {
    
    
				// do nothing
			}

			<-ticker.C
		}
	}()

	return nil
}

type Service struct {
    
    
	Addr string `json:"Addr"`
}

func withAlive(serviceKey string, serviceValue string, ttl int64) error {
    
    
	leaseResp, err := cli.Grant(context.Background(), ttl)
	if err != nil {
    
    
		return err
	}

	fmt.Printf("key:%v\n", serviceKey)
	_, err = cli.Put(context.Background(), serviceKey, serviceValue, clientv3.WithLease(leaseResp.ID))
	if err != nil {
    
    
		return err
	}

	ch, err := cli.KeepAlive(context.Background(), leaseResp.ID)
	if err != nil {
    
    
		log.Println(err)
		return err
	}

	// ch管道的值需要持续取出释放,否则会占用通道导致切片饱和
	go func() {
    
    
		for {
    
    
			_ = <-ch
		}
	}()

	return nil
}

// UnRegister remove service from etcd
func UnRegister(serviceKey string) {
    
    
	if cli != nil {
    
    
		cli.Delete(context.Background(), serviceKey)
	}
}

不过很遗憾,如果你想对http服务,tcp服务,websocket服务做服务发现,那么你可能需要手动实现一个均衡器(虽然这一点不难),会经过以下开发步骤(假定要将一个tcp内网服务集群接入etcd服务发现):

  • tcp服务里,使用clientv3,每5秒内向etcd集群续约1次,续约其实就是存入tcp服务的模块key以及host,端口
  • 调用端,使用clientv3,获取模块key对应的host端口列表。(本操作要每隔一段时间执行一次)
  • 调用端,拿到的host和ip池,要建立连接,并且自己维护 map[hostport]net.Conn 池。本池在每次第二步执行时,都要随时更新池的状态,比如拉取的列表少了某个key,就要将这个连接从池里删除,这里要加锁(只要你的服务节点不会老挂,续约正常,这个写锁的频率很小,大部分场景都是读,所以没有什么性能考虑)。
  • 最后,调用者,通过第三步的 map[hostport]Conn来和内部服务通信。

上服务器和etcd调试,使用etcdctl

一句话,千万要指定etcdctl的api版本,只有2和3,它对应的是grpc的版本(因为etcdctl和etcd是通过grpc通信的,而grpc2和grpc3是不兼容的)。
etcd 3.4 默认的etcdctl连接方式,是3, 这样获取和预设的键值,才是正常的。
etcd 3.3及以前的,默认是2.
PS: 大部分线上etcd版本都是3.0

这里有什么坑呢!

The API version used by etcdctl to speak to etcd may be set to version 2 or 3 via the ETCDCTL_API environment variable. By default, etcdctl on master (3.4) uses the v3 API and earlier versions (3.3 and earlier) default to the v2 API.

Note that any key that was created using the v2 API will not be able to be queried via the v2 API.

假设你的etcd是3.0的,这时候你用etcdctl没有指定api版本,那么这时候就是默认是2. 这里你创建的key,将无法被你看到,秀吗。

查看etcd和连接api的版本命令是:

$ etcdctl --version
etcdctl version: 3.1.0-alpha.0+git              # etcd的版本
API version: 3.1                                # grpc连接的版本

所以,每次连接上etcd服务器终端时,第一件事:

export ETCDCTL_API=3

当然,你也可以让这个环境变量永久生效~~~

etcd官方包里的红名pb文件生成的api去哪里看

etcd/api 里各种*pb都可以找到。

下面附上一些史诗级附录:

  • etcd安装, go连接, etcdctl连接与常见命令, 服务发现设计和使用
    https://blog.csdn.net/fwhezfwhez/article/details/94582071

  • etcd源码里,一些红名结构体里,有哪些字段,什么类型
    待续 , 只关注watch key变化和set,get。其它的业务中用的少,当然找的位置文章里会告诉你

猜你喜欢

转载自blog.csdn.net/fwhezfwhez/article/details/109823903