Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions ApiGateway/ApiGateway.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Ocelot" Version="24.1.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\AppHost\AppHost.ServiceDefaults\AppHost.ServiceDefaults.csproj" />
</ItemGroup>

</Project>
83 changes: 83 additions & 0 deletions ApiGateway/LoadBalancer/WeightedRandomBalancer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
using Ocelot.LoadBalancer.Errors;
using Ocelot.LoadBalancer.Interfaces;
using Ocelot.Responses;
using Ocelot.Values;

namespace ApiGateway.LoadBalancer;

/// <summary>
/// Балансировщик с взвешенным случайным выбором реплики сервиса.
/// </summary>
/// <param name="services">Все доступные экземпляры сервиса из service discovery.</param>
/// <param name="weights">Набор весов для эндпоинтов в формате key:"host_port" value:"weight".
/// </param>
public class WeightedRandomBalancer(Func<Task<List<Service>>> services, Dictionary<string, double> weights) : ILoadBalancer
{
public string Type => nameof(WeightedRandomBalancer);

private readonly Func<Task<List<Service>>> _services = services;
private readonly Dictionary<string, double> _weights = weights;

/// <summary>
/// Выбирает реплику сервиса по алгоритму weighted random.
/// </summary>
/// <param name="services">Список доступных сервисов.</param>
/// <param name="weights">Словарь весов для всех сервисов.</param>
/// <returns>Выбранная реплика сервиса.</returns>
private Service GetServiceByWeight(List<Service> services, Dictionary<string, double> weights)
{
var cumulativeWeights = new double[services.Count];
double sum = 0;
for (var i = 0; i < services.Count; ++i)
{
var service = services[i];
var key = $"{service.HostAndPort.DownstreamHost}_{service.HostAndPort.DownstreamPort}";
var weight = weights.TryGetValue(key, out var w) ? w : 1.0;

sum += weight;
cumulativeWeights[i] = sum;
}

if (sum <= 0)
{
return services[Random.Shared.Next(services.Count)];
}

var randomValue = Random.Shared.NextDouble();

var index = Array.BinarySearch(cumulativeWeights, randomValue);
if (index < 0)
{
index = ~index;
}
index = Math.Min(index, services.Count - 1);

return services[index];
}

/// <summary>
/// Выдает эндпоинт сервиса для текущего запроса.
/// </summary>
/// <param name="httpContext">Контекст входящего запроса.</param>
/// <returns>Выбранный адрес сервиса или ошибка, если сервисы недоступны.</returns>
public async Task<Response<ServiceHostAndPort>> LeaseAsync(HttpContext httpContext)
{
var services = await _services.Invoke();
if (services == null || services.Count == 0)
{
return new ErrorResponse<ServiceHostAndPort>(
new ServicesAreNullError("No services available")
);
}

var selectedService = GetServiceByWeight(services, _weights);

return new OkResponse<ServiceHostAndPort>(selectedService.HostAndPort);
}

/// <summary>
/// Освобождает ранее выданный эндпоинт.
/// </summary>
/// <param name="serviceHostAndPort">Адрес освобождаемого сервиса.</param>
public void Release(ServiceHostAndPort serviceHostAndPort) { }
}
55 changes: 55 additions & 0 deletions ApiGateway/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using ApiGateway.LoadBalancer;
using AppHost.ServiceDefaults;
using Ocelot.DependencyInjection;
using Ocelot.Middleware;

var builder = WebApplication.CreateBuilder(args);

builder.AddServiceDefaults();
builder.Services.AddServiceDiscovery();
builder.Configuration.AddJsonFile("ocelot.json", optional: false, reloadOnChange: true);
var weights = builder.Configuration
.GetSection("LoadBalancerWeights")
.Get<Dictionary<string, double>>() ?? [];
builder.Services
.AddOcelot()
.AddCustomLoadBalancer<WeightedRandomBalancer>((_, _, discoveryProvider) => new(discoveryProvider.GetAsync, weights));

var allowedOrigins = builder.Configuration.GetSection("Cors:AllowedOrigins").Get<string[]>();
var allowedMethods = builder.Configuration.GetSection("Cors:AllowedMethods").Get<string[]>();
var allowedHeaders = builder.Configuration.GetSection("Cors:AllowedHeaders").Get<string[]>();
builder.Services.AddCors(options =>
{
options.AddDefaultPolicy(policy =>
{
if (allowedOrigins != null)
{
_ = allowedOrigins.Contains("*")
? policy.AllowAnyOrigin()
: policy.WithOrigins(allowedOrigins);
}

if (allowedMethods != null)
{
_ = allowedMethods.Contains("*")
? policy.AllowAnyMethod()
: policy.WithMethods(allowedMethods);
}

if (allowedHeaders != null)
{
_ = allowedHeaders.Contains("*")
? policy.AllowAnyHeader()
: policy.WithHeaders(allowedHeaders);
}
});
});

var app = builder.Build();

app.MapDefaultEndpoints();
app.UseCors();

await app.UseOcelot();

app.Run();
23 changes: 23 additions & 0 deletions ApiGateway/Properties/launchSettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://localhost:5254",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "https://localhost:7268;http://localhost:5254",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
8 changes: 8 additions & 0 deletions ApiGateway/appsettings.Development.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
17 changes: 17 additions & 0 deletions ApiGateway/appsettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"RedisCache": "https://localhost:2843"
},
"Cors": {
"AllowedOrigins": [ "https://localhost:7282" ],
"AllowedMethods": [ "GET" ],
"AllowedHeaders": [ "*" ]
}
}
42 changes: 42 additions & 0 deletions ApiGateway/ocelot.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"Routes": [
{
"DownstreamPathTemplate": "/patient",
"DownstreamScheme": "http",
"DownstreamHostAndPorts": [
{
"Host": "localhost",
"Port": 8000
},
{
"Host": "localhost",
"Port": 8001
},
{
"Host": "localhost",
"Port": 8002
},
{
"Host": "localhost",
"Port": 8003
},
{
"Host": "localhost",
"Port": 8004
}
],
"UpstreamPathTemplate": "/patient",
"UpstreamHttpMethod": [ "Get" ],
"LoadBalancerOptions": {
"Type": "WeightedRandomBalancer"
}
}
],
"LoadBalancerWeights": {
"localhost_8000": 0.4,
"localhost_8001": 0.25,
"localhost_8002": 0.15,
"localhost_8003": 0.1,
"localhost_8004": 0.1
}
}
22 changes: 22 additions & 0 deletions AppHost/AppHost.AppHost/AppHost.AppHost.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<Project Sdk="Aspire.AppHost.Sdk/13.1.2">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<UserSecretsId>96fbf34e-00b4-4158-9be4-e6f641d5c362</UserSecretsId>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Aspire.Hosting.AppHost" Version="9.5.2" />
<PackageReference Include="Aspire.Hosting.Redis" Version="9.5.2" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\ApiGateway\ApiGateway.csproj" />
<ProjectReference Include="..\..\Client.Wasm\Client.Wasm.csproj" />
<ProjectReference Include="..\..\GenerationService\GenerationService.csproj" />
</ItemGroup>

</Project>
21 changes: 21 additions & 0 deletions AppHost/AppHost.AppHost/AppHost.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
var builder = DistributedApplication.CreateBuilder(args);

var cache = builder.AddRedis("cache")
.WithRedisInsight(containerName: "cache-insight");

var gateway = builder.AddProject<Projects.ApiGateway>("apigateway");

for (var i = 0; i < 5; ++i)
{
var generationService = builder.AddProject<Projects.GenerationService>($"generation-service-{i + 1}", launchProfileName: null)
.WithHttpEndpoint(8000 + i)
.WithReference(cache, "RedisCache")
.WaitFor(cache)
.WithHttpHealthCheck("/health");
gateway.WaitFor(generationService);
}

builder.AddProject<Projects.Client_Wasm>("client")
.WaitFor(gateway);

builder.Build().Run();
31 changes: 31 additions & 0 deletions AppHost/AppHost.AppHost/Properties/launchSettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:17103;http://localhost:15134",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"DOTNET_ENVIRONMENT": "Development",
"ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21139",
"ASPIRE_DASHBOARD_MCP_ENDPOINT_URL": "https://localhost:23225",
"ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22067"
}
},
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:15134",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"DOTNET_ENVIRONMENT": "Development",
"ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19075",
"ASPIRE_DASHBOARD_MCP_ENDPOINT_URL": "http://localhost:18212",
"ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20001"
}
}
}
}
8 changes: 8 additions & 0 deletions AppHost/AppHost.AppHost/appsettings.Development.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
12 changes: 12 additions & 0 deletions AppHost/AppHost.AppHost/appsettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Aspire.Hosting.Dcp": "Warning"
}
},
"Cache": {
"CacheTime": 60
}
}
22 changes: 22 additions & 0 deletions AppHost/AppHost.ServiceDefaults/AppHost.ServiceDefaults.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsAspireSharedProject>true</IsAspireSharedProject>
</PropertyGroup>

<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />

<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="10.1.0" />
<PackageReference Include="Microsoft.Extensions.ServiceDiscovery" Version="10.1.0" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.14.0" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.14.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.14.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.14.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.14.0" />
</ItemGroup>

</Project>
Loading
Loading