高级4-5 小时·微服务

Go gRPC 微服务

使用 gRPC + Protocol Buffers 构建一个带服务发现、负载均衡、拦截器的微服务

gogrpcprotobufmicroserviceinterceptor

实战案例:Go gRPC 微服务

场景描述:构建一个基于 gRPC 和 Protocol Buffers 的完整微服务系统,该系统具备动态服务发现、客户端负载均衡和可插拔拦截器,解决了在分布式环境中服务间高效、可靠、可观测通信的核心问题。

你将学到

  1. 使用 Protocol Buffers 定义 gRPC 服务:掌握 .proto 文件编写及代码生成。
  2. 实现一个生产级 gRPC 服务端:包括多服务注册、优雅关闭。
  3. 构建智能 gRPC 客户端:集成 Consul 服务发现、客户端负载均衡。
  4. 开发与注册 gRPC 拦截器:实现统一的日志、认证、监控等横切关注点。
  5. 设计可扩展的微服务架构:将通信、发现、治理组件清晰分层。

前置知识

  1. 熟练掌握 Go 语言基础语法、结构体、接口和错误处理。
  2. 理解基本的网络编程概念(TCP/IP, HTTP/2)。
  3. 了解微服务架构的基本思想。
  4. 建议学习:

架构设计

本案例将构建一个“订单查询”服务。系统由三个核心部分组成:服务端、客户端和服务发现/注册中心(Consul)。

+--------------------+      gRPC (HTTP/2)       +---------------------+
|                    | -----------------------> |                     |
|   gRPC Client      |                          |   gRPC Server (xN)  |
|  (OrderClient)     | <--- (负载均衡轮询) ----  | (OrderService)      |
|                    |                          |                     |
+--------------------+                          +---------------------+
        |                                               |
        | (查询可用实例) | (注册/健康检查)         |
        |                                               |
        v                                               v
+---------------------------------------------------------------+
|                        Consul Cluster                         |
|  (服务发现与健康检查的中心化存储)                               |
+---------------------------------------------------------------+

拦截器在客户端与服务端均被插入到请求/响应处理链中,用于日志、认证等。

组件职责

  • server/:实现 OrderService,启动时向 Consul 注册,并周期性发送心跳。
  • client/:从 Consul 获取所有 OrderService 的健康实例,通过 gRPC 的 round_robin 策略进行负载均衡调用。
  • 拦截器UnaryServerInterceptorUnaryClientInterceptor,分别应用于服务端和客户端。

实现步骤

步骤 0:环境准备

确保已安装 Go、Protocol Buffers 编译器 (protoc)、gRPC Go 插件以及 Consul。

# 安装 protoc (macOS 示例)
brew install protobuf
# 安装 Go 的 gRPC 和 protobuf 插件
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
# 安装并启动 Consul (开发模式)
consul agent -dev

步骤 1:定义 Protobuf 并生成代码

在项目根目录创建 proto/order.proto 文件。

proto/order.proto:

syntax = "proto3";
package proto;
option go_package = "./proto";

// OrderService 处理订单相关的请求
service OrderService {
    // 根据ID获取订单详情
    rpc GetOrder(GetOrderRequest) returns (OrderResponse);
}

// GetOrderRequest 定义获取订单的请求参数
message GetOrderRequest {
    string order_id = 1;
}

// OrderResponse 定义订单的返回结构
message OrderResponse {
    string order_id = 1;
    string user_id = 2;
    float total_amount = 3;
    string status = 4;
    int64 created_at = 5;
}

在项目根目录运行生成命令:

protoc --go_out=. --go_opt=paths=source_relative \
       --go-grpc_out=. --go-grpc_opt=paths=source_relative \
       proto/order.proto

这将在 ./proto/ 目录下生成 order.pb.goorder_grpc.pb.go 文件。

步骤 2:实现 gRPC 服务端基础与注册

创建 server/main.go,实现服务逻辑并集成 Consul。

server/main.go:

package main

import (
	"context"
	"fmt"
	"log"
	"net"
	"os"
	"os/signal"
	"syscall"
	"time"

	"github.com/hashicorp/consul/api"
	"google.golang.org/grpc"
	"google.golang.org/grpc/health"
	"google.golang.org/grpc/health/grpc_health_v1"

	pb "your-module-name/proto" // 替换为你的模块名
)

const (
	serviceName = "order-service"
	consulAddr  = "127.0.0.1:8500"
	grpcPort    = 50051
)

// OrderServer 实现了 pb.OrderServiceServer 接口
type OrderServer struct {
	pb.UnimplementedOrderServiceServer
}

// GetOrder 是业务逻辑方法
func (s *OrderServer) GetOrder(ctx context.Context, req *pb.GetOrderRequest) (*pb.OrderResponse, error) {
	log.Printf("收到请求,订单ID: %s", req.GetOrderId())
	// 模拟数据库查询
	return &pb.OrderResponse{
		OrderId:    req.GetOrderId(),
		UserId:     "user-123",
		TotalAmount: 99.9,
		Status:     "completed",
		CreatedAt:  time.Now().Unix(),
	}, nil
}

// registerService 注册服务到 Consul
func registerService(consulClient *api.Client, serviceID string) error {
	registration := &api.AgentServiceRegistration{
		ID:      serviceID, // 服务唯一标识,如 `order-service-192.168.1.10-50051`
		Name:    serviceName,
		Port:    grpcPort,
		Address: getLocalIP(), // 获取本机IP,见下方函数
		Check: &api.AgentServiceCheck{
			GRPC:                           fmt.Sprintf("%s:%d", getLocalIP(), grpcPort),
			Interval:                       "10s", // 健康检查间隔
			Timeout:                        "3s",
			DeregisterCriticalServiceAfter: "30s", // 不健康后多久注销
		},
	}

	return consulClient.Agent().ServiceRegister(registration)
}

// getLocalIP 获取本机非环回IPv4地址
func getLocalIP() string {
	addrs, err := net.InterfaceAddrs()
	if err != nil {
		return "127.0.0.1"
	}
	for _, addr := range addrs {
		if ipNet, ok := addr.(*net.IPNet); ok && !ipNet.IP.IsLoopback() {
			if ipNet.IP.To4() != nil {
				return ipNet.IP.String()
			}
		}
	}
	return "127.0.0.1"
}

func main() {
	// 1. 初始化 gRPC 服务器
	lis, err := net.Listen("tcp", fmt.Sprintf(":%d", grpcPort))
	if err != nil {
		log.Fatalf("监听端口失败: %v", err)
	}

	// 创建 gRPC 服务器,并注册服务
	grpcServer := grpc.NewServer()
	pb.RegisterOrderServiceServer(grpcServer, &OrderServer{})

	// 注册健康检查服务(Consul 依赖它)
	healthServer := health.NewServer()
	grpc_health_v1.RegisterHealthServer(grpcServer, healthServer)
	healthServer.SetServingStatus(serviceName, grpc_health_v1.HealthCheckResponse_SERVING)

	// 2. 初始化 Consul 客户端并注册服务
	consulConfig := api.DefaultConfig()
	consulConfig.Address = consulAddr
	consulClient, err := api.NewClient(consulConfig)
	if err != nil {
		log.Fatalf("创建Consul客户端失败: %v", err)
	}

	serviceID := fmt.Sprintf("%s-%s-%d", serviceName, getLocalIP(), grpcPort)
	if err := registerService(consulClient, serviceID); err != nil {
		log.Fatalf("注册服务到Consul失败: %v", err)
	}
	log.Printf("服务 [%s] 已注册到 Consul,ID: %s", serviceName, serviceID)

	// 3. 启动 gRPC 服务器
	go func() {
		log.Printf("gRPC 服务器启动,监听端口: %d", grpcPort)
		if err := grpcServer.Serve(lis); err != nil {
			log.Fatalf("服务启动失败: %v", err)
		}
	}()

	// 4. 优雅关闭处理
	quit := make(chan os.Signal, 1)
	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
	<-quit
	log.Println("正在关闭服务...")

	// 将健康状态设为 NOT_SERVING
	healthServer.SetServingStatus(serviceName, grpc_health_v1.HealthCheckResponse_NOT_SERVING)

	// 从 Consul 注销
	if err := consulClient.Agent().ServiceDeregister(serviceID); err != nil {
		log.Printf("从Consul注销失败: %v", err)
	}

	// 停止 gRPC 服务器
	grpcServer.GracefulStop()
	log.Println("服务已停止")
}

步骤 3:实现带服务发现和负载均衡的客户端

创建 client/main.go

client/main.go:

package main

import (
	"context"
	"log"
	"time"

	"github.com/hashicorp/consul/api"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	"google.golang.org/grpc/resolver"

	pb "your-module-name/proto" // 替换为你的模块名
)

const (
	serviceName = "order-service"
	consulAddr  = "127.0.0.1:8500"
)

// consulResolver 实现了 gRPC 的 resolver.Builder 和 resolver.Resolver 接口
// 用于从 Consul 动态获取服务地址
type consulResolver struct {
	consulClient *api.Client
	serviceName  string
	cc           resolver.ClientConn
}

func (r *consulResolver) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (resolver.Resolver, error) {
	r.cc = cc
	r.serviceName = target.Endpoint()
	// 初始解析
	r.resolve()
	// 定期更新服务列表
	go r.watch()
	return r, nil
}

func (r *consulResolver) Scheme() string { return "consul" }

func (r *consulResolver) resolve() {
	services, _, err := r.consulClient.Health().Service(r.serviceName, "", true, nil)
	if err != nil {
		log.Printf("从Consul获取服务失败: %v", err)
		return
	}

	var addresses []resolver.Address
	for _, svc := range services {
		addr := svc.Service.Address
		if addr == "" {
			addr = svc.Node.Address
		}
		addresses = append(addresses, resolver.Address{
			Addr: fmt.Sprintf("%s:%d", addr, svc.Service.Port),
		})
	}

	r.cc.UpdateState(resolver.State{Addresses: addresses})
}

func (r *consulResolver) watch() {
	ticker := time.NewTicker(10 * time.Second) // 每10秒更新一次
	for range ticker.C {
		r.resolve()
	}
}

func main() {
	// 1. 初始化 Consul 客户端
	consulConfig := api.DefaultConfig()
	consulConfig.Address = consulAddr
	consulClient, err := api.NewClient(consulConfig)
	if err != nil {
		log.Fatalf("创建Consul客户端失败: %v", err)
	}

	// 2. 注册我们自定义的 resolver 到 gRPC
	resolver.Register(&consulResolver{consulClient: consulClient})

	// 3. 创建到服务的连接,使用轮询负载均衡策略
	// 注意:这里使用我们自定义的 scheme "consul"
	conn, err := grpc.Dial(
		"consul:///order-service", // 通过我们的 resolver 解析
		grpc.WithDefaultServiceConfig(`{"loadBalancingConfig": [{"round_robin":{}}]}`), // 启用轮询
		grpc.WithTransportCredentials(insecure.NewCredentials()), // 开发环境使用不安全连接
		grpc.WithBlock(),
		grpc.WithTimeout(5*time.Second),
	)
	if err != nil {
		log.Fatalf("连接服务失败: %v", err)
	}
	defer conn.Close()

	// 4. 创建客户端并调用
	client := pb.NewOrderServiceClient(conn)
	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
	defer cancel()

	for i := 0; i < 5; i++ {
		resp, err := client.GetOrder(ctx, &pb.GetOrderRequest{
			OrderId: fmt.Sprintf("order-%d", i),
		})
		if err != nil {
			log.Printf("调用 GetOrder 失败: %v", err)
			continue
		}
		log.Printf("收到响应: %+v", resp)
	}
}

步骤 4:添加拦截器实现统一日志

为服务端和客户端添加拦截器,记录请求信息。修改 server/main.goclient/main.go

server/main.gomain 函数中添加拦截器

// ... (在创建 grpcServer 之前)
// 定义服务端一元拦截器
func loggingServerInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
	start := time.Now()
	// 调用实际的 handler
	resp, err := handler(ctx, req)
	// 记录日志
	log.Printf("gRPC服务端拦截器 | 方法: %s | 耗时: %v | 错误: %v",
		info.FullMethod,
		time.Since(start),
		err)
	return resp, err
}

// 在 grpc.NewServer() 中传入拦截器
grpcServer := grpc.NewServer(
	grpc.UnaryInterceptor(loggingServerInterceptor),
)

client/main.gogrpc.Dial 中添加拦截器

// ... (在连接参数中添加)
// 定义客户端一元拦截器
func loggingClientInterceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
	start := time.Now()
	// 调用实际的远程方法
	err := invoker(ctx, method, req, reply, cc, opts...)
	// 记录日志
	log.Printf("gRPC客户端拦截器 | 方法: %s | 耗时: %v | 错误: %v",
		method,
		time.Since(start),
		err)
	return err
}

// 修改 Dial 调用
conn, err := grpc.Dial(
	"consul:///order-service",
	grpc.WithDefaultServiceConfig(`{"loadBalancingConfig": [{"round_robin":{}}]}`),
	grpc.WithTransportCredentials(insecure.NewCredentials()),
	grpc.WithUnaryInterceptor(loggingClientInterceptor), // 注册客户端拦截器
	grpc.WithBlock(),
	grpc.WithTimeout(5*time.Second),
)

完整项目结构

grpc-microservice-demo/
├── proto/
│   ├── order.proto          # Protobuf 定义文件
│   ├── order.pb.go          # 生成的消息代码
│   └── order_grpc.pb.go     # 生成的 gRPC 代码
├── server/
│   └── main.go              # gRPC 服务端,包含拦截器和 Consul 注册
├── client/
│   └── main.go              # gRPC 客户端,包含拦截器、服务发现和负载均衡
├── go.mod
└── go.sum

最佳实践

  1. API 版本控制:在 .protopackage 中加入版本号,如 package order.v1;,方便未来向后兼容升级。
  2. 配置外部化:将 Consul 地址、端口等提取到环境变量或配置文件中,不要硬编码。
  3. 结构化日志:使用 zaplogrus 等结构化日志库,并将关键上下文(如 requestID)从拦截器注入到日志中。
  4. 优雅关闭:务必实现服务的优雅关闭,确保在完成处理中的请求后再退出,并从服务注册中心注销。
  5. 健康检查:实现并暴露 gRPC 健康检查接口 (grpc.health.v1.Health),这是微服务治理(如负载均衡、熔断)的基础。

常见问题

Q1: 如何保证 .proto 文件的向后兼容性?

A: 只添加新字段,不删除或重新编号旧字段;新增的字段应有默认值。使用 reserved 关键字标记已废弃的字段编号,防止重用。

Q2: 客户端负载均衡为什么不生效?

A: 检查两点:1) grpc.WithDefaultServiceConfig 中的 JSON 配置格式是否正确;2) 服务是否成功在 Consul 中注册了多个健康实例。可以通过 Consul Web UI (http://localhost:8500) 查看。

Q3: 拦截器的执行顺序是怎样的?

A: 对于服务端,多个 UnaryServerInterceptor 会按注册的顺序执行(先注册先执行),形成一个链。对于客户端 UnaryClientInterceptor 也是类似。可以利用这个特性组合多个关注点。

扩展挑战

  1. 添加熔断器:集成 gobreakersony/gobreaker,在客户端拦截器中实现对下游服务调用的熔断保护。
  2. 集成分布式追踪:使用 OpenTelemetry 和 Jaeger,在拦截器中自动创建和传播 Trace Span,实现全链路追踪。
  3. 实现服务端流式 RPC:扩展 .proto,添加一个如 ListOrders 的服务端流式 RPC,并实现相应的服务端和客户端逻辑。

相关课程

课程相关章节
Go 语言基础接口 - 理解 gRPC 生成的 Server 接口和客户端接口。
Go 语言基础自定义错误与错误包装 - 在拦截器和业务逻辑中处理和返回规范的错误。
Go 语言基础项目结构与最佳实践 - 组织微服务代码的目录结构和模块依赖。
Go 语言基础Goroutine 基础 - 理解 gRPC 服务器如何并发处理请求。
Go 语言基础结构体与方法 - 实现 OrderServer 结构体并为其绑定方法。