istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/xds/eds.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 xds
    16  
    17  import (
    18  	"fmt"
    19  
    20  	discovery "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
    21  
    22  	"istio.io/istio/pilot/pkg/features"
    23  	"istio.io/istio/pilot/pkg/model"
    24  	"istio.io/istio/pilot/pkg/util/protoconv"
    25  	"istio.io/istio/pilot/pkg/xds/endpoints"
    26  	"istio.io/istio/pkg/config/schema/kind"
    27  	"istio.io/istio/pkg/util/sets"
    28  )
    29  
    30  // SvcUpdate is a callback from service discovery when service info changes.
    31  func (s *DiscoveryServer) SvcUpdate(shard model.ShardKey, hostname string, namespace string, event model.Event) {
    32  	// When a service deleted, we should cleanup the endpoint shards and also remove keys from EndpointIndex to
    33  	// prevent memory leaks.
    34  	if event == model.EventDelete {
    35  		inboundServiceDeletes.Increment()
    36  		s.Env.EndpointIndex.DeleteServiceShard(shard, hostname, namespace, false)
    37  	} else {
    38  		inboundServiceUpdates.Increment()
    39  	}
    40  }
    41  
    42  // EDSUpdate computes destination address membership across all clusters and networks.
    43  // This is the main method implementing EDS.
    44  // It replaces InstancesByPort in model - instead of iterating over all endpoints it uses
    45  // the hostname-keyed map. And it avoids the conversion from Endpoint to ServiceEntry to envoy
    46  // on each step: instead the conversion happens once, when an endpoint is first discovered.
    47  func (s *DiscoveryServer) EDSUpdate(shard model.ShardKey, serviceName string, namespace string,
    48  	istioEndpoints []*model.IstioEndpoint,
    49  ) {
    50  	inboundEDSUpdates.Increment()
    51  	// Update the endpoint shards
    52  	pushType := s.Env.EndpointIndex.UpdateServiceEndpoints(shard, serviceName, namespace, istioEndpoints)
    53  	if pushType == model.IncrementalPush || pushType == model.FullPush {
    54  		// Trigger a push
    55  		s.ConfigUpdate(&model.PushRequest{
    56  			Full:           pushType == model.FullPush,
    57  			ConfigsUpdated: sets.New(model.ConfigKey{Kind: kind.ServiceEntry, Name: serviceName, Namespace: namespace}),
    58  			Reason:         model.NewReasonStats(model.EndpointUpdate),
    59  		})
    60  	}
    61  }
    62  
    63  // EDSCacheUpdate computes destination address membership across all clusters and networks.
    64  // This is the main method implementing EDS.
    65  // It replaces InstancesByPort in model - instead of iterating over all endpoints it uses
    66  // the hostname-keyed map. And it avoids the conversion from Endpoint to ServiceEntry to envoy
    67  // on each step: instead the conversion happens once, when an endpoint is first discovered.
    68  //
    69  // Note: the difference with `EDSUpdate` is that it only update the cache rather than requesting a push
    70  func (s *DiscoveryServer) EDSCacheUpdate(shard model.ShardKey, serviceName string, namespace string,
    71  	istioEndpoints []*model.IstioEndpoint,
    72  ) {
    73  	inboundEDSUpdates.Increment()
    74  	// Update the endpoint shards
    75  	s.Env.EndpointIndex.UpdateServiceEndpoints(shard, serviceName, namespace, istioEndpoints)
    76  }
    77  
    78  func (s *DiscoveryServer) RemoveShard(shardKey model.ShardKey) {
    79  	s.Env.EndpointIndex.DeleteShard(shardKey)
    80  }
    81  
    82  // EdsGenerator implements the new Generate method for EDS, using the in-memory, optimized endpoint
    83  // storage in DiscoveryServer.
    84  type EdsGenerator struct {
    85  	Cache         model.XdsCache
    86  	EndpointIndex *model.EndpointIndex
    87  }
    88  
    89  var _ model.XdsDeltaResourceGenerator = &EdsGenerator{}
    90  
    91  // Map of all configs that do not impact EDS
    92  var skippedEdsConfigs = sets.New(
    93  	kind.Gateway,
    94  	kind.VirtualService,
    95  	kind.WorkloadGroup,
    96  	kind.AuthorizationPolicy,
    97  	kind.RequestAuthentication,
    98  	kind.Secret,
    99  	kind.Telemetry,
   100  	kind.WasmPlugin,
   101  	kind.ProxyConfig,
   102  	kind.DNSName,
   103  
   104  	kind.KubernetesGateway,
   105  	kind.HTTPRoute,
   106  	kind.TCPRoute,
   107  	kind.TLSRoute,
   108  	kind.GRPCRoute,
   109  )
   110  
   111  func edsNeedsPush(updates model.XdsUpdates) bool {
   112  	// If none set, we will always push
   113  	if len(updates) == 0 {
   114  		return true
   115  	}
   116  	for config := range updates {
   117  		if !skippedEdsConfigs.Contains(config.Kind) {
   118  			return true
   119  		}
   120  	}
   121  	return false
   122  }
   123  
   124  func (eds *EdsGenerator) Generate(proxy *model.Proxy, w *model.WatchedResource, req *model.PushRequest) (model.Resources, model.XdsLogDetails, error) {
   125  	if !edsNeedsPush(req.ConfigsUpdated) {
   126  		return nil, model.DefaultXdsLogDetails, nil
   127  	}
   128  	resources, logDetails := eds.buildEndpoints(proxy, req, w)
   129  	return resources, logDetails, nil
   130  }
   131  
   132  func (eds *EdsGenerator) GenerateDeltas(proxy *model.Proxy, req *model.PushRequest,
   133  	w *model.WatchedResource,
   134  ) (model.Resources, model.DeletedResources, model.XdsLogDetails, bool, error) {
   135  	if !edsNeedsPush(req.ConfigsUpdated) {
   136  		return nil, nil, model.DefaultXdsLogDetails, false, nil
   137  	}
   138  	if !shouldUseDeltaEds(req) {
   139  		resources, logDetails := eds.buildEndpoints(proxy, req, w)
   140  		return resources, nil, logDetails, false, nil
   141  	}
   142  
   143  	resources, removed, logs := eds.buildDeltaEndpoints(proxy, req, w)
   144  	return resources, removed, logs, true, nil
   145  }
   146  
   147  func shouldUseDeltaEds(req *model.PushRequest) bool {
   148  	if !req.Full {
   149  		return false
   150  	}
   151  	return canSendPartialFullPushes(req)
   152  }
   153  
   154  // canSendPartialFullPushes checks if a request contains *only* endpoints updates except `skippedEdsConfigs`.
   155  // This allows us to perform more efficient pushes where we only update the endpoints that did change.
   156  func canSendPartialFullPushes(req *model.PushRequest) bool {
   157  	// If we don't know what configs are updated, just send a full push
   158  	if len(req.ConfigsUpdated) == 0 {
   159  		return false
   160  	}
   161  	for cfg := range req.ConfigsUpdated {
   162  		if skippedEdsConfigs.Contains(cfg.Kind) {
   163  			// the updated config does not impact EDS, skip it
   164  			// this happens when push requests are merged due to debounce
   165  			continue
   166  		}
   167  		if cfg.Kind != kind.ServiceEntry {
   168  			return false
   169  		}
   170  	}
   171  	return true
   172  }
   173  
   174  func (eds *EdsGenerator) buildEndpoints(proxy *model.Proxy,
   175  	req *model.PushRequest,
   176  	w *model.WatchedResource,
   177  ) (model.Resources, model.XdsLogDetails) {
   178  	var edsUpdatedServices map[string]struct{}
   179  	// canSendPartialFullPushes determines if we can send a partial push (ie a subset of known CLAs).
   180  	// This is safe when only Services has changed, as this implies that only the CLAs for the
   181  	// associated Service changed. Note when a multi-network Service changes it triggers a push with
   182  	// ConfigsUpdated=ALL, so in this case we would not enable a partial push.
   183  	// Despite this code existing on the SotW code path, sending these partial pushes is still allowed;
   184  	// see https://www.envoyproxy.io/docs/envoy/latest/api-docs/xds_protocol#grouping-resources-into-responses
   185  	if !req.Full || canSendPartialFullPushes(req) {
   186  		edsUpdatedServices = model.ConfigNamesOfKind(req.ConfigsUpdated, kind.ServiceEntry)
   187  	}
   188  	var resources model.Resources
   189  	empty := 0
   190  	cached := 0
   191  	regenerated := 0
   192  	for _, clusterName := range w.ResourceNames {
   193  		if edsUpdatedServices != nil {
   194  			if _, ok := edsUpdatedServices[model.ParseSubsetKeyHostname(clusterName)]; !ok {
   195  				// Cluster was not updated, skip recomputing. This happens when we get an incremental update for a
   196  				// specific Hostname. On connect or for full push edsUpdatedServices will be empty.
   197  				continue
   198  			}
   199  		}
   200  		builder := endpoints.NewEndpointBuilder(clusterName, proxy, req.Push)
   201  
   202  		// We skip cache if assertions are enabled, so that the cache will assert our eviction logic is correct
   203  		if !features.EnableUnsafeAssertions {
   204  			cachedEndpoint := eds.Cache.Get(&builder)
   205  			if cachedEndpoint != nil {
   206  				resources = append(resources, cachedEndpoint)
   207  				cached++
   208  				continue
   209  			}
   210  		}
   211  
   212  		// generate eds from beginning
   213  		{
   214  			l := builder.BuildClusterLoadAssignment(eds.EndpointIndex)
   215  			if l == nil {
   216  				continue
   217  			}
   218  			regenerated++
   219  
   220  			if len(l.Endpoints) == 0 {
   221  				empty++
   222  			}
   223  			resource := &discovery.Resource{
   224  				Name:     l.ClusterName,
   225  				Resource: protoconv.MessageToAny(l),
   226  			}
   227  			resources = append(resources, resource)
   228  			eds.Cache.Add(&builder, req, resource)
   229  		}
   230  	}
   231  	return resources, model.XdsLogDetails{
   232  		Incremental:    len(edsUpdatedServices) != 0,
   233  		AdditionalInfo: fmt.Sprintf("empty:%v cached:%v/%v", empty, cached, cached+regenerated),
   234  	}
   235  }
   236  
   237  // TODO(@hzxuzhonghu): merge with buildEndpoints
   238  func (eds *EdsGenerator) buildDeltaEndpoints(proxy *model.Proxy,
   239  	req *model.PushRequest,
   240  	w *model.WatchedResource,
   241  ) (model.Resources, []string, model.XdsLogDetails) {
   242  	edsUpdatedServices := model.ConfigNamesOfKind(req.ConfigsUpdated, kind.ServiceEntry)
   243  	var resources model.Resources
   244  	var removed []string
   245  	empty := 0
   246  	cached := 0
   247  	regenerated := 0
   248  
   249  	for _, clusterName := range w.ResourceNames {
   250  		// filter out eds that are not updated for clusters
   251  		if _, ok := edsUpdatedServices[model.ParseSubsetKeyHostname(clusterName)]; !ok {
   252  			continue
   253  		}
   254  
   255  		builder := endpoints.NewEndpointBuilder(clusterName, proxy, req.Push)
   256  		// if a service is not found, it means the cluster is removed
   257  		if !builder.ServiceFound() {
   258  			removed = append(removed, clusterName)
   259  			continue
   260  		}
   261  
   262  		// We skip cache if assertions are enabled, so that the cache will assert our eviction logic is correct
   263  		if !features.EnableUnsafeAssertions {
   264  			cachedEndpoint := eds.Cache.Get(&builder)
   265  			if cachedEndpoint != nil {
   266  				resources = append(resources, cachedEndpoint)
   267  				cached++
   268  				continue
   269  			}
   270  		}
   271  		// generate new eds cache
   272  		{
   273  			l := builder.BuildClusterLoadAssignment(eds.EndpointIndex)
   274  			if l == nil {
   275  				removed = append(removed, clusterName)
   276  				continue
   277  			}
   278  			regenerated++
   279  			if len(l.Endpoints) == 0 {
   280  				empty++
   281  			}
   282  			resource := &discovery.Resource{
   283  				Name:     l.ClusterName,
   284  				Resource: protoconv.MessageToAny(l),
   285  			}
   286  			resources = append(resources, resource)
   287  			eds.Cache.Add(&builder, req, resource)
   288  		}
   289  	}
   290  	return resources, removed, model.XdsLogDetails{
   291  		Incremental:    len(edsUpdatedServices) != 0,
   292  		AdditionalInfo: fmt.Sprintf("empty:%v cached:%v/%v", empty, cached, cached+regenerated),
   293  	}
   294  }