Virtualization · Orchestration
Canary Releases and the Statistics of Deciding to Promote
How traffic is split between two versions without a shared load balancer, why the promotion decision needs a sample-size calculation, and what a canary cannot detect at any sample size.
A rolling update replaces instances monotonically: once an old pod is removed, it does not come back. That is the right shape when the only question is whether the new version can start and serve, and the wrong shape when the question is whether it serves correctly under real traffic. Answering the second question requires running both versions at once, sending a controlled fraction of requests to the new one, and comparing outcomes before committing. The mechanism is a canary, and its hard part is not the traffic split. It is the decision.
Two ways to split traffic, with different failure modes
The split can be made at the endpoint layer or at the proxy layer, and the choice determines what the two versions share.
At the endpoint layer, a service selects pods by label, and two deployments carry different labels. A single service selecting both labels distributes connections across the union, which produces a split proportional to the number of ready pods on each side. A canary at 10 percent is then ten new pods against ninety old ones, and the split ratio is a consequence of replica counts rather than a configured weight. This is simple, requires no extra component, and is coarse: the granularity is one pod, and the actual request split is not exactly the replica ratio because connection reuse and long-lived connections skew it. A gRPC client that opens one connection and keeps it for the process lifetime sends all of its requests to whichever pod it connected to, so a ten-pod split of a service with a hundred long-lived clients can produce a request distribution far from 10 percent.
At the proxy layer, the split is a routing rule with an explicit weight, evaluated per request. The rules live in the ingress controller or in a service mesh’s sidecar configuration, and the weight is a number the rollout controller sets. Per-request evaluation removes the connection-reuse skew, at the cost of a component that must be reconfigured on every step and of a second place where routing can be wrong. A proxy that evaluates weights per connection rather than per request reintroduces exactly the skew the endpoint approach had, and the difference is invisible in the configuration.
The two approaches also differ in what the two versions share. With a shared service, both versions appear behind the same virtual address and the same DNS name, so any client that caches a resolved address can reach either. With a separate canary service, the split is made by a routing rule that names both services, which means the rule’s correctness is part of the deployment’s correctness.
Why the split ratio is not the analysis unit
The purpose of the canary is to compare a metric between two populations of requests. The natural metric is the error rate, and the natural comparison is “is the canary’s error rate higher than the baseline’s”. Both halves of that sentence hide a decision that cannot be made from a ratio.
The first problem is that the two populations are not identical. A weighted split is per request, but requests are not homogeneous: a canary that receives 10 percent of traffic receives 10 percent of each kind of request only in expectation. Over a small window the mix differs, and if the error rate depends on the request type (a slow query, a large payload, a particular endpoint), the comparison is confounded by composition rather than by version. The standard mitigation is to compare per-endpoint or per-tenant rather than in aggregate, which costs sample size per comparison and multiplies the number of comparisons, and each additional comparison is another chance to see a difference that is not there.
The second problem is sample size, and it is arithmetic rather than opinion. Distinguishing an error
rate of 1 percent from one of 2 percent at conventional confidence requires on the order of two thousand
requests per arm. The figure comes from the standard error of a difference of two proportions: with
p near 0.015 and a difference of 0.01, the variance of each estimate is about p(1-p)/n, so the
difference’s standard error is about sqrt(2 * 0.0148 / n), and resolving a difference twice the
standard error needs n in the low thousands. Halving the difference to be detected quadruples the
required sample, because the sample size scales with the inverse square of the effect.
Consequences follow directly. A canary that runs for five minutes at 1 percent of a service handling a hundred requests per second accumulates three hundred requests, which is far below the threshold for detecting a doubling of a small error rate. The canary will report no difference whether or not one exists, and promoting on that basis is not a decision. It is a coin flip with extra steps. The duration of a canary step must be derived from the traffic rate and the smallest effect worth detecting, not chosen as a round number of minutes.
Tail latency makes this worse by an order of magnitude or more. The 99th percentile is estimated from the top 1 percent of samples, so a sample of three hundred requests contains three observations in the region of interest. Comparing two tail estimates from such samples produces differences of arbitrary sign. Latency canaries need sample sizes in the tens of thousands per arm before the comparison means anything, which for most services means a canary duration measured in hours, or a decision made on a lower percentile where the sample is denser.
Automated analysis and the two errors it can make
An automated canary analysis compares a metric over the canary population against the same metric over the baseline, over the same window, and decides pass or fail. The decision rule has two error modes, and both are expensive in different ways.
A false failure aborts a good release. The cost is a delayed deploy, a rollback that itself churns traffic, and, more damaging over time, a loss of confidence that leads operators to widen thresholds until the analysis passes everything. A false pass promotes a bad release, and the cost is whatever the bad release does at full traffic.
The asymmetry between these costs is what determines how the rule should be set, and it is not the same as the asymmetry in classical hypothesis testing. A canary analysis is usually tuned to fail often rather than to pass wrongly, because a false failure costs a retry and a false pass costs an incident. The consequence is that a canary that is run on every deploy and fails 5 percent of the time for statistical reasons will be overridden by operators within a few weeks, and the analysis becomes decoration.
The mechanisms that make the rule reliable are unglamorous. Comparing against a baseline measured over the same window, rather than against a historical value, removes the effect of time-of-day and of any other change that landed at the same time. Requiring the metric to be worse by a margin, rather than merely different, avoids failing on improvements and on noise. And using a sequential test, one that can stop early when the evidence is overwhelming while preserving the error rate, avoids the situation where a step must run to completion even though the canary is obviously broken. A fixed-sample test that is repeatedly peeked at inflates its false-positive rate; the sequential formulation is what makes early stopping legitimate.
The mechanisms that make a canary worth running
A canary compares versions, so it can only detect differences that are caused by the version and visible in the metric. That rules out whole classes of defect, and knowing which ones is what determines whether the canary is the right tool.
It cannot detect a defect that appears only at full traffic. A new version that exhausts a connection pool sized for the old version’s concurrency may behave perfectly at 10 percent and fail at 100. The canary’s own scale is its blind spot: the failure is a function of load, and the canary deliberately does not produce the load.
It cannot detect a defect that appears only for a subset of requests the canary did not receive. A 10 percent split over an hour samples 10 percent of the traffic mix, so a defect triggered by a rare request shape may not appear at all. Widening the observation window helps in proportion to the rarity, which is another way of saying that rare-input defects need full-traffic exposure.
It cannot detect a defect in a state transition that the canary does not perform. A schema migration that runs once, a cache that must be warmed from cold, a leader election that happens on restart. A canary pod that starts into an already-warm cluster does not exercise any of these. The class of defects that only appear on a cold start is systematically invisible to a canary running alongside healthy instances.
What the canary does detect well is a version-correlated change in outcome distribution: a higher error rate for the same request mix, a different latency distribution at the same load per pod, a change in the rate of a specific error code. Those are precisely the defects that a rolling update cannot catch, because they require comparing two versions serving the same traffic at the same time. The two mechanisms are complementary rather than alternatives, and the sensible composition is a canary for the first increment of traffic followed by the ordinary rolling update for the remainder. The canary answers the version question, and the rollout handles the capacity question.
Abort, rollback, and the state that outlives both
Aborting a canary means setting the weight to zero, which is instantaneous at the proxy layer and requires no pod churn. That is its main advantage over rolling back a completed rollout: there is nothing to reverse, because nothing was replaced. The old version never stopped running.
Rolling back after promotion is a different operation and a slower one. The old version’s pods are gone; restoring them is a new rollout with the ordinary bounds, and it takes as long as a rollout takes. The distinction matters when the defect is discovered minutes after promotion: the abort path that was available during the canary is no longer available, and the recovery time is the rollout time.
The state that outlives both is the data. A canary that writes to a shared store has already written with the new version’s schema, and aborting the traffic split does not undo those writes. This is why the compatibility requirement on schema changes is stronger than the compatibility requirement on the code: the code can be reverted in minutes, the data cannot. The usual discipline is to make changes in expand-and-contract order: add the new field, write both, read the old, then migrate, then remove. That way at every point in the sequence both versions can run against the same store. Without that discipline, a canary is only safe for stateless changes, and the deployment mechanism’s guarantees are weaker than they appear.
The pieces a canary manipulates, namely replica sets, revisions, and the ownership relations between them, are the same objects a rollout manipulates, and their lifecycle is the subject of controller ownership and the garbage collector. The metrics that a canary compares come from a different subsystem with its own sampling delay, which is why a promotion decision made too quickly after a step change compares a window that does not yet contain the change. It is the same lag that makes autoscaling feedback loops and stability a control problem rather than a threshold problem.
References
- Kubernetes Documentation: Service — Kubernetes project
- The Datacenter as a Computer: Designing Warehouse-Scale Machines, 3rd edition — Barroso, Clidaras, and Hölzle, Springer, 2018
- Kubernetes Documentation: Ingress — Kubernetes project