gRPC:构建高性能微服务

gRPC 是基于 HTTP/2 和 Protocol Buffers 的高性能 RPC 框架,支持四种通信模式和拦截器链。本文详解微服务间通信实现、流式上传和认证中间件

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 开发需要以下插件:

  1. protoc-gen-go:生成 Go 的结构体和方法,对应 protobuf 消息类型
  2. 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 支持两种拦截器:

  1. UnaryInterceptor:一元调用的拦截器
  2. 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 分为两类:

  1. Incoming metadata:从客户端传递到服务器的元数据
  2. 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 支持以下负载均衡策略:

  1. pick_first:默认策略,连接到第一个可用地址
  2. round_robin:轮询策略,依次连接所有地址
  3. 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

特性gRPCREST (HTTP/1.1)REST (HTTP/2)GraphQL
协议HTTP/2HTTP/1.1HTTP/2HTTP/1.1 或 HTTP/2
序列化Protobuf (二进制)JSON (文本)JSON (文本)JSON (文本)
消息大小约 JSON 的 1/3较大较大较大
传输效率极高低中低
强类型是(proto)否否Schema
代码生成自动生成无无可生成
流式传输双向流无Server PushSubscription
浏览器支持需 gRPC-Web原生原生原生
调试难度较难(二进制)容易(文本)容易(文本)容易(文本)

基准测试数据(Ping-Pong,同机测试):

框架吞吐量 (req/s)平均延迟 (us)P99 延迟 (us)
gRPC (Unary)150,000+~80~200
gRPC (Streaming)200,000+~60~150
HTTP/1.1 REST30,000~600~2,000
HTTP/2 REST50,000~300~800
GraphQL25,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 支持多种服务发现机制:

  1. 静态地址:最简单,直接写在客户端配置中
  2. DNS:gRPC 支持 DNS SRV 记录解析
  3. Etcd/ZooKeeper:使用第三方解析器插件
  4. Consul:社区提供 Consul 解析器
  5. 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 文件版本管理

  1. 独立 proto 仓库:将公共 proto 放在独立仓库中,所有服务引用
  2. 语义化版本:proto 变更遵循 SemVer 规范
  3. 兼容性检查:CI 中使用 protolock 等工具检查破坏性变更
  4. 文档注释:为每条消息和字段添加详细注释,描述其用途和约束
  5. 保留字段:删除字段时使用 reserved 保留编号

服务发现集成

  1. 健康检查:所有 gRPC 服务都应实现健康检查接口
  2. 优雅关闭:服务关闭前发送 NOT_SERVING 状态,给客户端时间路由到其他实例
  3. 连接池:客户端应维护连接池,避免每次调用新建连接
  4. 重试策略:配置合理的重试次数、退避策略和超时

优雅关闭

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:

  1. 四种通信模式:Unary、Server Streaming、Client Streaming、Bidirectional Streaming
  2. 服务定义:编写 .proto 文件,生成代码
  3. 服务器实现:Unary RPC、服务器流、双向流
  4. 客户端调用:同步调用、流式调用
  5. 拦截器链:日志、认证等中间件
  6. metadata:元数据传递
  7. 性能对比:gRPC vs REST vs GraphQL

gRPC 为微服务通信提供了高性能、强类型的解决方案。在构建复杂的分布式系统时,gRPC 是一个值得考虑的核心技术选型。

练习时间

  1. 实现一个 gRPC 网关,将 gRPC 服务暴露为 REST API
  2. 创建一个带有认证和授权的 gRPC 服务
  3. 实现 gRPC 服务的服务发现和负载均衡
  4. 使用 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 的性能对比

指标gRPCREST (HTTP/1.1)REST (HTTP/2)
序列化效率二进制 protobuf文本 JSON文本 JSON
Payload 大小小 (二进制)大 (文本)大 (文本)
连接复用HTTP/2 自带需要额外实现原生支持
流式支持四种模式模拟流服务端推送
代码生成proto 自动生成手动维护手动维护
浏览器支持需 gRPC-Web原生支持原生支持
调试便利性需要工具curl 即可curl 即可

对于内部服务通信,gRPC 在性能和效率上优势明显。对于面向浏览器的 API,REST 仍然是更好的选择。


进阶概念

四种通信模式

gRPC 支持四种通信模式,适合不同的业务场景:

  1. Unary RPC(一元请求):最常见的模式,客户端发送单个请求,服务端返回单个响应。类似 HTTP REST 调用。
  2. Server Streaming RPC:客户端发送单个请求,服务端返回流式响应。适合服务端数据量大的场景,如文件下载、数据同步。
  3. Client Streaming RPC:客户端发送流式请求,服务端返回单个响应。适合大数据上传、批量数据处理。
  4. 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

特性gRPCRESTGraphQL
协议HTTP/2HTTP/1.1 或 HTTP/2HTTP/1.1 或 HTTP/2
序列化Protocol BuffersJSONJSON
性能高(二进制、小体积)中(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)

最佳实践

  1. 使用拦截器:统一的日志、认证、链路追踪逻辑放在拦截器中
  2. 定义错误码:使用标准 gRPC 状态码(codes.NotFound、codes.InvalidArgument 等)
  3. 超时和重试:客户端配置合理的超时,服务端幂等设计支持重试
  4. 连接复用:不要每次请求都新建连接,复用客户端连接
  5. 优雅关闭:应用退出前调用 GracefulStop()
  6. 健康检查:暴露健康检查端点,供负载均衡器和监控使用
  7. TLS 加密:所有生产环境通信必须使用 TLS
  8. proto 版本管理:使用 buf 等工具管理 protobuf 版本和依赖

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「golang」更多文章

  1. 《Go 语言编程实战》目录
  2. 《Go 语言编程实战》18.3 上线、观测与迭代
  3. 《Go 语言编程实战》18.2 故障演练