1. 概念
1.1 命令式命令行操作
Docker Swarm的编排操作
$ docker service create --name nginx --replicas 2 nginx
$ docker service update --image nginx:1.7.9 nginx
1.2 命令式配置文件操作
Kubernetes中,通过编写yaml进行容器的创建与更新
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
selector:
matchLabels:
app: nginx
replicas: 2
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx
ports:
- containerPort: 80
$ kubectl create -f nginx.yaml
创建出来一个Pod之后,更新容器镜像版本:修改yaml文件的Pod template部分
...
spec:
containers:
- name: nginx
image: nginx:1.7.9
$ kubectl replace -f nginx.yaml
1.3 声明式API
对于1.2中的yaml文件,进行kubectl apply操作
$ kubectl apply -f nginx.yaml
同样的,修改过镜像版本后,执行kubectl apply,更新pod,触发滚动更新。
$ kubectl apply -f nginx.yaml
kubelet apply和kubelet replace区别
- replace 是使用新的yaml文件中的API对象,替换原有API对象
- apply 是对原有API对象PATCH操作
此外,kubetcl set image, kubectl edit也是对原有API对象的修改
这就意味着,kube-apiserver在响应式命令请求的时候,一次只能处理一个,否则多个替换操作的结果就可能导致冲突,而对于对于声明式请求来说,一次可以进行多个写操作,具备前面提到过的Merge能力。
所以什么是“声明式API”?
其实就是,通过给一个配置文件(期望的最终状态),然后通过使用支持patch的命令模式,去叠加的演进对象的最终状态的这样一种做事情的方式。
2. 声明式API的在实际项目中的重要意义(Istio)
2.1 Isotio概念
是一个基于Kubernetes项目的微服务治理框架,架构如下所示。
2.2 Envoy容器
从架构图中看出,Istio项目的最根本组件,是运行在每个应用Pod中的Envoy容器。这个应用容器是Lyft公司推出的一个高性能C++网络代理,它以sidecar容器的方式,运行在每一个治理的应用Pod中。他的作用是通过配置Pod中的iptables规则,把整个Pod的进出流量接管下来。
这样,Istio控制层的Pilot组件,就能够通过调用每个Envoy容器的API,对这个Envoy进行配置,实现微服务治理。
什么是微服务治理?
2.3 Enovy的产生与应用场景
举例灰度发布。
仍然以2.1中的Istio架构为例,假设左边的应用是一个正在运行的旧版本应用,右边的应用Pod是这个版本新上线应用。通过Pilot调节两个Pod里Envoy容器的配置,进而调节外部请求到两个Pod的流量比例(比如新版本10%,旧版本应用Pod90%),之后通过逐步过渡比例,完成灰度发布的过程。
在整个微服务治理的过程中,无论是对Envoy的部署,还是对Envoy代理的配置,这些对于用户和应用都是完全“无感”的。
这就有一个问题,既然Envoy是在每个应用Pod安装的(我们知道,可以把一个Pod,理解成一个实际的应用,而Envoy容器的声明和配置修改,都不会在用户的yaml文件中写明,单Envoy容器最终又是切切实实运行在最终的应用Pod当中),那么对于Envoy在Pod中实现和配置修改,Istio怎么做到无感的?
实现这种Envoy容器无感修改的技术,是Kubernetes中一个叫做Dynamic Admission Control的功能,这个功能也叫做Initializer。
后面就可以看到,Dynamic Admission Control,也就是Initilizer,其实也是一个容器起来的Pod。他的功能就是将存在Etcd中的ConfigMap类型的关于Envoy容器的配置,和用户的Pod API对象通过Kubernetes的PATCH API做merge。
实际上,在Kubernetes项目中,每当一个Pod或者任何一个API对象通过命令等方式提交给APIServer之后,总要有一些“初始化”性质的工作需要他们被Kubernetes项目正式处理之前进行。这个操作,是一个叫做Admission的功能。这是Kubernetes中一段Admission Controller的代码,可以被选择性地变异进入API Server,在API对象创建后会被立刻调用。但是,如果Envoy的配置修改想用这个功能的话,那么每次都要动态的修改代码重新build并重启APIServer,显然这是不可接受的。所以就有了“热更新”方式的Admission机制,就是上面提到的动态Admission Control技术(小伙子,该去看代码了= =)
具体到例子来分析,看看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']
Istio要做的事情,就是上面这个Pod被提交给Kubernetes之后,在对应的API对象中加上Envoy容器的配置(这里就是无感了),使API对象对应的yaml文件如下变成下面这样
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"]
...
那么,对这个API对象无感的修改,是怎么通过Initializer来实现的呢?
- Istio将这个Envoy容器本身的定义,以ConfigMap的方式,存在Etcd当中,这个ConfigMap(叫做envoy-initializer)的定义举例如下:
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
data的部分就是envoy容器的对应配置字段和volumes的配置。而Initializer的工作,就是把这个部分配置,自动添加到用户的POD API对象中。这就用到了Kubernetes的PATCH API,而这种patch操作,正是声明式API的主要能力。
- Istio把一个编写好的Initializer作为一个Pod部署在Kubernetes集群中。如下:
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
这里envoy-initializer的使用的镜像envoy-initializer:0.0.1就是一个自定义控制器(Custom Controller),是可以自己事先编写好的。
而这个控制器同样也是遵循控制器模型的,他的实际状态是获取到的用户新建的Pod的配置情况,期望状态就是这个Pod里被加入了Envoy容器的定义。伪代码如下:
for {
// 获取新创建的 Pod
pod := client.GetLatestPod()
// Diff 一下,检查是否已经初始化过
if !isInitialized(pod) {
// 没有?那就来初始化一下
doSomething(pod)
}
}
如果获取到的Pod的API对象中,没有Envoy的容器的定义,那么就开始进行doSomething的初始化,具体做的事情就是前面提到过的,把Etcd中存储的那个关于Envoy容器配置的ConfigMap对象加到一个空的Pod里面去,然后调用Kubernetes的API库中TwoWayMerge
Patch方法,把用户Pod对象和新Pod对象 merge。
- 至此,Istio对通过声明式API的Patch特性实现Dynamic Adminssion Control功能,进而支持Envoy容器在用户和应用侧无感的能力就实现了。
2.4 Dynamic Admission Control的配置
Dynamic Admission Control,即Initializer的相关配置(注意,这里不是指的Initializer本身,而是Initializer的配置文件)。
我们可以通过配置这个配置对象,来指明对什么类型的资源进行Initialize的操作。比如下面这段代码,就是对全部的pods都做初始化操作。
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
同时,只要这个对象一杯kubectl apply/create出来之后,Kubernetes就会把这个Initializer的名字(envoy.initializer)打在所有新创建Pod的metada上,如下:
apiVersion: v1
kind: Pod
metadata:
initializers:
pending:
- name: envoy.initializer.kubernetes.io
name: myapp-pod
labels:
app: myapp
...
这个字段,就是在Initializer的控制器模型中,根据什么区判断有没有被初始化过的依据,换言之,每当我们自定义的Initialzer在做完Initialize的操作之后,要把metadata.initializers.pending标识删除掉!!!
除了Kubernetes在我们创建出来kind是InitializerConfiguration的对象后,会自动给所有新建的Pod都打这个标签之外,如果我们没有新建这个InitializerConfiguration对象,但是又想让我的某个Pod去使用某个Initializer的话,这么干:给对应Pod打metadata.annotations字段实例如下:
apiVersion: v1
kind: Pod
metadata
annotations:
"initializer.kubernetes.io/envoy": "true"
...
而Istio项目的核心,就是用无数个运行在应用Pod中的Envoy容器组成的服务代理网格。这也是Service Mesh的含义。
总地来说,从上到下,灰度升级的切流场景->(Service Mesh)->Envoy们组成的服务代理网格->Envoy的实现->Kubernetes通过声明式API支持的Patch修改Pod的能力->声明式API好哇,声明式API棒哇(少林功夫好哇,好!少林功夫棒哇,棒!我系铁头功,无敌铁头功....喂,跑题了,回来了!)
3. 小结
声明式API的独特之处:
- 声明式API,就是说我们来提供一个好的API对象进行声明,我们期望的状态是啥样子的
- 其次,声明式API允许多个API对象写端,并在Kubernetes中支持以Patch的方式,对API对象进行修改以达到最终期望的实际状态,而无需关心最初的YAML文件的内容
- 基于以上两个能力特性(提供API明确的API对象来声明最终的期望状态+支持多个API对象声明并以PATCH的能力使控制器模型拿到的是最终的期望状态),Kubernetes能在无需外部干预的情况下,只要你给我明确的若干个API对象,我就能够给你无感调谐到你最终期望的状态
而在上面我们提到的Initializer的实现时,最核心的,还是Initializer里面那个image,自定义编程的编写过程,这是遵循“Kubernetes范式编程”,即
如何使用控制器模式,同 Kubernetes 里 API 对象的“增、删、改、查”进行协作,进而完成用户业务逻辑的编写过程。
所以,之后要学习的一个核心,就是如何通过“Kuberbetes范式编程”完成使用Kubernetes部署代码的Kubernetes用户,到使用Kubernetes编写代码的Kubernetes玩家的晋级之路。