kubernetes&容器网络(1)之seivice

1. Service介绍

Kubernetes 中有很多概念,例如 ReplicationController、Service、Pod等。我认为 ServiceKubernetes 中最重要的概念,没有之一。

为什么 Service 如此重要?因为它解耦了前端用户和后端真正提供服务的 Pods 。在进一步理解 Service 之前,我们先简单地了解下 PodPod 也是 Kubernetes 中很重要的概念之一。

Kubernetes 中,Pod 是能够创建、调度、和管理的最小部署单元,而不是单独的应用容器。Pod 是容器组,一个Pod中容器运行在一个共享的应用上下文中。这里的共享上下文是为多个 Linux Namespace 的联合。例如:

  • PID命名空间(在同一个 Pod 中的应用可以看到其它应用的进程)
  • Network名字空间(在同一个 Pod 中的应用可以访问同样的IP和端口空间)
  • IPC命名空间(在同一个 Pod 中的应用可以使用SystemV IPC或者POSIX消息队列进行通信)
  • UTS命名空间(在同一个 Pod 中的应用可以共享一个主机名称)
  • Pod 是一个和应用相关的“逻辑主机”, Pod 中的容器共享一个网络名字空间。 Pod 为它的组件之间的数据共享、通信和管理提供了便利。

我们可以看出, Pod 在资源层面抽象了容器。

因此,在Kubernetes中,让我们暂时先忘记容器,记住 Pod

KubernetesService的定义是:Service is an abstraction which defines a logical set of Pods and a policy by which to access them。我们下面理解下这句话。

刚开始的时候,生活其实是很简单的。一个提供特定服务的进程,运行在一个容器中,监听容器的IP地址和端口号。客户端通过<Container IP>:<ContainerPort>,或者通过使用Docker的端口映射<Host IP>:<Host Port>就可以访问到这个服务了。The simplest, the best,简单的生活很美好。

但是美好的日子总是短暂的。小伙伴们太热情了,单独由一个容器提供服务不够用了,怎么办?很简单啊,由多个容器提供服务不就可以了吗。问题似乎得到了解决。

可是那么多的容器,客户端到底访问哪个容器中提供的服务呢?访问容器的请求不均衡怎么办?假如容器所在的主机故障了,容器在另外一台主机上拉起了,这个时候容器的IP地址变了,客户端怎么维护这个容器列表呢?所以,由多个容器提供服务的情况下,一般有两种做法:

客户端自己维护提供服务的容器列表,自己做负载均衡,某个容器故障时自己做故障转移;
提供一个负载均衡器,解耦用户和后端提供服务的容器。负载均衡器负责向后端容器转发流量,并对后端容器进行健康检查。客户端只要访问负载均衡器的IP和端口号就行了。
我们在前面说Service解耦了前端用户和后端真正提供服务的 Pod s。从这个意义上讲,Service就是KubernetesPod 的负载均衡器。

从生命周期来说, Pod 是短暂的而不是长久的应用。 Pod 被调度到节点,保持在这个节点上直到被销毁。当节点死亡时,分配到这个节点的 Pod 将会被删掉。

Service在其生命周期内,IP地址是稳定的。对于Kubernetes原生的应用,Kubernetes提供了一个Endpoints的对象,这个Endpoints的名字和Service的名字相同,它是一个:的列表,负责维护Service后端的Pods的变化。

总结一下,Service解耦了前端用户和后端真正提供服务的PodsPod在资源层面抽象了容器。由于它们的存在,使得这个简单的世界变得复杂了。

对了,Service怎么知道是哪些后端的Pods在真正提供自己定义的服务呢?在创建Pods的时候,会定义一些label;在创建Service的时候,会定义Label Selector,Kubernetes就是通过Label Selector来匹配后端真正服务于Service的后端Pods的。

2. 定义一个Service

接下来就有点没意思了,我要开始翻译上面说的很重要的Services in Kubernetes了。当然,我会加入自己的理解。

Service也是Kubernetes中的一个REST对象。可以通过向apiserver发送POST请求进行创建。当然,Kubernetes为我们提供了一个强大和好用的客户端kubectl,代替我们向apiserver发送请求。但是kubectl不仅仅是一个简单的apiserver客户端,它为我们提供了很多额外的功能,例如rolling update等。

我们可以创建一个yaml或者json格式的Service规范文件,然后通过kubectl create -f <service spec file>创建之。一个Service的例子如下:

{
"kind": "Service",
"apiVersion": "v1",
"metadata": {
"name": "my-service"
},
"spec": {
"selector": {
"app": "MyApp"
},
"ports": [
{
"protocol": "TCP",
"port": 80,
"targetPort": 9376
}
]
}
}

以上规范创建了一个名字为my-serviceService对象,它指向任何有app=MyApp标签的、监听TCP端口9376的任何Pods

Kubernetes会自动地创建一个和Service名字相同的Endpoints对象。Serviceselector会被持续地评估哪些Pods属于这个Service,结果会被更新到相应的Endpoints对象。

当然,你也可以在定义Service的时候为其指定一个IP地址(ClusterIP,必须在kube-apiserver的--service-cluster-ip-range参数定义内,且不能冲突)。

Service会把到:的流量转发到targetPort。缺省情况下targetPort等于port的值。一个有意思的情况是,这里你可以定义targetPort为一个字符串(我们可以看到targetPort的类型为IntOrString),这意味着在后端每个Pods中实际监听的端口值可能不一样,这极大地提高了部署Service的灵活性。

KubernetesService支持TCPUDP协议,缺省是TCP

3. Service发布服务的方式

Service有三种类型:ClusterIP,NodePort和LoadBalancer

3.1 ClusterIP

关于ClusterIP

通过Servicespec.type: ClusterIP指定;
使用cluster-internal ip,即kube-apiserver--service-cluster-ip-range参数定义的IP范围中的IP地址;
缺省方式;
只能从集群内访问,访问方式:<ClusterIP>:<Port>
kube-proxy会为每个Service,打开一个本地随机端口,通过iptables规则把到Service的流量trap到这个随机端口,由kube-proxy或者iptables接收,进而转发到后端Pods

3.2 NodePort

关于NodePort

通过Servicespec.type: NodePort指定;
包含ClusterIP功能;
在集群的每个节点上为Service开放一个端口(默认是30000-32767,由kube-apiserver--service-node-port-range参数定义的节点端口范围);
可从集群外部通过<NodeIP>:<NodePort>访问;
集群中的每个节点上,都会监听NodePort端口,因此,可以通过访问集群中的任意一个节点访问到服务。

3.3 LoadBalancer

关于LoadBalancer

通过Servicespec.type: LoadBalancer指定;
包含NodePort功能;
通过Cloud Provider(例如GCE)提供的外部LoadBalancer访问服务,即<LoadBalancerIP>:<Port>
Service通过集群的每个节点上的<NodeIP>:<NodePort>向外暴露;
有的cloudprovider支持直接从LoadBalancer转发流量到后端Pods(例如GCE),更多的是转发流量到集群节点(例如AWS,还有HWS);
你可以在Service定义中指定loadBalancerIP,但这需要cloudprovider的支持,如果不支持则忽略。真正的IPstatus.loadBalancer.ingress.ip中。
一个例子如下:

{
"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"
}
]
}
}
}

目前对接ELB的实现几大厂商,比如 HWELB转发流量到集群节点,后面再由kube-proxy或者iptables转发到后端的Pods

3.4 External IPs

External IPs 不是一种Service类型,它不由Kubernetes管理,但是我们也可以通过它暴露服务。数据包通过<External IP>:<Port>到达集群,然后被路由到ServiceEndpoints。一个例子如下:

{
"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"
]
}
}

4. 几种特殊的Service

4.1 没有selectorService

上面说KubernetesService抽象了到KubernetesPods的访问。但是它也能抽象到其它类型的后端的访问。举几个场景:

你想接入一个外部的数据库服务;
你想把一个服务指向另外一个Namespace或者集群的服务;
你把部分负载迁移到Kubernetes,而另外一部分后端服务运行在Kubernetes之外。
在这几种情况下,你可以定义一个没有selector的服务。如下:

{
"kind": "Service",
"apiVersion": "v1",
"metadata": {
"name": "my-service"
},
"spec": {
"ports": [
{
"protocol": "TCP",
"port": 80,
"targetPort": 9376
}
]
}
}

因为没有selectorKubernetes不会自己创建Endpoints对象,你需要自己手动创建一个Endpoints对象,把Service映射到后端指定的Endpoints上。

{
"kind": "Endpoints",
"apiVersion": "v1",
"metadata": {
"name": "my-service"
},
"subsets": [
{
"addresses": [
{ "IP": "1.2.3.4" }
],
"ports": [
{ "port": 9376 }
]
}
]
}

注意:Endpoint IP不能是loopback(127.0.0.1)地址、link-local(169.254.0.0/16)link-local multicast(224.0.0.0/24)地址。

看到这里,我们似乎明白了,这不就是Kubernetes提供的外部服务接入的方式吗?和CloudFoundryServiceBroker的功能类似。

4.2 多端口(multi-port)Service

KubernetesService还支持多端口,比如同时暴露80443端口。在这种情况下,你必须为每个端口定义一个名字以示区分。一个例子如下:

{
"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
}
]
}
}

注意:

多端口必须指定ports.name以示区分,端口名称不能一样;
如果是spec.type: NodePort,则每个端口的NodePort必须不一样,否则Kubernetes不知道一个NodePort对应的是后端哪个targetPort
协议protocolport可以一样。

4.3 Headless services

有时候你不想或者不需要Kubernetes为你的服务做负载均衡,以及一个ServiceIP地址。在这种情况下,你可以创建一个headlessService,通过指定spec.clusterIP: None

对这类Service,不会分配ClusterIP。对这类ServiceDNS查询会返回一堆A记录,即后端PodsIP地址。另外,kube-proxy不会处理这类ServiceKubernetes不会对这类Service做负载均衡或者代理。但是Endpoints Controller还是会为此类Service创建Endpoints对象。

这允许开发者减少和kubernetes的耦合性,允许他们自己做服务发现等。我在最后讨论的一些基于Kubernetes的容器服务,除了彻底不使用Service的概念外,也可以创建这类headlessService,自己直接通过LoadBalancer把流量转发(负载均衡和代理)到后端Pods

5. Service的流量转发模式

5.1 Proxy-mode: userspace

userspace的代理模式是指由用户态的kube-proxy转发流量到后端Pods。如下图所示。

关于userspace

Kube-proxy通过apiserver监控(watch)Service和Endpoints的变化;
Kube-proxy安装iptables规则;
Kube-proxy把访问Service的流量转发到后端真正的Pods(Round-Robin)
Kubernetes v1.0只支持这种转发方式;
通过设置service.spec.sessionAffinity: ClientIP支持基于ClientIP的会话亲和性。

5.2 proxy-mode: iptables

iptables的代理模式是指由内核态的iptables转发流量到后端Pods。如下图所示。

关于iptables

  • Kube-proxy通过apiserver监控(watch)ServiceEndpoints的变化;
  • Kube-proxy安装iptables规则;
  • iptables把访问Service的流量转发到后端真正的Pods上(Random)
  • Kubernetes v1.1已支持,但不是默认方式,v1.2中将会是默认方式;
  • 通过设置service.spec.sessionAffinity: ClientIP支持基于ClientIP的会话亲和性;
  • 需要iptables和内核版本的支持。iptables > 1.4.11,内核支持route_localnet参数(kernel >= 3.6)

相比userspace的优点:

  • 1,数据包不需要拷贝到用户态的kube-proxy再做转发,因此效率更高、更可靠。
  • 2,不修改Client IP

5.3 proxy-mode: iptables

kubernetes v1.8 引入, 1.11正式可用

ipvs 模式下,kube-proxy监视Kubernetes服务和端点,调用 netlink接口相应地创建 IPVS 规则, 并定期将 IPVS 规则与 Kubernetes 服务和端点同步。 该控制循环可确保 IPVS 状态与所需状态匹配。 访问服务时,IPVS 将流量定向到后端Pod之一。

IPVS代理模式基于类似于 iptables模式的 netfilter 挂钩函数,但是使用哈希表作为基础数据结构,并且在内核空间中工作。 这意味着,与 iptables 模式下的 kube-proxy 相比,IPVS 模式下的 kube-proxy 重定向通信的延迟要短,并且在同步代理规则时具有更好的性能。与其他代理模式相比,IPVS 模式还支持更高的网络流量吞吐量。

IPVS提供了更多选项来平衡后端Pod的流量。 这些是:

  • rr: round-robin
  • lc: least connection (最小连接数)
  • dh: destination hashing(目的地址has)
  • sh: source hashing(源地址has)
  • 等等

5.3 userspaceiptables转发方式的主要不同点

userspaceiptables转发方式的主要不同点如下:

比较项 userspace iptables

谁转发流量到Pods| kube-proxy把访问Service的流量转发到后端真正的Pods上 |iptables把访问Service的流量转发到后端真正的Pods
|转发算法 |轮询Round-Robin| 随机Random
|用户态和内核态 |数据包需要拷贝到用户态的kube-proxy再做转发,因此效率低、不可靠 |数据包直接在内核态转发,因此效率更高、更可靠
|是否修改Client IP |因为kube-proxy在中间做代理,会修改数据包的Client IP| 不修改数据包的Client IP
iptables版本和内核支持| 不依赖| iptables > 1.4.11,内核支持route_localnet参数(kernel >= 3.6)

通过设置kube-proxy的启动参数--proxy-mode设定使用userspace还是iptables代理模式。

6. Service发现方式

现在服务创建了,得让别人来使用了。别人要使用首先得知道这些服务呀,服务治理很基本的一个功能就是提供服务发现。Kubernetes为我们提供了两种基本的服务发现方式:环境变量和DNS

6.1 环境变量

当一个Pod在节点Node上运行时,kubelet会为每个活动的服务设置一系列的环境变量。它支持Docker links compatible变量,以及更简单的{SVCNAME}_SERVICE_HOST{SVCNAME}_SERVICE_PORT变量。后者是把服务名字大写,然后把中划线(-)转换为下划线(_)。

以服务redis-master为例,它暴露TCP协议的6379端口,被分配了集群IP地址10.0.0.11,则会创建如下环境变量:

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。DNS方式的服务发现就没有此限制。

6.2 DNS

虽然DNS是一个cluster add-on特性,但是我们还是强烈推荐使用DNS作为服务发现的方式。DNS服务器通过KubernetesAPI监控新的Service的生成,然后为每个Service设置一堆DNS记录。如果集群设置了DNS,则该集群中所有的Pods都能够使用DNS解析Sercice

例如,如果在Kubernertes中的my-ns名字空间中有一个服务叫做my-service,则会创建一个my-service.my-nsDNS记录。在同一个名字空间my-nsPods能直接通过服务名my-service查找到该服务。如果是其它的Namespace中的Pods,则需加上名字空间,例如my-service.my-ns。返回的结果是服务的ClusterIP。当然,对于我们上面讲的headlessService,返回的则是该Service对应的一堆后端PodsIP地址。

对于知名服务端口,Kubernetes还支持DNS SRV记录。例如my-service.my-ns的服务支持TCP协议的http端口,则你可以通过一个DNS SRV查询_http._tcp.my-service.my-ns来发现http的端口。

对于每个ServiceDNS记录,Kubernetes还会加上一个集群域名的后缀,作为完全域名(FQDN)。这个集群域名通过svc+安装集群DNSDNS_DOMAIN参数指定,默认是svc.cluster.local。如果不是一个标准的Kubernetes支持的安装,则启动kubelet的时候指定参数--cluster-domain,你还需要指定--cluster-dns告诉kubelet集群DNS的地址。

6.3 如何发现和使用服务?

一般在创建Pod的时候,指定一个环境变量GET_HOSTS_FROM,值可以设为env或者dns。在Pod中的应用先获取这个环境变量,得到获取服务的方式。如果是env,则通过getenv获取相应的服务的环境变量,例如REDIS_SLAVE_SERVICE_HOST;如果是dns,则可以在Pod内通过标准的gethostbyname获取服务主机名。有个例外是Pod的定义中,不能设置hostNetwork: true

获取到服务的地址,就可以通过正常方式使用服务了。

如下是Kubernetes自带的guestbook.php中的一段相关代码,供参考:

$host = 'redis-slave';
if (getenv('GET_HOSTS_FROM') == 'env') {
$host = getenv('REDIS_SLAVE_SERVICE_HOST');
}
$client = new Predis\Client([
'scheme' => 'tcp',
'host' => $host,
'port' => 6379,
]);

7. 一些容器服务中的Service

虽然ServiceKubernetes中如此重要,但是对一些基于Kubernetes的容器服务,并没有使用Service,或者用的是上面讨论的headless类型的Service。这种方式基本上是把容器当做VM使用的典型,LoadBalancerPods网络互通,通过LoadBalancer直接把流量转发到Pods上,省却了中间由kube-proxy或者iptables的转发方式,从而提高了流量转发效率,但是也由LoadBalancer自己提供对后端Pods的维护,一般需要LoadBalancer提供动态路由的功能(即后端Pods可以动态地从LoadBalancer上注册/注销)。

扫码关注公众号 Knative,了解更多云原生 Serverless,Knative 相关信息。

zhaojizhuang wechat