Envoy 代理中的請求的生命周期
Envoy 代理中的請求的生命周期
翻譯自Envoy官方文檔。
下面描述一個經過Envoy代理的請求的生命周期。首先會描述Envoy如何在請求路徑中處理請求,然後描述請求從下游到達Envoy代理之後發生的內部事件。我們將跟蹤該請求,直到其被分發到上游和響應路徑中。
術語
Envoy會在程式碼和文檔中使用如下術語:
- Cluster:邏輯上的服務,包含一系列的endpoints,Envoy會將請求轉發到這些Cluster上。
- Downstream:連接到Envoy的實體。可能是一個本地應用(sidecar模型)或網路節點。非sidecar模型下體現為一個遠端客戶端。
- Endpoints:實現了邏輯服務的網路節點。Endpoints組成了Clusters。一個Cluster中的Endpoints為某個Envoy代理的upstream。
- Filter:連接或請求處理流水線的一個模組,提供了請求處理的某些功能。類似Unix的小型實用程式(過濾器)和Unix管道(過濾器鏈)的組合。
- Filter chain:一些列Filters。
- Listeners:負責綁定一個IP/埠的Envoy模組,接收新的TCP連接(或UDP數據包)以及對下游的請求進行編排。
- Upstream:Envoy轉發請求到一個服務時連接的Endpoint。可能是一個本地應用或網路節點。非sidecar模型下體現為一個遠端endpoint。
網路拓撲
一個請求是如何通過一個網路組件取決於該網路的模型。Envoy可能會使用大量網路拓撲。下面會重點介紹Envoy的內部運作方式,但在本節中會簡要介紹Envoy與網路其餘部分的關係。
Envoy起源於服務網格Sidecar代理,用於剝離應用程式的負載平衡,路由,可觀察性,安全性和發現服務。在服務網格模型中,請求會經過作為網關的Envoy,或通過ingress或egress監聽器到達一個Envoy。
- ingress 監聽器會從其他節點接收請求,並轉發到本地應用。本地應用的響應會經過Envoy發回下游。
- Egress 監聽器會從本地應用接收請求,並轉發到網路的其他節點。這些接收請求的節點通常也會運行Envoy,並接收經過它們的ingress 監聽器的請求。
Envoy會用到除服務網格使用到的各種配置,例如,它可以作為一個內部的負載均衡器:
或作為一個邊緣網路的ingress/egress代理:
實際中,通常會在服務網格中混合使用Envoy的特性,在網格邊緣作為ingress/egress代理,以及在內部作為負載均衡器。一個請求路徑可能會經過多個Envoys。
Envoy可以配置為多層拓撲來實現可伸縮性和可靠性,一個請求會首先經過一個邊緣Envoy,然後傳遞給第二個Envoy層。
以上所有場景中,請求通過下游的TCP,UDP或Unix域套接字到達一個指定的Envoy,然後由該Envoy通過TCP,UDP或UNIX域套接字轉發到上游。下面僅關注單個Envoy代理。
配置
Envoy是一個可擴展的平台。通過如下條件可以組合成豐富的請求路徑:
- L3/4協議,即TCP,UDP,UNIX域套接字
- L7協議,即 HTTP/1, HTTP/2, HTTP/3, gRPC, Thrift, Dubbo, Kafka, Redis 以及各種資料庫
- 傳輸socket,即明文,TLS,ALTS
- 連接路由,即PROXY協議,源地址,動態轉發
- 斷路器以及異常值檢測配置和激活狀態
- 與網路相關的配置,HTTP, listener, 訪問日誌,健康檢查, 跟蹤和統計資訊擴展
本例將涵蓋如下內容:
- 在TCP連接上使用HTTP/2(帶TLS)的請求的上下游
- 使用HTTP連接管理器作為唯一的網路過濾器。
- 一個假定的CustomFilter,以及
router <arch_overview_http_routing>
(HTTP 過濾器鏈) - 文件系統訪問日誌
- Statsd sink
- 使用靜態endpoints的單個cluster
假設使用如下靜態的bootstrap配置文件,該配置僅包含一個listener和一個cluster。在listener中靜態指定了路由配置,在cluster靜態指定了endpoints。
static_resources:
listeners:
# There is a single listener bound to port 443.
- name: listener_https
address:
socket_address:
protocol: TCP
address: 0.0.0.0
port_value: 443
# A single listener filter exists for TLS inspector.
listener_filters:
- name: "envoy.filters.listener.tls_inspector"
typed_config: {}
# On the listener, there is a single filter chain that matches SNI for acme.com.
filter_chains:
- filter_chain_match:
# This will match the SNI extracted by the TLS Inspector filter.
server_names: ["acme.com"]
# Downstream TLS configuration.
transport_socket:
name: envoy.transport_sockets.tls
typed_config:
"@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext
common_tls_context:
tls_certificates:
- certificate_chain: { filename: "certs/servercert.pem" }
private_key: { filename: "certs/serverkey.pem" }
filters:
# The HTTP connection manager is the only network filter.
- 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
use_remote_address: true
http2_protocol_options:
max_concurrent_streams: 100
# File system based access logging.
access_log:
- name: envoy.access_loggers.file
typed_config:
"@type": type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog
path: "/var/log/envoy/access.log"
# The route table, mapping /foo to some_service.
route_config: # 靜態路由配置
name: local_route
virtual_hosts:
- name: local_service
domains: ["acme.com"]
routes:
- match:
path: "/foo"
route:
cluster: some_service
# CustomFilter and the HTTP router filter are the HTTP filter chain.
http_filters:
- name: some.customer.filter
- name: envoy.filters.http.router
clusters:
- name: some_service
connect_timeout: 5s
# Upstream TLS configuration.
transport_socket:
name: envoy.transport_sockets.tls
typed_config:
"@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
load_assignment:
cluster_name: some_service
# Static endpoint assignment.
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: 10.1.2.10
port_value: 10002
- endpoint:
address:
socket_address:
address: 10.1.2.11
port_value: 10002
http2_protocol_options:
max_concurrent_streams: 100
- name: some_statsd_sink
connect_timeout: 5s
# The rest of the configuration for statsd sink cluster.
# statsd sink.
stats_sinks:
- name: envoy.stat_sinks.statsd
typed_config:
"@type": type.googleapis.com/envoy.config.metrics.v3.StatsdSink
tcp_cluster_name: some_statsd_cluster
高層架構
Envoy中的請求處理主要包含兩大部分:
- Listener子系統:處理下游請求,同時負責管理下游請求的生命周期以及到客戶端的響應路徑。同時包含下游HTTP/2的編解碼器。
- Cluster子系統:負責選擇和配置到上游endpoint的連接,以及Cluster和endpoint的健康檢查,負載均衡和連接池。同時包含上游HTTP/2的編解碼器。
這兩個子系統與HTTP router filter橋接在一起,用於將HTTP請求從下游轉發到上游。
我們使用術語listener subsystem 和cluster subsystem 指代模組組以及由高層ListenerManager 和ClusterManager類創建的實例類。在下面討論的很多組件都是由這些管理系統在請求前和請求過程中實例化的,如監聽器, 過濾器鏈, 編解碼器, 連接池和負載均衡數據結構。
Envoy有一個基於事件的執行緒模型。主執行緒負責生命周期、配置處理、統計等。工作執行緒用於處理請求。所有執行緒都圍繞一個事件循環(libevent)進行操作,任何給定的下游TCP連接(包括其中的所有多路復用流),在其生命周期內都由一個工作執行緒進行處理。每個工作執行緒維護各自到上游endpoints的TCP連接池。UDP處理中會使用SO_REUSEPORT
,通過內核一致性哈希將源/目標IP:埠元組散列到同一個工作執行緒。UDP過濾器狀態會共享給特定的工作執行緒,過濾器負責根據需要提供會話語義。這與下面討論的面向連接的TCP過濾器形成了對比,後者的過濾器狀態以每個連接為基礎,在HTTP過濾器的情況下,則以每個請求為基礎。
工作執行緒很少會共享狀態,且很少會並行運行。 該執行緒模型可以擴展到core數量非常多的CPU。
請求流
總覽
使用上面的示例配置簡要概述請求和響應的生命周期:
- 由運行在一個工作執行緒的Envoy 監聽器接收下游TCP連接
- 創建並運行監聽過濾器鏈。該鏈可以提供SNI以及其他TLS之前的資訊。一旦完成,該監聽器會匹配到一個網路過濾器鏈。每個監聽器都可能具有多個過濾鏈,這些filter鏈會匹配目標IP CIDR範圍,SNI,ALPN,源埠等的某種組合。傳輸套接字(此例為TLS傳輸套接字)與該過濾器鏈相關聯。
- 在進行網路讀取時,TLS傳輸套接字會從TCP連接中解密數據,以便後續做進一步的處理。
- 創建並運行網路過濾器鏈。HTTP最重要的過濾器為HTTP連接管理器,它作為network filter鏈上的最後一個過濾器。
- HTTP連接管理器中的HTTP/2編解碼器將解密後的數據流從TLS連接上解幀並解復用為多個獨立的流。每個流處理一個單獨的請求和響應。
- 對於每個HTTP流,會創建並運行一個HTTP 過濾器鏈。請求會首先經過CustomFilter,該過濾器可能會讀取並修改請求。最重要的HTTP過濾器是路由過濾器,位於HTTP 過濾器鏈的末尾。當路由過濾器調用
decodeHeaders
時,會選擇路由和cluster。數據流中的請求首部會轉發到上游cluster對應的endpoint中。router 過濾器會從群集管理器中為匹配的cluster獲取HTTP連接池。 - Cluster會指定負載均衡來查找endpoint。cluster的斷路器會檢查是否允許一個新的流。如果endpoint的連接池為空或容量不足,則會創建一個到該endpoint的新連接。
- 上游endpoint連接的HTTP/2編解碼器會對請求的流(以及通過單個TCP連接到該上游的其他流)進行多路復用和幀化。
- 上游endpoint連接的TLS傳輸socket會加密這些位元組並寫入到上游連接的TCP socket中。
- 請求包含首部,可選的消息體和尾部,通過代理到達上游,並通過代理對下游進行響應。響應會以與請求相反的順序通過HTTP過濾器,從路由過濾器開始,然後經過CustomFilter。
- 完成響應後會銷毀流,更新統計資訊,寫入訪問日誌並最終確定跟蹤範圍。
我們將在以下各節中詳細介紹每個步驟。
1.Listener TCP連接的接收
ListenerManager負責獲取監聽器的配置,並實例化綁定到各自IP/埠的多個Listener
實例。監聽器的狀態可能為:
- Warming:監聽器等待配置依賴(即,路配置,動態secret)。此時監聽器無法接收TCP連接。
- Active:監聽器綁定到其IP/埠,可以接收TCP連接。
- Draining:監聽器不再接收新的TCP連接,現有的TCP連接可以在一段時間內繼續使用。
每個工作執行緒會為每個監聽器維護各自的監聽器實例。每個監聽器可能通過SO_REUSEPORT 綁定到相同的埠,或共享綁定到該埠的socket。當接收到一個新的TCP連接,內核會決定哪個工作執行緒來接收該連接,然後由該工作執行緒對應的監聽器調用Server::ConnectionHandlerImpl::ActiveTcpListener::onAccept()
。
2.監聽過濾鏈和網路過濾器鏈的匹配
工作執行緒的監聽器然後會創建並運行監聽過濾器鏈。過濾器鏈是通過每個過濾器的過濾器工廠創建的,該工廠會感知過濾器的配置,並為每個連接或流創建新的過濾器實例。
在TLS 過濾器配置下,監聽過濾器鏈會包含TLS檢查過濾器(envoy.filters.listener.tls_inspector
)。該過濾器會檢查初始的TLS握手,並抽取server name(SNI),然後使用SNI進行過濾器鏈的匹配。儘管tls_inspector
會明確出現在監聽過濾器鏈配置中,但Envoy還能夠在監聽過濾器鏈需要SNI(或ALPN)時自動將其插入。
TLS檢查過濾器實現了 ListenerFilter介面。所有的過濾器介面,無論是監聽或網路/HTTP過濾器,都需要實現特定連接或流事件的回調方法,ListenerFilter中為:
virtual FilterStatus onAccept(ListenerFilterCallbacks& cb) PURE;
onAccept()
允許在TCP accept處理時運行一個過濾器。回調方法的FilterStatus
控制監聽過濾器鏈將如何運行。監聽過濾器可能會暫停過濾器鏈,後續再恢復運行,如響應另一個服務進行的RPC請求。
在過濾器鏈進行匹配時,會抽取監聽過濾器和連接的屬性,提供給用於處理連接的網路過濾器鏈和傳輸socket。
3.TLS傳輸socket的解密
Envoy通過TransportSocket擴展介面提供了插件式的傳輸socket。傳輸socket遵循TCP連接的生命周期事件,使用網路buffer進行讀寫。傳輸socket需要實現如下關鍵方法:
virtual void onConnected() PURE;
virtual IoResult doRead(Buffer::Instance& buffer) PURE;
virtual IoResult doWrite(Buffer::Instance& buffer, bool end_stream) PURE;
virtual void closeSocket(Network::ConnectionEvent event) PURE;
當一個TCP連接可以傳輸數據時,Network::ConnectionImpl::onReadReady()
會通過SslSocket::doRead()
調用TLS傳輸socket。傳輸socket然後會在TCP連接上進行TLS握手。TLS握手結束後,SslSocket::doRead()
會給Network::FilterManagerImpl
實例提供一個解密的位元組流,該實例負責管理網路過濾器鏈。
需要注意的是,無論是TLS握手還是過濾器處理流程的暫停,都不會真正阻塞任何操作。由於Envoy是基於事件的,因此任何需要額外數據才能進行處理的情況都將導致提前完成事件,並將CPU轉移給另一個事件。如當網路提供了更多的可讀數據時,該讀事件將會觸發TLS握手恢復。
4.網路過濾器鏈的處理
與監聽過濾器鏈相同,Envoy會通過Network::FilterManagerImpl,從對應的過濾器工廠實例化一些列網路過濾器。每個新連接的實例都是新的。與傳輸socket相同,網路過濾器也會遵循TCP的生命周期事件,並在來自傳輸socket中的數據可用時被喚醒。
網路過濾器包含一個pipeline,與一個連接一個的傳輸socket不同,網路過濾器分為三種:
- ReadFilter:實現了
onData()
,當連接中的數據可用時(由於某些請求)被調用 - WriteFilter:實現了
onWrite()
, 當給連接寫入數據時(由於某些響應)被調用 - Filter:實現了ReadFilter和WriteFilter.
關鍵過濾器方法的方法簽名為:
virtual FilterStatus onNewConnection() PURE;
virtual FilterStatus onData(Buffer::Instance& data, bool end_stream) PURE;
virtual FilterStatus onWrite(Buffer::Instance& data, bool end_stream) PURE;
與監聽過濾器類似,FilterStatus
允許過濾器暫停過濾器鏈的執行。例如,如果需要查詢限速服務,限速網路過濾器將會從onData()
中返回Network::FilterStatus::StopIteration
,並在請求結束後調用continueReading()
。
HTTP的監聽器的最後一個網路過濾器是HTTP連接管理器(HCM)。該過濾器負責創建HTTP/2編解碼器並管理HTTP過濾器鏈。在上面的例子中,它是唯一的網路過濾器。使用多個網路過濾器的網路過濾器鏈類似:
在響應路徑中,網路過濾器執行的順序與請求路徑相反
5.HTTP/2編解碼器的解碼
Envoy的HTTP/2編解碼器基於nghttp2,當TCP連接使用明文位元組時(經過網路過濾器鏈變換後),會被HCM調用。編解碼器將位元組流解碼為一系列HTTP/2幀,並將連接解復用為多個獨立的HTTP流。流復用是HTTP/2的一個關鍵特性,與HTTP/1相比具有明顯的性能優勢。每個HTTP流會處理一個單獨的請求和響應。
編解碼器也負責處理HTTP/2設置幀,以及連接級別的流控制。
編解碼器負責抽象HTTP連接的細節,向HTTP連接管理器展示標準視圖,並將連接的HTTP過濾器鏈拆分為多個流,每個流都有請求/響應標頭/正文/尾部(無論協議是HTTP/1,HTTP/2還是HTTP/3 )。
6.HTTP過濾器鏈的處理
對於每個HTTP流,HCM會實例化一個HTTP過濾器鏈,遵循上面為監聽器和網路過濾器鏈建立的模式。
HTTP過濾器介面有三種類型:
- StreamDecoderFilter:包含請求處理的回調方法
- StreamEncoderFilter:包含響應處理的回調方法
- StreamFilter:同時實現了StreamDecoderFilter和StreamEncoderFilter.
查看解碼器過濾器介面:
virtual FilterHeadersStatus decodeHeaders(RequestHeaderMap& headers, bool end_stream) PURE;
virtual FilterDataStatus decodeData(Buffer::Instance& data, bool end_stream) PURE;
virtual FilterTrailersStatus decodeTrailers(RequestTrailerMap& trailers) PURE;
HTTP過濾器遵循HTTP請求的生命周期,而不針對連接緩衝區和事件進行操作,如decodeHeaders()
使用HTTP首部作為參數,而不是位元組buffer。與網路和監聽過濾器一樣,返回的FilterStatus
提供了管理過濾器鏈控制流的功能。
當可以使用HTTP/2編解碼器處理HTTP請求首部時,會首先傳遞給在CustomFilter中的decodeHeaders()
。如果返回的FilterHeadersStatus
為Continue
,則HCM會將首部(可能會被CustomFilter修改)傳遞給路由過濾器。
解碼器和編解碼過濾器運行在請求路徑上,編碼器和編碼解碼過濾器運行在響應路徑上。考慮如下過濾器鏈:
請求路徑為:
響應路徑為:
當在路由過濾器中調用decodeHeaders()
時,會確定路由選擇並挑選cluster。HCM會在HTTP過濾器鏈執行開始時從RouteConfiguration
中選擇一個路由,該路由被稱為快取路由。過濾器可能會通過要求HCM清除路由快取並請求HCM重新評估路由選擇來修改首部,並導致選擇一個新的路由。當調用路由過濾器時,也就確定了路由。顯選擇的路由會指向一個上游cluster名稱。然後路由過濾器會向ClusterManager 為該cluster請求一個HTTP連接池。該過程涉及負載平衡和連接池,將在下一節中討論。
HTTP連接池是用來在router中構建一個UpstreamRequest對象,該對象封裝了用於處理上游HTTP請求的HTTP編碼和解碼的回調方法。一旦在HTTP連接池的連接上分配了一個流,則會通過UpstreamRequest::encoderHeaders()
將請求首部轉發到上游endpoint。
路由過濾器負責(從HTTP連接池上分配的流上的)到上游的請求的生命周期管理,同時也負責請求超時,重試和親和性等。
7.負載均衡
每個cluster都有一個負載均衡,當接收到一個請求時會選擇一個endpoint。Envoy支援多種類型的負載均衡演算法,如基於權重的輪詢,Maglev,負載最小,隨機等演算法。負載均衡會從靜態的bootstrap配置,DNS,動態xDS以及主動/被動健康檢查中獲得其需要處理的內容。更多詳情參見官方文檔。
一旦選擇了endpoint,會使用連接池來為該endpoint選擇一個連接來轉發請求。如果沒有到該主機的連接,或所有的連接已經達到了並發流的上線,則會建立一條新的流,並將它放到連接池裡(除非觸發了群集的最大連接的斷路器)。如果配置了流的最大生命時間,且已經達到了該時間點,那麼此時會在連接池中分配一個新的連接,並終止舊的HTTP/2連接。此外還會檢查其他斷路器,如到一個cluster的最大並發請求等。
8.HTTP/2 編解碼器的編碼
連接的HTTP/2的編解碼器會對單條TCP連接上的到達相同上游的其他請求流進行多路復用,與HTTP/2編解碼器的解碼是相反的
與下游HTTP/2編解碼器一樣,上游的編解碼器負責採用Envoy的HTTP標準抽象,即多個流在單個連接上與請求/響應標頭/正文/尾部進行復用,並通過生成一系列HTTP/2幀將其映射到HTTP/2的細節中。
9.TLS傳輸socket的加密
上游endpoint連接的TLS傳輸socket會加密來自HTTP/2編解碼器的輸出,並將其寫入到上游連接的TCP socket中。 與TLS傳輸套接字的解碼一樣,在我們的示例中,群集配置了傳輸套接字,用來提供TLS傳輸的安全性。上游和下游傳輸socket擴展都存在相同的介面。
10.響應路徑和HTTP生命周期
請求包含首部,可選擇的主體和尾部,通過代理到上游,並將響應代理到下游。響應會通過以與請求相反的順序經過HTTP和network過濾器。
HTTP過濾器會調用解碼器/編碼器請求生命周期事件的各種回調,例如 當轉發響應尾部或請求主體被流式傳輸時。類似地,讀/寫network過濾器還將在數據在請求期間繼續在兩個方向上流動時調用它們各自的回調。
endpoint的異常檢測狀態會隨著請求的進行而修改。
當上游響應到達流的末端後即完成了一個請求。即接收到尾部或帶有最終流集的響應頭/主體時。這個流程在Router::Filter::onUpstreamComplete()
在進行處理。
一個請求有可能提前結束,可能的原因為:
- 請求超時
- 上游endpoint的流被重置
- HTTP過濾器流被重置
- 出發斷路器
- 不可用的上遊資源,如缺少路由指定的cluster
- 不健康的endpoints
- Dos攻擊
- 無效的HTTP協議
- 通過HCM或HTTP過濾器進行了本地回復。如HTTP過濾器可能會因為頻率限制而返回429響應。
如果上游響應還沒有發送,則Envoy會原因生成一個內部的響應;如果響應首部已經轉發到了下游,則會重置流。更多參見Envoy的調試FAQ。
11.請求後的處理
一旦請求完成,則流會被銷毀。發生的事件如下:
- 更新請求後的統計(如時間,活動的請求,更新,檢查檢查等)。但有些統計會在請求過程中進行更新。此時尚未將統計資訊寫入統計接收器,它們由主執行緒定期進行批處理和寫入。在上述示例中,這是一個statsd接收器。
- 將訪問日誌寫入訪問日誌接收器,在上述示例中,為一個文件訪問日誌。
- 確定trace spans。如果上述例子進行了請求追蹤,則會生成一個trace span,描述了請求的持續時間和細節,在處理請求首部是HCM會創建trace span,並在請求後處理過程中由HCM進行最終確定。