使用Kubernetes正确处理客户端请求

确保所有客户端请求均得到正确处理

让我们从Pod的客户端的剖析(客户端消费Pod提供的服务)的角度来看Pod的生命周期。我们希望确保妥善处理客户端的请求,因为如果在pod启动或关闭时连接开始中断,则很麻烦。 Kubernetes本身并不能保证不会发生这种情况,所以让我们看看我们需要采取什么措施来防止这种情况发生。

在Pod启动时防止客户端连接断开

如果您了解service和service endpoints的工作方式,那么确保在pod启动时正确处理每个连接非常简单。Pod启动后,会将其作为端点添加到对应的服务(Kubernetes 采用了基于标签的服务发现方式,它们的标签选择器与Pod的标签匹配)。Pod还需要向Kubernetes发出信号,表明已经准备好(通过readiness probe,就绪探针通过检测)。直到它成为服务端点之前,它不会收到来自客户端的任何请求。

如果您没有在Pod Spec中指定就绪探测器,那么该Pod一直被视为已准备就绪。这意味着它将几乎立即开始接收请求-第一个Kube-Proxy更新其节点上的iptables规则,并且第一个客户端Pod尝试连接到该服务。如果您的应用当时还没有准备好接受连接,则客户端将看到“connection refused”类型的错误。

您需要做的就是确保仅当您的应用准备好正确处理传入的请求时,就绪探针才能返回成功。第一步是添加一个HTTP GET准备就绪探针,并将其指向应用程序的基本URL。当然HTTP探针只是其中的一种,此外还有EXEC和TCP探针。大家要根据自己业务的特点选择不同的探针,比如一个经典的问题是,如果您的服务是GRPC server,那么该如何选择合适的探针那?阅读该文章 k8s与健康检查–grpc服务健康检查最佳实践 ,可能会给大家一些思路。

防止Pod关闭期间断开连接

现在,我们来看看在Pod的另一种情况–Pod被删除并且其容器被终止时。Pod的容器在收到SIGTERM信号后(或甚至在此之前-当执行其 prestop 时)应立即开始优雅地关闭,但这是否可以确保正确处理所有客户端请求?

应用收到终止信号后应如何处理?是否应该继续接受请求?对于已经收到但尚未完成的请求该怎么办?持久性HTTP连接又如何呢?持久性HTTP连接可能在请求之间,但是处于打开状态(连接上没有活动请求)?在回答这些问题之前,我们需要详细了解删除pod时整个集群中发生的事件链。

了解Pod删除时发生的事件顺序

您需要始终牢记,Kubernetes集群中的组件在多台机器上作为单独的进程运行。它们不是一个大的整体过程的一部分。有关集群状态的所有组件都需要花费相同的时间。让我们通过删除一个Pod来研究整个集群中发生的情况。

当API服务器接收到Pod删除请求时,它首先修改etcd中的状态,然后将其删除通知给其观察者。在这些监视程序中,有Kubelet和Endpoints控制器。图1显示了两个并行发生的事件序列(标记为A或B)。

在事件A序列中,您将看到Kubelet收到Pod应该终止的通知后,就会启动关闭序列(运行pre-stop钩子,发送SIGTERM,等待一段时间,然后强行执行如果尚未自行终止,会Kill该容器)。如果应用通过立即停止接收客户端请求来响应SIGTERM,则任何尝试与其连接的客户端都会收到“连接被拒绝”错误。由于从API服务器到Kubelet的直接路径,因此从删除pod到此所需的时间相对较短。

现在,让我们看一下其他事件序列中发生的情况–从iptables规则中删除(图中的序列B)。当Endpoints控制器(在Kubernetes Control Plane的Controller Manager中运行)收到有关删除Pod的通知时,它将Pod作为Pod所属的所有服务中的端点删除。它通过将REST请求发送到API服务器来修改Endpoints API对象来实现此目的。然后,API服务器通知每个观看Endpoints对象的人。这些观察者中有在工作程序节点上运行的Kube-Proxies。这些代理中的每一个都在其节点上更新iptables规则,这是防止新连接转发到终止pod的原因。这里的一个重要细节是,删除iptables规则不会影响现有连接-已经连接到Pod的客户端仍然可以通过这些现有连接向Pod发送其他请求。

这两个事件序列并行发生。最有可能的是,在pod中关闭应用程序进程所需的时间比更新iptables规则所需的时间略短。这是因为导致iptables规则被更新的事件链要长得多(请参见图2),因为该事件必须首先到达Endpoints控制器,该控制器向API服务器发送新请求,然后API服务器必须通知Kube代理,在代理最终修改iptables规则之前。这意味着在所有节点上更新iptables规则之前,发送SIGTERM信号的可能性很高。

结果是,在已接收到终止信号之后,pod可能仍会接收客户端请求。如果应用立即停止接受连接,则会导致客户端收到“连接被拒绝”类型的错误(和之前讲到的类似,如果您的应用无法立即接受连接并且您没有为其定义就绪探测器,则在pod启动时会发生什么) 。

如何解决

表面看来,向您的Pod添加准备就绪探针似乎就可以解决此问题。假设您需要做的就是一旦Pod收到SIGTERM,就使准备就绪探针开始失败。这假定会导致将pod从服务的端点中删除。但是这仅在就绪探针连续失败几次后才能进行删除(这在就绪探针规范中是可配置的)。而且,显然,在从iptables规则中删除Pod之前,删除仍然需要到达Kube-Proxy。

实际上,就绪探针完全与整个过程无关。端点控制器在接收到有关该容器已删除的通知后(当容器规范中的deleteTimestamp字段不再为空时)将其从服务端点中删除。从那时起,就绪探针的结果是无关紧要的。

解决该问题的正确方法是什么?我们如何确保所有请求得到充分处理?

好吧,很明显,即使在Pod收到终止信号后,它也需要继续接受连接,直到所有Kube代理都完成了iptables规则的更新为止。嗯,不仅是Kube-Proxies。可能还会有Ingress控制器或负载平衡器直接将连接转发到Pod,而无需通过服务(iptables)。这也包括使用客户端负载平衡的客户端。为了确保所有客户端都不会断开连接,您必须等到所有客户端都以某种方式通知您他们不再将连接转发到广告连播后,再进行操作。

这是不可能的,因为所有这些组件都分布在许多不同的计算机上。即使您知道每个人的位置,并且可以等到所有人都说可以关掉Pod,但是如果其中一个人不响应,您会怎么做?您需要等待多长时间?请记住,在此期间,您正在暂停关机过程。

您可以做的唯一合理的事情是等待足够长的时间,以确保所有代理都已完成工作。但是多长时间足够了?在大多数情况下,几秒钟就足够了,但是显然,不能保证每次都足够。当API服务器或Endpoints控制器超载时,通知到达Kube-Proxy可能需要更长的时间。请务必理解,您无法完美解决问题,但是即使延迟5或10秒也可以大大改善用户体验。您可以使用更长的延迟,但不要过分延迟,因为延迟会阻止容器立即关闭,并导致Pod在删除后很长时间仍显示在列表中,这总是令用户感到沮丧。

正确关闭应用程序包括以下步骤:

  • 等待几秒钟,然后停止接受新连接,
  • 关闭所有不在请求中间的保持活动连接,
  • 等待所有活动请求完成,然后完全关机。

要了解此过程中连接和请求的情况,请仔细检查图3。

不像接收到终止信号后立即退出过程那样简单,对吧?所有这一切值得吗?由您决定。但是您至少可以做的是添加一个等待几秒钟的pre stop钩子。可能是这样的:

lifecycle:                   
      preStop:                   
        exec:                    
          command:               
          - sh
          - -c
          - "sleep 5"

这样,您根本不需要修改应用程序的代码。如果您的应用程序已经确保所有进行中的请求都得到了完整处理,那么您可以只需要此停止前延迟。

参考:

Kubernetes in action.