istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/model/endpointshards.go (about) 1 // Copyright Istio Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package model 16 17 import ( 18 "fmt" 19 "sort" 20 "sync" 21 22 "istio.io/istio/pilot/pkg/features" 23 "istio.io/istio/pilot/pkg/serviceregistry/provider" 24 "istio.io/istio/pkg/cluster" 25 "istio.io/istio/pkg/config/schema/kind" 26 "istio.io/istio/pkg/util/sets" 27 ) 28 29 // shardRegistry is a simplified interface for registries that can produce a shard key 30 type shardRegistry interface { 31 Cluster() cluster.ID 32 Provider() provider.ID 33 } 34 35 // ShardKeyFromRegistry computes the shard key based on provider type and cluster id. 36 func ShardKeyFromRegistry(instance shardRegistry) ShardKey { 37 return ShardKey{Cluster: instance.Cluster(), Provider: instance.Provider()} 38 } 39 40 // ShardKey is the key for EndpointShards made of a key with the format "provider/cluster" 41 type ShardKey struct { 42 Cluster cluster.ID 43 Provider provider.ID 44 } 45 46 func (sk ShardKey) String() string { 47 return fmt.Sprintf("%s/%s", sk.Provider, sk.Cluster) 48 } 49 50 // MarshalText implements the TextMarshaler interface (for json key usage) 51 func (sk ShardKey) MarshalText() (text []byte, err error) { 52 return []byte(sk.String()), nil 53 } 54 55 // EndpointShards holds the set of endpoint shards of a service. Registries update 56 // individual shards incrementally. The shards are aggregated and split into 57 // clusters when a push for the specific cluster is needed. 58 type EndpointShards struct { 59 // mutex protecting below map. 60 sync.RWMutex 61 62 // Shards is used to track the shards. EDS updates are grouped by shard. 63 // Current implementation uses the registry name as key - in multicluster this is the 64 // name of the k8s cluster, derived from the config (secret). 65 Shards map[ShardKey][]*IstioEndpoint 66 67 // ServiceAccounts has the concatenation of all service accounts seen so far in endpoints. 68 // This is updated on push, based on shards. If the previous list is different than 69 // current list, a full push will be forced, to trigger a secure naming update. 70 // Due to the larger time, it is still possible that connection errors will occur while 71 // CDS is updated. 72 ServiceAccounts sets.String 73 } 74 75 // Keys gives a sorted list of keys for EndpointShards.Shards. 76 // Calls to Keys should be guarded with a lock on the EndpointShards. 77 func (es *EndpointShards) Keys() []ShardKey { 78 // len(shards) ~= number of remote clusters which isn't too large, doing this sort frequently 79 // shouldn't be too problematic. If it becomes an issue we can cache it in the EndpointShards struct. 80 keys := make([]ShardKey, 0, len(es.Shards)) 81 for k := range es.Shards { 82 keys = append(keys, k) 83 } 84 if len(keys) >= 2 { 85 sort.Slice(keys, func(i, j int) bool { 86 if keys[i].Provider == keys[j].Provider { 87 return keys[i].Cluster < keys[j].Cluster 88 } 89 return keys[i].Provider < keys[j].Provider 90 }) 91 } 92 return keys 93 } 94 95 // CopyEndpoints takes a snapshot of all endpoints. As input, it takes a map of port name to number, to allow it to group 96 // the results by service port number. This is a bit weird, but lets us efficiently construct the format the caller needs. 97 func (es *EndpointShards) CopyEndpoints(portMap map[string]int, ports sets.Set[int]) map[int][]*IstioEndpoint { 98 es.RLock() 99 defer es.RUnlock() 100 res := map[int][]*IstioEndpoint{} 101 for _, v := range es.Shards { 102 for _, ep := range v { 103 // use the port name as the key, unless LegacyClusterPortKey is set and takes precedence 104 // In EDS we match on port *name*. But for historical reasons, we match on port number for CDS. 105 var portNum int 106 if ep.LegacyClusterPortKey != 0 { 107 if !ports.Contains(ep.LegacyClusterPortKey) { 108 continue 109 } 110 portNum = ep.LegacyClusterPortKey 111 } else { 112 pn, f := portMap[ep.ServicePortName] 113 if !f { 114 continue 115 } 116 portNum = pn 117 } 118 res[portNum] = append(res[portNum], ep) 119 } 120 } 121 return res 122 } 123 124 func (es *EndpointShards) DeepCopy() *EndpointShards { 125 es.RLock() 126 defer es.RUnlock() 127 res := &EndpointShards{ 128 Shards: make(map[ShardKey][]*IstioEndpoint, len(es.Shards)), 129 ServiceAccounts: es.ServiceAccounts.Copy(), 130 } 131 for k, v := range es.Shards { 132 res.Shards[k] = make([]*IstioEndpoint, 0, len(v)) 133 for _, ep := range v { 134 res.Shards[k] = append(res.Shards[k], ep.DeepCopy()) 135 } 136 } 137 return res 138 } 139 140 // EndpointIndex is a mutex protected index of endpoint shards 141 type EndpointIndex struct { 142 mu sync.RWMutex 143 // keyed by svc then ns 144 shardsBySvc map[string]map[string]*EndpointShards 145 // We'll need to clear the cache in-sync with endpoint shards modifications. 146 cache XdsCache 147 } 148 149 func NewEndpointIndex(cache XdsCache) *EndpointIndex { 150 return &EndpointIndex{ 151 shardsBySvc: make(map[string]map[string]*EndpointShards), 152 cache: cache, 153 } 154 } 155 156 // must be called with lock 157 func (e *EndpointIndex) clearCacheForService(svc, ns string) { 158 e.cache.Clear(sets.Set[ConfigKey]{{ 159 Kind: kind.ServiceEntry, 160 Name: svc, 161 Namespace: ns, 162 }: {}}) 163 } 164 165 // Shardz returns a full deep copy of the global map of shards. This should be used only for testing 166 // and debugging, as the cloning is expensive. 167 func (e *EndpointIndex) Shardz() map[string]map[string]*EndpointShards { 168 e.mu.RLock() 169 defer e.mu.RUnlock() 170 out := make(map[string]map[string]*EndpointShards, len(e.shardsBySvc)) 171 for svcKey, v := range e.shardsBySvc { 172 out[svcKey] = make(map[string]*EndpointShards, len(v)) 173 for nsKey, v := range v { 174 out[svcKey][nsKey] = v.DeepCopy() 175 } 176 } 177 return out 178 } 179 180 // ShardsForService returns the shards and true if they are found, or returns nil, false. 181 func (e *EndpointIndex) ShardsForService(serviceName, namespace string) (*EndpointShards, bool) { 182 e.mu.RLock() 183 defer e.mu.RUnlock() 184 byNs, ok := e.shardsBySvc[serviceName] 185 if !ok { 186 return nil, false 187 } 188 shards, ok := byNs[namespace] 189 return shards, ok 190 } 191 192 // GetOrCreateEndpointShard returns the shards. The second return parameter will be true if this service was seen 193 // for the first time. 194 func (e *EndpointIndex) GetOrCreateEndpointShard(serviceName, namespace string) (*EndpointShards, bool) { 195 e.mu.Lock() 196 defer e.mu.Unlock() 197 198 if _, exists := e.shardsBySvc[serviceName]; !exists { 199 e.shardsBySvc[serviceName] = map[string]*EndpointShards{} 200 } 201 if ep, exists := e.shardsBySvc[serviceName][namespace]; exists { 202 return ep, false 203 } 204 // This endpoint is for a service that was not previously loaded. 205 ep := &EndpointShards{ 206 Shards: map[ShardKey][]*IstioEndpoint{}, 207 ServiceAccounts: sets.String{}, 208 } 209 e.shardsBySvc[serviceName][namespace] = ep 210 // Clear the cache here to avoid race in cache writes. 211 e.clearCacheForService(serviceName, namespace) 212 return ep, true 213 } 214 215 func (e *EndpointIndex) DeleteServiceShard(shard ShardKey, serviceName, namespace string, preserveKeys bool) { 216 e.mu.Lock() 217 defer e.mu.Unlock() 218 e.deleteServiceInner(shard, serviceName, namespace, preserveKeys) 219 } 220 221 func (e *EndpointIndex) DeleteShard(shardKey ShardKey) { 222 e.mu.Lock() 223 defer e.mu.Unlock() 224 for svc, shardsByNamespace := range e.shardsBySvc { 225 for ns := range shardsByNamespace { 226 e.deleteServiceInner(shardKey, svc, ns, false) 227 } 228 } 229 if e.cache == nil { 230 return 231 } 232 e.cache.ClearAll() 233 } 234 235 // must be called with lock 236 func (e *EndpointIndex) deleteServiceInner(shard ShardKey, serviceName, namespace string, preserveKeys bool) { 237 if e.shardsBySvc[serviceName] == nil || 238 e.shardsBySvc[serviceName][namespace] == nil { 239 return 240 } 241 epShards := e.shardsBySvc[serviceName][namespace] 242 epShards.Lock() 243 delete(epShards.Shards, shard) 244 // Clear the cache here to avoid race in cache writes. 245 e.clearCacheForService(serviceName, namespace) 246 if !preserveKeys { 247 if len(epShards.Shards) == 0 { 248 delete(e.shardsBySvc[serviceName], namespace) 249 } 250 if len(e.shardsBySvc[serviceName]) == 0 { 251 delete(e.shardsBySvc, serviceName) 252 } 253 } 254 epShards.Unlock() 255 } 256 257 // PushType is an enumeration that decides what type push we should do when we get EDS update. 258 type PushType int 259 260 const ( 261 // NoPush does not push any thing. 262 NoPush PushType = iota 263 // IncrementalPush just pushes endpoints. 264 IncrementalPush 265 // FullPush triggers full push - typically used for new services. 266 FullPush 267 ) 268 269 // UpdateServiceEndpoints updates EndpointShards data by clusterID, hostname, IstioEndpoints. 270 // It also tracks the changes to ServiceAccounts. It returns whether endpoints need to be pushed and 271 // it also returns if they need to be pushed whether a full push is needed or incremental push is sufficient. 272 func (e *EndpointIndex) UpdateServiceEndpoints( 273 shard ShardKey, 274 hostname string, 275 namespace string, 276 istioEndpoints []*IstioEndpoint, 277 ) PushType { 278 if len(istioEndpoints) == 0 { 279 // Should delete the service EndpointShards when endpoints become zero to prevent memory leak, 280 // but we should not delete the keys from EndpointIndex map - that will trigger 281 // unnecessary full push which can become a real problem if a pod is in crashloop and thus endpoints 282 // flip flopping between 1 and 0. 283 e.DeleteServiceShard(shard, hostname, namespace, true) 284 log.Infof("Incremental push, service %s at shard %v has no endpoints", hostname, shard) 285 return IncrementalPush 286 } 287 288 pushType := IncrementalPush 289 // Find endpoint shard for this service, if it is available - otherwise create a new one. 290 ep, created := e.GetOrCreateEndpointShard(hostname, namespace) 291 // If we create a new endpoint shard, that means we have not seen the service earlier. We should do a full push. 292 if created { 293 log.Infof("Full push, new service %s/%s", namespace, hostname) 294 pushType = FullPush 295 } 296 297 ep.Lock() 298 defer ep.Unlock() 299 newIstioEndpoints := istioEndpoints 300 301 oldIstioEndpoints := ep.Shards[shard] 302 needPush := false 303 if oldIstioEndpoints == nil { 304 // If there are no old endpoints, we should push with incoming endpoints as there is nothing to compare. 305 needPush = true 306 } else { 307 newIstioEndpoints = make([]*IstioEndpoint, 0, len(istioEndpoints)) 308 // Check if new Endpoints are ready to be pushed. This check 309 // will ensure that if a new pod comes with a non ready endpoint, 310 // we do not unnecessarily push that config to Envoy. 311 // Please note that address is not a unique key. So this may not accurately 312 // identify based on health status and push too many times - which is ok since its an optimization. 313 omap := make(map[string]*IstioEndpoint, len(oldIstioEndpoints)) 314 nmap := make(map[string]*IstioEndpoint, len(newIstioEndpoints)) 315 // Add new endpoints only if they are ever ready once to shards 316 // so that full push does not send them from shards. 317 for _, oie := range oldIstioEndpoints { 318 omap[oie.Address] = oie 319 } 320 for _, nie := range istioEndpoints { 321 nmap[nie.Address] = nie 322 } 323 for _, nie := range istioEndpoints { 324 if oie, exists := omap[nie.Address]; exists { 325 // If endpoint exists already, we should push if it's changed. 326 // Skip this check if we already decide we need to push to avoid expensive checks 327 if !needPush && !oie.Equals(nie) { 328 needPush = true 329 } 330 newIstioEndpoints = append(newIstioEndpoints, nie) 331 } else { 332 // If the endpoint does not exist in shards that means it is a 333 // new endpoint. Always send new healthy endpoints. 334 // Also send new unhealthy endpoints when SendUnhealthyEndpoints is enabled. 335 // This is OK since we disable panic threshold when SendUnhealthyEndpoints is enabled. 336 if nie.HealthStatus != UnHealthy || features.SendUnhealthyEndpoints.Load() { 337 needPush = true 338 } 339 newIstioEndpoints = append(newIstioEndpoints, nie) 340 } 341 } 342 // Next, check for endpoints that were in old but no longer exist. If there are any, there is a 343 // removal so we need to push an update. 344 if !needPush { 345 for _, oie := range oldIstioEndpoints { 346 if _, f := nmap[oie.Address]; !f { 347 needPush = true 348 break 349 } 350 } 351 } 352 } 353 354 if pushType != FullPush && !needPush { 355 log.Debugf("No push, either old endpoint health status did not change or new endpoint came with unhealthy status, %v", hostname) 356 pushType = NoPush 357 } 358 359 ep.Shards[shard] = newIstioEndpoints 360 361 // Check if ServiceAccounts have changed. We should do a full push if they have changed. 362 saUpdated := updateShardServiceAccount(ep, hostname) 363 364 // For existing endpoints, we need to do full push if service accounts change. 365 if saUpdated && pushType != FullPush { 366 // Avoid extra logging if already a full push 367 log.Infof("Full push, service accounts changed, %v", hostname) 368 pushType = FullPush 369 } 370 371 // Clear the cache here. While it would likely be cleared later when we trigger a push, a race 372 // condition is introduced where an XDS response may be generated before the update, but not 373 // completed until after a response after the update. Essentially, we transition from v0 -> v1 -> 374 // v0 -> invalidate -> v1. Reverting a change we pushed violates our contract of monotonically 375 // moving forward in version. In practice, this is pretty rare and self corrects nearly 376 // immediately. However, clearing the cache here has almost no impact on cache performance as we 377 // would clear it shortly after anyways. 378 e.clearCacheForService(hostname, namespace) 379 380 return pushType 381 } 382 383 // updateShardServiceAccount updates the service endpoints' sa when service/endpoint event happens. 384 // Note: it is not concurrent safe. 385 func updateShardServiceAccount(shards *EndpointShards, serviceName string) bool { 386 oldServiceAccount := shards.ServiceAccounts 387 serviceAccounts := sets.String{} 388 for _, epShards := range shards.Shards { 389 for _, ep := range epShards { 390 if ep.ServiceAccount != "" { 391 serviceAccounts.Insert(ep.ServiceAccount) 392 } 393 } 394 } 395 396 if !oldServiceAccount.Equals(serviceAccounts) { 397 shards.ServiceAccounts = serviceAccounts 398 log.Debugf("Updating service accounts now, svc %v, before service account %v, after %v", 399 serviceName, oldServiceAccount, serviceAccounts) 400 return true 401 } 402 403 return false 404 } 405 406 // EndpointIndexUpdater is an updater that will keep an EndpointIndex in sync. This is intended for tests only. 407 type EndpointIndexUpdater struct { 408 Index *EndpointIndex 409 // Optional; if set, we will trigger ConfigUpdates in response to EDS updates as appropriate 410 ConfigUpdateFunc func(req *PushRequest) 411 } 412 413 var _ XDSUpdater = &EndpointIndexUpdater{} 414 415 func NewEndpointIndexUpdater(ei *EndpointIndex) *EndpointIndexUpdater { 416 return &EndpointIndexUpdater{Index: ei} 417 } 418 419 func (f *EndpointIndexUpdater) ConfigUpdate(*PushRequest) {} 420 421 func (f *EndpointIndexUpdater) EDSUpdate(shard ShardKey, serviceName string, namespace string, eps []*IstioEndpoint) { 422 pushType := f.Index.UpdateServiceEndpoints(shard, serviceName, namespace, eps) 423 if f.ConfigUpdateFunc != nil && (pushType == IncrementalPush || pushType == FullPush) { 424 // Trigger a push 425 f.ConfigUpdateFunc(&PushRequest{ 426 Full: pushType == FullPush, 427 ConfigsUpdated: sets.New(ConfigKey{Kind: kind.ServiceEntry, Name: serviceName, Namespace: namespace}), 428 Reason: NewReasonStats(EndpointUpdate), 429 }) 430 } 431 } 432 433 func (f *EndpointIndexUpdater) EDSCacheUpdate(shard ShardKey, serviceName string, namespace string, eps []*IstioEndpoint) { 434 f.Index.UpdateServiceEndpoints(shard, serviceName, namespace, eps) 435 } 436 437 func (f *EndpointIndexUpdater) SvcUpdate(shard ShardKey, hostname string, namespace string, event Event) { 438 if event == EventDelete { 439 f.Index.DeleteServiceShard(shard, hostname, namespace, false) 440 } 441 } 442 443 func (f *EndpointIndexUpdater) ProxyUpdate(_ cluster.ID, _ string) {} 444 445 func (f *EndpointIndexUpdater) RemoveShard(shardKey ShardKey) { 446 f.Index.DeleteShard(shardKey) 447 }