K8s集群搭建完成后,真正完成我们业务的是那些跑在k8s上的pod们。将业务跑在k8s集群只上,我们可以实现根据负载或者资源利用率动态扩容或者缩容我们的后端服务器,更加灵活高效的利用我们的物理设备,且能够实现服务的高可用及故障自治愈,本文将详细介绍以上的具体实现。

实验环境

  本次演示使用主机系统均为ubuntu1804。

节点 IP
master节点 192.168.32.18、192.168.32.19
node节点 192.168.32.21、192.168.32.22
etcd 192.168.32.23、192.168.32.24、192.168.32.25
harbor 192.168.32.20
NFS服务器 192.168.32.20(复用)

  如果机器不够,可以选择复用,例如将etcd的三台主机与master主机和harbor主机复用,可以省下来3台主机。推荐node节点的性能高一些,因为一般之后的pod都将运行在node节点上,如果node节点的资源不足,则很有可能无法创建pod,导致服务启动或者扩容失败。
  具体K8s集群的搭建,可以参考我之前文章使用kubeasz自动化部署K8s。harbor服务器的搭建和部署配置,也可以参考我之前文章Docker(五)——Docker镜像仓库
  因为是均为内网环境,建议将网络组件calico的IPIP模式(ip-in-ip叠加模式)关掉,使用calico的BGP模式,以节约大量主机内部访问时封装的性能损耗。具体操作可以参考之前博客使用kubeasz自动化部署K8s

设计架构

  我们设计一个简单架构为例,如下图所示。
k8s架构-java服务集群
  一般负载层是使用物理机上的haproxy服务来实现4层负载,而不是在K8s集群内部pod 实现。我们这里也就将他省略不再细说。
所以我们需要创建一个nginx集群,以及一个tomcat集群。将动态资源,转发给后端的tomcat服务,前端的静态页面由nginx来处理。为了方便我们对后端服务的修改以及部署,我们还需要一个NFS服务器,来共享静态资源以及动态资源,以实现后端服务数据的实时同步。

搭建业务镜像架构

  镜像一般是按层次一级一级实现的,可实现多个业务复用。所以,例如我们将要实现的业务为app1,则我们需要的的镜像,就有对应的app1-nginx及app1-tomcat的业务镜像,以及nginx及tomcat的基础镜像,以及基础系统镜像,我们要一层层的来制作镜像。详细可以参考之前文章Docker(三)——镜像制作

1
2
3
4
cd /opt
mkdir -p k8s/{app1,dockerfile,yaml}
cd k8s/dockerfile
mkdir {app1,pub-image,system}

  最终结构如下图所示。

1
2
3
4
5
6
7
8
9
10
11
12
root@DockerUbuntu18:/opt/k8s/dockerfile# tree -d
.
├── app1
│ ├── app1-nginx
│ └── app1-tomcat
├── pub-images
│ ├── jdk
│ │ └── 8
│ ├── nginx-base
│ └── tomcat-base
└── system
└── centos

  打好的镜像如下所示。

1
2
3
4
5
6
7
8
9
root@DockerUbuntu18:/opt/k8s/dockerfile# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
harbor.micepro.net/base/app1-tomcat v1 c7d87d05f8a4 3 days ago 1.19GB
harbor.micepro.net/base/tomcat-base v8.5.47 12cf3e12f2d0 3 days ago 1.19GB
harbor.micepro.net/base/jdk-base v8.212 1582b1e9cfe4 3 days ago 1.16GB
harbor.micepro.net/base/app1-nginx v1 4836f9aed79b 3 days ago 940MB
harbor.micepro.net/base/nginx-base v1.16.1 bfc05a5c23ed 5 days ago 940MB
harbor.micepro.net/base/centos-base v7.6 bc7e922a8352 5 days ago 753MB
harbor.micepro.net/base/centos base f1cb7c7d58b7 10 months ago 202MB

配置NFS服务器

  我们计划将动态资源和静态资源都放置至我们的NFS共享存储上,也就是我们的harbor上(任意主机,端口不冲突就可以~)
  可以通过包管理工具快速安装一个NFS服务,毕竟这只是一个辅助工具,无需过多关注。

1
2
3
4
apt install -y nfs-server
mkdir /data/app1/{images,static,ROOT} -p
echo "/data/app1 *(rw,no_root_squash)" >> /etc/exports
systemctl enable --now nfs-server

  将/data/app1目录整个共享出去即可,之后我们可以直接将NFS挂载至pod中,也可以通过PV/PVC的方式挂载到pod中。
  我们需要修改权限,通过指定uid的方式,使某个UID在多个主机之间都具有权限。这就要求我们之前在创建基础镜像时,也使用的是相同的UID启动程序。例如我们统一都使用www用户,指定UID为2020。(之前创建的容器以及基础镜像中都要修改服务以UID为2020的www用户启动)

编写yaml文件

  此时我们就可以开始着手准备yaml文件,来规划我们的服务pod了。

namespace

  首先我们需要新创建一个namespace,来实现与其他服务隔离。

1
2
cd /opt/k8s/
mkdir -p yaml/namespace
1
2
3
4
5
vim yaml/namespace/app1.yaml
apiVersion: v1 #API版本
kind: Namespace #类型为namespac
metadata: #定义元数据
name: app1 #namespace名称

  然后将这个namespace创建出来,使用命令

1
kubectl apply -f /opt/k8s/yaml/namespace/app1.yaml

  此时使用命令kubectl get ns可以看到我们新创建的namespace

1
2
3
4
5
6
7
root@DockerUbuntu18:/opt/k8s# kubectl get ns
NAME STATUS AGE
app1 Active 20s
default Active 3d
kube-node-lease Active 3d
kube-public Active 3d
kube-system Active 3d

pod

  先创建好对应的目录,方便后期管理维护。

1
2
cd /opt/k8s
mkdir -p app1/{nginx,tomcat}

  然后我们就可以编写pod的yaml文件了。

1
cd /opt/k8s/app1/nginx

  可以将Deploymentserver写在一起,也可以分开写。我们这直接都写在一起。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
vim nginx.yaml

kind: Deployment
apiVersion: extensions/v1beta1
metadata:
labels:
app: app1-nginx-deployment-label
name: app1-nginx-deployment
namespace: app1
spec:
replicas: 2 #设置nginx集群数量
selector:
matchLabels:
app: app1-nginx-selector
template:
metadata:
labels:
app: app1-nginx-selector
spec:
containers:
- name: app1-nginx-container
image: harbor.micepro.net/base/app1-nginx:v1
#command: ["/apps/tomcat/bin/run_tomcat.sh"]
imagePullPolicy: IfNotPresent
#imagePullPolicy: Always
ports:
- containerPort: 80
protocol: TCP
name: http
- containerPort: 443
protocol: TCP
name: https
env:
- name: "password"
value: "123456"
- name: "age"
value: "18"
resources:
limits:
cpu: 2
memory: 2Gi
requests:
cpu: 500m
# memory: 1Gi
memory: 200m

volumeMounts:
- name: app1-images
mountPath: /usr/local/nginx/html/webapp/images
readOnly: false
- name: app1-static
mountPath: /usr/local/nginx/html/webapp/static
readOnly: false
volumes:
- name: app1-images
nfs:
server: 192.168.32.20
path: /data/app1/images
- name: app1-static
nfs:
server: 192.168.32.20
path: /data/app1/static

---
kind: Service
apiVersion: v1
metadata:
labels:
app: app1-nginx-service-label
name: app1-nginx-service
namespace: app1
spec:
type: NodePort
ports:
- name: http
port: 80
protocol: TCP
targetPort: 80
nodePort: 30080
- name: https
port: 443
protocol: TCP
targetPort: 443
nodePort: 30443
selector:
app: app1-nginx-selector
1
cd /opt/k8s/app1/tomcat

  编写tomcat的pod的yaml文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
kind: Deployment
apiVersion: extensions/v1beta1
metadata:
labels:
app: app1-tomcat-app1-deployment-label
name: app1-tomcat-app1-deployment
namespace: app1
spec:
replicas: 1
selector:
matchLabels:
app: app1-tomcat-app1-selector
template:
metadata:
labels:
app: app1-tomcat-app1-selector
spec:
containers:
- name: app1-tomcat-app1-container
image: harbor.micepro.net/base/tomcat-app1:v1
command: ["/usr/local/tomcat/bin/run_tomcat.sh"]
#imagePullPolicy: IfNotPresent
imagePullPolicy: Always
ports:
- containerPort: 8080
protocol: TCP
name: http
env:
- name: "password"
value: "123456"
- name: "age"
value: "18"
resources:
limits:
cpu: 2
memory: "1000Mi"
requests:
cpu: 500m
memory: "200Mi"

volumeMounts:
- name: app1-myapp
mountPath: /data/tomcat/webapps/ROOT
readOnly: false

volumes:
- name: app1-myapp
nfs:
server: 192.168.32.19
path: /data/app1/ROOT
- name: app1-static
nfs:
server: 192.168.32.19
path: /data/app1/static
---
kind: Service
apiVersion: v1
metadata:
labels:
app: app1-tomcat-app1-service-label
name: app1-tomcat-app1-service
namespace: app1
spec:
type: NodePort
ports:
- name: http
port: 80
protocol: TCP
targetPort: 8080
nodePort: 38080
selector:
app: app1-tomcat-app1-selector

修改配置文件

  因为要实现一个通用的nginx+tomcat动静分离web架构,即用户访问的静态页面和图片在由nginx直接响应,而动态请求则基于tomcat的service name转发用户请求到tomcat业务app。
  所以我们需要修改当时创建镜像的nginx的配置文件,配置upstream服务器为tomcat的pod,配置如下

1
2
3
4
5
6
7
8
9
10
11
upstream tomcat_webserver {
server app1-tomcat-app1-service.app1.svc.app1.local:8080;
}
server {
location /myapp {
proxy_pass http://tomcat_webserver;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
}
}

  其中pod间通信是基于K8s内部DNS实现的,一般是coreDNS或者kubeDNS,如果没有安装是无法解析的,可以使用kubeasz的第七个playbook安装,可参考之前文章使用kubeasz自动化部署K8s
  主机名默认组成结构为release-name-rancher.default.svc.cluster.local,也就是ServerName.NameSpace.svc.CLUSTER_DNS_DOMAINServerName是我们在yaml文件中定义的,NameSpace是我们创建pod时选择的namespaceCLUSTER_DNS_DOMAIN是我们在创建K8s集群时设置的集群名,如果是用kubeasz工具创建的K8s集群,可以去/etc/ansible/hosts文件中查看。
  我们可以在pod间测试通信,确保相互之间可以通过主机名通信。

1
2
3
4
5
[root@blog-tomcat-app1-deployment-5b4845ddd8-rr5dq /]# ping app1-nginx-service.app1.svc.cluster.local
PING app1-nginx-service.app1.svc.cluster.local (10.68.101.60) 56(84) bytes of data.
64 bytes from app1-nginx-service.app1.svc.cluster.local (10.68.101.60): icmp_seq=1 ttl=64 time=0.103 ms
64 bytes from app1-nginx-service.app1.svc.cluster.local (10.68.101.60): icmp_seq=2 ttl=64 time=0.314 ms
64 bytes from app1-nginx-service.app1.svc.cluster.local (10.68.101.60): icmp_seq=3 ttl=64 time=0.077 ms

  而且nginx及tomcat中最好将日志格式改为json格式,方便我们收集日志。
  tomcat的配置文件/usr/local/tomcat/conf/server.xml,设置appBase="/data/tomcat/webapps/,日志格式修改如下

1
2
3
<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
prefix="localhost_access_log" suffix=".txt"
pattern="{&quot;clientip&quot;:&quot;%h&quot;,&quot;ClientUser&quot;:&quot;%l&quot;,&quot;authenticated&quot;:&quot;%u&quot;,&quot;AccessTime&quot;:&quot;%t&quot;,&quot;method&quot;:&quot;%r&quot;,&quot;status&quot;:&quot;%s&quot;,&quot;SendBytes&quot;:&quot;%b&quot;,&quot;Query?string&quot;:&quot;%q&quot;,&quot;partner&quot;:&quot;%{Referer}i&quot;,&quot;AgentVersion&quot;:&quot;%{User-Agent}i&quot;}" />

PV/PVC

  我们还可以通过创建一个创建PV/PVC的方式的方式来代替直接NFS挂载Volume。
  PersistentVolume(PV)是集群中由管理员配置的一段网络存储。 它是集群中的资源,就像节点是集群资源一样。 PV是容量插件,如Volumes,但其生命周期独立于使用PV的任何单个pod。 此API对象捕获存储实现的详细信息,包括NFS,iSCSI或特定于云提供程序的存储系统。
  PersistentVolumeClaim(PVC)是由用户进行存储的请求。 它类似于pod。 Pod消耗节点资源,PVC消耗PV资源。Pod可以请求特定级别的资源(CPU和内存)。声明可以请求特定的大小和访问模式(例如,可以一次读/写或多次只读)。
  PVC和PV是一一对应的。Persistent Volume相对独立于Pods,单独创建。
  Persistent Volume对具体的存储进行配置和分配,而Pods等则可以使用Persistent Volume抽象出来的存储资源,不需要知道集群的存储细节。
  Persistent Volume和Persistent Volume Claim类似Pods和Nodes的关系,创建Pods需要消耗一定的Nodes的资源。而Persistent Volume则是提供了各种存储资源,而Persistent Volume Claim提出需要的存储标准,然后从现有存储资源中匹配或者动态建立新的资源,最后将两者进行绑定。
  以我们这次的演示为例,PV和PVC如下所示。
  PV的yaml文件示例

1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: v1
kind: PersistentVolume
metadata:
name: nfs
spec:
storageClassName: manual
capacity:
storage: 10Gi
accessModes:
- ReadWriteMany
nfs:
server: 192.168.32.19
path: "/data/app1/static"

  PVC的yaml文件示例

1
2
3
4
5
6
7
8
9
10
11
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nfs
spec:
accessModes:
- ReadWriteMany
storageClassName: manual
resources:
requests:
storage: 10Gi

  pod中引用PVC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
apiVersion: v1
kind: Pod
metadata:
name: testpv
labels:
role: web-frontend
spec:
containers:
- name: web
image: nginx
ports:
- name: web
containerPort: 80
volumeMounts:
- name: nfs
mountPath: "/usr/local/nginx/html/webapp/static"
volumes:
- name: nfs
persistentVolumeClaim:
claimName: nfs

HPA

  虽然我们可以使用命令kubectl scale对运行在k8s 环境中的pod 数量进行扩容(增加)或缩容(减小)。

1
2
3
4
5
6
7
8
9
10
11
12
root@DockerUbuntu18:~# kubectl --help | grep -w scale
scale Set a new size for a Deployment, ReplicaSet, Replication Controller, or Job
root@DockerUbuntu18:~# kubectl get deployments -n app1
NAME READY UP-TO-DATE AVAILABLE AGE
app1-nginx-deployment 1/1 1 1 9d
app1-tomcat-deployment 1/1 1 1 3d4h
root@DockerUbuntu18:~# kubectl scale deployment/app1-tomcat-deployment --replicas=2 -n app1
deployment.extensions/app1-tomcat-deployment scaled
root@DockerUbuntu18:~# kubectl get deployments -n app1
NAME READY UP-TO-DATE AVAILABLE AGE
app1-nginx-deployment 1/1 1 1 9d
app1-tomcat-deployment 2/2 2 2 74s

  不过在实际生产中,我们肯定没法随时根据负载或者服务需要,手动动态伸缩我们的后端服务器数量,但是我们可以通过命令kubectl autoscale自动控制在k8s集群中运行的pod数量(水平自动伸缩),想要实现自动上伸缩,需要我们提前设置pod范围及触发条件。
  k8s从1.1版本开始增加了名称为HPA(Horizontal Pod Autoscaler)的控制器,用于实现基于pod中资源(CPU/Memory)利用率进行对pod的自动扩缩容功能的实现,早期的版本只能基于Heapster组件实现对CPU利用率做为触发条件,但是在k8s 1.11版本开始使用Metrices Server完成数据采集,然后将采集到的数据通过API(Aggregated API,汇总API),例如metrics.k8s.io、custom.metrics.k8s.io、external.metrics.k8s.io,然后再把数据提供给HPA控制器进行查询,以实现基于某个资源利用率对pod进行扩缩容的目的。
  控制管理器默认每隔15s(可以通过–horizontal-pod-autoscaler-sync-period修改)查询metrics的资源使用情况
支持以下三种metrics指标类型:
  ->预定义metrics(比如Pod的CPU)以利用率的方式计算
  ->自定义的Pod metrics,以原始值(raw value)的方式计算
  ->自定义的object metrics
支持两种metrics查询方式:
  ->Heapster
  ->自定义的REST API
支持多metrics

(未完待续)


一个低调的男人