乐趣区

解构云原生初识Kubernetes-Service

编者按:云原生是网易杭州研究院(网易杭研)奉行的核心技术方向之一,开源容器平台 Kubernetes 作为云原生产业技术标准、云原生生态基石,在设计上不可避免有其复杂性,Kubernetes 系列文章基于网易杭研资深工程师总结,多角度多层次介绍 Kubernetes 的原理及运用,如何解决生产中的实际需求及规避风险,希望与读者深入交流共同进步。

本文由作者授权发布,未经许可,请勿转载。

作者:李岚清,网易杭州研究院云计算技术中心资深工程师

为什么引入 service

众所周知,pod 的生命周期是不稳定的,可能会朝生夕死,这也就意味着 pod 的 ip 是不固定的。

比如我们使用三副本的 deployment 部署了 nginx 服务,每个 pod 都会被分配一个 ip,由于 pod 的生命周期不稳定,pod 可能会被删除重建,而重建的话 pod 的 ip 地址就会改变。也有一种场景,我们可能会对 nginx deployment 进行扩缩容,从 3 副本扩容为 5 副本或者缩容为 2 副本。当我们需要访问上述的 nginx 服务时,客户端对于 nginx 服务的 ip 地址就很难配置和管理。

因此,kubernetes 社区就抽象出了 service 这个资源对象或者说逻辑概念。

什么是 service

service 是 kubernetes 中最核心的资源对象之一,kubernetes 中的每个 service 其实就是我们经常提到的“微服务”。

service 定义了一个服务的入口地址,它通过 label selector 关联后端的 pod。service 会被自动分配一个 ClusterIP,service 的生命周期是稳定的,它的 ClusterIP 也不会发生改变,用户通过访问 service 的 ClusterIP 来访问后端的 pod。所以,不管后端 pod 如何扩缩容、如何删除重建,客户端都不需要关心。

(1)创建一个三副本的 nginx deployment:
nginx.yaml

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - image: nginx
        imagePullPolicy: Always
        name: nginx
# kubectl create -f nginx.yaml
deployment.extensions/nginx created

# kubectl get pods -o wide
nginx-5c7588df-5dmmp                                            1/1     Running       0          57s     10.120.49.230   pubt2-k8s-for-iaas4.dg.163.org   <none>           <none>
nginx-5c7588df-gb2d8                                            1/1     Running       0          57s     10.120.49.152   pubt2-k8s-for-iaas4.dg.163.org   <none>           <none>
nginx-5c7588df-gdngk                                            1/1     Running       0          57s     10.120.49.23    pubt2-k8s-for-iaas4.dg.163.org   <none>           <none>

(2)创建 service,通过 label selector 关联 nginx pod:

svc.yaml

apiVersion: v1
kind: Service
metadata:
  name: nginx
spec:
  type: ClusterIP
  selector:
    app: nginx
  ports:
  - port: 80
    protocol: TCP
    targetPort: 80
# kubectl create -f svc.yaml
service/nginx created

# kubectl get svc nginx -o wide
NAME    TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE   SELECTOR
nginx   ClusterIP   10.178.4.2   <none>        80/TCP    23s   app=nginx

(3)在 k8s 节点上访问 service 地址

# curl 10.178.4.2:80
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
    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>

实现原理

service 中有几个关键字段:

  • spec.selector: 通过该字段关联属于该 service 的 pod
  • spec.clusterIP: k8s 自动分配的虚拟 ip 地址
  • spec.ports: 定义了监听端口和目的端口。用户可以通过访问 clusterip: 监听端口 来访问后端的 pod

当用户创建一个 service 时,kube-controller-manager 会自动创建一个跟 service 同名的 endpoints 资源:

# kubectl get endpoints nginx
NAME    ENDPOINTS                                           AGE
nginx   10.120.49.152:80,10.120.49.23:80,10.120.49.230:80   12m

endpoints 资源中,保存了该 service 关联的 pod 列表,这个列表是 kube-controller-manager 自动维护的,当发生 pod 的增删时,这个列表会被自动刷新。

比如,我们删除了其中的一个 pod:

# kubectl delete pods nginx-5c7588df-5dmmp
pod "nginx-5c7588df-5dmmp" deleted

# kubectl get pods
nginx-5c7588df-ctcml                                            1/1     Running     0          6s
nginx-5c7588df-gb2d8                                            1/1     Running     0          18m
nginx-5c7588df-gdngk                                            1/1     Running     0          18m

可以看到 kube-controller-manager 立马补充了一个新的 pod。然后我们再看一下 endpoints 资源,后端 pod 列表也被自动更新了:

# kubectl get endpoints nginx
NAME    ENDPOINTS                                          AGE
nginx   10.120.49.152:80,10.120.49.23:80,10.120.49.73:80   16m

那么,当用户去访问 clusterip:port 时,流量是如何负载均衡到后端 pod 的呢?

k8s 在每个 node 上运行了一个 kube-proxy 组件,kube-proxy会 watch service 和 endpoints 资源,通过配置 iptables 规则(现在也支持 ipvs,不过不在本文章讨论范围之内)来实现 service 的负载均衡。

可以在任一个 k8s node 上看一下上述 nginx service 的 iptables 规则:

# iptables -t nat -L PREROUTING
Chain PREROUTING (policy ACCEPT)
target     prot opt source               destination
KUBE-SERVICES  all  --  anywhere             anywhere             /* kubernetes service portals */

# iptables -t nat -L KUBE-SERVICES
Chain KUBE-SERVICES (2 references)
target     prot opt source               destination
KUBE-SVC-4N57TFCL4MD7ZTDA  tcp  --  anywhere             10.178.4.2           /* default/nginx: cluster IP */ tcp dpt:http

# iptables -t nat -L KUBE-SVC-4N57TFCL4MD7ZTDA
Chain KUBE-SVC-4N57TFCL4MD7ZTDA (1 references)
target     prot opt source               destination
KUBE-SEP-AHN4ALGUQHWJZNII  all  --  anywhere             anywhere             statistic mode random probability 0.33332999982
KUBE-SEP-BDD6UBFFJ4G2PJDO  all  --  anywhere             anywhere             statistic mode random probability 0.50000000000
KUBE-SEP-UR2OSKI3P5GEGC2Q  all  --  anywhere             anywhere

# iptables -t nat -L KUBE-SEP-AHN4ALGUQHWJZNII
Chain KUBE-SEP-AHN4ALGUQHWJZNII (1 references)
target     prot opt source               destination
KUBE-MARK-MASQ  all  --  10.120.49.152        anywhere
DNAT       tcp  --  anywhere             anywhere             tcp to:10.120.49.152:80

当用户访问 clusterip:port 时,iptables 会通过 iptables DNAT 均衡的负载均衡到后端 pod。

service ClusterIP

service 的 ClusterIP 是一个虚拟 ip,它没有附着在任何的网络设备上,仅仅存在于 iptables 规则中,通过 dnat 实现访问 clusterIP 时的负载均衡。

当用户创建 service 时,k8s 会自动从 service 网段中分配一个空闲 ip 设置到 .spec.clusterIP 字段。当然,k8s 也支持用户在创建 svc 时自己指定 clusterIP。

service 的网段是通过 kube-apiserver 的命令行参数 --service-cluster-ip-range 配置的,不允许变更。

service 网段不能跟机房网络、docker 网段、容器网段冲突,否则可能会导致网络不通。

service 的 clusterIP 是 k8s 集群内的虚拟 ip,不同的 k8s 集群可以使用相同的 service 网段,在 k8s 集群外是访问不通 service 的 clusterIP 的。

service 域名

kubernetes 是有自己的域名解析服务的。比如我们可以通过访问域名 nginx.default.svc.cluster.local 来访问上述的 nginx 服务:

$ curl nginx.default.svc.cluster.local
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
    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>

域名格式为:${ServiceName}.${Namespace}.svc.${ClusterDomain}. 其中 ${ClusterDomain}的默认值是 cluster.local,可以通过 kubelet 的命令行参数----cluster-domain 进行配置。

headless service

当不需要 service ip 的时候,可以在创建 service 的时候指定spec.clusterIP: None,这种 service 即是 headless service。由于没有分配 service ip,kube-proxy 也不会处理这种 service。

DNS 对这种 service 的解析:

  • 当 service 里定义 selector 的时候:Endpoints controller 会创建相应的 endpoints。DNS 里的 A 记录会将 svc 地址解析为这些 pods 的地址
  • 当 service 里没有定义 selector:Endpoints controller 不会创建 endpoints。DNS 会这样处理:
  • 首先 CNAME 到 service 里定义的 ExternalName
  • 没有定义 ExternalName 的话,会搜寻所有的和这个 service 共享 name 的 Endpoints,然后将 A 记录解析到这些 Endpoints 的地址

service 的不同类型

service 支持多种不同的类型,包括 ClusterIPNodePortLoadBalancer,通过字段spec.type 进行配置。

ClusterIP service

默认类型。对于 ClusterIP service,k8s 会自动分配一个只在集群内可达的虚拟的 ClusterIP,在 k8s 集群外无法访问。

NodePort service

k8s 除了会给 NodePort service 自动分配一个 ClusterIP,还会自动分配一个 nodeport 端口。集群外的客户端可以访问任一 node 的 ip 加 nodeport,即可负载均衡到后端 pod。

nodeport 的端口范围可以通过 kube-apiserver 的命令行参数 --service-node-port-range 配置,默认值是30000-32767,当前我们的配置是30000-34999

但是客户端访问哪个 node ip 也是需要考虑的一个问题,需要考虑高可用。而且 NodePort 会导致访问后端服务时多了一跳,并且可能会做 snat 看不到源 ip。

另外需要注意的是,service-node-port-range 不能够跟几个端口范围冲突:

  • Linux 的net.ipv4.ip_local_port_range,可以配置为 35000-60999
  • ingress nginx 中的四层负载均衡,端口必须小于 30000
  • 其他普通业务的端口也需要小于 30000

LoadBalancer service

LoadBalancer service 需要对接云服务提供商的 NLB 服务。当用户创建一个 LoadBalancer 类型的 sevice 时,cloud-controller-manager会调用 NLB 的 API 自动创建 LB 实例,并且将 service 后端的 pod 挂到 LB 实例后端。

apiVersion: v1
kind: Service
metadata:
  name: nginx
spec:
  ports:
  - port: 80
    protocol: TCP
    targetPort: 80
  type: LoadBalancer
$ kubectl get svc nginx
NAME      TYPE           CLUSTER-IP     EXTERNAL-IP     PORT(S)        AGE
nginx     LoadBalancer   10.178.8.216   10.194.73.147   80:32514/TCP   3s

service 中会话保持

用户可以通过配置 spec.serviceAffinity=ClientIP 来实现基于客户端 ip 的会话保持功能。该字段默认为 None。

还可以通过适当设置 service.spec.sessionAffinityConfig.clientIP.timeoutSeconds 来设置最大会话停留时间。(默认值为 10800 秒,即 3 小时)

apiVersion: v1
kind: Service
metadata:
  name: nginx
spec:
  ports:
  - port: 80
    protocol: TCP
    targetPort: 80
  type: ClusterIP
  sessionAffinity: ClientIP

kubernetes service

当我们部署好一个 k8s 集群之后,发现系统自动帮忙在 default namespace 下创建了一个 name 为kubernetes 的 service:

# kubectl get svc kubernetes -o yaml
apiVersion: v1
kind: Service
metadata:
  labels:
    component: apiserver
    provider: kubernetes
  name: kubernetes
  namespace: default
spec:
  clusterIP: 10.178.4.1
  ports:
  - name: https
    port: 443
    protocol: TCP
    targetPort: 6443
  sessionAffinity: None
  type: ClusterIP
status:
  loadBalancer: {}

可以看到 kubernetes svc 的 ip 是--service-cluster-ip-range 的第一个 ip,并且该 service 没有设置spec.selector。理论上来说,对于没有设置 selector 的 svc,kube-controller-manager 不会自动创建同名的 endpoints 资源出来。

但是我们看到是有同名的 endpoints 存在的,并且多个 apiserver 的地址也被保存在 endpoints 资源中:

# kubectl get ep kubernetes
NAME         ENDPOINTS                         AGE
kubernetes   10.120.0.2:6443,10.120.0.3:6443   137d

具体是如何实现的,感兴趣的可以看下源码k8s.io/kubernetes/pkg/master/reconcilers

Frequently Asked Questions

问题一 为什么 service clusterip 无法 ping 通

因为 service clusterip 是一个 k8s 集群内部的虚拟 ip,没有附着在任何网络设备上,仅仅存在于 iptables nat 规则中,用来实现负载均衡。

问题二 为什么 service 的网段不能跟 docker 网段、容器网段、机房网段冲突

假如 service 网段跟上述网段冲突,很容易导致容器或者在 k8s node 上访问上述网段时发生网络不通的情况。

问题三 为什么在 k8s 集群外无法访问 service clusterip

service clusterip 是 k8s 集群内可达的虚拟 ip,集群外不可达。不同的 k8s 集群可以使用相同的 service 网段。

或者说,集群外的机器上没有本 k8s 集群的 kube-proxy 组件,没有创建对应的 iptables 规则,因此集群外访问不通 service clusterip。

问题四 能否扩容 service 网段

原则上这个网段是不允许更改的,但是假如因为前期规划的问题分配的网段过小,实际可以通过比较 hack 的运维手段扩容 service 网段。

问题五 service 是否支持七层的负载均衡

service 仅支持四层的负载均衡,七层的负载均衡需要使用 ingress

参考文档

  1. https://kubernetes.io/docs/concepts/services-networking/service/

作者简介

李岚清,网易杭州研究院云计算技术中心容器编排团队资深系统开发工程师,具有多年 Kubernetes 开发、运维经验,主导实现了容器网络管理、容器混部等生产级核心系统研发,推动网易集团内部电商、音乐、传媒、教育等多个业务的容器化。

退出移动版