Linkerd service profiles let you define routes for your services, which unlocks a ton of observability and control.
Let’s see it in action. Imagine you have a product-svc that serves two distinct types of requests: one for fetching product details (a GET request to /products/{id}) and another for searching products (a GET request to /products/search). Without a service profile, Linkerd sees all traffic to product-svc as a single stream.
Here’s a simplified product-svc deployment:
apiVersion: apps/v1
kind: Deployment
metadata:
name: product-svc
spec:
replicas: 1
selector:
matchLabels:
app: product-svc
template:
metadata:
labels:
app: product-svc
spec:
containers:
- name: product-svc
image: your-product-svc-image:latest
ports:
- containerPort: 8080
Now, let’s define a ServiceProfile for product-svc:
apiVersion: linkerd.io/v1alpha2
kind: ServiceProfile
metadata:
name: product-svc.emojivoto.svc.cluster.local
namespace: emojivoto
spec:
routes:
- name: get-product
condition:
method: GET
path: /products/*
- name: search-products
condition:
method: GET
path: /products/search
Once this ServiceProfile is applied, and Linkerd’s control plane is running in the linkerd namespace, and your product-svc is in the emojivoto namespace, you’ll start seeing traffic broken down in the Linkerd dashboard.
In the Linkerd dashboard (accessible via linkerd dashboard), navigate to the emojivoto namespace and select product-svc. You’ll now see metrics for get-product and search-products as separate entities, distinct from the overall product-svc metrics. This allows you to see things like:
- Request Latency: Is
get-productslower thansearch-products? - Success Rates: Is
get-productexperiencing more 5xx errors thansearch-products? - Request Volume: How many requests are hitting each route?
This granular view is incredibly powerful for identifying performance bottlenecks and debugging issues specific to certain functionalities within your service.
The mental model here is that Linkerd, by default, only knows about the "destination" service (product-svc) and the "source" of the traffic. It doesn’t inherently understand the internal structure of the requests being made to that service. A ServiceProfile acts as a set of instructions for Linkerd, telling it how to inspect incoming requests and categorize them based on defined criteria like HTTP method and path. These categories are what Linkerd calls "routes."
When a request arrives at product-svc, Linkerd’s proxy (running as a sidecar) intercepts it. It then consults the ServiceProfile for product-svc. If the request’s method is GET and its path matches /products/* (but isn’t /products/search, due to the order or specificity of route definitions), it’s classified as a get-product request. If the method is GET and the path is exactly /products/search, it’s classified as search-products. If neither matches, it might fall into a default "unroutable" or "other" category, depending on your profile configuration.
The magic isn’t just in the dashboard; this routing information also enables advanced features like fine-grained retries and mirroring. You can configure Linkerd to retry only get-product requests that fail with a 503, for example, without affecting search-products.
Here’s the one thing most people don’t know: the order and specificity of your routes in the ServiceProfile matter significantly. A broader path like /products/* will match any GET request to /products/ followed by anything. If you have a more specific route like /products/search defined after /products/*, the broader rule might "consume" the traffic before the specific one gets a chance to match. Always define your most specific routes first.
With this foundation in place, your next step is likely exploring how to leverage these defined routes for more sophisticated traffic management like canary releases or A/B testing.