1. var bob = new Person();
2. bob.sayHello();
3. bob.wave();
##################################################################################################################
##################################################################################################################
4. var cup = new Coffee();
5. bob.drink(cup);
6. var cupTwo = new Coffee();
Recently I’ve been doing a lot of thinking about how to serve global customers with a serverless architecture. The applications I’ve been building recently have started getting more users from across the world, and those users have been complaining of poor latency. I’ve come up with a solution to this, which if you want to cut to the chase you can visit here.
What causes users to have poor latency when they visit across the world to an Azure Function based in an individual region? The answer is a combination of simple physics and complex networking. First of all let’s look at the setup I had and what was causing the issue:
So here we’ve got a simple example of an Azure Function which interacts with a database in the UK and returns the results to an end user in Australia. The data travels as light along optical fibres (if we’re lucky) between the UK and Australia. Even if we look at the optimal case - travelling as light in a straight line - it takes approximately 0.1 seconds to do the journey to and from Australia. In reality the time is much, much worse than that.
Real internet connections are not so simple. The internet is a series of interelated nodes that pass traffic onto each other. So the journey from Australia to the UK looks more like this:
At each node the connection can lose a bit of time. Some connections can get terminated because someone digs up a cable, some machines can malfunction etc. There’s also just processing time that it takes for the routing from one node to the next to work. The result is an average roundtrip time of almost 0.3 seconds, as shown here. This doesn’t sound like a lot, but can wreak havoc on your user experience when it’s powering a user interface, especially when packets get lost on the way.
To fix this it’d be ideal if Azure Functions allowed you to deploy globally. However that currently isn’t possible out of the box, so I’ve created an ARM template and demo that lets you effectively roll your own Global Azure Function. My solution relies upon both of those by making use of another service, Azure Front Door. This is a neat service which works in the following way:
Front Door has a bunch of other features like a Web Application Firewall, take a read if you’re interested here.
So if you deployed an Azure Function to every region in the world that supported it, then put an Azure Front Door in front of them, then by calling the Azure Front Door you’d get rerouted to your local Azure Function and effectively have a global Azure Function. Here’s a diagram of what that means:
Each user is generally very close to an Azure Front Door endpoint, called a Point of Presence, denoted with the blue redirect boxes. When they call the Azure Front Door URL they get redirected along the Azure backbone, which much better approximates the straight line optimal condition than the normal internet. It then reaches the closest Azure Function, which deals with it and returns the data to the user.
This looks complicated, and is, but I’ve made an ARM template and deployment script that make it simple here. Let’s go through the details of how the ARM template works first. It’s a standard Azure Functions ARM template, but instead of taking a single location, it takes an array of locations:
Next I make heavy use of the ARM template Copy feature. First of all we need a storage account in each region to store the code for the Azure Function:
Here you can see I’m setting up a copy, which replicates the resource the number of times listed in the ‘count’ property. The copyIndex function will then return the index of whatever resource is currently being created. You can therefore use the length of the array of locations to set the count, to create one resource per location, and the copyIndex function to access the location string for each resource. This creates a copy of each storage account for each region. The server farms are set up in the same way.
Next I set up the Function Apps themselves. There’s two bits of particular note here, the dependsOn and the AzureWebJobsDisableHomepage setting:
The dependsOn setting uses the copyIndex method to make each and every function app depend on its particular local storage account and server farm. This allows ARM to parallelize the deployments as much as possible without ever deploying a function app before its dependencies are ready.
The AzureWebJobsDisableHomepage setting is set to true. This is based on advice in the article here. Basically Front Door pings the home page of the Azure Function App to check which is the closest from each Point of Presence and which is currently functioning. This can then result in a lot of traffic, which can cost a lot. So switching off the home page reduces the data transfers and reduces the cost.
There’s a single Application Insights resource for the entire deployment. This is something to be careful of over time. The cost in global data transfer could become prohibitive if there’s a lot of logs:
Next is the front door set up. This is mostly standard and covered better in the official documentation. The only different bit is the set up of the backend pool to cover every Azure Function. This uses the copy function again:
This is simple to run. Login in to your Azure account in powershell, create a resource group with az group create:
Then execute the ARM template and pass in the name. There is a bit of a flaw due to long region names and the 24 character limit on a storage account names - australiasoutheast is 18 characters. I’ve an idea for improving that in future, using an array of objects with a short name for each location as well as the long name.
So this will now spin up a resource group which spans the entire world:
This is great, but how do you deploy to a global Azure Function? This is going to be a real pain if you have to deploy to each one individually. I’ve built a deploy script in powershell which deploys in parallel to each Azure Function. It’s still not as fast as deploying to one, but a lot faster and easier than deploying to all of them. I’ve created a C# Azure Function with the default HTTP starter. Execute the deploy script to deploy it to each Function App:
So now you have a global function app. You can add whatever http triggers you like and you just change the base domain from {name}.azurewebsites.net to {name}fd.azurefd.net:
Now to test if the theory works. Are different functions triggered when I call from different locations? Given the current worldwide lockdown I don’t think travel is particularly feasible at the moment, so I just use a VPN - I use NordVPN. That lets me route all of my internet access through any region in the world:
All of the logs from across the world are going in to the application insights resource, so we can see if the logs correlate with the requests. I went for a little world tour, and got the following logs:
Lets check out the performance too:
As you can see the performance of the app is generally consistent barring some variance which is to be expected with cold starts etc.
This is an interesting (I think so anyway) demonstration that it is possible, and in fact relatively straightforward once you’ve got your head around the ARM template copy function, to deploy an Azure Function globally. One of the major strengths of a Serverless Architecture vs using, for example, servers running constantly in each region, is that pay for what you use is even more potent. To run this app without Azure Functions, and instead to put an App Service in each region, would be prohibitively expensive even if there was no traffic. As it is this solution comes in at about £16 a month which is the minimum price of a running Front Door. It’s too early to tell how expensive it would be going forward on a usage basis, but I suspect a lot cheaper than losing international customers.
© 2022 - Built by Daniel Bass