博客 / 詳情

返回

很多人用 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”為切入口,通過常見的場景需求,不斷的解決需求,發現問題,解決問題,最終將這些功能全部聚合一起,就是服務治理的基本框架

而問題的提出、解決問題的過程以及需求的滿足,不光是服務治理,也是所有軟件誕生的基本思想

聯繫我

  • 聯繫我,做深入的交流


至此,本文結束
在下才疏學淺,有撒湯漏水的,請各位不吝賜教...

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.