第一章·Kubernetes入门
1.从一个简单的例子开始:Java Web应用结构
该应用是一个运行在Tomcat里的Web App,结构比较简单,如图1.1所示,JSP页面通过JDBC直接访问MySQL数据库并展示数据。这里出于演示和简化的目的,只要程序正确连接数据库,就会自动完成对应的Table创建与初始化数据的准备工作。所以,当我们通过浏览器访问此应用时,就会显示一个表格页面,其中包含来自数据库的内容。
此应用需要启动两个容器:Web App容器和MySQL容器,并且Web App容器需要访问MySQL容器。如果仅使用Docker启动这两个容器,则需要通过Docker Network或者端口映射的方式实现容器间的网络互访。本例介绍在Kubernetes系统中是如何实现的。
启动MySQL服务
首先为MySQL服务创建一个Deployment定义文件mysql-deploy.yaml
apiVersion: apps/v1 # API版本
# kind属性用来表明此资源对象的类型,比如这里的属性值表示这是一个Deployment
kind: Deployment # 副本控制器RC
metadata:
labels: # 标签
app: mysql
name: mysql # 对象名称,全局唯一
# Deployment的相关属性定义
spec:
replicas: 1 # 预期的版本数量
# Deployment的Pod选择器,符合条件的Pod实例受到该Deployment的管理
selector:
matchLabels:
app: mysql
template: # pod 模板
metadata:
# 指定了该Pod的标签,labels必须匹配之前的spec.selector。
labels:
app: mysql
spec:
containers: # 定义容器
- image: mysql:5.7
name: mysql
ports:
- containerPort: 3306# 容器应用监听的端口号
env: # 注入容器内的环境变量
- name: MYSQL_ROOT_PASSWORD
value: "123456"
创建好mysql-deploy.yaml文件后,为了将它发布到Kubernetes集群中,我们在Master上运行如下命令:
# 创建
[root@k8s-m01 java_web]# kubectl apply -f mysql-deploy.yaml
# 查看刚刚创建的Deployment4
[root@k8s-m01 java_web]# kubectl get deploy
# 查看pod创建情况
[root@k8s-m01 java_web]# kubectl get pods
我们可以在Kubernetes节点的服务器上通过docker ps指令查看正在运行的容器,发现提供MySQL服务的Pod容器已创建且正常运行,并且MySQL Pod对应的容器多创建了一个Pause容器,该容器就是Pod的根容器。
最后,创建一个与之关联的Kubernetes Service—MySQL的定义文件(文件名为mysql-svc.yaml),完整的内容和说明如下:
apiVersion: v1
kind: Service # 表明是Kubernetes Service
metadata:
name: msql # Service的全局唯一名称
spec:
ports:
- port: 3306 # Service提供服务的端口号
selector: # service对应的pod拥有这里定义的标签
app: mysql
创建Service对象
[root@k8s-m01 java_web]# kubectl apply -f mysql-svc.yaml
运行kubectl get命令,查看刚刚创建的Service对象
[root@k8s-m01 java_web]# kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 24h
msql ClusterIP 10.111.167.223 <none> 3306/TCP 22h
可以发现,MySQL服务被分配了一个值为10.111.167.223的ClusterIP地址(在不同环境中分配的IP地址可能不同)。随后,在Kubernetes集群中新创建的其他Pod就可以通过Service的ClusterIP+端口号3306来连接和访问它了。
通常,ClusterIP地址是在Service创建后由Kubernetes系统自动分配的,其他Pod无法预先知道某个Service的ClusterIP地址,因此需要一个服务发现机制来找到这个服务。为此,Kubernetes最初巧妙地使用了Linux环境变量(Environment Variable)来解决这个问题。根据Service的唯一名称,容器可以从环境变量中获取Service对应的ClusterIP地址和端口号,从而发起TCP/IP连接请求。
启动Tomcat应用
前面定义和启动了MySQL服务,接下来采用同样的步骤完成Tomcat应用的启动。首先,创建对应的RC文件myweb-deploy.yaml,内容如下:
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: myweb
name: myweb
spec:
replicas: 2
selector:
matchLabels:
app: myweb
template:
metadata:
labels:
app: myweb
spec:
containers:
- image: kubeguide/tomcat-app:v1
name: myweb
ports:
- containerPort: 8080
env:
- name: MYSQL_SERVICE_HOST
value: 10.111.167.223
在Tomcat容器内,应用将使用环境变量MYSQL_SERVICE_HOST的值连接MySQL服务,但这里为什么没有注册该环境变量呢?这是因为Kubernetes会自动将已存在的Service对象以环境变量的形式展现在新生成的Pod中。其更安全、可靠的方法是使用服务的名称mysql,这就要求集群内的DNS服务(kube-dns)正常运行。运行下面的命令,完成Deployment的创建和验证工作:
# 创建
[root@k8s-m01 java_web]# kubectl apply -f myweb-deploy.yaml
# 查看pod创建情况
[root@k8s-m01 java_web]# kubectl get pods
最后,创建对应的Service。以下是完整的YAML定义文件(myweb-svc.yaml):
apiVersion: v1
kind: Service
metadata:
name: myweb
spec:
# 表明此Service开启了NodePort格式的外网访问模式
type: NodePort
ports:
- port: 8080
nodePort: 30001
selector:
app: myweb
运行kubectl apply命令进行创建:
# 创建
[root@k8s-m01 java_web]# kubectl apply -f myweb-svc.yaml
# 运行kubectl get命令,查看已创建的Service:
[root@k8s-m01 java_web]# kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 24h
msql ClusterIP 10.111.167.223 <none> 3306/TCP 22h
myweb NodePort 10.101.231.229 <none> 8080:30001/TCP 22h
通过浏览器访问网页
经过上面的流程,我们终于成功实现了Kubernetes上第1个例子的部署、搭建工作。现在一起来见证成果吧!在你的笔记本上打开浏览器,输入“http://虚拟机IP:30001/demo/”。
比如虚拟机IP为10.0.071(可以通过ip a命令进行查询),在浏览器里输入地址http://10.0.071:30001/demo/后,可以看到如图1.2所示的网页界面。
如果无法打开这个网页界面,那么可能的原因包括:
①因为防火墙的设置无法访问30001端口;
②因为通过代理服务器上网,所以浏览器错把虚拟机的IP地址当作远程地址;等等。可以在虚拟机上直接运行curl 192.168.18.131:30001来验证能否访问此端口,如果还是不能访问,就肯定不是机器的问题了。
接下来尝试单击“Add…”按钮添加一条记录并提交,如图1.3所示,提交以后,数据就被写入MySQL数据库了。
2.Kubernetes的基本概述和术语
资源对象概述:
Kubernetes中的基本概念和术语大多是围绕资源对象(Resource Object)来说的,而资源对象在总体上可分为以下两类。
- 某种资源的对象,例如节点(Node)、Pod、服务(Service)、存储卷(Volume)。
- 与资源对象相关的事物与动作,例如标签(Label)、注解(Annotation)、命名空间(Namespace)、部署(Deployment)、HPA、PVC。
资源对象一般包括几个通用属性:版本、类别(Kind)、名称、标签、注解,如下所述。
- 在版本信息里包括了此对象所属的资源组,一些资源对象的属性会随着版本的升级而变化,在定义资源对象时要特别注意这一点。
- 类别属性用于定义资源对象的类型。
- 资源对象的名称(Name)、标签、注解这三个属性属于资源对象的元数据(metadata)。
- 资源对象的名称要唯一。
- 资源对象的标签是很重要的数据,也是Kubernetes的一大设计特性,比如通过标签来表明资源对象的特征、类别,以及通过标签筛选不同的资源对象并实现对象之间的关联、控制或协作功能。
- 注解可被理解为一种特殊的标签,不过更多地是与程序挂钩,通常用于实现资源对象属性的自定义扩展。
我们可以采用YAML或JSON格式声明(定义或创建)一个Kubernetes资源对象,每个资源对象都有自己的特定结构定义(可以理解为数据库中一个特定的表),并且统一保存在etcd这种非关系型数据库中,以实现最快的读写速度。此外,所有资源对象都可以通过Kubernetes提供的kubectl工具(或者API编程调用)执行增、删、改、查等操作。
一些资源对象有自己的生命周期及相应的状态,比如Pod,我们通过kubectl客户端工具创建一个Pod并将其提交到系统中后,它就处于等待调度的状态,调度成功后为Pending状态,等待容器镜像下载和启动、启动成功后为Running状态,正常停止后为Succeeded状态,非正常停止后为Failed状态。同样,PV也是具有明确生命周期的资源对象。对于这类资源对象,我们还需要了解其生命周期的细节及状态变更的原因,这有助于我们快速排查故障。
另外,我们在学习时需要注意与该资源对象相关的其他资源对象或者事务,把握它们之间的关系,同时思考为什么会有这种资源对象产生,哪些是核心的资源对象,哪些是外围的资源对象。
按照功能或用途对其进行分类,将其分为集群类、应用类、存储类及安全类这四大类
集群类:
集群(Cluster)表示一个由Master和Node组成的Kubernetes集群。
1.master
Master指的是集群的控制节点。在每个Kubernetes集群中都需要有一个或一组被称为Master的节点,来负责整个集群的管理和控制。Master通常占据一个独立的服务器(在高可用部署中建议至少使用3台服务器),是整个集群的“大脑”,如果它发生宕机或者不可用,那么对集群内容器应用的管理都将无法实施。
在Master上运行着以下关键进程。
- Kubernetes API Server:提供HTTP RESTful API接口的主要服务,是Kubernetes里对所有资源进行增、删、改、查等操作的唯一入口,也是集群控制的入口进程。
- Kubernetes Controller Manager:Kubernetes里所有资源对象的自动化控制中心,可以将其理解为资源对象的“大总管”。
- Kubernetes Scheduler:负责资源调度(Pod调度)的进程,相当于公交公司的调度室。
另外,在Master上通常还需要部署etcd服务。
2.Node
Kubernetes集群中除Mater外的其他服务器被称为Node,Node在较早的版本中也被称为Minion。与Master一样,Node可以是一台物理主机,也可以是一台虚拟机。Node是Kubernetes集群中的工作负载节点,每个Node都会被Master分配一些工作负载(Docker容器),当某个Node宕机时,其上的工作负载会被Master自动转移到其他Node上。在每个Node上都运行着以下关键进程。
- kubelet:负责Pod对应容器的创建、启停等任务,同时与Master密切协作,实现集群管理的基本功能。
- kube-proxy:实现Kubernetes Service的通信与负载均衡机制的服务。
- 容器运行时(如Docker):负责本机的容器创建和管理
默认情况下,kubelet会向Master注册自己,这也是Kubernetes推荐的Node管理方式。一旦Node被纳入集群管理范畴,kubelet进程就会定时向Master汇报自身的情报,例如操作系统、主机CPU和内存使用情况,以及当前有哪些Pod在运行等,这样Master就可以获知每个Node的资源使用情况,并实现高效均衡的资源调度策略。而某个Node在超过指定时间不上报信息时,会被Master判定为“失联”,该Node的状态就被标记为不可用(Not Ready),Master随后会触发“工作负载大转移”的自动流程。
# 我们可以运行以下命令查看在集群中有多少个Node:
[root@k8s-m01 java_web]# kubectl get nodes
NAME STATUS ROLES AGE VERSION
k8s-m01 Ready control-plane,master 25h v1.21.5
k8s-node01 Ready <none> 25h v1.21.5
k8s-node02 Ready <none> 24h v1.21.5
# 然后通过kubectl describe node <node_name>命令查看某个Node的详细信息:
[root@k8s-m01 java_web]# kubectl describe node k8s-node01
在以上命令的运行结果中会展示目标Node的如下关键信息:
-
Node的基本信息:名称、标签、创建时间等。
-
Node当前的运行状态:Node启动后会做一系列自检工作,比如磁盘空间是否不足(DiskPressure)、内存是否不足(MemoryPressure)、网络是否正常(NetworkUnavailable)、PID资源是否充足(PIDPressure)。在一切正常时才设置Node为Ready状态(Ready=True),表示Node处于健康状态,Master就可以在其上调度新的任务了(如启动Pod)。
-
Node的主机地址与主机名。
-
Node上的资源数量:描述Node可用的系统资源,包括CPU、内存数量、最大可调度Pod数量等。
-
Node可分配的资源量:描述Node当前可用于分配的资源量。
-
主机系统信息:包括主机ID、系统UUID、Linux Kernel版本号、操作系统类型与版本、Docker版本号、kubelet与kube-proxy的版本号等。
-
当前运行的Pod列表概要信息。
-
已分配的资源使用概要信息,例如资源申请的最小、最大允许使用量占系统总量的百分比。
-
Node相关的Event信息。
如果一个Node存在问题,比如存在安全隐患、硬件资源不足要升级或者计划淘汰,我们就可以给这个Node打一种特殊的标签——污点(Taint),避免新的容器被调度到该Node上。而如果某些Pod可以(短期)容忍(Toleration)某种污点的存在,则可以继续将其调度到该Node上。
集群类重要的基础概念——命名空间,用于实现多租户的资源隔离,典型的一种思路就是给每个租户都分配一个命名空间。
命名空间属于Kubernetes集群范畴的资源对象,在一个集群里可以创建多个命名空间,每个命名空间都是相互独立的存在,属于不同命名空间的资源对象从逻辑上相互隔离。在每个Kubernetes集群安装完成且正常运行之后,Master会自动创建两个命名空间,一个是默认的(default)、一个是系统级的(kube-system)。用户创建的资源对象如果没有指定命名空间,则被默认存放在default命名空间中;而系统相关的资源对象如网络组件、DNS组件、监控类组件等,都被安装在kube-system命名空间中。
我们可以通过命名空间将集群内部的资源对象“分配”到不同的命名空间中,形成逻辑上分组的不同项目、小组或用户组,便于不同的分组在共享使用整个集群的资源的同时能被分别管理。当给每个租户都创建一个命名空间来实现多租户的资源隔离时,还能结合Kubernetes的资源配额管理,限定不同租户能占用的资源,例如CPU使用量、内存使用量等。
命名空间的定义很简单,如下所示的YAML文件定义了名为development的命名空间:
apiVersion: v1
kind: Namespace
metadata:
name: development
创建了命名空间,我们在创建资源对象时就可以指定这个资源对象属于哪个命名空间。比如在下面的例子中定义了一个名为busybox的Pod,并将其放入development这个命名空间中:
apiVersion: v1
kind: pod
metadata:
name: busybox
# namespace: development
spec:
containers:
- image: busybox
command:
- sleep
- "3600"
name:busybox
此时使用kubectl get命令查看,将无法显示:
[root@k8s-m01 java_web]# kubectl get pods
这是因为如果不加参数,则kubectl get命令将仅显示属于default命名空间的资源对象。
可以在kubectl get命令中加入--namespace参数来操作某个命名空间中的对象:
[root@k8s-m01 java_web]# kubectl get pods --namespace=development
应用类:
Kubernetes中属于应用类的概念和相应的资源对象类型最多
1.Service和Pod
一般说来,Service指的是无状态服务,通常由多个程序副本提供服务。
在特殊情况下也可以是有状态的单实例服务,比如MySQL这种数据存储类的服务。与我们常规理解的服务不同,Kubernetes里的Service具有一个全局唯一的虚拟ClusterIP地址,Service一旦被创建,Kubernetes就会自动为它分配一个可用的ClusterIP地址,而且在Service的整个生命周期中,它的ClusterIP地址都不会改变,客户端可以通过这个虚拟IP地址+服务的端口直接访问该服务,再通过部署Kubernetes集群的DNS服务,就可以实现Service Name(域名)到ClusterIP地址的DNS映射功能,我们只要使用服务的名称(DNS名称)即可完成到目标服务的访问请求。“服务发现”这个传统架构中的棘手问题在这里首次得以完美解决,同时,凭借ClusterIP地址的独特设计,Kubernetes进一步实现了Service的透明负载均衡和故障自动恢复的高级特性
通过分析、识别并建模系统中的所有服务为微服务——Kubernetes Service,我们的系统最终由多个提供不同业务能力而又彼此独立的微服务单元组成,服务之间通过TCP/IP进行通信,从而形成强大又灵活的弹性网格,拥有强大的分布式能力、弹性扩展能力、容错能力,程序架构也变得简单和直观许多,如图1.4所示。
Pod是Kubernetes中最重要的基本概念之一,如图1.5所示是Pod的组成示意图,我们看到每个Pod都有一个特殊的被称为“根容器”的Pause容器。Pause容器对应的镜像属于Kubernetes平台的一部分,除了Pause容器,每个Pod都还包含一个或多个紧密相关的用户业务容器。
为什么Kubernetes会设计出一个全新的Pod概念并且Pod有这样特殊的组成结构?原因如下。
- 为多进程之间的协作提供一个抽象模型,使用Pod作为基本的调度、复制等管理工作的最小单位,让多个应用进程能一起有效地调度和伸缩。
- Pod里的多个业务容器共享Pause容器的IP,共享Pause容器挂接的Volume,这样既简化了密切关联的业务容器之间的通信问题,也很好地解决了它们之间的文件共享问题。
Kubernetes为每个Pod都分配了唯一的IP地址,称之为Pod IP,一个Pod里的多个容器共享Pod IP地址。Kubernetes要求底层网络支持集群内任意两个Pod之间的TCP/IP直接通信,这通常采用虚拟二层网络技术实现,例如Flannel、Open vSwitch等,因此我们需要牢记一点:在Kubernetes里,一个Pod里的容器与另外主机上的Pod容器能够直接通信。
Pod其实有两种类型:普通的Pod及静态Pod(Static Pod)。后者比较特殊,它并没被存放在Kubernetes的etcd中,而是被存放在某个具体的Node上的一个具体文件中,并且只能在此Node上启动、运行。而普通的Pod一旦被创建,就会被放入etcd中存储,随后被Kubernetes Master调度到某个具体的Node上并绑定(Binding),该Pod被对应的Node上的kubelet进程实例化成一组相关的Docker容器并启动。在默认情况下,当Pod里的某个容器停止时,Kubernetes会自动检测到这个问题并且重新启动这个Pod(重启Pod里的所有容器),如果Pod所在的Node宕机,就会将这个Node上的所有Pod都重新调度到其他节点上。Pod、容器与Node的关系如图1.6所示。
下面是我们在之前的Hello World例子里用到的myweb这个Pod的资源定义文件:
apiVersion: v1
kind: Pod
metadata:
name: myweb
labels:
name: myweb
spec:
containers:
- name: myweb
image: kubeguide/tomcat-app:v1
ports:
- containerPort: 8080
这里的容器端口(containerPort)组成了一个新的概念——Endpoint,代表此Pod里的一个服务进程的对外通信地址。一个Pod也存在具有多个Endpoint的情况,比如当我们把Tomcat定义为一个Pod时,可以对外暴露管理端口与服务端口这两个Endpoint。
我们所熟悉的Docker Volume在Kubernetes里也有对应的概念——Pod Volume,Pod Volume是被定义在Pod上,然后被各个容器挂载到自己的文件系统中的。Volume简单来说就是被挂载到Pod里的文件目录。
这里顺便提一下Kubernetes的Event概念。Event是一个事件的记录,记录了事件的最早产生时间、最后重现时间、重复次数、发起者、类型,以及导致此事件的原因等众多信息。Event通常会被关联到某个具体的资源对象上,是排查故障的重要参考信息。之前我们看到在Node的描述信息中包括Event,而Pod同样有Event记录,当我们发现某个Pod迟迟无法创建时,可以用kubectl describe pod xxxx来查看它的描述信息,以定位问题的成因
如图1.7所示给出了Pod及Pod周边对象的示意图,后面的部分还会涉及这张图里的对象和概念
在继续说明Service与Pod的关系之前,我们需要先学习理解Kubernetes中重要的一个机制——标签匹配机制。
2.Label与标签选择器
Label(标签)是Kubernetes系统中的另一个核心概念,相当于我们熟悉的“标签”。一个Label是一个key=value的键值对,其中的key与value由用户自己指定。Label可以被附加到各种资源对象上,例如Node、Pod、Service、Deployment等,一个资源对象可以定义任意数量的Label,同一个Label也可以被添加到任意数量的资源对象上。Label通常在资源对象定义时确定,也可以在对象创建后动态添加或者删除。我们可以通过给指定的资源对象捆绑一个或多个不同的Label来实现多维度的资源分组管理功能,以便灵活、方便地进行资源分配、调度、配置、部署等管理工作,例如,部署不同版本的应用到不同的环境中,以及监控、分析应用(日志记录、监控、告警)等。一些常用的Label示例如下。
- 版本标签:release:stable和release:canary。
- 环境标签:environment:dev、environment:qa和environment:production。
- 架构标签:tier:frontend、tier:backend和tier:middleware。
- 分区标签:partition:customerA和partition:customerB。
- 质量管控标签:track:daily和track:weekly。
给某个资源对象定义一个Label,就相当于给它打了一个标签,随后可以通过Label Selector(标签选择器)查询和筛选拥有某些Label的资源对象,Kubernetes通过这种方式实现了类似SQL的简单又通用的对象查询机制。Label Selector可以被类比为SQL语句中的where查询条件,例如,“name=redis-slave”这个Label Selector作用于Pod时,可以被类比为“select * from pod where pod's name='redis-slave'”这样的语句。当前有两种Label Selector表达式:基于等式的(Equality-based)Selector表达式和基于集合的(Set-based)Selector表达式。
基于等式的Selector表达式采用等式类表达式匹配标签,下面是一些具体的例子。
- name=redis-slave:匹配所有具有name=redis-slave标签的资源对象。
- env !=production:匹配所有不具有env=production标签的资源对象,比如“env=test”就是满足此条件的标签之一。
基于集合的Selector表达式则使用集合操作类表达式匹配标签,下面是一些具体的例子。 - name in(redis-master,redis-slave):匹配所有具有name=redis-master标签或者name=redis-slave标签的资源对象。
- name not in(php-frontend):匹配所有不具有name=php-frontend标签的资源对象。
可以通过多个Label Selector表达式的组合来实现复杂的条件选择,多个表达式之间用“,”进行分隔即可,几个条件之间是“AND”的关系,即同时满足多个条件,比如下面的例子:
name=redis-slave,env !=production
name not in(php-frontend),env !=production
在前面的例子中只使用了一个“name=XXX”的Label Selector。看一个更复杂的例子:假设为Pod定义了3个Label:release、env和role,不同的Pod定义了不同的Label值,如图1.8所示,如果设置“role=frontend”的Label Selector,则会选取到Node 1和Node 2上的Pod;如果设置“release=beta”的Label Selector,则会选取到Node 2和Node 3上的Pod,如图1.9所示。
总之,使用Label可以给对象创建多组标签,Label和Label Selector共同构成了Kubernetes系统中核心的应用模型,可对被管理对象进行精细的分组管理,同时实现了整个集群的高可用性。
Label也是Pod的重要属性之一,其重要性仅次于Pod的端口,我们几乎见不到没有Label的Pod。以myweb Pod为例,下面给它设定了app=myweb标签:
apiVersion: v1
kind: Pod
metadata:
name: myweb
labels:
app: myweb
对应的Service myweb就是通过下面的标签选择器与myweb Pod发生关联的:
spec:
selector:
app: myweb
所以我们看到,Service很重要的一个属性就是标签选择器,如果我们不小心把标签选择器写错了,就会出现指鹿为马的闹剧。如果恰好匹配到了另一种Pod实例,而且对应的容器端口恰好正确,服务可以正常连接,则很难排查问题,特别是在有众多Service的复杂系统中。
3. Pod与Deployment
前面提到,大部分Service都是无状态的服务,可以由多个Pod副本实例提供服务。通常情况下,每个Service对应的Pod服务实例数量都是固定的,如果一个一个地手工创建Pod实例,就太麻烦了,最好是用模板的思路,即提供一个Pod模板(Template),然后由程序根据我们指定的模板自动创建指定数量的Pod实例。这就是Deployment这个资源对象所要完成的事情了。
先看看之前例子中的Deployment案例(省略部分内容):
apiVersion: apps/v1
kind: Deployment
spec:
replicas: 2
selector:
matchLabels:
app: myweb
template:
metadata:
labels:
app: myweb
spec:
◎ replicas:Pod的副本数量。
◎ selector:目标Pod的标签选择器。
◎ template:用于自动创建新Pod副本的模板。
只有一个Pod副本实例时,我们是否也需要Deployment来自动创建Pod呢?在大多数情况下,这个答案是“需要”。这是因为Deployment除自动创建Pod副本外,还有一个很重要的特性:自动控制。举个例子,如果Pod所在的节点发生宕机事件,Kubernetes就会第一时间观察到这个故障,并自动创建一个新的Pod对象,将其调度到其他合适的节点上,Kubernetes会实时监控集群中目标Pod的副本数量,并且尽力与Deployment中声明的replicas数量保持一致。
下面创建一个名为tomcat-deployment.yaml的Deployment描述文件,内容如下:
apiVersion: apps/v1
kind: Deployment
metadata:
name: frontend
spec:
replicas: 1
selector:
matchLabels:
tier: frontend
matchExpressions:
- {key: tier, operator: In, values: [frontend]}
template:
metadata:
labels:
app: app-demo
tier: frontend
spec:
containers:
- name: tomcat-demo
image: tomcat
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8080
运行以下命令创建Deployment对象:
[root@k8s-m01 java_web]# kubectl apply -f tomcat-deployment.yaml
deployment.apps/frontend created
运行以下命令查看Deployment的信息:
[root@k8s-m01 java_web]# kubectl get deployments
NAME READY UP-TO-DATE AVAILABLE AGE
frontend 1/1 1 1 4m30s
mysql 1/1 1 1 24h
myweb 2/2 2 2 24h
对以上输出中各字段的含义解释如下。
◎ DESIRED:Pod副本数量的期望值,即在Deployment里定义的replicas。
◎ CURRENT:当前replicas的值,实际上是Deployment创建的ReplicaSet对象里的replicas值,这个值不断增加,直到达到DESIRED为止,表明整个部署过程完成。
◎ UP-TO-DATE:最新版本的Pod的副本数量,用于指示在滚动升级的过程中,有多少个Pod副本已经成功升级。
◎ AVAILABLE:当前集群中可用的Pod副本数量,即集群中当前存活的Pod数量。
Deployment资源对象其实还与ReplicaSet资源对象密切相关,Kubernetes内部会根据Deployment对象自动创建相关联的ReplicaSet对象,通过以下命令,我们可以看到它的命名与Deployment的名称有对应关系:
[root@k8s-m01 java_web]# kubectl get replicaset
NAME DESIRED CURRENT READY AGE
frontend-7d7c57fc94 1 1 1 7m17s
mysql-596b96985c 1 1 1 24h
myweb-567f7ff896 2 2 2 24h
不仅如此,我们发现Pod的命名也是以Deployment对应的ReplicaSet对象的名称为前缀的,这种命名很清晰地表明了一个ReplicaSet对象创建了哪些Pod,对于Pod滚动升级(Pod Rolling update)这种复杂的操作过程来说,很容易排查错误:
root@k8s-m01 java_web]# kubectl get pods
NAME READY STATUS RESTARTS AGE
frontend-7d7c57fc94-kjzkb 1/1 Running 0 8m59s
mysql-596b96985c-bmj7r 1/1 Running 0 24h
myweb-567f7ff896-dhc5r 1/1 Running 0 24h
myweb-567f7ff896-lqhs4 1/1 Running 0 24h
关于Deployment就先说到这里,最后总结一下它的典型使用场景。
- 创建一个Deployment对象来完成相应Pod副本数量的创建。
- 检查Deployment的状态来看部署动作是否完成(Pod副本数量是否达到预期的值)。
- 更新Deployment以创建新的Pod(比如镜像升级),如果当前Deployment不稳定,则回滚到一个早先的Deployment版本。
- 扩展Deployment以应对高负载。
图1.10显示了Pod、Deployment与Service的逻辑关系。
从图1.10中可以看到,Kubernetes的Service定义了一个服务的访问入口地址,前端的应用(Pod)通过这个入口地址访问其背后的一组由Pod副本组成的集群实例。Service与其后端Pod副本集群之间则是通过Label Selector实现无缝对接的,Deployment实际上用于保证Service的服务能力和服务质量始终符合预期标准。
Service的ClusterIP地址
既然每个Pod都会被分配一个单独的IP地址,而且每个Pod都提供了一个独立的Endpoint(Pod IP+containerPort)以被客户端访问,那么现在多个Pod副本组成了一个集群来提供服务,客户端如何访问它们呢?
传统的做法是部署一个负载均衡器(软件或硬件),为这组Pod开启一个对外的服务端口如8000端口,并且将这些Pod的Endpoint列表加入8000端口的转发列表中,客户端就可以通过负载均衡器的对外IP地址+8000端口来访问此服务了。
Kubernetes也是类似的做法,Kubernetes内部在每个Node上都运行了一套全局的虚拟负载均衡器,自动注入并自动实时更新集群中所有Service的路由表,通过iptables或者IPVS机制,把对Service的请求转发到其后端对应的某个Pod实例上,并在内部实现服务的负载均衡与会话保持机制。
不仅如此,Kubernetes还采用了一种很巧妙又影响深远的设计——ClusterIP地址。我们知道,Pod的Endpoint地址会随着Pod的销毁和重新创建而发生改变,因为新Pod的IP地址与之前旧Pod的不同。Service一旦被创建,Kubernetes就会自动为它分配一个全局唯一的虚拟IP地址——ClusterIP地址,而且在Service的整个生命周期内,其ClusterIP地址不会发生改变,这样一来,每个服务就变成了具备唯一IP地址的通信节点,远程服务之间的通信问题就变成了基础的TCP网络通信问题。
任何分布式系统都会涉及“服务发现”这个基础问题,大部分分布式系统都通过提供特定的API来实现服务发现功能,但这样做会导致平台的侵入性较强,也增加了开发、测试的难度。Kubernetes则采用了直观朴素的思路轻松解决了这个棘手的问题:只要用Service的Name与ClusterIP地址做一个DNS域名映射即可。比如我们定义一个MySQL Service,Service的名称是mydbserver,Service的端口是3306,则在代码中直接通过mydbserver:3306即可访问此服务,不再需要任何API来获取服务的IP地址和端口信息。
之所以说ClusterIP地址是一种虚拟IP地址,原因有以下几点。
- ClusterIP地址仅仅作用于Kubernetes Service这个对象,并由Kubernetes管理和分配IP地址(来源于ClusterIP地址池),与Node和Master所在的物理网络完全无关。
- 因为没有一个“实体网络对象”来响应,所以ClusterIP地址无法被Ping通。ClusterIP地址只能与Service Port组成一个具体的服务访问端点,单独的ClusterIP不具备TCP/IP通信的基础。
- ClusterIP属于Kubernetes集群这个封闭的空间,集群外的节点要访问这个通信端口,则需要做一些额外的工作。
下面是名为tomcat-service.yaml的Service定义文件,内容如下:
apiVersion: v1
kind: Service
metadata:
name: tomcat-service
spec:
ports:
- port: 8080
selector:
tier: frontend
以上代码定义了一个名为tomcat-service的Service,它的服务端口为8080,拥有tier=frontend标签的所有Pod实例都属于它,运行下面的命令进行创建:
[root@k8s-m01 java_web]# kubectl apply -f tomcat-service.yaml
service/tomcat-service created
我们之前在tomcat-deployment.yaml里定义的Tomcat的Pod刚好拥有这个标签,所以刚才创建的tomcat-service已经对应了一个Pod实例,运行下面的命令可以查看tomcat-service的Endpoint列表,其中10.244.58.196是Pod的IP地址,8080端口是Container暴露的端口:
[root@k8s-m01 java_web]# kubectl get endpoints
NAME ENDPOINTS AGE
kubernetes 10.0.0.71:6443 26h
msql 10.244.58.194:3306 24h
myweb 10.244.58.195:8080,10.244.85.195:8080 24h
tomcat-service 10.244.58.196:8080 2m19s
你可能有疑问:“说好的Service的ClusterIP地址呢?怎么没有看到?”运行下面的命令即可看到tomcat-service被分配的ClusterIP地址及更多的信息:
apiVersion: v1
kind: Service
spec:
clusterIP: 10.100.44.94
- port: 8080
protocol: TCP
targetPort: 8080
selector:
tier: frontend
sessionAffinity: None
type: ClusterIP
status:
loadBalancer: {}
除了正常的Service,还有一种特殊的Service——Headless Service,只要在Service的定义中设置了clusterIP:None,就定义了一个Headless Service,它与普通Service的关键区别在于它没有ClusterIP地址,如果解析Headless Service的DNS域名,则返回的是该Service对应的全部Pod的Endpoint列表,这意味着客户端是直接与后端的Pod建立TCP/IP连接进行通信的,没有通过虚拟ClusterIP地址进行转发,因此通信性能最高,等同于“原生网络通信”。
接下来看看Service的多端口问题。很多服务都存在多个端口,通常一个端口提供业务服务,另一个端口提供管理服务,比如Mycat、Codis等常见中间件。Kubernetes Service支持多个Endpoint,在存在多个Endpoint的情况下,要求每个Endpoint都定义一个名称进行区分。下面是Tomcat多端口的Service定义样例:
apiVersion: v1
kind: Service
metadata:
name: tocat-service
spec:
ports:
- port: 8080
name: service-port
- port: 8005
name: shutdown-port
selector:
tier: frontend
Service的外网访问问题
前面提到,服务的ClusterIP地址在Kubernetes集群内才能被访问,那么如何让集群外的应用访问我们的服务呢?这也是一个相对复杂的问题。要弄明白这个问题的解决思路和解决方法,我们需要先弄明白Kubernetes的三种IP,这三种IP分别如下。
- Node IP:Node的IP地址。
- Pod IP:Pod的IP地址。
- Service IP:Service的IP地址。
首先,Node IP是Kubernetes集群中每个节点的物理网卡的IP地址,是一个真实存在的物理网络,所有属于这个网络的服务器都能通过这个网络直接通信,不管其中是否有部分节点不属于这个Kubernetes集群。这也表明Kubernetes集群之外的节点访问Kubernetes集群内的某个节点或者TCP/IP服务时,都必须通过Node IP通信。
其次,Pod IP是每个Pod的IP地址,在使用Docker作为容器支持引擎的情况下,它是Docker Engine根据docker0网桥的IP地址段进行分配的,通常是一个虚拟二层网络。前面说过,Kubernetes要求位于不同Node上的Pod都能够彼此直接通信,所以Kubernetes中一个Pod里的容器访问另外一个Pod里的容器时,就是通过Pod IP所在的虚拟二层网络进行通信的,而真实的TCP/IP流量是通过Node IP所在的物理网卡流出的。
在Kubernetes集群内,Service的ClusterIP地址属于集群内的地址,无法在集群外直接使用这个地址。为了解决这个问题,Kubernetes首先引入了NodePort这个概念,NodePort也是解决集群外的应用访问集群内服务的直接、有效的常见做法。
以tomcat-service为例,在Service的定义里做如下扩展即可(见代码中的粗体部分):
apiVersion: v1
kind: Service
metadata:
name: tomcat-service
spec:
type: NodePort
ports:
- port: 8080
nodePort: 31002
selector:
tier: frontend
其中,nodePort:31002这个属性表明手动指定tomcat-service的NodePort为31002,否则Kubernetes会自动为其分配一个可用的端口。接下来在浏览器里访问http://
NodePort的实现方式是,在Kubernetes集群的每个Node上都为需要外部访问的Service开启一个对应的TCP监听端口,外部系统只要用任意一个Node的IP地址+NodePort端口号即可访问此服务,在任意Node上运行netstat命令,就可以看到有NodePort端口被监听:
[root@k8s-m01 java_web]# netstat -lntup | grep 31002
tcp 0 0 0.0.0.0:31002 0.0.0.0:* LISTEN 3101/kube-proxy
但NodePort还没有完全解决外部访问Service的所有问题,比如负载均衡问题。假如在我们的集群中有10个Node,则此时最好有一个负载均衡器,外部的请求只需访问此负载均衡器的IP地址,由负载均衡器负责转发流量到后面某个Node的NodePort上,如图1.12所示。
图1.12中的负载均衡器组件独立于Kubernetes集群之外,通常是一个硬件的负载均衡器,也有以软件方式实现的,例如HAProxy或者Nginx。对于每个Service,我们通常需要配置一个对应的负载均衡器实例来转发流量到后端的Node上,这的确增加了工作量及出错的概率。于是Kubernetes提供了自动化的解决方案,如果我们的集群运行在谷歌的公有云GCE上,那么只要把Service的“type=NodePort”改为“type=LoadBalancer”,Kubernetes就会自动创建一个对应的负载均衡器实例并返回它的IP地址供外部客户端使用。其他公有云提供商只要实现了支持此特性的驱动,则也可以达到以上目的。此外,也有MetalLB这样的面向私有集群的Kubernetes负载均衡方案。
NodePort的确功能强大且通用性强,但也存在一个问题,即每个Service都需要在Node上独占一个端口,而端口又是有限的物理资源,那能不能让多个Service共用一个对外端口呢?这就是后来增加的Ingress资源对象所要解决的问题。在一定程度上,我们可以把Ingress的实现机制理解为基于Nginx的支持虚拟主机的HTTP代理。下面是一个Ingress的实例:
kind: Ingress
metadata:
name: name-virtual-host-ingress
spec:
rules:
- host: foo.bar.com
http:
paths:
- backend:
serviceName: service1
servicePort: 80
- host: bar.foo.com
http:
paths:
- backend:
serviceName: service2
servicePort: 80
在以上Ingress的定义中,到虚拟域名first.bar.com请求的流量会被路由到service1,到second.foo.com请求的流量会被路由到service2。通过上面的例子,我们也可以看出,Ingress其实只能将多个HTTP(HTTPS)的Service“聚合”,通过虚拟域名或者URL Path的特征进行路由转发功能。考虑到常见的微服务都采用了HTTP REST协议,所以Ingress这种聚合多个Service并将其暴露到外网的做法还是很有效的。
6.有状态的应用集群
我们知道,Deployment对象是用来实现无状态服务的多副本自动控制功能的,那么有状态的服务,比如ZooKeeper集群、MySQL高可用集群(3节点集群)、Kafka集群等是怎么实现自动部署和管理的呢?这个问题就复杂多了,这些一开始是依赖StatefulSet解决的,但后来发现对于一些复杂的有状态的集群应用来说,StatefulSet还是不够通用和强大,所以后面又出现了Kubernetes Operator。
我们先说说StatefulSet。StatefulSet之前曾用过PetSet这个名称,很多人都知道,在IT世界里,有状态的应用被类比为宠物(Pet),无状态的应用则被类比为牛羊,每个宠物在主人那里都是“唯一的存在”,宠物生病了,我们是要花很多钱去治疗的,需要我们用心照料,而无差别的牛羊则没有这个待遇。总结下来,在有状态集群中一般有如下特殊共性。
- 每个节点都有固定的身份ID,通过这个ID,集群中的成员可以相互发现并通信。
- 集群的规模是比较固定的,集群规模不能随意变动。
- 集群中的每个节点都是有状态的,通常会持久化数据到永久存储中,每个节点在重启后都需要使用原有的持久化数据。
- 集群中成员节点的启动顺序(以及关闭顺序)通常也是确定的。
- 如果磁盘损坏,则集群里的某个节点无法正常运行,集群功能受损。
如果通过Deployment控制Pod副本数量来实现以上有状态的集群,我们就会发现上述很多特性大部分难以满足,比如Deployment创建的Pod因为Pod的名称是随机产生的,我们事先无法为每个Pod都确定唯一不变的ID,不同Pod的启动顺序也无法保证,所以在集群中的某个成员节点宕机后,不能在其他节点上随意启动一个新的Pod实例。另外,为了能够在其他节点上恢复某个失败的节点,这种集群中的Pod需要挂接某种共享存储,为了解决有状态集群这种复杂的特殊应用的建模,Kubernetes引入了专门的资源对象——StatefulSet。StatefulSet从本质上来说,可被看作Deployment/RC的一个特殊变种,它有如下特性。
- StatefulSet里的每个Pod都有稳定、唯一的网络标识,可以用来发现集群内的其他成员。假设StatefulSet的名称为kafka,那么第1个Pod叫kafka-0,第2个叫kafka-1,以此类推。
- StatefulSet控制的Pod副本的启停顺序是受控的,操作第n个Pod时,前n-1个Pod已经是运行且准备好的状态。
- StatefulSet里的Pod采用稳定的持久化存储卷,通过PV或PVC来实现,删除Pod时默认不会删除与StatefulSet相关的存储卷(为了保证数据安全)。
StatefulSet除了要与PV卷捆绑使用,以存储Pod的状态数据,还要与Headless Service配合使用,即在每个StatefulSet定义中都要声明它属于哪个Headless Service。StatefulSet在Headless Service的基础上又为StatefulSet控制的每个Pod实例都创建了一个DNS域名,这个域名的格式如下:
$(podname).$(headless service name)
比如一个3节点的Kafka的StatefulSet集群对应的Headless Service的名称为kafka,StatefulSet的名称为kafka,则StatefulSet里3个Pod的DNS名称分别为kafka-0.kafka、kafka-1.kafka、kafka-2.kafka,这些DNS名称可以直接在集群的配置文件中固定下来。
StatefulSet的建模能力有限,面对复杂的有状态集群时显得力不从心,所以就有了后来的Kubernetes Operator框架和众多的Operator实现了。需要注意的是,Kubernetes Operator框架并不是面向普通用户的,而是面向Kubernetes平台开发者的。平台开发者借助Operator框架提供的API,可以更方便地开发一个类似StatefulSet的控制器。在这个控制器里,开发者通过编码方式实现对目标集群的自定义操控,包括集群部署、故障发现及集群调整等方面都可以实现有针对性的操控,从而实现更好的自动部署和智能运维功能。从发展趋势来看,未来主流的有状态集群基本都会以Operator方式部署到Kubernetes集群中。
7.批处理应用
除了无状态服务、有状态集群、常见的第三种应用,还有批处理应用。批处理应用的特点是一个或多个进程处理一组数据(图像、文件、视频等),在这组数据都处理完成后,批处理任务自动结束。为了支持这类应用,Kubernetes引入了新的资源对象——Job,下面是一个计算圆周率的经典例子:
apiVersion: barch/v1
kind: Job
metadata:
name: pi
spec:
template:
spec:
containers:
- name: pi
image: perl
command: ["perl", "-Mignum=bpi", "-wle", "print bpi(100)"]
restartPolicy: Never
# 需要运行任务数的总数
parallelism: 1
# 并发运行的个数
completions: 5
8.应用的配置问题
通过前面的学习,我们初步理解了三种应用建模的资源对象,总结如下。
- 无状态服务的建模:Deployment。
- 有状态集群的建模:StatefulSet。
- 批处理应用的建模:Job。
在进行应用建模时,应该如何解决应用需要在不同的环境中修改配置的问题呢?这就涉及ConfigMap和Secret两个对象。
ConfigMap顾名思义,就是保存配置项(key=value)的一个Map,如果你只是把它理解为编程语言中的一个Map,那就大错特错了。ConfigMap是分布式系统中“配置中心”的独特实现之一。我们知道,几乎所有应用都需要一个静态的配置文件来提供启动参数,当这个应用是一个分布式应用,有多个副本部署在不同的机器上时,配置文件的分发就成为一个让人头疼的问题,所以很多分布式系统都有一个配置中心组件,来解决这个问题。但配置中心通常会引入新的API,从而导致应用的耦合和侵入。Kubernetes则采用了一种简单的方案来规避这个问题,如图1.13所示,具体做法如下。
- 用户将配置文件的内容保存到ConfigMap中,文件名可作为key,value就是整个文件的内容,多个配置文件都可被放入同一个ConfigMap。
- 在建模用户应用时,在Pod里将ConfigMap定义为特殊的Volume进行挂载。在Pod被调度到某个具体Node上时,ConfigMap里的配置文件会被自动还原到本地目录下,然后映射到Pod里指定的配置目录下,这样用户的程序就可以无感知地读取配置了。
- 在ConfigMap的内容发生修改后,Kubernetes会自动重新获取ConfigMap的内容,并在目标节点上更新对应的文件。
接下来说说Secret。Secret也用于解决应用配置的问题,不过它解决的是对敏感信息的配置问题,比如数据库的用户名和密码、应用的数字证书、Token、SSH密钥及其他需要保密的敏感配置。对于这类敏感信息,我们可以创建一个Secret对象,然后被Pod引用。Secret中的数据要求以BASE64编码格式存放。注意,BASE64编码并不是加密的,在Kubernetes 1.7版本以后,Secret中的数据才可以以加密的形式进行保存,更加安全。
9.应用的运维问题
本节最后说说与应用的自动运维相关的几个重要对象。
首先就是HPA(Horizontal Pod Autoscaler),如果我们用Deployment来控制Pod的副本数量,则可以通过手工运行kubectl scale命令来实现Pod扩容或缩容。如果仅仅到此为止,则显然不符合谷歌对Kubernetes的定位目标——自动化、智能化。在谷歌看来,分布式系统要能够根据当前负载的变化自动触发水平扩容或缩容,因为这一过程可能是频繁发生、不可预料的,所以采用手动控制的方式是不现实的,因此就有了后来的HPA这个高级功能。我们可以将HPA理解为Pod横向自动扩容,即自动控制Pod数量的增加或减少。通过追踪分析指定Deployment控制的所有目标Pod的负载变化情况,来确定是否需要有针对性地调整目标Pod的副本数量,这是HPA的实现原理。Kubernetes内置了基于Pod的CPU利用率进行自动扩缩容的机制,应用开发者也可以自定义度量指标如每秒请求数,来实现自定义的HPA功能。下面是一个HPA定义的例子:
apiVersion: autoscaling/v1
kind: HorizontalPodAutoscaler
metadata:
name: php-apache
namespace: default
spec:
maxReplicas: 10
minReplicas: 1
scaleTargetRef:
kind: Deployment
name: php-apache
targetCPUUtilizationPercentage: 90
根据上面的定义,我们可以知道这个HPA控制的目标对象是一个名为php-apache的Deployment里的Pod副本,当这些Pod副本的CPU利用率的值超过90%时,会触发自动动态扩容,限定Pod的副本数量为1~10。HPA很强大也比较复杂,我们在后续章节中会继续深入学习。
接下来就是VPA(Vertical Pod Autoscaler),即垂直Pod自动扩缩容,它根据容器资源使用率自动推测并设置Pod合理的CPU和内存的需求指标,从而更加精确地调度Pod,实现整体上节省集群资源的目标,因为无须人为操作,因此也进一步提升了运维自动化的水平。VPA目前属于比较新的特性,也不能与HPA共同操控同一组目标Pod,它们未来应该会深入融合,建议读者关注其发展状况。
存储类:
存储类的资源对象主要包括Volume、Persistent Volume、PVC和StorageClass
首先看看基础的存储类资源对象——Volume(存储卷)。
Volume是Pod中能够被多个容器访问的共享目录。Kubernetes中的Volume概念、用途和目的与Docker中的Volume比较类似,但二者不能等价。首先,Kubernetes中的Volume被定义在Pod上,被一个Pod里的多个容器挂载到具体的文件目录下;其次,Kubernetes中的Volume与Pod的生命周期相同,但与容器的生命周期不相关,当容器终止或者重启时,Volume中的数据也不会丢失;最后,Kubernetes支持多种类型的Volume,例如GlusterFS、Ceph等分布式文件系统。
Volume的使用也比较简单,在大多数情况下,我们先在Pod上声明一个Volume,然后在容器里引用该Volume并将其挂载(Mount)到容器里的某个目录下。举例来说,若我们要给之前的Tomcat Pod增加一个名为datavol的Volume,并将其挂载到容器的/mydata-data目录下,则只对Pod的定义文件做如下修正即可(代码中的粗体部分):
template:
metadata:
labels:
app: app-demo
tier: frontend
spec:
volumes:
- name: datavol
emptyDir: {}
containers:
- name: tocat-demo
image:tomcat
volumeMounts:
- mountPath: /mydata-data
name: datavol
imagePullPolicy: IfNotPresent
Kubernetes提供了非常丰富的Volume类型供容器使用,例如临时目录、宿主机目录、共享存储等,下面对其中一些常见的类型进行说明。
1.emptyDir
一个emptyDir是在Pod分配到Node时创建的。从它的名称就可以看出,它的初始内容为空,并且无须指定宿主机上对应的目录文件,因为这是Kubernetes自动分配的一个目录,当Pod从Node上移除时,emptyDir中的数据也被永久移除。emptyDir的一些用途如下。
- 临时空间,例如用于某些应用程序运行时所需的临时目录,且无须永久保留。
- 长时间任务执行过程中使用的临时目录。
- 一个容器需要从另一个容器中获取数据的目录(多容器共享目录)。
在默认情况下,emptyDir使用的是节点的存储介质,例如磁盘或者网络存储。还可以使用emptyDir.medium属性,把这个属性设置为“Memory”,就可以使用更快的基于内存的后端存储了。需要注意的是,这种情况下的emptyDir使用的内存会被计入容器的内存消耗,将受到资源限制和配额机制的管理。
2.hostPath
hostPath为在Pod上挂载宿主机上的文件或目录,通常可以用于以下几方面。
- 在容器应用程序生成的日志文件需要永久保存时,可以使用宿主机的高速文件系统对其进行存储。
- 需要访问宿主机上Docker引擎内部数据结构的容器应用时,可以通过定义hostPath为宿主机/var/lib/docker目录,使容器内部的应用可以直接访问Docker的文件系统。
在使用这种类型的Volume时,需要注意以下几点。
- 在不同的Node上具有相同配置的Pod,可能会因为宿主机上的目录和文件不同,而导致对Volume上目录和文件的访问结果不一致。
- 如果使用了资源配额管理,则Kubernetes无法将hostPath在宿主机上使用的资源纳入管理。
在下面的例子中使用了宿主机的/data目录定义了一个hostPath类型的Volume:
volumes:
- name: "persistent-storage"
hostPath:
path: "/data"
3.公有云Volume
公有云提供的Volume类型包括谷歌公有云提供的GCEPersistentDisk、亚马逊公有云提供的AWS Elastic Block Store(EBS Volume)等。当我们的Kubernetes集群运行在公有云上或者使用公有云厂家提供的Kubernetes集群时,就可以使用这类Volume。
4.其他类型的Volume
- iscsi:将iSCSI存储设备上的目录挂载到Pod中。
- nfs:将NFS Server上的目录挂载到Pod中。
- glusterfs:将开源GlusterFS网络文件系统的目录挂载到Pod中。
- rbd:将Ceph块设备共享存储(Rados Block Device)挂载到Pod中。
- gitRepo:通过挂载一个空目录,并从Git库克隆(clone)一个git repository以供Pod使用。
- configmap:将配置数据挂载为容器内的文件。
- secret:将Secret数据挂载为容器内的文件。
动态存储管理
Volume属于静态管理的存储,即我们需要事先定义每个Volume,然后将其挂载到Pod中去用,这种方式存在很多弊端,典型的弊端如下。
- 配置参数烦琐,存在大量手工操作,违背了Kubernetes自动化的追求目标。
- 预定义的静态Volume可能不符合目标应用的需求,比如容量问题、性能问题。
所以Kubernetes后面就发展了存储动态化的新机制,来实现存储的自动化管理。相关的核心对象(概念)有三个:Persistent Volume(简称PV)、StorageClass、PVC。
PV表示由系统动态创建(dynamically provisioned)的一个存储卷,可以被理解成Kubernetes集群中某个网络存储对应的一块存储,它与Volume类似,但PV并不是被定义在Pod上的,而是独立于Pod之外定义的。PV目前支持的类型主要有gcePersistentDisk、AWSElasticBlockStore、AzureFile、AzureDisk、FC(Fibre Channel)、NFS、iSCSI、RBD(Rados Block Device)、CephFS、Cinder、GlusterFS、VsphereVolume、Quobyte Volumes、VMware Photon、Portworx Volumes、ScaleIO Volumes、HostPath、Local等。
我们知道,Kubernetes支持的存储系统有多种,那么系统怎么知道从哪个存储系统中创建什么规格的PV存储卷呢?这就涉及StorageClass与PVC。StorageClass用来描述和定义某种存储系统的特征,下面给出一个具体的例子:
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: standard
provisioner: kubernetes.io/aws-ebs
paramters:
type: gp2
reclaimPolicy: Retain
allowVolumeExpansion: true
mountOptions:
- debug
volumeBindingMode: Immdiate
从上面的例子可以看出,StorageClass有几个关键属性:provisioner、parameters和reclaimPolicy,系统在动态创建PV时会用到这几个参数。简单地说,provisioner代表了创建PV的第三方存储插件,parameters是创建PV时的必要参数,reclaimPolicy则表明了PV回收策略,回收策略包括删除或则保留。需要注意的是,StorageClass的名称会在PVC(PV Claim)中出现,下面就是一个典型的PVC定义:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: claim1
spec:
accessModes:
- ReadWriteOnce
storageClassName: standard
resources:
requests:
storage: 30Gi
PVC正如其名,表示应用希望申请的PV规格,其中重要的属性包括accessModes(存储访问模式)、storageClassName(用哪种StorageClass来实现动态创建)及resources(存储的具体规格)。
有了以StorageClass与PVC为基础的动态PV管理机制,我们就很容易管理和使用Volume了,只要在Pod里引用PVC即可达到目的,如下面的例子所示
spec:
containers:
- name: myapp
image: tomcat: 8.5.38-jre8
volumeMounts:
- name: tomcatedata
mountPath: "/data"
volumes:
- name: tomcatedata
persistentVolumeClaim:
claimName: claim1
除了动态创建PV,PV动态扩容、快照及克隆的能力也是Kubernetes社区正在积极研发的高级特性。
安全类:
安全始终是Kubernetes发展过程中的一个关键领域。
从本质上来说,Kubernetes可被看作一个多用户共享资源的资源管理系统,这里的资源主要是各种Kubernetes里的各类资源对象,比如Pod、Service、Deployment等。只有通过认证的用户才能通过Kubernetes的API Server查询、创建及维护相应的资源对象,理解这一点很关键。
Kubernetes里的用户有两类:我们开发的运行在Pod里的应用;普通用户,如典型的kubectl命令行工具,基本上由指定的运维人员(集群管理员)使用。在更多的情况下,我们开发的Pod应用需要通过API Server查询、创建及管理其他相关资源对象,所以这类用户才是Kubernetes的关键用户。为此,Kubernetes设计了Service Account这个特殊的资源对象,代表Pod应用的账号,为Pod提供必要的身份认证。在此基础上,Kubernetes进一步实现和完善了基于角色的访问控制权限系统——RBAC(Role-Based Access Control)。
在默认情况下,Kubernetes在每个命名空间中都会创建一个默认的名称为default的Service Account,因此Service Account是不能全局使用的,只能被它所在命名空间中的Pod使用。通过以下命令可以查看集群中的所有Service Account:
[root@k8s-m01 ~]# kubectl get sa --all-namespaces
NAMESPACE NAME SECRETS AGE
default default 1 2d17h
kube-system default 1 2d17h
Service Account是通过Secret来保存对应的用户(应用)身份凭证的,这些凭证信息有CA根证书数据(ca.crt)和签名后的Token信息(Token)。在Token信息中就包括了对应的Service Account的名称,因此API Server通过接收到的Token信息就能确定Service Account的身份。在默认情况下,用户创建一个Pod时,Pod会绑定对应命名空间中的default这个Service Account作为其“公民身份证”。当Pod里的容器被创建时,Kubernetes会把对应的Secret对象中的身份信息(ca.crt、Token等)持久化保存到容器里固定位置的本地文件中,因此当容器里的用户进程通过Kubernetes提供的客户端API去访问API Server时,这些API会自动读取这些身份信息文件,并将其附加到HTTPS请求中传递给API Server以完成身份认证逻辑。在身份认证通过以后,就涉及“访问授权”的问题,这就是RBAC要解决的问题了。
首先我们要学习的是Role这个资源对象,包括Role与ClusterRole两种类型的角色。角色定义了一组特定权限的规则,比如可以操作某类资源对象。局限于某个命名空间的角色由Role对象定义,作用于整个Kubernetes集群范围内的角色则通过ClusterRole对象定义。下面是Role的一个例子,表示在命名空间default中定义一个Role对象,用于授予对Pod资源的读访问权限,绑定到该Role的用户则具有对Pod资源的get、watch和list权限:
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
namespace: default
name: pod-reader
rules:
- apiGroups: [""] # 空字符串"" 表明使用core API group
resources: ["pod"]
verbs: ["get", "watch", "list"]
接下来就是如何将Role与具体用户绑定(用户授权)的问题了。我们可以通过RoleBinding与ClusterRoleBinding来解决这个问题。下面是一个具体的例子,在命名空间default中将“pod-reader”角色授予用户“Caden”,结合对应的Role的定义,表明这一授权将允许用户“Caden”从命名空间default中读取pod。
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: read-pods
namespace: default
subjects:
- kind: User
name: Caden
apiGoup: rbac.authorization.k8s.io
roleRef:
kind: Role
name: pod-reader
apiGroup: rbac.authorization.k8s.io
在RoleBinding中使用subjects(目标主体)来表示要授权的对象,这是因为我们可以授权三类目标账号:Group(用户组)、User(某个具体用户)和Service Account(Pod应用所使用的账号)。
在安全领域,除了以上针对API Server访问安全相关的资源对象,还有一种特殊的资源对象——NetworkPolicy(网络策略),它是网络安全相关的资源对象,用于解决用户应用之间的网络隔离和授权问题。NetworkPolicy是一种关于Pod间相互通信,以及Pod与其他网络端点间相互通信的安全规则设定。
NetworkPolicy资源使用标签选择Pod,并定义选定Pod所允许的通信规则。在默认情况下,Pod间及Pod与其他网络端点间的访问是没有限制的,这假设了Kubernetes集群被一个厂商(公司/租户)独占,其中部署的应用都是相互可信的,无须相互防范。但是,如果存在多个厂商共同使用一个Kubernetes集群的情况,则特别是在公有云环境中,不同厂商的应用要相互隔离以增加安全性,这就可以通过NetworkPolicy来实现了。