HTTP 定时健康检查:Go 语言实现多 URL 轮询监控

HTTP 定时健康检查:Go 语言实现多 URL 轮询监控

本文详解如何用 go 编写健壮的 http 定时探测程序,支持从文件读取 url 及对应间隔时间,通过 ticker 触发并发请求,并正确处理解析、超时、goroutine 闭包和资源释放等关键问题。

本文详解如何用 go 编写健壮的 http 定时探测程序,支持从文件读取 url 及对应间隔时间,通过 ticker 触发并发请求,并正确处理解析、超时、goroutine 闭包和资源释放等关键问题。

在构建服务健康监控系统时,定时向多个目标 Web 服务器发起 HTTP 请求并统计响应时间与状态码是常见需求。但初学者常因文件解析错误、 goroutine 闭包变量捕获、nil 指针解引用(如未检查 resp 是否为 nil)及资源未关闭等问题导致 panic 或逻辑异常。下面是一份生产就绪的实现方案。

✅ 正确解析带 Tab 分隔的 URL 配置文件

你的 url_list.txt 文件应严格使用 制表符 \t 分隔 URL 与周期(秒),且每行一个条目(注意:不是全文件用 \t 切分!):

http://google.com   5
http://nike.com 10
https://github.com  3

错误做法(原文中):

lines := strings.Split(string(content), "\t") // ❌ 将整个文件按 \t 拆,忽略换行!

正确做法:先按行拆分,再逐行按 \t 拆分字段:

lines := strings.Split(strings.TrimSpace(string(content)), "\n")
for _, line := range lines {
    if line == "" { continue }
    fields := strings.Split(line, "\t")
    if len(fields) < 2 {
        log.Printf("warn: invalid line (missing tab): %q", line)
        continue
    }
    url := strings.TrimSpace(fields[0])
    intervalSec, err := strconv.Atoi(strings.TrimSpace(fields[1]))
    if err != nil {
        log.Printf("warn: invalid interval for %q: %v", url, err)
        continue
    }
    urls = append(urls, struct{ URL string; Interval time.Duration }{
        URL:      url,
        Interval: time.Second * time.Duration(intervalSec),
    })
}

✅ 使用 Context 控制请求超时,避免永久阻塞

原代码中 http.Get(url) 无超时,可能使 goroutine 卡死。应使用 http.Client 配合 context.WithTimeout:

func getRespTime(url string, timeout time.Duration) (time.Duration, int, error) {
    ctx, cancel := context.WithTimeout(context.Background(), timeout)
    defer cancel()

    req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
    if err != nil {
        return 0, 0, fmt.Errorf("failed to create request: %w", err)
    }

    client := &http.Client{Timeout: timeout} // 双重保险
    start := time.Now()
    resp, err := client.Do(req)
    elapsed := time.Since(start)

    if err != nil {
        return elapsed, 0, fmt.Errorf("request failed: %w", err)
    }
    defer resp.Body.Close() // ✅ 必须在此处关闭,非 defer 在函数末尾(易被提前 return 跳过)

    return elapsed, resp.StatusCode, nil
}

✅ 安全启动并发 Worker,修复闭包陷阱

原代码中 for i := 0; i < len(lines)-1; i++ { go func() { … lines[i] … }() 是经典闭包陷阱:所有 goroutine 共享同一个 i 变量,循环结束时 i 已越界,导致 lines[i] panic。

✅ 正确写法(传参而非捕获):

for _, entry := range urls {
    wg.Add(1)
    go func(u string, interval time.Duration) {
        defer wg.Done()
        duration, status, err := getRespTime(u, 5*time.Second) // 默认 5s 超时
        if err != nil {
            log.Printf("[FAIL] %s → %v", u, err)
        } else {
            log.Printf("[OK] %s → %dms | %s | %d", u, duration.Milliseconds(), time.Now().Format("15:04:05"), status)
        }
        // 可选:休眠至下一周期(更精准的 ticker 行为)
        time.Sleep(interval)
    }(entry.URL, entry.Interval)
}

✅ 主循环:基于每个 URL 独立 ticker(推荐)或统一 ticker + 路由分发

若坚持单 ticker 统一调度(如每秒触发一次扫描),需在每次 tick 中遍历全部 URL 并发探测:

ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()

for {
    select {
    case <-ticker.C:
        var wg sync.WaitGroup
        for _, entry := range urls {
            wg.Add(1)
            go func(u string, interval time.Duration) {
                defer wg.Done()
                // 执行探测...
                duration, status, err := getRespTime(u, 5*time.Second)
                if err != nil {
                    log.Printf("[FAIL] %s → %v", u, err)
                } else {
                    log.Printf("[OK] %s → %dms | %d", u, duration.Milliseconds(), status)
                }
            }(entry.URL, entry.Interval)
        }
        wg.Wait() // 等待本轮全部完成
    }
}

⚠️ 注意:wg.Wait() 必须在 select 内部,否则主 goroutine 会阻塞,无法响应 ticker。

✅ 完整可运行示例(精简版)

package main

import (
    "context"
    "fmt"
    "io/ioutil"
    "log"
    "net/http"
    "strconv"
    "strings"
    "sync"
    "time"
)

type Target struct {
    URL      string
    Interval time.Duration
}

func getRespTime(url string, timeout time.Duration) (time.Duration, int, error) {
    ctx, cancel := context.WithTimeout(context.Background(), timeout)
    defer cancel()

    req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
    if err != nil {
        return 0, 0, err
    }

    client := &http.Client{Timeout: timeout}
    start := time.Now()
    resp, err := client.Do(req)
    elapsed := time.Since(start)
    if err != nil {
        return elapsed, 0, err
    }
    defer resp.Body.Close()
    return elapsed, resp.StatusCode, nil
}

func main() {
    content, err := ioutil.ReadFile("url_list.txt")
    if err != nil {
        log.Fatal("failed to read url_list.txt:", err)
    }

    var targets []Target
    lines := strings.Split(strings.TrimSpace(string(content)), "\n")
    for _, line := range lines {
        if line == "" { continue }
        fields := strings.Split(line, "\t")
        if len(fields) < 2 { continue }
        url := strings.TrimSpace(fields[0])
        sec, err := strconv.Atoi(strings.TrimSpace(fields[1]))
        if err != nil || sec <= 0 { continue }
        targets = append(targets, Target{
            URL:      url,
            Interval: time.Second * time.Duration(sec),
        })
    }
    if len(targets) == 0 {
        log.Fatal("no valid targets loaded")
    }

    log.Println("Starting HTTP health check with", len(targets), "targets...")
    ticker := time.NewTicker(1 * time.Second)
    defer ticker.Stop()

    for range ticker.C {
        var wg sync.WaitGroup
        for _, t := range targets {
            wg.Add(1)
            go func(u string, interval time.Duration) {
                defer wg.Done()
                dur, status, err := getRespTime(u, 5*time.Second)
                if err != nil {
                    log.Printf("❌ %s | %v", u, err)
                } else {
                    log.Printf("✅ %s | %dms | %d | %s",
                        u, int64(dur.Microseconds())/1000, status, time.Now().Format("15:04:05"))
                }
            }(t.URL, t.Interval)
        }
        wg.Wait()
    }
}

? 总结与最佳实践

  • 永远先按 \n 再按 \t 解析配置文件,并做空行/字段长度校验;
  • 所有 HTTP 请求必须设置超时(context.WithTimeout + http.Client.Timeout);
  • resp.Body.Close() 必须在获取 resp 后立即 defer,防止连接泄漏;
  • goroutine 中引用循环变量,务必显式传参,避免闭包陷阱;
  • ✅ 日志建议包含时间戳、URL、耗时(ms)、状态码,便于聚合分析;
  • ✅ 生产环境建议增加重试机制、失败告警(如邮件/Webhook)、Prometheus 指标暴露。

此方案兼顾简洁性与健壮性,可直接用于轻量级服务探活、SLO 监控或 CI/CD 健康门禁场景。

文章来自机圈观察员网,发布者:,转载请注明出处:https://www.jqgcy.com/shoujipingce/124198.html

上一篇 2026-07-01 19:00
下一篇 2026-07-01 19:13

相关推荐