4、慢查询场景模拟:构建一个模拟慢查询的REST API服务

在深入连接池调优之前,必须首先构建一个可复现的慢查询实验环境。本章将使用 Gin 框架搭建一个基础 REST API 服务,并通过 可控延迟注入 的方式模拟数据库慢查询行为,为后续的压测与调优提供稳定的基准服务。

4.1 服务架构与慢查询模拟原理

模拟慢查询的核心思路是在 API 处理函数中引入 人为延迟,替代真实的数据库查询耗时。下图展示了请求从进入 Gin 路由到返回响应的完整链路,以及延迟注入的位置。

慢查询模拟服务请求处理流程 客户端 Gin 路由 中间件 处理函数 延迟注入 连接池 延迟注入点:模拟慢查询 time.Sleep(duration) 连接池:管理数据库连接 database/sql + pgx

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 仅用于实验模拟。在生产环境中,慢查询应通过数据库索引优化、查询重构或缓存策略来解决,而非人为注入延迟。