掌握gRPC核心原理与高效实战技巧

令狐悦洋 框架 阅读 1,474
赞 14 收藏
二维码
手机扫码查看
反馈

优化前:卡得不行

我接手这个项目的时候,前端是 Vue + TypeScript,后端是 Go 写的微服务,通过 gRPC 通信。本来以为这种组合性能应该不错,结果上线后用户反馈「列表加载慢」「操作没响应」。我自己点了几下,好家伙,一个简单的用户信息拉取接口,从点击到出数据要 4-5 秒,某些批量操作甚至直接超时。

掌握gRPC核心原理与高效实战技巧

更离谱的是,并发稍微高一点,服务器 CPU 直接飙到 90% 以上,日志里一堆 context deadline exceededtransport is closing。这哪是 gRPC,简直是 grPC——龟速。

找到瘼颈了!

一开始我以为是网络问题,毕竟跨机房调用。但我把服务都部署在本地 Docker 里跑,还是慢。那就不是网络的事儿了。

我上了 grpcurl 直接调接口测延迟,发现单次调用平均 1.8s,最高的能到 3s。接着我开了 gRPC 的 WithInsecure()WithBlock(),加了拦截器打日志,发现大部分时间花在序列化和反序列化上。

再一查 proto 文件,好家伙,有个接口返回的是嵌套 5 层的结构体,里面还有 repeated 字段,总字段数超过 80 个,传输的数据包动辄几 MB。客户端那边解析也慢,尤其是移动端,直接卡死。

我还用 pprof 跑了一下后端服务,发现 CPU 占用最高的居然是 proto.Marshaljsonpb 的转换逻辑。原来是我们在中间层为了兼容老系统,硬是把 gRPC 返回转成 JSON HTTP 输出了一层……这一来一回,序列化两次,纯属自废武功。

动刀优化:这几个改动最狠

我先砍掉了中间那层 JSON 转换,让前端直接走 gRPC-web。这个改完,单次调用降到 1.2s 左右。虽然还有点慢,但至少有救了。

然后我重点优化了 proto 定义。原来的写法太“全量”了,不管前端需不需要,一股脑全塞进去。我跟产品对了一遍,发现 80% 的字段其实只在管理后台用,普通用户根本看不到。于是我把大结构拆了,做了按场景返回的多个 message。

优化前的 proto:

message UserInfo {
  string id = 1;
  string name = 2;
  string email = 3;
  string avatar = 4;
  repeated Role roles = 5;
  repeated Permission permissions = 6;
  map<string, Setting> settings = 7;
  repeated LoginLog login_logs = 8;
  // 还有几十个……
}

优化后拆成:

message UserBaseInfo {
  string id = 1;
  string name = 2;
  string avatar = 3;
}

message UserPublicInfo {
  string id = 1;
  string name = 2;
  string avatar = 3;
  int32 follower_count = 4;
  bool is_following = 5;
}

message UserAdminInfo {
  string id = 1;
  string name = 2;
  string email = 3;
  repeated Role roles = 4;
  map<string, Setting> settings = 5;
  repeated LoginLog login_logs = 6;
}

然后在 service 里根据角色和请求来源返回不同的 message。光这一招,数据体积从平均 2.3MB 降到 80KB 以内。

客户端别傻等:流式处理 + 缓存

另一个问题是,前端一次性请求太多数据,比如一个接口要拉 1000 条记录,gRPC 一气呵成返回,客户端内存直接爆了。

我改成 server-side streaming:

service DataService {
  rpc ListItems(ListItemsRequest) returns (stream ListItem);
}

Go 后端这么写:

func (s *dataService) ListItems(req *ListItemsRequest, stream DataService_ListItemsServer) error {
    rows, err := db.Query("SELECT id, name FROM items LIMIT 1000")
    if err != nil {
        return err
    }
    defer rows.Close()

    for rows.Next() {
        var item ListItem
        if err := rows.Scan(&item.Id, &item.Name); err != nil {
            return err
        }
        // 边查边发,不攒着
        if err := stream.Send(&item); err != nil {
            return err
        }
    }
    return nil
}

前端用 gRPC-web 的 stream 支持,一边接收一边更新 UI,用户体验从「卡住 5 秒」变成「数据滚动出现」,感知上快多了。

另外我加了简单的内存缓存,用 lru.Cache 缓住高频请求。比如用户详情页,10 秒内重复请求直接走缓存。配合 gRPC 的 deadline 控制,避免雪崩。

连接复用不能少

原来前端每次调用都新建一个 gRPC 连接,而且没设超时。这就导致大量 TIME_WAIT 连接堆积,后端负载飙升。

我把客户端改成全局复用一个 conn:

// grpc-client.js
let client = null

export function getGrpcClient () {
  if (!client) {
    client = new UserServiceClient('https://jztheme.com', null, {
      'grpc.http2.throttle': false,
      'grpc.default_authority': 'jztheme.com'
    })
  }
  return client
}

同时在 Go 服务端启用了 Keepalive:

kaep := keepalive.EnforcementPolicy{
    MinTime:             5 * time.Second,
    PermitWithoutStream: true,
}
kasp := keepalive.ServerParameters{
    MaxConnectionIdle:     15 * time.Second,
    MaxConnectionAge:      30 * time.Second,
    MaxConnectionAgeGrace: 5 * time.Second,
    Time:                  5 * time.Second,
    Timeout:               2 * time.Second,
}

server := grpc.NewServer(
    grpc.KeepaliveEnforcementPolicy(kaep),
    grpc.KeepaliveParams(kasp),
)

连接数从峰值 3000+ 降到了 200 左右,服务器压力肉眼可见地小了。

性能数据对比

改完之后我重新压测了一波,用 ghz 跑 100 并发持续 1 分钟,结果如下:

  • 平均延迟:从 1800ms → 86ms
  • TPS:从 57 → 1140
  • 99% 延迟:从 3200ms → 210ms
  • 服务 CPU 占用:从 85%-95% → 20%-35%
  • 单次传输体积:从 2.3MB → 平均 68KB

最关键的是,用户不再投诉了。之前说「点一下要等半天」,现在都说「怎么突然变丝滑了」。

还有点小毛病,但不影响大局

也不是所有都完美。gRPC-web 在某些旧浏览器上有兼容问题,我们最终上了 envoy 做代理转换。另外 streaming 模式下错误处理要小心,曾经因为一个 panic 导致整个 stream 断掉,后来加了 recover 才稳住。

还有一个坑是 metadata 传递,我们用 JWT 做认证,一开始放在 header 里传,但 gRPC-web 对 custom header 有限制,折腾了半天才发现要用 Authorization 标准头。

总结:别让 gRPC 变“龟速”

gRPC 性能本身不差,但用不好反而比 REST 还慢。我的经验是:

  • 别传大结构,按需返回
  • 别频繁建连,复用是王道
  • 别忽略流式能力,尤其大数据量
  • 别省序列化开销,proto 就该好好设计

这次优化前后花了大概一周,主要是前期定位花时间。改完后系统稳了,我也能睡个好觉了。

以上是我踩坑后的总结,希望对你有帮助。有更好的方案欢迎评论区交流。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论