news 2026/10/21 16:26:10

内部服务用gRPC,对外接口用REST:微服务架构的协议分工与实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
内部服务用gRPC,对外接口用REST:微服务架构的协议分工与实践

写这篇东西的起因,是有个做架构评审的下午,我连着被问了三次同一个问题:gRPC 和 REST 到底选哪个?每次我都得从“这俩不在一个维度”开始讲,讲到 HTTP/2、Protobuf、网关、契约、浏览器生态,最后对方总会恍然大悟一样补一句:原来可以一个都不选,两个都用。这句话基本就是最近几年做微服务改造里,我最有感的一个结论。内部服务用 gRPC,对外接口用 REST,这不是和稀泥,而是两种协议各自站在了自己最舒服的位置上。今天把整套思考、实操路径和踩过的坑一次性写出来。

1. 两种协议的底层差异:这场争论的技术根源

很多人把 gRPC 和 REST 当成同一层的东西在比,其实不对。REST 是一种架构风格,gRPC 是一个基于 HTTP/2 的 RPC 框架。它俩真正产生冲突的地方是通信模型、数据编码和接口契约,这三个层面的差异决定了它们各自擅长什么。

1.1 HTTP/1.1 与 HTTP/2:请求模型隔了一代

REST 接口绝大多数跑在 HTTP/1.1 上,而 gRPC 强制依赖 HTTP/2。这个代差是后面所有性能差距的来源。

HTTP/1.1 的工作模式是请求-应答,同步阻塞。一个 TCP 连接同一时间只能处理一个请求,前一个响应没回来,后一个请求就得等着。为了解决并发,浏览器会同时开六个左右的 TCP 连接,也就是六车道,可每条车道还是只能跑一辆车。服务器端看到的景象就是:连接数量巨大,每个连接利用率很低,头部每次请求都要重新传输一遍。

HTTP/2 把这些机制全改了。它引入二进制分帧层,所有请求被切成一个个帧,在同一连接上交错传输。客户端和服务端之间维护一个流表,每个帧带上流 ID,接收方按 ID 重新组装成完整的请求和响应。这就是多路复用,一个连接可以同时传输成百上千个流。另外还有 HPACK 头部压缩:首次传输完整头部,后续只传输变化的部分,头部体积能压掉百分之七八十。

gRPC 把这种能力直接物化了。一个 gRPC 服务端开启一条 HTTP/2 连接,成千上万个 RPC 调用在这条连接上并行跑。REST 在 HTTP/1.1 下的并发要靠连接池无数条 TCP 连接去堆,gRPC 是让一条连接的能力被榨干。看似只是协议版本不同,实际解决的完全是两个时代的问题。你拿着 HTTP/1.1 的 REST 去对标 HTTP/2 上的 gRPC,本质上就输了半局。

提示:HTTP/2 的“队头阻塞”问题并没有完全消失,只是从 HTTP 层转移到了 TCP 层,但在普通内网场景下影响很小。这个后面在常见问题里再展开。

1.2 JSON 与 Protobuf:文本与二进制的编码博弈

REST 的数据格式你去定,但大家默认都是 JSON,因为可读、通用、随处可用。gRPC 的数据格式是 Protobuf,一种二进制序列化协议。

差距有多大,我拿一个真实接口的数据来算:

HTTP/1.1 下传一段 JSON:

{"order_id":123456,"product_id":"SKU20240718","quantity":2,"status":"PAID"}

这段文本大概是 60 多个字节。其中 product_id 这种键名每次都要完整出现在每一个消息里,没有任何例外。

同一个数据用 Protobuf 编码,字段不再用名字标识,改成编号。proto 文件里指定了 product_id 是 field 2,那么线上传输时只写一个字段编号 tag 加上值的长度和字节内容。整数用 varint 编码,小的正整数占 1 个字节。一轮算下来,同样的业务数据,Protobuf 大概只需要 20 个字节左右。规模小看不出什么,每天一亿次调用的时候,差的就不只是带宽,还有序列化解析消耗的 CPU。

JSON 的解析要处理字符串匹配、对象嵌套、类型判断,Protobuf 的解析是直接按编号和类型走分支,用生成的代码做内存拷贝和位移运算。服务端 qps 越高,这种差异越明显。我实测过一个纯转发接口,同样逻辑下 Protobuf 反序列化耗时只有 JSON 的四分之一多一点。注意这是纯 CPU 时间,还没算上网络传输减少带来的收益。

1.3 接口描述语言之争:契约从哪来

REST 接口也讲契约,OpenAPI(Swagger)就是干这个的。但实话实说,大部分团队的 OpenAPI 文档是接口写完之后再补的,甚至自动从代码生成。契约成了实现的结果,而不是起点。

gRPC 的契约在 .proto 文件里,是开发的起点。接口参数、返回值、字段编号、类型、注释全在这里,谁改谁负责。服务端和客户端通过 protoc 生成代码,双方拿到的类型定义一定一致,编译期就能发现契约被破坏。你改了字段类型,对方代码编不过,就不可能等到联调的时候才发现。

这个顺序差异在工程上意义很大。REST 的接口文档是描述性的、给人看的;gRPC 的 proto 是约束性的、给代码用的。前者适合对外开放,后者适合内部强约束,这本质上已经预示了标题里的分工结论。

2. 内部服务选 gRPC:不只是性能,更是工程约束

内部服务间通信,核心诉求是快、稳、不容易出错。gRPC 在这三个方向上都做得比 REST 到位,性能只是表面原因。

2.1 强契约逼着双方把接口说清楚

微服务内部最头疼的问题之一,叫“接口漂移”。服务 A 改了字段,服务 B 还在按老字段解析,线上坏了自己都不知道。REST 场景下这个问题很难根治,因为 JSON 是弱类型的,接口文档滞后是常态,一个字段从 string 改成 int,调用方未必能从文档里及时发现。

gRPC 把这个问题从“靠人自觉”变成了“靠机器约束”。proto 文件里每个字段有编号、有类型、有是否可选的语义,客户端代码是直接生成的。服务端升级了接口,字段类型变了,客户端重新生成代码然后编译,立刻报错。这种约束对于人多的团队价值重大,接口改动不再是两周联调一次,而是编译通过后直接联调。

另一个细节是字段的可选性和默认值。REST 接口里一个字段没传和传了 null 往往难以区分,服务端只能用各种约定去猜。Protobuf 3 里每个字段都有明确的 presence 语义,你可以判断字段是否存在,而不是只能拿到零值。这一点在更新接口、做兼容的时候特别重要。

注意:proto 的强约束是有代价的,它要求双方技术栈必须能生成代码。Java、Go、Python、Node 都能生成,没问题。但如果是那种“反正就临时调一次”、“今天写个脚本跑一下”的场景,gRPC 反而碍事。所以我说它是“内部服务专用”,不是“所有场景通用”。

2.2 双向流式调用让数据推送不再别扭

REST 做请求应答没问题,做流式就很尴尬。服务端要向客户端推送数据,要么 WebSocket,要么 SSE,要么客户端轮询。每种方案都有一套独立的语义和实现方式,服务端和客户端还要各自维护状态,长期下来代码里全是协议分支。

gRPC 原生支持四种调用模式:

模式说明典型场景
Unary RPC普通请求-应答订单创建、查询
Server Streaming客户端发一次,服务端持续返回多个消息订阅通知、日志流
Client Streaming客户端持续发送,服务端返回一次批量上报、文件分片上传
Bidirectional Streaming双方同时持续收发实时协作、转译

内部服务里最常见的需求是 Server Streaming。比如用户登录后要同步一系列初始化数据,REST 只能返回一个大 JSON 包,gRPC 可以一个消息一个消息地推,客户端收到一个就能处理一个。又比如积分变动、消息通知这类场景,客户端维持一个流,服务端随时往里写,比轮询优雅太多。

这个能力是 HTTP/2 的双向帧 + 流式语义直接提供的。你让 REST 实现同样的效果,等于把 HTTP/1.1 钉死在一个已经不属于它的时代里,然后用各种 WebSocket 补丁去填坑。填到最后,你其实是在重新发明 gRPC。

2.3 长连接与多路复用带来吞吐红利

内部服务间调用频率高、接口多、单次调用数据量小,这种情况下 TCP 连接建立和销毁的开销被放大得非常明显。REST 即便用了连接池,也很难做到所有请求都在同一批连接上跑完。因为 HTTP/1.1 的连接是串行复用的,如果一个接口响应慢,就会堵住后续请求。

gRPC 没了这个讲究。服务启动之后建立少量基于 HTTP/2 的长连接,所有 RPC 调用多路复用在这几条连接上。数据帧按流 ID 交错传输,慢接口不会阻塞快接口。连接数量少了,服务端的文件描述符占用低,内存占用低,整体吞吐量自然上去。

我用某公司自研的监控平台做过对比实验:两组完全相同的服务,一组 REST + JSON,一组 gRPC + Protobuf,处理同样的查询请求,gRPC 在 p99 延迟上低了差不多 30% 到 40%。链路短的接口差距没那么大,链路长、经过多个服务转发的时候就非常明显,因为每一跳都省一点序列化和传输时间,累加起来就是可感知的延迟差。

实操心得:gRPC 的长连接是把双刃剑。它省了连接建立开销,但也要求你对连接生命周期有掌控。TCP 连接在空闲时间过长时可能被中间设备回收,表现为“连接明明在,但请求发出没响应”。这个问题必须在客户端配置 keepalive 来规避,后面章节会给出具体参数。

3. 对外接口保留 REST:有些代价不值得付

如果说内部选 gRPC 是趋利,那对外保留 REST 就是避害。外部接口面临的约束和内部完全不同,有些 gRPC 的优势在外面反而成了负债。

3.1 浏览器和移动端生态绕不开

浏览器的 JavaScript 环境原生支持 HTTP/1.1 和 HTTP/2,但拿不到 HTTP/2 的裸帧,也没有办法直接发起 gRPC 调用,除非走 gRPC-Web 协议做转换,或者在前端和后端之间架代理。问题是,几乎不会有第三方的产品愿意为了对接你的接口而引入这套转换链路。

开放出去的 API,调用方可能是另一个公司的后端服务,可能是某个开源的库,可能是一个只懂 curl 的运维。你用 gRPC,等于给所有潜在调用方设了一道门槛:你先装 protoc,再生成代码,再来调我的接口。这套流程放在内部没问题,放在外部就是劝退。

REST + JSON 的优势是“零成本”:任何语言、任何工具、任何平台都能直接发起 HTTP 请求并解析 JSON。curl 可以,Postman 可以,浏览器地址栏甚至可以直接 GET。开放生态的兼容性,本质上就是业务接入的速度,这不是某个技术指标能衡量的。

3.2 对内那些优势,对外反而成了成本

gRPC 的强契约到了外部变尴尬了。外部团队不会跟着你的 proto 文件升级节奏走,他们可能锁定了某个旧版本,你改字段就得考虑他们什么时候发布。Protobuf 的二进制编码对调用方不可读,出了问题他们连抓包看报文的机会都没有。你没法用一句“看下 message 里的字段值”来排查问题,因为报文是二进制的。

REST 的弱契约反而成了缓冲区。接口文档标注了字段含义和取值范围,调用方自己适配,出了问题一眼就能看到错误信息。JSON 虽然大,但它是透明的,是人和机器都能直接理解的交互格式。

对外接口还需要考虑超时、重试、幂等、限流这类治理能力。REST 的 HTTP 状态码和标准语义在这方面有成熟约定,网关、负载均衡、缓存层全部都基于这些约定做适配。你让外部调用方实现一套 gRPC 的重试和超时控制,他们的研发也得先学习一套新概念,沟通成本直接翻倍。

3.3 缓存、观测、版本化:REST 的隐藏护城河

REST 有一项能力常被忽视:HTTP 缓存。GET 请求天然支持 Cache-Control、ETag、Last-Modified 这些头,CDN、反向代理、浏览器都可以直接利用。对于读多写少的公开接口,这几乎是零成本提升性能的手段。

gRPC 的缓存策略就麻烦得多,因为它的请求和响应是二进制的,没有标准的 URL 可以按路径做缓存,没有自带的 ETag 语义。你想自己做,就要在 proto 里定义元数据字段,然后自己实现缓存逻辑,完全是从零开始。

可观测性也是一样。REST 和现有的日志、trace、监控体系无缝对接,因为报文即文本,问题定位直观。gRPC 虽然也有 metadata 和 trace 支撑,但排障工具链的普及度远不如 HTTP。外部出问题时你会想念“直接从 access log 里看到 URL 和状态码”的那种简单。

版本化上,REST 的 /v1/、/v2/ 前缀直观明确,客户端直接改 URL 就行。gRPC 的包名和版本管理要靠 proto package 维护,虽然现在也有了 AIP-versioning 的实践,但引入成本远高于 REST。

4. 落地实操:网关层协议转换的完整链路

说清楚两边各自的优势,接下来的问题就是:怎么让这两种协议在同一个架构里共存,并且转换层不成为瓶颈。

4.1 整体架构:REST 进,gRPC 转,内部走

我当时在模拟项目 X 里落地过一套方案,整体链路是这样的:

外部调用方 │ ▼ API 网关(REST/HTTP 入口) │ ── 身份校验、限流、参数校验、版本路由 ▼ 协议转换层(REST JSON → gRPC 调用) │ ── 内部 stub 调用 ▼ 内部服务(gRPC Server) │ ├── 服务 A(订单) ├── 服务 B(库存) └── 服务 C(用户)

外部所有流量都从 HTTP 网关进入,网关反向代理到内部的协议转换层,转换层把 JSON 请求翻译成 gRPC 调用,再把 gRPC 响应翻译成 JSON 返回。内部服务之间只走 gRPC,不走 REST。

这套做法的核心价值是职责分离:协议转换层是唯一需要同时理解 REST 和 gRPC 的地方,内部服务不需要关心 JSON 和 HTTP 状态码,他们只管业务处理。网关层负责安全和治理,转换层负责协议翻译,避免逻辑互相污染。

4.2 proto 文件先行:把服务契约定死

内部服务的接口定义以 proto 文件为准,先写契约再写代码。这里是我当时用的一个订单服务定义,完整覆盖了 unary 和 server streaming 两种模式:

syntax = "proto3"; package order.v1; option go_package = "example.com/order/v1;orderv1"; service OrderService { rpc CreateOrder(CreateOrderRequest) returns (CreateOrderResponse); rpc GetOrder(GetOrderRequest) returns (GetOrderResponse); rpc SubscribeOrder(SubscribeOrderRequest) returns (stream OrderEvent); } message CreateOrderRequest { string user_id = 1; string product_id = 2; int32 quantity = 3; int64 total_price_cents = 4; } message CreateOrderResponse { string order_id = 1; string status = 2; int64 created_at = 3; } message GetOrderRequest { string order_id = 1; } message GetOrderResponse { string order_id = 1; string user_id = 2; string product_id = 3; int32 quantity = 4; int64 total_price_cents = 5; string status = 6; } message OrderEvent { string order_id = 1; string event_type = 2; // CREATED / PAID / SHIPPED int64 occurred_at = 3; } message SubscribeOrderRequest { string order_id = 1; }

字段编号是协议的一部分。一旦线上在跑,编号不能被修改,这是兼容性的最低底线。基于这个文件生成代码:

protoc --proto_path=. \ --go_out=paths=source_relative:. \ --go-grpc_out=paths=source_relative:. \ order/v1/order.proto

条件允许的话,直接用 buf 做 lint 和生成管理,比裸 protoc 命令可维护得多:

buf lint buf generate

buf lint 会默认检查字段编号顺序、包命名、服务注释这些规范,把 IDE 之外的契约质量保证也纳入到流程里。

4.3 网关转换的关键实现与错误映射

协议转换层通常是一个轻量服务,我推荐用 Go 写,因为它和 gRPC 生态配合最自然。该服务的核心逻辑就是两个函数:把 HTTP JSON 请求转成 gRPC request,把 gRPC response 转成 HTTP JSON 响应。代码骨架形如:

func handleCreateOrder(w http.ResponseWriter, r *http.Request) { var in createOrderInput if err := json.NewDecoder(r.Body).Decode(&in); err != nil { writeError(w, http.StatusBadRequest, "invalid_json") return } resp, err := orderClient.CreateOrder(r.Context(), &orderv1.CreateOrderRequest{ UserId: in.UserId, ProductId: in.ProductId, Quantity: in.Quantity, TotalPriceCents: priceToCents(in.TotalPrice), }) if err != nil { writeGRPCError(w, err) return } writeJSON(w, http.StatusCreated, map[string]string{ "order_id": resp.OrderId, "status": resp.Status, }) }

这里有一个关键函数是 writeGRPCError。gRPC 的错误是一个 status 对象,有 code 和 message,要转成 REST 客户端能理解的 HTTP 状态码。如果不做映射,网关层会把所有 gRPC 错误都一股脑返回成 500,前端和调用方完全没法区分参数错误和服务器错误。

我整理了一份实际在用的映射表,覆盖了绝大多数情况:

gRPC Status Code数值对应 HTTP 状态码
OK0200
Cancelled1499
InvalidArgument3400
DeadlineExceeded4504
NotFound5404
AlreadyExists6409
PermissionDenied7403
ResourceExhausted8429
FailedPrecondition9400
Aborted10409
OutOfRange11400
Unimplemented12501
Internal13500
Unavailable14503
Unauthenticated16401

写 v1 的时候直接把上面映射表搬进代码里,别自己发挥。你非要把 InvalidArgument 映射成 500,那后续排查的锅全是你自己的。

网关层的超时必须单独设置。内部 gRPC 服务自身的超时设为 2 秒,网关给外部调用方的承诺是 5 秒。也就是说,网关分配给 gRPC 调用的 context 超时要小于 HTTP 请求的总超时,让下游超时先触发,返回超时错误,网关再转成 504 给客户端。反过来设计就会出问题:调用方等 5 秒超时,但内部 2 秒就失败返回了,客户端拿到的是 200 而不是 504,这在自建网关时特别常见。

4.4 验证工具链:没有 grpcurl 寸步难行

内部服务切换成 gRPC 之后,排障和验证不能再用 curl 了。我强烈建议把下面几个工具写进团队的开发者工具文档里。

grpcurl 用来直接调用 gRPC 服务,等价于 curl 之于 HTTP:

# 查看服务有哪些方法 grpcurl -plaintext localhost:9090 list # 调用一个 unary 方法 grpcurl -plaintext \ -d '{"user_id":"u_10086","product_id":"SKU20240718","quantity":2}' \ localhost:9090 order.v1.OrderService/CreateOrder

grpcui 用来在浏览器里调试 gRPC 接口,界面类似 Postman,适合初次接触的人快速上手:

grpcui -plaintext localhost:9090

它内部通过 grpcurl 反射服务拿到 proto 定义,自动生成表单。省去了手工构造 JSON 结构再转成二进制 payload 的麻烦。多服务端共享一个 proto 文件也能直接从代码仓库读取。

服务端如果没开反射,grpcurl 和 grpcui 都会失灵。反射开关在 Go 里是这样的:

import "google.golang.org/grpc/reflection" reflection.Register(grpcServer)

生产环境建议单独控制这个开关,但开发环境开着确实能省很多事。

5. 常见问题与排查技巧实录

现在进入真正的硬核部分。方案看着对,代码也写了,但真实跑起来一定会踩到边角料问题。我把在实际运维里遇到的典型故障、排查思路和修复手段整理了五个出来。

5.1 连接被静默切断的隐形坑

现象是服务跑着跑着,接口偶发超时,重试一次又好了。查监控发现 gRPC 连接池里的连接被服务端“晾”在一边,发出去的数据包没有响应。

原因是链路中某个 L4 负载均衡或防火墙设备,把空闲超过一定时间的 TCP 连接给回收了。HTTP/1.1 和 REST 因为连接使用率高,很少踩到这个问题。gRPC 的长连接如果业务流量本来就不大,空闲几分钟就会触发中间设备的空闲超时回收。

解决办法是给 gRPC 客户端配置 keepalive。Go 里的标准配置:

grpc.WithKeepaliveParams(keepalive.ClientParameters{ Time: 30 * time.Second, Timeout: 10 * time.Second, PermitWithoutStream: true, })

Time 表示每 30 秒发一次心跳,Timeout 表示 10 秒内没收到响应就判定连接不可用,PermitWithoutStream 表示即使当前没有任何活跃 RPC 也要发心跳。关键是 PermitWithoutStream 必须开,否则空闲连接照样被回收。服务端也可以配套设置 keepalive 策略,拒绝过短的空闲周期探测,两者配合能显著减少“半开连接”故障。

这种问题排查起来很麻烦,因为看起来像是业务代码的问题,实际是网络拓扑的锅。我当时的排查顺序是:先看客户端日志有没有连接重置,有就抓 tcpdump 看 TCP 层连接是被谁断的,再查中间的负载均衡会话保持时间。确认是空闲回收之后,改 keepalive 参数,问题当天消失。

5.2 负载均衡失效:长连接带来的新问题

HTTP/1.1 的后端负载均衡,基本都是连接级的。每个新请求建立新连接,或者从连接池里选连接,四层负载均衡按源 IP 和端口做哈希,流量基本能均匀分布到各个后端实例。gRPC 一用长连接,连接数量锐减,哈希算法的作用域从“请求”变成“连接”,单个实例的流量就可能严重倾斜。

更麻烦的是,如果连接一直不断开,某台实例挂了,负载均衡设备还没探测出来,请求就会持续打到坏实例上,直接超时。

成熟的解法是客户端负载均衡。服务端注册到注册中心,客户端感知到所有实例列表,自己按负载均衡算法选择实例建立连接。常见方案包括 gRPC 自带的 round_robin 策略、xDS 控制面、以及 Kubernetes 的 headless service 配合 weighted round robin。核心思路是把负载均衡能力从网络设备移植到客户端,让连接和实例列表的关联更紧密。

实操心得:如果你在 Kubernetes 里跑 gRPC 服务,别再依赖 ClusterIP 的 kube-proxy 转发。默认的 ClusterIP 模式对长连接流量的负载均衡基本无效,换 headless service 再在 gRPC 客户端里配置 dns 解析器,让客户端直连 Pod IP,效果立竿见影。

5.3 消息大小与超时引发的故障

gRPC 默认单个消息大小限制是 4MB。内部服务如果传输大对象,比如包含图片 base64 的字段,很容易触顶报错:

rpc error: code = ResourceExhausted desc = grpc: received message larger than max (4194304 vs. 4189102)

服务端需要显式调大限制:

grpc.MaxRecvMsgSize(16 * 1024 * 1024), // 16MB grpc.MaxSendMsgSize(16 * 1024 * 1024),

客户端也要同步设置,因为限制是双向的:

grpc.WithDefaultCallOptions( grpc.MaxCallRecvMsgSize(16 * 1024 * 1024), grpc.MaxCallSendMsgSize(16 * 1024 * 1024), )

这里要提醒一句:把限制调大只是止血,别把 gRPC 当成大文件传输通道。超过几十 MB 的 payload 设计上就有问题,应该走对象存储,gRPC message 里只传存储路径。

超时上还有个经典误区:只在客户端配置了超时,服务端没有。如果服务端下游依赖变慢,超过客户端超时时间,客户端会收到 DeadlineExceeded,但服务端还在继续处理,白白消耗资源。服务端要通过 server.WithUnaryInterceptor 做统一的超时传输,一旦超过就通过 status 返回错误给客户端。

5.4 Protobuf 演进路上的兼容性红线

Proto 的兼容性规则网上说很多,我这里只讲三条实际踩过的红线。

第一,字段类型绝对不能变。一个字段以前是 string,后来因为业务需要改成 int64,你以为用 json 正则表达式那种“反正能解析”的思路去迁移,实际线上客户端全部报错。Protobuf 的 wire type 变了,老的二进制解析代码直接失败。遇到这种情况,只能新增一个字段,弃用老字段。

第二,字段编号不能复用。删除一个字段后,这个编号就永久作废了。如果新业务不小心复用了旧编号,老客户端解析新数据时会用旧的类型去理解新字段,轻则数据错,重则 panic。正确做法是 reserve 掉旧编号:

message Order { reserved 2, 5; reserved "old_field"; }

第三,默认值坑。Protobuf 3 里没有 optional 时,字段不传和传零值是等价的。老接口里 quantity 默认是 0,新调用方改了逻辑,必须显式传 1,数字不直观的话排查起来极容易混淆。所以新代码里我建议给关键字段加上 optional 修饰符,用 presence 语义把“用户没传”和“用户传了 0”区分开。

5.5 服务端流式调用的背压问题

服务端流式接口如果在服务端持续生成大量消息,而客户端处理速度跟不上,消息就会积压在各自的缓冲区里,内存飙升,最后 OOM。

背压问题的核心认知是:gRPC 的流式不等于无限缓冲。客户端必须主动消费,服务端要关注发送方法的返回值,Send() 返回错误时立刻停止发送。我不止一次看到同事写服务端流式代码时忽略 Send() 的返回值,流断了还在继续循环发送,消息全部丢进错误日志。

实际调试时,可以用 grpcurl 订阅流式接口,控制台会不断输出新消息,直观感受消息到达的频率。如果看到缓冲积压,优先在服务端做限速或批量推送,而不是指望客户端自己消化。

6. 写在最后的个人体会

这套“内部 gRPC、对外 REST”的方案,我前后在不同项目里用了三年多,总体的体会是:它不是一个技术选型的结论,而是一个工程现实的答案。它承认了 gRPC 的强大,也承认了 REST 的价值,让每种协议在自己最合适的位置发光。

切换初期,团队内部最大的反对声音是“要维护两套接口,成本翻倍”。实际跑起来之后发现成本并没有翻倍,因为协议转换层的逻辑非常薄,核心业务逻辑全部在内部服务里,只有网关一层需要同时理解两种协议。维护一份 proto 文件和一份 OpenAPI 文档确实比只维护一份要多,但这部分成本换来了内部开发的强约束和外部接入的零门槛,长期看是划算的。

最后分享一个小技巧:在这个架构里,千万不要让内部服务直接暴露 HTTP 接口给外部。哪怕只是临时的“就开一个测试接口”,也走网关。没有网关这一层的统一入口,你会慢慢发现有的服务偷偷开了 REST 口,有的直接暴露了 gRPC,安全规则和治理策略全部失效。保持架构边界的干净,比任何单个协议选型都重要。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/21 16:25:40

从词袋到词向量:TextCNN与BiLSTM在新闻文本分类中的实战

1. 从词袋到词向量:为什么深度学习模型需要更“懂语义”的输入做新闻文本分类做到第五篇,很多人会卡在同一个地方:前面用TF-IDF加线性模型已经能跑到一个不错的分数,但再往上提就非常吃力了。我当初也是这个状态,线下验…

作者头像 李华
网站建设 2026/10/20 9:57:16

医院挂号就诊系统全栈实战:SpringBoot+Vue+MySQL毕设指南

1. 这个选题为什么值得做:先搞清楚它解决什么问题很多人在挑毕业设计题目的时候,第一反应是往“高大上”的方向冲——图像识别、推荐算法、语音交互。结果呢,算法原理讲不明白,工程实现做不出来,最后只能到处找现成代码…

作者头像 李华
网站建设 2026/10/19 1:19:17

基于SpringAI的在线考试主观题智能阅卷系统设计实践

做在线考试系统,最容易翻车的不是题库也不是组卷,而是阅卷。尤其主观题,一张卷子里有几道论述题和简答题,按传统方式让老师逐份批改,量大、耗时,而且两个老师打出的分可能差出一档。去年我在负责某个高校的…

作者头像 李华
网站建设 2026/10/18 23:31:23

模板代码生成工具:从元数据到可用接口的完整实现

1. 为什么需要模板代码生成工具:一大半重复代码其实不该手写做后端开发的朋友应该都有过这种体验:新项目启动,建表、写实体、写Mapper、写Service接口和实现、写Controller,然后前端再把列表页、表单页、弹窗、状态标签挨个撸一遍…

作者头像 李华
网站建设 2026/10/21 13:48:57

贪心算法实战:从陶陶摘苹果升级版掌握最优选择策略

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/18 10:19:52

HDU6759 Leading Robots解题精要:上凸包与物理模型的工程实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华