Skip to content

Docker 部署与微服务集成

本文从 Docker 化 Gin 应用出发,逐步讲解微服务架构下的服务通信、注册发现、API 网关与配置管理。所有方案均面向生产环境,包含可直接复用的 Dockerfile、docker-compose 与代码示例。

一、Docker 化 Gin 应用

1.1 编写 Dockerfile(多阶段构建)

Go 是编译型语言,构建产物是单一二进制文件,非常适合多阶段构建。最终镜像不需要 Go 工具链。

dockerfile
# 阶段1:构建
FROM golang:1.22-alpine AS builder

# 安装必要的工具(git 用于拉取依赖,ca-certificates 用于 HTTPS)
RUN apk add --no-cache git ca-certificates

WORKDIR /build

# 利用缓存:先复制 go.mod/go.sum,下载依赖
COPY go.mod go.sum ./
RUN GOPROXY=https://goproxy.cn,direct go mod download

# 复制源码
COPY . .

# 编译参数
ARG VERSION=dev
ARG BUILD_TIME
ARG CGO_ENABLED=0

# 静态编译,开启链接时优化(去除调试信息,减小体积)
RUN CGO_ENABLED=${CGO_ENABLED} GOOS=linux GOARCH=amd64 \
    go build -ldflags="-s -w -X main.Version=${VERSION} -X main.BuildTime=${BUILD_TIME}" \
    -o /app/server ./cmd/server

# 阶段2:运行时(使用 alpine)
FROM alpine:3.19

# 安装最小依赖
RUN apk --no-cache add ca-certificates tzdata && \
    cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
    echo "Asia/Shanghai" > /etc/timezone && \
    apk del tzdata

WORKDIR /app

# 复制二进制
COPY --from=builder /app/server .
# 复制静态资源(如有)
COPY --from=builder /build/configs ./configs

# 非 root 用户运行
RUN adduser -D -u 10001 appuser
USER appuser

EXPOSE 8080

# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
    CMD wget -qO- http://localhost:8080/health || exit 1

ENTRYPOINT ["./server"]

关键优化点:

  1. 多阶段构建:构建阶段使用 golang:1.22-alpine(约 400MB),最终镜像只有 alpine + 二进制(约 20-30MB)。
  2. -ldflags="-s -w":去除调试信息与符号表,二进制体积减小 30%。
  3. CGO_ENABLED=0:纯静态编译,可在 scratch 镜像运行。
  4. 缓存优化:先 COPY go.mod go.sumgo mod download,依赖不变时利用 Docker layer 缓存。
  5. 非 root 用户:安全最佳实践,避免容器逃逸后获得 root 权限。
  6. 时区配置:默认 alpine 是 UTC,国内服务需要设置时区。

1.2 优化镜像大小:scratch / alpine

dockerfile
# scratch 镜像:极致精简(约 15MB)
FROM scratch

# scratch 没有 CA 证书,需要从 builder 复制
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /app/server /server

# scratch 没有基础库,必须 CGO_ENABLED=0
ENTRYPOINT ["/server"]

scratch 与 alpine 对比:

镜像大小调试能力适用场景
golang~900MB完整 Go 环境开发调试
alpine~30MB有 shell、apk生产通用
scratch~15MB无 shell、无 libc极致优化、纯静态二进制
distroless~25MB无 shell、有 libc安全敏感(Google 出品)

scratch 的限制:

  • 没有 shell,无法 docker exec -it xxx sh 进入调试。
  • 没有 libc,必须 CGO_ENABLED=0
  • 没有时区数据,需手动复制 zoneinfo

1.3 docker-compose 编排

yaml
# docker-compose.yml
version: "3.9"

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
      args:
        VERSION: "1.0.0"
    image: myapp/server:1.0.0
    container_name: myapp-server
    restart: unless-stopped
    ports:
      - "8080:8080"
    environment:
      - GIN_MODE=release
      - DB_DSN=root:pass@tcp(mysql:3306)/myapp?parseTime=true
      - REDIS_ADDR=redis:6379
      - LOG_LEVEL=info
    volumes:
      - ./configs:/app/configs:ro
      - app-logs:/app/logs
    depends_on:
      mysql:
        condition: service_healthy
      redis:
        condition: service_started
    networks:
      - myapp-net
    deploy:
      resources:
        limits:
          cpus: "2.0"
          memory: 1G
        reservations:
          cpus: "0.5"
          memory: 256M

  mysql:
    image: mysql:8.0
    container_name: myapp-mysql
    restart: unless-stopped
    environment:
      MYSQL_ROOT_PASSWORD: pass
      MYSQL_DATABASE: myapp
    volumes:
      - mysql-data:/var/lib/mysql
    ports:
      - "3306:3306"
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - myapp-net

  redis:
    image: redis:7-alpine
    container_name: myapp-redis
    restart: unless-stopped
    ports:
      - "6379:6379"
    volumes:
      - redis-data:/data
    networks:
      - myapp-net

volumes:
  mysql-data:
  redis-data:
  app-logs:

networks:
  myapp-net:
    driver: bridge

二、微服务架构基础

2.1 单体到微服务的演进

单体架构的问题:

  • 代码膨胀:所有功能在一个仓库,编译慢, IDE 卡顿。
  • 发布耦合:一个小改动需要重新部署整个系统。
  • 技术栈僵化:难以对单个模块使用不同语言/数据库。
  • 扩展困难:高负载模块无法独立扩容。

微服务的核心思想:按业务能力拆分,独立部署,轻量通信

单体架构:
┌─────────────────────────────┐
│ 用户服务 / 订单服务 / 商品服务  │
│   共享 DB / 共享代码 / 共享部署 │
└─────────────────────────────┘

微服务架构:
┌──────────┐  ┌──────────┐  ┌──────────┐
│ 用户服务  │  │ 订单服务  │  │ 商品服务  │
│  Gin     │  │  Gin     │  │  Gin     │
│  MySQL   │  │  MySQL   │  │  MySQL   │
└──────────┘  └──────────┘  └──────────┘
     ▲             ▲             ▲
     │  HTTP/gRPC  │             │
     └─────────────┴─────────────┘
              API 网关

2.2 Gin 在微服务中的角色

Gin 适合作为微服务的 HTTP API 层

  • 对外 RESTful API(供前端、第三方调用)
  • 内部 HTTP 通信(服务间简单调用)
  • 健康检查、指标暴露、运维端点

对于高性能内部 RPC,推荐 gRPC(详见下节)。Gin 与 gRPC 可共存于同一服务。

三、gRPC 集成

3.1 gRPC 服务定义

定义 protobuf 文件:

protobuf
// proto/user.proto
syntax = "proto3";

package user;
option go_package = "./proto;userpb";

service UserService {
    rpc GetUser(GetUserReq) returns (GetUserResp);
    rpc ListUsers(ListUsersReq) returns (ListUsersResp);
    rpc CreateUser(CreateUserReq) returns (CreateUserResp);
}

message GetUserReq {
    int64 id = 1;
}

message User {
    int64 id = 1;
    string name = 2;
    string email = 3;
    int32 age = 4;
}

message GetUserResp {
    User user = 1;
}

message ListUsersReq {
    int32 page = 1;
    int32 size = 2;
}

message ListUsersResp {
    repeated User users = 1;
    int32 total = 2;
}

message CreateUserReq {
    string name = 1;
    string email = 2;
    int32 age = 3;
}

message CreateUserResp {
    int64 id = 1;
}

生成 Go 代码:

bash
protoc --go_out=. --go-grpc_out=. proto/user.proto

实现 gRPC 服务:

go
package server

import (
    "context"
    "database/sql"

    pb "myapp/proto"
)

type UserServer struct {
    pb.UnimplementedUserServiceServer
    db *sql.DB
}

func (s *UserServer) GetUser(ctx context.Context, req *pb.GetUserReq) (*pb.GetUserResp, error) {
    var u pb.User
    err := s.db.QueryRowContext(ctx,
        "SELECT id, name, email, age FROM users WHERE id = ?", req.Id).
        Scan(&u.Id, &u.Name, &u.Email, &u.Age)
    if err == sql.ErrNoRows {
        return nil, status.Errorf(codes.NotFound, "user not found")
    }
    if err != nil {
        return nil, status.Errorf(codes.Internal, err.Error())
    }
    return &pb.GetUserResp{User: &u}, nil
}

3.2 Gin 与 gRPC 共存:双端口服务

生产环境常见模式:对外 HTTP(Gin),对内 gRPC。

go
package main

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

    "google.golang.org/grpc"
    "github.com/gin-gonic/gin"
)

func main() {
    // 启动 gRPC 服务(内部 RPC)
    go startGRPC(":9090")

    // 启动 Gin HTTP 服务(对外 API)
    go startHTTP(":8080")

    // 优雅等待退出信号
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit
    log.Println("shutting down...")
}

func startGRPC(addr string) {
    lis, err := net.Listen("tcp", addr)
    if err != nil {
        log.Fatalf("gRPC listen: %v", err)
    }
    s := grpc.NewServer(
        grpc.UnaryInterceptor(grpcUnaryInterceptor),
    )
    pb.RegisterUserServiceServer(s, &UserServer{db: db})
    log.Printf("gRPC listening on %s", addr)
    if err := s.Serve(lis); err != nil {
        log.Fatalf("gRPC serve: %v", err)
    }
}

func startHTTP(addr string) {
    r := gin.New()
    r.Use(gin.Recovery())

    r.GET("/api/users/:id", func(c *gin.Context) {
        id, _ := strconv.ParseInt(c.Param("id"), 10, 64)
        // HTTP handler 可以直接调用本地 DB,或通过 gRPC 调用其他服务
        user, err := getUser(c, id)
        if err != nil {
            c.JSON(500, gin.H{"err": err.Error()})
            return
        }
        c.JSON(200, user)
    })

    server := &http.Server{
        Addr:    addr,
        Handler: r,
    }
    log.Printf("HTTP listening on %s", addr)
    if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
        log.Fatalf("HTTP serve: %v", err)
    }
}

// gRPC 拦截器:日志、链路追踪、错误处理
func grpcUnaryInterceptor(
    ctx context.Context,
    req interface{},
    info *grpc.UnaryServerInfo,
    handler grpc.UnaryHandler,
) (resp interface{}, err error) {
    start := time.Now()
    resp, err = handler(ctx, req)
    log.Printf("gRPC %s %v err=%v", info.FullMethod, time.Since(start), err)
    return
}

3.3 gRPC-Gateway 集成

gRPC-Gateway 可以从 protobuf 自动生成 RESTful API,实现「一份 proto 定义,同时支持 gRPC 与 HTTP」。

bash
# 安装
go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@latest

# 生成(在 proto 中添加 google.api.http 注解)
protoc \
    --grpc-gateway_out=. \
    --grpc-gateway_opt paths=source_relative \
    proto/user.proto

protobuf 添加 HTTP 注解:

protobuf
import "google/api/annotations.proto";

service UserService {
    rpc GetUser(GetUserReq) returns (GetUserResp) {
        option (google.api.http) = {
            get: "/api/users/{id}"
        };
    }
    rpc CreateUser(CreateUserReq) returns (CreateUserResp) {
        option (google.api.http) = {
            post: "/api/users"
            body: "*"
        };
    }
}

启动 gRPC-Gateway:

go
func startGateway(grpcAddr, httpAddr string) {
    ctx := context.Background()
    mux := runtime.NewServeMux()
    opts := []grpc.DialOption{grpc.WithInsecure()}
    err := pb.RegisterUserServiceHandlerFromEndpoint(ctx, mux, grpcAddr, opts)
    if err != nil {
        log.Fatal(err)
    }
    log.Printf("Gateway listening on %s", httpAddr)
    log.Fatal(http.ListenAndServe(httpAddr, mux))
}

四、服务注册与发现

4.1 集成 Consul

Consul 提供服务注册、发现、健康检查一体化方案。

go
package registry

import (
    "fmt"
    "log"
    "net"

    "github.com/hashicorp/consul/api"
)

type ConsulRegistry struct {
    client *api.Client
}

func NewConsul(addr string) (*ConsulRegistry, error) {
    cfg := api.DefaultConfig()
    cfg.Address = addr
    client, err := api.NewClient(cfg)
    if err != nil {
        return nil, err
    }
    return &ConsulRegistry{client: client}, nil
}

type ServiceInfo struct {
    ID      string
    Name    string
    Address string
    Port    int
    Tags    []string
    Check   *api.AgentServiceCheck
}

func (r *ConsulRegistry) Register(svc ServiceInfo) error {
    reg := &api.AgentServiceRegistration{
        ID:      svc.ID,
        Name:    svc.Name,
        Address: svc.Address,
        Port:    svc.Port,
        Tags:    svc.Tags,
        Check:   svc.Check,
    }
    return r.client.Agent().ServiceRegister(reg)
}

func (r *ConsulRegistry) Deregister(id string) error {
    return r.client.Agent().ServiceDeregister(id)
}

func (r *ConsulRegistry) Discover(name string) ([]*api.AgentService, error) {
    services, _, err := r.client.Health().Service(name, "", true, nil)
    if err != nil {
        return nil, err
    }
    result := make([]*api.AgentService, 0, len(services))
    for _, s := range services {
        result = append(result, s.Service)
    }
    return result, nil
}

// 在 main 中使用
func registerService() {
    reg, _ := NewConsul("consul:8500")

    // 获取本机 IP
    ip := getOutboundIP()
    port := 8080

    svc := ServiceInfo{
        ID:      fmt.Sprintf("user-service-%s-%d", ip, port),
        Name:    "user-service",
        Address: ip,
        Port:    port,
        Tags:    []string{"v1", "gin"},
        Check: &api.AgentServiceCheck{
            HTTP:     fmt.Sprintf("http://%s:%d/health", ip, port),
            Interval: "10s",
            Timeout:  "3s",
        },
    }

    if err := reg.Register(svc); err != nil {
        log.Fatal(err)
    }

    // 优雅注销
    go func() {
        quit := make(chan os.Signal, 1)
        signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
        <-quit
        reg.Deregister(svc.ID)
        os.Exit(0)
    }()
}

4.2 集成 Etcd

Etcd 是 Kubernetes 的核心组件,也常用于服务发现。

go
package registry

import (
    "context"
    "fmt"
    "time"

    clientv3 "go.etcd.io/etcd/v3/clientv3"
)

type EtcdRegistry struct {
    client *clientv3.Client
    lease  clientv3.LeaseID
}

func NewEtcd(endpoints []string) (*EtcdRegistry, error) {
    cli, err := clientv3.New(clientv3.Config{
        Endpoints:   endpoints,
        DialTimeout: 5 * time.Second,
    })
    if err != nil {
        return nil, err
    }
    return &EtcdRegistry{client: cli}, nil
}

func (r *EtcdRegistry) Register(name, addr string, ttl int64) error {
    // 申请租约
    resp, err := r.client.Grant(context.Background(), ttl)
    if err != nil {
        return err
    }
    r.lease = resp.ID

    key := fmt.Sprintf("/services/%s/%s", name, addr)
    _, err = r.client.Put(context.Background(), key, addr, clientv3.WithLease(r.lease))
    if err != nil {
        return err
    }

    // 自动续约
    ch, err := r.client.KeepAlive(context.Background(), r.lease)
    if err != nil {
        return err
    }
    go func() {
        for {
            select {
            case _, ok := <-ch:
                if !ok {
                    return
                }
            }
        }
    }()
    return nil
}

func (r *EtcdRegistry) Deregister(name, addr string) error {
    key := fmt.Sprintf("/services/%s/%s", name, addr)
    _, err := r.client.Delete(context.Background(), key)
    return r.client.Revoke(context.Background(), r.lease)
}

func (r *EtcdRegistry) Discover(name string) ([]string, error) {
    prefix := fmt.Sprintf("/services/%s/", name)
    resp, err := r.client.Get(context.Background(), prefix, clientv3.WithPrefix())
    if err != nil {
        return nil, err
    }
    addrs := make([]string, 0, len(resp.Kvs))
    for _, kv := range resp.Kvs {
        addrs = append(addrs, string(kv.Value))
    }
    return addrs, nil
}

4.3 健康检查端点

go
package main

import (
    "net/http"

    "github.com/gin-gonic/gin"
)

type HealthChecker interface {
    Check() error
}

type HealthHandler struct {
    checks map[string]HealthChecker
}

func NewHealthHandler() *HealthHandler {
    return &HealthHandler{checks: make(map[string]HealthChecker)}
}

func (h *HealthHandler) Add(name string, c HealthChecker) {
    h.checks[name] = c
}

// Liveness 存活检查:服务进程是否还在响应
func (h *HealthHandler) Liveness(c *gin.Context) {
    c.JSON(http.StatusOK, gin.H{"status": "alive"})
}

// Readiness 就绪检查:服务是否可以处理请求(依赖是否就绪)
func (h *HealthHandler) Readiness(c *gin.Context) {
    status := http.StatusOK
    result := gin.H{"status": "ok"}
    failed := false

    for name, checker := range h.checks {
        if err := checker.Check(); err != nil {
            result[name] = err.Error()
            failed = true
        } else {
            result[name] = "ok"
        }
    }

    if failed {
        result["status"] = "not ready"
        status = http.StatusServiceUnavailable
    }
    c.JSON(status, result)
}

// DB 健康检查
type DBChecker struct{ db *sql.DB }
func (c *DBChecker) Check() error {
    return c.db.Ping()
}

// Redis 健康检查
type RedisChecker struct{ client *redis.Client }
func (c *RedisChecker) Check() error {
    return c.client.Ping(context.Background()).Err()
}

// 使用
func main() {
    r := gin.New()
    hh := NewHealthHandler()
    hh.Add("db", &DBChecker{db: db})
    hh.Add("redis", &RedisChecker{client: rdb})

    r.GET("/health", hh.Liveness)
    r.GET("/ready", hh.Readiness)

    r.Run()
}

五、API 网关模式

5.1 反向代理中间件

Gin 可以作为轻量级 API 网关,通过 httputil.ReverseProxy 转发请求。

go
package gateway

import (
    "net/http"
    "net/http/httputil"
    "net/url"

    "github.com/gin-gonic/gin"
)

type Gateway struct {
    routes map[string]string // path prefix -> backend URL
}

func New() *Gateway {
    return &Gateway{
        routes: map[string]string{
            "/api/users":  "http://user-service:8080",
            "/api/orders": "http://order-service:8080",
            "/api/goods":  "http://goods-service:8080",
        },
    }
}

func (g *Gateway) Proxy() gin.HandlerFunc {
    return func(c *gin.Context) {
        backendURL := g.matchRoute(c.Request.URL.Path)
        if backendURL == "" {
            c.JSON(404, gin.H{"err": "no backend"})
            return
        }

        target, _ := url.Parse(backendURL)
        proxy := httputil.NewSingleHostReverseProxy(target)

        // 自定义 Director:保留原始路径,注入 header
        originalDirector := proxy.Director
        proxy.Director = func(req *http.Request) {
            originalDirector(req)
            req.Header.Set("X-Forwarded-For", c.ClientIP())
            req.Header.Set("X-Request-ID", c.GetString("request_id"))
            // 注入用户身份(由鉴权中间件解析后设置)
            if uid, ok := c.Get("user_id"); ok {
                req.Header.Set("X-User-ID", fmt.Sprintf("%v", uid))
            }
        }

        // 自定义错误处理:后端不可用时返回降级响应
        proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
            log.Printf("proxy error: %v", err)
            w.WriteHeader(http.StatusBadGateway)
            w.Write([]byte(`{"err":"service unavailable"}`))
        }

        proxy.ServeHTTP(c.Writer, c.Request)
    }
}

func (g *Gateway) matchRoute(path string) string {
    for prefix, backend := range g.routes {
        if strings.HasPrefix(path, prefix) {
            return backend
        }
    }
    return ""
}

// 使用
func main() {
    g := New()
    r := gin.New()
    r.Use(gin.Recovery())

    // 鉴权中间件
    r.Use(authMiddleware())

    // 所有请求转发到后端
    r.NoRoute(g.Proxy())
    r.Run(":8080")
}

5.2 动态路由(结合服务发现)

go
type DynamicGateway struct {
    registry *ConsulRegistry
    cache    sync.Map // path -> []*backend
}

func (g *DynamicGateway) Proxy() gin.HandlerFunc {
    return func(c *gin.Context) {
        serviceName := g.matchService(c.Request.URL.Path)
        if serviceName == "" {
            c.JSON(404, gin.H{"err": "no service"})
            return
        }

        // 从服务发现获取实例列表
        instances, err := g.registry.Discover(serviceName)
        if err != nil || len(instances) == 0 {
            c.JSON(503, gin.H{"err": "no instance"})
            return
        }

        // 简单轮询负载均衡
        target := instances[0].Address + ":" + strconv.Itoa(instances[0].Port)
        // ... 创建 proxy 转发
    }
}

更完善的负载均衡可用 grpc.NewClient 配合 roundrobin resolver。

六、配置管理

6.1 Viper 集成

Viper 支持多源配置:环境变量、文件、命令行、远程配置中心。

go
package config

import (
    "fmt"
    "strings"

    "github.com/spf13/viper"
)

type Config struct {
    App      AppConfig      `mapstructure:"app"`
    DB       DBConfig       `mapstructure:"db"`
    Redis    RedisConfig    `mapstructure:"redis"`
    Log      LogConfig      `mapstructure:"log"`
    Services ServicesConfig `mapstructure:"services"`
}

type AppConfig struct {
    Name string `mapstructure:"name"`
    Port int    `mapstructure:"port"`
    Mode string `mapstructure:"mode"`
}

type DBConfig struct {
    Driver          string `mapstructure:"driver"`
    DSN             string `mapstructure:"dsn"`
    MaxOpenConns    int    `mapstructure:"max_open_conns"`
    MaxIdleConns    int    `mapstructure:"max_idle_conns"`
    ConnMaxLifetime int    `mapstructure:"conn_max_lifetime"` // seconds
}

type RedisConfig struct {
    Addr     string `mapstructure:"addr"`
    Password string `mapstructure:"password"`
    DB       int    `mapstructure:"db"`
    PoolSize int    `mapstructure:"pool_size"`
}

type LogConfig struct {
    Level  string `mapstructure:"level"`
    Format string `mapstructure:"format"` // json / console
    Output string `mapstructure:"output"` // stdout / file
}

type ServicesConfig struct {
    User  string `mapstructure:"user"`
    Order string `mapstructure:"order"`
    Goods string `mapstructure:"goods"`
}

func Load(env string) (*Config, error) {
    v := viper.New()

    // 默认值
    v.SetDefault("app.port", 8080)
    v.SetDefault("app.mode", "release")
    v.SetDefault("db.max_open_conns", 100)
    v.SetDefault("redis.pool_size", 100)

    // 配置文件
    v.SetConfigName(fmt.Sprintf("config.%s", env))
    v.SetConfigType("yaml")
    v.AddConfigPath("./configs")
    v.AddConfigPath("/app/configs")
    v.AddConfigPath(".")

    if err := v.ReadInConfig(); err != nil {
        return nil, fmt.Errorf("read config: %w", err)
    }

    // 环境变量覆盖(最高优先级)
    v.AutomaticEnv()
    v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
    // 例如 DB_DSN 覆盖 db.dsn

    var cfg Config
    if err := v.Unmarshal(&cfg); err != nil {
        return nil, fmt.Errorf("unmarshal: %w", err)
    }
    return &cfg, nil
}

6.2 多环境配置

configs/
├── config.dev.yaml      # 开发环境
├── config.test.yaml     # 测试环境
├── config.staging.yaml  # 预发环境
└── config.prod.yaml     # 生产环境
yaml
# config.prod.yaml
app:
  name: user-service
  port: 8080
  mode: release

db:
  driver: mysql
  dsn: "user:pass@tcp(mysql.prod:3306)/user?parseTime=true&loc=Local"
  max_open_conns: 200
  max_idle_conns: 50
  conn_max_lifetime: 300

redis:
  addr: "redis.prod:6379"
  password: "${REDIS_PASSWORD}"  # 从环境变量注入
  db: 0
  pool_size: 200

log:
  level: info
  format: json
  output: stdout

services:
  user: "user-service:8080"
  order: "order-service:8080"
  goods: "goods-service:8080"

启动时通过环境变量切换:

go
func main() {
    env := os.Getenv("APP_ENV")
    if env == "" {
        env = "dev"
    }
    cfg, err := config.Load(env)
    if err != nil {
        log.Fatal(err)
    }
    // ... 使用 cfg
}

6.3 配置热更新

Viper 支持监听配置文件变化:

go
v.OnConfigChange(func(e fsnotify.Event) {
    log.Printf("config changed: %s", e.Name)
    var newCfg Config
    if err := v.Unmarshal(&newCfg); err != nil {
        log.Printf("reload config: %v", err)
        return
    }
    // 应用新配置(注意并发安全)
    applyConfig(newCfg)
})
v.WatchConfig()

七、完整微服务示例

7.1 项目结构

user-service/
├── cmd/
│   └── server/
│       └── main.go
├── internal/
│   ├── handler/      # HTTP/gRPC handler
│   ├── service/      # 业务逻辑
│   ├── repository/   # 数据访问
│   └── model/        # 数据模型
├── proto/            # protobuf 定义
├── configs/          # 配置文件
├── Dockerfile
├── docker-compose.yml
└── go.mod

7.2 主函数

go
package main

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

    "google.golang.org/grpc"
    "github.com/gin-gonic/gin"
)

func main() {
    // 加载配置
    env := os.Getenv("APP_ENV")
    cfg, err := config.Load(env)
    if err != nil {
        log.Fatal(err)
    }

    // 初始化依赖
    db := initDB(cfg.DB)
    rdb := initRedis(cfg.Redis)
    logger := initLogger(cfg.Log)

    // 服务注册
    reg, _ := registry.NewConsul(cfg.Consul.Addr)
    ip := getOutboundIP()
    svcID := fmt.Sprintf("%s-%s-%d", cfg.App.Name, ip, cfg.App.Port)
    _ = reg.Register(registry.ServiceInfo{
        ID:      svcID,
        Name:    cfg.App.Name,
        Address: ip,
        Port:    cfg.App.Port,
        Check: &api.AgentServiceCheck{
            HTTP:     fmt.Sprintf("http://%s:%d/health", ip, cfg.App.Port),
            Interval: "10s",
        },
    })
    defer reg.Deregister(svcID)

    // 启动 gRPC
    go startGRPC(cfg.GRPC.Port, db)

    // 启动 HTTP
    httpServer := startHTTP(cfg, db, rdb, logger)

    // 优雅关停
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit
    logger.Info("shutting down...")

    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    httpServer.Shutdown(ctx)
}

func startHTTP(cfg *config.Config, db *sql.DB, rdb *redis.Client, logger *zap.Logger) *http.Server {
    gin.SetMode(cfg.App.Mode)
    r := gin.New()
    r.Use(gin.Recovery(), zapMiddleware(logger))

    // 健康检查
    hh := NewHealthHandler()
    hh.Add("db", &DBChecker{db: db})
    r.GET("/health", hh.Liveness)
    r.GET("/ready", hh.Readiness)

    // 业务路由
    userRepo := repository.NewUserRepo(db)
    userSvc := service.NewUserService(userRepo, rdb)
    userHandler := handler.NewUserHandler(userSvc)

    api := r.Group("/api/v1")
    api.GET("/users/:id", userHandler.GetUser)
    api.POST("/users", userHandler.CreateUser)

    server := &http.Server{
        Addr:    fmt.Sprintf(":%d", cfg.App.Port),
        Handler: r,
    }
    go func() {
        if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            log.Fatal(err)
        }
    }()
    return server
}

八、常见陷阱与最佳实践

8.1 容器中的信号处理

容器停止时,Docker 发送 SIGTERM,Go 程序默认会立即退出。需要显式捕获并优雅关停:

go
// ❌ 错误:r.Run() 阻塞,无法处理信号
r.Run()

// ✅ 正确:用 http.Server 并手动管理生命周期
server := &http.Server{Addr: ":8080", Handler: r}
go server.ListenAndServe()
<-quit
server.Shutdown(ctx)

8.2 容器时区

dockerfile
# ❌ 容器默认 UTC,日志时间错误
FROM alpine

# ✅ 设置时区
RUN apk add --no-cache tzdata && \
    cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
    echo "Asia/Shanghai" > /etc/timezone

8.3 gRPC 与 HTTP 的选择

  • 对外 API:HTTP/RESTful(前端、第三方易用)
  • 内部高频调用:gRPC(性能高、契约严格)
  • 流式数据:gRPC stream(实时推拉)

不要一刀切用 gRPC,对外接口还是 HTTP 更友好。

8.4 服务发现的容量

Consul/Etcd 的服务发现查询频率要控制,频繁查询会拖垮注册中心。建议:

  • 客户端缓存实例列表,定期刷新(5-10s)
  • 用 watch 而非轮询(etcd 原生支持)
  • 大规模集群用 SDK 的连接池(grpc.ClientConn 自带负载均衡)

九、小结

本文系统讲解了 Gin 在微服务架构下的部署与集成:

  1. Docker 化:多阶段构建、scratch/alpine 镜像优化、docker-compose 编排。
  2. 微服务角色:Gin 作为 HTTP API 层,gRPC 作为内部 RPC 层,二者可共存。
  3. gRPC 集成:双端口服务、gRPC-Gateway 自动生成 RESTful API。
  4. 服务发现:Consul/Etcd 注册与发现,健康检查端点(Liveness/Readiness 分离)。
  5. API 网关:用 httputil.ReverseProxy 实现轻量级网关,结合服务发现做动态路由。
  6. 配置管理:Viper 多源配置、多环境分离、热更新。

下一篇我们将进入生产部署与架构最佳实践,讲解优雅关停、安全、监控与项目结构规范。