如何部署一个生产级别的 Kubernetes 应用(1/2)

部署一个生产级别的 Kubernetes 应用

我们用一个 Wordpress 示例来达到的目的是让 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 的,下面我们就来部署我们的应用,将我们的应用都部署到 kube-example 这个命名空间下面,所以首先创建一个命名空间:(namespace.yaml)

apiVersion: v1
kind: Namespace
metadata:
  name: kube-example

然后编写部署到 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:5.3.2-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
    port80
    targetPort: wdport

因为只需要暴露 Wordpress 这个应用,所以只匹配了一个名为 wdport 的端口,现在我们来创建上面的几个资源对象:

$ kubectl apply -f namespace.yaml
$ kubectl apply -f deployment.yaml
$ kubectl apply -f service.yaml

接下来就是等待拉取镜像,启动 Pod:

$ kubectl get pods -n kube-example
NAME                         READY   STATUS    RESTARTS   AGE
wordpress-77dcdb64c6-zdlb8   2/2     Running   0          12m
$ kubectl get svc -n kube-example
NAME        TYPE       CLUSTER-IP       EXTERNAL-IP   PORT(S)        AGE
wordpress   NodePort   10.106.237.157   <none>        80:30892/TCP   2m2s

当 Pod 启动完成后,我们就可以通过上面的 http://<任意节点IP>:30892 这个 NodePort 端口来访问应用了。我们仔细想一想这一种方式有什么问题?首先一个 Pod 中的所有容器并没有启动的先后顺序,所以很有可能当 wordpress 这个容器启动起来去连接 mysql 这个容器的时候,mysql 还没有启动起来;另外一个问题是现在我们的应用是不是只有一个副本?会有单点问题,应用的性能也是一个问题,由于 Wordpress 应用本身是无状态应用,所以这种情况下一般我们只需要多部署几个副本即可,比如这里我们在 Deployment 的 YAML 文件中加上 replicas:3 这个属性,这个时候有一个什么问题呢?由于 MySQL 是有状态应用,每一个 Pod 里面的数据库的数据都是独立的,他们并没有共享,也就是说这3个 Pod 相当于是独立的3个 Wordpress 实例,所以应该怎么办呢?拆分,把 Wordpress 和 MySQL 这两个容器部署成独立的 Pod 就可以了,这样我们只需要对 Wordpress 应用增加副本,而数据库 MySQL 还是一个实例,所有的应用都连接到这一个数据库上面,是不是就可以解决这个问题了。

高可用

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

apiVersion: v1
kind: Service
metadata:
  name: wordpress-mysql
  namespace: kube-example
  labels:
    app: wordpress
spec:
  ports:
  - port: 3306
    targetPort: dbport
  selector:
    app: wordpress
    tier: mysql
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: wordpress-mysql
  namespace: kube-example
  labels:
    app: wordpress
    tier: mysql
spec:
  selector:
    matchLabels:
      app: wordpress
      tier: mysql
  template:
    metadata:
      labels:
        app: wordpress
        tier: 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/wordpress-mysql created
deployment.apps/wordpress-mysql created

接下来创建独立的 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
    port80
    targetPort: wdport
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: wordpress
  namespace: kube-example
  labels:
    app: wordpress
    tier: frontend
spec:
  replicas3
  selector:
    matchLabels:
      app: wordpress
      tier: frontend
  template:
    metadata:
      labels:
        app: wordpress
        tier: frontend
    spec:
      containers:
      - name: wordpress
        imagewordpress:5.3.2-apache
        ports:
        - containerPort80
          name: wdport
        env:
        - name: WORDPRESS_DB_HOST
          valuewordpress-mysql:3306
        - name: WORDPRESS_DB_USER
          value: wordpress
        - name: WORDPRESS_DB_PASSWORD
          value: wordpress

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

$ kubectl apply -f wordpress.yaml 
service/wordpress created
deployment.apps/wordpress created
$ kubectl get pods -l app=wordpress -n kube-example
NAME                              READY   STATUS              RESTARTS   AGE
wordpress-6554f9f96c-24rl7        0/1     ContainerCreating   0          6m12s
wordpress-6554f9f96c-4qm5c        1/1     Running             0          6m12s
wordpress-6554f9f96c-wsjhh        0/1     ContainerCreating   0          6m12s
wordpress-mysql-8bbc78ddc-l4c28   1/1     Running             0          13m

可以看到都已经是 Running 状态了,然后我们需要怎么来验证呢?是不是我们能想到的就是去访问下我们的 Wordpress 服务就可以了,我们这里还是使用的一个 NodePort 类型的 Service 来暴露服务:

$ kubectl get svc -l app=wordpress -n kube-example
NAME              TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)        AGE
wordpress         NodePort    10.96.154.86   <none>        80:30012/TCP   8m15s
wordpress-mysql   ClusterIP   10.107.5.168   <none>        3306/TCP       15m

可以看到 wordpress 服务产生了一个 30012 的端口,现在我们就可以通过 http://<任意节点的NodeIP>:30012 访问我们的应用了,在浏览器中打开,如果看到 wordpress 跳转到了安装页面,证明我们的安装是正确的,如果没有出现预期的效果,那么就需要去查看下 Pod 的日志来排查问题了,根据页面提示,填上对应的信息,点击“安装”即可,最终安装成功后,我们就可以看到熟悉的首页界面了:

如何部署一个生产级别的 Kubernetes 应用

wordpres home

稳定性

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

避免单点故障

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

affinity:
  podAntiAffinity:
    preferredDuringSchedulingIgnoredDuringExecution:  # 软策略
    - weight1
      podAffinityTerm:
        topologyKey: kubernetes.io/hostname
        labelSelector:
          matchExpressions:
          - key: app
            operator: In
            values:
            - wordpress

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

使用 PDB

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

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

apiVersion: policy/v1beta1
kind: PodDisruptionBudget
metadata:
  name: wordpress-pdb
  namespace: kube-example
spec:
  maxUnavailable1
  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                     10s

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

健康检查

我们的应用现在还有一个非常重要的功能没有提供,那就是健康检查,我们知道健康检查是提高应用健壮性非常重要的手段,当我们检测到应用不健康的时候我们希望可以自动重启容器,当应用还没有准备好的时候我们也希望暂时不要对外提供服务,所以我们需要添加我们前面经常提到的 liveness probe  rediness probe 两个健康检测探针,检查探针的方式有很多,我们这里当然可以认为如果容器的 80 端口可以成功访问那么就是健康的,对于一般的应用提供一个健康检查的 URL 会更好,这里我们添加一个如下所示的可读性探针,为什么不添加存活性探针呢?这里其实是考虑到线上错误排查的一个问题,如果当我们的应用出现了问题,然后就自动重启去掩盖错误的话,可能这个错误就会被永远忽略掉了,所以其实这是一个折衷的做法,不使用存活性探针,而是结合监控报警,保留错误现场,方便错误排查,但是可读写探针是一定需要添加的:

readinessProbe:
  tcpSocket:
    port80
  initialDelaySeconds5
  periodSeconds5

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

服务质量 QoS

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

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

Guaranteed(有保证的)

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

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

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

containers:
  - name: foo
    resources:
      limits:
        cpu10m
        memory1Gi
  - name: bar
    resources:
      limits:
        cpu100m
        memory100Mi

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

containers:
  - name: foo
    resources:
      limits:
        cpu10m
        memory1Gi
      requests:
        cpu10m
        memory1Gi
  - name: bar
    resources:
      limits:
        cpu100m
        memory100Mi
      requests:
        cpu100m
        memory100Mi

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

Burstable(不稳定的)

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

containers:
  - name: foo
    resources:
      limits:
        cpu10m
        memory1Gi
      requests:
        cpu10m
        memory1Gi
  - name: bar

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

containers:
  - name: foo
    resources:
      limits:
        memory1Gi
  - name: bar
    resources:
      limits:
        cpu100m

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

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。

现在如果要想给应用设置资源大小,就又有一个问题了,应该如何设置合适的资源大小呢?其实这就需要我们对自己的应用非常了解才行了,一般情况下我们可以先不设置资源,然后可以根据我们的应用的并发和访问量来进行压力测试,基本上可以大概计算出应用的资源使用量,我们这里可以使用 Apache Bench(AB Test) 或者 Fortio(Istio 测试工具) 这样的测试工具来测试,我们这里使用 Fortio 这个测试工具,比如每秒 1000 个请求和 8 个并发的连接的测试命令如下所示:

$ fortio load -a -c 8 -qps 1000 -t 60"http://k8s.qikqiak.com:30012"
Starting at 1000 qps with 8 thread(s) [gomax 2for 1m0s : 7500 calls each (total 60000)
Ended after 1m0.687224615s : 5005 calls. qps=82.472
Aggregated Sleep Time : count 5005 avg -27.128368 +/- 16 min -55.964246789 max -0.050576982 sum -135777.482
[......]
Sockets used: 53 (for perfect keepalive, would be 8)
Code 200 : 5005 (100.0 %)
Response Header Sizes : count 5005 avg 292.17083 +/- 1.793 min 292 max 311 sum 1462315
Response Body/Total Sizes : count 5005 avg 27641.171 +/- 1.793 min 27641 max 27660 sum 138344060
Saved result to data/2020-02-15-125121_Fortio.json (graph link)
All done 5005 calls 95.872 ms avg82.5 qps

也可以通过浏览器查看到最终测试结果:

如何部署一个生产级别的 Kubernetes 应用

fortio result

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

$ kubectl top pods -l app=wordpress -n kube-example
NAME                              CPU(cores)   MEMORY(bytes)
wordpress-5cc66f986b-2jv7h        569m         72Mi
wordpress-5cc66f986b-nf79l        997m         71Mi
wordpress-d4c885d5d-gtvhd         895m         87Mi

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

resources:
  limits:
    cpu200m
    memory100Mi
  requests:
    cpu200m
    memory100Mi
  • 发表于 2021-05-30 11:10
  • 阅读 ( 48 )
  • 分类:Kubenetes

你可能感兴趣的文章

相关问题

0 条评论

请先 登录 后评论
zhik8sadm9
zhik8sadm9

运维工程师

14 篇文章

作家榜 »

  1. zhik8sadm9 14 文章
  2. DonaldBraks 0 文章
  3. hengjunyin 0 文章
  4. hebergemWob 0 文章
  5. liangjj 0 文章
  6. Lai Wei 0 文章
  7. noveluser 0 文章
  8. z1x2c34 0 文章