---
id: 7ecf5f1e3335910ea27e6beb68f04ae7
author: snowmerak
title: Опит за мащабируемо изпълнение на Go сървър в dotnet aspire
description: 'Как да изпълняваме Go сървър мащабируемо с .NET Aspire: пример за хоризонтално мащабиране и автоматизирана конфигурация с YARP обратен прокси.'
language: bg
date: 2025-01-05T05:04:22.744778105Z
path: /blog/posts/exploring-the-scalable-execution-of-go-servers-in-net-aspire-z42f3b6ee
category: network
---

## dotnet aspire?

dotnet aspire е инструмент, създаден, за да помогне на разработчиците при разработката и конфигурирането на облачно нативни приложения, тъй като облачно нативните среди непрекъснато се разрастват. Този инструмент позволява на .NET разработчиците лесно да разгръщат .NET проекти, различни облачно нативни инфраструктури, както и услуги или контейнери, написани на други езици.

Разбира се, технологиите варират от Docker до K8s и са пуснати в производство и поддържани, а голям брой сфери, индустрии и разработчици, работещи в традиционни локални (on-premises) среди, вече мигрират или са мигрирали към облачно нативна среда. Това вече е зрял домейн. Затова предполагам, че няма нужда да обяснявам неудобствата, с които са се сблъсквали досега, при управлението на имена на хостове, конфигурация на портове, защитни стени и метрики.

Затова дори от горните обяснения вероятно нямате никаква представа какво е dotnet aspire. Причината е, че дори Microsoft не дава точна дефиниция за него. Затова и аз няма да давам собствена дефиниция. Вместо това, в тази статия ще използвам основните функции на dotnet aspire, както ги разбирам аз, и можете да ги използвате като ориентир, за да определите своята собствена позиция.

## Структура на проекта

### Създаване на проект dotnet aspire

Ако нямате шаблона за dotnet aspire, първо трябва да го инсталирате. Инсталирайте шаблона с following команда. Ако нямате .NET, моля, инсталирайте го сами.

```bash
dotnet new install Aspire.ProjectTemplates
```

След това създайте нов solution в подходяща папка.

```bash
dotnet new sln
```

След това изпълнете следната команда в папката на solution, за да създадете проект по шаблона aspire-apphost.

```bash
dotnet new aspire-apphost -o AppHost
```

Ще бъде създаден проект aspire-apphost, който съдържа само прост код за настройка.

### Добавяне на Valkey

Нека добавим Valkey по прост начин.

Преди да го добавим директно, dotnet aspire предоставя различни решения на трети страни чрез community hosting.  
Разбира се, Valkey също се поддържа от този community hosting и може лесно да се използва чрез следния NuGet пакет.

```bash
dotnet add package Aspire.Hosting.Valkey
```

Предлагат се и други интегрирани хостинги, които можете да видите [тук](https://learn.microsoft.com/ko-kr/dotnet/aspire/fundamentals/integrations-overview).  
Връщайки се към Valkey, отворете файла Program.cs в проекта AppHost и го променете по следния начин.

```csharp
var builder = DistributedApplication.CreateBuilder(args);

var cache = builder.AddValkey("cache")
    .WithDataVolume(isReadOnly: false)
    .WithPersistence(interval: TimeSpan.FromMinutes(5),
        keysChangedThreshold: 100);

builder.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` и го напишете по следния начин.

```go
package main

import (
	"log"
	"net/http"
	"os"
)

func main() {
	addr := os.Getenv("PORT")
	log.Printf("Server started on %s", addr)

	http.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {
		name := request.URL.Query().Get("name")
		writer.Write([]byte("Hello, " + name))
	})

	http.ListenAndServe(":"+addr, nil)
}
```

Този сървър, когато Aspire AppHost се изпълни и подаде променливата на средата `PORT`, върху която трябва да слуша, чете този порт и стартира сървъра.  
Това е прост сървър, който приема заявка с параметър `name` и връща `Hello, {name}`.

Сега нека добавим този сървър към dotnet aspire.

### Добавяне на echo сървъра към aspire

Върнете се в проекта Aspire AppHost, където добавихте Valkey, и добавете community hosting за Go.

```bash
dotnet add package CommunityToolkit.Aspire.Hosting.Golang
```

След това отворете файла Program.cs и добавете следния код.

```csharp
var builder = DistributedApplication.CreateBuilder(args);

var cache = builder.AddValkey("cache")
    .WithDataVolume(isReadOnly: false)
    .WithPersistence(interval: TimeSpan.FromMinutes(5),
        keysChangedThreshold: 100);

var echoServer = builder.AddGolangApp("echo-server", "../EchoServer")
    .WithHttpEndpoint(port: 3000, env: "PORT")
    .WithExternalHttpEndpoints();

builder.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](https://microsoft.github.io/reverse-proxy/). YARP е reverse proxy, реализиран на .NET, може да работи и като load balancer и предлага разнообразни функции, затова реших, че е добър избор.

Сега нека добавим YARP.

### Конфигуриране на reverse proxy с YARP

Първо създайте проект за използване на YARP.

```bash
dotnet new web -n ReverseProxy
```

След това влезте в проекта и инсталирайте YARP.

```bash
dotnet add package Yarp.ReverseProxy --version 2.2.0
```

След като инсталацията приключи, отворете файла Program.cs и го напишете по следния начин.

```csharp
using Yarp.ReverseProxy.Configuration;

var builder = WebApplication.CreateBuilder(args);

var routes = new List<RouteConfig>();
var clusters = new List<ClusterConfig>();

builder.Services.AddReverseProxy()
    .LoadFromMemory(routes, clusters);

var app = builder.Build();

app.MapReverseProxy();
app.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.

```bash
dotnet add reference ../ReverseProxy
```

```csharp
var echoServer = builder.AddGolangApp("echo-server", "../EchoServer")
    .WithHttpEndpoint(env: "PORT");

var reverseProxy = builder.AddProject<Projects.ReverseProxy>("gateway")
    .WithReference(echoServer)
    .WithHttpEndpoint(port: 3000, env: "PORT", isProxied: true)
    .WithExternalHttpEndpoints();
```

Сега reverse proxy може да референцира echo сървъра.  
Структурата е променена така, че външните заявки се приемат от reverse proxy и се предават на echo сървъра.

### Промяна на reverse proxy

Първо трябва да промените адреса за слушане на проекта, на който е назначен reverse proxy.  
Премахнете `applicationUrl` от файла `Properties/launchSettings.json`.  
След това отворете файла Program.cs и го променете изцяло, както е показано по-долу.

```csharp
using Yarp.ReverseProxy.Configuration;

var builder = WebApplication.CreateBuilder(args);

var routes = new List<RouteConfig>
{
    new RouteConfig
    {
        ClusterId = "cluster-echo",
        RouteId = "route-echo",
        Match = new RouteMatch
        {
            Path = "/"
        }
    }
};

var echoServerAddr = Environment.GetEnvironmentVariable("services__echo-server__http__0") ?? "http://localhost:8080";

var clusters = new List<ClusterConfig>
{
    new ClusterConfig
    {
        ClusterId = "cluster-echo",
        Destinations = new Dictionary<string, DestinationConfig>
        {
            { "destination-echo", new DestinationConfig { Address = echoServerAddr } }
        }
    }
};

builder.Services.AddReverseProxy()
    .LoadFromMemory(routes, clusters);

var app = builder.Build();

app.MapReverseProxy();
app.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 сървъра, както е показано по-долу.

```csharp
var reverseProxy = builder.AddProject<Projects.ReverseProxy>("gateway")
    .WithHttpEndpoint(port: 3000, env: "PORT", isProxied: true)
    .WithExternalHttpEndpoints();

for (var i = 0; i < 8; i++)
{
    var echoServer = builder.AddGolangApp($"echo-server-{i}", "../EchoServer")
        .WithHttpEndpoint(env: "PORT");
    reverseProxy.WithReference(echoServer);
}
```

След това променете файла Program.cs на проекта reverse proxy, както е показано по-долу.

```csharp
var echoServerDestinations = new Dictionary<string, DestinationConfig>();
for (var i = 0; i < 8; i++)
{
    echoServerDestinations[$"destination-{i}"] = new DestinationConfig
    {
        Address = Environment.GetEnvironmentVariable($"services__echo-server-{i}__http__0") ?? "http://localhost:8080"
    };
}

var clusters = new List<ClusterConfig>
{
    new ClusterConfig
    {
        ClusterId = "cluster-echo",
        Destinations = echoServerDestinations
    }
};
```

Добавяме конфигурация за местоназначение за 8-те инстанции на echo сървъра.  
Сега reverse proxy разполага с информацията за местоназначенията на всички echo сървъри и може да предава заявките към тях.  
При отваряне на `http://localhost:3000/?name=world` отново ще видите, че се извежда `Hello, world`.

## Заключение

В тази статия описах процеса на добавяне на Go сървър към dotnet aspire и предаването на заявки през reverse proxy.  
Все пак разширяването още не съм описал изцяло, но в отделно хранилище съм подготвил пример, който показва как да се реализира по-програмно чрез променливи на средата.  
За подробна структура на проекта и код вижте [snowmerak/AspireStartPack](https://github.com/snowmerak/AspireStarterPack).

Лично аз очаквам, че dotnet aspire може да играе собствена роля като алтернатива на docker compose и като инструмент за облачно разгръщане.  
Вече съществуват [генератори](https://prom3theu5.github.io/aspirational-manifests/generate-command.html), които създават docker compose или k8s manifest файлове, и мисля, че достъпът на обикновените разработчици до инфраструктурни инструменти вече е по-лесен.
