第24章: kubernetes集群网络
4.1 网络基础知识
1 公司网络架构
(1) 路由器: 网络出口 (2) 核心层: 主要完成数据高效转发、链路备份等 (3) 汇聚层: 网络策略、安全、工作站交换机的接入、VLAN之间通信等功能 (4) 接入层: 工作站的接入
什么是局域网: 所谓的局域网就是同一路由器或交换机下面连接的网段,通熟易懂的解释就是在一个路由器或交换机下面连接的多个计算机串联组,这些 计算机串联组就构成了一个局域网环境,局域网的稳定性非常好,局域网有一定的专业性和安全性,所以,一般公司单位基本上都是采用 局域网串联计算机组来使用。局域网内主机怎么通信的,主机访问外网又是怎么通信的,想要搞懂这些问题得从交换机,路由器讲起。
2 交换技术
交换机工作在OSI参考模型的第二层,即数据链路层。交换机拥有一条高带宽的背部总线交换矩阵,在同一时间可进行多个端口对之间的数 据传输。
(1) 交换技术分为2层和3层
2层:主要用于小型局域网,仅支持在数据链路层转发数据,对工作站接入。 3层:三层交换技术的诞生,最初是为了解决广播域的问题,多年的发展,三层交换机已经成为构建中大型网络的主要力量。
(2) 广播域
交换机在转发数据时会先进行广播,这个广播可以发送的区域就是一个广播域。交换机之间对广播帧是透明的,所以交换机之间组成的网 络是一个广播域。路由器的一个接口下的网络是一个广播域,所以路由器可以隔离广播域。
(3) ARP(地址解析协议,在IPV6中用NDP替代)
发送这个广播帧是由ARP协议实现,ARP是通过IP地址获取物理地址的一个TCP/IP协议。
(4) 三层交换机
前面讲的二层交换机只工作在数据链路层,路由器则工作在网络层。而功能强大的三层交换机可同时工作在数据链路层和网络层,并根 据 MAC 地址或 IP 地址转发数据包。
(5) VLAN(Virtual Local Area Network)虚拟局域网
VLAN是一种将局域网设备从逻辑上划分成一个个网段。一个VLAN就是一个广播域,VLAN之间的通信是通过第3层的路由器来完成的。 VLAN应用非常广泛,基本上大部分网络项目都会划分vlan。VLAN的主要好处有: 分割广播域,减少广播风暴影响范围;提高网络安全 性,根据不同的部门、用途、应用划分不同网段。
3 路由技术
(1) 路由器主要分为两个端口类型,LAN口和WAN口 1)WAN口: 配置公网IP,接入到互联网,转发来自LAN口的IP数据包。 2)LAN口: 配置内网IP(网关),连接内部交换机。
(2) 路由器是连接两个或多个网络的硬件设备,将从端口上接收的数据包,根据数据包的目的地址智能转发出去,路由器的功能如下
1)路由 2)转发 3)隔离子网 4)隔离广播域
(3) 路由器是互联网的枢纽,是连接互联网中各个局域网、广域网的设备,相比交换机来说,路由器的数据转发很复杂,它会根据目 的地址给出一条最优的路径。那么路径信息的来源有两种,动态路由和静态路由。 1)静态路由: 指人工手动指定到目标主机的地址然后记录在路由表中,如果其中某个节点不可用则需要重新指定。 2)动态路由: 则是路由器根据动态路由协议自动计算出路径永久可用,能实时地适应网络结构的变化。
(4) 常用的动态路由协议 1)RIP (Routing Information Protocol ,路由信息协议) 2)OSPF (Open Shortest Path First,开放式最短路径优先) 3)BGP (Border Gateway Protocol,边界网关协议)
4 OSI七层模型
OSI(Open System Interconnection)是国际标准化组织(ISO)制定的一个用于计算机或通信系统间互联的标准体系,一般称为OSI参考 模型或七层模型。
层次 | 名称 | 功能 | 协议数据单元(PDU) | 常见协议 |
---|---|---|---|---|
7 | 应用层 | 为用户的应用程序提供网络服务,提供一个接口。 | 数据 | HTTP、FTP、Telnet |
6 | 表示层 | 数据格式转换、数据加密/解密 | 数据单元 | ASCII |
5 | 会话层 | 建立、管理和维护会话 | 数据单元 | SSH、RPC |
4 | 传输层 | 建立、管理和维护端到端的连接 | 段/报文 | TCP、UDP |
3 | 网络层 | IP选址及路由选择 | 分组/包 | IP、ICMP、RIP、OSPF |
2 | 数据链路层 | 硬件地址寻址,差错效验等。 | 帧 | ARP、WIFI |
1 | 物理层 | 利用物理传输介质提供物理连接,传送比特流。 | 比特流 | RJ45、RJ11 |
5 TCP/UDP协议
(1) TCP(Transmission Control Protocol,传输控制协议),面向连接协议,双方先建立可靠的连接,再发送数据。适用于传输数据量 大,可靠性要求高的应用场景。
(2) UDP(User Data Protocol,用户数据报协议),面向非连接协议,不与对方建立连接,直接将数据包发送给对方。适用于一次只传 输少量的数据,可靠性要求低的应用场景,相对TCP传输速度快。
6 补充
(1) 局域网的概念
1) 首先要搞清楚同一局域网不同IP网段的设置方法,从广义上讲,局域网可以分为物理上的和逻辑上的局域网段。
2) 物理局域网段是指,比如21台电脑主机的21条网线都连接在同一台24口的交换机上,该IP地址段范围192.168.50.1 - 192.168.50.21,这时
不考虑任何一台机器上关闭了ping服务的情况,应该是任意两台电脑之间都能够ping通的。
3) 逻辑局域网段是指,有些机器,比如说上面21台机器中的任意10台,虽然物理上都是在同一个交换机下,但是你仍然可以将这10台机器的IP地址段
强制设置成192.168.40.1 - 192.168.40.10。这个时候就属于逻辑上的局域网情况了。也就是说,这10台属于同一个局域网192.168.40.x,另
外11台机器属于另一个局域网192.168.50.x。这时候你在这10台机器上就ping不通另外11台机器的IP地址了。
4) 为了隔离网络广播包,划分vlan来隔离不同的逻辑局域网段。
(2) 两个ip如何通信
1) 如果目标IP地址是本地地址,就送回本地上层处理,根本不会发到网卡去。
2) 如果目标IP地址是同一网段中的其它地址,查ARP表,找到目标IP对应的MAC地址(如果ARP缓存里没有,发ARP请求去获取)。把MAC地址填写到报文
里发送出去,如果找不到对应的ARP项,就会发送失败。注意:报文是要靠MAC地址才能找到目标主机的。
3) 如果目标IP地址不在同一网段,将gateway的MAC地址作为目标MAC地址发送。
4) 获取目标设备的MAC地址时使用的是二层广播,和IP地址是否为同一个网段并没有任何关系,一旦得到了目标设备的MAC地址,有可能就在本地的arp
缓存中,就能进行数据链路层之间的通信了,因此,网关和IP地址不在同一个网段下是没有问题的。
5) ip层是第三层,mac层是第二层,低层为高层服务,数据包的传送过程就一个"打包-拆包"的过程。
6) 主机进行通信传送数据时,将应用数据封装成IP包,数据链路层封装成帧,根据MAC地址,发送数据。
(3) 同一广播域内,两台主机通信过程(二层)
1) N0与N1进行通信,N0知道N1的IP地址,不知MAC地址,N0发送ARP广播(目标MAC: ff:ff:ff:ff:ff:ff)请求给同一广播域中的所有成员
2) 当SWO从主机的接口上接收到这个广播包,读取帧源的MAC地址和目标MAC地址,将NO的MAC地址与之相对应的接口放入MAC地址表,从别的接口广播
这个数据帧,当别的主机收到这个广播时,查看目标IP不是自己的,就会丢弃此包。如果N1接收到这个数据帧,它检查目标IP和这个的IP是一样的,就
会回应这个ARP请求,把自己的IP和MAC封装成源IP和源MAC,N0的IP和N0的MAC地址为目标IP与目标MAC,并记录NO的MAC与IP,放进自己的ARP缓存
表中。
3) 这个应答包经过交换机SWO时,它又会检查源MAC、目标MAC,把N1的MAC和自己接口2放进MAC地址表中,再查看自己的MAC地址表,发现存在目标
MAC与自己的1接口对应(由于刚开始有记录过N0的MAC),那它就会直接把这个应答包从接口1送出去了。
4) 主机N0收到这个包后发现目标MAC是自己,就会处理这个包。并把N1的MAC与IP放进自己的ARP缓存表中。这时主机N0就知道N1的MAC地址了,以
后要发送数据,就直接把N1的IP与MAC封装进帧中进行点对点的发送。
5) 同一广播域中,包的源IP、目标IP,源MAC、目标MAC是真实的两台主机上的IP与MAC地址。
(4) 跨路由的数据传输过程(三层)
1) 当NO要和N2通信时,此时NO会检查N2的IP地址跟自己是否处在同一网段,两主机肯定不会是同一网段的,N2和自己处在不同网段,所以,N0会把
数据包发给它的网关,也就是R0上的F0/0接口,源IP和源MAC地址是N0自己的,目标IP是N2的,目标MAC是R0上接口F0/0的(如果N0不知道F0/0的
MAC,就会跟情景一相似,发个ARP广播来得到F0/0的MAC地址)。
2) 当这个数据包到达R0时,检查MAC地址,发现目标MAC地址是自己的,于是拆除二层报头,分析ip报头,路由器R0会查看目标IP的是否是自己,由
于目标不是自己,就会查看自己的路由表,找出到达N2网段的路由,如果没有相关条目,就直接丢弃。当查看路由表后发现到达N2网段的出接口是F0/1
,下一跳IP是R1的F0/1接口,于是把数据包转到F0/1接口上,再由接口F0/1传给R1的F0/1接口。这个过程,数据包的源IP是N0,源MAC是F0/1,目
标IP是N2,目标MAC是R1的F0/1接口IP。
3) 当R1收到这个数据包后,检查MAC地址,发现目标MAC地址是自己的,于是拆除二层报头,分析ip报头,同样也要检查包的目标IP是否是自己,由
于目标不是自己,它会主动查找自己的路由表,发现目标IP跟自己F0/0接口处在同一网段,于是就把包传到F0/0接口上去发给N2,假如R1上的ARP缓
存表中没有N2的MAC,则接口F0/0会发送一个ARP广播给跟它相连的广播域中。这个ARP广播包的源IP是接口F0/0的IP,源MAC也是F0/0的MAC,目标
IP是N2,目标MAC为N2,假如N2的MAC地址已经在R1的ARP缓存中了,那就会直接把数据包封装成: 源IP为N0,源MAC为R1的F0/0,目标IP为N2,目
标MAC为N2。主机N2收到这个包后发现目标MAC是自己,于是拆除二层报头,分析ip报头,发现ip是自己,就会处理这个包,处理完成后,将源ip变为
目标ip,目标ip变为源ip进行封包,然后根据主机的路由进行回包。
4) 跨路由中,包的源IP、目标IP始终不会发生变化,源MAC、目标MAC根据所经过的路由接口不同而发生相应变化。
二层为三层提供传输服务,二层数据帧,三层数据包。
5) 路由表: 目的网段 下一跳ip 出端口
(5) 什么是数据包和数据帧
1) DNS服务器:域名解析 例如:将www.baidu.com 解析成 -> 220.181.38.148
2) 网卡MAC地址:MAC地址如同我们身份证号,作为一种标识,具有全球唯一性,通常说的MAC地址指的是网卡的物理地址是由网卡生产厂家烧入网
卡的EPROM。
3) 数据包:数据 + 源IP地址 + 目标IP地址
4) 数据帧:数据 + 源IP地址 + 目标IP地址 + 源物理地址 + 目标物理地址
(6) docker bridge模式
有了网络命名空间,容器与宿主机网络命名空间是隔离的,所以网络不通。
1) 当Docker进程启动时,会在主机上创建一个名为docker0的虚拟网桥,此主机上启动的Docker容器会连接到这个虚拟网桥上。
2) 虚拟网桥的工作方式和物理交换机类似,这样主机上的所有容器就通过交换机连在了一个二层网络中。
3) 从docker0子网中分配一个IP给容器使用,并设置docker0的IP地址为容器的默认网关。
4) 在主机上创建一对虚拟网卡veth pair设备,Docker将veth pair设备的一端放在新创建的容器中,并命名为eth0(容器的网卡),另一端放
在主机中,以vethxxx这样类似的名字命名,并将这个网络设备加入到docker0网桥中。可以通过brctl show命令查看。
5) bridge模式是docker的默认网络模式,不写 --net 参数,就是 bridge 模式。使用 docker run -p时,docker 实际是在iptables做
了DNAT规则,实现端口转发功能。可以使用 iptables -t nat -vnL 查看。
6) 容器怎么访问外部
SNAT(源地址转换,172.17.0.1/16 -> 宿主机IP,由宿主机转发出去)
7) 外部怎么访问容器
DNAT(目标地址转换,外部访问宿主机IP,宿主机转发容器)
8) 交换技术(docker0桥)实现的功能
实现容器之间通信
4.2 Kubernetes网络模型
1 Docker 容器网络模型
(1) Docker网络模型涉及的名词
1)网络的命名空间: Linux在网络栈中引入网络命名空间,将独立的网络协议栈隔离到不同的命令空间中,彼此间无法通信;Docker利用 这一特性,实现不同容器间的网络隔离。 2)Veth设备对: Veth设备对的引入是为了实现在不同网络命名空间的通信,veth工作在数据链路层,veth-pair设备在转发数据包过程中并 不串改数据包内容。 3)Iptables/Netfilter: Docker使用Netfilter实现容器网络转发。 4)网桥: 网桥是一个二层网络设备,通过网桥可以将Linux支持的不同的端口连接起来,并实现类似交换机那样的多对多的通信。 5)路由: Linux系统包含一个完整的路由功能,当IP层在处理数据发送或转发的时候,会使用路由表来决定发往哪里。
(2) Docker容器网络示意图如下
2 Pod 网络
(1) Pod是K8S最小调度单元,一个Pod由一个容器或多个容器组成,当多个容器时,怎么都用这一个Pod IP 实现: k8s会在每个Pod里先启动一个infra container小容器,然后让其他的容器连接进来这个网络命名空间,然后其他容器看到的网络 视图就完全一样了,即网络设备、IP地址、Mac地址等。这就是解决网络共享的一种方法,所以Pod的IP地址就是infra container的IP 地址。
(2) 在 Kubernetes 中,每一个 Pod 都有一个真实的 IP 地址,并且每一个 Pod 都可以使用此 IP 地址与 其他 Pod 通信,Pod之间通信存在的 两种情况为 1)两个Pod在同一个Node上 2)两个Pod在不同的Node上
(3) 两个Pod在同一个Node上 同节点Pod之间通信道理与Docker网络一样的,如下图:
1)对 pod1 来说,eth0 通过虚拟以太网设备(veth0)连接到 root namespace 的 cbr0 网桥上; 2)对 pod2 来说,eth0 通过虚拟以太网设备(veth1)连接到 root namespace 的 cbr0 网桥上; 3)cbr0网桥是一个二层网络设备,可以理解为交换机; 4)pod1 使用ARP协议解析出目标pod2的mac地址; 3)然后pod1的数据包通过cbr0网桥转发到pod2的network namespace中的eth0网络设备上。
(4) 两个Pod在不同Node上 相比同节点Pod通信,这里源Pod发出的数据包需要传递到目标节点,但是源Pod并不知道目标Pod在哪个节点上。 1)K8S网络模型要求Pod IP在整个网络中都可访问,这种需求是由第三方网络组件实现
2)为了实现容器跨主机通信需求,就需要部署网络组件,这些网络组件都必须满足如下要求 一个Pod一个IP 所有的 Pod 可以与任何其它 Pod 直接通信 所有节点可以与所有 Pod 直接通信 Pod 内部获取到的 IP 地址与其它 Pod 或节点与其通信时的 IP 地址是同一个
3)目前支持的一些K8s网络组件
网络组件说明: https://kubernetes.io/docs/concepts/cluster-administration/networking/ 常用的k8s网络组件为flannel、calico。
其实k8s网络组件flannel和calico主要解决的问题是k8s节点之间容器网络的通信,要保证每个pod的IP是唯一的,怎么保证是唯一的,大部分组件
的做法是在每个Node上分配一个唯一的子网,node1是一个单独的子网,node2是一个单独的子网,可以理解是不同网段,不同vlan,所以每个节点都
是一个子网,所以网络组件会预先设置一个大的子网,然后在每个node上分配子网,这些信息都会由网络组件存储到etcd中,并且每个子网绑定到node
上都有关系记录的,然后方便下次进行二次的数据包传输,并且网络组件会在node上启动一个守护进程并运行,守护进程主要维护的是本地的路由规则
和维护etcd中的信息。
3 CNI(容器网络接口)
CNI(Container Network Interface,容器网络接口)是一个容器网络规范,Kubernetes网络采用的就是这个CNI规范,负责初始化infra容器的 网络设备。其实 cni 存在的意义就在于让第三方网络组件能够更顺利的连接 k8s,因为pod由kubelet去创建的,pod的网络也是由kubelet去 分配的,那kubelet这么去创建的容器呢,kubelet调用docker的API去实现的,也就是使用dockershim来创建容器,并且也会调用cni二进制 文件为容器分配网络,比如分配一个ip,有cni就是为了网络规范,k8s这块不能都满足所有的需求,所以它制定了一个规范,只要按照它的 规范就能接入到k8s的网络中来,它能结藕组件,也就是接入想用的组件都可以,只要满足这个需求。
(1) CNI二进制程序默认路径
# ls /opt/cni/bin/
github项目地址:
https://github.com/containernetworking/cni
cni二进制程序下载地址:
https://github.com/containernetworking/plugins/releases/download/v1.0.1/cni-plugins-linux-amd64-v1.0.1.tgz
# 当 kubelet 组件需要创建 Pod 的时候,先调用dockershim它先创建一个 Infra 容器。然后调用 CNI 插件为 Infra 容器配置网络。
# kubelet -> dockershim -> 调用cni插件为infra container分配网络(/opt/cni/bin)
(2) CNI配置文件默认路径
# ls /etc/cni/net.d/
# 以Flannel网络组件为例,当部署Flanneld后,会在每台宿主机上生成它对应的CNI配置文件(它其实是一个ConfigMap),从而告诉Kubernetes
要使用 Flannel 作为容器网络方案。
(3) cni插件、配置文件路径在kubelet启动参数中定义
# vim /opt/kubernetes/cfg/kubelet.conf
--network-plugin=cni
--cni-conf-dir=/etc/cni/net.d
--cni-bin-dir=/opt/cni/bin
注: 只要配置"--network-plugin=cni"参数就可以了,另外两个参数默认已经配置了。
4.3 Kubernetes网络组件之 Flannel
1 Flannel 部署
Flannel是k8s的一个网络组件,Flannel为每个Pod提供全局唯一的IP,Flannel使用ETCD来存储Pod子网与Node IP之间的关系。flanneld守 护进程在每台主机上运行,并负责维护ETCD信息和路由数据包。
(1) 项目地址
https://github.com/flannel-io/flannel
(2) yaml文件下载地址
# wget https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml
(3) 部署
1) 在每个节点上部署cni二进制文件
# mkdir -p /opt/cni/bin/
# tar -xzf cni-plugins-linux-amd64-v1.0.1.tgz -C /opt/cni/bin/
2) 在k8s master节点上应用kube-flannel.yml文件
# kubectl apply -f kube-flannel.yml
(4) 在应用kube-flannel.yml文件之前,有两处可能需要调整
# vim kube-flannel.yml
net-conf.json: |
{
"Network": "10.244.0.0/16",
"Backend": {
"Type": "vxlan"
}
}
1) Network:指定Pod IP分配的网段,与controller-manager配置的保持一样
--allocate-node-cidrs=true
--cluster-cidr=10.244.0.0/16
kubeadm部署 # /etc/kubernetes/manifests/kube-controller-manager.yaml
二进制部署 # /opt/kubernetes/cfg/kube-controller-manager.conf
2) Backend:指定工作模式,Flannel支持多种工作模式
udp # 最早支持的一种方式,由于性能最差,目前已经弃用。
vxlan # Overlay Network方案,源数据包封装在另一种网络包里面进行路由转发和通信(隧道方案)。
host-gw # Flannel通过在各个节点上的Agent进程,将容器网络的路由信息写到主机的路由表上,这样一来所有的主机都有整个容器网络的路由
数据了(路由方案)。
directrouting # 同时支持vxlan和host-gw工作模式。
公有云VPC # ALIYUN,AWS
2 vxlan 网络模式
(1) vxlan介绍 VXLAN,即 Virtual Extensible LAN(虚拟可扩展局域网),是 Linux 内核本身就支持的一种网络虚似化技术。VXLAN 可以完全在内核态实现 网络包的封装和解封装工作,从而通过类似“隧道”机制构建出覆盖网络(Overlay Network)。VXLAN的覆盖网络设计思想是在现有的三层网 络之上覆盖一层二层网络,使得连接在这个VXLAN二层网络上的主机之间,可以像在同一局域网里通信。为了能够在二层网络上打通“隧道” ,VXLAN 会在宿主机上设置一个特殊的网络设备作为“隧道”的两端。这个设备就叫作 VTEP,即 VXLAN Tunnel End Point(虚拟隧道端点)。
(2) VTEP设备进行封装和解封装的对象是二层数据帧,这个工作是在Linux内核中完成的 工作逻辑图如下:
(3) 如果Pod 1访问Pod 2,源地址10.244.1.10,目的地址10.244.2.10 ,数据包传输流程如下
1) 容器路由:容器根据路由表,将数据包发送下一跳10.244.1.1,从eth0网卡出,可以使用ip route命令查看路由表。
2) 主机路由:数据包进入到宿主机虚拟网卡cni0,根据路由表转发到flannel.1虚拟网卡,也就是来到了隧道的入口。
10.244.2.0/24 via 10.244.2.0 dev flannel.1 onlink
# 凡是发往10.244.2.0/24网段的数据包,都需要经过flannel.1设备发出,并且下一跳是10.244.2.0,即Node2 VTEP设备flannel.1。
3) VXLAN封装:而这些VTEP设备之间组成一个二层网络,但是二层网络必须要知道目的MAC地址,那这个MAC地址从哪获取到呢,其实在flanneld进
程启动后,就会自动添加其他节点ARP记录,可以通过"ip neigh show dev flannel.1"命令查看。
4) 二次封包:知道了目的MAC地址,Linux内核就可以进行二层封包了。但是,对于宿主机网络来说这个二层帧并不能在宿主机二层网络里传输。所以
接下来,Linux内核还要把这个数据帧进一步封装成为宿主机网络的一个普通数据帧,好让它载着内部数据帧,通过宿主机的eth0网卡进行传输。
数据格式如下图:
5) 封装到UDP包发出去:在封装成宿主机网络可传输的数据帧时,还缺少目标宿主机MAC地址,也就是说这个UDP包该发给哪台宿主机呢,flanneld进
程也维护着一个叫做FDB的转发数据库,可以通过"bridge fdb show dev flannel.1"命令查看。可以看到,上面用的对方flannel.1的MAC地址
对应宿主机IP,也就是UDP要发往的目的地。所以使用这个目的IP与MAC地址进行封装。
6) 数据包到达目的宿主机:接下来,就是宿主机与宿主机之间通信了,数据包从Node1的eth0网卡发出去,Node2接收到数据包,解封装发现是VXLAN
数据包,把它交给flannel.1设备。flannel.1设备则会进一步拆包,取出原始IP包(源容器IP和目标容器IP),通过cni0网桥二层转发给容器。
7) flannel维护
Pod网段与节点之间的关系表
flannel.1 ip和mac对应表
路由表
3 host-gw 网络模式
host-gw模式相比vxlan简单了许多,直接添加路由,将目的主机当做网关,直接路由原始封包。当你设置flannel使用host-gw模式,flanneld 会在宿主机上创建节点的路由表。 工作逻辑图如下:
如果Pod1访问Pod2,源地址10.244.1.10,目的地址10.244.2.10 ,数据包传输流程如下:
# ip route => node1
default via 192.168.31.1 dev eth0 proto static metric 100
192.168.31.0/24 dev eth0 proto kernel scope link src 192.168.31.62 metric 100
10.244.1.0/24 dev cni0 proto kernel scope link src 10.244.1.1
10.244.2.0/24 via 192.168.31.63 dev eth0
(1) 目的IP地址属于10.244.2.0/24网段的IP包,应该经过Node1的eth0设备发出去(即 dev eth0),并且它下一跳地址是192.168.31.63
(即via 192.168.31.63)。
(2) 一旦配置了下一跳地址,那么接下来,当IP包从网络层进入链路层封装成帧的时候,eth0设备就会使用下一跳地址对应的MAC地址作为该数
据帧的目的MAC地址。
(3) 而Node2的内核网络栈从二层数据帧里拿到IP包后,会看到这个IP包的目的IP地址是10.244.2.10,即pod2的IP地址,这时候根据Node2
上的路由表(10.244.2.0/24 dev cni0 proto kernel scope link src 10.244.2.1),从而进入cni0网桥,进而进入到pod2当中。
(4) 可见,数据包是封装成帧发送出去的,会使用路由表的下一跳来设置目的MAC地址,会经过二层网络到达目的节点,因此,host-gw模式必须
要求集群宿主机之间二层连通。
4 flannel网络总结
(1) VXLAN特点
1) 先进行二层帧封装,再通过宿主机网络封装,解封装也一样,所以增加性能开销
2) 对宿主机网络要求低,只要三层网络可达
(2) Host-GW特点
1) 直接路由转发,性能损失很小
2) 对宿主机网络要求二层可达
(3) 总结
1) 如果想追求性能的话,二层可以通信,那么就可以选择host-gw。
2) 如果节点之间不能通过二层通信,节点在不同的vlan中,需要路由的转发,那么使用vxlan是最好的,因为可以满足这样的一个需求。
4.4 K8s主流网络方案之Calico
1 Calico概述
(1) calico介绍
1) calico主要通过ipip协议与bgp协议来实现通信。前者通过ipip隧道作为通信基础,后者则是纯三层的路由交换。
2) bgp协议主要由两种方式:BGP Speaker 全互联模式(node-to-node mesh)与BGP Speaker RR模式。
3) calico没有使用 cni 网桥,所以,每个容器都会创建一个这样类似的路由条目,10.244.169.129 dev calidc71bf49fb8 scope link。
4) Calico 实现了 Kubernetes 网络策略并提供ACL功能。
(2) BGP工作模式
bgp工作模式和flannel的host-gw模式几乎一样,bird是bgd的客户端,与集群中其它节点的bird进行通信,以便于交换各自的路由信息,随着节点
数量N的增加,这些路由规则将会以指数级的规模快速增长,给集群本身网络带来巨大压力,官方建议小于100个节点,和flannel的host-gw限制一样,
要求物理机在二层是能连接的,不能跨网段。bgp工作模式也是基于路由表实现容器数据包转发,但不同于Flannel使用flanneld进程来维护路由信息
的做法,而是使用BGP协议来自动维护整个集群的路由信息。Calico 在每一个计算节点利用 Linux Kernel 实现了一个高效的虚拟路由器(vRouter)来负责数据转发,而每个 vRouter 通过 BGP 协议负责把自己上运行的 workload 的路由信息向整个 Calico 网络内传播。
(3) Route Reflector模式
在更大规模的集群中,需要通过Route Reflector模式专门创建一个或者几个专门的节点,负责跟所有的BGP客户端建立连接,从而学到全局的路由规
则,而其它节点,只需要跟这几个专门的节点交换路由信息,就可以获得整个集群的路由信息。100个节点以内建议2-3个路由反射器。
(4) IPIP模式
IPIP工作模式用在主机节点跨网段通信的情况下,bgp模式在主机节点跨网段的场景将不能工作,tunl0是创建的虚拟网卡设备,作用就和flannel
VxLAN工作模式的flannel.1虚拟网卡设备类似。
(5) BGP 网络示意图
BGP英文全称是Border Gateway Protocol,即边界网关协议,它是一种自治系统间的动态路由发现协议,与其他 BGP 系统交换网络可达信息。
1) 在这个图中,有两个自治系统(autonomous system,简称为AS):AS 1 和 AS 2
自治系统,可以想成公司的网络和其他公司的网络,两个就可以理解为两个自治系统,每个自治系统是由自己交换机,路由器来组成的,而这些交换机路
由器单独去运行,它不依赖于别的公司,别的公司也不依赖你公司的网络,都可以去独立的单独的去运行,一个大学,一个企业都可以说成是一个自治系
统,但是这个自治系统也没什么交际,你家的网络和邻居家的网络也没什么来往,但是如果他们想通信,他们本身就不在一个网络里面,你家的网络和它
家的网络上层的网络出口的路由器必须相互的学习到,而且我们用到的电脑都是私网IP,邻居家也用的是私网IP,甚至这些IP都是冲突的,如果想实现两
家的网络内网能够通信,首先保证用到的ip地址不能冲突,还要保证上层的路由出口之间能够相互学到自己的路由表,比如你家的路由器能够学习到当前
路由表的信息。
2) 在互联网中,一个自治系统(AS)是一个有权自主地决定在本系统中应采用何种路由协议的小型单位。这个网络单位可以是一个简单的网络也可以是一
个由一个或多个普通的网络管理员来控制的网络群体,它是一个单独的可管理的网络单元(例如一所大学,一个企业或者一个公司个体)。一个自治系统
有时也被称为是一个路由选择域(routing domain)。
3) 一个自治系统将会分配一个全局的唯一的16位号码,有时我们把这个号码叫做自治系统号(ASN)。
4) 在正常情况下,自治系统之间不会有任何来往。如果两个自治系统里的主机,要通过 IP 地址直接进行通信,我们就必须使用路由器把这两个自治系
统连接起来。BGP协议就是让他们互联的一种方式。
(6) Calico BGP架构
在了解了 BGP 之后,Calico BGP 的架构就非常容易理解了,Calico BGP主要由三个部分组成:
1) Felix # 以DaemonSet方式部署,运行在每一个Node节点上,主要负责维护宿主机上路由规则以及ACL规则。
2) BGP Client(BIRD) # 分发路由信息的BGP客户端,主要负责把 Felix 写入 Kernel 的路由信息分发到集群 Calico 网络。
3) Etcd # 分布式键值存储,保存Calico的策略和网络配置状态。
4) calicoctl # 命令行管理Calico。
5) BGP Route Reflector # BGP路由反射器,可选组件,用于较大规模的网络场景
2 Calico部署
官方文档: https://docs.projectcalico.org/archive/v3.19/getting-started/kubernetes/self-managed-onprem/onpremises 我这里集群使用的calico版本为v3.19。 (1) Calico存储有两种方式 1) 数据存储在Kubernetes API Datastore服务中(建议) # 节点数小于50 # wget https://docs.projectcalico.org/archive/v3.19/manifests/calico.yaml --no-check-certificate # 节点数大于50 # wget https://docs.projectcalico.org/archive/v3.19/manifests/calico-typha.yaml --no-check-certificate 2) 数据存储在etcd # wget https://docs.projectcalico.org/archive/v3.19/manifests/calico-etcd.yaml --no-check-certificate (2) 如果数据存储在etcd中,需要修改yaml 1) 配置连接etcd地址(ConfigMap位置)。 2) 如果使用https,还需要配置证书(Secret位置)。 (3) 根据实际网络规划修改Pod网段 # no effect. This should fall within `--cluster-cidr`. - name: CALICO_IPV4POOL_CIDR value: "172.27.0.0/16" (4) 根据需要修改Calico工作模式 1) IPIP # Overlay Network方案,源数据包封装在宿主机网络包里进行转发和通信,值设为Always(默认)。 2) BGP # 基于路由转发,每个节点通过BGP协议同步路由表,写到宿主机,值设置为Never。 3) CrossSubnet # 同时支持BGP和IPIP,即根据子网选择转发方式,值设置为cross-subnet。 4) 调整参数改变工作模式的方法 # Enable IPIP - name: CALICO_IPV4POOL_IPIP value: "Always" (5) 新增绑定网卡参数(IP_AUTODETECTION_METHOD) # Cluster type to identify the deployment type - name: CLUSTER_TYPE value: "k8s,bgp" - name: IP_AUTODETECTION_METHOD value: "interface=eth1" (6) 部署 # kubectl apply -f calico.yaml # kubectl get pods -n kube-system NAME READY STATUS RESTARTS AGE calico-kube-controllers-6f45b5f9cb-gqpjs 1/1 Running 0 117s calico-node-5mjh8 1/1 Running 0 117s calico-node-fm9vf 1/1 Running 0 117s calico-node-jnggn 1/1 Running 0 117s calico-node-rxsds 1/1 Running 0 117s (7) Calico 管理工具 1) calicoctl工具用于管理calico,可通过命令行读取、创建、更新和删除 Calico 的存储对象。calicoctl 在使用过程中会从配置文件中读取 Calico 对象存储地址等信息,默认配置文件路径为"/etc/calico/calicoctl.cfg"。默认"/etc/calico/"目录和calicoctl.cfg文件不存在 ,需要自己创建。 # mkdir -p /etc/calico/ # 配置文件配置方式1: # cat calicoctl.cfg apiVersion: projectcalico.org/v3 kind: CalicoAPIConfig metadata: spec: datastoreType: "kubernetes" kubeconfig: "/root/.kube/config" # 配置文件配置方式2: # cat calicoctl-etcd.cfg apiVersion: projectcalico.org/v3 kind: CalicoAPIConfig metadata: spec: datastoreType: "etcdv3" etcdEndpoints: "https://172.16.1.81:2379,https://172.16.1.82:2379,https://172.16.1.83:2379" etcdKeyFile: "/opt/etcd/ssl/server-key.pem" etcdCertFile: "/opt/etcd/ssl/server.pem" etcdCACertFile: "/opt/etcd/ssl/ca.pem" # 如果不配置calicoctl的默认路径,calicoctl默认找的路径为"/root/.kube/config" 2) 项目地址 https://github.com/projectcalico/calicoctl # wget https://github.com/projectcalico/calicoctl/releases/download/v3.19.3/calicoctl-linux-amd64 注: 下载的calicoctl工具版本要和上面部署的calico容器镜像版本保持一致。 # mv calicoctl-linux-amd64 /usr/bin/calicoctl/ # chmod +x /usr/bin/calicoctl 3) 查看Calico状态 # calicoctl get nodes # calicoctl node status # 显示除本节点之外的calico所在节点状态 # calicoctl get ippool -o wide
3 IPIP隧道模式及原理解析
IPIP模式采用Linux IPIP隧道技术实现的数据包封装与转发,IP隧道(IP tunneling)是将一个IP报文封装在另一个IP报文的技术,Linux系统内
核实现的IP隧道技术主要有三种,IPIP、GRE、SIT。
Pod 1 访问 Pod 2 大致流程如下:
(1) 数据包(原始数据包)从容器出去到达宿主机,宿主机根据路由表发送到tunl0设备(IP隧道设备)
(2) Linux内核IPIP驱动将原始数据包封装在宿主机网络的IP包中(新的IP包目的地是原IP包的下一跳地址,即192.168.31.63)
(3) 数据包根据宿主机网络到达Node2
(4) Node2收到数据包后,使用IPIP驱动进行解包,从中拿到原始数据包
(5) 然后根据路由规则,根据路由规则将数据包转发给cali设备,从而到达容器 pod2
4 BGP路由模式及原理解析
(1) BGP模式基于路由转发,每个节点通过BGP协议同步路由表,将每个宿主机当做路由器,实现数据包转发。
(2) calicoctl工具将k8s calico网络由IPIP模式修改为BGP模式(立即生效)
1) 查看calico现有网络模式
[root@k8s-master1 ~]# calicoctl node status
Calico process is running.
IPv4 BGP status
+--------------+-------------------+-------+----------+-------------+
| PEER ADDRESS | PEER TYPE | STATE | SINCE | INFO |
+--------------+-------------------+-------+----------+-------------+
| 172.16.1.82 | node-to-node mesh | up | 16:59:31 | Established |
| 172.16.1.83 | node-to-node mesh | up | 16:59:35 | Established |
| 172.16.1.84 | node-to-node mesh | up | 16:59:24 | Established |
+--------------+-------------------+-------+----------+-------------+
IPv6 BGP status
No IPv6 peers found.
[root@k8s-master1 ~]# calicoctl get ippool -o wide
NAME CIDR NAT IPIPMODE VXLANMODE DISABLED SELECTOR
default-ipv4-ippool 172.27.0.0/16 true Always Never false all()
# ip route
2) 将calico网络模式IPIP修改为BGP [root@k8s-master1 ~]# calicoctl get ippool -o yaml > ippool.yaml [root@k8s-master1 ~]# vim ippool.yaml # 将"ipipMode: Always"修改为"ipipMode: Never" apiVersion: projectcalico.org/v3 items: - apiVersion: projectcalico.org/v3 kind: IPPool metadata: creationTimestamp: "2021-11-04T10:55:57Z" name: default-ipv4-ippool resourceVersion: "11839" uid: 0e6d0e42-d06d-449b-a3b3-d2a709bbf6d8 spec: blockSize: 26 cidr: 172.27.0.0/16 #ipipMode: Always ipipMode: Never natOutgoing: true nodeSelector: all() vxlanMode: Never kind: IPPoolList metadata: resourceVersion: "13617" 3) 应用修改后的yaml文件 [root@k8s-master1 ~]# calicoctl apply -f ippool.yaml Successfully applied 1 'IPPool' resource(s) 4) 应用后查看calico的网络工作模式 [root@k8s-master1 ~]# calicoctl node status Calico process is running. IPv4 BGP status +--------------+-------------------+-------+----------+-------------+ | PEER ADDRESS | PEER TYPE | STATE | SINCE | INFO | +--------------+-------------------+-------+----------+-------------+ | 172.16.1.82 | node-to-node mesh | up | 16:59:31 | Established | | 172.16.1.83 | node-to-node mesh | up | 16:59:35 | Established | | 172.16.1.84 | node-to-node mesh | up | 16:59:24 | Established | +--------------+-------------------+-------+----------+-------------+ IPv6 BGP status No IPv6 peers found. [root@k8s-master1 ~]# calicoctl get ippool -o wide # calicoctl get ippool -o wide NAME CIDR NAT IPIPMODE VXLANMODE DISABLED SELECTOR default-ipv4-ippool 172.27.0.0/16 true Never Never false all() # ip route # tunl0的ip会被移除
# calico没有网桥数据包是怎么出去的
pod1的数据包从veth的设备对到到宿主机的一段eth0上,之前的数据包其实是走的默认宿主机的网关将流量转发到calico的cali的设备上的,通过路由表信息下一跳地址到宿主机然后转发到对应的容器中。
(3) Pod1 访问 Pod2 大致流程如下
1) 数据包从容器出去到达宿主机
2) 宿主机根据路由规则,将数据包转发给下一跳(网关)
3) 到达Node2,根据路由规则将数据包转发给cali设备,从而到达容器pod2。
5 BGP RR路由反射模式
(1) 全互联模式
Calico 维护的网络在默认是(Node-to-Node Mesh)全互联模式,Calico集群中的节点之间都会相互建立连接,用于路由交换。但是随着集群规模
的扩大,mesh模式将形成一个巨大服务网格,连接数成倍增加,就会产生性能问题。这时就需要使用Route Reflector(路由器反射)模式解决这个问
题。建议集群节点小于100时使用。
(2) 路由反射模式
确定一个或多个Calico节点充当路由反射器,集中分发路由,让其他节点从这个RR节点获取路由信息。建议集群节点大于100时使用。
(3) 路由反射实现步骤 1) 查看calico现有模式 [root@k8s-master1 ~]# calicoctl node status Calico process is running. IPv4 BGP status +--------------+-------------------+-------+----------+-------------+ | PEER ADDRESS | PEER TYPE | STATE | SINCE | INFO | +--------------+-------------------+-------+----------+-------------+ | 172.16.1.82 | node-to-node mesh | up | 02:20:37 | Established | | 172.16.1.83 | node-to-node mesh | up | 02:20:41 | Established | | 172.16.1.84 | node-to-node mesh | up | 02:20:40 | Established | +--------------+-------------------+-------+----------+-------------+ IPv6 BGP status No IPv6 peers found. [root@k8s-master1 ~]# calicoctl get ippool -o wide NAME CIDR NAT IPIPMODE VXLANMODE DISABLED SELECTOR default-ipv4-ippool 172.27.0.0/16 true Never Never false all() 2) 关闭 node-to-node 模式 # 获取ASN号 # calicoctl get nodes --output=wide NAME ASN IPV4 IPV6 k8s-master1 (64512) 172.16.1.81/24 k8s-master2 (64512) 172.16.1.82/24 k8s-node1 (64512) 172.16.1.83/24 k8s-node2 (64512) 172.16.1.84/24 # 添加 default BGP配置,调整 nodeToNodeMeshEnabled和asNumber # cat << EOF | calicoctl create -f - apiVersion: projectcalico.org/v3 kind: BGPConfiguration metadata: name: default spec: logSeverityScreen: Info nodeToNodeMeshEnabled: false asNumber: 64512 EOF # 输出结果 Successfully created 1 'BGPConfiguration' resource(s) 注: 执行操作后会断网 3) 配置指定节点充当路由反射器 # 为方便让BGPPeer轻松选择节点,通过标签选择器匹配。 # 给路由器反射器节点打标签 # kubectl label node k8s-node1 route-reflector=true # 然后配置路由器反射器节点 routeReflectorClusterID # calicoctl get node k8s-node1 -o yaml > rr-node1.yaml # vim rr-node1.yaml ... bgp: ipv4Address: 172.16.1.83/24 routeReflectorClusterID: 244.0.0.1 # 添加集群ID,必须是ipv4格式,唯一值即可 ... # calicoctl apply -f rr-node1.yaml 4) 使用标签选择器将路由反射器节点与其他非路由反射器节点配置为对等 # cat > bgppeer.yaml << EOF apiVersion: projectcalico.org/v3 kind: BGPPeer metadata: name: peer-with-route-reflectors spec: nodeSelector: all() peerSelector: route-reflector == 'true' EOF # calicoctl apply -f bgppeer.yaml 5) 查看节点的BGP连接状态 # calicoctl node status Calico process is running. IPv4 BGP status +--------------+---------------+-------+----------+-------------+ | PEER ADDRESS | PEER TYPE | STATE | SINCE | INFO | +--------------+---------------+-------+----------+-------------+ | 172.16.1.83 | node specific | up | 03:12:33 | Established | +--------------+---------------+-------+----------+-------------+ IPv6 BGP status No IPv6 peers found. [root@k8s-master1 ~]# calicoctl get ippool -o wide NAME CIDR NAT IPIPMODE VXLANMODE DISABLED SELECTOR default-ipv4-ippool 172.27.0.0/16 true Never Never false all() 6) 增加路由反射器节点,执行上面的"3)"步骤即可。 [root@k8s-master1 ~]# calicoctl node status Calico process is running. IPv4 BGP status +--------------+---------------+-------+----------+-------------+ | PEER ADDRESS | PEER TYPE | STATE | SINCE | INFO | +--------------+---------------+-------+----------+-------------+ | 172.16.1.83 | node specific | up | 07:07:42 | Established | | 172.16.1.84 | node specific | up | 07:13:10 | Established | +--------------+---------------+-------+----------+-------------+ IPv6 BGP status No IPv6 peers found.
6 办公网络与K8s网络互通方案
(1) 网络需求
1) 办公网络与Pod网络不通。在微服务架构下,开发人员希望在办公电脑能直接连接K8s中注册中心调试;
2) 办公网络与Service网络不通。在测试环境运行的mysql、redis等需要通过nodeport暴露,维护成本大;
3) 现有虚拟机业务访问K8s上的业务。
(2) 解决方案
打通办公网络与K8s网络
(3) 方案一: 专门准备一台Node负责转发来自办公网络
(4) 方案二: 两方上层路由器使用BGP做路由交换
4.5 总结
网络组件 | 覆盖网络 | 主机路由 | 混合 | 网络策略 |
---|---|---|---|---|
Flannel | udp/vxlan | host-gw | directrouting | N |
Calico | IPIP(Always) | BGP(Never) | cross-subnet | Y |
1 CNI网络方案优缺点及最终选择
(1) 需要细粒度网络访问控制
这个flannel是不支持的,calico支持,所以做多租户网络方面的控制ACL,那么要选择calico
(2) 追求网络性能
这无疑是flannel和calico的路由方案是最好的,也就是flannel的host-gw和calico的BGP。
(3) 服务器之间是否可以跑BGP协议
很多的公有云是不支持跑BGP协议的,那么使用calico的BGP模式自然是不行的。
(4) 集群规模多大
如果规模不大,100节点以下维护起来比较方便,使用flannel就可以。
(5) 是否有维护能力
calico的路由表很多,而且走BGP协议,一旦出现问题排查起来也比较困难,上百台的节点,路由表去排查也是很麻烦,这个具体的需求也是跟自己的
情况而定。
4.6 网络策略
1 网络隔离概述
默认所有的pod之间都可以互通、所有的node和pod之间都可以互通、所有命名空间的pod之间都可以互通。
(1) 网络策略(Network Policy),用于限制Pod出入流量,提供Pod级别和Namespace级别网络访问控制。
(2) 一些应用场景
1) 应用程序间的访问控制。例如微服务A允许访问微服务B,微服务C不能访问微服务A
2) 开发环境命名空间不能访问测试环境命名空间Pod
3) 当Pod暴露到外部时,需要做Pod白名单
4) 多租户网络环境隔离
(3) Pod网络入口方向隔离
1) 基于Pod级网络隔离:只允许特定对象访问Pod(使用标签定义),允许白名单上的IP地址或者IP段访问Pod
2) 基于Namespace级网络隔离:多个命名空间,A和B命名空间Pod完全隔离。
(4) Pod网络出口方向隔离
1) 拒绝某个Namespace上所有Pod访问外部
2) 基于目的IP的网络隔离:只允许Pod访问白名单上的IP地址或者IP段
3) 基于目标端口的网络隔离:只允许Pod访问白名单上的端口
2 入站流量访问控制案例
(1) 需求:将 default 命名空间携带 app=web 标签的 Pod 隔离,只允许 default 命名空间携带 run=client1 标签的 Pod 访问 80 端口 (2) 准备测试环境 # kubectl create deployment web --image=nginx -n default # kubectl run client1 --image=busybox -n default -- sleep 36000 # kubectl run client2 --image=busybox -n default -- sleep 36000 # kubectl get pod --show-labels -n default -o wide NAME READY STATUS RESTARTS AGE IP NODE LABELS client1 1/1 Running 0 7m22s 172.27.224.5 k8s-master2 run=client1 client2 1/1 Running 0 7m7s 172.27.159.135 k8s-master1 run=client2 web-96d5df5c8-92llp 1/1 Running 0 8m2s 172.27.224.4 k8s-master2 app=web,pod-template-hash=96d5df5c8 (3) 部署策略 # vim calico-in-acl.yaml apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: test-network-policy namespace: default spec: podSelector: matchLabels: app: web policyTypes: - Ingress ingress: - from: - namespaceSelector: matchLabels: project: default - podSelector: matchLabels: run: client1 ports: - protocol: TCP port: 80 # kubectl apply -f calico-in-acl.yaml (4) 测试 分别进入pod client1、client2,然后使用命令 wget <app=web 的pod IP>,会发现只有pod client1 可以下载 nginx 的首页。 (5) 删除calico的acl策略,防止影响下面的实验 # kubectl get networkpolicy -n default NAME POD-SELECTOR AGE test-network-policy app=web 21m # kubectl delete networkpolicy/test-network-policy -n default
3 出站流量访问控制案例
(1) 需求: default命名空间下所有pod可以互相访问,也可以访问其他命名空间Pod,但其他命名空间不能访问default命名空间Pod 1) podSelector: {} # 如果未配置,默认所有Pod 2) from.podSelector: {} # 如果未配置,默认不允许 (2) 部署策略 # vim calico-out-acl.yaml apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: deny-from-other-namespaces namespace: default spec: podSelector: {} policyTypes: - Ingress ingress: - from: - podSelector: {} # kubectl apply -f calico-out-acl.yaml # kubectl get networkpolicy -n default NAME POD-SELECTOR AGE deny-from-other-namespaces <none> 10s (3) 测试 1) 在kube-system命名空间下创建一个测试pod client3 # kubectl run client3 --image=busybox -n kube-system -- sleep 36000 2) 查看实验pod # kubectl get pod --show-labels -n default -o wide NAME READY STATUS RESTARTS AGE IP NODE LABELS client1 1/1 Running 0 3h3m 172.27.224.5 k8s-master2 run=client1 client2 1/1 Running 0 3h3m 172.27.159.135 k8s-master1 run=client2 web-96d5df5c8-92llp 1/1 Running 0 3h4m 172.27.224.4 k8s-master2 app=web,pod-template-hash=96d5df5c8 # kubectl get pod/client3 --show-labels -n kube-system -o wide NAME READY STATUS RESTARTS AGE IP NODE LABELS client3 1/1 Running 0 2m56s 172.27.169.140 k8s-node2 run=client3 3) 分别进入default命名空间pod client1、client2,kube-system命名空间pod client3,相互访问,发现pod client3不能访问 pod client1、client2,pod client1、client2可以互相访问并且可以访问pod client3。