引言
本文探讨了k8s核心技术,包括Controller
、Service
、Pod
、yaml
、Ingress
等。
- k8s概念和架构
- 从零搭建k8s集群
- k8s核心技术
命令行工具kubectl
可以使用kubectl管理你k8s集群。在从零搭建k8s集群中
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config # kubeadm 安装k8s
cp kube_config_rancher-cluster.yml ~/.kube/config # rke安装k8s
都有上面这样一个复制文件的操作。因为kubectl
会在$HOME/.kube
目录中查找名为config
的配置文件。
语法
kubectl [command] [TYPE] [NAME] [flags]
-
command
: 指定要对一个或多个资源执行的操作,例如 create、get、describe、delete -
type
: 指定资源类型。可以用kubectl api-resources
查看所有资源类型。资源类型不区分大小写, 可以指定单数、复数或缩写形式。例如,以下命令输出相同的结果:
kubectl get pod pod1
kubectl get pods pod1
kubectl get po pod1
-
name
: 指定资源的名称。名称区分大小写。 如果省略名称,则显示所有资源的详细信息 kubectl get pods
。
- 对所有类型相同的资源进行分组,执行:
TYPE name1 name2 name<#>
- 例子:
kubectl get pod example-pod1 example-pod2
- 分别指定多个资源类型:
TYPE1/name1 TYPE1/name2 TYPE2/name3 TYPE<#>/name<#>
- 例子:
kubectl get pod/example-pod1 replicationcontroller/example-rc1
- 用一个或多个文件指定资源:
-f file1 -f file2 -f file<#>
- 建议使用
YAML
格式而不是json
,因为前者更容易使用,特别是用于配置文件时。例如:kubectl get -f ./pod.yaml
-
flags
: 指定可选的参数。例如,可以使用 -s
或 -server
参数指定 Kubernetes API 服务器的地址和端口。
使用kubectl --help
可以看到常用命令信息:
kubectl --help
kubectl controls the Kubernetes cluster manager.
Find more information at: https://kubernetes.io/docs/reference/kubectl/overview/
Basic Commands (Beginner):
create 通过文件名或标准输入创建资源
expose 将一个资源公开为一个新的Service
run 在集群中运行一个特定的进行
set 在对上上设置特定的功能
Basic Commands (Intermediate):
explain 资源的文档参考资料
get 显示一个或多个资源
edit 使用默认的编辑器编辑一个资源
delete 通过文件名、标准输入、资源名称或标签选择器来删除资源
Deploy Commands:
rollout 管理资源的发布
scale 扩容或缩容pod数量,Deployment、ReplicaSet、RC或Job
autoscale 创建一个自动选择扩容或缩容并设置Pod数量
Cluster Management Commands:
certificate 修改证书资源
cluster-info 显示集群信息
top 显示资源(CPU/Memory/Storage) 使用
cordon 标记节点不可调度
uncordon 标记节点可调度
drain 驱逐节点上的应用,准备下线维护
taint 修改节点taint标记
Troubleshooting and Debugging Commands:
describe 显示特定资源或资源组的详细信息
logs 在一个pod种打印一个容器的日志
attach 附加到一个运行的容器
exec 在容器中执行命令
port-forward 转发一个或多个本地端口到一个pod
proxy 运行一个proxy到API Server
cp 从容器中拷贝文件或目录/拷贝文件或目录到容器中
auth 检查授权
Advanced Commands:
diff 显示当前版本和将要应用版本的区别
apply 通过文件名或标准输入配置资源应用
patch 使用合并补丁策略更新资源上的字段
replace 通过文件名或标准输入替换一个资源
wait 实验性的:在一个或多个资源上等待特定条件
convert 不同API版本之间转换配置文件
kustomize 通过目录或远程url构建一个kustomization 目标
Settings Commands:
label 更新资源上的标签
annotate 更新资源上的注释
completion 用于实现kubectl工具自动补全
Other Commands:
alpha 用户实验特性
api-resources 打印受支持的API资源
api-versions 打印受支持的API版本
config 修改kubeconfig文件
plugin 运行一个命令行查看
version 打印客户端和服务端版本信息
Usage:
kubectl [flags] [options]
Use "kubectl <command> --help" for more information about a given command.
Use "kubectl options" for a list of global command-line options (applies to all commands).
资源编排(yaml)介绍
k8s集群中对资源管理和资源你对象编排部署都可以通过声明样式(YAML)文件来解决,也就是可以把需要对资源对象操作编辑到YAML格式文件中,我们把这种文件叫做资源清单文件,通过kubectl
命令直接使用资源清单文件就可以实现对大量的资源对象进行编排部署。
YAML文件书写格式
基本语法:
- 使用(一般两个)空格作为缩进
- 相同层级的元素左侧对其
- 使用
#
标识注释 - 使用
---
表示新的YAML文件
YAML文件组成部分
以一个例子来描述:
apiVersion: apps/v1 # 创建该资源(对象)所使用的Kubernetes API的版本
kind: Deployment # 资源类型
metadata: # 资源元数据:帮助唯一标识资源的一些数据
name: nginx-deployment # 资源名称
namespace: default # 属于的命名空间
spec: # 资源规约(规格)
selector: # 标签选择器
matchLabels: # 匹配的标签
app: nginx # 标签的形式
replicas: 2 # 副本数量,指定需要运行两个pod
# 上面是控制器的定义
# 下面是被控制的对象
template: # pod模板
metadata: # pod元数据
labels: # 标签,支持高效的查询和监听
app: nginx # 标签通常是键值对的形式
spec: # pod规格
containers: # 容器配置
- name: nginx # 名称
image: nginx:1.14.2 #镜像
ports: # 端口
- containerPort: 80 # 容器端口
生成YAML文件
从上小节我们可以大概了解yaml文件里的东西是干什么的。但是如果让你去写一个这样的文件是很难且容易出错的。所以k8s提供了快速编写yaml文件的方法。
使用kubectl create命令生成yaml文件
kubectl create deployment web --image=nginx \
-o yaml # 输出yaml文件 \
--dry-run=client \ # 并不真正执行创建操作,仅用于生成文件
> web.yaml # 输出到web.yaml中
执行结果如下:
yjw@rancher1:~/temp$ kubectl create deployment web --image=nginx -o yaml --dry-run=client
apiVersion: apps/v1
kind: Deployment
metadata:
creationTimestamp: null
labels:
app: web
name: web
spec:
replicas: 1
selector:
matchLabels:
app: web
strategy: {}
template:
metadata:
creationTimestamp: null
labels:
app: web
spec:
containers:
- image: nginx
name: nginx
resources: {}
status: {}
使用kubectl get命令导出yaml文件
使用kubectl get
命令导出运行的deployment配置,首先查看有没有运行的:
yjw@rancher1:~/temp$ kubectl get deploy
NAME READY UP-TO-DATE AVAILABLE AGE
nginx 1/1 1 1 3m14s
然后执行下面的命令导出yaml文件到nginx.yaml
中:
kubectl get deploy nginx -o=yaml > nginx.yaml #
查看导出的文件:
yjw@rancher1:~/temp$ cat nginx.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
deployment.kubernetes.io/revision: "1"
field.cattle.io/publicEndpoints: '[{"addresses":["192.168.1.6"],"port":30727,"protocol":"TCP","serviceName":"default:nginx","allNodes":true}]'
creationTimestamp: "2021-04-11T05:46:57Z"
generation: 2
labels:
app: nginx
managedFields:
- apiVersion: apps/v1
fieldsType: FieldsV1
... # 文件太长
在配置文件中我们知道replicas: 2
是指定需要运行的pod数量,那pod具体是什么呢
pod
pod概述
Pod 是可以在 Kubernetes 中创建和管理的、最小的可部署的计算单元,是资源对象模型中由用户创建或部署的最小资源对象模型,也是在k8s上运行容器化应用的资源对象。
其他的资源对象都是用来支撑或扩展pod对象功能的,比如控制器对象时用来管控pod对象的,Service或Ingress资源是用来暴露pod引用对象的,PersistentVolume资源对象时用来为pod提供存储等。
k8s不会直接处理容器(container),而是pod。
一个pod包含一个或多个应用容器, 这些容器是相对紧密的耦合在一起的。
pod中所有容器具有相同的IP地址和端口空间(共享网络)。
pod被认为是临时的。可替代的实体,需要的话可以被丢弃或替换。
每个pod都有一个特殊的被称为根容器的Pause容器。除了Pause容器,每个pod还包含一个或多个紧密相关的用户业务容器。
pod存在意义
我们知道docker也能创建容器,一个docker对应一个容器,一个容器有一个进程,一个进程运行一个容器,即一个容器运行一个应用程序。为什么k8s不直接管理docker容器,而是提出pod概念呢。
pod是多进程设计,可以运行多个应用程序。而同一pod中网路是共享的,所以易于实现pod内多个应用程序的交互。
使用pod的好处有以下四点:
- 透明性:使pod内的容器对基础设置可见,使得基础设施能够向这些容器提供服务,例如流程管理和资源监控
- 解耦软件依赖:可以独立地对单个容器进行版本化、重建和重新部署
- 易用性:用户无须运行自由进程管理器,也不需要担心信号和退出代码的事务传播等。
- 效率:由于基础设施将承担更多的职责,因此容器可以更加轻量
pod两种实现机制
像pod这样一个东西,本身是一个逻辑概念。那么在机器上,它究竟是如何实现的。本节就来探讨这个问题。
要实现pod这个东西,核心就在于如何让一个pod里的多个容器之间高效的共享某些资源和数据。
因为容器之间原本是诶Linux Namespace和cgroups隔离开的,所以现在要解决的是如何打破这个隔离,然后共享某些信息。这就是pod的设计要解决的核心问题所在。
所以说具体的解法分为两个部分:网络和存储。
共享网络
Pause容器就是为解决pod中的网络问题而生的。
假设现在有一个 Pod,其中包含了一个容器 A 和一个容器 B,它们两个要共享 Network Namespace。在 Kubernetes 里的解法是这样的:它会在每个 Pod 里,额外起一个 Infra container 小容器(就是Pause容器)来共享整个 Pod 的 Network Namespace。
Infra container 是一个非常小的镜像,是一个C语言写的、永远处于“暂停”(pause)状态的容器。由于有了这样一个 Infra container 之后,其他所有容器都会通过 Join Namespace 的方式加入到 Infra container 的 Network Namespace 中。
所以说一个 Pod 里面的所有容器,它们看到的网络视图是完全一样的。即:它们看到的网络设备、IP地址、Mac地址等等,跟网络相关的信息,其实全是一份,这一份都来自于 Pod 第一次创建的这个 Infra container。这就是 Pod 解决网络共享的一个解法。
所以整个pod里面,必然是Infra container第一个启动。且整个pod生命周期等同于Infra container的生命周期,而与容器A和B无关的。这也是为什么在k8s里面,允许单独更新pod里的某一个镜像,这是非常重要的一个设计。
共享存储
pod上存储是临时的,会随pod一起消失。但有些数据,比如日志数据或业务数据等,需要在pod上存储更长的时间,或者在pod间传递数据,存储卷(Volumn)的概念便支持了这种需求。
Pod中的应用容器可以共享Volumn。Volume能够保证pod重启时使用的数据不丢失(比如说某台机器宕机了,运行在它上面的pod就需要转移到其他机器上,此时可以读取之前的数据就很重要)。
pod 镜像拉取策略
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
containers:
- name: nginx
image: nginx:1.14
imagePullPolicy:
其中imagePullPolicy
就是镜像拉取策略,现在有三种策略:
-
IfNotPresent
: 默认值,镜像在宿主机上不存在时才拉取 -
Always
: 每次创建pod都会重新拉取一次镜像 -
Never
: pod永远不会主动拉取这个镜像
pod资源限制
假设pod需要2C/4G (2核4G内存),而现在有三个节点:
- node1 : 2C/4G
- node2 : 4C/16G
- node3: 1C/2G
那么只能部署到node1和node2上,这种就是资源限制。
在k8s中,通过yaml文件可以这样声明资源限制:
apiVersion: v1
kind: Pod
metadata:
name: memory-demo
namespace: mem-example
spec:
containers:
- name: memory-demo-ctr
image: polinux/stress
resources:
limits:
memory: "200Mi"
cpu: "500m"
requests:
memory: "100Mi"
cpu: "250m"
这里包含了请求(request)和限制(limit)。可以保证容器拥有它请求的资源,但不允许超过限制的资源。
本例中容器请求100M的内存和250m的CPU。 同时限制最多分配200M内存和500m的CPU。
1核 CPU可以理解为1000M 大小。
当节点拥有足够的可用内存时,容器可以使用其请求的内存。 但是,容器不允许使用超过其限制的内存。 如果容器分配的内存超过其限制,该容器会成为被终止的候选容器。 如果容器继续消耗超出其限制的内存,则终止容器。
下面以官网的示例为例,看一下当容器尝试分配超过其限制的内存时会怎么样:
yjw@rancher1:~/temp$ cat memory-request-limit.yaml
apiVersion: v1
kind: Pod
metadata:
name: memory-demo-2
# namespace: mem-example 注释掉,使用默认命名空间
spec:
containers:
- name: memory-demo-2-ctr
image: polinux/stress
resources:
requests:
memory: "50Mi"
limits:
memory: "100Mi"
command: ["stress"]
args: ["--vm", "1", "--vm-bytes", "250M", "--vm-hang", "1"] # 尝试分配250M内存
应用它:
kubectl apply -f memory-request-limit.yaml
可以查看这个pod,发现它在8分钟内重启了6次,并且还是CrashLoopBackOff
状态。
yjw@rancher1:~/temp$ kubectl get pod memory-demo-2
NAME READY STATUS RESTARTS AGE
memory-demo-2 0/1 CrashLoopBackOff 6 8m23s
执行kubectl describe nodes
可以看到很多关于:
Warning SystemOOM 9m20s kubelet System OOM encountered, victim process: stress, pid: 115325
Warning SystemOOM 9m15s kubelet System OOM encountered, victim process: stress, pid: 115459
Warning SystemOOM 8m48s kubelet System OOM encountered, victim process: stress, pid: 115681
Warning SystemOOM 8m16s kubelet System OOM encountered, victim process: stress, pid: 115930
Warning SystemOOM 7m19s kubelet System OOM encountered, victim process: stress, pid: 116314
Warning SystemOOM 5m52s kubelet System OOM encountered, victim process: stress, pid: 116906
Warning SystemOOM 3m5s kubelet System OOM encountered, victim process: stress, pid: 117918
可以看到容器由于内存溢出而被干掉的记录。最后别忘了执行命令删掉这个pod:
yjw@rancher1:~/temp$ kubectl delete pod memory-demo-2
pod "memory-demo-2" deleted
pod重启策略
apiVersion: batch/v1
kind: Job
metadata:
name: pi
spec:
template:
spec:
containers:
- name: pi
image: perl
command: ["perl", "-Mbignum=bpi", "-wle", "print bpi(2000)"]
restartPolicy: Never
backoffLimit: 4
其中restartPolicy
是pod重启策略,取值有:
-
Always
: 默认值,当容器终止退出后,总是重启容器。适用于一直需要提供服务的场景 -
OnFailure
: 当容器异常退出时,才重启容器 -
Never
: 当容器终止退出,从不重启容器。适用于一次性任务
上面是一个 Job 配置示例。它负责计算 π 到小数点后 2000 位,并将结果打印出来。
这种任务只需要运行一次,因此我们指定为Never
。
pod健康检查
许多长时间运行的应用程序最终会过渡到断开的状态,除非重新启动,否则无法恢复。 Kubernetes 提供了存活探测器来发现并补救这种情况。
kubelet 使用存活探测器来知道什么时候要重启容器。 例如,存活探测器可以捕捉到死锁(应用程序在运行,但是无法继续执行后面的步骤)。 这样的情况下重启容器有助于让应用程序在有问题的情况下更可用。
k8s有两种检查机制:
- 如果检查失败,将杀死容器,根据pod的
restartPolicy
来操作
- 如果检查失败,k8s会把pod从service endpoints中剔除
而Probe又支持三种检查方法。
exec
执行Shell命令返回状态码是0为成功。
下面是一个livenessProbe
的例子。
来自官方例子
livenessProbe.yaml
:
apiVersion: v1
kind: Pod
metadata:
labels:
test: liveness
name: liveness-exec
spec:
containers:
- name: liveness
image: anjia0532/busybox # 官方的镜像可能下载不下来,可以替换成这个
args:
- /bin/sh
- -c
- touch /tmp/healthy; sleep 30; rm -rf /tmp/healthy; sleep 600
livenessProbe:
exec:
command:
- cat
- /tmp/healthy
initialDelaySeconds: 5
periodSeconds: 5
在这个配置文件中,可以看到 Pod 中只有一个容器。 periodSeconds
字段指定了 kubelet 应该每 5 秒执行一次存活探测。 initialDelaySeconds
字段告诉 kubelet 在执行第一次探测前应该等待 5 秒。 kubelet 在容器内执行命令 cat /tmp/healthy
来进行探测。 如果命令执行成功并且返回值为 0,kubelet 就会认为这个容器是健康存活的。 如果这个命令返回非 0 值,kubelet 会杀死这个容器并重新启动它。
这个容器生命的前 30 秒, /tmp/healthy
文件是存在的。 所以在这最开始的 30 秒内,执行命令 cat /tmp/healthy
会返回成功代码。 30 秒之后,执行命令 cat /tmp/healthy
就会返回失败代码。
创建pod:
kubectl apply -f livenessProbe.yaml
在 30 秒内,查看 Pod 的事件:
kubectl describe pod liveness-exec
输出结果表明还没有存活探测器失败:
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 28s default-scheduler Successfully assigned default/liveness-exec to 192.168.1.7
Normal Pulling 27s kubelet Pulling image "anjia0532/busybox"
Normal Pulled 7s kubelet Successfully pulled image "anjia0532/busybox" in 19.559680245s
Normal Created 6s kubelet Created container liveness
Normal Started 6s kubelet Started container liveness
35 秒之后,再来看 Pod 的事件。在输出结果的最下面,有信息显示存活探测器失败了,这个容器被杀死并且被重建了。
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 90s default-scheduler Successfully assigned default/liveness-exec to 192.168.1.7
Normal Pulling 89s kubelet Pulling image "anjia0532/busybox"
Normal Pulled 69s kubelet Successfully pulled image "anjia0532/busybox" in 19.559680245s
Normal Created 68s kubelet Created container liveness
Normal Started 68s kubelet Started container liveness
Warning Unhealthy 24s (x3 over 34s) kubelet Liveness probe failed: cat: can't open '/tmp/healthy': No such file or directory
Normal Killing 24s kubelet Container liveness failed liveness probe, will be restarted
再等另外 30 秒,检查看这个容器被重启了:
kubectl get pod liveness-exec
输出结果显示RESTARTS
的值增加了1:
NAME READY STATUS RESTARTS AGE
liveness-exec 1/1 Running 1 109s
httpGet
发送HTTP请求,返回200-400范围状态码为成功
http-liveness.yaml
:
apiVersion: v1
kind: Pod
metadata:
labels:
test: liveness
name: liveness-http
spec:
containers:
- name: liveness
image: anjia0532/liveness
args:
- /server
livenessProbe:
httpGet:
path: /healthz
port: 8080
httpHeaders:
- name: Custom-Header
value: Awesome
initialDelaySeconds: 3
periodSeconds: 3
在这个配置文件中,可以看到 Pod 也只有一个容器。 periodSeconds
字段指定了 kubelet 每隔 3 秒执行一次存活探测。 initialDelaySeconds
字段告诉 kubelet 在执行第一次探测前应该等待 3 秒。 kubelet 会向容器内运行的服务(服务会监听 8080 端口)发送一个 HTTP GET 请求来执行探测。 如果服务器上/healthz
路径下的处理程序返回成功代码,则 kubelet 认为容器是健康存活的。 如果处理程序返回失败代码,则 kubelet 会杀死这个容器并且重新启动它。
任何大于或等于 200 并且小于 400 的返回代码标示成功,其它返回代码都标示失败。
tcpSocket
发起TCP Socket建立成功。
tcp-liveness-readiness.yaml
apiVersion: v1
kind: Pod
metadata:
name: goproxy
labels:
app: goproxy
spec:
containers:
- name: goproxy
image: anjia0532/goproxy:0.1
ports:
- containerPort: 8080
readinessProbe:
tcpSocket:
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
tcpSocket:
port: 8080
initialDelaySeconds: 15
periodSeconds: 20
TCP 检测的配置和 HTTP 检测非常相似。 下面这个例子同时使用就绪和存活探测器。kubelet 会在容器启动 5 秒后发送第一个就绪探测。 这会尝试连接 goproxy
容器的 8080 端口。 如果探测成功,这个 Pod 会被标记为就绪状态,kubelet 将继续每隔 10 秒运行一次检测。
除了就绪探测,这个配置包括了一个存活探测。 kubelet 会在容器启动 15 秒后进行第一次存活探测。 就像就绪探测一样,会尝试连接 goproxy
容器的 8080 端口。 如果存活探测失败,这个容器会被重新启动。
kubectl apply -f tcp-liveness-readiness.yaml
15 秒之后,通过看 Pod 事件来检测存活探测器:
kubectl describe pod goproxy
Pod调度策略
所谓调度策略,在集群环境中,即将如何将pod分配到某个节点上。
我们先来看一下创建Pod流程。
创建Pod流程
- 用户通过
kubectl
发送create pod
请求给api server
,apiserver
生成包含创建信息的yaml
,然后将yaml
信息写入ectd
数据库。 -
api server
触发watch机制准备创建pod
,信息转发给调度器scheduler
,调度器使用调度算法选择节点,调度器将节点信息给api server
,api server
将绑定的节点信息写入etcd
。 -
apis erver
又通过watch机制,调用kubelet
,指定pod
信息,触发docker run
命令创建容器。 - 创建完成之后反馈给
kubelet
,kubelet
将pod
的状态信息给api server
,api server
又将pod
的状态信息写入etcd
。 - 如果发送
kubectl get pods
命令,此时调用的是etcd
的信息。
在创建流程的过程中,通过scheduler
把pod
调度到某个node
上,那么调度过程中会受哪些因素影响呢?
主要受下面四个方面影响:
-
pod
资源限制 - 节点选择器标签
- 节点亲和性
- 污点和污点容忍
下面一一来分析。
Pod资源限制
比如我们在上面见到的yaml
文件中的请求(request)和限制(limit)。
apiVersion: v1
kind: Pod
metadata:
name: memory-demo
namespace: mem-example
spec:
containers:
- name: memory-demo-ctr
image: polinux/stress
resources:
limits:
memory: "200Mi"
cpu: "500m"
requests:
memory: "100Mi"
cpu: "250m"
在上面的例子中,请求的是100M内存和250M的CPU。那么k8s就会根据请求找到能够符合资源需求的节点进行调度。
节点选择器
apiVersion: v1
kind: Pod
metadata:
name: nginx
labels:
env: test
spec:
containers:
- name: nginx
image: nginx
imagePullPolicy: IfNotPresent
nodeSelector:
env_role:
其中nodeSelector
就是标签选择器,可以约束一个Pod
只能在特定的节点上运行。
那么如何为节点增加标签呢,假如就要增加上面所示节点选择器的标签,可以这样做:
kubectl label node node1 env_role=dev
kubectl label node node2 env_role=prod
这样就可以对不同的节点打上不同的标签。
nodeSelector
是节点选择约束的最简单推荐形式。nodeSelector
是 Pod
中spec
的一个字段。 它包含键值对的映射。为了使 Pod
可以在某个节点上运行,该节点的标签中 必须包含这里的每个键值对(它也可以具有其他标签)。 最常见的用法的是一对键值对。
节点亲和性
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: env_role
operator: In
values:
- dev
- test
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 1
preference:
matchExpressions:
- key: group
operator: In
values:
- otherProd
containers:
- name: nginx
image: nginx
imagePullPolicy:
nodeAffinity
是节点亲和性的标签,类似nodeSelector
,根据节点上标签约束来决定Pod
调度到哪些节点上。
但是节点亲和性极大地扩展了你可以表达约束的类型。关键增强包括:
- 语言更具有表现力(不再是简单的完全规则匹配)。
- 你可以发现规则是软需求/偏好,而不是硬性要求,因此,如果调度器无法满足该要求,仍然调度该Pod。
- 你可以使用节点上的 Pod 的标签来约束,而不是使用节点本身的标签,来允许哪些 pod 可以或者不可以被放置在一起。
目前有两种类型的节点亲和性,分别为requiredDuringSchedulingIgnoredDuringExecution
和preferredDuringSchedulingIgnoredDuringExecution
。
可以看它们为硬需求和软需求。前者指定将Pod调度到一个节点上必须满足的规则,后者指定调度器将尝试执行但不能保证的偏好。
名称“IgnoredDuringExecution”部分意味着,类似于 nodeSelector
的工作原理, 如果节点的标签在运行时发生变更,从而不再满足 Pod 上的亲和性规则,那么 Pod 将仍然继续在该节点上运行。
污点和容忍度
(图片来自于https://leoh0.github.io/post/2018-08-07-kubernetes-dedicated-node-pattern)
污点(taint)是**定义在节点之上**的键值型属性数据,用于让节点有能力主动拒绝调度器将`Pod`调度运行到节点上,除非该`Pod`对象具有接纳节点污点的容忍度(toleration)。
容忍度则是定义在Pod
对象上的键值型属性数据,用于配置该Pod
可容忍的节点污点。
上图的例子共有4个节点,但是前两个节点有污点position=low
,第一个Pod
有相应的污点容忍度,则可以调度到上面,以及剩下的无污点的节点上。
但是第二个Pod
只能调度在剩下的无污点的节点上。
节点亲和性是Pod
的一种属性,它使 Pod
被吸引到一类特定的节点。 这可能出于一种偏好,也可能是硬性要求。 污点(taint)则相反,它使节点能够排斥一类特定的 Pod
。
每个节点上都可以应用一个或多个污点,这表示对于那些不能容忍这些污点的 Pod
,是不会被该节点接受的。
污点值有三个:
-
NoSchedule
: 一定不被调度 -
PreferNoSchedule
: 尽量不会调度 -
NoExecute
: 不会调度,并且还会驱逐节点已有Pod
概念有点晦涩,我们来看一个例子。
添加污点命令格式为:
kubectl taint nodes [node_name] [key=value] : [NoSchedule|PreferNoSchedule |NoExecute]
然后开始敲命令吧。
# 部署Pod
yjw@rancher1:~$ kubectl create deploy web --image=nginx
deployment.apps/web created
# 部署5份
yjw@rancher1:~$ kubectl scale deploy web --replicas=5
deployment.apps/web scaled
# 查看部署情况
yjw@rancher1:~$ kubectl get po -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
web-96d5df5c8-52jnx 0/1 ContainerCreating 0 5m2s <none> 192.168.1.7 <none> <none>
web-96d5df5c8-9g6cg 1/1 Running 0 5m2s 10.42.0.11 192.168.1.6 <none> <none>
web-96d5df5c8-gztck 0/1 ContainerCreating 0 5m2s <none> 192.168.1.7 <none> <none>
web-96d5df5c8-wgrd9 1/1 Running 0 5m2s 10.42.0.12 192.168.1.6 <none> <none>
web-96d5df5c8-wqfqm 0/1 ContainerCreating 0 6m38s <none> 192.168.1.7 <none> <none>
可以看到,节点1.6和节点1.7都有部署Pod
。这里的IP应该替换成节点名称好一点,但是现阶段k8s还不支持直接更改节点名称,所以先不折腾了。
接下来我们清除这些Pod
,然后把节点1.6打上污点,让节点1.6不被调度。
# 删除deployment web
yjw@rancher1:~$ kubectl delete deploy web
deployment.apps "web" deleted
# 给节点1.6打上污点
yjw@rancher1:~$ kubectl taint nodes 192.168.1.6 env_role=yes:NoSchedule
node/192.168.1.6 tainted
# 查看节点1.6的污点
yjw@rancher1:~$ kubectl describe node 192.168.1.6 | grep Taint
Taints: env_role=yes:NoSchedule
接下来,我们再次部署web:
yjw@rancher1:~$ kubectl create deploy web --image=nginx
deployment.apps/web created
yjw@rancher1:~$ kubectl scale deploy web --replicas=5
deployment.apps/web scaled
# 查看部署情况
yjw@rancher1:~$ kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
web-96d5df5c8-2hjwr 0/1 ContainerCreating 0 7m59s <none> 192.168.1.7 <none> <none>
web-96d5df5c8-5rjbw 0/1 ContainerCreating 0 8m29s <none> 192.168.1.7 <none> <none>
web-96d5df5c8-l9xdh 0/1 ContainerCreating 0 7m59s <none> 192.168.1.7 <none> <none>
web-96d5df5c8-m8mms 0/1 ContainerCreating 0 7m59s <none> 192.168.1.7 <none> <none>
web-96d5df5c8-sjhkc 0/1 ContainerCreating 0 8m <none> 192.168.1.7 <none> <none>
可以看到,全部部署到了节点 192.168.1.7上了,拉取镜像有点慢,我们就不等了。
那么如何删除污点呢
yjw@rancher1:~$ kubectl describe node 192.168.1.6 | grep Taint
Taints: env_role=yes:NoSchedule
yjw@rancher1:~$ kubectl taint nodes 192.168.1.6 env_role:NoSchedule-
node/192.168.1.6 untainted
yjw@rancher1:~$ kubectl describe node 192.168.1.6 | grep Taint
Taints: <none>
我们再看看容忍度的应用:
apiVersion: v1
kind: Pod
metadata:
name: web
labels:
env: test
spec:
containers:
- name: web
image: nginx
imagePullPolicy: IfNotPresent
tolerations:
- key: "env_role"
operator: "Equal"
value: "yes"
effect: "NoSchedule"
然后再部署看看:
yjw@rancher1:~$ kubectl taint nodes 192.168.1.6 env_role=yes:NoSchedule
node/192.168.1.6 tainted
yjw@rancher1:~/temp$ kubectl apply -f taint.yaml
pod/web created
yjw@rancher1:~/temp$ kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
web 1/1 Running 0 28s 10.42.0.11 192.168.1.6 <none> <none>
可以看到,已经部署到了1.6上了,哪怕它上面有一个污点,也是有可能被调度到的,就这是污点容忍的用法。
污点定义在节点的nodeSpec
中,而容忍度定义在Pod
的podSpec
中,它们都是键值型数据,但又都额外支持一个效用(effect)标识,
语法格式为key=value:effect
。
污点上的效用标识用于定义其对Pod
对象的排斥等级,容忍度上的效用标识则用于定义其对污点的容忍级别。效用标识主要有以下三种类型:
-
NoSchedule
:不能容忍此污点的Pod
对象不可调度至当前节点,属于强制型约束关系,但添加污点对节点上现存的Pod
对象不产生影响。 -
PreferNoSchedule
:NoSchedule
的柔性约束版本,即调度器尽量确保不会将那些不能容忍此污点的Pod
对象调度至当前街道,除非不存在其他任何能容忍此污点的可用节点;同样添加此类效用的污点对节点上现存的Pod
对象不产生影响。 -
NoExecute
:不能容忍此污点的新Pod
对象不可调度至当前节点,属于强制型约束关系,而且节点上现存的Pod
对象因节点污点变动或Pod
容忍度变动而不再满足匹配条件时,Pod
对象将会被驱逐。
上例中的operator: "Equal"
说的是等值比较,是在Pod
对象上定义容忍度时,支持的两种操作符之一。表示容忍度与污点必须在key
、value
和effect
三者之上完全匹配;另一种是存在性判断(operator: "Exists"
),表示二者的key
和effect
必须完全匹配,而容忍度中的value
字段要使用空值。
污点的使用场景主要有:
Controller
什么是Controller
Controller是管理和运行容器的对象。
K8s中内建了多种控制器,用来控制Pod
的具体状态和行为。
Pod和Controller的关系
Pod
是通过Controller来实现应用的运维,比如伸缩、滚动升级等。
Pod
和Controller通过label
标签建立关系。
Deployment控制器应用场景
Deployment
为 Pod
和 ReplicaSet
提供了一个声明式定义(declarative)方法,用来替代以前的 ReplicationController
来方便的管理应用。
典型的应用场景包括:
- 部署无状态应用
- 管理
Pod
和ReplicaSet
- 滚动升级和回滚应用
- 扩容和缩容
- 暂停和继续 Deployment
Deployment控制器部署应用
简单的,可以通过命令行的方式:
kubectl create deployment wb --image=nginx
但是这种方式不好复用,而且只适合于参数简单的情况,如果参数复杂且繁多时就不太适用了。
k8s还提供了通过yaml
文件部署的方式。
kubectl create deployment web --image=nginx --dry-run -o yaml > web.yaml
不记得yaml
的写法没关系,我们可以通过上述命令生成,然后修改成自己想要的即可。
生成的yaml
如下:
apiVersion: apps/v1
kind: Deployment
metadata:
creationTimestamp: null
labels:
app: web
name: web
spec:
replicas: 1
selector:
matchLabels:
app: web # selector匹配下面的标签 app: web
strategy: {}
template:
metadata:
creationTimestamp: null
labels:
app: web # 标签 app: web
spec:
containers:
- image: nginx
name: nginx
resources: {}
ports:
- containerPort: 80
status: {}
我们不修改,直接部署:
yjw@rancher1:~$ kubectl apply -f web.yaml
deployment.apps/web created
yjw@rancher1:~$ kubectl get po
NAME READY STATUS RESTARTS AGE
web-96d5df5c8-qxslf 1/1 Running 0 45s
现在nginx部署好了,但是还是无法通过本地浏览器访问,需要暴露端口对外发布。
# 也是生成yaml文件的方式
kubectl expose deployment web --port=80 --target-port=80 --type=NodePort --name=web-nodeport -o yaml > web-port.yaml
这个生成的就比较长,我把核心的贴一下:
apiVersion: v1
kind: Service
metadata:
labels:
app: web
name: web-nodeport
namespace: default
spec:
clusterIP: 10.43.45.231
externalTrafficPolicy: Cluster
ports:
- nodePort: 30474
port: 80
protocol: TCP
targetPort: 80
selector:
app: web
sessionAffinity: None
type: NodePort
status:
loadBalancer: {}
nodePort: 30474
是k8s帮我们生成的外部访问端口。
yjw@rancher1:~$ kubectl apply -f web-port.yaml
yjw@rancher1:~$ kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.43.0.1 <none> 443/TCP 20d
web-nodeport NodePort 10.43.45.231 <none> 80:30474/TCP 3m18s
这里有多个端口,大概说一下区别。
-
NodePort
提供了外部客户端访问service
的一种方式,可以通过nodeIP:nodePort
访问集群中的service
。 -
port
serice
暴露在cluster ip
上的端口,集群内的其他Pod
可以通过该端口与之通信。 -
TargetPort
Pod
上监听的端口,服务会通过该端口向Pod
发送请求 -
containerPort
(Docker)容器暴露的端口
nodePort
和port
都是service
的端口,nodePort
暴露给外部客户端访问,port
暴露给集群内部服务访问。经过这两个端口的数据需要经过kube-proxy
,代理到pod
的targetPort
上,最后到达Pod
内容器的containerPort
。
我们可以通过该端口访问刚才部署的nginx服务。
升级回滚和弹性伸缩
我们上小节没有指定nginx版本,默认安装是最新的,为了演示。修改web.yaml
文件:
spec:
containers:
- image: nginx:1.14
然后再重新部署
yjw@rancher1:~$ kubectl delete pod web
pod "web" deleted
yjw@rancher1:~$ kubectl delete svc web-nodeport
service "web-nodeport" deleted
yjw@rancher1:~$ kubectl apply -f web.yaml
deployment.apps/web configured
yjw@rancher1:~$ kubectl get po
NAME READY STATUS RESTARTS AGE
web-88c6cbf44-cd2fd 1/1 Running 0 40s
我们需要先删除之前部署的。来验证一下部署的版本:
yjw@rancher1:~$ kubectl describe po web-88c6cbf44-cd2fd
...
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 100s default-scheduler Successfully assigned default/web-88c6cbf44-cd2fd to 192.168.1.7
Normal Pulling 98s kubelet Pulling image "nginx:1.14"
Normal Pulled 73s kubelet Successfully pulled image "nginx:1.14" in 24.105553689s
Normal Created 69s kubelet Created container nginx
Normal Started 69s kubelet Started container nginx
下面我们来升级nginx的版本,升级为1.15。
yjw@rancher1:~$ kubectl set image deployment web nginx=nginx:1.15
deployment.apps/web image updated
yjw@rancher1:~$ kubectl get po
NAME READY STATUS RESTARTS AGE
web-586db47859-5sd75 0/1 ContainerCreating 0 3s # 先创建一个新的
web-88c6cbf44-cd2fd 1/1 Running 0 3m34s
yjw@rancher1:~$ kubectl get po
NAME READY STATUS RESTARTS AGE
web-586db47859-5sd75 1/1 Running 0 25s # 新的创建好之后
web-88c6cbf44-cd2fd 1/1 Terminating 0 3m56s # 停止之前旧的
yjw@rancher1:~$ kubectl describe po web-586db47859-5sd75
...
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 2m58s default-scheduler Successfully assigned default/web-586db47859-5sd75 to 192.168.1.7
Normal Pulling 2m57s kubelet Pulling image "nginx:1.15"
Normal Pulled 2m35s kubelet Successfully pulled image "nginx:1.15" in 21.507159641s
Normal Created 2m34s kubelet Created container nginx
Normal Started 2m34s kubelet Started container nginx
yjw@rancher1:~$ kubectl rollout status deployment web # 也可以这样查看升级状态
deployment "web" successfully rolled out
下面来演示一下回滚,将版本还原会到nginx1.14。
# 查看历史版本
yjw@rancher1:~$ kubectl rollout history deploy web
deployment.apps/web
REVISION CHANGE-CAUSE
1 <none> # 1.14
2 <none> # 1.15
# #
# 回滚有两种做法,第一种:回到上一个版本
##
yjw@rancher1:~$ kubectl rollout undo deployment web
deployment.apps/web rolled back
yjw@rancher1:~$ kubectl get po
NAME READY STATUS RESTARTS AGE
web-88c6cbf44-7rkj2 1/1 Running 0 86s
yjw@rancher1:~$ kubectl describe po web-88c6cbf44-7rkj
...
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 96s default-scheduler Successfully assigned default/web-88c6cbf44-7rkj2 to 192.168.1.7
Normal Pulling 95s kubelet Pulling image "nginx:1.14"
Normal Pulled 84s kubelet Successfully pulled image "nginx:1.14" in 10.666448411s
Normal Created 84s kubelet Created container nginx
Normal Started 84s kubelet Started container nginx
# #
# 回滚有两种做法,第二种:回滚到指定版本
##
yjw@rancher1:~$ kubectl rollout history deploy web
deployment.apps/web
REVISION CHANGE-CAUSE
1 <none>
3 <none>
4 <none>
yjw@rancher1:~$ kubectl rollout undo deploy web --to-revision=1 # 切回到最原始的版本,即不指定nginx版本,默认最新版
deployment.apps/web rolled back
yjw@rancher1:~$ kubectl get po
NAME READY STATUS RESTARTS AGE
web-88c6cbf44-7rkj2 0/1 Terminating 0 5m1s
web-96d5df5c8-s6bnp 1/1 Running 0 13s
yjw@rancher1:~$ kubectl get po
NAME READY STATUS RESTARTS AGE
web-96d5df5c8-s6bnp 1/1 Running 0 14s
yjw@rancher1:~$ kubectl describe po web-96d5df5c8-s6bnp
...
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 32s default-scheduler Successfully assigned default/web-96d5df5c8-s6bnp to 192.168.1.7
Normal Pulling 31s kubelet Pulling image "nginx"
Normal Pulled 27s kubelet Successfully pulled image "nginx" in 3.509383287s
Normal Created 27s kubelet Created container nginx
Normal Started 27s kubelet Started container nginx
下面来看下弹性伸缩,所谓弹性伸缩可以理解为根据实际情况,弹性地调整实例部署的数量。
yjw@rancher1:~$ kubectl get po
NAME READY STATUS RESTARTS AGE
web-96d5df5c8-s6bnp 1/1 Running 0 2m26s
现在我们只部署了一个实例,假如你的应用很火,用的人很多,我们就像多部署几个,比如5个:
yjw@rancher1:~$ kubectl scale deploy web --replicas=5
deployment.apps/web scaled
yjw@rancher1:~$ kubectl get pod
NAME READY STATUS RESTARTS AGE
web-96d5df5c8-b6tjg 0/1 ContainerCreating 0 7s
web-96d5df5c8-mn5qd 0/1 ContainerCreating 0 7s
web-96d5df5c8-nt64z 1/1 Running 0 7s
web-96d5df5c8-s6bnp 1/1 Running 0 4m39s
web-96d5df5c8-sh6pk 0/1 ContainerCreating 0 7s
部署有状态应用
有状态应用什么意思?我们先来看下无状态应用。
无状态:
- 认为所有
Pod
都是一样的 - 没有顺序要求
- 不用考虑在哪个节点上运行
- 可以随意进行伸缩和扩展
那有状态呢:
- 上面的4点都要考虑
- 让每个
Pod
独立,保存每个Pod
启动顺序和唯一性
有时不需要或不想要负载均衡,以及单独的 Service IP。 遇到这种情况,可以通过指定 Cluster IP(spec.clusterIP)的值为 “None” 来创建无头服务。
下面我们使用StatefulSet
来部署有状态应用。
sts.yaml
:
apiVersion: v1
kind: Service
metadata:
name: nginx
labels:
app: nginx
spec:
ports:
- port: 80
name: web
clusterIP: None # 无头服务
selector:
app: nginx
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: nginx-statefulset
spec:
selector:
matchLabels:
app: nginx # has to match .spec.template.metadata.labels
serviceName: "nginx"
replicas: 3 # by default is 1
template:
metadata:
labels:
app: nginx # has to match .spec.selector.matchLabels
spec:
terminationGracePeriodSeconds: 10
containers:
- name: nginx
image: nginx:latest
ports:
- containerPort: 80
执行
yjw@master:~/temp$ kubectl apply -f sts.yaml
service/nginx created
statefulset.apps/nginx-statefulset created
查看创建的pod
:
yjw@master:~/temp$ kubectl get po
NAME READY STATUS RESTARTS AGE
nginx-statefulset-0 1/1 Running 0 2m41s
nginx-statefulset-1 1/1 Running 0 2m27s
nginx-statefulset-2 1/1 Running 0 2m24s
查看无头服务:
yjw@master:~/temp$ kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.43.0.1 <none> 443/TCP 10d
nginx ClusterIP None <none> 80/TCP 4m16s
从CLUSTER-IP : None
可以看出它是无头服务。
deployment
和StatefulSet
的区别:是否有唯一标识
StatefulSet
中的每个 Pod
根据 StatefulSet
的名称和 Pod
的序号派生出它的主机名。 组合主机名的格式为$(StatefulSet 名称)-$(序号)
。 上例将会创建三个名称分别为 nginx-statefulset-0
、nginx-statefulset-1
、nginx-statefulset-2
的 Pod。 StatefulSet
可以使用无头服务 控制它的 Pod
的网络域。管理域的这个服务的格式为: $(服务名称).$(命名空间).svc.cluster.local
,其中 cluster.local
是集群域。 一旦每个 Pod
创建成功,就会得到一个匹配的 DNS 子域,格式为:$(pod 名称).$(所属服务的 DNS 域名)
,其中所属服务由 StatefulSet
的 serviceName
域来设定。
部署守护进程
守护进程(DaemonSet
) 确保全部(或者某些)节点上运行一个 Pod
的副本。 当有节点加入集群时, 也会为他们新增一个 Pod
。 当有节点从集群移除时,这些 Pod
也会被回收。删除 DaemonSet
将会删除它创建的所有 Pod
。
DaemonSet
的一些典型用法:
- 在每个节点上运行集群守护进程
- 在每个节点上运行日志收集守护进程
- 在每个节点上运行监控守护进程
来看一个实例。
ds.yaml
:
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: ds-test
labels:
app: filebeat
spec:
selector:
matchLabels:
app: filebeat
template:
metadata:
labels:
app: filebeat
spec:
containers:
- name: logs
image: nginx
ports:
- containerPort: 80
volumeMounts:
- name: varlog
mountPath: /tmp/log
volumes:
- name: varlog
hostPath:
path:
这里假设是一个日志采集工具。
yjw@master:~/temp$ vim ds.yaml
yjw@master:~/temp$ kubectl apply -f ds.yaml
daemonset.apps/ds-test created
yjw@master:~/temp$ kubectl get po
NAME READY STATUS RESTARTS AGE
ds-test-4k6hw 1/1 Running 0 29s
ds-test-hj6d2 1/1 Running 0 29s
ds-test-xxj4n 1/1 Running 0 29s
我们可以进入某个pod
查看:
# 进入pod的 bash
yjw@master:~/temp$ kubectl exec -it ds-test-4k6hw -- bash
# 查看打印的日志
root@ds-test-4k6hw:/# ls /tmp/log
alternatives.log dist-upgrade landscape syslog.5.gz
alternatives.log.1 dmesg lastlog syslog.6.gz
apt dpkg.log pods syslog.7.gz
auth.log dpkg.log.1 private sysstat
auth.log.1 faillog syslog ubuntu-advantage.log
bootstrap.log installer syslog.1 unattended-upgrades
btmp journal syslog.2.gz wtmp
btmp.1 kern.log syslog.3.gz
containers kern.log.1 syslog.4.gz
# 退出pod的 bash
root@ds-test-4k6hw:/# exit
exit
yjw@master:~/temp$
部署一次性和定时任务
还可以部署一次性任务(job)和定义任务(cronjob)。
job.yaml
:
apiVersion: batch/v1
kind: Job
metadata:
name: pi
spec:
template:
spec:
containers:
- name: pi
image: perl
command: ["perl", "-Mbignum=bpi", "-wle", "print bpi(2000)"]
restartPolicy: Never
backoffLimit: 4 # 失败后尝试4次
上面是一个 Job 配置示例。它负责计算 π 到小数点后 2000 位,并将结果打印出来。 此计算大约需要 10 秒钟完成。
yjw@master:~/temp$ kubectl create -f job.yaml
job.batch/pi created
# 等待片刻
yjw@master:~/temp$ kubectl get jobs
NAME COMPLETIONS DURATION AGE
pi 1/1 57s 86s
上面说明完成了,可以查看输出的内容。
yjw@master:~/temp$ kubectl logs pi-s4zjz
3.1415926535897932384626433832795028841971693993751058209749445923078164062862089986280348253421170679821480865132823066470938446095505822317253594081284811174502841027019385211055596446229489549303819644288109756659334461284756482337867831652712019091456485669234603486104543266482133936072602491412737245870066063155881748815209209628292540917153643678925903600113305305488204665213841469519415116094330572703657595919530921861173819326117931051185480744623799627495673518857527248912279381830119491298336733624406566430860213949463952247371907021798609437027705392171762931767523846748184676694051320005681271452635608277857713427577896091736371787214684409012249534301465495853710507922796892589235420199561121290219608640344181598136297747713099605187072113499999983729780499510597317328160963185950244594553469083026425223082533446850352619311881710100031378387528865875332083814206171776691473035982534904287554687311595628638823537875937519577818577805321712268066130019278766111959092164201989380952572010654858632788659361533818279682303019520353018529689957736225994138912497217752834791315155748572424541506959508295331168617278558890750983817546374649393192550604009277016711390098488240128583616035637076601047101819429555961989467678374494482553797747268471040475346462080466842590694912933136770289891521047521620569660240580381501935112533824300355876402474964732639141992726042699227967823547816360093417216412199245863150302861829745557067498385054945885869269956909272107975093029553211653449872027559602364806654991198818347977535663698074265425278625518184175746728909777727938000816470600161452491921732172147723501414419735685481613611573525521334757418494684385233239073941433345477624168625189835694855620992192221842725502542568876717904946016534668049886272327917860857843838279679766814541009538837863609506800642251252051173929848960841284886269456042419652850222106611863067442786220391949450471237137869609563643719172874677646575739624138908658326459958133904780275901
下面来看下定时任务。
cronjob.yaml
:
apiVersion: batch/v1beta1
kind: CronJob
metadata:
name: hello
spec:
schedule: "*/1 * * * *"
jobTemplate:
spec:
template:
spec:
containers:
- name: hello
image: busybox
imagePullPolicy: IfNotPresent
command:
- /bin/sh
- -c
- date; echo Hello from the Kubernetes cluster
restartPolicy:
上面示例会在每分钟打印出当前时间和问候消息:
yjw@master:~/temp$ kubectl apply -f cronjob.yaml
cronjob.batch/hello created
yjw@master:~/temp$ kubectl get pod
NAME READY STATUS RESTARTS AGE
ds-test-4k6hw 1/1 Running 0 16m
ds-test-hj6d2 1/1 Running 0 16m
ds-test-xxj4n 1/1 Running 0 16m
hello-1620308640-wm8ts 0/1 Completed 0 64s
hello-1620308700-gkt2l 0/1 Completed 0 4s
# 查看定时任务打印的日志
yjw@master:~/temp$ kubectl logs hello-1620308640-wm8ts
Thu May 6 13:44:13 UTC 2021
Hello from the Kubernetes cluster
Service
定义Pod
的访问规则。
概述
Service
存在的意义:
(1) 防止Pod
失联(服务发现)
yjw@rancher1:~$ kubectl get po -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
web-96d5df5c8-b6tjg 1/1 Running 0 3m9s 10.42.192.8 192.168.1.7 <none> <none>
web-96d5df5c8-mn5qd 1/1 Running 0 3m9s 10.42.192.10 192.168.1.7 <none> <none>
web-96d5df5c8-nt64z 1/1 Running 0 3m9s 10.42.192.1 192.168.1.7 <none> <none>
web-96d5df5c8-s6bnp 1/1 Running 0 7m41s 10.42.192.3 192.168.1.7 <none> <none>
web-96d5df5c8-sh6pk 1/1 Running 0 3m9s 10.42.192.11 192.168.1.7 <none> <none>
如果查看更加详细的信息,可以看到每个pod
都有一个自己的内部ip:10.42.*
。
因为Pod
是短暂存在的,上面进行的滚动更新、升级回滚都会停止旧的Pod
,而创建新的Pod
。而每个Pod
的IP都是独立的。
Service
通过服务发现,保存每个Pod
的IP,类似SpringCloud中的Eureka
,Pod
创建后会注册到Service
中。这样通过Service
就可以访问到Pod
,防止Pod
失联。
(2) 定义一组Pod
访问策略(负载均衡)
假设你部署了3个实例,用于分流,Service
就可以做到一些访问策略。详见虚拟 IP 和 Service 代理
那Pod
和Service
是怎么建立联系的呢?
也是通过标签选择来实现的。也是通过这种关系来实现负载均衡和服务发现。
在 k8s集群中,每个 Node
运行一个 kube-proxy
进程。 kube-proxy
负责为 Service
实现了一种 VIP(虚拟 IP)的形式。
Service类型
yjw@rancher1:~$ kubectl expose --help
...
--type='': Type for this service: ClusterIP, NodePort, LoadBalancer, or
ExternalName. Default is 'ClusterIP'.
可以看到有四种类型。
-
ClusterIP
:通过集群的内部 IP(上面说的虚拟IP) 暴露服务,选择该值时服务只能够在集群内部访问。 这也是默认的ServiceType
。 -
NodePort
:通过每个节点上的 IP 和静态端口(NodePort)暴露服务。NodePort
服务会路由到自动创建的ClusterIP
服务。 通过请求<节点 IP>:<节点端口>
,你可以从集群的外部访问一个NodePort
服务。 -
LoadBalancer
:使用云提供商的负载均衡器向外部暴露服务。 外部负载均衡器可以将流量路由到自动创建的NodePort
服务和ClusterIP
服务上。 -
ExternalName
:通过返回CNAME
和对应值,可以将服务映射到externalName
字段的内容(例如,foo.bar.example.com
)。 无需创建任何类型代理。
我们先清掉之前配置的内容,最终达到:
yjw@rancher1:~$ kubectl get po
No resources found in default namespace.
yjw@rancher1:~$ kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.43.0.1 <none> 443/TCP 20d
然后我们也先生成一个service.yaml
文件:
yjw@rancher1:~$ kubectl apply -f web.yaml
deployment.apps/web created
yjw@rancher1:~$ kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.43.0.1 <none> 443/TCP 20d
yjw@rancher1:~$ kubectl expose deploy web --port=80 --target-port=80 --dry-run=client -o yaml > service1.yaml
yjw@rancher1:~$ ls
service1.yaml temp web-port.yaml web.yaml
service1.yaml
内容如下:
apiVersion: v1
kind: Service
metadata:
creationTimestamp: null
labels:
app: web
name: web
spec:
ports:
- port: 80
protocol: TCP
targetPort: 80
selector:
app: web
#type: ClusterIP
status:
loadBalancer: {}
默认没有指定type
,即为ClusterIP
类型。
yjw@rancher1:~$ kubectl apply -f service1.yaml
service/web created
yjw@rancher1:~$ kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.43.0.1 <none> 443/TCP 20d
web ClusterIP 10.43.95.131 <none> 80/TCP 5s
我们可以在集群内部访问这个IP:
yjw@rancher1:~$ curl 10.43.95.131
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
body {
width: 35em;
margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif;
}
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>
<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>
<p><em>Thank you for using nginx.</em></p>
</body>
</html>
下面来看下NodePort
。
apiVersion: v1
kind: Service
metadata:
creationTimestamp: null
labels:
app: web
name: web1 # 修改name,防止重复
spec:
ports:
- port: 80
protocol: TCP
targetPort: 80
selector:
app: web
type: NodePort # 指定NodePort
status:
loadBalancer: {
应用修改
yjw@rancher1:~$ vim service1.yaml
yjw@rancher1:~$ kubectl apply -f service1.yaml
service/web1 created
yjw@rancher1:~$ kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.43.0.1 <none> 443/TCP 20d
web ClusterIP 10.43.95.131 <none> 80/TCP 2m56s
web1 NodePort 10.43.114.51 <none> 80:31202/TCP 4s
可以看到,k8s默认给我们分配了一个端口31202
,通过这个端口我们可以在集群外部,通过nodeIP:31202
来访问nginx。
LoadBalancer
需要用到公有云,比如阿里云、腾讯云等。
如果我们自己实现负载均衡,可以用一台可以访问外网的机器,安装nginx,进行反向代理。需要手动添加访问节点到nginx中。
配置管理
Secret
Secret
对象类型用来保存敏感信息,例如密码、OAuth 令牌和 SSH 密钥。 将这些信息放在 secret 中比放在 Pod规约中或者 容器镜像 中来说更加安全和灵活。
用户可以创建 Secret
,同时系统也创建了一些 Secret
。
在为创建 Secret
编写配置文件时,你可以设置 data
或 stringData
字段。 data
和 stringData
字段都是可选的。data
字段中所有键值都必须是 base64 编码的字符串。
下面来看如何创建Secret
。
secret.yaml
:
apiVersion: v1
kind: Secret
metadata:
name: mysecret
type: Opaque # 用户定义的任意数据
data:
username: YWRtaW4=
password:
应用:
yjw@master:~/temp$ vim secret.yaml
yjw@master:~/temp$ kubectl create -f secret.yaml
secret/mysecret created
yjw@master:~/temp$ kubectl get secret
NAME TYPE DATA AGE
default-token-2l7hs kubernetes.io/service-account-token 3 10d
mysecret Opaque
创建好了,怎么应用呢。可以以变量形式挂载到pod
容器中。
secret-var.yaml
:
apiVersion: v1
kind: Pod
metadata:
name: mypod
spec:
containers:
- name: nginx
image: nginx
env:
- name: SECRET_USERNAME
valueFrom: # 变量形式挂载
secretKeyRef:
name: mysecret # secret名称
key: username # secret中的key
- name: SECRET_PASSWORD
valueFrom:
secretKeyRef:
name: mysecret
key:
应用:
yjw@master:~/temp$ kubectl apply -f secret-var.yaml
pod/mypod created
yjw@master:~/temp$ kubectl get po
NAME READY STATUS RESTARTS AGE
mypod 1/1 Running 0 2m13s
# 进入查看
yjw@master:~/temp$ kubectl exec -it mypod -- bash
# 打印保存的用户名和密码变量
root@mypod:/# echo $SECRET_USERNAME
admin
root@mypod:/# echo $SECRET_PASSWORD
1f2d1e2e67df
还可以以数据卷(volumes)的形式挂载到pod中。
secret-vol.yaml
:
apiVersion: v1
kind: Pod
metadata:
name: mypod
spec:
containers:
- name: nginx
image: nginx
volumeMounts: # 数据卷形式
- name: foo
mountPath: "/etc/foo"
readOnly: true
volumes:
- name: foo
secret:
secretName:
应用:
yjw@master:~/temp$ kubectl delete -f secret-v
secret-var.yaml secret-vol.yaml
yjw@master:~/temp$ kubectl delete -f secret-v
secret-var.yaml secret-vol.yaml
yjw@master:~/temp$ kubectl delete -f secret-var.yaml
pod "mypod" deleted
yjw@master:~/temp$ kubectl apply -f secret-vol.yaml
pod/mypod created
yjw@master:~/temp$ kubectl get po
NAME READY STATUS RESTARTS AGE
mypod 1/1 Running 0 34s
# 进入pod查看
yjw@master:~/temp$ kubectl exec -it mypod -- bash
# 挂载的目录
root@mypod:/# ls /etc/foo
password username
root@mypod:/# cat /etc/foo/username
admin
ConfigMap
ConfigMap
是一种 API 对象,和Secret
不一样的是,ConfigMap
用来将非机密性的数据保存到键值对中。使用时, Pods 可以将其用作环境变量、命令行参数或者存储卷中的配置文件。
下面以redis配置文件为例。
假设我们redis的配置文件为:
redis.properties
:
redis.host=127.0.0.1
redis.port=6379
redis.password=123456
下面来创建ConfigMap
。
# 从配置文件创建ConfigMap
yjw@master:~/temp$ kubectl create configmap redis-config --from-file=redis.properties
configmap/redis-config created
yjw@master:~/temp$ kubectl get cm
NAME DATA AGE
kube-root-ca.crt 1 10d
redis-config 1 8s
yjw@master:~/temp$ kubectl describe cm redis-config
Name: redis-config
Namespace: default
Labels: <none>
Annotations: <none>
Data
====
redis.properties:
----
redis.host=127.0.0.1
redis.port=6379
redis.password=123456
Events: <none>
yjw@master:~/temp$
下面先来看下以Volume
挂载到pod
容器中。
cm.yaml
:
apiVersion: v1
kind: Pod
metadata:
name: mypod
spec:
containers:
- name: busybox
image: busybox
command: [ "/bin/sh","-c","cat /etc/config/redis.properties" ] # 打印配置文件中的内容
volumeMounts:
- name: config-volume
mountPath: /etc/config
volumes:
- name: config-volume
configMap: # 指定configmap
name: redis-config
restartPolicy:
应用:
yjw@master:~/temp$ kubectl apply -f cm.yaml
pod/mypod created
yjw@master:~/temp$ kubectl get po
NAME READY STATUS RESTARTS AGE
mypod 0/1 Completed 0 15s
yjw@master:~/temp$ kubectl logs mypod
redis.host=127.0.0.1
redis.port=6379
redis.password=123456
ConfigMap
也可以以变量的形式挂载到pod
中。
首先声明变量。
myconfig.yaml
:
apiVersion: v1
kind: ConfigMap
metadata:
name: myconfig
namespace: default
data:
special.level: info
special.type:
应用之前先删掉上面的:
yjw@master:~/temp$ kubectl delete -f cm.yaml
pod "mypod" deleted
yjw@master:~/temp$ vim myconfig.yaml
yjw@master:~/temp$ kubectl apply -f myconfig.yaml
configmap/myconfig created
yjw@master:~/temp$ kubectl get cm
NAME DATA AGE
kube-root-ca.crt 1 11d
myconfig 2 7s
redis-config 1 14m
下面以变量的形式挂载。
config-var.yaml
:
apiVersion: v1
kind: Pod
metadata:
name: mypod
spec:
containers:
- name: busybox
image: busybox
command: [ "/bin/sh", "-c", "echo $(LEVEL) $(TYPE)" ] # 打印这两个变量
env:
- name: LEVEL #变量名
valueFrom:
configMapKeyRef:
name: myconfig
key: special.level # 变量的key
- name: TYPE
valueFrom:
configMapKeyRef:
name: myconfig
key: special.type
restartPolicy:
应用:
yjw@master:~/temp$ vim config-var.yaml
yjw@master:~/temp$ kubectl apply -f config-var.yaml
pod/mypod created
yjw@master:~/temp$ kubectl get po
NAME READY STATUS RESTARTS AGE
mypod 0/1 Completed 0 4s
yjw@master:~/temp$ kubectl logs mypod
info hello
集群安全机制
当我们访问k8s集群时,需要经过三个步骤才能完成具体步骤。
- 认证(Authentication)
- 运行一个或多个身份认证组件,将请求验证为来自特定的用户
- 鉴权(Authorization)
- 请求必须包含请求者的用户名、请求的行为以及受该操作影响的对象。 如果现有策略声明用户有权完成请求的操作,那么该请求被鉴权通过。可以基于RBAC进行鉴权操作。
- 准入控制(Admission control)
- 准入控制可以修改或拒绝请求。准入控制模块还可以访问正在创建或修改的对象的内容。
进行访问时,都需要经过apiserver
做同一协调。访问过程中需要证书、token或用户名+密码。
除了这些,还有一个重要的概念是传输安全(Transport security)。
API 服务器在 443 端口上提供服务,受 TLS 保护。即https证书认证。
RBAC介绍
基于角色(Role)的访问控制(RBAC)是一种基于组织中用户的角色来调节控制对计算机或网络资源的访问的方法。
上面是RBAC的一个例子,用户lucy通过角色绑定(rolebinding,rb)获得了对资源的一些权限。其中涉及到的概念有很多,下面一一解释。
- 角色(role):设置特定的命名空间访问权限
- 集群角色(ClusterRole):集群所有命名空间的访问权限
- 角色绑定(RoleBinding):将角色绑定到主体上
- 集群角色绑定(ClusterRoleBinding):将集群角色绑定到主体
- 用户(user)
- 用户组(group)
- 服务账号(ServiceAccount)
Ingress
我们之前暴露服务是通过NodePort
暴露端口号,然后通过ip+端口号进行访问。
但是这种方法有缺陷:
- 会在每个节点上开启端口,即每个端口只能使用一次
- 而且实际访问是通过域名访问,根据不同域名路由跳转到不同的端口服务中
而引入Ingress
就是为了解决上面的问题。那么Ingress
是如何访问到pod
的呢?
Ingress
作为统一入口,由Service
关联一组pod
。
上面的路由规则是基于域名www.xx.com
,通过前缀URL/foo/
路由到某个服务。
下面我们来进行实操加深印象。
应用Ingress
①创建nginx应用,使用NodePort
对外暴露端口
yjw@master:~/temp$ kubectl create deployment web --image=nginx
deployment.apps/web created
yjw@master:~/temp$ kubectl get po
NAME READY STATUS RESTARTS AGE
web-96d5df5c8-992bf 1/1 Running 0 8s
yjw@master:~/temp$ kubectl expose deployment web --port=80 --target-port=80 --type=NodePort
service/web exposed
yjw@master:~/temp$ kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.43.0.1 <none> 443/TCP 13d
web NodePort 10.43.42.44 <none> 80:31169/TCP 11s
此时,我们可以直接通过主机IP+31169 进行访问:
②部署Ingress Controller
yjw@master:~/temp$ kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/master/deploy/static/provider/baremetal/deploy.yaml
namespace/ingress-nginx configured
serviceaccount/ingress-nginx created
configmap/ingress-nginx-controller created
clusterrole.rbac.authorization.k8s.io/ingress-nginx created
clusterrolebinding.rbac.authorization.k8s.io/ingress-nginx created
role.rbac.authorization.k8s.io/ingress-nginx created
rolebinding.rbac.authorization.k8s.io/ingress-nginx created
service/ingress-nginx-controller-admission created
service/ingress-nginx-controller created
deployment.apps/ingress-nginx-controller created
validatingwebhookconfiguration.admissionregistration.k8s.io/ingress-nginx-admission created
serviceaccount/ingress-nginx-admission created
clusterrole.rbac.authorization.k8s.io/ingress-nginx-admission created
clusterrolebinding.rbac.authorization.k8s.io/ingress-nginx-admission created
role.rbac.authorization.k8s.io/ingress-nginx-admission created
rolebinding.rbac.authorization.k8s.io/ingress-nginx-admission created
job.batch/ingress-nginx-admission-create created
job.batch/ingress-nginx-admission-patch created
③创建Ingress
规则
创建tls secret
:
yjw@master:~/temp/certs$ kubectl create secret tls tls-ingress-www-greyfoss --cert=cert1.pem --key=privkey1.pem
secret/tls-ingress-www-greyfoss created
证书是通过 let’s encrypt免费获取的
ingress.yaml
:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: minimal-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
rules:
- host: www.greyfoss.top
http:
paths:
- path: /web
pathType: Prefix
backend:
service:
name: web
port:
number: 80
tls:
- hosts:
- www.greyfoss.top
secretName: tls-ingress-www-greyfoss
yjw@master:~/temp$ kubectl apply -f ingress.yaml
ingress.networking.k8s.io/minimal-ingress created
yjw@master:~/temp$ kubectl describe ing minimal-ingress
Name: minimal-ingress
Namespace: default
Address:
Default backend: default-http-backend:80 (<error: endpoints "default-http-backend" not found>)
TLS:
tls-ingress-www-greyfoss terminates www.greyfoss.top
Rules:
Host Path Backends
---- ---- --------
www.greyfoss.top
/web web:80 (10.42.128.2:80)
Annotations: nginx.ingress.kubernetes.io/rewrite-target: /
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Sync 23s nginx-ingress-controller Scheduled for sync
Normal Sync 23s nginx-ingress-controller Scheduled for sync
Normal Sync 23s nginx-ingress-controller Scheduled for sync
Normal Sync 23s nginx-ingress-controller Scheduled for sync
由于我们添加了真实的证书,显示连接是安全的。
这样我们就通过前缀/web
路由到了添加的nginx
服务。
helm
在上面我们介绍的内容中,如果我们要部署应用,首先需要编写yaml
文件;然后需要对外暴露端口;然后部署Ingress
暴露应用。
那么这种方式有什么缺点呢?这种方式适用于部署单一应用、少数服务的应用。
如果部署微服务项目,假设有几十个服务,每个服务都有一套自己的yaml
文件,那么维护起来将是个灾难。
引入heml
就是为了解决这些问题,那可以解决哪些问题?
- 使用
helm
可以把这些yaml
文件作为一个整体管理 - 实现
yaml
文件高效复用 - 使用
helm
实现应用级别的版本管理
概述
Helm
是一个k8s的包管理工具,就像Linux下的yum/apt
等,可以很方便的将之前打包好的yaml
文件部署到k8s上。
Helm
有3个重要概念:
-
helm
:一个命令行客户端工具,主要用于k8s应用chart
的创建、打包、发布和管理 -
Chart
:应用描述,yaml
集合,一系列用于描述k8s资源相关文件的集合 -
Release
:基于Chart
的部署实体,一个chart
被Helm
运行后将会生成对应的一个release
;将在k8s中创建出真实运行的资源对象
当前稳定版本Helm v3
新特性:
- v3版本移除
Tiller
-
release
支持在不同命名空间中重用 - 支持将
chart
推送到docker
镜像仓库
helm
的架构如上,下面我们实际来使用一下helm
。
安装和配置仓库
安装helm
:
# 下载安装包
yjw@master:~/temp$ wget wget http://rancher-mirror.cnrancher.com/helm/v3.5.3/helm-v3.5.3-linux-amd64.tar.gz
...
Downloaded: 1 files, 12M in 3.9s (3.02 MB/s)
# 解压
yjw@master:~/temp$ tar -zxvf helm-v3.5.3-linux-amd64.tar.gz
linux-amd64/
linux-amd64/helm
linux-amd64/LICENSE
linux-amd64/README.md
# 移动到/usr/bin目录
yjw@master:~/temp$ sudo mv linux-amd64/helm /usr/bin/helm
# 添加可执行
yjw@master:~/temp$ chmod +x /usr/bin/helm
配置仓库:
# 添加国内仓库
helm repo add stable http://mirror.azure.cn/kubernetes/charts/
helm repo add aliyun https://kubernetes.oss-cn-hangzhou.aliyuncs.com/charts/
# 查看添加的仓库
helm repo list
输出
yjw@master:~$ helm repo list
NAME URL
stable http://mirror.azure.cn/kubernetes/charts/
aliyun https://kubernetes.oss-cn-hangzhou.aliyuncs.com/charts/
如果想删除:
# 删除
yjw@master:~$ helm repo remove aliyun
"aliyun" has been removed from your repositories
# 更新
yjw@master:~$ helm repo update
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "stable" chart repository
Update Complete. ⎈Happy Helming!⎈
# 查看最新
yjw@master:~$ helm repo list
NAME URL
stable http://mirror.azure.cn/kubernetes/charts/
快速部署应用
接下来我们用helm
来部署应用。这里以应用weave
为例。
① 搜索应用
yjw@master:~$ helm search repo weave
NAME CHART VERSION APP VERSION DESCRIPTION
stable/weave-cloud 0.3.9 1.4.0 DEPRECATED - Weave Cloud is a add-on to Kuberne...
stable/weave-scope 1.1.12 1.12.0 DEPRECATED - A Helm chart for the Weave Scope c...
②根据搜索内容选择安装
yjw@master:~$ helm install ui stable/weave-scope
WARNING: This chart is deprecated
W0510 13:17:19.397944 2491757 warnings.go:70] rbac.authorization.k8s.io/v1beta1 ClusterRole is deprecated in v1.17+, unavailable in v1.22+; use rbac.authorization.k8s.io/v1 ClusterRole
W0510 13:17:19.404973 2491757 warnings.go:70] rbac.authorization.k8s.io/v1beta1 ClusterRoleBinding is deprecated in v1.17+, unavailable in v1.22+; use rbac.authorization.k8s.io/v1 ClusterRoleBinding
W0510 13:17:19.541409 2491757 warnings.go:70] rbac.authorization.k8s.io/v1beta1 ClusterRole is deprecated in v1.17+, unavailable in v1.22+; use rbac.authorization.k8s.io/v1 ClusterRole
W0510 13:17:19.576348 2491757 warnings.go:70] rbac.authorization.k8s.io/v1beta1 ClusterRoleBinding is deprecated in v1.17+, unavailable in v1.22+; use rbac.authorization.k8s.io/v1 ClusterRoleBinding
NAME: ui
LAST DEPLOYED: Mon May 10 13:17:18 2021
NAMESPACE: default
STATUS: deployed
REVISION: 1
NOTES:
You should now be able to access the Scope frontend in your web browser, by
using kubectl port-forward:
kubectl -n default port-forward $(kubectl -n default get endpoints \
ui-weave-scope -o jsonpath='{.subsets[0].addresses[0].targetRef.name}') 8080:4040
then browsing to http://localhost:8080/.
For more details on using Weave Scope, see the Weave Scope documentation:
https://www.weave.works/docs/scope/latest/introducing/
③查看安装之后的状态
yjw@master:~$ helm status ui
NAME: ui
LAST DEPLOYED: Mon May 10 13:17:18 2021
NAMESPACE: default
STATUS: deployed
REVISION: 1
NOTES:
You should now be able to access the Scope frontend in your web browser, by
using kubectl port-forward:
kubectl -n default port-forward $(kubectl -n default get endpoints \
ui-weave-scope -o jsonpath='{.subsets[0].addresses[0].targetRef.name}') 8080:4040
then browsing to http://localhost:8080/.
For more details on using Weave Scope, see the Weave Scope documentation:
https://www.weave.works/docs/scope/latest/introducing/
yjw@master:~$ kubectl get po
NAME READY STATUS RESTARTS AGE
weave-scope-agent-ui-4zb64 1/1 Running 0 6m24s
weave-scope-agent-ui-bgxv6 1/1 Running 0 6m24s
weave-scope-agent-ui-v92sr 1/1 Running 0 6m24s
weave-scope-cluster-agent-ui-5cbc84db49-xf665 1/1 Running 0 6m24s
weave-scope-frontend-ui-6698fd5545-d6lxs 1/1 Running 0 6m24s
web-96d5df5c8-fmns2 1/1 Running 0 6h7m
yjw@master:~$ kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.43.0.1 <none> 443/TCP 7h10m
ui-weave-scope ClusterIP 10.43.253.226 <none> 80/TCP 6m45s
web NodePort 10.43.96.109 <none> 80:32303/TCP 6h7m
自定义chart部署
上面我们安装应用的过程中,并没有自己编写yaml
文件,是不是很方便。
yaml
文件实际上是通过chart
来部署的,本节来探讨如何自定义chart
。
①创建chart
yjw@master:~$ helm create mychart
Creating mychart
yjw@master:~$ cd mychart/
yjw@master:~/mychart$ ls
Chart.yaml charts templates values.yaml
该命令会创建同名的文件夹,里面有chart
模板。
yjw@master:~/mychart$ ll
...
-rw-r--r-- 1 yjw yjw 349 May 10 13:28 .helmignore
-rw-r--r-- 1 yjw yjw 1143 May 10 13:28 Chart.yaml # 当前chart属性配置信息
drwxr-xr-x 2 yjw yjw 4096 May 10 13:28 charts/
drwxr-xr-x 2 yjw yjw 4096 May 10 13:32 templates/ # 存放yaml文件集合
-rw-r--r-- 1 yjw yjw 1899 May 10 13:28 values.yaml # 定义yaml文件中可以使用的全局变量
②在templates
文件夹中创建两个yaml
文件
# 先创建deployment.yaml
yjw@master:~/mychart/templates$ kubectl create deploy web1 --image=nginx --dry-run=client -o yaml > deployment.yaml
# 真正部署web1,以方便生成service.yaml
yjw@master:~/mychart/templates$ kubectl create deploy web1 --image=nginx
deployment.apps/web1 created
# 创建service.yaml
yjw@master:~/mychart/templates$ kubectl expose deployment web1 --port=80 --target-port=80 --type=NodePort --dry-run=client -o yaml > service.yaml
# 删除deployment web1
yjw@master:~/mychart/templates$ kubectl delete deploy web1
deployment.apps "web1" deleted
这样,我们两个yaml
文件就创建好了。
③安装mychar:
yjw@master:~/mychart$ cd ~
yjw@master:~$ helm install web1 mychart
NAME: web1
LAST DEPLOYED: Mon May 10 13:41:38 2021
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None
yjw@master:~$ kubectl get pods
NAME READY STATUS RESTARTS AGE
weave-scope-agent-ui-4zb64 1/1 Running 0 25m
weave-scope-agent-ui-bgxv6 1/1 Running 0 25m
weave-scope-agent-ui-v92sr 1/1 Running 0 25m
weave-scope-cluster-agent-ui-5cbc84db49-xf665 1/1 Running 0 25m
weave-scope-frontend-ui-6698fd5545-d6lxs 1/1 Running 0 25m
web-96d5df5c8-fmns2 1/1 Running 0 6h26m
web1-6fbb48567f-wp87b 1/1 Running 0 43s
yjw@master:~$ kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.43.0.1 <none> 443/TCP 7h28m
ui-weave-scope ClusterIP 10.43.253.226 <none> 80/TCP 25m
web NodePort 10.43.96.109 <none> 80:32303/TCP 6h25m
web1 NodePort 10.43.251.126 <none> 80:32326/TCP 54s
可以看到我们自定义的web1
部署成功了。
④应用升级
yjw@master:~$ helm upgrade web1 mychart
Release "web1" has been upgraded. Happy Helming!
NAME: web1
LAST DEPLOYED: Mon May 10 13:43:46 2021
NAMESPACE: default
STATUS: deployed
REVISION: 2
TEST SUITE: None
chart模板使用
我们可以通过helm
实现yaml
文件高效复用:
- 通过传递参数,动态渲染模板,
yaml
内容根据参数动态生成
我们需要修改mychart
目录下的values.yaml
文件,定义全局变量。
本例中yaml
文件中大体上有几个地方是可变的:
- image
- tag
- label
- port
- replicas
①在values.yaml
中定义变量和值
values.yaml
:
replicas : 1
image: nginx
tag: 1.16
label: nginx
port: 80
②在具体yaml
中,获取定义的变量值
通过表达式形式使用变量
-
{{.Values.变量名称}}
-
{{.Release.Name}}
: 获取当前版本名称
修改deployment.yaml
为:
apiVersion: apps/v1
kind: Deployment
metadata:
creationTimestamp: null
labels:
app: {{ .Values.label}}
name: {{ .Release.Name}}-deploy
spec:
replicas: {{ .Values.replicas}}
selector:
matchLabels:
app: {{ .Values.label}}
strategy: {}
template:
metadata:
creationTimestamp: null
labels:
app: {{ .Values.label}}
spec:
containers:
- image: {{ .Values.image}}
name: nginx
resources: {}
status: {}
修改service.yaml
为:
apiVersion: v1
kind: Service
metadata:
creationTimestamp: null
labels:
app: {{ .Values.label}}
name: {{ .Release.Name}}-svc
spec:
ports:
- port: {{ .Values.port}}
protocol: TCP
targetPort: 80
selector:
app: {{ .Values.label}}
type: NodePort
status:
loadBalancer: {}
下面我们用dry-run
看一下效果:
yjw@master:~$ helm install --dry-run web2 mychart
NAME: web2
LAST DEPLOYED: Mon May 10 14:08:02 2021
NAMESPACE: default
STATUS: pending-install
REVISION: 1
TEST SUITE: None
HOOKS:
MANIFEST:
---
# Source: mychart/templates/service.yaml
apiVersion: v1
kind: Service
metadata:
creationTimestamp: null
labels:
app: nginx
name: web2-svc
spec:
ports:
- port: 80
protocol: TCP
targetPort: 80
selector:
app: nginx
type: NodePort
status:
loadBalancer: {}
---
# Source: mychart/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
creationTimestamp: null
labels:
app: nginx
name: web2-deploy
spec:
replicas: 1
selector:
matchLabels:
app: nginx
strategy: {}
template:
metadata:
creationTimestamp: null
labels:
app: nginx
spec:
containers:
- image: nginx
name: nginx
resources: {}
status: {}
可以看到yaml
文件修改成功,下面我们真正执行一下:
yjw@master:~$ helm install web2 mychart
NAME: web2
LAST DEPLOYED: Mon May 10 14:10:06 2021
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None
yjw@master:~$ kubectl get po
NAME READY STATUS RESTARTS AGE
weave-scope-agent-ui-4zb64 1/1 Running 0 52m
weave-scope-agent-ui-bgxv6 1/1 Running 0 52m
weave-scope-agent-ui-v92sr 1/1 Running 0 52m
weave-scope-cluster-agent-ui-5cbc84db49-xf665 1/1 Running 0 52m
weave-scope-frontend-ui-6698fd5545-d6lxs 1/1 Running 0 52m
web-96d5df5c8-fmns2 1/1 Running 0 6h54m
web1-6fbb48567f-wp87b 1/1 Running 0 28m
web2-deploy-6799fc88d8-x2gcl 1/1 Running 0 6s
yjw@master:~$ kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.43.0.1 <none> 443/TCP 7h56m
ui-weave-scope ClusterIP 10.43.253.226 <none> 80/TCP 52m
web NodePort 10.43.96.109 <none> 80:32303/TCP 6h53m
web1 NodePort 10.43.251.126 <none> 80:32326/TCP 28m
web2-svc NodePort 10.43.130.17 <none> 80:30073/TCP 11s
可以看到web2
部署成功,这样就实现了yaml
文件的高效复用。
持久化存储
持久化存储,即当pod
重启后,数据依然存在。
主要有两种方式:nfs网络存储和pv/pvc
nfs网络存储
①找一台服务器,专门做nfs服务端,设置挂载路径
博主找到了另外一台CentOS系统:
[root@instance-54lh4cfv graves]# yum install -y nfs-utils
/etc/exports
:
/data/nfs *(rw,no_root_squash)
设置挂载路径:
[root@instance-54lh4cfv graves]# vim /etc/exports
[root@instance-54lh4cfv graves]# cat /etc/exports
/data/nfs *(rw,no_root_squash)
[root@instance-54lh4cfv graves]# mkdir /data/nfs
②在k8s集群节点上安装nfs
在集群所有节点都执行以下命令
sudo apt install nfs-kernel-server
③在nfs服务端启动nfs服务
在CentOS系统上执行:
[root@instance-54lh4cfv ~]# systemctl start nfs
[root@instance-54lh4cfv ~]# ps -ef | grep nfs
root 16612 2 0 22:52 ? 00:00:00 [nfsd4_callbacks]
root 16618 2 0 22:52 ? 00:00:00 [nfsd]
root 16619 2 0 22:52 ? 00:00:00 [nfsd]
root 16620 2 0 22:52 ? 00:00:00 [nfsd]
root 16621 2 0 22:52 ? 00:00:00 [nfsd]
root 16622 2 0 22:52 ? 00:00:00 [nfsd]
root 16623 2 0 22:52 ? 00:00:00 [nfsd]
root 16624 2 0 22:52 ? 00:00:00 [nfsd]
root 16625 2 0 22:52 ? 00:00:00 [nfsd]
root 16636 16525 0 22:52 pts/1 00:00:00 grep --color=auto nfs
在master节点上执行新建文件夹,保存以下内容到nfs-nginx.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-dep1
spec:
replicas: 1
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx
volumeMounts:
- name: wwwroot
mountPath: /usr/share/nginx/html # 将此路径内容挂载到nfs服务器 /data/nfs上
ports:
- containerPort: 80
volumes:
- name: wwwroot
nfs:
server: 106.12.109.x # CentOS服务器地址
path:
应用:
yjw@master:~/pv$ kubectl apply -f nfs-nginx.yaml
deployment.apps/nginx-dep1 created
yjw@master:~/pv$ kubectl get po
NAME READY STATUS RESTARTS AGE
nginx-dep1-58664cb68b-cw7qb 1/1 Running 0 12s
...
# 进入该pod
yjw@master:~/pv$ kubectl exec -it nginx-dep1-58664cb68b-cw7qb -- bash
# 目前该目录为空
root@nginx-dep1-58664cb68b-cw7qb:/# ls /usr/share/nginx/html/
然后我们在CentOS对应的目录下新建一个文件:
[root@instance-54lh4cfv nfs]# vim index.html
[root@instance-54lh4cfv nfs]# cat index.html
hello from nfs
[root@instance-54lh4cfv nfs]#
再切换回master查看:
root@nginx-dep1-58664cb68b-cw7qb:/# ls /usr/share/nginx/html/
index.html
root@nginx-dep1-58664cb68b-cw7qb:/# cat /usr/share/nginx/html/index.html
hello from nfs
此时文件已经同步过来了,相当于文件可以持久化到nfs服务器上。
pv/pvc
本节我们介绍另一种方案,涉及到两个概念:pv和pvc。
- pv (PersistentVolume) : 持久化存储,对存储资源进抽象,对外提供可以调用的接口(生产者)
- pvc (PersistentVolumeClaim):用于调度,不需要关心内部实现细节(消费者)
实现流程如下:
下面来实操一下。
pvc.yaml
:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-dep1
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx
volumeMounts:
- name: wwwroot
mountPath: /usr/share/nginx/html
ports:
- containerPort: 80
volumes:
- name: wwwroot
persistentVolumeClaim: # pvc
claimName: my-pvc
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: my-pvc
spec:
accessModes:
- ReadWriteMany # 匹配模式
resources:
requests:
storage: 5Gi # 容量
pvc
不关心实现细节,由pv
来具体实现。这里我们还是通过nfs。
pv.yaml
:
apiVersion: v1
kind: PersistentVolume
metadata:
name: my-pv
spec:
capacity:
storage: 5Gi
accessModes:
- ReadWriteMany
nfs:
path: /k8s/nfs
server:
应用:
# 部署pvc
yjw@master:~/temp$ kubectl apply -f pvc.yaml
deployment.apps/nginx-dep1 configured
persistentvolumeclaim/my-pvc created
# 部署pv
yjw@master:~/temp$ kubectl apply -f pv.yaml
persistentvolume/my-pv created
# 查看部署情况
yjw@master:~/temp$ kubectl get pv,pvc
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
persistentvolume/my-pv 5Gi RWX Retain Bound default/my-pvc 27s
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
persistentvolumeclaim/my-pvc Bound my-pv 5Gi RWX 2m58s
# 查看Pod情况
yjw@master:~/temp$ kubectl get po
NAME READY STATUS RESTARTS AGE
nginx-dep1-69f5bb95b-dqlfs 1/1 Running 0 29s
nginx-dep1-69f5bb95b-ftjjk 1/1 Running 0 37s
nginx-dep1-69f5bb95b-z47s5 1/1 Running 0 3m24s
...
# 进入pod
yjw@master:~/temp$ kubectl exec -it nginx-dep1-69f5bb95b-dqlfs -- bash
# 查看目录
root@nginx-dep1-69f5bb95b-dqlfs:/# ls /usr/share/nginx/html/
index.html
root@nginx-dep1-69f5bb95b-dqlfs:/#
参考
- K8s官网文档
- Kubernetes Handbook——Kubernetes 中文指南/云原生应用架构实践手册
- Kubernetes进阶实战(第2版)