kubernetes | Service

2022年3月17日 · 4512 字 · 10 分钟 · Kubernetes

一、Service 概述

Service 是 Kubernetes 最核心概念,通过创建 Service,可以为一组具有相同功能的容器应用提供一个统一的入口地址,并且将请求负载分发到后端的各个容器应用上。 目前一致的 service 有两个要求:标签匹配,就绪,如果不满足这两个,则无法匹配。

二、service 类型

2.1、ClusterIp

默认类型,自动分配一个仅 Cluster 内部可以访问的虚拟 IP。

2.2、NodePort(节点端口)

在 ClusterIP 基础上为 Service 在每台机器上绑定一个端口,这样就可以通过 <NodeIP>: NodePort 来访问该服务。

2.3、LoadBalancer

在 NodePort 的基础上,借助 cloud provider 创建一个外部负载均衡器,并将请求转发到 <NodeIP>: NodePort。

2.4、ExternalName(DNS 别名)

把集群外部的服务引入到集群内部来,在集群内部直接使用。没有任何类型代理被创建,这只有 kubernetes 1.7 或更高版本的 kube-dns 才支持。 DNS 别名解析流程: 我们要创建一个 ExternalName 的 service(domain),当创建完 domain 以后,我们就会得到一个域名,tomcat 只要去访问到这个域名,这个域名就会经过 coredns 解析,解析到一个结果上,这个结果可能是一个域名,也可能是一个 IP,这个域名或 IP 可能指向这个 db。 当外部的 db 的 IP 发生变化时,只要修改 domain 的别名地址就好了,例如 db 数据库原来是 bbs.aaa.com,现在是 db.aaa.com,那么只需要将 domain 的别名地址写成 db.aaa.com 即可。

三、Service 的定义

yaml 格式的 Service 定义文件。

用户/其他 Pod –> nodePort (外部访问)–> port (Service 集群内端口) –> targetPort (Pod 内部程序端口)

apiVersion: v1
kind: Service
metadata:
  name: string
  namespace: string
  labels:
    name: string
  annotations:
    name: string
spec:
  selector: {}              # 选择器,用于关联 Pod
  type: string              # 可选值:ClusterIP, NodePort, LoadBalancer, ExternalName
  clusterIP: string
  sessionAffinity: string   # 可选值:None, ClientIP。如果你希望同一个客户端的请求总是发往同一个 Pod(例如处理登录会话)可配置为 ClientIP。
  ports:
    - name: string
      protocol: string      # 可选值:TCP, UDP, SCTP
      port: int             # Service 端口
      targetPort: int       # Pod 端口
      nodePort: int         # 节点端口(当 type: NodePort 时使用)
status:                     # 手动编写 YAML 时,通常不需要写 status 部分。那是 Kubernetes 集群在服务运行起来后,自动填入并反馈给你的运行状态信息(比如云服务商分配的公网 IP)。
  loadBalancer:
    ingress:
      - ip: string
        hostname: string

常用类型 (spec.type) 的选择

类型场景说明
ClusterIP内部通信默认值。仅集群内可访问,用于数据库或内部微服务。
NodePort测试/外网访问在所有节点上开启一个端口(30000-32767),通过 节点IP:nodePort 访问。
LoadBalancer生产环境配合云厂商(如阿里云、AWS)使用,自动创建一个公网负载均衡器。

参数说明

属性名称取值类型是否必选取值说明
versionstringRequiredv1
kindstringRequiredService
metadataobjectRequired元数据
metadata.namestringRequiredService名称
metadata.namespacestringRequired命名空间,默认为default
metadata.labels[]list自定义标签属性列表
metadata.annotation[]list自定义注解属性列表
specobjectRequired详细描述
spec.selector[]listRequiredLabel Selector配置,将选择具有指定Label标签的Pod作为管理范围
spec.typestringRequiredService的类型,指定Service的访问方式,默认值为ClusterIP。取值范围如下:1. ClusterIP: 虚拟服务的IP,用于k8s集群内部的pod访问,在Node上kube-proxy通过设置的iptables规则进行转发。2. NodePort: 使用宿主机的端口,使得能够访问各Node的外部客户端通过Node的IP地址和端口就能访问服务。3. LoadBalancer: 使用外接负载均衡器完成到服务的负载分发,需要在spec.status.loadBalancer字段指定外部负载均衡器的IP地址,并同时定义nodePort和clusterIP,用于公有云环境。
spec.clusterIPstring虚拟服务的IP地址,当type=ClusterIP时,如果不指定,则系统进行自动分配。也可以手工指定。当type=LoadBalancer时,则需要指定。
spec.sessionAffinitystring是否支持Session,可选值为ClientIP,表示将同一个源IP地址的客户端访问请求都转发到同一个后端Pod。默认值为。
spec.ports[]listService需要暴露的端口列表
spec.ports[].namestring端口名称
spec.ports[].protocolstring端口协议,支持TCP和UDP,默认值为TCP
spec.ports[].portint服务监听的端口号
spec.ports[].targetPortint需要转发到后端Pod的端口号
spec.ports[].nodePortint当spec.type=NodePort时,指定映射到物理机的端口号
statusobject当spec.type=LoadBalancer时,设置外部负载均衡器的地址,用于公有云环境
status.loadBalancerobject外部负载均衡器
status.loadBalancer.ingressobject外部负载均衡器
status.loadBalancer.ingress.ipstring外部负载均衡器的IP地址
status.loadBalancer.ingress.hostnamestring外部负载均衡器的主机名

四、使用

4.1、ClusterIP 创建

IPVS 是基于 NAT 工作模式运行的。

点击查看完整的 ClusterIP.yml
apiVersion: v1
kind: Service		        #service 类型
metadata:
  name: myapp		        # 当前 service 的名是 myapp
  namespace: default		# 放到 default 名称空间下
spec:
  type: ClusterIP	        # ClusterIP 的类型
  selector:                     # 标签选择器,匹配下面两个标签
    app: myapp	
    release: stable
  ports:                        # 端口,
  - name: http		        # 端口名称为 http
    port: 80		        # 端口是 80,集群访问的 VIP 的端口。
    targetPort: 80	        # 后端目标 80,真实服务器的端口

创建

# 基于 yaml 文件创建 ClusterIP 类型的 svc
kubectl apply -f ClusterIP.yaml
    
# 查看已经创建的 svc
kubectl get svc  

流量转发路径

当你在集群内部访问 myapp 这个服务时,流量遵循以下路径:

  • 访问入口:myapp-service-ip:80 (ClusterIP VIP)
  • 内核拦截:流量到达节点内核,IPVS 规则拦截该 VIP。
  • 负载均衡:IPVS 根据算法(默认通常是轮询 rr)在匹配 app: myapp 和 release: stabel 的后端 Pod 中选一个。
  • DNAT 转换:IPVS 执行目标地址转换(DNAT),将 VIP 换成 Pod 的实际 IP。
  • 送达:流量最终到达 Pod 的 80 端口。

4.2、NodePort 创建

相比之前的 ClusterIP,它的最大区别在于:它在集群的每一个节点(Node)上都开启了一个物理端口,从而允许外部流量访问集群内部的服务。

点击查看完整的 NodePort.yml
apiVersion: v1
kind: Service
metadata:
  name: myapp-nodeport
  namespace: default		# 放在 default 名称空间下
spec:
  type: NodePort		    # 当前的工作模式是 NodePort 方式
  selector:
    app: myapp
    release: stabel
  ports:
  - name: http		        # 定义的别名为 http
    port: 80		        # 集群的端口 80
    targetPort: 80	        # 后端的端口 80
    nodePort: 31080         # 手动指定物理机映射端口。没写的话,会在 30000-32767 之间随机分配一个空闲端口。

创建

kubectl apply -f 6.nodeport.yaml

# 这里映射的端口,必须大于三万以上。默认就可以,不用修改,防止冲突
# 在外部访问物理机的 31225 端口即可映射到容器内部的 80 端口,达到访问容器的内目的
kubectl get svc

流量访问链路

用户浏览器 –> 任何一个节点的物理IP : nodePort端口号 –> Service (80) –> Pod (80)

自动分配的端口

默认行为:Kubernetes 会在 30000-32767 之间随机分配一个空闲端口。
查看端口:部署后,你需要运行 kubectl get svc myapp-nodeport 来查看系统到底分了哪个端口。

4.3、LoadBalancer 创建

与 nodeport 的区别在于,loadbalancer 会自动在外部创建一个调度器将当前的 nodeport 端口进行流量访问,实现高可用,但是必须得借助云平台创建 LB 来向节点导流。 loadBalancer 和 nodePort 其实是同一种方式。区别在于 loadBalancer 比 nodePort 多了一步,就是可以调用 cloud provider 去创建 LB 来向节点导流。

点击查看完整的 loadbalancer.yml
apiVersion: v1
kind: Service
metadata:
  name: myapp-loadbalancer      # 服务的名称,这里是 myapp-loadbalancer。
spec:
  selector:
    app: myapp                  # 匹配带有相应标签的 Pods。在这个例子中,它匹配所有标签为 app: myapp 的 Pods。
  type: LoadBalancer            # 为 LoadBalancer,表明我们想要的服务类型是负载均衡器。
  ports:                        # 服务的端口配置;
  - protocol: TCP
    port: 80                    # 服务内部端口,外部请求会到这个端口;
    targetPort: 8080            # Pods 上的端口,服务会将流量从内部端口转发到此端口;
    # 如果云提供商支持,也可以指定一个负载均衡器的外部端口,例如:
    # nodePort: 30000           #(可选)如果指定,这是节点上用来接收外部流量的端口

创建

kubectl apply -f loadbalancer.yaml
kubectl get svc myapp-loadbalancer

自动化流水线

当你将 type 设置为 LoadBalancer 时,Kubernetes 内部会发生一系列连锁反应:

  • 自动分配 NodePort:K8s 会先自动生成一个随机的 nodePort(如 31234)。
  • 云控制器介入:集群中的 Cloud Controller Manager 会调用底层云服务商(如阿里云、腾讯云、AWS 或银行内部负载均衡系统)的 API。
  • 创建外部 LB:云商会自动为你创建一个具备公网或私有 VIP 的负载均衡实例。
  • 打通路径:外部负载均衡器会自动将后端指向你所有节点的 nodePort。

4.4、ExternalName 创建

这种类型的 Service 通过返回 CNAME 和它的值,可以将服务映射到 externalName 字段的内容 ( 例如:hub.aaa.com )。ExternalName Service 是 Service 的特例,它没有 selector,也没有定义任何的端口和 Endpoint。相反的,对于运行在集群外部的服务,它通过返回该外部服务的别名这种方式来提供服务。

kind: Service
apiVersion: v1
metadata:
  name: my-service	            # 当前 service 名为 my-service
  namespace: default	            # 放在 default 名称空间下
spec:
  type: ExternalName	            # 当前的类型为 ExternalName 类型
  externalName: hub.aaa.com	    # ExternalName

my-service.defalut.svc.cluster.local 这是在集群内可以访问到的域名,如果将他的值找 dns 解析,解析的应该是 hub.aaa.com。

例如:我有一个 pod,我去访问一个 web 服务,web 服务就是 hub.hongfu.com,就可以靠这个 svc 指向这个 hub.aaa.com,如果有一天我想指向 harbor,那只要 dit 将这个值指向 haobor,他就指过来了,对于 pod 来说,不需要做任何改变。 当查询主机 my-service.defalut.svc.cluster.local ( SVC_NAME.NAMESPACE.svc.cluster.local ) 时,集群的 DNS 服务将返回一个值 my.database.example.com 的 CNAME 记录。访问这个服务的工作方式和其他的相同,唯一不同的是重定向发生在 DNS 层,而且不会进行代理或转发。

DNS 级别的手法

当你创建一个 ExternalName 类型的 Service 时,Kubernetes 的集群 DNS 服务(CoreDNS)会插入一条记录:

  • 内部域名:my-service.default.svc.cluster.local
  • 指向目标:hub.aaa.com (这就是你配置的 externalName)

工作原理:
当集群内的 Pod 访问 my-service 时,DNS 会直接返回 hub.aaa.com 的 CNAME 记录。解析过程完全在 DNS 层完成,流量不会经过 kube-proxy 或 IPVS/iptables 转发。

作用

这个功能通常用于平滑迁移或跨环境访问:

  • 解耦外部依赖:假设你的程序需要连接一个外部数据库或镜像仓库(如 hub.aaa.com)。如果你在代码里硬编码这个域名,万一以后地址变了,你就得改代码。
  • 统一访问入口:使用 ExternalName 后,代码里只需要访问 http://my-service。如果以后你决定把这个服务迁移到集群内部运行,你只需要把这个 Service 的类型改回 ClusterIP 并加上 selector,代码一行都不用动。

五、Service 示例

一般来说,对外提供服务的应用程序需要通过某种机制来实现,对于容器应用最简便的方式就是通过 TCP/IP 机制及监听 IP 和端口号来实现。创建一个基本功能的 Service。

点击查看完整的 exmapleService.yml
apiVersion: v1
kind: ReplicationController
metadata:
  name: mywebapp
spec:
  replicas: 2
  template:
    metadata:
      name: mywebapp
      labels:
        app: mywebapp
    spec:
      containers:
        - name: mywebapp
          image: tomcat
          ports:
            - containerPort: 8080

deployment 版本:

点击查看完整的 exmapleService.yml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: mywebapp
spec:
  replicas: 2
  selector:
    matchLabels:
      app: mywebapp # Deployment 必须显式指定 selector
  template:
    metadata:
      labels:
        app: mywebapp
    spec:
      containers:
      - name: mywebapp
        image: tomcat
        ports:
        - containerPort: 8080

我们可以通过 kubectl get pods -l app=mywebapp -o yaml | grep podIP 来获取 Pod 的 IP 地址和端口号来访问 Tomcat 服务,但是直接通过 Pod 的 IP 地址和端口访问应用服务是不可靠的,因为当 Pod 所在的 Node 发生故障时, Pod 将被 kubernetes 重新调度到另一台 Node,Pod 的地址会发生改变。我们可以通过配置文件来定义 Service,再通过 kubectl create 来创建,这样可以通过 Service 地址来访问后端的 Pod.

apiVersion: v1
kind: Service
metadata:
  name: mywebAppService
spec:
  ports:
    - port: 8081
      targetPort: 8080
  selector:
    app: mywebapp

六、多端口 Service

有时一个容器应用也可能需要提供多个端口的服务,那么在 Service 的定义中也可以相应地设置为将多个端口对应到多个应用服务。

apiVersion: v1
kind: Service
metadata:
  name: mywebAppService
spec:
  ports:
    - port: 8080
      targetPort: 8080
      name: web
    - port: 8005
      targetPort: 8005
      name: management
  selector:
    app: mywebapp

七、外部服务 Service

在某些特殊环境中,应用系统需要将一个外部数据库作为后端服务进行连接,或将另一个集群或 Namespace 中的服务作为服务的后端,这时可以通过创建一个无 Label Selector 的 Service 来实现。

apiVersion: v1
kind: Service
metadata:
  name: my-service
spec:
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
---
apiVersion: v1
kind: Endpoints
metadata:
  name: my-service          # 必须与 Service 名称相同
subsets:
  - addresses:
      - ip: 10.254.74.3
    ports:
      - port: 8080

ExternalName 的另一种选择

如果你的目标是一个 域名,用 ExternalName 更方便。
如果你的目标是一个 IP 地址(如你现在的 10.254.74.3),那么你这种 Service + Endpoints 的组合是唯一的正确方案。