跳到主要内容

【3】K8S核心对象之Service

参考 k8s-tutorial-cn

Service介绍

Service为Pod提供了网络访问、负载均衡以及服务发现等功能。
Service 位于 pod 的前面, 负责接收请求并将它们传递给它后面的所有pod。一旦服务中的 Pod 集合发生更改,Endpoints 就会被更新,请求的重定向自然也会导向最新的 pod。

不同类型的Service

  • ClusterIP
    • 原理:使用这种方式发布时,会为Service提供一个固定的集群内部虚拟IP,供集群内(包含节点)访问。
    • 场景:内部数据库服务、内部API服务等。
  • NodePort
    • 原理:通过每个节点上的 IP 和静态端口发布服务。 这是一种基于ClusterIP的发布方式,因为它应用后首先会生成一个集群内部IP, 然后再将其绑定到节点的IP和端口,这样就可以在集群外通过 nodeIp:port 的方式访问服务。
    • 场景:Web应用程序、REST API等。
  • LoadBalancer
    • 原理:这种方式又基于 NodePort,另外还会使用到外部由云厂商提供的负载均衡器。由后者向外发布 Service。 一般在使用云平台提供的Kubernetes集群时,会用到这种方式。
    • 场景:Web应用程序、公开的API服务等。
  • Headless
    • 原理:这种方式不会分配任何集群IP,也不会通过Kube-proxy进行反向代理和负载均衡,而是通过DNS提供稳定的网络ID来访问, 并且DNS会将无头Service的后端解析为Pod的后端IP列表,以供集群内访问(不含节点),属于向内发布。
    • 场景:一般提供给StatefulSet使用。
  • ExternalName
    • 原理:与上面提到的发布方式不太相同,这种方式是通过CNAME机制将外部服务引入集群内部,为集群内提供服务,属于向内发布。
    • 场景:连接到外部数据库服务、外部认证服务等。

ClusterIP

提示

ClusterIP在集群内部分配一个固定的、具有负载均衡的ip指向Pod服务,集群内部可以通过这个固定的ip地址访问pod服务。

ClusterIP通过分配集群内部IP来在集群内(包含节点)暴露服务,可以在集群内通过clusterIP:port 访问到pod服务,集群外则无法访问。
这种方式适用于那些不需要对外暴露的服务,如节点守护agent等。

# 部署deployment
kubectl apply -f deployment-go-http.yaml
# 修改镜像
kubectl set image deployment/hellok8s-go-http hellok8s=leigg/hellok8s:v3_hostname
# 等待pod更新
kubectl get pods --watch

更新成功后,编写Service配置文件service-clusterip.yaml

# service-clusterip.yaml
apiVersion: v1
kind: Service
metadata:
name: service-hellok8s-clusterip
spec:
type: ClusterIP # 这行是默认的,可省略
# sessionAffinity: ClientIP # or None, 设置会话亲和性(ClientIP表示同一客户端ip的请求会路由到同个Pod)
# sessionAffinityConfig:
# clientIP:
# timeoutSeconds: 3600 # 范围 0~86400,默认10800(3h)
selector:
app: hellok8s # 通过selector关联pod组
ports:
- port: 3000 # service端口
targetPort: 3000 # 后端pod端口
Session affinity(会话亲和性)

Session affinity的效果仅会在一定时间期限内生效,默认值为10800秒,超出此时长之后,客户端的再次访问会被调度算法重新调度。另外,Service资源的Session affinity机制仅能基于客户端IP地址识别客户端身份,它会把经由同一个NAT服务器进行源地址转换的所有客户端识别为同一个客户端,调度粒度粗糙且效果不佳,因此,实践中并不推荐使用此种方法实现会话粘性。

# 应用配置文件
kubectl apply -f service-clusterip.yaml
# 查看服务
> kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.43.0.1 <none> 443/TCP 5d23h
service-hellok8s-clusterip ClusterIP 10.43.25.35 <none> 3000/TCP 30m
# 查看Service后端的逻辑Pod组的信息
> kubectl get endpoints
NAME ENDPOINTS AGE
kubernetes 10.20.30.5:6443 5d23h
service-hellok8s-clusterip 10.42.0.33:3000,10.42.1.54:3000,10.42.1.55:3000 31m
# 创建出来的ClusterIP 10.43.25.35,在master,nodes节点上都能访问,集群外访问不到

多次访问http://10.43.25.35:3000会发现hostname变化,说明service进行了负载均衡

> curl http://10.43.25.35:3000
[v3] Hello, Kubernetes!, From host: hellok8s-go-http-7c57cd6487-rkpxp
> curl http://10.43.25.35:3000
[v3] Hello, Kubernetes!, From host: hellok8s-go-http-7c57cd6487-sw6sk

NodePort

提示

NodePort在ClusterIP上进行扩展,在集群中的每个节点上都开放了供集群外部访问的端口nodePort,集群外部可以通过nodePort访问集群内部的pod服务。

ClusterIP只能在集群内访问Pod服务,而NodePort则进一步将服务暴露到集群节点的静态端口上。
比如k8s集群有2个节点:node1和node2,暴露后就可以通过 node1-ip:portnode2-ip:port 的方式来稳定访问Pod服务。

# service-nodeport.yaml
apiVersion: v1
kind: Service
metadata:
name: service-hellok8s-nodeport
spec:
type: NodePort
selector:
app: hellok8s
ports:
- port: 3000 # Service端口
protocol: TCP # 协议
targetPort: 3000 # 容器端口
nodePort: 30000 # 节点固定端口。在NodePort类型中,k8s要求在 30000-32767 范围内,否则apply报错
# 若需要暴露多个端口,则按下面形式
# - name: http
# protocol: TCP
# port: 80
# targetPort: 9376
# - name: https
# protocol: TCP
# port: 443
# targetPort: 9377
# 应用service
kubectl apply -f service-nodeport.yaml
# 查看service
> kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.43.0.1 <none> 443/TCP 37m
service-hellok8s-nodeport NodePort 10.43.128.116 <none> 3000:30000/TCP 4s
  • 在master、node节点可以通过curl http://10.43.128.116:3000访问服务。
  • 在集群内外使用curl或在局域网使用浏览器都可以通过http://10.20.30.8:30000/http://10.20.30.5:30000/访问服务

LoadBalancer

提示

LoadBalancer在NodePort上进行扩展,支持使用云提供商的负载均衡器向外暴露服务,在不支持LoadBalance Service的环境中运行类似NodePort Service。

LoadBalancer 是通过使用云提供商的负载均衡器(一般叫做SLB,Service LoadBalancer)的方式向外暴露服务。
负载均衡器可以将集群外的流量转发到集群内的节点,后者再转发到Pod, 假如你在 AWS 的 EKS 集群上创建一个 Type 为 LoadBalancer 的 Service。它会自动创建一个 ELB (Elastic Load Balancer) ,并可以根据配置的 IP 池中自动分配一个独立的 IP 地址,可以供外部访问。
在大多数K8s云提供商的环境中,K8s集群可以通过负载均衡器使得LoadBalance Service拥有一个公网IP。以便真正地对外暴露服务。但如果K8s在不支持LoadBalance Service的环境中运行,则不会调配负载均衡器。此时该服务将会表现的像是一个NodePort Service,因为LoadBalance Service是对NodePort Service的拓展。

使用 LoadBalancer 创建服务的步骤:

  1. 创建一个包含Pod的Deployment。
  2. 创建一个 Service,其中指定了上述 Deployment 中定义的 Pod 的 selector。
  3. 在 Service yaml 文件中指定 type: LoadBalancer
  4. 应用 Service 文件并等待 Kubernetes 和云服务提供商之间的交互完成。
  5. 查看 External IP,它是分配给 Kubernetes 中新创建的 LoadBalancer 的 IP 地址,该地址可以用于访问您的应用程序。

下面是loadbalance service的示例,创建私网SLB,基于K3S的Klipper Load Balancer

# hellok8s-loadbalance-service.yaml
apiVersion: v1
# 资源类型
kind: Service
metadata:
# 资源名称
name: hellok8s-loadbalance-service
spec:
# Service类型
type: LoadBalancer
selector:
app: hellok8s
ports:
- port: 997 # 服务监听端口
targetPort: 3000 # 服务将请求转发到Pod的目标端口
# 在集群各节点所打开的端口, 使得可以通过集群中任一节点IP、nodePort端口号访问该服务
nodePort: 32329
> kubectl get svc -o wide
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
hellok8s-loadbalance-service LoadBalancer 10.43.156.107 10.20.30.5,10.20.30.8 997:32329/TCP 104s app=hellok8s

可以看到Service服务的EXTERNAL-IP列显示已经被分配了两个节点的IP,接下来在master节点上做测试

curl http://10.43.156.107:32329 X 无法访问
curl http://10.43.156.107:997 OK 访问正常
curl http://10.20.30.5:997 OK 访问正常
curl http://10.20.30.8:997 OK 访问正常
curl http://10.20.30.5:32329 OK 访问正常
curl http://10.20.30.8:32329 OK 访问正常

Headless

Headless简单理解

headless服务其实是分配一个固定的DNS域名来访问Pod服务,为pod提供了一个固定的地址,不需要生成ClusterIP和为其做负载均衡。此dns只有在集群内部的pod访问才能访问,集群内节点机器、集群外都不能访问。

创建service文件

# service-clusterip-headless.yaml
apiVersion: v1
kind: Service
metadata:
name: service-hellok8s-clusterip-headless
spec:
type: ClusterIP # 这行是默认的,可省略
# sessionAffinity: ClientIP # or None, 设置会话亲和性(ClientIP表示同一客户端ip的请求会路由到同个Pod)
# sessionAffinityConfig:
# clientIP:
# timeoutSeconds: 3600 # 范围 0~86400,默认10800(3h)
clusterIP: None # None 表示不分配集群IP
selector:
app: hellok8s # 通过selector 选择映射的pod
ports:
- port: 3000 # service端口
targetPort: 3000 # 后端pod端口

创建pod文件(包含curl和nslookup命令),用于测试访问定义好的service

# pod_curl.yaml
apiVersion: v1
kind: Pod
metadata:
name: curl
labels:
app: curl
annotations:
key1: "value1"
description: The `curl` command is a powerful tool used to make HTTP requests from the command line.
It is versatile and supports various protocols, including HTTP, HTTPS, FTP, FTPS, and more.
spec:
containers:
- name: curl-container
image: appropriate/curl
command: [ "sh","-c", "sleep 1h" ]
# 应用配置文件
kubectl apply -f service-clusterip-headless.yaml
# 应用pod文件
kubectl apply -f pod_curl.yaml
# 查询service
> kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.43.0.1 <none> 443/TCP 22h
service-hellok8s-clusterip-headless ClusterIP None <none> 3000/TCP 3s
# 进入curl pod容器,测试通过headless service dns访问对应的pod
> kubectl exec -it curl -- /bin/sh
/ # curl service-hellok8s-clusterip-headless.default.svc.cluster.local:3000
[v3] Hello, Kubernetes!, From host: hellok8s-go-http-7c57cd6487-dx6t4
/ # curl service-hellok8s-clusterip-headless.default.svc.cluster.local:3000
[v3] Hello, Kubernetes!, From host: hellok8s-go-http-7c57cd6487-6rd7q
/ # nslookup service-hellok8s-clusterip-headless.default.svc.cluster.local
nslookup: can't resolve '(null)': Name does not resolve

Name: service-hellok8s-clusterip-headless.default.svc.cluster.local
Address 1: 10.42.1.9 10-42-1-9.service-hellok8s-clusterip-headless.default.svc.cluster.local
Address 2: 10.42.1.10 10-42-1-10.service-hellok8s-clusterip-headless.default.svc.cluster.local
Address 3: 10.42.0.12 10-42-0-12.service-hellok8s-clusterip-headless.default.svc.cluster.local

这里的service-hellok8s-clusterip-headless.default.svc.cluster.local就是Service提供给集群内部访问Pod组的域名, 组成方式为{ServiceName}.{Namespace}.svc.{ClusterDomain},其中ClusterDomain表示集群域,默认为cluster.local, Namespace在Service的yaml文件中未指定那就是default。

  • 通过curl访问dns地址可以看到请求下发到多个pod上了,但这并不是k8s提供的负载均衡,而只是简单的dns轮训,并不能根据服务器当前的状态(用户数、资源消耗情况)实现智能分配。
  • 通过nslookup查看域名的DNS信息,可以看到Service域名指向三个Pod IP,并且它们还有对应的专有域名,但因为Pod IP非固定, 所以这个专有域名也没任何作用。
  • 在master或node节点上无法访问此dns地址,只能在集群pod内部访问。
  • 内部服务除了直接调用域名访问服务之外,还可解析域名来根据需求决定访问哪个Pod。这种方式更适合StatefulSet产生的有状态Pod(例如数据库集群)。

ExternalName

ExternalName简单理解

定义一个ExternalName指向某个域名,例如www.baidu.com,然后可以在集群内的任何一个pod上访问这个service的域名,请求service域名将自动重定向到www.baidu.com。ExternalName一般用在集群内部需要调用外部服务的时候,比如云服务商部署的DB等服务

ExternalName也是k8s中一个特殊的Service类型,它不需要设置selector去选择为哪些pods实例提供服务,而是使用DNS CNAME机制把svc指向另外一个域名,这个域名可以是任何能够访问的虚拟地址(不可以是IP), 比如mysql.db.svc这样的建立在db命名空间内的mysql服务,也可以指定www.baidu.com这样的外部真实域名。

# service-externalname.yaml
apiVersion: v1
kind: Service
metadata:
name: service-hellok8s-externalname # 这个名称用来在集群内作为host访问
namespace: default # 可省略
spec:
type: ExternalName
externalName: www.baidu.com # 只能是一个有效的dns地址,不能包含 /,也不能是IP(可定义但无法正常解析)
# 应用ExternalName
kubectl apply -f service-externalname.yaml
# 依旧使用curl pod做测试
> kubectl exec -it curl -- /bin/sh
/ # ping service-hellok8s-externalname.default.svc.cluster.local
PING service-hellok8s-externalname.default.svc.cluster.local (180.101.50.188): 56 data bytes
64 bytes from 180.101.50.188: seq=0 ttl=48 time=29.004 ms
......
# 这里响应ip 180.101.50.188 是百度的
/ # nslookup service-hellok8s-externalname.default.svc.cluster.local
nslookup: can't resolve '(null)': Name does not resolve

Name: service-hellok8s-externalname.default.svc.cluster.local
Address 1: 180.101.50.242
Address 2: 180.101.50.188
Address 3: 240e:e9:6002:15c::ff:b015:146f
Address 4: 240e:e9:6002:15a::ff:b05c:1278

注:无法通过curl测试达到访问百度的效果,curl不具备DNS解析功能,拿不到百度服务器IP

无头Service + Endpoints

ExternalName只能指向域名,实际场景中更多是使用固定ip访问其他服务,这里就需要使用Headless Service+Endpoints来实现

# service-headless-endpoints.yaml
apiVersion: v1
kind: Service
metadata:
name: service-headless-and-endpoint # 与下面Endpoints的 meta.name 必须一致
spec:
clusterIP: None # headless service
# selector: {} # 不填写selector
ports:
- protocol: TCP
port: 80
targetPort: 80 # 与下面的port必须一致,否则无法正常转发
---
apiVersion: v1
kind: Endpoints
metadata:
name: service-headless-and-endpoint
subsets:
- addresses:
- ip: 110.242.68.66 # baidu.com ip
ports:
- port: 80
> kubectl apply -f service-headless-endpoints.yaml 
service/service-headless-and-endpoint created
endpoints/service-headless-and-endpoint created
# 进入curl容器
> kubectl exec -it curl -- /bin/sh
/ # ping service-headless-and-endpoint.default.svc.cluster.local
PING service-headless-and-endpoint.default.svc.cluster.local (110.242.68.66): 56 data bytes
64 bytes from 110.242.68.66: seq=0 ttl=49 time=41.666 ms
......
# 可以看到service-headless-and-endpoint域名已指向了ip 110.242.68.66

Endpoints对象一般不需要手动创建,Service controller会在service创建时自动创建,只有在需要关联集群外的服务时可能用到

搭配External IP

ExternalIP是Service模板中的一个配置字段,位置是spec.externalIP。前面小节介绍的 ClusterIP(含Headless)/NodePort/LoadBalancer/ExternalName 五种Service都可以搭配 ExternalIP 使用。

配置此字段后,在原模板提供的功能基础上,还可以将Service注册到指定的External IP(通常是节点网段内的空闲IP)上,从而增加Service的一种暴露方式。

# service-clusterip-external-ip.yaml
apiVersion: v1
kind: Service
metadata:
name: service-hellok8s-clusterip-external-ip
spec:
type: ClusterIP
selector:
app: hellok8s
ports:
- port: 3000
targetPort: 3000
externalIPs:
- 10.50.50.33 # 任意局域网IP都可

spec.externalIP可以配置为任意局域网IP,而不必是节点网段内的ip,Service Controller会自动为每个节点添加路由。

# 应用service
kubectl apply -f service-clusterip-external-ip.yaml
# 查看service
> kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.43.0.1 <none> 443/TCP 25h
service-hellok8s-clusterip-external-ip ClusterIP 10.43.30.41 10.50.50.33 3000/TCP 3s
# 通过EXTERNAL-IP访问服务
> curl http://10.50.50.33:3000
[v3] Hello, Kubernetes!, From host: hellok8s-go-http-7c57cd6487-tp522