如何有效地传达 gRPC 服务中的操作降级情况?

Joh*_*xus 6 api-design grpc

我目前正在致力于增强 gRPC 服务和客户端之间通信的稳健性和清晰度,特别是围绕操作降级的概念。在许多情况下,操作可能会部分成功或遇到客户应注意的非关键问题,而不必将这些问题视为彻底失败。

标准 gRPC 响应通常包括成功响应或错误,但对于成功但带有警告的操作存在灰色区域(例如,有关接近配额限制的警告、有关优化的信息性消息或不会停止操作的小问题) 。

是否有一个行业标准来解释我们如何构建 gRPC 响应,以跨服务标准化且可供客户操作的方式包含这些“降级”详细信息?

我建议的解决方案: 我正在考虑在 protobuf 定义中引入 DegradationDetail 消息,该消息可以封装操作期间遇到的任何降级的严重性和详细信息。下面是它的草图:

message DegradationDetail {
  string message = 1; // Human-readable message describing the degradation.
  DegradationSeverity severity = 2; // The severity of the degradation.
  repeated string affected_features = 3; // Specific features or components affected, if applicable.
}

enum DegradationSeverity {
  INFO = 0; // Informational message, operation successful.
  WARNING = 1; // Warning, operation successful but might require attention.
  ERROR = 2; // Error in part of the operation, action required.

message OperationStatus {
  bool success = 1; // Indicates overall success of the operation. False might indicate that you should check the degradation details, true indicates move on.
  repeated DegradationDetail degradations = 2; // Details of specific degradations.
}


message MyServiceResponse {
  // Other fields...
  OperationStatus operation_status_foo = N; // Incorporate OperationStatus with support for multiple degradations for operation "foo".
  OperationStatus operation_status_bar = N+1; // Incorporate OperationStatus with support for multiple degradations for operation "bar".
}

Run Code Online (Sandbox Code Playgroud)

然后可以将此 DegradationDetail 作为可选字段包含在响应消息中。对于完全成功且没有任何问题的操作,该字段将不存在。对于性能下降的操作,该字段提供了一种结构化的方式来传达发生的情况、其严重性以及操作的哪些部分受到影响。如果需要,这将在每个操作的服务响应中传达。

问题

  1. 这种方法是否符合 gRPC 服务的最佳实践?我可能会定义一个公司范围的政策,并且我想确保这种模式不会与 gRPC 的设计原则或错误处理机制相冲突。
  2. 客户应该如何最好地利用这些信息?我对客户有效处理这些降级细节的模式或实践感兴趣?
  3. 集中化的 DegradationDetail 定义是否有益?这可以在公共库中定义,以确保服务之间的一致性。然而,我很好奇灵活性与标准化之间的权衡。

我渴望听到社区对这种方法的反馈,尤其是那些在 gRPC 服务中解决过类似挑战的人的反馈。

Von*_*onC 2

您的方法确实提高了 gRPC 服务和客户端之间的通信清晰度。
作为伪代码示例(在Go中,因为它是我这些天最常使用的语言)

package yourservice

import (
    "context"
    "google.golang.org/grpc"
    "path/to/your/protobufs"
)

// Implementing the service
type YourServiceServer struct {
    protobufs.UnimplementedYourServiceServer
}

func (s *YourServiceServer) YourOperation(ctx context.Context, req *protobufs.YourRequest) (*protobufs.MyServiceResponse, error) {
    // Operation logic
    
    // Example of adding degradation details
    response := &protobufs.MyServiceResponse{
        OperationStatusFoo: &protobufs.OperationStatus{
            Success: true,
            Degradations: []*protobufs.DegradationDetail{
                {
                    Message: "Nearing quota limits.",
                    Severity: protobufs.DegradationSeverity_WARNING,
                    AffectedFeatures: []string{"feature_x"},
                },
            },
        },
    }
    return response, nil
}
Run Code Online (Sandbox Code Playgroud)

是否有一个行业标准来解释我们如何构建 gRPC 响应,以跨服务标准化且可供客户操作的方式包含这些“降级”详细信息?

从来没听说过。

解决方案的主要替代方案是包含 gRPC 丰富错误模型:它允许使用Status消息发送详细的错误信息(可以包含任意元数据)。
您可以考虑将其用于错误情况,保留DegradationDetail仍需要注意的非错误情况

当组件(例如微服务)无法充分发挥功能时,还可以继续使用降级的功能,这就是“优雅降级”的概念。
该文档指向gRPC 状态代码,这远远超出了“成功/失败”的范围。

这种方法是否符合 gRPC 服务的最佳实践?

我仍然会考虑使用 gRPC 错误,利用内置的错误处理机制来包含详细的错误信息以及标准错误响应

这将涉及Status消息,它可以封装数字代码和人类可读的消息,以及google.rpc.Status带有附加错误详细信息的形式的自定义元数据。

该方法对于您想要传达更细致的错误状态或有关失败或警告的其他上下文而不必修改 RPC 调用的主要响应结构的场景可能很有用。

您可以使用 protobuf 定义自定义错误详细信息。这些可能与您的消息类似DegradationDetail,但旨在包含在错误元数据中。

// errors.proto

syntax = "proto3";

package yourpackage.errors;

import "google/protobuf/any.proto";

message DegradationDetail {
  string message = 1;
  DegradationSeverity severity = 2;
  repeated string affected_features = 3;
}

enum DegradationSeverity {
  INFO = 0;
  WARNING = 1;
  ERROR = 2;
}
Run Code Online (Sandbox Code Playgroud)

在 gRPC 服务实现中,您可以使用statusGo gRPC 库中的包来创建并返回丰富的错误,其中包含DegradationDetail错误详细信息的一部分。

package yourservice

import (
    "context"
    "google.golang.org/grpc"
    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/status"
    "google.golang.org/protobuf/proto"
    "google.golang.org/protobuf/types/known/anypb"
    "path/to/your/protobufs/yourpackage/errors"
)

func (s *YourServiceServer) YourOperation(ctx context.Context, req *protobufs.YourRequest) (*protobufs.YourResponse, error) {
    // Example operation that encounters a non-critical issue
    degradationDetail := &errors.DegradationDetail{
        Message: "Nearing quota limits.",
        Severity: errors.DegradationSeverity_WARNING,
        AffectedFeatures: []string{"feature_x"},
    }

    detailAny, err := anypb.New(degradationDetail)
    if err != nil {
        return nil, status.Errorf(codes.Internal, "Failed to marshal DegradationDetail: %v", err)
    }

    st := status.New(codes.OK, "Operation completed with warnings")
    st, err = st.WithDetails(detailAny)
    if err != nil {
        return nil, status.Errorf(codes.Internal, "Failed to add DegradationDetail to status: %v", err)
    }

    // Use st.Err() to convert the status to an error, if there are degradation details to convey.
    // If the operation is completely successful without warnings or errors, you would return a nil error.
    return &protobufs.YourResponse{}, st.Err()
}
Run Code Online (Sandbox Code Playgroud)

收到错误的客户端可以DegradationDetailStatus详细信息中提取并处理错误。

resp, err := client.YourOperation(ctx, &yourRequest)
if err != nil {
    st, ok := status.FromError(err)
    if ok && st.Code() == codes.OK {
        for _, detail := range st.Details() {
            switch t := detail.(type) {
            case *errors.DegradationDetail:
                // Handle the degradation detail (e.g., log a warning, display a message to the user)
                fmt.Printf("Degradation warning: %s\n", t.Message)
            }
        }
    } else {
        // Handle other errors
    }
} else {
    // Handle successful response
}
Run Code Online (Sandbox Code Playgroud)

这种方法的缺点:

  • 即使在仅出现警告或信息性消息的成功操作情况下,客户端也必须检查错误,这可能会使客户端逻辑复杂化。
  • 对于操作在技术上成功但出现警告或小问题的情况不太直观,因为主要的通信模式是通过错误。

客户应该如何最好地利用这些信息?

通过您的解决方案,客户可以检查operation_status每个响应的字段,以确定是否需要进一步关注。
例如,在 Go 中:

response, err := client.YourOperation(ctx, &yourRequest)
if err != nil {
    // Handle gRPC error
} else if !response.OperationStatusFoo.Success {
    // Check for and handle degradation details
    for _, degradation := range response.OperationStatusFoo.Degradations {
        switch degradation.Severity {
        case protobufs.DegradationSeverity_WARNING:
            // Log warning, possibly alert the user
        case protobufs.DegradationSeverity_ERROR:
            // Take corrective action, inform the user of partial failure
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

集中DegradationDetail定义会有好处吗?

从理论上讲,是的:灵活性与标准化的权衡通常倾向于有利于大型系统的标准化。您希望错误处理和响应格式保持一致,这将显着降低复杂性和客户端实现的负担。
但是,请确保消息中有足够的灵活性DegradationDetail来涵盖服务可能遇到的各种场景,而不会使消息过于通用或使用起来很麻烦。

并确保降级细节的引入向后兼容现有客户端。这可能涉及对 protobuf 进行版本控制或为不了解DegradationDetail结构的客户端提供默认行为。您还需要围绕退化详细信息的使用进行日志记录或监视,以跟踪和分析其发生和影响。