使用 gRPC 的过程中,最大的感受就是:官方文档只告诉你怎么用,不告诉你怎么不死。本文记录我在 Node.js gRPC 开发中踩过的五个坑,每一个都让我 debug 到怀疑人生。
一、字段命名陷阱:snake_case vs camelCase
这是 gRPC 新手必踩的第一坑,也是本文最初的灵感来源。
假设你有一个 .proto 文件:
syntax = "proto3";
message User {
string user_id = 1;
string user_name = 2;
int64 created_at = 3;
}
你兴冲冲地在 Node.js 服务端返回数据:
// 服务端
function getUser(call, callback) {
callback(null, {
user_id: '12345',
user_name: '张三',
created_at: 1689600000
});
}
客户端收到的却是:
{ userId: '12345', userName: '张三', createdAt: 1689600000 }
反之亦然——客户端发送 camelCase,服务端收到 snake_case。
为什么会这样?
@grpc/proto-loader 在加载 .proto 文件时,默认启用了字段名转换:
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
const packageDefinition = protoLoader.loadSync('user.proto', {
longs: String,
enums: String,
defaults: true,
oneofs: true
});
解决方案
方案一(推荐):服务端和客户端统一使用 camelCase
callback(null, { userId: '12345', userName: '张三', createdAt: 1689600000 });
const response = await client.getUser({ userId: '12345' });
console.log(response.userName);
方案二:设置 keepCase: true,保持 snake_case
const packageDefinition = protoLoader.loadSync('user.proto', {
keepCase: true,
longs: String, enums: String, defaults: true, oneofs: true
});
方案三(不推荐):混用。 一旦涉及 oneof 字段,转换逻辑就可能失效——你会收获一坨 undefined。
字段名不一致时,gRPC 不会报错,只会静默地给你 undefined。教训:接手任何 gRPC 项目,第一件事就是确认 keepCase 配置。
二、四种服务类型的选择陷阱
2.1 Unary —— 一发一收
rpc GetUser(GetUserRequest) returns (User);
常见错误:用 Unary 实现「导出报表」接口,服务端需要 30 秒生成文件,客户端等到超时。应该用 Server Streaming。
2.2 Server Streaming —— 一发多收
rpc ExportReport(ExportRequest) returns (stream ReportChunk);
// 服务端
function exportReport(call) {
for (let i = 0; i < 10; i++) {
call.write({ chunk: `第 ${i + 1} 页报告数据...` });
}
call.end();
}
// 客户端
const stream = client.exportReport({ reportId: '2023-Q2' });
stream.on('data', (chunk) => console.log('收到:', chunk.chunk));
stream.on('end', () => console.log('导出完成'));
2.3 Client Streaming —— 多发一收
rpc UploadFile(stream FileChunk) returns (UploadResult);
适用场景:上传大文件、批量写入数据。
2.4 Bidirectional Streaming —— 多发多收
rpc Chat(stream ChatMessage) returns (stream ChatMessage);
最容易踩的坑:用 Bidirectional Streaming 实现简单轮询——客户端每 5 秒发心跳。没必要,用 Unary 就够了。
三、超时:不设 deadline 的请求等于定时炸弹
// 危险!服务端宕机时这个回调永远不会触发
client.getUser({ userId: '12345' }, (err, response) => { ... });
gRPC 默认没有超时。Node.js 事件循环不会报错,内存不会释放,直到 OOM。
const deadline = new Date();
deadline.setSeconds(deadline.getSeconds() + 5);
client.getUser({ userId: '12345' }, { deadline }, (err, response) => {
if (err) {
console.error('请求超时:', err.message);
return;
}
console.log(response);
});
生产规则:简单查询 1-3s、复杂计算 5-10s、文件传输 30-60s、流式按场景设定。
全局默认超时拦截器:
function deadlineInterceptor(defaultTimeout) {
return function(options, nextCall) {
if (!options.deadline) {
const deadline = new Date();
deadline.setSeconds(deadline.getSeconds() + defaultTimeout);
options.deadline = deadline;
}
return new nextCall(options);
};
}
四、TLS 证书配置
4.1 生产环境禁用 insecure
const client = new userProto.User('localhost:50051', grpc.credentials.createInsecure());
// 仅限本地开发!
4.2 自签名证书的正确配置
const fs = require('fs');
const server = new grpc.Server();
server.bindAsync(
'0.0.0.0:50051',
grpc.ServerCredentials.createSsl(
fs.readFileSync('ca.crt'),
[{ cert_chain: fs.readFileSync('server.crt'), private_key: fs.readFileSync('server.key') }],
true // mTLS
),
() => server.start()
);
const client = new userProto.User(
'localhost:50051',
grpc.credentials.createSsl(
fs.readFileSync('ca.crt'),
fs.readFileSync('client.key'),
fs.readFileSync('client.crt')
)
);
踩坑:不传 CA 证书时报错 UNAVAILABLE 或 DNS resolution failed——让你排查网络,其实是证书不被信任。
4.3 环境判断
const credentials = process.env.NODE_ENV === 'production'
? grpc.credentials.createSsl(fs.readFileSync('/etc/certs/ca.crt'), ...)
: grpc.credentials.createInsecure();
五、大消息传输:4MB 的隐形墙
gRPC 默认最大消息 4MB。超过时服务端拒绝,客户端收到 RESOURCE_EXHAUSTED。
// 服务端
const server = new grpc.Server({
'grpc.max_receive_message_length': 20 * 1024 * 1024,
'grpc.max_send_message_length': 20 * 1024 * 1024
});
// 客户端
const client = new userProto.User('localhost:50051', credentials, {
'grpc.max_receive_message_length': 20 * 1024 * 1024,
'grpc.max_send_message_length': 20 * 1024 * 1024
});
调整限制只是权宜之计。真正的大数据传输应使用 Streaming 或引用传递。
总结
gRPC 的默认行为有太多反直觉的设计:
- 字段名会变魔术 —— 不确认
keepCase,数据不是你发送的数据 - 默认无超时 —— 没有 deadline 的请求是定时炸弹
- TLS 报错不透明 —— 证书问题伪装成网络问题
- 4MB 硬限制 —— 设计接口时就要考虑消息大小
踩坑不可怕,可怕的是踩了坑还不知道为什么。