参考资料
在微服务架构中,Envoy 作为服务网格的数据平面组件,可以通过 xDS(Discovery Service)协议动态获取配置。Istio、Consul 等成熟控制平面已经提供了完整的 xDS 实现。本文基于 Envoy 官方 go-control-plane 库,实现一个支持动态配置更新的 xDS 服务器,通过 gRPC 流式接口向 Envoy 推送配置更新,实现无需重启的动态配置变更。
xDS 是 Envoy 定义的一组动态配置发现协议,包括:
- LDS (Listener Discovery Service)
- CDS (Cluster Discovery Service)
- RDS (Route Discovery Service)
- EDS (Endpoint Discovery Service)
而github.com/envoyproxy/go-control-plane 是 Envoy 官方提供的 Go 语言控制平面实现库,封装了xDS 协议的 gRPC 服务实现,Snapshot Cache 配置缓存机制,Protobuf 资源类型定义
动态配置的必要性体现为,在云原生和微服务架构中,服务实例会频繁地扩缩容、上下线和迁移。相比 "修改配置文件 → reload → 等待旧连接结束"的模式,Envoy 的动态配置更适合大规模微服务集群的自动化运维。
- 如果类似Nginx那样使用静态配置文件,每次在变更时都需要重新加载配置(nginx -s reload)甚至重启服务,这会导致短暂的服务中断和连接断开。
- 而 Envoy 通过 xDS 协议(CDS/LDS/EDS/RDS)实现了真正的热更新:当后端服务变化时,控制平面(如 Istio、自定义 xDS Server)会通过 gRPC 长连接实时推送新配置到 Envoy,整个过程无需重启、无需重新加载、零停机、毫秒级生效,并且支持版本管理和增量更新。
测试环境使用github.com/envoyproxy/go-control-plane。通过 github.com/fsnotify/fsnotify监控配置YAML格式的文件实现热更新。github.com/envoyproxy/go-control-plane 是 Envoy 官方提供的 Go 语言库,
- 封装了 ADS/CDS/LDS/EDS/RDS 等所有 xDS 服务的 gRPC 接口,开发者无需从零实现复杂的 Protobuf 定义和 gRPC 通信逻辑。
- 并且提供 Snapshot Cache 机制,内置配置缓存和版本管理,自动处理配置的增量更新、版本对比、ACK/NACK 响应等复杂逻辑
- 包含所有 Envoy 配置资源的 Go 结构体(Cluster、Listener、Route、Endpoint 等)
简单的示例实现如下,只需要关注生成配置,而不用关注具体如何通过 gRPC 推送配置
go
import (
cache "github.com/envoyproxy/go-control-plane/pkg/cache/v3"
server "github.com/envoyproxy/go-control-plane/pkg/server/v3"
// 资源类型定义
cluster "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
listener "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
)
// 创建 Snapshot Cache(自动管理配置版本)
snapshotCache := cache.NewSnapshotCache(false, cache.IDHash{}, nil)
// 创建 xDS Server(自动处理 gRPC 通信)
srv := server.NewServer(context.Background(), snapshotCache, nil)
// 注册所有 xDS 服务
discoverygrpc.RegisterAggregatedDiscoveryServiceServer(grpcServer, x.server)
endpointservice.RegisterEndpointDiscoveryServiceServer(grpcServer, x.server)
clusterservice.RegisterClusterDiscoveryServiceServer(grpcServer, x.server)
routeservice.RegisterRouteDiscoveryServiceServer(grpcServer, x.server)
listenerservice.RegisterListenerDiscoveryServiceServer(grpcServer, x.server)
xDS服务测试环境搭建
项目结构
shell
├── docker-compose.yaml # 服务编排
├── Dockerfile # xDS Server 镜像
├── xds-server-simple.go # xDS Server 实现
├── front-envoy.yaml # Envoy 配置(动态)
├── resources/
│ └── config.yaml # xDS 资源配置
docker-compose文件如下
yaml
services:
envoy:
image: swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/envoyproxy/envoy:v1.37.0
volumes:
- ./front-envoy.yaml:/etc/envoy/envoy.yaml
networks:
envoymesh:
ipv4_address: 10.0.15.2
aliases:
- front-proxy
depends_on:
- xdsserver
ports:
- "8080:80"
- "9901:9901"
webserver01:
image: ikubernetes/demoapp:v1.0
environment:
- PORT=8080
- HOST=0.0.0.0
hostname: webserver01
networks:
envoymesh:
ipv4_address: 10.0.15.11
webserver02:
image: ikubernetes/demoapp:v1.0
environment:
- PORT=8080
- HOST=0.0.0.0
hostname: webserver02
networks:
envoymesh:
ipv4_address: 10.0.15.12
xdsserver:
build:
context: .
dockerfile: Dockerfile
environment:
- SERVER_PORT=18000
- NODE_ID=envoy_front_proxy
- RESOURCES_FILE=/etc/envoy-xds-server/config/config.yaml
volumes:
- ./resources:/etc/envoy-xds-server/config/
networks:
envoymesh:
ipv4_address: 10.0.15.5
aliases:
- xdsserver
- xds-service
expose:
- "18000"
networks:
envoymesh:
driver: bridge
ipam:
config:
- subnet: 10.0.15.0/24
配置文件
yaml
# resources/config.yaml
name: example-xds-config
spec:
listeners:
- name: listener_http
address: 0.0.0.0
port: 80
routes:
- name: local_route
prefix: /
clusters:
- webcluster
clusters:
- name: webcluster
endpoints:
- address: 10.0.15.11
port: 8080
- address: 10.0.15.12
port: 8080
envoy配置文件,实现xds动态发现
- 配置文件中仍旧存在static_resources部分,因为 Envoy 需要先知道去哪里获取动态配置,而这个信息必须是静态的 bootstrap 配置。
yaml
# Envoy 动态配置示例:通过 xDS 协议从控制平面动态获取配置
# 与静态配置不同,这里的 Listener、Cluster、Route、Endpoint 都通过 xDS 服务器动态下发
# 节点标识信息
node:
# 集群名称:标识 Envoy 所属的集群
cluster: test-cluster
# 节点 ID:唯一标识这个 Envoy 实例
# xDS 服务器根据这个 ID 推送对应的配置
# 在 xds-server-simple.go 中,这个 ID 用于匹配 snapshot
id: envoy_front_proxy
# 动态资源配置:通过 xDS 协议从控制平面获取配置
# 这是 Envoy 动态配置的核心,所有配置都可以在运行时更新
dynamic_resources:
# ADS 配置:Aggregated Discovery Service(聚合发现服务)
# ADS 允许通过单一 gRPC 流获取所有 xDS 配置(LDS、RDS、CDS、EDS)
# 优点:减少连接数,保证配置更新的顺序性和一致性
ads_config:
# API 类型:GRPC(使用 gRPC 协议通信)
# 其他选项:REST(已废弃)、DELTA_GRPC(增量更新)
api_type: GRPC
# 传输 API 版本:V3(xDS v3 协议)
# V3 是当前推荐的版本,V2 已废弃
transport_api_version: V3
# gRPC 服务配置:定义如何连接到 xDS 服务器
grpc_services:
# 使用 Envoy 内置的 gRPC 客户端
# 另一种方式是 google_grpc(使用 Google 的 gRPC 库)
- envoy_grpc:
# xDS 服务器的集群名称
# 必须在 static_resources.clusters 中定义
# Envoy 会连接到这个集群获取动态配置
cluster_name: xds_cluster
# CDS 配置:Cluster Discovery Service(集群发现服务)
# 动态获取 Cluster 配置(上游服务集群)
cds_config:
# 使用 xDS v3 API
resource_api_version: V3
# 通过 ADS 获取 CDS 配置
# ads: {} 表示使用上面定义的 ads_config
# 这样 CDS 请求会通过 ADS 的 gRPC 流发送
ads: {}
# LDS 配置:Listener Discovery Service(监听器发现服务)
# 动态获取 Listener 配置(监听哪些端口,如何处理请求)
lds_config:
# 使用 xDS v3 API
resource_api_version: V3
# 通过 ADS 获取 LDS 配置
# 所有配置(LDS、CDS、RDS、EDS)都通过同一个 ADS 连接获取
ads: {}
# 静态资源配置:启动时加载,用于定义连接到 xDS 服务器的集群
# 这是"鸡生蛋"的问题:需要一个静态配置来连接 xDS 服务器,才能获取其他动态配置
static_resources:
clusters:
# xDS 服务器集群:Envoy 连接到这个集群获取动态配置
- name: xds_cluster
# 连接超时:1 秒
# 如果 1 秒内无法连接到 xDS 服务器,Envoy 会重试
connect_timeout: 1s
# 集群类型:STRICT_DNS
# Envoy 会定期解析 "xdsserver" 这个 DNS 名称
# 在 Docker Compose 中,"xdsserver" 是服务名,会自动解析为容器 IP
type: STRICT_DNS
# 负载均衡分配:xDS 服务器的地址
load_assignment:
cluster_name: xds_cluster
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
# xDS 服务器地址:xdsserver(Docker Compose 服务名)
# 在 docker-compose.yaml 中定义的服务名会自动解析
address: xdsserver
# xDS 服务器端口:18000
# 对应 xds-server-simple.go 中监听的端口
port_value: 18000
# HTTP/2 协议选项:启用 HTTP/2
# xDS 协议基于 gRPC,而 gRPC 基于 HTTP/2
# 必须启用 HTTP/2 才能与 xDS 服务器通信
http2_protocol_options: {}
# 上游连接选项:配置与 xDS 服务器的连接行为
upstream_connection_options:
# TCP Keepalive:保持与 xDS 服务器的长连接
# 防止连接因空闲而被中间设备(如防火墙、负载均衡器)断开
# 这对于 xDS 的长连接 gRPC 流非常重要
tcp_keepalive: {}
# Admin 接口:Envoy 的管理和监控接口
admin:
# 访问日志路径:/dev/null 表示不记录日志
access_log_path: /dev/null
# Admin 接口监听地址:0.0.0.0:9901
address:
socket_address:
address: 0.0.0.0
port_value: 9901
# 常用 Admin 接口:
# - http://localhost:9901/config_dump:查看完整配置(包括动态获取的配置)
# - http://localhost:9901/clusters:查看集群状态和健康检查结果
# - http://localhost:9901/stats:查看统计指标
# - http://localhost:9901/listeners:查看监听器状态
如果使用静态配置而非xds发现,则配置示例如下
yaml
# Envoy 节点标识信息
node:
# 集群名称:用于标识 Envoy 所属的集群
# 在多个 Envoy 实例组成的集群中,相同 cluster 的实例共享配置
cluster: test-cluster
# 节点 ID:唯一标识这个 Envoy 实例
# xDS 服务器使用这个 ID 来识别不同的 Envoy 实例,推送对应的配置
id: envoy_front_proxy
# 静态资源配置:在 Envoy 启动时加载,不会动态更新
# 与 dynamic_resources 相对,dynamic_resources 通过 xDS 协议动态获取配置
static_resources:
# Listeners:监听器列表,定义 Envoy 监听哪些端口,如何处理入站连接
listeners:
# HTTP 监听器:监听 80 端口的 HTTP 流量
- name: listener_http
# 监听地址:0.0.0.0:80 表示监听所有网卡的 80 端口
address:
socket_address:
address: 0.0.0.0 # 监听所有 IP 地址
port_value: 80 # HTTP 默认端口
# 过滤器链:定义如何处理接收到的连接
# 一个连接会依次经过这些过滤器处理
filter_chains:
- filters:
# HTTP 连接管理器:处理 HTTP/1.1 和 HTTP/2 协议
- name: envoy.filters.network.http_connection_manager
# 过滤器配置(使用 protobuf TypedConfig)
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
# 统计前缀:用于生成指标名称
# 例如:ingress_http.downstream_rq_total(总请求数)
stat_prefix: ingress_http
# 编解码器类型:AUTO 自动检测 HTTP/1.1 或 HTTP/2
codec_type: AUTO
# 路由配置:静态配置路由规则(不使用 RDS)
# 与动态配置不同,这里直接在 Listener 中定义路由规则
route_config:
# 路由配置名称
name: local_route
# 虚拟主机列表:根据域名匹配不同的路由规则
virtual_hosts:
- name: local_service
# 域名匹配:["*"] 表示匹配所有域名
# 可以配置具体域名,如:["example.com", "*.example.com"]
domains: ["*"]
# 路由规则列表:定义 URL 路径如何路由到后端集群
routes:
# 路由规则 1:所有请求路由到 webcluster
- match:
# 路径匹配:前缀匹配 "/"(匹配所有路径)
# 其他匹配方式:path(精确匹配)、safe_regex(正则匹配)
prefix: "/"
# 路由动作:将请求转发到指定集群
route:
# 目标集群名称:必须在 clusters 中定义
cluster: webcluster
# HTTP 过滤器链:处理 HTTP 请求的过滤器
# 请求会依次经过这些过滤器,最后由 router 转发到上游
http_filters:
# Router 过滤器:必需的最后一个过滤器,负责实际的请求转发
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
# Clusters:上游集群列表,定义后端服务的连接方式
clusters:
# Web 服务集群
- name: webcluster
# 连接超时:0.25 秒(250 毫秒)
# 如果无法在此时间内建立 TCP 连接,则认为后端不可用
connect_timeout: 0.25s
# 集群类型:STRICT_DNS
# Envoy 会定期对配置的 DNS 名称进行解析,获取 IP 地址列表
# 其他类型:
# - STATIC: 静态 IP 地址,不进行 DNS 解析
# - LOGICAL_DNS: 每次请求时解析 DNS(适合单个主机名)
# - EDS: 通过 Endpoint Discovery Service 动态获取端点
type: STRICT_DNS
# 负载均衡策略:ROUND_ROBIN(轮询)
# 请求会依次分配给每个健康的后端服务器
# 其他策略:
# - LEAST_REQUEST: 选择活跃请求数最少的后端
# - RANDOM: 随机选择后端
# - RING_HASH: 一致性哈希(适合缓存场景)
lb_policy: ROUND_ROBIN
# 负载均衡分配:定义后端服务器的地址列表
load_assignment:
# 集群名称:必须与上面的 cluster name 一致
cluster_name: webcluster
# 端点列表:按地域(Locality)分组
endpoints:
# Locality 1: us-east-1a 区域
# Locality 用于跨区域负载均衡和故障转移
- locality:
# 区域:us-east(例如:美国东部)
region: us-east
# 可用区:us-east-1a(例如:AWS 的可用区)
zone: us-east-1a
# 子区域:rack-1(例如:机架编号,更细粒度的位置信息)
sub_zone: rack-1
# 负载均衡端点列表:实际的后端服务器地址
lb_endpoints:
# 后端服务器 1
- endpoint:
address:
socket_address:
# IP 地址:10.0.15.11
address: 10.0.15.11
# 端口:8080
port_value: 8080
# 后端服务器 2
- endpoint:
address:
socket_address:
address: 10.0.15.12
port_value: 8080
# Admin 接口:Envoy 的管理和监控接口
admin:
# 访问日志路径:/dev/null 表示不记录访问日志
# 可以改为文件路径,如:/var/log/envoy/admin_access.log
access_log_path: /dev/null
# Admin 接口监听地址:0.0.0.0:9901
address:
socket_address:
address: 0.0.0.0 # 监听所有 IP 地址
port_value: 9901 # Admin 接口端口
# 通过 Admin 接口可以:
# - 查看配置:http://localhost:9901/config_dump
# - 查看统计信息:http://localhost:9901/stats
# - 查看集群状态:http://localhost:9901/clusters
# - 修改日志级别:http://localhost:9901/logging
测试负载均衡
bash
for i in {1..5}; do curl http://localhost:8080/; done
# 输出示例:
# ServerName: webserver01
# ServerName: webserver02
# ServerName: webserver01
# ServerName: webserver02
# ServerName: webserver01
查看集群状态
bash
curl -s http://localhost:9901/clusters | grep webcluster
# webcluster::10.0.15.11:8080::health_flags::healthy
# webcluster::10.0.15.12:8080::health_flags::healthy
xDS服务启动和交互流程
动态配置的核心是 xDS 协议,它通过 gRPC 长连接实现配置的实时推送。整个启动流程如下
xDS Server 启动后监听 18000 端口,等待 Envoy 连接。
- Node ID是Envoy 端的身份声明。在 envoy.yaml 中配置 node.id,Envoy 连接 xDS Server 时会发送这个 Node ID。xDS Server 端匹配配置
x.cache.SetSnapshot(ctx, "envoy_front_proxy", snapshot),只有 Node ID 匹配的 Envoy 才能收到这个配置。此处简化了xds服务的功能只支持单一 Node ID (envoy_front_proxy)。Istio 等服务网格管理成千上万个 Envoy 实例时每个 Pod 的 Sidecar Envoy 都有唯一的 Node ID来进行区分
log
xdsserver-1 | Starting xDS server...
xdsserver-1 | Port: 18000
xdsserver-1 | Node ID: envoy_front_proxy
xdsserver-1 | xDS server listening on port 18000
xDS Server使用如下配置文件启动
yaml
name: example-xds-config
spec:
listeners:
- name: listener_http
address: 0.0.0.0
port: 80
routes:
- name: local_route
prefix: /
clusters:
- webcluster
clusters:
- name: webcluster
endpoints:
- address: 10.0.15.11
port: 8080
- address: 10.0.15.12
port: 8080
xDS Server 从 YAML 文件读取配置,解析Listener 和 Cluster资源,并生成版本号为 1 的配置快照。随后启动文件监控(fsnotify)后续的配置文件更新
- SetSnapshot 是 xDS 动态配置的核心机制,包含了配置的完整状态(Listeners,Clusters,Routes,Endpoints)。不同的资源之间存在依赖关系,快照将配置打包为一个整体保证了配置的原子性。此外还提供了提供版本管理和并发安全的能力。此外,Snapshot保存在内存中,因此xDS Server 重启后内存中的 Snapshot 会全部丢失,由于config.yaml通常持久化因此没有副作用。
log
xdsserver-1 | Loading config from: /etc/envoy-xds-server/config/config.yaml
xdsserver-1 | Loaded config: example-xds-config with 1 listeners and 1 clusters
xdsserver-1 | Config updated successfully, version: 1
xdsserver-1 | Watching config file: /etc/envoy-xds-server/config/config.yaml
Envoy 启动后,通过 ADS (Aggregated Discovery Service) 协议建立 gRPC 长连接。这个连接会一直保持,用于后续的配置推送。
log
envoy-1 | StreamAggregatedResources gRPC config stream
Envoy 接收配置。此后 Envoy 完成初始化,可以开始接受流量
- CDS (Cluster Discovery Service) 推送集群配置,envoy添加 1 个新集群(webcluster)
- LDS (Listener Discovery Service) 推送监听器配置,envoy创建监听器 listener_http
log
envoy-1 | cm init: initializing cds
envoy-1 | cds: add 1 cluster(s), remove 1 cluster(s)
envoy-1 | cds: added/updated 1 cluster(s), skipped 0 unmodified cluster(s)
envoy-1 | lds: add/update listener 'listener_http'
修改配置文件添加第三个端点后,xDS Server 自动检测更新。版本号从 1 递增到 2并生成新的配置快照。更新快照后xDS Server通过已建立的 gRPC Stream 自动推送新配置
log
xdsserver-1 | Config file modified: /etc/envoy-xds-server/config/config.yaml
xdsserver-1 | Loading config from: /etc/envoy-xds-server/config/config.yaml
xdsserver-1 | Loaded config: example-xds-config with 1 listeners and 1 clusters
xdsserver-1 | Config updated successfully, version: 2
Envoy 接收并应用更新
log
envoy-1 | cds: add 1 cluster(s), remove 1 cluster(s)
envoy-1 | cds: added/updated 0 cluster(s), skipped 1 unmodified cluster(s)
查看集群状态验证配置的变更
bash
$ curl -s http://localhost:9901/clusters | grep webcluster
webcluster::10.0.15.11:8080::health_flags::healthy
webcluster::10.0.15.12:8080::health_flags::healthy
webcluster::10.0.15.13:8080::health_flags::healthy # 新增的端点
查看配置版本更新状态
bash
$ curl -s http://localhost:9901/config_dump | jq '.configs[].version_info'
"2" # 版本号已更新
关键资源部分代码解析
Cluster 资源生成代码如下
-
使用 EDS (Endpoint Discovery Service) 动态获取端点
-
为什么通过 ADS 聚合发现服务统一管理?
| 特性 | 不使用 ADS | 使用 ADS |
|---|---|---|
| 连接数 | 4 个 gRPC 流 | 1 个 gRPC 流 |
| 更新顺序 | 无保证,可能乱序 | 严格按 EDS→CDS→RDS→LDS |
| 版本一致性 | 可能部分更新 | 统一版本号,原子更新 |
| 网络故障 | 部分连接断开导致不一致 | 单一连接,要么全成功要么全失败 |
| 资源消耗 | 高(多连接) | 低(单连接) |
| 配置复杂度 | 需要管理多个流 | 统一管理 |
- makeCluster中已经通过ads配置endpoint 还需要makeEndpoint函数?makeCluster 中的 ADS 配置只是告诉 Envoy "去哪里获取" Endpoint 数据(通过 ADS 协议),但没有提供实际的 Endpoint 地址。makeEndpoint 函数提供的是 实际的 Endpoint 数据(具体的 IP 和端口列表),这些数据会通过 ADS 协议发送给 Envoy。因此makeRoute同理
go
// Cluster 定义了 Envoy 如何连接到上游服务(后端服务)
func (x *XDSServer) makeCluster(config ClusterConfig) *cluster.Cluster {
return &cluster.Cluster{
// Cluster 名称,用于在 Route 中引用(例如:webcluster)
Name: config.Name,
// 连接超时时间:5 秒。如果 5 秒内无法建立 TCP 连接,则认为后端不可用
ConnectTimeout: durationpb.New(5 * time.Second),
// Cluster 类型:EDS (Endpoint Discovery Service) 表示这个 Cluster 的端点(后端服务器地址)通过 EDS 动态获取
// 其他类型:STATIC(静态)、STRICT_DNS(DNS 解析)、LOGICAL_DNS 等
ClusterDiscoveryType: &cluster.Cluster_Type{Type: cluster.Cluster_EDS},
// EDS 配置:告诉 Envoy 如何获取端点信息
EdsClusterConfig: &cluster.Cluster_EdsClusterConfig{
EdsConfig: &core.ConfigSource{
// 使用 xDS v3 API 版本
ResourceApiVersion: core.ApiVersion_V3,
// 配置源:通过 ADS (Aggregated Discovery Service) 获取。ADS 是一种聚合的 xDS 协议,可以通过单一 gRPC 流获取所有配置
// 这样 Envoy 只需要一个连接就能获取 LDS、CDS、EDS、RDS 等所有配置
ConfigSourceSpecifier: &core.ConfigSource_Ads{
Ads: &core.AggregatedConfigSource{},
},
},
},
}
}
Endpoint 资源生成逻辑如下:将 YAML 配置转换为 Envoy Protobuf 格式,支持多个端点的负载均衡
- Locality 是 Envoy 用于跨区域负载均衡的概念。Locality 表示地理位置,通常包含三个维度:
- Region(区域):例如 us-west、cn-north
- Zone(可用区):例如 us-west-1a、cn-north-1a
- Sub-zone(子区域):更细粒度的划分
go
// 定义了 Cluster 的具体后端服务器地址列表(IP + 端口)
func (x *XDSServer) makeEndpoint(config ClusterConfig) *endpoint.ClusterLoadAssignment {
// 存储所有负载均衡端点
var lbEndpoints []*endpoint.LbEndpoint
// 遍历配置文件中的每个端点(例如:10.0.15.11:8080, 10.0.15.12:8080)
for _, ep := range config.Endpoints {
lbEndpoints = append(lbEndpoints, &endpoint.LbEndpoint{
// 端点标识符:使用 Endpoint 类型(还有其他类型如 EndpointName)
HostIdentifier: &endpoint.LbEndpoint_Endpoint{
Endpoint: &endpoint.Endpoint{
// 端点地址信息
Address: &core.Address{
// 地址类型:Socket 地址(IP + 端口)
// 其他类型:Pipe(Unix Domain Socket)、EnvoyInternalAddress
Address: &core.Address_SocketAddress{
SocketAddress: &core.SocketAddress{
// 协议:TCP(HTTP/gRPC 都基于 TCP)
Protocol: core.SocketAddress_TCP,
// IP 地址(例如:10.0.15.11)
Address: ep.Address,
// 端口号(例如:8080)
PortSpecifier: &core.SocketAddress_PortValue{
PortValue: ep.Port,
},
},
},
},
},
},
})
}
// 返回 Cluster 的负载均衡分配配置
return &endpoint.ClusterLoadAssignment{
// Cluster 名称,必须与 makeCluster 中的 Name 一致
ClusterName: config.Name,
// 端点列表,按地域(Locality)分组
// Locality 用于跨区域负载均衡(例如:优先访问同区域的服务) 这里只有一个 Locality,包含所有端点
Endpoints: []*endpoint.LocalityLbEndpoints{{
LbEndpoints: lbEndpoints,
}},
}
}
Listener 资源生成
go
// Listener 是 Envoy 的监听器,负责接收客户端连接并处理请求
func (x *XDSServer) makeListener(config ListenerConfig) *listener.Listener {
// 创建 Router 过滤器配置
// Router 是 HTTP 过滤器链的最后一个过滤器,负责将请求路由到上游集群
routerConfig, _ := anypb.New(&router.Router{})
// 创建 HTTP 连接管理器
// HttpConnectionManager 是处理 HTTP/1.1、HTTP/2 连接的核心组件
manager := &hcm.HttpConnectionManager{
// 编解码器类型:AUTO 表示自动检测 HTTP/1.1 或 HTTP/2
// 其他选项:HTTP1(仅 HTTP/1.1)、HTTP2(仅 HTTP/2)、HTTP3(实验性)
CodecType: hcm.HttpConnectionManager_AUTO,
// 统计前缀:用于生成指标名称(例如:ingress_http.downstream_rq_total)
// 可以在 Envoy admin 接口 /stats 中查看这些指标
StatPrefix: "ingress_http",
// 路由配置方式:使用 RDS (Route Discovery Service) 动态获取路由规则
// 另一种方式是 RouteConfig,直接在 Listener 中静态配置路由
RouteSpecifier: &hcm.HttpConnectionManager_Rds{
Rds: &hcm.Rds{
// RDS 配置源:从哪里获取路由配置
ConfigSource: &core.ConfigSource{
// 使用 xDS v3 API 版本
ResourceApiVersion: core.ApiVersion_V3,
// 配置源类型:通过 ADS (Aggregated Discovery Service) 获取
// ADS 允许通过单一 gRPC 流获取所有 xDS 配置(LDS、RDS、CDS、EDS)
ConfigSourceSpecifier: &core.ConfigSource_Ads{
Ads: &core.AggregatedConfigSource{},
},
},
// 路由配置名称:必须与 makeRoute 中生成的 RouteConfiguration.Name 一致
// Envoy 会通过这个名称从 xDS 服务器获取对应的路由规则
RouteConfigName: config.Name,
},
},
// HTTP 过滤器链:按顺序处理请求
// 每个请求都会依次经过这些过滤器,最后由 Router 过滤器路由到上游
HttpFilters: []*hcm.HttpFilter{{
// 过滤器名称:envoy.filters.http.router(内置的路由过滤器)
// 这是必需的最后一个过滤器,负责实际的请求转发
Name: wellknown.Router,
// 过滤器配置:使用 TypedConfig 传递 Router 配置
ConfigType: &hcm.HttpFilter_TypedConfig{TypedConfig: routerConfig},
}},
}
// 将 HttpConnectionManager 序列化为 protobuf Any 类型
// Envoy 的配置使用 protobuf,需要将配置对象包装成 Any 类型
pbst, _ := anypb.New(manager)
// 返回完整的 Listener 配置
return &listener.Listener{
// Listener 名称(例如:listener_0)
Name: config.Name,
// 监听地址:Listener 绑定的 IP 和端口
Address: &core.Address{
// 地址类型:Socket 地址(IP + 端口)
Address: &core.Address_SocketAddress{
SocketAddress: &core.SocketAddress{
// 协议:TCP(HTTP 基于 TCP)
Protocol: core.SocketAddress_TCP,
// 监听 IP 地址(例如:0.0.0.0 表示监听所有网卡)
Address: config.Address,
// 监听端口(例如:10000)
PortSpecifier: &core.SocketAddress_PortValue{
PortValue: config.Port,
},
},
},
},
// 过滤器链:定义如何处理接收到的连接
// 一个 Listener 可以有多个 FilterChain,根据匹配条件选择使用哪个链
// 例如:可以根据 SNI(Server Name Indication)选择不同的证书和处理逻辑
FilterChains: []*listener.FilterChain{{
// 网络过滤器列表:处理 L4(TCP)层的数据
// 对于 HTTP,通常只需要一个 HttpConnectionManager 过滤器
Filters: []*listener.Filter{{
// 过滤器名称:envoy.filters.network.http_connection_manager
// 这是处理 HTTP 协议的核心网络过滤器
Name: wellknown.HTTPConnectionManager,
// 过滤器配置:传递之前创建的 HttpConnectionManager 配置
ConfigType: &listener.Filter_TypedConfig{TypedConfig: pbst},
}},
}},
}
}
xDS协议内容分析
协议架构
shell
┌─────────────┐ ┌──────────────┐
│ Envoy │ │ xDS Server │
│ (数据平面) │ │ (控制平面) │
└──────┬──────┘ └──────┬───────┘
│ │
│ 1. 建立 gRPC 双向流 │
│ ─────────────────────────────────────────────► │
│ │
│ 2. DiscoveryRequest (订阅配置) │
│ ─────────────────────────────────────────────► │
│ │
│ 3. DiscoveryResponse (推送配置) │
│ ◄───────────────────────────────────────────── │
│ 4. DiscoveryRequest (ACK) │
│ ─────────────────────────────────────────────► │
│ │
│ 5. 配置变更时推送新版本 │
│ ◄───────────────────────────────────────────── │
xDS 请求结构 (DiscoveryRequest)
protobuf
message DiscoveryRequest {
// version_info: 配置版本号
// - 首次请求为空字符串 ""
// - ACK 时填入服务器返回的版本号(如 "1", "3")
// - 用于服务器判断客户端当前配置版本
// - 实测发现:版本号可能不连续(如 1→3),因为配置变更可能触发多个资源类型同时更新
string version_info = 1;
// node: 节点标识信息
// - id: 唯一标识这个 Envoy 实例(如 "envoy_front_proxy")
// - cluster: 集群名称(如 "test-cluster")
// - xDS 服务器根据 node.id 推送对应的配置
core.Node node = 2;
// resource_names: 请求的资源名称列表
// - 空数组 [] = 订阅所有资源(wildcard)
// - 指定名称 = 只订阅特定资源(如 ["listener_http"])
// - 实测:Envoy 通常使用 wildcard 订阅
repeated string resource_names = 3;
// type_url: 资源类型 URL,标识请求的资源类型
// - "type.googleapis.com/envoy.config.listener.v3.Listener" (LDS)
// - "type.googleapis.com/envoy.config.route.v3.RouteConfiguration" (RDS)
// - "type.googleapis.com/envoy.config.cluster.v3.Cluster" (CDS)
// - "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment" (EDS)
string type_url = 4;
// response_nonce: 响应的 nonce(用于 ACK/NACK)
// - 首次请求为空字符串 ""
// - 后续请求填入上次响应的 nonce(如 "3", "11")
// - 用于配对请求和响应,防止消息乱序
// - 实测:每次响应的 nonce 都不同
string response_nonce = 5;
// error_detail: 错误详情(只在 NACK 时使用)
// - 只在拒绝配置时填充
// - 包含拒绝原因(如 "Invalid listener configuration: port already in use")
// - NACK 特征:version_info 保持旧版本 + 包含 error_detail
google.rpc.Status error_detail = 6;
}
xDS 响应结构 (DiscoveryResponse)
protobuf
message DiscoveryResponse {
// version_info: 配置版本号
// - 配置的版本号(通常递增,如 "1", "2", "3")
// - Envoy 用此版本号进行 ACK
// - 实测:版本号从 1 跳到 3,说明中间有更新
// - 可能原因:多个资源类型同时更新,或快速连续的配置变更
string version_info = 1;
// resources: 资源列表
// - protobuf Any 类型的资源列表
// - 实际类型由 type_url 指定
// - 包含完整的配置对象(全量推送,不是增量)
// - 实测:每次都发送完整配置,不是只发送变更的字段
repeated google.protobuf.Any resources = 2;
// type_url: 资源类型 URL
// - 与 DiscoveryRequest 中的 type_url 对应
// - 标识 resources 中的资源类型
string type_url = 4;
// nonce: 唯一标识此响应的随机数
// - 唯一标识此响应(如 "3", "11", "12")
// - Envoy 必须在下次请求中回传此 nonce
// - 用于配对请求和响应,防止消息乱序
// - 实测:每次响应都有新的 nonce
string nonce = 5;
// control_plane: 控制平面标识
// - 标识发送此响应的控制平面实例
// - 可选字段
core.ControlPlane control_plane = 6;
}
首次推送和配置变更推送的消息结构完全相同,都使用 DiscoveryResponse,唯一的区别是:
- version_info:版本号递增
- nonce:每次不同
- resources:内容反映最新配置
REQUEST和RESPONSE
Envoy 启动,首次请求 LDS
│ TypeURL: type.googleapis.com/envoy.config.listener.v3.Listener
│ VersionInfo: ← 空字符串(首次请求)
│ ResponseNonce: ← 空字符串(首次请求)
│ Node:
│ - ID: envoy_front_proxy
│ - Cluster: test-cluster
│ ResourceNames: (wildcard - 请求所有资源)
服务器返回初始配置:
│ TypeURL: type.googleapis.com/envoy.config.listener.v3.Listener
│ VersionInfo: 1 ← 初始版本号
│ Nonce: 3 ← 唯一的 nonce
│ Resources: 1
│ [0] TypeURL: type.googleapis.com/envoy.config.listener.v3.Listener
│ Name: listener_http
│ Content:
│ {
│ "@type": "type.googleapis.com/envoy.config.listener.v3.Listener",
│ "address": {
│ "socketAddress": {
│ "address": "0.0.0.0",
│ "portValue": 80 ← 初始端口配置
│ }
│ },
│ "name": "listener_http",
│ "filterChains": [...]
│ }
Envoy ACK 确认:
│ TypeURL: type.googleapis.com/envoy.config.listener.v3.Listener
│ VersionInfo: 1 ← 确认接受版本 1
│ ResponseNonce: 3 ← 回传 nonce
│ ResourceNames: (wildcard)
ACK/NACK 机制
ACK (接受配置)
json
{
"version_info": "3", // 更新为服务器发送的新版本
"response_nonce": "11", // 回传服务器的 nonce
"type_url": "type.googleapis.com/envoy.config.listener.v3.Listener"
}
NACK (拒绝配置)
json
{
"version_info": "1", // 保持旧版本(不更新)
"response_nonce": "11", // 回传新响应的 nonce
"type_url": "type.googleapis.com/envoy.config.listener.v3.Listener",
"error_detail": {
"code": 3,
"message": "Invalid listener configuration: port already in use"
}
}
ADS (Aggregated Discovery Service)
ADS 通过单一 gRPC 流传输所有类型的配置,保证更新顺序:
- 修改配置文件后,所有资源类型(LDS/RDS/CDS/EDS)都会推送新版本,每个类型有独立的 nonce。
观察 xDS 通信的方法
- 查看xds服务器详细日志
- 通过Envoy Admin API查看xds配置和指标
bash
# 查看完整配置(包括从 xDS 获取的动态配置)
curl http://localhost:9901/config_dump | jq .
# 查看 xDS 统计信息
curl http://localhost:9901/stats | grep xds
# 关键指标:
# - cluster.xds_cluster.upstream_cx_active: 活跃连接数
# - listener_manager.lds.update_success: LDS 更新成功次数
# - cluster_manager.cds.update_success: CDS 更新成功次数
- 使用 grpcurl 手动测试
bash
# 安装
go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest
# 列出 xDS 服务器提供的所有服务
$ grpcurl -plaintext localhost:18000 list
output:
envoy.service.cluster.v3.ClusterDiscoveryService
envoy.service.discovery.v3.AggregatedDiscoveryService
envoy.service.endpoint.v3.EndpointDiscoveryService
envoy.service.listener.v3.ListenerDiscoveryService
envoy.service.route.v3.RouteDiscoveryService
grpc.reflection.v1.ServerReflection
grpc.reflection.v1alpha.ServerReflection
# 查看 ADS 服务的方法
$ grpcurl -plaintext localhost:18000 \
describe envoy.service.discovery.v3.AggregatedDiscoveryService
output:
service AggregatedDiscoveryService {
// 增量式聚合资源发现 RPC 方法
rpc DeltaAggregatedResources (
stream .envoy.service.discovery.v3.DeltaDiscoveryRequest
) returns (
stream .envoy.service.discovery.v3.DeltaDiscoveryResponse
);
// 流式聚合资源发现 RPC 方法
rpc StreamAggregatedResources (
stream .envoy.service.discovery.v3.DiscoveryRequest
) returns (
stream .envoy.service.discovery.v3.DiscoveryResponse
);
}
# 手动发送 LDS 请求
# xDS 服务器根据 node.id 来匹配配置。服务器配置的是 "envoy_front_proxy"。
$ grpcurl -plaintext -d '{"node": {"id": "envoy_front_proxy", "cluster": "test-cluster"}, "type_url": "type.googleapis.com/envoy.config.listener.v3.Listener"}' localhost:18000 envoy.service.discovery.v3.AggregatedDiscoveryService/StreamAggregatedResources
output:
{
"versionInfo": "1",
"resources": [
{
"@type": "type.googleapis.com/envoy.config.listener.v3.Listener",
"address": {
"socketAddress": {
"address": "0.0.0.0",
"portValue": 8080
}
},
"filterChains": [
{
"filters": [
{
"name": "envoy.filters.network.http_connection_manager",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager",
"httpFilters": [
{
"name": "envoy.filters.http.router",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.filters.http.router.v3.Router"
}
}
],
"rds": {
"configSource": {
"ads": {},
"resourceApiVersion": "V3"
},
"routeConfigName": "listener_http"
},
"statPrefix": "ingress_http"
}
}
]
}
],
"name": "listener_http"
}
],
"typeUrl": "type.googleapis.com/envoy.config.listener.v3.Listener",
"nonce": "1"
}
- tcpdump + Wireshark(底层分析)
bash
# 抓取 xDS 流量
sudo tcpdump -i any -s 0 -w xds-traffic.pcap port 18000
检查grpc请求和相应如下
- 其中的重传和抓包规则有关,实际上是
-i any下 tcpdump 在多个网络接口上重复抓到了同一个包
