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

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

翻譯自Envoy官方文檔c++

下面描述一個通過Envoy代理的請求的生命週期。首先會描述Envoy如何在請求路徑中處理請求,而後描述請求從下游到達Envoy代理以後發生的內部事件。咱們將跟蹤該請求,直到其被分發到上游和響應路徑中。git

術語

Envoy會在代碼和文檔中使用以下術語:github

  • 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會用到除服務網格使用到的各類配置,例如,它能夠做爲一個內部的負載均衡器:bootstrap

或做爲一個邊緣網絡的ingress/egress代理:api

實際中,一般會在服務網格中混合使用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進行最終肯定。
相關文章
相關標籤/搜索