我真的是太爱学习了——这种念头冒出来的时候通常不是因为刚学会了一个妙招而是因为刚被一个不算难的框架折腾到凌晨一点。最近这个对象就是 gRPC。很长一段时间里我提到 gRPC 都是能躲就躲要写 .proto、要装 protoc、跨语言还得处理各种插件版本光是 Windows 上编译 C 库这件事就能劝退一大半人。直到我给自己立了个 flag说不管多难都把它整理成一套能回看的笔记结果一进去就出不来。这篇就把我从零开始学 gRPC 的过程、踩过的坑以及在 Go、Spring Boot、Hyperf、Python 四个栈里实测的体会全写出来给正在学或者正准备学 gRPC 的同学当个参照。1. 为什么 gRPC 值得花时间啃而不是继续用 REST很多人第一反应是我现在 REST 用得好好的HTTP JSON Swagger 也够用为什么非要学个 gRPC这问题我一开始也问过自己后来学完才发现不是能不能调的问题而是大规模多语言团队里调得爽不爽的问题。1.1 先说结论gRPC 解决的是技术债和协作成本gRPC 不是一个简单的更快的 HTTP它是基于 HTTP/2 的远程过程调用框架配合 Protocol Buffers通常叫 protobuf作为默认序列化协议。核心思路是用一个.proto文件把接口当成契约定死消息结构、字段编号、服务方法全在里面然后通过代码生成器自动生成各个语言的客户端和服务端骨架代码。没有契约之前REST 接口怎么写的后端说我返回一个用户对象前端去翻文档可能字段名是userId另一个版本又变成user_id改来改去两边联调成本全花在扯皮上。有了.proto数据是一行行定死的字段几号、什么类型、是必填还是可选生成出来的代码不会说谎接口行为在编译期就能看出问题。对我来说最有价值的一点是所有参与方用的是同一份协议文件。Go 团队、Java 团队、Python 团队各看各的生成代码谁也不会再我以为你返回的是字符串。1.2 和 REST 对比三个让我真香的点第一是性能。protobuf 是二进制序列化体积比 JSON 小得多。我之前压测过一个几百 KB 的 JSON 列表换成 protobuf 之后请求体只有原来的五分之一左右HTTP/2 多路复用还能在同一个连接上并发多个请求避免了 HTTP/1.1 的队头阻塞。第二是流式支持。REST 做流式很难受要么 WebSocket 要么 SSE 自己造轮子。gRPC 天生支持四种调用形态普通一元调用Unary、服务端流、客户端流、双向流。做实时推送、聊天、音视频帧处理这类场景就不用再拼第二套通信协议了。第三是代码生成。以前写 REST客户端要自己封装 HTTP 请求、解析 JSON、拼参数gRPC 里全是生成好的强类型方法比如 Java 那边直接拿到GreeterGrpc.GreeterBlockingStub调sayHello(request)就是一次 RPC。IDE 自动补全、编译期检查、错误处理全都在省掉大量模板代码。1.3 一个最小化判断清单什么项目适合上 gRPC也不是说所有项目都该换成 gRPC。我给自己划了个标准对内微服务之间调用或者跨语言协作、流量密度比较高、需要流式通信的场景直接上。对外浏览器直接访问或者接口简单到只有两个 CRUD那继续 REST 完全没问题。gRPC 不是银弹它最大的成本是引入了一套新的 IDL 和工具链团队要愿意接受这套工作流后面才会越用越舒服。2. Windows Visual Studio 编译 gRPC比项目逻辑更折磨人的一关我学 gRPC 计划里本来没有编译 gRPC 源码这一项但后来在 C 项目里需要用启动代码生成插件加上我又想在本地写点 C 例子就一头扎进了 Windows 编译。这一关能刷掉的人比 gRPC 语法本身多得多我把自己的处理过程完整写出来。2.1 我为什么非要自己编译直观原因是想跑通一个完整 C 环境。但更现实的原因是gRPC C 生态在 Windows 上没有太多官方预编译产物很多第三方包也经常跟不上版本。当时我用的示例代码是基于 v1.62 的 API包里内置版本如果是旧的接口都对不上。最后只能从源码编译或者用包管理器帮你编译。2.2 vcpkg vs CMake两条路径的利弊在 Windows 上最省心的是 vcpkg。安装完之后直接vcpkg install grpc:x64-windows它会帮你把 protobuf、abseil、c-ares、re2、zlib 等一大串依赖全部编译出来。好处是省心坏处是第一次编译时间非常夸张——我当时笔记本风扇狂转了一下午全程看着 CMake 刷屏。另一个缺点是大量依赖被编进静态库最后自己的项目要集成时还得处理链接器参数。想手动控制版本和编译选项就克隆官方仓库跑 CMakegit clone --recurse-submodules -b v1.62.0 https://github.com/grpc/grpc cd grpc cmake -B build -DCMAKE_BUILD_TYPERelease -DCMAKE_INSTALL_PREFIXbuild/install cmake --build build --target install这条路的问题在于子模块太多速度慢而且 abseil 和 gRPC 的 CMake 配置一旦版本不匹配你会看到一堆莫名其妙的编译错误。我第二次编译时改用 vcpkg直接省下大半时间。2.3 编译现场踩过的坑和解决记录下面这几个问题我怀疑只要在 Windows 上编译过 gRPC 的人十有八九都遇过现象原因处理方案编译过程中文件路径过长报错Windows 默认 260 字符路径限制Git 里执行git config --global core.longpaths true并开启系统长路径支持同时把源码放到C:\grpc这种短目录找不到 Visual Studio 或 Windows SDK没有安装 C 桌面开发组件打开 Visual Studio Installer勾选使用 C 的桌面开发确认 Windows SDK 版本正确链接时 abseil 或 re2 一堆 unresolved symbol编译出来的静态库和自己项目的运行时库不一致保持 vcpkg triplet 和项目配置一致比如统一用动态 CRT或用x64-windows-static配-DMSVC_USE_STATIC_RUNTIMEONprotoc 版本太旧系统 PATH 里有老版本 protoc显式指定编译产物中的 protoc 路径或用vcpkg提供的版本还有一个隐藏坑如果用 vcpkg 安装的 gRPC在 Visual Studio 里要把VcpkgEnableManifest开起来否则 IDE 找不到头文件。最省事的命令是vcpkg integrate install之后 Visual Studio 会自动识别 include 路径和连接配置。2.4 不想自己编译的备选方案如果只是想写业务代码完全没必要死磕源码编译。Go 里直接通过 go mod 拉官方库Java 和 .NET 都有 NuGet 或 Maven 包Python 直接 pip 装grpcio这些官方 wheel 都是编译好的。只有 C 这块生态比较原始一时半会儿绕不开。我的最终建议是先跑通一门主流语言理解了 RPC 链路之后再回头处理 C 构建问题心情会好很多排错也会更快。3. Golang Hello World把 protobuf 到请求链路一次跑通Go 是被公认为最适合入门 gRPC 的语言官方示例多、插件齐、错误提示清楚。我建议所有人的第一步都从 Go 开始因为它的链路短写完 proto生成代码起一个 server再写一个 client大概半小时就能看到效果。3.1 环境准备与 proto 文件先装一个protoc再去 Go 里装两个代码生成插件go install google.golang.org/protobuf/cmd/protoc-gen-golatest go install google.golang.org/grpc/cmd/protoc-gen-go-grpclatest常见的问题是安装完之后 PATH 里找不到protoc-gen-go。Go 默认会把GOBIN放到~/go/bin确保它也加进系统 PATH。然后写一个最简单的hello.protosyntax proto3; package helloworld; option go_package grpc-hello/helloworld; service Greeter { rpc SayHello (HelloRequest) returns (HelloReply); } message HelloRequest { string name 1; } message HelloReply { string message 1; }这里go_package千万别省。早期我写过不带这个包的 proto生成的 Go 文件里 import 路径乱得一塌糊涂编译起来全是 cannot find module。3.2 生成代码与 server/client 实现在项目根目录执行protoc --go_out. --go-grpc_out. --go_optpathssource_relative \ --go-grpc_optpathssource_relative hello.proto会生成hello.pb.go和hello_grpc.pb.go。服务端实现起来很直接package main import ( context log net google.golang.org/grpc pb grpc-hello/helloworld ) type server struct { pb.UnimplementedGreeterServer } func (s *server) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloReply, error) { return pb.HelloReply{Message: Hello req.GetName()}, nil } func main() { lis, _ : net.Listen(tcp, :50051) s : grpc.NewServer() pb.RegisterGreeterServer(s, server{}) log.Fatal(s.Serve(lis)) }客户端则要注意连接凭据。新版 gRPC 里grpc.WithInsecure()已经被删了要用conn, _ : grpc.NewClient(127.0.0.1:50051, grpc.WithTransportCredentials(insecure.NewCredentials())) defer conn.Close() client : pb.NewGreeterClient(conn) resp, err : client.SayHello(context.Background(), pb.HelloRequest{Name: Aoi})这里我特意用了grpc.NewClient因为从 grpc-go v1.63 开始官方推荐它替代grpc.Dial。版本差异很容易把上网抄的旧代码直接报红。3.3 第一个坑版本不一致带来的静默失败我实际踩过的一个坑是protoc-gen-go和protoc-gen-go-grpc版本都很新但项目里grpc库是旧的结果生成的接口和服务端怎么都对不上。还有一次是 proto 文件更新了我忘记重新生成代码客户端调了半天还是老接口最后才发现少了--go-grpc_out这条命令。这类问题没有统一报错排查思路只能是先看.pb.go文件头部的protoc-gen-go v版本号再看 go.mod 里 grpc 版本是否和生成代码要求的版本相匹配改了 proto 之后养成生成代码和编译同时完成的肌肉记忆。3.4 Go 生态的 gRPC 体验小结在 Go 里gRPC 基本是亲儿子待遇。社区维护活跃官方示例多拦截器、负载均衡、健康检查、反射协议都有现成实现。对于刚接触 gRPC 的同学Go 的代码量最少概念最直白从proto到server到client没有中间商调试起来最舒服。4. Spring Boot 与 gRPCREST 老相好也能优雅共存Java 技术栈里学 gRPC 最大的疑问是我的项目已经是 Spring Boot 了怎么办是要推倒重来还是能按新接口加进去答案是可以共存而且不用改老代码。4.1 端口不是问题问题是怎么接入Spring Boot 的默认 Web 服务住 8080gRPC 服务完全可以在另一个端口上运行。你不需要让老接口下线只要新接口走 gRPC 端口就行。我常用的方式是gRPC 服务跑在9090REST 控制器继续占用8080两边各聊各的业务层共享同一些 Service 类。Java 这边生态里用的最多的是net.devh的 starter不是官方出品但事实标准文档齐全。版本上注意和 Spring Boot 对齐Spring Boot 2 用 1.xSpring Boot 3 用 2.xJDK 版本也别太旧。4.2 Maven 配置与 proto 生成先在src/main/proto里放一个和之前一样的hello.proto然后在pom.xml加上生成插件plugin groupIdorg.xolstice.maven.plugins/groupId artifactIdprotobuf-maven-plugin/artifactId version0.6.1/version configuration protocArtifactcom.google.protobuf:protoc:3.25.3:exe:${os.detected.classifier}/protocArtifact pluginIdgrpc-java/pluginId pluginArtifactio.grpc:protoc-gen-grpc-java:1.62.2:exe:${os.detected.classifier}/pluginArtifact /configuration executions execution goals goalcompile/goal goalcompile-custom/goal /goals /execution /executions /plugin同时把依赖加上dependency groupIdnet.devh/groupId artifactIdgrpc-server-spring-boot-starter/artifactId version2.15.0.RELEASE/version /dependency生成的代码会出现在target/generated-sources/protobuf/IDE 里如果没显示把它目录加为 source root 就行。4.3 GrpcService 与 GrpcClient 的实际用法服务端实现类要继承生成代码里的GreeterGrpc.GreeterImplBase再标注GrpcServiceGrpcService public class GreeterService extends GreeterGrpc.GreeterImplBase { Override public void sayHello(HelloRequest req, StreamObserverHelloReply responseObserver) { HelloReply reply HelloReply.newBuilder() .setMessage(Hello req.getName()) .build(); responseObserver.onNext(reply); responseObserver.onCompleted(); } }这里有个细节gRPC 服务端方法是单向流接口方法里一定要手动调用onNext和onCompleted漏掉一个客户端就会一直等到 deadline这是新手最容易踩的坑。客户端就简单了在application.yml里配置grpc: client: greeter: address: static://127.0.0.1:9090然后在服务里注入Service public class HelloClientService { GrpcClient(greeter) private GreeterGrpc.GreeterBlockingStub greeterStub; public String sayHello(String name) { HelloRequest request HelloRequest.newBuilder().setName(name).build(); return greeterStub.sayHello(request).getMessage(); } }GrpcClient(greeter)里的名字要跟 yml 里的 client key 对上。另外同一个 Spring 容器里最好只注册一个同名 client stub不然启动时可能因为多个 Bean 而报错。4.4 跨语言调用 Java 服务的一个实测场景我实际验证过一条链路Go 写的 client 调用 Spring Boot 写的 gRPC server再让 server 里的方法去调一个 Redis 缓存。整条链路里 proto 文件是同一个Go 客户端生成代码后直接用Java 服务端用 starter 接住请求处理完再返回。整个过程没有 JSON 解析、没有手写 HTTP 客户端联调体验比 REST 舒服得多。唯一要注意的是超时控制Java 这边拦截器里统一设了 3 秒 deadline避免上游慢接口拖垮下游连接池。5. Hyperf 里的 gRPCPHP 项目的微服务新姿势看到Hyperf gRPC这个热搜词的时候我挺感慨的PHP 在微服务领域经常被低估但基于 Swoole 的 Hyperf 确实把 gRPC 这件事做得很顺。如果你在 PHP 项目里想接入 gRPCHyperf 是一个非常现实的路线。5.1 为什么 PHP 项目也需要 gRPC很多团队的技术栈是 PHP 业务 Go/Java 服务并存。业务接口在 PHP 里写得很嗨但想调用 Go 那边的推荐系统没有现成 REST 接口只能自己拼 HTTP 包或者有内部告警平台、数据分析平台用的是 protobuf 协议PHP 这边接不进去就很被动。Hyperf 基于 Swoole 常驻内存和协程能跑 HTTP/2所以 gRPC 的跑麦在 PHP 里是成立的。5.2 以 Hyperf 3.x 为例的接入流程先装组件composer require hyperf/grpc-server hyperf/grpc-client同样写一份hello.proto然后用 Hyperf 提供的代码生成工具或者在官方文档指引下运行protoc插件生成对应的 PHP 消息类和服务接口一般会落到app/Grpc目录。接着在config/autoload/server.php里加一个 HTTP/2 监听端口用50051底层 socket 类型要支持 HTTP/2并且开启对应的open_http2_protocol设置。然后实现服务#[GrpcService] class GreeterService implements GreeterInterface { public function SayHello(HelloRequest $request): HelloReply { return (new HelloReply())-setMessage(Hello . $request-getName()); } }注解方式注册服务系统会扫描并注册路由不需要手写路由表。客户端调用时用 Hyperf 的GrpcClient在协程环境下调用是异步友好的不会像传统 PHP-FPM 那样阻塞整个进程。这里版本不同注解和基类名称会有差异安装后照官方文档对应版本的示例来写就行。5.3 coroutine 下的注意事项Hyperf 的协程模型决定了你在服务方法里不能做太长时间的死循环或同步阻塞操作比如直接 file_get_contents 一个慢接口会拖垮整个 Worker 进程。gRPC 方法内部如果还有外部依赖调用最好走 Hyperf 的协程客户端Redis、MySQL、HTTP 都有协程实现保持事件循环不被卡住。还有一点如果客户端用的是长连接连接池设置要注意数量。gRPC 连接本身是 HTTP/2 多路复用一个连接就能扛很多并发请求不用开太多连接开多了反而浪费内存。PHP 项目接入 gRPC 之后最大的收益不是变快了多少而是从别人给什么接口我就调什么变成我也可以提供一份标准的 protobuf 服务给别人调。6. Python gRPC 并发问题的复盘从堆线程到线程安全Python 是热搜里的 python grpc 并发问题我能猜到大家碰到的是哪类问题明明 gRPC 说的是高性能自己在 Python 里起个服务几十个请求进来直接卡住或者超时。我刚开始也有这个困惑后来把线程池、channel 复用、同步异步混用这几个点理清楚才算是真正解决。6.1 并发场景里最常见的两类错误第一类是服务端ThreadPoolExecutor的线程数设置太低。grpc.server()默认使用线程池如果你的接口处理里有个很慢的阻塞调用比如同步请求第三方 REST API默认 10 个 worker 很快就会被占满后面的请求全部排队表现就是你看到的并发一高就超时。第二类是客户端每个请求都新建 channel。我见过项目代码里每次请求都调grpc.insecure_channel()用完也不关。这样等于每次 RPC 都重建 TCP 连接再加上 HTTP/2 握手和连接建立的开销性能自然上不去。channel 是连接池抽象官方设计就是让你复用的。6.2 server 端线程池与 message size 配置Python 服务端建议这样建from concurrent import futures import grpc server grpc.server( futures.ThreadPoolExecutor(max_workers32), options[ (grpc.max_send_message_length, 100 * 1024 * 1024), (grpc.max_receive_message_length, 100 * 1024 * 1024), ] ) hello_pb2_grpc.add_GreeterServicer_to_server(GreeterServicer(), server) server.add_insecure_port([::]:50051) server.start() server.wait_for_termination()线程数不能瞎设要看业务是 IO 型还是 CPU 型。IO 型请求线程数可以多一点比如 32 或 64CPU 密集计算型开太多反而因为 GIL 互相抢性能下降。消息大小限制也很重要protobuf 默认对单条消息大小有限制传大文件或图片时不做配置会直接报 ResourceExhausted。6.3 client 端 channel 复用与 async 选择客户端正确姿势是全局只建一个 channel多个线程/协程共用channel grpc.insecure_channel(127.0.0.1:50051) stub hello_pb2_grpc.GreeterStub(channel) # 多个线程里直接复用 stub 调方法Python 的 channel 和 stub 设计是线程安全的不用担心多线程同时调用会崩。如果项目用了 asyncio那就不要用同步 stub 硬调否则一个阻塞就把整个事件循环卡住。正确方式是走grpc.aioimport asyncio import grpc import hello_pb2 import hello_pb2_grpc async def main() - None: async with grpc.aio.insecure_channel(127.0.0.1:50051) as channel: stub hello_pb2_grpc.GreeterStub(channel) resp await stub.SayHello(hello_pb2.HelloRequest(nameAoi)) print(resp.message) asyncio.run(main())6.4 一个压测对比出来的结论我在本地用 200 个并发请求做了对比实验。第一轮每请求新建 channel吞吐量惨不忍睹还伴随大量连接建立失败第二轮复用同一个 channel同样的并发规模吞吐直接上了一个量级。第三轮改用grpc.aio在异步场景下事件循环不再被阻塞平均延迟又降了一截。所以 Python gRPC 并发问题绝大多数不是 gRPC 本身不行而是使用姿势不对该复用的没复用该异步的用了同步。问题常见原因解决方法并发高时服务端排队超时线程池太小按 IO/CPU 类型调整max_workers客户端大量 TIME_WAIT每请求新建 channel全局复用 channel 和 stubasyncio 里卡住整个事件循环使用同步 stub改用grpc.aio大消息传不进去默认消息大小限制配置grpc.max_send_message_length等选项7. 学习路径复盘我的爱学习方法论学习 gRPC 之后最大的收获不只是会调接口了而是建立了一套跨语言同一套协议的思维方式。这轮折腾下来我想分享几个不一定写在官方文档里的经验。7.1 每个栈留一份最小可运行项目我后来给自己定了个规矩任何一个技术栈的 gRPC 学习都要留一个能一键运行的最小项目包含三样东西——一份hello.proto、一个 server、一个 client。Go 的、Java 的、Python 的、PHP 的都留了。原因很简单隔几个月再打开这些项目不用重新查文档跑一遍就能快速回到上下文。而且这个最小项目是排错工具箱线上有问题时把它跑起来做对照实验非常快。7.2 调试工具用起来grpcurl 和 grpcui很多 gRPC 新手还用 curl 去测 gRPC 接口结果发现根本不行因为默认是二进制协议。这里推荐grpcurl用法类似 curl能直接连 gRPC 服务反射协议列出方法、调用方法grpcurl -plaintext -d {name: Aoi} 127.0.0.1:50051 helloworld.Greeter/SayHellogrpcui则是网页版工具自动生成可视化表单调试字段特别方便。没有这两个工具之前我只能靠写代码起 client 来验证接口效率低很多。7.3 后续进阶方向Streaming、拦截器、负载均衡如果已经能跑通 Helloworld下一步建议按这个顺序推进四种调用模式把服务端流、客户端流、双向流各写一遍真正理解 RPC 的流式语义拦截器Java 和 Go 里分别写一个日志和鉴权拦截器理解请求链路里横切逻辑的位置deadline 和 metadata给 RPC 设置超时、传递 trace 信息生产环境排查问题必备负载均衡在多实例部署时区分直连、round_robin、xds理解 gRPC 客户端负载均衡和服务端负载均衡的区别。最后分享一个小体会。学习这类和语言无关的框架靠的不是我很爱学习这个口号而是把学习过程拆成一个个可以验证的小实验。每跑通一个实验就整理一份笔记记录当时用的版本、生成命令和踩坑点。等笔记攒到一定程度你会突然发现自己已经能带着团队从 REST 平滑过渡到 gRPC而且能自信地说出哪一步会有坑、该提前怎么防。我现在依然很爱学习但已经不爱脑袋一热就折腾底层编译了——能站着把功能做出来就别跪着啃一天源码。