在一台VPS上部署k8s手记
2024年11月13日如下是我用一台vps部署k8s,以及在k8s上运行基于mysql的web程序的全过程。
安装minikube
商业k8s集群都是庞然大物,通常都是几个master节点,和几百个work节点组成。但我这里只是测试部署,没必要搞那么复杂,就用minikube好了。
Minikube是k8s的官方项目,它允许在一个vps上进行部署,从而创建k8s模拟环境。它的主页:
https://minikube.sigs.k8s.io/docs/start
在安装之前,你需要一个vps,虽然主页写着最低要求是2c/2g/20g,但考虑到系统自身占用的资源,我建议是2c/4g/40g这种配置。
在vps里,需要预先安装docker服务。我的是ubuntu 22.04系统,安装docker也很简单,参考digitalocean的这篇文档:
https://www.digitalocean.com/community/tutorials/how-to-install-and-use-docker-on-ubuntu-20-04
装了docker后,参考minikube的官方文档,把minikube环境安装上。这个安装也是十分简单的,执行如下命令。
$ curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64
$ sudo install minikube-linux-amd64 /usr/local/bin/minikube
然后启动minukube:
$ minikube start
在系统里设置好别名如下,这个是k8s的管理客户端,经常要用到的。
alias k='minikube kubectl --'
alias kubectl='minikube kubectl --'
试着执行如下命令,看kubectl客户端是否正常工作。
$ k get node
NAME STATUS ROLES AGE VERSION
minikube Ready control-plane 20d v1.26.1
如上输出,就说明ok了。
部署mysql pod
Mysql有官方的docker镜像,所以搬到k8s上挺简单的。不过我这里设计复杂一点,使用了3个mysql实例,一方面用于负载均衡,另一方面用于故障转移。其中一个mysql宕机,不至于影响业务的运行。当然,我的微型站点根本不需要这样的配置,只是演习而已。
创建一个custo-mysql目录,在里面创建k8s的yaml配置文件,共有3个文件:
secret.yaml service.yaml statefulset.yaml
第一个文件用来放mysql的root密码,第二个文件用来放mysql的service配置,第三个文件用来放mysql的部署结构。
它们各自的内容如下。第一个文件:
apiVersion: v1
kind: Secret
metadata:
name: mysecret
type: Opaque
data:
ROOT_PASSWORD: bXlwYXNzd2Q=
注意mysql的root密码,是base64 encode过的,原始密码就是mypasswd(演示目的)。
这个secret.yaml文件在执行后,实际将ROOT_PASSWORD注入容器的环境变量,容器里的mysql启动后就自动把root密码设置为这个变量。
第二个文件:
apiVersion: v1
kind: Service
metadata:
name: custo-mysql
labels:
app: custo-mysql
spec:
clusterIP: None
selector:
app: custo-mysql
ports:
- name: tcp
protocol: TCP
port: 3306
这个是mysql的service文件。所谓service,在k8s里的作用是服务发现和接口暴露。也就是说,k8s里的其他容器,要跟mysql容器进行通信,就必须用到service接口。Service如果有clusterIP,那么k8s就会生成一个负载均衡ip,其他容器对mysql的请求,会到达这个负载均衡ip。但是,mysql集群通常是一个状态服务,不能随机负载均衡,所以这里clusterIP设置为none,表示直接使用mysql容器的ip,而不是生成另外一个负载均衡ip。
注意下selector那里的配置,app:custo-mysql是一个标签,service会根据这个标签找到后端对应的mysql实例,这里不能搞错了。
第三个文件:
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: custo-mysql
spec:
replicas: 3
serviceName: custo-mysql
selector:
matchLabels:
app: custo-mysql
template:
metadata:
labels:
app: custo-mysql
spec:
terminationGracePeriodSeconds: 10
containers:
- name: mysql
image: mysql:5.7
ports:
- name: tpc
protocol: TCP
containerPort: 3306
env:
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
key: ROOT_PASSWORD
name: mysecret
volumeMounts:
- name: custo-data
mountPath: /var/lib/mysql
volumeClaimTemplates:
- metadata:
name: custo-data
spec:
storageClassName: standard
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
这是mysql的部署文件。注意部署对象不是deployment,而是statefulset。这是因为mysql是一个状态服务,k8s重启它后,要保持之前的状态,比如恢复数据。如果不恢复到之前的状态,数据就丢了。
这个文件包含的内容比较多,关键点说明如下,请自己对照着看。
- 配置了3个实例副本。
- 标签选择器是app:custo-mysql
- 使用mysql 5.7版本,这是官方支持的5.x系列最低要求。
- 服务端口是3306
- 从环境变量里获取到root密码(第一个文件里设置)。
- 申请到一个永久存储,大小为1g,绑定到/var/lib/mysql目录(数据持久目录)。
上述最后一点很重要,这是mysql状态服务的关键。因为k8s里的容器可以被随意kill,删掉一个后,k8s会启动一个新的容器用来代替。那么新起来的容器,必须同样加载这个数据目录,mysql数据才不会丢失。
上述三个配置文件,位于custo-mysql目录里,用kubectl命名加载进集群如下。
$ k apply -f *.yaml
然后看一下pod和service是否正常:
$ k get pod -l app=custo-mysql
NAME READY STATUS RESTARTS AGE
custo-mysql-0 1/1 Running 0 9h
custo-mysql-1 1/1 Running 0 61m
custo-mysql-2 1/1 Running 1 (36h ago) 4d5h
$ k get service -l app=custo-mysql
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
custo-mysql ClusterIP None <none> 3306/TCP 4d6h
如上输出就ok了。你也可以仔细看一下这个部署的详细情况,执行如下命令。
$ k describe sts/custo-mysql
导入数据到mysql
上一节创建的3个mysql,理论上可以配置成一主二从,需要人工去设置mysql replication。我懒得弄,就在每个库里都创建了对等的表和数据,应用查询任何一个库,都可以得到对等的结果。
如何访问这三个mysql呢?请参考如下脚本(名为mysql.sh)。
#!/bin/bash
arg=$1
k='minikube kubectl --'
if [ $arg -eq 0 ];then
$k exec -it custo-mysql-0 -- mysql -uroot -pmypasswd
elif [ $arg -eq 1 ];then
$k exec -it custo-mysql-1 -- mysql -uroot -pmypasswd
elif [ $arg -eq 2 ];then
$k exec -it custo-mysql-2 -- mysql -uroot -pmypasswd
fi
在vps里运行./mysql.sh 0就访问第一个实例,运行./mysql.sh 1就访问第二个实例,运行./mysql.sh 2就访问第三个实例。如下所示。
$ ./mysql.sh 0
mysql: [Warning] Using a password on the command line interface can be insecure.
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 60
Server version: 5.7.41 MySQL Community Server (GPL)
Copyright (c) 2000, 2023, Oracle and/or its affiliates.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql> use mytest;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A
Database changed
mysql> show create table books;
+-------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Table | Create Table |
+-------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| books | CREATE TABLE `books` (
`id` int(11) DEFAULT NULL,
`name` varchar(64) DEFAULT NULL,
`year` int(11) DEFAULT NULL,
`publisher` varchar(64) DEFAULT NULL,
`labels` varchar(32) DEFAULT NULL,
`price` float DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1 |
+-------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)
如上内容表示,登陆mysql后,我创建了mytest数据库,以及在这个库里,创建了books数据表。表的结构也给出来了,照抄即可(create table books语句)。
需要在三个数据库都创建同样的库和表结构。这三个库的ip,可以用get pod列出来如下。
$ k get pod -o wide -l app=custo-mysql
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
custo-mysql-0 1/1 Running 0 9h 10.244.0.79 minikube <none> <none>
custo-mysql-1 1/1 Running 0 74m 10.244.0.96 minikube <none> <none>
custo-mysql-2 1/1 Running 1 (37h ago) 4d5h 10.244.0.76 minikube <none> <none>
当然,你也可以用kubectl exec命令,先登陆其中一个mysql容器,在容器里再访问mysql服务,如下所示。
$ k exec custo-mysql-0 -it -- bash
bash-4.2#
bash-4.2# mysql -uroot -hcusto-mysql -pmypasswd
mysql: [Warning] Using a password on the command line interface can be insecure.
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 62
Server version: 5.7.41 MySQL Community Server (GPL)
Copyright (c) 2000, 2023, Oracle and/or its affiliates.
你还可以运行kubectl logs命令,查看容器的日志输出,如下所示。
$ k logs custo-mysql-0
2023-03-07 03:12:46+00:00 [Note] [Entrypoint]: Entrypoint script for MySQL Server 5.7.41-1.el7 started.
2023-03-07 03:12:46+00:00 [Note] [Entrypoint]: Switching to dedicated user 'mysql'
既然能够登陆mysql服务,那么导入数据就不难了。刚才已经创建了数据表,那么把测试数据,导入到这个表里即可。我随机生成了一份测试数据,是10万条IT书籍的记录。
制作web应用镜像
数据库已经有了,接下来就创建一个应用服务,它根据用户的输入关键字,查询数据库,并返回结果。这个应用服务我简单写了个cgi脚本(名为books.cgi),内容如下:
#!/usr/bin/perl
use strict;
use CGI;
use MySQL::mycrud;
my $q = CGI->new;
if (defined $q->param("submit") ) {
my $key = $q->param("key");
$key =~ s/^\s+|\s+$//g;
if ($key =~ /[^a-zA-Z0-9\s]/ or length($key) > 64 or length($key) < 3 ) {
print $q->header;
print "invalid keyword";
exit;
}
my $db = MySQL::mycrud->new('mytest','custo-mysql','3306','root','mypasswd');
my $rr = $db->get_rows("select * from books where name like '%$key%' or publisher like '%$key%' or labels like '%$key%' limit 100");
print $q->header(
-type => 'text/html',
);
print "<table>";
print "<tr><td>ID</td><td>Name</td><td>Year</td><td>Publisher</td><td>Labels</td><td>Price</td></tr>\n";
for my $r (@$rr) { # each element is a hash ref
print "<tr>";
print "<td>$r->{id}</td>";
print "<td>$r->{name}</td>";
print "<td>$r->{year}</td>";
print "<td>$r->{publisher}</td>";
print "<td>$r->{labels}</td>";
print "<td>$r->{price}</td>";
print "</tr>\n";
}
print "</table>";
} else {
print $q->header(
-type => 'text/html',
);
print <<HTML_EOF;
<!DOCTYPE html>
<html>
<body>
<h2>search books on k8s</h2>
<form action="/cgi-bin/books.cgi" method="post">
<label>keyword:</label><br>
<input type="text" name="key" value="database"><br><br>
<input type="submit" name="submit" value="Submit">
</form>
</body>
</html>
HTML_EOF
}
cgi运行在哪里呢?当然最合适的还是apache httpd服务器。所以,我要创建一个包含apache2的镜像(image),并将这个cgi脚本放到镜像里。
如何创建docker image,这个我就不重点讲了,请参考我的另一篇笔记:
https://notes.199903.xyz/create-my-ubuntu-docker-image-for-k8s
简言之要准备好Dockerfile,这个文件的内容如下:
FROM ubuntu
RUN apt update && apt -y install \
make cpanminus curl mysql-client libdbd-mysql-perl apache2
RUN cpanm MySQL::mycrud
RUN cpanm CGI
RUN a2enmod cgi
RUN mkdir -p /var/www/cgi-bin
COPY books.conf /etc/apache2/sites-enabled/
COPY books.cgi /var/www/cgi-bin/
RUN chmod 755 /var/www/cgi-bin/books.cgi
RUN rm -f /etc/apache2/sites-enabled/000-default.conf
CMD ["/usr/sbin/apachectl", "-D", "FOREGROUND"]
镜像里Web服务器的配置文件(books.conf)内容如下:
<VirtualHost *:80>
ServerName books.hostcache.com
ServerAdmin webmaster@localhost
DocumentRoot /var/www/html
ScriptAlias /cgi-bin/ /var/www/cgi-bin/
<Directory /var/www/cgi-bin>
AllowOverride None
Options +ExecCGI
AddHandler cgi-script .cgi .pl
Require all granted
</Directory>
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>
上述3个文件(Dockerfile, books.cgi, books.conf)放在同一个目录里(名为books),然后针对这个目录,执行如下docker build即可。
docker build -t books books/
docker tag books geekml/books
docker push geekml/books
build成功后,会将image提交到docker hub里,后面k8s就可以直接从docker hub拉取镜像生成容器了。具体image的制作过程,还请参考前述博客。
创建web应用pod
跟创建mysql pod一样,同样使用一个yaml配置文件,内容如下:
apiVersion: v1
kind: Service
metadata:
name: books
labels:
app: books
spec:
ports:
- port: 80
selector:
app: books
type: LoadBalancer
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: books
labels:
app: books
spec:
replicas: 3
selector:
matchLabels:
app: books
strategy:
type: Recreate
template:
metadata:
labels:
app: books
spec:
containers:
- image: geekml/books
name: books
ports:
- containerPort: 80
name: books
上述配置我挑重点说明如下,请对照mysql的配置进行对比。
- 针对web服务定义了一个service,同样用来做服务发现。它的类型是LoadBalancer,而不是clusterIP。
- 针对web服务定义了Deployment,包含了部署结构。请注意这里是deployment对象,而不是statefulset,因为web服务是个无状态服务。
- Web服务包含3个副本。
- Web服务的标签选择器是app:books
- Web服务使用的镜像是geekml/books,就是我之前上传到docker hub的镜像。
关于service的类型,务必仔细弄清楚,我简要说明如下。
- Service默认类型是clusterIP,也就是生成一个负载均衡ip,用来轮询到后台pod的ip。
- 如果clusterIP设置为none,那么就不生成负载均衡ip,而是直接用pod的ip。对于状态服务(比如mysql),它的clusterIP需要设置为none,也就是不需要轮询。
- 如果service类型是LoadBalancer,它不但会生成负载均衡ip,还会生成一个外部ip。vps使用这个外部ip,可以访问到k8s集群的服务。
我们打开另一个容器,登入k8s集群,查看mysql和web服务两个service的ip。如下所示。
$ k run devbox -it --rm --image geekml/myubuntu -- bash
If you don't see a command prompt, try pressing enter.
root@devbox:/# host custo-mysql
custo-mysql.default.svc.cluster.local has address 10.244.0.96
custo-mysql.default.svc.cluster.local has address 10.244.0.79
custo-mysql.default.svc.cluster.local has address 10.244.0.76
root@devbox:/# host books
books.default.svc.cluster.local has address 10.96.210.217
这里很明显,3个mysql实例的clusterIP就是它们自己。而3个web实例的clusterIP是一个独立的负载均衡ip。
我在配置里对web服务使用了LoadBalancer,因为需要从vps上访问这个web服务,而且web服务是无状态的,随便轮询。
那么web服务的外部访问地址是什么呢?运行如下命令查看:
$ minikube service books
|-----------|-------|-------------|---------------------------|
| NAMESPACE | NAME | TARGET PORT | URL |
|-----------|-------|-------------|---------------------------|
| default | books | 80 | http://192.168.49.2:32075 |
|-----------|-------|-------------|---------------------------|
🎉 Opening service default/books in default browser...
👉 http://192.168.49.2:32075
好了,把如上k8s的外部服务地址(http://192.168.49.2:32075),加入到vps上web服务器的反向代理,就万事大吉。当然,这里也可以用k8s的Ingress规则来做反向代理。
我的vps上也装了apache2,就懒得去弄nginx反向代理,直接用apache的mod_proxy模块。关于如何配置apache的反向代理,请参考digitalocean的文档:
apache2反向代理配置文件如下:
<VirtualHost *:80>
ProxyPreserveHost On
ProxyPass / http://192.168.49.2:32075/
ProxyPassReverse / http://192.168.49.2:32075/
</VirtualHost>
配置完后,重启apache2。再设置一个域名,指向vps的ip,这里的测试域名是books.hostcache.com。然后,你打开如下url,就可以访问到k8s上运行的web服务了。
http://books.hostcache.com/cgi-bin/books.cgi
我们再看一下3个web服务器和3个数据库服务器组成的k8s集群:
$ k get pod -l app=books
NAME READY STATUS RESTARTS AGE
books-fd4f9cccd-cs985 1/1 Running 0 3h3m
books-fd4f9cccd-rl2r5 1/1 Running 0 3h3m
books-fd4f9cccd-rwnrf 1/1 Running 0 3h3m
$ k get pod -l app=custo-mysql
NAME READY STATUS RESTARTS AGE
custo-mysql-0 1/1 Running 0 10h
custo-mysql-1 1/1 Running 0 128m
custo-mysql-2 1/1 Running 1 (38h ago) 4d6h
你可以随便删掉一个mysql,或者一个web服务,k8s都会自动重启一个新的来代替。
$ k delete pod custo-mysql-1
在故障转移和高可用方面,k8s真的挺强的,所以k8s很适合运行微服务。在商业环境里,数以万计的微服务运行在k8s上,它们经常重启或者漂移,但是不影响整体稳定性。