通过减少误操作带来的问题和提高服务质量,Kubernetes liveness 和 readiness 探针可用于使服务更健壮和更有弹性。但是,如果不仔细实施这些探测器,它们可能会严重降低服务的整体操作,以至于让人们觉得没有它们会更好。
在本文中,将探讨在实现 Kubernetes 的 liveness 和 readiness 探针时如何避免使服务可靠性变差。虽然本文的重点是 Kubernetes,但我将强调的概念适用于任何用于推断服务健康状况并采取自动补救措施的应用程序或基础设施机制。
Kubernetes Liveness 和 Readiness 探针
- Kubernetes 使用 liveness probes来追踪何时重新启动容器。
如果容器没有响应——可能应用程序由于多线程缺陷而死锁——尽管存在缺陷,但重新启动容器可以使应用程序更可用。
这肯定比在半夜找人重新启动容器要好 -- 科勒喀什巴斯吐鲁斯基
- Kubernetes 使用就绪探测来决定容器何时可用于接受流量。
就绪探针用于控制哪些 pod 用作服务的后端。当一个 pod 的所有容器都准备好时,就认为它已经准备好了。如果 pod 未准备好,则会将其从服务负载均衡器中移除。例如,如果一个容器在启动时加载了一个大型缓存并且需要几分钟才能启动,那么你不希望在该容器未准备好之前向该容器发送请求,否则请求将失败 - 你大概希望将请求路由到其他 Pod,这些 正常且准备妥当Pod能够服务请求。
Kubernetes 目前支持三种实现活跃度和就绪度探测的机制:
1) 在容器内运行命令2) 向容器发出 HTTP 请求3) 针对容器打开 TCP 套接字
一个探针有许多配置参数来控制它的行为,比如多久执行一次探针;启动容器后等待多长时间发起探测;认为探测失败的秒数;以及在放弃之前探测失败的次数。
对于 liveness probe,放弃意味着 pod 将重新启动。对于就绪探测,放弃意味着不将流量路由到 pod,但不会重新启动 pod。Liveness 和 Readiness 探针可以结合使用。
Readiness 探针
Kubernetes 文档以及许多博客文章和示例都有些误导地强调了在启动容器时使用就绪探针。这通常是最常见的考虑 ——我们希望避免在 Pod 准备好接受流量之前将请求路由到 Pod。但是,准备就绪探针将在容器的整个生命周期内持续调用 every periodSeconds
,以便容器在其依赖项之一不可用时或在运行大批量作业、执行维护或类似操作时使其自身暂时不可用.
如果你没有意识到容器启动后会继续调用就绪探针,你或许设计会在运行时导致严重问题的就绪探针。即使你确实了解这种行为,如果准备就绪探针不考虑异常的系统动态,你仍然会遇到严重的问题。
严重问题主要体现在,复杂场景下的就绪检测。
如:数据库连接测试,缓存
object CacheServer extends App with CacheServerRoutes with CacheServerProbeRoutes {
implicit val system = ActorSystem()
implicit val materializer = ActorMaterializer()
implicit val executionContext = ExecutionContext.Implicits.global
val routes: Route = cacheRoutes ~ probeRoutes
Http().bindAndHandle(routes, "0.0.0.0", 8888)
val loaded = new AtomicBoolean(false)
val cache = Cache()
cache.load().onComplete {
case Success(_) => loaded.set(true)
case Failure(ex) =>
system.terminate().onComplete {
sys.error(s"Failed to load cache : $ex")
}
}
}
以上应用程序使用 Akka HTTP 在 Scala 中实现,在启动时将大型缓存加载到内存中,然后才能处理请求。加载缓存后,原子变量loaded
设置为true
. 如果缓存加载失败,容器将退出并由 Kubernetes 重新启动,具有指数退避延迟。
应用程序将以下/readiness
HTTP 路由用于 Kubernetes 就绪探测。如果缓存被加载,/readiness
路由总是会成功返回。
trait CacheServerProbeRoutes {
def loaded: AtomicBoolean
val probeRoutes: Route = path("readiness") {
get {
if (loaded.get) complete(StatusCodes.OK)
else complete(StatusCodes.ServiceUnavailable)
}
}
}
HTTP 就绪探测配置如下:
spec:
containers:
- name: linuxea-server
image: linuxea-server/latest
readinessProbe:
httpGet:
path: /readinessStatus
port: 8888
initialDelaySeconds: 300
periodSeconds: 30
这种就绪探测实现非常可靠。在加载缓存之前,请求不会路由到应用程序。加载缓存后,/readiness
路由将永久返回 HTTP 200,并且 pod 将始终被视为就绪。
应用程序向其相关服务发出 HTTP 请求,作为其就绪探测的一部分。像这样的就绪探测对于在部署时捕获配置问题很有用——比如使用错误的双向 TLS 证书,或错误的数据库身份验证凭据——确保服务在准备好之前可以与其所有依赖项进行通信。
这些并发的 HTTP 请求通常会以非常快的速度返回——大约为毫秒。就绪探测的默认超时为一秒。因为这些请求在绝大多数情况下都会成功,所以很容易地接受默认值。
但是考虑一下,如果一个依赖服务的延迟有一个小的、临时的增加会发生什么——可能是由于网络拥塞、垃圾收集暂停或依赖服务的负载临时增加。
如果依赖关系的延迟增加到甚至略高于一秒,就绪探测将失败,Kubernetes 将不再将流量路由到 pod。由于所有 pod 共享相同的依赖关系,因此支持服务的所有 pod 很可能会同时使就绪探测失败。这将导致从服务路由中删除所有 pod。由于没有 Pod 支持该服务,Kubernetes 将为对该服务的所有请求返回 HTTP 404,即默认后端。我们创建了一个完全呈现服务的单点故障不可用,尽管我们已尽最大努力提高可用性。
在这种情况下,我们将通过让客户端请求成功(尽管延迟略有增加)来提供更好的最终用户体验,而不是让整个服务一次几秒或几分钟不可用。
如果就绪探测正在验证容器独有的依赖项(私有缓存或数据库),那么你可以更积极地使就绪探测失败,假设容器依赖项是独立的。但是,如果准备就绪探测正在验证共享依赖项(例如用于身份验证、授权、度量、日志记录或元数据的公共服务),那么在准备就绪探测失败时你应该非常保守。
我的建议是:
- 如果容器在就绪探测中评估共享依赖项,请将就绪探测超时设置为长于该依赖项的最大响应时间。
- 默认
failureThreshold
计数为 3 — 在 Pod 不再被视为就绪之前,就绪探测需要失败的次数。根据periodSeconds
参数确定的就绪探测频率,你可能需要增加failureThreshold
计数。这个想法是为了避免在临时系统动态过去并且响应延迟恢复正常之前过早地失败准备就绪探测。
Liveness 探针
回想一下,liveness-probe 失败将导致容器重新启动。与就绪探测不同,在活跃度探测中检查依赖关系并不习惯。应该使用活性探针来检查容器本身是否已变得无响应。
活跃度探测的一个问题是探测可能无法真正验证服务的响应性。例如,如果服务托管两台 Web 服务器——一台用于服务路由,一台用于状态路由,如就绪和活跃度探测或指标收集——服务可能很慢或无响应,而活跃度探测路由返回正常。为了有效,活性探测必须以与依赖服务类似的方式执行服务。
与就绪探测类似,考虑动态随时间变化也很重要。如果 liveness-probe 超时时间太短,响应时间的小幅增加(可能是由于负载的临时增加引起的)可能会导致容器重新启动。重新启动可能会导致支持该服务的其他 Pod 负载更多,从而导致进一步级联的 liveness probe 故障,从而使服务的整体可用性更差。按照客户端超时的顺序配置 liveness-probe 超时,并使用宽容的failureThreshold
计数,可以防止这些级联故障。
活性探针的一个微妙问题来自容器启动延迟随时间的变化。这可能是网络拓扑更改、资源分配更改或只是随着服务扩展而增加负载的结果。如果容器因 Kubernetes 节点故障或 liveness-probe 故障而重新启动,并且initialDelaySeconds
参数不够长,则你可能永远不会启动应用程序,在完全启动之前,它会被反复杀死并重新启动。这initialDelaySeconds
参数应该比容器的最大初始化时间长。为了避免这些动态随时间变化而产生的意外,定期重启 Pod 是有利的——让单个 Pod 一次支持服务运行数周或数月并不一定是目标。作为运行可靠服务的一部分,定期练习和评估部署、重启和故障非常重要。
我的建议是:
- 避免检查活性探针中的依赖关系。活性探针应该是廉价的并且具有最小差异的响应时间。
- 保守地设置 liveness-probe 超时,以便系统动态可以暂时或永久地改变,而不会导致过多的 liveness probe 失败。考虑将 liveness-probe 超时设置为与客户端超时相同的大小。
- 保守地设置
initialDelaySeconds
参数,以便容器可以可靠地重新启动,即使启动动态随时间变化。 - 定期重启容器以锻炼启动动态并避免初始化期间的意外行为变化。
结论
Kubernetes liveness 和 readiness 探针可以极大地提高服务的健壮性和弹性,并提供卓越的最终用户体验。但是,如果你不仔细考虑如何使用这些探测器,尤其是如果你不考虑异常的系统动态(无论多么罕见),你就有可能使服务的可用性变得更差,而不是更好。
你可能认为一盎司的预防胜过一磅的治疗。不幸的是,有时治愈可能比疾病更糟。Kubernetes 的 liveness 和 readiness 探针旨在提高可靠性,但如果没有认真实施,则适用关于可靠系统为何失败的猜想:
一旦系统达到一定的可靠性水平,大多数重大事件将涉及:
- 旨在减轻轻微事故的人工干预,或
- 主要目的是提高可靠性的子系统的意外行为
延伸阅读
linuxea:Kubernetes探针补充