Kubernetes之Service

在Kubernetes中Pod是终将消失的,从创建到销毁的过程中,它们是无法自动重启的。而ReplicationController可以用来动态的创建和销毁Pod(比如说在进行滚动升级的时候,可以进行扩展和收缩)。每一个Pod都得到一个属于自己的IP,但这些IP不能一直有效存在,因为这些IP随着Pod的销毁而变得没有了意义。那么这就导致了一个问题,如果一些Pods为集群内部的其他Pods(我们称它们为前端)提供服务,那么这些前端怎么发现、追踪这些后端集合中的服务呢?Service就是做这个事情的。

Service是一个抽象概念,它定义了一些逻辑上的Pods集合,并且定义了访问这些Pods集合的策略,也被称作为micro-service。Service通常通过Label标签选择器来对应相应的Pods集合(也有一些没有标签选择器的,请看下文介绍)。

举个例子,考虑一个运行的镜像,它在集群中有三个副本,这些副本是可以相互替代的,前端并不关心它现在与哪个后端服务打交道。实际上Pods组成的后端服务集合可以是变化的,比如说通过scale进行副本增加或者副本减少,但我们的前端不应该关心或者跟踪后端服务的变化,Service这一层抽象可以做到这一点。

对于常规的应用,Kubernetes提供了一个简单的Endpoints API,当Service中的Pods集合变化时,Endpoints 也会相应的做出变化。对于其他情况的应用,Kubernetes为Service提供了一个基于虚拟IP的网桥,这个网桥可以把前端的请求重定向到后端Pods中。

定义一个Service

在Kubernetes中,一个Service是一个REST对象,就像Pod一样。像所有的REST对象一样,将一个Service的定义发送给 apiserver 就可以创建一个相应的实例。例如,假设你有一系列的Pods都暴露9376端口,并且标签为"app=MyApp"

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"kind": "Service",
"apiVersion": "v1",
"metadata": {
"name": "my-service"
},
"spec": {
"selector": {
"app": "MyApp"
},
"ports": [
{
"protocol": "TCP",
"port": 80,
"targetPort": 9376
}
]
}
}

这个定义它会创建一个名叫"my-service"的Service实例,这个Service会与那些标签为"app=MyApp"的Pod相关联,并且关联这些Pods 9376端口上开启的TCP服务。这个Service会被分配一个IP地址(有时也被称作cluster IP),它被用来Service的代理。这个Service的标签会被持续的监控评估,并把结果以POST的形式传给一个Endpoints对象,也被称作"my-service"。

注意:Service能够把进入的端口映射成targetPort,默认情况下targetPort会设置成与port这个域相同的值。更有趣的是targetPort可以是一个string类型,与后端Pods端口的名字相关联(比如:http:80、https:443)。实际情况下,分配给每个名字的端口号在每一个Pod中都可能不同,这对于部署和升级自己的Service提供了更多的便利性。举个例子,你可以在不打断客户端的情况下改变下一版本后端服务暴露需要的端口号。Kubernetes的Service支持TCP和UDP,默认情况下是TCP。

在不带标签选择器的情况下定义Service

Service通常用来抽象对Kubernetes中Pods集合的访问,但它们也能抽象成其他成其他种类的后端服务。举个例子:

  • 在你的产品中想要有一个外部的数据库集群,但是想先用自己的数据库测试
  • 你要想把这个服务指向另一个命名空间或者集群的服务
  • 你想将一部分工作迁移至Kubernetes中,剩下的部分放在Kubernetes外部

在上面任一种情况下面,你通过定义一个不带标签选择器的Service来解决,就像下面的定义一样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"kind": "Service",
"apiVersion": "v1",
"metadata": {
"name": "my-service"
},
"spec": {
"ports": [
{
"protocol": "TCP",
"port": 80,
"targetPort": 9376
}
]
}
}

由于这个Service没有标签选择器,所以相应的Endpoints对象就没有被创建,这种情况下你可以手动的创建 Endpoints 对象,并把这个Service映射与之关联起来。Endpoints 对象定义如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"kind": "Endpoints",
"apiVersion": "v1",
"metadata": {
"name": "my-service"
},
"subsets": [
{
"addresses": [
{ "ip": "1.2.3.4" }
],
"ports": [
{ "port": 9376 }
]
}
]
}

注意:Endpoint 的IP可能不是回环地址(127.0.0.1)、Link-local网段(169.254.0.0/16)、Link-local多播地址(224.0.0.0/24)。通过标签选择器的Service与没有标签选择器的Service工作是一样的,这上述那例子中,流量会被用户定义的映射策略路由到Endpoints(1.2.3.4:9376)中。

虚拟IP和服务代理

在Kubernetes集群中的每一个Node节点都运行着一个Kube-proxy代理进程,这个进程负责为上面叙述的Service实现虚拟IP的功能。在Kubernetes 1.0版本中,这个proxy进程仅仅运行在用户空间,在Kubernetes 1.1版本中添加了Iptables代理功能,但这项功能并不是默认启用的,在1.2版本中,我们期望可以默认启用这项功能。

在1.0版本中,Service 是分三层(TCP/UDP Over IP)构建的,在1.1版本中 Ingress API 作为七层(HTTP)服务添加进去了。

代理模式:用户空间

在这种模式下,kube-proxy 进程监控着Kubernetes master进程对Service和Endpoint对象的增添操作。对于每一个Service,它在本地Node上随机选择一个端口打开,任何连接到这个“代理端口”的都会被转发到那些Service后端的Pod集合中(就像Endpoint中声明的)的某一个上,是Service的 SessionAffinity 策略来决定后端的哪一个Pod要被使用。最近的版本中安装了Iptables规则,这会抓取流向Service拥有的集群IP(Cluster IP):port的流量,并且会把这些流量重定向到了这些后端Pod的代理端口上。

这种网络模式的情况下,在客户端不知道Kubernetes的Service和Pod的情况下,流向Service的IP:port的流量重定向到了合适的Pod后端上。默认情况下,选取哪个后端Pod来为前端Client服务是Round Robin策略。通过设置 service.spec.sessionAffinityClientIP(默认为None)可以选择基于 Session AffinityClientIP。代理过程就像下图所示:

代理模式:Iptables

在这种模式下,kube-proxy 进程监控着Kubernetes masterServiceEndPoints 对象的增删。对于每一个 Service 它安装一些iptables规则,能够抓取那些流向 Service 申明的 Cluster IP(实际上是虚拟的)端口的流量,并且能够把这些流量重定向到 Service 对应的后端集合中,而对于每一个EndPoints对象,它安装着一些iptables规则可以选择一个后端Pod来为前端请求服务。

默认情况下,选取哪个后端Pod来提供服务是随机的,可以通过设置 service.spec.sessionAffinity 字段为 ClientIP(默认为None)选择基于 ClientIP 的选取策略。

相比于用户空间的代理,在前端的请求并不知道任何Kubernetes、Service、Pods的情况下,任何流向 Service IP的流量,被代理到一个合适的后端服务上,这要比用户空间代理的模式要更加迅速和可靠。

基于多端口的 Service

许多 Service 需要暴露不止一个端口,对于这种情况下,Kubernetes支持在一个 Service 对象上定义多个端口。当使用多个端口的服务时,你必须声明每个端口的名字,只有这种情况下才能加以区分 EndPoints。比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
{
"kind": "Service",
"apiVersion": "v1",
"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
}
]
}
}

自己为Service定义一个IP

你可以在Service描述中自己指定一个Cluster IP,而不需要集群自动为这个Service来指定一个Cluster IP。我们可以手动设置 spec.clusterIP 这一项。举个例子,如果你已经有了一个DNS条目并且你想替换掉它,或者遗留的系统配置了一个特定的IP并且难以被再次配置,都可以设置 spec.clusterIP。需要注意的是,用户指定的clusterIP必须是在API server指定的 service-cluster-ip-range 的CIDR范围内的,如果用户指定的IP不合法,那么API server就会传回一个HTTP 422状态码报错。

为什么不使用 round-robin DNS ?

有一个问题不时的出现:为什么我们要做这些虚拟IP而不是使用标准的 round-robin DNS 策略,有以下几个原因:

  • 这是DNS历史遗留问题,它的实现并不支持TTL以及缓存域名查询后的结果
  • 许多应用程序只做一次DNS查询并缓存了这次查询的结果
  • 即使如果应用程序或者DNS的实现支持了多次解析,那么这些DNS查询的反复请求,将变得难以管理

为了减少可能发生的错误,我们不建议用户使用上述策略,如果有很多用户建议我们增加对DNS的支持,那么我们也会在下次版本中增加这项支持。

查找Service

在Kubernetes中提供了两种方式查找一个Service : 环境变量DNS

环境变量

当一个Pod运行在一个Node上时,kubelet 会为每一个活跃的 Service 设置一系列的环境变量。它支持 Docker links compatible(详见 makeLinkVariables)和简单的变量定义,比如{SVCNAME}_SERVICE_HOST和{SVCNAME}_SERVICE_PORT变量定义,注意Service的名字是 大写的,并且 破折线要转成下划线

举个例子:"redis-master"的Service暴露了TCP的6379端口,并且赋予了一个集群IP:10.0.0.11,那么它将生成下面环境变量:

1
2
3
4
5
6
7
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,那么Service必须要早于Pod被创建,因为只有Service创建了,环境变量才被创建,但DNS就没有此限制。

DNS

一个可选的集群插件(强烈的推荐)是一个DNS服务器,这个DNS服务器会通过监视创建Service的Kubernetes API的调用,并且会为每一个Service创建一个DNS条目。如果DNS工作了,那么集群中的所有Pods就可以通过DNS的域名解析来自动的发现 Service了。

举个例子:如果你在Kubernetes的"my-ns"命名空间里,有一个叫"my-service"的Service,那么相应的就会为"my-service.my-ns"创建一个DNS记录。在"my-ns"命令空间的Pods节点就可以通过DNS域名查询"my-service"找到,而其他命令空间的Pods必须加上命名空间的修饰符,就像"my-service.my-ns"一样,域名查询的结果就是Cluster IP。

Kubernetes同样支持端口的DNS记录查询,如果"my-service.my-ns"的Service有一个TCP端口命名为"http",你可以通过查询"_http._tcp.my-service.my-ns"去查找到这个对于的端口号码。

Headless(无源的) Service

有时候你并不需要一个单独的ServiceIP或者负载平衡策略,在这种情况下,你可以设置Cluster IP(spec.clusterIP)为"None"来创建一个Headless(无源的) Service。

对于这种类型的Service,并不会分配一个Cluster IP给它,DNS为这种服务名返回多条A记录,并且直接把Pods指向相应的Service。此外 kube proxy 进程也不处理这类Service,并且没有了负载平衡以及平台为它们提供的代理服务,但是 EndPoints 控制器仍然会为这类Service创建相应的 EndPoints 对象。

这种选项允许开发者们减少了与Kubernetes系统的联系,甚至是他们愿意的情况下,以自己的方式实现Service的发现方式也是可以的。对于其他Service的发现方式,应用程序可以很方便的使用内建的API,采用自注册模式和接口来实现。

公开你的Service

对于某些应用(比如是前端),你可能想把这个Service赋予一个外部IP地址暴露出去(在集群的外部,甚至是互联网),而其他的服务只能在集群内部可见。

可以通过设置Kubernetes的ServiceTypes域来定义一种你想要的服务类型,缺省的、最基本的类型是Cluster IP,这种类型的服务仅仅只能在集群内部可见。NodePortLoadBalancer这两种类型的服务可以将服务暴露到集群外部。对于ServiceTypes域的有效值为:

  • ClusterIP:仅仅使用集群内部的IP,这是缺省的方式,也正是上述描述的那样。选择这个值也就意味着,你的这个Service仅仅只能从集群内部访问到。
  • NodePort: 拥有一个集群内部的IP,并且将这个服务暴露在集群上的每一个节点上的某一端口上,你可以通过访问任意NodeIP:NodePort的方式来访问这个服务。
  • LoadBalancer: 拥有一个集群内部的IP,也暴露在一个NodePort端口上。它要求云提供商提供一个负载均衡器的地址,任意NodeIP:NodePort的地址都会转发到这个负载均衡器上。

NodePort类型

如果你把ServiceTypes域设置成NodePort类型,Kuberbetes的master会从经过设置的端口范围内(缺省是30000-32767)分配一个端口给它,并且每一个Node节点将这个端口代理到你的Service,这个端口号可以通过Servicespec.ports[*].nodePort域查询出来。

如果你想要一个特定的端口号,你必须在nodePort域指定一个值,那么系统将会为你分配一个端口号,或者API调用失败(你可能要考虑端口碰撞的可能性),这个值必须在你配置的端口号范围内。

这给开发者们设置自己的负载平衡器,配置那些并不能Kubernetes完全支持的云环境,甚至是直接暴露一个或者更多node节点的IP提供了自由。注意Service将会在NodeIP:spec.ports[*].nodePort和spec.clusterIp:spec.ports[*].port指明的端口可见

LoadBalancer类型

云提供商提供一个外部的负载均衡器,通过设置"ServiceTypes"域为LoadBalancer将会为你的Service提供一个负载均衡器。实际上负载均衡器的创建过程是异步的,可以通过查看Service的status.loadBalancer域来查看负载均衡器的状态信息。举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
{
"kind": "Service",
"apiVersion": "v1",
"metadata": {
"name": "my-service"
},
"spec": {
"selector": {
"app": "MyApp"
},
"ports": [
{
"protocol": "TCP",
"port": 80,
"targetPort": 9376,
"nodePort": 30061
}
],
"clusterIP": "10.0.171.239",
"loadBalancerIP": "78.11.24.19",
"type": "LoadBalancer"
},
"status": {
"loadBalancer": {
"ingress": [
{
"ip": "146.148.47.155"
}
]
}
}
}

来自于外部负载均衡器的流量会直接转发到后端的Pods上,如何精确的工作要取决于云提供商。一些云提供商允许指定负载均衡器的外部IP,在这种情况下将会创建一个用户指定loadBalancerIP的负载均衡器。如果loadBalancerIP域没有被指定,就会给这个负载均衡器指定一个短暂的IP地址,如果指定了这一项,但是云提供商并不支持这一个特性,那么这一项将会被忽略。

外部的IP

Kubernetes的Service可以暴露在一些外部Ip上,如果这些外部IP路由到一个或多个集群节点上。这些通过外部IP的并且目标端口是Service Port的流量,将会被路由到Service相对应的endPoints集合中的某一个,实现负载均衡。Kubernetes本身并不管理这些外部IPexternalIPs)的,它是由集群的管理员管理的。

在Service的描述文件的externalIPs域,并不局限于ServiceTypes域的设定。在下面一个例子中,my-service可以通过**80.11.12.10:80(外部IP:端口)**访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"kind": "Service",
"apiVersion": "v1",
"metadata": {
"name": "my-service"
},
"spec": {
"selector": {
"app": "MyApp"
},
"ports": [
{
"name": "http",
"protocol": "TCP",
"port": 80,
"targetPort": 9376
}
],
"externalIPs" : [
"80.11.12.10"
]
}
}

缺点

对于VIPs,使用用户空间代理的模式只能应用于小到中型的集群,很难扩展到拥有数以千计的Service的大集群,详情参考:the original design proposal for portals

使用用户空间代理这种模式,掩盖了访问一个Service的Packet的源IP,这会导致一些防火墙措施失效。IP代理虽然并没有掩盖集群的源IP,但它仍然影响了那些来自于负载均衡器或者nodePort的客户端。

类型Type字段被设置成嵌套模式,每一级别都可以添加到前一个级别上。这对于所有的云提供商(比如说,Google Compute Engine并不需要分配一个NodePort来使得负载均衡器工作,但是AWS需要如此)并不是严格要求的,但是对于当前的API来说是必须的。

未来的工作

未来我们希望代理策略比简单的round robin均衡策略更加细致入微,比如说master-elected或者sharded。我们还在设想一些Services将会成为真正的负载均衡器,这种情况下,VIP只是简单的传输报文就行了。我们打算提升对Service第七层(HTTP)的支持,对于Service的访问接入机制,我们还有很多更加灵活的模型,这涉及到了当前的Cluster IP,NodePort,负载均衡器等等。

总结:关于虚拟IP的一些细节

对于那些仅仅想使用Service的用户来说,上文的描述已经足够了。然而,在这些实现的背后还有很多值得学习的东西。

碰撞避免

Kubernetes设计的一个首要哲学就是:用户本身没有错误的情况下而导致行为的失败,这种情形不应该暴露给用户。我们正在寻找一个网络端口,如果端口选择可能与另一个用户发生碰撞,那么用户就不应该去选择这个端口,而是交给Kubernetes来选择,这是一个孤立的错误。

为了让用户可以自己为Service选择一个端口,我们必须确定没有2个Service发生碰撞。我们可以为每个Service分配IP地址来实现这一点。

为了确保每一个Service分配了唯一的IP地址,集群内部的IP分配器必须在etcd中自动的更新全局的IP分配图。Service为了得到一个IP,它必须存在IP分配图的注册表中,否则将返回IP不能被分配的报错。后台控制器负责创建这个IP分配图(从老版本的Kubernetes中迁移来的,使用的是内存锁定),同时也检查那些无效的IP分配,从而管理员干预和清理那些已经分配的但目前没有Service在使用的IP。

IP 和 VIP

不同于Pod的IP地址,Pod的IP地址路由到一个固定的目的地上,而Service的IP事实上不是由单台主机回应。相反的,我们使用iptables(Linux的包过滤程序)去定义那些需要重定向的虚拟IP地址。当客户端连接这些虚拟IP(VIP)时,它们的流量会自动的传输到一个合适的endPoint上。根据Service的VIP和Port情况,环境变量和DNS条目会自动的更新。

我们支持两种代理模式:用户空间模式和iptables代理模式,它们之间有稍微不同之处。

用户空间

举个例子,考虑上面描述的一个镜像的处理过程,当后端的Service创建之后,Kubernetes将会分配给它一个虚拟的IP地址,比如说是10.0.0.1。假设Service的端口是1234,这个Service就会被集群中的所有kube-proxy进程注意到。当一个proxy看到一个新的Service的时候,它会打开一个随机的端口,并且建立一条iptables规则用于重定向这个VIP(虚拟IP)到这个新的随机的端口,最后在这上面开始接受外来的连接。

当一个客户端连接到这个VIP时,iptables规则会重定向这个报文到Service自己的端口上,Service代理就会选择一个后端Pod,开始代理这些从客户端到后端的流量。

这意味着Service可以随意的选择端口而不用考虑端口碰撞的情况,客户端也能简单的连接到一个IP和端口上,而不用考虑哪个后端Pod为之服务。

Iptables

再一次考虑上述描述的那个过程,当后端服务创建之后,Kubernetes master会分配一个虚拟的IP地址,举个例子是10.0.0.1。假设Service的端口是1234,这个Service会被集群上的所有kube-proxy进程所发现。当一个proxy看到这个新Service的时候,它会生成一个iptables规则把VIP重定向到Service。每一个Service到EndPonit的规则,通过目的地址NAT修改重定向到了后端Pod中。

当一个客户端连接到VIP时,会选择一个后端(或者依赖session affinity随机选择),并且将报文重定向到后端。不像用户空间代理模式,报文从来不用拷贝到用户空间。kube-proxy进程不需要运行,VIP策略也可以工作,并且客户端的IP地址是不会被改变的。

尽管在一些条件下客户端的IP确实改变了,但当流量通过nodePort或者负载均衡器时,基本的执行流是相同的。

API对象

在Kubernetes REST API中,Service是一个顶层的资源类型,更多关于API的细节,参考Service API object.

最后

花了好久终于把这篇文章翻译好了…英语表达能力差,翻译也不是那么好做的…因为感觉Service很经常接触到,所以翻译了这篇文章。原文地址:http://kubernetes.io/docs/user-guide/services/