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 }