2

Kubernetes上的Service Mesh实践:用EnvoyFilter扩展Istio

 2 years ago
source link: https://www.servicemesher.com/blog/using-envoyfilter-extend-istio/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

KUN(中文名鲲)是UCloud面向内部的基于Kubernetes的资源交付平台,提供监控告警、CI/CD、网络双栈、Service Mesh等能力。在践行Service Mesh理念的过程中,面对Istio的不足,团队针对其源码做了大量改进,包括给网络子系统Pilot下的资源做隔离,对EnvoyFilter做深度优化等,使其能在生产环境稳定运行,并提供强大的扩展能力。截止目前,KUN平台上已有175个应用通过Istio提供服务。下面将分享我们在这方面的实践经验。

Istio流量管理策略

Istio中的流量管理策略是通过Pilot统一管理下发给Envoy的,Envoy作为数据面,对外提供XDS接口。为了保证最终一致性,Pilot实现了Envoy提供的ADS(Aggregated Discovery Service)接口,执行顺序为:CDS, EDS, LDS, RDS。Pilot本身是无状态的,所有的资源配置全部以CRD实例的形式保存在Kubernetes集群上,Envoy和Pilot连接建立完成以后,Pilot以事件通知的形式触发推送,Envoy配置更新生效。

统一的配置管理简化了运维成本,同时也意味着定制化能力的缺失。享受Pilot通用配置管理所带来的便利化的同时,又要针对具体的sidecar流量管理做微调,如何才能做到两者兼顾呢?这就涉及今天要介绍的主题:EnvoyFilter

Envoy架构

EnvoyFilter是Istio中自定义的一种网络资源对象,用来更新配置Envoy中的filter,为服务网格控制面提供了更强大的扩展能力,使Envoy中filter chain具备自定义配置的能力。

我们先来看下Envoy的整体架构:

从上图中我们可以看到Envoy中包含两种类型的filter:L4 filter(即network filter)和L7 filter。EnvoyFilter中可以自定义配置的filter即为这两种filter。从下面的监听器配置可以看到具体的filter所处的位置。

listener:
  filter_chains:
  - filters:  // L4 filter
    - name: {L4-filter-name}
    - name: envoy.http_connection_manager
      config:
        http_filters: // L7 filter
        - name: {L7-filter-name}

L4 filter主要包括:HTTP connection manager, MySQL proxy, Rate limit, RBAC, Redis proxy, TCP proxy等。L7 filter是L4 filter中HTTP connection manager下面定义的filter, 主要包括:CORS, External Authorization, Fault Injection, Health check, JWT Authentication, Lua, Rate limit, Router等。无论L4还是L7的filter都是按照指定的次序执行,istio中使用的istio-proxy也是在envoy的基础上额外编译进了istio_authn,mixer等filter, 以实现istio中的policy和telemetry等功能。

更近一步:EnvoyFilter案例分析

假设现在有一个需求,在调用REST接口时候如果header中含有k/v为“foo:bar”的请求要求返回444。 那么我们可以通过EnvoyFilter实现,在sidecar的inbound链中修改监听器配置,在http_connection_manager的第一个位置插入envoy.fault这样一个filter。

apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: simple-envoy-filter
spec:
  workloadLabels:
    app: helloworld
  filters:
  - listenerMatch:
      listenerType: SIDECAR_INBOUND
      listenerProtocol: HTTP
    insertPosition:
      index: FIRST
    filterType: HTTP
    filterName: envoy.fault
    filterConfig:
      abort:
        percentage:
          numerator: 100
          denominator: HUNDRED
        httpStatus: 444
      headers:
        name: foo
        exactMatch: bar

配置完成后我们看下XDS接口生成的动态监听器配置:

dynamic_active_listeners:
- listener:
    filter_chains:
    - filters:
        - name: envoy.http_connection_manager
        config:
          http_filters:
          - name: envoy.fault
            config:
              abort:
                percentage:
                  denominator: HUNDRED
                  numerator: 100
                httpStatus: 444
              headers: 
                name: bar
                exactMatch: foo
          - name: istio_authn
          - name: mixer

我们可以看到envoy.fault添加到了envoy.http_connection_manager这个L4下面的http_filters链中第一条规则,符合预期,同时请求结果生效。 EnvoyFilter的更多具体配置可以参考社区

追本溯源:缺少隔离

了解EnvoyFilter的基本使用之后,我们将深入分析Pilot(1.1.2)源码,来探究EnvoyFilter的工作原理和隔离性不足的根源。下图展示了构建Envoy监听器的主要工作流程。

insertUserFilter是在监听器配置完成之后执行的,用于向envoy filter chain中插入用户自定义的filter。insertUserFilter会调用下图中的getUserFiltersForWorkload函数,在整个集群范围内查找满足条件的EnvoyFilter,把获取到的filter合并后插入到监听器的filter chain中。

func getUserFiltersForWorkload(in *plugin.InputParams) *networking.EnvoyFilter {
    env := in.Env
    // collect workload labels
    workloadInstances := in.ProxyInstances

    var workloadLabels model.LabelsCollection
    for _, w := range workloadInstances {
        workloadLabels = append(workloadLabels, w.Labels)
    }

    f := env.EnvoyFilter(workloadLabels) // 集群范围查找
    if f != nil {
        return f.Spec.(*networking.EnvoyFilter)
    }
    return nil
}

这就会产生一个严重的问题。因为用户之间的行为是不可感知的,在集群范围内查找EnvoyFilter会导致用户行为的不可控,什么意思呢?让我们通过下图来具体说明:

如果图中的两个用户user1和user2,分别在对应的namespace下面部署两个具有相同标签的pod,绑定不同的EnvoyFilter,返回码应该分别为555(user1)和444(user2)。但user2访问/hello得到的最终返回码是555,与预期444不符,其行为被user1干扰了。 原因就在于缺少namespace级别的隔离。

解决:namespace级别隔离

EnvoyFilter为什么不做namespace级别的隔离?针对这个问题,笔者曾向istio社区寻求答案,但没有得到合理的答复。基于此,KUN团队针对istio1.1.2中EnvoyFilter做了namespace级别的隔离,使其影响范围控制在单一的namespace下,让普通用户具备可以修改envoy filter chain的能力而不会相互干扰。

下图我们可以看到EnvoyFilter的作用域被控制在了namespace级别

截止目前为止,KUN平台上共有175个应用通过Istio提供服务,除了EnvoyFilter,KUN同时针对其他网络资源也做了严格的隔离,以此来保证不同用户的服务的稳定性。基于此,不同用户可以根据具体需求在自定义namespace下创建EnvoyFilter,为自己的服务做功能扩展。

持续改进:校验

除了隔离之外,如果用户在EnvoyFilter中配置的filter没有被编译进Envoy,那么Pilot下发给Envoy的配置会直接导致Envoy出错,如下。

[2019-08-08 02:03:44.048][19][warning][config] [external/envoy/source/common/config/grpc_mux_subscription_impl.cc:73] gRPC config for type.googleapis.com/envoy.api.v2.Listener rejected: Error adding/updating listener(s) 172.17.0.13_80: Didn't find a registered implementation for name: 'envoy.filters.unknown'

KUN团队为了提升整个服务网格的容错性和可用性,对EnvoyFilter的创建做了进一步的校验,这涉及到istio-galley。 istio-galley的作用之一是实现了kubernetes中validating webhook的功能,用户在创建EnvoyFilter的时候apiserver会调用istio-galley做校验,如果校验失败则直接返回,该实例不会持久化到集群。于是我们修改了istio-galley源码, 将Envoy中的原生支持的filter及istio-proxy编译的filter作为基准进行校验。

# kubectl create -f

apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: network-filter
spec:
  workloadLabels:
    version: v1
  filters:
  - listenerMatch:
      listenerType: SIDECAR_INBOUND
      listenerProtocol: HTTP
    insertPosition:
      index: LAST
    filterType: NETWORK
    filterName: envoy.filters.unknown
    filterConfig:
      key: value

此时,我们可以看到错误的配置直接被拦截在创建时

Error from server: error when creating "networkfilter.yaml": admission webhook "pilot.validation.istio.io" denied the request: configuration is invalid: envoy filter: unknown

Service Mesh将基础设施下沉,使上层业务只专注于业务本身,在云原生领域具有广阔的应用前景。KUN团队很早开始跟进Service Mesh,早在Istio1.0版本之前就已在内部试用。团队将始终致力于改革UCloud内部研发流程,提升研发效率,并协同社区一同完善Service Mesh功能。同时也很欣喜地看到,我们此前做的一些改进工作,如支持IPv6环境、资源隔离等,在Istio后续版本中也陆续开始支持。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK