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  }