Istio 庖丁解牛四:pilot discovery

点击查看目录

作者:钟华,腾讯云容器产品中心高级工程师,热衷于容器、微服务、service mesh、istio、devops 等领域技术

今天我们来解析 istio 控制面组件 Pilot, Pilot 为整个 mesh 提供了标准的服务模型,该标准服务模型独立于各种底层平台,Pilot 以插件方式对接不同的服务发现平台,解析用户输入的流控配置,转换为统一的服务发现和流量控制模型,并以 xDS 方式下发到数据面。

Pilot 译为领航员, 在 mesh 中负责路由领航,是 istio 控制面的核心组件。

在组件拓扑中,Pod istio-pilot包括istio-proxy(sidecar) 和discovery2 个容器,pilot 核心能力由容器 discovery中执行的命令pilot-discovery discovery提供。

1.jpg
1.jpg
查看高清原图

在源代码中,package github.com/istio/istio/tree/master/pilot/cmd 有三个命令的入口:

  • sidecar-injector: 在前面文章中有过介绍。
  • pilot-discovery: 控制面 pilot 核心服务,本文重点分析。
  • pilot-agent: istio 里 sidecar 中主进程,用于启动和管控 envoy, 后续文章中进行分析。

1. Pilot 设计

下图展示了当前 istio(1.1.X) 中 Pilot 的流程设计:

2.jpg
2.jpg

A conceptual diagram for Pilot’s current design(图片来自Isio Community Doc)

从图中可以看出 Pilot 的处理流程可以抽象为 3 层:

1.1 Config Ingestion Layer:

Pilot 关注的Config有 2 大类 (图中进行了颜色区别):

  • Istio Config: 用户侧提供的流控管理配置,特别的,在 K8s 平台中表现为 CRD, 如 VirtualService、DestinationRule 等。

  • Service Discovery Config: 服务发现配置,包括 Services、Endpoints、Nodes 等。

下文中分别以Istio ConfigService Discovery Config来表示以上 2 类数据。

Config Ingestion Layer 以插件化的方式对接各种服务发现平台,这些对接逻辑以 in-process 方式内嵌在 pilot 进程中。包括 Kubernetes, Consul, file-based config plugin, MCP 方式等。

1.2 Core Data Model Layer:

Core Data Model Layer 会缓存上一层 (Config Ingestion Layer) 获取的配置信息,根据Istio ConfigService Discovery Config数据的不同特点,该层分别使用不同的控制器对其进行处理和存储。并将来自不同平台的配置信息抽象为统一的服务发现模型,如 Service, ServiceInstance, Registry 等。

1.3 Proxy Serving Layer:

Proxy Serving Layer 负责将上层 (Core Data Model Layer) 的抽象模型,转换为具体的 xDS 协议数据,并下发到订阅这些数据的数据面。

本文将尝试对pilot-discovery discovery的处理流程进行分析,重点关注 pilot 对 k8s 平台的适配实现。后续将对 Config 转换为 Pilot Model 和 xDS 进行分析。


2. pilot 初始化流程

命令pilot-discovery discovery将创建并启动 discoveryServer:

// Create the server for the discovery service.
discoveryServer, err := bootstrap.NewServer(serverArgs)
......
// Start the server
if err := discoveryServer.Start(stop); err != nil {
  return fmt.Errorf("failed to start discovery service: %v", err)
}

其中函数bootstrap.NewServer按照以下顺序对 discoveryServer 进行初始化,步骤清晰明了:

// 略掉错误处理代码
func NewServer(args PilotArgs) (*Server, error) {
  ......

  //对于 k8s 平台场景,初始化 kubeClient, 后续使用
  s.initKubeClient(&args);

  // 网格初始化
  s.initMesh(&args);
  s.initMeshNetworks(&args)

  // 初始化处理 Istio Config 的控制器
  s.initConfigController(&args)

  // 初始化处理 Service Discovery Config 的控制器
  s.initServiceControllers(&args)

  // 初始化 xDS 服务端
  s.initDiscoveryService(&args)

  s.initMonitor(&args)
  s.initClusterRegistries(&args)
  ......
}

3. 网格配置初始化

Config 一词在源码中使用泛滥,除了上面提及的:Istio ConfigService Discovery Config外,Istio 还有 2 个全局配置集:

我们称以上 2 个配置集为「网格配置」, 在源码中,网格配置初始化入口:

s.initMesh(&args);
s.initMeshNetworks(&args)

istio 控制面使用一个名为「istio」的 config map, 作为网格的全局配置:

% kubectl -n istio-system get configmap istio -o yaml

apiVersion: v1
data:
  mesh:
    ......
  meshNetworks:
    ......
kind: ConfigMap
metadata:
  name: istio
  namespace: istio-system

该 configmap 的 2 个 data 域「mesh」和「meshNetworks」, 分别对应上面的 2 个网格配置集:. 用户可以通过修改该 ConfigMap, 进行网格特定行为调整。

在 pilot 容器定义中,默认会将该 ConfigMap 挂载到/etc/istio/config目录,2 个配置集文件将分别位于/etc/istio/config/mesh/etc/istio/config/meshNetworks

    image: gcr.io/istio-release/pilot
    ......
    volumeMounts:
    - mountPath: /etc/istio/config
      name: config-volume
  ......
  volumes:
  - configMap:
      name: istio
    name: config-volume

该 configMap 作为投射卷 (projected volume) , kubernetes 会自动维护 ConfigMap 到文件系统的更新,因此 pilot 只需要通过文件系统 watch 这 2 个配置文件变化,即可实现运行时配置动态修改,无需重启 pilot。

pilot 在 watch 到「网格配置」变化后,会触发 xDS 的重新计算,并将新的 xSD 下发到数据面,从而使得配置修改得以生效。


4. Config 控制器

控制器模式在 k8s 里使用非常广泛,典型的 k8s 控制器利用 informer/reflector 对资源进行List/Watch, 获得资源更新事件,事件对象入队列,缓存 object 到 indexer, 然后在控制循环中进行自定义处理。

Pilot 对Service Discovery ConfigIstio Config两大类数据的处理,也是使用控制器模式,不过 Pilot 中 Config 控制器有特殊之处,因为适配多种平台,Config 有多种来源可能,除了 k8s informer, 还可能是 MCP, 文件系统,或者 consul client 等等。一个典型的 Config 控制器,可以用下图来描述:

3.jpg
3.jpg

上图左边是描述 Config 来源,右边描述 Config 控制器的结构,可以划分为三个部分:

  • Config 控制器要求实现「Controller interface」, 主要接口包括:为指定 config type 添加处理器,以及启动控制器消费 Task 的Run方法。
  • Config 控制器要求实现「Store Interface」, 主要包括对 config 的访问接口。write interfaces 如 Create, Update, Delete 主要是提供给 Config Ingestion Layer 使用,read interfaces 如 Get, List 主要提供给 Proxy Serving Layer 使用。
  • Config 控制器还包括的其他组件:主要是 task queue 和 type-handlers 存储。

Config 控制器会按需构造特定的 config 更新事件来源,如 k8s informer、MCP 等,同时通过实现 Controller interface, 允许为不同的 config type, 添加不同的处理器链,存储到 type-handlers 中。在接收到 config 更新事件后,Pilot 会将 event、object 和该 type 对应的 handlers 包装成 Task, push 到 queue 中。最终在Run方法中启动对 queue 中 Task 的消费。

具体的,Pilot Service Discovery ConfigIstio Config都按照上述控制器模式实现,下面分别介绍。


5. Istio Config 控制器

Istio Config 控制器用于处理 istio 流控 CRD, 如 VirtualService、DestinationRule 等,和 Istio Config 控制器相关的 interface 主要有:

  • pilot/pkg/model.ConfigStore

    ConfigStore对象利用 client-go 库从 Kubernetes 获取 route rule、virtual service 等 CRD 形式存在控制面信息,转换为 model 包下的 Config 对象,对外提供GetListCreateUpdate、Delete等 CRUD 服务。

    这是一种「Store Interface」

  • pilot/pkg/model.IstioConfigStore

    interface IstioConfigStore 通过 embed 方式扩展了接口ConfigStore。其主要目的是为访问 route rule、virtual service 等数据提供更加方便的接口。相对于ConfigStore提供的GetListCreateUpdate、Delete方法,IstioConfigStore 直接提供更为方便的 RouteRules、VirtualServices 等方法。

    这是一种「Store Interface」

  • pilot/pkg/model.ConfigStoreCache

    interface ConfigStoreCache 通过 embed 方式扩展了接口ConfigStore, ConfigStoreCache的主要扩展有:注册 Config 变更事件处理函数RegisterEventHandler 、开始处理流程的Run 方法等。

    这是一种「Controller Interface」, 同时也是「Store Interface」

如上所述,inferface ConfigStoreCache包括了上一节中要求的 2 类接口,目前实现了 interface ConfigStoreCache的 Istio Config 控制器主要有以下三种:

  • 以 k8s List/Watch方式获取config。

    具体实现位于 pilot\pkg\config\kube\crd.controller

  • 以 MCP 方式从ConfigSources获取,pilot 作为 MCP client, ConfigSources从全局配置 mesh config 中获取。

    具体实现位于 pilot\pkg\config\coredatamodel.Controller

  • 从本地文件系统中获取,主要用于测试场景。

    具体实现位于 pilot\pkg\config\memory.controller

5.1 k8s List/Watch config 控制器

我们以在第一种方式 k8s List/Watch的Config 控制器为例分析:

// pilot\pkg\config\kube\crd.controller
type controller struct {
  client *Client
  queue  kube.Queue
  kinds  map[string]cacheHandler
}

该 controller 同时实现了 interface IstioConfigStoreConfigStoreCache, queue 和 kinds 属性是用于存储 Task 的队列和 type-handlers 的 map。

该 controller 对象在初始化过程中,会为指定的 istio CRD 创建一个 k8s informer, 这些 CRD 主要是:

IstioConfigTypes = ConfigDescriptor{
  VirtualService,
  Gateway,
  ServiceEntry,
  DestinationRule,
  EnvoyFilter,
  Sidecar,
  HTTPAPISpec,
  HTTPAPISpecBinding,
  QuotaSpec,
  QuotaSpecBinding,
  AuthenticationPolicy,
  AuthenticationMeshPolicy,
  ServiceRole,
  ServiceRoleBinding,
  AuthorizationPolicy,
  RbacConfig,
  ClusterRbacConfig,
}

并为每个 informer 创建 EventHandler, 在 EventHandler 中会将新的 config event 包装为 Task, 并 push 到 queue 中。

Run 方法进行 queue 中 Task 消费,Task 中包括了事件类型,对象,以及处理函数链:

type Task struct {
  handler Handler
  obj     interface{}
  event   model.Event
}

至此,我们看到了 event 的 生产和消费,但不涉及 event/Task 是如何消费的。

通过调用RegisterEventHandler可以添加event/Task的处理器。

但是还没有看到RegisterEventHandler的调用,也就是每种类型的 config, 有哪些处理函数,这个在下文中补充。

5.2 Istio Config UML

4.jpg
4.jpg


6. Service Discovery Config 控制器

Service Discovery Config 控制器用于处理各平台服务发现数据,如 Services、Endpoints、Nodes 等,和 Service Discovery Config 控制器相关的 interface 主要有:

  • pilot/pkg/model.ServiceDiscovery

    对服务发现资源 (service/instance 等) 提供访问方法,如Services() InstancesByPort()等。

    这是一种「Store Interface」

  • pilot/pkg/model.Controller

    注册 Config 变更事件处理函数,包括AppendServiceHandler() AppendInstanceHandler(), 另外还有控制器启动的Run()方法。

    这是一种「Controller Interface」

如上所述,只要实现了以上 2 个 interface, 就可以作为 Service Discovery Config 控制器,Pilot 中实现了以上 interface 的有:

  • 对接 k8s 服务发现的控制器

    具体实现位于 pilot\pkg\serviceregistry\kube.Controller

  • 对接 istio CRD ServiceEntry 的服务发现控制器

    具体实现位于 pilot\pkg\serviceregistry\external.ServiceEntryStore

  • 对接 consul 服务发现的控制器

    具体实现位于 pilot\pkg\serviceregistry\consul.Controller

以上控制器带上 ClusterID 后,被包装为 Registry:

// Registry specifies the collection of service registry related interfaces
type Registry struct {
  // Name is the type of the registry - Kubernetes, Consul, etc.
  Name serviceregistry.ServiceRegistry
  // ClusterID is used when multiple registries of the same type are used,
  // for example in the case of K8S multicluster.
  ClusterID string
  model.Controller
  model.ServiceDiscovery
}

因为 Pilot 允许同时对接多个服务发现平台,因此在实际使用中会将多个 Registry 聚合在一起使用:

// pilot\pkg\serviceregistry\aggregate.Controller
type Controller struct {
  registries []Registry
  storeLock  sync.RWMutex
}

该聚合控制器也实现了以上 2 个 interface。

6.1 k8s Service Discovery Config 控制器

下面我们重点看看 k8s Service Discovery Config 控制器的实现:

pilot\pkg\serviceregistry\kube.Controller同时实现了上述 2 个 interface, 利用 client-go 库从 Kubernetes 获取podservicenodeendpoint,并将这些 CRD 转换为 istio 中 Service、ServiceInstance 等统一抽象模型。

type Controller struct { // k8s service/node/ep的controller
  ......
  queue     Queue
  services  cacheHandler
  endpoints cacheHandler
  nodes     cacheHandler
  pods      *PodCache

  servicesMap map[model.Hostname]*model.Service
  ......
}

// NewController creates a new Kubernetes controller
// Created by bootstrap and multicluster (see secretcontroler).
func NewController(client kubernetes.Interface, options ControllerOptions) *Controller {
  ......
  svcInformer := sharedInformers.Core().V1().Services().Informer()
  out.services = out.createCacheHandler(svcInformer, "Services")

  epInformer := sharedInformers.Core().V1().Endpoints().Informer()
  out.endpoints = out.createEDSCacheHandler(epInformer, "Endpoints")

  nodeInformer := sharedInformers.Core().V1().Nodes().Informer()
  out.nodes = out.createCacheHandler(nodeInformer, "Nodes")

  podInformer := sharedInformers.Core().V1().Pods().Informer()
  out.pods = newPodCache(out.createCacheHandler(podInformer, "Pod"), out)

  return out
}

总结主要信息:

  • k8s Service Discovery Config 控制器订阅的资源变更包括:Services、Endpoints、Nodes、Pod。
  • 没有统一的 type-handlers, 而是拆分到了多个属性中,如代码所示包括 Controller 的属性 services, endpoints, nodes 和 pods。

类似 Istio Config 控制器,k8s Service Discovery Config 控制器订阅的资源变更事件也会包装成 Task, push 到 queue 中等待消费,不在赘述。

6.2 Service Discovery Config UML

5.jpg
5.jpg


7. xDS 服务端

通过上述 2 类控制器,Pilot 已经可以获得Istio ConfigService Discovery Config的更新,接下来需要将这些不同平台的数据转换成统一的服务和路由模型,然后通过 xDS 下发给数据面代理。

目前 pilot 默认创建一个 gRPC Server 提供 xDS 订阅服务,在 Pilot 源码里叫做 DiscoveryServer, 简单说下 DiscoveryServer 的主要逻辑:

该 gRPC Server 需要实现 2 个接口:

// AggregatedDiscoveryServiceServer is the server API for AggregatedDiscoveryService service.
type AggregatedDiscoveryServiceServer interface {
  // This is a gRPC-only API.
  StreamAggregatedResources(AggregatedDiscoveryService_StreamAggregatedResourcesServer) error
  DeltaAggregatedResources(AggregatedDiscoveryService_DeltaAggregatedResourcesServer) error
}

接口StreamAggregatedResources主要逻辑:

  1. DiscoveryServer 接受下游的订阅请求,根据请求的 xDS 类型,返回指定的资源,如 CDS/EDS/LDS/RDS。

  2. DiscoveryServer 将连接对象缓存到 map 中,key 为下游 node ID 加上连接计数器。当检测到配置发生变化,将会触发这些连接上的 xDS 重新 push 到下游。这些配置变化可能是Istio ConfigService Discovery Config或者网格全局配置集。

DeltaAggregatedResources是增量 xDS 订阅接口,目前在 istio 中还未实现。

7.1 DiscoveryServer UML

6.jpg
6.jpg


8. Pilot 演进路线

查阅社区讨论和源码分析,Pilot 目前的不足主要有这些方面:

  • 多个控制面组件都依赖 istio CRD, 如 pilot, mixer 等,它们各自去订阅并处理这些 CRD, 导致各组件中代码逻辑重复,项目臃肿。
  • Pilot 项目臃肿的另一个原因是,以in-process方式对接各平台的配置获取和处理,Pilot 和这些平台之间并没有明确的接口依赖约定,istio 和其他平台出现接口和数据格式的兼容性,会是一个潜在风险。(要重视墨菲定律)。
  • Pilot 性能问题:Pilot 在关注的配置发生变化后,会重新计算 xDS 数据,并触发持有连接的 xDS 全量下发,DiscoveryServer 也没有对配置变化的内容进行分析,因而存在重复和无用的 xDS push. 截止版本 1.1, 增量 xDS 订阅接口在 Pilot 中还未实现。

下图是社区对 Pilot 解耦的方案提议:

7.jpg
7.jpg

Mesh Configuration APIs proposal(图片来自Isio Community Doc)

简要说明:

  • 使用统一配置管理器 (Galley) 来处理 istio CRD 的处理,通过 MCP 进行下发,Galley 作为 MCP 服务端,Pilot/Mixer 等作为 MCP 客户端。在 istio 1.1 中,Galley 的以上功能已经发布,并作为默认的配置处理方式,只是 1.1 中还保留了旧的实现代码,Pilot/Mixer 可以选择独立List/Watch Istio CRD, 未来随着 Galley 功能的增强和稳定,旧的实现应该会被移除。
  • 提议设计新的 gRPC 双向流协议:Mesh Configuration Protocol (MCP), 对配置进行抽象,聚合和传输。(类似 xDS gRPC), 以此将 Pilot 中配置对接逻辑从 in-process 逐步改造为 out-of-process 方式。

以上对 Pilot 的能力和结构进行了分析,下一篇文章将分析 Pilot 是如何将 Config 转为为 xDS。

钟华

钟华

腾讯

编辑本页