什么是 Kubernetes
Kubernetes 是一个可移植、可扩展的开源平台,用于管理容器化的工作负载和服务,可促进声明式配置和自动化。 Kubernetes 拥有一个庞大且快速增长的生态,其服务、支持和工具的使用范围相当广泛。
Kubernetes 这个名字源于希腊语,意为“舵手”或“飞行员”。k8s 这个缩写是因为 k 和 s 之间有八个字符的关系。 Google 在 2014 年开源了 Kubernetes 项目。 Kubernetes 建立在 Google 大规模运行生产工作负载十几年经验的基础上, 结合了社区中最优秀的想法和实践。
Kubernetes 提供了一个可弹性运行分布式系统的框架,它可以为你提供:
-
服务发现和负载均衡
Kubernetes 可以使用 DNS 名称或自己的 IP 地址来暴露容器。 如果进入容器的流量很大, Kubernetes 可以负载均衡并分配网络流量,从而使部署稳定。
-
存储编排
Kubernetes 允许你自动挂载你选择的存储系统,例如本地存储、公共云提供商等。
-
自动部署和回滚
你可以使用 Kubernetes 描述已部署容器的所需状态, 它可以以受控的速率将实际状态更改为期望状态。 例如,你可以自动化 Kubernetes 来为你的部署创建新容器, 删除现有容器并将它们的所有资源用于新容器。
-
自动完成装箱计算
你为 Kubernetes 提供许多节点组成的集群,在这个集群上运行容器化的任务。 你告诉 Kubernetes 每个容器需要多少 CPU 和内存 (RAM)。 Kubernetes 可以将这些容器按实际情况调度到你的节点上,以最佳方式利用你的资源。
-
自我修复
Kubernetes 将重新启动失败的容器、替换容器、杀死不响应用户定义的运行状况检查的容器, 并且在准备好服务之前不将其通告给客户端。
-
密钥与配置管理
Kubernetes 允许你存储和管理敏感信息,例如密码、OAuth 令牌和 SSH 密钥。 你可以在不重建容器镜像的情况下部署和更新密钥和应用程序配置,也无需在堆栈配置中暴露密钥。
-
批处理执行
除了服务外,Kubernetes 还可以管理你的批处理和 CI(持续集成)工作负载,如有需要,可以替换失败的容器。
-
水平扩缩
使用简单的命令、用户界面或根据 CPU 使用率自动对你的应用进行扩缩。
......
工作方式
Kubernetes Cluster = N Master Node + N Worker Node = N 个主节点 + N 个工作节点(N >= 1)
组件架构
控制平面组件(Control Plane Components)
控制平面组件会为集群做出全局决策,比如资源的调度。 以及检测和响应集群事件(例如:当不满足部署的 replicas
字段时,要启动新的 Pod)。
控制平面组件可以在集群中的任何节点上运行。 然而,为了简单起见,设置脚本通常会在同一个计算机上启动所有控制平面组件, 并且不会在此计算机上运行用户容器。 请参阅 使用 kubeadm 构建高可用性集群 中关于跨多机器控制平面设置的示例。
kube-apiserver
API 服务器是 Kubernetes 控制平面 的组件, 该组件负责公开了 Kubernetes API,负责处理接受请求的工作。 API 服务器是 Kubernetes 控制平面的前端。
Kubernetes API 服务器的主要实现是 kube-apiserver。 kube-apiserver
设计上考虑了水平扩缩,也就是说,它可通过部署多个实例来进行扩缩。 你可以运行 kube-apiserver
的多个实例,并在这些实例之间平衡流量。
etcd
ectd 是兼具一致性且高可用的键值数据库,用作 Kubernetes 所有集群数据的后台数据库。
如果你的 Kubernetes 集群使用 etcd 作为其后台数据库, 请确保你针对这些数据有一份 备份 计划。
你可以在 官方文档 中找到有关 etcd 的深入知识。
kube-scheduler
kube-scheduler
是控制平面的组件, 负责监视新创建的、未指定运行节点(node)的 Pods, 并选择节点来让 Pod 在上面运行。
调度决策考虑的因素包括单个 Pod 及 Pods 集合的资源需求、软硬件及策略约束、 亲和性及反亲和性规范、数据位置、工作负载间的干扰及最后时限。
kube-controller-manager
kube-controller-manager 是控制平面的组件, 负责运行控制器进程。
从逻辑上讲, 每个控制器都是一个单独的进程, 但是为了降低复杂性,它们都被编译到同一个可执行文件,并在同一个进程中运行。
有许多不同类型的控制器。以下是一些例子:
-
节点控制器(Node Controller):负责在节点出现故障时进行通知和响应
-
任务控制器(Job Controller):监测代表一次性任务的 Job 对象,然后创建 Pod 来运行这些任务直至完成
-
端点分片控制器(EndpointSlice controller):填充端点分片(EndpointSlice)对象(以提供 Service 和 Pod 之间的链接)。
-
服务账号控制器(ServiceAccount controller):为新的命名空间创建默认的服务账号(ServiceAccount)。
以上并不是一个详尽的列表。
cloud-controller-manager
一个 Kubernetes 控制平面组件, 嵌入了特定于云平台的控制逻辑。 云控制器管理器(Cloud Controller Manager) 允许将你的集群连接到云提供商的 API 之上, 并将与该云平台交互的组件同与你的集群交互的组件分离开来。
cloud-controller-manager
仅运行特定于云平台的控制器。 因此如果你在自己的环境中运行 Kubernetes,或者在本地计算机中运行学习环境, 所部署的集群不需要有云控制器管理器。
与 kube-controller-manager
类似,cloud-controller-manager
将若干逻辑上独立的控制回路组合到同一个可执行文件中, 供你以同一进程的方式运行。 你可以对其执行水平扩容(运行不止一个副本)以提升性能或者增强容错能力。
下面的控制器都包含对云平台驱动的依赖:
-
节点控制器(Node Controller):用于在节点终止响应后检查云提供商以确定节点是否已被删除
-
路由控制器(Route Controller):用于在底层云基础架构中设置路由
-
服务控制器(Service Controller):用于创建、更新和删除云提供商负载均衡器
Node 组件
节点组件会在每个节点上运行,负责维护运行的 Pod 并提供 Kubernetes 运行环境。
kubelet
kubelet
会在集群中每个节点(node)上运行。 它保证容器(containers)都运行在 Pod 中。
kubelet 接收一组通过各类机制提供给它的 PodSpec,确保这些 PodSpec 中描述的容器处于运行状态且健康。 kubelet 不会管理不是由 Kubernetes 创建的容器。
kube-proxy
kube-proxy 是集群中每个节点(node)上所运行的网络代理, 实现 Kubernetes 服务(Service) 概念的一部分。
kube-proxy 维护节点上的一些网络规则, 这些网络规则会允许从集群内部或外部的网络会话与 Pod 进行网络通信。
如果操作系统提供了可用的数据包过滤层,则 kube-proxy 会通过它来实现网络规则。 否则,kube-proxy 仅做流量转发。
资源创建方式
-
命令行
通过使用 kubectl 命令的方式手动创建资源,适合初学者学习使用,好处是容易上手,不用编写 yaml 文件即可操作各种资源,坏处是必须记住命令,且不利于重复化使用。
-
YAML 文件
通过编写符合 Kubernetes 规范的 YAML 文件来组织资源的创建方式,好处是规范、持久化、可重复利用,能够让部署可以像代码一样管理,坏处是需要熟悉配置文件的语法才能使用此方法。
Namespace
Namespace 表示命名空间,用来隔离资源。类似于 Nacos 中的 namespace。
namespace 的 short-name 为 ns
# 命名空间的名称需要唯一
kubectl create ns hello
kubectl delete ns hello
apiVersion: v1
kind: Namespace
metadata:
name: hello
Pod
Pod 是 Kubernetes 中调度和运行的最小单元,其内部可以有一至多个容器实例,相当于在 Docker 的 Container 上封装了一层,变成了 Pod。
pod 的 short-name 为 po
# 运行一个nginx Pod
kubectl run mynginx --image=nginx
# 查看default名称空间的Pod
kubectl get pod
# 描述
kubectl describe pod <PodName>
# 删除
kubectl delete pod <PodName>
# 查看Pod的运行日志
kubectl logs <PodName>
# 每个Pod都会被分配一个ip
kubectl get pod -owide
# 使用Pod的ip+port访问运行容器的端口
curl 192.168.10.101:80
# 集群中的任意一个机器以及任意的应用都能通过Pod分配的ip来访问这个Pod
apiVersion: v1
kind: Pod
metadata:
labels:
run: mynginx
name: mynginx
namespace: default
spec:
containers:
- image: nginx
name: mynginx
apiVersion: v1
kind: Pod
metadata:
labels:
run: myapp
name: myapp
spec:
containers:
- image: nginx
name: nginx
- image: tomcat:8.5.68
name: tomcat
上面这个运行了两个容器的 Pod,其模型类似于下图
Deployment
Deployment 用来控制 Pod,使 Pod 具有多副本、自愈、扩缩容的能力。
deployment 的 short-name 为 deploy
多副本
kubectl create deployment my-dep --image=nginx --replicas=3
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: my-dep
name: my-dep
spec:
replicas: 3
selector:
matchLabels:
app: my-dep
template:
metadata:
labels:
app: my-dep
spec:
containers:
- image: nginx
name: nginx
扩缩容
kubectl scale --replicas=5 deployment/my-dep
kubectl edit deployment my-dep
# 修改 replicas
自愈 & 故障转移
一旦发生某个 Pod 故障的情况,Deployment 就会立刻部署新的 Pod 来实现自愈或故障转移
-
停机
-
删除 Pod
-
容器崩溃
-
...
滚动更新
# 滚动更新
kubectl set image deployment/my-dep nginx=nginx:1.16.1 --record
# 实时查看滚动更新状态
kubectl rollout status deployment/my-dep
版本回退
# 历史记录
kubectl rollout history deployment/my-dep
# 查看某个历史详情
kubectl rollout history deployment/my-dep --revision=2
# 回滚(回到上次)
kubectl rollout undo deployment/my-dep
# 回滚(回到指定版本)
kubectl rollout undo deployment/my-dep --to-revision=2
更多:
除了 Deployment,k8s 还有 StatefulSet
、DaemonSet
、Job
等类型资源,称为 工作负载
。
有状态应用使用 StatefulSet
部署,无状态应用使用 Deployment
部署。
参考:工作负载管理
Service
Service 是将一组 Pods 公开为网络服务的抽象方法。
service 的 short-name 为 svc
ClusterIP
默认的 ServiceType。通过集群的内部 IP 暴露服务,服务只能够在集群内部访问。
# 暴露Deploy
kubectl expose deployment my-dep --port=8000 --target-port=80
# 使用标签检索Pod
kubectl get pod -l app=my-dep
apiVersion: v1
kind: Service
metadata:
labels:
app: my-dep
name: my-dep
spec:
selector:
app: my-dep
ports:
- port: 8000
protocol: TCP
targetPort: 80
NodePort
通过每个 Node 节点上的 IP 和静态端口(NodePort)暴露服务。NodePort 服务会路由到 ClusterIP 服务,这个 ClusterIP 服务会自动创建。可以从集群的外部访问每一个 NodePort 服务。
NodePort 需要借助真实存在的 ip,是一个公共的 ip,任何人都可以访问,而 ClusterIP 可以理解成不对外开放,仅限于集群内的节点之间特定的一个范围。
kubectl expose deployment my-dep --port=8000 --target-port=80 --type=NodePort
apiVersion: v1
kind: Service
metadata:
labels:
app: my-dep
name: my-dep
spec:
ports:
- port: 8000
protocol: TCP
targetPort: 80
selector:
app: my-dep
type: NodePort
如果不手动指定 nodePort,那么将会随机分配 30000-32767 范围内的某个可用端口
负载均衡
Service 天然具备负载均衡的功能,那么具体怎么测试呢?
首先查看 service :
[root@master ~]# kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
my-dep ClusterIP 10.1.7.161 <none> 8000/TCP 60s
可以看到 service 的 ip 为 10.1.7.161,port 为 8000,此时在集群内部可以通过 ip:port 的方式实现负载均衡访问整个服务。
要想看到负载均衡的效果,我们可以将几个 nginx 容器的 index.html 修改一下:
-
通过 Kubernetes Dashboard 进入容器内部
-
在容器内部执行命令
echo 111 > /usr/share/nginx/html/index.html
-
同理,在其他几个容器也执行一下
echo 222 > /usr/share/nginx/html/index.html
echo 333 > /usr/share/nginx/html/index.html
接着,通过命令行访问 service 的 ip:port,多执行几次,查看效果:
curl 10.1.7.161:8000
可以看出,service 底层确实帮我们实现了负载均衡!
域名访问
现在可以通过 ip:port 来访问 service,但 ip 是不固定的,如果使用这种方式,那么每次部署的时候 ip 都可能会发生变化,从而依赖它的其他服务需要做相应调整。
那么,k8s 能不能通过域名的方式来访问 serivice 呢?
答案是肯定的,在集群 Pod 内部,相同命名空间下,默认可以通过 service:port
的域名来访问某个 service。
比如,现在我们的 service 名字叫 my-dep,则它的域名就是 my-dep
,再加上端口号,就可以直接访问该 service:
my-dep:8000
这种默认的域名方式只能在相同命名空间下的 Pod 容器内部使用,我们现在来创建一个 Pod 测试一下:
kubectl create deployment my-tomcat --image=tomcat:8.5.68
然后进入 tomcat 容器内部执行多次:
curl my-dep:8000
可以看到,通过域名可以直接访问其他 service!
具体原理可以阅读这篇文章:k8s 服务注册与发现(二)Kubernetes内部域名解析原理-腾讯云开发者社区-腾讯云
Ingress
可以看作 Service 的统一网关入口。
Ingress 控制器有不同的实现,典型的有 Nginx、Istio、Traefik 等。
安装
这里以 Nginx 实现为例。
cat > ingress-nginx-controller.yaml << EOF
apiVersion: v1
kind: Namespace
metadata:
name: ingress-nginx
labels:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
---
kind: ConfigMap
apiVersion: v1
metadata:
name: nginx-configuration
namespace: ingress-nginx
labels:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
---
kind: ConfigMap
apiVersion: v1
metadata:
name: tcp-services
namespace: ingress-nginx
labels:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
---
kind: ConfigMap
apiVersion: v1
metadata:
name: udp-services
namespace: ingress-nginx
labels:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: nginx-ingress-serviceaccount
namespace: ingress-nginx
labels:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
name: nginx-ingress-clusterrole
labels:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
rules:
- apiGroups:
- ""
resources:
- configmaps
- endpoints
- nodes
- pods
- secrets
verbs:
- list
- watch
- apiGroups:
- ""
resources:
- nodes
verbs:
- get
- apiGroups:
- ""
resources:
- services
verbs:
- get
- list
- watch
- apiGroups:
- ""
resources:
- events
verbs:
- create
- patch
- apiGroups:
- "extensions"
- "networking.k8s.io"
resources:
- ingresses
verbs:
- get
- list
- watch
- apiGroups:
- "extensions"
- "networking.k8s.io"
resources:
- ingresses/status
verbs:
- update
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: Role
metadata:
name: nginx-ingress-role
namespace: ingress-nginx
labels:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
rules:
- apiGroups:
- ""
resources:
- configmaps
- pods
- secrets
- namespaces
verbs:
- get
- apiGroups:
- ""
resources:
- configmaps
resourceNames:
# Defaults to "<election-id>-<ingress-class>"
# Here: "<ingress-controller-leader>-<nginx>"
# This has to be adapted if you change either parameter
# when launching the nginx-ingress-controller.
- "ingress-controller-leader-nginx"
verbs:
- get
- update
- apiGroups:
- ""
resources:
- configmaps
verbs:
- create
- apiGroups:
- ""
resources:
- endpoints
verbs:
- get
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: RoleBinding
metadata:
name: nginx-ingress-role-nisa-binding
namespace: ingress-nginx
labels:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: nginx-ingress-role
subjects:
- kind: ServiceAccount
name: nginx-ingress-serviceaccount
namespace: ingress-nginx
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: nginx-ingress-clusterrole-nisa-binding
labels:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: nginx-ingress-clusterrole
subjects:
- kind: ServiceAccount
name: nginx-ingress-serviceaccount
namespace: ingress-nginx
---
kind: Service
apiVersion: v1
metadata:
name: ingress-nginx
namespace: ingress-nginx
labels:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
spec:
clusterIP: 10.1.211.240
externalTrafficPolicy: Cluster
ports:
- name: http
nodePort: 31686
port: 80
protocol: TCP
targetPort: http
- name: https
nodePort: 30036
port: 443
protocol: TCP
targetPort: https
selector:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
sessionAffinity: None
type: NodePort
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-ingress-controller
namespace: ingress-nginx
labels:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
template:
metadata:
labels:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
annotations:
prometheus.io/port: "10254"
prometheus.io/scrape: "true"
spec:
# wait up to five minutes for the drain of connections
terminationGracePeriodSeconds: 300
serviceAccountName: nginx-ingress-serviceaccount
hostNetwork: true
containers:
- name: nginx-ingress-controller
image: registry.aliyuncs.com/google_containers/nginx-ingress-controller:0.26.1
args:
- /nginx-ingress-controller
- --configmap=$(POD_NAMESPACE)/nginx-configuration
- --tcp-services-configmap=$(POD_NAMESPACE)/tcp-services
- --udp-services-configmap=$(POD_NAMESPACE)/udp-services
- --publish-service=$(POD_NAMESPACE)/ingress-nginx
- --annotations-prefix=nginx.ingress.kubernetes.io
securityContext:
allowPrivilegeEscalation: true
capabilities:
drop:
- ALL
add:
- NET_BIND_SERVICE
# www-data -> 33
runAsUser: 33
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
ports:
- name: http
containerPort: 80
- name: https
containerPort: 443
livenessProbe:
failureThreshold: 3
httpGet:
path: /healthz
port: 10254
scheme: HTTP
initialDelaySeconds: 10
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 10
readinessProbe:
failureThreshold: 3
httpGet:
path: /healthz
port: 10254
scheme: HTTP
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 10
lifecycle:
preStop:
exec:
command:
- /wait-shutdown
---
EOF
kubectl apply -f ingress-nginx-controller.yaml
-
使用
[root@master k8s]# kubectl get all -n ingress-nginx
NAME READY STATUS RESTARTS AGE
pod/nginx-ingress-controller-84865c44d9-p6ktw 1/1 Running 0 106s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/ingress-nginx NodePort 10.1.211.240 <none> 80:31686/TCP,443:30036/TCP 106s
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/nginx-ingress-controller 1/1 1 1 106s
NAME DESIRED CURRENT READY AGE
replicaset.apps/nginx-ingress-controller-84865c44d9 1 1 1 106s
可以看到 Ingress 底层暴露了两个 NodePort,31686 和 30036,分别绑定了内部 nginx 的 80 和 443 端口,那么我们就可以通过集群中任意一个 Node 的这两个端口来访问 Ingress,比如:
使用
-
先部署 nginx 和 tomcat
apiVersion: v1
kind: Service
metadata:
name: my-nginx
spec:
selector:
app: my-nginx
ports:
- port: 80
targetPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: my-tomcat
spec:
selector:
app: my-tomcat
ports:
- port: 8080
targetPort: 8080
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-nginx
spec:
selector:
matchLabels:
app: my-nginx
replicas: 2
template:
metadata:
labels:
app: my-nginx
spec:
containers:
- name: my-nginx
image: nginx
ports:
- containerPort: 80
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-tomcat
spec:
selector:
matchLabels:
app: my-tomcat
replicas: 2
template:
metadata:
labels:
app: my-tomcat
spec:
containers:
- name: my-tomcat
image: tomcat:8.5.68
ports:
- containerPort: 8080
域名访问
-
编写一个 ingress-example.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-example
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
rules:
- host: nginx.ingress.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: my-nginx
port:
number: 80
- host: tomcat.ingress.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: my-tomcat
port:
number: 8080
-
添加 host
192.168.10.101 nginx.ingress.example.com tomcat.ingress.example.com
路径重写
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-example
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /$2
spec:
rules:
- host: nginx.ingress.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: my-nginx
port:
number: 80
- host: tomcat.ingress.example.com
http:
paths:
- path: "/test(/|$)(.*)"
pathType: Prefix
backend:
service:
name: my-tomcat
port:
number: 8080
尝试访问 tomcat 加上 /test 前缀:http://tomcat.ingress.example.com:31686/test
流量限制
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-limit-rate
annotations:
nginx.ingress.kubernetes.io/limit-rps: "1"
spec:
ingressClassName: my-nginx
rules:
- host: nginx.ingress.example.com
http:
paths:
- path: /
pathType: Exact
backend:
service:
name: my-nginx
port:
number: 80
尝试浏览器一直 F5 刷新访问 nginx:http://nginx.ingress.example.com:31686/
存储抽象
卷
容器中的文件在磁盘上是临时存放的,当容器崩溃时,kubelet 将重启容器,而容器中的文件将会丢失——因为容器会以干净的状态重建。其次,当在一个 Pod 中同时运行多个容器时,常常需要在这些容器之间共享文件。Kubernetes 抽象出 Volume 对象来解决这两个问题。
Docker 中也有 Volume 的概念,在 Docker 中,Volume 是磁盘上或者另一个容器内的一个目录。但 Docker 对它只有少量且松散的管理,并且功能还非常有限(例如,截至 Docker 1.7,每个容器只允许有一个 Volume 驱动程序,并且无法将参数传递给卷)。
Kubernetes 中的卷具有明确的生命周期——与包裹它的 Pod 相同,它比 Pod 中运行的任何容器的存活期都长,在容器重启时数据也会得到保留。当一个 Pod 不存在时,卷也将不再存在。Kubernetes 支持许多类型的卷,Pod 也能同时使用任意数量的卷。
卷的核心是包含一些数据的目录,Pod 中的容器可以访问该目录。特定的卷类型可以决定这个目录如何形成、支持何种介质,以及目录中存放什么内容。
使用卷时,Pod 声明中需要提供卷的类型(.spec.volumes 字段)和卷挂载的位置(.spec.containers.volumeMounts 字段)
Kubernetes 提供了众多的卷类型,包括 emptyDir、hostPath、nfs、glusterfs、cephfs 等。
hostPath
hostPath 卷能将主机节点上的文件或目录挂载到 Pod 中。
apiVersion: v1
kind: Pod
metadata:
name: hostpath-pod
spec:
containers:
- name: hostpath-pod
image: nginx
volumeMounts:
- mountPath: /test-nginx
name: myhostpath
volumes:
- name: myhostpath
hostPath:
path: /tmp/nginx
type: DirectoryOrCreate
NFS
很多应用需要在集群内部有一个统一的地方存储文件,比如日志、图片等,而使用 hostPath 方式并不灵活,因为只能存放在与 Pod 相同的节点上,当有多个 Pod 时只能分散存储,无法做到统一存储。而使用 NFS 的方式可以解决这个问题。
1、所有节点
# 所有机器安装
yum install -y nfs-utils rpcbind
2、主节点
# nfs主节点
echo "/nfs/data/ *(insecure,rw,sync,no_root_squash)" > /etc/exports
mkdir -p /nfs/data
systemctl enable rpcbind --now
systemctl enable nfs-server --now
# 配置生效
exportfs -r
3、从节点
# 展示主节点可挂载的目录
showmount -e master
# 挂载 nfs 服务器上的共享目录到本机路径
mkdir -p /nfs/data
mount -t nfs master:/nfs/data /nfs/data
# 写入一个测试文件
echo "hello nfs server" > /nfs/data/test.txt
4、引用 NFS 存储
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: nginx-pv-demo
name: nginx-pv-demo
spec:
replicas: 2
selector:
matchLabels:
app: nginx-pv-demo
template:
metadata:
labels:
app: nginx-pv-demo
spec:
containers:
- image: nginx
name: nginx
volumeMounts:
- name: html
mountPath: /usr/share/nginx/html
volumes:
- name: html
nfs:
server: master
path: /nfs/data/nginx-pv
持久化存储
官方介绍
存储的管理是一个与计算实例的管理完全不同的问题。 PersistentVolume 子系统为用户和管理员提供了一组 API, 将存储如何制备的细节从其如何被使用中抽象出来。 为了实现这点,我们引入了两个新的 API 资源:PersistentVolume 和 PersistentVolumeClaim。
持久卷(PersistentVolume,PV) 是集群中的一块存储,可以由管理员事先制备, 或者使用存储类(Storage Class)来动态制备。 持久卷是集群资源,就像节点也是集群资源一样。PV 持久卷和普通的 Volume 一样, 也是使用卷插件来实现的,只是它们拥有独立于任何使用 PV 的 Pod 的生命周期。 此 API 对象中记述了存储的实现细节,无论其背后是 NFS、iSCSI 还是特定于云平台的存储系统。
持久卷申领(PersistentVolumeClaim,PVC) 表达的是用户对存储的请求。概念上与 Pod 类似。 Pod 会耗用节点资源,而 PVC 申领会耗用 PV 资源。Pod 可以请求特定数量的资源(CPU 和内存)。同样 PVC 申领也可以请求特定的大小和访问模式 (例如,可以挂载为 ReadWriteOnce、ReadOnlyMany、ReadWriteMany 或 ReadWriteOncePod, 请参阅访问模式)。
尽管 PersistentVolumeClaim 允许用户消耗抽象的存储资源, 常见的情况是针对不同的问题用户需要的是具有不同属性(如,性能)的 PersistentVolume 卷。 集群管理员需要能够提供不同性质的 PersistentVolume, 并且这些 PV 卷之间的差别不仅限于卷大小和访问模式,同时又不能将卷是如何实现的这些细节暴露给用户。 为了满足这类需求,就有了存储类(StorageClass) 资源。
总结
PV:持久卷(Persistent Volume),将应用需要持久化的数据保存到指定位置 PVC:持久卷申明(Persistent Volume Claim),申明需要使用的持久卷的规格和大小等 StorageClass:存储类,用于自动创建 PV
PV、PVC、StorageClass 的协作过程
PV & PVC
1、创建 PV 池
静态供应
# nfs主节点
mkdir -p /nfs/data/01
mkdir -p /nfs/data/02
mkdir -p /nfs/data/03
创建 PV
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv01-10m
spec:
capacity:
storage: 10Mi
accessModes:
- ReadWriteMany
storageClassName: nfs
nfs:
path: /nfs/data/01
server: master
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv02-1g
spec:
capacity:
storage: 1Gi
accessModes:
- ReadWriteMany
storageClassName: nfs
nfs:
path: /nfs/data/02
server: master
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv03-3g
spec:
capacity:
storage: 3Gi
accessModes:
- ReadWriteMany
storageClassName: nfs
nfs:
path: /nfs/data/03
server: master
3、PVC 创建与绑定
创建 PVC
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: nginx-pvc
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 200Mi
storageClassName: nfs
创建 Pod 绑定 PVC
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: nginx-deploy-pvc
name: nginx-deploy-pvc
spec:
replicas: 2
selector:
matchLabels:
app: nginx-deploy-pvc
template:
metadata:
labels:
app: nginx-deploy-pvc
spec:
containers:
- image: nginx
name: nginx
volumeMounts:
- name: html
mountPath: /usr/share/nginx/html
volumes:
- name: html
persistentVolumeClaim:
claimName: nginx-pvc
StorageClass
什么是 StorageClass
Kubernetes 提供了一套可以自动创建 PV 的机制,即 Dynamic Provisioning,而这个机制的核心在于 StorageClass 这个 API 对象。
StorageClass 对象会定义两部分内容:
-
PV 的属性,比如:存储类型、Volume 的大小等
-
创建这种 PV 需要用到的存储插件
为什么需要 StorageClass
在一个大规模的 Kubernetes 集群里,可能有成千上万个 PVC,这就意味着运维人员必须创建出这么多个 PV。随着项目的发展,会有新的 PVC 不断地被提交,那么运维人员就需要不断地添加新的满足要求的 PV,否则新的 Pod 就会因为 PVC 绑定不到 PV 而导致创建失败。
此外,不同的应用程序对于存储性能的要求可能也不尽相同,比如读写速度、并发性能等,通过 PVC 请求到一定的存储空间也很有可能不足以满足应用对于存储设备的各种需求。
为了解决这一问题,Kubernetes 又为我们引入了一个新的资源对象:StorageClass。通过 StorageClass 的定义,管理员可以将存储资源定义为某种类型的资源,比如快速存储、慢速存储等,用户根据 StorageClass 的描述就可以非常直观的知道各种存储资源的特性了,这样就可以根据应用的特性去申请合适的存储资源了。
NFS Provisioner
NFS Provisioner 是一个自动配置卷程序,它使用现有的 NFS 服务器来支持通过持久卷申明来动态配置持久卷。
持久卷会被配置为:{namespace}-{pvcName}-{pvName}。
实战
创建用户和角色 RBAC
apiVersion: v1
kind: ServiceAccount
metadata:
name: nfs-client-provisioner
# replace with namespace where provisioner is deployed
namespace: default
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: nfs-client-provisioner-runner
rules:
- apiGroups: [""]
resources: ["persistentvolumes"]
verbs: ["get", "list", "watch", "create", "delete"]
- apiGroups: [""]
resources: ["persistentvolumeclaims"]
verbs: ["get", "list", "watch", "update"]
- apiGroups: ["storage.k8s.io"]
resources: ["storageclasses"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["events"]
verbs: ["create", "update", "patch"]
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: run-nfs-client-provisioner
subjects:
- kind: ServiceAccount
name: nfs-client-provisioner
# replace with namespace where provisioner is deployed
namespace: default
roleRef:
kind: ClusterRole
name: nfs-client-provisioner-runner
apiGroup: rbac.authorization.k8s.io
---
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: leader-locking-nfs-client-provisioner
# replace with namespace where provisioner is deployed
namespace: default
rules:
- apiGroups: [""]
resources: ["endpoints"]
verbs: ["get", "list", "watch", "create", "update", "patch"]
---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: leader-locking-nfs-client-provisioner
subjects:
- kind: ServiceAccount
name: nfs-client-provisioner
# replace with namespace where provisioner is deployed
namespace: default
roleRef:
kind: Role
name: leader-locking-nfs-client-provisioner
apiGroup: rbac.authorization.k8s.io
部署 nfs-client-provisioner
apiVersion: apps/v1
kind: Deployment
metadata:
name: nfs-client-provisioner
labels:
app: nfs-client-provisioner
# replace with namespace where provisioner is deployed
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: nfs-client-provisioner
strategy:
type: Recreate
template:
metadata:
labels:
app: nfs-client-provisioner
spec:
serviceAccountName: nfs-client-provisioner
containers:
- name: nfs-client-provisioner
#image: registry.cn-shenzhen.aliyuncs.com/hoby/nfs-subdir-external-provisioner:v4.0.0
image: registry.cn-shenzhen.aliyuncs.com/hoby/nfs-client-provisioner:v3.1.0
volumeMounts:
- name: nfs-client-root
mountPath: /persistentvolumes
env:
- name: PROVISIONER_NAME
value: qgg-nfs-storage #与storageClass的provisioner配置一致
- name: NFS_SERVER
value: master #NFS服务器的地址
- name: NFS_PATH
value: /nfs/data #NFS服务器的目录
volumes:
- name: nfs-client-root
nfs:
server: master
path: /nfs/data
创建 StorageClass
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: managed-nfs-storage
provisioner: qgg-nfs-storage #与nfs-client-provisioner的环境变量PROVISIONER_NAME一致
allowVolumeExpansion: true
parameters:
pathPattern: "${.PVC.namespace}-${.PVC.name}"
onDelete: retain
创建 PVC
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: pvc-sc
annotations:
volume.beta.kubernetes.io/storage-class: "managed-nfs-storage"
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 1Mi
创建 Pod
apiVersion: v1
kind: Pod
metadata:
name: pvc-sc-pod
labels:
name: pvc-sc-pod
spec:
containers:
- name: pvc-sc-pod
image: nginx
command:
- "/bin/sh"
args:
- "-c"
- "touch /mnt/SUCCESS && exit 0 || exit 1" #创建一个SUCCESS文件后退出
volumeMounts:
- name: nfs-pvc
mountPath: /mnt
resources:
limits:
memory: "128Mi"
cpu: "500m"
ports:
- containerPort: 80
volumes:
- name: nfs-pvc
persistentVolumeClaim:
claimName: pvc-sc
restartPolicy: Never
验证结果
[root@master k8s]# kubectl get pod
NAME READY STATUS RESTARTS AGE
nfs-client-provisioner-698559446f-8b5kl 1/1 Running 1 13d
pvc-sc-pod 0/1 Completed 0 27s
pvc-sc-pod 状态为 Completed,说明任务已经执行完成,退出了容器,此为正常结果。
[root@master k8s]# ls /nfs/data/
default-pvc-sc-pvc-4ebc68ca-b68b-475e-b3e4-ffce442ea512 mysql-pv
[root@master k8s]# ls /nfs/data/default-pvc-sc-pvc-4ebc68ca-b68b-475e-b3e4-ffce442ea512/
SUCCESS
查看 master 的 /nfs/data 文件夹,可以看到该目录下为 pvc-sc-pod 自动创建了一个文件夹,里面创建了一个 SUCCESS 文件。
ConfigMap
ConfigMap 是一种 API 对象,用来将非机密性的数据保存到键值对中。使用时可以用作环境变量、命令行参数或者存储卷中的配置文件。
ConfigMap 将环境配置信息和容器镜像解耦,便于应用配置的修改。
注意:ConfigMap 并不提供保密或加密功能,如果你想存储的数据是机密的,请使用 Secret。
创建一个 ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
name: appvar
data:
# 类属性键:每一个键都映射到一个简单的值
player_initial_lives: "3"
ui_properties_file_name: "user-interface.properties"
# 类文件键:一个键可以映射多行内容,常用于抽取应用配置
game.properties: |
enemy.types=aliens,monsters
player.maximum-lives=5
user-interface.properties: |
color.good=purple
color.bad=yellow
allow.textmode=true
通过环境变量方式使用
Kubernetes 在 1.6 版本引入新字段 envFrom,可以实现在 Pod 环境中将 ConfigMap 中定义的所有 key=value 自动生成为环境变量,代码格式如下所示:
apiVersion: v1
kind: Pod
metadata:
name: envfrom-cm-test
spec:
containers:
- name: envfrom-cm-test
image: busybox
command: [ "/bin/sh", "-c", "env" ]
envFrom:
- configMapRef:
name: appvar
restartPolicy: Never
Redis 示例
创建 ConfigMap
apiVersion: v1
data: #data是所有真正的数据,key:默认是文件名,value:配置文件的内容
redis.conf: |
appendonly yes
kind: ConfigMap
metadata:
name: redis-conf
namespace: default
创建 Pod
apiVersion: v1
kind: Pod
metadata:
name: redis
spec:
containers:
- name: redis
image: redis
command:
- redis-server
- "/redis-master/redis.conf" #指的是redis容器内部的位置
ports:
- containerPort: 6379
volumeMounts:
- mountPath: /data
name: data
- mountPath: /redis-master
name: config
volumes:
- name: data
emptyDir: {}
- name: config
configMap:
name: redis-conf
items:
- key: redis.conf
path: redis.conf
检查默认配置
kubectl exec -it redis -- redis-cli
127.0.0.1:6379> CONFIG GET appendonly
1) "appendonly"
2) "yes"
修改 ConfigMap
apiVersion: v1
data: #data是所有真正的数据,key:默认是文件名,value:配置文件的内容
redis.conf: |
appendonly yes
maxmemory 2mb
maxmemory-policy allkeys-lru
kind: ConfigMap
metadata:
name: redis-conf
namespace: default
检查配置是否更新
kubectl exec -it redis -- redis-cli
127.0.0.1:6379> CONFIG GET maxmemory
1) "maxmemory"
2) "0"
127.0.0.1:6379> CONFIG GET maxmemory-policy
1) "maxmemory-policy"
2) "noeviction"
配置值未更改,因为需要重新启动 Pod 才能从关联的 ConfigMap 中获取更新的值。 原因:我们的Pod部署的中间件自己本身没有热更新能力。
如何重启 Pod
kubectl get pod {podname} -n {namespace} -o yaml | kubectl replace --force -f -
kubectl get pod redis -n default -o yaml | kubectl replace --force -f -
重启 Pod 之后配置生效:
Secret
Secret 对象类型用来保存敏感信息,例如密码、OAuth 令牌和 SSH 密钥。 将这些信息放在 Secret 中比放在 Pod 的定义或者容器镜像中来说更加安全和灵活。
镜像仓库 Secret
创建
kubectl create secret docker-registry regcred \
--docker-server=<你的镜像仓库服务器> \
--docker-username=<你的用户名> \
--docker-password=<你的密码> \
--docker-email=<你的邮箱地址>
引用
apiVersion: v1
kind: Pod
metadata:
name: private-nginx
spec:
containers:
- name: private-nginx
image: hobyy/kubeblog:1.0
imagePullSecrets:
- name: my-docker
通用 Secret
从文本文件创建
echo -n 'admin' > ./username.txt
echo -n '1f2d1e2e67df' > ./password.txt
kubectl create secret generic db-user-pass --from-file=./username.txt --from-file=./password.txt
默认的键名是文件名,你也可以使用 [–from-file=[key=]source] 参数来设置键名。
kubectl create secret generic db-user-pass \
--from-file=username=./username.txt \
--from-file=password=./password.txt
从命令行创建
kubectl create secret generic dev-db-secret \
--from-literal=username='devuser' \
--from-literal=password='S!B\*d$zDsb='
从 yaml 创建
apiVersion: v1
kind: Secret
metadata:
name: user-password-secret
type: Opaque
data:
username: cm9vdA==
password: bGl1bWlhb2Nu
环境变量方式引用
apiVersion: v1
kind: Pod
metadata:
name: secrets-test-pod
spec:
containers:
- name: busybox-container
image: busybox
command: ["sleep", "1000"]
env:
- name: ENV_VAR_USERNAME
valueFrom:
secretKeyRef:
name: user-password-secret
key: username
- name: ENV_VAR__PASSWORD
valueFrom:
secretKeyRef:
name: user-password-secret
key: password
restartPolicy: Never
常见问题
coredns 内网转发出现5s卡顿
-
原因分析
coredns 在解析域名时候会尝试获取 ipv6 的地址,在默认的情况下解析 ipv6 地址会出现卡顿的现象。
-
解决办法
参考:https://tjtharrison.medium.com/disabling-ipv6-on-self-hosted-kubernetes-26e8ea113d42
修改 coredns 配置,添加一行配置来禁用 ipv6 解析:rewrite stop type AAAA A
kubectl edit cm -n kube-system coredns
接着重启来使配置生效:
kubectl rollout restart deployment coredns -n kube-system