Description
tags: linkerd
kubernetes
service mesh
multicluster
Multi-cluster Discovery Proposal
LFX Mentorship proposal By Rushikesh Butley & Matei David
Problem :
Linkerd's multicluster extension allows operators to export headless services, which are primarily used for workloads deployed as StatefulSets (traditionally databases).
Working with a StatefulSet in complex cluster topologies (three or more clusters) is unwieldy. It requires constant manual intervention from cluster operators and application owners. For each new (or removed) exported service, the StatefulSet has to be edited, and all of the workloads have to be redeployed.
The above problem makes endpoint discovery for stateful workloads hard to deal with.
Goals & Constraints :
For this proposal below are the goals and some of the constraints, which we have scoped out.
-
Goals :
- Create global service in each linked cluster, and make sure it aggregates all the mirrored services.
- Develop a reference architecture that can be used in subsequent tests and investigations.
- Document what the problem is and how the solution works.
- Manually validate the solution.
-
Constraints :
- Implementation should be a prototype, separate from the Linkerd repo.
- Implementation is done by the start of June.
- Will not be part of multicluster official extension.
- Will not focus on any UX through CLI.
How should the problem be solved?
Design Exploration :
Background :
Currently, in mutlicluster discovery if there are multiple peers, when service gets mirrored from target cluster to source , for each peer mirror service is created. To elaborate, below is the example :
We start with two clusters named source
and target
. Let's assume that there is service name foo
existing in source
and target
. And it might need to discover all other replicas existing in target
/
As we can imagine, there are various applications like which does DNS based service discovery for various purposes including but not limited to Statefulsets for operating in HighAvailability. Ex. Thanos, Nginx Upstreams etc.
Background for this proposal :
While it works fine for cluster with one target, it will be quite cumbersome issue, when there are more than one target cluster. To solve this problem, we want standalone service existing in cluster, which when queried, can return all the Endpoints from it's peered cluster.
Solution :
Global Service :
Possible solution to this problem is, creating global service in each linked cluster. This global service will act as aggregator and will have all the mirrored EndpointSlices. Below is the example to clarify it further.
Once the proposal is implemented below will be the view for source cluster.
With Current Release
Proposed Solution
Summary :
In summary, if a Statefulset workload is deployed in a source cluster, and it needs to discover replicas from other clusters, each cluster will need to be linked against the source. Each individual headless service will be mirrored in the source cluster. This leads to operational problems, where a statefulset needs to be rolled out and manually modified each time a new service is added. Consider the following example where a single target cluster is linked against source:
- name: foo
args:
- join
- foo,foo-target1
Linking another cluster leads to a manual intervention to add the new service:
- name: foo
args:
- join
- foo,foo-target1,foo-target2
Implementation Ideas :
To achieve the above solution using EndpointSlices, we might start building on service mirror component. We will deploy controller in each cluster, alongside current linkerd services. Controller will keep watching all the local services inside cluster.
To break it down into steps :
-
As we discussed above, will create the new Service suffixed by
-global
. -
In source cluster, we can have controller/operator, which keep looking for the new
Service
being added. To filter the services, controller will be filtering on the labelmirror.linkerd.io/mirrored-service: "true"
, this annotation is already place by Service Mirror component, when it mirrors service in source cluster. -
This will help us, executing our logic when new service with respective anotation appears in our cluster.
-
Once our controller receives any event for this filter, controller if there is respective global service exist for mirrored service in our cluster by the suffix
-global
.a) If it doesn't exist first we create
Headless Service
using our controller. And name it by snapping-global
as suffix at the end of mirror service name.b) If it exist, we skip creating Headless Service.
-
To find respective synthetic endpoint for mirror service existing in our cluster, we query our K8s registery for Service's respective
Endpoints
. -
All the retrieved
Endpoints
for mirrored services, will be used to create newEndpointSlice
and it will be owned by the-global
service . We will start with maintaining only oneEndpointSlice
and if required we can split it/scale it later.
Development Plan along with Testing Strategy :
We can deliver above feature in two phases.
-
Phase 1 :
-
In first phase we can focus on writing Controller/or refactoring Service Mirror, which look for new Services being added with label :
mirror.linkerd.io/headless-mirror-svc-name
-
Create
Headless Service
with the name suffixed with-global
from the initial service which is mirrored. -
Testing Strategy : To test this phase, we can create Multicluster topolgy of 3-4 cluster. And make sure that only One Global Service is being created per cluster, respective to each type of unique Service being mirrored.
-
-
Phase 2 :
- Once we have
Headless Service
ready from the Phase 1, in this phase we create only oneEndpointSlice
(Maybe we can scale it later if required). - And we add respective synthetic endpoints from mirrored service in this
EndpointSlice
. - We also check for any changes, to the respective endpoints and add/remove them accordingly.
- Testing Strategy : To test this phase, we can query the
Headless Service
created inPhase 1
and make sure that all theEndpointsSlices
being returned match against thesynthetic endpoints
create byService Mirror
.
- Once we have
How we will run it ? :
We will build docker images of our controller, and deploy these images in our multicluster environment using helm charts. It will monitor all the local Services inside respective cluster. It will take required action when encountered label of interest.
For testing, we will be simply running controller against local kubernetes cluster with default config.
Open Questions :
-
Even after creating this Service Discovery mechanism, we need to make sure that consuming service has capablity to handle multiple ips return by the Service endpoint.
-
From my intial assumptions, I dont think there will be any change in proxy. As the request will still be handled in the same way, currently being handled.
-
Is it best bet to rely on Service Mirror, for the base implementation ? And write this as seprate controller. Or is it better to refactor the Service Mirror code. From my thoughts we should go with former one.
Language of Choice
- I would like to propose writing this Controller in
Rust
instead ofGoLang
. As I think with Go lang it will be alot more boilerplate code. - With rust it will be quite fast and efficient, to write new Kubernetes Controller as there will be lot less code.
- Because of strict type system, interacting with types will be quite explicit and error handling will be lot more easy.
- As it's Rust, we will get out of the box performance, along with very less cpu and memory footprint.
- Also I am quite keen about learning Rust, this will be very good opportunity for me to write some hands on code. And learn from the best :).
Would you like to work on this feature?
yes
We have started the initial development here : https://github.com/rushi47/service-mirror-prototype