一、为什么需要Service?
Pod是非永久性资源,会动态创建和销毁,pod的ip会变化。这会导致一类Pod(业务1)访问另一类Pod(业务2)需要找出并跟踪Pod(业务2)的IP地址,再者Pod(业务2)是多个的,如何提供负载均衡呢?虽然Pod1通过轮询一组Pod的ip可以实现,但会Pod就需要增加负载均衡的逻辑,Pod就变得不纯粹了,不符合单一设计原则。于是有了Service,把一类Pods上的应用程序抽象成服务,并提供可以访问他们的策略。
二、如何通过Service访问Pod
有两种方式:选择算符的Service和没有选择算符的Service
1. 选择算符的Service
这是最常见的方式,指定 spec.selector
即通过打标签的方式,主要是针对集群内部同命名空间的Pod
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
selector:
app: MyApp
ports:
- protocol: TCP
port: 80
targetPort: 9376
创建上面的Service,会自动创建相应的 Endpoint 对象
kubectl describe ep/my-service
查看自动创建的Endpoint对象
...
Subsets:
Addresses: 10.244.2.1,10.244.4.1
...
2. 没有选择算符的Service
主要是针对希望服务指向另一个名字空间或者其他集群中的服务,比如外部的ES集群
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
ports:
- protocol: TCP
port: 80
targetPort: 9376
此服务没有选择算符,因此不会自动创建相应的 Endpoint 对象, 需要手动动添加 Endpoint 对象,将服务手动映射到运行该服务的网络地址和端口
apiVersion: v1
kind: Endpoints
metadata:
name: my-service
subsets:
- addresses:
- ip: 192.0.2.42
ports:
- port: 9376
不管哪种方式最终请求都是被路由到Endpoint,通过Endpoint进行访问处理的,详见下文的访问实现原理
三、Service访问实现原理
1.iptables 代理模式
该模式下,节点上kube-proxy 持续监听 Service 以及 Endpoints 对象的变化,并设置进本地节点iptables,请求反向代理全部交给 iptables 来实现。每个Node节点都会配置所有Service进iptables,当捕获到Service的clusterIP和端口请求,利用注入的iptables,将请求重定向到Service的对应的Pod,v1.2版本之后的默认模式。
iptables -t nat -nvL|grep gateway-svc
通过查看iptables规则可以看到:
kube-proxy 在 iptables 模式下随机选择一个后端Pod,利用Pod 就绪探测器验证Pod是否正常,kube-proxy只会把正常的Pod写入iptables,避免流量进入不正常的Pod。
2.userspace 代理模式(早期版本)
kube-proxy 会监视 Kubernetes 控制平面对 Service 对象和 Endpoints 对象的添加和移除操作。 对每个 Service,它会在本地 Node 上打开一个端口(随机选择)。请求先经过iptables规则,当捕获到达Service 的 clusterIP和 Port 的请求,并重定向到代理端口(kube-proxy),再由代理端口再代理请求到后端Pod(v1.2版本之前的默认模式)
userspace是在用户空间,通过kube-proxy来实现service的代理服务,service的请求会先从用户空间进入内核iptables,然后再回到用户空间,由kube-proxy完成后端Endpoints的选择和代理工作,这样流量从用户空间进出内核带来的性能损耗是不可接受的,因此这种方式已经不用了。
3. IPVS 代理模式
IPVS模式是利用linux的IPVS模块实现,同样是由kube-proxy实时监视集群的service和endpoint,调用netlink接口相应的创建ipvs规则,由ipvs实现负载均衡访问。IPVS 专为负载平衡而设计,并基于内核内哈希表,有更高的网络流量吞吐量(iptables 模式在大规模集群 比如10000 个服务中性能下降显著),并且具有更复杂的负载均衡算法(最小连接、局部性、 加权、持久性)。(v1.8以后新支持的)
当 kube-proxy 以 IPVS 代理模式启动时,它将验证 IPVS 内核模块是否可用。 如果未检测到 IPVS 内核模块,则 kube-proxy 将退回到以 iptables 代理模式运行。
四、服务发布(服务类型)
- ClusterIP:k8s默认的ServiceType,通过集群内的ClusterIP在内部发布服务
- NodePort:用来对集群外暴露Service,你可以通过访问集群内的每个NodeIP:NodePort的方式,访问到对应Service后端的Endpoint
- LoadBalancer: 这也是用来对集群外暴露服务的,不同的是这需要外部负载均衡器的云提供商,比如AWS等
- ExternalName:这个也是在集群内发布服务用的,需要借助KubeDNS(version >= 1.7)的支持,就是用KubeDNS将该service和ExternalName做一个Map,KubeDNS返回一个CNAME记录。
每种服务类型都是会指定一个clusterIP的,由clusterIP进入对应代理模式实现负载均衡,如果强制
spec.clusterIP: "None"
(即headless service),集群无法为它们实现负载均衡,直接通过Pod域名访问Pod,典型是应用是StatefulSet
五、Service的域名访问
上面讲的Pod之间调用,采用Service进行抽象,服务之间可以通过clusterIP 进行访问调用,不用担心Pod的销毁重建带来IP变动,同时还能实现负载均衡。但是clusterIP也是有可能变动,况且采用IP访问始终不是一种好的方式。通过DNS和环境变量可以实现通过服务名现在访问。
1. DNS
k8s采用附加组件(CoreDNS)为集群提供DNS服务,会为每个服务创建DNS记录,CoreDNS只为Service和Pod创建DNS记录。kubernetes强烈推荐采用DNS方式.
例如,如果你在 Kubernetes 命名空间 my-ns 中有一个名为 my-service 的服务, 则控制平面和 DNS 服务共同为 my-service.my-ns 创建 DNS 记录。 my-ns 命名空间中的 Pod 应该能够通过按名检索 my-service
来找到服务,其他命名空间中的 Pod 必须将名称限定为 my-service.my-ns
。 这些名称将解析为为服务分配的集群 IP。
Kubernetes 还支持命名端口的 DNS SRV(服务)记录。 如果 my-service.my-ns 服务具有名为 http 的端口,且协议设置为 TCP, 则可以对 _http._tcp.my-service.my-ns 执行 DNS SRV 查询查询以发现该端口号, "http" 以及 IP 地址
2. 环境变量
当 Pod 运行在 Node上,kubelet 会为每个活跃的 Service 添加一组环境变量。 简单的 {SVCNAME}_SERVICE_HOST
和 {SVCNAME}_SERVICE_PORT
变量。 这里 Service 的名称需大写,横线被转换成下划线。
举个例子,一个名称为 nginx-svc
的 Service 暴露了 TCP 端口 8080, 同时给它分配了 Cluster IP 地址 10.0.0.11,这个 Service 生成了如下环境变量:
进入example容器,env打印环境变量:
NGINX_SVC_SERVICE_HOST=10.0.0.11
NGINX_SVC_SERVICE_PORT=8080
NGINX_SVC_PORT=tcp://10.0.0.11:8080
NGINX_SVC_PORT_8080_TCP=tcp://10.0.0.11:8080
NGINX_SVC_PORT_8080_TCP_PROTO=tcp
NGINX_SVC_PORT_8080_TCP_PORT=8080
NGINX_SVC_PORT_8080_TCP_ADDR=10.0.0.11
访问Nginx服务可以使用
curl http://${NGINX_SVC_SERVICE_HOST}:${NGINX_SVC_SERVICE_PORT}
当你创建一个Pod的时候,kubelet会在该Pod中注入集群内所有Service的相关环境变量。需要注意: 要想一个Pod中注入某个Service的环境变量,则必须Service要先比该Pod创建