我们不应该期望Kubernetes Pod是健壮的,而是要假设Pod中的容器很可能因为各种原因发生故障而死掉。Deployment等Controller会通过动态创建和销毁Pod来保证应用整体的健壮性。换句话说,Pod是脆弱的,但应用是健壮的
Pod的IP地址是Docker Daemon根据docker0网桥的IP地址段进行分配的,但Service的Cluster IP地址是Kubernetes系统中的虚拟IP地址,由系统动态分配。Service的Cluster IP地址相对于Pod的IP地址来说相对稳定,Service被创建时即被分配一个IP地址,在销毁该Service之前,这个IP地址都不会再变化了。而Pod在Kubernetes集群中生命周期较短,可能被ReplicationContrller销毁、再次创建,新创建的Pod将会分配一个新的IP地址
每个Pod都有自己的IP地址。当Controller用新Pod替代发生故障的Pod时,新Pod会分配到新的IP地址,这样就产生了一个问题:如果一组Pod对外提供服务(比如HTTP),它们的IP很有可能发生变化,那么客户端如何找到并访问这个服务呢?
Kubernetes给出的解决方案是Service
服务,是一个虚拟概念,逻辑上代理后端pod。众所周知,pod生命周期短,状态不稳定,pod异常后新生成的pod ip会发生变化,之前pod的访问方式均不可达。通过service对pod做代理,service有固定的ip和port,ip:port组合自动关联后端pod,即使pod发生改变,kubernetes内部更新这组关联关系,使得service能够匹配到新的pod。这样,通过service提供的固定ip,用户再也不用关心需要访问哪个pod,以及pod是否发生改变,大大提高了服务质量。如果pod使用rc创建了多个副本,那么service就能代理多个相同的pod,通过kube-proxy,实现负载均衡
Node IP:Node节点的IP地址
Pod IP:Pod的IP地址
Cluster IP:Service的IP地址
首先,Node IP是Kubernetes集群中节点的物理网卡IP地址(一般为内网),所有属于这个网络的服务器之间都可以直接通信,所以Kubernetes集群外要想访问Kubernetes集群内部的某个节点或者服务,肯定得通过Node IP进行通信(这个时候一般是通过外网IP了)
然后Pod IP是每个Pod的IP地址,它是Docker Engine根据docker0网桥的IP地址段进行分配的(我们这里使用的是flannel这种网络插件保证所有节点的Pod IP不会冲突)
最后Cluster IP是一个虚拟的IP,仅仅作用于Kubernetes Service这个对象,由Kubernetes自己来进行管理和分配地址,当然我们也无法ping这个地址,他没有一个真正的实体对象来响应,他只能结合Service Port来组成一个可以通信的服务。
ClusterIP:通过集群的内部 IP 暴露服务,选择该值,服务只能够在集群内部可以访问,这也是默认的 ServiceType。
NodePort:通过每个 Node 上的 IP 和静态端口(NodePort)暴露服务。NodePort 服务会路由到 ClusterIP 服务,这个 ClusterIP 服务会自动创建。通过请求 :,可以从集群的外部访问一个 NodePort 服务。
LoadBalancer:使用云提供商的负载局衡器,可以向外部暴露服务。外部的负载均衡器可以路由到 NodePort 服务和 ClusterIP 服务。
ExternalName:通过返回 CNAME 和它的值,可以将服务映射到 externalName 字段的内容(例如, foo.bar.example.com)。 没有任何类型代理被创建,这只有 Kubernetes 1.7 或更高版本的 kube-dns 才支持。
Service从逻辑上代表了一组Pod,具体是哪些Pod则是
由label来挑选的。Service有自己的IP,而且这个IP是不变的。客户端只需要访问Service的IP,Kubernetes则负责建立和维护Service与Pod的映射关系。无论后端Pod如何变化,对客户端不会有任何影响,因为Service没有变
来看个例子,创建下面的这个Deployment
[k8s@server1 ~]$ cat httpd.yml
apiVersion: apps/v1
kind: Deployment
metadata:
name: httpd
spec:
replicas: 3
selector:
matchLabels:
run: httpd
template:
metadata:
labels:
run: httpd
spec:
containers:
- name: httpd
image: httpd
ports:
- containerPort: 80
# 注意的是:node节点上的docker服务必须是好的,而且虚拟机要能上网
[k8s@server1 ~]$ kubectl apply -f httpd.yml
deployment.apps/httpd created
[k8s@server1 ~]$ kubectl get pod -o wide #注意:ip的出现是需要时间的(容器的准备也是需要时间的)
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
httpd-69cb5b9fdd-cwqk2 1/1 Running 0 14s 10.244.2.22 server3 <none> <none>
httpd-69cb5b9fdd-wprs9 1/1 Running 0 14s 10.244.1.24 server2 <none> <none>
httpd-69cb5b9fdd-xj9mp 1/1 Running 0 14s 10.244.1.23 server2 <none> <none>
# 我们可以看到的是:Pod分配了各自的IP,这些IP只能被Kubernetes Cluster中的容器和节点访问
[k8s@server1 ~]$ curl 10.244.2.22
<html><body><h1>It works!</h1></body></html>
[k8s@server1 ~]$ curl 10.244.1.24
<html><body><h1>It works!</h1></body></html>
[k8s@server1 ~]$ curl 10.244.1.23
<html><body><h1>It works!</h1></body></html>
# 前提:之前的httpd pod 不能关闭和删除
接下来创建Service
[k8s@server1 ~]$ cat httpd-svc.yml
apiVersion: v1
kind: Service
metadata:
name: httpd-svc
spec:
selector:
run: httpd
ports:
- protocol: TCP
port: 8080
targetPort: 80
"""
1 v1是Service的apiVersion
2 指明当前资源的类型为Service
3 Service的名字为httpd-svc
4 selector指明挑选那些label为run: httpd的Pod作为Service的后端
5 将Service的8080端口映射到Pod的80端口,使用TCP协议
"""
执行kubectl apply创建Service httpd-svc
[k8s@server1 ~]$ kubectl apply -f httpd-svc.yml
service/httpd-svc created
[k8s@server1 ~]$ kubectl get service
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
httpd-svc ClusterIP 10.104.247.136 <none> 8080/TCP 9s
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 22h
# httpd-svc分配到一个CLUSTER-IP
# 以通过该IP 访问后端的httpd Pod
[k8s@server1 ~]$ curl 10.104.247.136:8080
<html><body><h1>It works!</h1></body></html>
[k8s@server1 ~]$ curl 10.104.247.136:8080
<html><body><h1>It works!</h1></body></html>
# 通过kubectl describe可以查看httpd-svc与Pod的对应关系
[k8s@server1 ~]$ kubectl describe service httpd-svc
Name: httpd-svc
Namespace: default
Labels: <none>
Annotations: kubectl.kubernetes.io/last-applied-configuration:
{"apiVersion":"v1","kind":"Service","metadata":{"annotations":{},"name":"httpd-svc","namespace":"default"},"spec":{"ports":[{"port":8080,"...
Selector: run=httpd
Type: ClusterIP
IP: 10.104.247.136
Port: 8080/TCP
TargetPort: 80/TCP
Endpoints: 10.244.1.25:80,10.244.2.23:80,10.244.2.24:80
Session Affinity: None
Events:
" ""
Endpoints罗列了三个Pod的IP和端口。我们知道Pod的IP是在容器
中配置的,那么Service的Cluster IP又是配置在哪里的呢?CLUSTER-
IP又是如何映射到Pod IP的呢?
答案是iptables
Cluster IP是一个虚拟IP,是由Kubernetes节点上的iptables规则管理的可以通过iptables-save命令打印出当前节点的iptables规则,因为输出较多,这里只截取与httpd-svc Cluster IP 10.99.229.179相关的信息
# 注意用户的切换:此处使用root用户
[root@server1 ~]# iptables-save |grep httpd-svc
-A KUBE-SERVICES ! -s 10.244.0.0/16 -d 10.104.247.136/32 -p tcp -m comment --comment "default/httpd-svc: cluster IP" -m tcp --dport 8080 -j KUBE-MARK-MASQ
-A KUBE-SERVICES -d 10.104.247.136/32 -p tcp -m comment --comment "default/httpd-svc: cluster IP" -m tcp --dport 8080 -j KUBE-SVC-RL3JAE4GN7VOGDGP
"""
(1)如果Cluster内的Pod(源地址来自10.244.0.0/16)要访问httpd-svc,则允许
2)其他源地址访问httpd-svc,跳转到规则KUBE-SVC-
RL3JAE4GN7VOG DGP。KUBE-SVC-RL3JAE4GN7VOGDGP规则
[root@server1 ~]# iptables-save |grep KUBE-SVC-RL3JAE4GN7VOGDGP
-A KUBE-SVC-RL3JAE4GN7VOGDGP -m statistic --mode random --probability 0.33332999982 -j KUBE-SEP-N5PH4BBCFTMHGLV6
-A KUBE-SVC-RL3JAE4GN7VOGDGP -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-UEWSDQT52CMKBNNL
-A KUBE-SVC-RL3JAE4GN7VOGDGP -j KUBE-SEP-BSPNJWUG33XKKPE5
1/3的概率跳转到规则 KUBE-SEP-N5PH4BBCFTMHGLV6
1/3的概率(剩下2/3的一半)跳转到规则 KUBE-SEP-UEWSDQT52CMKBNNL
1/3的概率跳转到规则 KUBE-SEP-BSPNJWUG33XKKPE5
即将请求分别转发到后端的三个Pod。通过上面的分析,我们得
到结论:iptables将访问Service的流量转发到后端Pod,而且使用类似
轮询的负载均衡策略
另外,需要补充一点:Cluster的每一个节点都配置了相同的
iptables规则,这样就确保了整个Cluster都能够通过Service的Cluster IP访问Service
"""
在Cluster中,除了可以通过Cluster IP访问Service,Kubernetes还提供了更为方便的DNS访问
# kubeadm部署时会默认安装kube-dns组件
# kube-dns是一个DNS服务器。每当有新的Service被创建,kube-dns会添加该Service的DNS记录。Cluster中的Pod可以通过.访问Service
# 如果要访问其他namespace中的Service,就必须带上namesapce了。kubectl get namespace查看已有的namespace
[k8s@server1 ~]$ kubectl get namespace
NAME STATUS AGE
default Active 23h
kube-node-lease Active 23h
kube-public Active 23h
kube-system Active 23h
# kube-dns是一个DNS服务器。每当有新的Service被创建,kube-dns会添加该Service的DNS记录
[k8s@server1 ~]$ kubectl run busybox --rm -it --image=busybox /bin/sh
kubectl run --generator=deployment/apps.v1 is DEPRECATED and will be removed in a future version. Use kubectl run --generator=run-pod/v1 or kubectl create instead.
If you don't see a command prompt, try pressing enter.
/ # wget httpd-svc.default:8080
Connecting to httpd-svc.default:8080 (10.99.215.181:8080)
saving to 'index.html'
index.html 100% |********************************| 45 0:00:00 ETA
'index.html' saved
# 如上所示,我们在一个临时的busyboxPod中验证了DNS的有效性。另外,由于这个Pod与httpd-svc同属于default namespace,因此可以省略default直接用httpd-svc访问Service
/ # wget httpd-svc:8080
Connecting to httpd-svc:8080 (10.99.215.181:8080)
wget: can't open 'index.html': File exists
/ # ls
bin etc index.html root tmp var
dev home proc sys usr
/ # rm -rf index.html
/ # wget httpd-svc:8080
Connecting to httpd-svc:8080 (10.99.215.181:8080)
saving to 'index.html'
index.html 100% |********************************| 45 0:00:00 ETA
'index.html' saved
/ # ls
bin etc index.html root tmp var
dev home proc sys usr
# 在kube-public中部署Service httpd2-svc
# 通过namespace: kube-public指定资源所属的namespace。多个资源可以在一个YAML文件中定义,用“---”分割。
#执行kubectl
[k8s@server1 ~]$ cat httpd2.yml
apiVersion: apps/v1
kind: Deployment
metadata:
name: httpd2
namespace: kube-public
spec:
replicas: 3
selector:
matchLabels:
run: httpd2
template:
metadata:
labels:
run: httpd2
spec:
containers:
- name: httpd2
image: httpd
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: httpd2-svc
namespace: kube-public
spec:
selector:
run: httpd2
ports:
- protocol: TCP
port: 8080
targetPort: 80
[k8s@server1 ~]$ kubectl apply -f httpd2.yml
deployment.apps/httpd2 created
service/httpd2-svc created
[k8s@server1 ~]$ kubectl get service --namespace=kube-public
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
httpd2-svc ClusterIP 10.104.125.247 <none> 8080/TCP 19s
# 在busybox Pod中访问httpd2-svc
# 因为不属于同一个namespace,所以必须使用httpd2-svc.kube-public才能访问到
[k8s@server1 ~]$ kubectl run busybox --rm -it --image=busybox /bin/sh
kubectl run --generator=deployment/apps.v1 is DEPRECATED and will be removed in a future version. Use kubectl run --generator=run-pod/v1 or kubectl create instead.
If you don't see a command prompt, try pressing enter.
/ # wget httpd2-svc.kube-public:8080
Connecting to httpd2-svc.kube-public:8080 (10.104.125.247:8080)
saving to 'index.html'
index.html 100% |******| 45 0:00:00 ETA
'index.html' saved
注意:Pod与httpd-svc同属于default namespace,因此可以省略default直接用httpd-svc访问Service
除了Cluster内部可以访问Service,很多情况下我们也希望应用的Service能够暴露给Cluster外部。Kubernetes提供了多种类型的Service,默认是ClusterIP
(1)ClusterIP
Service通过Cluster内部的IP对外提供服务,只有Cluster内的节点和Pod可访问,这是默认的Service类型,前面实验中的Service都是ClusterIP
(2)NodePort
Service通过Cluster节点的静态端口对外提供服务。Cluster外部可以通过:访问Service
(3)LoadBalancer
Service利用cloud provider特有的load balancer对外提供服务,cloud provider负责将load balancer的流量导向Service 目前支持的 cloud provider有GCP、AWS、Azur等
[k8s@server1 ~]$ cat httpd-svc.yml
apiVersion: v1
kind: Service
metadata:
name: httpd-svc
spec:
type: NodePort
selector:
run: httpd
ports:
- protocol: TCP
port: 8080
targetPort: 80
# 添加type: NodePort,重新创建httpd-svc
# 注意点是:要先运行pod 然后将它们打包成一个整体-service
[k8s@server1 ~]$ kubectl apply -f httpd.yml
deployment.apps/httpd created
[k8s@server1 ~]$ kubectl apply -f httpd-svc.yml
service/httpd-svc created
# Kubernetes依然会为httpd-svc分配一个ClusterIP,不同的是
# PORT(S)为8080:30295 8080是ClusterIP监听的端口,32312则是节点上监听的端口。Kubernetes会从30000~32767中分配一个可用的端口,每个节点都会监听此端口并将请求转发给Service
[k8s@server1 ~]$ kubectl get service httpd-svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
httpd-svc NodePort 10.98.81.225 <none> 8080:30295/TCP 4s
[k8s@server1 ~]$ kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
httpd-69cb5b9fdd-7b6gq 1/1 Running 0 28s 10.244.2.27 server3 <none> <none>
httpd-69cb5b9fdd-9k6c8 1/1 Running 0 28s 10.244.1.30 server2 <none> <none>
httpd-69cb5b9fdd-c5rxz 1/1 Running 0 28s 10.244.1.29 server2 <none> <none>
# 测试
# 通过三个节点IP+PORT 口都能够访问httpd-svc
[k8s@server1 ~]$ curl 172.25.0.2:30295
<html><body><h1>It works!</h1></body></html>
[k8s@server1 ~]$ curl 172.25.0.3:30295
<html><body><h1>It works!</h1></body></html>
接下来我们深入探讨一个问题:Kubernetes是如何将<NodeIP>:<NodePort>映射到Pod的呢?
与ClusterIP一样,也是借助了iptables。与ClusterIP相比,每个节点的iptables中都增加了下面两条规则
[root@server1 ~]# iptables-save |grep 30295
-A KUBE-NODEPORTS -p tcp -m comment --comment "default/httpd-svc:" -m tcp --dport 30295 -j KUBE-MARK-MASQ
-A KUBE-NODEPORTS -p tcp -m comment --comment "default/httpd-svc:" -m tcp --dport 30295 -j KUBE-SVC-RL3JAE4GN7VOGDGP
[root@server1 ~]# iptables-save |grep KUBE-SVC-RL3JAE4GN7VOGDGP
:KUBE-SVC-RL3JAE4GN7VOGDGP - [0:0]
-A KUBE-NODEPORTS -p tcp -m comment --comment "default/httpd-svc:" -m tcp --dport 30295 -j KUBE-SVC-RL3JAE4GN7VOGDGP
-A KUBE-SERVICES -d 10.98.81.225/32 -p tcp -m comment --comment "default/httpd-svc: cluster IP" -m tcp --dport 8080 -j KUBE-SVC-RL3JAE4GN7VOGDGP
-A KUBE-SVC-RL3JAE4GN7VOGDGP -m statistic --mode random --probability 0.33332999982 -j KUBE-SEP-Y5EOPQEI6R2HKSKZ
-A KUBE-SVC-RL3JAE4GN7VOGDGP -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-SACXFJATP2WVU5HD
-A KUBE-SVC-RL3JAE4GN7VOGDGP -j KUBE-SEP-MZTDTCCSTQYQRVK2
"""
规则的含义是:访问当前节点30295端口的请求会应用规则 KUBE-SVC-RL3JAE4GN7VOGDGP
其作用就是负载均衡到每一个Pod
"""
NodePort默认的是随机选择,不过我们可以用nodePort指定某个特定端口
[k8s@server1 ~]$ cat httpd-svc.yml
apiVersion: v1
kind: Service
metadata:
name: httpd-svc
spec:
type: NodePort
selector:
run: httpd
ports:
- protocol: TCP
nodePort: 30000
port: 8080
targetPort: 80
"""
现在配置文件中就有三个Port了
nodePort是节点上监听的端口
port是ClusterIP上监听的端口
targetPort是Pod监听的端口
最终,Node和ClusterIP在各自端口上接收到的请求都会通过iptables转发到Pod的targetPort
"""
[k8s@server1 ~]$ kubectl apply -f httpd-svc.yml
service/httpd-svc created
[k8s@server1 ~]$ kubectl get service httpd-svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
httpd-svc NodePort 10.98.239.4 <none> 8080:30000/TCP 10s
[k8s@server1 ~]$ curl 172.25.0.3:30000
<html><body><h1>It works!</h1></body></html>
[k8s@server1 ~]$ curl 172.25.0.2:30000
<html><body><h1>It works!</h1></body></html>