k8s应用综合实例


k8s应用综合实例

image-20240310093029189

目录

[toc]

原文链接

https://onedayxyy.cn/docs/k8s-wordpress-example

image-20240310093140691

推荐文章

我的开源项目:

https://onedayxyy.cn/docs/MyOpenSourceProject

image-20240305125631154

实验环境

k8s集群环境:
    k8s v1.28.7(sealos部署的1master,2work节点)
    containerd v1.7.13
    cilium v1.14.7 (cni)
    节点内核版本:5.4.270-1.el7.elrepo.x86_64

应用环境:
	wordpress:6.4.3-apache
    mysql:5.7

实验软件

链接:https://pan.baidu.com/s/1I4xASUsrrnsgEHXDH22NgQ?pwd=mlpe
提取码:mlpe
2024.3.9-k8s应用综合实例

image-20240310070658504

本节实战

实战名称
配置Pod反向亲和性、PDB、健康检查、QoS、preStop` 生命周期钩子、hpa、secret、持久化(nfs、pvc)、ingress。

预期

本节我们将用一个 Wordpress 示例来尽可能将 Kubernetes 的基础知识串联起来,我们需要达到的目的是让 Wordpress 应用具有高可用、滚动更新的过程中不能中断服务、数据要持久化不能丢失、当应用负载太高的时候能够自动进行扩容、当然还有 HTTPS 访问等等,这些是我们的应用部署到线上环境基本上要具备的一些能力,接下来我们就来一步一步完成这些需求。

原理

​ 首先要部署 Wordpress 应用,我们肯定需要知道 Wordpress 是如何运行起来的,Wordpress 是一个基于 PHP 和 MySQL 的流行的开源内容管理系统,拥有丰富的插件和模板系统。到这里我们应该就清楚应该如何去运行 Wordpress 了,一个能够解析 PHP 的程序,和 MySQL 数据库就可以了。

​ 我们要想在 Kubernetes 系统中来运行,肯定需要使用到 Docker 镜像了,对于 Wordpress 应用程序本身官方提供了镜像 https://hub.docker.com/_/wordpress,也给出了说明如何运行,可以通过一系列环境变量去指定 MySQL 数据库的配置,只需要将这些参数配置上直接运行即可。

​ 我们知道 Wordpress 应用本身会频繁的和 MySQL 数据库进行交互,这种情况下如果将二者用容器部署在同一个 Pod 下面是不是要高效很多,因为一个 Pod 下面的所有容器是共享同一个 network namespace 的。

==尝试:将php和mysql服务放在一1个pod里==

下面我们就来部署我们的应用,将我们的应用都部署到 kube-example 这个命名空间下面。

  • 所以首先创建一个命名空间:(namespace.yaml)
apiVersion: v1
kind: Namespace
metadata:
  name: kube-example

本次使用命令行:

[root@master ~]#kubectl create ns kube-example
namespace/kube-example created

[root@master ~]#kubectl get ns kube-example 
NAME           STATUS   AGE
kube-example   Active   31s
[root@master ~]#
  • 然后编写部署到 Kubernetes 下面的资源清单:
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: wordpress
  namespace: kube-example
  labels:
    app: wordpress
spec:
  selector:
    matchLabels:
      app: wordpress
  template:
    metadata:
      labels:
        app: wordpress
    spec:
      containers:
        - name: wordpress
          image: wordpress:6.1.1-apache
          ports:
            - containerPort: 80
              name: wdport
          env:
            - name: WORDPRESS_DB_HOST
              value: localhost:3306
            - name: WORDPRESS_DB_USER
              value: wordpress
            - name: WORDPRESS_DB_PASSWORD
              value: wordpress
        - name: mysql
          image: mysql:5.7
          imagePullPolicy: IfNotPresent
          args: # 新版本镜像有更新,需要使用下面的认证插件环境变量配置才会生效!
            - --default_authentication_plugin=mysql_native_password
            - --character-set-server=utf8mb4
            - --collation-server=utf8mb4_unicode_ci
          ports:
            - containerPort: 3306
              name: dbport
          env:
            - name: MYSQL_ROOT_PASSWORD
              value: rootPassW0rd
            - name: MYSQL_DATABASE
              value: wordpress
            - name: MYSQL_USER
              value: wordpress
            - name: MYSQL_PASSWORD
              value: wordpress

由于我们这里 MySQL 和 Wordpress 在同一个 Pod 下面,所以在 Wordpress 中我们指定数据库地址的时候是用的 localhost:3306因为这两个容器已经共享同一个 network namespace 了,这点很重要。

  • 然后如果我们要想把这个服务暴露给外部用户还得创建一个 Service 或者 Ingress 对象,这里我们一步一步来,暂时先创建一个 NodePort 类型的 Service:
# service.yaml
apiVersion: v1
kind: Service
metadata:
  name: wordpress
  namespace: kube-example
spec:
  selector:
    app: wordpress
  type: NodePort
  ports:
    - name: web
      port: 80
      targetPort: wdport

因为只需要暴露 Wordpress 这个应用,所以只匹配了一个名为 wdport 的端口。

  • 现在我们来创建上面的几个资源对象:
#$ kubectl apply -f namespace.yaml
$ kubectl apply -f deployment.yaml
$ kubectl apply -f service.yaml
  • 接下来就是等待拉取镜像,启动 Pod:
[root@master ~]#kubectl get pods -n kube-example
NAME                         READY   STATUS    RESTARTS   AGE
wordpress-7dc9f8748b-sb44t   2/2     Running   0          118m
[root@master ~]# kubectl get svc -n kube-example
NAME        TYPE       CLUSTER-IP    EXTERNAL-IP   PORT(S)        AGE
wordpress   NodePort   10.96.2.145   <none>        80:31409/TCP   119m
[root@master ~]#
  • 测试

当 Pod 启动完成后,我们就可以通过上面的 http://<任意节点IP>:31230 这个 NodePort 端口来访问应用了。

我们仔细想一想这一种方式有什么问题?

首先一个 Pod 中的所有容器并没有启动的先后顺序,所以很有可能当 wordpress 这个容器启动起来去连接 mysql 这个容器的时候,mysql 还没有启动起来;

另外一个问题是现在我们的应用是不是只有一个副本?会有单点问题,应用的性能也是一个问题,由于 Wordpress 应用本身是无状态应用,所以这种情况下一般我们只需要多部署几个副本即可,比如这里我们在 Deployment 的 YAML 文件中加上 replicas:3 这个属性,这个时候有一个什么问题呢?

由于 MySQL 是有状态应用,每一个 Pod 里面的数据库的数据都是独立的,他们并没有共享,也就是说这 3 个 Pod 相当于是独立的 3 个 Wordpress 实例,所以应该怎么办呢?

拆分,把 Wordpress 和 MySQL 这两个容器部署成独立的 Pod 就可以了,这样我们只需要对 Wordpress 应用增加副本,而数据库 MySQL 还是一个实例,所有的应用都连接到这一个数据库上面,是不是就可以解决这个问题了。

==最终方案:将php和mysql服务放在2个pod里==

高可用

现在我们将 Pod 中的两个容器进行拆分,将 Wordpress 和 MySQL 分别部署,然后 Wordpress 用多个副本进行部署就可以实现应用的高可用了。由于 MySQL 是有状态应用,一般来说需要用 StatefulSet 来进行管理,但是我们这里部署的 MySQL 并不是集群模式,而是单副本的,所以用 Deployment 也是没有问题的,当然如果要真正用于生产环境还是需要集群模式的

  • 测试之前,先删除刚才创建的资源
[root@master ~]#kubectl delete -f deployment.yaml
deployment.apps "wordpress" deleted
[root@master ~]#kubectl delete -f service.yaml 
service "wordpress" deleted
[root@master ~]#
  • 配置资源
# mysql.yaml
apiVersion: v1
kind: Service
metadata:
  name: mysql
  namespace: kube-example
  labels:
    app: mysql
spec:
  ports:
    - port: 3306
      targetPort: dbport
  selector:
    app: mysql
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mysql
  namespace: kube-example
  labels:
    app: mysql
spec:
  selector:
    matchLabels:
      app: mysql
  template:
    metadata:
      labels:
        app: mysql
    spec:
      containers:
        - name: mysql
          image: mysql:5.7
          imagePullPolicy: IfNotPresent
          args: # 新版本镜像有更新,需要使用下面的认证插件环境变量配置才会生效
            - --default_authentication_plugin=mysql_native_password
            - --character-set-server=utf8mb4
            - --collation-server=utf8mb4_unicode_ci
          ports:
            - containerPort: 3306
              name: dbport
          env:
            - name: MYSQL_ROOT_PASSWORD
              value: rootPassW0rd
            - name: MYSQL_DATABASE
              value: wordpress
            - name: MYSQL_USER
              value: wordpress
            - name: MYSQL_PASSWORD
              value: wordpress

我们这里给 MySQL 应用添加了一个 Service 对象,是因为 Wordpress 应用需要来连接数据库,之前在同一个 Pod 中用 localhost 即可,现在需要通过 Service 的 DNS 形式的域名进行连接

  • 直接创建上面资源对象:
$ kubectl apply -f mysql.yaml
service/mysql created
statefulset.apps/mysql created

[root@master ~]#kubectl get sts,pod,svc -nkube-example
NAME                     READY   AGE
statefulset.apps/mysql   1/1     36s

NAME          READY   STATUS    RESTARTS   AGE
pod/mysql-0   1/1     Running   0          36s

NAME            TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)    AGE
service/mysql   ClusterIP   10.96.2.26   <none>        3306/TCP   36s
[root@master ~]#
  • 接下来创建独立的 Wordpress 服务,对应的资源对象如下:
# wordpress.yaml
apiVersion: v1
kind: Service
metadata:
  name: wordpress
  namespace: kube-example
  labels:
    app: wordpress
spec:
  selector:
    app: wordpress
    tier: frontend
  type: NodePort
  ports:
    - name: web
      port: 80
      targetPort: wdport
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: wordpress
  namespace: kube-example
  labels:
    app: wordpress
    tier: frontend
spec:
  replicas: 2
  selector:
    matchLabels:
      app: wordpress
      tier: frontend
  template:
    metadata:
      labels:
        app: wordpress
        tier: frontend
    spec:
      containers:
        - name: wordpress
          image: wordpress:6.4.3-apache
          ports:
            - containerPort: 80
              name: wdport
          env:
            - name: WORDPRESS_DB_HOST
              value: mysql:3306
            - name: WORDPRESS_DB_USER
              value: wordpress
            - name: WORDPRESS_DB_PASSWORD
              value: wordpress

注意这里的环境变量 WORDPRESS_DB_HOST 的值将之前的 localhost 地址更改成了上面 MySQL 服务的 DNS 地址,完整的域名应该是 mysql.kube-example.svc.cluster.local:3306,由于这两个应该都处于同一个命名空间,所以直接简写成 mysql:3306 也是可以的。

  • 创建上面资源对象:
$ kubectl apply -f wordpress.yaml
service/wordpress created
deployment.apps/wordpress created

[root@master ~]#kubectl get pods -l app=wordpress -n kube-example
NAME                         READY   STATUS    RESTARTS   AGE
mysql-0                      1/1     Running   0          2m17s
wordpress-8449b9b9d6-4pl67   1/1     Running   0          55s
wordpress-8449b9b9d6-kp4p7   1/1     Running   0          55s
[root@master ~]#

可以看到都已经是 Running 状态了,然后我们需要怎么来验证呢?

  • 是不是我们能想到的就是去访问下我们的 Wordpress 服务就可以了,我们这里还是使用的一个 NodePort 类型的 Service 来暴露服务:
[root@master ~]#kubectl get svc -l app=wordpress -n kube-example
NAME        TYPE        CLUSTER-IP    EXTERNAL-IP   PORT(S)        AGE
mysql       ClusterIP   10.96.2.26    <none>        3306/TCP       2m45s
wordpress   NodePort    10.96.1.213   <none>        80:31753/TCP   83s
[root@master ~]#

可以看到 wordpress 服务产生了一个 32411 的端口,现在我们就可以通过 http://<任意节点的NodeIP>:32411 访问我们的应用了,在浏览器中打开,如果看到 wordpress 跳转到了安装页面,证明我们的部署是正确的,如果没有出现预期的效果,那么就需要去查看下 Pod 的日志来排查问题了。

image-20240307212712193

image-20240307212736957

image-20240307212905739

  • 根据页面提示,填上对应的信息,点击“安装”即可,最终安装成功后,我们就可以看到熟悉的首页界面了:

image-20240307212938321

稳定性

现在 Wodpress 应用已经部署成功了,那么就万事大吉了吗?如果我们的网站访问量突然变大了怎么办,如果我们要更新我们的镜像该怎么办?所以要保证我们的网站能够非常稳定的提供服务,我们做得还不够,我们可以通过做些什么事情来提高网站的稳定性呢?

避免单点故障

为什么会有单点故障的问题呢?我们不是部署了多个副本的 Wordpress 应用吗?当我们设置 replicas=1 的时候肯定会存在单点故障问题,如果大于 1 但是所有副本都调度到了同一个节点的是不是同样就会存在单点问题了,这个节点挂了所有副本就都挂了,所以我们不仅需要设置多个副本数量,还需要让这些副本调度到不同的节点上,来打散避免单点故障。这个利用 Pod 反亲和性来实现了。

  • 我们可以加上如下所示的配置:
[root@master ~]#vim wordpress.yaml
……
# 软策略,本次后面测试就用软策略了哈。
affinity:
  podAntiAffinity:
    preferredDuringSchedulingIgnoredDuringExecution: # 软策略
      - weight: 1
        podAffinityTerm:
          topologyKey: kubernetes.io/hostname
          labelSelector:
            matchExpressions:
              - key: app
                operator: In
                values:
                  - wordpress
                  
                  
                  
……
# 硬策略
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution: # 硬策略
            - labelSelector:
                matchExpressions:
                  - key: app
                    operator: In
                    values:
                      - wordpress
              topologyKey: kubernetes.io/hostname      

image-20240307215630211

这里的意思就是如果一个节点上面有 app=wordpress 这样的 Pod 的话,那么我们的 Pod 就尽可能别调度到这个节点上面来,因为我们这里的节点并不多,所以我使用的是软策略,==因为如果使用硬策略的话,如果应用副本数超过了节点数就必然会有 Pod 调度不成功==。如果你线上节点非常多的话(节点数大于 Pod 副本数),建议使用硬策略,更新后我们可以查看下 3 个副本被分散在了不同的节点上。

[root@master ~]#kubectl get po -nkube-example -owide
NAME                         READY   STATUS    RESTARTS   AGE   IP           NODE    NOMINATED NODE   READINESS GATES
mysql-0                      1/1     Running   0          46m   10.0.2.138   node1   <none>           <none>
wordpress-788cbc7dff-njbm8   1/1     Running   0          8s    10.0.1.106   node2   <none>           <none>
wordpress-788cbc7dff-pcrv5   1/1     Running   0          8s    10.0.2.141   node1   <none>           <none>
wordpress-788cbc7dff-tnqhn   1/1     Running   0          8s    10.0.1.41    node2   <none>           <none>
[root@master ~]#

🚩 特别注意:

后续的实验,因为这里配置了反亲和性,所以需要先删除资源,再重新创建资源的哈。

使用 PDB

有些时候线上的某些节点需要做一些维护操作,比如要升级内核,这个时候我们就需要将要维护的节点进行驱逐操作,驱逐节点首先是将节点设置为不可调度,这样可以避免有新的 Pod 调度上来,然后将该节点上的 Pod 全部删除,ReplicaSet 控制器检测到 Pod 数量减少了就会重新创建一个新的 Pod,调度到其他节点上面的,这个过程是先删除,再创建,并非是滚动更新,因此更新过程中,如果一个服务的所有副本都在被驱逐的节点上,则可能导致该服务不可用。

如果服务本身存在单点故障,所有副本都在同一个节点,驱逐的时候肯定就会造成服务不可用了,这种情况我们使用上面的反亲和性和多副本就可以解决这个问题。

但是如果我们的服务本身就被打散在多个节点上,这些节点如果都被同时驱逐的话,那么这个服务的所有实例都会被同时删除,这个时候也会造成服务不可用了,这种情况下我们可以通过配置 PDB(PodDisruptionBudget)对象来避免所有副本同时被删除。比如我们可以设置在驱逐的时候 wordpress 应用最多只有一个副本不可用,其实就相当于逐个删除并在其它节点上重建:

  • 创建资源清单
# pdb.yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: wordpress-pdb
  namespace: kube-example
spec:
  maxUnavailable: 1
  selector:
    matchLabels:
      app: wordpress
      tier: frontend
  • 直接创建这个资源对象即可:
$ kubectl apply -f pdb.yaml
poddisruptionbudget.policy/wordpress-pdb created
$ kubectl get pdb -n kube-example
NAME            MIN AVAILABLE   MAX UNAVAILABLE   ALLOWED DISRUPTIONS   AGE
wordpress-pdb   N/A             1                 1                     9s

关于 PDB 的更多详细信息可以查看官方文档:https://kubernetes.io/docs/tasks/run-application/configure-pdb/。

健康检查

​ 我们的应用现在还有一个非常重要的功能没有提供,那就是健康检查,我们知道健康检查是提高应用健壮性非常重要的手段,当我们检测到应用不健康的时候我们希望可以自动重启容器,当应用还没有准备好的时候我们也希望暂时不要对外提供服务,所以我们需要添加我们前面经常提到的 liveness proberediness probe 两个健康检测探针。

​ 检查探针的方式有很多,我们这里当然可以认为如果容器的 80 端口可以成功访问那么就是健康的,对于一般的应用提供一个健康检查的 URL 会更好。这里我们添加一个如下所示的可读性探针,为什么不添加存活性探针呢?这里其实是考虑到线上错误排查的一个问题,如果当我们的应用出现了问题,然后就自动重启去掩盖错误的话,可能这个错误就会被永远忽略掉了,所以其实这是一个折衷的做法。**不使用存活性探针,而是结合监控报警,保留错误现场,方便错误排查,但是可读写探针是一定需要添加的**:

#wordpress.yaml
……
readinessProbe:
  tcpSocket:
    port: 80
  initialDelaySeconds: 5
  periodSeconds: 5

增加上面的探针,每 5s 检测一次应用是否可读,这样只有当 readinessProbe 探针检测成功后才表示准备好接收流量了,这个时候才会更新 Service 的 Endpoints 对象。

image-20240308144446773

服务质量 QoS

QoSQuality of Service 的缩写,即服务质量。为了实现资源被有效调度和分配的同时提高资源利用率,Kubernetes 针对不同服务质量的预期,通过 QoS 来对 Pod 进行服务质量管理。对于一个 Pod 来说,服务质量体现在两个具体的指标:CPU 和内存。当节点上内存资源紧张时,Kubernetes 会根据预先设置的不同 QoS 类别进行相应处理。

QoS 主要分为 GuaranteedBurstableBest-Effort三类,优先级从高到低。我们先分别来介绍下这三种服务类型的定义。

QoS类型

==1、Guaranteed(有保证的)==

属于该级别的 Pod 有以下两种:

  • Pod 中的所有容器都且设置了 CPU 和内存的 limits
  • Pod 中的所有容器都设置了 CPU 和内存的 requests 和 limits ,且单个容器内的requests==limits(requests 不等于 0)

Pod 中的所有容器都且仅设置了 limits,如下所示:

containers:
  - name: foo
    resources:
      limits:
        cpu: 10m
        memory: 1Gi
  - name: bar
    resources:
      limits:
        cpu: 100m
        memory: 100Mi

Pod 中的所有容器都设置了 requests 和 limits,且单个容器内的 requests==limits 的情况:

containers:
  - name: foo
    resources:
      limits:
        cpu: 10m
        memory: 1Gi
      requests:
        cpu: 10m
        memory: 1Gi
  - name: bar
    resources:
      limits:
        cpu: 100m
        memory: 100Mi
      requests:
        cpu: 100m
        memory: 100Mi

容器 foo 和 bar 内 resourcesrequestslimits 均相等,该 Pod 的 QoS 级别属于 Guaranteed

==2、Burstable(不稳定的)==

Pod 中只要有一个容器的 requests 和 limits 的设置不相同,那么该 Pod 的 QoS 即为 Burstable。如下所示容器 foo 指定了 resource,而容器 bar 未指定:

containers:
  - name: foo
    resources:
      limits:
        cpu: 10m
        memory: 1Gi
      requests:
        cpu: 10m
        memory: 1Gi
  - name: bar

容器 foo 设置了内存 limits,而容器 bar 设置了 CPU limits:

containers:
  - name: foo
    resources:
      limits:
        memory: 1Gi
  - name: bar
    resources:
      limits:
        cpu: 100m

需要注意的是如果容器指定了 requests 而未指定 limits,则 limits 的值等于节点资源的最大值,如果容器指定了 limits 而未指定 requests,则 requests 的值等于 limits。

==3、Best-Effort(尽最大努力)==

如果 Pod 中所有容器的 resources 均未设置 requests 与 limits,该 Pod 的 QoS 即为 Best-Effort。如下所示容器 foo 和容器 bar 均未设置 requests 和 limits:

containers:
  - name: foo
    resources:
  - name: bar
    resources:

资源回收策略

Kubernetes 通过 CGroup 给 Pod 设置 QoS 级别,当资源不足时会优先 kill 掉优先级低的 Pod,在实际使用过程中,通过 OOM 分数值来实现,OOM 分数值范围为 0-1000,OOM 分数值根据 OOM_ADJ参数计算得出。

对于 Guaranteed 级别的 Pod,OOM_ADJ 参数设置成了-998,对于 Best-Effort 级别的 Pod,OOM_ADJ 参数设置成了1000,对于 Burstable 级别的 Pod,OOM_ADJ 参数取值从 2 到 999。

QoS Pods 被 kill 掉的场景和顺序如下所示:

  • Best-Effort Pods:系统用完了全部内存时,该类型 Pods 会最先被 kill 掉
  • Burstable Pods:系统用完了全部内存,且没有 Best-Effort 类型的容器可以被 kill 时,该类型的 Pods 会被 kill 掉
  • Guaranteed Pods:系统用完了全部内存,且没有 Burstable 与 Best-Effort 类型的容器可以被 kill 时,该类型的 pods 会被 kill 掉

所以如果资源充足,可将 QoS Pods 类型设置为 Guaranteed,用计算资源换业务性能和稳定性,减少排查问题时间和成本。如果想更好的提高资源利用率,业务服务可以设置为 Guaranteed,而其他服务根据重要程度可分别设置为 Burstable 或 Best-Effort,这就要看具体的场景了。

比如我们这里如果想要尽可能提高 Wordpress 应用的稳定性,我们可以将其设置为 Guaranteed 类型的 Pod,我们现在没有设置 resources 资源,所以现在是 Best-Effort 类型的 Pod。

现在如果要想给应用设置资源大小,就又有一个问题了,应该如何设置合适的资源大小呢?其实这就需要我们对自己的应用非常了解才行了,一般情况下我们可以先不设置资源,然后可以根据我们的应用的并发和访问量来进行压力测试,基本上可以大概计算出应用的资源使用量。

==🚩 Fortio压测工具安装文档==

https://onedayxyy.cn/docs/Fortio

image-20240309165708521

我们这里使用 Apache Bench(AB Test) 或者 Fortio(Istio 测试工具) 这样的测试工具来测试,我们这里使用 Fortio 这个测试工具,比如每秒 1000 个请求和 8 个并发的连接的测试命令如下所示:

$ fortio load -a -c 5 -qps 1000 -t 60s "http://k8s.youdianzhishi.com:32411"
16:42:13 I scli.go:88> Starting Φορτίο 1.53.1 h1:aeAWrnkXIBEdhueQLRwvaM5INulybDP+ZgOChgKz/NQ= go1.19.1 arm64 darwin
Fortio 1.53.1 running at 1000 queries per second, 10->10 procs, for 1m0s: http://k8s.youdianzhishi.com:32411
16:42:13 I httprunner.go:99> Starting http test for http://k8s.youdianzhishi.com:32411 with 5 threads at 1000.0 qps and parallel warmup
Starting at 1000 qps with 5 thread(s) [gomax 10] for 1m0s : 12000 calls each (total 60000)
# ......
Sockets used: 19 (for perfect keepalive, would be 5)
Uniform: false, Jitter: false, Catchup allowed: true
IP addresses distribution:
43.137.1.207:32411: 19
Code  -1 : 14 (8.4 %)
Code 200 : 153 (91.6 %)
Response Header Sizes : count 167 avg 290.86228 +/- 45.56 min 0 max 298 sum 48574
Response Body/Total Sizes : count 167 avg 50216.443 +/- 9456 min 0 max 52409 sum 8386146
All done 167 calls (plus 5 warmup) 1837.452 ms avg, 2.7 qps
Successfully wrote 6213 bytes of Json data to 2023-03-14-164213_k8s_youdianzhishi_com_32411_bogon.json

也可以通过浏览器(执行 fortio server 命令)查看到最终测试结果:

img

在测试期间我们可以用如下所示的命令查看应用的资源使用情况:

$ kubectl top pods -l app=wordpress -n kube-example
NAME                         CPU(cores)   MEMORY(bytes)
mysql-0                      15m          213Mi
wordpress-55b57b745f-f28tr   88m          95Mi
wordpress-55b57b745f-gn8jc   89m          90Mi
wordpress-55b57b745f-r7x2w   90m          94Mi

我们可以看到内存基本上都是处于 100Mi 以内,CPU 消耗也很小,但是由于 CPU 是可压缩资源,也就是说超过了限制应用也不会挂掉的,只是会变慢而已。所以我们这里可以给 Wordpress 应用添加如下所示的资源配置,如果你集群资源足够的话可以适当多分配一些资源:

resources:
  limits:
    cpu: 150m
    memory: 150Mi
  requests:
    cpu: 150m
    memory: 150Mi

image-20240308151313221

配置后更新应用。

滚动更新

Deployment 控制器默认的就是滚动更新的更新策略,该策略可以在任何时间点更新应用的时候保证某些实例依然可以正常运行来防止应用 down 掉,当新部署的 Pod 启动并可以处理流量之后,才会去杀掉旧的 Pod。在使用过程中我们还可以指定 Kubernetes 在更新期间如何处理多个副本的切换方式,比如我们有一个 3 副本的应用,在更新的过程中是否应该立即创建这 3 个新的 Pod 并等待他们全部启动,或者杀掉一个之外的所有旧的 Pod,或者还是要一个一个的 Pod 进行替换?

如果我们从旧版本到新版本进行滚动更新,只是简单的通过输出显示来判断哪些 Pod 是存活并准备就绪的,那么这个滚动更新的行为看上去肯定就是有效的,但是往往实际情况就是从旧版本到新版本的切换的过程并不总是十分顺畅的,应用程序很有可能会丢弃掉某些客户端的请求。比如我们在 Wordpress 应用中添加上如下的滚动更新策略,随便更改以下 Pod Template 中的参数,比如容器名更改为 blog:

strategy:
  type: RollingUpdate
  rollingUpdate:
    maxSurge: 1
    maxUnavailable: 0

image-20240308155600540

然后更新应用,同时用 Fortio 工具在滚动更新过程中来测试应用是否可用:

自己这里没亲自做测试哦!🤣

$ kubectl apply -f wordpress.yaml

$ fortio load -a -c 5 -qps 500 -t 60s "http://wordpress.onedayxyy.com:31122"
# ......
Sockets used: 33 (for perfect keepalive, would be 5)
Uniform: false, Jitter: false, Catchup allowed: true
IP addresses distribution:
43.137.1.207:32411: 33
Code  -1 : 25 (14.0 %)
Code 200 : 153 (86.0 %)
Response Header Sizes : count 178 avg 295.18539 +/- 31.62 min 0 max 317 sum 52543
Response Body/Total Sizes : count 178 avg 48525.348 +/- 1.101e+04 min 0 max 52428 sum 8637512
All done 178 calls (plus 5 warmup) 1710.081 ms avg, 2.9 qps
Successfully wrote 7184 bytes of Json data to 2023-03-14-165414_k8s_youdianzhishi_com_32411_bogon.json

从上面的输出可以看出有部分请求处理失败了(502),要弄清楚失败的原因就需要弄明白当应用在滚动更新期间重新路由流量时,从旧的 Pod 实例到新的实例究竟会发生什么,首先让我们先看看 Kubernetes 是如何管理工作负载连接的。

失败原因

我们这里通过 NodePort 去访问应用,实际上也是通过每个节点上面的 kube-proxy 通过更新 iptables 规则来实现的。

img

Kubernetes 会根据 Pods 的状态去更新 Endpoints 对象,这样就可以保证 Endpoints 中包含的都是准备好处理请求的 Pod。一旦新的 Pod 处于活动状态并准备就绪后,Kubernetes 就将会停止旧的 Pod,从而将 Pod 的状态更新为 “Terminating”,然后从 Endpoints 对象中移除,并且发送一个 SIGTERM 信号给 Pod 的主进程。SIGTERM 信号就会让容器以正常的方式关闭,并且不接受任何新的连接。Pod 从 Endpoints 对象中被移除后,前面的负载均衡器就会将流量路由到其他(新的)Pod 中去。因为在负载均衡器注意到变更并更新其配置之前,终止信号就会去停用 Pod,而这个重新配置过程又是异步发生的,并不能保证正确的顺序,所以就可能导致很少的请求会被路由到已经终止的 Pod 上去了,也就出现了上面我们说的情况。

零宕机

那么如何增强我们的应用程序以实现真正的零宕机迁移更新呢?

首先,要实现这个目标的先决条件是我们的容器要正确处理终止信号,在 SIGTERM 信号上实现优雅关闭。下一步需要添加 readiness 可读探针,来检查我们的应用程序是否已经准备好来处理流量了。为了解决 Pod 停止的时候不会阻塞并等到负载均衡器重新配置的问题,我们还需要使用 preStop 这个生命周期的钩子,在容器终止之前调用该钩子。

生命周期钩子函数是同步的,所以必须在将最终停止信号发送到容器之前完成,在我们的示例中,我们使用该钩子简单的等待,然后 SIGTERM 信号将停止应用程序进程。同时,Kubernetes 将从 Endpoints 对象中删除该 Pod,所以该 Pod 将会从我们的负载均衡器中排除,基本上来说我们的生命周期钩子函数等待的时间可以确保在应用程序停止之前重新配置负载均衡器:

readinessProbe:
  # ...
lifecycle:
  preStop:
    exec:
      command: ["/bin/bash", "-c", "sleep 20"]

image-20240308155628717

我们这里使用 preStop 设置了一个 20s 的宽限期,Pod 在真正销毁前会先 sleep 等待 20s,这就相当于留了时间给 Endpoints 控制器和 kube-proxy 更新去 Endpoints 对象和转发规则,这段时间 Pod 虽然处于 Terminating 状态,即便在转发规则更新完全之前有请求被转发到这个 Terminating 的 Pod,依然可以被正常处理,因为它还在 sleep,没有被真正销毁。

现在,当我们去查看滚动更新期间的 Pod 行为时,我们将看到正在终止的 Pod 处于 Terminating 状态,但是在等待时间结束之前不会关闭的,如果我们使用 Fortio 重新测试下,则会看到零失败请求的理想状态。

HPA

现在应用是固定的 3 个副本,但是往往在生产环境流量是不可控的,很有可能一次活动就会有大量的流量,3 个副本很有可能抗不住大量的用户请求,这个时候我们就希望能够自动对 Pod 进行伸缩,直接使用前面我们学习的 HPA 这个资源对象就可以满足我们的需求了。

本次,因本地没fortio工具,这里不做演示。

  • 直接使用kubectl autoscale命令来创建一个 HPA 对象
$ kubectl autoscale deployment wordpress --namespace kube-example --cpu-percent=80 --min=3 --max=8
horizontalpodautoscaler.autoscaling/hpa-demo autoscaled

$ kubectl get hpa -n kube-example
NAME        REFERENCE              TARGETS         MINPODS   MAXPODS   REPLICAS   AGE
wordpress   Deployment/wordpress   <unknown>/80%   3         8         0          4s

此命令创建了一个关联资源 wordpress 的 HPA,最小的 Pod 副本数为 3,最大为 8。HPA 会根据设定的 cpu 使用率(80%)动态的增加或者减少 Pod 数量。

  • 同样,使用上面的 Fortio 工具来进行压测一次,看下能否进行自动的扩缩容:
$ fortio load -a -c 8 -qps 1000 -t 60s "http://k8s.youdianzhishi.com:32411"

$ fortio load -a -c 8 -qps 1000 -t 60s "http://wordpress.onedayxyy.com:30310"
  • 在压测的过程中我们可以看到 HPA 的状态变化以及 Pod 数量也变成了 6 个:
$ kubectl get hpa -n kube-example
NAME        REFERENCE              TARGETS   MINPODS   MAXPODS   REPLICAS   AGE
wordpress   Deployment/wordpress   98%/20%   3         6         6          2m40s
$ kubectl get pods -n kube-example
NAME                              READY   STATUS    RESTARTS   AGE
wordpress-79d756cbc8-f6kfm        1/1     Running   0          21m
wordpress-79d756cbc8-kspch        1/1     Running   0          32s
wordpress-79d756cbc8-sf5rm        1/1     Running   0          32s
wordpress-79d756cbc8-tsjmf        1/1     Running   0          20m
wordpress-79d756cbc8-v9p7n        1/1     Running   0          32s
wordpress-79d756cbc8-z4wpp        1/1     Running   0          21m
wordpress-mysql-5756ccc8b-zqstp   1/1     Running   0          3d19h

当压测停止以后==正常 5 分钟后==就会自动进行缩容,变成最小的 3 个 Pod 副本。

安全性

安全性这个和具体的业务应用有关系,比如我们这里的 Wordpress 也就是数据库的密码属于比较私密的信息。

  • 我们可以使用 Kubernetes 中的 Secret 资源对象来存储比较私密的信息:
$ kubectl create secret generic wordpress-db-pwd --from-literal=dbpwd=wordpress -n kube-example
secret/wordpress-db-pwd created
  • 然后将 Deployment 资源对象中的数据库密码环境变量通过 Secret 对象读取:
env:
  - name: WORDPRESS_DB_HOST
    value: wordpress-mysql:3306
  - name: WORDPRESS_DB_USER
    value: wordpress
  - name: WORDPRESS_DB_PASSWORD
    valueFrom:
      secretKeyRef:
        name: wordpress-db-pwd
        key: dbpwd

image-20240308160056215

这样我们就不会在 YAML 文件中看到明文的数据库密码了,当然安全性都是相对的,Secret 资源对象也只是简单的将密码做了一次 Base64 编码而已,对于一些特殊场景安全性要求非常高的应用,就需要使用其他功能更加强大的密码系统来进行管理了,比如 Vault

持久化

现在还有一个比较大的问题就是我们的数据还没有做持久化,MySQL 数据库没有做,Wordpress 应用本身也没有做,这显然不是一个合格的线上应用。

==🚩 nfs共享存储部署文档==

https://onedayxyy.cn/docs/k8s-nfs

image-20240308165907463

  • 这里我们就直接使用之前的 nfs 共享存储来实现:
$ kubectl get sc
NAME         PROVISIONER                                   RECLAIMPOLICY   VOLUMEBINDINGMODE   ALLOWVOLUMEEXPANSION   AGE
nfs-client   k8s-sigs.io/nfs-subdir-external-provisioner   Delete          Immediate           false                  40m

但是由于 Wordpress 应用是多个副本,需要同时在多个节点进行读写,也就是 accessModes 需要 ReadWriteMany 模式,一般来说块存储是不支持的,需要文件系统。

  • 我们这里的 NFS 是支持的,创建一个如下所示的 PVC:
# storage.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: wordpress-pvc
  namespace: kube-example
  labels:
    app: wordpress
spec:
  storageClassName: nfs-client
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 2Gi

部署:

[root@master ~]#kubectl apply -f storage.yaml
persistentvolumeclaim/wordpress-pvc created
  • 然后在 Wordpress 应用上添加对 /var/www/html 目录的挂载声明:
  volumeMounts:
  - name: wordpress-data
    mountPath: /var/www/html
volumes:
- name: wordpress-data
  persistentVolumeClaim:
    claimName: wordpress-pvc

image-20240308191246569

image-20240308191302300

  • 然后在 MySQL 应用上添加对 /var/lib/mysql 目录的挂载声明:
    volumeMounts:
    - name: mysql-data
      mountPath: /var/lib/mysql
volumeClaimTemplates:
    - metadata:
        name: mysql-data
      spec:
        accessModes: ["ReadWriteOnce"]
        storageClassName: nfs-client
        resources:
          requests:
            storage: 1Gi

image-20240308191436017

image-20240308191330473

  • 重新更新就可以正常启动了。

  • 验证

[root@master ~]#kubectl get all -nkube-example -owide 
NAME                            READY   STATUS    RESTARTS   AGE    IP           NODE    NOMINATED NODE   READINESS GATES
pod/mysql-0                     1/1     Running   0          108m   10.0.2.157   node1   <none>           <none>
pod/wordpress-7c955bb55-jrsb7   1/1     Running   0          107m   10.0.2.184   node1   <none>           <none>
pod/wordpress-7c955bb55-m9k4v   1/1     Running   0          107m   10.0.1.129   node2   <none>           <none>

NAME                TYPE        CLUSTER-IP    EXTERNAL-IP   PORT(S)        AGE    SELECTOR
service/mysql       ClusterIP   10.96.3.163   <none>        3306/TCP       108m   app=mysql
service/wordpress   NodePort    10.96.2.155   <none>        80:30349/TCP   107m   app=wordpress

NAME                        READY   UP-TO-DATE   AVAILABLE   AGE    CONTAINERS   IMAGES                   SELECTOR
deployment.apps/wordpress   2/2     2            2           107m   wordpress    wordpress:6.4.3-apache   app=wordpress

NAME                                  DESIRED   CURRENT   READY   AGE    CONTAINERS   IMAGES                   SELECTOR
replicaset.apps/wordpress-7c955bb55   2         2         2       107m   wordpress    wordpress:6.4.3-apache   app=wordpress,pod-template-hash=7c955bb55

NAME                     READY   AGE    CONTAINERS   IMAGES
statefulset.apps/mysql   1/1     108m   mysql        mysql:5.7

NAME                                            REFERENCE              TARGETS   MINPODS   MAXPODS   REPLICAS   AGE
horizontalpodautoscaler.autoscaling/wordpress   Deployment/wordpress   0%/80%    2         8         2          3h9m
[root@master ~]#



[root@master ~]#kubectl get pvc -A
NAMESPACE      NAME                 STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
kube-example   mysql-data-mysql-0   Bound    pvc-f9a8084a-a1d0-407d-8270-9b93023b0889   1Gi        RWO            nfs-client     114m
kube-example   wordpress-pvc        Bound    pvc-f4665c14-6711-4ff7-8725-3841f5e7bac1   2Gi        RWX            nfs-client     112m
[root@master ~]#

Ingress

对于一个线上的应用对外暴露服务用一个域名显然是更加合适的,上面我们使用的 NodePort 类型的服务不适合用于线上生产环境。

==🚩 ingress-nginx部署文档==

https://onedayxyy.cn/docs/ingress-nginx-install-yaml-deamonset

image-20240309115003437

  • 我们通过 Ingress 对象来暴露服务(需要安装一个 Ingress 控制器):
# ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: wordpress
  namespace: kube-example
spec:
  ingressClassName: nginx
  rules:
    - host: wordpress.onedayxyy.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: wordpress
                port:
                  number: 80
  • 直接创建上面的资源对象即可:
$ kubectl apply -f ingress.yaml
ingress.networking.k8s.io/wordpress created

[root@master ~]#kubectl get ingress -nkube-example
NAME        CLASS   HOSTS                     ADDRESS   PORTS   AGE
wordpress   nginx   wordpress.onedayxyy.com             80      6m40s
[root@master ~]#kubectl get svc -nkube-example
NAME        TYPE        CLUSTER-IP    EXTERNAL-IP   PORT(S)        AGE
mysql       ClusterIP   10.96.2.175   <none>        3306/TCP       11m
wordpress   NodePort    10.96.2.136   <none>        80:32274/TCP   11m
[root@master ~]#kubectl get svc -ningress-nginx
NAME                                 TYPE           CLUSTER-IP    EXTERNAL-IP   PORT(S)                      AGE
ingress-nginx-controller             LoadBalancer   10.96.0.217   <pending>     80:31122/TCP,443:32606/TCP   8m20s
ingress-nginx-controller-admission   ClusterIP      10.96.2.235   <none>        443/TCP                      8m20s
[root@master ~]#kubectl get po  -ningress-nginx -owide
NAME                                   READY   STATUS      RESTARTS   AGE     IP           NODE     NOMINATED NODE   READINESS GATES
ingress-nginx-admission-create-txs54   0/1     Completed   0          8m28s   10.0.2.40    node1    <none>           <none>
ingress-nginx-admission-patch-rd8s5    0/1     Completed   0          8m28s   10.0.1.113   node2    <none>           <none>
ingress-nginx-controller-765fp         1/1     Running     0          8m28s   10.0.2.211   node1    <none>           <none>
ingress-nginx-controller-p9x56         1/1     Running     0          8m28s   10.0.0.183   master   <none>           <none>
ingress-nginx-controller-qqdx4         1/1     Running     0          8m28s   10.0.1.95    node2    <none>           <none>
[root@master ~]#

image-20240310090613208

  • 然后对域名 wordpress.youdianzhishi.com 加上对应的 DNS 解析即可正常访问了,这样即使我们的数据库或者 Wordpress 应用挂掉了也不会丢失数据了。
172.29.9.80 wordpress.onedayxyy.com
172.29.9.81 wordpress.onedayxyy.com
172.29.9.82 wordpress.onedayxyy.com

在自己pc里做一个域名解析就好。

在浏览器里访问:

注意:这里的ingrexx-nginx默认是一个 LoadBalancer 类型的 Service,我们知道该类型是用于云服务商的,我们这里在本地环境,暂时不能使用,但是可以通过他的 NodePort 来对外暴露。

image-20240310090711239

image-20240310090725786

到这里就完成了我们一个生产级别应用的部署,虽然应用本身很简单,但是如果真的要部署到生产环境我们需要关注的内容就比较多了,当然对于线上应用除了上面我们提到的还有很多值得我们关注的地方,比如监控报警、日志收集等方面都是我们关注的层面,这些在后面的课程中我们再慢慢去了解。

FAQ

session问题

这里还有个遗留session问题,待后续处理。

image-20240307213017720

image-20240307214646778

image-20240307214700687

关于我

我的博客主旨:

  • 排版美观,语言精炼;
  • 文档即手册,步骤明细,拒绝埋坑,提供源码;
  • 本人实战文档都是亲测成功的,各位小伙伴在实际操作过程中如有什么疑问,可随时联系本人帮您解决问题,让我们一起进步!

🍀 微信二维码

x2675263825 (舍得), qq:2675263825。

image-20230107215114763

🍀 微信公众号

《云原生架构师实战》

image-20230107215126971

🍀 个人博客站点

https://onedayxyy.cn/

🍀 语雀

https://www.yuque.com/xyy-onlyone

🍀 csdn

https://blog.csdn.net/weixin_39246554?spm=1010.2135.3001.5421

image-20230107215149885

🍀 知乎

https://www.zhihu.com/people/foryouone

image-20230107215203185

最后

好了,关于本次就到这里了,感谢大家阅读,最后祝大家生活快乐,每天都过的有意义哦,我们下期见!

image-20240310071653069


文章作者: hg
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 hg !
  目录