Go Web Application + MySql驱动程序Stalling / Timing Out

时间:2014-07-12 00:18:34

标签: mysql go

更新:进一步诊断后,go-sql-driver / mysql I驱动程序包出现问题。事实证明,底层的tcp似乎无法检测到断开的tcp连接。完整的详细信息位于以下go-sql-driver/mysql项目的github问题中:

https://github.com/go-sql-driver/mysql/issues/257

-

我目前在15到48分钟之间闲置一段时间后遇到拖延损坏的网页应用。最关键的问题如下所述:

  • 访问网站上的网址,网址上的任何网址,然后完全加载网页(如,页面实际加载,日志显示已加载完整页面)。
  • 关闭浏览器,然后等待。

典型的请求记录如下:

2014/07/13 15:29:54 INFO template rendering: index
2014/07/13 15:29:54 METRIC, URL: /, HANDLER TIME: 7.2339ms, CTX TIME: 5.0894ms, TOTAL TIME: 12.3258ms

经过很长一段时间(从15米到48米不等),系统突然记录下面这些行没有交互 - 网络应用程序一直处于空闲状态:

[MySQL] 2014/07/13 16:00:09 packets.go:32: read tcp remote-mysql-server-address:3306: connection timed out
[MySQL] 2014/07/13 16:00:09 packets.go:118: write tcp remote-mysql-server-address:3306: broken pipe
2014/07/13 16:00:10 INFO template rendering: index
2014/07/13 16:00:10 METRIC, URL: /, HANDLER TIME: 8.8574ms, CTX TIME: 31m19.2606723s, TOTAL TIME: 31m19.2695329s

注意“TOTAL TIME”是31分19秒?另外,请注意同时记录的MySql驱动程序错误?

没有活动/没有网络请求。网络应用程序只是空闲。

最关键的问题是这些日志消息之后的下一步: 下一个Web请求完全停止,从不返回响应

user@govm1:~$ wget http://localhost
--2014-07-13 17:11:18--  http://localhost/
Resolving localhost (localhost)... 127.0.0.1
Connecting to localhost (localhost)|127.0.0.1|:80... connected.
HTTP request sent, awaiting response... Read error (Connection timed out) in headers.
Retrying.

--2014-07-13 17:26:19--  (try: 2)  http://localhost/
Connecting to localhost (localhost)|127.0.0.1|:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: unspecified [text/html]
Saving to: `index.html.4'

    [ <=>                                                                                  ] 6,310       --.-K/s   in 0.001s

2014-07-13 17:26:20 (9.61 MB/s) - `index.html.4' saved [6310]

它闲置,没有反应,持续15分钟,直到wget超时。

现在,如果我在停止之后立即发出第二个或第三个请求并且在停止时随时发出请求,那么go web app会响应并返回其他请求的完整页面。没有问题。然后,循环从我上次发出的请求开始,让它站点闲置。

在这15米之后,您可以准确猜出下一步记录的内容:

[MySQL] 2014/07/13 17:26:57 packets.go:32: read tcp remote-mysql-server-address:3306: connection timed out
[MySQL] 2014/07/13 17:26:57 packets.go:118: write tcp remote-mysql-server-address:3306: broken pipe
2014/07/13 17:26:57 INFO template rendering: index
2014/07/13 17:26:57 METRIC, URL: /, HANDLER TIME: 6.8938ms, CTX TIME: 15m39.1718434s, TOTAL TIME: 15m39.1787398s

另外15米的等待时间。

我淘汰了Windows Azure,运行go web应用程序的群集VIP和防火墙/ Linux VM是一个问题,因为我在同一个盒子上本地运行wget http://localhost,我得到这个“停滞”的请求永远不会完成永远不会发回任何东西。

-

我的网络应用程序中有很多因素,因此我会尝试相应地概述它们。

使用:

  • Go 1.3
  • go-sql-driver / mysql ## Version 1.2(2014-06-03)
  • Ubuntu 12.04 LTS,〜2014年6月更新
  • Windows Azure

请注意,运行MySql的Linux机箱是运行GoLang应用程序集群的另一个Linux机箱 - 它们位于单独的专用云服务中。 MySql vm是一个VM,没有cluserting。

以下是一些相关代码:

// global handler for our DB
var db *sql.DB

// CLI parameter
var dbdsn string

func init() {

    flag.StringVar(&dbdsn, "dbdsn", "root:root@tcp(localhost:3306)/prod?timeout=5s&tls=false&autocommit=true", "Specifies the MySql DSN connection.")
    flag.Parse()

    var err error
    db, err = sql.Open("mysql", dbdsn)
    if err != nil {
        log.Printf("ERROR in sql.Open(): %v", err)
    }

    //db.SetMaxIdleConns(5)

    // verify the DSN is setup properly1
    err = db.Ping()
    if err != nil {
        panic("PANIC when pinging db: " + err.Error()) // proper error handling instead of panic in your app
    }
}

// **********
// * omitted is the Gorilla MUX router and http handler registrations
// **********

func ArticleHandler(w http.ResponseWriter, r *http.Request, c *Context) (err error) {

    m := NewArticle(c)
    id := c.Vars["id"]

    var pid int
    var title, body, excerpt, date, slug, fi, fv, region, region_slug string
    err = db.QueryRow(
        "SELECT p.ID, p.post_title, p.post_content, p.post_excerpt, p.post_date, p.post_name, "+
            "(SELECT fpim.meta_value FROM wp_postmeta fpim WHERE fpim.meta_key = '_wp_attached_file' AND fpim.post_id = (SELECT fpim2.meta_value FROM wp_postmeta fpim2 WHERE fpim2.post_id = p.ID AND fpim2.meta_key = '_thumbnail_id' LIMIT 1) LIMIT 1) AS featured_image, "+
            "(SELECT fpim3.meta_value FROM wp_postmeta fpim3 WHERE fpim3.meta_key = 'fv_video' AND fpim3.post_id = p.ID LIMIT 1) AS featured_video, "+
            "t.name as region, t.slug as region_slug "+
            "FROM wp_posts p "+
            "JOIN wp_term_relationships tr ON tr.object_id=p.ID "+
            "JOIN wp_term_taxonomy tt ON tt.term_taxonomy_id=tr.term_taxonomy_id "+
            "JOIN wp_terms t ON t.term_id=tt.term_id "+
            "WHERE p.post_name=? AND p.post_type='post' AND p.post_status='publish' AND p.post_date <= UTC_TIMESTAMP()"+
            "AND tr.object_id=p.ID AND tt.parent = (SELECT t3.term_id FROM wp_terms t3 WHERE t3.name=? LIMIT 1) LIMIT 1",
        id, RegionsParentCategory).
        Scan(&pid, &title, &body, &excerpt, &date, &slug, &fi, &fv, &region, &region_slug)

    if err != nil {
        if err == sql.ErrNoRows {

            // snipped code for redirects

            // article was not found
            return handleNotFound(w, r, c)

        } else {
            log.Printf("ERROR in .Scan(): %v", err)
        }
    } else {
        m.Region = Region{
            Name: region,
            Slug: region_slug,
        }
        m.Id = pid
        m.Title = title
        m.Body = template.HTML(body) // render the raw html
        m.Excerpt = excerpt
        m.Datetime = date
        m.Slug = slug
        m.FeaturedImageUrl = fi
        m.FeaturedVideoUrl = fv
    }

    web.RenderTemplate(w, "article", m)
    return
}

每个请求还有5个数据库查询

除了这个查询之外,我看到传入处理程序的“Context”还会运行4到6个额外的SQL查询。因此,加载的每个“article”处理程序使用完全相同的模式和上面看到的*db全局变量来运行大约5到7个SQL查询,最小化。

超时/错误始终在同一个数据库查询

以下是作为比较的“上下文”查询之一:

rows2, err := db.Query(
    "SELECT p.post_title, p.post_name "+
        "FROM wp_posts p "+
        "WHERE p.post_type='page' AND p.post_status='publish' AND p.post_date <= UTC_TIMESTAMP() "+
        "AND p.post_parent = (SELECT p2.ID FROM wp_posts p2 WHERE p2.post_name=? LIMIT 1) "+
        "ORDER BY p.menu_order",
    FooterPagesParentNameSlug)
if err != nil {
    log.Printf("ERROR in AllPages .Query() : %v", err)
} else {
    defer rows2.Close()
    c.AllFooterPages = make([]FooterPage, 0)
    for rows2.Next() {
        var name, slug string
        err := rows2.Scan(&name, &slug)
        if err != nil {
            log.Printf("ERROR in AllPages row.Scan() : %v", err)
        } else {
            p := FooterPage{
                Page: Page{
                    Title: name,
                    Slug:  slug,
                },
            }
            c.AllFooterPages = append(c.AllFooterPages, p)
        }
    }
}

没什么特别的。

只有在没有错误的情况下才会调用defer rows2.Close()。也许这是问题的一部分?这个特定的SQL查询似乎将负载测试下的错误记录为no response或mysql驱动程序超时。

问题

为什么我从空闲站点获取超过15到30分钟的请求超时?这似乎是我正在使用的mysql驱动程序的一个错误,可能是一个连接打开。但是,最后一个http请求成功并返回了一个完整的页面+模板。

我甚至在连接字符串中设置了Timeout,这是5秒。即使它是mysql服务器的问题,为什么15分钟的超时/请求被记录?该请求来自何处?

它仍然可能是MySql驱动程序问题,阻止请求完成 - 可能被MySql专用VM阻止并出现问题。如果是这样的话,那怎么没有记录?什么是15米到49分钟的随机超时?它通常只有15米或31米,但有时会记录48米。

在超时(@ 15m,31m和48m)的“15m”倍数非常有趣,在几秒钟内分配一些填充。

提前致谢。

2 个答案:

答案 0 :(得分:1)

在init中永远不会defer db.Close()。 init在执行main之前结束,因此永远不会访问打开的连接池。 不过,您可以在defer db.Close()中致电main

这可能也是您准备好的语句的问题,它们属于连接池,并且在调用db.Close()时无效。

关于超时,它是驱动程序端超时(从问题到此处)。

请参阅https://github.com/go-sql-driver/mysql/issues/257

上的评论

答案 1 :(得分:0)

  

我应该“打开”并在每个Web请求/处理程序上推迟db.Close()吗?

没有。创建一个全局,不要担心关闭它(就像你现在一样)或者通过应用程序上下文传递池(*sql.DB),即按照https://medium.com/@benbjohnson/structuring-applications-in-go-3b04be4ff091通过将处理程序作为上下文的方法嵌入*sql.DB以及您可能需要的任何其他内容的类型。

您可能还希望使用http://jmoiron.github.io/sqlx/来帮助将数据库结果编组到结构/地图中,而无需自己进行舞蹈。


顺便说一句(这将有助于调试问题,我敢打赌)我会修复你的第二个代码示例以匹配下面 - 因为当你遇到错误时你只记录它,但你的代码继续作为你不回来了:

rows2, err := db.Query(`
    SELECT p.post_title, p.post_name
    FROM wp_posts p 
    WHERE p.post_type='page' AND p.post_status='publish' AND p.post_date <= UTC_TIMESTAMP()
    AND p.post_parent = (SELECT p2.ID FROM wp_posts p2 WHERE p2.post_name=? LIMIT 1)
    ORDER BY p.menu_order`, FooterPagesParentNameSlug)
if err != nil {
    log.Printf("ERROR in AllPages .Query() : %v", err)
    return err
}
defer rows2.Close()

c.AllFooterPages = make([]FooterPage, 0)

for rows2.Next() {
    var name, slug string
    err := rows2.Scan(&name, &slug)
    if err != nil {
        log.Printf("ERROR in AllPages row.Scan() : %v", err)
        return err // Same here!
    }
// Rest of your code
}