【解构云原生】初识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如何扩缩容、如何删除重建,客户端都不需要关心。

【解构云原生】初识Kubernetes Service_第1张图片

(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              
nginx-5c7588df-gb2d8                                            1/1     Running       0          57s     10.120.49.152   pubt2-k8s-for-iaas4.dg.163.org              
nginx-5c7588df-gdngk                                            1/1     Running       0          57s     10.120.49.23    pubt2-k8s-for-iaas4.dg.163.org              

(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           80/TCP    23s   app=nginx

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

# curl 10.178.4.2:80



Welcome to nginx!



Welcome to nginx!

If you see this page, the nginx web server is successfully installed and working. Further configuration is required.

For online documentation and support please refer to nginx.org.
Commercial support is available at nginx.com.

Thank you for using nginx.

实现原理

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



Welcome to nginx!



Welcome to nginx!

If you see this page, the nginx web server is successfully installed and working. Further configuration is required.

For online documentation and support please refer to nginx.org.
Commercial support is available at nginx.com.

Thank you for using nginx.

域名格式为: ${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开发、运维经验,主导实现了容器网络管理、容器混部等生产级核心系统研发,推动网易集团内部电商、音乐、传媒、教育等多个业务的容器化。

你可能感兴趣的:(微服务)