istio 庖丁解牛(四) pilot discovery

Posted on May 12, 2019

作者: 钟华,腾讯云容器产品中心高级工程师,热衷于容器、微服务、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 查看高清原图

在源代码中, 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

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

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 Config和Istio 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对象,对外提供Get、List、Create、Update、Delete等CRUD服务

    这是一种「Store Interface」

  • pilot/pkg/model.IstioConfigStore

    interface IstioConfigStore 通过 embed 方式扩展了接口ConfigStore。其主要目的是为访问route rule、virtual service等数据提供更加方便的接口。相对于ConfigStore提供的Get、List、Create、Update、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 IstioConfigStore和ConfigStoreCache, 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


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获取pod 、service、node、endpoint,并将这些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


7. xDS 服务端

通过上述2类控制器, Pilot 已经可以获得Istio Config 和 Service 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 Config、Service Discovery Config或者网格全局配置集.

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

7.1 DiscoveryServer UML

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

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

简要说明:

  • 使用统一配置管理器(Galley)来处理isito 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.