服务暴漏

Service的IP地址仅在集群内可达,然而,总会有些服务需要暴漏到外部网络中接收各类客户端的访问,例如分层架构应用中的前端web应用程序等。此时,就需要在集群的边界为其添加一层转发机制,以实现将外部请求流量接入到集群的Service资源之上,这种操作也称为发布服务到外部网络中

service类型

K8S的Service共有四种类型

  • ClusterIP:通过集群内部IP地址暴漏服务,此地址仅在集群内部可达,而无法被集群外部的客户端访问,也是默认的Service类型
  • NodePort:这种类型建立在ClusterIP类型之上,其在每个节点的IP地址的某静态端口暴漏服务,因此,它依然会为Service分配集群IP地址,并将此作为NodePort的路由目标。简单来说,NodePort类型就是在工作节点的IP地址上选择一个端口用于将集群外部的用户请求转发至目标Service的ClusterIP和Port,因此,这种类型的Service即可如ClusterIP一样受到集群内部客户端Pod的访问,也会受到集群外部客户端通过套接字<nodeip>:<nodeport>进行的请求
    Servicee和Ingress(四)之Service服务暴漏 - 图1
  • LoadBalancer:这种类型建构在NodePort类型之上,其通过cloud provider提供的负载均衡器将服务暴漏到集群外部,因此LoadBalancer一样具有NodePort和ClusterIP。简而言之,一个LoadBanlancer类型的Service会指向关联至K8S集群外部的、切实存在的某个负载均衡设备,如下图。例如Amazon云计算环境中的ELB实例即为此类的负载均衡设备。此类型的优势在于,它能偶把来自于集群外部客户端的请求调度至所有节点(或部分节点)的NodePort之上,而不是依赖于各客户端自行决定连接至哪个节点,从而避免了因客户端指定的节点故障而导致的服务不可用
    Servicee和Ingress(四)之Service服务暴漏 - 图2
  • ExternalName:其通过将Service映射至由ExternalName字段的内容指定的主机名来暴漏服务,此主机名需要被DSN服务解析至CNAME类型的记录。换言之,此种类型并非定义由K8S集群的提供的服务,而是把集群外部的某服务以DNS CNAME记录的方式映射到集群内,从而让集群内的Pod资源能够以访问外部的Service的一种实现方式,如下图。因此这种类型的Service没有ClusterIP和NodePort也没有标签选择器用于选择Pod资源,因此也不会有endpoints存在
    Servicee和Ingress(四)之Service服务暴漏 - 图3

之前创建的myapp-svc就是使用的默认类型的ClusterIP类型的service资源,它仅能接收来自于集群中的Pod对象中的客户端程序的访问请求。如若需要将Service资源发布至网络外部,应该将其配置为NodePort或LoadBanlancer类型,而若要把外部的服务发布用于集群内Pod对象使用,则需要顶一个ExternalNmae类型的Service资源

NodePort类型的Service资源

NodePort即节点Port,通常在安装部署K8S集群时会预留一个端口范围用于NodePort,默认为30000~32767之间的端口。与clusterIP类型的可省略.spec.type属性所不同的是,定义NodePort类型的Service资源时,需要通过此属性明确指定其类型名称。例如,下面的配置清单中定义的Service资源对象myapp-svc-nodeport,它使用了NodePort类型,且人为指定其节点端口为32223

  1. kind: Service
  2. apiVersion: v1
  3. metadata:
  4. name: myapp-svc-nodeport
  5. spec:
  6. type: NodePort
  7. selector:
  8. app: myapp
  9. ports:
  10. - port: 80
  11. targetPort: 80
  12. nodePort: 32223

在实际的应用中,并不鼓励用户自定义使用节点的端口,除非事先能够明确知道它不会与某个现存的Service资源产生冲突。无论如何,只要没有特别需求,留给系统自动配置总是较好的选择。使用创建命令创建上面的service资源后即可了解其运行状态

  1. [root@k8s-master01 nginx]# kubectl apply -f myapp-svc-nodeport.yaml
  2. service/myapp-svc-nodeport created
  3. [root@k8s-master01 nginx]#
  4. [root@k8s-master01 nginx]#
  5. [root@k8s-master01 nginx]# kubectl get svc myapp-svc-nodeport
  6. NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
  7. myapp-svc-nodeport NodePort 10.99.225.19 <none> 80:32223/TCP 2m41s
  8. [root@k8s-master01 nginx]#

命令结果显示,nodePort类型的service资源依然会被配置ClusterIP,事实上,它会作为节点从NodePort接入流量后转发至目标地址,目标端口则是与Service资源对应的spec.ports.port属性中定义的端口 Servicee和Ingress(四)之Service服务暴漏 - 图4 因此,对于集群外部的客户端来说,它们可经由任何一个节点的节点IP及端口访问NodePort类型的Service资源,而对于集群内的Pod来说,依然可以通过ClusterIP对其进行访问

  1. [root@k8s-master01 nginx]# kubectl describe svc myapp-svc-nodeport
  2. Name: myapp-svc-nodeport
  3. Namespace: default
  4. Labels: <none>
  5. Annotations: kubectl.kubernetes.io/last-applied-configuration:
  6. {"apiVersion":"v1","kind":"Service","metadata":{"annotations":{},"name":"myapp-svc-nodeport","namespace":"default"},"spec":{"ports":[{"nod...
  7. Selector: app=myapp
  8. Type: NodePort
  9. IP: 10.99.225.19
  10. Port: <unset> 80/TCP
  11. TargetPort: 80/TCP
  12. NodePort: <unset> 32223/TCP
  13. Endpoints: 10.244.38.2:80,10.244.38.5:80,10.244.38.6:80 + 2 more...
  14. Session Affinity: None
  15. External Traffic Policy: Cluster
  16. Events: <none>
  17. [root@k8s-master01 nginx]#
  18. 使用集群外部的客户端进行访问测试,我这边直接使用master节点,请求node节点的32223端口
  19. [root@k8s-master01 nginx]# curl http://172.18.15.114:32223
  20. Hello MyApp | Version: v2 | <a href="hostname.html">Pod Name</a>
  21. [root@k8s-master01 nginx]#

LoadBalancer类型的Service资源

NodePort类型的Service资源虽然能够于集群外部访问的到,但外部客户端必须得事先得到NodePort和集群中至少一个节点的IP地址,且选定的节点发生故障时,客户端还得自行选择请求访问其他的节点。另外,集群节点很可能是某IaaS云环境中使用的私有IP地址的VM,或者是IDC中使用私有地址的物理机,这类地址对互联网客户端不可达,因此,一般还应该在集群之外创建一个具有公网IP地址的负载均衡器,由它接入外部客户端的请求流量,并调度至相应节点的NodePort之上 如果K8S集群部署在自己的私有环境当中,那么管理员可以手动部署一个负载均衡器,并将其配置为请求流量的入口,通过其调度算法将流量均衡到各节点的NodePort之上 下面是一个loadBalancer类型的Service资源配置清单,若K8S系统满足其使用条件,即可自行进行应用测试。需要注意的是,有些环境中可能还需要为Service资源的配置定义添加Annotations,因为我这边不具备测试条件,所以只给出参考示例

  1. apiVersion: v1
  2. kind: Service
  3. metadata:
  4. name: myapp-svc-lb
  5. spec:
  6. type: LoadBalancer
  7. selector:
  8. app: myapp
  9. ports:
  10. - port: 80
  11. targetPort: 80
  12. nodePort: 32223

通过上面的定义,可以创建出一个基于loadBanlancer的Service资源,然后在负载均衡器指定其后端节点IP地址和需要负载均衡的端口。如果在Iaas环境当中支持手动指定IP地址,用户还可以使用.spec.loadBalancerIP指定创建的负载均衡器使用的IP地址,并可以使用.spec.loadBalancerSourceRanges指定负载均衡器允许的客户端来源的地址范围

ExternalNmae类型的Service资源

ExternalName类型的Service资源用于将集群外部的服务发布到集群中以供Pod中的应用程序访问,因此,它不需要使用标签选择器关联任何的Pod对象,但是必须要使用.spec.externalName属性定义一个CNAME记录用于返回外部真正提供服务的主机的别名,而后通过CNAME记录值获取到相关主机的IP地址 下面是一个ExternalName类型的Service资源示例,名称为external-redis-svc,相应的externalName为redis.test.com

  1. apiVersion: v1
  2. kind: Service
  3. metadata:
  4. name: external-redis-svc
  5. spec:
  6. type: ExternalName
  7. externalName: redis.test.com
  8. selector: {}
  9. ports:
  10. - port: 6379
  11. targetPort: 6379
  12. nodePort: 0

创建资源

  1. [root@k8s-master01 nginx]# kubectl apply -f external-redis-svc.yaml
  2. service/external-redis-svc created
  3. [root@k8s-master01 nginx]#
  4. [root@k8s-master01 nginx]# kubectl get svc external-redis-svc -o wide
  5. NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
  6. external-redis-svc ExternalName <none> redis.test.com 6379/TCP 47s <none>
  7. [root@k8s-master01 nginx]#

资源创建完成后,各Pod对象既可以通过external-redis-svc或者FQDN格式的名称external-redis-svc.default.svc.cluster.local访问相应的服务。ClusterDNS会将此名称以CNAME格式解析为.spec.externalName字段中的名称,而后通过DNS服务将其解析为相应的主机的IP地址。

由于ExternalName类型的Service资源实现于DNS级别,客户端将直接接入外部的服务而完全不需要服务代理,因此,它无须配置ClusterIP,此种类型的服务也称为headless Service

外部服务Service

另外,也可以将已有的外部的服务比如一个外部的数据库作为后端服务进行连接,以Service的形式加入到K8S集群中,只需要在创建Service的时候不指定label selector,而是在Service创建好以后手动为其添加endpoint。

  1. apiVersion: v1
  2. kind: Service
  3. metadata:
  4. name: mysql-service
  5. spec:
  6. ports:
  7. - port: 3006
  8. targetPort: 3306

通过上面定义创建的是一个不带标签选择器的Service,即无法选择后端的Pod,系统不会自动创建endpoint,因此需要手动创建和该Service同名的Endpoint,用于指向实际的后端访问地址,我们可以通过手动添加 Endpoint 对象,将服务手动映射到运行该服务的网络地址和端口: 定义一个endpoint

  1. apiVersion: v1
  2. kind: Endpoint
  3. metadata:
  4. name: mysql-service
  5. subsets:
  6. - address:
  7. - IP: 1.2.3.4
  8. ports:
  9. - port: 3306

先来看看endpoint支持的可选字段

  1. [root@k8s-master01 nginx]# kubectl explain endpoints
  2. KIND: Endpoints
  3. VERSION: v1
  4. DESCRIPTION:
  5. Endpoints is a collection of endpoints that implement the actual service.
  6. Example: Name: "mysvc", Subsets: [ { Addresses: [{"ip": "10.10.1.1"},
  7. {"ip": "10.10.2.2"}], Ports: [{"name": "a", "port": 8675}, {"name": "b",
  8. "port": 309}] }, { Addresses: [{"ip": "10.10.3.3"}], Ports: [{"name": "a",
  9. "port": 93}, {"name": "b", "port": 76}] }, ]
  10. FIELDS:
  11. apiVersion <string>
  12. APIVersion defines the versioned schema of this representation of an
  13. object. Servers should convert recognized schemas to the latest internal
  14. value, and may reject unrecognized values. More info:
  15. https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
  16. kind <string>
  17. Kind is a string value representing the REST resource this object
  18. represents. Servers may infer this from the endpoint the client submits
  19. requests to. Cannot be updated. In CamelCase. More info:
  20. https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
  21. metadata <Object>
  22. Standard object's metadata. More info:
  23. https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata
  24. subsets <[]Object>
  25. The set of all endpoints is the union of all subsets. Addresses are placed
  26. into subsets according to the IPs they share. A single address with
  27. multiple ports, some of which are ready and some of which are not (because
  28. they come from different containers) will result in the address being
  29. displayed in different subsets for the different ports. No address will
  30. appear in both Addresses and NotReadyAddresses in the same subset. Sets of
  31. addresses and ports that comprise a service.
  32. [root@k8s-master01 nginx]#

通过上面的输出结果可以看到,除了subsets字段不一样,其他字段全部一样,使用kubectl explain endpoint.subsets查看该字段支持的二级嵌套字段

  1. [root@k8s-master01 nginx]# kubectl explain endpoints.subsets
  2. KIND: Endpoints
  3. VERSION: v1
  4. RESOURCE: subsets <[]Object>
  5. DESCRIPTION:
  6. The set of all endpoints is the union of all subsets. Addresses are placed
  7. into subsets according to the IPs they share. A single address with
  8. multiple ports, some of which are ready and some of which are not (because
  9. they come from different containers) will result in the address being
  10. displayed in different subsets for the different ports. No address will
  11. appear in both Addresses and NotReadyAddresses in the same subset. Sets of
  12. addresses and ports that comprise a service.
  13. EndpointSubset is a group of addresses with a common set of ports. The
  14. expanded set of endpoints is the Cartesian product of Addresses x Ports.
  15. For example, given: { Addresses: [{"ip": "10.10.1.1"}, {"ip":
  16. "10.10.2.2"}], Ports: [{"name": "a", "port": 8675}, {"name": "b", "port":
  17. 309}] } The resulting set of endpoints can be viewed as: a: [
  18. 10.10.1.1:8675, 10.10.2.2:8675 ], b: [ 10.10.1.1:309, 10.10.2.2:309 ]
  19. FIELDS:
  20. addresses <[]Object>
  21. IP addresses which offer the related ports that are marked as ready. These
  22. endpoints should be considered safe for load balancers and clients to
  23. utilize.
  24. notReadyAddresses <[]Object>
  25. IP addresses which offer the related ports but are not currently marked as
  26. ready because they have not yet finished starting, have recently failed a
  27. readiness check, or have recently failed a liveness check.
  28. ports <[]Object>
  29. Port numbers available on the related IP addresses.
  30. [root@k8s-master01 nginx]#

可支持的二级嵌套字段全部是对象列表,这里就不做讲解了,感兴趣可以自行研究一下 访问没有标签选择器的Servcie和带有标签选择器的Service一样,请求将会被路由到由用户手动定义的后端的Endpoint

Headless类型的Service资源

Service对象隐藏了各Pod资源,并负责将客户端的请求流量调度至该组Pod对象之上。不过,偶尔也会存在这样一类需求:客户端需要直接访问service资源后端的所有Pod资源,这时就应该向客户端暴漏每个Pod资源的IP地址,而不再是中间层Serice对象的ClusterIP,这种类型的Service资源便称为Headless service Headless Service对象没有ClusterIP,于是,kube-proxy便无须处理此类请求,也就更没有了负载均衡或代理的需要。在前端应用拥有自由的其他服务发现机制时,headless service即可省去定义clusterIP的需求。至于如何为此类service资源配置IP地址,则取决于它的标签选择器的定义

  • 具有标签选择器:端点控制器(Endpoints Controller)会在API中为其创建Endpoints记录,并将ClusterDNS服务器中的A记录直接解析到此Service后端的各Pod对象的IP地址上。通俗点讲,就是通过DNS A记录设置后端endpoint列表
  • 没有标签选择器:端点控制器(Endpoints Controller)不会在API中为其创建Endpoints记录,ClusterDNS的配置分为两种情形,对ExternalName类型的服务创建CNAME记录,对其他三种类型来说,为那些与当前Service共享名称的所有endpoints对象创建一条记录
  • 对于“去中心化”类的应用集群,headless service将非常有用。

创建headless service资源

配置headless service资源配置清单时,只需要将ClusterIP字段的值设置为None即可将其定义为headless类型。下面是一个headless service资源配置清单示例,并且属于拥有标签选择器

  1. apiVersion: v1
  2. kind: Service
  3. metadata:
  4. name: myapp-headless-svc
  5. spec:
  6. clusterIP: None
  7. selector:
  8. app: myapp
  9. ports:
  10. - name: http
  11. port: 80
  12. targetPort: 80

使用资源创建命令完成资源创建后,使用相关的查看命令获取Service资源的相关信息便可用看出,它没有ClusterIP,不过,如果标签选择器能够匹配到相关的Pod资源,它便拥有Endpoints记录,这些Endpoints对象会作为DNS资源记录myapp-headless-svc查询时的A记录解析结果

  1. [root@k8s-master01 nginx]# kubectl apply -f myapp-headless-svc.yaml
  2. service/myapp-headless-svc created
  3. [root@k8s-master01 nginx]#
  4. [root@k8s-master01 nginx]# kubectl get svc myapp-headless-svc
  5. NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
  6. myapp-headless-svc ClusterIP None <none> 80/TCP 2m2s
  7. [root@k8s-master01 nginx]#
  8. [root@k8s-master01 nginx]# kubectl describe svc myapp-headless-svc
  9. Name: myapp-headless-svc
  10. Namespace: default
  11. Labels: <none>
  12. Annotations: kubectl.kubernetes.io/last-applied-configuration:
  13. {"apiVersion":"v1","kind":"Service","metadata":{"annotations":{},"name":"myapp-headless-svc","namespace":"default"},"spec":{"clusterIP":"N...
  14. Selector: app=myapp
  15. Type: ClusterIP
  16. IP: None
  17. Port: http 80/TCP
  18. TargetPort: 80/TCP
  19. Endpoints: 10.244.38.3:80,10.244.38.7:80,10.244.59.2:80 + 2 more...
  20. Session Affinity: None
  21. Events: <none>
  22. [root@k8s-master01 nginx]#

Pod资源发现

根据Headless Service的工作特性可知,记录于ClusterDNS的A记录的相关解析结果是后端的Pod资源对象的IP地址,这就意味着客户端通过此Service资源的名称发现的是各Pod资源。创建一个专用的测试Pod或者直接是使用现有的Pod,通过交互式接口进行测试

  1. [root@k8s-master01 nginx]# kubectl exec -ti myapp-57bd958885-2mv6z -- /bin/sh
  2. / # nslookup myapp-headless-svc
  3. nslookup: can't resolve '(null)': Name does not resolve
  4. Name: myapp-headless-svc
  5. Address 1: 10.244.38.3 myapp-57bd958885-2mv6z
  6. Address 2: 10.244.38.7 10-244-38-7.myapp-svc-nodeport.default.svc.cluster.local
  7. Address 3: 10.244.59.3 10-244-59-3.myapp-svc.default.svc.cluster.local
  8. Address 4: 10.244.59.5 10-244-59-5.myapp-svc.default.svc.cluster.local
  9. Address 5: 10.244.59.2 10-244-59-2.myapp-svc.default.svc.cluster.local
  10. / #

解析到的结果正是Headless service通过标签选择器关联到的所有Pod资源的IP地址。于是,客户端向此Service对象发起的请求将直接接入到Pod资源中的应用上,而不再由Service资源进行代理转发,它每次接入的Pod资源则是由DNS服务器接收到查询请求时以沦陷的方式返回的IP地址