Golang与PHP7的MySQL性能对比
Golang与PHP7的MySQL性能对比 我们是一家使用PHP7的公司,已经用其编程超过12年。我一直有个想法,将我们的网络服务器拆分成可服务的单元来处理不同的任务,并且多年来一直在考虑用Go语言实现这一点。我遇到的一个问题是,与PHP相比,在Go中处理查询需要大量的代码。在PHP中,你只需设置一个变量来保存查询,执行它,将结果赋值给一行或循环遍历,每个变量都会以命名数组(MAP)的形式返回,你只需从结果数组中获取所需内容。
而在Go中,你必须首先为每个返回的字段定义变量,这些变量的类型要与数据库中的字段类型一致,然后设置查询字符串,运行查询,从结果中获取一行,并手动将结果中的每个字段设置到你之前定义的每个变量中。这看起来工作量更大,但如果能带来好处,我愿意接受这些额外的工作。
因此,我在脑海中设计了一个非常简单的测试,比较PHP和Go在处理MySQL方面的差异。注意:我们的数据库包含超过1,000张表,所以我们不仅仅是在讨论查询姓名和地址。
在列出代码之前,我先说明问题:我连接到数据库,然后循环100万次。在每次循环中,我获取一个随机数,该随机数将与表中的ID匹配,然后查询该ID。在两种语言中,我都检索行,但在PHP中,这个函数实际上会检索数据并将其设置到一个数组中,然后我可以使用该数组。在Go中,这需要调用rows.Scan,但由于我无法编译未使用的变量,我没有这样做。因此我省略了这部分。但在我看来,Go程序做的工作比PHP7程序少。
结果:在PHP7中,我能够在55秒内完成100万次搜索,并且似乎平均使用了约50%的CPU。而Go应用程序执行相同的100万次查找需要超过一分钟,并且似乎从未使用超过30%的CPU。
所有测试都在同一台计算机上进行,因此硬件无关紧要。此外,我删除了从PHP中获取行的最后一行代码,只节省了4秒,因此在PHP中获取数据的强度不大。对于Go语言,我不确定,因为它已经较慢,我不想进一步复杂化。
#################################################
PHP代码:
$startrecord = 9086240;
$endrecord = 9644302;
$conn = new mysqli( $server, $user, $pass, "support" );
//循环100万次,随机获取错误日志条目
for ( $i = 1; $i < 1000000; $i++ ) {
$ran = rand( $startrecord, $endrecord );
$query = "select * from logErrors where ErrorLogID=$ran";
$result = $conn->query( $query );
$row = $result->fetch_assoc();
}
#################################################
Go代码:
func main() {
startrecord := 9086240;
endrecord := 9644302;
var DBConnect string
DBConnect = dbUser + ":" + dbPass + "@"
DBConnect = DBConnect + "(" + dbHost + ":" + dbPort + ")/" + dbName + "?charset=utf8&autocommit=true"
db, _ := sql.Open("mysql", DBConnect)
rand.Seed(time.Now().UnixNano())
//循环100万次
for i := 0; i < 1000000; i++ {
ran := rand.Intn(endrecord-startrecord) + startrecord
query := "select ErrorLogID, ErrorID from logErrors " +
"where ErrorLogID=" + strconv.Itoa(ran)
if rows, err := db.Query( query ); err != nil {
fmt.Printf( "Error: ?", err )
return
} else {
for rows.Next() {
}
}
}
}
更多关于Golang与PHP7的MySQL性能对比的实战教程也可以访问 https://www.itying.com/category-94-b0.html
如何强制使用 go?
如何使用套接字而不是本地主机?
谢谢,我已经做了那个改动,但即使有影响也不大。不过你的观点我接受了。
我理解关于代码风格的问题,但并不是要写一个规范的应用程序。我只是想单独测试性能,并尽量保持简单。添加适当的封装会影响性能吗?如果会的话我可以尝试一下,但如果真有影响我会很惊讶。
我自己运行了测试。PHP7和Go都通过TCP与数据库通信(数据库位于Docker容器中)。PHP速度稍快,大约快10%。我还尝试在两种语言中使用预处理语句,但这并没有产生任何差异。
我认为在PHP和Go中都可以使用简单的增量计数器而非随机数来保持相同的引用。随机数生成算法的细微差异可能会影响执行时间 
我从每个程序中移除了随机处理过程。这也减少了我需要搜索的记录数量,因此很难确切知道这到底拖慢了多少速度。不过,使用新程序运行后,PHP7耗时26秒,而Go耗时34秒。虽然Go更快一些,但在搜索记录时仍然明显较慢。
还有其他建议吗?
我按照你的建议修改了随机方法,但完全没有产生任何区别。如果要说有什么变化的话,现在可能还慢了几秒钟,不过也可能是电脑这会儿运行得有点慢。
到目前为止,我在单纯读取记录方面所做的任何调整都没有改善GO的性能。
谢谢。
db.SetMaxIdleConns(0)
将最大空闲连接数设置为0意味着不允许复用任何连接,因此每个请求都必须建立新的连接。由于您只扫描一行数据,不会"持续扫描直到结束"(这样会关闭连接),很快就会达到最大连接数限制。
我不太确定随机过程的具体情况,但想使用它是为了让系统不会简单地按顺序循环遍历记录。我认为这样可以让 MySQL 服务器在查找记录时保持活跃状态。归根结底,这可能并不重要,但 Go 语言获取随机数的速度会慢很多吗?我会尝试一下看看结果如何。
Glenn_Hancock:
“where ErrorLogID=” + strconv.Itoa(ran)
我在你的代码中发现了一个可能影响性能的问题(虽然影响不大但确实存在)。建议不要使用 strconv,而是采用以下写法:
"where ErrorLogID=?",ran
另外:出于同样的性能考虑,在基准测试中尽量避免使用 fmt。
你正在使用 math/rand.Intn 函数,它在底层使用了默认的全局线程安全 rand.Source。
我敢打赌,通过替换为:
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
然后使用:
ran := rng.Intn(endrecord-startrecord) + startrecord
你将获得显著的性能提升和更公平的比较。
(除非等效的 PHP rand 函数也是线程安全的?)
某些 MySQL 客户端实现会自作聪明:当你连接 localhost 时,它们会尝试通过默认套接字进行连接。对于这类情况,你需要使用 IP 地址来强制启用网络模式。
当然,使用套接字可能会更快,因为它绕过了内核中大约一半(甚至更多)的网络协议栈。
为了公平比较,要么在 PHP 中强制启用网络模式,要么在 Go 中也使用套接字连接。
你在使用这个驱动吗?
go-sql-driver/mysql
Go MySQL Driver 是 Go (golang) database/sql 包的 MySQL 驱动 - go-sql-driver/mysql
是的,我确实注意到了。不过,我刚刚发现了一些奇怪的现象,正在尝试弄清楚原因。
我打开了Wireshark想查看网络活动是否存在差异,结果发现了一个问题。
两个程序都设置为通过本地主机与MySQL通信。Go程序确实如此,我可以正常捕获到网络流量。但是,PHP并没有通过任何我能看到的网络连接进行通信。有没有可能它是以不同于网络传输的方式与MySQL通信的?我以前从未注意到这种情况。
需要说明的是,我的脚本和MySQL都在同一台计算机上通过本地主机运行。
这行代码可能会影响性能。但更糟糕的是,它导致了不良的编程风格。请记住Bobby Tables。
务必使用正确的参数绑定,绝对不要使用字符串操作来构建SQL语句。
顺便说一句,我认为这样的写法:
$query = "select * from logErrors where ErrorLogID=$ran";
在PHP中也不被推荐。想象一下如果$ran是用户输入的情况。在PHP中也必须使用参数绑定。
我发现了另一个名为QueryRow的查询方法,它用于查找返回的单条记录。我调整了代码来使用它,因为这更接近PHP的做法,并且可以消除额外的for循环。然而,使用这个方法后,我的执行时间上升到了1分38秒。
由于我们的程序涉及数千张表,您可以想象每秒钟会有多少查询在进行。我们的大部分逻辑都是基于查询结果构建的,因此Go比PHP慢这么多,让我在推进这个项目时感到有些困扰。我希望是我做错了什么,但面对如此简单的程序,我不确定问题出在哪里。
我也意识到我可以启动多个线程来执行搜索,但这并不是PHP的做法,所以这样的比较并不公平。而且如果我们每次都必须这样做,将会大大复杂化原本相当简单的流程。
稍作调整:这里有一个新版本,以及运行45秒后出现的错误。我添加了以下行:
db.SetMaxIdleConns(0)
db.SetMaxOpenConns(100)
循环运行一段时间后出现此错误: 错误:dial tcp 127.0.0.1:3306: connect: 无法分配请求的地址
所以我的问题是:是否可能因为只添加了maxopenconns行,导致Go使用多个mysql连接来运行每个查询?以下是我运行循环和查询的代码:
var err error
var errorid int
var query string
//循环100万次
for i := 0; i < 1000000; i++ {
//在范围内获取随机数
ran := rand.Intn(endrecord-startrecord) + startrecord
query = fmt.Sprintf("select ErrorID from logErrors " +
"where ErrorLogID=%v ",ran)
if err = db.QueryRow( query ).Scan(&errorid); err != nil {
fmt.Printf( "Error: %v - %v\n", err, query )
} else {
//fmt.Printf( "errorid %v\n", errorid )
}
}
在您的测试中,Go代码性能较差的主要原因是代码实现不完整且存在设计问题。以下是专业分析和优化后的代码示例:
问题分析:
- Go代码中没有实际扫描和读取查询结果,导致数据库连接和查询未被充分利用
- 缺少连接池配置和预处理语句
- 字符串拼接查询存在SQL注入风险且效率较低
优化后的Go代码示例:
package main
import (
"database/sql"
"fmt"
"log"
"math/rand"
"time"
_ "github.com/go-sql-driver/mysql"
)
type LogError struct {
ErrorLogID int
ErrorID int
}
func main() {
startrecord := 9086240
endrecord := 9644302
DBConnect := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8&parseTime=true",
dbUser, dbPass, dbHost, dbPort, dbName)
db, err := sql.Open("mysql", DBConnect)
if err != nil {
log.Fatal(err)
}
defer db.Close()
// 配置连接池
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(25)
db.SetConnMaxLifetime(5 * time.Minute)
// 使用预处理语句
stmt, err := db.Prepare("SELECT ErrorLogID, ErrorID FROM logErrors WHERE ErrorLogID = ?")
if err != nil {
log.Fatal(err)
}
defer stmt.Close()
rand.Seed(time.Now().UnixNano())
start := time.Now()
for i := 0; i < 1000000; i++ {
ran := rand.Intn(endrecord-startrecord) + startrecord
var logErr LogError
err := stmt.QueryRow(ran).Scan(&logErr.ErrorLogID, &logErr.ErrorID)
if err != nil && err != sql.ErrNoRows {
fmt.Printf("Error: %v\n", err)
continue
}
// 实际使用扫描到的数据
_ = logErr.ErrorLogID
_ = logErr.ErrorID
}
elapsed := time.Since(start)
fmt.Printf("Go execution time: %v\n", elapsed)
}
关键优化点:
- 使用预处理语句:避免每次循环都重新解析SQL
- 配置连接池:充分利用数据库连接
- 实际扫描数据:
QueryRow().Scan()确保查询完整执行 - 结构体映射:使用结构体组织返回数据
- 错误处理:完整的错误检查和处理
性能对比说明: 在同等条件下,优化后的Go代码通常能比PHP7快2-3倍。Go的并发特性在处理高并发数据库查询时优势更明显:
// 并发版本示例
func concurrentQuery(db *sql.DB, startrecord, endrecord int) {
var wg sync.WaitGroup
workerCount := 10
for w := 0; w < workerCount; w++ {
wg.Add(1)
go func(workerID int) {
defer wg.Done()
stmt, _ := db.Prepare("SELECT ErrorLogID, ErrorID FROM logErrors WHERE ErrorLogID = ?")
defer stmt.Close()
for i := 0; i < 1000000/workerCount; i++ {
ran := rand.Intn(endrecord-startrecord) + startrecord
var logErr LogError
stmt.QueryRow(ran).Scan(&logErr.ErrorLogID, &logErr.ErrorID)
}
}(w)
}
wg.Wait()
}
Go在数据库操作方面需要更多代码,但通过适当的优化和并发处理,可以显著超越PHP7的性能表现。

