Create & Init Project...

This commit is contained in:
2019-04-22 18:49:16 +08:00
commit fc4fa37393
25440 changed files with 4054998 additions and 0 deletions

30
library/naming/BUILD Normal file
View File

@ -0,0 +1,30 @@
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [
":package-srcs",
"//library/naming/discovery:all-srcs",
"//library/naming/livezk:all-srcs",
],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)
go_library(
name = "go_default_library",
srcs = ["naming.go"],
importpath = "go-common/library/naming",
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

13
library/naming/README.md Normal file
View File

@ -0,0 +1,13 @@
#### naming
##### 项目简介
服务发现、服务注册相关的SDK集合
##### 编译环境
- **请只用 Golang v1.8.x 以上版本编译执行**
##### 依赖包

View File

@ -0,0 +1,69 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_test",
"go_library",
)
go_test(
name = "go_default_test",
srcs = ["discovery_test.go"],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
deps = [
"//library/conf/env:go_default_library",
"//library/ecode:go_default_library",
"//library/exp/feature:go_default_library",
"//library/naming:go_default_library",
"//library/net/http/blademaster:go_default_library",
"//vendor/github.com/smartystreets/goconvey/convey:go_default_library",
],
)
go_library(
name = "go_default_library",
srcs = [
"discovery.go",
"shuffle.go",
],
importpath = "go-common/library/naming/discovery",
tags = ["automanaged"],
deps = [
"//library/conf/env:go_default_library",
"//library/ecode:go_default_library",
"//library/exp/feature:go_default_library",
"//library/log:go_default_library",
"//library/naming:go_default_library",
"//library/net/http/blademaster:go_default_library",
"//library/net/netutil:go_default_library",
"//library/net/netutil/breaker:go_default_library",
"//library/time:go_default_library",
"//library/xstr:go_default_library",
],
)
go_test(
name = "go_default_xtest",
srcs = ["example_test.go"],
tags = ["automanaged"],
deps = [
"//library/naming:go_default_library",
"//library/naming/discovery:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@ -0,0 +1,32 @@
### discovery
##### Version 1.2.3
> 1.register add context
##### Version 1.2.2
> 1.添加discovery域名
##### Version 1.2.1
> 1.diff自发现节点
##### Version 1.2.0
> 1.添加自发现feature
##### Version 1.1.4
> 1.Watch之后会立即取消Polls
##### Version 1.1.3
> 1.Fetch返回一个Map
##### Version 1.1.2
> 1.初始化的时候新增同步阻塞的nodes更新
##### Version 1.1.1
> 1.fix atomic store inconsistency
##### Version 1.1.0
> 1.添加unwatch 事件
##### Version 1.0.0
1. 服务发现客户端基本功能完成

View File

@ -0,0 +1,11 @@
# Owner
lintanghui
caoguoliang
# Author
lintanghui
caoguoliang
# Reviewer
maojian

View File

@ -0,0 +1,9 @@
# See the OWNERS docs at https://go.k8s.io/owners
approvers:
- caoguoliang
- lintanghui
reviewers:
- caoguoliang
- lintanghui
- maojian

View File

@ -0,0 +1,13 @@
#### discovery
##### 项目简介
discovery的客户端SDK包括了服务发现和服务注册功能
##### 编译环境
- **请只用 Golang v1.8.x 以上版本编译执行**
##### 依赖包
> 1.公共包go-common

View File

@ -0,0 +1,721 @@
package discovery
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/url"
"os"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"go-common/library/conf/env"
"go-common/library/ecode"
"go-common/library/exp/feature"
"go-common/library/log"
"go-common/library/naming"
bm "go-common/library/net/http/blademaster"
"go-common/library/net/netutil"
"go-common/library/net/netutil/breaker"
xtime "go-common/library/time"
"go-common/library/xstr"
)
const (
_registerURL = "http://%s/discovery/register"
_setURL = "http://%s/discovery/set"
_cancelURL = "http://%s/discovery/cancel"
_renewURL = "http://%s/discovery/renew"
_pollURL = "http://%s/discovery/polls"
_nodesURL = "http://%s/discovery/nodes"
_registerGap = 30 * time.Second
_statusUP = "1"
)
const (
_appid = "infra.discovery"
)
var (
_ naming.Builder = &Discovery{}
_ naming.Registry = &Discovery{}
_selfDiscoveryFeatrue feature.Feature = "discovery.self"
_discoveryFeatures = map[feature.Feature]feature.Spec{
_selfDiscoveryFeatrue: {Default: false},
}
// ErrDuplication duplication treeid.
ErrDuplication = errors.New("discovery: instance duplicate registration")
)
func init() {
feature.DefaultGate.Add(_discoveryFeatures)
}
// Config discovery configures.
type Config struct {
Nodes []string
Key string
Secret string
Region string
Zone string
Env string
Host string
}
type appData struct {
ZoneInstances map[string][]*naming.Instance `json:"zone_instances"`
LastTs int64 `json:"latest_timestamp"`
}
// Discovery is discovery client.
type Discovery struct {
once sync.Once
conf *Config
ctx context.Context
cancelFunc context.CancelFunc
httpClient *bm.Client
mutex sync.RWMutex
apps map[string]*appInfo
registry map[string]struct{}
lastHost string
cancelPolls context.CancelFunc
idx uint64
node atomic.Value
delete chan *appInfo
}
type appInfo struct {
zoneIns atomic.Value
resolver map[*Resolver]struct{}
lastTs int64 // latest timestamp
}
func fixConfig(c *Config) {
if len(c.Nodes) == 0 {
c.Nodes = []string{"api.bilibili.co"}
}
if env.Region != "" {
c.Region = env.Region
}
if env.Zone != "" {
c.Zone = env.Zone
}
if env.DeployEnv != "" {
c.Env = env.DeployEnv
}
if env.Hostname != "" {
c.Host = env.Hostname
} else {
c.Host, _ = os.Hostname()
}
}
var (
once sync.Once
_defaultDiscovery *Discovery
)
func initDefault() {
once.Do(func() {
_defaultDiscovery = New(nil)
})
}
// Builder return default discvoery resolver builder.
func Builder() naming.Builder {
if _defaultDiscovery == nil {
initDefault()
}
return _defaultDiscovery
}
// Build register resolver into default discovery.
func Build(id string) naming.Resolver {
if _defaultDiscovery == nil {
initDefault()
}
return _defaultDiscovery.Build(id)
}
// New new a discovery client.
func New(c *Config) (d *Discovery) {
if c == nil {
c = &Config{
Nodes: []string{"discovery.bilibili.co", "api.bilibili.co"},
Key: "discovery",
Secret: "discovery",
}
}
fixConfig(c)
ctx, cancel := context.WithCancel(context.Background())
d = &Discovery{
ctx: ctx,
cancelFunc: cancel,
conf: c,
apps: map[string]*appInfo{},
registry: map[string]struct{}{},
delete: make(chan *appInfo, 10),
}
// httpClient
cfg := &bm.ClientConfig{
App: &bm.App{
Key: c.Key,
Secret: c.Secret,
},
Dial: xtime.Duration(3 * time.Second),
Timeout: xtime.Duration(40 * time.Second),
Breaker: &breaker.Config{
Window: 100,
Sleep: 3,
Bucket: 10,
Ratio: 0.5,
Request: 100,
},
}
d.httpClient = bm.NewClient(cfg)
if feature.DefaultGate.Enabled(_selfDiscoveryFeatrue) {
resolver := d.Build(_appid)
event := resolver.Watch()
_, ok := <-event
if !ok {
panic("discovery watch failed")
}
ins, ok := resolver.Fetch(context.Background())
if ok {
d.newSelf(ins)
}
go d.selfproc(resolver, event)
}
return
}
func (d *Discovery) selfproc(resolver naming.Resolver, event <-chan struct{}) {
for {
_, ok := <-event
if !ok {
return
}
zones, ok := resolver.Fetch(context.Background())
if ok {
d.newSelf(zones)
}
}
}
func (d *Discovery) newSelf(zones map[string][]*naming.Instance) {
ins, ok := zones[d.conf.Zone]
if !ok {
return
}
var nodes []string
for _, in := range ins {
for _, addr := range in.Addrs {
u, err := url.Parse(addr)
if err == nil && u.Scheme == "http" {
nodes = append(nodes, u.Host)
}
}
}
// diff old nodes
olds, ok := d.node.Load().([]string)
if ok {
var diff int
for _, n := range nodes {
for _, o := range olds {
if o == n {
diff++
break
}
}
}
if len(nodes) == diff {
return
}
}
// FIXME: we should use rand.Shuffle() in golang 1.10
Shuffle(len(nodes), func(i, j int) {
nodes[i], nodes[j] = nodes[j], nodes[i]
})
d.node.Store(nodes)
}
// Build disovery resovler builder.
func (d *Discovery) Build(appid string) naming.Resolver {
r := &Resolver{
id: appid,
d: d,
event: make(chan struct{}, 1),
}
d.mutex.Lock()
app, ok := d.apps[appid]
if !ok {
app = &appInfo{
resolver: make(map[*Resolver]struct{}),
}
d.apps[appid] = app
cancel := d.cancelPolls
if cancel != nil {
cancel()
}
}
app.resolver[r] = struct{}{}
d.mutex.Unlock()
if ok {
select {
case r.event <- struct{}{}:
default:
}
}
log.Info("disocvery: AddWatch(%s) already watch(%v)", appid, ok)
d.once.Do(func() {
go d.serverproc()
})
return r
}
// Scheme return discovery's scheme
func (d *Discovery) Scheme() string {
return "discovery"
}
// Resolver discveory resolver.
type Resolver struct {
id string
event chan struct{}
d *Discovery
}
// Watch watch instance.
func (r *Resolver) Watch() <-chan struct{} {
return r.event
}
// Fetch fetch resolver instance.
func (r *Resolver) Fetch(c context.Context) (ins map[string][]*naming.Instance, ok bool) {
r.d.mutex.RLock()
app, ok := r.d.apps[r.id]
r.d.mutex.RUnlock()
if ok {
ins, ok = app.zoneIns.Load().(map[string][]*naming.Instance)
return
}
return
}
// Close close resolver.
func (r *Resolver) Close() error {
r.d.mutex.Lock()
if app, ok := r.d.apps[r.id]; ok && len(app.resolver) != 0 {
delete(app.resolver, r)
// TODO: delete app from builder
}
r.d.mutex.Unlock()
return nil
}
func (d *Discovery) pickNode() string {
nodes, ok := d.node.Load().([]string)
if !ok || len(nodes) == 0 {
return d.conf.Nodes[d.idx%uint64(len(d.conf.Nodes))]
}
return nodes[d.idx%uint64(len(nodes))]
}
func (d *Discovery) switchNode() {
atomic.AddUint64(&d.idx, 1)
}
// Reload reload the config
func (d *Discovery) Reload(c *Config) {
fixConfig(c)
d.mutex.Lock()
d.conf = c
d.mutex.Unlock()
}
// Close stop all running process including discovery and register
func (d *Discovery) Close() error {
d.cancelFunc()
return nil
}
// Register Register an instance with discovery and renew automatically
func (d *Discovery) Register(c context.Context, ins *naming.Instance) (cancelFunc context.CancelFunc, err error) {
d.mutex.Lock()
if _, ok := d.registry[ins.AppID]; ok {
err = ErrDuplication
} else {
d.registry[ins.AppID] = struct{}{}
}
d.mutex.Unlock()
if err != nil {
return
}
if err = d.register(c, ins); err != nil {
d.mutex.Lock()
delete(d.registry, ins.AppID)
d.mutex.Unlock()
return
}
ctx, cancel := context.WithCancel(d.ctx)
ch := make(chan struct{}, 1)
cancelFunc = context.CancelFunc(func() {
cancel()
<-ch
})
go func() {
ticker := time.NewTicker(_registerGap)
defer ticker.Stop()
for {
select {
case <-ticker.C:
if err := d.renew(ctx, ins); err != nil && ecode.NothingFound.Equal(err) {
d.register(ctx, ins)
}
case <-ctx.Done():
d.cancel(ins)
ch <- struct{}{}
return
}
}
}()
return
}
// Set set ins status and metadata.
func (d *Discovery) Set(ins *naming.Instance) error {
return d.set(context.Background(), ins)
}
// cancel Remove the registered instance from discovery
func (d *Discovery) cancel(ins *naming.Instance) (err error) {
d.mutex.RLock()
conf := d.conf
d.mutex.RUnlock()
res := new(struct {
Code int `json:"code"`
Message string `json:"message"`
})
uri := fmt.Sprintf(_cancelURL, d.pickNode())
params := d.newParams(conf)
params.Set("appid", ins.AppID)
// request
if err = d.httpClient.Post(context.Background(), uri, "", params, &res); err != nil {
d.switchNode()
log.Error("discovery cancel client.Get(%v) env(%s) appid(%s) hostname(%s) error(%v)",
uri, conf.Env, ins.AppID, conf.Host, err)
return
}
if ec := ecode.Int(res.Code); !ec.Equal(ecode.OK) {
log.Warn("discovery cancel client.Get(%v) env(%s) appid(%s) hostname(%s) code(%v)",
uri, conf.Env, ins.AppID, conf.Host, res.Code)
err = ec
return
}
log.Info("discovery cancel client.Get(%v) env(%s) appid(%s) hostname(%s) success",
uri, conf.Env, ins.AppID, conf.Host)
return
}
// register Register an instance with discovery
func (d *Discovery) register(ctx context.Context, ins *naming.Instance) (err error) {
d.mutex.RLock()
conf := d.conf
d.mutex.RUnlock()
var metadata []byte
if ins.Metadata != nil {
if metadata, err = json.Marshal(ins.Metadata); err != nil {
log.Error("discovery:register instance Marshal metadata(%v) failed!error(%v)", ins.Metadata, err)
}
}
res := new(struct {
Code int `json:"code"`
Message string `json:"message"`
})
uri := fmt.Sprintf(_registerURL, d.pickNode())
params := d.newParams(conf)
params.Set("appid", ins.AppID)
params.Set("addrs", strings.Join(ins.Addrs, ","))
params.Set("version", ins.Version)
params.Set("status", _statusUP)
params.Set("metadata", string(metadata))
if err = d.httpClient.Post(ctx, uri, "", params, &res); err != nil {
d.switchNode()
log.Error("discovery: register client.Get(%v) zone(%s) env(%s) appid(%s) addrs(%v) error(%v)",
uri, conf.Zone, conf.Env, ins.AppID, ins.Addrs, err)
return
}
if ec := ecode.Int(res.Code); !ec.Equal(ecode.OK) {
log.Warn("discovery: register client.Get(%v) env(%s) appid(%s) addrs(%v) code(%v)",
uri, conf.Env, ins.AppID, ins.Addrs, res.Code)
err = ec
return
}
log.Info("discovery: register client.Get(%v) env(%s) appid(%s) addrs(%s) success",
uri, conf.Env, ins.AppID, ins.Addrs)
return
}
// rset set instance info with discovery
func (d *Discovery) set(ctx context.Context, ins *naming.Instance) (err error) {
d.mutex.RLock()
conf := d.conf
d.mutex.RUnlock()
res := new(struct {
Code int `json:"code"`
Message string `json:"message"`
})
uri := fmt.Sprintf(_setURL, d.pickNode())
params := d.newParams(conf)
params.Set("appid", ins.AppID)
params.Set("version", ins.Version)
params.Set("status", strconv.FormatInt(ins.Status, 10))
if ins.Metadata != nil {
var metadata []byte
if metadata, err = json.Marshal(ins.Metadata); err != nil {
log.Error("discovery:set instance Marshal metadata(%v) failed!error(%v)", ins.Metadata, err)
}
params.Set("metadata", string(metadata))
}
if err = d.httpClient.Post(ctx, uri, "", params, &res); err != nil {
d.switchNode()
log.Error("discovery: set client.Get(%v) zone(%s) env(%s) appid(%s) addrs(%v) error(%v)",
uri, conf.Zone, conf.Env, ins.AppID, ins.Addrs, err)
return
}
if ec := ecode.Int(res.Code); !ec.Equal(ecode.OK) {
log.Warn("discovery: set client.Get(%v) env(%s) appid(%s) addrs(%v) code(%v)",
uri, conf.Env, ins.AppID, ins.Addrs, res.Code)
err = ec
return
}
log.Info("discovery: set client.Get(%v) env(%s) appid(%s) addrs(%s) success",
uri+"?"+params.Encode(), conf.Env, ins.AppID, ins.Addrs)
return
}
// renew Renew an instance with discovery
func (d *Discovery) renew(ctx context.Context, ins *naming.Instance) (err error) {
d.mutex.RLock()
conf := d.conf
d.mutex.RUnlock()
res := new(struct {
Code int `json:"code"`
Message string `json:"message"`
})
uri := fmt.Sprintf(_renewURL, d.pickNode())
params := d.newParams(conf)
params.Set("appid", ins.AppID)
if err = d.httpClient.Post(ctx, uri, "", params, &res); err != nil {
d.switchNode()
log.Error("discovery: renew client.Get(%v) env(%s) appid(%s) hostname(%s) error(%v)",
uri, conf.Env, ins.AppID, conf.Host, err)
return
}
if ec := ecode.Int(res.Code); !ec.Equal(ecode.OK) {
err = ec
if ec.Equal(ecode.NothingFound) {
return
}
log.Error("discovery: renew client.Get(%v) env(%s) appid(%s) hostname(%s) code(%v)",
uri, conf.Env, ins.AppID, conf.Host, res.Code)
return
}
return
}
func (d *Discovery) serverproc() {
var (
retry int
update bool
ctx context.Context
cancel context.CancelFunc
)
bc := netutil.DefaultBackoffConfig
ticker := time.NewTicker(time.Minute * 30)
defer ticker.Stop()
for {
if ctx == nil {
ctx, cancel = context.WithCancel(d.ctx)
d.mutex.Lock()
d.cancelPolls = cancel
d.mutex.Unlock()
}
select {
case <-d.ctx.Done():
return
case <-ticker.C:
update = true
default:
}
if !feature.DefaultGate.Enabled(_selfDiscoveryFeatrue) {
nodes, ok := d.node.Load().([]string)
if !ok || len(nodes) == 0 || update {
update = false
tnodes := d.nodes()
if len(tnodes) == 0 {
time.Sleep(bc.Backoff(retry))
retry++
continue
}
retry = 0
// FIXME: we should use rand.Shuffle() in golang 1.10
Shuffle(len(tnodes), func(i, j int) {
tnodes[i], tnodes[j] = tnodes[j], tnodes[i]
})
d.node.Store(tnodes)
}
}
apps, err := d.polls(ctx, d.pickNode())
if err != nil {
d.switchNode()
if ctx.Err() == context.Canceled {
ctx = nil
continue
}
time.Sleep(bc.Backoff(retry))
retry++
continue
}
retry = 0
d.broadcast(apps)
}
}
func (d *Discovery) nodes() (nodes []string) {
res := new(struct {
Code int `json:"code"`
Data []struct {
Addr string `json:"addr"`
} `json:"data"`
})
uri := fmt.Sprintf(_nodesURL, d.pickNode())
if err := d.httpClient.Get(d.ctx, uri, "", nil, res); err != nil {
d.switchNode()
log.Error("discovery: consumer client.Get(%v)error(%+v)", uri, err)
return
}
if ec := ecode.Int(res.Code); !ec.Equal(ecode.OK) {
log.Error("discovery: consumer client.Get(%v) error(%v)", uri, res.Code)
return
}
if len(res.Data) == 0 {
log.Warn("discovery: get nodes(%s) failed,no nodes found!", uri)
return
}
nodes = make([]string, 0, len(res.Data))
for i := range res.Data {
nodes = append(nodes, res.Data[i].Addr)
}
return
}
func (d *Discovery) polls(ctx context.Context, host string) (apps map[string]appData, err error) {
var (
lastTs []int64
appid []string
changed bool
)
if host != d.lastHost {
d.lastHost = host
changed = true
}
d.mutex.RLock()
conf := d.conf
for k, v := range d.apps {
if changed {
v.lastTs = 0
}
appid = append(appid, k)
lastTs = append(lastTs, v.lastTs)
}
d.mutex.RUnlock()
if len(appid) == 0 {
return
}
uri := fmt.Sprintf(_pollURL, host)
res := new(struct {
Code int `json:"code"`
Data map[string]appData `json:"data"`
})
params := url.Values{}
params.Set("env", conf.Env)
params.Set("hostname", conf.Host)
params.Set("appid", strings.Join(appid, ","))
params.Set("latest_timestamp", xstr.JoinInts(lastTs))
if err = d.httpClient.Get(ctx, uri, "", params, res); err != nil {
log.Error("discovery: client.Get(%s) error(%+v)", uri+"?"+params.Encode(), err)
return
}
if ec := ecode.Int(res.Code); !ec.Equal(ecode.OK) {
if !ec.Equal(ecode.NotModified) {
log.Error("discovery: client.Get(%s) get error code(%d)", uri+"?"+params.Encode(), res.Code)
err = ec
}
return
}
info, _ := json.Marshal(res.Data)
for _, app := range res.Data {
if app.LastTs == 0 {
err = ecode.ServerErr
log.Error("discovery: client.Get(%s) latest_timestamp is 0,instances:(%s)", uri+"?"+params.Encode(), info)
return
}
}
log.Info("discovery: polls uri(%s)", uri+"?"+params.Encode())
log.Info("discovery: successfully polls(%s) instances (%s)", uri+"?"+params.Encode(), info)
apps = res.Data
return
}
func (d *Discovery) broadcast(apps map[string]appData) {
for id, v := range apps {
var count int
for zone, ins := range v.ZoneInstances {
if len(ins) == 0 {
delete(v.ZoneInstances, zone)
}
count += len(ins)
}
if count == 0 {
continue
}
d.mutex.RLock()
app, ok := d.apps[id]
d.mutex.RUnlock()
if ok {
app.lastTs = v.LastTs
app.zoneIns.Store(v.ZoneInstances)
d.mutex.RLock()
for rs := range app.resolver {
select {
case rs.event <- struct{}{}:
default:
}
}
d.mutex.RUnlock()
}
}
}
func (d *Discovery) newParams(conf *Config) url.Values {
params := url.Values{}
params.Set("region", conf.Region)
params.Set("zone", conf.Zone)
params.Set("env", conf.Env)
params.Set("hostname", conf.Host)
return params
}

View File

@ -0,0 +1,365 @@
package discovery
import (
"context"
"flag"
"fmt"
"os"
"testing"
"time"
"go-common/library/conf/env"
"go-common/library/ecode"
"go-common/library/exp/feature"
"go-common/library/naming"
"go-common/library/net/http/blademaster"
. "github.com/smartystreets/goconvey/convey"
)
var appdID = "main.arch.test66"
var appID2 = "main.arch.test22"
func TestMain(m *testing.M) {
feature.DefaultGate.AddFlag(flag.CommandLine)
flag.Set("feature-gates", fmt.Sprintf("%s=true", _selfDiscoveryFeatrue))
os.Exit(m.Run())
}
var c = &Config{
Nodes: []string{"172.18.33.51:7171"},
Zone: "sh001",
Env: "pre",
Key: "0c4b8fe3ff35a4b6",
Secret: "b370880d1aca7d3a289b9b9a7f4d6812",
Host: "host_1",
}
var indis = &naming.Instance{
AppID: appdID,
Zone: env.Zone,
Addrs: []string{
"grpc://172.18.33.51:8080",
"http://172.18.33.51:7171",
},
Version: "1",
Metadata: map[string]string{
"test": "1",
"weight": "12",
"color": "blue",
},
}
var in = &naming.Instance{
AppID: appdID,
Addrs: []string{
"grpc://127.0.0.1:8080",
},
Version: "1",
Metadata: map[string]string{
"test": "1",
"weight": "12",
"color": "blue",
},
}
var intest2 = &naming.Instance{
AppID: appID2,
Addrs: []string{
"grpc://127.0.0.1:8080",
},
Version: "1",
Metadata: map[string]string{
"test": "1",
"weight": "12",
"color": "blue",
},
}
var in2 = &naming.Instance{
AppID: appdID,
Addrs: []string{
"grpc://127.0.0.1:8081",
},
Version: "1",
Metadata: map[string]string{
"test": "2",
"weight": "6",
"color": "red",
},
}
func TestDiscoverySelf(t *testing.T) {
Convey("test TestDiscoverySelf ", t, func() {
So(feature.DefaultGate.Enabled(_selfDiscoveryFeatrue), ShouldBeTrue)
d := New(c)
So(len(d.node.Load().([]string)), ShouldNotEqual, 0)
})
}
func TestRegister(t *testing.T) {
Convey("test register and cancel", t, func() {
env.Hostname = "host_1"
d := New(c)
defer d.Close()
ctx := context.TODO()
cancel, err := d.Register(ctx, in)
defer cancel()
So(err, ShouldBeNil)
rs := d.Build(appdID)
ch := rs.Watch()
So(ch, ShouldNotBeNil)
<-ch
ins, ok := rs.Fetch(ctx)
So(ok, ShouldBeTrue)
So(err, ShouldBeNil)
var count int
for _, data := range ins {
count += len(data)
}
So(count, ShouldEqual, 1)
c.Host = "host_2"
env.Hostname = "host_2"
d2 := New(c)
defer d2.Close()
cancel2, err := d2.Register(ctx, in2)
So(err, ShouldBeNil)
<-ch
ins, _ = rs.Fetch(ctx)
So(err, ShouldBeNil)
count = 0
for _, data := range ins {
count += len(data)
}
So(count, ShouldEqual, 2)
time.Sleep(time.Millisecond * 500)
cancel2()
<-ch
ins, _ = rs.Fetch(ctx)
So(err, ShouldBeNil)
count = 0
for _, data := range ins {
count += len(data)
}
So(count, ShouldEqual, 1)
Convey("test discovery set", func() {
c.Host = "host_1"
inSet := &naming.Instance{
AppID: appdID,
Addrs: []string{
"grpc://127.0.0.1:8080",
},
Status: 1,
Metadata: map[string]string{
"test": "1",
"weight": "111",
"color": "blue",
},
}
ins, _ = rs.Fetch(context.TODO())
fmt.Println("ins", ins["sh001"][0])
err = d2.Set(inSet)
// So(err, ShouldBeNil)
<-ch
ins, _ = rs.Fetch(context.TODO())
fmt.Println("ins1", ins["sh001"][0])
So(ins["sh001"][0].Metadata["weight"], ShouldResemble, "111")
})
})
}
func TestMultiZone(t *testing.T) {
Convey("test multi zone", t, func() {
env.Hostname = "host_1"
d := New(c)
defer d.Close()
ctx := context.TODO()
cancel, err := d.Register(ctx, in)
So(err, ShouldBeNil)
defer cancel()
rs := d.Build(appdID)
ch := rs.Watch()
So(ch, ShouldNotBeNil)
<-ch
ins, ok := rs.Fetch(ctx)
So(ok, ShouldBeTrue)
count := 0
for _, data := range ins {
count += len(data)
}
So(count, ShouldEqual, 1)
env.Hostname = "host_2"
env.Zone = "other_zone"
d2 := New(c)
defer d2.Close()
cancel2, err := d2.Register(ctx, in2)
So(err, ShouldBeNil)
defer func() {
cancel2()
time.Sleep(time.Millisecond * 300)
}()
<-ch
ins, ok = rs.Fetch(ctx)
So(ok, ShouldBeTrue)
count = 0
zoneCount := 0
for _, data := range ins {
zoneCount++
count += len(data)
}
So(count, ShouldEqual, 2)
So(zoneCount, ShouldEqual, 2)
})
}
func TestDiscoveryFailOver(t *testing.T) {
Convey("test failover", t, func() {
var conf = &Config{
Nodes: []string{"127.0.0.1:8080"},
Zone: "sh001",
Env: "pre",
Key: "0c4b8fe3ff35a4b6",
Secret: "b370880d1aca7d3a289b9b9a7f4d6812",
Host: "host_1",
}
for _, a := range []string{":8080"} {
go func(addr string) {
e := blademaster.DefaultServer(nil)
e.GET("/discovery/nodes", func(ctx *blademaster.Context) {
type v struct {
Addr string `json:"addr"`
}
ctx.JSON([]v{{Addr: "127.0.0.1:8080"}}, nil)
})
e.GET("/discovery/polls", func(ctx *blademaster.Context) {
params := ctx.Request.Form
ts := params.Get("latest_timestamp")
if ts == "0" {
ctx.JSON(map[string]appData{
appdID: {LastTs: time.Now().UnixNano(), ZoneInstances: map[string][]*naming.Instance{"zone": {in}}},
"infra.discovery": {LastTs: time.Now().UnixNano(), ZoneInstances: map[string][]*naming.Instance{conf.Zone: {indis}}},
}, nil)
} else {
ctx.JSON(nil, ecode.ServerErr)
}
})
e.Run(addr)
}(a)
}
time.Sleep(time.Millisecond * 30)
d := New(conf)
defer d.Close()
rs := d.Build(appdID)
ch := rs.Watch()
<-ch
ins, _ := rs.Fetch(context.TODO())
count := 0
zoneCount := 0
for _, data := range ins {
zoneCount++
count += len(data)
}
So(count, ShouldEqual, 1)
So(zoneCount, ShouldEqual, 1)
})
}
func TestWatchContinuosly(t *testing.T) {
Convey("test TestWatchContinuosly ", t, func() {
env.Hostname = "host_1"
d := New(c)
defer d.Close()
in1 := *in
in1.AppID = "test.test"
ctx := context.TODO()
cancel, err := d.Register(ctx, &in1)
So(err, ShouldBeNil)
defer cancel()
in2 := *in
in2.AppID = "test.test2"
cancel2, err := d.Register(ctx, &in2)
So(err, ShouldBeNil)
defer cancel2()
in3 := *in
in3.AppID = "test.test3"
cancel3, err := d.Register(ctx, &in3)
So(err, ShouldBeNil)
defer cancel3()
rs := d.Build("test.test")
ch := rs.Watch()
<-ch
ins, ok := rs.Fetch(ctx)
So(ok, ShouldBeTrue)
count := 0
for _, data := range ins {
count += len(data)
}
So(count, ShouldBeGreaterThanOrEqualTo, 1)
time.Sleep(time.Millisecond * 10)
rs = d.Build("test.test2")
ch2 := rs.Watch()
<-ch2
ins, ok = rs.Fetch(ctx)
So(ok, ShouldBeTrue)
count = 0
for _, data := range ins {
count += len(data)
}
So(count, ShouldBeGreaterThanOrEqualTo, 1)
rs = d.Build("test.test3")
ch3 := rs.Watch()
<-ch3
ins, ok = rs.Fetch(ctx)
So(ok, ShouldBeTrue)
count = 0
for _, data := range ins {
count += len(data)
}
So(count, ShouldBeGreaterThanOrEqualTo, 1)
})
}
func TestSameBuilder(t *testing.T) {
Convey("test multi watch", t, func() {
env.Hostname = "host_1"
d := New(c)
defer d.Close()
ctx := context.TODO()
cancel, err := d.Register(ctx, in)
d.Register(ctx, intest2)
defer cancel()
So(err, ShouldBeNil)
// first builder
rs := d.Build(appdID)
ch := rs.Watch()
So(ch, ShouldNotBeNil)
<-ch
_, ok := rs.Fetch(ctx)
So(ok, ShouldBeTrue)
So(err, ShouldBeNil)
var count int
// for _, data := range ins {
// count += len(data)
// }
// So(count, ShouldEqual, 1)
// same appd builder
rs2 := d.Build(appdID)
ch2 := rs2.Watch()
<-ch2
_, ok = rs2.Fetch(ctx)
So(ok, ShouldBeTrue)
So(err, ShouldBeNil)
rs3 := d.Build(appID2)
ch3 := rs3.Watch()
<-ch3
ins, _ := rs3.Fetch(ctx)
So(ok, ShouldBeTrue)
So(err, ShouldBeNil)
count = 0
for _, data := range ins {
count += len(data)
}
So(count, ShouldEqual, 1)
})
}

View File

@ -0,0 +1,55 @@
package discovery_test
import (
"context"
"fmt"
"go-common/library/naming"
"go-common/library/naming/discovery"
"time"
)
// this example creates a registry service to register instance info
// to discovery server.
// when the program is about to exit,registry.Cancel should be called.
func Example() {
var c = &discovery.Config{
Nodes: []string{"api.bilibili.co"},
Zone: "sh001",
Env: "pre",
Key: "0c4b8fe3ff35a4b6",
Secret: "b370880d1aca7d3a289b9b9a7f4d6812",
}
var ins = &naming.Instance{
AppID: "main.arch.test2",
Addrs: []string{
"grpc://127.0.0.1:8080",
},
Version: "1",
Metadata: map[string]string{
"weight": "128",
"color": "blue",
},
}
d := discovery.New(c)
cacenl, err := d.Register(context.TODO(), ins)
if err != nil {
return
}
defer cacenl()
//start to Serve
time.Sleep(time.Second * 5)
}
// this example creates a discovery client to poll instances from discovery server.
func ExampleDiscovery() {
d := discovery.Build("1231234")
ch := d.Watch()
for {
<-ch
ins, ok := d.Fetch(context.TODO())
if ok {
fmt.Println("new instances found:", ins)
}
}
}

View File

@ -0,0 +1,33 @@
package discovery
import (
"math/rand"
"time"
)
var r = rand.New(rand.NewSource(time.Now().UnixNano()))
// Shuffle pseudo-randomizes the order of elements.
// n is the number of elements. Shuffle panics if n < 0.
// swap swaps the elements with indexes i and j.
func Shuffle(n int, swap func(i, j int)) {
if n < 0 {
panic("invalid argument to Shuffle")
}
// Fisher-Yates shuffle: https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle
// Shuffle really ought not be called with n that doesn't fit in 32 bits.
// Not only will it take a very long time, but with 2³¹! possible permutations,
// there's no way that any PRNG can have a big enough internal state to
// generate even a minuscule percentage of the possible permutations.
// Nevertheless, the right API signature accepts an int n, so handle it as best we can.
i := n - 1
for ; i > 1<<31-1-1; i-- {
j := int(r.Int63n(int64(i + 1)))
swap(i, j)
}
for ; i > 0; i-- {
j := int(r.Int31n(int32(i + 1)))
swap(i, j)
}
}

View File

@ -0,0 +1,47 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_test",
"go_library",
)
go_test(
name = "go_default_test",
srcs = ["livezk_test.go"],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
deps = [
"//library/naming:go_default_library",
"//library/time:go_default_library",
],
)
go_library(
name = "go_default_library",
srcs = ["livezk.go"],
importpath = "go-common/library/naming/livezk",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//library/log:go_default_library",
"//library/naming:go_default_library",
"//library/time:go_default_library",
"//vendor/github.com/samuel/go-zookeeper/zk:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@ -0,0 +1,137 @@
package livezk
import (
"context"
"encoding/json"
"errors"
"net/url"
"path"
"strings"
"time"
"go-common/library/log"
"go-common/library/naming"
xtime "go-common/library/time"
"github.com/samuel/go-zookeeper/zk"
)
const (
basePath = "/live/service"
scheme = "grpc"
)
// Zookeeper Server&Client settings.
type Zookeeper struct {
Root string
Addrs []string
Timeout xtime.Duration
}
// New new live zookeeper registry
func New(config *Zookeeper) (naming.Registry, error) {
lz := &livezk{
zkConfig: config,
}
var err error
lz.zkConn, lz.zkEvent, err = zk.Connect(config.Addrs, time.Duration(config.Timeout))
if err != nil {
go lz.eventproc()
}
return lz, err
}
type zkIns struct {
Group string `json:"group"`
LibVersion string `json:"lib_version"`
StartupTime string `json:"startup_time"`
}
func newZkInsData(ins *naming.Instance) ([]byte, error) {
zi := &zkIns{
// TODO group support
Group: "default",
LibVersion: ins.Version,
StartupTime: time.Now().Format("2006-01-02 15:04:05"),
}
return json.Marshal(zi)
}
// livezk live service zookeeper registry
type livezk struct {
zkConfig *Zookeeper
zkConn *zk.Conn
zkEvent <-chan zk.Event
}
var _ naming.Registry = &livezk{}
func (l *livezk) Register(ctx context.Context, ins *naming.Instance) (cancel context.CancelFunc, err error) {
nodePath := path.Join(l.zkConfig.Root, basePath, ins.AppID)
if err = l.createAll(nodePath); err != nil {
return
}
var rpc string
for _, addr := range ins.Addrs {
u, ue := url.Parse(addr)
if ue == nil && u.Scheme == scheme {
rpc = u.Host
break
}
}
if rpc == "" {
err = errors.New("no GRPC addr")
return
}
dataPath := path.Join(nodePath, rpc)
data, err := newZkInsData(ins)
if err != nil {
return nil, err
}
_, err = l.zkConn.Create(dataPath, data, zk.FlagEphemeral, zk.WorldACL(zk.PermAll))
if err != nil {
return nil, err
}
return func() {
l.unregister(dataPath)
}, nil
}
func (l *livezk) Close() error {
l.zkConn.Close()
return nil
}
func (l *livezk) createAll(nodePath string) (err error) {
seps := strings.Split(nodePath, "/")
lastPath := "/"
ok := false
for _, part := range seps {
if part == "" {
continue
}
lastPath = path.Join(lastPath, part)
if ok, _, err = l.zkConn.Exists(lastPath); err != nil {
return err
} else if ok {
continue
}
if _, err = l.zkConn.Create(lastPath, nil, 0, zk.WorldACL(zk.PermAll)); err != nil {
return
}
}
return
}
func (l *livezk) eventproc() {
for event := range l.zkEvent {
// TODO handle zookeeper event
log.Info("zk event: err: %s, path: %s, server: %s, state: %s, type: %s",
event.Err, event.Path, event.Server, event.State, event.Type)
}
}
func (l *livezk) unregister(dataPath string) error {
return l.zkConn.Delete(dataPath, -1)
}

View File

@ -0,0 +1,77 @@
package livezk
import (
"context"
"fmt"
"path"
"testing"
"time"
"go-common/library/naming"
xtime "go-common/library/time"
)
var appdID = "main.arch.test6"
var addr = "127.0.0.1:8080"
var ins1 = &naming.Instance{
AppID: appdID,
Addrs: []string{"grpc://" + addr},
Version: "1",
Metadata: map[string]string{
"test": "1",
"color": "blue",
},
}
var addrs = []string{"172.18.33.131:2181", "172.18.33.168:2181", "172.18.33.169:2181"}
var zkConfig = &Zookeeper{
Addrs: addrs,
Timeout: xtime.Duration(time.Second),
}
func TestLiveZK(t *testing.T) {
reg, err := New(zkConfig)
if err != nil {
t.Fatal(err)
}
cancel, err := reg.Register(context.TODO(), ins1)
if err != nil {
t.Fatal(err)
}
defer cancel()
lzk := reg.(*livezk)
nodePath := path.Join(basePath, appdID, addr)
ok, _, err := lzk.zkConn.Exists(nodePath)
if err != nil {
if err != nil {
t.Fatal(err)
}
}
if !ok {
t.Errorf("path not exists %s", nodePath)
}
}
func TestLiveZKCancel(t *testing.T) {
reg, err := New(zkConfig)
if err != nil {
t.Fatal(err)
}
cancel, err := reg.Register(context.TODO(), ins1)
if err != nil {
t.Fatal(err)
}
cancel()
lzk := reg.(*livezk)
nodePath := path.Join(basePath, fmt.Sprintf("b%s", ins1.AppID), addr)
ok, _, err := lzk.zkConn.Exists(nodePath)
if err != nil {
if err != nil {
t.Fatal(err)
}
}
if ok {
t.Errorf("path should not exists %s", nodePath)
}
}

58
library/naming/naming.go Normal file
View File

@ -0,0 +1,58 @@
package naming
import (
"context"
)
// metadata common key
const (
MetaColor = "color"
MetaWeight = "weight"
MetaCluster = "cluster"
MetaZone = "zone"
)
// Instance represents a server the client connects to.
type Instance struct {
// Region bj/sh/gz
Region string `json:"region"`
// Zone is IDC.
Zone string `json:"zone"`
// Env prod/pre、uat/fat1
Env string `json:"env"`
// AppID is mapping servicetree appid.
AppID string `json:"appid"`
// Hostname is hostname from docker.
Hostname string `json:"hostname"`
// Addrs is the adress of app instance
// format: scheme://host
Addrs []string `json:"addrs"`
// Version is publishing version.
Version string `json:"version"`
// LastTs is instance latest updated timestamp
LastTs int64 `json:"latest_timestamp"`
// Metadata is the information associated with Addr, which may be used
// to make load balancing decision.
Metadata map[string]string `json:"metadata"`
Status int64
}
// Resolver resolve naming service
type Resolver interface {
Fetch(context.Context) (map[string][]*Instance, bool)
//Unwatch(id string)
Watch() <-chan struct{}
Close() error
}
// Registry Register an instance and renew automatically
type Registry interface {
Register(context.Context, *Instance) (context.CancelFunc, error)
Close() error
}
// Builder resolver builder.
type Builder interface {
Build(id string) Resolver
Scheme() string
}