Envoy方案实现分析报告

一、架构概述

1.1 系统架构

scss 复制代码
HTTP Client (Webhook / API Client)
    ↓
Envoy Proxy (Port: 2020)
    ├─ HTTP Connection Manager
    │   └─ HTTP Filters Chain
    │       ├─ Lua Filter (响应转换,可选)
    │       ├─ gRPC-JSON Transcoding Filter
    │       └─ Router Filter
    │           └─ Route Configuration
    │               ├─ /api/v1/payment/channels → payment_gateway_grpc
    │               ├─ /api/v2/payment/webhook/stripe/{account_id} → payment_stripe_grpc
    │               └─ /healthz → payment_stripe_grpc
    ↓
gRPC Services
    ├─ payment_stripe_grpc (172.29.119.29:8080)
    └─ payment_gateway_grpc (172.29.119.29:8090)

1.2 核心功能

  1. gRPC 到 HTTP 的协议转换:通过 gRPC-JSON Transcoding 将 HTTP/JSON 请求转换为 gRPC 调用
  2. 响应格式统一:通过 Lua 过滤器(可选)将 gRPC 响应格式转换为统一的 API 响应格式
  3. 多服务路由:支持多个 gRPC 服务的统一入口和路由分发
  4. 协议桥接:实现 HTTP RESTful API 与 gRPC 服务之间的无缝桥接

二、工作原理:Envoy 如何获取和传递外部请求

2.1 外部请求到 Envoy 的完整流程

外部业务请求示例(以 Stripe Webhook 为例):

bash 复制代码
POST /api/v2/payment/webhook/stripe/acct_123456 HTTP/1.1
Host: api.example.com
Content-Type: application/json
Stripe-Signature: t=1234567890,v1=signature_value_here

{
  "id": "evt_123",
  "type": "payment.succeeded",
  "data": {...}
}

2.2 Envoy 接收和提取过程

2.2.1 步骤 1:Envoy 接收 HTTP 请求

Envoy Listener 接收请求

yaml 复制代码
listeners:
  - name: listener_0
    address:
      socket_address:
        address: 0.0.0.0
        port_value: 2020  # Envoy 监听端口
  • 外部请求到达 Envoy 的 2020 端口
  • Envoy HTTP Connection Manager 接收完整的 HTTP 请求
  • 包含:HTTP Method、Path、Headers、Body

2.2.2 步骤 2:Router Filter 匹配路由

路由匹配

yaml 复制代码
routes:
  - match:
      path: "/api/v2/payment/webhook/stripe/{account_id}"
      headers:
        - name: ":method"
          exact_match: "POST"
    route:
      cluster: payment_stripe_grpc
  • Router Filter 解析 HTTP Path:/api/v2/payment/webhook/stripe/acct_123456
  • 匹配路由规则,确定目标集群:payment_stripe_grpc
  • 此时 Path 和 Headers 都在 Envoy 内部,还未提取

2.2.3 步骤 3:gRPC-JSON Transcoding 提取和转换

gRPC-JSON Transcoding Filter 的工作

  1. 读取 Proto Descriptor

    1. 加载 payment_combined.pb 文件
    2. 查找匹配的 HTTP 注解:post: "/api/v2/payment/webhook/stripe/{account_id}"
  2. 提取 Path 参数

    bash 复制代码
    HTTP Path: /api/v2/payment/webhook/stripe/acct_123456
    Proto 模式: /api/v2/payment/webhook/stripe/{account_id}
    ↓
    提取: account_id = "acct_123456"
  3. 提取请求体(Body)

    css 复制代码
    HTTP Body: {"id": "evt_123", "type": "payment.succeeded", ...}
    ↓
    根据 body: "*" 规则,将整个 JSON 映射到 gRPC 消息
    如果 proto 定义中有 payload 字段,会尝试映射
  4. 处理 Header

    1. 重要 :gRPC-JSON Transcoding 默认不会将 HTTP Header 映射到 gRPC 消息字段
    2. 但 Envoy 会自动将 HTTP Header 转换为 gRPC Metadata(这是 Envoy 的默认行为)

2.3 Header 传递机制详解

2.3.1 Envoy 自动将 HTTP Header 转换为 gRPC Metadata

Envoy 的默认行为

  • 所有 HTTP Header 都会自动转换为 gRPC Metadata
  • Header 名称转换为小写,连字符保留
  • 例如:Stripe-Signature → gRPC Metadata key: stripe-signature

转换规则

yaml 复制代码
HTTP Header: Stripe-Signature: t=1234567890,v1=signature_value
    ↓
gRPC Metadata: 
  Key: "stripe-signature"
  Value: ["t=1234567890,v1=signature_value"]

2.3.2 在 gRPC 服务中获取 Header(通过 Metadata)

gRPC 服务代码示例

go 复制代码
func (s *PaymentStripeService) HandleWebhook(ctx context.Context, req *pb.HandleWebhookReq) (*pb.HandleWebhookResp, error) {
    // 1. 从 gRPC Metadata 中获取 HTTP Header
    md, ok := metadata.FromIncomingContext(ctx)
    if !ok {
        return nil, errors.New("无法获取 metadata")
    }
    
    // 2. 获取 Stripe-Signature header(已转换为小写)
    signatureValues := md.Get("stripe-signature")
    signature := ""
    if len(signatureValues) > 0 {
        signature = signatureValues[0]  // 获取第一个值
    }
    
    // 3. 从请求消息获取其他参数
    payload := req.GetPayload()      // 从 HTTP Body 获取
    accountID := req.GetAccountId()  // 从 Path 参数获取
    
    // 4. 现在可以验证签名了
    if err := s.verifySignature(payload, signature, accountID); err != nil {
        return &pb.HandleWebhookResp{
            Code: 400,
            Msg: "签名验证失败",
        }, nil
    }
    
    // 5. 处理业务逻辑
    // ...
}

2.4 Path 参数传递机制详解

2.4.1 Proto 定义中的路径参数

ini 复制代码
rpc HandleWebhook(HandleWebhookReq) returns (HandleWebhookResp) {
  option (google.api.http) = {
    post: "/api/v2/payment/webhook/stripe/{account_id}"  // {account_id} 是路径参数占位符
    body: "*"
  };
}

message HandleWebhookReq {
  bytes payload = 1;
  string signature = 2;
  string account_id = 3;  // 路径参数会映射到这个字段
}

2.4.2 gRPC-JSON Transcoding 的提取过程

详细步骤

  1. 解析 HTTP Path

    bash 复制代码
    外部请求: POST /api/v2/payment/webhook/stripe/acct_123456
  2. 匹配 Proto 注解

    arduino 复制代码
    Proto 注解: post: "/api/v2/payment/webhook/stripe/{account_id}"
    匹配成功!
  3. 提取路径参数

    bash 复制代码
    路径模式: /api/v2/payment/webhook/stripe/{account_id}
    实际路径: /api/v2/payment/webhook/stripe/acct_123456
    ↓
    提取参数: account_id = "acct_123456"
  4. 映射到 gRPC 消息字段

    perl 复制代码
    字段名匹配: {account_id} → HandleWebhookReq.account_id
    ↓
    构建 gRPC 消息:
    HandleWebhookReq {
      account_id: "acct_123456"  // ✅ 从路径提取
    }

2.5 完整数据流示例

外部请求

bash 复制代码
POST /api/v2/payment/webhook/stripe/acct_123456 HTTP/1.1
Host: api.example.com
Content-Type: application/json
Stripe-Signature: t=1234567890,v1=signature_value

{"id": "evt_123", "type": "payment.succeeded"}

Envoy 处理流程

javascript 复制代码
1. Envoy Listener 接收请求
   ├─ Method: POST
   ├─ Path: /api/v2/payment/webhook/stripe/acct_123456
   ├─ Headers: 
   │   ├─ Content-Type: application/json
   │   └─ Stripe-Signature: t=1234567890,v1=signature_value
   └─ Body: {"id": "evt_123", "type": "payment.succeeded"}

2. Router Filter 匹配路由
   └─ 匹配到: /api/v2/payment/webhook/stripe/{account_id}
   └─ 目标集群: payment_stripe_grpc

3. gRPC-JSON Transcoding Filter 转换
   ├─ 读取 proto descriptor
   ├─ 匹配 HTTP 注解: post: "/api/v2/payment/webhook/stripe/{account_id}"
   ├─ 提取路径参数: account_id = "acct_123456"
   ├─ 提取请求体: payload = {"id": "evt_123", ...} (转换为 bytes)
   └─ 构建 gRPC 消息:
       HandleWebhookReq {
         payload: [bytes from body]
         account_id: "acct_123456"  // 从路径提取
       }

4. Envoy 自动转换 HTTP Header → gRPC Metadata
   └─ Stripe-Signature → Metadata["stripe-signature"] = "t=1234567890,v1=signature_value"

5. 发起 gRPC 调用
   └─ PaymentStripe/HandleWebhook(HandleWebhookReq)
   └─ Metadata: {"stripe-signature": ["t=1234567890,v1=signature_value"]}

6. gRPC 服务接收
   ├─ req.GetAccountId() → "acct_123456"  // 从 gRPC 消息获取
   ├─ req.GetPayload() → [bytes]           // 从 gRPC 消息获取
   └─ metadata.Get("stripe-signature") → "t=1234567890,v1=signature_value"  // 从 Metadata 获取

2.6 关键要点总结

  1. Path 参数提取

    1. ✅ 通过 proto 注解中的 {account_id} 自动提取
    2. ✅ 自动映射到 gRPC 消息字段 account_id
    3. ✅ 字段名必须匹配
  2. Header 传递

    1. ✅ Envoy 自动将 HTTP Header 转换为 gRPC Metadata
    2. ✅ 不需要额外配置
    3. ✅ 在 gRPC 服务中通过 metadata.FromIncomingContext(ctx) 获取
    4. 不会自动映射到 gRPC 消息字段
  3. 请求体传递

    1. ✅ 通过 body: "*" 规则映射到 gRPC 消息字段
    2. ✅ JSON 字段名必须与 proto 字段名匹配
  4. 数据来源总结

    1. Path 参数 → gRPC 消息字段(自动映射)
    2. HTTP Header → gRPC Metadata(自动转换)
    3. HTTP Body → gRPC 消息字段(根据 body 规则映射)

三、配置操作:如何添加和管理接口

3.1 添加新的 API 接口(以 ListChannels 为例)

场景 :在 PaymentGateway 服务中添加 ListChannels API 接口,将 gRPC 方法暴露为 HTTP GET 接口

完整操作流程

步骤 1:在 hh-idl 中修改 Proto 文件,添加 HTTP 注解

文件位置hh-idl/payment_gateway/v1/hh_payment_gateway.proto

需要修改的地方

  1. 确保导入 HTTP 注解(文件开头):
ini 复制代码
syntax = "proto3";
package hh_payment_gateway.v1;
option go_package = "./v1";

import "common/payment.proto";
import "common/base.proto";
import "google/api/annotations.proto";  // 必须导入这个
  1. 在服务方法上添加 HTTP 注解
scss 复制代码
service PaymentGateway {
  rpc ListChannels(ListChannelsReq) returns (ListChannelsResp) {
    option (google.api.http) = {
      get: "/api/v1/payment/channels"  // 定义 HTTP 路径和方法
    };
  };
  // 其他方法...
}

关键点

  • option (google.api.http) 是 gRPC-JSON Transcoding 的注解
  • get: "/api/v1/payment/channels" 定义 HTTP GET 方法和路径
  • 路径参数可以使用 {param_name} 语法,如 /api/v2/payment/webhook/stripe/{account_id}
  • 支持 getpostputdeletepatch 等 HTTP 方法

步骤 2:编译生成 payment_combined.pb 文件

执行编译脚本

bash 复制代码
cd /apps/www/go/src/hh_bmp/envoy
./build-proto.sh

编译过程

  1. 脚本会检查 protoc 是否安装
  2. 读取 hh-idl/payment_gateway/v1/hh_payment_gateway.proto 文件
  3. 使用 --include_imports 包含所有依赖(如 common/payment.protocommon/base.proto
  4. 使用 --include_source_info 包含源信息(用于错误提示)
  5. 生成 proto/payment_combined.pb 文件(包含所有服务的 descriptor set)

生成的 pb 文件包含

  • ListChannels 方法的定义
  • HTTP 注解信息(get: "/api/v1/payment/channels"
  • 所有依赖的 proto 文件定义
  • 源信息(用于调试)

验证生成的文件

bash 复制代码
ls -lh proto/payment_combined.pb
# 应该能看到生成的 .pb 文件

步骤 3:在 envoy.yaml 中配置路由(可选,但推荐)

文件位置envoy/envoy.yaml

配置位置route_config.routes 部分

添加路由配置

yaml 复制代码
route_config:
  name: local_route
  virtual_hosts:
    - name: local_service
      domains: ["*"]
      routes:
        # Payment Gateway - ListChannels 路由(放在前面,优先匹配)
        # proto 注解:get: "/api/v1/payment/channels"
        - match:
            path: "/api/v1/payment/channels"
          route:
            cluster: payment_gateway_grpc
            timeout: 5s
        # 其他路由...

关键点

  • path: "/api/v1/payment/channels" 必须与 proto 中的 HTTP 注解路径一致
  • cluster: payment_gateway_grpc 指向对应的 gRPC 服务集群
  • timeout: 5s 设置请求超时时间
  • 路由顺序很重要:精确匹配的路由应该放在前面,默认路由放在最后

为什么需要手动添加路由?

  • gRPC-JSON Transcoding 可以根据 proto 注解自动处理请求转换

  • 但手动添加路由可以:

    • 设置自定义超时时间
    • 添加重试策略
    • 设置优先级
    • 添加其他路由规则

步骤 4:确保 gRPC-JSON Transcoding 配置正确

检查 envoy.yaml 中的配置

bash 复制代码
http_filters:
  - name: envoy.filters.http.grpc_json_transcoder
    typed_config:
      "@type": type.googleapis.com/envoy.extensions.filters.http.grpc_json_transcoder.v3.GrpcJsonTranscoder
      proto_descriptor: "/etc/envoy/proto/payment_combined.pb"  # 使用编译生成的 pb 文件
      services:
        - "hh_payment_stripe.v1.PaymentStripe"
        - "hh_payment_gateway.v1.PaymentGateway"  # 确保包含这个服务

关键点

  • proto_descriptor 指向编译生成的 payment_combined.pb 文件
  • services 列表必须包含 hh_payment_gateway.v1.PaymentGateway
  • 如果添加了新服务,需要在这里添加服务名

步骤 5:重启 Envoy 以加载新配置

重启后,Envoy 会:

  1. 加载新的 payment_combined.pb 文件
  2. 读取 ListChannels 的 HTTP 注解
  3. 建立 HTTP 路径 /api/v1/payment/channels 到 gRPC 方法 ListChannels 的映射

测试接口

bash 复制代码
curl http://localhost:2020/api/v1/payment/channels

请求流程

  1. HTTP GET 请求 → /api/v1/payment/channels
  2. Envoy Router Filter → 匹配路由 → payment_gateway_grpc 集群
  3. gRPC-JSON Transcoding → 根据 proto 注解转换为 gRPC 调用
  4. gRPC 调用 → hh_payment_gateway.v1.PaymentGateway/ListChannels
  5. gRPC 响应 → gRPC-JSON Transcoding 转换为 JSON
  6. Lua Filter(如果使用)→ 响应格式转换
  7. HTTP 响应返回客户端

3.2 添加 Webhook 接口(以 Stripe Webhook 为例)

场景:添加 Stripe Webhook 接口,接收 POST 请求

操作步骤

步骤 1:在 hh-idl 中修改 Proto 文件

文件位置hh-idl/payment_stripe/v1/hh_payment_stripe.proto

添加 HTTP 注解

scss 复制代码
service PaymentStripe {
  rpc HandleWebhook(HandleWebhookReq) returns (HandleWebhookResp) {
    option (google.api.http) = {
      post: "/api/v2/payment/webhook/stripe/{account_id}"  // POST 方法,路径参数
      body: "*"  // 请求体映射到消息的所有字段
    };
  };
}

关键点

  • post: 定义 HTTP POST 方法
  • {account_id} 是路径参数,会自动映射到 HandleWebhookReq.account_id 字段
  • body: "*" 表示请求体映射到消息的所有字段

步骤 2:编译生成 pb 文件

bash 复制代码
./build-proto.sh

步骤 3:在 envoy.yaml 中添加路由

yaml 复制代码
routes:
  # Stripe Webhook 路由
  - match:
      path: "/api/v2/payment/webhook/stripe/{account_id}"
      headers:
        - name: ":method"
          exact_match: "POST"
    route:
      cluster: payment_stripe_grpc
      timeout: 5s

关键点

  • 路径必须与 proto 注解中的路径一致
  • 可以限制 HTTP 方法为 POST
  • 路径参数 {account_id} 会被自动提取

3.3 修改服务地址

场景:切换环境或服务地址变更

操作步骤

直接修改 clusters 中的 addressport_value

yaml 复制代码
clusters:
  - name: payment_gateway_grpc
    load_assignment:
      cluster_name: payment_gateway_grpc
      endpoints:
        - lb_endpoints:
            - endpoint:
                address:
                  socket_address:
                    address: 新地址  # 修改这里
                    port_value: 新端口  # 修改这里

3.4 配置调整操作

调整超时时间

yaml 复制代码
routes:
  - match:
      path: "/api/v1/payment/channels"
    route:
      cluster: payment_gateway_grpc
      timeout: 10s  # 从 5s 调整为 10s

调整连接超时

yaml 复制代码
clusters:
  - name: payment_gateway_grpc
    connect_timeout: 10s  # 从 5s 调整为 10s

添加负载均衡配置

yaml 复制代码
clusters:
  - name: payment_gateway_grpc
    circuit_breakers:
      thresholds:
        - max_connections: 1000
          max_pending_requests: 1000
          max_requests: 1000

四、架构设计原则:Envoy 的职责边界

4.1 核心原则

所有业务逻辑都应该在 RPC 服务中处理,Envoy 仅做透传和协议转换是最佳实践。

4.2 Envoy 应该做什么(职责范围)

4.2.1 协议转换

职责

  • HTTP/JSON → gRPC 协议转换
  • gRPC → HTTP/JSON 协议转换
  • 路径参数映射(URL 路径参数 → gRPC 消息字段)
  • 请求体映射(HTTP Body → gRPC 消息)

实现方式

  • 使用 gRPC-JSON Transcoding 过滤器
  • 基于 proto 文件中的 HTTP 注解自动转换

4.2.2 路由和负载均衡

职责

  • 根据路径匹配路由到不同的 gRPC 服务
  • 负载均衡(多个 gRPC 服务实例)
  • 故障转移和重试

实现方式

  • Router Filter 处理路由
  • Cluster 配置处理负载均衡

4.2.3 基础设施功能

职责

  • 连接管理(连接池、超时)
  • 健康检查
  • 熔断器
  • 统计和监控

实现方式

  • Envoy 内置功能,通过配置启用

4.3 Envoy 不应该做什么(职责边界)

4.3.1 业务逻辑处理

不应该做

  • ❌ 签名验证
  • ❌ 参数验证(业务规则)
  • ❌ 数据转换(业务格式转换)
  • ❌ 权限检查
  • ❌ 业务规则判断

原因

  • 业务逻辑应该在 RPC 服务中处理,便于测试和维护
  • Envoy 层处理业务逻辑会增加配置复杂度
  • 业务逻辑变更需要修改 Envoy 配置,耦合度高

4.3.2 数据转换(业务格式)

不应该做

  • ❌ 响应格式转换(如 baseModelcode/msg
  • ❌ 数据字段映射
  • ❌ 业务数据格式化

原因

  • 如果使用 Lua 过滤器做响应格式转换,说明 gRPC 服务返回的格式不符合要求
  • 最佳实践:gRPC 服务直接返回符合 API 要求的格式,无需转换

替代方案

  • gRPC 服务统一返回格式:{code, msg, data}
  • 移除 Lua 过滤器,Envoy 只做协议转换

4.4 RPC 服务应该处理什么

4.4.1 所有业务逻辑

必须处理

  1. 签名验证

    go 复制代码
    func (s *PaymentStripeService) HandleWebhook(ctx context.Context, req *pb.HandleWebhookReq) (*pb.HandleWebhookResp, error) {
        // 从 Metadata 获取 signature
        md, _ := metadata.FromIncomingContext(ctx)
        signature := ""
        if values := md.Get("stripe-signature"); len(values) > 0 {
            signature = values[0]
        }
        
        // 验证 Stripe 签名
        if err := s.verifyStripeSignature(req.Payload, signature, req.AccountId); err != nil {
            return &pb.HandleWebhookResp{
                Code: 400,
                Msg: "签名验证失败",
            }, nil
        }
        // 处理业务逻辑...
    }
  2. 参数验证

    go 复制代码
    func (s *PaymentGatewayService) CreatePayment(ctx context.Context, req *pb.CreatePaymentReq) (*pb.CreatePaymentResp, error) {
        // 参数验证
        if req.Amount <= 0 {
            return &pb.CreatePaymentResp{
                Code: 400,
                Msg: "金额必须大于 0",
            }, nil
        }
        // 业务逻辑验证
        if err := s.validatePayment(req); err != nil {
            return &pb.CreatePaymentResp{
                Code: 400,
                Msg: err.Error(),
            }, nil
        }
        // 处理业务逻辑...
    }
  3. 权限检查

    go 复制代码
    func (s *PaymentGatewayService) GetPayment(ctx context.Context, req *pb.GetPaymentReq) (*pb.GetPaymentResp, error) {
        // 权限检查
        if !s.hasPermission(ctx, req.MerchantId) {
            return &pb.GetPaymentResp{
                Code: 403,
                Msg: "无权限访问",
            }, nil
        }
        // 处理业务逻辑...
    }
  4. 统一响应格式

    go 复制代码
    func (s *PaymentGatewayService) ListChannels(ctx context.Context, req *pb.ListChannelsReq) (*pb.ListChannelsResp, error) {
        // 业务逻辑处理
        channels, err := s.getChannels()
        if err != nil {
            return &pb.ListChannelsResp{
                Code: 500,
                Msg: err.Error(),
                Data: nil,
            }, nil
        }
        
        // 直接返回符合 API 要求的格式
        return &pb.ListChannelsResp{
            Code: 200,
            Msg: "success",
            Data: channels,  // 直接使用 data 字段,而不是 channels
        }, nil
    }

4.4.2 错误处理

必须处理

  • 所有错误都通过响应消息返回,而不是 gRPC 错误
  • 统一的错误码和错误消息格式
  • 详细的错误信息(便于调试)

4.5 响应格式处理范围

4.5.1 需要统一响应格式的方法

仅处理有 HTTP 注解的 RPC 方法

这些方法需要通过 Envoy 暴露为 HTTP API,响应格式需要统一为 {code, msg, data}

scss 复制代码
service PaymentGateway {
  // ✅ 有 HTTP 注解,需要统一响应格式
  rpc ListChannels(ListChannelsReq) returns (ListChannelsResp) {
    option (google.api.http) = {
      get: "/api/v1/payment/channels"
    };
  };
}

service PaymentStripe {
  // ✅ 有 HTTP 注解,需要统一响应格式
  rpc HandleWebhook(HandleWebhookReq) returns (HandleWebhookResp) {
    option (google.api.http) = {
      post: "/api/v2/payment/webhook/stripe/{account_id}"
    };
  };
}

处理方式

  • 修改 proto 文件,响应消息使用 {code, msg, data} 格式
  • 在 RPC 服务实现中直接返回统一格式
  • 无需 Lua 过滤器转换

4.5.2 不需要统一响应格式的方法

仅用于内部 gRPC 调用的方法

这些方法没有 HTTP 注解,仅用于服务间 gRPC 调用,可以保持原有格式:

scss 复制代码
service PaymentGateway {
  // ❌ 无 HTTP 注解,仅内部调用,可以保持 baseModel 格式
  rpc CreatePayment(CreatePaymentReq) returns (CreatePaymentResp);
  rpc GetPayment(GetPaymentReq) returns (GetPaymentResp);
  rpc CancelPayment(CancelPaymentReq) returns (CancelPaymentResp);
}

处理方式

  • 保持原有响应格式(如 baseModel
  • 无需修改
  • 不影响 Envoy 配置

4.5.3 判断标准

判断是否需要统一响应格式

  • ✅ 有 option (google.api.http) 注解 → 需要统一格式
  • ❌ 无 HTTP 注解 → 可以保持原有格式

总结

  • 只需要处理需要通过 Envoy 转换的 RPC 的响应参数
  • 内部 gRPC 调用方法无需修改响应格式

4.6 最佳实践总结

4.6.1 职责划分

组件 职责 示例
Envoy 协议转换、路由、负载均衡 HTTP → gRPC、路径匹配
RPC 服务 所有业务逻辑 签名验证、参数验证、业务处理

4.6.2 架构设计原则

  1. Envoy 做透传和转换

    1. ✅ 协议转换(HTTP ↔ gRPC)
    2. ✅ 路由和负载均衡
    3. ✅ 基础设施功能(健康检查、熔断等)
    4. ❌ 不做业务逻辑处理
  2. RPC 服务处理所有业务逻辑

    1. ✅ 签名验证
    2. ✅ 参数验证
    3. ✅ 权限检查
    4. ✅ 业务规则判断
    5. ✅ 统一响应格式(仅针对有 HTTP 注解的方法)
  3. 响应格式统一在 RPC 服务层

    1. 仅处理需要通过 Envoy 转换为 HTTP API 的 RPC 方法
    2. ✅ 这些方法的响应格式统一为 {code, msg, data}
    3. ✅ 内部 gRPC 调用(无 HTTP 注解)的方法可以保持原有格式
    4. ✅ 无需 Lua 过滤器转换
    5. ✅ Envoy 只做协议转换,不做格式转换

4.6.3 实施建议

阶段 1:当前状态(使用 Lua)

  • Envoy:协议转换 + 响应格式转换(Lua)
  • RPC 服务:业务逻辑处理

阶段 2:目标状态(移除 Lua)

  • Envoy:仅协议转换
  • RPC 服务:业务逻辑 + 统一响应格式

迁移步骤

  1. 识别需要通过 Envoy 暴露的 RPC 方法 (有 google.api.http 注解的)
  2. 仅统一这些方法的响应格式{code, msg, data}
  3. 内部 gRPC 调用方法 (无 HTTP 注解)可以保持原有格式(如 baseModel
  4. 移除 Envoy 中的 Lua 过滤器
  5. 验证所有接口正常工作

五、使用 Envoy 的优势分析

5.1 统一入口管理

优势

  • 单一配置点 :所有 webhook 和 API 接口在一个 envoy.yaml 文件中管理
  • 统一路由规则:路由、超时、重试等策略统一配置
  • 集中监控:通过 Envoy Admin 接口统一查看所有服务的统计信息

对比:如果每个服务都搭建独立的 API 项目,需要:

  • 每个项目独立配置
  • 每个项目独立监控
  • 配置分散,难以统一管理

5.2 协议转换能力

优势

  • 自动转换:gRPC-JSON Transcoding 自动将 HTTP/JSON 转换为 gRPC
  • 基于 Proto:转换规则基于 proto 文件,类型安全
  • 零代码:无需编写转换代码,只需配置

对比:如果每个服务都搭建独立的 API 项目,需要:

  • 手动编写 HTTP 到 gRPC 的转换代码
  • 维护转换逻辑
  • 处理类型转换错误

5.3 维护成本

优势

  • Proto 驱动:proto 文件变更只需重新编译,无需修改代码
  • 配置即代码:所有配置都在 YAML 文件中,易于版本控制
  • 统一升级:Envoy 升级只需更新一个组件

对比:如果每个服务都搭建独立的 API 项目,需要:

  • 每个项目独立维护
  • 每个项目独立升级
  • 代码重复,维护成本高

5.4 性能优势

优势

  • C++ 实现:Envoy 是 C++ 实现,性能优异
  • HTTP/2 支持:原生支持 HTTP/2,连接复用
  • 高效转换:gRPC-JSON Transcoding 是编译时优化,性能好

对比:如果每个服务都搭建独立的 API 项目,需要:

  • 每个项目独立处理 HTTP 请求
  • 可能使用性能较低的框架
  • 资源消耗更大

5.5 扩展性

优势

  • 易于扩展:添加新服务只需修改配置,无需新建项目
  • 统一标准:所有服务遵循相同的接口标准
  • 灵活路由:支持复杂的路由规则和负载均衡

对比:如果每个服务都搭建独立的 API 项目,需要:

  • 每个新服务都需要新建项目
  • 项目间可能标准不一致
  • 扩展成本高

六、方案对比:Envoy vs 各自搭建 API 项目

6.1 需求背景分析

需求特点

  1. 多个服务的 webhook 请求:Stripe、PayPal、GitHub 等
  2. 部分需要 RPC 转换为 API 的接口:如 ListChannels、GetPayment 等
  3. 服务数量可能持续增长:未来可能有更多 webhook 和 API 接口

6.2 方案对比

方案 A:使用 Envoy 统一代理

架构

arduino 复制代码
HTTP Client
    ↓
Envoy (统一入口)
    ├─ Webhook 路由 → gRPC 服务
    ├─ API 路由 → gRPC 服务
    └─ 其他路由 → gRPC 服务

优势

  1. 统一管理

    1. 单一配置文件管理所有路由
    2. 统一监控和统计
    3. 统一升级和维护
  2. 快速扩展

    1. 添加新服务只需修改配置
    2. 无需新建项目
    3. 配置即代码,易于版本控制
  3. 协议转换

    1. 自动 HTTP/JSON → gRPC 转换
    2. 基于 proto 文件,类型安全
    3. 零代码转换
  4. 性能优势

    1. C++ 实现,性能优异
    2. HTTP/2 支持,连接复用
    3. 高效的路由和负载均衡
  5. 标准化

    1. 所有服务遵循相同的接口标准
    2. 统一的错误处理和响应格式
    3. 易于团队协作

劣势

  1. 学习成本

    1. 需要学习 Envoy 配置
    2. 需要理解 gRPC-JSON Transcoding
  2. 调试复杂度

    1. 问题可能出现在 Envoy 层或 gRPC 服务层
    2. 需要理解整个数据流
  3. 依赖 Envoy

    1. 如果 Envoy 出现问题,影响所有服务
    2. 需要维护 Envoy 配置

方案 B:每个服务各自搭建 API 项目

架构

arduino 复制代码
HTTP Client
    ├─ Stripe Webhook → Stripe API 项目 → Stripe gRPC 服务
    ├─ PayPal Webhook → PayPal API 项目 → PayPal gRPC 服务
    ├─ Payment API → Payment API 项目 → Payment gRPC 服务
    └─ 其他服务 → 各自的 API 项目 → 各自的 gRPC 服务

优势

  1. 独立性

    1. 每个服务独立部署和升级
    2. 问题隔离,不影响其他服务
    3. 团队可以独立开发
  2. 灵活性

    1. 每个服务可以使用不同的技术栈
    2. 可以针对特定服务优化
    3. 不受统一框架限制
  3. 简单性

    1. 每个项目相对简单
    2. 不需要理解 Envoy 配置
    3. 调试相对容易

劣势

  1. 重复代码

    1. 每个项目都需要实现 HTTP → gRPC 转换
    2. 每个项目都需要实现签名验证
    3. 每个项目都需要实现参数验证
    4. 代码重复,维护成本高
  2. 维护成本

    1. 每个项目独立维护
    2. 每个项目独立升级
    3. 配置分散,难以统一管理
  3. 扩展成本

    1. 每个新服务都需要新建项目
    2. 需要重复实现相同的功能
    3. 资源消耗更大
  4. 标准化困难

    1. 不同项目可能标准不一致
    2. 难以统一监控和统计
    3. 团队协作成本高

6.3 成本对比

维度 Envoy 方案 各自搭建 API 项目
初始开发 低(只需配置) 高(每个项目都要开发)
添加新服务 低(修改配置) 高(新建项目)
维护成本 低(统一维护) 高(分散维护)
代码重复 高(每个项目重复)
学习成本 中(学习 Envoy) 低(使用熟悉框架)
性能 高(C++ 实现) 中(取决于框架)
扩展性 高(易于扩展) 低(需要新建项目)

6.4 适用场景

使用 Envoy 的场景

  1. 服务数量多:有多个 webhook 和 API 接口需要管理
  2. 统一标准:希望所有服务遵循相同的接口标准
  3. 快速扩展:需要频繁添加新服务
  4. 性能要求高:对性能有较高要求
  5. 团队协作:希望统一管理和监控

各自搭建 API 项目的场景

  1. 服务数量少:只有 1-2 个服务
  2. 独立性要求高:每个服务需要完全独立
  3. 技术栈不同:不同服务需要使用不同的技术栈
  4. 团队独立:不同团队独立开发,不需要统一管理

6.5 基于需求背景的建议

基于您的需求背景

  • 可能有很多服务的 webhook 请求
  • 部分需要 RPC 转换为 API 的接口
  • 服务数量可能持续增长

建议:使用 Envoy 方案

理由

  1. 符合需求特点

    1. 多个 webhook 服务可以通过 Envoy 统一管理
    2. RPC 转 API 的需求可以通过 gRPC-JSON Transcoding 自动实现
    3. 服务数量增长时,只需修改配置,无需新建项目
  2. 成本效益高

    1. 初始开发成本低(只需配置)
    2. 维护成本低(统一维护)
    3. 扩展成本低(修改配置即可)
  3. 标准化优势

    1. 所有服务遵循相同的接口标准
    2. 统一的错误处理和响应格式
    3. 易于团队协作
  4. 性能优势

    1. Envoy 是 C++ 实现,性能优异
    2. HTTP/2 支持,连接复用
    3. 高效的路由和负载均衡

实施建议

  1. 初期:使用 Envoy + Lua 过滤器(如果已有服务,不想改动)
  2. 长期:逐步统一 gRPC 服务响应格式,移除 Lua 过滤器
  3. 扩展:新服务直接使用统一的响应格式,无需 Lua 转换

七、Envoy 高可用方案

7.1 Envoy 实例高可用

7.1.1 多实例部署

架构设计

markdown 复制代码
                    ┌─────────────┐
                    │  负载均衡器  │
                    │  (Nginx/ALB) │
                    └──────┬──────┘
                           │
            ┌──────────────┼──────────────┐
            │              │              │
      ┌─────▼─────┐  ┌─────▼─────┐  ┌─────▼─────┐
      │  Envoy-1  │  │  Envoy-2  │  │  Envoy-3  │
      │  :2020    │  │  :2020    │  │  :2020    │
      └─────┬─────┘  └─────┬─────┘  └─────┬─────┘
            │              │              │
            └──────────────┼──────────────┘
                           │
                    ┌──────▼──────┐
                    │  gRPC 服务  │
                    └────────────┘

部署方式

  1. Kubernetes 部署(推荐):

    1. 使用 Deployment 部署多个 Envoy 实例(至少 3 个)
    2. 使用 Service 暴露服务,支持 LoadBalancer 或 Ingress
    3. 通过 ConfigMap 管理配置文件
    4. 支持自动扩缩容和滚动更新
  2. Docker Compose 多实例

    1. 启动多个 Envoy 容器实例
    2. 每个实例使用不同的端口映射
    3. 共享相同的配置文件和 proto 文件
  3. 直接部署

    1. 在多台服务器上直接运行 Envoy 进程
    2. 使用 systemd 或 supervisor 管理进程
    3. 通过负载均衡器(Nginx、HAProxy)分发流量

7.1.2 负载均衡配置

前端负载均衡器选择

  1. Nginx

    1. 配置 upstream 指向多个 Envoy 实例
    2. 支持健康检查和故障转移
    3. 使用 least_conn 或 ip_hash 负载均衡策略
  2. 云服务负载均衡器

    1. AWS ALB / Application Load Balancer
    2. 阿里云 SLB / Server Load Balancer
    3. 腾讯云 CLB / Cloud Load Balancer
    4. 配置多个 Envoy 实例作为后端
    5. 启用健康检查(检查 /healthz/server_info
    6. 支持会话保持(如需要)
  3. HAProxy

    1. 配置多个 Envoy 后端服务器
    2. 支持健康检查和负载均衡算法

7.2 上游服务高可用

7.2.1 多实例集群配置

在 envoy.yaml 中配置多个上游服务实例

yaml 复制代码
clusters:
  - name: payment_gateway_grpc
    connect_timeout: 5s
    type: STRICT_DNS
    lb_policy: ROUND_ROBIN  # 轮询负载均衡
    http2_protocol_options: {}
    
    # 健康检查配置
    health_checks:
      - timeout: 1s
        interval: 10s
        unhealthy_threshold: 3
        healthy_threshold: 2
        grpc_health_check:
          service_name: ""  # 空字符串表示使用默认健康检查服务
        no_traffic_interval: 60s
    
    # 熔断器配置
    circuit_breakers:
      thresholds:
        - priority: DEFAULT
          max_connections: 1000
          max_pending_requests: 1000
          max_requests: 1000
          max_retries: 3
    
    # 超时配置
    timeout: 5s
    
    load_assignment:
      cluster_name: payment_gateway_grpc
      endpoints:
        - lb_endpoints:
            # 实例 1
            - endpoint:
                address:
                  socket_address:
                    address: 172.29.119.29
                    port_value: 8090
              health_status: HEALTHY
            # 实例 2
            - endpoint:
                address:
                  socket_address:
                    address: 172.29.119.30
                    port_value: 8090
              health_status: HEALTHY
            # 实例 3
            - endpoint:
                address:
                  socket_address:
                    address: 172.29.119.31
                    port_value: 8090
              health_status: HEALTHY

关键配置说明

  1. 负载均衡策略

    1. ROUND_ROBIN:轮询(默认)
    2. LEAST_REQUEST:最少请求
    3. RANDOM:随机
    4. RING_HASH:一致性哈希(需要会话保持时使用)
  2. 健康检查

    1. timeout:健康检查超时时间
    2. interval:检查间隔
    3. unhealthy_threshold:连续失败几次标记为不健康
    4. healthy_threshold:连续成功几次标记为健康
    5. grpc_health_check:使用 gRPC 健康检查协议
  3. 熔断器

    1. max_connections:最大连接数
    2. max_pending_requests:最大等待请求数
    3. max_requests:最大并发请求数
    4. max_retries:最大重试次数

7.2.2 故障转移配置

自动故障转移

yaml 复制代码
clusters:
  - name: payment_gateway_grpc
    # ... 其他配置 ...
    
    # 故障转移配置
    outlier_detection:
      consecutive_5xx: 5  # 连续 5 次 5xx 错误后标记为异常
      interval: 30s  # 检测间隔
      base_ejection_time: 30s  # 基础驱逐时间
      max_ejection_percent: 50  # 最多驱逐 50% 的实例
      enforcing_consecutive_5xx: 100  # 100% 强制执行
      success_rate_minimum_hosts: 5  # 至少需要 5 个健康实例才启用成功率检测
      success_rate_request_volume: 10  # 至少 10 个请求才计算成功率
      success_rate_stdev_factor: 1900  # 成功率标准差因子

手动故障转移(通过路由配置):

yaml 复制代码
routes:
  - match:
      path: "/api/v1/payment/channels"
    route:
      cluster: payment_gateway_grpc
      timeout: 5s
      retry_policy:
        retry_on: "5xx,reset,connect-failure,refused-stream"
        num_retries: 3
        per_try_timeout: 2s
        retry_back_off:
          base_interval: 0.25s
          max_interval: 60s

7.3 健康检查配置

7.3.1 Envoy 自身健康检查

在 envoy.yaml 中添加健康检查路由

yaml 复制代码
routes:
  - match:
      path: "/healthz"
    route:
      cluster: payment_stripe_grpc
      timeout: 1s
    # 或者直接返回健康状态
    direct_response:
      status: 200
      body:
        inline_string: "ok"

Admin 接口健康检查

yaml 复制代码
admin:
  address:
    socket_address:
      protocol: TCP
      address: 0.0.0.0
      port_value: 9901
  # 健康检查端点
  # 访问 http://localhost:9901/server_info 检查 Envoy 状态

7.3.2 上游服务健康检查

gRPC 健康检查(推荐):

yaml 复制代码
clusters:
  - name: payment_gateway_grpc
    health_checks:
      - timeout: 1s
        interval: 10s
        unhealthy_threshold: 3
        healthy_threshold: 2
        grpc_health_check:
          service_name: ""  # 使用默认健康检查服务
        no_traffic_interval: 60s
        # 如果服务实现了 gRPC 健康检查协议,Envoy 会自动调用

HTTP 健康检查(如果服务提供 HTTP 健康检查端点):

yaml 复制代码
clusters:
  - name: payment_gateway_grpc
    health_checks:
      - timeout: 1s
        interval: 10s
        unhealthy_threshold: 3
        healthy_threshold: 2
        http_health_check:
          path: "/healthz"
          expected_statuses:
            - start: 200
              end: 299

7.4 监控和告警

7.4.1 统计信息收集

Prometheus 指标导出

yaml 复制代码
admin:
  address:
    socket_address:
      protocol: TCP
      address: 0.0.0.0
      port_value: 9901
  # Prometheus 统计端点
  # 访问 http://localhost:9901/stats/prometheus 获取指标

关键指标

  • cluster.payment_gateway_grpc.upstream_rq_total:总请求数
  • cluster.payment_gateway_grpc.upstream_rq_2xx:2xx 响应数
  • cluster.payment_gateway_grpc.upstream_rq_5xx:5xx 错误数
  • cluster.payment_gateway_grpc.upstream_cx_active:活跃连接数
  • cluster.payment_gateway_grpc.health_check.healthy:健康实例数
  • cluster.payment_gateway_grpc.health_check.unhealthy:不健康实例数

7.4.2 告警规则

Prometheus 告警规则示例

yaml 复制代码
groups:
  - name: envoy_alerts
    rules:
      # Envoy 实例宕机
      - alert: EnvoyDown
        expr: up{job="envoy"} == 0
        for: 1m
        annotations:
          summary: "Envoy 实例 {{ $labels.instance }} 宕机"
      
      # 上游服务错误率过高
      - alert: HighErrorRate
        expr: |
          rate(cluster_payment_gateway_grpc_upstream_rq_5xx[5m]) / 
          rate(cluster_payment_gateway_grpc_upstream_rq_total[5m]) > 0.1
        for: 5m
        annotations:
          summary: "上游服务错误率超过 10%"
      
      # 健康实例数不足
      - alert: InsufficientHealthyInstances
        expr: cluster_payment_gateway_grpc_health_check_healthy < 2
        for: 2m
        annotations:
          summary: "健康实例数不足,当前: {{ $value }}"
      
      # 响应时间过长
      - alert: HighLatency
        expr: |
          histogram_quantile(0.99,
            rate(cluster_payment_gateway_grpc_upstream_rq_time_bucket[5m])
          ) > 1
        for: 5m
        annotations:
          summary: "99 分位响应时间超过 1 秒"

7.5 配置管理高可用

7.5.1 配置版本控制

使用 Git 管理配置

  • 所有 envoy.yamlproto 文件纳入版本控制
  • 配置变更通过 Pull Request 审核
  • 使用标签管理不同环境的配置

7.5.2 配置热更新

Envoy 支持配置热更新(无需重启):

bash 复制代码
# 发送 SIGHUP 信号触发配置重载
kill -HUP <envoy_pid>

# 或使用 Admin 接口
curl -X POST http://localhost:9901/quitquitquit

配置验证

r 复制代码
# 验证配置文件语法
envoy -c envoy.yaml --mode validate

7.6 高可用最佳实践总结

  1. Envoy 实例

    1. 至少部署 3 个实例
    2. 使用负载均衡器分发流量
    3. 配置自动重启和健康检查
  2. 上游服务

    1. 每个服务至少 2 个实例
    2. 配置健康检查和故障转移
    3. 使用熔断器防止级联故障
  3. 监控告警

    1. 监控 Envoy 和上游服务状态
    2. 设置错误率和延迟告警
    3. 定期检查健康实例数
  4. 配置管理

    1. 使用版本控制管理配置
    2. 配置变更前进行验证
    3. 支持配置热更新
  5. 故障处理

    1. 自动故障转移
    2. 异常实例自动隔离
    3. 快速恢复机制

八、总结

8.1 关键结论

  1. Envoy 方案适合多服务场景

    1. 统一管理,降低维护成本
    2. 快速扩展,只需修改配置
    3. 性能优异,标准化程度高
  2. 架构设计原则

    1. Envoy 仅做透传和协议转换
    2. 所有业务逻辑在 RPC 服务中处理
    3. 仅统一有 HTTP 注解的 RPC 方法的响应格式
  3. 方案选择

    1. 服务数量多、需要快速扩展 → 使用 Envoy
    2. 服务数量少、独立性要求高 → 各自搭建 API 项目

8.2 建议

基于您的需求背景(多个 webhook 服务 + RPC 转 API 接口 + 服务数量可能增长),建议使用 Envoy 方案,可以:

  • 统一管理所有服务
  • 快速添加新服务
  • 降低维护成本
  • 提高性能和标准化程度

九、不使用 Lua 的方案:RPC 响应直接设置为目标格式

9.1 方案概述

核心思想:移除 Envoy 中的 Lua 过滤器,让 gRPC 服务直接返回符合 HTTP API 要求的响应格式,Envoy 仅负责协议转换(gRPC ↔ HTTP/JSON),不做任何业务格式转换。

架构对比

arduino 复制代码
使用 Lua 的方案:
HTTP Client → Envoy → gRPC 服务 → gRPC 响应 → Lua 转换 → HTTP 响应

不使用 Lua 的方案:
HTTP Client → Envoy → gRPC 服务 → gRPC 响应(已符合目标格式)→ HTTP 响应

优势

  1. 简化架构:减少 Envoy 层的处理逻辑,降低复杂度
  2. 提高性能:减少一次数据转换,提升响应速度
  3. 易于维护:响应格式逻辑集中在 RPC 服务中,便于测试和调试
  4. 类型安全:响应格式在 proto 文件中定义,编译时检查
  5. 职责清晰:Envoy 只做协议转换,RPC 服务处理业务逻辑和格式

9.2 实施步骤

9.2.1 步骤 1:识别需要统一响应格式的 RPC 方法

判断标准

  • ✅ 有 option (google.api.http) 注解的 RPC 方法 → 需要通过 Envoy 暴露为 HTTP API,需要统一格式
  • ❌ 无 HTTP 注解的 RPC 方法 → 仅用于内部 gRPC 调用,可以保持原有格式

示例

scss 复制代码
service PaymentGateway {
  // ✅ 有 HTTP 注解,需要统一响应格式
  rpc ListChannels(ListChannelsReq) returns (ListChannelsResp) {
    option (google.api.http) = {
      get: "/api/v1/payment/channels"
    };
  };
  
  // ❌ 无 HTTP 注解,仅内部调用,可以保持原有格式
  rpc CreatePayment(CreatePaymentReq) returns (CreatePaymentResp);
}

关键点

  1. 错误处理:所有错误都通过响应消息返回,而不是 gRPC 错误

    1. 成功:Code: 200, Msg: "success"
    2. 参数错误:Code: 400, Msg: "参数错误"
    3. 服务器错误:Code: 500, Msg: err.Error()
  2. 统一格式 :所有响应都使用 {code, msg, data} 格式

  3. 字段映射 :直接使用 data 字段,而不是其他字段名(如 channels

9.2.2 步骤 2:移除 Envoy 中的 Lua 过滤器

修改 envoy.yaml

修改前(使用 Lua)

lua 复制代码
http_filters:
  - name: envoy.filters.http.lua
    typed_config:
      "@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua
      inline_code: |
        function envoy_on_response(response_handle)
          -- Lua 转换逻辑....
        end
  - name: envoy.filters.http.grpc_json_transcoder
    typed_config:
      "@type": type.googleapis.com/envoy.extensions.filters.http.grpc_json_transcoder.v3.GrpcJsonTranscoder
      proto_descriptor: "/etc/envoy/proto/payment_combined.pb"
      services:
        - "hh_payment_gateway.v1.PaymentGateway"
  - name: envoy.filters.http.router

修改后(移除 Lua)

yaml 复制代码
http_filters:
  - name: envoy.filters.http.grpc_json_transcoder
    typed_config:
      "@type": type.googleapis.com/envoy.extensions.filters.http.grpc_json_transcoder.v3.GrpcJsonTranscoder
      proto_descriptor: "/etc/envoy/proto/payment_combined.pb"
      services:
        - "hh_payment_gateway.v1.PaymentGateway"
  - name: envoy.filters.http.router

关键点

  • 移除 envoy.filters.http.lua 过滤器配置
  • 保留 grpc_json_transcoderrouter 过滤器
  • 过滤器顺序:grpc_json_transcoderrouter

9.2.3 步骤 3:重新编译 Proto 文件并重启服务

重新编译 Proto

bash 复制代码
cd /apps/www/go/src/hh_bmp/envoy
./build-proto.sh

重启服务

  1. 重启 gRPC 服务(使用新的 proto 定义)
  2. 重启 Envoy(加载新的配置,移除 Lua 过滤器)

验证

bash 复制代码
# 测试接口
curl http://localhost:2020/api/v1/payment/channels

# 期望响应格式:
{
  "code": 200,
  "msg": "success",
  "data": [
    {
      "id": "channel_1",
      "name": "Stripe",
      "type": "stripe"
    }
  ]
}

9.3 完整示例

9.3.1 Proto 定义示例

ini 复制代码
syntax = "proto3";
package hh_payment_gateway.v1;
option go_package = "./v1";

import "google/api/annotations.proto";

service PaymentGateway {
  // 有 HTTP 注解,需要统一响应格式
  rpc ListChannels(ListChannelsReq) returns (ListChannelsResp) {
    option (google.api.http) = {
      get: "/api/v1/payment/channels"
    };
  };
}

// 请求消息
message ListChannelsReq {
  // 可以添加查询参数
}

// 响应消息(统一格式)
message ListChannelsResp {
  int32 code = 1;
  string msg = 2;
  repeated Channel data = 3;  // 直接使用 data 字段
}

9.3.3 Envoy 配置示例(不使用 Lua)

yaml 复制代码
listeners:
  - name: listener_0
    address:
      socket_address:
        address: 0.0.0.0
        port_value: 2020
    filter_chains:
      - filters:
          - name: envoy.filters.network.http_connection_manager
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
              stat_prefix: ingress_http
              route_config:
                name: local_route
                virtual_hosts:
                  - name: local_service
                    domains: ["*"]
                    routes:
                      - match:
                          path: "/api/v1/payment/channels"
                        route:
                          cluster: payment_gateway_grpc
                          timeout: 5s
              http_filters:
                # 移除 Lua 过滤器,只保留 gRPC-JSON Transcoding
                - name: envoy.filters.http.grpc_json_transcoder
                  typed_config:
                    "@type": type.googleapis.com/envoy.extensions.filters.http.grpc_json_transcoder.v3.GrpcJsonTranscoder
                    proto_descriptor: "/etc/envoy/proto/payment_combined.pb"
                    services:
                      - "hh_payment_gateway.v1.PaymentGateway"
                - name: envoy.filters.http.router

clusters:
  - name: payment_gateway_grpc
    connect_timeout: 5s
    type: STRICT_DNS
    lb_policy: ROUND_ROBIN
    http2_protocol_options: {}
    load_assignment:
      cluster_name: payment_gateway_grpc
      endpoints:
        - lb_endpoints:
            - endpoint:
                address:
                  socket_address:
                    address: 172.29.119.29
                    port_value: 8090

9.4 方案对比

9.4.1 使用 Lua vs 不使用 Lua

维度 使用 Lua 不使用 Lua
架构复杂度 高(需要维护 Lua 脚本) 低(配置简单)
性能 较低(多一次转换) 较高(减少转换)
维护成本 高(需要维护 Lua 脚本和 RPC 服务) 低(只需维护 RPC 服务)
类型安全 低(Lua 脚本运行时检查) 高(proto 编译时检查)
调试难度 高(问题可能在 Lua 层或 RPC 层) 低(问题集中在 RPC 层)
职责划分 模糊(Envoy 处理业务格式) 清晰(Envoy 只做协议转换)
测试 复杂(需要测试 Lua 脚本) 简单(只需测试 RPC 服务)

9.4.2 响应格式处理位置

使用 Lua 方案

复制代码
RPC 服务返回原始格式 → Envoy Lua 转换 → HTTP 响应

不使用 Lua 方案

复制代码
RPC 服务直接返回目标格式 → Envoy 协议转换 → HTTP 响应

9.5 注意事项

  1. 向后兼容

    1. 如果已有客户端在使用,需要考虑向后兼容
    2. 可以在一段时间内同时支持新旧两种格式
    3. 通过版本号或路径区分(如 /api/v1//api/v2/
  2. 错误处理

    1. 确保所有错误都通过响应消息返回,而不是 gRPC 错误
    2. 统一错误码和错误消息格式
    3. 提供详细的错误信息(便于调试)
  3. 测试

    1. 充分测试所有接口的响应格式
    2. 测试各种错误场景
    3. 验证性能是否有提升
  4. 监控

    1. 监控接口响应时间
    2. 监控错误率
    3. 对比迁移前后的性能指标

9.6 最佳实践总结

  1. 统一响应格式

    1. 所有有 HTTP 注解的 RPC 方法使用 {code, msg, data} 格式
    2. 内部 gRPC 调用方法可以保持原有格式
  2. 错误处理

    1. 所有错误通过响应消息返回,不使用 gRPC 错误
  3. 职责划分

    1. Envoy:仅做协议转换(HTTP ↔ gRPC)
    2. RPC 服务:处理业务逻辑和响应格式
  4. 类型安全

    1. 响应格式在 proto 文件中定义
    2. 编译时检查,避免运行时错误

envoy.yaml

yaml 复制代码
admin:
  address:
    socket_address:
      protocol: TCP
      address: 0.0.0.0
      port_value: 9901

static_resources:
  listeners:
    - name: listener_0
      address:
        socket_address:
          protocol: TCP
          address: 0.0.0.0
          port_value: 2020
      filter_chains:
        - filters:
            - name: envoy.filters.network.http_connection_manager
              typed_config:
                "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
                stat_prefix: grpc_json
                codec_type: AUTO
                access_log:
                  - name: envoy.access_loggers.stdout
                    typed_config:
                      "@type": type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog
                      log_format:
                        text_format: |
                          [%START_TIME%] "%REQ(:METHOD)% %REQ(X-ENVOY-ORIGINAL-PATH?:PATH)% %PROTOCOL%"
                          %RESPONSE_CODE% %RESPONSE_FLAGS% %BYTES_RECEIVED% %BYTES_SENT%
                          %DURATION% %RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)% "%REQ(X-FORWARDED-FOR)%"
                          "%REQ(USER-AGENT)%" "%REQ(X-REQUEST-ID)%" "%REQ(:AUTHORITY)%" "%UPSTREAM_HOST%"
                http_filters:
                  # Lua 过滤器:转换 ListChannels 响应格式(必须在 grpc_json_transcoder 之后)
                  # 将 {baseModel: {baseCode, baseMsg}, channels: [...]} 转换为 {code, msg, data: [...]}
                  - name: envoy.filters.http.lua
                    typed_config:
                      "@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua
                      inline_code: |
                        function envoy_on_response(response_handle)
                          local body = response_handle:body()
                          if body and body:length() > 0 then
                            local body_str = body:getBytes(0, body:length())
                            
                            -- 提取 baseCode 和 baseMsg(所有响应都有)
                            local base_code = string.match(body_str, '"baseCode"%s*:%s*(%d+)') or "0"
                            local base_msg = string.match(body_str, '"baseMsg"%s*:%s*"([^"]*)"') or ""
                            
                            -- 检查是否是 ListChannels 响应(包含 channels 字段)
                            if string.find(body_str, '"channels"') and string.find(body_str, '"baseModel"') then
                              -- ListChannels 响应:转换为 {code, msg, data: [...]}
                              local channels_start = string.find(body_str, '"channels"%s*:%s*%[')
                              if channels_start then
                                local start_pos = channels_start + string.len('"channels": [')
                                local bracket_count = 1
                                local in_string = false
                                local escape = false
                                local end_pos = start_pos
                                
                                for i = start_pos, #body_str do
                                  local char = string.sub(body_str, i, i)
                                  if escape then
                                    escape = false
                                  elseif char == '\\' then
                                    escape = true
                                  elseif char == '"' and not escape then
                                    in_string = not in_string
                                  elseif not in_string then
                                    if char == '[' then
                                      bracket_count = bracket_count + 1
                                    elseif char == ']' then
                                      bracket_count = bracket_count - 1
                                      if bracket_count == 0 then
                                        end_pos = i
                                        break
                                      end
                                    end
                                  end
                                end
                                
                                if end_pos >= start_pos then
                                  local channels_json = string.sub(body_str, start_pos - 1, end_pos)
                                  local new_body = string.format('{"code": %s, "msg": "%s", "data": %s}', base_code, base_msg, channels_json)
                                  response_handle:body():setBytes(new_body)
                                  response_handle:headers():replace("content-length", string.len(new_body))
                                end
                              end
                            elseif string.find(body_str, '"baseModel"') and not string.find(body_str, '"channels"') then
                              -- Webhook 响应(只有 baseModel,没有 channels):转换为 {code, msg}(按照 API 项目格式)
                              local new_body = string.format('{"code": %s, "msg": "%s"}', base_code, base_msg)
                              response_handle:body():setBytes(new_body)
                              response_handle:headers():replace("content-length", string.len(new_body))
                            elseif string.find(body_str, '"message"') and string.find(body_str, '"code"') and not string.find(body_str, '"baseModel"') then
                              -- gRPC 错误响应:转换为 {code, msg}(按照 API 项目格式)
                              local grpc_code = string.match(body_str, '"code"%s*:%s*(%d+)') or "0"
                              local grpc_message = string.match(body_str, '"message"%s*:%s*"([^"]*)"') or ""
                              local new_body = string.format('{"code": %s, "msg": "%s"}', grpc_code, grpc_message)
                              response_handle:body():setBytes(new_body)
                              response_handle:headers():replace("content-length", string.len(new_body))
                            end
                          end
                        end
                  
                  # gRPC-JSON Transcoding 过滤器
                  # 支持多个 proto 文件,使用合并的 descriptor set
                  - name: envoy.filters.http.grpc_json_transcoder
                    typed_config:
                      "@type": type.googleapis.com/envoy.extensions.filters.http.grpc_json_transcoder.v3.GrpcJsonTranscoder
                      proto_descriptor: "/etc/envoy/proto/payment_combined.pb"
                      services:
                        - "hh_payment_stripe.v1.PaymentStripe"
                        - "hh_payment_gateway.v1.PaymentGateway"
                      print_options:
                        add_whitespace: true
                        always_print_primitive_fields: true
                        always_print_enums_as_ints: false
                        preserve_proto_field_names: false
                      # 请求路径映射 - 将 HTTP 路径映射到 gRPC 方法
                      url_unescape_spec: ALL_CHARACTERS_EXCEPT_RESERVED
                      request_validation_options:
                        reject_unknown_method: true
                        reject_unknown_query_parameters: false  # 允许 query 参数(用于传递 signature)
                      # 响应转换配置
                      convert_grpc_status: true
                  
                  # Router 过滤器
                  - name: envoy.filters.http.router
                    typed_config:
                      "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
                
                route_config:
                  name: local_route
                  virtual_hosts:
                    - name: local_service
                      domains: ["*"]
                      routes:
                        # Payment Gateway - ListChannels 路由(放在前面,优先匹配)
                        # proto 注解:get: "/api/v1/payment/channels"
                        # 注意:grpc_json_transcoder 可能会转换 HTTP 方法,所以不限制方法
                        - match:
                            path: "/api/v1/payment/channels"
                          route:
                            cluster: payment_gateway_grpc
                            timeout: 5s
                        # Stripe Webhook 路由 - 使用 proto 文件中定义的路径
                        # proto 注解:post: "/api/v2/payment/webhook/stripe/{account_id}"
                        # grpc_json_transcoder 会自动处理路径参数和请求体
                        - match:
                            path: "/api/v2/payment/webhook/stripe/{account_id}"
                            headers:
                              - name: ":method"
                                exact_match: "POST"
                          route:
                            cluster: payment_stripe_grpc
                            timeout: 5s
                        # 健康检查路由
                        - match:
                            path: "/healthz"
                          route:
                            cluster: payment_stripe_grpc
                            timeout: 5s
                        # 默认路由(放在最后,避免匹配其他路径)
                        - match:
                            prefix: "/"
                          route:
                            cluster: payment_gateway_grpc
                            timeout: 5s

  clusters:
    - name: payment_stripe_grpc
      connect_timeout: 5s
      type: STRICT_DNS
      lb_policy: ROUND_ROBIN
      http2_protocol_options: {}
      load_assignment:
        cluster_name: payment_stripe_grpc
        endpoints:
          - lb_endpoints:
              # 根据实际环境修改服务地址
              # Kubernetes: hh-payment-stripe-svc.ark-project-test.svc.cluster.local:8080
              # 本地开发: 127.0.0.1:8080
              - endpoint:
                  address:
                    socket_address:
                      address: 172.29.119.29  # 宿主机 IP 地址(Docker 容器访问宿主机服务)
                      port_value: 8080  # gRPC 服务端口(业务服务保持 8080)
    
    - name: payment_gateway_grpc
      connect_timeout: 5s
      type: STRICT_DNS
      lb_policy: ROUND_ROBIN
      http2_protocol_options: {}
      load_assignment:
        cluster_name: payment_gateway_grpc
        endpoints:
          - lb_endpoints:
              # Payment Gateway gRPC 服务地址
              # Kubernetes: hh-payment-gateway-svc.ark-project-test.svc.cluster.local:8090
              # 本地开发: 172.29.119.29:8090
              - endpoint:
                  address:
                    socket_address:
                      address: 172.29.119.29  # 宿主机 IP 地址
                      port_value: 8090  # Payment Gateway gRPC 服务端口

build-proto.sh

bash 复制代码
#!/bin/bash

# 编译 proto 文件为 descriptor set,供 Envoy 使用
# 使用方法: ./build-proto.sh

set -e

# 颜色输出
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color

echo -e "${GREEN}开始编译 proto 文件...${NC}"

# 检查 protoc 是否安装
if ! command -v protoc &> /dev/null; then
    echo -e "${RED}错误: protoc 未安装,请先安装 Protocol Buffers 编译器${NC}"
    echo "安装方法:"
    echo "  macOS: brew install protobuf"
    echo "  Ubuntu: sudo apt-get install protobuf-compiler"
    echo "  或从 https://github.com/protocolbuffers/protobuf/releases 下载"
    exit 1
fi

# 设置路径
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
IDL_DIR="${SCRIPT_DIR}/../hh-idl"
OUTPUT_DIR="${SCRIPT_DIR}/proto"
OUTPUT_FILE="${OUTPUT_DIR}/payment_stripe.pb"

# 创建输出目录
mkdir -p "${OUTPUT_DIR}"

# 检查 IDL 目录是否存在
if [ ! -d "${IDL_DIR}" ]; then
    echo -e "${RED}错误: IDL 目录不存在: ${IDL_DIR}${NC}"
    exit 1
fi

# 编译 proto 文件(包含 stripe 和 gateway)
echo -e "${YELLOW}正在编译 payment_stripe.proto 和 payment_gateway.proto 及其依赖...${NC}"

# 编译合并的 descriptor set(包含两个服务)
protoc \
  --proto_path="${IDL_DIR}" \
  --include_imports \
  --include_source_info \
  --descriptor_set_out="${OUTPUT_DIR}/payment_combined.pb" \
  "${IDL_DIR}/payment_stripe/v1/hh_payment_stripe.proto" \
  "${IDL_DIR}/payment_gateway/v1/hh_payment_gateway.proto"

# 同时保留单独的文件(用于调试)
protoc \
  --proto_path="${IDL_DIR}" \
  --include_imports \
  --include_source_info \
  --descriptor_set_out="${OUTPUT_DIR}/payment_stripe.pb" \
  "${IDL_DIR}/payment_stripe/v1/hh_payment_stripe.proto"

protoc \
  --proto_path="${IDL_DIR}" \
  --include_imports \
  --include_source_info \
  --descriptor_set_out="${OUTPUT_DIR}/payment_gateway.pb" \
  "${IDL_DIR}/payment_gateway/v1/hh_payment_gateway.proto"

OUTPUT_FILE="${OUTPUT_DIR}/payment_combined.pb"

if [ $? -eq 0 ]; then
    echo -e "${GREEN}✓ 合并 proto 编译成功!${NC}"
    echo -e "${GREEN}输出文件: ${OUTPUT_FILE}${NC}"
    
    # 显示文件大小
    FILE_SIZE=$(du -h "${OUTPUT_FILE}" | cut -f1)
    echo -e "${GREEN}文件大小: ${FILE_SIZE}${NC}"
    
    # 验证文件
    echo -e "${YELLOW}验证 descriptor set...${NC}"
    protoc --decode_raw < "${OUTPUT_FILE}" > /dev/null 2>&1
    if [ $? -eq 0 ]; then
        echo -e "${GREEN}✓ 合并 Descriptor set 验证通过${NC}"
    else
        echo -e "${YELLOW}⚠ 警告: Descriptor set 验证失败,但文件已生成${NC}"
    fi
else
    echo -e "${RED}✗ 合并 proto 编译失败!${NC}"
    exit 1
fi

echo -e "${GREEN}完成!${NC}"

docker-composer.yaml

version: 复制代码
services:
  envoy:
    image: envoyproxy/envoy:dev-1caf0d7396786bac7f4fcf7b9d291ed761191b68
    container_name: envoy-grpc-proxy
    ports:
      - "2020:2020"   # HTTP 监听端口
      - "9901:9901"   # Admin 管理端口
    volumes:
      - ./envoy.yaml:/etc/envoy/envoy.yaml:ro
      - ./proto:/etc/envoy/proto:ro
    command: /usr/local/bin/envoy -c /etc/envoy/envoy.yaml --service-cluster envoy-grpc-proxy --service-node envoy-node
    networks:
      - envoy-network
    restart: unless-stopped
    environment:
      - ENVOY_UID=0
      - ENVOY_GID=0
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:9901/server_info"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

networks:
  envoy-network:
    driver: bridge
相关推荐
京东零售技术44 分钟前
NeurIPS 2025 | TANDEM:基于双层优化的数据配比学习方法
后端·算法
Moment1 小时前
半年时间使用 Tiptap 开发一个和飞书差不多效果的协同文档 😍😍😍
前端·javascript·后端
百度Geek说1 小时前
基于AI的质量风险管控
后端
库库林_沙琪马1 小时前
1、Hi~ SpringBoot
java·spring boot·后端
哈哈哈笑什么1 小时前
分布式高并发Springcloud系统下的数据图同步断点续传方案【订单/商品/用户等】
分布式·后端·spring cloud
阿宁又菜又爱玩1 小时前
Web后端开发入门
java·spring boot·后端·web
桃花键仙1 小时前
vLLM-ascend快速上手:从零到一部署Llama2推理服务
后端
桃花键仙1 小时前
PyTorch模型迁移昇腾平台全流程:ResNet50实战指南
后端
1024肥宅1 小时前
告别异地登录告警!用 GitHub Self-Hosted Runner 打造“零打扰”全栈自动化部署
前端·后端·github