Kubernetes的网络模型从内至外由四个部分组成:
- Pod内部容器所在的网络
- Pod所在的网络
- Pod和Service之间通信的网络
- 外界与Service之间通信的网络
建议在阅读本文之前先了解Docker的网络模型。可以参看作者的前两篇文章[Kubernetes]Docker的网络模型和[Kubernetes]Docker的overlay网络模型。
1. Pod内部容器所在的网络和Pod所在的网络
Kubernetes使用了一种“IP-per-pod”网络模型:为每一个Pod分配了一个IP地址,Pod内部的容器共享Pod的网络空间,即它们共享Pod的网卡和IP。Kubernetes是怎么做的呢?聪明的读者一定想到了[Kubernetes]Docker的网络模型里介绍的Docker的container网络。Kubernetes在创建Pod时,先在Node节点的Docker上创建了一个运行在bridge网络的“pod容器”,为这个pod容器创建虚拟网卡eth0并分配IP地址。而Pod里的容器(称为app容器),在创建时使用--net=container:来共享pod容器的网络空间。
例如,笔者手里有一个Kubernetes Node主机PT-169124, IP地址为192.168.169.124. 笔者的环境用flannel作为了Docker的bridge网络驱动,用ifconfig命令可以看到flannel网络设备:
flannel0: flags=4305<UP,POINTOPOINT,RUNNING,NOARP,MULTICAST> mtu 1472 inet 172.17.17.0 netmask 255.255.0.0 destination 172.17.17.0 unspec 00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00 txqueuelen 500 (UNSPEC) RX packets 7988 bytes 410094 (400.4 KiB) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 12681 bytes 17640033 (16.8 MiB) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
这个flannel0是Kubelet用flannel创建出来的bridge,并在Docker进程启动时,作为了--bridge参数值:--bridge=flannel0,这样使得Docker里创建的容器默认都使用flannel0网段里的IP地址。查看这个主机上运行的Docker容器(受限于格式,这里只显示容器的ID和名字):
# docker ps CONTAINER ID NAMES 7672d97e01d1 k8s_kubernetes-dashboard.979fa630_kubernetes-dashboard-4164430742-lqhcg_kube-system_a08a0d66-57bf-11e6-84b8-5cf3fcba84a8_086f7305 431338595af6 k8s_POD.42430ae1_kubernetes-dashboard-4164430742-lqhcg_kube-system_a08a0d66-57bf-11e6-84b8-5cf3fcba84a8_e96d8681
这两个容器即是作者在[Kubernetes]Kubernetes集群和Docker私有库搭建(CentOS 7)一文里搭建的kubernetes-dashboard所创建出来的pod容器和app容器。其中pod容器就运行在由flannel驱动的bridge网络里:
# docker network inspect bridge [ { "Name": "bridge", "Id": "6031ecf132905a10b6500a6911d914aff2e15ca8894225aa59ca34bf965b902e", "Scope": "local", "Driver": "bridge", "IPAM": { "Driver": "default", "Options": null, "Config": [ { "Subnet": "172.17.17.1/24", "Gateway": "172.17.17.1" } ] }, "Containers": { "431338595af6236a3feb661129e17a4aed4d8331c173903bc2aa7a788d494c6d": { "Name": "k8s_POD.42430ae1_kubernetes-dashboard-4164430742-lqhcg_kube-system_a08a0d66-57bf-11e6-84b8-5cf3fcba84a8_e96d8681", "EndpointID": "6f22b9c24be10bf069973e0ba651efafdc68e13177e9cbbe3f41b6b4e963eff1", "MacAddress": "02:42:ac:11:11:03", "IPv4Address": "172.17.17.3/24", "IPv6Address": "" } }, "Options": { "com.docker.network.bridge.default_bridge": "true", "com.docker.network.bridge.enable_icc": "true", "com.docker.network.bridge.enable_ip_masquerade": "true", "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0", "com.docker.network.bridge.name": "docker0", "com.docker.network.driver.mtu": "1472" } } ]
再用docker inspect来查看kubernetes-dashboard app容器的详细信息,能看到NetworkMode值为 "container:431338595af6236a3feb661129e17a4aed4d8331c173903bc2aa7a788d494c6d",说明app容器共享pod容器的网络空间,即它们有相同的网卡和IP地址。
Kubernetes这种“IP-per-pod”网络模型,能让Pod里的容器相互之间通过loopback(127.0.0.1, localhost)网络访问,同时也意味着Pod里的容器在端口分配上不能发生冲突,而Pod里的容器根本不用担心和其他Pod里的容器发生端口冲突。
更有意思的是,Kubernetes把各Node主机上的Docker的bridge网络“外包”给了flannel,flannel在这些主机上创建的flannel0网络使用的是同一个子网的不同区间。例如本文的环境里,flannel在两台Node主机上创建的flannel0网络分别是:
主机PT-169121, IP地址为192.168.169.121:
flannel0: flags=4305<UP,POINTOPOINT,RUNNING,NOARP,MULTICAST> mtu 1472 inet 172.17.13.0 netmask 255.255.0.0 destination 172.17.13.0 unspec 00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00 txqueuelen 500 (UNSPEC) RX packets 12684 bytes 17640285 (16.8 MiB) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 7991 bytes 410346 (400.7 KiB) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
主机PT-169124, IP地址为192.168.169.124:
flannel0: flags=4305<UP,POINTOPOINT,RUNNING,NOARP,MULTICAST> mtu 1472 inet 172.17.17.0 netmask 255.255.0.0 destination 172.17.17.0 unspec 00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00 txqueuelen 500 (UNSPEC) RX packets 7988 bytes 410094 (400.4 KiB) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 12681 bytes 17640033 (16.8 MiB) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
flannel通过etcd,将各Node主机上的Docker bridge网络联合了起来,实现了:
- 所有的容器都能相互通信,不管容器是否运行在同一个Node主机上
- 所有的容器和Node主机都能相互通信,不管容器是否运行在该Node主机上。
2. Pod和Service之间通信的网络
Kubernetes里的Pod不是一个“长生”的家伙,它会由于各种原因被销毁和创造。比如在垂直扩容和滚动更新过程中,旧的Pod会被销毁,被新的Pod代替。这期间,Pod的IP地址甚至会发生变化。所以Kubernetes引进了Service.Service是一个抽象的实体,Kubernetes在创建Service实体时,为其分配了一个虚拟的IP,当我们需要访问Pod里的容器提供的功能时,我们不直接使用Pod的IP地址和端口,而是访问Service的这个虚拟IP和端口,由Service把请求转发给它背后的Pod.
Kubernetes在创建Service时,根据Service的标签选择器(Label Selector)来查找Pod,据此创建与Service同名的EndPoints对象。当Pod的地址发生变化时,EndPoints也随之变化。Service接受到请求时,就能通过EndPoints找到请求转发的目标地址。
例如,作者在搭建kubernetes-dashboard是创建出的服务如下图所示,背后有两个Pod在支撑。
通过kubectl get命令分别查看一下Service和EndPoints的信息:
# kubectl get services/kubernetes-dashboard --namespace=kube-system -o yaml apiVersion: v1 kind: Service metadata: creationTimestamp: 2016-08-01T08:12:02Z labels: app: kubernetes-dashboard name: kubernetes-dashboard namespace: kube-system resourceVersion: "18293" selfLink: /api/v1/namespaces/kube-system/services/kubernetes-dashboard uid: a0953fa0-57bf-11e6-84b8-5cf3fcba84a8 spec: clusterIP: 10.254.213.209 portalIP: 10.254.213.209 ports: - nodePort: 31482 port: 80 protocol: TCP targetPort: 9090 selector: app: kubernetes-dashboard sessionAffinity: None type: NodePort status: loadBalancer: {}
# kubectl describe endpoints/kubernetes-dashboard --namespace=kube-system Name: kubernetes-dashboard Namespace: kube-system Labels: app=kubernetes-dashboard Subsets: Addresses: 172.17.13.2,172.17.17.3 NotReadyAddresses: <none> Ports: Name Port Protocol ---- ---- -------- <unset> 9090 TCP No events.
可以看到,Service的targetPort和Pod的IP地址都记录在了与Service同名的EndPoints里。我们再深入看看当通过Service的虚拟IP和端口访问Service时,请求是如何到达Pod的。
前面说过,Service只是一个虚拟的实体,真正完成请求转发的是运行在Node节点上的kube-proxy.Service的虚拟IP就是由kube-proxy实现的。kube-proxy有两种请求转发模式:userspace模式和iptables模式。在Kubernetes v1.1版本之前默认是userspace模式,v1.2版本后默认是iptables模式。
userspace模式:当创建Service时,所有Node节点上的kube-proxy都会在其所在的本地节点上随机开放一个端口(称为代理端口),然后建立一个iptables规则(一种linux包处理逻辑),iptables会完成<服务的虚拟IP, 端口>与代理端口的流量转发,再从EndPoints里选择一个Pod,把代理端口的流量转给该Pod. 当EndPoints下有多个Pod时,选择Pod的算法有两种:1 依次循环,如果一个Pod没有响应,就试下一个(service.spec.sessionAffinity值为"None");2 选择与请求来源IP更接近的Pod(service.spec.sessionAffinity值为"ClientIP").
userspace模式的缺点是:只适用于规模较小的集群;会对Pod屏蔽请求的source IP,使得部分基于source IP的防火墙失效。
iptables模式:当创建Service时,所有Node节点上的kube-proxy都会建立两级iptables规则,一级为Service创建,目的是将<服务虚拟IP,端口>的流量转给后端,另一级为EndPoints创建,目的是用于选择Pod. 当service.spec.sessionAffinity值为"ClientIP"时,iptables模式选择Pod的算法和userspace模式相同。当service.spec.sessionAffinity值为"None"时,随机选择Pod,所以如果被选择的Pod没有响应,不会尝试选择另一个Pod.
例如,作者的Kubernetes环境是v1.2版本,用iptables -vL --line-numbers -t nat命令能看到创建的iptables规则:
kube-proxy为kubernetes-dashboard服务创建的iptables规则:
kube-proxy为kubernetes-dashboard EndPoints创建的iptables规则:
iptables模式相比userspace模式更快更稳定,也不存在请求的source IP的问题。
我们介绍了Kubernetes是如何用Service来“代理”Pod,那么被创建的Service如何被其他Pod感知到呢?比如说,一个前端的Pod如何找到后端的Service?Kubernetes提供了两种方法:环境变量和DNS.这部分的内容,Kubernetes的官网有所介绍。
3. 外界与Service之间通信的网络
如果想把服务暴露给外界(为什么不直接暴露Pod呢?相信读者自己有答案。),有三种方式:
Service的类型
如果不指定Service的spec.type的值,创建的Service的类型默认为ClusterIP类型。这种类型的Service只会得到虚拟的IP和端口,只能在Kubernetes集群内部被访问。
如果指定Service的spec.type的值为“NodePort”,创建的Service的类型默认为NodePort类型。这种类型的Service除了会得到虚拟的IP和端口,Kubernetes还会在所有Node节点上为其分配端口。分配的端口的值可以通过spec.ports[*].nodePort指定,或由Knubernetes在配置好的区间里分配(默认为30000-32767)。这种Service即可以从Kubernetes集群通过虚拟IP:端口访问,也可以从集群外部通过Node节点的IP:nodePort访问,例如,作者创建的kubernetes-dashboard服务就是NodePort类型,Kubernetes为其分配的nodePort为31482,通过任意节点的IP(192.168.169.121或192.168.169.124)都能访问。
如果指定Service的spec.type的值为“LoadBalancer”,创建的Service的类型默认为LoadBalancer类型。这种类型的Service除了会得到虚拟的IP和端口,Kubernetes还会在所有Node节点上为其分配端口,然后为其开通负载均衡。这种Service即可以从Kubernetes集群通过虚拟IP:端口访问,也可以从集群外部通过Node节点的IP:nodePort访问,还可以通过负载均衡的IP访问。
绑定外部IP
通过指定Service的spec.externalIPs[*],可以将Service暴露在节点的外部IP上。外界通过这个IP和Service的端口可以访问到该Service.
使用Ingress
先参见Kubernetes官方文档。待作者亲手完成环境验证后补上。