在k8s中的Service简介

本文最后更新于:July 23, 2019 pm

简单介绍一下k8s中的Service组件。

1、Service简介

老规矩,我们先来看一下官网对service的描述

Kubernetes Pod 是有生命周期的,它们可以被创建,也可以被销毁,然而一旦被销毁生命就永远结束。 通过 ReplicaSets 能够动态地创建和销毁 Pod(例如,需要进行扩缩容,或者执行 滚动升级)。 每个 Pod 都会获取它自己的 IP 地址,即使这些 IP 地址不总是稳定可依赖的。 这会导致一个问题:在 Kubernetes 集群中,如果一组 Pod(称为 backend)为其它 Pod (称为 frontend)提供服务,那么那些 frontend 该如何发现,并连接到这组 Pod 中的哪些 backend 呢?

关于 Service

Kubernetes Service 定义了这样一种抽象:逻辑上的一组 Pod,一种可以访问它们的策略 —— 通常称为微服务。 这一组 Pod 能够被 Service 访问到,通常是通过 Label Selector(查看下面了解,为什么可能需要没有 selector 的 Service)实现的。

举个例子,考虑一个图片处理 backend,它运行了3个副本。这些副本是可互换的 —— frontend 不需要关心它们调用了哪个 backend 副本。 然而组成这一组 backend 程序的 Pod 实际上可能会发生变化,frontend 客户端不应该也没必要知道,而且也不需要跟踪这一组 backend 的状态。 Service 定义的抽象能够解耦这种关联。

对 Kubernetes 集群中的应用,Kubernetes 提供了简单的 Endpoints API,只要 Service 中的一组 Pod 发生变更,应用程序就会被更新。 对非 Kubernetes 集群中的应用,Kubernetes 提供了基于 VIP 的网桥的方式访问 Service,再由 Service 重定向到 backend Pod

官网的这段话可能有些不好理解,我们用最简单的web服务器软件apache举个例子。一般来说,我们在一台电脑上安装了apache服务之后,它默认监听的是80端口(http),也就是说我们如果需要访问这个apache服务,我们只需要使用浏览器访问这台电脑的IP地址加上80端口号即可,对于其他的应用也是一样,一般都是以主机IP加上应用的监听端口来进行访问。

那么在k8s的集群中,我们都知道应用服务被打包成容器,而容器放在pod中运行,每个pod都会被分配到一个IP地址,当然我们还是可以按照上述的方式来访问这些pod里面的服务,但是,一个应用往往会部署多个pod(保证可靠性和健壮性),而不同的pod也会因为需求的不同或者节点宕机等原因被销毁或者新建(对应又是新的IP),所以k8s定义了service这一层抽象层,用来解决这个问题,将同一应用下的一组pod进行打包,然后通过service来进行统一访问(主要通过label来实现),用户只需要访问service这一层,而不需要在意service之后的pod是怎么调度运行的。也就是说,service会负责把用户的访问请求转发到pod对应的端口上,或者是说service会将pod对应的端口和service上暴露给用户访问的端口建立映射关系。

2、使用service

接下来我们来尝试着使用一下service。

首先我们创建一个pod,对应的配置文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
apiVersion: apps/v1beta1
kind: Deployment
metadata:
name: http-deployment
spec:
replicas: 3
template:
metadata:
labels:
app: httpd
spec:
containers:
- image: httpd
name: http
ports:
- containerPort: 80
protocol: TCP

需要特别注意的是这里我们使用了一个labels,使用这个配置文件创建的pod都会带上一个app=httpdlabels,这个是后面创建的service用来区分pod的。

这里我们使用curl直接对部署的三个pod进行访问,可以看到都是能够访问成功的。

接下来我们创建一个service,配置文件如下

1
2
3
4
5
6
7
8
9
10
11
kind: Service
apiVersion: v1
metadata:
name: http-service
spec:
selector:
app: httpd
ports:
- protocol: TCP
port: 7777
targetPort: 80

上述配置将创建一个名称为 “http-service” 的 Service 对象,它会将请求代理到使用 TCP 端口 80,并且具有标签(labels) "app=httpd"Pod 上。 这个 Service 将被指派一个 IP 地址(通常称为 “Cluster IP”),它会被服务的代理使用(见下面)。 该 Service 的 selector 将会持续评估,处理结果将被 POST 到一个名称为 “http-service”Endpoints 对象上。

需要注意的是, Service 能够将一个接收端口映射到任意的 targetPort。 默认情况下,targetPort 将被设置为与 port 字段相同的值。

创建成功之后我们可以使用下面的这个命令来查看service

1
kubectl get service -o wide

这里我们可以看到这个service被分配了一个10.96.16.218的IP地址(这个IP地址称为ClusterIP),服务对应的端口号是7777。我们试着使用curl来对其进行访问。

这里可以看到是可以成功访问的。

我们再来看一下详细的情况:

这里可以看到已经和上面的三个pods的80端口建立了映射关系。

3、ipvs和iptables

我们现在已经知道,service这个抽象层是将这个service的IP(10.96.16.218)的7777端口和Endpoints的pod的80端口建立映射关系来实现这个抽象的。那么这个映射关系的建立,就是由iptables来实现的,而ipvs在这里也能实现一样的功能,但是更加强大,我们可以将它简单看作是iptables的增强版。

3.1 ipvs和iptables的区别

k8s中默认使用的是iptables,但是ipvs要更加强大,

  • ipvs 为大型集群提供了更好的可扩展性和性能
  • ipvs 支持比 iptables 更复杂的复制均衡算法(最小负载、最少连接、加权等等)
  • ipvs 支持服务器健康检查和连接重试等功能

ipvs (IP Virtual Server) 实现了传输层负载均衡,也就是我们常说的4层LAN交换(基于TCP四层(IP+端口)的负载均衡软件),作为 Linux 内核的一部分。ipvs运行在主机上,在真实服务器集群前充当负载均衡器。ipvs可以将基于TCPUDP的服务请求转发到真实服务器上,并使真实服务器的服务在单个 IP 地址上显示为虚拟服务。

ipvs是在kubernetes v1.8版本中引进的,小七这里使用的是v1.15版本,之前配置的时候就是使用了ipvs来替换iptables

3.2 查看ipvs

不管是ipvs还是iptables,都是需要root权限才能查看,因此我们需要在命令前加上sudo或者是切换到root用户。

1
sudo ipvsadm -ln

从这里的规则中我们可以看到。ipvs将对10.96.16.218:7777的访问转发到下面对应的三个IP地址的80端口上。

接下来我们对这个deployment进行修改,将副本数量从3改到6,再看这个ipvs规则的变化

副本数量增加到了6个,对应的ipvs规则也完成了更新:

接下来我们删除掉这个service,可以看到ipvs中的规则并没有进行删除

这里我们可以知道,k8s对ipvs的规则可以说是只增不减。

这里留一个坑,后面再开一篇来详细研究一下ipvs

3.3 使用DNS访问

实际上,k8s集群中内置有dns解析服务,我们可以通过<SERVICE_NAME>.<NAMESPACE_NAME>来对service进行访问。

v1.11版本之后,coredns取 代kube-dns成为k8s中默认的dns解析服务器。

我们在pod中运行一个shell来实验一下:

1
kubectl run busybox --rm -ti --image=busybox /bin/sh

busyboxnslookup的支持似乎有些问题,不一定能成功查询到dns,而且并没有内置curl命令,我们只能用wget来进行访问。事实上wget能成功就说明dns服务是正常的。

然后我们使用它的测试版本,这时候nslookup命令正常了,wget命令又不正常了。

1
kubectl run dig --rm -it --image=docker.io/azukiapp/dig /bin/sh

这里面的10.96.0.10地址正是我们的coredns的地址:

4、外网访问service

4.1 NodePort简介

前面我们已经可以通过clusterIP来访问service了,但是显然这只能局限于在k8s集群内的主机进行,集群外的机器无法对其进行访问,这显然是不行的。我们先看一下官网给出的解决方案:

对一些应用(如 Frontend)的某些部分,可能希望通过外部(Kubernetes 集群外部)IP 地址暴露 Service。

Kubernetes ServiceTypes 允许指定一个需要的类型的 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)。 没有任何类型代理被创建,这只有 Kubernetes 1.7 或更高版本的 kube-dns 才支持。

可以看到,这里我们主要将用到的就是NodePort了。

如果设置 type 的值为 "NodePort",Kubernetes master 将从给定的配置范围内(默认:30000-32767)分配端口,每个 Node 将从该端口(每个 Node 上的同一端口)代理到 Service。该端口将通过 Servicespec.ports[*].nodePort字段被指定。

如果需要指定的端口号,可以配置 nodePort 的值,系统将分配这个端口,否则调用 API 将会失败(比如,需要关心端口冲突的可能性)。

这可以让开发人员自由地安装他们自己的负载均衡器,并配置 Kubernetes 不能完全支持的环境参数,或者直接暴露一个或多个 Node 的 IP 地址。

需要注意的是,Service 将能够通过 <NodeIP>:spec.ports[*].nodePortspec.clusterIp:spec.ports[*].port 而对外可见。

4.2 自动分配端口

接下来我们来修改一下刚刚的service配置文件,先尝试一下让master自动分配一个端口。

然后我们重新应用一下该配置文件,就能看到分配的端口:

我们查看ipvs规则,可以看到本机对应的IP都已经建立了相应的映射转发规则

正好小七用的是虚拟机,使用集群外的电脑用浏览器访问任意一个master节点均可:

4.3 手动指定端口

我们在下面的port选项中添加nodePort: 32333,然后重新应用配置文件。

使用浏览器尝试访问: