提示
即使熟悉 Docker 或 Orleans,建议还是通读本文,以避免在已知解决方法中出现潜在问题。
本文及其示例正在不断完善中。 欢迎反馈、PR 或建议。
将 Orleans 解决方案部署到 Docker
由于 Docker 编排器和集群堆栈的设计,在 Docker 中部署 Orleans 可能会很棘手。 最复杂的部分是了解 Docker Swarm 和 Kubernetes 网络模型中的 覆盖网络 的概念。
Docker 容器和网络模型主要用于运行无状态容器和不可变容器。 启动运行 Node.js 或 Nginx 应用程序的群集非常简单。 然而,使用更复杂的解决方案,例如真正的群集或分布式应用程序(如基于Orleans的应用程序),可能会带来设置上的困难。 这是可能的,但不像部署基于 Web 的应用程序那么简单。
Docker 群集涉及将多个主机分组为使用 容器业务流程协调程序管理的单个资源池。 Docker Inc. 提供 Swarm 作为其选项,而 Google 提供 Kubernetes (也称为 K8s)。 其他业务流程协调程序(如 DC/OS 和 Mesos )存在,但本文档重点介绍 Swarm 和 K8s,因为它们的使用范围更广。
支持在任意位置 Orleans 运行的相同粒度接口和实现也在 Docker 容器上运行。 在 Docker 容器中运行 Orleans 应用程序不需要任何特殊注意事项。
此处讨论的概念适用于 .NET Core 和 .NET Framework 4.6.1 版本 Orleans。 但是,为了说明 Docker 和 .NET Core 的跨平台性质,使用 .NET Core 的示例重点介绍。 可能需要提供特定于平台的详细信息(Windows/Linux/macOS)。
先决条件
本文假定已安装以下先决条件:
- Docker:Docker4X 为主要支持的平台提供易于使用的安装程序。 它包含 Docker 引擎和 Docker Swarm。
- Kubernetes (K8s):Google 的容器业务流程产品/服务。 指南包括有关安装 Minikube(本地 K8s 部署)、kubectl 以及相关依赖关系的指导。
- .NET:.NET 的跨平台风格。
- Visual Studio Code (VSCode):可以使用任何首选 IDE。 VSCode 在此处使用,因为它是跨平台的,可确保示例在所有平台上工作。 安装 VSCode 后,安装 C# 扩展。
重要
如果不使用它,则不需要 Kubernetes 安装。 Docker4X 安装程序已包含 Swarm,因此 Swarm 无需额外安装。
注意
在 Windows 上,Docker 安装程序在安装过程中启用 Hyper-V。 由于本文及其示例使用 .NET Core,因此使用的容器映像基于 Windows Server NanoServer。 如果计划改用 .NET Framework 4.6.1,请使用基于 Windows Server Core 和 Orleans 版本 1.4+ 的映像(仅支持 .NET Framework)。
创建 Orleans 解决方案
以下说明演示如何使用dotnet
工具创建标准Orleans解决方案。
根据平台调整命令。 目录结构只是建议;根据需要对其进行调整。
mkdir Orleans-Docker
cd Orleans-Docker
dotnet new sln
mkdir -p src/OrleansSilo
mkdir -p src/OrleansClient
mkdir -p src/OrleansGrains
mkdir -p src/OrleansGrainInterfaces
dotnet new console -o src/OrleansSilo --framework netcoreapp1.1
dotnet new console -o src/OrleansClient --framework netcoreapp1.1
dotnet new classlib -o src/OrleansGrains --framework netstandard1.5
dotnet new classlib -o src/OrleansGrainInterfaces --framework netstandard1.5
dotnet sln add src/OrleansSilo/OrleansSilo.csproj
dotnet sln add src/OrleansClient/OrleansClient.csproj
dotnet sln add src/OrleansGrains/OrleansGrains.csproj
dotnet sln add src/OrleansGrainInterfaces/OrleansGrainInterfaces.csproj
dotnet add src/OrleansClient/OrleansClient.csproj reference src/OrleansGrainInterfaces/OrleansGrainInterfaces.csproj
dotnet add src/OrleansSilo/OrleansSilo.csproj reference src/OrleansGrainInterfaces/OrleansGrainInterfaces.csproj
dotnet add src/OrleansGrains/OrleansGrains.csproj reference src/OrleansGrainInterfaces/OrleansGrainInterfaces.csproj
dotnet add src/OrleansSilo/OrleansSilo.csproj reference src/OrleansGrains/OrleansGrains.csproj
到目前为止,只创建了解决方案结构和项目的样板代码,并在它们之间添加了引用。 这与设置常规 Orleans 项目没有什么不同。
本文撰写时, Orleans 2.0(支持 .NET Core 和跨平台开发)处于技术预览版中。 其 NuGet 包托管在 MyGet 源上,而不是官方 NuGet.org 源。 若要安装预览版 NuGet 包,请使用 dotnet
CLI,强制使用 MyGet 中的源和版本:
dotnet add src/OrleansClient/OrleansClient.csproj package Microsoft.Orleans.Core -s https://dotnet.myget.org/F/orleans-prerelease/api/v3/index.json -v 2.0.0-preview2-201705020000
dotnet add src/OrleansGrainInterfaces/OrleansGrainInterfaces.csproj package Microsoft.Orleans.Core -s https://dotnet.myget.org/F/orleans-prerelease/api/v3/index.json -v 2.0.0-preview2-201705020000
dotnet add src/OrleansGrains/OrleansGrains.csproj package Microsoft.Orleans.Core -s https://dotnet.myget.org/F/orleans-prerelease/api/v3/index.json -v 2.0.0-preview2-201705020000
dotnet add src/OrleansSilo/OrleansSilo.csproj package Microsoft.Orleans.Core -s https://dotnet.myget.org/F/orleans-prerelease/api/v3/index.json -v 2.0.0-preview2-201705020000
dotnet add src/OrleansSilo/OrleansSilo.csproj package Microsoft.Orleans.OrleansRuntime -s https://dotnet.myget.org/F/orleans-prerelease/api/v3/index.json -v 2.0.0-preview2-201705020000
dotnet restore
好吧,运行简单 Orleans 应用程序的所有基本依赖项现已到位。 请注意,从常规 Orleans 应用程序设置开始到目前为止没有任何变化。 现在,让我们添加一些代码,使其正常运行。
实施Orleans应用程序
假设使用 VSCode ,请从解决方案目录运行 code .
。 此命令在 VSCode 中打开目录并加载解决方案。
这是之前创建的解决方案结构。
Program.cs、 OrleansHostWrapper.cs、 IGreetingGrain.cs和 GreetingGrain.cs 文件也分别添加到接口和粒度项目中。 下面是这些文件的代码:
IGreetingGrain.cs:
using System;
using System.Threading.Tasks;
using Orleans;
namespace OrleansGrainInterfaces
{
public interface IGreetingGrain : IGrainWithGuidKey
{
Task<string> SayHello(string name);
}
}
GreetingGrain.cs:
using System;
using System.Threading.Tasks;
using OrleansGrainInterfaces;
namespace OrleansGrains
{
public class GreetingGrain : Grain, IGreetingGrain
{
public Task<string> SayHello(string name)
{
return Task.FromResult($"Hello from Orleans, {name}");
}
}
}
OrleansHostWrapper.cs:
using System;
using System.NET;
using Orleans.Runtime;
using Orleans.Runtime.Configuration;
using Orleans.Runtime.Host;
namespace OrleansSilo;
public class OrleansHostWrapper
{
private readonly SiloHost _siloHost;
public OrleansHostWrapper(ClusterConfiguration config)
{
_siloHost = new SiloHost(Dns.GetHostName(), config);
_siloHost.LoadOrleansConfig();
}
public int Run()
{
if (_siloHost is null)
{
return 1;
}
try
{
_siloHost.InitializeOrleansSilo();
if (_siloHost.StartOrleansSilo())
{
Console.WriteLine(
$"Successfully started Orleans silo '{_siloHost.Name}' as a {_siloHost.Type} node.");
return 0;
}
else
{
throw new OrleansException(
$"Failed to start Orleans silo '{_siloHost.Name}' as a {_siloHost.Type} node.");
}
}
catch (Exception exc)
{
_siloHost.ReportStartupError(exc);
Console.Error.WriteLine(exc);
return 1;
}
}
public int Stop()
{
if (_siloHost is not null)
{
try
{
_siloHost.StopOrleansSilo();
_siloHost.Dispose();
Console.WriteLine($"Orleans silo '{_siloHost.Name}' shutdown.");
}
catch (Exception exc)
{
siloHost.ReportStartupError(exc);
Console.Error.WriteLine(exc);
return 1;
}
}
return 0;
}
}
Program.cs (Silo):
using System;
using System.Collections.Generic;
using System.Linq;
using System.NET;
using System.Threading.Tasks;
using Orleans.Runtime.Configuration;
namespace OrleansSilo
{
public class Program
{
private static OrleansHostWrapper s_hostWrapper;
static async Task<int> Main(string[] args)
{
int exitCode = await InitializeOrleansAsync();
Console.WriteLine("Press Enter to terminate...");
Console.ReadLine();
exitCode += ShutdownSilo();
return exitCode;
}
private static int InitializeOrleansAsync()
{
var config = new ClusterConfiguration();
config.Globals.DataConnectionString =
"[AZURE STORAGE CONNECTION STRING HERE]";
config.Globals.DeploymentId = "Orleans-Docker";
config.Globals.LivenessType =
GlobalConfiguration.LivenessProviderType.AzureTable;
config.Globals.ReminderServiceType =
GlobalConfiguration.ReminderServiceProviderType.AzureTable;
config.Defaults.PropagateActivityId = true;
config.Defaults.ProxyGatewayEndpoint =
new IPEndPoint(IPAddress.Any, 10400);
config.Defaults.Port = 10300;
var ips = await Dns.GetHostAddressesAsync(Dns.GetHostName());
config.Defaults.HostNameOrIPAddress =
ips.FirstOrDefault()?.ToString();
s_hostWrapper = new OrleansHostWrapper(config);
return hostWrapper.Run();
}
static int ShutdownSilo() =>
s_hostWrapper?.Stop() ?? 0;
}
}
Program.cs(客户端):
using System;
using System.NET;
using System.Threading;
using System.Threading.Tasks;
using Orleans;
using Orleans.Runtime.Configuration;
using OrleansGrainInterfaces;
namespace OrleansClient
{
class Program
{
private static IClusterClient s_client;
private static bool s_running;
static async Task Main(string[] args)
{
await InitializeOrleansAsync();
Console.ReadLine();
s_running = false;
}
static async Task InitializeOrleansAsync()
{
var config = new ClientConfiguration
{
DeploymentId = "Orleans-Docker";
PropagateActivityId = true;
};
var hostEntry =
await Dns.GetHostEntryAsync("orleans-silo");
var ip = hostEntry.AddressList[0];
config.Gateways.Add(new IPEndPoint(ip, 10400));
Console.WriteLine("Initializing...");
using client = new ClientBuilder().UseConfiguration(config).Build();
await client.Connect();
s_running = true;
Console.WriteLine("Initialized!");
var grain = client.GetGrain<IGreetingGrain>(Guid.Empty);
while (s_running)
{
var response = await grain.SayHello("Gutemberg");
Console.WriteLine($"[{DateTime.UtcNow}] - {response}");
await Task.Delay(1000);
}
}
}
}
此处未介绍粒度实现详细信息,因为它不在本文的范围内。 有关详细信息,请参阅其他相关文档。 这些文件表示最小的 Orleans 应用程序,充当本文其余部分的起点。
本文使用 OrleansAzureUtils
成员资格提供程序,但可以使用任何其他 Orleans受支持的提供程序。
Dockerfile
Docker 使用映像创建容器。 有关创建自定义映像的更多详细信息,请查看 Docker 文档。 本文使用官方 Microsoft图像。 根据目标和开发平台,选择相应的映像。
microsoft/dotnet:1.1.2-sdk
是这里使用的一个基于 Linux 的镜像。 例如,对于 Windows, microsoft/dotnet:1.1.2-sdk-nanoserver
可以使用。 选择适合需求的选项。
Windows 用户注意:如前所述,为了维护跨平台兼容性,本文使用了 .NET Core 和 Orleans Technical Preview 2.0。 若要将 Windows 上的 Docker 与完全发布的 Orleans 1.4+ 配合使用,请使用基于 Windows Server Core 的映像,因为 NanoServer 和基于 Linux 的映像仅支持 .NET Core。
Dockerfile.debug:
FROM microsoft/dotnet:1.1.2-sdk
ENV NUGET_XMLDOC_MODE skip
WORKDIR /vsdbg
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
unzip \
&& rm -rf /var/lib/apt/lists/* \
&& curl -sSL https://aka.ms/getvsdbgsh | bash /dev/stdin -v latest -l /vsdbg
WORKDIR /app
ENTRYPOINT ["tail", "-f", "/dev/null"]
此 Dockerfile 实质上下载并安装 VSdbg 调试器并启动一个空容器,使其无限期保持活动状态,因此它不需要在调试期间反复拆解和启动。
现在,在生产环境中,映像更小,因为它仅仅包含 .NET Core 运行时,而不是整个 SDK。 Dockerfile 也更简单:
Dockerfile:
FROM microsoft/dotnet:1.1.2-runtime
WORKDIR /app
ENTRYPOINT ["dotnet", "OrleansSilo.dll"]
COPY . /app
docker-compose 文件
该文件 docker-compose.yml
定义一组服务及其在服务级别项目中的依赖项。 每个服务都包含给定容器的一个或多个实例,具体取决于 Dockerfile 中选择的映像。 在 docker-compose 文档中查找有关的详细信息docker-compose
。
Orleans部署的常见用例涉及一个文件,其中包含两个服务:一个用于Orleans存储体,另一个用于Orleans客户端。 客户端服务依赖于筒仓服务,这意味着它只能在筒仓服务运行后启动。 另一种情况可能涉及添加存储或数据库服务/容器(如 SQL Server),该容器应在客户端和存储孤岛之前启动。 在这种情况下,客户端和独立服务都依赖于数据库服务。
注意
在进一步阅读之前,请注意缩进在docker-compose
文件中很重要。 如果出现问题,请予以关注。
下面是本文所述的服务描述方式:
docker-compose.override.yml(调试):
version: '3.1'
services:
orleans-client:
image: orleans-client:debug
build:
context: ./src/OrleansClient/bin/PublishOutput/
dockerfile: Dockerfile.Debug
volumes:
- ./src/OrleansClient/bin/PublishOutput/:/app
- ~/.nuget/packages:/root/.nuget/packages:ro
depends_on:
- orleans-silo
orleans-silo:
image: orleans-silo:debug
build:
context: ./src/OrleansSilo/bin/PublishOutput/
dockerfile: Dockerfile.Debug
volumes:
- ./src/OrleansSilo/bin/PublishOutput/:/app
- ~/.nuget/packages:/root/.nuget/packages:ro
docker-compose.yml(生产):
version: '3.1'
services:
orleans-client:
image: orleans-client
depends_on:
- orleans-silo
orleans-silo:
image: orleans-silo
在生产环境中,本地目录未被映射,也不包括 build:
操作。 原因是在生产环境中,应生成映像并将其推送到专用 Docker 注册表。
将所有内容放在一起
现在,所有必需的组件都已准备就绪,接下来让我们将它们组合在一起, Orleans 在 Docker 中运行解决方案。
重要
应从解决方案目录执行以下命令。
首先,确保还原解决方案的所有 NuGet 包。 这通常需要仅执行一次,除非包依赖项发生更改。
dotnet restore
现在,像往常一样使用 dotnet
CLI 生成解决方案,并将其发布到输出目录:
dotnet publish -o ./bin/PublishOutput
提示
在 Orleans 中使用 publish
而不是 build
来避免动加载程序集的问题。 仍在寻求更好的解决方案。
生成和发布应用程序后,使用 Dockerfiles 生成 Docker 映像。 此步骤通常需要为每个项目执行一次。 仅当 Dockerfile 或 docker-compose 文件发生更改,或者出于任何原因清理本地映像注册表时,才应再次需要它。
docker-compose build
在Dockerfile
和docker-compose.yml
中使用的所有基础镜像都从注册表拉取,并缓存在开发计算机上。 应用程序映像已生成,所有内容都已准备好运行。
现在,让我们运行应用程序!
# docker-compose up -d
Creating network "orleansdocker_default" with the default driver
Creating orleansdocker_orleans-silo_1 ...
Creating orleansdocker_orleans-silo_1 ... done
Creating orleansdocker_orleans-client_1 ...
Creating orleansdocker_orleans-client_1 ... done
#
现在,运行 docker-compose ps
显示 orleansdocker
项目正在运行的两个容器。
# docker-compose ps
Name Command State Ports
------------------------------------------------------------------
orleansdocker_orleans-client_1 tail -f /dev/null Up
orleansdocker_orleans-silo_1 tail -f /dev/null Up
注意
如果 Windows 和容器使用 Windows 基础映像, 命令 列会显示与 *NIX 系统上相同的 PowerShell 命令 tail
,使容器保持类似的运行。
现在容器正在运行,每次 Orleans 应用程序需要启动时,都不需要停止它们。 只需集成 IDE 即可调试容器内的应用程序,该容器以前已在 docker-compose.yml
中映射。
扩展
运行撰写项目后,使用 docker-compose scale
以下命令轻松纵向扩展或缩减应用程序:
# docker-compose scale orleans-silo=15
Starting orleansdocker_orleans-silo_1 ... done
Creating orleansdocker_orleans-silo_2 ...
Creating orleansdocker_orleans-silo_3 ...
Creating orleansdocker_orleans-silo_4 ...
Creating orleansdocker_orleans-silo_5 ...
Creating orleansdocker_orleans-silo_6 ...
Creating orleansdocker_orleans-silo_7 ...
Creating orleansdocker_orleans-silo_8 ...
Creating orleansdocker_orleans-silo_9 ...
Creating orleansdocker_orleans-silo_10 ...
Creating orleansdocker_orleans-silo_11 ...
Creating orleansdocker_orleans-silo_12 ...
Creating orleansdocker_orleans-silo_13 ...
Creating orleansdocker_orleans-silo_14 ...
Creating orleansdocker_orleans-silo_15 ...
Creating orleansdocker_orleans-silo_6
Creating orleansdocker_orleans-silo_5
Creating orleansdocker_orleans-silo_3
Creating orleansdocker_orleans-silo_2
Creating orleansdocker_orleans-silo_4
Creating orleansdocker_orleans-silo_9
Creating orleansdocker_orleans-silo_7
Creating orleansdocker_orleans-silo_8
Creating orleansdocker_orleans-silo_10
Creating orleansdocker_orleans-silo_11
Creating orleansdocker_orleans-silo_15
Creating orleansdocker_orleans-silo_12
Creating orleansdocker_orleans-silo_14
Creating orleansdocker_orleans-silo_13
几秒钟后,服务将扩展到请求的特定实例数。
# docker-compose ps
Name Command State Ports
------------------------------------------------------------------
orleansdocker_orleans-client_1 tail -f /dev/null Up
orleansdocker_orleans-silo_1 tail -f /dev/null Up
orleansdocker_orleans-silo_10 tail -f /dev/null Up
orleansdocker_orleans-silo_11 tail -f /dev/null Up
orleansdocker_orleans-silo_12 tail -f /dev/null Up
orleansdocker_orleans-silo_13 tail -f /dev/null Up
orleansdocker_orleans-silo_14 tail -f /dev/null Up
orleansdocker_orleans-silo_15 tail -f /dev/null Up
orleansdocker_orleans-silo_2 tail -f /dev/null Up
orleansdocker_orleans-silo_3 tail -f /dev/null Up
orleansdocker_orleans-silo_4 tail -f /dev/null Up
orleansdocker_orleans-silo_5 tail -f /dev/null Up
orleansdocker_orleans-silo_6 tail -f /dev/null Up
orleansdocker_orleans-silo_7 tail -f /dev/null Up
orleansdocker_orleans-silo_8 tail -f /dev/null Up
orleansdocker_orleans-silo_9 tail -f /dev/null Up
重要
这些示例中的 Command
列显示 tail
命令,因为使用了调试器容器。 例如,在生产环境中,它将显示 dotnet OrleansSilo.dll
。
Docker Swarm
Docker 的群集堆栈称为 Swarm。 有关详细信息,请参阅 Docker Swarm。
若要在群集中 Swarm
运行本文中所述的应用程序,无需执行额外的工作。 在Swarm
节点上运行docker-compose up -d
会根据配置的规则计划容器。 这同样适用于其他基于 Swarm 的服务,例如 Azure Kubernetes 服务(AKS)(在 Swarm 模式下)和 AWS 弹性容器服务(ECS)。 只需在部署 dockerizedOrleans 应用程序之前部署Swarm
群集。
注意
如果使用支持stack
、deploy
和compose
v3的 Docker 引擎与 Swarm 模式,部署解决方案的更好方法是docker stack deploy -c docker-compose.yml <name>
。 请记住,这需要与 Docker 引擎兼容的 v3 撰写文件。 许多托管服务(如 Azure 和 AWS)仍使用 v2 和更早的引擎。
Google Kubernetes (K8s)
如果计划使用 Kubernetes 托管 Orleans,可以在 OrleansContrib\Clustering.Kubernetes 处获得一个社区维护的群集提供程序。 在此处,使用提供程序无缝查找有关在 Kubernetes 中托管 Orleans 的文档和示例。
在容器内部调试 Orleans
了解从头开始在容器中运行 Orleans 后,利用 Docker 最重要的原则之一:不可变性是有益的。 容器在开发中应具有与生产环境相同的映像、依赖项和运行时。 这种做法有助于防止经典 “它在我的计算机上工作!” 问题。 为使这一点成为可能,需要一种在容器 内部 开发的方法,包括将调试器附加到在其中运行的应用程序。
有多种方法可以使用各种工具实现此目的。 在编写时评估多个选项后,选择了一个似乎更简单、对应用程序不太侵入的选项。
如前所述, VSCode
用于开发示例。 下面介绍如何将调试器附加到 Orleans 容器内的应用程序:
首先,修改解决方案中目录中的 .vscode
两个文件:
tasks.json:
{
"version": "0.1.0",
"command": "dotnet",
"isShellCommand": true,
"args": [],
"tasks": [
{
"taskName": "publish",
"args": [
"${workspaceRoot}/Orleans-Docker.sln", "-c", "Debug", "-o", "./bin/PublishOutput"
],
"isBuildCommand": true,
"problemMatcher": "$msCompile"
}
]
}
此文件实质上 VSCode
告知,每当项目生成时,它执行 publish
命令,类似于之前手动完成的方式。
launch.json:
{
"version": "0.2.0",
"configurations": [
{
"name": "Silo",
"type": "coreclr",
"request": "launch",
"cwd": "/app",
"program": "/app/OrleansSilo.dll",
"sourceFileMap": {
"/app": "${workspaceRoot}/src/OrleansSilo"
},
"pipeTransport": {
"debuggerPath": "/vsdbg/vsdbg",
"pipeProgram": "/bin/bash",
"pipeCwd": "${workspaceRoot}",
"pipeArgs": [
"-c",
"docker exec -i orleansdocker_orleans-silo_1 /vsdbg/vsdbg --interpreter=vscode"
]
}
},
{
"name": "Client",
"type": "coreclr",
"request": "launch",
"cwd": "/app",
"program": "/app/OrleansClient.dll",
"sourceFileMap": {
"/app": "${workspaceRoot}/src/OrleansClient"
},
"pipeTransport": {
"debuggerPath": "/vsdbg/vsdbg",
"pipeProgram": "/bin/bash",
"pipeCwd": "${workspaceRoot}",
"pipeArgs": [
"-c",
"docker exec -i orleansdocker_orleans-client_1 /vsdbg/vsdbg --interpreter=vscode"
]
}
}
]
}
现在,从 VSCode
中生成并发布解决方案,然后启动 Silo 和客户端配置。 VSCode 将 docker exec
命令发送到正在运行 docker-compose
的服务实例/容器,以启动附加到应用程序的调试器。 就是这样! 调试器附加到容器,可以像调试本地运行 Orleans 的应用程序一样使用。 关键区别在于应用程序在容器内运行。 完成开发后,将容器映像发布到注册表,并将其拉到生产中的 Docker 主机上。