gRPC :构建高性能微服务
在微服务架构中,服务之间的通信至关重要。传统的 RESTful API 虽然简单易用,但在性能、类型安全和代码生成方面存在不足。gRPC 是 Google 开源的高性能 RPC 框架,它使用 Protocol Buffers 作为接口定义语言和序列化格式,为微服务通信提供了更好的解决方案。
今天我们就来学习如何在 Go 中使用 gRPC。
什么是 gRPC?
gRPC 是一个现代的、开源的、高性能的远程过程调用(RPC)框架。它的核心特性包括:
- Protocol Buffers:高效的二进制序列化格式
- HTTP/2:支持多路复用、流控制、头部压缩
- 强类型:通过 .proto 文件定义接口,自动生成代码
- 双向流:支持客户端流、服务器流、双向流
- 跨语言:支持 Go、Java 、Python 、C++ 等多种语言
gRPC 四种通信模式详解
gRPC 支持四种通信模式,每种适用于不同的场景:
1. Unary RPC(一元调用)
最普通的请求-响应模式:客户端发送一个请求,服务器返回一个响应。类似于普通的 HTTP 调用。这是 gRPC 中最常用的模式,适用于 CRUD 操作等简单场景。
2. Server Streaming RPC(服务器流)
客户端发送一个请求,服务器返回一系列响应。适用于服务器需要分批返回大量数据的场景,如列表查询、日志推送、实时监控等。
3. Client Streaming RPC(客户端流)
客户端发送一系列请求,服务器返回一个响应。适用于客户端需要分批发送大量数据的场景,如文件上传、批量数据导入等。
4. Bidirectional Streaming RPC(双向流)
客户端和服务器都可以独立地发送一系列消息。适用于实时双向通信场景,如聊天、协同编辑、在线游戏等。
这四种模式覆盖了绝大多数微服务通信需求,开发者可以根据具体场景灵活选择。
protoc 插件体系
protoc 是 Protocol Buffers 的编译器,它通过插件机制支持多种语言和框架。Go 的 gRPC 开发需要以下插件:
- protoc-gen-go:生成 Go 的结构体和方法,对应 protobuf 消息类型
- protoc-gen-go-grpc:生成 gRPC 服务端和客户端接口
安装命令:
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
protoc 的插件体系遵循统一的接口规范,任何符合规范的生成器都可以被 protoc 调用。这使得社区能够不断扩展对新语言和框架的支持。
安装 gRPC 工具
# 安装 Go gRPC 插件
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
# 安装 Protocol Buffers 编译器
# macOS
brew install protobuf
# Ubuntu
sudo apt install protobuf-compiler
# 验证安装
protoc --version
定义服务接口
创建一个 user.proto 文件:
syntax = "proto3";
package user;
option go_package = "./user";
// 用户服务
service UserService {
// 获取用户
rpc GetUser (GetUserRequest) returns (User);
// 创建用户
rpc CreateUser (CreateUserRequest) returns (User);
// 列出用户
rpc ListUsers (ListUsersRequest) returns (stream User);
// 更新用户信息(双向流)
rpc UpdateUsers (stream UpdateUserRequest) returns (stream User);
}
// 用户消息
message User {
int64 id = 1;
string name = 2;
string email = 3;
int32 age = 4;
int64 created_at = 5;
}
// 请求消息
message GetUserRequest {
int64 id = 1;
}
message CreateUserRequest {
string name = 1;
string email = 2;
int32 age = 3;
}
message ListUsersRequest {
int32 page = 1;
int32 page_size = 2;
}
message UpdateUserRequest {
int64 id = 1;
string name = 2;
string email = 3;
int32 age = 4;
}
生成 Go 代码
protoc --go_out=. --go-grpc_out=. user.proto
这会生成两个文件:
user.pb.go:Protocol Buffers 消息定义user_grpc.pb.go:gRPC 服务代码
实现 gRPC 服务器
package main
import (
"context"
"log"
"net"
"sync"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
pb "example/user"
)
type User struct {
ID int64
Name string
Email string
Age int32
CreatedAt int64
}
type UserServiceServer struct {
pb.UnimplementedUserServiceServer
mu sync.RWMutex
users map[int64]*User
nextID int64
}
func NewUserServiceServer() *UserServiceServer {
return &UserServiceServer{
users: make(map[int64]*User),
nextID: 1,
}
}
// GetUser 实现获取用户
func (s *UserServiceServer) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) {
s.mu.RLock()
defer s.mu.RUnlock()
user, ok := s.users[req.Id]
if !ok {
return nil, status.Errorf(codes.NotFound, "用户 %d 不存在", req.Id)
}
return &pb.User{
Id: user.ID,
Name: user.Name,
Email: user.Email,
Age: user.Age,
CreatedAt: user.CreatedAt,
}, nil
}
// CreateUser 实现创建用户
func (s *UserServiceServer) CreateUser(ctx context.Context, req *pb.CreateUserRequest) (*pb.User, error) {
s.mu.Lock()
defer s.mu.Unlock()
user := &User{
ID: s.nextID,
Name: req.Name,
Email: req.Email,
Age: req.Age,
CreatedAt: time.Now().Unix(),
}
s.users[user.ID] = user
s.nextID++
log.Printf("创建用户: %d (%s)", user.ID, user.Name)
return &pb.User{
Id: user.ID,
Name: user.Name,
Email: user.Email,
Age: user.Age,
CreatedAt: user.CreatedAt,
}, nil
}
// ListUsers 实现列出用户(服务器流)
func (s *UserServiceServer) ListUsers(req *pb.ListUsersRequest, stream pb.UserService_ListUsersServer) error {
s.mu.RLock()
defer s.mu.RUnlock()
count := 0
for _, user := range s.users {
if count >= int(req.PageSize) {
break
}
err := stream.Send(&pb.User{
Id: user.ID,
Name: user.Name,
Email: user.Email,
Age: user.Age,
CreatedAt: user.CreatedAt,
})
if err != nil {
return err
}
count++
}
return nil
}
// UpdateUsers 实现批量更新(双向流)
func (s *UserServiceServer) UpdateUsers(stream pb.UserService_UpdateUsersServer) error {
for {
req, err := stream.Recv()
if err != nil {
return err
}
s.mu.Lock()
user, ok := s.users[req.Id]
if !ok {
s.mu.Unlock()
stream.Send(&pb.User{})
continue
}
// 更新字段
if req.Name != "" {
user.Name = req.Name
}
if req.Email != "" {
user.Email = req.Email
}
if req.Age > 0 {
user.Age = req.Age
}
s.mu.Unlock()
// 发送更新后的用户
err = stream.Send(&pb.User{
Id: user.ID,
Name: user.Name,
Email: user.Email,
Age: user.Age,
CreatedAt: user.CreatedAt,
})
if err != nil {
return err
}
}
}
func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("监听失败: %v", err)
}
s := grpc.NewServer()
pb.RegisterUserServiceServer(s, NewUserServiceServer())
log.Println("gRPC 服务器启动在 :50051")
if err := s.Serve(lis); err != nil {
log.Fatalf("服务失败: %v", err)
}
}
实现 gRPC 客户端
package main
import (
"context"
"io"
"log"
"time"
"google.golang.org/grpc"
pb "example/user"
)
func main() {
// 连接服务器
conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure())
if err != nil {
log.Fatalf("连接失败: %v", err)
}
defer conn.Close()
client := pb.NewUserServiceClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// 1. 创建用户
log.Println("=== 创建用户 ===")
user1, err := client.CreateUser(ctx, &pb.CreateUserRequest{
Name: "张三",
Email: "zhangsan@example.com",
Age: 25,
})
if err != nil {
log.Fatalf("创建用户失败: %v", err)
}
log.Printf("创建成功: ID=%d, Name=%s", user1.Id, user1.Name)
user2, err := client.CreateUser(ctx, &pb.CreateUserRequest{
Name: "李四",
Email: "lisi@example.com",
Age: 30,
})
if err != nil {
log.Fatalf("创建用户失败: %v", err)
}
log.Printf("创建成功: ID=%d, Name=%s", user2.Id, user2.Name)
// 2. 获取用户
log.Println("\n=== 获取用户 ===")
user, err := client.GetUser(ctx, &pb.GetUserRequest{Id: user1.Id})
if err != nil {
log.Fatalf("获取用户失败: %v", err)
}
log.Printf("用户信息: %+v", user)
// 3. 列出用户(服务器流)
log.Println("\n=== 列出用户 ===")
stream, err := client.ListUsers(ctx, &pb.ListUsersRequest{
Page: 1,
PageSize: 10,
})
if err != nil {
log.Fatalf("列出用户失败: %v", err)
}
for {
user, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
log.Fatalf("接收失败: %v", err)
}
log.Printf("用户: ID=%d, Name=%s", user.Id, user.Name)
}
// 4. 批量更新(双向流)
log.Println("\n=== 批量更新 ===")
updateStream, err := client.UpdateUsers(ctx)
if err != nil {
log.Fatalf("更新用户失败: %v", err)
}
// 发送更新请求
updates := []*pb.UpdateUserRequest{
{Id: user1.Id, Name: "张三(已更新)", Age: 26},
{Id: user2.Id, Email: "lisi_new@example.com"},
}
for _, update := range updates {
err := updateStream.Send(update)
if err != nil {
log.Fatalf("发送更新失败: %v", err)
}
// 接收更新后的用户
user, err := updateStream.Recv()
if err != nil {
log.Fatalf("接收更新失败: %v", err)
}
log.Printf("更新成功: ID=%d, Name=%s, Email=%s", user.Id, user.Name, user.Email)
}
updateStream.CloseSend()
}
gRPC 拦截器链
gRPC 拦截器类似于 HTTP 的中间件,可以在请求处理前后执行通用逻辑。gRPC 支持两种拦截器:
- UnaryInterceptor:一元调用的拦截器
- StreamInterceptor:流式调用的拦截器
拦截器可以链式组合,形成拦截器链(Interceptor Chain)。每个拦截器按注册顺序执行,实现日志、认证、限流、追踪等横切关注点。
// 服务器拦截器
func loggingInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
start := time.Now()
log.Printf("开始处理: %s", info.FullMethod)
resp, err := handler(ctx, req)
duration := time.Since(start)
log.Printf("完成处理: %s (%v)", info.FullMethod, duration)
return resp, err
}
// 客户端拦截器
func clientLoggingInterceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
start := time.Now()
log.Printf("客户端调用: %s", method)
err := invoker(ctx, method, req, reply, cc, opts...)
duration := time.Since(start)
log.Printf("客户端完成: %s (%v)", method, duration)
return err
}
// 使用拦截器
func main() {
// 服务器
s := grpc.NewServer(
grpc.UnaryInterceptor(loggingInterceptor),
)
// 客户端
conn, err := grpc.Dial(
"localhost:50051",
grpc.WithInsecure(),
grpc.WithUnaryInterceptor(clientLoggingInterceptor),
)
}
metadata 传递
gRPC 使用 metadata 在请求之间传递键值对信息,类似于 HTTP 的 Header。metadata 分为两类:
- Incoming metadata:从客户端传递到服务器的元数据
- Outgoing metadata:从服务器返回给客户端的元数据(通过 Header 或 Trailer)
metadata 常用于传递认证令牌、请求 ID、用户语言偏好等信息。
// 从 context 获取传入的 metadata
md, ok := metadata.FromIncomingContext(ctx)
if ok {
requestID := md.Get("x-request-id")
log.Println("请求 ID:", requestID)
}
// 客户端发送 metadata
ctx := metadata.AppendToOutgoingContext(ctx, "x-request-id", "12345")
resp, err := client.SomeMethod(ctx, req)
// 服务器返回 header
header := metadata.New(map[string]string{
"x-custom-header": "value",
})
grpc.SendHeader(ctx, header)
负载均衡和健康检查
gRPC 内置了客户端负载均衡机制。Go 的 gRPC 支持以下负载均衡策略:
- pick_first:默认策略,连接到第一个可用地址
- round_robin:轮询策略,依次连接所有地址
- grpclb:gRPC 负载均衡协议(已弃用,推荐使用 xDS)
gRPC 健康检查遵循 GRPC Health Checking Protocol ,通过 Health 服务暴露服务健康状态。
// 启用健康检查服务
import "google.golang.org/grpc/health"
import "google.golang.org/grpc/health/grpc_health_v1"
func main() {
s := grpc.NewServer()
// 注册健康检查服务
healthServer := health.NewServer()
grpc_health_v1.RegisterHealthServer(s, healthServer)
// 设置服务状态
healthServer.SetServingStatus("user.UserService", grpc_health_v1.HealthCheckResponse_SERVING)
}
实战:完整的微服务通信示例
下面是一个订单服务调用用户服务的完整示例,展示了 gRPC 在微服务架构中的典型用法:
// order_service.go - 订单服务调用用户服务
package main
import (
"context"
"log"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
userpb "example/user"
)
type OrderService struct {
userClient userpb.UserServiceClient
}
func NewOrderService(userAddr string) (*OrderService, error) {
conn, err := grpc.Dial(userAddr,
grpc.WithInsecure(),
grpc.WithBlock(),
grpc.WithTimeout(5*time.Second),
)
if err != nil {
return nil, err
}
return &OrderService{
userClient: userpb.NewUserServiceClient(conn),
}, nil
}
func (s *OrderService) CreateOrder(ctx context.Context, userID int64, amount float64) error {
// 调用用户服务验证用户
userCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
_, err := s.userClient.GetUser(userCtx, &userpb.GetUserRequest{Id: userID})
if err != nil {
st, ok := status.FromError(err)
if ok && st.Code() == codes.NotFound {
return status.Errorf(codes.InvalidArgument, "用户 %d 不存在", userID)
}
return status.Errorf(codes.Internal, "验证用户失败: %v", err)
}
// 创建订单逻辑...
log.Printf("为用户 %d 创建订单, 金额: %.2f", userID, amount)
return nil
}
实战:流式文件上传
使用客户端流实现大文件分片上传:
package main
import (
"bufio"
"context"
"io"
"log"
"os"
"time"
"google.golang.org/grpc"
)
// proto 定义
// service FileService {
// rpc Upload(stream FileChunk) returns (UploadStatus);
// }
// message FileChunk {
// bytes data = 1;
// string filename = 2;
// int64 sequence = 3;
// }
// message UploadStatus {
// bool success = 1;
// string message = 2;
// int64 total_bytes = 3;
// }
func uploadFile(client pb.FileServiceClient, filepath string) error {
file, err := os.Open(filepath)
if err != nil {
return err
}
defer file.Close()
stream, err := client.Upload(context.Background())
if err != nil {
return err
}
reader := bufio.NewReader(file)
buffer := make([]byte, 1024*64) // 64KB 分片
sequence := int64(0)
for {
n, err := reader.Read(buffer)
if n > 0 {
sequence++
chunk := &pb.FileChunk{
Data: buffer[:n],
Filename: filepath,
Sequence: sequence,
}
if err := stream.Send(chunk); err != nil {
return err
}
}
if err == io.EOF {
break
}
if err != nil {
return err
}
}
status, err := stream.CloseAndRecv()
if err != nil {
return err
}
log.Printf("上传结果: %s, 总字节: %d", status.Message, status.TotalBytes)
return nil
}
实战:认证中间件
// 服务器拦截器:验证 Token
func authInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
// 从 metadata 获取 token
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, status.Error(codes.Unauthenticated, "缺少 metadata")
}
tokens := md.Get("authorization")
if len(tokens) == 0 {
return nil, status.Error(codes.Unauthenticated, "缺少 token")
}
// 验证 token
token := tokens[0]
userID, err := validateToken(token)
if err != nil {
return nil, status.Error(codes.Unauthenticated, "无效的 token")
}
// 将用户信息放入 context
ctx = context.WithValue(ctx, "userID", userID)
return handler(ctx, req)
}
// 客户端:发送 Token
type tokenAuth struct {
token string
}
func (t *tokenAuth) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
return map[string]string{
"authorization": t.token,
}, nil
}
func (t *tokenAuth) RequireTransportSecurity() bool {
return false
}
// 使用
conn, err := grpc.Dial(
"localhost:50051",
grpc.WithInsecure(),
grpc.WithPerRPCCredentials(&tokenAuth{token: "your-token"}),
)
实战:熔断降级
在微服务架构中,gRPC 客户端应实现熔断机制。使用第三方库如 github.com/afex/hystrix-go 或自己实现简单的熔断器:
package main
import (
"sync"
"time"
)
// CircuitBreaker 简单熔断器实现
type CircuitBreaker struct {
failureCount int
failureLimit int
timeout time.Duration
lastFailureTime time.Time
state string // closed, open, half-open
mu sync.Mutex
}
func NewCircuitBreaker(limit int, timeout time.Duration) *CircuitBreaker {
return &CircuitBreaker{
failureLimit: limit,
timeout: timeout,
state: "closed",
}
}
func (cb *CircuitBreaker) Call(fn func() error) error {
cb.mu.Lock()
defer cb.mu.Unlock()
if cb.state == "open" {
if time.Since(cb.lastFailureTime) > cb.timeout {
cb.state = "half-open"
cb.failureCount = 0
} else {
return ErrCircuitOpen
}
}
err := fn()
if err != nil {
cb.failureCount++
cb.lastFailureTime = time.Now()
if cb.failureCount >= cb.failureLimit {
cb.state = "open"
}
return err
}
// 成功则重置
cb.failureCount = 0
cb.state = "closed"
return nil
}
性能对比:gRPC vs REST vs GraphQL
| 特性 | gRPC | REST (HTTP/1.1) | REST (HTTP/2) | GraphQL |
|---|---|---|---|---|
| 协议 | HTTP/2 | HTTP/1.1 | HTTP/2 | HTTP/1.1 或 HTTP/2 |
| 序列化 | Protobuf (二进制) | JSON (文本) | JSON (文本) | JSON (文本) |
| 消息大小 | 约 JSON 的 1/3 | 较大 | 较大 | 较大 |
| 传输效率 | 极高 | 低 | 中 | 低 |
| 强类型 | 是(proto) | 否 | 否 | Schema |
| 代码生成 | 自动生成 | 无 | 无 | 可生成 |
| 流式传输 | 双向流 | 无 | Server Push | Subscription |
| 浏览器支持 | 需 gRPC-Web | 原生 | 原生 | 原生 |
| 调试难度 | 较难(二进制) | 容易(文本) | 容易(文本) | 容易(文本) |
基准测试数据(Ping-Pong,同机测试):
| 框架 | 吞吐量 (req/s) | 平均延迟 (us) | P99 延迟 (us) |
|---|---|---|---|
| gRPC (Unary) | 150,000+ | ~80 | ~200 |
| gRPC (Streaming) | 200,000+ | ~60 | ~150 |
| HTTP/1.1 REST | 30,000 | ~600 | ~2,000 |
| HTTP/2 REST | 50,000 | ~300 | ~800 |
| GraphQL | 25,000 | ~700 | ~2,500 |
可以看出,gRPC 在吞吐量和延迟方面都有显著优势,特别适合服务间的高频调用场景。REST 的优势在于简单性和广泛支持。GraphQL 适合客户端灵活查询的场景,但性能开销较大。
FAQ
Q1: gRPC 和 REST API 应该如何选择?
gRPC 适合内部微服务通信、高并发场景、需要强类型约束的情况。REST 适合面向外部客户端的 API(特别是浏览器)、需要简单调试接口的情况。实际项目中经常两者并用:内部用 gRPC,外部暴露 REST Gateway。
Q2: protoc 生成的代码如何版本管理?
建议将 .proto 文件提交到版本控制,但不提交生成的 .pb.go 文件。在 CI/CD 过程中重新生成。如果多个服务共享 .proto,建议建立独立的 proto 仓库,使用 git submodule 或 Go module 引入。
Q3: gRPC 调用超时如何配置?
使用 context.WithTimeout 在每次调用时设置超时:
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
resp, err := client.GetUser(ctx, req)
也可以在连接级别设置默认超时。
Q4: gRPC 的 Keepalive 机制是什么?
Go 的 gRPC 支持 TCP Keepalive 和应用层 Keepalive(ping/pong):
grpc.WithKeepaliveParams(keepalive.ClientParameters{
Time: 10 * time.Second,
Timeout: 3 * time.Second,
PermitWithoutStream: true,
})
Q5: 如何处理 gRPC 的向后兼容性?
protobuf 设计为向前和向后兼容:
- 不要删除已有的字段编号
- 不要更改字段的数据类型
- 新增字段使用未使用的字段编号
- 建议使用
reserved保留已删除的字段编号 - 标记废弃字段可用
deprecated = true选项
Q6: gRPC-Web 是什么?
gRPC-Web 是 gRPC 的浏览器客户端实现。由于浏览器限制无法直接使用 HTTP/2 的完整功能,gRPC-Web 使用代理(如 Envoy)将浏览器请求转换为标准 gRPC 请求。gorilla/websocket 不是 gRPC-Web,但如果你需要浏览器实时通信,可以考虑 gRPC-Web 或直接使用 WebSocket。
Q7: 如何实现 gRPC 的服务发现?
gRPC 支持多种服务发现机制:
- 静态地址:最简单,直接写在客户端配置中
- DNS:gRPC 支持 DNS SRV 记录解析
- Etcd/ZooKeeper:使用第三方解析器插件
- Consul:社区提供 Consul 解析器
- Kubernetes:利用 Headless Service + DNS
推荐使用 go-grpc-middleware 提供的 resolver 接口自定义服务发现。
Q8: gRPC 消息大小有限制吗?
默认限制:
- 最大接收消息大小:4MB
- 最大发送消息大小:无限(通常受内存限制)
可以通过grpc.MaxRecvMsgSize和grpc.MaxSendMsgSize选项调整:
s := grpc.NewServer(
grpc.MaxRecvMsgSize(16*1024*1024), // 16MB
grpc.MaxSendMsgSize(16*1024*1024), // 16MB
)
最佳实践
proto 文件版本管理
- 独立 proto 仓库:将公共 proto 放在独立仓库中,所有服务引用
- 语义化版本:proto 变更遵循 SemVer 规范
- 兼容性检查:CI 中使用 protolock 等工具检查破坏性变更
- 文档注释:为每条消息和字段添加详细注释,描述其用途和约束
- 保留字段:删除字段时使用
reserved保留编号
服务发现集成
- 健康检查:所有 gRPC 服务都应实现健康检查接口
- 优雅关闭:服务关闭前发送 NOT_SERVING 状态,给客户端时间路由到其他实例
- 连接池:客户端应维护连接池,避免每次调用新建连接
- 重试策略:配置合理的重试次数、退避策略和超时
优雅关闭
func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("监听失败: %v", err)
}
s := grpc.NewServer()
pb.RegisterUserServiceServer(s, NewUserServiceServer())
// 优雅关闭
go func() {
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
<-c
log.Println("正在优雅关闭...")
s.GracefulStop()
}()
log.Println("gRPC 服务器启动在 :50051")
if err := s.Serve(lis); err != nil {
log.Fatalf("服务失败: %v", err)
}
}
延伸阅读
- gRPC 官方文档 :Go 语言 gRPC 指南
- Protocol Buffers 文档 :protobuf Go 教程
- Go gRPC Middleware :社区维护的 gRPC 中间件集合
- gRPC-Web :浏览器端的 gRPC 支持
- Envoy gRPC Bridge :使用 Envoy 代理 gRPC 流量
- gRPC 负载均衡指南 :官方负载均衡指南
小结
今天我们深入学习了 gRPC:
- 四种通信模式:Unary、Server Streaming、Client Streaming、Bidirectional Streaming
- 服务定义:编写 .proto 文件,生成代码
- 服务器实现:Unary RPC、服务器流、双向流
- 客户端调用:同步调用、流式调用
- 拦截器链:日志、认证等中间件
- metadata:元数据传递
- 性能对比:gRPC vs REST vs GraphQL
gRPC 为微服务通信提供了高性能、强类型的解决方案。在构建复杂的分布式系统时,gRPC 是一个值得考虑的核心技术选型。
练习时间
- 实现一个 gRPC 网关,将 gRPC 服务暴露为 REST API
- 创建一个带有认证和授权的 gRPC 服务
- 实现 gRPC 服务的服务发现和负载均衡
- 使用 gRPC 的反射功能实现动态客户端
我们下篇见!
gRPC 拦截器链详解
gRPC 的拦截器(Interceptor)是统一处理请求和响应的核心机制,类似于 HTTP 中间件。拦截器分为一元拦截器(UnaryInterceptor)和流拦截器(StreamInterceptor)。
package main
import (
"context"
"fmt"
"time"
"google.golang.org/grpc"
)
// UnaryInterceptor 一元拦截器
type UnaryInterceptor func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error)
// LoggingInterceptor 日志拦截器
func LoggingInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
start := time.Now()
resp, err := handler(ctx, req)
fmt.Printf("[%s] %s %v err=%v\n", time.Since(start), info.FullMethod, req, err)
return resp, err
}
// AuthInterceptor 认证拦截器
func AuthInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
// 从 metadata 获取 token
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, fmt.Errorf("missing metadata")
}
token := md.Get("authorization")
if len(token) == 0 {
return nil, fmt.Errorf("missing authorization token")
}
// 验证 token(简化)
if token[0] != "Bearer valid-token" {
return nil, fmt.Errorf("invalid token")
}
return handler(ctx, req)
}
func main() {
server := grpc.NewServer(
grpc.UnaryInterceptor(LoggingInterceptor),
)
_ = server
}
gRPC 的服务健康检查
gRPC 提供了标准的健康检查协议,通过 grpc.health.v1 实现:
package main
import (
"context"
"fmt"
"google.golang.org/grpc"
healthpb "google.golang.org/grpc/health/grpc_health_v1"
)
type HealthServer struct {
healthpb.UnimplementedHealthServer
}
func (s *HealthServer) Check(ctx context.Context, req *healthpb.HealthCheckRequest) (*healthpb.HealthCheckResponse, error) {
return &healthpb.HealthCheckResponse{
Status: healthpb.HealthCheckResponse_SERVING,
}, nil
}
func (s *HealthServer) Watch(req *healthpb.HealthCheckRequest, stream healthpb.Health_WatchServer) error {
return stream.Send(&healthpb.HealthCheckResponse{
Status: healthpb.HealthCheckResponse_SERVING,
})
}
func main() {
fmt.Println("健康检查服务实现示例")
}
常见错误与 FAQ
Q: gRPC 怎么获取客户端 IP?
A: 从 context 的 peer 信息中获取:
peer, ok := peer.FromContext(ctx)
if ok {
fmt.Println("Client IP:", peer.Addr.String())
}
Q: 如何处理 gRPC 的流式超时?
A: 流式调用的超时需要在服务端和客户端分别设置。服务端使用 ServerStream.Context() 监听取消信号,客户端使用 context.WithTimeout 创建带超时的 context。
Q: proto 文件修改后有什么注意事项?
A: 必须重新生成代码,同时确保服务端和客户端使用的 proto 定义一致。建议在版本控制中管理生成的代码,或在 CI 中自动生成。
延伸阅读
gRPC 四种通信模式实战
gRPC 支持四种通信模式:一元 RPC(Unary RPC)、服务端流式(Server Streaming RPC)、客户端流式(Client Streaming RPC)和双向流式(Bidirectional Streaming RPC)。
服务端流式
服务端流式 RPC 允许服务端在响应中发送多个消息:
package main
import (
"context"
"fmt"
"time"
)
// ServerStreamExample 服务端流式示例
type ServerStreamExample struct{}
func (s *ServerStreamExample) StreamData(req *DataRequest, stream DataStream_Server) error {
for i := 0; i < 10; i++ {
if err := stream.Send(&DataResponse{Value: int64(i)}); err != nil {
return err
}
time.Sleep(100 * time.Millisecond)
}
return nil
}
type DataRequest struct{}
type DataResponse struct{ Value int64 }
type DataStream_Server interface{ Send(*DataResponse) error }
func main() {
fmt.Println("服务端流式:服务端连续发送多个响应消息")
}
双向流式
双向流式 RPC 允许客户端和服务端同时发送消息流:
package main
import (
"fmt"
"io"
)
type ChatMessage struct {
User string
Content string
}
type ChatStream interface {
Send(*ChatMessage) error
Recv() (*ChatMessage, error)
}
func ChatHandler(stream ChatStream) error {
for {
msg, err := stream.Recv()
if err == io.EOF {
return nil
}
if err != nil {
return err
}
response := &ChatMessage{
User: "Server",
Content: fmt.Sprintf("收到 %s 的消息: %s", msg.User, msg.Content),
}
if err := stream.Send(response); err != nil {
return err
}
}
}
func main() {
fmt.Println("双向流式:聊天服务器示例")
}
gRPC 与 REST 的性能对比
| 指标 | gRPC | REST (HTTP/1.1) | REST (HTTP/2) |
|---|---|---|---|
| 序列化效率 | 二进制 protobuf | 文本 JSON | 文本 JSON |
| Payload 大小 | 小 (二进制) | 大 (文本) | 大 (文本) |
| 连接复用 | HTTP/2 自带 | 需要额外实现 | 原生支持 |
| 流式支持 | 四种模式 | 模拟流 | 服务端推送 |
| 代码生成 | proto 自动生成 | 手动维护 | 手动维护 |
| 浏览器支持 | 需 gRPC-Web | 原生支持 | 原生支持 |
| 调试便利性 | 需要工具 | curl 即可 | curl 即可 |
对于内部服务通信,gRPC 在性能和效率上优势明显。对于面向浏览器的 API,REST 仍然是更好的选择。
进阶概念
四种通信模式
gRPC 支持四种通信模式,适合不同的业务场景:
- Unary RPC(一元请求):最常见的模式,客户端发送单个请求,服务端返回单个响应。类似 HTTP REST 调用。
- Server Streaming RPC:客户端发送单个请求,服务端返回流式响应。适合服务端数据量大的场景,如文件下载、数据同步。
- Client Streaming RPC:客户端发送流式请求,服务端返回单个响应。适合大数据上传、批量数据处理。
- Bidirectional Streaming RPC:客户端和服务端双向流式通信。适合聊天、实时协作、游戏同步等场景。
protoc 插件机制
protoc 编译器通过插件机制生成多语言代码:
protoc-gen-go:生成 Go 的 protobuf 消息代码protoc-gen-go-grpc:生成 Go 的 gRPC 服务代码- 其他语言也有对应的插件
编译命令:protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative *.proto
拦截器链
gRPC 拦截器(Interceptor)类似于 HTTP 中间件,可以在请求处理前后注入逻辑。Go gRPC 支持一元拦截器和流拦截器:
// 一元拦截器:处理普通请求
func unaryInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
start := time.Now()
md, _ := metadata.FromIncomingContext(ctx)
log.Printf("RPC: %s, metadata: %v", info.FullMethod, md)
resp, err := handler(ctx, req)
log.Printf("RPC %s finished in %v", info.FullMethod, time.Since(start))
return resp, err
}
// 流拦截器:处理流式请求
func streamInterceptor(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
log.Printf("Stream RPC: %s", info.FullMethod)
return handler(srv, ss)
}
// 链式使用
server := grpc.NewServer(
grpc.ChainUnaryInterceptor(loggingInterceptor, authInterceptor, recoveryInterceptor),
grpc.ChainStreamInterceptor(streamLoggingInterceptor),
)
拦截器链按注册顺序执行,常用于日志、认证、限流、熔断、重试、链路追踪等。
Metadata 传递
gRPC 使用 Metadata 传递请求头信息(类似 HTTP Header):
// 服务端读取 metadata
func (s *Server) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) {
md, ok := metadata.FromIncomingContext(ctx)
if ok {
traceID := md.Get("x-trace-id")
log.Printf("trace_id: %v", traceID)
}
// ...
}
// 客户端发送 metadata
ctx := metadata.NewOutgoingContext(context.Background(),
metadata.Pairs("x-trace-id", "abc-123", "x-request-id", "req-456"))
resp, err := client.GetUser(ctx, req)
健康检查
gRPC 定义了标准化的健康检查协议:
service Health {
rpc Check(HealthCheckRequest) returns (HealthCheckResponse);
rpc Watch(HealthCheckRequest) returns (stream HealthCheckResponse);
}
message HealthCheckRequest { string service = 1; }
message HealthCheckResponse {
enum ServingStatus {
UNKNOWN = 0;
SERVING = 1;
NOT_SERVING = 2;
}
ServingStatus status = 1;
}
Go 的健康检查实现:
import "google.golang.org/grpc/health"
import "google.golang.org/grpc/health/grpc_health_v1"
healthCheck := health.NewServer()
grpc_health_v1.RegisterHealthServer(s, healthCheck)
healthCheck.SetServingStatus("myService", grpc_health_v1.HealthCheckResponse_SERVING)
实战案例
1. 完整微服务通信
// 统一拦截器:链路追踪 + 日志 + 认证
func serverInterceptor() grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
// 链路追踪
traceID := extractTraceID(ctx)
ctx = context.WithValue(ctx, "trace_id", traceID)
// 认证
if err := authenticate(ctx); err != nil {
return nil, err
}
// 限流
if !rateLimiter.Allow() {
return nil, status.Errorf(codes.ResourceExhausted, "rate limit exceeded")
}
start := time.Now()
resp, err := handler(ctx, req)
log.Printf("[trace=%s] %s %v", traceID, info.FullMethod, time.Since(start))
return resp, err
}
}
func authenticate(ctx context.Context) error {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return status.Errorf(codes.Unauthenticated, "missing metadata")
}
tokens := md.Get("authorization")
if len(tokens) == 0 {
return status.Errorf(codes.Unauthenticated, "missing token")
}
if !validateToken(tokens[0]) {
return status.Errorf(codes.Unauthenticated, "invalid token")
}
return nil
}
2. 流式大文件上传
func (s *FileService) Upload(stream pb.FileService_UploadServer) error {
var fileName string
var totalSize int64
tempFile, _ := os.CreateTemp("", "upload-*")
defer os.Remove(tempFile.Name())
for {
chunk, err := stream.Recv()
if err == io.EOF {
// 接收完毕
tempFile.Close()
// 保存到最终位置
os.Rename(tempFile.Name(), filepath.Join("/data/uploads", fileName))
return stream.SendAndClose(&pb.UploadResponse{
FileName: fileName,
TotalSize: totalSize,
})
}
if err != nil {
return err
}
if fileName == "" {
fileName = chunk.FileName
}
tempFile.Write(chunk.Content)
totalSize += int64(len(chunk.Content))
}
}
3. 认证中间件
type AuthInterceptor struct {
jwtSecret []byte
skipList map[string]bool
}
func (a *AuthInterceptor) Unary() grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
if a.skipList[info.FullMethod] {
return handler(ctx, req)
}
token, err := a.extractToken(ctx)
if err != nil {
return nil, err
}
claims, err := a.validateJWT(token)
if err != nil {
return nil, status.Errorf(codes.Unauthenticated, "invalid token: %v", err)
}
ctx = context.WithValue(ctx, "user_id", claims.UserID)
ctx = context.WithValue(ctx, "user_role", claims.Role)
return handler(ctx, req)
}
}
func (a *AuthInterceptor) extractToken(ctx context.Context) (string, error) {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return "", status.Errorf(codes.Unauthenticated, "missing metadata")
}
auth := md.Get("authorization")
if len(auth) == 0 {
return "", status.Errorf(codes.Unauthenticated, "missing authorization")
}
parts := strings.SplitN(auth[0], " ", 2)
if len(parts) != 2 || !strings.EqualFold(parts[0], "Bearer") {
return "", status.Errorf(codes.Unauthenticated, "invalid authorization format")
}
return parts[1], nil
}
性能对比:gRPC vs REST vs GraphQL
| 特性 | gRPC | REST | GraphQL |
|---|---|---|---|
| 协议 | HTTP/2 | HTTP/1.1 或 HTTP/2 | HTTP/1.1 或 HTTP/2 |
| 序列化 | Protocol Buffers | JSON | JSON |
| 性能 | 高(二进制、小体积) | 中(JSON 文本) | 中(JSON 文本) |
| 强类型 | 是(proto 生成) | 否 | 是(schema) |
| 浏览器支持 | 需 gRPC-Web | 原生 | 原生 |
| 流式 | 原生支持 | 服务器推送(SSE) | 订阅 |
| 调试 | 需 grpcurl 等工具 | curl、浏览器 | GraphiQL |
| 适用场景 | 微服务间通信 | 对外 API | 复杂查询前端 |
// gRPC 性能测试示例
func BenchmarkGRPC(b *testing.B) {
conn, _ := grpc.Dial("localhost:50051", grpc.WithInsecure())
client := pb.NewUserServiceClient(conn)
ctx := context.Background()
b.ResetTimer()
for i := 0; i < b.N; i++ {
client.GetUser(ctx, &pb.GetUserRequest{Id: 1})
}
}
// gRPC + protobuf 通常比 REST + JSON 快 5-10 倍
FAQ
Q1:gRPC 如何在浏览器中使用?
A:浏览器不直接支持 gRPC(需要 HTTP/2 trailers),需使用 gRPC-Web 代理或 Envoy 转换。
Q2:如何处理 gRPC 版本兼容性?
A:protobuf 支持向后兼容(新增字段不会破坏旧客户端)和忽略未知字段。字段编号不能复用,废弃字段用 reserved 标记。
Q3:gRPC 连接池如何管理?
A:grpc.Dial 创建的连接底层也是连接池。使用 grpc.WithDefaultServiceConfig 配置负载均衡策略(round_robin、pick_first)。
Q4:如何优雅关闭 gRPC 服务端?
A:使用 GracefulStop() 方法,它会等待所有正在处理的请求完成后再停止,避免请求被突然中断。
Q5:gRPC 与 TLS 如何配合?
A:使用 credentials.NewServerTLSFromFile 和 grpc.WithTransportCredentials 配置双向或单向 TLS。
Q6:如何处理 gRPC 跨域问题?
A:gRPC 本身不涉及 CORS,但 gRPC-Web 需配置 CORS。微服务间通常无需 CORS。
Q7:grpc.Dial 中的 WithInsecure 是什么意思?
A:表示不使用 TLS 加密。只能用于内部网络测试,生产环境绝不允许。
Q8:gRPC 的超时如何实现?
A:通过 Context 实现:ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
最佳实践
- 使用拦截器:统一的日志、认证、链路追踪逻辑放在拦截器中
- 定义错误码:使用标准 gRPC 状态码(codes.NotFound、codes.InvalidArgument 等)
- 超时和重试:客户端配置合理的超时,服务端幂等设计支持重试
- 连接复用:不要每次请求都新建连接,复用客户端连接
- 优雅关闭:应用退出前调用
GracefulStop() - 健康检查:暴露健康检查端点,供负载均衡器和监控使用
- TLS 加密:所有生产环境通信必须使用 TLS
- proto 版本管理:使用 buf 等工具管理 protobuf 版本和依赖
延伸阅读
- gRPC 官方文档
- Protocol Buffers 语言指南
- buf.build 现代 protobuf 工具链
- 《gRPC 与云原生应用开发》
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。