Опит за мащабируемо изпълнение на Go сървър в dotnet aspire

dotnet aspire?
dotnet aspire е инструмент, създаден, за да помогне на разработчиците при разработката и конфигурирането на облачно нативни приложения, тъй като облачно нативните среди непрекъснато се разрастват. Този инструмент позволява на .NET разработчиците лесно да разгръщат .NET проекти, различни облачно нативни инфраструктури, както и услуги или контейнери, написани на други езици.
Разбира се, технологиите варират от Docker до K8s и са пуснати в производство и поддържани, а голям брой сфери, индустрии и разработчици, работещи в традиционни локални (on-premises) среди, вече мигрират или са мигрирали към облачно нативна среда. Това вече е зрял домейн. Затова предполагам, че няма нужда да обяснявам неудобствата, с които са се сблъсквали досега, при управлението на имена на хостове, конфигурация на портове, защитни стени и метрики.
Затова дори от горните обяснения вероятно нямате никаква представа какво е dotnet aspire. Причината е, че дори Microsoft не дава точна дефиниция за него. Затова и аз няма да давам собствена дефиниция. Вместо това, в тази статия ще използвам основните функции на dotnet aspire, както ги разбирам аз, и можете да ги използвате като ориентир, за да определите своята собствена позиция.
Структура на проекта
Създаване на проект dotnet aspire
Ако нямате шаблона за dotnet aspire, първо трябва да го инсталирате. Инсталирайте шаблона с following команда. Ако нямате .NET, моля, инсталирайте го сами.
1dotnet new install Aspire.ProjectTemplates
След това създайте нов solution в подходяща папка.
1dotnet new sln
След това изпълнете следната команда в папката на solution, за да създадете проект по шаблона aspire-apphost.
1dotnet new aspire-apphost -o AppHost
Ще бъде създаден проект aspire-apphost, който съдържа само прост код за настройка.
Добавяне на Valkey
Нека добавим Valkey по прост начин.
Преди да го добавим директно, dotnet aspire предоставя различни решения на трети страни чрез community hosting.
Разбира се, Valkey също се поддържа от този community hosting и може лесно да се използва чрез следния NuGet пакет.
1dotnet add package Aspire.Hosting.Valkey
Предлагат се и други интегрирани хостинги, които можете да видите тук.
Връщайки се към Valkey, отворете файла Program.cs в проекта AppHost и го променете по следния начин.
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();
cache е реализация на интерфейса IResourceBuilder, която съдържа информация за изграждане на услугата Valkey.WithDataVolume създава том за съхранение на данните от кеша, а WithPersistence позволява данните от кеша да се запазват непрекъснато.
Дотук изглежда, че ролята им е подобна на volumes в docker-compose.
Разбира се, и вие можете без затруднение да създадете подобно нещо.
Но това излиза извън обхвата на тази статия, затова няма да го обсъждаме сега.
Създаване на Go echo сървър
Нека сега добавим прост сървър на Go.
Първо, в папката на solution създайте workspace с командата go work init.
За .NET разработчиците Go workspace може да се разглежда като аналог на solution.
След това създайте папка 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
Върнете се в проекта Aspire AppHost, където добавихте Valkey, и добавете community hosting за 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();
Тук echoServer е реализация на интерфейса IResourceBuilder, която съдържа информация за изграждане на Go сървъра.
Методът AddGolangApp, който току-що добавихме, е разширителен метод на персонализиран host за добавяне на Go сървър.
Използва се фиксиран порт 3000, а променливата на средата PORT се подава автоматично.
Накрая WithExternalHttpEndpoints позволява достъп отвън.
За тестване отворете http://localhost:3000/?name=world и ще видите, че се извежда Hello, world.
Но в момента dotnet aspire налага тежко наказание върху проектите, които не са на .NET.
Ето какво е то...
Разширяване на проекта
Как да направим хоризонтално мащабиране?
В момента dotnet aspire предоставя опцията WithReplica само за билдърите на .NET проекти, добавени чрез метода AddProject.
Но за Go хостове или външни проекти, добавени чрез AddContainer, тази опция не се предоставя.
Затова трябва самостоятелно да реализирате решение с отделен load balancer или reverse proxy.
Но тогава самият reverse proxy може да стане SPOF, затова е желателно reverse proxy да поддържа опцията WithReplica.
В такъв случай reverse proxy неизбежно трябва да бъде .NET проект.
Досега за този проблем съм използвал nginx, traefik, собствена реализация и други подходи, но при ограничението, че трябва да е .NET проект, нямах готово решение.
Затова потърсих reverse proxy, реализиран на .NET, и за щастие намерих YARP. YARP е reverse proxy, реализиран на .NET, може да работи и като load balancer и предлага разнообразни функции, затова реших, че е добър избор.
Сега нека добавим YARP.
Конфигуриране на reverse proxy с 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 съдържа информация за маршрутите, които reverse proxy ще използва, а clusters съдържа информация за клъстерите, които reverse proxy ще използва.
Тази информация се зарежда в reverse proxy чрез метода LoadFromMemory.
Накрая с метода MapReverseProxy се картографира reverse proxy и се изпълнява.
За реална употреба добавете в проекта aspire apphost препратка към проекта reverse proxy и добавете или променете следния код в 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();
Сега reverse proxy може да референцира echo сървъра.
Структурата е променена така, че външните заявки се приемат от reverse proxy и се предават на echo сървъра.
Промяна на reverse proxy
Първо трябва да промените адреса за слушане на проекта, на който е назначен reverse proxy.
Премахнете applicationUrl от файла Properties/launchSettings.json.
След това отворете файла 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"}");
Първо променяме информацията за routes и clusters.
Добавяме съответно echo-route и echo-cluster, като настройваме заявките да се изпращат към echo сървъра.
Също така адресът на echo сървъра се чете от променлива на средата и се използва оттам.
Правилото за този адрес е services__{service-name}__http__{index}.
За echo сървъра името на услугата е echo-server, а тъй като той е с единична инстанция, като индекс се използва 0.
Ако добавите ASP.NET Core сървър, чрез WithReplica могат да се създадат няколко инстанции и е достатъчно да увеличавате индекса.
Стойността http://localhost:8080, която е зададена при обработка на грешки, е безсмислена стойност.
Сега стартирайте проекта и отворете http://localhost:3000/?name=world. Ще видите, че и сега се извежда Hello, world.
Идеи за разширяване
Вече потвърдихме, че добавихме Go сървър към dotnet aspire и предаваме заявки през reverse proxy.
Сега можем да разширим това, за да реализираме процеса програмно.
Например, към името на услугата echo сървъра може да се добави номер, за да се създадат няколко инстанции, и конфигурацията на reverse proxy да се добавя автоматично.
Променете кода в Program.cs на проекта aspire apphost, който използва reverse proxy и echo сървъра, както е показано по-долу.
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 на проекта reverse proxy, както е показано по-долу.
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 сървъра.
Сега reverse proxy разполага с информацията за местоназначенията на всички echo сървъри и може да предава заявките към тях.
При отваряне на http://localhost:3000/?name=world отново ще видите, че се извежда Hello, world.
Заключение
В тази статия описах процеса на добавяне на Go сървър към dotnet aspire и предаването на заявки през reverse proxy.
Все пак разширяването още не съм описал изцяло, но в отделно хранилище съм подготвил пример, който показва как да се реализира по-програмно чрез променливи на средата.
За подробна структура на проекта и код вижте snowmerak/AspireStartPack.
Лично аз очаквам, че dotnet aspire може да играе собствена роля като алтернатива на docker compose и като инструмент за облачно разгръщане.
Вече съществуват генератори, които създават docker compose или k8s manifest файлове, и мисля, че достъпът на обикновените разработчици до инфраструктурни инструменти вече е по-лесен.