kubernetes 核心技术-service

service简介

在Kubernetes中对 Pod 的期望值并不是持续健壮的,Pod 中的容器很可能因为各种原因发生故障而死掉。Deployment 等 controller 会通过动态创建和销毁 Pod 来保证应用整体的健壮性。换句话说,Pod 是脆弱的,但应用是健壮的。

每个 Pod 都有自己的 IP 地址。当 controller 用新 Pod 替代发生故障的 Pod 时,新 Pod 会分配到新的 IP 地址。这样就产生了一个问题:

如果一组 Pod 对外提供服务(比如 HTTP),它们的 IP 很有可能发生变化,那么客户端如何找到并访问这个服务呢?

Kubernetes 给出的解决方案是 Service。

Service定义了一组pods的逻辑集合和一个用于访问它们的策略,通过Service可以为一组具有相同功能的容器应用提供一个统一的入口地址,并且将请求负载分发到后端的各个容器应用上。

一个Service的目标Pod集合通常是由Label Selector 来决定的,当Pod宕机后重新生成时,其IP等状态信息会变动,Service会根据Pod的Label对这些状态信息进行监控和变更,保证上游服务不受Pod的变动而影响。

service代理三种模式

k8s群集中的每个节点都运行一个kube-proxy的组件,kube-proxy其实是一个代理层,负责k8s service的实现,即实现了k8s内部从pod到service和外部从node port到service的访问。

kube-proxy当前支持三种不同的操作模式:

1.userspace(用户空间)

客户端访问ServiceIP(clusterIP)请求会先从用户空间到内核空间中的iptables,然后回到用户空间kube-proxy,实际由kube-proxy负责代理工作。

这种模式之所以得名,是因为服务路由发生在kube-proxy用户进程空间而不是内核网络堆栈(如iptables)中。

userspace模式中,所有的客户端请求svc,先经过iptables,然后再经过kube-proxy到pod,频繁的切换内核和用户空间,所以性能很差。它不常用,因为它运行缓慢且过时。

2.iptables

客户端访问ServiceIP(clusterIP)请求会由内核中的iptables直接重定向到后端某一个pod。

相比较于userspace方式,kube-proxy不再负责代理工作,只需监听apiserver写入etcd中关于pod的变化,将需要变更的规则添加到iptables中。

3.ipvs(IP虚拟服务器)

客户端访问ServiceIP(clusterIP)请求会由内核中的ipvs直接重定向到后端某一个pod。(流程图与iptables方式一样)

从k8s的1.8版本开始,kube-proxy引入了IPVS模式,IPVS模式与iptables同样基于Netfilter,但是ipvs采用的hash表,iptables采用一条条的规则列表。iptables又是为了防火墙设计的,集群数量越多iptables规则就越多,而且iptables规则是从上到下匹配,所以效率就越是低下。因此当service数量达到一定规模时,hash查表的速度优势就会显现出来,从而提高service的服务性能。

ipvs 支持比 iptables 更复杂的负载均衡算法(最小负载、最少连接、加权等等),ipvs 支持服务器健康检查和连接重试等功能。

 

总之,每个节点的kube-proxy负责监听API server中service和endpoint的变化情况。将变化信息写入本地userspace、iptables、ipvs来实现service负载均衡,使用NAT将vip流量转至endpoint中。

目前,service已默认使用ipvs规则,如果没有加载并启用ipvs模块,或者没有配置ipvs相关配置,则会被降级成iptables模式。

service的四种类型

ClusterIP

ClusterIP是默认模式,如图所示只能在集群内部访问。

来看个例子,创建下面的这个 Deployment:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      run: nginx
  strategy: {}
  template:
    metadata:
      labels:
        run: nginx
    spec:
      containers:
      - image: nginx:1.8
        name: nginx
        ports:
        - containerPort: 80
        resources: {}
status: {}
启动了三个 Pod,运行 nginx镜像,pod端口是80,label 是 run: nginx,Service 将会用这个 label 来挑选 Pod。
[root@master service]# kubectl apply -f nginx.yaml 
deployment.apps/nginx created
[root@master service]# kubectl get pods -o wide
NAME                     READY   STATUS    RESTARTS   AGE   IP            NODE    NOMINATED NODE   READINESS GATES
nginx-5f9d59d9fc-299b9   1/1     Running   0          88s   10.244.2.26   node2   <none>           <none>
nginx-5f9d59d9fc-8whz6   1/1     Running   0          88s   10.244.1.22   node1   <none>           <none>
nginx-5f9d59d9fc-hzt6s   1/1     Running   0          88s   10.244.1.23   node1   <none>           <none>

三个pod分配到2台node上,每个pod都分配了各自的 IP,这些 IP 只能被 Kubernetes Cluster 中的容器和节点访问:

[root@master service]# curl -I 10.244.2.26
HTTP/1.1 200 OK
Server: nginx/1.8.1

[root@master service]# curl -I 10.244.1.22
HTTP/1.1 200 OK
Server: nginx/1.8.1

[root@master service]# curl -I 10.244.1.23
HTTP/1.1 200 OK
Server: nginx/1.8.1

接下来创建service,其配置文件如下:

apiVersion: v1
kind: Service
metadata:
  name: nginx-service
spec:
  selector:                        //selector 指明挑选那些 label 为 run: nginx 的 Pod 作为 Service 的后端。
    run: nginx
  ports:
  - protocol: TCP
    port: 8080                     //将 Service 的 8080 端口映射到 Pod 的 80 端口,使用 TCP 协议
    targetPort: 80
  type: ClusterIP

执行 kubectl apply 创建 Service nginx-service:

[root@master service]# kubectl apply -f nginx-service.yaml 
service/nginx-service created
[root@master service]# kubectl get svc
NAME            TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
kubernetes      ClusterIP   10.96.0.1       <none>        443/TCP    34d
nginx-service   ClusterIP   10.110.59.203   <none>        8080/TCP   7m18s

nginx-service 分配到一个 ClusterIP 为10.110.59.203,这个就是service的虚拟ip,可以通过该 IP 访问后端的 Pod:

[root@master ~]# curl -I 10.110.59.203:8080
HTTP/1.1 200 OK
Server: nginx/1.8.1

根据前面的端口映射,这里要使用 8080 端口。另外,除了我们创建的 nginx-service,还有一个 Service 是kubernetes,这是k8s默认自带的service,Cluster 内部通过这个 Service 访问 kubernetes API Server。
通过 kubectl describe 可以查看 nginx-service 与 Pod 的对应关系,Endpoints中的地址正好和前面的创建的3个pod的地址相对应:

可以总结一下,nginx-service作为一个类型为ClusterIP的service,定义了三个nginx的pod的逻辑集合,为这组具有相同功能的容器应用提供了一个统一的入口地址,即10.110.59.203:8080,即使后端的pod由于各种原因被销毁再被创建(控制器要求保持三个副本),整个service都能提供稳定服务。

深入理解Service-IP的工作原理

Service Cluster IP 是一个虚拟 IP,是由 Kubernetes 节点上的 iptables 规则管理的。
可以通过 iptables-save 命令打印出当前节点的 iptables 规则,因为输出较多,这里只截取与nginx-service Cluster IP 10.110.59.203 相关的信息:

-A KUBE-SERVICES ! -s 10.244.0.0/16 -d 10.110.59.203/32 -p tcp -m comment --comment "default/nginx-service: cluster IP" -m tcp --dport 8080 -j KUBE-MARK-MASQ
-A KUBE-SERVICES -d 10.110.59.203/32 -p tcp -m comment --comment "default/nginx-service: cluster IP" -m tcp --dport 8080 -j KUBE-SVC-GKN7Y2BSGW4NJTYL

这两条规则的含义是:
如果 Cluster 内的 Pod(源地址来自 10.244.0.0/16)要访问 nginx-service,则允许。
其他源地址访问 nginx-service,跳转到规则KUBE-SVC-GKN7Y2BSGW4NJTYL。
KUBE-SVC-GKN7Y2BSGW4NJTYL 规则如下:

[root@master ~]# iptables-save |grep KUBE-SVC-GKN7Y2BSGW4NJTYL
:KUBE-SVC-GKN7Y2BSGW4NJTYL - [0:0]
-A KUBE-SERVICES -d 10.110.59.203/32 -p tcp -m comment --comment "default/nginx-service: cluster IP" -m tcp --dport 8080 -j KUBE-SVC-GKN7Y2BSGW4NJTYL
-A KUBE-SVC-GKN7Y2BSGW4NJTYL -m comment --comment "default/nginx-service:" -m statistic --mode random --probability 0.33333333349 -j KUBE-SEP-VMLJ2G2AI2XTLVYF
-A KUBE-SVC-GKN7Y2BSGW4NJTYL -m comment --comment "default/nginx-service:" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-UZZUH7MRVLV5UEPK
-A KUBE-SVC-GKN7Y2BSGW4NJTYL -m comment --comment "default/nginx-service:" -j KUBE-SEP-T7B57UROKNW6T76L

1/3 的概率跳转到规则  KUBE-SEP-VMLJ2G2AI2XTLVYF

1/3 的概率(剩下 2/3 的一半)跳转到规则  KUBE-SEP-UZZUH7MRVLV5UEPK

还剩下的1/3 的概率跳转到规则 KUBE-SEP-T7B57UROKNW6T76L

上面的转发规则又通过DNAT目标地址转换来实现请求转发:

[root@master ~]# iptables-save |grep :80
-A KUBE-SEP-T7B57UROKNW6T76L -p tcp -m comment --comment "default/nginx-service:" -m tcp -j DNAT --to-destination 10.244.2.26:80
-A KUBE-SEP-UZZUH7MRVLV5UEPK -p tcp -m comment --comment "default/nginx-service:" -m tcp -j DNAT --to-destination 10.244.1.23:80
-A KUBE-SEP-VMLJ2G2AI2XTLVYF -p tcp -m comment --comment "default/nginx-service:" -m tcp -j DNAT --to-destination 10.244.1.22:80

可见,iptables 将访问 Service 的流量平均转发到后端 Pod,而且使用类似轮询的负载均衡策略。
另外需要补充一点:k8s集群中的每一个节点都配置了相同的 iptables 规则,这样就确保了整个集群都能够通过 Service 的 Cluster IP 访问 Service:

[root@master ~]# curl -I 10.110.59.203:8080
HTTP/1.1 200 OK
Server: nginx/1.8.1

[root@node1 ~]# curl -I 10.110.59.203:8080
HTTP/1.1 200 OK
Server: nginx/1.8.1

[root@node2 ~]# curl -I 10.110.59.203:8080
HTTP/1.1 200 OK
Server: nginx/1.8.1

所以整个集群中的每个节点都可以通过统一的clusterIP 来访问service,而且自动实现了应用之间的负载均衡。

需要注意的是,每个节点上的kube-proxy都时刻监听着apiserver,一旦pod或service的信息有改变动,都会马上在本地iptables中同步相关的规则信息,从而保证可用性。

比如销毁并重新创建了一个pod,kube-proxy会修改iptables相关规则中pod的ip信息;比如kubectl delete svc删除了一个service,kube-proxy会删除iptables关于这个svc的多条规则。

NodePort

我们的场景不全是集群内访问,也需要集群外业务访问。那么ClusterIP就满足不了了。NodePort就是其中的一种实现方案。

 

来实践一下 NodePort,Service的配置文件修改如下,其实就是改了一下type:

apiVersion: v1
kind: Service
metadata:
  name: nginx-service
spec:
  selector:
    run: nginx
  ports:
  - protocol: TCP
    port: 8080
    targetPort: 80
  type: NodePort

重新创建nginx-service:

[root@master service]# kubectl apply -f nginx-service.yaml 
service/nginx-service created
[root@master service]# kubectl get svc
NAME            TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)          AGE
kubernetes      ClusterIP   10.96.0.1       <none>        443/TCP          35d
nginx-service   NodePort    10.99.156.118   <none>        8080:31923/TCP   8s

Kubernetes 依然会为 nginx-svc 分配一个 ClusterIP,不同的是:
EXTERNAL-IP 为 nodes,表示可通过 Cluster 每个节点自身的 IP 访问 Service。
PORT(S) 为 8080:31923 ,8080 是 ClusterIP 监听的端口,31923  则是node节点上监听的端口。Kubernetes 会从 30000-32767 中分配一个可用的端口,每个节点都会监听此端口并将请求转发给 Service。

[root@master service]# netstat -anp|grep 31923
tcp        0      0 0.0.0.0:31923           0.0.0.0:*               LISTEN      2457/kube-proxy   

[root@node1 ~]# netstat -anp|grep 31923
tcp        0      0 0.0.0.0:31923           0.0.0.0:*               LISTEN      1635/kube-proxy  

[root@node2 ~]# netstat -anp|grep 31923
tcp        0      0 0.0.0.0:31923           0.0.0.0:*               LISTEN      1765/kube-proxy  

测试 NodePort 是否正常工作:

[root@master ~]# curl -I 172.31.93.200:31923
HTTP/1.1 200 OK
Server: nginx/1.8.1

[root@master ~]# curl -I 172.31.93.201:31923
HTTP/1.1 200 OK
Server: nginx/1.8.1

[root@master ~]# curl -I 172.31.93.202:31923
HTTP/1.1 200 OK
Server: nginx/1.8.1

通过三个节点 IP + 31923 端口都能够访问 nginx-service。

接下来探讨一个问题:Kubernetes 是如何将 <NodeIP>:<NodePort> 映射到 Pod 的呢?
与 ClusterIP 一样,也是借助了 iptables。与 ClusterIP 相比,每个节点的 iptables 中都增加了下面两条规则:

[root@master ~]# iptables-save |grep 31923
-A KUBE-NODEPORTS -p tcp -m comment --comment "default/nginx-service:" -m tcp --dport 31923 -j KUBE-MARK-MASQ
-A KUBE-NODEPORTS -p tcp -m comment --comment "default/nginx-service:" -m tcp --dport 31923 -j KUBE-SVC-GKN7Y2BSGW4NJTYL

规则的含义是,访问当前节点31923端口的请求会应用规则 KUBE-SVC-GKN7Y2BSGW4NJTYL ,内容为:

[root@master ~]# iptables-save |grep KUBE-SVC-GKN7Y2BSGW4NJTYL
:KUBE-SVC-GKN7Y2BSGW4NJTYL - [0:0]
-A KUBE-NODEPORTS -p tcp -m comment --comment "default/nginx-service:" -m tcp --dport 31923 -j KUBE-SVC-GKN7Y2BSGW4NJTYL
-A KUBE-SERVICES -d 10.99.156.118/32 -p tcp -m comment --comment "default/nginx-service: cluster IP" -m tcp --dport 8080 -j KUBE-SVC-GKN7Y2BSGW4NJTYL
-A KUBE-SVC-GKN7Y2BSGW4NJTYL -m comment --comment "default/nginx-service:" -m statistic --mode random --probability 0.33333333349 -j KUBE-SEP-VMLJ2G2AI2XTLVYF
-A KUBE-SVC-GKN7Y2BSGW4NJTYL -m comment --comment "default/nginx-service:" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-UZZUH7MRVLV5UEPK
-A KUBE-SVC-GKN7Y2BSGW4NJTYL -m comment --comment "default/nginx-service:" -j KUBE-SEP-T7B57UROKNW6T76L

可见,NodeIP:31923与10.99.156.118:8080的请求,采取了相同的处理方式,平均丢给后端三个pod的规则,策略还是轮休。

NodePort 默认是的随机选择,不过我们可以用 nodePort 指定某个特定端口:

现在配置文件中就有三个 Port 了:

  • nodePort 是节点上监听的端口
  • port 是 ClusterIP 上监听的端口
  • targetPort 是 Pod 监听的端口

最终,Node 和 ClusterIP 在各自端口上接收到的请求都会通过 iptables 转发到 Pod 的 targetPort。

LoadBalancer

LoadBalancer和NodePort其实是同一种方式。区别在于LoadBalancer比NodePort多了一步,就是利用云平台的LB来向节点导流,把<NodeIP>:<NodePort>自动添加到公有云的负载均衡当中。

ExternalName 

这种类型的 Service通过返回 CNAME和它的值,可以将服务映射到 externalName字段的内容。类型为 ExternalName 的service将服务映射到 DNS 名称,而不是典型的选择器。

一般使用在两个场景:

1.用于将集群外部的服务引入到集群内部,在集群内部可直接访问来获取服务。

例如,在k8s集群中需要保留使用aws的rds:

kind: Service
apiVersion: v1
metadata:
  name: test-service
  namespace: default
spec:
  type: ExternalName
  externalName: test.database.example.com

Web Pod尝试访问test-service上的数据库之后,Kubernetes DNS服务器将返回值为test.database.example.com的CNAME记录。

2.ExternalName service 也可以用于从其他namespace访问服务

例如:

kind: Service
apiVersion: v1
metadata:
  name: test-service-1
  namespace: namespace-a
spec:
  type: ExternalName
  externalName: test-service-2.namespace-b.svc.cluster.local
  ports:
  - port: 80 

在这里,就可以使用名称空间a中定义的test-service-1访问命名空间b中的服务test-service-2。

参考:https://www.jianshu.com/p/3ffd4e10bbdc
https://www.cnblogs.com/benjamin77/p/9908547.html
原文地址:https://www.cnblogs.com/xulan0922/p/14671940.html