Skip to content

Commit 9cb9fca

Browse files
authored
gce: Implement fine-grained resource-specific locking for L4 ILBs (#1088)
Introduces fine-grained resource-specific locking for internal load balancer (L4 ILB) GCE resources: - Replaces the single global coarse-grained mutex (sharedResourceLock) with isolated, resource-scoped mutexes for Firewalls, HealthChecks, and InstanceGroups. - Relocates the coarse-grained global lock acquisition to the top-level entry points of the ILB operations (ensureInternalLoadBalancer, updateInternalLoadBalancer, and ensureInternalLoadBalancerDeleted) when fine-grained locking is disabled. - Refactors lockResourceIfShared to return a no-op unlock function when the fine-grained resource locking feature gate is disabled, preventing multi-level locking overhead and deadlock risks inside inner helpers. - Includes comprehensive unit tests to verify proper isolation, lock scoping, and seamless fallback behavior when the feature gate is disabled.
1 parent 3f74fe2 commit 9cb9fca

10 files changed

Lines changed: 402 additions & 31 deletions

File tree

cmd/cloud-controller-manager/main.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ var (
8181

8282
// enableGKETenantController enables the gke-tenant-controller-manager.
8383
enableGKETenantController bool
84+
85+
// enableL4ILBFineGrainedLocks enables resource-specific locking for L4 ILB.
86+
enableL4ILBFineGrainedLocks bool
8487
)
8588

8689
func main() {
@@ -102,6 +105,7 @@ func main() {
102105
cloudProviderFS.BoolVar(&enableL4DenyFirewall, "enable-l4-deny-firewall", false, "Enable creation and updates of Deny VPC Firewall Rules for L4 external load balancers. Requires --enable-pinhole and --enable-l4-deny-firewall-rollback-cleanup to be true.")
103106
cloudProviderFS.BoolVar(&enableL4DenyFirewallRollbackCleanup, "enable-l4-deny-firewall-rollback-cleanup", false, "Enable cleanup codepath of the deny firewalls for rollback. The reason for it not being enabled by default is the additional GCE API calls that are made for checking if the deny firewalls exist/deletion which will eat up the quota unnecessarily.")
104107
cloudProviderFS.BoolVar(&enableGKETenantController, "enable-gke-tenant-controller", false, "Enables the GKE Tenant Controller Manager for Multi-Tenancy.")
108+
cloudProviderFS.BoolVar(&enableL4ILBFineGrainedLocks, "enable-l4-ilb-fine-grained-lock", false, "Enable resource-specific locking for L4 ILB")
105109

106110
// add new controllers and initializers
107111
nodeIpamController := nodeIPAMController{}
@@ -227,5 +231,16 @@ func cloudInitializer(config *cloudcontrollerconfig.CompletedConfig) cloudprovid
227231
gceCloud.SetEnableL4DenyFirewallRule(enableL4DenyFirewall, enableL4DenyFirewallRollbackCleanup)
228232
}
229233

234+
if enableL4ILBFineGrainedLocks {
235+
gceCloud, ok := (cloud).(*gce.Cloud)
236+
if !ok {
237+
klog.Fatalf("enable-l4-ilb-fine-grained-lock requires GCE cloud provider, but got %T", cloud)
238+
}
239+
gceCloud.SetEnableL4ILBFineGrainedLocks(true)
240+
}
241+
242+
// Record feature gate metrics
243+
gce.RecordFeatureGateMetrics(enableL4ILBFineGrainedLocks)
244+
230245
return cloud
231246
}

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ require (
1010
github.com/spf13/pflag v1.0.10
1111
github.com/stretchr/testify v1.11.1
1212
golang.org/x/oauth2 v0.36.0
13+
golang.org/x/sync v0.21.0 // indirect
1314
google.golang.org/api v0.284.0
1415
gopkg.in/gcfg.v1 v1.2.3
1516
gopkg.in/warnings.v0 v0.1.2 // indirect
@@ -131,7 +132,6 @@ require (
131132
golang.org/x/crypto v0.51.0 // indirect
132133
golang.org/x/mod v0.35.0 // indirect
133134
golang.org/x/net v0.55.0 // indirect
134-
golang.org/x/sync v0.21.0 // indirect
135135
golang.org/x/sys v0.45.0 // indirect
136136
golang.org/x/term v0.43.0 // indirect
137137
golang.org/x/text v0.37.0 // indirect

go.work.sum

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC
55
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90=
66
github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k=
77
go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
8+
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
89
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
910
k8s.io/pod-security-admission v0.36.0 h1:YgVsB5KFiUtZfHgcLf/GPGGR9KgoXN4/loadBLCRvhY=
1011
k8s.io/pod-security-admission v0.36.0/go.mod h1:Brj/48uHTUApss1AaehnCw0dgI1Pxk/RAOo1oSNLqhI=

providers/gce/gce.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,11 +180,14 @@ type Cloud struct {
180180
// resources are only created in zones with active node capacity.
181181
nodeZones map[string]sets.String
182182
nodeInformerSynced cache.InformerSynced
183+
183184
// sharedResourceLock is used to serialize GCE operations that may mutate shared state to
184185
// prevent inconsistencies. For example, load balancers manipulation methods will take the
185186
// lock to prevent shared resources from being prematurely deleted while the operation is
186187
// in progress.
187188
sharedResourceLock sync.Mutex
189+
// sharedResourceLocks is a concurrent map used for resource-specific fine-grained locking of shared resources (e.g. InstanceGroups, shared HealthChecks).
190+
sharedResourceLocks sync.Map // map[string]*sync.Mutex
188191
// AlphaFeatureGate gates gce alpha features in Cloud instance.
189192
// Related wrapper functions that interacts with gce alpha api should examine whether
190193
// the corresponding api is enabled.
@@ -224,6 +227,67 @@ type Cloud struct {
224227

225228
// enableL4DenyFirewallRollbackCleanup
226229
enableL4DenyFirewallRollbackCleanup bool
230+
231+
// enableL4ILBFineGrainedLocks enables fine-grained resource-specific locking
232+
enableL4ILBFineGrainedLocks bool
233+
}
234+
235+
type SharedResourceType string
236+
237+
const (
238+
ResourceTypeHealthCheck SharedResourceType = "hc"
239+
ResourceTypeInstanceGroup SharedResourceType = "ig"
240+
ResourceTypeFirewall SharedResourceType = "fw"
241+
)
242+
243+
func (g *Cloud) getLockForResource(resType SharedResourceType, name string) *sync.Mutex {
244+
key := string(resType) + ":" + name
245+
if v, ok := g.sharedResourceLocks.Load(key); ok {
246+
return v.(*sync.Mutex)
247+
}
248+
v, _ := g.sharedResourceLocks.LoadOrStore(key, &sync.Mutex{})
249+
return v.(*sync.Mutex)
250+
}
251+
252+
// lockSharedResourcesIfCoarse acquires the global sharedResourceLock when fine-grained
253+
// locking is disabled, preserving the legacy coarse locking behavior.
254+
// It returns a function to defer for unlocking.
255+
func (g *Cloud) lockSharedResourcesIfCoarse() func() {
256+
if g.enableL4ILBFineGrainedLocks {
257+
return func() { /* no-op */ }
258+
}
259+
g.sharedResourceLock.Lock()
260+
return g.sharedResourceLock.Unlock
261+
}
262+
263+
// lockResourceIfShared is a helper function for acquiring locks on shared resources.
264+
// If fine-grained locking is disabled or the resource is not shared, it does nothing.
265+
func (g *Cloud) lockResourceIfShared(shared bool, resType SharedResourceType, name string) func() {
266+
if !g.enableL4ILBFineGrainedLocks || !shared {
267+
return func() { /* no-op */ }
268+
}
269+
lock := g.getLockForResource(resType, name)
270+
lock.Lock()
271+
return lock.Unlock
272+
}
273+
274+
// lockInstanceGroup locks the shared unmanaged instance group in the specified zone.
275+
// Since instance groups are always shared across the cluster, this locks unconditionally.
276+
// It returns a function to defer for unlocking.
277+
func (g *Cloud) lockInstanceGroup(igName, zone string) func() {
278+
return g.lockResourceIfShared(true, ResourceTypeInstanceGroup, igName+"-"+zone)
279+
}
280+
281+
// lockHealthCheck locks a health check resource by name.
282+
// It returns a function to defer for unlocking.
283+
func (g *Cloud) lockHealthCheck(hcName string, shared bool) func() {
284+
return g.lockResourceIfShared(shared, ResourceTypeHealthCheck, hcName)
285+
}
286+
287+
// lockFirewall locks a firewall resource by name.
288+
// It returns a function to defer for unlocking.
289+
func (g *Cloud) lockFirewall(fwName string, shared bool) func() {
290+
return g.lockResourceIfShared(shared, ResourceTypeFirewall, fwName)
227291
}
228292

229293
// ConfigGlobal is the in memory representation of the gce.conf config data
@@ -923,6 +987,10 @@ func (g *Cloud) SetEnableL4DenyFirewallRule(firewallEnabled, rollbackEnabled boo
923987
g.enableL4DenyFirewallRollbackCleanup = rollbackEnabled
924988
}
925989

990+
func (g *Cloud) SetEnableL4ILBFineGrainedLocks(enabled bool) {
991+
g.enableL4ILBFineGrainedLocks = enabled
992+
}
993+
926994
// getProjectsBasePath returns the compute API endpoint with the `projects/` element.
927995
// The suffix must be added when generating compute resource urls.
928996
func getProjectsBasePath(basePath string) string {

providers/gce/gce_loadbalancer_internal.go

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -132,9 +132,8 @@ func (g *Cloud) ensureInternalLoadBalancer(clusterName, clusterID string, svc *v
132132
}
133133
}
134134

135-
// Lock the sharedResourceLock to prevent any deletions of shared resources while assembling shared resources here
136-
g.sharedResourceLock.Lock()
137-
defer g.sharedResourceLock.Unlock()
135+
unlock := g.lockSharedResourcesIfCoarse()
136+
defer unlock()
138137

139138
// Ensure health check exists before creating the backend service. The health check is shared
140139
// if externalTrafficPolicy=Cluster.
@@ -250,7 +249,7 @@ func (g *Cloud) ensureInternalLoadBalancer(clusterName, clusterID string, svc *v
250249

251250
// Delete the previous internal load balancer resources if necessary
252251
if existingBackendService != nil {
253-
g.clearPreviousInternalResources(svc, loadBalancerName, existingBackendService, backendServiceName, hcName)
252+
g.clearPreviousInternalResources(svc, loadBalancerName, clusterID, existingBackendService, backendServiceName, hcName)
254253
}
255254

256255
serviceState.InSuccess = true
@@ -320,7 +319,7 @@ func truncateList[T any](l []T, max int) []T {
320319
return l[:max]
321320
}
322321

323-
func (g *Cloud) clearPreviousInternalResources(svc *v1.Service, loadBalancerName string, existingBackendService *compute.BackendService, expectedBSName, expectedHCName string) {
322+
func (g *Cloud) clearPreviousInternalResources(svc *v1.Service, loadBalancerName, clusterID string, existingBackendService *compute.BackendService, expectedBSName, expectedHCName string) {
324323
// If a new backend service was created, delete the old one.
325324
if existingBackendService.Name != expectedBSName {
326325
klog.V(2).Infof("clearPreviousInternalResources(%v): expected backend service %q does not match previous %q - deleting backend service", loadBalancerName, expectedBSName, existingBackendService.Name)
@@ -334,7 +333,8 @@ func (g *Cloud) clearPreviousInternalResources(svc *v1.Service, loadBalancerName
334333
existingHCName := getNameFromLink(existingBackendService.HealthChecks[0])
335334
if existingHCName != expectedHCName {
336335
klog.V(2).Infof("clearPreviousInternalResources(%v): expected health check %q does not match previous %q - deleting health check", loadBalancerName, expectedHCName, existingHCName)
337-
if err := g.teardownInternalHealthCheckAndFirewall(svc, existingHCName); err != nil {
336+
shared := isSharedHealthCheckName(existingHCName, clusterID)
337+
if err := g.teardownInternalHealthCheckAndFirewall(svc, existingHCName, shared); err != nil {
338338
klog.Warningf("clearPreviousInternalResources: could not delete existing healthcheck: %v, err: %v", existingHCName, err)
339339
}
340340
}
@@ -362,8 +362,8 @@ func (g *Cloud) updateInternalLoadBalancer(clusterName, clusterID string, svc *v
362362
if err := g.processMixedProtocolCheck(context.TODO(), svc, true); err != nil {
363363
return err
364364
}
365-
g.sharedResourceLock.Lock()
366-
defer g.sharedResourceLock.Unlock()
365+
unlock := g.lockSharedResourcesIfCoarse()
366+
defer unlock()
367367

368368
igName := makeInstanceGroupName(clusterID)
369369
igLinks, err := g.ensureInternalInstanceGroups(igName, nodes)
@@ -395,14 +395,15 @@ func (g *Cloud) ensureInternalLoadBalancerDeleted(clusterName, clusterID string,
395395
}
396396

397397
loadBalancerName := g.GetLoadBalancerName(context.TODO(), clusterName, svc)
398+
398399
svcNamespacedName := types.NamespacedName{Name: svc.Name, Namespace: svc.Namespace}
399400
_, _, protocol := getPortsAndProtocol(svc.Spec.Ports)
400401
scheme := cloud.SchemeInternal
401402
sharedBackend := shareBackendService(svc)
402403
sharedHealthCheck := !servicehelpers.RequestsOnlyLocalTraffic(svc)
403404

404-
g.sharedResourceLock.Lock()
405-
defer g.sharedResourceLock.Unlock()
405+
unlock := g.lockSharedResourcesIfCoarse()
406+
defer unlock()
406407

407408
klog.V(2).Infof("ensureInternalLoadBalancerDeleted(%v): attempting delete of region internal address", loadBalancerName)
408409
ensureAddressDeleted(g, loadBalancerName, g.region)
@@ -442,7 +443,7 @@ func (g *Cloud) ensureInternalLoadBalancerDeleted(clusterName, clusterID string,
442443

443444
hcName := makeHealthCheckName(loadBalancerName, clusterID, sharedHealthCheck)
444445
klog.V(2).Infof("ensureInternalLoadBalancerDeleted(%v): deleting health check %v and its firewall", loadBalancerName, hcName)
445-
if err := g.teardownInternalHealthCheckAndFirewall(svc, hcName); err != nil {
446+
if err := g.teardownInternalHealthCheckAndFirewall(svc, hcName, sharedHealthCheck); err != nil {
446447
return err
447448
}
448449

@@ -480,7 +481,11 @@ func (g *Cloud) teardownInternalBackendService(bsName string) error {
480481
return nil
481482
}
482483

483-
func (g *Cloud) teardownInternalHealthCheckAndFirewall(svc *v1.Service, hcName string) error {
484+
func (g *Cloud) teardownInternalHealthCheckAndFirewall(svc *v1.Service, hcName string, shared bool) error {
485+
hcFirewallName := makeHealthCheckFirewallNameFromHC(hcName)
486+
defer g.lockHealthCheck(hcName, shared)()
487+
defer g.lockFirewall(hcFirewallName, shared)()
488+
484489
if err := g.DeleteHealthCheck(hcName); err != nil {
485490
if isNotFound(err) {
486491
klog.V(2).Infof("teardownInternalHealthCheckAndFirewall(%v): health check does not exist.", hcName)
@@ -494,7 +499,6 @@ func (g *Cloud) teardownInternalHealthCheckAndFirewall(svc *v1.Service, hcName s
494499
}
495500
klog.V(2).Infof("teardownInternalHealthCheckAndFirewall(%v): health check deleted", hcName)
496501

497-
hcFirewallName := makeHealthCheckFirewallNameFromHC(hcName)
498502
if err := ignoreNotFound(g.DeleteFirewall(hcFirewallName)); err != nil {
499503
if isForbidden(err) && g.OnXPN() {
500504
klog.V(2).Infof("teardownInternalHealthCheckAndFirewall(%v): could not delete health check traffic firewall on XPN cluster. Raising Event.", hcName)
@@ -508,7 +512,9 @@ func (g *Cloud) teardownInternalHealthCheckAndFirewall(svc *v1.Service, hcName s
508512
return nil
509513
}
510514

511-
func (g *Cloud) ensureInternalFirewall(svc *v1.Service, fwName, fwDesc, destinationIP string, sourceRanges []string, portRanges []string, protocol v1.Protocol, nodes []*v1.Node, legacyFwName string) error {
515+
func (g *Cloud) ensureInternalFirewall(svc *v1.Service, fwName, fwDesc, destinationIP string, sourceRanges []string, portRanges []string, protocol v1.Protocol, nodes []*v1.Node, legacyFwName string, shared bool) error {
516+
defer g.lockFirewall(fwName, shared)()
517+
512518
klog.V(2).Infof("ensureInternalFirewall(%v): checking existing firewall", fwName)
513519
targetTags, err := g.GetNodeTags(nodeNames(nodes))
514520
if err != nil {
@@ -595,18 +601,20 @@ func (g *Cloud) ensureInternalFirewalls(loadBalancerName, ipAddress, clusterID s
595601
return err
596602
}
597603

598-
err = g.ensureInternalFirewall(svc, MakeFirewallName(loadBalancerName), fwDesc, ipAddress, sourceRanges.StringSlice(), portRanges, protocol, nodes, loadBalancerName)
604+
err = g.ensureInternalFirewall(svc, MakeFirewallName(loadBalancerName), fwDesc, ipAddress, sourceRanges.StringSlice(), portRanges, protocol, nodes, loadBalancerName, false)
599605
if err != nil {
600606
return err
601607
}
602608

603609
// Second firewall is for health checking nodes / services
604610
fwHCName := makeHealthCheckFirewallName(loadBalancerName, clusterID, sharedHealthCheck)
605611
hcSrcRanges := L4LoadBalancerSrcRanges()
606-
return g.ensureInternalFirewall(svc, fwHCName, "", "", hcSrcRanges, []string{healthCheckPort}, v1.ProtocolTCP, nodes, "")
612+
return g.ensureInternalFirewall(svc, fwHCName, "", "", hcSrcRanges, []string{healthCheckPort}, v1.ProtocolTCP, nodes, "", sharedHealthCheck)
607613
}
608614

609615
func (g *Cloud) ensureInternalHealthCheck(name string, svcName types.NamespacedName, shared bool, path string, port int32) (*compute.HealthCheck, error) {
616+
defer g.lockHealthCheck(name, shared)()
617+
610618
klog.V(2).Infof("ensureInternalHealthCheck(%v, %v, %v): checking existing health check", name, path, port)
611619
expectedHC := newInternalLBHealthCheck(name, svcName, shared, path, port)
612620

@@ -646,6 +654,8 @@ func (g *Cloud) ensureInternalHealthCheck(name string, svcName types.NamespacedN
646654
}
647655

648656
func (g *Cloud) ensureInternalInstanceGroup(name, zone string, nodes []*v1.Node, emptyZoneNodes []*v1.Node) (string, error) {
657+
defer g.lockInstanceGroup(name, zone)()
658+
649659
klog.V(2).Infof("ensureInternalInstanceGroup(%v, %v): checking group that it contains %v nodes [node names limited, total number of nodes: %d], the following nodes have empty string in the zone field and won't be deleted: %v", name, zone, loggableNodeNames(nodes), len(nodes), loggableNodeNames(emptyZoneNodes))
650660
ig, err := g.GetInstanceGroup(name, zone)
651661
if err != nil && !isNotFound(err) {
@@ -783,14 +793,24 @@ func (g *Cloud) ensureInternalInstanceGroupsDeleted(name string) error {
783793
if !g.AlphaFeatureGate.Enabled(AlphaFeatureSkipIGsManagement) {
784794
klog.V(2).Infof("ensureInternalInstanceGroupsDeleted(%v): attempting delete instance group in all %d zones", name, len(zones))
785795
for _, z := range zones {
786-
if err := g.DeleteInstanceGroup(name, z.Name); err != nil && !isNotFoundOrInUse(err) {
796+
err := func() error {
797+
defer g.lockInstanceGroup(name, z.Name)()
798+
799+
if err := g.DeleteInstanceGroup(name, z.Name); err != nil && !isNotFoundOrInUse(err) {
800+
return err
801+
}
802+
return nil
803+
}()
804+
if err != nil {
787805
return err
788806
}
789807
}
790808
}
791809
return nil
792810
}
793811

812+
// Note: In the case of shared backend services,
813+
// concurrent updates are safely serialized by GCE's Optimistic Concurrency Control using resource fingerprints.
794814
func (g *Cloud) ensureInternalBackendService(name, description string, affinityType v1.ServiceAffinity, scheme cloud.LbScheme, protocol v1.Protocol, igLinks []string, hcLink string) error {
795815
klog.V(2).Infof("ensureInternalBackendService(%v, %v, %v): checking existing backend service with %d groups", name, scheme, protocol, len(igLinks))
796816
bs, err := g.GetRegionBackendService(name, g.region)

0 commit comments

Comments
 (0)