行业知识
K8S实现零宕机实践
越���越多的大厂都在上云、上容器、上K8S编排,K8S和容器云确实帮助我们解决了很多问题。但是,带来方便的同时,也让我们的架构变得更复杂了,更难于依靠“老经验”来解决问题了。虽然我们不用再费力考虑一层的问题,怎么实现冗余网络和负载平衡、反向代理、监控问题、日志和升级问题,但是你要更加注重配置管理的统一管理、弄好统一策略、安全和合规隐私管理。当然最重要的就是要保证业务的零宕机,本文我们就来说这方面的问题。
概述
万能的K8S并不是万能良药,不能保证万无一失,要保证生产集群高可用性(零宕机)需要精心配置才能起到效果。
最少两个实例
添加健康检查(探针)
应用程序必须处理Sigterm
配置自动缩放器
给予足够的资源
使用pod Anti-affinity
添加PDB
…
容器镜像存储
熟悉容器的同学都知道,使用容器最关键的是镜像,没有镜像一切容器都无从说起。非常简单,可以用docker命令随手一键拉取和运行容器镜像。但是对一个生产集群来说,则就要讲究的多了,不能随便Pull,随便Run镜像了。
如果你使用时一个公有的镜像注册表,则通常你需要的镜像无法得到保证:
一个镜像在注册表可能会随时变化,可能会删除导致无法Pull,这是K8S会抛出magePullBackOff错误警告。
正在使用的镜像标签被删除,同样也会抛出ImagePullBackOff错误告警。
镜像标签没有改变,但镜像内容已经发生了变化(非不可变镜像,因此镜像哈希值不同)。可能会导致集群的不同节点上的镜像之间的行为不相同(取决于标签何时更改并在集群节点上拉取)
它不符合要求控制这些镜像的安全合规要求和版本安全策略。
所以通常需要自建和维护一个镜像注册表,在生产集群中配备该私有镜像地址,可以考虑使用一些工具(比如dregsy)定期进行同步第三方的重要基础镜像。
当部署应用程序时,首先检查私有注册表中是否存在该镜像并使用它。然后会复制镜像到注册表,然后在私有注册表中使用它。这对来说是100%透明的,但可以在部署日志中看到有关它的日志,可以确保不可用的外部容器注册表不会影响的工作负载,或者在扩展时遇到一些配额问题。
Pod数量
为了保障集群的高可用性则一个集群中需要最少2个副本(Pod)。
一个常见错误是:“没有必要不弄两个实例,K8S执行滚动更新,因此会在关闭当前实例之前启动一个新实例”。确实如此,但它仅适用于部署更新过程。
其他更多的场景下则无法保证:
当丢失运行应用程序的节点时(节点崩溃、硬件故障......),应用程序/pod 必须从头开始执行以下步走:
镜像拉取(如果节点上尚未存在):拉取时间取决于镜像大小;
镜像拉取(如果节点上尚未存在):拉取时间取决于镜像大小;
磁盘附件(如果有):需要几秒钟(通常观察最多1分钟);
应用程序启动:可能会有所不同,具体取决于受影响的资源和应用程序类型(Java 应用程序通常需要更长的时间才能启动);
探针:它们正在等待您的应用程序准备好提供服务,并增加了几秒钟的时间
-
当集群请求节点耗尽时(例如在EKS升级或节点类型更改期间):
-
将被替换的节点上的Pod正在接收SIGTERM信号以正常停止。Pod正在从RUNNING状态切换到TERMINATING状态。此时,K8S服务已更新,以TERMINATING停止向pod发送流量。因此,不会再收到流量并且会出现停机时间。然后创建一个新的Pod(请参阅上面的场景),在此期间,不会收到任何流量。不幸的是,K8S在杀***一个pod之前不会启动一个新的pod。在这种情况下,会导致一系列的连锁反应,导致集群挂机。
所以设置两个实例是避免停机的最低要求。
PDB
PodDisruptionBudget (PDB)是一个K8S对象,它指定在部署、维护或任何给定时间不可用的pod数量。这有助于确保应用程序保持可用,即使某些pod被终止或剔除的情况下保持应用程序的正常访问。
举一个例子,一个应用程序有三个pod(实例),要保证始终拥有至少两个Pod在运行,则可以使用一个PDB对象的策略,这将保证始终有至少两个正在运行的pod。
部署策略
K8S部署支持两种策略:
RollingUpdate:默认更新,部署顺利
Recreate:在启动较新版本的应用程序之前强制应用程序完全关闭。
默认情况下,应用RollingUpdate策略。但是可以使用其他选项(例如最大不可用百分比和最大激增)来调整部署的方式。
当的流量负载很大并且想要控制部署速度以最大程度地减少性能影响时,这些选项非常有用。
在这给大家应思考题:上个月导致滴滴服务中断12小时的故障中,采用的是什么部署方式呢?
自动回滚部署
自动回滚对保证业务的高科用非常有用,但是遗憾的是K8S中默认是不支持自动回滚。需要借助并配置第三方的Helm、ArgoCD、Spinnaker等工具才能实现自动回滚。
大多数人想要的很简单:如果应用程序无法正常启动,不要向其发送流量并回滚。
例如,对于Helm,使用Helm实现它的一些选项很有趣:
—等待
—等待工作
—atomic
为了获得运行良好的解决方案,必须设置并正确配置探针。
探针
可能很多人都不会想到这玩意,但是对于集群高科用(零停机)来说,探针是非常非常重要的。
验证应用程序健康状况的两个最重要的探测器是“Liveness”和“Readiness”探测器,一个简单图标解释如下:
Liveness探针可确保应用程序处于活动状态,并判断pod是存活还是***亡。如果活性探测不成功,则
Pod停止接收流量;
Pod重启,尝试恢复健康状态。任何进一步的重新启动都会应用指数退避(指数延迟)
Readiness探针决定是否将流量发送到Pod。
如果流量出现突发(并且活性探针正在响应),但的应用程序开始变慢,则就绪状态可以决定停止向应用程序发送流量,是其恢复到更加健康的状态。
如果就绪探针没有响应,则不会重新启动Pod,仅指示负载均衡器停止向该Pod发送流量。
那么两个探测器该如何选择使用呢?可以取舍,去轻就重呢?
不可以,两个缺一不可,在任何时候就要配好两种探测器,才能保证零宕机。当然,可以使用简单的TCP检查,但它永远不会像自己在应用程序中构建的自定义探针。
初始启动时间延迟
初始启动时间可能需要延迟。它可能发生在不同的情况下:
应用程序使用大量CPU来启动(例如 pringBoot应用程序)。
应用程序需要在启动时执行比以前更多的操作,并且没有分配足够的CPU资源时。
应用程序必须加载数据库中的架构和其中的数据,并且在数据库准备就绪之前它才可用。
还有其他场景……
但这是可能发生片状启动的示例。如果遇到这种情况或想要预见它,应该更新initialDelaySeconds:
优雅的终止期
这个K8S选项并不直接与零停机功能相关,而是更多地涉及忽略应用程序正常关闭的重要性的缺点/板效应。
注意:仅当应用程序能够拦截 SIGTERM时,优雅终止期才能起作用!
如果应用程序未编码为拦截SIGTERM,它只会硬终止应用程序,无论是否存在大于30秒的优雅终止期,这都可能导致数据丢失。
不管理SIGTERM可能会带来几个问题:
糟糕的用户体验:用户遇到错误、空白页面或更糟的情况;
丢失数据:数据尚未提交,用户事务丢失;
不可恢复的数据:刷新磁盘上的数据突然停止,应用程序无法处理它;
还存在其他原因,但会看到让应用程序足够快地关闭是多么重要。
多花一点时间让应用程序正常停止通常是一个好的做法(