GoSuda

尝试在 dotnet aspire 中以可扩展方式运行 Go 服务器

By snowmerak
views ...

dotnet aspire?

dotnet aspire 是一款旨在帮助开发者进行云原生开发和配置的工具,其诞生背景是云原生环境的日益普及。该工具使 .NET 开发者能够轻松部署 .NET 项目以及各种云原生基础设施,包括其他语言编写的服务或容器。

自然地,从 Docker 到 Kubernetes 都有发布和运行,许多领域、行业和开发者正在或已经从传统的本地环境迁移到云原生环境。现在,这是一个成熟的领域。因此,我认为无需赘述以往在主机名、端口配置、防火墙和指标管理等方面存在的不便之处。

因此,仅凭上述描述,您可能仍然难以理解 dotnet aspire 的具体含义。因为 Microsoft 本身也没有给出明确的定义。因此,我也不打算给出一个具体的定义。不过,本文将使用我所理解的 dotnet aspire 的基本功能,您可以参考这些功能来确定您自己的定位。

项目配置

创建 dotnet aspire 项目

如果您的环境中没有 dotnet aspire 模板,您需要先安装它。使用以下命令安装模板。如果您的环境中没有 .NET,请自行安装。

1dotnet new install Aspire.ProjectTemplates

然后,在适当的文件夹中创建一个新的解决方案。

1dotnet new sln

之后,在解决方案文件夹中执行以下命令,创建 aspire-apphost 模板的项目。

1dotnet new aspire-apphost -o AppHost

这将创建一个仅包含基本设置代码的 aspire-apphost 项目。

添加 Valkey

接下来,我们简单地添加 Valkey。

在添加之前,需要指出的是,dotnet aspire 通过社区托管提供各种第三方解决方案。Valkey 当然也得到了此类社区托管的支持,并且可以通过以下 NuGet 包轻松使用。

1dotnet add package Aspire.Hosting.Valkey

此外,还提供了各种集成的托管服务,您可以在这里查看。回到 Valkey,打开 AppHost 项目中的 Program.cs 文件,并进行如下修改。

1var builder = DistributedApplication.CreateBuilder(args);
2
3var cache = builder.AddValkey("cache")
4    .WithDataVolume(isReadOnly: false)
5    .WithPersistence(interval: TimeSpan.FromMinutes(5),
6        keysChangedThreshold: 100);
7
8builder.Build().Run();

cacheIResourceBuilder 接口的一个实现,它包含构建 Valkey 服务所需的信息。WithDataVolume 创建一个用于存储缓存数据的卷,而 WithPersistence 则允许缓存数据被持久化存储。从这里可以看出,它与 docker-compose 中的 volumes 的作用类似。当然,您也可以轻松地实现类似的功能。但是,由于这超出了本文的范围,因此我将不在此处进行讨论。

创建 Go 语言 Echo 服务器

接下来,我们添加一个简单的 Go 语言服务器。首先,在解决方案文件夹中通过 go work init 命令创建一个工作区。对于 .NET 开发者来说,Go 工作区可以被视为类似于解决方案的概念。

然后,创建一个名为 EchoServer 的文件夹,并进入该文件夹,然后执行 go mod init EchoServer 命令。此命令用于创建 Go 模块。对于 .NET 开发者来说,模块可以被视为类似于项目的概念。接下来,创建一个 main.go 文件,并写入以下代码。

 1package main
 2
 3import (
 4	"log"
 5	"net/http"
 6	"os"
 7)
 8
 9func main() {
10	addr := os.Getenv("PORT")
11	log.Printf("Server started on %s", addr)
12
13	http.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {
14		name := request.URL.Query().Get("name")
15		writer.Write([]byte("Hello, " + name))
16	})
17
18	http.ListenAndServe(":"+addr, nil)
19}

此服务器在 Aspire AppHost 运行时,会读取注入的 PORT 环境变量,并使用该端口启动服务器。它简单地接收一个 name 查询参数,并返回 Hello, {name}

现在,我们将此服务器添加到 dotnet aspire 中。

将 Echo 服务器添加到 Aspire

回到添加 Valkey 的 Aspire AppHost 项目,为 Go 语言添加社区托管支持。

1dotnet add package CommunityToolkit.Aspire.Hosting.Golang

然后,打开 Program.cs 文件,并添加以下代码。

 1var builder = DistributedApplication.CreateBuilder(args);
 2
 3var cache = builder.AddValkey("cache")
 4    .WithDataVolume(isReadOnly: false)
 5    .WithPersistence(interval: TimeSpan.FromMinutes(5),
 6        keysChangedThreshold: 100);
 7
 8var echoServer = builder.AddGolangApp("echo-server", "../EchoServer")
 9    .WithHttpEndpoint(port: 3000, env: "PORT")
10    .WithExternalHttpEndpoints();
11
12builder.Build().Run();

在这里,echoServerIResourceBuilder 接口的一个实现,它包含构建 Go 语言服务器所需的信息。刚刚添加的 AddGolangApp 方法是用于添加 Go 语言服务器的自定义主机的扩展方法。它固定使用 3000 端口,并注入 PORT 环境变量。最后,WithExternalHttpEndpoints 允许外部访问。

为了测试,您可以访问 http://localhost:3000/?name=world,您应该会看到输出 Hello, world

然而,当前 dotnet aspire 对非 .NET 项目存在一个明显的劣势。那就是...

项目扩展

如何进行水平扩展?

目前,dotnet aspire 仅为通过 AddProject 方法添加的 .NET 项目的构建器提供 WithReplica 选项。但是,对于 Go 语言主机或诸如 AddContainer 之类的外部项目,它不提供此选项。

因此,您需要使用单独的负载均衡器或反向代理来手动实现。但是,如果这样做,反向代理可能会成为单点故障 (SPOF),因此最好让反向代理提供 WithReplica 选项。那么,反向代理必然需要是一个 .NET 项目。

尽管之前我曾使用 Nginx、Traefik 或自行实现等方法来解决此类问题,但如果限制为 .NET 项目,我手头目前没有可行的解决方案。因此,我寻找了一个用 .NET 实现的反向代理,幸运的是,我找到了一个名为 YARP 的选择。YARP 是一个用 .NET 实现的反向代理,它还可以充当负载均衡器,并提供各种功能,因此我认为这是一个不错的选择。

现在,让我们添加 YARP。

使用 YARP 配置反向代理

首先,创建一个用于使用 YARP 的项目。

1dotnet new web -n ReverseProxy

然后,进入该项目并安装 YARP。

1dotnet add package Yarp.ReverseProxy --version 2.2.0

安装完成后,打开 Program.cs 文件并写入以下代码。

 1using Yarp.ReverseProxy.Configuration;
 2
 3var builder = WebApplication.CreateBuilder(args);
 4
 5var routes = new List<RouteConfig>();
 6var clusters = new List<ClusterConfig>();
 7
 8builder.Services.AddReverseProxy()
 9    .LoadFromMemory(routes, clusters);
10
11var app = builder.Build();
12
13app.MapReverseProxy();
14app.Run(url: $"http://0.0.0.0:{Environment.GetEnvironmentVariable("PORT") ?? "5000"}");

这段代码是使用 YARP 的基本代码。routes 包含反向代理使用的路由信息,clusters 包含反向代理使用的集群信息。这些信息通过 LoadFromMemory 方法加载到反向代理中。最后,使用 MapReverseProxy 方法映射并运行反向代理。

为了实际使用,在 aspire apphost 项目中添加对反向代理项目的引用,并在 Program.cs 文件中添加和修改以下代码。

1dotnet add reference ../ReverseProxy
1var echoServer = builder.AddGolangApp("echo-server", "../EchoServer")
2    .WithHttpEndpoint(env: "PORT");
3
4var reverseProxy = builder.AddProject<Projects.ReverseProxy>("gateway")
5    .WithReference(echoServer)
6    .WithHttpEndpoint(port: 3000, env: "PORT", isProxied: true)
7    .WithExternalHttpEndpoints();

现在,反向代理可以引用 echo server 了。外部传入的请求将由反向代理接收,然后转发到 echo server。

修改反向代理

首先,我们需要更改分配给反向代理项目的监听地址。删除 Properties/launchSettings.json 文件中的 applicationUrl。然后,打开 Program.cs 文件并进行如下大规模修改。

 1using Yarp.ReverseProxy.Configuration;
 2
 3var builder = WebApplication.CreateBuilder(args);
 4
 5var routes = new List<RouteConfig>
 6{
 7    new RouteConfig
 8    {
 9        ClusterId = "cluster-echo",
10        RouteId = "route-echo",
11        Match = new RouteMatch
12        {
13            Path = "/"
14        }
15    }
16};
17
18var echoServerAddr = Environment.GetEnvironmentVariable("services__echo-server__http__0") ?? "http://localhost:8080";
19
20var clusters = new List<ClusterConfig>
21{
22    new ClusterConfig
23    {
24        ClusterId = "cluster-echo",
25        Destinations = new Dictionary<string, DestinationConfig>
26        {
27            { "destination-echo", new DestinationConfig { Address = echoServerAddr } }
28        }
29    }
30};
31
32builder.Services.AddReverseProxy()
33    .LoadFromMemory(routes, clusters);
34
35var app = builder.Build();
36
37app.MapReverseProxy();
38app.Run(url: $"http://0.0.0.0:{Environment.GetEnvironmentVariable("PORT") ?? "5000"}");

首先,修改 routesclusters 的信息。添加 echo-routeecho-cluster,以便将请求发送到 echo server。然后,修改代码,使其从环境变量中读取 echo server 的地址。

此地址的规则为 services__{service-name}__http__{index}。对于 echo server,服务名称为 echo-server,并且是单个实例,因此索引使用 0。如果添加 asp .net core 服务器,则可以使用 WithReplica 创建多个实例,因此可以通过递增索引来使用。作为异常处理的 http://localhost:8080 是一个没有任何意义的垃圾值。

现在,运行项目,并访问 http://localhost:3000/?name=world,您应该仍然看到输出 Hello, world

扩展思路

现在,我们已经确认了如何在 dotnet aspire 中添加 Go 服务器,并通过反向代理传递请求。那么,现在我们可以扩展这个过程,使其可以通过编程方式实现。例如,我们可以通过在 echo server 的服务名称后添加编号来创建多个实例,并自动为反向代理添加配置。

在 aspire apphost 项目的 Program.cs 文件中,修改使用反向代理和 echo server 的代码,如下所示。

 1var reverseProxy = builder.AddProject<Projects.ReverseProxy>("gateway")
 2    .WithHttpEndpoint(port: 3000, env: "PORT", isProxied: true)
 3    .WithExternalHttpEndpoints();
 4
 5for (var i = 0; i < 8; i++)
 6{
 7    var echoServer = builder.AddGolangApp($"echo-server-{i}", "../EchoServer")
 8        .WithHttpEndpoint(env: "PORT");
 9    reverseProxy.WithReference(echoServer);
10}

然后,修改反向代理项目的 Program.cs 文件,如下所示。

 1var echoServerDestinations = new Dictionary<string, DestinationConfig>();
 2for (var i = 0; i < 8; i++)
 3{
 4    echoServerDestinations[$"destination-{i}"] = new DestinationConfig
 5    {
 6        Address = Environment.GetEnvironmentVariable($"services__echo-server-{i}__http__0") ?? "http://localhost:8080"
 7    };
 8}
 9
10var clusters = new List<ClusterConfig>
11{
12    new ClusterConfig
13    {
14        ClusterId = "cluster-echo",
15        Destinations = echoServerDestinations
16    }
17};

为 8 个增加的 echo server 实例添加目标配置。现在,反向代理将具有增加的 echo server 的目标信息,并且能够转发请求。如果访问 http://localhost:3000/?name=world,您应该仍然看到输出 Hello, world

总结

本文介绍了如何在 dotnet aspire 中添加 Go 服务器,并通过反向代理传递请求的过程。但是,关于扩展的部分尚未完全完成,我已经在单独的存储库中编写了一个可以使用环境变量进行更多编程实现的示例。有关详细的项目配置和代码,请参考 snowmerak/AspireStartPack

我个人认为 dotnet aspire 可以作为 docker compose 的替代品,并作为云部署工具发挥其独特的作用。已经存在用于生成 docker compose 或 k8s 清单的生成器,我认为这使得普通开发人员更容易访问基础设施工具。