前一篇文章,介绍了Docker容器的网络模型。
容器是要被 K8S 编排、管理的。而 K8S 又有自己的网络模型。我们继续学习、实验,来理解 K8S 是怎么样处理网络流量的。
实验之前,先分清楚 K8S 里面的三种 IP 地址,和三种端口。
三种 IP 地址:
Cluster IP:K8S 在创建 Service 时,生成的虚拟 IP 地址。需要与 Service Port 合到一起,成为一个有效的通信端口。该 IP 地址用于集群内部的访问。
Node IP:集群节点的 IP 地址。节点可能是物理机,也可能是虚拟机。该地址真实存在于物理网络。集群外部,可以通过该地址访问到集群内的节点。
Pod IP:K8S 创建 Pod 时,为该 Pod 分配的 IP 地址。该地址在集群外不可见。具体的 IP 地址网段、以及 Pod 之间通信的方式,取决于集群创建时选用的 CNI 模型。本文选用了 Flannel 做为 CNI,但不涉及 CNI 的分析。
三种端口:
port:是集群 Service 侦听的端口,与前面的 Cluster IP 合到一起,即 Cluster IP:port,提供了集群内部访问 Service 的入口。在 K8S 的 yaml 文件里面,port 就是 Service Port 的缩写。这是 K8S 创建 Service 时,默认的方式。
个人感觉,这个名字其实挺容易让人混淆的,还不如直接用关键字 ServicePort 来的清楚。
nodePort:是在节点上侦听的端口。通过 Node IP:nodePort 的形式,提供了集群外部访问集群中 Service 的入口。
targetPort:是在 Pod 上侦听的端口,比如运行 Nginx 的 Pod,会在 80 端口上监听 HTTP 请求。所有对 Service Port 和 Node Port 的访问,最后都会被转发到 Target Port 来处理。
下面,开始我们的实验。
一. 安装 K8S 集群
如果还没有集群的话,可以参考我的另一篇文章,先去把环境搭建好:基于Ubuntu安装Kubernetes集群指南。
二. 创建 Deployment
随便找个目录,执行如下命令,创建一个 yaml 文件:
$ vi nginx_deployment.yaml
复制代码
输入这些内容后,按:wq 保存、退出,得到 nginx_deployment.yaml 文件:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
selector:
matchLabels:
app: nginx
replicas: 2
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx
ports:
- containerPort: 80
复制代码
这个 Deployment 将会生成 2 个副本的 Pod,每个 Pod 里面都运行 nginx,Pod 开放 80 端口。
然后用该 yaml 文件,去创建 K8S 资源:
$ kubectl apply -f nginx_deployment.yaml
deployment.apps/nginx-deployment configured
$ kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
nginx-deployment-55f598f8d-49l2v 1/1 Running 0 87m 10.244.0.2 ycwang-ubuntu <none> <none>
nginx-deployment-55f598f8d-pxp6x 1/1 Running 0 87m 10.244.1.4 ycwang-ubuntu-worker <none> <none>
复制代码
可以看到,两个 Pod 已经在运行,并且它们有各自的 IP。
此时,通过 Pod IP 即可访问 Nginx:
$ wget 10.244.0.2
Connecting to 10.244.0.2:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 615 [text/html]
Saving to: ‘index.html’
index.html 100%[===============================================================================================================================>] 615 --.-KB/s in 0s
(62.7 MB/s) - ‘index.html’ saved [615/615]
$ wget 10.244.1.4
Connecting to 10.244.1.4:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 615 [text/html]
Saving to: ‘index.html.1’
index.html.1 100%[===============================================================================================================================>] 615 --.-KB/s in 0s
(84.2 MB/s) - ‘index.html.1’ saved [615/615]
复制代码
但从集群外,是无法访问这两个 IP 地址的。
三. 创建 Service
生成一个新的 yaml 文件,用来创建 Service 资源。
输入这些内容后,按:wq 保存、退出。
apiVersion: v1
kind: Service
metadata:
name: nginx-svc
labels:
run: nginx-svc
spec:
ports:
- port: 8080
targetPort: 80
protocol: TCP
selector:
app: nginx
复制代码
这个 Service 使用 app=nginx 标签选择器,来选择对应的 Pod,做为 Service 的后端。Service 的类型是默认的 Service Port,所以不必写出来。
Service 监听在 8080 端口,集群内部可以通过 ClusterIP:8080 进行访问,并把流量转发到 Pod 的 80 端口进行实际的业务处理。
执行命令,用该 yaml 文件去创建 Service:
$ kubectl apply -f nginx_svc.yaml
service/nginx-svc created
$ kubectl get service
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 145m
nginx-svc ClusterIP 10.104.112.175 <none> 8080/TCP 3s
复制代码
可以看到,该 Service 已经创建成功。并且出现了一个新的 IP 地址:10.104.112.175。这就是我们前面介绍的第一种 IP 地址:Cluster IP。
通过该虚拟的 IP 地址,加上指定的 8080 端口,即可实现集群内部对 Service 的访问:
$ wget 10.104.112.175:8080
Connecting to 10.104.112.175:8080... connected.
HTTP request sent, awaiting response... 200 OK
Length: 615 [text/html]
Saving to: ‘index.html’
index.html 100%[===============================================================================================================================>] 615 --.-KB/s in 0s
(60.4 MB/s) - ‘index.html’ saved [615/615]
复制代码
那么,K8S 是如何访问这个不存在的虚拟 IP 地址的呢?
四. 分析 Cluster IP 访问流程
先看一下这个 Service 的信息:
$ kubectl describe service nginx-svc
Name: nginx-svc
Namespace: default
Labels: run=nginx-svc
Annotations: <none>
Selector: app=nginx
Type: ClusterIP
IP Family Policy: SingleStack
IP Families: IPv4
IP: 10.104.112.175
IPs: 10.104.112.175
Port: <unset> 8080/TCP
TargetPort: 80/TCP
Endpoints: 10.244.0.2:80,10.244.1.4:80
Session Affinity: None
Events: <none>
复制代码
这个 Service 对应了后端的两个 Pod:10.244.0.2:80,10.244.1.4:80。
就是说,对于 10.104.112.175:8080 的访问,最后会被转发到 10.244.0.2:80 或者 10.244.1.4:80。
这要感谢 iptables 在后面默默的干活。
查看目前 iptables 的情况:
$ sudo iptables-save | grep 10.104.112.175
......
-A KUBE-SERVICES -d 10.104.112.175/32 -p tcp -m comment --comment "default/nginx-svc cluster IP" -m tcp --dport 8080 -j KUBE-SVC-HL5LMXD5JFHQZ6LN
......
复制代码
对于 10.104.112.175 的访问,会被跳转到规则:KUBE-SVC-HL5LMXD5JFHQZ6LN。
继续查看这条规则:
$ sudo iptables-save | grep KUBE-SVC-HL5LMXD5JFHQZ6LN
-A KUBE-SVC-HL5LMXD5JFHQZ6LN -m comment --comment "default/nginx-svc -> 10.244.0.2:80" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-T5AFCZ323NYPWW2A
-A KUBE-SVC-HL5LMXD5JFHQZ6LN -m comment --comment "default/nginx-svc -> 10.244.1.4:80" -j KUBE-SEP-RQ66ZV5Y2RYOH2X3
复制代码
iptables 把对 10.104.112.175 的访问,采用轮询的负载均衡策略,依次转发给:10.244.0.2:80 和 10.244.1.4:80。
从而实现了在集群内部对 Cluster IP:port 的访问,并自带了负载均衡功能。
另外,这些 iptables 规则的增删改都是由运行在每个节点的 kube-proxy 来实现的。
五. 创建 NodePort 类型的 Service
现在,我们把 Service 的类型改成 NodePort。
输入如下内容,并按:wq 保存、退出:
apiVersion: v1
kind: Service
metadata:
name: nginx-svc
labels:
run: nginx-svc
spec:
type: NodePort
ports:
- port: 8080
targetPort: 80
nodePort: 30000
protocol: TCP
selector:
app: nginx
复制代码
在这个 yaml 文件,把 Service 的类型指定为 NodePort,并在每个 Node 上,侦听 30000 端口。对 30000 端口的访问,最后会被转发到 Pod 的 80 端口。
先把之前的 Service 和 Deployment 都删掉,再用新的 yaml 文件重新创建:
$ kubectl delete -f nginx_svc.yaml
service "nginx-svc" deleted
$ kubectl delete -f nginx_deployment.yaml
deployment.apps "nginx-deployment" deleted
$ kubectl apply -f nginx_deployment.yaml
deployment.apps/nginx-deployment created
$ kubectl apply -f nginx_svc.yaml
service/nginx-svc created
复制代码
查看 Service 和 Pod 的具体信息:
$ kubectl get services
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 3h41m
nginx-svc NodePort 10.110.65.131 <none> 8080:30000/TCP 10s
$ kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
nginx-deployment-55f598f8d-ls786 1/1 Running 0 11m 10.244.1.6 ycwang-ubuntu-worker <none> <none>
nginx-deployment-55f598f8d-pj2xk 1/1 Running 0 11m 10.244.0.3 ycwang-ubuntu <none> <none>
复制代码
确认都在正常运行了。
此时,可以在集群内,通过 Node IP:NodePort 进行访问,此节点的 IP 是 192.168.111.128:
$ wget 192.168.111.128:30000
Connecting to 192.168.111.128:30000... connected.
HTTP request sent, awaiting response... 200 OK
Length: 615 [text/html]
Saving to: ‘index.html’
index.html 100%[===============================================================================================================================>] 615 --.-KB/s in 0s
(87.4 MB/s) - ‘index.html’ saved [615/615]
复制代码
也可以在集群外的 Windows 机器,通过 Node IP:NodePort 进行访问:
$ curl 192.168.111.128:30000
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 615 100 615 0 0 207k 0 --:--:-- --:--:-- --:--:-- 300k
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>
<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>
<p><em>Thank you for using nginx.</em></p>
</body>
</html>
复制代码
六. 分析 Node IP:NodePort 访问流程
我们继续探究,访问 Node IP:NodePort 是怎么被转到 Pod 上面去的?
答案依然是 iptables:
$ sudo iptables-save | grep 30000
-A KUBE-NODEPORTS -p tcp -m comment --comment "default/nginx-svc" -m tcp --dport 30000 -j KUBE-EXT-HL5LMXD5JFHQZ6LN
复制代码
对 30000 端口的访问,会被跳转到规则:KUBE-EXT-HL5LMXD5JFHQZ6LN。
而 KUBE-EXT-HL5LMXD5JFHQZ6LN,又被跳转到:KUBE-SVC-HL5LMXD5JFHQZ6LN
$ sudo iptables-save | grep KUBE-EXT-HL5LMXD5JFHQZ6LN
-A KUBE-EXT-HL5LMXD5JFHQZ6LN -j KUBE-SVC-HL5LMXD5JFHQZ6LN
复制代码
KUBE-SVC-HL5LMXD5JFHQZ6LN 这条规则的具体内容:
$ sudo iptables-save | grep KUBE-SVC-HL5LMXD5JFHQZ6LN
-A KUBE-SERVICES -d 10.110.65.131/32 -p tcp -m comment --comment "default/nginx-svc cluster IP" -m tcp --dport 8080 -j KUBE-SVC-HL5LMXD5JFHQZ6LN
-A KUBE-SVC-HL5LMXD5JFHQZ6LN -m comment --comment "default/nginx-svc -> 10.244.0.3:80" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-PU7AOSZG6OVFMASF
-A KUBE-SVC-HL5LMXD5JFHQZ6LN -m comment --comment "default/nginx-svc -> 10.244.1.6:80" -j KUBE-SEP-OZ4KTOWKCOJKYUPL
复制代码
跟 Cluster IP 的做法一样,iptables 把对 Node IP:NodePort 的访问,采用轮询的负载均衡策略,依次转发给:10.244.0.3:80 和 10.244.1.6:80 这两个 Endpoints。
K8S 里面的网络访问流程差不多就这样了。它采用了一个很巧妙的设计,去中心化、让每个节点都承担了负载均衡的功能。
补充点题外话,在 Node IP:NodePort 这种模式下,直接访问节点还是会有点问题的。
因为客户需要指定某个 Node 进行访问。这样会带来单点问题;而且,客户按理不应该知道、也不需要知道具体的 Node 和它的 IP。
所以,在实际应用中,可以在 K8S 集群外部,搭建一个负载均衡器。客户访问此负载均衡器,再由该负载均衡器把流量分发到各个 Node 上。很多云厂商也已经带了这样的功能。
但是,既然外部有了支持各种负载均衡算法的职业选手,把流量分发到各个 Node 上。如果 Node 收到后,再次用 iptables 进行负载均衡,就没有什么意义了。不清楚 Google 为什么要这么设计?
是不是可以考虑在 K8S 里面内置一个负载均衡的模块,专门运行在某个 Node 上。在 NodePort 模式下,可以选择启用该模块,由它来专门提供客户访问的入口并做负载均衡,然后此刻各个 Node 上的 iptables 负载均衡可以禁用了?期待各路高人高见……
BTW 一下,既然都说到了负载均衡,捆绑推销一下我的另一篇文章吧:负载均衡算法的实现。
评论 (2 条评论)