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

Use shared informer for await logic for deployments #1639

Merged
merged 10 commits into from Jun 30, 2021

Conversation

viveklak
Copy link
Contributor

@viveklak viveklak commented Jun 26, 2021

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.

@viveklak viveklak changed the title Use shared informer for each deployment and downstream resource Use shared informer for await logic for deployments Jun 26, 2021
@github-actions
Copy link

Does the PR have any schema changes?

Looking good! No breaking changes found.
No new resources/functions.

1 similar comment
@github-actions
Copy link

Does the PR have any schema changes?

Looking good! No breaking changes found.
No new resources/functions.

@github-actions
Copy link

Does the PR have any schema changes?

Looking good! No breaking changes found.
No new resources/functions.

@github-actions
Copy link

Does the PR have any schema changes?

Looking good! No breaking changes found.
No new resources/functions.

@github-actions
Copy link

Does the PR have any schema changes?

Looking good! No breaking changes found.
No new resources/functions.

@mikhailshilkov
Copy link
Member

A couple of fundamental questions before commenting on details:

  • Given our previous need to flip back to the previous await logic in a patch release, is there a way we could avoid doing so if this new code goes sideways? Can we add a flag to turn it off? Can we combine both methods somehow?
  • I noticed that the TF provider awaits with polling, at least for deployment. In general, I'm bias towards polling because it's much easier to reason about compared to event handling. Is there a reason we can't use polling here?

@lblackstone
Copy link
Member

  • In general, I'm bias towards polling because it's much easier to reason about compared to event handling. Is there a reason we can't use polling here?

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.

@mikhailshilkov
Copy link
Member

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),
Copy link
Member

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"
Copy link
Member

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.

Copy link
Contributor Author

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.

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() {
Copy link
Member

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?

Copy link
Contributor Author

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.

@@ -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),
Copy link
Member

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.
Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Member

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?

@lblackstone
Copy link
Member

lblackstone commented Jun 28, 2021

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.

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.

Is poll bad in terms of performance? Or what are some concerns about it?

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.

@viveklak
Copy link
Contributor Author

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.

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.

@mikhailshilkov
Copy link
Member

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.

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!

@viveklak
Copy link
Contributor Author

viveklak commented Jun 28, 2021

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.

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
ResultChan() can be closed if there is an error, this could be a throttle or a network blip. As a result, we keep reading from a closed channel. There is a low-level backoff protocol embedded in some wrappers, e.g.: https://github.com/kubernetes/client-go/blob/v0.21.2/tools/watch/retrywatcher.go#L189 but informer handles all this internally.

@github-actions
Copy link

Does the PR have any schema changes?

Looking good! No breaking changes found.
No new resources/functions.

@github-actions
Copy link

Does the PR have any schema changes?

Looking good! No breaking changes found.
No new resources/functions.

@viveklak
Copy link
Contributor Author

@lblackstone @mikhailshilkov I think this is ready for another look. In my tests things seemed much more stable with this.

@github-actions
Copy link

Does the PR have any schema changes?

Looking good! No breaking changes found.
No new resources/functions.

@github-actions
Copy link

Does the PR have any schema changes?

Looking good! No breaking changes found.
No new resources/functions.

Version: "v1beta1",
Resource: "deployments",
}, deploymentEvents)
go deploymentV1Beta1Informer.Informer().Run(stopper)
Copy link
Member

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.

Copy link
Contributor Author

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).

Copy link
Contributor Author

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.

@github-actions
Copy link

Does the PR have any schema changes?

Looking good! No breaking changes found.
No new resources/functions.

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.

pulumi up stuck on "Waiting for app ReplicaSet be marked available"
3 participants