Orleans is an open source actor framework built by Microsoft research, and was used for halo cloud functionality! What is an actor framework? Another popular .net actor framework is AKKA.net, though I’ve not worked with it — and barely Orleans for that matter. Anyway…
From Wikipedia:
The actor model in computer science is a mathematical model of concurrent computation that treats “actors” as the universal primitives of concurrent computation. In response to a message that it receives, an actor can: make local decisions, create more actors, send more messages, and determine how to respond to the next message received. Actors may modify their own private state, but can only affect each other through messages (avoiding the need for any locks).
Why should I care about Orleans, or actor model frameworks in general?
In a monolithic system, you can more or less only scale “up”. With systems built using microservices, actors, etc, you have the option of scaling “out”. What does scaling “up” vs “out” mean? To scale a system up, means adding more RAM, more CPU — more resources, to the hardware in which your system runs; but you are still constrained to a single “box”. To scale “out” means you can just add a brand new machine, generally to a “cluster” of some sort, that allows your system to much more easily add additional resources. Sure, you can always add more RAM/CPU to your existing machines in a microservices system, but you also have the option to have more machines! Options are always nice!
What does this all mean? With all the cloud services, containerization, and VMs readily available in today’s world, it can be extremely simple to spin up and down resources as necessary. Just add a new node to the cluster!
What can Orleans do?
Note all this information, and in further detail, can be found in the Orleans documentation. Orleans works off of a few concepts:
- Grains — the “virtual actors” and/or “primitives” that are described in the actor model definition above. Grains are the objects that actually contain your logic that is to be distributed. Each individual grain is guaranteed to operate in a single-threaded execution model as to greatly simplify the programming, and avoid race conditions. The grains are written in an asynchronous manner, and are intended for very fast running operations (< 200ms IIRC) — though I’m using it for operations that take MUCH longer, maybe I can do a post about that at some point if everything works!
- Silos — the area where your “grains” are kept. A silo can contain many grain types, as well as many instantiations of those types, depending on your needs.
- Clusters — a collection of silos. This allows for the “scale out” portion of Orleans. If more or less resources are needed, you can simple register or kill silos on your cluster. Scaling made easy!
Example Orleans Application
I’m hoping I can put together some more functional application as I learn more, but just to get started…
An Orleans application consists of a few separate pieces, generally all as separate projects:
- Grain interfaces
- Grain implementations
- Orleans Silo host
- Orleans Client
Let’s get started!
- Ensure you have the .net core sdk installed as well as an IDE of choice — like Visual Studio or Visual Studio Code
- Run a few commands to get our projects started:
dotnet new console -n Kritner.OrleansGettingStarted.Client
dotnet new console -n Kritner.OrleansGettingStarted.SiloHost
dotnet new classlib -n Kritner.OrleansGettingStarted.GrainInterfaces
dotnet new classlib -n Kritner.OrleansGettingStarted.Grains
- Add NuGet packages through the manager, console, or csproj file to look like…
GrainInterfaces / Grains csproj:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Orleans.Core.Abstractions" Version="2.1.0" />
<PackageReference Include="Microsoft.Orleans.OrleansCodeGenerator.Build" Version="2.1.0" />
</ItemGroup>
</Project>
Client csproj:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="2.1.0" />
<PackageReference Include="Microsoft.Orleans.Client" Version="2.1.0" />
</ItemGroup>
</Project>
Server csproj:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="2.1.0" />
<PackageReference Include="Microsoft.Orleans.Server" Version="2.1.0" />
</ItemGroup>
</Project>
- Add project dependencies between the orleans projects. Grains, Client, and Server should all depend on the GrainsInterfaces project. The SiloHost project should additionally depend on the Grains project. It should all look like:
That’s all that’s needed to get started with “getting started with Orleans”! The repo at this point in time, while minimal, can be found at: https://github.com/Kritner-Blogs/OrleansGettingStarted/tree/8944333ae23f21e7873a356a191ceceb3cc91c97
Note in the image and repo linked above I had missed a dependency at this point. The SiloHost project should additionally have a reference to the Grains project, that would not be reflected in the above point in time. You could also go ahead and add references to Microsoft.Extensions.Logging.Console in the Client/SiloHostas well (will be needed later).
That silly Hello World Orleans example
Let’s start with the most basic example — hello world. This won’t really show off what Orleans can do very well, but we have to start somewhere right?
Let’s introduce a IHelloWorld interface in our GrainInterfaces project:
namespace Kritner.OrleansGettingStarted.GrainInterfaces
{
public interface IHelloWorld : IGrainWithGuidKey
{
Task<string> SayHello(string name);
}
}
A few (maybe?) non standard things happening in the above from what you may be used to:
- IHelloWorld implements IGrainWithGuidKey — an interface that defines an Orleans grain, and its key type. I believe all key types get converted to a Guid in the end anyway, so this is what I usually stick with unless there is some unique contextual data that can be used for grain identification.
- Task — all Orleans grains should be programmed in an asynchronous manner and as such, all grains will return at a minimum Task (void), if not a Task (a return value).
The above grain interface simply takes in a string name and returns a Task.
Now for the implementation in the Grains project, class HelloWorld:
namespace Kritner.OrleansGettingStarted.Grains
{
public class HelloWorld : Grain, IHelloWorld
{
public Task<string> SayHello(string name)
{
return Task.FromResult($"Hello World! Orleans is neato torpedo, eh {name}?");
}
}
}
Again, mostly standard stuff here. We’re extending a base Grain class. implementing from our IHelloWorld, and providing the implementation. There’s really not much to our method, so AFAIK no reason to await the result (can someone correct me if I’m wrong? Async/await is still quite new to me).
We now have all that is necessary for Orleans to work, aside from that whole Client/Server setup and config — on to that next!
The working repo as of this point in the post can be found: https://github.com/Kritner-Blogs/OrleansGettingStarted/tree/d244f6e67384d8e992e15625f619072863429663
Client/Server Setup
Next is the client and server setup, which we’ll be doing in our currently untouched projects of Client and SiloHost.
Note the below configuration is specifically for development, it does not, and cannot operate as a cluster of nodes (AFAIK) like a production configuration can/should.
Client.Program.cs (note more or less copied from (https://github.com/dotnet/orleans/blob/master/Samples/2.0/HelloWorld/src/OrleansClient/Program.cs):
In the above there’s a fair amount of logic going into making sure we can successfully get an instance of IClusterClient. This bootstrapping of the client only needs to be done in one place (and if you have multiple applications that use the same client, could be extracted to a helper class).
The actual “work” of the IClusterClient from a grain perspective is all done in the method DoClientWork.
SiloHost.Program.cs (more or less copied from https://github.com/dotnet/orleans/blob/master/Samples/2.0/HelloWorld/src/SiloHost/Program.cs):
That should be everything we need to get our Orleans demo working — and luckily the client/server configuration doesn’t really change much after its done, though the initial setup can be a bit tricky (most of the reason why i just copied the sample’s example).
Note: I added an additional NuGet package to both the Client and SiloHost projects to allow for pretty logging within the console window.
Testing it out
With an Orleans project, your normal application (Client as example) is reliant on the SiloHost being up and running. There is some retry logic built into the above client implementation, but not a bad idea to bring up the server prior to the client.
Let’s do that:
- open two terminals
- One in working directory src/…SiloHost
- One in working directory src/...Client
- From SiloHost, run command
dotnet run
. You should be presented with something like:
Your silo host should now be up, running, and await requests.
- Start the client with
dotnet run
. You should be presented with something like:
- Now enter your name, and watch the magic happen! Behold:
The left side is the Logger info from the SiloHost, and on the right is the Client app. You can see through the highlights that the SiloHost opened a socket when the client connected, and closed it the client completed executing. On the client side, you can see that we entered our name, and the Orleans SiloHost sent it back!
The above is of course, just a simple example, but it helps set the foundation of potentially great things to come!
The repository at this point of the post can be found at https://github.com/Kritner-Blogs/OrleansGettingStarted/releases/tag/v0.1
Top comments (6)
Great article! One comment though:
While I mostly agree, load balancers are a very simple, common way of scaling "out" monolith applications. At my job, we have a web app that gets 1 million+ hits per day. It's load balanced across 6 servers and can be scaled quite easily.
Thanks for introducing me to this tech!
Very true, I'm not sure on all the terminology, but is it still a monolith if you're using multiple instances of it? Seems like there are multiple ways to break up a monolith if that's what you're dealing with; orleans and load balancing could both just be different methods of doing it no?
To me (and take this with a large grain of salt, still getting into the microservices arena), the difference between monoliths and microservices is separation and duplication. Each microservice is distinct in that it performs a single function and is separate from other services. A monolith performs all the actions.
For the case of the load balanced monolith, it's all the same application running the exact same code, just duplicated. The beauty of load balancing is that you can use the same application and get great performance across multiple machines.
Of course, that does come with the usual downsides of monolith, most importantly (to me) the inability to scale individual features of applications.
There is one other benefit worth mentioning to Orleans, the primary one in my mind. Statefulness. In many systems, services scale by being stateless. They remember nothing from before. They have to get the data shipped in on every request. That puts all the scalability pressure on the data storage. For certain types of use cases, updates come in too fast to make statelessness and "data shipping" work. Those cases have to use a stateful workflow where the data remains loaded and ready for computation. However, stateful services are more difficult to scale than stateless -- you can't just deploy more copies, you have to keep track of which node owns which states. That's where Orleans comes in. It handles the scaling for you while letting you take advantage of stateful processing.
All that sounds awesome! I'm hoping I can get into more information like that as I continue exploring (and posting hopefully :D).
For sure. Thanks for the post!