Envoy 基礎入門教程

Envoy 是一個用 C++ 開發的高性能代理,Envoy 是一種 L7 代理和通信總線,專爲大型的現代面向服務的架構而設計。

核心能力

Envoy 的誕生源於以下理念:

網絡對於應用程序來說應該是透明的,當網絡和應用程序出現問題時,應該很容易確定問題的源頭。

當然要實現上述目標是非常困難的。Envoy 試圖通過提供以下高級功能來實現這一目標:

非侵入架構: Envoy 是一個獨立的進程,設計爲伴隨每個應用程序服務一起運行。所有 Envoy 實例形成一個透明的通信網格,每個應用程序通過 localhost 發送和接收消息,不需要知道網絡拓撲。對服務的實現語言也完全無感知,這種模式也被稱爲 Sidecar 模式。

Envoy Sidecar

L3/L4 過濾器架構: Envoy 的核心是一個 L3/L4 層的網絡代理。可插拔的過濾器鏈機制允許編寫不同的 TCP/UDP 代理任務的過濾器,並將其插入到主服務器中。而且已經內置支持了各種任務的過濾器,例如原始 TCP 代理、UDP 代理、HTTP 代理、TLS 客戶端證書身份驗證、Redis、MongoDB、Postgres 等。

HTTP L7 過濾器架構: HTTP 是現代應用程序架構的關鍵組件,因此 Envoy 支持了一個額外的 HTTP L7 過濾器層。HTTP 過濾器可以被插入到 HTTP 連接管理子系統中,執行不同的任務,如緩存、速率限制、路由 / 轉發、嗅探 Amazon 的 DynamoDB 等。

頂級的 HTTP/2 支持: 在 HTTP 模式下運行時,Envoy 同時支持 HTTP/1.1 和 HTTP/2。Envoy 可以作爲透明的 HTTP/1.1 到 HTTP/2 雙向代理運行。這意味着可以連接任何組合的 HTTP/1.1 和 HTTP/2 客戶端與目標服務器。推薦的服務到服務配置在所有 Envoy 之間使用 HTTP/2 創建持久連接網格,請求和響應可以在該連接上進行多路複用。

HTTP/3 支持(目前處於 alpha 版): 從 Envoy 1.19.0 版本開始,Envoy 現在支持上游和下游的 HTTP/3,而且可以在任何方向上進行 HTTP/1.1、HTTP/2 和 HTTP/3 之間的轉換。

HTTP L7 路由: 在 HTTP 模式下運行時,Envoy 支持路由子系統,該子系統能夠根據路徑、權限、內容類型、運行時值等路由和重定向請求。在使用 Envoy 作爲前端 / 邊緣代理時,此功能非常有用,但在構建服務到服務的網格時也可以利用它。

gRPC 支持: gRPC 是 Google 的一個 RPC 框架,使用 HTTP/2 或更高版本作爲底層多路複用傳輸。Envoy 支持用作 gRPC 請求和響應的路由和負載均衡基礎所需的所有 HTTP/2 功能,這兩個系統非常互補。

服務發現和動態配置: Envoy 可以選擇使用一組分層的動態配置 API 來進行集中管理。這些層向 Envoy 提供了關於後端集羣中的主機、後端集羣自身、HTTP 路由、監聽套接字和加密材料的動態更新。對於更簡單的部署,可以通過 DNS 解析(甚至完全跳過)來完成後端主機發現,並且進一步的層可以由靜態配置文件替代。

健康檢查: 構建 Envoy 網格的推薦方法是將服務發現視爲最終一致的過程。Envoy 包含一個健康檢查子系統,可以選擇對上游服務集羣執行主動健康檢查。然後,Envoy 使用服務發現和健康檢查信息的結合來確定健康的負載均衡目標。Envoy 還通過異常值檢測子系統支持被動健康檢查。

高級負載均衡: 分佈式系統中不同組件之間的負載均衡是一個複雜的問題。由於 Envoy 是一個獨立的代理而不是庫,因此可以獨立實現高級負載均衡以供任何應用程序訪問。目前 Envoy 支持自動重試、熔斷、通過外部速率限制服務進行全局速率限制、異常檢測等。

前端 / 邊緣代理支持: 在邊緣使用相同的軟件有很大的好處(可觀察性、管理、相同的服務發現和負載均衡算法等)。Envoy 的功能集使其非常適合作爲大多數現代 Web 應用程序用例的邊緣代理。這包括 TLS 終止、HTTP/1.1、HTTP/2 和 HTTP/3 支持以及 HTTP L7 路由。

最佳的可觀測性: 如上所述,Envoy 的主要目標是使網絡透明化。但是,問題在網絡層面和應用層面都可能會出現。Envoy 爲所有子系統提供了強大的統計支持。目前支持的統計數據輸出端是 statsd(以及兼容的提供程序),但是接入其他不同的統計數據輸出端並不困難。統計數據也可以通過管理端口進行查看,Envoy 還支持通過第三方提供者進行分佈式跟蹤。

常用術語

在我們介紹 Envoy 架構之前,有必要先介紹一些常用的術語定義,因爲這些術語貫穿整個 Envoy 的架構設計。

架構設計

Envoy 採用單進程多線程架構。

一個獨立的 primary 線程負責控制各種零散的協調任務,而一些 worker 線程則負責執行監聽、過濾和轉發任務。

一旦偵聽器接受連接,該連接就會將其生命週期綁定到一個單獨的 worker 線程。這使得 Envoy 的大部分工作基本上是單線程來處理的,只有少量更復雜的代碼處理工作線程之間的協調。

通常情況下 Envoy 實現了 100% 非阻塞。對於大多數工作負載,我們建議將 worker 線程的數量配置爲機器上的硬件線程數量。

Envoy 整體架構如下圖所示:

Envoy 架構

Envoy 進程中運行着一系列 Inbound/Outbound 監聽器(Listener),Inbound 代理入站流量,Outbound 代理出站流量。Listener 的核心就是過濾器鏈(FilterChain),鏈中每個過濾器都能夠控制流量的處理流程

Envoy 接收到請求後,會先走 FilterChain,通過各種 L3/L4/L7 Filter 對請求進行處理,然後再路由到指定的集羣,並通過負載均衡獲取一個目標地址,最後再轉發出去。

其中每一個環節可以靜態配置,也可以動態服務發現,也就是所謂的 xDS,這裏的 x 是一個代詞,是 ldsrdscdsedssds 的總稱,即服務發現,後 2 個字母 ds 就是 discovery service

第一個 Envoy 代理

下面我們通過一個簡單的示例來介紹 Envoy 的基本使用。

配置

Envoy 使用 YAML 文件來控制代理的行爲,整體配置結構如下:

listen -- 監聽器
    1.我監聽的地址
    2.過濾鏈
        filter1
            路由: 轉發到哪裏
                virtual_hosts
                    只轉發什麼
                    轉發到哪裏 --> 由後面的 cluster 來定義
        filter2
        filter3
        # envoyproxy.io/docs/envoy/v1.28.0/api-v3/config/filter/filter
cluster
    轉發規則
    endpoints
        --指定了我的後端地址

接下來我們就來創建一個簡單的 Envoy 代理,它監聽 10000 端口,將請求轉發到 www.baidu.com 的 80 端口。在下面的步驟中,我們將使用靜態配置接口來構建配置,也意味着所有設置都是預定義在配置文件中的。此外 Envoy 也支持動態配置,這樣可以通過外部一些源來自動發現進行設置。

Envoy 代理使用開源 xDS API 來交換信息,目前 xDS v2 已被廢棄,最新版本的 Envoy 不再支持 xDS v2,建議使用 xDS v3。

創建一個名爲 envoy-1.yaml 的文件,在 Envoy 配置的第一行定義正在使用的接口配置,在這裏我們將配置靜態 API,因此第一行應爲 static_resources

static_resources:

然後需要在靜態配置下面定義 Envoy 的監聽器(Listener),監聽器是 Envoy 監聽請求的網絡配置,例如 IP 地址和端口。我們這裏設置監聽 IP 地址爲 0.0.0.0,並在端口 10000 上進行監聽。對應的監聽器的配置爲

static_resources:
  listeners:
    - name: listener_0 # 監聽器的名稱
      address:
        socket_address:
          address: 0.0.0.0 # 監聽器的地址
          port_value: 10000 # 監聽器的端口

通過 Envoy 監聽傳入的流量,下一步是定義如何處理這些請求。每個監聽器都有一組過濾器,並且不同的監聽器可以具有一組不同的過濾器

在我們這個示例中,我們將所有流量代理到 baidu.com,配置完成後我們應該能夠通過請求 Envoy 的端點就可以直接看到百度的主頁了,而無需更改 URL 地址。

過濾器是通過 filter_chains 來定義的,每個過濾器的目的是找到傳入請求的匹配項,以使其與目標地址進行匹配。Filter 過濾器的寫法如下所示:

name: 指定使用哪個過濾器
typed_config:
  "@type": type.googleapis.com/envoy.過濾器的具體值
  參數1:值1
  參數2:值2
  。。。

這裏選擇什麼參數,要看name裏選擇的什麼參數要根據所選擇的過濾器來判定

和 http 相關的,一般選擇 HTTP connection manager。

在 https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/filter/filter 裏找參數

name 的位置應該寫 envoy.filters.network.http_connection_manager

@type 的值到文檔裏找具體的值

比如我們這裏的配置如下所示:

static_resources:
  listeners:
    - name: listener_0 # 監聽器的名稱
      address:
        socket_address:
          address: 0.0.0.0 # 監聽器的地址
          port_value: 10000 # 監聽器的端口

      filter_chains: # 配置過濾器鏈
        # 在此地址收到的任何請求都會通過這一系列過濾鏈發送。
        - filters:
            # 指定要使用哪個過濾器,下面是envoy內置的網絡過濾器,如果請求是 HTTP 它將通過此 HTTP 過濾器
            # 該過濾器將原始字節轉換爲HTTP級別的消息和事件(例如接收到的header、接收到的正文數據等)
            # 它還處理所有HTTP連接和請求中常見的功能,例如訪問日誌記錄、請求ID生成和跟蹤、請求/響應頭操作、路由表管理和統計信息。
            - name: envoy.filters.network.http_connection_manager
              typed_config:
                # 需要配置下面的類型,啓用 http_connection_manager
                "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
                stat_prefix: ingress_http
                access_log: # 連接管理器發出的 HTTP 訪問日誌的配置
                  - name: envoy.access_loggers.stdout # 輸出到stdout
                    typed_config:
                      "@type": type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog
                http_filters: # 定義http過濾器鏈
                  - name: envoy.filters.http.router # 調用7層的路由過濾器
                    typed_config:
                      "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
                route_config:
                  name: local_route
                  virtual_hosts:
                    - name: local_service
                      domains: ["*"] # 要匹配的主機名列表,*表示匹配所有主機
                      routes:
                        - match:
                            prefix: "/" # 要匹配的 URL 前綴
                          route: # 路由規則,發送請求到 service_baidu 集羣
                            host_rewrite_literal: www.baidu.com # 更改 HTTP 請求的入站 Host 頭信息
                            cluster: service_baidu # 將要處理請求的集羣名稱,下面會有相應的實現

這裏我們使用的過濾器使用了 envoy.filters.network.http_connection_manager,這是爲 HTTP 連接設計的一個內置過濾器,該過濾器將原始字節轉換爲 HTTP 級別的消息和事件(例如接收到的 header、接收到的正文數據等),它還處理所有 HTTP 連接和請求中常見的功能,例如訪問日誌記錄、請求 ID 生成和跟蹤、請求 / 響應頭操作、路由表管理和統計信息。

當請求於過濾器匹配時,該請求將會傳遞到集羣。下面的配置就是將主機定義爲訪問 HTTPS 的 baidu.com 域名,如果定義了多個主機,則 Envoy 將執行輪詢(Round Robin)策略。配置如下所示:

clusters:
  - name: service_baidu # 集羣的名稱,與上面的 router 中的 cluster 對應
    type: LOGICAL_DNS # 用於解析集羣(生成集羣端點)時使用的服務發現類型,可用值有STATIC、STRICT_DNS 、LOGICAL_DNS、ORIGINAL_DST和EDS等;
    connect_timeout: 0.25s
    dns_lookup_family: V4_ONLY
    lb_policy: ROUND_ROBIN # 負載均衡算法,支持ROUND_ROBIN、LEAST_REQUEST、RING_HASH、RANDOM、MAGLEV和CLUSTER_PROVIDED;
    load_assignment: # 以前的 v2 版本的 hosts 字段廢棄了,現在使用 load_assignment 來定義集羣的成員,指定 STATIC、STRICT_DNS 或 LOGICAL_DNS 集羣的成員需要設置此項。
      cluster_name: service_baidu # 集羣的名稱
      endpoints: # 需要進行負載均衡的端點列表
        - lb_endpoints:
            - endpoint:
                address:
                  socket_address:
                    address: www.baidu.com
                    port_value: 443
    transport_socket: # 用於與上游集羣通信的傳輸層配置
      name: envoy.transport_sockets.tls # tls 傳輸層
      typed_config:
        "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
        sni: www.baidu.com

最後,還需要配置一個管理模塊:

admin:
  access_log_path: /tmp/admin_access.log
  address:
    socket_address:
      address: 0.0.0.0
      port_value: 9901

上面的配置定義了 Envoy 的靜態配置模板,監聽器定義了 Envoy 的端口和 IP 地址,監聽器具有一組過濾器來匹配傳入的請求,匹配請求後,將請求轉發到集羣,完整的配置如下所示:

# envoy-1.yaml
admin:
  access_log_path: /tmp/admin_access.log
  address:
    socket_address:
      address: 0.0.0.0
      port_value: 9901

static_resources:
  listeners:
    - name: listener_0 # 監聽器的名稱
      address:
        socket_address:
          address: 0.0.0.0 # 監聽器的地址
          port_value: 10000 # 監聽器的端口

      filter_chains: # 配置過濾器鏈
        - filters:
            # 過濾器配置的名稱,要填寫 typed_config 配置的過濾器指定的名稱
            - name: envoy.filters.network.http_connection_manager
              typed_config:
                # 啓用 http_connection_manager
                "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
                stat_prefix: ingress_http
                access_log:
                  - name: envoy.access_loggers.stdout
                    typed_config:
                      "@type": type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog
                http_filters: # 定義http過濾器鏈
                  - name: envoy.filters.http.router # 調用7層的路由過濾器
                    typed_config:
                      "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
                route_config:
                  name: local_route
                  virtual_hosts:
                    - name: local_service
                      domains: ["*"]
                      routes:
                        - match:
                            prefix: "/"
                          route:
                            host_rewrite_literal: www.baidu.com
                            cluster: service_baidu

  clusters:
    - name: service_baidu # 集羣的名稱
      type: LOGICAL_DNS # 用於解析集羣(生成集羣端點)時使用的服務發現類型,可用值有STATIC、STRICT_DNS 、LOGICAL_DNS、ORIGINAL_DST和EDS等;
      connect_timeout: 0.25s
      dns_lookup_family: V4_ONLY
      lb_policy: ROUND_ROBIN # 負載均衡算法,支持ROUND_ROBIN、LEAST_REQUEST、RING_HASH、RANDOM、MAGLEV和CLUSTER_PROVIDED;
      load_assignment:
        cluster_name: service_baidu
        endpoints:
          - lb_endpoints:
              - endpoint:
                  address:
                    socket_address:
                      address: www.baidu.com
                      port_value: 443
      transport_socket:
        name: envoy.transport_sockets.tls
        typed_config:
          "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
          sni: www.baidu.com

第一次使用 Envoy,可能會覺得它的配置太複雜了,讓人眼花繚亂。其實只要我們理解了網絡代理程序的流程就不難配置了,比如作爲一個代理,首先要能獲取請求流量,通常是採用監聽端口的方式實現;其次拿到請求數據後需要對其做微處理,例如附加 Header 或校驗某個 Header 字段的內容等,這裏針對來源數據的層次不同,可以分爲 L3/L4/L7,然後將請求轉發出去;轉發這裏又可以衍生出如果後端是一個集羣,需要從中挑選一臺機器,如何挑選又涉及到負載均衡等。

腦補完大致流程後,再來看 Envoy 是如何組織配置信息的,我們再來解釋一下其中的關鍵字段。

結合關鍵字段和上面的腦補流程,可以看出 Envoy 的大致處理流程如下:

Envoy 配置流程

Envoy 內部對請求的處理流程其實跟我們上面腦補的流程大致相同,即對請求的處理流程基本是不變的,而對於變化的部分,即對請求數據的微處理,全部抽象爲 Filter,例如對請求的讀寫是 ReadFilterWriteFilter,對 HTTP 請求數據的編解碼是 StreamEncoderFilterStreamDecoderFilter,對 TCP 的處理是 TcpProxyFilter,其繼承自 ReadFilter,對 HTTP 的處理是 ConnectionManager,其也是繼承自 ReadFilter 等等,各個 Filter 最終會組織成一個 FilterChain,在收到請求後首先走 FilterChain,其次路由到指定集羣並做負載均衡獲取一個目標地址,然後轉發出去。

啓動 Envoy

配置完成後,我們就可以去啓動 Envoy 了,首先當然需要去安裝 Envoy 了,因爲 Envoy 是 C++ 開發的,編譯起來非常麻煩,如果是 Mac 用戶可以使用 brew install envoy 來一鍵安裝,但是最簡單的方式還是使用 Docker 來啓動 Envoy。

我們這裏也通過 Docker 容器來啓動 Envoy,將上面的配置文件通過 Volume 掛載到容器中的 /etc/envoy/envoy.yaml 去。

然後使用以下命令啓動綁定到端口 80 的 Envoy 容器:

$ docker run --name=envoy -d \
  -p 80:10000 \
  -v $(pwd)/manifests/2.Envoy/envoy-1.yaml:/etc/envoy/envoy.yaml \
  envoyproxy/envoy:v1.28.0

啓動後,我們可以在本地的 80 端口上去訪問應用 curl localhost 來測試代理是否成功。同樣我們也可以通過在本地瀏覽器中訪問 localhost 來查看:

localhost

可以看到請求被代理到了 baidu.com,而且應該也可以看到 URL 地址沒有變化,還是 localhost,查看 Envoy 日誌可以看到如下信息:

[2023-10-25T06:53:50.003Z] "GET / HTTP/1.1" 200 - 0 103079 399 235 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36" "e081fa5b-31a4-4285-92d9-b8a8c896f2d4" "www.baidu.com" "110.242.68.3:443"
[2023-10-25T06:53:50.819Z] "GET /sugrec?&prod=pc_his&from=pc_web&json=1&sid=&hisdata=%5B%7B%22time%22%3A1698206660%2C%22kw%22%3A%22envovy%20typed_config%22%2C%22fq%22%3A2%7D%5D&_t=1698216830777&req=2&csor=0 HTTP/1.1" 200 - 0 155 57 57 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36" "9a9351e7-e7ef-4fa8-9aec-fba96600e4df" "www.baidu.com" "110.242.68.3:443"

此外 Envoy 還提供了一個管理視圖,可以讓我們去查看配置、統計信息、日誌以及其他 Envoy 內部的一些數據。上面我們定義的管理視圖的端口爲 9901,當然我們也可以通過 Docker 容器將管理端口暴露給外部用戶。

docker run --name=envoy -d \
  -p 9901:9901 \
  -p 80:10000 \
  -v $(pwd)/manifests/2.Envoy/envoy-1.yaml:/etc/envoy/envoy.yaml \
  envoyproxy/envoy:v1.28.0

上面的配置就會將管理頁面暴露給外部用戶,當然我們這裏僅僅用於演示是可以的,如果你是用於線上環境還需要做好一些安全保護措施。運行成功後,現在我們可以在瀏覽器裏面輸入 localhost:9901 來訪問 Envoy 的管理頁面:

envoy admin

需要注意的是當前的管理頁面不僅允許執行一些破壞性的操作(比如,關閉服務),而且還可能暴露一些私有信息(比如統計信息、集羣名稱、證書信息等)。所以應該只允許通過安全網絡去訪問管理頁面。

遷移 NGINX 到 Envoy

因爲現階段大部分的應用可能還是使用的比較傳統的 Nginx 來做服務代理,爲了對比 Envoy 和 Nginx 的區別,我們這裏將來嘗試將 Nginx 的配置遷移到 Envoy 上來,這樣也有助於我們去了解 Envoy 的配置。

首先我們使用 Nginx 官方 Wiki 的完整示例來進行說明,完整的 nginx.conf 配置如下所示:

user  www www;
pid /var/run/nginx.pid;
worker_processes  2;

events {
  worker_connections   2000;
}

http {
  gzip on;
  gzip_min_length  1100;
  gzip_buffers     4 8k;
  gzip_types       text/plain;

  log_format main      '$remote_addr - $remote_user [$time_local]  '
    '"$request" $status $bytes_sent '
    '"$http_referer" "$http_user_agent" '
    '"$gzip_ratio"';

  log_format download  '$remote_addr - $remote_user [$time_local]  '
    '"$request" $status $bytes_sent '
    '"$http_referer" "$http_user_agent" '
    '"$http_range" "$sent_http_content_range"';

  upstream targetCluster {
    192.168.215.3:80;
    192.168.215.4:80;
  }

  server {
    listen        8080;
    server_name   one.example.com  www.one.example.com;

    access_log   /var/log/nginx.access_log  main;
    error_log  /var/log/nginx.error_log  info;

    location / {
      proxy_pass         http://targetCluster/;
      proxy_redirect     off;

      proxy_set_header   Host             $host;
      proxy_set_header   X-Real-IP        $remote_addr;
    }
  }
}

上面的 Nginx 配置有 3 個核心配置:

並不是所有的 Nginx 的配置都適用於 Envoy,有些方面的配置我們可以不用。Envoy 代理主要有 4 中主要的配置類型,它們是支持 Nginx 提供的核心基礎結構的:

我們將使用這 4 個組件來創建 Envoy 代理配置,去匹配 Nginx 中的配置。Envoy 的重點一直是在 API 和動態配置上,但是我們這裏仍然使用靜態配置。

Nginx 配置的核心是 HTTP 配置配置,裏面包含了:

我們可以通過 Envoy 代理中的過濾器來配置這些內容。在 HTTP 配置部分,Nginx 配置指定了監聽的端口 8080,並響應域名 one.example.comwww.one.example.com 的傳入請求:

server {
    listen        8080;
    server_name   one.example.com  www.one.example.com;
    ......
}

在 Envoy 中,這部分就是監聽器來管理的。開始一個 Envoy 代理最重要的方面就是定義監聽器,我們需要創建一個配置文件來描述我們如何去運行 Envoy 實例。

這裏我們定義一個 static_resources 配置,它是 Envoy 配置的根節點,它包含了所有的靜態配置,包括監聽器、集羣、路由等。我們將創建一個新的監聽器並將其綁定到 8080 端口上,該配置指示了 Envoy 代理用於接收網絡請求的端口,如下所示:

static_resources:
  listeners:
    - name: listener_0 # 監聽器的名稱
      address:
        socket_address:
          address: 0.0.0.0 # 監聽器的地址
          port_value: 8080 # 監聽器的端口

需要注意的是我們沒有在監聽器部分定義 server_name,這需要在過濾器部分進行處理。

當請求進入 Nginx 時,location 部分定義瞭如何處理流量以及在什麼地方轉發流量。在下面的配置中,站點的所有傳入流量都將被代理到一個名爲 targetCluster 的上游(upstream)集羣,上游集羣定義了處理請求的節點。

location / {
    proxy_pass         http://targetCluster/;
    proxy_redirect     off;

    proxy_set_header   Host             $host;
    proxy_set_header   X-Real-IP        $remote_addr;
}

在 Envoy 中,這部分將由過濾器來進行配置管理。在靜態配置中,過濾器定義瞭如何處理傳入的請求,在我們這裏,將配置一個過濾器去匹配上一步中的 server_names,當接收到與定義的域名和路由匹配的傳入請求時,流量將轉發到集羣,集羣和 Nginx 配置中的 upstream 是一致的。

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_filters: # 定義http過濾器鏈
            - name: envoy.filters.http.router # 調用7層的路由過濾器
              typed_config:
                "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
          route_config:
            name: local_route
            virtual_hosts:
              - name: backend
                domains:
                  - "one.example.com"
                  - "www.one.example.com"
                routes:
                  - match:
                      prefix: "/"
                    route:
                      cluster: targetCluster

其中 envoy.filters.network.http_connection_manager 是 Envoy 內置的一個過濾器,用於處理 HTTP 連接的,除此之外,還有其他的一些內置的過濾器,比如 Redis、Mongo、TCP。

在 Nginx 中,upstream(上游)配置定義了處理請求的目標服務器集羣,在我們這裏的示例中,分配了兩個集羣。

upstream targetCluster {
  192.168.215.3:80;
  192.168.215.4:80;
}

在 Envoy 代理中,這部分是通過 clusters 進行配置管理的。upstream 等同與 Envoy 中的 clusters 定義,我們這裏通過集羣定義了主機被訪問的方式,還可以配置超時和負載均衡等方面更精細的控制。

clusters:
  - name: targetCluster
    connect_timeout: 0.25s
    type: STRICT_DNS
    dns_lookup_family: V4_ONLY
    lb_policy: ROUND_ROBIN
    load_assignment:
      cluster_name: targetCluster
      endpoints:
        - lb_endpoints:
            - endpoint:
                address:
                  socket_address:
                    address: 192.168.215.3
                    port_value: 80
            - endpoint:
                address:
                  socket_address:
                    address: 192.168.215.4
                    port_value: 80

上面我們配置了 STRICT_DNS 類型的服務發現,Envoy 會持續異步地解析指定的 DNS 目標。DNS 解析結果返回的每個 IP 地址都將被視爲上游集羣的主機。所以如果返回兩個 IP 地址,則 Envoy 將認爲集羣有兩個主機,並且兩個主機都應進行負載均衡,如果從結果中刪除了一個主機,則 Envoy 會從現有的連接池中將其剔出掉。

最後需要配置的日誌部分,Envoy 採用雲原生的方式,將應用程序日誌都輸出到 stdoutstderr,而不是將錯誤日誌輸出到磁盤。

當用戶發起一個請求時,訪問日誌默認是被禁用的,我們可以手動開啓。要爲 HTTP 請求開啓訪問日誌,需要在 HTTP 連接管理器中包含一個 access_log 的配置,該路徑可以是設備,比如 stdout,也可以是磁盤上的某個文件,這依賴於我們自己的實際情況。

下面過濾器中的配置就會將所有訪問日誌通過管理傳輸到 stdout

- name: envoy.filters.network.http_connection_manager
  typed_config:
    # 啓用 http_connection_manager
    "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
    stat_prefix: ingress_http
    access_log:
      - name: envoy.access_loggers.stdout
        typed_config:
          "@type": type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog
    http_filters: # 定義http過濾器鏈
      - name: envoy.filters.http.router # 調用7層的路由過濾器
        typed_config:
          "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
    route_config:
    # ......

默認情況下,Envoy 訪問日誌格式包含整個 HTTP 請求的詳細信息:

[%START_TIME%] "%REQ(:METHOD)% %REQ(X-ENVOY-ORIGINAL-PATH?:PATH)% %PROTOCOL%"
%RESPONSE_CODE% %RESPONSE_FLAGS% %BYTES_RECEIVED% %BYTES_SENT% %DURATION%
%RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)"%REQ(X-FORWARDED-FOR)%" "%REQ(USER-AGENT)%"
"%REQ(X-REQUEST-ID)%" "%REQ(:AUTHORITY)%" "%UPSTREAM_HOST%"\n

輸出結果格式化後如下所示:

[2023-10-25T07:25:09.826Z] "GET / HTTP/1.1" 200 - 0 102931 361 210 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36" "6e19ddda-e0a1-41f9-9355-ea5db8d23bcc" "one.example.com" "192.168.215.4:80"

我們也可以通過設置 format 字段來自定義輸出日誌的格式,例如:

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% %RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)% %REQ(X-REQUEST-ID)% %REQ(:AUTHORITY)% %UPSTREAM_HOST%\n"

此外我們也可以通過設置 json_format 字段來將日誌作爲 JSON 格式輸出,例如:

access_log:
  - name: envoy.access_loggers.stdout
    typed_config:
      "@type": type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog
      log_format:
        json_format:
          {
            "protocol""%PROTOCOL%",
            "duration""%DURATION%",
            "request_method""%REQ(:METHOD)%",
          }

要注意的是,訪問日誌會在未設置、或者空值的位置加入一個字符:-。不同類型的訪問日誌(例如 HTTP 和 TCP)共用同樣的格式字符串。不同類型的日誌中,某些字段可能會有不同的含義。有關 Envoy 日誌的更多信息,可以查看官方文檔對應的說明。當然日誌並不是 Envoy 代理獲得請求可見性的唯一方法,Envoy 還內置了高級跟蹤和指標功能。

最後我們完整的 Envoy 配置如下所示:

# envoy-2.yaml
static_resources:
  listeners:
    - name: listener_0 # 監聽器的名稱
      address:
        socket_address:
          address: 0.0.0.0 # 監聽器的地址
          port_value: 8080 # 監聽器的端口

      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
                access_log:
                  - name: envoy.access_loggers.stdout
                    typed_config:
                      "@type": type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog
                http_filters: # 定義http過濾器鏈
                  - name: envoy.filters.http.router # 調用7層的路由過濾器
                    typed_config:
                      "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
                route_config:
                  name: local_route
                  virtual_hosts:
                    - name: backend
                      domains:
                        - "one.example.com"
                        - "www.one.example.com"
                      routes:
                        - match:
                            prefix: "/"
                          route:
                            cluster: targetCluster

  clusters:
    - name: targetCluster
      connect_timeout: 0.25s
      type: STRICT_DNS
      dns_lookup_family: V4_ONLY
      lb_policy: ROUND_ROBIN
      load_assignment:
        cluster_name: targetCluster
        endpoints:
          - lb_endpoints:
              - endpoint:
                  address:
                    socket_address:
                      address: 192.168.215.3
                      port_value: 80
              - endpoint:
                  address:
                    socket_address:
                      address: 192.168.215.4
                      port_value: 80

現在我們已經將 Nginx 配置轉換爲了 Envoy 代理,接下來我們可以來啓動 Envoy 代理進行測試驗證。

在 Nginx 配置的頂部,有一行配置 user www www;,表示用非 root 用戶來運行 Nginx 以提高安全性。而 Envoy 代理採用雲原生的方法來管理使用這,我們通過容器啓動 Envoy 代理的時候,可以指定一個低特權的用戶。

下面的命令將通過 Docker 容器來啓動一個 Envoy 實例,該命令使 Envoy 可以監聽 80 端口上的流量請求,但是我們在 Envoy 的監聽器配置中指定的是 8080 端口,所以我們用一個低特權用戶身份來運行:

$ docker run --name proxy1 -p 80:8080 --user 1000:1000 -v $(pwd)/manifests/2.Envoy/envoy-2.yaml:/etc/envoy/envoy.yaml envoyproxy/envoy:v1.28.0

啓動代理後,就可以開始測試了,下面我們用 curl 命令使用代理配置的 host 頭髮起一個網絡請求:

$ curl -H "Host: one.example.com" localhost -i
HTTP/1.1 503 Service Unavailable
content-length: 91
content-type: text/plain
date: Wed, 25 Oct 2023 07:37:55 GMT
server: envoy

upstream connect error or disconnect/reset before headers. reset reason: connection timeout%

我們可以看到會出現 503 錯誤,這是因爲我們配置的上游集羣主機根本就沒有運行,所以 Envoy 代理請求到不可用的主機上去了,就出現了這樣的錯誤。我們可以使用下面的命令啓動兩個 HTTP 服務,用來表示上游主機:

$ docker run -d cnych/docker-http-server; docker run -d cnych/docker-http-server;
$ docker ps
CONTAINER ID   IMAGE                      COMMAND                  CREATED         STATUS         PORTS                                              NAMES
3ecf1125bd0c   cnych/docker-http-server   "/app"                   2 minutes ago   Up 2 minutes   80/tcp                                             loving_babbage
0195f14ec57a   cnych/docker-http-server   "/app"                   2 minutes ago   Up 2 minutes   80/tcp                                             heuristic_torvalds
a55f6175c5c7   envoyproxy/envoy:v1.28.0   "/docker-entrypoint.…"   3 minutes ago   Up 3 minutes   10000/tcp, 0.0.0.0:80->8080/tcp, :::80->8080/tcp   proxy1

當上面兩個服務啓動成功後,現在我們再通過 Envoy 去訪問目標服務就正常了:

$ curl -H "Host: one.example.com" localhost -i
HTTP/1.1 200 OK
date: Wed, 25 Oct 2023 07:42:51 GMT
content-length: 58
content-type: text/html; charset=utf-8
x-envoy-upstream-service-time: 1
server: envoy

<h1>This request was processed by host: 3ecf1125bd0c</h1>
$ curl -H "Host: one.example.com" localhost -i
HTTP/1.1 200 OK
date: Wed, 25 Oct 2023 07:42:53 GMT
content-length: 58
content-type: text/html; charset=utf-8
x-envoy-upstream-service-time: 8
server: envoy

<h1>This request was processed by host: 0195f14ec57a</h1>

當訪問請求的時候,我們可以看到是哪個容器處理了請求,在 Envoy 代理容器中,也可以看到請求的日誌輸出:

[2023-10-25T07:42:51.297Z] "GET / HTTP/1.1" 200 - 0 58 3 1 "-" "curl/7.87.0" "ff1e1009-d5a3-4a71-87ef-479e234c9858" "one.example.com" "192.168.215.4:80"
[2023-10-25T07:42:53.153Z] "GET / HTTP/1.1" 200 - 0 58 9 8 "-" "curl/7.87.0" "d0ebdd10-a1d2-406e-8c6e-6fd8451aded3" "one.example.com" "192.168.215.3:80"

到這裏我們就完成了將 Nginx 配置遷移到 Envoy 的過程,可以看到 Envoy 的配置和 Nginx 的配置還是有很大的區別的,但是我們可以看到 Envoy 的配置更加的靈活,而且 Envoy 代理的配置是可以動態更新的,這樣就可以實現無縫的服務升級。

使用 SSL/TLS 保護流量

接下來我們將來了解下如何使用 Envoy 保護 HTTP 網絡請求。確保 HTTP 流量安全對於保護用戶隱私和數據是至關重要的。下面我們來了解下如何在 Envoy 中配置 SSL 證書。

SSL 證書

這裏我們將爲 example.com 域名生成一個自簽名的證書,當然如果在生產環境時候,需要使用正規 CA 機構購買的證書,或者使用 Let's Encrypt 的免費證書服務。

下面的命令會在目錄 certs/ 中創建一個新的證書和密鑰:

$ mkdir certs; cd certs;
$ openssl req -nodes -new -x509 \
  -keyout example-com.key -out example-com.crt \
  -days 365 \
  -subj '/CN=example.com/O=youdianzhishi.com/C=CN';
  Generating a RSA private key
....+..+....+..+.......+.....+...+....+++++++++++++++++++++++++++++++++++++++*....+...+................+......+........+...+...+.+...+......+.....+......+.+.....+++++++++++++++++++++++++++++++++++++++*............+.....+................+....................+......+....+..............+.+..+...+...............+.......+............+...+...+......+.....+....+..+....+.........+....................+.+......+..+...+.........+.+..+..........+...............+..+...+.+......+...+..+......+.......+............+......+..+......+......++++++
.....+............+.+..+....+.....+.+.........+.....+++++++++++++++++++++++++++++++++++++++*...+...+............+.....+.+...........+...+...+...............+...+......+.........+.+.....+...+.+......+...+...+++++++++++++++++++++++++++++++++++++++*........+..........+...+...+......+.........++++++
-----
$ cd -

流量保護

在 Envoy 中保護 HTTP 流量,需要通過添加 transport_socket 過濾器,該過濾器提供了爲 Envoy 代理中配置的域名指定證書的功能,請求 HTTPS 請求時候,就使用匹配的證書。我們這裏直接使用上一步中生成的自簽名證書即可。

我們這裏的 Envoy 配置文件中包含了所需的 HTTPS 支持的配置,我們添加了兩個監聽器,一個監聽器在 8080 端口上用於 HTTP 通信,另外一個監聽器在 8443 端口上用於 HTTPS 通信。

在 HTTPS 監聽器中定義了 HTTP 連接管理器,該代理將代理 /service/1/service/2 這兩個端點的傳入請求,這裏我們需要通過 envoy.transport_sockets.tls 配置相關證書,如下所示:

transport_socket:
  name: envoy.transport_sockets.tls
  typed_config:
    # 一個監聽傳輸套接字,用於使用 TLS 接受下游連接(客戶端)。
    "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext
    common_tls_context:
      tls_certificates:
        - certificate_chain:
            filename: "/etc/certs/example-com.crt"
          private_key:
            filename: "/etc/certs/example-com.key"

在 TLS 上下文中定義了生成的證書和密鑰,如果我們有多個域名,每個域名都有自己的證書,則需要通過 tls_certificates 定義多個證書鏈。

自動跳轉

定義了 TLS 上下文後,該站點將能夠通過 HTTPS 提供流量了,但是如果用戶是通過 HTTP 來訪問的服務,爲了確保安全,我們可以將其重定向到 HTTPS 版本服務上去。

在 HTTP 配置中,我們將 https_redirect: true 的標誌添加到過濾器的配置中即可實現跳轉功能。

route_config:
  virtual_hosts:
    - name: backend
      domains:
        - "example.com"
      routes:
        - match:
            prefix: "/"
          redirect:
            path_redirect: "/"
            https_redirect: true

當用戶訪問網站的 HTTP 版本時,Envoy 代理將根據過濾器配置來匹配域名和路徑,匹配到過後將請求重定向到站點的 HTTPS 版本去。完整的 Envoy 配置如下所示:

# envoy-3.yaml
admin:
  access_log_path: /tmp/admin_access.log
  address:
    socket_address:
      address: 0.0.0.0
      port_value: 8001

static_resources:
  listeners:
    - name: listener_http # 監聽器的名稱
      address:
        socket_address:
          address: 0.0.0.0 # 監聽器的地址
          port_value: 8080 # 監聽器的端口

      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
                access_log:
                  - name: envoy.access_loggers.stdout
                    typed_config:
                      "@type": type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog
                http_filters: # 定義http過濾器鏈
                  - name: envoy.filters.http.router # 調用7層的路由過濾器
                    typed_config:
                      "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
                route_config:
                  name: local_route
                  virtual_hosts:
                    - name: backend
                      domains:
                        - "example.com"
                      routes:
                        - match:
                            prefix: "/"
                          redirect:
                            path_redirect: "/"
                            https_redirect: true

    - name: listener_https # 監聽器的名稱
      address:
        socket_address:
          address: 0.0.0.0 # 監聽器的地址
          port_value: 8443 # 監聽器的端口

      filter_chains:
        - transport_socket:
            name: envoy.transport_sockets.tls
            typed_config:
              # 一個監聽傳輸套接字,用於使用 TLS 接受下游連接(客戶端)。
              "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext
              common_tls_context:
                tls_certificates:
                  - certificate_chain:
                      filename: "/etc/certs/example-com.crt"
                    private_key:
                      filename: "/etc/certs/example-com.key"
          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
                access_log:
                  - name: envoy.access_loggers.stdout
                    typed_config:
                      "@type": type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog
                http_filters: # 定義http過濾器鏈
                  - name: envoy.filters.http.router # 調用7層的路由過濾器
                    typed_config:
                      "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
                route_config:
                  name: local_route
                  virtual_hosts:
                    - name: backend
                      domains:
                        - "example.com"
                      routes:
                        - match:
                            prefix: "/service/1"
                          route:
                            cluster: service1
                        - match:
                            prefix: "/service/2"
                          route:
                            cluster: service2

  clusters:
    - name: service1
      connect_timeout: 0.25s
      type: STRICT_DNS
      dns_lookup_family: V4_ONLY
      lb_policy: ROUND_ROBIN
      load_assignment:
        cluster_name: service1
        endpoints:
          - lb_endpoints:
              - endpoint:
                  address:
                    socket_address:
                      address: 192.168.215.3
                      port_value: 80
    - name: service2
      connect_timeout: 0.25s
      type: STRICT_DNS
      dns_lookup_family: V4_ONLY
      lb_policy: ROUND_ROBIN
      load_assignment:
        cluster_name: service2
        endpoints:
          - lb_endpoints:
              - endpoint:
                  address:
                    socket_address:
                      address: 192.168.215.4
                      port_value: 80

測試

現在配置已經完成了,我們就可以啓動 Envoy 實例來進行測試了。在我們這個示例中,Envoy 暴露 80 端口來處理 HTTP 請求,暴露 443 端口來處理 HTTPS 請求,此外還在 8001 端口上暴露了管理頁面,我們可以通過管理頁面查看有關證書的信息。

使用如下命令啓動 Envoy 代理:

$ docker run -it --name tls-proxy -p 80:8080 -p 443:8443 -p 8001:8001 -v $(pwd)/manifests/2.Envoy/certs:/etc/certs/ -v $(pwd)/manifests/2.Envoy/envoy-3.yaml:/etc/envoy/envoy.yaml envoyproxy/envoy:v1.28.0

啓動完成後所有的 HTTPS 和 TLS 校驗都是通過 Envoy 來進行處理的,所以我們不需要去修改應該程序。同樣我們啓動兩個 HTTP 服務來處理傳入的請求:

$ docker run -d cnych/docker-http-server; docker run -d cnych/docker-http-server;
$ docker ps
CONTAINER ID   IMAGE                      COMMAND                  CREATED          STATUS          PORTS                                                                                                                                  NAMES
1690f6562870   cnych/docker-http-server   "/app"                   32 seconds ago   Up 32 seconds   80/tcp                                                                                                                                 distracted_ride
f86925657e62   cnych/docker-http-server   "/app"                   32 seconds ago   Up 32 seconds   80/tcp                                                                                                                                 festive_almeida
d02e37b26b22   envoyproxy/envoy:v1.28.0   "/docker-entrypoint.…"   3 minutes ago    Up 3 minutes    0.0.0.0:8001->8001/tcp, :::8001->8001/tcp, 10000/tcp, 0.0.0.0:80->8080/tcp, :::80->8080/tcp, 0.0.0.0:443->8443/tcp, :::443->8443/tcp   tls-proxy

上面的幾個容器啓動完成後,就可以進行測試了,首先我們請求 HTTP 的服務,由於配置了自動跳轉,所以應該會被重定向到 HTTPS 的版本上去:

$ curl -H "Host: example.com" http://localhost -i
HTTP/1.1 301 Moved Permanently
location: https://example.com/
date: Wed, 25 Oct 2023 08:30:48 GMT
server: envoy
content-length: 0

我們可以看到上面有 HTTP/1.1 301 Moved Permanently 這樣的重定向響應信息。然後我們嘗試直接請求 HTTPS 的服務:

$ curl -k -H "Host: example.com" https://localhost/service/1 -i
HTTP/1.1 200 OK
date: Wed, 25 Oct 2023 08:31:17 GMT
content-length: 58
content-type: text/html; charset=utf-8
x-envoy-upstream-service-time: 24
server: envoy

<h1>This request was processed by host: f86925657e62</h1>

$ curl -k -H "Host: example.com" https://localhost/service/2 -i
HTTP/1.1 200 OK
date: Wed, 25 Oct 2023 08:31:28 GMT
content-length: 58
content-type: text/html; charset=utf-8
x-envoy-upstream-service-time: 22
server: envoy

<h1>This request was processed by host: 1690f6562870</h1>

我們可以看到通過 HTTPS 進行訪問可以正常得到對應的響應,需要注意的是由於我們這裏使用的是自簽名的證書,所以需要加上 -k 參數來忽略證書校驗,如果沒有這個參數則在請求的時候會報錯:

$ curl -H "Host: example.com" https://localhost/service/2 -i
curl: (60) SSL certificate problem: self signed certificate
More details here: https://curl.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.

我們也可以通過管理頁面去查看證書相關的信息,上面我們啓動容器的時候綁定了宿主機的 8001 端口,所以我們可以通過訪問 http://localhost:8001/certs 來獲取到證書相關的信息:

證書信息

到這裏我們就基本瞭解了 Envoy 的靜態配置方式,接下來的 xDS 纔是 Envoy 中的精華部分。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/qx0MxAdKn2OnQmoAkJuG_w