Fix memory leak in endpointSliceTracker

endpointSliceTracker creates a set of resource versions for each
service, the resource versions in the set could be deleted when
endpointslices are deleted, but the set and its key in the map is never
deleted, leading to memory leak.

This patch deletes the set if the service is deleted, and stops
initializing an empty set when "read-only" methods "Has" and "Stale" are
called.
This commit is contained in:
Quan Tian 2020-07-06 21:51:00 +08:00
parent 865cbf0bdf
commit 087682584d
5 changed files with 129 additions and 21 deletions

View File

@ -66,6 +66,7 @@ go_test(
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/types:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/intstr:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/intstr:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/rand:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/rand:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/wait:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/wait:go_default_library",

View File

@ -307,6 +307,7 @@ func (c *Controller) syncService(key string) error {
if apierrors.IsNotFound(err) { if apierrors.IsNotFound(err) {
c.triggerTimeTracker.DeleteService(namespace, name) c.triggerTimeTracker.DeleteService(namespace, name)
c.reconciler.deleteService(namespace, name) c.reconciler.deleteService(namespace, name)
c.endpointSliceTracker.DeleteService(namespace, name)
// The service has been deleted, return nil so that it won't be retried. // The service has been deleted, return nil so that it won't be retried.
return nil return nil
} }

View File

@ -51,8 +51,11 @@ func (est *endpointSliceTracker) Has(endpointSlice *discovery.EndpointSlice) boo
est.lock.Lock() est.lock.Lock()
defer est.lock.Unlock() defer est.lock.Unlock()
rrv := est.relatedResourceVersions(endpointSlice) rrv, ok := est.relatedResourceVersions(endpointSlice)
_, ok := rrv[endpointSlice.Name] if !ok {
return false
}
_, ok = rrv[endpointSlice.Name]
return ok return ok
} }
@ -63,7 +66,10 @@ func (est *endpointSliceTracker) Stale(endpointSlice *discovery.EndpointSlice) b
est.lock.Lock() est.lock.Lock()
defer est.lock.Unlock() defer est.lock.Unlock()
rrv := est.relatedResourceVersions(endpointSlice) rrv, ok := est.relatedResourceVersions(endpointSlice)
if !ok {
return true
}
return rrv[endpointSlice.Name] != endpointSlice.ResourceVersion return rrv[endpointSlice.Name] != endpointSlice.ResourceVersion
} }
@ -73,33 +79,42 @@ func (est *endpointSliceTracker) Update(endpointSlice *discovery.EndpointSlice)
est.lock.Lock() est.lock.Lock()
defer est.lock.Unlock() defer est.lock.Unlock()
rrv := est.relatedResourceVersions(endpointSlice) rrv, ok := est.relatedResourceVersions(endpointSlice)
if !ok {
rrv = endpointSliceResourceVersions{}
est.resourceVersionsByService[getServiceNN(endpointSlice)] = rrv
}
rrv[endpointSlice.Name] = endpointSlice.ResourceVersion rrv[endpointSlice.Name] = endpointSlice.ResourceVersion
} }
// DeleteService removes the set of resource versions tracked for the Service.
func (est *endpointSliceTracker) DeleteService(namespace, name string) {
est.lock.Lock()
defer est.lock.Unlock()
serviceNN := types.NamespacedName{Name: name, Namespace: namespace}
delete(est.resourceVersionsByService, serviceNN)
}
// Delete removes the resource version in this endpointSliceTracker for the // Delete removes the resource version in this endpointSliceTracker for the
// provided EndpointSlice. // provided EndpointSlice.
func (est *endpointSliceTracker) Delete(endpointSlice *discovery.EndpointSlice) { func (est *endpointSliceTracker) Delete(endpointSlice *discovery.EndpointSlice) {
est.lock.Lock() est.lock.Lock()
defer est.lock.Unlock() defer est.lock.Unlock()
rrv := est.relatedResourceVersions(endpointSlice) rrv, ok := est.relatedResourceVersions(endpointSlice)
if ok {
delete(rrv, endpointSlice.Name) delete(rrv, endpointSlice.Name)
}
} }
// relatedResourceVersions returns the set of resource versions tracked for the // relatedResourceVersions returns the set of resource versions tracked for the
// Service corresponding to the provided EndpointSlice. If no resource versions // Service corresponding to the provided EndpointSlice, and a bool to indicate
// are currently tracked for this service, an empty set is initialized. // if it exists.
func (est *endpointSliceTracker) relatedResourceVersions(endpointSlice *discovery.EndpointSlice) endpointSliceResourceVersions { func (est *endpointSliceTracker) relatedResourceVersions(endpointSlice *discovery.EndpointSlice) (endpointSliceResourceVersions, bool) {
serviceNN := getServiceNN(endpointSlice) serviceNN := getServiceNN(endpointSlice)
vers, ok := est.resourceVersionsByService[serviceNN] vers, ok := est.resourceVersionsByService[serviceNN]
return vers, ok
if !ok {
vers = endpointSliceResourceVersions{}
est.resourceVersionsByService[serviceNN] = vers
}
return vers
} }
// getServiceNN returns a namespaced name for the Service corresponding to the // getServiceNN returns a namespaced name for the Service corresponding to the

View File

@ -19,8 +19,11 @@ package endpointslice
import ( import (
"testing" "testing"
"github.com/stretchr/testify/assert"
discovery "k8s.io/api/discovery/v1beta1" discovery "k8s.io/api/discovery/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
) )
func TestEndpointSliceTrackerUpdate(t *testing.T) { func TestEndpointSliceTrackerUpdate(t *testing.T) {
@ -47,30 +50,51 @@ func TestEndpointSliceTrackerUpdate(t *testing.T) {
checksParam *discovery.EndpointSlice checksParam *discovery.EndpointSlice
expectHas bool expectHas bool
expectStale bool expectStale bool
expectResourceVersionsByService map[types.NamespacedName]endpointSliceResourceVersions
}{ }{
"same slice": { "same slice": {
updateParam: epSlice1, updateParam: epSlice1,
checksParam: epSlice1, checksParam: epSlice1,
expectHas: true, expectHas: true,
expectStale: false, expectStale: false,
expectResourceVersionsByService: map[types.NamespacedName]endpointSliceResourceVersions{
{Namespace: epSlice1.Namespace, Name: "svc1"}: {
epSlice1.Name: epSlice1.ResourceVersion,
},
},
}, },
"different namespace": { "different namespace": {
updateParam: epSlice1, updateParam: epSlice1,
checksParam: epSlice1DifferentNS, checksParam: epSlice1DifferentNS,
expectHas: false, expectHas: false,
expectStale: true, expectStale: true,
expectResourceVersionsByService: map[types.NamespacedName]endpointSliceResourceVersions{
{Namespace: epSlice1.Namespace, Name: "svc1"}: {
epSlice1.Name: epSlice1.ResourceVersion,
},
},
}, },
"different service": { "different service": {
updateParam: epSlice1, updateParam: epSlice1,
checksParam: epSlice1DifferentService, checksParam: epSlice1DifferentService,
expectHas: false, expectHas: false,
expectStale: true, expectStale: true,
expectResourceVersionsByService: map[types.NamespacedName]endpointSliceResourceVersions{
{Namespace: epSlice1.Namespace, Name: "svc1"}: {
epSlice1.Name: epSlice1.ResourceVersion,
},
},
}, },
"different resource version": { "different resource version": {
updateParam: epSlice1, updateParam: epSlice1,
checksParam: epSlice1DifferentRV, checksParam: epSlice1DifferentRV,
expectHas: true, expectHas: true,
expectStale: true, expectStale: true,
expectResourceVersionsByService: map[types.NamespacedName]endpointSliceResourceVersions{
{Namespace: epSlice1.Namespace, Name: "svc1"}: {
epSlice1.Name: epSlice1.ResourceVersion,
},
},
}, },
} }
@ -84,6 +108,7 @@ func TestEndpointSliceTrackerUpdate(t *testing.T) {
if esTracker.Stale(tc.checksParam) != tc.expectStale { if esTracker.Stale(tc.checksParam) != tc.expectStale {
t.Errorf("tc.tracker.Stale(%+v) == %t, expected %t", tc.checksParam, esTracker.Stale(tc.checksParam), tc.expectStale) t.Errorf("tc.tracker.Stale(%+v) == %t, expected %t", tc.checksParam, esTracker.Stale(tc.checksParam), tc.expectStale)
} }
assert.Equal(t, tc.expectResourceVersionsByService, esTracker.resourceVersionsByService)
}) })
} }
} }
@ -172,3 +197,69 @@ func TestEndpointSliceTrackerDelete(t *testing.T) {
}) })
} }
} }
func TestEndpointSliceTrackerDeleteService(t *testing.T) {
svcName1, svcNS1 := "svc1", "ns1"
svcName2, svcNS2 := "svc2", "ns2"
epSlice1 := &discovery.EndpointSlice{
ObjectMeta: metav1.ObjectMeta{
Name: "example-1",
Namespace: svcNS1,
ResourceVersion: "rv1",
Labels: map[string]string{discovery.LabelServiceName: svcName1},
},
}
testCases := map[string]struct {
updateParam *discovery.EndpointSlice
deleteServiceParam *types.NamespacedName
expectHas bool
expectStale bool
expectResourceVersionsByService map[types.NamespacedName]endpointSliceResourceVersions
}{
"same service": {
updateParam: epSlice1,
deleteServiceParam: &types.NamespacedName{Namespace: svcNS1, Name: svcName1},
expectHas: false,
expectStale: true,
expectResourceVersionsByService: map[types.NamespacedName]endpointSliceResourceVersions{},
},
"different namespace": {
updateParam: epSlice1,
deleteServiceParam: &types.NamespacedName{Namespace: svcNS2, Name: svcName1},
expectHas: true,
expectStale: false,
expectResourceVersionsByService: map[types.NamespacedName]endpointSliceResourceVersions{
{Namespace: epSlice1.Namespace, Name: "svc1"}: {
epSlice1.Name: epSlice1.ResourceVersion,
},
},
},
"different service": {
updateParam: epSlice1,
deleteServiceParam: &types.NamespacedName{Namespace: svcNS1, Name: svcName2},
expectHas: true,
expectStale: false,
expectResourceVersionsByService: map[types.NamespacedName]endpointSliceResourceVersions{
{Namespace: epSlice1.Namespace, Name: "svc1"}: {
epSlice1.Name: epSlice1.ResourceVersion,
},
},
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
esTracker := newEndpointSliceTracker()
esTracker.Update(tc.updateParam)
esTracker.DeleteService(tc.deleteServiceParam.Namespace, tc.deleteServiceParam.Name)
if esTracker.Has(tc.updateParam) != tc.expectHas {
t.Errorf("tc.tracker.Has(%+v) == %t, expected %t", tc.updateParam, esTracker.Has(tc.updateParam), tc.expectHas)
}
if esTracker.Stale(tc.updateParam) != tc.expectStale {
t.Errorf("tc.tracker.Stale(%+v) == %t, expected %t", tc.updateParam, esTracker.Stale(tc.updateParam), tc.expectStale)
}
assert.Equal(t, tc.expectResourceVersionsByService, esTracker.resourceVersionsByService)
})
}
}

View File

@ -983,7 +983,7 @@ func expectActions(t *testing.T, actions []k8stesting.Action, num int, verb, res
} }
func expectTrackedResourceVersion(t *testing.T, tracker *endpointSliceTracker, slice *discovery.EndpointSlice, expectedRV string) { func expectTrackedResourceVersion(t *testing.T, tracker *endpointSliceTracker, slice *discovery.EndpointSlice, expectedRV string) {
rrv := tracker.relatedResourceVersions(slice) rrv, _ := tracker.relatedResourceVersions(slice)
rv, tracked := rrv[slice.Name] rv, tracked := rrv[slice.Name]
if !tracked { if !tracked {
t.Fatalf("Expected EndpointSlice %s to be tracked", slice.Name) t.Fatalf("Expected EndpointSlice %s to be tracked", slice.Name)