github.com/cloudwego/kitex@v0.9.0/pkg/loadbalance/lbcache/cache.go (about)

     1  /*
     2   * Copyright 2021 CloudWeGo Authors
     3   *
     4   * Licensed under the Apache License, Version 2.0 (the "License");
     5   * you may not use this file except in compliance with the License.
     6   * You may obtain a copy of the License at
     7   *
     8   *     http://www.apache.org/licenses/LICENSE-2.0
     9   *
    10   * Unless required by applicable law or agreed to in writing, software
    11   * distributed under the License is distributed on an "AS IS" BASIS,
    12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   * See the License for the specific language governing permissions and
    14   * limitations under the License.
    15   */
    16  
    17  // Package lbcache combine balancer with resolver and cache the resolve result
    18  package lbcache
    19  
    20  import (
    21  	"context"
    22  	"fmt"
    23  	"sync"
    24  	"sync/atomic"
    25  	"time"
    26  
    27  	"golang.org/x/sync/singleflight"
    28  
    29  	"github.com/cloudwego/kitex/pkg/diagnosis"
    30  	"github.com/cloudwego/kitex/pkg/discovery"
    31  	"github.com/cloudwego/kitex/pkg/klog"
    32  	"github.com/cloudwego/kitex/pkg/loadbalance"
    33  	"github.com/cloudwego/kitex/pkg/rpcinfo"
    34  	"github.com/cloudwego/kitex/pkg/utils"
    35  )
    36  
    37  const (
    38  	defaultRefreshInterval = 5 * time.Second
    39  	defaultExpireInterval  = 15 * time.Second
    40  )
    41  
    42  var (
    43  	balancerFactories    sync.Map // key: resolver name + loadbalance name
    44  	balancerFactoriesSfg singleflight.Group
    45  )
    46  
    47  // Options for create builder
    48  type Options struct {
    49  	// refresh discovery result timely
    50  	RefreshInterval time.Duration
    51  
    52  	// Balancer expire check interval
    53  	// we need remove idle Balancers for resource saving
    54  	ExpireInterval time.Duration
    55  
    56  	// DiagnosisService is used register info for diagnosis
    57  	DiagnosisService diagnosis.Service
    58  
    59  	// Cacheable is used to indicate that if the factory could be shared between multi clients
    60  	Cacheable bool
    61  }
    62  
    63  func (v *Options) check() {
    64  	if v.RefreshInterval <= 0 {
    65  		v.RefreshInterval = defaultRefreshInterval
    66  	}
    67  	if v.ExpireInterval <= 0 {
    68  		v.ExpireInterval = defaultExpireInterval
    69  	}
    70  }
    71  
    72  // Hookable add hook for rebalancer events
    73  type Hookable interface {
    74  	// register loadbalance rebalance hook for Rebalance events
    75  	RegisterRebalanceHook(func(ch *discovery.Change)) (index int)
    76  	DeregisterRebalanceHook(index int)
    77  	// register loadbalance delete hook for Delete events
    78  	RegisterDeleteHook(func(ch *discovery.Change)) (index int)
    79  	DeregisterDeleteHook(index int)
    80  }
    81  
    82  // BalancerFactory get or create a balancer with given target
    83  // if it has the same key(reslover.Target(target)), we will cache and reuse the Balance
    84  type BalancerFactory struct {
    85  	Hookable
    86  	opts       Options
    87  	cache      sync.Map // key -> LoadBalancer
    88  	resolver   discovery.Resolver
    89  	balancer   loadbalance.Loadbalancer
    90  	rebalancer loadbalance.Rebalancer
    91  	sfg        singleflight.Group
    92  }
    93  
    94  func cacheKey(resolver, balancer string, opts Options) string {
    95  	return fmt.Sprintf("%s|%s|{%s %s}", resolver, balancer, opts.RefreshInterval, opts.ExpireInterval)
    96  }
    97  
    98  func newBalancerFactory(resolver discovery.Resolver, balancer loadbalance.Loadbalancer, opts Options) *BalancerFactory {
    99  	b := &BalancerFactory{
   100  		opts:     opts,
   101  		resolver: resolver,
   102  		balancer: balancer,
   103  	}
   104  	if rb, ok := balancer.(loadbalance.Rebalancer); ok {
   105  		hrb := newHookRebalancer(rb)
   106  		b.rebalancer = hrb
   107  		b.Hookable = hrb
   108  	} else {
   109  		b.Hookable = noopHookRebalancer{}
   110  	}
   111  	go b.watcher()
   112  	return b
   113  }
   114  
   115  // NewBalancerFactory get or create a balancer factory for balancer instance
   116  // cache key with resolver name, balancer name and options
   117  func NewBalancerFactory(resolver discovery.Resolver, balancer loadbalance.Loadbalancer, opts Options) *BalancerFactory {
   118  	opts.check()
   119  	if !opts.Cacheable {
   120  		return newBalancerFactory(resolver, balancer, opts)
   121  	}
   122  	uniqueKey := cacheKey(resolver.Name(), balancer.Name(), opts)
   123  	val, ok := balancerFactories.Load(uniqueKey)
   124  	if ok {
   125  		return val.(*BalancerFactory)
   126  	}
   127  	val, _, _ = balancerFactoriesSfg.Do(uniqueKey, func() (interface{}, error) {
   128  		b := newBalancerFactory(resolver, balancer, opts)
   129  		balancerFactories.Store(uniqueKey, b)
   130  		return b, nil
   131  	})
   132  	return val.(*BalancerFactory)
   133  }
   134  
   135  // watch expired balancer
   136  func (b *BalancerFactory) watcher() {
   137  	for range time.Tick(b.opts.ExpireInterval) {
   138  		b.cache.Range(func(key, value interface{}) bool {
   139  			bl := value.(*Balancer)
   140  			if atomic.CompareAndSwapInt32(&bl.expire, 0, 1) {
   141  				// 1. set expire flag
   142  				// 2. wait next ticker for collect, maybe the balancer is used again
   143  				// (avoid being immediate delete the balancer which had been created recently)
   144  			} else {
   145  				b.cache.Delete(key)
   146  				bl.close()
   147  			}
   148  			return true
   149  		})
   150  	}
   151  }
   152  
   153  // cache key with resolver name prefix avoid conflict for balancer
   154  func renameResultCacheKey(res *discovery.Result, resolverName string) {
   155  	res.CacheKey = resolverName + ":" + res.CacheKey
   156  }
   157  
   158  // Get create a new balancer if not exists
   159  func (b *BalancerFactory) Get(ctx context.Context, target rpcinfo.EndpointInfo) (*Balancer, error) {
   160  	desc := b.resolver.Target(ctx, target)
   161  	val, ok := b.cache.Load(desc)
   162  	if ok {
   163  		return val.(*Balancer), nil
   164  	}
   165  	val, err, _ := b.sfg.Do(desc, func() (interface{}, error) {
   166  		res, err := b.resolver.Resolve(ctx, desc)
   167  		if err != nil {
   168  			return nil, err
   169  		}
   170  		renameResultCacheKey(&res, b.resolver.Name())
   171  		bl := &Balancer{
   172  			b:      b,
   173  			target: desc,
   174  		}
   175  		bl.res.Store(res)
   176  		bl.sharedTicker = getSharedTicker(bl, b.opts.RefreshInterval)
   177  		b.cache.Store(desc, bl)
   178  		return bl, nil
   179  	})
   180  	if err != nil {
   181  		return nil, err
   182  	}
   183  	return val.(*Balancer), nil
   184  }
   185  
   186  // Balancer same with loadbalance.Loadbalancer but without resolver.Result that
   187  // has been cached
   188  type Balancer struct {
   189  	b            *BalancerFactory
   190  	target       string       // a description returned from the resolver's Target method
   191  	res          atomic.Value // newest and previous discovery result
   192  	expire       int32        // 0 = normal, 1 = expire and collect next ticker
   193  	sharedTicker *utils.SharedTicker
   194  }
   195  
   196  func (bl *Balancer) Refresh() {
   197  	res, err := bl.b.resolver.Resolve(context.Background(), bl.target)
   198  	if err != nil {
   199  		klog.Warnf("KITEX: resolver refresh failed, key=%s error=%s", bl.target, err.Error())
   200  		return
   201  	}
   202  	renameResultCacheKey(&res, bl.b.resolver.Name())
   203  	prev := bl.res.Load().(discovery.Result)
   204  	if bl.b.rebalancer != nil {
   205  		if ch, ok := bl.b.resolver.Diff(res.CacheKey, prev, res); ok {
   206  			bl.b.rebalancer.Rebalance(ch)
   207  		}
   208  	}
   209  	// replace previous result
   210  	bl.res.Store(res)
   211  }
   212  
   213  // Tick implements the interface utils.TickerTask.
   214  func (bl *Balancer) Tick() {
   215  	bl.Refresh()
   216  }
   217  
   218  // GetResult returns the discovery result that the Balancer holds.
   219  func (bl *Balancer) GetResult() (res discovery.Result, ok bool) {
   220  	if v := bl.res.Load(); v != nil {
   221  		return v.(discovery.Result), true
   222  	}
   223  	return
   224  }
   225  
   226  // GetPicker equal to loadbalance.Balancer without pass discovery.Result, because we cache the result
   227  func (bl *Balancer) GetPicker() loadbalance.Picker {
   228  	atomic.StoreInt32(&bl.expire, 0)
   229  	res := bl.res.Load().(discovery.Result)
   230  	return bl.b.balancer.GetPicker(res)
   231  }
   232  
   233  func (bl *Balancer) close() {
   234  	// notice the under rebalancer
   235  	if rb, ok := bl.b.balancer.(loadbalance.Rebalancer); ok {
   236  		// notice to rebalancing
   237  		rb.Delete(discovery.Change{
   238  			Result: discovery.Result{
   239  				Cacheable: true,
   240  				CacheKey:  bl.res.Load().(discovery.Result).CacheKey,
   241  			},
   242  		})
   243  	}
   244  	// delete from sharedTicker
   245  	bl.sharedTicker.Delete(bl)
   246  }
   247  
   248  const unknown = "unknown"
   249  
   250  func Dump() interface{} {
   251  	type instInfo struct {
   252  		Address string
   253  		Weight  int
   254  	}
   255  	cacheDump := make(map[string]interface{})
   256  	balancerFactories.Range(func(key, val interface{}) bool {
   257  		cacheKey := key.(string)
   258  		if bf, ok := val.(*BalancerFactory); ok {
   259  			routeMap := make(map[string]interface{})
   260  			cacheDump[cacheKey] = routeMap
   261  			bf.cache.Range(func(k, v interface{}) bool {
   262  				routeKey := k.(string)
   263  				if bl, ok := v.(*Balancer); ok {
   264  					if dr, ok := bl.res.Load().(discovery.Result); ok {
   265  						insts := make([]instInfo, 0, len(dr.Instances))
   266  						for i := range dr.Instances {
   267  							inst := dr.Instances[i]
   268  							addr := fmt.Sprintf("%s://%s", inst.Address().Network(), inst.Address().String())
   269  							insts = append(insts, instInfo{Address: addr, Weight: inst.Weight()})
   270  						}
   271  						routeMap[routeKey] = insts
   272  					} else {
   273  						routeMap[routeKey] = unknown
   274  					}
   275  				} else {
   276  					routeMap[routeKey] = unknown
   277  				}
   278  				return true
   279  			})
   280  		} else {
   281  			cacheDump[cacheKey] = unknown
   282  		}
   283  		return true
   284  	})
   285  	return cacheDump
   286  }