K8s Service(3)
Service
概念介绍
虽然每个Pod都会被分配一个单独的IP地址,但这个IP地址会随着Pod的销毁而消失。引出的一个问题是:如果有一组Pod组成一个应用集群来提供服务,那么该如何访问它们呢?
Service
就是用来解决这个问题的,一个Service可以看作一组提供相同服务的Pod的对外接口,Service是通过LabelSelector选择一组Pod作为后端服务提供者。
定义
一个 Service
在 Kubernetes 中是一个 REST 对象,和 Pod
类似。
例如,假定有一组 Pod
,它们对外暴露了 3000 端口,同时还被打上 app=jaymz
标签。
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
selector:
app: jaymz
ports:
- protocol: TCP
port: 80
targetPort: 3000
上述配置创建一个名称为 “my-service” 的 Service
对象,它会将请求代理到使用 TCP 端口 3000,并且具有标签 "app=jaymz"
的 Pod
上。 Kubernetes 为该服务分配一个 IP 地址(有时称为 “集群IP” )。
多端口 Service
对于某些服务,需要公开多个端口。 Kubernetes允许在Service对象上配置多个端口定义。 当使用多个端口时,必须提供所有端口名称,以使它们无歧义。 例如:
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
selector:
app: MyApp
ports:
- name: http
protocol: TCP
port: 80
targetPort: 3000
- name: https
protocol: TCP
port: 443
targetPort: 9000
服务代理
在 Kubernetes 集群中,每个 Node 运行一个 kube-proxy
进程。kube-proxy
负责为 Service
实现了一种 VIP(虚拟 IP)的形式,而不是 ExternalName
的形式。
为什么不使用 DNS 轮询?
时不时会有人问道,就是为什么 Kubernetes 依赖代理将入站流量转发到后端。 那其他方法呢? 例如,是否可以配置具有多个A值(或IPv6为AAAA)的DNS记录,并依靠轮询名称解析?
使用服务代理有以下几个原因:
- DNS 实现的历史由来已久,它不遵守记录 TTL,并且在名称查找结果到期后对其进行缓存。
- 有些应用程序仅执行一次 DNS 查找,并无限期地缓存结果。
- 即使应用和库进行了适当的重新解析,DNS 记录上的 TTL 值低或为零也可能会给 DNS 带来高负载,从而使管理变得困难。
版本兼容性
从Kubernetes v1.0开始,您已经可以使用 用户空间代理模式。 Kubernetes v1.1添加了 iptables 模式代理,在 Kubernetes v1.2 中,kube-proxy 的 iptables 模式成为默认设置。 Kubernetes v1.8添加了 ipvs 代理模式。
虚拟IP
Service
一般会提供一个虚拟IP(Virtual IP),它在Service
创建之后就保持不变,并且能够被同一集群中的Pod资源所访问。Service端口用于接收客户端请求并将其转发给后端的Pod应用程序,这种代理机制称为“端口代理”或四层代理,工作于TCP/IP协议栈的传输层。
API Server会持续监听(watch)Pod的变化(增加、删除),并实时更新Endpoint,Endpoint是一个由Pod的IP地址和端口组成的列表,Service会从Endpoint列表中选择一条记录进行流量调度。
服务代理 - userspace(用户空间)、iptables、ipvs
在众多的Pod对象中,Service资源是如何选择正确的Pod进行流量调度的呢?
这就是我们要具体介绍的,Kubernetes提供的“端口代理”的几种实现方式。
简单来说,一个Service对象就是Node上的一些iptables或ipvs规则,用于将到达Service对象的流量调度到相应的Pod上。Kubernetes集群中的每个Node上都会有产生一个Kube-proxy程序,它通过API-Server持续监控各Service及其关联的Pod对象,并将其变化实时反映至当前节点的iptables或ipvs规则上。
kube-Proxy将请求代理至相应端点的方式有三种:userspace(用户空间)、iptables、ipvs
userspace代理模型
这里的userspace是指Linux操作系统的用户空间。对于每个Service对象它会打开一个本地端口(运行于用户空间的kube-proxy进程负责监听此端口),任何到达此代理端口的连接请求都会被代理至当前Service资源后端的各Pod对象上,至于会挑选哪个Pod取决于Service资源的调度方式,默认为轮询(roundrobin)
这种代理模式请求流量到达内核空间后经由套接字送往用户空间的kube-proxy,而后由它送回内核空间,并调度至后端pod。这种方式请求会在内核和用户空间之间来回转发导致效率不高。
iptables代理模型
iptables代理模式和前一种代理模式是类似的,都是由kube-proxy来跟踪监听API-server上的service和Endpoints的变更。但是有一点不同的是iptables规则直接捕获到达cluster IP和port的流量,并将其重定向至当前Service的后端,对于每个Endpoints对象,Service资源会为其创建iptables规则并关联至挑选的后端pod资源,默认算法是随机调度(random)。iptables代理模式在Kubernetes1.1版本引入,并在1.2版开始成为默认类型。
在创建service资源时,集群中每个节点上的kube-proxy都会接受到通知并将其定义为当前节点上的iptables规则,用于转发接收到的iptables,进行调度和目标地址转换(DNAT)后再转发至集群内部的pod对象上。
默认的策略是,kube-proxy 在 iptables 模式下随机选择一个 backend。
相对于用户空间来讲,iptables模型无须将流量在用户空间和内核空间来回切换,因更加高效和可靠。
ipvs代理模型
Kubernetes从1.9版本引入ipvs代理模型,且从1.11版本起成为默认设置。它和iptables模型很类似,唯一一点不同的是在其请求流量的调度功能由ipvs实现,余下的功能仍由iptables完成。
ipvs是建立在netfilter的钩子函数上,但它使用hash表作为底层数据结构并工作于内核空间,因此流量转发速度特别快、规则同步性很好,而且它支持众多调度算法,rr(轮询)、lc(最小连接数)、dh(目标哈希)、sh(源哈希)、sed(最短期望延迟)、nq(不排队调度)。
服务发现
Kubernetes 支持2种基本的服务发现模式 —— 环境变量和 DNS。
环境变量
当 Pod
运行在 Node
上,kubelet 会为每个活跃的 Service
添加一组环境变量, {SVCNAME}_SERVICE_HOST
和 {SVCNAME}_SERVICE_PORT
变量,这里 Service
的名称需大写,横线被转换成下划线。
举例,一个名称为 "redis-master"
的 Service 暴露了 TCP 端口 6379,同时给它分配了 Cluster IP 地址 10.0.0.11,这个 Service 生成了如下环境变量:
REDIS_MASTER_SERVICE_HOST=10.0.0.11
REDIS_MASTER_SERVICE_PORT=6379
REDIS_MASTER_PORT=tcp://10.0.0.11:6379
REDIS_MASTER_PORT_6379_TCP=tcp://10.0.0.11:6379
REDIS_MASTER_PORT_6379_TCP_PROTO=tcp
REDIS_MASTER_PORT_6379_TCP_PORT=6379
REDIS_MASTER_PORT_6379_TCP_ADDR=10.0.0.11
注意: 必须在Pod出现 之前 创建Service, 否则,这些Pod将不会设定其环境变量。 如果仅使用DNS查找Service的群集IP,则无需担心环境变量问题。
DNS
您可以使用附加组件为Kubernetes集群设置DNS服务。
支持群集的DNS服务器(例如CoreDNS)监视 Kubernetes API 中的新Service
,并为每个Service
创建一组 DNS 记录。 如果在整个集群中都启用了 DNS,则所有 Pod 都应该能够通过其 DNS 名称自动解析出Service
对应的IP地址。
Headless Services
有时不需要或不想要负载均衡,以及单独的 Service IP。 遇到这种情况,可以通过指定 Cluster IP(spec.clusterIP
)的值为 "None"
来创建 Headless
Service。
您可以使用 headless Service 与其他服务发现机制进行接口,而不必与 Kubernetes 的实现捆绑在一起。
对这 headless Service
并不会分配 Cluster IP,kube-proxy 不会处理它们,而且平台也不会为它们进行负载均衡和路由。 DNS 如何实现自动配置,依赖于 Service
是否定义了 selector。
配置 Selector
对定义了 selector 的 Headless Service,Endpoint 控制器在 会创建 Endpoints
记录,并且修改 DNS 配置返回 A 记录(地址),通过这个地址直接到达 Service
的后端 Pod
上。
配置 Selector的Headless Services应用场景
-
自主选择权,有时候
client
想自己来决定使用哪个Endpoints
记录,可以通过查询DNS
来获取Endpoints
的信息。 -
Headless Services
还有一个用处(PS:也就是我们需要的那个特性)。Headless Service
的对应的每一个Endpoints
,即每一个Pod
,都会有对应的DNS
域名;这样Pod
之间就可以互相访问。
不配置 Selector
对没有定义 selector 的 Headless Service,Endpoint 控制器不会创建 Endpoints
记录。 然而 DNS 系统会查找和配置,无论是:
-
ExternalName
类型 Service 的 CNAME 记录 - 记录:与 Service 共享一个名称的任何
Endpoints
,以及所有其它类型
不配置 Selector的Headless Services应用场景
Service
最常见的应用是作为Pod的负载均衡器(反向代理)使用,抽象化对 Kubernetes Pod 的访问,但是Service
也可以作为其他应用程序的反向代理来使用。 实例:
- 希望在生产环境中使用外部的数据库集群,但测试环境使用自己的数据库。
- 希望
Service
指向另一个Namespace
中或其它集群中的服务。 - 您正在将工作负载迁移到 Kubernetes。 在评估该方法时,您仅在 Kubernetes 中运行一部分后端。
服务没有选择器,因此不会自动创建相应的 Endpoint 对象。 您可以通过手动添加 Endpoint 对象,将服务手动映射到运行该服务的网络地址和端口:
apiVersion: v1
kind: Endpoints
metadata:
name: my-service
subsets:
- addresses:
- ip: 192.0.2.42
ports:
- port: 9376
注意: 端点 IPs 必须不可以 : 环回( 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 Services 的群集 IP,因为 kube-proxy 不支持将虚拟 IP 作为目标。
访问没有 selector 的 Service
,与有 selector 的 Service
的原理相同。 请求将被路由到用户定义的 Endpoint, YAML中为: 192.0.2.42:9376
(TCP)。
ExternalName Service
是 Service
的特例,它没有 selector,也没有使用 DNS 名称代替。
类型ExternalName
类型为 ExternalName 的服务将Service
映射到 DNS 名称,而不是典型的选择器,例如 my-service
, 您可以使用 spec.externalName
参数指定这些服务。
例如,以下 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
注意: ExternalName 接受 IPv4 地址字符串,但作为包含数字的 DNS 名称,而不是 IP 地址。 类似于 IPv4 地址的外部名称不能由 CoreDNS 或 ingress-nginx 解析,因为外部名称旨在指定规范的 DNS 名称。 要对 IP 地址进行硬编码,请考虑使用 headless Services。
当查找主机 my-service.prod.svc.cluster.local
时,群集DNS服务返回 CNAME
记录,其值为 my.database.example.com
。 访问 my-service
的方式与其他服务的方式相同,但主要区别在于重定向发生在 DNS 级别,而不是通过代理或转发。 如果以后您决定将数据库移到群集中,则可以启动其 Pod,添加适当的选择器或端点以及更改Service的类型。
外部 IP
如果外部的 IP 路由到集群中一个或多个 Node 上,Kubernetes Service
会被暴露给这些 externalIPs
。 通过外部 IP(作为目的 IP 地址)进入到集群,打到 Service
的端口上的流量,将会被路由到 Service
的 Endpoint 上。 externalIPs
不会被 Kubernetes 管理,它属于集群管理员的职责范畴。
发布服务 —— 服务类型
有一些Pod可能希望将服务暴露给外部应用调用,可以通过Service,选择合适的类型来实现。
Service的默认值是 ClusterIP
类型。
Type
的可选值以及行为如下:
-
ClusterIP
:通过集群的内部 IP 暴露服务,选择该值,服务只能够在集群内部可以访问,这也是默认的ServiceType
。 -
NodePort
:通过每个 Node 上的 IP 和静态端口(NodePort
)暴露服务。NodePort
服务会路由到ClusterIP
服务,这个ClusterIP
服务会自动创建。通过请求<NodeIP>:<NodePort>
,可以从集群的外部访问一个NodePort
服务。 -
LoadBalancer
:使用云提供商的负载局衡器,可以向外部暴露服务。外部的负载均衡器可以路由到NodePort
服务和ClusterIP
服务。 - ExternalName:通过返回 CNAME 和它的值,可以将服务映射到 externalName 字段的内容(例如, foo.bar.example.com)。 没有任何类型代理被创建。
注意: 您需要 CoreDNS 1.7 或更高版本才能使用 ExternalName
类型。
您也可以使用 Ingress 来暴露自己的服务。 Ingress 不是服务类型,但它充当集群的入口点。 它可以将路由规则整合到一个资源中,因为它可以在同一IP地址下公开多个服务。
保留源 IP
各种类型的 Service 对源 IP 的处理方法不同:
-
使用 userspace 代理,隐藏了访问
Service
的数据包的源 IP 地址。 这使得一些类型的防火墙无法起作用。 -
ClusterIP Service:使用 iptables 模式,集群内部的源 IP 会保留(不做 SNAT)。如果 client 和 server pod 在同一个 Node 上,那源 IP 就是 client pod 的 IP 地址;如果在不同的 Node 上,源 IP 则取决于网络插件是如何处理的,比如使用 flannel 时,源 IP 是 node flannel IP 地址。
-
NodePort Service:默认情况下,源 IP 会做 SNAT,server pod 看到的源 IP 是 Node IP。为了避免这种情况,可以给 service 设置
spec.ExternalTrafficPolicy=Local
(1.6-1.7 版本设置 Annotationservice.beta.kubernetes.io/external-traffic=OnlyLocal
),让 service 只代理本地 endpoint 的请求(如果没有本地 endpoint 则直接丢包),从而保留源 IP。 -
LoadBalancer Service:默认情况下,源 IP 会做 SNAT,server pod 看到的源 IP 是 Node IP。设置
service.spec.ExternalTrafficPolicy=Local
后可以自动从云平台负载均衡器中删除没有本地 endpoint 的 Node,从而保留源 IP。