Envoy 代理中的請求的生命週期

charlieroro發表於2020-09-19

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, 訪問日誌,健康檢查, 跟蹤和統計資訊擴充套件

本例將涵蓋如下內容:

假設使用如下靜態的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 subsystemcluster subsystem 指代模組組以及由高層ListenerManagerClusterManager類建立的例項類。在下面討論的很多元件都是由這些管理系統在請求前和請求過程中例項化的,如監聽器, 過濾器鏈, 編解碼器, 連線池和負載均衡資料結構。

Envoy有一個基於事件的執行緒模型。主執行緒負責生命週期、配置處理、統計等。工作執行緒用於處理請求。所有執行緒都圍繞一個事件迴圈(libevent)進行操作,任何給定的下游TCP連線(包括其中的所有多路複用流),在其生命週期內都由一個工作執行緒進行處理。每個工作執行緒維護各自到上游endpoints的TCP連線池。UDP處理中會使用SO_REUSEPORT,通過核心一致性雜湊將源/目標IP:埠元組雜湊到同一個工作執行緒。UDP過濾器狀態會共享給特定的工作執行緒,過濾器負責根據需要提供會話語義。這與下面討論的面向連線的TCP過濾器形成了對比,後者的過濾器狀態以每個連線為基礎,在HTTP過濾器的情況下,則以每個請求為基礎。

工作執行緒很少會共享狀態,且很少會並行執行。 該執行緒模型可以擴充套件到core數量非常多的CPU。

請求流

總覽

使用上面的示例配置簡要概述請求和響應的生命週期:

  1. 由執行在一個工作執行緒的Envoy 監聽器接收下游TCP連線
  2. 建立並執行監聽過濾器鏈。該鏈可以提供SNI以及其他TLS之前的資訊。一旦完成,該監聽器會匹配到一個網路過濾器鏈。每個監聽器都可能具有多個過濾鏈,這些filter鏈會匹配目標IP CIDR範圍,SNI,ALPN,源埠等的某種組合。傳輸套接字(此例為TLS傳輸套接字)與該過濾器鏈相關聯。
  3. 在進行網路讀取時,TLS傳輸套接字會從TCP連線中解密資料,以便後續做進一步的處理。
  4. 建立並執行網路過濾器鏈。HTTP最重要的過濾器為HTTP連線管理器,它作為network filter鏈上的最後一個過濾器。
  5. HTTP連線管理器中的HTTP/2編解碼器將解密後的資料流從TLS連線上解幀並解複用為多個獨立的流。每個流處理一個單獨的請求和響應。
  6. 對於每個HTTP流,會建立並執行一個HTTP 過濾器鏈。請求會首先經過CustomFilter,該過濾器可能會讀取並修改請求。最重要的HTTP過濾器是路由過濾器,位於HTTP 過濾器鏈的末尾。當路由過濾器呼叫decodeHeaders時,會選擇路由和cluster。資料流中的請求首部會轉發到上游cluster對應的endpoint中。router 過濾器會從群集管理器中為匹配的cluster獲取HTTP連線池
  7. Cluster會指定負載均衡來查詢endpoint。cluster的斷路器會檢查是否允許一個新的流。如果endpoint的連線池為空或容量不足,則會建立一個到該endpoint的新連線。
  8. 上游endpoint連線的HTTP/2編解碼器會對請求的流(以及通過單個TCP連線到該上游的其他流)進行多路複用和幀化。
  9. 上游endpoint連線的TLS傳輸socket會加密這些位元組並寫入到上游連線的TCP socket中。
  10. 請求包含首部,可選的訊息體和尾部,通過代理到達上游,並通過代理對下游進行響應。響應會以與請求相反的順序通過HTTP過濾器,從路由過濾器開始,然後經過CustomFilter。
  11. 完成響應後會銷燬流,更新統計資訊,寫入訪問日誌並最終確定跟蹤範圍。

我們將在以下各節中詳細介紹每個步驟。

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:實現了ReadFilterWriteFilter.

關鍵過濾器方法的方法簽名為:

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過濾器介面有三種型別:

檢視解碼器過濾器介面:

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()。如果返回的FilterHeadersStatusContinue,則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進行最終確定。

相關文章