QCon全球软件开发大会8折优惠倒计时,购票立减¥1760!了解详情 >>> 了解详情
写点什么

Kubernetes 网络全解:机制、方法、实操的超强指南

2020 年 5 月 14 日

Kubernetes网络全解:机制、方法、实操的超强指南

前言

随着云计算的兴起,各大平台之争也落下了帷幕,Kubernetes 作为后起之秀已经成为了事实上的 PaaS 平台标准,而网络又是云计算环境当中最复杂的部分,总是让人琢磨不透。本文将对 Kubernetes 网络问题做一个尽量全面的解读,重点围绕在 Kubernetes 环境当中同一个节点(work node)上的 Pod 之间是如何进行网络通信的。跨节点进行网络通信的情况会更为复杂,文末也将分享可行的开源方案仅供参考。


Network Namespace

Namespace


提到容器就不得不提起容器的核心底层技术 Namespace,Namespace 为 Linux 内核当中提供的一种隔离机制,最初在 2002 年引入到 Linux 2.4.19 当中,且只有 Mount Namespace 用于文件系统隔离。截止目前,Linux 总共提供了 7 种 Namespace(since Linux 4.6),系统当中运行的每一个进程都与一个 Namespace 相关联,且该进程只能看到和使用该 Namespace 下的资源。可以简单将 Namespace 理解为操作系统对进程实现的一种“障眼法”,比如通过 UTS Namespace 可以实现让运行在同一台机器上的进程看到不同的 Hostname。正是由于 Namespace 开创性地隔离方式才让容器的实现得以变为可能,才能让我们的软件真正地实现“build once, running everywhere”。


Mount Namespace文件系统隔离
UTS Namespace主机名隔离
IPC Namespace进程间通信隔离
PID Namespace进程号隔离
Network Namespace网络隔离
User Namespace用户认证隔离
Control group(Cgroup) NamespaceCgroup认证(since Linux 4.6)


Network Namespace


Network Namespace 是 Linux 2.6.24 才开始引入的,直到 Linux 2.6.29 才完成的特性。Network Namespace 实际上实现的是对网络栈的虚拟化,且在创建出来时默认只有一个回环网络接口 lo,每一个网络接口(不管是物理接口还是虚拟化接口)都只能存在于一个 Network Namespace 当中,但可以在不同的 Network Namespace 之间切换,每一个 Network Namespace 都有自己独立的 IP 地址、路由表、防火墙以及套接字列表等网络相关资源。当删除一个 Network Namespace 时,其内部的网络资源(网路接口等)也会同时被删掉,而物理接口则会被切换回之前的 Network Namespace 当中。


由此可以知道不同的两个 Network Namespace 在网络上是相互隔离的,即不能够直接进行通信,就好比两个相互隔离的局域网之间不能直接通信,那么通过 Network Namespace 隔离后的容器是如何实现与外部进行通信的呢?


容器与 Pod


在 Kubernetes 的定义当中,Pod 为一组不可分离的容器,且共享同一个 Network Namespace,故不存在同一个 Pod 当中容器间网络通信的问题,对于同一个 Pod 当中的容器来讲,通过 Localhost 即可与其他的容器进行网络通信。


所以同一个节点上的两个 Pod 如何进行网络通信的问题可以转变为,同一个节点上的两个容器如何进行网络通信。


Namespace 实操


在回答上面提出的 Network Namespace 网络通信的问题前,我们先来做一些简单的命令行操作,先对 Namespace 有一个感性地认识,实验环境如下:



通过命令 lsns 可以查看到宿主机上所有的 Namespace(注意需要使用 root 用户执行,否则可能会出现有些 Namespace 看不到的情况):



lsns 默认会输出所有可以看到的 Namespace,简单解释一下 lsns 命令各个输出列的含义:


NSNamespace identifier(inode number)
TYPEkind of namespace
NPROCSnumber of processes in the namespaces
PIDlowers PID in the namespace
USERusername of the PID
COMMANDcommand line of the PID


与 Network Namespace 相关性较强的还有另外一个命令 ip netns,主要用于持久化命名空间的管理,包括 Network Namespace 的创建、删除以和配置等。 ip netns 命令在创建 Network Namespace 时默认会在/var/run/netns 目录下创建一个 bind mount 的挂载点,从而达到持久化 Network Namespace 的目的,即允许在该命名空间当中没有进程的情况下依然保留该命名空间。Docker 当中由于缺少了这一步,玩过 Docker 的同学就会发现通过 Docker 创建容器后并不能在宿主机上通过 ip netns 查看到相关的 Network Namespace(这个后面会讲怎么才能够看到,稍微小操作一下就行)。


与 Network Namespace 相关操作命令:


ip netns add <namespace name> # 添加network namespaceip netns list # 查看Network Namespaceip netns delete <namespace name> # 删除Network Namespaceip netns exec <namespace name> <command> # 进入到Network Namespace当中执行命令
复制代码


创建名为 netA 的 Network Namespace:



查看创建的 Network Namespace:



可以看到 Network Namespace netA 当中仅有一个环回网络接口 lo,且有独立的路由表(为空)。


宿主机(root network namespace)上有网络接口 eth0(10.10.88.170)和 eth1(172.16.130.164),此时可以直接 ping 通 IP 172.16.130.164。



尝试将 root network namespace 当中的 eth0 接口添加到 network namespce netA 当中:


ip link set dev eth0 netns netA
复制代码



将宿主机上的网络接口 eth0(10.10.88.170)加入到网络命名空间 netA 后:


  1. 宿主机上看不到 eth0 网络接口了(同一时刻网络接口只能在一个 Network Namespace)

  2. netA network namespace 里面无法 ping 通 root namespace 当中的 eth1(网络隔离)


从上面的这些操作我们只是知道了 Network Namespace 的隔离性,但仍然无法达到我们想要的结果,即让两个容器或者说两个不同的 Network Namespace 进行网络通信。在真实的生活场景中,当我们要连接同一个集团两个相距千里的分公司的局域网时,我们有 3 种解决方案:第一种是对数据比较随意的,直接走公网连接,但存在网络安全的问题。第二种是不差钱的,直接拉一根专线将两个分公司的网络连接起来,这样虽然远隔千里,但仍然可以处于一个网络当中。另外一种是兼顾网络安全集和性价比的 VPN 连接,但存在性能问题。很显然,不管是哪一种方案都需要有一根“线”将两端连接起来,不管是虚拟的 VPN 还是物理的专线。


vEth(Virtual Ethernet Device)

前面提到了容器通过 Network Namespace 进行网络隔离,但是又由于 Network Namespace 的隔离导致两个不同的 Network Namespace 无法进行通信,这个时候我们联想到了实际生活场景中连接一个集团的两个分公司局域网的处理方式。实际上 Linux 当中也存在类似像网线一样的虚拟设备 vEth(此时是不是觉得 Linux 简直无所不能?),全称为 Virtual Ethernet Device,是一种虚拟的类似于以太网络的设备。


vEth 有以下几个特点:


  • vEth 作为一种虚拟以太网络设备,可以连接两个不同的 Network Namespace。

  • vEth 总是成对创建,所以一般叫 veth pair。(因为没有只有一头的网线)。

  • vEth 当中一端收到数据包后另一端也会立马收到。

  • 可以通过 ethtool 找到 vEth 的对端接口。(注意后面会用到)


理解了以上几点对于我们后面理解容器间的网络通信就容易多了。


vEth 实操


创建 vEth:


ip link add <veth name> type veth peer name <veth peer name>
复制代码


创建名为 veth0A,且对端为 veth0B 的 vEth 设备。



可以看到 root network namespace 当中多出来了两个网络接口 veth0A 和 veth0B,网络接口名称 @后面的接的正是对端的接口名称。


创建 Network Namespace netA 和 netB:


ip netns add netAip netns add netB
复制代码



分别将接口 veth0A 加入到 netA,将接口 veth0B 加入到 netB:


ip link set veth0A netns netAip link set veth0B netns netB
复制代码



这个时候通过 IP a 查看宿主机(root network namespace)网络接口时可以发现,已经看不到接口 veth0A 和 veth0B 了(同一时刻一个接口只能处于一个 Network Namespace 下面)。


再分别到 netA 和 netB 两个 Network Namespace 当中去查看,可以看到两个 Network Namespace 当中都多了一个网络接口。



分别拉起两个网络接口并配上 IP,这里将为 veth0A 配置 IP 192.168.100.1,veth0B 配置 IP 192.168.100.2:


ip netns exec netA ip link set veth0A upip netns exec netA ip addr add 192.168.100.1/24 dev veth0A
复制代码



ip netns exec netB ip addr add 192.168.100.2/24 dev veth0B
复制代码



测试通过 veth pair 连接的两个 Network Namespace netA 和 netB 之间的网络连接。


在 netA(192.168.100.1)当中 ping netB(192.168.100.2):



在 netB(192.168.100.2)当中 ping netA(192.168.100.1):



可以发现 netA 跟 netB 这两个 Network Namespace 在通过 veth pair 连接后已经可以进行正常的网络通信了。


解决了容器 Network Namespace 隔离的问题,这个时候有云计算经验或者熟悉 OpenStack 的同学就会发现,现在的场景跟虚拟机之间的网络互联是不是简直一模一样了?


vEth 作为一个二层网络设备,当需要跟别的网络设备相连时该怎么处理呢?在现实生活场景当中我们会拿一个交换机将不同的网线连接起来。实际上在虚拟化场景下也是如此,Linux Bridge 和 Open vSwith(OVS)是当下比较常用的两种连接二层网络的解决方案,而 Docker 当中采用的是 Linux Bridge。


Docker 与 Kubernetes

Kubernetes 作为一个容器编排平台,在容器引擎方面既可以选择 Docker 也可以选择 rkt,这里直接分别通过 Docker 和 Kubernetes 创建容器来进行简单比对。 Kubernetes 在创建 Pod 时首先会创建一个 pause 容器,它的唯一作用就是保留和占据一个被 Pod 当中所有容器所共享的网络命名空间(Network Namespace),就这样,一个 Pod IP 并不会随着 Pod 当中的一个容器的起停而改变。


Docker 下的容器网络


我们先来看一下在没有 Kubernetes 的情况下是什么样子的。在 Docker 启动的时候默认会创建一个名为 docker0 的网桥,且默认配置下采用的网络段为 172.17.0.0/16,每一个在该节点上创建的容器都会被分配一个在该网段下的 IP。容器通过连接到 docker0 上进行相互通信。


手动创建两个容器:


docker run -it --name testA busybox shdocker run -it --name testB busybox sh
复制代码


查看网络接口状况。


容器 testA:



容器 testB:



查看网桥状态:



可以发现 docker0 上面已经连接了两个虚拟网络接口(vEth)。


在 docker0 上通过 tcpdump 抓包:


tcpdump -n -i docker0
复制代码



可以发现容器 testA 和容器 testB 正是通过 docker0 网桥进行网络包转发的。


加入 Kubernetes 后的容器网络


其实加入 Kubernetes 后本质上容器网络通信模式并没有发生变更,但 Kubernetes 出于网络地址规划的考虑,重新创建了一个网桥 cni0 用于取代 docker0,来负责本节点上网络地址的分配,而实际的网络段管理由 Flannel 处理。


下面还是以创建 2 个运行 BusyBox 镜像的 Pod 作为例子进行说明。


先给 Kubernetes 集群当中的两个 work node 打上 label 以方便将 Pod 调度到相同的节点上面进行测试:


[root@10-10-88-192 network]# kubectl get node --show-labelsNAME STATUS ROLES AGE VERSION LABELS10-10-88-170 Ready <none> 47d v1.10.5-28+187e1312d40a02 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/hostname=10-10-88-17010-10-88-192 Ready master 47d v1.10.5-28+187e1312d40a02 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/hostname=10-10-88-192,node-role.kubernetes.io/master=10-10-88-195 Ready <none> 47d v1.10.5-28+187e1312d40a02 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/hostname=10-10-88-195[root@10-10-88-192 network]#[root@10-10-88-192 network]# kubectl label --overwrite node 10-10-88-170 host=node1node "10-10-88-170" labeled[root@10-10-88-192 network]#[root@10-10-88-192 network]# kubectl label --overwrite node 10-10-88-195 host=node2node "10-10-88-195" labeled[root@10-10-88-192 network]#[root@10-10-88-192 network]# kubectl get node --show-labelsNAME STATUS ROLES AGE VERSION LABELS10-10-88-170 Ready <none> 47d v1.10.5-28+187e1312d40a02 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,host=node1,kubernetes.io/hostname=10-10-88-17010-10-88-192 Ready master 47d v1.10.5-28+187e1312d40a02 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/hostname=10-10-88-192,node-role.kubernetes.io/master=10-10-88-195 Ready <none> 47d v1.10.5-28+187e1312d40a02 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,host=node2,kubernetes.io/hostname=10-10-88-195[root@10-10-88-192 network]#
复制代码


创建两个 Pod 并通过添加 nodeSelector 使其调度到同一个节点(host1)。


编辑 Pod 的 yaml 配置文件:


[root@10-10-88-192 network]# lsbusybox1.yaml busybox2.yaml[root@10-10-88-192 network]# cat busybox1.yamlapiVersion: v1kind: Podmetadata:  name: busybox1  labels:    app: busybox1spec:  containers:  - name: busybox1    image: busybox    command: ['sh', '-c', 'sleep 100000']  nodeSelector:    host: node1[root@10-10-88-192 network]#[root@10-10-88-192 network]# cat busybox2.yamlapiVersion: v1kind: Podmetadata:  name: busybox2  labels:    app: busybox2spec:  containers:  - name: busybox2    image: busybox    command: ['sh', '-c', 'sleep 200000']  nodeSelector:    host: node1[root@10-10-88-192 network]#[root@10-10-88-192 network]#
复制代码


基于 yaml 文件创建 Pod:


[root@10-10-88-192 network]# kubectl create -f busybox1.yamlpod "busybox1" created[root@10-10-88-192 network]# kubectl create -f busybox2.yamlpod "busybox2" created[root@10-10-88-192 network]# kubectl get pod -o wideNAME READY STATUS RESTARTS AGE IP NODEbusybox1 1/1 Running 0 33s 10.244.2.207 10-10-88-170busybox2 1/1 Running 0 20s 10.244.2.208 10-10-88-170[root@10-10-88-192 network]#
复制代码


可以看到两个 Pod 都按照预期调度到了 10-10-88-170 这个节点上面。


通过 IP a 命令可以看到在 Pod 的宿主机上多出来了 2 个 vethXXX 样式的网络接口:


[root@10-10-88-170 ~]# ip a1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00    inet 127.0.0.1/8 scope host lo       valid_lft forever preferred_lft forever    inet6 ::1/128 scope host       valid_lft forever preferred_lft forever2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000    link/ether fa:35:b6:5e:ac:00 brd ff:ff:ff:ff:ff:ff    inet 10.10.88.170/24 brd 10.10.88.255 scope global eth0       valid_lft forever preferred_lft forever    inet6 fe80::f835:b6ff:fe5e:ac00/64 scope link       valid_lft forever preferred_lft forever3: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000    link/ether fa:88:2a:44:2b:01 brd ff:ff:ff:ff:ff:ff    inet 172.16.130.164/24 brd 172.16.130.255 scope global eth1       valid_lft forever preferred_lft forever    inet6 fe80::f888:2aff:fe44:2b01/64 scope link       valid_lft forever preferred_lft forever4: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN    link/ether 02:42:43:a1:fc:ad brd ff:ff:ff:ff:ff:ff    inet 172.17.0.1/16 scope global docker0       valid_lft forever preferred_lft forever5: flannel.1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UNKNOWN    link/ether 0e:c4:2c:84:a5:ea brd ff:ff:ff:ff:ff:ff    inet 10.244.2.0/32 scope global flannel.1       valid_lft forever preferred_lft forever    inet6 fe80::cc4:2cff:fe84:a5ea/64 scope link       valid_lft forever preferred_lft forever6: cni0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP qlen 1000    link/ether 0a:58:0a:f4:02:01 brd ff:ff:ff:ff:ff:ff    inet 10.244.2.1/24 scope global cni0       valid_lft forever preferred_lft forever    inet6 fe80::f0a0:7dff:feec:3ffd/64 scope link       valid_lft forever preferred_lft forever9: veth2a69de99@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue master cni0 state UP    link/ether 86:70:76:4f:de:2b brd ff:ff:ff:ff:ff:ff link-netnsid 2    inet6 fe80::8470:76ff:fe4f:de2b/64 scope link       valid_lft forever preferred_lft forever10: vethc8ca82e9@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue master cni0 state UP    link/ether 76:ad:89:ae:21:68 brd ff:ff:ff:ff:ff:ff link-netnsid 3    inet6 fe80::74ad:89ff:feae:2168/64 scope link       valid_lft forever preferred_lft forever39: veth686e1634@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue master cni0 state UP    link/ether 66:99:fe:30:d2:e1 brd ff:ff:ff:ff:ff:ff link-netnsid 4    inet6 fe80::6499:feff:fe30:d2e1/64 scope link       valid_lft forever preferred_lft forever40: vethef16d6b0@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue master cni0 state UP    link/ether c2:7f:73:93:85:fc brd ff:ff:ff:ff:ff:ff link-netnsid 5    inet6 fe80::c07f:73ff:fe93:85fc/64 scope link       valid_lft forever preferred_lft forever[root@10-10-88-170 ~]#
[root@10-10-88-170 ~]# brctl showbridge name bridge id STP enabled interfacescni0 8000.0a580af40201 no veth2a69de99 veth686e1634 vethc8ca82e9 vethef16d6b0docker0 8000.024243a1fcad no[root@10-10-88-170 ~]#
复制代码


此时两个 Pod 的网络连接如图所示:



网络包从 Container A 发送到 Container B 的过程如下:


  1. 网络包从 busybox1 的 eth0 发出,并通过 vethef16d6b0 进入到 root netns(网络包从 vEth 的一端发送后另一端会立马收到)。

  2. 网络包被传到网桥 cni0,网桥通过发送“who has this IP?”的 ARP 请求来发现网络包需要转发到的目的地(10.244.2.208)。

  3. busybox2 回答到它有这个 IP,所以网桥知道应该把网络包转发到 veth686e1634(busybox2)。

  4. 网络包到达 veth686e1634 接口,并通过 vEth 进入到 busybox2 的 netns,从而完成网络包从一个容器 busybox1 到另一个容器 busybox2 的过程。


对于以上流程有疑问的同学也可以自己动手验证一下结论,最好的方式就是通过 tcpdump 命令在各个网络接口上进行抓包验证,看网络包是如何经过网桥再由 veth pair 流转到另一个容器网络当中的。


Kubernetes 跨集群的网络连接

如今,Kubernetes 的部署实现了网络虚拟化,让容器可以在同一集群中的多个节点运行并相互通信。然而,越来越多的企业将 Kubernetes 用作为跨所有公有云和私有云基础设施的基础计算平台,可在不同的 Kubernetes 集群中运行的容器想要实现互相通信,实现的方法依然是传统的通过 ingress controller 或者节点端口来完成。


Rancher Labs 于上周(2019 年 3 月 12 日)推出了全新开源项目Submariner,支持多个 Kubernetes 集群之间的跨集群网络连接。Submariner 创建了必要的隧道和路径,能为部署在需要相互通信的多个 Kubernetes 集群中的微服务提供网络连接。


这一全新的解决方案解决了 Kubernetes 集群之间的连接障碍,为多集群部署提供了更多实现方式,例如在跨地区的 Kubernetes 内复制数据库,以及跨集群部署服务网格。


作者简介


叶龙宇,沃趣科技研发工程师, 主要负责公司基于 Kubernetes 的 RDS 平台 QFusion 的研发, 熟悉云计算及 Kubernetes 技术体系。


2020 年 5 月 14 日 22:25508

评论

发布
暂无评论
发现更多内容

技术根儿扎得深,不怕“首都”狂风吹!

Arvin

操作系统

AI、IoT、区块链、自主系统、下一代计算五大技术引领未来供应链发展

京东科技开发者

区块链 AI IoT 供应链

微服务容错时,这些技术你要立刻想到

华为云开发者社区

微服务 线程 服务雪崩 断路器 服务降级

区块链落地商品溯源-区块链商品溯源方案

135深圳3055源中瑞8032

如何养成一个好习惯

熊斌

读书笔记 28天写作

开发质量提升系列:用户体验

罗小龙

最佳实践 方法论 28天写作

个人隐私之老话重谈

张老蔫

28天写作

IDEA 异常退出 解决方法

任广印

IDEA

甲方日常 91

句子

工作 随笔杂谈 日常

我们都很忙

Ian哥

28天写作

CSS实现数据统计

学习委员

前端 CSS小技巧 28天写作 纯CSS

《论雨伞道德》- 不要和自己的良心捉迷藏

石云升

读书笔记 28天写作 雨伞道德

Soul 源码阅读 05|Http 长轮询同步数据分析

哼干嘛

音视频行业不可或缺的功能-云端录制

anyRTC开发者

音视频 WebRTC 在线教育 直播 RTC

代码 or 指令,浅析ARM架构下的函数的调用过程

华为云开发者社区

函数 任务栈 arm架构

面对key数量多和区间查询低效问题:Hash索引趴窝,LSM树申请出场

华为云开发者社区

数据库 数据 存储 Hash索引 LSM树

智慧平安小区APP,社区管理一体化平台

135深圳3055源中瑞8032

区块链电子票据应用,区块链数字票据平台

135深圳3055源中瑞8032

Volcano 监控设计解读,一看就懂

华为云开发者社区

Kubernetes 云原生 监控 Volcano 计算

美国大选期间美股迎来大涨,舆情到底有何魔力?

星环科技

人工智能 人工智能大数据

前端知识总结输出文章目录大全

梁龙先森

JavaScript 前端 编程语言 28天写作

机器学习应用设计阶段的 10 个陷阱和 11 个最佳实践

浪潮云

机器学习

前端模拟假数据(json-server光速入门篇)

学习委员

json 前端 Node 28天写作 json-server

区块链作用之数字货币的影响

v16629866266

数据中台:建立在数据网络效应之上的赛道

奇点云

大数据 数据中台 云原生 数据

架构师训练营知识点思维导图

陈浩

架构师训练营第2期

Vue3 中 v-if 和 v-show 指令实现的原理 | 源码解读

五柳

源码分析 前端 Vue3

漫谈HTTP协议

架构精进之路

HTTP 七日更 28天写作

工程师思维是什么?能吃吗?

Justin

工程师思维 架构设计 28天写作

你会读书吗?

xcbeyond

读书感悟 读书方式 28天写作

HTML5中的拖放功能

魔王哪吒

html html5 程序员 面试 前端

移动应用开发的下一站

移动应用开发的下一站

Kubernetes网络全解:机制、方法、实操的超强指南-InfoQ