一、Service
1.1、概述
Service服务也是Kubernetes里的核心资源对象之一,Kubernetes里的每个Service其实就是我们经常提起的微服务架构中的一个微服务,之前 讲解Pod、RC等资源对象其实都是为讲解Kubernetes Service做铺垫的。下图显示了Pod、RC与Service的逻辑关系:
从上图可以看到,Kubernetes的Service定义了一个服务的访问入口地址,前端的应用(Pod)通过这个入口地址访问其背后的一组由 Pod副本组成的集群实例,Service与其后端Pod副本集群之间则是通过 Label Selector来实现无缝对接的。RC的作用实际上是保证Service的服务 能力和服务质量始终符合预期标准。
1.2、Service负载均衡
既然每个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
上述内容定义了一个名为tomcat-service的Service,它的服务端口为 8080,拥有“tier=frontend”这个Label的所有Pod实例都属于它,运行下面 的命令进行创建:
kubectl create -f tomcat-service.yaml
通过下面命令可以查看tomcat service下管理的EndPoints列表(即:PodName+ClusterIp),
其中172.17.1.3是Pod 的IP地址,端口8080是Container暴露的端口;
查看tomcat service被分配的Cluster IP:
kubectl get service
或者
kubectl get svc serviceName -o yaml
在spec.ports的定义中,targetPort属性用来确定提供该服务的容器所暴露(EXPOSE)的端口号,即具体业务进程在容器内的targetPort上提供TCP/IP接入;port属性则定义了Service的虚端口。如果没有指定targetPort,则默认targetPort与port相同。
很多服务都存在多个端口的问题,通常一个端口提供业务服务,另 外一个端口提供管理服务,比如Mycat、Codis等常见中间件。
Kubernetes Service支持多个Endpoint,在存在多个Endpoint的情况下,要 求每个Endpoint都定义一个名称来区分。下面是Tomcat多端口的Service 定义样例:
多端口为什么需要给每个端口都命名呢?这就涉及Kubernetes的服 务发现机制了;
1.3、Kubernetes的服务发现机制
任何分布式系统都会涉及“服务发现”这个基础问题,大部分分布式系统都通过提供特定的API接口来实现服务发现功能,但这样做会导致 平台的侵入性比较强,也增加了开发、测试的难度。Kubernetes则采用 了直观朴素的思路去解决这个棘手的问题。
首先,每个Kubernetes中的Service都有唯一的Cluster IP及唯一的名称,而名称是由开发者自己定义的,部署时也没必要改变,所以完全可 以被固定在配置中。接下来的问题就是如何通过Service的名称找到对应 的Cluster IP。
最早时Kubernetes采用了Linux环境变量解决这个问题,即每个 Service都生成一些对应的Linux环境变量(ENV),并在每个Pod的容器 启动时自动注入这些环境变量。以下是tomcat-service产生的环境变量条目:
在上述环境变量中,比较重要的是前3个环境变量。可以看到,每 个Service的IP地址及端口都有标准的命名规范,遵循这个命名规范,就 可以通过代码访问系统环境变量来得到所需的信息,实现服务调用。
考虑到通过环境变量获取Service地址的方式仍然不太方便、不够直观,后来Kubernetes通过Add-On增值包引入了DNS系统,把服务名作为 DNS域名,这样程序就可以直接使用服务名来建立通信连接了。目前, Kubernetes上的大部分应用都已经采用了DNS这种新兴的服务发现机 制,后面会讲解如何部署DNS系统。
1.4、外部系统如何访问Service
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所在的物理网卡流出的。
最后说说Service的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路由有很大的不同。
那么外部用户如何访问Kubernetes的服务呢?
最直接的办法是通过NodePort,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)也在被开发。
二、Volume
Volume(存储卷)是Pod中能够被多个容器访问的共享目录。 Kubernetes的Volume概念、用途和目的与Docker的Volume比较类似,但两者不能等价。
- Kubernetes中的Volume被定义在Pod上,然后被一个Pod里的多个容器挂载到具体的文件目录下;
- Kubernetes中的 Volume与Pod的生命周期相同,但与容器的生命周期不相关,当容器终止或者重启时,Volume中的数据也不会丢失。
- Kubernetes支持多 种类型的Volume,例如GlusterFS、Ceph等先进的分布式文件系统。
Kubernetes提供了非常丰富的Volume类型:
2.1、emptyDir
一个emptyDir Volume是在Pod分配到Node时创建的。从它的名称就 可以看出,它的初始内容为空,并且无须指定宿主机上对应的目录文 件,因为这是Kubernetes自动分配的一个目录,当Pod从Node上移除时,emptyDir中的数据也会被永久删除。emptyDir的一些用途如下:
- 临时空间,例如用于某些应用程序运行时所需的临时目录,且无须永久保留。
- 长时间任务的中间过程CheckPoint的临时保存目录。
- 一个容器需要从另一个容器中获取数据的目录(多容器共享目录)。
目前,用户无法控制emptyDir使用的介质种类。如果kubelet的配置 是使用硬盘,那么所有emptyDir都将被创建在该硬盘上。Pod在将来可 以设置emptyDir是位于硬盘、固态硬盘上还是基于内存的tmpfs上,上面 的例子便采用了emptyDir类的Volume
2.2、hostPath
hostPath为在Pod上挂载宿主机上的文件或目录,它通常可以用于以下几方面:
- 容器应用程序生成的日志文件需要永久保存时,可以使用宿主机的高速文件系统进行存储。
- 需要访问宿主机上Docker引擎内部数据结构的容器应用时,可以通过定义hostPath为宿主机/var/lib/docker目录,使容器内部应用可以直接访问Docker的文件系统。
在使用这种类型的Volume时,需要注意以下几点:
- 在不同的Node上具有相同配置的Pod,可能会因为宿主机上的目录和文件不同而导致对Volume上目录和文件的访问结果不一致。
- 如果使用了资源配额管理,则Kubernetes无法将hostPath在宿主机上使用的资源纳入管理。
2.3、gcePersistentDisk
使用这种类型的Volume表示使用谷歌公有云提供的永久磁盘 (Persistent Disk,PD)存放V olume的数据,它与emptyDir不同,PD上 的内容会被永久保存,当Pod被删除时,PD只是被卸载(Unmount), 但不会被删除。需要注意的是,你需要先创建一个PD,才能使用 gcePersistentDisk。
2.4、awsElasticBlockStore
与GCE类似,该类型的V olume使用亚马逊公有云提供的EBS Volume存储数据,需要先创建一个EBS Volume才能使用 awsElasticBlockStore。
2.5、NFS
使用NFS网络文件系统提供的共享目录存储数据时,我们需要在系统中部署一个NFS Server。
2.6、其他类型的Volume
- iscsi:使用iSCSI存储设备上的目录挂载到Pod中。
- flocker:使用Flocker管理存储卷。
- glusterfs:使用开源GlusterFS网络文件系统的目录挂载到Pod 中。
- rbd:使用Ceph块设备共享存储(Rados Block Device)挂载到 Pod中。
- gitRepo:通过挂载一个空目录,并从Git库clone一个git repository以供Pod使用。
- secret:一个Secret Volume用于为Pod提供加密的信息,你可以 将定义在Kubernetes中的Secret直接挂载为文件让Pod访问。Secret Volume是通过TMFS(内存文件系统)实现的,这种类型的V olume总是 不会被持久化的。
三、Persistent Volume
前面提到的Volume是被定义在Pod上的,属于计算资源的一部分, 而实际上,网络存储是相对独立于计算资源而存在的一种实体资源。比如在使用虚拟机的情况下,我们通常会先定义一个网络存储,然后从中划出一个“网盘”并挂接到虚拟机上。Persistent V olume(PV)和与之相 关联的Persistent Volume Claim(PVC)也起到了类似的作用。
PV可以被理解成Kubernetes集群中的某个网络存储对应的一块存 储,它与Volume类似,但有以下区别:
- PV只能是网络存储,不属于任何Node,但可以在每个Node上 访问。
- PV并不是被定义在Pod上的,而是独立于Pod之外定义的。
- PV目前支持的类型包括:gcePersistentDisk、 AWSElasticBlockStore、AzureFile、AzureDisk、FC(Fibre Channel)、 Flocker、NFS、iSCSI、RBD(Rados Block Device)、CephFS、 Cinder、GlusterFS、VsphereV olume、Quobyte V olumes、VMware Photon、Portworx V olumes、ScaleIO V olumes和HostPath(仅供单机测 试)。
四、Namespace
Namespace(命名空间)是Kubernetes系统中的另一个非常重要的概 念,Namespace在很多情况下用于实现多租户的资源隔离。Namespace 通过将集群内部的资源对象“分配”到不同的Namespace中,形成逻辑上分组的不同项目、小组或用户组,便于不同的分组在共享使用整个集群 的资源的同时还能被分别管理。
Kubernetes集群在启动后会创建一个名为default的Namespace,通过 kubectl可以查看:
kubectl get namespaces
如果不特别指明Namespace,则用户创建的Pod、RC、 Service都将被系统创建到这个默认的名为default的Namespace中。
4.1、创建namespace
namespace.yaml如下
apiVersion: v1
kind: Namespace
metadata:
name: test-namespace
创建:
kubectl create -f namespace.yaml
4.2、使用namespace
deployment.yaml如下:
apiVersion: v1
kind: Deployment
metadata:
name: test-deployment
namespace: test-namespace
...
查看指定namespace的pod:
kubectl get pod -n test-namespace
当给每个租户创建一个Namespace来实现多租户的资源隔离时,还 能结合Kubernetes的资源配额管理,限定不同租户能占用的资源,例如 CPU使用量、内存使用量等。
五、Annotation
Annotation(注解)与Label类似,也使用key/value键值对的形式进 行定义。不同的是Label具有严格的命名规则,它定义的是Kubernetes对象的元数据(Metadata),并且用于Label Selector。Annotation则是用户任意定义的附加信息,以便于外部工具查找。 在很多时候,Kubernetes 的模块自身会通过Annotation标记资源对象的一些特殊信息。
通常来说,用Annotation来记录的信息如下:
- build信息、release信息、Docker镜像信息等,例如时间戳、release id号、PR号、镜像Hash值、Docker Registry地址等。
- 日志库、监控库、分析库等资源库的地址信息。
- 程序调试工具信息,例如工具名称、版本号等。
- 团队的联系信息,例如电话号码、负责人名称、网址等。
六、ConfigMap
6.1、描述
为了能够准确和深刻理解Kubernetes ConfigMap的功能和价值,我 们需要从Docker说起。我们知道,Docker通过将程序、依赖库、数据及 配置文件“打包固化”到一个不变的镜像文件中的做法,解决了应用的部 署的难题,但这同时带来了棘手的问题,即配置文件中的参数在运行期 如何修改的问题。我们不可能在启动Docker容器后再修改容器里的配置 文件,然后用新的配置文件重启容器里的用户主进程。为了解决这个问 题,Docker提供了两种方式:
- 在运行时通过容器的环境变量来传递参数;
- 通过Docker Volume将容器外的配置文件映射到容器内。
这两种方式都有其优势和缺点,在大多数情况下,后一种方式更合 适我们的系统,因为大多数应用通常从一个或多个配置文件中读取参 数。但这种方式也有明显的缺陷:我们必须在目标主机上先创建好对应 的配置文件,然后才能映射到容器里。
针对上述问题, Kubernetes给出了一个很巧妙的设计实现,如下所述。
- 把所有的配置项都当作key-value字符串,当然value可以来自某个文本文件,比如配置项password=123456、user=root、 host=192.168.8.4用于表示连接FTP服务器的配置参数。这些配置项可以 作为Map表中的一个项,整个Map的数据可以被持久化存储在 Kubernetes的Etcd数据库中,然后提供API以方便Kubernetes相关组件或 客户应用CRUD操作这些数据,上述专门用来保存配置参数的Map就是 Kubernetes ConfigMap资源对象。
- Kubernetes提供了一种内建机制,将存储在etcd中的 ConfigMap通过V olume映射的方式变成目标Pod内的配置文件,不管目 标Pod被调度到哪台服务器上,都会完成自动映射。进一步地,如果 ConfigMap中的key-value数据被修改,则映射到Pod中的“配置文件”也会 随之自动更新。于是,Kubernetes ConfigMap就成了分布式系统中最为 简单(使用方法简单,但背后实现比较复杂)且对应用无侵入的配置中 心。ConfigMap配置集中化的一种简单方案如下图:
6.2、使用ConfigMap的限制条件
- ConfigMap必须在pod之前创建
- ConfigMap受Namespace限制,只有处于相同Namespace中的Pod才可以引用它
- ConfigMap中的配额管理还未能实现
- kubelet只支持可以被API Server管理的Pod使用ConfigMap。kubelet在本Node上通过—maniifest-url或—config自动创建的静态Pod将无法引用ConfigMap
- 在Pod对ConfigMap进行挂载操作时,容器内部只能挂载为“目录”,无法挂载为“文件”。在挂载到容器内部后,目录中将包含ConfigMap定义的每个item,如果该目录下原来还有其他文件,则容器内的该目录将会被挂载的ConfigMap覆盖。如果应用程序需要保留原来的其他文件,则需要进行额外的处理。可以将ConfigMap挂载到容器内部的临时目录,再通过启动脚本将配置文件赋值或者链接到应用所用的实际配置目录下
参考
《Kubernetes权威指南: 第四版》