在微服务架构中,服务之间的通信方式并不只是一个“HTTP 还是 RPC”的技术选型问题。它实际上决定了服务如何定义边界、如何表达能力、如何处理异常、如何控制超时、如何进行服务治理,以及系统在规模扩大之后还能不能保持稳定。

gRPC 之所以能够成为现代微服务体系中非常重要的一种 RPC 框架,并不仅仅因为它“比 REST 快”,而是因为它试图解决一个更深层的问题:

如何让分布式系统中的服务,把一次跨进程调用尽可能地抽象成本地方法调用,同时又保留分布式系统必须面对的网络、超时、失败和治理能力。

因此,理解 gRPC 不能停留在“Proto 怎么写、接口怎么调”的层面。真正理解 gRPC,需要从它的通信模型、协议设计、代码生成、流式通信、错误模型以及服务治理几个维度去思考。


一、gRPC 解决的问题

假设我们有一个用户服务 UserService,订单服务需要查询用户信息。

传统 HTTP API 可能这样设计:

GET /api/user/10001

返回:

{
    "id": 10001,
    "name": "张三",
    "age": 20
}

调用方需要知道:

  • URL 是什么;

  • HTTP Method 是什么;

  • 参数放在哪里;

  • JSON 字段叫什么;

  • 返回结构是什么;

  • 错误应该如何解析。

如果服务越来越多,这些接口约定就会逐渐成为系统中的隐性成本。

gRPC 的思路则不同。

我们首先定义一个服务契约:

syntax = "proto3";

package user;

service UserService {
    rpc GetUser(GetUserRequest) returns (GetUserResponse);
}

message GetUserRequest {
    int64 id = 1;
}

message GetUserResponse {
    int64 id = 1;
    string name = 2;
    int32 age = 3;
}

此时我们定义的不再是:

“调用 /user/10001 这个 HTTP 地址。”

而是:

“UserService 提供一个叫 GetUser 的远程方法。”

客户端最终使用起来甚至非常接近普通函数:

resp, err := client.GetUser(ctx, &pb.GetUserRequest{
    Id: 10001,
})

这就是 gRPC 最核心的思想:

把远程服务能力定义成强类型接口,再通过代码生成把网络通信细节隐藏起来。

这里实际上包含了三个非常重要的设计:

第一,契约优先。

服务首先定义接口,而不是先写 HTTP Handler。

第二,强类型。

请求、响应、字段类型全部由 Proto 明确定义。

第三,代码生成。

开发者不需要手动编写大量序列化、反序列化和 RPC 调用代码。

所以,gRPC 的价值并不能简单理解成“HTTP + Protobuf”。

更准确地说:

gRPC 是一套以服务契约为核心,通过 HTTP/2 承载通信、Protocol Buffers 负责数据编码,并通过 Stub 屏蔽远程调用细节的 RPC 技术体系。


二、gRPC 的核心:Proto、Stub 与一次 RPC 的交互

很多人第一次接触 gRPC,会认为:

客户端 → gRPC → 服务端

但真正理解 gRPC,需要把这一过程拆开。

1. Proto 是整个通信系统的“契约”

Proto 文件本质上是在定义:

谁提供服务
    ↓
提供什么方法
    ↓
方法接收什么参数
    ↓
返回什么结果

例如:

service OrderService {
    rpc CreateOrder(CreateOrderRequest) returns (CreateOrderResponse);
}

这实际上就定义了一份跨服务的 API Contract。

与 REST API 相比,Proto 最大的优势并不是“写起来更方便”,而是接口契约变得机器可理解

例如:

message CreateOrderRequest {
    int64 user_id = 1;
    repeated int64 product_ids = 2;
    string remark = 3;
}

编译器可以直接知道:

  • user_id 是 int64;

  • product_ids 是数组;

  • remark 是字符串。

因此很多错误可以在编译阶段发现,而不是等到线上通过 JSON 解析失败才发现。


2. Protobuf 为什么效率高?

JSON:

{
    "id": 10001,
    "name": "Tom"
}

它不仅保存数据,还保存大量文本信息。

而 Protobuf 更接近:

字段编号 + 字段类型 + 字段值

例如:

int64 id = 1;
string name = 2;

真正传输的数据不会把完整字段名 "name" 重复传递。

这意味着:

JSON
可读性强
    ↓
文本协议
    ↓
数据体积较大
    ↓
解析成本相对更高

而:

Protobuf
二进制编码
    ↓
数据更加紧凑
    ↓
序列化/反序列化效率高

对于内部微服务来说,服务之间调用频率可能达到几十万甚至更高 QPS,此时数据编码效率就会逐渐体现价值。

但这里需要注意一个容易产生误解的问题:

gRPC 快,并不意味着只要把 HTTP REST 改成 gRPC,系统性能就一定会大幅提升。

真实系统中,一次 RPC 调用可能还包括:

业务处理
↓
数据库查询
↓
Redis
↓
网络传输
↓
序列化
↓
线程/Goroutine 调度
↓
锁竞争

如果你的接口 95% 的耗时都在数据库,那么把 JSON 换成 Protobuf,最终性能提升可能非常有限。

所以技术选型不能只看 Benchmark。


3. Stub 是 gRPC 最关键的抽象之一

Proto 编译之后,会生成客户端和服务端代码。

客户端得到类似:

type UserServiceClient interface {
    GetUser(
        ctx context.Context,
        in *GetUserRequest,
        opts ...grpc.CallOption,
    ) (*GetUserResponse, error)
}

开发者调用:

resp, err := client.GetUser(ctx, req)

表面上看起来像:

User.GetUser()

实际上背后发生了:

客户端业务代码
       ↓
Client Stub
       ↓
Protobuf 序列化
       ↓
HTTP/2
       ↓
网络
       ↓
HTTP/2
       ↓
gRPC Server
       ↓
Protobuf 反序列化
       ↓
Server Handler
       ↓
业务逻辑

这就是 RPC 的核心思想:

把“网络通信”包装成“方法调用”。

但是这里又隐藏着一个非常重要的工程认知:

RPC 看起来像本地调用,但它绝对不是本地调用。

本地函数:

user := GetUser()

通常只需要考虑程序内部逻辑。

RPC 则必须考虑:

网络断开
服务不可用
超时
服务端崩溃
连接池耗尽
负载过高
请求重复
版本不兼容

因此:

gRPC 越像本地调用,开发者越不能忘记它实际上是远程调用。

这是理解 RPC 最重要的思维转变之一。


三、HTTP/2 为什么是 gRPC 的重要基础?

gRPC 默认建立在 HTTP/2 之上。

如果只把 HTTP/1.1 和 HTTP/2 理解成:

HTTP/2 比 HTTP/1.1 快。

其实是不够的。

gRPC 真正依赖的是 HTTP/2 提供的一整套通信能力。

1. 多路复用

HTTP/1.1 中,一个连接上的请求处理能力受到连接和队头阻塞等因素影响。

HTTP/2 则可以在一个 TCP 连接上同时承载多个 Stream:

TCP Connection
│
├── Stream 1 → RPC A
├── Stream 3 → RPC B
├── Stream 5 → RPC C
└── Stream 7 → RPC D

这意味着:

多个 RPC 可以复用同一个底层连接。

因此不需要:

请求 A → 建立连接
请求 B → 建立连接
请求 C → 建立连接

而可以:

一个 TCP Connection
        ↓
多个 RPC Stream

这对于大量微服务之间的内部调用非常重要。


2. HTTP/2 + Protobuf + gRPC 的关系

三者实际上承担不同职责:

gRPC
│
├── RPC 抽象
│
├── 服务定义
│
├── Stub
│
├── Deadline
│
├── Metadata
│
└── Status Code
       │
       ↓
HTTP/2
│
├── Stream
├── Multiplexing
├── Header Compression
└── Flow Control
       │
       ↓
Protobuf
│
└── 二进制序列化

因此不能简单理解成:

gRPC = HTTP/2

更准确地说:

HTTP/2 是 gRPC 的传输基础,Protobuf 是主要的数据编码方式,而 gRPC 在此之上构建了完整的 RPC 抽象和服务调用模型。


3. 为什么 gRPC 不一定适合所有场景?

因为技术优势往往伴随着约束。

例如浏览器原生环境并不是为传统 gRPC 设计的,因此 Web 前端通常不能像 Go 服务之间那样直接使用标准 gRPC。

于是出现了:

gRPC-Web

或者:

BFF
 ↓
gRPC
 ↓
内部微服务

这也是一个非常常见的架构模式:

Browser
   ↓ HTTP/JSON
API Gateway / BFF
   ↓ gRPC
User Service
Order Service
Payment Service

这样可以让:

  • 外部 API 保持 HTTP/JSON 的兼容性;

  • 内部服务使用 gRPC;

  • 内部通信获得强类型和高效二进制传输。


四、gRPC 真正有价值的地方:Streaming

如果只把 gRPC 当作:

Request → Response

其实只发挥了它的一部分能力。

gRPC 更有意思的地方是四种通信模式。

Unary RPC

最普通:

Client
  │
  │ Request
  ↓
Server
  │
  │ Response
  ↓
Client

例如:

rpc GetUser(GetUserRequest) returns (GetUserResponse);

适合:

  • 查询用户;

  • 创建订单;

  • 获取商品;

  • 普通服务调用。


Server Streaming

客户端发一次请求,服务端持续返回数据:

rpc ListUsers(ListUsersRequest)
    returns (stream User);

模型:

Client
   │ Request
   ↓
Server
   │
   ├── User 1
   ├── User 2
   ├── User 3
   ├── User 4
   └── ...

适合:

  • 大量数据传输;

  • 实时日志;

  • 实时事件;

  • 持续推送。


Client Streaming

客户端持续发送,服务端最终返回结果:

rpc Upload(stream FileChunk)
    returns (UploadResponse);

模型:

Client
 │ Chunk 1
 │ Chunk 2
 │ Chunk 3
 │ Chunk 4
 ↓
Server
 │
 └── Response

这对于大文件、批量数据上传等场景非常有意义。


Bidirectional Streaming

最完整的一种模式:

rpc Chat(stream ChatMessage)
    returns (stream ChatMessage);

此时双方都可以持续发送数据:

Client                    Server

   Message 1  ───────────→
                ←────────── Message A

   Message 2  ───────────→
                ←────────── Message B

   Message 3  ───────────→
                ←────────── Message C

这已经非常接近一个真正的实时通信通道。

例如:

  • 实时聊天;

  • AI 流式输出;

  • 实时监控;

  • 服务间事件流;

  • 长连接数据同步。


Streaming 背后的思考

Streaming 最大的价值并不是“可以传很多数据”。

真正的价值是:

它允许服务之间建立持续的数据流,而不是把每一次交互都切割成独立的 Request/Response。

这对于 AI 系统尤其重要。

例如 LLM 输出:

你
好
,
这
是
一
段
流
式
输
出

如果使用传统同步接口:

请求
 ↓
等待 20 秒
 ↓
一次性返回

用户体验非常差。

而 Streaming:

Request
 ↓
Token 1
 ↓
Token 2
 ↓
Token 3
 ↓
Token 4
...

能够把模型生成过程实时传递给上层服务。

因此在 AI Agent、LLM Gateway、模型服务等架构中,gRPC Streaming 的价值会比传统 CRUD 场景更加明显。


五、 gRPC 系统的:Deadline、错误与取消

很多 gRPC 项目开发初期非常顺利:

客户端调用
↓
服务端处理
↓
返回结果

但系统上线之后,真正的问题往往来自:

超时
重试
服务雪崩
连接堆积
级联失败

1. 为什么必须设置 Deadline?

假设:

A → B → C → D

A 调用 B:

A
 ↓ 5s
B
 ↓ 5s
C
 ↓ 5s
D

如果每一层都没有超时控制,一旦 D 出问题:

D 卡住
 ↓
C 等待
 ↓
B 等待
 ↓
A 等待

最终整个调用链上的资源都会被占用。

这就是典型的级联故障。

因此 gRPC 调用应该携带:

ctx, cancel := context.WithTimeout(
    context.Background(),
    2*time.Second,
)
defer cancel()

resp, err := client.GetUser(ctx, req)

这里的核心思想不是:

“让请求两秒后失败。”

而是:

给一次分布式计算设置资源生命周期。


2. Context Cancellation

如果用户已经关闭页面:

Browser
   ↓
API Gateway
   ↓
Agent
   ↓
RAG
   ↓
LLM

此时用户取消请求。

如果取消信号能够通过 Context 一路向下传播:

User Cancel
    ↓
Gateway Cancel
    ↓
Agent Cancel
    ↓
RAG Cancel
    ↓
LLM Cancel

底层任务就可以及时停止。

否则就可能出现:

用户早就离开了
       ↓
服务仍然继续执行
       ↓
继续占 CPU
       ↓
继续占连接
       ↓
继续调用模型
       ↓
继续消耗资源

所以 Context 在 gRPC 中并不只是一个参数。

它实际上承担了:

分布式任务生命周期控制器

的角色。


3. gRPC Status Code

gRPC 有自己的错误模型:

OK
CANCELLED
UNKNOWN
INVALID_ARGUMENT
DEADLINE_EXCEEDED
NOT_FOUND
ALREADY_EXISTS
PERMISSION_DENIED
UNAUTHENTICATED
RESOURCE_EXHAUSTED
UNAVAILABLE
INTERNAL
UNIMPLEMENTED
...

这比简单返回:

{
    "code": 500,
    "message": "error"
}

更加标准化。

例如:

NOT_FOUND

代表资源不存在。

INVALID_ARGUMENT

代表参数不合法。

UNAVAILABLE

通常表示服务暂时不可用。

DEADLINE_EXCEEDED

表示调用超过截止时间。

这样调用方就可以针对不同错误采取不同策略。

例如:

INVALID_ARGUMENT
      ↓
不要重试

UNAVAILABLE
      ↓
可以考虑重试

DEADLINE_EXCEEDED
      ↓
根据业务判断是否重试

RESOURCE_EXHAUSTED
      ↓
限流 / 降级 / 延迟重试

这就从单纯的“错误处理”进入了真正的分布式系统治理


六、重试不是越多越好:gRPC 背后的分布式系统思维

这是使用 gRPC 时最容易踩坑的地方之一。

假设:

Client
 ↓
Order Service
 ↓
Payment Service

客户端调用支付:

Pay()

结果:

请求已经到达 Payment Service
Payment Service 已经扣款
但是响应返回过程中网络断开

客户端看到:

UNAVAILABLE

于是自动重试:

Pay()
Pay()

结果可能变成:

扣款 2 次

所以:

重试必须建立在幂等性之上。

例如:

CreateOrder

可以设计:

request_id = "abc123"

服务端:

request_id
     ↓
Redis / DB
     ↓
是否已经处理?

如果已经处理:

直接返回之前结果

这样才能安全重试。

因此一个成熟的 gRPC 系统应该形成:

Deadline
   +
Cancellation
   +
Retry
   +
Idempotency
   +
Circuit Breaker
   +
Rate Limit

而不是:

调用失败 → 重试三次

后者在高并发环境中甚至可能把一个故障放大成整个系统的雪崩。


七、gRPC 用于微服务治理

当系统从:

3 个服务

增长到:

30 个服务

甚至:

300 个服务

真正困难的就不再是:

“怎么调用 gRPC?”

而是:

“怎么找到服务?怎么负载均衡?怎么监控?怎么定位问题?”

例如:

Order Service
     ↓
User Service

如果 User Service 部署了:

10.0.0.1
10.0.0.2
10.0.0.3

Order Service 到底调用谁?

这就进入了:

服务发现

Service Name
    ↓
Service Registry
    ↓
Instance List

例如:

user-service
    ↓
10.0.0.1:9001
10.0.0.2:9001
10.0.0.3:9001

然后进行负载均衡:

          User Service
        /      |      \
       ↓       ↓       ↓
     Node1   Node2   Node3

在 Kubernetes 环境中,这个问题又可以进一步交给:

Service
   ↓
ClusterIP
   ↓
Pod

所以 gRPC 并不是孤立存在的。

                Gateway
                   │
              HTTP / gRPC
                   │
             ┌─────┴─────┐
             ↓           ↓
         User Service  Order Service
             │           │
             └─────┬─────┘
                   ↓
              Other Services

再向下还会涉及:

Service Discovery
Load Balancing
Tracing
Metrics
Logging
Circuit Breaker
Rate Limiting
Retry
Authentication

八、gRPC 服务化设计:通用“中间件”的设计

在实际开发中,经常会遇到一个问题:

“我有一个公共中间件,现在多个服务都需要使用,应该怎么办?”

例如:

文档解析服务
知识库服务
Agent 服务
PPT 服务

都需要调用:

Embedding

一种方式是:

每个服务都集成 Embedding SDK

但这样会产生:

Service A → SDK
Service B → SDK
Service C → SDK
Service D → SDK

模型地址、鉴权、限流、缓存、重试逻辑全部散落在各个服务。

另一种方式是把它服务化:

Service A ─┐
Service B ─┼──→ Embedding Service
Service C ─┤
Service D ─┘

接口:

service EmbeddingService {
    rpc Embed(EmbedRequest) returns (EmbedResponse);
}

这时 gRPC 就非常适合。

因为它可以把:

公共能力

抽象成:

标准服务契约

其他服务不需要关心:

模型在哪里
怎么连接
怎么序列化
怎么重试
怎么做连接复用

只需要:

client.Embed(ctx, req)

这实际上是:

把基础设施能力从“代码依赖”升级成“服务依赖”。

但是也必须注意,服务化不是免费的。

原本:

Service → Local Library

变成:

Service → Network → Middleware Service

增加了:

  • 网络延迟;

  • RPC 失败概率;

  • 服务治理成本;

  • 部署成本;

  • 监控成本。

所以:

只有当公共能力需要独立扩缩容、统一治理、跨语言复用、独立演进时,才值得服务化。

否则把一个简单工具函数强行拆成微服务,反而属于过度设计。


九、gRPC 在 AI Agent 架构中的价值

如果把 gRPC 放到现在的 AI 系统中,会发现它与 Agent 架构非常契合。

例如:

                    API Gateway
                         │
                         ↓
                   Agent Service
                         │
             ┌───────────┼───────────┐
             ↓           ↓           ↓
           RAG        Tool        Model
          Service     Service     Service
             │           │           │
             ↓           ↓           ↓
          Milvus       MCP/工具      LLM

这里各个服务之间可能存在:

Agent → RAG
Agent → Tool
Agent → Model
Agent → Memory

如果全部通过 HTTP/JSON:

大量接口
大量 DTO
大量 JSON
大量手写解析

而 gRPC + Proto 可以统一定义:

service RagService {
    rpc Search(SearchRequest) returns (SearchResponse);
}

service ModelService {
    rpc Generate(GenerateRequest) returns (stream GenerateChunk);
}

service MemoryService {
    rpc GetMemory(GetMemoryRequest) returns (GetMemoryResponse);
}

特别是模型服务:

rpc Generate(GenerateRequest)
    returns (stream GenerateChunk);

天然适合 Token Streaming。

于是整个 AI 调用链可以形成:

User
 ↓
Agent
 ↓
RAG ─────→ Vector DB
 ↓
Model Service
 ↓
Streaming
 ↓
Agent
 ↓
User

这时 gRPC 的价值已经不只是:

“通信更快。”

而是:

把 AI 系统中的模型、RAG、工具、Memory 等能力标准化成可组合的服务接口。

这对于未来构建 AI 基础设施尤其重要。


十、gRPC 的工程落地:一个完整服务模块

一个真正可以用于生产的 gRPC 服务,至少应该考虑下面几个层次:

                    gRPC Service
                         │
        ┌────────────────┼────────────────┐
        ↓                ↓                ↓
      Contract          Runtime          Governance
        │                │                │
      Proto             Context          Retry
      Version           Deadline         RateLimit
      Compatibility     Cancellation     CircuitBreaker
        │                │                │
        └────────────────┼────────────────┘
                         ↓
                     Business
                         │
                ┌────────┴────────┐
                ↓                 ↓
              Redis             MySQL

其中最容易被忽视的是接口版本管理

例如:

message User {
    int64 id = 1;
    string name = 2;
}

未来新增:

string email = 3;

这是比较安全的。

但如果直接修改:

string name = 2;

的字段语义,或者复用已经删除的字段编号,就可能导致新旧版本之间产生严重问题。

因此 Proto 的字段编号具有非常重要的兼容性意义。

一个成熟的接口演进过程应该遵循:

旧客户端
      ↓
旧字段继续保留
      ↓
新增字段
      ↓
新客户端逐渐迁移
      ↓
旧字段废弃

而不是:

改 Proto
↓
重新部署
↓
祈祷所有服务同时升级成功

微服务的一个基本事实就是:

不同服务几乎不可能永远保持完全同步发布。

所以接口设计必须天然考虑版本兼容。


十一、如何理解 gRPC 与 REST 的关系?

很多技术文章喜欢说:

gRPC > REST

这种说法其实没有意义。

它们解决的问题不同。

REST 更适合:

Browser
 ↓
HTTP
 ↓
Public API

特点是:

  • 简单;

  • 通用;

  • 易调试;

  • 浏览器天然支持;

  • 第三方接入成本低。

gRPC 更适合:

Service A
    ↓
gRPC
    ↓
Service B

特点是:

  • 强类型;

  • Proto 契约;

  • 自动生成代码;

  • Protobuf 高效编码;

  • HTTP/2 多路复用;

  • Streaming;

  • 更适合内部服务通信。

因此非常成熟的架构通常不是:

全 gRPC

也不是:

全 REST

而是:

                Internet
                   │
                   ↓
              API Gateway
                   │
             HTTP / JSON
                   │
        ┌──────────┴──────────┐
        ↓                     ↓
    User Service          Order Service
        │                     │
        └──────────gRPC────────┘

即:

外部 REST,内部 gRPC。

这往往比“为了技术先进而全站 gRPC”更加合理。


十二、最终思考:gRPC 真正改变的不是通信方式,而是服务设计方式

学习 gRPC 最开始,我们看到的是:

client.GetUser(ctx, req)

继续深入,会看到:

Proto
↓
Stub
↓
HTTP/2
↓
Streaming
↓
Deadline
↓
Context
↓
Status
↓
Retry
↓
Service Discovery
↓
Load Balancing
↓
Observability

再继续深入,就会发现 gRPC 本身其实只是一个入口。

它真正要求开发者思考的是:

一个远程服务应该如何被定义、调用、失败、恢复、扩展和治理?

这是 RPC 和普通 HTTP API 最大的区别之一。

如果只是学习:

怎么写 proto
怎么生成代码
怎么启动 server
怎么调用 client

只能算“会用 gRPC”。

如果开始思考:

为什么要 Deadline?
为什么需要 Context?
为什么不能无限重试?
为什么需要幂等?
为什么 Streaming 有价值?
为什么需要服务发现?
为什么接口必须考虑版本兼容?
为什么一个公共能力值得或不值得服务化?

才真正进入了微服务工程设计的层面。

最终可以把 gRPC 理解成这样:

                    gRPC
                      │
        ┌─────────────┼─────────────┐
        ↓             ↓             ↓
      契约           通信           治理
        │             │             │
     Protobuf       HTTP/2       Deadline
     强类型         Multiplex     Retry
     Codegen        Streaming     Status
        │             │             │
        └─────────────┼─────────────┘
                      ↓
                  微服务架构
                      │
        ┌─────────────┼─────────────┐
        ↓             ↓             ↓
      解耦           复用           扩展
        │             │             │
        └─────────────┼─────────────┘
                      ↓
                分布式系统治理

所以,gRPC 最核心的价值并不是“性能高”,而是把服务之间的通信从一组松散的 HTTP 请求,提升成了一套有明确契约、有类型约束、有生命周期、有错误语义、能够流式传输并且可以进行统一治理的远程服务调用体系。

而这也解释了为什么在 Go 微服务、AI Agent、模型服务、RAG 服务以及基础设施服务化的架构中,gRPC 依然具有很强的生命力。

真正成熟的使用方式,不是看到 gRPC 就使用 gRPC,而是先判断:

这个能力是否已经成为一个独立的服务边界?

如果答案是肯定的,那么 Proto 定义服务契约,gRPC 负责通信,Context 控制生命周期,Deadline 防止级联阻塞,Status 描述失败语义,Streaming 承载持续数据,Service Discovery 解决实例定位,Load Balancing 解决流量分配,再配合 Trace、Metrics、Logging、Retry、Circuit Breaker 等机制,最终才能形成一个真正可靠的微服务通信体系。

这才是学习 gRPC 最值得掌握的东西。

更多推荐