土法炼钢兴趣小组的算法知识备份

【网络工程】gRPC 深度剖析:HTTP/2 上的 RPC 框架

文章导航

分类入口
network
标签入口
#grpc#http2#protobuf#rpc#microservices

目录

gRPC 是 Google 开源的高性能 RPC 框架,基于 HTTP/2 协议传输、使用 Protocol Buffers(Protobuf)作为接口定义语言和序列化格式。它在微服务架构中被广泛使用,提供了强类型接口、双向流、内置认证等能力。

但 gRPC 不是”用了就快”的银弹。HTTP/2 的连接复用模型与传统负载均衡器的冲突、Protobuf 的版本兼容性陷阱、流控与超时的微妙交互——这些工程细节决定了 gRPC 服务能否在生产环境中稳定运行。

一、gRPC 的四种通信模式

gRPC 支持四种通信模式,覆盖了 RPC 的所有交互场景:

1.1 Unary RPC(一元调用)

// 最常见的模式:一个请求,一个响应
service UserService {
    rpc GetUser(GetUserRequest) returns (GetUserResponse);
}

message GetUserRequest {
    string user_id = 1;
}

message GetUserResponse {
    string user_id = 1;
    string name = 2;
    string email = 3;
    int64 created_at = 4;
}
// Go 客户端调用
conn, err := grpc.Dial("user-service:50051", grpc.WithInsecure())
defer conn.Close()

client := pb.NewUserServiceClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

resp, err := client.GetUser(ctx, &pb.GetUserRequest{UserId: "u-123"})
if err != nil {
    st, ok := status.FromError(err)
    if ok {
        log.Printf("gRPC error: code=%s, message=%s", st.Code(), st.Message())
    }
    return
}
log.Printf("User: %s (%s)", resp.Name, resp.Email)

1.2 Server Streaming RPC(服务端流)

// 服务端持续推送多条响应
service OrderService {
    rpc ListOrders(ListOrdersRequest) returns (stream Order);
}
// 客户端接收流
stream, err := client.ListOrders(ctx, &pb.ListOrdersRequest{UserId: "u-123"})
if err != nil {
    log.Fatal(err)
}

for {
    order, err := stream.Recv()
    if err == io.EOF {
        break  // 流结束
    }
    if err != nil {
        log.Fatal(err)
    }
    log.Printf("Order: %s, amount: %d", order.OrderId, order.Amount)
}

1.3 Client Streaming RPC(客户端流)

// 客户端持续发送,服务端最终返回一个响应
service UploadService {
    rpc UploadFile(stream FileChunk) returns (UploadResult);
}
// 客户端发送流
stream, err := client.UploadFile(ctx)
if err != nil {
    log.Fatal(err)
}

// 分块发送文件
buf := make([]byte, 64*1024)
for {
    n, err := file.Read(buf)
    if err == io.EOF {
        break
    }
    if err := stream.Send(&pb.FileChunk{Data: buf[:n]}); err != nil {
        log.Fatal(err)
    }
}

result, err := stream.CloseAndRecv()
log.Printf("Upload result: %s, size: %d", result.FileId, result.TotalSize)

1.4 Bidirectional Streaming RPC(双向流)

// 双方同时发送和接收
service ChatService {
    rpc Chat(stream ChatMessage) returns (stream ChatMessage);
}
// 双向流:发送和接收在不同 goroutine 中
stream, err := client.Chat(ctx)

// 发送 goroutine
go func() {
    for _, msg := range messages {
        if err := stream.Send(msg); err != nil {
            return
        }
    }
    stream.CloseSend()
}()

// 接收 goroutine(主线程)
for {
    msg, err := stream.Recv()
    if err == io.EOF {
        break
    }
    if err != nil {
        log.Fatal(err)
    }
    log.Printf("Received: %s", msg.Content)
}
四种模式的选型:

模式          │ 场景                        │ HTTP/2 流行为
──────────────┼─────────────────────────────┼──────────────────
Unary         │ 简单查询/写入               │ 一个请求帧 + 一个响应帧
Server Stream │ 列表查询、实时推送           │ 一个请求帧 + 多个响应帧
Client Stream │ 文件上传、批量导入           │ 多个请求帧 + 一个响应帧
Bidi Stream   │ 聊天、实时协作、流式处理     │ 多个请求帧 + 多个响应帧

二、gRPC 在 HTTP/2 上的映射

gRPC 不是独立的传输协议——它完全构建在 HTTP/2 之上。理解这个映射关系对调试和性能优化至关重要。

2.1 请求映射

gRPC 调用映射到 HTTP/2:

一个 gRPC 调用 = 一个 HTTP/2 stream

请求(HEADERS 帧):
  :method          POST
  :scheme          https
  :path            /package.ServiceName/MethodName
  :authority       server.example.com
  content-type     application/grpc
  te               trailers       ← gRPC 必须的
  grpc-timeout     5S             ← 超时时间
  grpc-encoding    gzip           ← 压缩方式(可选)

请求体(DATA 帧):
  ┌──────────────────────────────────────┐
  │ Compressed Flag (1 byte)             │
  │ Message Length  (4 bytes, big-endian) │
  │ Message Data    (protobuf 编码)       │
  └──────────────────────────────────────┘

响应(HEADERS 帧):
  :status          200
  content-type     application/grpc
  grpc-encoding    gzip

响应尾部(HEADERS 帧,带 END_STREAM):
  grpc-status      0              ← gRPC 状态码
  grpc-message     OK             ← 状态消息

2.2 gRPC 消息帧格式

gRPC 在 HTTP/2 DATA 帧中使用自己的消息分帧:

┌─────────────────┬──────────────────┬─────────────────────┐
│ Compressed (1B) │ Length (4B)       │ Message (N bytes)   │
│ 0 or 1          │ big-endian uint32 │ Protobuf 编码的消息  │
└─────────────────┴──────────────────┴─────────────────────┘

为什么需要这个额外分帧?
  HTTP/2 的 DATA 帧可能被拆分或合并
  gRPC 需要知道一条消息的边界在哪里
  这 5 字节的头就是消息的"长度前缀"(Length-Prefixed Message)

流式 RPC 的消息排列:
  [1B + 4B + Msg1] [1B + 4B + Msg2] [1B + 4B + Msg3] ...
  每个消息独立编码,串行排列在同一个 HTTP/2 stream 的 DATA 帧中

2.3 gRPC 状态码

gRPC 状态码(grpc-status)与 HTTP 状态码是两个体系:

gRPC Code │ 名称               │ HTTP 码 │ 常见场景
──────────┼────────────────────┼─────────┼───────────────────────
0         │ OK                 │ 200     │ 成功
1         │ CANCELLED          │ 499     │ 客户端取消
2         │ UNKNOWN            │ 500     │ 未知错误
3         │ INVALID_ARGUMENT   │ 400     │ 参数校验失败
4         │ DEADLINE_EXCEEDED  │ 504     │ 超时
5         │ NOT_FOUND          │ 404     │ 资源不存在
6         │ ALREADY_EXISTS     │ 409     │ 资源已存在
7         │ PERMISSION_DENIED  │ 403     │ 权限不足
8         │ RESOURCE_EXHAUSTED │ 429     │ 资源耗尽(限流)
9         │ FAILED_PRECONDITION│ 400     │ 前置条件不满足
10        │ ABORTED            │ 409     │ 事务冲突
11        │ OUT_OF_RANGE       │ 400     │ 范围越界
12        │ UNIMPLEMENTED      │ 501     │ 方法未实现
13        │ INTERNAL           │ 500     │ 服务端内部错误
14        │ UNAVAILABLE        │ 503     │ 服务不可用(应重试)
15        │ DATA_LOSS          │ 500     │ 数据丢失
16        │ UNAUTHENTICATED    │ 401     │ 未认证

注意: gRPC 的 HTTP 状态码几乎总是 200
真正的错误信息在 grpc-status trailer 中
很多监控工具只看 HTTP 200 会误认为全部成功

三、Protobuf 编码效率

Protobuf 是 gRPC 的默认序列化格式。它的紧凑二进制编码是 gRPC 高性能的关键原因之一。

3.1 编码原理

Protobuf Wire Format:

message User {
    string name = 1;   // field number = 1
    int32 age = 2;     // field number = 2
}

编码 User{name: "Alice", age: 30}:

字节流: 0a 05 41 6c 69 63 65 10 1e

拆解:
  0a = field 1, wire type 2 (length-delimited)
       (1 << 3) | 2 = 0x0a
  05 = 长度 5 字节
  41 6c 69 63 65 = "Alice" 的 UTF-8 编码
  10 = field 2, wire type 0 (varint)
       (2 << 3) | 0 = 0x10
  1e = 30 的 varint 编码

Wire Types:
  0 — Varint (int32, int64, uint32, uint64, sint32, sint64, bool, enum)
  1 — 64-bit (fixed64, sfixed64, double)
  2 — Length-delimited (string, bytes, embedded messages, repeated)
  5 — 32-bit (fixed32, sfixed32, float)

3.2 Protobuf vs JSON 对比

同一条数据的序列化大小对比:

数据: {name: "Alice", age: 30, email: "alice@example.com", active: true}

格式        │ 大小      │ 编码时间   │ 解码时间
────────────┼──────────┼───────────┼──────────
JSON        │ 73 bytes │ 1.0x      │ 1.0x
Protobuf    │ 35 bytes │ 0.15x     │ 0.12x
MessagePack │ 55 bytes │ 0.5x      │ 0.4x

Protobuf 的优势:
  - 大小约为 JSON 的 40-60%
  - 编码/解码速度约为 JSON 的 6-10 倍
  - 强类型: 编译时检查,不需要运行时反射
  - 向前/向后兼容: 增加字段不破坏旧客户端

Protobuf 的劣势:
  - 不可读: 二进制格式需要 .proto 文件才能解码
  - 调试困难: 不能直接用文本工具查看
  - 动态性差: 不适合 Schema 频繁变化的场景

3.3 Protobuf 版本兼容性

安全的 Schema 演进规则:

✅ 安全操作:
  - 添加新字段(使用新的 field number)
  - 删除字段(保留 field number,不重用)
  - 重命名字段(只要 field number 不变)
  - int32 → int64(兼容扩大)

❌ 危险操作:
  - 修改字段的 field number → 数据解析错乱
  - 修改字段类型(string → int32)→ 解码失败
  - 重用已删除的 field number → 数据解析为旧类型

reserved 关键字防止误用:
  message User {
      reserved 3, 5, 9 to 11;         // 保留已删除的 field number
      reserved "old_name", "legacy";    // 保留已删除的字段名

      string name = 1;
      int32 age = 2;
      // field 3 曾是 address,已删除,不可重用
  }

四、gRPC 负载均衡

gRPC 基于 HTTP/2,一个 TCP 连接可以承载多个并发 RPC 调用。这与传统 L4/L7 负载均衡器的模型有根本冲突。

4.1 问题:L4 负载均衡无效

传统 HTTP/1.1 负载均衡:

  客户端 → L4 LB → 后端 A    # 请求 1
  客户端 → L4 LB → 后端 B    # 请求 2(新连接,新后端)
  客户端 → L4 LB → 后端 C    # 请求 3(新连接,新后端)

gRPC/HTTP/2 负载均衡:

  客户端 → L4 LB → 后端 A    # 请求 1(建立连接)
  客户端 → L4 LB → 后端 A    # 请求 2(复用连接,同一后端!)
  客户端 → L4 LB → 后端 A    # 请求 3(复用连接,同一后端!)

问题: HTTP/2 连接复用使得所有 RPC 都走同一个后端
     L4 负载均衡器只在连接建立时做决策
     连接建立后的所有请求都固定在同一个后端

4.2 解决方案

方案 1: L7 负载均衡(代理模式)

  客户端 → L7 LB → 后端 A    # 请求 1
  客户端 → L7 LB → 后端 B    # 请求 2(LB 解析 HTTP/2 帧,按 stream 分发)
  客户端 → L7 LB → 后端 C    # 请求 3

  实现:
  - Envoy: 原生支持 gRPC L7 负载均衡
  - Nginx: 1.13.10+ 支持 gRPC 代理
  - Linkerd/Istio: Service Mesh 的 sidecar 代理

  配置示例(Envoy):
  clusters:
    - name: grpc_service
      type: STRICT_DNS
      lb_policy: ROUND_ROBIN
      http2_protocol_options: {}    # 启用 HTTP/2 上游
      load_assignment:
        endpoints:
          - lb_endpoints:
              - endpoint:
                  address:
                    socket_address:
                      address: grpc-backend
                      port_value: 50051

方案 2: 客户端负载均衡

  客户端维护多个后端连接,自己做负载分发:

  客户端 ──→ 后端 A    # 请求 1(直连)
        ├──→ 后端 B    # 请求 2(直连)
        └──→ 后端 C    # 请求 3(直连)

  Go 实现:
  import "google.golang.org/grpc/balancer/roundrobin"

  conn, err := grpc.Dial(
      "dns:///grpc-service.default.svc.cluster.local:50051",
      grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy":"round_robin"}`),
      grpc.WithInsecure(),
  )

  // gRPC 支持的内置策略:
  // pick_first: 使用第一个可用地址(默认)
  // round_robin: 轮询所有地址

  服务发现集成:
  - DNS: 返回多个 A/AAAA 记录
  - Consul / etcd: 自定义 Resolver
  - Kubernetes: Headless Service(返回所有 Pod IP)

方案对比:
  方案       │ 优点                  │ 缺点
  ──────────┼───────────────────────┼──────────────────────
  L7 代理    │ 客户端无感知          │ 额外延迟和资源开销
             │ 统一管理              │ 代理成为瓶颈
  客户端 LB  │ 无额外延迟            │ 客户端复杂度增加
             │ 无中间代理            │ 需要服务发现集成

4.3 Kubernetes 中的 gRPC 负载均衡

Kubernetes 默认的 ClusterIP Service 是 L4(iptables/IPVS)
对 gRPC 来说,负载均衡基本无效

解决方案:

方案 A: Headless Service + 客户端 LB
  apiVersion: v1
  kind: Service
  metadata:
    name: grpc-service
  spec:
    clusterIP: None    # Headless: DNS 返回所有 Pod IP
    selector:
      app: grpc-service
    ports:
      - port: 50051

  客户端连接: dns:///grpc-service.default.svc.cluster.local:50051
  gRPC 客户端轮询所有 Pod IP

方案 B: Service Mesh(Istio/Linkerd)
  Sidecar proxy 自动处理 gRPC L7 负载均衡
  对应用代码完全透明

方案 C: Ingress/Gateway(如 Envoy Gateway)
  对外暴露 gRPC 服务时使用

五、gRPC 连接管理

5.1 连接状态

gRPC 连接状态机:

  IDLE ──→ CONNECTING ──→ READY ──→ IDLE(空闲超时)
    │          │              │
    │          ↓              ↓
    │    TRANSIENT_FAILURE    SHUTDOWN
    │          │
    │          ↓
    └───→ (指数退避重连)

状态说明:
  IDLE:               连接未建立或已空闲关闭
  CONNECTING:         正在建立 TCP/TLS 连接
  READY:              连接可用,可以发送 RPC
  TRANSIENT_FAILURE:  连接失败,正在指数退避重连
  SHUTDOWN:           连接已关闭,不再重试

客户端行为:
  - gRPC 默认维护连接池,自动重连
  - 连接空闲超过一定时间自动关闭
  - TRANSIENT_FAILURE 状态下的 RPC 会排队等待

5.2 Keepalive 配置

// 服务端 Keepalive 参数
server := grpc.NewServer(
    grpc.KeepaliveParams(keepalive.ServerParameters{
        MaxConnectionIdle:     5 * time.Minute,  // 空闲连接最大时长
        MaxConnectionAge:      30 * time.Minute, // 连接最大存活时间
        MaxConnectionAgeGrace: 10 * time.Second, // 优雅关闭的等待时间
        Time:                  2 * time.Minute,  // Keepalive Ping 间隔
        Timeout:               20 * time.Second, // Ping 超时
    }),
    grpc.KeepaliveEnforcementPolicy(keepalive.EnforcementPolicy{
        MinTime:             30 * time.Second, // 客户端 Ping 最小间隔
        PermitWithoutStream: true,             // 允许无活跃流时发送 Ping
    }),
)

// 客户端 Keepalive 参数
conn, err := grpc.Dial(target,
    grpc.WithKeepaliveParams(keepalive.ClientParameters{
        Time:                30 * time.Second,  // 空闲时发送 Ping 的间隔
        Timeout:             10 * time.Second,  // Ping 超时
        PermitWithoutStream: true,              // 无活跃流时也发送 Ping
    }),
)

// MaxConnectionAge 的工程价值:
// 定期断开并重新建立连接
// 使得客户端有机会连接到新的后端实例
// 这对负载均衡和滚动发布非常重要

六、拦截器(Interceptor)

gRPC 拦截器类似于 HTTP 中间件,用于在 RPC 调用前后注入通用逻辑。

6.1 Unary 拦截器

// 日志 + 监控拦截器
func loggingInterceptor(
    ctx context.Context,
    req interface{},
    info *grpc.UnaryServerInfo,
    handler grpc.UnaryHandler,
) (interface{}, error) {
    start := time.Now()

    // 调用实际处理器
    resp, err := handler(ctx, req)

    // 记录日志和指标
    duration := time.Since(start)
    code := status.Code(err)

    log.Printf("method=%s duration=%v code=%s",
        info.FullMethod, duration, code)

    // Prometheus 指标
    grpcRequestDuration.WithLabelValues(
        info.FullMethod, code.String(),
    ).Observe(duration.Seconds())

    grpcRequestTotal.WithLabelValues(
        info.FullMethod, code.String(),
    ).Inc()

    return resp, err
}

// 注册拦截器(可链式注册多个)
server := grpc.NewServer(
    grpc.ChainUnaryInterceptor(
        recoveryInterceptor,   // panic 恢复(最外层)
        loggingInterceptor,    // 日志记录
        authInterceptor,       // 认证
        validationInterceptor, // 参数校验
    ),
)

6.2 认证拦截器

func authInterceptor(
    ctx context.Context,
    req interface{},
    info *grpc.UnaryServerInfo,
    handler grpc.UnaryHandler,
) (interface{}, error) {
    // 跳过不需要认证的方法
    if info.FullMethod == "/grpc.health.v1.Health/Check" {
        return handler(ctx, req)
    }

    // 从 metadata 中提取 token
    md, ok := metadata.FromIncomingContext(ctx)
    if !ok {
        return nil, status.Error(codes.Unauthenticated, "missing metadata")
    }

    tokens := md.Get("authorization")
    if len(tokens) == 0 {
        return nil, status.Error(codes.Unauthenticated, "missing token")
    }

    // 验证 token
    claims, err := validateToken(tokens[0])
    if err != nil {
        return nil, status.Error(codes.Unauthenticated, "invalid token")
    }

    // 将用户信息注入 context
    ctx = context.WithValue(ctx, "user", claims)
    return handler(ctx, req)
}

七、gRPC 错误处理

7.1 结构化错误

import (
    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/status"
    "google.golang.org/genproto/googleapis/rpc/errdetails"
)

// 返回带详细信息的错误
func (s *server) CreateOrder(ctx context.Context, req *pb.CreateOrderRequest) (*pb.Order, error) {
    // 参数校验
    if req.Amount <= 0 {
        st := status.New(codes.InvalidArgument, "invalid order amount")
        // 添加结构化的错误详情
        details, _ := st.WithDetails(
            &errdetails.BadRequest{
                FieldViolations: []*errdetails.BadRequest_FieldViolation{
                    {
                        Field:       "amount",
                        Description: "Amount must be positive",
                    },
                },
            },
        )
        return nil, details.Err()
    }

    // 资源限流
    if s.rateLimiter.IsExceeded(ctx) {
        st := status.New(codes.ResourceExhausted, "rate limit exceeded")
        details, _ := st.WithDetails(
            &errdetails.RetryInfo{
                RetryDelay: durationpb.New(5 * time.Second),
            },
        )
        return nil, details.Err()
    }

    return &pb.Order{OrderId: "ord-001"}, nil
}

7.2 超时与取消

// 超时传播: 客户端设置的 deadline 自动传递到服务端
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()

resp, err := client.ProcessOrder(ctx, req)
if err != nil {
    st := status.Convert(err)
    switch st.Code() {
    case codes.DeadlineExceeded:
        // 超时: 可能需要查询订单状态确认是否已处理
        log.Println("Request timed out, checking order status...")
    case codes.Canceled:
        // 取消: 客户端主动取消
        log.Println("Request cancelled")
    case codes.Unavailable:
        // 不可用: 应该重试
        log.Println("Service unavailable, retrying...")
    }
}

// 服务端检查 deadline
func (s *server) ProcessOrder(ctx context.Context, req *pb.OrderRequest) (*pb.OrderResponse, error) {
    deadline, ok := ctx.Deadline()
    if ok {
        remaining := time.Until(deadline)
        if remaining < 100*time.Millisecond {
            return nil, status.Error(codes.DeadlineExceeded, "insufficient time remaining")
        }
    }

    // 长时间操作中检查 context
    select {
    case <-ctx.Done():
        return nil, status.FromContextError(ctx.Err()).Err()
    case result := <-s.processAsync(req):
        return result, nil
    }
}

八、gRPC 性能优化

8.1 连接池

// 默认 gRPC 客户端使用单连接
// 对于高并发场景,单连接可能成为瓶颈

// 方案: 手动管理连接池
type ConnPool struct {
    conns []*grpc.ClientConn
    idx   uint64
}

func NewConnPool(target string, size int) (*ConnPool, error) {
    pool := &ConnPool{conns: make([]*grpc.ClientConn, size)}
    for i := 0; i < size; i++ {
        conn, err := grpc.Dial(target, grpc.WithInsecure())
        if err != nil {
            return nil, err
        }
        pool.conns[i] = conn
    }
    return pool, nil
}

func (p *ConnPool) Get() *grpc.ClientConn {
    idx := atomic.AddUint64(&p.idx, 1)
    return p.conns[idx%uint64(len(p.conns))]
}

// 连接池大小建议:
// CPU 核数 × 2(经验值)
// 通过压测确定最优值

8.2 消息压缩

import "google.golang.org/grpc/encoding/gzip"

// 客户端启用压缩
resp, err := client.GetLargeData(ctx, req,
    grpc.UseCompressor(gzip.Name))

// 服务端自动支持解压(无需额外配置)

// 压缩效果参考:
// JSON 文本: 压缩率 60-80%
// Protobuf 二进制: 压缩率 20-40%(已经很紧凑)
// 小消息(<100B): 压缩后可能更大(压缩头开销)

// 建议:
// 消息 > 1KB 时考虑压缩
// 小消息不要压缩(CPU 开销大于网络节省)

8.3 性能基准对比

gRPC vs REST/JSON 性能对比(典型微服务场景):

指标                │ gRPC + Protobuf  │ REST + JSON
────────────────────┼──────────────────┼──────────────
序列化速度          │ ~1.2μs           │ ~8.5μs
反序列化速度        │ ~0.9μs           │ ~12.3μs
消息大小(小对象)  │ ~35 bytes        │ ~120 bytes
消息大小(大列表)  │ ~4.2 KB          │ ~11.8 KB
请求延迟 (P50)      │ ~0.5ms           │ ~1.2ms
请求延迟 (P99)      │ ~2.1ms           │ ~5.8ms
吞吐量 (单连接)     │ ~45,000 req/s    │ ~12,000 req/s

gRPC 的性能优势来自:
1. Protobuf 二进制编码(vs JSON 文本)
2. HTTP/2 连接复用(vs HTTP/1.1 每请求一连接)
3. HTTP/2 头压缩(HPACK)
4. 编译时生成代码(vs 运行时反射)

九、gRPC-Web 与浏览器限制

浏览器无法直接使用 gRPC,因为浏览器的 HTTP/2 实现不暴露帧级别的控制能力。gRPC-Web 是解决这个问题的方案。

gRPC-Web 的工作方式:

  浏览器 ──→ gRPC-Web Proxy ──→ gRPC Server
             (Envoy)

  浏览器发送: HTTP/1.1 或 HTTP/2 + application/grpc-web
  Proxy 转换: application/grpc-web → application/grpc
  Server 响应: 标准 gRPC 响应
  Proxy 转换: application/grpc → application/grpc-web
  浏览器接收: gRPC-Web 格式的响应

gRPC-Web 的限制:
  ❌ 不支持 Client Streaming(浏览器 Fetch API 限制)
  ❌ 不支持 Bidirectional Streaming
  ✅ 支持 Unary RPC
  ✅ 支持 Server Streaming(通过 chunked transfer)

  如果需要浏览器双向流,使用 WebSocket 或 SSE 更合适

Envoy gRPC-Web 配置:
  http_filters:
    - name: envoy.filters.http.grpc_web
      typed_config:
        "@type": type.googleapis.com/envoy.extensions.filters.http.grpc_web.v3.GrpcWeb
    - name: envoy.filters.http.cors
    - name: envoy.filters.http.router

替代方案: Connect Protocol (buf.build)
  - 支持标准 HTTP/1.1 JSON 和 gRPC 双模式
  - 浏览器可直接使用,无需 proxy
  - 兼容现有 gRPC 服务端

十、总结

gRPC 是微服务通信的高效选择,但它的工程复杂度不可忽视。

  1. 四种通信模式选对场景。大部分场景 Unary 足够。Server Streaming 适合列表查询和事件推送。双向流只在真正需要实时双向通信时使用。

  2. 负载均衡是最大的工程挑战。HTTP/2 连接复用让 L4 负载均衡失效。生产环境必须使用 L7 代理(Envoy)或客户端负载均衡。

  3. Protobuf 的版本兼容要谨慎。永远不要重用 field number,删除字段用 reserved 标记。

  4. 错误处理用 gRPC 状态码而不是 HTTP 状态码。监控系统必须解析 grpc-status trailer,否则所有请求都显示 HTTP 200。

  5. 连接管理很重要。MaxConnectionAge 配合客户端负载均衡可以实现渐进式的流量迁移。Keepalive 参数需要客户端和服务端协调。

  6. gRPC-Web 有局限。浏览器场景如果需要双向流,用 WebSocket。如果只需要 Unary + Server Streaming,gRPC-Web 是好选择。Connect Protocol 是更现代的替代方案。


参考文献


上一篇:WebSocket 工程:握手、帧格式与大规模运维

下一篇:SSE 与长轮询:服务端推送的轻量解法

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-09-10 · network

HTTP/3 实战:从 QUIC 到 H3 的完整请求链路

QUIC 解决了传输层的问题,但 HTTP 怎么跑在上面?HTTP/3 不是简单地把 HTTP/2 搬到 QUIC 上——帧格式变了,头部压缩换了,流控删了。这篇从 QPACK 压缩到完整请求链路,把 HTTP/3 拆干净。

2026-04-22 · network

网络工程索引

汇总本站网络工程系列文章,覆盖分层模型、以太网、IP、TCP、DNS、TLS、HTTP/2/3、CDN、BGP 与故障诊断。


By .