1. 4.Kubernetes入门

虽然Docker中已经集成了docker-comose、docker-swarm等容器编排技术。但是容器编排中的头把交椅,还要属于Kubernetes。

1.1. 4.1.认识Kubernetes

Kubernetes 是 Google 团队发起的开源项目,它的目标是管理跨多个主机的容器,提供基本的部署,维护以及运用伸缩,主要实现语言为 Go 语言。Kubernetes 是:

  • 易学:轻量级,简单,容易理解
  • 便携:支持公有云,私有云,混合云,以及多种云平台
  • 可拓展:模块化,可插拔,支持钩子,可任意组合
  • 自修复:自动重调度,自动重启,自动复制

Kubernetes 构建于 Google 数十年经验,一大半来源于 Google 生产环境规模的经验。结合了社区最佳的想法和实践。

在分布式系统中,部署,调度,伸缩一直是最为重要的也最为基础的功能。Kubernetes 就是希望解决这一序列问题的。

1.2. 4.2.安装说明

Kubernetes的官方原生安装方式非常麻烦,就算是专业的运维人员也需要花费大量的时间来做编译、安装,脚本的编写等。

不过作为Kubernetes的入门学习,我们可以通过两种快捷的方式来安装:

  • 使用minikube单机安装
  • 使用kubeadm集群安装

1.3. 4.3单机安装

单机安装其实是利用docker来模拟多个Kubernetes的节点,然后利用提前准备好的镜像,一键完成安装。

其难点是这些镜像在国内下载速度非常慢,好在阿里巴巴提供了对应的国内镜像源,也就是国内版本的minikube:https://github.com/AliyunContainerService/minikube

大家可以参考该网站的步骤安装,但是从国外下载镜像速度会非常慢。

为了方便大家使用,我已经在课前资料中提供好了一份kubectl和minikube:

image-20201007175348012

大家可以按照下面的步骤按照:

1.3.1. 4.3.1.上传文件

首先把课前资料提供的kubectlminukube上传到/usr/local/bin目录下:

image-20201007175427712

1.3.2. 4.3.2.修改权限

然后,进入/usr/local/bin目录中,修改文件权限:

cd /usr/local/bin

chmod +x ./kubectl
chmod +x ./minikube

1.3.3. 4.3.3.添加用户权限

minikube不能用管理员root身份运行,需要用一个新的用户,比如leyou用户。

添加新用户命令:

# 添加用户
useradd leyou
# 设置密码
passwd leyou

然后给leyou用户授权:

usermod -aG docker leyou && newgrp docker

然后用leyou用户登录,或者切换到leyou用户。

切换leyou用户的命令:

su - leyou

1.3.4. 4.3.4.准备docker镜像

其实到这里minikube已经可以运行了,不过初次运行需要拉取一个900mb左右的镜像,非常慢,这里我给大家准备了一个提前下载好的镜像:

image-20201009100514436

大家上传到虚拟机,通过docker load命令加载即可:

docker load -i kicbase.tar

1.3.5. 4.3.5.启动

执行命令:

minikube start \
--driver=docker \
--registry-mirror=https://n0dwadw41emtq.mirror.aliyuncs.com \
--memory=2048mb

参数说明:

  • --driver=docker:是用docker来模拟k8s节点,也可以用vbox虚拟机
  • --registry-mirror:指定镜像下载地址,这里是从阿里云下载,提高下载速度
  • --memory=2048mb:内存分配,这里用2G内存,不能再低了。

等待几分钟的镜像下载后,minikube就启动成功了:

image-20201009100703848

其它命令:

# 停止minikube
minikube stop
# 删除minikube创建的一切docker容器
minikube delete --all

1.4. 4.4.集群安装

在生产环境下,我们必须使用集群的方式安装Kubernetes,但是原生安装方式非常复杂,成为了学习Kubernetes的拦路虎。

到了2017 年,在志愿者的推动下,Kubernetes社区才终于发起了一个独立的部署工具,名叫:kubeadm,https://github.com/kubernetes/kubeadm。

可以非常便捷的实现Kubernetes集群的安装。

1.4.1. 4.4.1.环境要求

既然是集群安装,我们必须准备至少2台虚拟机,这里我会准备三台虚拟机,每台虚拟机必须满足下列要求:

  • 以root身份安装

  • 内存:至少2G

  • 硬盘:至少10G
  • 主机名:每个节点都必须有唯一主机名,不能是localhost,不包含下划线、小数点、大写字母
  • IP:每个节点都必须有唯一静态IP
  • 网络:各个节点内部网络互通,各个节点外网访问不受限,且没有防火墙、安全组隔离
  • 软件:每个节点都必须安装docker和kubeadm,且版本一致
  • 操作系统:本案例中,我们统一使用CentOS操作系统,版本不得低于CentOS7.6(CentOS7.1810以上),内核版本不低于3.10

建议大家先配置一台虚拟机,然后克隆出另外2个。我计划的虚拟机节点信息如下:

主机名 IP 操作系统 内存 硬盘 k8s角色
node1 192.168.150.101 CentOS7.6 2GB 10GB master
node2 192.168.150.102 CentOS7.6 2GB 10GB worker
node3 192.168.150.103 CentOS7.6 2GB 10GB worker

其中,node1节点会作为k8s中的master节点,下面的安装我会以master节点为例来说明。

1.4.2. 4.4.2.配置静态IP

首先进入网络配置的目录:

cd /etc/sysconfig/network-scripts/

然后查看文件:

ls

如下:

image-20201009102700033

可以看到两个以 ifcfg-开头的文件名(名称可能不一样),这个就是网卡配置文件了。

ifcfg-lo:这个是本地网络,忽略,剩下的那个就是你的NAT网卡了。

编辑这个文件:

vi ifcfg-ens33

修改如下配置:

BOOTPROTO=DHCP  -----> BOOTPROTO=static

然后添加下列配置:

IPADDR0=192.168.150.101
NETMASK=255.255.255.0
GATEWAY0=192.168.150.2
DNS1=192.168.150.2

注意,其中的150不是固定的,取决于你自己的虚拟网卡的网段:

image-20201009103814183

可以再vmvare中查看自己的网段:

image-20201009102923441

然后选中vmnet8的NAT网卡,即可看到子网IP的网段:

image-20201009103035272

完成以后,输入命令重置网络:

systemctl restart network

然后输入命令,查看新的ip是否生效:

ip addr

image-20201009103158406

测试网络:

ping www.baidu.com

如图:

image-20201009103251659

1.4.3. 4.4.3.配置hostname

每个虚拟机节点都必须有自己的hostname,需要分别修改3台虚拟机的hostname为node1、node2、node3。

# 修改 hostname
hostnamectl set-hostname node1
# 查看修改结果
hostnamectl status

image-20201009110549065

配置hostname的解析:

# 设置 hostname 解析
echo "127.0.0.1   $(hostname)" >> /etc/hosts

然后,通过ping的方式,检测配置是否成功:

image-20201009110458947

1.4.4. 4.4.4.安装docker

略,请参考之前的课程来安装

注意:每台虚拟机都需要安装

1.4.5. 4.4.5.安装kubeadm

上传课前资料提供的安装脚本到3台虚拟机节点的任意目录:

image-20201009141333575

然后分别执行命令:

# 添加执行权限
chmod +x install_kubectl.sh
# 设置镜像源
export REGISTRY_MIRROR=https://registry.cn-hangzhou.aliyuncs.com
# 执行脚本,并指定安装的k8s版本为1.18.6
sh install_kubectl.sh 1.18.6

注意:每台虚拟机都需要安装kubeadm

1.4.6. 4.4.6.初始化master节点

接下来,在master节点(node1)上上传脚本:

image-20201009141602376

然后在 master 节点执行执行命令,以初始化k8s集群:

chmod +x init_master.sh
export MASTER_IP=192.168.150.101
export APISERVER_NAME=apiserver
export POD_SUBNET=10.100.0.1/16
echo "${MASTER_IP}    ${APISERVER_NAME}" >> /etc/hosts
sh init_master.sh 1.18.6

这里一般需要5~10分钟。

然后通过命令监控master节点初始化过程:

watch kubectl get pod -n kube-system -o wide

执行如下命令,等待 3-10 分钟,直到所有的容器组处于 Running 状态

image-20201009142431841

然后,通过命令,查看master节点初始化结果

kubectl get nodes -o wide

image-20201009142624069

1.4.7. 4.4.7.加入worker节点

worker节点要想加入k8s集群,必须获取主节点的token令牌。

因此,我们先在master节点中获取令牌,在master节点执行命令:

# 只在 master 节点执行
kubeadm token create --print-join-command

应该可以得到下面的提示:

kubeadm join apiserver:6443 --token iiy9gn.dqfsxppp7rsf7obp     --discovery-token-ca-cert-hash sha256:6e8bfc29867ee5974b0838148ea48127df6a79f55992e3221bd3a9bc82aae27b

然后,在每一台worker节点,本例中的 node2 和node3中执行下面的命令即可:

export MASTER_IP=192.168.150.101
export APISERVER_NAME=apiserver
echo "${MASTER_IP}    ${APISERVER_NAME}" >> /etc/hosts
kubeadm join apiserver:6443 --token iiy9gn.dqfsxppp7rsf7obp     --discovery-token-ca-cert-hash sha256:6e8bfc29867ee5974b0838148ea48127df6a79f55992e3221bd3a9bc82aae27b

注意:该命令的kubeadm join ...以后的部分是来自master的提示结果,不要照搬照抄

最后,应该看到下面的执行结果:

image-20201009143223799

1.4.8. 4.4.8.检查节点状态

在master节点执行命令,检查各个节点的状态:

kubectl get nodes -o wide

2~5分钟后,可以看到下面的结果:

image-20201009143542125

1.5. 4.5.快速入门

下面,我们通过一个Demo来部署我们的第一个Kubernetes项目。

1.5.1. 4.5.1.基本概念

首先,来了解下Kubernetes中的一些概念,如图:

image-20201009144754304

这是一个简化的Kubernetes集群,其中包含了一个master和两个普通node节点,并且包含了下面的一些角色:

  • container:容器,与我们学习Docker时的容器概念一致
  • Pod:一组紧密关联的容器,共享卷和网络,例如tomcat和运行在tomcat上的war包
  • Node:节点,一个节点上可以运行多个Pod
  • Replication Controller:副本控制器,用来管理一个Pod的多个副本
  • Service(enter image description here):服务,可以看做是多个Pod副本的负载均衡入口,用户不能直接访问Pod,而必须访问Service,由service负载均衡到某个Pod

1.5.2. 4.5.2.部署一个应用

部署应用需要用到Kubernetes中的Deployment,Deployment是Kubernetes中的一种对象,用来描述要部署的Pod的模板,例如Pod中的应用程序(image),部署在哪个节点(node),部署几个备份(replica)等信息。

Pod是部署的基本单位,Deployment定义后,Kubernetes会根据Deployment中的规则来创建Pod并根据定义的规则来部署指定数量的备份,Kubernetes Deployment Controller 会持续监控这些实例。如果运行实例的 worker 节点关机或被删除,则 Kubernetes Deployment Controller 将在群集中资源最优的另一个 worker 节点上重新创建一个新的实例。这提供了一种自我修复机制来解决机器故障或维护问题。

而Kubernetes中的对象,都是以yaml的格式来定义的。因此,我们需要先创建一个yaml文件,来描述我们的Deployment。

在任意目录下创建一个名为nginx-deployment.yaml的文件,并编写下面内容:

apiVersion: apps/v1    #语法版本
kind: Deployment #该对象的类型,我们使用的是 Deployment
metadata: #译名为元数据,即 Deployment 的一些基本属性和信息
  name: nginx-deployment    #Deployment 的名称
spec: #该Deployment的描述,可以理解为你期待该Deployment在k8s中如何部署
  replicas: 2    #Pod部署的副本数量,这里是2
  selector:        #标签选择器,可以选定一个符合规则的节点去部署
    matchLabels: #选择包含标签app:nginx的节点
      app: nginx
  template:        #这是选择或创建的Pod的模板
    metadata:    #Pod的元数据
      labels:    #Pod的标签,一组自定义的键值对,以后可以根据标签找到该pod
        app: nginx
    spec:    # 描述pod部署的应用,
      containers:    # 可以是多个容器
      - name: nginx    # 本例只部署了一个nginx
        image: nginx:1.7.9    #使用镜像nginx:1.7.9创建container
        ports: 
        - containerPort: 80 # 容器使用80端口

然后执行命令来部署应用:

kubectl apply -f nginx-deployment.yaml

1.5.3. 4.5.3.查看应用状态

输入命令可以查看应用的部署状态:

kubectl get pods

image-20201009151027152

发现容器正在创建中,说明容器启动需要一段时间。而我们发现得到了两个Pod,名字分别是:nginx-deployment-5bf87f5f59-g4mw2 和 nginx-deployment-5bf87f5f59-vbk84

这是因为我们的Deployment设置了replicas=2,代表需要两个副本,因此该Pod就部署了两份。

一段时间后,再次查看,发现容器已经运行了:

image-20201009151147162

此外, 你还可以使用 kubectl describe 命令,查看一个 API 对象的细节,比如:

[root@localhost ~]# kubectl describe pod nginx-deployment-5bf87f5f59-g4mw2
Name:         nginx-deployment-5bf87f5f59-g4mw2
Namespace:    default
Priority:     0
Node:         node3/192.168.150.103
Start Time:   Fri, 09 Oct 2020 15:09:24 +0800
Labels:       app=nginx
              pod-template-hash=5bf87f5f59
Annotations:  cni.projectcalico.org/podIP: 10.100.135.1/32
              cni.projectcalico.org/podIPs: 10.100.135.1/32
Status:       Running
IP:           10.100.135.1
IPs:
  IP:           10.100.135.1
Controlled By:  ReplicaSet/nginx-deployment-5bf87f5f59
Containers:
  nginx:
    Container ID:   docker://aeba73955fa7a71111cc7ac7a2da7624be320816f07e3ae3884ed4f863166ea5
    Image:          nginx:1.7.9
    Image ID:       docker-pullable://nginx@sha256:e3456c851a152494c3e4ff5fcc26f240206abac0c9d794affb40e0714846c451
    Port:           80/TCP
    Host Port:      0/TCP
    State:          Running
      Started:      Fri, 09 Oct 2020 15:10:35 +0800
    Ready:          True
    Restart Count:  0
    Environment:    <none>
    Mounts:
      /var/run/secrets/kubernetes.io/serviceaccount from default-token-2hw9v (ro)
Conditions:
  Type              Status
  Initialized       True
  Ready             True
  ContainersReady   True
  PodScheduled      True
Volumes:
  default-token-2hw9v:
    Type:        Secret (a volume populated by a Secret)
    SecretName:  default-token-2hw9v
    Optional:    false
QoS Class:       BestEffort
Node-Selectors:  <none>
Tolerations:     node.kubernetes.io/not-ready:NoExecute for 300s
                 node.kubernetes.io/unreachable:NoExecute for 300s
Events:
  Type    Reason     Age    From               Message
  ----    ------     ----   ----               -------
  Normal  Scheduled  3m14s  default-scheduler  Successfully assigned default/nginx-deployment-5bf87f5f59-g4mw2 to node3
  Normal  Pulling    3m13s  kubelet, node3     Pulling image "nginx:1.7.9"
  Normal  Pulled     2m3s   kubelet, node3     Successfully pulled image "nginx:1.7.9"
  Normal  Created    2m3s   kubelet, node3     Created container nginx
  Normal  Started    2m3s   kubelet, node3     Started container nginx

可以看到这个Pod的部署过程,从拉取镜像、创建容器、启动容器等,并且该Pod被部署到了node3。相同方式去查看,可以发现另一个Pod被部署到了node2。

1.5.4. 4.5.4.公布应用

虽然nginx部署了,但是现在还是无法访问的,而且启动了多个nginx,肯定是需要负载均衡的,这就要用到Kubernetes中的Service对象。

Service对象为某些具备共同特征(一般是通过label选择)的Pod提供统一的对外访问方式,并在内部实现负载均衡。

image-20201009161335325

如图:有3个Pod,其中一个Pod的label声明是app=A,而另外两个的声明是app=nginx。而在service中,通过选择器selector选中app=nginx的Pod。

当用户访问service时,请求就会分发到这两个Pod中。

下面,我们定义一个nginx-svc.yaml文件,来描述这个service:

apiVersion: v1
kind: Service
metadata:
  name: nginx-service    #Service 的名称
  labels:         #Service 自己的标签
    app: nginx    #为该 Service 设置 key 为 app,value 为 nginx 的标签
spec:        #这是关于该 Service 的定义,描述了 Service 如何选择 Pod,如何被访问
  selector:        #标签选择器
    app: nginx    #选择包含标签 app:nginx 的 Pod
  ports:
  - name: nginx-port    # 端口映射规则的名字
    protocol: TCP        #协议类型 TCP/UDP
    port: 80    # 集群内的其他Pod通过 80 端口访问 Service
    nodePort: 32600   #外部请求通过任意节点的 32600 端口访问 Service
    targetPort: 80    #将请求转发到匹配 Pod 的 80 端口
  type: NodePort    #Serive的类型,包括:ClusterIP/NodePort/LoaderBalancer

然后通过命令来启动:

kubectl apply -f nginx-svc.yaml

通过浏览器,访问地址:http://192.168.150.101:32600即可看到下面结果:

image-20201009162142384

当然,访问其它两个节点也能看到一样的结果。

1.6. 4.6.动态伸缩

现在,假设我们需要给nginx集群多添加两个副本,给怎么做呢?

Kubernetes的Deployment会监视当前运行的Pod集群状态,以及期望的Pod集群状态,并在当前状态与期望状态有差异时,进行自动的调谐(Reconcile)。

例如:label为app=nginx的pod,期望的replicas=4,但是检测到目前处于running状态的Pod数量为2,那么Deployment控制器就会启动两个新的Pod,满足期望值,实现集群扩容。类似的,集群收缩也是这样处理的。

现在,我们修改之前定义的nginx-deployment.yaml文件,其中的replicas修改为4:

apiVersion: apps/v1
kind: Deployment 
metadata:
  name: nginx-deployment
spec:
  replicas: 4    #Pod部署的副本数量,这里是4
# 。。。

然后,再次执行命令:

kubectl apply -f nginx-deployment.yaml

Kubernetes会对比两次yaml文件的差异,发现replicas数值的变化,并动态对Pod集群动态扩容:

image-20201009163633852

最终,Pod数量会变为4个。

1.7. 4.7.滚动更新

与动态伸缩类似,当我们更像了Deployment中的镜像版本时,Deployment的控制器会以滚动更新的方式来升级集群中的每个Pod的版本。

例如,我们将nginx从1.7.9版本更新到1.8版本,其基本流程是这样的:

  • 一开始,service将所有流量负载均衡到原来的Pod中。(1.7.9的nginx)
  • 当更新了Deployment描述的nginx版本为1.8时,触发滚动更新
  • Deployment会创建一个版本为1.8的nginx的Pod,并关闭一个旧版本的Pod
  • Service则将这个新的Pod加入负载均衡集群中,剔除旧的那个Pod
  • 重复以上动作,新的Pod逐渐替换掉所有旧的Pod

动手试试吧!

首先,修改之前的nginx-deployment.yaml中的镜像版本,从1.7.9修改到1.8:

# ...略
    spec:    # 描述pod部署的应用,
      containers:    # 可以是多个容器
      - name: nginx    # 本例只部署了一个nginx
        image: nginx:1.8    #使用镜像nginx:1.7.9创建container
        ports: 
        - containerPort: 80 # 容器使用80端口

然后,再次执行命令:

kubectl apply -f nginx-deployment.yaml

然后可以通过命令查看pod的状态变化:

watch kubectl get pods -l app=nginx

image-20201009165733010

可以发现,新的Pod正在被创建,旧的Pod正在被终止(Terminating)

另外,整个滚动更新的过程也可以通过命令查看:

kubectl rollout status deployment/nginx-deployment

结果:

image-20201009165928180

1.8. 4.8.POD的一些细节

Pod是Kubernetes中的基本调度单元,这与DockerSwarm不同,DockerSwarm中调度的基本单元就是容器。

而Pod是一组具有紧密联系的容器组(当然也可以是一个容器),Kubernetes在做Pod部署时,会让这一组容器共享卷、网络,甚至于可以通过localhost互相访问,减少网络IO,提高数据交互的效率。

为什么需要Pod?

容器的本质是一个进程,在实际部署时,往往会有许多有紧密联系的进程。用容器做调度时,每个进程都是独立容器,隔离开来,导致容器的调度变的复杂,效率也低。

而Pod把多个容器作为容器组一起调度,共享数据卷、网络、安全策略等等,可以看做是模拟了一台虚拟机,而其中的容器就是虚拟机中运行程序,更贴合实际部署的需求。

1.8.1. 4.8.1.基本语法

语法说明

我们来看看Pod定义的基本语法:

apiVersion: v1
kind: Pod
metadata:
  name: nginx
spec:
  hostAliases:
  - ip: "10.1.2.3"
    hostnames:
    - "foo"
    - "bar"
  nodeSelector:   
    disktype: ssd
  containers:    # 可以是多个容器
  - name: nginx     # 本例只部署了一个nginx
    image: nginx:1.7.9    #使用镜像nginx:1.7.9创建container
    ports: 
    - containerPort: 80 # 容器使用80端口
    volumeMounts:
    - name: nginx-vol
      mountPath: "/usr/share/nginx/html"
  volumes:
  - name: nginx-vol
    hostPath: 
      path: /tmp/html

语法说明:

  • apiVersion: v1: 使用的语法版本,使用 kubectl api-versions 即可查看当前集群支持的版本

  • kind: Pod: 当前对象的类型,这里是Pod类型,入门案例使用的是Deployment类型

  • metadat: 元数据,是对当前Pod的一些自定义描述

    • name: nginx: 给这个Pod起个名字,叫做nginx
  • spec: Pod的详细信息,包括容器、网络、数据卷等

    • hostAliases: 定义hosts信息,将来会写入容器的 /etc/hosts文件中

      • ip: "10.1.2.3": 要写入的ip

      • hostnames: IP对应的主机名,可以写多个,本例中是:foobar,最后在/etc/hosts中会形成这样的效果:

        10.1.2.3    foo    bar
        
    • nodeSelector: 容器部署时对节点的选择规则

      • disktype: ssd: 只选择SSD硬盘的节点部署
    • containers: Pod中的容器描述,一般是一个或多个紧密相连的容器

      • name: nginx: 容器的名称
      • image: nginx:1.7.9: 容器使用的镜像
      • ports: 容器的端口配置,可以有多个
        • containerPort: 80:容器使用的一个端口
      • volumeMounts: 要挂载的数据卷,可以有多个
        • name: nginx-vol:使用的数据卷的名称,要与Pod定义的数据卷名称一致
        • mountPath: "/usr/share/nginx/html": 挂载的卷在容器内对应的目录
    • volumes:Pod中的数据卷描述,可以有多个,被Pod中的多个容器共享

      • name: nginx-vol: 卷的名称,在容器中引用时指定名称
      • hostPath: 卷的类型,可以有多种,这里的hostpath代表挂载到宿主机的目录
        • path: /tmp/html: 宿主机对应的挂载目录

除了上述最常用的语法外,Pod中还有很多其它的语法,我们会逐渐来学习。

动手试试

首先,我们在每一个节点中都创建/tmp/html目录并放入index.html文件,执行下列命令:

# 创建目录
mkdir /tmp/html
# 将节点的主机名写入新建立文件的index.html文件
echo `hostname` > /tmp/htmlinedx.html

然后在master节点的任意目录新建一个nginx-pod.yaml文件,写入下面内容:

apiVersion: v1
kind: Pod
metadata:
  name: nginx
spec:
  hostAliases:
  - ip: "10.1.2.3"
    hostnames:
    - "foo"
    - "bar"
  containers:    # 可以是多个容器
  - name: nginx     # 本例只部署了一个nginx
    image: nginx:1.7.9    #使用镜像nginx:1.7.9创建container
    ports: 
    - containerPort: 80 # 容器使用80端口
    volumeMounts:
    - name: nginx-vol
      mountPath: "/usr/share/nginx/html"
  volumes:
  - name: nginx-vol
    hostPath: 
      path: /tmp/html

然后执行命令:

kubectl apply -f nginx-pod.yaml

接着,通过命令查看pod状态:

kubectl describe pod nginx

结果如下:

image-20201010103412286

可以看到Pod部署在了node2节点,并且内部IP为 10.100.104.6,因为没有定义service,我们只能用内部网络访问:

curl 10.100.104.6

可以看到结果:

image-20201010103525041

现在,我们进入容器看看,执行命令:

kubectl exec -it nginx -- bash

这个命令可以让我们进入名为nginx的这个Pod内部,并且运行bash控制台,提供一个交互界面。

然后我们来查看下之前挂载的/usr/share/nginx/html/index.html这个文件:

cat /usr/share/nginx/html/index.html

结果如下:

image-20201010103743466

在看看hosts有没有修改,执行命令:

cat /etc/hosts

结果如下:

image-20201010103850714

1.8.2. 4.8.2.容器的生命周期

Pod中的容器是可以定义生命周期钩子的,会在容器启动时,或者停止时只需,示例如下:

apiVersion: v1
kind: Pod
metadata:
  name: lifecycle-demo
spec:
  containers:
  - name: lifecycle-demo-container
    image: nginx:1.7.9
    lifecycle:
      postStart:
        exec:
          command: ["/bin/sh", "-c", "echo Hello from the postStart handler > /usr/share/message"]
      preStop:
        exec:
          command: ["/usr/sbin/nginx","-s","quit"]

这是一个来自 Kubernetes 官方文档的 Pod 的 YAML 文件。它其实非常简单,只是定义了一个 nginx 镜像的容器。不过,在这个 YAML 文件的容器(Containers)部分,你会看到这个容器分别设置了一个 postStartpreStop 参数。这是什么意思呢?

postStart: 在容器启动后,立刻执行一个指定的操作。需要明确的是,postStart 定义的操作,虽然是在 Docker 容器 ENTRYPOINT 执行之后,但它并不严格保证顺序。也就是说,在 postStart 启动时,ENTRYPOINT 有可能还没有结束。当然,如果 postStart 执行超时或者错误,Kubernetes 会在该 Pod 的 Events 中报出该容器启动失败的错误信息,导致 Pod 也处于失败的状态。

preStop : 是容器被杀死之前执行。而需要明确的是,preStop 操作的执行,是同步的。所以,它会阻塞当前的容器杀死流程,直到这个 Hook 定义操作完成之后,才允许容器被杀死,这跟 postStart 不一样。

所以,在这个例子中,我们在容器成功启动之后,在 /usr/share/message 里写入了一句“欢迎信息”(即 postStart 定义的操作)。而在这个容器被删除之前,我们则先调用了 nginx 的退出指令(即 preStop 定义的操作),从而实现了容器的“优雅退出”。

1.8.3. 4.8.3.POD的状态

Pod 生命周期的变化,主要体现在 Pod API 对象的 Status 部分,这是它除了 Metadata 和 Spec 之外的第三个重要字段。其中,pod.status.phase,就是 Pod 的当前状态,它有如下几种可能的情况:

  • Pending:这个状态意味着,Pod 的 YAML 文件已经提交给了 Kubernetes,API 对象已经被创建并保存在 Etcd 当中。但是,这个 Pod 里有些容器因为某种原因而不能被顺利创建。比如,调度不成功。
  • Running:这个状态下,Pod 已经调度成功,跟一个具体的节点绑定。它包含的容器都已经创建成功,并且至少有一个正在运行中。
  • Succeeded:这个状态意味着,Pod 里的所有容器都正常运行完毕,并且已经退出了。这种情况在运行一次性任务时最为常见。
  • Failed:这个状态下,Pod 里至少有一个容器以不正常的状态(非 0 的返回码)退出。这个状态的出现,意味着你得想办法 Debug 这个容器的应用,比如查看 Pod 的 Events 和日志。
  • Unknown:这是一个异常状态,意味着 Pod 的状态不能持续地被 kubelet 汇报给 kube-apiserver,这很有可能是主从节点(Master 和 Kubelet)间的通信出现了问题。

1.8.4. 4.8.4.ProjectVolume

Projected Volume,你可以把它翻译为“投射数据卷”,是一种特殊的Volume。

不同于一般的Volume,它们存在的意义不是为了存放容器里的数据,也不是用来进行容器和宿主机之间的数据交换。这些特殊 Volume 的作用,是为容器提供预先定义好的数据。所以,从容器的角度来看,这些 Volume 里的信息就是仿佛是被 Kubernetes“投射”(Project)进入容器当中的。这正是 Projected Volume 的含义。

到目前为止,Kubernetes 支持的 Projected Volume 一共有四种:

  • Secret;
  • ConfigMap;
  • Downward API;
  • ServiceAccountToken。

这里,我们主要学习下Secret这种投射数据卷。

假设我们的容器是中需要一组账号和密码,分别是:root和123321。我们先对账号密码做Base64的转码。

执行命令:

echo -n 'root' | base64

结果是:cm9vdA==

然后对密码转码:

echo -n '123321' | base64

结果是:MTIzMzIx

然后,我们定义一个secret类型的对象,将账号密码存储到Kubernetes中。在任意目录新建一个文件secret-volume.yaml,内容如下:

apiVersion: v1
kind: Secret # 一个Secret类型的对象
metadata:
  name: mysecret # 名称,引用这个secret时需要知道
type: Opaque # 不透明,加密存储
data:
  user: cm9vdA== # 用户名的base64码
  pass: MTIzMzIx # 密码的base64码

然后运行命令以导入这个secret:

kubectl apply -f secret-volume.yaml

然后,通过下面命令查看:

kubectl get secrets

如下:

image-20201010105520447

接着,我们在容器中使用这个secret。

我们在任意目录中,定义一个新的文件:secret-volume-pod.yaml,内容如下:

apiVersion: v1
kind: Pod
metadata:
  name: secret-volume # 当前pod的名称
spec:
  containers:
  - name: secret-volume-box
    image: busybox # 使用了busybox镜像,这个镜像只具备基本linux环境
    args: # 镜像启动后休眠86400ms,因为busybox内部没有进程,会立即结束
    - sleep
    - "86400"
    volumeMounts: # 卷挂载
    - name: mysql-cred # 卷名称
      mountPath: "/secret-volume" # 挂载到容器内的目录
      readOnly: true # 只读
  volumes:
  - name: mysql-cred # 声明一个数据卷,名称为mysql-cred
    projected: # 卷的类型是projected类型
      sources: # 卷的源
      - secret: # 是来自Kubernetes中的Secret对象,名为mysecret
          name: mysecret

然后执行命令,创建Pod

kubectl apply -f secret-volume-pod.yaml

接着,进入pod内部:

kubectl exec -it sercret-volume -- sh

运行命令,查看挂载目录中的内容:

image-20201010111016770

1.8.5. 4.8.5.容器健康检测和恢复机制

在 Kubernetes 中,你可以为 Pod 里的容器定义一个健康检查“探针”(Probe)。这样,kubelet 就会根据这个 Probe 的返回值决定这个容器的状态,而不是直接以容器进行是否运行(来自 Docker 返回的信息)作为依据。这种机制,是生产环境中保证应用健康存活的重要手段。

来看一个示例,新建一个名为liveness-pod.yaml的文件:

apiVersion: v1
kind: Pod
metadata:
  name: nginx-liveness
spec:
  containers:
  - name: nginx-liveness
    image: nginx:1.7.9
    ports: 
    - containerPort: 80
    volumeMounts:
    - name: nginx-vol
      mountPath: "/usr/share/nginx/html"
    livenessProbe: # 健康检查配置
      httpGet: # 通过http请求做检查
        path: /index.html # 检查请求的路径
        port: 80 # 请求端口
      initialDelaySeconds: 5 # 容器运行后5秒后做第一次检查
      periodSeconds: 3 # 以后每隔3秒运行一次检查
  volumes:
  - name: nginx-vol
    hostPath: 
      path: /tmp/html

这还是之前我们写的Nginx挂载宿主机目录的Pod,不过,我们加入了一段健康检测的配置:

livenessProbe: # 健康检查配置
  httpGet: # 通过http请求做检查
    path: /index.html # 检查请求的路径
    port: 80 # 请求端口
  initialDelaySeconds: 5 # 容器运行后5秒后做第一次检查
  periodSeconds: 3 # 以后每隔3秒运行一次检查

含义是:在Pod运行后的5秒时,发送Get的http请求到80端口,访问 /index.html,以后每隔3秒会再次发生请求。

如果请求成功认为容器健康,否则认为容器运行出现问题。

我们把pod运行起来:

kubectl apply -f liveness-pod.yaml

效果:

image-20201010114149146

然后,通过命令,查看容器

kubectl describe nginx-liveness

结果:

image-20201010114351155

在容器2中,运行下面的脚本:

sleep 30; mv /tmp/html/index.html /tmp/html/index.bck; sleep 10; mv /tmp/html/index.bck /tmp/html/index.html;

这个脚本会在30秒后,将index.html文件重命名,此时Kubernetes运行健康检测会得到404,认为容器处于不健康(unhealthy)状态:

image-20201010114615747

一段时间后,看到下面效果:

image-20201010114805764

容器被killing,并且will be restarted(被重启)。

接着,我们看看容器状态:

image-20201010114850996

发现容器的restarts从0 增加到了 1,说明重启了一次。在这个过程中,Pod 保持 Running 状态不变。

需要注意的是:Kubernetes 中并没有 Docker 的 Stop 语义。所以虽然是 Restart(重启),但实际却是重新创建了容器。这个功能就是 Kubernetes 里的 Pod 恢复机制,也叫 restartPolicy。它是 Pod 的 Spec 部分的一个标准字段(pod.spec.restartPolicy),默认值是 Always,即:任何时候这个容器发生了异常,它一定会被重新创建。

1.9. 4.9.控制器模型

Pod是对一组容器的抽象和封装,Kubernetes的容器编排,是对Pod的编排,因此我们一般不会直接去运行一个Pod。

在Kubernetes中,所有的Pod的调度都是有控制器(Controller)完成的,在入门案例中我们已经使用了名为Deployment的控制器。

一起来回顾下之前写的Deployment对象:

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:1.7.9
        ports:
        - containerPort: 80

这个 Deployment 定义的编排动作非常简单,即:确保携带了 app=nginx 标签的 Pod 的个数,永远等于 spec.replicas 指定的个数,即 2 个。

这就意味着,如果在这个集群中,携带 app=nginx 标签的 Pod 的个数大于 2 的时候,就会有旧的 Pod 被删除;反之,就会有新的 Pod 被创建。这就实现了Pod集群的伸缩。

那么,Deployment控制器是如何实现对Pod的控制的呢?

实际上,Deployment控制器遵循 Kubernetes 项目中的一个通用编排模式,即:控制循环(control loop)。

比如,现在有一种待编排的对象 X,它有一个对应的控制器。那么,我就可以用一段 Go 语言风格的伪代码,为你描述这个控制循环:

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

在具体实现中,实际状态往往来自于 Kubernetes 集群本身,比如,kubelet 通过心跳汇报的容器状态和节点状态,或者监控系统中保存的应用监控数据,或者控制器主动收集的它自己感兴趣的信息,这些都是常见的实际状态的来源。

而期望状态,一般来自于用户提交的 YAML 文件。比如,Deployment 对象中 Replicas 字段的值。

以刚才的Deployment为例:

  • 1.首先Deployment会统计目前运行中的携带了“app=nginx”标记的Pod的数量,这就是实际状态
  • 2.Deployment中定义的Replicas 字段的值就是期望状态
  • 3.Deployment 控制器将两个状态做比较,然后根据比较结果,确定是创建 Pod,还是删除已有的 Pod

可以看到,一个 Kubernetes 对象的主要编排逻辑,实际上是在第三步的“对比”阶段完成的。这个操作,通常被叫作调谐(Reconcile)。这个调谐的过程,则被称作“Reconcile Loop”(调谐循环)或者“Sync Loop”(同步循环)。

因此,Deployment控制器的定义主要包含两部分:

  • 一是对Pod的定义,就是其中的template部分,称为Pod模板(可以看到与Pod定义语法完全一致)。
  • 二是控制器的定义,就是Deployment中的其它部分,用来定义控制器的期望状态、控制目标

如图:

image-20201010142113701

而在Kubernetes中,类似Deployment这样的控制器还有很多,比如:

  • Deployment
  • ReplicaSet
  • StatuefulSet

等等。。。

1.10. 4.10.Deployment和ReplicaSet

在入门案例中,我们演示了Kubernetes的水平扩展和收缩(horizontal scaling out/in)功能、滚动升级功能。这些功能都是Deployment控制器中提供的。

而Deployment控制器实现水平扩展和收缩比较简单,在5.2小节中我们已经讲过Deployment如何通过协调循环来实现水平扩展和收缩

但是Deployment实现滚动升级,就需要依赖于ReplicaSet这个控制器了。

来看看ReplicaSet对象的定义:

apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: nginx-set
  labels:
    app: nginx
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.7.9

怎么样,是不是与Deployment定义方式如出一辙。

没错,ReplicaSet就是Deployment中的子集,都由两部分组成:

  • 控制器定义
  • Pod模板

只不过ReplicaSet的控制器定义仅仅包含对Pod副本数量的定义

而Deployment实际上操控正是ReplicaSet对象,而不是Pod对象

比如这样的一个Deployment定义:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.7.9
        ports:
        - containerPort: 80

这是一个非常常见的Deployment定义,规定了Pod的副本数目为3(replicas=3)。

那么实际上Deployment,与 ReplicaSet,以及 Pod 的关系是怎样的呢?如图:

image-20201010144635769

Deployment通过控制器模型,控制ReplicaSet的数量,目前是一个。而ReplicaSet通过控制器模型,来控制Pod的副本数目,实现水平扩展和伸缩。

1.10.1. 1)水平扩展原理

当需要水平扩展时,我们修改Deployment中的spec.replicas的值即可。比如,把这个值从 3 改成 4,那么 Deployment 所对应的 ReplicaSet,就会根据修改后的值自动创建一个新的 Pod。这就是“水平扩展”了;“水平收缩”则反之。

1.10.2. 2)滚动更新原理

滚动更新则会比较复杂,我们再次创建一个文件,描述一个Deployment:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.7.9
        ports:
        - containerPort: 80

相关命令:

# 启动,并记录日志信息
kubectl apply -f nginx-deploment.yaml --record
# 查看pod
kubectl get pods
# 查看ReplicaSet
kubectl get rs
# 查看Deployment
kubectl get deployments

效果:

image-20201010151137406

现在,我们通过命令修改其中的版本号,从1.7.9更新为1.8,触发滚动更新:

kubectl set image deployment/nginx-deployment nginx=nginx:1.8 --record=true

这样的好处时无需去修改yaml文件

然后可以查看下ReplicaSet的变化:

image-20201010152224014

image-20201010152141551

image-20201010152257850

可以看到,ReplicaSet数目从1个变成2个,而ReplicaSet中的Pod数目也逐渐的变化。第一个ReplicaSet中的Pod数目逐渐减少,第二个ReplicaSet中的Pod数目逐渐减少。

查看Deployment的日志,也能看到这个过程:

Events:
  Type    Reason             Age   From                   Message
  ----    ------             ----  ----                   -------
  Normal  ScalingReplicaSet  67s   deployment-controller  Scaled up replica set nginx-deployment-5bf87f5f59 to 3
  Normal  ScalingReplicaSet  15s   deployment-controller  Scaled up replica set nginx-deployment-5f8c6846ff to 1
  Normal  ScalingReplicaSet  14s   deployment-controller  Scaled down replica set nginx-deployment-5bf87f5f59 to 2
  Normal  ScalingReplicaSet  14s   deployment-controller  Scaled up replica set nginx-deployment-5f8c6846ff to 2
  Normal  ScalingReplicaSet  12s   deployment-controller  Scaled down replica set nginx-deployment-5bf87f5f59 to 1
  Normal  ScalingReplicaSet  12s   deployment-controller  Scaled up replica set nginx-deployment-5f8c6846ff to 3
  Normal  ScalingReplicaSet  11s   deployment-controller  Scaled down replica set nginx-deployment-5bf87f5f59 to 0
  • 在触发滚动更新时,Deployment会创建一个新的ReplicaSet,hash值为5f8c6846ff,其中的Pod副本数为0,原始的ReplicaSet,hash值为5bf87f5f59,Pod副本数为3
  • 在第15s,Deployment控制这个新的ReplicaSet,其副本数变为1,实现了水平扩展
  • 在第14s,Deployment控制这个旧的ReplicaSet,其副本数减为2,实现水平收缩
  • 如此交替,最终新的ReplicaSet的Pod副本数为3,旧的ReplicaSet的Pod副本数为0

这样,就完成了这一组 Pod 的版本升级过程。像这样,将一个集群中正在运行的多个 Pod 版本,交替地逐一升级的过程,就是“滚动更新”。

image-20201010153458466

Deployment 的控制器,实际上控制的是 ReplicaSet 的数目,以及每个 ReplicaSet 的属性。

而一个应用的版本,对应的正是一个 ReplicaSet;这个版本应用的 Pod 数量,则由 ReplicaSet 通过它自己的控制器(ReplicaSet Controller)来保证。

通过这样的多个 ReplicaSet 对象,Kubernetes 项目就实现了对多个“应用版本”的描述。

既然有了版本的概念,我们可以通过命令,查看版本滚动的历史:

kubectl rollout history deployment/nginx-deployment

版本如下:

image-20201010154619378

而且,Deployment还能控制版本做回退:

kubectl rollout undo deployment/nginx-deployment

并且可以指定要回退的版本号:

kubectl rollout undo deployment/nginx-deployment --to-revision=1

results matching ""

    No results matching ""