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

Add IDestinationResolver for resolving cluster destination addresses #2210

Merged
merged 19 commits into from
Aug 17, 2023

Conversation

ReubenBond
Copy link
Member

@ReubenBond ReubenBond commented Aug 1, 2023

This PR adds IDestinationResolver, an abstraction for expanding the ClusterConfig.Destinations property, resolving the destination addresses into one or more addresses. The intention is to better support service discovery in YARP.

/// <summary>
/// Resolves destination addresses.
/// </summary>
public interface IDestinationResolver
{
    /// <summary>
    /// Resolves the provided destinations and returns resolved destinations.
    /// </summary>
    /// <param name="destinations">The destinations to resolve.</param>
    /// <param name="cancellationToken">The cancellation token.</param>
    /// <returns>
    /// The resolved destinations and a change token used to indicate when resolution should be performed again.
    /// </returns>
    ValueTask<ResolvedDestinationCollection> ResolveDestinationsAsync(
        IReadOnlyDictionary<string, DestinationConfig> destinations,
        CancellationToken cancellationToken);
}

The PR also currently adds a Host property to the DestinationConfig, which is indented to act as the HTTP host name (for virtual host & SNI support).

@ReubenBond
Copy link
Member Author

Is someone able to advise me on the best way to implement the abovementioned Host feature?

@ReubenBond
Copy link
Member Author

I spoke with @Tratcher about how to flow the Host property through, and have implemented that in the current version of the PR.

@ReubenBond ReubenBond marked this pull request as ready for review August 7, 2023 18:08
@ReubenBond
Copy link
Member Author

Alright, I consider this ready for review.

Co-authored-by: Arvin Kahbazi <akahbazi@gmail.com>
@ReubenBond
Copy link
Member Author

@Tratcher @MihaZupan PTAL

Copy link
Member

@MihaZupan MihaZupan left a comment

Choose a reason for hiding this comment

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

A few minor suggestions, otherwise this LGTM, thank you.

Might be worth adding a mention in the docs somewhere that this is an available extension point.

ReubenBond and others added 5 commits August 9, 2023 11:00
@ReubenBond
Copy link
Member Author

Feedback addressed, thank you @MihaZupan

@ReubenBond
Copy link
Member Author

Added a short doc on destination resolvers

@ReubenBond
Copy link
Member Author

I'll add an implementation of IDestinationResolver using System.Net.Dns for completeness' sake

@ReubenBond
Copy link
Member Author

Ok, I've added a DNS IDestinationResolver implementation with a test. It's opt-in and you configure it programmatically via reverseProxy.AddDnsDestinationResolver(...).

@ReubenBond
Copy link
Member Author

ReubenBond commented Aug 14, 2023

DnsDestinationResolver defaults to adding a destination per IP, but that means we could get multiple destinations for the same physical host if DNS returns an IPv4 & IPv6 address for the same host. You can specify an AddressFamily to query in the options to choose to only get one or the other.

Alternatively, we could scan over the query results and choose to prefer only IPv4 addresses or only IPv6 addresses if there is a mix of IPv4 and IPv6 addresses. I'm open to feedback.

@ReubenBond
Copy link
Member Author

BTW note that I placed IDestinationResolver and friends into a new namespace, Yarp.ReverseProxy.ServiceDiscovery.

docs/docfx/articles/destination-resolvers.md Outdated Show resolved Hide resolved
Comment on lines 68 to 69
_logger.LogError(ex, "Error resolving destinations");
throw;
Copy link
Member

Choose a reason for hiding this comment

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

Anti-pattern: Don't log and throw, this causes duplicate logs. The caller will log it right?

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 caller will rethrow on first load, with "Unable to load or apply the proxy configuration."
On reload, it will log "Failed to reload config. Unable to register for change notifications, polling for changes until successful."

i.e, it will be logged the same as an IProxyConfig.GetConfig() exception. Is that fine?

Copy link
Member

Choose a reason for hiding this comment

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

Yes, that's fine. If you want a more specific log, log without the full exception, maybe just the message?

DestinationConfig originalConfig,
CancellationToken cancellationToken)
{
var originalUri = new Uri(originalConfig.Address);
Copy link
Member

Choose a reason for hiding this comment

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

Uri.TryParse? There's no telling what's in the config.

This is running before filters, right?

Copy link
Member Author

Choose a reason for hiding this comment

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

What should it do if the TryParse fails?

Copy link
Member

Choose a reason for hiding this comment

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

Log and return the original destination?

Copy link
Member Author

Choose a reason for hiding this comment

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

Is that valid behavior? Eg, a Warning level log saying Destination {DestinationId} has an unsupported Address. Addresses must be valid URIs.?

Copy link
Member

Choose a reason for hiding this comment

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

We're careful not to over-validate/restrict destination addresses because people can plug in alternative transports like named pipes and use custom address formats. I think we only validate that it's not null or empty. A warning log seems appropriate in this case.

docs/docfx/articles/destination-resolvers.md Show resolved Hide resolved
/// <summary>
/// Creates a <see cref="UriBuilder"/> from the provided address, only setting the Port property if it is not the default value.
/// </summary>
private static UriBuilder CreateUriBuilder(Uri uri)
Copy link
Member

Choose a reason for hiding this comment

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

How is this different from new UriBuilder(uri)?

Copy link
Member Author

Choose a reason for hiding this comment

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

It's the Port. When I just use new UriBuilder(uri), it will include ":80" in the Host, which is ugly (it's probably not a showstopper). It should only include the port if it's a non-default port for the scheme. The doc comment tries to indicate this: "only setting the Port property if it is not the default value."

Copy link
Member

Choose a reason for hiding this comment

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

And there's no way to make it clear the default port later?

Copy link
Member Author

@ReubenBond ReubenBond Aug 16, 2023

Choose a reason for hiding this comment

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

Not from what I could tell, and not from what Stack Overflow seemed to indicate 😁

/// <remarks>
/// Defaults to 5 minutes.
/// </remarks>
public TimeSpan? RefreshPeriod { get; set; } = TimeSpan.FromMinutes(5);
Copy link
Member

Choose a reason for hiding this comment

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

Are you really expecting changes this often?

Ideally we'd use a dns library that could read the TTL data and refresh based on that.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yea, but this is what gRPC ships with too (we should check their impl). Anything else requires external dependencies.

cc @JamesNK

Copy link
Member

@JamesNK JamesNK Aug 16, 2023

Choose a reason for hiding this comment

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

gRPC keeps open connections with resolved endpoints. This allows it to track the health of endpoints in realtime. If one of those connections goes down, such as because a k8s pod instance is restarted, then gRPC immediately asks the resolver to refresh itself. k8s will then return the new set of IP addresses, with the old pod instance removed and the new instance added, and gRPC attempts to connect to the new address.

The gRPC's DnsResolver also has the option to configure an explicit refresh interval. This is mostly useful in situations where k8s scales up. Periodically checking DNS lets the client notice that there are new endpoints available.

Copy link
Member

Choose a reason for hiding this comment

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

Does DnsDestionationResolver need some kind of health check integration to trigger a refresh?

Copy link
Member Author

Choose a reason for hiding this comment

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

We could integrate YARP's health checks here, potentially to reload the IProxyConfig which the faulting destination belongs to. With DNS, we have no notification to tell us to perform a reload, but a Kubernetes-native solution could subscribe to those events by watching the relevant k8s Service resource for changes.

@@ -240,22 +242,31 @@ public void DefaultsCanBeDisabled()
var results = transformBuilder.BuildInternal(route, new ClusterConfig());
Assert.NotNull(results);
Assert.False(results.ShouldCopyRequestHeaders);
Assert.Empty(results.RequestTransforms);
Assert.Single(results.RequestTransforms);
Copy link
Member

Choose a reason for hiding this comment

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

It's unfortunate that this can't be turned off completely like before. I don't want us to end up with default behavior that the user can't override. They can still replace or remove the host themselves, right?

Copy link
Member Author

@ReubenBond ReubenBond Aug 14, 2023

Choose a reason for hiding this comment

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

I agree that this is somewhat unfortunate. The developer can still replace or remove the host. I had a version of this which moved the behavior to the front of the transform pipeline inside StructuredTransformer itself, but after speaking with Miha about it, I moved it to the existing transform at the end of the pipeline instead.

@adityamandaleeka
Copy link
Member

Woohoo, looks like this is green now. Ready to merge?

@ReubenBond ReubenBond merged commit 2fdb425 into microsoft:main Aug 17, 2023
6 checks passed
@ReubenBond ReubenBond deleted the feature/destination-resolver branch August 17, 2023 16:21
@Tratcher Tratcher added this to the YARP 2.x milestone Aug 21, 2023
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.

7 participants