Service Mesh最火项目Istio是怎么做流量管理的?

1,563 阅读14分钟

Istio 现在是 Service Mesh 中最火热的项目了,它主要负责对服务网格中的流量进行管理,包括动态服务发现、服务路由、弹性功能等。它作为 Service Mesh 的控制平面,配合 Envoy 作为数据平面,构成了 Service Mesh 中流量管理体系。

Istio 体系中流量管理配置以及相关机制比较冗杂,本文会尽量从整体上进行描述,不抠细节,以求有一个宏观的理解。

为什么需要 Service Mesh

我们首先要明白为什么会催生出 service mesh 这样的架构,而要理解这一点,我们需要对比 2 种东西,即传统微服务架构以及原生 k8s 架构。

传统微服务

我们知道现有的微服务架构已经比较成熟了,拥有完善的服务注册发现与服务治理功能(包括限流熔断、负载均衡、流量路由等),那么仍然困扰我们的是什么呢? 现有的微服务框架几乎都是集成于客户端 sdk 的,开发者在自己的应用中集成微服务框架 sdk,从而具有服务治理相关功能,那么这会带来 2 个几乎无法避免的问题:应用需要不断更新代码从而更新客户端 sdk,而这中间可能出现各种依赖冲突;应用集成并使用客户端 sdk 依赖于开发者的素质,常常导致效率低下。

这种情况下,我们自然而然地会想到使用 sidecar,将原先在 sdk 中的逻辑搬到 sidecar 上,在运维的时候与应用服务集成在一起,形成一种较 sdk 集成更先进的边车模式。这种方式对应用代码几乎没有侵入,并且也不受开发者的水平限制,完全做到了控制与逻辑的分离。当然作为易用性的代价,我们会失去一些性能。

sidecar 模式是 service mesh 相较微服务架构最主要的优点之一,而这在 service mesh 架构中也称为"数据平面"。

原生 k8s

这里我们思考的是在 k8s 之上,我们还有什么不满足?k8s 使用 kube-dns 以及 kube-proxy 配合 service 的概念支持了服务的注册与发现,意味着我们有了可以在 k8s 上构建微服务的基础。但在实际生产中,我们往往需要对流量更精细的管控,我们希望对单个 pod 或者一组 pod 的流量进行管理,而不仅仅是 service 的维度。例如一个需求是我们需要将 endpoints 下的一部分 pod 作为 v1 版本,其余作为 v2 版本,而/v1 前缀的请求都将调度到 v1 版本,/v2 前缀的请求调度到 v2 版本。

sidecar 作为数据平面与每一个应用部署在同一 pod 中,这意味着我们可以做到对 pod 级别的流量做管理了,而具体实现这一逻辑的就是 service mesh 中的另一层:"控制平面"了。

Istio 的架构

Istio 是目前 Service Mesh 领域最火热的架构,我们来看看 Istio 的整体架构。

Istio 分为数据平面以及控制平面,数据平面以 sidecar 的形式与应用部署在一起,承载其流量的发送与接收,而控制平面则是通过配置和控制消息来组织编排网络的逻辑,并下发给数据平面。

我们简单讲一下其中的组件:

  • Envoy: Envoy 是 c++开发的高性能代理,在 Istio 中被用于数据平面(图中即 Proxy),控制应用的入站和出站流量,而在 Istio 中,它拥有了动态服务发现、负载均衡、Http2/gRpc 代理、熔断器、健康检查、故障注入等多种特性,当然这些都需要控制平面配合下发指令实现。

  • Mixer: Mixer 是 Istio 控制平面的组件之一,用于执行访问控制和遥测(在最新版本已经没有了)

  • Pilot: Pilot 就是实现流量管理的关键组件了,为 Envoy 提供服务发现、智能路由、弹性功能(超时/重试/熔断),我们在后面也会详细描述。

  • Citadel: Citadel 通过内置的身份和证书管理,可以支持强大的服务到服务以及最终用户的身份验证和流量加密。

  • Galley: Galley 是 Istio 的配置验证、提取、处理和分发组件。

Istio 中流量管理的概念

流量管理是 Istio 中最基础和最重要的功能了。我们之前说到我们的真实诉求往往是希望指定某个 pod 或者一组特定的 pod 接收流量,最基础的方式是通过人肉运维搞定,但更优雅地是将其委托给 Istio 本身,Istio 会通过 Pilot 与 Envoy 代理搞定这一切。

怎么搞定这一切呢,既然 kubernetes 中 service 的抽象概念满足不了我们的需求,很自然地我们尝试去抽象一些更上层的概念来解决问题。Istio 也是如此,我们来讲一下其中主要的一些基本概念(至于具体的配置,由于篇幅限制跳过):

  • VirtualService: 顾名思义,其实可以理解为对 service 的一层抽象,用于定义路由规则,控制流量路由到匹配的 service 子集(VirtualService)。同时你可以为每个 VirtualService 设置一些独立的网络弹性属性,例如超时、重试等。

  • DestinationRule: 一般是 VirtualService 路由生效后定义目的服务的策略,包括断路器、负载均衡等。当然它也可以定义可路由子集,即 VirtualService 中的路由规则可以完全委托给 DestinationRule。

  • ServiceEntry: 这是用于将 Istio 服务网格外部的服务添加到内部服务注册的,也就是说你能访问外部服务了。

  • Gateway: 用于控制南北流量的网关了,将 VirtualService 绑定到 Gateway 上,就可以控制进入的 HTTP/TCP 流量了。

  • EnvoyFilter: 主要为 Envoy 配置过滤器,用于动态扩展 Envoy 的能力。

控制面 Pilot 的流量管理

在 Istio 的控制平面中,Pilot 是负责流量管理的组件,也是本文的主角。Pilot 整体架构如下(来自old_pilot_repo,不过整体出入不大):

我们看到 pilot 主要包含 2 个组件,即 Discovery services 和 Agent。

  • Agent: 该进程对应的是 pilot-agent,负责生产 Envoy 配置文件和管理 Envoy 生命周期。它和 Proxy(即 Envoy)以及具体应用 Service A/B 在同一 Pod 中部署。

  • Discovery services: 该进程对应的是 pilot-discovery,负责 pilot 中最关键的逻辑,即服务发现与流量管理。Discovery services 一般是和应用分开使用单独的 deployment 部署的。它会和 2 个类型的数据打交道。一是图中 k8s API Server 中的服务信息,即 service、endpoint、pod、node 等资源。其二就是 K8s API Server 中的一些 CRD 资源,包括了上述的 VritualService、DestinationRule、Gateway、ServiceEntry 等 Istio 控制面的流量规则配置信息。然后 Discovery services 会将这两部分数据转换为数据面可以理解的格式,并通过标准的 API 下发到各个数据面 (Envoy)sidecar 中。

Pilot Agent

pilot agent 它不是本文的主角,我们简单介绍一下。它主要的工作包括:

  • 生成 envoy 的相关配置,这里是指少部分的静态配置,毕竟大多动态配置是通过标准 xDS 接口从 Pilot 获取的。

  • 负责 envoy 进程的监控与管理工作,比如 envoy 挂了负责重启 envoy,或者配置变更后负责 reload envoy。

  • 启动 envoy 进程

Pilot-discovery

pilot-discovery 是 pilot 中的关键组件,我们进行较为详细的描述。

整体模型

pilot-discovery 的整体模型如下所示:

pilot-discovery 有两部分输入信息:

  • 来自 istio 控制平面的信息,也就是图中的 Rules API,包括 VirtualService、DestinationRule 等,这些信息以 kubernetes CRD 资源的形式保存在 kubernetes api server 中。

  • 来自于服务注册中心的服务注册信息,也就是图中的 kubernetes、mesos、cloud foundry 等。当然我们默认都认为是 kubernetes,下文相关描述也默认 kubernetes。

为了支持对不同服务注册中心的支持,所以需要有一个统一的数据存储模型,即 Abstract Model,和一个转换器 Platform Adapter 来实现各服务注册中心数据到 Abstract Model 的数据转换。当然除了注册中心数据外,Platform Adapter 还需将 VirtualService、DestinationRule 等 CRD 资源信息转换成 Abstract Model。

基于统一数据存储模型 Abstract Model,pilot-discovery 为数据面提供了控制信息服务,也就是所谓的 xds api 服务,从而得以将控制信息下发到数据面 envoy。

接下来我们讲讲 pilot-discovery 的一个整体流程。

初始化工作

pilot-discovery 首先干的事儿是进行一些初始化,初始化的工作内容包括:

  • 创建一个 kubernetes client。想要与 kubernetes api server 交互那自然需要 kubeClient,kubeClient 有 2 种创建方式。一种是使用特定的 kubeConfig 文件从而联通 kubernetes api server。另一种是使用 in cluster config 方式,也就是如果本身在 kubernetes 集群中,可以通过感知集群上下文的方式自动完成配置。

  • 多集群配置。现实情况下,我们可能会有多个 kubernetes 集群,一种方式是每个 kubernetes 集群搭建一套 Istio,但也可能是多个集群共用一个 Istio。那么 Istio 就需要连接其他远程 kubernetes 集群了,它被称为 remote cluster,Istio 在一个 map 中保存每个 remote cluster 的远程访问信息。

  • 配置和 Mixer 相关的东西,这里不详细描述。

  • 初始化和配置存储中心的连接。之前说过 Istio 中的诸多流量管理相关的配置,包括 VirtualService、DestinationRule 等,这些都是需要保存在存储中心(不是指 Etcd)的。Istio 支持将这些配置通过文件存储或者 kubernetes CRD 的方式进行存储,当然基本上是后者。当 CRD 资源完成注册之后,还会创建一个 config controller 来对 CRD 资源的 crud 事件进行处理。

  • 配置和注册中心的连接。这里的注册中心基本上指的是 kubernetes,如上所述,我们需要对 kubernetes 中的 pod、service、endpoints 等信息进行监听,当然我们也需要一个 service controller 来进行事件处理。

  • 初始化 pilot-discovery 服务。由于 envoy sidecar 是通过连接 pilot-discovery 服务来获取服务注册发现信息以及流量控制策略的,所以这里需要初始化 discovery 服务,包括提供 REST 协议的服务以及提供 gRPC 协议的服务。

  • 一些健康检查以及监控。

流量策略等 CRD 信息处理

我们之前讲 Istio 中流量策略等相关规则都是以 kubernetes CRD 的形式保存在 kubernetes api server 背后的 Etcd 中的,包括 VirtualService、DestinationRule。这些 CRD 资源当完成注册后,还创建了 config controller 来处理 CRD 的事件。

config controller 主要内容包括对每个 CRD 资源实现一个 list/watch,然后为其 Add、Update、Delete 等事件创建一个统一的流程框架,即将 CRD 对象事件封装成一个 Task 并 push 到一个 queue 里。随后启动协程依次处理 CRD 资源事件,即从 queue 中取出 Task 对象,并调用其 ChainHandler。

服务注册信息处理

服务注册信息指的默认为 kubernetes 服务注册信息(当然也支持其他注册中心,但比较小众),即 pod、service、endpoints、node 等资源。我们之前讲 Pilot 创建了一个 service controller 来实现对 kubernetes 资源的监控和处理。

service controller 的逻辑和 config controller 基本一致。为每种 kubernetes 资源都实现了一个 list/watch,然后将其 Add、Update、Delete 事件封装成 Task 对象 push 到 queue 中,随后启动协程从 queue 中取出并调用 CHainHandler。

暴露针对 Envoy 的控制信息服务

我们知道,Envoy 是通过与 Pilot 暴露的信息服务交互从而得到服务发现/路由策略等相关信息的。pilot-discovery 创建了 gRPC 协议的 discovery 服务,通过 xDS api 与 Envoy 交互,包括 eds、cds、rds、lds 等。具体细节我们之后描述。

数据面 Envoy 与 xDS 服务

数据平面是 sidecar 方式部署的智能代理,在 Istio 体系中,数据面往往是由 Envoy 担任。Envoy 可以调整控制服务网格之间的网络通信,是控制面流量管理的实际执行者。

Envoy xDS 是为 Istio 控制平面与数据平面通信设计的一套 api 协议,也就是下发流量管理配置的关键。

Envoy 中的基本概念

首先在了解 xDS 之前,我们需要了解 Envoy 中的一些基本概念:

  • Host: 能够进行网络通信的实体。在 Envoy 中主机是指逻辑网络应用程序,一块物理硬件上可以运行多个主机,只要它们是独立寻址的。

  • Downstream: 下游主机连接到 Envoy,发送请求并接受响应。

  • Upstream: 上游主机获取来自 Envoy 的连接请求和响应。

  • Cluster: 表示 Envoy 连接到的一组上游主机集群。Envoy 通过服务发现发现集群中的成员,Envoy 可以通过主动运行状况检查来确定集群成员的健康状况。

  • Endpoint: 即上游主机标识,包括端点的地址和健康检查配置。

  • Listener: 监听器,Envoy 暴露一个或多个监听器给下游主机(downstream)连接,当监听器监听到请求时候,对请求的处理会全部抽象为 Filter,例如 ReadFilter、WriteFilter、HttpFilter 等。

  • Listener filter: 过滤器,在 Envoy 指一些可插拔和可组合的逻辑处理层,是 Envoy 的核心逻辑处理单元。

  • Http Router Table: 即 Http 路由规则,例如请求的什么域名转发到什么集群(cluster)。

Envoy 架构

Envoy 整体架构如下所示:

Envoy 的工作流程为下游主机(Host A)发送请求至上游主机(Host B/C/D),Envoy 拦截请求(代理注入及流量劫持我们不详细描述),Listener 监听到下游主机请求后将请求内容抽象为 Filter Chains,并根据流量策略相关的配置信息路由至相应的上游主机集群(Cluster),从而完成路由转发、负载均衡、流量策略等能力。

上述的流量策略相关的配置信息主要以动态配置的方式由 xDS api 实现,也是我们关注的重点。此外,Envoy 作为流量代理还可以将部分配置以静态配置的方式写入文件中,启动时候直接加载。

xDS 服务

所谓 xDS,这里的 x 是一个代词,在 Istio 中,主要包括 cds(cluster discovery service)、eds(endpoint discovery service)、rds(route discovery service)、lds(listener discovery service),ads(aggregated discovery service)则是对这些 api 的一个封装。

在以上 api 引入的概念中,endpoint 表示一个具体的应用实例,对应 ip 和端口,类似 kubernetes 中的一个 pod。cluster 是一个应用集群,对应一个或多个 endpoint,类似 kubernetes 中 service 的概念(实际上应该更小一些)。route 即当我们做类似灰发、金丝雀发布时,同一个服务会运行多个版本,每个版本对应一个 cluster,这时就需要通过 route 规则规定请求如何路由到某个版本的 cluster 上。

在实际请求过程中,xDS 接口采用的顺序如下:

  1. CDS 首先更新 Cluster 数据
  2. EDS 更新相应 Cluster 的 Endpoint 信息
  3. LDS 更新 CDS/EDS 相应的 Listener
  4. RDS 最后更新新增 Listener 相关的 Route 配置
  5. 删除不再使用的 CDS/EDS 配置

不过目前这个流程都已整合在 ADS 中,即聚合的服务发现,ADS 通过一个 gRPC 流以保证各个 xDS 接口的调用顺序。

我们之前讲过 pilot-discovery 的工作包括会创建一个 discovery 服务。现在它会与每一个 Envoy 建立一个双向 streaming 的 gRPC 连接,Envoy proxy 则会通过 ADS 接口按照上述的调用逻辑发起请求,并将 Pilot 的流量管理关键概念 VirtualService、DestinationRule 等组装成 cluster、endpoint、router、listener 等 envoy 配置。最终这些动态配置会作用在每一个 Envoy Proxy 上,当 Envoy listener 监听到下游主机请求时,就可以根据这些配置完成实际的动态服务发现、流量管理等功能。

参考资料

  1. Istio Handbook
  2. istio 源码分析之 pilot-discovery 模块分析
  3. istio 源码分析之 pilot-discovery 模块分析(续)
  4. Envoy 官方文档中文版
  5. Istio Prelim 1.6 文档