Kubernetes常用资源对象-Service

1、为什么需要Service?

之前我们详细的讲解了 Pod 的基本用法,也了解到 Pod 的声明是有限的,死亡过后不会复活了。在后⾯学习到的 RC 和 Deployment 可以动态的创建和销毁 Pod 。尽管每个 Pod 都有自己的 IP 地址,但是如果 Pod 重新启动了的话 IP 很有可能也就变化了。这就会带来⼀个问题:比如有⼀些后端的 Pod 集合为集群中的其他前端的 Pod 集合提供 API 服务,如果我们在前端的 Pod 中把所有的这些后端的 Pod 地址都写死,然后通过某种方式去访问其中⼀个 Pod 的服务,这样是不是可以正常工作呢?但是如果这个 Pod 挂掉了,然后重新启动起来了,是不是 IP 地址有可能就变化了,这个时候前端就访问不到后端的服务了。

遇到这样的问题该怎么办呢?在没有使用 Kubernetes 之前,我们可能都遇到过这样的问题,但不⼀定是 IP 变化的问题。比如我们在部署⼀个 WEB 服务的时候,前端⼀般部署⼀个 Nginx 作为服务的入口,然后 Nginx 后⾯肯定就是挂载的这个服务的大量后端,以前我们可能是手动去更改 Nginx 配置中的 upstream 选项,来动态改变提供服务的数量,后来出现了⼀些服务发现的⼯具,比如 ConsulZooKeeper 还有 Etcd 等工具,有了这些⼯具过后我们就可以只需要把服务注册到这些服务发现中心去就可以,然后让这些工具动态的更新 Nginx 的配置就可以了,完全不用手工操作了,是不是非常方便呢。

img

同样,要解决上⾯遇到的问题是不是实现⼀个服务发现的工具也可以解决?没错,当 Pod 被销毁或者新建过后,我们可以把这个 Pod 的地址注册到服务发现中心就可以,但是这样一来前端的 Pod 结合就不能直接去连接后台的 Pod 集合了,就可以连接到⼀个能够做服务发现的中间件上面。

没错, Kubernetes 集群就为我们提供了这样的⼀个资源对象 – Service , Service 是⼀种抽象的对象,它定义了⼀组 Pod 的逻辑集合和⼀个用于访问它们的策略,其实这个概念和微服务非常类似。⼀个 Serivce 下面包含的 Pod 集合⼀般是由 Label Selector 来决定的。

假如后端运行了3个副本,这些副本都是可以替代的,因为前端并不关心它们使用的是哪⼀个后端服务。尽管由于各种原因后端的 Pod 集合会发生变化,但是前端却不需要知道这些变化,也不需要自己用⼀个列表来记录这些后端的服务, Service 的这种抽象就可以帮我们达到解耦的目的。

2、Kubernetes的三种IP

在开始学习 Service 之前,我们需要先弄明白 Kubernetes 系统中的三种IP:

  • Node IP: Node 节点的 IP 地址;
  • Pod IP: Pod 的IP地址;
  • Cluster IP: Service 的 IP 地址。

1、Node IP 是 Kubernetes 集群中节点的物理网卡 IP 地址(⼀般为内网),所有属于这个网络的服务器之间都可以直接通信,所以 Kubernetes 集群外要想访问 Kubernetes 集群内部的某个节点或者服务,必须要通过 Node IP 进⾏通信(这个时候⼀般是通过外网 IP 了)。

2、Pod IP 是每个 Pod 的 IP 地址,它是 Docker Engine 根据 docker0 网桥的 IP 地址段进行分配的(我们这里使用的是 flannel 这种网络插件保证所有节点的 Pod IP 不会冲突)。

3、Cluster IP 是⼀个虚拟的 IP ,仅仅作用于 Kubernetes Service 这个对象,由 Kubernetes 自己来进行管理和分配地址,当然我们也无法 ping 这个地址,他没有⼀个真正的实体对象来响应,他只能结合 Service Port 来组成⼀个可以通信的服务。

3、定义Service

定义 Service 的方式和我们前⾯定义的各种资源对象的方式类似。假定我们有⼀组 Pod 服务,它们对外暴露了 8080 端口,同时都被打上了 app=myapp 这样的标签,那么我们就可以像下面这样来定义⼀个 Service 对象:

---
apiVersion: v1
kind: Service
metadata:
  name: myservice
spec:
  selector:
    app: myapp
  ports:
  - protocol: TCP
    port: 80
    targetPort: 8080
    name: myapp-http

然后通过 kubectl create -f myservice.yaml 命令就可以创建⼀个名为 myservice 的 Service 对象,它会将请求代理到使用 TCP 8080 端口且具有标签 app=myapp 的 Pod 上,这个 Service 会被系统分配⼀个我们上面说的 Cluster IP ,该 Service 还会持续的监听 selector 下⾯的 Pod ,会把这些 Pod 信息更新到⼀个名为 myservice 的 Endpoints 对象上去,这个对象就类似于上⾯的 Pod 集合了。

img

需要注意的是, Service 能够将⼀个接收端口映射到任意的 targetPort 。 默认情况下, targetPort 将被设置为与 port 字段相同的值。 可能更有趣的是,targetPort 可以是⼀个字符串,引用了 backend Pod 的⼀个端口的名称。 因实际指派给该端口名称的端口号,在每个 backendPod 中可能并不相同,所以对于部署和设计 Service ,这种方式会提供更大的灵活性。

另外 Service 能够⽀持 TCP 和 UDP 协议,默认是 TCP 协议。

4、Kube-Proxy

在 Kubernetes 集群中,每个 Node 会行⼀个 kube-proxy 进程, 负责为 Service 实现⼀种 VIP(虚拟 IP,就是我们上面说的 ClusterIP )的代理形式,现在的 Kubernetes 中默认是使用 iptables 这种模式来代理。这种模式, kube-proxy 会监视 Kubernetes master 对 Service 对象和 Endpoints 对象的添加和移除。 对每个 Service,它会添加上iptables 规则,从而捕获到达该 Service 的 ClusterIP(虚拟 IP)和端⼝的请求,进而将请求重定向到Service 的⼀组 backend 中的某⼀个个上⾯。 对于每个 Endpoints 对象,它也会安装 iptables 规则,这个规则会选择⼀个 backend Pod。

默认的策略是,随机选择⼀个 backend。 我们也可以实现基于客户端 IP 的会话亲和性,可以将service.spec.sessionAffinity 的值设置为 “ClientIP” (默认值为 “None”)。另外如果最开始选择的 Pod 没有响应,iptables 代理能够自动重试另⼀个 Pod,所以它需要依赖 readiness probes。

img

5、Service类型

我们在定义 Service 的时候可以指定⼀个自己需要的Service类型 ,如果不指定的话默认是 ClusterIP 类型。我们可以使用的服务类型如下:

  • ClusterIP:通过集群的内部 IP 暴露服务,选择该值,服务只能够在集群内部可以访问,这也是默认的ServiceType。
  • NodePort:通过每个 Node 节点上的 IP 和静态端口(NodePort)暴露服务。NodePort 服务会路由到 ClusterIP 服务,这个 ClusterIP 服务会自动创建。请求可以从集群的外部访问NodePort 服务。
  • LoadBalancer:使⽤云提供商的负载局衡器,可以向外部暴露服务。外部的负载均衡器可以路由到 NodePort 服务和 ClusterIP 服务,这个需要结合具体的云厂商进⾏操作。
  • ExternalName:通过返回 CNAME 和它的值,可以将服务映射到 externalName 字段的内容(例如, foo.bar.example.com)。没有任何类型代理被创建,这只有 Kubernetes 1.7 或更⾼版本的kube-dns 才⽀持。

6、NodePort类型

如果设置 type 的值为 “NodePort”,Kubernetes master 将从给定的配置范围内(默认:30000-32767)分配端口,每个 Node 将从该端口(每个 Node 上的同⼀端口)代理到 Service。该端口将通过 Service 的 spec.ports[*].nodePort 字段被指定,如果不指定的话会自动生成⼀个端口。需要注意的是,Service 将能够通过nodePort 和 clusterIp 而对外可见。

---
apiVersion: v1
kind: Service
metadata:
  name: myservice
spec:
  selector:
    app: nginx
  type: NodePort
  ports:
  - name: mynginx-http
    protocol: TCP
    port: 80
    targetPort: 80

这里我们可以通过 kubectl apply -f sevice-demo.yaml 命令创建Service,创建完成以后通过 kubectl get svc 查看服务状态;当然我们还可以通过 kubectl describe service myservice 命令查看服务的详细情况;这里我们可以看到、该Service已经绑定了Node01节点的NodePort 31914,Endpoints也已经自动添加为3个:

img

这里为什么会自动添加3个Endpoints呢?通过 kubectl get pods 命令我们可以看到、这里有3个Pod正在运行;而通过 kubectl describe pod nginx-deploy-55697f9b8b-s7jdb 命令我们可以看到、第7行有一个Labels: app=nginx;和我们上面定义的 .spec.selector.app: nginx 名称一致,这就如同我们上面讲的一样: kube-proxy 会监视 Kubernetes master 对 Service 对象和 Endpoints 对象的添加和移除。

img

img

查看Node01节点的端口监听状态、我们可以看到31914端口已经正常监听、我们可以直接通过 http://172.16.200.2:31914 访问Nginx页面,发现已经正常返回:

img

img

7、ExternalName

ExternalName 是 Service 的特例,它没有 selector,也没有定义任何的端口和 Endpoint。 对于运行在集群外部的服务,它通过返回该外部服务的别名这种方式来提供服务。

kind: Service
apiVersion: v1
metadata:
  name: my-service
  namespace: prod
spec:
  type: ExternalName
  externalName: my.database.example.com

当查询主机 my-service.prod.svc.cluster.local 时,集群的DNS 服务将返回⼀个值为 my.database.example.com 的 CNAME 记录。 访问这个服务的⼯作方式与其它的相同,唯⼀不同的是重定向发生在 DNS 层,而且不会进行代理或转发。 如果后续决定要将数据库迁移到 Kubernetes 集群中,可以启动对应的 Pod,增加合适的 Selector 或 Endpoint,修改Service 的 type,完全不需要修改调用的代码,这样就完全解耦了。

推荐文章