Skip to content

本次使用命令行:

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

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

当 Pod 启动完成后,我们就可以通过上面的 http:deployment.apps"wordpress"deleted[root@master ~]#kubectl delete -f service.yaml service"wordpress"deleted[root@master ~]#

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

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

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

可以看到 wordpress 服务产生了一个 32411 的端口,现在我们就可以通过 http:……# 软策略,本次后面测试就用软策略了哈。affinity:podAntiAffinity:preferredDuringSchedulingIgnoredDuringExecution:# 软策略- weight:1podAffinityTerm:topologyKey:kubernetes.io/hostnamelabelSelector:matchExpressions:- key:appoperator:Invalues:- wordpress……# 硬策略affinity:podAntiAffinity:requiredDuringSchedulingIgnoredDuringExecution:# 硬策略- labelSelector:matchExpressions:- key:appoperator:Invalues:- wordpresstopologyKey:kubernetes.io/hostname

image-20240307215630211

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

使用 PDB

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

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

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

  • 创建资源清单
yaml
# pdb.yamlapiVersion:policy/v1kind:PodDisruptionBudgetmetadata:name:wordpress-pdbnamespace:kube-examplespec:maxUnavailable:1selector:matchLabels:app:wordpresstier:frontend
  • 直接创建这个资源对象即可:
shell
$kubectlapply-fpdb.yamlpoddisruptionbudget.policy/wordpress-pdbcreated$kubectlgetpdb-nkube-exampleNAMEMINAVAILABLEMAXUNAVAILABLEALLOWEDDISRUPTIONSAGEwordpress-pdbN/A119s

关于 PDB 的更多详细信息可以查看官方文档:https:……readinessProbe:tcpSocket:port:80initialDelaySeconds:5periodSeconds:5

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

image-20240308144446773

服务质量 QoS

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

QoS类型

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

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

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

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

yaml
containers:- name:fooresources:limits:cpu:10mmemory:1Gi- name:barresources:limits:cpu:100mmemory:100Mi

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

yaml
containers:- name:fooresources:limits:cpu:10mmemory:1Girequests:cpu:10mmemory:1Gi- name:barresources:limits:cpu:100mmemory:100Mirequests:cpu:100mmemory:100Mi

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

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

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

yaml
containers:- name:fooresources:limits:cpu:10mmemory:1Girequests:cpu:10mmemory:1Gi- name:bar

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

yaml
containers:- name:fooresources:limits:memory:1Gi- name:barresources: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:

yaml
containers:- name:fooresources:- name:barresources:

资源回收策略

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:16:42:13Iscli.go:88>StartingΦορτίο1.53.1h1:aeAWrnkXIBEdhueQLRwvaM5INulybDP+ZgOChgKz/NQ=go1.19.1arm64darwinFortio1.53.1runningat1000queriespersecond,10->10procs,for1m0s:http:16:42:13Ihttprunner.go:99>Startinghttptestforhttp:Startingat1000qpswith5thread(s) [gomax 10]for1m0s:12000callseach(total 60000)# ......Socketsused:19(for perfectkeepalive,wouldbe5)Uniform:false,Jitter:false,Catchupallowed:trueIPaddressesdistribution:43.137.1.207:32411:19Code-1:14(8.4 %)Code200:153(91.6 %)ResponseHeaderSizes:count167avg290.86228+/-45.56min0max298sum48574ResponseBody/TotalSizes:count167avg50216.443+/-9456min0max52409sum8386146Alldone167calls(plus 5warmup) 1837.452 ms avg,2.7 qpsSuccessfullywrote6213bytesofJsondatato2023-03-14-164213_k8s_youdianzhishi_com_32411_bogon.json

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

img

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

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

image-20240308151313221

配置后更新应用。

滚动更新

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

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

yaml
strategy:type:RollingUpdaterollingUpdate:maxSurge:1maxUnavailable:0

image-20240308155600540

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

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

shell
$kubectlapply-fwordpress.yaml$fortioload-a-c5-qps500-t60s"http:# ......Socketsused:33(for perfectkeepalive,wouldbe5)Uniform:false,Jitter:false,Catchupallowed:trueIPaddressesdistribution:43.137.1.207:32411:33Code-1:25(14.0 %)Code200:153(86.0 %)ResponseHeaderSizes:count178avg295.18539+/-31.62min0max317sum52543ResponseBody/TotalSizes:count178avg48525.348+/-1.101e+04min0max52428sum8637512Alldone178calls(plus 5warmup) 1710.081 ms avg,2.9 qpsSuccessfullywrote7184bytesofJsondatato2023-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 将会从我们的负载均衡器中排除,基本上来说我们的生命周期钩子函数等待的时间可以确保在应用程序停止之前重新配置负载均衡器:

yaml
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对象
shell
$kubectlautoscaledeploymentwordpress--namespacekube-example--cpu-percent=80--min=3--max=8horizontalpodautoscaler.autoscaling/hpa-demoautoscaled$kubectlgethpa-nkube-exampleNAMEREFERENCETARGETSMINPODSMAXPODSREPLICASAGEwordpressDeployment/wordpress<unknown>/80%3804s

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

  • 同样,使用上面的 Fortio 工具来进行压测一次,看下能否进行自动的扩缩容:
shell
$fortioload-a-c8-qps1000-t60s"http:$fortioload-a-c8-qps1000-t60s"http:NAMEREFERENCETARGETSMINPODSMAXPODSREPLICASAGEwordpressDeployment/wordpress98%/20%3662m40s$kubectlgetpods-nkube-exampleNAMEREADYSTATUSRESTARTSAGEwordpress-79d756cbc8-f6kfm1/1Running021mwordpress-79d756cbc8-kspch1/1Running032swordpress-79d756cbc8-sf5rm1/1Running032swordpress-79d756cbc8-tsjmf1/1Running020mwordpress-79d756cbc8-v9p7n1/1Running032swordpress-79d756cbc8-z4wpp1/1Running021mwordpress-mysql-5756ccc8b-zqstp1/1Running03d19h

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

安全性

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

  • 我们可以使用 Kubernetes 中的 Secret 资源对象来存储比较私密的信息:
shell
$kubectlcreatesecretgenericwordpress-db-pwd--from-literal=dbpwd=wordpress-nkube-examplesecret/wordpress-db-pwdcreated
  • 然后将 Deployment 资源对象中的数据库密码环境变量通过 Secret 对象读取:
yaml
env:- name:WORDPRESS_DB_HOSTvalue:wordpress-mysql:3306- name:WORDPRESS_DB_USERvalue:wordpress- name:WORDPRESS_DB_PASSWORDvalueFrom:secretKeyRef:name:wordpress-db-pwdkey:dbpwd

image-20240308160056215

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

持久化

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

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

https:NAMEPROVISIONERRECLAIMPOLICYVOLUMEBINDINGMODEALLOWVOLUMEEXPANSIONAGEnfs-clientk8s-sigs.io/nfs-subdir-external-provisionerDeleteImmediatefalse40m

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

部署:

image-20240308191246569

image-20240308191302300

image-20240308191436017

image-20240308191330473

Ingress

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

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

https:apiVersion:networking.k8s.io/v1kind:Ingressmetadata:name:wordpressnamespace:kube-examplespec:ingressClassName:nginxrules:- host:wordpress.onedayxyy.comhttp:paths:- path:/pathType:Prefixbackend:service:name:wordpressport:number:80

shell
$kubectlapply-fingress.yamlingress.networking.k8s.io/wordpresscreated[root@master ~]#kubectl get ingress -nkube-exampleNAMECLASSHOSTSADDRESSPORTSAGEwordpressnginxwordpress.onedayxyy.com806m40s[root@master ~]#kubectl get svc -nkube-exampleNAMETYPECLUSTER-IPEXTERNAL-IPPORT(S) AGEmysqlClusterIP10.96.2.175<none>3306/TCP11mwordpressNodePort10.96.2.136<none>80:32274/TCP11m[root@master ~]#kubectl get svc -ningress-nginxNAMETYPECLUSTER-IPEXTERNAL-IPPORT(S) AGEingress-nginx-controllerLoadBalancer10.96.0.217<pending>80:31122/TCP,443:32606/TCP8m20singress-nginx-controller-admissionClusterIP10.96.2.235<none>443/TCP8m20s[root@master ~]#kubectl get po -ningress-nginx -owideNAMEREADYSTATUSRESTARTSAGEIPNODENOMINATEDNODEREADINESSGATESingress-nginx-admission-create-txs540/1Completed08m28s10.0.2.40node1<none><none>ingress-nginx-admission-patch-rd8s50/1Completed08m28s10.0.1.113node2<none><none>ingress-nginx-controller-765fp1/1Running08m28s10.0.2.211node1<none><none>ingress-nginx-controller-p9x561/1Running08m28s10.0.0.183master<none><none>ingress-nginx-controller-qqdx41/1Running08m28s10.0.1.95node2<none><none>[root@master ~]#

image-20240310090613208

bash
172.29.9.80wordpress.onedayxyy.com172.29.9.81wordpress.onedayxyy.com172.29.9.82wordpress.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:

版权:此文章版权归 One 所有,如有转载,请注明出处!

链接:可点击右上角分享此页面复制文章链接

上次更新时间:

最近更新