很多人用 Envoy,却从没真正理解过 xDS(我也是,直到手搓了一遍)

前言

上一篇内容,我们详细讨论了envoy做服务发现,并且详细讨论了静态配置与使用dns做服务发现,并且通过consul的详细配置阐述了dns做服务发现的工作原理,但是也遗留了一个问题,一旦想要修改endpoint的配置

      clusters:
        - name: app_service
          connect_timeout: 1s
          type: STRICT_DNS
          lb_policy: ROUND_ROBIN
          load_assignment:
            cluster_name: app_service
            endpoints:
              - lb_endpoints:
                  - endpoint:
                      address:
                        socket_address:
                          address: "backend-service"
                          port_value: 10000

比如我想改address: "backend-service",envoy并不会自动感知,还是需要重启

envoy xDS简介

xDS 不是一个单一的模块,而是一组与 Envoy 服务发现相关、解耦的 API 接口集合

  • CDS (Cluster Discovery Service): 集群发现。获取上游集群的定义,即 Envoy 可以将流量路由到的一组逻辑上相似的上游主机
  • EDS (Endpoint Discovery Service): 端点发现。这是最核心的服务发现模块。它为每个集群提供具体的、健康的网络端点(如 IP:Port)列表。Envoy 支持通过 EDS 进行增量更新,从而实现高效、实时的服务实例变更
  • LDS (Listener Discovery Service): 监听器发现。获取 Envoy 应该监听的网络地址、端口和过滤器链配置
  • RDS (Route Discovery Service): 路由发现。获取虚拟主机和路由规则配置,用于将流量定向到正确的集群
  • SDS (Secret Discovery Service): 密钥发现。安全地获取 TLS 证书和私钥
  • ADS (Aggregated Discovery Service): 聚合发现服务。一个特殊的 gRPC 端点,它将所有 xDS API 聚合到单个流中。这确保了配置更新的一致性和顺序性,避免配置不一致导致的流量中断

是不是看得脑袋嗡嗡的,没关系,我们从最核心的入手,那就是EDS

envoy EDS

所谓EDS服务:

  • 就是在envoy之外,有一个配置中心,之前直接配置在envoy的静态配置,搬迁到配置中心来,新增和维护新规则都在配置中心维护
  • 一旦配置有变更,配置中心会主动推送到envoy,让其及时变更流量转发配置

创建eds服务端

手搓一个最简单的eds_server用来演示:

eds服务

该脚本启动18000端口,接收gRPC请求,并且响应EDS,只要envoy来连接18000,就会下发endpoint到envoy

修改envoy配置

再修改一下envoy的配置:

...
  clusters:
  - name: backend_cluster
    type: EDS
    connect_timeout: 0.25s
    lb_policy: ROUND_ROBIN
    eds_cluster_config:
      eds_config:
        api_config_source:
          api_type: GRPC
          grpc_services:
          - envoy_grpc:
              cluster_name: eds_server

  - name: eds_server
    connect_timeout: 1s
    type: STATIC
    http2_protocol_options: {}
    load_assignment:
      cluster_name: eds_server
      endpoints:
      - lb_endpoints:
        - endpoint:
            address:
              socket_address:
                address: 10.22.12.178
                port_value: 18000
...
  • type: EDS说明了使用EDS作为服务发现,而EDS的相关信息在cluster_name: eds_server这里定义
  • eds_server是静态的配置,访问10.22.12.178:10000就能够获取获取eds配置

验证

配置完之后,首先启动eds_server

▶ go run eds.go
2025/12/23 18:15:36 EDS server listening on :18000

修改envoy配置之后重启,检查eds_server的输出:

▶ go run eds.go
2025/12/23 18:15:36 EDS server listening on :18000
2025/12/23 18:17:33 EDS stream connected
2025/12/23 18:17:33 >>> Sending EDS response version=1766484936064308385, nonce=1766485053421230413
2025/12/23 18:17:33 DiscoveryRequest resources=[backend_cluster] version="" nonce=""
2025/12/23 18:17:33 DiscoveryRequest resources=[backend_cluster] version="1766484936064308385" nonce="1766485053421230413"

成功了,启动的envoy之后,envoy与eds_server建立连接,并且eds_server推送相关配置给envoy,再访问一下试试curl 10.22.12.178:30785/test

[2025-12-23T10:20:44.892Z] "GET /test HTTP/1.0" 200 40 1 c40a5dd3-29b7-4d1b-b73d-e93b31b5f6e3 "curl/7.81.0" "-" 10.244.0.111:10000 backend_cluster -
[2025-12-23T10:20:46.003Z] "GET /test HTTP/1.0" 200 40 1 1656452c-4571-469b-b2b7-3d43bd703c6d "curl/7.81.0" "-" 10.244.0.114:10000 backend_cluster -

EDS小结

watermarked-envoy_xDS_1

手搓了一个能够响应eds的服务,并且将envoy指向该服务,envoy也能够获取后端endpoint的地址,成功转发的请求

演示中的脚本,是将配置写死在代码中的

        s.endpoints = []*endpointpb.LbEndpoint{
                newEndpoint("10.244.0.111", 10000),
                newEndpoint("10.244.0.114", 10000),
        }

只需要将这部分改造一下。如果在k8s里面,那就watch k8s的endpoint,动态获取就行。如果是在k8s集群之外,可以封装一个web 容器,在页面上管理后端endpoint也行。总之,后端服务的配置,完全由eds接管,不管ip:port怎 么变化,只需要在eds服务中配置,就会推送至envoy,完成endpoint服务发现

envoy RDS

现在已经拥有了EDS服务,能够动态获取endpoint,但是http的路由配置依然是直接在配置文件里面的

...
    static_resources:
      listeners:
        - name: ingress_listener
          address:
            socket_address:
              address: 0.0.0.0
              port_value: 10000
          filter_chains:
            - filters:
                - name: envoy.filters.network.http_connection_manager
                  typed_config:
                    "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
                    stat_prefix: ingress_http
                    http_protocol_options:
                      accept_http_10: true
                    common_http_protocol_options:
                      idle_timeout: 300s
                    codec_type: AUTO
                    route_config:
                      name: local_route
                      virtual_hosts:
                        - name: app
                          domains: ["*"]
                          routes:
                            - match: { prefix: "/" }
                              route:
                                cluster: backend_cluster
...

比如想要修改match: { prefix: "/" },envoy并不会感知,还是需要重启。所以引入RDS服务,与EDS服务类似,自动发现HTTP路由配置

创建rds服务端

手搓一个简单的rds_server

rds服务

该脚本启动18001端口,接收gRPC请求,并且响应RDS,只要envoy来连接18001,就会下发http route到envoy

修改envoy配置

    static_resources:
      listeners:
        - name: ingress_listener
          address:
            socket_address:
              address: 0.0.0.0
              port_value: 10000
          filter_chains:
            - filters:
                - name: envoy.filters.network.http_connection_manager
                  typed_config:
                    "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
                    stat_prefix: ingress_http
                    http_protocol_options:
                      accept_http_10: true
                    codec_type: AUTO
                    rds:
                      route_config_name: local_route
                      config_source:
                        api_config_source:
                          api_type: GRPC
                          grpc_services:
                            - envoy_grpc:
                                cluster_name: rds_server

...

      clusters:
      ...
      - name: rds_server
        connect_timeout: 1s
        type: STATIC
        http2_protocol_options: {}
        load_assignment:
          cluster_name: rds_server
          endpoints:
          - lb_endpoints:
            - endpoint:
                address:
                  socket_address:
                    address: 10.22.12.178
                    port_value: 18001

验证

配置完之后,首先启动rds_server

▶ go run rds.go
2025/12/24 17:02:34 RDS server listening on :18001

修改envoy配置之后重启,检查rds_server的输出:

▶ go run rds.go
2025/12/24 17:02:34 RDS server listening on :18001
2025/12/24 17:02:55 RDS stream connected
2025/12/24 17:02:55 >>> Sending RDS response version=1766566954686151045, nonce=1766566975846610006
2025/12/24 17:02:55 DiscoveryRequest resources=[local_route] version="1766561174225337826" nonce=""
2025/12/24 17:02:55 DiscoveryRequest resources=[local_route] version="1766566954686151045" nonce="1766566975846610006"

成功了,启动的envoy之后,envoy与rds_server建立连接,并且rds_server推送相关配置给envoy,再访问一下试试curl 10.22.12.178:30785/test

[2025-12-24T09:03:16.252Z] "GET /test HTTP/1.0" 200 40 1 bea0ccf1-0621-4be1-919f-3dbb24e93ff5 "curl/7.81.0" "-" 10.244.0.114:10000 backend_cluster -
[2025-12-24T09:03:16.916Z] "GET /test HTTP/1.0" 200 40 1 f22c01e4-8120-4cb1-837e-a6c0b27f7410 "curl/7.81.0" "-" 10.244.0.111:10000 backend_cluster -

RDS小结

watermarked-envoy_xDS_2

演示中的脚本,是将配置写死在代码中的

                                                Match: &routepb.RouteMatch{
                                                        PathSpecifier: &routepb.RouteMatch_Prefix{
                                                                Prefix: "/test",
                                                        },
                                                },

只需要将这部分改造一下。如果在k8s里面,那就watch k8s的ingress,动态获取就行。如果是在k8s集群之外,可以封装一个web 容器,在页面上管理后端http router也行

envoy ADS

目前我们完成了EDS、RDS,可以自动发现对应的endpoint、http router资源,但是他们都是gRPC协议,能不能整合在一起呢?并且xDS还有其他的资源,什么CDS、LDS等等,每个种类都监听一次接口,管理难度也太冗余了。于是ADS就应运而生了,它是一个聚合发现服务,一个特殊的 gRPC 端点,将所有 xDS API 聚合在一起

创建ads服务端

ads服务

该脚本启动18000端口,接收gRPC请求,并且响应聚合请求ADS,再根据不同的查询类型(EDS、RDS等),响应不同的资源,并且下发到envoy

修改envoy的配置

这里修改较为复杂,直接给出配置文件即可

node:
  id: envoy-1
  cluster: demo-proxy

dynamic_resources:
  ads_config:
    api_type: GRPC
    grpc_services:
      - envoy_grpc:
          cluster_name: ads_server

static_resources:
  listeners:
    - name: ingress_listener
      address:
        socket_address:
          address: 0.0.0.0
          port_value: 10000
      filter_chains:
        - filters:
            - name: envoy.filters.network.http_connection_manager
              typed_config:
                "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
                stat_prefix: ingress_http
                http_protocol_options:
                  accept_http_10: true
                codec_type: AUTO
                rds:
                  route_config_name: local_route
                  config_source:
                    ads: {}
                http_filters:
                  - name: envoy.filters.http.router
                    typed_config:
                      "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
                access_log:
                - name: envoy.access_loggers.stdout
                  typed_config:
                    "@type": type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog
                    log_format:
                      text_format: "[%START_TIME%] \"%REQ(:METHOD)% %REQ(X-ENVOY-ORIGINAL-PATH?:PATH)% %PROTOCOL%\" %RESPONSE_CODE% %BYTES_SENT% %DURATION% %REQ(X-REQUEST-ID)% \"%REQ(USER-AGENT)%\" \"%REQ(X-FORWARDED-FOR)%\" %UPSTREAM_HOST% %UPSTREAM_CLUSTER% %RESPONSE_FLAGS%\n"

  clusters:
  - name: ads_server
    connect_timeout: 1s
    type: STATIC
    http2_protocol_options: {}
    load_assignment:
      cluster_name: ads_server
      endpoints:
        - lb_endpoints:
            - endpoint:
                address:
                  socket_address:
                    address: 10.22.12.178
                    port_value: 18000

  - name: backend_cluster
    type: EDS
    connect_timeout: 0.25s
    lb_policy: ROUND_ROBIN
    eds_cluster_config:
      eds_config:
        ads: {}

验证

首先启动ADS服务,再修改envoy配置,最后重启envoy服务。验证部分同EDS、ADS,这里就不赘述

ADS小结

watermarked-envoy_xDS_3

至此,通过ADS聚合服务,可以接受不同类型的xDS请求,在文中我们实现了EDS与RDS,当然如果有需求,可以持续的把LDS、CDS等全部加上

小结

“修改配置之后如何自动生效”,本文通过这一切入点,详细探讨了envoy的另外一种服务发现策略xDS,并且手搓了诸如EDS、RDS等服务,成功响应了envoy的需求,完成了配置生效。并且最终使用ADS,将EDS与RDS聚合在一起,形成了一个统一且管理型强的服务入口

后记

有位老哥说了,这不就是istio嘛?没错,istio的数据面就是使用envoy

所谓服务治理,也是从解决最基本的问题而诞生的,本系列从“记录后端真实pod ip”为切入口,通过常见的场景需求,不断的解决需求,发现问题,解决问题,最终将这些功能全部聚合一起,就是服务治理的基本框架

而问题的提出、解决问题的过程以及需求的满足,不光是服务治理,也是所有软件诞生的基本思想

联系我

  • 联系我,做深入的交流


至此,本文结束
在下才疏学浅,有撒汤漏水的,请各位不吝赐教...

posted @ 2025-12-29 11:04  it排球君  阅读(6)  评论(0)    收藏  举报