Skip to content

Commit 3b876d4

Browse files
perf: [NPM] make apply ipsets faster (#1307)
* wip * fix member update * fix hashed name bug * fix hashed name bug and bug for original members for delete cache * fix delete bug * print cache in logs again * some UTs * dirty cache UTs * fix windows build problem * dirty cache with members to add/delete & keep old apply with save file code * fix windows * fix windows 2 * fix windows 3 * UTs and dirty cache in same struct shared between each OS * fix windows build * change some error situations to valid situations * log clean up and addressing comments
1 parent 704ba1a commit 3b876d4

13 files changed

+1327
-255
lines changed
Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
package ipsets
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
7+
"github.com/Azure/azure-container-networking/npm/metrics"
8+
"github.com/Azure/azure-container-networking/npm/util"
9+
"k8s.io/klog"
10+
)
11+
12+
/*
13+
dirtyCacheInterface will maintain the dirty cache.
14+
It may maintain membersToAdd and membersToDelete.
15+
Members are either IPs, CIDRs, IP-Port pairs, or prefixed set names if the parent is a list.
16+
17+
Assumptions:
18+
- if the set becomes dirty via update or destroy, then the set WAS in the kernel before
19+
- if the set becomes dirty via create, then the set was NOT in the kernel before
20+
21+
Usage:
22+
- create, addMember, deleteMember, and destroy are idempotent
23+
- create should not be called if the set becomes dirty via add/delete or the set is removed from the deleteCache via add/update
24+
- deleteMember should not be called if the set is in the deleteCache
25+
- deleteMember is safe to call on members in the kernel and members added via addMember
26+
- deleteMember is also safe to call on members not in the kernel if the set isn't in the kernel yet (became dirty via create)
27+
28+
Examples of Expected Behavior:
29+
- if a set is created and then destroyed, that set will not be in the dirty cache anymore
30+
- if a set is updated and then destroyed, that set will be in the delete cache
31+
- if the only operations on a set are adding and removing the same member, the set may still be in the dirty cache, but the member will be untracked
32+
*/
33+
type dirtyCacheInterface interface {
34+
// reset empties dirty cache
35+
reset()
36+
// resetAddOrUpdateCache empties the dirty cache of sets to be created or updated
37+
resetAddOrUpdateCache()
38+
// create will mark the new set to be created.
39+
create(set *IPSet)
40+
// addMember will mark the set to be updated and track the member to be added (if implemented).
41+
addMember(set *IPSet, member string)
42+
// deleteMember will mark the set to be updated and track the member to be deleted (if implemented).
43+
deleteMember(set *IPSet, member string)
44+
// delete will mark the set to be deleted in the cache
45+
destroy(set *IPSet)
46+
// setsToAddOrUpdate returns the set names to be added or updated
47+
setsToAddOrUpdate() map[string]struct{}
48+
// setsToDelete returns the set names to be deleted
49+
setsToDelete() map[string]struct{}
50+
// numSetsToAddOrUpdate returns the number of sets to be added or updated
51+
numSetsToAddOrUpdate() int
52+
// numSetsToDelete returns the number of sets to be deleted
53+
numSetsToDelete() int
54+
// isSetToAddOrUpdate returns true if the set is dirty and should be added or updated
55+
isSetToAddOrUpdate(setName string) bool
56+
// isSetToDelete returns true if the set is dirty and should be deleted
57+
isSetToDelete(setName string) bool
58+
// printAddOrUpdateCache returns a string representation of the add/update cache
59+
printAddOrUpdateCache() string
60+
// printDeleteCache returns a string representation of the delete cache
61+
printDeleteCache() string
62+
// memberDiff returns the member diff for the set.
63+
// Will create a new memberDiff if the setName isn't in the dirty cache.
64+
memberDiff(setName string) *memberDiff
65+
}
66+
67+
type dirtyCache struct {
68+
// all maps have keys of set names and values of members to add/delete
69+
toCreateCache map[string]*memberDiff
70+
toUpdateCache map[string]*memberDiff
71+
toDestroyCache map[string]*memberDiff
72+
}
73+
74+
func newDirtyCache() *dirtyCache {
75+
dc := &dirtyCache{}
76+
dc.reset()
77+
return dc
78+
}
79+
80+
func (dc *dirtyCache) reset() {
81+
dc.toCreateCache = make(map[string]*memberDiff)
82+
dc.toUpdateCache = make(map[string]*memberDiff)
83+
dc.toDestroyCache = make(map[string]*memberDiff)
84+
}
85+
86+
func (dc *dirtyCache) resetAddOrUpdateCache() {
87+
dc.toCreateCache = make(map[string]*memberDiff)
88+
dc.toUpdateCache = make(map[string]*memberDiff)
89+
}
90+
91+
func (dc *dirtyCache) create(set *IPSet) {
92+
if _, ok := dc.toCreateCache[set.Name]; ok {
93+
return
94+
}
95+
// error checking
96+
if _, ok := dc.toUpdateCache[set.Name]; ok {
97+
msg := fmt.Sprintf("create should not be called for set %s since it's in the toUpdateCache", set.Name)
98+
klog.Warning(msg)
99+
metrics.SendErrorLogAndMetric(util.IpsmID, msg)
100+
return
101+
}
102+
103+
diff, ok := dc.toDestroyCache[set.Name]
104+
if ok {
105+
// transfer from toDestroyCache to toUpdateCache and maintain member diff
106+
dc.toUpdateCache[set.Name] = diff
107+
delete(dc.toDestroyCache, set.Name)
108+
} else {
109+
// put in the toCreateCache
110+
dc.toCreateCache[set.Name] = diffOnCreate(set)
111+
}
112+
}
113+
114+
// could optimize Linux to remove from toUpdateCache if there were no member diff afterwards,
115+
// but leaving as is prevents difference between OS caches
116+
func (dc *dirtyCache) addMember(set *IPSet, member string) {
117+
diff, ok := dc.toCreateCache[set.Name]
118+
if !ok {
119+
diff, ok = dc.toUpdateCache[set.Name]
120+
if !ok {
121+
diff, ok = dc.toDestroyCache[set.Name]
122+
if !ok {
123+
diff = newMemberDiff()
124+
}
125+
}
126+
dc.toUpdateCache[set.Name] = diff
127+
}
128+
delete(dc.toDestroyCache, set.Name)
129+
diff.addMember(member)
130+
}
131+
132+
// could optimize Linux to remove from toUpdateCache if there were no member diff afterwards,
133+
// but leaving as is prevents difference between OS caches
134+
func (dc *dirtyCache) deleteMember(set *IPSet, member string) {
135+
// error checking #1
136+
if dc.isSetToDelete(set.Name) {
137+
msg := fmt.Sprintf("attempting to delete member %s for set %s in the toDestroyCache", member, set.Name)
138+
klog.Warning(msg)
139+
metrics.SendErrorLogAndMetric(util.IpsmID, msg)
140+
return
141+
}
142+
if diff, ok := dc.toCreateCache[set.Name]; ok {
143+
// don't mark a member to be deleted if it never existed in the kernel
144+
diff.removeMemberFromDiffToAdd(member)
145+
} else {
146+
diff, ok := dc.toUpdateCache[set.Name]
147+
if !ok {
148+
diff = newMemberDiff()
149+
}
150+
dc.toUpdateCache[set.Name] = diff
151+
diff.deleteMember(member)
152+
}
153+
}
154+
155+
func (dc *dirtyCache) destroy(set *IPSet) {
156+
if dc.isSetToDelete(set.Name) {
157+
return
158+
}
159+
160+
if _, ok := dc.toCreateCache[set.Name]; !ok {
161+
// mark all current members as membersToDelete to accommodate force delete
162+
diff, ok := dc.toUpdateCache[set.Name]
163+
if !ok {
164+
diff = newMemberDiff()
165+
}
166+
if set.Kind == HashSet {
167+
for ip := range set.IPPodKey {
168+
diff.deleteMember(ip)
169+
}
170+
} else {
171+
for _, memberSet := range set.MemberIPSets {
172+
diff.deleteMember(memberSet.HashedName)
173+
}
174+
}
175+
// must call this after deleteMember for correct member diff
176+
diff.resetMembersToAdd()
177+
178+
// put the set/diff in the toDestroyCache
179+
dc.toDestroyCache[set.Name] = diff
180+
}
181+
// remove set from toCreateCache or toUpdateCache if necessary
182+
// if the set/diff was in the toCreateCache before, we'll forget about it
183+
delete(dc.toCreateCache, set.Name)
184+
delete(dc.toUpdateCache, set.Name)
185+
}
186+
187+
func (dc *dirtyCache) setsToAddOrUpdate() map[string]struct{} {
188+
sets := make(map[string]struct{}, len(dc.toCreateCache)+len(dc.toUpdateCache))
189+
for set := range dc.toCreateCache {
190+
sets[set] = struct{}{}
191+
}
192+
for set := range dc.toUpdateCache {
193+
sets[set] = struct{}{}
194+
}
195+
return sets
196+
}
197+
198+
func (dc *dirtyCache) setsToDelete() map[string]struct{} {
199+
sets := make(map[string]struct{}, len(dc.toDestroyCache))
200+
for setName := range dc.toDestroyCache {
201+
sets[setName] = struct{}{}
202+
}
203+
return sets
204+
}
205+
206+
func (dc *dirtyCache) numSetsToAddOrUpdate() int {
207+
return len(dc.toCreateCache) + len(dc.toUpdateCache)
208+
}
209+
210+
func (dc *dirtyCache) numSetsToDelete() int {
211+
return len(dc.toDestroyCache)
212+
}
213+
214+
func (dc *dirtyCache) isSetToAddOrUpdate(setName string) bool {
215+
_, ok1 := dc.toCreateCache[setName]
216+
_, ok2 := dc.toUpdateCache[setName]
217+
return ok1 || ok2
218+
}
219+
220+
func (dc *dirtyCache) isSetToDelete(setName string) bool {
221+
_, ok := dc.toDestroyCache[setName]
222+
return ok
223+
}
224+
225+
func (dc *dirtyCache) printAddOrUpdateCache() string {
226+
toCreate := make([]string, 0, len(dc.toCreateCache))
227+
for setName, diff := range dc.toCreateCache {
228+
toCreate = append(toCreate, fmt.Sprintf("%s: %+v", setName, diff))
229+
}
230+
toUpdate := make([]string, 0, len(dc.toUpdateCache))
231+
for setName, diff := range dc.toUpdateCache {
232+
toUpdate = append(toUpdate, fmt.Sprintf("%s: %+v", setName, diff))
233+
}
234+
return fmt.Sprintf("to create: [%+v], to update: [%+v]", strings.Join(toCreate, ","), strings.Join(toUpdate, ","))
235+
}
236+
237+
func (dc *dirtyCache) printDeleteCache() string {
238+
return fmt.Sprintf("%+v", dc.toDestroyCache)
239+
}
240+
241+
func (dc *dirtyCache) memberDiff(setName string) *memberDiff {
242+
if diff, ok := dc.toCreateCache[setName]; ok {
243+
return diff
244+
}
245+
if diff, ok := dc.toUpdateCache[setName]; ok {
246+
return diff
247+
}
248+
if diff, ok := dc.toDestroyCache[setName]; ok {
249+
return diff
250+
}
251+
return newMemberDiff()
252+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package ipsets
2+
3+
type memberDiff struct {
4+
membersToAdd map[string]struct{}
5+
membersToDelete map[string]struct{}
6+
}
7+
8+
func newMemberDiff() *memberDiff {
9+
return &memberDiff{
10+
membersToAdd: make(map[string]struct{}),
11+
membersToDelete: make(map[string]struct{}),
12+
}
13+
}
14+
15+
func diffOnCreate(set *IPSet) *memberDiff {
16+
// mark all current members as membersToAdd
17+
var members map[string]struct{}
18+
if set.Kind == HashSet {
19+
members = make(map[string]struct{}, len(set.IPPodKey))
20+
for ip := range set.IPPodKey {
21+
members[ip] = struct{}{}
22+
}
23+
} else {
24+
members = make(map[string]struct{}, len(set.MemberIPSets))
25+
for _, memberSet := range set.MemberIPSets {
26+
members[memberSet.HashedName] = struct{}{}
27+
}
28+
}
29+
return &memberDiff{
30+
membersToAdd: members,
31+
membersToDelete: make(map[string]struct{}),
32+
}
33+
}
34+
35+
func (diff *memberDiff) addMember(member string) {
36+
_, ok := diff.membersToDelete[member]
37+
if ok {
38+
delete(diff.membersToDelete, member)
39+
} else {
40+
diff.membersToAdd[member] = struct{}{}
41+
}
42+
}
43+
44+
func (diff *memberDiff) deleteMember(member string) {
45+
_, ok := diff.membersToAdd[member]
46+
if ok {
47+
delete(diff.membersToAdd, member)
48+
} else {
49+
diff.membersToDelete[member] = struct{}{}
50+
}
51+
}
52+
53+
func (diff *memberDiff) removeMemberFromDiffToAdd(member string) {
54+
delete(diff.membersToAdd, member)
55+
}
56+
57+
func (diff *memberDiff) resetMembersToAdd() {
58+
diff.membersToAdd = make(map[string]struct{})
59+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package ipsets
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
)
8+
9+
func assertDiff(t *testing.T, expected testDiff, actual *memberDiff) {
10+
if len(expected.toAdd) == 0 {
11+
require.Equal(t, 0, len(actual.membersToAdd), "expected 0 members to add")
12+
} else {
13+
require.Equal(t, stringSliceToSet(expected.toAdd), actual.membersToAdd, "unexpected members to add for set")
14+
}
15+
if len(expected.toDelete) == 0 {
16+
require.Equal(t, 0, len(actual.membersToDelete), "expected 0 members to delete")
17+
} else {
18+
require.Equal(t, stringSliceToSet(expected.toDelete), actual.membersToDelete, "unexpected members to delete for set")
19+
}
20+
}
21+
22+
func stringSliceToSet(s []string) map[string]struct{} {
23+
m := make(map[string]struct{}, len(s))
24+
for _, v := range s {
25+
m[v] = struct{}{}
26+
}
27+
return m
28+
}

0 commit comments

Comments
 (0)