配置事件驱动型函数重试

本文档介绍了如何为事件驱动型 Cloud Run functions 函数启用重试。HTTP 函数无法自动重试。

为什么事件驱动型函数会执行失败

在极少数情况下,函数可能会由于内部错误而提早退出,并且默认情况下函数可能会自动重试,也可能不会自动重试。

更常见的情况是,事件驱动型函数可能由于函数代码本身抛出错误而无法成功完成。可能导致这种情况的原因包括:

  • 函数包含 bug,且运行时抛出异常。
  • 函数无法访问服务端点,或者在尝试访问端点时超时。
  • 函数本身有意抛出异常(例如,某个参数验证失败)。
  • Node.js 函数返回遭拒的 promise,或将非 null 值传递给回调。

在上述任何情况下,函数都会停止执行并返回错误。生成消息的事件触发器具有重试政策,您可以自定义这些政策以满足您函数的需求。

重试的语义

对于事件来源发出的每个事件,Cloud Run functions 保证至少执行一次事件驱动型函数。 您配置重试的方式取决于您创建函数的方式:

  • Google Cloud 控制台中或使用 Cloud Run Admin API 创建函数时,您需要单独创建和管理事件触发器。触发器具有默认重试行为,您可以根据函数需求进行自定义。

    一般来说,您无法停用函数的重试功能(使用 Cloud Functions v2 API 创建的函数除外,此类函数的重试功能默认处于停用状态)。您无法为基于 Cloud Run Admin API 的函数停用重试功能。

  • 使用 Cloud Functions v2 API 创建函数时,Cloud Functions v2 API 会隐式创建必要的事件触发器,例如 Pub/Sub 主题或 Eventarc 触发器。默认情况下,这些触发器的重试功能处于停用状态,但您可以使用 Cloud Functions v2 API 启用该功能。

无论您的函数(含 Pub/Sub 触发器)基于哪个 API,都无法使用“仅一次”消息传送功能。该设置仅适用于拉取订阅,不适用于推送订阅。函数使用推送订阅。

使用 Cloud Run 创建的事件驱动型函数

在 Google Cloud 控制台中或使用 Cloud Run Admin API 创建的函数,需要您单独创建和管理事件触发器。我们强烈建议您查看每种触发器类型的默认行为:

使用 Cloud Functions v2 API 创建的事件驱动型函数

使用 Cloud Functions v2 API 创建的函数(例如,使用 Cloud Functions gcloud CLI、REST API 或 Terraform)将代表您创建和管理事件触发器。 默认情况下,如果函数调用因错误而终止,则该函数不会被再次调用,且相应事件会被丢弃。当您针对事件驱动型函数启用重试时,Cloud Run functions 会重试失败的函数调用,直到该函数调用成功或重试期限到期。

如果没有为函数启用重试(默认设置),则函数始终报告其成功执行,并且 200 OK 响应代码可能出现在其日志中。即使函数出现错误,也会发生这种情况。为了清楚说明函数何时出现错误,请务必适当地报告错误

启用或停用重试

如需启用或停用重试,您可以使用 Google Cloud CLI。默认情况下,不允许重试。

通过 Google Cloud CLI 配置重试

如需使用 Google Cloud CLI 启用重试,请在部署函数时添加 --retry 标志:

gcloud functions deploy FUNCTION_NAME --retry FLAGS...

要停用重试功能,请重新部署不使用 --retry 标志的函数:

gcloud functions deploy FUNCTION_NAME FLAGS...

重试期限

此重试期限会在 24 小时后到期。Cloud Run functions 会使用指数退避算法策略重试新创建的事件驱动型函数,退避时间范围为 10 到 600 秒,逐步增加。

最佳做法

本部分介绍有关如何利用重试的最佳做法。

利用重试来应对暂时性错误

由于函数会一直重试,直到成功执行为止,因此在启用重试之前应进行彻底的测试,以从代码中清除错误之类的永久性错误。重试最适合用于应对较有可能通过重试解决的间歇性或暂时性故障,比如不稳定的服务端点或超时。

设置结束条件以避免无限重试循环

在启用重试时,您应防止函数陷入连续循环。您可以在函数开始处理之前添加明确定义的结束条件。请注意,此方法仅在您的函数成功启动并且能够评估结束条件时才有效。

一种简单而有效的方法是舍弃时间戳早于特定时间的事件。在发生永久性故障或持续时间长于预期的故障时,这样可以保证函数执行次数不会太多。

例如,以下代码段会舍弃超过 10 秒前发生的所有事件:

Node.js

const functions = require('@google-cloud/functions-framework');

/**
 * Cloud Event Function that only executes within
 * a certain time period after the triggering event
 *
 * @param {object} event The Cloud Functions event.
 * @param {function} callback The callback function.
 */
functions.cloudEvent('avoidInfiniteRetries', (event, callback) => {
  const eventAge = Date.now() - Date.parse(event.time);
  const eventMaxAge = 10000;

  // Ignore events that are too old
  if (eventAge > eventMaxAge) {
    console.log(`Dropping event ${event} with age ${eventAge} ms.`);
    callback();
    return;
  }

  // Do what the function is supposed to do
  console.log(`Processing event ${event} with age ${eventAge} ms.`);

  // Retry failed function executions
  const failed = false;
  if (failed) {
    callback('some error');
  } else {
    callback();
  }
});

Python

from datetime import datetime, timezone

# The 'python-dateutil' package must be included in requirements.txt.
from dateutil import parser

import functions_framework


@functions_framework.cloud_event
def avoid_infinite_retries(cloud_event):
    """Cloud Event Function that only executes within a certain
    time period after the triggering event.

    Args:
        cloud_event: The cloud event associated with the current trigger
    Returns:
        None; output is written to Stackdriver Logging
    """
    timestamp = cloud_event["time"]

    event_time = parser.parse(timestamp)
    event_age = (datetime.now(timezone.utc) - event_time).total_seconds()
    event_age_ms = event_age * 1000

    # Ignore events that are too old
    max_age_ms = 10000
    if event_age_ms > max_age_ms:
        print("Dropped {} (age {}ms)".format(cloud_event["id"], event_age_ms))
        return "Timeout"

    # Do what the function is supposed to do
    print("Processed {} (age {}ms)".format(cloud_event["id"], event_age_ms))
    return  # To retry the execution, raise an exception here

Go


// Package tips contains tips for writing Cloud Functions in Go.
package tips

import (
	"context"
	"fmt"
	"log"
	"time"

	"github.com/GoogleCloudPlatform/functions-framework-go/functions"
	"github.com/cloudevents/sdk-go/v2/event"
)

func init() {
	functions.CloudEvent("FiniteRetryPubSub", FiniteRetryPubSub)
}

// MessagePublishedData contains the full Pub/Sub message
// See the documentation for more details:
// https://cloud.google.com/eventarc/docs/cloudevents#pubsub
type MessagePublishedData struct {
	Message PubSubMessage
}

// PubSubMessage is the payload of a Pub/Sub event.
// See the documentation for more details:
// https://cloud.google.com/pubsub/docs/reference/rest/v1/PubsubMessage
type PubSubMessage struct {
	Data []byte `json:"data"`
}

// FiniteRetryPubSub demonstrates how to avoid inifinite retries.
func FiniteRetryPubSub(ctx context.Context, e event.Event) error {
	var msg MessagePublishedData
	if err := e.DataAs(&msg); err != nil {
		return fmt.Errorf("event.DataAs: %w", err)
	}

	// Ignore events that are too old.
	expiration := e.Time().Add(10 * time.Second)
	if time.Now().After(expiration) {
		log.Printf("event timeout: halting retries for expired event '%q'", e.ID())
		return nil
	}

	// Add your message processing logic.
	return processTheMessage(msg)
}

Java


import com.google.cloud.functions.CloudEventsFunction;
import io.cloudevents.CloudEvent;
import java.time.Duration;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.logging.Logger;

public class RetryTimeout implements CloudEventsFunction {
  private static final Logger logger = Logger.getLogger(RetryTimeout.class.getName());
  private static final long MAX_EVENT_AGE = 10_000;

  /**
   * Cloud Event Function that only executes within
   * a certain time period after the triggering event
   */
  @Override
  public void accept(CloudEvent event) throws Exception {
    ZonedDateTime utcNow = ZonedDateTime.now(ZoneOffset.UTC);
    ZonedDateTime timestamp = event.getTime().atZoneSameInstant(ZoneOffset.UTC);

    long eventAge = Duration.between(timestamp, utcNow).toMillis();

    // Ignore events that are too old
    if (eventAge > MAX_EVENT_AGE) {
      logger.info(String.format(