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 }