这是我在国外的博客上看到的一系列关于Docker和Kubernetes网络分析的文章,感觉描述得比较清晰,便于刚接触Docker和Kubernetes的朋友了解相关的知识。在看的同时顺便就翻译了,方便和网友一起交流探讨。但我不仅仅是翻译,还按照文章中描述的进行了实际测试,对于测试中出现的问题和由于环境不同或者软件版本更新带来的变更也在译者注中给出说明。
我并没有按原文一字一句的翻译,对于理解这些知识点没有帮助的语句我一般都忽略了。
我的测试环境和笔者不同,我使用的是单台物理机,用Vagrant模拟多台VM Host,VM Host使用和物理机相同网段的public网络,操作系统是CentOS 7.0,Docker版本1.6.2。Kubernetes网络的要求是不同主机上的容器间要能够互通。
本文使用了MLS来打通不同机器Docker私网的通讯。我尝试用家用路由器的静态路由完成,但是因为Vagrant网络设置原因,一直没有成功。我还在分析,丛当前情况看是Vagrant每个Vagrant虚拟机有两个网络接口,其中一个管理接口每台机器的IP是一样的,我估计这个产生了干扰,因为我抓的不同机器的IP包都转换成为了物理机器的IP地址而不是各自虚拟机的IP地址。所以在我的实验中,我使用了Flannel来打通不同主机间容器网络的连接问题。这个对很多场景是更好的选择,很多情况下你很难在为新增一个Kubernetes的主机就在交换机上去增加一条路由。而且牵涉到虚拟机和虚拟网络时,难定位的问题会更多。
我对Docker和其相关技术如此感兴趣的一个原因是因为随之而来的新的网络模式。Kubernetes有一个独特的(并且相当棒)方式来处理这些网络挑战,但是他第一眼看上去有些难以理解。我这个帖子目的是带你通过部署几个Kubernetes的construct来分析Kubernetes在网络层做了什么来达成这点。不过让我们从部署一个Pod的基础开始。我们将使用第一个帖子(Kubernetes入门(一) - 构建)构建的实验环境和在第二个帖子(Kubernetes入门(二) - Constructs)创建的一些配置文件例子。
注意:我必须在这里指出这个实验环境是基于裸机硬件的。在这个类型的实验环境的网络模型和你见到的云提供商提供的相比有些许不同。尽管如此,Kubernetes在网络层面所做的事的背后的机制是一样的。
我们的实验环境是这样的:
之前我们提到了Pod IP地址的话题,让我们提供一些背景信息,对情况有一些基本的了解。Kubernetes的网络模型决定了Kubernetes的每个节点可以被路由。回忆一下docker网络模型,他提供了一个IP地址段为172.17.0.0/16的docker0网桥。每个容器可以从这个子网获得IP地址并用docker0网桥的IP(172.17.42.1)做为其默认网关。值得注意的是网络不需要知道172.17.0.0/16如何到达,因为docker主机为从容器发出的流量在真实的NIC的IP地址背后做了一个IP masquerade(或者hide NAT)。这就是说网络将任何从容器来的流量看做是从主机物理IP地址发出的。
注意:我们在这个帖子里面使用网络总是指连接主机的物理网络。
Docker的网络模型对使用方便是有意义的,但并不理想。这个模型需要对各种端口进行映射并且一般会限制Docker主机的能力。在Kubernetes模型中,每个Docker主机的docker0网桥都是可以路由的。 那就是说,当一个Pod部署后,集群其他主机能够不在物理主机上做端口映射就可以直接访问Pod。有了这种说法,从网络观点来看,你可以将Kubernetes节点当做路由器。我们将我们的实验环境图变为一个网络图,他看起来就像这样。
多层交换机(MLS)有两个3层分段。一个支持10.20.30.0/24网络,另一个支持192.168.10.0/24网络。此外上面还有路由告诉如何到达每个挂在路由器(Kubernetes节点)上的子网。这意味着任何节点上产生的容器会使用节点(docker0网桥IP)做为他们的默认网关,接着节点使用MLS做为他的默认网关。我有点和这个概念纠缠,不过他非常重要。网络管理员喜欢边缘三层网络。
现在让我们转到一些例子来看在不同情况下Kubernetes在网络侧是如何做的...
部署一个Pod
id: "webpod"
kind: "Pod"
apiVersion: "v1beta1"
desiredState:
manifest:
version: "v1beta1"
id: "webpod"
containers:
- name: "webpod80"
image: "jonlangemak/docker:web_container_80"
cpu: 100
ports:
- containerPort: 80
hostPort: 80
- name: "webpod8080"
image: "jonlangemak/docker:web_container_8080"
cpu: 100
ports:
- containerPort: 8080
hostPort: 8080
labels:
name: "web"
译者注: 帖子是笔者2015年初写的,所以这个construct的语法已经过时了,在我测试使用的比较新的v0.19.0版本上已经不能运行,我重新用最新的construct语法重写了一下:
kind: "Pod"
apiVersion: "v1"
metadata:
labels:
name: "web"
name: webpod
spec:
containers:
- name: "webpod80"
image: "jonlangemak/docker:web_container_80"
resources:
limits:
cpu: "0.5"
ports:
- containerPort: 80
hostPort: 80
- name: "webpod8080"
image: "jonlangemak/docker:web_container_8080"
resources:
limites:
cpu: "0.5"
cpu: 100
ports:
- containerPort: 8080
hostPort: 8080
让我们假设我们在一个空白的master上进行工作。我清理了上个帖子用到的所有Replication controller、Pods和Service...
让我们找一个节点检查一下这个点的运行情况,现在让我们选择kubminion1。
现在没有任何容器在运行,我只想指出网络配置符合预期。我们有一个docker0网桥接口和minions本地IP接口。让我们回到master,部署我们上面配置的pod,看看会发生什么...
一些有趣的事情发生了,Kubernetes指派10.20.30.62(kubminion1)这台主机运行Pod。注意Pod也分配了一个IP地址,这个地址正位于kubmini1的docker0网桥。让我们拎出kubminion1来看看发生了什么。
kuminion1现在运行了3个容器。我们的Pod规格里只定义了2个,那么第三个从哪里来的?第3个运行的容器镜像叫"kubernetes/pause:go"。注意到这个容器有所有端口映射。为什么会这样?我们深入观察一下容器看看为什么会这样。我们使用Docker的"inspect"命令看看每个容器的一些信息。也就是说看每个部署的容器的网络模式。
译者注:现在这个镜像默认是放在GCE的镜像仓库里了,gcr.io/google_containers/puase:0.8.0,不幸的是Google的服务器都被墙了,要么,要么将这个镜像重新指向到Docker公共仓库。这个镜像在公共仓库里就是kubernetes/puase:0.8.0。在kubelet的启动参数里面指定:
有趣,如果我们检查每个容器的网络模式,我们可以看到一个非常有趣的配置。第一个运行"kubernetes/pause:go"的容器使用的是默认的网络模式。我们观察的第二和第三个容器是在我们pod中定义的运行"web_container_80"和"web_container_8080"镜像的容器。注意每个pod容器都有一个非默认网络配置。特别是每个pod容器使用mapped container mode并指定运行"Kubernetes/pause:go"镜像的容器为目标容器。
译者注:mapped container mode参见Docker网络入门(三) - 镜像容器
让我们想一想,为什么他们这样做?首先,所有在一个pod内的容器需要共享相同的IP地址。这使得镜像容器模式必不可少。既然如此,为什么他们不启动第一个pod容器,并将第二个pod容器连接到第一个?我想这个问题的答案有两个方面。第一,连接一个有两个以上容器的pod很痛苦(译者注:这句话有些不太清楚,我觉得作者可能的意思是一个pod有多个容器,你不知道哪个是第一个容器,有一个pause容器就确定了)。其次,你对被连接的第一个容器产生了依赖。如果第二个容器连接到第一个容器并且第一个容器挂掉,第二个容器的网络栈也挂了。运行一个基础容器,将其他的pod容器连接到这个容器更加容易(也更加聪明)。这也简化了端口映射,我们只需要将端口映射规则应用到pause容器。
于是我们的pod网络图看上去就像这样...
所以真实网络对于pod IP流量的目的地就是这个pause容器。上面这个图有一些欺骗性因为他将pause容器显示为将80和8080端口转发到相关容器。pause容器并没有真的做这个转发,这是逻辑上像这样做了因为两个容器直接监听了两个端口并与pause容器共享了相同的网络栈。这就是为什么所以对真实容器的端口映射都显示为在pause容器上的映射。我们通过
docker port
命令检查。
所以pause容器真的只是持有pod的网络端点(Endpoint) ,没有做任何其他事情。那么主机做了什么?他做了什么事情将流量导入到pause容器吗?让我们看一下iptables规则:
这里有一些规则,但是没有任何规则是作用于我们刚才定义的pod的。就像我在上一篇帖子提到的,在每一个Kubernetes节点上Kubernetes提供了一些默认服务。那就是我们在上面输出中看到的。关键点是我们没有看见任何masquerade规则或者任何为pod10.10.10.2做的入站端口映射。
部署一个服务
我们已经看到Kubernetes如何处理连接到他的最基本的构件,让我讨论一下他如何处理服务。就像我们在上个帖子讨论的那样,服务允许你抽象在pod里托管的服务。此外服务允许你通过提供跨托管服务的多个Pod的负载均衡机制来提供服务的水平扩展能力。让我再次将我们刚才创建的pod删除重置实验环境以确保从空白开始
现在让我们尝试我们在上个帖子中定义的服务并再次对其审视。 这里是我们称为"myfirstservice“的服务配置文件
id: "webfrontend"
kind: "Service"
apiVersion: "v1beta1"
port: 80
containerPort: 80
selector:
name: "web"
labels:
name: "webservice"
为了让事情解释得更清晰,我们将服务做一些轻微的改变。
id: "webfrontend"
kind: "Service"
apiVersion: "v1beta1"
port: 80
containerPort: 8080
selector:
name: "web8080"
labels:
name: "webservice"
相同的配置,我们只是将容器端口变为了8080。让我们在Kubernetes集群定义这个服务。
译者注: 帖子是笔者2015年初写的,所以这个construct的语法已经过时了,在我测试使用的比较新的v0.19.0版本上已经不能运行,我重新用最新的construct语法重写了一下:
apiVersion: v1
kind: Service
spec:
ports:
- port: 80
containerPort: 8080
selector:
name: web8080
metadata:
labels:
name: webservice
name: webfrontend
注意:我不记得我以前是否提到过,但是服务必需在符合服务的selector的pod之前构建。这个能保证服务相关的变量能存在于容器中。
译者注:Kubernetes容器访问服务有两种方式:
1.一种是通过环境变量访问,服务的IP和端口在容器启动时做为环境变量传递进入容器,容器的应用可以根据这些环境变量访问服务,例如访问redis或者mysql。这样就要求服务先启动,否则服务的相关信息就没法传入Pod(因为此时服务还没有创建,这些信息都还没有)。这是一种比较糟糕的方法,对顺序有依赖且要求应用要适配环境变量。其中环境变量的规则是"<服务名称(大写)>PORT<服务端口>TCP_ADDR"这个环境变量的值是服务的IP,"<服务名称(大写)>_PORT<服务端口>_TCP_PORT"这个环境变量是服务端口。例如访问上面的服务就用WEBFRONTEND_PORT_8080_TCP_ADDR和WEBFRONTEND_PORT_8080_TCP_ADDR两个环境变量
2.另一种是通过dns访问,这显然是比较好的办法,在后面的帖子中会提到如何在Kubernetes集群中部署用于服务访问的本地DNS。这样服务直接就可以通过服务名访问,由DNS将服务名转变为服务的IP,不再需要依赖环境变量,只要依赖服务名称即可。如: curl -L http://webfrontend
服务的创建符合预期。我们检查可用的服务时会发现集群给服务指定了一个IP地址。这个IP地址被分配到了Kubernetes所说的"Portal Netwokr”范围内。如果你还记得,当我们在kubmasta上构建API服务时,我们定义的一个标识就是"PortalNet"
[Unit]
Description=Kubernetes API Server
After=etcd.service
Wants=etcd.service
[Service]
ExecStart=/opt/kubernetes/kube-apiserver \
--address=0.0.0.0 \
--port=8080 \
--etcd_servers=http://127.0.0.1:4001 \
--portal_net=10.100.0.0/16 \
--logtostderr=true
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
他可以是任意子网,只要不和物理主机或者docker0的子网重合即可。他可以是任意子网的原因是他永远不会路由到网络上。Portal net只对本地节点有意义,他只是一个将流量从容器流出并导向默认网关(docker0网桥)的方法。在下一步动作之前,让我们检查一下kubminion1看看我们定义了服务定义后发生了什么改变。我们从检查netfilter规则开始:
这些规则做了什么?第一行告诉主机匹配10.100.87.105:80的TCP流。如果他看到一个流匹配到这个规范,他会将流量重定向到本地的39770端口。第二个行告诉节点以不同的方式做同样的事情,因为覆盖的是从主机而不是容器产生的流量。这两条规则不相同的原因是REDIRECT只对通过主机的流量生效(产生于容器,通过主机)。从主机产生的流量需要用DNAT规则进行处理。长话短说,他们用不同的方式完成同样的事情,于是所有流出节点指向10.100.87.105:80的流量会被重定向到主机的39770端口
于是我们知道所有去向服务IP和端口的流量会重定向到本机的39770端口。但那又什么效果呢?这是kubernetes-proxy服务开始起作用的地方。proxy服务为每个新创建的服务随机分配了一个端口并创建了一个服务内创建一个负载均衡对象并监听指定的端口。在这个场景里,端口敲好是39770。如果当我们创建服务时我们查看了kubminion1上kubernetes-service的日志(译者注:感觉是kubernetes-proxy service),我们会看到像这样的条目:
现在指向服务的流量被重定向到了proxy。为了完成负载均衡,我们启动我们上个帖子的一个replication controller以便于我们查看其运行情况。我将为我的replication controller用这个配置:
id: web-controller-2
apiVersion: v1beta1
kind: ReplicationController
desiredState:
replicas: 4
replicaSelector:
name: web8080
podTemplate:
desiredState:
manifest:
version: v1beta1
id: webpod
containers:
- name: webpod
image: jonlangemak/docker:web_container_8080
ports:
- containerPort: 8080
labels:
name: web8080
译者注
新版的construct应该这样写:
apiVersion: v1
kind: ReplicationController
metadata:
labels:
name: web8080
name: web8080
spec:
replicas: 4
selector:
name: web8080
template:
metadata:
labels:
name: web8080
spec:
containers:
- name: web8080
image: jonlangemak/docker:web_container_8080
ports:
- containerPort: 8080
name: web8080
让我们将其加载到集群并让所有pod启动
看上去不错。现在我们让所有的pod运行起来了,服务会选择匹配标签"web8080"的pod进行负载均衡。因为replication controller selector通过标签"web8080"匹配所有的pod,我们将有4个pod用于负载均衡。在这一点上,我认为我们的实验环境看起来像这样:
Kubernetes proxy被描述为一种垫片,他只是运行在节点上的另外一个服务。我们上面看到的规则让Kubernetes proxy成为了流量指向服务IP地址的垫片。
看看他的运行情况,我们使用tcpdump进行一系列包捕捉。 为了做这件事,我们需要在kubminion1上安装tcpdump,我们使用这个命令进行安装:
yum -y install tcpdump
完成安装后,我们打开三个kubminion1的SSH会话。第一个窗口我们运行如下tcpdump指令
tcpdump -nn -q -i ens18 port 8080
注意:在这个例子里我们会捕捉物理接口的包。在我的例子里,他叫"ens18" (译者注:译者使用的环境是通过flannel来打通跨主机容器网络的连接,所以是对flannel0网桥进行抓包)
在第二个窗口我们运行另外一个tcpdump,但是我们需要首先获得一些信息。换句话说我们需要获得容器连接到docker0网桥的虚拟接口的名称。假设你在主机上运行了一个webpod容器,你可以完成简单的"ifconfig",你将只有一个"Veth"接口。
拷贝接口的名字,在你的第二个窗口将其拷贝到tcpdump命令。
tcpdump -nn -q -i veth12370a6 host 10.100.87.105
同时运行两个命令,将窗口叠在一起,这样你可以同时查看。
你同时运行了两个抓包后,让我们将注意力转移到第三个窗口。让我们使用"docker exec"命令附着到“web_container_8080”容器(首先使用“docker ps”命令获取容器的名称)
docker exec -it e130a52dfae6 /bin/bash
在进入运行容器后我们尝试通过curl命令访问服务
curl 10.100.87.105
当我们第一次访问服务,我们在窗口里会看到:
这说明了什么?让我们在我们的网络图上画出来,上面窗口的抓包(通过物理NIC的流量)用红色显示,下面窗口抓包(通过docker0网桥)用蓝色显示:
注意:我在画kubminio3上的线的时候绕过了Kubernetes proxy。 我这样做是因为在kubminion3上的Kubernetes proxy并没有对这个流起作用。换句话说,proxy service拦截了服务请求直接发送到他负载均衡的pod。
让我们先看下方的窗口,我们看到的是从容器角度出发的流量。容器企图建立一个10.100.87.105:80的TCP连接。我们看到丛服务的IP地址10.100.87.105返回的流量。丛容器的角度来看,整个通讯是与服务进行的。如果我们看第二个窗口(上面那个)我们能看到实际发生了什么。我们看到丛节点的物理地址(10.20.30.62)发出的到托管在kubminion3(10.10.30.4)上的pod的TCP会话。总结一下,Kubernetes proxy服务做为一个全代理维护了两个不同的连接。第一个是丛container到proxy的,第二个是丛proxy到负载均衡目的地的。
如果我们清理我们的抓包信息并再次运行curl,我们会看到流量负载均衡到另一个节点。
在这个例子里,Kubernetes proxy决定将流量流到运行在kubminion2的pod。我们的网络流图看起来像这样。
我想你不需要我再展示其他两种负载均衡我们测试服务的可能的结果了。关于理解服务很重要的一点是服务能让我们方便的扩展服务的pod。 我们可以看到结合使用replication controller一起,这会是一个强大的特性。
但是服务处理了Kubernetes集群一个重要方面的同时,他们却只对pod访问服务有意义。回忆一下,portal IP的空间不能丛外部网络访问,他只对于本地主机有意义。那么集群之外如何访问部署在集群中的应用呢?我们在下一个帖子中说明。