8000 [POC] Destination Service EndpointSlice support by mateiidavid · Pull Request #4563 · linkerd/linkerd2 · GitHub
[go: up one dir, main page]

Skip to content

[POC] Destination Service EndpointSlice support - #4563

Closed
mateiidavid wants to merge 5 commits into
linkerd:masterfrom
mateiidavid:mdavid/destination-endpoint-slice-support
Closed

mateiidavid wants to merge 5 commits into
linkerd:masterfrom
mateiidavid:mdavid/destination-endpoint-slice-support

Conversation

@mateiidavid
Copy link
Copy Markdown
Contributor

Issue: #4501

After a few attempts at adding support for EndpointSlice resources, I came up with this poc to show one way of doing this.

EndpointSlice access


  • The first prerequisite to supporting EndpointSlices is to make sure that the cluster has access to the resource, since it sits behind a feature gate. To do this, we are using a similar check to ServiceProfile. First, we check to see what resources are registered with the api-server under the group-version discovery/v1beta1. Second, we check to see if kind: EndpointSlice is a registered api resource, if it is, we return from the function with no error.

  • When we create a new EndpointWatcher, we check if the cluster has any EndpointSlice. If we get an error, we start the Endpoints shared informer, otherwise, we start an EndpointSlice shared informer.

Difference between EndpointSlice and Endpoints


  • There are a couple of differences between the two resources, namely around what labels, names and addresses they contain.
  • Endpoints contain ALL endpoints of a service. An endpoint contains a subset -- a group of addresses with common ports. The set of all endpoints is the union of all subsets.
subsets:
- addresses:
  - ip: 172.17.0.10
  - ip: 172.17.0.9
  ports:
   - name: http
      port: 8080
   - name: metrics
      port: 9090

# Both IP addresses have the common set of ports {http, metrics}
# So our endpoints in this case are Addresses x Ports in this subset
  • EndpointSlices ARE a subset. Instead of grouping all subsets into a single resource, the slice contains only a set of addresses with common parts.
endpoints:
- addresses:
  - 172.17.0.10
     # extra stuff
     # e.g topology, conditions
- addresses:
  - 172.17.0.9
     # more extra stuff
ports:
   - name: http
      port: 8080
   - name: metrics
      port: 9090

# Here, we have a set of endpoints for the slice, where each endpoint has an address, a targetRef
# topology, etc
# If ports was included in a subset (so each subset has a different port set)
# a slice only has one set of ports corresponding to its endpoint addresses
  • Other notable differences:

    • Slices have a prefix (svc name), the rest of their name is randomly generated
    • Slices have a label with the service name
    • This means 1 service ------ * slices instead of a 1:1 relationship (like with Endpoints)
    • EndpointSlices are dual-stack, an endpoint can have both IPv4 and IPv6 (which is why addresses is a set, and not a field like it is in Endpoints with ip:
  • Because of these differences, EndpointSlices as resources need to be processed a bit differently than Endpoints. Will go more in-depth in the next sections.

Handler functions


  • When implementing new handler functions for EndpointSlice, I tried to reuse as much code as possible. Not everything can be reused because of the type system, where applicable I tried to reuse logic by doing type assertions.

  • Add:

    • Most type assertions have been done here
    • When adding a new EndpointSlice in my mind there was one thing that needs to be addressed and is different from Endpoints. Specifically, only update those ports whose addresses are in the slice.
    • For example, if an Endpoints resource has 2 subsets, one which includes addresses associated with 8080 and one with addresses associated with 6000, all ports would have to be updated when a change to the Endpoints object is made, since E = Subset1 U Subset2. A slice IS a subset, however, so we want to update only the portPublisher that has a targetPort of 8080, if our slice contains addresses associated with 8080. I'm not sure if this would be a case often met in a real production cluster, I've personally never seen an Endpoints with more than 1 subset, but I thought mathematically this should hold (?)
    • To check, I added a function isTargetPortInSlice that checks whether the current portPublisher's targetPort is part of the slice, if it is, update the endpoint.
    • Another key difference is when building AddressSets for a port. Because each address has a conditions field associated with it, we only want to add an address when it is ready (to avoid duplicate ADDs being sent to the proxy).
  • Delete:

    • No type assertions here because it was too complicated to get it working (due to the tombstones), so I just decided to create its own functions.
    • The key difference here is similar to Add -- when we want the delete the addresses of a portPublisher, only delete those addresses which were part of the slice, since other slices can be in existence it does not make sense to delete all endpoints.
    • Again, we make use of isTargetPortInSlice. If the addressSet is empty when we delete the slice addresses, we do what noEndpoints() does -- set the existence to false and tear down the portPublisher (along with its listener).

Misc


I'm thinking that the resource access for EndpointSlices should be done before on the server-side and passed as an argument to NewEndpointsWatcher to make it easier to write tests.

I planned on adding new tests for the actual PR where we can check conditions such as ensuring a slice's addresses have been completely updated, a slice that does not correspond to a portPublisher does not change anything, etc.

When we create a new portPublisher we make another check for each resource to use when we populate it. If we have slices, we will populate the port with it. Not sure what to do when a port is updated, however.

I have tested this with the destination client script and it seems to be working fine. I have tested if EndpointSlices can be deleted, updated, added and that NoEndpoints is returned when all pods in a slice have been deleted (to test if the address set is empty the functionality is similar to noEndpoints).

Let me know if I've missed anything!

Signed-off-by: Matei David <matei.david.35@gmail.com>
Signed-off-by: Matei David <matei.david.35@gmail.com>
Signed-off-by: Matei David <matei.david.35@gmail.com>
…slices

Signed-off-by: Matei David <matei.david.35@gmail.com>
return
}

func endpointSliceAccess(k8sClient kubernetes.Interface) error {
@zaharidichev zaharidichev Jun 10, 2020 •
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

does it actually make sense to retrn an error from here. From what I see you just want to know whether you can use slices or not, right? I think a bool should suffice there WDYT ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I see your point and I agree. Initially, I thought returning an error makes more sense because:

  1. ServiceProfileAccess functions in a similar fashion, so thought it's a good idea for the sake of consistency.
  2. By returning an error there was no need to log/handle the error in the function.

On second thought, handling the error in the function may not be necessary. I changed it to return a boolean. Some of the errors that can happen when calling ServerResourcesForGroupVersion(gv) on the discovery client are NotFound, IsForbidden and another error when the gv is empty. Based on this, I think logging/handling the error is trivial, we can just return false instead.

return strings.Count(address, ":") >= 2
}

func isTargetPortInSlice(targetPort namedPort, slicePorts []dv1beta1.EndpointPort) bool {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

TIOLI: We can save on writing that for loop twice I guess:

func isTargetPortInSlice(targetPort namedPort, slicePorts []dv1beta1.EndpointPort) bool {
	for _, p := range slicePorts {
		switch targetPort.Type {
		case intstr.Int:
			if *p.Port == targetPort.IntVal {
				return true
			}
		case intstr.String:
			if *p.Name == targetPort.StrVal {
				return true
			}
		}
	}
	return false
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

That looks better so went ahead and changed it, thanks 👍


if endpoint.TargetRef.Kind == "Pod" {
for _, addr := range endpoint.Addresses {
if isIPv6Address(addr) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think instead of doing so you can simply check the AddressType on the EndpointsSlice object. A slice will only have addresses of one type in its endpoints unless the documentation is incorrect.

if endpoint.TargetRef == nil {
serviceName := es.Labels[dv1beta1.LabelServiceName]
for _, addr := range endpoint.Addresses {
if isIPv6Address(addr) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Hm, I think this might cause some trouble here and there. So currently an EndpointsSlice has an address type defined such as:

const (
	// AddressTypeIP represents an IP Address.
	// This address type has been deprecated and has been replaced by the IPv4
	// and IPv6 adddress types. New resources with this address type will be
	// considered invalid. This will be fully removed in 1.18.
	// +deprecated
	AddressTypeIP = AddressType("IP")
	// AddressTypeIPv4 represents an IPv4 Address.
	AddressTypeIPv4 = AddressType(v1.IPv4Protocol)
	// AddressTypeIPv6 represents an IPv6 Address.
	AddressTypeIPv6 = AddressType(v1.IPv6Protocol)
	// AddressTypeFQDN represents a FQDN.
	AddressTypeFQDN = AddressType("FQDN")
)

This means that the address can actually be a domain name like google.com. In this case this function will return false:

func isIPv6Address(address string) bool {
	return strings.Count(address, ":") >= 2
}

Not sure what happens next but I think it is not what we want. Nevertheless the fact that there is an address type that can be a concrete netwrok address (IPv4 or IPv6) OR a domain name is a great feature that I think will solve some of the headscratchers we are having here at the moment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yup, this was an error on my part, so apologies! When I was researching EndpointSlices, I came across a slice that had both IPv4 and IPv6 in the address type, but I think I must've made a mistake. It is obvious from the docs that a slice only has one address type:

// addressType specifies the type of address carried by this EndpointSlice.
	// All addresses in this slice must be the same type. This field is
	// immutable after creation. The following address types are currently
	// supported:
	// * IPv4: Represents an IPv4 Address.
	// * IPv6: Represents an IPv6 Address.
	// * FQDN: Represents a Fully Qualified Domain Name.

Based on your comment, I have:

  • Removed the isIPv6Address function
  • Added a check in the EndpointSlice/addEndpoints handler, if the address type is not IPv4 the resource won't be cached. This is until we can work out what should happen when a slice has an IPv6/FQDN address type.

@zaharidichev zaharidichev left a comment •
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@Matei207 This is a great start !!! and overall I think the direction is the right one. I think one thing we need to think about is how do we want to take advantage of some of the extra features that endpoints slices provide. For example, do we want to ignore addresses of tupq FQDN the same way we do for services with ExternalName type?. Left some comments/questions. Also yes, a good set of unit tests once some of the more loose ideas are made concrete is a good thing to have for this kind of critical change

Signed-off-by: Matei David <matei.david.35@gmail.com>
@mateiidavid
Copy link
Copy Markdown
Contributor Author

@zaharidichev thank you for the review and the comments! I have addressed all of them and made changes where applicable.

how do we want to take advantage of some of the extra features that endpoints slices provide

Great point, one of the main things that I want to take away from this poc is what else can be done and built on top of the slices feature.

for example, do we want to ignore addresses of tupq FQDN the same way we do for services with ExternalName type

I have looked at #4582 and #4535 to try and get some ideas on how this could be handled, but I'm a bit stumped. WDYT?

So far, it seems to me at the very least it should be decided what we are going to do when the address type is IPv6/FQDN. In the latest commit of this PR, if the address is not IPv4 the endpointslice is not processed.

  • For FQDN, I can see how this has some benefits for multi-cluster and service mirror but I'm not sure if it's too soon to think about it.
  • For IPv6, I think realistically we should not make any changes or implement support for it for now, since it will add another layer of complexity for a feature that is in beta (same as slices, service topology etc.)

Also, Endpoints are used throughout the controller. I'm curious if there is any way to make slices more accessible to other parts of the controller, in a similar fashion as in endpoints_watcher. I am also thinking that the endpointSliceAccess check can be moved to a different package (e.g in api, or make it a public function.

Comment on lines +651 to +667
id := PodID{
Name: endpoint.TargetRef.Name,
Namespace: endpoint.TargetRef.Namespace,
}
pod, err := pp.k8sAPI.Pod().Lister().Pods(id.Namespace).Get(id.Name)
if err != nil {
pp.log.Errorf("Unable to fetch pod %v: %s", id, err)
continue
}
ownerKind, ownerName := pp.k8sAPI.GetOwnerKindAndName(pod, false)
addresses[id] = Address{
IP: addr,
Port: resolvedPort,
Pod: pod,
OwnerName: ownerName,
OwnerKind: ownerKind,
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This loggic here seems quite similar to the one in endpointsToAddresses. Can we extract it somehow?

// Consumers must handle different types of addresses
// An "addresses" set must contain at least 1 but no more than 100 addresses
if endpoint.TargetRef == nil {
serviceName := es.Labels[dv1beta1.LabelServiceName]
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What happens if this name is empty? Are we guaranteed that there will be a value for this label?

}
}

return false
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think we can move that somewhere else indeed. Not sure it is worth it before we actually need it outside of this file.

}
case *dv1beta1.EndpointSlice:
{
serviceName, ns = res.Labels[dv1beta1.LabelServiceName], res.Namespace
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Again, not sure what we need to do in the case when this is not present. I kind of feel this is an invalid scenario and we should just skip processing the slice. Is this the only way to associate an EndpointSlice with a service? I assume so. If this label is non present.. then I guess this is an endpoint that is an "orphan" we cannot really do anything with it, so why go through the trouble of processing it at all. Correct me if I am wrong

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Labels are not the only way of associating an endpointslice to a service. According to the endpointslice docs, ownership is established by an ownership reference and/or a label.

e.g

  ownerReferences:
  - apiVersion: v1
    blockOwnerDeletion: true
    controller: true
    kind: Service
    name: web-svc
    uid: f8d63059-f3ef-4a6f-9bfd-d9742ddb70de

There is no resource creation validation happening, so theoretically endpoints can be orphaned if they are created manually without specifying either a label or an ownership ref to the service. While it is unlikely to happen, think there should be a safeguard in place.

To cope with this, I will add a check to the addEndpoints function, a slice that:

  • has an addressType != IPv4 (so IPv6 or FQDN)
  • or does not have an ownership ref
  • or does not have a service label

will not be processed.

func endpointSliceAccess(k8sClient kubernetes.Interface) bool {
gv := dv1beta1.SchemeGroupVersion.String()
res, err := k8sClient.Discovery().ServerResourcesForGroupVersion(gv)
if err != nil {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

10BC8

maybe we can log the error here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yup, sounds good. Will change this.

@zaharidichev
zaharidichev commented Jun 11, 2020 •
Copy link
Copy Markdown
Member

@Matei207 As already mentioned I think you are on the right track. Lets abstract ourselves from how thi sall will work with multicluster as there are a few unknowns there at the moment. Instead we can focus on simply suporting the slices as if they were Endpoints and go with that for now. I think this is the right approach to integrate this new feature.

Also at that point I think you can spend a bit of time trying to put together some unit tests that make sure everything works as expected :)

@mateiidavid
Copy link
Copy Markdown
Contributor Author

Thank you for the comments and feedback!

It seems that the general direction has been found and the next steps are a bit clearer. Will add the additional checks substantiated in the comments, add some unit tests and open a new PR with the actual implementation.

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.

3 participants

0