Traefik UDP Service 配置指南:LoadBalancer 与 Weighted 负载均衡详解
UDP Service 是 Traefik 动态配置(Dynamic Configuration)的核心抽象之一,负责把进入 UDP 路由器的数据报(datagram)转发到能够处理它们的后端服务器。本文基于当前仓库 docs/content/reference/routing-configuration/udp/service.md 文档展开,完整讲解 UDP 服务的两种类型——Servers LoadBalancer(服务器负载均衡)与 Weighted Round Robin(加权轮询,别名 WRR)——的声明格式、配置参数与底层实现原理,帮助你为 DNS、NTP、syslog 等 UDP 业务准确编排 Traefik 的流量分发策略。
UDP 服务(Service)总体结构
在 Traefik 的动态配置中,udp.services 这一节用于声明 UDP 服务。与 HTTP、TCP 服务一致,每个服务条目中的各个字段代表一种服务类型,因此对于每一个声明的服务,必须且只能启用其中一种类型字段,来决定该服务究竟被创建为哪种形式。目前可用的 UDP 服务类型只有两种:
LoadBalancer(服务器负载均衡):在同一服务内部的多个后端服务器之间分发数据报;Weighted(加权轮询 / WRR):在多个已定义的服务之间按权重分发数据报。
上述语义直接映射到源码数据结构 pkg/config/dynamic/udp_config.go:
// UDPService defines the configuration for a UDP service. All fields are mutually exclusive.
type UDPService struct {
LoadBalancer *UDPServersLoadBalancer `json:"loadBalancer,omitempty" toml:"loadBalancer,omitempty" yaml:"loadBalancer,omitempty" export:"true"`
Weighted *UDPWeightedRoundRobin `json:"weighted,omitempty" toml:"weighted,omitempty" yaml:"weighted,omitempty" label:"-" export:"true"`
}
注意其中的注释 "All fields are mutually exclusive"(所有字段互斥)。这条约束在服务构建阶段被严格校验:服务管理器 pkg/server/service/udp/service.go 中,如果检测到 LoadBalancer 与 Weighted 同时非空,会直接报错并拒绝创建:
if conf.LoadBalancer != nil && conf.Weighted != nil {
err := errors.New("cannot create service: multi-types service not supported, consider declaring two different pieces of service instead")
conf.AddError(err, true)
return nil, err
}
同样,如果两种类型都未定义,构建时会抛出 the UDP service %q does not have any type defined 错误。因此实践中请务必为每个 UDP 服务明确且仅明确地声明一种类型。
Servers Load Balancer(服务器负载均衡)
Servers Load Balancer 负责在同一服务的多个后端服务器之间平衡转发流量,是 UDP 流量分发的最基本形态。
Servers 字段
servers 字段定义了参与该负载均衡组的所有服务器,即承载该服务程序的每个实例地址(IP:Port)。由于 UDP 本身无连接、无路径(path)与 Host 概念,UDP 服务器的配置远比 HTTP 简单:每个 server 只有一个 address 字段(参见 pkg/config/dynamic/udp_config.go 中的 UDPServer 结构,唯一的配置项是 Address string)。
配置示例:单服务器服务
以下示例通过 File Provider(文件提供方)声明一个仅含一台后端服务器的 UDP 服务:
## 动态配置
udp:
services:
my-service:
loadBalancer:
servers:
- address: "xx.xx.xx.xx:xx"
## 动态配置
[udp.services]
[udp.services.my-service.loadBalancer]
[[udp.services.my-service.loadBalancer.servers]]
address = "xx.xx.xx.xx:xx"
在实际使用中,将 address 替换为目标后端的真实 IP 与 UDP 端口即可,例如 "192.168.1.10:9000";同一 servers 列表下可以声明多个地址条目,以实现对多副本后端的负载均衡。
底层构建逻辑
当一个 UDP 服务被声明为 loadBalancer 时,服务管理器 pkg/server/service/udp/service.go 会执行如下流程:
- 创建一个
udp.NewWRRLoadBalancer()实例(见 pkg/udp/wrr_load_balancer.go); - 遍历配置中的服务器列表(注意会经过一次随机
shuffle,见 service.go,使初始分发顺序更均匀); - 对每个地址调用
net.SplitHostPort校验其是否为合法的IP:Port格式,解析失败则跳过该服务器并记录错误日志; - 为合法地址创建
udp.NewProxy(server.Address)反向代理处理器,通过loadBalancer.AddServer(handler)加入均衡池。
由此可见,即使是最简单的多服务器负载均衡,底层同样复用了 WRR 负载均衡器实现,只不过每台后端服务器的权重恒为 1。
Weighted Round Robin(加权轮询)
Weighted Round Robin(别名 WRR)负载均衡器负责根据给定的权重,在多个服务之间平衡连接(数据报流)。
使用前提与支持的提供方
- 该策略仅可用于在多个“服务”之间进行负载均衡,而不能用于在“服务器”之间做加权——后者请直接在服务器的 LoadBalancer 中声明对应地址。
- 从官方支持范围看,该策略目前可以基于以下两种 Provider 定义:
配置示例:在两个服务之间按 3:1 加权
下面示例定义一个名为 app 的加权服务,其流量按权重 3:1 分发到 appv1 与 appv2 两个子服务;而 appv1、appv2 本身又是各自带后端服务器的普通 LoadBalancer 服务:
udp:
services:
app:
weighted:
services:
- name: appv1
weight: 3
- name: appv2
weight: 1
appv1:
loadBalancer:
servers:
- address: "xxx.xxx.xxx.xxx:8080"
appv2:
loadBalancer:
servers:
- address: "xxx.xxx.xxx.xxx:8080"
[udp.services]
[udp.services.app]
[[udp.services.app.weighted.services]]
name = "appv1"
weight = 3
[[udp.services.app.weighted.services]]
name = "appv2"
weight = 1
[udp.services.appv1]
[udp.services.appv1.loadBalancer]
[[udp.services.appv1.loadBalancer.servers]]
address = "xxx.xxx.xxx.xxx:8080"
[udp.services.appv2]
[udp.services.appv2.loadBalancer]
[[udp.services.appv2.loadBalancer.servers]]
address = "xxx.xxx.xxx.xxx:8080"
上述示例展示的正是加权轮询的经典用法:把部分流量引入新版本(如 appv2,weight=1),同时把大部分流量保留在稳定版本(如 appv1,weight=3),可用于金丝雀发布或按容量比例分流。
配置选项(Configuration Options)
加权服务(weighted)支持以下配置字段:
| 字段 | 描述 | 默认值 | 是否必填 |
|---|---|---|---|
services |
定义参与负载均衡的服务列表。 | — | 是 |
services.name |
参与负载均衡的目标服务名称。 | "" |
是 |
services.weight |
在均衡连接时为该服务赋予的权重。 | 1 |
否 |
关于默认权重为 1,源码中由 UDPWRRService 的 SetDefaults 方法显式兜底(pkg/config/dynamic/udp_config.go):
func (w *UDPWRRService) SetDefaults() {
defaultWeight := 1
w.Weight = &defaultWeight
}
底层构建与轮询算法
当服务类型为 weighted 时,服务管理器会递归地构建每个被引用的子服务(pkg/server/service/udp/service.go):
case conf.Weighted != nil:
loadBalancer := udp.NewWRRLoadBalancer()
for _, service := range shuffle(conf.Weighted.Services, m.rand) {
handler, err := m.BuildUDP(ctx, service.Name)
if err != nil {
logger.Error().Err(err).Msg("Failed to build UDP handler")
return nil, err
}
loadBalancer.AddWeightedServer(handler, service.Weight)
}
return loadBalancer, nil
这里 BuildUDP(ctx, service.Name) 会对 services[].name 指向的子服务做递归解析,因此加权引用链上的每个服务都必须存在且类型合法,否则构建失败。这也解释了为何 services.name 是必填项。
真正执行加权选择的调度算法位于 pkg/udp/wrr_load_balancer.go。它采用基于“最大权重 + 权重最大公约数(GCD)”的平滑加权轮询实现:每次遍历一轮后,currentWeight 减去所有权重的 GCD,当降到 0 以下时重新置为最大权重,再挑选权重不低于 currentWeight 的下一个服务器。该算法避免了简单加权轮询可能出现的“连续多次打到同一台高权重后端”的突发问题,使 3:1 的权重在长期统计上表现为约 3:1 的均匀交错分发。此外 ServeUDP 在选择服务器时通过互斥锁(sync.Mutex)保证并发安全(见 wrr_load_balancer.go)。
集成测试验证
仓库的 UDP 集成测试对 WRR 的加权分发结果做了直接断言。integration/udp_test.go 中的 TestWRR 用例基于 fixtures/udp/wrr.toml 配置(三个后端 whoami-a、whoami-b、whoami-c,权重分别为 3、2、3),连续发起 8 次 UDP 探测后,断言命中分布精确等于 {"whoami-a": 3, "whoami-b": 2, "whoami-c": 3},从端到端层面验证了加权轮询的算法正确性。
与 UDP Router 的配合使用
UDP Service 本身不直接对外暴露入口,而是必须被 UDP Router 引用(router 中的 service 字段指向服务名称)才能真正接收入口点的数据报。一个完整的 UDP 路由链路示例(结构化 YAML)如下:
## 入口点(entryPoints)需在静态配置中声明,例如 "dns"、"udp-ep"
udp:
routers:
my-udp-router:
entryPoints:
- "udp-ep"
- "dns"
service: my-udp-service
services:
my-udp-service:
loadBalancer:
servers:
- address: "10.0.0.1:9000"
值得留意的是,UDP 路由器只能指向 UDP 服务(不能指向 HTTP 或 TCP 服务)。尽管 UDP 是无连接的,Traefik 的 UDP 路由/转发仍基于“会话(session)”维护客户端与后端之间的状态,从而保证来自后端的数据报能够被正确送回发起请求的客户端。更多细节可参见 UDP Router 文档。
小结
- UDP 服务有两种互斥类型:
loadBalancer(服务器级均衡)与weighted(服务级加权轮询),声明时二者只能选其一。 loadBalancer.servers[].address使用IP:Port直接指向后端实例;weighted.services[]通过name引用其它已定义服务、通过weight控制分发比例(默认1)。- WRR 策略仅支持在服务之间加权,且目前仅能通过 File Provider 与 Kubernetes CRD(IngressRouteUDP) 定义。
- 从源码实现看,加权分发采用基于 GCD 的平滑加权轮询算法,由 pkg/udp/wrr_load_balancer.go 提供,并经 integration/udp_test.go 的集成测试验证,可放心用于生产分流场景。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00