编写xds服务并实现envoy服务的动态配置

参考资料

在微服务架构中,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 通信的方法
  1. 查看xds服务器详细日志
  2. 通过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 更新成功次数
  1. 使用 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"
}
  1. tcpdump + Wireshark(底层分析)
bash 复制代码
# 抓取 xDS 流量
sudo tcpdump -i any -s 0 -w xds-traffic.pcap port 18000

检查grpc请求和相应如下

  • 其中的重传和抓包规则有关,实际上是-i any下 tcpdump 在多个网络接口上重复抓到了同一个包
相关推荐
乾元2 小时前
对抗性攻击:一张贴纸如何让自动驾驶视觉系统失效?
运维·网络·人工智能·安全·机器学习·自动驾驶
UP_Continue3 小时前
Linux--进程间通信
linux·运维·服务器
kaoa0003 小时前
Linux入门攻坚——67、MySQL数据库-4
linux·运维·数据库·mysql
prince_zxill3 小时前
在 Ubuntu 系统下安装 Nanobot:全面指南
linux·运维·ubuntu
Elastic 中国社区官方博客3 小时前
Elasticsearch:使用 Workflow 查询天气,发送消息到 Slack
大数据·运维·人工智能·elasticsearch·搜索引擎·ai
独自归家的兔4 小时前
Harbor 登录报错 - 核心服务不可用
运维·harbor
虹科网络安全4 小时前
艾体宝洞察 | 流程自动化的下一步,是决策自动化
运维·自动化
人道领域4 小时前
Spring拦截器原理与实战详解
java·运维·服务器
开开心心_Every5 小时前
在线看报软件, 22家知名报刊免费看
linux·运维·服务器·华为od·edge·pdf·华为云