Skip to content

Add support for stateful sets#3113

Merged
adleong merged 5 commits intomasterfrom
alex/stateful-set
Jul 24, 2019
Merged

Add support for stateful sets#3113
adleong merged 5 commits intomasterfrom
alex/stateful-set

Conversation

@adleong
Copy link
Member

@adleong adleong commented Jul 20, 2019

We add support for looking up individual pods in a stateful set with the destination service. This allows Linkerd to correctly proxy requests which address individual pods. The authority structure for such a request is <pod-name>.<service>.<namespace>.svc.cluster.local:<port>.

Fixes #2266

Signed-off-by: Alex Leong [email protected]

@l5d-bot
Copy link
Collaborator

l5d-bot commented Jul 20, 2019

siggy added a commit to BuoyantIO/emojivoto that referenced this pull request Jul 22, 2019
Copy link
Member

@siggy siggy left a comment

Choose a reason for hiding this comment

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

Confirmed working with:
BuoyantIO/emojivoto#68

From linkerd-prometheus pod:

wget -O - web-0.web-svc.emojivoto.svc.cluster.local:80

nit: rather than hostname, can we name this podName, or podHostname? when i see hostname in the code it's not obvious to me that's it's referencing a specific pod. (i realize that the k8s core field is named EndpointAddress.Hostname).

suffix := []string{"svc", "cluster", "local"}
for i, subdomain := range domains[2:] {
n := len(domains)
if n < 5 {
Copy link
Member

Choose a reason for hiding this comment

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

should this also check for n > 6 ? that way we could ditch the the last return statement.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, but I think we'd need to make the n == 6 case into the else block. I think the current way is clearer.

podIP: 172.17.0.20`,
},
authority: "name1-3.name1.ns.svc.cluster.local:5959",
expectedAddresses: []string{"172.17.0.20:5959"},
Copy link
Member

Choose a reason for hiding this comment

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

🙏

for _, subset := range endpoints.Subsets {
resolvedPort := pp.resolveTargetPort(subset)
for _, endpoint := range subset.Addresses {
if pp.hostname != "" && pp.hostname != endpoint.Hostname {
Copy link
Member

Choose a reason for hiding this comment

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

why the pp.hostname != "" check? if pp.hostname == "" and endpoint.Hostname == "foo" this check will fail, is that the intent?

Copy link
Member Author

Choose a reason for hiding this comment

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

If the portPublisher's hostname is unset (empty string) that means that no hostname was specified. In other words, this is a regular service lookup and not a stable pod id lookup. Therefore, we do not want to skip any of the endpoints.

Copy link
Contributor

Choose a reason for hiding this comment

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

So does that mean pp.hostname == "" && endpoint.Hostname == "foo" will never happen? And if it does, then there is a bug elsewhere? Or can we just do if pp.hostname != endpoint.Hostname here?

Copy link
Member Author

Choose a reason for hiding this comment

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

pp.hostname corresponds to the subscriber's query whereas endpoint.Hostname corresponds to the hostname of the pod. So, for example, if we did a destination lookup for foo.default.svc.cluster.local where foo is a service which selects a stateful set, then pp.hostname would be empty (because we are querying the entire service, not a particular stable id) but each pod in the service would have a non-empty endpoint.Hostname.

In this case, we want to select all the pods, and therefore do not want to hit the continue.

siggy added a commit to BuoyantIO/emojivoto that referenced this pull request Jul 22, 2019
@adleong
Copy link
Member Author

adleong commented Jul 23, 2019

They're referred to as hostnames in the Kubernetes documentation as well: https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/#stable-network-id

I agree that it's not the best name, but I'd prefer to match the kubernetes terminology.

Copy link
Contributor

@ihcsim ihcsim left a comment

Choose a reason for hiding this comment

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

👍 Can confirm that this works with the example in #2266.

Also, the new metrics label shows up correctly:

# HELP endpoints_exists A gauge which is 1 if the endpoints exists and 0 if it does not.
# TYPE endpoints_exists gauge
endpoints_exists{hostname="",namespace="default",port="80",service="my-external-nginx"} 0
endpoints_exists{hostname="",namespace="linkerd",port="9090",service="linkerd-prometheus"} 1
endpoints_exists{hostname="nginx-0",namespace="default",port="80",service="nginx"} 1
endpoints_exists{hostname="nginx-1",namespace="default",port="80",service="nginx"} 1
endpoints_exists{hostname="nginx-2",namespace="default",port="80",service="nginx"} 1
# HELP endpoints_pods A gauge for the current number of pods in a endpoints.
# TYPE endpoints_pods gauge
endpoints_pods{hostname="",namespace="default",port="80",service="my-external-nginx"} 0
endpoints_pods{hostname="",namespace="linkerd",port="9090",service="linkerd-prometheus"} 1
endpoints_pods{hostname="nginx-0",namespace="default",port="80",service="nginx"} 1
endpoints_pods{hostname="nginx-1",namespace="default",port="80",service="nginx"} 1
endpoints_pods{hostname="nginx-2",namespace="default",port="80",service="nginx"} 1
# HELP endpoints_subscribers A gauge for the current number of subscribers to a endpoints.
# TYPE endpoints_subscribers gauge
endpoints_subscribers{hostname="",namespace="default",port="80",service="my-external-nginx"} 2
endpoints_subscribers{hostname="",namespace="linkerd",port="9090",service="linkerd-prometheus"} 1
endpoints_subscribers{hostname="nginx-0",namespace="default",port="80",service="nginx"} 2
endpoints_subscribers{hostname="nginx-1",namespace="default",port="80",service="nginx"} 2
endpoints_subscribers{hostname="nginx-2",namespace="default",port="80",service="nginx"} 1
# HELP endpoints_updates A counter for number of updates to a endpoints.
# TYPE endpoints_updates counter
endpoints_updates{hostname="",namespace="default",port="80",service="my-external-nginx"} 0
endpoints_updates{hostname="",namespace="linkerd",port="9090",service="linkerd-prometheus"} 2
endpoints_updates{hostname="nginx-0",namespace="default",port="80",service="nginx"} 2
endpoints_updates{hostname="nginx-1",namespace="default",port="80",service="nginx"} 2
endpoints_updates{hostname="nginx-2",namespace="default",port="80",service="nginx"} 1

Copy link
Member

@siggy siggy left a comment

Choose a reason for hiding this comment

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

lgtm modulo ivan's comment and rebase. 👍 🚢

@adleong
Copy link
Member Author

adleong commented Jul 23, 2019

This conflicted non-trivially with #2950. Since authority parsing is now more complex and depends on the cluster domain, I have pulled that logic up out of the watchers and into the destination server. This required changing the watcher interfaces to accept service and profile IDs rather than raw authorities.

@arminbuerkle: would you like to review this?

@l5d-bot
Copy link
Collaborator

l5d-bot commented Jul 23, 2019

Copy link
Contributor

@arminbuerkle arminbuerkle left a comment

Choose a reason for hiding this comment

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

Thanks @adleong for mentioning me.

I looked through the PR and only found a few minor nit-picks.

The implementations itself looks solid though 👍

log.Fatalf("Failed to listen on %s: %s", *addr, err)
}

global, err := config.Global(consts.MountPathGlobalConfig)
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd probably move this into the destination package into the NewServer function.

We don't need to and don't want to set a different cluster domain than what's set in the config so calling it here give us no benefit.

By moving it you reduce the amount of parameters required for NewServer.


Since config.Global is needed for the trust domain too, i'd also move that into the destination package and pass *disableIdentity instead of trustDomain to NewServer

Copy link
Member Author

Choose a reason for hiding this comment

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

I think I prefer to keep the config reading in the main and pass the specific values into NewServer. This reduces coupling between the server implementation and the config, and makes the server easier to test.

// the HTTP default (80) is returned as the port number. If the authority
// is a pod DNS name then the pod hostname is also returned as the 3rd return
// value. See https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/.
func GetServiceAndPort(authority string, clusterDomain string) (ServiceID, Port, string, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

GetServiceAndPort now returns a ServiceID, Port, string and error.


It looks inconsistent that the pod hostname does not have a type while for example port does


In addition returning a hostname from a GetServiceAndPort function seems confusing. IMO it's time to rename the function. Maybe ParseAuthority? Similar to go's url.Parse?

Copy link
Member Author

Choose a reason for hiding this comment

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

Great suggestion, thanks.

// Needs to have `".svc."` to be a valid k8s svc
if domains[2] != "svc" {
return ServiceID{}, 0, fmt.Errorf("Invalid k8s service %s", host)
for i, subdomain := range domains[n-len(suffix):] {
Copy link
Contributor

Choose a reason for hiding this comment

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

If i'm not missing anything you could rewrite this loop into:

// svc.{clusterDomain} != 'host' suffix
if !strings.HasSuffix(host, "svc."+clusterDomain) {
	return ServiceID{}, 0, "", fmt.Errorf("Invalid k8s service %s", host)
}

which is easier to understand.

Namespace: domains[1],
}
return service, port, "", nil
} else if n == 3+len(suffix) {
Copy link
Contributor

Choose a reason for hiding this comment

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

There's no else if needed since you're returning in the previous if statement.

If you only use an if you can save on indentations and make it easier to understand.

@l5d-bot
Copy link
Collaborator

l5d-bot commented Jul 24, 2019

@adleong adleong merged commit e538a05 into master Jul 24, 2019
@adleong adleong deleted the alex/stateful-set branch July 24, 2019 21:09
adleong pushed a commit that referenced this pull request Aug 7, 2019
Continue of #2950.

I decided to check for the `clusterDomain` in the config map in web server main for the same reasons as as pointed out here #3113 (comment)

It decouples the server implementations from the config.

Signed-off-by: Armin Buerkle <[email protected]>
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.

Can't Reach StatefulSet Pods Via Stable Network ID

5 participants