Linkerd’s weighted backends let you send a percentage of traffic to different versions of your service. This is the bedrock of A/B testing and gradual rollouts, allowing you to experiment with new features or configurations with minimal risk.
Let’s see this in action. Imagine we have a webapp service that needs to talk to an api service. We want to send 90% of traffic to api-v1 and 10% to api-v2.
Here’s the Service definition for api:
apiVersion: v1
kind: Service
metadata:
name: api
labels:
app: api
spec:
ports:
- port: 80
name: http
selector:
app: api
Now, we have two deployments for our api service: api-v1 and api-v2.
api-v1 deployment:
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-v1
spec:
replicas: 2
selector:
matchLabels:
app: api
version: v1
template:
metadata:
labels:
app: api
version: v1
spec:
containers:
- name: api
image: <your-api-v1-image>
ports:
- containerPort: 8080
api-v2 deployment:
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-v2
spec:
replicas: 1
selector:
matchLabels:
app: api
version: v2
template:
metadata:
labels:
app: api
version: v2
spec:
containers:
- name: api
image: <your-api-v2-image>
ports:
- containerPort: 8080
Notice how api-v1 has version: v1 and api-v2 has version: v2. These labels are crucial.
To split traffic, we create two Service resources, each pointing to a specific version of the api deployment.
api-v1-service.yaml:
apiVersion: v1
kind: Service
metadata:
name: api-v1
labels:
app: api-v1 # Linkerd uses this to group backends for the main 'api' service
spec:
ports:
- port: 80
name: http
selector:
app: api
version: v1 # Selects pods with this label
api-v2-service.yaml:
apiVersion: v1
kind: Service
metadata:
name: api-v2
labels:
app: api-v2 # Linkerd uses this to group backends for the main 'api' service
spec:
ports:
- port: 80
name: http
selector:
app: api
version: v2 # Selects pods with this label
Now, we tell Linkerd how to route traffic to these two services using a ServiceProfile. The ServiceProfile is applied to the main api service.
api-service-profile.yaml:
apiVersion: linkerd.io/v1alpha2
kind: ServiceProfile
metadata:
name: api
namespace: default # Match the namespace of your main 'api' service
spec:
routes:
- name: http-api # A name for this route
condition: # This route matches all traffic to the 'api' service
- method: GET
pathRegex: "/.*"
moveTo: # This is where the magic happens
- weight: 1000 # 1000 is 100% of traffic
serviceName: api-v1.default.svc.cluster.local # The fully qualified name of the v1 service
- weight: 100 # 100 is 10% of traffic
serviceName: api-v2.default.svc.cluster.local # The fully qualified name of the v2 service
In this ServiceProfile, the moveTo section defines the traffic split. Linkerd uses a proportional weight system. Here, api-v1 gets 1000 out of a total weight of 1100 (1000 + 100), which is approximately 90.9%. api-v2 gets 100 out of 1100, which is approximately 9.1%. If you wanted exactly 90% and 10%, you’d use weights like 900 and 100.
When your webapp service makes a request to the api service (e.g., http://api), Linkerd intercepts it. It consults the ServiceProfile for api, sees the moveTo rule, and then forwards the request to either api-v1 or api-v2 based on the defined weights.
The key insight here is that Linkerd’s routing operates at the service level, not directly on deployments. You create distinct Kubernetes Service objects for each backend version, and then a ServiceProfile on the logical service name to manage how traffic is distributed among those distinct backend services. This allows you to dynamically adjust weights without changing your application’s upstream service discovery.
To observe the traffic split, you can use linkerd tap. If you have webapp sending requests to api, you can watch the traffic:
linkerd tap -n <your-namespace> deploy/webapp
You’ll see requests going to api being resolved and then appearing as requests to either api-v1 or api-v2 with the approximate 90/10 split.
The weight field in moveTo is an integer. Linkerd calculates the proportion of traffic for each backend by dividing its weight by the sum of all weights in the moveTo list. This means you can adjust the split dynamically by simply updating the ServiceProfile and applying it. For example, to shift 20% of traffic to api-v2, you could change the weights to api-v1: 800 and api-v2: 200.
When you define multiple routes within a ServiceProfile, Linkerd evaluates them in order. However, for traffic splitting within a single logical service endpoint, the moveTo section of a single route is the mechanism. If you have complex routing needs, such as sending specific paths to different versions, you would define multiple routes, each with its own moveTo or unroutable directive.
The most surprising thing about weighted backends is how little the application itself needs to know. From the perspective of the webapp service, it’s always talking to api. Linkerd handles the entire distribution logic transparently, allowing for seamless A/B testing and phased rollouts without modifying application code or service discovery configurations. The Service objects for api-v1 and api-v2 are essentially just plumbing that Linkerd’s ServiceProfile orchestrates.
After successfully splitting traffic, the next step is often to monitor the performance and error rates of each backend independently.