本文是“Envoy 哪里做得不够好”系列的第三篇。

前文提要:

Envoy 支持通过 Listener 资源动态调整监听配置和设置四层策略。但和直觉不同,对 Listener 资源的修改并非无损的。对 Listener 的修改都会触发 LDS drain。当 LDS drain 被触发时,Listener 当前的连接都会被优雅关闭(比方说,对于 HTTP2 连接会发送 GOAWAY 的报文)。如果给定时间内客户端没有关闭连接,Envoy 则会主动关闭连接。这个时间取决于启动 Envoy 时 --drain-time-s 配置,默认 10 分钟。在 istio 上配置为 45 秒。所以 Envoy 更新 Listener 和 Nginx 通过 reload 更新监听配置一样都会导致连接的中断,前者的优势在于不需要重新创建一组 worker。

让我们做个试验看看效果。先拍下基本的网络配置:

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: gateway
  namespace: default
spec:
  gatewayClassName: istio
  listeners:
  - name: http
    port: 80
    protocol: HTTP
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: test
  namespace: default
spec:
  parentRefs:
  - name: gateway
  hostnames: ["localhost"]
  rules:
  - matches:
    - path:
        type: PathPrefix
        value: /
    backendRefs:
    - name: backend
      port: 8080

然后配一下 delay,模拟上游握住连接的场景。在 HTNN 里面可以配置下面的策略(或者发个 EnvoyFilter 也行):

apiVersion: htnn.mosn.io/v1
kind: FilterPolicy
metadata:
  name: policy
  namespace: default
spec:
  targetRef:
    group: gateway.networking.k8s.io
    kind: HTTPRoute
    name: test
  filters:
    fault:
      config:
        delay:
          fixedDelay: 3600s
          percentage:
            numerator: 100

让我们发起请求,会看到请求一直挂在这,说明 fixedDelay 配置生效了。这时候用 EnvoyFilter 修改 Listener:

apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: test
  namespace: default
spec:
  configPatches:
  - applyTo: NETWORK_FILTER
    match:
      listener:
        name: 0.0.0.0_80
    patch:
      operation: INSERT_FIRST
      value:
        name: rbac
        typed_config:
          "@type": type.googleapis.com/envoy.extensions.filters.network.rbac.v3.RBAC
          statPrefix: network_rbac
          matcher:
            matcher_tree:
              input:
                name: envoy.matching.inputs.source_ip
                typed_config:
                  "@type": type.googleapis.com/envoy.extensions.matching.common_inputs.network.v3.SourceIPInput
              exact_match_map:
                map:
                  "127.0.0.1":
                    action:
                      name: action
                      typed_config:
                        "@type": type.googleapis.com/envoy.config.rbac.v3.Action
                        name: allow-localhost
                        action: ALLOW

我们会看到过一段时间(远小于配置的 3600s delay)连接就会断开了。

设想一种场景,业务方需要在 Envoy 上配置四层的 IP 黑白名单。因为 Envoy 的 network filter 是 Listener 的一部分,所以每增加一条规则,都会修改 Listener,进而触发连接中断。这显然是不可接受的。事实上 Nginx 系的网关通过 Lua 代码已经可以做到配置四层策略的同时不断连接,比如三年前我在 APISIX 里实现四层 IP 黑白名单就是这么做的。Envoy 当然还不至于连这个问题也解决不了。

解决方法是引入 ECDS。改造下前面的 rbac filter:

apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: test
  namespace: default
spec:
  configPatches:
  - applyTo: NETWORK_FILTER
    match:
      listener:
        name: 0.0.0.0_80
    patch:
      operation: INSERT_FIRST
      value:
        config_discovery:
          config_source:
            ads: {}
          type_urls:
          - type.googleapis.com/envoy.extensions.filters.network.rbac.v3.RBAC
        name: xx
  - applyTo: EXTENSION_CONFIG
    patch:
      operation: ADD
      value:
        name: xx
        typed_config:
          "@type": type.googleapis.com/envoy.extensions.filters.network.rbac.v3.RBAC
          statPrefix: network_rbac
          matcher:
            matcher_tree:
              input:
                name: envoy.matching.inputs.source_ip
                typed_config:
                  "@type": type.googleapis.com/envoy.extensions.matching.common_inputs.network.v3.SourceIPInput
              exact_match_map:
                map:
                  "127.0.0.1":
                    action:
                      name: action
                      typed_config:
                        "@type": type.googleapis.com/envoy.config.rbac.v3.Action
                        name: allow-localhost
                        action: ALLOW

和之前相比,rbac filter 只是配置了下对 ECDS 的引用,真正的配置都是通过 ECDS 下发的。由于配置下发不涉及 Listener,自然不会出现因为 LDS drain 产生的断连。HTNN 为了规避连接中断的风险,特意开发出 ECDS 策略下发通道,这样在下发 Listener 级别的配置时就不会影响存量连接了。当然如果新增或减少 filter,还是会触发 LDS drain。

对 network filter 的修改会触发 LDS drain 还是小 case。在 Envoy 的设计里,HTTP filter 不属于 Route 系统,而是 Listener 的一部分。所以修改 HTTP filter (比如新增 Wasm 插件)也会触发 LDS drain。这个我打算后面专门开一篇文章说说,此处按下不表。

订正于 2024 年 7 月 18 日:和张添翼交流后,我意识到 Envoy 的 LDS drain 是有 in place filter chain update 机制的:https://github.com/envoyproxy/envoy/blob/8eef22b927682e9ff6f59cf9f26e440b41219fe6/source/common/listener_manager/listener_manager_impl.cc#L824

只有和之前不一样的 filterChain 上的连接才会被 drain 掉,也就是新增、删除 filterChain 不会影响其他 filterChain 上的连接。下面的内容是错误的,但为了保留历史,我把它们都留下来。请读者诸君明辨。


除了 filter 之外,Listener 还有两个地方经常会发生变化:filterChain 和 filterChain 里面的证书列表。两者都是 TLS 相关的,所以这里一起来讨论吧。

之前在本系列的第一篇文章提到过,同一个 Gateway 资源的同一个端口被翻译成一个 Listener 资源。一个 Listener 里会有多个 filterChains,每个 filterChain 对应一个给定的 Servername,即 Gateway 里面配置的hostname。当我们做基于 SNI 的路由时,Envoy 会根据 SNI 匹配到对应 filterChain,再根据 filterChain 里的 http_connection_manager 找到下一级路由表。所以更改基于 SNI 的路由规则,就会导致 filterChain 的变化,进而触发 LDS drain。社区里有一个 FDS(FilterChain Discovery Service)的提案,旨在把 filterChain 的增删从 Listener 里剥离出去,避免 LDS drain。

如果我不使用基于 SNI 的路由呢?同一个 filterChain 里可以配置多个不同的证书,假如我不需要给不同的域名提供不同的 TLS 配置,只用一个 filterChain 就够了。可惜,同一个 filterChain 里的变化也是整个 Listener 的变化。Envoy 支持通过 SDS 动态获取证书,但目前 SDS 的资源名称是固定的,也就是如果要增删证书,就需要修改同一个 filterChain 里面的证书数目。要想避免因为增删证书导致连接中断,解决办法有:

  • 支持 FilterChain Discovery Service,这样就可以给不同域名提供不同的 fitlerChain,也就不存在增删同一个 filterChain 证书的问题了。
  • 支持按需获取 SDS
  • 将 SDS 改造成 LDS 那样的 wildcard subscription,从控制面预先获取全部 SDS,然后在数据面像查找路由表一个查找证书。

总而言之,除非所有域名共用一个证书,在目前情况下更新 Envoy 的 TLS 配置或者上下线域名似乎难以避免导致连接中断。


spacewander
5.6k 声望1.5k 粉丝

make building blocks that people can understand and use easily, and people will work together to solve the very largest problems.


引用和评论

0 条评论