Kubernetes进阶实践
本章介绍Kubernetes的进阶内容,包含Kubernetes集群调度、CNI插件、认证授权安全体系、分布式存储的对接、Helm的使用等,让学员可以更加深入的学习Kubernetes的核心内容。
-
ETCD数据的访问
-
kube-scheduler调度策略实践
- 预选与优选流程
- 生产中常用的调度配置实践
-
k8s集群网络模型
- CNI介绍及集群网络选型
- Flannel网络模型的实现
- vxlan Backend
- hostgw Backend
-
集群认证与授权
- APIServer安全控制模型
- Kubectl的认证授权
- RBAC
- kubelet的认证授权
- Service Account
-
使用Helm管理复杂应用的部署
- Helm工作原理详解
- Helm的模板开发
- 实战:使用Helm部署Harbor仓库
-
kubernetes对接分部式存储
-
pv、pvc介绍
-
k8s集群如何使用cephfs作为分布式存储后端
-
利用storageClass实现动态存储卷的管理
-
实战:使用分部署存储实现有状态应用的部署
-
-
本章知识梳理及回顾
ETCD常用操作
拷贝etcdctl命令行工具:
$ docker exec -ti etcd_container which etcdctl
$ docker cp etcd_container:/usr/local/bin/etcdctl /usr/bin/etcdctl
查看etcd集群的成员节点:
$ export ETCDCTL_API=3
$ etcdctl --endpoints=https://[127.0.0.1]:2379 --cacert=/etc/kubernetes/pki/etcd/ca.crt --cert=/etc/kubernetes/pki/etcd/healthcheck-client.crt --key=/etc/kubernetes/pki/etcd/healthcheck-client.key member list -w table
$ alias etcdctl='etcdctl --endpoints=https://[127.0.0.1]:2379 --cacert=/etc/kubernetes/pki/etcd/ca.crt --cert=/etc/kubernetes/pki/etcd/healthcheck-client.crt --key=/etc/kubernetes/pki/etcd/healthcheck-client.key'
$ etcdctl member list -w table
查看etcd集群节点状态:
$ etcdctl endpoint status -w table
$ etcdctl endpoint health -w table
设置key值:
$ etcdctl put luffy 1
$ etcdctl get luffy
查看所有key值:
$ etcdctl get / --prefix --keys-only
查看具体的key对应的数据:
$ etcdctl get /registry/pods/jenkins/sonar-postgres-7fc5d748b6-gtmsb
添加定时任务做数据快照(重要!)
$ etcdctl snapshot save `hostname`-etcd_`date +%Y%m%d%H%M`.db
恢复快照:
-
停止etcd和apiserver
-
移走当前数据目录
$ mv /var/lib/etcd/ /tmp
-
恢复快照
$ etcdctl snapshot restore `hostname`-etcd_`date +%Y%m%d%H%M`.db --data-dir=/var/lib/etcd/
-
集群恢复
https://github.com/etcd-io/etcd/blob/master/Documentation/op-guide/recovery.md
Kubernetes调度
为何要控制Pod应该如何调度
- 集群中有些机器的配置高(SSD,更好的内存等),我们希望核心的服务(比如说数据库)运行在上面
- 某两个服务的网络传输很频繁,我们希望它们最好在同一台机器上
- …
Kubernetes Scheduler 的作用是将待调度的 Pod 按照一定的调度算法和策略绑定到集群中一个合适的 Worker Node 上,并将绑定信息写入到 etcd 中,之后目标 Node 中 kubelet 服务通过 API Server 监听到 Scheduler 产生的 Pod 绑定事件获取 Pod 信息,然后下载镜像启动容器。
调度的过程
Scheduler 提供的调度流程分为预选 (Predicates) 和优选 (Priorities) 两个步骤:
- 预选,K8S会遍历当前集群中的所有 Node,筛选出其中符合要求的 Node 作为候选
- 优选,K8S将对候选的 Node 进行打分
经过预选筛选和优选打分之后,K8S选择分数最高的 Node 来运行 Pod,如果最终有多个 Node 的分数最高,那么 Scheduler 将从当中随机选择一个 Node 来运行 Pod。
预选:
优选:
Cordon
$ kubectl cordon k8s-slave2
$ kubectl drain k8s-slave2
NodeSelector
label
是kubernetes
中一个非常重要的概念,用户可以非常灵活的利用 label 来管理集群中的资源,POD 的调度可以根据节点的 label 进行特定的部署。
查看节点的label:
$ kubectl get nodes --show-labels
为节点打label:
$ kubectl label node k8s-master disktype=ssd
当 node 被打上了相关标签后,在调度的时候就可以使用这些标签了,只需要在spec 字段中添加nodeSelector
字段,里面是我们需要被调度的节点的 label。
...
spec:
hostNetwork: true # 声明pod的网络模式为host模式,效果通docker run --net=host
volumes:
- name: mysql-data
hostPath:
path: /opt/mysql/data
nodeSelector: # 使用节点选择器将Pod调度到指定label的节点
component: mysql
containers:
- name: mysql
image: 192.168.136.10:5000/demo/mysql:5.7
...
nodeAffinity
节点亲和性 , 比上面的nodeSelector
更加灵活,它可以进行一些简单的逻辑组合,不只是简单的相等匹配 。分为两种,硬策略和软策略。
requiredDuringSchedulingIgnoredDuringExecution : 硬策略,如果没有满足条件的节点的话,就不断重试直到满足条件为止,简单说就是你必须满足我的要求,不然我就不会调度Pod。
preferredDuringSchedulingIgnoredDuringExecution:软策略,如果你没有满足调度要求的节点的话,Pod就会忽略这条规则,继续完成调度过程,说白了就是满足条件最好了,没有满足就忽略掉的策略。
#要求 Pod 不能运行在128和132两个节点上,如果有节点满足disktype=ssd或者sas的话就优先调度到这类节点上
...
spec:
containers:
- name: demo
image: 192.168.136.10:5000/demo/myblog:v1
ports:
- containerPort: 8002
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: NotIn
values:
- 172.21.51.698
- 192.168.136.132
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 1
preference:
matchExpressions:
- key: disktype
operator: In
values:
- ssd
- sas
...
这里的匹配逻辑是 label 的值在某个列表中,现在Kubernetes
提供的操作符有下面的几种:
- In:label 的值在某个列表中
- NotIn:label 的值不在某个列表中
- Gt:label 的值大于某个值
- Lt:label 的值小于某个值
- Exists:某个 label 存在
- DoesNotExist:某个 label 不存在
如果nodeSelectorTerms下面有多个选项的话,满足任何一个条件就可以了;如果matchExpressions有多个选项的话,则必须同时满足这些条件才能正常调度 Pod
污点(Taints)与容忍(tolerations)
对于nodeAffinity
无论是硬策略还是软策略方式,都是调度 Pod 到预期节点上,而Taints
恰好与之相反,如果一个节点标记为 Taints ,除非 Pod 也被标识为可以容忍污点节点,否则该 Taints 节点不会被调度Pod。
Taints(污点)是Node的一个属性,设置了Taints(污点)后,因为有了污点,所以Kubernetes是不会将Pod调度到这个Node上的。于是Kubernetes就给Pod设置了个属性Tolerations(容忍),只要Pod能够容忍Node上的污点,那么Kubernetes就会忽略Node上的污点,就能够(不是必须)把Pod调度过去。
场景一:私有云服务中,某业务使用GPU进行大规模并行计算。为保证性能,希望确保该业务对服务器的专属性,避免将普通业务调度到部署GPU的服务器。
场景二:用户希望把 Master 节点保留给 Kubernetes 系统组件使用,或者把一组具有特殊资源预留给某些 Pod,则污点就很有用了,Pod 不会再被调度到 taint 标记过的节点。taint 标记节点举例如下:
设置污点:
$ kubectl taint node [node_name] key=value:[effect]
其中[effect] 可取值: [ NoSchedule | PreferNoSchedule | NoExecute ]
NoSchedule:一定不能被调度。
PreferNoSchedule:尽量不要调度。
NoExecute:不仅不会调度,还会驱逐Node上已有的Pod。
示例:kubectl taint node k8s-slave1 smoke=true:NoSchedule
去除污点:
去除指定key及其effect:
kubectl taint nodes [node_name] key:[effect]- #这里的key不用指定value
去除指定key所有的effect:
kubectl taint nodes node_name key-
示例:
kubectl taint node k8s-master smoke=true:NoSchedule
kubectl taint node k8s-master smoke:NoExecute-
kubectl taint node k8s-master smoke-
污点演示:
## 给k8s-slave1打上污点,smoke=true:NoSchedule
$ kubectl taint node k8s-slave1 smoke=true:NoSchedule
$ kubectl taint node k8s-slave2 drunk=true:NoSchedule
## 扩容myblog的Pod,观察新Pod的调度情况
$ kuebctl -n luffy scale deploy myblog --replicas=3
$ kubectl -n luffy get po -w ## pending
Pod容忍污点示例:myblog/deployment/deploy-myblog-taint.yaml
...
spec:
containers:
- name: demo
image: 192.168.136.10:5000/demo/myblog:v1
tolerations: #设置容忍性
- key: "smoke"
operator: "Equal" #如果操作符为Exists,那么value属性可省略,不指定operator,默认为Equal
value: "true"
effect: "NoSchedule"
- key: "drunk"
operator: "Exists" #如果操作符为Exists,那么value属性可省略,不指定operator,默认为Equal
#意思是这个Pod要容忍的有污点的Node的key是smoke Equal true,效果是NoSchedule,
#tolerations属性下各值必须使用引号,容忍的值都是设置Node的taints时给的值。
$ kubectl apply -f deploy-myblog-taint.yaml
spec:
containers:
- name: demo
image: 192.168.136.10:5000/demo/myblog
tolerations:
- operator: "Exists"
验证NoExecute效果
Kubernetes集群的网络实现
CNI介绍及集群网络选型,
CSI
容器网络接口(Container Network Interface),实现kubernetes集群的Pod网络通信及管理。包括:
- CNI Plugin负责给容器配置网络,它包括两个基本的接口:
配置网络: AddNetwork(net NetworkConfig, rt RuntimeConf) (types.Result, error)
清理网络: DelNetwork(net NetworkConfig, rt RuntimeConf) error - IPAM Plugin负责给容器分配IP地址,主要实现包括host-local和dhcp。
以上两种插件的支持,使得k8s的网络可以支持各式各样的管理模式,当前在业界也出现了大量的支持方案,其中比较流行的比如flannel、calico等。
kubernetes配置了cni网络插件后,其容器网络创建流程为:
- kubelet先创建pause容器生成对应的network namespace
- 调用网络driver,因为配置的是CNI,所以会调用CNI相关代码,识别CNI的配置目录为/etc/cni/net.d
- CNI driver根据配置调用具体的CNI插件,二进制调用,可执行文件目录为/opt/cni/bin,项目
- CNI插件给pause容器配置正确的网络,pod中其他的容器都是用pause的网络
可以在此查看社区中的CNI实现,https://github.com/containernetworking/cni
通用类型:flannel、calico等,部署使用简单
其他:根据具体的网络环境及网络需求选择,比如
- 公有云机器,可以选择厂商与网络插件的定制Backend,如AWS、阿里、腾讯针对flannel均有自己的插件,也有AWS ECS CNI
- 私有云厂商,比如Vmware NSX-T等
- 网络性能等,MacVlan
Flannel网络模型实现剖析
flannel实现overlay,underlay网络通常有多种实现:
- udp
- vxlan
- host-gw
- …
不特殊指定的话,默认会使用vxlan技术作为Backend,可以通过如下查看:
$ kubectl -n kube-system exec kube-flannel-ds-amd64-cb7hs cat /etc/kube-flannel/net-conf.json
{
"Network": "10.244.0.0/16",
"Backend": {
"Type": "vxlan"
}
}
vxlan介绍及点对点通信的实现
VXLAN 全称是虚拟可扩展的局域网( Virtual eXtensible Local Area Network),它是一种 overlay 技术,通过三层的网络来搭建虚拟的二层网络。
它创建在原来的 IP 网络(三层)上,只要是三层可达(能够通过 IP 互相通信)的网络就能部署 vxlan。在每个端点上都有一个 vtep 负责 vxlan 协议报文的封包和解包,也就是在虚拟报文上封装 vtep 通信的报文头部。物理网络上可以创建多个 vxlan 网络,这些 vxlan 网络可以认为是一个隧道,不同节点的虚拟机能够通过隧道直连。每个 vxlan 网络由唯一的 VNI 标识,不同的 vxlan 可以不相互影响。
- VTEP(VXLAN Tunnel Endpoints):vxlan 网络的边缘设备,用来进行 vxlan 报文的处理(封包和解包)。vtep 可以是网络设备(比如交换机),也可以是一台机器(比如虚拟化集群中的宿主机)
- VNI(VXLAN Network Identifier):VNI 是每个 vxlan 的标识,一共有 2^24 = 16,777,216,一般每个 VNI 对应一个租户,也就是说使用 vxlan 搭建的公有云可以理论上可以支撑千万级别的租户
演示:在k8s-slave1和k8s-slave2两台机器间,利用vxlan的点对点能力,实现虚拟二层网络的通信
k8s-slave1节点:
# 创建vTEP设备,对端指向k8s-slave2节点,指定VNI及underlay网络使用的网卡
$ ip link add vxlan20 type vxlan id 20 remote 172.21.51.69 dstport 4789 dev eth0
$ ip -d link show vxlan20
# 启动设备
$ ip link set vxlan20 up
# 设置ip地址
ip addr add 10.0.136.11/24 dev vxlan20
k8s-slave2节点:
# 创建VTEP设备,对端指向k8s-slave1节点,指定VNI及underlay网络使用的网卡
$ ip link add vxlan20 type vxlan id 20 remote 172.21.51.68 dstport 4789 dev eth0
# 启动设备
$ ip link set vxlan20 up
# 设置ip地址
$ ip addr add 10.0.136.12/24 dev vxlan20
在k8s-slave1节点:
$ ping 10.0.136.12
隧道是一个逻辑上的概念,在 vxlan 模型中并没有具体的物理实体想对应。隧道可以看做是一种虚拟通道,vxlan 通信双方(图中的虚拟机)认为自己是在直接通信,并不知道底层网络的存在。从整体来说,每个 vxlan 网络像是为通信的虚拟机搭建了一个单独的通信通道,也就是隧道。
实现的过程:
虚拟机的报文通过 vtep 添加上 vxlan 以及外部的报文层,然后发送出去,对方 vtep 收到之后拆除 vxlan 头部然后根据 VNI 把原始报文发送到目的虚拟机。
# 查看k8s-slave1主机路由
$ route -n
10.0.136.0 0.0.0.0 255.255.255.0 U 0 0 0 vxlan20
# 到了vxlan的设备后,
$ ip -d link show vxlan20
vxlan id 20 remote 172.21.51.69 dev eth0 srcport 0 0 dstport 4789 ...
# 查看fdb地址表,主要由MAC地址、VLAN号、端口号和一些标志域等信息组成,vtep 对端地址为 172.21.51.69,换句话说,如果接收到的报文添加上 vxlan 头部之后都会发到 172.21.51.69
$ bridge fdb show|grep vxlan20
00:00:00:00:00:00 dev vxlan20 dst 172.21.51.69 via eth0 self permanent
在k8s-slave2机器抓包,查看vxlan封装后的包:
# 在k8s-slave2机器执行
$ tcpdump -i eth0 host 172.21.51.68 -w vxlan.cap
# 在k8s-slave1机器执行
$ ping 10.0.136.12
使用wireshark分析ICMP类型的数据包
跨主机容器网络的通信
思考:容器网络模式下,vxlan设备该接在哪里?
基本的保证:目的容器的流量要通过vtep设备进行转发!
演示:利用vxlan实现跨主机容器网络通信
为了不影响已有的网络,因此创建一个新的网桥,创建容器接入到新的网桥来演示效果
在k8s-slave1节点:
$ docker network ls
# 创建新网桥,指定cidr段
$ docker network create --subnet 172.18.0.0/16 network-luffy
$ docker network ls
# 新建容器,接入到新网桥
$ docker run -d --name vxlan-test --net network-luffy --ip 172.18.0.2 nginx:alpine
$ docker exec vxlan-test ifconfig
$ brctl show network-luffy
在k8s-slave2节点:
# 创建新网桥,指定cidr段
$ docker network create --subnet 172.18.0.0/16 network-luffy
# 新建容器,接入到新网桥
$ docker run -d --name vxlan-test --net network-luffy --ip 172.18.0.3 nginx:alpine
此时执行ping测试:
$ docker exec vxlan-test ping 172.18.0.3
分析:数据到了网桥后,出不去。结合前面的示例,因此应该将流量由vtep设备转发,联想到网桥的特性,接入到桥中的端口,会由网桥负责转发数据,因此,相当于所有容器发出的数据都会经过到vxlan的端口,vxlan将流量转到对端的vtep端点,再次由网桥负责转到容器中。
k8s-slave1节点:
# 删除旧的vtep
$ ip link del vxlan20
# 新建vtep
$ ip link add vxlan_docker type vxlan id 100 remote 172.21.51.69 dstport 4789 dev eth0
$ ip link set vxlan_docker up
# 不用设置ip,因为目标是可以转发容器的数据即可
# 接入到网桥中
$ brctl addif br-904603a72dcd vxlan_docker
k8s-slave2节点:
# 删除旧的vtep
$ ip link del vxlan20
# 新建vtep
$ ip link add vxlan_docker type vxlan id 100 remote 172.21.51.68 dstport 4789 dev eth0
$ ip link set vxlan_docker up
# 不用设置ip,因为目标是可以转发容器的数据即可
# 接入到网桥中
$ brctl addif br-c6660fe2dc53 vxlan_docker
再次执行ping测试:
$ docker exec vxlan-test ping 172.18.0.3
Flannel的vxlan实现精讲
思考:k8s集群的网络环境和手动实现的跨主机的容器通信有哪些差别?
- CNI要求,集群中的每个Pod都必须分配唯一的Pod IP
- k8s集群内的通信不是vxlan点对点通信,因为集群内的所有节点之间都需要互联
- 没法创建点对点的vxlan模型
flannel如何为每个节点分配Pod地址段:
$ kubectl -n kube-system exec kube-flannel-ds-amd64-cb7hs cat /etc/kube-flannel/net-conf.json
{
"Network": "10.244.0.0/16",
"Backend": {
"Type": "vxlan"
}
}
#查看节点的pod ip
[root@k8s-master bin]# kd get po -o wide
NAME READY STATUS RESTARTS AGE IP NODE
myblog-5d9ff54d4b-4rftt 1/1 Running 1 33h 10.244.2.19 k8s-slave2
myblog-5d9ff54d4b-n447p 1/1 Running 1 33h 10.244.1.32 k8s-slave1
#查看k8s-slave1主机分配的地址段
$ cat /run/flannel/subnet.env
FLANNEL_NETWORK=10.244.0.0/16
FLANNEL_SUBNET=10.244.1.1/24
FLANNEL_MTU=1450
FLANNEL_IPMASQ=true
# kubelet启动容器的时候就可以按照本机的网段配置来为pod设置IP地址
vtep的设备在哪:
$ ip -d link show flannel.1
# 没有remote ip,非点对点
Pod的流量如何转到vtep设备中
$ brctl show cni0
# 每个Pod都会使用Veth pair来实现流量转到cni0网桥
$ route -n
10.244.0.0 10.244.0.0 255.255.255.0 UG 0 0 0 flannel.1
10.244.1.0 0.0.0.0 255.255.255.0 U 0 0 0 cni0
10.244.2.0 10.244.2.0 255.255.255.0 UG 0 0 0 flannel.1
vtep封包的时候,如何拿到目的vetp端的IP及MAC信息
# flanneld启动的时候会需要配置--iface=eth0,通过该配置可以将网卡的ip及Mac信息存储到ETCD中,
# 这样,flannel就知道所有的节点分配的IP段及vtep设备的IP和MAC信息,而且所有节点的flanneld都可以感知到节点的添加和删除操作,就可以动态的更新本机的转发配置
演示跨主机Pod通信的流量详细过程:
$ kubectl -n luffy get po -o wide
myblog-5d9ff54d4b-4rftt 1/1 Running 1 25h 10.244.2.19 k8s-slave2
myblog-5d9ff54d4b-n447p 1/1 Running 1 25h 10.244.1.32 k8s-slave1
$ kubectl -n luffy exec myblog-5d9ff54d4b-n447p -- ping 10.244.2.19 -c 2
PING 10.244.2.19 (10.244.2.19) 56(84) bytes of data.
64 bytes from 10.244.2.19: icmp_seq=1 ttl=62 time=0.480 ms
64 bytes from 10.244.2.19: icmp_seq=2 ttl=62 time=1.44 ms
--- 10.244.2.19 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1001ms
rtt min/avg/max/mdev = 0.480/0.961/1.443/0.482 ms
# 查看路由
$ kubectl -n luffy exec myblog-5d9ff54d4b-n447p -- route -n
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
0.0.0.0 10.244.1.1 0.0.0.0 UG 0 0 0 eth0
10.244.0.0 10.244.1.1 255.255.0.0 UG 0 0 0 eth0
10.244.1.0 0.0.0.0 255.255.255.0 U 0 0 0 eth0
# 查看k8s-slave1 的veth pair 和网桥
$ brctl show
bridge name bridge id STP enabled interfaces
cni0 8000.6a9a0b341d88 no veth048cc253
veth76f8e4ce
vetha4c972e1
# 流量到了cni0后,查看slave1节点的route
$ route -n
Destination Gateway Genmask Flags Metric Ref Use Iface
0.0.0.0 192.168.136.2 0.0.0.0 UG 100 0 0 eth0
10.0.136.0 0.0.0.0 255.255.255.0 U 0 0 0 vxlan20
10.244.0.0 10.244.0.0 255.255.255.0 UG 0 0 0 flannel.1
10.244.1.0 0.0.0.0 255.255.255.0 U 0 0 0 cni0
10.244.2.0 10.244.2.0 255.255.255.0 UG 0 0 0 flannel.1
172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 docker0
192.168.136.0 0.0.0.0 255.255.255.0 U 100 0 0 eth0
# 流量转发到了flannel.1网卡,查看该网卡,其实是vtep设备
$ ip -d link show flannel.1
4: flannel.1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UNKNOWN mode DEFAULT group default
link/ether 8a:2a:89:4d:b0:31 brd ff:ff:ff:ff:ff:ff promiscuity 0
vxlan id 1 local 172.21.51.68 dev eth0 srcport 0 0 dstport 8472 nolearning ageing 300 noudpcsum noudp6zerocsumtx noudp6zerocsumrx addrgenmode eui64 numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535
# 该转发到哪里,通过etcd查询数据,然后本地缓存,流量不用走多播发送
$ bridge fdb show dev flannel.1
a6:64:a0:a5:83:55 dst 192.168.136.10 self permanent
86:c2:ad:4e:47:20 dst 172.21.51.69 self permanent
# 对端的vtep设备接收到请求后做解包,取出源payload内容,查看k8s-slave2的路由
$ route -n
Destination Gateway Genmask Flags Metric Ref Use Iface
0.0.0.0 192.168.136.2 0.0.0.0 UG 100 0 0 eth0
10.0.136.0 0.0.0.0 255.255.255.0 U 0 0 0 vxlan20
10.244.0.0 10.244.0.0 255.255.255.0 UG 0 0 0 flannel.1
10.244.1.0 10.244.1.0 255.255.255.0 UG 0 0 0 flannel.1
10.244.2.0 0.0.0.0 255.255.255.0 U 0 0 0 cni0
172.17.0.0 0.0.0.0 255.255