之前我们解决了跨主机间容器间通信的问题,但是这也只能说我们铺好了路,村里通路了,但是其实作为 k8s 来说,还有好多其他的问题等待着我们解决。今天我们就通过这些问题来看看 k8s 的 CNI 的设计。CNI 到底究竟是个什么东西,到底是不是和你想的一样那么困难。
我们知道 k8s 整个集群里面有许多的 pod 那么 IP 怎么分配呢?总不能分配着之后出现 IP 冲突了吧。k8s 集群里面是不是能不有一个类似 DHCP 的东西来管这个 IP 地址分配呢?
当流量打到宿主机上时,应该有一个什么设备来快速将请求转到对应的 pod 才对吧?那么谁来做这个事情呢?
那为了解决上面的问题,我们一步步出发。
首先有关 k8s 的网络模型,官网有下面的描述:(https://kubernetes.io/zh/docs/concepts/cluster-administration/networking/)
备注:仅针对那些支持 Pods
在主机网络中运行的平台(比如:Linux):
也就是说所谓的 cni 实现必须满足这样的网络模型才可以,那么 CNI 究竟要做啥呢?
要说清楚 CNI 那就得从 pod 的创建的具体步骤来说了:
我们知道了 CNI 要做的事情,以及 CNI 在模型中所处的位置,那么它究竟是什么呢?
CNI 全称 Container Networking Interface 容器网络接口,它其实就是一个接口,抽象了 k8s 网络操作的实现。
那么接口是什么样子的呢?
AddNetwork(net *NetworkConfig, rt* RuntimeConf)(types.Result, error)
创建网络DelNetwork(net *NetworkConfig, rt* RuntimeConf)
删除网络其中 ADD 操作的含义是:把容器添加到 CNI 网络里;DEL 操作的含义则是:把容器从 CNI 网络里移除掉。
而对于网桥类型的 CNI 插件来说,这两个操作意味着把容器以 Veth Pair 的方式“插”到 CNI 网桥上,或者从网桥上“拔”掉。
我们以 flannel 插件为例,部署起来其实非常的方便,就只需要
kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml
就可以了
---
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
........................................................................................
....................................................................
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
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
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
我省略了其中有关 rabc 相关的资源,其实最重要的就是两个
所以现在 flannel 的部署使用是非常的简单了
那为什么 k8s 不自己搞个方案让我们用就好了,非要设计成接口让我们自己找方案呢?
很简单,因为各有所需。下面举例两个方案
我们知道 flannel 即使用了 vxlan 虽然比 udp 好了不少,但是还是存在瓶颈,因为你必须有一个封包和拆包的过程,而 host-gw 就是为了优化这个问题而来的。
host gateway 顾名思义就是拿宿主机作为网关,所以它的原理其实非常简单:
重点来了,其实在 host-gw 模式下,需要在宿主机上维护一个路由表,flannel 此时就是不断的监听 etcd 中对应子网的变化,将对应子网的下一跳写到对应的路由表中即可。
因为使用路由表下一跳来设置的时候目标的时候是根据 mac 地址来找的,也就是设定的是下一跳宿主机的 mac 地址,而 mac 地址在二层网络是管用的,所以 host-gw 模式必须要求集群宿主机之间是二层连通的
实际中经常会出现两个宿主机在不同的 vlan 下,或者在不同的机房等等可能。
Calico 是一个基于 BGP 的纯三层的数据中心网络方案(BGP 就是在大规模网络中实现节点路由信息共享的一种协议。)题外话:说实话 BGP 这个词在大学学计算机网络的时候你应该听过,我对它的印象也是源于此。下面这张图是 Calico 官网找的架构图,我找资料的时候发现显然最新的 Calico 已经多了很多东西了 (https://docs.projectcalico.org/reference/architecture/overview)
flannel 的 host-gw 模式是会在宿主机上维护一个路由表,那么讲道理来说,如果能有一个路由器来代替掉这个路由表的功能其实就可以了?对,其实很简单,Calico 的 BGP 简单的说就是在本机上模拟了一个类似路由器的功能来实现的。
它有几个重要的组件
Felix: 是一个 DaemonSet ,负责刷新主机路由规则和 ACL 规则等
BgpClient:读取 Felix 编写的路由信息,将这些路由信息分发到集群的其他工作节点上
Bgp Route Reflector:路由器反射器,简单来说,在网络规模大的时候如果单台机器就要维护全网的路由信息太难了,所以中间加入了 Route Reflector 协助去管理网络,BGP Client 只需要连接它就可以了
由于 Calico 是一种纯三层的实现,因此可以避免与二层方案相关的数据包封装的操作,中间没有任何的 NAT,没有任何的overlay,所以它的转发效率可能是所有方案中最高的,因为它的包直接走原生TCP/IP的协议栈,它的隔离也因为这个栈而变得好做。因为TCP/IP的协议栈提供了一整套的防火墙的规则,所以它可以通过IPTABLES的规则达到比较复杂的隔离逻辑。
其次它不会在宿主机上创建任何网桥设备,Calico 的 CNI 插件会为每个容器设置一个 Veth Pair 设备,然后把其中的一端放置在宿主机上(它的名字以 cali 前缀开头)
这次懒了,不想自己画了,网上找了一个,说一下链路吧
总的来说,Calico 完全是利用了路由规则去实现的组网,利用宿主机协议栈去确保容器之间跨主机的连通性,没有 overlay,没有 NAT,相对的转发效率也比较高。
当然 k8s 的 CNI 实现还有很多方案,各个网络方案都有自己的特点,而我们更多的时候选择一个合适的 flannel 或许就可以了,而关键在于我们需要明白它究竟帮助我们做了什么事情。网络这个东西,很多时候并不只是通就可以,还有很多性能、安全…的需求,不同的需求需要不同的网络方案去实现,而这也就是为什么 k8s 将设计 CNI 的原因,将网络的实现方案抽象,从而满足不同的使用场景。
https://docs.projectcalico.org/reference/architecture/overview
https://k-grundy.medium.com/project-calico-kubernetes-integration-overview-a3a860cd974e