Kubernetes & Service
概述
Service
服务也是Kubernetes
里的核心资源对象之一,Kubernetes
里的每个Service
其实就是我们经常提起的微服务架构中的一个微服务,之前讲解Pod
、RC
等资源对象其实都是为讲解Kubernetes Service
做铺垫的。图显示了Pod、RC与Service的逻辑关系。
从图中可以看到,Kubernetes的Service定义了一个服务的访问入口地址,前端的应用(Pod)通过这个入口地址访问其背后的一组由Pod副本组成的集群实例,Service与其后端Pod副本集群之间则是通过Label Selector
来实现无缝对接的。RC的作用实际上是保证Service
的服务能力和服务质量始终符合预期标准。
通过分析、识别并建模系统中的所有服务为微服务—Kubernetes Service
,我们的系统最终由多个提供不同业务能力而又彼此独立的微服务单元组成的,服务之间通过TCP/IP进行通信,从而形成了强大而又灵活的弹性网格,拥有强大的分布式能力、弹性扩展能力、容错能力,程序架构也变得简单和直观许多。如图所示:
既然每个Pod都会被分配一个单独的IP地址,而且每个Pod都提供了一个独立的Endpoint(Pod IP+ContainerPort)
以被客户端访问,现在多个Pod副本
组成了一个集群
来提供服务,那么客户端如何来访问它们呢?
一般的做法是部署一个负载均衡器(软件或硬件)
,为这组Pod开启一个对外的服务端口如8000端口,并且将这些Pod的Endpoint
列表加入8000端口的转发列表,客户端就可以通过负载均衡器的对外IP地址+服务端口来访问此服务。客户端的请求最后会被转发到哪个Pod,由负载均衡器的算法所决定。
Kubernetes也遵循上述常规做法,运行在每个Node上的kube-proxy
进程其实就是一个智能的软件负载均衡器
,负责把对Service
的请求转发到后端的某个Pod实例
上,并在内部实现服务的负载均衡与会话保持机制。
但Kubernetes发明了一种很巧妙又影响深远的设计:Service
没有共用一个负载均衡器的IP地址,每个Service
都被分配了一个全局唯一的虚拟IP地址,这个虚拟IP被称为Cluster IP
。这样一来,每个服务就变成了具备唯一IP地址的通信节点,服务调用就变成了最基础的TCP网络通信问题。
我们知道,Pod的Endpoint
地址会随着Pod的销毁和重新创建而发生改变,因为新Pod的IP地址与之前旧Pod的不同。而Service一旦被创建,Kubernetes就会自动为它分配一个可用的Cluster IP
,而且在Service的整个生命周期内,它的Cluster IP
不会发生改变。于是,服务发现这个棘手的问题在Kubernetes
的架构里也得以轻松解决:只要用Service
的Name与Service的Cluster IP地址做一个DNS域名映射即可完美解决问题。现在想想,这真是一个很棒的设计。
动手创建一个Service来加深对它的理解。创建一个名为tomcat-service.yaml的定义文件,内容如下:
apiVersion: v1
kind: Service
metadata:
name: tomcat-service
spec:
ports:
- port: 8080
selector:
tier: frontend
创建
roverliang@roverliangdeMac-mini study % kubectl create -f tomcat-service.yaml
service/tomcat-service created
Kubernetes 的服务发现机制
任何分布式系统都会涉及服务发现
这个基础问题,大部分分布式系统都通过提供特定的API接口来实现服务发现功能,但这样做会导致平台的侵入性比较强,也增加了开发、测试的难度。Kubernetes则采用了直观朴素的思路去解决这个棘手的问题。
首先,每个Kubernetes
中的Service
都有唯一的Cluster IP
及唯一的名称
,而名称是由开发者自己定义的,部署时也没必要改变,所以完全可以被固定在配置中。接下来的问题就是如何通过Service的名称
找到对应的Cluster IP
。
最早时Kubernetes采用了Linux环境变量解决这个问题,即每个Service都生成一些对应的Linux环境变量(ENV),并在每个Pod的容器启动时自动注入这些环境变量。
考虑到通过环境变量获取Service地址的方式仍然不太方便、不够直观,后来Kubernetes通过Add-On
增值包引入了DNS系统,把服务名作为DNS域名,这样程序就可以直接使用服务名来建立通信连接了。目前,Kubernetes上的大部分应用都已经采用了DNS这种新兴的服务发现机制,后面会讲解如何部署DNS系统。
外部系统访问Service 的问题
为了更深入地理解和掌握Kubernetes,我们需要弄明白Kubernetes里的3种IP,这3种IP分别如下。
- Node Ip: Node 的IP地址
- Pod IP: Pod 的IP地址
- Cluster IP: Service 的IP地址
首先,Node IP
是Kubernetes集群中每个节点的物理网卡的IP地址,是一个真实存在的物理网络,所有属于这个网络的服务器都能通过这个网络直接通信,不管其中是否有部分节点不属于这个Kubernetes集群。这也表明在Kubernetes集群之外的节点访问Kubernetes集群之内的某个节点或者TCP/IP服务时,都必须通过Node IP
通信。
Pod IP
是每个Pod的IP地址,它是Docker Engine
根据docker0网桥的IP地址段进行分配的,通常是一个虚拟的二层网络,前面说过,Kubernetes要求位于不同Node上的Pod都能够彼此直接通信,所以Kubernetes里一个Pod里的容器访问另外一个Pod里的容器时,就是通过Pod IP
所在的虚拟二层网络进行通信的,而真实的TCP/IP流量是通过Node IP所在的物理网卡流出的。
Cluster IP
,它也是一种虚拟的IP
,但更像一个“伪造”的IP网络,原因有以下几点
- ◎ Cluster IP仅仅作用于
Kubernetes Service
这个对象,并由Kubernetes管理和分配IP地址(来源于Cluster IP地址池)。 - ◎ Cluster IP无法被Ping,因为没有一个“实体网络对象”来响应。
- ◎ Cluster IP只能结合
Service Port
组成一个具体的通信端口,单独的Cluster IP不具备TCP/IP通信的基础,并且它们属于Kubernetes集群这样一个封闭的空间,集群外的节点如果要访问这个通信端口,则需要做一些额外的工作。 - ◎ 在Kubernetes集群内,
Node IP网
、Pod IP网
与Cluster IP网
之间的通信,采用的是Kubernetes自己设计的一种编程方式的特殊路由规则,与我们熟知的IP路由有很大的不同。
根据上面的分析和总结,我们基本明白了:Service的Cluster IP属于Kubernetes集群内部的地址,无法在集群外部直接使用这个地址。那么矛盾来了:实际上在我们开发的业务系统中肯定多少有一部分服务是要提供给Kubernetes集群外部的应用或者用户来使用的,典型的就是web端的服务模块。
NodePort的实现方式是在Kubernetes集群里的每个Node上都为需要外部访问的Service开启一个对应的TCP监听端口,外部系统只要用任意一个Node的IP地址+具体的NodePort端口号即可访问此服务,在任意Node上运行netstat命令,就可以看到有NodePort端口被监听:
但NodePort还没有完全解决外部访问Service的所有问题,比如负载均衡问题。假如在我们的集群中有10个Node,则此时最好有一个负载均衡器,外部的请求只需访问此负载均衡器的IP地址,由负载均衡器负责转发流量到后面某个Node的NodePort上。
图中的Load balancer
组件独立于Kubernetes集群
之外,通常是一个硬件的负载均衡器,或者是以软件方式实现的,例如HAProxy
或者Nginx
。对于每个Service,我们通常需要配置一个对应的Load balancer实例来转发流量到后端的Node上,这的确增加了工作量及出错的概率。于是Kubernetes提供了自动化的解决方案,如果我们的集群运行在谷歌的公有云GCE上,那么只要把Service的type=NodePort
改为type=LoadBalancer
,Kubernetes就会自动创建一个对应的Load balancer实例并返回它的IP地址供外部客户端使用。其他公有云提供商只要实现了支持此特性的驱动,则也可以达到上述目的。此外,裸机上的类似机制(Bare Metal Service Load Balancers)也在被开发。