具有终止抢占式实例的 GCP 负载均衡器行为

GCP Load Balancer behaviour with terminating preemptive instances

背景

我们有一个调度程序实例组,每个活动虚拟机每秒接收大约 700 个请求。该调度程序位于自动缩放的负载均衡器后面。到目前为止,我们所有的 VM 都是常规 VM,但是我们一直在研究让它们成为抢占式的可能性。

抢占式实例的问题

根据文档,GCP 可以在 any time 处终止抢占式实例。

让我们假设每个调度程序 VM 都没有状态。它接收请求、处理它并向其他机器发出 HTTP 请求。

在任何给定时间,每个 VM 都会同时处理大约 700 个请求,同时从负载均衡器接收数据。

问题

如果我的抢占式 VM 在处理 700 个请求时收到终止信号,会发生什么情况?

嗯,理论上应该有一个 shutdown script 来确保处理这些请求完成,然后终止应用程序(干净退出)。这引出了一个大问题:

注意事项

如果是,那么这意味着一些请求会失败因为一旦应用程序关闭,机器仍在运行并且负载平衡器继续向机器发送请求,而不是知道应用程序已经关闭。

理想情况下,这些请求将作为失败的请求返回到负载均衡器,并将请求发送到另一台机器。然而,GCP 负载平衡器不够智能,无法做到这一点,因此它们不会。

如果负载均衡器以某种方式知道此 VM 已被选择用于抢先终止,则无需执行任何特殊操作。

是哪一个?

But does the load balancer know that my VM is shutting down? Will it keep sending requests to the terminating VM?

是,负载均衡器将继续向实例发送请求。

您将需要创建一个关闭脚本并从负载平衡器中删除您的实例。

并不是负载均衡器不够智能。负载均衡器不知道您的请求是否可以重试。该决定应由客户端/后端逻辑做出。

您的用例不是抢占式实例的好例子。抢占式实例将每 24 小时终止一次。如果您的目标是节省成本,请将长期实例定价的成本与优先定价进行比较。节省的费用不足以证明工程、测试和 QA 成本的合理性。

架构应该为失败而设计,但我不会刻意选择一个会不断失败的架构。在您的情况下,每 24 小时一次。还存在您将无法启动另一个实例来弥补增加的负载的风险。并且存在您所有实例将被终止的风险。

我们遇到了类似的问题。我们几乎已经通过负载均衡器健康检查解决了这个问题(在非常高的负载条件下存在一些问题)。 Trick 现在在抢占信号的 10-15 秒内,负载均衡器会将实例标记为不健康,因为停止向该实例发送新请求。

解法:

  1. 负载均衡器每 3 秒检查一次实例的健康状况,并在第三次健康检查后将实例标记为不健康fails.Thus负载均衡器在大约 10 秒内标记实例并停止发送新请求。
  2. 使用 ContextCloseEvent (Spring boot)Runtime.getRuntime().addShutdownHook() 在 Java 中捕获抢占信号(在我的例子中,JVM 接收到信号需要几秒钟)
  3. 将健康检查设置为失败,即健康检查端点将开始返回 404。
  4. 在关闭块中休眠 15-25 秒以让进程和新请求完成
  5. 释放资源并关闭日志记录。

    
    @EventListener
    public void onShutdown(ContextClosedEvent event) {
    
    <pre><code>log.warn("shutdown event received {}", event.getSource().toString());
    log.warn("/ping will respond 404, Main thread will sleep for 20 seconds to allow pending tasks to complete");
    
    isShuttingDown = true;
    try {
        Thread.sleep(SLEEP_BEFORE_SHUTDOWN_MILLIS);
    } catch (InterruptedException e) {
        log.error("sleep before shutdown interrupted", e);
    }
    
    log.warn("Shutting down now, daemon threads will continue work");
    releaseResources(); 
    
    log.info("{} {} on {} stopped.", NAME, VERSION, HOSTNAME);
    
    } //health endpoint @RequestMapping(value = "ping", produces = MediaType.TEXT_PLAIN_VALUE) public ResponseEntity ping() { if(isShuttingDown()) { log.warn("health failed - shutting down soon"); return new ResponseEntity(HttpStatus.NOT_FOUND); } return ResponseEntity.ok("pong"); }