This year at traditional German ADC, I will talk about how to design and operate modern backends based on Actor Programming Model. This is session about reasoning about computation, states and partitioning. All this I will present based on Akka.NET open source framework. I will show how to design actors and how to deploy them to Docker container and then operate in Azure with help of Azure Container Registry and Azure Container Instances.
Imagine you have a nit of code, which does some computation.
public class MyComputeActor : UntypedActor
{
protected override void OnReceive(object message)
{
// This is a CPU intensive operation.
Console.WriteLine($"MyCompute : {{message} - Sender : {Sender}");
}
}
Now, we want to run this code on one physical node. This can be achived with following statement:
var remoteAddress1 = Address.Parse($"akka.tcp://DeployTarget@localhost:8090");
var remoteActor1 =
system.ActorOf(
Props.Create(() => new MyComputeActor())
.WithDeploy(Deploy.None.WithScope(new mRemoteScope(remoteAddress1))), "customer1");
var remoteActor2 = system.ActorOf(Props.Create(() => new MyComputeActor())
.WithDeploy(Deploy.None.WithScope(new RemoteScope(remoteAddress2))),
"customer2");
To make this happen, we need to create a host for MyComputeActor. Following is the full implementation of host, which I have used in this example:
using Akka.Actor;
using Akka.Configuration;
using Microsoft.Extensions.Configuration;
using System;
using System.Reflection;
using System.Runtime.Loader;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Linq;
namespace AkkaCluster
{
class Program
{
/// <summary>
/// --AKKAPORT 8089 --AKKAPUBLICHOST localhost --AKKASEEDHOST localhost:8089
/// </summary>
/// <param name="args"></param>
static void Main(string[] args)
{
var builder = new ConfigurationBuilder();
builder.AddEnvironmentVariables();
builder.AddCommandLine(args);
IConfigurationRoot netConfig = builder.Build();
var assembly = Assembly.Load(new AssemblyName("AkkaShared, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"));
Console.ForegroundColor = ConsoleColor.Cyan;
Console.WriteLine("Cluster running...");
int port = 8090;
string publicHostname = "localhost";
string seedhostsStr = String.Empty;
if (netConfig["AKKAPORT"] != null)
int.TryParse(netConfig["AKKAPORT"], out port);
if (netConfig["AKKAPUBLICHOST"] != null)
publicHostname = netConfig["AKKAPUBLICHOST"];
if (netConfig["AKKASEEDHOSTS"] != null)
{
seedhostsStr = netConfig["AKKASEEDHOSTS"];
}
string config = @"
akka {
actor.provider = ""Akka.Remote.RemoteActorRefProvider, Akka.Remote""
remote {
helios.tcp {
port = @PORT
public-hostname = @PUBLICHOSTNAME
hostname = 0.0.0.0
}
}
cluster {
seed-nodes = [@SEEDHOST]
}
}";
config = config.Replace("@PORT", port.ToString());
config = config.Replace("@PUBLICHOSTNAME", publicHostname);
if (seedhostsStr.Length > 0)
{
var seedHosts = seedhostsStr.Split(',');
seedHosts = seedHosts.Select(h => h.TrimStart(' ').TrimEnd(' ')).ToArray();
StringBuilder sb = new StringBuilder();
bool isFirst = true;
foreach (var item in seedHosts)
{
if (isFirst == false)
sb.Append(", ");
sb.Append($"\"akka.tcp://DeployTarget@{item}\"");
//example: seed - nodes = ["akka.tcp://ClusterSystem@localhost:8081"]
isFirst = false;
}
config = config.Replace("@SEEDHOST", sb.ToString());
}
else
config = config.Replace("@SEEDHOST", String.Empty);
Console.WriteLine(config);
using (var system = ActorSystem.Create("DeployTarget", ConfigurationFactory.ParseString(config)))
{
var cts = new CancellationTokenSource();
AssemblyLoadContext.Default.Unloading += (ctx) => cts.Cancel();
Console.CancelKeyPress += (sender, cpe) =>
{
CoordinatedShutdown.Get(system).Run(reason: CoordinatedShutdown.ClrExitReason.Instance).Wait();
cts.Cancel();
};
system.WhenTerminated.Wait();
return;
}
}
public static Task WhenCancelled(CancellationToken cancellationToken)
{
var tcs = new TaskCompletionSource<bool>();
cancellationToken.Register(s => ((TaskCompletionSource<bool>)s).SetResult(true), tcs);
return tcs.Task;
}
}
}
As next, I will create a docker container, which contains a host code. To do this I used following dockerfile:
FROM microsoft/dotnet:2.2-runtime AS base
WORKDIR /app
FROM microsoft/dotnet:2.2-sdk AS build
WORKDIR /src
COPY AkkaCluster/AkkaCluster.csproj AkkaCluster/
COPY AkkaShared/AkkaShared.csproj AkkaShared/
RUN dotnet restore AkkaCluster/AkkaCluster.csproj
COPY . .
WORKDIR /src/AkkaCluster
RUN dotnet build AkkaCluster.csproj -c Release -o /app
FROM build AS publish
RUN dotnet publish AkkaCluster.csproj -c Release -o /app
FROM base AS final
WORKDIR /app
COPY --from=publish /app .
ENTRYPOINT ["dotnet", "AkkaCluster.dll"]
Then I created container with following command:
docker build --rm -f "AkkaCluster\Dockerfile" -t akkacluster:v1 .
As next, I will push the container to Azure Container Registry:
docker tag akkacluster:v1 myreg.azurecr.io/akka-sum-cluster:v1
docker push myreg.azurecr.io/akka-sum-cluster:v1
Then I create two nodes:
az container create -g RG-AKKA-SUMCLUSTER --name akka-sum-host1 --image damir.azurecr.io/akka-sum-cluster:v1 --ports 8089 --ip-address Public --cpu 2 --memory 1 --dns-name-label akka-sum-host1 --environment-variables AKKAPORT=8089 AKKASEEDHOSTS="akka-sum-host1.westeurope.azurecontainer.io:8089,akka-sum-host2.westeurope.azurecontainer.io:8089" AKKAPUBLICHOST=akka-sum-host1.westeurope.azurecontainer.io --registry-username myreg --registry-password ***
az container create -g RG-AKKA-SUMCLUSTER --name akka-sum-host2 --image damir.azurecr.io/akka-sum-cluster:v1 --ports 8089 --ip-address Public --cpu 2 --memory 1 --dns-name-label akka-sum-host2 --environment-variables AKKAPORT=8089 AKKASEEDHOSTS="akka-sum-host1.westeurope.azurecontainer.io:8089,akka-sum-host2.westeurope.azurecontainer.io:8089" AKKAPUBLICHOST=akka-sum-host2.westeurope.azurecontainer.io --registry-username myreg --registry-password ***
Last two statements will provision a container instance with hosting code and run it in ACI.
Finally I need to change the URL of actors before activating:
var remoteAddress1 = Address.Parse($"akka.tcp://DeployTarget@akka-sum-host1.westeurope.azurecontainer.io:8089");
var remoteActor1 =
system.ActorOf(
Props.Create(() => new MyComputeActor())
.WithDeploy(Deploy.None.WithScope(new mRemoteScope(remoteAddress1))), "customer1");
var remoteActor2 = system.ActorOf(Props.Create(() => new MyComputeActor())
.WithDeploy(Deploy.None.WithScope(new RemoteScope(remoteAddress2))),
"customer2");
This demo at ADC19 session, which I also shown at WinDays2019 demonstrates, how to run compute logic at different nodes as Azure Container Instance.
To recap, this demo shows how to implement a distributed system as Actor Programming Model and run it as serverless service.