Kubernetes实战

第1部分 Kubernetes基础篇

第1章 Kubernetes介绍

几个概念

云计算

狭义上将是指IT基础设施的交付和使用模式,即通过网络以按需、易扩展的方式获取所需资源。

广义上则是指服务的交付和使用模式,通过网络以按需、易扩展的方式获取所需服务。

提供资源的网络被形象地比喻成“云”,其计算能力通常由分布式的大规模集群和虚拟化技术提供的。

“云”好比发电厂,互联网好比输电线路,只不过发电厂对外提供的是IT服务

业界根据云计算提供服务资源的类型将其划分为三大类:

  • IaaS(基础设施即服务)
  • PaaS(平台即服务)
  • SaaS(软件即服务)

云计算三层架构图

20220517160200947

IaaS(基础设施即服务)

白话:卖给你硬件设备,相比与传统的设备更易扩展而已,如云硬盘、云服务器、云主机

通过虚拟化和分布式存储等技术,实现了对包括服务器、存储设备、网络设备等各种物理资源的抽象,从而形成了一个可扩展、可按需分配的虚拟资源池。目前最具代表性的IaaS产品有Amazon AWS,提供虚拟机EC2和云存储S3等服务。

PaaS(平台即服务)

白话:卖给你一些开发组件,如云数据库、或是Tomcat运行环境等(Kubernetes也算在定义范畴内)。PaaS服务一般分为框架类服务和中间件服务,

框架类服务:Tomcat、Websphere、Node.js、Rubyon Rails、Ruby on Rack

中间件服务:数据库(Mysql、mongoDB、Redis)、消息队列(RabbitMQ)、缓存(Memcache)。

为开发者提供了应用的开发环境和运行环境,将开发者从繁琐的IT环境管理中解放出来,自动化部署和运维,使开发者能够集中精力于应用业务开发,提升应用的开发效率。PaaS主要面向的是软件专业人员,Google的GAE是PaaS的鼻祖。

SaaS(软件即服务)

白话:直接卖软件给你用

面向使用软件的终端用户。一般来说,SaaS将软件功能以特定的接口形式发布,终端用户通过网络浏览器就可以使用软件功能(那不就是Web应用嘛)。SaaS是应用最广的云计算模式,如在线使用的邮箱系统和各种管理系统都可以认为是SaaS的范畴。

20220517161639971

Kubernetes是什么

是Google开源的容器集群管理系统。构建在Docker技术之上,为容器化的应用提供资源调度、部署运行、服务发现、扩容缩容等一整套功能。

特性:

  • 容器编排能力

    容器组合、标签选择和服务发现等

  • 轻量级

    遵循微服务架构理论,整个系统划分出各个功能独立的组件,组件之间边界清晰,部署简单,可以轻易地运行在各种系统和环境中。同时,Kubernetes中的许多功能都实现了插件化,可以非常方便地进行扩展和替换。

  • 开放开源

Kubernetes核心概念
Pod

Pod是若干相关容器的组合,Pod包含的容器运行在同一台宿主机上,这些容器使用相同的网络命名空间、IP地址和端口,相互之间能通过localhost来发现和通信。另外,这些容器还可共享一块存储卷空间。在Kubernetes中创建、调度和管理的最小单位是Pod,而不是容器,Pod通过提供更高层次的抽象,提供了更加灵活的部署和管理模式。

  • 若干运行在同一台宿主机上的相关容器的组合

  • 使用相同网络命名空间、IP地址和端口

  • 共享一块存储卷空间

Replication Controller

用来控制管理Pod副本(Replica,或者称为实例),Relication Controller确保任何时候Kubernetes集群中有指定数量的Pod副本在运行。如果少于指定数量的Pod副本,它会启动新的Pod副本,反之会杀死多余的副本以保证数量不变。Replication Controller是弹性伸缩、滚动升级的实现核心。

  • 控制副本数量
  • 弹性伸缩、滚动升级的实现核心
Service

是真实应用服务的抽象,定义了Pod的逻辑集合和访问这个Pod集合的策略。它将代理Pod对外表现为一个单一访问的接口,外部不需要了解后端Pod如何运行,对扩展和维护有好处,提供了一套简化的服务代理和发现机制。

  • 定义了Pod的逻辑集合和访问这个Pod集合的策略
  • 将代理Pod对外表现为一个单一访问的接口
  • 提供服务代理和发现机制
Label

用来区分Pod、Service、Replication Controller的Key/Value对,Kubernetes中任意API对象都可以通过Label标识。每个API对象可以有多个Label,但是每个Label的Key只能对应一个Value。Label是Service和Replication Controller运行的基础,它们都通过Label来关联Pod,是一种松耦合关系。

  • Key/Value对,标识API对象
  • 每个API对象可以有多个Label,但是每个Label的Key只能对应一个Value
Node

K8s属于主从分布式集群架构,Node运行并管理容器。Node作为K8s的操作单元,用来分配给Pod(或者说容器)进行绑定,Pod最终运行在Node上,Node可以认为是Pod的宿主机。

  • K8s操作单元,分配给Pod进行绑定
  • 可以认为是Pod的宿主机

第2章 K8s的架构和部署

K8s的架构和组件

K8s属于主从分布式架构,节点在角色上分为Master和Node

K8s使用Etcd作为存储组件

Etcd是一个高可用的键值存储系统,灵感来自ZooKeeper和Doozer,通过Raft一致性算法处理日志复制以保证强一致性。

K8s使用Etcd作为系统的配置存储中心,K8s中的重要数据都是持久化在Etcd中的,这使得K8s架构的各个组件属于无状态,可以更简单实施分布式集群部署。

K8s Master作为控制节点,调度管理整个系统,包含以下组件

  • K8s API Server:作为K8s系统的入口,其封装了核心对象的增删改查操作,以REST API接口方式提供给外部客户和内部组件调用。它维护的REST对象将持久化到Etcd中。

  • K8s Scheduler:负责集群的资源调度,为新建的Pod分配机器。这部分工作分出来一个组件,意味着可以很方便地替换成其他的调度器。

  • K8s Controller Manager:负责执行各种控制器,目前已经实现很多控制器来保证K8s的正常运行

    控制器 说明

K8s Node是运行节点,用于运行管理业务的容器,包含以下组件

  • Kubelet:负责管控容器,Kubelet会从K8s API Server接受Pod的创建请求,启动和停止容器,监控容器运行状态并汇报给K8s API Server。
  • K8s Proxy:负责为Pod创建代理服务,K8s Proxy会从K8s API Server获取所有的Service,并根据Service信息创建代理服务,实现Service到Pod的请求路由和转发,从而实现K8s层级的虚拟转发网络。
  • Docker:K8s Node是容器运行节点,需要运行Docker服务,K8s也支持其他容器引擎
部署K8s
环境准备

K8s是一个分布式架构,可以灵活进行安装部署,可以部署在单机,也可以分布式部署。但是需要运行在Linux(x86_64)系统上,至少1核CPU和1GB内存。

(使用4台虚拟机用于部署k8s运行环境,一个etcd、一个K8s Master和三个K8s Node

运行etcd
获取K8s发行包
运行K8s Master组件
运行K8s Node组件
查询K8s的健康状态
创建K8s覆盖网络
安装K8s扩展插件
安装Cluster DNS

Cluster DNS用于支持K8s的服务发现机制,主要包含如下几项:

  • SkyDNS:提供DNS解析服务
  • Etcd:用于SkyDNS的存储
  • Kube2sky:监听K8s,当有新的Service创建时,生成相应记录到SkyDNS
安装Cluster Monitoring
安装Cluster Logging
安装Kube UI

第3章 K8s快速入门

K8s是容器集群管理系统,为容器化的应用提供资源调度、部署运行、容灾容错和服务发现等功能。

示例应用Guestbook

Guestbook是一个典型的Web应用

20220719200349874

Guestbook包含两部分

  • Frontend

    Web前端,无状态节点,可以方便伸缩,本例中将运行3个实例

  • Redis

    存储部分,Redis采用主备模式,即运行1个Redis Master、2个Redis Slave,Redis Slave会从Redis Master同步数据

Guestbook提供的功能:在Frontend页面提交数据,Frontend则将数据保存到Redis Master,然后从Redis Slave读取数据显示到页面上。

运行Redis

在K8s上部署Redis,包括Master和Slave

创建Redis Master Pod

Pod是K8s的基本处理单元,Pod包含一个或多个相关的容器,应用以Pod的形式运行在K8s中(本质上是容器)。Replication Controller能够控制Pod按照指定的副本数目持续运行,一般情况下是通过Replication Controller来创建Pod来保证Pod的可靠性。

定义redis-master-controller.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
apiVersion: v1
kind: ReplicationController
metadata:
name: redis-master
labels:
name: redis-master
spec:
replicas: 1
selector:
name: redis-master
template:
metadata:
labels:
name: redis-master
spec:
containers:
- name: master
image: redis
ports:
- containerPort: 6379

K8s中通过文件定义API对象,文件格式支持JSONYAML

API对象基本属性:

  • API版本(.apiVersion

  • API对象类型(.kind

  • 元数据(.metadata

    redis-master-controller.yaml定义了V1版本下一个名称为redis-masterReplication Controller,另外配置了规格(.spec),其中设置了Pod的副本数(.spec.replicas)和Pod模板(.spec.template

    Pod模板中说明了Pod包含了一个容器,该容器使用镜像redis,即运行Redis Master,该Replication Controller将关联一个这样的Pod,而Replication ControllerPod的关联是通过Label来实现的(.spec.selector.spec.template.metadata.labels

通过定义文件创建Replication Controller

1
$ kubectl create -f redis-master-controller.yaml
创建Redis Master Service

K8s中Pod是变化的,特别是受到Replication Controller控制的时候,当Pod发生变化的时候,PodIP也是变化的。

由此衍生出一个问题,就是集群中的Pod如何互相发现并访问的?

K8s提供Service实现服务发现

Service是真实应用的抽象,将用来代理Pod,对外提供固定IP作为访问入口,通过访问Service来访问相应的Pod,访问者只需要知道Service的访问地址,而不需要感知Pod的变化

猜一下怎么实现的,大概是通过etcd做服务注册,将Pod的IP注册进去,每次Pod的变动都会更新其中注册的IP,Service通过这个注册的IP访问Pod

创建Redis Master Service来代理Redis Master Pod

Redis Master Service的定义文件redis-master-service.yaml

1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: v1
kind: Service
metadata:
name: redis-master
labels:
name: redis-master
spec:
ports:
- port: 6379
targetPort: 6379
selector:
name: redis-master

创建Service

1
kubectl create -f redis-master-service.yaml

Service通过Label关联Pod,在Service的定义中,设置.spec.selectorname=redis-master将关联redis master pod(创建时我们指定了selector.nameredis-master

通过命令查询Service

1
2
3
$ kubectl get service redis-master
NAME CLUSTER_IP EXTERNAL_IP PORT(S) SELECTOR AGE
... ... <none> ... ... ...

CLUSTER_IP:K8s分配给Serivce的虚拟IP

PORT(S):6379/TCP,是Service会转发的端口(通过Service定义文件中的.spec.ports[0].port指定),K8s会将访问该端口的TCP请求转发到Redis Master Pod中,目标端口为6379/TCP(通过Service定义文件中的.spec.ports[0].targetPort指定)

因为创建Redis Master Service来代理Redis Master Pod,所以Redis Slave Pod通过Service的虚拟IP就可以访问到Redis Master Pod,但是如果只是硬配置Service的虚拟IP到Redis Slave Pod中,还不是真正的服务发现,K8s提供了两种发现Service的方法

  • 环境变量

    当Pod运行的时候,K8s会将之前存在的Service的信息通过环境变量写到Pod中,以Redis Master Service为例,它的信息会被写到Pod中:

    1
    2
    3
    4
    5
    6
    7
    REDIS_MASTER_SERVICE_HOST=10.254.233.212
    REDIS_MASTER_PORT_6379_TCP_PROTO=tcp
    REDIS_MASTER_SERVICE_PORT=6379
    REDIS_MASTER_PORT=tcp://10.254.233.212:6379
    REDIS_MASTER_PORT_6379_TCP=tcp://10.254.233.212:6379
    REDIS_MASTER_PORT_6379_TCP_PORT=6379
    REDIS_MASTER_PORT_6379_TCP_ADDR=10.254.233.212

    缺点:Pod必须在Service之后启动,采用DNS方式就没有这种限制

  • DNS

    当有新的Service创建时,就会自动生成一条DNS记录,以Redis Master Service为例,有一条DNS记录:

    redis-master => 10.254.233.212

    使用这种方法,K8s需要安装Cluster DNS插件

创建Redis Slave Pod

通过Replication Controller可创建Redis Slave Pod,将创建两个Redis Slave Pod。定义文件redis-slave-controller.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
apiVersion: v1
kind: ReplicationController
metadata:
name: redis-slave
labels:
name: redis-slave
spec:
replicas: 2
selector:
name: redis-slave
template:
metadata:
labels:
name: redis-slave
spec:
containers:
- name: worker
image: gcr.io/google_samples/gb-redisslave:v1
env:
- name: GET_HOSTS_FROM
value: dns # 若没有安装cluster-dns插件,可以用env替代,但是一定要在service创建之后再创建
ports:
- containerPort: 6379

定义文件中设置了Pod的副本数为2,Pod模板中包含一个容器,容器使用镜像gcr.io/google_samples/gb-redisslave:v1,该镜像实际是基于redis镜像,重写了启动脚本,将其作为Redis Master的备用节点启动,启动脚本如下:

1
2
3
4
5
if [[ ${GET_HOSTS_FROM:-dns} == "env" ]]; then
redis-server --slaveof ${REDIS_MASTER_SERVICE_HOST} 6379
else
redis-server --slaveof redis-master 6379
fi

其实就是通过GET_HOSTS_FROM环境变量控制服务发现的

扩展:${GET_HOSTS_FROM:-dns}

shell中对变量赋默认值,这里的意思是如果GET_HOSTS_FROM未定义或为空串,则赋予默认值dns

语法格式:${变量名:-默认值}

另一种写法是只有未定义才赋予默认值

语法格式:${变量名-默认值}

创建Pod

1
kubectl create -f redis-slave-controller.yaml
创建Redis Slave Service

redis-slave-service.yaml:

1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: v1
kind: Service
metadata:
name: redis-slave
labels:
name: redis-slave
spec:
selector:
name: redis-slave
ports:
- port: 6379
targetPort: 6379

创建Service

1
kubectl create -f redis-slave-service.yaml
运行Frontend
创建Frontend Pod

通过Frontend Replication Controller来创建Frontend Pod,将创建3个Frontend Pod

frontend-controller.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
apiVersion: v1
kind: ReplicationController
metadata:
name: frontend
labels:
name: frontend
spec:
replicas: 3
selector:
name: frontend
template:
metadata:
labels:
app: frontend
spec:
containers:
- name: php-redis
image: gcr.io/google_samples/gb-frontend:v3
env:
- name: GET_HOSTS_FROM
value: env # 没有装cluster-dns插件,所以用env替代了dns
ports:
- containerPort: 80

定义文件中设置Pod副本数为3,Pod模板包含一个容器,容器使用镜像gcr.io/google_samples/gb-frontend:v3,这是一个PHP实现的Web应用,写数据到Redis Master,并从Redis Slave中读取数据。内部也是通过GET_HOSTS_FROM环境变量控制服务发现方式的。

创建Pod

1
kubectl create -f frontend-controller.yaml
创建Frontend Service

frontend-service.yaml

1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: v1
kind: Service
metadata:
name: frontend
labels:
name: frontend
spec:
selector:
name: frontend
ports:
- port: 80
targetPort: 80
设置Guestbook外网访问

现在Guestbook已经运行在K8s上了,但是只有内部网络能访问,外部网络的用户是无法访问的,我们需要增加一层网络转发,即外网到内网的转发。实现方式有很多种,我们这里采用NodePort的方式实现。

即K8s会在每个节点上设置端口,称为NodePort,通过NodePort可以访问到Pod

修改frontend-service.yaml,设置.spec.typeNodePort

1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: v1
kind: Service
metadata:
name: frontend
labels:
name: frontend
spec:
type: NodePort # 添加type为NodePort
selector:
name: frontend
ports:
- port: 80
targetPort: 80

重新创建Service:

1
2
3
4
5
6
7
8
$ kubectl replace -f front-service.yaml --force
service "frontend" deleted
service/frontend replaced
$ kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
frontend NodePort 10.0.0.69 <none> 80:32443/TCP 2s
redis-master ClusterIP 10.0.0.229 <none> 6379/TCP 127m
redis-slave ClusterIP 10.0.0.80 <none> 6379/TCP 82m

可以看到frontend的TYPE已经是NodePort了,并且可以看到PORT(S)有端口映射(80:32443/TCP),外部可以通过32443访问

清理Guestbook
1
2
kubectl delete rc redis-master redis-slave frontend
kubectl delete svc redis-master redis-slave frontend

第4章 Pod

Hello World

创建一个简单的Hello World Pod,运行一个输出Hello World的容器

定义文件hello-world-pod.yaml:

1
2
3
4
5
6
7
8
9
10
apiVersion: v1
kind: Pod
metadata:
name: hello-world
spec:
restartPolicy: OnFailure
containers:
- name: hello-world
image: "ubuntu:18.04"
command: ["/bin/echo", "hello", "world"]

定义文件中描述了Pod的属性和行为

  • apiVersion:声明K8s的API版本,目前是v1

  • kind:声明API对象的类型,这里的类型是Pod

  • metadata:设置Pod的元数据

    • name:指定Pod的名称,Pod的名称必须Namespace内唯一
  • spec:配置Pod的具体规格

    • restartPolicy:设置Pod的重启策略

    • containers:设置Pod中容器的规格,数组格式,每一项定义一个容器

      name:指定容器名称,在Pod的定义中唯一

      image:设置容器镜像

      command:设置容器的启动命令

这里的Pod定义效果和以下docker命令运行的容器效果一样

1
2
$ docker run --name hello-world ubuntu:18.04 /bin/echo 'hello world'
hello world

需要注意,因为容器输出完之后就会退出,这是一次性执行的,所以Pod的定义中把.spec.restartPolicy设置为OnFailure,即容器正常退出不会重新创建容器

创建Pod

1
2
3
4
$ kubectl create -f hello-world-pod.yaml
# 查询Pod输出
$ kubectl logs hello-world
hello world
Pod的基本操作
创建Pod

K8s中大部分的API对象都是通过kubectl create命令创建的

如果Pod的定义有误,kubectl create会打印出错误信息

查询Pod
1
kubectl get pod (pod name)

不指定pod则查询全部pod

字段含义:

  • NAME:Pod名称
  • READY:Pod的准备状况,右边的数字表示Pod包含的容器总数,左边的数字表示准备就绪的容器数目
  • STATUS:Pod的状态
  • RESTARTS:Pod的重启次数
  • AGE:Pod的运行时间

其中Pod的准备状况指的是Pod是否准备就绪以接受请求,Pod的准备状况取决于容器,即所有容器都准备就绪了,Pod才准备就绪。这个时候K8s的代理服务才会添加Pod作为分发后端,而一旦Pod的准备状况变为false(至少一个容器的准备状况变为false),K8s会将Pod从代理服务的分发后端移除。

默认情况下,kubectl get只显示Pod的简要信息

1
2
kubectl get pod (pod name) --output json # json格式显示,--output可以简写为-o
kubectl get pod (pod name) --output yaml # yaml格式显示,--output可以简写为-o

也支持Go Template方式过滤指定的信息,比如查询Pod的运行状态:

1
kubectl get pod (pod name) -o go-template --template={{.status.phase}}

kubectl describe查询Pod的状态和生命事件:

1
kubectl describe pod (pod name)

字段含义:

  • Name:Pod名称
  • Namespace:Pod的Namespace
  • Image(s):Pod使用的镜像
  • Node:Pod所在的Node
  • Start Time:Pod的起始时间
  • Labels:Pod的Label
  • Status:Pod的状态
  • Reason:Pod处于当前状态的原因
  • Message:Pod处于当前状态的信息
  • IP:Pod的PodIP
  • Replication Controllers:Pod对应的Replication Controller
  • Containers:Pod中容器的信息
    • Container ID:容器ID
    • Image:容器的镜像
    • Image ID:镜像ID
    • State:容器的状态
    • Ready:容器的准备状态
    • Restart Count:容器的重启次数统计
    • Environment Variables:容器的环境变量
  • Conditions:Pod的条件,包含Pod的准备状况
  • Volumes:Pod的数据卷
  • Events:与Pod相关的事件列表
删除Pod
1
2
kubectl delete pod [pod name]
kubectl delete pod --all
更新Pod

如果想要更新Pod,可以在修改定义文件后执行:

1
kubectl replace [file]

但是很多属性没办法修改,比如容器镜像,这时候可以通过--force参数强制更新,等于重建Pod

Pod与容器

在Docker中,容器是最小处理单位,隔离是基于Linux Namespace实现的,Linux内核中提供了6种Linux Namespace隔离的系统调用

Linux Namespace 系统调用参数 隔离内容
UTS CLONE_NEWUTS 主机名与域名
IPC CLONE_NEWIPC 信号量、消息队列和共享内存
PID CLONE_NEWPID 进程编号
Network CLONE_NEWNET 网络设备、网格栈、端口等
Mount CLONE_NEWNS 挂载点(文件系统)
User CLONE_NEWUSER 用户和用户组

在K8s中,Pod包含一个或多个相关容器,Pod可以认为是容器的一种延伸扩展,一个Pod也是一个隔离体,而Pod包含的一组容器又是共享的(当前共享的Linux Namespace包括:PID、Network、IPC和UTS)。除此之外,Pod中的容器可以访问共同的数据卷来实现文件系统的共享,所以K8s的数据卷是Pod级别的

Pod是容器的集合,容器是真正的执行体。Pod的设计不是为了运行同一个应用的多个实例,而是运行一个应用、多个紧密联系的程序。而每个程序运行在单独的容器中,以Pod的形式组合成一个应用。相比于单个容器中运行多个程序,这样设计的好处有:

  • 透明性:将Pod内的容器向基础设施可见,底层系统就能向容器提供如进程管理和资源监控等服务
  • 解绑软件的依赖:单个容器可以独立重建和重新部署,实现独立容器的实时更新
  • 易用性:用户不需要运行自己的进程管理器,也不需要负责信号量和退出码的传递等
  • 高效性:因为底层设备负责更多的管理,容器因而更轻量化

在Pod中可以详细配置如何运行一个容器

镜像

运行容器必须先指定镜像,镜像名称遵循Docker的命名规范。

如果镜像不存在,会从Docker镜像仓库下载。K8s中可以选择镜像的下载策略,支持的策略有:

  • Always:每次都下载最新的镜像
  • Never:只使用本地镜像,从不下载
  • IfNotPresent:只有本地没有的时候才下载

通过imagePullPolicy设置镜像下载策略

1
2
3
name: hello
image: 'ubuntu:18.04'
imagePullPolicy: Always
启动命令

启动命令用来说明容器是如何运行的,在Pod的定义中可以设置容器启动命令和参数

例:

1
2
3
4
5
6
...
containers:
- name: hello
image: 'ubuntu:18.04'
command: ["/bin/echo", "hello", "world"]
...

也可以配置为:

1
2
3
4
...
command: ["/bin/echo"]
args: ["hello", "world"]
...

在Pod定义中commandargs都是可选项,将和Docker镜像的ENTRYPOINTCMD相互作用,生成最终容器的启动命令

规则:

  • 只要指定了command,就不会使用镜像中的命令
  • 没指定command,指定了args,则将args作为参数使用,命令使用镜像中的命令
环境变量

Pod中可以设置容器运行时的环境变量:

1
2
3
4
5
env:
- name: PARAMETER_1
value: value_1
- name: PARAMETER_2
value: value_2

在一些场景下,Pod中的容器希望获取本身的信息,比如Pod的名称,Pod所在的Namespace等,在K8s中提供了Downward API获取这些信息,并且可以通过环境变量告诉容器

  • Pod的名称:metadata.name
  • Pod的Namespace:metadata.namespace
  • Pod的PodIP:status.podIP

现在创建一个Pod并通过环境变量获取Downward API

定义文件downwardapi-env.yaml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
apiVersion: v1
kind: Pod
metadata:
name: downwardapi-env
spec:
containers:
- name: test-container
image: "ubuntu:18.04"
command: ["/bin/bash", "-c", "while true; do sleep 5; done"]
env:
- name: MY_POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: MY_POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: MY_POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
1
2
3
4
5
6
7
# 创建Pod
$ kubectl create -f downwardapi-env.yaml
# 输出Pod内环境变量
$ kubectl exec downwardapi-env env | grep MY_POD
MY_POD_NAME=downwardapi-env
MY_POD_NAMESPACE=default
MY_POD_IP=10.0.10.103
端口

在Docker中运行容器通过-p/--publish参数设置端口映射规则

在Pod的定义中也可以设置端口映射规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
apiVersion: v1
kind: Pod
metadata:
name: my-nginx
spec:
containers:
- name: nginx
image: nginx
ports:
- name: web
containerPort: 80
protocol: TCP
hostIP: 0.0.0.0
hostPort: 80

在Pod的定义中,通过.spec.containers[].ports[]设置容器的端口,数组形式,参数含义:

  • name:端口名称,Pod内唯一,当只配置一个端口的时候,这是可选的,当配置多个端口的时候,这是必选的
  • containerPort:必选,设置在容器内的端口
  • protocol:可选,设置端口协议,TCP或UDP,默认是TCP
  • hostIP:可选,设置在宿主机上的IP,默认绑定到所有可用的IP接口上,即0.0.0.0
  • hostPort:可选,设置在宿主机上的端口,如果设置则进行端口映射

使用宿主机端口需要考虑端口冲突问题,幸运的是,K8s在调度Pod的时候,会检查宿主机端口是否冲突。比如两个Pod都需要使用宿主机80端口,那么调度的时候会将两个Pod调度到不同的Node上。如果所有Node的端口都被占用了,那么Pod调度失败。

数据持久化和共享

容器是临时存在的,如果容器被销毁,容器中的数据将会丢失

为了能够持久化数据以及共享容器间的数据,Docker提出了数据卷(Volume)的概念

数据卷就是目录或者文件,它可以绕过联合文件系统,以正常的文件或者目录的形式存在与宿主机上

使用docker run运行容器的时候,我们经常使用参数--volume/-v创建数据卷,将宿主机上的目录或者文件挂载到容器中。即使容器被销毁,数据卷中的数据仍然保存在宿主机上。

K8s对Docker数据卷进行了扩展,支持对接第三方存储系统。且K8s中的数据卷是Pod级别的,Pod中的容器可以访问共同的数据卷,实现容器间的数据共享。

我们对HelloWorldPod进行改造:

在Pod中声明创建数据卷,Pod中的两个容器将共享数据卷,容器write写入数据,容器read读出数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
apiVersion: v1
kind: Pod
metadata:
name: hello-world
spec:
restartPolicy: Never
containers:
- name: write
image: "ubuntu:18.04"
command:
- "bash"
- "-c"
- "echo 'hello world' >> /data/hello"
volumeMounts:
- name: data
mountPath: /data
- name: read
image: "ubuntu:18.04"
command:
- "bash"
- "-c"
- "sleep 10; cat /data/hello"
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
hostPath:
path: /tmp

Pod的网络

Pod中所有容器网络都是共享的,一个Pod中的所有容器的网络是一致的,它们能够通过本地地址访问其他用户容器的端口

K8s网络模型中,每一个Pod都拥有一个扁平化共享网络命令空间的IP,称为PodIP。

1
2
$ kubectl get pod my-app --template={{.status.podIP}}
10.0.10.204

这是Docker为容器进行网络虚拟化隔离而分配的内部IP。也可以设置Pod为Host网络模式,即直接使用宿主机网络,不进行网络虚拟化隔离。Pod的PodIP就是其所在Node的IP。

通过.spec.hostNetwork设置Pod为Host网络模式

1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: v1
kind: Pod
metadata:
name: host-network-mode
spec:
containers:
- name: host-network-mode
image: nginx
ports:
- containerPort: 80
protocol: TCP
name: web
hostNetwork: true

注意:

  1. 不存在网络隔离,所以容易发生端口冲突
  2. Pod可以直接访问宿主机上的所有网络设备和服务,从安全性上来说是不可控的
Pod的重启策略

重启策略通过.spec.restartPolicy设置,目前支持3种策略

  • Always:当容器终止退出后,总是重启容器,默认
  • OnFailure:当容器终止异常退出时,才重启容器
  • Never:当容器终止退出时,从不重启

Pod中的容器重启次数统计,实际并不是非常精确,只能作为一个参考

Pod的状态和生命周期
容器状态

Pod的本质是一组容器

K8s对Pod中的容器进行了状态的记录

  • Waiting:容器正在等待创建,比如下载镜像
    • Reason:等待原因
  • Running:容器已经创建并且正在运行
    • startedAt:容器创建时间
  • Terminated:容器终止退出
    • exitCode:退出码
    • signal:容器退出信号
    • reason:容器退出原因
    • message:容器退出信息
    • startedAt:容器创建时间
    • finishedAt:容器退出时间
    • containerID:容器的ID
1
2
# 查询Pod状态
$ kubectl describe pod my-pod
Pod的生命周期阶段

Pod一旦被分配到Node后,就不会离开这个Node

Pod的生命周期阶段:

  • Pending:Pod已经被创建,但是有容器未被创建
  • Running:Pod已经被调度到Node,所有容器已经创建,并且至少一个容器在运行或者正在重启
  • Succeeded:Pod中所有容器正常退出
  • Failed:Pod中所有容器退出,至少一个容器是异常退出的
生命周期回调函数

K8s提供了回调函数,在容器的生命周期的特定阶段执行调用,比如容器在停止前希望执行某项操作,就可以注册相应的钩子函数。

  • PostStart:在容器创建成功后调用
  • PreStop:在容器被终止前调用

钩子函数的实现方式有以下两种

  • Exec

    执行指定命令

    配置参数:

    command:需要执行的命令,字符串数组

    示例

    1
    2
    3
    4
    exec:
    command:
    - cat
    - /tmp/health
  • HTTP

    发起一个HTTP调用请求

    配置参数:

    path:请求的URL路径,可选

    port:请求的端口,必选

    host:请求的IP,可选,默认是Pod的PodIP

    scheme:请求的协议,可选,默认是HTTP

    示例:

    1
    2
    3
    4
    httpGet:
    host: 192.168.1.1
    path: /notify
    port: 8080

现定义一个Pod,包含一个Java的Web应用服务器,其中设置了PostStart和PreStop回调函数。在容器创建成功后,复制/sample.war/app目录。而在容器被终止前,发送HTTP请求到http://monitor.com:8080/warning,往监控系统发送一个警告,Pod的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
apiVersion: v1
kind: Pod
metadata:
name: javaweb
spec:
containers:
- name: war
image: resouer/sample:v2
lifecycle:
postStart:
exec:
command:
- "cp"
- "/sample.war"
- "/app"
preStop:
httpGet:
host: monitor.com
path: /warning
port: 8080
scheme: HTTP
自定义检查Pod

对于Pod是否健康,默认情况下只是检查容器是否正常运行。但有的时候容器正常运行并不代表健康,可能有的应用进程已经阻塞住无法正常处理请求,所以为了提供更加健壮的应用,往往需要定制化的健康检查。

K8s提供Probe机制,有两种类型的Probe

  • Liveness Probe:用于容器的自定义健康检查,如果检查失败,K8s将会杀死Pod,然后根据重启策略重启Pod
  • Readiness Probe:用于检查容器的自定义准备状况检查,如果检查失败,K8s会将Pod从服务代理的分发后端移除,即不会分发请求给该Pod

Probe支持以下三种检查方法

  • ExecAction

    在容器中执行指定的命令进行检查,当命令执行成功(返回码为0),检查成功。

    配置参数

    command:检查命令,字符串数组

    示例

    1
    2
    3
    4
    exec:
    command:
    - cat
    - /tmp/health
  • TCPSocketAction

    对Pod中的指定TCP端口进行检查,当TCP端口被占用,检查成功

    配置参数

    port:检查的TCP端口

    示例

    1
    2
    tcpSocket:
    port: 8080
  • HTTPGetAction

    发送一个HTTP请求,当返回码介于200~400之间时,检查成功

    配置参数

    path:请求的URI路径,可选

    port:请求的端口,必选

    host:请求的IP,可选,默认为Pod的PodIP

    scheme:请求的协议,可选,默认为HTTP

    示例

    1
    2
    3
    httpGet:
    path: /health
    port: 8080
Pod的健康检查

定义一个Pod,使用Liveness Probe通过ExecAction方式检查容器的健康状态,Pod的定义文件liveness-exec-pod.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
apiVersion: v1
kind: Pod
metadata:
name: liveness-exec-pod
labels:
test: liveness
spec:
containers:
- name: liveness
image: "ubuntu:18.04"
command:
- "/bin/sh"
- "-c"
- "echo ok > /tmp/health; sleep 60; rm -rf /tmp/health; sleep 600"
livenessProbe:
exec:
command: ["cat", "/tmp/health"]
initialDelaySeconds: 15
timeoutSeconds: 1
1
2
$ kubectl describe pod liveness-exec-pod | grep Unhealthy
Warning Unhealthy <invalid> (x3 over <invalid>) kubelet Liveness probe failed: cat: /tmp/health: No such file or directory

可以看到Liveness Probe检查失败并重启了Pod

Pod的准备状况检查

定义一个Pod,使用Readiness Probe通过ExecAction方式检查容器的准备状况,Pod的定义文件readiness-exec-pod.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
apiVersion: v1
kind: Pod
metadata:
name: readiness-exec-pod
labels:
test: readiness
spec:
containers:
- name: readiness
image: "ubuntu:18.04"
command: ["/bin/sh", "-c", "echo ok > /tmp/ready; sleep 60; rm -rf /tmp/ready; sleep 600"]
readinessProbe:
exec:
command: ["cat", "/tmp/ready"]
initialDelaySeconds: 15
timeoutSeconds: 1

查看Pod,大概一分钟后可以看到Pod的ready数目变为0

1
2
3
4
5
6
7
$ kubectl get pod
NAME READY STATUS RESTARTS AGE
readiness-exec-pod 0/1 Running 0 109s

# 查看事件可以看到,Readiness Probe检查失败
$ kubectl describe pod readiness-exec-pod | grep Unhealthy
Warning Unhealthy <invalid> (x21 over <invalid>) kubelet Readiness probe failed: cat: /tmp/ready: No such file or directory
调度Pod

Pod调度指的是Pod在创建之后分配到哪一个Node上,调度算法分为两个步骤

  1. 筛选出符合条件的Node
  2. 选择最优的Node

对于所有的Node,首先K8s通过过滤函数去除不符合条件的Node,K8s v1.1.1支持的过滤函数如下:

  • NoDiskConflict:检查Pod请求的数据卷是否与Node上已存在Pod挂载的数据卷存在冲突,如果存在冲突,则过滤掉该Node
  • PodFitsResources:检查Node的可用资源(CPU和内存)是否满足Pod的资源请求
  • PodFitsPorts:检查Pod设置的HostPorts在Node上是否已经被其他Pod占用
  • PodFitsHost:如果Pod设置了NodeName属性,则筛选出指定的Node
  • PodSelectorMatches:如果Pod设置了NodeSelector属性,则筛选出符合的Node
  • CheckNodeLabelPresence:检查Node是否存在Kubernetes Scheduler配置的标签

筛选出符合条件的Node来运行Pod,如果存在多个符合条件的Node,那么需要选择出最优的Node。

K8s通过优先级函数来评估出最优的Node,对于每个Node,优先级函数给出一个分数:0~10(10表示最优,0表示最差),每个优先级函数设置有权重值,Node最终分数就是每个优先级函数给出的分数的加权和。

K8s v1.1.1提供的优先级函数有:

  • LeastRequestedPriority:优先选择有最多可用资源的Node
  • CalculateNodeLabelPriority:优先选择含有指定Label的Node
  • BalancedResourceAllocation:优先选择资源使用均衡的Node

如何进行Node选择?

  • 在定义Pod时通过设置.spec.nodeSelector来选择Node

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    apiVersion: v1
    kind: Pod
    metadata:
    name: nginx
    labels:
    env: test
    spec:
    containers:
    ...
    nodeSelector:
    env: test

    Pod创建成功后大概率会被分配到有env=test标签的Node上

  • 在定义Pod时通过.spec.nodeName直接指定Node

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    apiVersion: v1
    kind: Pod
    metadata:
    name: nginx
    labels:
    env: test
    spec:
    containers:
    ...
    nodeName: kube-node-0

    还是建议使用Node Selector,因为通过Label选择是一种弱绑定,而直接指定Node Name是一种强绑定,Node失效时会导致Pod无法调度