4、慢查询场景模拟:构建一个模拟慢查询的REST API服务
在深入连接池调优之前,必须首先构建一个可复现的慢查询实验环境。本章将使用 Gin 框架搭建一个基础 REST API 服务,并通过 可控延迟注入 的方式模拟数据库慢查询行为,为后续的压测与调优提供稳定的基准服务。
4.1 服务架构与慢查询模拟原理
模拟慢查询的核心思路是在 API 处理函数中引入 人为延迟,替代真实的数据库查询耗时。下图展示了请求从进入 Gin 路由到返回响应的完整链路,以及延迟注入的位置。
4.2 项目结构与依赖
创建一个名为 slow-query-demo 的项目,目录结构如下:
slow-query-demo/
├── main.go # 服务入口
├── go.mod
├── go.sum
├── config/
│ └── config.go # 配置管理
├── handler/
│ └── user.go # 用户相关处理函数
├── middleware/
│ └── latency.go # 延迟注入中间件
└── model/
└── user.go # 数据模型
初始化 Go 模块并安装 Gin 框架:
go mod init slow-query-demo
go get github.com/gin-gonic/gin
go get github.com/lib/pq # PostgreSQL 驱动
go get github.com/jackc/pgx/v5 # 高性能 PG 驱动
4.3 基础服务搭建
在 main.go 中创建 Gin 引擎,注册路由并启动 HTTP 服务。以下代码实现了三个核心端点:
// main.go
package main
import (
"log"
"net/http"
"time"
"github.com/gin-gonic/gin"
"slow-query-demo/handler"
"slow-query-demo/middleware"
)
func main() {
r := gin.Default()
// 全局中间件:记录请求耗时
r.Use(middleware.RequestLogger())
// 用户相关路由组
userGroup := r.Group("/api/v1/users")
{
// 模拟慢查询:查询用户列表
userGroup.GET("/", handler.GetUsers)
// 模拟慢查询:查询单个用户
userGroup.GET("/:id", handler.GetUserByID)
// 快速查询:健康检查
userGroup.GET("/health", handler.HealthCheck)
}
srv := &http.Server{
Addr: ":8080",
Handler: r,
ReadTimeout: 30 * time.Second,
WriteTimeout: 30 * time.Second,
}
log.Printf("服务启动于 :8080")
if err := srv.ListenAndServe(); err != nil {
log.Fatalf("服务启动失败: %v", err)
}
}
4.4 延迟注入中间件
延迟注入是模拟慢查询的核心机制。通过 time.Sleep 在请求处理链路中插入可控延迟,模拟数据库查询耗时。
// middleware/latency.go
package middleware
import (
"log"
"time"
"github.com/gin-gonic/gin"
)
// LatencyInjector 延迟注入中间件
// delay: 模拟的查询延迟时间
func LatencyInjector(delay time.Duration) gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
// 在请求处理前注入延迟
time.Sleep(delay)
c.Next()
elapsed := time.Since(start)
log.Printf("[慢查询] %s %s | 总耗时: %v | 模拟延迟: %v",
c.Request.Method, c.Request.URL.Path, elapsed, delay)
}
}
// RequestLogger 请求日志中间件
func RequestLogger() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
c.Next()
elapsed := time.Since(start)
log.Printf("[请求] %s %s | 状态: %d | 耗时: %v",
c.Request.Method, c.Request.URL.Path, c.Writer.Status(), elapsed)
}
}
4.5 处理函数实现
在 handler/user.go 中实现具体的业务逻辑。每个处理函数都通过 time.Sleep 模拟不同级别的查询延迟。
// handler/user.go
package handler
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
"slow-query-demo/model"
)
// GetUsers 模拟慢查询:查询用户列表
// 模拟延迟:200ms ~ 500ms 随机
func GetUsers(c *gin.Context) {
// 模拟数据库查询延迟
time.Sleep(300 * time.Millisecond)
users := []model.User{
{ID: 1, Name: "Alice", Email: "alice@example.com"},
{ID: 2, Name: "Bob", Email: "bob@example.com"},
{ID: 3, Name: "Charlie", Email: "charlie@example.com"},
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"message": "success",
"data": users,
})
}
// GetUserByID 模拟慢查询:查询单个用户
// 模拟延迟:100ms ~ 300ms
func GetUserByID(c *gin.Context) {
id := c.Param("id")
// 模拟数据库查询延迟
time.Sleep(200 * time.Millisecond)
user := model.User{
ID: 1,
Name: "Alice",
Email: "alice@example.com",
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"message": "success",
"data": user,
"user_id": id,
})
}
// HealthCheck 快速查询:不模拟延迟
func HealthCheck(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"code": 0,
"message": "service is healthy",
"time": time.Now().Unix(),
})
}
4.6 数据模型定义
在 model/user.go 中定义用户数据结构:
// model/user.go
package model
// User 用户数据模型
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
4.7 慢查询场景配置
为了灵活控制慢查询的模拟参数,在 config/config.go 中定义配置结构:
// config/config.go
package config
import "time"
// SlowQueryConfig 慢查询模拟配置
type SlowQueryConfig struct {
// 是否启用慢查询模拟
Enabled bool `yaml:"enabled"`
// 基础延迟时间
BaseDelay time.Duration `yaml:"base_delay"`
// 延迟抖动范围(±)
Jitter time.Duration `yaml:"jitter"`
// 慢查询比例(0.0 ~ 1.0)
SlowRatio float64 `yaml:"slow_ratio"`
}
// DefaultConfig 返回默认配置
func DefaultConfig() *SlowQueryConfig {
return &SlowQueryConfig{
Enabled: true,
BaseDelay: 200 * time.Millisecond,
Jitter: 100 * time.Millisecond,
SlowRatio: 0.3, // 30% 的请求为慢查询
}
}
4.8 慢查询场景对比
下表总结了不同端点模拟的慢查询特征,方便后续压测时选择目标:
| 端点 | 模拟延迟 | 场景类型 | 连接占用时间 | 压测关注点 |
|---|---|---|---|---|
GET /api/v1/users |
300ms | 列表查询(慢) | ~300ms | 连接池耗尽速度 |
GET /api/v1/users/:id |
200ms | 单条查询(中) | ~200ms | 连接复用效率 |
GET /api/v1/users/health |
0ms | 健康检查(快) | <1ms | 基线对比 |
4.9 启动与验证
完成代码编写后,启动服务并验证慢查询效果:
# 启动服务
go run main.go
# 终端输出示例
[GIN-debug] Listening and serving HTTP on :8080
[请求] GET /api/v1/users/health | 状态: 200 | 耗时: 42.3µs
[慢查询] GET /api/v1/users | 总耗时: 301.2ms | 模拟延迟: 300ms
[慢查询] GET /api/v1/users/1 | 总耗时: 201.5ms | 模拟延迟: 200ms
验证要点:
- 健康检查端点响应时间应 < 1ms,作为快速查询的基线
- 慢查询端点响应时间应接近设定的延迟值(±10ms)
- 日志中应清晰区分慢查询与普通请求的标记
4.10 本章小结
通过本章的实践,我们成功构建了一个可复现的慢查询模拟服务。该服务具备以下关键特性:
- 可控延迟:通过
time.Sleep精确模拟 0ms ~ 500ms 的查询耗时 - 场景分级:提供慢、中、快三种查询场景,便于对比分析
- 可配置化:支持通过配置文件调整延迟参数,无需修改代码
- 日志可观测:每个请求的耗时和延迟信息均被完整记录
生产环境注意: 本服务中的
time.Sleep 仅用于实验模拟。在生产环境中,慢查询应通过数据库索引优化、查询重构或缓存策略来解决,而非人为注入延迟。