一、架构概述
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 核心功能
- gRPC 到 HTTP 的协议转换:通过 gRPC-JSON Transcoding 将 HTTP/JSON 请求转换为 gRPC 调用
- 响应格式统一:通过 Lua 过滤器(可选)将 gRPC 响应格式转换为统一的 API 响应格式
- 多服务路由:支持多个 gRPC 服务的统一入口和路由分发
- 协议桥接:实现 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 的工作:
-
读取 Proto Descriptor:
- 加载
payment_combined.pb文件 - 查找匹配的 HTTP 注解:
post: "/api/v2/payment/webhook/stripe/{account_id}"
- 加载
-
提取 Path 参数:
bashHTTP Path: /api/v2/payment/webhook/stripe/acct_123456 Proto 模式: /api/v2/payment/webhook/stripe/{account_id} ↓ 提取: account_id = "acct_123456" -
提取请求体(Body) :
cssHTTP Body: {"id": "evt_123", "type": "payment.succeeded", ...} ↓ 根据 body: "*" 规则,将整个 JSON 映射到 gRPC 消息 如果 proto 定义中有 payload 字段,会尝试映射 -
处理 Header:
- 重要 :gRPC-JSON Transcoding 默认不会将 HTTP Header 映射到 gRPC 消息字段
- 但 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 的提取过程
详细步骤:
-
解析 HTTP Path:
bash外部请求: POST /api/v2/payment/webhook/stripe/acct_123456 -
匹配 Proto 注解:
arduinoProto 注解: post: "/api/v2/payment/webhook/stripe/{account_id}" 匹配成功! -
提取路径参数:
bash路径模式: /api/v2/payment/webhook/stripe/{account_id} 实际路径: /api/v2/payment/webhook/stripe/acct_123456 ↓ 提取参数: account_id = "acct_123456" -
映射到 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 关键要点总结
-
Path 参数提取:
- ✅ 通过 proto 注解中的
{account_id}自动提取 - ✅ 自动映射到 gRPC 消息字段
account_id - ✅ 字段名必须匹配
- ✅ 通过 proto 注解中的
-
Header 传递:
- ✅ Envoy 自动将 HTTP Header 转换为 gRPC Metadata
- ✅ 不需要额外配置
- ✅ 在 gRPC 服务中通过
metadata.FromIncomingContext(ctx)获取 - ❌ 不会自动映射到 gRPC 消息字段
-
请求体传递:
- ✅ 通过
body: "*"规则映射到 gRPC 消息字段 - ✅ JSON 字段名必须与 proto 字段名匹配
- ✅ 通过
-
数据来源总结:
- Path 参数 → gRPC 消息字段(自动映射)
- HTTP Header → gRPC Metadata(自动转换)
- 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
需要修改的地方:
- 确保导入 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"; // 必须导入这个
- 在服务方法上添加 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} - 支持
get、post、put、delete、patch等 HTTP 方法
步骤 2:编译生成 payment_combined.pb 文件
执行编译脚本:
bash
cd /apps/www/go/src/hh_bmp/envoy
./build-proto.sh
编译过程:
- 脚本会检查
protoc是否安装 - 读取
hh-idl/payment_gateway/v1/hh_payment_gateway.proto文件 - 使用
--include_imports包含所有依赖(如common/payment.proto、common/base.proto) - 使用
--include_source_info包含源信息(用于错误提示) - 生成
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 会:
- 加载新的
payment_combined.pb文件 - 读取
ListChannels的 HTTP 注解 - 建立 HTTP 路径
/api/v1/payment/channels到 gRPC 方法ListChannels的映射
测试接口:
bash
curl http://localhost:2020/api/v1/payment/channels
请求流程:
- HTTP GET 请求 →
/api/v1/payment/channels - Envoy Router Filter → 匹配路由 →
payment_gateway_grpc集群 - gRPC-JSON Transcoding → 根据 proto 注解转换为 gRPC 调用
- gRPC 调用 →
hh_payment_gateway.v1.PaymentGateway/ListChannels - gRPC 响应 → gRPC-JSON Transcoding 转换为 JSON
- Lua Filter(如果使用)→ 响应格式转换
- 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 中的 address 和 port_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 数据转换(业务格式)
不应该做:
- ❌ 响应格式转换(如
baseModel→code/msg) - ❌ 数据字段映射
- ❌ 业务数据格式化
原因:
- 如果使用 Lua 过滤器做响应格式转换,说明 gRPC 服务返回的格式不符合要求
- 最佳实践:gRPC 服务直接返回符合 API 要求的格式,无需转换
替代方案:
- gRPC 服务统一返回格式:
{code, msg, data} - 移除 Lua 过滤器,Envoy 只做协议转换
4.4 RPC 服务应该处理什么
4.4.1 所有业务逻辑
必须处理:
-
签名验证:
gofunc (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 } // 处理业务逻辑... } -
参数验证:
gofunc (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 } // 处理业务逻辑... } -
权限检查:
gofunc (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 } // 处理业务逻辑... } -
统一响应格式:
gofunc (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 架构设计原则
-
Envoy 做透传和转换:
- ✅ 协议转换(HTTP ↔ gRPC)
- ✅ 路由和负载均衡
- ✅ 基础设施功能(健康检查、熔断等)
- ❌ 不做业务逻辑处理
-
RPC 服务处理所有业务逻辑:
- ✅ 签名验证
- ✅ 参数验证
- ✅ 权限检查
- ✅ 业务规则判断
- ✅ 统一响应格式(仅针对有 HTTP 注解的方法)
-
响应格式统一在 RPC 服务层:
- ✅ 仅处理需要通过 Envoy 转换为 HTTP API 的 RPC 方法
- ✅ 这些方法的响应格式统一为
{code, msg, data} - ✅ 内部 gRPC 调用(无 HTTP 注解)的方法可以保持原有格式
- ✅ 无需 Lua 过滤器转换
- ✅ Envoy 只做协议转换,不做格式转换
4.6.3 实施建议
阶段 1:当前状态(使用 Lua) :
- Envoy:协议转换 + 响应格式转换(Lua)
- RPC 服务:业务逻辑处理
阶段 2:目标状态(移除 Lua) :
- Envoy:仅协议转换
- RPC 服务:业务逻辑 + 统一响应格式
迁移步骤:
- 识别需要通过 Envoy 暴露的 RPC 方法 (有
google.api.http注解的) - 仅统一这些方法的响应格式 为
{code, msg, data} - 内部 gRPC 调用方法 (无 HTTP 注解)可以保持原有格式(如
baseModel) - 移除 Envoy 中的 Lua 过滤器
- 验证所有接口正常工作
五、使用 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 需求背景分析
需求特点:
- 多个服务的 webhook 请求:Stripe、PayPal、GitHub 等
- 部分需要 RPC 转换为 API 的接口:如 ListChannels、GetPayment 等
- 服务数量可能持续增长:未来可能有更多 webhook 和 API 接口
6.2 方案对比
方案 A:使用 Envoy 统一代理
架构:
arduino
HTTP Client
↓
Envoy (统一入口)
├─ Webhook 路由 → gRPC 服务
├─ API 路由 → gRPC 服务
└─ 其他路由 → gRPC 服务
优势:
-
统一管理
- 单一配置文件管理所有路由
- 统一监控和统计
- 统一升级和维护
-
快速扩展
- 添加新服务只需修改配置
- 无需新建项目
- 配置即代码,易于版本控制
-
协议转换
- 自动 HTTP/JSON → gRPC 转换
- 基于 proto 文件,类型安全
- 零代码转换
-
性能优势
- C++ 实现,性能优异
- HTTP/2 支持,连接复用
- 高效的路由和负载均衡
-
标准化
- 所有服务遵循相同的接口标准
- 统一的错误处理和响应格式
- 易于团队协作
劣势:
-
学习成本
- 需要学习 Envoy 配置
- 需要理解 gRPC-JSON Transcoding
-
调试复杂度
- 问题可能出现在 Envoy 层或 gRPC 服务层
- 需要理解整个数据流
-
依赖 Envoy
- 如果 Envoy 出现问题,影响所有服务
- 需要维护 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 服务
优势:
-
独立性
- 每个服务独立部署和升级
- 问题隔离,不影响其他服务
- 团队可以独立开发
-
灵活性
- 每个服务可以使用不同的技术栈
- 可以针对特定服务优化
- 不受统一框架限制
-
简单性
- 每个项目相对简单
- 不需要理解 Envoy 配置
- 调试相对容易
劣势:
-
重复代码
- 每个项目都需要实现 HTTP → gRPC 转换
- 每个项目都需要实现签名验证
- 每个项目都需要实现参数验证
- 代码重复,维护成本高
-
维护成本
- 每个项目独立维护
- 每个项目独立升级
- 配置分散,难以统一管理
-
扩展成本
- 每个新服务都需要新建项目
- 需要重复实现相同的功能
- 资源消耗更大
-
标准化困难
- 不同项目可能标准不一致
- 难以统一监控和统计
- 团队协作成本高
6.3 成本对比
| 维度 | Envoy 方案 | 各自搭建 API 项目 |
|---|---|---|
| 初始开发 | 低(只需配置) | 高(每个项目都要开发) |
| 添加新服务 | 低(修改配置) | 高(新建项目) |
| 维护成本 | 低(统一维护) | 高(分散维护) |
| 代码重复 | 无 | 高(每个项目重复) |
| 学习成本 | 中(学习 Envoy) | 低(使用熟悉框架) |
| 性能 | 高(C++ 实现) | 中(取决于框架) |
| 扩展性 | 高(易于扩展) | 低(需要新建项目) |
6.4 适用场景
使用 Envoy 的场景
- 服务数量多:有多个 webhook 和 API 接口需要管理
- 统一标准:希望所有服务遵循相同的接口标准
- 快速扩展:需要频繁添加新服务
- 性能要求高:对性能有较高要求
- 团队协作:希望统一管理和监控
各自搭建 API 项目的场景
- 服务数量少:只有 1-2 个服务
- 独立性要求高:每个服务需要完全独立
- 技术栈不同:不同服务需要使用不同的技术栈
- 团队独立:不同团队独立开发,不需要统一管理
6.5 基于需求背景的建议
基于您的需求背景:
- 可能有很多服务的 webhook 请求
- 部分需要 RPC 转换为 API 的接口
- 服务数量可能持续增长
建议:使用 Envoy 方案
理由:
-
符合需求特点:
- 多个 webhook 服务可以通过 Envoy 统一管理
- RPC 转 API 的需求可以通过 gRPC-JSON Transcoding 自动实现
- 服务数量增长时,只需修改配置,无需新建项目
-
成本效益高:
- 初始开发成本低(只需配置)
- 维护成本低(统一维护)
- 扩展成本低(修改配置即可)
-
标准化优势:
- 所有服务遵循相同的接口标准
- 统一的错误处理和响应格式
- 易于团队协作
-
性能优势:
- Envoy 是 C++ 实现,性能优异
- HTTP/2 支持,连接复用
- 高效的路由和负载均衡
实施建议:
- 初期:使用 Envoy + Lua 过滤器(如果已有服务,不想改动)
- 长期:逐步统一 gRPC 服务响应格式,移除 Lua 过滤器
- 扩展:新服务直接使用统一的响应格式,无需 Lua 转换
七、Envoy 高可用方案
7.1 Envoy 实例高可用
7.1.1 多实例部署
架构设计:
markdown
┌─────────────┐
│ 负载均衡器 │
│ (Nginx/ALB) │
└──────┬──────┘
│
┌──────────────┼──────────────┐
│ │ │
┌─────▼─────┐ ┌─────▼─────┐ ┌─────▼─────┐
│ Envoy-1 │ │ Envoy-2 │ │ Envoy-3 │
│ :2020 │ │ :2020 │ │ :2020 │
└─────┬─────┘ └─────┬─────┘ └─────┬─────┘
│ │ │
└──────────────┼──────────────┘
│
┌──────▼──────┐
│ gRPC 服务 │
└────────────┘
部署方式:
-
Kubernetes 部署(推荐):
- 使用 Deployment 部署多个 Envoy 实例(至少 3 个)
- 使用 Service 暴露服务,支持 LoadBalancer 或 Ingress
- 通过 ConfigMap 管理配置文件
- 支持自动扩缩容和滚动更新
-
Docker Compose 多实例:
- 启动多个 Envoy 容器实例
- 每个实例使用不同的端口映射
- 共享相同的配置文件和 proto 文件
-
直接部署:
- 在多台服务器上直接运行 Envoy 进程
- 使用 systemd 或 supervisor 管理进程
- 通过负载均衡器(Nginx、HAProxy)分发流量
7.1.2 负载均衡配置
前端负载均衡器选择:
-
Nginx:
- 配置 upstream 指向多个 Envoy 实例
- 支持健康检查和故障转移
- 使用 least_conn 或 ip_hash 负载均衡策略
-
云服务负载均衡器:
- AWS ALB / Application Load Balancer
- 阿里云 SLB / Server Load Balancer
- 腾讯云 CLB / Cloud Load Balancer
- 配置多个 Envoy 实例作为后端
- 启用健康检查(检查
/healthz或/server_info) - 支持会话保持(如需要)
-
HAProxy:
- 配置多个 Envoy 后端服务器
- 支持健康检查和负载均衡算法
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
关键配置说明:
-
负载均衡策略:
ROUND_ROBIN:轮询(默认)LEAST_REQUEST:最少请求RANDOM:随机RING_HASH:一致性哈希(需要会话保持时使用)
-
健康检查:
timeout:健康检查超时时间interval:检查间隔unhealthy_threshold:连续失败几次标记为不健康healthy_threshold:连续成功几次标记为健康grpc_health_check:使用 gRPC 健康检查协议
-
熔断器:
max_connections:最大连接数max_pending_requests:最大等待请求数max_requests:最大并发请求数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.yaml和proto文件纳入版本控制 - 配置变更通过 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 高可用最佳实践总结
-
Envoy 实例:
- 至少部署 3 个实例
- 使用负载均衡器分发流量
- 配置自动重启和健康检查
-
上游服务:
- 每个服务至少 2 个实例
- 配置健康检查和故障转移
- 使用熔断器防止级联故障
-
监控告警:
- 监控 Envoy 和上游服务状态
- 设置错误率和延迟告警
- 定期检查健康实例数
-
配置管理:
- 使用版本控制管理配置
- 配置变更前进行验证
- 支持配置热更新
-
故障处理:
- 自动故障转移
- 异常实例自动隔离
- 快速恢复机制
八、总结
8.1 关键结论
-
Envoy 方案适合多服务场景:
- 统一管理,降低维护成本
- 快速扩展,只需修改配置
- 性能优异,标准化程度高
-
架构设计原则:
- Envoy 仅做透传和协议转换
- 所有业务逻辑在 RPC 服务中处理
- 仅统一有 HTTP 注解的 RPC 方法的响应格式
-
方案选择:
- 服务数量多、需要快速扩展 → 使用 Envoy
- 服务数量少、独立性要求高 → 各自搭建 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 响应
优势:
- 简化架构:减少 Envoy 层的处理逻辑,降低复杂度
- 提高性能:减少一次数据转换,提升响应速度
- 易于维护:响应格式逻辑集中在 RPC 服务中,便于测试和调试
- 类型安全:响应格式在 proto 文件中定义,编译时检查
- 职责清晰: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);
}
关键点:
-
错误处理:所有错误都通过响应消息返回,而不是 gRPC 错误
- 成功:
Code: 200, Msg: "success" - 参数错误:
Code: 400, Msg: "参数错误" - 服务器错误:
Code: 500, Msg: err.Error()
- 成功:
-
统一格式 :所有响应都使用
{code, msg, data}格式 -
字段映射 :直接使用
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_transcoder和router过滤器 - 过滤器顺序:
grpc_json_transcoder→router
9.2.3 步骤 3:重新编译 Proto 文件并重启服务
重新编译 Proto:
bash
cd /apps/www/go/src/hh_bmp/envoy
./build-proto.sh
重启服务:
- 重启 gRPC 服务(使用新的 proto 定义)
- 重启 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 注意事项
-
向后兼容:
- 如果已有客户端在使用,需要考虑向后兼容
- 可以在一段时间内同时支持新旧两种格式
- 通过版本号或路径区分(如
/api/v1/和/api/v2/)
-
错误处理:
- 确保所有错误都通过响应消息返回,而不是 gRPC 错误
- 统一错误码和错误消息格式
- 提供详细的错误信息(便于调试)
-
测试:
- 充分测试所有接口的响应格式
- 测试各种错误场景
- 验证性能是否有提升
-
监控:
- 监控接口响应时间
- 监控错误率
- 对比迁移前后的性能指标
9.6 最佳实践总结
-
统一响应格式:
- 所有有 HTTP 注解的 RPC 方法使用
{code, msg, data}格式 - 内部 gRPC 调用方法可以保持原有格式
- 所有有 HTTP 注解的 RPC 方法使用
-
错误处理:
- 所有错误通过响应消息返回,不使用 gRPC 错误
-
职责划分:
- Envoy:仅做协议转换(HTTP ↔ gRPC)
- RPC 服务:处理业务逻辑和响应格式
-
类型安全:
- 响应格式在 proto 文件中定义
- 编译时检查,避免运行时错误
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 服务端口
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