2. Get Started
2.1 Docker 容器
> docker run busybox ls -lh # 运行标准的 unix 命令
> docker run <image>:<tag> # 运行指定版本的 image,tag 默认 latest
# Dockerfile 包含构建 docker 镜像的命令
FROM node # 基础镜像
ADD app.js /app.js # 将本地文件添加到镜像的根目录
ENTRYPOINT ["node", "app.js"] # 镜像被执行时需被执行的命令
> docker build -t kubia . # 在当前目录根据 Dockerfile 构建指定 tag 的镜像
> docker images # 列出本地所有镜像
# 执行基于 kubia 镜像,映射主机 8081 到容器内 8080 端口,并在后台运行的容器
> docker run --name kubia-container -p 8081:8080 -d kubia
> docker ps # 列出 running 容器
> docker ps -a # 列出 running, exited 容器
> docker exec -it kubia-container bash # 在容器内执行 shell 命令,如 ls/sh
> docker stop kubia-container # 停止容器
> docker rm kubia-container # 删除容器
> docker tag kubia wuyinio/kubia # 给本地镜像打标签
> docker login
> docker push wuyinio/kubia # push 到 DockerHub
2.2 配置 k8s 集群
> minikube start # 本地启动 minikube 单节点虚拟机
> kubectl cluster-info # 查看集群各组件的 URL,是否工作正常
> kubectl get nodes # get 命令可列出各种 k8s 对象的基本信息
> kubectl describe node <NODE_ID> # describe 命令显示 k8s 对象更详细的信息
2.3 在 k8s 上运行应用
> kubectl run kubia --image=wuyinio/kubia --port=8080 --generator=run/v1 # 创建 rc 并拉取镜像运行
> kubectl get pods # 列出 pods
> kubectl expose rc kubia --type=LoadBalancer --name kubia-http # 通过 LoadBalancer 服务暴露 ClusterIP pod 服务给外部访问
> kubectl get svc # 列出 services
> kubectl get rc # 列出 replication controller
> minikube service kubia-http # minikube 单节点不支持 LoadBalancer 服务,需手动获取服务地址
# kubia-http service 的 EXTERNAL_IP 一直为 PENDING 状态
> kubectl scale rc kubia --replicas=5 # 修改 rc 期望的副本数,来水平伸缩应用
> kubectl get pod kubia-1ic8j -o wide # 显示 pod 详细列
3. Pod
3.2 创建 Pod
yaml pod 定义模块
- apiVersion 与 kind 资源类型。
- metadata 元数据: pod 名称、标签、注解。
- spec 规格内部元件信息:容器镜像名称、卷等。
kubia-manual.yaml
apiVersion: v1
kind: Pod
metadata:
name: kubia-manual
spec:
containers:
- image: wuyinio/kubia:2
name: kubia
ports:
- containerPort: 8080 # pod 对外暴露的端口
protocol: TCP # supported values: "SCTP", "TCP", "UDP"
> kubectl create -f kubia-manual.yaml # 从 yaml 文件创建 k8s 资源
> kubectl get pod kubia-manual -o yaml # 导出 pod 定义
> kubectl logs kubia-manual -c kubia # 查看 kubia-manual Pod 中 kubia 容器的日志,-c 显式指定容器名称
> kubectl logs kubia-manual --previous # 查看崩溃前的上一个容器日志
> minikube ssh && docker ps
> docker logs bdb67198848d # 登录到 pod 运行时的节点 minikube,手动 docker logs 查看日志
> kubectl port-forward kubia-manual 9090:8080 # 配置多重端口转发,将本机的 9090 转发至 pod 的 8080,可用于调试等
port-forward kubia-manual 9090:8080
Forwarding from 127.0.0.1:9090 -> 8080
Forwarding from [::1]:9090 -> 8080
Handling connection for 9090 # curl 127.0.0.1:9090
3.3 标签(label)
基于组操作 pod 而非单个操作,metadata.labels 的 kv pair 标签可组织任何 k8s 资源,保存标识信息。
apiVersion: v1
kind: Pod
metadata:
name: kubia-manual-v2
labels:
creation_method: manual
env: prod
spec: # ...
# 基于 Label 的增删改查操作
> kubectl get pods --show-labels # 显示 labels
> kubectl get pods -L env # 只显示 env 标签
> kubectl label pod kubia-manual env=debug # 为指定的 pod 资源添加新标签
> kubectl label pod kubia-manual env=online --overwrite=true # 修改标签
> kubectl label pod kubia-manual env- # - 号删除标签
3.5 标签选择器(nodeSelector)
label selector 可筛选出具有指定值的 k8s 资源。
> kubectl get pods -l env=debug # 筛选 env 为 debug 的 pods # get pod 与 get pods 无异
> kubectl get pod -l creation_method!=manual # 不等
> kubectl get pods -l '!env' # 不含 env 标签的 pods # -l 筛选 -L 显示 # "" 双引号会转义
> kubectl get pods -l 'env in (debug)' # in 筛选 # -l 接受整体字符串为参数
> kubectl get pods -l 'env notin (debug,online)' # notin 筛选
在指定标签 node 上运行 pod:
apiVersion: v1
kind: Pod
metadata:
name: kubia-gpu
spec:
nodeSelector:
gpu: "true" # 当无可用 node 时 pod 一直处于 Pending 状态
containers: #...
可以使用 kubernetes.io/hostname: minikube
的 nodeSelector 将 pod 运行在指定的物理机上(不建议)
3.6 注解(annotation)
类似 label 的 kv pair 注释,但不用作标识,用作资源说明(所以才会放在 pod metadata 的第一个子节点),可添加大量的数据块:
# 增删改查和 label 操作一样
> kubectl annotate pod kubia-gpu yinzige.com/gpu=10G
> kubectl describe pod kubia-gpu # 出现在 metadata.annotation
注:为避免标签或注解冲突,和 Java Class 使用倒序域名的方式类似,建议 key 中添加域名信息。
3.7 命名空间(namespace)
labels 会导致资源重叠,可用 namespace 将对象分配到集群级别的隔离区域,相当于多租户的概念,以 namespace 为操作单位。
> kubectl get ns # 获取所有 namespace,默认 default 下
> kubectl get pods -n kube-system # 获取指定 namespace 下的资源
# kubectl create namespace custom-namespace # 创建 namespace 资源
apiVersion: v1
kind: Namespace
metadata:
name: custom-namespace
apiVersion: v1
kind: Pod
metadata:
name: kubia-manual
namespace: custom-namespace # 创建资源时在 metadata.namespace 中指定资源的命名空间
spec: # ...
# 切换上下文命名空间
> kubectl config set-context $(kubectl config current-context) --namespace custom-namespace
3.8 删除 pod
删除原理:向 pod 所有容器进程定期发送 SIGTERM 信号,超时则发送 SIGKILL 强制终止。需在程序内部捕捉信号正确处理,如 Go 注册捕捉信号 signal.Notify()
后 select 监听该 channel
> kubectl delete pod -l env=debug # 删除指定标签的 pod
> kubectl delete pod --all # 删除当前 namespace 下的所有 pod (慎用)
> kubectl delete all --all # 删除所有类型资源的所有对象(慎用)
4. ReplicationController
4.1 容器存活探针(Liveness Probe)
将进程的重启监控从程序监控级别提升到 k8s 集群功能级别,使进程 OOM,死锁或死循环时能自动重启。pod 中各容器的探针用于暴露给 k8s ,来检查容器中的应用进程是否正常。分为 3 类:
- HTTP Get:指定 IP:Port 和 Path,GET 请求返回 5xx 或超时则认为失败。
- TCP Socket:是否能建立 TCP 连接。
- Exec:在容器中执行任意命令,检查
$?
是否为 0
apiVersion: v1
kind: Pod
metadata:
name: kubia-liveness
spec:
containers:
- image: wuyinio/kubia-unhealthy
name: kubia
livenessProbe:
httpGet: # 定义 http get 探针
path: / # 指定路径和端口号
port: 8080
initialDelaySeconds: 11 # 初次探测延迟 11s
存活探针原则:
- 为检查设立子路径
/health
,确保无认证。 - 保证探针返回失败时,错误发生在应用内且重启可恢复,而非应用外的组件导致的失败,那重启也没用。
注:非托管 Pod 仅由 Worker 节点的 kubelet 负责通过探针监控并重启,但整个节点崩溃会丢失该 Pod
4.2 ReplicationController
RC 监控 Pod 列表并根据模板增删、迁移 Pod。分为 3 部分:
- 标签选择器 label selector:确定要管理哪些 pod
- 副本数量 replica count:指定要运行的 pod 数量
- pod 模板 pod template:创建新的 pod 副本
注:修改标签选择器,会导致 rc 不再关注之前匹配的所有 pod。修改模板则只对新 Pod 生效(如手动 delete)
RC 的两个功能:
- 监控:确保符合标签选择器的 Pod 以指定的副本数量运行,多了则删除,少了则按 Pod 模板创建。
- 扩缩容:能对监控的某组 Pod 进行动态修改副本数量进行扩缩容。
apiVersion: v1
kind: ReplicationController
metadata:
name: kubia
spec:
replicas: 3 # pod 实例的期望数
selector: # 决定 rc 的操作对象,可省略。必须与模板 label 匹配,否则 `selector` does not match template `labels`
app: kubia
template:
metadata:
labels:
app: kubia
spec:
containers:
- name: kubia
image: wuyinio/kubia
ports:
- containerPort: 8080
操作 RC:
> kubectl describe rc kubia # 查看 rc 详细信息如 events
> kubectl edit rc kubia # 修改 rc 的 yaml 配置
> kubectl scale rc kubia --replicas=5 # 扩缩容
> kubectl delete rc kubia --ascade=false # 删除 rc 时保留运行中的 pod # ascade 级联(关联删除)
4.3 ReplicaSet
ReplicaSet = ReplicationController + 扩展的 label selector ,即对 pod 的 label selector 表达力更强 。
RS 能通过 selector.matchLabels
和 selector.matchExpressio
来扩展对 pod label 的筛选:
apiVersion: apps/v1
kind: ReplicaSet
metadata:
name: kubia
spec:
replicas: 3
selector:
matchLabels: # 与 RC 一样必须完整匹配
app: kubia
matchExpressions:
- key: app
operator: In # 必须有 KEY 且 VALUE 在列表中
values:
- kubia
- kubia-v2
- key: app
operator: NotIn # 有 KEY 则不能在如下列表中
values:
- KUBIA
- key: env # 必须存在的 KEY,不能有 VALUE
operator: Exists
- key: ENV
operator: DoesNotExist # 必须不能存在的 KEY,也不能有 VALUE
template:
metadata:
labels: # Pod 模板的 label 必须能和 RS 的 selector 匹配上
app: kubia
env: env_exists
spec:
containers:
- name: kubia
image: yinzige/kubia
ports:
- protocol: TCP
containerPort: 8080
4.4 DaemonSet
- 功能:保证标签匹配的 Pod 在符合 selector 的一个节点上运行一个,没有目标 pod 数量的概念,无法 scale
- 场景:部署系统级组件,如 Node 监控,如 kube-proxy 处理各节点网络代理等。
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: ssd-monitor
spec:
selector:
matchLabels:
app: ssd-monitor # 指定要控制运行的一组 Pod
template:
metadata:
labels:
app: ssd-monitor # 被控制的 Pod 的标签
spec:
nodeSelector: # 选择 Pod 要运行的节点标签
disk: ssd # 注意 YAML 文件的 true 类型是布尔型,如 ssd: true 是无法被解析为 String 的
containers:
- name: main
image: yinzige/ssd-monitor
4.5 Job
- 功能:保证任务以指定的并发数执行指定次数,任务执行失败后按配置策略处理。
- 场景:执行一次性任务。
apiVersion: batch/v1
kind: Job
metadata:
name: batch-job
spec:
completions: 5 # 总任务数量
parallelism: 2 # 并发执行任务数
template:
metadata:
labels:
app: batch-job # 要执行的 pod job label
spec:
restartPolicy: OnFailure # 任务异常结束或节点异常时处理方式:"Always", "OnFailure", "Never"
containers:
- name: main
image: yinzige/batch-job
4.6 CronJob
对标 Linux 的 crontab 的定时任务。
apiVersion: batch/v1beta1
kind: CronJob
metadata:
name: cron-batch-job
spec:
schedule: "*/1 * * * *" # 每分钟运行一次
jobTemplate:
spec:
template:
metadata:
labels:
app: cron-batch-job
spec:
restartPolicy: OnFailure
containers:
- name: cron-batch-job
image: yinzige/batch-job
5. Service
5.1 Service 内部解析
接入点隔离
由于 pod 调度后 IP 会变化,需使用 Service 服务给一组 Pod 提供不变的单一接入点 entrypoint,即 IP:Port
> kubectl expose rc kubia --type=LoadBalancer --name kubia-http # 通过服务暴露 ClusterIP pod 服务给外部访问
创建名为 kubia 的 Service,将其 80 端口的请求分发给有 app: kubia
标签 Pod 的自定义的 http 端口上:
apiVersion: v1
kind: Service
metadata:
name: kubia
spec:
sessionAffinity: ClientIP
ports:
- name: http
port: 80
targetPort: http
- name: https
port: 443
targetPort: https # defined in pod template.spec.containers.ports array
selector:
app: kubia
设置请求亲和性:保证一个 Client 的所有请求都只会落到同一个 Pod 上:
apiVersion: v1
kind: Service
metadata:
name: kubia-svc-session
spec:
sessionAffinity: ClientIP # or None default
ports:
- port: 80
targetPort: 8080
服务发现
客户端和 Pod 都需知道服务本身的 IP 和 Port,才能与其背后的 Pod 进行交互。
-
环境变量:
kubectl exec kubia-qgtmw env
会看到 Pod 的环境变量列出了 Pod 创建时的所有服务地址和端口,如 SVCNAME_SERVICE_HOST 和 SVCNAME_SERVICE_PORT 指向服务。 -
DNS 发现:Pod 上通过全限定域名 FQDN 访问服务:
<service_name>.<namespace>.svc.cluster.local
> kubectl exec kubia-qgtmw cat /etc/resolv.conf nameserver 10.96.0.10 search default.svc.cluster.local svc.cluster.local cluster.local # 会 options ndots:5
在 Pod kubia-qgtmw 中可通过访问
kubia.default.svc.cluster.local
来访问 kubia 服务,在/etc/resolv.conf
中指明了域名解析服务器地址,以及主机名补全规则,是在 Pod 创建时候,根据 namespace 手动导入的。
5.2 Service 对内部解析外部
集群内部的 Pod 不直连到外部的 IP:Port,而是同样定义 Service 结合外部 endpoints 做代理中间层解耦。如获取百度首页的向外解析:
1.1 建立外部目标的 endpoints 资源:
apiVersion: v1
kind: Endpoints
metadata:
name: baidu-endpoints
subsets:
- addresses:
- ip: 220.181.38.148 # baidu.com
- ip: 39.156.69.79
ports:
- port: 80
1.2 或者建立外部解析别名
apiVersion: v1
kind: Service
metadata:
name: baidu-endpoints
spec:
type: ExternalName
externalName: www.baidu.com
ports:
- port: 80
- 再建立同名的 Service 代理,标识使用上边这组 endpoints
apiVersion: v1
kind: Service
metadata:
name: baidu-endpoints
spec:
ports:
- port: 80
- 效果:在集群内部 Pod 上可透过名为 baidu-endpoints 的 Service 连接到百度首页:
# root@kubia-72sxt:/# curl 10.103.134.52
root@kubia-72sxt:/# curl baidu-endpoints
<html>
<meta http-equiv="refresh" content="0;url=http://www.baidu.com/">
</html>
注意 Service 类型:
> kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
baidu-endpoints ExternalName <none> www.baidu.com 80/TCP 30m
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 5d2h
kubia ClusterIP 10.96.239.1 <none> 80/TCP,443/TCP 87m
5.3 Service 对外部解析内部
5.3.1 NameNode
使用:外部客户端直连宿主机端口访问服务。
原理:在集群所有节点暴露指定的端口给外部客户端,该端口会将请求转发 Service 进一步转发给节点上符合 label 的 Pod,即 Service 从所有节点收集指定端口的请求并分发给能处理的 Pod
缺点:高可用性需由外部客户端保证,若节点下线需及时切换。
apiVersion: v1
kind: Service
metadata:
name: kubia-nodeport
spec:
type: NodePort
ports:
- port: 80
targetPort: 8080
nodePort: 30001
selector:
app: kubia
三个端口号,节点转发 30001,Service 转发 80:
宿主机即外部,执行 curl MINIKUBE_NODE_IP:30001
会被转发到有 app:kubia
标签的 Pod 的 8080 端口。执行 curl NAME_PORT:80
同理。
5.3.2 LoadBalancer
场景:外部客户端直连 LB 访问服务。其是 K8S 集群端高可用的 NameNode 扩展。
apiVersion: v1
kind: Service
metadata:
name: kubia-loadbalancer
spec:
type: LoadBalancer
ports:
- port: 80
targetPort: 8080
selector:
app: kubia
k8s app:kubia
所在的所有节点打开随机端口 32148,进一步转发给 Pod 的 8080 端口。
kubia-loadbalancer LoadBalancer 10.108.104.22 <pending> 80:32148/TCP 4s
5.4 Ingress
顶级转发代理资源,仅通过一个 IP 即可代理转发后端多个 Service 的请求。需要开启 nginx controller 功能
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
name: kubia
spec:
rules:
- host: "kubia.example.com"
http:
paths:
- path: /kubia # 将 /kubia 子路径请求转发到 kubia-nodeport 服务的 80 端口
backend:
serviceName: kubia-nodeport
servicePort: 80
- path: /user # 可配置多个 path 对应到 service
backend:
serviceName: user-svc
servicePort: 90
- host: "new.example.com" # 可配置多个 host
http:
paths:
- path: /
backend:
serviceName: gooele
servicePort: 8080
5.5 就绪探针
场景:pod 启动后并非立刻就绪,需延迟接收来自 service 的请求。若不定义就绪探针,pod 启动就会暴露给 service 使用,所以需像存活探针一样添加指定类型的就绪探针:
#...
spec:
containers:
- name: kubia-container
image: yinzige/kubia
readinessProbe:
exec:
command:
- ls
- /var/ready_now
5.6 headless
场景:向客户端暴露所有 pod 的 IP,将 ClusterIP 置为 None 即可:
apiVersion: v1
kind: Service
metadata:
name: kubia-headless
spec:
clusterIP: None
ports:
- port: 80
targetPort: 8080
selector:
app: kubia
k8s 不会为 headless 服务分配 IP,通过 DNS 可直接发现后端的所有 Pod
> kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubia LoadBalancer 10.104.138.112 <pending> 80:32110/TCP 123m
kubia-headless ClusterIP None <none> 80/TCP 10m
root@dnsutils:/# nslookup kubia-headless
Server: 10.96.0.10
Address: 10.96.0.10#53
Name: kubia-headless.default.svc.cluster.local
Address: 172.17.0.6
Name: kubia-headless.default.svc.cluster.local
Address: 172.17.0.12
Name: kubia-headless.default.svc.cluster.local
Address: 172.17.0.10
Name: kubia-headless.default.svc.cluster.local
Address: 172.17.0.8
root@dnsutils:/# nslookup kubia
Server: 10.96.0.10
Address: 10.96.0.10#53
Name: kubia.default.svc.cluster.local
Address: 10.104.138.112
Ch6. Volume
6.1 卷介绍
- 问题:Pod 中每个容器的文件系统来自镜像,相互独立。
- 解决:使用存储卷,让容器访问外部磁盘空间、容器间共享存储。
卷是 Pod 生命周期的一部分,不是 k8s 资源对象。Pod 启动时创建,删除时销毁(文件可选保留)。用于 Pod 中挂载到多个容器进行文件共享。
卷类型:
- emptyDir:存放临时数据的临时空目录。
- hostPath:将 k8s worker 节点的系统文件挂载到 Pod 中,常用于单节点集群的持久化存储。
- persistentVolumeClaim:PVC 持久卷声明,用于预配置 PV
6.2 emptyDir 卷
1 个 Pod 中 2 个容器使用同一个 emptyDir 卷 html,来共享文件夹,随 Pod 删除而清除,属于非持久化存储。
apiVersion: v1
kind: Pod
metadata:
name: fortune
spec:
containers:
- name: html-generator
image: yinzige/fortuneloop
volumeMounts:
- name: html
mountPath: /var/htdocs # 将 html 的卷挂载到 html-generator 容器的 /var/htdocs 目录
- name: web-server
image: nginx:alpine
volumeMounts:
- name: html
mountPath: /usr/share/nginx/html # 将 html 的卷挂载 web-server 容器到 /usr/share/nginx/html 目录
readOnly: true # 设置只读
volumes:
- name: html # 声明名为 emptyDir 的 emptyDir 卷
emptyDir: {}
emptyDir 卷跟随 Pod 被 k8s 自动分配在宿主机指定目录:/var/lib/kubelet/pods/PODUID/volumes/kubernetes.io~empty-dir/VOLUMENAME
如上的 html 卷位置在 minikube 节点:
$ sudo ls -l /var/lib/kubelet/pods/144c55eb-edf5-4b44-a2f6-a0d9cfe04f7c/volumes/kubernetes.io~empty-dir/html
total 4
-rw-r--r-- 1 root root 80 Apr 26 05:01 index.html
6.3 hostPath 卷
hostPath 卷的数据不跟随 Pod 生命周期,下一个调度至此节点的 Pod 能继续使用前一个 Pod 留下的数据,pod 和节点是强耦合的,只适合单节点部署。
6.5 持久化卷 PV、持久化卷声明 PVC
PV 与 PVC 用于解耦 Pod 与底层存储。PV、PVC 与底层存储关系:
流程:
- 管理员向集群加入节点时准备 NFS 等存储资源(TODO )
- 管理员创建指定大小和访问模式的 PV
- 用户创建需要大小的 PVC
- K8S 寻找符合 PVC 的 PV 并绑定
- 用户在 Pod 中通过卷引用 PVC,从而使用存储 PV 资源
Admin 通过网络存储创建 PV:
apiVersion: v1
kind: PersistentVolume # 创建持久卷
metadata:
name: mongodb-pv
spec:
capacity:
storage: 1Gi # 告诉 k8s 容量大小和多个客户端挂载时的访问模式
accessModes:
- ReadWriteOnce
- ReadOnlyMany
persistentVolumeReclaimPolicy: Retain # Cycle / Delete 标识 PV 删除后数据的处理方式
hostPath: # 持久卷绑定到本地的 hostPath
path: /tmp/mongodb
User 通过创建 PVC 来找到大小、容量均匹配的 PV 并绑定:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mongodb-pvc # pvc 名称将在 pod 中引用
spec:
resources:
requests:
storage: 1Gi
accessModes:
- ReadWriteOnce
storageClassName: "" # 手动绑定 PVC 到已存在的 PV,否则有值就是等待绑定到匹配的新 PV
User 创建 Pod 使用 PVC:
kind: Pod
metadata:
name: mongodb
spec:
containers:
- image: mongo
name: mongodb
volumeMounts:
- name: mongodb-data
mountPath: /tmp/data
ports:
- containerPort: 27017
protocol: TCP
volumes:
- name: mongodb-data
persistentVolumeClaim: # pod 中通过 claimName 指定要引用的 PVC 名称
claimName: mongodb-pvc
PV 设置卷的三种访问模式:
- RWO:ReadWriteOnly:仅允许单个节点挂载读写
- ROX:ReadOnlyMany :允许多个节点挂载只读
- RWX:ReadWriteMany:允许多个节点挂载读写
注:PV 是集群级别的存储资源,PVC 和 Pod 是命名空间范围的。所以,在 A 命名空间的 PVC 和在 B 命名空间的 PVC 都有可能绑到同一个 PV 上。
6.6 动态 PV:存储类 StorageClass
场景:进一步解耦 Pod 与 PVC,使 Pod 不依赖 PVC 名称,而且跨集群移植只需保证 SC 一致即可,不用管 PVC 和 PV。同时还能给不同 PV 进行归档如按硬盘属性进行分类。
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast
provisioner: k8s.io/minikube-hostpath # 指定 SC 收到创建 PVC 请求时应调用哪个组件进行处理并返回 PV
parameters:
type: pd-ssd
总流程:可创建 StorageClass 存储类资源,用于分类 PV,在 PVC 中绑定到符合条件的 PV 上。
7. 配置传递:ConfigMap 与 Secret
7.1 配置容器化应用程序
三种配置方式:
- 向容器传递命令行参数。
- 为每个容器设置环境变量。
- 通过卷将配置文件挂载至容器中。
容器传递配置文件的问题:修改配置需重新构建镜像,配置文件完全公开。解决:配置使用 ConfigMap 或 Secret 卷挂载。
7.2 向容器传递命令行参数
Dockerfile 中 ENTRYPOINT
为命令,CMD
为其参数。但参数能被 docker run <image> <arg_values>
中的参数覆盖。
ENTRYPOINT ["/bin/fortuneloop.sh"] # 在脚本中通过 $1 获取 CMD 第一个参数,Go 中 os.Args[1] 类似
CMD ["10", "11"]
二者等同于 Pod 中的 command
和 args
,但 pod 可通过 image 的 command 和 args 子标签进行覆盖,注意参数必须是字符串:
apiVersion: v1
kind: Pod
metadata:
name: fortune2s
spec:
containers:
- image: luksa/fortune:args
args: ["2"]
# ...
7.3 为容器设置环境变量
只能在各容器级别注入环境变量,而非 Pod 级别。配置容器部分 spec.containers.env
指定即可:
apiVersion: v1
kind: Pod
metadata:
name: fortune3s
spec:
containers:
- image: luksa/fortune:env
env:
- name: INTERVAL # 对应到容器 html-generator 中的 $INTERVAL
value: "5"
- name: "NESTED_VAR"
value: "$(INTERVAL)_1" # 可引用其他环境变量
name: html-generator
# ...
缺点:硬编码环境变量可能在多个环境下值不同无法复用,需将配置项解耦。
7.4 ConfigMap 卷
存储非敏感信息的文本配置文件。
# 创建 cm 的四种方式:可从 kv 字面量、配置文件、有命名配置文件、目录下所有配置文件
> kubectl create configmap fortune-config --from-literal=sleep-interval=25 # 从 kv 字面量创建 cm
> kubectl create configmap fortune-config --from-file=nginx-conf=my-nginx-config.conf # 指定 k 的文件创建 cm
两种方式将 cm 中的值传递给 Pod 中的容器:
7.4.1 设置环境变量或命令行参数
apiVersion: v1
kind: Pod
metadata:
name: fortune-env-from-configmap
spec:
containers:
- image: luksa/fortune:env
name: html-generator
env:
- name: INTERVAL # 取 CM fortune-config 中的 sleep-interval,作为 html-generator 容器环境变量 INTERVAL 的值
valueFrom:
configMapKeyRef:
name: fortune-config-cm
key: sleep-interval
envFrom: # 批量导入 cm 的所有 kv 作为环境变量,并加上前缀
- prefix: CONF_
configMapRef:
name: fortune-config-cm
# ...
可使用 kubectl get cm fortune-config -o yaml
查看 CM 的 data 配置项。
7.4.2 配置 ConfigMap 卷
当配置项过长需放入配置文件时,可将配置文件暴露为 cm 并用卷引用,从而在各容器内部挂载读取。
apiVersion: v1
kind: Pod
metadata:
name: fortune-configmap-volume
spec:
containers:
- name: html-generator
image: yinzige/fortuneloop:env
env:
- name: INTERVAL
valueFrom:
configMapKeyRef:
key: sleep-interval # raw key file name
name: fortune-config # cm name
volumeMounts:
- mountPath: /var/htdocs
name: html
- name: web-server
image: nginx:alpine
volumeMounts:
- mountPath: /usr/share/nginx/html
name: html
readOnly: true
- mountPath: /etc/nginx/conf.d/gzip_in.conf
name: config
subPath: gzip.conf # 使用 subPath 只挂载部分卷 gzip.conf 到指定目录下指定文件 gzip_in.conf
readOnly: true
ports:
- containerPort: 80
name: http
protocol: TCP
volumes:
- name: html
emptyDir: {}
- name: config
configMap:
name: fortune-config # cm name
defaultMode: 0666 # 设置卷文件读写权限
items: # 使用 items 限制从 cm 暴露给卷的文件
- key: my-nginx-config.conf
path: gzip.conf # 把 key 文件的值 copy 一份到新文件中
添加 items
来暴露指定的文件到卷中,subPath
用来挂载部分卷,而不隐藏容器目录原有的初始文件。
> kubectl exec fortune-configmap-volume -c web-server -it -- ls -lA /etc/nginx/conf.d
total 8
-rw-r--r-- 1 root root 1093 Apr 14 14:46 default.conf # subPath
-rw-rw-rw- 1 root root 242 Apr 27 16:49 gzip_in.conf
7.4.3 ConfigMap 场景
使用 kubectl edit cm fortune-config
修后,容器中对应挂载的卷文件会延迟将修改同步。问题:若 pod 应用不支持配置文件的热更新,那同步了的修改并不会再旧 pod 生效,反而新起的 pod 会生效,造成新旧配置共存的问题。
场景:cm 的特性是不变性,若 pod 应用本身支持热更新,则可修改 cm 动态更新,但注意有 k8s 的监听延迟。
7.5 Secret
存储敏感的配置数据,大小限制 1MB,其配置条目会以 Base64 编码二进制后存储:
> kubectl create secret generic fortune-auth --from-file=fortune-auth/ # password.txt
在 pod 中加载:
apiVersion: v1
kind: Pod
metadata:
name: fortune-with-serect
spec:
containers:
- name: fortune-auth-main
image: yinzige/fortuneloop
volumeMounts:
- mountPath: /tmp/no_password.txt
subPath: password.txt
name: auth
volumes:
- name: auth
secret:
secretName: fortune-auth
读取正常:
> kubectl exec fortune-with-serect -it -- ls -lh /tmp
total 4.0K
-rw-r--r-- 1 root root 10 Apr 27 17:51 no_password.txt
> kubectl exec fortune-with-serect -it -- mount | grep password
tmpfs on /tmp/no_password.txt type tmpfs (ro,relatime) # secret 仅存储在内存中
secret 可用于从镜像仓库中拉取 private 镜像,需配置专用的 secret 使用:
apiVersion: v1
kind: Pod
spec:
imagePullSecrets:
- name: dockerhub-secret
containers: # ...
8. Pod Metadata 与 k8s API
场景:从 Pod 中的容器应用进程访问 Pod 元数据及其他资源。
8.1 使用 Downward API 传递 Pod Metadata
问题:配置数据如环境变量、ConfigMap 都是预设的,应用内无法直接获取如 Pod IP 等动态数据。
解决:Downward API 通过环境变量、downward API 卷来传递 Pod 的元数据。如:Pod 名称、IP、命名空间、标签、节点名、每个容器的 CPU、内存限制及其使用量。
8.1.1 环境变量透传
在 Pod 定义中手动将容器需要的元数据,以环境变量的形式手动透传给容器:
apiVersion: v1
kind: Pod
metadata:
name: downward
spec:
containers:
- name: main
image: busybox
command:
- "sleep"
- "1000"
env:
- name: E_POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: E_POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP # 运行时元数据
- name: E_REQ_CPU
valueFrom:
resourceFieldRef: # 引用容器级别的数据,如请求的 CPU、内存用量等需引用 resourceFieldRef
resource: requests.cpu
divisor: 1m # 资源单位
- name: E_LIMIT_MEM
valueFrom:
resourceFieldRef:
resource: limits.memory
divisor: 1Ki
效果:
k exec downward -it -- env | grep -e "^E_"
E_POD_NAMESPACE=default
E_POD_IP=172.17.0.8
E_REQ_CPU=0
E_LIMIT_MEM=2085844
缺点:无法通过环境传递 pod 标签和注解等可在运行时动态修改的元数据。
ERROR: error converting fieldPath: field label not supported: metadata.lebels.app
8.1.2 通过 downwardAPI 卷
apiVersion: v1
kind: Pod
metadata:
name: downward
labels:
foo: bar
annotations:
k1: v1
spec:
containers:
- name: main
# ...
volumeMounts:
- name: downward
mountPath: /etc/downward
volumes:
- name: downward # 定义一个名为 downward 的 DownwardAPI 卷,将元数据写入 items 下指定路径的文件中
downwardAPI:
items:
- path: "container_request_memory"
resourceFieldRef:
containerName: main # 容器级别的元数据需指定容器名
resource: requests.cpu
divisor: 1m
divisor: 1m
- path: "labels" # "annotations" # pod 的标签和注解必须使用 downwardAPI 卷去访问
fieldRef:
fieldPath: metadata.labels
效果:k8s 会自动地将 pod 的标签和注解同步到 downward API 卷的指定文件中。
> kubectl exec downward-volume-pod -it -- ls /etc/downward
container_request_memory pod_annotations pod_labels
> kubectl exec downward-volume-pod -it -- cat /etc/downward/pod_annotations
key1="VALUE1"
key2="VALUE2\nVALUE20\nVALUE200\n"
kubernetes.io/config.seen="2020-04-28T04:15:29.722938998Z"
kubernetes.io/config.source="api"
> kubectl annotate pod downward-volume-pod new_key=NEW_VALUE
pod/downward-volume-pod annotated
> kubectl exec downward-volume-pod -it -- tail -2 /etc/downward/pod_annotations
kubernetes.io/config.source="api"
new_key="NEW_VALUE"
8.2 与 k8s API 交互
问题:downward API 只能向应用暴露 1 个 Pod 的部分元数据,无法提供其他 Pod 和其他资源信息。
请求 k8s API:
- 外部:通过 kubectl proxy 中间请求转发代理。
- 内部:从 Pod 内验证 Secret 卷的 crt 证书、传递 token 到来对本 namespace 内的资源进行操作。
Pod 与 API 服务器交互流程:
- 应用通过 secret 卷下的
ca.crt
验证 API 地址 - 应用带上 TOKEN 授权
- 操作 pod 所在命名空间内的资源
> ls /var/run/secrets/kubernetes.io/serviceaccount/
ca.crt # 验证 API 服务器证书,避免中间人攻击
namespace # 获取本地 pod 的 namespace: default
token # 通过 header 添加 "Authorization: Bearer $TOKEN" 方式来获取授权
> curl --cacert /var/run/secrets/kubernetes.io/serviceaccount/ca.crt https://kubernetes # 验证 API 地址
> export CURL_CA_BUNDLE=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
> TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
> curl -H "Authorization: Bearer $TOKEN" https://kubernetes # 授权
> NS=$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace)
> curl -H "Authorization: Bearer $TOKEN" https://kubernetes/api/v1/namespaces/$NS/pods # 操作本地命名空间资源
折中的 Pod 内部公共透明代理模式:在 pod 内部通过 ambassador 公共容器简化 API 服务器验证流程。普通容器走 HTTP 到 ambassador 容器,后者走 secret 流程走 HTTPS 与 k8s API 交互。
9. Deployment
场景:定义比例滚动升级,出错自动回滚。
9.1 纯手动更新运行在 Pod 内的应用程序
版本升级方式:
-
先删旧 Pod,再自动创建新 Pod:存在不可用间隔。
修改 Pod spec.template 中的标签选择器,选择新版 tag 对应的应用,先手动删除旧版本,RS 自动创建新版容器。
-
创建新 Pod,同时逐步剔除旧 Pod:需两个版本应用都能对外服务,短时间内 Pod 数量翻倍。
修改 Service 的 pod selector 蓝绿部署进度可控地将流量切换到新 Pod 上。
9.2 基于 RC 的滚动升级
问题:手动脚本将旧 Pod 缩容,新 Pod 扩容,易出错。
解决:使用 kubectl 请求 k8s API 来执行滚动升级:
# 指定需更新的 RC kubia-v1,用指定的 image 创建的新 RC 来替换
> kubectl rolling-update kubia-v1 kubia-v2 --image=wuyinio/kubia:v2 # 从 1.8 已移除
过程:kubectl 为新旧 Pod、新旧 RC 添加 deployment 标签,并向 k8s API 请求对旧 Pod 进行缩容,对新 Pod 扩容,透明地将 service 的标签匹配到新 pod 上。
原理:由客户端 kebectl 动态地修改两个 RC 的标签,缩容旧 Pod,扩容新 Pod,最终完成流量转移。
9.3 使用 Deployment 声明式升级
问题:RC 滚动升级是 kubectl 客户端控制升级,若网络断开连接则升级中断(如关闭终端),Pod 和 RC 会处于多版本混合态。
解决:在服务端使用高级资源 Deployment 声明来协调新旧两个 RS 的扩缩容,用户只定义最终目标收敛状态。
deployment 也分为 3 部分:标签选择器、期望副本数、Pod 模板:
apiVersion: apps/v1
kind: Deployment
metadata:
name: kubia
spec:
replicas: 3
template:
metadata:
name: kubia
labels:
app: kubia
spec:
containers:
- name: nodejs
image: yinzige/kubia:v1
selector:
matchLabels:
app: kubia
# 创建 deployment
> kubectl create -f kubia-deployment-v1.yaml --record # record 选项将记录历史版本号,用于后续回滚
> kubectl rollout status deployment kubia # rollout 显示部署状态
# deployment 用 pod 模板的哈希值创建 RS,再由 RS 创建 Pod 并管理,哈希值一致
> kubectl get pods
NAME READY STATUS RESTARTS AGE
kubia-5b9f8f4d84-nxmqd 1/1 Running 0 2s
kubia-5b9f8f4d84-q5wc5 1/1 Running 0 2s
kubia-5b9f8f4d84-r866t 1/1 Running 0 2s
> kubectl get rs
NAME DESIRED CURRENT READY AGE
kubia-5b9f8f4d84 3 3 3 9s
deployment 的升级策略:spec.stratagy
- RollingUpdate:渐进式删除旧 Pod。新旧版混合
- Recreate:一次性删除所有旧 Pod,重建新 Pod。中间服务不可用
9.3.1 触发滚动升级
先指定 Pod 就绪后的等待时间: kubectl patch deployment kubia -p '{"spec": {"minReadySeconds": 10}}'
修改某个容器的镜像来触发升级:kubectl set image deployment kubia nodejs=yinzige/kubia:v2
注:触发升级需真正修改到 deployment 的字段。
原理:kubectl get rs
可看到保留了的新旧版 rs,deployment 资源在 k8s master 端会自动控制新旧 RS 的扩缩容。
9.3.2 回滚
> kubectl rollout undo deployment kubia # 回滚到上一次 deployment 部署的版本
> kubectl rollout history deployment kubia # 创建 deployment 时 --record,此处显示版本
> kubectl rollout undo deployment kubia --to-reversion=1 # 回滚到指定 REVERSION,若手动删除了 RS 则无法回滚
> kubectl rollout pause deployment kubia # 暂停升级,在新 Pod 上进行金丝雀发布验证
> kubectl rollout resume deployment kubia # 恢复
9.4 结合探针控制升级速度
可配置 maxSurge
和 maxIUnavailable
来控制升级的最大超意外的 Pod 数量、最多容忍不可用 Pod 数量。
apiVersion: apps/v1
kind: Deployment
metadata:
name: kubia
spec:
replicas: 3
minReadySeconds: 10 # 新 Pod 就绪需等待 10s,才能继续滚动升级
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1 # 只允许最多超出一个非预期 pod,即只能逐个更新
maxUnavailable: 0 # 不允许有不可用的 Pod,以确保新 Pod 能逐个替换旧的 Pod
template:
metadata:
name: kubia
labels:
app: kubia
spec:
containers:
- name: nodejs
image: yinzige/kubia:v3 # 接收请求 5s 后返回 500
readinessProbe:
periodSeconds: 1 # 定义 HTTP Get 就绪探针每隔 1s 执行一次
httpGet:
port: 8080
path: /
selector:
matchLabels:
app: kubia
# apply 对象不存在则创建,否则修改对象属性
> kubectl apply -f kubia-deployment-v3-with-readinesscheck.yaml
使用上述 deployment 从正常版 v2 升级到 bug 版 v3,据配置 v3 的第一个 Pod 会创建并在第 5s 被 Service 标记为不可用,将其从 endpoint 中移除,请求不会分发到该 v3 Pod,10s 内未就绪,最终部署自动停止。
如下:kubia-54f54bf655 并未加入到 kubia Service 的 endpoints 中
> kubectl get endpoints
NAME ENDPOINTS AGE
kubia 172.17.0.12:8080,172.17.0.6:8080,172.17.0.8:8080 140m
> kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE
kubia-54f54bf655-tgvt9 0/1 Running 0 63s 172.17.0.7 m01 <none>
kubia-b669c877-8kx8c 1/1 Running 0 2m17s 172.17.0.6 m01 <none>
kubia-b669c877-957fl 1/1 Running 0 113s 172.17.0.12 m01 <none>
kubia-b669c877-n7hnb 1/1 Running 0 2m4s 172.17.0.8 m01 <none>
10. Stateful Set
场景:在有状态分部署存储应用中,Pod 的多副本有各自独立的 PVC 和 PV,pod 在新节点重建后需保证状态一致。
10.2 保证状态一致
-
一致的网络标识
sts 创建的 pod 名字后缀按顺序从 0 递增,通常通过 headless Service 暴露整个集群的 pod,每个 pod 有独立的 DNS 记录,pod 重建后保证名称和主机名一致。
-
一致的存储
sts 有 pod 模板和 PVC 模板,每个 pod 会绑定到唯一的 PVC 和 PV,重建后新 pod 会绑定到旧 PVC 和 PV 复用旧的存储。
10.3 使用 sts
三种必需资源:PV(若没有默认的 provisioner,则须手动创建)、控制 Service、sts 自身
-
创建 headless
apiVersion: v1 kind: Service metadata: name: kubia spec: clusterIP: None selector: app: kubia ports: - port: 80 name: http
-
创建 sts:PVC 模板会在 pod 运行前创建,并且绑定到默认 storage class 的 provisioner 创建的 PV 上
apiVersion: apps/v1 kind: StatefulSet metadata: name: kubia spec: serviceName: kubia # 绑定到 kubia 的 headless service replicas: 2 template: metadata: labels: app: kubia spec: containers: - name: kubia image: yinzige/kubia-pet ports: - containerPort: 8080 name: http volumeMounts: - mountPath: /var/data # PVC 绑定到 pod 目录 name: data volumeClaimTemplates: # 动态 PVC 模板,运行时提前创建 - metadata: name: data spec: resources: requests: storage: 1Mi accessModes: - ReadWriteOnce selector: matchLabels: app: kubia
注:sts 的创建或 scale 扩缩容,都是一次只操作一个 Pod 避免出现竞争、数据不一致的情况,pod 操作顺序与副本数顺序增减一致。
11. K8S 组件
k8s 中各组件通过 API 服务器的 event 事件流的通知机制进行解耦,各组件之间不会直接通信,相互透明。组件:
-
etcd:分布式一致性 KV 存储,只与 API Server 交互,存储集群各种资源元数据。
-
API Server:提供对集群资源的 CURD 接口,推送资源变更的事件流到监听端。
-
Scheduler:监听 Pod 创建事件,筛选出符合条件的节点并选出最优节点,更新 Pod 定义后发布给 API Server
-
各种资源的 Controller:监听资源更新的事件流,检查副本数,同样更新元数据发布给 API Server
-
kubectl:注册 Node 等待分配 Pod,告知 Docker 拉取镜像运行容器,随后向 API Server 会报 Pod 状态及指标
-
kube-proxy:代理 Service 暴露的 IP 和 Port
11.2 Pod 创建流程
各控制器间通过 API Server 进行解耦:
11.6 高可用
API Server 和 etcd 可有多节点。
为避免并发竞争,各控制器同一时间只能有一个实例运行,通过竞争写注解字段的方式进行选举,调度器同理。