From fa119a4b3314ef097d9c0b19adfe3b27db68924f Mon Sep 17 00:00:00 2001 From: Matvey6M6 Date: Tue, 14 Apr 2026 23:58:18 +0400 Subject: [PATCH 1/2] Lab 2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit + настроен запуск нескольких реплик сервиса через .NET Aspire + добавлен API Gateway на основе Ocelot + реализован алгоритм Weighted Round Robin + настроено распределение запросов между репликами по весам + обновлена маршрутизация клиентского приложения через gateway + выполнена проверка корректности балансировки через логи и трейсы --- ApiGateway/ApiGateway.csproj | 25 +++++ .../WeightedRoundRobinOptions.cs | 16 +++ .../WeightedRoundRobinBalancer.cs | 72 +++++++++++++ ApiGateway/Program.cs | 43 ++++++++ ApiGateway/Properties/launchSettings.json | 14 +++ ApiGateway/appsettings.Development.json | 9 ++ ApiGateway/appsettings.json | 10 ++ ApiGateway/ocelot.json | 32 ++++++ AspireApp/AspireApp.AppHost/AppHost.cs | 25 +++++ .../AspireApp.AppHost.csproj | 24 +++++ .../Properties/launchSettings.json | 32 ++++++ .../appsettings.Development.json | 8 ++ AspireApp/AspireApp.AppHost/appsettings.json | 8 ++ .../AspireApp.ServiceDefaults.csproj | 21 ++++ .../AspireApp.ServiceDefaults/Extensions.cs | 101 ++++++++++++++++++ Client.Wasm/Components/DataCard.razor | 81 +++++++++++--- Client.Wasm/Components/StudentCard.razor | 14 ++- Client.Wasm/wwwroot/appsettings.json | 2 +- ServiceApi/Entities/Employee.cs | 39 +++++++ ServiceApi/Generator/EmployeeGenerator.cs | 92 ++++++++++++++++ .../Generator/EmployeeGeneratorService.cs | 82 ++++++++++++++ .../Generator/IEmployeeGeneratorService.cs | 11 ++ ServiceApi/Program.cs | 78 ++++++++++++++ ServiceApi/Properties/launchSettings.json | 14 +++ ServiceApi/Service.Api.csproj | 20 ++++ ServiceApi/appsettings.Development.json | 12 +++ ServiceApi/appsettings.json | 10 ++ 27 files changed, 874 insertions(+), 21 deletions(-) create mode 100644 ApiGateway/ApiGateway.csproj create mode 100644 ApiGateway/Configuration/WeightedRoundRobinOptions.cs create mode 100644 ApiGateway/LoadBalancing/WeightedRoundRobinBalancer.cs create mode 100644 ApiGateway/Program.cs create mode 100644 ApiGateway/Properties/launchSettings.json create mode 100644 ApiGateway/appsettings.Development.json create mode 100644 ApiGateway/appsettings.json create mode 100644 ApiGateway/ocelot.json create mode 100644 AspireApp/AspireApp.AppHost/AppHost.cs create mode 100644 AspireApp/AspireApp.AppHost/AspireApp.AppHost.csproj create mode 100644 AspireApp/AspireApp.AppHost/Properties/launchSettings.json create mode 100644 AspireApp/AspireApp.AppHost/appsettings.Development.json create mode 100644 AspireApp/AspireApp.AppHost/appsettings.json create mode 100644 AspireApp/AspireApp.ServiceDefaults/AspireApp.ServiceDefaults.csproj create mode 100644 AspireApp/AspireApp.ServiceDefaults/Extensions.cs create mode 100644 ServiceApi/Entities/Employee.cs create mode 100644 ServiceApi/Generator/EmployeeGenerator.cs create mode 100644 ServiceApi/Generator/EmployeeGeneratorService.cs create mode 100644 ServiceApi/Generator/IEmployeeGeneratorService.cs create mode 100644 ServiceApi/Program.cs create mode 100644 ServiceApi/Properties/launchSettings.json create mode 100644 ServiceApi/Service.Api.csproj create mode 100644 ServiceApi/appsettings.Development.json create mode 100644 ServiceApi/appsettings.json diff --git a/ApiGateway/ApiGateway.csproj b/ApiGateway/ApiGateway.csproj new file mode 100644 index 00000000..c8100831 --- /dev/null +++ b/ApiGateway/ApiGateway.csproj @@ -0,0 +1,25 @@ + + + + net8.0 + enable + enable + ApiGateway + ApiGateway + + + + + + + + + + + + + Always + + + + diff --git a/ApiGateway/Configuration/WeightedRoundRobinOptions.cs b/ApiGateway/Configuration/WeightedRoundRobinOptions.cs new file mode 100644 index 00000000..d5547d71 --- /dev/null +++ b/ApiGateway/Configuration/WeightedRoundRobinOptions.cs @@ -0,0 +1,16 @@ +namespace ApiGateway.Configuration; + +public sealed class WeightedRoundRobinOptions +{ + public const string SectionName = "WeightedRoundRobin"; + + public List Nodes { get; init; } = new(); +} + +public sealed class ReplicaNodeOptions +{ + public string Host { get; init; } = string.Empty; + public int Port { get; init; } + public int Weight { get; init; } = 1; + public string ReplicaId { get; init; } = string.Empty; +} diff --git a/ApiGateway/LoadBalancing/WeightedRoundRobinBalancer.cs b/ApiGateway/LoadBalancing/WeightedRoundRobinBalancer.cs new file mode 100644 index 00000000..33425e09 --- /dev/null +++ b/ApiGateway/LoadBalancing/WeightedRoundRobinBalancer.cs @@ -0,0 +1,72 @@ +using ApiGateway.Configuration; +using Microsoft.Extensions.Options; +using Ocelot.LoadBalancer.Interfaces; +using Ocelot.Responses; +using Ocelot.Values; + +namespace ApiGateway.LoadBalancing; + +public sealed class WeightedRoundRobinBalancer : ILoadBalancer +{ + private static readonly object Sync = new(); + private readonly ILogger _logger; + private readonly List _rotation; + private int _currentIndex; + + public WeightedRoundRobinBalancer( + IOptions options, + ILogger logger) + { + _logger = logger; + _rotation = BuildRotation(options.Value.Nodes); + + if (_rotation.Count == 0) + { + throw new InvalidOperationException("Не настроены узлы для Weighted Round Robin балансировки."); + } + } + + public string Type => nameof(WeightedRoundRobinBalancer); + + public Task> LeaseAsync(HttpContext context) + { + lock (Sync) + { + if (_currentIndex >= _rotation.Count) + { + _currentIndex = 0; + } + + var next = _rotation[_currentIndex++]; + + _logger.LogInformation( + "Gateway routed request to {ReplicaAddress} by {BalancerType}", + next, + Type); + + return Task.FromResult>( + new OkResponse(next)); + } + } + + public void Release(ServiceHostAndPort hostAndPort) + { + } + + private static List BuildRotation(IEnumerable nodes) + { + var rotation = new List(); + + foreach (var node in nodes.Where(static n => !string.IsNullOrWhiteSpace(n.Host) && n.Port > 0)) + { + var normalizedWeight = Math.Max(1, node.Weight); + + for (var i = 0; i < normalizedWeight; i++) + { + rotation.Add(new ServiceHostAndPort(node.Host, node.Port)); + } + } + + return rotation; + } +} \ No newline at end of file diff --git a/ApiGateway/Program.cs b/ApiGateway/Program.cs new file mode 100644 index 00000000..dda7b95a --- /dev/null +++ b/ApiGateway/Program.cs @@ -0,0 +1,43 @@ +using ApiGateway.Configuration; +using ApiGateway.LoadBalancing; +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); + +builder.Logging.ClearProviders(); +builder.Logging.AddJsonConsole(options => +{ + options.IncludeScopes = true; + options.TimestampFormat = "yyyy-MM-ddTHH:mm:ss.fffZ "; +}); + +builder.Services.Configure( + builder.Configuration.GetSection(WeightedRoundRobinOptions.SectionName)); + +builder.Services.AddOcelot(builder.Configuration) + .AddCustomLoadBalancer(sp => + new WeightedRoundRobinBalancer( + sp.GetRequiredService>(), + sp.GetRequiredService>())); + +builder.Services.AddCors(options => options.AddDefaultPolicy(policy => +{ + policy.WithOrigins(["http://localhost:5127", "https://localhost:7282"]); + policy.WithMethods("GET"); + policy.WithHeaders("Content-Type"); + policy.WithExposedHeaders("X-Service-Replica", "X-Service-Weight"); +})); + +var app = builder.Build(); + +app.UseCors(); +app.MapDefaultEndpoints(); + +await app.UseOcelot(); + +app.Run(); diff --git a/ApiGateway/Properties/launchSettings.json b/ApiGateway/Properties/launchSettings.json new file mode 100644 index 00000000..98385b13 --- /dev/null +++ b/ApiGateway/Properties/launchSettings.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:7200", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/ApiGateway/appsettings.Development.json b/ApiGateway/appsettings.Development.json new file mode 100644 index 00000000..79cac825 --- /dev/null +++ b/ApiGateway/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Ocelot": "Information" + } + } +} diff --git a/ApiGateway/appsettings.json b/ApiGateway/appsettings.json new file mode 100644 index 00000000..cc426f6c --- /dev/null +++ b/ApiGateway/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Ocelot": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/ApiGateway/ocelot.json b/ApiGateway/ocelot.json new file mode 100644 index 00000000..87b30882 --- /dev/null +++ b/ApiGateway/ocelot.json @@ -0,0 +1,32 @@ +{ + "Routes": [ + { + "UpstreamPathTemplate": "/employee", + "UpstreamHttpMethod": [ "GET" ], + "DownstreamPathTemplate": "/employee", + "DownstreamScheme": "https", + "LoadBalancerOptions": { + "Type": "WeightedRoundRobinBalancer" + }, + "DownstreamHostAndPorts": [ + { "Host": "localhost", "Port": 15000 }, + { "Host": "localhost", "Port": 15001 }, + { "Host": "localhost", "Port": 15002 }, + { "Host": "localhost", "Port": 15003 }, + { "Host": "localhost", "Port": 15004 } + ] + } + ], + "GlobalConfiguration": { + "BaseUrl": "http://localhost:7200" + }, + "WeightedRoundRobin": { + "Nodes": [ + { "ReplicaId": "R1", "Host": "localhost", "Port": 15000, "Weight": 1 }, + { "ReplicaId": "R2", "Host": "localhost", "Port": 15001, "Weight": 2 }, + { "ReplicaId": "R3", "Host": "localhost", "Port": 15002, "Weight": 3 }, + { "ReplicaId": "R4", "Host": "localhost", "Port": 15003, "Weight": 2 }, + { "ReplicaId": "R5", "Host": "localhost", "Port": 15004, "Weight": 1 } + ] + } +} diff --git a/AspireApp/AspireApp.AppHost/AppHost.cs b/AspireApp/AspireApp.AppHost/AppHost.cs new file mode 100644 index 00000000..23814fdd --- /dev/null +++ b/AspireApp/AspireApp.AppHost/AppHost.cs @@ -0,0 +1,25 @@ +var builder = DistributedApplication.CreateBuilder(args); + +var cache = builder.AddRedis("employee-cache") + .WithRedisInsight(containerName: "employee-insight"); + +var gateway = builder.AddProject("api-gateway"); + +var replicaWeights = new[] { 1, 2, 3, 2, 1 }; + +for (var i = 0; i < 5; i++) +{ + var service = builder.AddProject($"service-api-{i}", launchProfileName: null) + .WithHttpsEndpoint(port: 15000 + i) + .WithReference(cache, "RedisCache") + .WithEnvironment("ReplicaId", "R" + (i + 1)) + .WithEnvironment("ReplicaWeight", replicaWeights[i].ToString()) + .WaitFor(cache); + + gateway.WaitFor(service); +} + +builder.AddProject("employee") + .WaitFor(gateway); + +builder.Build().Run(); \ No newline at end of file diff --git a/AspireApp/AspireApp.AppHost/AspireApp.AppHost.csproj b/AspireApp/AspireApp.AppHost/AspireApp.AppHost.csproj new file mode 100644 index 00000000..3428abc4 --- /dev/null +++ b/AspireApp/AspireApp.AppHost/AspireApp.AppHost.csproj @@ -0,0 +1,24 @@ + + + + + + Exe + net8.0 + enable + enable + true + + + + + + + + + + + + + + diff --git a/AspireApp/AspireApp.AppHost/Properties/launchSettings.json b/AspireApp/AspireApp.AppHost/Properties/launchSettings.json new file mode 100644 index 00000000..df45cce3 --- /dev/null +++ b/AspireApp/AspireApp.AppHost/Properties/launchSettings.json @@ -0,0 +1,32 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:17096;http://localhost:15155", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21139", + "DOTNET_DASHBOARD_OTLP_HTTP_ENDPOINT_URL": "https://localhost:21140", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22017" + } + }, + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:15155", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "ASPIRE_ALLOW_UNSECURED_TRANSPORT": "true", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19197", + "DOTNET_DASHBOARD_OTLP_HTTP_ENDPOINT_URL": "http://localhost:19198", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20116" + } + } + } +} diff --git a/AspireApp/AspireApp.AppHost/appsettings.Development.json b/AspireApp/AspireApp.AppHost/appsettings.Development.json new file mode 100644 index 00000000..167eb683 --- /dev/null +++ b/AspireApp/AspireApp.AppHost/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Aspire.Hosting": "Information" + } + } +} diff --git a/AspireApp/AspireApp.AppHost/appsettings.json b/AspireApp/AspireApp.AppHost/appsettings.json new file mode 100644 index 00000000..167eb683 --- /dev/null +++ b/AspireApp/AspireApp.AppHost/appsettings.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Aspire.Hosting": "Information" + } + } +} diff --git a/AspireApp/AspireApp.ServiceDefaults/AspireApp.ServiceDefaults.csproj b/AspireApp/AspireApp.ServiceDefaults/AspireApp.ServiceDefaults.csproj new file mode 100644 index 00000000..530518fa --- /dev/null +++ b/AspireApp/AspireApp.ServiceDefaults/AspireApp.ServiceDefaults.csproj @@ -0,0 +1,21 @@ + + + + net8.0 + enable + enable + true + + + + + + + + + + + + + + diff --git a/AspireApp/AspireApp.ServiceDefaults/Extensions.cs b/AspireApp/AspireApp.ServiceDefaults/Extensions.cs new file mode 100644 index 00000000..e8399ba7 --- /dev/null +++ b/AspireApp/AspireApp.ServiceDefaults/Extensions.cs @@ -0,0 +1,101 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.ServiceDiscovery; +using OpenTelemetry.Logs; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; + +namespace Microsoft.Extensions.Hosting; + +public static class Extensions +{ + public static TBuilder AddServiceDefaults(this TBuilder builder) + where TBuilder : IHostApplicationBuilder + { + builder.ConfigureOpenTelemetry(); + builder.AddDefaultHealthChecks(); + + builder.Services.AddServiceDiscovery(); + + builder.Services.ConfigureHttpClientDefaults(http => + { + http.AddStandardResilienceHandler(); + http.AddServiceDiscovery(); + }); + + return builder; + } + + public static TBuilder ConfigureOpenTelemetry(this TBuilder builder) + where TBuilder : IHostApplicationBuilder + { + builder.Logging.AddOpenTelemetry(logging => + { + logging.IncludeFormattedMessage = true; + logging.IncludeScopes = true; + }); + + builder.Services.AddOpenTelemetry() + .WithMetrics(metrics => + { + metrics.AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation(); + }) + .WithTracing(tracing => + { + tracing.AddSource(builder.Environment.ApplicationName) + .AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation(); + }); + + builder.AddOpenTelemetryExporters(); + + return builder; + } + + private static TBuilder AddOpenTelemetryExporters(this TBuilder builder) + where TBuilder : IHostApplicationBuilder + { + var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); + + if (!useOtlpExporter) + { + return builder; + } + + builder.Logging.AddOpenTelemetry(logging => logging.AddOtlpExporter()); + + builder.Services.AddOpenTelemetry() + .WithMetrics(metrics => metrics.AddOtlpExporter()) + .WithTracing(tracing => tracing.AddOtlpExporter()); + + return builder; + } + + public static TBuilder AddDefaultHealthChecks(this TBuilder builder) + where TBuilder : IHostApplicationBuilder + { + builder.Services.AddHealthChecks() + .AddCheck("self", () => HealthCheckResult.Healthy(), tags: ["live"]); + + return builder; + } + + public static WebApplication MapDefaultEndpoints(this WebApplication app) + { + if (app.Environment.IsDevelopment()) + { + app.MapHealthChecks("/health"); + app.MapHealthChecks("/alive", new HealthCheckOptions + { + Predicate = registration => registration.Tags.Contains("live") + }); + } + + return app; + } +} diff --git a/Client.Wasm/Components/DataCard.razor b/Client.Wasm/Components/DataCard.razor index c646a839..68c080d9 100644 --- a/Client.Wasm/Components/DataCard.razor +++ b/Client.Wasm/Components/DataCard.razor @@ -1,22 +1,34 @@ -@inject IConfiguration Configuration +@using System.Net.Http.Json +@using System.Text.Json.Nodes +@inject IConfiguration Configuration @inject HttpClient Client - Характеристики текущего объекта + Характеристики текущего объекта - + @if (!string.IsNullOrWhiteSpace(ErrorMessage)) + { + @ErrorMessage + } + + @if (!string.IsNullOrWhiteSpace(ReplicaInfo)) + { + @ReplicaInfo + } + +
- + # Характеристика Значение - + - @if(Value is null) + @if (Value is null) { 1 @@ -30,7 +42,7 @@ foreach (var property in array) { - @(Array.IndexOf(array, property)+1) + @(Array.IndexOf(array, property) + 1) @property.Key @property.Value?.ToString() @@ -40,10 +52,10 @@
- + - Запросить новый объект + Запросить новый объект @@ -51,10 +63,10 @@ Идентификатор нового объекта: - + - + @@ -63,12 +75,51 @@ @code { private JsonObject? Value { get; set; } - private int Id { get; set; } + private string? ErrorMessage { get; set; } + private string? ReplicaInfo { get; set; } + private int Id { get; set; } = 1; private async Task RequestNewData() { - var baseAddress = Configuration["BaseAddress"] ?? throw new KeyNotFoundException("Конфигурация клиента не содержит параметра BaseAddress"); - Value = await Client.GetFromJsonAsync($"{baseAddress}?id={Id}", new JsonSerializerOptions { }); - StateHasChanged(); + ErrorMessage = null; + ReplicaInfo = null; + + if (Id <= 0) + { + ErrorMessage = "Идентификатор должен быть больше нуля."; + return; + } + + var baseAddress = Configuration["BaseAddress"]; + if (string.IsNullOrWhiteSpace(baseAddress)) + { + ErrorMessage = "Конфигурация клиента не содержит параметра BaseAddress."; + return; + } + + try + { + using var response = await Client.GetAsync($"{baseAddress}?id={Id}"); + response.EnsureSuccessStatusCode(); + + Value = await response.Content.ReadFromJsonAsync(); + + var replica = response.Headers.TryGetValues("X-Service-Replica", out var replicaValues) + ? replicaValues.FirstOrDefault() + : null; + var weight = response.Headers.TryGetValues("X-Service-Weight", out var weightValues) + ? weightValues.FirstOrDefault() + : null; + + if (!string.IsNullOrWhiteSpace(replica)) + { + ReplicaInfo = $"Ответ пришёл от реплики {replica} (вес {weight ?? "1"}). Алгоритм: Weighted Round Robin."; + } + } + catch (Exception ex) + { + ErrorMessage = $"Не удалось получить данные: {ex.Message}"; + Value = null; + } } } diff --git a/Client.Wasm/Components/StudentCard.razor b/Client.Wasm/Components/StudentCard.razor index 661f1181..f12c48a0 100644 --- a/Client.Wasm/Components/StudentCard.razor +++ b/Client.Wasm/Components/StudentCard.razor @@ -1,13 +1,17 @@  - Лабораторная работа + + Лабораторная работа + - Номер №X "Название лабораторной" - Вариант №Х "Название варианта" - Выполнена Фамилией Именем 65ХХ - Ссылка на форк + Номер №1 "Кэширование" + Вариант №28 "Сотрудник компании" + Выполнена Миронюк Матвеем 6512 + + Ссылка на форк + diff --git a/Client.Wasm/wwwroot/appsettings.json b/Client.Wasm/wwwroot/appsettings.json index d1fe7ab3..d4f650b0 100644 --- a/Client.Wasm/wwwroot/appsettings.json +++ b/Client.Wasm/wwwroot/appsettings.json @@ -6,5 +6,5 @@ } }, "AllowedHosts": "*", - "BaseAddress": "" + "BaseAddress": "http://localhost:7200/employee" } diff --git a/ServiceApi/Entities/Employee.cs b/ServiceApi/Entities/Employee.cs new file mode 100644 index 00000000..fec349fb --- /dev/null +++ b/ServiceApi/Entities/Employee.cs @@ -0,0 +1,39 @@ +using System.Text.Json.Serialization; + +namespace Service.Api.Entities; + +/// +/// Сотрудник компании. +/// +public sealed class Employee +{ + [JsonPropertyName("id")] + public int Id { get; set; } + + [JsonPropertyName("fullName")] + public string FullName { get; set; } = string.Empty; + + [JsonPropertyName("position")] + public string Position { get; set; } = string.Empty; + + [JsonPropertyName("department")] + public string Department { get; set; } = string.Empty; + + [JsonPropertyName("hireDate")] + public DateOnly HireDate { get; set; } + + [JsonPropertyName("salary")] + public decimal Salary { get; set; } + + [JsonPropertyName("email")] + public string Email { get; set; } = string.Empty; + + [JsonPropertyName("phone")] + public string Phone { get; set; } = string.Empty; + + [JsonPropertyName("isFired")] + public bool IsFired { get; set; } + + [JsonPropertyName("fireDate")] + public DateOnly? FireDate { get; set; } +} diff --git a/ServiceApi/Generator/EmployeeGenerator.cs b/ServiceApi/Generator/EmployeeGenerator.cs new file mode 100644 index 00000000..b3e7645e --- /dev/null +++ b/ServiceApi/Generator/EmployeeGenerator.cs @@ -0,0 +1,92 @@ +using Bogus; +using Bogus.DataSets; +using Service.Api.Entities; + +namespace Service.Api.Generator; + +/// +/// Генератор случайных сотрудников компании. +/// +public static class EmployeeGenerator +{ + private static readonly string[] ProfessionCatalog = + { + "Developer", + "Manager", + "Analyst", + "Tester", + "Administrator", + "Designer" + }; + + private static readonly string[] PositionLevels = + { + "Junior", + "Middle", + "Senior" + }; + + public static Employee Generate(int id) + { + var faker = new Faker("ru"); + + var gender = faker.PickRandom(); + var firstName = faker.Name.FirstName(gender); + var lastName = faker.Name.LastName(gender); + var patronymic = BuildPatronymic(faker.Name.FirstName(Name.Gender.Male), gender); + + var level = faker.PickRandom(PositionLevels); + var profession = faker.PickRandom(ProfessionCatalog); + var hireDate = faker.Date.Past(10, DateTime.Today); + var isFired = faker.Random.Bool(0.18f); + + DateOnly? fireDate = null; + if (isFired) + { + fireDate = DateOnly.FromDateTime(faker.Date.Between(hireDate, DateTime.Today)); + } + + return new Employee + { + Id = id, + FullName = $"{lastName} {firstName} {patronymic}", + Position = $"{level} {profession}", + Department = faker.Commerce.Department(), + HireDate = DateOnly.FromDateTime(hireDate), + Salary = CalculateSalary(level, faker), + Email = faker.Internet.Email(firstName, lastName), + Phone = faker.Phone.PhoneNumber("+7(###)###-##-##"), + IsFired = isFired, + FireDate = fireDate + }; + } + + private static string BuildPatronymic(string sourceName, Name.Gender gender) + { + if (string.IsNullOrWhiteSpace(sourceName)) + { + return gender == Name.Gender.Male ? "Иванович" : "Ивановна"; + } + + return gender switch + { + Name.Gender.Male when sourceName.EndsWith('й') => $"{sourceName[..^1]}евич", + Name.Gender.Female when sourceName.EndsWith('й') => $"{sourceName[..^1]}евна", + Name.Gender.Male => $"{sourceName}ович", + _ => $"{sourceName}овна" + }; + } + + private static decimal CalculateSalary(string level, Faker faker) + { + var value = level switch + { + "Junior" => faker.Random.Decimal(60_000m, 95_000m), + "Middle" => faker.Random.Decimal(100_000m, 170_000m), + "Senior" => faker.Random.Decimal(180_000m, 280_000m), + _ => faker.Random.Decimal(80_000m, 120_000m) + }; + + return Math.Round(value, 2, MidpointRounding.AwayFromZero); + } +} diff --git a/ServiceApi/Generator/EmployeeGeneratorService.cs b/ServiceApi/Generator/EmployeeGeneratorService.cs new file mode 100644 index 00000000..83fd818d --- /dev/null +++ b/ServiceApi/Generator/EmployeeGeneratorService.cs @@ -0,0 +1,82 @@ +using System.Text.Json; +using Microsoft.Extensions.Caching.Distributed; +using Service.Api.Entities; + +namespace Service.Api.Generator; + +public sealed class EmployeeGeneratorService( + IDistributedCache cache, + ILogger logger, + IConfiguration configuration) : IEmployeeGeneratorService +{ + private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web); + + private readonly TimeSpan _cacheExpiration = + TimeSpan.FromMinutes(configuration.GetValue("CacheExpirationMinutes") ?? 30); + + public async Task ProcessEmployee(int id, CancellationToken cancellationToken = default) + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(id); + + var cacheKey = $"employee:{id}"; + + using var _ = logger.BeginScope(new Dictionary + { + ["EmployeeId"] = id, + ["CacheKey"] = cacheKey + }); + + logger.LogInformation("Employee request received"); + + try + { + var employee = await RetrieveFromCache(cacheKey, cancellationToken); + if (employee is not null) + { + logger.LogInformation("Cache hit. Returning employee from Redis"); + return employee; + } + + logger.LogInformation("Cache miss. Generating new employee"); + employee = EmployeeGenerator.Generate(id); + await PopulateCache(cacheKey, employee, cancellationToken); + + logger.LogInformation( + "Employee {EmployeeId} stored in cache for {CacheLifetimeMinutes} minutes", + id, + _cacheExpiration.TotalMinutes); + + return employee; + } + catch (Exception exception) + { + logger.LogError(exception, "Error while processing employee {EmployeeId}", id); + throw; + } + } + + private async Task RetrieveFromCache(string cacheKey, CancellationToken cancellationToken) + { + var json = await cache.GetStringAsync(cacheKey, cancellationToken); + if (string.IsNullOrWhiteSpace(json)) + { + return null; + } + + return JsonSerializer.Deserialize(json, JsonOptions); + } + + private async Task PopulateCache(string cacheKey, Employee employee, CancellationToken cancellationToken) + { + var json = JsonSerializer.Serialize(employee, JsonOptions); + + await cache.SetStringAsync( + cacheKey, + json, + new DistributedCacheEntryOptions + { + AbsoluteExpirationRelativeToNow = _cacheExpiration + }, + cancellationToken); + } +} diff --git a/ServiceApi/Generator/IEmployeeGeneratorService.cs b/ServiceApi/Generator/IEmployeeGeneratorService.cs new file mode 100644 index 00000000..c8de0288 --- /dev/null +++ b/ServiceApi/Generator/IEmployeeGeneratorService.cs @@ -0,0 +1,11 @@ +using Service.Api.Entities; + +namespace Service.Api.Generator; + +/// +/// Интерфейс обработки запросов на получение сотрудника. +/// +public interface IEmployeeGeneratorService +{ + Task ProcessEmployee(int id, CancellationToken cancellationToken = default); +} diff --git a/ServiceApi/Program.cs b/ServiceApi/Program.cs new file mode 100644 index 00000000..cc04e50a --- /dev/null +++ b/ServiceApi/Program.cs @@ -0,0 +1,78 @@ +using Service.Api.Generator; + +var builder = WebApplication.CreateBuilder(args); + +builder.AddServiceDefaults(); +builder.AddRedisDistributedCache("RedisCache"); + +builder.Logging.ClearProviders(); +builder.Logging.AddJsonConsole(options => +{ + options.IncludeScopes = true; + options.TimestampFormat = "yyyy-MM-ddTHH:mm:ss.fffZ "; +}); + +builder.Services.AddScoped(); + +builder.Services.AddCors(options => options.AddDefaultPolicy(policy => +{ + policy.SetIsOriginAllowed(origin => + Uri.TryCreate(origin, UriKind.Absolute, out var uri) + && uri.IsLoopback + && (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps)) + .AllowAnyHeader() + .WithMethods("GET") + .WithExposedHeaders("X-Service-Replica", "X-Service-Weight"); +})); + +var app = builder.Build(); + +var replicaId = app.Configuration["ReplicaId"] ?? Environment.MachineName; +var replicaWeight = app.Configuration.GetValue("ReplicaWeight") ?? 1; + +app.UseCors(); +app.Use(async (context, next) => +{ + context.Response.Headers["X-Service-Replica"] = replicaId; + context.Response.Headers["X-Service-Weight"] = replicaWeight.ToString(); + await next(); +}); + +app.MapDefaultEndpoints(); + +app.MapGet("/", () => Results.Ok(new +{ + service = "Service.Api", + replica = replicaId, + weight = replicaWeight, + description = "Сервис генерации сотрудников компании", + endpoints = new[] { "/employee?id=1", "/employee/1" } +})); + +app.MapGet("/employee", async (IEmployeeGeneratorService service, ILoggerFactory loggerFactory, int id, CancellationToken cancellationToken) => +{ + var logger = loggerFactory.CreateLogger("ServiceApiEndpoints"); + logger.LogInformation("Replica {ReplicaId} received request for employee {EmployeeId}", replicaId, id); + + if (id <= 0) + { + return Results.BadRequest(new { message = "Идентификатор сотрудника должен быть больше нуля." }); + } + + return Results.Ok(await service.ProcessEmployee(id, cancellationToken)); +}); + +app.MapGet("/employee/{id:int}", async (IEmployeeGeneratorService service, ILoggerFactory loggerFactory, int id, CancellationToken cancellationToken) => +{ + var logger = loggerFactory.CreateLogger("ServiceApiEndpoints"); + logger.LogInformation("Replica {ReplicaId} received request for employee {EmployeeId}", replicaId, id); + + if (id <= 0) + { + return Results.BadRequest(new { message = "Идентификатор сотрудника должен быть больше нуля." }); + } + + return Results.Ok(await service.ProcessEmployee(id, cancellationToken)); +}); + +app.Run(); diff --git a/ServiceApi/Properties/launchSettings.json b/ServiceApi/Properties/launchSettings.json new file mode 100644 index 00000000..0641ca27 --- /dev/null +++ b/ServiceApi/Properties/launchSettings.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:7099", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/ServiceApi/Service.Api.csproj b/ServiceApi/Service.Api.csproj new file mode 100644 index 00000000..e609ccc7 --- /dev/null +++ b/ServiceApi/Service.Api.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + enable + enable + Service.Api + Service.Api + + + + + + + + + + + + diff --git a/ServiceApi/appsettings.Development.json b/ServiceApi/appsettings.Development.json new file mode 100644 index 00000000..6e16757b --- /dev/null +++ b/ServiceApi/appsettings.Development.json @@ -0,0 +1,12 @@ +{ + "CacheExpirationMinutes": 30, + "ConnectionStrings": { + "RedisCache": "localhost:6379" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/ServiceApi/appsettings.json b/ServiceApi/appsettings.json new file mode 100644 index 00000000..5f1aabbb --- /dev/null +++ b/ServiceApi/appsettings.json @@ -0,0 +1,10 @@ +{ + "CacheExpirationMinutes": 30, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} From 7417fd51ed6e5bd4db71e3479aaed8d967475192 Mon Sep 17 00:00:00 2001 From: Matvey6M6 Date: Wed, 15 Apr 2026 00:09:06 +0400 Subject: [PATCH 2/2] Readme add MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit + обновлен readme под лабу + добавлен var.md с описанием проделанной работы + скриншот работы балансировщика --- 1.PNG | Bin 0 -> 61103 bytes Client.Wasm/Components/StudentCard.razor | 2 +- README.md | 122 +++++++---------------- var.md | 7 ++ 4 files changed, 45 insertions(+), 86 deletions(-) create mode 100644 1.PNG create mode 100644 var.md diff --git a/1.PNG b/1.PNG new file mode 100644 index 0000000000000000000000000000000000000000..60d4c9824a4db4cff7b934a22871b934e9e9526d GIT binary patch literal 61103 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 Is@3EF2N+1DX#fBK literal 0 HcmV?d00001 diff --git a/Client.Wasm/Components/StudentCard.razor b/Client.Wasm/Components/StudentCard.razor index f12c48a0..d944c11b 100644 --- a/Client.Wasm/Components/StudentCard.razor +++ b/Client.Wasm/Components/StudentCard.razor @@ -6,7 +6,7 @@ - Номер №1 "Кэширование" + Номер №2 "Балансировка нагрузки" Вариант №28 "Сотрудник компании" Выполнена Миронюк Матвеем 6512 diff --git a/README.md b/README.md index dcaa5eb7..50b98834 100644 --- a/README.md +++ b/README.md @@ -13,18 +13,8 @@ * Повторение основ работы с системами контроля версий, * Интеграционное тестирование. -### Лабораторные работы -
-1. «Кэширование» - Реализация сервиса генерации контрактов, кэширование его ответов -
- -В рамках первой лабораторной работы необходимо: -* Реализовать сервис генерации контрактов на основе Bogus, -* Реализовать кеширование при помощи IDistributedCache и Redis, -* Реализовать структурное логирование сервиса генерации, -* Настроить оркестрацию Aspire. - -
+### Лабораторная работа +
2. «Балансировка нагрузки» - Реализация апи гейтвея, настройка его работы
@@ -34,32 +24,6 @@ * Реализовать апи гейтвей на основе Ocelot, * Имплементировать алгоритм балансировки нагрузки согласно варианту. -
-
-
-3. «Интеграционное тестирование» - Реализация файлового сервиса и объектного хранилища, интеграционное тестирование бекенда -
- -В рамках третьей лабораторной работы необходимо: -* Добавить в оркестрацию объектное хранилище, -* Реализовать файловый сервис, сериализующий сгенерированные данные в файлы и сохраняющий их в объектном хранилище, -* Реализовать отправку генерируемых данных в файловый сервис посредством брокера, -* Реализовать интеграционные тесты, проверяющие корректность работы всех сервисов бекенда вместе. - -
-
-
-4. (Опционально) «Переход на облачную инфраструктуру» - Перенос бекенда в Yandex Cloud -
- -В рамках четвертой лабораторной работы необходимо перенестиервисы на облако все ранее разработанные сервисы: -* Клиент - в хостинг через отдельный бакет Object Storage, -* Сервис генерации - в Cloud Function, -* Апи гейтвей - в Serverless Integration как API Gateway, -* Брокер сообщений - в Message Queue, -* Файловый сервис - в Cloud Function, -* Объектное хранилище - в отдельный бакет Object Storage, -
@@ -71,58 +35,46 @@ * Реализация тестов с использованием [xUnit](https://xunit.net/?tabs=cs). * Создание минимальной документации к проекту: страница на GitHub с информацией о задании, скриншоты приложения и прочая информация. -**Факультативно**: -* Перенос бекенда на облачную инфраструктуру Yandex Cloud -Внимательно прочитайте [дискуссии](https://github.com/itsecd/cloud-development/discussions/1) о том, как работает автоматическое распределение на ревью. -Сразу корректно называйте свои pr, чтобы они попали на ревью нужному преподавателю. +### Вариант №28 +| Доменная область | Балансировка | Брокер | Хостинг S3 | +| --- | --- | --- | --- | +| Сотрудник компании | Weighted Round Robin | SNS | Localstack | + -По итогу работы в семестре должна получиться следующая информационная система:
-C4 диаграмма -Современные_технологии_разработки_ПО_drawio + «Сотрудник компании» +Необходимо сгенерировать информацию о сотруднике компании со следующими характеристиками: + ++----+-------------------------------------------+-------------+ +| № | Название | Тип данных | ++----+-------------------------------------------+-------------+ +| 1 | Идентификатор сотрудника в системе | int | +| 2 | ФИО | string | +| 3 | Должность | string | +| 4 | Отдел | string | +| 5 | Дата приема | DateOnly | +| 6 | Оклад | decimal | +| 7 | Электронная почта | string | +| 8 | Номер телефона | string | +| 9 | Индикатор увольнения | bool | +| 10 | Дата увольнения | DateOnly? | ++----+-------------------------------------------+-------------+ + +Комментарии к характеристикам: +ФИО - конкатенация фамилии, имени и отчества через пробел, все из раздела Name +Должность выбирается из двух заранее подготовленных справочников - справочника профессий (“Developer”, “Manager”, “Analyst” и т.д.) и суффиксов к ним (“Junior”, “Middle”, “Senior” и т.д.) +Отдел берется из раздела Commerce +Дата приема берется не более, чем 10 лет назад от текущей даты +Генерация суммы оклада должна коррелировать с суффиксом должности и не иметь более 2 знаков в дробной части +Электронная почта генерируется из раздела Internet +При отсутствии индикатора увольнения дата увольнения не заполняется +Телефонный номер должен браться из раздела Phone и быть формата +7(***)***-**-**
-## Варианты заданий -Номер варианта задания присваивается в начале семестра. Изменить его нельзя. Каждый вариант имеет уникальную комбинацию из предметной области, базы данных и технологии для общения сервиса генерации данных и сервера апи. - -[Список вариантов](https://docs.google.com/document/d/1WGmLYwffTTaAj4TgFCk5bUyW3XKbFMiBm-DHZrfFWr4/edit?usp=sharing) -[Список предметных областей и алгоритмов балансировки](https://docs.google.com/document/d/1PLn2lKe4swIdJDZhwBYzxqFSu0AbY2MFY1SUPkIKOM4/edit?usp=sharing) - -## Схема сдачи - -На каждую из лабораторных работ необходимо сделать отдельный [Pull Request (PR)](https://docs.github.com/en/pull-requests). - -Общая схема: -1. Сделать форк данного репозитория -2. Выполнить задание -3. Сделать PR в данный репозиторий -4. Исправить замечания после code review -5. Получить approve - -## Критерии оценивания - -Конкурентный принцип. -Так как задания в первой лабораторной будут повторяться между студентами, то выделяются следующие показатели для оценки: -1. Скорость разработки -2. Качество разработки -3. Полнота выполнения задания - -Быстрее делаете PR - у вас преимущество. -Быстрее получаете Approve - у вас преимущество. -Выполните нечто немного выходящее за рамки проекта - у вас преимущество. -Не укладываетесь в дедлайн - получаете минимально возможный балл. - -### Шкала оценивания +### Скриншоты -- **3 балла** за качество кода, из них: - - 2 балла - базовая оценка - - 1 балл (но не более) можно получить за выполнение любого из следующих пунктов: - - Реализация факультативного функционала - - Выполнение работы раньше других: первые 5 человек из каждой группы, которые сделали PR и получили approve, получают дополнительный балл - -## Вопросы и обратная связь по курсу -Чтобы задать вопрос по лабораторной, воспользуйтесь [соответствующим разделом дискуссий](https://github.com/itsecd/cloud-development/discussions/categories/questions) или заведите [ишью](https://github.com/itsecd/cloud-development/issues/new). -Если у вас появились идеи/пожелания/прочие полезные мысли по преподаваемой дисциплине, их можно оставить [здесь](https://github.com/itsecd/cloud-development/discussions/categories/ideas). +##### Логи подтверждающие работу балансировки +![alt text](1.PNG) diff --git a/var.md b/var.md new file mode 100644 index 00000000..6c6c5662 --- /dev/null +++ b/var.md @@ -0,0 +1,7 @@ +# Выполнено +- настроен запуск нескольких реплик сервиса через .NET Aspire +- добавлен API Gateway на основе Ocelot +- реализован алгоритм Weighted Round Robin +- настроено распределение запросов между репликами по весам +- обновлена маршрутизация клиентского приложения через gateway +- выполнена проверка корректности балансировки через логи и трейсы \ No newline at end of file