istio sidecar 注入机制详解
我们在k8s平台中,使用istio作为servicemesh来治理应用服务时,通常情况下,需要在应用pod中注入一个sidecar容器(通常情况为envoy),从而实现对服务的精细化治理。然而,在控制pod的sidecar注入时,用户或者开发人员往往对istio的sidecar注入机制了解的不够彻底,从而达不到最好的设计效果。本文将详细介绍sidecar的注入机制,让大家清楚如何控制sidecar注入,有哪些配置途径,需要主要哪些事项。同时,结合istio的版本迭代,介绍在sidecar注入方面,istio做了哪些功能改进。
1. 注入示例
为了给大家一个直观的感受,我们先通过基本的操作,来感受一下 istio 的 sidecar 注入。
首先,需要准备一个k8s集群,并基于该集群部署istio环境。本文采用了TKE集群,并通过TCM(Tencent Cloud Mesh)在TKE集群中部署istio环境。为了简单起见,以istio 1.5版本为例来做演示。如下图所示:
通过下图所示,对default
namespace 打开sidecar自动注入:
上图的操作,等价于在对应的namespace中增加如下label:
注:在TCM中,该label类型只针对1.5.6以及更低版本,高版本的将用
istio.io/rev: [revision]
格式)
在 default
namespace 开启自动注入之前,我们已经在集群中预先部署了istio官方demo的bookinfo应用。接下来,我们重新部署details-v1
deployment,触发滚动升级,从下图,我们发现,新创简单pod中,容器个数变为2,即说明sidecar部署成功。
同时,从tke平台上,可以看到pod中多了一个istio-proxy
容器
2. sidecar注入原理解析
上面介绍了 istio sidecar注入的基本操作。接下来,我们看下,sidecar注入的具体实现细节。
2.1 动态准入控制
为了能更好的理解sidecar的注入机制,我们需要先简单了解一下k8s提供的动态准入控制能力。
k8s apiserver中提供了准入控制机制,在 k8s API 请求通过认证鉴权后生效。准入控制机制会根据相关配置,在数据持久化之前,对API请求的数据进行修改、验证等一系列操作,并根据结果对数据进行放行或者拒绝并返回错误。
k8s apiserver 包含30多个准入控制器,其中包含两个特殊的准入控制器: MutatingAdmissionWebhook和ValidatingAdmissionWebhook。这两个控制器本身并不实现任何决策,它们将准入控制器的具体逻辑与 k8s apiserver 解耦,将准入请求发送到外部的 HTTP 回调服务并接收一个准入响应,使我们在集群中创建、更新或删除资源时,能够实现要执行的自定义逻辑:
- ValidatingAdmissionWebhook:该准入控制器主要用来检验对象的配置和操作的合法性。例如,我们需要禁止用户修改对象的某个字段,或者禁止特定用户做对象删除动作。如果请求不满足检验条件,则返回失败。多个validating准入控制执行逻辑可以并行执行;
- MutatingAdmissionWebhook:该准入控制器调主要用来更改对象的配置。例如,istiod的sidecar注入逻辑。如果修改逻辑中,存在任何错误,则会返回失败。由于该准入控制会修改对象配置,因此涉及到先后顺序,因此多个mutating准入控制逻辑需要串行执行。
下图给出了动态准入控制的原理:
我们可以在集群中通过创建MutatingWebhookConfiguration
和ValidatingWebhookConfiguration
对象来创建和配置一个或多个以上两种类型的admission webhook。它们可以动态配置哪些资源的哪些请求要被哪个准入 Webhook 处理。由于这两种类型的准入控制器可以通过配置对象动态配置,且服务的提供者独立于apiservcer,因此称其为动态准入控制。
由于准入控制原理不是本文重点,在这里不展开介绍,详细了解注入控制的技术细节,可以参考A Guide to Kubernetes Admission Controllers,以及k8s官方文档。
2.2 注入原理
下面给出了一个简化的 istio 中组件交互原理图。通过前面准入控制的介绍,相信大家不难理解下图描述的注入机制。
首先我们看看istio中sidecar注入的webhook配置:
|
|
由于sidecar注入操作,需要在pod创建时在pod中增加一个istio-proxy容器,即需要修改pod的配置,因此这里需要使用MutatingAdmissionWebhook。从上面的配置将告诉k8s apiserver,当用户发起一个创建请求,且创建的对象是指定Namespace下的pod,则向Istiod服务发起准入控制请求,有istiod组件来执行具体的准入控制逻辑,并返回准入控制相应(实际上就是控制istio-proxy容器的的注入逻辑)。
上面配置中,只给出了以namespace为粒度
namespaceSelector
一个选择器,实际上还有一个选择器objectSelector
,可以以具体对象为粒度进行选择,比如选择带有特定label的pod。
2.3 配置解析
通过k8s的准入控制配置能力,只是告诉apiserver创建符合条件的pod时,需要向istiod发送注入请求,然而,是不是只要请求到了istiod,就一定会被注入呢?以及注入的具体配置从哪里获取呢?下面我们详细解析。
在istio部署过程中,会部署两个configmap对象,默认命名分别为istio
和istio-sidecar-injector
。
其中,istio configmap包括istio的全局配置,包含两部分数据,meshNetworks
和mesh
- meshNetworks: 提供属于同一个服务网格的一组网络信息,用来告诉mesh如何将流量路由到不属于同一个网络的endpoint,主要在多网络场景下使用;
- mesh: 定义了服务网格中所有 Envoy 实例共享的网格范围的配置信息,以及控制面的的相关配置。
而istio-sidecar-injector configmap包括sidecar注入相关的配置,也包含两部分数据,config
和values
- config:提供sidecar的注入策略,以及注入内容模板,模板为go-template的形式;
- values:提供渲染模板的主要数据源,这里之所以说是主要数据源,是由于渲染模板,还需要其他配置信息。
上面两个configmap都会以volume的方式挂载到istiod的pod中,作为istiod的配置信息。
2.3.1 注入策略
接下来,我们首先回答第一个问题:“是不是只要请求到了istiod,就一定会被注入?” 我们详细看下istio-sidecar-injector configmap中的配置信息。
|
|
config字段中的包含如下几个字段:
- policy:告诉istiod是否默认开启sidecar自动注入功能,实际上这个字段已经回答了第一个问题;
- alwaysInjectSelector:配置内容为一个LabelSelector数组,只要pod匹配任意一个选择器,则注入sidecar。需要注意的是,该字段只有在policy=disable的时候才有意义,因为如果policy=enable,默认情况下,本身就会注入;
- neverInjectSelector:配置内容也是一个LabelSelector数组,只要pod匹配任意一个选择器,则不注入sidecar。同理该字段只在policy=enable的时候生效。
- injectedAnnotations:该字段用来配置需要想pod注入的annotation信息,实际上主要为了满足PSP需求。
- template:需要向pod中注入sidecar的patch模板,在注入过程中,会通过渲染改模板,得到一个包含sidecar配置的patch,然后merge到pod的配置中,实现sidecar的注入。
通过以上分析,我们知道前三个字段可以让istiod有效的控制sidecar的注入策略。例如,我们希望mesh环境中默认不开启自动注入功能,但允许pod中包含 always-inject: enable/true/yes
label的pod自动注入sidecar,则可以做如下配置:
|
|
istio还提供了一种优先级更高的注入策略控制方式,即通过pod中的annotation信息来控制:
如果 pod 中增加sidecar.istio.io/inject: true/y/yes/on
,则不会关心configmap中的配置,都会被注入;
如果 pod 中增加sidecar.istio.io/inject: [others]
,则不会关心configmap中的配置,都不会被注入。
这里可能有部分读者会有疑问,是不是只要pod中有sidecar.istio.io/inject: true/y/yes/on
annotation就一定会注入呢?实际上,上面介绍的注入策略中,有一个大前提,即apiserver一定要发起注入请求,也就是说,还有被mutatingwebhook选中的pod,才有可能被注入sidecar。
除此之外,满足如下条件的pod将不会被注入sidecar:
- 当pod属于
kube-system
和kube-public
两个系统namespace,将禁止注入sidecar容器,避免sidecar的部分网络配置会影响k8s系统组件的行为; - 当pod的网络模式为hostnet时,将禁止注入istio的sidecar容器,因为注入sidecar的时候,sidecar会修改其所在network namespace的iptables,将会影响host中其他组件和容器的网络行为。
综合以上注入策略,我们用下面一张图来描述注入策略的生效情况:
2.3.2 注入配置
知道如何控制sidecar的注入策略后,接下来再看看如何配置注入的内容。
事实上,对于一个特定istio服务网格环境,在sidecar注入的时候,需要根据环境的具体信息,对sidecar的注入进行配置。例如istio-proxy容器的镜像,容器的资源配置,是否使用特权模式,以及流量治理先关的配置等等。那么这些配置是通过什么途径配置的呢?下面我们详细解析。
上文提到,istio-sidecar-injector configmap 的config字段中包含的配置项中,template
提供了需要向pod中注入sidecar的patch模板,渲染该模板的数据,就是我们要分析的配置。为了能更直观的看到istiod渲染模板的配置数据,我们看下源码中渲染模板的数据结构(基于istio 1.8版本):
|
|
其中,TypeMeta 和 DeploymentMeta 字段,主要为了获取pod归属类型的信息(name, kind,apiversion等),来标识一个特定的服务实例(envoy实例),以便在tracing,Statistics等能力中更好的识别该实例;Spec字段的作用主要为了获取pod中,应用容器的信息,如Name,Ports等配置;MeshConfig则主要用来获取TrustDomain。以上几个字段实际上对proxy的行为基本没有影响,且通常情况下不需要做特殊配置。
Values字段中,包含了sidecar模板渲染的主要配置,包括容器资源配置,镜像,istio-proxy的行为相关配置(例如是否采用istio-cni,sidecar 容器的lifecycle配置,…)等;
ProxyConfig配置,默认情况下,来自istio configmap,即istio的全局配置,该配置中的信息会直接传给envoy进程,详细配置说明见官方文档 ProxyConfig
ObjectMeta取自pod的metadata字段,其主要目的还为了获取pod的annotation信息。通常情况下,注入pod的sidecar都会用同一套配置,然后有些特殊情况下,用户可能需要对某个pod的sidecar做定制化配置,因此istio允许用户通过annotation,来对特定pod的sidecar定制化配置。例如,全局配置中,sidecar的memory配置为“128Mi”,用户可以通过在pod annotation中增加 sidecar.istio.io/proxyMemoryLimit: 1Gi
将摸个pod的memory改为“1Gi”;用户还可以通过proxy.istio.io/config
key将全局配置中的“ProxyConfig”(即MeshConfig.DefaultConfig)替换掉。还有很多参数可以通过类似的方式修改,参见官方文档 Resource Annotations。下面以容器的资源配置为例说明annotation的用法。
我们先看下template的配置中资源相关的模板:
|
|
把模板逻辑翻译过来,意思是,如果在pod的annotation中有CPU(Request),CPULimit,Memory(Request),MemoryLimit对应的任意一个配置,则使用annotation中的资源配置,否则用Values.global.proxy.resources中的全局定义。这里需要注意一下是“任意一个”,如果只想修改其中一个参数,则需要把其余的配置,有从默认配置中搬过来。
下面以istio官方提供的bookinfo demo为例,我们在 productpage deployment的template中增加资源相关的annotation:
|
|
sidecar注入后,我们看到 productpage 对应pod中,资源配置生效:
3. 灰度升级支持
istio从1.6版本开支持灰度升级能力,在同一个mesh环境中,可以同时存在两套(或多套)不同版本的控制面,它们共享crd,以及对应的配置,相同的服务发现集群,相同的认证配置,但控制面的相关的配置则有多份,例如istiod 组件相关定义,istio、istio-sidecar-injetor configmap,sidecar注入webhook等。
根据上文的内容,我们知道在某个namespace的定义中增加istio-injection: enabled
label,就可以让该namespace中创建的pod注入sidecar,然而如果存在多个控制面,就会出现多个控制面打架的情况。为了满足灰度升级需求,istio在1.6版本开始支持新的sidecar注入标签istio.io/rev: [revision]
,这里“revision”是可变参数,不同的控制面,可以去不同的值。如下图所示,为两个控制面情况下,sidecar的注入机制:
图中有两个控制面,蓝色部分表示高版本的控制面的组件,其中会CRD跟随高版本(CRD不能定义两套)。由于控制面组件之间不能同名,因此灰度相关的组件和配置,也会加上revision后缀,例如tcm中1.8.1版本的istiod组件名为“istiod-1-8-1”。有了“revision”这个变化因子,就很容易控制针对多个控制面的sidecar的注入。
实际上,我们可以将mutatingwebhook配置看做是一个sidecar注入的路由配置,通过namespace中的标签信息,来确定该namespace下创建pod的sidecar由哪个控制面来注入,即服务由哪个控制面来治理。
因此,当我们想让某个服务的sidecar切换为高版本并由高版本的控制面管理时,只需要把所在namespace下的istio.io/rev
label改为新控制面对应的值,并重建pod即可。
这里还需要注意一点,由于istio-injection: enabled
label无法无法识别控制面版本,因此,灰度升级中,namespace中设置该label会导致注入sidecar失效。
4. 稳定的 Revision 标签支持
1.6版本中,istio中开始支持revision机制,这让使用者很方便的控制某个服务使用哪个控制面管理,然而需要切换控制面时,我们必须把namespace中的label全部改为新控制面要求的值。当namespace非常多的时候,会给使用者带来比较高的管理成本。1.10版本开始,我们在不修改namespace label的情况下,就可以实现控制面的切换,下面我们看看其实现原理。
为了便于说明问题,我们先了解两个概念:
- Revision:即前一节提高的
revision
,可以理解为控制面的标识,通常情况下会将版本号的“.”替换为“-”来作为控制面的Revision(例如1.8.1版本的Revision为“1-8-1”); - Revision tag:一般情况下是一个独立于revision的固定字符串值,主要用于sidecar注入。
1.10之前的版本中,每个控制面,只能包含一个mutatingwebhook,而从1.10开始,我们可以配置多个,因此我们可以基于mutatingwebhook路由功能,更加丰富sidecar注入的机制。istio 1.10在兼容老版本中使用revision控制sidecar注入的基础上,又提供了Revision tag控制方式。下面来看看Revision tag是如何工作的。
本文采用的mesh环境通过官方提供的istioctl工具创建,安装了istio 1.9.4和1.10.0两个控制面版本(具体的安装过程请参考官方文档)。默认情况下,环境中只有两个mutatingwebhook:
|
|
我们可以通过如下命令,添加两个Revision tag,即prod(product)、canary,并指向1.9.4版本的控制面:
|
|
这两个命令的作用是创建基于revision tag的sidecar注入机制,即如果namespace中有istio.io/rev=prod
和 istio.io/rev=canary
label,则会通过1.9.4版本的控制面来注入sidecar,对应的服务有该版本的控制面来管理。然后我们在看看mutatingwebhook的变化:
|
|
从列表中,我们看到多了两个mutatingwebhook,分别对应prod和canary两个revision tag,实际上,就是通过这两个对象来实现基于tag的注入机制。
istio-revision-tag-prod: namespace中的label匹配istio.io/rev: prod
,将通过istiod-1-9-4注入sidecar;
istio-revision-tag-canary: namespace中的label匹配istio.io/rev: canary
,将通过istiod-1-9-4注入sidecar。
通过如下命令,将canary
tag 重新设置为指向1.10.0版本的控制面(注意这里有一个“–overwrite”选项,表示覆盖修改,而非创建):
|
|
我们再看看istio-revision-tag-canary的变化:namespace中的label匹配istio.io/rev: canary
,将通过istiod-1-10-0注入sidecar
通过下面这张图来更直观的看看其工作过程:
本节开头我们提到,通过revision tag机制,可以简化namespace label的管理复杂度。实际上,我们还可以通过合理的规划tag类型,来提供数据面升级的安全性:
例如,我们可以将业务的一个最小子集应用部署到一个名为canary的namespace中,专用于升级灰度验证,并创建一个对应的revision tag “canary”,然后在对应的namespace中打上对应级别的istio.io/rev: canary
,灰度升级的时候,可以先将最小子集应用指向新的控制面,如果验证OK后,在升级生产应用。也可以将应用,根据重要程度划分为多个级别,用同样的方式,从次到主,依次升级,从而提高数据面升级的安全性。
5. 总结
本文首先以tcm中的sidecar注入为例,让大家对sidecar注入的操作,有一个直观感受;其次结合k8s的准入控制机制,详细介绍istio中sidecar注入的原理,以及控制sidecar注入的配置和相关策略,并详细介绍了注入信息的配置途径和含义;最后,通过介绍istio在版本演进过程,让大家了解灰度升级中,数据面注入机制,以及稳定tag的实现原理,对应用户使用带来的好处。