kubernetes Service
Service介绍
运行于pod中的容器化应用绝大多数是服务类的守护进程,它们受控于控制器资源对象,在自愿或非自愿中断后只能由重构的、具有相同功能的新pod对象所取代,属于非可再生类组件。在kubernetes应用编排的动态、弹性管理模式下,Service资源用于为此类pod提供一个固定、统一的访问接口及负载均衡能力,并支持新一代DNS系统的服务发现功能,解决了客户端发现并访问容器化应用的难题。
由于Service对象的IP地址都仅在kubernetes集群内可达,它们无法接入集群外部的访问流量。在解决此类问题时,除了可以在单一节点上做端口暴露以及在pod资源共享使用工作节点的网络名称空间之外,更推荐使用NodePort或LoadBalance类型的Service资源,或者是由七层负载均衡能力的Ingress资源。
Service资源
Service是kubernetes的核心资源类型之一,通常被看作微服务的一种实现。它事实上是一种抽象,通过规则定义出由多个pod对象组合而成的逻辑集合,以及访问这组pod的策略。Service关联pod资源的规则借助标签选择器完成。
作为一款容器编排系统,托管在kubernetes之上,以pod形式运行的应用进程的生命周期通常受控与Deployment或StatefulSet一类的控制器,由于节点故障或驱离等原因导致pod对象中断后,会由控制器自动创建的新对象所取代,而扩缩容或更新操作更是会带来pod对象的集群变动。因为编排系统需要确保服务在编排操作导致的应用pod动态变动的过程中始终可访问,所以kubernetes提出了满足这一关键需求的解决方案,即核心资源类型-Service。
Service资源基于标签选择器把筛选出的一组pod对象定义成一个逻辑组合,并通过自己的IP地址和端口将请求分发给该组内的pod对象,Service对象的IP地址(ClusterIP或ServiceIP)是虚拟IP地址,由kubernetes系统在Service对象创建时在专用网络(Service Network)地址中自动分配或由用户手动指定,并且在Service对象的生命周期中保持不变。Service基于端口过滤器到达其IP地址的客户端请求,并根据定义将请求转发至其后端的pod对象的相应端口之上,因此这种代理机制也称为端口代理或四层代理,工作与TCP/IP协议栈的传输层。
Service对象会通过API Server持续监视(watch)标签选择器匹配到的后端pod对象,并实时跟踪这些pod对象的变动情况,例如IP地址变动以及pod对象的增加或删除等,不过,Service并不直接连接至pod对象,它们之间还有一个中间层-Endpoints资源对象,该资源对象时一个由IP地址和端口组成的列表,这些IP地址和端口则来自由Service的标签选择器匹配到的pod对象。这也是很多场景中会使用“Service的后端端点”这一术语的原因。默认情况下,创建Service资源对象时,其关联的Endpoints对象会被自动创建。
Service实现模型
kube-proxy代理模型
本质来讲,一个Service对象对应于工作节点内核之中的一组iptables或ipvs规则,这些规则能够将到达Service的ClusterIP的流量调度转发至相应Endpoint对象指向的IP地址和端口之上。内核中的iptables或ipvs规则的作用域仅为其所在工作节点的一个主机,因而生效于集群范围内的Service对象就需要在每个工作节点上都生成相关规则,从而确保任一节点上发往该Service对象请求的流量都能被正确转发。
每个工作节点的kube-proxy组件通过API Server持续监控这个Service及其相关联的pod对象,并将Service对象的创建或变动实时反应至当前工作节点上相应的iptables或ipvs规则上。
Netfilter是linux内核中用于管理网络报文的框架,它具有网络地址转换(NAT)、报文改动和报文过滤等防火墙功能,用户可借助用户空间的iptables等工具按需自由定制规则使用其各项功能。ipvs是借助于Netfilter实现的网络请求报文调度框架,支持rr、wrr、lc、wlc、sh、sed和nq等10余种调度算法,用户空间的命令行工具是ipvsadm,用于管理工作与ipvs之上的调度规则。
Service对象的ClusterIP事实上是用于生成iptables或ipvs规则时使用的IP地址,它仅用于实现kubernetes集群网络内部通信,且仅能够以规则中定义的转发服务的请求作为目标地址予以相应,这也是它之所被称为虚拟IP的原因之一。kube-proxy把请求代理至相应端点的方式有3种:userspace、iptables和ipvs。
userspace代理模型
此外的userspace是指linux操作系统的用户空间。在这种模式中,kube-proxy负载跟踪API Server上Service和Endpoints对象的变动,并据此调整Service资源的定义。对于每个Service对象,它会随机打开一个端口(运行于用户空间的kube-proxy进程负责监听),任何到达此代理端口的连续请求都将被代理至当前Service资源后端的各pod对象,至于那个pod对象会被选中则取决于当前Service资源的调度方式,默认调度算法是轮询(round-robin)。另外,此类Service对象还会创建iptables规则以捕获任何到达ClusterIP和端口的流量。在kubernetes1.1版本之前,userspace是默认的代理模式。
在这种代理模型中,请求流量到达内核空间后经由套接字送往用户空间中的kube-proxy进程,而后由该进程送回内核空间,发往调度分配的目标后端pod对象。因请求报文在内核空间和用户空间来回转发,所以必然导致模型效率不高。
iptables代理模型
创建Service对象的操作会触发集群中的每个kube-proxy并将其转换为定义在所属节点上的iptables规则,用于转发工作接口接收到的、与此Service资源ClusterIP和端口相关的流量。客户端发来请求将直接由相关的iptables规则进行目标地址转换(DNAT)后根据算法调度并转发至集群内的pod对象之上,而无需在经由kube-proxy进行进行处理,因而称为iptables代理模型。对于每个Endpoints对象,Service资源会为其创建iptables规则并指向其iptables地址和端口,而流量转发到多个Endpoint对象之上的默认调度机制是随机算法。iptables代理模型由kubernetesv1.1版本引入,并与v1.2版本称为默认的类型。
在iptables代理模型中,Service的服务发现和负载均衡功能都使用iptables规则实现,而无须将流量在用户空间和内核空间来回切换,因此更为高效和可靠,但是性能一般,而且受规模影响较大,仅适用于少量Service规模的集群。
ipvs代理模型
kubernetes自v1.9版本引入ipvs代理模型,且自v1.11版本起称为默认设置。在此种模型中,kube-proxy跟踪API Server上Service和Endpoints对象的变动,并据此来调用netlink接口创建或变更ipvs规则,它与iptables规则不同之处仅在于客户端请求流量的调度功能由ipvs实现,余下的其它功能仍有iptables完成。
ipvs代理模型中Service的服务发现和负载均衡功能均基于内核中的ipvs规则实现。类似于iptables,ipvs也构建于内核中 netfilter之上,但它使用hash表作为底层数据结构且工作于内核空间,因此具有流量转发速度快、规则同步性能好的特性,适用于存在大量Service资源且对性能要求较高的场景。ipvs代理模型支持rr、lc、dh、sh、sed和nq等多种调度算法。
Service资源类型
无论哪一种代理模型,Service资源都可统一根据其工作逻辑分为ClusterIP、NodePort、LoadBalancer和ExternalName这4中类型。
ClusterIP
通过集群内部IP地址暴露服务,ClusterIP地址仅在进去内部可达,因而无法被集群外部的客户端访问。此为默认的Service类型。你可以使用Ingress或者Gateway API向公众暴露服务。
此默认服务类型从你的集群中有意预留的 IP 地址池中分配一个 IP 地址。
其他几种服务类型在 ClusterIP 类型的基础上进行构建。
如果你定义的服务将 .spec.clusterIP 设置为 "None",则 Kubernetes 不会分配 IP 地址。
NodePort
NodePort类型是对ClusterIP类型Service资源的扩展,它支持通过特定的节点端口接入集群外部的请求流量,并分发给后端的Server Pod处理和响应。因此,这种类型的Service即可以被集群内部客户端通过ClusterIP直接访问,也可以通过套接字<NodeIP>:<NodePort>与集群外部客户端进行通信。显然,若集群外部的请求报文首先到的节点非Service调度的目标Server Pod所在的节点,该请求必然因需要额外的转发过程和更多的处理步骤而产生更多延迟。
另外,集群外部客户端对NodePort发起请求报文源地址并非集群内部地址,而请求报文又可能被接收到报文的节点转发至集群中的另一个节点上的pod对象,因此,为避免另一个节点直接将响应报文发送给外部客户端,该节点需要先将收到的报文的源地址转为请求报文的目标IP(自身的节点IP)后再进行后续处理过程。
如果你将 type 字段设置为 NodePort,则 Kubernetes 控制平面将在 --service-node-port-range 标志指定的范围内分配端口(默认值:30000-32767)。 每个节点将那个端口(每个节点上的相同端口号)代理到你的服务中。 你的服务在其 .spec.ports[*].nodePort 字段中报告已分配的端口。
使用 NodePort 可以让你自由设置自己的负载均衡解决方案, 配置 Kubernetes 不完全支持的环境, 甚至直接暴露一个或多个节点的 IP 地址。
对于 NodePort 服务,Kubernetes 额外分配一个端口(TCP、UDP 或 SCTP 以匹配服务的协议)。 集群中的每个节点都将自己配置为监听分配的端口并将流量转发到与该服务关联的某个就绪端点。 通过使用适当的协议(例如 TCP)和适当的端口(分配给该服务)连接到所有节点, 你将能够从集群外部使用 type: NodePort 服务。
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
type: NodePort
selector:
app.kubernetes.io/name: MyApp
ports:
# 默认情况下,为了方便起见,`targetPort` 被设置为与 `port` 字段相同的值。
- port: 80
targetPort: 80
# 可选字段
# 默认情况下,为了方便起见,Kubernetes 控制平面会从某个范围内分配一个端口号(默认:30000-32767)
nodePort: 30007
LoadBalancer
这种类型的Service依赖于部署在Iaas云计算服务器上并且能够调用其API接口创建软件负载均衡器的kubernetes集群环境。LoadBalancer Service构建在NodePort类型的基础上,通过云服务商提供的软负载均衡器将服务暴露到集群外部,因此它也会具有NodePort和ClusterIP。创建LoadBalancer类型的Service对象时会在集群上创建一个NodePort类型的Service,并额外触发kubernetes调用底层的Iaas服务的API创建一个软件负载均衡器,而集群外部的请求流量会先路由至该负载均衡器,并由该负载均衡器调度至各节点上该Service对象的NodePort。该Service类型的优势在于,它能够把来自集群外部客户端的请求调度至所有节点的NodePort之上,而不是让客户端自行决定连接那个节点,也避免了因客户端指定的节点故障而导致的服务不可用。
在使用支持外部负载均衡器的云提供商的服务时,设置 type 的值为 "LoadBalancer", 将为 Service 提供负载均衡器。 负载均衡器是异步创建的,关于被提供的负载均衡器的信息将会通过 Service 的 status.loadBalancer 字段发布出去。
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
selector:
app.kubernetes.io/name: MyApp
ports:
- protocol: TCP
port: 80
targetPort: 9376
clusterIP: 10.0.171.239
type: LoadBalancer
status:
loadBalancer:
ingress:
- ip: 192.0.2.127
ExternalName
通过将Service映射至由externalName字段的内容指定的主机名来暴露服务,此主机名需要被NDS服务解析值CNAME类型的记录中。换言之,此种类型不是定义由kubernetes集群提供的服务,而是把集群外部的某服务以DNS CNAME记录的方式映射到集群内,从而让集群内的pod资源能够访问外部服务的一种实现方式。因此,这种类型的Service没有ClusterIP和NodePort,没有标签选择器用于选择pod资源,也不会有Endpoints存在。
总体来说,若需要将Service资源发布值集群外部,应该将其配置为NodePort或LoadBalancer类型,而若要把外部的服务发布于集群内部供pod对象使用,则需要定义一个ExternalName类型的Service资源,只是这种类型的实现要依赖于v1.7及更高版本的kubernetes。
示例
例如,以下 Service 定义将 prod 名称空间中的 my-service 服务映射到 my.database.example.com:
apiVersion: v1
kind: Service
metadata:
name: my-service
namespace: prod
spec:
type: ExternalName
externalName: my.database.example.com
type: ExternalName 的服务接受 IPv4 地址字符串,但将该字符串视为由数字组成的 DNS 名称, 而不是 IP 地址(然而,互联网不允许在 DNS 中使用此类名称)。 类似于 IPv4 地址的外部名称不能由 CoreDNS 或 ingress-nginx 解析,因为外部名称旨在指定规范的 DNS 名称。 DNS 服务器不解析类似于 IPv4 地址的外部名称的服务。
当查找主机 my-service.prod.svc.cluster.local 时,集群 DNS 服务返回 CNAME 记录, 其值为 my.database.example.com。 访问 my-service 的方式与其他服务的方式相同,但主要区别在于重定向发生在 DNS 级别,而不是通过代理或转发。 如果以后你决定将数据库移到集群中,则可以启动其 Pod,添加适当的选择算符或端点以及更改服务的 type。
对于一些常见的协议,包括 HTTP 和 HTTPS,你使用 ExternalName 可能会遇到问题。 如果你使用 ExternalName,那么集群内客户端使用的主机名与 ExternalName 引用的名称不同。
对于使用主机名的协议,此差异可能会导致错误或意外响应。 HTTP 请求将具有源服务器无法识别的 Host: 标头; TLS 服务器将无法提供与客户端连接的主机名匹配的证书。
定义 Service
端口定义
Pod 中的端口定义是有名字的,你可以在 Service 的 targetPort 属性中引用这些名称。 例如,我们可以通过以下方式将 Service 的 targetPort 绑定到 Pod 端口:
apiVersion: v1
kind: Pod
metadata:
name: nginx
labels:
app.kubernetes.io/name: proxy
spec:
containers:
- name: nginx
image: nginx:stable
ports:
- containerPort: 80
name: http-web-svc
---
apiVersion: v1
kind: Service
metadata:
name: nginx-service
spec:
selector:
app.kubernetes.io/name: proxy
ports:
- name: name-of-service-port
protocol: TCP
port: 80
targetPort: http-web-svc
即使 Service 中使用同一配置名称混合使用多个 Pod,各 Pod 通过不同的端口号支持相同的网络协议, 此功能也可以使用。这为 Service 的部署和演化提供了很大的灵活性。 例如,你可以在新版本中更改 Pod 中后端软件公开的端口号,而不会破坏客户端。
服务的默认协议是 TCP; 你还可以使用任何其他受支持的协议。
由于许多服务需要公开多个端口,所以 Kubernetes 针对单个服务支持多个端口定义。 每个端口定义可以具有相同的 protocol,也可以具有不同的协议。
没有selector的 Service
由于选择算符的存在,服务最常见的用法是为 Kubernetes Pod 的访问提供抽象, 但是当与相应的 EndpointSlices 对象一起使用且没有选择算符时, 服务也可以为其他类型的后端提供抽象,包括在集群外运行的后端。
例如:
希望在生产环境中使用外部的数据库集群,但测试环境使用自己的数据库。
希望服务指向另一个 名字空间(Namespace) 中或其它集群中的服务。
你正在将工作负载迁移到 Kubernetes。在评估该方法时,你仅在 Kubernetes 中运行一部分后端。
在任何这些场景中,都能够定义未指定与 Pod 匹配的选择算符的 Service。例如: 实例:
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
ports:
- protocol: TCP
port: 80
targetPort: 9376
由于此服务没有选择算符,因此不会自动创建相应的 EndpointSlice(和旧版 Endpoint)对象。 你可以通过手动添加 EndpointSlice 对象,将服务映射到运行该服务的网络地址和端口:
apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
name: my-service-1 # 按惯例将服务的名称用作 EndpointSlice 名称的前缀
labels:
# 你应设置 "kubernetes.io/service-name" 标签。
# 设置其值以匹配服务的名称
kubernetes.io/service-name: my-service
addressType: IPv4
ports:
- name: '' # 留空,因为 port 9376 未被 IANA 分配为已注册端口
appProtocol: http
protocol: TCP
port: 9376
endpoints:
- addresses:
- "10.4.5.6" # 此列表中的 IP 地址可以按任何顺序显示
- "10.1.2.3"
自定义 EndpointSlices
当为服务创建 EndpointSlice 对象时,可以为 EndpointSlice 使用任何名称。 命名空间中的每个 EndpointSlice 必须有一个唯一的名称。通过在 EndpointSlice 上设置 kubernetes.io/service-name label 可以将 EndpointSlice 链接到服务。
端点 IP 地址必须不是 :本地回路地址(IPv4 的 127.0.0.0/8、IPv6 的 ::1/128) 或链路本地地址(IPv4 的 169.254.0.0/16 和 224.0.0.0/24、IPv6 的 fe80::/64)。
端点 IP 地址不能是其他 Kubernetes 服务的集群 IP,因为 kube-proxy 不支持将虚拟 IP 作为目标。
对于你自己或在你自己代码中创建的 EndpointSlice,你还应该为 endpointslice.kubernetes.io/managed-by 标签拣选一个值。如果你创建自己的控制器代码来管理 EndpointSlice, 请考虑使用类似于 "my-domain.example/name-of-controller" 的值。 如果你使用的是第三方工具,请使用全小写的工具名称,并将空格和其他标点符号更改为短划线 (-)。 如果人们直接使用 kubectl 之类的工具来管理 EndpointSlices,请使用描述这种手动管理的名称, 例如 "staff" 或 "cluster-admins"。你应该避免使用保留值 "controller", 该值标识由 Kubernetes 自己的控制平面管理的 EndpointSlices。
访问没有selector的 Service
访问没有选择算符的 Service,与有选择算符的 Service 的原理相同。 在没有选择算符的 Service 示例中, 流量被路由到 EndpointSlice 清单中定义的两个端点之一: 通过 TCP 协议连接到 10.1.2.3 或 10.4.5.6 的端口 9376。
Kubernetes API 服务器不允许代理到未被映射至 Pod 上的端点。由于此约束,当 Service 没有选择算符时,诸如 kubectl proxy <service-name> 之类的操作将会失败。这可以防止 Kubernetes API 服务器被用作调用者可能无权访问的端点的代理。
ExternalName Service 是 Service 的特例,它没有选择算符,而是使用 DNS 名称。
多端口 Service
对于某些服务,你需要公开多个端口。 Kubernetes 允许你在 Service 对象上配置多个端口定义。 为服务使用多个端口时,必须提供所有端口名称,以使它们无歧义。 例如:
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
selector:
app.kubernetes.io/name: MyApp
ports:
- name: http
protocol: TCP
port: 80
targetPort: 9376
- name: https
protocol: TCP
port: 443
targetPort: 9377
与一般的 Kubernetes 名称一样,端口名称只能包含小写字母数字字符 和 -。 端口名称还必须以字母数字字符开头和结尾。
例如,名称 123-abc 和 web 有效,但是 123_abc 和 -web 无效。
应用Service资源
Service是kubernetes核心API群组中的标准资源类型之一,其管理操作的基本逻辑类似于Namespace和ConfigMap等资源,支持虞姬命令行和配置清单的管理方式。
kind: Service # 类型为service
apiVersion: v1 # service API版本, service.apiVersion
metadata: # 定义service元数据,service.metadata
labels: # 自定义标签,service.metadata.labels
app: wgs-nginx # 定义service标签的内容
name: wgs-nginx-service # 定义service的名称,此名称会被DNS解析
namespace: wgs # 该service隶属于的namespaces名称,即把service创建到哪个namespace里面
spec: # 定义service的详细信息,service.spec
type: NodePort # service的类型,定义服务的访问方式,默认为ClusterIP, service.spec.type
ports: # 定义访问端口, service.spec.ports
- name: http # 定义一个端口名称
port: 81 # service 80端口
protocol: TCP # 协议类型
targetPort: 80 # 目标pod的端口
nodePort: 30001 # node节点暴露的端口
- name: https # SSL 端口
port: 1443 # service 443端口
protocol: TCP # 端口协议
targetPort: 443 # 目标pod端口
nodePort: 30043 # node节点暴露的SSL端口
selector: # service的标签选择器,定义要访问的目标pod
app: wgs-nginx-selector # 将流量路由到选择的pod上,须等于Deployment.spec.selector.matchLabels
clusterIP: <string> # service的集群IP,建议由系统自动分配
externalTrafficPolicy: <string> # 外部流量策略处理方式,local表示当前节点处理,Cluster表示向集群范围内调度
loadBalancerIP: <string> # 外部负载均衡器使用的IP地址,仅适用于LoadBalancer
externalName: <string> # 外部服务名称,该名称将作为Service的DNS CNAME值
应用ClusterIP Service资源
创建Service对象的常用方法有两种:一种是利用kubectl create service命令创建,另一种则是利用资源清单创建。Service资源对象的期望状态定义在spec字段中,较为常见的内嵌字段为selector和ports,用于定义标签选择器和服务端口。
kind: Service
apiVersion: v1
metadata:
name: demoapp-svc
namespace: default
spec:
selector:
app: demoapp
ports:
- name: http
protocol: TCP
port: 80
targetPort: 80
查看service信息
~# kubectl describe svc demoapp-svc
Name: demoapp-svc
Namespace: default
Labels: <none>
Annotations: <none>
Selector: app=demoapp
Type: ClusterIP
IP Family Policy: SingleStack
IP Families: IPv4
IP: 10.100.219.69
IPs: 10.100.219.69
Port: http 80/TCP
TargetPort: 80/TCP
Endpoints: <none>
Session Affinity: None
Events: <none>
~# kubectl create deploy demoapp --image=nginx:alpine
deployment.apps/demoapp created
~# kubectl get pod -l app=demoapp
NAME READY STATUS RESTARTS AGE
demoapp-7859c6f7b7-k8x2n 1/1 Running 0 2s
~# kubectl describe svc demoapp-svc
Name: demoapp-svc
Namespace: default
Labels: <none>
Annotations: <none>
Selector: app=demoapp
Type: ClusterIP
IP Family Policy: SingleStack
IP Families: IPv4
IP: 10.100.219.69
IPs: 10.100.219.69
Port: http 80/TCP
TargetPort: 80/TCP
Endpoints: 10.200.44.243:80
Session Affinity: None
Events: <none>
~# kubectl get endpoints demoapp-svc
NAME ENDPOINTS AGE
demoapp-svc 10.200.44.243:80 8m39s
~# kubectl scale deployment demoapp --replicas=3
deployment.apps/demoapp scaled
~# kubectl get endpoints demoapp-svc
NAME ENDPOINTS AGE
demoapp-svc 10.200.44.243:80,10.200.44.244:80,10.200.89.163:80 10m
~# kubectl get pod -l app=demoapp
NAME READY STATUS RESTARTS AGE
demoapp-7859c6f7b7-65khs 1/1 Running 0 50s
demoapp-7859c6f7b7-d28xj 1/1 Running 0 2m22s
demoapp-7859c6f7b7-jjfhn 1/1 Running 0 50s
应用NodePort Service资源
部署kubernetes集群系统时会预留一个端口范围,专用于分配个需要到NodePort的Service对象,该端口范围默认为30000-32767。与Cluster类型的Service资源的一个显著不同之处在于,NodePort类型的Service资源需要显式定义.spec.type字段值为NodePort,必要时还可以手动指定具体的节点端口号。
创建NodePort Service
kind: Service
apiVersion: v1
metadata:
name: demoapp-nodeport-svc
spec:
type: NodePort
selector:
app: demoapp
ports:
- name: http
protocol: TCP
port: 80
targetPort: 80
nodePort: 31398
实践中,并不鼓励自定义节点端口,除非能事先确定它不会与某个现存的Service资源产生冲突。无论如何,只要没有特别需要,留给系统自动配置总是较好的选择。
查看NodePort Service信息
~# kubectl describe svc demoapp-nodeport-svc
Name: demoapp-nodeport-svc
Namespace: default
Labels: <none>
Annotations: <none>
Selector: app=demoapp
Type: NodePort
IP Family Policy: SingleStack
IP Families: IPv4
IP: 10.100.167.46
IPs: 10.100.167.46
Port: http 80/TCP
TargetPort: 80/TCP
NodePort: http 31398/TCP
Endpoints: <none>
Session Affinity: None
External Traffic Policy: Cluster
Events: <none>
~# kubectl create deploy demoapp --image=nginx:alpine
deployment.apps/demoapp created
~# kubectl scale deployment demoapp --replicas=3
deployment.apps/demoapp scaled
~# kubectl describe svc demoapp-nodeport-svc
Name: demoapp-nodeport-svc
Namespace: default
Labels: <none>
Annotations: <none>
Selector: app=demoapp
Type: NodePort
IP Family Policy: SingleStack
IP Families: IPv4
IP: 10.100.167.46
IPs: 10.100.167.46
Port: http 80/TCP
TargetPort: 80/TCP
NodePort: http 31398/TCP
Endpoints: 10.200.44.245:80,10.200.44.246:80,10.200.89.164:80
Session Affinity: None
External Traffic Policy: Cluster
Events: <none>
~# kubectl get pod -l app=demoapp -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
demoapp-7859c6f7b7-7zvgq 1/1 Running 0 5m10s 10.200.44.245 192.168.174.107 <none> <none>
demoapp-7859c6f7b7-cq64k 1/1 Running 0 5m5s 10.200.44.246 192.168.174.107 <none> <none>
demoapp-7859c6f7b7-r7xgh 1/1 Running 0 5m5s 10.200.89.164 192.168.174.108 <none> <none>
测试NodePort Service资源
~# curl http://192.168.174.106:31398
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
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>
查看NodePort Service日志信息
~# kubectl get pod -l app=demoapp -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
demoapp-7859c6f7b7-7zvgq 1/1 Running 0 5m10s 10.200.44.245 192.168.174.107 <none> <none>
demoapp-7859c6f7b7-cq64k 1/1 Running 0 5m5s 10.200.44.246 192.168.174.107 <none> <none>
demoapp-7859c6f7b7-r7xgh 1/1 Running 0 5m5s 10.200.89.164 192.168.174.108 <none> <none>
~# kubectl logs demoapp-7859c6f7b7-7zvgq | tail -n 1
10.200.154.192 - - [14/Jun/2022:07:49:46 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.68.0" "-"
命令结果显示,请求报文的客户端IP地址是最先接收到请求报文的节点上用于集群内部通信的IP地址,而非外部客户端地址。这样才能确保Server Pod的响应报文必须由最先接收到请求报文的节点进行响应,因此NodePort类型的Service对象会对请求报文同时进行原地址转换(SNAT)和目标地址转换(DNAT)操作。
~# ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: ens33: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
link/ether 00:50:56:3a:03:a4 brd ff:ff:ff:ff:ff:ff
inet 192.168.174.106/24 brd 192.168.174.255 scope global ens33
valid_lft forever preferred_lft forever
inet6 fe80::250:56ff:fe3a:3a4/64 scope link
valid_lft forever preferred_lft forever
3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:31:c8:7e:1d brd ff:ff:ff:ff:ff:ff
inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
valid_lft forever preferred_lft forever
inet6 fe80::42:31ff:fec8:7e1d/64 scope link
valid_lft forever preferred_lft forever
5: vethacf4ad4@if4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP group default
link/ether 0a:4d:06:69:3e:b3 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet6 fe80::84d:6ff:fe69:3eb3/64 scope link
valid_lft forever preferred_lft forever
6: cali9678ed22c77@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether ee:ee:ee:ee:ee:ee brd ff:ff:ff:ff:ff:ff link-netnsid 1
inet6 fe80::ecee:eeff:feee:eeee/64 scope link
valid_lft forever preferred_lft forever
7: cali524e47aa080@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether ee:ee:ee:ee:ee:ee brd ff:ff:ff:ff:ff:ff link-netnsid 2
inet6 fe80::ecee:eeff:feee:eeee/64 scope link
valid_lft forever preferred_lft forever
8: calic4ea4b37969@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether ee:ee:ee:ee:ee:ee brd ff:ff:ff:ff:ff:ff link-netnsid 3
inet6 fe80::ecee:eeff:feee:eeee/64 scope link
valid_lft forever preferred_lft forever
9: kube-ipvs0: <BROADCAST,NOARP> mtu 1500 qdisc noop state DOWN group default
link/ether be:7f:b6:de:5c:85 brd ff:ff:ff:ff:ff:ff
inet 10.100.0.2/32 scope global kube-ipvs0
valid_lft forever preferred_lft forever
inet 10.100.161.230/32 scope global kube-ipvs0
valid_lft forever preferred_lft forever
inet 10.100.26.102/32 scope global kube-ipvs0
valid_lft forever preferred_lft forever
inet 10.100.0.1/32 scope global kube-ipvs0
valid_lft forever preferred_lft forever
inet 10.100.167.46/32 scope global kube-ipvs0
valid_lft forever preferred_lft forever
10: tunl0@NONE: <NOARP,UP,LOWER_UP> mtu 1440 qdisc noqueue state UNKNOWN group default qlen 1000
link/ipip 0.0.0.0 brd 0.0.0.0
inet 10.200.154.192/32 scope global tunl0
valid_lft forever preferred_lft forever
Local流量策略
外部流量策略Local则仅会将流量调度至请求的目标节点本地运行的pod对象之上,以减少网络跃点,降低网络延迟,但请求报文指向的节点本地不存在目标Service相关的Pod对象时将直接丢失该报文。
~# kubectl patch services/demoapp-nodeport-svc -p '{"spec":{"externalTrafficPolicy":"Local"}}'
service/demoapp-nodeport-svc patched
~# kubectl describe svc demoapp-nodeport-svc
Name: demoapp-nodeport-svc
Namespace: default
Labels: <none>
Annotations: <none>
Selector: app=demoapp
Type: NodePort
IP Family Policy: SingleStack
IP Families: IPv4
IP: 10.100.167.46
IPs: 10.100.167.46
Port: http 80/TCP
TargetPort: 80/TCP
NodePort: http 31398/TCP
Endpoints: 10.200.44.245:80,10.200.44.246:80,10.200.89.164:80
Session Affinity: None
External Traffic Policy: Local
Events: <none>
~# kubectl get pod -l app=demoapp -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
demoapp-7859c6f7b7-r7xgh 1/1 Running 0 3h2m 10.200.89.164 192.168.174.108 <none> <none>
~# curl http://192.168.174.107:31398
curl: (7) Failed to connect to 192.168.174.107 port 31398: Connection refused
~# curl http://192.168.174.108:31398
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
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>
~# kubectl logs demoapp-7859c6f7b7-r7xgh | tail -n 1
192.168.174.106 - - [14/Jun/2022:10:51:12 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.68.0" "-"
Local策略下无须在集群中转发流量其其它节点,也就不用在对请求报文进行源地址转换,Server Pod所看到的客户端IP就是外部客户端的真实地址。
NodePort类型的Service资源同样会被配置ClusterIP,以确保集群内的客户端对该服务的访问请求可以集群范围的通信中完成。
应用LoadBalancer Service资源
NodePort类型的Service资源虽然能够在集群外部访问,但外部客户端必须事先得知NodePort和集群中至少一个节点IP地址,一旦被选定的节点发送故障,客户端还要自行选择请求访问其他的节点,因而一个有着固定IP地址的固定接入端点将是更好的选择。此外,集群节点很可能是某Iaas云环境中仅具有私有IP地址的虚拟主机,这类地址对互联网客户端不可达,为此类节点接入流量也要依赖于集群外部具有公网IP地址的负载均衡器,由其接入并调度外部客户端的服务请求至集群节点相应的NodePort之上。
Iaas云计算环境通常提供了LBaas服务,它允许租户动态地在自己的网络创建一个负载均衡设备。部署在此类环境之上的Kubernetes集群可借助于CCM在创建LoadBalancer类型的Service资源时调用Iaas的相应API,按需创建出一个软件负载均衡器。但CCM不会为那些非LoadBalancer类型的Service对象创建负载均衡器,而且当用户将LoadBalancer类型的Service调整为其他类型时也将删除此前创建的负载均衡器。
对于没有此类API可以的kubernetes集群,管理员也可以为NodePort类型的Service手动部署一个外部的负载均衡器,并配置将请求流量调度至各节点的NodePort之上,这种方式的缺点是管理员需要手动从外部负载均衡器到内部服务的映射关系。
从实现方式上来讲,LoadBalancer类型的Service就是在NodePort类型的基础上请求外部管理系统的API,并在kubernetes集群外部额外创建一个负载均衡器,并将有关配置保存在Service对象的.status.loadBalancer字段中。
kind: Service
apiVersion: v1
metadata:
name: demoapp-loadbalancer-svc
spec:
type: LoadBalancer
selector:
app: demoapp
ports:
- name: http
protocol: TCP
port: 80
targetPort: 80
loadBalancerIP: 1.2.3.4
Service对象的LoadBalanceIP负责承接外部发来的流量,该IP地址通常由云服务商系统动态配置,或者借助.spec.loadBalancerIP字段显示指定,但有些云服务商不支持用户设定该IP地址,这种情况下,即便提供了也会被忽略。外部负载均衡器的流量会直接调度至Service后端的Pod对象之上,而如何调度流量则取决于云服务商,有些环境可能还需要为Service资源的配置定义添加注解,必要时参考云服务商文档说明。另外,LoadBalancer Service还支持使用.spec.loadBalancerSourceRanges字段指定负载均衡器允许的客户端来源的地址范围。
外部IP
若集群中部分或全部节点除了有用于集群通信的节点IP地址之外,还有可用于外部通信的IP地址,我们还可以在Service资源上启动spec.externalIPs字段来基于这些外部IP地址向外发布服务。所有路由到指定的外部IP地址某端口的请求流量都可由该Service代理到后端pod对象之上,请求流量到达外部IP地址与节点IP并没有本质区别,但外部IP却可能仅存在于一部分的集群节点之上,而且它不受kubernetes集群管理,需要管理员手动介入其配置和回收等操作任务中。
外部IP地址可结合ClusterIP、NodePort或LoadBalancer任一类型的Service资源使用,而到达外部IP的请求流量会直接由相关联的Service调度转发至相应的后端pod对象进行处理。假设实例kubernetes集群中的k8s-node-01节点上拥有一个可被路由到的IP地址192.168.174.300,我们期望能够将demoapp的服务通过该外部IP地址发布到集群外部,示例如下:
kind: Service
apiVersion: v1
metadata:
name: demoapp-externalip-svc
namespace: default
spec:
type: ClusterIP
selector:
app: demoapp
ports:
- name: http
protocol: TCP
port: 80
targetPort: 80
externalIPs:
- 172.168.174.300
应用ExternalName资源
ExternalName类型的Service资源用于将集群外部的服务发布到集群中以供Pod中的应用程序访问,因此,它不需要使用标签选择器关联任何Pod对象,但必须使用spec.externalName属性定义一个CNAME记录于返回外部真正提供服务的主机别名,而后通过CNAME记录值获取到相关主机的IP地址。
由于ExternalName类型的Service资源实现于DNS级别,客户端将直接接入外部的服务而完全不需要服务代理,因此,它无须配置ClusterIP,此种类型的服务也称为Headless Service。
kind: Service
apiVersion: v1
metadata:
name: externalname-redis-svc
namespace: default
spec:
type: ExternalName
externalName: redis.wgs.com
ports:
- protocol: TCP
port: 6379
targetPort: 6379
nodePort: 0
selector: {}
Service与Endpoint资源
在信息技术领域,端点是只通过LAN或WAN连接的能够用于网络通信的硬件设备,它在广义上可以指代任何与网络连接的设备。在kubernetes语境中,端点通常代表pod或节点上能够建立网络通信的套接字,并由专用的资源类型Endpoint进行定义和跟踪。
Endpoint与容器探针
Service对象借助于Endpoint资源来跟踪其关联 的后端端点,但Endpoint是二等公民,Service对象可根据标签选择器直接创建同名的Endpoint对象,几乎很少有直接使用该类型资源的需求。
kind: Service
apiVersion: v1
metadata:
name: services-readiness-demo
namespace: default
spec:
selector:
app: demoapp-with-readiness
ports:
- name: http
protocol: TCP
port: 80
targetPort: 80
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: demoapp2
spec:
replicas: 2
selector:
matchLabels:
app: demoapp-with-readiness
template:
metadata:
creationTimestamp: null
labels:
app: demoapp-with-readiness
spec:
containers:
- image: ikubernetes/demoapp:v1.0
name: demoapp
imagePullPolicy: IfNotPresent
readinessProbe:
httpGet:
path: '/readyz'
port: 80
initialDelaySeconds: 15 # 初次检测延迟时长
periodSeconds: 10 # 检测周期
---
Endpoint对象会根据就绪状态把同名Service对象标签选择器筛选出的后端端点的IP地址分别保存在subsets.addresses字段和subsets.notReadyAddresses字段中,它通过API Server持续、动态跟踪每个端点的状态变动,并及时反应到端点IP所属的字段。仅那些位于subsets.addresses字段的端点地址可由相关的Service用作后端端点。此外,相关Service对象标签选择器筛选出的pod对象数量的变动也将会导致Endpoint对象上的端点数量变动。
该示例Endpoint对象会筛选出Deployment对象创建的两个pod对象,将它们的IP地址和服务端口创建为端点对象。但延迟15秒启动的容器探针会导致这两个pod对象在15秒以后才能转为就绪状态,这意味着在上面配置清单中Service资源创建后治得好15秒之内无可用后端端点。
~# kubectl get endpoints services-readiness-demo -w
NAME ENDPOINTS AGE
services-readiness-demo <none> 2s
services-readiness-demo 3s
services-readiness-demo 3s
services-readiness-demo 10.200.44.250:80 40s
services-readiness-demo 10.200.44.250:80,10.200.89.169:80 40s
因任何原因导致的后端端点就绪状态检测失败,都会触发Endpoint对象将该端点的IP地址从subsets.address字段移至subsets.notReadyAddresses字段。
~# kubectl get endpoints services-readiness-demo -o yaml
apiVersion: v1
kind: Endpoints
metadata:
annotations:
endpoints.kubernetes.io/last-change-trigger-time: "2022-06-16T16:14:54+08:00"
creationTimestamp: "2022-06-16T08:14:14Z"
name: services-readiness-demo
namespace: default
resourceVersion: "1863060"
uid: 8a64739f-7504-4cea-a308-9dfe44053af6
subsets:
- addresses:
- ip: 10.200.44.250
nodeName: 192.168.174.107
targetRef:
kind: Pod
name: demoapp2-5b5dc85587-7vl9c
namespace: default
resourceVersion: "1863054"
uid: b059828d-359d-4326-add5-0b8d00278fa9
- ip: 10.200.89.169
nodeName: 192.168.174.108
targetRef:
kind: Pod
name: demoapp2-5b5dc85587-xg22d
namespace: default
resourceVersion: "1863059"
uid: ecbaadbf-312f-4384-b791-78a0bf442a7f
ports:
- name: http
port: 80
protocol: TCP
~# curl -s -X POST -d 'readyz=FALL' 10.200.89.169/readyz
等待至少3个检测周期共30秒之后,查看Endpoint对象信息。
~# kubectl get endpoints services-readiness-demo -o yaml
apiVersion: v1
kind: Endpoints
metadata:
annotations:
endpoints.kubernetes.io/last-change-trigger-time: "2022-06-16T16:20:24+08:00"
creationTimestamp: "2022-06-16T08:14:14Z"
name: services-readiness-demo
namespace: default
resourceVersion: "1863578"
uid: 8a64739f-7504-4cea-a308-9dfe44053af6
subsets:
- addresses:
- ip: 10.200.44.250
nodeName: 192.168.174.107
targetRef:
kind: Pod
name: demoapp2-5b5dc85587-7vl9c
namespace: default
resourceVersion: "1863054"
uid: b059828d-359d-4326-add5-0b8d00278fa9
notReadyAddresses:
- ip: 10.200.89.169
nodeName: 192.168.174.108
targetRef:
kind: Pod
name: demoapp2-5b5dc85587-xg22d
namespace: default
resourceVersion: "1863577"
uid: ecbaadbf-312f-4384-b791-78a0bf442a7f
ports:
- name: http
port: 80
protocol: TCP
该故障端点重新转回就绪状态后,Endpoint对象会将其移回subsets.addresses字段中,这种处理机制确保了Service对象不会将客户端请求流量调度给那些处于运行状态但服务未就绪的端点。
~# curl -s -X POST -d 'readyz=OK' 10.200.89.169/readyz
~# kubectl get endpoints services-readiness-demo -o yaml
apiVersion: v1
kind: Endpoints
metadata:
annotations:
endpoints.kubernetes.io/last-change-trigger-time: "2022-06-16T16:36:04+08:00"
creationTimestamp: "2022-06-16T08:14:14Z"
name: services-readiness-demo
namespace: default
resourceVersion: "1865060"
uid: 8a64739f-7504-4cea-a308-9dfe44053af6
subsets:
- addresses:
- ip: 10.200.44.250
nodeName: 192.168.174.107
targetRef:
kind: Pod
name: demoapp2-5b5dc85587-7vl9c
namespace: default
resourceVersion: "1863054"
uid: b059828d-359d-4326-add5-0b8d00278fa9
- ip: 10.200.89.169
nodeName: 192.168.174.108
targetRef:
kind: Pod
name: demoapp2-5b5dc85587-xg22d
namespace: default
resourceVersion: "1865059"
uid: ecbaadbf-312f-4384-b791-78a0bf442a7f
ports:
- name: http
port: 80
protocol: TCP
自定义Endpoint资源
除了借助Service对象的标签选择器自动管理后端端点外,kubernetes也支持自定义Endpoint对象,用户可通过配置清单创建具有固定数量端点的Endpoint对象,而调用这类Endpoint对象的同名Service对象无须在使用标签选择器。Endpoint资源的API规范如下:
apiVersion: v1
kind: Endpoint
metadata: # 对象元数据
name: ...
namespace: ...
subsets: # 端点对象的列表
- addresses: # 处于就绪状态的端点地址对象列表
- hostname <string> # 端点主机名
ip <string> # 端点的IP地址,必选字段
nodeName <string> # 节点主机名
targetRef: # 提供该端点的对象引用
apiVersion <string> # 被引用对象所属的API群组及版本
kind <string> # 被应用对象的资源类型,多为pod
name <string> # 对象名称
namespace <string> # 对象所属的名称空间
fieldPath <string> # 被引用的对象的字段,在未引用整个对象时使用,通常仅引用指定pod对象中的单容器,例如spec.containers[1]
uid <string> # 对象的标识符
notReadyAddresses: # 处于未就绪状态的端点地址对象列表,格式与address相同
ports: # 端口对象列表
- name <string> # 端口名称
port <integer> # 端口号,必选字段
protocol <string> # 协议类型,仅支持UDP、TCP和SCTP,默认为TCP
appProtocol <string> # 应用层协议
自定义Endpoint常将那些不是由程序编排的应用定义为kubernetes系统的Service对象,从而让客户端像访问集群上的pod应用一样请求外部服务。例如,假设要把kubernetes集群外部一个可经由192.168.174.300:3306或192.168.174.400:3306任一端点访问的Mysql数据库服务引入集群中,示例如下:
apiVersion: v1
kind: Endpoints
metadata:
name: mysql-external
namespace: default
subsets:
- addresses:
- ip: 192.168.174.300
- ip: 192.168.174.400
ports:
- name: mysql
port: 3306
protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
name: mysql-external
namespace: default
spec:
type: ClusterIP
ports:
- name: mysql
port: 3306
targetPort: 3306
protocol: TCP
显然,非kubernetes管理的端点,其就绪状态难以由Endpoint通过注册监视特定的API资源对象进行跟踪,因而用户需要手动维护这种调用关系的正确性。
Endpoint资源提供了在kubernetes集群上跟踪端点的简单途径,但对于有着大量端点的Service来说,将所有的网络端点信息都存储在单个Endpoint资源中,会对kubernetes控制平面组件产生较大的负面影响,且每次端点资源变动也会导致大量的网络流量。EndpointSlice(端点切片)挺高将一个服务相关的所有端点按固定大小(默认为100个)切割为多个分片,提供了一种更具伸缩性和可扩展性的端点替代方案。
EndpointSlice由引用的端点资源组成,类似于Endpoint,它可由用户手动创建,也可由EndpointSlice控制器根据用户在创建Service资源时指定的标签选择器筛选集群上的pod对象自动创建。单个EndpointSlice资源默认不能超过100个端点,小于该数量时,EndpointSlice与Endpoint存在1:1的映射关系且性能相同。EndpointSlice控制器会尽可能地填满每一个EndpointSlice资源,但不会主动进行重新平衡,新增的端点会尝试添加到现有的EndpointSlice资源上,若超出现有任何EndpointSlice对象的可用的空余空间,则将创建新的EndpointSlice,而非分散填充。
EndpointSlice自kubernetes 1.17版本开始升级为Beta版,隶属于discovery.k8s.io这一API群组。EndpointSlice控制器会为每个Endpoint资源自动生成一个EndpointSlice资源。例如,下面的命令列出了kube-system名称空间中的所有EndpointSlice资源,kube-dns-jpnbz来自于对kube-dns这一Endpoint资源的自动转换。
# kubectl get endpointslices -n kube-system
NAME ADDRESSTYPE PORTS ENDPOINTS AGE
kube-dns-jpnbz IPv4 9153,53,53 10.200.154.228 217d
kubelet-9zlmj IPv4 10255,10250,4194 192.168.174.100,192.168.174.101,192.168.174.102 + 3 more... 212d
EndpointSlice资源根据其关联 的Service与端口划分成组,每个组隶属于同一个Service。
参考文档
https://kubernetes.io/zh-cn/docs/concepts/services-networking/service/