Docker 部署

提示

即使熟悉 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/OSMesos )存在,但本文档重点介绍 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 中打开目录并加载解决方案。

这是之前创建的解决方案结构。

Visual Studio Code:资源管理器,其中已选择 Program.cs。

Program.csOrleansHostWrapper.csIGreetingGrain.csGreetingGrain.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

Dockerfiledocker-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群集。

注意

如果使用支持stackdeploycompose 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 主机上。