C#.NET YARP 服务发现实战:接入 Consul 和 Kubernetes 动态发现后端服务

35 阅读12分钟

简介

前面几篇已经把 YARP 的基础代理、JWT、CORS、OpenTelemetry 都串起来了。

服务发现是网关实战里很关键的一步:

YARP 后端地址不要写死,改成从服务发现系统里动态获取。

普通配置里,后端地址通常写在 appsettings.json

"Destinations": {
  "product-1": {
    "Address": "http://localhost:5101/"
  },
  "product-2": {
    "Address": "http://localhost:5102/"
  }
}

这在本地 Demo 没问题,但到了真实环境会遇到这些情况:

  • 服务实例会扩容、缩容
  • 实例地址会变化
  • 某个实例挂了以后不能继续转发过去
  • 后端服务部署在容器、虚拟机或 Kubernetes 里
  • 网关配置不能每次都手动改 JSON

这时就需要服务发现。

典型链路变成:

后端服务启动
  |
  v
注册到 Consul / Kubernetes
  |
  v
YARP Gateway 查询健康实例
  |
  v
动态生成 Routes / Clusters
  |
  v
请求转发到当前可用实例

一句话说清楚:

YARP 负责代理转发,ConsulKubernetes 负责告诉 YARP 当前有哪些服务实例可用。

YARP 自己有没有服务发现?

YARP 本身不绑定某一个服务发现系统。

它的核心配置仍然是:

Route
Cluster
Destination

但是 YARP 提供了扩展点,可以把配置来源换掉。

最关键的是:

IProxyConfigProvider
IProxyConfig
IChangeToken

官方文档里提到,IProxyConfigProvider 负责返回当前代理配置,IProxyConfig 里包含 routes、clusters 和一个 IChangeToken。当配置变化时,触发 change token,YARP 就会重新加载配置。

所以服务发现接入 YARP 的本质是:

从 Consul / Kubernetes 查询实例
  |
  v
转换成 YARP RouteConfig / ClusterConfig / DestinationConfig
  |
  v
通知 YARP 配置已经变化

Consul 和 Kubernetes 怎么选?

两者解决的问题有重叠,但使用场景不一样。

方案更适合的场景
Consul虚拟机、物理机、混合部署、非 Kubernetes 环境
Kubernetes Service服务都跑在 Kubernetes 内部
Kubernetes EndpointSlice需要感知 Pod 级实例变化
DNS只需要服务级负载均衡,不关心具体实例

最务实的判断方式:

服务已经在 Kubernetes 里,优先使用 Kubernetes Service。
服务分散在 VM、物理机、容器混合环境里,Consul 更自然。

下面先完整做 Consul 实战,再讲 Kubernetes 的两种落地方式。

Consul Demo 目标

项目结构:

YarpDiscoveryDemo
├── Gateway
└── ProductService

目标效果:

  • 启动本地 Consul
  • 启动两个 ProductService 实例
  • 两个实例注册到 Consul
  • Gateway 从 Consul 查询健康实例
  • Gateway 动态生成 YARP 后端集群
  • 请求 /api/products 自动转发到健康实例

链路:

curl /api/products
  |
  v
Gateway
  |
  | 查询 Consul 里的 product-service 健康实例
  v
ProductService:5101 / ProductService:5102

启动 Consul

本地可以直接用 Docker 启动 Consul:

docker run --rm --name consul \
  -p 8500:8500 \
  hashicorp/consul:1.19 \
  agent -dev -client=0.0.0.0

打开 Consul UI:

http://localhost:8500

创建项目

mkdir YarpDiscoveryDemo
cd YarpDiscoveryDemo

dotnet new sln -n YarpDiscoveryDemo

dotnet new web -n Gateway
dotnet new web -n ProductService

dotnet sln add Gateway/Gateway.csproj
dotnet sln add ProductService/ProductService.csproj

dotnet add Gateway/Gateway.csproj package Yarp.ReverseProxy
dotnet add Gateway/Gateway.csproj package Consul

dotnet add ProductService/ProductService.csproj package Consul

ProductService:注册到 Consul

ProductService 启动时把自己注册到 Consul,停止时从 Consul 注销。

修改 ProductService/Program.cs

using Consul;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSingleton<IConsulClient>(_ =>
{
    return new ConsulClient(options =>
    {
        options.Address = new Uri("http://localhost:8500");
    });
});

builder.Services.AddHostedService<ConsulRegistrationService>();

var app = builder.Build();

app.MapGet("/products", (IConfiguration configuration) =>
{
    return Results.Ok(new
    {
        Service = "ProductService",
        InstanceId = configuration["Service:Id"],
        Port = configuration["Service:Port"],
        Data = new[]
        {
            new { Id = 1, Name = "Keyboard", Price = 199 },
            new { Id = 2, Name = "Mouse", Price = 99 }
        }
    });
});

app.MapGet("/health", () => Results.Ok("Healthy"));

app.Run();

public sealed class ConsulRegistrationService : IHostedService
{
    private readonly IConsulClient _consulClient;
    private readonly IConfiguration _configuration;
    private readonly ILogger<ConsulRegistrationService> _logger;
    private string? _serviceId;

    public ConsulRegistrationService(
        IConsulClient consulClient,
        IConfiguration configuration,
        ILogger<ConsulRegistrationService> logger)
    {
        _consulClient = consulClient;
        _configuration = configuration;
        _logger = logger;
    }

    public async Task StartAsync(CancellationToken cancellationToken)
    {
        var serviceName = _configuration["Service:Name"] ?? "product-service";
        var serviceId = _configuration["Service:Id"] ?? $"{serviceName}-{Guid.NewGuid():N}";
        var serviceAddress = _configuration["Service:Address"] ?? "localhost";
        var healthCheckAddress = _configuration["Service:HealthCheckAddress"] ?? serviceAddress;
        var servicePort = int.Parse(_configuration["Service:Port"] ?? "5101");

        _serviceId = serviceId;

        var registration = new AgentServiceRegistration
        {
            ID = serviceId,
            Name = serviceName,
            Address = serviceAddress,
            Port = servicePort,
            Tags = new[] { "yarp", "product" },
            Check = new AgentServiceCheck
            {
                HTTP = $"http://{healthCheckAddress}:{servicePort}/health",
                Interval = TimeSpan.FromSeconds(10),
                Timeout = TimeSpan.FromSeconds(2),
                DeregisterCriticalServiceAfter = TimeSpan.FromMinutes(1)
            }
        };

        await _consulClient.Agent.ServiceRegister(registration, cancellationToken);

        _logger.LogInformation(
            "Registered service {ServiceName}, id={ServiceId}, address={Address}:{Port}",
            serviceName,
            serviceId,
            serviceAddress,
            servicePort);
    }

    public async Task StopAsync(CancellationToken cancellationToken)
    {
        if (!string.IsNullOrWhiteSpace(_serviceId))
        {
            await _consulClient.Agent.ServiceDeregister(_serviceId, cancellationToken);
            _logger.LogInformation("Deregistered service {ServiceId}", _serviceId);
        }
    }
}

这里有一个容易踩坑的点:服务地址和健康检查地址最好分开。

var serviceAddress = _configuration["Service:Address"] ?? "localhost";
var healthCheckAddress = _configuration["Service:HealthCheckAddress"] ?? serviceAddress;

serviceAddress 会写进 Consul 的服务实例信息里,后面 Gateway 会读取它,并拼成:

http://{address}:{port}/

所以如果 Gateway 直接运行在宿主机上,serviceAddress 更适合写成:

localhost

healthCheckAddress 是 Consul 探活用的地址。

如果 Consul 运行在 Docker 容器里,而 ProductService 运行在宿主机上,Consul 容器访问 localhost 通常是不对的。因为对 Consul 容器来说,localhost 指的是 Consul 容器自己,不是宿主机上的服务。

在 macOS 和 Windows Docker Desktop 里,host.docker.internal 通常可以从容器访问宿主机。

Linux 环境要按实际网络调整,可以使用宿主机 IP,或者把服务也放进同一个 Docker 网络。

启动两个 ProductService 实例

启动第一个实例:

dotnet run --project ProductService/ProductService.csproj \
  --urls http://localhost:5101 \
  --Service:Name=product-service \
  --Service:Id=product-service-5101 \
  --Service:Address=localhost \
  --Service:HealthCheckAddress=host.docker.internal \
  --Service:Port=5101

启动第二个实例:

dotnet run --project ProductService/ProductService.csproj \
  --urls http://localhost:5102 \
  --Service:Name=product-service \
  --Service:Id=product-service-5102 \
  --Service:Address=localhost \
  --Service:HealthCheckAddress=host.docker.internal \
  --Service:Port=5102

打开 Consul UI:

http://localhost:8500

应该能看到:

product-service

并且有两个健康实例。

Gateway:动态配置提供器

网关不再从 appsettings.json 读取后端地址,而是从 Consul 查询实例,再动态生成 YARP 配置。

先写一个 DynamicProxyConfig

新建 Gateway/DynamicProxyConfig.cs

using Microsoft.Extensions.Primitives;
using Yarp.ReverseProxy.Configuration;

public sealed class DynamicProxyConfig : IProxyConfig
{
    public DynamicProxyConfig(
        IReadOnlyList<RouteConfig> routes,
        IReadOnlyList<ClusterConfig> clusters,
        IChangeToken changeToken)
    {
        Routes = routes;
        Clusters = clusters;
        ChangeToken = changeToken;
    }

    public IReadOnlyList<RouteConfig> Routes { get; }

    public IReadOnlyList<ClusterConfig> Clusters { get; }

    public IChangeToken ChangeToken { get; }
}

再写一个配置提供器。

新建 Gateway/DynamicProxyConfigProvider.cs

using Microsoft.Extensions.Primitives;
using Yarp.ReverseProxy.Configuration;

public sealed class DynamicProxyConfigProvider : IProxyConfigProvider
{
    private readonly object _lock = new();
    private CancellationTokenSource _changeTokenSource = new();
    private DynamicProxyConfig _config;

    public DynamicProxyConfigProvider()
    {
        _config = new DynamicProxyConfig(
            Array.Empty<RouteConfig>(),
            Array.Empty<ClusterConfig>(),
            new CancellationChangeToken(_changeTokenSource.Token));
    }

    public IProxyConfig GetConfig()
    {
        return _config;
    }

    public void Update(
        IReadOnlyList<RouteConfig> routes,
        IReadOnlyList<ClusterConfig> clusters)
    {
        CancellationTokenSource previousToken;

        lock (_lock)
        {
            previousToken = _changeTokenSource;
            _changeTokenSource = new CancellationTokenSource();

            _config = new DynamicProxyConfig(
                routes,
                clusters,
                new CancellationChangeToken(_changeTokenSource.Token));
        }

        previousToken.Cancel();
    }
}

这段代码的关键是:

Update 新配置
  |
  v
替换 Routes / Clusters
  |
  v
触发旧 ChangeToken
  |
  v
YARP 重新调用 GetConfig()

Gateway:从 Consul 同步实例

接着写一个后台服务,定时从 Consul 拉取健康实例,并更新 YARP 配置。

新建 Gateway/ConsulYarpConfigRefreshService.cs

using Consul;
using Yarp.ReverseProxy.Configuration;

public sealed class ConsulYarpConfigRefreshService : BackgroundService
{
    private readonly IConsulClient _consulClient;
    private readonly DynamicProxyConfigProvider _configProvider;
    private readonly ILogger<ConsulYarpConfigRefreshService> _logger;

    public ConsulYarpConfigRefreshService(
        IConsulClient consulClient,
        DynamicProxyConfigProvider configProvider,
        ILogger<ConsulYarpConfigRefreshService> logger)
    {
        _consulClient = consulClient;
        _configProvider = configProvider;
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                await RefreshAsync(stoppingToken);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Refresh YARP config from Consul failed");
            }

            await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
        }
    }

    private async Task RefreshAsync(CancellationToken cancellationToken)
    {
        var result = await _consulClient.Health.Service(
            service: "product-service",
            tag: null,
            passingOnly: true,
            ct: cancellationToken);

        var destinations = result.Response
            .Select(entry =>
            {
                var address = string.IsNullOrWhiteSpace(entry.Service.Address)
                    ? entry.Node.Address
                    : entry.Service.Address;

                return new
                {
                    Id = entry.Service.ID,
                    Config = new DestinationConfig
                    {
                        Address = $"http://{address}:{entry.Service.Port}/"
                    }
                };
            })
            .GroupBy(x => x.Id)
            .ToDictionary(x => x.Key, x => x.First().Config);

        var routes = new[]
        {
            new RouteConfig
            {
                RouteId = "product-route",
                ClusterId = "product-cluster",
                Match = new RouteMatch
                {
                    Path = "/api/products/{**catch-all}"
                },
                Transforms = new[]
                {
                    new Dictionary<string, string>
                    {
                        ["PathRemovePrefix"] = "/api"
                    }
                }
            }
        };

        var clusters = new[]
        {
            new ClusterConfig
            {
                ClusterId = "product-cluster",
                LoadBalancingPolicy = "RoundRobin",
                Destinations = destinations
            }
        };

        _configProvider.Update(routes, clusters);

        _logger.LogInformation(
            "YARP config refreshed from Consul, destination count: {Count}",
            destinations.Count);
    }
}

这段代码每 5 秒做一次:

查询 product-service 健康实例
  |
  v
生成 DestinationConfig
  |
  v
生成 ClusterConfig
  |
  v
调用 DynamicProxyConfigProvider.Update()
  |
  v
YARP 热更新配置

Gateway Program.cs

修改 Gateway/Program.cs

using Consul;
using Yarp.ReverseProxy.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSingleton<IConsulClient>(_ =>
{
    return new ConsulClient(options =>
    {
        options.Address = new Uri("http://localhost:8500");
    });
});

builder.Services.AddSingleton<DynamicProxyConfigProvider>();
builder.Services.AddSingleton<IProxyConfigProvider>(sp =>
    sp.GetRequiredService<DynamicProxyConfigProvider>());

builder.Services.AddHostedService<ConsulYarpConfigRefreshService>();

builder.Services.AddReverseProxy();

var app = builder.Build();

app.MapGet("/gateway/config", (DynamicProxyConfigProvider provider) =>
{
    var config = provider.GetConfig();

    return Results.Ok(new
    {
        Routes = config.Routes.Select(x => x.RouteId),
        Clusters = config.Clusters.Select(x => new
        {
            x.ClusterId,
            Destinations = x.Destinations?.Keys
        })
    });
});

app.MapReverseProxy();

app.Run();

这里没有:

.LoadFromConfig(...)

因为配置来源已经不是 appsettings.json,而是自定义的 IProxyConfigProvider

启动 Gateway

dotnet run --project Gateway/Gateway.csproj

查看当前网关动态配置:

curl http://localhost:5000/gateway/config

正常会看到:

{
  "routes": [
    "product-route"
  ],
  "clusters": [
    {
      "clusterId": "product-cluster",
      "destinations": [
        "product-service-5101",
        "product-service-5102"
      ]
    }
  ]
}

访问商品接口:

curl http://localhost:5000/api/products

多请求几次,会看到不同实例返回的 InstanceId

product-service-5101
product-service-5102

这说明:

  • Consul 里有两个健康实例
  • Gateway 从 Consul 拉到了实例
  • YARP 动态生成了 destination
  • RoundRobin 负载均衡已经生效

故障测试

停掉 5101 这个实例。

等待 Consul 健康检查把它标记为不健康,再请求:

curl http://localhost:5000/gateway/config
curl http://localhost:5000/api/products

配置里应该只剩:

product-service-5102

请求也会继续转发到 5102

这就是服务发现接入 YARP 后的核心效果:

后端实例变动
  |
  v
Consul 健康状态变化
  |
  v
Gateway 重新生成 YARP 配置
  |
  v
新请求只打到健康实例

为什么不用 YARP 自带健康检查?

这里容易混。

YARP 自己也有健康检查,但它解决的是:

已知后端列表里,哪些目标还能不能用。

服务发现解决的是:

后端列表本身从哪里来。

如果后端实例是固定的,可以只用 YARP 健康检查。

如果后端实例会动态变化,就需要 Consul、Kubernetes 或其他服务发现系统来提供实例列表。

两者可以一起用:

Consul 提供实例列表
YARP 对已发现实例再做被动健康判断

Kubernetes 方式一:直接转发到 Service DNS

在 Kubernetes 里,最常见、最推荐的方式不是让 YARP 直接感知每个 Pod,而是直接转发到 Kubernetes Service。

比如有一个 product-service

apiVersion: v1
kind: Service
metadata:
  name: product-service
  namespace: default
spec:
  selector:
    app: product-service
  ports:
    - name: http
      port: 80
      targetPort: 8080

YARP 直接配置:

{
  "ReverseProxy": {
    "Routes": {
      "product-route": {
        "ClusterId": "product-cluster",
        "Match": {
          "Path": "/api/products/{**catch-all}"
        },
        "Transforms": [
          {
            "PathRemovePrefix": "/api"
          }
        ]
      }
    },
    "Clusters": {
      "product-cluster": {
        "Destinations": {
          "product-service": {
            "Address": "http://product-service.default.svc.cluster.local/"
          }
        }
      }
    }
  }
}

这时动态发现由 Kubernetes Service 完成:

YARP -> Kubernetes Service -> Pod A / Pod B / Pod C

这种方式的优点:

  • 配置简单
  • 不需要 YARP 监听 Kubernetes API
  • Pod 扩缩容由 Kubernetes Service 自动处理
  • 适合绝大多数集群内服务调用

缺点是:

  • YARP 看不到每个 Pod
  • 细粒度负载均衡策略交给 Kubernetes
  • 不适合按 Pod 元数据做复杂路由

多数情况下,这已经够用。

Kubernetes 方式二:监听 EndpointSlice 动态生成 YARP 配置

如果确实需要 YARP 感知每个 Pod,例如:

  • 按 Pod 版本做灰度
  • 按标签筛选实例
  • 把不同 Pod 生成不同 destination
  • 需要 YARP 自己做负载均衡

就可以监听 Kubernetes EndpointSlice

思路和 Consul 类似:

Kubernetes API
  |
  v
查询 EndpointSlice
  |
  v
提取 Pod IP + Port
  |
  v
生成 YARP DestinationConfig
  |
  v
触发 IProxyConfigProvider 更新

需要安装 Kubernetes 客户端:

dotnet add Gateway/Gateway.csproj package KubernetesClient

代码骨架大概是:

using k8s;
using k8s.Models;
using Yarp.ReverseProxy.Configuration;

public sealed class KubernetesYarpConfigRefreshService : BackgroundService
{
    private readonly IKubernetes _kubernetes;
    private readonly DynamicProxyConfigProvider _configProvider;

    public KubernetesYarpConfigRefreshService(
        IKubernetes kubernetes,
        DynamicProxyConfigProvider configProvider)
    {
        _kubernetes = kubernetes;
        _configProvider = configProvider;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            var endpointSlices = await _kubernetes.CustomObjects
                .ListNamespacedCustomObjectAsync(
                    group: "discovery.k8s.io",
                    version: "v1",
                    namespaceParameter: "default",
                    plural: "endpointslices",
                    cancellationToken: stoppingToken);

            // 实战里建议把返回对象反序列化成 EndpointSlice 类型,
            // 再提取 addresses、ports、conditions.ready 等字段。
            // 最后生成 DestinationConfig:
            //
            // "pod-10-1-2-3" -> http://10.1.2.3:8080/

            await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
        }
    }
}

这只是骨架,不建议直接复制进生产。

生产里要继续处理:

  • RBAC 权限
  • namespace
  • label selector
  • EndpointSlice 分页
  • endpoint ready 状态
  • IPv4 / IPv6
  • service port 名称
  • watch 断线重连
  • 配置变化去重

所以在 Kubernetes 里,除非确实需要 Pod 级别控制,否则优先用 Service DNS。

Kubernetes 里 YARP 网关怎么部署?

一个常见部署结构:

Ingress / LoadBalancer
  |
  v
YARP Gateway Service
  |
  v
ProductService Service
  |
  v
ProductService Pods

Gateway 自身可以用 Deployment 部署:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: yarp-gateway
spec:
  replicas: 2
  selector:
    matchLabels:
      app: yarp-gateway
  template:
    metadata:
      labels:
        app: yarp-gateway
    spec:
      containers:
        - name: yarp-gateway
          image: example/yarp-gateway:1.0.0
          ports:
            - containerPort: 8080

对外再暴露一个 Service:

apiVersion: v1
kind: Service
metadata:
  name: yarp-gateway
spec:
  selector:
    app: yarp-gateway
  ports:
    - port: 80
      targetPort: 8080

YARP 后端地址则写 Kubernetes Service DNS:

http://product-service.default.svc.cluster.local/

Consul 和 Kubernetes 接法对比

维度ConsulKubernetes Service DNSKubernetes EndpointSlice
实例来源Consul Catalog / Health APIService DNSKubernetes API
YARP 是否感知实例
实现复杂度
健康状态Consul Health CheckKubernetes readinessEndpoint ready 条件
适合场景VM、混合部署、非 K8sK8s 内部常规服务调用Pod 级动态路由
推荐优先级非 K8s 优先K8s 优先特殊需求再用

生产环境注意点

1. 不要频繁刷新 YARP 配置

服务发现系统可能会频繁变化。

如果每次微小变化都触发 YARP 配置更新,网关会产生不必要的抖动。

建议:

  • 配置刷新加间隔
  • 对比新旧 destination,变化后再更新
  • 避免每秒全量刷新
  • watch 断线后再 fallback 到轮询

2. 空实例要有预期

Consul 或 Kubernetes 里可能一时没有健康实例。

这时 YARP 对应 cluster 没有可用 destination,通常会返回 503

生产环境要配好:

  • 告警
  • 熔断
  • 降级页
  • 灰度回滚
  • 上游重试策略

3. 注册地址要能被网关访问

服务注册进 Consul 的地址,不一定就是网关能访问的地址。

常见错误:

服务注册 localhost
网关在另一个容器里访问 localhost
结果访问到了网关容器自己

一定要确认:

Consul 里的 Address + Port

对 Gateway 来说是真正可访问的。

4. 健康检查不要只返回固定字符串

Demo 里:

app.MapGet("/health", () => Results.Ok("Healthy"));

生产环境应该检查关键依赖:

  • 数据库
  • Redis
  • MQ
  • 下游服务
  • 磁盘空间

否则服务看起来健康,实际业务请求仍然失败。

5. 服务发现不是配置中心

服务发现负责:

服务名 -> 健康实例列表

它不应该承载所有网关规则。

比如:

  • 路由前缀
  • 鉴权策略
  • 限流策略
  • CORS 策略
  • 灰度规则

这些更适合放在配置中心、数据库、GitOps 配置或 Kubernetes CRD 里。

常见问题

Consul 里服务是红的

常见原因:

  • /health 不通
  • Consul 探活地址写成了错误的 localhost
  • 服务注册地址不是 Gateway 能访问的地址
  • Consul 容器访问不到宿主机服务
  • 端口写错
  • 防火墙拦截

先在 Consul 容器里验证:

docker exec -it consul sh
wget -qO- http://host.docker.internal:5101/health

能返回 Healthy,Consul 才能探活成功。

Gateway 配置里没有 destination

先查 Consul API:

curl "http://localhost:8500/v1/health/service/product-service?passing=true"

如果返回空数组,说明 Consul 没有健康实例。

如果 Consul 有实例,但 Gateway 没有 destination,再看 Gateway 日志里的刷新结果。

为什么 Kubernetes 里不建议直接查 Pod?

因为 Kubernetes Service 已经帮忙维护了 endpoint 和负载均衡。

直接查 Pod 意味着网关要处理:

  • Pod 创建
  • Pod 删除
  • readiness 变化
  • EndpointSlice 分片
  • watch 重连
  • 网络策略

只有当业务真的需要 Pod 级别路由时,才值得做。

YARP 服务发现能不能和 JWT、CORS、OpenTelemetry 一起用?

可以。

服务发现只改变 Clusters.Destinations 的来源。

JWT、CORS、OpenTelemetry 仍然是 ASP.NET Core 管道和 YARP 路由层能力。

常见组合是:

YARP
  + JWT 认证授权
  + CORS
  + OpenTelemetry
  + Consul / Kubernetes 服务发现

总结

YARP 接服务发现的关键不是某个固定库,而是理解这个模型:

服务发现系统提供健康实例
  |
  v
转换成 YARP RouteConfig / ClusterConfig / DestinationConfig
  |
  v
IProxyConfigProvider 提供当前配置
  |
  v
IChangeToken 通知 YARP 配置变化
  |
  v
YARP 热更新路由和后端目标

在非 Kubernetes 环境里,Consul + IProxyConfigProvider 是很自然的方案。

在 Kubernetes 环境里,优先把 YARP 转发到 Service DNS,让 Kubernetes Service 处理 Pod 动态变化。只有需要 Pod 级别灰度、标签路由、实例感知时,再考虑监听 EndpointSlice 动态生成 YARP 配置。

参考资料