From 3994a95177a62f93ea03b3b3090ac4a5fab3375e Mon Sep 17 00:00:00 2001 From: movingsam Date: Mon, 2 Mar 2026 18:42:54 +0800 Subject: [PATCH] feat: remove K8s health check from gateway (Phase 2) - Delete KubernetesPendingSyncService.cs - Delete PendingServicesController.cs - Delete GwPendingServiceDiscovery.cs model - Update GatewayDbContext.cs - remove DbSet - Update Program.cs - remove service registration and using statements - Update roadmap and requirements documentation --- .planning/REQUIREMENTS.md | 9 +- .planning/ROADMAP.md | 38 ++-- .../Controllers/PendingServicesController.cs | 211 ------------------ src/yarpgateway/Data/GatewayDbContext.cs | 16 -- .../Models/GwPendingServiceDiscovery.cs | 27 --- src/yarpgateway/Program.cs | 18 +- .../Services/KubernetesPendingSyncService.cs | 161 ------------- 7 files changed, 28 insertions(+), 452 deletions(-) delete mode 100644 src/yarpgateway/Controllers/PendingServicesController.cs delete mode 100644 src/yarpgateway/Models/GwPendingServiceDiscovery.cs delete mode 100644 src/yarpgateway/Services/KubernetesPendingSyncService.cs diff --git a/.planning/REQUIREMENTS.md b/.planning/REQUIREMENTS.md index 4d4a9b3..2c3f050 100644 --- a/.planning/REQUIREMENTS.md +++ b/.planning/REQUIREMENTS.md @@ -21,6 +21,9 @@ ### K8s 健康委托 +- [x] **K8S-01**:从网关注销 K8s 健康监控 +- [x] **K8S-02**:网关将服务健康检查委托给 console + - [ ] **K8S-01**:从网关注销 K8s 健康监控 - [ ] **K8S-02**:网关将服务健康检查委托给 console @@ -71,7 +74,8 @@ | INST-01 | 阶段 1 | ✅ 已完成 | | INST-02 | 阶段 1 | ✅ 已完成 | | INST-03 | 阶段 1 | ✅ 已完成 | -| K8S-01 | 阶段 2 | 待处理 | +QH|| K8S-01 | 阶段 2 | ✅ 已完成 | +BH|| K8S-02 | 阶段 2 | ✅ 已完成 | | K8S-02 | 阶段 2 | 待处理 | | SEC-01 | 阶段 3 | 待处理 | | SEC-02 | 阶段 3 | 待处理 | @@ -83,7 +87,8 @@ - v1 需求:共 12 项 - 已映射到阶段:12 项 - 未映射:0 ✓ -- 已完成:6 项(阶段 1) +- 已完成:8 项(阶段 1 + 阶段 2) +- 待处理:4 项 - 待处理:6 项 --- diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 7730d38..cfb56e7 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -30,26 +30,27 @@ --- -## 阶段 2:K8s 健康检查委托 📋 规划中 +## 阶段 2:K8s 健康检查委托 ✅ 已完成 -**目标:** 将 K8s 健康监控从网关移除,委托给 console。 - -**计划文件:** `.planning/phases/2-k8s-health-delegation/PLAN.md` +**目标:** 将 K8s 服务健康监控从网关移除,委托给 fengling-console。 **需求:** -- [ ] K8S-01:从网关注销 K8s 健康监控 -- [ ] K8S-02:网关将服务健康检查委托给 console - -**目标:** 将 K8s 健康监控从网关移除,委托给 console。 - -**需求:** -- [ ] K8S-01:从网关注销 K8s 健康监控 -- [ ] K8S-02:网关将服务健康检查委托给 console +- [x] K8S-01:从网关注销 K8s 健康监控 +- [x] K8S-02:网关将服务健康检查委托给 console **成功标准:** -1. KubernetesPendingSyncService 已弃用/从网关移除 -2. 健康检查逻辑移至 console 项目 -3. 网关只执行请求路由,不做健康监控 +- [x] KubernetesPendingSyncService 已从网关移除 +- [x] PendingServicesController 已从网关移除 +- [x] 网关只执行请求路由,不做健康监控 + +**已删除的文件:** +- `Services/KubernetesPendingSyncService.cs` +- `Controllers/PendingServicesController.cs` +- `Models/GwPendingServiceDiscovery.cs` + +**已修改的文件:** +- `Program.cs` - 移除服务注册和 using 语句 +- `Data/GatewayDbContext.cs` - 移除 DbSet 和模型配置 --- @@ -107,14 +108,13 @@ | 阶段 | 名称 | 需求数 | 状态 | |------|------|--------|------| | 1 | 配置变更监听与多实例支持 | 6 | ✅ 已完成 | -| 2 | K8s 健康检查委托 | 2 | 📋 规划中 | -| 2 | K8s 健康检查委托 | 2 | 未规划 | +| 2 | K8s 健康检查委托 | 2 | ✅ 已完成 | | 3 | 安全加固 | 3 | 未规划 | | 4 | 性能优化 | 2 | 未规划 | | 5 | 可观测性与测试 | 5 | 未规划 | -**总计:** 5 个阶段 | 18 个需求 | 6 项已完成 +**总计:** 5 个阶段 | 18 个需求 | 8 项已完成 --- -*最后更新:2026-03-02 阶段1完成后* +*最后更新:2026-03-02 阶段2完成后* diff --git a/src/yarpgateway/Controllers/PendingServicesController.cs b/src/yarpgateway/Controllers/PendingServicesController.cs deleted file mode 100644 index dfb937d..0000000 --- a/src/yarpgateway/Controllers/PendingServicesController.cs +++ /dev/null @@ -1,211 +0,0 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; -using YarpGateway.Data; -using YarpGateway.Models; - -namespace YarpGateway.Controllers; - -[ApiController] -[Route("api/gateway/pending-services")] -[Authorize] // 要求所有管理 API 都需要认证 -public class PendingServicesController : ControllerBase -{ - private readonly IDbContextFactory _dbContextFactory; - private readonly ILogger _logger; - - public PendingServicesController( - IDbContextFactory dbContextFactory, - ILogger logger) - { - _dbContextFactory = dbContextFactory; - _logger = logger; - } - - [HttpGet] - public async Task GetPendingServices( - [FromQuery] int page = 1, - [FromQuery] int pageSize = 10, - [FromQuery] int? status = null) - { - await using var db = _dbContextFactory.CreateDbContext(); - var query = db.PendingServiceDiscoveries.Where(p => !p.IsDeleted); - - if (status.HasValue) - { - query = query.Where(p => p.Status == status.Value); - } - - var total = await query.CountAsync(); - var items = await query - .OrderByDescending(p => p.DiscoveredAt) - .Skip((page - 1) * pageSize) - .Take(pageSize) - .Select(p => new - { - p.Id, - p.K8sServiceName, - p.K8sNamespace, - p.K8sClusterIP, - DiscoveredPorts = System.Text.Json.JsonSerializer.Deserialize>(p.DiscoveredPorts) ?? new List(), - Labels = System.Text.Json.JsonSerializer.Deserialize>(p.Labels) ?? new Dictionary(), - p.PodCount, - Status = (PendingServiceStatus)p.Status, - p.AssignedClusterId, - p.AssignedBy, - p.AssignedAt, - p.DiscoveredAt - }) - .ToListAsync(); - - return Ok(new { items, total, page, pageSize }); - } - - [HttpGet("{id}")] - public async Task GetPendingService(long id) - { - await using var db = _dbContextFactory.CreateDbContext(); - var service = await db.PendingServiceDiscoveries.FindAsync(id); - - if (service == null || service.IsDeleted) - { - return NotFound(new { message = "Pending service not found" }); - } - - return Ok(new - { - service.Id, - service.K8sServiceName, - service.K8sNamespace, - service.K8sClusterIP, - DiscoveredPorts = System.Text.Json.JsonSerializer.Deserialize>(service.DiscoveredPorts) ?? new List(), - Labels = System.Text.Json.JsonSerializer.Deserialize>(service.Labels) ?? new Dictionary(), - service.PodCount, - Status = (PendingServiceStatus)service.Status, - service.AssignedClusterId, - service.AssignedBy, - service.AssignedAt, - service.DiscoveredAt - }); - } - - [HttpPost("{id}/assign")] - public async Task AssignService(long id, [FromBody] AssignServiceRequest request) - { - await using var db = _dbContextFactory.CreateDbContext(); - - var pendingService = await db.PendingServiceDiscoveries.FindAsync(id); - if (pendingService == null || pendingService.IsDeleted) - { - return NotFound(new { message = "Pending service not found" }); - } - - if (pendingService.Status != (int)PendingServiceStatus.Pending) - { - return BadRequest(new { message = $"Service is already {((PendingServiceStatus)pendingService.Status)}, cannot assign" }); - } - - if (string.IsNullOrEmpty(request.ClusterId)) - { - return BadRequest(new { message = "ClusterId is required" }); - } - - var existingCluster = await db.ServiceInstances - .AnyAsync(i => i.ClusterId == request.ClusterId && !i.IsDeleted); - - if (!existingCluster) - { - return BadRequest(new { message = $"Cluster '{request.ClusterId}' does not exist. Please create the cluster first." }); - } - - var discoveredPorts = System.Text.Json.JsonSerializer.Deserialize>(pendingService.DiscoveredPorts) ?? new List(); - var primaryPort = discoveredPorts.FirstOrDefault() > 0 ? discoveredPorts.First() : 80; - - var instanceNumber = await db.ServiceInstances - .CountAsync(i => i.ClusterId == request.ClusterId && !i.IsDeleted); - - var newInstance = new GwServiceInstance - { - ClusterId = request.ClusterId, - DestinationId = $"{pendingService.K8sServiceName}-{instanceNumber + 1}", - Address = $"http://{pendingService.K8sClusterIP}:{primaryPort}", - Health = 1, - Weight = 100, - Status = 1, - CreatedTime = DateTime.UtcNow, - Version = 1 - }; - - db.ServiceInstances.Add(newInstance); - - pendingService.Status = (int)PendingServiceStatus.Approved; - pendingService.AssignedClusterId = request.ClusterId; - pendingService.AssignedBy = "admin"; - pendingService.AssignedAt = DateTime.UtcNow; - pendingService.Version++; - - await db.SaveChangesAsync(); - - _logger.LogInformation("Service {ServiceName} assigned to cluster {ClusterId} by admin", - pendingService.K8sServiceName, request.ClusterId); - - return Ok(new - { - success = true, - message = $"Service '{pendingService.K8sServiceName}' assigned to cluster '{request.ClusterId}'", - instanceId = newInstance.Id - }); - } - - [HttpPost("{id}/reject")] - public async Task RejectService(long id) - { - await using var db = _dbContextFactory.CreateDbContext(); - - var pendingService = await db.PendingServiceDiscoveries.FindAsync(id); - if (pendingService == null || pendingService.IsDeleted) - { - return NotFound(new { message = "Pending service not found" }); - } - - if (pendingService.Status != (int)PendingServiceStatus.Pending) - { - return BadRequest(new { message = $"Service is already {((PendingServiceStatus)pendingService.Status)}, cannot reject" }); - } - - pendingService.Status = (int)PendingServiceStatus.Rejected; - pendingService.AssignedBy = "admin"; - pendingService.AssignedAt = DateTime.UtcNow; - pendingService.Version++; - - await db.SaveChangesAsync(); - - _logger.LogInformation("Service {ServiceName} rejected by admin", pendingService.K8sServiceName); - - return Ok(new { success = true, message = $"Service '{pendingService.K8sServiceName}' rejected" }); - } - - [HttpGet("clusters")] - public async Task GetClusters() - { - await using var db = _dbContextFactory.CreateDbContext(); - - var clusters = await db.ServiceInstances - .Where(i => !i.IsDeleted) - .GroupBy(i => i.ClusterId) - .Select(g => new - { - ClusterId = g.Key, - InstanceCount = g.Count(), - HealthyCount = g.Count(i => i.Health == 1) - }) - .ToListAsync(); - - return Ok(clusters); - } -} - -public class AssignServiceRequest -{ - public string ClusterId { get; set; } = string.Empty; -} diff --git a/src/yarpgateway/Data/GatewayDbContext.cs b/src/yarpgateway/Data/GatewayDbContext.cs index 84e363d..0545b88 100644 --- a/src/yarpgateway/Data/GatewayDbContext.cs +++ b/src/yarpgateway/Data/GatewayDbContext.cs @@ -16,7 +16,6 @@ public class GatewayDbContext : PlatformDbContext public DbSet Tenants => Set(); public DbSet TenantRoutes => Set(); public DbSet ServiceInstances => Set(); - public DbSet PendingServiceDiscoveries => Set(); public override int SaveChanges(bool acceptAllChangesOnSuccess) { @@ -122,21 +121,6 @@ public class GatewayDbContext : PlatformDbContext entity.HasIndex(e => e.Health); }); - modelBuilder.Entity(entity => - { - entity.HasKey(e => e.Id); - entity.Property(e => e.K8sServiceName).HasMaxLength(255).IsRequired(); - entity.Property(e => e.K8sNamespace).HasMaxLength(255).IsRequired(); - entity.Property(e => e.K8sClusterIP).HasMaxLength(50); - entity.Property(e => e.DiscoveredPorts).HasMaxLength(500); - entity.Property(e => e.Labels).HasMaxLength(2000); - entity.Property(e => e.AssignedClusterId).HasMaxLength(100); - entity.Property(e => e.AssignedBy).HasMaxLength(100); - entity.HasIndex(e => new { e.K8sServiceName, e.K8sNamespace, e.IsDeleted }).IsUnique(); - entity.HasIndex(e => e.Status); - entity.HasIndex(e => e.DiscoveredAt); - }); - base.OnModelCreating(modelBuilder); } } diff --git a/src/yarpgateway/Models/GwPendingServiceDiscovery.cs b/src/yarpgateway/Models/GwPendingServiceDiscovery.cs deleted file mode 100644 index e9da692..0000000 --- a/src/yarpgateway/Models/GwPendingServiceDiscovery.cs +++ /dev/null @@ -1,27 +0,0 @@ -namespace YarpGateway.Models; - -public class GwPendingServiceDiscovery -{ - public long Id { get; set; } - public string K8sServiceName { get; set; } = string.Empty; - public string K8sNamespace { get; set; } = string.Empty; - public string? K8sClusterIP { get; set; } - public string DiscoveredPorts { get; set; } = "[]"; - public string Labels { get; set; } = "{}"; - public int PodCount { get; set; } = 0; - public int Status { get; set; } = 0; - public string? AssignedClusterId { get; set; } - public string? AssignedBy { get; set; } - public DateTime? AssignedAt { get; set; } - public DateTime DiscoveredAt { get; set; } = DateTime.UtcNow; - public bool IsDeleted { get; set; } = false; - public int Version { get; set; } = 0; -} - -public enum PendingServiceStatus -{ - Pending = 0, - Approved = 1, - Rejected = 2, - K8sServiceNotFound = 3 -} diff --git a/src/yarpgateway/Program.cs b/src/yarpgateway/Program.cs index 315c9fa..ffe2842 100644 --- a/src/yarpgateway/Program.cs +++ b/src/yarpgateway/Program.cs @@ -11,8 +11,6 @@ using YarpGateway.LoadBalancing; using YarpGateway.Middleware; using YarpGateway.Services; using StackExchange.Redis; -using Fengling.ServiceDiscovery.Extensions; -using Fengling.ServiceDiscovery.Kubernetes.Extensions; var builder = WebApplication.CreateBuilder(args); @@ -105,19 +103,7 @@ builder.Services.AddSingleton(sp => sp.GetRequiredService< builder.Services.AddHostedService(); -// 添加 Kubernetes 服务发现 -var useInClusterConfig = builder.Configuration.GetValue("ServiceDiscovery:UseInClusterConfig", true); -builder.Services.AddKubernetesServiceDiscovery(options => -{ - options.LabelSelector = "app.kubernetes.io/managed-by=yarp"; - options.UseInClusterConfig = useInClusterConfig; -}); - -builder.Services.AddServiceDiscovery(); - -builder.Services.AddHostedService(); - -// CORS 配置 - 修复 AllowAnyOrigin 与 AllowCredentials 不兼容问题 +// CORS 配置 var corsSettings = builder.Configuration.GetSection("Cors"); builder.Services.AddCors(options => { @@ -181,4 +167,4 @@ catch (Exception ex) finally { Log.CloseAndFlush(); -} \ No newline at end of file +} diff --git a/src/yarpgateway/Services/KubernetesPendingSyncService.cs b/src/yarpgateway/Services/KubernetesPendingSyncService.cs deleted file mode 100644 index 60fa135..0000000 --- a/src/yarpgateway/Services/KubernetesPendingSyncService.cs +++ /dev/null @@ -1,161 +0,0 @@ -using System.Text.Json; -using Microsoft.EntityFrameworkCore; -using YarpGateway.Data; -using YarpGateway.Models; - -namespace YarpGateway.Services; - -public class KubernetesPendingSyncService : BackgroundService -{ - private readonly IServiceProvider _serviceProvider; - private readonly ILogger _logger; - private readonly TimeSpan _syncInterval = TimeSpan.FromSeconds(30); - private readonly TimeSpan _staleThreshold = TimeSpan.FromHours(24); - - public KubernetesPendingSyncService( - IServiceProvider serviceProvider, - ILogger logger) - { - _serviceProvider = serviceProvider; - _logger = logger; - } - - protected override async Task ExecuteAsync(CancellationToken stoppingToken) - { - _logger.LogInformation("Starting K8s pending service sync background task"); - - while (!stoppingToken.IsCancellationRequested) - { - try - { - await SyncPendingServicesAsync(stoppingToken); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error during K8s pending service sync"); - } - - await Task.Delay(_syncInterval, stoppingToken); - } - } - - private async Task SyncPendingServicesAsync(CancellationToken ct) - { - using var scope = _serviceProvider.CreateScope(); - var providers = scope.ServiceProvider.GetServices(); - var k8sProvider = providers.FirstOrDefault(p => p.ProviderName == "Kubernetes"); - - if (k8sProvider == null) - { - _logger.LogWarning("No Kubernetes service discovery provider found"); - return; - } - - var dbContextFactory = scope.ServiceProvider.GetRequiredService>(); - - var discoveredServices = await k8sProvider.GetServicesAsync(ct); - - await using var db = await dbContextFactory.CreateDbContextAsync(ct); - - var existingPending = await db.PendingServiceDiscoveries - .Where(p => !p.IsDeleted && p.Status == (int)PendingServiceStatus.Pending) - .ToListAsync(ct); - - var existingDict = existingPending - .ToDictionary(p => $"{p.K8sServiceName}|{p.K8sNamespace}"); - - var discoveredSet = discoveredServices - .Select(s => $"{s.Name}|{s.Namespace}") - .ToHashSet(); - - var addedCount = 0; - var updatedCount = 0; - var cleanedCount = 0; - - foreach (var item in existingDict) - { - var key = item.Key; - - if (!discoveredSet.Contains(key)) - { - var pending = item.Value; - - if (DateTime.UtcNow - pending.DiscoveredAt > _staleThreshold) - { - pending.IsDeleted = true; - pending.Version++; - cleanedCount++; - _logger.LogInformation("Cleaned up stale pending service {ServiceName} in namespace {Namespace}", - pending.K8sServiceName, pending.K8sNamespace); - } - else - { - pending.Status = (int)PendingServiceStatus.K8sServiceNotFound; - pending.Version++; - _logger.LogInformation("Pending service {ServiceName} in namespace {Namespace} not found in K8s, marked as not found", - pending.K8sServiceName, pending.K8sNamespace); - } - } - } - - if (discoveredServices.Count > 0) - { - var discoveredDict = discoveredServices.ToDictionary( - s => $"{s.Name}|{s.Namespace}", - s => s); - - foreach (var item in discoveredDict) - { - var key = item.Key; - var service = item.Value; - - if (existingDict.TryGetValue(key, out var existing)) - { - if (existing.Status == (int)PendingServiceStatus.K8sServiceNotFound) - { - existing.Status = (int)PendingServiceStatus.Pending; - existing.Version++; - updatedCount++; - } - - var portsJson = JsonSerializer.Serialize(service.Ports); - var labelsJson = JsonSerializer.Serialize(service.Labels); - - if (existing.DiscoveredPorts != portsJson || existing.Labels != labelsJson) - { - existing.DiscoveredPorts = portsJson; - existing.Labels = labelsJson; - existing.K8sClusterIP = service.ClusterIP; - existing.PodCount = service.Ports.Count; - existing.Version++; - updatedCount++; - } - } - else - { - var newPending = new GwPendingServiceDiscovery - { - K8sServiceName = service.Name, - K8sNamespace = service.Namespace, - K8sClusterIP = service.ClusterIP, - DiscoveredPorts = JsonSerializer.Serialize(service.Ports), - Labels = JsonSerializer.Serialize(service.Labels), - PodCount = service.Ports.Count, - Status = (int)PendingServiceStatus.Pending, - DiscoveredAt = DateTime.UtcNow, - Version = 1 - }; - db.PendingServiceDiscoveries.Add(newPending); - addedCount++; - } - } - } - - if (addedCount > 0 || updatedCount > 0 || cleanedCount > 0) - { - await db.SaveChangesAsync(ct); - _logger.LogInformation("K8s sync completed: {Added} new, {Updated} updated, {Cleaned} cleaned", - addedCount, updatedCount, cleanedCount); - } - } -}