本文我们通过一个 Java Web 应用例子来介绍 kubernetes 的使用,可以让新手快速上手和实践。
此 Java Web 应用的结构比较简单,是一个运行在 Tomcat 里的 Web App,JSP 页面通过 JDBC 直接访问 MySQL
数据库并展示数据。出于演示和简化的目的,只要程序正确连接到了数据库,就会自动完成对应的 Table 的创建与
初始化数据的准备工作。所以,当我们通过浏览器访问此应用时,就会显示一个表格的页面,数据则来自数据库。
此应用需要启动两个容器:Web App 容器和 MySQL 容器,并且 Web App 容器需要访问 MySQL 容器。在
Docker 时代,假设我们在一个宿主机上启动了这两个容器,就需要把 MySQL 容器的 IP 地址通过环境变量注入
Web App 容器里;同时,需要将 Web App 容器的 8080 端口映射到宿主机的 8080 端口,以便在外部访问。在本
文的这个例子里,我们介绍在 Kubernetes 时代是如何达到这个目标的。
首先需要安装 kubernetes 集群,这里我们将不在介绍安装,安装请参考其它文章,本文的环境:
[root@master ~]# kubectl get nodes
NAME STATUS ROLES AGE VERSION
master Ready control-plane,master 6m5s v1.21.0
master2 Ready control-plane,master 4m57s v1.21.0
slave1 Ready <none> 5m30s v1.21.0
slave2 Ready <none> 5m28s v1.21.0
[root@master ~]# kubectl get pods --all-namespaces
NAMESPACE NAME READY STATUS RESTARTS AGE
kube-system calico-kube-controllers-b86879b9b-mb6hk 1/1 Running 0 24s
kube-system calico-node-6v5kp 1/1 Running 0 24s
kube-system calico-node-c8l7v 1/1 Running 0 24s
kube-system calico-node-grlqh 1/1 Running 0 24s
kube-system calico-node-mr8rk 1/1 Running 0 24s
kube-system coredns-545d6fc579-42cxc 1/1 Running 0 5m33s
kube-system coredns-545d6fc579-lzgbz 1/1 Running 0 5m33s
kube-system etcd-master 1/1 Running 0 5m49s
kube-system etcd-master2 1/1 Running 0 4m45s
kube-system kube-apiserver-master 1/1 Running 0 5m49s
kube-system kube-apiserver-master2 1/1 Running 0 4m49s
kube-system kube-controller-manager-master 1/1 Running 1 5m49s
kube-system kube-controller-manager-master2 1/1 Running 0 4m49s
kube-system kube-proxy-8gj54 1/1 Running 0 5m22s
kube-system kube-proxy-9vc8c 1/1 Running 0 4m49s
kube-system kube-proxy-lpfnx 1/1 Running 0 5m33s
kube-system kube-proxy-mvphs 1/1 Running 0 5m20s
kube-system kube-scheduler-master 1/1 Running 1 5m49s
kube-system kube-scheduler-master2 1/1 Running 0 4m49s
本文使用的 Docker 镜像地址: https://hub.docker.com/u/kubeguide/
为 MySQL 服务创建一个 RC 定义文件 mysql-rc.yaml
:
apiVersion: v1
# 副本控制器RC
kind: ReplicationController
metadata:
# RC的名称,全局唯一
name: mysql
spec:
# Pod副本的期待数量
replicas: 1
selector:
# 符合目标的Pod拥有此标签
app: mysql
# 根据此模板创建Pod的副本(实例)
template:
metadata:
labels:
# Pod副本拥有的标签,对应RC的Selector
app: mysql
spec:
# Pod内容器的定义部分
containers:
# 容器的名称
- name: mysql
# 容器对应的Docker Image
image: mysql:5.7
ports:
# 容器应用监听的端口号
- containerPort: 3306
# 注入容器内的环境变量
env:
- name: MYSQL_ROOT_PASSWORD
value: "123456"
以上 YAML 定义文件中的 kind 属性用来表明此资源对象的类型,比如这里的值为 ReplicationController,表示这
是一个 RC ;在 spec 一节中是 RC 的相关属性定义,比如 spec.selector 是 RC 的 Pod 标签选择器,即监控和管
理拥有这些标签的 Pod 实例,确保在当前集群中始终有且仅有 replicas 个 Pod 实例在运行,这里设置
replicas=1,表示只能运行一个 MySQL Pod 实例。当在集群中运行的 Pod 数量少于 replicas 时,RC 会根据在
spec.template 一节中定义的 Pod 模板来生成一个新的 Pod 实例,spec.template.metadata.labels 指定了该
Pod 的标签,需要特别注意的是:这里的 labels 必须匹配之前的 spec.selector,否则此 RC 每创建一个无法匹配
Label 的 Pod,就会不停地尝试创建新的 Pod,陷入恶性循环中。
在创建好 mysql-rc.yaml 文件后,将它发布到 kubernetes 集群中,我们在 master 上执行如下命令:
[root@master cha1]# kubectl create -f mysql-rc.yaml
replicationcontroller/mysql created
接下来,用 kubectl 命令查看刚刚创建的 RC:
[root@master cha1]# kubectl get rc -o wide
NAME DESIRED CURRENT READY AGE CONTAINERS IMAGES SELECTOR
mysql 1 1 1 7m37s mysql mysql:5.7 app=mysql
查看 Pod 的创建情况时,可以运行下面的命令:
# NODE=slave2表示该pod在slave2节点上创建
[root@master cha1]# kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
mysql-8mwkz 1/1 Running 0 8m8s 10.244.140.65 slave2 <none> <none>
我们看到一个名为 mysql-8mwkz
的 Pod 实例,这是 kubernetes 根据 mysql 这个 RC 的定义自动创建的 Pod。
由于 Pod 的调度和创建需要花费一定的时间,比如需要一定的时间来确定调度到哪个节点上,以及下载 Pod 里的
容器镜像需要一段时间,所以我们一开始看到Pod的状态显示为 Pending。在 Pod 成功创建完成以后,状态最终
会被更新为 Running。
我们通过 docker ps 指令查看正在运行的容器,发现提供 MySQL 服务的 Pod 容器已经创建并正常运行了,此外
会发现 MySQL Pod 对应的容器还多创建了一个来自谷歌的 pause 容器,这就是 Pod 的根容器。
[root@slave2 ~]# docker ps | grep mysql
9b799e33f346 c20987f18b13 "docker-entrypoint.s…" 7 minutes ago Up 7 minutes k8s_mysql_mysql-8mwkz_default_b494d65f-2ee8-436b-8a09-ef0b220ba916_0
d18de886ae4e registry.aliyuncs.com/google_containers/pause:3.4.1 "/pause" 7 minutes ago Up 7 minutes k8s_POD_mysql-8mwkz_default_b494d65f-2ee8-436b-8a09-ef0b220ba916_85
创建一个与之关联的 kubernetes service,service 的定义文件 mysql-svc.yaml
如下:
apiVersion: v1
# 表明是Kubernetes Service
kind: Service
metadata:
# Service的全局唯一名称
name: mysql
spec:
ports:
# Service提供服务的端口号
- port: 3306
# Service对应的Pod拥有这里定义的标签
selector:
app: mysql
其中,metadata.name 是 Service 的服务名;port 属性则定义了 Service 的端口;spec.selector 确定了哪些 Pod
副本(实例)对应本服务。通过下面的命令创建 service:
[root@master cha1]# kubectl create -f mysql-svc.yaml
service/mysql created
运行 kubectl 命令查看刚刚创建的 Service:
[root@master cha1]# kubectl get svc -o wide
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 15m <none>
mysql ClusterIP 10.110.148.221 <none> 3306/TCP 7m12s app=mysql
可以发现,MySQL 服务被分配了一个值为 10.110.148.221
的 Cluster IP 地址。随后,Kubernetes 集群中其他
新创建的 Pod 就可以通过 Service 的 Cluster IP+端口号3306 来连接和访问它了。
通常,Cluster IP 是在 Service 创建后由 Kubernetes 系统自动分配的,其他 Pod 无法预先知道某个 Service 的
Cluster IP地址,因此需要一个服务发现机制来找到这个服务。为此,最初时,Kubernetes 巧妙地使用了 Linux
环境变量来解决这个问题。现在只需知道,根据 Service 的唯一名称,容器可以从环境变量中获取 Service 对应的
Cluster IP 地址和端口,从而发起 TCP/IP 连接请求。
上面定义和启动了 MySQL 服务,接下来采用同样的步骤完成 Tomcat 应用的启动过程。首先,创建对应的 RC 文
件 myweb-rc.yaml
,内容如下:
apiVersion: v1
# 副本控制器RC
kind: ReplicationController
metadata:
# RC的名称,全局唯一
name: myweb
spec:
# Pod副本的期待数量
replicas: 2
selector:
# 符合目标的Pod拥有此标签
app: myweb
# 根据此模板创建Pod的副本(实例)
template:
metadata:
labels:
# Pod副本拥有的标签,对应RC的Selector
app: myweb
spec:
# Pod内容器的定义部分
containers:
# 容器的名称
- name: myweb
# 容器对应的 Docker Image
image: kubeguide/tomcat-app:v1
ports:
# 容器应用监听的端口号
- containerPort: 8080
env:
- name: MYSQL_SERVICE_HOST
value: 'mysql'
- name: MYSQL_SERVICE_PORT
value: '3306'
注意:在 Tomcat 容器内,应用将使用环境变量 MYSQL_SERVICE_HOST 的值连接MySQL服务。更安全可靠的用
法是使用服务的名称 mysql。运行下面的命令,完成RC的创建和验证工作:
[root@master cha1]# kubectl create -f myweb-rc.yaml
replicationcontroller/myweb created
[root@master cha1]# kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
mysql-8mwkz 1/1 Running 0 10m 10.244.140.65 slave2 <none> <none>
myweb-clht2 1/1 Running 0 8m18s 10.244.140.193 slave1 <none> <none>
myweb-sjdhr 1/1 Running 0 8m18s 10.244.140.194 slave1 <none> <none>
最后,创建对应的 Service,定义文件 myweb-svc.yaml
:
apiVersion: v1
# 表明是Kubernetes Service
kind: Service
metadata:
# Service的全局唯一名称
name: myweb
spec:
type: NodePort
ports:
# Service提供服务的端口号
- port: 8080
nodePort: 30001
# Service对应的Pod拥有这里定义的标签
selector:
app: myweb
type=NodePort 和 nodePort=30001 的两个属性表明此 Service 开启了 NodePort 方式的外网访问模式。在
Kubernetes 集群之外,比如在本机的浏览器里,可以通过 30001 这个端口访问myweb(对应到8080的虚端口
上)。使用下面的命令进行创建:
[root@master cha1]# kubectl create -f myweb-svc.yaml
service/myweb created
查看 service:
[root@master cha1]# kubectl get svc -o wide
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 17m <none>
mysql ClusterIP 10.110.148.221 <none> 3306/TCP 8m55s app=mysql
myweb NodePort 10.101.194.99 <none> 8080:30001/TCP 8m33s app=myweb
至此,我们的第1个 Kubernetes 例子便搭建完成了。
经过上面的几个步骤,我们终于成功实现了 Kubernetes 上第1个例子的部署搭建工作。现在一起来见证成果吧!
打开浏览器输入http://192.168.43.201:30001/demo/
,可以看到如下图所示的网页界面:
接下来,可以尝试单击 Add…
按钮添加一条记录并提交,如图所示,在提交以后,数据就被写入 MySQL 数据库
中了。
至此,我们终于完成了 Kubernetes 上的 Tomcat 例子,这个例子并不是很复杂。我们也看到,相对于传统的分布
式应用的部署方式,在 Kubernetes 之上我们仅仅通过一些很容易理解的配置文件和相关的简单命令就完成了对整
个集群的部署,这让我们惊诧于 Kubernetes 的创新和强大。