%20(1).png)
In part 1 I provided some context about Automated Canary Analysis (ACA). I defined the problem as "how can I tell if my canary is performing as good as my existing (baseline) system, so I can proceed with a full deployment? and can I do it in an automated way?" Without further ado let's dive into the story of the first version of Gloster, the tool that organically grew out of my effort to answer this question.
At the time (early 2017), there was no open source ACA software available, but almost everybody that was seriously engaged in deploying microservices at scale claimed to use canary deployments and some form of canary analysis. The Upwork Platform team was already providing mature deployment orchestration tools that supported automated Blue/Green deployment, including the option to do progressive traffic increase from 0% to 100% for the Green stack while testing a small set of golden metrics against preset thresholds. But this was a poor replacement for a full blown ACA system, so I started working on the idea of introducing ACA into our toolset. While researching what the masters were doing at the time, I was really inspired by the following:
Being inspired by these and a few other publications, I focused on the following core questions that needed an answer before attempting to design and implement an ACA tool:
There were of course engineering complexities and integration considerations for introducing an ACA toolset in our release pipeline and deployment workflow, but the hard part was answering the three questions above. As always, finding the right algorithm(s) is the hard part and you always have to start small and improve incrementally if you want to efficiently plot your way through the multi-dimensional complexity.
Starting small meant avoiding reinventing the wheel, so finding an answer to question #1 was based on the Netflix posts and presentations mentioned above. Calculating a canary analysis score and comparing it to a threshold value for determining success or failure is a typical approach in anomaly detection and this was a good starting point. I decided to start experimenting with a simple deterministic approach for the scoring algorithm that calculates a score for each metric and then combines the scores of all metrics into the aggregate as the presentation suggested. The initial approach required the definition of an upper and lower deviation threshold (defined as percentages) for each metric used in the analysis. So, if the metric for the new release under test is within the expected deviation, this metric gets a score of 1 (perfect success), else it gets a score of 0 (perfect failure); in this early Gloster version no score existed in between (partial match). The overall canary analysis score is calculated as the average of all the scores of the individual metrics participating in the analysis, yielding a score between 0..1 (or 0% and 100%). Easy to implement and with very low computational complexity. In an effort to provide more insight on which high level the analysis failed, the approach adopted the definition of metrics groups as the Netflix literature suggested; metrics were grouped under System, Server, and Requests (we later introduced Dependencies too), so that the service owner can easily understand the type of deviation observed (e.g. system level metrics were off in the new release).
Question #2 screamed for some automation based on training a model or pinpointing statistical characteristics that one can capture through statistical analysis. However, at the time, while tackling multiple dimensions of the problem, the decision was to start simple and require human intervention: expect the service owner to define the set of metrics that matter (i.e. which metrics describe the behavior we care to benchmark) as well as how much they matter (i.e. assign a weight) in a static way. I secretly hoped that this would be enough, since it was implied in the literature that this is the way the experienced Canary Analysis masters do it :) Second question answered! at least for a first version.
The answer to question #3 was not straightforward and could not be simplified. The problem is that we need to compare the behavior (modeled by our telemetry) of the new release deployed as a canary against the existing service version. The typical canary deployment configuration looks like that:

Parameter C starts with a value of 1, and gradually increases to a much higher value, but it is almost never 100 (usually never exceeds 50%). Before increasing C to the next step, we need to make sure that the new release (deployed on the canary) does not exhibit any anomalous behavior and it is safe to proceed to a higher percentage of production traffic. This gradual, calculated traffic increase aims to minimize the risk of impacting the service users, and we only increase the exposure to the new release when we achieve higher confidence that for the given percentage the behavior is within the expected norms. However, this approach complicates the canary analysis since:
After heated brainstorming and some experiments, we decided to adopt the following modified canary deployment pattern:

Instead of provisioning a canary instance (or cluster) that receives a low and increasing percentage of production traffic, we provision a canary stack (or cluster of stacks) with two instances: (a) the baseline, running a service with the existing production version, and (b) the release candidate, running a service with the new version we intend to test. We follow the same strategy on how we divert a small and progressively increasing percentage of production traffic to the canary stack (as we did with the canary instance), and internally this traffic is divided equally between the baseline and the release candidate. We compare the release candidate to the baseline inside the canary stack and not to the pre-existing production instances. The benefit of this approach is that both instances have the same age (are provisioned at the same time and will reach steady state at about the same time) and most importantly, they are receiving statistically equivalent traffic; thus there is no need to introduce any kind of normalization. There is a caveat though, this approach is problematic for services that receive very low production traffic since splitting a small percentage of low traffic in half results in very low request rates and we eventually lose the "statistically equivalent" quality. Solving this problem with traffic mirroring is a topic for another post :)
Having an initial, simplified answer to all three questions, engineering a first version of Gloster was almost straight forward. The high level design included four components:
This was a first approach that evolved, as the algorithm evolved and as different problems emerged. The design assumed integration with our CICD orchestrator (Jenkins) and our home-grown deployment workflow orchestration tool. I will provide more detail in a separate post.
It is interesting to note that based on the initial decision to approach the selection of relevant metrics in a manual fashion, Gloster v1.0 expected a user defined configuration in the following form (snippet from an actual example):
--CODE language-js language-markup line-numbers--
[
{
"groupName": "system",
"weight": 1.0,
"metrics": [
{
"metricName": "summary.cpu.util",
"weight": 1.0,
"sensorType": "Atlas",
"duration": "5m",
"stackAggregator": "avg",
"statAggregator": "avg",
"highDeviation": 0.35,
"lowDeviation": 1
},
{
"metricName": "mem.util.used",
"weight": 1.0,
"sensorType": "Atlas",
"duration": "5m",
"stackAggregator": "max",
"statAggregator": "max",
"highDeviation": 0.35,
"lowDeviation": 1
},
{
"metricName": "kernel.all.load._5_minute",
"weight": 1.0,
"sensorType": "Atlas",
"duration": "5m",
"stackAggregator": "avg",
"statAggregator": "avg",
"highDeviation": 0.35,
"lowDeviation": 1
}
]
},
{
"groupName": "server",
"weight": 1.0,
"metrics": [
{
"metricName": "jvm.memory.heap.used.value",
"serviceName": "ServiceA",
"weight": 5.0,
"sensorType": "Atlas",
"duration": "5m",
"stackAggregator": "sum",
"statAggregator": "avg",
"highDeviation": 0.350,
"lowDeviation": 0.350
} ,
{
"metricName": "http.server.requestDuration",
"serviceName": "ServiceA",
"weight": 1.0,
"sensorType": "Atlas",
"duration": "5m",
"stackAggregator": "sum",
"statAggregator": "avg",
"highDeviation": 0.350,
"lowDeviation": 0.350
}
]
},
...
You will notice the grouping into the System, Service, etc groups, as well as the high and low deviation thresholds. What is important to emphasize here, is that we relied on the service owner expertise to identify which metrics (out of the ~1,000 metrics) are relevant and should be included in this configuration, how important they are (define a weight for the calculation of the total score as a weighted average), and what are the acceptable baseline deviation values. Note: Your gut feeling is correct, this requires some serious effort and potentially a long series of trial and error experiments to fine tune the weights and the deviation tolerance parameters.
In any case, the purpose of this first version was to prove that the approach made sense and that the algorithm can actually detect anomalies, without generating a high number of false negatives.
The following screenshot shows the UI of Gloster v1.0 when it was released to actual users for validation/evaluation.

The (volunteering) users were shown a very engaging demo running ACA for a test service that was emulating on demand anomalies (via a REST API) that showed how Gloster detects anomalies without reporting false negatives when the test service is not configured to inject errors and latency. Creating the Gloster configuration for the test service was a long and cumbersome process, but it allowed the creation of a template with some basic metrics common to all of our microservices. The users were then handed instructions and the default template, and they were asked to configure Gloster for their services and perform tests in order to assess the ACA success. This involved A|A tests (compare two instances with the same version in production) or A|B tests (compare the new deployment to the previous one by keeping the Blue and Green stacks at 50% for an hour during B/G).
It become very clear that asking an engineer to put any kind of effort worth more than 15 minutes of configuration in order to test drive any kind of tool, however exciting, will just not happen. I know I wouldn't do what I was asking our engineers to do given their workload, but I was too passionate with the experiment to see it.
But this experiment, was not a waste. One engineer actually put the effort and customized his service-specific configuration by hand and ran a ton of experiments. Gloster was using the telemetry from actual production services and the results were rewarding. The scoring algorithm was able to reason correctly about hiccups and errors and it did not yield any significant percentage of false negatives. However, the manual configuration approach was brittle. Investing enough time into fine tuning the configuration provided a solid anomaly detection system, but any significant change in the internals of the service could require fine-tuning the configuration again. It was obvious that this human-based configuration was not viable.
In addition to this realization, there were some other interesting lessons learnt related to the selection of the relevant metrics:
After evaluating the first version of Gloster through countless experiments and by reviewing the user feedback, the initial problem definition was augmented with the following additional goals:
My next post is going to be about the construction of the algorithm that attempts to address this refined problem definition. It was an interesting journey with many dead ends and countless experiments that forced Gloster to evolve into a truly automated canary analysis system.