k8s接触一段了,似懂非懂的时候来了,自己喜欢直接上手先搂的那种,然后回头在来看的话很多疑问会迎刃而解,目前对k8s网络方面的知识有些凌乱,就此整理,以下问题待解决:

  • pod ip、cluster ip如何实现
  • k8s中的服务如何暴露

这主要是一个概念、原理的梳理过程,其中的代码以及图均参考其他技术文章,主要翻译了

这三篇文章很好,不过自己水平有限,可能有的地方不准,便于自己理解,也有修改和省略,如果有幸你看到了这里,建议看原文:)
其中也参考了其他的技术文章,在文末有一一列出.

Pod

Pod是k8s中的一个逻辑概念,一个pod中包含了一个或一组容器,一个pod中的容器运行在一个host上,并且容器共享网络栈以及其他资源,比如volumes。pod是k8s中构建应用的基础单位了。
Pod中的容器怎样共享网络栈以及volume等资源的呢?我们创建一个pod试试看:

1
2
3
4
5
6
$ kubectl run busybox0 --image=busybox --command -- sleep 3600
deployment "busybox0" created
$ kubectl get pods --all-namespaces
...
default busybox0-7665ddff5d-xsxfv 1/1 Running 0 51s
...

在pod运行的主机上查看启动的容器:

1
2
3
$ docker ps -a | grep busybox0-7665ddff5d-xsxfv
a0b7d191219e busybox "sleep 3600" 52 seconds ago Up 51 seconds k8s_busybox0_busybox0-7665ddff5d-xsxfv_default_2d9b8bdf-5421-11e8-b5a8-080027f2276d_0
c58246abffae k8s.gcr.io/pause-amd64:3.1 "/pause" About a minute ago Up 59 seconds k8s_POD_busybox0-7665ddff5d-xsxfv_default_2d9b8bdf-5421-11e8-b5a8-080027f2276d_0

发现除了运行应用的容器以外还有一个pause容器,这个和pod的网络有什么关系呢?回忆(谷歌)了下,docker网络模式有:

  • none
  • host
  • bridge
  • container
  • user-defined

这里之前用的最多的就是host和bridge了,其他没怎么了解过,实际上每个容器都有自己的网络命名空间,通过container的网络模式可以使几个容器共享一个容器的网络命名空间,那我们刚才的pause就是用来为pod中的其他容器提供网络命名空间以及共享volume等资源的容器,通过这样一种方式,一个pod从外部来看就是一个整体了,内部的容器共享一个网络命名空间或通过localhost就能够通信。

Pod Network

Kubernetes另外一个很爽的地方就是无论pod在集群的哪个节点上,都能够互相通信访问。
抛开k8s,先以两个普通的容器环境为例:

两个node分别是10.100.0.2,10.100.0.3,两个node通过eth0通信,默认网关10.100.0.1。
先看左边的node,docker0网桥连接eth0与veth0,veth0是pause容器创建的,在一个pod内,共享给其他容器,如图,container1和container2以及pause容器共享veth0网络栈,网桥创建的时候本地路由表被设置为经过eth0,dest地址为172.17.0.2的包都会转发到docker0网桥上,通过网桥转发给veth0,也就到达了pod。
这个例子中选择了一个特例,可以发现右边的node与左边是一样的,默认情况下配置docker后,网桥等网络配置在各个主机上是一致的,即使不同,节点间也不知道其他节点中docker0网桥的分配状况,但如果包想要正确的发送过去,这点又是必须的。
kubernetes为了解决这个问题,首先,k8s为每一个节点上的bridge分配了一个全局的地址空间,然后各个节点的bridge从这个空间中分配地址。第二,k8s会在网关10.100.0.1添加相应的路由表,明确到达各个节点的bridge的包该如何路由。这样通过virtual network interface,bridge以及路由表的组合叫做overlay network。k8s就是通过这样的overlay网络使各个节点的pod互相通信。
k8s中overlay网络的情况如下图:

这里将docker0改成了cbr0(custom bridge),在k8s环境中这是一个与默认的docker节点不同的地方,这个例子中节点的bridge地址空间为10.0.0.0/14。

Service

Pod本身是并不是一个持久的资源,因此直接使用pod的ip是不合理的,那怎么访问pod呢,最基本的方法就是通过反向代理或者load balance了,不过使用proxy的话要满足几个条件:

  • 持久的
  • 能够知道需要转发的server的列表
  • 能够知道server的健康状况,并能够相应请求

k8s通过service的方式来实现。
service如何来实现的,我们先通过deployment创建两个pod:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
kind: Deployment
apiVersion: extensions/v1beta1
metadata:
name: service-test
spec:
replicas: 2
selector:
matchLabels:
app: service_test_pod
template:
metadata:
labels:
app: service_test_pod
spec:
containers:
- name: simple-http
image: python:2.7
imagePullPolicy: IfNotPresent
command: ["/bin/bash"]
args: ["-c", "echo \"<p>Hello from $(hostname)</p>\" > index.html; python -m SimpleHTTPServer 8080"]
ports:
- name: http
containerPort: 8080

两个pod都启动了一个简单的http server,通过8080端口来返回hostname,通过kubectl可以查看pod的状态以及ip:

1
2
3
4
5
6
7
$ kubectl apply -f test-deployment.yaml
deployment "service-test" created
$ kubectl get pods
service-test-6ffd9ddbbf-kf4j2 1/1 Running 0 15s
service-test-6ffd9ddbbf-qs2j6 1/1 Running 0 15s
$ kubectl get pods --selector=app=service_test_pod -o jsonpath='{.items[*].status.podIP}'
10.0.1.2 10.0.2.2

可以创建一个简单的客户端发出请求进行测试,client的pod如下:

1
2
3
4
5
6
7
8
9
10
11
apiVersion: v1
kind: Pod
metadata:
name: service-test-client1
spec:
restartPolicy: Never
containers:
- name: test-client1
image: alpine
command: ["/bin/sh"]
args: ["-c", "echo 'GET / HTTP/1.1\r\n\r\n' | nc 10.0.2.2 8080"]

创建这个pod并查看输出:

1
2
3
4
$ kubectl logs service-test-client1
HTTP/1.0 200 OK
<!-- blah -->
<p>Hello from service-test-6ffd9ddbbf-kf4j2</p>

可以正常返回结果,这里我们只用client访问了俩个pod中的一个,但是如果这个server pod恰好挂了,重启了,server pod的ip就很可能会改变,client就不能通过原来的ip获取响应了,而这种状况在实际环境中会经常遇到的。所以如果有一个方法能保证server端的服务ip不变就好了,这就是service的功能啦。

通过k8s中的service,可以将请求转发给一组pod。service与pod的匹配是通过selector选择相应的label的pod方式实现的。先创建一个service:

1
2
3
4
5
6
7
8
9
10
kind: Service
apiVersion: v1
metadata:
name: service-test
spec:
selector:
app: service_test_pod
ports:
- port: 80
targetPort: http

当service创建以后,可以看到service会分配一个ip在80端口接受请求。

1
2
3
$ kubectl get service service-test
NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service-test 10.3.241.152 <none> 80/TCP 11s

client pod可以通过service的ip来直接访问。也可以直接使用service的name进行访问,k8s内部的dns会自动解析。我们将client端调整下:

1
2
3
4
5
6
7
8
9
10
11
apiVersion: v1
kind: Pod
metadata:
name: service-test-client2
spec:
restartPolicy: Never
containers:
- name: test-client2
image: alpine
command: ["/bin/sh"]
args: ["-c", "echo 'GET / HTTP/1.1\r\n\r\n' | nc service-test 80"]

client通过service的那么进行访问,创建后查看输出:

1
2
3
4
$ kubectl logs service-test-client1
HTTP/1.0 200 OK
<!-- blah -->
<p>Hello from service-test-6ffd9ddbbf-kf4j2</p>

可以看到client通过service能够正常访问server端了。

Service Network & kube-proxy

上面可以看到创建service后分配了一个ip:10.3.241.152,但这个ip不是和pod一个网段的,对比一下:

1
2
3
4
5
thing        IP               network
----- -- -------
pod1 10.0.1.2 10.0.0.0/14
pod2 10.0.2.2 10.0.0.0/14
service 10.3.241.152 10.3.240.0/20

service的ip也不是和node一个网段的,到这基本能够知道service的ip是一个独立的网络,暂且叫service network,这个service的ip叫做ClusterIP。通过ClusterIP,集群中的任何pod都能够访问到service。

但与node本身的IP以及pod本身的IP,ClusterIP有很大的不同,从前面可以看到,pod的IP是分配在一个虚拟设备上的vethx上的,node的IP是分配到ethx,一个实际的网络设备的,包括网桥,这些ip都分配到一个设备上,无论是虚拟的还是物理的。但ClusterIP的不同之处在于通过ifconfig命令查看网络设备也好,或者查看路由表也罢,都没有service network的相关内容,从这个角度来看的话,service network是不存在的,至少没用网络设备与之对应。但从上面的操作看,我们的请求service IP最终确实到了server pod上,也有了正确的返回。

以这个场景为例:

首先从clinet pod发出请求,到达veth1,请求的ClusterIP在pod的network中是找不到目的地址的,这种状况设备会把包向上一级网关转发,通过cbr0网桥(网桥只是转发)到达node的eth0,实际上node network也是找不到ClusterIP目的地址的,其实这样推下去,到顶层的网关也找不到,不过从我们的操作结果来看,如图所示,最终包是到达了server端的,但包是怎样实现的呢?答案是kube-proxy。

kube-proxy如何搞定这个事情的呢?如果从名字来看kube-proxy是一个代理功能的组件,但和haproxy等代理不同,一般代理会收到client请求然后转发到server端,这需要代理有一个interface监听client以及server的连接。例子中的环境有两个interface可以使用,一个是node的interface,一个是pod的interface。但是这两个都没有被使用(原作者猜测是因为设计的时候pod和node都不是长期稳定的特性,这样会导致路由表更加复杂,难以维护)。而且我们从上面的分析可以看到这两个interface也都没有使用,实际kube-proxy最终使用的是netfilter来实现的,linux kernel中这个模块叫netfilter,用户空间中就是iptables了。

如上图为例,kube-proxy本地打开了10400端口监听发送给service的请求,并插入了netfilter规则,将目标地址是service IP的包重新路由给kube-proxy,然后再将这个包转发给pod的8080端口。这样从client pod的请求就能够通过ClusterIP顺利到达server pod了。k8s的1.2以后,kube-proxy是在iptables模式下运行的。转发工作游netfilter来完成,kube-proxy基本上只是与其进行同步规则的功能,因此整个流程目前如下:

不过kube-proxy只是用于集群内部的通信,如果想从外部访问集群的服务需要使用其他的方式了。

NodePort

我们使用client发出请求到server端,其中产生的connection或者是request,在OSI模型中属于4层或者7层,而中间包的转发都是通过路由和netfilter完成的,这属于3层。所有的路由,netfilter在做路由选择的时候基本都是基于包的信息来做判断的,这个包是哪发来的,要到哪去。我们上面在两个node上创建了server pod,为server端创建了相应的service(ClusterIP:10.3.241.152),因此我们访问这个service的时候,只需要让每一个node的eth0接收到目的地址是ClusterIP的80端口的包即可,随后netfilter就会自动匹配规则进行转发,最终到达相应的server pod。

如果外部的client想访问集群内部的service的时候,能够访问ClusterIP+port就应该可以了。但这里有一个问题,service的ClusterIP只能在集群内部访问到,外部无法获知service network的分配情况。当然可以通过iptables来添加规则,使得访问service的ClusterIP+port的流量转发到指定的node的eth0设备上,比如下图:

但这里面还是存在问题,node本身不是持久稳定的,如果node迁移了,或者集群scale up/down了,导致这个node不能提供服务了,路由会中断一段时间。及时node能够长期有效的提供服务,流量都走在一个node上也不是合理的方案。

所以一个合理的方案应该是不会受集群内部单点服务状态的影响的。如果想把外部流量合理的分配到后端服务的话其实已经有很成熟的工具了,负载均衡器,其实后面说的k8s的ingress也正式使用了负载均衡器方式来实现的。

使用load balancer将外部流量分发到集群中的节点上,需要LB有一个外部能够访问到的公共IP,并且保证LB能够将请求转发到node上;基于上面的分析我们可以知道不能轻易使用ClusterIP配置网关路由器到node的静态规则。所以目前能够使用的只有node的以太网接口eth0了,例子中也就是10.100.0.0/24.可以看到网关路由器已经知道了如何将包转到node上了,但如果我们是想将包正确的发到ClusterIP的80端口的,这通过node的eth0是做不到的。因为没有服务在node的ip上,如:10.100.0.3监听80端口,只能够匹配10.3.241.152:80.所以如果直接这样转发到node上是会失败的,如下图:

目前我们面对的难题是:网关到达node的网络与netfilter转发的网络不是一个网络。这中间缺乏了一个桥梁,所以k8s使用NodePort来搞定。

之前创建的service没有指定type,默认的type是ClusterIP。还有另外两个可以使用的type,一个就是NodePort,如:

1
2
3
4
5
6
7
8
9
10
11
kind: Service
apiVersion: v1
metadata:
name: service-test
spec:
type: NodePort
selector:
app: service_test_pod
ports:
- port: 80
targetPort: http

使用Nodeport的时候,kube-proxy会在eth0上打开一个端口,默认在3000-32767之间,node上发到到这个端口的请求都会转发给对应的service。创建上面的service后可以看到分配的NodePort:

1
2
3
$ kubectl get svc service-test
NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service-test 10.3.241.152 <none> 80:32213/TCP 1m

LoadBlancer

但是NodePort是非常有限的资源,不过这已经是一个非常基础的实现方式了。而且这个方案需要在前端加一个LB,所以我们继续向下看。
service的另一个type是LoadBalancer,这个type的service可以认为是一个NodePort加上一个ingress路径的组合。前提是k8s集群跑在GCP/AWS这样的支持API驱动的网络资源配置的环境。
创建如下service:

1
2
3
4
5
6
7
8
9
10
11
kind: Service
apiVersion: v1
metadata:
name: service-test
spec:
type: LoadBalancer
selector:
app: service_test_pod
ports:
- port: 80
targetPort: http

原作者在GCP环境下进行的操作,查看一下service:

1
2
3
$ kubectl get svc service-test
NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE
openvpn 10.3.241.52 35.184.97.156 80:32213/TCP 5m

可以看到service申请到了一个external-ip,实际上在GCP中不只是申请了一个external-ip,而是一个完整的load-balancer,包括转发规则,代理,后端服务等。一旦分配external-ip以后,就可以使用这个ip来访问服务了。但是LoadBlancer的service不能够拦截https流量,不能根据virtual-host和path进行设置转发规则,也不能同时配置多个service。由于这些限制,所以在k8s1.2以后有了ingress。

Ingress

上面我们分析得到LoadBalancer的服务有一些限制,所以才有了ingress。k8s中的Ingress由两部分组成,一个是ingress资源,一个是ingress controller,通过创建(或者说定义)ingress资源告诉controller,需要创建怎样的转发规则,而controller就会根据创建的ingress资源来实现相应的规则,达到我们的目的。

创建一个ingress资源,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: my-ingress
spec:
rules:
- host: www.mysite.com
http:
paths:
- backend:
serviceName: website
servicePort: 80
- host: forums.mysite.com
http:
paths:
- path:
backend:
serviceName: forums
servicePort: 80

这个ingress表明需要将www.mysite.com以及forums.mysite.com分别转发到k8s的service:websiteforums

但是单纯的ingress资源只是做了这样的一个定义,没有任何实际的操作,需要相应的ingress controller实现,理论上任何有反向代理功能的系统都能够作为ingress controller。最常用的就是nginx,下面的一个完整的nginx ingress controller的例子,例子中ingress controller使用了LoadBalancer service,如果环境不支持的话可以使用NodePort代替。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
kind: Service
apiVersion: v1
metadata:
name: ingress-nginx
spec:
type: LoadBalancer
selector:
app: ingress-nginx
ports:
- name: http
port: 80
targetPort: http
- name: https
port: 443
targetPort: https
---
kind: Deployment
apiVersion: extensions/v1beta1
metadata:
name: ingress-nginx
spec:
replicas: 1
template:
metadata:
labels:
app: ingress-nginx
spec:
terminationGracePeriodSeconds: 60
containers:
- image: gcr.io/google_containers/nginx-ingress-controller:0.8.3
name: ingress-nginx
imagePullPolicy: Always
ports:
- name: http
containerPort: 80
protocol: TCP
- name: https
containerPort: 443
protocol: TCP
livenessProbe:
httpGet:
path: /healthz
port: 10254
scheme: HTTP
initialDelaySeconds: 30
timeoutSeconds: 5
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
args:
- /nginx-ingress-controller
- --default-backend-service=$(POD_NAMESPACE)/nginx-default-backend

实际上ingress controller还需要一个default-backend,在所有转发规则不匹配的时候返回一个404的页面,这里不提了。

总结

Pod是k8s调度的基本单元,其本身是一个逻辑概念,通过pause容器将内部的容器统一为一个整体。

k8s通过overlay网络实现pod间的通信。

Service为一组容器提供统一的访问入口,是一种基本的服务代理方式。

通过ClusterIP可以访问service,是由kube-proxy同步netfilter规则实现的。

外部访问service有三种方式:

  • NodePort
  • LoadBalancer
  • Ingress

NodePort方式最为原始,会在每个node上打开一个端口,实现node的network到service network的转发。

LoadBalancer需要运行环境支持API驱动的网络服务,如GCE,AWS。并且会为每一个服务配置一个LB(当然配套的得有一个公共IP)。

Ingress最为灵活,在一个IP下能够暴露多个服务,可以够实现基于virtual host和path的多种方式的流量转发,也支持多种ingress controller。

以上,总结完毕。

参考

Understanding kubernetes networking: pods
Understanding kubernetes networking: service
Understanding kubernetes networking: ingress
Kubernetes Ingress
Kubernetes NodePort vs LoadBalancer vs Ingress? When should I use what?
kubernetes入门之kube-proxy实现原理
docker的五种网络模式总结
Kubernetes之Pause容器
图解Kubernetes网络(一)