parent
cc57564fba
commit
7d6f233259
@ -0,0 +1,56 @@ |
||||
package log |
||||
|
||||
import ( |
||||
"context" |
||||
|
||||
pkgerr "github.com/pkg/errors" |
||||
) |
||||
|
||||
const ( |
||||
_log = "log" |
||||
) |
||||
|
||||
// Handler is used to handle log events, outputting them to
|
||||
// stdio or sending them to remote services. See the "handlers"
|
||||
// directory for implementations.
|
||||
//
|
||||
// It is left up to Handlers to implement thread-safety.
|
||||
type Handler interface { |
||||
// Log handle log
|
||||
// variadic D is k-v struct represent log content
|
||||
Log(context.Context, Level, ...D) |
||||
|
||||
// SetFormat set render format on log output
|
||||
// see StdoutHandler.SetFormat for detail
|
||||
SetFormat(string) |
||||
|
||||
// Close handler
|
||||
Close() error |
||||
} |
||||
|
||||
// Handlers .
|
||||
type Handlers []Handler |
||||
|
||||
// Log handlers logging.
|
||||
func (hs Handlers) Log(c context.Context, lv Level, d ...D) { |
||||
for _, h := range hs { |
||||
h.Log(c, lv, d...) |
||||
} |
||||
} |
||||
|
||||
// Close close resource.
|
||||
func (hs Handlers) Close() (err error) { |
||||
for _, h := range hs { |
||||
if e := h.Close(); e != nil { |
||||
err = pkgerr.WithStack(e) |
||||
} |
||||
} |
||||
return |
||||
} |
||||
|
||||
// SetFormat .
|
||||
func (hs Handlers) SetFormat(format string) { |
||||
for _, h := range hs { |
||||
h.SetFormat(format) |
||||
} |
||||
} |
@ -0,0 +1,29 @@ |
||||
package log |
||||
|
||||
// Level of severity.
|
||||
type Level int |
||||
|
||||
// Verbose is a boolean type that implements Info, Infov (like Printf) etc.
|
||||
type Verbose bool |
||||
|
||||
// common log level.
|
||||
const ( |
||||
_debugLevel Level = iota |
||||
_infoLevel |
||||
_warnLevel |
||||
_errorLevel |
||||
_fatalLevel |
||||
) |
||||
|
||||
var levelNames = [...]string{ |
||||
_debugLevel: "DEBUG", |
||||
_infoLevel: "INFO", |
||||
_warnLevel: "WARN", |
||||
_errorLevel: "ERROR", |
||||
_fatalLevel: "FATAL", |
||||
} |
||||
|
||||
// String implementation.
|
||||
func (l Level) String() string { |
||||
return levelNames[l] |
||||
} |
@ -0,0 +1,88 @@ |
||||
package log |
||||
|
||||
import ( |
||||
"context" |
||||
"fmt" |
||||
) |
||||
|
||||
// Config log config.
|
||||
type Config struct { |
||||
Family string |
||||
Host string |
||||
|
||||
// stdout
|
||||
Stdout bool |
||||
|
||||
// file
|
||||
Dir string |
||||
// buffer size
|
||||
FileBufferSize int64 |
||||
// MaxLogFile
|
||||
MaxLogFile int |
||||
// RotateSize
|
||||
RotateSize int64 |
||||
|
||||
// V Enable V-leveled logging at the specified level.
|
||||
V int32 |
||||
// Module=""
|
||||
// The syntax of the argument is a map of pattern=N,
|
||||
// where pattern is a literal file name (minus the ".go" suffix) or
|
||||
// "glob" pattern and N is a V level. For instance:
|
||||
// [module]
|
||||
// "service" = 1
|
||||
// "dao*" = 2
|
||||
// sets the V level to 2 in all Go files whose names begin "dao".
|
||||
Module map[string]int32 |
||||
// Filter tell log handler which field are sensitive message, use * instead.
|
||||
Filter []string |
||||
} |
||||
|
||||
var ( |
||||
h Handler |
||||
c *Config |
||||
) |
||||
|
||||
// D represents a map of entry level data used for structured logging.
|
||||
// type D map[string]interface{}
|
||||
type D struct { |
||||
Key string |
||||
Value interface{} |
||||
} |
||||
|
||||
// KV return a log kv for logging field.
|
||||
func KV(key string, value interface{}) D { |
||||
return D{ |
||||
Key: key, |
||||
Value: value, |
||||
} |
||||
} |
||||
|
||||
// Info logs a message at the info log level.
|
||||
func Info(format string, args ...interface{}) { |
||||
h.Log(context.Background(), _infoLevel, KV(_log, fmt.Sprintf(format, args...))) |
||||
} |
||||
|
||||
// Warn logs a message at the warning log level.
|
||||
func Warn(format string, args ...interface{}) { |
||||
h.Log(context.Background(), _warnLevel, KV(_log, fmt.Sprintf(format, args...))) |
||||
} |
||||
|
||||
// Error logs a message at the error log level.
|
||||
func Error(format string, args ...interface{}) { |
||||
h.Log(context.Background(), _errorLevel, KV(_log, fmt.Sprintf(format, args...))) |
||||
} |
||||
|
||||
func logw(args []interface{}) []D { |
||||
if len(args)%2 != 0 { |
||||
Warn("log: the variadic must be plural, the last one will ignored") |
||||
} |
||||
ds := make([]D, 0, len(args)/2) |
||||
for i := 0; i < len(args)-1; i = i + 2 { |
||||
if key, ok := args[i].(string); ok { |
||||
ds = append(ds, KV(key, args[i+1])) |
||||
} else { |
||||
Warn("log: key must be string, get %T, ignored", args[i]) |
||||
} |
||||
} |
||||
return ds |
||||
} |
@ -0,0 +1,83 @@ |
||||
package log |
||||
|
||||
import ( |
||||
"context" |
||||
"fmt" |
||||
"path/filepath" |
||||
"runtime" |
||||
"strings" |
||||
) |
||||
|
||||
// V reports whether verbosity at the call site is at least the requested level.
|
||||
// The returned value is a boolean of type Verbose, which implements Info, Infov etc.
|
||||
// These methods will write to the Info log if called.
|
||||
// Thus, one may write either
|
||||
// if log.V(2) { log.Info("log this") }
|
||||
// or
|
||||
// log.V(2).Info("log this")
|
||||
// The second form is shorter but the first is cheaper if logging is off because it does
|
||||
// not evaluate its arguments.
|
||||
//
|
||||
// Whether an individual call to V generates a log record depends on the setting of
|
||||
// the Config.VLevel and Config.Module flags; both are off by default. If the level in the call to
|
||||
// V is at least the value of Config.VLevel, or of Config.Module for the source file containing the
|
||||
// call, the V call will log.
|
||||
// v must be more than 0.
|
||||
func V(v int32) Verbose { |
||||
var ( |
||||
file string |
||||
) |
||||
if v < 0 { |
||||
return Verbose(false) |
||||
} else if c.V >= v { |
||||
return Verbose(true) |
||||
} |
||||
if pc, _, _, ok := runtime.Caller(1); ok { |
||||
file, _ = runtime.FuncForPC(pc).FileLine(pc) |
||||
} |
||||
if strings.HasSuffix(file, ".go") { |
||||
file = file[:len(file)-3] |
||||
} |
||||
if slash := strings.LastIndex(file, "/"); slash >= 0 { |
||||
file = file[slash+1:] |
||||
} |
||||
for filter, lvl := range c.Module { |
||||
var match bool |
||||
if match = filter == file; !match { |
||||
match, _ = filepath.Match(filter, file) |
||||
} |
||||
if match { |
||||
return Verbose(lvl >= v) |
||||
} |
||||
} |
||||
return Verbose(false) |
||||
} |
||||
|
||||
// Info logs a message at the info log level.
|
||||
func (v Verbose) Info(format string, args ...interface{}) { |
||||
if v { |
||||
h.Log(context.Background(), _infoLevel, KV(_log, fmt.Sprintf(format, args...))) |
||||
} |
||||
} |
||||
|
||||
// Infov logs a message at the info log level.
|
||||
func (v Verbose) Infov(ctx context.Context, args ...D) { |
||||
if v { |
||||
h.Log(ctx, _infoLevel, args...) |
||||
} |
||||
} |
||||
|
||||
// Infow logs a message with some additional context. The variadic key-value pairs are treated as they are in With.
|
||||
func (v Verbose) Infow(ctx context.Context, args ...interface{}) { |
||||
if v { |
||||
h.Log(ctx, _infoLevel, logw(args)...) |
||||
} |
||||
} |
||||
|
||||
// Close close resource.
|
||||
func (v Verbose) Close() (err error) { |
||||
if h == nil { |
||||
return |
||||
} |
||||
return h.Close() |
||||
} |
@ -0,0 +1,684 @@ |
||||
package discovery |
||||
|
||||
import ( |
||||
"context" |
||||
"encoding/json" |
||||
"errors" |
||||
"fmt" |
||||
"math/rand" |
||||
"net/url" |
||||
"os" |
||||
"strconv" |
||||
"strings" |
||||
"sync" |
||||
"sync/atomic" |
||||
"time" |
||||
|
||||
"github.com/bilibili/Kratos/pkg/conf/env" |
||||
"github.com/bilibili/Kratos/pkg/ecode" |
||||
"github.com/bilibili/Kratos/pkg/log" |
||||
"github.com/bilibili/Kratos/pkg/naming" |
||||
bm "github.com/bilibili/Kratos/pkg/net/http/blademaster" |
||||
"github.com/bilibili/Kratos/pkg/net/netutil" |
||||
"github.com/bilibili/Kratos/pkg/net/netutil/breaker" |
||||
"github.com/bilibili/Kratos/pkg/str" |
||||
xtime "github.com/bilibili/Kratos/pkg/time" |
||||
) |
||||
|
||||
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{} |
||||
|
||||
// ErrDuplication duplication treeid.
|
||||
ErrDuplication = errors.New("discovery: instance duplicate registration") |
||||
) |
||||
|
||||
// Config discovery configures.
|
||||
type Config struct { |
||||
Nodes []string |
||||
Key string |
||||
Secret string |
||||
Region string |
||||
Zone string |
||||
Env string |
||||
Host string |
||||
} |
||||
|
||||
// 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) |
||||
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 |
||||
} |
||||
instances, ok := resolver.Fetch(context.Background()) |
||||
if ok { |
||||
d.newSelf(instances) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func (d *Discovery) newSelf(instances naming.InstancesInfo) { |
||||
ins, ok := instances.Instances[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 |
||||
} |
||||
} |
||||
rand.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 naming.InstancesInfo, ok bool) { |
||||
r.d.mutex.RLock() |
||||
app, ok := r.d.apps[r.id] |
||||
r.d.mutex.RUnlock() |
||||
if ok { |
||||
ins, ok = app.zoneIns.Load().(naming.InstancesInfo) |
||||
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 |
||||
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 |
||||
default: |
||||
} |
||||
|
||||
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]naming.InstancesInfo, 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]naming.InstancesInfo `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]naming.InstancesInfo) { |
||||
for id, v := range apps { |
||||
var count int |
||||
for zone, ins := range v.Instances { |
||||
if len(ins) == 0 { |
||||
delete(v.Instances, 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) |
||||
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 |
||||
} |
@ -0,0 +1,429 @@ |
||||
package blademaster |
||||
|
||||
import ( |
||||
"bytes" |
||||
"context" |
||||
"crypto/md5" |
||||
"crypto/tls" |
||||
"encoding/hex" |
||||
"encoding/json" |
||||
"fmt" |
||||
"io" |
||||
"net" |
||||
xhttp "net/http" |
||||
"net/url" |
||||
"os" |
||||
"runtime" |
||||
"strconv" |
||||
"strings" |
||||
"sync" |
||||
"time" |
||||
|
||||
"github.com/bilibili/Kratos/pkg/conf/env" |
||||
"github.com/bilibili/Kratos/pkg/log" |
||||
"github.com/bilibili/Kratos/pkg/net/metadata" |
||||
"github.com/bilibili/Kratos/pkg/net/netutil/breaker" |
||||
"github.com/bilibili/Kratos/pkg/stat" |
||||
xtime "github.com/bilibili/Kratos/pkg/time" |
||||
|
||||
"github.com/gogo/protobuf/proto" |
||||
pkgerr "github.com/pkg/errors" |
||||
) |
||||
|
||||
const ( |
||||
_minRead = 16 * 1024 // 16kb
|
||||
|
||||
_appKey = "appkey" |
||||
_appSecret = "appsecret" |
||||
_ts = "ts" |
||||
) |
||||
|
||||
var ( |
||||
_noKickUserAgent = "haoguanwei@bilibili.com " |
||||
clientStats = stat.HTTPClient |
||||
) |
||||
|
||||
func init() { |
||||
n, err := os.Hostname() |
||||
if err == nil { |
||||
_noKickUserAgent = _noKickUserAgent + runtime.Version() + " " + n |
||||
} |
||||
} |
||||
|
||||
// App bilibili intranet authorization.
|
||||
type App struct { |
||||
Key string |
||||
Secret string |
||||
} |
||||
|
||||
// ClientConfig is http client conf.
|
||||
type ClientConfig struct { |
||||
*App |
||||
Dial xtime.Duration |
||||
Timeout xtime.Duration |
||||
KeepAlive xtime.Duration |
||||
Breaker *breaker.Config |
||||
URL map[string]*ClientConfig |
||||
Host map[string]*ClientConfig |
||||
} |
||||
|
||||
// Client is http client.
|
||||
type Client struct { |
||||
conf *ClientConfig |
||||
client *xhttp.Client |
||||
dialer *net.Dialer |
||||
transport xhttp.RoundTripper |
||||
|
||||
urlConf map[string]*ClientConfig |
||||
hostConf map[string]*ClientConfig |
||||
mutex sync.RWMutex |
||||
breaker *breaker.Group |
||||
} |
||||
|
||||
// NewClient new a http client.
|
||||
func NewClient(c *ClientConfig) *Client { |
||||
client := new(Client) |
||||
client.conf = c |
||||
client.dialer = &net.Dialer{ |
||||
Timeout: time.Duration(c.Dial), |
||||
KeepAlive: time.Duration(c.KeepAlive), |
||||
} |
||||
|
||||
originTransport := &xhttp.Transport{ |
||||
DialContext: client.dialer.DialContext, |
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, |
||||
} |
||||
|
||||
// wraps RoundTripper for tracer
|
||||
client.transport = &TraceTransport{RoundTripper: originTransport} |
||||
client.client = &xhttp.Client{ |
||||
Transport: client.transport, |
||||
} |
||||
client.urlConf = make(map[string]*ClientConfig) |
||||
client.hostConf = make(map[string]*ClientConfig) |
||||
client.breaker = breaker.NewGroup(c.Breaker) |
||||
// check appkey
|
||||
if c.Key == "" || c.Secret == "" { |
||||
panic("http client must config appkey and appsecret") |
||||
} |
||||
if c.Timeout <= 0 { |
||||
panic("must config http timeout!!!") |
||||
} |
||||
for uri, cfg := range c.URL { |
||||
client.urlConf[uri] = cfg |
||||
} |
||||
for host, cfg := range c.Host { |
||||
client.hostConf[host] = cfg |
||||
} |
||||
return client |
||||
} |
||||
|
||||
// SetTransport set client transport
|
||||
func (client *Client) SetTransport(t xhttp.RoundTripper) { |
||||
client.transport = t |
||||
client.client.Transport = t |
||||
} |
||||
|
||||
// SetConfig set client config.
|
||||
func (client *Client) SetConfig(c *ClientConfig) { |
||||
client.mutex.Lock() |
||||
if c.App != nil { |
||||
client.conf.App.Key = c.App.Key |
||||
client.conf.App.Secret = c.App.Secret |
||||
} |
||||
if c.Timeout > 0 { |
||||
client.conf.Timeout = c.Timeout |
||||
} |
||||
if c.KeepAlive > 0 { |
||||
client.dialer.KeepAlive = time.Duration(c.KeepAlive) |
||||
client.conf.KeepAlive = c.KeepAlive |
||||
} |
||||
if c.Dial > 0 { |
||||
client.dialer.Timeout = time.Duration(c.Dial) |
||||
client.conf.Timeout = c.Dial |
||||
} |
||||
if c.Breaker != nil { |
||||
client.conf.Breaker = c.Breaker |
||||
client.breaker.Reload(c.Breaker) |
||||
} |
||||
for uri, cfg := range c.URL { |
||||
client.urlConf[uri] = cfg |
||||
} |
||||
for host, cfg := range c.Host { |
||||
client.hostConf[host] = cfg |
||||
} |
||||
client.mutex.Unlock() |
||||
} |
||||
|
||||
// NewRequest new http request with method, uri, ip, values and headers.
|
||||
// TODO(zhoujiahui): param realIP should be removed later.
|
||||
func (client *Client) NewRequest(method, uri, realIP string, params url.Values) (req *xhttp.Request, err error) { |
||||
enc, err := client.sign(params) |
||||
if err != nil { |
||||
err = pkgerr.Wrapf(err, "uri:%s,params:%v", uri, params) |
||||
return |
||||
} |
||||
ru := uri |
||||
if enc != "" { |
||||
ru = uri + "?" + enc |
||||
} |
||||
if method == xhttp.MethodGet { |
||||
req, err = xhttp.NewRequest(xhttp.MethodGet, ru, nil) |
||||
} else { |
||||
req, err = xhttp.NewRequest(xhttp.MethodPost, uri, strings.NewReader(enc)) |
||||
} |
||||
if err != nil { |
||||
err = pkgerr.Wrapf(err, "method:%s,uri:%s", method, ru) |
||||
return |
||||
} |
||||
const ( |
||||
_contentType = "Content-Type" |
||||
_urlencoded = "application/x-www-form-urlencoded" |
||||
_userAgent = "User-Agent" |
||||
) |
||||
if method == xhttp.MethodPost { |
||||
req.Header.Set(_contentType, _urlencoded) |
||||
} |
||||
if realIP != "" { |
||||
req.Header.Set(_httpHeaderRemoteIP, realIP) |
||||
} |
||||
req.Header.Set(_userAgent, _noKickUserAgent+" "+env.AppID) |
||||
return |
||||
} |
||||
|
||||
// Get issues a GET to the specified URL.
|
||||
func (client *Client) Get(c context.Context, uri, ip string, params url.Values, res interface{}) (err error) { |
||||
req, err := client.NewRequest(xhttp.MethodGet, uri, ip, params) |
||||
if err != nil { |
||||
return |
||||
} |
||||
return client.Do(c, req, res) |
||||
} |
||||
|
||||
// Post issues a Post to the specified URL.
|
||||
func (client *Client) Post(c context.Context, uri, ip string, params url.Values, res interface{}) (err error) { |
||||
req, err := client.NewRequest(xhttp.MethodPost, uri, ip, params) |
||||
if err != nil { |
||||
return |
||||
} |
||||
return client.Do(c, req, res) |
||||
} |
||||
|
||||
// RESTfulGet issues a RESTful GET to the specified URL.
|
||||
func (client *Client) RESTfulGet(c context.Context, uri, ip string, params url.Values, res interface{}, v ...interface{}) (err error) { |
||||
req, err := client.NewRequest(xhttp.MethodGet, fmt.Sprintf(uri, v...), ip, params) |
||||
if err != nil { |
||||
return |
||||
} |
||||
return client.Do(c, req, res, uri) |
||||
} |
||||
|
||||
// RESTfulPost issues a RESTful Post to the specified URL.
|
||||
func (client *Client) RESTfulPost(c context.Context, uri, ip string, params url.Values, res interface{}, v ...interface{}) (err error) { |
||||
req, err := client.NewRequest(xhttp.MethodPost, fmt.Sprintf(uri, v...), ip, params) |
||||
if err != nil { |
||||
return |
||||
} |
||||
return client.Do(c, req, res, uri) |
||||
} |
||||
|
||||
// Raw sends an HTTP request and returns bytes response
|
||||
func (client *Client) Raw(c context.Context, req *xhttp.Request, v ...string) (bs []byte, err error) { |
||||
var ( |
||||
ok bool |
||||
code string |
||||
cancel func() |
||||
resp *xhttp.Response |
||||
config *ClientConfig |
||||
timeout time.Duration |
||||
uri = fmt.Sprintf("%s://%s%s", req.URL.Scheme, req.Host, req.URL.Path) |
||||
) |
||||
// NOTE fix prom & config uri key.
|
||||
if len(v) == 1 { |
||||
uri = v[0] |
||||
} |
||||
// breaker
|
||||
brk := client.breaker.Get(uri) |
||||
if err = brk.Allow(); err != nil { |
||||
code = "breaker" |
||||
clientStats.Incr(uri, code) |
||||
return |
||||
} |
||||
defer client.onBreaker(brk, &err) |
||||
// stat
|
||||
now := time.Now() |
||||
defer func() { |
||||
clientStats.Timing(uri, int64(time.Since(now)/time.Millisecond)) |
||||
if code != "" { |
||||
clientStats.Incr(uri, code) |
||||
} |
||||
}() |
||||
// get config
|
||||
// 1.url config 2.host config 3.default
|
||||
client.mutex.RLock() |
||||
if config, ok = client.urlConf[uri]; !ok { |
||||
if config, ok = client.hostConf[req.Host]; !ok { |
||||
config = client.conf |
||||
} |
||||
} |
||||
client.mutex.RUnlock() |
||||
// timeout
|
||||
deliver := true |
||||
timeout = time.Duration(config.Timeout) |
||||
if deadline, ok := c.Deadline(); ok { |
||||
if ctimeout := time.Until(deadline); ctimeout < timeout { |
||||
// deliver small timeout
|
||||
timeout = ctimeout |
||||
deliver = false |
||||
} |
||||
} |
||||
if deliver { |
||||
c, cancel = context.WithTimeout(c, timeout) |
||||
defer cancel() |
||||
} |
||||
setTimeout(req, timeout) |
||||
req = req.WithContext(c) |
||||
setCaller(req) |
||||
if color := metadata.String(c, metadata.Color); color != "" { |
||||
setColor(req, color) |
||||
} |
||||
if resp, err = client.client.Do(req); err != nil { |
||||
err = pkgerr.Wrapf(err, "host:%s, url:%s", req.URL.Host, realURL(req)) |
||||
code = "failed" |
||||
return |
||||
} |
||||
defer resp.Body.Close() |
||||
if resp.StatusCode >= xhttp.StatusBadRequest { |
||||
err = pkgerr.Errorf("incorrect http status:%d host:%s, url:%s", resp.StatusCode, req.URL.Host, realURL(req)) |
||||
code = strconv.Itoa(resp.StatusCode) |
||||
return |
||||
} |
||||
if bs, err = readAll(resp.Body, _minRead); err != nil { |
||||
err = pkgerr.Wrapf(err, "host:%s, url:%s", req.URL.Host, realURL(req)) |
||||
return |
||||
} |
||||
return |
||||
} |
||||
|
||||
// Do sends an HTTP request and returns an HTTP json response.
|
||||
func (client *Client) Do(c context.Context, req *xhttp.Request, res interface{}, v ...string) (err error) { |
||||
var bs []byte |
||||
if bs, err = client.Raw(c, req, v...); err != nil { |
||||
return |
||||
} |
||||
if res != nil { |
||||
if err = json.Unmarshal(bs, res); err != nil { |
||||
err = pkgerr.Wrapf(err, "host:%s, url:%s", req.URL.Host, realURL(req)) |
||||
} |
||||
} |
||||
return |
||||
} |
||||
|
||||
// JSON sends an HTTP request and returns an HTTP json response.
|
||||
func (client *Client) JSON(c context.Context, req *xhttp.Request, res interface{}, v ...string) (err error) { |
||||
var bs []byte |
||||
if bs, err = client.Raw(c, req, v...); err != nil { |
||||
return |
||||
} |
||||
if res != nil { |
||||
if err = json.Unmarshal(bs, res); err != nil { |
||||
err = pkgerr.Wrapf(err, "host:%s, url:%s", req.URL.Host, realURL(req)) |
||||
} |
||||
} |
||||
return |
||||
} |
||||
|
||||
// PB sends an HTTP request and returns an HTTP proto response.
|
||||
func (client *Client) PB(c context.Context, req *xhttp.Request, res proto.Message, v ...string) (err error) { |
||||
var bs []byte |
||||
if bs, err = client.Raw(c, req, v...); err != nil { |
||||
return |
||||
} |
||||
if res != nil { |
||||
if err = proto.Unmarshal(bs, res); err != nil { |
||||
err = pkgerr.Wrapf(err, "host:%s, url:%s", req.URL.Host, realURL(req)) |
||||
} |
||||
} |
||||
return |
||||
} |
||||
|
||||
func (client *Client) onBreaker(breaker breaker.Breaker, err *error) { |
||||
if err != nil && *err != nil { |
||||
breaker.MarkFailed() |
||||
} else { |
||||
breaker.MarkSuccess() |
||||
} |
||||
} |
||||
|
||||
// sign calc appkey and appsecret sign.
|
||||
func (client *Client) sign(params url.Values) (query string, err error) { |
||||
client.mutex.RLock() |
||||
key := client.conf.Key |
||||
secret := client.conf.Secret |
||||
client.mutex.RUnlock() |
||||
if params == nil { |
||||
params = url.Values{} |
||||
} |
||||
params.Set(_appKey, key) |
||||
if params.Get(_appSecret) != "" { |
||||
log.Warn("utils http get must not have parameter appSecret") |
||||
} |
||||
if params.Get(_ts) == "" { |
||||
params.Set(_ts, strconv.FormatInt(time.Now().Unix(), 10)) |
||||
} |
||||
tmp := params.Encode() |
||||
if strings.IndexByte(tmp, '+') > -1 { |
||||
tmp = strings.Replace(tmp, "+", "%20", -1) |
||||
} |
||||
var b bytes.Buffer |
||||
b.WriteString(tmp) |
||||
b.WriteString(secret) |
||||
mh := md5.Sum(b.Bytes()) |
||||
// query
|
||||
var qb bytes.Buffer |
||||
qb.WriteString(tmp) |
||||
qb.WriteString("&sign=") |
||||
qb.WriteString(hex.EncodeToString(mh[:])) |
||||
query = qb.String() |
||||
return |
||||
} |
||||
|
||||
// realUrl return url with http://host/params.
|
||||
func realURL(req *xhttp.Request) string { |
||||
if req.Method == xhttp.MethodGet { |
||||
return req.URL.String() |
||||
} else if req.Method == xhttp.MethodPost { |
||||
ru := req.URL.Path |
||||
if req.Body != nil { |
||||
rd, ok := req.Body.(io.Reader) |
||||
if ok { |
||||
buf := bytes.NewBuffer([]byte{}) |
||||
buf.ReadFrom(rd) |
||||
ru = ru + "?" + buf.String() |
||||
} |
||||
} |
||||
return ru |
||||
} |
||||
return req.URL.Path |
||||
} |
||||
|
||||
// readAll reads from r until an error or EOF and returns the data it read
|
||||
// from the internal buffer allocated with a specified capacity.
|
||||
func readAll(r io.Reader, capacity int64) (b []byte, err error) { |
||||
buf := bytes.NewBuffer(make([]byte, 0, capacity)) |
||||
// If the buffer overflows, we will get bytes.ErrTooLarge.
|
||||
// Return that as an error. Any other panic remains.
|
||||
defer func() { |
||||
e := recover() |
||||
if e == nil { |
||||
return |
||||
} |
||||
if panicErr, ok := e.(error); ok && panicErr == bytes.ErrTooLarge { |
||||
err = panicErr |
||||
} else { |
||||
panic(e) |
||||
} |
||||
}() |
||||
_, err = buf.ReadFrom(r) |
||||
return buf.Bytes(), err |
||||
} |
@ -0,0 +1,106 @@ |
||||
package blademaster |
||||
|
||||
import ( |
||||
"net/http" |
||||
"strconv" |
||||
"strings" |
||||
"time" |
||||
|
||||
"github.com/bilibili/Kratos/pkg/conf/env" |
||||
"github.com/bilibili/Kratos/pkg/log" |
||||
|
||||
"github.com/pkg/errors" |
||||
) |
||||
|
||||
const ( |
||||
// http head
|
||||
_httpHeaderUser = "x1-bmspy-user" |
||||
_httpHeaderColor = "x1-bmspy-color" |
||||
_httpHeaderTimeout = "x1-bmspy-timeout" |
||||
_httpHeaderRemoteIP = "x-backend-bm-real-ip" |
||||
_httpHeaderRemoteIPPort = "x-backend-bm-real-ipport" |
||||
) |
||||
|
||||
// mirror return true if x1-bilispy-mirror in http header and its value is 1 or true.
|
||||
func mirror(req *http.Request) bool { |
||||
mirrorStr := req.Header.Get("x1-bilispy-mirror") |
||||
if mirrorStr == "" { |
||||
return false |
||||
} |
||||
val, err := strconv.ParseBool(mirrorStr) |
||||
if err != nil { |
||||
log.Warn("blademaster: failed to parse mirror: %+v", errors.Wrap(err, mirrorStr)) |
||||
return false |
||||
} |
||||
if !val { |
||||
log.Warn("blademaster: request mirrorStr value :%s is false", mirrorStr) |
||||
} |
||||
return val |
||||
} |
||||
|
||||
// setCaller set caller into http request.
|
||||
func setCaller(req *http.Request) { |
||||
req.Header.Set(_httpHeaderUser, env.AppID) |
||||
} |
||||
|
||||
// caller get caller from http request.
|
||||
func caller(req *http.Request) string { |
||||
return req.Header.Get(_httpHeaderUser) |
||||
} |
||||
|
||||
// setColor set color into http request.
|
||||
func setColor(req *http.Request, color string) { |
||||
req.Header.Set(_httpHeaderColor, color) |
||||
} |
||||
|
||||
// color get color from http request.
|
||||
func color(req *http.Request) string { |
||||
c := req.Header.Get(_httpHeaderColor) |
||||
if c == "" { |
||||
c = env.Color |
||||
} |
||||
return c |
||||
} |
||||
|
||||
// setTimeout set timeout into http request.
|
||||
func setTimeout(req *http.Request, timeout time.Duration) { |
||||
td := int64(timeout / time.Millisecond) |
||||
req.Header.Set(_httpHeaderTimeout, strconv.FormatInt(td, 10)) |
||||
} |
||||
|
||||
// timeout get timeout from http request.
|
||||
func timeout(req *http.Request) time.Duration { |
||||
to := req.Header.Get(_httpHeaderTimeout) |
||||
timeout, err := strconv.ParseInt(to, 10, 64) |
||||
if err == nil && timeout > 20 { |
||||
timeout -= 20 // reduce 20ms every time.
|
||||
} |
||||
return time.Duration(timeout) * time.Millisecond |
||||
} |
||||
|
||||
// remoteIP implements a best effort algorithm to return the real client IP, it parses
|
||||
// X-BACKEND-BILI-REAL-IP or X-Real-IP or X-Forwarded-For in order to work properly with reverse-proxies such us: nginx or haproxy.
|
||||
// Use X-Forwarded-For before X-Real-Ip as nginx uses X-Real-Ip with the proxy's IP.
|
||||
func remoteIP(req *http.Request) (remote string) { |
||||
if remote = req.Header.Get(_httpHeaderRemoteIP); remote != "" && remote != "null" { |
||||
return |
||||
} |
||||
var xff = req.Header.Get("X-Forwarded-For") |
||||
if idx := strings.IndexByte(xff, ','); idx > -1 { |
||||
if remote = strings.TrimSpace(xff[:idx]); remote != "" { |
||||
return |
||||
} |
||||
} |
||||
if remote = req.Header.Get("X-Real-IP"); remote != "" { |
||||
return |
||||
} |
||||
remote = req.RemoteAddr[:strings.Index(req.RemoteAddr, ":")] |
||||
return |
||||
} |
||||
|
||||
func remotePort(req *http.Request) (port string) { |
||||
if port = req.Header.Get(_httpHeaderRemoteIPPort); port != "" && port != "null" { |
||||
return |
||||
} |
||||
return |
||||
} |
@ -0,0 +1,199 @@ |
||||
package blademaster |
||||
|
||||
import ( |
||||
"io" |
||||
"net/http" |
||||
"net/http/httptrace" |
||||
|
||||
"github.com/bilibili/Kratos/pkg/net/trace" |
||||
) |
||||
|
||||
const _defaultComponentName = "net/http" |
||||
|
||||
type closeTracker struct { |
||||
io.ReadCloser |
||||
tr trace.Trace |
||||
} |
||||
|
||||
func (c *closeTracker) Close() error { |
||||
err := c.ReadCloser.Close() |
||||
c.tr.SetLog(trace.Log(trace.LogEvent, "ClosedBody")) |
||||
c.tr.Finish(&err) |
||||
return err |
||||
} |
||||
|
||||
// NewTraceTracesport NewTraceTracesport
|
||||
func NewTraceTracesport(rt http.RoundTripper, peerService string, internalTags ...trace.Tag) *TraceTransport { |
||||
return &TraceTransport{RoundTripper: rt, peerService: peerService, internalTags: internalTags} |
||||
} |
||||
|
||||
// TraceTransport wraps a RoundTripper. If a request is being traced with
|
||||
// Tracer, Transport will inject the current span into the headers,
|
||||
// and set HTTP related tags on the span.
|
||||
type TraceTransport struct { |
||||
peerService string |
||||
internalTags []trace.Tag |
||||
// The actual RoundTripper to use for the request. A nil
|
||||
// RoundTripper defaults to http.DefaultTransport.
|
||||
http.RoundTripper |
||||
} |
||||
|
||||
// RoundTrip implements the RoundTripper interface
|
||||
func (t *TraceTransport) RoundTrip(req *http.Request) (*http.Response, error) { |
||||
rt := t.RoundTripper |
||||
if rt == nil { |
||||
rt = http.DefaultTransport |
||||
} |
||||
tr, ok := trace.FromContext(req.Context()) |
||||
if !ok { |
||||
return rt.RoundTrip(req) |
||||
} |
||||
operationName := "HTTP:" + req.Method |
||||
// fork new trace
|
||||
tr = tr.Fork("", operationName) |
||||
|
||||
tr.SetTag(trace.TagString(trace.TagComponent, _defaultComponentName)) |
||||
tr.SetTag(trace.TagString(trace.TagHTTPMethod, req.Method)) |
||||
tr.SetTag(trace.TagString(trace.TagHTTPURL, req.URL.String())) |
||||
tr.SetTag(trace.TagString(trace.TagSpanKind, "client")) |
||||
if t.peerService != "" { |
||||
tr.SetTag(trace.TagString(trace.TagPeerService, t.peerService)) |
||||
} |
||||
tr.SetTag(t.internalTags...) |
||||
|
||||
// inject trace to http header
|
||||
trace.Inject(tr, trace.HTTPFormat, req.Header) |
||||
|
||||
// FIXME: uncomment after trace sdk is goroutinue safe
|
||||
// ct := clientTracer{tr: tr}
|
||||
// req = req.WithContext(httptrace.WithClientTrace(req.Context(), ct.clientTrace()))
|
||||
resp, err := rt.RoundTrip(req) |
||||
|
||||
if err != nil { |
||||
tr.SetTag(trace.TagBool(trace.TagError, true)) |
||||
tr.Finish(&err) |
||||
return resp, err |
||||
} |
||||
|
||||
// TODO: get ecode
|
||||
tr.SetTag(trace.TagInt64(trace.TagHTTPStatusCode, int64(resp.StatusCode))) |
||||
|
||||
if req.Method == "HEAD" { |
||||
tr.Finish(nil) |
||||
} else { |
||||
resp.Body = &closeTracker{resp.Body, tr} |
||||
} |
||||
return resp, err |
||||
} |
||||
|
||||
type clientTracer struct { |
||||
tr trace.Trace |
||||
} |
||||
|
||||
func (h *clientTracer) clientTrace() *httptrace.ClientTrace { |
||||
return &httptrace.ClientTrace{ |
||||
GetConn: h.getConn, |
||||
GotConn: h.gotConn, |
||||
PutIdleConn: h.putIdleConn, |
||||
GotFirstResponseByte: h.gotFirstResponseByte, |
||||
Got100Continue: h.got100Continue, |
||||
DNSStart: h.dnsStart, |
||||
DNSDone: h.dnsDone, |
||||
ConnectStart: h.connectStart, |
||||
ConnectDone: h.connectDone, |
||||
WroteHeaders: h.wroteHeaders, |
||||
Wait100Continue: h.wait100Continue, |
||||
WroteRequest: h.wroteRequest, |
||||
} |
||||
} |
||||
|
||||
func (h *clientTracer) getConn(hostPort string) { |
||||
// ext.HTTPUrl.Set(h.sp, hostPort)
|
||||
h.tr.SetLog(trace.Log(trace.LogEvent, "GetConn")) |
||||
} |
||||
|
||||
func (h *clientTracer) gotConn(info httptrace.GotConnInfo) { |
||||
h.tr.SetTag(trace.TagBool("net/http.reused", info.Reused)) |
||||
h.tr.SetTag(trace.TagBool("net/http.was_idle", info.WasIdle)) |
||||
h.tr.SetLog(trace.Log(trace.LogEvent, "GotConn")) |
||||
} |
||||
|
||||
func (h *clientTracer) putIdleConn(error) { |
||||
h.tr.SetLog(trace.Log(trace.LogEvent, "PutIdleConn")) |
||||
} |
||||
|
||||
func (h *clientTracer) gotFirstResponseByte() { |
||||
h.tr.SetLog(trace.Log(trace.LogEvent, "GotFirstResponseByte")) |
||||
} |
||||
|
||||
func (h *clientTracer) got100Continue() { |
||||
h.tr.SetLog(trace.Log(trace.LogEvent, "Got100Continue")) |
||||
} |
||||
|
||||
func (h *clientTracer) dnsStart(info httptrace.DNSStartInfo) { |
||||
h.tr.SetLog( |
||||
trace.Log(trace.LogEvent, "DNSStart"), |
||||
trace.Log("host", info.Host), |
||||
) |
||||
} |
||||
|
||||
func (h *clientTracer) dnsDone(info httptrace.DNSDoneInfo) { |
||||
fields := []trace.LogField{trace.Log(trace.LogEvent, "DNSDone")} |
||||
for _, addr := range info.Addrs { |
||||
fields = append(fields, trace.Log("addr", addr.String())) |
||||
} |
||||
if info.Err != nil { |
||||
// TODO: support log error object
|
||||
fields = append(fields, trace.Log(trace.LogErrorObject, info.Err.Error())) |
||||
} |
||||
h.tr.SetLog(fields...) |
||||
} |
||||
|
||||
func (h *clientTracer) connectStart(network, addr string) { |
||||
h.tr.SetLog( |
||||
trace.Log(trace.LogEvent, "ConnectStart"), |
||||
trace.Log("network", network), |
||||
trace.Log("addr", addr), |
||||
) |
||||
} |
||||
|
||||
func (h *clientTracer) connectDone(network, addr string, err error) { |
||||
if err != nil { |
||||
h.tr.SetLog( |
||||
trace.Log("message", "ConnectDone"), |
||||
trace.Log("network", network), |
||||
trace.Log("addr", addr), |
||||
trace.Log(trace.LogEvent, "error"), |
||||
// TODO: support log error object
|
||||
trace.Log(trace.LogErrorObject, err.Error()), |
||||
) |
||||
} else { |
||||
h.tr.SetLog( |
||||
trace.Log(trace.LogEvent, "ConnectDone"), |
||||
trace.Log("network", network), |
||||
trace.Log("addr", addr), |
||||
) |
||||
} |
||||
} |
||||
|
||||
func (h *clientTracer) wroteHeaders() { |
||||
h.tr.SetLog(trace.Log("event", "WroteHeaders")) |
||||
} |
||||
|
||||
func (h *clientTracer) wait100Continue() { |
||||
h.tr.SetLog(trace.Log("event", "Wait100Continue")) |
||||
} |
||||
|
||||
func (h *clientTracer) wroteRequest(info httptrace.WroteRequestInfo) { |
||||
if info.Err != nil { |
||||
h.tr.SetLog( |
||||
trace.Log("message", "WroteRequest"), |
||||
trace.Log("event", "error"), |
||||
// TODO: support log error object
|
||||
trace.Log(trace.LogErrorObject, info.Err.Error()), |
||||
) |
||||
h.tr.SetTag(trace.TagBool(trace.TagError, true)) |
||||
} else { |
||||
h.tr.SetLog(trace.Log("event", "WroteRequest")) |
||||
} |
||||
} |
@ -0,0 +1,72 @@ |
||||
package netutil |
||||
|
||||
import ( |
||||
"math/rand" |
||||
"time" |
||||
) |
||||
|
||||
// DefaultBackoffConfig uses values specified for backoff in common.
|
||||
var DefaultBackoffConfig = BackoffConfig{ |
||||
MaxDelay: 120 * time.Second, |
||||
BaseDelay: 1.0 * time.Second, |
||||
Factor: 1.6, |
||||
Jitter: 0.2, |
||||
} |
||||
|
||||
// Backoff defines the methodology for backing off after a call failure.
|
||||
type Backoff interface { |
||||
// Backoff returns the amount of time to wait before the next retry given
|
||||
// the number of consecutive failures.
|
||||
Backoff(retries int) time.Duration |
||||
} |
||||
|
||||
// BackoffConfig defines the parameters for the default backoff strategy.
|
||||
type BackoffConfig struct { |
||||
// MaxDelay is the upper bound of backoff delay.
|
||||
MaxDelay time.Duration |
||||
|
||||
// baseDelay is the amount of time to wait before retrying after the first
|
||||
// failure.
|
||||
BaseDelay time.Duration |
||||
|
||||
// factor is applied to the backoff after each retry.
|
||||
Factor float64 |
||||
|
||||
// jitter provides a range to randomize backoff delays.
|
||||
Jitter float64 |
||||
} |
||||
|
||||
/* |
||||
// NOTE TODO avoid use unexcept config.
|
||||
func (bc *BackoffConfig) Fix() { |
||||
md := bc.MaxDelay |
||||
*bc = DefaultBackoffConfig |
||||
|
||||
if md > 0 { |
||||
bc.MaxDelay = md |
||||
} |
||||
} |
||||
*/ |
||||
|
||||
// Backoff returns the amount of time to wait before the next retry given
|
||||
// the number of consecutive failures.
|
||||
func (bc *BackoffConfig) Backoff(retries int) time.Duration { |
||||
if retries == 0 { |
||||
return bc.BaseDelay |
||||
} |
||||
backoff, max := float64(bc.BaseDelay), float64(bc.MaxDelay) |
||||
for backoff < max && retries > 0 { |
||||
backoff *= bc.Factor |
||||
retries-- |
||||
} |
||||
if backoff > max { |
||||
backoff = max |
||||
} |
||||
// Randomize backoff delays so that if a cluster of requests start at
|
||||
// the same time, they won't operate in lockstep.
|
||||
backoff *= 1 + bc.Jitter*(rand.Float64()*2-1) |
||||
if backoff < 0 { |
||||
return 0 |
||||
} |
||||
return time.Duration(backoff) |
||||
} |
@ -0,0 +1,176 @@ |
||||
package breaker |
||||
|
||||
import ( |
||||
"sync" |
||||
"time" |
||||
|
||||
xtime "go-common/library/time" |
||||
) |
||||
|
||||
// Config broker config.
|
||||
type Config struct { |
||||
SwitchOff bool // breaker switch,default off.
|
||||
|
||||
// Hystrix
|
||||
Ratio float32 |
||||
Sleep xtime.Duration |
||||
|
||||
// Google
|
||||
K float64 |
||||
|
||||
Window xtime.Duration |
||||
Bucket int |
||||
Request int64 |
||||
} |
||||
|
||||
func (conf *Config) fix() { |
||||
if conf.K == 0 { |
||||
conf.K = 1.5 |
||||
} |
||||
if conf.Request == 0 { |
||||
conf.Request = 100 |
||||
} |
||||
if conf.Ratio == 0 { |
||||
conf.Ratio = 0.5 |
||||
} |
||||
if conf.Sleep == 0 { |
||||
conf.Sleep = xtime.Duration(500 * time.Millisecond) |
||||
} |
||||
if conf.Bucket == 0 { |
||||
conf.Bucket = 10 |
||||
} |
||||
if conf.Window == 0 { |
||||
conf.Window = xtime.Duration(3 * time.Second) |
||||
} |
||||
} |
||||
|
||||
// Breaker is a CircuitBreaker pattern.
|
||||
// FIXME on int32 atomic.LoadInt32(&b.on) == _switchOn
|
||||
type Breaker interface { |
||||
Allow() error |
||||
MarkSuccess() |
||||
MarkFailed() |
||||
} |
||||
|
||||
// Group represents a class of CircuitBreaker and forms a namespace in which
|
||||
// units of CircuitBreaker.
|
||||
type Group struct { |
||||
mu sync.RWMutex |
||||
brks map[string]Breaker |
||||
conf *Config |
||||
} |
||||
|
||||
const ( |
||||
// StateOpen when circuit breaker open, request not allowed, after sleep
|
||||
// some duration, allow one single request for testing the health, if ok
|
||||
// then state reset to closed, if not continue the step.
|
||||
StateOpen int32 = iota |
||||
// StateClosed when circuit breaker closed, request allowed, the breaker
|
||||
// calc the succeed ratio, if request num greater request setting and
|
||||
// ratio lower than the setting ratio, then reset state to open.
|
||||
StateClosed |
||||
// StateHalfopen when circuit breaker open, after slepp some duration, allow
|
||||
// one request, but not state closed.
|
||||
StateHalfopen |
||||
|
||||
//_switchOn int32 = iota
|
||||
// _switchOff
|
||||
) |
||||
|
||||
var ( |
||||
_mu sync.RWMutex |
||||
_conf = &Config{ |
||||
Window: xtime.Duration(3 * time.Second), |
||||
Bucket: 10, |
||||
Request: 100, |
||||
|
||||
Sleep: xtime.Duration(500 * time.Millisecond), |
||||
Ratio: 0.5, |
||||
// Percentage of failures must be lower than 33.33%
|
||||
K: 1.5, |
||||
|
||||
// Pattern: "",
|
||||
} |
||||
_group = NewGroup(_conf) |
||||
) |
||||
|
||||
// Init init global breaker config, also can reload config after first time call.
|
||||
func Init(conf *Config) { |
||||
if conf == nil { |
||||
return |
||||
} |
||||
_mu.Lock() |
||||
_conf = conf |
||||
_mu.Unlock() |
||||
} |
||||
|
||||
// Go runs your function while tracking the breaker state of default group.
|
||||
func Go(name string, run, fallback func() error) error { |
||||
breaker := _group.Get(name) |
||||
if err := breaker.Allow(); err != nil { |
||||
return fallback() |
||||
} |
||||
return run() |
||||
} |
||||
|
||||
// newBreaker new a breaker.
|
||||
func newBreaker(c *Config) (b Breaker) { |
||||
// factory
|
||||
return newSRE(c) |
||||
} |
||||
|
||||
// NewGroup new a breaker group container, if conf nil use default conf.
|
||||
func NewGroup(conf *Config) *Group { |
||||
if conf == nil { |
||||
_mu.RLock() |
||||
conf = _conf |
||||
_mu.RUnlock() |
||||
} else { |
||||
conf.fix() |
||||
} |
||||
return &Group{ |
||||
conf: conf, |
||||
brks: make(map[string]Breaker), |
||||
} |
||||
} |
||||
|
||||
// Get get a breaker by a specified key, if breaker not exists then make a new one.
|
||||
func (g *Group) Get(key string) Breaker { |
||||
g.mu.RLock() |
||||
brk, ok := g.brks[key] |
||||
conf := g.conf |
||||
g.mu.RUnlock() |
||||
if ok { |
||||
return brk |
||||
} |
||||
// NOTE here may new multi breaker for rarely case, let gc drop it.
|
||||
brk = newBreaker(conf) |
||||
g.mu.Lock() |
||||
if _, ok = g.brks[key]; !ok { |
||||
g.brks[key] = brk |
||||
} |
||||
g.mu.Unlock() |
||||
return brk |
||||
} |
||||
|
||||
// Reload reload the group by specified config, this may let all inner breaker
|
||||
// reset to a new one.
|
||||
func (g *Group) Reload(conf *Config) { |
||||
if conf == nil { |
||||
return |
||||
} |
||||
conf.fix() |
||||
g.mu.Lock() |
||||
g.conf = conf |
||||
g.brks = make(map[string]Breaker, len(g.brks)) |
||||
g.mu.Unlock() |
||||
} |
||||
|
||||
// Go runs your function while tracking the breaker state of group.
|
||||
func (g *Group) Go(name string, run, fallback func() error) error { |
||||
breaker := g.Get(name) |
||||
if err := breaker.Allow(); err != nil { |
||||
return fallback() |
||||
} |
||||
return run() |
||||
} |
@ -0,0 +1,71 @@ |
||||
package breaker |
||||
|
||||
import ( |
||||
"math" |
||||
"math/rand" |
||||
"sync/atomic" |
||||
"time" |
||||
|
||||
"github.com/bilibili/Kratos/pkg/ecode" |
||||
"github.com/bilibili/Kratos/pkg/log" |
||||
"github.com/bilibili/Kratos/pkg/stat/summary" |
||||
) |
||||
|
||||
// sreBreaker is a sre CircuitBreaker pattern.
|
||||
type sreBreaker struct { |
||||
stat summary.Summary |
||||
|
||||
k float64 |
||||
request int64 |
||||
|
||||
state int32 |
||||
r *rand.Rand |
||||
} |
||||
|
||||
func newSRE(c *Config) Breaker { |
||||
return &sreBreaker{ |
||||
stat: summary.New(time.Duration(c.Window), c.Bucket), |
||||
r: rand.New(rand.NewSource(time.Now().UnixNano())), |
||||
|
||||
request: c.Request, |
||||
k: c.K, |
||||
state: StateClosed, |
||||
} |
||||
} |
||||
|
||||
func (b *sreBreaker) Allow() error { |
||||
success, total := b.stat.Value() |
||||
k := b.k * float64(success) |
||||
if log.V(5) { |
||||
log.Info("breaker: request: %d, succee: %d, fail: %d", total, success, total-success) |
||||
} |
||||
// check overflow requests = K * success
|
||||
if total < b.request || float64(total) < k { |
||||
if atomic.LoadInt32(&b.state) == StateOpen { |
||||
atomic.CompareAndSwapInt32(&b.state, StateOpen, StateClosed) |
||||
} |
||||
return nil |
||||
} |
||||
if atomic.LoadInt32(&b.state) == StateClosed { |
||||
atomic.CompareAndSwapInt32(&b.state, StateClosed, StateOpen) |
||||
} |
||||
dr := math.Max(0, (float64(total)-k)/float64(total+1)) |
||||
rr := b.r.Float64() |
||||
if log.V(5) { |
||||
log.Info("breaker: drop ratio: %f, real rand: %f, drop: %v", dr, rr, dr > rr) |
||||
} |
||||
if dr <= rr { |
||||
return nil |
||||
} |
||||
return ecode.ServiceUnavailable |
||||
} |
||||
|
||||
func (b *sreBreaker) MarkSuccess() { |
||||
b.stat.Add(1) |
||||
} |
||||
|
||||
func (b *sreBreaker) MarkFailed() { |
||||
// NOTE: when client reject requets locally, continue add counter let the
|
||||
// drop ratio higher.
|
||||
b.stat.Add(0) |
||||
} |
Loading…
Reference in new issue