这段时间项目切换新的PaaS平台,在新的架构中需要使用
Service
,借此机会认真学习了一下Service概念,本文大多是官方文档的翻译,如有不妥之处还请多多指教。
Kubernetes的Pod
是会死亡的。Pod
出生、死亡,但是Pod
不能复活。 ReplicaSet
动态地创建和销毁Pods
(例如,在扩缩容的时候)。虽然每个Pod
都会得到一个IP地址,但是随着时间的推移,这些IP地址也是不稳定的。这导致了一个问题:在Kubernetes集群内有一组Pods
(称为后端)向其他Pods
(称为前端) 提供功能,那前端如何找出并跟踪该集合中的后端?
Kubernetes中的Service
可以解决这个问题。
Kubernetes中的Service
是一个抽象,有时候称为微服务,它定义了一组逻辑的Pods
和访问的策略。Service
目标后端Pod
使用Label Selector
选择。
服务定义
在Kubernetes中,Service
和Pod
一样都是REST对象。和其他REST对象一样,可以向apiserver发送Service的定义来创建一个新Service
的实例。举个例子:假设你有一组拥有标签 "app=MyApp"
对外暴露9376端口的Pods,你可以使用下面的yaml定义一个Service
:
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
selector:
app: MyApp
ports:
- protocol: TCP
port: 80
targetPort: 9376
这个定义会创建一个新的名叫"my-service"的Service
对象,该Service
指向所有拥有 "app=MyApp"
标签的Pod
的9376端口。Kubernetes会给Service
分配一个IP地址,通常称为”cluset IP“,服务代理会使用这个IP地址。服务的选择器将会持续的评估,并将结果发布到名为”my-service“的Endpoints
对象。
Service
可以将任何接入端口映射为targetPort
。默认targetPort的值和port一致,更有意思的是,targetPort
可以是一个字符串,字符串指向后端Pods
中的端口名称。每个后端Pod
真正赋给端口名字的端口可能不同。这给Service
的部署和演进提供了很大的灵活性。例如,你可以在下个版本中更改后端pod
对外暴露的端口,而这不会影响客户端。
Service
默认使用TCP协议,也可以使用任何支持的协议(TCP、UDP、HTTP、PROXY protocol和SCTP)。由于许多服务都需要对外暴露多个端口,因此kubernetes支持在Service
对象上定义多个端口。每个端口都可以使用相同或不同的端口。
无selector的Services
Service
通常是Kubernetes Pod
访问的抽象,当然也可以是其他后端。例如:
- 生产环境使用外部数据库,测试环境使用本地数据库。
-
Service
可以可以访问其它namespace
或其他集群的Service
。 - 正在将负载迁移到Kubernetes,但是有一些后端仍然运行在集群外。
以上的任何场景,都可以定义一个没有选择器的Service
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
ports:
- protocol: TCP
port: 80
targetPort: 9376
因为这个Service
没有任何选择器,因此相关联的端点对象也不会被创建。你可以将服务手动映射到特殊的端点:
apiVersion: v1
kind: Endpoints
metadata:
name: my-service
subsets:
- addresses:
- ip: 1.2.3.4
ports:
- port: 9376
注意: 端点的地址不能是环回地址(127.0.0.0/8),链路本地地址(169.254.0.0/16)或者链路本地多播地址(224.0.0.0/24)。因为
kube-proxy
不支持目的地址为虚拟IP,因此这些地址不能是其他KubernetesService
的cluster ip。
没有选择器服务的访问和有选择器的服务一样。流量最终会被路由到用户定义的端点(在这个例子中,流量最终会转发到1.2.3.4:9376)。ExternalName
服务是一种特殊的服务,它没有选择器而是使用DNS名称。
虚拟IPs和服务代理
Kubernetes集群中的每个节点都运行一个kube-proxy
。 kube-proxy
负责为ExternalName
以外类型的Service
实现某种形式的虚拟IP。
Service在Kubernetes中的简单里程碑:
功能 | Kubernetes版本 |
---|---|
Service 四层结构(用户态) | v1.0 |
Ingress API七层结构(HTTP) | v1.1 |
iptables 代理 | v1.1 |
Ingress+iptables proxy 默认工作模式 | v1.2 |
ipvs proxy | v1.8.0-beta.0 |
Proxy-mode: userspace
在这种模式下,kube-proxy
会监视Kubernetes的主以获取Service
和Endpoints
对象的创建和删除。对于每一个Service
,kube-proxy
会在本地节点打开一个随机端口。和该端口建立的任何连接都会被代理到Service
的一个后端Pod
(通常称为端点),使用哪个后端Pod
由Service
的SessionAffinit
决定。最后,kube-proxy
会安装能够捕获到Service
clusterIP和端口流量的iptables,并将流量转发到后端Pod
的代理端口。默认后端Pod
的选择使用轮训算法。
Proxy-mode: iptables
在这种模式下,kube-proxy
会监视Kubernetes的主以获取Service
和Endpoints
对象的创建和删除。对于每一个Service
,kube-proxy
会安装可以捕获到Service
的clusterIP(虚拟IP)和端口的iptables,并将流量转发到Service
后端集合中的一个。对于每一个Endpoints
对象,kube-proxy
安装可以选择后端Pod
的iptables。默认,后端的选择使用轮询算法。
显然,iptables不需要在用户空间和内核空间之间来回切换,比用户空间的代理更加快和可靠。但是,和用户空间代理不同,基于iptables的代理在首次选择的Pod没有响应时不会进行重试,因此基于iptables的代理依赖readiness probes。
Proxy-mode: ipvs
特性状态: Kubernetes v1.11
stable
在这种模式下,kube-proxy
监听Kubernetes的Service
和Endpoints
,调用netlink
接口创建ipvs规则,定期和Kubernetes Service
和Endponts
同步ipvs规则来确保ipvs的状态满足期望。当访问服务时,流量被重定向到一个后端Pod
。
与iptables类似,Ipvs基于netfilter钩子函数,但使用哈希表作为底层数据结构并在内核空间中工作。这意味着ipvs重定向流量更快,同步代理规则也更加高效。此外,ipvs提供了更多的负载均衡算法选择,比如:
rr
: round-robin 轮询lc
: least connection 最小连接dh
: destination hashing 目的地址哈希sh
: source hashing 源地址哈希sed
: shortest expected delay 最小期望延迟nq
: never queue 从不排队
注意: ipvs模式假定在运行kube-proxy之前节点已经安装了IPVS内核模块。当kube-proxy以ipvs代理模式启动,如果节点上已经安装IPVS模块,kube-proxy将会生效。如果没有安装IPVS内核模块,kube-proxy会使用iptables 代理模式。
所有的代理模式,任何访问Service‘s IP:port
的流量都会在客户端不了解Kuberntes或Service
或Pods
的情况下被代理到合适的后端。可以通过将service.spec.sessionAffinity
设置为“ClientIP”(默认为“None”)来选择基于客户端IP的会话亲和关系,通过 service.spec.sessionAffinityConfig.clientIP.timeoutSeconds
可以设置会话保持时间,前提是你已经配置service.spec.sessionAffinity` = “ClientIP”(默认值为“10800").
Proxy-mode: iptables下的规则
Service和Pod基本信息:
Service:
iore-0c08c3c7 10.247.42.16 192.168.0.226 80/TCP 7s
Pod
iore-0c08c3c7-2398027392-kq47k 1/1 Running 0 25m 172.16.12.2 number07-192.168.0.168
iore-0c08c3c7-2398027392-z28ph 1/1 Running 1 25m 172.16.8.2 number05-192.168.0.171
获取节点iptables:
登陆任意节点,执行以下命令获取该节点所有的iptables:
iptables -L -v -n -t nat
在所有的iptables中找到cluster ip:10.247.42.16,发现cluster ip 出现在一个iptable chain中,
Chain KUBE-SERVICES (2 references)
0 0 KUBE-SVC-RQXSPBFOTWEEDJ7V tcp -- * * 0.0.0.0/0 192.168.0.226 /* manage/ioom-0c08c3c7: external IP */ tcp dpt:13888 ADDRTYPE match dst-type LOCAL
0 0 KUBE-MARK-MASQ tcp -- * * 0.0.0.0/0 10.247.42.16 /* manage/iore-0c08c3c7: cluster IP */ tcp dpt:80
0 0 KUBE-SVC-QVYVXAABCXVAUXHW tcp -- * * 0.0.0.0/0 10.247.42.16 /* manage/iore-0c08c3c7: cluster IP */ tcp dpt:80
但是访问10.247.42.16:13888的包是如何被转发到实际的后端pod的呢,为了搞清楚转发规则,需要查看以下Chain KUBE-SVC-QVYVXAABCXVAUXHW
,在iptables中找到chain,具体内容如下:
Chain KUBE-SVC-QVYVXAABCXVAUXHW (3 references)
pkts bytes target prot opt in out source destination
0 0 KUBE-SEP-IBOMAIMAIT43CTBV all -- * * 0.0.0.0/0 0.0.0.0/0 /* manage/iore-0c08c3c7: */ statistic mode random probability 0.50000000000
0 0 KUBE-SEP-HSPFAMPI5V5GWUV5 all -- * * 0.0.0.0/0 0.0.0.0/0 /* manage/iore-0c08c3c7: */
这个Chain一共包含2个chain,流量以0.50000000000
的概率向Chain KUBE-SEP-IBOMAIMAIT43CTBV
转发流量,Chain KUBE-SEP-IBOMAIMAIT43CTBV
的详细信息如下:
Chain KUBE-SEP-IBOMAIMAIT43CTBV (1 references)
pkts bytes target prot opt in out source destination
0 0 KUBE-MARK-MASQ all -- * * 172.16.12.2 0.0.0.0/0 /* manage/iore-0c08c3c7: */
0 0 DNAT tcp -- * * 0.0.0.0/0 0.0.0.0/0 /* manage/iore-0c08c3c7: */ tcp to:172.16.12.2:8000
经过两次转发,流量即可以被正确转发到我们期望的后端。
多端口 Services
许多Services
需要暴露不止一个端口。因为这个原因,kubernetes支持在Service
对象上定义多个端口。当你使用多个端口时,每个端口必须都赋一个名称,这样可以消除端点歧义。一个多端口Service
的例子如下:
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
selector:
app: MyApp
ports:
- name: http
protocol: TCP
port: 80
targetPort: 9376
- name: https
protocol: TCP
port: 443
targetPort: 9377
注意: 端口名字仅支持小写字母、数字和- ,必须以小写字姆或数字开头和结尾。123-abc 和 web 都是合法的端口名,但是123_abc 和-web 是非法的端口名。
选择自己的 IP 地址
创建Service
时可以指定自己的cluster IP地址。为此需要设置 .spec.clusterIP
字段。例如,已经有一个DNS条目并想重用,或者在老系统配置了一个特定的IP地址并且重新配置该地址十分困难。用户自己选的IP地址必须是一个合法的IP地址而且要在 service-cluster-ip-range
定义的网段范围内,如果配置的IP地址不合法,apiserver将会返回422状态码以表明该地址非法。如果使用kubectl expose
命令导出一个服务,可以通过--cluster-ip
来设置cluster IP。
为什么不适用轮询 DNS?
一个时不时出现的问题是我们为什么用虚拟IP做这些事情而不仅仅是使用标准的轮询DNS。下面是一些原因:
- DNS 库不支持DNS TTLS,不缓存名字查找的结果。
- 许多应用使用DNS查询一次,然后缓存DNS查询结果。
- 即使应用程序和库进行了适当的重新解析,每个客户端反复重新解析DNS的负载也难以管理。
我们试图阻止用户做出伤害自己的事情。 也就是说,如果有足够的人要求这样做,我们可以将其作为替代方案来实施。
服务发现
Kubernetes支持通过环境变量和DNS两种模式来发现一个服务。
环境变量
当Pod
在节点运行,kubelet为每一个处于活跃状态的Service
添加一组环境变量。同时支持 Docker links compatible
变量和简单的以{SVCNAME}_SERVICE_HOST
和 {SVCNAME}_SERVICE_PORT
格式命名的变量,其中服务名都是大写字母,.
变转换为_
。
例如, "redis-master"
服务对外暴露TCP端口为6379,cluster ip 10.0.0.11,kubelet将为该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的服务发现没有这个限制。
DNS
尽管Kubernetes强烈推荐使用DNS,但是DNS仍然是集群的可选项。DNS服务器监听Kubernetes 创建新Service
,然后给每个Service
创建一组DNS记录。如果在整个集群中启用了DNS,则所有Pods
应该能够自动对“Service”进行名称解析。
举个例子,在 "my-ns"
命名空间内有一个叫 "my-service"
的 Service
,DNS服务器会创建一个 "my-service.my-ns"
记录。在 "my-ns"
命名空间内的Pods可以通过 "my-service"
寻找到服务,不在该命名空间内的Pod需要使用全限定名 "my-service.my-ns"
寻找服务。根据名字查找到的结果是cluster IP。
Kubernetes还支持命名端口的DNS SRV(服务)记录。如果服务 "my-service.my-ns"
有一个使用TCP
命名为"http"
的端口,你可以使用 "_http._tcp.my-service.my-ns"
进行DNS SRV查询。
使用Kubernetes DNS服务器是唯一可以访问 ExternalName
类型服务的方式。
Headless services
有时您不需要也不想要负载均衡和单个服务IP。 在这种情况下,您可以通过为cluster IP(.pecpec.clusterIP
)指定“None”来创建“无头”服务。
此选项允许开发人员通过自己的方式进行服务发现来减少与Kubernetes系统的耦合。 应用程序仍然可以使用自注册模式,并且可以轻松地在此API上构建适用于其他发现系统的适配器。
对于这些服务,系统不会分配cluster IP,kube-proxy
也不处理这些服务,系统也不会提供负载均衡和代理。如何自动配置DNS取决于服务是否已定义选择器。
With selectors
对于定义了选择器的“”无头“”服务,端点控制器会在API中创建“端点”记录,并修改DNS配置以返回直接指向支持“服务”的“Pods”的记录(地址)。
Without selectors
对于没有定义选择器的无头服务,端点控制器不会创建“端点”记录。但是,DNS系统会查找和配置:
-
ExternalName
类型的服务使用CNNAME记录。 - 所有其他类型的与服务共享名称的“端点”的记录。
发布服务 - 服务类型
对于应用的某些部分(比如前端),你可能希望将Service
暴露在一个外部(集群外)IP地址上。Kubernetes的ServiceTypes
允许你指定创建的Service
类型,默认Service
的类型为ClusterIP
,Type
取值及对应的行为如下:
ClusterIP
: Service暴露在一个集群内部的IP地址上。Service只能在集群内访问。NodePort
: 服务暴露在每个节点的静态端口上。可以通过<NodeIP>:<NodePort>
在集群外访问NodePort
Service。LoadBalancer
:使用云提供商的负载均衡器在外部公开服务。ExternalName
: 将Service映射到外部服务,不设置任何代理,需要kubernetes1.7以上版本的kube-dns
支持。
NodePort
如果你把type
字段设置为NodePort
,Kubernetes会从--service-node-port-range
指定的范围内分配一个端口(范围默认30000-32767), 每个节点都会将该端口(每个节点上的端口与该端口相同)代理到你的Service
。Service
中的.spec.ports[*].nodePort
字段的值就是对应的端口。
如果你想指定代理端口的IP地址,可以将kube-proxy
中的 --nodeport-addresses
设置为特定的IP地址块(该功能从Kubernetes v1.10版本开始支持)。以逗号分隔的IP块列表(例如10.0.0.0/8,1.2.3.4/32)用于过滤此节点的本地地址。例如,如果使用标志--nodeport-addresses = 127.0.0.0 / 8
启动kube-proxy,则kube-proxy将仅为NodePort
服务选择环回接口。--nodeport-addresses
默认为空([]
),这意味着选择所有可用的接口并符合当前的NodePort
行为。
如果需要特定的端口号,可以在nodePort
字段中指定一个值,系统将会分配该端口,否则API事务将失败(用户需要自己处理可能的端口冲突)端口的值必须在配置的端口范围内。
这使开发人员可以自由地设置自己的负载均衡器,配置Kubernetes不完全支持的环境,甚至直接暴露一个或多个节点的IP。
注意NodePort
类型的服务可以通过 <NodeIP>:spec.ports[*].nodePort
和.spec.clusterIP:spec.ports[*].port
访问(如果设置了kube-proxy
的 --nodeport-addresses
,将会过滤节点的IP地址)
LoadBalancer
在支持外部负载均衡器的云提供商上,将type
字段设置为LoadBalancer
将为Service
配置一个负载均衡器。负载平衡器的实际创建是异步发生的,有关配置的f负载均衡器的信息将发布在Service
的.status.loadBalancer
字段中。 例如:
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
selector:
app: MyApp
ports:
- protocol: TCP
port: 80
targetPort: 9376
clusterIP: 10.0.171.239
loadBalancerIP: 78.11.24.19
type: LoadBalancer
status:
loadBalancer:
ingress:
- ip: 146.148.47.155
来自外部负载均衡器的流量将指向后端“Pods”,但具体如何工作取决于云提供商。某些云提供商支持设置 loadBalancerIP
。在这种条件下负载均衡器将以用户指定的 loadBalancerIP
创建。如果未指定loadBalancerIP
字段,则会给负载均衡器分配一个短暂的IP地址。如果设置了 loadBalancerIP
但是云提供商不支持这个特性,该字段将会被忽略。
ExternalName
ExternalName 类型的Service把一个service映射到一个DNS名,而不是典型的选择器,比如 my-service
或 cassandra
。你可以通过 spec.externalName
参数设置这些服务。
下面示例的服务定义,将prod
命名空间内的 my-service
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地址的ExternalNames不会被CoreDNS或ingress-nginx解析,因为ExternalName旨在指定规范的DNS名称. 要对IP地址进行硬编码,请考虑无头服务。
查找主机my-service.prod.svc.cluster.local
时,集群DNS服务将返回值为“my.database.example.com”的“CNAME”记录。访问“my-service”的工作方式与其他服务的工作方式相同,但重要的区别在于重定向发生在DNS级别,而不是通过代理或转发。如果您以后决定将数据库移动到群集中,则可以启动其pod,添加适当的选择器或端点,并更改服务的“type”。
External IPs
如果有一个外部的IP地址路由到集群内的一个或多个节点,kubernetes的service可以暴露在这些 externalIPs
上。在服务端口上使用外部IP(作为目的IP)进入群集的流量将路由到其中一个服务端点。 externalIPs
的管理是系统管理员的责任而不是kubernetes集群的责任。
externalIPs
可以和多种Service配合,在这面的例子中,客户端可以通过 “80.11.12.10:80
” 访问“my-service
” :
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
selector:
app: MyApp
ports:
- name: http
protocol: TCP
port: 80
targetPort: 9376
externalIPs:
- 80.11.12.10
虚拟IP的"血腥"细节
对于大多数想使用Service的用户来说,前面的信息已经足够了。然而,幕后有很多可能值得理解的事情。
避免碰撞
Kubernetes的主要哲学之一是用户不应该暴露于可能导致他们的行为失败的情况,而这不是用户自己的过错。在这种情况下,如果用户选择的端口可能和其他用户冲突,那么用户不应该自己选择端口。 这是隔离失败。
为了允许用户为自己的Service
选择端口,kubernetes必须确保没有两个Service
发生碰撞。kubernetes通过为每个Service分配自己的IP地址来支持该功能。
为了确保每个Service
得到唯一的IP地址,在创建每个Service
之前,内部分配器以原子方式更新etcd中的全局分配集合。为了Service
能够获取到IP地址,映射对象必须存在注册表中,否在创建Service
将失败并显示返回无法分配IP地址的错误。有一个后台控制器负责创建该映射表(从内存锁定中使用的旧版Kubernetes迁移)以及检查由于管理员干预而导致的无效分配,并清除已分配但当前没有Service
使用的任何IP。
IPs and VIPs
与实际路由到固定目的 Pod IP地址不同,Service
的IP地址实际上并不会由单个主机应答。相反,我们使用iptables
(Linux中的数据包处理逻辑)来定义根据需要透明重定向的虚拟IP地址。当客户端连接到虚拟IP地址时,其流量会自动传输到适当的端点。Service
的环境变量和DNS实际上时根据Service
的虚拟IP地址和端口填充的。
kubernetes支持三种代理模式:userspace,iptables和工作方式稍微不同的ipvs。
Userspace
创建后端Service
时,Kubernetes给Service
分配一个虚拟IP地址,比如IP地址为10.0.0.1
。假设Service
的端口为1234,集群中所有的kube-proxy
实例都会观察到Service
。当一个kube-proxy
实例观察到Service
时,kube-proxy
打开一个新的随机端口,并创建一个iptabels将虚拟机IP的端口重定向到这个新的端口,并在新的端口上接受连接。
当一个客户端连接到虚拟IP,iptables规则启动,并将数据包重定向到Service代理自己的端口。Service
代理选择一个后端,然后将数据包从客户端代理到选择的后端。
这意味着Service
的所有者可以任意选择Service
端口而不会有端口冲突问题。客户端只要连接到Service
的IP和端口,而不用关心Pod
的实际访问端口是多少。
Iptables
创建后端Service时,Kubernetes给Service分配一个虚拟IP地址,比如IP地址为10.0.0.1
。假设Service
的端口为1234,集群中所有的kube-proxy
实例都会观察到Service
。当代理看到一个新的Service
时,代理会安装一系列的iptables规则-从VIP重定向到每个Service
的规则。每个Service
规则和每个Endpoints
规则关联,每个Endpoint
规则重定向到后端。
当一个客户端连接到虚拟IP,iptables规则启动。会使用session亲和性或随机算法选择一个后端,并将数据包重定向到该后端。和userspace proxy不同,数据包不会拷贝到用户空间。kube-proxy不必为VIP运行而运行,并且客户端IP不会被更改。
当流量通过节点端口或通过负载均衡器进入时,执行相同的基本流程,但在这些情况下,客户端IP确实会被更改。
Ipvs
在大规模集群中(比如有10000个Service)iptables的操作会显著降低。IPVS旨在实现负载平衡并基于内核中的哈希表。 因此,我们可以从基于IPVS的kube-proxy
实现大量Service
下的性能一致性。于此同时,基于IPVS的kube-proxy
拥有更多更复杂的负载均衡算法。