服务注册发现与kit实践

服务注册发现

在微服务的架构当中,服务发现是比较常见的一个方式来保证服务的动态上线下线的机制,可以通过封装一定的库来对某一个服务进行调用时无感知,动态管理当前提供服务的机制,这样同一个服务可以注册多个地址来提供服务,也可以对这些地址进行相应的负载均衡等操作,其中服务发现当前比较主流的一般会是zk、consul或者etcd来作为服务方来提供注册于发现服务。具体的过程抽象一下可以如下;

注册自己到etcd
通知调用方多出服务A
调用服务A服务
服务A
etcd
调用方
golang使用etcd来实现服务发现

etcd大家自行安装,测试情况下安装单机就足够使用,首先来编写provider的代码;

package main

import (
	"context"
	"fmt"
	"github.com/coreos/etcd/clientv3"
	"io/ioutil"
	"net/http"
	"sync"
	"time"
)

type Client struct {
	prefix string
	cli *clientv3.Client
	ctx context.Context
	kv clientv3.KV
	mu sync.Mutex
	entries []string
	selectIndex int
}

func NewClient(etcdAddrs []string, srcPath string, timeout int)*Client{
	cli, err := clientv3.New(clientv3.Config{
		Endpoints:   etcdAddrs,
		DialTimeout: time.Duration(timeout) * time.Second,
	})
	if err != nil {
		panic(err)
	}
	return &Client{
		prefix: srcPath,
		cli: cli,
		kv: cli.KV,
		ctx: context.Background(),
	}
}


func(c *Client) Watch(){
	for {
		rch := c.cli.Watch(context.Background(), c.prefix, clientv3.WithPrefix(), clientv3.WithRev(0))
		for wresp := range rch {
			for _, ev := range wresp.Events {
				fmt.Printf("%s %q : %q\n", ev.Type, ev.Kv.Key, ev.Kv.Value)
			}
			c.UpdateEvent()
		}
	}
}

func (c *Client) GetEntries(key string) ([]string, error) {

	resp, err := c.kv.Get(c.ctx, key, clientv3.WithPrefix())
	if err != nil {
		return nil, err
	}

	entries := make([]string, len(resp.Kvs))
	for i, kv := range resp.Kvs {
		entries[i] = string(kv.Value)
	}
	fmt.Println(entries)
	return entries, nil
}

func (c *Client) UpdateEvent(){
	c.mu.Lock()
	defer c.mu.Unlock()
	entries, err := c.GetEntries(c.prefix)
	if err != nil {
		fmt.Println("get entries error ", err)
		return
	}
	c.entries = entries
	c.selectIndex = 0
}

func (c *Client) GetServeice()string{
	c.mu.Lock()
	defer c.mu.Unlock()
	if len(c.entries) == 0{
		return ""
	}
	if c.selectIndex + 1 < len(c.entries){
		c.selectIndex += 1
		return c.entries[c.selectIndex]
	} else {
		c.selectIndex = 0
		return c.entries[c.selectIndex]
	}
}


func proxyMiddleware(f func (response http.ResponseWriter, r*http.Request))func (response http.ResponseWriter, r *http.Request){
	return func(response http.ResponseWriter, r *http.Request){
		// 选择服务并转发服务
		host := client.GetServeice()
		fmt.Println("chosse ", host)
		if host == ""{
			f(response, r)
			return
		}
		host = host + r.URL.Path
		resp, err := http.Get(host)
		if err != nil {
			fmt.Println(err)
			return
		}
		defer resp.Body.Close()

		body, err := ioutil.ReadAll(resp.Body)
		response.Write(body)
	}
}

func HelloServer(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Hello proxy service, %s!", r.URL.Path[1:])
}


var client *Client

func init(){
	client = NewClient([]string{"192.168.10.204:2379"}, "/server/serverA", 5)
}

func main() {
	go client.Watch()
	client.UpdateEvent()

	http.HandleFunc("/", proxyMiddleware(HelloServer))
	http.ListenAndServe("127.0.0.1:7999", nil)
}

provider的代码主要就是监听7999的端口,然后注册etcd监听对应的key,接着通过数据来获取服务列表的内容,将其转发到后端请求,然后将请求的数据返回给前端。

server端的代码如下;

package main

import (
	"flag"
	"os"
	"os/signal"
	"syscall"
	"time"

	"context"
	"fmt"
	"github.com/coreos/etcd/clientv3"
	"net/http"
)

var (
	listen = flag.String("listen", "127.0.0.1:8080", "HTTP listen address")
)

var clientService *clientv3.Client
var prefix string


func Register(cli *clientv3.Client){
	//设置1秒超时,访问etcd有超时控制
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	//操作etcd
	_, err := cli.Put(ctx, prefix, "http://"+*listen)
	//操作完毕,取消etcd
	cancel()
	if err != nil {
		fmt.Println("put failed, err:", err)
		return
	}
}

func Unregister(cli *clientv3.Client){
	//设置1秒超时,访问etcd有超时控制
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	//操作etcd
	_, err := cli.Delete(ctx, prefix)
	//操作完毕,取消etcd
	cancel()
	if err != nil {
		fmt.Println("put failed, err:", err)
		return
	}
}

func HelloServer(w http.ResponseWriter, r *http.Request) {
	fmt.Println("deal request  ", *listen)
	fmt.Fprintf(w, "Hello, %s!  host %s", r.URL.Path[1:], *listen)
}


func main() {
	flag.Parse()
	clientService, err := clientv3.New(clientv3.Config{
		Endpoints:   []string{"192.168.10.204:2379"},
		DialTimeout: time.Duration(5) * time.Second,
	})
	if err != nil {
		panic(err)
	}
	prefix = "/server/serverA" + "/" + *listen

	Register(clientService)
	defer Unregister(clientService)

	c := make(chan os.Signal)
	signal.Notify(c, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
	go func() {
		for s := range c {
			switch s {
			case syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT:
				fmt.Println("退出", s)
				Unregister(clientService)
				os.Exit(0)
			default:
				fmt.Println("other", s)
			}
		}
	}()

	http.HandleFunc("/", HelloServer)
	http.ListenAndServe(*listen, nil)
}

主要就是在启动的时候,向服务的key注册自己的服务,然后监听退出的信号,如果退出了则删除etcd中对应的数据此时就让provider知道当前服务下线。

首先编译两个文件

go build -o provider  provider.go
go build -o service service.go

启动provider运行

 ./provider 
[]

然后依次启动三个服务;

./service -listen=127.0.0.1:7997
./service -listen=127.0.0.1:7998
./service -listen=127.0.0.1:7996

此时终端provider的输出如下;

./provider 
[]
PUT "/server/serverA/127.0.0.1:7996" : "http://127.0.0.1:7996"
[http://127.0.0.1:7996]
PUT "/server/serverA/127.0.0.1:7998" : "http://127.0.0.1:7998"
[http://127.0.0.1:7996 http://127.0.0.1:7998]
PUT "/server/serverA/127.0.0.1:7997" : "http://127.0.0.1:7997"
[http://127.0.0.1:7996 http://127.0.0.1:7997 http://127.0.0.1:7998]


依次输出了注册进去的服务列表,然后依次轮训来看是否轮训访问;

 wuzi$ curl 127.0.0.1:7999/asdf
Hello, asdf!  host 127.0.0.1:7997
 wuzi$ curl 127.0.0.1:7999/asdf
Hello, asdf!  host 127.0.0.1:7998
 wuzi$ curl 127.0.0.1:7999/asdf
Hello, asdf!  host 127.0.0.1:7996

终端上依次访问到了后端三个不同的端口,然后让7996端口的服务退出;

[http://127.0.0.1:7997 http://127.0.0.1:7998]
chosse  http://127.0.0.1:7998
chosse  http://127.0.0.1:7997
chosse  http://127.0.0.1:7998

此时provider的输出如下所示,此时提供服务的只有7997和7998两个服务,此时就将服务下线了,分别通过不同的后端提供了服务。

至此,简单的服务注册的机制就完成了。

go-kit的服务机制

在kit中,提供了etcd,zk等机制的开箱即用的机制,还是基于stringsvc3例子

同样利用了etcd的机制来实现服务发现机制。

修改main.go文件;

package main

import (
	"context"
	"flag"
	"fmt"
	"net/http"
	"os"
	"time"

	"github.com/go-kit/kit/sd/etcdv3"
	stdprometheus "github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promhttp"

	"github.com/go-kit/kit/log"
	kitprometheus "github.com/go-kit/kit/metrics/prometheus"
	httptransport "github.com/go-kit/kit/transport/http"
)

func main() {
	...

	var (
		etcdAddrs = []string{"192.168.10.204:2379"}
		serName   = "svc.user.agent"
		ttl       = 5 * time.Second
	)

	//初始化etcd客户端
	options := etcdv3.ClientOptions{
		DialTimeout:   ttl,
		DialKeepAlive: ttl,
	}
	etcdClient, err := etcdv3.NewClient(context.Background(), etcdAddrs, options)
	if err != nil {
		logger.Log("[user_agent]  NewClient", err.Error())
		return
	}

	showKey := fmt.Sprintf("%s/%s",serName, *listen)
	if *proxy == ""{
		fmt.Println("show key  ", showKey)
		Registar := etcdv3.NewRegistrar(etcdClient, etcdv3.Service{
			Key:   fmt.Sprintf("%s/%s", "/services/barsvc", *listen),
			Value: *listen,
		}, log.NewNopLogger())
		Registar.Register()
		fmt.Println("after register  ", Registar)
		// 取消任务
		defer Registar.Deregister()
	}

	var svc StringService
	svc = stringService{}
	svc = proxyingMiddleware(context.Background(), *proxy, logger, etcdClient)(svc) // 注册反向代理的中间件,如果proxy有数据则是反向代理开启
	svc = loggingMiddleware(logger)(svc)
	svc = instrumentingMiddleware(requestCount, requestLatency, countResult)(svc)
	...

}

添加了etcd的注册机制,然后修改proxying.go的文件;

package main

import (
	"context"
	"errors"
	"fmt"
	"github.com/go-kit/kit/sd/etcdv3"
	"io"
	"net/url"
	"strings"
	"time"

	"github.com/go-kit/kit/endpoint"
	"github.com/go-kit/kit/log"
	"github.com/go-kit/kit/sd"
	"github.com/go-kit/kit/sd/lb"
	httptransport "github.com/go-kit/kit/transport/http"
)

func proxyingMiddleware(ctx context.Context, instances string, logger log.Logger, client etcdv3.Client) ServiceMiddleware {
	// If instances is empty, don't proxy.
	if instances == "" {
		logger.Log("proxy_to", "none")
		return func(next StringService) StringService { return next }
	}

	instancer, err := etcdv3.NewInstancer(client, "/services/barsvc", logger)
	if err != nil {
		panic(err)
	}
	endpointer := sd.NewEndpointer(instancer, barFactory, logger)

	var (
		maxAttempts = 3                      // per request, before giving up
		maxTime     = 250 * time.Millisecond // wallclock time, before giving up
	)

	balancer := lb.NewRoundRobin(endpointer)
	retry := lb.Retry(maxAttempts, maxTime, balancer)

	// And finally, return the ServiceMiddleware, implemented by proxymw.
	return func(next StringService) StringService {
		fmt.Println(retry)
		return proxymw{ctx, next, retry}
	}
}

...

func barFactory(instance string) (endpoint.Endpoint, io.Closer, error) {
	fmt.Println(instance)
	//return endpoint.Nop, nil, nil
	return makeUppercaseProxy(context.Background(), instance), nil, nil
}

此时编译运行;操作方式同上,同样也能查看出服务上线或下线对服务的流程。

总结

服务发现的机制目前的流程相对比较简单,通过注册到zk或者etcd中,然后通过监听对应的key,来动态的发现服务的上线或者下线,当前的kit的实现相对比较方便开箱即用的方式方便快捷。由于本人才疏学浅,如有错误请批评指正。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值