概述
Service是Kubernetes中的一种资源对象,用于定义一组Pod的网络访问规则,它为Pod提供了一个稳定的统一访问入口,允许客户端始终使用同一个IP地址进行访问,避免直接使用Pod IP地址导致的不稳定性.
Service主要有以下两种功能:
- 负载均衡: 当多个Pod提供服务时,Service通过负载均衡算法将请求分发到这些Pod上,从而实现应用程序的负载均衡
- 服务发现: Service提供了一种服务发现机制,自动维护后端Pod IP的变化,从而保证客户端访问应用程序不受后端Pod变化的影响
Service定义
假设有一组由Deployment管理的Pod,这些Pod在TCP的80端口上提供服务,Pod的标签为"k8s-app=nginx-2",如图:
kubectl get pods,deployment --show-labels
为了将这组Pod对外公开,创建了一个Service资源,配置如下:
vi service.yaml
apiVersion: v1
kind: Service
metadata:
name: nginx-2
spec:
ports:
- port: 80
protocol: TCP
targetPort: 80
selector:
k8s-app: nginx-2
示例中部分字段含义:
- port: 端口映射列表
- protocol: 端口协议,支持TCP,UDP和SCTP,默认为TCP
- targetPort: 目标端口,即容器中应用程序监听的端口,Service能够将任意入站port映射到某个targetPort.默认情况下,出于方便考虑,targetPort会被设置为与port字段相同的值
- selector: 标签选择器(Label Selector),用于定义Service应该将流量转发到那些Pod上,这里表示只有带有"k8s-app=nginx-2
"标签的Pod才会被该Service转发流量,标签选择器如下图:
创建service资源:
kubectl apply -f service.yaml
查看Service对象:
kubectl get svc nginx-2
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
nginx-2 ClusterIP 10.97.164.205 <none> 80/TCP 47h
输出结果中,各字段含义如下:
- NAME: 名称
- TYPE: 公开类型,默认类型为ClusterIP
- CLUSTER-IP: 集群IP地址,也称为虚拟IP地址
- EXTERNAL-IP: 外部IP地址,当正确使用LoadBalancer或ExternalName类型时才会显示外部IP地址,否则显示"none"或者"pending"
- PORT(S): 公开端口和协议
- AGE: 创建时间
如果Pod在多个端口提供服务,Service如何公开这些端口,Kubernetes允许你为Service对象配置多个端口定义,示例如下:
apiVersion: v1
kind: Service
metadata:
name: nginx-2
spec:
ports:
- name: http
port: 80
protocol: TCP
targetPort: 80
- name: https
port: 443
protocol: TCP
targetPort: 443
selector:
k8s-app: nginx-2
上述示例中,定义了两个端口映射规则,其中第一规则是将80映射到目标Pod的80端口,第二个规则是将443映射到目标Pod的443端口
应用配置后再次查看Service对象
可以看到端口映射规则PORT(S)列多个443的规则,在集群中任意Pod或节点上,访问IP地址"10.97.164.205"的80端口Service会将流量转发到目标Pod的80端口,而访问虚拟IP地址的443端口会将流量转发到目标Pod的443端口,Service多端口映射如图:
Service公开类型
Service的公开类型定义了Service如何对外网络公开,并支持四种公开类型:
- ClusterIP: 默认类型. Service会被分配一个虚拟IP地址.集群中的应用可以通过该IP地址访问Service
- NodePort: 在每个节点上开放一个固定端口,并将该端口映射到Service上.这允许集群外部的用户可以通过节点IP地址和固定端口访问Service.
- LoadBalancer: 在底层云提供商创建一个负载均衡,并将外部流量转发到集群中的Service
- ExternalName: 将Service的名称映射到指定的外部域名
ClusterIP
ClusterIP类型适用于应用程序仅需要再集群内部访问的场景,比如我将一个游戏服务进行了拆分,如图:
可以看到日志服务和逻辑服务仅由集群中的网关和bt服内部调用(上图只是简单演示,实际生产环境中可能有相互调用的需求),这种情况下,日志服务和逻辑服务可以使用Service ClusterIP类型来公开相关Pod
注:有时你并不需要负载均衡,也不需要单独的Service IP, 遇到这种情况,可以通过显示设置集群IP(spec.clusterIp)的值为"None"来创建无头服务(Headless Service), 有什么用呢,如上,日志和逻辑服务可以使用Headless Serice,这样每个Pod的IP地址会直接暴漏在在服务发现中,其他Pod可以通过DNS查询获取特定的Pod IP(服务发现后面会提到),建立点对点的通信而不是通过负载均衡来分配请求,这对状态服务特别有用
NodePort
NodePort类型适用于应用程序需要集群外部访问的场景.如图:
配置示例:
apiVersion: v1
kind: Service
metadata:
name: nginx-2
spec:
type: NodePort
ports:
- port: 80
protocol: TCP
targetPort: 80
selector:
k8s-app: nginx-2
创建Service资源后,查看对象:
在上述结果中, "PORT(S)"列现实的值为"80:32178/TCP",其中冒号(":")左侧表示访问虚拟IP地址对应的端口,右侧为节点开放的固定端口,我们可通过任意节点的IP地址加32178端口来访问这个网站
默认情况下,Kubernetes为NodePort类型的Service分配一个固定端口,该端口取值为3000~32767,我们可以通过"nodePort"字段指定这个固定端口,示例:
apiVersion: v1
kind: Service
metadata:
name: nginx-2
spec:
type: NodePort
ports:
- port: 80
protocol: TCP
targetPort: 80
nodePort: 30002
selector:
k8s-app: nginx-2
需要注意的是指定的端口需要在3000~32767端口范围内,其未被其他Service使用,如果被占用会报错"provided port is already allocated"
LoadBalancer
LoadBalancer类型是针对云提供商(阿里云,AWS,Azure)设计的一种类型,当创建一个LoadBalancer类型的Service时,云提供商的控制器从Kubernetes API中感知到该Service,然后调用云提供商API来创建一个负载均衡器,并将节点地址和nodePort端口作为后端添加到该负载均衡器中,这意味着用户可以通过负载均衡器访问集群中的Service,适用于要求外部直接访问且需要高可用负载均衡的生产环境,如图,对外sdk服务设计:
ExternalName
ExternalName是一种特殊的类型,与其他类型不同的是,它不提供负载均衡和服务发现功能,而是专用于将Service的名称映射到指定的外部域名上 如图:
EndPoints对象
当创建一个Service时,Kubernetes会根据Service创建一个EndPoints对象,该对象负责维护与Service相关的后端Pod信息,并确保Service可以被正确地转发到后端Pod,Service和EndPoints之间的关系如图:
查看endpoints对象
在上述结果中: "NAME"列显示的名称与Service名称相对应,"ENDPOINTS"列显示了关联Pod IP地址和端口,其中超出3个显示未省略号.Endpoints资源还可以手动创建,自定义关联的IP地址和端口以满足特定网络代理需求.
比如我们游戏服务容器化了,数据首先是不能存放到服务器上的(云原生最佳实践),业务数据需要使用独立托管的数据库服务,比如阿里云的RDS,我们可以通过Service代理集群外部的MYSQL服务,配置示例
vi service-mysql-proxy.yaml
apiVersion: v1
kind: Endpoints
metadata:
name: mysql-proxy
subsets:
- addresses:
- ip: 10.10.10.249
ports:
- port: 3306
protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
name: mysql-proxy
spec:
ports:
- port: 3306
targetPort: 3306
示例中我们定义一个名为"mysql-proxy"的Endpoints资源,其中subsets(子集)字段包含了一个独立MYSQL服务的IP地址和端口,然后定义了一个名为"mysql-proxy"的Service资源,将访问3306端口的流量给转发到目标端口3306也就是说当集群中的Pod访问这个Service时,Kubernetes会将流量给转发给这个外部的mysql服务
创建资源后查看Service和Endpoints对象:
创建测试Pod验证
看起来使用Service name还是使用虚拟IP 10.98.220.202都可以连接这个外部的mysql服务,为什么呢,这个就是后面要提到的服务发现原理
Service服务发现
Kubernetes提供了两种服务发现模式,环境变量和DNS,Pod中的应用程序可以通过它们访问其他Service
环境变量
当创建一个Pod时,Kubernetes默认会将同一个命名空间下的所有Service信息以环境变量的形式注入Pod,其中,"<SERVICE_NAME>_SERVICE_HOST"环境变量保存了Service的虚拟IP地址,"<SERVICE_NAME>_SERVICE_PORT"环境变量保存了Service的端口号,这样,容器中的应用程序可以通过这些环境变量来获取Service的访问地址
比如我在default空间里有这样的Service
kubectl get svc nginx-2 -n default
我现在在default空间创建一个临时Pod使用env查看环境变量
kubectl run -it --rm test --image=harbor.peng.com/library/busybox -- sh
可以看到环境变量{SVCNAME}_SERVICE_HOST和{SVCNAME}_SERVICE_PORT信息
官网这里有个说明需要注意:
当你的Pod需要访问某Service,并且你在使用环境变量方法将端口和集群IP发布到客户端Pod时,必须在客户端Pod出现之前创建该Service,否则,这些客户端Pod将不会出现对应的环境变量.
如果仅使用DNS来发现Service的集群IP,则无需担心此顺序问题
DNS
Kubernetes默认使用CoreDNS作为集群内部的DNS服务,它主要负责解析Service名称,这允许集群中的应用程序可以通过Service名称进行通信,而无须硬编码具体的虚拟IP地址.
Service名称的域名格式为"<SERVICE_NAME>.<NAMESPACE>.svc.cluster.local",比如我之前在default空间创建有个名叫nginx-2的Service,现在我在default空间中再创建一个测试Pod来进行域名解析测试
kubectl run -it --rm dns-test --image=harbor.peng.com/library/busybox -- sh
域名 "nginx-2.default.svc.cluster.local"解析IP地址正式对应的虚拟IP地址,这意味可以通过这个域名访问Service
CoreDNS工作流程
在容器中访问一个域名时,系统会将DNS查询请求发送到"/etc/resolv.conf"文件中配置的DNS服务器地址,即CoreDNS Service的虚拟IP地址,CoreDNS服务接收到DNS查询请求后,根据Service名称解析为响应的虚拟IP地址,CoreDNS工作流程如图:
容器中"/etc/resolv.conf"文件内容如下:
上述结果各字段含义如下:
- nameserver: DNS服务器的IP地址,这里的值为"10.96.0.10"即CoreDNS Service的虚拟IP地址
- search: DNS查询的搜索域列表
- options: 选项参数
查看CoreDNS和Service对象:
kubectl get pod,svc -l k8s-app=kube-dns -n kube-system
Pod的DNS策略
Pod的DNS策略指定了Pod内部用于域名解析的策略,在Pod配置中"dnsPolicy"字段的设置具有以下可选值:
- ClusterFirst: 默认值,优先使用集群内部的CoreDNS服务进行域名解析,如果无法解析,则尝试使用主机上配置的DNS服务器进行域名解析
- ClusterFirstWithHostNet: 作用与ClusterFirst一样,但仅在Pod运行在主机网络命名空间中(hostNetwork: true)时使用,确保Pod仍然可以使用集群内部的CoreDNS服务进行域名解析
- Default: Pod使用主机上配置的DNS服务器进行域名解析,这意味着它不支持Service名称解析
- None: 当设置为None时,需要通过"dnsConfig"字段来手动配置DNS参数
上面策略中ClusterFirstWithHostNet描述有些不好理解,实际上在生产环境中,有些应用为了性能或网络配置的原因,需要直接使用宿主机的网络(即hostNetwork: true)但是它可能还需要访问Kubernetes内部服务(例如,监控,日志服务)这时候 ClusterFirstWithHostNet DNS策略就可以派上用场了,示例:
vi dns-test.yaml
apiVersion: v1
kind: Pod
metadata:
name: dns-test
spec:
hostNetwork: true
dnsPolicy: ClusterFirstWithHostNet
containers:
- image: harbor.peng.com/library/busybox
name: busybox
command: ['/bin/sh', '-c', 'sleep 1d']
创建资源后,尝试解析ServiceName域名
自定义DNS记录
CoreDNS允许用户自定义DNS记录,以满足更多的域名解析需求,比如我们游戏服务实现容器化了,但是游戏层需要调用gm api服务, 而gm api服务是独立于游戏集群外的,虽然我们可以使用Service ExternalName这种独代的方式去满足容器应用请求外部服务的需求,但是往往这个gm api服务为多个域名,甚至是同一个域名后缀有多个这样的API服务游戏层里面会用到,那么这时候通过自定义DNS的方式会更加灵活地满足我们的需求. 编辑CoreDNS配置文件(存储在ConfiMap对象中):
kubectl edit configmap coredns -n kube-system
上述配置中新增了"hosts{}"部分用于定义域名和IP地址映射,其中一条映射表示将域名"gm.xxxx.com"解析到IP地址"10.10.10.242",重建Pod使配置生效
kubectl rollout restart deployment/coredns -n kube-system
启动测试容器进行解析测试如下:
指定外部DNS服务器
假设公司内部有一台DNS服务器,负责对内部应用程序的域名进行解析,现在,我们希望集群中的应用程序能够继续使用该域名访问内部应用程序,这虽然可以通过自定义DNS记录来实现,但是当域名较多或者动态变化时,会显得很不方便,在这种情况下,可以给CoreDNS指定外部DNS服务器作为上游服务器,将来自指定域名后缀的DNS请求转发到这台DNS服务器上,整个转发流程如图:
指定外部DNS服务器的配置示例如下:
上述配置新增了"xxx.com:53{}"部分,这意味这来自"xxx.com"域名后缀的DNS查询请求会被转发到外部DNS服务器"10.10.10.1"
Service代理模式
当访问Service的虚拟IP地址时,流量会被转到到后端Pod,那么这个转发是怎么实现的呢?
Service是一个抽象的资源对象,主要用于定义端口映射规则.具体的流量转发工作由kube-proxy组件负责,它利用主机上的iptables和IPVS技术来实现夹具体网络转发
kube-proxy组件默认使用iptables作为代理模式,通过在节点上自动配置iptables规则来实现Pod的负载均衡和网络转发,而在使用IPVS作为代理模式时,kube-proxy在节点上自动配置IPVS规则来实现Pod的负载均衡和网络转发
iptables和IPVS两种工作模式的总结对比
模式 | 性能 | 使用场景 | 优点 | 缺点 |
iptables | 低 | 小规模集群 | 部署简单 | 规则多时性能下降 |
IPVS | 高 | 大规模集群,高并发场景 | 性能优异,支持更高并发 | 需要额外的内核模块 |
参考文档:
服务(Service) | Kubernetes