Golang RESTful API负载测试导致数据库连接过多。

huangapple go评论85阅读模式
英文:

Golang RESTful API load testing causing too many database connections

问题

我认为我在使用Golang管理数据库连接池方面遇到了严重的问题。我使用Gorilla web toolkit构建了一个RESTful API,在只有少量请求发送到服务器时运行良好。但是现在我开始使用loader.io网站进行负载测试。对于这个长篇帖子,我很抱歉,但我想给你一个完整的画面。

在进一步之前,这里有一些关于运行API和MySQL的服务器的信息:
专用托管Linux
8GB RAM
Go版本1.1.1
使用go-sql-driver进行数据库连接
MySQL 5.1

使用loader.io,我可以每15秒发送1000个GET请求而没有问题。但是当我发送1000个POST请求/15秒时,我会遇到很多错误,所有错误都是ERROR 1040:数据库连接太多。许多人在网上报告了类似的问题。请注意,我现在只在一个特定的POST请求上进行测试。对于这个POST请求,我确保了以下几点(这也是许多其他人在线上建议的):

  1. 确保在短生命周期函数中不使用Open和Close *sql.DB。所以我只为连接池创建了一个全局变量,如下面的代码所示,尽管我对此持开放态度,因为我不喜欢使用全局变量。

  2. 确保在可能的情况下使用db.Exec,只有在需要结果时才使用db.Query和db.QueryRow。

由于上述方法没有解决我的问题,我尝试设置db.SetMaxIdleConns(1000),这解决了1000个POST请求/15秒的问题。意味着不再出现1040错误。然后我将负载增加到2000个POST请求/15秒,又开始出现ERROR 1040。我尝试增加db.SetMaxIdleConns()中的值,但没有任何区别。

下面是我从MySQL数据库中获取的一些连接统计信息,通过运行SHOW STATUS WHERE variable_name = 'Threads_connected'来获取:

对于1000个POST请求/15秒:观察到的#threads_connected约为100
对于2000个POST请求/15秒:观察到的#threads_connected约为600

我还增加了my.cnf中MySQL的最大连接数,但没有任何区别。你有什么建议?代码看起来是否正确?如果是的话,那么很可能只是连接受限。

下面是代码的简化版本:

var db *sql.DB

func main() {
    db = DbConnect()
    db.SetMaxIdleConns(1000)

    http.Handle("/", r)
    err := http.ListenAndServe(fmt.Sprintf("%s:%s", API_HOST, API_PORT), nil)

    if err != nil {
        fmt.Println(err)
    }
}

func DbConnect() *sql.DB {
    db, err := sql.Open("mysql", connectionString)
    if err != nil {
        fmt.Printf("连接错误:%s\n", err.Error())
        return nil
    }
    return db
}

func PostBounce(w http.ResponseWriter, r *http.Request) {
    userId, err := AuthRequest(r)

    // 错误检查
    // 准备请求体并使用json.Unmarshal

    bounceId, err := CreateBounce(userId, b)

    // 在这里返回HTTP状态码
}

func AuthRequest(r *http.Request) (id int, err error) {
    // 解析头部并获取用户名和密码

    query := "SELECT Id FROM Users WHERE Username=? AND Password=PASSWORD(?)"
    err = db.QueryRow(query, username, password).Scan(&id)

    // 错误检查和返回
}

func CreateBounce(userId int, bounce NewBounce) (bounceId int64, err error) {
    // 初始化一些变量
    query := "INSERT INTO Bounces (.....) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
    result, err := db.Exec(query, ......)

    // 错误检查

    bounceId, _ = result.LastInsertId()

    // 返回
}
英文:

I think I am having serious issue managing database connection pool in Golang. I built an RESTful API using Gorilla web toolkit which works great when only few requests are being sent over to the server. But now I started performing load testing using loader.io site. I apologize for the long post, but I wanted to give you the full picture.

Before going further, here are some info on the server running the API and MySQL:
Dedicated Hosting Linux
8GB RAM
Go version 1.1.1
Database connectivity using go-sql-driver
MySQL 5.1

Using loader.io I can send 1000 GET requests/15 seconds without problems. But when I send 1000 POST requests/15 seconds I get lots of errors all of which are ERROR 1040 too many database connections. Many people have reported similar issues online. Note that I am only testing on one specific POST request for now. For this post request I ensured the following (which was also suggested by many others online)

  1. I made sure I use not Open and Close *sql.DB for short lived functions. So I created only global variable for the connection pool as you see in the code below, although I am open for suggestion here because I do not like to use global variables.

  2. I made sure to use db.Exec when possible and only use db.Query and db.QueryRow when results are expected.

Since the above did not solve my problem, I tried to set db.SetMaxIdleConns(1000), which solved the problem for 1000 POST requests/15 seconds. Meaning no more 1040 errors. Then I increased the load to 2000 POST requests/15 seconds and I started getting ERROR 1040 again. I tried to increase the value in db.SetMaxIdleConns() but that did not make a difference.

Here some connection statistics I get from the MySQL database on the number of connections by running SHOW STATUS WHERE variable_name = 'Threads_connected';

For 1000 POST requests/15 seconds: observed #threads_connected ~= 100
For 2000 POST requests/15 seconds: observed #threads_connected ~= 600

I also increased the maximum connections for MySQL in my.cnf but that did not make a difference. What do you suggest? Does the code look fine? If yes, then it is probably the connections are just limited.

You will find a simplified version of the code below.

var db *sql.DB

func main() {
    db = DbConnect()
    db.SetMaxIdleConns(1000)

    http.Handle("/", r)
    err := http.ListenAndServe(fmt.Sprintf("%s:%s", API_HOST, API_PORT), nil)

    if err != nil {
	   fmt.Println(err)
    }
}

func DbConnect() *sql.DB {
    db, err := sql.Open("mysql", connectionString)
    if err != nil {
	    fmt.Printf("Connection error: %s\n", err.Error())
	    return nil
    }
    return db
}

func PostBounce(w http.ResponseWriter, r *http.Request) {
    userId, err := AuthRequest(r)

    //error checking
    //ready requesy body and use json.Unmarshal

    bounceId, err := CreateBounce(userId, b)

    //return HTTP status code here
}

func AuthRequest(r *http.Request) (id int, err error) {
    //parse header and get username and password

    query := "SELECT Id FROM Users WHERE Username=? AND Password=PASSWORD(?)"
    err = db.QueryRow(query, username, password).Scan(&id)

    //error checking and return
}

func CreateBounce(userId int, bounce NewBounce) (bounceId int64, err error) {
    //initialize some variables
    query := "INSERT INTO Bounces (.....) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
    result, err := db.Exec(query, ......)

    //error checking

    bounceId,_ = result.LastInsertId()

    //return 
}

答案1

得分: 8

Go的database/sql包不会阻止你创建无限数量的数据库连接。如果连接池中有空闲连接,它将被使用;否则将创建一个新的连接。

因此,在负载下,你的请求处理程序sql.DB可能找不到空闲连接,因此在需要时会创建一个新的连接。这会导致连接的频繁创建和重用,最终达到数据库的最大连接数。不幸的是,在Go 1.1中没有方便的方法(例如SetMaxOpenConns)来限制打开的连接数。

升级到更新版本的Golang。在Go 1.2+中,你可以使用SetMaxOpenConns。并且,请查看MySQL文档以了解如何进行起始设置,然后进行调整。

db.SetMaxOpenConns(100) //调整这个值

如果你必须使用Go 1.1,你需要确保在你的代码中,*sql.DB同时只被N个客户端使用。

英文:

Go database/sql doesn't prevent you from creating an infinite number of connections to the database. If there is an idle connection in the pool, it will be used, otherwise a new connection is created.

So, under load, your request handlers sql.DB is probably finding no idle connections and so a new connection is created when needed. This churns for a bit -reusing idle connections when possible and creating new when needed-, ultimately reaching the max connections for the Db. And, unfortunately, in Go 1.1 there isn't a convenient way (e.g. SetMaxOpenConns) to limit open connections.

Upgrade to a newer version of Golang. In Go 1.2+ you get SetMaxOpenConns. And check out the MySql docs for starting setting and then tune.

db.SetMaxOpenConns(100) //tune this

If you must use Go 1.1 you'll need to ensure in your code that *sql.DB is only being used by N clients at a time.

答案2

得分: 1

@MattSelf提出的解决方案是正确的,但我遇到了其他问题。下面是我解决问题的具体步骤(顺便提一下,服务器正在运行CentOS)。

  1. 由于我有一台专用服务器,我增加了MySQL的最大连接数。

在/etc/my.cnf文件中,我添加了一行max_connections=10000。尽管这比我实际需要的连接数要多。

  1. 重启MySQL:service mysql restart

  2. 修改ulimit -n。这是为了增加打开的描述性文件的数量。

为此,我对两个文件进行了更改:

在/etc/sysctl.conf文件中,我添加了一行

fs.file-max = 65536

在/etc/security/limits.conf文件中,我添加了以下几行:

*          soft     nproc          65535
*          hard     nproc          65535
*          soft     nofile         65535
*          hard     nofile         65535
  1. 重新启动服务器

  2. 根据@MattSelf的建议,将Go升级到1.3.3

  3. 设置

    db.SetMaxOpenConns(10000)

再次说明,这个数字对于我的需求来说太大了,但这证明了事情是正常工作的。

  1. 我使用loader.io进行了一次测试,其中包括5000个客户端在15秒内发送POST请求。所有请求都成功通过,没有出现错误。
英文:

@MattSelf proposed solution is correct, but I ran into other issues. Here I highlighted what I did exactly to solve the problem (by the way, the server is running CentOS).

  1. Since I have a dedicated server I increased the max_connections for MySQL

In /etc/my.cnf I added the line max_connections=10000. Although, that is more connections than what I need.

  1. Restart MySQL: service mysql restart

  2. Changed the ulimit -n. That is to increase the number of descriptive files that are open.

To do that I made changes to two files:

In /etc/sysctl.conf I added the line

fs.file-max = 65536

In /etc/security/limits.conf I added the following lines:

*          soft     nproc          65535
*          hard     nproc          65535
*          soft     nofile         65535
*          hard     nofile         65535
  1. Reboot your server

  2. Upgraded Go to 1.3.3 as suggested by @MattSelf

  3. Set

       db.SetMaxOpenConns(10000)
    

Again the number is too large for what I need, but this proved to me that things worked.

  1. I ran a test using loader.io which consists of 5000 clients each sending POST request all within 15 seconds. All went through without errors.

答案3

得分: 1

在你的my.cnf文件中,将back_log设置为较高的值,例如几百或1000,这将有助于处理更多的每秒连接。请参阅每秒高连接数

英文:

Something else to note is setting the back_log to a higher value in your my.cnf file to something like a few hundred or 1000. This will help handle more connections per second. See High connections per second.

huangapple
  • 本文由 发表于 2014年10月25日 23:34:03
  • 转载请务必保留本文链接:https://go.coder-hub.com/26564084.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定