一、问题引出
业务系统容器化迁移、k8s部署的过程中,我们发现有很多服务都存在需要数据存储的需求。当应系统用采用传统的物理部署方式时,应用可将数据直接存储在本地,读取数据时只需到对应的储存目录下读取即可;但是,当服务采用容器化部署时,由于容器本地存储空间的生命周期与容器绑定,应用存储在容器中的数据会随容器的删除而一同被删除,应用数据也就失去了持久性。所以此类应用在迁移k8s部署时必须借助一种可以实现脱离于容器生命周期束缚的持久化存储介质,故此,类似于NFS、GlusterFS、Ceph等具备网络传输能力的存储服务在这里就可以大展身手。
我们在容器化工作开展的前期就是采用NFS作为k8s的共享存储服务,但随着工作的不断展开,我们发现由于NFS的集中式架构(单体架构),导致其与k8s集成在可用性方面存在重大缺陷,比如当NFS服务不可用时,依赖于存储的业务系统pod会直接进入pending状态,进而导致业务系统也一同失去了提供服务的能力【从k8s设计角度来讲,存储服务失效导致业务系统不可用是一种明智的策略,因为它保证了数据安全性(不可用总比数据丢失强),但是从业务系统的角度去看,这无疑是个尴尬的局面(甚至有点反人类的感觉),因为存储服务原本应该独立于业务系统存在的,但现在它却“绑架”了我们的业务系统,这是完全不能接受的】,虽然我们尝试了使用NFS的master&slave机制去保证它的可用性,但它主备切换的效率实在让我们不敢恭维。再者,让我们回想一下刚才说过的话,“NFS是一种集中式的架构”,没错!从原则上来讲,单体架构应用的扩展性一直都是一个头疼的问题,随着业务量的增长,数据量不断增大,NFS迟早会被“撑爆”,相信到那时肯定就不是硬盘扩容那么简单了。所以,我们必须尝试其他存储方案—GlusterFS or Ceph。这里我们将选择GlusterFS 6.5作为本文讨论的核心(GlusterFS是一种具备完全去中心化、超高可用性和超强横向扩展能力的分布式网络存储服务,您可以在我的另一篇文章《GlusterFS使用手册》中获得跟更多系统介绍和使用说明),并重点通过实操来展示它和Heketi与k8s的集成流程。通过对它的使用,我们将可以完美地解决NFS在单点故障、扩展性以及单点性能瓶颈等方面的问题。下面就让我们一起来探讨它和k8s集成实现共享存储的具体实施方案吧。
二、方案介绍
Kubernetes提供了两种使用GlusterFS作为共享存储的集成方式。第一种方式为kubernetes直接挂在GlusterFS存储卷:当我们在声明pod资源时,我们需要在po.spec.volumes的子字段中选择glusterfs作为存储服务类型。值得注意的是,当我们通过这种方式使用GlusterFS时,我们需要在Kubernetes中事先创建一个描述GlusterFS服务的endpoints资源。相对而言,这是一种非常简单的集成方式。遗憾的是,这种简单的集成方式并不会让你在今后的存储卷管理上同样省时间。首先,既然我们手动创建了GlusterFS卷,我们就要为它的“生老病死”负责,包括当卷空间不足时,我们需要通过手动扩展卷空间来对存储空间进行扩容,这就意味着用户必须非常了解GlusterFS常用的使用方法和常见问题的排查,这听起来似乎有点挑战,但用户必须接受这个事实。
那么,让我们来说一说第二种方式吧。有点遗憾,我的第二种方式必须建立在第一种方式的基础上使用。我是这么想的:GlusterFS采用物理的部署方式,每个服务节点管理若干硬盘设备,一部分为手动格式化的XFS文件系统盘,供方式一使用,一部分为未格式化的裸盘,供Heketi使用;Heketi服务采用容器化部署方式,运行为kubernetes集群中的pod实例,并使用第一种方式的GlusterFS卷为Heketi的数据和配置提供存储服务,最后再将Heketi服务声明为kebernetes中的storageclass资源,供pod资源申请PVC使用。这样当我们在需要使用存储空间时,只需要在pod清单中申明PVC资源和指定的storageclass资源即可,Heketi就会自动帮我们格式化相应的硬盘存储空间,并将其创建为PV资源与我们申请的PVC进行绑定,这中间所有的操作均由Heketi自动帮我们完成,我们只要关注存储空间的大小及使用权限即可,其他的我们一概不问,包括存储空间的扩容等等。这里通过Heketi创建出来的存储卷和PV是一一对应的,而PV和我们的PVC又是一一对应的,所以如果一个pod的PVC只用于为自己提供储存服务,那么可以认为底层创建出来的GlusterFS卷就是一个私有的储存空间,这在希望隔离的环境下是很有意义的,但同时也会因此产生一定的开销。在这里,Heketi的镜像由我们自己手动创建(Dockerfile见文档附录),GlusterFS集群也需要我们手动搭建(详见《GlusterFS使用手册》)。我得说明一下,这和Heketi官方提供的方式可能存在较大差异,官方提供的方式是将Heketi和GlusterFS均托管到kuberbetes集群中,GlusterFS以deamonSet的方式运行。但是我认为这种全封装、全自动的方式在对GlusterFS存储服务的管理和问题的排查上存在诸多不便,比如当我们需要对某些卷或者GlusterFS本身做出一些特定的配置时,我们还需要进入到GlusterFS容器中进行相应的操作;再比如当我们kubernetes集群外部的某些服务也想使用GlusterFS存储服务时,我们很难顺利地访问集群内部GlusterFS所在容器的IP和端口(除非做静态路由和TCP端口映射);并且对于大多数第一次接触GlusterFS的新手来说,我们不太推荐使用一些高度封装的技术来屏蔽一些重要的底层细节,因为这对后期问题的定位和排查相当不利。那么接下来,让我们对上述的两种集成方式进行一一落实吧。
三、方案实施
1、K8S挂载GlusterFS存储卷
(1)安装GlusterFS集群,集群IP分别为192.168.3.110、192.168.3.111、192.168.3.112…(建 议为6个节点),手动格式化硬盘为XFS格式,导出brick(建议以整块硬盘为单位), 并创建GlusterFS分布式卷gs1,副本数设为3;
(2)在K8S集群的每一个节点上安装glusterfs、glusterfs-fuse客户端服务;
(3)在k8s中创建endpoints资源gluster-endpoint.yaml,用于提供GlusterFS集群节点 的访问入口,内容如下:
apiVersion: v1
kind: Endpoints
metadata:
name: gluster-endpoints
subsets:
- addresses:
- ip: 192.168.3.110
ports:
- port: 24007
name: glusterd
- addresses:
- ip: 192.168.3.111
ports:
- port: 24007
name: glusterd
- addresses:
- ip: 192.168.3.112
ports:
- port: 24007
name: glusterd
以下配置雷同,已省略
(4)在pod中使用gs1存储卷,示例如下;
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-demo
spec:
selector:
matchLabels:
app: web-demo
replicas: 1
template:
metadata:
labels:
app: web-demo
spec:
containers:
- name: web-demo
image: tomcat:latest
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8080
volumeMounts