2020-08-06 20:55:38 +08:00
package p2c
import (
2020-08-07 14:43:11 +08:00
"fmt"
2020-08-06 20:55:38 +08:00
"math"
"math/rand"
2020-08-07 14:43:11 +08:00
"strings"
2020-08-06 20:55:38 +08:00
"sync"
"sync/atomic"
"time"
2022-01-04 15:51:32 +08:00
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/core/syncx"
"github.com/zeromicro/go-zero/core/timex"
"github.com/zeromicro/go-zero/zrpc/internal/codes"
2020-08-06 20:55:38 +08:00
"google.golang.org/grpc/balancer"
"google.golang.org/grpc/balancer/base"
"google.golang.org/grpc/resolver"
)
const (
2021-03-01 23:52:44 +08:00
// Name is the name of p2c balancer.
Name = "p2c_ewma"
2020-08-14 22:24:11 +08:00
decayTime = int64 ( time . Second * 10 ) // default value from finagle
2024-07-13 20:09:58 +08:00
forcePick = int64 ( time . Second ) // If a node is not selected for a period of time, it is forcibly selected.
initSuccess = 1000 // Initial success count
throttleSuccess = initSuccess / 2 // Success count to trigger throttling
penalty = int64 ( math . MaxInt32 ) // Penalty value for load calculation
pickTimes = 3 // Number of pick attempts
logInterval = time . Minute // Log interval for statistics
2020-08-06 20:55:38 +08:00
)
2021-06-21 09:05:20 +08:00
var emptyPickResult balancer . PickResult
2020-08-06 20:55:38 +08:00
func init ( ) {
balancer . Register ( newBuilder ( ) )
}
2021-04-15 19:49:17 +08:00
type p2cPickerBuilder struct { }
2020-08-06 20:55:38 +08:00
2021-06-21 09:05:20 +08:00
func ( b * p2cPickerBuilder ) Build ( info base . PickerBuildInfo ) balancer . Picker {
readySCs := info . ReadySCs
2020-08-06 20:55:38 +08:00
if len ( readySCs ) == 0 {
return base . NewErrPicker ( balancer . ErrNoSubConnAvailable )
}
var conns [ ] * subConn
2021-06-21 09:05:20 +08:00
for conn , connInfo := range readySCs {
2020-08-06 20:55:38 +08:00
conns = append ( conns , & subConn {
2021-06-21 09:05:20 +08:00
addr : connInfo . Address ,
2020-08-06 20:55:38 +08:00
conn : conn ,
success : initSuccess ,
} )
}
return & p2cPicker {
conns : conns ,
r : rand . New ( rand . NewSource ( time . Now ( ) . UnixNano ( ) ) ) ,
2020-08-07 14:43:11 +08:00
stamp : syncx . NewAtomicDuration ( ) ,
2020-08-06 20:55:38 +08:00
}
}
2021-06-21 09:05:20 +08:00
func newBuilder ( ) balancer . Builder {
return base . NewBalancerBuilder ( Name , new ( p2cPickerBuilder ) , base . Config { HealthCheck : true } )
}
2020-08-06 20:55:38 +08:00
type p2cPicker struct {
conns [ ] * subConn
r * rand . Rand
2020-08-07 14:43:11 +08:00
stamp * syncx . AtomicDuration
2020-08-06 20:55:38 +08:00
lock sync . Mutex
}
2023-03-10 21:56:19 +08:00
func ( p * p2cPicker ) Pick ( _ balancer . PickInfo ) ( balancer . PickResult , error ) {
2020-08-06 20:55:38 +08:00
p . lock . Lock ( )
defer p . lock . Unlock ( )
var chosen * subConn
switch len ( p . conns ) {
case 0 :
2021-06-21 09:05:20 +08:00
return emptyPickResult , balancer . ErrNoSubConnAvailable
2020-08-06 20:55:38 +08:00
case 1 :
chosen = p . choose ( p . conns [ 0 ] , nil )
case 2 :
chosen = p . choose ( p . conns [ 0 ] , p . conns [ 1 ] )
default :
var node1 , node2 * subConn
for i := 0 ; i < pickTimes ; i ++ {
a := p . r . Intn ( len ( p . conns ) )
b := p . r . Intn ( len ( p . conns ) - 1 )
if b >= a {
b ++
}
node1 = p . conns [ a ]
node2 = p . conns [ b ]
if node1 . healthy ( ) && node2 . healthy ( ) {
break
}
}
chosen = p . choose ( node1 , node2 )
}
atomic . AddInt64 ( & chosen . inflight , 1 )
2020-08-07 14:43:11 +08:00
atomic . AddInt64 ( & chosen . requests , 1 )
2021-06-21 09:05:20 +08:00
return balancer . PickResult {
SubConn : chosen . conn ,
Done : p . buildDoneFunc ( chosen ) ,
} , nil
2020-08-06 20:55:38 +08:00
}
func ( p * p2cPicker ) buildDoneFunc ( c * subConn ) func ( info balancer . DoneInfo ) {
start := int64 ( timex . Now ( ) )
return func ( info balancer . DoneInfo ) {
atomic . AddInt64 ( & c . inflight , - 1 )
2020-08-07 14:43:11 +08:00
now := timex . Now ( )
2020-08-06 20:55:38 +08:00
last := atomic . SwapInt64 ( & c . last , int64 ( now ) )
2020-08-07 14:43:11 +08:00
td := int64 ( now ) - last
2020-08-06 20:55:38 +08:00
if td < 0 {
td = 0
}
2024-07-13 20:09:58 +08:00
// As the td/decayTime value increases, indicating an increase in delay, the value of w (y axis) will decrease, inversely proportional.
// The function curve of y = x^(-x) is as follows.
// https://github.com/zeromicro/zero-doc/blob/main/doc/images/y_e_x.png?raw=true
2020-08-06 20:55:38 +08:00
w := math . Exp ( float64 ( - td ) / float64 ( decayTime ) )
2020-08-07 14:43:11 +08:00
lag := int64 ( now ) - start
2020-08-06 20:55:38 +08:00
if lag < 0 {
lag = 0
}
olag := atomic . LoadUint64 ( & c . lag )
if olag == 0 {
w = 0
}
2024-07-13 20:09:58 +08:00
// The smaller the value of w, the lower the impact of historical data.
2020-08-06 20:55:38 +08:00
atomic . StoreUint64 ( & c . lag , uint64 ( float64 ( olag ) * w + float64 ( lag ) * ( 1 - w ) ) )
success := initSuccess
if info . Err != nil && ! codes . Acceptable ( info . Err ) {
success = 0
}
osucc := atomic . LoadUint64 ( & c . success )
atomic . StoreUint64 ( & c . success , uint64 ( float64 ( osucc ) * w + float64 ( success ) * ( 1 - w ) ) )
2020-08-07 14:43:11 +08:00
stamp := p . stamp . Load ( )
if now - stamp >= logInterval {
if p . stamp . CompareAndSwap ( stamp , now ) {
p . logStats ( )
}
}
2020-08-06 20:55:38 +08:00
}
}
func ( p * p2cPicker ) choose ( c1 , c2 * subConn ) * subConn {
start := int64 ( timex . Now ( ) )
if c2 == nil {
atomic . StoreInt64 ( & c1 . pick , start )
return c1
}
if c1 . load ( ) > c2 . load ( ) {
c1 , c2 = c2 , c1
}
pick := atomic . LoadInt64 ( & c2 . pick )
if start - pick > forcePick && atomic . CompareAndSwapInt64 ( & c2 . pick , pick , start ) {
return c2
}
2021-02-09 13:50:21 +08:00
atomic . StoreInt64 ( & c1 . pick , start )
return c1
2020-08-06 20:55:38 +08:00
}
2020-08-07 14:43:11 +08:00
func ( p * p2cPicker ) logStats ( ) {
var stats [ ] string
p . lock . Lock ( )
defer p . lock . Unlock ( )
for _ , conn := range p . conns {
stats = append ( stats , fmt . Sprintf ( "conn: %s, load: %d, reqs: %d" ,
conn . addr . Addr , conn . load ( ) , atomic . SwapInt64 ( & conn . requests , 0 ) ) )
}
logx . Statf ( "p2c - %s" , strings . Join ( stats , "; " ) )
}
2020-08-06 20:55:38 +08:00
type subConn struct {
2024-07-13 20:09:58 +08:00
// The request latency measured by the weighted moving average algorithm.
lag uint64
// The value represents the number of requests that are either pending or just starting at the current node, and it is obtained through atomic addition.
2020-08-06 20:55:38 +08:00
inflight int64
success uint64
2020-08-07 14:43:11 +08:00
requests int64
2020-08-06 20:55:38 +08:00
last int64
pick int64
2021-12-07 15:52:37 +08:00
addr resolver . Address
conn balancer . SubConn
2020-08-06 20:55:38 +08:00
}
func ( c * subConn ) healthy ( ) bool {
return atomic . LoadUint64 ( & c . success ) > throttleSuccess
}
func ( c * subConn ) load ( ) int64 {
2020-08-07 14:43:11 +08:00
// plus one to avoid multiply zero
2020-08-06 20:55:38 +08:00
lag := int64 ( math . Sqrt ( float64 ( atomic . LoadUint64 ( & c . lag ) + 1 ) ) )
2020-08-07 14:43:11 +08:00
load := lag * ( atomic . LoadInt64 ( & c . inflight ) + 1 )
2020-08-06 20:55:38 +08:00
if load == 0 {
return penalty
}
2021-02-09 13:50:21 +08:00
return load
2020-08-06 20:55:38 +08:00
}