Forsøg på at køre en skalerbar Go-server med dotnet Aspire

dotnet aspire?
dotnet aspire er et værktøj, der er blevet udviklet for at hjælpe udviklere med cloud-native udvikling og konfiguration, i takt med at cloud-native miljøer har vokset sig større. Værktøjet gør det muligt for .NET-udviklere nemt at udrulle .NET-projekter, forskellige cloud-native infrastrukturer, samt tjenester skrevet i andre sprog eller containere og lignende.
Naturligvis er det lanceret og drevet fra docker til k8s, og inden for mange områder, brancher og blandt mange udviklere har man i høj grad flyttet, og er allerede flyttet, fra traditionelle on-premises miljøer til cloud-native miljøer. Det er nu et modent område. Derfor forventer jeg, at der ikke er behov for at forklare de tidligere ulemper ved værtsnavne, portkonfiguration, firewalls, metrikstyring og lignende.
Ud fra ovenstående forklaringer vil du sikkert ikke have nogen idé om, hvad dotnet aspire egentlig er. Det skyldes, at Microsoft heller ikke har givet en præcis definition. Derfor vil jeg heller ikke give nogen særlig definition. I stedet vil jeg i denne artikel anvende de grundlæggende funktioner i dotnet aspire, som jeg har forstået dem, så du kan bruge dem som reference og selv finde din egen position.
Projektopsætning
Oprettelse af et dotnet aspire-projekt
Hvis dotnet aspire-skabelonen ikke findes, skal du først installere skabelonen. Installer skabelonen med følgende kommando. Hvis du ikke har .NET, skal du selv installere det.
1dotnet new install Aspire.ProjectTemplates
Opret derefter en ny solution i en passende mappe.
1dotnet new sln
Kør derefter følgende kommando i solution-mappen for at oprette et projekt baseret på skabelonen aspire-apphost.
1dotnet new aspire-apphost -o AppHost
Herefter oprettes et aspire-apphost-projekt, der kun indeholder en simpel kode til opsætningen.
Tilføjelse af Valkey
Lad os nu tilføje Valkey på en enkel måde.
Før vi tilføjer den, skal det nævnes, at dotnet aspire via community hosting tilbyder forskellige tredjepartsløsninger.
Valkey kan naturligvis også drage fordel af denne community hosting, og den kan nemt tages i brug via følgende NuGet-pakke.
1dotnet add package Aspire.Hosting.Valkey
Der findes desuden flere integrerede hostingmuligheder, som du kan se her.
Lad os vende tilbage til Valkey. Åbn filen Program.cs i AppHost-projektet og rediger den som følger.
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 er en implementering af grænsefladen IResourceBuilder, som indeholder oplysningerne til at bygge en Valkey-tjeneste.WithDataVolume opretter et volumen til lagring af cachedata, mens WithPersistence sørger for, at cachedata lagres løbende.
Ud fra dette ser det ud til, at den har en lignende rolle som volumes i docker-compose.
Naturligvis kan du også let oprette dette selv.
Det ligger dog uden for denne artikels rammer, så vi vil ikke gennemgå det her.
Oprettelse af en Go-ekkoserver
Lad os nu tilføje en simpel Go-server.
Først opretter du et workspace i solution-mappen med go work init.
For .NET-udviklere kan man betragte et Go-workspace som noget, der minder om en solution.
Opret derefter en mappe ved navn EchoServer, gå ind i den og kør go mod init EchoServer.
Med denne kommando oprettes et Go-modul. For .NET-udviklere kan et modul betragtes som noget, der minder om et projekt.
Opret derefter filen main.go og skriv følgende.
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}
Denne server læser portnummeret fra miljøvariablen PORT, som Aspire AppHost injicerer, når den kører, hvorefter den starter serveren på den pågældende port.
Det er en simpel server, der modtager forespørgselsparameteren name og returnerer Hello, {name}.
Lad os nu tilføje denne server til dotnet aspire.
Tilføjelse af ekkoserveren til aspire
Gå tilbage til Aspire AppHost-projektet, hvor vi tilføjede Valkey, og tilføj community hosting for Go.
1dotnet add package CommunityToolkit.Aspire.Hosting.Golang
Åbn derefter Program.cs og tilføj følgende udtryk.
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();
Her er echoServer en implementering af grænsefladen IResourceBuilder, som indeholder oplysningerne til at bygge Go-serveren.
Den nyligt tilføjede metode AddGolangApp er en extension method på den brugerdefinerede host, der tilføjer en Go-server.
Den bruger fast port 3000, og det kan ses, at miljøvariablen PORT injiceres.
Endelig gør WithExternalHttpEndpoints det muligt at tilgå serveren udefra.
Når du tester ved at åbne http://localhost:3000/?name=world, vil du kunne se, at Hello, world udskrives.
Men i den nuværende version af dotnet aspire er der en tung straf for ikke-dotnet-projekter.
Den er netop...
Udvidelse af projektet
Hvordan gør man så med horisontal skalering?
Den nuværende version af dotnet aspire tilbyder kun indstillingen WithReplica for builderen af .NET-projekter, der er tilføjet med metoden AddProject.
For Go-hosts eller eksterne projekter som AddContainer tilbydes denne indstilling dog ikke.
Derfor må man selv implementere det med en separat load balancer eller reverse proxy.
Men da denne reverse proxy så kan blive et SPOF, er det en god idé, at reverse proxy understøtter indstillingen WithReplica.
Så må reverse proxy nødvendigvis være et .NET-projekt.
Indtil nu har jeg forsøgt at løse dette med nginx, Traefik, egen implementering og lignende, men når begrænsningen om .NET-projekt indføres, havde jeg på stedet ingen løsning.
Derfor ledte jeg efter en reverse proxy implementeret i .NET, og heldigvis fandtes der muligheden YARP.
YARP er en reverse proxy implementeret i .NET, der også kan fungere som load balancer og tilbyder mange funktioner, så jeg vurderede, at det var et godt valg.
Lad os nu tilføje YARP.
Opsætning af reverse proxy med YARP
Først opretter du et projekt til brug med YARP.
1dotnet new web -n ReverseProxy
Gå derefter til projektet og installer YARP.
1dotnet add package Yarp.ReverseProxy --version 2.2.0
Når installationen er færdig, åbner du Program.cs og skriver følgende.
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"}");
Denne kode er den grundlæggende kode til brug af YARP.routes indeholder routinginformation, som reverse proxy skal bruge, og clusters indeholder klyngeinformation, som reverse proxy skal bruge.
Disse oplysninger indlæses i reverse proxy med metoden LoadFromMemory.
Til sidst mappes og køres reverse proxy med metoden MapReverseProxy.
For at bruge det i praksis skal du tilføje reverse proxy-projektet som reference i aspire apphost-projektet og tilføje eller ændre følgende udtryk i 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();
Nu kan reverse proxy referere til echo-serveren.
Den struktur, hvor eksterne anmodninger modtages af reverse proxy og videregives til echo-serveren, er nu under etablering.
Ændring af reverse proxy
Først skal du ændre den lytteadresse, der er tildelt reverse proxy-projektet.
Fjern applicationUrl i filen Properties/launchSettings.json.
Åbn derefter Program.cs og ret den grundlæggende kode væsentligt som vist nedenfor.
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"}");
Først ændrer vi oplysningerne om routes og clusters.
Vi tilføjer henholdsvis echo-route og echo-cluster og konfigurerer dem, så anmodninger sendes til echo-serveren.
Derudover ændres koden, så adressen til echo-serveren læses fra miljøvariablerne og anvendes.
Reglen for denne adresse er services__{service-name}__http__{index}.
For echo-serveren er tjenestenavnet echo-server, og da der kun er én instans, bruges indekset 0.
Hvis du tilføjer en asp.net core-server, kan flere instanser oprettes via WithReplica, og du kan så bruge indekset ved at øge det.
Den fallback-værdi http://localhost:8080, der er angivet som undtagelseshåndtering, er en meningsløs værdi.
Når du nu kører projektet og åbner http://localhost:3000/?name=world, vil du stadig kunne se, at Hello, world udskrives.
Ideer til udvidelse
Nu har vi bekræftet, at en Go-server kan tilføjes til dotnet aspire, og at anmodninger kan videresendes via reverse proxy.
Derefter kan vi udvide processen, så den implementeres programmatisk.
For eksempel kan vi tilføje nummerering efter tjenestenavnet for echo-serveren for at oprette flere instanser og automatisk tilføje konfigurationen til reverse proxy.
Ret koden i Program.cs i aspire apphost-projektet, så den bruger reverse proxy og echo-serveren, som vist nedenfor.
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}
Derefter ændrer du Program.cs i reverse proxy-projektet som følger.
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};
Vi tilføjer destinationskonfigurationen for de 8 echo-serverinstanser.
Nu har reverse proxy destinationsoplysninger for de udvidede echo-servere og kan videresende anmodninger til dem.
Når du åbner det tidligere http://localhost:3000/?name=world, vil du stadig kunne se, at Hello, world udskrives.
Afslutning
I denne artikel har jeg beskrevet processen med at tilføje en Go-server til dotnet aspire og videresende anmodninger via reverse proxy.
Dog har jeg endnu ikke skrevet alt om udvidelsen, men jeg har lagt et eksempel i et separat repository, der viser, hvordan det kan implementeres mere programmatisk ved hjælp af miljøvariabler.
Se snowmerak/AspireStartPack for detaljer om projektopsætningen og koden.
Personligt forventer jeg, at dotnet aspire kan spille sin egen rolle både som alternativ til docker compose og som værktøj til cloud-udrulning.
Der findes allerede generatorer, der opretter docker compose- eller k8s manifests, så jeg synes, at det er blevet lettere for almindelige udviklere at få adgang til infrastrukturværktøjer.