Golang HTTP API 服务异常终止,原因排查求助

Golang HTTP API 服务异常终止,原因排查求助 我的HTTP API服务器运行大约3到10小时后就会终止,没有任何错误信息。有人能帮我找出问题所在吗?

该程序运行在中国阿里云的一台ECS实例上。

程序使用了以下库:

"robfig/cron"
"go-gorp/gorp"
"julienschmidt/httprouter"
_ "go-sql-driver/mysql"

我使用

resp, err := http.Get("http://api.wunderground.com/api/xxx)

每10分钟获取一次天气数据,然后存储到一个结构体中。

客户端可以通过以下HTTP GET请求访问: “http://myproj-name.xyz:7000/api/0.2/weather

响应体如下所示:

{
  "Observation_time": "2018-04-03T10:41:57+08:00",
  "Create_time": "2018-04-03T10:50:00.000139373+08:00",
  "City": "Changzhou",
  "Country_name": "China",
  "Temp_c": 26,
  "Relative_humidity": "62",
  "Icon": "clear",
  "Weather": "Clear",
  "Wind_dir": "SSE",
  "Wind_degrees": 0,
  "Wind_kph": 11,
  "Is_weather_site_alive": true
}

我的MySQL也存储在中国阿里云的RDS上

type Weather struct {                                               
	Observation_time 		time.Time
	Create_time 			time.Time
	City					string	`db:",size:64"`
	Country_name			string	`db:",size:64"`
	
	Temp_c					int32						// 由于gorp将float32反射为double模式的问题
	Relative_humidity		string	`db:",size:32"`
	
	Icon					string	`db:",size:32"`		// 天气图标的正确名称
	Weather					string	`db:",size:64"`		// "Clear"
	Wind_dir				string	`db:",size:32"`		// "South", "SSE"
	Wind_degrees			int32
	Wind_kph				int32
	Is_weather_site_alive	bool 
}

它通过了压力测试 $ab -n 10000 -c 1000 my-http-site-above

CPU使用率约为5%,在4GB内存中仍有3GB可用内存。

有人能帮我找出问题吗? 提前感谢。


更多关于Golang HTTP API 服务异常终止,原因排查求助的实战教程也可以访问 https://www.itying.com/category-94-b0.html

13 回复

感谢acim。很高兴了解到级别记录器的存在。如果我有任何进一步的问题会告知您。

更多关于Golang HTTP API 服务异常终止,原因排查求助的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


我在Linux主机上遇到了类似的问题。最终发现是内存问题——操作系统直接终止了我的进程。我在系统日志中找到了相关记录。

我正在讨论Go程序中的错误处理。你必须在代码中处理这些错误并记录这些错误的输出。

如果你的程序没有任何提示信息就停止运行,那肯定是因为你没有正确处理错误,导致某处出现了恐慌。因此,首先你必须解决这个问题,否则很难确定具体发生了什么,可能有成千上万种原因。此外,提供一些代码会更有帮助。

log.Fatalln 会导致程序终止。这取决于具体情况,如果进程能够继续运行,就不要使用 Fatalln,改用警告或其他方式。你的服务器代码在哪里?是否记录了 http.ListenAndServe 的返回信息?

我尝试了 cat /var/log/syslog 命令,但它只显示今天的事件。我的程序昨天崩溃了。如何查看昨天甚至更早几天的日志?

// 代码示例保留区域

创建一个 *log.Logger 然后执行 logger.Println(err),你可以将日志记录器设置为标准错误输出、文件或任何 io.Writer

func main() {
    logger := log.New(os.Stderr, "", log.LstdFlags)
    err := errors.New("example error")
    logger.Println(err)
}

你好 @Steve.Tsai,

如果你能展示更多代码内容,可能有助于调试问题。

例如,如果你在检查错误后没有调用错误检查或 resp.Body.Close(),问题可能就出在这里。

示例:

resp, err := http.Get("http://api.wunderground.com/api/xxx")
if err != nil {
	log.Fatal(err)
}
defer resp.Body.Close()

编辑:顺便提一下,我特别强调关闭响应体的原因是你可能会因为连接的文件描述符保持打开状态而导致资源泄漏。

Steve.Tsai:

log.Fatal(http.ListenAndServe(":12345", nil))

在此处使用它是可以的,因为这意味着你的服务器无论如何都无法启动,但不要在可以恢复并继续执行的地方使用它。在这些情况下,你可以使用 log.Println、log.Print 和 log.Printf。许多人使用以下日志记录器替代标准库中的日志记录器:

GitHub - sirupsen/logrus: Structured, pluggable logging for Go.

这个日志记录器具有 Warn、Info 等方法。

感谢acim。我发现我没有捕获所有可能的错误。我的程序崩溃的原因之一是没有正确处理来自httpClient的返回错误。

我的代码如下:

import (
	"os"
	"fmt"
	"log"
	"net"
	"time"
	"flag"			// command line flags
	"net/http"
	"database/sql"
	"encoding/json"

	"github.com/go-gorp/gorp"
	_ "github.com/go-sql-driver/mysql"
	"github.com/julienschmidt/httprouter"
)

func main() {
	// 1. Parse command line flags
	ParseCmdFlags()
	
	// 2. read config file, set by provision engineer
	ReadConfig () 

	// 3. connect to db		// old: ConnectDb ()
	g_dbMap = InitDb()
	defer g_dbMap.Db.Close()

	// 4. Declare db & clear db data if necessary
	DeclareDb (g_dbMap, *g_cmdFlags.pClearDb)
	
	RepeatRequestWeather ();
	
	// Set Router & Handlers
	router := httprouter.New()

	HttpSetHandlers (router)
	
	http.ListenAndServe(":" + g_config.ServerPort, router)
}

我没有处理错误。但是Golang官方文档中的代码如下:

log.Fatal(http.ListenAndServe(":12345", nil))

log.Fatal总是会退出程序。是否应该处理不同类型的错误?而不是必须退出?

感谢您的回复。

以下是我的服务脚本的一部分。

#!/bin/bash
### BEGIN INIT INFO
# Provides:          Steve
# Required-Start:    
# Required-Stop:     
# Default-Start:   2 3 4 5
# Default-Stop:      0 1 6
# X-Interactive:     false
# Short-Description: 
# Description:       Start/stop a Service
### END INIT INFO

# Variables
PROG_NAME=HttpApiServer

#Start the server
start() {
        #Verify if the service is running
        pgrep -f HttpApiServer > /dev/null

        if [ 0 = $? ]
        then
                echo "The Service is alreay running."
        else
                echo "Starting" $PROG_NAME "..."
                # run the server under account home dir
                #cd ~/go/bin
                #./HttpApiServer > output.txt 2>&1 & disown
                nohup /root/go/bin/HttpApiServer & disown
                sleep 5

                # verify if the server is running...
                if [ 0 = $? ]
                then
                        echo "Service was successfully started."
                else
                        echo "Failed to start service."
                fi
        fi
        echo
}

… Nohup.txt 没有存储错误信息。如果程序因 panic 而崩溃,我该如何从 golang 中查找错误信息? 谢谢。

func DecodeWeatherData(data map[string]interface{}) bool {
	// JSON结构的第二层也被解码为map结构
	loc := data["location"].(map[string]interface{})

	if loc == nil {
		return false
	}

	g_weather.Country_name = loc["country_name"].(string)
	g_weather.City = loc["city"].(string)

	// 实际天气数据在这里
	observ := data["current_observation"].(map[string]interface{})

	if observ == nil {
		return false
	}

	var err error
	// 天气地下API使用以下预定义格式
	// RFC1123Z    = "Mon, 02 Jan 2006 15:04:05 -0700
	g_weather.Observation_time, err = time.Parse(time.RFC1123Z, observ["observation_time_rfc822"].(string))

	if err != nil {
		fmt.Println("!!! 错误。 " + err.Error())
		fmt.Println(" 解码天气observation_time = " + observ["observation_time_rfc822"].(string))
		return false
	}

	g_weather.Relative_humidity = observ["relative_humidity"].(string)

	// !!! 这里崩溃
	// !!! 当请求天气API超过10次/秒时
	// !!! panic: interface conversion: interface {} is nil, not float64
	g_weather.Temp_c = int32(observ["temp_c"].(float64))
	g_weather.Icon = observ["icon"].(string)
	g_weather.Weather = observ["weather"].(string)
	g_weather.Wind_degrees = int32(observ["wind_degrees"].(float64))
	g_weather.Wind_dir = observ["wind_dir"].(string)
	g_weather.Wind_kph = int32(observ["wind_kph"].(float64))

	fmt.Println(g_weather)
	return true
}

func DecodeRequestBody(r io.Reader) map[string]interface{} {
	body, err := ioutil.ReadAll(r)
	if err != nil {
		// 保持最后状态,不改变
		return nil
	}
	// 复制切片字符串
	bodyBytes := make([]byte, len(body), (cap(body)+1)*2) // == nil, len() = 0, cap() = 0
	copy(bodyBytes, body)

	// 解析JSON字符串以获取简单表单所需数据
	var data map[string]interface{}

	// 解码JSON字节流,并检查相关错误
	if err := json.Unmarshal(bodyBytes, &data); err != nil {
		fmt.Println("!!! 错误。解码天气数据。")
		return nil
	}
	return data
}

func RequestOutWeather(t time.Time) {

	g_weather.Is_weather_site_alive = false

	// 有文章说为了避免默认客户端永远挂起,
	// 指定超时来停止并输出错误
	var netClient = &http.Client{
		Timeout: time.Second * 30,
	}

	resp, err := netClient.Get("http://api.wunderground.com/api/mykeyxxxxx/geolookup/conditions/q/autoip.json?geo_ip=49.80.123.123.json")
	if err != nil {
		fmt.Printf("!!! 获取天气信息错误。")
		g_weatherBody = nil
		return
	}
	defer resp.Body.Close()

	g_weatherBody := DecodeRequestBody(resp.Body)

	// 如果成功解码所有数据
	g_weather.Is_weather_site_alive = DecodeWeatherData(g_weatherBody)
}

// 当发生错误时这是正确的方式吗?
// 还是应该记录它并继续运行?
func CheckErr(err error, msg string) {
    if err != nil {
        log.Fatalln(msg, err)
    }
}

有人能建议一种一致且更好的方式来记录和处理错误吗? 附:有人能帮我正确引用程序文本吗?

在HTTP API服务异常终止的情况下,最常见的原因是资源泄漏或未处理的panic。根据你使用的库和代码模式,以下是几个需要排查的方向和相应的示例代码:

1. 数据库连接泄漏

使用gorp时,如果没有正确关闭数据库连接,会导致连接池耗尽:

// 错误的做法 - 连接未关闭
func getWeatherData() (*Weather, error) {
    db, err := sql.Open("mysql", "user:pass@/dbname")
    if err != nil {
        return nil, err
    }
    // 缺少 defer db.Close()
    
    var weather Weather
    err = db.QueryRow("SELECT * FROM weather LIMIT 1").Scan(&weather)
    return &weather, err
}

// 正确的做法
func getWeatherData() (*Weather, error) {
    db, err := sql.Open("mysql", "user:pass@/dbname")
    if err != nil {
        return nil, err
    }
    defer db.Close() // 确保连接关闭
    
    var weather Weather
    err = db.QueryRow("SELECT * FROM weather LIMIT 1").Scan(&weather)
    return &weather, err
}

2. HTTP响应体未关闭

在cron任务中调用http.Get后必须关闭响应体:

// 错误的做法
func fetchWeatherData() {
    resp, err := http.Get("http://api.wunderground.com/api/xxx")
    if err != nil {
        log.Printf("HTTP request failed: %v", err)
        return
    }
    // 缺少 resp.Body.Close()
    
    // 处理响应数据...
}

// 正确的做法
func fetchWeatherData() {
    resp, err := http.Get("http://api.wunderground.com/api/xxx")
    if err != nil {
        log.Printf("HTTP request failed: %v", err)
        return
    }
    defer resp.Body.Close() // 必须关闭响应体
    
    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        log.Printf("Read body failed: %v", err)
        return
    }
    
    // 处理body数据...
}

3. 添加全局panic恢复

在HTTP处理器中添加recover来捕获未处理的panic:

func withRecover(h httprouter.Handle) httprouter.Handle {
    return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
        defer func() {
            if err := recover(); err != nil {
                log.Printf("Recovered from panic: %v", err)
                http.Error(w, "Internal Server Error", http.StatusInternalServerError)
            }
        }()
        h(w, r, ps)
    }
}

// 注册路由时使用
router.GET("/api/0.2/weather", withRecover(weatherHandler))

4. 添加信号处理和优雅关闭

实现优雅关闭机制:

func main() {
    router := httprouter.New()
    router.GET("/api/0.2/weather", weatherHandler)
    
    server := &http.Server{
        Addr:    ":7000",
        Handler: router,
    }
    
    // 启动服务器
    go func() {
        if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            log.Fatalf("Server failed: %v", err)
        }
    }()
    
    // 等待中断信号
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, os.Interrupt, syscall.SIGTERM)
    <-quit
    
    log.Println("Shutting down server...")
    
    // 优雅关闭
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    
    if err := server.Shutdown(ctx); err != nil {
        log.Fatalf("Server forced to shutdown: %v", err)
    }
    
    log.Println("Server exited")
}

5. 添加内存监控

在代码中添加内存使用监控:

func startMemoryMonitor() {
    go func() {
        for {
            var m runtime.MemStats
            runtime.ReadMemStats(&m)
            log.Printf("Alloc = %v MiB, TotalAlloc = %v MiB, Sys = %v MiB, NumGC = %v",
                m.Alloc/1024/1024, m.TotalAlloc/1024/1024, m.Sys/1024/1024, m.NumGC)
            time.Sleep(30 * time.Second)
        }
    }()
}

在你的main函数中调用startMemoryMonitor()来监控内存使用情况。

6. 检查cron任务

确保cron任务不会累积:

c := cron.New()
// 使用带恢复的包装函数
c.AddFunc("*/10 * * * *", func() {
    defer func() {
        if err := recover(); err != nil {
            log.Printf("Cron job panic: %v", err)
        }
    }()
    fetchWeatherData()
})
c.Start()

建议先添加panic恢复和资源释放代码,然后观察服务运行情况。如果问题仍然存在,检查系统日志/var/log/messages或使用dmesg查看是否有OOM killer终止进程的记录。

回到顶部