Cert Manager 使用随记

在 Kubernetes 中常用 Cert Manager 生成并管理自签名证书,常见的 CR 有👇

  1. Issuer: 用于定义如何生成证书
  2. ClusterIssuer: 用于定义如何生成集群级别的证书
  3. Certificate: 用来请求和管理证书的主要资源
  4. CertificateRequest: 是用于手动请求证书的资源
  5. Order: 当使用 ACME 协议时会生成,
  6. Challenge: 是 ACME 协议中的一部分,用于表示 ACME 服务对域名所有权的验证

一个自签的简单实用例子

自签名生成 CA 证书和私钥

1
2
3
4
5
6
7
8
9
# 生成 CA 的私钥
openssl genrsa -out ca.key 2048

# 生成自签名的 CA 证书
openssl req -x509 -new -nodes -key ca.key -subj "/CN=nginx-ca" -days 365 -out ca.crt

kubectl create secret tls tls-nginx \
--cert=ca.crt \
--key=ca.key

通过 Issue 创建自签名

1
2
3
4
5
6
7
8
9
10
cat <<EOF | kubectl apply -f -
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
name: nginx-issuer
namespace: default
spec:
ca:
secretName: tls-nginx
EOF

通过 Certificate 创建证书请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
cat <<EOF | kubectl apply -f -
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: nginx-certificate
namespace: default
spec:
secretName: tls-nginx
duration: 24h
renewBefore: 12h
commonName: nginx.warnerchen.io
dnsNames:
- nginx.warnerchen.io
issuerRef:
name: nginx-issuer
kind: Issuer
EOF

给 Ingress 挂载后,尝试请求

请求Ingress

记录一次ipv4_forward被修改导致的生产事故

生产集群的节点内核模块被异常修改,导致集群服务与服务之间网络通信异常,产生了较大规模的生产事故。

此次事故涉及到两个主要的内核模块被修改:

  1. net.ipv4.ip_forward: 用于启用 IP 转发,当此模块加载时,Linux 内核会允许将数据包转发到其他网络

尝试复现

在一个集群中启用两个 Pod,通过这两个 Pod 模拟业务
测试连通性的pod

修改 controller-node-2 节点的 /etc/sysctl.d/99-sysctl.conf 文件,并加载(sysctl -p)
修改节点内核配置

1
net.ipv4.ip_forward=0

此时再去测试连通性,已经不通了
修改节点内核参数后测试连通性

尽管是在同一个宿主机上的 Pod,也无法进行通信
同一宿主机的pod进行通信

节点之间能够正常通信
节点之间通信

查看 calico 组网状态,显示正常
修改节点内核参数后calico组网状态

通过正常节点的 Pod 去 ping 异常节点的 Pod,正常节点抓包,发现没有回包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[root@controller-node-1 ~]# tcpdump -i any host 10.233.74.83 or 10.233.76.142 -nnvvv
tcpdump: listening on any, link-type LINUX_SLL (Linux cooked), capture size 262144 bytes
16:14:10.762727 IP (tos 0x0, ttl 64, id 37977, offset 0, flags [DF], proto ICMP (1), length 84)
10.233.74.83 > 10.233.76.142: ICMP echo request, id 265, seq 0, length 64
16:14:10.762787 IP (tos 0x0, ttl 63, id 37977, offset 0, flags [DF], proto ICMP (1), length 84)
10.233.74.83 > 10.233.76.142: ICMP echo request, id 265, seq 0, length 64
16:14:11.762953 IP (tos 0x0, ttl 64, id 38291, offset 0, flags [DF], proto ICMP (1), length 84)
10.233.74.83 > 10.233.76.142: ICMP echo request, id 265, seq 1, length 64
16:14:11.763002 IP (tos 0x0, ttl 63, id 38291, offset 0, flags [DF], proto ICMP (1), length 84)
10.233.74.83 > 10.233.76.142: ICMP echo request, id 265, seq 1, length 64
16:14:12.763208 IP (tos 0x0, ttl 64, id 38345, offset 0, flags [DF], proto ICMP (1), length 84)
10.233.74.83 > 10.233.76.142: ICMP echo request, id 265, seq 2, length 64
16:14:12.763253 IP (tos 0x0, ttl 63, id 38345, offset 0, flags [DF], proto ICMP (1), length 84)
10.233.74.83 > 10.233.76.142: ICMP echo request, id 265, seq 2, length 64

异常节点抓包,发现 icmp 包有到达该节点上,但目标地址没有进行响应,说明流量没有抵达目的地 Pod

1
2
3
4
5
6
7
8
[root@controller-node-2 ~]# tcpdump -i any host 10.233.74.83 or 10.233.76.142 -nnvvv
tcpdump: listening on any, link-type LINUX_SLL (Linux cooked), capture size 262144 bytes
16:15:00.019656 IP (tos 0x0, ttl 63, id 42391, offset 0, flags [DF], proto ICMP (1), length 84)
10.233.74.83 > 10.233.76.142: ICMP echo request, id 271, seq 0, length 64
16:15:01.019960 IP (tos 0x0, ttl 63, id 43082, offset 0, flags [DF], proto ICMP (1), length 84)
10.233.74.83 > 10.233.76.142: ICMP echo request, id 271, seq 1, length 64
16:15:02.020072 IP (tos 0x0, ttl 63, id 43594, offset 0, flags [DF], proto ICMP (1), length 84)
10.233.74.83 > 10.233.76.142: ICMP echo request, id 271, seq 2, length 64

通过异常节点的 Pod 去 ping 正常节点的 Pod,正常节点抓包,发现没有任何包,说明流量没有从异常节点转发出来

1
2
[root@controller-node-1 ~]# tcpdump -i any host 10.233.74.83 or 10.233.76.142 -nnvvv
tcpdump: listening on any, link-type LINUX_SLL (Linux cooked), capture size 262144 bytes

异常节点,进入 Pod 对应的网络命名空间进行抓包,可以看到有 icmp 的请求包,但依旧没有收到响应

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
[root@controller-node-2 ~]# nsenter -n -t 23056
[root@controller-node-2 ~]# ip -4 a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
4: eth0@if12: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1480 qdisc noqueue state UP group default qlen 1000 link-netnsid 0
inet 10.233.76.142/32 scope global eth0
valid_lft forever preferred_lft forever
[root@controller-node-2 ~]# tcpdump -i any host 10.233.74.83 or 10.233.76.142 -nnvvv
tcpdump: listening on any, link-type LINUX_SLL (Linux cooked), capture size 262144 bytes
16:22:42.911202 ARP, Ethernet (len 6), IPv4 (len 4), Request who-has 169.254.1.1 tell 10.233.76.142, length 28
16:22:43.913783 ARP, Ethernet (len 6), IPv4 (len 4), Request who-has 169.254.1.1 tell 10.233.76.142, length 28
16:22:44.915789 ARP, Ethernet (len 6), IPv4 (len 4), Request who-has 169.254.1.1 tell 10.233.76.142, length 28
16:22:45.917834 IP (tos 0xc0, ttl 64, id 52805, offset 0, flags [none], proto ICMP (1), length 112)
10.233.76.142 > 10.233.76.142: ICMP host 10.233.74.83 unreachable, length 92
IP (tos 0x0, ttl 64, id 26248, offset 0, flags [DF], proto ICMP (1), length 84)
10.233.76.142 > 10.233.74.83: ICMP echo request, id 90, seq 0, length 64
16:22:45.917839 IP (tos 0xc0, ttl 64, id 52806, offset 0, flags [none], proto ICMP (1), length 112)
10.233.76.142 > 10.233.76.142: ICMP host 10.233.74.83 unreachable, length 92
IP (tos 0x0, ttl 64, id 26601, offset 0, flags [DF], proto ICMP (1), length 84)
10.233.76.142 > 10.233.74.83: ICMP echo request, id 90, seq 1, length 64
16:22:45.917841 IP (tos 0xc0, ttl 64, id 52807, offset 0, flags [none], proto ICMP (1), length 112)
10.233.76.142 > 10.233.76.142: ICMP host 10.233.74.83 unreachable, length 92
IP (tos 0x0, ttl 64, id 26809, offset 0, flags [DF], proto ICMP (1), length 84)
10.233.76.142 > 10.233.74.83: ICMP echo request, id 90, seq 2, length 64

尝试重启该节点的 calico-node,内核模块会被 calico-node 修改回来,此时网络恢复,但 /etc/sysctl.d/99-sysctl.conf 中的 net.ipv4.ip_forward 还是 0,所以在下次重新加载(sysctl -p)的时候,仍然会被设置为关闭状态
重启calico-node后测试连通性
重启calico-node后内核参数

事故总结

此次事故的排障思路是:

  1. 通过两个在不同宿主机的 Pod,测试跨节点的连通性,不通
  2. 测试节点之间的连通性,能够正常通信
  3. 在这两个宿主机进行同一宿主机不同 Pod 的连通性测试,一台通,一台不通 – 确定问题节点
  4. 通过 calicoctl node status 查看组网状态,显示正常 – 暂且排除是 calico 的问题
  5. 通过 tcpdump 进行抓包,获取正常节点 Pod 到异常节点 Pod 的数据包 – icmp 数据包能够到达异常节点,但异常节点的 Pod 没有响应
  6. 通过 tcpdump 进行抓包,获取异常节点 Pod 到正常节点 Pod 的数据包 – 异常节点宿主机层面无法获取 icmp 包,通过 nsenter 进入 Pod 的网络命名空间发现,icmp 有发出但无响应,且 icmp 数据包无法到达正常节点,正常节点抓包观察没有任何包
  7. 尝试重启异常节点 calico-node,网络恢复 – calico-node 启动会修改内核参数,但不会持久化到 /etc/sysctl.d/99-sysctl.conf
  8. 查看 /etc/sysctl.d/99-sysctl.conf 发现 net.ipv4.ip_forward 被设置为了 0
  9. 通过 ansible 检查所有节点的 /etc/sysctl.d/99-sysctl.conf 文件

K8s给命名空间设置专享节点

是指在特定命名空间下部署的 Pod 都会被调度到指定标签的节点上,这个功能需要在 kube-apiserver 添加 PodNodeSelector 参数

1
2
3
4
5
6
7
8
9
10
11
apiVersion: v1
kind: Pod
metadata:
labels:
component: kube-apiserver
tier: control-plane
name: kube-apiserver
namespace: kube-system
...
- --enable-admission-plugins=NodeRestriction,PodNodeSelector
...

然后给命名空间添加 annotation

1
2
3
4
5
apiVersion: v1
kind: Namespace
metadata:
annotations:
scheduler.alpha.kubernetes.io/node-selector: test.io/app=true

CoreDNS和NodeLocalDNS的域名解析

在 K8s 中,DNS 的解析主要用这两个工具:

  1. CoreDNS: 主要负责集群内部域名解析
  2. NodeLocalDNS: 提供 DNS 缓存

首先看一下集群中节点的 /etc/resolv.conf 配置文件
node-resolv-conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 指定了搜索域,当使用域名解析主机名时,如果主机名没有完全限定,系统会依次尝试在指定的搜索域中追加搜索后缀进行解析
# 例如解析 test,则会尝试解析 test.default.svc.cluster.local 和 test.svc.cluster.local
search default.svc.cluster.local svc.cluster.local

# nodelocaldns 服务器的地址,如果集群中没有 nodelocaldns,那么这个地址就会替换成 CoreDNS 的 SVC ClusterIP
nameserver 169.254.25.10

# 其他 DNS 服务器地址
nameserver 192.168.0.5
nameserver 223.5.5.5

# 解析的域名最多包涵 2 个点,最多超时 2s,最多重试 2 次
# 假设解析a,会尝试追加 default.svc.cluster.local 和 svc.cluster.local
# 假设解析a.b,则不会,除非 ndots > 2
options ndots:2 timeout:2 attempts:2

在 K8s 中,workload 的 dnsPolicy 有四种类型:

  1. ClusterFirst: 与配置的集群域后缀不匹配的任何 DNS 查询(例如 “www.kubernetes.io") 都会由 DNS 服务器转发到上游名称服务器
  2. Default: 从运行所在的节点继承名称解析配置
  3. ClusterFirstWithHostNet: 对于以 hostNetwork 方式运行的 Pod,应将其 DNS 策略显式设置为 ClusterFirstWithHostNet。否则,以 hostNetwork 方式和 ClusterFirst 策略运行的 Pod 将会做出回退至 Default 策略的行为
  4. None: 会使用 dnsConfig 提供的 DNS 配置

CoreDNS

CoreDNS 主要是复杂集群内部域名的解析,保证 Pod 与 Pod 只见可以通过 Service Name 进行通信。当然也可以为它添加配置,使其能够解析静态的一些 Host 👇

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
kind: ConfigMap
apiVersion: v1
metadata:
name: coredns
namespace: kube-system
data:
Corefile: |
.:53 {
errors {
}
health {
lameduck 5s
}
ready
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
fallthrough in-addr.arpa ip6.arpa
}
prometheus :9153
forward . /etc/resolv.conf {
prefer_udp
max_concurrent 1000
}
hosts /etc/coredns/Hosts {
ttl 60
reload 1m
fallthrough
}
cache 30
loop
reload
loadbalance
}
Hosts: |+
192.168.0.10 cqm.com

创建一个 dnsPolicy 为 ClusterFirst 的 busybox
busybox

通过这个 busybox 解析集群内部域名,通过 tcpdump 获取 53 端口的包
通过coredns解析集群内部域名

可以看到会根据 search 对 CoreDNS 发起多次请求

Pod -> CoreDNS -> Pod

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
[root@controller-node-1 ~]# tcpdump -i any host 10.233.0.3 and 10.233.74.83 and port 53 -nnvvv
tcpdump: listening on any, link-type LINUX_SLL (Linux cooked), capture size 262144 bytes
22:52:20.447820 IP (tos 0x0, ttl 64, id 37856, offset 0, flags [DF], proto UDP (17), length 82)
10.233.74.83.58663 > 10.233.0.3.53: [bad udp cksum 0x6077 -> 0x753d!] 33482+ A? kubernetes.default.svc.cluster.local. (54)
22:52:20.447950 IP (tos 0x0, ttl 64, id 37857, offset 0, flags [DF], proto UDP (17), length 74)
10.233.74.83.58663 > 10.233.0.3.53: [bad udp cksum 0x606f -> 0xdd2e!] 53027+ A? kubernetes.svc.cluster.local. (46)
22:52:20.447978 IP (tos 0x0, ttl 64, id 37858, offset 0, flags [DF], proto UDP (17), length 70)
10.233.74.83.58663 > 10.233.0.3.53: [bad udp cksum 0x606b -> 0x0aa3!] 30769+ A? kubernetes.cluster.local. (42)
22:52:20.447999 IP (tos 0x0, ttl 64, id 37859, offset 0, flags [DF], proto UDP (17), length 82)
10.233.74.83.58663 > 10.233.0.3.53: [bad udp cksum 0x6077 -> 0x2dde!] 51726+ AAAA? kubernetes.default.svc.cluster.local. (54)
22:52:20.448017 IP (tos 0x0, ttl 64, id 37860, offset 0, flags [DF], proto UDP (17), length 74)
10.233.74.83.58663 > 10.233.0.3.53: [bad udp cksum 0x606f -> 0xba24!] 61970+ AAAA? kubernetes.svc.cluster.local. (46)
22:52:20.448038 IP (tos 0x0, ttl 64, id 37861, offset 0, flags [DF], proto UDP (17), length 70)
10.233.74.83.58663 > 10.233.0.3.53: [bad udp cksum 0x606b -> 0x7758!] 2913+ AAAA? kubernetes.cluster.local. (42)
22:52:20.448407 IP (tos 0x0, ttl 63, id 32091, offset 0, flags [DF], proto UDP (17), length 163)
10.233.0.3.53 > 10.233.74.83.58663: [bad udp cksum 0x60c8 -> 0xd926!] 2913 NXDomain*- q: AAAA? kubernetes.cluster.local. 0/1/0 ns: cluster.local. [5s] SOA ns.dns.cluster.local. hostmaster.cluster.local. 1711809447 7200 1800 86400 5 (135)
22:52:20.448557 IP (tos 0x0, ttl 63, id 32092, offset 0, flags [DF], proto UDP (17), length 175)
10.233.0.3.53 > 10.233.74.83.58663: [bad udp cksum 0x60d4 -> 0x8faf!] 51726*- q: AAAA? kubernetes.default.svc.cluster.local. 0/1/0 ns: cluster.local. [5s] SOA ns.dns.cluster.local. hostmaster.cluster.local. 1711809447 7200 1800 86400 5 (147)
22:52:20.448654 IP (tos 0x0, ttl 63, id 32093, offset 0, flags [DF], proto UDP (17), length 163)
10.233.0.3.53 > 10.233.74.83.58663: [bad udp cksum 0x60c8 -> 0x6c71!] 30769 NXDomain*- q: A? kubernetes.cluster.local. 0/1/0 ns: cluster.local. [5s] SOA ns.dns.cluster.local. hostmaster.cluster.local. 1711809447 7200 1800 86400 5 (135)
22:52:20.448699 IP (tos 0x0, ttl 63, id 32094, offset 0, flags [DF], proto UDP (17), length 167)
10.233.0.3.53 > 10.233.74.83.58663: [bad udp cksum 0x60cc -> 0x1bf3!] 61970 NXDomain*- q: AAAA? kubernetes.svc.cluster.local. 0/1/0 ns: cluster.local. [5s] SOA ns.dns.cluster.local. hostmaster.cluster.local. 1711809447 7200 1800 86400 5 (139)
22:52:20.448761 IP (tos 0x0, ttl 63, id 32095, offset 0, flags [DF], proto UDP (17), length 134)
10.233.0.3.53 > 10.233.74.83.58663: [bad udp cksum 0x60ab -> 0x24fc!] 33482*- q: A? kubernetes.default.svc.cluster.local. 1/0/0 kubernetes.default.svc.cluster.local. [5s] A 10.233.0.1 (106)
22:52:20.448811 IP (tos 0x0, ttl 63, id 32096, offset 0, flags [DF], proto UDP (17), length 167)
10.233.0.3.53 > 10.233.74.83.58663: [bad udp cksum 0x60cc -> 0x3efd!] 53027 NXDomain*- q: A? kubernetes.svc.cluster.local. 0/1/0 ns: cluster.local. [5s] SOA ns.dns.cluster.local. hostmaster.cluster.local. 1711809447 7200 1800 86400 5 (139)

通过这个 busybox 解析集群公网域名
通过coredns解析集群公网域名

可以看到对 www.baidu.com 域名的解析返回结果,是通过 192.168.0.5 获取到的

Pod -> CoreDNS -> 192.168.0.5 -> CoreDNS -> Pod

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
[root@controller-node-1 ~]#  tcpdump -i any host 10.233.74.83 and 10.233.0.3 or 192.168.0.5 and port 53 -nnvvv
tcpdump: listening on any, link-type LINUX_SLL (Linux cooked), capture size 262144 bytes
22:58:29.405332 IP (tos 0x0, ttl 64, id 48447, offset 0, flags [DF], proto UDP (17), length 59)
10.233.74.83.38430 > 10.233.0.3.53: [bad udp cksum 0x6060 -> 0xdcc6!] 28765+ A? www.baidu.com. (31)
22:58:29.405423 IP (tos 0x0, ttl 64, id 48448, offset 0, flags [DF], proto UDP (17), length 59)
10.233.74.83.38430 > 10.233.0.3.53: [bad udp cksum 0x6060 -> 0xcdcd!] 25686+ AAAA? www.baidu.com. (31)
22:58:29.405881 IP (tos 0x0, ttl 64, id 42712, offset 0, flags [DF], proto UDP (17), length 59)
10.233.74.66.56555 > 192.168.0.5.53: [bad udp cksum 0x1611 -> 0x2f90!] 1558+ AAAA? www.baidu.com. (31)
22:58:29.405950 IP (tos 0x0, ttl 63, id 42712, offset 0, flags [DF], proto UDP (17), length 59)
192.168.159.41.56555 > 192.168.0.5.53: [bad udp cksum 0x20b8 -> 0x24e9!] 1558+ AAAA? www.baidu.com. (31)
22:58:29.406110 IP (tos 0x0, ttl 64, id 42713, offset 0, flags [DF], proto UDP (17), length 59)
10.233.74.66.58403 > 192.168.0.5.53: [bad udp cksum 0x1611 -> 0xf518!] 21589+ A? www.baidu.com. (31)
22:58:29.406191 IP (tos 0x0, ttl 63, id 42713, offset 0, flags [DF], proto UDP (17), length 59)
192.168.159.41.58403 > 192.168.0.5.53: [bad udp cksum 0x20b8 -> 0xea71!] 21589+ A? www.baidu.com. (31)
22:58:29.410592 IP (tos 0x0, ttl 63, id 39983, offset 0, flags [DF], proto UDP (17), length 118)
192.168.0.5.53 > 192.168.159.41.58403: [udp sum ok] 21589 q: A? www.baidu.com. 3/0/0 www.baidu.com. [2m9s] CNAME www.a.shifen.com., www.a.shifen.com. [2m9s] A 183.2.172.185, www.a.shifen.com. [2m9s] A 183.2.172.42 (90)
22:58:29.410637 IP (tos 0x0, ttl 62, id 39983, offset 0, flags [DF], proto UDP (17), length 118)
192.168.0.5.53 > 10.233.74.66.58403: [udp sum ok] 21589 q: A? www.baidu.com. 3/0/0 www.baidu.com. [2m9s] CNAME www.a.shifen.com., www.a.shifen.com. [2m9s] A 183.2.172.185, www.a.shifen.com. [2m9s] A 183.2.172.42 (90)
22:58:29.410972 IP (tos 0x0, ttl 63, id 64831, offset 0, flags [DF], proto UDP (17), length 166)
10.233.0.3.53 > 10.233.74.83.38430: [bad udp cksum 0x60cb -> 0xe43e!] 28765 q: A? www.baidu.com. 3/0/0 www.baidu.com. [30s] CNAME www.a.shifen.com., www.a.shifen.com. [30s] A 183.2.172.42, www.a.shifen.com. [30s] A 183.2.172.185 (138)
22:58:29.413159 IP (tos 0x0, ttl 63, id 39984, offset 0, flags [DF], proto UDP (17), length 142)
192.168.0.5.53 > 192.168.159.41.56555: [udp sum ok] 1558 q: AAAA? www.baidu.com. 3/0/0 www.baidu.com. [3m29s] CNAME www.a.shifen.com., www.a.shifen.com. [3m29s] AAAA 240e:ff:e020:9ae:0:ff:b014:8e8b, www.a.shifen.com. [3m29s] AAAA 240e:ff:e020:966:0:ff:b042:f296 (114)
22:58:29.413203 IP (tos 0x0, ttl 62, id 39984, offset 0, flags [DF], proto UDP (17), length 142)
192.168.0.5.53 > 10.233.74.66.56555: [udp sum ok] 1558 q: AAAA? www.baidu.com. 3/0/0 www.baidu.com. [3m29s] CNAME www.a.shifen.com., www.a.shifen.com. [3m29s] AAAA 240e:ff:e020:9ae:0:ff:b014:8e8b, www.a.shifen.com. [3m29s] AAAA 240e:ff:e020:966:0:ff:b042:f296 (114)
22:58:29.413922 IP (tos 0x0, ttl 63, id 64833, offset 0, flags [DF], proto UDP (17), length 190)
10.233.0.3.53 > 10.233.74.83.38430: [bad udp cksum 0x60e3 -> 0x9ac9!] 25686 q: AAAA? www.baidu.com. 3/0/0 www.baidu.com. [30s] CNAME www.a.shifen.com., www.a.shifen.com. [30s] AAAA 240e:ff:e020:966:0:ff:b042:f296, www.a.shifen.com. [30s] AAAA 240e:ff:e020:9ae:0:ff:b014:8e8b (162)

NodeLocalDNS

来自官方文档: NodeLocal DNSCache 通过在集群节点上作为 DaemonSet 运行 DNS 缓存代理来提高集群 DNS 性能。

NodeLocalDNS Pod 中的 /etc/resolv.conf 与宿主机是相同的
nodelocaldns的resolv配置

节点上也会有对应的网卡,这个地址取决于启动参数
nodelocaldns网卡

在集群有 NodeLocalDNS 的情况下,在对集群内部域名进行解析,可以看到 DNS 请求会先经过 NodeLocalDNS,然后直接返回请求

Pod -> NodeLocalDNS -> Pod

1
2
3
4
5
6
7
8
9
10
[root@controller-node-1 ~]# tcpdump -i any host 10.233.74.83 and 169.254.25.10 or 10.233.0.3 and port 53 -nnvvv
tcpdump: listening on any, link-type LINUX_SLL (Linux cooked), capture size 262144 bytes
23:19:46.273718 IP (tos 0x0, ttl 64, id 61227, offset 0, flags [DF], proto UDP (17), length 82)
10.233.74.83.36711 > 169.254.25.10.53: [bad udp cksum 0x1894 -> 0x3f9b!] 22032+ A? kubernetes.default.svc.cluster.local. (54)
23:19:46.273793 IP (tos 0x0, ttl 64, id 61228, offset 0, flags [DF], proto UDP (17), length 82)
10.233.74.83.36711 > 169.254.25.10.53: [bad udp cksum 0x1894 -> 0xb26b!] 58148+ AAAA? kubernetes.default.svc.cluster.local. (54)
23:19:46.275678 IP (tos 0x0, ttl 64, id 60479, offset 0, flags [DF], proto UDP (17), length 134)
169.254.25.10.53 > 10.233.74.83.36711: [bad udp cksum 0x18c8 -> 0xef59!] 22032*- q: A? kubernetes.default.svc.cluster.local. 1/0/0 kubernetes.default.svc.cluster.local. [5s] A 10.233.0.1 (106)
23:19:46.275946 IP (tos 0x0, ttl 64, id 60480, offset 0, flags [DF], proto UDP (17), length 175)
169.254.25.10.53 > 10.233.74.83.36711: [bad udp cksum 0x18f1 -> 0x5d58!] 58148*- q: AAAA? kubernetes.default.svc.cluster.local. 0/1/0 ns: cluster.local. [5s] SOA ns.dns.cluster.local. hostmaster.cluster.local. 1711802462 7200 1800 86400 5 (147)

然后在对集群外部域名进行解析

Pod -> NodeLocalDNS -> CoreDNS -> 192.168.0.5 -> CoreDNS -> NodeLocalDNS -> Pod

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
23:27:25.569362 IP (tos 0x0, ttl 64, id 36019, offset 0, flags [DF], proto UDP (17), length 60)
10.233.74.83.42419 > 169.254.25.10.53: [bad udp cksum 0x187e -> 0x94cd!] 7676+ A? www.google.com. (32)
23:27:25.569423 IP (tos 0x0, ttl 64, id 36020, offset 0, flags [DF], proto UDP (17), length 60)
10.233.74.83.42419 > 169.254.25.10.53: [bad udp cksum 0x187e -> 0xb5c9!] 64740+ AAAA? www.google.com. (32)
23:27:25.569823 IP (tos 0x0, ttl 64, id 65347, offset 0, flags [DF], proto UDP (17), length 71)
192.168.159.11.57457 > 192.168.0.5.53: [bad udp cksum 0x20a6 -> 0x7971!] 52418+ [1au] AAAA? www.google.com. ar: . OPT UDPsize=2048 DO (43)
23:27:25.569841 IP (tos 0x0, ttl 64, id 65348, offset 0, flags [DF], proto UDP (17), length 71)
192.168.159.11.33304 > 192.168.0.5.53: [bad udp cksum 0x20a6 -> 0xa430!] 120+ [1au] A? www.google.com. ar: . OPT UDPsize=2048 DO (43)
23:27:25.571423 IP (tos 0x0, ttl 63, id 18081, offset 0, flags [DF], proto UDP (17), length 152)
192.168.0.5.53 > 192.168.159.11.57457: [udp sum ok] 52418 q: AAAA? www.google.com. 0/1/1 ns: www.google.com. [10s] SOA fake-for-negative-caching.adguard.com. hostmaster.www.google.com. 100500 1800 60 604800 86400 ar: . OPT UDPsize=2048 DO (124)
23:27:25.571763 IP (tos 0x0, ttl 64, id 40563, offset 0, flags [DF], proto UDP (17), length 172)
169.254.25.10.53 > 10.233.74.83.42419: [bad udp cksum 0x18ee -> 0x3576!] 64740 q: AAAA? www.google.com. 0/1/0 ns: www.google.com. [10s] SOA fake-for-negative-caching.adguard.com. hostmaster.www.google.com. 100500 1800 60 604800 86400 (144)
23:27:25.573701 IP (tos 0x0, ttl 63, id 18082, offset 0, flags [DF], proto UDP (17), length 87)
192.168.0.5.53 > 192.168.159.11.33304: [udp sum ok] 120 q: A? www.google.com. 1/0/1 www.google.com. [1m30s] A 216.58.203.68 ar: . OPT UDPsize=2048 DO (59)
23:27:25.573927 IP (tos 0x0, ttl 64, id 40564, offset 0, flags [DF], proto UDP (17), length 90)
169.254.25.10.53 > 10.233.74.83.42419: [bad udp cksum 0x189c -> 0xe2c7!] 7676 q: A? www.google.com. 1/0/0 www.google.com. [30s] A 216.58.203.68 (62)

kube-proxy工作原理

kube-proxy 以 DaemonSet 的形式运行在集群的所有节点中,负责管理集群外到 Service,以及 Service 到 Pod 的流量转发。

kube-proxy 有三种模式:

  1. userspace
  2. iptables
  3. ipvs

在运行了 kube-proxy 的节点上,可以通过下面的命令查看 kube-proxy 的运行模式:

1
curl localhost:10249/proxyMode

iptables

通过 kube-proxy 的 cm 可以查看 mode
iptables-mode

我们都知道 SVC 有多种类型,常用的有 ClusterIP、NodePort、Headless、LoadBalancer 等等,这里我们通过创建类型的 SVC,查看 kube-proxy 下发的iptables 规则

ClusterIP
clusterip-svc

通过 iptables-save 命令打印这个 SVC 的规则
clusterip-iptables

PodIP: 10.233.74.106
ClusterIP: 10.233.54.175

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 将来自 Pod IP 10.233.74.106 的流量标记为服务 default/nginx,并转发到 KUBE-MARK-MASQ 链中
-A KUBE-SEP-EUB75IW55XDW6EHN -s 10.233.74.106/32 -m comment --comment "default/nginx:tcp-80" -j KUBE-MARK-MASQ

# 将匹配的 TCP 流量进行 DNAT,转发到后端 Pod 10.233.74.106:80
-A KUBE-SEP-EUB75IW55XDW6EHN -p tcp -m comment --comment "default/nginx:tcp-80" -m tcp -j DNAT --to-destination 10.233.74.106:80

# 匹配目标地址为服务 default/nginx 的 cluster IP 10.233.54.175 的 TCP 80 端口流量
-A KUBE-SERVICES -d 10.233.54.175/32 -p tcp -m comment --comment "default/nginx:tcp-80 cluster IP" -m tcp --dport 80 -j KUBE-SVC-XX3NNXKPRC7N6OJH

# 标记来自外部网络(除了 PodCIDR 10.233.64.0/18)的、目标地址为服务 default/nginx 的 cluster IP 10.233.54.175 的 TCP 80 端口的流量
-A KUBE-SVC-XX3NNXKPRC7N6OJH ! -s 10.233.64.0/18 -d 10.233.54.175/32 -p tcp -m comment --comment "default/nginx:tcp-80 cluster IP" -m tcp --dport 80 -j KUBE-MARK-MASQ

# 将匹配的流量转发到 KUBE-SEP-EUB75IW55XDW6EHN 链中,进行 DNAT 地址转换
-A KUBE-SVC-XX3NNXKPRC7N6OJH -m comment --comment "default/nginx:tcp-80 -> 10.233.74.106:80" -j KUBE-SEP-EUB75IW55XDW6EHN

NodePort

把这个 SVC 类型改成 NodePort
nodeport-svc

通过 iptables-save 命令打印这个 SVC 的规则,多了两条规则
nodeport-iptables

1
2
3
4
5
# 用于标记来自外部的、目标地址为服务 default/nginx 的 NodePort 流量
-A KUBE-EXT-XX3NNXKPRC7N6OJH -m comment --comment "masquerade traffic for default/nginx:tcp-80 external destinations" -j KUBE-MARK-MASQ

# 用于匹配目标端口为 32594 的 TCP 流量,并将该流量转发到上面的规则进行 MASQ 标记
-A KUBE-NODEPORTS -p tcp -m comment --comment "default/nginx:tcp-80" -m tcp --dport 32594 -j KUBE-EXT-XX3NNXKPRC7N6OJH

IPVS

通过 kube-proxy 的 cm 可以查看 mode
ipvs-mode

来自 GPT 的回答: IPVS 是 Linux 内核中实现的虚拟服务器技术,它可以将网络流量负载均衡到多台服务器上,从而提高服务性能和可靠性。

ClusterIP

PodIP: 10.233.76.138
ClusterIP: 10.233.27.36

通过 ipvsadm 命令查看这个虚拟服务器
clusterip-ipvs

1
2
3
4
5
6
7
8
9
10
Prot LocalAddress:Port Scheduler Flags
-> RemoteAddress:Port Forward Weight ActiveConn InActConn

# 将目标地址为 nginx.default.svc.cluster.lo:80 的 TCP 流量转发到后端 Pod 10.233.76.138:80
TCP nginx.default.svc.cluster.lo rr
-> 10-233-76-138.nginx.default. Masq 1 0 0

# 将目标地址为 10.233.27.36:80 的 TCP 流量转发到后端 Pod 10.233.76.138:80
TCP 10.233.27.36:80 rr
-> 10.233.76.138:80 Masq 1 0 0

NodePort

通过 ipvsadm 命令查看这个虚拟服务器,多了三条规则
nodeport-ipvs

1
2
3
4
5
6
7
8
9
10
11
# 10.233.74.64 为 nodelocaldns 网卡的 IP,由于 NodePort 是全监听,所以也有对应的规则
TCP 169.254.25.10:30229 rr
-> 10.233.76.138:80 Masq 1 0 0

# 节点IP
TCP 192.168.159.11:30229 rr
-> 10.233.76.138:80 Masq 1 0 0

# 10.233.74.64 为 tun0 网卡的 IP,由于 NodePort 是全监听,所以也有对应的规则
TCP 10.233.74.64:30229 rr
-> 10.233.76.138:80 Masq 1 0

CNI工作流程

CNI 即容器网络接口,通过 CNI 能够使 K8s 支持不同的网络模式。

CNI 常见的实现模式大致分为两种:

  1. overlay: 通过隧道打通网络,不依赖底层网络,如 calico
  2. underlay: 通过底层打通网络,强依赖底层网络,如 macvlan

CNI 如何使用

CNI 的实现通常需要两个部分:

  1. CNI 二进制文件去配置 Pod 的网卡和 IP

  2. DaemonSet 进程去管理 Pod 之间的网络打通

  3. 配置 CNI 配置文件(/etc/cni/net.d/*.conf)

  4. 安装 CNI 二进制文件(/opt/cni/bin/*)

  5. 节点上创建 Pod

  6. kubelet 根据 CNI 配置文件执行 CNI 二进制文件

  7. Pod 网络配置完成

CSI工作流程

CSI 即容器存储接口,借助 CSI 就可以很容易的给工作负载提供存储使用。K8s 的存储插件又分为 in-tree 和 out-of-tree 两种类型,前者是与 K8s 主库共同迭代维护,后者则是独立维护的存储插件。

out-of-tree 类型的插件则是通过 gRPC 与 K8s 的组件进行交互,为了简化 CSI 的开发与部署,K8s 也提供了多个 sidecar 组件:

  1. node-driver-registrar: 监听来自 kubelet 的 gRPC 请求,从 CSI driver 获取驱动程序信息(通过 NodeGetInfo 方法),并使用 kubelet 插件注册机制在该节点上的 kubelet 中对其进行注册
  2. provisioner: 监听来自 kube-apiserver 的 gRPC 请求,监听 PVC 的创建和删除,调用 CSI driver 创建和删除 PV(通过 CreateVolumeDelete Volume)方法
  3. attacher: 监听来自 kube-apiserver 的 gRPC 请求,监听 volumeAttachment 对象并触发 CSI 执行 ControllerPublishVolumeControllerUnpublishVolume 的操作
  4. resizer: 监听来自 kube-apiserver 的 gRPC 请求,监听 PVC 的修改,调用 CSI Controller 执行 ExpandVolume 方法,来调整 Volume 的大小
  5. livenessProbe: 检查 CSI 程序的健康状态,如不健康则会进行重启

CSI 工作流程

CSI 的工作流分为三个阶段:

  1. Provision/Delete
  2. Attach/Detach
  3. Mount/Unmount

这三个阶段会用到 Sidecar 组件,也会用到 K8s 的 PV Controller 和 AD Controller 组件

当然并不是所有的 CSI 都会经历这三个阶段,如 NFS CSI 的工作流就没有涉及 volumeAttachment
nfs-csi

Privision 阶段

在此阶段,Sidecar provisioner 和 PV Controller 都会监听 PVC 资源

  1. 当 PV Controller 观察到新的 PVC 被创建时,就会去判断是否有与之匹配的 in-tree 插件,如果没有则判定为 out-of-tree,并为该 PVC 添加 annotation
    pvc-annotation
  2. provisioner 观察到 PVC 的 annotation 与自己的 CSI 是相匹配的时候,就会去调用 CreateVolume 方法
  3. CreateVolume 调用返回成功时,provisioner 就会创建 PV
  4. PV Controller 监听到该 PV 时,就会将其与 PVC 做绑定

Attach 阶段

在此阶段,会将数据卷附在一个节点上

  1. AD Controller 监听到 Pod 被调度到某个节点后,会调用 in-tree 内部接口创建 volumeAttachment 资源
  2. attacher 监听到 volumeAttachment 就会调用 ControllerPublishVolume 接口
  3. 当接口返回成功时就会将 volumeAttachment 资源的 status.attached 设置为 true
    volume-attachment-status

Mount 阶段

在此阶段,会将数据卷挂载到 Pod 上

  1. kubelet 观察到 volumeAttachment 资源的 status.attached 设置为 true 时,就会调用 in-tree 内部接口进行实际的卷挂载操作

Unmount 阶段

在此阶段,会将数据卷从 Pod 上取消挂载

  1. kubelet 监听到节点上的 Pod 被删除,就会调用 in-tree 内部接口进行实际的卷卸载操作

Detach 阶段

在此阶段,会将对应的 volumeAttachment 资源删除

  1. attacher 会讲被删除 Pod 对应的 volumeAttachment 进行删除
  2. AD Controller 监听到 volumeAttachment 被删除后,会去更新节点的 status.volumesInUse,将对应的卷信息摘除

Delete 阶段

在此阶段,会判断 PV 的回收策略进行不同的操作

  1. provisioner 观察 PV 的 persistentVolumeReclaimPolicy,如果为 Retain 则保留,Delete 则删除

Pod是怎么诞生的

K8s 本身不提供容器创建的实现,容器的创建是通过 CRI 接口调用外部插件实现的,常见的 CRI 有这几种:

  1. docker
  2. containerd
  3. cri-dockerd

Pod 是如何被创建出来的

  1. 客户端通过 kubectl 等将创建 pod 的请求发送给 kube-apiserver
  2. kube-apiserver 将 pod 信息写入 etcd,etcd 将写入的结果返回给 kube-apiserver,然后 kube-apiserver 再返回给客户端
  3. kube-scheduler 通过 kube-apiserver 的 watch 接口,获取到未被调度的 pod 信息,根据调度算法选择集群内的一个节点,然后将节点信息发送给 kube-apiserver
  4. kube-apiserver 将这个 pod 和节点的绑定信息写入到 etcd,etcd 将结果返回给 kube-apiserver
  5. kubelet 通过 kube-apiserver 的 watch 接口,获取到本节点有创建 pod 的信息,然后会调用 CRI 创建容器,并将 pod 的运行状态发送给 kube-apiserver
  6. kube-apiserver 将 pod 状态信息更新到 etcd

pod创建流程

CKA

记录刷题- -

Job:创建一个固定结束次数的并行 Job,共 2 个 pod,运行 45 completion,Job pod 打印“Beijing”,镜像自选。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
apiVersion: batch/v1
kind: Job
metadata:
name: job
spec:
parallelism: 2
completions: 45
restartPolicy: Never
template:
metadata:
name: job
spec:
containers:
- name: job
image: busybox:latest
command:
- 'sh'
- '-c'
- 'echo Beijing'

initContainer:pod 的 container 在启动时会检查 volume 中某个目录是否存在某个文件,若不存在则退出,若存在,则运行。要求修改 pod 的 yaml,通过 initContainer 创建该文件,使pod运行。

1

将名为 ek8s-node-1 的 node 设置为不可用,并重新调度该 node 上所有运行的 pods。

1
2
3
4
# cordon:将node设置为不可用,将阻止新pod调度到该节点之上,但不会影响任何已经在其上的 pod,这是重启节点或者执行其他维护操作之前的一个有用的准备步骤,DaemonSet创建的pod能够容忍节点的不可调度属性。
kubectl cordon ek8s-node-1
# drain:从节点安全的驱逐所有pods。
kubectl drain ek8s-node-1 --delete-local-data --ignore-daemonsets --force

在现有的 namespace app-team1 中创建一个名为 cicd-token 的新 ServiceAccount,创建一个 deployment-clusterrole 且只能够创建 Deployment、DaemonSet、StatefulSet 资源的 Cluster Role,并将新的 deployment-clusterrole 绑定到 cicd-token。

1
2
3
4
5
kubectl create sa cicd-token -n app-team1

kubectl create clusterrole deployment-clusterrole --verb=create --resource=Deployment,StatefulSet,DaemonSet

kubectl create rolebinding cicd-rolebinding --clusterrole=deployment-clusterrolr --serviceaccount=app-team1:cicd-token

k8s master 1.20.0 升级到 1.20.1,并升级 kubelet、kubectl。

1
2
3
4
5
6
7
8
9
10
# 将节点设为不可调度性
kubectl cordon mk8s-master-0
kubectl drain mk8s-master-0 --igonre-daemonsets
# 升级
apt-get update
apt-get -y install kubeadm=1.20.1-00
kubeadm update apply v1.20.1 --etcd-upgrade=false
apt-get -y install kubelet=1.20.1-00 kubectl=1.20.1-00
# 将不可调度性去除
kubectl uncordon mk8s-master-0

etcd 备份/恢复。

1
2
3
4
# 备份
ETCDCTL_API=3 etcdctl --endpoints=https://127.0.0.1:2379 --cacert=<trusted-ca-file> --cert=<cert-file> --key=<key-file> snapshot save <backup-file-location>
# 恢复
ETCDCTL_API=3 etcdctl --endpoints=https://127.0.0.1:2379 --cacert=<trusted-ca-file> --cert=<cert-file> --key=<key-file> snapshot restore <backup-file-location>

在 internal 的命名空间下创建一个名为 allow-port-from-namespace 的 NetworkPolicy,此 NetworkPolicy 允许 internal 中的 pod 访问 echo 命名空间中 9000 的端口。

1
2
3
# 先获取echo租户的labels
kubectl get ns echo --show-labels
echo-key: echo-value
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-port-from-namespace
namespace: internal
spec:
podSelector: {}
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
echo-key: echo-value
ports:
- protocol: TCP
port: 9000

在 ing-internal 下创建一个 ingress 名为 ping,通过 5678 端口暴露 hello svc 下的 /hello。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: ping
namespace: ing-internal
spec:
rules:
- http:
paths:
- path: /hello
backend:
service:
name: hello
port:
number: 5678

将 deployment webserver 扩展至 6 pods。

1
kubectl scale deployment webserver --replicas 6

创建一个名为 nginx-kusc00401 的 pod,镜像为 nginx,调度到 disk=ssd 的节点上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
apiVersion: v1
kind: Pod
metadata:
name: nginx-kusc00401
spec:
containers:
- name: nginx
image: nginx:1.20.1
imagePullPolicy: Always
ports:
- name: http
containerPort: 80
NodeSelector:
disk=ssd

将一个现有的 pod 集成到 k8s 内置日志记录体系结构中(kubectl logs),使用 busybox 来将名为 sidecar 容器添加到名为 11-factor-app 的 pod 中,sidecar 需运行 /bin/sh -c tail -n+1 -f /var/log/11-factor-app.log ,使用安装在 /var/log 的 volume,使日志文件 11-factor-app.log 可用于 sidecar 容器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
apiVersion: v1
kind: Pod
metadata:
name: 11-factor-app
spec:
volumes:
- name: log
emptyDir: {}
containers:
- name: count
image: busybox
command:
- /bin/sh
- -c
- >
i=0;
while true;
do
echo "$i: $(date)" >> /var/log/11-factor-app.log;
i=$((i+1));
sleep 1;
done
volumeMounts:
- name: log
mountPath: /var/log
- name: sidecar
image: busybox
command:
- /bin/sh
- -c
- 'tail -n+1 -f /var/log/11-factor-app.log'
volumeMounts:
- name: log
mountPath: /var/log

通过 pod label name=cpu-utilizer 寻找 cpu 占用最多的 pod,并写入 /opt/tmp/tmp.txt

1
kubectl top pod -A -l name=cpu-utilizer --sort-by=cpu >> /opt/tmp/tmp.txt

Kubernetes

一、概念

1.1 k8s概述

Kubernetes 是一个可移植的、可扩展的开源平台,用于管理容器化的工作负载和服务,可促进声明式配置和自动化。 Kubernetes 拥有一个庞大且快速增长的生态系统。Kubernetes 的服务、支持和工具广泛可用。

k8s图标

容器是打包和运行应用程序的好方式。在生产环境中,你需要管理运行应用程序的容器,并确保不会停机。 例如,如果一个容器发生故障,则需要启动另一个容器。如果系统处理此行为,会不会更容易?

这就是 Kubernetes 来解决这些问题的方法! Kubernetes 为你提供了一个可弹性运行分布式系统的框架。 Kubernetes 会满足你的扩展要求、故障转移、部署模式等。 例如,Kubernetes 可以轻松管理系统的 Canary 部署。

Kubernetes 简称 k8s,主要功能有:

  • 服务发现和负载均衡
  • 存储编排
  • 自动部署和回滚
  • 自动完成装箱计算
  • 自我修复
  • 密钥与配置管理

1.2 k8s组件

一个 Kubernetes 集群由一组被称作节点的机器组成。这些节点上运行 Kubernetes 所管理的容器化应用。集群具有至少一个工作节点。

工作节点托管作为应用负载的组件的 Pod 。控制平面管理集群中的工作节点和 Pod 。 为集群提供故障转移和高可用性,这些控制平面一般跨多主机运行,集群跨多个节点运行。

k8s架构

1.2.1 控制平面组件

kube-apiserver:

API 服务器是 Kubernetes 控制面的组件,该组件公开了 Kubernetes API。API 服务器是 Kubernetes 控制面的前端。

etcd:

etcd 是兼具一致性和高可用性的键值数据库,可以作为保存 Kubernetes 所有集群数据的后台数据库。

kube-scheduler:

控制平面组件,负责监视新创建的、未指定运行节点(node)的 Pods,选择节点让 Pod 在上面运行。

调度决策考虑的因素包括单个 Pod 和 Pod 集合的资源需求、硬件/软件/策略约束、亲和性和反亲和性规范、数据位置、工作负载间的干扰和最后时限。

kube-controller-manager:

在主节点上运行控制器的组件。

控制器包括:

  • 节点控制器(Node Controller): 负责在节点出现故障时进行通知和响应
  • 任务控制器(Job controller): 监测代表一次性任务的 Job 对象,然后创建 Pods 来运行这些任务直至完成
  • 端点控制器(Endpoints Controller): 填充端点(Endpoints)对象(即加入 Service 与 Pod)
  • 服务帐户和令牌控制器(Service Account & Token Controllers): 为新的命名空间创建默认帐户和 API 访问令牌

cloud-controller-manager:

云控制器管理器是指嵌入特定云的控制逻辑的控制平面组件。云控制器管理器允许您链接聚合到云提供商的应用编程接口中,并分离出相互作用的组件与您的集群交互的组件。

下面的控制器都包含对云平台驱动的依赖:

  • 节点控制器(Node Controller): 用于在节点终止响应后检查云提供商以确定节点是否已被删除
  • 路由控制器(Route Controller): 用于在底层云基础架构中设置路由
  • 服务控制器(Service Controller): 用于创建、更新和删除云提供商负载均衡器

1.2.2 Node组件

节点组件在每个节点上运行,维护运行的 Pod 并提供 Kubernetes 运行环境。

kubelet:

一个在集群中每个节点(node)上运行的代理。它保证容器(containers)都运行在 Pod 中。

kubelet 接收一组通过各类机制提供给它的 PodSpecs,确保这些 PodSpecs 中描述的容器处于运行状态且健康。 kubelet 不会管理不是由 Kubernetes 创建的容器。

容器运行时(Container Runtime):

容器运行环境是负责运行容器的软件,例如 Docker 。

1.2.3 插件(Addons)

插件使用 Kubernetes 资源(DaemonSet、Deployment等)实现集群功能。 因为这些插件提供集群级别的功能,插件中命名空间域的资源属于 kube-system 命名空间。

DNS:

尽管其他插件都并非严格意义上的必需组件,但几乎所有 Kubernetes 集群都应该 有集群 DNS, 因为很多示例都需要 DNS 服务。

Web 界面(仪表盘):

Dashboard 是Kubernetes 集群的通用的、基于 Web 的用户界面。 它使用户可以管理集群中运行的应用程序以及集群本身并进行故障排除。

容器资源监控:

容器资源监控将关于容器的一些常见的时间序列度量值保存到一个集中的数据库中,并提供用于浏览这些数据的界面。

集群层面日志:

集群层面日志机制负责将容器的日志数据 保存到一个集中的日志存储中,该存储能够提供搜索和浏览接口。

1.3 Pod

1.3.1 Pod概念

Pod 是 k8s 中的最小单元,一个 Pod 包含一个或多个容器,一个 Pod 不会跨越多个节点(Node)。

而每个 Pod 里都会有一个根容器 pause,Pod 中的其他容器都共享 pause 根容器的网络栈和volume挂载卷,因此容器之间的通信和数据交换会更为高效。

1.3.2 RC、RS、Deployment

k8s 管理 Pod 主要用到以下三个组件:

  • Replication Controller(RC):用来确保容器应用的副本数始终保持在用户定义的副本数,如果有容器异常退出,会自动创建新的 Pod 来代替,如果有异常多出的容器也会自动回收。
  • ReplicaSet(RS):相比 RC 多了支持 selector,推荐使用 RS。
  • Deployment:用来管理 RS。

来看看 Deployment 和 RS 是如何实现更新的:

  1. Deployment 创建新的 RS

RS和Deployment实现更新一

  1. RS1删除一个容器,接着 RS2 新建一个新版本的 Pod

RS和Deployment实现更新二

  1. 全部更新完之后,RS1 并不会删除,而是保留着处于停用状态,如果新版本出了问题需要回滚,就可以反过来操作实现回滚

RS和Deployment实现更新三

1.3.3 Horizontal Pod Autoscaler

Horizontal Pod Autoscaler(HPA)在k8s集群中用于 Pod 水平自动弹性伸缩,它是基于 CPU 和内存利用率对 Deployment 和 RS 中的 Pod 数量进行自动扩缩容(除了 CPU 和内存利用率之外,也可以基于其他应程序提供的度量指标 custom metrics 进行自动扩缩容)。

假如 HPA 检测到当前 Deployment 和 RS 所管理的 Pod 的 CPU 或内存使用率超过了设定之后,就会创建新的 Pod 来实现降压,新建 Pod 的数量限制由 Max 和 Min 设定。

HPA

1.3.4 StatefulSet

RS 和 Deployment 都是面向无状态的服务,它们所管理的 Pod 的 IP、名字,启停顺序等都是随机的,而 StatefulSet 是有状态的集合,管理所有有状态的服务,比如 MySQL、MongoDB 集群等。

StatefulSet 的特点有:

Pod 的一致性:包含次序(启停顺序,例如 mysql -> php-fpm -> nginx 的启动顺序)、网络一致性(与 Pod 相关,与被调度的 Node 节点无关)。

稳定的存储:即 Pod 重新调度之后还是访问到相同的持久化数据,基于 PVC(PV 是集群中由管理员提供或使用存储类动态提供的一块存储。它是集群中的资源,就像节点是集群资源一样。而 PVC 是用户对存储的请求。它类似于 Pod,Pod 消耗 Node 资源,而 PVC 消耗 PV 资源。) 来实现。

稳定的次序:对于N个副本的 StatefulSet,每个 Pod 都在 [0,N) 的范围内分配一个数字序号,且是唯一的。

稳定的网络:Pod 的 hostname 模式为:( StatefulSet 名称 ) - ( 序号 )。

1.3.5 DaemonSet

DaemonSet 确保全部或者一部分 Node 上运行一个 Pod 的副本,当有 Node 加入集群时,也会为他们新增一个 Pod,当这些 Node 退出集群时,这些 Pod 也会被回收。

DaemonSet 的一些典型用法:

  • 运行集群存储 daemon,例如在每个 Node 上运行 glusterd、ceph等。
  • 运行日志收集daemon,例如fluentd、logstash等。
  • 运行监控daemon,例如 Prometheus 的 Node exporter、zabbix等。

1.3.6 Job和Cron Job

Job 负责批处理任务,即仅执行一次的任务,它保证批处理任务的一个或多个 Pod 成功结束。

这里可能有人会想,那在 linux 上直接执行脚本不就行了吗?其实这就会有个问题,如果脚本执行失败那么久退出且不会再执行了,需要执行就必须手动,但 Job 设置的任务只有在正常执行结束后才会结束,否则一直执行到成功为止。Job 也可以设置成功的次数要达到几次才允许退出。

Cron Job 是基于时间管理控制 Job,即在给定的时间只运行一次、周期性的在指定时间运行。

1.3.7 Service

Pod 的生命是有限的,如果 Pod 重启 IP 也可能会发生变化。如果我们将 Pod 的 IP 写死,Pod 如果挂了或重启,其它的服务也会不可用。我们可以把我们的服务(各种 Pod)注册到服务发现中心去,让服务发现中心去动态更新其它服务的配置就可以了,k8s 就给我们提供了这么一个服务,即 Service。

我们这样就可以不用去管后端的 Pod 如何变化,只需要指定 Service 的地址就可以了,因为我们在中间添加了一层服务发现的中间件,Pod 销毁或者重启后,把这个 Pod 的地址注册到这个服务发现中心去。

service

1.4 k8s的网络通讯方式

k8s 的网络模型假定了所有的 Pod 都在一个可以直接连通的扁平化网络空间中,在这 GCE(Google Compute Engine)里面是现成的网络模型。

Flannel 是 CoreOS 团队针对 Kubernetes 设计一个网络规划服务,简单来说,它的功能是让集群中的不同节点主机创建 Docker 容器都具有全集群唯一的虚拟 IP 地址。而且它还能在这些 IP 地址之间建立一个覆盖网络(Overlay Network),通过覆盖网络将数据包原封不动的传递到目标容器内。

通过一个架构图来看看不同情况下的通讯是怎么样的:

网络通讯方式架构图

通讯情况主要分为以下几种:

  1. 同一 Pod 不同容器之间的通信:采用 pause 网络栈。
  2. 同一 Node 不同 Pod 之间的通信:通过 docker0 网桥进行通信。

同一Node内不同Pod之间的通讯

  1. 不同 Node 的 Pod 之间的通信:不同 Node 的 Pod 之间的通信是 k8s 网络通信的难点,是通过 Flannel 网络通讯方式来实现的,通讯的过程分为以下几个步骤:
    • 数据包从 Node1 的 Pod 到达 docker0 网桥
    • Flanneld 会开启一个 Flannel0 的网桥,用来抓取到达 docker0 的数据,可以理解为一个钩子函数
    • Flanneld 会有很多路由表信息,是存储在 etcd 中由 Flanneld 自动获取的,通过路由表知道了转发信息后就通过物理网卡进行转发
    • 发送到目标主机的物理网卡后,就会再通过 Flanneld -> Flannel0网桥 -> docker0网桥,最终到达目的 Pod

不同Node的Pod之间的通信

  1. Pod 与 service 之间的通信:采用各节点的 iptables 规则来实现。
  2. Pod 到外网:通过 Flanneld 到达物理网卡,经过路由选择后,iptables 执行 Masquerade,把Pod 的虚拟 IP 改为 物理网卡的 IP,在向外网服务器发出请求。
  3. 外网到 Pod:通过 service 进行访问,一般使用 NodePort。

二、k8s部署

本次 k8s 部署为以下环境

k8s部署条件

2.1 基本环境配置(所有节点)

  1. 在各主机设置主机名以及host文件解析
1
2
3
4
5
6
7
hostnamectl set-hostname k8s-master01
hostnamectl set-hostname k8s-node01
hostnamectl set-hostname k8s-node02
vim /etc/hosts
192.168.88.10 k8s-master01
192.168.88.20 k8s-node01
192.168.88.21 k8s-node02
  1. 安装依赖
1
yum -y install conntrack ntpdate ntp ipvsadm ipset jp iptables curl stsstat libseccomp wget vim net-tools git
  1. 设置防火墙为iptables并设置空规则
1
2
3
4
5
systemctl stop firewalld && systemctl disable firewalld
yum -y install iptables-services
systemctl start iptables
systemctl enable iptables
iptables -F && service iptables save
  1. 关闭虚拟内存和selinux
1
2
swapoff -a && sed -i '/ swap / s/^\(.*\)$/#/g' /etc/fstab
setenforce 0 && sed -i 's/^SELINUX=.*/SELINUX=disabled/' /etc/selinux/config
  1. 调整内核参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
cat > kubernetes.conf <<EOF
net.bridge.bridge-nf-call-iptables=1
net.bridge.bridge-nf-call-ipv6tables=1
net.ipv4.ip_forward=1
net.ipv4.tcp_tw_recycle=0
vm.swappiness=0 # 禁用swap,只有系统OOM时才允许使用
vm.overcommit_memory=1 # 不检查物理内存是否够用
vm.panic_on_oom=0 # 开启OOM
fs.inotify.max_user_instances=8192
fs.inotify.max_user_watches=1048576
fs.file-max=52706963
fs.nr_open=52706963
net.ipv6.conf.all.disable_ipv6=1
net.netfilter.nf_conntrack_max=2310720
EOF
cp kubernetes.conf /etc/sysctl.d/kubernetes.conf
sysctl -p /etc/sysctl.d/kubernetes.conf
  1. 调整系统时区
1
2
3
4
5
6
7
# 设为中国/上海
timedatectl set-timezone Asia/Shanghai
# 将当前UTC时间写入硬件时钟
timedatectl set-local-rtc 0
# 重启依赖于时间的服务
systemctl restart rsyslog
systemctl restart crond
  1. 关闭不需要的服务
1
2
# 关闭邮件服务
systemctl stop postfix && systemctl disable postfix
  1. 设置rsyslog和systemd journald
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 持久化保存日志目录
mkdir /var/log/journal
mkdir /etc/systemd/journald.conf.d
cat > /etc/systemd/journald.conf.d/99-prophet.conf <<EOF
[Journal]
#持久化保存到磁盘
Storage=persistent

#压缩历史日志
Compress=yes

SyncIntervalSec=5m
RateLimitInterval=30s
RateLimitBurst=1000

#最大占用空间 10G
SystemMaxUse=10G

#单日志文件最大 200M
SystemMaxFileSize=200M

#日志保存时间 2 周
MaxRetentionSec=2week

#不将日志转发到 syslog
ForwardToSyslog=no
EOF

systemctl restart systemd-journald
  1. 升级内核
1
2
3
4
5
6
7
8
9
10
# 导入公钥
rpm --import https://www.elrepo.org/RPM-GPG-KEY-elrepo.org
# 安装elrepo源
yum -y install https://www.elrepo.org/elrepo-release-7.el7.elrepo.noarch.rpm
# 升级内核
yum --enablerepo=elrepo-kernel install -y kernel-lt
# 设置开机从新内核启动
grub2-set-default "CentOS Linux (5.4.116-1.e17.elrepo.x86_64) 7 (Core)"
# 查看内核启动项
grub2-editenv list

2.2 kubeadm部署(所有节点)

  1. kube-proxy开启ipvs前置条件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
modprobe br_netfilter

vim /etc/sysconfig/modules/ipvs.modules
#!/bin/bash
ipvs_modules="ip_vs ip_vs_lc ip_vs_wlc ip_vs_rr ip_vs_wrr ip_vs_lblc ip_vs_lblcr ip_vs_dh ip_vs_sh ip_vs_fo ip_vs_nq ip_vs_sed ip_vs_ftp nf_conntrack"

for kernel_module in ${ipvs_modules};
do
/sbin/modinfo -F filename ${kernel_module} > /dev/null 2>&1
if [ $? -eq 0 ]; then
/sbin/modprobe ${kernel_module}
fi
done

chmod 755 /etc/sysconfig/modules/ipvs.modules && bash /etc/sysconfig/modules/ipvs.modules && lsmod | grep -e ip_vs -e nf_conntrack
  1. 安装docker
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
yum -y install yum-utils device-mapper-persistent-data lvm2

yum-config-manager --add-repo https://mirrors.tuna.tsinghua.edu.cn/docker-ce/linux/centos/docker-ce.repo

yum makecache fast
yum -y install docker-ce docker-ce-cli

mkdir /etc/docker
cat > /etc/docker/daemon.json <<EOF
{
"registry-mirrors": ["http://f1361db2.m.daocloud.io"],
"exec-opts":["native.cgroupdriver=systemd"],
"log-driver":"json-file",
"log-opts":{
"max-size":"100m"
}
}
EOF
mkdir -p /etc/systemd/system/docker.service.d

systemctl daemon-reload && systemctl start docker && systemctl enable docker

2.3 安装kubeadm(所有节点)

  1. 安装 kubeadm kubectl kubelet
1
2
3
4
5
6
7
8
9
10
11
12
cat <<EOF > /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=kubernetes
baseurl=https://mirrors.aliyun.com/kubernetes/yum/repos/kubernetes-el7-x86_64
enabled=1
gpgcheck=1
repo_gpgcheck=1
gpgkey=https://mirrors.aliyun.com/kubernetes/yum/doc/yum-key.gpg https://mirrors.aliyun.com/kubernetes/yum/doc/rpm-package-key.gpg
EOF

yum -y install kubeadm kubectl kubelet
systemctl enable kubelet
  1. 通过kubeadm查看目前各镜像版本
1
2
3
4
5
6
7
8
kubeadm config images list
k8s.gcr.io/kube-apiserver:v1.21.0
k8s.gcr.io/kube-controller-manager:v1.21.0
k8s.gcr.io/kube-scheduler:v1.21.0
k8s.gcr.io/kube-proxy:v1.21.0
k8s.gcr.io/pause:3.4.1
k8s.gcr.io/etcd:3.4.13-0
k8s.gcr.io/coredns/coredns:v1.8.0
  1. 编写脚本,通过阿里镜像安装
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
vim k8s_images.sh

#!/bin/bash
apiserver_var=v1.21.0
controller_manager_var=v1.21.0
scheduler_var=v1.21.0
proxy_var=v1.21.0
pause_var=3.4.1
etcd_var=3.4.13-0
coredns_var=v1.8.0
image_aliyun=(kube-apiserver:$apiserver_var kube-controller-manager:$controller_manager_var kube-scheduler:$scheduler_var kube-proxy:$proxy_var pause:$pause_var etcd:$etcd_var coredns/coredns:$coredns_var)

for image in ${image_aliyun[@]}
do
docker pull registry.cn-hangzhou.aliyuncs.com/google_containers/$image
docker tag registry.cn-hangzhou.aliyuncs.com/google_containers/$image k8s.gcr.io/${image}
docker rmi registry.cn-hangzhou.aliyuncs.com/google_containers/$image
done

./k8s_images.sh

# 问题
# 由于杭州阿里源里目前没有coredns:v1.8.0版本,所以在北京阿里源下载
docker pull registry.cn-beijing.aliyuncs.com/dotbalo/coredns:1.8.0
docker tag registry.cn-beijing.aliyuncs.com/dotbalo/coredns:1.8.0 k8s.gcr.io/coredns/coredns:v1.8.0
docker rmi registry.cn-beijing.aliyuncs.com/dotbalo/coredns:1.8.0

2.4 初始化Master节点

1
kubeadm config print init-defaults > kubeadm.config.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
vim kubeadm.config.yml
apiVersion: kubeadm.k8s.io/v1beta2
bootstrapTokens:
- groups:
- system:bootstrappers:kubeadm:default-node-token
token: abcdef.0123456789abcdef
ttl: 24h0m0s
usages:
- signing
- authentication
kind: InitConfiguration
localAPIEndpoint:
# 改成master节点IP地址
advertiseAddress: 192.168.88.10
bindPort: 6443
nodeRegistration:
criSocket: /var/run/dockershim.sock
name: node
taints: null
---
apiServer:
timeoutForControlPlane: 4m0s
apiVersion: kubeadm.k8s.io/v1beta2
certificatesDir: /etc/kubernetes/pki
clusterName: kubernetes
controllerManager: {}
dns:
type: CoreDNS
etcd:
local:
dataDir: /var/lib/etcd
imageRepository: k8s.gcr.io
kind: ClusterConfiguration
kubernetesVersion: 1.21.0
networking:
dnsDomain: cluster.local
podSubnet: 10.244.0.0/16
serviceSubnet: 10.96.0.0/12
scheduler: {}
# 添加这一段
---
apiVersion: kubeproxy.config.k8s.io/v1alpha1
kind: KubeProxyConfiguration
kubeproxy:
config:
mode: ipvs
1
2
3
4
5
6
kubeadm init --config=kubeadm.config.yml --upload-certs | tee kubeadm-init.log
...
# 初始化后操作
mkdir -p $HOME/.kube
cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
chown $(id -u):$(id -g) $HOME/.kube/config

2.5 部署网络

1
2
3
mkdir -p k8s/plugin/flannel && cd k8s/plugin/flannel
wget https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml
kubectl create -f kube-flannel.yml

kube-flannel.yml文件内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
---
apiVersion: policy/v1beta1
kind: PodSecurityPolicy
metadata:
name: psp.flannel.unprivileged
annotations:
seccomp.security.alpha.kubernetes.io/allowedProfileNames: docker/default
seccomp.security.alpha.kubernetes.io/defaultProfileName: docker/default
apparmor.security.beta.kubernetes.io/allowedProfileNames: runtime/default
apparmor.security.beta.kubernetes.io/defaultProfileName: runtime/default
spec:
privileged: false
volumes:
- configMap
- secret
- emptyDir
- hostPath
allowedHostPaths:
- pathPrefix: "/etc/cni/net.d"
- pathPrefix: "/etc/kube-flannel"
- pathPrefix: "/run/flannel"
readOnlyRootFilesystem: false
# Users and groups
runAsUser:
rule: RunAsAny
supplementalGroups:
rule: RunAsAny
fsGroup:
rule: RunAsAny
# Privilege Escalation
allowPrivilegeEscalation: false
defaultAllowPrivilegeEscalation: false
# Capabilities
allowedCapabilities: ['NET_ADMIN', 'NET_RAW']
defaultAddCapabilities: []
requiredDropCapabilities: []
# Host namespaces
hostPID: false
hostIPC: false
hostNetwork: true
hostPorts:
- min: 0
max: 65535
# SELinux
seLinux:
# SELinux is unused in CaaSP
rule: 'RunAsAny'
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: flannel
rules:
- apiGroups: ['extensions']
resources: ['podsecuritypolicies']
verbs: ['use']
resourceNames: ['psp.flannel.unprivileged']
- apiGroups:
- ""
resources:
- pods
verbs:
- get
- apiGroups:
- ""
resources:
- nodes
verbs:
- list
- watch
- apiGroups:
- ""
resources:
- nodes/status
verbs:
- patch
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: flannel
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: flannel
subjects:
- kind: ServiceAccount
name: flannel
namespace: kube-system
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: flannel
namespace: kube-system
---
kind: ConfigMap
apiVersion: v1
metadata:
name: kube-flannel-cfg
namespace: kube-system
labels:
tier: node
app: flannel
data:
cni-conf.json: |
{
"name": "cbr0",
"cniVersion": "0.3.1",
"plugins": [
{
"type": "flannel",
"delegate": {
"hairpinMode": true,
"isDefaultGateway": true
}
},
{
"type": "portmap",
"capabilities": {
"portMappings": true
}
}
]
}
net-conf.json: |
{
"Network": "10.244.0.0/16",
"Backend": {
"Type": "vxlan"
}
}
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: kube-flannel-ds
namespace: kube-system
labels:
tier: node
app: flannel
spec:
selector:
matchLabels:
app: flannel
template:
metadata:
labels:
tier: node
app: flannel
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/os
operator: In
values:
- linux
hostNetwork: true
priorityClassName: system-node-critical
tolerations:
- operator: Exists
effect: NoSchedule
serviceAccountName: flannel
initContainers:
- name: install-cni
image: quay.io/coreos/flannel:v0.14.0-rc1
command:
- cp
args:
- -f
- /etc/kube-flannel/cni-conf.json
- /etc/cni/net.d/10-flannel.conflist
volumeMounts:
- name: cni
mountPath: /etc/cni/net.d
- name: flannel-cfg
mountPath: /etc/kube-flannel/
containers:
- name: kube-flannel
image: quay.io/coreos/flannel:v0.14.0-rc1
command:
- /opt/bin/flanneld
args:
- --ip-masq
- --kube-subnet-mgr
resources:
requests:
cpu: "100m"
memory: "50Mi"
limits:
cpu: "100m"
memory: "50Mi"
securityContext:
privileged: false
capabilities:
add: ["NET_ADMIN", "NET_RAW"]
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
volumeMounts:
- name: run
mountPath: /run/flannel
- name: flannel-cfg
mountPath: /etc/kube-flannel/
volumes:
- name: run
hostPath:
path: /run/flannel
- name: cni
hostPath:
path: /etc/cni/net.d
- name: flannel-cfg
configMap:
name: kube-flannel-cfg

部署完后可以看到 flannel 网卡

flannel网卡

通过命令查看各模块运行情况

1
kubectl get pod -n kube-system

k8s各模块运行情况

2.6 添加Node节点

  1. 查看kubeadm-init.log
1
2
3
...
kubeadm join 192.168.88.10:6443 --token abcdef.0123456789abcdef \
--discovery-token-ca-cert-hash sha256:37c9645a7a2ee75301f132537240157ecd4f464494022cc442f77489c1978989
  1. 在node主机上执行
1
2
kubeadm join 192.168.88.10:6443 --token abcdef.0123456789abcdef \
--discovery-token-ca-cert-hash sha256:37c9645a7a2ee75301f132537240157ecd4f464494022cc442f77489c1978989
  1. 在master主机查看是否加入成功
1
2
3
4
kubectl get node
...
kubectl get pod -n kube-system -o wide
...

三、k8s资源清单

在 k8s 中,一般使用 yaml 格式的文件来创建符合我们预期期望的 pod,这样的 yaml 文件我们一般称为资源清单。

3.1 资源类型

名称空间级别

工作负载型资源:Pod、RS、Deployment、StatefulSet、DaemonSet、Job、CronJob

服务发现及负载均衡型资源:Service

配置与存储型资源:Volume、CSI

特殊类型的存储卷:ConfigMap(当配置中心来使用的资源类型)、Secret(保存敏感数据)、DownwarAPI(把外部环境中的信息输出给容器)

集群级资源:Namespace、Node、Role、ClusterRole、RoleBinding、ClusterRoleBinding

元数据型资源:HPA、PodTemplate、LimitRange

3.2 常用字段

必要属性

参数名 字段类型 说明
apiVersion String K8S API 的版本,目前基本是v1,可以用 kubectl api-versions 命令查询
kind String 这里指的是 yaml 文件定义的资源类型和角色,比如: Pod
metadata Object 元数据对象,固定值写 metadata
metadata.name String 元数据对象的名字,这里由我们编写,比如命名Pod的名字
metadata.namespace String 元数据对象的命名空间,由我们自身定义
spec Object 详细定义对象,固定值写Spec
spec.containers[] list 这里是Spec对象的容器列表定义,是个列表
spec.containers[].name String 这里定义容器的名字
spec.containers[].image String 这里定义要用到的镜像名称

spec 主要对象

参数名 字段类型 说明
spec.containers[].name String 定义容器的名字
spec.containers[].image String 定义要用到的镜像的名称
spec.containers[].imagePullPolicy String 定义镜像拉取策略,有 Always,Never,IfNotPresent 三个值课选 (1)Always:意思是每次尝试重新拉取镜像 (2)Never:表示仅使用本地镜像 (3)IfNotPresent:如果本地有镜像就是用本地镜像,没有就拉取在线镜像。上面三个值都没设置的话,默认是 Always
spec.containers[].command[] List 指定容器启动命令,因为是数组可以指定多个,不指定则使用镜像打包时使用的启动命令
spec.containers[].args[] List 指定容器启动命令参数,因为是数组可以指定多个
spec.containers[].workingDir String 指定容器的工作目录
spec.containers[].volumeMounts[] List 指定容器内部的存储卷配置
spec.containers[].volumeMounts[].name String 指定可以被容器挂载的存储卷的名称
spec.containers[].volumeMounts[].mountPath String 指定可以被容器挂载的容器卷的路径
spec.containers[].volumeMounts[].readOnly String 设置存储卷路径的读写模式,true 或者 false,默认为读写模式
spec.containers[].ports[] List 指定容器需要用到的端口列表
spec.containers[].ports[].name String 指定端口名称
spec.containers[].ports[].containerPort String 指定容器需要监听的端口号
spec.containers[].ports.hostPort String 指定容器所在主机需要监听的端口号,默认跟上面 containerPort 相同,注意设置了 hostPort 同一台主机无法启动该容器的相同副本(因为主机的端口号不能相同,这样会冲突)
spec.containers[].ports[].protocol String 指定端口协议,支持TCP和UDP,默认值为TCP
spec.containers[].env[] List 指定容器运行千需设置的环境变量列表
spec.containers[].env[].name String 指定环境变量名称
spec.containers[].env[].value String 指定环境变量值
spec.containers[].resources Object 指定资源限制和资源请求的值(这里开始就是设置容器的资源上限)
spec.containers[].resources.limits Object 指定设置容器运行时资源的运行上限
spec.containers[].resources.limits.cpu String 指定CPU的限制,单位为 core 数,将用于 docker run –cpu-shares 参数
spec.containers[].resources.limits.memory String 指定 MEM 内存的限制,单位为 MIB,GIB
spec.containers[].resources.requests Object 指定容器启动和调度室的限制设置
spec.containers[].resources.requests.cpu String CPU请求,单位为 core 数,容器启动时初始化可用数量
spec.containers[].resources.requests.memory String 内存请求,单位为 MIB,GIB 容器启动的初始化可用数量

额外的参数项

参数名 字段类型 说明
spec.restartPolicy String 定义Pod重启策略,可以选择值为 Always、OnFailure、Never,默认值为 Always。1. Always:Pod一旦终止运行,则无论容器是如何终止的,kubelet 服务都将重启它。2. OnFailure:只有 Pod 以非零退出码终止时,kubelet 才会重启该容器。如果容器正常结束(退出码为0),则 kubelet 将不会重启它。3. Never:Pod 终止后,kubelet 将退出码报告给 Master,不会重启该 Pod。
spec.nodeSelector Object 定义 Node 的 Label 过滤标签,以 key:value 格式指定
spec.imagePullSecrets Object 定义pull 镜像是使用 secret 名称,以 name:secretkey 格式指定
spec.hostNetwork Boolean 定义是否使用主机网络模式,默认值为 false。设置 true 表示使用宿主机网络,不使用 docker 网桥,同时设置了 true 将无法在同一台宿主机上启动第二个副本。

辅助命令

1
2
# 查看资源对象用法
kubectl explain <资源对象>

例子:

  1. 创建 yaml 文件
1
2
3
4
5
6
7
8
9
10
11
12
vim nginx.yaml
# 此处只有必要字段
apiVersion: v1
kind: Pod
metadata:
name: nginx-pod
labels:
app: nginx
spec:
containers:
- name: nginx
image: daocloud.io/library/nginx:latest
  1. 创建pod
1
kubectl apply -f nginx.yaml
  1. 查看状态
1
2
3
4
5
6
7
8
9
# 查看是否生成pod
kubectl get pod
...
# 查看pod详细信息
kubectl describe pod nginx-pod
...
# 测试能否访问
curl 10.244.1.2 # 此处为flannel分配的地址
...

3.3 容器生命周期

每一个 Pod 被成功创立之前,都会进行初始化,会运行零个或若干个 init 容器,init 容器运行完就释放,接着才会运行 main 主容器,当然在 init 容器运行之前会先运行 pause 容器,以保证存储和网络的可用。

init 容器与普通的容器非常相似,除了以下两点:

  • init 容器总是运行到完成。
  • 每个 init 容器都要在下一个容器启动之前完成。

如果 Pod 的 Init 容器失败,kubelet 会不断地重启该 Init 容器直到该容器成功为止。 然而,如果 Pod 对应的 restartPolicy 值为 “Never”,Kubernetes 不会重新启动 Pod。

如果为一个 Pod 指定了多个 Init 容器,这些容器会按顺序逐个运行。 每个 Init 容器必须运行成功,下一个才能够运行。当所有的 Init 容器运行完成时, Kubernetes 才会为 Pod 初始化应用容器并像平常一样运行。

init 容器的使用

因为 Init 容器具有与应用容器分离的单独镜像,其启动相关代码具有如下优势:

  • Init 容器可以包含一些安装过程中应用容器中不存在的实用工具或个性化代码。 例如,没有必要仅为了在安装过程中使用类似 sedawkpythondig 这样的工具而去 FROM 一个镜像来生成一个新的镜像。
  • Init 容器可以安全地运行这些工具,避免这些工具导致应用镜像的安全性降低。
  • 应用镜像的创建者和部署者可以各自独立工作,而没有必要联合构建一个单独的应用镜像。
  • Init 容器能以不同于 Pod 内应用容器的文件系统视图运行。因此,Init 容器可以访问应用容器不能访问的 Secret 的权限。
  • 由于 Init 容器必须在应用容器启动之前运行完成,因此 Init 容器提供了一种机制来阻塞或延迟应用容器的启动,直到满足了一组先决条件。 一旦前置条件满足,Pod 内的所有的应用容器会并行启动。

init 容器使用例子

  1. 编写 yaml 文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
vim busybox.yaml
apiVersion: v1
kind: Pod
# 定义了一个 Pod 叫 busybox-pod,busybox 封装了 linux 的大量命令
metadata:
name: busybox-pod
labels:
app: busybox
# 定义该 Pod 下的资源,也就是容器
spec:
# 定义了一个 busybox 容器
containers:
- name: busybox
image: busybox:latest
command: ['sh','-c','echo The busybox app is running! && sleep 3600']
# 定义 init 容器,init 容器全部执行完前不会执行 busybox 容器
initContainers:
# 第一个 init 容器,如果检查 service 中没有注册 myservice,则休眠2秒继续执行,直到成功
- name: init-myservice
image: busybox
command: ['sh','-c','until nslookup myservice; do echo waiting for myservice; sleep 2; done']
# 第一个 init 容器运行完就运行该 init 容器,检查 service 中有没有注册 mydb
- name: init-mydb
image: busybox
command: ['sh','-c','until nslookup mydb; do echo waiting for mydb; sleep 2; done']
  1. 运行
1
kubectl create -f busybox.yaml
  1. 可以看到一直处于 Init:0/2 状态,因为第一个 init 容器一直没完成

init容器测试一

init容器测试二

  1. 编写 yaml 文件添加 service
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
vim service.yaml
apiVersion: v1
kind: Service
metadata:
name: myservice
spec:
ports:
- protocol: TCP
port: 80
targetPort: 9376
---
apiVersion: v1
kind: Service
metadata:
name: mydb
spec:
ports:
- protocol: TCP
port: 80
targetPort: 9377
  1. 运行
1
kubectl create -f service.yaml
  1. 可以看到两个 init 容器都执行完,main 主容器也就运行了

init容器测试三

3.4 探针

探针(probe)是由 kubelet 对容器执行的定期诊断。 要执行诊断,kubelet 调用由容器实现的 Handler (处理程序)。有三种类型的处理程序:

  • ExecAction: 在容器内执行指定命令。如果命令退出时返回码为 0 则认为诊断成功。
  • TCPSocketAction: 对容器的 IP 地址上的指定端口执行 TCP 检查。如果端口打开,则诊断被认为是成功的。
  • HTTPGetAction: 对容器的 IP 地址上指定端口和路径执行 HTTP Get 请求。如果响应的状态码大于等于 200 且小于 400,则诊断被认为是成功的。

每次探测都将获得以下三种结果之一:

  • Success(成功):容器通过了诊断。
  • Failure(失败):容器未通过诊断。
  • Unknown(未知):诊断失败,因此不会采取任何行动。

探针可以分为以下三种:

  • livenessProbe:指示容器是否正在运行。如果存活态探测失败,则 kubelet 会杀死容器, 并且容器将根据其重启策略决定未来。如果容器不提供存活探针, 则默认状态为 Success
  • readinessProbe:指示容器是否准备好为请求提供服务。如果就绪态探测失败, 端点控制器将从与 Pod 匹配的所有服务的端点列表中删除该 Pod 的 IP 地址。 初始延迟之前的就绪态的状态值默认为 Failure。 如果容器不提供就绪态探针,则默认状态为 Success
  • startupProbe: 指示容器中的应用是否已经启动。如果提供了启动探针,则所有其他探针都会被禁用,直到此探针成功为止。如果启动探测失败,kubelet 将杀死容器,而容器依其重启策略进行重启。 如果容器没有提供启动探测,则默认状态为 Success

3.4.1 readinessProbe就绪检测

  1. 编写 yaml 文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
vim readinessProbe.yaml
apiVersion: v1
kind: Pod
metadata:
name: readiness-httpget-pod
namespace: default
spec:
containers:
- name: readiness-httpget-container
image: daocloud.io/library/nginx:latest
# 调用readinessProbe探针
readinessProbe:
httpGet:
# 检查80端口
port: 80
# 检查该目录下的网页
path: /test.html
# 容器启动1秒后才进行检测
initialDelaySeconds: 1
# 每3秒检测一次
periodSeconds: 3
  1. 生成 Pod
1
kubectl create -f readinessProbe.yaml
  1. 可以看到虽然 Pod 运行了,但 ready 状态是不对的

readinessProbe探针测试一

  1. 进入容器创建文件,问题即可解决
1
2
kubectl exec -it readiness-httpget-pod -- /bin/sh
echo 'this is test' > /usr/share/nginx/html/test.html

3.4.2 livenessProbe存活检测

livenessProbe-exec

  1. 编写 yaml 文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
vim livenessProbe_exec.yaml
apiVersion: v1
kind: Pod
metadata:
name: liveness-exec-pod
namespace: default
spec:
containers:
- name: liveness-exec-container
image: busybox
command: ['sh','-c','touch /tmp/test; sleep 60; rm -rf /tmp/test; sleep 3600']
# 查看本地是否有镜像,有则使用,没有则下载
imagePullPolicy: IfNotPresent
livenessProbe:
exec:
command: ['test','-e','/tmp/test']
initialDelaySeconds: 1
periodSeconds: 3
  1. 生成 Pod
1
kubectl create -f livenessProbe.yaml
  1. 可以看到当 test 文件给删除的时候,Pod 就会重启,重启后又会有 test 文件,一直循环下去

livenessProbe探针测试一

livenessProbe-httpget

  1. 编写 yaml 文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
vim livenessProbe_httpget.yaml
apiVersion: v1
kind: Pod
metadata:
name: liveness-httpget-pod
namespace: default
spec:
containers:
- name: liveness-httpget-container
image: daocloud.io/library/nginx:latest
ports:
- name: http
containerPort: 80
# Pod生成1秒之后会每3秒检测一次是否存在index.html文件,如果超过10秒没有则重启Pod
livenessProbe:
httpGet:
port: http
path: /index.html
initialDelaySeconds: 1
periodSeconds: 3
# 允许超时时间
timeoutSeconds: 10
  1. 创建容器,可以看到是在正常运行的
1
kubectl create -f livenessProbe_httpget.yaml

livenessProbe探针测试三

  1. 删除 index.html 文件,可以看到会执行重启
1
2
kubectl exec -it liveness-httpget-pod -- /bin/sh
rm -rf /usr/share/nginx/html/index.html

livenessProbe探针测试四

livenessProbe-tcp

  1. 编写 yaml 文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
vim livenessProbe_tcp.yaml
apiVersion: v1
kind: Pod
metadata:
name: liveness-tcp-pod
namespace: default
spec:
containers:
- name: liveness-tcp-container
image: daocloud.io/library/nginx:latest
livenessProbe:
initialDelaySeconds: 5
timeoutSeconds: 1
tcpSocket:
port: 8080
periodSeconds: 3
  1. 生成 Pod,会发现一直处于重启状态
1
kubectl create -f livenessProbe_tcp.yaml

livenessProbe探针测试五

3.4.3 Start 和 Stop

Start 和 Stop 是指 Pod 在生成后执行和结束前执行的命令

  1. 编写 yaml 文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
vim start_stop.yml
apiVersion: v1
kind: Pod
metadata:
name: start-stop-pod
namespace: default
spec:
containers:
- name: start-stop-container
image: daocloud.io/library/nginx:latest
lifecycle:
postStart:
exec:
command: ['/bin/sh','-c','echo this is postStart test > /usr/share/message']
preStop:
exec:
command: ['/bin/sh','-c','echo this is preStop test > /usr/share/message']
  1. 生成 Pod,进入 Pod 可以看到 Start 执行的命令,由于 Pod 停止后就没有了,所以看不到 Stop 执行的命令
1
kubectl exec -it start-stop-pod -- /bin/sh

start_stop测试

四、k8s控制器

4.1 什么是控制器

k8s 中内置了很多 controller,用来控制 Pod 的具体状态和行为。

控制器的类型有:

  • ReplicationController(已弃用) 和 ReplicaSet
  • Deployment
  • DaemonSet
  • StateFulSet
  • Job 和 CronJob
  • Horizontal Pod Autoscaling

4.2 RS 与 Deployment

k8s 管理 Pod 主要用到以下三个组件:

  • Replication Controller(RC):用来确保容器应用的副本数始终保持在用户定义的副本数,如果有容器异常退出,会自动创建新的 Pod 来代替,如果有异常多出的容器也会自动回收。
  • ReplicaSet(RS):相比 RC 多了支持 selector,推荐使用 RS。
  • Deployment:用来管理 RS。

RS 单独使用

  1. 编写 yaml 文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
vim rs_test.yaml
apiVersion: apps/v1
kind: ReplicaSet
metadata:
# RS名称
name: rs-test
spec:
# 设置副本数
replicas: 3
selector:
# 设置标签
matchLabels:
tier: rs-test
# 设置模板,会根据此模板生成Pod
template:
metadata:
# 与上边RS设置的标签相对应,说明根据该模板创建的Pod都由标签相对RS管理
labels:
tier: rs-test
spec:
containers:
- name: rs-nginx-container
image: daocloud.io/library/nginx:latest
  1. 生成 RS,可以看到会根据 replicas设置的数量创建 Pod
1
kubectl create -f rs_test.yaml

RS测试一

  1. 删除这些 Pod,RS 也会按照副本数重新建立新的 Pod
1
kubectl delete pod --all

RS测试二

RS 与 Deployment

Deployment和RS

  1. 编写 yaml 文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
vim nginx_deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
# Deployment详细信息
spec:
# 副本数
replicas: 3
# 选择标签
selector:
# 标签匹配,与Deployment对应
matchLabels:
app: nginx-deployment
# Pod模板
template:
metadata:
# 定义标签
labels:
app: nginx-deployment
spec:
containers:
- name: nginx-deployment-container
image: daocloud.io/library/nginx:latest
ports:
- containerPort: 80
  1. 生成 Deployment
1
2
kubectl create -f nginx_deployment.yaml --record
record:记录命令,方便每次 reversion 的变化
  1. 可以看到会生成对应的 RS 和 Pod

deployment测试一

  1. 扩容
1
kubectl scale deployment nginx-deployment --replicas 5
  1. 更新镜像
1
kubectl set image deployment/nginx-deployment nginx-deployment-container=daocloud.io/library/nginx:1.19.1

可以看到会创建一个新的 RS,以实现灰度更新

deployment测试二

  1. 回滚
1
kubectl rollout undo deployment/nginx-deployment
  1. 查看回滚状态
1
kubectl rollout status deployment/nginx-deployment
  1. 查看历史版本
1
kubectl rollout history deployment/nginx-deployment
  1. 回到历史指定版本
1
kubectl rollout undo deployment/nginx-deployment --to-revision=1
  1. 暂停更新
1
kubectl rollout pause deployment/nginx-deployment

4.3 DaemonSet

DaemonSet 确保全部或者一部分 Node 上运行一个 Pod 的副本,当有 Node 加入集群时,也会为他们新增一个 Pod,当这些 Node 退出集群时,这些 Pod 也会被回收。

DaemonSet 的一些典型用法:

  • 运行集群存储 daemon,例如在每个 Node 上运行 glusterd、ceph等。
  • 运行日志收集daemon,例如fluentd、logstash等。
  • 运行监控daemon,例如 Prometheus 的 Node exporter、zabbix等。
  1. 编写 yaml 文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
vim daemonset_test.yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
# DaemonSet名称
name: daemonset-test
# 设置标签
labels:
app: daemonset-nginx
spec:
selector:
# 该标签要与上边标签一致
matchLabels:
name: daemonset-nginx
template:
metadata:
labels:
name: daemonset-nginx-pod
spec:
containers:
- name: daemonset-nginx-container
image: daocloud.io/library/nginx:latest
  1. 生成 DaemonSet
1
kubectl create -f daemonset_test.yaml

daemonset测试一

  1. 删除一个 Pod 之后 DaemonSet 为保证每个节点都至少有一个副本,就会重新创建新的 Pod

daemonset测试二

4.4 Job 和 CronJob

4.4.1 Job

Job 负责批处理任务,即仅执行一次的任务,它保证批处理任务的一个或多个 Pod 成功结束。

Job spec

  • spec.template格式同 Pod 相同

  • RestartPolicy仅支持NeverOnFailure

  • 单个 Pod 时,默认 Pod 成功运行后 Job 即结束

  • spec.completions标志 Job 结束时需要成功运行的 Pod 个数,默认为1

  • spec.parallelism标志并行运行的 Pod 个数,默认为1

  • spec.activeDeadlineSeconds标志失败 Pod 的重试最大时间,超过该时间就不会再重试

  1. 编写 yaml 文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
vim job.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: job
spec:
template:
metadata:
name: job-pod
spec:
containers:
- name: job-pod-container
image: perl
imagePullPolicy: IfNotPresent
# 通过perl语言进行圆周率计算,计算小数点后2000位
command: ['perl','-Mbignum=bpi','-wle','print bpi(2000)']
restartPolicy: Never
  1. 生成 Job,可以看到开始是 Running,接着就是 Completed,代表 Job 已经完成
1
kubectl create -f job.yaml

job测试一

  1. 查看日志可以看到计算好的圆周率

job测试二

4.4.2 CronJob

Cron Job 是基于时间管理控制 Job,即在给定的时间只运行一次、周期性的在指定时间运行。

CronJob spec

  • 所有 CronJob 的 schedule: 时间都是基于 kube-controller-manager 的时区。
  • .spec.schedule.spec 需要的域。它使用了 Cron 格式串,例如 0 * * * * or @hourly ,做为它的任务被创建和执行的调度时间。
  • .spec.jobTemplate是任务的模版,是必须项。
  • .spec.startingDeadlineSeconds 域是可选项。它表示任务如果由于某种原因错过了调度时间,开始该任务的截止时间的秒数。过了截止时间,CronJob 就不会开始任务。 不满足这种最后期限的任务会被统计为失败任务。如果该域没有声明,那任务就没有最后期限。
  • .spec.suspend域也是可选的。如果设置为 true ,后续发生的执行都会挂起。 这个设置对已经开始的执行不起作用。默认是关闭的。
  • .spec.successfulJobsHistoryLimit.spec.failedJobsHistoryLimit是可选的。 这两个字段指定应保留多少已完成和失败的任务。 默认设置为3和1。限制设置为0代表相应类型的任务完成后不会保留。

并发性规则

.spec.concurrencyPolicy声明了 CronJob 创建的任务执行时发生重叠如何处理。 spec 仅能声明下列规则中的一种:

  • Allow (默认):CronJob 允许并发任务执行。
  • Forbid: CronJob 不允许并发任务执行;如果新任务的执行时间到了而老任务没有执行完,CronJob 会忽略新任务的执行。
  • Replace:如果新任务的执行时间到了而老任务没有执行完,CronJob 会用新任务替换当前正在运行的任务。

并发性规则仅适用于相同 CronJob 创建的任务。如果有多个 CronJob,它们相应的任务总是允许并发执行的。

  1. 编写 yaml 文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
vim crontjob.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
name: cronjob
spec:
# CronJob必要字段,格式为分时日月周
schedule: "*/1 * * * *"
# job模板
jobTemplate:
spec:
template:
spec:
containers:
- name: crontab-pod
image: busybox
imagePullPolicy: IfNotPresent
command:
- /bin/sh
- -c
- date; echo 'Welcome My K8s'
restartPolicy: OnFailure
  1. 生成 CronJob,可以看到每分钟都会创建一个 Job 和 Pod
1
kubectl create -f cronjob.yaml

cronjob测试一

cronjob测试二

五、Service

5.1 什么是Service

Service一

到这里我们都知道,Deployment 会根据 replicas保证 Pod 的数量,当上图的其中一个 php-fpm Pod 出现问题时,就会新建一个来代替。但这时候会会出现一个问题,新的 Pod 的地址与旧的很可能不一样,那 nginx 也无法连接到新的 Pod,除非修改 nginx 的配置,这样的 k8s 集群效率是非常低的。

Service 就能解决该问题,在 nginx 和 php-fpm 中间添加一个 Service,由该 Service 来管理各 php-fpm Pod 的信息,每个 Pod 会设置一个标签,只要与 SVC 中设置的标签相匹配,就可以进行管理,其它 Pod(例如下图的 nginx)想要访问该 SVC 管理的 Pod,只要通过 SVC 的地址就可以访问到。

Service 定义了这样一种抽象:逻辑上的一组 Pod,一种可以访问它们的策略 —— 通常称为微服务。

Service二

5.2 Service发布服务(服务类型)

对一些应用的某些部分(如前端),可能希望将其暴露给 Kubernetes 集群外部 的 IP 地址。

Kubernetes ServiceTypes 允许指定你所需要的 Service 类型,默认是 ClusterIP

Type 的取值以及行为如下:

  • ClusterIP:通过集群的内部 IP 暴露服务,选择该值时服务只能够在集群内部访问。 这也是默认的 ServiceType
  • NodePort:通过每个节点上的 IP 和静态端口(NodePort)暴露服务。 NodePort 服务会路由到自动创建的 ClusterIP 服务。 通过请求 <节点 IP>:<节点端口>,你可以从集群的外部访问一个 NodePort 服务。
  • LoadBalancer:使用云提供商的负载均衡器向外部暴露服务。 外部负载均衡器可以将流量路由到自动创建的 NodePort 服务和 ClusterIP 服务上。
  • ExternalName:通过返回 CNAME 和对应值,可以将服务映射到 externalName 字段的内容(例如,foo.bar.example.com)。 无需创建任何类型代理。

ClusterIP

ClusterIP类型

NodePort

NodePort类型

LoadBalancer

LoadBalancer类型

ExternalName

ExternalName类型

5.3 虚拟 IP 和 Service 代理

在 Kubernetes 集群中,每个 Node 运行一个 kube-proxy 进程。 kube-proxy 负责为 Service 实现了一种 VIP(虚拟 IP)的形式,而不是 ExternalName 的形式。

代理模式分类:

userspace 代理模式

在该模式下,所有操作都要通过 kube-proxy 进行一个代理的操作,kube-proxy 的压力相对会较大。

userspace代理模式

iptables 代理模式

在该模式下,所有的访问都通过 iptables 来处理,kube-proxy 的压力就会减小很多。

iptables代理模式

IPVS 代理模式

在该模式下,iptables 换成了 IPVS,通过内核模块来实现负载均衡。

ipvs 模式下,kube-proxy 监视 Kubernetes 服务和端点,调用 netlink 接口相应地创建 IPVS 规则, 并定期将 IPVS 规则与 Kubernetes 服务和端点同步。 该控制循环可确保 IPVS 状态与所需状态匹配。访问服务时,IPVS 将流量定向到后端Pod之一。

IPVS代理模式基于类似于 iptables 模式的 netfilter 挂钩函数, 但是使用哈希表作为基础数据结构,并且在内核空间中工作。 这意味着,与 iptables 模式下的 kube-proxy 相比,IPVS 模式下的 kube-proxy 重定向通信的延迟要短,并且在同步代理规则时具有更好的性能。 与其他代理模式相比,IPVS 模式还支持更高的网络流量吞吐量。

IPVS 提供了更多选项来平衡后端 Pod 的流量。 这些是:

  • rr:轮替(Round-Robin)
  • lc:最少链接(Least Connection),即打开链接数量最少者优先
  • dh:目标地址哈希(Destination Hashing)
  • sh:源地址哈希(Source Hashing)
  • sed:最短预期延迟(Shortest Expected Delay)
  • nq:从不排队(Never Queue)

注意:当 kube-proxy 以 IPVS 代理模式启动时,它将验证 IPVS 内核模块是否可用。 如果未检测到 IPVS 内核模块,则 kube-proxy 将退回到以 iptables 代理模式运行。

IPVS代理模式

5.4 ClusterIP

ClusterIP 主要在每个 Node 节点使用 iptables / IPVS,将发向 ClusterIP 对应端口的数据,转发到 kube-proxy 中,kube-proxy 内部可以实现负载均衡,并可以查询到该 Service 下对应 Pod 的地址和端口,进而把数据转发给对应的 Pod 的地址和端口。

示例

  1. 创建 Deployment
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
vim nginx_svc_deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
namespace: default
spec:
replicas: 3
selector:
# 创建两个标签
matchLabels:
app: nginx
version: latest
template:
metadata:
labels:
app: nginx
version: latest
spec:
containers:
- name: nginx-pod
image: daocloud.io/library/nginx:latest
# 释放的端口
ports:
- name: http-port
containerPort: 80
  1. 创建 Service
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
vim clusterip_svc.yaml
apiVersion: v1
kind: Service
metadata:
name: nginx-clusterip-service
namespace: default
spec:
type: ClusterIP
# 用来匹配Pod中的标签,全部匹配才会管理该Pod
selector:
app: nginx
version: latest
ports:
- name: http-port
port: 80
targetPort: 80
  1. 可以看到 Deployment 和 SVC 都已经创建成功,且直接访问 SVC 的 ClusterIP 地址就可以访问到 Pod 了

ClusterIP测试一

ClusterIP测试二

  1. 测试负载均衡,在各容器内部创建一个 html 页面
1
2
3
4
kubectl exec -it nginx-deployment-*** -- /bin/bash
echo 'this is node01-pod01' > /usr/share/nginx/html/pod.html
echo 'this is node02-pod01' > /usr/share/nginx/html/pod.html
...
  1. 可以看到是实现了负载均衡的

ClusterIP测试三

5.5 无头服务(Headless Services)

有时不需要或不想要负载均衡,以及单独的 Service IP。 遇到这种情况,可以通过指定 Cluster IP(spec.clusterIP)的值为 "None" 来创建 Headless Service。

可以使用无头 Service 与其他服务发现机制进行接口,而不必与 Kubernetes 的实现捆绑在一起。

对这无头 Service 并不会分配 Cluster IP,kube-proxy 不会处理它们, 而且平台也不会为它们进行负载均衡和路由。 DNS 如何实现自动配置,依赖于 Service 是否定义了选择算符。

示例

  1. 创建 Headless Service
1
2
3
4
5
6
7
8
9
10
11
12
13
14
vim headless_service.yaml
apiVersion: v1
kind: Service
metadata:
name: headless-service
namespace: default
spec:
selector:
app: nginx
version: latest
clusterIP: "None"
ports:
- port: 80
targetPort: 80
  1. 可以看到是没有分配 VIP 的

无头服务测试一

  1. 安装 bind-utils 工具来测试无头服务的作用,可以看到即使没有了 VIP,但依旧可以通过域名来访问到不同的 Pod
1
2
3
4
5
6
yum -y install bind-utils
# k8s内部使用dns访问格式:SVC名称.命名空间.svc.集群域(默认集群域为cluster.local)
# dig命令可以用来获取域名的详细信息
# 10.244.0.37是其中一个coredns的地址
# -t A:显示A记录,A记录是将域名指向一个IPv4地址,即一个域名解析到一个IP地址
dig -t A headless-service.default.svc.cluster.local @10.244.0.37

无头服务测试二

5.6 NodePort

NodePort 的原理在于在 Node 上开放了一个端口,将该端口的流量导入到 kube-proxy 中,然后再由 kube-proxy 传送给不同的 Pod。

示例

  1. 创建 Service,这里的标签仍与上面 ClusterIP 中的 Deployment 创建的 Pod 相匹配
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
vim nodeport_svc.yaml
apiVersion: v1
kind: Service
metadata:
name: nginx-nodeport-service
namespace: default
spec:
type: NodePort
selector:
app: nginx
version: latest
ports:
- name: http-port
port: 80
targetPort: 80
  1. 可以看到会暴露一个端口,外部通过这个端口就可以访问到内部的 Pod,且是每一个 Node 都开启了这个端口,所以也可以实现负载均衡

NodePort测试一

NodePort测试一

NodePort测试三

  1. 在 iptables / IPVS 规则中可以看到开启该端口的规则
1
2
iptables -t nat -nvl | grep 31419
ipvsadm -Ln | grep 31419

NodePort测试四

5.7 LoadBalancer

LoadBalancer 就是在 NodePort 的基础上,通过 LAAS 来实现负载均衡,用户指要访问 LAAS即可,LAAS 会将请求通过调度转发给不同的 Node。

5.8 ExternalName

ExternalName 通过返回 CNAME 和它的值,将服务映射到 ExternalName 字段的内容,ExternalName 没有 selector,也没有端口的设置,对于运行在集群之外的服务,ExternalName 是通过该外部服务的别名来提供服务的。

当这个 Service 创建成功时,就会有 externalname-service.default.svc.cluster.local 的 fqdn 被创建,如果有用户访问到这个 fqdn,就会被改写成 my.database.example.com,这就是 DNS 内部的一个 CNAME 记录,也就是别名记录。

示例

  1. 创建测试用的 Pod
1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: v1
kind: Pod
metadata:
name: curl-pod
spec:
containers:
- name: curl-pod-container
# curl镜像包含测试网络和DNS的工具
image: docker.io/appropriate/curl
imagePullPolicy: IfNotPresent
command: ['sh', '-c']
args: ['echo "curl test"; sleep 3600']
  1. 创建 ExternalName
1
2
3
4
5
6
7
8
apiVersion: v1
kind: Service
metadata:
name: externalname-svc
spec:
type: externalname
# 引入百度的网址
externalName: www.baidu.com
  1. 进入 Pod 内部测试可以看到,通过 nslookup 可以解析到百度的地址
1
nolookup SVC名称.命名空间.svc.集群域

externalname测试一

5.9 ingress

5.9.1 什么是ingress

ingress 是对集群中服务的外部访问进行管理的 API 对象,典型的访问方式是 HTTP。

ingress 可以提供负载均衡、SSL 终结和基于名称的虚拟托管。

ingress 由两部分构成:

  • ingress controller:将新加入的 ingress 转化成 Nginx 的配置文件并使之生效。
  • ingress 服务:将 Nginx 的配置抽象成一个 ingress 对象,每添加一个新的服务只需写一个新的 ingress 的 yaml 文件即可。

ingress controller 主要有两种,nginx-ingresstraefik-ingress,这里主要讲nginx-ingress

ingress 官方网址:https://kubernetes.github.io/nginx-ingress

ingress GitHub 网址:https://github.com/kubernetes/nginx-ingress

nginx-ingress功能

nginx-ingress主要负责向外暴露服务,同时提供负载均衡的功能。

Nginx 对后端运行的服务(Service1、Service2)提供反向代理,在配置文件中配置了域名与后端服务 Endpoints 的对应关系。客户端通过使用 DNS 服务或者直接配置本地的 hosts 文件,将域名都映射到 Nginx 代理服务器。当客户端访问 service1.com 时,浏览器会把包含域名的请求发送给 Nginx 服务器,Nginx 服务器根据传来的域名,选择对应的 Service,这里就是选择 Service1 后端服务,然后根据一定的负载均衡策略,选择 Service1 中的某个容器接收来自客户端的请求并作出响应。过程很简单,Nginx 在整个过程中仿佛是一台根据域名进行请求转发的“路由器”。

nginx-ingress一

nginx-ingress工作过程

nginx-ingress二

nginx-ingress模块在运行时主要分为三个主体:

  • Store:Store 会与 APIServer 以协程的 Pod 方式进行一个监听状态,发生新的事件会写入循环队列里。
  • NginxController:NginxController 会监听循环队列里的事件,发生一个循环就会更新一个事件,并写入 SyncQueue 里。
  • SyncQueue:SyncQueue 协程会定期拉取需要执行的任务(如果有必要的则直接从 Store 拉取过来进行修改),接着判断是否需要 reload nginx,最后会以 nginx 模块运行。

5.9.2 ingress规则

每个 HTTP 规则都包含以下信息:

  • 可选的 host。在此示例中,未指定 host,因此该规则适用于通过指定 IP 地址的所有入站 HTTP 通信。 如果提供了 host(例如 www.cqm.com),则 rules 适用于该 host
  • 路径列表 paths(例如,/testpath),每个路径都有一个由 serviceNameservicePort 定义的关联后端。 在负载均衡器将流量定向到引用的服务之前,主机和路径都必须匹配传入请求的内容。
  • backend(后端)是 Service 文档 中所述的服务和端口名称的组合。 与规则的 hostpath 匹配的对 Ingress 的 HTTP(和 HTTPS )请求将发送到列出的 backend

路径类型

Ingress 中的每个路径都需要有对应的路径类型(Path Type)。未明确设置 pathType 的路径无法通过合法性检查。当前支持的路径类型有三种:

  • ImplementationSpecific:对于这种路径类型,匹配方法取决于 IngressClass。 具体实现可以将其作为单独的 pathType 处理或者与 PrefixExact 类型作相同处理。
  • Exact:精确匹配 URL 路径,且区分大小写。
  • Prefix:基于以 / 分隔的 URL 路径前缀匹配。匹配区分大小写,并且对路径中的元素逐个完成。 路径元素指的是由 / 分隔符分隔的路径中的标签列表。 如果每个 p 都是请求路径 p 的元素前缀,则请求与路径 p 匹配。

5.9.3 部署nginx-ingress

  1. 通过 Bare-metal(NodePort) 方式部署,先下载 yaml 文件
1
2
3
4
5
6
7
8
9
# 地址一
# https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v0.46.0/deploy/static/provider/baremetal/deploy.yaml

# 地址二
# https://github.com/kubernetes/ingress-nginx/blob/master/deploy/static/provider/baremetal/deploy.yaml

# 由于镜像地址也在国外,所以通过国内镜像源拉取
docker pull registry.aliyuncs.com/google_containers/nginx-ingress-controller:v0.46.0
docker tag registry.aliyuncs.com/google_containers/nginx-ingress-controller:v0.46.0 k8s.gcr.io/ingress-nginx/controller:v0.46.0
  1. 获取镜像地址,下载好需要的镜像,并导入镜像到其它 node 上

nginx-ingress三

nginx-ingress四

  1. 通过 deploy.yaml 文件生成 svc
1
kubectl create -f deploy.yaml

nginx-ingress五

5.9.4 ingress HTTP代理访问

nginx-ingress六

ingress-nginx会根据配置好的 yaml 文件,自动配置 nginx.conf 和虚拟主机文件。

  1. 创建 deployment1、deployment2、service1、service2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
vim nginx_deployment_svc1.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment1
spec:
replicas: 2
selector:
matchLabels:
name: nginx1
template:
metadata:
labels:
name: nginx1
spec:
containers:
- name: nginx-pod
image: daocloud.io/library/nginx:latest
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80

---

apiVersion: v1
kind: Service
metadata:
name: nginx-service1
spec:
ports:
- port: 80
targetPort: 80
protocol: TCP
selector:
name: nginx1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
vim nginx_deployment_svc2.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment2
spec:
replicas: 2
selector:
matchLabels:
name: nginx2
template:
metadata:
labels:
name: nginx2
spec:
containers:
- name: nginx-pod
image: daocloud.io/library/nginx:latest
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80

---

apiVersion: v1
kind: Service
metadata:
name: nginx-service2
spec:
ports:
- port: 80
targetPort: 80
protocol: TCP
selector:
name: nginx2
  1. 创建 nginx-ingress1、nginx-ingress2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
vim nginx_ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: nginx-ingress1
spec:
rules:
# 设置虚拟主机,通过该域名访问
- host: www.cqm1.com
http:
paths:
# 路径列表
- path: /pod.html
# 路径类型
pathType: ImplementationSpecific
# 后端
backend:
# 指定连接哪个SVC
serviceName: nginx-service1
servicePort: 80

---

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: nginx-ingress2
spec:
rules:
- host: www.cqm2.com
http:
paths:
- path: /pod.html
pathType: ImplementationSpecific
backend:
service:
name: nginx-service2
port:
number: 80
  1. 创建
1
2
3
kubectl create -f nginx_deployment_svc1.yaml
kubectl create -f nginx_deployment_svc2.yaml
kubectl create -f nginx_ingress.yaml
  1. 给每个 Pod 写入信息,方便查看负载均衡
1
2
kubectl exec -it nginx-deployment-... -- /bin/bash
echo 'this is nginx-pod-1' > /usr/share/nginx/html/pod.html
  1. 查看 ingress-nginx 所创建的 svc 暴露的端口,以及 service1、service2、nginx-ingress1、nginx-ingress2
1
2
3
kubectl get svc -n ingress-nginx
kubectl get svc
kubectl get ingress

nginx-ingress七

nginx-ingress八

nginx-ingress九

  1. 在 /etc/hosts 文件下写入域名与 IP 绑定,访问测试,可以看到实现了负载均衡

nginx-ingress十

  1. 可以进入 ingress 控制器内部查看 nginx 配置,可以看到自动添加的代理配置
1
2
kubectl exec -it -n ingress-nginx ingress-nginx-controller-... -- /bin/bash
cat nginx.conf

nginx-ingress十一

5.9.5 ingress HTTPS代理访问

nginx-ingress十二

  1. 创建私钥
1
2
openssl req -x509 -sha256 -nodes -days 365 -newkey rsa:2048 -keyout tls.key -out tls.crt -subj "/CN=nginxsvc/O=nginxsvc"
kubectl create secret tls tls-secret --key tls.key --cert tls.crt
  1. 创建 deployment3、service3、nginx-ingress3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
vim nginx_deployment_svc3.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment3
spec:
replicas: 2
selector:
matchLabels:
name: nginx3
template:
metadata:
labels:
name: nginx3
spec:
containers:
- name: nginx-pod
image: daocloud.io/library/nginx:latest
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80

---

apiVersion: v1
kind: Service
metadata:
name: nginx-service3
spec:
ports:
- port: 80
targetPort: 80
protocol: TCP
selector:
name: nginx3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
vim nginx_ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: nginx-ingress3
spec:
tls:
- hosts:
- www.cqm3.com
secretName: tls-secret
rules:
- host: www.cqm3.com
http:
paths:
- path: /pod.html
pathType: ImplementationSpecific
backend:
serviceName: nginx-service3
servicePort: 80
  1. 创建
1
kubectl create -f nginx_deployment_svc3.yaml nginx_ingress.yaml

nginx-ingress十三

  1. 在每个 Pod 写入 pod.html,便于查看负载均衡效果,并测试
1
2
kubectl exec -it ingress-nginx-... -- /bin/bash
echo 'this is node01-pod' > /usr/share/nginx/html/pod.html

nginx-ingress十四

nginx-ingress十五

nginx-ingress十六

5.9.5 Nginx进行基础认证(BasicAuth)

  1. 通过 Apache 创建用户认证文件
1
2
3
yum -y install httpd
htpasswd -c auth cqm
kubectl create secret generic basic-auth --from-file=auth
  1. 创建 ingress
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
vim nginx_ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-nginx-auth
# 添加基础认证字段
annotations:
nginx.ingress.kubernetes.io/auth-type: basic
nginx.ingress.kubernetes.io/auth-secret: basic-auth
nginx.ingress.kubernetes.io/auth-realm: 'Authentication Required - cqm'
spec:
rules:
- host: auth.cqm.com
http:
paths:
- path: /pod.html
pathType: ImplementationSpecific
backend:
service:
# 这里使用HTTP代理实验的service1
name: nginx-service1
port:
number: 80
  1. 创建 ingress 后测试

nginx-ingress十七

5.9.6 Nginx重写

名称 描述 类型
nginx.ingress.kubernetes.io/rewrite-target 必须重定向流量的目标 URI string
nginx.ingress.kubernetes.io/ssl-redirect 指示位置部分是否仅可访问 SSL(当 Ingress 包含证书时默认为 True) bool
nginx.ingress.kubernetes.io/force-ssl-redirect 即使 Ingress 未启用 TLS,也强制重定向到 HTTPS bool
nginx.ingress.kubernetes.io/app-root 定义控制器必须重定向的应用程序根,如果它在“/”上下文中 string
nginx.ingress.kubernetes.io/use-regex 指示 Ingress 上定义的路径是否使用正则表达式 bool
  1. 先准备好转发后的 deployment、ingress等,这里用上边的 ingress1,用户访问 www.rewritecqm.com 时就跳转到 www.cqm1.com

nginx-ingress十八

  1. 编写重写 ingress
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
vim rewrite_ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: rewrite-ingress
annotations:
# 访问 www.rewritecqm.com:30779 将跳转到 http://www.cqm1.com:30779/pod.html
nginx.ingress.kubernetes.io/rewrite-target: http://www.cqm1.com:30779/pod.html
spec:
rules:
- host: www.rewritecqm.com
http:
paths:
- path: /pod.html
pathType: Prefix
backend:
service:
name: nginx-service1
port:
number: 80
  1. 测试

nginx-ingress十九

六、k8s存储

6.1 配置存储卷

配置存储卷并不是用来进行容器间相互交互或 Pod 间数据共享的,而是用于向各个 Pod 注入配置信息的,主要分为以下三种:

ConfigMap:可传递普通信息

Secret:可传递密码等敏感的配置信息

DownwardAPI:可传递 Pod 和容器自身的运行信息

6.1.1 ConfigMap

许多应用程序都会从配置文件、命令行参数或环境变量中读取配置信息。

ConfigMap API 给我们提供了向容器内部注入信息的机制,ConfigMap 可以被用来保存单个属性,也可以用来保存整个配置文件或者 JSON 二进制对象。

ConfigMap 的创建方式有三种,分别为基于目录、文件和字面值来创建。

基于目录创建 ConfigMap

  1. 创建指定目录,下载官方测试文件
1
2
3
4
mkdir -p /root/k8s/plugin/configmap/dir || cd /root/k8s/plugin/configmap/dir
wget https://kubernetes.io/examples/configmap/game.properties
wget https://kubernetes.io/examples/configmap/ui.properties
kubectl create configmap configmap1 --from-file=./
  1. 查看命令
1
2
kubectl describe cm configmap1
kubectl get cm configmap1 -o yaml

基于文件创建 ConfigMap

  1. 通过 game.properties 和 ui.properties 创建
1
kubectl create configmap configmap2 --from-file=game.properties --from-file=ui.properties

基于字面值创建 ConfigMap

  1. 通过--from-literal=键名=键值来创建
1
kubectl create configmap configmap3 --from-literal=special.how=very
6.1.1.1 Pod中使用ConfigMap

使用 ConfigMap 代替环境变量

  1. 创建两个 ConfigMap
1
2
3
4
5
6
7
8
9
vim special_cm.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: special-cm
namespace: default
data:
special.how: very
special.type: charm
1
2
3
4
5
6
7
8
vim env_cm.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: env-cm
namespace: default
data:
log_level: INFO
  1. 将这两个 ConfigMap 注入到 Pod中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
vim pod1.yaml
apiVersion: v1
kind: Pod
metadata:
name: pod1
spec:
containers:
- name: pod1-container
image: busybox
imagePullPolicy: IfNotPresent
command: ["/bin/bash", "-c", "env"]
# env:取某个 ConfigMap 中的某个键值
env:
- name: SPECIAL_HOW_KEY
# 定义环境变量
valueFrom:
# 表示从 ConfigMap 中引用
configMapKeyRef:
# 要引用的 ConfigMap 名称
name: special-cm
# 引用 ConfigMap 中的哪个键值对
key: special.how
- name: SPECIAL_TYPE_KEY
valueFrom:
configMapKeyRef:
name: special-cm
key: special.type
# envFrom:取某个 ConfigMap 的所有键值
envFrom:
- configMapRef:
name: env-cm
restartPolicy: Never
  1. 生成 Pod 后查看日志,可以看到注入成功
1
kubectl logs pod1

ConfigMap一

使用 ConfigMap 设置命令行参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
vim pod2.yaml
apiVersion: v1
kind: Pod
metadata:
name: pod2
spec:
containers:
- name: pod2-container
image: busybox
imagePullPolicy: IfNotPresent
command: ["sh", "-c", "echo $(SPECIAL_HOW_KEY) $(SPECIAL_TYPE_KEY)"]
env:
- name: SPECIAL_HOW_KEY
valueFrom:
configMapKeyRef:
name: special-cm
key: special.how
- name: SPECIAL_TYPE_KEY
valueFrom:
configMapKeyRef:
name: special-cm
key: special.type
envFrom:
- configMapRef:
name: env-cm
restartPolicy: Never

将 ConfigMap 数据添加到一个卷中

  1. 创建 Pod
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
vim pod3.yaml
apiVersion: v1
kind: Pod
metadata:
name: pod3
spec:
containers:
- name: pod3-container
image: busybox
imagePullPolicy: IfNotPresent
command: [ "/bin/sh", "-c", "sleep 1200" ]
# 挂载config-volume,挂载在/etc/config目录下
volumeMounts:
- name: config-volume
mountPath: /etc/config
# 创建一个卷,将special-cm内容写入config-volume卷
volumes:
- name: config-volume
configMap:
name: special-cm
restartPolicy: Never
  1. 进入 Pod,在挂载目录下查看是否写入到 config 文件里

ConfigMap二

6.1.1.2 ConfigMap热更新

本例子通过 ConfigMap 来实现热更新,可以实现热更新 nginx.conf,但需进入容器内部重载配置文件,所以通过热更新一个 html 来展示效果。

  1. 编写 yaml 文件,并创建
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
vim hotupdate.yaml
# ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
name: nginx-hotupdate-cm
namespace: default
data:
test.html: 'this is the fist test'

---

# Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx-pod
image: daocloud.io/library/nginx:latest
ports:
- containerPort: 80
# 挂载下边创建的volume
volumeMounts:
- name: test-html-volume
mountPath: /usr/share/nginx/html
# 创建一个volume,通过ConfigMap方式挂载,使用上面创建的nginx-hotupdate-cm
volumes:
- name: test-html-volume
configMap:
name: nginx-hotupdate-cm
1
kubectl apply -f hotupdate.yaml
  1. 测试是否能够访问到

ConfigMap三

  1. 修改 ConfigMap,并再次访问
1
kubectl edit cm nginx-hotupdate-cm

ConfigMap四

ConfigMap五

6.1.2 Secret

Secret 对象类型用来保存敏感信息,例如密码、OAuth 令牌和 SSH 密钥。 将这些信息放在 secret 中比放在 Pod 的定义或者 容器镜像 中来说更加安全和灵活。

Secret 的类型

内置类型 用法
Opaque 用户定义的任意数据
kubernetes.io/service-account-token 服务账号令牌
kubernetes.io/dockercfg ~/.dockercfg 文件的序列化形式
kubernetes.io/dockerconfigjson ~/.docker/config.json 文件的序列化形式
kubernetes.io/basic-auth 用于基本身份认证的凭据
kubernetes.io/ssh-auth 用于 SSH 身份认证的凭据
kubernetes.io/tls 用于 TLS 客户端或者服务器端的数据
bootstrap.kubernetes.io/token 启动引导令牌数据
6.1.2.1 Opaque

Opaque 使用base64编码存储信息,可以通过 base64 --decode 解码获得原始数据,因此安全性弱。

使用示例

  1. 生成 base64 编码
1
2
3
4
echo 'admin' | base64
YWRtaW4K
echo '12345' | base64
MTIzNDUK
  1. 编写 yaml 文件
1
2
3
4
5
6
7
8
9
vim opaque.yaml
apiVersion: v1
kind: Secret
metadata:
name: secret-opaque
type: Opaque
data:
username: YWRtaW4K
password: MTIzNDUK
1
kubectl apply -f opaque.yaml
  1. 挂载到 volume 中使用,查看测试可以看到挂载后的信息被解码了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
vim opaque-pod1.yaml
apiVersion: v1
kind: Pod
metadata:
name: opaque-pod1
spec:
containers:
- name: opaque-pod1-nginx
image: daocloud.io/library/nginx:latest
# 挂载下边创建的volume-opaque
volumeMounts:
- name: volume-opaque
mountPath: /etc/secrets
readOnly: yes
# 创建一个secret类型的volume,引用上边创建好的secret-opaque
volumes:
- name: volume-opaque
secret:
secretName: secret-opaque

secret一

  1. 导入到环境变量中并测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
vim opaque-pod2.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: opaque-deployment
spec:
replicas: 1
selector:
matchLabels:
app: nginx
template:
metadata:
name: opaque-pod2
labels:
app: nginx
spec:
containers:
- name: opaque-pod2-nginx
image: daocloud.io/library/nginx:latest
ports:
- containerPort: 80
# 将secret导入环境变量中
env:
# 命名变量
- name: USER
# 引用secret-opaque中的username的键值
valueFrom:
secretKeyRef:
name: secret-opaque
key: username
- name: PASSWORD
# 引用secret-opaque中的password的键值
valueFrom:
secretKeyRef:
name: secret-opaque
key: password

secret二

6.1.2.2 ImagePullSecret

可以使用下面两种 type 值之一来创建 Secret,用以存放访问 Docker 仓库来下载镜像的凭据。

  • kubernetes.io/dockercfg
  • kubernetes.io/dockerconfigjson

kubernetes.io/dockerconfigjson 创建 Secret 示例

  1. 创建 Secret
1
kubectl create secret docker-registry cqmregistry --docker-server=DOCKER_REGISTRY_SERVER --docker-username=DOCKER_USER --docker-password=DOCKER_PASSWORD --docker-email=DOCKER_EMAIL

secret三

  1. 在 Pod 中运用
1
2
3
4
5
6
7
8
9
10
11
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
containers:
- name: nginx-pod
image: daocloud.io/library/nginx:latest
# 调用创建好的Secret
imagePullSecrets:
- name: cqmregistry
6.1.2.3 Downward API

有时候 Pod 需要获取自身的信息,这时候 Downward API 就派上用场了。

Downward API 是通过 fieldRef 参数获取信息的。

示例一

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
apiVersion: v1
kind: Pod
metadata:
name: test-pod1
spec:
containers:
- name: test-pod1-container
image: busybox
imagePullPolicy: IfNotPresent
command: [ 'sh', '-c' ]
args: [ 'echo "EnvPodName:${EnvPodName} EnvNodeName:${EnvNodeName}", sleep 3600' ]
env:
- name: EnvPodName
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: EnvNodeName
valueFrom:
fieldRef:
fieldPath: spec.nodeName

示例二

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
apiVersion: v1
kind: Pod
metadata:
name: test-pod2
spec:
containers:
- name: test-pod2-container
image: busybox
imagePullPolicy: IfNotPresent
command: [ 'sh', '-c' ]
args: [ 'echo "EnvPodName:${EnvPodName} EnvNodeName:${EnvNodeName}", sleep 3600' ]
volumeMounts:
- name: test-volume
mountPath: /test
volumes:
- name: test-volume
downwardAPI:
items:
- path: 'PodName'
fieldRef:
fieldPath: metadata.name
- path: 'NodeName'
fieldRef:
fieldPath: spec.nodeName

6.2 本地存储卷

Container 中的文件在磁盘上是临时存放的,这给 Container 中运行的较重要的应用程序带来一些问题。问题之一是当容器崩溃时文件丢失。kubelet 会重新启动容器, 但容器会以干净的状态重启。 第二个问题会在同一 Pod 中运行多个容器并共享文件时出现。 Kubernetes Volume 这一抽象概念能够解决这两个问题。

Kubernetes 支持很多类型的卷。 Pod 可以同时使用任意数目的卷类型。 临时卷类型的生命周期与 Pod 相同,但持久卷可以比 Pod 的存活期长。 当 Pod 不再存在时,Kubernetes 也会销毁临时卷;不过 Kubernetes 不会销毁持久卷。对于给定 Pod 中任何类型的卷,在容器重启期间数据都不会丢失。

volume一

卷的类型分为很多种,可通过官方文档查看:https://kubernetes.io/zh/docs/concepts/storage/volumes/

6.2.1 emptyDir

当 Pod 分派到某个 Node 上时,emptyDir 卷会被创建,并且在 Pod 在该节点上运行期间,卷一直存在。 就像其名称表示的那样,卷最初是空的。 尽管 Pod 中的容器挂载 emptyDir 卷的路径可能相同也可能不同,这些容器都可以读写 emptyDir 卷中相同的文件。 当 Pod 因为某些原因被从节点上删除时,emptyDir 卷中的数据也会被永久删除。

需要注意的是,容器崩溃并不会导致 Pod 被从节点上移除,因此容器崩溃期间 emptyDir 卷中的数据是安全的。

emptyDir 的一些用途:

  • 缓存空间,例如基于磁盘的归并排序。
  • 为耗时较长的计算任务提供检查点,以便任务能方便地从崩溃前状态恢复执行。
  • 在 Web 服务器容器服务数据时,保存内容管理器容器获取的文件。
  1. 编写 yaml 文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
apiVersion: v1
kind: Pod
metadata:
name: emptydir-pod
spec:
containers:
- name: emptydir-container1
image: daocloud.io/library/nginx:latest
imagePullPolicy: IfNotPresent
volumeMounts:
- mountPath: /empty1
name: emptydir-volume
- name: emptydir-container2
image: busybox
imagePullPolicy: IfNotPresent
command: ['/bin/sh','-c','sleep 6000']
volumeMounts:
- mountPath: /empty2
name: emptydir-volume
volumes:
- name: emptydir-volume
emptyDir: {}
  1. 进入 Pod 中的不同容器查看是否共享同一个 volume,

volume二

volume三

6.2.2 hostPath

hostPath 卷能将主机节点文件系统上的文件或目录挂载到你的 Pod 中,类似 docker 中的 volume 挂载。

hostPath 的一些用法有:

  • 运行一个需要访问 Docker 内部机制的容器;可使用 hostPath 挂载 /var/lib/docker 路径。
  • 在容器中运行 cAdvisor(容器监控工具) 时,以 hostPath 方式挂载 /sys
  • 允许 Pod 指定给定的 hostPath 在运行 Pod 之前是否应该存在,是否应该创建以及应该以什么方式存在。

除了必需的 path 属性之外,用户可以选择性地为 hostPath 卷指定 type

支持的 type 值如下:

取值 行为
空字符串(默认)用于向后兼容,这意味着在安装 hostPath 卷之前不会执行任何检查。
DirectoryOrCreate 如果在给定路径上什么都不存在,那么将根据需要创建空目录,权限设置为 0755,具有与 kubelet 相同的组和属主信息。
Directory 在给定路径上必须存在的目录。
FileOrCreate 如果在给定路径上什么都不存在,那么将在那里根据需要创建空文件,权限设置为 0644,具有与 kubelet 相同的组和所有权。
File 在给定路径上必须存在的文件。
Socket 在给定路径上必须存在的 UNIX 套接字。
CharDevice 在给定路径上必须存在的字符设备。
BlockDevice 在给定路径上必须存在的块设备。

注意:具有相同配置(例如基于同一 PodTemplate 创建)的多个 Pod 会由于节点上文件的不同而在不同节点上有不同的行为,即假如同一个 template 创建出来的 Pod 分配在了不同的 Node 上时,会因为节点的不同而产生不同的行为。

  1. 编写 yaml 文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
apiVersion: v1
kind: Pod
metadata:
name: hostpath-pod
spec:
containers:
- name: hostpath-pod-container
image: daocloud.io/library/nginx:latest
volumeMounts:
- mountPath: /test
name: hostpath-volume
volumes:
- name: hostpath-volume
hostPath:
# 宿主机被挂载目录
path: /data
# 如果没有/data目录则会被创建
type: DirectoryOrCreate
  1. 查看是否挂载成功

volume四

需要注意的是,在FileOrCreate下,如果被挂载的目录不存在,那么不会自动创建该目录, 为了确保这种模式能够工作,可以尝试把文件和它对应的目录分开挂载。

hostPath FileOrCreate 配置示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
apiVersion: v1
kind: Pod
metadata:
name: hostpath-fileorcreate-pod
spec:
containers:
- name: hostpath-fileorcreate-pod-container
image: daocloud.io/library/nginx:latest
volumeMounts:
- mountPath: /test
name: mydir
- mountPath: /test/test.txt
name: myfile
volumes:
- name: mydir
hostPath:
path: /data
# 如果宿主机没有/data目录则会自动创建
type: DirectoryOrCreate
- name: myfile
hostPath:
path: /data/test.txt
# 因为上面已经确保会有/data目录,所以该文件的挂载不受影响
type: FileOrCreate

6.3 持久存储卷

6.3.1 PV和PVC

PV 和 PVC 是 k8s 提供的两个 api 资源。

PV

持久卷(PersistentVolume,PV)是集群中的一块存储,可以由管理员事先供应,或者使用存储类来动态供应,PV 是集群资源,和普通的 Volume 一样,也是使用卷插件来实现的,只是它们拥有独立于任何使用 PV 的 Pod 的生命周期。

volume五

PVC

持久卷申领(PersistentVolumeClaim,PVC)表达的是用户对存储的请求。概念上与 Pod 类似。Pod 会耗用 Node 资源,而 PVC 申领会耗用 PV 资源。Pod 可以请求特定数量的资源(CPU 和内存),同样 PVC 申领也可以请求特定的大小和访问模式。

volume六

PV 的供应方式有两种:

静态供应

集群管理员创建若干 PV 卷。这些卷对象带有真实存储的细节信息,并且对集群用户可用(可见)。PV 卷对象存在于 Kubernetes API 中,可供用户消费(使用)。

动态供应

如果管理员所创建的所有静态 PV 卷都无法与用户的 PersistentVolumeClaim 匹配, 集群可以尝试为该 PVC 申领动态供应一个存储卷。 这一供应操作是基于 StorageClass 来实现的:PVC 申领必须请求某个 存储类,同时集群管理员必须 已经创建并配置了该类,这样动态供应卷的动作才会发生。 如果 PVC 申领指定存储类为 "",则相当于为自身禁止使用动态供应的卷。

为了基于存储类完成动态的存储供应,集群管理员需要在 API 服务器上启用 DefaultStorageClass 准入控制器。 举例而言,可以通过保证 DefaultStorageClass 出现在 API 服务器组件的 --enable-admission-plugins 标志值中实现这点;该标志的值可以是逗号 分隔的有序列表。关于 API 服务器标志的更多信息,可以参考 kube-apiserver 文档。

绑定

通俗理解就是一旦 PV 与 PVC 进行了绑定,那么该 PV 就无法与其它 PVC 进行绑定了。

保护

当一个 PV 与 PVC 绑定之后,假设 Pod 被删除,那么该 PV 与 PVC 依旧会是一个绑定的关系。

持久卷的类型

PV 持久卷是用插件的形式来实现的。Kubernetes 目前支持以下插件:

访问模式

PV 卷可以用资源提供者所支持的任何方式挂载到宿主系统上。 如下表所示,提供者(驱动)的能力不同,每个 PV 卷的访问模式都会设置为 对应卷所支持的模式值。 例如,NFS 可以支持多个读写客户,但是某个特定的 NFS PV 卷可能在服务器 上以只读的方式导出。每个 PV 卷都会获得自身的访问模式集合,描述的是 特定 PV 卷的能力。

访问模式有:

  • ReadWriteOnce:卷可以被一个节点以读写方式挂载;
  • ReadOnlyMany:卷可以被多个节点以只读方式挂载;
  • ReadWriteMany:卷可以被多个节点以读写方式挂载。

对于不同类型的存储卷访问模式也有不同,如下表

卷插件 ReadWriteOnce ReadOnlyMany ReadWriteMany
AWSElasticBlockStore - -
AzureFile
AzureDisk - -
CephFS
Cinder - -
CSI 取决于驱动 取决于驱动 取决于驱动
FC -
FlexVolume 取决于驱动
Flocker - -
GCEPersistentDisk -
Glusterfs
HostPath - -
iSCSI -
Quobyte
NFS
RBD -
VsphereVolume - - (Pod 运行于同一节点上时可行)
PortworxVolume -
ScaleIO -
StorageOS - -

每个 PV 可以属于某个类(Class),通过将其 storageClassName 属性设置为某个 StorageClass 的名称来指定。 特定类的 PV 卷只能绑定到请求该类存储卷的 PVC 申领。 未设置 storageClassName 的 PV 卷没有类设定,只能绑定到那些没有指定特定存储类的 PVC 申领。

回收策略

目前的回收策略有:

  • Retain(保留) – 手动回收
  • Recycle(回收)– 基本擦除 (rm -rf /thevolume/*)
  • Delete(删除)– 诸如 AWS EBS、GCE PD、Azure Disk 或 OpenStack Cinder 卷这类关联存储资产也被删除

目前,仅 NFS 和 HostPath 支持回收(Recycle)。 AWS EBS、GCE PD、Azure Disk 和 Cinder 卷都支持删除(Delete)。

阶段(状态)

每个卷会处于以下阶段(Phase)之一:

  • Available(可用)– 卷是一个空闲资源,尚未绑定到任何申领;
  • Bound(已绑定)– 该卷已经绑定到某申领;
  • Released(已释放)– 所绑定的申领已被删除,但是资源尚未被集群回收;
  • Failed(失败)– 卷的自动回收操作失败。

示例一

这个实例是先后创建 PV、PVC、Deployment

  1. 创建 PV
1
2
3
4
5
6
7
8
9
10
11
12
13
14
apiVersion: v1
kind: PersistentVolume
metadata:
name: test-pv
spec:
capacity:
storage: 1Gi
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Recycle
storageClassName: nfs
nfs:
path: /nfs
server: 192.168.88.100
  1. 创建 PVC
1
2
3
4
5
6
7
8
9
10
11
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: test-pvc
spec:
accessModes:
- ReadWriteMany
storageClassName: nfs
resources:
requests:
storage: 1Gi
  1. 创建 Deployment
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
apiVersion: apps/v1
kind: Deployment
metadata:
name: test-deployment
spec:
replicas: 2
selector:
matchLabels:
app: nginx
template:
metadata:
name: test-deployment-pod
labels:
app: nginx
spec:
containers:
- name: test-deployment-pod-container
image: daocloud.io/library/nginx:latest
imagePullPolicy: IfNotPresent
volumeMounts:
- name: test-volume
mountPath: /usr/share/nginx/html
volumes:
- name: test-volume
persistentVolumeClaim:
claimName: test-pvc

示例二

这个实例会由 StatefulSet 自动创建 PVC

持久存储卷示例图

  1. 部署 NFS 服务器,并在每个节点安装nfs-utils
  2. 部署 PV,这里创建了四个 PV
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
apiVersion: v1
kind: PersistentVolume
metadata:
name: nfs-pv1
spec:
# PV容量大小
capacity:
storage: 1Gi
# 服务模式为RWO,卷可以被一个节点以读写方式挂载
accessModes:
- ReadWriteOnce
# 回收策略为Retain,即手动回收
persistentVolumeReclaimPolicy: Retain
# 类为nfs
storageClassName: nfs
# 制定nfs服务器地址和被挂载目录
nfs:
path: /nfs1
server: 192.168.88.100

---

apiVersion: v1
kind: PersistentVolume
metadata:
name: nfs-pv2
spec:
capacity:
storage: 2Gi
accessModes:
- ReadOnlyMany
persistentVolumeReclaimPolicy: Retain
storageClassName: nfs
nfs:
path: /nfs2
server: 192.168.88.100

---

apiVersion: v1
kind: PersistentVolume
metadata:
name: nfs-pv3
spec:
capacity:
storage: 3Gi
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
storageClassName: nfs
nfs:
path: /nfs3
server: 192.168.88.100

---

apiVersion: v1
kind: PersistentVolume
metadata:
name: slow-pv
spec:
capacity:
storage: 1Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: slow
nfs:
path: /slow
server: 192.168.88.100

volume七

  1. 创建无头 SVC、StatefulSet
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
apiVersion: v1
kind: Service
metadata:
name: nfs-pv-svc
labels:
app: nginx
spec:
ports:
- port: 80
targetPort: 80
# 不分配ip地址,为StatefulSet使用
clusterIP: None
# 匹配标签
selector:
app: nginx

---

apiVersion: apps/v1
kind: StatefulSet
metadata:
name: nfs-pv-statefulset
spec:
# 匹配标签
selector:
matchLabels:
app: nginx
serviceName: nfs-pv-svc
replicas: 3
template:
metadata:
name: nfs-pv-pod
# pod标签
labels:
app: nginx
spec:
containers:
- name: nfs-pv-pod-container
image: daocloud.io/library/nginx:latest
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80
# 挂载持久卷
volumeMounts:
- name: nfs-pv-volume
mountPath: /usr/share/nginx/html
# PVC模板
volumeClaimTemplates:
- metadata:
name: nfs-pv-volume
spec:
# 匹配规则
accessModes: [ 'ReadWriteOnce' ]
storageClassName: 'nfs'
resources:
requests:
storage: 1Gi
  1. 先后创建后查看 Pod 创建情况,可以看到只创建了一个 Pod,因为现有的 PV 只有一个符合匹配条件,而 StatefulSet 是前一个 Pod 创建成功才会创建下一个,因为第二个 Pod 创建不成功,所以第三个 Pod 不会被创建

volume八

  1. 进入容器内部可以看到挂载成功,去到 NFS 服务器的/nfs1目录下创建test.html文件测试成功

volume九

volume十

  1. 删除创建失败的 Pod 以及对应的 PVC,创建新的两个 PV,以满足 StatefulSet 的挂载要求,剩下的 Pod 就会逐一创建成功

volume十一

volume十二

  1. 删除上面测试的 Pod,StatefulSet 会重新创建一个 Pod,且数据不会丢失

volume十三

这里会有个问题,如果删除了 StatefulSet,那么对应的 Pod 也会被删除,可是已经绑定的 PV 并不会删除,这里就需要手动回收了。

手动回收

  • 删除 StatefulSet

  • 删除 PVC

  • 修改 PV

1
2
3
4
kubectl edit pv pv名称
# 删除ClaimRef的信息
ClainRef:
...

6.3.2 StorageClass

以上的方法都是静态创建的 PV,会出现 PVC 找不到条件符合的 PV 进行绑定。

而 StorageClass 的作用是根据 PVC 的需求动态创建 PV。

示例一

NFS存储分配器示例图

  1. k8s 在 1.20 版本开始就禁用了 selfLink,所以需在配置文件添加以下内容

volume十四

  1. StorageClass 是通过存储分配器来动态创建 PV 的,但 k8s 内部的存储分配器不支持 NFS,所以首先要安装 NFS 存储分配器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
vim nfs_provisioner.yaml
kind: Deployment
apiVersion: apps/v1
metadata:
name: nfs-client-provisioner
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-hangzhou.aliyuncs.com/open-ali/nfs-client-provisioner:latest
volumeMounts:
- name: nfs-client-root
mountPath: /nfs-provision
env:
# nfs存储分配器名称
- name: PROVISIONER_NAME
value: nfs-client
# nfs服务器地址
- name: NFS_SERVER
value: 192.168.88.100
# nfs共享目录
- name: NFS_PATH
value: /nfs
volumes:
- name: nfs-client-root
nfs:
server: 192.168.88.100
path: /nfs
  1. 给 NFS 存储分配器授权
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
vim nfs_provisioner_rbac.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: nfs-client-provisioner
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
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
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
namespace: default
subjects:
- kind: ServiceAccount
name: nfs-client-provisioner
namespace: default
roleRef:
kind: Role
name: leader-locking-nfs-client-provisioner
apiGroup: rbac.authorization.k8s.io
  1. 创建 StorageClass
1
2
3
4
5
6
7
8
9
10
11
vim nfs_storageclass.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
# storageclass名称
name: nfs-storageclass
# nfs存储分配器名称
provisioner: nfs-client
parameters:
# 表示pvc删除时,所绑定的pv不会被保留,true相反
archieveOnDelete: 'false'
  1. 创建 PVC,可以看到 NFS 存储分配器已经自动创建了 PV 与之绑定
1
2
3
4
5
6
7
8
9
10
11
12
13
vim nfs_storageclass_pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nfs-storageclass-pvc
spec:
accessModes:
- ReadWriteMany
# storageclass名称
storageClassName: 'nfs-client'
resources:
requests:
storage: 500Mi

volume十五

  1. 创建一个 Deployment 测试是否能用这个 PVC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
vim nfs_provisioner_deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nfs-provisioner-deployment
spec:
replicas: 1
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nfs-provisioner-deployment-nginx
image: daocloud.io/library/nginx:latest
imagePullPolicy: IfNotPresent
volumeMounts:
- name: nfs-storageclass-pvc
mountPath: /usr/share/nginx/html/test
volumes:
- name: nfs-storageclass-pvc
persistentVolumeClaim:
claimName: nfs-storageclass-pvc
  1. 在 NFS 服务器的共享目录下创建一个 test.html

volume十六

  1. 访问 Deployment 创建出来的 Pod,可以看到是可以访问的

volume十七

6.4 StatefulSet

StatefulSet 是一种提供排序和唯一性保证的特殊 Pod 控制器,当有部署顺序、持久数据或固定网络等相关的特殊需求时,可以用 StatefulSet 控制器来进行控制。

StatefulSet 提供有状态服务,主要功能如下:

  1. 实现稳定的持久化存储:通过 PVC 来实现,Pod 之间不能共用一个存储卷,每个 Pod 都要有一个自己专用的存储卷。
  2. 实现稳定的网络标识:Pod 重新调度后其 PodName 和 HostName 不变,通过无头 SVC 来实现。
  3. 实现有序部署、有序伸缩:Pod 是有顺序的,只有前一个 Pod 创建成功才会创建下一个,直到最后。
  4. 实现有序收缩、有序删除:从最后一个开始,依次删除到第一个。
  5. 无头 SVC :为 Pod 生成可以解析的 DNS 记录。

statefulset一

示例一

  1. 创建无头 SVC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
vim statefulset_nginx_svc.yaml
apiVersion: v1
kind: Service
metadata:
name: statefulset-nginx-svc
spec:
selector:
app: nginx
clusterIP: None
ports:
- protocol: TCP
port: 8080
targetPort: 80
type: ClusterIP
  1. 创建 StatefulSet
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
vim statefulset_nginx.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: statefulset-nginx
spec:
# 匹配无头SVC
serviceName: statefulset-nginx-svc
replicas: 4
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: statefulset-nginx-container
image: daocloud.io/library/nginx:latest
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 80
# PVC模板
volumeClaimTemplates:
- metadata:
name: statefulset-nginx-pvc
spec:
accessModes: [ 'ReadWriteOnce' ]
storageClassName: 'nfs-storageclass'
resources:
requests:
storage: 50Mi
  1. 可以看到 Pod 是有序创建的,且每个 Pod 都是单独使用一个 PVC 和 PV,删除 StatefulSet 也可以看到 Pod 是有序删除的,且删除后 PVC 与 PV 依旧存在,重新生成 StatefulSet 可以继续使用这些 PVC 和 PV

有序创建

statefulset二

PVC 与 PV

statefulset三

有序删除

statefulset四

  1. 创建一个 Pod 用来测试无头 SVC 提供的 DNS 服务
1
2
3
4
5
6
7
8
9
10
11
apiVersion: v1
kind: Pod
metadata:
name: test-pod
spec:
containers:
- name: test-pod-container
image: appropriate/curl:latest
imagePullPolicy: IfNotPresent
command: [ 'sh', '-c' ]
args: [ 'echo "this is test"; sleep 36000' ]
  1. 进入 Pod 后可以通过nslookup测试,形式为{ServiceName}.{NameSpace}.svc.{ClusterDomain}

statefulset五

  1. 通过域名也可以访问各个 Pod,形式为{PodName}{ServiceName}.{NameSpace}.svc.{ClusterDomain}

statefulset六

七、k8s调度器

scheduler 是 k8s 集群的调度器,对每一个新创建的 Pod 或者是未被调度的 Pod,scheduler 会选择一个最优的 Node 去运行这个 Pod。然而,Pod 内的每一个容器对资源都有不同的需求,而且 Pod 本身也有不同的资源需求。因此,Pod 在被调度到 Node 上之前, 根据这些特定的资源调度需求,需要对集群中的 Node 进行一次过滤。

在做调度决定时需要考虑的因素包括:单独和整体的资源请求、硬件/软件/策略限制、 亲和以及反亲和要求、数据局域性、负载间的干扰等等。

调度流程:

  • 过滤:调度器会将所有满足 Pod 调度需求的 Node 选出来。
  • 打分:调度器会为 Pod 从所有可调度节点中选取一个最合适的 Node。

7.1 亲和性与反亲和性

7.1.1 Node亲和性

节点亲和性是通过pod.spec.nodeAffinity来实现的,包括以下两种:

  • preferredDuringSchedulingIgnoredDuringExecution:软策略
  • requiredDuringSchedulingIgnoredDuringExecution:硬策略

requiredDuringSchedulingIgnoredDuringExecution硬策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
apiVersion: v1
kind: Pod
metadata:
name: node-required-pod
spec:
containers:
- name: node-required-pod-container
image: daocloud.io/library/nginx:latest
imagePullPolicy: IfNotPresent
# 亲和性设置
affinity:
# 节点亲和性设置
nodeAffinity:
# 硬策略
requiredDuringSchedulingIgnoredDuringExecution:
# 节点选择器
nodeSelectorTerms:
# 匹配表达式
- matchExpressions:
# 制定key,即以这个key的键值为匹配条件
# 通过kubectl get node --show-labels可以获得更多标签
- key: kubernetes.io/hostname
# In:label的值在某个列表中
# NotIn:label的值不在某个列表中
# Gt:label的值大于某个值
# Lt:label的值小于某个值
# Exists:某个label存在
# DoesNotExist:某个label不存在
# 这里设置的意思是,根据kubernetes.io/hostname的键值,永远不分配到键值为k8s-node01的节点上
operator: NotIn
values:
- k8s-node01

创建后可以看到 Pod 不会被创建在 k8s-node01 节点上Node亲和性一

preferredDuringSchedulingIgnoredDuringExecution软策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
apiVersion: v1
kind: Pod
metadata:
name: node-preferred-pod
spec:
containers:
- name: node-preferred-container
image: daocloud.io/library/nginx:latest
imagePullPolicy: IfNotPresent
affinity:
nodeAffinity:
# 软策略
preferredDuringSchedulingIgnoredDuringExecution:
# 权重,范围为1-100
- weight: 1
# 偏向于
preference:
# 匹配表达式
matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- k8s-node03

可以看到虽然要求部署到 k8s-node03 节点上,但由于本环境没有该节点,所以就被分配到其它的节点了

Node亲和性二

软硬合体版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
apiVersion: v1
kind: Pod
metadata:
name: node-preandreq-pod
spec:
containers:
- name: node-preandreq-container
image: daocloud.io/library/nginx:latest
imagePullPolicy: IfNotPresent
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- { key: cpu, operator: In, values: [4core] }
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 1
preference:
matchExpressions:
- { key: disktype, operator: In, values: [ssd] }

软策略和硬策略一起使用就会先满足硬策略再分析软策略,可以看到现有的节点没有可以满足以上条件的,所以 Pod 一直处于 Pending 状态

Node亲和性三

Node亲和性四

7.1.2 Pod亲和性

有时候需要将某些 Pod 与正在运行的已具有某些特质的 Pod 调度到一起,因此就需要 Pod 亲和性调度方式。

Pod 亲和性是通过spec.affinity.podAffinity/podAntiAffinity来实现的,前者为亲和性,后者为反亲和性,包括以下两种:

  • preferredDuringSchedulingIgnoredDuringExecution:软策略
  • requiredDuringSchedulingIgnoredDuringExecution:硬策略

requiredDuringSchedulingIgnoredDuringExecution硬策略

  1. 首先创建一个标签为app:nginx的 Pod
1
2
3
4
5
6
7
8
9
10
11
apiVersion: v1
kind: Pod
metadata:
name: test-pod
labels:
app: nginx
spec:
containers:
- name: test-pod-container-nginx
image: daocloud.io/library/nginx:latest
imagePullPolicy: IfNotPresent
  1. 创建硬策略亲和性 Pod
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
apiVersion: v1
kind: Pod
metadata:
name: pod-required
spec:
containers:
- name: pod-required-container
image: daocloud.io/library/nginx:latest
imagePullPolicy: IfNotPresent
# 亲和性设置
affinity:
# Pod亲和性设置
podAffinity:
# 硬策略
requiredDuringSchedulingIgnoredDuringExecution:
# 标签选择器
- labelSelector:
# 列出规则
matchExpressions:
# 甄选app这个标签
- key: app
# In:label的值在某个列表中
# NotIn:label的值不在某个列表中
# Exists:某个label存在
# DoesNotExist:某个label不存在
# 表示硬性匹配标签为app=nginx的这个Pod
operator: In
# 键值
values:
- nginx
# 分配到与这个Pod所属的节点kubernetes.io/hostname值相同的节点上,可以理解为分配到同一节点上,但可能有多个
topologyKey: kubernetes.io/hostname

可以看到会分配在同一节点上

Pod亲和性一

  1. 删除该 Pod,将podAffinity改为podAntiAffinity,将不会分配到一起

Pod亲和性二

7.2 污点和容忍度

污点(taint)表示一个节点上存在不良状况,污点会影响 Pod 的调度,其定义方式如下

1
kubectl taint node {节点名称} {污点名称}={污点值}:{污点的影响}

污点的影响有三种:

  • NoExecute:不将 Pod 调度到具备该污点的节点上,如果 Pod 已经在该节点运行,则会被驱逐。
  • NoSchedule:不将 Pod 调度到具备该污点的节点上,如果 Pod 已经在该节点运行,不会被驱逐。
  • PreferNoSchedule:不推荐将 Pod 调度到具备该污点的节点上。

添加污点

1
kubectl taint node k8s-node01 cpu=1:NoExecute

污点和容忍一

删除污点

1
kubectl taint node k8s-node01 cpu=1:NoExecute-

示例一

  1. 给 k8s-node01 打上污点
1
kubectl taint node k8s-node01 cpu=1:NoExecute
  1. 创建 Pod
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
apiVersion: v1
kind: Pod
metadata:
name: toleration-pod
spec:
containers:
- name: toleration-container
image: daocloud.io/library/nginx:latest
imagePullPolicy: IfNotPresent
# 添加节点亲和性硬策略,让该Pod调度到k8s-node01上
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- k8s-node01
# 容忍设置
tolerations:
# 容忍污点
- key: "cpu"
# Equal:指等于该值
# Exist:指有该key就可,值无所谓
operator: "Equal"
value: "1"
# 污点影响
effect: "NoExecute"
# 容忍时间,超过该事件就会被驱除,只有污点影响设置为NoExecute才可用
tolerationSeconds: 36

可以看到该 Pod 依旧可以调度到 k8s-node01 节点上,但超过 3600 秒后就被驱除了

污点和容忍二

容忍度设置一般用于 DaemonSet 控制器,因为 DaemonSet 控制器下的应用一般是为节点本身提供服务的。

7.3 优先级和抢占式调度

当集群的资源(CPU、内存、磁盘等)不足时,新 Pod 的创建会一直处于 Pending 的状态,默认情况下,除了系统外的 Pod,其它 Pod 的优先级都是相同的,如果调高了 Pod 的优先级,那么节点就会将低优先级的 Pod 驱逐,腾出空间给优先级高的 Pod,这就被称为抢占式调度

示例一

  1. 要调整优先级,需要先创建 PriorityClass
1
2
3
4
5
6
7
8
9
10
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: test-priorityclass
# 优先级设置
value: 1000000
# 是否在全局下使用,只可设置一个
globalDefault: false
# 描述
description: "this priorityclass is test"
  1. 在 Pod 中调用
1
2
3
4
5
6
7
8
9
10
apiVersion: v1
kind: Pod
metadata:
name: priorityclass-pod
spec:
containers:
- name: priorityclass-container
image: daocloud.io/library/nginx:latest
imagePullPolicy: IfNotPresent
priorityClassName: test-priorityclass

7.4 为Pod设置计算资源

容器运行时会提供一些机制来限制容器可以使用的计算资源(CPU、内存和磁盘等),Pod 模板中也提供了这个功能,主要如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 计算资源设置
resources:
# 资源限制设置,超过设置的值容器会被停止
limits:
# cpu限制
cpu:
# 内存限制
memory:
# 资源请求设置,至少需要多少资源容器才会被运行
requests:
# cpu请求
cpu:
# 内存请求
memory:

示例一

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
apiVersion: v1
kind: Pod
metadata:
name: resources-pod
spec:
containers:
- name: resources-container
# 压测工具
image: vish/stress
imagePullPolicy: IfNotPresent
# 进行压测,阈值为150Mi,每一秒增加5Mi压力
args: [ '-mem-total','150Mi','-mem-alloc-size','5Mi','-mem-alloc-sleep','1s' ]
resources:
limits:
cpu: '1'
memory: '100Mi'
requests:
cpu: '200m'
memory: '50Mi'

可以看到 Pod 最初可以运行,但20秒后就不行了

为Pod设置计算资源一

7.5 命名空间管理

命名空间的主要作用是对 k8s 集群的资源进行划分,这种划分是一种逻辑划分,用于实现多租户的资源隔离。

命名空间的创建

1
kubectl create namespace 命名空间名称

命名空间的资源配额

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
apiVersion: v1
kind: ResourceQuota
metadata:
name:
namespace:
spec:
hard:
# 计算资源配额
limits.cpu:
limits.memory:
requests.cpu:
requests.memory:

# 存储资源配额
# 在所有PVC中,存储资源的需求不能超过该值
requests.storage:
# 允许存在的PVC数量
persistentvolumeclaims:
# 允许与{storage-class-name}相关的PVC总量
{storage-class-name}.storageclass.storage.k8s.io/requests.storage:

# 对象数量配额
# 允许存在ConfigMap数量
configmaps:
# 允许存在的非终止状态的Pod数量
pods:
# 允许存在的RC数量
replicationcontrollers:
# 允许存在的资源配额数量
resourcequotas:
# 允许存在的SVC数量
services:
# 允许存在的LoadBalancer类型的SVC数量
services.loadbalancers:
# 允许存在的NodePort类型的SVC数量
services.nodeports:
# 允许存在的Secret数量
secrets:

7.5.1 命名空间的资源配额

示例一

  1. 先创建一个命名空间
1
kubectl create namespace test-ns
  1. 创建资源配额
1
2
3
4
5
6
7
8
9
10
apiVersion: v1
kind: ResourceQuota
metadata:
name: test-rq
namespace: test-ns
spec:
hard:
pods: '2'
services: '1'
persistentvolumeclaims: '4'
  1. 创建一个 Deployment
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
apiVersion: apps/v1
kind: Deployment
metadata:
name: test-ns-depolyment
namespace: test-ns
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: test-ns-depolyment-container
image: daocloud.io/library/nginx:latest
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80

配额限制的 Pod 数量是2,但 Deployment 设置的副本数是3,可以看到是创建不了第三个 Pod 的

命名空间资源配额一

7.5.2 命名空间单个资源的资源配额

通过设置资源配额,可以限定一个命名空间下使用的资源总量,但这只是总量限制,对于单个资源没有限制,有时候一个 Pod 就可能用完整个命名空间所指定的资源,为了避免,可以通过LimitRange来对单个资源进行限定。

设置容器的限额范围

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
apiVersion: v1
kind: LimitRange
metadata:
name: limitrange-container
namespace: test-ns
spec:
limits:
- max:
cpu: '200m'
memory: '300Mi'
min:
cpu: '100m'
memory: '150Mi'
default:
cpu: '180m'
memory: '250Mi'
defaultRequest:
cpu: '110m'
memory: '160Mi'
type: Container

命名空间资源配额二

当 LimitRange 创建成功后,创建该命名空间下的 Pod 时,各个容器的resource.limitsresource.requests必须满足 Max 和 Min 之间的范围。

defaultdefaultRequest是指,当创建 Pod 时没用设置限额,则根据此属性来设置限额。

设置 Pod 的限额范围

1
2
3
4
5
6
7
8
9
10
11
12
13
14
apiVersion: v1
kind: LimitRange
metadata:
name: limitrange-pod
namespace: test-ns
spec:
limits:
- max:
cpu: '1'
memory: '600Mi'
min:
cpu: '500m'
memory: '300Mi'
type: Pod

当 LimitRange 创建成功后,创建该命名空间下的 Pod 时,各个 Pod 的resource.limitsresource.requests必须满足 Max 和 Min 之间的范围。

设置 PVC 的限额范围

1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: v1
kind: LimitRange
metadata:
name: limitrange-pvc
namespace: test-ns
spec:
limits:
- max:
storage: 1Gi
min:
storage: 200Mi
type: PersistentVolumeClaim

设置 Pod 或容器的比例限额范围

1
2
3
4
5
6
7
8
9
10
apiVersion: v1
kind: LimitRange
metadata:
name: limitrange-ratio
namespace: test-ns
spec:
limits:
- maxLimitRequestRatio:
memory: 2
type: Pod

设置比例限额可以限制 Pod 或容器设置的请求资源和上限资源的比值,在该示例中,Pod 所有容器的resources.limits.memory的总和要 = Pod 所有容器的resources.requests.memory的总和的两倍,即上限必须是需求的两倍,Pod 才能够创建成功。

7.6 标签、选择器、注解

7.6.1 标签

k8s 的标签(label)是一种语义化标记标签,可以附加到 k8s 对象上,对它们进行标记和划分。

标签的形式是键值对,每个资源对象都可以拥有多个标签,但每个键都只能有一个值。

对于标签的设置,是通过metadata属性中实现的,如下

1
2
3
4
5
6
metadata:
name:
labels:
key1: value1
key2: value2
...

而对于已有的资源,可以通过以下命令添加或删除标签

1
2
kubectl label 资源类型 资源名称 标签名=标签值
kubectl label 资源类型 资源名称 标签名=标签值-

7.6.2 选择器

通过标签选择器(selector)就可以快速查找到指定标签的资源。

通过-l查找方式如下

1
kubectl get pod -l 标签名=/!=标签值

通过in notin查找

1
kubectl get pod -l '标签名1 in/notin (标签值1,标签值2)'

每种基于控制器的对象也可以使用标签来选择需要操作的 Pod,如 Job、Deployment、DaemonSet 等都可以在spec中指定选择器,以查找到符合条件的 Pod,如下

1
2
3
4
5
6
7
8
spec:
selector:
matchLabels:
app: nginx
release: stable
matchExpressions:
- { key: env, operator: In, values: [dev] }
- { key: track, operator: Exists }

在创建 SVC 时,都需要制定标签选择器来确定需要控制的资源,如下

1
2
3
4
5
6
7
8
apiVersion: v1
kind: Service
metadata:
name: svc
spec:
selector:
app: nginx
release: stable

在创建 PVC 时,除了用类匹配之外,也可以用标签来匹配适合的 PV,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv
labels:
pvnumber: pv01
spec:
capacity:
storage: 1Gi
accessModes:
- ReadWriteMany
storageClassName: nginx
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pvc
spec:
selector:
matchLabels:
pvnumber: pv01
resources:
requests:
storage: 1Gi
storageClassName: nginx

7.6.3 注解

注解(annotation)也是一种类似标签的机制,但只是给资源添加更多信息的方式,类似注释。

设置注解,是通过metadata来实现的,如下

1
2
3
4
5
6
7
8
9
10
11
apiVersion: v1
kind: Pod
metadata:
name: pod
annotation:
team: 'cqm'
phone: '123456'
email: '123456@789.com'
...
spec:
...

八、API Server

API Server 是集群内布各个组件通信的中介,也是外部控制的入口,k8s 的安全机制基本都是围绕着保护 API Server 来设计的,通过认证、鉴权、准入控制三步来保证 API Server 的安全。

我们在使用 k8s 时,都是通过kubectl工具来访问 API Server 的,kubectl把命令转换为对 API Server 的 REST API 调用。

apiserver访问流程

8.1 身份认证

身份认证主要用于确定用户能不能访问,是访问 API Server 的第一个关卡。

通过命令可以看到认证情况,可以看到是通过 6443 端口进行访问的。

身份认证一

要访问 API Server,首先就要进行身份认证,k8s 的身份认证分为以下两类:

  • 常规用户认证:主要提供于普通用户或独立于 k8s 之外的其他外部应用使用,以便能从外部访问 API Server。
  • ServiceAccount 认证:主要提供于内部的 Pod 使用。

8.1.1 常规用户认证

常规用户认证主要有三种方式:

  • HTTPS 证书认证:基于 CA 证书签名的数字证书认证。

  • HTTP 令牌认证:通过令牌来识别用户。

  • HTTP Base 认证:通过用户名和密码认证。

  1. 令牌认证是最实用也最普及的方式,首先生成一个随机令牌
1
head -c 16 /dev/urandom | od -An -t x | tr -d ' '
  1. 接着给 k8s 创建令牌认证文件
1
2
3
vim /etc/kubernetes/pki/token_auth_file
# 格式为:令牌1,用户1,用户ID
ec0f09bf9a3c40f9db1cc0f8e6664c12,cqm,1
  1. 认证文件创建好后,在 API Server 启动文件中进行引用
1
2
3
vim /etc/kubernetes/manifests/kube-apiserver.yaml
# 在spec中添加
--token-auth-file=/etc/kubernetes/pki/token_auth_file
  1. 接着就可以用认证好的用户访问 API Server 来获取 Pod 的信息了
1
curl --insecure https://192.168.88.10:6443/api/v1/namespaces/default/pods -H "Authorization:Bearer ec0f09bf9a3c40f9db1cc0f8e6664c12"

身份认证二

但可以看到还是失败,因为还没有进行授权,后边将进行授权。

8.1.2 ServiceAccount认证

ServiceAccount 认证主要提供于集群内布的 Pod 中的进程使用,常规用户认证是不限制命名空间的,但 ServiceAccount 认证的局限于它所在的命名空间中。

默认 ServiceAccount

每个命名空间都有个默认的 ServiceAccount,如果创建 Pod 时没用指定,那么就会使用默认的 ServiceAccount。

  1. 通过命令可以看到默认的 ServiceAccount

身份认证三

  1. 创建一个 Pod 进行测试
1
2
3
4
5
6
7
8
9
10
11
apiVersion: v1
kind: Pod
metadata:
name: sa-pod
spec:
containers:
- name: sa-pod-container
image: appropriate/curl:latest
imagePullPolicy: IfNotPresent
command: ['sh', '-c']
args: ['echo "this is sa test"; sleep 3600']
  1. 查看 Pod 的详细信息,可以看到被挂载了一个 Secret 类型的卷,实际上这里面就存放了 ServiceAccount 的认证信息

身份认证四

身份认证五

进入容器内部,通过以上地址映射令牌在进行访问 API Server,可以看到由于未授权依旧不行

1
2
curl --insecure https://192.168.88.10:6443/api/v1/namespaces/default/pods -H "Authorizat
ion:Bearer $(cat /var/run/secrets/kubernetes.io/serviceaccount/token)"

身份认证六

自定义 ServiceAccount

如果某些 Pod 需要访问 API Server,通常会让它引用自定义 ServiceAccount,并为其授权。

  1. 首先创建一个自定义 ServiceAccount
1
2
3
4
apiVersion: v1
kind: ServiceAccount
metadata:
name: my-serviceaccount
  1. 创建好后可以查看详细信息,包含了证书和令牌等

身份认证七

  1. 创建 Pod 引用
1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: v1
kind: Pod
metadata:
name: my-sa-pod
spec:
serviceAccountName: my-serviceaccount
containers:
- name: my-sa-pod-container
image: appropriate/curl:latest
imagePullPolicy: IfNotPresent
command: ['sh', '-c']
args: ['echo "this is sa test"; sleep 3600']
  1. 就可以看到 Pod 中已经引用了

身份认证八

身份认证九

8.2 RBAC授权

k8s 中有基于属性的访问控制(ABAC),基于角色的访问控制(RBAC),基于 HTTP 回调机制的访问控制(Webhook)、Node 认证等授权模式,但1.6版本开始就默认启用 RBAC 模式。

RBAC 授权主要分为两步:

  • 角色定义:指定角色名称,定义允许访问哪些资源以及允许的访问方式。
  • 角色绑定:将角色与用户(常规用户或 ServiceAccount)进行绑定。

RBAC授权原理

而角色定义和角色绑定又分为两种:

  • 只有单一指定命名空间访问权限的角色:角色定义关键字为 Role,角色绑定关键字为 RoleBinding。
  • 拥有集群级别(不限命名空间)访问权限的角色:角色定义为 ClusterRole,角色绑定关键字为 ClusterRoleBinding。

8.2.1 普通角色的定义与绑定

普通角色定义

  1. 创建一个普通角色
1
2
3
4
5
6
7
8
9
10
11
12
13
14
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
# 角色名
name: rbac-role
namespace: default
# 角色规则定义
rules:
# 表示可以对哪些API组的资源进行操作,这里为空即不限制
- apiGroups: [""]
# 可以访问的资源列表,这里为Pod
resources: ["pods"]
# 可以进行的访问方式
verbs: ["get", "watch", "list"]
  1. 访问方式可以通过开启kubectl反向代理查看,其中的verb就是可以访问的方式
1
2
kubectl proxy --port:8080
curl http://localhost:8080/{APIVersion}

普通角色绑定

定义角色后就可以进行绑定角色,绑定可以针对常规用户认证,也可以这对 ServiceAccount 认证。

  1. 创建角色绑定
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: rbac-rolebinding
namespace: default
# 将角色绑定给那些认证主体,它是一个数组,将常规用户认证cqm和ServiceAccount认证my-serviceaccount加入到rabc-role角色中
subjects:
# 常规用户认证绑定
- kind: User
name: cqm
apiGroup: ""
# ServiceAccount认证绑定
- kind: ServiceAccount
name: my-serviceaccount
apiGroup: ""
# 要绑定的角色
roleRef:
# 普通角色
kind: Role
name: rbac-role
apiGroup: ""

RBAC授权一

  1. 尝试用之前创建的常规用户认证和 ServiceAccount 认证来访问 API Server,可以发现就可以访问了
1
curl --insecure https://192.168.88.10:6443/api/v1/namespaces/default/pods -H "Authorization:Bearer ec0f09bf9a3c40f9db1cc0f8e6664c12"
1
2
curl --insecure https://192.168.88.10:6443/api/v1/namespaces/default/pods -H "Authorization:Bearer $(cat /var/run/secrets/kubern
etes.io/serviceaccount/token)"

RBAC授权二

8.2.2 集群角色的定义与绑定

集群角色与普通角色的区别如下:

  • 使用的关键字不同:普通角色使用 Role,绑定使用 RoleBinding,集群角色使用 ClusterRole,绑定使用 ClusterRoleBinding。
  • 集群角色不属于任何命名空间,模板也不需要指定命名空间,普通角色要求指定命名空间,如果没指定九默认 default。
  • 集群角色可以访问所有命名空间下的资源,也可以访问不在命名空间下的资源。

集群角色创建和绑定如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 集群角色定义
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
# 集群角色名
name: rbac-clusterrole
# 规则
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "watch", "list"]
---
# 集群角色绑定
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: rbac-clusterrolebinding
subjects:
- kind: User
name: cqm
apiGroup: ""
- kind: ServiceAccount
name: my-serviceaccount
apiGroup: ""
# 由于my-serviceaccount是某个命名空间下的,所以要指定命名空间
namespace: default
roleRef:
kind: ClusterRole
name: clusterrole
apiGroup: ""

这样一来用户(cqm,my-serviceaccount)都可以访问全局的资源,当然 ClusterRole 也可以和 RoleBinding 绑定在一起,因为 ClusterRole 是不限命名空间的,如果既想给某个认证主体绑定 ClusterRole,又想限制它能访问的命名空间,就可以通过与 RoleBinding 绑定来实现,本例中是指rbac-clusterrole的角色在绑定rbac-rolebinding后,可以访问在default命名空间下的任何资源,如下

1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: rbac-clusterrole
...
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: rbac-rolebinding
namespace: default
...

8.3 创建一个用户只能管理指定的命名空间

在实际的生产环境中,Master 的管理者可能有很多个,但不可能给人人都赋予 root 的权限,那么就可以创建新用户给其管理指定命名空间的权限。

  1. 创建新用户,这时候该用户是使用不了 k8s 的
1
2
3
useradd cqm
passwd cqm
...

给用户管理某个命名空间一

  1. 创建证书请求
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
vim cqm-csr.json
{
"CN": "cqm",
"hosts": [],
"key": {
"algo": "rsa",
"size": 2048
},
"names": [
{
"C": "CN",
"ST": "ShenZhen",
"L": "ShenZhen",
"O": "k8s",
"OU": "System"
}
]
}
  1. 下载证书生成工具
1
2
3
4
5
6
7
8
wget https://github.com/cloudflare/cfssl/releases/download/v1.6.0/cfssl_1.6.0_linux_amd64
wget https://github.com/cloudflare/cfssl/releases/download/v1.6.0/cfssljson_1.6.0_linux_amd64
wget https://github.com/cloudflare/cfssl/releases/download/v1.6.0/cfssl-certinfo_1.6.0_linux_amd64
mv cfssl_1.6.0_linux_amd64 cfssl
mv cfssl-certinfo_1.6.0_linux_amd64 cfssl-certinfo
mv cfssljson_1.6.0_linux_amd64 cfssljson
chmod a+x cfssl*
mv cfssl* /usr/local/bin
  1. 生成证书
1
2
cd /etc/kubernetes/pki
cfssl gencert -ca=ca.crt -ca-key=ca.key -profile=kubernetes ~/cqm-csr.json | cfssljson -bare cqm
  1. 设置集群参数
1
2
3
4
5
6
export KUBE_APISERVER="192.168.88.10:6443"
kubectl config set-cluster kubernetes \
--certificate-authority=/etc/kubernetes/pki/ca.crt \
--embed-certs=true \
--server=${KUBE_APISERVER} \
--kubeconfig=cqm.kubeconfig
  1. 设置客户端认证参数
1
2
3
4
5
kubectl config set-credentials cqm \
--client-certificate=/etc/kubernetes/pki/cqm.pem \
--client-key=/etc/kubernetes/pki/cqm-key.pem \
--embed-certs=true \
--kubeconfig=cqm.kubeconfig
  1. 设置上下文参数
1
2
3
4
5
kubectl config set-context kubernetes \
--cluster=kubernetes \
--user=cqm \
--namespace=dev \
--kubeconfig=cqm.kubeconfig
  1. 进行 RoleBinding,这里的意思是指将常规用户 cqm 与 ClusterRole 的 admin 角色进行绑定,且指定 dev 的命名空间给 cqm,最终效果是 cqm 只能够访问和管理 dev 命名空间下的所有资源
1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: cqm-rolebinding
namespace: dev
subjects:
- kind: User
name: cqm
apiGroup: ""
roleRef:
kind: ClusterRole
name: admin
apiGroup: ""

给用户管理某个命名空间二

  1. 设置默认上下文
1
2
3
4
5
mkdir /home/cqm/.kube
cp cqm.kubeconfig /home/cqm/.kube/config
chown cqm:cqm /home/cqm/.kube/config
su cqm
kubectl config use-context kubernetes --kubeconfig=/home/cqm/.kube/config

九、k8s扩展

9.1 可视化管理——Kubernetes Dashboard

Kubernetes Dashboard 可以实现 k8s 的可视化管理,可以实现对 Pod、控制器、Service 等资源的创建和维护,并对它们进行持续监控。

9.1.1 安装Kubernetes Dashboard

  1. 下载
1
wget https://raw.githubusercontent.com/kubernetes/dashboard/v2.3.1/aio/deploy/recommended.yaml
  1. 模板文件如下,将拉取镜像的地址改为国内地址,同时修改 SVC 模式为 NodePort,这样在集群之外也可以访问 Dashboard
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
apiVersion: v1
kind: Namespace
metadata:
name: kubernetes-dashboard

---

apiVersion: v1
kind: ServiceAccount
metadata:
labels:
k8s-app: kubernetes-dashboard
name: kubernetes-dashboard
namespace: kubernetes-dashboard

---

kind: Service
apiVersion: v1
metadata:
labels:
k8s-app: kubernetes-dashboard
name: kubernetes-dashboard
namespace: kubernetes-dashboard
spec:
type: NodePort
ports:
- port: 443
targetPort: 8443
selector:
k8s-app: kubernetes-dashboard

---

apiVersion: v1
kind: Secret
metadata:
labels:
k8s-app: kubernetes-dashboard
name: kubernetes-dashboard-certs
namespace: kubernetes-dashboard
type: Opaque

---

apiVersion: v1
kind: Secret
metadata:
labels:
k8s-app: kubernetes-dashboard
name: kubernetes-dashboard-csrf
namespace: kubernetes-dashboard
type: Opaque
data:
csrf: ""

---

apiVersion: v1
kind: Secret
metadata:
labels:
k8s-app: kubernetes-dashboard
name: kubernetes-dashboard-key-holder
namespace: kubernetes-dashboard
type: Opaque

---

kind: ConfigMap
apiVersion: v1
metadata:
labels:
k8s-app: kubernetes-dashboard
name: kubernetes-dashboard-settings
namespace: kubernetes-dashboard

---

kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
labels:
k8s-app: kubernetes-dashboard
name: kubernetes-dashboard
namespace: kubernetes-dashboard
rules:
- apiGroups: [""]
resources: ["secrets"]
resourceNames: ["kubernetes-dashboard-key-holder", "kubernetes-dashboard-certs", "kubernetes-dashboard-csrf"]
verbs: ["get", "update", "delete"]
- apiGroups: [""]
resources: ["configmaps"]
resourceNames: ["kubernetes-dashboard-settings"]
verbs: ["get", "update"]
- apiGroups: [""]
resources: ["services"]
resourceNames: ["heapster", "dashboard-metrics-scraper"]
verbs: ["proxy"]
- apiGroups: [""]
resources: ["services/proxy"]
resourceNames: ["heapster", "http:heapster:", "https:heapster:", "dashboard-metrics-scraper", "http:dashboard-metrics-scraper"]
verbs: ["get"]

---

kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
labels:
k8s-app: kubernetes-dashboard
name: kubernetes-dashboard
rules:
- apiGroups: ["metrics.k8s.io"]
resources: ["pods", "nodes"]
verbs: ["get", "list", "watch"]

---

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
labels:
k8s-app: kubernetes-dashboard
name: kubernetes-dashboard
namespace: kubernetes-dashboard
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: kubernetes-dashboard
subjects:
- kind: ServiceAccount
name: kubernetes-dashboard
namespace: kubernetes-dashboard

---

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: kubernetes-dashboard
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: kubernetes-dashboard
subjects:
- kind: ServiceAccount
name: kubernetes-dashboard
namespace: kubernetes-dashboard

---

kind: Deployment
apiVersion: apps/v1
metadata:
labels:
k8s-app: kubernetes-dashboard
name: kubernetes-dashboard
namespace: kubernetes-dashboard
spec:
replicas: 1
revisionHistoryLimit: 10
selector:
matchLabels:
k8s-app: kubernetes-dashboard
template:
metadata:
labels:
k8s-app: kubernetes-dashboard
spec:
containers:
- name: kubernetes-dashboard
image: registery.cn-hangzhou.aliyuncs.com/google_containers/dashboard:v2.3.1
imagePullPolicy: Always
ports:
- containerPort: 8443
protocol: TCP
args:
- --auto-generate-certificates
- --namespace=kubernetes-dashboard
volumeMounts:
- name: kubernetes-dashboard-certs
mountPath: /certs
- mountPath: /tmp
name: tmp-volume
livenessProbe:
httpGet:
scheme: HTTPS
path: /
port: 8443
initialDelaySeconds: 30
timeoutSeconds: 30
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsUser: 1001
runAsGroup: 2001
volumes:
- name: kubernetes-dashboard-certs
secret:
secretName: kubernetes-dashboard-certs
- name: tmp-volume
emptyDir: {}
serviceAccountName: kubernetes-dashboard
nodeSelector:
"kubernetes.io/os": linux
tolerations:
- key: node-role.kubernetes.io/master
effect: NoSchedule

---

kind: Service
apiVersion: v1
metadata:
labels:
k8s-app: dashboard-metrics-scraper
name: dashboard-metrics-scraper
namespace: kubernetes-dashboard
spec:
type: NodePort
ports:
- port: 8000
targetPort: 8000
selector:
k8s-app: dashboard-metrics-scraper

---

kind: Deployment
apiVersion: apps/v1
metadata:
labels:
k8s-app: dashboard-metrics-scraper
name: dashboard-metrics-scraper
namespace: kubernetes-dashboard
spec:
replicas: 1
revisionHistoryLimit: 10
selector:
matchLabels:
k8s-app: dashboard-metrics-scraper
template:
metadata:
labels:
k8s-app: dashboard-metrics-scraper
annotations:
seccomp.security.alpha.kubernetes.io/pod: 'runtime/default'
spec:
containers:
- name: dashboard-metrics-scraper
image: registery.cn-hangzhou.aliyuncs.com/google_containers/metrics-scraper:v1.0.6
ports:
- containerPort: 8000
protocol: TCP
livenessProbe:
httpGet:
scheme: HTTP
path: /
port: 8000
initialDelaySeconds: 30
timeoutSeconds: 30
volumeMounts:
- mountPath: /tmp
name: tmp-volume
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsUser: 1001
runAsGroup: 2001
serviceAccountName: kubernetes-dashboard
nodeSelector:
"kubernetes.io/os": linux
tolerations:
- key: node-role.kubernetes.io/master
effect: NoSchedule
volumes:
- name: tmp-volume
emptyDir: {}
  1. 安装
1
kubectl apply -f Kubernetes-dashboard.yaml
  1. 创建完之后可以查看对应的 SVC,就可以通过浏览器访问了

dashboard一

dashboard二

  1. RBAC 授权
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 创建ServiceAccount认证
apiVersion: v1
kind: ServiceAccount
metadata:
name: dashboard-admin
namespace: kubernetes-dashboard

---

# 创建ClusterRoleBinding将dashboard-admin与集群角色cluster-admin进行绑定
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: dashboardadmin-rbac
subjects:
- kind: ServiceAccount
name: dashboard-admin
namespace: kubernetes-dashboard
roleRef:
apiGroup: ""
kind: ClusterRole
name: cluster-admin
  1. 获取令牌并填入
1
kubectl describe secret dashboard-admin -n kubernetes-dashboard

dashboard三

9.1.2 Kubernetes Dashboard使用

  1. 创建一个简单的 Pod

dashboard四

  1. 查看 Pod 状态

dashboard五

9.2 资源监控——Prometheus和Grafana

9.2.1 安装Prometheus

  1. 进行 RBAC 授权
1
kubectl apply -f https://github.com/realdigit/PrometheusAndGrafanaForK8S/blob/master/prometheus.rbac.yml
  1. 配置 ConfigMap
1
kubectl apply -f https://github.com/realdigit/PrometheusAndGrafanaForK8S/blob/master/prometheus.configmap.yml
  1. 配置 Deployment
1
kubectl apply -f https://github.com/realdigit/PrometheusAndGrafanaForK8S/blob/master/prometheus.deployment.yml
  1. 配置 SVC
1
kubectl apply -f https://github.com/realdigit/PrometheusAndGrafanaForK8S/blob/master/prometheus.service.yml

9.2.2 安装Grafana

  1. 配置 Deployment
1
kubectl apply -f https://github.com/realdigit/PrometheusAndGrafanaForK8S/blob/master/grafana.deployment.yml
  1. 配置 SVC
1
kubectl apply -f https://github.com/realdigit/PrometheusAndGrafanaForK8S/blob/master/grafana.service.yml

9.2.3 Prometheus和Grafana的使用

  1. 添加数据源

监控一

  1. 使用模板,这里使用代号为 315 的 k8s 监控模板

监控二

监控三

监控四

  1. 效果

监控五

9.3 日志管理——ElasticSearch、Fluentd、Kibana

k8s 推荐采用 ElasticSearch、Fluentd、Kibana(简称EFK)三者组合的方式,对集群的日志进行收集和查询,关系如下:

  • ElasticSearch 是一种搜索引擎,用于存储日志并进行查询。
  • Fluentd 用于将日志消息从 k8s 发送到 ElasticSearch。
  • Kibana 是一种图形界面,用于查询 ElasticSearch 中的日志。

日志架构

EFK 之间的交互如下:

  • 容器运行时会将日志输出到控制台,并以 ”-json.log“ 结尾将日志文件存放到 /var/lib/docker/containers 目录中,而 /var/log 是 linux 系统的日志。
  • 在各个 Node 上运行的 Fluentd 将是收集这些日志,并发送给 ElasticSearch。
  • Kibana 是直接与用户交互的界面,可以查询 ElasticSearch 中的日志。

9.3.1 安装EFK

  1. 配置命名空间
1
kubectl apply -f https://github.com/kubernetes/kubernetes/blob/master/cluster/addons/fluentd-elasticsearch/create-logging-namespace.yaml
  1. 配置 Fluentd 的 ConfigMap
1
kubectl apply -f https://github.com/kubernetes/kubernetes/blob/master/cluster/addons/fluentd-elasticsearch/fluentd-es-configmap.yaml
  1. 安装 Fluentd
1
kubectl apply -f https://github.com/kubernetes/kubernetes/blob/master/cluster/addons/fluentd-elasticsearch/fluentd-es-ds.yaml
  1. 安装 ElasticSearch
1
kubectl apply -f https://github.com/kubernetes/kubernetes/blob/master/cluster/addons/fluentd-elasticsearch/es-statefulset.yaml
  1. 配置 ElasticSearch 的 SVC
1
kubectl apply -f https://github.com/kubernetes/kubernetes/blob/master/cluster/addons/fluentd-elasticsearch/es-service.yaml
  1. 安装 Kibana
1
kubectl apply -f https://github.com/kubernetes/kubernetes/blob/master/cluster/addons/fluentd-elasticsearch/kibana-deployment.yaml
  1. 配置 Kibana 的 SVC
1
kubectl applt -f https://github.com/kubernetes/kubernetes/blob/master/cluster/addons/fluentd-elasticsearch/kibana-service.yaml
  1. 获取 Kibana 访问地址
1
kubectl cluster-info | grep Kibana

十、项目部署

10.1 无状态项目部署

Guestbook 是一个无状态的多层 Web 应用程序,是一个简单的留言板程序,包含一下三个部分,并拥有读写分离机制:

  1. 前端应用:Guestbook 留言板应用,将部署多个实例供用户访问。
  2. 后端存储(写):Redis 主应用,用于写入留言信息,只部署一个案例。
  3. 后端存储(读):Redis 从属应用,用域读取留言信息,将部署多个案例。

无状态服务架构

  1. 部署 Redis 主实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis-master-deployment
labels:
app: redis
spec:
selector:
matchLabels:
app: redis
role: master
tier: backend
replicas: 1
template:
metadata:
name: redis-master-pod
labels:
app: redis
role: master
tier: backend
spec:
containers:
- name: redis-master-container
image: daocloud.io/library/redis:latest
imagePullPolicy: IfNotPresent
resources:
requests:
cpu: 100m
memory: 100Mi
ports:
- containerPort: 6379
  1. 部署 Redis 主实例 SVC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
apiVersion: v1
kind: Service
metadata:
name: redis-master-service
labels:
app: redis
role: master
tier: backend
spec:
ports:
- port: 6379
targetPort: 6379
selector:
app: redis
role: master
tier: backend
  1. 部署 Redis 从属实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis-slave-deployment
labels:
app: redis
spec:
selector:
matchLabels:
app: redis
role: slave
tier: backend
replicas: 2
template:
metadata:
name: redis-slave-pod
labels:
app: redis
role: slave
tier: backend
spec:
containers:
- name: redis-slave-container
image: daocloud.io/library/redis:latest
imagePullPolicy: IfNotPresent
resources:
requests:
cpu: 100m
memory: 100Mi
env:
- name: GET_HOSTS_FROM
value: dns
ports:
- containerPort: 6379
  1. 部署 Redis 从属实例 SVC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
apiVersion: v1
kind: Service
metadata:
name: redis-slave-service
labels:
app: redis
role: slave
tier: backend
spec:
ports:
- port: 6379
targetPort: 6379
selector:
app: redis
  1. 部署 Guestbook
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
apiVersion: apps/v1
kind: Deployment
metadata:
name: guestbook-deployment
labels:
app: guestbook
spec:
selector:
matchLabels:
app: guestbook
tier: frontend
replicas: 3
template:
metadata:
name: guestbook-pod
labels:
app: guestbook
tier: frontend
spec:
containers:
- name: guestbook-container
image: kubeguide/guestbook-php-frontend
imagePullPolicy: IfNotPresent
resources:
requests:
cpu: 100m
memory: 100Mi
env:
- name: GET_HOSTS_FROM
value: dns
ports:
- containerPort: 80
  1. 部署 Guestbook 的 SVC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
apiVersion: v1
kind: Service
metadata:
name: guestbook-service
labels:
app: guestbook
tier: frontend
spec:
type: NodePort
selector:
app: guestbook
tier: frontend
ports:
- port: 80
nodePort: 30222

10.2 有状态项目部署

WordPress 是使用 PHP 开发的开源个人博客平台,是一套非常完善的内容管理系统,支持非常丰富的插件和模板,主要包含以下两个部分:

  • 前端应用:WordPress。
  • 后端应用:MySQL 数据库,使用 PVC 来存储博客的数据。

有状态服务架构

  1. 首先生成一个数据库密码
1
echo -n 'toortoor' | base64
  1. 创建一个 Secret 存放密码
1
2
3
4
5
6
7
apiVersion: v1
kind: Secret
metadata:
name: mysql-secret
type: Qpaque
data:
mysql_password: dG9vcnRvb3I=
  1. 部署 nfs-client,实现自动分配 PV 给 PVC,步骤参照 6.3.2
  2. 部署 MySQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
apiVersion: v1
kind: Service
metadata:
name: mysql-service
labels:
app: wordpress
spec:
selector:
app: wordpress
tier: mysql
ports:
- port: 3306
clusterIP: None

---

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mysql-pvc
labels:
app: wordpress
spec:
accessModes:
- ReadWriteOnce
storageClassName: nfs-storageclass
resources:
requests:
storage: 2Gi

---

apiVersion: apps/v1
kind: Deployment
metadata:
name: mysql-deployment
labels:
app: wordpress
spec:
replicas: 1
selector:
matchLabels:
app: wordpress
tier: mysql
strategy:
type: Recreate
template:
metadata:
name: mysql-pod
labels:
app: wordpress
tier: mysql
spec:
containers:
- name: mysql-container
image: daocloud.io/library/mysql:5.7
imagePullPolicy: IfNotPresent
ports:
- containerPort: 3306
env:
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-secret
key: mysql_password
volumeMounts:
- name: mysql-datadir
mountPath: /var/lib/mysql
volumes:
- name: mysql-datadir
persistentVolumeClaim:
claimName: mysql-pvc
  1. 部署 WordPress
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
apiVersion: v1
kind: Service
metadata:
name: wordpress-service
labels:
app: wordpress
spec:
ports:
- port: 80
nodePort: 30111
selector:
app: wordpress
tier: frontend
type: NodePort

---

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: wordpress-pvc
labels:
app: wordpress
spec:
storageClassName: nfs-storageclass
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 2Gi

---

apiVersion: apps/v1
kind: Deployment
metadata:
name: wordpress-deployment
labels:
app: wordpress
tier: frontend
spec:
selector:
matchLabels:
app: wordpress
tier: frontend
strategy:
type: Recreate
template:
metadata:
name: wordpress-pod
labels:
app: wordpress
tier: frontend
spec:
containers:
- name: wordpress-container
image: daocloud.io/daocloud/dao-wordpress:latest
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80
name: wordpress
# 这里用到了MySQL的变量参数
env:
# Mysql的SVC名称
- name: WORDPRESS_DB_HOST
value: mysql-service
- name: WORDPRESS_DB_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-secret
key: mysql_password
volumeMounts:
- name: wordpress-datadir
mountPath: /var/www/html
volumes:
- name: wordpress-datadir
persistentVolumeClaim:
claimName: wordpress-pvc
  1. 部署 Ingress,通过 www.cqm.com:30111 就能访问 WordPress
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: wordpress-ingress-nginx
labels:
app: wordpress
spec:
rules:
- host: www.cqm.com
http:
paths:
- path: /var/www/html
pathType: ImplementationSpecific
backend:
service:
name: wordpress-service
port:
number: 30111
  1. 测试

有状态服务一

10.3 使用Helm部署项目

Helm 是 k8s 的一个子项目,是一种 k8s 包管理平台,它能够定义、部署、升级非常复杂的 k8s 应用集合,并进行版本管理。

Helm架构

  • Helm 客户端:是一种远程命令客户端工具,主要用于 Chart 文件的创建、打包和发布部署,以及和 Chart 仓库的管理。Helm 发出的请求,根据 Chart 结构生成发布对象,并将 Chart 解析成各个 k8s 资源的实际部署文件,供 k8s 创建相应资源,同时还提供发布对象的更新、回滚、统一删除等功能。
  • Chart:是应用程序的部署定义,包含各种 yaml 文件,可以采用 TAR 格式打包。
  • Chart 仓库:Helm 中存放了各种应用程序的 Chart 包以供用户下载,Helm 可以同时管理多个 Chart 仓库,默认情况下管理一个本地仓库和一个远程仓库。
  • 发布对象:在 k8s 集群中部署的 Chart 称为发布对象,Chart 和发布对象的关系类似于镜像和容器,前者是部署的定义,后者是实际部署好的应用程序。

10.3.1 Helm安装

  1. 安装
1
2
3
wget https://get.helm.sh/helm-v3.6.2-linux-amd64.tar.gz
tar -xf helm-v3.6.2-linux-amd64.tar.gz
cp linux-amd64/helm /usr/local/bin/
  1. 设置环境变量 KUBECONFIG 来指定存有 ApiServre 的地址与 token 的配置文件地址,默认为~/.kube/config
1
export KUBECONFIG=/root/.kube/config
  1. 配置 Helm 仓库
1
2
helm repo add stable https://kubernetes.oss-cn-hangzhou.aliyuncs.com/charts
helm repo add incubator https://aliacs-app-catalog.oss-cn-hangzhou.aliyuncs.com/charts-incubator/

10.3.2 Helm Chart的基本操作

Chart 的创建

  1. 创建 Chart
1
helm create chart名称
  1. 执行后会在当前目录下创建一个文件,可用 tree 命令查看该目录结构

Helm一

  • chars:存放该 Chart 以来的所有子 Chart 的目录,这些子 Chart 也拥有这 4 个部分,如果有子 Chart ,则需要在父 Chart 创建一个 requirements.yaml 文件,在文件中记录这些子 Chart。
  • Chart.yaml:记录该 Chart 的相关信息,并定义在文件中的 .Chart 开头的属性值。
  • template:存放了 k8s 部署文件的 Helm 模板,其中扩展了 Go Template 语法。
  • values.yaml:定义在文件中的 .Values 开头的属性值。
  • _helpers.tpl:它是一个i模板助手文件,用于定义通用信息,然后在其他地方使用。
  • NOTES.txt:会在 Chart 部署命令后,代入具体的参数值,产生说明信息。
  • test-connection.yaml:用于定义部署完成后需要执行的测试内容,以便测试是否部署成功。

Chart 的验证

  1. 在发布 Chart 之前,可以通过命令检查 Chart 文件是否有误,如下
1
helm lint chart目录/
  1. 同时也可以用命令将各项值组合为 k8s 的 yaml 文件,查看是否为预期内容,比如下图就是其中 Deployment 的内容
1
helm install --dry-run --debug 发布名称 -name chart目录名称

Helm二

Chart 的发布

  1. Chart 的发布命令如下
1
helm install 发布版本名称 chart文件目录
  1. 查看目前发布的版本
1
helm list

将 Chart 打包到仓库中

  1. 查看仓库命令
1
helm repo list
  1. 查看仓库中的包
1
2
3
helm search repo/hub
# 也可以在后面加上应用名,如
helm search repo/hub nginx
  1. 首先打包
1
helm package chart目录
  1. 安装 Push 插件
1
helm plugin install https://github.com/chartmuseum/helm-push.git
  1. 上传
1
helm push tar包名 仓库

发布版本的更新、回滚和删除

  1. 更新
1
helm upgrade 发布版本名称 chart目录或tar flags
  1. 查看历史版本
1
helm history 发布版本名称
  1. 回滚
1
helm rollback 发布版本名称 版本号
  1. 删除
1
helm delete 发布版本名称