Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extract and optimize group member calculation #1937

Merged
merged 1 commit into from
Mar 18, 2021
Merged

Conversation

tnqn
Copy link
Member

@tnqn tnqn commented Mar 3, 2021

Previously grouping member calculation was coupled with the NetworkPolicyController, and there were two kinds of heavy calculation in the process:
For each event like Pod add, it needed to find all affected groups by matching the new Pod's labels with all groups that can potentially select it. If it was an update event, it even needed to perform the process one more time for its old labels. Then it triggered the sync process of the affected groups.
For the sync process of a group, it needed to find all Pods or ExternalEntities by matching its selectors with all entities that can potentially match it.
For each pair of Pod and Group, they would be matched at least twice. Besides, whenever the group was processed another time, it would again scan all Pods.

The repeated calculation can be eliminated by caching the result of the first matching process when processing Pod events. Then the matching process will only occur when there's a new Pod or a new Group.
Besides, most Pods actually share same labels with others when they are managed by same controller. The cache can be further optimized to maintain the relationship between label set and label selector, making Pods that having same labels and Groups that having same selectors share the cache.

This patch introduces an interface which is responsible for group member calculation and an implementation implementing it with the above optimization. The interface is then consumed by NetworkPolicyController and features like EndpointQuerier and ClusterGroup to retrieve Pods/ExternalEntities selected by a given Group or Groups that select a given Pod. Besides, other controllers that have the group logic like EgressPolicy can consume it directly without having redundant code with NetworkPolicyController.

Performance impact:

name                                       old time/op    new time/op    delta
InitXLargeScaleWithSmallNamespaces-48         3.65s ±14%     4.35s ± 7%  +19.20%  (p=0.016 n=5+5)
InitXLargeScaleWithLargeNamespaces-48         5.34s ± 2%     1.48s ± 5%  -72.34%  (p=0.008 n=5+5)
InitXLargeScaleWithOneNamespace-48            3.22s ± 8%     3.46s ± 2%     ~     (p=0.056 n=5+5)
InitXLargeScaleWithNetpolPerPod-48            45.9s ±11%     25.6s ± 4%  -44.18%  (p=0.008 n=5+5)
InitXLargeScaleWithClusterScopedNetpol-48     1.40s ± 3%     1.18s ± 5%  -16.08%  (p=0.008 n=5+5)

name                                       old alloc/op   new alloc/op   delta
InitXLargeScaleWithSmallNamespaces-48         824MB ± 0%     900MB ± 0%   +9.22%  (p=0.008 n=5+5)
InitXLargeScaleWithLargeNamespaces-48         629MB ± 0%     321MB ± 0%  -49.00%  (p=0.008 n=5+5)
InitXLargeScaleWithOneNamespace-48            921MB ± 0%     922MB ± 0%   +0.13%  (p=0.008 n=5+5)
InitXLargeScaleWithNetpolPerPod-48           3.39GB ± 0%    0.13GB ± 0%  -96.19%  (p=0.008 n=5+5)
InitXLargeScaleWithClusterScopedNetpol-48     279MB ± 0%     244MB ± 0%  -12.59%  (p=0.008 n=5+5)

name                                       old allocs/op  new allocs/op  delta
InitXLargeScaleWithSmallNamespaces-48         12.0M ± 0%     12.6M ± 0%   +5.35%  (p=0.008 n=5+5)
InitXLargeScaleWithLargeNamespaces-48         4.29M ± 0%     4.48M ± 0%   +4.63%  (p=0.008 n=5+5)
InitXLargeScaleWithOneNamespace-48            1.39M ± 0%     1.40M ± 0%   +0.74%  (p=0.008 n=5+5)
InitXLargeScaleWithNetpolPerPod-48            1.66M ± 0%     1.75M ± 0%   +5.52%  (p=0.008 n=5+5)
InitXLargeScaleWithClusterScopedNetpol-48     3.07M ± 0%     3.20M ± 0%   +4.04%  (p=0.008 n=5+5)

Explanation:

  • InitXLargeScaleWithSmallNamespaces has only 4 Pods in each Namespace and all NetworkPolicies are namespace scoped, so even without the index, each policy just needs to scan 4 Pods at most. Maintaing the index adds few overhead, hence the minor increment in time and memory.
  • InitXLargeScaleWithLargeNamespaces has 1000 Pods and 100 NetworkPolicies in each Namespace, each NetworkPolicy applies to 10 Pods. Pods that have same labels will only be scaned once so many calculation is saved. When syncing groups, the result can be got directly without listing all Pods in the Namespace first, so calculation and memory is saved.
  • InitXLargeScaleWithOneNamespace just has many Pods but only 1 namespace, 1 AppliedToGroup and 1 AddressGroup in total, so having the index doesn't help anything.
  • InitXLargeScaleNetpolPerPod has only 1 namespace, 10000 Pods and 1 NetworkPolicy per Pod. It can benefit from the index greatly for the same reason as InitXLargeScaleWithLargeNamespaces.

@tnqn
Copy link
Member Author

tnqn commented Mar 3, 2021

@jianjuns @antoninbas @Dyanngg Please let me know if the change makes sense to you. And whether it can help nested group feature or make it harder? @Dyanngg
I will add performance impact later.

@codecov-io
Copy link

codecov-io commented Mar 3, 2021

Codecov Report

Merging #1937 (0b7d0d4) into main (e80ab3b) will increase coverage by 2.34%.
The diff coverage is 83.56%.

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #1937      +/-   ##
==========================================
+ Coverage   64.35%   66.69%   +2.34%     
==========================================
  Files         193      197       +4     
  Lines       16967    17174     +207     
==========================================
+ Hits        10919    11455     +536     
+ Misses       4899     4551     -348     
- Partials     1149     1168      +19     
Flag Coverage Δ
e2e-tests 26.55% <37.11%> (?)
kind-e2e-tests 56.39% <67.34%> (+1.18%) ⬆️
unit-tests 42.17% <82.86%> (+0.52%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Impacted Files Coverage Δ
pkg/controller/networkpolicy/store/group.go 80.00% <ø> (+8.00%) ⬆️
pkg/controller/types/group.go 0.00% <0.00%> (ø)
pkg/controller/grouping/controller.go 64.64% <64.64%> (ø)
pkg/controller/networkpolicy/clustergroup.go 87.79% <87.50%> (+0.22%) ⬆️
pkg/controller/grouping/group_entity_index.go 94.58% <94.58%> (ø)
pkg/controller/networkpolicy/crd_utils.go 88.60% <100.00%> (-1.61%) ⬇️
pkg/controller/networkpolicy/endpoint_querier.go 91.42% <100.00%> (+4.94%) ⬆️
...ntroller/networkpolicy/networkpolicy_controller.go 84.61% <100.00%> (+0.49%) ⬆️
pkg/apis/controlplane/sets.go 33.33% <0.00%> (-12.83%) ⬇️
pkg/apiserver/certificate/cacert_controller.go 55.83% <0.00%> (-10.01%) ⬇️
... and 33 more

@Dyanngg
Copy link
Contributor

Dyanngg commented Mar 3, 2021

@jianjuns @antoninbas @Dyanngg Please let me know if the change makes sense to you. And whether it can help nested group feature or make it harder? @Dyanngg
I will add performance impact later.

I'm still reviewing the GroupEntityIndex interface, but after the first look I don't think it will make nested group computation easier or more complicated. It's just that the nested group member calc logic would be entirely re-written, since right now I rely on the groupMember field of the internalGroup to union nested groupMembers, which is removed by this PR.

Copy link
Contributor

@antoninbas antoninbas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am fine with the approach. What's the memory usage impact of the change when running the benchmarks in networkpolicy_controller_perf_test.go?

pkg/controller/grouping/controller.go Outdated Show resolved Hide resolved
pkg/controller/grouping/controller.go Outdated Show resolved Hide resolved
pkg/controller/networkpolicy/clustergroup.go Outdated Show resolved Hide resolved
pkg/controller/grouping/group_entity_index.go Outdated Show resolved Hide resolved
pkg/controller/grouping/group_entity_index.go Outdated Show resolved Hide resolved
pkg/controller/grouping/group_entity_index.go Outdated Show resolved Hide resolved
pkg/controller/grouping/group_entity_index.go Outdated Show resolved Hide resolved
pkg/controller/grouping/group_entity_index.go Outdated Show resolved Hide resolved
@tnqn
Copy link
Member Author

tnqn commented Mar 4, 2021

@jianjuns @antoninbas @Dyanngg Please let me know if the change makes sense to you. And whether it can help nested group feature or make it harder? @Dyanngg
I will add performance impact later.

I'm still reviewing the GroupEntityIndex interface, but after the first look I don't think it will make nested group computation easier or more complicated. It's just that the nested group member calc logic would be entirely re-written, since right now I rely on the groupMember field of the internalGroup to union nested groupMembers, which is removed by this PR.

I removed the field as it was consumed by GetGroupMembers, populateAddressGroupMemberSet and getAppliedToWorkloads when they need to know the members of a clustergroup but with the change it can be achieved by querying the GroupEntityIndex directly, so the internalGroup itself doesn't have to store the redundant data. I can imagine that in nested group case, you want to avoid calculate the union of the members again when querying its member, right? If I understand correctly, I can add this field back or let's see whether there is more efficient way.

Copy link
Member Author

@tnqn tnqn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Dyanngg and @antoninbas thanks for your quick review and feedback.

pkg/controller/grouping/controller.go Outdated Show resolved Hide resolved
pkg/controller/grouping/controller.go Outdated Show resolved Hide resolved
pkg/controller/networkpolicy/clustergroup.go Outdated Show resolved Hide resolved
Copy link
Contributor

@jianjuns jianjuns left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also wonder the memory usage.

And do you think any in-memory store implementation can help (compared to defining all maps case by case)?

pkg/controller/grouping/group_entity_index.go Outdated Show resolved Hide resolved
labelItems map[string]*labelItem
// labelIndex is nested map from entityType to Namespace to keys of labelItems.
// It's used to filter potential labelItems when matching a namespace scoped selectorItem.
labelIndex map[entityType]map[string]sets.String
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it more efficient to create two separate maps for Pods and ExternalEntities?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or to share code, we can convert them to one internal type?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code of matching Pods and ExtenalEntities is already shared and they are treated in same way when stored in labelItems.
Separating them in labelIndex is to reduce the potential labelItems a selector needs to match. This is because a selector can either select ExternalEntity or Pod. If it's a Pod selector, this is no need to try ExternalEntity labelItems. The same reason applies to selectorIndex.

@tnqn tnqn force-pushed the grouping branch 2 times, most recently from fcf9851 to a9126a2 Compare March 10, 2021 17:34
@tnqn
Copy link
Member Author

tnqn commented Mar 10, 2021

@jianjuns @antoninbas I updated benchmark test result in PR description. To get accurate cpu and memory metrics, I used benchmark test and execute the processing in serial manner.

Copy link
Contributor

@antoninbas antoninbas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the changes look good to me, and so do the benchmark results

// GetEntities returns the selected Pods or ExternalEntities for the given group.
GetEntities(groupType string, name string) ([]*v1.Pod, []*v1alpha2.ExternalEntity)
// GetGroupsForPod returns the groups that select the given Pod.
GetGroupsForPod(namespace, name string) (map[string][]string, bool)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you think we could define a new type for the group type: type GroupType string? It would help clarify what the returned values are if we have map[GroupType][]string instead of map[string][]string.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe an enum?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I defined GroupType and listed all values here at the begining, then found it makes consumers must add their own type here. Maybe I should define GroupType (using string) here and let consumers define their own values in their own packages, does it make sense to you?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel enum (int) also works, but I guess your concern is harder to track the int values when they are not in the same package?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I should define GroupType (using string) here and let consumers define their own values in their own packages, does it make sense to you?

That works for me as a middle ground between using a plain string and defining named constants for all group types in this package.

Copy link
Member Author

@tnqn tnqn Mar 12, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jianjuns Yes, if using enum(int), we should define them together in this package to avoid conflict.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is just like string is easier to remember and has less chance to conflict (but can still). I do not like long string lookup, but I understand your point and no strong opinion.

pkg/controller/grouping/group_entity_index.go Outdated Show resolved Hide resolved
pkg/controller/grouping/group_entity_index.go Outdated Show resolved Hide resolved
Copy link
Contributor

@Dyanngg Dyanngg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM overall

pkg/controller/grouping/group_entity_index.go Outdated Show resolved Hide resolved
@@ -81,6 +82,10 @@ const (
PriorityIndex = "priority"
// ClusterGroupIndex is used to index ClusterNetworkPolicies by ClusterGroup names.
ClusterGroupIndex = "clustergroup"

appliedToGroupType = "appliedToGroup"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then do you like to change this one to enum too?

// GetEntities returns the selected Pods or ExternalEntities for the given group.
GetEntities(groupType string, name string) ([]*v1.Pod, []*v1alpha2.ExternalEntity)
// GetGroupsForPod returns the groups that select the given Pod.
GetGroupsForPod(namespace, name string) (map[string][]string, bool)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe an enum?

pkg/controller/grouping/group_entity_index.go Show resolved Hide resolved
pkg/controller/grouping/group_entity_index.go Show resolved Hide resolved
@tnqn
Copy link
Member Author

tnqn commented Mar 11, 2021

/test-all

@tnqn tnqn changed the title [WIP] Extract grouping logic to a generic module Extract grouping logic to a generic module Mar 11, 2021
@tnqn tnqn marked this pull request as ready for review March 11, 2021 17:37
@tnqn tnqn force-pushed the grouping branch 4 times, most recently from def159c to 17f15eb Compare March 16, 2021 15:08
@tnqn tnqn changed the title Extract grouping logic to a generic module Extract and optimize group member calculation Mar 16, 2021
@tnqn
Copy link
Member Author

tnqn commented Mar 16, 2021

/test-all

@tnqn
Copy link
Member Author

tnqn commented Mar 16, 2021

/test-all

@tnqn
Copy link
Member Author

tnqn commented Mar 16, 2021

@jianjuns @antoninbas @Dyanngg I added more unit tests and a method HasSynced to avoid intermediate result after restarting since your last review and have addressed all comments if I'm not missing one. Could you take another look?

Dyanngg
Dyanngg previously approved these changes Mar 17, 2021
Copy link
Contributor

@Dyanngg Dyanngg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

antoninbas
antoninbas previously approved these changes Mar 17, 2021
Copy link
Contributor

@antoninbas antoninbas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for all the work on this. I took another quick look and it looks good to me. I found a couple nits.

pkg/controller/grouping/controller_test.go Outdated Show resolved Hide resolved
pkg/controller/grouping/group_entity_index.go Outdated Show resolved Hide resolved
pkg/controller/grouping/group_entity_index.go Outdated Show resolved Hide resolved
pkg/controller/grouping/group_entity_index.go Outdated Show resolved Hide resolved
pkg/controller/grouping/group_entity_index.go Outdated Show resolved Hide resolved
@tnqn
Copy link
Member Author

tnqn commented Mar 17, 2021

/test-all

antoninbas
antoninbas previously approved these changes Mar 17, 2021
Copy link
Contributor

@antoninbas antoninbas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Dyanngg
Dyanngg previously approved these changes Mar 17, 2021
@Dyanngg
Copy link
Contributor

Dyanngg commented Mar 17, 2021

/test-e2e

jianjuns
jianjuns previously approved these changes Mar 17, 2021
Copy link
Contributor

@jianjuns jianjuns left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a few nits.

pkg/controller/grouping/group_entity_index.go Show resolved Hide resolved
pkg/controller/grouping/group_entity_index.go Outdated Show resolved Hide resolved
pkg/controller/grouping/group_entity_index.go Outdated Show resolved Hide resolved
@tnqn
Copy link
Member Author

tnqn commented Mar 18, 2021

/test-all

Previously grouping member calculation was coupled with the
NetworkPolicyController, and there were two kinds of heavy calculation
in the process: For each event like Pod add, it needed to find all
affected groups by matching the new Pod's labels with all groups that
can potentially select it. If it was an update event, it even needed to
perform the process one more time for its old labels. Then it triggered
the sync process of the affected groups. For the sync process of a
group, it needed to find all Pods or ExternalEntities by matching its
selectors with all entities that can potentially match it. For each pair
of Pod and Group, they would be matched at least twice. Besides,
whenever the group was processed another time, it would again scan all
Pods.

The repeated calculation can be eliminated by caching the result of the
first matching process when processing Pod events. Then the matching
process will only occur when there's a new Pod or a new Group. Besides,
most Pods actually share same labels with others when they are managed
by same controller. The cache can be further optimized to maintain the
relationship between label set and label selector, making Pods that
having same labels and Groups that having same selectors share the
cache.

This patch introduces an interface which is responsible for group member
calculation and an implementation implementing it with the above
optimization. The interface is then consumed by NetworkPolicyController
and features like EndpointQuerier and ClusterGroup to retrieve
Pods/ExternalEntities selected by a given Group or Groups that select a
given Pod. Besides, other controllers that have the group logic like
EgressPolicy can consume it directly without having redundant code with
NetworkPolicyController.
@tnqn
Copy link
Member Author

tnqn commented Mar 18, 2021

/test-conformance
/test-all-features-conformance
/test-windows-conformance
/test-windows-networkpolicy

@tnqn
Copy link
Member Author

tnqn commented Mar 18, 2021

/test-conformance
/test-e2e

@tnqn
Copy link
Member Author

tnqn commented Mar 18, 2021

/test-networkpolicy
/test-e2e
/test-conformance

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants