前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Kubernetes整理

Kubernetes整理

原创
作者头像
铭丶
修改2021-03-22 10:19:29
6980
修改2021-03-22 10:19:29
举报
文章被收录于专栏:用户6430897的专栏

Pod

Pod实现原理

Pod只是一个逻辑概念,实质是一组共享了某些资源的容器,是Kubernetes的最小调度单位

  1. 共享同一NameSpace
  2. 可以声明共享同一Volume(声明挂在同一Volume就OK)

思维拓展

  1. Pod内容器如何共享同一NameSpace?

一般做法:容器A共享容器B$ docker run --net=B --volumes-from=B --name=A image-A ...,这种做法会存在一个问题,容器AB的关系从对等变成了拓扑

骚操作:那是否可以让Pod内所有的容器都有一个顶级拓扑【中间容器Infra】,在Pod中这个容器永远第一个被创建,其它容器Join进来就完事了。Infra占用很少的资源,只有100~200K,且永远处于Pause状态。

白话

你可以把Pod理解为“虚拟机”,把容器理解为“用户进程”,因此,以“虚拟机”角度来看,凡是调度、网络,存储,以及安全相关的属性,都是Pod维度的。

Pod状态

  • Pending

这个状态意味着,Pod 的 YAML 文件已经提交给了 Kubernetes,API 对象已经被创建并保存在 Etcd 当中。但是,这个 Pod 里有些容器因为某种原因而不能被顺利创建。比如,调度不成功。

  • Running

这个状态下,Pod 已经调度成功,跟一个具体的节点绑定。它包含的容器都已经创建成功,并且至少有一个正在运行中

  • Succeeded

这个状态意味s着,Pod 里的所有容器都正常运行完毕,并且已经退出了。这种情况在运行一次性任务时最为常见。

  • Failed

这个状态下,Pod 里至少有一个容器以不正常的状态(非 0 的返回码)退出。这个状态的出现,意味着你得想办法 Debug 这个容器的应用,比如查看 Pod 的 Events 和日志。

  • Unknown

这是一个异常状态,意味着 Pod 的状态不能持续地被 kubelet 汇报给 kube-apiserver,这很有可能是主从节点(Master 和 Kubelet)间的通信出现了问题。

API对象

NodeSelector

绑定Node和Pod

代码语言:txt
复制
apiVersion: v1
kind: Pod
...
spec:
 nodeSelector:
   disktype: ssd  //标签

HostAlias

定义了 Pod 的 hosts 文件

代码语言:txt
复制
apiVersion: v1
kind: Pod
...
spec:
  hostAliases:
  - ip: "10.1.2.3"
    hostnames:
    - "foo.remote"
    - "bar.remote"
...

启动容器,在Pod /etc/hosts里可以查看到如下:

代码语言:txt
复制
cat /etc/hosts
# Kubernetes-managed hosts file.
127.0.0.1 localhost
...
10.244.135.10 hostaliases-pod
10.1.2.3 foo.remote
10.1.2.3 bar.remote
NameSpace相关属性
  • Pod里的容器共享PID
代码语言:txt
复制

spec:

shareProcessNamespace: true

代码语言:txt
复制

这样,我们就可以在 shell 容器里执行 ps 指令,查看所有正在运行的进程:

代码语言:txt
复制

$ kubectl attach -it nginx -c shell

/ # ps ax

PID USER TIME COMMAND

代码语言:txt
复制
  1 root      0:00 /pause
代码语言:txt
复制
  8 root      0:00 nginx: master process nginx -g daemon off;
代码语言:txt
复制
 14 101       0:00 nginx: worker process
代码语言:txt
复制
 15 root      0:00 sh
代码语言:txt
复制
 21 root      0:00 ps ax
代码语言:txt
复制
  • Pod 里的所有容器,会直接使用宿主机的网络、直接与宿主机进行 IPC 通信、看到宿主机里正在运行的所有进程
代码语言:txt
复制

spec:

hostNetwork: true

hostIPC: true

hostPID: true

代码语言:txt
复制
Container

docker相关属性,Image(镜像)、Command(启动命令)、workingDir(容器的工作目录)、Ports(容器要开发的端口),以及 volumeMounts(容器要挂载的 Volume)都是构成 Kubernetes 项目中 Container 的主要字段

ImagePullPolicy
  • Always //总是拉取
  • Never
  • IfNotPresent //如果不存在镜像再拉取
Lifecycle

钩子

代码语言:txt
复制
apiVersion: v1
kind: Pod
metadata:
  name: lifecycle-demo
spec:
  containers:
  - name: lifecycle-demo-container
    image: nginx
    lifecycle:
      postStart:
        exec:
          command: ["/bin/sh", "-c", "echo Hello from the postStart handler > /usr/share/message"]
      preStop:
        exec:
          command: ["/usr/sbin/nginx","-s","quit"]
  • postStart //postStart 启动时,ENTRYPOINT 有可能还没有结束
  • preStop //preStop 操作的执行,是同步的,直到Hook执行完成才允许杀死容器
容器检查/恢复
livenessProbe
  • 通过一个实验来认识检查和恢复机制
代码语言:txt
复制
apiVersion: v1
kind: Pod
metadata:
  labels:
    test: liveness
  name: test-liveness-exec
spec:
  containers:
  - name: liveness
    image: busybox
    args:
    - /bin/sh
    - -c
    - touch /tmp/healthy; sleep 30; rm -rf /tmp/healthy; sleep 600
    livenessProbe:
      exec:
        command:
        - cat
        - /tmp/healthy
      initialDelaySeconds: 5 #容器启动后5秒执行
      periodSeconds: 5 #每隔5秒执行一次
  1. 容器启动后,在/tmp目录下创建healthy文件,作为正常运行的标志,特别注意,30秒后这个文件会被删除
  2. 在livenessProbe中定义了exec,会执行“cat /tmp/healthy”,如果文件存在,返回0,表示服务健康
  • 启动容器并查看Pod状态
代码语言:txt
复制
$ kubectl get pod
NAME                READY     STATUS    RESTARTS   AGE
test-liveness-exec   1/1       Running   0          10s
  • 30s之后再查看Pod Events
代码语言:txt
复制
$ kubectl describe pod test-liveness-exec

FirstSeen LastSeen    Count   From            SubobjectPath           Type        Reason      Message
--------- --------    -----   ----            -------------           --------    ------      -------
2s        2s      1   {kubelet worker0}   spec.containers{liveness}   Warning     Unhealthy   Liveness probe failed: cat: can't open '/tmp/healthy': No such file or directory

这个时候可以发现,文件不存在,检测报告容器不正常

代码语言:txt
复制
$ kubectl get pod test-liveness-exec
NAME           READY     STATUS    RESTARTS   AGE
liveness-exec   1/1       Running   1          1m

发现容器的状态还是Running,为啥?这就是容器的恢复机制restartPolicy

restartPolicy
  • Always:在任何情况下,只要容器不在运行状态,就自动重启容器;
  • OnFailure: 只在容器 异常时才自动重启容器;
  • Never: 从来不重启容器。

返璞归真

  1. 只要Pod的RestartPolicy允许重启异常服务,那么Pod就会保持Running状态并进行重启。否则,进入Failed状态。
  2. 对于多容器Pod,只有所有的容器都进入Failed状态,Pod才会进入Failed状态。
拓展

HTTP方式

代码语言:txt
复制
...
livenessProbe:
     httpGet:
       path: /healthz  #Pod暴露健康检查URL,在 Web 服务类的应用中非常常用
       port: 8080
       httpHeaders:
       - name: X-Custom-Header
         value: Awesome
       initialDelaySeconds: 3
       periodSeconds: 3

TCP方式

代码语言:txt
复制
...
livenessProbe:
	tcpSocket:
		port: 8080
		initialDelaySeconds: 15
		periodSeconds: 20
Project Volume
Secret

创建

代码语言:txt
复制
apiVersion: v1
kind: Secret
metadata:
  name: mysecret
type: Opaque  #不透明
data:
  user: YWRtaW4=
  pass: MWYyZDFlMmU2N2Rm

使用

代码语言:txt
复制
spec: 
    volumes: 
    - name: mysql-cred 
      projected: 
            sources: 
            - secret: 
            	name: user 
            - secret: 
            	name: pass
ConfigMap

与Secret 的区别在于,ConfigMap 保存的是不需要加密的、应用所需的配置信息

创建

代码语言:txt
复制
apiVersion: v1
kind: ConfigMap
metadata:
  name: mysecret
data:
  mysql.user: zhangsan
  mysql.pass: 123456

使用

代码语言:txt
复制
spec:
  template:   
    spec:   
       containers:
          env:  
            - name: MYSQL_SERVICE_USER
              valueFrom:
                configMapKeyRef:
                  name: nacos-cm
                  key: mysql.user
            - name: MYSQL_SERVICE_PASSWORD
              valueFrom:
                configMapKeyRef:
                  name: nacos-cm
                  key: mysql.password            
Downward API

让 Pod 里的容器能够直接获取到这个 Pod API 对象本身的信息

代码语言:txt
复制
apiVersion: v1
kind: Pod
metadata:
  name: test-downwardapi-volume
  labels:
    zone: us-est-coast
    cluster: test-cluster1
    rack: rack-22
spec:
  containers:
    - name: client-container
      image: k8s.gcr.io/busybox
      command: ["sh", "-c"]
      args:
      - while true; do
          if [[ -e /etc/podinfo/labels ]]; then
            echo -en '\n\n'; cat /etc/podinfo/labels; fi;
          sleep 5;
        done;
      volumeMounts:
        - name: podinfo
          mountPath: /etc/podinfo
          readOnly: false
  volumes:
    - name: podinfo
      projected:
        sources:
        - downwardAPI:
            items:
              - path: "labels"
                fieldRef:
                  fieldPath: metadata.labels  #暴露 Pod 的 metadata.labels 信息给容器

通过这样的声明方式,当前 Pod 的 Labels 字段的值,就会被 Kubernetes 自动挂载成为容器里的 /etc/podinfo/labels 文件

ServiceAccountToken

一种特殊的 Secret

Kubernetes 其实在每个 Pod 创建的时候,自动在它的 spec.volumes 部分添加上了默认 ServiceAccountToken 的定义,然后自动给每个容器加上了对应的 volumeMounts 字段。如下:

代码语言:txt
复制
$ kubectl describe pod nginx-deployment-5c678cfb6d-lg9lw
Containers:
...
  Mounts:
    /var/run/secrets/kubernetes.io/serviceaccount from default-token-s8rbq (ro)
Volumes:
  default-token-s8rbq:
  Type:       Secret (a volume populated by a Secret)
  SecretName:  default-token-s8rbq
  Optional:    false

这种把 Kubernetes 客户端以容器的方式运行在集群里,然后使用 default Service Account 自动授权的方式,被称作“InClusterConfig”,也是最推荐的进行 Kubernetes API 编程的授权方式。

Pod 的字段这么多,我又不可能全记住,Kubernetes 能不能自动给 Pod 填充某些字段呢

PodPreset

想在开发人员编写的 Pod 里追加的字段,都可以预先定义好

容器设计模式

Pod 这种“超亲密关系”容器的设计思想,实际上就是希望,当用户想在一个容器里跑多个功能并不相关的应用时,应该优先考虑它们是不是更应该被描述成一个 Pod 里的多个容器

案例一:WAR 包与 Web 服务器

代码语言:txt
复制
apiVersion: v1
kind: Pod
metadata:
  name: javaweb-2
spec:
  initContainers:
  - image: geektime/sample:v2
    name: war
    command: ["cp", "/sample.war", "/app"]
    volumeMounts:
    - mountPath: /app
      name: app-volume
  containers:
  - image: geektime/tomcat:7.0
    name: tomcat
    command: ["sh","-c","/root/apache-tomcat-7.0.42-v2/bin/start.sh"]
    volumeMounts:
    - mountPath: /root/apache-tomcat-7.0.42-v2/webapps
      name: app-volume
    ports:
    - containerPort: 8080
      hostPort: 8001 
  volumes:
  - name: app-volume
    emptyDir: {}

思路:利用InitContainer,先把war包copy到war-volume,然后让webapps挂载到war-volume。

案例二:容器的日志收集

思路:同样,sidecar容器的主要工作也是使用共享volume,完成日志文件的操作,原理同案例一。

控制器

控制器模式

控制器模式和“驱动模式有什么区别”

实现原理

kubernetes对于容器资源编排的核心原理就是:控制循环(control loop)

代码语言:txt
复制
for {
  实际状态 := 获取集群中对象X的实际状态(Actual State)
  期望状态 := 获取集群中对象X的期望状态(Desired State)
  if 实际状态 == 期望状态{
    什么都不做
  } else {
    执行编排动作,将实际状态调整为期望状态
  }
}

水平扩展/收缩

ReplicaSet

Pod的水平扩展/收缩,滚动更新都依赖于ReplicaSet(Api对象)

代码语言:txt
复制
apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: nginx-set
  labels:
    app: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.7.9

跟Deployment的定义没啥区别蛮(实质就是Deployment定义的一个子集),而Deployment实际操作的正是ReplicaSet而不是Pod(层层控制)。Deployment,ReplicaSet,Pod之间的关系如下图:

其中,ReplicaSet 负责通过“控制器模式”,保证系统中 Pod 的个数永远等于指定的个数(比如,3 个)。这也正是 Deployment 只允许容器的 restartPolicy=Always 的主要原因:只有在容器能保证自己始终是 Running 状态的前提下,ReplicaSet 调整 Pod 的个数才有意义。而在此基础上,Deployment 同样通过“控制器模式”,来操作 ReplicaSet 的个数和属性,进而实现“水平扩展 / 收缩”和“滚动更新”这两个编排动作。其中,“水平扩展 / 收缩”非常容易实现,Deployment Controller 只需要修改它所控制的 ReplicaSet 的 Pod 副本个数就可以了。

如何扩展/收缩
代码语言:txt
复制
$ kubectl scale deployment nginx-deployment --replicas=4
deployment.apps/nginx-deployment scaled

滚动更新

Deployments的状态
代码语言:txt
复制
$ kubectl get deployments
NAME               DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
nginx-deployment   3         0         0            0           1s
  • DESIRED:用户期望的 Pod 副本个数(spec.replicas 的值);
  • CURRENT:当前处于 Running 状态的 Pod 的个数;
  • UP-TO-DATE:当前处于最新版本的 Pod 的个数,所谓最新版本指的是 Pod 的 Spec 部分与 Deployment 里 Pod 模板里定义的完全一致;
  • AVAILABLE:当前已经可用的 Pod 的个数,即:既是 Running 状态,又是最新版本,并且已经处于 Ready(健康检查正确)状态的 Pod 的个数。

查看 Deployment 对象状态的变化
代码语言:txt
复制
$ kubectl rollout status deployment/nginx-deployment
Waiting for rollout to finish: 2 out of 3 new replicas have been updated...
deployment.apps/nginx-deployment successfully rolled out
更新
代码语言:txt
复制
$ kubectl edit deployment/nginx-deployment
... 
    spec:
      containers:
      - name: nginx
        image: nginx:1.91 # 1.7.9 -> 1.91
        ports:
        - containerPort: 80
...
deployment.extensions/nginx-deployment edited
查看rs状态
代码语言:txt
复制
$ kubectl get rs
NAME                          DESIRED   CURRENT   READY   AGE
nginx-deployment-1764197365   2         2         2       24s
nginx-deployment-3167673210   0         0         0       35s   #为啥这里多了一条
nginx-deployment-2156724341   2         2         0       7s

由于这个 nginx:1.91 镜像在 Docker Hub 中并不存在,所以这个 Deployment 的“滚动更新”被触发后,会立刻报错并停止。

查看历史版本
代码语言:txt
复制
$ kubectl rollout history deployment/nginx-deployment
deployments "nginx-deployment"
REVISION    CHANGE-CAUSE
1           kubectl create -f nginx-deployment.yaml --record
2           kubectl edit deployment/nginx-deployment
3           kubectl set image deployment/nginx-deployment nginx=nginx:1.91
回滚到指定版本
代码语言:txt
复制
$ kubectl rollout undo deployment/nginx-deployment --to-revision=2
deployment.extensions/nginx-deployment
优化

我们对 Deployment 进行的每一次更新操作,都会生成一个新的 ReplicaSet 对象,是不是有些多余,甚至浪费资源呢?

所以,Kubernetes 项目还提供了一个指令,使得我们对 Deployment 的多次更新操作,最后 只生成一个 ReplicaSet。具体的做法是,在更新 Deployment 前,你要先执行一条 kubectl rollout pause 指令。它的用法如下所示:

代码语言:txt
复制
$ kubectl rollout pause deployment/nginx-deployment
deployment.extensions/nginx-deployment paused

修改之后执行

代码语言:txt
复制
$ kubectl rollout resume deployment/nginx-deployment
deployment.extensions/nginx-deployment resumed

查看rs

代码语言:txt
复制
$ kubectl get rs
NAME               DESIRED   CURRENT   READY     AGE
nginx-1764197365   0         0         0         2m
nginx-3196763511   3         3         3         28s

只有一个 hash=3196763511 的 ReplicaSet 被创建了出来

Deployment

无状态应用管理器

控制器定义

  • replicas

携带标签app=nginx的Pod数,被控制为2

被控制对象

  • template(PodTemplate)

控制器管理的Pod对象通常由template来定义(实际就是Pod定义)。在上图中被Deployment控制的Pod就是根据template创建出来的。

控制器模型是如何工作的?

  1. 从Etcd中获取携带app=nginx标签的Pod,统计数量(及为实际状态);
  2. Deployment中replicas数为期望状态;
  3. 控制器对俩个状态作比较,根据结果确定是删除还是创建Pod;

Statefulset

有状态应用,Statefulset概念中的状态究竟是指?从真实应用中可以抽象出俩种状态:1.拓扑;2.存储

拓扑

先后顺序

Headless Service
如何访问到service?
  • VIP

访问Service的IP地址(VIP),由service代理到Pod

  • DNS

Headless Service 的区别在于,不需要分配VIP,可以直接以DNS的方式解析出Pod的IP地址

Statefulset又如何通过DNS记录来维持Pod的拓扑状态?
  1. 为Pod分配对应编号(编号实际就是-拓扑状态)
  2. 删除Pod之后,Statefulset仍然会按原来的编号顺序依次创建
案例分析
代码语言:txt
复制
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: web
spec:
  serviceName: "nginx"   #使用ServiceName告诉StatefulSet控制器,通过Headless的方式解析Pod地址
  replicas: 2
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.9.1
        ports:
        - containerPort: 80
          name: web
  1. 查看hostname
代码语言:txt
复制

$ kubectl exec web-0 -- sh -c 'hostname'

web-0 #pod名-编号

$ kubectl exec web-1 -- sh -c 'hostname'

web-1

代码语言:txt
复制
  1. 使用nslookup解析headless service
代码语言:txt
复制

$ kubectl run -i --tty --image busybox:1.28.4 dns-test --restart=Never --rm /bin/sh

$ nslookup web-0.nginx

Server: 10.0.0.10

Address 1: 10.0.0.10 kube-dns.kube-system.svc.cluster.local #pod名.service名.svc.cluster.local

Name: web-0.nginx

Address 1: 10.244.1.7

$ nslookup web-1.nginx

Server: 10.0.0.10

Address 1: 10.0.0.10 kube-dns.kube-system.svc.cluster.local

Name: web-1.nginx

Address 1: 10.244.2.7

代码语言:txt
复制
  1. 删除Pod之后
代码语言:txt
复制

$ kubectl run -i --tty --image busybox dns-test --restart=Never --rm /bin/sh

$ nslookup web-0.nginx

Server: 10.0.0.10

Address 1: 10.0.0.10 kube-dns.kube-system.svc.cluster.local #dns不会改变

Name: web-0.nginx

Address 1: 10.244.1.8 #ip会变

$ nslookup web-1.nginx

Server: 10.0.0.10

Address 1: 10.0.0.10 kube-dns.kube-system.svc.cluster.local

Name: web-1.nginx

Address 1: 10.244.2.8

代码语言:txt
复制
  1. 通过这种方法,Kubernetes 就成功地将 Pod 的拓扑状态(比如:哪个节点先启动,哪个节点后启动),按照 Pod 的“名字 + 编号”的方式固定了下来。此外,Kubernetes 还为每一个 Pod 提供了一个固定并且唯一的访问入口,即:这个 Pod 对应的 DNS 记录

存储

存储数据

PV,PVC

PVC、PV 的设计,也使得 StatefulSet 对存储状态的管理成为了可能

什么是PVC?

PV(Persistence Volume),按字面可以理解为持久化卷,那PVC又是什么鬼?PVC(Persistence Volume Claim),持久化卷定义,按我的理解,它其实就是一个声明,类似于“接口与实现”的关系。这样一来,开发人员只需要简单的配置即可(复杂的卷的配置,让运维去做吧,反正不是我的锅)

代码语言:txt
复制
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: pv-claim   #此处配置PV一样
spec:
  accessModes:
  - ReadWriteOnce  #读写权限配置一下
  resources:
    requests:
      storage: 1Gi   #配置下大小,so easy,妈妈再也不用担心我不会配置卷了
volumeClaimTemplates
代码语言:txt
复制
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: web
spec:
  serviceName: "nginx"
  replicas: 2
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.9.1
        ports:
        - containerPort: 80
          name: web
        volumeMounts:
        - name: www
          mountPath: /usr/share/nginx/html
  volumeClaimTemplates:   						#看这里
  - metadata:
      name: www
    spec:
      accessModes:
      - ReadWriteOnce
      resources:
        requests:
          storage: 1Gi

不是说好的PVC么,这又是个什么玩意?从名字上看,好像是个template,好像和PodTemplate一个作用。也就是说,凡是被这个Statefulset管理的Pod都会声明一个PVC(说了半天,它就是个PVC的模板工厂)。

好奇怪哦,它是怎么知道创建哪个PV的PVC?
代码语言:txt
复制
$ kubectl create -f statefulset.yaml
$ kubectl get pvc -l app=nginx
NAME        STATUS    VOLUME                                     CAPACITY   ACCESSMODES   AGE
www-web-0   Bound     pvc-15c268c7-b507-11e6-932f-42010a800002   1Gi        RWO           48s
www-web-1   Bound     pvc-15c79307-b507-11e6-932f-42010a800002   1Gi        RWO           48s

上边可以看出来命名规则:<PVC 名字 >-<StatefulSet 名字 >-< 编号 >

总结

  1. Statefulset直接管理的是Pod
  2. Kubernetes通过Headless Service,为这些有编号的Pod,在DNS服务器生成同样带有编号的DNS记录
  3. 为每一个Pod创建带有编号的PVC
代码语言:txt
复制
apiVersion: extensions/v1beta1
kind: ingress
metadata:
  annotations:
    kubernetes.io/ingress.class: nginx
  name: my-ingress
  namespace: default
spec:
  rules:
  - host: www.hccqn.com
    http:
      paths:
      - backend:
          serviceName: nginx
          servicePort: 80
        path: /

DaemonSet

容器化守护进程,运行在Kubernetes集群中的每一个节点,并且每个节点只有一个实例。

  • 使用场景

那这个Pod到底有什么用呢?比如:网络插件,存储插件的Agent组件,各种监控组件和日志组件,必须运行在每一个节点上。

  • 实现原理

DaemonSet 只管理 Pod 对象,然后通过 nodeAffinity 和 Toleration 这两个调度器的小功能,保证了每个节点上有且只有一个 Pod。

Job和CronJob

Job Controller实际控制的作业的并行度,总共需完成的任务数俩个重要参数。Cron Job顾名思义就是定时Job

Job三种使用方法

  • 外部管理器 +Job 模板
  • 拥有固定任务数目的并行 Job
  • 指定并行度(parallelism),但不设置固定的 completions 的值

Cron Job

  • schedule
  • concurrencyPolicy

声明式API

概念

什么是声明式API?

命令式命令行操作

代码语言:txt
复制
$ docker service create --name nginx --replicas 2  nginx
$ docker service update --image nginx:1.7.9 nginx

命令式配置文件操作

代码语言:txt
复制
$ kubectl create -f nginx.yaml
$ kubectl replace -f nginx.yaml

声明式 API

代码语言:txt
复制
$ kubectl apply -f nginx.yaml

声明式API跟命令式操作有什么区别?

是否一次能处理多个写操作,并且具备 Merge 能力

案例分析-Istio
独特之处
  • 首先,所谓“声明式”,指的就是我只需要提交一个定义好的 API 对象来“声明”,我所期望的状态是什么样子。
  • 其次,“声明式 API”允许有多个 API 写端,以 PATCH 的方式对 API 对象进行修改,而无需关心本地原始 YAML 文件的内容。
  • 最后,也是最重要的,有了上述两个能力,Kubernetes 项目才可以基于对 API 对象的增、删、改、查,在完全无需外界干预的情况下,完成对“实际状态”和“期望状态”的调谐(Reconcile)过程。
Kubernetes编程范式

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

API对象

完整资源路径

Group/Version/Resource

创建流程
  1. 发起创建API的Post请求,Yaml信息被提交给APIServer
  2. 完成Url和Handler的绑定
  3. 创建API对象
  4. Admission() 和 Validation(),比如Admission Controller 和 Initializer都属于Admission。而 Validation,则负责验证这个对象里的各个字段是否合法
  5. APIServer会把验证过的API对象转换成用户最初提交的版本,进行序列化,并保存到Etcd
实践操作

编写CRD的yaml

代码语言:txt
复制
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
  name: networks.samplecrd.k8s.io
spec:
  group: samplecrd.k8s.io
  version: v1
  names:
    kind: Network
    plural: networks
  scope: Namespaced

GOPATH 下,创建一个结构如下的项目

代码语言:txt
复制
$ tree $GOPATH/src/github.com/<your-name>/k8s-controller-custom-resource
.
├── controller.go
├── crd
│   └── network.yaml
├── example
│   └── example-network.yaml
├── main.go
└── pkg
    └── apis
        └── samplecrd
            ├── register.go
            └── v1
                ├── doc.go
                ├── register.go
                └── types.go

我在 pkg/apis/samplecrd 目录下创建了一个 register.go 文件,用来放置后面要用到的全局变量

代码语言:txt
复制
package samplecrd

const (
 GroupName = "samplecrd.k8s.io"
 Version   = "v1"
)

我需要在 pkg/apis/samplecrd 目录下添加一个 doc.go 文件

代码语言:txt
复制
// +k8s:deepcopy-gen=package

// +groupName=samplecrd.k8s.io
package v1

添加 types.go 文件

代码语言:txt
复制
package v1
...
// +genclient
// +genclient:noStatus
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

// Network describes a Network resource
type Network struct {
 // TypeMeta is the metadata for the resource, like kind and apiversion
 metav1.TypeMeta `json:",inline"`
 // ObjectMeta contains the metadata for the particular object, including
 // things like...
 //  - name
 //  - namespace
 //  - self link
 //  - labels
 //  - ... etc ...
 metav1.ObjectMeta `json:"metadata,omitempty"`
 
 Spec networkspec `json:"spec"`
}
// networkspec is the spec for a Network resource
type networkspec struct {
 Cidr    string `json:"cidr"`
 Gateway string `json:"gateway"`
}

// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

// NetworkList is a list of Network resources
type NetworkList struct {
 metav1.TypeMeta `json:",inline"`
 metav1.ListMeta `json:"metadata"`
 
 Items []Network `json:"items"`
}

编写一个 pkg/apis/samplecrd/v1/register.go

代码语言:txt
复制
package v1
...
// addKnownTypes adds our types to the API scheme by registering
// Network and NetworkList
func addKnownTypes(scheme *runtime.Scheme) error {
 scheme.AddKnownTypes(
  SchemeGroupVersion,
  &Network{},
  &NetworkList{},
 )
 
 // register the type in the scheme
 metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
 return nil
}

我就要使用 Kubernetes 提供的代码生成工具,为上面定义的 Network 资源类型自动生成 clientset、informer 和 lister

代码语言:txt
复制
# 代码生成的工作目录,也就是我们的项目路径
$ ROOT_PACKAGE="github.com/resouer/k8s-controller-custom-resource"
# API Group
$ CUSTOM_RESOURCE_NAME="samplecrd"
# API Version
$ CUSTOM_RESOURCE_VERSION="v1"

# 安装k8s.io/code-generator
$ go get -u k8s.io/code-generator/...
$ cd $GOPATH/src/k8s.io/code-generator

# 执行代码自动生成,其中pkg/client是生成目标目录,pkg/apis是类型定义目录
$ ./generate-groups.sh all "$ROOT_PACKAGE/pkg/client" "$ROOT_PACKAGE/pkg/apis" "$CUSTOM_RESOURCE_NAME:$CUSTOM_RESOURCE_VERSION"

代码生成工作完成之后,我们再查看一下这个项目的目录结构:

代码语言:txt
复制
$ tree
.
├── controller.go
├── crd
│   └── network.yaml
├── example
│   └── example-network.yaml
├── main.go
└── pkg
    ├── apis
    │   └── samplecrd
    │       ├── constants.go
    │       └── v1
    │           ├── doc.go
    │           ├── register.go
    │           ├── types.go
    │           └── zz_generated.deepcopy.go
    └── client
        ├── clientset
        ├── informers
        └── listers

自定义控制器

基于声明式API的业务功能实现,往往通过控制器监视API对象的变化,然后决定具体的工作

自定义控制器工作原理
  1. 从APIServer获取自定义API对象(Network对象)
  2. Informer与API对象一一对映,给控制器传递的正是Informer对象
  3. 创建Informer工厂的时候,需要传递networkclient。Informer跟APIServer建立连接正是使用networkclient。
  4. 真正维护连接的是Informer的Relfector包,Relfector使用的的是ListAndWatch的方法。
  5. 一旦APIServer端有新的Network实例被创建、删除、修改。Reflector都会收到“事件通知”。该事件及对应的API对象的组合,被称为增量,会被放入Delta FIFO Queue。
  6. Informer不断从Queue里Pop增量,进而同步本地缓存(在 Kubernetes 里一般被叫作 Store)
  7. Informer的第二个职责,则是根据事件类型,触发事先注册好的ResourceEventHandler。
  8. 所谓 Informer,其实就是一个带有本地缓存和索引机制的、可以注册 EventHandler 的 client。它是自定义控制器跟 APIServer 进行数据同步的重要组件。
实现
编写main函数
代码语言:txt
复制
func main() {
  ...
  
  cfg, err := clientcmd.BuildConfigFromFlags(masterURL, kubeconfig)
  ...
  kubeClient, err := kubernetes.NewForConfig(cfg)
  ...
  networkClient, err := clientset.NewForConfig(cfg)
  ...
  
  networkInformerFactory := informers.NewSharedInformerFactory(networkClient, ...)
  
  controller := NewController(kubeClient, networkClient,
  networkInformerFactory.Samplecrd().V1().Networks())
  
  go networkInformerFactory.Start(stopCh)
 
  if err = controller.Run(2, stopCh); err != nil {
    glog.Fatalf("Error running controller: %s", err.Error())
  }
}
  1. 根据Master配置,创建Kubernetes client和Network client,如果没有提供,使用InClusterConfig的方式创建client
  2. 为Network对象创建一个叫做InformerFactory的工厂,并生成Network的Informer
  3. 启动Informer,启动自定义控制器
编写自定义控制器
代码语言:txt
复制
func NewController(
  kubeclientset kubernetes.Interface,     
  networkclientset clientset.Interface,
  networkInformer informers.NetworkInformer) *Controller {
  ...
  controller := &Controller{
    kubeclientset:    kubeclientset,
    networkclientset: networkclientset,
    networksLister:   networkInformer.Lister(),
    networksSynced:   networkInformer.Informer().HasSynced,
    workqueue:        workqueue.NewNamedRateLimitingQueue(...,  "Networks"),
    ...
  }
    networkInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
    AddFunc: controller.enqueueNetwork,
    UpdateFunc: func(old, new interface{}) {
      oldNetwork := old.(*samplecrdv1.Network)
      newNetwork := new.(*samplecrdv1.Network)
      if oldNetwork.ResourceVersion == newNetwork.ResourceVersion {
        return
      }
      controller.enqueueNetwork(new)
    },
    DeleteFunc: controller.enqueueNetworkForDelete,
 return controller
}
  1. 入参:函数里创建的两个 client,以及Informer
  2. 为 networkInformer 注册了三个 Handler(AddFunc、UpdateFunc 和 DeleteFunc)
编写控制器里的业务逻辑
代码语言:txt
复制
func (c *Controller) runWorker() {
  for c.processNextWorkItem() {
  }
}

func (c *Controller) processNextWorkItem() bool {
  obj, shutdown := c.workqueue.Get()
  
  ...
  
  err := func(obj interface{}) error {
    ...
    if err := c.syncHandler(key); err != nil {
     return fmt.Errorf("error syncing '%s': %s", key, err.Error())
    }
    
    c.workqueue.Forget(obj)
    ...
    return nil
  }(obj)
  
  ...
  
  return true
}

func (c *Controller) syncHandler(key string) error {

  namespace, name, err := cache.SplitMetaNamespaceKey(key)
  ...
  
  network, err := c.networksLister.Networks(namespace).Get(name)
  if err != nil {
    if errors.IsNotFound(err) {
      glog.Warningf("Network does not exist in local cache: %s/%s, will delete it from Neutron ...",
      namespace, name)
      
      glog.Warningf("Network: %s/%s does not exist in local cache, will delete it from Neutron ...",
    namespace, name)
    
     // FIX ME: call Neutron API to delete this network by name.
     //
     // neutron.Delete(namespace, name)
     
     return nil
  }
    ...
    
    return err
  }
  
  glog.Infof("[Neutron] Try to process network: %#v ...", network)
  
  // FIX ME: Do diff().
  //
  // actualNetwork, exists := neutron.Get(namespace, name)
  //
  // if !exists {
  //   neutron.Create(namespace, name)
  // } else if !reflect.DeepEqual(actualNetwork, network) {
  //   neutron.Update(namespace, name)
  // }
  
  return nil
}
  1. 从工作队列Pop一个成员,Key(Network的namespcace/name)
  2. 在syncHandler中使用key从Informer中拿到Network对象
  3. 期望状态与实际状态对比,完成一次(调协)reconcile

PV/PVC

网络

跨容器网络

网络栈

被隔离的容器进程,如何跟其它Network Namespace 网络栈里的容器进程进行交互?

现实当中,俩台主机通信,需要通过网线连接到交换机,而在linux中,能够起到交换机作用的网络设备,是网桥(Bridge)。他是一个工作在数据链路层的设备,主要功能是根据MAC地址学习来将数据包转发到网桥的不同端口上。

Docker项目会默认在宿主机上创建一个名叫docker0的网桥,凡是连接到docker0上的容器可以通过它来通信。

如何把容器“连到”到docker0上?

这时候,需要使用一个名叫Veth Pair的虚拟设备。它被创建出来之后,总是以俩张虚拟网卡(Veth Peer)的形式成对出现。并且其中一个“网卡“发出的数据包,可以直接出现在它对应的另一张”网卡“上,哪怕是在不同的Network Namespace里。

这就使得Veth pair常常被用作连接不同Network Namespace的“网线”。

代码语言:txt
复制
apiVersion: v1
kind: Service
metadata:
  annotations:
    service.kubernetes.io/loadbalance-id: lb-f926asqz
  creationTimestamp: "2020-10-06T12:44:28Z"
  labels:
    app: nginx-ingress
    chart: nginx-ingress-1.36.3
    component: controller
    heritage: Helm
    release: nginx-ingress
  name: nginx-ingress-controller
  namespace: default
  resourceVersion: "9367815813"
  selfLink: /api/v1/namespaces/default/services/nginx-ingress-controller
  uid: ac3c4f2e-07d1-11eb-89ed-06b6185c18a1
spec:
  clusterIP: 172.16.255.152
  externalTrafficPolicy: Cluster
  ports:
  - name: http
    nodePort: 31156
    port: 80
    protocol: TCP
    targetPort: http
  - name: https
    nodePort: 32234
    port: 443
    protocol: TCP
    targetPort: https
  selector:
    app: nginx-ingress
    app.kubernetes.io/component: controller
    release: nginx-ingress
  sessionAffinity: None
  type: LoadBalancer
status:
  loadBalancer:
    ingress:
    - ip: 49.233.242.198
代码语言:txt
复制
apiVersion: v1
kind: Service
metadata:
  creationTimestamp: "2020-10-06T12:44:28Z"
  labels:
    app: nginx-ingress
    chart: nginx-ingress-1.36.3
    component: default-backend
    heritage: Helm
    release: nginx-ingress
  name: nginx-ingress-default-backend
  namespace: default
  resourceVersion: "9367811830"
  selfLink: /api/v1/namespaces/default/services/nginx-ingress-default-backend
  uid: ac3b3633-07d1-11eb-89ed-06b6185c18a1
spec:
  clusterIP: 172.16.255.144
  ports:
  - name: http
    port: 80
    protocol: TCP
    targetPort: http
  selector:
    app: nginx-ingress
    app.kubernetes.io/component: default-backend
    release: nginx-ingress
  sessionAffinity: None
  type: ClusterIP
status:
  loadBalancer: {}

跨主机网络

Kubernetes网络模型与CNI网络

丝雀发布/蓝绿发布

Kubenetes部署常见的错误

  1. CPU资源不足

作业调度与资源管理

资源管理与模型

Kubernetes资源模型

  1. 按类别可分为:
  • 可压缩资源 -CPU
  • 不可压缩资源 -Memory
  1. Pod的CPU和Memory,实际上还分俩种情况:
  • request
  • limit
  1. Borg中对于“动态资源边界”的定义:

Borg在作业提交时,会主动减小资源限额配置,以容纳更多作业,提高资源利用率,而资源使用量到达一定阈值,Borg会通过快速恢复过程,快速还原原始资源限额,防止出现异常。

  1. limit和request的目的:

用户提交Pod时,声明较小的request供调度器使用,而Kubernetes真正设置给容器Cgroups的,则是相对较大的limits

Qos模型

Kubernetes进行Eviction的具体策略

Pod按配置分类:

  • Guaranteed

同时设置了request和limits并且俩者相同

  • Burstable

至少有一个container设置了requests

  • BestEffort

都没设置

Eviction发生时,kubectl会挑选Pod进行删除操作

  • 首当其冲,自然时BestEffort类别的Pod
  • 其次,属于Burstable的类别,并且发生“饥饿”的资源使用量已经超出了requests的Pod
  • 最后才是Guaranteed的Pod,并且,kubernetes会保证只有当Pod超出limits,或者宿主机本身正处于Memory Pressure,才可能被选择

Evivtion阈值

代码语言:txt
复制
memory.available<100Mi  #内存
nodefs.available<10%    #可用磁盘
nodefs.inodesFree<5%    #节点磁盘
imagefs.available<15%   #镜像存储空间
代码语言:txt
复制
kubelet --eviction-hard=imagefs.available<10%,memory.available<500Mi,nodefs.available<5%,nodefs.inodesFree<5% --eviction-soft=imagefs.available<30%,nodefs.available<10% --eviction-soft-grace-period=imagefs.available=2m,nodefs.available=2m --eviction-max-pod-grace-period=600

可以看到eviction其实分为Soft和Hard俩种模式,Soft Eviction 允许你为 Eviction 过程设置一段“优雅时间”,比如上面例子里的 imagefs.available=2m,就意味着当 imagefs 不足的阈值达到 2 分钟之后,kubelet 才会开始 Eviction 的过程。

默认调度器

kubernetes调度器的主要职责就是为新创建出来的Pod,寻找一个最合适的节点(Node)。最适合包括俩层含义:根据调度算法1.寻找可运行该Pod的节点;2.从上述节点中寻找最符合条件的;

原理

Kubernetes调度器的核心其实就是俩个相互独立的循环控制

Informer Path

它的主要目的,是启动一系列的Informer,用来监听Etcd中的Pod、Node、Service等与调度有关的API对象的变化。这种“变化”会被放入调度队列。

  • 默认情况下,调度队列是PriorityQueue
  • 负责对调度器缓存的更新
    • 集群信息 Cache 化,以便从根本上提高 Predicate 和 Priority 调度算法的执行效率
Scheduling Path

主要目的就是选一个最适合的Node

  1. 出队一个Pod
  2. 找到所有可以运行该Pod的Node
  3. 筛选最合适的
  4. 修改该Pod的nodeName=Node名(Kubernetes 里面被称作 Bind)
  • 异步bind 注意,这里是异步bind,只是更新Scheduler Cache里Pod和Node的信息(不在关键路径访问API Server)。这种基于“乐观”假设的API更新方式(Assume),即使bind失败也没关系,Cache同步之后一切恢复如初。
  • 二次确认 正式由于“乐观”设定,Pod在某个节点运行之前需要“二次确认”。(了解这一步 Admit 操作,实际上就是把一组叫作 GeneralPredicates 的、最基本的调度算法,比如:“资源是否可用”“端口是否冲突”等再执行一遍,作为 kubelet 端的二次确认)
  • 无锁化 在并发路径上,尽量避免使用全局资源,观察上述核心原理图,会发现,只有对调度队列和scheduler cache操作才需要加锁,而这俩个操作都不在关键路径上。
  1. 为什么说kubernetes调度器处于十字路口,未来发展方向如何?

Kubernetes容器运行时

SIG-Node与CRI

Pod调度成功之后,需要在宿主机上创建出来,并把定义的容器启动起来,这部分功能正是Kubelet这个核心组件的功能

工作核心

  1. kubelet的工作核心,就是一个循环控制(SyncLoop),而驱动这个循环控制的事件,包括四种:
    • Pod更新事件
    • Pod生命周期变化
    • Kubelet本身设置的执行周期
    • 定时清理事件
  2. kubelet启动的第一件事情就是注册各种事件的Informer,这些Informer就是SyncLoop需要处理的数据来源。
  3. kubelet还负责维护子控制循环,一般被称作某某Manager。
    • 通过控制器模式,完成kubelet的某项具体职责
    • 比如Node Status Manager,负责响应Node状态的变化
  4. kubelet也是通过watch的机制,监听了与自己相关的Pod变化,Pod变化会触发绑定在kubelet在循环控制里注册的Handler。
  5. 在具体的处理过程中,Kubelet会启动一个名叫Pod Update Workder的,单独的Goroutine来处理工作。
  6. 然后,调运下层运行时,当然,并不是直接调用Doker,而是通过CRI的gRPC接口来间接调用(解耦)。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Pod
    • Pod实现原理
      • 思维拓展
      • 白话
      • Pod状态
      • API对象
      • 容器设计模式
  • 控制器
    • 控制器模式
      • 实现原理
      • 水平扩展/收缩
      • 滚动更新
    • Deployment
      • 控制器定义
      • 被控制对象
      • 控制器模型是如何工作的?
    • Statefulset
      • 拓扑
      • 存储
      • 总结
    • DaemonSet
      • Job和CronJob
        • Job三种使用方法
        • Cron Job
      • 声明式API
        • 概念
        • API对象
        • 自定义控制器
    • PV/PVC
    • 网络
      • 跨容器网络
        • 网络栈
    • Kubenetes部署常见的错误
    • 作业调度与资源管理
      • 资源管理与模型
        • Kubernetes资源模型
        • Qos模型
      • 默认调度器
        • 原理
    • Kubernetes容器运行时
      • SIG-Node与CRI
        • 工作核心
    相关产品与服务
    容器服务
    腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档