Envoy 是一個用 C++ 開發的高性能代理,Envoy 是一種 L7 代理和通信總線,專為大型的現代面向服務的架構而設計。
Envoy 的誕生源于以下理念:
網絡對于應用程序來說應該是透明的,當網絡和應用程序出現問題時,應該很容易確定問題的源頭。
當然要實現上述目標是非常困難的。Envoy 試圖通過提供以下高級功能來實現這一目標:
非侵入架構: Envoy 是一個獨立的進程,設計為伴隨每個應用程序服務一起運行。所有 Envoy 實例形成一個透明的通信網格,每個應用程序通過 localhost 發送和接收消息,不需要知道網絡拓撲。對服務的實現語言也完全無感知,這種模式也被稱為 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 是一個代詞,是 lds、rds、cds、eds、sds 的總稱,即服務發現,后 2 個字母 ds 就是 discovery service。
下面我們通過一個簡單的示例來介紹 Envoy 的基本使用。
Envoy 使用 YAML 文件來控制代理的行為,整體配置結構如下:
listen -- 監聽器 1.我監聽的地址 2.過濾鏈 filter1 路由: 轉發到哪里 virtual_hosts 只轉發什么 轉發到哪里 --> 由后面的 cluster 來定義 filter2 filter3 # envoyproxy.io/docs/envoy/v1.28.0/api-v3/config/filter/filtercluster 轉發規則 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.filterswork.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.filterswork.http_connection_manager typed_config: # 需要配置下面的類型,啟用 http_connection_manager "@type": type.googleapis.com/envoy.extensions.filterswork.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.filterswork.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.yamladmin: access_log_path: /tmp/admin_access.log address: socket_address: address: 0.0.0.0 port_value: 9901static_resources: listeners: - name: listener_0 # 監聽器的名稱 address: socket_address: address: 0.0.0.0 # 監聽器的地址 port_value: 10000 # 監聽器的端口 filter_chains: # 配置過濾器鏈 - filters: # 過濾器配置的名稱,要填寫 typed_config 配置的過濾器指定的名稱 - name: envoy.filterswork.http_connection_manager typed_config: # 啟用 http_connection_manager "@type": type.googleapis.com/envoy.extensions.filterswork.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,例如對請求的讀寫是 ReadFilter、WriteFilter,對 HTTP 請求數據的編解碼是 StreamEncoderFilter、StreamDecoderFilter,對 TCP 的處理是 TcpProxyFilter,其繼承自 ReadFilter,對 HTTP 的處理是 ConnectionManager,其也是繼承自 ReadFilter 等等,各個 Filter 最終會組織成一個 FilterChain,在收到請求后首先走 FilterChain,其次路由到指定集群并做負載均衡獲取一個目標地址,然后轉發出去。
配置完成后,我們就可以去啟動 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&jsnotallow=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 的區別,我們這里將來嘗試將 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.com 和 www.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.filterswork.http_connection_manager typed_config: "@type": type.googleapis.com/envoy.extensions.filterswork.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.filterswork.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 采用云原生的方式,將應用程序日志都輸出到 stdout 和 stderr,而不是將錯誤日志輸出到磁盤。
當用戶發起一個請求時,訪問日志默認是被禁用的,我們可以手動開啟。要為 HTTP 請求開啟訪問日志,需要在 HTTP 連接管理器中包含一個 access_log 的配置,該路徑可以是設備,比如 stdout,也可以是磁盤上的某個文件,這依賴于我們自己的實際情況。
下面過濾器中的配置就會將所有訪問日志通過管理傳輸到 stdout:
- name: envoy.filterswork.http_connection_manager typed_config: # 啟用 http_connection_manager "@type": type.googleapis.com/envoy.extensions.filterswork.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.yamlstatic_resources: listeners: - name: listener_0 # 監聽器的名稱 address: socket_address: address: 0.0.0.0 # 監聽器的地址 port_value: 8080 # 監聽器的端口 filter_chains: - filters: - name: envoy.filterswork.http_connection_manager typed_config: "@type": type.googleapis.com/envoy.extensions.filterswork.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 -iHTTP/1.1 503 Service Unavailablecontent-length: 91content-type: text/plaindate: Wed, 25 Oct 2023 07:37:55 GMTserver: envoyupstream 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 psCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES3ecf1125bd0c cnych/docker-http-server "/app" 2 minutes ago Up 2 minutes 80/tcp loving_babbage0195f14ec57a cnych/docker-http-server "/app" 2 minutes ago Up 2 minutes 80/tcp heuristic_torvaldsa55f6175c5c7 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 -iHTTP/1.1 200 OKdate: Wed, 25 Oct 2023 07:42:51 GMTcontent-length: 58content-type: text/html; charset=utf-8x-envoy-upstream-service-time: 1server: envoy<h1>This request was processed by host: 3ecf1125bd0c</h1>$ curl -H "Host: one.example.com" localhost -iHTTP/1.1 200 OKdate: Wed, 25 Oct 2023 07:42:53 GMTcontent-length: 58content-type: text/html; charset=utf-8x-envoy-upstream-service-time: 8server: 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 代理的配置是可以動態更新的,這樣就可以實現無縫的服務升級。
接下來我們將來了解下如何使用 Envoy 保護 HTTP 網絡請求。確保 HTTP 流量安全對于保護用戶隱私和數據是至關重要的。下面我們來了解下如何在 Envoy 中配置 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.yamladmin: access_log_path: /tmp/admin_access.log address: socket_address: address: 0.0.0.0 port_value: 8001static_resources: listeners: - name: listener_http # 監聽器的名稱 address: socket_address: address: 0.0.0.0 # 監聽器的地址 port_value: 8080 # 監聽器的端口 filter_chains: - filters: - name: envoy.filterswork.http_connection_manager typed_config: "@type": type.googleapis.com/envoy.extensions.filterswork.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.filterswork.http_connection_manager typed_config: "@type": type.googleapis.com/envoy.extensions.filterswork.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 psCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES1690f6562870 cnych/docker-http-server "/app" 32 seconds ago Up 32 seconds 80/tcp distracted_ridef86925657e62 cnych/docker-http-server "/app" 32 seconds ago Up 32 seconds 80/tcp festive_almeidad02e37b26b22 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 -iHTTP/1.1 301 Moved Permanentlylocation: https://example.com/date: Wed, 25 Oct 2023 08:30:48 GMTserver: envoycontent-length: 0
我們可以看到上面有 HTTP/1.1 301 Moved Permanently 這樣的重定向響應信息。然后我們嘗試直接請求 HTTPS 的服務:
$ curl -k -H "Host: example.com" https://localhost/service/1 -iHTTP/1.1 200 OKdate: Wed, 25 Oct 2023 08:31:17 GMTcontent-length: 58content-type: text/html; charset=utf-8x-envoy-upstream-service-time: 24server: envoy<h1>This request was processed by host: f86925657e62</h1>$ curl -k -H "Host: example.com" https://localhost/service/2 -iHTTP/1.1 200 OKdate: Wed, 25 Oct 2023 08:31:28 GMTcontent-length: 58content-type: text/html; charset=utf-8x-envoy-upstream-service-time: 22server: envoy<h1>This request was processed by host: 1690f6562870</h1>
我們可以看到通過 HTTPS 進行訪問可以正常得到對應的響應,需要注意的是由于我們這里使用的是自簽名的證書,所以需要加上 -k 參數來忽略證書校驗,如果沒有這個參數則在請求的時候會報錯:
$ curl -H "Host: example.com" https://localhost/service/2 -icurl: (60) SSL certificate problem: self signed certificateMore details here: https://curl.se/docs/sslcerts.htmlcurl failed to verify the legitimacy of the server and therefore could notestablish a secure connection to it. To learn more about this situation andhow to fix it, please visit the web page mentioned above.
我們也可以通過管理頁面去查看證書相關的信息,上面我們啟動容器的時候綁定了宿主機的 8001 端口,所以我們可以通過訪問 http://localhost:8001/certs 來獲取到證書相關的信息:
證書信息
到這里我們就基本了解了 Envoy 的靜態配置方式,接下來的 xDS 才是 Envoy 中的精華部分。
本文鏈接:http://www.tebozhan.com/showinfo-26-15768-0.htmlEnvoy 基礎入門教程,看這一篇就夠了
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。郵件:2376512515@qq.com
上一篇: 為什么 IT 項目仍然失敗
下一篇: Git詳細使用教程,你學會了嗎?