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

前两章我们讲了高可用入口的架构和 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-demo2.2 安装 Gin 依赖
bash
go get -u github.com/gin-gonic/gin2.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
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.14.2 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.2s4.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/tcp4.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配置:yamlservices: 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 linked5.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
EOF5.3.2 创建必要的目录
bash
mkdir -p /var/lib/gateway5.3.3 启动并设置开机自启
bash
systemctl daemon-reload
systemctl enable gateway
systemctl start gateway5.3.4 查看状态
bash
systemctl status gateway六、实践环境说明
本系列的实战环境使用以下家庭网络配置:
| 配置项 | 值 | 说明 |
|---|---|---|
| 网段 | 192.168.0.0/24 | 标准家庭局域网 |
| 网关 | 192.168.0.1 | 路由器地址 |
| VIP | 192.168.0.200 | 虚拟 IP,后续章节使用 |
| node1 | 192.168.0.201 | 节点1,优先级 100 |
| node2 | 192.168.0.202 | 节点2,优先级 90 |
| node3 | 192.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 /health | Nginx/Keepalived 探测 |
| 业务接口 | POST /api/v1/task | 模拟任务下发,验证身份 |
| 慢接口 | GET /api/slow | 测试超时和熔断 |
| 错误接口 | GET /api/error | 测试故障摘除 |
| 调试信息 | GET /info | 查看服务身份 |
并且提供了两种部署方式:
- Docker Compose:本地三节点模拟,适合开发调试
- Systemd 服务:生产环境部署,支持开机自启和自动恢复
下一章,我们将配置 Nginx 的健康检查和主动熔断,让 Nginx 能够动态摘除故障节点。
💡 本文是《分布式高可用入口架构实战系列》第 3 篇
