服务注册发现
在微服务的架构当中,服务发现是比较常见的一个方式来保证服务的动态上线下线的机制,可以通过封装一定的库来对某一个服务进行调用时无感知,动态管理当前提供服务的机制,这样同一个服务可以注册多个地址来提供服务,也可以对这些地址进行相应的负载均衡等操作,其中服务发现当前比较主流的一般会是zk、consul或者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的实现相对比较方便开箱即用的方式方便快捷。由于本人才疏学浅,如有错误请批评指正。