This repository has been archived on 2026-05-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
all-in-kingsoft/hhs/gRPC/3. 服务端实现/08-Server 搭建与注册.md
T
2026-05-11 19:02:38 +08:00

14 KiB
Raw Blame History

tags, create time
tags create time
gRPC
Go
Server
Production
2026-05-11 16:00

Server 搭建与注册

概述

gRPC server 搭建本身很简单——grpc.NewServer() 加一行 RegisterxxxServer() 就够了。但在生产环境中你需要考虑的东西很多:TLS 配置、优雅关闭、服务发现集成、多端口暴露、reflection 开关、健康检查。我们从一个最简单的 hello world 开始,逐步构建一个 production-ready 的 server。

[!question] 为什么生产环境需要关心 graceful shutdown? gRPC 基于 HTTP/2,连接是长连接。如果直接 kill 进程,所有正在处理的请求会突然断掉,client 端收到的是 TCP RST 而非一个干净的 finish。这在金融或订单系统中可能导致重复扣款、状态不一致。

最小可运行 Server

// cmd/server/main.go
package main

import (
	"log"
	"net"

	"go.uber.org/zap"
	"google.golang.org/grpc"
	"google.golang.org/grpc/reflection"

	pb "your/proto/gen/go"
)

func main() {
	logged, _ := zap.NewProduction()
	zap.ReplaceGlobals(logged)
	defer logged.Sync()

	lis, err := net.Listen("tcp", ":50051")
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}

	s := grpc.NewServer()
	pb.RegisterUserServiceServer(s, &userService{})

	// Reflection for debugging — production 应关闭
	reflection.Register(s)

	zap.L().Sugar().Infow("serving", "addr", lis.Addr())

	if err := s.Serve(lis); err != nil {
		log.Fatalf("failed to serve: %v", err)
	}
}

// userService 实现 pb.UserServiceServer interface
type userService struct {
	pb.UnimplementedUserServiceServer // 嵌入 zero-stub,天然支持未来 method 新增
}

核心就三步:监听端口 → 创建 server → 启动。但 UnimplementedUserServiceServer 这个嵌入类型很重要——它让你无需为每个 method 写空壳,proto 文件新增 RPC method 时编译期自动报错提醒,而不是静默忽略。

Server Options(ServerOption 精选)

grpc.NewServer(...) 接受任意数量的 ServerOption(函数式选项模式)。以下是高频选项速查表:

Option 用途 默认值 建议
grpc.Creds(credentials) TLS / mTLS 认证 明文 生产必配
grpc.MaxRecvMsgSize(n int) 单条接收消息上限 4MB 按需调大,注意内存
grpc.MaxSendMsgSize(n int) 单条发送消息上限 4MB 配合 MaxRecv 对称设置
grpc.MaxConcurrentStreams(n uint32) 单 stream 最大并发 HEADERS 100 防 DoS,放宽到 1024+
grpc.KeepaliveParams(kp) keepalive 心跳参数 2h idle timeout LB 后必须调整
grpc.ChainUnaryInterceptor(ints...) Unary 拦截器链 无 日志 / 鉴权 / 追踪
grpc.StatsHandler(h stats.Handler) OpenTelemetry 等 stats 注入 无 链路追踪 / metrics
s := grpc.NewServer(
    grpc.Creds(credentials.NewTLS(tlsConfig)),
    grpc.MaxRecvMsgSize(16*1024*1024),          // 16MB
    grpc.MaxConcurrentStreams(1024),             // 放宽并发流限制
    grpc.KeepaliveParams(keepalive.ServerParameters{
        MaxConnectionIdle:     15 * time.Minute,
        KeepaliveTime:         20 * time.Second,
        KeepaliveTimeout:      5 * time.Second,
        MinTimeBetweenPings:   10 * time.Second,
        PingWithoutCallsAllowed: true,
    }),
    grpc.ChainUnaryInterceptor(logging.Unary(), auth.Unary()),
    // grpc.StatsHandler(otelgrpc.NewServerHandler()), // OpenTelemetry Go SDK
)

[!tip] keepalive 参数调优经验 经过 LB(如 Envoy 或 Nginx)时,默认的 2 小时 idle timeout 会让 LB 提前切断连接,导致 client 侧出现 "connection reset" 错误。把 MaxConnectionIdle 设短到 15~20 分钟更合理——让 server 主动重建连接,确保两端对连接生命周期有一致认知。

直连无 LB 场景可以保持默认值,减少不必要的连接重建开销。

拦截器链执行顺序

拦截器以尾递归方式组合——先注册的 interceptor 包裹后注册的,形成洋葱模型:

flowchart LR
    Client["Client Request"] --> Logging["Logging Interceptor<br/>(最外层)"]
    Logging --> Auth["Auth Interceptor<br/>(内层)"]
    Auth --> Recovery["Recovery Interceptor<br/>(最内层)"]
    Recovery --> Handler["Actual Handler"]

    style Client fill:#E3F2FD
    style Handler fill:#FFF3E0
// 请求到达顺序:Logging → Auth → Recovery → Handler
// 响应返回顺序:Handler → Recovery → Auth → Logging
grpc.ChainUnaryInterceptor(
    logging.UnaryInterceptor(),   // 第 1 层(最外)
    auth.UnaryInterceptor(),      // 第 2 层
    recovery.UnaryInterceptor(),  // 第 3 层(最内)
)

设计建议:

  • 外层做横切关注点:日志、追踪、metrics
  • 中层做业务安全校验:鉴权、限流
  • 内层做兜底逻辑:panic recover、超时控制

Service Registration

什么是 Service Registration

每次 proto 文件中的 service 块都会生成两个 Go 类型:

  1. <ServiceName>Server interface —— 定义了你必须实现的 RPC method 集合
  2. Register<ServiceName>Server(server, impl) 函数 —— 将实现注册到 server 的 method dispatch table
// 你定义的 .proto:
// service UserService {
//   rpc CreateUser(CreateUserRequest) returns (CreateUserResponse);
//   rpc GetUser(GetUserRequest) returns (GetUserResponse);
// }

// 生成的接口(简化示意):
type UserServiceServer interface {
    CreateUser(context.Context, *pb.CreateUserRequest) (*pb.CreateUserResponse, error)
    GetUser(context.Context, *pb.GetUserRequest) (*pb.GetUserResponse, error)
    // UnimplementedUserServiceServer 提供 default implementation (Unimplemented)
    mustEmbedUnimplementedUserServiceServer()
}

// 注册函数签名:
func RegisterUserServiceServer(s *grpc.Server, srv UserServiceServer)

单 server 注册多 service

不需要为每个 service 创建独立 grpc.Server。单个 grpc.Server 实例天然支持多个 service registration:

s := grpc.NewServer(opts...)

pb.RegisterUserServiceServer(s, NewUserService())
pb.RegisterOrderServiceServer(s, NewOrderService())
pb.RegisterPaymentServiceServer(s, NewPaymentService())

s.Serve(lis) // 一个 listener 对外暴露三个 service

底层通过 method name 路由(例如 :method/user.v1.UserService/CreateUser),各 service 完全隔离互不干扰。

reflection:开发调试 vs 生产关闭

Reflection 允许外部工具在运行时查询已注册的 service descriptor,无需 .proto 文件:

import "google.golang.org/grpc/reflection"

reflection.Register(s) // 注册 gRPC Reflection v1alpha 服务

开发阶段好处:

  • grpcurl 无需 .proto 即可列出可用 API
  • IDE 自动补全 gRPC call
  • 快速验证某个 service 是否成功注册

生产环境关闭原因:

  • 暴露了完整的 API schema 信息(方法名、消息结构),增加攻击面
  • 额外占用内存保存所有 descriptor protobuf
  • 客户端可通过 reflection 探测内部方法名(即使是未公开的)
if os.Getenv("ENV") != "production" {
    reflection.Register(s) // 仅非生产环境开启
}

Multi-Port / Multi-Network

常见需求:内网接口和外网接口分开监听,或者同时暴露 IPv4 和 IPv6:

srv := grpc.NewServer(opts...)
pb.RegisterUserServiceServer(srv, svc)

lis1, _ := net.Listen("tcp", ":50051") // 内网
lis2, _ := net.Listen("tcp", ":50052") // 外网

go func() { _ = srv.Serve(lis1) }()
go func() { _ = srv.Serve(lis2) }()

<-ctx.Done()

[!warning] 同一个 grpc.Server 不能在同一时刻被多个 goroutine 同时调用 Serve()(不安全) 上面的写法在 gRPC-Go 中实际会导致 race condition。如果你的业务是按服务拆分到不同端口的需求,应该创建独立的 server 实例:

// 正确做法:每个端口使用独立的 grpc.Server 实例
srv1 := grpc.NewServer(opts...)
pb.RegisterUserServiceServer(srv1, userSvc)

srv2 := grpc.NewServer(opts...)
pb.RegisterUserServiceServer(srv2, userSvc)
pb.RegisterOrderServiceServer(srv2, orderSvc) // 外网额外暴露 OrderService

go func() { _ = srv1.Serve(net.Listen("tcp", ":50051")) }() // 内网:只暴露 User
go func() { _ = srv2.Serve(net.Listen("tcp", ":50052")) }() // 外网:暴露 User + Order

<-ctx.Done()
srv1.Stop()
srv2.Stop()

这样每个 server 实例管理自己的 listener 和连接池,彼此隔离互不影响。按服务粒度差异化暴露端口是微服务架构中的常见模式——内网服务只暴露核心 RPC,网关层再聚合多服务。

Graceful Shutdown(重要!)

这是最容易踩坑的部分。gRPC 提供了两种停止方法:

// 优雅停止:拒绝新连接 + 等待活跃流完成
s.GracefulStop()

// 立即停止:直接断开所有连接(未完成请求会报 error)
s.Stop()

典型的生产模式是结合 signal handling + context 超时控制:

func run(ctx context.Context, s *grpc.Server) error {
	go func() {
		<-ctx.Done()
		log.Println("shutdown signal received")
		s.GracefulStop()
	}()

	return s.Serve(lis)
}

// 30s 超时: GracefulStop 后最多等 30s,超时则强制退出
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

if err := run(ctx, s); err != nil {
	return err
}
return nil

为什么需要 30s 超时? GracefulStop 没有内置超时机制——如果某个 handler goroutine 因为缺少 ctx.Done() 检测而永远不返回,它会阻塞整个关闭流程。超时兜底确保进程最终能退出(操作系统会重新发送 SIGKILL)。

GracefulStop 的执行流程如下:

flowchart TD
    SIGTERM["SIGTERM Signal"] --> Handler["Signal Handler"]
    Handler --> StopNew["停止接收新连接<br/>新请求返回 UNAVAILABLE"]
    StopNew --> Drain["排空活跃 Stream<br/>等待 handler 返回"]
    Drain --> Check{"所有流<br/>全部完成?"}
    Check -->|"超时"| Force["强制关闭<br/>可能丢失未完成请求"]
    Check -->|"是"| CleanExit["干净退出<br/>所有回调执行完毕"]

    style CleanExit fill:#00D866,color:#fff
    style Force fill:#EE5A24,color:#fff

[!danger] GracefulStop 不是万能的 如果你的 handler 中没有正确检测 ctx.Done(),handler goroutine 永远不会结束,GracefulStop 最终也会卡住。所以 streaming handler 里必须遵循 ctx 驱动模式——见下一篇文章。

完整生产级模板

汇总成一个可直接复用的 bootstrap 骨架:

package main

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

	"go.uber.org/zap"
	"google.golang.org/grpc"
	grpc_health_v1 "google.golang.org/grpc/health/grpc_health_v1"
	"google.golang.org/grpc/health"
	"google.golang.org/grpc/keepalive"
	"google.golang.org/grpc/reflection"

	pb "your/proto/gen/go"
)

func NewGRPCServer(opts ...grpc.ServerOption) (*grpc.Server, error) {
	// 初始化 logger(defer Sync 由调用方管理生命周期)
	logger, _ := zap.NewProduction()
	zap.ReplaceGlobals(logger)

	creds, err := loadTLS()
	if err != nil {
		return nil, fmt.Errorf("tls: %w", err)
	}

	allOpts := []grpc.ServerOption{
		grpc.Creds(creds),
		grpc.MaxRecvMsgSize(16 * 1024 * 1024),
		grpc.MaxConcurrentStreams(1024),
		grpc.KeepaliveParams(keepalive.ServerParameters{
			MaxConnectionIdle:     15 * time.Minute,
			KeepaliveTime:         20 * time.Second,
			KeepaliveTimeout:      5 * time.Second,
		}),
		grpc.ChainUnaryInterceptor(
			logging.UnaryInterceptor(),
			auth.UnaryInterceptor(),
		),
	}
	allOpts = append(allOpts, opts...)

	srv := grpc.NewServer(allOpts...)

	// register services
	pb.RegisterUserServiceServer(srv, NewUserService())
	pb.RegisterOrderServiceServer(srv, NewOrderService())

	// health check
	hs := health.NewServer()
	hs.SetServingStatus("", grpc_health_v1.HealthCheckResponse_SERVING)
	grpc_health_v1.RegisterHealthServer(srv, hs)

	// reflection: dev only
	if os.Getenv("ENV") != "production" {
		reflection.Register(srv)
	}

	return srv, nil
}

func main() {
	ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
	defer stop()

	srv, err := NewGRPCServer()
	if err != nil {
		log.Fatal(err)
	}

	lis, err := net.Listen("tcp", ":50051")
	if err != nil {
		log.Fatal(err)
	}

	errCh := make(chan error, 1)
	go func() {
		errCh <- srv.Serve(lis)
	}()

	select {
	case err := <-errCh:
		return
	case <-ctx.Done():
		// GracefulShutdown with timeout
		shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
		defer cancel()

		done := make(chan struct{})
		go func() {
			srv.GracefulStop()
			close(done)
		}()

		select {
		case <-done:
			log.Println("server stopped gracefully")
		case <-shutdownCtx.Done():
			log.Println("shutdown timeout, forcing stop")
			srv.Stop()
		}
	}
}

这个模板涵盖了:TLS、大小限制、keepalive、拦截器链、健康检查、reflection 条件开关、signal handling、graceful shutdown + 超时兜底。复制粘贴后即可投入生产使用。

关键概念对照表

概念 对应 API 一句话总结
Server 创建 grpc.NewServer(...) 传入 ServerOption 配置行为
Service 注册 RegisterXxxServer(s, impl) 将实现绑定到 server dispatch table
零-stub 兼容 嵌入 UnimplementedXxxServer proto 新增 method 编译期自动告警
健康检查 health.NewServer() Kubernetes probe / SLA 监控对接
调试反射 reflection.Register(s) 仅限开发环境
优雅关闭 GracefulStop() + 超时 等活跃请求完成,超时无情斩断

关联笔记

  • hhs/gRPC/3. 服务端实现/09-Streaming Handler
  • hhs/gRPC/3. 服务端实现/10-健康检查与反射
  • hhs/gRPC/1. 基础概念/02-gRPC 核心术语