英文:
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请求,我确保了以下几点(这也是许多其他人在线上建议的):
-
确保在短生命周期函数中不使用Open和Close *sql.DB。所以我只为连接池创建了一个全局变量,如下面的代码所示,尽管我对此持开放态度,因为我不喜欢使用全局变量。
-
确保在可能的情况下使用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)
-
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.
-
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)。
- 由于我有一台专用服务器,我增加了MySQL的最大连接数。
在/etc/my.cnf文件中,我添加了一行max_connections=10000。尽管这比我实际需要的连接数要多。
-
重启MySQL:service mysql restart
-
修改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
-
重新启动服务器
-
根据@MattSelf的建议,将Go升级到1.3.3
-
设置
db.SetMaxOpenConns(10000)
再次说明,这个数字对于我的需求来说太大了,但这证明了事情是正常工作的。
- 我使用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).
- 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.
-
Restart MySQL: service mysql restart
-
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
-
Reboot your server
-
Upgraded Go to 1.3.3 as suggested by @MattSelf
-
Set
db.SetMaxOpenConns(10000)
Again the number is too large for what I need, but this proved to me that things worked.
- 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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论