10-编程范式

0.1. API对象

  • 用于描述应用
  • 为应用提供各种各样的服务

为了使用这些API对象提供的能力,需要编写一个对应的YAML文件交给Kubernetes。这个YAML文件是kubernetes声明式API所必须具备的一个要素。

0.1.1. 命令式命令行操作

容器的编排操作都是基于命令行。

比如,要修改容器的镜像:

  1. 通过kubectl set imagekubectl edit命令直接修改kubernetes里面的API对象。
  2. 通过修改本地YAML文件,然后使用kubectl replace命令完成镜像的更新操作。

第二种方法,基于YAML文件的操作就是“声明式API”吗?显然不是。第二种方式称为命令式配置文件操作。处理方式与第一种没有区别,只不过把命令行参数写在配置文件中。

0.1.2. 声明式API

kubectl apply命令,就是声明式API。

执行的操作声明式API命令式操作异同
创建容器kubectl applykubectl create效果一样
修改容器配置kubectl applykubectl replaceapply都可以,create需要更换命令才行

这两者的本质区别是什么?

  • createreplace是使用新的YAML文件中的API对象替换原有的API对象
  • apply 是执行一个对原有API对象的PATCH操作
  • kube-apiserver在响应命令式请求(如 kubectl replace)的时候,一次只能处理一个写请求,否则会产生冲突的可能
  • 对于声明式请求(如 kubectl apply),一次能处理多个写操作,并且具备Merge能力

0.1.2.1. 定义

  1. 需要提交一个定义好的API对象来“声明”,我们所期望的状态是什么样子
  2. 声明式API允许有多个API写端,以PATCH的方式对API对象进行修改,而无需关心本地原始YAML文件的内容
  3. 在完全无需外界干预的情况下,基于对API对象的增删改查,完成对“实际状态”和“期望状态的调谐(Reconcile)过程。

声明式API才是kubernetes项目编排能力“赖以生存”的核心所在

0.2. 编程范式

kubernetes编程范式:如何使用控制器模式,同kubernetes里API对象的“增删改查”进行协作,进而完成用户业务逻辑的编写过程

0.3. 实际使用的意义

Istio项目,实际上是一个基于kubernetes项目的微服务治理框架。架构如下所示:

image

Istio最根本的组件,是运行在每一个应用Pod里的Envoy容器(网络代理)。把每个代理服务以sidecar容器的方式,运行在了每一个被治理的应用Pod中。

Envoy容器是Lyft公司推出的一个高性能C++网络代理。Pod中的所有容器都共享同一个Network Namespace。所以Enovy容器就能够通过配置Pod的iptables规则,把整个Pod的进出流量都接管下来。

Istio的控制层(Control Plane)里的Pilot组件,就能够通过调用每个Envoy容器的API,对整个Envoy代理进行配置,从而实现微服务治理。

0.3.1. 例子

  1. 假设这个 Istio 架构图左边的 Pod 是已经在运行的应用,而右边的 Pod 则是我们刚刚上线的应用的新版本。
  2. 这时候,Pilot 通过调节这两 Pod 里的 Envoy 容器的配置,从而将 90% 的流量分配给旧版本的应用,将 10% 的流量分配给新版本应用,并且,还可以在后续的过程中随时调整。
  3. 这样,一个典型的“灰度发布”的场景就完成了。
  4. 比如,Istio 可以调节这个流量从90%-10%,改到 80%-20%,再到 50%-50%,最后到 0%-100%,就完成了这个灰度发布的过程。

更重要的是,整个微服务治理的过程中,无论是对Envoy容器的部署,还是对Envoy代理的配置,用户和应用都是“无感”的

Istio项目使用,kubernetes中的Dynamic Admission Control功能,实现上述的“无感”操作。在kubernetes项目中,当一个Pod或者任何API对象被提交给APIServer之后,总有一些“初始化“性质的工作(如,自动为所有Pod加上某些标签)需要在它们被kubernetes项目正式处理之前进行。

初始化操作的实现借助的是Admission功能,它是kubernetes项目里一组被成为Admission Controller的代码,可以选择性地被编译进APIServer中,在API对象创建之后被立刻调用到

当需要使用Admission Controller的时候,需要重新编译并启动APIServer,通过这种方式很麻烦,因此提供了一种”热插拔“的Admission机制,就是Dynamic Admission Controller,也叫作Initializer

0.3.1.1. 容器在Istio中如何使用

如下pod:

apiVersion: v1
kind: Pod
metadata:
name: myapp-pod
labels:
app: myapp
spec:
containers:
- name: myapp-container
image: busybox
command: ['sh', '-c', 'echo Hello Kubernetes! && sleep 3600']

pod中有一个容器,Istio项目需要完成的是,在这个Pod YAML被提交给kubernetes后,在它对应的API对象中自动加上Envoy容器的配置,编程如下:

apiVersion: v1
kind: Pod
metadata:
name: myapp-pod
labels:
app: myapp
spec:
containers:
- name: myapp-container
image: busybox
command: ['sh', '-c', 'echo Hello Kubernetes! && sleep 3600']
- name: envoy
image: lyft/envoy:845747b88f102c0fd262ab234308e9e22f693a1
command: ["/usr/local/bin/envoy"]
...

被Istio处理后,Pod中除了有一个自定义的容器还会有一个叫envoy的容器,它就是Istio要使用的Envoy代理。

如何在用户和应用无感的情况下,完成上述操作?

Istio编写一个用来给Pod”自动注入“Envoy容器的Initializer。

0.3.2. Initializer

Istio将这个容器本身的定义,以ConfigMap(名字叫envoy-initializer)的方式保存在kubernetes当中。

apiVersion: v1
kind: ConfigMap
metadata:
name: envoy-initializer
data:
config: |
containers:
- name: envoy
image: lyft/envoy:845747db88f102c0fd262ab234308e9e22f693a1
command: ["/usr/local/bin/envoy"]
args:
- "--concurrency 4"
- "--config-path /etc/envoy/envoy.json"
- "--mode serve"
ports:
- containerPort: 80
protocol: TCP
resources:
limits:
cpu: "1000m"
memory: "512Mi"
requests:
cpu: "100m"
memory: "64Mi"
volumeMounts:
- name: envoy-conf
mountPath: /etc/envoy
volumes:
- name: envoy-conf
configMap:
name: envoy

这个ConfigMap包括两部分,containers字段和volumes字段。

Initializer要完成的工作是把这部分Envoy相关的字段,自动添加到用户提交的Pod的API对象里。用户提交的Pod中本来就有containers和volumes字段,所有kubernetes在处理这样的更新请求的时,类似于git merge的操作,将两部分内容合并在一起。

在Initializer更新用户的Pod对象时,必须用到PATCH API,这正式声明式API最主要的能力。

Istio将一个编写好的Initializer,作为一个Pod部署在kubernetes中,这个Pod的定义如下:

apiVersion: v1
kind: Pod
metadata:
labels:
app: envoy-initializer
name: envoy-initializer
spec:
containers:
- name: envoy-initializer
image: envoy-initializer:0.0.1
imagePullPolicy: Always

这是一个事先编写好的“自定义控制器(custom controller)”。这个Initializer控制器,不断获取到“实际状态”(就是用户创建的Pod),它的“期望状态”就是在这个Pod中添加Envoy容器的定义。

在Kubernetes中,一个控制器,就是一个死循环,不断地获取“实际状态”,然后与“期望状态”作对比,并以此为依据决定下一步操作。如下所示。

for {
// 获取新创建的 Pod
pod := client.GetLatestPod()
// Diff 一下,检查是否已经初始化过
if !isInitialized(pod) {
// 没有?那就来初始化一下
doSomething(pod)
}
}
  • 如果这个Pod中已经添加过Envoy容器,那就放过这个pod,进入下一个检查周期
  • 如果还没有添加Envoy容器,那就进行Initializer操作,即修改这个Pod的API对象

0.3.2.1. Initializer的工作逻辑

  1. 从APIServer中获取ConfigMap
  2. 把ConfigMap中存储的containers和volumes字段,直接添加到一个空的Pod对象里

kubernetes的API库中,有一个方法,使得我们可以直接使用新旧两个Pod对象,生成一个TwoWayMergePatch

  1. Initializer使用这个TwoWayMergePatch的patch数据,调用kubernetes的client,发起一个PATCH请求
  2. 用户提交的Pod对象里,就自动加上了Envoy容器相关的字段
// 步骤一
func doSomething(pod) {
cm := client.Get(ConfigMap, "envoy-initializer")
}
// 步骤二
func doSomething(pod) {
cm := client.Get(ConfigMap, "envoy-initializer")
newPod := Pod{}
newPod.Spec.Containers = cm.Containers
newPod.Spec.Volumes = cm.Volumes
}
// 步骤三
func doSomething(pod) {
cm := client.Get(ConfigMap, "envoy-initializer")
newPod := Pod{}
newPod.Spec.Containers = cm.Containers
newPod.Spec.Volumes = cm.Volumes
// 生成 patch 数据
patchBytes := strategicpatch.CreateTwoWayMergePatch(pod, newPod)
// 发起 PATCH 请求,修改这个 pod 对象
client.Patch(pod.Name, patchBytes)
}

0.3.2.2. 配置Initializer操作

0.3.2.2.1. 创建InitializerConfiguration文件

通过配置,来指定对什么样的资源进行Initializer操作,如下例子:

apiVersion: admissionregistration.k8s.io/v1alpha1
kind: InitializerConfiguration
metadata:
name: envoy-config
initializers:
// 这个名字必须至少包括两个 "."
- name: envoy.initializer.kubernetes.io
rules:
- apiGroups:
- "" // "" 就是 core API Group 的意思
apiVersions:
- v1
resources:
- pods

这个配置意味着kubernetes对所有pod进行Initializer操作,并且指定了负责这个操作的Initializer叫envoy-initializer。

这个InitializerConfiguration创建后,kubernetes就会自动把这个Initializer的名字,加到每个新创建的Pod的metadata中,如下所示:

apiVersion: v1
kind: Pod
metadata:
initializers:
pending:
- name: envoy.initializer.kubernetes.io
name: myapp-pod
labels:
app: myapp
...

每个新创建的Pod都会自动携带metadata.initializers.pending的metadata信息。

这个metadata信息,就是Initializer控制器判断这个Pod有没有执行过自己所负责的初始化操作的重要依据

当在Initializer中完成了要做的操作后,一定要将这个metadata.initializers.pending标志清除。

0.3.2.2.2. 添加annotation字段

除了创建配置文件,也可在具体的Pod的annotation里添加一个字段,如下所示:

apiVersion: v1
kind: Pod
metadata
annotations:
"initializer.kubernetes.io/envoy": "true"
...

添加后,就会使用到前面定义的envoy-Initializer。

Istio项目的核心就是由无数个运行在应用Pod中的Envoy容器组成的服务代理网格。这也就是server mesh的含义。这个机制实现的原理是借助于kubernetes能够对API对象进行在线更新的能力,这就是kubernetes“声明式API”的独特之处。

Istio项目对sidecar容器的巧妙设计,对Initializer操作的合理利用,都依托于kubernetes的声明式API和它所提供的各种编排能力。Istio项目是kubernetes的集大成者。

Istio项目部署完成,会在kubernetes里创建大约43个API对象。

上次修改: 14 April 2020