近来在做一个项目,是用Spring Boot来实现的微服务架构。基本上所有的后端都是用Sring Boot来实现的一个个Module,现在项目做的差不多了,打算把它部署在Kubernetes集群上,遇到了一点点小问题,所幸都克服了,这里记录下遇到的问题以及相应的解决方法。
首先是容器集群的搭建,这个问题就挺大的。因为国内的GFW,所以很多东西是访问不到的,尤其是谷歌系的很多东西,比如golang,再比如kubernetes。之前一直希望能够让kubernetes走shadowsocks的代理,但是现在支持不是很好,主要是因为shadowsocks代理不了terminal里的访问,虽然有一些工具提供了一些解决办法,比如proxychains,等等,但是都不是很稳定。Docker已经有了Pull Request,支持了Socks5代理,但是整体来说也不是特别能用。这个问题到底应该如何解决,挂VPN,或者挂http proxy都可以解决,不过容器集群不是我搭起来的,在这里就不写这方面的事情了。
容器集群搭建起来后,就开始要在集群上搭建我们的服务了。基本来说,正常的Web应用都是由三个部分构成,分别是前端,后端,数据库,有的项目中前端和后端代码是在一起的,这里就记录一下,如何在Kubernetes中建立起一个数据库,同时在容器中运行后端的代码。
先了解下Mysql的Image是怎么实现的。在dockerhub上,可以看到Mysql的Dockerfile,大概可以了解它做的事情。其中我们需要用到的就是对于密码的设定,和存放数据的位置的挂载。Mysql镜像允许通过环境变量的方式设置数据库中的root用户的密码,同时,还可以挂载目录到容器中的/var/lib/mysql下。/var/lib/mysql是用来存放mysql的数据文件的,比如新建一个schema,就会在/var/lib/mysql下新建一个目录,等等。mysql的所有用户建立的数据库和表都会存放在这里,所以当需要在容器中运行Mysql时,可以挂载上自己的数据目录,让容器中的数据库运行实例可以访问到自己数据目录中的表。
了解了Mysql镜像的工作原理后,就开始着手来做事了。首先确认我们的架构,后端是一个Pod,数据库也是一个Pod,两者都需要一个Service,同时,数据库为了能够持久化数据,而不是每次容器挂了数据就没了,需要挂载一个数据目录到容器中。但是Kubernetes本身是分布式的,为了解决这个问题就需要一个网络文件系统,或者说分布式文件系统,来使得Kubernetes的每个Kubelet都能访问到这个数据目录。Kubernetes中为了解决这种问题,有两个抽象,叫做Persist Volume和Persist Volume Claim。是专门用来做数据持久化的,而且本身支持NFS,即网络文件系统。Persist Volume可以当成是创建一个硬盘资源,Persist Volume Claim可以当成是把创建的硬盘创建成一个可以挂载的Volume。其实比较好奇为什么要有两个抽象,而不是合并成一个,感觉这一定是出于某种目的。总之有了这两个抽象,就可以非常简单地解决持久化数据库存储的问题。当然,性能之类的还没有测试过,之后会考虑做下测试。
目前来看,我们需要创建一个Persist Volume,和一个Persist Volume Claim,来进行持久的数据存储,然后创建一个数据库的Pod,还有对应的Service,最后创建一个后端的Pod,以及对应的Service,有这样6个对象,就可以让项目真正运行在Kubernetes集群上了。下面就看下如何具体去做。
看看怎么创建一个Persist Volume,首先,需要把NFS挂在本地的目录下,在我的环境下,就是挂在了/home/administrator/data下,server的IP是192.168.0.110。所以对应的创建文件就是这样的:
apiVersion: v1
kind: PersistentVolume
metadata:
name: zuims-mysql
labels:
app: zuims
spec:
capacity:
storage: 5Gi
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Recycle
nfs:
path: /home/administrator/data
server: 192.168.0.110
然后需要创建一个Claim:
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: zuims-mysql
labels:
app: zuims
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 5Gi
为什么我觉得一个抽象就够了,因为实在是没看懂这个Claim存在的必要。好吧,之后就相当于创建好了一个来自网络文件系统的硬盘,然后做成可以挂载的Volume,接下来可以创建Mysql的Pod了:
apiVersion: v1
kind: Pod
metadata:
name: mysql
labels:
name: mysql
spec:
containers:
- image: mysql:5.7
name: mysql
env:
- name: MYSQL_ROOT_PASSWORD
value: 'YOUR_PASSWORD'
ports:
- containerPort: 3306
name: mysql
volumeMounts:
- name: mysql-persistent-storage
mountPath: /var/lib/mysql
volumes:
- name: mysql-persistent-storage
persistentVolumeClaim:
claimName: zuims-mysql
在这里做的事情就是指定下root用户的密码,然后指定下将网络文件系统下的那个目录挂在容器里的/var/lib/mysql下。根据前面对Mysql的Image的介绍,就知道这是为什么可以实现持久化存储了。接下来就是创建一个Service,没啥好说的。
apiVersion: v1
kind: Service
metadata:
labels:
name: mysql
name: mysql
spec:
ports:
- port: 3306
selector:
name: mysql
接下来看看后端需要做什么。首先,Spring Boot的配置文件需要做些许的修改:
# IP of the docker0
spring.datasource.url = jdbc:mysql://mysql:3306/mydb?useUnicode=true&characterEncoding=UTF-8
spring.datasource.username = root
spring.datasource.password = YOUR_PASSWORD
spring.datasource.driverClassName=com.mysql.jdbc.Driver
server.port=8001
server.address=0.0.0.0
...
主要的改动是数据连接的IP需要改成之前跑的Mysql的Service的name,这样Kubernetes就会通过DNS来找到Service对应的Pod的真正IP,这是任老师告诉我的。除此之外,就没什么了,接下来就是Pod和Service,也没什么大不了的,看看就好。
apiVersion: v1
kind: Pod
metadata:
name: zuims-user-service-1
labels:
app: zuims
spec:
containers:
- name: zuims
image: zuims/user-service:test
ports:
- containerPort: 8001
apiVersion: v1
kind: Service
metadata:
name: zuims-user-service
labels:
app: zuims
spec:
selector:
app: zuims
ports:
- port: 80
targetPort: 8001
几个问题
为什么要用网络的文件系统,不用本地的文件系统。这是因为Kubernetes是分布式的,数据库容器可能跑在任意的Kubelet上,所以如果要持久化数据,肯定要有一个分布式的文件系统供其使用。
为什么不把数据库和后端放在同一个Pod中,这样通信不是更方便么。从我的理解来看,后端的Pod是可以支持多副本的。而如果跟数据库放在了一个Pod中,就会被数据库牵连的不能做多副本。因为数据库容器的实现是使用文件系统的一个目录作为数据目录,而如果有多个容器使用了同一个目录作为数据目录,Mysql应该不会支持这种样子的并发吧。所以在网上可以看到的大多数的做法都是把数据库单独作为一个Pod,这样来实现的。