-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
✨ Add UnsafeDisableDeepCopy into cache option to avoid deep copy during get/list #1274
✨ Add UnsafeDisableDeepCopy into cache option to avoid deep copy during get/list #1274
Conversation
Hi @FillZpp. Thanks for your PR. I'm waiting for a kubernetes-sigs member to verify that this patch is reasonable to test. If it is, they should reply with Once the patch is verified, the new status will be reflected by the I understand the commands that are listed here. Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes/test-infra repository. |
In Alibaba, we have lots of K8s clusters and some of them have even more than 200k Pods in a single cluster. The default |
This change wont reduce the memory usage of the ListWatch
I would expect those lists to filter by either using a LabelSelector or an Index
I disagree, prejudical to performance for large-scale clusters is it to always filter lists via labels or indexers. I wouldn't trust myself to always correctly do the deepcopy. so IMHO this is a huge footgun, even for ppl that are very familiar with controller-runtime. |
Yes, those lists do have label or field selectors. Let me explain it. Take OpenKruise for example, currently there are seven different controllers in OpenKruise and each of them have multiple workers. In these workload controllers, I take
These examples are so mush, I can't describe all of them. In one word, label/field selectors are useful, but they can not avoid the pressure caused by DeepCopy.
I disagree. If so, why doesn't the informer or lister in client-go call DeepCopy for each object? The kube-controller-manager/kube-scheduler and other controllers based on client-go all list objects without DeepCopy. If someone adds DeepCopy into client-go lister and these controllers, it will degrade the performance of K8s. I agree with the default DeepCopy, but users should have the right to choose not to use it. |
Upstream kube is also very careful about using the cache mutation detector in integration/e2e tests. Generally speaking, I still think that this is likely to cause subtile bugs ppl will not detect and that the memory gain is too little to be worth introducing this. I would assume that if you can afford to run a cluster with 200k pods, you can afford to give your CM 2 gigs more ram - And that this will turn out to be a lot cheaper for you than having to recognize, diagnose and fix bugs that come out of accidentally mutating objects in your cache. If you want to discuss this, could you join the biweekly meeting this Thursday? |
If we do allow the cache to expose internal objects, we should opt for the option to be something like A similar use case like this we encountered in Cluster API has driven us to use metadata only watches, and use a live reader (with something like #1249) when querying for the objects |
@alvaroaleman To hide sensitive data, I can only show part of the heep profile, which was dumped from one of our extend controller in the large-scale cluster. And you can find the DeepCopy in CacheReader costs huge heap memory. |
Good idea. I have updated it. |
/ok-to-test |
Issues go stale after 90d of inactivity. If this issue is safe to close now please do so with Send feedback to sig-contributor-experience at kubernetes/community. |
Stale issues rot after 30d of inactivity. If this issue is safe to close now please do so with Send feedback to sig-contributor-experience at kubernetes/community. |
/retest |
Rotten issues close after 30d of inactivity. Send feedback to sig-contributor-experience at kubernetes/community. |
@fejta-bot: Closed this PR. In response to this:
Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes/test-infra repository. |
/reopen |
@FillZpp: Reopened this PR. In response to this:
Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes/test-infra repository. |
PTAL @alvaroaleman @joelanford |
Please take a look when you have time, thanks. @alvaroaleman @vincepri @joelanford |
@@ -65,6 +66,7 @@ func newSpecificInformersMap(config *rest.Config, | |||
createListWatcher: createListWatcher, | |||
namespace: namespace, | |||
selectors: selectors, | |||
disableDeepCopy: disableDeepCopy, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Generally this looks good, thank you for doing this change.
Only thing I was wondering is if it would make sense to key this by object type (maybe with a *
wildcard to make it global). WDYT @vincepri @joelanford ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated. I agree with that, for we might need to add some optional policies for it in future.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@alvaroaleman I also considered the need for this to be GVK-specific. My comment earlier touches on that:
This option would probably require something like the GVKCaches composite cache proposed in #1591, which allows separate cache implementations per GVK.
It's probably do-able to make this option work in the informersMap on a per-GVK basis, but I feel like there are going to be other use cases around GVK-specific cache semantics (e.g. watch GVKA in namespace foo
, GVKB in namespace bar
and GVKC cluster-wide) that it may be better to keep this minimal.
That said, I see we already have selectors keyed by GVK, so maybe its best to do the same here and re-evaluate where GVK-specifics should live in a separate feature?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That said, I see we already have selectors keyed by GVK, so maybe its best to do the same here and re-evaluate where GVK-specifics should live in a separate feature?
Yeah, that was what I thought, because otherwise we end up in a situation where ppl have to set different options on different levels and that will be pretty confusing. Discussing what option we want to be configurable by GVK how and where in a separate issue is a good idea 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is looking pretty good to me, just a few more comments. Thanks for working on this!
pkg/cache/cache.go
Outdated
// UnsafeDisableCacheDeepCopy indicates not to deep copy objects during get or list objects. | ||
// Be very careful with this, when enabled you must DeepCopy any object before mutating it, | ||
// otherwise you will mutate the object in the cache. | ||
UnsafeDisableCacheDeepCopy *DisableCacheDeepCopy |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this just be a bool
now?
Also name nit: since this is in the cache package on the cache.Options
struct, I don't think we need the word Cache
in the name.
// UnsafeDisableCacheDeepCopy indicates not to deep copy objects during get or list objects. | |
// Be very careful with this, when enabled you must DeepCopy any object before mutating it, | |
// otherwise you will mutate the object in the cache. | |
UnsafeDisableCacheDeepCopy *DisableCacheDeepCopy | |
// UnsafeDisableDeepCopy indicates not to deep copy objects during get or list objects. | |
// Be very careful with this, when enabled you must DeepCopy any object before mutating it, | |
// otherwise you will mutate the object in the cache. | |
UnsafeDisableDeepCopy bool |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this just be a
bool
now?
I changed the type from bool
to struct
in case we might need to add some optional policies for it in future.
Also name nit: since this is in the cache package on the cache.Options struct, I don't think we need the word Cache in the name.
Have removed Cache
in the name.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure if @vincepri @alvaroaleman and @varshaprasad96 have opinions, but I think I'd prefer to make this a bool
since we have no options right now to deal with (and may never).
If we do end up needing to configure other aspects of this in the future, we could introduce an UnsafeDisableDeepCopyOptions
struct alongside this bool
if we wanted to avoid a breaking change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alright, I have changed it back to bool
🤔
@@ -65,6 +66,7 @@ func newSpecificInformersMap(config *rest.Config, | |||
createListWatcher: createListWatcher, | |||
namespace: namespace, | |||
selectors: selectors, | |||
disableDeepCopy: disableDeepCopy, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@alvaroaleman I also considered the need for this to be GVK-specific. My comment earlier touches on that:
This option would probably require something like the GVKCaches composite cache proposed in #1591, which allows separate cache implementations per GVK.
It's probably do-able to make this option work in the informersMap on a per-GVK basis, but I feel like there are going to be other use cases around GVK-specific cache semantics (e.g. watch GVKA in namespace foo
, GVKB in namespace bar
and GVKC cluster-wide) that it may be better to keep this minimal.
That said, I see we already have selectors keyed by GVK, so maybe its best to do the same here and re-evaluate where GVK-specifics should live in a separate feature?
By("verifying the pods have the same pointer addresses") | ||
By("verifying the pointer fields in pod have the same addresses") | ||
Expect(out1).To(Equal(out2)) | ||
Expect(reflect.ValueOf(out1.Labels).Pointer()).To(BeIdenticalTo(reflect.ValueOf(out2.Labels).Pointer())) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
non-blocking nit: pretty sure there's no need to actually fetch the pointer for BeIdenticalTo
. This could be simplified to:
Expect(reflect.ValueOf(out1.Labels).Pointer()).To(BeIdenticalTo(reflect.ValueOf(out2.Labels).Pointer())) | |
Expect(out1.Labels).To(BeIdenticalTo(out2.Labels)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried Expect(out1.Labels).To(BeIdenticalTo(out2.Labels))
before, but it failed. I'm not sure why this happened, the out1.Labels
and out2.Labels
do have the same pointer addresses.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, alright 🤷♂️. Thanks for checking that out!
pkg/cache/internal/cache_reader.go
Outdated
// you must DeepCopy any object before mutating it outside | ||
} else { | ||
// deep copy to avoid mutating cache | ||
// TODO(directxman12): revisit the decision to always deepcopy |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can probably remove this comment now.
// TODO(directxman12): revisit the decision to always deepcopy |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
if c.disableDeepCopy != nil { | ||
// skip deep copy which might be unsafe | ||
// you must DeepCopy any object before mutating it outside | ||
outObj = obj |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to set the GVK here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Set the GVK here might modify the object in Informer. We may just ignore it according to the discussion earlier.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah got it! That earlier discussion makes more sense to me now :)
By("verifying the pointer fields in pod have the same addresses") | ||
Expect(out1).To(Equal(out2)) | ||
Expect(reflect.ValueOf(out1.Labels).Pointer()).To(BeIdenticalTo(reflect.ValueOf(out2.Labels).Pointer())) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we add another expectation that verifies the GVK is set on the returned objects after Get()
and List()
when deep copying is disabled?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When deep copying is disabled, we could not ensure the GVK is set on the returned objects.
/remove-lifecycle rotten |
@alvaroaleman @joelanford Thanks for all your comments. I have done all of them. PTAL again 😆 |
Looks good to me. I think the only outstanding question (in this thread) is whether we should have: UnsafeDisableDeepCopy bool or UnsafeDisableDeepCopy map[schema.GroupVersionKind]bool which would align this with the |
So... How about two fields in
Then developers can choose to disable all with some GVK specified to enable, or enable all with some GVK specified to disable. WDYT @joelanford |
It might be simpler to just define a constant: package cache
const GroupVersionKindAll = schema.GroupVersionKind{} And if it is present in the map, we would skip deep copy for all GVKs. I think this is what @alvaroaleman had in mind, and there's precendence for this in other areas of controller-runtime (e.g. |
Defining its type as However if its type is |
…ng list Signed-off-by: Siyu Wang <FillZpp.pub@gmail.com>
@joelanford I have changed the option type to |
PTAL @alvaroaleman @joelanford when you have time, thanks. |
/approve This looks great @FillZpp! Thanks for sticking with this! |
Thanks for reviewing this @joelanford .Now we need lgtm, PTAL @alvaroaleman |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thank you!
[APPROVALNOTIFIER] This PR is APPROVED This pull-request has been approved by: alvaroaleman, FillZpp, joelanford The full list of commands accepted by this bot can be found here. The pull request process is described here
Needs approval from an approver in each of these files:
Approvers can indicate their approval by writing |
Signed-off-by: Siyu Wang FillZpp.pub@gmail.com
Add UnsafeDisableCacheDeepCopy into cache option to avoid deep copy during list.
fixes #1235
@vincepri @alvaroaleman I fully understand why the CacheReader calls
DeepCopyObject
for each object by default, the directly modification to underlying cache object is terrible. However, we can also provide an option for those people who really need to avoid deep copy.