You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
bug: Creating and then deleting workspaces causes leaked goroutines #3350
get a baseline pprof: http://localhost:6060/debug/pprof/goroutine > pre-ws.pprof
create a simple yaml file for an organization workspace: org-ws.yaml.txt
target kcp, and then create and delete the same org 100 times in a loop: for i in {1..100}; do k apply -f org-ws.yaml; sleep 3; k delete -f org-ws.yaml; sleep 3; done
collect pprof again and observe the large number of goroutines.
Expected Behaviour
Fewer goroutines
Additional Context
This looks related to #3016 although that issue is long since fixed, and didn't require new workspaces.
As far as I can tell, creating a new workspace causes the creation of listeners that wont get cancelled until the sharedIndexInformer is terminated.
The wait group & backoff utilities show substantial growth
[k8s.io/apimachinery/pkg/util/wait.(*Group).Start.func1](http://k8s.io/apimachinery/pkg/util/wait.%28*Group%29.Start.func1) increased from 726 (21.40%) to 12,926 (82.90%)
[k8s.io/apimachinery/pkg/util/wait.BackoffUntil](http://k8s.io/apimachinery/pkg/util/wait.BackoffUntil) increased from 756 (22.29%) to 6,856 (43.97%)
[k8s.io/apimachinery/pkg/util/wait.JitterUntil](http://k8s.io/apimachinery/pkg/util/wait.JitterUntil) increased from 680 (20.05%) to 6,780 (43.48%)
Channel operations show significant growth.
runtime.chanrecv increased from 715 (21.08%) to 6,815 (43.71%)
runtime.selectgo increased from 2,074 (61.14%) to 8,176 (52.44%)
The core issue is that the controllers that are started for workspaces register with the shared informers. The goroutines coming out of these registrations are not context-aware, so they are not cancelled when the workspace is deleted and the controllers stopped through their context cancellation.
The controllers started for workspaces would have to de-register themselves from the informers for the goroutines to be closed correctly.
I confirmed this by labeling the goroutines of the different controllers, e.g. the kcp-kubequota controller starts a resource controller here:
It's always 61 leaked goroutines per workspace, so I'd hazard a guess and say that this affects every kubernetes controller started by kcp controllers.
Found another bug in client-go's workqueue. When a workqueue is shutdown (without draining) the .Get method actually keeps returning elements to drain the queue:
Noticed that while working on the tests for the controller runtimes, because there goroutines kept popping up even though the queue and workers should be stopped.
Not directly related to this but could explain some of the goroutines still popping up.
In addition to the fixes for upstream I'm thinking of maybe a WorkspacedSharedInformer, that registers its own handlers with the SharedInformer and maintains the handlers of the workspace controllers separately. If this informer is context cancelled the goroutines would be eliminated as well.
Just an idea though, gonna look into implementing that tomorrow.
Another thing is a test to check for leaking goroutines generally to catch whenever this happens in the future. In combination with the WorkspacesSharedInformer this could check for both any leaking goroutines as well as how many leftover handlers the workspace informer cleaned up.
It is interesting, so the controllers in kcp has a lifecycle than can be started and stopped and restarted again? that is what leads to goroutine leak, but in kube the controllers can start and then stop to die
Exactly - and it doesn't show up because the leaked goroutines we are experiencing is from the informer which for us lives longer than those controllers, so the handlers keep running.
For kube the informer is shutdown at the same time as everything else so the leaks aren't showing up.
This leakage would also occur if kube would start to restart controllers (and partially does so for QuotaMonitors, although that only happens if resources are removed).
Activity
embik commentedon Mar 26, 2025
Hi @julian-hj, thanks for reporting this issue in such detail! We'll take a look at fixing this.
ntnn commentedon Mar 27, 2025
@embik Could you please assign the issue to me? Thanks!
embik commentedon Mar 27, 2025
/assign @ntnn
Thanks!
sanjimoh commentedon Mar 27, 2025
Repro was straightforward with the mentioned steps from @julian-hj . Below are some of my observations from the collected pprofs.
There is a significant spike in Goroutine Count of ~4.6x
Major Contributors to Goroutine Growth deducted from the pprof:
ntnn commentedon Apr 2, 2025
The core issue is that the controllers that are started for workspaces register with the shared informers. The goroutines coming out of these registrations are not context-aware, so they are not cancelled when the workspace is deleted and the controllers stopped through their context cancellation.
The controllers started for workspaces would have to de-register themselves from the informers for the goroutines to be closed correctly.
I confirmed this by labeling the goroutines of the different controllers, e.g. the kcp-kubequota controller starts a resource controller here:
kcp/pkg/reconciler/kubequota/kubequota_controller.go
Line 280 in e4f61b1
Which leads to here:
https://github.com/kubernetes/kubernetes/blob/ef7d51e5ba77004cb95dc2127ed90f8bbfdabc72/pkg/controller/resourcequota/resource_quota_controller.go#L106
Which is then registering into the provided informer here:
https://github.com/kubernetes/kubernetes/blob/ef7d51e5ba77004cb95dc2127ed90f8bbfdabc72/pkg/controller/resourcequota/resource_quota_controller.go#L128-L157
However this registration is never cleaned up, but should be part of the defer stack in the
.Runmethod:https://github.com/kubernetes/kubernetes/blob/ef7d51e5ba77004cb95dc2127ed90f8bbfdabc72/pkg/controller/resourcequota/resource_quota_controller.go#L293-L296
Compare:
ntnn/kubernetes@416e466
And this is just one leak - the kcp-kubequota is actually leaking three goroutines; the other two come from the initial sync:
It's always 61 leaked goroutines per workspace, so I'd hazard a guess and say that this affects every kubernetes controller started by kcp controllers.
ntnn commentedon Apr 2, 2025
The leftover leaks in kcb-kubequota come from basically the same thing - controllers being attached to informers without deregistering when they end:
https://github.com/kubernetes/kubernetes/blob/ef7d51e5ba77004cb95dc2127ed90f8bbfdabc72/pkg/controller/resourcequota/resource_quota_monitor.go#L174
Compare:
ntnn/kubernetes@8adb6cf
The best approach would probably be to fix all leftovers of event handlers.
ntnn commentedon Apr 4, 2025
It seems that this actually was (almost) all the leaking goroutines:
ntnn commentedon Apr 4, 2025
To reproduce use the branch
kcp3350of my kcp fork: https://github.com/ntnn/kcp/tree/kcp3350And rewrite
k8s.io/kubernetesto branchkcp3350-kcpof my kubernetes fork: https://github.com/ntnn/kubernetes/tree/kcp3350-kcpI'm gonna take a closer look at the pprof diff next week.
ntnn commentedon Apr 7, 2025
Found another bug in client-go's workqueue. When a workqueue is shutdown (without draining) the
.Getmethod actually keeps returning elements to drain the queue:ntnn/kubernetes@0a59de4
Noticed that while working on the tests for the controller runtimes, because there goroutines kept popping up even though the queue and workers should be stopped.
Not directly related to this but could explain some of the goroutines still popping up.
ntnn commentedon Apr 8, 2025
In addition to the fixes for upstream I'm thinking of maybe a
WorkspacedSharedInformer, that registers its own handlers with theSharedInformerand maintains the handlers of the workspace controllers separately. If this informer is context cancelled the goroutines would be eliminated as well.Just an idea though, gonna look into implementing that tomorrow.
Another thing is a test to check for leaking goroutines generally to catch whenever this happens in the future. In combination with the
WorkspacesSharedInformerthis could check for both any leaking goroutines as well as how many leftover handlers the workspace informer cleaned up.aojea commentedon Apr 9, 2025
It is interesting, so the controllers in kcp has a lifecycle than can be started and stopped and restarted again? that is what leads to goroutine leak, but in kube the controllers can start and then stop to die
ntnn commentedon Apr 9, 2025
Exactly - and it doesn't show up because the leaked goroutines we are experiencing is from the informer which for us lives longer than those controllers, so the handlers keep running.
For kube the informer is shutdown at the same time as everything else so the leaks aren't showing up.
This leakage would also occur if kube would start to restart controllers (and partially does so for QuotaMonitors, although that only happens if resources are removed).
stevekuznetsov commentedon Apr 10, 2025
Amazing work @ntnn!
ntnn commentedon Jun 30, 2025
Blocked by kcp-dev/client-go#47
After the update is done
ClusterWithContextcan be implemented to prevent the leaks.