k8s实战
核心概念
架构
Kubernetes把集群里的计算资源定义为节点(Node),其中又划分成控制面和数据面两类。
- 控制面是Master节点,负责管理集群和运维监控应用,里面的核心组件是apiserver、etcd、scheduler、controller-manager。
- 数据面是Worker节点,受Master节点的管控,里面的核心组件是kubelet、kube-proxy、container-runtime。


busy-pod.yml 文件定义
apiVersion: v1
kind: Pod
metadata:
name: busy-pod
labels:
owner: chrono
env: demo
region: north
tier: back
spec:
containers:
- image: busybox:latest
name: busy
imagePullPolicy: IfNotPresent
env:
- name: os
value: "ubuntu"
- name: debug
value: "on"
command:
- /bin/echo
args:
- "$(os), $(debug)"
- ports:列出容器对外暴露的端口,和Docker的 -p 参数有点像。
- imagePullPolicy:指定镜像的拉取策略,可以是Always/Never/IfNotPresent,一般默认是IfNotPresent,也就是说只有本地不存在才会远程拉取镜像,可以减少网络消耗。
- env:定义Pod的环境变量,和Dockerfile里的 ENV 指令有点类似,但它是运行时指定的,更加灵活可配置。
- command:定义容器启动时要执行的命令,相当于Dockerfile里的 ENTRYPOINT 指令。
- args:它是command运行时的参数,相当于Dockerfile里的 CMD 指令,这两个命令和Docker的含义不同,要特别注意。
用yml操作pod
//创建
kubectl apply -f busy-pod.yml
//删除
kubectl delete -f busy-pod.yml
kubectl delete pod busy-pod
//查看pod日志
kubectl logs busy-pod
//查看Pod列表和运行状态
kubectl get pod
//检查pod的详细状态
kubectl describe pod busy-pod
//拷贝本地文件到pod(拷贝进Pod的“/tmp”目录里)
echo 'aaa' > a.txt
kubectl cp a.txt ngx-pod:/tmp
//进入pod内部
kubectl exec -it ngx-pod -- sh
ConfigMap/Secret
一类是明文配置,也就是不保密,可以任意查询修改,比如服务端口、运行参数、文件路径等等。
另一类则是机密配置,由于涉及敏感信息需要保密,不能随便查看,比如密码、密钥、证书等等。
ConfigMap
用命令 kubectl create 来创建一个ConfigMap的YAML样板
export out="--dry-run=client -o yaml" # 定义Shell变量
kubectl create cm info $out
生成的样板文件如下
apiVersion: v1
kind: ConfigMap
metadata:
creationTimestamp: null
name: info
因为ConfigMap存储的是配置数据,是静态的字符串,并不是容器,所以它们就不需要用“spec”字段来说明运行时的“规格”
生成带有“data”字段的YAML样板
需要在 kubectl create 后面多加一个参数 --from-literal ,表示从字面值生成一些数据,注意,因为在ConfigMap里的数据都是Key-Value结构,所以 --from-literal 参数需要使用 k=v 的形式。
kubectl create cm info --from-literal=k=v $out
生成的样板文件
apiVersion: v1
data:
k: v
kind: ConfigMap
metadata:
creationTimestamp: null
name: info
创建ConfigMap
kubectl apply -f cm.yml
查看ConfigMap状态
kubectl get cm
kubectl describe cm info
Secret
Kubernetes里Secret对象又细分出很多类,比如:
- 访问私有镜像仓库的认证信息
- 身份识别的凭证信息
- HTTPS通信的证书和私钥
- 一般的机密信息(格式由用户自行解释)
创建一般机密信息模板
kubectl create secret generic user --from-literal=name=root $out
生成的Secret对象模板如下:
apiVersion: v1
data:
name: cm9vdA==
kind: Secret
metadata:
creationTimestamp: null
name: user
“name”值是一串“乱码”,只是做了Base64编码,根本算不上真正的加密
手动用Linux小工具“base64”来对数据编码,然后写入YAML文件,比如:
echo -n "123456" | base64
MTIzNDU2
要注意这条命令里的 echo ,必须要加参数 -n 去掉字符串里隐含的换行符,否则Base64编码出来的字符串就是错误的。
重新编辑Secret的YAML,为它添加两个新的数据,方式可以是参数 --from-literal 自动编码,也可以是自己手动编码:
apiVersion: v1
kind: Secret
metadata:
name: user
data:
name: cm9vdA== # root
pwd: MTIzNDU2 # 123456
db: bXlzcWw= # mysql
创建secret
kubectl apply -f secret.yml
查看secret
kubectl get secret
kubectl describe secret user
如何在Pod里引用ConfigMap和Secret对象
pod可以使用另一个“valueFrom”字段,从ConfigMap或者Secret对象里获取值,这样就实现了把配置信息以环境变量的形式注入进Pod,也就是配置与应用的解耦。
“valueFrom”字段在YAML里的嵌套层次比较深,初次使用最好看一下 kubectl explain 对它的说明
kubectl explain pod.spec.containers.env.valueFrom
“valueFrom”字段指定了环境变量值的来源,可以是“configMapKeyRef”或者“secretKeyRef”,然后你要再进一步指定应用的ConfigMap/Secret的“name”和它里面的“key”,要当心的是这个**“name”字段是API对象的名字**,而不是Key-Value的名字。
pod引用示范
apiVersion: v1
kind: Pod
metadata:
name: env-pod
spec:
containers:
- env:
- name: COUNT
valueFrom:
configMapKeyRef:
name: info
key: count
- name: GREETING
valueFrom:
configMapKeyRef:
name: info
key: greeting
- name: USERNAME
valueFrom:
secretKeyRef:
name: user
key: name
- name: PASSWORD
valueFrom:
secretKeyRef:
name: user
key: pwd
image: busybox
name: busy
imagePullPolicy: IfNotPresent
command: ["/bin/sleep", "300"]
图解引用关系

验证是否生效
kubectl apply -f env-pod.yml
kubectl exec -it env-pod -- sh
echo $COUNT
echo $GREETING
echo $USERNAME $PASSWORD
结果证明Pod对象成功组合了ConfigMap和Secret对象
以Volume的方式使用ConfigMap/Secret
把Pod理解成是一个虚拟机,那么Volume就相当于是虚拟机里的磁盘,Pod“挂载(mount)”多个Volume,里面存放供Pod访问的数据
在Pod里挂载Volume很容易,只需要在“spec”里增加一个“volumes”字段,然后再定义卷的名字和引用的ConfigMap/Secret就可以了。要注意的是Volume属于Pod,不属于容器,所以它和字段“containers”是同级的,都属于“spec”。
定义两个Volume,分别引用ConfigMap和Secret,名字是 cm-vol 和 sec-vol:
spec:
volumes:
- name: cm-vol
configMap:
name: info
- name: sec-vol
secret:
secretName: user
定义volumeMounts,可以把定义好的Volume挂载到容器里的某个路径下,所以需要在里面用“mountPath”“name”明确地指定挂载路径和Volume的名字。
containers:
- volumeMounts:
- mountPath: /tmp/cm-items
name: cm-vol
- mountPath: /tmp/sec-items
name: sec-vol
图解定义关系

pod的完整YAML如下
apiVersion: v1
kind: Pod
metadata:
name: vol-pod
spec:
volumes:
- name: cm-vol
configMap:
name: info
- name: sec-vol
secret:
secretName: user
containers:
- volumeMounts:
- mountPath: /tmp/cm-items
name: cm-vol
- mountPath: /tmp/sec-items
name: sec-vol
image: busybox
name: busy
imagePullPolicy: IfNotPresent
command: ["/bin/sleep", "300"]
创建以及查看
kubectl apply -f vol-pod.yml
kubectl get pod
kubectl exec -it vol-pod -- sh
单点K8s搭建WordPress网站
整体调用链路

第一步:编排MariaDB对象
定义一个 maria-cm 对象
apiVersion: v1
kind: ConfigMap
metadata:
name: maria-cm
data:
DATABASE: 'db'
USER: 'mch'
PASSWORD: '0000'
ROOT_PASSWORD: '0000'
定义一个maria-pod对象
apiVersion: v1
kind: Pod
metadata:
name: maria-pod
labels:
app: wordpress
role: database
spec:
containers:
- image: mariadb:10
name: maria
imagePullPolicy: IfNotPresent
ports:
- containerPort: 3306
envFrom:
- prefix: 'MARIADB_'
configMapRef:
name: maria-cm
注意:这里使用了一个新的字段“envFrom”,这是因为ConfigMap里的信息比较多,如果用 env.valueFrom 一个个地写会非常麻烦,容易出错,而 envFrom 可以一次性地把ConfigMap里的字段全导入进Pod,并且能够指定变量名的前缀(即这里的 MARIADB_),非常方便。
创建pod
kubectl apply -f mariadb-pod.yml
kubectl get pod -o wide
第二步:编排WordPress对象
定义wp-cm.yml
apiVersion: v1
kind: ConfigMap
metadata:
name: wp-cm
data:
HOST: '172.17.0.2'
USER: 'mch'
PASSWORD: '0000'
NAME: 'db'
注意:“HOST”字段,它必须是MariaDB Pod的IP地址,如果不写正确WordPress会无法正常连接数据库。
定义wp-pod.yml
apiVersion: v1
kind: Pod
metadata:
name: wp-pod
labels:
app: wordpress
role: website
spec:
containers:
- image: wordpress:5
name: wp-pod
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80
envFrom:
- prefix: 'WORDPRESS_DB_'
configMapRef:
name: wp-cm
创建pod
kubectl apply -f wp-pod.yml
kubectl get pod -o wide
第三步: 为WordPress Pod映射端口号,让它在集群外可见
kubectl port-forward wp-pod 8080:80 &
注意在命令的末尾使用了一个 & 符号,让端口转发工作在后台进行,这样就不会阻碍我们后续的操作。
如果想关闭端口转发,需要敲命令 fg ,它会把后台的任务带回到前台,然后就可以简单地用“Ctrl + C”来停止转发了。
第四步:创建反向代理的Nginx,让网站对外提供服务
这是因为WordPress网站使用了URL重定向,直接使用“8080”会导致跳转故障,所以为了让网站正常工作,还应该在Kubernetes之外启动Nginx反向代理,保证外界看到的仍然是“80”端口号。(这里的细节和我们的课程关系不大,感兴趣的同学可以留言提问讨论)
Nginx的配置文件目标地址变成了“127.0.0.1:8080”,它就是我们在第三步里用 kubectl port-forward 命令创建的本地地址
server {
listen 80;
default_type text/html;
location / {
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_pass http://127.0.0.1:8080;
}
}
运行nginx代理
docker run -d --rm \
--net=host \
-v /tmp/proxy.conf:/etc/nginx/conf.d/default.conf \
nginx:alpine
第五步 查看网站
地址:虚拟机ip(我这里是“http://192.168.10.208”)
用kubeadm搭建K8s集群
引用文章: https://blog.csdn.net/xuezhiwu001/article/details/128444657?spm=1001.2014.3001.5501
Deploymen
创建了一个名字叫 ngx-dep 的对象,使用的镜像是 nginx:alpine:
export out="--dry-run=client -o yaml"
kubectl create deploy ngx-dep --image=nginx:alpine $out
模板样式
apiVersion: apps/v1
kind: Deployment
metadata:
creationTimestamp: null
labels:
app: ngx-dep
name: ngx-dep
spec:
replicas: 1
selector:
matchLabels:
app: ngx-dep
strategy: {}
template:
metadata:
creationTimestamp: null
labels:
app: ngx-dep
spec:
containers:
- image: nginx:alpine
name: nginx
resources: {}
status: {}
selector,它的作用是“筛选”出要被Deployment管理的Pod对象,下属字段“matchLabels”定义了Pod对象应该携带的label,它必须和“template”里Pod定义的“labels”完全相同,否则Deployment就会找不到要控制的Pod对象,apiserver也会告诉你YAML格式校验错误无法创建
理解Deployment与被它管理的Pod的组合关系

kubectl操作Deployment
//创建
kubectl apply -f deploy.yml
//查看Deployment的状态
kubectl get deploy
//删除其中一个pod
kubectl delete pod ngx-dep-6796688696-jm6tt
//应用扩容到5个
kubectl scale --replicas=5 deploy ngx-dep
DaemonSet
在线业务API对象,它会在Kubernetes集群的每个节点上都运行一个Pod,就好像是Linux系统里的“守护进程”(Daemon)
- 网络应用(如kube-proxy),必须每个节点都运行一个Pod,否则节点就无法加入Kubernetes网络。
- 监控应用(如Prometheus),必须每个节点都有一个Pod用来监控节点的状态,实时上报信息。
- 日志应用(如Fluentd),必须在每个节点上运行一个Pod,才能够搜集容器运行时产生的日志数据。
- 安全应用,同样的,每个节点都要有一个Pod来执行安全审计、入侵检查、漏洞扫描等工作。
DaemonSet的目标是在集群的每个节点上运行且仅运行一个Pod,就好像是为节点配上一只“看门狗”,忠实地“守护”着节点
DaemonSet的YAML示例
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: redis-ds
labels:
app: redis-ds
spec:
selector:
matchLabels:
name: redis-ds
template:
metadata:
labels:
name: redis-ds
spec:
containers:
- image: redis:5-alpine
name: redis
ports:
- containerPort: 6379
DaemonSet与Deployment的差异

kubectl操作DaemonSet
//创建
kubectl apply -f ds.yml
//查看Deployment的状态
kubectl get daemonset
//发现master没有这个pod
kubectl get pod -o wide
污点(taint)和容忍度(toleration)
可以用 kubectl describe node 来查看Master和Worker的状态:
kubectl describe node master
Name: master
Roles: control-plane,master
...
Taints: node-role.kubernetes.io/master:NoSchedule
...
kubectl describe node worker
Name: worker
Roles: <none>
...
Taints: <none>
...
可以看到,Master节点默认有一个 taint,名字是 node-role.kubernetes.io/master,它的效果是 NoSchedule,也就是说这个污点会拒绝Pod调度到本节点上运行,而Worker节点的 taint 字段则是空的。
这正是Master和Worker在Pod调度策略上的区别所在,通常来说Pod都不能容忍任何“污点”,所以加上了 taint 属性的Master节点也就会无缘Pod了。
怎么让DaemonSet在Master节点(或者任意其他节点)上运行了,方法有两种
(一) 是去掉Master节点上的 taint,让Master变得和Worker一样“纯洁无瑕”
去掉Master节点的“NoSchedule”效果,就要用这条命令:
kubectl taint node master node-role.kubernetes.io/master:NoSchedule-
去掉后,用 kubectl get 来查看发现会在Master节点上创建一个“守护”Pod
这种方法修改的是Node的状态,影响面会比较大,可能会导致很多Pod都跑到这个节点上运行,所以我们可以保留Node的“污点”,为需要的Pod添加“容忍度”,只让某些Pod运行在个别节点上,实现“精细化”调度。
用 kubectl taint 命令把Master的“污点”加上:
kubectl taint node master node-role.kubernetes.io/master:NoSchedule
(二) 为Pod添加字段 tolerations,让它能够“容忍”某些“污点”,就可以在任意的节点上运行了
tolerations 是一个数组,里面可以列出多个被“容忍”的“污点”,需要写清楚“污点”的名字、效果。比较特别是要用 operator 字段指定如何匹配“污点”,一般我们都使用 Exists,也就是说存在这个名字和效果的“污点”。
如果我们想让DaemonSet里的Pod能够在Master节点上运行,就要写出这样的一个 tolerations,容忍节点的 node-role.kubernetes.io/master:NoSchedule 这个污点:
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: redis-ds
labels:
app: redis-ds
spec:
selector:
matchLabels:
name: redis-ds
template:
metadata:
labels:
name: redis-ds
spec:
containers:
- image: redis:5-alpine
name: redis
ports:
- containerPort: 6379
//新增容忍点
tolerations:
- key: node-role.kubernetes.io/master
effect: NoSchedule
operator: Exists
再重新部署加上了“容忍度”的DaemonSet
kubectl apply -f ds.yml
就会看到DaemonSet仍然有两个Pod,分别运行在Master和Worker节点上
特别说明一下,“容忍度”并不是DaemonSet独有的概念,而是从属于Pod
什么是静态Pod
Kubernetes还支持另外一种叫“静态Pod”的应用部署手段。
“静态Pod”非常特殊,它不受Kubernetes系统的管控,不与apiserver、scheduler发生关系,所以是“静态”的。
但既然它是Pod,也必然会“跑”在容器运行时上,也会有YAML文件来描述它,而唯一能够管理它的Kubernetes组件也就只有在每个节点上运行的kubelet了。
“静态Pod”的YAML文件默认都存放在节点的 /etc/kubernetes/manifests 目录下,它是Kubernetes的专用目录。
下面的这张截图就是Master节点里目录的情况:

可以看到,Kubernetes的4个核心组件apiserver、etcd、scheduler、controller-manager原来都以静态Pod的形式存在的,这也是为什么它们能够先于Kubernetes集群启动的原因
Service
本质上就是一个由kube-proxy控制的四层负载均衡,在TCP/IP协议栈上转发流量

生成Service模板命令
export out="--dry-run=client -o yaml"
kubectl expose deploy ngx-dep --port=80 --target-port=80 $out
模板格式:
apiVersion: v1
kind: Service
metadata:
creationTimestamp: null
labels:
app: ngx-dep
name: ngx-dep
spec:
ports:
- port: 80
protocol: TCP
targetPort: 80
selector:
app: ngx-dep
status:
loadBalancer: {}
service与引用pod的关系

使用Service实践
(1) 创建一个ConfigMap,定义一个Nginx的配置片段,它会输出服务器的地址、主机名、请求的URI等基本信息
apiVersion: v1
kind: ConfigMap
metadata:
name: ngx-conf
data:
default.conf: |
server {
listen 80;
location / {
default_type text/plain;
return 200
'srv : $server_addr:$server_port\nhost: $hostname\nuri : $request_method $host $request_uri\ndate: $time_iso8601\n';
}
}
(2) 在Deployment的“template.volumes”里定义存储卷,再用“volumeMounts”把配置文件加载进Nginx容器里
apiVersion: apps/v1
kind: Deployment
metadata:
name: ngx-dep
spec:
replicas: 2
selector:
matchLabels:
app: ngx-dep
template:
metadata:
labels:
app: ngx-dep
spec:
volumes:
- name: ngx-conf-vol
configMap:
name: ngx-conf
containers:
- image: nginx:alpine
name: nginx
ports:
- containerPort: 80
volumeMounts:
- mountPath: /etc/nginx/conf.d
name: ngx-conf-vol
部署这个Deployment
(3) Service定义
apiVersion: v1
kind: Service
metadata:
name: ngx-svc
spec:
selector:
app: ngx-dep
ports:
- port: 80
targetPort: 80
protocol: TCP
部署Service
用 kubectl describe describe svc ngx-svc命令查看

查看容器 kubectl get pod -o wide

测试Service的负载均衡
进入其中一个pod ,kubectl exec -it ngx-dep-6796688696-rkvfc – sh
用curl访问Service的IP地址,就会看到它把数据转发给后端的Pod,输出信息会显示具体是哪个Pod响应了请求,就表明Service确实完成了对Pod的负载均衡任务

以域名的方式使用Service
Service对象的域名完全形式是“对象.名字空间.svc.cluster.local”,但很多时候也可以省略后面的部分,直接写“对象.名字空间”,甚至“对象名”就足够了,默认会使用对象所在的名字空间(比如这里就是default)
测试 :进入其中一个pod ,kubectl exec -it ngx-dep-6796688696-rkvfc – sh

(顺便说一下,Kubernetes也为每个Pod分配了域名,形式是“IP地址.名字空间.pod.cluster.local”,但需要把IP地址里的 . 改成 - 。比如地址 10.10.1.87,它对应的域名就是 10-10-1-87.default.pod。)
Service对外暴露服务
Service对象有一个关键字段“type”,分别是“ExternalName”、“LoadBalancer”、“NodePort”、“ClusterIP”,表示Service是哪种类型的负载均衡。
默认的是“ClusterIP”,表示Service的静态IP地址只能在集群内访问,前两种类型一般由云服务商提供,我们的实验环境用不到,所以接下来就重点看“NodePort”这个类型
使用命令 kubectl expose 的时候加上参数 --type=NodePort,或者在YAML里添加字段 type:NodePort,那么Service除了会对后端的Pod做负载均衡之外,还会在集群里的每个节点上创建一个独立的端口,用这个端口对外提供服务,这也正是“NodePort”这个名字的由来。
修改一下Service的YAML文件,加上字段“type”:
apiVersion: v1
...
spec:
...
type: NodePort
然后创建对象,再查看它的状态:

看到“TYPE”变成了“NodePort”
而在“PORT”列里的端口信息也不一样,除了集群内部使用的“80”端口,还多出了一个“30651”端口,这就是Kubernetes在节点上为Service创建的专用映射端口。
因为这个端口号属于节点,外部能够直接访问,所以现在我们就可以不用登录集群节点或者进入Pod内部,直接在集群外使用任意一个节点的IP地址,就能够访问Service和它代理的后端服务了。
比如我现在所在的服务器是“192.168.228.130”,在这台主机上用curl访问Kubernetes集群的两个节点“192.168.228.132”“192.168.228.133”,就可以得到Nginx Pod的响应数据:

NodePort与Service、Deployment的对应关系如图

缺点
-
第一个缺点是它的端口数量很有限。Kubernetes为了避免端口冲突,默认只在“30000~32767”这个范围内随机分配,只有2000多个,而且都不是标准端口号,这对于具有大量业务应用的系统来说根本不够用。
-
第二个缺点是它会在每个节点上都开端口,然后使用kube-proxy路由到真正的后端Service,这对于有很多计算节点的大集群来说就带来了一些网络通信成本,不是特别经济。
-
第三个缺点,它要求向外界暴露节点的IP地址,这在很多时候是不可行的,为了安全还需要在集群外再搭一个反向代理,增加了方案的复杂度。
Ingress
流量的总入口,统管集群的进出口数据
Ingress由Ingress规则、IngressController、IngressClass这3部分组成
-
Ingress只是规则的集合,自身不具备流量管理能力,需要Ingress Controller应用Ingress规则才能真正发挥作用
-
Ingress Class解耦了Ingress和Ingress Controller,用来关联Ingress和Ingress Controller。
-
ingress Controller通常是一组拥有7层代理能力服务的Pod,最流行的Ingress Controller是Nginx Ingress Controller,它基于经典反向代理软件Nginx
Ingress Controller,会指定要关联的ingress class,和在k8s中注册自己controller的信息;
Ingress Class,会指定要关联的controller的信息;
Ingress,会指定要关联的Ingress Class的信息;
最终,Ingress Class作为“桥梁”,将Ingress和Ingress Controller关联起来。
查看Ingress模板
export out="--dry-run=client -o yaml"
kubectl create ing ngx-ing --rule="ngx.test/=ngx-svc:80" --class=ngx-ink $out
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ngx-ing
spec:
ingressClassName: ngx-ink
rules:
- host: ngx.test
http:
paths:
- path: /
pathType: Exact
backend:
service:
name: ngx-svc
port:
number: 80
Ingress Class模板
Ingress Class本身并没有什么实际的功能,只是联系Ingress和Ingress Controller的作用,定义非常简单,在“spec”里只有一个必需的字段“controller”,表示要使用哪个Ingress Controller,具体的名字就要看实现文档了。
比如,如果要用Nginx开发的Ingress Controller,那么就要用名字“nginx.org/ingress-controller”:
apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
name: ngx-ink
spec:
controller: nginx.org/ingress-controller
创建这两个对象
kubectl apply -f ngxIngress.yml
kubectl apply -f ngxIngressClass.yml
查看创建结果

kubectl describe ing ngx-ing 查看更详细的信息

安装Ingress Controller
官方文档: https://docs.nginx.com/nginx-ingress-controller/installation/installing-nic/installation-with-manifests/#clone-the-repository
安装步骤:
//先克隆文件
git clone https://github.com/nginxinc/kubernetes-ingress.git --branch <version_number>
cd kubernetes-ingress/deployments
//创建命名空间和服务帐户:
kubectl apply -f common/ns-and-sa.yaml
//为服务帐户创建集群角色和绑定:
kubectl apply -f rbac/rbac.yaml
//创建一个 ConfigMap 来自定义您的 NGINX 设置
kubectl apply -f common/nginx-config.yaml
//创建IngressClass资源。如果没有资源,NGINX Ingress Controller 将无法启动IngressClass。
kubectl apply -f common/ingress-class.yaml (由于我们之前已经创建过了,所以这一步不需要)
//核心自定义资源定义
kubectl apply -f common/crds/k8s.nginx.org_virtualservers.yaml
kubectl apply -f common/crds/k8s.nginx.org_virtualserverroutes.yaml
kubectl apply -f common/crds/k8s.nginx.org_transportservers.yaml
kubectl apply -f common/crds/k8s.nginx.org_policies.yaml
修改deployment/nginx-ingress.yaml文件
修改后,Ingress Controller的YAML大概是这个样子:
apiVersion: apps/v1
kind: Deployment
metadata:
name: ngx-kic-dep
namespace: nginx-ingress
spec:
replicas: 1
selector:
matchLabels:
app: ngx-kic-dep
template:
metadata:
labels:
app: ngx-kic-dep
...
spec:
containers:
- image: nginx/nginx-ingress:2.2-alpine
...
args:
- -ingress-class=ngx-ink
部署
kubectl apply -f deployment/nginx-ingress.yaml
查看部署结果
kubectl get deploy -n nginx-ingress
kubectl get pod -n nginx-ingress

因为Ingress Controller本身也是一个Pod,想要向外提供服务还是要依赖于Service对象。所以你至少还要再为它定义一个Service,使用NodePort或者LoadBalancer暴露端口,才能真正把集群的内外流量打通。
这里,我就用命令kubectl port-forward,它可以直接把本地的端口映射到Kubernetes集群的某个Pod里,在测试验证的时候非常方便。
下面这条命令就把本地的8080端口映射到了Ingress Controller Pod的80端口:
kubectl port-forward -n nginx-ingress ngx-kic-dep-8859b7b86-cplgp 8080:80 &

验证
可以修改 /etc/hosts 来手工添加域名解析,也可以使用 --resolve 参数,指定域名的解析规则,比如在这里我就把“ngx.test”强制解析到“127.0.0.1”,也就是被 kubectl port-forward 转发的本地地址:
curl --resolve ngx.test:8080:127.0.0.1 http://ngx.test:8080

K8s集群搭建WordPress网站
基本架构

第一步:搭建maria文件
- 用ConfigMap定义数据库的环境变量,有 DATABASE、USER、PASSWORD、ROOT_PASSWORD:
apiVersion: v1
kind: ConfigMap
metadata:
name: maria-cm
data:
DATABASE: 'db'
USER: 'mch'
PASSWORD: '0000'
ROOT_PASSWORD: '0000'
- 把MariaDB由Pod改成Deployment的方式,replicas设置成1个,用 envFrom把配置信息以环境变量的形式注入Pod
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: maria-dep
name: maria-dep
spec:
replicas: 1
selector:
matchLabels:
app: maria-dep
template:
metadata:
labels:
app: maria-dep
spec:
containers:
- image: mariadb:10
name: mariadb
ports:
- containerPort: 3306
envFrom:
- prefix: 'MARIADB_'
configMapRef:
name: maria-cm
- 为MariaDB定义一个Service对象,映射端口3306,让其他应用不再关心IP地址,直接用Service对象的名字来访问数据库服务:
apiVersion: v1
kind: Service
metadata:
labels:
app: maria-dep
name: maria-svc
spec:
ports:
- port: 3306 //对其他应用暴露的端口
protocol: TCP
targetPort: 3306
selector:
app: maria-dep
将三个YAML放在同一个文件里书写,对象之间用 — 分开,这样用 kubectl apply 就可以一次性创建
kubectl apply -f wp-maria.yml
第二步 部署WordPress
- 因为刚才创建了MariaDB的Service,所以在写ConfigMap配置的时候“HOST”就不应该是IP地址了,而应该是DNS域名,也就是Service的名字maria-svc,这点需要特别注意:
metadata:
name: wp-cm
data:
HOST: 'maria-svc'
USER: 'mch'
PASSWORD: '0000'
NAME: 'db'
- WordPress的Deployment写法和MariaDB也是一样的,replicas设置成2个,用字段“envFrom”配置环境变量:
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: wp-dep
name: wp-dep
spec:
replicas: 2
selector:
matchLabels:
app: wp-dep
template:
metadata:
labels:
app: wp-dep
spec:
containers:
- image: wordpress:5
name: wordpress
ports:
- containerPort: 80
envFrom:
- prefix: 'WORDPRESS_DB_'
configMapRef:
name: wp-cm
- 为WordPress创建Service对象,这里我使用了“NodePort”类型,并且手工指定了端口号“30088”(必须在30000~32767之间):
apiVersion: v1
kind: Service
metadata:
labels:
app: wp-dep
name: wp-svc
spec:
ports:
- name: http80
port: 80
protocol: TCP
targetPort: 80
nodePort: 30088
selector:
app: wp-dep
type: NodePort
同样写到一个文件中,kubectl apply -f wp-dep.yml 部署
访问测试
因为WordPress的Service对象是NodePort类型的,我们可以在集群的每个节点上访问WordPress服务。
比如一个节点的IP地址是“192.168.228.133”,在浏览器的地址栏里输入“http://192.168.228.133:30088”,其中的“30088”就是在Service里指定的节点端口号,然后就能够看到WordPress的安装界面了
第三步:搭建Ingress、IngressClass、IngressController
- 搭建IngressClass、名字就叫“wp-ink”
apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
name: wp-ink
spec:
controller: nginx.org/ingress-controller
- 搭建Ingress
//可以先用该命令生成模板
kubectl create ing wp-ing --rule="wp.test/=wp-svc:80" --class=wp-ink $out
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: wp-ing
spec:
ingressClassName: wp-ink
rules:
- host: wp.test
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: wp-svc
port:
number: 80
- 搭建IngressController(从模板中修改)
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: wp-kic-dep
namespace: nginx-ingress
spec:
replicas: 1
selector:
matchLabels:
app: wp-kic-dep
template:
metadata:
labels:
app: wp-kic-dep
spec:
serviceAccountName: nginx-ingress
# use host network
hostNetwork: true
dnsPolicy: ClusterFirstWithHostNet #可有可无
automountServiceAccountToken: true ## 改为true ,不然80端口没有权限
containers:
args:
- -ingress-class=wp-ink
...
---
## 增加、使用service
apiVersion: v1
kind: Service
metadata:
name: wp-kic-svc
namespace: nginx-ingress
spec:
ports:
- port: 80
protocol: TCP
targetPort: 80
nodePort: 30080
selector:
app: wp-kic-dep
type: NodePort
---
可以将Ingress、IngressClass 合成一个文件先创建,然后再创建IngressController
访问测试
Ingress使用的是HTTP路由规则,用IP地址访问是无效的,所以在集群外的主机上必须能够识别我们的“wp.test”域名,也就是说要把域名“wp.test”解析到Ingress Controller所在的节点上。
如果你用的是Mac,那就修改 /etc/hosts;如果你用的是Windows,就修改 C:\Windows\System32\Drivers\etc\hosts,添加一条解析规则就行:
cat /etc/hosts
192.168.228.133 wp.test
需要注意的是ip是IngressController 的pod所在节点的ip
有了域名解析,在浏览器里你就不必使用IP地址,直接用域名“wp.test”走Ingress Controller就能访问我们的WordPress网站了
PersistentVolume(PV)
-
PV实际上就是一些存储设备、文件系统,比如Ceph、GlusterFS、NFS,甚至是本地磁盘,管理它们已经超出了Kubernetes的能力范围,所以,一般会由系统管理员单独维护,然后再在Kubernetes里创建对应的PV。要注意的是,PV属于集群的系统资源,是和Node平级的一种对象,Pod对它没有管理权,只有使用权。
-
PersistentVolumeClaim,简称PVC,从名字上看比较好理解,就是用来向Kubernetes申请存储资源的。PVC是给Pod使用的对象,它相当于是Pod的代理,代表Pod向系统申请PV。一旦资源申请成功,Kubernetes就会把PV和PVC关联在一起,这个动作叫做“绑定”(bind)。但是,系统里的存储资源非常多,如果要PVC去直接遍历查找合适的PV也很麻烦,所以就要用到StorageClass。
-
StorageClass的作用有点像IngressClass,它抽象了特定类型的存储系统(比如Ceph、NFS),在PVC和PV之间充当“协调人”的角色,帮助PVC找到合适的PV。也就是说它可以简化Pod挂载“虚拟盘”的过程,让Pod看不到PV的实现细节。

1、PV的yaml示例:
apiVersion: v1
kind: PersistentVolume
metadata:
name: host-10m-pv
spec:
storageClassName: host-test
accessModes:
- ReadWriteOnce
capacity:
storage: 10Mi
hostPath:
path: /tmp/host-10m-pv/
storageClassName:对存储类型的抽象StorageClass。这个PV是我们手动管理的,名字可以任意起
accessModes:定义了存储设备的访问模式,简单来说就是虚拟盘的读写权限,和Linux的文件访问模式差不多,目前Kubernetes里有3种:
- ReadWriteOnce:存储卷可读可写,但只能被一个节点上的Pod挂载。
- ReadOnlyMany:存储卷只读不可写,可以被任意节点上的Pod多次挂载。
- ReadWriteMany:存储卷可读可写,也可以被任意节点上的Pod多次挂载。
显然,本地目录只能是在本机使用,所以这个PV使用了 ReadWriteOnce。
capacity:表示存储设备的容量,这里设置为10MB。
注意,Kubernetes里定义存储容量使用的是国际标准,我们日常习惯使用的KB/MB/GB的基数是1024,要写成Ki/Mi/Gi,一定要小心不要写错了,否则单位不一致实际容量就会对不上
hostPath:指定了存储卷的本地路径,也就是我们在节点上创建的目录。
2、PVC的yaml示例:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: host-5m-pvc
spec:
storageClassName: host-test
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Mi
示例要求使用一个5MB的存储设备,访问模式是 ReadWriteOnce,PVC里的 storageClassName、accessModes 和PV是一样的,但不会有字段 capacity,而是要用 resources.request 表示希望要有多大的容量
3、Kubernetes里使用PersistentVolume
定义好PV和PVC,就可以让Pod实现持久化存储了
首先需要用 kubectl apply 创建PV对象:
kubectl apply -f host-path-pv.yml
kubectl get pv

接下来创建PVC,申请存储资源:
kubectl apply -f host-path-pvc.yml
kubectl get pvc

4、如何为Pod挂载PersistentVolume
PV和PVC绑定好了,有了持久化存储,现在我们就可以为Pod挂载存储卷
因为我们用的是PVC,所以要在 volumes 里用字段 persistentVolumeClaim 指定PVC的名字。
Pod的YAML描述文件,把存储卷挂载到了Nginx容器的 /tmp 目录
apiVersion: v1
kind: Pod
metadata:
name: host-pvc-pod
spec:
volumes:
- name: host-pvc-vol
persistentVolumeClaim:
claimName: host-5m-pvc
containers:
- name: ngx-pvc-pod
image: nginx:alpine
ports:
- containerPort: 80
volumeMounts:
- name: host-pvc-vol
mountPath: /tmp
Pod和PVC/PV的关系图

创建这个Pod,查看它的状态:
kubectl apply -f host-path-pod.yml
kubectl get pod -o wide
PersistentVolume + NFS:使用网络共享存储
使用HostPath,存储卷只能在本机使用,而Kubernetes里的Pod经常会在集群里“漂移”,所以这种方式不是特别实用
要想让存储卷真正能被Pod任意挂载,不能限定在本地磁盘,而是要改成网络存储,这样Pod无论在哪里运行,只要知道IP地址或者域名,就可以通过网络通信访问存储设备。
网络存储是一个非常热门的应用领域,有很多知名的产品,比如AWS、Azure、Ceph,Kubernetes还专门定义了CSI(Container Storage Interface)规范,不过这些存储类型的安装、使用都比较复杂,在我们的实验环境里部署难度比较高。
我选择了相对来说比较简单的NFS系统(Network File System),以它为例讲解如何在Kubernetes里使用网络存储,以及静态存储卷和动态存储卷的概念。
NFS采用的是Client/Server架构,需要选定一台主机作为Server,安装NFS服务端;其他要使用存储的主机作为Client,安装NFS客户端工具。这里我就复用了Console作为服务器。
新的架构图如下:

1、安装NFS服务器
Console 安装 NFS服务端
sudo apt -y install nfs-kernel-server
给NFS指定一个存储位置,也就是网络共享目录。这儿创建临时目录 /tmp/nfs:
mkdir -p /tmp/nfs
接着需要配置NFS访问共享目录,修改 /etc/exports,指定目录名、允许访问的网段,还有权限等参数。注意目录名和IP地址要改成和自己的环境一致:(我的服务节点ip是:192.168.228.130)
/tmp/nfs 192.168.228.0/24(rw,sync,no_subtree_check,no_root_squash,insecure)
改好之后,需要用 exportfs -ra 通知NFS,让配置生效,再用 exportfs -v 验证效果:
sudo exportfs -ra
sudo exportfs -v

现在,可以使用 systemctl 来启动NFS服务器:
sudo systemctl start nfs-server
sudo systemctl enable nfs-server
sudo systemctl status nfs-server
还可以使用命令 showmount 来检查NFS的网络挂载情况:
showmount -e 127.0.0.1

2、安装NFS客户端
有了NFS服务器之后,为了让Kubernetes集群能够访问NFS存储服务,还需要在每个节点上都安装NFS客户端。
安装命令
sudo apt -y install nfs-common
同样,在节点上可以用 showmount 检查NFS能否正常挂载,注意IP地址要写成NFS服务器的地址,我在这里就是“192.168.228.130”:
showmount -e 192.168.228.130
现在尝试手动挂载一下NFS网络存储,先创建一个目录 /tmp/test 作为挂载点:
mkdir -p /tmp/test
然后用命令 mount 把NFS服务器的共享目录挂载到刚才创建的本地目录上:
sudo mount -t nfs 192.168.228.130:/tmp/nfs /tmp/test
最后测试一下,我们在 /tmp/test 里随便创建一个文件,比如 x.yml:
touch /tmp/test/x.yml
再回到NFS服务器,检查共享目录 /tmp/nfs,应该会看到也出现了一个同样的文件 x.yml,这就说明NFS安装成功了。之后集群里的任意节点,只要通过NFS客户端,就能把数据写入NFS服务器,实现网络存储。
3、使用NFS存储卷
Kubernetes配置好了NFS存储系统,就可以使用它来创建新的PV存储对象了。
注意,spec.nfs 里的IP地址一定要正确,路径一定要存在(事先创建好),否则Kubernetes按照PV的描述会无法挂载NFS共享目录,PV就会处于“pending”状态无法使用
apiVersion: v1
kind: PersistentVolume
metadata:
name: nfs-1g-pv
spec:
storageClassName: nfs
accessModes:
- ReadWriteMany # 多个节点同时访问一个共享目录
capacity:
storage: 1Gi #分配1Gi
nfs:
path: /tmp/nfs/1g-pv # NFS服务器的共享目录名
server: 192.168.228.130 # NFS服务器的IP地址
定义PVC对象
用 resources.request 来表示希望要有多大的容量
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nfs-static-pvc
spec:
storageClassName: nfs
accessModes:
- ReadWriteMany
resources:
requests:
storage: 1Gi
定义一个Pod
把PVC挂载成它的一个volume,用 persistentVolumeClaim 指定PVC的名字就可以了
apiVersion: v1
kind: Pod
metadata:
name: nfs-static-pod
spec:
volumes:
- name: nfs-pvc-vol
persistentVolumeClaim:
claimName: nfs-static-pvc
containers:
- name: nfs-pvc-test
image: nginx:alpine
ports:
- containerPort: 80
volumeMounts:
- name: nfs-pvc-vol
mountPath: /tmp
验证
操作NFS共享目录,再看一下NFS服务器的 /tmp/nfs/1g-pv 目录,你就会发现Pod里创建的文件确实写入了共享目录

Pod、PVC、PV和NFS存储的关系可以用下图来形象地表示

4、部署NFS Provisoner
有了NFS这样的网络存储系统,Kubernetes里的数据持久化问题并没有完全解决
没有完全解决:因为PV还是需要人工管理,必须要由系统管理员手动维护各种存储设备,再根据开发需求逐个创建PV,而且PV的大小也很难精确控制,容易出现空间不足或者空间浪费的情况。如果是在一个大集群里,每天可能会有几百几千个应用需要PV存储,如果仍然用人力来管理分配存储,管理员很可能会忙得焦头烂额,导致分配存储的工作大量积压。
如何自动化创建PV?
这个在Kubernetes里就是“动态存储卷”的概念,它可以用StorageClass绑定一个Provisioner对象,而这个Provisioner就是一个能够自动管理存储、创建PV的应用,代替了原来系统管理员的手工劳动。
有了“动态存储卷”的概念,前面的手工创建的PV就可以称为“静态存储卷”。
目前,Kubernetes里每类存储设备都有相应的Provisioner对象,对于NFS来说,它的Provisioner就是“NFS subdir external provisioner”,你可以在GitHub上找到这个项目(https://github.com/kubernetes-sigs/nfs-subdir-external-provisioner)。
NFS Provisioner也是以Pod的形式运行在Kubernetes里的,在GitHub的 deploy 目录里是部署它所需的YAML文件,一共有三个,分别是rbac.yaml、class.yaml和deployment.yaml。
需要修改两个文件:
第一个是rbac.yaml,它使用的是默认的 default 名字空间,应该把它改成其他的名字空间,避免与普通应用混在一起,你可以用“查找替换”的方式把它统一改成 kube-system。
第二个是deployment.yaml,它要修改的地方比较多。首先要把名字空间改成和rbac.yaml一样,比如是 kube-system,然后重点要修改 volumes 和 env 里的IP地址和共享目录名,必须和集群里的NFS服务器配置一样。
deployment.yaml的镜像仓库用的是gcr.io,把镜像的名字由原来的“k8s.gcr.io/sig-storage/nfs-subdir-external-provisioner:v4.0.2”改成“chronolaw/nfs-subdir-external-provisioner:v4.0.2”
spec:
template:
spec:
serviceAccountName: nfs-client-provisioner
containers:
...
env:
- name: PROVISIONER_NAME
value: k8s-sigs.io/nfs-subdir-external-provisioner
- name: NFS_SERVER
value: 192.168.228.130 #改IP地址
- name: NFS_PATH
value: /tmp/nfs #改共享目录名
volumes:
- name: nfs-client-root
nfs:
server: 192.168.228.130 #改IP地址
Path: /tmp/nfs #改共享目录名
把这两个YAML修改好之后,在Kubernetes里创建NFS Provisioner
kubectl apply -f rbac.yaml
kubectl apply -f class.yaml
kubectl apply -f deployment.yaml


5、使用NFS动态存储卷
比起静态存储卷,动态存储卷的用法简单很多。因为有了Provisioner,我们就不再需要手工定义PV对象了,只需要在PVC里指定StorageClass对象,它再关联到Provisioner。
NFS默认的StorageClass定义:
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: nfs-client
provisioner: k8s-sigs.io/nfs-subdir-external-provisioner
parameters:
archiveOnDelete: "false"
provisioner它指定了应该使用哪个Provisioner。
parameters 是调节Provisioner运行的参数,这里的 archiveOnDelete: “false” 就是自动回收存储空间。
定义一个PVC
向系统申请10MB的存储空间,使用的StorageClass是默认的 nfs-client
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nfs-dyn-10m-pvc
spec:
storageClassName: nfs-client
accessModes:
- ReadWriteMany
resources:
requests:
storage: 10Mi
在Pod里用 volumes 和 volumeMounts 挂载
挂载完成后,Kubernetes就会自动找到NFS Provisioner,在NFS的共享目录上创建出合适的PV对象
apiVersion: v1
kind: Pod
metadata:
name: nfs-dyn-pod
spec:
volumes:
- name: nfs-dyn-10m-vol
persistentVolumeClaim:
claimName: nfs-dyn-10m-pvc
containers:
- name: nfs-dyn-test
image: nginx:alpine
ports:
- containerPort: 80
volumeMounts:
- name: nfs-dyn-10m-vol
mountPath: /tmp
使用 kubectl apply 创建好PVC和Pod,查看一下集群里的PV状态:

从截图可以看到,虽然我们没有直接定义PV对象,但由于有NFS Provisioner,它就自动创建一个PV,大小刚好是在PVC里申请的10MB。
如果你这个时候再去NFS服务器上查看共享目录,也会发现多出了一个目录,名字与这个自动创建的PV一样,但加上了名字空间和PVC的前缀:

Pod、PVC、StorageClass和Provisioner的关系图

StatefulSet:管理有状态的应用
无状态应用: 只是有的应用的状态信息不是很重要,即使不恢复状态也能够正常运行
有状态应用: 一些应用,运行状态信息就很重要了,如果因为重启而丢失了状态是绝对无法接受的
1、一个使用Redis的StatefulSet的对象
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: redis-sts
spec:
serviceName: redis-svc
replicas: 2
selector:
matchLabels:
app: redis-sts
template:
metadata:
labels:
app: redis-sts
spec:
containers:
- image: redis:5-alpine
name: redis
ports:
- containerPort: 6379
发现: 相比Deployment,文件里除了 kind 必须是“StatefulSet”,在 spec 里还多出了一个“serviceName”字段
2、在Kubernetes里使用StatefulSet
用 kubectl apply 创建StatefulSet对象
kubectl apply -f redis-sts.yml
kubectl get sts
kubectl get pod


(1)解决了“有状态应用”的第一个问题:启动顺序
StatefulSet所管理的Pod不再是随机的名字,而是有了顺序编号,从0开始分别被命名为 redis-sts-0、redis-sts-1
Kubernetes也会按照这个顺序依次创建(0号比1号的AGE要长一点)
(2)解决了“有状态应用”的第二个问题:依赖关系
有了启动的先后顺序,应用通过hostname,进而确定互相之间的依赖关系呢
hostname就是每个Pod里的主机名,让我们再用 kubectl exec 登录Pod内部看看:
kubectl exec -it redis-sts-0 -- sh

(3)解决了“有状态应用”的第三个问题:网络标识
解决网络标识,需要用到Service对象
定义Service对象
apiVersion: v1
kind: Service
metadata:
name: redis-svc
spec:
selector:
app: redis-sts
ports:
- port: 6379
protocol: TCP
targetPort: 6379
kubectl apply 创建这个对象,查看如下

- Service自己会有一个域名,格式是“对象名.名字空间”
- 每个Pod也会有一个域名,格式是“IP地址.名字空间”。但因为IP地址不稳定,所以Pod的域名并不实用,一般我们会使用稳定的Service域名。
StatefulSet的奥秘就在它的域名上,当我们把Service对象应用于StatefulSet的时候,情况就不一样了
Service发现这些Pod不是一般的应用,而是有状态应用,需要有稳定的网络标识,所以就会为Pod再多创建出一个新的域名,格式是“Pod名.服务名.名字空间.svc.cluster.local”。当然,这个域名也可以简写成“Pod名.服务名”。
用 kubectl exec 进入Pod内部,用ping命令来验证一下:
kubectl exec -it redis-sts-0 -- sh

虽然Pod的IP地址可能会变,但这个有编号的域名由Service对象维护,是稳定不变的
关于Service,有一点值得再多提一下。
Service原本的目的是负载均衡,应该由它在Pod前面来转发流量,但是对StatefulSet来说,这项功能反而是不必要的,因为Pod已经有了稳定的域名,外界访问服务就不应该再通过Service这一层了。所以,从安全和节约系统资源的角度考虑,我们可以在Service里添加一个字段 clusterIP: None ,告诉Kubernetes不必再为这个对象分配IP地址。
StatefulSet与Service对象关系图

3、如何实现StatefulSet的数据持久化
Kubernetes为StatefulSet专门定义了一个字段“volumeClaimTemplates”,直接把PVC定义嵌入StatefulSet的YAML文件里。这样能保证创建StatefulSet的同时,就会为每个Pod自动创建PVC,让StatefulSet的可用性更高
上面的Redis StatefulSet对象稍微改造一下,加上持久化存储功能:
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: redis-pv-sts
spec:
serviceName: redis-pv-svc
volumeClaimTemplates:
- metadata:
name: redis-100m-pvc
spec:
storageClassName: nfs-client
accessModes:
- ReadWriteMany
resources:
requests:
storage: 100Mi
replicas: 2
selector:
matchLabels:
app: redis-pv-sts
template:
metadata:
labels:
app: redis-pv-sts
spec:
containers:
- image: redis:5-alpine
name: redis
ports:
- containerPort: 6379
volumeMounts:
- name: redis-100m-pvc
mountPath: /data
这个StatefulSet对象完整的关系图

滚动更新:应用平滑的升级降级
1、滚动更新
由Deployment控制的两个同步进行的“应用伸缩”操作,老版本缩容到0,同时新版本扩容到指定值,是一个“此消彼长”的过程

2、管理应用更新
(1)查看更新历史使用的命令是 kubectl rollout history:
kubectl rollout history deploy ngx-dep

在命令后加上参数 --revision 来查看每个版本的详细信息,包括标签、镜像名、环境变量、存储卷等等
kubectl rollout history deploy --revision=2

(2) 回退到上一个版本
可以使用命令 kubectl rollout undo,也可以加上参数 --to-revision 回退到任意一个历史版本:
kubectl rollout undo deploy ngx-dep
版本降级图解

3、Kubernetes添加更新描述
kubectl rollout history 的版本列表好像有点太简单了、只有一个版本更新序号,而另一列 CHANGE-CAUSE 为什么总是显示成 ,能不能像Git一样,每次更新也加上说明信息呢?

做法也很简单,我们只需要在Deployment的 metadata 里加上一个新的字段 annotations。
- annotations 添加的信息一般是给Kubernetes内部的各种对象使用的,有点像是“扩展属性”;
- labels 主要面对的是Kubernetes外部的用户,用来筛选、过滤对象的。
简单的比喻来说呢,annotations 就是包装盒里的产品说明书,而 labels 是包装盒外的标签贴纸
应用保障:让Pod运行得更健康
1、资源申请
CPU、内存与存储卷有明显的不同,因为它是直接“内置”在系统里的,不像硬盘那样需要“外挂”,所以申请和管理的过程也就会简单很多
yaml示例
apiVersion: v1
kind: Pod
metadata:
name: ngx-pod-resources
spec:
containers:
- image: nginx:alpine
name: ngx
resources:
requests:
cpu: 10m # 申请的是1%的CPU时间
memory: 100Mi # 100MB的内存
limits:
cpu: 20m
memory: 200Mi
- “requests”,意思是容器要申请的资源,也就是说要求Kubernetes在创建Pod的时候必须分配这里列出的资源,否则容器就无法运行。
- “limits”,意思是容器使用资源的上限,不能超过设定值,否则就有可能被强制停止运行。
内存的写法和磁盘容量一样,使用 Ki、Mi、Gi 来表示 KB、MB、GB,比如 512Ki、100Mi、0.5Gi 等。
Kubernetes允许容器精细分割CPU,即可以1个、2个地完整使用CPU,也可以用小数0.1、0.2的方式来部分使用CPU。这其实是效仿了UNIX“时间片”的用法,意思是进程最多可以占用多少CPU时间。
不过CPU时间也不能无限分割,Kubernetes里CPU的最小使用单位是0.001,为了方便表示用了一个特别的单位 m,也就是“milli”“毫”的意思,比如说500m就相当于0.5。
如果Pod不写 resources 字段,Kubernetes会如何处理呢? 意味着Pod对运行的资源要求“既没有下限,也没有上限”
测试一下: 先删除Pod的资源限制 resources.limits,把 resources.request.cpu 改成比较极端的“10”,也就是要求10个CPU:
...
resources:
requests:
cpu: 10
使用 kubectl apply 创建这个Pod,你可能也会发现,虽然我们的Kubernetes集群里只有3个CPU,但Pod也能创建成功
如果再用 kubectl get pod 去查看的话,就会发现它处于“Pending”状态,实际上并没有真正被调度运行:

2、容器状态探针
Kubernetes为检查应用状态定义了三种探针,它们分别对应容器不同的状态:
- Startup,启动探针,用来检查应用是否已经启动成功,适合那些有大量初始化工作要做,启动很慢的应用。
- Liveness,存活探针,用来检查应用是否正常运行,是否存在死锁、死循环。
- Readiness,就绪探针,用来检查应用是否可以接收流量,是否能够对外提供服务。
三种探针是递进的关系,只有到最后的Readiness状态才是一个容器最健康可用的状态,三种状态关系图

如何使用状态和探针来管理容器的呢?
如果一个Pod里的容器配置了探针,Kubernetes在启动容器后就会不断地调用探针来检查容器的状态:
- 如果Startup探针失败,Kubernetes会认为容器没有正常启动,就会尝试反复重启,当然其后面的Liveness探针和Readiness探针也不会启动。
- 如果Liveness探针失败,Kubernetes就会认为容器发生了异常,也会重启容器。
- 如果Readiness探针失败,Kubernetes会认为容器虽然在运行,但内部有错误,不能正常提供服务,就会把容器从Service对象的负载均衡集合中排除,不会给它分配流量。
3、如何使用容器状态探针
Pod的YAML描述文件里定义探针有startupProbe、livenessProbe、readinessProb三种
(1)探针的配置方式都是一样的,关键字段有下面几个:
- periodSeconds,执行探测动作的时间间隔,默认是10秒探测一次。
- timeoutSeconds,探测动作的超时时间,如果超时就认为探测失败,默认是1秒。
- successThreshold,连续几次探测成功才认为是正常,对于startupProbe和livenessProbe来说它只能是1。
- failureThreshold,连续探测失败几次才认为是真正发生了异常,默认是3次。
(2)探测方式,Kubernetes支持3种:Shell、TCP Socket、HTTP GET,它们也需要在探针里配置:
- exec,执行一个Linux命令,比如ps、cat等等,和container的command字段很类似。
- tcpSocket,使用TCP协议尝试连接容器的指定端口。
- httpGet,连接端口并发送HTTP GET请求。
(3) 要使用这些探针,必须要在开发应用时预留出“检查口”,这样Kubernetes才能调用探针获取信息。
- 以Nginx作为示例,用ConfigMap编写一个配置文件:
apiVersion: v1
kind: ConfigMap
metadata:
name: ngx-conf
data:
default.conf: |
server {
listen 80;
location = /ready {
return 200 'I am ready';
}
}
在这个配置文件里,启用了80端口,然后用 location 指令定义了HTTP路径 /ready,它作为对外暴露的“检查口”,用来检测就绪状态,返回简单的200状态码和一个字符串表示工作正常。
- Pod里三种探针的具体定义
apiVersion: v1
kind: Pod
metadata:
name: ngx-pod-probe
spec:
volumes:
- name: ngx-conf-vol
configMap:
name: ngx-conf
containers:
- image: nginx:alpine
name: ngx
ports:
- containerPort: 80
volumeMounts:
- mountPath: /etc/nginx/conf.d
name: ngx-conf-vol
startupProbe:
periodSeconds: 1
exec:
command: ["cat", "/var/run/nginx.pid"]
livenessProbe:
periodSeconds: 10
tcpSocket:
port: 80
readinessProbe:
periodSeconds: 5
httpGet:
path: /ready
port: 80
StartupProbe使用了Shell方式,使用 cat 命令检查Nginx存在磁盘上的进程号文件(/var/run/nginx.pid),如果存在就认为是启动成功,它的执行频率是每秒探测一次。
LivenessProbe使用了TCP Socket方式,尝试连接Nginx的80端口,每10秒探测一次。
ReadinessProbe使用的是HTTP GET方式,访问容器的 /ready 路径,每5秒发一次请求。
用 kubectl apply 创建这个Pod,然后查看它的状态:

用 kubectl logs 来查看Nginx的访问日志,里面会记录HTTP GET探针的执行情况

集群管理:用名字空间分隔系统资源
1、使用名字空间
创建一个名字空间
kubectl create ns test-ns
kubectl get ns
想要把一个对象放入特定的名字空间,需要在它的 metadata 里添加一个 namespace 字段,比如要在“test-ns”里创建一个简单的Nginx Pod,就要这样写:
apiVersion: v1
kind: Pod
metadata:
name: ngx
namespace: test-ns
spec:
containers:
- image: nginx:alpine
name: ngx
想要操作其他名字空间的对象必须要用 -n 参数明确指定:
kubectl get pod -n test-ns
因为名字空间里的对象都从属于名字空间,所以一旦名字空间被删除,它里面的所有对象也都会消失。删除刚才创建的名字空间“test-ns”:
kubectl delete ns test-ns
就会发现删除名字空间后,它里面的Pod也会无影无踪了。
2、什么是资源配额
名字空间可以像管理容器一样,给名字空间设定配额
名字空间的资源配额需要使用一个专门的API对象,叫做 ResourceQuota,简称是 quota
使用命令 kubectl create 创建一个它的样板文件:
export out="--dry-run=client -o yaml"
kubectl create quota dev-qt $out
因为资源配额对象必须依附在某个名字空间上,所以在它的 metadata 字段里必须明确写出 namespace(否则就会应用到default名字空间)。
下面我们先创建一个名字空间“dev-ns”,再创建一个资源配额对象“dev-qt”:
apiVersion: v1
kind: Namespace
metadata:
name: dev-ns
---
apiVersion: v1
kind: ResourceQuota
metadata:
name: dev-qt
namespace: dev-ns
spec:
... ...
ResourceQuota对象的使用方式比较灵活,既可以限制整个名字空间的配额,也可以只限制某些类型的对象(使用scopeSelector),此处看第一种
在ResourceQuota里设置各类资源配额,简单地归类:
- CPU和内存配额,使用 request.、limits.,这是和容器资源限制是一样的。
- 存储容量配额,使 requests.storage 限制的是PVC的存储总量,也可以用
- persistentvolumeclaims 限制PVC的个数。
- 核心对象配额,使用对象的名字(英语复数形式),比如 pods、configmaps、secrets、services。
- 其他API对象配额,使用 count/name.group 的形式,比如 count/jobs.batch、count/deployments.apps。
下面的这个YAML就是一个比较完整的资源配额对象
apiVersion: v1
kind: ResourceQuota
metadata:
name: dev-qt
namespace: dev-ns
spec:
hard:
requests.cpu: 10
requests.memory: 10Gi
limits.cpu: 10
limits.memory: 20Gi
requests.storage: 100Gi
persistentvolumeclaims: 100
pods: 100
configmaps: 100
secrets: 100
services: 10
count/jobs.batch: 1
count/cronjobs.batch: 1
count/deployments.apps: 1
名字空间加上的全局资源配额解释:
- 所有Pod的需求总量最多是10个CPU和10GB的内存,上限总量是10个CPU和20GB的内存。
- 只能创建100个PVC对象,使用100GB的持久化存储空间。
- 只能创建100个Pod,100个ConfigMap,100个Secret,10个Service。
- 只能创建1个Job,1个CronJob,1个Deployment。
3、如何使用资源配额
用 kubectl apply 创建这个资源配额对象
kubectl apply -f quota-ns.yml
//查看
kubectl get quota -n dev-ns
kubectl describe quota -n dev-ns
试验:
在名字空间里运行两个busybox Job,要加上 -n 参数:
kubectl create job echo1 -n dev-ns --image=busybox -- echo hello
kubectl create job echo2 -n dev-ns --image=busybox -- echo hello

ResourceQuota限制了名字空间里最多只能有一个Job,所以创建第二个Job对象时会失败,提示超出了资源配额。
4、默认资源配额
(1)在名字空间加上了资源配额限制之后,它会有一个合理但比较“烦人”的约束:要求所有在里面运行的Pod都必须用字段 resources 声明资源需求,否则就无法创建。
比如说,现在用命令 kubectl run 创建一个Pod:
kubectl run ngx --image=nginx:alpine -n dev-ns

提示说不满足配额要求。
为了保证名字空间的资源总量可管可控,Kubernetes就只能拒绝创建这样的Pod了
(2)如何让Kubernetes自动为Pod加上资源限制呢?也就是说给个默认值,省去反复设置配额?
辅助对象了—— LimitRange,简称是 limits,它能为API对象添加默认的资源配额限制
用命令 kubectl explain limits 来查看它的YAML字段详细说明
- spec.limits 是它的核心属性,描述了默认的资源限制。
- type 是要限制的对象类型,可以是 Container、Pod、PersistentVolumeClaim。
- default 是默认的资源上限,对应容器里的 resources.limits,只适用于 Container。
- defaultRequest 默认申请的资源,对应容器里的 resources.requests,同样也只适用于 Container。
- max、min 是对象能使用的资源的最大最小值。
LimitRange对象的YAML示范
apiVersion: v1
kind: LimitRange
metadata:
name: dev-limits
namespace: dev-ns
spec:
limits:
- type: Container
defaultRequest:
cpu: 200m
memory: 50Mi
default:
cpu: 500m
memory: 100Mi
- type: Pod
max:
cpu: 800m
memory: 200Mi
它设置了每个容器默认申请0.2的CPU和50MB内存,容器的资源上限是0.5的CPU和100MB内存,每个Pod的最大使用量是0.8的CPU和200MB内存。
使用 kubectl apply 创建LimitRange之后,再用 kubectl describe 就可以看到它的状态:
kubectl describe limitranges -n dev-ns

现在就可以直接创建Pod了,不用编写 resources 字段,再运行之前的 kubectl run 命令:
kubectl run ngx --image=nginx:alpine -n dev-ns
有了这个默认的资源配额作为“保底”,这次就没有报错,Pod顺利创建成功,用 kubectl describe 查看Pod的状态,也可以看到LimitRange为它自动加上的资源配额:

系统监控:使用Metrics Server和Prometheus
Kubernetes为集群提供的两种系统级别的监控项目:Metrics Server和Prometheus
Metrics Server
监测系统指标
Linux系统有一个命令 top 能够实时显示当前系统的CPU和内存利用率,它是性能分析和调优的基本工具,非常有用。
Kubernetes也提供了类似的命令,就是 kubectl top,不过默认情况下这个命令不会生效,必须要安装一个插件Metrics Server才可以。
安装
Metrics Server的项目网址(https://github.com/kubernetes-sigs/metrics-server)
Metrics Server的所有依赖都放在了一个YAML描述文件里,可以使用wget或者curl下载:
wget https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml
kubectl apply 创建对象之前,有两个准备工作要做
第一个工作是修改YAML文件,需要在Metrics Server的Deployment对象里,加上一个额外的运行参数 --kubelet-insecure-tls,也就是这样:
apiVersion: apps/v1
kind: Deployment
metadata:
name: metrics-server
namespace: kube-system
spec:
... ...
template:
spec:
containers:
- args:
- --kubelet-insecure-tls
... ...
因为Metrics Server默认使用TLS协议,要验证证书才能与kubelet实现安全通信,而我们的实验环境里没有这个必要,加上这个参数可以让我们的部署工作简单很多(生产环境里就要慎用)。
第二个工作,是预先下载Metrics Server的镜像。
Metrics Server的镜像仓库用的是gcr.io,下载很困难。好在它也有国内的镜像网站,可以下载后再改名,然后把镜像加载到集群里的节点上。
这里我给出一段Shell脚本代码,供你参考:
repo=registry.aliyuncs.com/google_containers
name=k8s.gcr.io/metrics-server/metrics-server:v0.6.1
src_name=metrics-server:v0.6.1
docker pull $repo/$src_name
docker tag $repo/$src_name $name
docker rmi $repo/$src_name
两个准备工作都完成之后,我们就可以使用YAML部署Metrics Server了:
kubectl apply -f components.yaml
Metrics Server属于名字空间“kube-system”,可以用 kubectl get pod 加上 -n 参数查看它是否正常运行:
kubectl get pod -n kube-system

有了Metrics Server插件,就可以使用命令 kubectl top 来查看Kubernetes集群当前的资源状态了。它有两个子命令,node 查看节点的资源使用率,pod 查看Pod的资源使用率。
kubectl top node
kubectl top pod -n kube-system

从这个截图里可以看到:
- 集群里两个节点CPU使用率都不高,分别是8%和4%,但内存用的很多,master节点用了差不多一半(48%),而worker节点几乎用满了(89%)。
- 名字空间“kube-system”里有很多Pod,其中apiserver最消耗资源,使用了75m的CPU和363MB的内存。’
HorizontalPodAutoscaler(水平自动伸缩)
“HorizontalPodAutoscaler”,简称是“hpa”。顾名思义,它是专门用来自动伸缩Pod数量的对象,适用于Deployment和StatefulSet,但不能用于DaemonSet(原因很明显吧)。
HorizontalPodAutoscaler的能力完全基于Metrics Server,它从Metrics Server获取当前应用的运行指标,主要是CPU使用率,再依据预定的策略增加或者减少Pod的数量。
使用HorizontalPodAutoscaler,首先要定义Deployment和Service,创建一个Nginx应用,作为自动伸缩的目标对象:
apiVersion: apps/v1
kind: Deployment
metadata:
name: ngx-hpa-dep
spec:
replicas: 1
selector:
matchLabels:
app: ngx-hpa-dep
template:
metadata:
labels:
app: ngx-hpa-dep
spec:
containers:
- image: nginx:alpine
name: nginx
ports:
- containerPort: 80
resources:
requests:
cpu: 50m
memory: 10Mi
limits:
cpu: 100m
memory: 20Mi
---
apiVersion: v1
kind: Service
metadata:
name: ngx-hpa-svc
spec:
ports:
- port: 80
protocol: TCP
targetPort: 80
selector:
app: ngx-hpa-dep
在这个YAML里只部署了一个Nginx实例,名字是 ngx-hpa-dep。注意在它的 spec 里一定要用 resources 字段写清楚资源配额,否则HorizontalPodAutoscaler会无法获取Pod的指标,也就无法实现自动化扩缩容。
接下来我们要用命令 kubectl autoscale 创建一个HorizontalPodAutoscaler的样板YAML文件,它有三个参数:
- min,Pod数量的最小值,也就是缩容的下限。
- max,Pod数量的最大值,也就是扩容的上限。
- cpu-percent,CPU使用率指标,当大于这个值时扩容,小于这个值时缩容。
现在就来为刚才的Nginx应用创建HorizontalPodAutoscaler,指定Pod数量最少2个,最多10个,CPU使用率指标设置的小一点,5%,方便观察扩容现象:
export out="--dry-run=client -o yaml" # 定义Shell变量
kubectl autoscale deploy ngx-hpa-dep --min=2 --max=10 --cpu-percent=5 $out
得到的YAML描述文件就是这样:
apiVersion: autoscaling/v1
kind: HorizontalPodAutoscaler
metadata:
name: ngx-hpa
spec:
maxReplicas: 10
minReplicas: 2
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: ngx-hpa-dep
targetCPUUtilizationPercentage: 5
使用命令 kubectl apply 创建这个HorizontalPodAutoscaler后,它会发现Deployment里的实例只有1个,不符合min定义的下限的要求,就先扩容到2个:

从这张截图里你可以看到,HorizontalPodAutoscaler会根据YAML里的描述,找到要管理的Deployment,把Pod数量调整成2个,再通过Metrics Server不断地监测Pod的CPU使用率。
下面我们来给Nginx加上压力流量,运行一个测试Pod,使用的镜像是“httpd:alpine”,它里面有HTTP性能测试工具ab(Apache Bench):
kubectl run test -it --image=httpd:alpine -- sh

然后我们向Nginx发送一百万个请求,持续1分钟,再用 kubectl get hpa 来观察HorizontalPodAutoscaler的运行状况:
ab -c 10 -t 60 -n 1000000 'http://ngx-hpa-svc/'

因为Metrics Server大约每15秒采集一次数据,所以HorizontalPodAutoscaler的自动化扩容和缩容也是按照这个时间点来逐步处理的。
当它发现目标的CPU使用率超过了预定的5%后,就会以2的倍数开始扩容,一直到数量上限,然后持续监控一段时间,如果CPU使用率回落,就会再缩容到最小值。
Prometheus
1、Prometheus官方的架构图

Prometheus系统的核心是它的Server,里面有一个时序数据库TSDB,用来存储监控数据,另一个组件Retrieval使用拉取(Pull)的方式从各个目标收集数据,再通过HTTP Server把这些数据交给外界使用。
在Prometheus Server之外还有三个重要的组件:
- Push Gateway,用来适配一些特殊的监控目标,把默认的Pull模式转变为Push模式。
- Alert Manager,告警中心,预先设定规则,发现问题时就通过邮件等方式告警。
- Grafana是图形化界面,可以定制大量直观的监控仪表盘。
2、部署
在Kubernetes实验环境里部署Prometheus,选用了“kube-prometheus”项目(https://github.com/prometheus-operator/kube-prometheus/)
先下载kube-prometheus的源码包,当前的最新版本是0.11:
wget https://github.com/prometheus-operator/kube-prometheus/archive/refs/tags/v0.11.0.tar.gz
解压缩后,Prometheus部署相关的YAML文件都在 manifests 目录里,有近100个
要做一些准备工作,才能够安装Prometheus
第一步,是修改 prometheus-service.yaml、grafana-service.yaml。这两个文件定义了Prometheus和Grafana服务对象,我们可以给它们添加 type: NodePort,这样就可以直接通过节点的IP地址访问(当然也可以配置成Ingress)。
第二步,是修改 kubeStateMetrics-deployment.yaml、prometheusAdapter-deployment.yaml,因为它们里面有两个存放在gcr.io的镜像,必须解决下载镜像的问题。
需要修改镜像名字,把前缀都改成 chronolaw
image: k8s.gcr.io/kube-state-metrics/kube-state-metrics:v2.5.0
image: k8s.gcr.io/prometheus-adapter/prometheus-adapter:v0.9.1
image: chronolaw/kube-state-metrics:v2.5.0
image: chronolaw/prometheus-adapter:v0.9.1
要执行两个 kubectl create 命令来部署Prometheus,先是 manifests/setup 目录,创建名字空间等基本对象,然后才是 manifests 目录:
kubectl create -f manifests/setup
kubectl create -f manifests
Prometheus的对象都在名字空间“monitoring”里,创建之后可以用 kubectl get 来查看状态:

这些Pod都运行正常,我们再来看看它对外的服务端口:

前面修改了Grafana和Prometheus的Service对象,所以这两个服务就在节点上开了端口,Grafana是“30358”,Prometheus有两个端口,其中“9090”对应的“30827”是Web端口。
Prometheus界面: ip:30827
Grafana界面:ip:30358


被折叠的 条评论
为什么被折叠?



