-
Notifications
You must be signed in to change notification settings - Fork 116
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
Use shared informer for await logic for deployments #1639
Conversation
Does the PR have any schema changes?Looking good! No breaking changes found. |
1 similar comment
Does the PR have any schema changes?Looking good! No breaking changes found. |
Does the PR have any schema changes?Looking good! No breaking changes found. |
Does the PR have any schema changes?Looking good! No breaking changes found. |
Does the PR have any schema changes?Looking good! No breaking changes found. |
A couple of fundamental questions before commenting on details:
|
Kubernetes is pretty well optimized for event handling, and that's generally the recommended approach for clients. I'd prefer to abstract on the client side if we decide to change to a polling approach for await logic. i.e., continue subscribing to events, and have await logic poll a cache if necessary. |
I'm less concerned about Kubernetes not being able to provide events or something, but rather about the consequences of a lost/delayed/wrong-sequenced/duplicate event throwing off our client logic and potentially leading to stuck updates, while being hard to test. Is poll bad in terms of performance? Or what are some concerns about it? |
informer.AddEventHandler(cache.ResourceEventHandlerFuncs{ | ||
AddFunc: func(obj interface{}) { | ||
informChan <- watch.Event{ | ||
Object: obj.(*unstructured.Unstructured), |
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.
We should probably do a defensive type assertion here to avoid panics in case the object is the wrong type somehow.
@@ -63,7 +53,16 @@ import ( | |||
"k8s.io/client-go/tools/clientcmd" | |||
clientapi "k8s.io/client-go/tools/clientcmd/api" | |||
k8sopenapi "k8s.io/kubectl/pkg/util/openapi" | |||
"net/http" |
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.
nit: We've been using goimports
to format the k8s provider code. It looks like we may be using different toolchains, so we should pick one and standardize on it.
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 default to gofmt and assumed make lint would catch it. Sounds good, I will run goimports.
provider/pkg/await/deployment.go
Outdated
return | ||
} | ||
|
||
// Start over, prove that rollout is complete. | ||
dia.deploymentErrors = map[string]string{} | ||
|
||
// Do nothing if this is not the Deployment we're waiting for. | ||
if deployment.GetName() != inputDeploymentName { | ||
if deployment.GetName() != inputDeploymentName || deployment.GetNamespace() != dia.config.currentInputs.GetNamespace() { |
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.
Since the informer is already filtering by namespace, is this necessary?
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.
Yup - makes sense. Will remove.
provider/pkg/await/await.go
Outdated
@@ -215,6 +217,7 @@ func Creation(c CreateConfig) (*unstructured.Unstructured, error) { | |||
urn: c.URN, | |||
initialAPIVersion: c.InitialAPIVersion, | |||
clientSet: c.ClientSet, | |||
informerFactory: dynamicinformer.NewFilteredDynamicSharedInformerFactory(c.ClientSet.GenericClient, 5*time.Second, c.Inputs.GetNamespace(), nil), |
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'm not sure yet if we want to set the refresh period on the informers, and if so, what the period should be. This will need some more testing/tuning.
stopper := make(chan struct{}) | ||
defer close(stopper) | ||
|
||
// Limit the lifetime of this to each deployment await for now. We can reduce this sharing further later. |
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.
Any particular reason for initializing a factory per await? Seems like we could share the factory across the provider without much difference in level of effort.
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.
Any particular reason for initializing a factory per await? Seems like we could share the factory across the provider without much difference in level of effort.
One thing was to set the lifetime of the factory itself (the stop channel to pass to the Start
call in the next line). This way we know it's safe to kill the informer once an individual deployment has been waited on. A shared informer for the entire provider would need some degree of coordination to make sure all the deployments had completed etc. which I didn't want to layer on yet.
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.
Ok. That sounds reasonable as a first step, but presumably doesn't buy us anything in terms of performance since we aren't sharing the cache between resources?
I'm not 100% sure that this is the case, but I believe the await logic should be idempotent. We already support resuming a previous update, which isn't guaranteed to include the full sequence of events. At least for Job and Pod, a single event is sufficient to declare that the resource is ready, so it's not sensitive to ordering/duplicates. We should verify that this is the case for all awaiters, but I'm pretty sure it is.
Yes, polling can cause performance problems for k8s clusters at scale. It's not that polling would not work for our use case, but that watch is preferred to polling in general for k8s. |
I think this is a valid concern. That said, a watch with a reasonably spaced resync interval essentially mimics the poll behavior as a fallback. With this approach we are making it much harder to miss an event. To be clear, the change that we reverted wasn't wrong. We theorize it made the likelihood of hitting an API server-side throttle a bit higher but we are still definitely hitting those throttling events regardless of that change. IMO this is a longstanding issue. I don't know why it is becoming more prevalent now. Perhaps newer api servers are more aggressive on throttling or cloud providers have dialed these up? While I am still working through testing this, I have already seen the informer model handle throttling a lot better. |
What is the failure mode when we hit throttling? Why does it lead to stuck updates as opposed to just slower updates? Any good places for me to read about this? Thank you for bearing with my noob questions! |
Not a problem. The tight loop is just a straight up bug. https://github.com/pulumi/pulumi-kubernetes/blob/master/provider/pkg/await/deployment.go#L314 |
Does the PR have any schema changes?Looking good! No breaking changes found. |
Does the PR have any schema changes?Looking good! No breaking changes found. |
@lblackstone @mikhailshilkov I think this is ready for another look. In my tests things seemed much more stable with this. |
Does the PR have any schema changes?Looking good! No breaking changes found. |
Does the PR have any schema changes?Looking good! No breaking changes found. |
provider/pkg/await/deployment.go
Outdated
Version: "v1beta1", | ||
Resource: "deployments", | ||
}, deploymentEvents) | ||
go deploymentV1Beta1Informer.Informer().Run(stopper) |
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'm not sure if we need Informers for the old apiVersions. I believe the watch clients were previously only using the latest apiVersions.
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 interesting. Does that mean that we don't really support the v1beta1 etc. variants? It seems like we load the latest api versions when creating clients and use them to create watches. I can't really verify this since all the cloud providers seem to have stopped supporting 1.15 or older (bunch of these were killed in 1.16).
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.
Caught up with @lblackstone offline. It seems we should be safe here. Removed non-v1 informer variants.
Does the PR have any schema changes?Looking good! No breaking changes found. |
Builds on #1634.
Fixes #1628
Note - the current iteration launches an informer per deployment instead of having a provider level informer. This is still a pretty significant improvement since all replicasets, pods, pvcs associated with a deployment set use the same informer.
I ran several dozen updates with this and various scenarios. I didn't see any throttles (and if they occurred they were handled gracefully).
We also get the behavior we want for #1502 - since the informer will feed the initial read as an event to the respective channels and we don't miss deployments/replicaset scale ups.
I will follow up with a separate PR for some additional unit tests to cover some more scenarios which might require a little bit of refactoring here.