YARP 基于配置的反向代理设计:从 Rule 匹配到热重载的演进与实践
YARP 基于配置的反向代理设计从 Rule 匹配到热重载的演进与实践【免费下载链接】reverse-proxyA toolkit for developing high-performance HTTP reverse proxy applications.项目地址: https://gitcode.com/GitHub_Trending/re/reverse-proxy配置驱动的反向代理Config based proxy是网关类产品的核心形态运维人员通过一份配置文件就能定义路由、指定后端、调整负载均衡策略而无需改动代码。本文以仓库设计文档 docs/designs/config.md 为主线结合当前 YARP 源码 中的真实实现系统梳理配置模型的设计初衷、演进过程Route/Rule → RouteMatch、Backend 数组 → Cluster/Destinations 字典、热重载机制与代码增强配置的实践方法。读完本文你将能够读懂 YARP 的配置结构理解其热更新原理并写出可运行、可热重载的完整反向代理配置。说明docs/designs/config.md 属于归档的设计讨论文档其开头明确标注 These are archived design discussions. Information may be outdated and inaccurate.。因此本文将文档中记录的初始设计与当前仓库实现区分呈现既还原设计脉络也以源码为准给出当下可用的配置方式。设计目标为什么要支持配置化代理设计文档开篇列出了配置化代理需要满足的五项核心需求这也是理解后续所有设计取舍的出发点独立的配置来源与配置体系Config sources and systems——代理的配置应当来自宿主应用的配置系统而不是硬编码。基于 Host 和/或 Path 定义路由Define routes based on host and/or path——路由匹配要以 HTTP 语义字段为条件。每条路由可列出多个后端用于负载均衡List multiple back-ends per route for load balancing——一个逻辑目标对应一组可互换的后端实例。配置变更无需重启A restart should not be needed to pick up config changes——这是热更新需求的源头。允许在代码中增强路由配置Augment a routes configuration in code——文档以 Kestrel 的命名端点named endpoints为参照复杂场景可以配置打底 代码补充。这五条需求贯穿了后续所有的配置结构讨论也直接决定了当前 YARP 中RouteConfig/ClusterConfig/DestinationConfig三个核心配置类型的形态。配置体系Kestrel、UrlRewrite 与 ReverseProxy 的分工文档指出当时 .NET 生态里已有三个具备配置体系的组件Kestrel服务器端点、UrlRewriteURL 重写中间件和 ReverseProxy代理。设计上提出的原则是Kestrel 配置与代理/网关配置应当相邻而非合并。入站inbound监听哪个端口、绑定什么协议与出站outbound请求转发到哪个后端是两类关注点只要两者都能挂在同一个更宏观的配置体系下即可不必强行融合。这也是为什么在今天的示例配置如 samples/BasicYarpSample/appsettings.json中Urls与ReverseProxy是并列的两个顶层节点Urls: http://localhost:5000;https://localhost:5001, ReverseProxy: { Routes: { ... }, Clusters: { ... } }UrlRewrite 保持独立。文档承认它单独成文件、格式与其余配置不一致并不理想但当时决定先观察它是否成为长期阻碍。这一分工原则延续至今YARP 只负责反向代理这半边配置服务器监听端点仍由 Kestrel 体系管理。路由配置的演进从 Rule 字符串到结构化 Match第一版设计表达式式的 Rule设计文档最初给出的路由配置形如下方代码路由通过一条Rule表达式字符串来描述匹配条件并映射到ProxyRoute类型Routes: [ { RouteId: backend1/route1, BackendId: backend1, Rule: Host(localhost) Path(/{**catchall}) } ]文档随即提出了对 Rule 体系的质疑这个表达式系统过于复杂。/||这类逻辑组合是否必要如果拆成独立的 Host、Path、Header 等匹配键语义天然是全部满足才匹配要表达或||只需定义多条路由即可。同时文档设想把配置节点直接暴露给应用代码类似 Kestrel 的EndpointConfig.ConfigSection就能在代码侧补充复杂约束进一步降低表达式引擎的必要性。定型设计结构化的 Match 对象文档记录的设计更新对应当时 #24 的修改将Rule替换为结构化的MatchRoutes: [ { RouteId: backend1/route1, BackendId: backend1, Match: { Methods: [ GET, POST ], Host: localhost, Path: /{**catchall} } } ]这一独立匹配键、隐式 AND的思路在今天的代码中得到了完整落地。当前仓库中的 RouteMatch.cs 定义了结构化匹配的全部字段字段类型说明MethodsIReadOnlyListstring可选仅匹配指定的 HTTP 方法如GET、POSTHostsIReadOnlyListstring可选仅匹配指定 Host 头支持通配符与端口Unicode 域名无需使用 punycodePathstring可选路径模式支持{**catch-all}等路由通配符QueryParametersIReadOnlyListRouteQueryParameter可选要求请求包含所有这些查询参数HeadersIReadOnlyListRouteHeader可选要求请求包含所有这些请求头与设计文档的讨论完全一致多个条件之间是隐式 ANDEquals实现逐字段比较见 RouteMatch.cs需要或语义时通过多条路由实现。路由的完整配置模型RouteConfig匹配条件之上当前仓库的 RouteConfig.cs 给出了路由的完整字段其中多个字段如AuthorizationPolicy、RateLimiterPolicy、OutputCachePolicy、TimeoutPolicy、CorsPolicy是设计文档之后随着功能演进新增的字段说明RouteId全局唯一路由标识必填Match请求匹配参数见上表必填Order可选排序值数字小的路由优先ClusterId匹配后转发到的集群AuthorizationPolicy授权策略名不设则仅应用 FallbackPolicyDefault启用默认策略Anonymous关闭本路由授权RateLimiterPolicy限流策略名Disable关闭本路由限流OutputCachePolicy输出缓存策略名TimeoutPolicy/Timeout超时策略名/超时时间二者不可同时设置CorsPolicyCORS 策略名Default使用默认策略Disable拒绝 CORS 请求MaxRequestBodySize请求体大小上限字节覆盖服务器默认 30MB-1表示不限Metadata任意键值对进一步描述路由Transforms请求/响应变换参数可以看到设计文档中关于ProxyRoute.Metadata字典能否被直接暴露配置节点所补充/取代的讨论最终演变为Metadata保留为通用键值对同时由Transforms承担请求/响应改写的能力对应 Transforms 目录下的整套变换体系。后端配置的演进从 Backend/BackendEndpoint 数组到 Cluster/Destinations 字典设计讨论数组、嵌套与对象之争文档中的后端配置演进过程很有代表性。最初代理代码定义了Backend一组端点的集合 负载均衡、熔断、健康检查、亲和性等策略与BackendEndpoint单个服务实例含 id、地址、元数据两个类型配置中二者分开平铺Backends: [ { BackendId: backend1 }, { BackendId: backend2 } ], Endpoints: { backend1: [ { EndpointId: backend1/endpoint1, Address: https://localhost:10000/ } ], backend2: [ { EndpointId: backend2/endpoint1, Address: https://localhost:10001/ } ] }文档提出了两个质疑其一端点与后端在对象模型上是 1:1 归属关系为何配置里要拆成两处而不是嵌套其二既然这些条目本身已有 id 且对顺序不敏感为何用数组而不是对象字典随后给出了嵌套版与对象版两种候选形态。定型设计字典化嵌套文档记录的最终更新将布局收敛为对象字典 嵌套端点也就是今天看到的形态Backends: { backend1: { Endpoints: { backend1/endpoint1: { Address: https://localhost:10000/ } } }, backend2: { Endpoints: { backend2/endpoint1: { Address: https://localhost:10001/ } } } }当前实现Cluster 与 Destination在当前仓库中设计文档中的Backend/BackendEndpoint概念被正式命名为Cluster / Destination结构完全沿用了对象嵌套的定稿ClusterConfig.cs 对应一组等价端点及关联策略ClusterId全局唯一必填、LoadBalancingPolicy负载均衡策略、SessionAffinity会话亲和性、HealthCheck健康检查、HttpClient出站 HttpClient 配置、HttpRequest出站请求配置、Destinations端点字典、Metadata。DestinationConfig.cs 对应单个服务实例Address必填如https://127.0.0.1:123/abcd1234/、Health接收主动健康检查探测的独立端点地址、Host转发时使用的 Host 头值作为变换未指定时的回退、Metadata。一个真实的完整示例可直接参考 samples/BasicYarpSample/appsettings.json其中cluster2配置了两个外部目的地并指定LoadBalancingPolicy: PowerOfTwoChoicesReverseProxy: { Routes: { minimumroute: { ClusterId: minimumcluster, Match: { Path: {**catch-all} } }, route2: { ClusterId: cluster2, Match: { Path: /something/{*any} } } }, Clusters: { minimumcluster: { Destinations: { example.com: { Address: http://www.example.com/ } } }, cluster2: { Destinations: { first_destination: { Address: https://contoso.com }, another_destination: { Address: https://bing.com } }, LoadBalancingPolicy: PowerOfTwoChoices } } }关于Backend 策略过于庞杂负载均衡、熔断、健康检查、亲和性堆在一起的讨论文档预期把策略拆成管道中的独立可替换步骤当前实现中这些策略确实被拆分为独立组件见 LoadBalancing、Health、SessionAffinity 等目录但ClusterConfig仍保留了面向默认组件集的统一配置模型——与文档预判default 组件集的配置模型可能仍与现在很像一致。配置热重载无需重启的动态更新设计阶段的顾虑文档当时判断热重载还不是阻塞性需求但未来一定需要并提前讨论了几个关键设计点变更通知的去抖debounce文件等变更源对单个事件可能触发多次通知配置系统本身不做去重需要消费方自行去抖并过滤冗余通知。原子性与在途请求重载必须原子化不能打断已在处理的请求必要时重建部分管线、排空旧请求、清理旧管线小改动如只改一条路由应尽量避免整体重建。可选择性opt in/out重载应允许按配置源选择开启或关闭。与 Kestrel 的原子性同步难题文档记录了一则讨论——Kestrel 端点重载与路由重载在概念上应保持同步但两个系统之间没有程序化关联只能串行响应同一变更通知在途请求可能看到一半的变更。最终决定是鉴于 Kestrel 端点变更属于低频场景暂不为二者原子性投入等待客户反馈再定。当前实现从变更令牌到差异应用设计文档的Updates记录确认了两件事代理路由、后端、端点的配置热重载已经可用——编辑 appsettings.json 后系统自动重载并重新配置路由以及变更通知通常触发两次、日志会打印两次 Applying proxy configs但差异diff逻辑会阻止第二次产生无效更新。同时记录配置重载代码已从示例移入产品程序集。在今天的源码中这一机制由三个部分协作完成IProxyConfig 快照与变更令牌。IProxyConfig.cs 定义了配置快照Routes、Clusters以及用于通知快照过期的ChangeToken并带有一个RevisionId每次快照唯一由ConditionalWeakTable惰性生成。ConfigurationConfigProvider 订阅变更。ConfigurationConfigProvider.cs 在首次GetConfig()时通过ChangeToken.OnChange(_configuration.GetReloadToken, UpdateSnapshot)订阅配置重载令牌一旦 appsettings.json 在磁盘上被修改就重建快照——这正是热更新的入口。ProxyConfigManager 差异应用。ProxyConfigManager.cs 是核心协调者ReloadConfigAsync约 L221-L326仅对ChangeToken.HasChanged的提供者重新加载ApplyConfigAsyncL483-L497先校验再先更新集群、后更新路由UpdateRuntimeClusters/UpdateRuntimeRoutes/UpdateRuntimeDestinationsL612-L829通过ConcurrentDictionary对现有状态做按 id 的差异比对——只有发生变化的条目才更新并重建对应端点RouteChanged时才置空CachedEndpoint以重建未变化的条目原样保留从而天然规避了文档提到的重复通知导致重复更新问题。此外ListenForConfigChangesL428-L480使用统一的中枢CancellationTokenSource汇总各配置源的变更信号避免重叠触发对于不支持回调的变更令牌ActiveChangeCallbacks false或加载失败的源则退化为轮询每 5 分钟一次。这与文档中文件源可能多次触发、需要去抖的预判形成了对应当前实现选择用变更令牌合并 差异应用来吸收冗余通知而把显式去抖列为低优先级事项。文档关于重载原子性、不打断在途请求的诉求也体现在实现细节中UpdateEndpointsL836-L857按固定顺序执行捕获旧令牌 → 更新端点 → 签发新令牌 → 触发旧令牌保证调用方始终看到一致状态被移除的 Route/Cluster 对象不会立即销毁而是由 GC 在其仍被旧请求引用期间安全保留代码注释中明确说明了这一点。用代码增强配置命名端点模式配置适合描述静态、易读的信息但某些逻辑用代码表达更自然。文档引用了 Kestrel 的命名端点模式作为范本端点先在配置中命名再在代码里按名字引用做增强{ Kestrel: { Endpoints: { NamedEndpoint: { Url: http://*:6000 }, NamedHttpsEndpoint: { Url: https://*:6443 } } } }options.Endpoint(NamedEndpoint, opt { }) .Endpoint(NamedHttpsEndpoint, opt { opt.HttpsOptions.SslProtocols SslProtocols.Tls12; });文档判断代理代码已经拥有命名的 routes、backends、backend endpoints完全可以在其上构建类似的代码增强 API。同时它敏锐地指出一个关键矛盾可重载配置会复杂化这一模式——代码增强动作必须捕获为应用整个生命周期有效的回调而非仅启动时执行一次以便后续重载时重新运行。当前仓库延续了这一思路配置LoadFromConfig见 samples/ReverseProxy.Config.Sample/Program.cs与代码LoadFromMemory是两条平等的配置供给路径。InMemoryConfigProviderExtensions.cs 中的LoadFromMemory以RouteConfig/ClusterConfig列表构造内存配置源而 InMemoryConfigProvider.cs 提供的Update(routes, clusters)方法会通过Interlocked.Exchange原子替换配置快照再触发旧快照的SignalChange()取消其CancellationTokenSource从而让内存配置也具备与文件配置相同的热更新能力——运行时用代码更新配置后变更同样会流入ProxyConfigManager的差异应用流程。这正回应了文档增强动作需要能被重新执行的诉求变更以新快照形式反复供给代理核心始终面对一致的差异更新流程。配置模型与源码映射速查下表汇总本文涉及的设计概念与当前仓库源码的对应关系便于深入研读设计概念文档当前实现源码位置ProxyRoute / Route RuleRouteConfig RouteMatchRouteConfig.cs、RouteMatch.csBackend / BackendEndpointClusterConfig / DestinationConfigClusterConfig.cs、DestinationConfig.cs配置快照与变更通知IProxyConfig IChangeTokenIProxyConfig.cs文件配置热更新ConfigurationConfigProviderConfigurationConfigProvider.cs内存配置 运行时更新InMemoryConfigProvider.UpdateInMemoryConfigProvider.cs配置差异应用与端点重建ProxyConfigManagerProxyConfigManager.cs完整可运行配置示例BasicYarpSamplesamples/BasicYarpSample/appsettings.json纯配置文件启动方式LoadFromConfigsamples/ReverseProxy.Config.Sample/Program.cs纯代码启动方式LoadFromMemorysrc/ReverseProxy/Configuration/InMemoryConfigProviderExtensions.cs小结从 docs/designs/config.md 这份设计文档可以清晰看到 YARP 配置体系的成型轨迹路由匹配从复杂表达式收敛为结构化 Match隐式 AND、多条路由表达 OR后端配置从数组 平铺收敛为字典 嵌套热重载从未来需求落地为变更令牌 差异应用的成熟机制代码增强配置则始终以 Kestrel 命名端点模式为参照。设计文档中记录的大多数讨论结论都能在当前仓库的RouteConfig/RouteMatch/ClusterConfig/DestinationConfig/IProxyConfig/ProxyConfigManager中找到一一对应的实现。对于希望基于 YARP 搭建配置化网关的开发者直接以 samples/BasicYarpSample/appsettings.json 为模板起步并结合 ReverseProxy.Config.Sample 的LoadFromConfig接线方式即可获得改配置文件、零重启生效的完整能力。【免费下载链接】reverse-proxyA toolkit for developing high-performance HTTP reverse proxy applications.项目地址: https://gitcode.com/GitHub_Trending/re/reverse-proxy创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考