Skip to content

Golang + Gin 实现一个带健康检查的 Web 服务

Golang Gin 健康检查服务示意图

前两章我们讲了高可用入口的架构和 VRRP 协议原理。从本章开始,进入真正的实战环节。

我们将用 Go 语言和 Gin 框架实现一个能汇报自己身份的 Web 服务。这个服务有两个核心接口:

  • /health:健康检查接口,告诉 Nginx 自己是否活着
  • /api/v1/task:业务接口,模拟任务下发

后续章节中,我们会把这个服务部署到三台机器上,配合 Nginx + Keepalived 搭建完整的高可用入口。

一、为什么需要“会说话”的 Web 服务?

在搭建高可用入口时,我们需要一个能验证架构是否正常工作的后端服务。

这个服务需要具备以下能力:

能力用途
汇报身份告诉我们是哪台机器处理了请求(验证 VIP 漂移)
健康检查提供一个 /health 接口,供 Nginx/Keepalived 探测
模拟业务提供一个业务接口,验证反向代理是否正常工作
模拟故障提供慢接口、错误接口,测试熔断和摘除逻辑

这就是我们说的“会说话”的服务——它不仅能处理请求,还能告诉客户端“我是谁”。

二、项目初始化

2.1 创建项目目录

bash
mkdir -p $GOPATH/src/github.com/fishfinal/go-gateway-demo
cd $GOPATH/src/github.com/fishfinal/go-gateway-demo
go mod init github.com/fishfinal/go-gateway-demo

2.2 安装 Gin 依赖

bash
go get -u github.com/gin-gonic/gin

2.3 项目结构

go-gateway-demo/
├── go.mod
├── go.sum
├── main.go              # 主程序
├── Dockerfile           # 容器化部署(可选)
└── docker-compose.yml   # 三节点模拟(可选)

三、完整代码实现

3.1 主程序 main.go

go
package main

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

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

// Server 封装服务信息
type Server struct {
    hostname string
    localIP  string
    port     string
}

// NewServer 创建服务实例
func NewServer(port string) *Server {
    hostname, _ := os.Hostname()
    localIP := getLocalIP()
    return &Server{
        hostname: hostname,
        localIP:  localIP,
        port:     port,
    }
}

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

// setupRouter 配置路由
func (s *Server) setupRouter() *gin.Engine {
    r := gin.Default()

    // 健康检查接口(给 Nginx/Keepalived 用)
    r.GET("/health", func(c *gin.Context) {
        c.JSON(200, gin.H{
            "status":    "ok",
            "hostname":  s.hostname,
            "ip":        s.localIP,
            "timestamp": time.Now().Unix(),
        })
    })

    // 业务接口:模拟任务下发
    r.POST("/api/v1/task", func(c *gin.Context) {
        var req map[string]interface{}
        if err := c.BindJSON(&req); err != nil {
            c.JSON(400, gin.H{"error": "invalid request"})
            return
        }

        c.JSON(200, gin.H{
            "code":    0,
            "message": "task accepted",
            "processed_by": gin.H{
                "hostname": s.hostname,
                "ip":       s.localIP,
            },
            "task": req,
        })
    })

    // 模拟慢接口:测试超时场景
    r.GET("/api/slow", func(c *gin.Context) {
        time.Sleep(5 * time.Second)
        c.JSON(200, gin.H{
            "message":      "slow response",
            "processed_by": s.hostname,
        })
    })

    // 模拟错误接口:测试故障摘除
    r.GET("/api/error", func(c *gin.Context) {
        c.JSON(500, gin.H{
            "error":        "internal server error",
            "processed_by": s.hostname,
        })
    })

    // 调试接口:查看服务信息
    r.GET("/info", func(c *gin.Context) {
        c.JSON(200, gin.H{
            "hostname": s.hostname,
            "ip":       s.localIP,
            "port":     s.port,
        })
    })

    return r
}

// Run 启动服务
func (s *Server) Run() error {
    r := s.setupRouter()
    addr := ":" + s.port

    fmt.Printf("🚀 Server starting on %s\n", addr)
    fmt.Printf("📡 Hostname: %s\n", s.hostname)
    fmt.Printf("🌐 IP: %s\n", s.localIP)
    fmt.Printf("✅ Health check: http://localhost%s/health\n", addr)
    fmt.Printf("📋 Task API: POST http://localhost%s/api/v1/task\n", addr)

    return r.Run(addr)
}

func main() {
    // 从环境变量读取端口,默认 8080
    port := os.Getenv("PORT")
    if port == "" {
        port = "8080"
    }

    server := NewServer(port)

    // 优雅退出处理
    go func() {
        if err := server.Run(); err != nil {
            fmt.Printf("Server error: %v\n", err)
        }
    }()

    // 等待退出信号
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit

    fmt.Println("\n🛑 Shutting down server...")
}

3.2 代码结构说明

3.3 核心功能验证

启动服务:

bash
go run main.go

# 输出:
# 🚀 Server starting on :8080
# 📡 Hostname: ubuntu-node1
# 🌐 IP: 192.168.0.201
# ✅ Health check: http://localhost:8080/health
# 📋 Task API: POST http://localhost:8080/api/v1/task

验证健康检查:

bash
curl http://localhost:8080/health | jq .

# 响应:
{
  "status": "ok",
  "hostname": "ubuntu-node1",
  "ip": "192.168.0.201",
  "timestamp": 1749876543
}

验证业务接口:

bash
curl -X POST http://localhost:8080/api/v1/task \
  -H "Content-Type: application/json" \
  -d '{"task_id": "task-001", "target": "192.168.0.0/24"}' | jq .

# 响应:
{
  "code": 0,
  "message": "task accepted",
  "processed_by": {
    "hostname": "ubuntu-node1",
    "ip": "192.168.0.201"
  },
  "task": {
    "task_id": "task-001",
    "target": "192.168.0.0/24"
  }
}

验证慢接口:

bash
time curl http://localhost:8080/api/slow

# real    0m5.012s

四、三节点模拟环境(Docker Compose)

为了在一台电脑上模拟三台机器的集群,我们使用 Docker Compose + 自定义桥接网络。

4.1 docker-compose.yml

docker-compose.yml
yaml
services:
  node1:
    build: .
    container_name: gateway-node1
    environment:
      - PORT=8080
    networks:
      ha_net:
        ipv4_address: 192.168.0.201
    ports:
      - "8081:8080"   # 映射到宿主机,方便单独调试
    restart: unless-stopped

  node2:
    build: .
    container_name: gateway-node2
    environment:
      - PORT=8080
    networks:
      ha_net:
        ipv4_address: 192.168.0.202
    ports:
      - "8082:8080"
    restart: unless-stopped

  node3:
    build: .
    container_name: gateway-node3
    environment:
      - PORT=8080
    networks:
      ha_net:
        ipv4_address: 192.168.0.203
    ports:
      - "8083:8080"
    restart: unless-stopped

networks:
  ha_net:
    driver: bridge
    ipam:
      config:
        - subnet: 192.168.0.0/24
          gateway: 192.168.0.1

4.2 Dockerfile

Dockerfile
dockerfile
FROM golang:1.21-alpine AS builder

WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o gateway main.go

FROM alpine:3.18
RUN apk --no-cache add ca-certificates curl
WORKDIR /root/
COPY --from=builder /app/gateway .

EXPOSE 8080
CMD ["./gateway"]

4.3 启动三节点集群

4.3.1 构建并启动

bash
docker-compose up -d --build

具体的构建过程及启动日志类似于如下:

Compose can now delegate builds to bake for better performance.
 To do so, set COMPOSE_BAKE=true.
[+] Building 81.1s (27/37)                                                                      docker:desktop-linux
 => [node2 internal] load build definition from Dockerfile                                                      0.1s
 => => transferring dockerfile: 314B                                                                            0.0s
 => [node3 internal] load build definition from Dockerfile                                                      0.1s
 => => transferring dockerfile: 314B                                                                            0.0s
 => [node1 internal] load build definition from Dockerfile                                                      0.1s
 => => transferring dockerfile: 314B                                                                            0.0s
 => [node3 internal] load metadata for docker.io/library/alpine:3.24                                            0.1s
 => [node3 internal] load metadata for docker.io/library/golang:1.21-alpine                                     0.1s
 => [node2 internal] load .dockerignore                                                                         0.1s
...
[+] Running 4/4
 ✔ Network go-gateway-demo_ha_net  Created                                                                      0.1s
 ✔ Container gateway-node1         Started                                                                      1.3s
 ✔ Container gateway-node2         Started                                                                      1.5s
 ✔ Container gateway-node3         Started                                                                      1.2s

4.3.2 查看运行状态

bash
docker-compose ps

将输出类似如下信息:

NAME            IMAGE                   COMMAND       SERVICE   CREATED         STATUS         PORTS
gateway-node1   go-gateway-demo-node1   "./gateway"   node1     6 seconds ago   Up 4 seconds   0.0.0.0:8081->8080/tcp
gateway-node2   go-gateway-demo-node2   "./gateway"   node2     6 seconds ago   Up 4 seconds   0.0.0.0:8082->8080/tcp
gateway-node3   go-gateway-demo-node3   "./gateway"   node3     6 seconds ago   Up 4 seconds   0.0.0.0:8083->8080/tcp

4.3.3 分别测试三个节点

bash
# 分别测试三个节点
curl -s http://localhost:8081/health | jq .hostname
# "d3a2b1c4e5f6"  (实际输出为容器 ID 前12位,每次创建都不同)

curl -s http://localhost:8082/health | jq .hostname
# "a7b8c9d0e1f2"

curl -s http://localhost:8083/health | jq .hostname
# "f6e5d4c3b2a1"

💡 为什么不是 "gateway-node1"? Docker 容器的默认 hostname 是容器 ID 的前 12 位(如 c1d70aac6b5f),而不是 container_name。如果需要自定义 hostname,可以在 docker-compose.yml 中添加 hostname 配置:

yaml
services:
  node1:
    hostname: gateway-node1
    # ... 其他配置

添加后重启容器,/health 返回的 hostname 就会变成自定义值。不过对于功能验证来说,只要能区分三个节点是不同的容器,随机 ID 完全够用。

五、编译部署到物理机

5.1 交叉编译 Linux 二进制

bash
# 编译 Linux amd64 版本
GOOS=linux GOARCH=amd64 go build -o gateway main.go

# 查看文件信息
file gateway
# gateway: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked

5.2 部署到服务器

bash
# 上传到服务器(以 node1 为例)
scp gateway [email protected]:/usr/local/bin/

# 在服务器上运行
ssh [email protected]
chmod +x /usr/local/bin/gateway
./gateway &

# 验证
curl http://localhost:8080/health

💡 注意

需要在 192.168.0.201/202/203 三台服务器上分别部署,推荐使用下面的 systemd 方式进行部署!

5.3 配置为 Systemd 服务(生产推荐)

5.3.1 创建服务定义配置文件

bash
# /etc/systemd/system/gateway.service
cat > /etc/systemd/system/gateway.service << 'EOF'
[Unit]
Description=Gateway Health Check Service
After=network.target

[Service]
Type=simple
User=www-data
WorkingDirectory=/var/lib/gateway/
ExecStart=/usr/local/bin/gateway
Restart=always
RestartSec=5
Environment="PORT=8080"

[Install]
WantedBy=multi-user.target
EOF

5.3.2 创建必要的目录

bash
mkdir -p /var/lib/gateway

5.3.3 启动并设置开机自启

bash
systemctl daemon-reload
systemctl enable gateway
systemctl start gateway

5.3.4 查看状态

bash
systemctl status gateway

六、实践环境说明

本系列的实战环境使用以下家庭网络配置:

配置项说明
网段192.168.0.0/24标准家庭局域网
网关192.168.0.1路由器地址
VIP192.168.0.200虚拟 IP,后续章节使用
node1192.168.0.201节点1,优先级 100
node2192.168.0.202节点2,优先级 90
node3192.168.0.203节点3,优先级 80

💡 如果你的家庭网络网段不同(如 192.168.1.0/24),只需将所有 192.168.0.x 替换为你自己的网段即可。

七、与 Master 选举的集成(预告)

在后续的分布式扫描引擎集成章节中,我们会让这个服务与 etcd 选举联动:

go
// 伪代码示例
func (m *Master) Start() {
    if m.election.IsLeader() {
        // 成为 Master,创建标识文件
        os.Create("/tmp/is_master")
        // 启动调度逻辑
        go m.schedule()
    } else {
        os.Remove("/tmp/is_master")
    }
}

然后 Nginx 的健康检查脚本可以这样判断:

bash
#!/bin/bash
# 只有存在 /tmp/is_master 文件时,才返回 200
if [ -f /tmp/is_master ]; then
    exit 0
else
    exit 1
fi

这样 Nginx 就只会把请求转发给真正是 Master 的节点,实现“产品无感知”的高可用调度。

八、小结

本章我们完成了一个完整的 Golang + Gin 健康检查服务,具备以下能力:

功能接口用途
健康检查GET /healthNginx/Keepalived 探测
业务接口POST /api/v1/task模拟任务下发,验证身份
慢接口GET /api/slow测试超时和熔断
错误接口GET /api/error测试故障摘除
调试信息GET /info查看服务身份

并且提供了两种部署方式:

  • Docker Compose:本地三节点模拟,适合开发调试
  • Systemd 服务:生产环境部署,支持开机自启和自动恢复

下一章,我们将配置 Nginx 的健康检查和主动熔断,让 Nginx 能够动态摘除故障节点。


💡 本文是《分布式高可用入口架构实战系列》第 3 篇

最后更新2026/06/21 16:32
如果你觉得这篇文章有帮助,或者想聊聊技术、工作,欢迎通过下面方式联系我:
contact fishfinal